summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.eslintignore4
-rw-r--r--.eslintrc5
-rw-r--r--.flayignore1
-rw-r--r--.gitlab-ci.yml37
-rw-r--r--.haml-lint.yml24
-rw-r--r--.rubocop.yml5
-rw-r--r--.rubocop_todo.yml238
-rw-r--r--CHANGELOG.md453
-rw-r--r--CONTRIBUTING.md74
-rw-r--r--GITLAB_PAGES_VERSION1
-rw-r--r--GITLAB_WORKHORSE_VERSION2
-rw-r--r--Gemfile71
-rw-r--r--Gemfile.lock162
-rw-r--r--PROCESS.md123
-rw-r--r--README.md10
-rw-r--r--VERSION2
-rw-r--r--app/assets/images/auth_buttons/authentiq_64.pngbin0 -> 17679 bytes
-rw-r--r--app/assets/javascripts/admin.js9
-rw-r--r--app/assets/javascripts/api.js10
-rw-r--r--app/assets/javascripts/application.js129
-rw-r--r--app/assets/javascripts/aside.js4
-rw-r--r--app/assets/javascripts/autosave.js4
-rw-r--r--app/assets/javascripts/awards_handler.js18
-rw-r--r--app/assets/javascripts/behaviors/autosize.js6
-rw-r--r--app/assets/javascripts/behaviors/details_behavior.js3
-rw-r--r--app/assets/javascripts/behaviors/quick_submit.js5
-rw-r--r--app/assets/javascripts/behaviors/requires_input.js5
-rw-r--r--app/assets/javascripts/behaviors/toggler_behavior.js26
-rw-r--r--app/assets/javascripts/blob/blob_ci_yaml.js.es67
-rw-r--r--app/assets/javascripts/blob/blob_dockerfile_selector.js.es63
-rw-r--r--app/assets/javascripts/blob/blob_file_dropzone.js4
-rw-r--r--app/assets/javascripts/blob/blob_gitignore_selector.js6
-rw-r--r--app/assets/javascripts/blob/blob_gitignore_selectors.js4
-rw-r--r--app/assets/javascripts/blob/blob_license_selector.js6
-rw-r--r--app/assets/javascripts/blob/blob_license_selectors.js.es63
-rw-r--r--app/assets/javascripts/blob/template_selector.js.es6164
-rw-r--r--app/assets/javascripts/blob_edit/blob_edit_bundle.js5
-rw-r--r--app/assets/javascripts/blob_edit/edit_blob.js6
-rw-r--r--app/assets/javascripts/boards/boards_bundle.js.es668
-rw-r--r--app/assets/javascripts/boards/components/board.js.es641
-rw-r--r--app/assets/javascripts/boards/components/board_blank_state.js.es64
-rw-r--r--app/assets/javascripts/boards/components/board_card.js.es634
-rw-r--r--app/assets/javascripts/boards/components/board_list.js.es69
-rw-r--r--app/assets/javascripts/boards/components/board_new_issue.js.es61
-rw-r--r--app/assets/javascripts/boards/components/board_sidebar.js.es617
-rw-r--r--app/assets/javascripts/boards/components/issue_card_inner.js.es6111
-rw-r--r--app/assets/javascripts/boards/components/modal/empty_state.js.es670
-rw-r--r--app/assets/javascripts/boards/components/modal/filters.js.es649
-rw-r--r--app/assets/javascripts/boards/components/modal/filters/label.js.es654
-rw-r--r--app/assets/javascripts/boards/components/modal/filters/milestone.js.es655
-rw-r--r--app/assets/javascripts/boards/components/modal/filters/user.js.es696
-rw-r--r--app/assets/javascripts/boards/components/modal/footer.js.es683
-rw-r--r--app/assets/javascripts/boards/components/modal/header.js.es690
-rw-r--r--app/assets/javascripts/boards/components/modal/index.js.es6163
-rw-r--r--app/assets/javascripts/boards/components/modal/list.js.es6159
-rw-r--r--app/assets/javascripts/boards/components/modal/lists_dropdown.js.es656
-rw-r--r--app/assets/javascripts/boards/components/modal/tabs.js.es647
-rw-r--r--app/assets/javascripts/boards/components/new_list_dropdown.js.es624
-rw-r--r--app/assets/javascripts/boards/components/sidebar/remove_issue.js.es659
-rw-r--r--app/assets/javascripts/boards/mixins/modal_mixins.js.es614
-rw-r--r--app/assets/javascripts/boards/mixins/sortable_default_options.js.es67
-rw-r--r--app/assets/javascripts/boards/models/issue.js.es615
-rw-r--r--app/assets/javascripts/boards/models/label.js.es62
-rw-r--r--app/assets/javascripts/boards/models/list.js.es620
-rw-r--r--app/assets/javascripts/boards/models/milestone.js.es62
-rw-r--r--app/assets/javascripts/boards/models/user.js.es62
-rw-r--r--app/assets/javascripts/boards/services/board_service.js.es637
-rw-r--r--app/assets/javascripts/boards/stores/boards_store.js.es625
-rw-r--r--app/assets/javascripts/boards/stores/modal_store.js.es6107
-rw-r--r--app/assets/javascripts/boards/test_utils/simulate_drag.js231
-rw-r--r--app/assets/javascripts/boards/vue_resource_interceptor.js.es610
-rw-r--r--app/assets/javascripts/breakpoints.js5
-rw-r--r--app/assets/javascripts/broadcast_message.js3
-rw-r--r--app/assets/javascripts/build.js149
-rw-r--r--app/assets/javascripts/build_artifacts.js4
-rw-r--r--app/assets/javascripts/build_variables.js.es66
-rw-r--r--app/assets/javascripts/ci_lint_editor.js.es618
-rw-r--r--app/assets/javascripts/commit.js4
-rw-r--r--app/assets/javascripts/commit/file.js4
-rw-r--r--app/assets/javascripts/commit/image_file.js4
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_bundle.js.es626
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_service.js.es629
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_store.js.es650
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_table.js.es6107
-rw-r--r--app/assets/javascripts/commits.js12
-rw-r--r--app/assets/javascripts/compare.js4
-rw-r--r--app/assets/javascripts/compare_autocomplete.js.es612
-rw-r--r--app/assets/javascripts/confirm_danger_modal.js4
-rw-r--r--app/assets/javascripts/copy_as_gfm.js.es6355
-rw-r--r--app/assets/javascripts/copy_to_clipboard.js5
-rw-r--r--app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es69
-rw-r--r--app/assets/javascripts/diff.js.es636
-rw-r--r--app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es64
-rw-r--r--app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es626
-rw-r--r--app/assets/javascripts/diff_notes/components/resolve_btn.js.es610
-rw-r--r--app/assets/javascripts/diff_notes/diff_notes_bundle.js.es615
-rw-r--r--app/assets/javascripts/diff_notes/mixins/discussion.js.es66
-rw-r--r--app/assets/javascripts/diff_notes/models/discussion.js.es62
-rw-r--r--app/assets/javascripts/diff_notes/models/note.js.es62
-rw-r--r--app/assets/javascripts/diff_notes/services/resolve.js.es68
-rw-r--r--app/assets/javascripts/diff_notes/stores/comments.js.es64
-rw-r--r--app/assets/javascripts/dispatcher.js.es674
-rw-r--r--app/assets/javascripts/droplab/droplab.js741
-rw-r--r--app/assets/javascripts/droplab/droplab_ajax.js96
-rw-r--r--app/assets/javascripts/droplab/droplab_ajax_filter.js161
-rw-r--r--app/assets/javascripts/droplab/droplab_filter.js74
-rw-r--r--app/assets/javascripts/dropzone_input.js8
-rw-r--r--app/assets/javascripts/due_date_select.js.es613
-rw-r--r--app/assets/javascripts/environments/components/environment.js.es672
-rw-r--r--app/assets/javascripts/environments/components/environment_actions.js.es65
-rw-r--r--app/assets/javascripts/environments/components/environment_external_url.js.es65
-rw-r--r--app/assets/javascripts/environments/components/environment_item.js.es639
-rw-r--r--app/assets/javascripts/environments/components/environment_rollback.js.es65
-rw-r--r--app/assets/javascripts/environments/components/environment_stop.js.es65
-rw-r--r--app/assets/javascripts/environments/components/environment_terminal_button.js.es65
-rw-r--r--app/assets/javascripts/environments/environments_bundle.js.es619
-rw-r--r--app/assets/javascripts/environments/services/environments_service.js.es63
-rw-r--r--app/assets/javascripts/environments/stores/environments_store.js.es663
-rw-r--r--app/assets/javascripts/environments/vue_resource_interceptor.js.es612
-rw-r--r--app/assets/javascripts/extensions/array.js.es69
-rw-r--r--app/assets/javascripts/extensions/custom_event.js.es612
-rw-r--r--app/assets/javascripts/extensions/element.js.es66
-rw-r--r--app/assets/javascripts/extensions/jquery.js3
-rw-r--r--app/assets/javascripts/files_comment_button.js6
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_hint.js.es669
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_non_user.js.es644
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_user.js.es660
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_utils.js.es6126
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_bundle.js3
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6112
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6207
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_manager.js.es6230
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_token_keys.js.es696
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_tokenizer.js.es645
-rw-r--r--app/assets/javascripts/flash.js4
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js.es652
-rw-r--r--app/assets/javascripts/gl_dropdown.js56
-rw-r--r--app/assets/javascripts/gl_field_errors.js.es65
-rw-r--r--app/assets/javascripts/gl_form.js64
-rw-r--r--app/assets/javascripts/gl_form.js.es692
-rw-r--r--app/assets/javascripts/graphs/graphs_bundle.js15
-rw-r--r--app/assets/javascripts/graphs/stat_graph.js4
-rw-r--r--app/assets/javascripts/graphs/stat_graph_contributors.js6
-rw-r--r--app/assets/javascripts/graphs/stat_graph_contributors_graph.js10
-rw-r--r--app/assets/javascripts/graphs/stat_graph_contributors_util.js5
-rw-r--r--app/assets/javascripts/group_avatar.js8
-rw-r--r--app/assets/javascripts/group_label_subscription.js.es63
-rw-r--r--app/assets/javascripts/groups_select.js8
-rw-r--r--app/assets/javascripts/header.js4
-rw-r--r--app/assets/javascripts/importer_status.js3
-rw-r--r--app/assets/javascripts/issuable.js.es623
-rw-r--r--app/assets/javascripts/issuable/issuable_bundle.js.es61
-rw-r--r--app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js.es641
-rw-r--r--app/assets/javascripts/issuable/time_tracking/components/comparison_pane.js.es669
-rw-r--r--app/assets/javascripts/issuable/time_tracking/components/estimate_only_pane.js.es613
-rw-r--r--app/assets/javascripts/issuable/time_tracking/components/help_state.js.es624
-rw-r--r--app/assets/javascripts/issuable/time_tracking/components/no_tracking_pane.js.es611
-rw-r--r--app/assets/javascripts/issuable/time_tracking/components/spent_only_pane.js.es613
-rw-r--r--app/assets/javascripts/issuable/time_tracking/components/time_tracker.js.es6119
-rw-r--r--app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js.es662
-rw-r--r--app/assets/javascripts/issuable_context.js13
-rw-r--r--app/assets/javascripts/issuable_form.js8
-rw-r--r--app/assets/javascripts/issue.js15
-rw-r--r--app/assets/javascripts/issue_status_select.js4
-rw-r--r--app/assets/javascripts/issues_bulk_assignment.js.es66
-rw-r--r--app/assets/javascripts/label_manager.js.es612
-rw-r--r--app/assets/javascripts/labels.js6
-rw-r--r--app/assets/javascripts/labels_select.js47
-rw-r--r--app/assets/javascripts/layout_nav.js12
-rw-r--r--app/assets/javascripts/lib/ace.js3
-rw-r--r--app/assets/javascripts/lib/ace/ace_config_paths.js.erb34
-rw-r--r--app/assets/javascripts/lib/chart.js6
-rw-r--r--app/assets/javascripts/lib/d3.js6
-rw-r--r--app/assets/javascripts/lib/utils/animate.js3
-rw-r--r--app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js.es61
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js119
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js.es6256
-rw-r--r--app/assets/javascripts/lib/utils/custom_event_polyfill.js.es612
-rw-r--r--app/assets/javascripts/lib/utils/datetime_utility.js103
-rw-r--r--app/assets/javascripts/lib/utils/datetime_utility.js.es6126
-rw-r--r--app/assets/javascripts/lib/utils/emoji_aliases.js.erb6
-rw-r--r--app/assets/javascripts/lib/utils/notify.js3
-rw-r--r--app/assets/javascripts/lib/utils/pretty_time.js.es626
-rw-r--r--app/assets/javascripts/lib/utils/text_utility.js62
-rw-r--r--app/assets/javascripts/lib/utils/type_utility.js3
-rw-r--r--app/assets/javascripts/lib/utils/url_utility.js81
-rw-r--r--app/assets/javascripts/lib/utils/url_utility.js.es686
-rw-r--r--app/assets/javascripts/lib/vue_resource.js.es62
-rw-r--r--app/assets/javascripts/line_highlighter.js22
-rw-r--r--app/assets/javascripts/logo.js12
-rw-r--r--app/assets/javascripts/member_expiration_date.js37
-rw-r--r--app/assets/javascripts/member_expiration_date.js.es636
-rw-r--r--app/assets/javascripts/merge_conflicts/components/diff_file_editor.js.es610
-rw-r--r--app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js.es64
-rw-r--r--app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js.es64
-rw-r--r--app/assets/javascripts/merge_conflicts/merge_conflict_service.js.es65
-rw-r--r--app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es640
-rw-r--r--app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es628
-rw-r--r--app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js.es63
-rw-r--r--app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js.es63
-rw-r--r--app/assets/javascripts/merge_request.js20
-rw-r--r--app/assets/javascripts/merge_request_tabs.js.es681
-rw-r--r--app/assets/javascripts/merge_request_widget.js.es670
-rw-r--r--app/assets/javascripts/merge_request_widget/ci_bundle.js.es653
-rw-r--r--app/assets/javascripts/merged_buttons.js6
-rw-r--r--app/assets/javascripts/milestone.js4
-rw-r--r--app/assets/javascripts/milestone_select.js35
-rw-r--r--app/assets/javascripts/mini_pipeline_graph_dropdown.js.es623
-rw-r--r--app/assets/javascripts/namespace_select.js7
-rw-r--r--app/assets/javascripts/network/branch_graph.js38
-rw-r--r--app/assets/javascripts/network/network.js4
-rw-r--r--app/assets/javascripts/network/network_bundle.js13
-rw-r--r--app/assets/javascripts/new_branch_form.js8
-rw-r--r--app/assets/javascripts/new_commit_form.js6
-rw-r--r--app/assets/javascripts/notes.js211
-rw-r--r--app/assets/javascripts/notifications_dropdown.js4
-rw-r--r--app/assets/javascripts/notifications_form.js6
-rw-r--r--app/assets/javascripts/pipelines.js.es67
-rw-r--r--app/assets/javascripts/preview_markdown.js128
-rw-r--r--app/assets/javascripts/profile/gl_crop.js.es69
-rw-r--r--app/assets/javascripts/profile/profile.js.es614
-rw-r--r--app/assets/javascripts/profile/profile_bundle.js10
-rw-r--r--app/assets/javascripts/project.js44
-rw-r--r--app/assets/javascripts/project_avatar.js4
-rw-r--r--app/assets/javascripts/project_find_file.js10
-rw-r--r--app/assets/javascripts/project_fork.js4
-rw-r--r--app/assets/javascripts/project_import.js7
-rw-r--r--app/assets/javascripts/project_label_subscription.js.es67
-rw-r--r--app/assets/javascripts/project_new.js36
-rw-r--r--app/assets/javascripts/project_select.js7
-rw-r--r--app/assets/javascripts/project_show.js5
-rw-r--r--app/assets/javascripts/projects_list.js3
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js.es67
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_create.js.es68
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es64
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_edit.js.es68
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_edit_list.js.es69
-rw-r--r--app/assets/javascripts/protected_branches/protected_branches_bundle.js4
-rw-r--r--app/assets/javascripts/render_gfm.js5
-rw-r--r--app/assets/javascripts/render_math.js3
-rw-r--r--app/assets/javascripts/right_sidebar.js8
-rw-r--r--app/assets/javascripts/search.js16
-rw-r--r--app/assets/javascripts/search_autocomplete.js.es637
-rw-r--r--app/assets/javascripts/shortcuts.js11
-rw-r--r--app/assets/javascripts/shortcuts_blob.js31
-rw-r--r--app/assets/javascripts/shortcuts_blob.js.es629
-rw-r--r--app/assets/javascripts/shortcuts_dashboard_navigation.js6
-rw-r--r--app/assets/javascripts/shortcuts_find_file.js6
-rw-r--r--app/assets/javascripts/shortcuts_issuable.js68
-rw-r--r--app/assets/javascripts/shortcuts_navigation.js6
-rw-r--r--app/assets/javascripts/shortcuts_network.js6
-rw-r--r--app/assets/javascripts/sidebar.js.es628
-rw-r--r--app/assets/javascripts/single_file_diff.js26
-rw-r--r--app/assets/javascripts/smart_interval.js.es65
-rw-r--r--app/assets/javascripts/snippet/snippet_bundle.js9
-rw-r--r--app/assets/javascripts/snippets_list.js.es64
-rw-r--r--app/assets/javascripts/star.js4
-rw-r--r--app/assets/javascripts/subbable_resource.js.es63
-rw-r--r--app/assets/javascripts/subscription_select.js4
-rw-r--r--app/assets/javascripts/syntax_highlight.js4
-rw-r--r--app/assets/javascripts/templates/issuable_template_selector.js.es612
-rw-r--r--app/assets/javascripts/templates/issuable_template_selectors.js.es64
-rw-r--r--app/assets/javascripts/terminal/terminal_bundle.js.es68
-rw-r--r--app/assets/javascripts/todos.js.es634
-rw-r--r--app/assets/javascripts/tree.js10
-rw-r--r--app/assets/javascripts/u2f/authenticate.js112
-rw-r--r--app/assets/javascripts/u2f/authenticate.js.es6118
-rw-r--r--app/assets/javascripts/u2f/error.js23
-rw-r--r--app/assets/javascripts/u2f/register.js11
-rw-r--r--app/assets/javascripts/u2f/util.js4
-rw-r--r--app/assets/javascripts/user.js.es64
-rw-r--r--app/assets/javascripts/user_tabs.js.es67
-rw-r--r--app/assets/javascripts/users/calendar.js21
-rw-r--r--app/assets/javascripts/users/users_bundle.js10
-rw-r--r--app/assets/javascripts/users_select.js28
-rw-r--r--app/assets/javascripts/version_check_image.js.es610
-rw-r--r--app/assets/javascripts/visibility_select.js.es627
-rw-r--r--app/assets/javascripts/vue_common_component/commit.js.es6163
-rw-r--r--app/assets/javascripts/vue_pipelines_index/index.js.es636
-rw-r--r--app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6104
-rw-r--r--app/assets/javascripts/vue_pipelines_index/pipeline_url.js.es663
-rw-r--r--app/assets/javascripts/vue_pipelines_index/pipelines.js.es673
-rw-r--r--app/assets/javascripts/vue_pipelines_index/stage.js.es6103
-rw-r--r--app/assets/javascripts/vue_pipelines_index/status.js.es634
-rw-r--r--app/assets/javascripts/vue_pipelines_index/store.js.es668
-rw-r--r--app/assets/javascripts/vue_pipelines_index/time_ago.js.es676
-rw-r--r--app/assets/javascripts/vue_realtime_listener/index.js.es618
-rw-r--r--app/assets/javascripts/vue_shared/components/commit.js.es6163
-rw-r--r--app/assets/javascripts/vue_shared/components/pipelines_table.js.es661
-rw-r--r--app/assets/javascripts/vue_shared/components/pipelines_table_row.js.es6234
-rw-r--r--app/assets/javascripts/vue_shared/components/table_pagination.js.es6152
-rw-r--r--app/assets/javascripts/vue_shared/vue_resource_interceptor.js.es623
-rw-r--r--app/assets/javascripts/wikis.js.es66
-rw-r--r--app/assets/javascripts/zen_mode.js14
-rw-r--r--app/assets/stylesheets/framework.scss2
-rw-r--r--app/assets/stylesheets/framework/animations.scss97
-rw-r--r--app/assets/stylesheets/framework/avatar.scss6
-rw-r--r--app/assets/stylesheets/framework/awards.scss14
-rw-r--r--app/assets/stylesheets/framework/badges.scss6
-rw-r--r--app/assets/stylesheets/framework/blocks.scss38
-rw-r--r--app/assets/stylesheets/framework/buttons.scss12
-rw-r--r--app/assets/stylesheets/framework/calendar.scss1
-rw-r--r--app/assets/stylesheets/framework/common.scss22
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss22
-rw-r--r--app/assets/stylesheets/framework/files.scss49
-rw-r--r--app/assets/stylesheets/framework/filters.scss126
-rw-r--r--app/assets/stylesheets/framework/forms.scss2
-rw-r--r--app/assets/stylesheets/framework/gitlab-theme.scss35
-rw-r--r--app/assets/stylesheets/framework/header.scss19
-rw-r--r--app/assets/stylesheets/framework/icons.scss11
-rw-r--r--app/assets/stylesheets/framework/issue_box.scss2
-rw-r--r--app/assets/stylesheets/framework/layout.scss21
-rw-r--r--app/assets/stylesheets/framework/lists.scss64
-rw-r--r--app/assets/stylesheets/framework/markdown_area.scss5
-rw-r--r--app/assets/stylesheets/framework/mixins.scss2
-rw-r--r--app/assets/stylesheets/framework/mobile.scss8
-rw-r--r--app/assets/stylesheets/framework/nav.scss104
-rw-r--r--app/assets/stylesheets/framework/page-header.scss8
-rw-r--r--app/assets/stylesheets/framework/pagination.scss14
-rw-r--r--app/assets/stylesheets/framework/panels.scss8
-rw-r--r--app/assets/stylesheets/framework/progress.scss5
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss21
-rw-r--r--app/assets/stylesheets/framework/timeline.scss4
-rw-r--r--app/assets/stylesheets/framework/tw_bootstrap.scss10
-rw-r--r--app/assets/stylesheets/framework/tw_bootstrap_variables.scss9
-rw-r--r--app/assets/stylesheets/framework/typography.scss16
-rw-r--r--app/assets/stylesheets/framework/variables.scss60
-rw-r--r--app/assets/stylesheets/framework/wells.scss2
-rw-r--r--app/assets/stylesheets/framework/zen.scss2
-rw-r--r--app/assets/stylesheets/highlight/dark.scss13
-rw-r--r--app/assets/stylesheets/highlight/monokai.scss12
-rw-r--r--app/assets/stylesheets/highlight/solarized_dark.scss12
-rw-r--r--app/assets/stylesheets/highlight/solarized_light.scss15
-rw-r--r--app/assets/stylesheets/highlight/white.scss18
-rw-r--r--app/assets/stylesheets/notify.scss12
-rw-r--r--app/assets/stylesheets/pages/boards.scss196
-rw-r--r--app/assets/stylesheets/pages/branches.scss55
-rw-r--r--app/assets/stylesheets/pages/builds.scss154
-rw-r--r--app/assets/stylesheets/pages/ci_projects.scss2
-rw-r--r--app/assets/stylesheets/pages/commit.scss132
-rw-r--r--app/assets/stylesheets/pages/commits.scss188
-rw-r--r--app/assets/stylesheets/pages/cycle_analytics.scss33
-rw-r--r--app/assets/stylesheets/pages/deploy_keys.scss13
-rw-r--r--app/assets/stylesheets/pages/detail_page.scss8
-rw-r--r--app/assets/stylesheets/pages/diff.scss6
-rw-r--r--app/assets/stylesheets/pages/environments.scss19
-rw-r--r--app/assets/stylesheets/pages/events.scss2
-rw-r--r--app/assets/stylesheets/pages/groups.scss2
-rw-r--r--app/assets/stylesheets/pages/issuable.scss166
-rw-r--r--app/assets/stylesheets/pages/issues.scss4
-rw-r--r--app/assets/stylesheets/pages/labels.scss11
-rw-r--r--app/assets/stylesheets/pages/lint.scss10
-rw-r--r--app/assets/stylesheets/pages/login.scss13
-rw-r--r--app/assets/stylesheets/pages/members.scss6
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss112
-rw-r--r--app/assets/stylesheets/pages/milestone.scss12
-rw-r--r--app/assets/stylesheets/pages/note_form.scss22
-rw-r--r--app/assets/stylesheets/pages/notes.scss57
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss586
-rw-r--r--app/assets/stylesheets/pages/profile.scss15
-rw-r--r--app/assets/stylesheets/pages/profiles/preferences.scss4
-rw-r--r--app/assets/stylesheets/pages/projects.scss143
-rw-r--r--app/assets/stylesheets/pages/search.scss13
-rw-r--r--app/assets/stylesheets/pages/settings.scss2
-rw-r--r--app/assets/stylesheets/pages/status.scss27
-rw-r--r--app/assets/stylesheets/pages/todos.scss6
-rw-r--r--app/assets/stylesheets/pages/tree.scss29
-rw-r--r--app/assets/stylesheets/pages/wiki.scss4
-rw-r--r--app/controllers/admin/application_settings_controller.rb107
-rw-r--r--app/controllers/admin/deploy_keys_controller.rb4
-rw-r--r--app/controllers/admin/groups_controller.rb15
-rw-r--r--app/controllers/admin/projects_controller.rb4
-rw-r--r--app/controllers/admin/runner_projects_controller.rb2
-rw-r--r--app/controllers/admin/users_controller.rb44
-rw-r--r--app/controllers/autocomplete_controller.rb7
-rw-r--r--app/controllers/concerns/creates_commit.rb55
-rw-r--r--app/controllers/concerns/cycle_analytics_params.rb4
-rw-r--r--app/controllers/concerns/global_milestones.rb20
-rw-r--r--app/controllers/concerns/service_params.rb79
-rw-r--r--app/controllers/concerns/spammable_actions.rb20
-rw-r--r--app/controllers/confirmations_controller.rb8
-rw-r--r--app/controllers/dashboard/milestones_controller.rb13
-rw-r--r--app/controllers/dashboard/projects_controller.rb4
-rw-r--r--app/controllers/dashboard/todos_controller.rb3
-rw-r--r--app/controllers/dashboard_controller.rb5
-rw-r--r--app/controllers/explore/projects_controller.rb1
-rw-r--r--app/controllers/groups/milestones_controller.rb11
-rw-r--r--app/controllers/groups_controller.rb14
-rw-r--r--app/controllers/projects/application_controller.rb4
-rw-r--r--app/controllers/projects/blob_controller.rb2
-rw-r--r--app/controllers/projects/boards/issues_controller.rb6
-rw-r--r--app/controllers/projects/builds_controller.rb2
-rw-r--r--app/controllers/projects/commit_controller.rb50
-rw-r--r--app/controllers/projects/compare_controller.rb17
-rw-r--r--app/controllers/projects/cycle_analytics/events_controller.rb32
-rw-r--r--app/controllers/projects/cycle_analytics_controller.rb54
-rw-r--r--app/controllers/projects/deploy_keys_controller.rb4
-rw-r--r--app/controllers/projects/environments_controller.rb13
-rw-r--r--app/controllers/projects/git_http_client_controller.rb14
-rw-r--r--app/controllers/projects/git_http_controller.rb6
-rw-r--r--app/controllers/projects/group_links_controller.rb9
-rw-r--r--app/controllers/projects/hooks_controller.rb14
-rw-r--r--app/controllers/projects/issues_controller.rb37
-rw-r--r--app/controllers/projects/labels_controller.rb41
-rw-r--r--app/controllers/projects/lfs_api_controller.rb4
-rw-r--r--app/controllers/projects/mattermosts_controller.rb43
-rw-r--r--app/controllers/projects/merge_requests_controller.rb68
-rw-r--r--app/controllers/projects/notes_controller.rb3
-rw-r--r--app/controllers/projects/pages_controller.rb22
-rw-r--r--app/controllers/projects/pages_domains_controller.rb49
-rw-r--r--app/controllers/projects/pipelines_controller.rb30
-rw-r--r--app/controllers/projects/pipelines_settings_controller.rb11
-rw-r--r--app/controllers/projects/project_members_controller.rb52
-rw-r--r--app/controllers/projects/refs_controller.rb6
-rw-r--r--app/controllers/projects/runners_controller.rb8
-rw-r--r--app/controllers/projects/services_controller.rb4
-rw-r--r--app/controllers/projects/settings/ci_cd_controller.rb44
-rw-r--r--app/controllers/projects/settings/integrations_controller.rb18
-rw-r--r--app/controllers/projects/settings/members_controller.rb55
-rw-r--r--app/controllers/projects/snippets_controller.rb11
-rw-r--r--app/controllers/projects/tags_controller.rb2
-rw-r--r--app/controllers/projects/triggers_controller.rb10
-rw-r--r--app/controllers/projects/uploads_controller.rb2
-rw-r--r--app/controllers/projects/variables_controller.rb9
-rw-r--r--app/controllers/projects_controller.rb19
-rw-r--r--app/controllers/registrations_controller.rb20
-rw-r--r--app/controllers/root_controller.rb3
-rw-r--r--app/controllers/search_controller.rb14
-rw-r--r--app/controllers/snippets_controller.rb6
-rw-r--r--app/finders/environments_finder.rb55
-rw-r--r--app/finders/group_projects_finder.rb2
-rw-r--r--app/finders/issuable_finder.rb54
-rw-r--r--app/finders/members_finder.rb13
-rw-r--r--app/helpers/application_helper.rb4
-rw-r--r--app/helpers/auth_helper.rb2
-rw-r--r--app/helpers/blob_helper.rb6
-rw-r--r--app/helpers/boards_helper.rb4
-rw-r--r--app/helpers/ci_status_helper.rb2
-rw-r--r--app/helpers/commits_helper.rb80
-rw-r--r--app/helpers/compare_helper.rb2
-rw-r--r--app/helpers/diff_helper.rb6
-rw-r--r--app/helpers/gitlab_markdown_helper.rb44
-rw-r--r--app/helpers/gitlab_routing_helper.rb13
-rw-r--r--app/helpers/groups_helper.rb2
-rw-r--r--app/helpers/issuables_helper.rb17
-rw-r--r--app/helpers/issues_helper.rb12
-rw-r--r--app/helpers/javascript_helper.rb5
-rw-r--r--app/helpers/mattermost_helper.rb9
-rw-r--r--app/helpers/merge_requests_helper.rb24
-rw-r--r--app/helpers/projects_helper.rb97
-rw-r--r--app/helpers/search_helper.rb8
-rw-r--r--app/helpers/services_helper.rb20
-rw-r--r--app/helpers/sorting_helper.rb11
-rw-r--r--app/helpers/storage_helper.rb7
-rw-r--r--app/helpers/todos_helper.rb5
-rw-r--r--app/helpers/version_check_helper.rb3
-rw-r--r--app/helpers/visibility_level_helper.rb4
-rw-r--r--app/mailers/emails/notes.rb8
-rw-r--r--app/mailers/notify.rb20
-rw-r--r--app/models/ability.rb11
-rw-r--r--app/models/application_setting.rb80
-rw-r--r--app/models/board.rb4
-rw-r--r--app/models/ci/build.rb80
-rw-r--r--app/models/ci/pipeline.rb44
-rw-r--r--app/models/ci/runner.rb33
-rw-r--r--app/models/ci/stage.rb15
-rw-r--r--app/models/commit.rb37
-rw-r--r--app/models/commit_range.rb4
-rw-r--r--app/models/commit_status.rb6
-rw-r--r--app/models/concerns/cache_markdown_field.rb7
-rw-r--r--app/models/concerns/has_status.rb1
-rw-r--r--app/models/concerns/issuable.rb6
-rw-r--r--app/models/concerns/mentionable.rb10
-rw-r--r--app/models/concerns/milestoneish.rb17
-rw-r--r--app/models/concerns/participable.rb7
-rw-r--r--app/models/concerns/presentable.rb7
-rw-r--r--app/models/concerns/project_features_compatibility.rb2
-rw-r--r--app/models/concerns/reactive_caching.rb46
-rw-r--r--app/models/concerns/reactive_service.rb10
-rw-r--r--app/models/concerns/referable.rb2
-rw-r--r--app/models/concerns/routable.rb15
-rw-r--r--app/models/concerns/spammable.rb14
-rw-r--r--app/models/concerns/taskable.rb8
-rw-r--r--app/models/concerns/time_trackable.rb72
-rw-r--r--app/models/concerns/valid_attribute.rb10
-rw-r--r--app/models/cycle_analytics.rb60
-rw-r--r--app/models/cycle_analytics/summary.rb43
-rw-r--r--app/models/dashboard_milestone.rb5
-rw-r--r--app/models/deploy_key.rb14
-rw-r--r--app/models/deployment.rb2
-rw-r--r--app/models/environment.rb57
-rw-r--r--app/models/external_issue.rb2
-rw-r--r--app/models/forked_project_link.rb4
-rw-r--r--app/models/generic_commit_status.rb10
-rw-r--r--app/models/global_milestone.rb19
-rw-r--r--app/models/group.rb33
-rw-r--r--app/models/group_label.rb4
-rw-r--r--app/models/group_milestone.rb19
-rw-r--r--app/models/issue.rb5
-rw-r--r--app/models/key.rb17
-rw-r--r--app/models/label.rb11
-rw-r--r--app/models/lfs_objects_project.rb9
-rw-r--r--app/models/list.rb2
-rw-r--r--app/models/member.rb20
-rw-r--r--app/models/merge_request.rb69
-rw-r--r--app/models/merge_request_diff.rb25
-rw-r--r--app/models/milestone.rb8
-rw-r--r--app/models/namespace.rb69
-rw-r--r--app/models/note.rb19
-rw-r--r--app/models/notification_setting.rb4
-rw-r--r--app/models/pages_domain.rb119
-rw-r--r--app/models/project.rb179
-rw-r--r--app/models/project_group_link.rb3
-rw-r--r--app/models/project_label.rb4
-rw-r--r--app/models/project_services/asana_service.rb4
-rw-r--r--app/models/project_services/assembla_service.rb4
-rw-r--r--app/models/project_services/bamboo_service.rb49
-rw-r--r--app/models/project_services/bugzilla_service.rb2
-rw-r--r--app/models/project_services/buildkite_service.rb29
-rw-r--r--app/models/project_services/builds_email_service.rb4
-rw-r--r--app/models/project_services/campfire_service.rb4
-rw-r--r--app/models/project_services/chat_notification_service.rb30
-rw-r--r--app/models/project_services/chat_slash_commands_service.rb20
-rw-r--r--app/models/project_services/ci_service.rb29
-rw-r--r--app/models/project_services/custom_issue_tracker_service.rb2
-rw-r--r--app/models/project_services/deployment_service.rb4
-rw-r--r--app/models/project_services/drone_ci_service.rb80
-rw-r--r--app/models/project_services/emails_on_push_service.rb4
-rw-r--r--app/models/project_services/external_wiki_service.rb6
-rw-r--r--app/models/project_services/flowdock_service.rb4
-rw-r--r--app/models/project_services/gemnasium_service.rb4
-rw-r--r--app/models/project_services/gitlab_issue_tracker_service.rb2
-rw-r--r--app/models/project_services/hipchat_service.rb4
-rw-r--r--app/models/project_services/irker_service.rb4
-rw-r--r--app/models/project_services/issue_tracker_service.rb2
-rw-r--r--app/models/project_services/jira_service.rb8
-rw-r--r--app/models/project_services/kubernetes_service.rb13
-rw-r--r--app/models/project_services/mattermost_notification_service.rb41
-rw-r--r--app/models/project_services/mattermost_service.rb41
-rw-r--r--app/models/project_services/mattermost_slash_commands_service.rb36
-rw-r--r--app/models/project_services/pipelines_email_service.rb4
-rw-r--r--app/models/project_services/pivotaltracker_service.rb4
-rw-r--r--app/models/project_services/pushover_service.rb4
-rw-r--r--app/models/project_services/redmine_service.rb2
-rw-r--r--app/models/project_services/slack_notification_service.rb40
-rw-r--r--app/models/project_services/slack_service.rb40
-rw-r--r--app/models/project_services/slack_slash_commands_service.rb8
-rw-r--r--app/models/project_services/teamcity_service.rb79
-rw-r--r--app/models/project_snippet.rb4
-rw-r--r--app/models/project_statistics.rb44
-rw-r--r--app/models/repository.rb543
-rw-r--r--app/models/route.rb7
-rw-r--r--app/models/service.rb19
-rw-r--r--app/models/snippet.rb18
-rw-r--r--app/models/timelog.rb22
-rw-r--r--app/models/todo.rb12
-rw-r--r--app/models/user.rb40
-rw-r--r--app/policies/base_policy.rb4
-rw-r--r--app/policies/ci/build_policy.rb2
-rw-r--r--app/policies/group_policy.rb2
-rw-r--r--app/policies/project_policy.rb66
-rw-r--r--app/policies/project_snippet_policy.rb2
-rw-r--r--app/presenters/README.md154
-rw-r--r--app/presenters/ci/build_presenter.rb15
-rw-r--r--app/serializers/analytics_stage_entity.rb10
-rw-r--r--app/serializers/analytics_stage_serializer.rb3
-rw-r--r--app/serializers/analytics_summary_entity.rb7
-rw-r--r--app/serializers/analytics_summary_serializer.rb3
-rw-r--r--app/serializers/base_serializer.rb1
-rw-r--r--app/serializers/build_action_entity.rb14
-rw-r--r--app/serializers/build_artifact_entity.rb14
-rw-r--r--app/serializers/commit_entity.rb12
-rw-r--r--app/serializers/environment_entity.rb2
-rw-r--r--app/serializers/environment_serializer.rb47
-rw-r--r--app/serializers/issuable_entity.rb4
-rw-r--r--app/serializers/pipeline_entity.rb85
-rw-r--r--app/serializers/pipeline_serializer.rb25
-rw-r--r--app/serializers/request_aware_entity.rb9
-rw-r--r--app/serializers/stage_entity.rb38
-rw-r--r--app/serializers/status_entity.rb8
-rw-r--r--app/services/application_settings/base_service.rb7
-rw-r--r--app/services/application_settings/update_service.rb7
-rw-r--r--app/services/auth/container_registry_authentication_service.rb2
-rw-r--r--app/services/boards/create_service.rb1
-rw-r--r--app/services/boards/issues/list_service.rb14
-rw-r--r--app/services/ci/register_build_service.rb62
-rw-r--r--app/services/ci/stop_environments_service.rb7
-rw-r--r--app/services/ci/update_build_queue_service.rb19
-rw-r--r--app/services/commits/change_service.rb34
-rw-r--r--app/services/compare_service.rb26
-rw-r--r--app/services/create_branch_service.rb30
-rw-r--r--app/services/create_snippet_service.rb9
-rw-r--r--app/services/delete_tag_service.rb2
-rw-r--r--app/services/files/base_service.rb25
-rw-r--r--app/services/files/create_dir_service.rb10
-rw-r--r--app/services/files/create_service.rb14
-rw-r--r--app/services/files/delete_service.rb10
-rw-r--r--app/services/files/multi_service.rb8
-rw-r--r--app/services/files/update_service.rb10
-rw-r--r--app/services/git_hooks_service.rb6
-rw-r--r--app/services/git_operation_service.rb179
-rw-r--r--app/services/git_push_service.rb26
-rw-r--r--app/services/git_tag_push_service.rb2
-rw-r--r--app/services/groups/create_service.rb7
-rw-r--r--app/services/groups/update_service.rb8
-rw-r--r--app/services/issuable_base_service.rb55
-rw-r--r--app/services/issues/base_service.rb4
-rw-r--r--app/services/issues/create_service.rb12
-rw-r--r--app/services/labels/promote_service.rb71
-rw-r--r--app/services/merge_requests/base_service.rb20
-rw-r--r--app/services/merge_requests/build_service.rb132
-rw-r--r--app/services/merge_requests/merge_service.rb14
-rw-r--r--app/services/merge_requests/refresh_service.rb41
-rw-r--r--app/services/merge_requests/update_service.rb15
-rw-r--r--app/services/notes/create_service.rb14
-rw-r--r--app/services/notes/post_process_service.rb3
-rw-r--r--app/services/notes/slash_commands_service.rb6
-rw-r--r--app/services/notification_service.rb35
-rw-r--r--app/services/pages_service.rb15
-rw-r--r--app/services/projects/create_service.rb26
-rw-r--r--app/services/projects/import_export/export_service.rb2
-rw-r--r--app/services/projects/participants_service.rb2
-rw-r--r--app/services/projects/transfer_service.rb20
-rw-r--r--app/services/projects/update_pages_configuration_service.rb69
-rw-r--r--app/services/projects/update_pages_service.rb164
-rw-r--r--app/services/projects/update_service.rb8
-rw-r--r--app/services/search/global_service.rb5
-rw-r--r--app/services/slash_commands/interpret_service.rb72
-rw-r--r--app/services/spam_service.rb3
-rw-r--r--app/services/system_note_service.rb66
-rw-r--r--app/services/todo_service.rb30
-rw-r--r--app/services/user_project_access_changed_service.rb2
-rw-r--r--app/services/users/refresh_authorized_projects_service.rb50
-rw-r--r--app/services/validate_new_branch_service.rb22
-rw-r--r--app/uploaders/avatar_uploader.rb11
-rw-r--r--app/validators/certificate_key_validator.rb25
-rw-r--r--app/validators/certificate_validator.rb24
-rw-r--r--app/validators/project_path_validator.rb3
-rw-r--r--app/views/abuse_report_mailer/notify.html.haml6
-rw-r--r--app/views/admin/application_settings/_form.html.haml41
-rw-r--r--app/views/admin/applications/_delete_form.html.haml2
-rw-r--r--app/views/admin/applications/index.html.haml2
-rw-r--r--app/views/admin/background_jobs/show.html.haml2
-rw-r--r--app/views/admin/broadcast_messages/_form.html.haml2
-rw-r--r--app/views/admin/broadcast_messages/index.html.haml2
-rw-r--r--app/views/admin/builds/index.html.haml2
-rw-r--r--app/views/admin/dashboard/_head.html.haml4
-rw-r--r--app/views/admin/deploy_keys/index.html.haml57
-rw-r--r--app/views/admin/deploy_keys/new.html.haml8
-rw-r--r--app/views/admin/groups/_group.html.haml5
-rw-r--r--app/views/admin/groups/index.html.haml2
-rw-r--r--app/views/admin/groups/show.html.haml22
-rw-r--r--app/views/admin/hooks/index.html.haml5
-rw-r--r--app/views/admin/identities/_identity.html.haml2
-rw-r--r--app/views/admin/labels/_label.html.haml2
-rw-r--r--app/views/admin/projects/index.html.haml17
-rw-r--r--app/views/admin/projects/show.html.haml13
-rw-r--r--app/views/admin/runners/_runner.html.haml2
-rw-r--r--app/views/admin/runners/index.html.haml12
-rw-r--r--app/views/admin/runners/show.html.haml10
-rw-r--r--app/views/admin/spam_logs/_spam_log.html.haml2
-rw-r--r--app/views/admin/spam_logs/index.html.haml1
-rw-r--r--app/views/admin/system_info/show.html.haml10
-rw-r--r--app/views/admin/users/_access_levels.html.haml37
-rw-r--r--app/views/admin/users/_form.html.haml23
-rw-r--r--app/views/admin/users/_head.html.haml4
-rw-r--r--app/views/admin/users/_user.html.haml2
-rw-r--r--app/views/admin/users/groups.html.haml20
-rw-r--r--app/views/admin/users/projects.html.haml16
-rw-r--r--app/views/admin/users/show.html.haml4
-rw-r--r--app/views/award_emoji/_awards_block.html.haml3
-rw-r--r--app/views/ci/lints/_create.html.haml2
-rw-r--r--app/views/ci/lints/show.html.haml31
-rw-r--r--app/views/ci/status/_badge.html.haml3
-rw-r--r--app/views/ci/status/_dropdown_graph_badge.html.haml19
-rw-r--r--app/views/ci/status/_graph_badge.html.haml6
-rw-r--r--app/views/dashboard/issues.html.haml4
-rw-r--r--app/views/dashboard/merge_requests.html.haml4
-rw-r--r--app/views/dashboard/projects/_zero_authorized_projects.html.haml2
-rw-r--r--app/views/dashboard/todos/_todo.html.haml9
-rw-r--r--app/views/dashboard/todos/index.html.haml22
-rw-r--r--app/views/devise/sessions/_new_base.html.haml8
-rw-r--r--app/views/devise/sessions/_new_crowd.html.haml4
-rw-r--r--app/views/devise/sessions/_new_ldap.html.haml4
-rw-r--r--app/views/devise/sessions/two_factor.html.haml2
-rw-r--r--app/views/devise/shared/_omniauth_box.html.haml4
-rw-r--r--app/views/devise/shared/_signin_box.html.haml8
-rw-r--r--app/views/devise/shared/_signup_box.html.haml13
-rw-r--r--app/views/devise/shared/_tabs_ldap.html.haml2
-rw-r--r--app/views/devise/shared/_tabs_normal.html.haml8
-rw-r--r--app/views/discussions/_diff_discussion.html.haml2
-rw-r--r--app/views/discussions/_discussion.html.haml3
-rw-r--r--app/views/discussions/_jump_to_next.html.haml2
-rw-r--r--app/views/discussions/_parallel_diff_discussion.html.haml10
-rw-r--r--app/views/doorkeeper/applications/_delete_form.html.haml2
-rw-r--r--app/views/doorkeeper/applications/index.html.haml6
-rw-r--r--app/views/doorkeeper/authorizations/error.html.haml2
-rw-r--r--app/views/doorkeeper/authorizations/new.html.haml4
-rw-r--r--app/views/doorkeeper/authorizations/show.html.haml2
-rw-r--r--app/views/doorkeeper/authorized_applications/_delete_form.html.haml4
-rw-r--r--app/views/doorkeeper/authorized_applications/index.html.haml2
-rw-r--r--app/views/emojis/index.html.haml2
-rw-r--r--app/views/errors/access_denied.html.haml3
-rw-r--r--app/views/errors/encoding.html.haml3
-rw-r--r--app/views/errors/git_not_found.html.haml3
-rw-r--r--app/views/errors/not_found.html.haml3
-rw-r--r--app/views/errors/omniauth_error.html.haml3
-rw-r--r--app/views/events/_event_issue.atom.haml2
-rw-r--r--app/views/events/_event_merge_request.atom.haml2
-rw-r--r--app/views/events/_event_note.atom.haml2
-rw-r--r--app/views/events/_event_push.atom.haml2
-rw-r--r--app/views/events/event/_common.html.haml2
-rw-r--r--app/views/events/event/_created_project.html.haml2
-rw-r--r--app/views/explore/groups/index.html.haml2
-rw-r--r--app/views/explore/projects/_filter.html.haml4
-rw-r--r--app/views/groups/_group_lfs_settings.html.haml2
-rw-r--r--app/views/groups/group_members/_new_group_member.html.haml4
-rw-r--r--app/views/groups/group_members/index.html.haml8
-rw-r--r--app/views/groups/issues.html.haml5
-rw-r--r--app/views/groups/merge_requests.html.haml5
-rw-r--r--app/views/groups/milestones/index.html.haml2
-rw-r--r--app/views/groups/new.html.haml2
-rw-r--r--app/views/groups/projects.html.haml4
-rw-r--r--app/views/groups/show.html.haml11
-rw-r--r--app/views/help/_shortcuts.html.haml22
-rw-r--r--app/views/help/ui.html.haml124
-rw-r--r--app/views/import/_githubish_status.html.haml6
-rw-r--r--app/views/import/bitbucket/status.html.haml8
-rw-r--r--app/views/import/fogbugz/new_user_map.html.haml2
-rw-r--r--app/views/import/fogbugz/status.html.haml6
-rw-r--r--app/views/import/gitlab/status.html.haml4
-rw-r--r--app/views/import/google_code/new.html.haml2
-rw-r--r--app/views/import/google_code/status.html.haml8
-rw-r--r--app/views/kaminari/gitlab/_next_page.html.haml4
-rw-r--r--app/views/kaminari/gitlab/_page.html.haml4
-rw-r--r--app/views/kaminari/gitlab/_paginator.html.haml3
-rw-r--r--app/views/kaminari/gitlab/_prev_page.html.haml4
-rw-r--r--app/views/layouts/_head.html.haml44
-rw-r--r--app/views/layouts/_search.html.haml4
-rw-r--r--app/views/layouts/application.html.haml5
-rw-r--r--app/views/layouts/devise.html.haml4
-rw-r--r--app/views/layouts/devise_empty.html.haml2
-rw-r--r--app/views/layouts/devise_mailer.html.haml4
-rw-r--r--app/views/layouts/errors.html.haml4
-rw-r--r--app/views/layouts/header/_default.html.haml8
-rw-r--r--app/views/layouts/nav/_admin.html.haml2
-rw-r--r--app/views/layouts/nav/_admin_settings.html.haml2
-rw-r--r--app/views/layouts/nav/_dashboard.html.haml4
-rw-r--r--app/views/layouts/nav/_group_settings.html.haml2
-rw-r--r--app/views/layouts/nav/_project.html.haml6
-rw-r--r--app/views/layouts/nav/_project_settings.html.haml39
-rw-r--r--app/views/layouts/notify.html.haml8
-rw-r--r--app/views/layouts/profile.html.haml4
-rw-r--r--app/views/layouts/project.html.haml2
-rw-r--r--app/views/notify/_reassigned_issuable_email.html.haml4
-rw-r--r--app/views/notify/build_fail_email.html.haml6
-rw-r--r--app/views/notify/build_fail_email.text.erb2
-rw-r--r--app/views/notify/build_success_email.html.haml6
-rw-r--r--app/views/notify/build_success_email.text.erb2
-rw-r--r--app/views/notify/closed_issue_email.html.haml2
-rw-r--r--app/views/notify/closed_issue_email.text.haml2
-rw-r--r--app/views/notify/closed_merge_request_email.html.haml2
-rw-r--r--app/views/notify/closed_merge_request_email.text.haml2
-rw-r--r--app/views/notify/issue_status_changed_email.html.haml2
-rw-r--r--app/views/notify/links/ci/builds/_build.html.haml2
-rw-r--r--app/views/notify/links/ci/builds/_build.text.erb2
-rw-r--r--app/views/notify/links/generic_commit_statuses/_generic_commit_status.text.erb2
-rw-r--r--app/views/notify/merge_request_status_email.html.haml2
-rw-r--r--app/views/notify/merge_request_status_email.text.haml2
-rw-r--r--app/views/notify/merged_merge_request_email.html.haml2
-rw-r--r--app/views/notify/merged_merge_request_email.text.haml2
-rw-r--r--app/views/notify/new_issue_email.html.haml2
-rw-r--r--app/views/notify/new_mention_in_issue_email.html.haml2
-rw-r--r--app/views/notify/new_mention_in_merge_request_email.html.haml2
-rw-r--r--app/views/notify/new_merge_request_email.html.haml2
-rw-r--r--app/views/notify/note_personal_snippet_email.html.haml1
-rw-r--r--app/views/notify/note_personal_snippet_email.text.erb8
-rw-r--r--app/views/notify/pipeline_failed_email.html.haml138
-rw-r--r--app/views/notify/pipeline_success_email.html.haml116
-rw-r--r--app/views/notify/project_was_not_exported_email.text.haml6
-rw-r--r--app/views/notify/repository_push_email.html.haml8
-rw-r--r--app/views/profiles/_head.html.haml2
-rw-r--r--app/views/profiles/accounts/show.html.haml6
-rw-r--r--app/views/profiles/chat_names/new.html.haml2
-rw-r--r--app/views/profiles/keys/_key.html.haml3
-rw-r--r--app/views/profiles/keys/_key_details.html.haml3
-rw-r--r--app/views/profiles/keys/index.html.haml2
-rw-r--r--app/views/profiles/notifications/show.html.haml2
-rw-r--r--app/views/profiles/personal_access_tokens/index.html.haml2
-rw-r--r--app/views/profiles/preferences/show.html.haml4
-rw-r--r--app/views/profiles/show.html.haml15
-rw-r--r--app/views/projects/_activity.html.haml2
-rw-r--r--app/views/projects/_bitbucket_import_modal.html.haml6
-rw-r--r--app/views/projects/_customize_workflow.html.haml4
-rw-r--r--app/views/projects/_find_file_link.html.haml6
-rw-r--r--app/views/projects/_gitlab_import_modal.html.haml6
-rw-r--r--app/views/projects/_home_panel.html.haml2
-rw-r--r--app/views/projects/_last_commit.html.haml3
-rw-r--r--app/views/projects/_md_preview.html.haml22
-rw-r--r--app/views/projects/_merge_request_merge_settings.html.haml6
-rw-r--r--app/views/projects/_visibility_select.html.haml7
-rw-r--r--app/views/projects/_wiki.html.haml2
-rw-r--r--app/views/projects/artifacts/_tree_directory.html.haml2
-rw-r--r--app/views/projects/artifacts/browse.html.haml4
-rw-r--r--app/views/projects/blame/show.html.haml2
-rw-r--r--app/views/projects/blob/_actions.html.haml5
-rw-r--r--app/views/projects/blob/_blob.html.haml4
-rw-r--r--app/views/projects/blob/_editor.html.haml2
-rw-r--r--app/views/projects/blob/_image.html.haml8
-rw-r--r--app/views/projects/blob/_new_dir.html.haml2
-rw-r--r--app/views/projects/blob/_remove.html.haml2
-rw-r--r--app/views/projects/blob/_upload.html.haml6
-rw-r--r--app/views/projects/blob/diff.html.haml10
-rw-r--r--app/views/projects/blob/edit.html.haml2
-rw-r--r--app/views/projects/blob/new.html.haml2
-rw-r--r--app/views/projects/blob/preview.html.haml2
-rw-r--r--app/views/projects/blob/show.html.haml2
-rw-r--r--app/views/projects/boards/_show.html.haml12
-rw-r--r--app/views/projects/boards/components/_board.html.haml1
-rw-r--r--app/views/projects/boards/components/_board_list.html.haml1
-rw-r--r--app/views/projects/boards/components/_card.html.haml26
-rw-r--r--app/views/projects/boards/components/_sidebar.html.haml45
-rw-r--r--app/views/projects/boards/components/sidebar/_assignee.html.haml2
-rw-r--r--app/views/projects/branches/_branch.html.haml7
-rw-r--r--app/views/projects/branches/index.html.haml9
-rw-r--r--app/views/projects/branches/new.html.haml2
-rw-r--r--app/views/projects/builds/_header.html.haml10
-rw-r--r--app/views/projects/builds/_sidebar.html.haml20
-rw-r--r--app/views/projects/builds/_table.html.haml4
-rw-r--r--app/views/projects/builds/index.html.haml6
-rw-r--r--app/views/projects/builds/show.html.haml41
-rw-r--r--app/views/projects/buttons/_download.html.haml2
-rw-r--r--app/views/projects/buttons/_dropdown.html.haml2
-rw-r--r--app/views/projects/buttons/_fork.html.haml2
-rw-r--r--app/views/projects/buttons/_star.html.haml4
-rw-r--r--app/views/projects/ci/builds/_build.html.haml8
-rw-r--r--app/views/projects/ci/pipelines/_pipeline.html.haml38
-rw-r--r--app/views/projects/commit/_builds.html.haml2
-rw-r--r--app/views/projects/commit/_change.html.haml8
-rw-r--r--app/views/projects/commit/_ci_menu.html.haml6
-rw-r--r--app/views/projects/commit/_commit_box.html.haml12
-rw-r--r--app/views/projects/commit/_pipeline.html.haml4
-rw-r--r--app/views/projects/commit/_pipelines_list.haml40
-rw-r--r--app/views/projects/commit/builds.html.haml9
-rw-r--r--app/views/projects/commit/pipelines.html.haml9
-rw-r--r--app/views/projects/commit/show.html.haml4
-rw-r--r--app/views/projects/commits/_commit.html.haml46
-rw-r--r--app/views/projects/commits/_commit_list.html.haml4
-rw-r--r--app/views/projects/commits/_commits.html.haml4
-rw-r--r--app/views/projects/commits/show.html.haml11
-rw-r--r--app/views/projects/compare/_form.html.haml10
-rw-r--r--app/views/projects/compare/show.html.haml6
-rw-r--r--app/views/projects/cycle_analytics/_empty_stage.html.haml2
-rw-r--r--app/views/projects/cycle_analytics/show.html.haml14
-rw-r--r--app/views/projects/deploy_keys/_deploy_key.html.haml3
-rw-r--r--app/views/projects/deploy_keys/_form.html.haml9
-rw-r--r--app/views/projects/deployments/_actions.haml2
-rw-r--r--app/views/projects/deployments/_commit.html.haml2
-rw-r--r--app/views/projects/deployments/_deployment.html.haml4
-rw-r--r--app/views/projects/diffs/_content.html.haml4
-rw-r--r--app/views/projects/diffs/_diffs.html.haml7
-rw-r--r--app/views/projects/diffs/_file.html.haml10
-rw-r--r--app/views/projects/diffs/_file_header.html.haml4
-rw-r--r--app/views/projects/diffs/_image.html.haml40
-rw-r--r--app/views/projects/diffs/_line.html.haml4
-rw-r--r--app/views/projects/diffs/_parallel_view.html.haml34
-rw-r--r--app/views/projects/diffs/_stats.html.haml11
-rw-r--r--app/views/projects/diffs/_text_file.html.haml8
-rw-r--r--app/views/projects/edit.html.haml132
-rw-r--r--app/views/projects/empty.html.haml13
-rw-r--r--app/views/projects/environments/_stop.html.haml2
-rw-r--r--app/views/projects/environments/index.html.haml4
-rw-r--r--app/views/projects/environments/show.html.haml8
-rw-r--r--app/views/projects/environments/terminal.html.haml8
-rw-r--r--app/views/projects/find_file/show.html.haml6
-rw-r--r--app/views/projects/forks/index.html.haml4
-rw-r--r--app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml4
-rw-r--r--app/views/projects/graphs/_head.html.haml4
-rw-r--r--app/views/projects/graphs/ci/_build_times.haml2
-rw-r--r--app/views/projects/graphs/ci/_builds.haml14
-rw-r--r--app/views/projects/graphs/commits.html.haml10
-rw-r--r--app/views/projects/graphs/show.html.haml2
-rw-r--r--app/views/projects/group_links/_index.html.haml55
-rw-r--r--app/views/projects/group_links/index.html.haml55
-rw-r--r--app/views/projects/hooks/_index.html.haml (renamed from app/views/projects/hooks/index.html.haml)0
-rw-r--r--app/views/projects/issues/_discussion.html.haml4
-rw-r--r--app/views/projects/issues/_issue.html.haml6
-rw-r--r--app/views/projects/issues/_new_branch.html.haml5
-rw-r--r--app/views/projects/issues/index.html.haml10
-rw-r--r--app/views/projects/issues/show.html.haml14
-rw-r--r--app/views/projects/issues/verify.html.haml20
-rw-r--r--app/views/projects/labels/destroy.js.haml2
-rw-r--r--app/views/projects/labels/index.html.haml58
-rw-r--r--app/views/projects/mattermosts/_no_teams.html.haml16
-rw-r--r--app/views/projects/mattermosts/_team_selection.html.haml47
-rw-r--r--app/views/projects/mattermosts/new.html.haml8
-rw-r--r--app/views/projects/merge_requests/_merge_request.html.haml6
-rw-r--r--app/views/projects/merge_requests/_new_diffs.html.haml2
-rw-r--r--app/views/projects/merge_requests/_new_submit.html.haml23
-rw-r--r--app/views/projects/merge_requests/_show.html.haml126
-rw-r--r--app/views/projects/merge_requests/conflicts.html.haml21
-rw-r--r--app/views/projects/merge_requests/conflicts/_commit_stats.html.haml8
-rw-r--r--app/views/projects/merge_requests/conflicts/_file_actions.html.haml4
-rw-r--r--app/views/projects/merge_requests/conflicts/_submit_form.html.haml2
-rw-r--r--app/views/projects/merge_requests/conflicts/components/_diff_file_editor.html.haml2
-rw-r--r--app/views/projects/merge_requests/conflicts/components/_inline_conflict_lines.html.haml18
-rw-r--r--app/views/projects/merge_requests/show/_builds.html.haml1
-rw-r--r--app/views/projects/merge_requests/show/_commits.html.haml10
-rw-r--r--app/views/projects/merge_requests/show/_diffs.html.haml12
-rw-r--r--app/views/projects/merge_requests/show/_how_to_merge.html.haml10
-rw-r--r--app/views/projects/merge_requests/show/_mr_box.html.haml2
-rw-r--r--app/views/projects/merge_requests/show/_pipelines.html.haml4
-rw-r--r--app/views/projects/merge_requests/show/_versions.html.haml16
-rw-r--r--app/views/projects/merge_requests/widget/_closed.html.haml2
-rw-r--r--app/views/projects/merge_requests/widget/_heading.html.haml27
-rw-r--r--app/views/projects/merge_requests/widget/_merged_buttons.haml6
-rw-r--r--app/views/projects/merge_requests/widget/_open.html.haml4
-rw-r--r--app/views/projects/merge_requests/widget/_show.html.haml14
-rw-r--r--app/views/projects/merge_requests/widget/open/_accept.html.haml25
-rw-r--r--app/views/projects/merge_requests/widget/open/_archived.html.haml2
-rw-r--r--app/views/projects/merge_requests/widget/open/_build_failed.html.haml4
-rw-r--r--app/views/projects/merge_requests/widget/open/_check.html.haml9
-rw-r--r--app/views/projects/merge_requests/widget/open/_conflicts.html.haml30
-rw-r--r--app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml5
-rw-r--r--app/views/projects/merge_requests/widget/open/_nothing.html.haml2
-rw-r--r--app/views/projects/milestones/_form.html.haml2
-rw-r--r--app/views/projects/network/show.html.haml2
-rw-r--r--app/views/projects/new.html.haml4
-rw-r--r--app/views/projects/notes/_edit_form.html.haml13
-rw-r--r--app/views/projects/notes/_form.html.haml3
-rw-r--r--app/views/projects/notes/_note.html.haml21
-rw-r--r--app/views/projects/notes/_notes_with_form.html.haml7
-rw-r--r--app/views/projects/pages/_access.html.haml13
-rw-r--r--app/views/projects/pages/_destroy.haml12
-rw-r--r--app/views/projects/pages/_disabled.html.haml4
-rw-r--r--app/views/projects/pages/_list.html.haml17
-rw-r--r--app/views/projects/pages/_no_domains.html.haml7
-rw-r--r--app/views/projects/pages/_use.html.haml8
-rw-r--r--app/views/projects/pages/show.html.haml26
-rw-r--r--app/views/projects/pages_domains/_form.html.haml34
-rw-r--r--app/views/projects/pages_domains/new.html.haml6
-rw-r--r--app/views/projects/pages_domains/show.html.haml30
-rw-r--r--app/views/projects/pipelines/_head.html.haml4
-rw-r--r--app/views/projects/pipelines/_info.html.haml6
-rw-r--r--app/views/projects/pipelines/_stage.html.haml7
-rw-r--r--app/views/projects/pipelines/_with_tabs.html.haml4
-rw-r--r--app/views/projects/pipelines/index.html.haml52
-rw-r--r--app/views/projects/pipelines/show.html.haml2
-rw-r--r--app/views/projects/pipelines_settings/_show.html.haml96
-rw-r--r--app/views/projects/pipelines_settings/show.html.haml98
-rw-r--r--app/views/projects/project_members/_group_members.html.haml2
-rw-r--r--app/views/projects/project_members/_groups.html.haml2
-rw-r--r--app/views/projects/project_members/_index.html.haml22
-rw-r--r--app/views/projects/project_members/_new_project_member.html.haml38
-rw-r--r--app/views/projects/project_members/_shared_group_members.html.haml4
-rw-r--r--app/views/projects/project_members/_team.html.haml10
-rw-r--r--app/views/projects/project_members/import.html.haml3
-rw-r--r--app/views/projects/project_members/index.html.haml29
-rw-r--r--app/views/projects/protected_branches/index.html.haml2
-rw-r--r--app/views/projects/releases/edit.html.haml2
-rw-r--r--app/views/projects/runners/_form.html.haml2
-rw-r--r--app/views/projects/runners/_index.html.haml25
-rw-r--r--app/views/projects/runners/_runner.html.haml2
-rw-r--r--app/views/projects/runners/_shared_runners.html.haml2
-rw-r--r--app/views/projects/runners/_specific_runners.html.haml8
-rw-r--r--app/views/projects/runners/index.html.haml27
-rw-r--r--app/views/projects/services/_form.html.haml3
-rw-r--r--app/views/projects/services/_index.html.haml31
-rw-r--r--app/views/projects/services/index.html.haml33
-rw-r--r--app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml99
-rw-r--r--app/views/projects/services/mattermost_slash_commands/_help.html.haml113
-rw-r--r--app/views/projects/services/mattermost_slash_commands/_installation_info.html.haml7
-rw-r--r--app/views/projects/services/slack_slash_commands/_help.html.haml164
-rw-r--r--app/views/projects/settings/ci_cd/show.html.haml6
-rw-r--r--app/views/projects/settings/integrations/_project_hook.html.haml (renamed from app/views/projects/hooks/_project_hook.html.haml)0
-rw-r--r--app/views/projects/settings/integrations/show.html.haml3
-rw-r--r--app/views/projects/settings/members/show.html.haml6
-rw-r--r--app/views/projects/show.html.haml8
-rw-r--r--app/views/projects/snippets/_actions.html.haml7
-rw-r--r--app/views/projects/snippets/edit.html.haml2
-rw-r--r--app/views/projects/snippets/new.html.haml2
-rw-r--r--app/views/projects/snippets/show.html.haml2
-rw-r--r--app/views/projects/stage/_graph.html.haml7
-rw-r--r--app/views/projects/stage/_in_stage_group.html.haml15
-rw-r--r--app/views/projects/stage/_stage.html.haml6
-rw-r--r--app/views/projects/tags/_tag.html.haml41
-rw-r--r--app/views/projects/tags/index.html.haml12
-rw-r--r--app/views/projects/tags/new.html.haml2
-rw-r--r--app/views/projects/tags/show.html.haml16
-rw-r--r--app/views/projects/tree/_blob_item.html.haml2
-rw-r--r--app/views/projects/tree/_submodule_item.html.haml2
-rw-r--r--app/views/projects/tree/_tree_content.html.haml10
-rw-r--r--app/views/projects/tree/_tree_header.html.haml8
-rw-r--r--app/views/projects/tree/_tree_item.html.haml2
-rw-r--r--app/views/projects/triggers/_index.html.haml108
-rw-r--r--app/views/projects/triggers/index.html.haml110
-rw-r--r--app/views/projects/variables/_content.html.haml2
-rw-r--r--app/views/projects/variables/_index.html.haml16
-rw-r--r--app/views/projects/variables/index.html.haml18
-rw-r--r--app/views/projects/wikis/_new.html.haml4
-rw-r--r--app/views/projects/wikis/git_access.html.haml7
-rw-r--r--app/views/search/_category.html.haml26
-rw-r--r--app/views/search/results/_empty.html.haml2
-rw-r--r--app/views/search/results/_merge_request.html.haml2
-rw-r--r--app/views/search/results/_snippet_blob.html.haml2
-rw-r--r--app/views/search/results/_snippet_title.html.haml2
-rw-r--r--app/views/shared/_choose_group_avatar_button.html.haml2
-rw-r--r--app/views/shared/_clone_panel.html.haml6
-rw-r--r--app/views/shared/_confirm_modal.html.haml6
-rw-r--r--app/views/shared/_file_highlight.html.haml4
-rw-r--r--app/views/shared/_import_form.html.haml4
-rw-r--r--app/views/shared/_issues.html.haml15
-rw-r--r--app/views/shared/_label.html.haml12
-rw-r--r--app/views/shared/_merge_requests.html.haml14
-rw-r--r--app/views/shared/_milestones_filter.html.haml12
-rw-r--r--app/views/shared/_mini_pipeline_graph.html.haml18
-rw-r--r--app/views/shared/_nav_scroll.html.haml2
-rw-r--r--app/views/shared/_no_password.html.haml4
-rw-r--r--app/views/shared/_no_ssh.html.haml4
-rw-r--r--app/views/shared/_outdated_browser.html.haml3
-rw-r--r--app/views/shared/_sort_dropdown.html.haml2
-rw-r--r--app/views/shared/empty_states/_issues.html.haml6
-rw-r--r--app/views/shared/empty_states/_labels.html.haml11
-rw-r--r--app/views/shared/empty_states/_priority_labels.html.haml3
-rw-r--r--app/views/shared/empty_states/icons/_labels.svg1
-rw-r--r--app/views/shared/empty_states/icons/_priority_labels.svg1
-rw-r--r--app/views/shared/empty_states/icons/_todos_all_done.svg (renamed from app/views/shared/empty_states/_todos_all_done.svg)0
-rw-r--r--app/views/shared/empty_states/icons/_todos_empty.svg (renamed from app/views/shared/empty_states/_todos_empty.svg)0
-rw-r--r--app/views/shared/groups/_group.html.haml4
-rw-r--r--app/views/shared/icons/_icon_action_cancel.svg1
-rw-r--r--app/views/shared/icons/_icon_action_play.svg1
-rw-r--r--app/views/shared/icons/_icon_action_retry.svg1
-rw-r--r--app/views/shared/icons/_icon_action_stop.svg1
-rw-r--r--app/views/shared/icons/_icon_stopwatch.svg1
-rw-r--r--app/views/shared/icons/_mattermost_logo.svg.erb1
-rw-r--r--app/views/shared/icons/_scroll_down.svg3
-rw-r--r--app/views/shared/icons/_scroll_down_hover_active.svg3
-rw-r--r--app/views/shared/icons/_scroll_up.svg3
-rw-r--r--app/views/shared/icons/_scroll_up_hover_active.svg3
-rw-r--r--app/views/shared/issuable/_filter.html.haml27
-rw-r--r--app/views/shared/issuable/_form.html.haml9
-rw-r--r--app/views/shared/issuable/_label_dropdown.html.haml2
-rw-r--r--app/views/shared/issuable/_label_page_default.html.haml2
-rw-r--r--app/views/shared/issuable/_milestone_dropdown.html.haml2
-rw-r--r--app/views/shared/issuable/_nav.html.haml10
-rw-r--r--app/views/shared/issuable/_participants.html.haml4
-rw-r--r--app/views/shared/issuable/_search_bar.html.haml133
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml37
-rw-r--r--app/views/shared/issuable/form/_branch_chooser.html.haml9
-rw-r--r--app/views/shared/issuable/form/_merge_params.html.haml16
-rw-r--r--app/views/shared/issuable/form/_title.html.haml4
-rw-r--r--app/views/shared/members/_group.html.haml3
-rw-r--r--app/views/shared/milestones/_issuable.html.haml6
-rw-r--r--app/views/shared/milestones/_issuables.html.haml2
-rw-r--r--app/views/shared/milestones/_milestone.html.haml4
-rw-r--r--app/views/shared/notifications/_custom_notifications.html.haml2
-rw-r--r--app/views/shared/projects/_dropdown.html.haml2
-rw-r--r--app/views/shared/projects/_project.html.haml2
-rw-r--r--app/views/shared/snippets/_blob.html.haml2
-rw-r--r--app/views/shared/snippets/_form.html.haml5
-rw-r--r--app/views/shared/tokens/_scopes_form.html.haml4
-rw-r--r--app/views/shared/web_hooks/_form.html.haml9
-rw-r--r--app/views/sherlock/file_samples/show.html.haml2
-rw-r--r--app/views/sherlock/queries/_general.html.haml4
-rw-r--r--app/views/sherlock/queries/show.html.haml4
-rw-r--r--app/views/sherlock/transactions/index.html.haml2
-rw-r--r--app/views/sherlock/transactions/show.html.haml6
-rw-r--r--app/views/snippets/_actions.html.haml45
-rw-r--r--app/views/snippets/edit.html.haml2
-rw-r--r--app/views/snippets/new.html.haml2
-rw-r--r--app/views/u2f/_authenticate.html.haml19
-rw-r--r--app/views/u2f/_register.html.haml4
-rw-r--r--app/views/users/calendar.html.haml2
-rw-r--r--app/views/users/calendar_activities.html.haml2
-rw-r--r--app/views/users/show.html.haml20
-rw-r--r--app/workers/authorized_projects_worker.rb7
-rw-r--r--app/workers/build_queue_worker.rb10
-rw-r--r--app/workers/emails_on_push_worker.rb6
-rw-r--r--app/workers/pages_worker.rb23
-rw-r--r--app/workers/project_cache_worker.rb23
-rw-r--r--app/workers/reactive_caching_worker.rb4
-rw-r--r--app/workers/use_key_worker.rb13
-rwxr-xr-xbin/teaspoon8
-rw-r--r--changelogs/unreleased/15081-wrong-login-tab-ldap-frontend.yml4
-rw-r--r--changelogs/unreleased/17662-rename-builds.yml4
-rw-r--r--changelogs/unreleased/18435-autocomplete-is-not-performant.yml4
-rw-r--r--changelogs/unreleased/18546-update-wiki-page-design.yml4
-rw-r--r--changelogs/unreleased/19550-fix-contributer-graph-duplicates.yml4
-rw-r--r--changelogs/unreleased/19703-direct-link-pipelines.yml4
-rw-r--r--changelogs/unreleased/20052-actions-table-vscroll.yml4
-rw-r--r--changelogs/unreleased/20452-remove-require-from-request_profiler-initializer.yml4
-rw-r--r--changelogs/unreleased/20492-access-token-scopes.yml4
-rw-r--r--changelogs/unreleased/20852-getting-started-project-better-blank-state-for-labels-view.yml4
-rw-r--r--changelogs/unreleased/21518_recaptcha_spam_issues.yml4
-rw-r--r--changelogs/unreleased/22007-unify-projects-search.yml4
-rw-r--r--changelogs/unreleased/22348-gitea-importer.yml4
-rw-r--r--changelogs/unreleased/22373-reduce-queries-in-api-helpers-find_project.yml4
-rw-r--r--changelogs/unreleased/22604-manual-actions.yml4
-rw-r--r--changelogs/unreleased/22619-add-an-email-address-to-unsubscribe-list-header-in-email4
-rw-r--r--changelogs/unreleased/22638-creating-a-branch-matching-a-wildcard-fails.yml4
-rw-r--r--changelogs/unreleased/22719-provide-a-new-gitlab-workhorse-install-rake-task-similar-to-gitlab-shell-install.yml4
-rw-r--r--changelogs/unreleased/22781-user-generated-permalinks.yml4
-rw-r--r--changelogs/unreleased/22849-ci-build-ref-slug.yml4
-rw-r--r--changelogs/unreleased/22864-add-environment-slug.yml4
-rw-r--r--changelogs/unreleased/22864-kubernetes-service.yml4
-rw-r--r--changelogs/unreleased/22974-trigger-service-events-through-api.yml4
-rw-r--r--changelogs/unreleased/23500-enable-colorvariable.yml4
-rw-r--r--changelogs/unreleased/23524-notify-automerge-user-of-failed-build.yml4
-rw-r--r--changelogs/unreleased/23532-define-common-helper-for-describe-pagination-params-in-api.yml4
-rw-r--r--changelogs/unreleased/23573-sort-functionality-for-project-member.yml4
-rw-r--r--changelogs/unreleased/23589-open-issue-for-mr.yml5
-rw-r--r--changelogs/unreleased/23634-remove-project-grouping.yml4
-rw-r--r--changelogs/unreleased/23718-backup-rake-task-human-readable.yml4
-rw-r--r--changelogs/unreleased/23767-disable-storing-of-sensitive-information.yml4
-rw-r--r--changelogs/unreleased/24135-new-project-should-be-below-new-group-on-the-welcome-screen.yml4
-rw-r--r--changelogs/unreleased/24147-delete-env-button.yml4
-rw-r--r--changelogs/unreleased/24150-consistent-dropdown-styles.yml4
-rw-r--r--changelogs/unreleased/24281-issue-merge-request-sidebar-subscribe-button-style-improvement.yml4
-rw-r--r--changelogs/unreleased/24413-show-unconfirmed-email-status.yml4
-rw-r--r--changelogs/unreleased/24462-reduce_ldap_queries_for_lfs.yml4
-rw-r--r--changelogs/unreleased/24507_remove_deleted_branch_link_in_merge_request.yml4
-rw-r--r--changelogs/unreleased/24576_cant_stop_impersonating.yml4
-rw-r--r--changelogs/unreleased/24606-force-password-reset-on-next-login.yml4
-rw-r--r--changelogs/unreleased/24710-fix-generic-commit-status-table-row.yml4
-rw-r--r--changelogs/unreleased/24716-fix-ctrl-click-links.yml4
-rw-r--r--changelogs/unreleased/24726-remove-across-gitlab.yml4
-rw-r--r--changelogs/unreleased/24733-archived-project-merge-request-count.yml4
-rw-r--r--changelogs/unreleased/24795_refactor_merge_request_build_service.yml4
-rw-r--r--changelogs/unreleased/24803-change-cursor-for-ca-stages.yml5
-rw-r--r--changelogs/unreleased/24807-stop-ddosing-ourselves.yml4
-rw-r--r--changelogs/unreleased/24824-dropdown-items-focus.yml4
-rw-r--r--changelogs/unreleased/24833-Allow-to-search-by-commit-hash-within-project.yml4
-rw-r--r--changelogs/unreleased/24844-environments-date.yml4
-rw-r--r--changelogs/unreleased/24877-bulk-edit-only-keeps-common-labels-when-searching.yml4
-rw-r--r--changelogs/unreleased/24921-hide-prompt-to-add-ssh-key-if-ssh-protocol-is-disabled.yml4
-rw-r--r--changelogs/unreleased/24923_nested_tasks.yml4
-rw-r--r--changelogs/unreleased/24927-custom-event-polyfill-test.yml4
-rw-r--r--changelogs/unreleased/24949-view-2-up-swipe-onion-skin-controls-for-merge-request-diff-containing-an-image.yml4
-rw-r--r--changelogs/unreleased/24982-ux-improvement-sign-in-success-message.yml5
-rw-r--r--changelogs/unreleased/24999-fix-project-avatar-alignment.yml4
-rw-r--r--changelogs/unreleased/25002-sentence-case-dashboard-tabs.yml4
-rw-r--r--changelogs/unreleased/25011-hoverstates-for-collapsed-issue-merge-request-sidebar.yml4
-rw-r--r--changelogs/unreleased/25026-authenticate-user-for-new-snippet.yml4
-rw-r--r--changelogs/unreleased/25031-do-not-raise-error-in-autocomplete.yml4
-rw-r--r--changelogs/unreleased/25093-hide-new-issue-btn-non-loggedin-user.yml4
-rw-r--r--changelogs/unreleased/25098-header-margins-on-pipeline-settings.yml5
-rw-r--r--changelogs/unreleased/25106-hide-issue-mr-button-for-not-loggedin.yml4
-rw-r--r--changelogs/unreleased/25134-mobile-issue-view-doesn-t-show-organization-membership.yml4
-rw-r--r--changelogs/unreleased/25136-last-deployment-link.yml4
-rw-r--r--changelogs/unreleased/25144-gitlab-ce-mattermost-slash-command-for-issue-create-needs-better-documentation.yml4
-rw-r--r--changelogs/unreleased/25202-fix-mr-widget-content-wrapping.yml5
-rw-r--r--changelogs/unreleased/25207-text-overflow-env-table.yml4
-rw-r--r--changelogs/unreleased/25221-fix-build-status-overflow-mobile.yml4
-rw-r--r--changelogs/unreleased/25251-actionview-template-error-undefined-method-text-for-nil-nilclass.yml5
-rw-r--r--changelogs/unreleased/25264-ref-commit.yml4
-rw-r--r--changelogs/unreleased/25272_fix_comments_tab_disappearing.yml4
-rw-r--r--changelogs/unreleased/25294-remove-signed-out-msg.yml4
-rw-r--r--changelogs/unreleased/25301-git-2-11-force-push-bug.yml4
-rw-r--r--changelogs/unreleased/25312-search-input-cmd-click-issue.yml4
-rw-r--r--changelogs/unreleased/25324-change-housekeeping-btn-to-default.yml4
-rw-r--r--changelogs/unreleased/25339-2-webhooks-fired-for-issue-closed-and-reopened.yml4
-rw-r--r--changelogs/unreleased/25360-remove-flash-warning-from-login-page.yml4
-rw-r--r--changelogs/unreleased/25374-svg-as-prop.yml4
-rw-r--r--changelogs/unreleased/25460-replace-word-users-with-members.yml4
-rw-r--r--changelogs/unreleased/25482-fix-api-sudo.yml4
-rw-r--r--changelogs/unreleased/25483-broken-tabs.yml4
-rw-r--r--changelogs/unreleased/25534-adding-a-way-to-go-back-on-error-pages.yml4
-rw-r--r--changelogs/unreleased/25617-todos-filter-placeholder.yml4
-rw-r--r--changelogs/unreleased/25624-anticipate-obstacles-to-removing-turbolinks.yml4
-rw-r--r--changelogs/unreleased/25678-remove-user-build.yml4
-rw-r--r--changelogs/unreleased/25740-fix-new-branch-button-padding.yml4
-rw-r--r--changelogs/unreleased/25743-clean-up-css-for-project-alerts-and-flash-notifications.yml4
-rw-r--r--changelogs/unreleased/25811-pipeline-number-and-url-do-not-update-when-new-pipeline-is-triggered.yml4
-rw-r--r--changelogs/unreleased/25910-convert-manual-action-icons-to-svg-to-propperly-position-them.yml4
-rw-r--r--changelogs/unreleased/25989-fix-rogue-scrollbars-on-comments.yml4
-rw-r--r--changelogs/unreleased/26059-segoe-ui-vertical.yml4
-rw-r--r--changelogs/unreleased/26068_tasklist_issue.yml4
-rw-r--r--changelogs/unreleased/26117-sort-pipeline-for-commit.yml4
-rw-r--r--changelogs/unreleased/26186-diff-view-plus-and-minus-signs-as-part-of-line-number.yml4
-rw-r--r--changelogs/unreleased/26445-accessible-piplelines-buttons.yml4
-rw-r--r--changelogs/unreleased/26447-fix-tab-list-order.yml4
-rw-r--r--changelogs/unreleased/26468-fix-users-sort-in-admin-area.yml4
-rw-r--r--changelogs/unreleased/26787-add-copy-icon-hover-state.yml4
-rw-r--r--changelogs/unreleased/26852-fix-slug-for-openshift.yml4
-rw-r--r--changelogs/unreleased/26863-Remove-hover-animation-from-row-elements.yml4
-rw-r--r--changelogs/unreleased/26908-make-timelogs-use-foreign-keys4
-rw-r--r--changelogs/unreleased/26920-hover-cursor-on-pagination-element.yml4
-rw-r--r--changelogs/unreleased/26947-build-status-self-link.yml4
-rw-r--r--changelogs/unreleased/26982-improve-pipeline-status-icon-linking-in-widgets.yml4
-rw-r--r--changelogs/unreleased/27013-regression-in-commit-title-bar.yml4
-rw-r--r--changelogs/unreleased/27014-fix-pipeline-tooltip-wrapping.yml4
-rw-r--r--changelogs/unreleased/27021-line-numbers-now-in-copy-pasta-data.yml4
-rw-r--r--changelogs/unreleased/27178-update-builds-link-in-project-settings.yml4
-rw-r--r--changelogs/unreleased/27240-make-progress-bars-consistent.yml4
-rw-r--r--changelogs/unreleased/27267-unnecessary-queries-from-projects-dashboard-atom-and-json.yml4
-rw-r--r--changelogs/unreleased/27277-small-mini-pipeline-graph-glitch-upon-hover.yml4
-rw-r--r--changelogs/unreleased/27291-unify-mr-diff-file-buttons.yml4
-rw-r--r--changelogs/unreleased/27321-double-separator-line-in-edit-projects-settings.yml4
-rw-r--r--changelogs/unreleased/27332-mini-pipeline-graph-with-many-stages-has-no-line-spacing-in-firefox-and-safari.yml4
-rw-r--r--changelogs/unreleased/27343-autocomplete-post-to-wrong-url-when-not-hosting-in-root.yml5
-rw-r--r--changelogs/unreleased/27352-search-label-filter-header.yml4
-rw-r--r--changelogs/unreleased/27484-environment-show-name.yml4
-rw-r--r--changelogs/unreleased/27488-fix-jwt-version.yml4
-rw-r--r--changelogs/unreleased/27494-environment-list-column-headers.yml4
-rw-r--r--changelogs/unreleased/27516-fix-wrong-call-to-project_cache_worker-method.yml4
-rw-r--r--changelogs/unreleased/27602-fix-avatar-border-flicker-mention-dropdown.yml4
-rw-r--r--changelogs/unreleased/27632_fix_mr_widget_url.yml4
-rw-r--r--changelogs/unreleased/27639-emoji-panel-under-the-side-panel-in-the-merge-request.yml4
-rw-r--r--changelogs/unreleased/27774-text-color-contrast-is-barely-readable-for-pipelines-visualization-graph-page-with-roboto-fonts.yml4
-rw-r--r--changelogs/unreleased/27822-default-bulk-assign-labels.yml4
-rw-r--r--changelogs/unreleased/395-fix-notification-when-group-set-to-watch.yml4
-rw-r--r--changelogs/unreleased/4269-public-api.yml4
-rw-r--r--changelogs/unreleased/4269-public-files-api.yml4
-rw-r--r--changelogs/unreleased/4269-public-repositories-api.yml4
-rw-r--r--changelogs/unreleased/7749-add-setting-to-disable-html-emails.yml3
-rw-r--r--changelogs/unreleased/8-15-stable.yml4
-rw-r--r--changelogs/unreleased/8003-katex-math.yml4
-rw-r--r--changelogs/unreleased/8082-permalink-to-file.yml4
-rw-r--r--changelogs/unreleased/9-0-api-changes.yml4
-rw-r--r--changelogs/unreleased/Add-a-shash-command-for-target-merge-request-branch.yml4
-rw-r--r--changelogs/unreleased/abuse_report-fixture.yml4
-rw-r--r--changelogs/unreleased/add_info_to_qr.yml4
-rw-r--r--changelogs/unreleased/add_project_update_hook.yml4
-rw-r--r--changelogs/unreleased/allow-more-filenames.yml4
-rw-r--r--changelogs/unreleased/api-branch-status.yml4
-rw-r--r--changelogs/unreleased/api-cherry-pick.yml4
-rw-r--r--changelogs/unreleased/api-delete-group-share.yml4
-rw-r--r--changelogs/unreleased/api-expose-commiter-details.yml4
-rw-r--r--changelogs/unreleased/api-fix-files.yml4
-rw-r--r--changelogs/unreleased/api-remove-snippets-expires-at.yml4
-rw-r--r--changelogs/unreleased/api-remove-source-branch.yml4
-rw-r--r--changelogs/unreleased/api-simple-group-project.yml4
-rw-r--r--changelogs/unreleased/awards_handler.yml4
-rw-r--r--changelogs/unreleased/babel-all-the-things.yml5
-rw-r--r--changelogs/unreleased/bitbucket-oauth2.yml4
-rw-r--r--changelogs/unreleased/broken_iamge_when_doing_offline_update_check-help_page-.yml4
-rw-r--r--changelogs/unreleased/change_development_build_fixtures.yml4
-rw-r--r--changelogs/unreleased/chomp-git-status-message.yml5
-rw-r--r--changelogs/unreleased/cleanup-common_utils-js.yml4
-rw-r--r--changelogs/unreleased/clipboard-button-commit-sha.yml3
-rw-r--r--changelogs/unreleased/comments-fixture.yml4
-rw-r--r--changelogs/unreleased/contribution-calendar-scroll.yml4
-rw-r--r--changelogs/unreleased/cop-gem-fetcher.yml4
-rw-r--r--changelogs/unreleased/copy-as-md.yml4
-rw-r--r--changelogs/unreleased/create-dynamic-fixture-for-build_spec.yml4
-rw-r--r--changelogs/unreleased/destroy-session.yml4
-rw-r--r--changelogs/unreleased/dev-issue-24554.yml4
-rw-r--r--changelogs/unreleased/disable-autologin-on-email-confirmation-links.yml4
-rw-r--r--changelogs/unreleased/display-project-id.yml4
-rw-r--r--changelogs/unreleased/do-not-refresh-main-when-fork-target-branch-updated.yml4
-rw-r--r--changelogs/unreleased/dockerfile-templates.yml4
-rw-r--r--changelogs/unreleased/document-how-to-vue.yml4
-rw-r--r--changelogs/unreleased/dont-delete-assigned-issuables.yml4
-rw-r--r--changelogs/unreleased/dz-allow-nested-group-routing.yml4
-rw-r--r--changelogs/unreleased/dz-fix-route-rename.yml4
-rw-r--r--changelogs/unreleased/dz-nested-groups-improvements-2.yml4
-rw-r--r--changelogs/unreleased/dz-nested-groups.yml4
-rw-r--r--changelogs/unreleased/emoji-btn-disabled.yml4
-rw-r--r--changelogs/unreleased/empty-selection-reply-shortcut.yml4
-rw-r--r--changelogs/unreleased/enable-asciidoctor-admonition-icons.yml4
-rw-r--r--changelogs/unreleased/expose-deployment-variables.yml4
-rw-r--r--changelogs/unreleased/fe-commit-mr-pipelines.yml4
-rw-r--r--changelogs/unreleased/feature-admin-user-groups-link.yml4
-rw-r--r--changelogs/unreleased/feature-success-warning-icons-in-stages-builds.yml4
-rw-r--r--changelogs/unreleased/features-api-snippets.yml4
-rw-r--r--changelogs/unreleased/file-template-dropwdown-proper-position.yml4
-rw-r--r--changelogs/unreleased/fix-27479.yml4
-rw-r--r--changelogs/unreleased/fix-anchor-scrolling.yml4
-rw-r--r--changelogs/unreleased/fix-api-mr-permissions.yml4
-rw-r--r--changelogs/unreleased/fix-blame-500.yml4
-rw-r--r--changelogs/unreleased/fix-cancelling-pipelines.yml4
-rw-r--r--changelogs/unreleased/fix-ci-build-policy.yml4
-rw-r--r--changelogs/unreleased/fix-create-pipeline-with-builds-in-transaction.yml4
-rw-r--r--changelogs/unreleased/fix-deleting-project-again.yml4
-rw-r--r--changelogs/unreleased/fix-depr-warn.yml4
-rw-r--r--changelogs/unreleased/fix-drop-project-authorized-for-user.yml4
-rw-r--r--changelogs/unreleased/fix-gb-backwards-compatibility-coverage-ci-yml.yml4
-rw-r--r--changelogs/unreleased/fix-guest-access-posting-to-notes.yml4
-rw-r--r--changelogs/unreleased/fix-import-encrypt-atts.yml4
-rw-r--r--changelogs/unreleased/fix-import-export-build-token.yml4
-rw-r--r--changelogs/unreleased/fix-import-export-ee-services.yml4
-rw-r--r--changelogs/unreleased/fix-import-export-mr-error.yml4
-rw-r--r--changelogs/unreleased/fix-import-group-members.yml4
-rw-r--r--changelogs/unreleased/fix-references-header-parsing.yml5
-rw-r--r--changelogs/unreleased/fix-rename-mwbs-to-merge-when-pipeline-succeeds.yml4
-rw-r--r--changelogs/unreleased/fix-scroll-test.yml4
-rw-r--r--changelogs/unreleased/fix-slack-pipeline-message-by-api.yml4
-rw-r--r--changelogs/unreleased/fix-users-deleting-public-deployment-keys.yml4
-rw-r--r--changelogs/unreleased/fix-yaml-variables.yml4
-rw-r--r--changelogs/unreleased/fix_broken_diff_discussions.yml4
-rw-r--r--changelogs/unreleased/fwn-to-find-by-full-path.yml4
-rw-r--r--changelogs/unreleased/gem-update-grape.yml4
-rw-r--r--changelogs/unreleased/get-rid-of-water-from-notification_service_spec-to-make-it-DRY.yml4
-rw-r--r--changelogs/unreleased/git_to_html_redirection.yml4
-rw-r--r--changelogs/unreleased/gitlab-workhorse-multipart.yml4
-rw-r--r--changelogs/unreleased/glm-shorthand-reference.yml4
-rw-r--r--changelogs/unreleased/go-go-gadget-webpack.yml4
-rw-r--r--changelogs/unreleased/group-label-sidebar-link.yml4
-rw-r--r--changelogs/unreleased/hardcode-title-system-note.yml4
-rw-r--r--changelogs/unreleased/hoopes-gitlab-ce-21027-add-diff-hunks-to-notification-emails.yml4
-rw-r--r--changelogs/unreleased/html-safe-diff-line-content.yml4
-rw-r--r--changelogs/unreleased/improve-ci-example-php-doc.yml4
-rw-r--r--changelogs/unreleased/improve-handleLocationHash-tests.yml4
-rw-r--r--changelogs/unreleased/improve-invite-accept-page.yml4
-rw-r--r--changelogs/unreleased/issuable-sidebar-bug.yml4
-rw-r--r--changelogs/unreleased/issuable_filters_present-refactor.yml4
-rw-r--r--changelogs/unreleased/issue-20428.yml4
-rw-r--r--changelogs/unreleased/issue-24534.yml4
-rw-r--r--changelogs/unreleased/issue-boards-scrollable-element.yml4
-rw-r--r--changelogs/unreleased/issue-events-filter.yml4
-rw-r--r--changelogs/unreleased/issue-sidebar-empty-assignee.yml4
-rw-r--r--changelogs/unreleased/issue_13270.yml4
-rw-r--r--changelogs/unreleased/issue_19262.yml4
-rw-r--r--changelogs/unreleased/issue_22269.yml4
-rw-r--r--changelogs/unreleased/issue_24363.yml4
-rw-r--r--changelogs/unreleased/issue_24748.yml4
-rw-r--r--changelogs/unreleased/issue_24958.yml4
-rw-r--r--changelogs/unreleased/issue_27211.yml4
-rw-r--r--changelogs/unreleased/issues-1608-text.yml4
-rw-r--r--changelogs/unreleased/jej-22869.yml4
-rw-r--r--changelogs/unreleased/jej-23867-use-mr-finder-instead-of-access-check.yml4
-rw-r--r--changelogs/unreleased/jej-24637-move-issue-visible_to_user-to-finder.yml4
-rw-r--r--changelogs/unreleased/jej-fix-missing-access-check-on-issues.yml4
-rw-r--r--changelogs/unreleased/jej-fix-n-1-queries-milestones-show.yml4
-rw-r--r--changelogs/unreleased/jej-memoize-milestoneish-visible-to-user.yml4
-rw-r--r--changelogs/unreleased/jej-note-search-uses-finder.yml4
-rw-r--r--changelogs/unreleased/jej-pages-picked-from-ee.yml4
-rw-r--r--changelogs/unreleased/jej-use-issuable-finder-instead-of-access-check.yml4
-rw-r--r--changelogs/unreleased/label-promotion.yml4
-rw-r--r--changelogs/unreleased/ldap_maint_task.yml4
-rw-r--r--changelogs/unreleased/leave-project-btn.yml4
-rw-r--r--changelogs/unreleased/lfs-noauth-public-repo.yml4
-rw-r--r--changelogs/unreleased/markdown-plantuml.yml4
-rw-r--r--changelogs/unreleased/members-dropdowns.yml4
-rw-r--r--changelogs/unreleased/milestone_start_date.yml4
-rw-r--r--changelogs/unreleased/misalinged-discussion-with-no-avatar-26854.yml4
-rw-r--r--changelogs/unreleased/move-abuse-report-spinach-test-to-rspec.yml4
-rw-r--r--changelogs/unreleased/move-admin-abuse-report-spinach-test-to-rspec.yml4
-rw-r--r--changelogs/unreleased/move-admin-active-tab-spinach-tests-to-rspec.yml4
-rw-r--r--changelogs/unreleased/move-admin-hooks-spinach-test-to-rspec.yml4
-rw-r--r--changelogs/unreleased/move-admin-logs-spinach-test-to-rspec.yml4
-rw-r--r--changelogs/unreleased/move-admin-spam-spinach-test-to-rspec.yml4
-rw-r--r--changelogs/unreleased/mr-origin-7855.yml4
-rw-r--r--changelogs/unreleased/mr-tabs-container-offset.yml4
-rw-r--r--changelogs/unreleased/newline-eslint-rule.yml4
-rw-r--r--changelogs/unreleased/nick-thomas-gitlab-ce-22864-kubernetes-deploy-with-terminal.yml4
-rw-r--r--changelogs/unreleased/no-sidebar-on-action-btn-click.yml4
-rw-r--r--changelogs/unreleased/no_project_notes.yml4
-rw-r--r--changelogs/unreleased/nuke-ugly-spaces-in-changelog-generator.yml4
-rw-r--r--changelogs/unreleased/pass in current_user in MergeRequest and MergeRequestsHelper.yml4
-rw-r--r--changelogs/unreleased/pipeline-build-hitbox.yml4
-rw-r--r--changelogs/unreleased/pms-lowercase-system-notes.yml4
-rw-r--r--changelogs/unreleased/post_receive-any-email.yml4
-rw-r--r--changelogs/unreleased/process-commit-worker-migration-encoding.yml4
-rw-r--r--changelogs/unreleased/public-tags-api.yml4
-rw-r--r--changelogs/unreleased/readme-link-fix.yml4
-rw-r--r--changelogs/unreleased/redesign-searchbar-admin-project-26794.yml4
-rw-r--r--changelogs/unreleased/redirect-to-commit-when-only-commit-found.yml5
-rw-r--r--changelogs/unreleased/refactor-create-service-spec.yml4
-rw-r--r--changelogs/unreleased/refresh-permissions-when-moving-projects.yml4
-rw-r--r--changelogs/unreleased/relative-url-assets.yml4
-rw-r--r--changelogs/unreleased/removal_of_unused_parameter.yml4
-rw-r--r--changelogs/unreleased/remove-backup-strategies.yml4
-rw-r--r--changelogs/unreleased/remove-deploy-key-endpoint.yml4
-rw-r--r--changelogs/unreleased/remove-jsx-react-eslint-plugins.yml5
-rw-r--r--changelogs/unreleased/remove-require-from-services.yml4
-rw-r--r--changelogs/unreleased/remove-sidekiq-backup-ar-threads.yml4
-rw-r--r--changelogs/unreleased/remove-unnecessary-message-mr-commits-tab.yml4
-rw-r--r--changelogs/unreleased/remove-unnecessary-self-from-user-model.yml4
-rw-r--r--changelogs/unreleased/removing_unnecessary_indexes.yml4
-rw-r--r--changelogs/unreleased/render-svg-in-diffs-and-notes.yml4
-rw-r--r--changelogs/unreleased/right-sidebar-fixture.yml4
-rw-r--r--changelogs/unreleased/rounded-labels-fixes.yml4
-rw-r--r--changelogs/unreleased/route-map.yml4
-rw-r--r--changelogs/unreleased/rs-project-team-helpers.yml4
-rw-r--r--changelogs/unreleased/seed-runner-token.yml5
-rw-r--r--changelogs/unreleased/shortcuts-issuable-fixture.yml4
-rw-r--r--changelogs/unreleased/show-commit-status-from-latest-pipeline.yml4
-rw-r--r--changelogs/unreleased/simplify-create-new-list-issue-boards.yml4
-rw-r--r--changelogs/unreleased/slash-commands-typo.yml4
-rw-r--r--changelogs/unreleased/small-emoji-adjustments.yml4
-rw-r--r--changelogs/unreleased/small-screen-fullscreen-button.yml4
-rw-r--r--changelogs/unreleased/tc-only-mr-button-if-allowed.yml4
-rw-r--r--changelogs/unreleased/terminal-max-session-time.yml4
-rw-r--r--changelogs/unreleased/update-api-spec-files.yml4
-rw-r--r--changelogs/unreleased/update-button-font-weight.yml4
-rw-r--r--changelogs/unreleased/update-git-version-in-doc.yml4
-rw-r--r--changelogs/unreleased/upgrade-omniauth.yml4
-rw-r--r--changelogs/unreleased/use-st-commits-where-possible.yml5
-rw-r--r--changelogs/unreleased/username-exists-root.yml4
-rw-r--r--changelogs/unreleased/validate-state-param-when-filtering-issuables.yml4
-rw-r--r--changelogs/unreleased/wip-mr-from-commits.yml4
-rw-r--r--changelogs/unreleased/zen-mode-fixture.yml4
-rw-r--r--changelogs/unreleased/zj-expose-coverage-pipelines.yml4
-rw-r--r--changelogs/unreleased/zj-fix-label-creation-non-members.yml4
-rw-r--r--changelogs/unreleased/zj-format-chat-messages.yml4
-rw-r--r--changelogs/unreleased/zj-guest-reads-public-builds.yml4
-rw-r--r--changelogs/unreleased/zj-issue-new-over-issue-create.yml4
-rw-r--r--changelogs/unreleased/zj-issue-search-slash-command.yml4
-rw-r--r--changelogs/unreleased/zj-requeue-pending-delete.yml4
-rw-r--r--changelogs/unreleased/zj-slack-slash-commands.yml4
-rw-r--r--changelogs/unreleased/zj-use-ruby-2-3-3.yml4
-rw-r--r--config/application.rb29
-rw-r--r--config/database.yml.mysql8
-rw-r--r--config/dependency_decisions.yml121
-rw-r--r--config/gitlab.yml.example37
-rw-r--r--config/initializers/1_settings.rb61
-rw-r--r--config/initializers/5_backend.rb6
-rw-r--r--config/initializers/inflections.rb2
-rw-r--r--config/initializers/metrics.rb219
-rw-r--r--config/initializers/plantuml_lexer.rb2
-rw-r--r--config/initializers/request_profiler.rb2
-rw-r--r--config/initializers/rspec_profiling.rb14
-rw-r--r--config/initializers/sidekiq.rb13
-rw-r--r--config/initializers/static_files.rb31
-rw-r--r--config/initializers/workhorse_multipart.rb22
-rw-r--r--config/karma.config.js21
-rw-r--r--config/routes/admin.rb3
-rw-r--r--config/routes/group.rb6
-rw-r--r--config/routes/project.rb20
-rw-r--r--config/routes/snippets.rb1
-rw-r--r--config/sidekiq_queues.yml2
-rw-r--r--config/webpack.config.js126
-rw-r--r--db/fixtures/development/01_admin.rb2
-rw-r--r--db/fixtures/development/04_project.rb2
-rw-r--r--db/fixtures/development/05_users.rb2
-rw-r--r--db/fixtures/development/06_teams.rb2
-rw-r--r--db/fixtures/development/07_milestones.rb2
-rw-r--r--db/fixtures/development/09_issues.rb2
-rw-r--r--db/fixtures/development/10_merge_requests.rb4
-rw-r--r--db/fixtures/development/11_keys.rb22
-rw-r--r--db/fixtures/development/12_snippets.rb2
-rw-r--r--db/fixtures/development/13_comments.rb2
-rw-r--r--db/fixtures/development/14_pipelines.rb2
-rw-r--r--db/fixtures/development/15_award_emoji.rb2
-rw-r--r--db/fixtures/development/16_protected_branches.rb2
-rw-r--r--db/fixtures/development/17_cycle_analytics.rb2
-rw-r--r--db/migrate/20141006143943_move_slack_service_to_webhook.rb2
-rw-r--r--db/migrate/20151215132013_add_pages_size_to_application_settings.rb14
-rw-r--r--db/migrate/20160210105555_create_pages_domain.rb16
-rw-r--r--db/migrate/20160811172945_add_can_push_to_keys.rb14
-rw-r--r--db/migrate/20161114024742_add_coverage_regex_to_builds.rb13
-rw-r--r--db/migrate/20161117114805_remove_undeleted_groups.rb96
-rw-r--r--db/migrate/20161201001911_add_plant_uml_url_to_application_settings.rb12
-rw-r--r--db/migrate/20161201155511_create_project_statistics.rb20
-rw-r--r--db/migrate/20161201160452_migrate_project_statistics.rb23
-rw-r--r--db/migrate/20161206003819_add_plant_uml_enabled_to_application_settings.rb12
-rw-r--r--db/migrate/20161207231626_add_environment_slug.rb18
-rw-r--r--db/migrate/20161213172958_change_slack_service_to_slack_notification_service.rb13
-rw-r--r--db/migrate/20161220141214_remove_dot_git_from_group_names.rb82
-rw-r--r--db/migrate/20161221152132_add_last_used_at_to_key.rb9
-rw-r--r--db/migrate/20161223034433_add_estimate_to_issuables_ce.rb25
-rw-r--r--db/migrate/20161223034646_create_timelogs_ce.rb24
-rw-r--r--db/migrate/20161226122833_remove_dot_git_from_usernames.rb114
-rw-r--r--db/migrate/20161227192806_rename_slack_and_mattermost_notification_services.rb25
-rw-r--r--db/migrate/20170121123724_add_index_to_ci_builds_for_status_runner_id_and_type.rb17
-rw-r--r--db/migrate/20170121130655_add_index_to_ci_runners_for_is_shared.rb17
-rw-r--r--db/migrate/20170124174637_add_foreign_keys_to_timelogs.rb54
-rw-r--r--db/migrate/20170126174819_add_terminal_max_session_time_to_application_settings.rb33
-rw-r--r--db/migrate/20170127032550_remove_backlog_lists_from_boards.rb17
-rw-r--r--db/migrate/20170130204620_add_index_to_project_authorizations.rb11
-rw-r--r--db/migrate/20170204181513_add_index_to_labels_for_type_and_project.rb11
-rw-r--r--db/migrate/20170206071414_add_recaptcha_verified_to_spam_logs.rb15
-rw-r--r--db/post_migrate/20161221140236_remove_unneeded_services.rb15
-rw-r--r--db/post_migrate/20161221153951_rename_reserved_project_names.rb130
-rw-r--r--db/post_migrate/20170104150317_requeue_pending_delete_projects.rb49
-rw-r--r--db/post_migrate/20170106142508_fill_authorized_projects.rb30
-rw-r--r--db/post_migrate/20170106172224_remove_project_authorizations_id_column.rb12
-rw-r--r--db/post_migrate/20170206101007_remove_trackable_columns_from_timelogs.rb23
-rw-r--r--db/post_migrate/20170206101030_validate_foreign_keys_on_timelogs.rb32
-rw-r--r--db/schema.rb68
-rw-r--r--doc/README.md19
-rw-r--r--doc/administration/auth/README.md2
-rw-r--r--doc/administration/auth/authentiq.md69
-rw-r--r--doc/administration/auth/ldap.md20
-rw-r--r--doc/administration/build_artifacts.md6
-rw-r--r--doc/administration/container_registry.md4
-rw-r--r--doc/administration/custom_hooks.md3
-rw-r--r--doc/administration/high_availability/load_balancer.md2
-rw-r--r--doc/administration/high_availability/nfs.md18
-rw-r--r--doc/administration/housekeeping.md26
-rw-r--r--doc/administration/img/integration/plantuml-example.pngbin0 -> 33034 bytes
-rw-r--r--doc/administration/integration/plantuml.md95
-rw-r--r--doc/administration/integration/terminal.md26
-rw-r--r--doc/administration/monitoring/performance/introduction.md5
-rw-r--r--doc/administration/monitoring/performance/prometheus.md102
-rw-r--r--doc/administration/pages/index.md249
-rw-r--r--doc/administration/pages/source.md323
-rw-r--r--doc/administration/raketasks/maintenance.md5
-rw-r--r--doc/administration/reply_by_email.md4
-rw-r--r--doc/administration/reply_by_email_postfix_setup.md2
-rw-r--r--doc/administration/repository_checks.md10
-rw-r--r--doc/administration/repository_storage_paths.md102
-rw-r--r--doc/administration/repository_storages.md101
-rw-r--r--doc/api/README.md9
-rw-r--r--doc/api/commits.md2
-rw-r--r--doc/api/deploy_keys.md19
-rw-r--r--doc/api/enviroments.md4
-rw-r--r--doc/api/groups.md23
-rw-r--r--doc/api/issues.md144
-rw-r--r--doc/api/merge_requests.md142
-rw-r--r--doc/api/project_snippets.md1
-rw-r--r--doc/api/projects.md4
-rw-r--r--doc/api/repository_files.md6
-rw-r--r--doc/api/services.md2
-rw-r--r--doc/api/settings.md14
-rw-r--r--doc/api/users.md1
-rw-r--r--doc/api/v3_to_v4.md14
-rw-r--r--doc/ci/README.md1
-rw-r--r--doc/ci/autodeploy/img/auto_deploy_button.pngbin0 -> 43441 bytes
-rw-r--r--doc/ci/autodeploy/img/auto_deploy_dropdown.pngbin0 -> 44380 bytes
-rw-r--r--doc/ci/autodeploy/index.md41
-rw-r--r--doc/ci/environments.md66
-rw-r--r--doc/ci/examples/README.md6
-rw-r--r--doc/ci/examples/deployment/composer-npm-deploy.md156
-rw-r--r--doc/ci/examples/php.md6
-rw-r--r--doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md2
-rw-r--r--doc/ci/git_submodules.md13
-rw-r--r--doc/ci/img/view_on_env_blob.pngbin0 -> 111663 bytes
-rw-r--r--doc/ci/img/view_on_env_mr.pngbin0 -> 1005195 bytes
-rw-r--r--doc/ci/pipelines.md2
-rw-r--r--doc/ci/quick_start/README.md2
-rw-r--r--doc/ci/variables/README.md4
-rw-r--r--doc/ci/yaml/README.md84
-rw-r--r--doc/development/README.md1
-rw-r--r--doc/development/architecture.md14
-rw-r--r--doc/development/changelog.md15
-rw-r--r--doc/development/code_review.md12
-rw-r--r--doc/development/frontend.md152
-rw-r--r--doc/development/gemfile.md14
-rw-r--r--doc/development/gitlab_architecture_diagram.pngbin20339 -> 61667 bytes
-rw-r--r--doc/development/merge_request_performance_guidelines.md8
-rw-r--r--doc/development/performance.md35
-rw-r--r--doc/development/sidekiq_debugging.md13
-rw-r--r--doc/development/ui_guide.md6
-rw-r--r--doc/development/ux_guide/animation.md19
-rw-r--r--doc/development/ux_guide/basics.md21
-rw-r--r--doc/development/ux_guide/components.md29
-rw-r--r--doc/development/ux_guide/copy.md112
-rw-r--r--doc/development/ux_guide/img/animation-autoscroll.gifbin0 -> 302217 bytes
-rw-r--r--doc/development/ux_guide/img/animation-reorder.gifbin0 -> 70515 bytes
-rw-r--r--doc/development/ux_guide/img/color-textprimary.pngbin0 -> 2553 bytes
-rw-r--r--doc/development/ux_guide/img/color-textsecondary.pngbin0 -> 2956 bytes
-rw-r--r--doc/development/ux_guide/img/components-searchbox.pngbin0 -> 5292 bytes
-rw-r--r--doc/development/ux_guide/img/components-searchboxscoped.pngbin0 -> 9668 bytes
-rw-r--r--doc/gitlab-basics/add-image.md50
-rw-r--r--doc/install/README.md3
-rw-r--r--doc/install/database_mysql.md205
-rw-r--r--doc/install/digitaloceandocker.md136
-rw-r--r--doc/install/installation.md27
-rw-r--r--doc/install/relative_url.md8
-rw-r--r--doc/integration/README.md14
-rw-r--r--doc/integration/bitbucket.md6
-rw-r--r--doc/integration/chat_commands.md14
-rw-r--r--doc/integration/external-issue-tracker.md8
-rw-r--r--doc/integration/jira.md4
-rw-r--r--doc/integration/oauth_provider.md8
-rw-r--r--doc/integration/omniauth.md11
-rw-r--r--doc/pages/README.md1
-rw-r--r--doc/pages/administration.md1
-rw-r--r--doc/profile/2fa_u2f_authenticate.pngbin17585 -> 0 bytes
-rw-r--r--doc/profile/two_factor_authentication.md144
-rw-r--r--doc/project_services/bamboo.md61
-rw-r--r--doc/project_services/bugzilla.md18
-rw-r--r--doc/project_services/builds_emails.md17
-rw-r--r--doc/project_services/emails_on_push.md18
-rw-r--r--doc/project_services/hipchat.md55
-rw-r--r--doc/project_services/img/builds_emails_service.pngbin19203 -> 0 bytes
-rw-r--r--doc/project_services/img/mattermost_config_help.pngbin63138 -> 0 bytes
-rw-r--r--doc/project_services/irker.md52
-rw-r--r--doc/project_services/jira.md209
-rw-r--r--doc/project_services/kubernetes.md64
-rw-r--r--doc/project_services/mattermost.md46
-rw-r--r--doc/project_services/mattermost_slash_commands.md158
-rw-r--r--doc/project_services/project_services.md59
-rw-r--r--doc/project_services/redmine.md22
-rw-r--r--doc/project_services/services_templates.md26
-rw-r--r--doc/project_services/slack.md51
-rw-r--r--doc/project_services/slack_slash_commands.md1
-rw-r--r--doc/public_access/img/restrict_visibility_levels.pngbin0 -> 24593 bytes
-rw-r--r--doc/public_access/public_access.md9
-rw-r--r--doc/raketasks/backup_restore.md37
-rw-r--r--doc/raketasks/features.md2
-rw-r--r--doc/security/webhooks.md4
-rw-r--r--doc/ssh/README.md10
-rw-r--r--doc/system_hooks/system_hooks.md19
-rw-r--r--doc/university/README.md8
-rw-r--r--doc/university/glossary/README.md2
-rw-r--r--doc/university/support/README.md2
-rwxr-xr-xdoc/university/training/topics/additional_resources.md2
-rwxr-xr-xdoc/university/training/user_training.md2
-rw-r--r--doc/update/2.6-to-3.0.md2
-rw-r--r--doc/update/2.9-to-3.0.md2
-rw-r--r--doc/update/3.0-to-3.1.md2
-rw-r--r--doc/update/3.1-to-4.0.md2
-rw-r--r--doc/update/4.0-to-4.1.md2
-rw-r--r--doc/update/4.1-to-4.2.md2
-rw-r--r--doc/update/4.2-to-5.0.md2
-rw-r--r--doc/update/5.0-to-5.1.md2
-rw-r--r--doc/update/5.1-to-5.2.md2
-rw-r--r--doc/update/5.1-to-5.4.md2
-rw-r--r--doc/update/5.1-to-6.0.md2
-rw-r--r--doc/update/5.2-to-5.3.md2
-rw-r--r--doc/update/5.3-to-5.4.md2
-rw-r--r--doc/update/5.4-to-6.0.md2
-rw-r--r--doc/update/6.0-to-6.1.md2
-rw-r--r--doc/update/6.1-to-6.2.md2
-rw-r--r--doc/update/6.2-to-6.3.md2
-rw-r--r--doc/update/6.3-to-6.4.md2
-rw-r--r--doc/update/6.4-to-6.5.md2
-rw-r--r--doc/update/6.5-to-6.6.md2
-rw-r--r--doc/update/6.6-to-6.7.md2
-rw-r--r--doc/update/6.7-to-6.8.md2
-rw-r--r--doc/update/6.8-to-6.9.md2
-rw-r--r--doc/update/6.9-to-7.0.md2
-rw-r--r--doc/update/6.x-or-7.x-to-7.14.md4
-rw-r--r--doc/update/7.0-to-7.1.md2
-rw-r--r--doc/update/7.1-to-7.2.md2
-rw-r--r--doc/update/7.2-to-7.3.md2
-rw-r--r--doc/update/7.3-to-7.4.md2
-rw-r--r--doc/update/8.14-to-8.15.md87
-rw-r--r--doc/update/8.15-to-8.16.md239
-rw-r--r--doc/update/8.16-to-8.17.md239
-rw-r--r--doc/update/patch_versions.md41
-rw-r--r--doc/user/account/security.md4
-rw-r--r--doc/user/account/two_factor_authentication.md69
-rw-r--r--doc/user/markdown.md20
-rw-r--r--doc/user/permissions.md33
-rw-r--r--doc/user/profile/account/img/2fa.png (renamed from doc/profile/2fa.png)bin22047 -> 22047 bytes
-rw-r--r--doc/user/profile/account/img/2fa_auth.png (renamed from doc/profile/2fa_auth.png)bin14535 -> 14535 bytes
-rw-r--r--doc/user/profile/account/img/2fa_u2f_authenticate.pngbin0 -> 17582 bytes
-rw-r--r--doc/user/profile/account/img/2fa_u2f_register.png (renamed from doc/profile/2fa_u2f_register.png)bin35186 -> 35186 bytes
-rw-r--r--doc/user/profile/account/index.md5
-rw-r--r--doc/user/profile/account/two_factor_authentication.md217
-rw-r--r--doc/user/project/container_registry.md11
-rw-r--r--doc/user/project/cycle_analytics.md15
-rw-r--r--doc/user/project/integrations/bamboo.md59
-rw-r--r--doc/user/project/integrations/bugzilla.md18
-rw-r--r--doc/user/project/integrations/builds_emails.md15
-rw-r--r--doc/user/project/integrations/emails_on_push.md20
-rw-r--r--doc/user/project/integrations/hipchat.md53
-rw-r--r--doc/user/project/integrations/img/accessing_integrations.pngbin0 -> 8941 bytes
-rw-r--r--doc/user/project/integrations/img/emails_on_push_service.png (renamed from doc/project_services/img/emails_on_push_service.png)bin28535 -> 28535 bytes
-rw-r--r--doc/user/project/integrations/img/jira_add_user_to_group.png (renamed from doc/project_services/img/jira_add_user_to_group.png)bin24838 -> 24838 bytes
-rw-r--r--doc/user/project/integrations/img/jira_create_new_group.png (renamed from doc/project_services/img/jira_create_new_group.png)bin19127 -> 19127 bytes
-rw-r--r--doc/user/project/integrations/img/jira_create_new_group_name.png (renamed from doc/project_services/img/jira_create_new_group_name.png)bin5168 -> 5168 bytes
-rw-r--r--doc/user/project/integrations/img/jira_create_new_user.png (renamed from doc/project_services/img/jira_create_new_user.png)bin12625 -> 12625 bytes
-rw-r--r--doc/user/project/integrations/img/jira_group_access.png (renamed from doc/project_services/img/jira_group_access.png)bin19235 -> 19235 bytes
-rw-r--r--doc/user/project/integrations/img/jira_issue_reference.png (renamed from doc/project_services/img/jira_issue_reference.png)bin18399 -> 18399 bytes
-rw-r--r--doc/user/project/integrations/img/jira_merge_request_close.png (renamed from doc/project_services/img/jira_merge_request_close.png)bin21172 -> 21172 bytes
-rw-r--r--doc/user/project/integrations/img/jira_project_name.png (renamed from doc/project_services/img/jira_project_name.png)bin26685 -> 26685 bytes
-rw-r--r--doc/user/project/integrations/img/jira_service.png (renamed from doc/project_services/img/jira_service.png)bin37869 -> 37869 bytes
-rw-r--r--doc/user/project/integrations/img/jira_service_close_comment.png (renamed from doc/project_services/img/jira_service_close_comment.png)bin11893 -> 11893 bytes
-rw-r--r--doc/user/project/integrations/img/jira_service_close_issue.png (renamed from doc/project_services/img/jira_service_close_issue.png)bin30570 -> 30570 bytes
-rw-r--r--doc/user/project/integrations/img/jira_service_page.png (renamed from doc/project_services/img/jira_service_page.png)bin12228 -> 12228 bytes
-rw-r--r--doc/user/project/integrations/img/jira_user_management_link.png (renamed from doc/project_services/img/jira_user_management_link.png)bin23921 -> 23921 bytes
-rw-r--r--doc/user/project/integrations/img/jira_workflow_screenshot.png (renamed from doc/project_services/img/jira_workflow_screenshot.png)bin66685 -> 66685 bytes
-rw-r--r--doc/user/project/integrations/img/kubernetes_configuration.png (renamed from doc/project_services/img/kubernetes_configuration.png)bin113827 -> 113827 bytes
-rw-r--r--doc/user/project/integrations/img/mattermost_add_slash_command.png (renamed from doc/project_services/img/mattermost_add_slash_command.png)bin9265 -> 9265 bytes
-rw-r--r--doc/user/project/integrations/img/mattermost_bot_auth.png (renamed from doc/project_services/img/mattermost_bot_auth.png)bin8676 -> 8676 bytes
-rw-r--r--doc/user/project/integrations/img/mattermost_bot_available_commands.png (renamed from doc/project_services/img/mattermost_bot_available_commands.png)bin4647 -> 4647 bytes
-rw-r--r--doc/user/project/integrations/img/mattermost_config_help.pngbin0 -> 102890 bytes
-rw-r--r--doc/user/project/integrations/img/mattermost_configuration.png (renamed from doc/project_services/img/mattermost_configuration.png)bin73502 -> 73502 bytes
-rw-r--r--doc/user/project/integrations/img/mattermost_console_integrations.png (renamed from doc/project_services/img/mattermost_console_integrations.png)bin314642 -> 314642 bytes
-rw-r--r--doc/user/project/integrations/img/mattermost_gitlab_token.png (renamed from doc/project_services/img/mattermost_gitlab_token.png)bin3688 -> 3688 bytes
-rw-r--r--doc/user/project/integrations/img/mattermost_goto_console.png (renamed from doc/project_services/img/mattermost_goto_console.png)bin7754 -> 7754 bytes
-rw-r--r--doc/user/project/integrations/img/mattermost_slash_command_configuration.png (renamed from doc/project_services/img/mattermost_slash_command_configuration.png)bin24169 -> 24169 bytes
-rw-r--r--doc/user/project/integrations/img/mattermost_slash_command_token.png (renamed from doc/project_services/img/mattermost_slash_command_token.png)bin8624 -> 8624 bytes
-rw-r--r--doc/user/project/integrations/img/mattermost_team_integrations.png (renamed from doc/project_services/img/mattermost_team_integrations.png)bin4766 -> 4766 bytes
-rw-r--r--doc/user/project/integrations/img/project_services.pngbin0 -> 25753 bytes
-rw-r--r--doc/user/project/integrations/img/redmine_configuration.png (renamed from doc/project_services/img/redmine_configuration.png)bin10266 -> 10266 bytes
-rw-r--r--doc/user/project/integrations/img/services_templates_redmine_example.png (renamed from doc/project_services/img/services_templates_redmine_example.png)bin8776 -> 8776 bytes
-rw-r--r--doc/user/project/integrations/img/slack_configuration.png (renamed from doc/project_services/img/slack_configuration.png)bin29825 -> 29825 bytes
-rw-r--r--doc/user/project/integrations/img/slack_setup.pngbin0 -> 86314 bytes
-rw-r--r--doc/user/project/integrations/img/webhooks_ssl.png (renamed from doc/web_hooks/ssl.png)bin27799 -> 27799 bytes
-rw-r--r--doc/user/project/integrations/index.md26
-rw-r--r--doc/user/project/integrations/irker.md50
-rw-r--r--doc/user/project/integrations/jira.md209
-rw-r--r--doc/user/project/integrations/kubernetes.md66
-rw-r--r--doc/user/project/integrations/mattermost.md46
-rw-r--r--doc/user/project/integrations/mattermost_slash_commands.md164
-rw-r--r--doc/user/project/integrations/project_services.md76
-rw-r--r--doc/user/project/integrations/redmine.md23
-rw-r--r--doc/user/project/integrations/services_templates.md25
-rw-r--r--doc/user/project/integrations/slack.md51
-rw-r--r--doc/user/project/integrations/slack_slash_commands.md24
-rw-r--r--doc/user/project/integrations/webhooks.md1028
-rw-r--r--doc/user/project/issues/confidential_issues.md68
-rw-r--r--doc/user/project/issues/due_dates.md37
-rw-r--r--doc/user/project/issues/img/confidential_issues_create.pngbin0 -> 9659 bytes
-rw-r--r--doc/user/project/issues/img/confidential_issues_index_page.pngbin0 -> 9949 bytes
-rw-r--r--doc/user/project/issues/img/confidential_issues_issue_page.pngbin0 -> 16089 bytes
-rw-r--r--doc/user/project/issues/img/confidential_issues_search_guest.pngbin0 -> 10014 bytes
-rw-r--r--doc/user/project/issues/img/confidential_issues_search_master.pngbin0 -> 15332 bytes
-rw-r--r--doc/user/project/issues/img/confidential_issues_system_notes.pngbin0 -> 3025 bytes
-rw-r--r--doc/user/project/issues/img/due_dates_create.pngbin0 -> 7705 bytes
-rw-r--r--doc/user/project/issues/img/due_dates_edit_sidebar.pngbin0 -> 2424 bytes
-rw-r--r--doc/user/project/issues/img/due_dates_issues_index_page.pngbin0 -> 21402 bytes
-rw-r--r--doc/user/project/issues/img/due_dates_todos.pngbin0 -> 5644 bytes
-rw-r--r--doc/user/project/merge_requests/img/merge_conflict_editor.pngbin0 -> 50422 bytes
-rw-r--r--doc/user/project/merge_requests/img/resolve_discussion_issue_notice.pngbin0 -> 11123 bytes
-rw-r--r--doc/user/project/merge_requests/img/resolve_discussion_open_issue.pngbin0 -> 20967 bytes
-rw-r--r--doc/user/project/merge_requests/merge_request_discussion_resolution.md8
-rw-r--r--doc/user/project/merge_requests/resolve_conflicts.md12
-rw-r--r--doc/user/project/pages/img/pages_create_project.pngbin0 -> 33597 bytes
-rw-r--r--doc/user/project/pages/img/pages_create_user_page.pngbin0 -> 87071 bytes
-rw-r--r--doc/user/project/pages/img/pages_dns_details.pngbin0 -> 34686 bytes
-rw-r--r--doc/user/project/pages/img/pages_multiple_domains.pngbin0 -> 63716 bytes
-rw-r--r--doc/user/project/pages/img/pages_new_domain_button.pngbin0 -> 51136 bytes
-rw-r--r--doc/user/project/pages/img/pages_remove.pngbin0 -> 27259 bytes
-rw-r--r--doc/user/project/pages/img/pages_upload_cert.pngbin0 -> 103730 bytes
-rw-r--r--doc/user/project/pages/index.md435
-rw-r--r--doc/user/project/repository/web_editor.md3
-rw-r--r--doc/user/project/settings/import_export.md11
-rw-r--r--doc/user/project/slash_commands.md6
-rw-r--r--doc/web_hooks/web_hooks.md1026
-rw-r--r--doc/workflow/README.md5
-rw-r--r--doc/workflow/importing/import_projects_from_bitbucket.md2
-rw-r--r--doc/workflow/importing/import_projects_from_gitea.md5
-rw-r--r--doc/workflow/importing/import_projects_from_github.md2
-rw-r--r--doc/workflow/importing/import_projects_from_gitlab_com.md3
-rw-r--r--doc/workflow/importing/migrating_from_svn.md2
-rw-r--r--doc/workflow/lfs/lfs_administration.md8
-rw-r--r--doc/workflow/lfs/manage_large_binaries_with_git_lfs.md1
-rw-r--r--doc/workflow/notifications.md2
-rw-r--r--doc/workflow/time-tracking/time-tracking-example.pngbin0 -> 48350 bytes
-rw-r--r--doc/workflow/time-tracking/time-tracking-sidebar.pngbin0 -> 19467 bytes
-rw-r--r--doc/workflow/time_tracking.md73
-rw-r--r--features/admin/groups.feature49
-rw-r--r--features/admin/users.feature65
-rw-r--r--features/dashboard/active_tab.feature24
-rw-r--r--features/dashboard/archived_projects.feature17
-rw-r--r--features/dashboard/group.feature13
-rw-r--r--features/dashboard/help.feature9
-rw-r--r--features/dashboard/shortcuts.feature21
-rw-r--r--features/project/active_tab.feature13
-rw-r--r--features/project/commits/commits.feature2
-rw-r--r--features/project/issues/filter_labels.feature28
-rw-r--r--features/project/issues/issues.feature56
-rw-r--r--features/project/pages.feature82
-rw-r--r--features/steps/admin/groups.rb143
-rw-r--r--features/steps/admin/users.rb167
-rw-r--r--features/steps/dashboard/active_tab.rb5
-rw-r--r--features/steps/dashboard/archived_projects.rb26
-rw-r--r--features/steps/dashboard/dashboard.rb6
-rw-r--r--features/steps/dashboard/group.rb25
-rw-r--r--features/steps/dashboard/help.rb21
-rw-r--r--features/steps/dashboard/issues.rb4
-rw-r--r--features/steps/dashboard/merge_requests.rb4
-rw-r--r--features/steps/dashboard/shortcuts.rb7
-rw-r--r--features/steps/dashboard/todos.rb45
-rw-r--r--features/steps/group/milestones.rb2
-rw-r--r--features/steps/groups.rb2
-rw-r--r--features/steps/profile/profile.rb2
-rw-r--r--features/steps/project/active_tab.rb16
-rw-r--r--features/steps/project/builds/summary.rb2
-rw-r--r--features/steps/project/commits/commits.rb9
-rw-r--r--features/steps/project/deploy_keys.rb4
-rw-r--r--features/steps/project/fork.rb4
-rw-r--r--features/steps/project/forked_merge_requests.rb2
-rw-r--r--features/steps/project/graph.rb6
-rw-r--r--features/steps/project/hooks.rb6
-rw-r--r--features/steps/project/issues/labels.rb9
-rw-r--r--features/steps/project/merge_requests.rb3
-rw-r--r--features/steps/project/merge_requests/acceptance.rb2
-rw-r--r--features/steps/project/merge_requests/revert.rb7
-rw-r--r--features/steps/project/pages.rb139
-rw-r--r--features/steps/project/redirects.rb4
-rw-r--r--features/steps/project/services.rb2
-rw-r--r--features/steps/project/source/browse_files.rb2
-rw-r--r--features/steps/project/source/markdown_render.rb2
-rw-r--r--features/steps/project/team_management.rb8
-rw-r--r--features/steps/shared/admin.rb2
-rw-r--r--features/steps/shared/builds.rb4
-rw-r--r--features/steps/shared/group.rb2
-rw-r--r--features/steps/shared/paths.rb6
-rw-r--r--features/steps/shared/project.rb24
-rw-r--r--features/steps/user.rb2
-rw-r--r--features/support/env.rb3
-rw-r--r--lib/api/api.rb17
-rw-r--r--lib/api/boards.rb2
-rw-r--r--lib/api/branches.rb7
-rw-r--r--lib/api/builds.rb2
-rw-r--r--lib/api/commit_statuses.rb2
-rw-r--r--lib/api/commits.rb5
-rw-r--r--lib/api/deploy_keys.rb167
-rw-r--r--lib/api/entities.rb55
-rw-r--r--lib/api/files.rb4
-rw-r--r--lib/api/groups.rb37
-rw-r--r--lib/api/helpers.rb55
-rw-r--r--lib/api/helpers/internal_helpers.rb4
-rw-r--r--lib/api/helpers/pagination.rb45
-rw-r--r--lib/api/internal.rb8
-rw-r--r--lib/api/issues.rb60
-rw-r--r--lib/api/merge_request_diffs.rb8
-rw-r--r--lib/api/merge_requests.rb291
-rw-r--r--lib/api/notes.rb28
-rw-r--r--lib/api/project_hooks.rb2
-rw-r--r--lib/api/project_snippets.rb2
-rw-r--r--lib/api/projects.rb106
-rw-r--r--lib/api/services.rb72
-rw-r--r--lib/api/settings.rb124
-rw-r--r--lib/api/snippets.rb2
-rw-r--r--lib/api/subscriptions.rb4
-rw-r--r--lib/api/time_tracking_endpoints.rb114
-rw-r--r--lib/api/todos.rb2
-rw-r--r--lib/api/users.rb15
-rw-r--r--lib/api/v3/deploy_keys.rb122
-rw-r--r--lib/api/v3/entities.rb16
-rw-r--r--lib/api/v3/issues.rb231
-rw-r--r--lib/api/v3/merge_requests.rb280
-rw-r--r--lib/api/v3/project_snippets.rb135
-rw-r--r--lib/api/v3/projects.rb458
-rw-r--r--lib/backup/manager.rb46
-rw-r--r--lib/backup/pages.rb13
-rw-r--r--lib/banzai/cross_project_reference.rb2
-rw-r--r--lib/banzai/filter/abstract_reference_filter.rb24
-rw-r--r--lib/banzai/filter/external_link_filter.rb2
-rw-r--r--lib/banzai/filter/issue_reference_filter.rb2
-rw-r--r--lib/banzai/filter/math_filter.rb11
-rw-r--r--lib/banzai/filter/plantuml_filter.rb39
-rw-r--r--lib/banzai/filter/reference_filter.rb12
-rw-r--r--lib/banzai/filter/syntax_highlight_filter.rb6
-rw-r--r--lib/banzai/filter/user_reference_filter.rb4
-rw-r--r--lib/banzai/filter/video_link_filter.rb3
-rw-r--r--lib/banzai/pipeline/gfm_pipeline.rb7
-rw-r--r--lib/banzai/renderer.rb4
-rw-r--r--lib/ci/ansi2html.rb2
-rw-r--r--lib/ci/api/api.rb10
-rw-r--r--lib/ci/api/builds.rb32
-rw-r--r--lib/ci/api/helpers.rb2
-rw-r--r--lib/ci/gitlab_ci_yaml_processor.rb1
-rw-r--r--lib/constraints/project_url_constrainer.rb2
-rw-r--r--lib/email_template_interceptor.rb4
-rw-r--r--lib/gitlab/asciidoc.rb12
-rw-r--r--lib/gitlab/auth.rb11
-rw-r--r--lib/gitlab/auth/result.rb3
-rw-r--r--lib/gitlab/chat_commands/base_command.rb4
-rw-r--r--lib/gitlab/chat_commands/command.rb35
-rw-r--r--lib/gitlab/chat_commands/deploy.rb23
-rw-r--r--lib/gitlab/chat_commands/help.rb28
-rw-r--r--lib/gitlab/chat_commands/issue_create.rb26
-rw-r--r--lib/gitlab/chat_commands/issue_new.rb42
-rw-r--r--lib/gitlab/chat_commands/issue_search.rb8
-rw-r--r--lib/gitlab/chat_commands/issue_show.rb8
-rw-r--r--lib/gitlab/chat_commands/presenter.rb131
-rw-r--r--lib/gitlab/chat_commands/presenters/access.rb40
-rw-r--r--lib/gitlab/chat_commands/presenters/base.rb77
-rw-r--r--lib/gitlab/chat_commands/presenters/deploy.rb21
-rw-r--r--lib/gitlab/chat_commands/presenters/help.rb27
-rw-r--r--lib/gitlab/chat_commands/presenters/issuable.rb43
-rw-r--r--lib/gitlab/chat_commands/presenters/issue_new.rb50
-rw-r--r--lib/gitlab/chat_commands/presenters/issue_search.rb47
-rw-r--r--lib/gitlab/chat_commands/presenters/issue_show.rb61
-rw-r--r--lib/gitlab/checks/change_access.rb15
-rw-r--r--lib/gitlab/ci/config/entry/coverage.rb22
-rw-r--r--lib/gitlab/ci/config/entry/environment.rb1
-rw-r--r--lib/gitlab/ci/config/entry/job.rb8
-rw-r--r--lib/gitlab/ci/config/entry/legacy_validation_helpers.rb10
-rw-r--r--lib/gitlab/ci/config/entry/trigger.rb10
-rw-r--r--lib/gitlab/ci/config/entry/validators.rb45
-rw-r--r--lib/gitlab/ci/status/build/cancelable.rb2
-rw-r--r--lib/gitlab/ci/status/build/factory.rb7
-rw-r--r--lib/gitlab/ci/status/build/failed_allowed.rb27
-rw-r--r--lib/gitlab/ci/status/build/play.rb6
-rw-r--r--lib/gitlab/ci/status/build/retryable.rb2
-rw-r--r--lib/gitlab/ci/status/build/stop.rb2
-rw-r--r--lib/gitlab/ci/status/core.rb3
-rw-r--r--lib/gitlab/ci/status/external/common.rb22
-rw-r--r--lib/gitlab/ci/status/external/factory.rb13
-rw-r--r--lib/gitlab/ci/status/factory.rb43
-rw-r--r--lib/gitlab/ci/status/pipeline/factory.rb2
-rw-r--r--lib/gitlab/ci/status/pipeline/success_with_warnings.rb31
-rw-r--r--lib/gitlab/ci/status/stage/factory.rb4
-rw-r--r--lib/gitlab/ci/status/success_warning.rb33
-rw-r--r--lib/gitlab/ci/trace_reader.rb1
-rw-r--r--lib/gitlab/current_settings.rb43
-rw-r--r--lib/gitlab/cycle_analytics/base_event.rb57
-rw-r--r--lib/gitlab/cycle_analytics/base_event_fetcher.rb65
-rw-r--r--lib/gitlab/cycle_analytics/base_query.rb31
-rw-r--r--lib/gitlab/cycle_analytics/base_stage.rb54
-rw-r--r--lib/gitlab/cycle_analytics/code_event.rb28
-rw-r--r--lib/gitlab/cycle_analytics/code_event_fetcher.rb25
-rw-r--r--lib/gitlab/cycle_analytics/code_stage.rb21
-rw-r--r--lib/gitlab/cycle_analytics/event_fetcher.rb9
-rw-r--r--lib/gitlab/cycle_analytics/events.rb38
-rw-r--r--lib/gitlab/cycle_analytics/events_query.rb37
-rw-r--r--lib/gitlab/cycle_analytics/issue_event.rb27
-rw-r--r--lib/gitlab/cycle_analytics/issue_event_fetcher.rb23
-rw-r--r--lib/gitlab/cycle_analytics/issue_stage.rb22
-rw-r--r--lib/gitlab/cycle_analytics/metrics_fetcher.rb60
-rw-r--r--lib/gitlab/cycle_analytics/plan_event.rb46
-rw-r--r--lib/gitlab/cycle_analytics/plan_event_fetcher.rb44
-rw-r--r--lib/gitlab/cycle_analytics/plan_stage.rb22
-rw-r--r--lib/gitlab/cycle_analytics/production_event.rb26
-rw-r--r--lib/gitlab/cycle_analytics/production_event_fetcher.rb6
-rw-r--r--lib/gitlab/cycle_analytics/production_helper.rb9
-rw-r--r--lib/gitlab/cycle_analytics/production_stage.rb28
-rw-r--r--lib/gitlab/cycle_analytics/review_event.rb25
-rw-r--r--lib/gitlab/cycle_analytics/review_event_fetcher.rb22
-rw-r--r--lib/gitlab/cycle_analytics/review_stage.rb21
-rw-r--r--lib/gitlab/cycle_analytics/stage.rb9
-rw-r--r--lib/gitlab/cycle_analytics/stage_summary.rb23
-rw-r--r--lib/gitlab/cycle_analytics/staging_event.rb31
-rw-r--r--lib/gitlab/cycle_analytics/staging_event_fetcher.rb30
-rw-r--r--lib/gitlab/cycle_analytics/staging_stage.rb22
-rw-r--r--lib/gitlab/cycle_analytics/summary/base.rb20
-rw-r--r--lib/gitlab/cycle_analytics/summary/commit.rb39
-rw-r--r--lib/gitlab/cycle_analytics/summary/deploy.rb11
-rw-r--r--lib/gitlab/cycle_analytics/summary/issue.rb21
-rw-r--r--lib/gitlab/cycle_analytics/test_event.rb13
-rw-r--r--lib/gitlab/cycle_analytics/test_event_fetcher.rb6
-rw-r--r--lib/gitlab/cycle_analytics/test_stage.rb29
-rw-r--r--lib/gitlab/database.rb14
-rw-r--r--lib/gitlab/database/median.rb5
-rw-r--r--lib/gitlab/diff/file_collection/merge_request_diff.rb5
-rw-r--r--lib/gitlab/email/handler.rb3
-rw-r--r--lib/gitlab/email/handler/base_handler.rb43
-rw-r--r--lib/gitlab/email/handler/create_issue_handler.rb3
-rw-r--r--lib/gitlab/email/handler/create_note_handler.rb7
-rw-r--r--lib/gitlab/email/handler/reply_processing.rb54
-rw-r--r--lib/gitlab/email/handler/unsubscribe_handler.rb32
-rw-r--r--lib/gitlab/email/receiver.rb21
-rw-r--r--lib/gitlab/email/reply_parser.rb36
-rw-r--r--lib/gitlab/gfm/reference_rewriter.rb2
-rw-r--r--lib/gitlab/git.rb2
-rw-r--r--lib/gitlab/git/attributes.rb131
-rw-r--r--lib/gitlab/git/blame.rb75
-rw-r--r--lib/gitlab/git/blob.rb330
-rw-r--r--lib/gitlab/git/blob_snippet.rb32
-rw-r--r--lib/gitlab/git/branch.rb6
-rw-r--r--lib/gitlab/git/commit.rb310
-rw-r--r--lib/gitlab/git/commit_stats.rb26
-rw-r--r--lib/gitlab/git/compare.rb43
-rw-r--r--lib/gitlab/git/diff.rb322
-rw-r--r--lib/gitlab/git/diff_collection.rb129
-rw-r--r--lib/gitlab/git/encoding_helper.rb58
-rw-r--r--lib/gitlab/git/path_helper.rb16
-rw-r--r--lib/gitlab/git/popen.rb26
-rw-r--r--lib/gitlab/git/ref.rb49
-rw-r--r--lib/gitlab/git/repository.rb1251
-rw-r--r--lib/gitlab/git/tag.rb17
-rw-r--r--lib/gitlab/git/tree.rb104
-rw-r--r--lib/gitlab/git/util.rb18
-rw-r--r--lib/gitlab/git_access.rb152
-rw-r--r--lib/gitlab/git_access_wiki.rb4
-rw-r--r--lib/gitlab/git_post_receive.rb4
-rw-r--r--lib/gitlab/github_import/project_creator.rb4
-rw-r--r--lib/gitlab/gon_helper.rb1
-rw-r--r--lib/gitlab/import_export.rb2
-rw-r--r--lib/gitlab/import_export/import_export.yml3
-rw-r--r--lib/gitlab/import_export/members_mapper.rb14
-rw-r--r--lib/gitlab/import_export/project_tree_saver.rb27
-rw-r--r--lib/gitlab/import_export/reader.rb4
-rw-r--r--lib/gitlab/import_export/relation_factory.rb64
-rw-r--r--lib/gitlab/import_sources.rb2
-rw-r--r--lib/gitlab/incoming_email.rb21
-rw-r--r--lib/gitlab/job_waiter.rb27
-rw-r--r--lib/gitlab/kubernetes.rb4
-rw-r--r--lib/gitlab/ldap/access.rb26
-rw-r--r--lib/gitlab/ldap/auth_hash.rb2
-rw-r--r--lib/gitlab/ldap/config.rb12
-rw-r--r--lib/gitlab/ldap/person.rb17
-rw-r--r--lib/gitlab/metrics/rack_middleware.rb15
-rw-r--r--lib/gitlab/middleware/multipart.rb8
-rw-r--r--lib/gitlab/middleware/webpack_proxy.rb24
-rw-r--r--lib/gitlab/pages_transfer.rb7
-rw-r--r--lib/gitlab/project_search_results.rb28
-rw-r--r--lib/gitlab/project_transfer.rb35
-rw-r--r--lib/gitlab/recaptcha.rb4
-rw-r--r--lib/gitlab/redis.rb2
-rw-r--r--lib/gitlab/regex.rb4
-rw-r--r--lib/gitlab/request_profiler/middleware.rb3
-rw-r--r--lib/gitlab/route_map.rb50
-rw-r--r--lib/gitlab/search_results.rb4
-rw-r--r--lib/gitlab/serialize/ci/variables.rb27
-rw-r--r--lib/gitlab/serializer/ci/variables.rb27
-rw-r--r--lib/gitlab/serializer/pagination.rb36
-rw-r--r--lib/gitlab/shell.rb (renamed from lib/gitlab/backend/shell.rb)0
-rw-r--r--lib/gitlab/shell_adapter.rb (renamed from lib/gitlab/backend/shell_adapter.rb)0
-rw-r--r--lib/gitlab/sidekiq_status.rb66
-rw-r--r--lib/gitlab/sidekiq_status/client_middleware.rb10
-rw-r--r--lib/gitlab/sidekiq_status/server_middleware.rb13
-rw-r--r--lib/gitlab/template/gitlab_ci_yml_template.rb10
-rw-r--r--lib/gitlab/themes.rb2
-rw-r--r--lib/gitlab/time_tracking_formatter.rb34
-rw-r--r--lib/gitlab/update_path_error.rb3
-rw-r--r--lib/gitlab/upgrader.rb2
-rw-r--r--lib/gitlab/uploads_transfer.rb30
-rw-r--r--lib/gitlab/user_access.rb20
-rw-r--r--lib/gitlab/view/presenter/base.rb30
-rw-r--r--lib/gitlab/view/presenter/delegated.rb23
-rw-r--r--lib/gitlab/view/presenter/factory.rb24
-rw-r--r--lib/gitlab/view/presenter/simple.rb17
-rw-r--r--lib/gitlab/visibility_level.rb17
-rw-r--r--lib/gitlab/workhorse.rb12
-rw-r--r--lib/mattermost/client.rb51
-rw-r--r--lib/mattermost/command.rb10
-rw-r--r--lib/mattermost/error.rb3
-rw-r--r--lib/mattermost/session.rb63
-rw-r--r--lib/mattermost/team.rb7
-rw-r--r--lib/rouge/lexers/plantuml.rb21
-rwxr-xr-xlib/support/deploy/deploy.sh4
-rwxr-xr-xlib/support/init.d/gitlab68
-rw-r--r--[-rwxr-xr-x]lib/support/init.d/gitlab.default.example24
-rw-r--r--lib/support/nginx/gitlab-pages28
-rw-r--r--lib/support/nginx/gitlab-pages-ssl77
-rw-r--r--lib/tasks/config_lint.rake25
-rw-r--r--lib/tasks/dev.rake5
-rw-r--r--lib/tasks/gitlab/assets.rake48
-rw-r--r--lib/tasks/gitlab/backup.rake21
-rw-r--r--lib/tasks/gitlab/cleanup.rake2
-rw-r--r--lib/tasks/gitlab/git.rake8
-rw-r--r--lib/tasks/gitlab/import.rake5
-rw-r--r--lib/tasks/gitlab/info.rake5
-rw-r--r--lib/tasks/gitlab/sidekiq.rake2
-rw-r--r--lib/tasks/gitlab/test.rake2
-rw-r--r--lib/tasks/gitlab/update_commit_count.rake20
-rw-r--r--lib/tasks/gitlab/update_templates.rake2
-rw-r--r--lib/tasks/grape.rake6
-rw-r--r--lib/tasks/karma.rake25
-rw-r--r--lib/tasks/teaspoon.rake25
-rw-r--r--lib/tasks/test.rake2
-rw-r--r--package.json39
-rw-r--r--public/robots.txt5
-rw-r--r--public/slash-command-logo.pngbin0 -> 9509 bytes
-rw-r--r--rubocop/cop/gem_fetcher.rb28
-rw-r--r--rubocop/rubocop.rb1
-rwxr-xr-xscripts/notify_slack.sh2
-rw-r--r--shared/pages/.gitkeep0
-rw-r--r--spec/controllers/admin/groups_controller_spec.rb2
-rw-r--r--spec/controllers/admin/projects_controller_spec.rb2
-rw-r--r--spec/controllers/admin/services_controller_spec.rb26
-rw-r--r--spec/controllers/autocomplete_controller_spec.rb2
-rw-r--r--spec/controllers/blob_controller_spec.rb2
-rw-r--r--spec/controllers/ci/projects_controller_spec.rb2
-rw-r--r--spec/controllers/dashboard/todos_controller_spec.rb37
-rw-r--r--spec/controllers/explore/projects_controller_spec.rb27
-rw-r--r--spec/controllers/groups/milestones_controller_spec.rb2
-rw-r--r--spec/controllers/groups_controller_spec.rb23
-rw-r--r--spec/controllers/health_check_controller_spec.rb6
-rw-r--r--spec/controllers/import/bitbucket_controller_spec.rb4
-rw-r--r--spec/controllers/import/fogbugz_controller_spec.rb4
-rw-r--r--spec/controllers/import/gitlab_controller_spec.rb4
-rw-r--r--spec/controllers/import/google_code_controller_spec.rb4
-rw-r--r--spec/controllers/notification_settings_controller_spec.rb2
-rw-r--r--spec/controllers/projects/avatars_controller_spec.rb2
-rw-r--r--spec/controllers/projects/blame_controller_spec.rb2
-rw-r--r--spec/controllers/projects/blob_controller_spec.rb32
-rw-r--r--spec/controllers/projects/boards/issues_controller_spec.rb76
-rw-r--r--spec/controllers/projects/boards/lists_controller_spec.rb2
-rw-r--r--spec/controllers/projects/branches_controller_spec.rb2
-rw-r--r--spec/controllers/projects/commit_controller_spec.rb52
-rw-r--r--spec/controllers/projects/commits_controller_spec.rb2
-rw-r--r--spec/controllers/projects/compare_controller_spec.rb32
-rw-r--r--spec/controllers/projects/cycle_analytics_controller_spec.rb2
-rw-r--r--spec/controllers/projects/discussions_controller_spec.rb6
-rw-r--r--spec/controllers/projects/find_file_controller_spec.rb2
-rw-r--r--spec/controllers/projects/forks_controller_spec.rb2
-rw-r--r--spec/controllers/projects/graphs_controller_spec.rb2
-rw-r--r--spec/controllers/projects/group_links_controller_spec.rb8
-rw-r--r--spec/controllers/projects/issues_controller_spec.rb163
-rw-r--r--spec/controllers/projects/labels_controller_spec.rb45
-rw-r--r--spec/controllers/projects/mattermosts_controller_spec.rb58
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb172
-rw-r--r--spec/controllers/projects/milestones_controller_spec.rb2
-rw-r--r--spec/controllers/projects/notes_controller_spec.rb52
-rw-r--r--spec/controllers/projects/pages_domains_controller_spec.rb64
-rw-r--r--spec/controllers/projects/pipelines_controller_spec.rb22
-rw-r--r--spec/controllers/projects/project_members_controller_spec.rb18
-rw-r--r--spec/controllers/projects/raw_controller_spec.rb2
-rw-r--r--spec/controllers/projects/refs_controller_spec.rb2
-rw-r--r--spec/controllers/projects/releases_controller_spec.rb2
-rw-r--r--spec/controllers/projects/repositories_controller_spec.rb2
-rw-r--r--spec/controllers/projects/services_controller_spec.rb3
-rw-r--r--spec/controllers/projects/settings/ci_cd_controller_spec.rb20
-rw-r--r--spec/controllers/projects/settings/integrations_controller_spec.rb20
-rw-r--r--spec/controllers/projects/settings/members_controller_spec.rb14
-rw-r--r--spec/controllers/projects/snippets_controller_spec.rb106
-rw-r--r--spec/controllers/projects/tags_controller_spec.rb2
-rw-r--r--spec/controllers/projects/templates_controller_spec.rb5
-rw-r--r--spec/controllers/projects/tree_controller_spec.rb2
-rw-r--r--spec/controllers/projects/uploads_controller_spec.rb2
-rw-r--r--spec/controllers/projects/variables_controller_spec.rb59
-rw-r--r--spec/controllers/projects_controller_spec.rb48
-rw-r--r--spec/controllers/registrations_controller_spec.rb60
-rw-r--r--spec/controllers/search_controller_spec.rb16
-rw-r--r--spec/controllers/snippets_controller_spec.rb59
-rw-r--r--spec/controllers/uploads_controller_spec.rb2
-rw-r--r--spec/controllers/users_controller_spec.rb4
-rw-r--r--spec/db/production/settings.rb5
-rw-r--r--spec/factories/boards.rb1
-rw-r--r--spec/factories/ci/pipelines.rb8
-rw-r--r--spec/factories/ci/runners.rb8
-rw-r--r--spec/factories/ci/stages.rb3
-rw-r--r--spec/factories/deploy_keys_projects.rb2
-rw-r--r--spec/factories/events.rb14
-rw-r--r--spec/factories/file_uploader.rb2
-rw-r--r--spec/factories/groups.rb3
-rw-r--r--spec/factories/issues.rb2
-rw-r--r--spec/factories/labels.rb2
-rw-r--r--spec/factories/lfs_objects.rb2
-rw-r--r--spec/factories/lists.rb6
-rw-r--r--spec/factories/merge_requests.rb2
-rw-r--r--spec/factories/milestones.rb2
-rw-r--r--spec/factories/notes.rb20
-rw-r--r--spec/factories/pages_domains.rb153
-rw-r--r--spec/factories/project_group_links.rb2
-rw-r--r--spec/factories/project_members.rb2
-rw-r--r--spec/factories/project_snippets.rb2
-rw-r--r--spec/factories/project_statistics.rb6
-rw-r--r--spec/factories/projects.rb63
-rw-r--r--spec/factories/releases.rb2
-rw-r--r--spec/factories/sent_notifications.rb2
-rw-r--r--spec/factories/services.rb2
-rw-r--r--spec/factories/timelogs.rb9
-rw-r--r--spec/factories/todos.rb7
-rw-r--r--spec/factories/trending_project.rb6
-rw-r--r--spec/features/admin/admin_builds_spec.rb34
-rw-r--r--spec/features/admin/admin_deploy_keys_spec.rb37
-rw-r--r--spec/features/admin/admin_disables_git_access_protocol_spec.rb3
-rw-r--r--spec/features/admin/admin_groups_spec.rb123
-rw-r--r--spec/features/admin/admin_health_check_spec.rb9
-rw-r--r--spec/features/admin/admin_projects_spec.rb2
-rw-r--r--spec/features/admin/admin_runners_spec.rb3
-rw-r--r--spec/features/admin/admin_settings_spec.rb5
-rw-r--r--spec/features/admin/admin_users_spec.rb192
-rw-r--r--spec/features/admin/admin_uses_repository_checks_spec.rb9
-rw-r--r--spec/features/auto_deploy_spec.rb64
-rw-r--r--spec/features/boards/add_issues_modal_spec.rb233
-rw-r--r--spec/features/boards/boards_spec.rb221
-rw-r--r--spec/features/boards/modal_filter_spec.rb259
-rw-r--r--spec/features/boards/new_issue_spec.rb5
-rw-r--r--spec/features/boards/sidebar_spec.rb111
-rw-r--r--spec/features/ci_lint_spec.rb9
-rw-r--r--spec/features/copy_as_gfm_spec.rb432
-rw-r--r--spec/features/cycle_analytics_spec.rb125
-rw-r--r--spec/features/dashboard/active_tab_spec.rb46
-rw-r--r--spec/features/dashboard/archived_projects_spec.rb28
-rw-r--r--spec/features/dashboard/datetime_on_tooltips_spec.rb2
-rw-r--r--spec/features/dashboard/group_spec.rb20
-rw-r--r--spec/features/dashboard/help_spec.rb17
-rw-r--r--spec/features/dashboard/shortcuts_spec.rb29
-rw-r--r--spec/features/environment_spec.rb52
-rw-r--r--spec/features/environments_spec.rb18
-rw-r--r--spec/features/expand_collapse_diffs_spec.rb29
-rw-r--r--spec/features/groups/merge_requests_spec.rb2
-rw-r--r--spec/features/groups_spec.rb13
-rw-r--r--spec/features/issuables/default_sort_order_spec.rb10
-rw-r--r--spec/features/issues/award_emoji_spec.rb2
-rw-r--r--spec/features/issues/filter_by_labels_spec.rb152
-rw-r--r--spec/features/issues/filter_by_milestone_spec.rb91
-rw-r--r--spec/features/issues/filter_issues_spec.rb384
-rw-r--r--spec/features/issues/filtered_search/dropdown_assignee_spec.rb190
-rw-r--r--spec/features/issues/filtered_search/dropdown_author_spec.rb178
-rw-r--r--spec/features/issues/filtered_search/dropdown_hint_spec.rb134
-rw-r--r--spec/features/issues/filtered_search/dropdown_label_spec.rb287
-rw-r--r--spec/features/issues/filtered_search/dropdown_milestone_spec.rb239
-rw-r--r--spec/features/issues/filtered_search/filter_issues_spec.rb804
-rw-r--r--spec/features/issues/filtered_search/search_bar_spec.rb104
-rw-r--r--spec/features/issues/form_spec.rb16
-rw-r--r--spec/features/issues/gfm_autocomplete_spec.rb122
-rw-r--r--spec/features/issues/group_label_sidebar_spec.rb21
-rw-r--r--spec/features/issues/issue_sidebar_spec.rb25
-rw-r--r--spec/features/issues/markdown_toolbar_spec.rb37
-rw-r--r--spec/features/issues/new_branch_button_spec.rb20
-rw-r--r--spec/features/issues/reset_filters_spec.rb89
-rw-r--r--spec/features/issues/spam_issues_spec.rb66
-rw-r--r--spec/features/issues/user_uses_slash_commands_spec.rb60
-rw-r--r--spec/features/issues_spec.rb24
-rw-r--r--spec/features/login_spec.rb5
-rw-r--r--spec/features/merge_requests/cherry_pick_spec.rb3
-rw-r--r--spec/features/merge_requests/closes_issues_spec.rb4
-rw-r--r--spec/features/merge_requests/create_new_mr_spec.rb20
-rw-r--r--spec/features/merge_requests/created_from_fork_spec.rb8
-rw-r--r--spec/features/merge_requests/deleted_source_branch_spec.rb19
-rw-r--r--spec/features/merge_requests/diffs_spec.rb14
-rw-r--r--spec/features/merge_requests/edit_mr_spec.rb27
-rw-r--r--spec/features/merge_requests/filter_by_labels_spec.rb154
-rw-r--r--spec/features/merge_requests/filter_merge_requests_spec.rb355
-rw-r--r--spec/features/merge_requests/merge_immediately_with_pipeline_spec.rb41
-rw-r--r--spec/features/merge_requests/merge_request_versions_spec.rb52
-rw-r--r--spec/features/merge_requests/merge_when_pipeline_succeeds_spec.rb60
-rw-r--r--spec/features/merge_requests/mini_pipeline_graph_spec.rb100
-rw-r--r--spec/features/merge_requests/only_allow_merge_if_build_succeeds_spec.rb4
-rw-r--r--spec/features/merge_requests/reset_filters_spec.rb96
-rw-r--r--spec/features/merge_requests/toggler_behavior_spec.rb28
-rw-r--r--spec/features/merge_requests/user_uses_slash_commands_spec.rb129
-rw-r--r--spec/features/merge_requests/widget_spec.rb57
-rw-r--r--spec/features/merge_requests/wip_message_spec.rb63
-rw-r--r--spec/features/milestones/milestones_spec.rb6
-rw-r--r--spec/features/notes_on_merge_requests_spec.rb44
-rw-r--r--spec/features/projects/blobs/shortcuts_blob_spec.rb37
-rw-r--r--spec/features/projects/builds_spec.rb68
-rw-r--r--spec/features/projects/commit/builds_spec.rb12
-rw-r--r--spec/features/projects/commit/cherry_pick_spec.rb89
-rw-r--r--spec/features/projects/commits/cherry_pick_spec.rb88
-rw-r--r--spec/features/projects/compare_spec.rb (renamed from spec/features/compare_spec.rb)0
-rw-r--r--spec/features/projects/features_visibility_spec.rb11
-rw-r--r--spec/features/projects/files/editing_a_file_spec.rb2
-rw-r--r--spec/features/projects/files/project_owner_creates_license_file_spec.rb3
-rw-r--r--spec/features/projects/group_links_spec.rb4
-rw-r--r--spec/features/projects/import_export/export_file_spec.rb3
-rw-r--r--spec/features/projects/import_export/namespace_export_file_spec.rb62
-rw-r--r--spec/features/projects/import_export/test_project_export.tar.gzbin679415 -> 681799 bytes
-rw-r--r--spec/features/projects/issuable_templates_spec.rb61
-rw-r--r--spec/features/projects/labels/update_prioritization_spec.rb8
-rw-r--r--spec/features/projects/members/anonymous_user_sees_members_spec.rb4
-rw-r--r--spec/features/projects/members/group_links_spec.rb4
-rw-r--r--spec/features/projects/members/group_members_spec.rb8
-rw-r--r--spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb8
-rw-r--r--spec/features/projects/members/user_requests_access_spec.rb2
-rw-r--r--spec/features/projects/merge_request_button_spec.rb108
-rw-r--r--spec/features/projects/new_project_spec.rb20
-rw-r--r--spec/features/projects/pages_spec.rb60
-rw-r--r--spec/features/projects/pipelines/pipeline_spec.rb121
-rw-r--r--spec/features/projects/pipelines/pipelines_spec.rb441
-rw-r--r--spec/features/projects/project_settings_spec.rb14
-rw-r--r--spec/features/projects/ref_switcher_spec.rb5
-rw-r--r--spec/features/projects/services/mattermost_slash_command_spec.rb142
-rw-r--r--spec/features/projects/services/slack_service_spec.rb4
-rw-r--r--spec/features/projects/services/slack_slash_command_spec.rb21
-rw-r--r--spec/features/projects/settings/merge_requests_settings_spec.rb23
-rw-r--r--spec/features/projects/settings/visibility_settings_spec.rb47
-rw-r--r--spec/features/projects/view_on_env_spec.rb140
-rw-r--r--spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb12
-rw-r--r--spec/features/search_spec.rb48
-rw-r--r--spec/features/security/project/internal_access_spec.rb22
-rw-r--r--spec/features/security/project/private_access_spec.rb22
-rw-r--r--spec/features/security/project/public_access_spec.rb22
-rw-r--r--spec/features/signup_spec.rb21
-rw-r--r--spec/features/snippets/create_snippet_spec.rb14
-rw-r--r--spec/features/tags/master_creates_tag_spec.rb2
-rw-r--r--spec/features/task_lists_spec.rb44
-rw-r--r--spec/features/todos/todos_spec.rb19
-rw-r--r--spec/features/triggers_spec.rb2
-rw-r--r--spec/features/u2f_spec.rb119
-rw-r--r--spec/features/users_spec.rb18
-rw-r--r--spec/features/variables_spec.rb2
-rw-r--r--spec/finders/branches_finder_spec.rb2
-rw-r--r--spec/finders/contributed_projects_finder_spec.rb17
-rw-r--r--spec/finders/environments_finder_spec.rb110
-rw-r--r--spec/finders/group_projects_finder_spec.rb10
-rw-r--r--spec/finders/issues_finder_spec.rb2
-rw-r--r--spec/finders/joined_groups_finder_spec.rb2
-rw-r--r--spec/finders/merge_requests_finder_spec.rb6
-rw-r--r--spec/finders/move_to_project_finder_spec.rb22
-rw-r--r--spec/finders/notes_finder_spec.rb14
-rw-r--r--spec/finders/personal_projects_finder_spec.rb6
-rw-r--r--spec/finders/pipelines_finder_spec.rb2
-rw-r--r--spec/finders/projects_finder_spec.rb8
-rw-r--r--spec/finders/tags_finder_spec.rb2
-rw-r--r--spec/fixtures/api/schemas/issue.json1
-rw-r--r--spec/fixtures/api/schemas/list.json2
-rw-r--r--spec/fixtures/config/redis_config_with_env.yml2
-rw-r--r--spec/fixtures/emails/reply_without_subaddressing_and_key_inside_references_with_a_comma.eml42
-rw-r--r--spec/fixtures/pages.tar.gzbin0 -> 1795 bytes
-rw-r--r--spec/fixtures/pages.zipbin0 -> 1851 bytes
-rw-r--r--spec/fixtures/pages.zip.metabin0 -> 225 bytes
-rw-r--r--spec/fixtures/pages_empty.tar.gzbin0 -> 128 bytes
-rw-r--r--spec/fixtures/pages_empty.zipbin0 -> 160 bytes
-rw-r--r--spec/fixtures/pages_empty.zip.metabin0 -> 116 bytes
-rw-r--r--spec/helpers/application_helper_spec.rb4
-rw-r--r--spec/helpers/blob_helper_spec.rb2
-rw-r--r--spec/helpers/commits_helper_spec.rb19
-rw-r--r--spec/helpers/diff_helper_spec.rb10
-rw-r--r--spec/helpers/gitlab_markdown_helper_spec.rb28
-rw-r--r--spec/helpers/graph_helper_spec.rb2
-rw-r--r--spec/helpers/issuables_helper_spec.rb40
-rw-r--r--spec/helpers/issues_helper_spec.rb10
-rw-r--r--spec/helpers/members_helper_spec.rb2
-rw-r--r--spec/helpers/merge_requests_helper_spec.rb107
-rw-r--r--spec/helpers/milestones_helper_spec.rb14
-rw-r--r--spec/helpers/preferences_helper_spec.rb2
-rw-r--r--spec/helpers/projects_helper_spec.rb5
-rw-r--r--spec/helpers/search_helper_spec.rb11
-rw-r--r--spec/helpers/storage_helper_spec.rb21
-rw-r--r--spec/helpers/submodule_helper_spec.rb4
-rw-r--r--spec/helpers/tree_helper_spec.rb2
-rw-r--r--spec/helpers/visibility_level_helper_spec.rb8
-rw-r--r--spec/initializers/metrics_spec.rb16
-rw-r--r--spec/initializers/secret_token_spec.rb7
-rw-r--r--spec/javascripts/.eslintrc28
-rw-r--r--spec/javascripts/abuse_reports_spec.js.es69
-rw-r--r--spec/javascripts/activities_spec.js.es629
-rw-r--r--spec/javascripts/awards_handler_spec.js15
-rw-r--r--spec/javascripts/behaviors/autosize_spec.js9
-rw-r--r--spec/javascripts/behaviors/quick_submit_spec.js9
-rw-r--r--spec/javascripts/behaviors/requires_input_spec.js15
-rw-r--r--spec/javascripts/boards/boards_store_spec.js.es650
-rw-r--r--spec/javascripts/boards/issue_card_spec.js.es6191
-rw-r--r--spec/javascripts/boards/issue_spec.js.es623
-rw-r--r--spec/javascripts/boards/list_spec.js.es623
-rw-r--r--spec/javascripts/boards/mock_data.js.es65
-rw-r--r--spec/javascripts/boards/modal_store_spec.js.es6132
-rw-r--r--spec/javascripts/bootstrap_linked_tabs_spec.js.es630
-rw-r--r--spec/javascripts/build_spec.js.es619
-rw-r--r--spec/javascripts/commit/pipelines/mock_data.js.es692
-rw-r--r--spec/javascripts/commit/pipelines/pipelines_spec.js.es6105
-rw-r--r--spec/javascripts/commit/pipelines/pipelines_store_spec.js.es630
-rw-r--r--spec/javascripts/commits_spec.js.es662
-rw-r--r--spec/javascripts/dashboard_spec.js.es615
-rw-r--r--spec/javascripts/datetime_utility_spec.js.es62
-rw-r--r--spec/javascripts/diff_comments_store_spec.js.es611
-rw-r--r--spec/javascripts/environments/environment_actions_spec.js.es67
-rw-r--r--spec/javascripts/environments/environment_external_url_spec.js.es67
-rw-r--r--spec/javascripts/environments/environment_item_spec.js.es611
-rw-r--r--spec/javascripts/environments/environment_rollback_spec.js.es69
-rw-r--r--spec/javascripts/environments/environment_spec.js.es6125
-rw-r--r--spec/javascripts/environments/environment_stop_spec.js.es68
-rw-r--r--spec/javascripts/environments/environments_store_spec.js.es65
-rw-r--r--spec/javascripts/environments/mock_data.js.es628
-rw-r--r--spec/javascripts/extensions/array_spec.js.es65
-rw-r--r--spec/javascripts/extensions/element_spec.js.es62
-rw-r--r--spec/javascripts/extensions/jquery_spec.js9
-rw-r--r--spec/javascripts/extensions/object_spec.js.es62
-rw-r--r--spec/javascripts/filtered_search/dropdown_user_spec.js.es675
-rw-r--r--spec/javascripts/filtered_search/dropdown_utils_spec.js.es6290
-rw-r--r--spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js.es659
-rw-r--r--spec/javascripts/filtered_search/filtered_search_manager_spec.js.es667
-rw-r--r--spec/javascripts/filtered_search/filtered_search_token_keys_spec.js.es6110
-rw-r--r--spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js.es6104
-rw-r--r--spec/javascripts/fixtures/emoji_menu.js3
-rw-r--r--spec/javascripts/fixtures/environments/environments.html.haml2
-rw-r--r--spec/javascripts/fixtures/environments/table.html.haml2
-rw-r--r--spec/javascripts/fixtures/issuable_filter.html.haml8
-rw-r--r--spec/javascripts/fixtures/mini_dropdown_graph.html.haml11
-rw-r--r--spec/javascripts/fixtures/pipelines_table.html.haml2
-rw-r--r--spec/javascripts/fixtures/project_title.html.haml2
-rw-r--r--spec/javascripts/fixtures/projects.json446
-rw-r--r--spec/javascripts/fixtures/projects.rb28
-rw-r--r--spec/javascripts/fixtures/static_fixtures.rb31
-rw-r--r--spec/javascripts/fixtures/u2f.rb43
-rw-r--r--spec/javascripts/fixtures/u2f/authenticate.html.haml1
-rw-r--r--spec/javascripts/fixtures/u2f/register.html.haml2
-rw-r--r--spec/javascripts/gfm_auto_complete_spec.js.es691
-rw-r--r--spec/javascripts/gl_dropdown_spec.js.es670
-rw-r--r--spec/javascripts/gl_field_errors_spec.js.es613
-rw-r--r--spec/javascripts/gl_form_spec.js.es6123
-rw-r--r--spec/javascripts/graphs/stat_graph_contributors_graph_spec.js175
-rw-r--r--spec/javascripts/graphs/stat_graph_contributors_util_spec.js301
-rw-r--r--spec/javascripts/graphs/stat_graph_spec.js10
-rw-r--r--spec/javascripts/header_spec.js16
-rw-r--r--spec/javascripts/helpers/class_spec_helper.js.es69
-rw-r--r--spec/javascripts/helpers/class_spec_helper_spec.js.es636
-rw-r--r--spec/javascripts/issuable_spec.js.es680
-rw-r--r--spec/javascripts/issuable_time_tracker_spec.js.es6202
-rw-r--r--spec/javascripts/issue_spec.js43
-rw-r--r--spec/javascripts/labels_issue_sidebar_spec.js.es640
-rw-r--r--spec/javascripts/lib/utils/common_utils_spec.js.es6119
-rw-r--r--spec/javascripts/lib/utils/custom_event_polyfill_spec.js.es643
-rw-r--r--spec/javascripts/lib/utils/text_utility_spec.js.es639
-rw-r--r--spec/javascripts/line_highlighter_spec.js29
-rw-r--r--spec/javascripts/merge_request_spec.js9
-rw-r--r--spec/javascripts/merge_request_tabs_spec.js88
-rw-r--r--spec/javascripts/merge_request_widget_spec.js71
-rw-r--r--spec/javascripts/mini_pipeline_graph_dropdown_spec.js.es612
-rw-r--r--spec/javascripts/new_branch_spec.js11
-rw-r--r--spec/javascripts/notes_spec.js20
-rw-r--r--spec/javascripts/pipelines_spec.js.es611
-rw-r--r--spec/javascripts/pretty_time_spec.js.es624
-rw-r--r--spec/javascripts/project_dashboard_spec.js.es686
-rw-r--r--spec/javascripts/project_title_spec.js42
-rw-r--r--spec/javascripts/right_sidebar_spec.js21
-rw-r--r--spec/javascripts/search_autocomplete_spec.js51
-rw-r--r--spec/javascripts/shortcuts_issuable_spec.js73
-rw-r--r--spec/javascripts/signin_tabs_memoizer_spec.js.es68
-rw-r--r--spec/javascripts/smart_interval_spec.js.es67
-rw-r--r--spec/javascripts/spec_helper.js48
-rw-r--r--spec/javascripts/subbable_resource_spec.js.es69
-rw-r--r--spec/javascripts/syntax_highlight_spec.js11
-rw-r--r--spec/javascripts/test_bundle.js44
-rw-r--r--spec/javascripts/u2f/authenticate_spec.js50
-rw-r--r--spec/javascripts/u2f/mock_u2f_device.js7
-rw-r--r--spec/javascripts/u2f/register_spec.js17
-rw-r--r--spec/javascripts/visibility_select_spec.js.es6100
-rw-r--r--spec/javascripts/vue_common_components/commit_spec.js.es6131
-rw-r--r--spec/javascripts/vue_shared/components/commit_spec.js.es6131
-rw-r--r--spec/javascripts/vue_shared/components/pipelines_table_row_spec.js.es687
-rw-r--r--spec/javascripts/vue_shared/components/pipelines_table_spec.js.es664
-rw-r--r--spec/javascripts/vue_shared/components/table_pagination_spec.js.es6166
-rw-r--r--spec/javascripts/zen_mode_spec.js23
-rw-r--r--spec/lib/api/helpers/pagination_spec.rb94
-rw-r--r--spec/lib/banzai/cross_project_reference_spec.rb2
-rw-r--r--spec/lib/banzai/filter/abstract_link_filter_spec.rb52
-rw-r--r--spec/lib/banzai/filter/abstract_reference_filter_spec.rb103
-rw-r--r--spec/lib/banzai/filter/commit_range_reference_filter_spec.rb14
-rw-r--r--spec/lib/banzai/filter/commit_reference_filter_spec.rb10
-rw-r--r--spec/lib/banzai/filter/external_link_filter_spec.rb14
-rw-r--r--spec/lib/banzai/filter/gollum_tags_filter_spec.rb2
-rw-r--r--spec/lib/banzai/filter/math_filter_spec.rb7
-rw-r--r--spec/lib/banzai/filter/merge_request_reference_filter_spec.rb2
-rw-r--r--spec/lib/banzai/filter/milestone_reference_filter_spec.rb2
-rw-r--r--spec/lib/banzai/filter/plantuml_filter_spec.rb32
-rw-r--r--spec/lib/banzai/filter/relative_link_filter_spec.rb2
-rw-r--r--spec/lib/banzai/filter/syntax_highlight_filter_spec.rb8
-rw-r--r--spec/lib/banzai/filter/upload_link_filter_spec.rb2
-rw-r--r--spec/lib/banzai/filter/user_reference_filter_spec.rb24
-rw-r--r--spec/lib/banzai/filter/video_link_filter_spec.rb2
-rw-r--r--spec/lib/banzai/reference_parser/external_issue_parser_spec.rb12
-rw-r--r--spec/lib/banzai/reference_parser/user_parser_spec.rb6
-rw-r--r--spec/lib/ci/ansi2html_spec.rb8
-rw-r--r--spec/lib/ci/gitlab_ci_yaml_processor_spec.rb38
-rw-r--r--spec/lib/constraints/project_url_constrainer_spec.rb2
-rw-r--r--spec/lib/event_filter_spec.rb22
-rw-r--r--spec/lib/extracts_path_spec.rb2
-rw-r--r--spec/lib/gitlab/asciidoc_spec.rb4
-rw-r--r--spec/lib/gitlab/auth_spec.rb96
-rw-r--r--spec/lib/gitlab/backup/manager_spec.rb114
-rw-r--r--spec/lib/gitlab/badge/build/metadata_spec.rb2
-rw-r--r--spec/lib/gitlab/badge/build/status_spec.rb2
-rw-r--r--spec/lib/gitlab/badge/coverage/metadata_spec.rb2
-rw-r--r--spec/lib/gitlab/bitbucket_import/importer_spec.rb2
-rw-r--r--spec/lib/gitlab/blame_spec.rb2
-rw-r--r--spec/lib/gitlab/chat_commands/command_spec.rb28
-rw-r--r--spec/lib/gitlab/chat_commands/deploy_spec.rb24
-rw-r--r--spec/lib/gitlab/chat_commands/issue_create_spec.rb68
-rw-r--r--spec/lib/gitlab/chat_commands/issue_new_spec.rb78
-rw-r--r--spec/lib/gitlab/chat_commands/issue_search_spec.rb12
-rw-r--r--spec/lib/gitlab/chat_commands/issue_show_spec.rb25
-rw-r--r--spec/lib/gitlab/chat_commands/presenters/access_spec.rb49
-rw-r--r--spec/lib/gitlab/chat_commands/presenters/deploy_spec.rb47
-rw-r--r--spec/lib/gitlab/chat_commands/presenters/issue_new_spec.rb17
-rw-r--r--spec/lib/gitlab/chat_commands/presenters/issue_search_spec.rb23
-rw-r--r--spec/lib/gitlab/chat_commands/presenters/issue_show_spec.rb37
-rw-r--r--spec/lib/gitlab/checks/change_access_spec.rb5
-rw-r--r--spec/lib/gitlab/checks/force_push_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/config/entry/coverage_spec.rb54
-rw-r--r--spec/lib/gitlab/ci/config/entry/environment_spec.rb17
-rw-r--r--spec/lib/gitlab/ci/config/entry/global_spec.rb13
-rw-r--r--spec/lib/gitlab/ci/config/entry/job_spec.rb14
-rw-r--r--spec/lib/gitlab/ci/status/build/cancelable_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/status/build/factory_spec.rb125
-rw-r--r--spec/lib/gitlab/ci/status/build/failed_allowed_spec.rb110
-rw-r--r--spec/lib/gitlab/ci/status/build/play_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/status/build/retryable_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/status/build/stop_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/status/external/common_spec.rb39
-rw-r--r--spec/lib/gitlab/ci/status/external/factory_spec.rb38
-rw-r--r--spec/lib/gitlab/ci/status/factory_spec.rb133
-rw-r--r--spec/lib/gitlab/ci/status/pipeline/factory_spec.rb48
-rw-r--r--spec/lib/gitlab/ci/status/pipeline/success_with_warnings_spec.rb69
-rw-r--r--spec/lib/gitlab/ci/status/stage/factory_spec.rb21
-rw-r--r--spec/lib/gitlab/ci/status/success_warning_spec.rb75
-rw-r--r--spec/lib/gitlab/ci/trace_reader_spec.rb16
-rw-r--r--spec/lib/gitlab/closing_issue_extractor_spec.rb8
-rw-r--r--spec/lib/gitlab/conflict/file_spec.rb2
-rw-r--r--spec/lib/gitlab/contributions_calendar_spec.rb2
-rw-r--r--spec/lib/gitlab/current_settings_spec.rb68
-rw-r--r--spec/lib/gitlab/cycle_analytics/code_event_fetcher_spec.rb12
-rw-r--r--spec/lib/gitlab/cycle_analytics/code_event_spec.rb10
-rw-r--r--spec/lib/gitlab/cycle_analytics/code_stage_spec.rb8
-rw-r--r--spec/lib/gitlab/cycle_analytics/events_spec.rb137
-rw-r--r--spec/lib/gitlab/cycle_analytics/issue_event_fetcher_spec.rb8
-rw-r--r--spec/lib/gitlab/cycle_analytics/issue_event_spec.rb10
-rw-r--r--spec/lib/gitlab/cycle_analytics/issue_stage_spec.rb8
-rw-r--r--spec/lib/gitlab/cycle_analytics/plan_event_fetcher_spec.rb16
-rw-r--r--spec/lib/gitlab/cycle_analytics/plan_event_spec.rb18
-rw-r--r--spec/lib/gitlab/cycle_analytics/plan_stage_spec.rb8
-rw-r--r--spec/lib/gitlab/cycle_analytics/production_event_fetcher_spec.rb8
-rw-r--r--spec/lib/gitlab/cycle_analytics/production_event_spec.rb10
-rw-r--r--spec/lib/gitlab/cycle_analytics/production_stage_spec.rb8
-rw-r--r--spec/lib/gitlab/cycle_analytics/review_event_fetcher_spec.rb8
-rw-r--r--spec/lib/gitlab/cycle_analytics/review_event_spec.rb10
-rw-r--r--spec/lib/gitlab/cycle_analytics/review_stage_spec.rb8
-rw-r--r--spec/lib/gitlab/cycle_analytics/shared_event_spec.rb11
-rw-r--r--spec/lib/gitlab/cycle_analytics/shared_stage_spec.rb30
-rw-r--r--spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb61
-rw-r--r--spec/lib/gitlab/cycle_analytics/staging_event_fetcher_spec.rb12
-rw-r--r--spec/lib/gitlab/cycle_analytics/staging_event_spec.rb10
-rw-r--r--spec/lib/gitlab/cycle_analytics/staging_stage_spec.rb8
-rw-r--r--spec/lib/gitlab/cycle_analytics/test_event_fetcher_spec.rb12
-rw-r--r--spec/lib/gitlab/cycle_analytics/test_event_spec.rb10
-rw-r--r--spec/lib/gitlab/cycle_analytics/test_stage_spec.rb8
-rw-r--r--spec/lib/gitlab/data_builder/note_spec.rb2
-rw-r--r--spec/lib/gitlab/data_builder/pipeline_spec.rb2
-rw-r--r--spec/lib/gitlab/data_builder/push_spec.rb2
-rw-r--r--spec/lib/gitlab/database_spec.rb16
-rw-r--r--spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb19
-rw-r--r--spec/lib/gitlab/diff/file_spec.rb2
-rw-r--r--spec/lib/gitlab/diff/highlight_spec.rb10
-rw-r--r--spec/lib/gitlab/diff/line_mapper_spec.rb2
-rw-r--r--spec/lib/gitlab/diff/parallel_diff_spec.rb4
-rw-r--r--spec/lib/gitlab/diff/position_spec.rb2
-rw-r--r--spec/lib/gitlab/diff/position_tracer_spec.rb12
-rw-r--r--spec/lib/gitlab/email/email_shared_blocks.rb2
-rw-r--r--spec/lib/gitlab/email/handler/create_issue_handler_spec.rb4
-rw-r--r--spec/lib/gitlab/email/handler/create_note_handler_spec.rb10
-rw-r--r--spec/lib/gitlab/email/handler/unsubscribe_handler_spec.rb61
-rw-r--r--spec/lib/gitlab/email/message/repository_push_spec.rb2
-rw-r--r--spec/lib/gitlab/email/reply_parser_spec.rb2
-rw-r--r--spec/lib/gitlab/gfm/reference_rewriter_spec.rb4
-rw-r--r--spec/lib/gitlab/gfm/uploads_rewriter_spec.rb4
-rw-r--r--spec/lib/gitlab/git/attributes_spec.rb150
-rw-r--r--spec/lib/gitlab/git/blame_spec.rb66
-rw-r--r--spec/lib/gitlab/git/blob_snippet_spec.rb19
-rw-r--r--spec/lib/gitlab/git/blob_spec.rb489
-rw-r--r--spec/lib/gitlab/git/branch_spec.rb31
-rw-r--r--spec/lib/gitlab/git/commit_spec.rb408
-rw-r--r--spec/lib/gitlab/git/compare_spec.rb109
-rw-r--r--spec/lib/gitlab/git/diff_collection_spec.rb460
-rw-r--r--spec/lib/gitlab/git/diff_spec.rb287
-rw-r--r--spec/lib/gitlab/git/encoding_helper_spec.rb84
-rw-r--r--spec/lib/gitlab/git/hook_spec.rb2
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb1184
-rw-r--r--spec/lib/gitlab/git/rev_list_spec.rb2
-rw-r--r--spec/lib/gitlab/git/tag_spec.rb25
-rw-r--r--spec/lib/gitlab/git/tree_spec.rb76
-rw-r--r--spec/lib/gitlab/git/util_spec.rb16
-rw-r--r--spec/lib/gitlab/git_access_spec.rb66
-rw-r--r--spec/lib/gitlab/git_access_wiki_spec.rb6
-rw-r--r--spec/lib/gitlab/github_import/branch_formatter_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/comment_formatter_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/importer_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/issue_formatter_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/label_formatter_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/pull_request_formatter_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/release_formatter_spec.rb2
-rw-r--r--spec/lib/gitlab/google_code_import/importer_spec.rb2
-rw-r--r--spec/lib/gitlab/graphs/commits_spec.rb2
-rw-r--r--spec/lib/gitlab/highlight_spec.rb4
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml13
-rw-r--r--spec/lib/gitlab/import_export/import_export_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/members_mapper_spec.rb82
-rw-r--r--spec/lib/gitlab/import_export/project.json35
-rw-r--r--spec/lib/gitlab/import_export/project_tree_restorer_spec.rb28
-rw-r--r--spec/lib/gitlab/import_export/project_tree_saver_spec.rb71
-rw-r--r--spec/lib/gitlab/import_export/reader_spec.rb4
-rw-r--r--spec/lib/gitlab/import_export/relation_factory_spec.rb73
-rw-r--r--spec/lib/gitlab/import_export/repo_bundler_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml13
-rw-r--r--spec/lib/gitlab/import_export/wiki_repo_bundler_spec.rb2
-rw-r--r--spec/lib/gitlab/incoming_email_spec.rb57
-rw-r--r--spec/lib/gitlab/job_waiter_spec.rb30
-rw-r--r--spec/lib/gitlab/ldap/access_spec.rb65
-rw-r--r--spec/lib/gitlab/ldap/config_spec.rb23
-rw-r--r--spec/lib/gitlab/ldap/person_spec.rb46
-rw-r--r--spec/lib/gitlab/metrics/rack_middleware_spec.rb15
-rw-r--r--spec/lib/gitlab/middleware/multipart_spec.rb6
-rw-r--r--spec/lib/gitlab/project_search_results_spec.rb123
-rw-r--r--spec/lib/gitlab/project_transfer_spec.rb51
-rw-r--r--spec/lib/gitlab/redis_spec.rb16
-rw-r--r--spec/lib/gitlab/reference_extractor_spec.rb22
-rw-r--r--spec/lib/gitlab/route_map_spec.rb90
-rw-r--r--spec/lib/gitlab/search_results_spec.rb2
-rw-r--r--spec/lib/gitlab/serialize/ci/variables_spec.rb18
-rw-r--r--spec/lib/gitlab/serializer/ci/variables_spec.rb18
-rw-r--r--spec/lib/gitlab/serializer/pagination_spec.rb49
-rw-r--r--spec/lib/gitlab/sidekiq_status/client_middleware_spec.rb12
-rw-r--r--spec/lib/gitlab/sidekiq_status/server_middleware_spec.rb14
-rw-r--r--spec/lib/gitlab/sidekiq_status_spec.rb50
-rw-r--r--spec/lib/gitlab/template/issue_template_spec.rb18
-rw-r--r--spec/lib/gitlab/template/merge_request_template_spec.rb18
-rw-r--r--spec/lib/gitlab/uploads_transfer_spec.rb50
-rw-r--r--spec/lib/gitlab/url_builder_spec.rb2
-rw-r--r--spec/lib/gitlab/user_access_spec.rb9
-rw-r--r--spec/lib/gitlab/view/presenter/base_spec.rb51
-rw-r--r--spec/lib/gitlab/view/presenter/delegated_spec.rb33
-rw-r--r--spec/lib/gitlab/view/presenter/factory_spec.rb31
-rw-r--r--spec/lib/gitlab/view/presenter/simple_spec.rb34
-rw-r--r--spec/lib/gitlab/workhorse_spec.rb31
-rw-r--r--spec/lib/light_url_builder_spec.rb2
-rw-r--r--spec/lib/mattermost/client_spec.rb24
-rw-r--r--spec/lib/mattermost/command_spec.rb61
-rw-r--r--spec/lib/mattermost/session_spec.rb24
-rw-r--r--spec/lib/mattermost/team_spec.rb66
-rw-r--r--spec/lib/repository_cache_spec.rb2
-rw-r--r--spec/migrations/fill_authorized_projects_spec.rb18
-rw-r--r--spec/migrations/remove_dot_git_from_usernames_spec.rb57
-rw-r--r--spec/migrations/rename_reserved_project_names_spec.rb47
-rw-r--r--spec/models/ability_spec.rb37
-rw-r--r--spec/models/build_spec.rb1338
-rw-r--r--spec/models/ci/build_spec.rb1373
-rw-r--r--spec/models/ci/pipeline_spec.rb166
-rw-r--r--spec/models/ci/runner_spec.rb63
-rw-r--r--spec/models/ci/stage_spec.rb72
-rw-r--r--spec/models/commit_range_spec.rb2
-rw-r--r--spec/models/commit_spec.rb60
-rw-r--r--spec/models/commit_status_spec.rb21
-rw-r--r--spec/models/compare_spec.rb2
-rw-r--r--spec/models/concerns/has_status_spec.rb6
-rw-r--r--spec/models/concerns/issuable_spec.rb83
-rw-r--r--spec/models/concerns/mentionable_spec.rb32
-rw-r--r--spec/models/concerns/milestoneish_spec.rb2
-rw-r--r--spec/models/concerns/presentable_spec.rb15
-rw-r--r--spec/models/concerns/project_features_compatibility_spec.rb2
-rw-r--r--spec/models/concerns/routable_spec.rb10
-rw-r--r--spec/models/cycle_analytics/code_spec.rb46
-rw-r--r--spec/models/cycle_analytics/issue_spec.rb18
-rw-r--r--spec/models/cycle_analytics/plan_spec.rb6
-rw-r--r--spec/models/cycle_analytics/production_spec.rb36
-rw-r--r--spec/models/cycle_analytics/review_spec.rb10
-rw-r--r--spec/models/cycle_analytics/staging_spec.rb35
-rw-r--r--spec/models/cycle_analytics/summary_spec.rb59
-rw-r--r--spec/models/cycle_analytics/test_spec.rb62
-rw-r--r--spec/models/deploy_key_spec.rb14
-rw-r--r--spec/models/deploy_keys_project_spec.rb4
-rw-r--r--spec/models/deployment_spec.rb6
-rw-r--r--spec/models/diff_note_spec.rb4
-rw-r--r--spec/models/environment_spec.rb105
-rw-r--r--spec/models/event_spec.rb30
-rw-r--r--spec/models/forked_project_link_spec.rb4
-rw-r--r--spec/models/generic_commit_status_spec.rb33
-rw-r--r--spec/models/global_milestone_spec.rb92
-rw-r--r--spec/models/group_label_spec.rb2
-rw-r--r--spec/models/group_milestone_spec.rb32
-rw-r--r--spec/models/group_spec.rb40
-rw-r--r--spec/models/guest_spec.rb8
-rw-r--r--spec/models/hooks/system_hook_spec.rb2
-rw-r--r--spec/models/hooks/web_hook_spec.rb2
-rw-r--r--spec/models/issue/metrics_spec.rb2
-rw-r--r--spec/models/issue_collection_spec.rb2
-rw-r--r--spec/models/issue_spec.rb85
-rw-r--r--spec/models/key_spec.rb42
-rw-r--r--spec/models/label_spec.rb2
-rw-r--r--spec/models/lfs_objects_project_spec.rb36
-rw-r--r--spec/models/list_spec.rb39
-rw-r--r--spec/models/member_spec.rb2
-rw-r--r--spec/models/members/project_member_spec.rb10
-rw-r--r--spec/models/merge_request/metrics_spec.rb4
-rw-r--r--spec/models/merge_request_diff_spec.rb26
-rw-r--r--spec/models/merge_request_spec.rb188
-rw-r--r--spec/models/milestone_spec.rb6
-rw-r--r--spec/models/namespace_spec.rb95
-rw-r--r--spec/models/network/graph_spec.rb2
-rw-r--r--spec/models/note_spec.rb95
-rw-r--r--spec/models/pages_domain_spec.rb168
-rw-r--r--spec/models/project_feature_spec.rb6
-rw-r--r--spec/models/project_group_link_spec.rb2
-rw-r--r--spec/models/project_label_spec.rb2
-rw-r--r--spec/models/project_services/asana_service_spec.rb2
-rw-r--r--spec/models/project_services/assembla_service_spec.rb2
-rw-r--r--spec/models/project_services/bamboo_service_spec.rb149
-rw-r--r--spec/models/project_services/buildkite_service_spec.rb77
-rw-r--r--spec/models/project_services/campfire_service_spec.rb2
-rw-r--r--spec/models/project_services/drone_ci_service_spec.rb74
-rw-r--r--spec/models/project_services/external_wiki_service_spec.rb2
-rw-r--r--spec/models/project_services/flowdock_service_spec.rb2
-rw-r--r--spec/models/project_services/gemnasium_service_spec.rb2
-rw-r--r--spec/models/project_services/gitlab_issue_tracker_service_spec.rb6
-rw-r--r--spec/models/project_services/hipchat_service_spec.rb6
-rw-r--r--spec/models/project_services/irker_service_spec.rb2
-rw-r--r--spec/models/project_services/jira_service_spec.rb12
-rw-r--r--spec/models/project_services/kubernetes_service_spec.rb16
-rw-r--r--spec/models/project_services/mattermost_notification_service_spec.rb5
-rw-r--r--spec/models/project_services/mattermost_service_spec.rb5
-rw-r--r--spec/models/project_services/mattermost_slash_commands_service_spec.rb116
-rw-r--r--spec/models/project_services/pipeline_email_service_spec.rb2
-rw-r--r--spec/models/project_services/pushover_service_spec.rb2
-rw-r--r--spec/models/project_services/slack_notification_service_spec.rb5
-rw-r--r--spec/models/project_services/slack_service_spec.rb5
-rw-r--r--spec/models/project_services/slack_slash_commands_service.rb40
-rw-r--r--spec/models/project_services/slack_slash_commands_service_spec.rb41
-rw-r--r--spec/models/project_services/teamcity_service_spec.rb126
-rw-r--r--spec/models/project_spec.rb442
-rw-r--r--spec/models/project_statistics_spec.rb160
-rw-r--r--spec/models/project_team_spec.rb10
-rw-r--r--spec/models/repository_spec.rb298
-rw-r--r--spec/models/route_spec.rb2
-rw-r--r--spec/models/service_spec.rb16
-rw-r--r--spec/models/snippet_spec.rb6
-rw-r--r--spec/models/timelog_spec.rb38
-rw-r--r--spec/models/todo_spec.rb16
-rw-r--r--spec/models/tree_spec.rb2
-rw-r--r--spec/models/user_spec.rb209
-rw-r--r--spec/policies/base_policy_spec.rb17
-rw-r--r--spec/policies/ci/build_policy_spec.rb93
-rw-r--r--spec/policies/group_policy_spec.rb66
-rw-r--r--spec/policies/project_policy_spec.rb62
-rw-r--r--spec/policies/project_snippet_policy_spec.rb101
-rw-r--r--spec/presenters/ci/build_presenter_spec.rb77
-rw-r--r--spec/requests/api/access_requests_spec.rb2
-rw-r--r--spec/requests/api/boards_spec.rb4
-rw-r--r--spec/requests/api/branches_spec.rb2
-rw-r--r--spec/requests/api/builds_spec.rb63
-rw-r--r--spec/requests/api/commit_statuses_spec.rb30
-rw-r--r--spec/requests/api/commits_spec.rb12
-rw-r--r--spec/requests/api/deploy_keys_spec.rb13
-rw-r--r--spec/requests/api/environments_spec.rb2
-rw-r--r--spec/requests/api/files_spec.rb4
-rw-r--r--spec/requests/api/fork_spec.rb23
-rw-r--r--spec/requests/api/groups_spec.rb127
-rw-r--r--spec/requests/api/helpers_spec.rb7
-rw-r--r--spec/requests/api/internal_spec.rb15
-rw-r--r--spec/requests/api/issues_spec.rb106
-rw-r--r--spec/requests/api/labels_spec.rb2
-rw-r--r--spec/requests/api/members_spec.rb2
-rw-r--r--spec/requests/api/merge_requests_spec.rb83
-rw-r--r--spec/requests/api/notes_spec.rb20
-rw-r--r--spec/requests/api/notification_settings_spec.rb2
-rw-r--r--spec/requests/api/pipelines_spec.rb2
-rw-r--r--spec/requests/api/project_hooks_spec.rb9
-rw-r--r--spec/requests/api/project_snippets_spec.rb62
-rw-r--r--spec/requests/api/projects_spec.rb129
-rw-r--r--spec/requests/api/repositories_spec.rb14
-rw-r--r--spec/requests/api/runners_spec.rb4
-rw-r--r--spec/requests/api/services_spec.rb102
-rw-r--r--spec/requests/api/settings_spec.rb19
-rw-r--r--spec/requests/api/snippets_spec.rb32
-rw-r--r--spec/requests/api/tags_spec.rb6
-rw-r--r--spec/requests/api/todos_spec.rb19
-rw-r--r--spec/requests/api/triggers_spec.rb4
-rw-r--r--spec/requests/api/users_spec.rb28
-rw-r--r--spec/requests/api/v3/deploy_keys_spec.rb172
-rw-r--r--spec/requests/api/v3/issues_spec.rb1259
-rw-r--r--spec/requests/api/v3/merge_requests_spec.rb726
-rw-r--r--spec/requests/api/v3/project_snippets_spec.rb188
-rw-r--r--spec/requests/api/v3/projects_spec.rb1424
-rw-r--r--spec/requests/api/variables_spec.rb2
-rw-r--r--spec/requests/ci/api/builds_spec.rb65
-rw-r--r--spec/requests/ci/api/triggers_spec.rb6
-rw-r--r--spec/requests/git_http_spec.rb50
-rw-r--r--spec/requests/lfs_http_spec.rb1
-rw-r--r--spec/requests/projects/artifacts_controller_spec.rb2
-rw-r--r--spec/requests/projects/cycle_analytics_events_spec.rb44
-rw-r--r--spec/routing/admin_routing_spec.rb10
-rw-r--r--spec/routing/project_routing_spec.rb45
-rw-r--r--spec/serializers/analytics_build_serializer_spec.rb10
-rw-r--r--spec/serializers/analytics_issue_serializer_spec.rb5
-rw-r--r--spec/serializers/analytics_merge_request_serializer_spec.rb5
-rw-r--r--spec/serializers/analytics_stage_serializer_spec.rb24
-rw-r--r--spec/serializers/analytics_summary_serializer_spec.rb28
-rw-r--r--spec/serializers/build_action_entity_spec.rb21
-rw-r--r--spec/serializers/build_artifact_entity_spec.rb22
-rw-r--r--spec/serializers/commit_entity_spec.rb6
-rw-r--r--spec/serializers/environment_serializer_spec.rb141
-rw-r--r--spec/serializers/pipeline_entity_spec.rb150
-rw-r--r--spec/serializers/pipeline_serializer_spec.rb97
-rw-r--r--spec/serializers/request_aware_entity_spec.rb22
-rw-r--r--spec/serializers/stage_entity_spec.rb51
-rw-r--r--spec/serializers/status_entity_spec.rb23
-rw-r--r--spec/services/boards/create_service_spec.rb7
-rw-r--r--spec/services/boards/issues/list_service_spec.rb5
-rw-r--r--spec/services/boards/issues/move_service_spec.rb49
-rw-r--r--spec/services/boards/lists/create_service_spec.rb4
-rw-r--r--spec/services/boards/lists/destroy_service_spec.rb9
-rw-r--r--spec/services/boards/lists/list_service_spec.rb2
-rw-r--r--spec/services/boards/lists/move_service_spec.rb9
-rw-r--r--spec/services/ci/register_build_service_spec.rb55
-rw-r--r--spec/services/ci/stop_environments_service_spec.rb4
-rw-r--r--spec/services/ci/update_build_queue_service_spec.rb47
-rw-r--r--spec/services/compare_service_spec.rb6
-rw-r--r--spec/services/event_create_service_spec.rb6
-rw-r--r--spec/services/files/update_service_spec.rb33
-rw-r--r--spec/services/git_hooks_service_spec.rb2
-rw-r--r--spec/services/git_push_service_spec.rb23
-rw-r--r--spec/services/groups/create_service_spec.rb31
-rw-r--r--spec/services/groups/update_service_spec.rb51
-rw-r--r--spec/services/issuable/bulk_update_service_spec.rb12
-rw-r--r--spec/services/issues/create_service_spec.rb104
-rw-r--r--spec/services/issues/update_service_spec.rb11
-rw-r--r--spec/services/labels/promote_service_spec.rb187
-rw-r--r--spec/services/merge_requests/close_service_spec.rb2
-rw-r--r--spec/services/merge_requests/create_service_spec.rb2
-rw-r--r--spec/services/merge_requests/merge_request_diff_cache_service_spec.rb1
-rw-r--r--spec/services/merge_requests/refresh_service_spec.rb117
-rw-r--r--spec/services/merge_requests/resolve_service_spec.rb8
-rw-r--r--spec/services/merge_requests/update_service_spec.rb93
-rw-r--r--spec/services/notes/create_service_spec.rb62
-rw-r--r--spec/services/notes/slash_commands_service_spec.rb14
-rw-r--r--spec/services/notification_service_spec.rb388
-rw-r--r--spec/services/pages_service_spec.rb47
-rw-r--r--spec/services/projects/create_service_spec.rb4
-rw-r--r--spec/services/projects/destroy_service_spec.rb49
-rw-r--r--spec/services/projects/fork_service_spec.rb13
-rw-r--r--spec/services/projects/participants_service_spec.rb32
-rw-r--r--spec/services/projects/transfer_service_spec.rb28
-rw-r--r--spec/services/projects/update_pages_configuration_service_spec.rb24
-rw-r--r--spec/services/projects/update_pages_service_spec.rb80
-rw-r--r--spec/services/projects/update_service_spec.rb158
-rw-r--r--spec/services/search_service_spec.rb19
-rw-r--r--spec/services/slash_commands/interpret_service_spec.rb186
-rw-r--r--spec/services/spam_service_spec.rb47
-rw-r--r--spec/services/system_hooks_service_spec.rb2
-rw-r--r--spec/services/system_note_service_spec.rb131
-rw-r--r--spec/services/todo_service_spec.rb16
-rw-r--r--spec/services/user_project_access_changed_service_spec.rb12
-rw-r--r--spec/services/users/refresh_authorized_projects_service_spec.rb88
-rw-r--r--spec/spec_helper.rb7
-rw-r--r--spec/support/api/time_tracking_shared_examples.rb132
-rw-r--r--spec/support/api_helpers.rb9
-rw-r--r--spec/support/cycle_analytics_helpers.rb8
-rw-r--r--spec/support/cycle_analytics_helpers/test_generation.rb65
-rw-r--r--spec/support/fake_u2f_device.rb3
-rw-r--r--spec/support/features/issuable_slash_commands_shared_examples.rb22
-rw-r--r--spec/support/import_export/export_file_helper.rb2
-rw-r--r--spec/support/javascript_fixtures_helpers.rb21
-rw-r--r--spec/support/kubernetes_helpers.rb3
-rw-r--r--spec/support/matchers/be_valid_commit.rb8
-rw-r--r--spec/support/matchers/match_file.rb5
-rw-r--r--spec/support/mentionable_shared_examples.rb2
-rw-r--r--spec/support/mobile_helpers.rb13
-rw-r--r--spec/support/notify_shared_examples.rb17
-rw-r--r--spec/support/reactive_caching_helpers.rb30
-rw-r--r--spec/support/seed_helper.rb112
-rw-r--r--spec/support/seed_repo.rb143
-rw-r--r--spec/support/services/issuable_create_service_shared_examples.rb52
-rw-r--r--spec/support/services/issuable_create_service_slash_commands_shared_examples.rb4
-rw-r--r--spec/support/services/issuable_update_service_shared_examples.rb52
-rw-r--r--spec/support/sidekiq.rb5
-rw-r--r--spec/support/slack_mattermost_notifications_shared_examples.rb8
-rw-r--r--spec/support/stub_env.rb7
-rw-r--r--spec/support/taskable_shared_examples.rb43
-rw-r--r--spec/support/test_env.rb4
-rw-r--r--spec/support/time_tracking_shared_examples.rb82
-rw-r--r--spec/tasks/config_lint_spec.rb27
-rw-r--r--spec/tasks/gitlab/backup_rake_spec.rb16
-rw-r--r--spec/tasks/gitlab/mail_google_schema_whitelisting.rb2
-rw-r--r--spec/teaspoon_env.rb178
-rw-r--r--spec/uploaders/avatar_uploader_spec.rb8
-rw-r--r--spec/uploaders/file_uploader_spec.rb6
-rw-r--r--spec/views/ci/lints/show.html.haml_spec.rb2
-rw-r--r--spec/views/projects/builds/show.html.haml_spec.rb64
-rw-r--r--spec/views/projects/merge_requests/show.html.haml_spec.rb7
-rw-r--r--spec/views/projects/pipelines/_stage.html.haml_spec.rb44
-rw-r--r--spec/views/projects/pipelines/show.html.haml_spec.rb2
-rw-r--r--spec/views/shared/milestones/_issuables.html.haml.rb32
-rw-r--r--spec/workers/authorized_projects_worker_spec.rb12
-rw-r--r--spec/workers/git_garbage_collect_worker_spec.rb3
-rw-r--r--spec/workers/post_receive_spec.rb6
-rw-r--r--spec/workers/project_cache_worker_spec.rb42
-rw-r--r--spec/workers/project_destroy_worker_spec.rb4
-rw-r--r--spec/workers/repository_check/single_repository_worker_spec.rb8
-rw-r--r--spec/workers/use_key_worker_spec.rb23
-rw-r--r--vendor/assets/javascripts/date.format.js207
-rw-r--r--vendor/assets/javascripts/es6-promise.auto.js3
-rw-r--r--vendor/assets/javascripts/jquery.atwho.js1202
-rw-r--r--vendor/assets/javascripts/jquery.ba-resize.js246
-rw-r--r--vendor/assets/javascripts/jquery.caret.js436
-rw-r--r--vendor/assets/javascripts/katex.js6
-rw-r--r--vendor/assets/javascripts/u2f.js4
-rw-r--r--vendor/assets/javascripts/xterm/encoding-indexes.js39
-rw-r--r--vendor/assets/javascripts/xterm/encoding.js3309
-rw-r--r--vendor/assets/javascripts/xterm/fit.js4
-rw-r--r--vendor/assets/stylesheets/katex.css975
-rw-r--r--vendor/assets/stylesheets/katex.scss977
-rw-r--r--vendor/gitignore/Android.gitignore10
-rw-r--r--vendor/gitignore/Autotools.gitignore15
-rw-r--r--vendor/gitignore/CMake.gitignore2
-rw-r--r--vendor/gitignore/CodeIgniter.gitignore11
-rw-r--r--vendor/gitignore/CommonLisp.gitignore14
-rw-r--r--vendor/gitignore/Coq.gitignore29
-rw-r--r--vendor/gitignore/Dart.gitignore10
-rw-r--r--vendor/gitignore/Elisp.gitignore6
-rw-r--r--vendor/gitignore/Elixir.gitignore1
-rw-r--r--vendor/gitignore/Global/Emacs.gitignore5
-rw-r--r--vendor/gitignore/Global/IPythonNotebook.gitignore2
-rw-r--r--vendor/gitignore/Global/JetBrains.gitignore23
-rw-r--r--vendor/gitignore/Global/Matlab.gitignore3
-rw-r--r--vendor/gitignore/Global/Stata.gitignore24
-rw-r--r--vendor/gitignore/Global/SublimeText.gitignore3
-rw-r--r--vendor/gitignore/Global/Vim.gitignore6
-rw-r--r--vendor/gitignore/Global/VisualStudioCode.gitignore1
-rw-r--r--vendor/gitignore/Global/Windows.gitignore3
-rw-r--r--vendor/gitignore/Go.gitignore30
-rw-r--r--vendor/gitignore/Java.gitignore9
-rw-r--r--vendor/gitignore/Joomla.gitignore27
-rw-r--r--vendor/gitignore/KiCad.gitignore3
-rw-r--r--vendor/gitignore/Laravel.gitignore6
-rw-r--r--vendor/gitignore/Magento.gitignore120
-rw-r--r--vendor/gitignore/Maven.gitignore3
-rw-r--r--vendor/gitignore/Node.gitignore17
-rw-r--r--vendor/gitignore/Objective-C.gitignore3
-rw-r--r--vendor/gitignore/Perl.gitignore39
-rw-r--r--vendor/gitignore/PureScript.gitignore8
-rw-r--r--vendor/gitignore/Python.gitignore3
-rw-r--r--vendor/gitignore/Scala.gitignore2
-rw-r--r--vendor/gitignore/Swift.gitignore4
-rw-r--r--vendor/gitignore/Symfony.gitignore5
-rw-r--r--vendor/gitignore/TeX.gitignore22
-rw-r--r--vendor/gitignore/Unity.gitignore4
-rw-r--r--vendor/gitignore/UnrealEngine.gitignore4
-rw-r--r--vendor/gitignore/VisualStudio.gitignore13
-rw-r--r--vendor/gitignore/Waf.gitignore13
-rw-r--r--vendor/gitlab-ci-yml/Docker.gitlab-ci.yml5
-rw-r--r--vendor/gitlab-ci-yml/Elixir.gitlab-ci.yml6
-rw-r--r--vendor/gitlab-ci-yml/Go.gitlab-ci.yml37
-rw-r--r--vendor/gitlab-ci-yml/Gradle.gitlab-ci.yml2
-rw-r--r--vendor/gitlab-ci-yml/Openshift.gitlab-ci.yml92
-rw-r--r--vendor/gitlab-ci-yml/autodeploy/Kubernetes.gitlab-ci.yml76
-rw-r--r--vendor/gitlab-ci-yml/autodeploy/OpenShift.gitlab-ci.yml76
2881 files changed, 74671 insertions, 22490 deletions
diff --git a/.eslintignore b/.eslintignore
index b4bfa5a1f7a..c742b08c005 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -1,7 +1,9 @@
+/builds/
/coverage/
/coverage-javascript/
/node_modules/
/public/
/tmp/
/vendor/
-/builds/
+karma.config.js
+webpack.config.js
diff --git a/.eslintrc b/.eslintrc
index e13f76b213c..1a2cd821af7 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -15,6 +15,9 @@
"filenames"
],
"rules": {
- "filenames/match-regex": [2, "^[a-z0-9_]+(.js)?$"]
+ "filenames/match-regex": [2, "^[a-z0-9_]+(.js)?$"],
+ "no-multiple-empty-lines": ["error", { "max": 1 }],
+ "import/no-extraneous-dependencies": "off",
+ "import/no-unresolved": "off"
}
}
diff --git a/.flayignore b/.flayignore
index 44df2ba2371..fc64b0b5892 100644
--- a/.flayignore
+++ b/.flayignore
@@ -1,3 +1,4 @@
*.erb
lib/gitlab/sanitizers/svg/whitelist.rb
lib/gitlab/diff/position_tracer.rb
+app/policies/project_policy.rb
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index b256e8a2a5f..5ab3648d9f2 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -35,7 +35,6 @@ stages:
.dedicated-runner: &dedicated-runner
tags:
- gitlab-org
- - 2gb
.knapsack-state: &knapsack-state
services: []
@@ -108,11 +107,13 @@ setup-test-env:
<<: *dedicated-runner
stage: prepare
script:
- - bundle exec rake assets:precompile 2>/dev/null
+ - npm install
+ - bundle exec rake gitlab:assets:compile
- bundle exec ruby -Ispec -e 'require "spec_helper" ; TestEnv.init'
artifacts:
expire_in: 7d
paths:
+ - node_modules
- public/assets
- tmp/tests
@@ -233,9 +234,16 @@ spinach 9 10 ruby21: *spinach-knapsack-ruby21
script:
- bundle exec $CI_BUILD_NAME
-rubocop: *exec
+rubocop:
+ <<: *ruby-static-analysis
+ <<: *dedicated-runner
+ stage: test
+ script:
+ - bundle exec "rubocop --require rubocop-rspec"
+
rake haml_lint: *exec
rake scss_lint: *exec
+rake config_lint: *exec
rake brakeman: *exec
rake flay: *exec
license_finder: *exec
@@ -266,7 +274,7 @@ rake db:migrate:reset:
<<: *use-db
<<: *dedicated-runner
script:
- - rake db:migrate:reset
+ - bundle exec rake db:migrate:reset
rake db:seed_fu:
stage: test
@@ -286,18 +294,17 @@ rake db:seed_fu:
paths:
- log/development.log
-teaspoon:
+karma:
cache:
paths:
- vendor/ruby
- - node_modules/
+ - node_modules
stage: test
<<: *use-db
<<: *dedicated-runner
script:
- - npm install
- npm link istanbul
- - rake teaspoon
+ - bundle exec rake karma
artifacts:
name: coverage-javascript
expire_in: 31d
@@ -348,10 +355,10 @@ migration paths:
- cp config/resque.yml.example config/resque.yml
- sed -i 's/localhost/redis/g' config/resque.yml
- bundle install --without postgres production --jobs $(nproc) $FLAGS --retry=3
- - rake db:drop db:create db:schema:load db:seed_fu
+ - bundle exec rake db:drop db:create db:schema:load db:seed_fu
- git checkout $CI_BUILD_REF
- source scripts/prepare_build.sh
- - rake db:migrate
+ - bundle exec rake db:migrate
coverage:
stage: post-test
@@ -425,7 +432,7 @@ notify:slack:
SETUP_DB: "false"
USE_BUNDLE_INSTALL: "false"
script:
- - ./scripts/notify_slack.sh "#development" "Build on \`$CI_BUILD_REF_NAME\` failed! Commit \`$(git log -1 --oneline)\` See <https://gitlab.com/gitlab-org/$(basename "$PWD")/commit/"$CI_BUILD_REF"/builds>"
+ - ./scripts/notify_slack.sh "#development" "Build on \`$CI_BUILD_REF_NAME\` failed! Commit \`$(git log -1 --oneline)\` See <https://gitlab.com/gitlab-org/$(basename "$PWD")/commit/"$CI_BUILD_REF"/pipelines>"
when: on_failure
only:
- master@gitlab-org/gitlab-ce
@@ -439,14 +446,14 @@ pages:
<<: *dedicated-runner
dependencies:
- coverage
- - teaspoon
+ - karma
- lint:javascript:report
script:
- mv public/ .public/
- mkdir public/
- - mv coverage public/coverage-ruby
- - mv coverage-javascript/default/ public/coverage-javascript/
- - mv eslint-report.html public/
+ - mv coverage/ public/coverage-ruby/ || true
+ - mv coverage-javascript/default/ public/coverage-javascript/ || true
+ - mv eslint-report.html public/ || true
artifacts:
paths:
- public
diff --git a/.haml-lint.yml b/.haml-lint.yml
index da9a43d9c6d..528f99d08d2 100644
--- a/.haml-lint.yml
+++ b/.haml-lint.yml
@@ -7,10 +7,10 @@ exclude:
linters:
AltText:
- enabled: false
+ enabled: true
ClassAttributeWithStaticValue:
- enabled: false
+ enabled: true
ClassesBeforeIds:
enabled: false
@@ -29,14 +29,14 @@ linters:
enabled: true
FinalNewline:
- enabled: false
+ enabled: true
present: true
HtmlAttributes:
- enabled: false
+ enabled: true
ImplicitDiv:
- enabled: false
+ enabled: true
LeadingCommentSpace:
enabled: false
@@ -46,7 +46,7 @@ linters:
max: 80
MultilinePipe:
- enabled: false
+ enabled: true
MultilineScript:
enabled: true
@@ -77,13 +77,13 @@ linters:
- Style/WhileUntilModifier
RubyComments:
- enabled: false
+ enabled: true
SpaceBeforeScript:
- enabled: false
+ enabled: true
SpaceInsideHashAttributes:
- enabled: false
+ enabled: true
style: space
Indentation:
@@ -94,10 +94,10 @@ linters:
enabled: true
TrailingWhitespace:
- enabled: false
+ enabled: true
UnnecessaryInterpolation:
- enabled: false
+ enabled: true
UnnecessaryStringOutput:
- enabled: false
+ enabled: true
diff --git a/.rubocop.yml b/.rubocop.yml
index 80eb4a5c19e..cfff42e5c99 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -17,6 +17,7 @@ AllCops:
# Exclude some GitLab files
Exclude:
- 'vendor/**/*'
+ - 'node_modules/**/*'
- 'db/*'
- 'db/fixtures/**/*'
- 'tmp/**/*'
@@ -343,10 +344,6 @@ Style/ParenthesesAroundCondition:
Style/RedundantParentheses:
Enabled: true
-# Don't use return where it's not required.
-Style/RedundantReturn:
- Enabled: true
-
# Don't use semicolons to terminate expressions.
Style/Semicolon:
Enabled: true
diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml
index 11b34fafa2a..d581610162f 100644
--- a/.rubocop_todo.yml
+++ b/.rubocop_todo.yml
@@ -1,55 +1,70 @@
# This configuration was generated by
# `rubocop --auto-gen-config --exclude-limit 0`
-# on 2016-10-04 13:16:20 +0200 using RuboCop version 0.43.0.
+# on 2017-01-11 09:38:25 +0000 using RuboCop version 0.46.0.
# 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: 160
+# Offense count: 27
+# Configuration parameters: Include.
+# Include: **/Gemfile, **/gems.rb
+Bundler/OrderedGems:
+ Enabled: false
+
+# Offense count: 175
Lint/AmbiguousRegexpLiteral:
Enabled: false
-# Offense count: 40
+# Offense count: 53
# Configuration parameters: AllowSafeAssignment.
Lint/AssignmentInCondition:
Enabled: false
-# Offense count: 18
+# Offense count: 1
+Lint/EmptyWhen:
+ Enabled: false
+
+# Offense count: 20
Lint/HandleExceptions:
Enabled: false
-# Offense count: 2
+# Offense count: 1
Lint/Loop:
Enabled: false
-# Offense count: 19
+# Offense count: 27
Lint/ShadowingOuterLocalVariable:
Enabled: false
-# Offense count: 9
+# Offense count: 10
# Cop supports --auto-correct.
Lint/UnifiedInteger:
Enabled: false
-# Offense count: 13
+# Offense count: 21
# Cop supports --auto-correct.
Lint/UnneededSplatExpansion:
Enabled: false
-# Offense count: 69
+# Offense count: 82
# Cop supports --auto-correct.
# Configuration parameters: IgnoreEmptyBlocks, AllowUnusedKeywordArguments.
Lint/UnusedBlockArgument:
Enabled: false
-# Offense count: 144
+# Offense count: 173
# Cop supports --auto-correct.
# Configuration parameters: AllowUnusedKeywordArguments, IgnoreEmptyMethods.
Lint/UnusedMethodArgument:
Enabled: false
-# Offense count: 2
+# Offense count: 93
+# Configuration parameters: CountComments.
+Metrics/BlockLength:
+ Enabled: false
+
+# Offense count: 3
# Cop supports --auto-correct.
Performance/RedundantBlockCall:
Enabled: false
@@ -59,7 +74,7 @@ Performance/RedundantBlockCall:
Performance/RedundantMatch:
Enabled: false
-# Offense count: 26
+# Offense count: 32
# Cop supports --auto-correct.
# Configuration parameters: MaxKeyValuePairs.
Performance/RedundantMerge:
@@ -69,61 +84,89 @@ Performance/RedundantMerge:
RSpec/BeEql:
Enabled: false
-# Offense count: 20
+# Offense count: 15
# Configuration parameters: CustomIncludeMethods.
RSpec/EmptyExampleGroup:
Enabled: false
-# Offense count: 16
+# Offense count: 24
RSpec/ExpectActual:
Enabled: false
-# Offense count: 34
+# Offense count: 58
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: implicit, each, example
RSpec/HookArgument:
Enabled: false
-# Offense count: 168
+# Offense count: 12
+# Configuration parameters: EnforcedStyle, SupportedStyles.
+# SupportedStyles: is_expected, should
+RSpec/ImplicitExpect:
+ Enabled: false
+
+# Offense count: 237
RSpec/LeadingSubject:
Enabled: false
-# Offense count: 162
+# Offense count: 253
RSpec/LetSetup:
Enabled: false
-# Offense count: 10
+# Offense count: 13
RSpec/MessageChain:
Enabled: false
-# Offense count: 714
+# Offense count: 479
# Configuration parameters: EnforcedStyle, SupportedStyles.
-# SupportedStyles: allow, expect
-RSpec/MessageExpectation:
+# SupportedStyles: have_received, receive
+RSpec/MessageSpies:
Enabled: false
-# Offense count: 2423
+# Offense count: 3036
RSpec/MultipleExpectations:
- Max: 36
+ Enabled: false
-# Offense count: 1504
+# Offense count: 2133
RSpec/NamedSubject:
Enabled: false
-# Offense count: 1335
+# Offense count: 1974
# Configuration parameters: MaxNesting.
RSpec/NestedGroups:
Enabled: false
-# Offense count: 99
+# Offense count: 32
+RSpec/RepeatedDescription:
+ Enabled: false
+
+# Offense count: 1
+RSpec/SingleArgumentMessageChain:
+ Enabled: false
+
+# Offense count: 133
RSpec/SubjectStub:
Enabled: false
-# Offense count: 64
+# Offense count: 104
+# Cop supports --auto-correct.
+# Configuration parameters: Whitelist.
+# Whitelist: find_by_sql
+Rails/DynamicFindBy:
+ Enabled: false
+
+# Offense count: 932
+# Cop supports --auto-correct.
+# Configuration parameters: Include.
+# Include: spec/**/*, test/**/*
+Rails/HttpPositionalArguments:
+ Enabled: false
+
+# Offense count: 55
Rails/OutputSafety:
Enabled: false
-# Offense count: 151
+# Offense count: 182
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: strict, flexible
Rails/TimeZone:
@@ -136,33 +179,34 @@ Rails/TimeZone:
Rails/Validation:
Enabled: false
-# Offense count: 2
+# Offense count: 8
# Cop supports --auto-correct.
+# Configuration parameters: AutoCorrect.
Security/JSONLoad:
Enabled: false
-# Offense count: 284
+# Offense count: 346
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles, IndentationWidth.
# SupportedStyles: with_first_parameter, with_fixed_indentation
Style/AlignParameters:
Enabled: false
-# Offense count: 28
+# Offense count: 27
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: always, conditionals
Style/AndOr:
Enabled: false
-# Offense count: 52
+# Offense count: 54
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: percent_q, bare_percent
Style/BarePercentLiterals:
Enabled: false
-# Offense count: 291
+# Offense count: 358
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: braces, no_braces, context_dependent
@@ -173,66 +217,73 @@ Style/BracesAroundHashParameters:
Style/CaseEquality:
Enabled: false
-# Offense count: 26
+# Offense count: 37
# Cop supports --auto-correct.
Style/ColonMethodCall:
Enabled: false
-# Offense count: 2
+# Offense count: 4
# Cop supports --auto-correct.
# Configuration parameters: Keywords.
# Keywords: TODO, FIXME, OPTIMIZE, HACK, REVIEW
Style/CommentAnnotation:
Enabled: false
-# Offense count: 30
+# Offense count: 29
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles, SingleLineConditionsOnly.
# SupportedStyles: assign_to_condition, assign_inside_condition
Style/ConditionalAssignment:
Enabled: false
-# Offense count: 957
+# Offense count: 1210
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: leading, trailing
Style/DotPosition:
Enabled: false
-# Offense count: 13
+# Offense count: 18
Style/DoubleNegation:
Enabled: false
-# Offense count: 6
+# Offense count: 7
# Cop supports --auto-correct.
Style/EachWithObject:
Enabled: false
-# Offense count: 26
+# Offense count: 24
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: empty, nil, both
Style/EmptyElse:
Enabled: false
-# Offense count: 3
+# Offense count: 4
# Cop supports --auto-correct.
Style/EmptyLiteral:
Enabled: false
-# Offense count: 140
+# Offense count: 57
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, SupportedStyles.
+# SupportedStyles: compact, expanded
+Style/EmptyMethod:
+ Enabled: false
+
+# Offense count: 147
# Cop supports --auto-correct.
# Configuration parameters: AllowForAlignment, ForceEqualSignAlignment.
Style/ExtraSpacing:
Enabled: false
-# Offense count: 6
+# Offense count: 8
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: format, sprintf, percent
Style/FormatString:
Enabled: false
-# Offense count: 201
+# Offense count: 238
# Configuration parameters: MinBodyLength.
Style/GuardClause:
Enabled: false
@@ -241,27 +292,27 @@ Style/GuardClause:
Style/IfInsideElse:
Enabled: false
-# Offense count: 174
+# Offense count: 173
# Cop supports --auto-correct.
# Configuration parameters: MaxLineLength.
Style/IfUnlessModifier:
Enabled: false
-# Offense count: 53
+# Offense count: 55
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles, IndentationWidth.
# SupportedStyles: special_inside_parentheses, consistent, align_brackets
Style/IndentArray:
Enabled: false
-# Offense count: 95
+# Offense count: 101
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles, IndentationWidth.
# SupportedStyles: special_inside_parentheses, consistent, align_braces
Style/IndentHash:
Enabled: false
-# Offense count: 29
+# Offense count: 41
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: line_count_dependent, lambda, literal
@@ -273,16 +324,21 @@ Style/Lambda:
Style/LineEndConcatenation:
Enabled: false
-# Offense count: 15
+# Offense count: 19
# Cop supports --auto-correct.
Style/MethodCallParentheses:
Enabled: false
-# Offense count: 8
+# Offense count: 9
Style/MethodMissing:
Enabled: false
-# Offense count: 95
+# Offense count: 3
+# Cop supports --auto-correct.
+Style/MultilineIfModifier:
+ Enabled: false
+
+# Offense count: 179
# Cop supports --auto-correct.
Style/MutableConstant:
Enabled: false
@@ -299,32 +355,32 @@ Style/NestedParenthesizedCalls:
Style/Next:
Enabled: false
-# Offense count: 12
+# Offense count: 19
# Cop supports --auto-correct.
# Configuration parameters: EnforcedOctalStyle, SupportedOctalStyles.
# SupportedOctalStyles: zero_with_o, zero_only
Style/NumericLiteralPrefix:
Enabled: false
-# Offense count: 53
+# Offense count: 19
# Cop supports --auto-correct.
-# Configuration parameters: EnforcedStyle, SupportedStyles.
+# Configuration parameters: AutoCorrect, EnforcedStyle, SupportedStyles.
# SupportedStyles: predicate, comparison
Style/NumericPredicate:
Enabled: false
-# Offense count: 29
+# Offense count: 34
# Cop supports --auto-correct.
Style/ParallelAssignment:
Enabled: false
-# Offense count: 294
+# Offense count: 417
# Cop supports --auto-correct.
# Configuration parameters: PreferredDelimiters.
Style/PercentLiteralDelimiters:
Enabled: false
-# Offense count: 11
+# Offense count: 10
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: lower_case_q, upper_case_q
@@ -336,7 +392,7 @@ Style/PercentQLiterals:
Style/PerlBackrefs:
Enabled: false
-# Offense count: 38
+# Offense count: 64
# Configuration parameters: NamePrefix, NamePrefixBlacklist, NameWhitelist.
# NamePrefix: is_, has_, have_
# NamePrefixBlacklist: is_, has_, have_
@@ -344,17 +400,19 @@ Style/PerlBackrefs:
Style/PredicateName:
Enabled: false
-# Offense count: 26
+# Offense count: 33
# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, SupportedStyles.
+# SupportedStyles: short, verbose
Style/PreferredHashMethods:
Enabled: false
-# Offense count: 6
+# Offense count: 8
# Cop supports --auto-correct.
Style/Proc:
Enabled: false
-# Offense count: 22
+# Offense count: 50
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: compact, exploded
@@ -371,33 +429,34 @@ Style/RedundantBegin:
Style/RedundantException:
Enabled: false
-# Offense count: 24
+# Offense count: 29
# Cop supports --auto-correct.
Style/RedundantFreeze:
Enabled: false
-# Offense count: 427
+# Offense count: 11
+# Cop supports --auto-correct.
+# Configuration parameters: AllowMultipleReturnValues.
+Style/RedundantReturn:
+ Enabled: false
+
+# Offense count: 359
# Cop supports --auto-correct.
Style/RedundantSelf:
Enabled: false
-# Offense count: 97
+# Offense count: 105
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles, AllowInnerSlashes.
# SupportedStyles: slashes, percent_r, mixed
Style/RegexpLiteral:
Enabled: false
-# Offense count: 18
+# Offense count: 19
# Cop supports --auto-correct.
Style/RescueModifier:
Enabled: false
-# Offense count: 114
-# Cop supports --auto-correct.
-Style/SafeNavigation:
- Enabled: false
-
# Offense count: 7
# Cop supports --auto-correct.
Style/SelfAssignment:
@@ -405,7 +464,7 @@ Style/SelfAssignment:
# Offense count: 2
# Configuration parameters: Methods.
-# Methods: {"reduce"=>["a", "e"]}, {"inject"=>["a", "e"]}
+# Methods: {"reduce"=>["acc", "elem"]}, {"inject"=>["acc", "elem"]}
Style/SingleLineBlockParams:
Enabled: false
@@ -415,56 +474,63 @@ Style/SingleLineBlockParams:
Style/SingleLineMethods:
Enabled: false
-# Offense count: 125
+# Offense count: 138
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: space, no_space
Style/SpaceBeforeBlockBraces:
Enabled: false
-# Offense count: 10
+# Offense count: 8
# Cop supports --auto-correct.
# Configuration parameters: AllowForAlignment.
Style/SpaceBeforeFirstArg:
Enabled: false
-# Offense count: 145
+# Offense count: 37
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, SupportedStyles.
+# SupportedStyles: require_no_space, require_space
+Style/SpaceInLambdaLiteral:
+ Enabled: false
+
+# Offense count: 174
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles, EnforcedStyleForEmptyBraces, SpaceBeforeBlockParameters.
# SupportedStyles: space, no_space
Style/SpaceInsideBlockBraces:
Enabled: false
-# Offense count: 99
+# Offense count: 115
# Cop supports --auto-correct.
Style/SpaceInsideBrackets:
Enabled: false
-# Offense count: 65
+# Offense count: 77
# Cop supports --auto-correct.
Style/SpaceInsideParens:
Enabled: false
-# Offense count: 7
+# Offense count: 4
# Cop supports --auto-correct.
Style/SpaceInsidePercentLiteralDelimiters:
Enabled: false
-# Offense count: 41
+# Offense count: 53
# Cop supports --auto-correct.
# Configuration parameters: SupportedStyles.
# SupportedStyles: use_perl_names, use_english_names
Style/SpecialGlobalVars:
EnforcedStyle: use_perl_names
-# Offense count: 31
+# Offense count: 25
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: single_quotes, double_quotes
Style/StringLiteralsInInterpolation:
Enabled: false
-# Offense count: 33
+# Offense count: 54
# Cop supports --auto-correct.
# Configuration parameters: IgnoredMethods.
# IgnoredMethods: respond_to, define_method
@@ -474,18 +540,18 @@ Style/SymbolProc:
# Offense count: 5
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles, AllowSafeAssignment.
-# SupportedStyles: require_parentheses, require_no_parentheses
+# SupportedStyles: require_parentheses, require_no_parentheses, require_parentheses_when_complex
Style/TernaryParentheses:
Enabled: false
-# Offense count: 29
+# Offense count: 36
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyleForMultiline, SupportedStyles.
# SupportedStyles: comma, consistent_comma, no_comma
Style/TrailingCommaInArguments:
Enabled: false
-# Offense count: 102
+# Offense count: 150
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyleForMultiline, SupportedStyles.
# SupportedStyles: comma, consistent_comma, no_comma
@@ -498,12 +564,12 @@ Style/TrailingCommaInLiteral:
Style/TrailingUnderscoreVariable:
Enabled: false
-# Offense count: 76
+# Offense count: 67
# Cop supports --auto-correct.
Style/TrailingWhitespace:
Enabled: false
-# Offense count: 2
+# Offense count: 3
# 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
@@ -515,7 +581,7 @@ Style/TrivialAccessors:
Style/UnlessElse:
Enabled: false
-# Offense count: 14
+# Offense count: 17
# Cop supports --auto-correct.
Style/UnneededInterpolation:
Enabled: false
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 786b0128aac..71d38e5453d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,454 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
+## 8.16.4 (2017-02-02)
+
+- Support non-ASCII characters in GFM autocomplete. !8729
+- Fix search bar search param encoding. !8753
+- Fix project name label's for reference in project settings. !8795
+- Fix filtering with multiple words. !8830
+- Fixed services form cancel not redirecting back the integrations settings view. !8843
+- Fix filtering usernames with multiple words. !8851
+- Improve performance of slash commands. !8876
+- Remove old project members when retrying an export.
+- Fix permalink discussion note being collapsed.
+- Add project ID index to `project_authorizations` table to optimize queries.
+- Check public snippets for spam.
+- 19164 Add settings dropdown to mobile screens.
+
+## 8.16.3 (2017-01-27)
+
+- Add caching of droplab ajax requests. !8725
+- Fix access to the wiki code via HTTP when repository feature disabled. !8758
+- Revert 3f17f29a. !8785
+- Fix race conditions for AuthorizedProjectsWorker.
+- Fix autocomplete initial undefined state.
+- Fix Error 500 when repositories contain annotated tags pointing to blobs.
+- Fix /explore sorting.
+- Fixed label dropdown toggle text not correctly updating.
+
+## 8.16.2 (2017-01-25)
+
+- allow issue filter bar to be operated with mouse only. !8681
+- Fix CI requests concurrency for newer runners that prevents from picking pending builds (from 1.9.0-rc5). !8760
+- Add some basic fixes for IE11/Edge.
+- Remove blue border from comment box hover.
+- Fixed bug where links in merge dropdown wouldn't work.
+
+## 8.16.1 (2017-01-23)
+
+- Ensure export files are removed after a namespace is deleted.
+- Don't allow project guests to subscribe to merge requests through the API. (Robert Schilling)
+- Prevent users from creating notes on resources they can't access.
+- Prevent users from deleting system deploy keys via the project deploy key API.
+- Upgrade omniauth gem to 1.3.2.
+
+## 8.16.0 (2017-01-22)
+
+- Add LDAP Rake task to rename a provider. !2181
+- Validate label's title length. !5767 (Tomáš Kukrál)
+- Allow to add deploy keys with write-access. !5807 (Ali Ibrahim)
+- Allow to use + symbol in filenames. !6644 (blackst0ne)
+- Search bar redesign first iteration. !7345
+- Fix date inconsistency on due date picker. !7422 (Giuliano Varriale)
+- Add email confirmation field to registration form. !7432
+- Updated project visibility settings UX. !7645
+- Go to a project order. !7737 (Jacopo Beschi @jacopo-beschi)
+- Support slash comand `/merge` for merging merge requests. !7746 (Jarka Kadlecova)
+- Add more storage statistics. !7754 (Markus Koller)
+- Add support for PlantUML diagrams in AsciiDoc documents. !7810 (Horacio Sanson)
+- Remove extra orphaned rows when removing stray namespaces. !7841
+- Added lighter count badge background-color for on white backgrounds. !7873
+- Fixes issue boards list colored top border visual glitch. !7898 (Pier Paolo Ramon)
+- change 'gray' color theme name to 'black' to match the actual color. !7908 (BM5k)
+- Remove trailing whitespace when generating changelog entry. !7948
+- Remove checking branches state in issue new branch button. !8023
+- Log LDAP blocking/unblocking events to application log. !8042 (Markus Koller)
+- ensure permalinks scroll to correct position on multiple clicks. !8046
+- Allow to use ENV variables in redis config. !8073 (Semyon Pupkov)
+- fix button layout issue on branches page. !8074
+- Reduce DB-load for build-queues by storing last_update in Redis. !8084
+- Record and show last used date of SSH Keys. !8113 (Vincent Wong)
+- Resolves overflow in compare branch and tags dropdown. !8118
+- Replace wording for slash command confirmation message. !8123
+- remove build_user. !8162 (Arsenev Vladislav)
+- Prevent empty pagination when list is not empty. !8172
+- Make successful pipeline emails off for watchers. !8176
+- Improve copy in Issue Tracker empty state. !8202
+- Adds CSS class to status icon on MR widget to prevent non-colored icon. !8219
+- Improve visibility of "Resolve conflicts" and "Merge locally" actions. !8229
+- Add Gitaly to the architecture documentation. !8264 (Pablo Carranza <pablo@gitlab.com>)
+- Sort numbers in build names more intelligently. !8277
+- Show nested groups tab on group page. !8308
+- Rename users with namespace ending with .git. !8309
+- Rename filename to file path in tooltip of file header in merge request diff. !8314
+- About GitLab link in sidebar that links to help page. !8316
+- Merged the 'Groups' and 'Projects' tabs when viewing user profiles. !8323 (James Gregory)
+- re-enable change username button after failure. !8332
+- Darkened hr border color in descriptions because of update of bootstrap. !8333
+- display merge request discussion tab for empty branches. !8347
+- Fix double spaced CI log. !8349 (Jared Deckard <jared.deckard@gmail.com>)
+- Refactored note edit form to improve frontend performance on MR and Issues pages, especially pages with has a lot of discussions in it. !8356
+- Make CTRL+Enter submits a new merge request. !8360 (Saad Shahd)
+- Fixes too short input for placeholder message in commit listing page. !8367
+- Fix typo: seach to search. !8370
+- Adds label to Environments "Date Created". !8376 (Saad Shahd)
+- Convert project setting text into protected branch path link. !8377 (Ken Ding)
+- Precompile all JavaScript fixtures. !8384
+- Use original casing for build action text. !8387
+- Scroll to bottom on build completion if autoscroll was active. !8391
+- Properly handle failed reCAPTCHA on user registration. !8403
+- Changed alerts to be responsive, centered text on smaller viewports. !8424 (Connor Smallman)
+- Pass Gitaly resource path to gitlab-workhorse if Gitaly is enabled. !8440
+- Fixes and Improves CSS and HTML problems in mini pipeline graph and builds dropdown. !8443
+- Don't instrument 405 Grape calls. !8445
+- Change CI template linter textarea with Ace Editor. !8452 (Didem Acet)
+- Removes unneeded `window` declaration in environments related code. !8456
+- API: fix query response for `/projects/:id/issues?milestone="No%20Milestone"`. !8457 (Panagiotis Atmatzidis, David Eisner)
+- Fix broken url on group avatar. !8464 (hogewest)
+- Fixes buttons not being accessible via the keyboard when creating new group. !8469
+- Restore backup correctly when "BACKUP" environment variable is passed. !8477
+- Add new endpoints for Time Tracking. !8483
+- Fix Compare page throws 500 error when any branch/reference is not selected. !8492 (Martin Cabrera)
+- Treat environments matching `production/*` as Production. !8500
+- Hide build artifacts keep button if operation is not allowed. !8501
+- Update the gitlab-markup gem to the version 1.5.1. !8509
+- Remove Lock Icon on Protected Tag. !8513 (Sergey Nikitin)
+- Use cached values to compute total issues count in milestone index pages. !8518
+- Speed up dashboard milestone index by scoping IssuesFinder to user authorized projects. !8524
+- Copy <some text> to clipboard. !8535
+- Check for env[Grape::Env::GRAPE_ROUTING_ARGS] instead of endpoint.route. !8544
+- Fixes builds dropdown making request when clicked to be closed. !8545
+- Fixes pipeline status cell is too wide by adding missing classes in table head cells. !8549
+- Mutate the attribute instead of issuing a write operation to the DB in `ProjectFeaturesCompatibility` concern. !8552
+- Fix links to commits pages on pipelines list page. !8558
+- Ensure updating project settings shows a flash message on success. !8579 (Sandish Chen)
+- Fixes big pipeline and small pipeline width problems and tooltips text being outside the tooltip. !8593
+- Autoresize markdown preview. !8607 (Didem Acet)
+- Link external build badge to its target URL. !8611
+- Adjust ProjectStatistic#repository_size with values saved as MB. !8616
+- Correct User-agent placement in robots.txt. !8623 (Eric Sabelhaus)
+- Record used SSH keys only once per day. !8655
+- Do not generate pipeline branch/tag path if not present. !8658
+- Fix Merge When Pipeline Succeeds immediate merge bug. !8685
+- Fix blame 500 error on invalid path. !25761 (Jeff Stubler)
+- Added animations to issue boards interactions.
+- Check if user can read project before being assigned to issue.
+- Show 'too many changes' message for created merge requests when they are too large.
+- Fix redirect after update file when user has forked project.
+- Parse JIRA issue references even if Issue Tracker is disabled.
+- Made download artifacts button accessible via keyboard by changing it from an anchor tag to an actual button. (Ryan Harris)
+- Make play button on Pipelines page accessible via keyboard. (Ryan Harris)
+- Decreases font-size on login page.
+- Fixed merge request tabs dont move when opening collapsed sidebar.
+- Display project avatars on Admin Area and Projects pages for mobile views. (Ryan Harris)
+- Fix participants margins to fit on one line.
+- 26352 Change Profile settings to User / Settings.
+- Fix Commits API to accept a Project path upon POST.
+- Expire related caches after changing HEAD. (Minqi Pan)
+- Add various hover animations throughout the application.
+- Re-order update steps in the 8.14 -> 8.15 upgrade guide.
+- Move award emoji's out of the discussion tab for merge requests.
+- Synchronize all project authorization refreshing work to prevent race conditions.
+- Remove the project_authorizations.id column.
+- Combined the settings options project members and groups into a single one called members.
+- Change earlier to task_status_short to avoid titlebar line wraps.
+- 25701 standardize text colors.
+- Handle HTTP errors in environment list.
+- Re-add Google Cloud Storage as a backup strategy.
+- Change status colors of runners to better defaults.
+- Added number_with_delimiter to counter on milestone panels. (Ryan Harris)
+- Query external CI statuses in the background.
+- Allow group and project paths when transferring projects via the API.
+- Don't validate environment urls on .gitlab-ci.yml.
+- Fix a Grape deprecation, use `#request_method` instead of `#route_method`.
+- Fill missing authorized projects rows.
+- Allow API query to find projects with dots in their name. (Bruno Melli)
+- Fix import/export wrong user mapping.
+- Removed bottom padding from merge manually from CLI because of repositioning award emoji's.
+- Fix project queued for deletion re-creation tooltip.
+- Fix search group/project filtering to show results.
+- Fix 500 error when POSTing to Users API with optional confirm param.
+- 26504 Fix styling of MR jump to discussion button.
+- Add margin to markdown math blocks.
+- Add hover state to MR comment reply button.
+
+## 8.15.4 (2017-01-09)
+
+- Make successful pipeline emails off for watchers. !8176
+- Speed up group milestone index by passing group_id to IssuesFinder. !8363
+- Don't instrument 405 Grape calls. !8445
+- Update the gitlab-markup gem to the version 1.5.1. !8509
+- Updated Turbolinks to mitigate potential XSS attacks.
+- Re-order update steps in the 8.14 -> 8.15 upgrade guide.
+- Re-add Google Cloud Storage as a backup strategy.
+
+## 8.15.3 (2017-01-06)
+
+- Rename wiki_events to wiki_page_events in project hooks API to avoid errors. !8425
+- Rename projects wth reserved names. !8234
+- Cache project authorizations even when user has access to zero projects. !8327
+- Fix a minor grammar error in merge request widget. !8337
+- Fix unclear closing issue behaviour on Merge Request show page. !8345 (Gabriel Gizotti)
+- fix border in login session tabs. !8346
+- Copy, don't move uploaded avatar files. !8396
+- Increases width of mini-pipeline-graph dropdown to prevent wrong position on chrome on ubuntu. !8399
+- Removes invalid html and unneed CSS to prevent shaking in the pipelines tab. !8411
+- Gitlab::LDAP::Person uses LDAP attributes configuration. !8418
+- Fix 500 errors when creating a user with identity via API. !8442
+- Whitelist next project names: assets, profile, public. !8470
+- Fixed regression of note-headline-light where it was always placed on 2 lines, even on wide viewports.
+- Fix 500 error when visit group from admin area if group name contains dot.
+- Fix cross-project references copy to include the project reference.
+- Fix 500 error renaming group.
+- Fixed GFM dropdown not showing on new lines.
+
+## 8.15.2 (2016-12-27)
+
+- Fix finding the latest pipeline. !8301
+- Fix mr list timestamp alignment. !8271
+- Fix discussion overlap text in regular screens. !8273
+- Fixes mini-pipeline-graph dropdown animation and stage position in chrome, firefox and safari. !8282
+- Fix line breaking in nodes of the pipeline graph in firefox. !8292
+- Fixes confendential warning text alignment. !8293
+- Hide Scroll Top button for failed build page. !8295
+- Fix finding the latest pipeline. !8301
+- Disable PostgreSQL statement timeouts when removing unneeded services. !8322
+- Fix timeout when MR contains large files marked as binary by .gitattributes.
+- Rename "autodeploy" to "auto deploy".
+- Fixed GFM autocomplete error when no data exists.
+- Fixed resolve discussion note button color.
+
+## 8.15.1 (2016-12-23)
+
+- Push payloads schedule at most 100 commits, instead of all commits.
+- Fix Mattermost command creation by specifying username.
+- Do not override incoming webhook for mattermost and slack.
+- Adds background color for disabled state to merge when succeeds dropdown. !8222
+- Standardises font-size for titles in Issues, Merge Requests and Merge Request widget. !8235
+- Fix Pipeline builds list blank on MR. !8255
+- Do not show retried builds in pipeline stage dropdown. !8260
+
+## 8.15.0 (2016-12-22)
+
+- Whitelist next project names: notes, services.
+- Use Grape's new Route methods.
+- Fixed issue boards scrolling with a lot of lists & issues.
+- Remove unnecessary sentences for status codes in the API documentation. (Luis Alonso Chavez Armendariz)
+- Allow unauthenticated access to Repositories Files API GET endpoints.
+- Add note to the invite page when the logged in user email is not the same as the invitation.
+- Don't accidentally mark unsafe diff lines as HTML safe.
+- Add git diff context to notifications of new notes on merge requests. (Heidi Hoopes)
+- Shows group members in project members list.
+- Gem update: Update grape to 0.18.0. (Robert Schilling)
+- API: Expose merge status for branch API. (Robert Schilling)
+- Displays milestone remaining days only when it's present.
+- API: Expose committer details for commits. (Robert Schilling)
+- API: Ability to set 'should_remove_source_branch' on merge requests. (Robert Schilling)
+- Fix project import label priorities error.
+- Fix Import/Export merge requests error while importing.
+- Refactor Bitbucket importer to use BitBucket API Version 2.
+- Fix Import/Export duplicated builds error.
+- Ci::Builds have same ref as Ci::Pipeline in dev fixtures. (twonegatives)
+- For single line git commit messages, the close quote should be on the same line as the open quote.
+- Use authorized projects in ProjectTeam.
+- Destroy a user's session when they delete their own account.
+- Edit help text to clarify annotated tag creation. (Liz Lam)
+- Fixed file template dropdown for the "New File" editor for smaller/zoomed screens.
+- Fix Route#rename_children behavior.
+- Add nested groups support on data level.
+- Allow projects with 'dashboard' as path.
+- Disabled emoji buttons when user is not logged in.
+- Remove unused and void services from the database.
+- Add issue search slash command.
+- Accept issue new as command to create an issue.
+- Non members cannot create labels through the API.
+- API: expose pipeline coverage.
+- Validate state param when filtering issuables.
+- Username exists check respects relative root path.
+- Bump Git version requirement to 2.8.4.
+- Updates the font weight of button styles because of the change to system fonts.
+- Update API spec files to describe the correct class. (Livier)
+- Fixed timeago re-rendering every timeago.
+- Enable ColorVariable in scss-lint. (Sam Rose)
+- Various small emoji positioning adjustments.
+- Add shortcuts for adding users to a project team with a specific role. (Nikolay Ponomarev and Dino M)
+- Additional rounded label fixes.
+- Remove unnecessary database indices.
+- 24726 Remove Across GitLab from side navigation.
+- Changed cursor icon to pointer when mousing over stages on the Cycle Analytics pages. (Ryan Harris)
+- Add focus state to dropdown items.
+- Fixes Environments displaying incorrect date since 8.14 upgrade.
+- Improve bulk assignment for issuables.
+- Stop supporting Google and Azure as backup strategies.
+- Fix broken README.md UX guide link.
+- Allow public access to some Tag API endpoints.
+- Encode input when migrating ProcessCommitWorker jobs to prevent migration errors.
+- Adjust the width of project avatars to fix alignment within their container. (Ryan Harris)
+- Sentence cased the nav tab headers on the project dashboard page. (Ryan Harris)
+- Adds hoverstates for collapsed Issue/Merge Request sidebar.
+- Make CI badge hitboxes match parent.
+- Add a starting date to milestones.
+- Adjusted margins for Build Status and Coverage Report rows to match those of the CI/CD Pipeline row. (Ryan Harris)
+- Updated members dropdowns.
+- Move all action buttons to project header.
+- Replace issue access checks with use of IssuableFinder.
+- Fix missing Note access checks by moving Note#search to updated NoteFinder.
+- Centered Accept Merge Request button within MR widget and added padding for viewports smaller than 768px. (Ryan Harris)
+- Fix missing access checks on issue lookup using IssuableFinder.
+- Added top margin to Build status page header for mobile views. (Ryan Harris)
+- Fixes "ActionView::Template::Error: undefined method `text?` for nil:NilClass" on MR pages.
+- Issue#visible_to_user moved to IssuesFinder to prevent accidental use.
+- Replace MR access checks with use of MergeRequestsFinder.
+- Fix information disclosure in `Projects::BlobController#update`.
+- Allow branch names with dots on API endpoint.
+- Changed Housekeeping button on project settings page to default styling. (Ryan Harris)
+- Ensure issuable state changes only fire webhooks once.
+- Fix bad selection on dropdown menu for tags filter. (Luis Alonso Chavez Armendariz)
+- Fix title case to sentence case. (Luis Alonso Chavez Armendariz)
+- Fix appearance in error pages. (Luis Alonso Chavez Armendariz)
+- Create mattermost service.
+- 25617 Fix placeholder color of todo filters.
+- Made the padding on the plus button in the breadcrumb menu even. (Ryan Harris)
+- Allow to delete tag release note.
+- Ensure nil User-Agent doesn't break the CI API.
+- Replace Rack::Multipart with GitLab-Workhorse based solution. !5867
+- Add scopes for personal access tokens and OAuth tokens. !5951
+- API: Endpoint to expose personal snippets as /snippets. !6373 (Bernard Guyzmo Pratz)
+- New `gitlab:workhorse:install` rake task. !6574
+- Filter protocol-relative URLs in ExternalLinkFilter. Fixes issue #22742. !6635 (Makoto Scott-Hinkle)
+- Add support for setting the GitLab Runners Registration Token during initial database seeding. !6642
+- Guests can read builds when public. !6842
+- Made comment autocomplete more performant and removed some loading bugs. !6856
+- Add GitLab host to 2FA QR code and manual info. !6941
+- Add sorting functionality for group/project members. !7032
+- Rename Merge When Build Succeeds to Merge When Pipeline Succeeds. !7135
+- Resolve all discussions in a merge request by creating an issue collecting them. !7180 (Bob Van Landuyt)
+- Add Human Readable format for rake backup. !7188 (David Gerő)
+- post_receive: accept any user email from last commit. !7225 (Elan Ruusamäe)
+- Add support for Dockerfile templates. !7247
+- Add shorthand support to gitlab markdown references. !7255 (Oswaldo Ferreira)
+- Display error code for U2F errors. !7305 (winniehell)
+- Fix wrong tab selected when loggin fails and multiple login tabs exists. !7314 (Jacopo Beschi @jacopo-beschi)
+- Clean up common_utils.js. !7318 (winniehell)
+- Show commit status from latest pipeline. !7333
+- Remove the help text under the sidebar subscribe button and style it inline. !7389
+- Update wiki page design. !7429
+- Add nested groups support to the routing. !7459
+- Changed eslint airbnb config to the base airbnb config and corrected eslintrc plugins and envs. !7470 (Luke "Jared" Bennett)
+- Fix cancelling created or external pipelines. !7508
+- Allow admins to stop impersonating users without e-mail addresses. !7550 (Oren Kanner)
+- Remove unnecessary self from user model. !7551 (Semyon Pupkov)
+- Homogenize filter and sort dropdown look'n'feel. !7583 (David Wagner)
+- Create dynamic fixture for build_spec. !7589 (winniehell)
+- Moved Leave Project and Leave Group buttons to access_request_buttons from the settings dropdown. !7600
+- Remove unnecessary require_relative calls from service classes. !7601 (Semyon Pupkov)
+- Simplify copy on "Create a new list" dropdown in Issue Boards. !7605 (Victor Rodrigues)
+- Refactor create service spec. !7609 (Semyon Pupkov)
+- Shows unconfirmed email status in profile. !7611
+- The admin user projects view now has a clickable group link. !7620 (James Gregory)
+- Prevent DOM ID collisions resulting from user-generated content anchors. !7631
+- Replace static fixture for abuse_reports_spec. !7644 (winniehell)
+- Define common helper for describe pagination params in api. !7646 (Semyon Pupkov)
+- Move abuse report spinach test to rspec. !7659 (Semyon Pupkov)
+- Replace static fixture for awards_handler_spec. !7661 (winniehell)
+- API: Add ability to unshare a project from a group. !7662 (Robert Schilling)
+- Replace references to MergeRequestDiff#commits with st_commits when we care only about the number of commits. !7668
+- Add issue events filter and make all really show all events. !7673 (Oxan van Leeuwen)
+- Replace static fixture for notes_spec. !7683 (winniehell)
+- Replace static fixture for shortcuts_issuable_spec. !7685 (winniehell)
+- Replace static fixture for zen_mode_spec. !7686 (winniehell)
+- Replace static fixture for right_sidebar_spec. !7687 (winniehell)
+- Add online terminal support for Kubernetes. !7690
+- Move admin abuse report spinach test to rspec. !7691 (Semyon Pupkov)
+- Move admin spam spinach test to Rspec. !7708 (Semyon Pupkov)
+- Make API::Helpers find a project with only one query. !7714
+- Create builds in transaction to avoid empty pipelines. !7742
+- Render SVG images in diffs and notes. !7747 (andrebsguedes)
+- Add setting to enable/disable HTML emails. !7749
+- Use SmartInterval for MR widget and improve visibilitychange functionality. !7762
+- Resolve "Remove Builds tab from Merge Requests and Commits". !7763
+- Moved new projects button below new group button on the welcome screen. !7770
+- fix display hook error message. !7775 (basyura)
+- Refactor issuable_filters_present to reduce duplications. !7776 (Semyon Pupkov)
+- Redirect to sign-in page when unauthenticated user tries to create a snippet. !7786
+- Fix Archived project merge requests add to group's Merge Requests. !7790 (Jacopo Beschi @jacopo-beschi)
+- Update generic/external build status to match normal build status template. !7811
+- Enable AsciiDoctor admonition icons. !7812 (Horacio Sanson)
+- Do not raise error in AutocompleteController#users when not authorized. !7817 (Semyon Pupkov)
+- fix: 24982- Remove'Signed in successfully' message After this change the sign-in-success flash message will not be shown. !7837 (jnoortheen)
+- Fix Latest deployment link is broken. !7839
+- Don't display prompt to add SSH keys if SSH protocol is disabled. !7840 (Andrew Smith (EspadaV8))
+- Allow unauthenticated access to some Project API GET endpoints. !7843
+- Refactor presenters ChatCommands. !7846
+- Improve help message for issue create slash command. !7850
+- change text around timestamps to make it clear which timestamp is displayed. !7860 (BM5k)
+- Improve Build Log scrolling experience. !7895
+- Change ref property to commitRef in vue commit component. !7901
+- Prevent user creating issue or MR without signing in for a group. !7902
+- Provides a sensible default message when adding a README to a project. !7903
+- Bump ruby version to 2.3.3. !7904
+- Fix comments activity tab visibility condition. !7913 (Rydkin Maxim)
+- Remove unnecessary target branch link from MR page in case of deleted target branch. !7916 (Rydkin Maxim)
+- Add image controls to MR diffs. !7919
+- Remove wrong '.builds-feature' class from the MR settings fieldset. !7930
+- Resolve "Manual actions on pipeline graph". !7931
+- Avoid escaping relative links in Markdown twice. !7940 (winniehell)
+- Move admin hooks spinach to rspec. !7942 (Semyon Pupkov)
+- Move admin logs spinach test to rspec. !7945 (Semyon Pupkov)
+- fix: removed signed_out notification. !7958 (jnoortheen)
+- Accept environment variables from the `pre-receive` script. !7967
+- Do not reload diff for merge request made from fork when target branch in fork is updated. !7973
+- Fixes left align issue for long system notes. !7982
+- Add a slug to environments. !7983
+- Fix lookup of project by unknown ref when caching is enabled. !7988
+- Resolve "Provide SVG as a prop instead of hiding and copy them in environments table". !7992
+- Introduce deployment services, starting with a KubernetesService. !7994
+- Adds tests for custom event polyfill. !7996
+- Allow all alphanumeric characters in file names. !8002 (winniehell)
+- Added support for math rendering, using KaTeX, in Markdown and asciidoc. !8003 (Munken)
+- Remove unnecessary commits order message. !8004
+- API: Memoize the current_user so that sudo can work properly. !8017
+- group authors in contribution graph with case insensitive email handle comparison. !8021
+- Move admin active tab spinach tests to rspec. !8037 (Semyon Pupkov)
+- Add Authentiq as Oauth provider. !8038 (Alexandros Keramidas)
+- API: Ability to cherry pick a commit. !8047 (Robert Schilling)
+- Fix Slack pipeline message from pipelines made by API. !8059
+- API: Simple representation of group's projects. !8060 (Robert Schilling)
+- Prevent overflow with vertical scroll when we have space to show content. !8061
+- Allow to auto-configure Mattermost. !8070
+- Introduce $CI_BUILD_REF_SLUG. !8072
+- Added go back anchor on error pages. !8087
+- Convert CI YAML variables keys into strings. !8088
+- Adds Direct link from pipeline list to builds. !8097
+- Cache last commit id for path. !8098 (Hiroyuki Sato)
+- Pass variables from deployment project services to CI runner. !8107
+- New Gitea importer. !8116
+- Introduce "Set up autodeploy" button to help configure GitLab CI for deployment. !8135
+- Prevent enviroment table to overflow when name has underscores. !8142
+- Fix missing service error importing from EE to CE. !8144
+- Milestoneish SQL performance partially improved and memoized. !8146
+- Allow unauthenticated access to Repositories API GET endpoints. !8148
+- fix colors and margins for adjacent alert banners. !8151
+- Hides new issue button for non loggedin user. !8175
+- Fix N+1 queries on milestone show pages. !8185
+- Rename groups with .git in the end of the path. !8199
+- Whitelist next project names: help, ci, admin, search. !8227
+- Adds back CSS for progress-bars. !8237
+
+## 8.14.8 (2017-01-25)
+
+- Accept environment variables from the `pre-receive` script. !7967
+- Milestoneish SQL performance partially improved and memoized. !8146
+- Fix N+1 queries on milestone show pages. !8185
+- Speed up group milestone index by passing group_id to IssuesFinder. !8363
+- Ensure issuable state changes only fire webhooks once.
+
+## 8.14.6 (2017-01-10)
+
+- Update the gitlab-markup gem to the version 1.5.1. !8509
+- Updated Turbolinks to mitigate potential XSS attacks.
+
## 8.14.5 (2016-12-14)
- Moved Leave Project and Leave Group buttons to access_request_buttons from the settings dropdown. !7600
@@ -279,6 +727,11 @@ entry.
- Fix "Without projects" filter. !6611 (Ben Bodenmiller)
- Fix 404 when visit /projects page
+## 8.13.11 (2017-01-10)
+
+- Update the gitlab-markup gem to the version 1.5.1. !8509
+- Updated Turbolinks to mitigate potential XSS attacks.
+
## 8.13.10 (2016-12-14)
- API: Memoize the current_user so that sudo can work properly. !8017
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 659871a06a4..315cd1e598c 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -15,6 +15,7 @@
- [Issue weight](#issue-weight)
- [Regression issues](#regression-issues)
- [Technical debt](#technical-debt)
+ - [Stewardship][#stewardship]
- [Merge requests](#merge-requests)
- [Merge request guidelines](#merge-request-guidelines)
- [Contribution acceptance criteria](#contribution-acceptance-criteria)
@@ -80,16 +81,35 @@ the remaining issues on the GitHub issue tracker.
## I want to contribute!
If you want to contribute to GitLab, but are not sure where to start,
-look for [issues with the label `up-for-grabs`][up-for-grabs]. These issues
-will be of reasonable size and challenge, for anyone to start contributing to
-GitLab.
-
-This was inspired by [an article by Kent C. Dodds][medium-up-for-grabs].
+look for [issues with the label `Accepting Merge Requests` and weight < 5][accepting-mrs-weight].
+These issues will be of reasonable size and challenge, for anyone to start
+contributing to GitLab.
## Implement design & UI elements
Please see the [UX Guide for GitLab].
+## Release retrospective and kickoff
+
+### Retrospective
+
+After each release (usually on the 22nd of each month), we have a retrospective
+call where we discuss what went well, what went wrong, and what we can improve
+for the next release. The [retrospective notes] are public and you are invited
+to comment them.
+If you're interested, you can even join the [retrospective call][retro-kickoff-call].
+
+### Kickoff
+
+Before working on the next release (usually on the 8th of each month), we have a
+kickoff call to explain what we expect to ship in the next release. The
+[kickoff notes] are public and you are invited to comment them.
+If you're interested, you can even join the [kickoff call][retro-kickoff-call].
+
+[retrospective notes]: https://docs.google.com/document/d/1nEkM_7Dj4bT21GJy0Ut3By76FZqCfLBmFQNVThmW2TY/edit?usp=sharing
+[kickoff notes]: https://docs.google.com/document/d/1ElPkZ90A8ey_iOkTvUs_ByMlwKK6NAB2VOK5835wYK0/edit?usp=sharing
+[retro-kickoff-call]: https://gitlab.zoom.us/j/918821206
+
## Issue tracker
To get support for your particular problem please use the
@@ -211,19 +231,37 @@ for a release by the appropriate person.
Make sure to mention the merge request that the `technical debt` issue is
associated with in the description of the issue.
+### Stewardship
+
+For issues related to the open source stewardship of GitLab,
+there is the ~"stewardship" label.
+
+This label is to be used for issues in which the stewardship of GitLab
+is a topic of discussion. For instance if GitLab Inc. is planning to remove
+features from GitLab CE to make exclusive in GitLab EE, related issues
+would be labelled with ~"stewardship".
+
+A recent example of this was the issue for
+[bringing the time tracking API to GitLab CE][time-tracking-issue].
+
+[time-tracking-issue]: https://gitlab.com/gitlab-org/gitlab-ce/issues/25517#note_20019084
+
## Merge requests
We welcome merge requests with fixes and improvements to GitLab code, tests,
-and/or documentation. The features we would really like a merge request for are
-listed with the label [`Accepting Merge Requests` on our issue tracker for CE][accepting-mrs-ce]
-and [EE][accepting-mrs-ee] but other improvements are also welcome. Please note
-that if an issue is marked for the current milestone either before or while you
-are working on it, a team member may take over the merge request in order to
-ensure the work is finished before the release date.
+and/or documentation. The issues that are specifically suitable for
+community contributions are listed with the label
+[`Accepting Merge Requests` on our issue tracker for CE][accepting-mrs-ce]
+and [EE][accepting-mrs-ee], but you are free to contribute to any other issue
+you want.
+
+Please note that if an issue is marked for the current milestone either before
+or while you are working on it, a team member may take over the merge request
+in order to ensure the work is finished before the release date.
If you want to add a new feature that is not labeled it is best to first create
a feedback issue (if there isn't one already) and leave a comment asking for it
-to be marked as `Accepting merge requests`. Please include screenshots or
+to be marked as `Accepting Merge Requests`. Please include screenshots or
wireframes if the feature will also change the UI.
Merge requests should be opened at [GitLab.com][gitlab-mr-tracker].
@@ -285,14 +323,6 @@ request is as follows:
1. For tests that use Capybara or PhantomJS, see this [article on how
to write reliable asynchronous tests](https://robots.thoughtbot.com/write-reliable-asynchronous-integration-tests-with-capybara).
-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
-feedback fast. Before this time the GitLab Inc. team is still dealing with work
-that is created by the monthly release such as regressions requiring patch
-releases. After the 7th it is already getting closer to the release date of the
-next version. This means there is less time to fix the issues created by
-merging large new features.
-
Please keep the change in a single MR **as small as possible**. If you want to
contribute a large feature think very hard what the minimum viable change is.
Can you split the functionality? Can you only submit the backend/API code? Can
@@ -300,6 +330,7 @@ you start with a very simple UI? Can you do part of the refactor? The increased
reviewability of small MRs that leads to higher code quality is more important
to us than having a minimal commit log. The smaller an MR is the more likely it
is it will be merged (quickly). After that you can send more MRs to enhance it.
+The ['How to get faster PR reviews' document of Kubernetes](https://github.com/kubernetes/community/blob/master/contributors/devel/faster_reviews.md) also has some great points regarding this.
For examples of feedback on merge requests please look at already
[closed merge requests][closed-merge-requests]. If you would like quick feedback
@@ -449,8 +480,7 @@ available at [http://contributor-covenant.org/version/1/1/0/](http://contributor
[core team]: https://about.gitlab.com/core-team/
[getting-help]: https://about.gitlab.com/getting-help/
[codetriage]: http://www.codetriage.com/gitlabhq/gitlabhq
-[up-for-grabs]: https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name=up-for-grabs
-[medium-up-for-grabs]: https://medium.com/@kentcdodds/first-timers-only-78281ea47455
+[accepting-mrs-weight]: https://gitlab.com/gitlab-org/gitlab-ce/issues?assignee_id=0&label_name[]=Accepting%20Merge%20Requests&sort=weight_asc
[ce-tracker]: https://gitlab.com/gitlab-org/gitlab-ce/issues
[ee-tracker]: https://gitlab.com/gitlab-org/gitlab-ee/issues
[google-group]: https://groups.google.com/forum/#!forum/gitlabhq
diff --git a/GITLAB_PAGES_VERSION b/GITLAB_PAGES_VERSION
new file mode 100644
index 00000000000..abd410582de
--- /dev/null
+++ b/GITLAB_PAGES_VERSION
@@ -0,0 +1 @@
+0.2.4
diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION
index 6085e946503..f0bb29e7638 100644
--- a/GITLAB_WORKHORSE_VERSION
+++ b/GITLAB_WORKHORSE_VERSION
@@ -1 +1 @@
-1.2.1
+1.3.0
diff --git a/Gemfile b/Gemfile
index 774dceff4f4..4aff9cd396c 100644
--- a/Gemfile
+++ b/Gemfile
@@ -7,7 +7,6 @@ gem 'rails-deprecated_sanitizer', '~> 1.0.3'
gem 'responders', '~> 2.0'
gem 'sprockets', '~> 3.7.0'
-gem 'sprockets-es6', '~> 0.9.2'
# Default values for AR models
gem 'default_value_for', '~> 3.0.0'
@@ -16,10 +15,12 @@ gem 'default_value_for', '~> 3.0.0'
gem 'mysql2', '~> 0.3.16', group: :mysql
gem 'pg', '~> 0.18.2', group: :postgres
+gem 'rugged', '~> 0.24.0'
+
# Authentication libraries
gem 'devise', '~> 4.2'
gem 'doorkeeper', '~> 4.2.0'
-gem 'omniauth', '~> 1.3.1'
+gem 'omniauth', '~> 1.3.2'
gem 'omniauth-auth0', '~> 1.4.1'
gem 'omniauth-azure-oauth2', '~> 0.0.6'
gem 'omniauth-cas3', '~> 1.1.2'
@@ -32,8 +33,9 @@ gem 'omniauth-saml', '~> 1.7.0'
gem 'omniauth-shibboleth', '~> 1.2.0'
gem 'omniauth-twitter', '~> 1.2.0'
gem 'omniauth_crowd', '~> 2.2.0'
+gem 'omniauth-authentiq', '~> 0.2.0'
gem 'rack-oauth2', '~> 1.2.1'
-gem 'jwt'
+gem 'jwt', '~> 1.5.6'
# Spam and anti-bot protection
gem 'recaptcha', '~> 3.0', require: 'recaptcha/rails'
@@ -45,13 +47,12 @@ gem 'rqrcode-rails3', '~> 0.1.7'
gem 'attr_encrypted', '~> 3.0.0'
gem 'u2f', '~> 0.2.1'
+# GitLab Pages
+gem 'validates_hostname', '~> 1.0.6'
+
# Browser detection
gem 'browser', '~> 2.2'
-# Extracting information from a git repository
-# Provide access to Gitlab::Git library
-gem 'gitlab_git', '~> 10.7.0'
-
# LDAP Auth
# GitLab fork with several improvements to original library. For full list of changes
# see https://github.com/intridea/omniauth-ldap/compare/master...gitlabhq:master
@@ -85,10 +86,14 @@ gem 'dropzonejs-rails', '~> 0.7.1'
# for backups
gem 'fog-aws', '~> 0.9'
gem 'fog-core', '~> 1.40'
+gem 'fog-google', '~> 0.5'
gem 'fog-local', '~> 0.3'
gem 'fog-openstack', '~> 0.1'
gem 'fog-rackspace', '~> 0.1.1'
+# for Google storage
+gem 'google-api-client', '~> 0.8.6'
+
# for aws storage
gem 'unf', '~> 0.1.4'
@@ -96,18 +101,19 @@ gem 'unf', '~> 0.1.4'
gem 'seed-fu', '~> 2.3.5'
# Markdown and HTML processing
-gem 'html-pipeline', '~> 1.11.0'
-gem 'deckar01-task_list', '1.0.6', require: 'task_list/railtie'
-gem 'gitlab-markup', '~> 1.5.0'
-gem 'redcarpet', '~> 3.3.3'
-gem 'RedCloth', '~> 4.3.2'
-gem 'rdoc', '~> 4.2'
-gem 'org-ruby', '~> 0.9.12'
-gem 'creole', '~> 0.5.0'
-gem 'wikicloth', '0.8.1'
-gem 'asciidoctor', '~> 1.5.2'
-gem 'rouge', '~> 2.0'
-gem 'truncato', '~> 0.7.8'
+gem 'html-pipeline', '~> 1.11.0'
+gem 'deckar01-task_list', '1.0.6', require: 'task_list/railtie'
+gem 'gitlab-markup', '~> 1.5.1'
+gem 'redcarpet', '~> 3.3.3'
+gem 'RedCloth', '~> 4.3.2'
+gem 'rdoc', '~> 4.2'
+gem 'org-ruby', '~> 0.9.12'
+gem 'creole', '~> 0.5.0'
+gem 'wikicloth', '0.8.1'
+gem 'asciidoctor', '~> 1.5.2'
+gem 'asciidoctor-plantuml', '0.0.7'
+gem 'rouge', '~> 2.0'
+gem 'truncato', '~> 0.7.8'
# See https://groups.google.com/forum/#!topic/ruby-security-ann/aSbgDiwb24s
# and https://groups.google.com/forum/#!topic/ruby-security-ann/Dy7YiKb_pMM
@@ -215,11 +221,12 @@ gem 'oj', '~> 2.17.4'
gem 'chronic', '~> 0.10.2'
gem 'chronic_duration', '~> 0.10.6'
+gem 'webpack-rails', '~> 0.9.9'
+gem 'rack-proxy', '~> 0.6.0'
+
gem 'sass-rails', '~> 5.0.6'
gem 'coffee-rails', '~> 4.1.0'
gem 'uglifier', '~> 2.7.2'
-gem 'turbolinks', '~> 2.5.0'
-gem 'jquery-turbolinks', '~> 2.1.0'
gem 'addressable', '~> 2.3.8'
gem 'bootstrap-sass', '~> 3.3.0'
@@ -249,10 +256,9 @@ end
group :development do
gem 'foreman', '~> 0.78.0'
- gem 'brakeman', '~> 3.3.0', require: false
+ gem 'brakeman', '~> 3.4.0', require: false
gem 'letter_opener_web', '~> 1.3.0'
- gem 'rerun', '~> 0.11.0'
gem 'bullet', '~> 5.2.0', require: false
gem 'rblineprof', '~> 0.3.6', platform: :mri, require: false
gem 'web-console', '~> 2.0'
@@ -278,27 +284,24 @@ group :development, :test do
gem 'rspec-retry', '~> 0.4.5'
gem 'spinach-rails', '~> 0.2.1'
gem 'spinach-rerun-reporter', '~> 0.0.2'
+ gem 'rspec_profiling'
# Prevent occasions where minitest is not bundled in packaged versions of ruby (see #3826)
gem 'minitest', '~> 5.7.0'
# Generate Fake data
- gem 'ffaker', '~> 2.0.0'
+ gem 'ffaker', '~> 2.4'
gem 'capybara', '~> 2.6.2'
gem 'capybara-screenshot', '~> 1.0.0'
gem 'poltergeist', '~> 1.9.0'
- gem 'teaspoon', '~> 1.1.0'
- gem 'teaspoon-jasmine', '~> 2.2.0'
-
gem 'spring', '~> 1.7.0'
gem 'spring-commands-rspec', '~> 1.0.4'
gem 'spring-commands-spinach', '~> 1.1.0'
- gem 'spring-commands-teaspoon', '~> 0.0.2'
- gem 'rubocop', '~> 0.43.0', require: false
- gem 'rubocop-rspec', '~> 1.5.0', require: false
+ gem 'rubocop', '~> 0.46.0', require: false
+ gem 'rubocop-rspec', '~> 1.9.1', require: false
gem 'scss_lint', '~> 0.47.0', require: false
gem 'haml_lint', '~> 0.18.2', require: false
gem 'simplecov', '0.12.0', require: false
@@ -320,18 +323,18 @@ group :test do
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 'test_after_commit', '~> 1.1'
gem 'sham_rack', '~> 1.3.6'
gem 'timecop', '~> 0.8.0'
end
gem 'newrelic_rpm', '~> 3.16'
-gem 'octokit', '~> 4.3.0'
+gem 'octokit', '~> 4.6.2'
gem 'mail_room', '~> 0.9.0'
-gem 'email_reply_parser', '~> 0.5.8'
+gem 'email_reply_trimmer', '~> 0.1'
gem 'html2text'
gem 'ruby-prof', '~> 0.16.2'
@@ -346,5 +349,5 @@ gem 'paranoia', '~> 2.2'
gem 'health_check', '~> 2.2.0'
# System information
-gem 'vmstat', '~> 2.2'
+gem 'vmstat', '~> 2.3.0'
gem 'sys-filesystem', '~> 1.1.6'
diff --git a/Gemfile.lock b/Gemfile.lock
index 8bc22479346..c5dd6ab8d22 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -54,10 +54,16 @@ GEM
faraday_middleware-multi_json (~> 0.0)
oauth2 (~> 1.0)
asciidoctor (1.5.3)
+ asciidoctor-plantuml (0.0.7)
+ asciidoctor (~> 1.5)
ast (2.3.0)
attr_encrypted (3.0.3)
encryptor (~> 3.0.0)
attr_required (1.0.0)
+ autoparse (0.3.3)
+ addressable (>= 2.3.1)
+ extlib (>= 0.9.15)
+ multi_json (>= 1.0.0)
autoprefixer-rails (6.2.3)
execjs
json
@@ -66,10 +72,6 @@ GEM
descendants_tracker (~> 0.0.4)
ice_nine (~> 0.11.0)
thread_safe (~> 0.3, >= 0.3.1)
- babel-source (5.8.35)
- babel-transpiler (0.7.0)
- babel-source (>= 4.0, < 6)
- execjs (~> 2.0)
babosa (1.0.2)
base32 (0.3.2)
bcrypt (3.1.11)
@@ -82,7 +84,7 @@ GEM
bootstrap-sass (3.3.6)
autoprefixer-rails (>= 5.2.1)
sass (>= 3.3.4)
- brakeman (3.3.2)
+ brakeman (3.4.1)
browser (2.2.0)
builder (3.2.2)
bullet (5.2.0)
@@ -167,7 +169,7 @@ GEM
railties (>= 4.2)
dropzonejs-rails (0.7.2)
rails (> 3.1)
- email_reply_parser (0.5.8)
+ email_reply_trimmer (0.1.6)
email_spec (1.6.0)
launchy (~> 2.1)
mail (~> 2.2)
@@ -179,6 +181,7 @@ GEM
excon (0.52.0)
execjs (2.6.0)
expression_parser (0.9.0)
+ extlib (0.9.16)
factory_girl (4.7.0)
activesupport (>= 3.0.0)
factory_girl_rails (4.7.0)
@@ -191,7 +194,7 @@ GEM
faraday_middleware-multi_json (0.0.6)
faraday_middleware
multi_json
- ffaker (2.0.0)
+ ffaker (2.4.0)
ffi (1.9.10)
flay (2.6.1)
ruby_parser (~> 3.0)
@@ -208,6 +211,10 @@ GEM
builder
excon (~> 0.49)
formatador (~> 0.2)
+ fog-google (0.5.0)
+ fog-core
+ fog-json
+ fog-xml
fog-json (1.0.2)
fog-core (~> 1.0)
multi_json (~> 1.10)
@@ -254,12 +261,7 @@ GEM
diff-lcs (~> 1.1)
mime-types (>= 1.16, < 3)
posix-spawn (~> 0.3)
- gitlab-markup (1.5.0)
- gitlab_git (10.7.0)
- activesupport (~> 4.0)
- charlock_holmes (~> 0.7.3)
- github-linguist (~> 4.7.0)
- rugged (~> 0.24.0)
+ gitlab-markup (1.5.1)
gitlab_omniauth-ldap (1.2.1)
net-ldap (~> 0.9)
omniauth (~> 1.0)
@@ -284,6 +286,25 @@ GEM
json
multi_json
request_store (>= 1.0)
+ google-api-client (0.8.7)
+ activesupport (>= 3.2, < 5.0)
+ addressable (~> 2.3)
+ autoparse (~> 0.3)
+ extlib (~> 0.9)
+ faraday (~> 0.9)
+ googleauth (~> 0.3)
+ launchy (~> 2.4)
+ multi_json (~> 1.10)
+ retriable (~> 1.4)
+ signet (~> 0.6)
+ googleauth (0.5.1)
+ faraday (~> 0.9)
+ jwt (~> 1.4)
+ logging (~> 2.0)
+ memoist (~> 0.12)
+ multi_json (~> 1.11)
+ os (~> 0.9)
+ signet (~> 0.7)
grape (0.18.0)
activesupport
builder
@@ -347,15 +368,12 @@ GEM
rails-dom-testing (>= 1, < 3)
railties (>= 4.2.0)
thor (>= 0.14, < 2.0)
- jquery-turbolinks (2.1.0)
- railties (>= 3.1.0)
- turbolinks
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)
+ jwt (1.5.6)
kaminari (0.17.0)
actionpack (>= 3.0.0)
activesupport (>= 3.0.0)
@@ -383,14 +401,16 @@ GEM
xml-simple
licensee (8.0.0)
rugged (>= 0.24b)
- listen (3.0.5)
- rb-fsevent (>= 0.9.3)
- rb-inotify (>= 0.9)
+ little-plugger (1.1.4)
+ logging (2.1.0)
+ little-plugger (~> 1.1)
+ multi_json (~> 1.10)
loofah (2.0.3)
nokogiri (>= 1.5.9)
mail (2.6.4)
mime-types (>= 1.16, < 4)
mail_room (0.9.0)
+ memoist (0.15.0)
method_source (0.8.2)
mime-types (2.99.3)
mimemagic (0.3.0)
@@ -420,14 +440,16 @@ GEM
multi_json (~> 1.3)
multi_xml (~> 0.5)
rack (>= 1.2, < 3)
- octokit (4.3.0)
- sawyer (~> 0.7.0, >= 0.5.3)
+ octokit (4.6.2)
+ sawyer (~> 0.8.0, >= 0.5.3)
oj (2.17.4)
- omniauth (1.3.1)
+ omniauth (1.3.2)
hashie (>= 1.2, < 4)
rack (>= 1.0, < 3)
omniauth-auth0 (1.4.1)
omniauth-oauth2 (~> 1.1)
+ omniauth-authentiq (0.2.2)
+ omniauth-oauth2 (~> 1.3, >= 1.3.1)
omniauth-azure-oauth2 (0.0.6)
jwt (~> 1.0)
omniauth (~> 1.0)
@@ -476,6 +498,7 @@ GEM
org-ruby (0.9.12)
rubypants (~> 0.2)
orm_adapter (0.5.0)
+ os (0.9.6)
paranoia (2.2.0)
activerecord (>= 4.0, < 5.1)
parser (2.3.1.4)
@@ -519,6 +542,8 @@ GEM
rack (>= 1.1)
rack-protection (1.5.3)
rack
+ rack-proxy (0.6.0)
+ rack
rack-test (0.6.3)
rack (>= 1.0)
rails (4.2.7.1)
@@ -548,9 +573,6 @@ GEM
rainbow (2.1.0)
raindrops (0.17.0)
rake (10.5.0)
- rb-fsevent (0.9.6)
- rb-inotify (0.9.5)
- ffi (>= 0.5.0)
rblineprof (0.3.6)
debugger-ruby_core_source (~> 1.3)
rdoc (4.2.2)
@@ -579,14 +601,13 @@ GEM
redis-store (1.2.0)
redis (>= 2.2)
request_store (1.3.1)
- rerun (0.11.0)
- listen (~> 3.0)
responders (2.3.0)
railties (>= 4.2.0, < 5.1)
rest-client (2.0.0)
http-cookie (>= 1.0.2, < 2.0)
mime-types (>= 1.16, < 4.0)
netrc (~> 0.8)
+ retriable (1.4.1)
rinku (2.0.0)
rotp (2.1.2)
rouge (2.0.7)
@@ -617,14 +638,19 @@ GEM
rspec-retry (0.4.5)
rspec-core
rspec-support (3.5.0)
- rubocop (0.43.0)
+ rspec_profiling (0.0.4)
+ activerecord
+ pg
+ rails
+ sqlite3
+ rubocop (0.46.0)
parser (>= 2.3.1.1, < 3.0)
powerpack (~> 0.1)
rainbow (>= 1.99.1, < 3.0)
ruby-progressbar (~> 1.7)
unicode-display_width (~> 1.0, >= 1.0.1)
- rubocop-rspec (1.5.0)
- rubocop (>= 0.40.0)
+ rubocop-rspec (1.9.1)
+ rubocop (>= 0.42.0)
ruby-fogbugz (0.2.1)
crack (~> 0.4)
ruby-prof (0.16.2)
@@ -648,9 +674,9 @@ GEM
sprockets (>= 2.8, < 4.0)
sprockets-rails (>= 2.0, < 4.0)
tilt (>= 1.1, < 3)
- sawyer (0.7.0)
- addressable (>= 2.3.5, < 2.5)
- faraday (~> 0.8, < 0.10)
+ sawyer (0.8.1)
+ addressable (>= 2.3.5, < 2.6)
+ faraday (~> 0.8, < 1.0)
scss_lint (0.47.1)
rake (>= 0.9, < 11)
sass (~> 3.4.15)
@@ -678,6 +704,11 @@ GEM
sidekiq (>= 4.2.1)
sidekiq-limit_fetch (3.4.0)
sidekiq (>= 4)
+ signet (0.7.3)
+ addressable (~> 2.3)
+ faraday (~> 0.9)
+ jwt (~> 1.5)
+ multi_json (~> 1.10)
simplecov (0.12.0)
docile (~> 1.1.0)
json (>= 1.8, < 3)
@@ -700,19 +731,14 @@ GEM
spring (>= 0.9.1)
spring-commands-spinach (1.1.0)
spring (>= 0.9.1)
- spring-commands-teaspoon (0.0.2)
- spring (>= 0.9.1)
sprockets (3.7.0)
concurrent-ruby (~> 1.0)
rack (> 1, < 3)
- sprockets-es6 (0.9.2)
- babel-source (>= 5.8.11)
- babel-transpiler
- sprockets (>= 3.0.0)
sprockets-rails (3.1.1)
actionpack (>= 4.0)
activesupport (>= 4.0)
sprockets (>= 3.0.0)
+ sqlite3 (1.3.11)
stackprof (0.2.10)
state_machines (0.4.0)
state_machines-activemodel (0.4.0)
@@ -725,12 +751,8 @@ GEM
sys-filesystem (1.1.6)
ffi
sysexits (1.2.0)
- teaspoon (1.1.5)
- railties (>= 3.2.5, < 6)
- teaspoon-jasmine (2.2.0)
- teaspoon (>= 1.0.0)
temple (0.7.7)
- test_after_commit (0.4.2)
+ test_after_commit (1.1.0)
activerecord (>= 3.2)
thin (1.7.0)
daemons (~> 1.0, >= 1.0.9)
@@ -745,8 +767,6 @@ GEM
truncato (0.7.8)
htmlentities (~> 4.3.1)
nokogiri (~> 1.6.1)
- turbolinks (2.5.3)
- coffee-rails
tzinfo (1.2.2)
thread_safe (~> 0.1)
u2f (0.2.1)
@@ -765,13 +785,16 @@ GEM
get_process_mem (~> 0)
unicorn (>= 4, < 6)
uniform_notifier (1.10.0)
+ validates_hostname (1.0.6)
+ activerecord (>= 3.0)
+ activesupport (>= 3.0)
version_sorter (2.1.0)
virtus (1.0.5)
axiom-types (~> 0.1)
coercible (~> 1.0)
descendants_tracker (~> 0.0, >= 0.0.3)
equalizer (~> 0.0, >= 0.0.9)
- vmstat (2.2.0)
+ vmstat (2.3.0)
warden (1.2.6)
rack (>= 1.0)
web-console (2.3.0)
@@ -782,6 +805,8 @@ GEM
webmock (1.21.0)
addressable (>= 2.3.6)
crack (>= 0.3.2)
+ webpack-rails (0.9.9)
+ rails (>= 3.2.0)
websocket-driver (0.6.3)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.2)
@@ -807,6 +832,7 @@ DEPENDENCIES
allocations (~> 1.0)
asana (~> 0.4.0)
asciidoctor (~> 1.5.2)
+ asciidoctor-plantuml (= 0.0.7)
attr_encrypted (~> 3.0.0)
awesome_print (~> 1.2.0)
babosa (~> 1.0.2)
@@ -815,7 +841,7 @@ DEPENDENCIES
better_errors (~> 1.0.1)
binding_of_caller (~> 0.7.2)
bootstrap-sass (~> 3.3.0)
- brakeman (~> 3.3.0)
+ brakeman (~> 3.4.0)
browser (~> 2.2)
bullet (~> 5.2.0)
bundler-audit (~> 0.5.0)
@@ -837,13 +863,14 @@ DEPENDENCIES
diffy (~> 3.1.0)
doorkeeper (~> 4.2.0)
dropzonejs-rails (~> 0.7.1)
- email_reply_parser (~> 0.5.8)
+ email_reply_trimmer (~> 0.1)
email_spec (~> 1.6.0)
factory_girl_rails (~> 4.7.0)
- ffaker (~> 2.0.0)
+ ffaker (~> 2.4)
flay (~> 2.6.1)
fog-aws (~> 0.9)
fog-core (~> 1.40)
+ fog-google (~> 0.5)
fog-local (~> 0.3)
fog-openstack (~> 0.1)
fog-rackspace (~> 0.1.1)
@@ -854,12 +881,12 @@ DEPENDENCIES
gemojione (~> 3.0)
github-linguist (~> 4.7.0)
gitlab-flowdock-git-hook (~> 1.0.1)
- gitlab-markup (~> 1.5.0)
- gitlab_git (~> 10.7.0)
+ gitlab-markup (~> 1.5.1)
gitlab_omniauth-ldap (~> 1.2.1)
gollum-lib (~> 4.2)
gollum-rugged_adapter (~> 0.4.2)
gon (~> 6.1.0)
+ google-api-client (~> 0.8.6)
grape (~> 0.18.0)
grape-entity (~> 0.6.0)
haml_lint (~> 0.18.2)
@@ -873,10 +900,9 @@ DEPENDENCIES
jira-ruby (~> 1.1.2)
jquery-atwho-rails (~> 1.3.2)
jquery-rails (~> 4.1.0)
- jquery-turbolinks (~> 2.1.0)
jquery-ui-rails (~> 5.0.0)
json-schema (~> 2.6.2)
- jwt
+ jwt (~> 1.5.6)
kaminari (~> 0.17.0)
knapsack (~> 1.11.0)
kubeclient (~> 2.2.0)
@@ -893,10 +919,11 @@ DEPENDENCIES
newrelic_rpm (~> 3.16)
nokogiri (~> 1.6.7, >= 1.6.7.2)
oauth2 (~> 1.2.0)
- octokit (~> 4.3.0)
+ octokit (~> 4.6.2)
oj (~> 2.17.4)
- omniauth (~> 1.3.1)
+ omniauth (~> 1.3.2)
omniauth-auth0 (~> 1.4.1)
+ omniauth-authentiq (~> 0.2.0)
omniauth-azure-oauth2 (~> 0.0.6)
omniauth-cas3 (~> 1.1.2)
omniauth-facebook (~> 4.0.0)
@@ -918,6 +945,7 @@ DEPENDENCIES
rack-attack (~> 4.4.1)
rack-cors (~> 0.4.0)
rack-oauth2 (~> 1.2.1)
+ rack-proxy (~> 0.6.0)
rails (= 4.2.7.1)
rails-deprecated_sanitizer (~> 1.0.3)
rainbow (~> 2.1.0)
@@ -929,16 +957,17 @@ DEPENDENCIES
redis-namespace (~> 1.5.2)
redis-rails (~> 5.0.1)
request_store (~> 1.3)
- rerun (~> 0.11.0)
responders (~> 2.0)
rouge (~> 2.0)
rqrcode-rails3 (~> 0.1.7)
rspec-rails (~> 3.5.0)
rspec-retry (~> 0.4.5)
- rubocop (~> 0.43.0)
- rubocop-rspec (~> 1.5.0)
+ rspec_profiling
+ rubocop (~> 0.46.0)
+ rubocop-rspec (~> 1.9.1)
ruby-fogbugz (~> 0.2.1)
ruby-prof (~> 0.16.2)
+ rugged (~> 0.24.0)
sanitize (~> 2.0)
sass-rails (~> 5.0.6)
scss_lint (~> 0.47.0)
@@ -958,31 +987,28 @@ DEPENDENCIES
spring (~> 1.7.0)
spring-commands-rspec (~> 1.0.4)
spring-commands-spinach (~> 1.1.0)
- spring-commands-teaspoon (~> 0.0.2)
sprockets (~> 3.7.0)
- sprockets-es6 (~> 0.9.2)
stackprof (~> 0.2.10)
state_machines-activerecord (~> 0.4.0)
sys-filesystem (~> 1.1.6)
- teaspoon (~> 1.1.0)
- teaspoon-jasmine (~> 2.2.0)
- test_after_commit (~> 0.4.2)
+ test_after_commit (~> 1.1)
thin (~> 1.7.0)
timecop (~> 0.8.0)
truncato (~> 0.7.8)
- turbolinks (~> 2.5.0)
u2f (~> 0.2.1)
uglifier (~> 2.7.2)
underscore-rails (~> 1.8.0)
unf (~> 0.1.4)
unicorn (~> 5.1.0)
unicorn-worker-killer (~> 0.4.4)
+ validates_hostname (~> 1.0.6)
version_sorter (~> 2.1.0)
virtus (~> 1.0.1)
- vmstat (~> 2.2)
+ vmstat (~> 2.3.0)
web-console (~> 2.0)
webmock (~> 1.21.0)
+ webpack-rails (~> 0.9.9)
wikicloth (= 0.8.1)
BUNDLED WITH
- 1.13.6
+ 1.14.2
diff --git a/PROCESS.md b/PROCESS.md
index 8af660fbdd1..6eabaf05d24 100644
--- a/PROCESS.md
+++ b/PROCESS.md
@@ -12,105 +12,54 @@ etc.).
## Common actions
-### Issue team
-
-- Looks for issues without [workflow labels](#how-we-handle-issues) and triages
- issue
-- Closes invalid issues with a comment (duplicates,
- [fixed in newer version](#issue-fixed-in-newer-version),
- [issue report for old version](#issue-report-for-old-version), not a problem
- in GitLab, etc.)
-- Asks for feedback from issue reporter
- ([invalid issue reports](#improperly-formatted-issue),
- [format code](#code-format), etc.)
-- Monitors all issues for feedback (but especially ones commented on since
- automatically watching them)
-- Closes issues with no feedback from the reporter for two weeks
-
-### Merge marshall & merge request coach
-
-- Responds to merge requests the issue team mentions them in and monitors for
- new merge requests
-- Provides feedback to the merge request submitter to improve the merge request
- (style, tests, etc.)
-- Mark merge requests `Ready for Merge` when they meet the
- [contribution acceptance criteria]
-- Mention developer(s) based on the
- [list of members and their specialities][team]
-- Closes merge requests with no feedback from the reporter for two weeks
-
-## Priorities of the issue team
-
-1. Mentioning people (critical)
-1. Workflow labels (normal)
-1. Functional labels (minor)
-1. Assigning issues (avoid if possible)
-
-## Mentioning people
+### Issue triaging
+
+Our issue triage policies are [described in our handbook]. You are very welcome
+to help the GitLab team triage issues. We also organize [issue bash events] once
+every quarter.
The most important thing is making sure valid issues receive feedback from the
development team. Therefore the priority is mentioning developers that can help
on those issues. Please select someone with relevant experience from
-[GitLab core team][core-team]. If there is nobody mentioned with that expertise
+[GitLab team][team]. If there is nobody mentioned with that expertise
look in the commit history for the affected files to find someone. Avoid
mentioning the lead developer, this is the person that is least likely to give a
timely response. If the involvement of the lead developer is needed the other
core team members will mention this person.
-## Workflow labels
+[described in our handbook]: https://about.gitlab.com/handbook/engineering/issues/issue-triage-policies/
+[issue bash events]: https://gitlab.com/gitlab-org/gitlab-ce/issues/17815
-Workflow labels are purposely not very detailed since that would be hard to keep
-updated as you would need to re-evaluate them after every comment. We optionally
-use functional labels on demand when we want to group related issues to get an
-overview (for example all issues related to RVM, to tackle them in one go) and
-to add details to the issue.
+### Merge request coaching
-- ~"Awaiting Feedback" Feedback pending from the reporter
-- ~UX needs help from a UX designer
-- ~Frontend needs help from a Front-end engineer. Please follow the
- ["Implement design & UI elements" guidelines].
-- ~up-for-grabs is an issue suitable for first-time contributors, of reasonable difficulty and size. Not exclusive with other labels.
-- ~"feature proposal" is a proposal for a new feature for GitLab. People are encouraged to vote
-in support or comment for further detail. Do not use `feature request`.
-- ~bug is an issue reporting undesirable or incorrect behavior.
-- ~customer is an issue reported by enterprise subscribers. This label should
-be accompanied by *bug* or *feature proposal* labels.
+Several people from the [GitLab team][team] are helping community members to get
+their contributions accepted by meeting our [Definition of done](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#definition-of-done).
-Example workflow: when a UX designer provided a design but it needs frontend work they remove the UX label and add the frontend label.
+What you can expect from them is described at https://about.gitlab.com/jobs/merge-request-coach/.
+
+## Workflow labels
-## Functional labels
+Labelling issues is described in the [GitLab Inc engineering workflow].
-These labels describe what development specialities are involved such as: `CI`,
-`Core`, `Documentation`, `Frontend`, `Issues`, `Merge Requests`, `Omnibus`,
-`Release`, `Repository`, `UX`.
+[GitLab Inc engineering workflow]: https://about.gitlab.com/handbook/engineering/workflow/#labelling-issues
## Assigning issues
If an issue is complex and needs the attention of a specific person, assignment is a good option but assigning issues might discourage other people from contributing to that issue. We need all the contributions we can get so this should never be discouraged. Also, an assigned person might not have time for a few weeks, so others should feel free to takeover.
-## Label colors
-
-- Light orange `#fef2c0`: workflow labels for issue team members (awaiting
- feedback, awaiting confirmation of fix)
-- Bright orange `#eb6420`: workflow labels for core team members (attached MR,
- awaiting developer action/feedback)
-- Light blue `#82C5FF`: functional labels
-- Green labels `#009800`: issues that can generally be ignored. For example,
- issues given the following labels normally can be closed immediately:
- - Support (see copy & paste response:
- [Support requests and configuration questions](#support-requests-and-configuration-questions)
-
## Be kind
Be kind to people trying to contribute. Be aware that people may be a non-native
English speaker, they might not understand things or they might be very
sensitive as to how you word things. Use Emoji to express your feelings (heart,
-star, smile, etc.). Some good tips about giving feedback to merge requests is in
-the [Thoughtbot code review guide].
+star, smile, etc.). Some good tips about code reviews can be found in our
+[Code Review Guidelines].
+
+[Code Review Guidelines]: https://docs.gitlab.com/ce/development/code_review.html
## Feature Freeze
-5 working days before the 22nd the stable branches for the upcoming release will
+On the 7th of each month, the stable branches for the upcoming release will
be frozen for major changes. Merge requests may still be merged into master
during this period. By freezing the stable branches prior to a release there's
no need to worry about last minute merge requests potentially breaking a lot of
@@ -119,10 +68,9 @@ things.
What is considered to be a major change is determined on a case by case basis as
this definition depends very much on the context of changes. For example, a 5
line change might have a big impact on the entire application. Ultimately the
-decision will be made by those reviewing a merge request and the release
-manager.
+decision will be made by the maintainers and the release managers.
-During the feature freeze all merge requests that are meant to go into the next
+During the feature freeze all merge requests that are meant to go into the upcoming
release should have the correct milestone assigned _and_ have the label
~"Pick into Stable" set. Merge requests without a milestone and this label will
not be merged into any stable branches.
@@ -131,47 +79,47 @@ not be merged into any stable branches.
### Improperly formatted issue
-Thanks for the issue report. Please reformat your issue to conform to the \[contributing guidelines\]\(https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#issue-tracker-guidelines).
+Thanks for the issue report. Please reformat your issue to conform to the [contributing guidelines](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#issue-tracker-guidelines).
### Issue report for old version
-Thanks for the issue report but we only support issues for the latest stable version of GitLab. I'm closing this issue but if you still experience this problem in the latest stable version, please open a new issue (but also reference the old issue(s)). Make sure to also include the necessary debugging information conforming to the issue tracker guidelines found in our \[contributing guidelines\]\(https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#issue-tracker-guidelines).
+Thanks for the issue report but we only support issues for the latest stable version of GitLab. I'm closing this issue but if you still experience this problem in the latest stable version, please open a new issue (but also reference the old issue(s)). Make sure to also include the necessary debugging information conforming to the issue tracker guidelines found in our [contributing guidelines](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#issue-tracker-guidelines).
### Support requests and configuration questions
Thanks for your interest in GitLab. We don't use the issue tracker for support
requests and configuration questions. Please check our
-\[getting help\]\(https://about.gitlab.com/getting-help/) page to see all of the available
-support options. Also, have a look at the \[contribution guidelines\]\(https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md)
+[getting help](https://about.gitlab.com/getting-help/) page to see all of the available
+support options. Also, have a look at the [contribution guidelines](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md)
for more information.
### Code format
-Please use ``` to format console output, logs, and code as it's very hard to read otherwise.
+Please use \`\`\` to format console output, logs, and code as it's very hard to read otherwise.
### Issue fixed in newer version
-Thanks for the issue report. This issue has already been fixed in newer versions of GitLab. Due to the size of this project and our limited resources we are only able to support the latest stable release as outlined in our \[contributing guidelines\]\(https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#issue-tracker). In order to get this bug fix and enjoy many new features please \[upgrade\]\(https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update). If you still experience issues at that time please open a new issue following our issue tracker guidelines found in the \[contributing guidelines\]\(https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#issue-tracker-guidelines).
+Thanks for the issue report. This issue has already been fixed in newer versions of GitLab. Due to the size of this project and our limited resources we are only able to support the latest stable release as outlined in our [contributing guidelines](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#issue-tracker). In order to get this bug fix and enjoy many new features please [upgrade](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update). If you still experience issues at that time please open a new issue following our issue tracker guidelines found in the [contributing guidelines](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#issue-tracker-guidelines).
### Improperly formatted merge request
-Thanks for your interest in improving the GitLab codebase! Please update your merge request according to the \[contributing guidelines\]\(https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#pull-request-guidelines).
+Thanks for your interest in improving the GitLab codebase! Please update your merge request according to the [contributing guidelines](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#pull-request-guidelines).
### Inactivity close of an issue
-It's been at least 2 weeks (and a new release) since we heard from you. I'm closing this issue but if you still experience this problem, please open a new issue (but also reference the old issue(s)). Make sure to also include the necessary debugging information conforming to the issue tracker guidelines found in our \[contributing guidelines\]\(https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#issue-tracker-guidelines).
+It's been at least 2 weeks (and a new release) since we heard from you. I'm closing this issue but if you still experience this problem, please open a new issue (but also reference the old issue(s)). Make sure to also include the necessary debugging information conforming to the issue tracker guidelines found in our [contributing guidelines](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#issue-tracker-guidelines).
### Inactivity close of a merge request
-This merge request has been closed because a request for more information has not been reacted to for more than 2 weeks. If you respond and conform to the merge request guidelines in our \[contributing guidelines\]\(https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#pull-requests) we will reopen this merge request.
+This merge request has been closed because a request for more information has not been reacted to for more than 2 weeks. If you respond and conform to the merge request guidelines in our [contributing guidelines](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#pull-requests) we will reopen this merge request.
### Accepting merge requests
Is there an issue on the
-\[issue tracker\]\(https://gitlab.com/gitlab-org/gitlab-ce/issues) that is
+[issue tracker](https://gitlab.com/gitlab-org/gitlab-ce/issues) that is
similar to this? Could you please link it here?
Please be aware that new functionality that is not marked
-\[accepting merge requests\]\(https://gitlab.com/gitlab-org/gitlab-ce/issues?milestone_id=&scope=all&sort=created_desc&state=opened&utf8=%E2%9C%93&assignee_id=&author_id=&milestone_title=&label_name=Accepting+Merge+Requests)
+[accepting merge requests](https://gitlab.com/gitlab-org/gitlab-ce/issues?milestone_id=&scope=all&sort=created_desc&state=opened&utf8=%E2%9C%93&assignee_id=&author_id=&milestone_title=&label_name=Accepting+Merge+Requests)
might not make it into GitLab.
### Only accepting merge requests with green tests
@@ -186,9 +134,8 @@ rebase with master to see if that solves the issue.
We are currently in the process of closing down the issue tracker on GitHub, to
prevent duplication with the GitLab.com issue tracker.
Since this is an older issue I'll be closing this for now. If you think this is
-still an issue I encourage you to open it on the \[GitLab.com issue tracker\]\(https://gitlab.com/gitlab-org/gitlab-ce/issues).
+still an issue I encourage you to open it on the [GitLab.com issue tracker](https://gitlab.com/gitlab-org/gitlab-ce/issues).
-[core-team]: https://about.gitlab.com/core-team/
[team]: https://about.gitlab.com/team/
[contribution acceptance criteria]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#contribution-acceptance-criteria
["Implement design & UI elements" guidelines]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#implement-design-ui-elements
diff --git a/README.md b/README.md
index 68b709b85d5..4f85fac4a56 100644
--- a/README.md
+++ b/README.md
@@ -15,10 +15,10 @@ To see how GitLab looks please see the [features page on our website](https://ab
- Manage Git repositories with fine grained access controls that keep your code secure
- Perform code reviews and enhance collaboration with merge requests
-- Each project can also have an issue tracker and a wiki
+- Complete continuous integration (CI) and CD pipelines to builds, test, and deploy your applications
+- Each project can also have an issue tracker, issue board, and a wiki
- Used by more than 100,000 organizations, GitLab is the most popular solution to manage Git repositories on-premises
- Completely free and open source (MIT Expat license)
-- Powered by [Ruby on Rails](https://github.com/rails/rails)
## Hiring
@@ -74,11 +74,11 @@ 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
+- Ubuntu/Debian/CentOS/RHEL/OpenSUSE
- Ruby (MRI) 2.3
- Git 2.8.4+
- Redis 2.8+
-- MySQL or PostgreSQL
+- PostgreSQL (preferred) or MySQL
For more information please see the [architecture documentation](https://docs.gitlab.com/ce/development/architecture.html).
@@ -113,4 +113,4 @@ Please see [Getting help for GitLab](https://about.gitlab.com/getting-help/) on
## Is it awesome?
Thanks for [asking this question](https://twitter.com/supersloth/status/489462789384056832) Joshua.
-[These people](https://twitter.com/gitlab/favorites) seem to like it.
+[These people](https://twitter.com/gitlab/likes) seem to like it.
diff --git a/VERSION b/VERSION
index d59bc5cbc5c..5c99c061a47 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-8.15.0-pre
+8.17.0-pre
diff --git a/app/assets/images/auth_buttons/authentiq_64.png b/app/assets/images/auth_buttons/authentiq_64.png
new file mode 100644
index 00000000000..81767bbcc54
--- /dev/null
+++ b/app/assets/images/auth_buttons/authentiq_64.png
Binary files differ
diff --git a/app/assets/javascripts/admin.js b/app/assets/javascripts/admin.js
index 5a7d823e84c..424dc719c78 100644
--- a/app/assets/javascripts/admin.js
+++ b/app/assets/javascripts/admin.js
@@ -1,5 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, no-unused-vars, no-else-return, prefer-arrow-callback, camelcase, quotes, comma-dangle, padded-blocks, max-len */
-/* global Turbolinks */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, no-unused-vars, no-else-return, prefer-arrow-callback, camelcase, quotes, comma-dangle, max-len */
(function() {
this.Admin = (function() {
@@ -42,10 +41,10 @@
return $('.change-owner-link').show();
});
$('li.project_member').bind('ajax:success', function() {
- return Turbolinks.visit(location.href);
+ return gl.utils.refreshCurrentPage();
});
$('li.group_member').bind('ajax:success', function() {
- return Turbolinks.visit(location.href);
+ return gl.utils.refreshCurrentPage();
});
showBlacklistType = function() {
if ($("input[name='blacklist_type']:checked").val() === 'file') {
@@ -61,7 +60,5 @@
}
return Admin;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index f60f27d1210..b4a8c827d7f 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, quotes, object-shorthand, camelcase, no-var, comma-dangle, prefer-arrow-callback, indent, object-curly-spacing, quote-props, no-param-reassign, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, quotes, object-shorthand, camelcase, no-var, comma-dangle, prefer-arrow-callback, quote-props, no-param-reassign, max-len */
(function() {
var Api = {
@@ -29,9 +29,9 @@
return $.ajax({
url: url,
data: $.extend({
- search: query,
- per_page: 20
- }, options),
+ search: query,
+ per_page: 20
+ }, options),
dataType: "json"
}).done(function(groups) {
return callback(groups);
@@ -73,7 +73,7 @@
return $.ajax({
url: url,
type: "POST",
- data: {'label': data},
+ data: { 'label': data },
dataType: "json"
}).done(function(label) {
return callback(label);
diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js
index 043c6a11c4f..ea3f13bd00f 100644
--- a/app/assets/javascripts/application.js
+++ b/app/assets/javascripts/application.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, quotes, consistent-return, prefer-arrow-callback, comma-dangle, object-shorthand, no-new, max-len */
+/* eslint-disable func-names, space-before-function-paren, no-var, quotes, consistent-return, prefer-arrow-callback, comma-dangle, object-shorthand, no-new, max-len, no-multi-spaces, import/newline-after-import */
/* global bp */
/* global Cookies */
/* global Flash */
@@ -6,64 +6,60 @@
/* global AwardsHandler */
/* global Aside */
-// This is a manifest file that'll be compiled into including all the files listed below.
-// Add new JavaScript code in separate files in this directory and they'll automatically
-// be included in the compiled file accessible from http://example.com/assets/application.js
-// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
-// the compiled file.
-//
-/*= require jquery2 */
-/*= require jquery-ui/autocomplete */
-/*= require jquery-ui/datepicker */
-/*= require jquery-ui/draggable */
-/*= require jquery-ui/effect-highlight */
-/*= require jquery-ui/sortable */
-/*= require jquery_ujs */
-/*= require jquery.endless-scroll */
-/*= require jquery.highlight */
-/*= require jquery.waitforimages */
-/*= require jquery.atwho */
-/*= require jquery.scrollTo */
-/*= require jquery.turbolinks */
-/*= require js.cookie */
-/*= require turbolinks */
-/*= require autosave */
-/*= require bootstrap/affix */
-/*= require bootstrap/alert */
-/*= require bootstrap/button */
-/*= require bootstrap/collapse */
-/*= require bootstrap/dropdown */
-/*= require bootstrap/modal */
-/*= require bootstrap/scrollspy */
-/*= require bootstrap/tab */
-/*= require bootstrap/transition */
-/*= require bootstrap/tooltip */
-/*= require bootstrap/popover */
-/*= require select2 */
-/*= require underscore */
-/*= require dropzone */
-/*= require mousetrap */
-/*= require mousetrap/pause */
-/*= require shortcuts */
-/*= require shortcuts_navigation */
-/*= require shortcuts_dashboard_navigation */
-/*= require shortcuts_issuable */
-/*= require shortcuts_network */
-/*= require jquery.nicescroll */
-/*= require date.format */
-/*= require_directory ./behaviors */
-/*= require_directory ./blob */
-/*= require_directory ./templates */
-/*= require_directory ./commit */
-/*= require_directory ./extensions */
-/*= require_directory ./lib/utils */
-/*= require_directory ./u2f */
-/*= require_directory . */
-/*= require fuzzaldrin-plus */
-/*= require es6-promise.auto */
+function requireAll(context) { return context.keys().map(context); }
+
+window.$ = window.jQuery = require('jquery');
+require('jquery-ui/ui/autocomplete');
+require('jquery-ui/ui/datepicker');
+require('jquery-ui/ui/draggable');
+require('jquery-ui/ui/effect-highlight');
+require('jquery-ui/ui/sortable');
+require('jquery-ujs');
+require('vendor/jquery.endless-scroll');
+require('vendor/jquery.highlight');
+require('vendor/jquery.waitforimages');
+require('vendor/jquery.caret');
+require('vendor/jquery.atwho');
+require('vendor/jquery.scrollTo');
+window.Cookies = require('vendor/js.cookie');
+require('./autosave');
+require('bootstrap/js/affix');
+require('bootstrap/js/alert');
+require('bootstrap/js/button');
+require('bootstrap/js/collapse');
+require('bootstrap/js/dropdown');
+require('bootstrap/js/modal');
+require('bootstrap/js/scrollspy');
+require('bootstrap/js/tab');
+require('bootstrap/js/transition');
+require('bootstrap/js/tooltip');
+require('bootstrap/js/popover');
+require('select2/select2.js');
+window._ = require('underscore');
+window.Dropzone = require('dropzone');
+require('mousetrap');
+require('mousetrap/plugins/pause/mousetrap-pause');
+require('./shortcuts');
+require('./shortcuts_navigation');
+require('./shortcuts_dashboard_navigation');
+require('./shortcuts_issuable');
+require('./shortcuts_network');
+require('vendor/jquery.nicescroll');
+requireAll(require.context('./behaviors', false, /^\.\/.*\.(js|es6)$/));
+requireAll(require.context('./blob', false, /^\.\/.*\.(js|es6)$/));
+requireAll(require.context('./templates', false, /^\.\/.*\.(js|es6)$/));
+requireAll(require.context('./commit', false, /^\.\/.*\.(js|es6)$/));
+requireAll(require.context('./extensions', false, /^\.\/.*\.(js|es6)$/));
+requireAll(require.context('./lib/utils', false, /^\.\/.*\.(js|es6)$/));
+requireAll(require.context('./u2f', false, /^\.\/.*\.(js|es6)$/));
+requireAll(require.context('./droplab', false, /^\.\/.*\.(js|es6)$/));
+requireAll(require.context('.', false, /^\.\/(?!application\.js).*\.(js|es6)$/));
+require('vendor/fuzzaldrin-plus');
+window.ES6Promise = require('vendor/es6-promise.auto');
+window.ES6Promise.polyfill();
(function () {
- document.addEventListener('page:fetch', function () {
+ document.addEventListener('beforeunload', function () {
// Unbind scroll events
$(document).off('scroll');
// Close any open tooltips
@@ -83,12 +79,19 @@
var $sidebarGutterToggle = $('.js-sidebar-toggle');
var $flash = $('.flash-container');
var bootstrapBreakpoint = bp.getBreakpointSize();
- var checkInitialSidebarSize;
var fitSidebarForSize;
// Set the default path for all cookies to GitLab's root directory
Cookies.defaults.path = gon.relative_url_root || '/';
+ // `hashchange` is not triggered when link target is already in window.location
+ $body.on('click', 'a[href^="#"]', function() {
+ var href = this.getAttribute('href');
+ if (href.substr(1) === gl.utils.getLocationHash()) {
+ setTimeout(gl.utils.handleLocationHash, 1);
+ }
+ });
+
// prevent default action for disabled buttons
$('.btn').click(function(e) {
if ($(this).hasClass('disabled')) {
@@ -237,20 +240,14 @@
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 () {
return fitSidebarForSize();
});
gl.awardsHandler = new AwardsHandler();
- checkInitialSidebarSize();
new Aside();
-
// bind sidebar events
new gl.Sidebar();
+
+ gl.utils.initTimeagoTimeout();
});
}).call(this);
diff --git a/app/assets/javascripts/aside.js b/app/assets/javascripts/aside.js
index 9417afc2ea7..8438de6cdf1 100644
--- a/app/assets/javascripts/aside.js
+++ b/app/assets/javascripts/aside.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, prefer-arrow-callback, no-var, one-var, one-var-declaration-per-line, no-else-return, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, prefer-arrow-callback, no-var, one-var, one-var-declaration-per-line, no-else-return, max-len */
(function() {
this.Aside = (function() {
function Aside() {
@@ -21,7 +21,5 @@
}
return Aside;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/autosave.js b/app/assets/javascripts/autosave.js
index f45dbe4cbf2..b16a2c0f73a 100644
--- a/app/assets/javascripts/autosave.js
+++ b/app/assets/javascripts/autosave.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-param-reassign, quotes, prefer-template, no-var, one-var, no-unused-vars, one-var-declaration-per-line, no-void, consistent-return, no-empty, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-param-reassign, quotes, prefer-template, no-var, one-var, no-unused-vars, one-var-declaration-per-line, no-void, consistent-return, no-empty, max-len */
(function() {
this.Autosave = (function() {
function Autosave(field, key) {
@@ -58,7 +58,5 @@
};
return Autosave;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js
index 107a7978a87..9d776b74965 100644
--- a/app/assets/javascripts/awards_handler.js
+++ b/app/assets/javascripts/awards_handler.js
@@ -1,11 +1,13 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, max-len, no-var, spaced-comment, prefer-arrow-callback, consistent-return, one-var, one-var-declaration-per-line, no-unused-vars, no-else-return, prefer-template, quotes, comma-dangle, no-param-reassign, no-void, radix, keyword-spacing, space-before-blocks, brace-style, no-underscore-dangle, no-plusplus, no-return-assign, camelcase, padded-blocks */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, max-len, no-var, prefer-arrow-callback, consistent-return, one-var, one-var-declaration-per-line, no-unused-vars, no-else-return, prefer-template, quotes, comma-dangle, no-param-reassign, no-void, brace-style, no-underscore-dangle, no-return-assign, camelcase */
/* global Cookies */
+var emojiAliases = require('emoji-aliases');
+
(function() {
this.AwardsHandler = (function() {
- var FROM_SENTENCE_REGEX = /(?:, and | and |, )/; //For separating lists produced by ruby's Array#toSentence
+ var FROM_SENTENCE_REGEX = /(?:, and | and |, )/; // For separating lists produced by ruby's Array#toSentence
function AwardsHandler() {
- this.aliases = gl.emojiAliases();
+ this.aliases = emojiAliases;
$(document).off('click', '.js-add-award').on('click', '.js-add-award', (function(_this) {
return function(e) {
e.stopPropagation();
@@ -134,7 +136,7 @@
return this.decrementCounter($emojiButton, emoji);
} else {
counter = $emojiButton.find('.js-counter');
- counter.text(parseInt(counter.text()) + 1);
+ counter.text(parseInt(counter.text(), 10) + 1);
$emojiButton.addClass('active');
this.addYouToUserList(votesBlock, emoji);
return this.animateEmoji($emojiButton);
@@ -211,10 +213,10 @@
};
AwardsHandler.prototype.toSentence = function(list) {
- if(list.length <= 2){
+ if (list.length <= 2) {
return list.join(' and ');
}
- else{
+ else {
return list.slice(0, -1).join(', ') + ', and ' + list[list.length - 1];
}
};
@@ -339,7 +341,7 @@
if (Cookies.get('frequently_used_emojis')) {
frequentlyUsedEmojis = this.getFrequentlyUsedEmojis();
ul = $("<ul class='clearfix emoji-menu-list frequent-emojis'>");
- for (i = 0, len = frequentlyUsedEmojis.length; i < len; i++) {
+ for (i = 0, len = frequentlyUsedEmojis.length; i < len; i += 1) {
emoji = frequentlyUsedEmojis[i];
$(".emoji-menu-content [data-emoji='" + emoji + "']").closest('li').clone().appendTo(ul);
}
@@ -374,7 +376,5 @@
};
return AwardsHandler;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/behaviors/autosize.js b/app/assets/javascripts/behaviors/autosize.js
index c62a4c5a456..a489523b802 100644
--- a/app/assets/javascripts/behaviors/autosize.js
+++ b/app/assets/javascripts/behaviors/autosize.js
@@ -1,8 +1,7 @@
-/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, consistent-return, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, consistent-return, max-len */
/* global autosize */
-/*= require jquery.ba-resize */
-/*= require autosize */
+var autosize = require('vendor/autosize');
(function() {
$(function() {
@@ -26,5 +25,4 @@
autosize.update($fields);
return $fields.css('resize', 'vertical');
});
-
}).call(this);
diff --git a/app/assets/javascripts/behaviors/details_behavior.js b/app/assets/javascripts/behaviors/details_behavior.js
index 3998ee9a0a0..6af8f593872 100644
--- a/app/assets/javascripts/behaviors/details_behavior.js
+++ b/app/assets/javascripts/behaviors/details_behavior.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, quotes, no-var, vars-on-top, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, quotes, no-var, vars-on-top, max-len */
(function() {
$(function() {
$("body").on("click", ".js-details-target", function() {
@@ -23,5 +23,4 @@
return e.preventDefault();
});
});
-
}).call(this);
diff --git a/app/assets/javascripts/behaviors/quick_submit.js b/app/assets/javascripts/behaviors/quick_submit.js
index 586f941a6e3..7747306688c 100644
--- a/app/assets/javascripts/behaviors/quick_submit.js
+++ b/app/assets/javascripts/behaviors/quick_submit.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, prefer-arrow-callback, camelcase, consistent-return, quotes, object-shorthand, comma-dangle, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, prefer-arrow-callback, camelcase, consistent-return, quotes, object-shorthand, comma-dangle, max-len */
// Quick Submit behavior
//
@@ -6,7 +6,7 @@
// "Meta+Enter" (Mac) or "Ctrl+Enter" (Linux/Windows) key combination, the form
// is submitted.
//
-/*= require extensions/jquery */
+require('../extensions/jquery');
//
// ### Example Markup
@@ -74,5 +74,4 @@
return $this.tooltip('hide');
});
});
-
}).call(this);
diff --git a/app/assets/javascripts/behaviors/requires_input.js b/app/assets/javascripts/behaviors/requires_input.js
index 72362988b2e..6276933e93e 100644
--- a/app/assets/javascripts/behaviors/requires_input.js
+++ b/app/assets/javascripts/behaviors/requires_input.js
@@ -1,10 +1,10 @@
-/* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, quotes, prefer-template, prefer-arrow-callback, no-else-return, consistent-return, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, quotes, prefer-template, prefer-arrow-callback, no-else-return, consistent-return, max-len */
// Requires Input behavior
//
// When called on a form with input fields with the `required` attribute, the
// form's submit button will be disabled until all required fields have values.
//
-/*= require extensions/jquery */
+require('../extensions/jquery');
//
// ### Example Markup
@@ -59,5 +59,4 @@
return hideOrShowHelpBlock($form);
});
});
-
}).call(this);
diff --git a/app/assets/javascripts/behaviors/toggler_behavior.js b/app/assets/javascripts/behaviors/toggler_behavior.js
index 6a49715590c..a7181904ac9 100644
--- a/app/assets/javascripts/behaviors/toggler_behavior.js
+++ b/app/assets/javascripts/behaviors/toggler_behavior.js
@@ -1,6 +1,19 @@
/* eslint-disable wrap-iife, func-names, space-before-function-paren, prefer-arrow-callback, vars-on-top, no-var, max-len */
(function(w) {
$(function() {
+ var toggleContainer = function(container, /* optional */toggleState) {
+ var $container = $(container);
+
+ $container
+ .find('.js-toggle-button .fa')
+ .toggleClass('fa-chevron-up', toggleState)
+ .toggleClass('fa-chevron-down', toggleState !== undefined ? !toggleState : undefined);
+
+ $container
+ .find('.js-toggle-content')
+ .toggle(toggleState);
+ };
+
// Toggle button. Show/hide content inside parent container.
// Button does not change visibility. If button has icon - it changes chevron style.
//
@@ -10,14 +23,7 @@
//
$('body').on('click', '.js-toggle-button', function(e) {
e.preventDefault();
- $(this)
- .find('.fa')
- .toggleClass('fa-chevron-down fa-chevron-up')
- .end()
- .closest('.js-toggle-container')
- .find('.js-toggle-content')
- .toggle()
- ;
+ toggleContainer($(this).closest('.js-toggle-container'));
});
// If we're accessing a permalink, ensure it is not inside a
@@ -26,8 +32,8 @@
var anchor = hash && document.getElementById(hash);
var container = anchor && $(anchor).closest('.js-toggle-container');
- if (container && container.find('.js-toggle-content').is(':hidden')) {
- container.find('.js-toggle-button').trigger('click');
+ if (container) {
+ toggleContainer(container, true);
anchor.scrollIntoView();
}
});
diff --git a/app/assets/javascripts/blob/blob_ci_yaml.js.es6 b/app/assets/javascripts/blob/blob_ci_yaml.js.es6
index 57bd13eecf8..ec1c018424d 100644
--- a/app/assets/javascripts/blob/blob_ci_yaml.js.es6
+++ b/app/assets/javascripts/blob/blob_ci_yaml.js.es6
@@ -1,9 +1,9 @@
-/* eslint-disable padded-blocks, no-param-reassign, comma-dangle */
+/* eslint-disable no-param-reassign, comma-dangle */
/* global Api */
-/*= require blob/template_selector */
-((global) => {
+require('./template_selector');
+((global) => {
class BlobCiYamlSelector extends gl.TemplateSelector {
requestFile(query) {
return Api.gitlabCiYml(query.name, this.requestFileSuccess.bind(this));
@@ -39,5 +39,4 @@
}
global.BlobCiYamlSelectors = BlobCiYamlSelectors;
-
})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/blob/blob_dockerfile_selector.js.es6 b/app/assets/javascripts/blob/blob_dockerfile_selector.js.es6
index bdf95017613..d4f60cc6ecd 100644
--- a/app/assets/javascripts/blob/blob_dockerfile_selector.js.es6
+++ b/app/assets/javascripts/blob/blob_dockerfile_selector.js.es6
@@ -1,5 +1,6 @@
/* global Api */
-/*= require blob/template_selector */
+
+require('./template_selector');
(() => {
const global = window.gl || (window.gl = {});
diff --git a/app/assets/javascripts/blob/blob_file_dropzone.js b/app/assets/javascripts/blob/blob_file_dropzone.js
index eab686c45c3..04bfe363929 100644
--- a/app/assets/javascripts/blob/blob_file_dropzone.js
+++ b/app/assets/javascripts/blob/blob_file_dropzone.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, camelcase, object-shorthand, quotes, comma-dangle, prefer-arrow-callback, no-unused-vars, prefer-template, no-useless-escape, no-alert, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, camelcase, object-shorthand, quotes, comma-dangle, prefer-arrow-callback, no-unused-vars, prefer-template, no-useless-escape, no-alert, max-len */
/* global Dropzone */
(function() {
@@ -62,7 +62,5 @@
}
return BlobFileDropzone;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/blob/blob_gitignore_selector.js b/app/assets/javascripts/blob/blob_gitignore_selector.js
index 15563e429a0..1d0bcf6471f 100644
--- a/app/assets/javascripts/blob/blob_gitignore_selector.js
+++ b/app/assets/javascripts/blob/blob_gitignore_selector.js
@@ -1,7 +1,7 @@
-/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-rest-params, padded-blocks */
+/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-rest-params */
/* global Api */
-/*= require blob/template_selector */
+require('./template_selector');
(function() {
var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
@@ -19,7 +19,5 @@
};
return BlobGitignoreSelector;
-
})(gl.TemplateSelector);
-
}).call(this);
diff --git a/app/assets/javascripts/blob/blob_gitignore_selectors.js b/app/assets/javascripts/blob/blob_gitignore_selectors.js
index d7f95093688..8236457f0f1 100644
--- a/app/assets/javascripts/blob/blob_gitignore_selectors.js
+++ b/app/assets/javascripts/blob/blob_gitignore_selectors.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-unused-expressions, no-cond-assign, no-sequences, comma-dangle, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-unused-expressions, no-cond-assign, no-sequences, comma-dangle, max-len */
/* global BlobGitignoreSelector */
(function() {
@@ -22,7 +22,5 @@
}
return BlobGitignoreSelectors;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/blob/blob_license_selector.js b/app/assets/javascripts/blob/blob_license_selector.js
index d9c6f65a083..1d5672d4c48 100644
--- a/app/assets/javascripts/blob/blob_license_selector.js
+++ b/app/assets/javascripts/blob/blob_license_selector.js
@@ -1,7 +1,7 @@
-/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-rest-params, comma-dangle, padded-blocks */
+/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-rest-params, comma-dangle */
/* global Api */
-/*= require blob/template_selector */
+require('./template_selector');
(function() {
var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
@@ -24,7 +24,5 @@
};
return BlobLicenseSelector;
-
})(gl.TemplateSelector);
-
}).call(this);
diff --git a/app/assets/javascripts/blob/blob_license_selectors.js.es6 b/app/assets/javascripts/blob/blob_license_selectors.js.es6
index 268640681d4..c5067b0feae 100644
--- a/app/assets/javascripts/blob/blob_license_selectors.js.es6
+++ b/app/assets/javascripts/blob/blob_license_selectors.js.es6
@@ -1,4 +1,4 @@
-/* eslint-disable no-unused-vars, no-param-reassign, padded-blocks */
+/* eslint-disable no-unused-vars, no-param-reassign */
/* global BlobLicenseSelector */
((global) => {
@@ -20,5 +20,4 @@
}
global.BlobLicenseSelectors = BlobLicenseSelectors;
-
})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/blob/template_selector.js.es6 b/app/assets/javascripts/blob/template_selector.js.es6
index 7a1ee9998c8..7e03ec3b391 100644
--- a/app/assets/javascripts/blob/template_selector.js.es6
+++ b/app/assets/javascripts/blob/template_selector.js.es6
@@ -1,101 +1,101 @@
-/* eslint-disable indent, comma-dangle, object-shorthand, func-names, space-before-function-paren, arrow-parens, no-unused-vars, class-methods-use-this, no-var, consistent-return, prefer-const, no-param-reassign, space-in-parens, max-len */
+/* eslint-disable comma-dangle, object-shorthand, func-names, space-before-function-paren, arrow-parens, no-unused-vars, class-methods-use-this, no-var, consistent-return, no-param-reassign, max-len */
((global) => {
- class TemplateSelector {
- constructor({ dropdown, data, pattern, wrapper, editor, fileEndpoint, $input } = {}) {
- this.onClick = this.onClick.bind(this);
- this.dropdown = dropdown;
- this.data = data;
- this.pattern = pattern;
- this.wrapper = wrapper;
- this.editor = editor;
- this.fileEndpoint = fileEndpoint;
- this.$input = $input || $('#file_name');
- this.dropdownIcon = $('.fa-chevron-down', this.dropdown);
- this.buildDropdown();
- this.bindEvents();
- this.onFilenameUpdate();
+ class TemplateSelector {
+ constructor({ dropdown, data, pattern, wrapper, editor, fileEndpoint, $input } = {}) {
+ this.onClick = this.onClick.bind(this);
+ this.dropdown = dropdown;
+ this.data = data;
+ this.pattern = pattern;
+ this.wrapper = wrapper;
+ this.editor = editor;
+ this.fileEndpoint = fileEndpoint;
+ this.$input = $input || $('#file_name');
+ this.dropdownIcon = $('.fa-chevron-down', this.dropdown);
+ this.buildDropdown();
+ this.bindEvents();
+ this.onFilenameUpdate();
- this.autosizeUpdateEvent = document.createEvent('Event');
- this.autosizeUpdateEvent.initEvent('autosize:update', true, false);
- }
+ this.autosizeUpdateEvent = document.createEvent('Event');
+ this.autosizeUpdateEvent.initEvent('autosize:update', true, false);
+ }
- buildDropdown() {
- return this.dropdown.glDropdown({
- data: this.data,
- filterable: true,
- selectable: true,
- toggleLabel: this.toggleLabel,
- search: {
- fields: ['name']
- },
- clicked: this.onClick,
- text: function(item) {
- return item.name;
- }
- });
- }
+ buildDropdown() {
+ return this.dropdown.glDropdown({
+ data: this.data,
+ filterable: true,
+ selectable: true,
+ toggleLabel: this.toggleLabel,
+ search: {
+ fields: ['name']
+ },
+ clicked: this.onClick,
+ text: function(item) {
+ return item.name;
+ }
+ });
+ }
- bindEvents() {
- return this.$input.on('keyup blur', (e) => this.onFilenameUpdate());
- }
+ bindEvents() {
+ return this.$input.on('keyup blur', (e) => this.onFilenameUpdate());
+ }
- toggleLabel(item) {
- return item.name;
- }
+ toggleLabel(item) {
+ return item.name;
+ }
- onFilenameUpdate() {
- var filenameMatches;
- if (!this.$input.length) {
- return;
- }
- filenameMatches = this.pattern.test(this.$input.val().trim());
- if (!filenameMatches) {
- this.wrapper.addClass('hidden');
- return;
- }
- return this.wrapper.removeClass('hidden');
+ onFilenameUpdate() {
+ var filenameMatches;
+ if (!this.$input.length) {
+ return;
}
-
- onClick(item, el, e) {
- e.preventDefault();
- return this.requestFile(item);
+ filenameMatches = this.pattern.test(this.$input.val().trim());
+ if (!filenameMatches) {
+ this.wrapper.addClass('hidden');
+ return;
}
+ return this.wrapper.removeClass('hidden');
+ }
- requestFile(item) {
- // This `requestFile` method is an abstract method that should
- // be added by all subclasses.
- }
+ onClick(item, el, e) {
+ e.preventDefault();
+ return this.requestFile(item);
+ }
- // To be implemented on the extending class
- // e.g.
- // Api.gitignoreText item.name, @requestFileSuccess.bind(@)
- requestFileSuccess(file, { skipFocus } = {}) {
- if (!file) return;
+ requestFile(item) {
+ // This `requestFile` method is an abstract method that should
+ // be added by all subclasses.
+ }
- const oldValue = this.editor.getValue();
- let newValue = file.content;
+ // To be implemented on the extending class
+ // e.g.
+ // Api.gitignoreText item.name, @requestFileSuccess.bind(@)
+ requestFileSuccess(file, { skipFocus } = {}) {
+ if (!file) return;
- this.editor.setValue(newValue, 1);
- if (!skipFocus) this.editor.focus();
+ const oldValue = this.editor.getValue();
+ const newValue = file.content;
- if (this.editor instanceof jQuery) {
- this.editor.get(0).dispatchEvent(this.autosizeUpdateEvent);
- }
- }
+ this.editor.setValue(newValue, 1);
+ if (!skipFocus) this.editor.focus();
- startLoadingSpinner() {
- this.dropdownIcon
- .addClass('fa-spinner fa-spin')
- .removeClass('fa-chevron-down');
+ if (this.editor instanceof jQuery) {
+ this.editor.get(0).dispatchEvent(this.autosizeUpdateEvent);
}
+ }
- stopLoadingSpinner() {
- this.dropdownIcon
- .addClass('fa-chevron-down')
- .removeClass('fa-spinner fa-spin');
- }
+ startLoadingSpinner() {
+ this.dropdownIcon
+ .addClass('fa-spinner fa-spin')
+ .removeClass('fa-chevron-down');
+ }
+
+ stopLoadingSpinner() {
+ this.dropdownIcon
+ .addClass('fa-chevron-down')
+ .removeClass('fa-spinner fa-spin');
}
+ }
- global.TemplateSelector = TemplateSelector;
- })(window.gl || ( window.gl = {}));
+ global.TemplateSelector = TemplateSelector;
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/blob_edit/blob_edit_bundle.js b/app/assets/javascripts/blob_edit/blob_edit_bundle.js
index 8c40e36a80a..9e0754819fa 100644
--- a/app/assets/javascripts/blob_edit/blob_edit_bundle.js
+++ b/app/assets/javascripts/blob_edit/blob_edit_bundle.js
@@ -1,8 +1,8 @@
-/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, quotes, vars-on-top, no-unused-vars, no-new, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, quotes, vars-on-top, no-unused-vars, no-new, max-len */
/* global EditBlob */
/* global NewCommitForm */
-/*= require_tree . */
+require('./edit_blob');
(function() {
$(function() {
@@ -12,5 +12,4 @@
var blob = new EditBlob(url, $('.js-edit-blob-form').data('blob-language'));
new NewCommitForm($('.js-edit-blob-form'));
});
-
}).call(this);
diff --git a/app/assets/javascripts/blob_edit/edit_blob.js b/app/assets/javascripts/blob_edit/edit_blob.js
index fa43ff611cc..079445e8278 100644
--- a/app/assets/javascripts/blob_edit/edit_blob.js
+++ b/app/assets/javascripts/blob_edit/edit_blob.js
@@ -1,9 +1,9 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, camelcase, no-param-reassign, quotes, prefer-template, no-new, comma-dangle, one-var, one-var-declaration-per-line, prefer-arrow-callback, no-else-return, no-unused-vars, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, camelcase, no-param-reassign, quotes, prefer-template, no-new, comma-dangle, one-var, one-var-declaration-per-line, prefer-arrow-callback, no-else-return, no-unused-vars, max-len */
/* global ace */
/* global BlobGitignoreSelectors */
(function() {
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+ var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
this.EditBlob = (function() {
function EditBlob(assets_path, ace_mode) {
@@ -84,7 +84,5 @@
};
return EditBlob;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/boards/boards_bundle.js.es6 b/app/assets/javascripts/boards/boards_bundle.js.es6
index ab2343c72fc..c345fb6ce14 100644
--- a/app/assets/javascripts/boards/boards_bundle.js.es6
+++ b/app/assets/javascripts/boards/boards_bundle.js.es6
@@ -1,23 +1,27 @@
-/* eslint-disable one-var, indent, quote-props, comma-dangle, space-before-function-paren */
+/* eslint-disable one-var, quote-props, comma-dangle, space-before-function-paren, import/newline-after-import, no-multi-spaces, max-len */
/* global Vue */
/* global BoardService */
-//= require vue
-//= require vue-resource
-//= require Sortable
-//= require_tree ./models
-//= require_tree ./stores
-//= require_tree ./services
-//= require_tree ./mixins
-//= require_tree ./filters
-//= require ./components/board
-//= require ./components/board_sidebar
-//= require ./components/new_list_dropdown
-//= require ./vue_resource_interceptor
+function requireAll(context) { return context.keys().map(context); }
+
+window.Vue = require('vue');
+window.Vue.use(require('vue-resource'));
+window.Sortable = require('vendor/Sortable');
+requireAll(require.context('./models', true, /^\.\/.*\.(js|es6)$/));
+requireAll(require.context('./stores', true, /^\.\/.*\.(js|es6)$/));
+requireAll(require.context('./services', true, /^\.\/.*\.(js|es6)$/));
+requireAll(require.context('./mixins', true, /^\.\/.*\.(js|es6)$/));
+requireAll(require.context('./filters', true, /^\.\/.*\.(js|es6)$/));
+require('./components/board');
+require('./components/board_sidebar');
+require('./components/new_list_dropdown');
+require('./components/modal/index');
+require('../vue_shared/vue_resource_interceptor');
$(() => {
- const $boardApp = document.getElementById('board-app'),
- Store = gl.issueBoards.BoardsStore;
+ const $boardApp = document.getElementById('board-app');
+ const Store = gl.issueBoards.BoardsStore;
+ const ModalStore = gl.issueBoards.ModalStore;
window.gl = window.gl || {};
@@ -31,7 +35,8 @@ $(() => {
el: $boardApp,
components: {
'board': gl.issueBoards.Board,
- 'board-sidebar': gl.issueBoards.BoardSidebar
+ 'board-sidebar': gl.issueBoards.BoardSidebar,
+ 'board-add-issues-modal': gl.issueBoards.IssuesModal,
},
data: {
state: Store.state,
@@ -40,6 +45,8 @@ $(() => {
boardId: $boardApp.dataset.boardId,
disabled: $boardApp.dataset.disabled === 'true',
issueLinkBase: $boardApp.dataset.issueLinkBase,
+ rootPath: $boardApp.dataset.rootPath,
+ bulkUpdatePath: $boardApp.dataset.bulkUpdatePath,
detailIssue: Store.detail
},
computed: {
@@ -48,7 +55,7 @@ $(() => {
},
},
created () {
- gl.boardService = new BoardService(this.endpoint, this.boardId);
+ gl.boardService = new BoardService(this.endpoint, this.bulkUpdatePath, this.boardId);
},
mounted () {
Store.disabled = this.disabled;
@@ -59,8 +66,6 @@ $(() => {
if (list.type === 'done') {
list.position = Infinity;
- } else if (list.type === 'backlog') {
- list.position = -1;
}
});
@@ -73,7 +78,7 @@ $(() => {
});
gl.IssueBoardsSearch = new Vue({
- el: '#js-boards-seach',
+ el: document.getElementById('js-boards-search'),
data: {
filters: Store.state.filters
},
@@ -81,4 +86,27 @@ $(() => {
gl.issueBoards.newListDropdownInit();
}
});
+
+ gl.IssueBoardsModalAddBtn = new Vue({
+ mixins: [gl.issueBoards.ModalMixins],
+ el: document.getElementById('js-add-issues-btn'),
+ data: {
+ modal: ModalStore.store,
+ store: Store.state,
+ },
+ computed: {
+ disabled() {
+ return Store.shouldAddBlankState();
+ },
+ },
+ template: `
+ <button
+ class="btn btn-create pull-right prepend-left-10 has-tooltip"
+ type="button"
+ :disabled="disabled"
+ @click="toggleModal(true)">
+ Add issues
+ </button>
+ `,
+ });
});
diff --git a/app/assets/javascripts/boards/components/board.js.es6 b/app/assets/javascripts/boards/components/board.js.es6
index d1fb0ec48e0..18324de18b3 100644
--- a/app/assets/javascripts/boards/components/board.js.es6
+++ b/app/assets/javascripts/boards/components/board.js.es6
@@ -1,10 +1,10 @@
-/* eslint-disable comma-dangle, space-before-function-paren, one-var, indent, radix */
+/* eslint-disable comma-dangle, space-before-function-paren, one-var */
/* global Vue */
/* global Sortable */
-//= require ./board_blank_state
-//= require ./board_delete
-//= require ./board_list
+require('./board_blank_state');
+require('./board_delete');
+require('./board_list');
(() => {
const Store = gl.issueBoards.BoardsStore;
@@ -22,7 +22,8 @@
props: {
list: Object,
disabled: Boolean,
- issueLinkBase: String
+ issueLinkBase: String,
+ rootPath: String,
},
data () {
return {
@@ -45,14 +46,28 @@
const issue = this.list.findIssue(this.detailIssue.issue.id);
if (issue) {
+ const offsetLeft = this.$el.offsetLeft;
const boardsList = document.querySelectorAll('.boards-list')[0];
- const right = (this.$el.offsetLeft + this.$el.offsetWidth) - boardsList.offsetWidth;
- const left = boardsList.scrollLeft - this.$el.offsetLeft;
+ const left = boardsList.scrollLeft - offsetLeft;
+ let right = (offsetLeft + this.$el.offsetWidth);
+
+ if (window.innerWidth > 768 && boardsList.classList.contains('is-compact')) {
+ // -290 here because width of boardsList is animating so therefore
+ // getting the width here is incorrect
+ // 290 is the width of the sidebar
+ right -= (boardsList.offsetWidth - 290);
+ } else {
+ right -= boardsList.offsetWidth;
+ }
if (right - boardsList.scrollLeft > 0) {
- boardsList.scrollLeft = right;
+ $(boardsList).animate({
+ scrollLeft: right
+ }, this.sortableOptions.animation);
} else if (left > 0) {
- boardsList.scrollLeft = this.$el.offsetLeft;
+ $(boardsList).animate({
+ scrollLeft: offsetLeft
+ }, this.sortableOptions.animation);
}
}
},
@@ -65,7 +80,7 @@
}
},
mounted () {
- const options = gl.issueBoards.getBoardSortableDefaultOptions({
+ this.sortableOptions = gl.issueBoards.getBoardSortableDefaultOptions({
disabled: this.disabled,
group: 'boards',
draggable: '.is-draggable',
@@ -74,8 +89,8 @@
gl.issueBoards.onEnd();
if (e.newIndex !== undefined && e.oldIndex !== e.newIndex) {
- const order = this.sortable.toArray(),
- list = Store.findList('id', parseInt(e.item.dataset.id));
+ const order = this.sortable.toArray();
+ const list = Store.findList('id', parseInt(e.item.dataset.id, 10));
this.$nextTick(() => {
Store.moveList(list, order);
@@ -84,7 +99,7 @@
}
});
- this.sortable = Sortable.create(this.$el.parentNode, options);
+ this.sortable = Sortable.create(this.$el.parentNode, this.sortableOptions);
},
});
})();
diff --git a/app/assets/javascripts/boards/components/board_blank_state.js.es6 b/app/assets/javascripts/boards/components/board_blank_state.js.es6
index 0a47a22fad2..d76314c1892 100644
--- a/app/assets/javascripts/boards/components/board_blank_state.js.es6
+++ b/app/assets/javascripts/boards/components/board_blank_state.js.es6
@@ -1,4 +1,4 @@
-/* eslint-disable space-before-function-paren, comma-dangle, semi */
+/* eslint-disable space-before-function-paren, comma-dangle */
/* global Vue */
/* global ListLabel */
@@ -15,7 +15,7 @@
new ListLabel({ title: 'To Do', color: '#F0AD4E' }),
new ListLabel({ title: 'Doing', color: '#5CB85C' })
]
- }
+ };
},
methods: {
addDefaultLists () {
diff --git a/app/assets/javascripts/boards/components/board_card.js.es6 b/app/assets/javascripts/boards/components/board_card.js.es6
index 5fc50280811..0ea66bd027c 100644
--- a/app/assets/javascripts/boards/components/board_card.js.es6
+++ b/app/assets/javascripts/boards/components/board_card.js.es6
@@ -1,6 +1,8 @@
/* eslint-disable comma-dangle, space-before-function-paren, dot-notation */
/* global Vue */
+require('./issue_card_inner');
+
(() => {
const Store = gl.issueBoards.BoardsStore;
@@ -9,12 +11,16 @@
gl.issueBoards.BoardCard = Vue.extend({
template: '#js-board-list-card',
+ components: {
+ 'issue-card-inner': gl.issueBoards.IssueCardInner,
+ },
props: {
list: Object,
issue: Object,
issueLinkBase: String,
disabled: Boolean,
- index: Number
+ index: Number,
+ rootPath: String,
},
data () {
return {
@@ -28,31 +34,6 @@
}
},
methods: {
- filterByLabel (label, e) {
- let labelToggleText = label.title;
- const labelIndex = Store.state.filters['label_name'].indexOf(label.title);
- $(e.target).tooltip('hide');
-
- if (labelIndex === -1) {
- Store.state.filters['label_name'].push(label.title);
- $('.labels-filter').prepend(`<input type="hidden" name="label_name[]" value="${label.title}" />`);
- } else {
- Store.state.filters['label_name'].splice(labelIndex, 1);
- labelToggleText = Store.state.filters['label_name'][0];
- $(`.labels-filter input[name="label_name[]"][value="${label.title}"]`).remove();
- }
-
- const selectedLabels = Store.state.filters['label_name'];
- if (selectedLabels.length === 0) {
- labelToggleText = 'Label';
- } else if (selectedLabels.length > 1) {
- labelToggleText = `${selectedLabels[0]} + ${selectedLabels.length - 1} more`;
- }
-
- $('.labels-filter .dropdown-toggle-text').text(labelToggleText);
-
- Store.updateFiltersUrl();
- },
mouseDown () {
this.showDetail = true;
},
@@ -71,6 +52,7 @@
Store.detail.issue = {};
} else {
Store.detail.issue = this.issue;
+ Store.detail.list = this.list;
}
}
}
diff --git a/app/assets/javascripts/boards/components/board_list.js.es6 b/app/assets/javascripts/boards/components/board_list.js.es6
index 6711930622b..60b0a30af3f 100644
--- a/app/assets/javascripts/boards/components/board_list.js.es6
+++ b/app/assets/javascripts/boards/components/board_list.js.es6
@@ -1,9 +1,9 @@
-/* eslint-disable comma-dangle, space-before-function-paren, max-len, no-plusplus */
+/* eslint-disable comma-dangle, space-before-function-paren, max-len */
/* global Vue */
/* global Sortable */
-//= require ./board_card
-//= require ./board_new_issue
+require('./board_card');
+require('./board_new_issue');
(() => {
const Store = gl.issueBoards.BoardsStore;
@@ -23,6 +23,7 @@
issues: Array,
loading: Boolean,
issueLinkBase: String,
+ rootPath: String,
},
data () {
return {
@@ -43,7 +44,7 @@
issues () {
this.$nextTick(() => {
if (this.scrollHeight() <= this.listHeight() && this.list.issuesSize > this.list.issues.length) {
- this.list.page++;
+ this.list.page += 1;
this.list.getIssues(false);
}
diff --git a/app/assets/javascripts/boards/components/board_new_issue.js.es6 b/app/assets/javascripts/boards/components/board_new_issue.js.es6
index 2386d3a613c..b5c14a198ba 100644
--- a/app/assets/javascripts/boards/components/board_new_issue.js.es6
+++ b/app/assets/javascripts/boards/components/board_new_issue.js.es6
@@ -37,6 +37,7 @@
$(this.$refs.submitButton).enable();
Store.detail.issue = issue;
+ Store.detail.list = this.list;
})
.catch(() => {
// Need this because our jQuery very kindly disables buttons on ALL form submissions
diff --git a/app/assets/javascripts/boards/components/board_sidebar.js.es6 b/app/assets/javascripts/boards/components/board_sidebar.js.es6
index 02459722bbf..dfc6eed785c 100644
--- a/app/assets/javascripts/boards/components/board_sidebar.js.es6
+++ b/app/assets/javascripts/boards/components/board_sidebar.js.es6
@@ -5,6 +5,8 @@
/* global LabelsSelect */
/* global Sidebar */
+require('./sidebar/remove_issue');
+
(() => {
const Store = gl.issueBoards.BoardsStore;
@@ -18,7 +20,8 @@
data() {
return {
detail: Store.detail,
- issue: {}
+ issue: {},
+ list: {},
};
},
computed: {
@@ -29,7 +32,14 @@
watch: {
detail: {
handler () {
+ if (this.issue.id !== this.detail.issue.id) {
+ $('.js-issue-board-sidebar', this.$el).each((i, el) => {
+ $(el).data('glDropdown').clearMenu();
+ });
+ }
+
this.issue = this.detail.issue;
+ this.list = this.detail.list;
},
deep: true
},
@@ -54,6 +64,9 @@
new LabelsSelect();
new Sidebar();
gl.Subscription.bindAll('.subscription');
- }
+ },
+ components: {
+ removeBtn: gl.issueBoards.RemoveIssueBtn,
+ },
});
})();
diff --git a/app/assets/javascripts/boards/components/issue_card_inner.js.es6 b/app/assets/javascripts/boards/components/issue_card_inner.js.es6
new file mode 100644
index 00000000000..22a8b971ff8
--- /dev/null
+++ b/app/assets/javascripts/boards/components/issue_card_inner.js.es6
@@ -0,0 +1,111 @@
+/* global Vue */
+(() => {
+ const Store = gl.issueBoards.BoardsStore;
+
+ window.gl = window.gl || {};
+ window.gl.issueBoards = window.gl.issueBoards || {};
+
+ gl.issueBoards.IssueCardInner = Vue.extend({
+ props: {
+ issue: {
+ type: Object,
+ required: true,
+ },
+ issueLinkBase: {
+ type: String,
+ required: true,
+ },
+ list: {
+ type: Object,
+ required: false,
+ },
+ rootPath: {
+ type: String,
+ required: true,
+ },
+ },
+ methods: {
+ showLabel(label) {
+ if (!this.list) return true;
+
+ return !this.list.label || label.id !== this.list.label.id;
+ },
+ filterByLabel(label, e) {
+ let labelToggleText = label.title;
+ const labelIndex = Store.state.filters.label_name.indexOf(label.title);
+ $(e.currentTarget).tooltip('hide');
+
+ if (labelIndex === -1) {
+ Store.state.filters.label_name.push(label.title);
+ $('.labels-filter').prepend(`<input type="hidden" name="label_name[]" value="${label.title}" />`);
+ } else {
+ Store.state.filters.label_name.splice(labelIndex, 1);
+ labelToggleText = Store.state.filters.label_name[0];
+ $(`.labels-filter input[name="label_name[]"][value="${label.title}"]`).remove();
+ }
+
+ const selectedLabels = Store.state.filters.label_name;
+ if (selectedLabels.length === 0) {
+ labelToggleText = 'Label';
+ } else if (selectedLabels.length > 1) {
+ labelToggleText = `${selectedLabels[0]} + ${selectedLabels.length - 1} more`;
+ }
+
+ $('.labels-filter .dropdown-toggle-text').text(labelToggleText);
+
+ Store.updateFiltersUrl();
+ },
+ labelStyle(label) {
+ return {
+ backgroundColor: label.color,
+ color: label.textColor,
+ };
+ },
+ },
+ template: `
+ <div>
+ <h4 class="card-title">
+ <i
+ class="fa fa-eye-slash confidential-icon"
+ v-if="issue.confidential"></i>
+ <a
+ :href="issueLinkBase + '/' + issue.id"
+ :title="issue.title">
+ {{ issue.title }}
+ </a>
+ </h4>
+ <div class="card-footer">
+ <span
+ class="card-number"
+ v-if="issue.id">
+ #{{ issue.id }}
+ </span>
+ <a
+ class="card-assignee has-tooltip"
+ :href="rootPath + issue.assignee.username"
+ :title="'Assigned to ' + issue.assignee.name"
+ v-if="issue.assignee"
+ data-container="body">
+ <img
+ class="avatar avatar-inline s20"
+ :src="issue.assignee.avatar"
+ width="20"
+ height="20"
+ :alt="'Avatar for ' + issue.assignee.name" />
+ </a>
+ <button
+ class="label color-label has-tooltip"
+ v-for="label in issue.labels"
+ type="button"
+ v-if="showLabel(label)"
+ @click="filterByLabel(label, $event)"
+ :style="labelStyle(label)"
+ :title="label.description"
+ data-container="body">
+ {{ label.title }}
+ </button>
+ </div>
+ </div>
+ `,
+ });
+})();
diff --git a/app/assets/javascripts/boards/components/modal/empty_state.js.es6 b/app/assets/javascripts/boards/components/modal/empty_state.js.es6
new file mode 100644
index 00000000000..9538f5b69e9
--- /dev/null
+++ b/app/assets/javascripts/boards/components/modal/empty_state.js.es6
@@ -0,0 +1,70 @@
+/* global Vue */
+(() => {
+ const ModalStore = gl.issueBoards.ModalStore;
+
+ gl.issueBoards.ModalEmptyState = Vue.extend({
+ mixins: [gl.issueBoards.ModalMixins],
+ data() {
+ return ModalStore.store;
+ },
+ props: {
+ image: {
+ type: String,
+ required: true,
+ },
+ newIssuePath: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ contents() {
+ const obj = {
+ title: 'You haven\'t added any issues to your project yet',
+ content: `
+ An issue can be a bug, a todo or a feature request that needs to be
+ discussed in a project. Besides, issues are searchable and filterable.
+ `,
+ };
+
+ if (this.activeTab === 'selected') {
+ obj.title = 'You haven\'t selected any issues yet';
+ obj.content = `
+ Go back to <strong>All issues</strong> and select some issues
+ to add to your board.
+ `;
+ }
+
+ return obj;
+ },
+ },
+ template: `
+ <section class="empty-state">
+ <div class="row">
+ <div class="col-xs-12 col-sm-6 col-sm-push-6">
+ <aside class="svg-content" v-html="image"></aside>
+ </div>
+ <div class="col-xs-12 col-sm-6 col-sm-pull-6">
+ <div class="text-content">
+ <h4>{{ contents.title }}</h4>
+ <p v-html="contents.content"></p>
+ <a
+ :href="newIssuePath"
+ class="btn btn-success btn-inverted"
+ v-if="activeTab === 'all'">
+ New issue
+ </a>
+ <button
+ type="button"
+ class="btn btn-default"
+ @click="changeTab('all')"
+ v-if="activeTab === 'selected'">
+ All issues
+ </button>
+ </div>
+ </div>
+ </div>
+ </section>
+ `,
+ });
+})();
diff --git a/app/assets/javascripts/boards/components/modal/filters.js.es6 b/app/assets/javascripts/boards/components/modal/filters.js.es6
new file mode 100644
index 00000000000..6de06811d94
--- /dev/null
+++ b/app/assets/javascripts/boards/components/modal/filters.js.es6
@@ -0,0 +1,49 @@
+/* global Vue */
+const userFilter = require('./filters/user');
+const milestoneFilter = require('./filters/milestone');
+const labelFilter = require('./filters/label');
+
+module.exports = Vue.extend({
+ name: 'modal-filters',
+ props: {
+ projectId: {
+ type: Number,
+ required: true,
+ },
+ milestonePath: {
+ type: String,
+ required: true,
+ },
+ labelPath: {
+ type: String,
+ required: true,
+ },
+ },
+ destroyed() {
+ gl.issueBoards.ModalStore.setDefaultFilter();
+ },
+ components: {
+ userFilter,
+ milestoneFilter,
+ labelFilter,
+ },
+ template: `
+ <div class="modal-filters">
+ <user-filter
+ dropdown-class-name="dropdown-menu-author"
+ toggle-class-name="js-user-search js-author-search"
+ toggle-label="Author"
+ field-name="author_id"
+ :project-id="projectId"></user-filter>
+ <user-filter
+ dropdown-class-name="dropdown-menu-author"
+ toggle-class-name="js-assignee-search"
+ toggle-label="Assignee"
+ field-name="assignee_id"
+ :null-user="true"
+ :project-id="projectId"></user-filter>
+ <milestone-filter :milestone-path="milestonePath"></milestone-filter>
+ <label-filter :label-path="labelPath"></label-filter>
+ </div>
+ `,
+});
diff --git a/app/assets/javascripts/boards/components/modal/filters/label.js.es6 b/app/assets/javascripts/boards/components/modal/filters/label.js.es6
new file mode 100644
index 00000000000..4fc8f72a145
--- /dev/null
+++ b/app/assets/javascripts/boards/components/modal/filters/label.js.es6
@@ -0,0 +1,54 @@
+/* eslint-disable no-new */
+/* global Vue */
+/* global LabelsSelect */
+module.exports = Vue.extend({
+ name: 'filter-label',
+ props: {
+ labelPath: {
+ type: String,
+ required: true,
+ },
+ },
+ mounted() {
+ new LabelsSelect(this.$refs.dropdown);
+ },
+ template: `
+ <div class="dropdown">
+ <button
+ class="dropdown-menu-toggle js-label-select js-multiselect js-extra-options"
+ type="button"
+ data-toggle="dropdown"
+ data-show-any="true"
+ data-show-no="true"
+ :data-labels="labelPath"
+ ref="dropdown">
+ <span class="dropdown-toggle-text">
+ Label
+ </span>
+ <i class="fa fa-chevron-down"></i>
+ </button>
+ <div class="dropdown-menu dropdown-select dropdown-menu-paging dropdown-menu-labels dropdown-menu-selectable">
+ <div class="dropdown-title">
+ Filter by label
+ <button
+ class="dropdown-title-button dropdown-menu-close"
+ aria-label="Close"
+ type="button">
+ <i class="fa fa-times dropdown-menu-close-icon"></i>
+ </button>
+ </div>
+ <div class="dropdown-input">
+ <input
+ type="search"
+ class="dropdown-input-field"
+ placeholder="Search"
+ autocomplete="off" />
+ <i class="fa fa-search dropdown-input-search"></i>
+ <i role="button" class="fa fa-times dropdown-input-clear js-dropdown-input-clear"></i>
+ </div>
+ <div class="dropdown-content"></div>
+ <div class="dropdown-loading"><i class="fa fa-spinner fa-spin"></i></div>
+ </div>
+ </div>
+ `,
+});
diff --git a/app/assets/javascripts/boards/components/modal/filters/milestone.js.es6 b/app/assets/javascripts/boards/components/modal/filters/milestone.js.es6
new file mode 100644
index 00000000000..d555599d300
--- /dev/null
+++ b/app/assets/javascripts/boards/components/modal/filters/milestone.js.es6
@@ -0,0 +1,55 @@
+/* eslint-disable no-new */
+/* global Vue */
+/* global MilestoneSelect */
+module.exports = Vue.extend({
+ name: 'filter-milestone',
+ props: {
+ milestonePath: {
+ type: String,
+ required: true,
+ },
+ },
+ mounted() {
+ new MilestoneSelect(null, this.$refs.dropdown);
+ },
+ template: `
+ <div class="dropdown">
+ <button
+ class="dropdown-menu-toggle js-milestone-select"
+ type="button"
+ data-toggle="dropdown"
+ data-show-any="true"
+ data-show-upcoming="true"
+ data-field-name="milestone_title"
+ :data-milestones="milestonePath"
+ ref="dropdown">
+ <span class="dropdown-toggle-text">
+ Milestone
+ </span>
+ <i class="fa fa-chevron-down"></i>
+ </button>
+ <div class="dropdown-menu dropdown-select dropdown-menu-selectable dropdown-menu-milestone">
+ <div class="dropdown-title">
+ <span>Filter by milestone</span>
+ <button
+ class="dropdown-title-button dropdown-menu-close"
+ aria-label="Close"
+ type="button">
+ <i class="fa fa-times dropdown-menu-close-icon"></i>
+ </button>
+ </div>
+ <div class="dropdown-input">
+ <input
+ type="search"
+ class="dropdown-input-field"
+ placeholder="Search milestones"
+ autocomplete="off" />
+ <i class="fa fa-search dropdown-input-search"></i>
+ <i role="button" class="fa fa-times dropdown-input-clear js-dropdown-input-clear"></i>
+ </div>
+ <div class="dropdown-content"></div>
+ <div class="dropdown-loading"><i class="fa fa-spinner fa-spin"></i></div>
+ </div>
+ </div>
+ `,
+});
diff --git a/app/assets/javascripts/boards/components/modal/filters/user.js.es6 b/app/assets/javascripts/boards/components/modal/filters/user.js.es6
new file mode 100644
index 00000000000..8523028c29c
--- /dev/null
+++ b/app/assets/javascripts/boards/components/modal/filters/user.js.es6
@@ -0,0 +1,96 @@
+/* eslint-disable no-new */
+/* global Vue */
+/* global UsersSelect */
+module.exports = Vue.extend({
+ name: 'filter-user',
+ props: {
+ toggleClassName: {
+ type: String,
+ required: true,
+ },
+ dropdownClassName: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ toggleLabel: {
+ type: String,
+ required: true,
+ },
+ fieldName: {
+ type: String,
+ required: true,
+ },
+ nullUser: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ projectId: {
+ type: Number,
+ required: true,
+ },
+ },
+ mounted() {
+ new UsersSelect(null, this.$refs.dropdown);
+ },
+ computed: {
+ currentUsername() {
+ return gon.current_username;
+ },
+ dropdownTitle() {
+ return `Filter by ${this.toggleLabel.toLowerCase()}`;
+ },
+ inputPlaceholder() {
+ return `Search ${this.toggleLabel.toLowerCase()}`;
+ },
+ },
+ template: `
+ <div class="dropdown">
+ <button
+ class="dropdown-menu-toggle js-user-search"
+ :class="toggleClassName"
+ type="button"
+ data-toggle="dropdown"
+ data-current-user="true"
+ :data-any-user="'Any ' + toggleLabel"
+ :data-null-user="nullUser"
+ :data-field-name="fieldName"
+ :data-project-id="projectId"
+ :data-first-user="currentUsername"
+ ref="dropdown">
+ <span class="dropdown-toggle-text">
+ {{ toggleLabel }}
+ </span>
+ <i class="fa fa-chevron-down"></i>
+ </button>
+ <div
+ class="dropdown-menu dropdown-select dropdown-menu-user dropdown-menu-selectable"
+ :class="dropdownClassName">
+ <div class="dropdown-title">
+ {{ dropdownTitle }}
+ <button
+ class="dropdown-title-button dropdown-menu-close"
+ aria-label="Close"
+ type="button">
+ <i class="fa fa-times dropdown-menu-close-icon"></i>
+ </button>
+ </div>
+ <div class="dropdown-input">
+ <input
+ type="search"
+ class="dropdown-input-field"
+ autocomplete="off"
+ :placeholder="inputPlaceholder" />
+ <i class="fa fa-search dropdown-input-search"></i>
+ <i
+ role="button"
+ class="fa fa-times dropdown-input-clear js-dropdown-input-clear">
+ </i>
+ </div>
+ <div class="dropdown-content"></div>
+ <div class="dropdown-loading"><i class="fa fa-spinner fa-spin"></i></div>
+ </div>
+ </div>
+ `,
+});
diff --git a/app/assets/javascripts/boards/components/modal/footer.js.es6 b/app/assets/javascripts/boards/components/modal/footer.js.es6
new file mode 100644
index 00000000000..1cbc422c961
--- /dev/null
+++ b/app/assets/javascripts/boards/components/modal/footer.js.es6
@@ -0,0 +1,83 @@
+/* eslint-disable no-new */
+/* global Vue */
+/* global Flash */
+
+require('./lists_dropdown');
+
+(() => {
+ const ModalStore = gl.issueBoards.ModalStore;
+
+ gl.issueBoards.ModalFooter = Vue.extend({
+ mixins: [gl.issueBoards.ModalMixins],
+ data() {
+ return {
+ modal: ModalStore.store,
+ state: gl.issueBoards.BoardsStore.state,
+ };
+ },
+ computed: {
+ submitDisabled() {
+ return !ModalStore.selectedCount();
+ },
+ submitText() {
+ const count = ModalStore.selectedCount();
+
+ return `Add ${count > 0 ? count : ''} ${gl.text.pluralize('issue', count)}`;
+ },
+ },
+ methods: {
+ addIssues() {
+ const list = this.modal.selectedList || this.state.lists[0];
+ const selectedIssues = ModalStore.getSelectedIssues();
+ const issueIds = selectedIssues.map(issue => issue.globalId);
+
+ // Post the data to the backend
+ gl.boardService.bulkUpdate(issueIds, {
+ add_label_ids: [list.label.id],
+ }).catch(() => {
+ new Flash('Failed to update issues, please try again.', 'alert');
+
+ selectedIssues.forEach((issue) => {
+ list.removeIssue(issue);
+ list.issuesSize -= 1;
+ });
+ });
+
+ // Add the issues on the frontend
+ selectedIssues.forEach((issue) => {
+ list.addIssue(issue);
+ list.issuesSize += 1;
+ });
+
+ this.toggleModal(false);
+ },
+ },
+ components: {
+ 'lists-dropdown': gl.issueBoards.ModalFooterListsDropdown,
+ },
+ template: `
+ <footer
+ class="form-actions add-issues-footer">
+ <div class="pull-left">
+ <button
+ class="btn btn-success"
+ type="button"
+ :disabled="submitDisabled"
+ @click="addIssues">
+ {{ submitText }}
+ </button>
+ <span class="inline add-issues-footer-to-list">
+ to list
+ </span>
+ <lists-dropdown></lists-dropdown>
+ </div>
+ <button
+ class="btn btn-default pull-right"
+ type="button"
+ @click="toggleModal(false)">
+ Cancel
+ </button>
+ </footer>
+ `,
+ });
+})();
diff --git a/app/assets/javascripts/boards/components/modal/header.js.es6 b/app/assets/javascripts/boards/components/modal/header.js.es6
new file mode 100644
index 00000000000..70c088f9054
--- /dev/null
+++ b/app/assets/javascripts/boards/components/modal/header.js.es6
@@ -0,0 +1,90 @@
+/* global Vue */
+require('./tabs');
+const modalFilters = require('./filters');
+
+(() => {
+ const ModalStore = gl.issueBoards.ModalStore;
+
+ gl.issueBoards.ModalHeader = Vue.extend({
+ mixins: [gl.issueBoards.ModalMixins],
+ props: {
+ projectId: {
+ type: Number,
+ required: true,
+ },
+ milestonePath: {
+ type: String,
+ required: true,
+ },
+ labelPath: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return ModalStore.store;
+ },
+ computed: {
+ selectAllText() {
+ if (ModalStore.selectedCount() !== this.issues.length || this.issues.length === 0) {
+ return 'Select all';
+ }
+
+ return 'Deselect all';
+ },
+ showSearch() {
+ return this.activeTab === 'all' && !this.loading && this.issuesCount > 0;
+ },
+ },
+ methods: {
+ toggleAll() {
+ this.$refs.selectAllBtn.blur();
+
+ ModalStore.toggleAll();
+ },
+ },
+ components: {
+ 'modal-tabs': gl.issueBoards.ModalTabs,
+ modalFilters,
+ },
+ template: `
+ <div>
+ <header class="add-issues-header form-actions">
+ <h2>
+ Add issues
+ <button
+ type="button"
+ class="close"
+ data-dismiss="modal"
+ aria-label="Close"
+ @click="toggleModal(false)">
+ <span aria-hidden="true">×</span>
+ </button>
+ </h2>
+ </header>
+ <modal-tabs v-if="!loading && issuesCount > 0"></modal-tabs>
+ <div
+ class="add-issues-search append-bottom-10"
+ v-if="showSearch">
+ <modal-filters
+ :project-id="projectId"
+ :milestone-path="milestonePath"
+ :label-path="labelPath">
+ </modal-filters>
+ <input
+ placeholder="Search issues..."
+ class="form-control"
+ type="search"
+ v-model="searchTerm" />
+ <button
+ type="button"
+ class="btn btn-success btn-inverted prepend-left-10"
+ ref="selectAllBtn"
+ @click="toggleAll">
+ {{ selectAllText }}
+ </button>
+ </div>
+ </div>
+ `,
+ });
+})();
diff --git a/app/assets/javascripts/boards/components/modal/index.js.es6 b/app/assets/javascripts/boards/components/modal/index.js.es6
new file mode 100644
index 00000000000..f290cd13763
--- /dev/null
+++ b/app/assets/javascripts/boards/components/modal/index.js.es6
@@ -0,0 +1,163 @@
+/* global Vue */
+/* global ListIssue */
+
+require('./header');
+require('./list');
+require('./footer');
+require('./empty_state');
+
+(() => {
+ const ModalStore = gl.issueBoards.ModalStore;
+
+ gl.issueBoards.IssuesModal = Vue.extend({
+ props: {
+ blankStateImage: {
+ type: String,
+ required: true,
+ },
+ newIssuePath: {
+ type: String,
+ required: true,
+ },
+ issueLinkBase: {
+ type: String,
+ required: true,
+ },
+ rootPath: {
+ type: String,
+ required: true,
+ },
+ projectId: {
+ type: Number,
+ required: true,
+ },
+ milestonePath: {
+ type: String,
+ required: true,
+ },
+ labelPath: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return ModalStore.store;
+ },
+ watch: {
+ page() {
+ this.loadIssues();
+ },
+ searchTerm() {
+ this.searchOperation();
+ },
+ showAddIssuesModal() {
+ if (this.showAddIssuesModal && !this.issues.length) {
+ this.loading = true;
+
+ this.loadIssues()
+ .then(() => {
+ this.loading = false;
+ });
+ } else if (!this.showAddIssuesModal) {
+ this.issues = [];
+ this.selectedIssues = [];
+ this.issuesCount = false;
+ }
+ },
+ filter: {
+ handler() {
+ this.loadIssues(true);
+ },
+ deep: true,
+ },
+ },
+ methods: {
+ searchOperation: _.debounce(function searchOperationDebounce() {
+ this.loadIssues(true);
+ }, 500),
+ loadIssues(clearIssues = false) {
+ if (!this.showAddIssuesModal) return false;
+
+ const queryData = Object.assign({}, this.filter, {
+ search: this.searchTerm,
+ page: this.page,
+ per: this.perPage,
+ });
+
+ return gl.boardService.getBacklog(queryData).then((res) => {
+ const data = res.json();
+
+ if (clearIssues) {
+ this.issues = [];
+ }
+
+ data.issues.forEach((issueObj) => {
+ const issue = new ListIssue(issueObj);
+ const foundSelectedIssue = ModalStore.findSelectedIssue(issue);
+ issue.selected = !!foundSelectedIssue;
+
+ this.issues.push(issue);
+ });
+
+ this.loadingNewPage = false;
+
+ if (!this.issuesCount) {
+ this.issuesCount = data.size;
+ }
+ });
+ },
+ },
+ computed: {
+ showList() {
+ if (this.activeTab === 'selected') {
+ return this.selectedIssues.length > 0;
+ }
+
+ return this.issuesCount > 0;
+ },
+ showEmptyState() {
+ if (!this.loading && this.issuesCount === 0) {
+ return true;
+ }
+
+ return this.activeTab === 'selected' && this.selectedIssues.length === 0;
+ },
+ },
+ components: {
+ 'modal-header': gl.issueBoards.ModalHeader,
+ 'modal-list': gl.issueBoards.ModalList,
+ 'modal-footer': gl.issueBoards.ModalFooter,
+ 'empty-state': gl.issueBoards.ModalEmptyState,
+ },
+ template: `
+ <div
+ class="add-issues-modal"
+ v-if="showAddIssuesModal">
+ <div class="add-issues-container">
+ <modal-header
+ :project-id="projectId"
+ :milestone-path="milestonePath"
+ :label-path="labelPath">
+ </modal-header>
+ <modal-list
+ :image="blankStateImage"
+ :issue-link-base="issueLinkBase"
+ :root-path="rootPath"
+ v-if="!loading && showList"></modal-list>
+ <empty-state
+ v-if="showEmptyState"
+ :image="blankStateImage"
+ :new-issue-path="newIssuePath"></empty-state>
+ <section
+ class="add-issues-list text-center"
+ v-if="loading">
+ <div class="add-issues-list-loading">
+ <i class="fa fa-spinner fa-spin"></i>
+ </div>
+ </section>
+ <modal-footer></modal-footer>
+ </div>
+ </div>
+ `,
+ });
+})();
diff --git a/app/assets/javascripts/boards/components/modal/list.js.es6 b/app/assets/javascripts/boards/components/modal/list.js.es6
new file mode 100644
index 00000000000..3730c1ecaeb
--- /dev/null
+++ b/app/assets/javascripts/boards/components/modal/list.js.es6
@@ -0,0 +1,159 @@
+/* global Vue */
+/* global ListIssue */
+/* global bp */
+(() => {
+ const ModalStore = gl.issueBoards.ModalStore;
+
+ gl.issueBoards.ModalList = Vue.extend({
+ props: {
+ issueLinkBase: {
+ type: String,
+ required: true,
+ },
+ rootPath: {
+ type: String,
+ required: true,
+ },
+ image: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return ModalStore.store;
+ },
+ watch: {
+ activeTab() {
+ if (this.activeTab === 'all') {
+ ModalStore.purgeUnselectedIssues();
+ }
+ },
+ },
+ computed: {
+ loopIssues() {
+ if (this.activeTab === 'all') {
+ return this.issues;
+ }
+
+ return this.selectedIssues;
+ },
+ groupedIssues() {
+ const groups = [];
+ this.loopIssues.forEach((issue, i) => {
+ const index = i % this.columns;
+
+ if (!groups[index]) {
+ groups.push([]);
+ }
+
+ groups[index].push(issue);
+ });
+
+ return groups;
+ },
+ },
+ methods: {
+ scrollHandler() {
+ const currentPage = Math.floor(this.issues.length / this.perPage);
+
+ if ((this.scrollTop() > this.scrollHeight() - 100) && !this.loadingNewPage
+ && currentPage === this.page) {
+ this.loadingNewPage = true;
+ this.page += 1;
+ }
+ },
+ toggleIssue(e, issue) {
+ if (e.target.tagName !== 'A') {
+ ModalStore.toggleIssue(issue);
+ }
+ },
+ listHeight() {
+ return this.$refs.list.getBoundingClientRect().height;
+ },
+ scrollHeight() {
+ return this.$refs.list.scrollHeight;
+ },
+ scrollTop() {
+ return this.$refs.list.scrollTop + this.listHeight();
+ },
+ showIssue(issue) {
+ if (this.activeTab === 'all') return true;
+
+ const index = ModalStore.selectedIssueIndex(issue);
+
+ return index !== -1;
+ },
+ setColumnCount() {
+ const breakpoint = bp.getBreakpointSize();
+
+ if (breakpoint === 'lg' || breakpoint === 'md') {
+ this.columns = 3;
+ } else if (breakpoint === 'sm') {
+ this.columns = 2;
+ } else {
+ this.columns = 1;
+ }
+ },
+ },
+ mounted() {
+ this.scrollHandlerWrapper = this.scrollHandler.bind(this);
+ this.setColumnCountWrapper = this.setColumnCount.bind(this);
+ this.setColumnCount();
+
+ this.$refs.list.addEventListener('scroll', this.scrollHandlerWrapper);
+ window.addEventListener('resize', this.setColumnCountWrapper);
+ },
+ beforeDestroy() {
+ this.$refs.list.removeEventListener('scroll', this.scrollHandlerWrapper);
+ window.removeEventListener('resize', this.setColumnCountWrapper);
+ },
+ components: {
+ 'issue-card-inner': gl.issueBoards.IssueCardInner,
+ },
+ template: `
+ <section
+ class="add-issues-list add-issues-list-columns"
+ ref="list">
+ <div
+ class="empty-state add-issues-empty-state-filter text-center"
+ v-if="issuesCount > 0 && issues.length === 0">
+ <div
+ class="svg-content"
+ v-html="image">
+ </div>
+ <div class="text-content">
+ <h4>
+ There are no issues to show.
+ </h4>
+ </div>
+ </div>
+ <div
+ v-for="group in groupedIssues"
+ class="add-issues-list-column">
+ <div
+ v-for="issue in group"
+ v-if="showIssue(issue)"
+ class="card-parent">
+ <div
+ class="card"
+ :class="{ 'is-active': issue.selected }"
+ @click="toggleIssue($event, issue)">
+ <issue-card-inner
+ :issue="issue"
+ :issue-link-base="issueLinkBase"
+ :root-path="rootPath">
+ </issue-card-inner>
+ <span
+ :aria-label="'Issue #' + issue.id + ' selected'"
+ aria-checked="true"
+ v-if="issue.selected"
+ class="issue-card-selected text-center">
+ <i class="fa fa-check"></i>
+ </span>
+ </div>
+ </div>
+ </div>
+ </section>
+ `,
+ });
+})();
diff --git a/app/assets/javascripts/boards/components/modal/lists_dropdown.js.es6 b/app/assets/javascripts/boards/components/modal/lists_dropdown.js.es6
new file mode 100644
index 00000000000..3c05120a2da
--- /dev/null
+++ b/app/assets/javascripts/boards/components/modal/lists_dropdown.js.es6
@@ -0,0 +1,56 @@
+/* global Vue */
+(() => {
+ const ModalStore = gl.issueBoards.ModalStore;
+
+ gl.issueBoards.ModalFooterListsDropdown = Vue.extend({
+ data() {
+ return {
+ modal: ModalStore.store,
+ state: gl.issueBoards.BoardsStore.state,
+ };
+ },
+ computed: {
+ selected() {
+ return this.modal.selectedList || this.state.lists[0];
+ },
+ },
+ destroyed() {
+ this.modal.selectedList = null;
+ },
+ template: `
+ <div class="dropdown inline">
+ <button
+ class="dropdown-menu-toggle"
+ type="button"
+ data-toggle="dropdown"
+ aria-expanded="false">
+ <span
+ class="dropdown-label-box"
+ :style="{ backgroundColor: selected.label.color }">
+ </span>
+ {{ selected.title }}
+ <i class="fa fa-chevron-down"></i>
+ </button>
+ <div class="dropdown-menu dropdown-menu-selectable dropdown-menu-drop-up">
+ <ul>
+ <li
+ v-for="list in state.lists"
+ v-if="list.type == 'label'">
+ <a
+ href="#"
+ role="button"
+ :class="{ 'is-active': list.id == selected.id }"
+ @click.prevent="modal.selectedList = list">
+ <span
+ class="dropdown-label-box"
+ :style="{ backgroundColor: list.label.color }">
+ </span>
+ {{ list.title }}
+ </a>
+ </li>
+ </ul>
+ </div>
+ </div>
+ `,
+ });
+})();
diff --git a/app/assets/javascripts/boards/components/modal/tabs.js.es6 b/app/assets/javascripts/boards/components/modal/tabs.js.es6
new file mode 100644
index 00000000000..e8cb43f3503
--- /dev/null
+++ b/app/assets/javascripts/boards/components/modal/tabs.js.es6
@@ -0,0 +1,47 @@
+/* global Vue */
+(() => {
+ const ModalStore = gl.issueBoards.ModalStore;
+
+ gl.issueBoards.ModalTabs = Vue.extend({
+ mixins: [gl.issueBoards.ModalMixins],
+ data() {
+ return ModalStore.store;
+ },
+ computed: {
+ selectedCount() {
+ return ModalStore.selectedCount();
+ },
+ },
+ destroyed() {
+ this.activeTab = 'all';
+ },
+ template: `
+ <div class="top-area prepend-top-10 append-bottom-10">
+ <ul class="nav-links issues-state-filters">
+ <li :class="{ 'active': activeTab == 'all' }">
+ <a
+ href="#"
+ role="button"
+ @click.prevent="changeTab('all')">
+ All issues
+ <span class="badge">
+ {{ issuesCount }}
+ </span>
+ </a>
+ </li>
+ <li :class="{ 'active': activeTab == 'selected' }">
+ <a
+ href="#"
+ role="button"
+ @click.prevent="changeTab('selected')">
+ Selected issues
+ <span class="badge">
+ {{ selectedCount }}
+ </span>
+ </a>
+ </li>
+ </ul>
+ </div>
+ `,
+ });
+})();
diff --git a/app/assets/javascripts/boards/components/new_list_dropdown.js.es6 b/app/assets/javascripts/boards/components/new_list_dropdown.js.es6
index 3f5cf8420a8..556826a9148 100644
--- a/app/assets/javascripts/boards/components/new_list_dropdown.js.es6
+++ b/app/assets/javascripts/boards/components/new_list_dropdown.js.es6
@@ -1,4 +1,4 @@
-/* eslint-disable comma-dangle, func-names, no-new, space-before-function-paren, one-var, indent */
+/* eslint-disable comma-dangle, func-names, no-new, space-before-function-paren, one-var */
(() => {
window.gl = window.gl || {};
@@ -32,17 +32,17 @@
});
},
renderRow (label) {
- const active = Store.findList('title', label.title),
- $li = $('<li />'),
- $a = $('<a />', {
- class: (active ? `is-active js-board-list-${active.id}` : ''),
- text: label.title,
- href: '#'
- }),
- $labelColor = $('<span />', {
- class: 'dropdown-label-box',
- style: `background-color: ${label.color}`
- });
+ const active = Store.findList('title', label.title);
+ const $li = $('<li />');
+ const $a = $('<a />', {
+ class: (active ? `is-active js-board-list-${active.id}` : ''),
+ text: label.title,
+ href: '#'
+ });
+ const $labelColor = $('<span />', {
+ class: 'dropdown-label-box',
+ style: `background-color: ${label.color}`
+ });
return $li.append($a.prepend($labelColor));
},
diff --git a/app/assets/javascripts/boards/components/sidebar/remove_issue.js.es6 b/app/assets/javascripts/boards/components/sidebar/remove_issue.js.es6
new file mode 100644
index 00000000000..e74935e1cb0
--- /dev/null
+++ b/app/assets/javascripts/boards/components/sidebar/remove_issue.js.es6
@@ -0,0 +1,59 @@
+/* eslint-disable no-new */
+/* global Vue */
+/* global Flash */
+(() => {
+ const Store = gl.issueBoards.BoardsStore;
+
+ window.gl = window.gl || {};
+ window.gl.issueBoards = window.gl.issueBoards || {};
+
+ gl.issueBoards.RemoveIssueBtn = Vue.extend({
+ props: {
+ issue: {
+ type: Object,
+ required: true,
+ },
+ list: {
+ type: Object,
+ required: true,
+ },
+ },
+ methods: {
+ removeIssue() {
+ const issue = this.issue;
+ const lists = issue.getLists();
+ const labelIds = lists.map(list => list.label.id);
+
+ // Post the remove data
+ gl.boardService.bulkUpdate([issue.globalId], {
+ remove_label_ids: labelIds,
+ }).catch(() => {
+ new Flash('Failed to remove issue from board, please try again.', 'alert');
+
+ lists.forEach((list) => {
+ list.addIssue(issue);
+ });
+ });
+
+ // Remove from the frontend store
+ lists.forEach((list) => {
+ list.removeIssue(issue);
+ });
+
+ Store.detail.issue = {};
+ },
+ },
+ template: `
+ <div
+ class="block list"
+ v-if="list.type !== 'done'">
+ <button
+ class="btn btn-default btn-block"
+ type="button"
+ @click="removeIssue">
+ Remove from board
+ </button>
+ </div>
+ `,
+ });
+})();
diff --git a/app/assets/javascripts/boards/mixins/modal_mixins.js.es6 b/app/assets/javascripts/boards/mixins/modal_mixins.js.es6
new file mode 100644
index 00000000000..d378b7d4baf
--- /dev/null
+++ b/app/assets/javascripts/boards/mixins/modal_mixins.js.es6
@@ -0,0 +1,14 @@
+(() => {
+ const ModalStore = gl.issueBoards.ModalStore;
+
+ gl.issueBoards.ModalMixins = {
+ methods: {
+ toggleModal(toggle) {
+ ModalStore.store.showAddIssuesModal = toggle;
+ },
+ changeTab(tab) {
+ ModalStore.store.activeTab = tab;
+ },
+ },
+ };
+})();
diff --git a/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6 b/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6
index a5e62ed775d..b6c6d17274f 100644
--- a/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6
+++ b/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6
@@ -1,4 +1,4 @@
-/* eslint-disable no-unused-vars, no-mixed-operators, prefer-const, comma-dangle, semi */
+/* eslint-disable no-unused-vars, no-mixed-operators, comma-dangle */
/* global DocumentTouch */
((w) => {
@@ -19,7 +19,8 @@
gl.issueBoards.touchEnabled = ('ontouchstart' in window) || window.DocumentTouch && document instanceof DocumentTouch;
gl.issueBoards.getBoardSortableDefaultOptions = (obj) => {
- let defaultSortOptions = {
+ const defaultSortOptions = {
+ animation: 200,
forceFallback: true,
fallbackClass: 'is-dragging',
fallbackOnBody: true,
@@ -30,7 +31,7 @@
scrollSpeed: 20,
onStart: gl.issueBoards.onStart,
onEnd: gl.issueBoards.onEnd
- }
+ };
Object.keys(obj).forEach((key) => { defaultSortOptions[key] = obj[key]; });
return defaultSortOptions;
diff --git a/app/assets/javascripts/boards/models/issue.js.es6 b/app/assets/javascripts/boards/models/issue.js.es6
index 1199e022ff1..2d0a295ae4d 100644
--- a/app/assets/javascripts/boards/models/issue.js.es6
+++ b/app/assets/javascripts/boards/models/issue.js.es6
@@ -1,4 +1,4 @@
-/* eslint-disable no-unused-vars, space-before-function-paren, arrow-body-style, space-in-parens, arrow-parens, comma-dangle, max-len */
+/* eslint-disable no-unused-vars, space-before-function-paren, arrow-body-style, arrow-parens, comma-dangle, max-len */
/* global Vue */
/* global ListLabel */
/* global ListMilestone */
@@ -6,12 +6,15 @@
class ListIssue {
constructor (obj) {
+ this.globalId = obj.id;
this.id = obj.iid;
this.title = obj.title;
this.confidential = obj.confidential;
this.dueDate = obj.due_date;
this.subscribed = obj.subscribed;
this.labels = [];
+ this.selected = false;
+ this.assignee = false;
if (obj.assignee) {
this.assignee = new ListUser(obj.assignee);
@@ -37,12 +40,12 @@ class ListIssue {
}
findLabel (findLabel) {
- return this.labels.filter( label => label.title === findLabel.title )[0];
+ return this.labels.filter(label => label.title === findLabel.title)[0];
}
removeLabel (removeLabel) {
if (removeLabel) {
- this.labels = this.labels.filter( label => removeLabel.title !== label.title );
+ this.labels = this.labels.filter(label => removeLabel.title !== label.title);
}
}
@@ -51,7 +54,7 @@ class ListIssue {
}
getLists () {
- return gl.issueBoards.BoardsStore.state.lists.filter( list => list.findIssue(this.id) );
+ return gl.issueBoards.BoardsStore.state.lists.filter(list => list.findIssue(this.id));
}
update (url) {
@@ -60,7 +63,7 @@ class ListIssue {
milestone_id: this.milestone ? this.milestone.id : null,
due_date: this.dueDate,
assignee_id: this.assignee ? this.assignee.id : null,
- label_ids: this.labels.map( (label) => label.id )
+ label_ids: this.labels.map((label) => label.id)
}
};
@@ -71,3 +74,5 @@ class ListIssue {
return Vue.http.patch(url, data);
}
}
+
+window.ListIssue = ListIssue;
diff --git a/app/assets/javascripts/boards/models/label.js.es6 b/app/assets/javascripts/boards/models/label.js.es6
index 8f20a1bbec7..9af88d167d6 100644
--- a/app/assets/javascripts/boards/models/label.js.es6
+++ b/app/assets/javascripts/boards/models/label.js.es6
@@ -10,3 +10,5 @@ class ListLabel {
this.priority = (obj.priority !== null) ? obj.priority : Infinity;
}
}
+
+window.ListLabel = ListLabel;
diff --git a/app/assets/javascripts/boards/models/list.js.es6 b/app/assets/javascripts/boards/models/list.js.es6
index a8d38c16485..5152be56b66 100644
--- a/app/assets/javascripts/boards/models/list.js.es6
+++ b/app/assets/javascripts/boards/models/list.js.es6
@@ -1,4 +1,4 @@
-/* eslint-disable space-before-function-paren, no-underscore-dangle, class-methods-use-this, consistent-return, no-plusplus, prefer-const, space-in-parens, no-shadow, no-param-reassign, max-len, no-unused-vars */
+/* eslint-disable space-before-function-paren, no-underscore-dangle, class-methods-use-this, consistent-return, no-shadow, no-param-reassign, max-len, no-unused-vars */
/* global ListIssue */
/* global ListLabel */
@@ -9,7 +9,7 @@ class List {
this.position = obj.position;
this.title = obj.title;
this.type = obj.list_type;
- this.preset = ['backlog', 'done', 'blank'].indexOf(this.type) > -1;
+ this.preset = ['done', 'blank'].indexOf(this.type) > -1;
this.filters = gl.issueBoards.BoardsStore.state.filters;
this.page = 1;
this.loading = true;
@@ -58,7 +58,7 @@ class List {
nextPage () {
if (this.issuesSize > this.issues.length) {
- this.page++;
+ this.page += 1;
return this.getIssues(false);
}
@@ -66,12 +66,12 @@ class List {
getIssues (emptyIssues = true) {
const filters = this.filters;
- let data = { page: this.page };
+ const data = { page: this.page };
Object.keys(filters).forEach((key) => { data[key] = filters[key]; });
if (this.label) {
- data.label_name = data.label_name.filter( label => label !== this.label.title );
+ data.label_name = data.label_name.filter(label => label !== this.label.title);
}
if (emptyIssues) {
@@ -94,7 +94,7 @@ class List {
newIssue (issue) {
this.addIssue(issue);
- this.issuesSize++;
+ this.issuesSize += 1;
return gl.boardService.newIssue(this.id, issue)
.then((resp) => {
@@ -122,7 +122,7 @@ class List {
}
if (listFrom) {
- this.issuesSize++;
+ this.issuesSize += 1;
gl.boardService.moveIssue(issue.id, listFrom.id, this.id)
.then(() => {
listFrom.getIssues(false);
@@ -132,7 +132,7 @@ class List {
}
findIssue (id) {
- return this.issues.filter( issue => issue.id === id )[0];
+ return this.issues.filter(issue => issue.id === id)[0];
}
removeIssue (removeIssue) {
@@ -140,7 +140,7 @@ class List {
const matchesRemove = removeIssue.id === issue.id;
if (matchesRemove) {
- this.issuesSize--;
+ this.issuesSize -= 1;
issue.removeLabel(this.label);
}
@@ -148,3 +148,5 @@ class List {
});
}
}
+
+window.List = List;
diff --git a/app/assets/javascripts/boards/models/milestone.js.es6 b/app/assets/javascripts/boards/models/milestone.js.es6
index 9c173c1b70b..c867b06d320 100644
--- a/app/assets/javascripts/boards/models/milestone.js.es6
+++ b/app/assets/javascripts/boards/models/milestone.js.es6
@@ -6,3 +6,5 @@ class ListMilestone {
this.title = obj.title;
}
}
+
+window.ListMilestone = ListMilestone;
diff --git a/app/assets/javascripts/boards/models/user.js.es6 b/app/assets/javascripts/boards/models/user.js.es6
index a8a3892e2ad..8e9de4d4cbb 100644
--- a/app/assets/javascripts/boards/models/user.js.es6
+++ b/app/assets/javascripts/boards/models/user.js.es6
@@ -8,3 +8,5 @@ class ListUser {
this.avatar = user.avatar_url;
}
}
+
+window.ListUser = ListUser;
diff --git a/app/assets/javascripts/boards/services/board_service.js.es6 b/app/assets/javascripts/boards/services/board_service.js.es6
index 189a8703198..065e90518df 100644
--- a/app/assets/javascripts/boards/services/board_service.js.es6
+++ b/app/assets/javascripts/boards/services/board_service.js.es6
@@ -1,8 +1,14 @@
-/* eslint-disable space-before-function-paren, comma-dangle, no-param-reassign, camelcase, prefer-const, no-extra-semi, max-len, no-unused-vars */
+/* eslint-disable space-before-function-paren, comma-dangle, no-param-reassign, camelcase, max-len, no-unused-vars */
/* global Vue */
class BoardService {
- constructor (root, boardId) {
+ constructor (root, bulkUpdatePath, boardId) {
+ this.boards = Vue.resource(`${root}{/id}.json`, {}, {
+ issues: {
+ method: 'GET',
+ url: `${root}/${boardId}/issues.json`
+ }
+ });
this.lists = Vue.resource(`${root}/${boardId}/lists{/id}`, {}, {
generate: {
method: 'POST',
@@ -10,7 +16,12 @@ class BoardService {
}
});
this.issue = Vue.resource(`${root}/${boardId}/issues{/id}`, {});
- this.issues = Vue.resource(`${root}/${boardId}/lists{/id}/issues`, {});
+ this.issues = Vue.resource(`${root}/${boardId}/lists{/id}/issues`, {}, {
+ bulkUpdate: {
+ method: 'POST',
+ url: bulkUpdatePath,
+ },
+ });
Vue.http.interceptors.push((request, next) => {
request.headers['X-CSRF-Token'] = $.rails.csrfToken();
@@ -47,7 +58,7 @@ class BoardService {
}
getIssuesForList (id, filter = {}) {
- let data = { id };
+ const data = { id };
Object.keys(filter).forEach((key) => { data[key] = filter[key]; });
return this.issues.get(data);
@@ -65,4 +76,20 @@ class BoardService {
issue
});
}
-};
+
+ getBacklog(data) {
+ return this.boards.issues(data);
+ }
+
+ bulkUpdate(issueIds, extraData = {}) {
+ const data = {
+ update: Object.assign(extraData, {
+ issuable_ids: issueIds.join(','),
+ }),
+ };
+
+ return this.issues.bulkUpdate(data);
+ }
+}
+
+window.BoardService = BoardService;
diff --git a/app/assets/javascripts/boards/stores/boards_store.js.es6 b/app/assets/javascripts/boards/stores/boards_store.js.es6
index e7a14ea5bca..50842ecbaaa 100644
--- a/app/assets/javascripts/boards/stores/boards_store.js.es6
+++ b/app/assets/javascripts/boards/stores/boards_store.js.es6
@@ -1,4 +1,4 @@
-/* eslint-disable comma-dangle, space-before-function-paren, one-var, indent, space-in-parens, no-shadow, radix, dot-notation, semi, max-len */
+/* eslint-disable comma-dangle, space-before-function-paren, one-var, no-shadow, dot-notation, max-len */
/* global Cookies */
/* global List */
@@ -33,16 +33,11 @@
return list;
},
new (listObj) {
- const list = this.addList(listObj),
- backlogList = this.findList('type', 'backlog', 'backlog');
+ const list = this.addList(listObj);
list
.save()
.then(() => {
- // Remove any new issues from the backlog
- // as they will be visible in the new list
- list.issues.forEach(backlogList.removeIssue.bind(backlogList));
-
this.state.lists = _.sortBy(this.state.lists, 'position');
});
this.removeBlankState();
@@ -52,7 +47,7 @@
},
shouldAddBlankState () {
// Decide whether to add the blank state
- return !(this.state.lists.filter( list => list.type !== 'backlog' && list.type !== 'done' )[0]);
+ return !(this.state.lists.filter(list => list.type !== 'done')[0]);
},
addBlankState () {
if (!this.shouldAddBlankState() || this.welcomeIsHidden() || this.disabled) return;
@@ -82,30 +77,30 @@
if (!list) return;
- this.state.lists = this.state.lists.filter( list => list.id !== id );
+ this.state.lists = this.state.lists.filter(list => list.id !== id);
},
moveList (listFrom, orderLists) {
orderLists.forEach((id, i) => {
- const list = this.findList('id', parseInt(id));
+ const list = this.findList('id', parseInt(id, 10));
list.position = i;
});
listFrom.update();
},
moveIssueToList (listFrom, listTo, issue, newIndex) {
- const issueTo = listTo.findIssue(issue.id),
- issueLists = issue.getLists(),
- listLabels = issueLists.map( listIssue => listIssue.label );
+ const issueTo = listTo.findIssue(issue.id);
+ const issueLists = issue.getLists();
+ const listLabels = issueLists.map(listIssue => listIssue.label);
// Add to new lists issues if it doesn't already exist
if (!issueTo) {
listTo.addIssue(issue, listFrom, newIndex);
}
- if (listTo.type === 'done' && listFrom.type !== 'backlog') {
+ if (listTo.type === 'done') {
issueLists.forEach((list) => {
list.removeIssue(issue);
- })
+ });
issue.removeLabels(listLabels);
} else {
listFrom.removeIssue(issue);
diff --git a/app/assets/javascripts/boards/stores/modal_store.js.es6 b/app/assets/javascripts/boards/stores/modal_store.js.es6
new file mode 100644
index 00000000000..15fc6c79e8d
--- /dev/null
+++ b/app/assets/javascripts/boards/stores/modal_store.js.es6
@@ -0,0 +1,107 @@
+(() => {
+ window.gl = window.gl || {};
+ window.gl.issueBoards = window.gl.issueBoards || {};
+
+ class ModalStore {
+ constructor() {
+ this.store = {
+ columns: 3,
+ issues: [],
+ issuesCount: false,
+ selectedIssues: [],
+ showAddIssuesModal: false,
+ activeTab: 'all',
+ selectedList: null,
+ searchTerm: '',
+ loading: false,
+ loadingNewPage: false,
+ page: 1,
+ perPage: 50,
+ };
+
+ this.setDefaultFilter();
+ }
+
+ setDefaultFilter() {
+ this.store.filter = {
+ author_id: '',
+ assignee_id: '',
+ milestone_title: '',
+ label_name: [],
+ };
+ }
+
+ selectedCount() {
+ return this.getSelectedIssues().length;
+ }
+
+ toggleIssue(issueObj) {
+ const issue = issueObj;
+ const selected = issue.selected;
+
+ issue.selected = !selected;
+
+ if (!selected) {
+ this.addSelectedIssue(issue);
+ } else {
+ this.removeSelectedIssue(issue);
+ }
+ }
+
+ toggleAll() {
+ const select = this.selectedCount() !== this.store.issues.length;
+
+ this.store.issues.forEach((issue) => {
+ const issueUpdate = issue;
+
+ if (issueUpdate.selected !== select) {
+ issueUpdate.selected = select;
+
+ if (select) {
+ this.addSelectedIssue(issue);
+ } else {
+ this.removeSelectedIssue(issue);
+ }
+ }
+ });
+ }
+
+ getSelectedIssues() {
+ return this.store.selectedIssues.filter(issue => issue.selected);
+ }
+
+ addSelectedIssue(issue) {
+ const index = this.selectedIssueIndex(issue);
+
+ if (index === -1) {
+ this.store.selectedIssues.push(issue);
+ }
+ }
+
+ removeSelectedIssue(issue, forcePurge = false) {
+ if (this.store.activeTab === 'all' || forcePurge) {
+ this.store.selectedIssues = this.store.selectedIssues
+ .filter(fIssue => fIssue.id !== issue.id);
+ }
+ }
+
+ purgeUnselectedIssues() {
+ this.store.selectedIssues.forEach((issue) => {
+ if (!issue.selected) {
+ this.removeSelectedIssue(issue, true);
+ }
+ });
+ }
+
+ selectedIssueIndex(issue) {
+ return this.store.selectedIssues.indexOf(issue);
+ }
+
+ findSelectedIssue(issue) {
+ return this.store.selectedIssues
+ .filter(filteredIssue => filteredIssue.id === issue.id)[0];
+ }
+ }
+
+ gl.issueBoards.ModalStore = new ModalStore();
+})();
diff --git a/app/assets/javascripts/boards/test_utils/simulate_drag.js b/app/assets/javascripts/boards/test_utils/simulate_drag.js
index 01e09ec482e..f05780167bf 100644
--- a/app/assets/javascripts/boards/test_utils/simulate_drag.js
+++ b/app/assets/javascripts/boards/test_utils/simulate_drag.js
@@ -1,120 +1,119 @@
-/* eslint-disable wrap-iife, func-names, strict, indent, no-tabs, no-var, vars-on-top, no-param-reassign, object-shorthand, no-shadow, comma-dangle, prefer-template, consistent-return, no-mixed-operators, no-unused-vars, object-curly-spacing, no-unused-expressions, prefer-arrow-callback, max-len */
+/* eslint-disable wrap-iife, func-names, strict, no-var, vars-on-top, no-param-reassign, object-shorthand, no-shadow, comma-dangle, prefer-template, consistent-return, no-mixed-operators, no-unused-vars, no-unused-expressions, prefer-arrow-callback, max-len */
(function () {
- 'use strict';
-
- function simulateEvent(el, type, options) {
- var event;
- if (!el) return;
- var ownerDocument = el.ownerDocument;
-
- options = options || {};
-
- if (/^mouse/.test(type)) {
- event = ownerDocument.createEvent('MouseEvents');
- event.initMouseEvent(type, true, true, ownerDocument.defaultView,
- options.button, options.screenX, options.screenY, options.clientX, options.clientY,
- options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, options.button, el);
- } else {
- event = ownerDocument.createEvent('CustomEvent');
-
- event.initCustomEvent(type, true, true, ownerDocument.defaultView,
- options.button, options.screenX, options.screenY, options.clientX, options.clientY,
- options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, options.button, el);
-
- event.dataTransfer = {
- data: {},
-
- setData: function (type, val) {
- this.data[type] = val;
- },
-
- getData: function (type) {
- return this.data[type];
- }
- };
- }
-
- if (el.dispatchEvent) {
- el.dispatchEvent(event);
- } else if (el.fireEvent) {
- el.fireEvent('on' + type, event);
- }
-
- return event;
- }
-
- function getTraget(target) {
- var el = typeof target.el === 'string' ? document.getElementById(target.el.substr(1)) : target.el;
- var children = el.children;
-
- return (
- children[target.index] ||
- children[target.index === 'first' ? 0 : -1] ||
- children[target.index === 'last' ? children.length - 1 : -1]
- );
- }
-
- function getRect(el) {
- var rect = el.getBoundingClientRect();
- var width = rect.right - rect.left;
- var height = rect.bottom - rect.top;
-
- return {
- x: rect.left,
- y: rect.top,
- cx: rect.left + width / 2,
- cy: rect.top + height / 2,
- w: width,
- h: height,
- hw: width / 2,
- wh: height / 2
- };
- }
-
- function simulateDrag(options, callback) {
- options.to.el = options.to.el || options.from.el;
-
- var fromEl = getTraget(options.from);
- var toEl = getTraget(options.to);
+ 'use strict';
+
+ function simulateEvent(el, type, options) {
+ var event;
+ if (!el) return;
+ var ownerDocument = el.ownerDocument;
+
+ options = options || {};
+
+ if (/^mouse/.test(type)) {
+ event = ownerDocument.createEvent('MouseEvents');
+ event.initMouseEvent(type, true, true, ownerDocument.defaultView,
+ options.button, options.screenX, options.screenY, options.clientX, options.clientY,
+ options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, options.button, el);
+ } else {
+ event = ownerDocument.createEvent('CustomEvent');
+
+ event.initCustomEvent(type, true, true, ownerDocument.defaultView,
+ options.button, options.screenX, options.screenY, options.clientX, options.clientY,
+ options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, options.button, el);
+
+ event.dataTransfer = {
+ data: {},
+
+ setData: function (type, val) {
+ this.data[type] = val;
+ },
+
+ getData: function (type) {
+ return this.data[type];
+ }
+ };
+ }
+
+ if (el.dispatchEvent) {
+ el.dispatchEvent(event);
+ } else if (el.fireEvent) {
+ el.fireEvent('on' + type, event);
+ }
+
+ return event;
+ }
+
+ function getTraget(target) {
+ var el = typeof target.el === 'string' ? document.getElementById(target.el.substr(1)) : target.el;
+ var children = el.children;
+
+ return (
+ children[target.index] ||
+ children[target.index === 'first' ? 0 : -1] ||
+ children[target.index === 'last' ? children.length - 1 : -1]
+ );
+ }
+
+ function getRect(el) {
+ var rect = el.getBoundingClientRect();
+ var width = rect.right - rect.left;
+ var height = rect.bottom - rect.top;
+
+ return {
+ x: rect.left,
+ y: rect.top,
+ cx: rect.left + width / 2,
+ cy: rect.top + height / 2,
+ w: width,
+ h: height,
+ hw: width / 2,
+ wh: height / 2
+ };
+ }
+
+ function simulateDrag(options, callback) {
+ options.to.el = options.to.el || options.from.el;
+
+ var fromEl = getTraget(options.from);
+ var toEl = getTraget(options.to);
var scrollable = options.scrollable;
- var fromRect = getRect(fromEl);
- var toRect = getRect(toEl);
-
- var startTime = new Date().getTime();
- var duration = options.duration || 1000;
- simulateEvent(fromEl, 'mousedown', {button: 0});
- options.ontap && options.ontap();
- window.SIMULATE_DRAG_ACTIVE = 1;
-
- var dragInterval = setInterval(function loop() {
- var progress = (new Date().getTime() - startTime) / duration;
- var x = (fromRect.cx + (toRect.cx - fromRect.cx) * progress) - scrollable.scrollLeft;
- var y = (fromRect.cy + (toRect.cy - fromRect.cy) * progress) - scrollable.scrollTop;
- var overEl = fromEl.ownerDocument.elementFromPoint(x, y);
-
- simulateEvent(overEl, 'mousemove', {
- clientX: x,
- clientY: y
- });
-
- if (progress >= 1) {
- options.ondragend && options.ondragend();
- simulateEvent(toEl, 'mouseup');
- clearInterval(dragInterval);
- window.SIMULATE_DRAG_ACTIVE = 0;
- }
- }, 100);
-
- return {
- target: fromEl,
- fromList: fromEl.parentNode,
- toList: toEl.parentNode
- };
- }
-
-
- // Export
- window.simulateEvent = simulateEvent;
- window.simulateDrag = simulateDrag;
+ var fromRect = getRect(fromEl);
+ var toRect = getRect(toEl);
+
+ var startTime = new Date().getTime();
+ var duration = options.duration || 1000;
+ simulateEvent(fromEl, 'mousedown', { button: 0 });
+ options.ontap && options.ontap();
+ window.SIMULATE_DRAG_ACTIVE = 1;
+
+ var dragInterval = setInterval(function loop() {
+ var progress = (new Date().getTime() - startTime) / duration;
+ var x = (fromRect.cx + (toRect.cx - fromRect.cx) * progress) - scrollable.scrollLeft;
+ var y = (fromRect.cy + (toRect.cy - fromRect.cy) * progress) - scrollable.scrollTop;
+ var overEl = fromEl.ownerDocument.elementFromPoint(x, y);
+
+ simulateEvent(overEl, 'mousemove', {
+ clientX: x,
+ clientY: y
+ });
+
+ if (progress >= 1) {
+ options.ondragend && options.ondragend();
+ simulateEvent(toEl, 'mouseup');
+ clearInterval(dragInterval);
+ window.SIMULATE_DRAG_ACTIVE = 0;
+ }
+ }, 100);
+
+ return {
+ target: fromEl,
+ fromList: fromEl.parentNode,
+ toList: toEl.parentNode
+ };
+ }
+
+ // Export
+ window.simulateEvent = simulateEvent;
+ window.simulateDrag = simulateDrag;
})();
diff --git a/app/assets/javascripts/boards/vue_resource_interceptor.js.es6 b/app/assets/javascripts/boards/vue_resource_interceptor.js.es6
deleted file mode 100644
index 3723a2039f9..00000000000
--- a/app/assets/javascripts/boards/vue_resource_interceptor.js.es6
+++ /dev/null
@@ -1,10 +0,0 @@
-/* eslint-disable func-names, prefer-arrow-callback, no-unused-vars, no-plusplus */
-/* global Vue */
-
-Vue.http.interceptors.push((request, next) => {
- Vue.activeResources = Vue.activeResources ? Vue.activeResources + 1 : 1;
-
- next(function (response) {
- Vue.activeResources--;
- });
-});
diff --git a/app/assets/javascripts/breakpoints.js b/app/assets/javascripts/breakpoints.js
index a7e72430141..f8dac1ff56e 100644
--- a/app/assets/javascripts/breakpoints.js
+++ b/app/assets/javascripts/breakpoints.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, quotes, no-shadow, prefer-arrow-callback, prefer-template, consistent-return, padded-blocks, no-return-assign, new-parens, no-param-reassign, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, quotes, no-shadow, prefer-arrow-callback, prefer-template, consistent-return, no-return-assign, new-parens, no-param-reassign, max-len */
(function() {
var Breakpoints = (function() {
@@ -43,6 +43,7 @@
BreakpointInstance.prototype.getBreakpointSize = function() {
var $visibleDevice;
$visibleDevice = this.visibleDevice;
+ // TODO: Consider refactoring in light of turbolinks removal.
// the page refreshed via turbolinks
if (!$visibleDevice().length) {
this.setup();
@@ -52,7 +53,6 @@
};
return BreakpointInstance;
-
})();
Breakpoints.get = function() {
@@ -60,7 +60,6 @@
};
return Breakpoints;
-
})();
$((function(_this) {
diff --git a/app/assets/javascripts/broadcast_message.js b/app/assets/javascripts/broadcast_message.js
index 30432dae278..dbdadc73c3f 100644
--- a/app/assets/javascripts/broadcast_message.js
+++ b/app/assets/javascripts/broadcast_message.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, quotes, no-else-return, object-shorthand, comma-dangle, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, quotes, no-else-return, object-shorthand, comma-dangle, max-len */
(function() {
$(function() {
var previewPath;
@@ -31,5 +31,4 @@
}
});
});
-
}).call(this);
diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js
index 824febe3fd3..0152be88b48 100644
--- a/app/assets/javascripts/build.js
+++ b/app/assets/javascripts/build.js
@@ -1,9 +1,10 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-use-before-define, no-param-reassign, quotes, yoda, no-else-return, consistent-return, comma-dangle, semi, object-shorthand, prefer-template, one-var, one-var-declaration-per-line, no-unused-vars, max-len, vars-on-top, padded-blocks */
+/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-use-before-define, no-param-reassign, quotes, yoda, no-else-return, consistent-return, comma-dangle, object-shorthand, prefer-template, one-var, one-var-declaration-per-line, no-unused-vars, max-len, vars-on-top */
/* global Breakpoints */
-/* global Turbolinks */
(function() {
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+ var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
+ var AUTO_SCROLL_OFFSET = 75;
+ var DOWN_BUILD_TRACE = '#down-build-trace';
this.Build = (function() {
Build.interval = null;
@@ -19,6 +20,17 @@
this.buildStage = options.buildStage;
this.updateDropdown = bind(this.updateDropdown, this);
this.$document = $(document);
+ this.$body = $('body');
+ this.$buildTrace = $('#build-trace');
+ this.$autoScrollContainer = $('.autoscroll-container');
+ this.$autoScrollStatus = $('#autoscroll-status');
+ this.$autoScrollStatusText = this.$autoScrollStatus.find('.status-text');
+ this.$upBuildTrace = $('#up-build-trace');
+ this.$downBuildTrace = $(DOWN_BUILD_TRACE);
+ this.$scrollTopBtn = $('#scroll-top');
+ this.$scrollBottomBtn = $('#scroll-bottom');
+ this.$buildRefreshAnimation = $('.js-build-refresh');
+
clearInterval(Build.interval);
// Init breakpoint checker
this.bp = Breakpoints.get();
@@ -32,6 +44,7 @@
this.$document.off('click', '.js-sidebar-build-toggle').on('click', '.js-sidebar-build-toggle', this.sidebarOnClick.bind(this));
this.$document.off('click', '.stage-item').on('click', '.stage-item', this.updateDropdown);
+ this.$document.on('scroll', this.initScrollMonitor.bind(this));
$(window).off('resize.build').on('resize.build', this.sidebarOnResize.bind(this));
$('a', this.$buildScroll).off('click.stepTrace').on('click.stepTrace', this.stepTrace);
this.updateArtifactRemoveDate();
@@ -40,18 +53,6 @@
this.initScrollButtonAffix();
}
if (this.buildStatus === "running" || this.buildStatus === "pending") {
- // Bind autoscroll button to follow build output
- $('#autoscroll-button').on('click', function() {
- var state;
- state = $(this).data("state");
- if ("enabled" === state) {
- $(this).data("state", "disabled");
- return $(this).text("Enable autoscroll");
- } else {
- $(this).data("state", "enabled");
- return $(this).text("Disable autoscroll");
- }
- });
Build.interval = setInterval((function(_this) {
// Check for new build output if user still watching build page
// Only valid for runnig build when output changes during time
@@ -68,7 +69,7 @@
this.$sidebar = $('.js-build-sidebar');
this.sidebarTranslationLimits = {
min: $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight()
- }
+ };
this.sidebarTranslationLimits.max = this.sidebarTranslationLimits.min + $('.scrolling-tabs-container').outerHeight();
this.$sidebar.css({
top: this.sidebarTranslationLimits.max
@@ -83,17 +84,21 @@
};
Build.prototype.getInitialBuildTrace = function() {
- var removeRefreshStatuses = ['success', 'failed', 'canceled', 'skipped']
+ var removeRefreshStatuses = ['success', 'failed', 'canceled', 'skipped'];
return $.ajax({
url: this.buildUrl,
dataType: 'json',
success: function(buildData) {
$('.js-build-output').html(buildData.trace_html);
+ if (window.location.hash === DOWN_BUILD_TRACE) {
+ $("html,body").scrollTop(this.$buildTrace.height());
+ }
if (removeRefreshStatuses.indexOf(buildData.status) >= 0) {
- return $('.js-build-refresh').remove();
+ this.$buildRefreshAnimation.remove();
+ return this.initScrollMonitor();
}
- }
+ }.bind(this)
});
};
@@ -103,6 +108,8 @@
dataType: "json",
success: (function(_this) {
return function(log) {
+ var pageUrl;
+
if (log.state) {
_this.state = log.state;
}
@@ -114,7 +121,12 @@
}
return _this.checkAutoscroll();
} else if (log.status !== _this.buildStatus) {
- return Turbolinks.visit(_this.pageUrl);
+ pageUrl = _this.pageUrl;
+ if (_this.$autoScrollStatus.data('state') === 'enabled') {
+ pageUrl += DOWN_BUILD_TRACE;
+ }
+
+ return gl.utils.visitUrl(pageUrl);
}
};
})(this)
@@ -122,22 +134,95 @@
};
Build.prototype.checkAutoscroll = function() {
- if ("enabled" === $("#autoscroll-button").data("state")) {
- return $("html,body").scrollTop($("#build-trace").height());
+ if (this.$autoScrollStatus.data("state") === "enabled") {
+ return $("html,body").scrollTop(this.$buildTrace.height());
+ }
+
+ // Handle a situation where user started new build
+ // but never scrolled a page
+ if (!this.$scrollTopBtn.is(':visible') &&
+ !this.$scrollBottomBtn.is(':visible') &&
+ !gl.utils.isInViewport(this.$downBuildTrace.get(0))) {
+ this.$scrollBottomBtn.show();
}
};
Build.prototype.initScrollButtonAffix = function() {
- var $body, $buildTrace;
- $body = $('body');
- $buildTrace = $('#build-trace');
- return this.$buildScroll.affix({
- offset: {
- bottom: function() {
- return $body.outerHeight() - ($buildTrace.outerHeight() + $buildTrace.offset().top);
- }
+ // Hide everything initially
+ this.$scrollTopBtn.hide();
+ this.$scrollBottomBtn.hide();
+ this.$autoScrollContainer.hide();
+ };
+
+ // Page scroll listener to detect if user has scrolling page
+ // and handle following cases
+ // 1) User is at Top of Build Log;
+ // - Hide Top Arrow button
+ // - Show Bottom Arrow button
+ // - Disable Autoscroll and hide indicator (when build is running)
+ // 2) User is at Bottom of Build Log;
+ // - Show Top Arrow button
+ // - Hide Bottom Arrow button
+ // - Enable Autoscroll and show indicator (when build is running)
+ // 3) User is somewhere in middle of Build Log;
+ // - Show Top Arrow button
+ // - Show Bottom Arrow button
+ // - Disable Autoscroll and hide indicator (when build is running)
+ Build.prototype.initScrollMonitor = function() {
+ if (!gl.utils.isInViewport(this.$upBuildTrace.get(0)) && !gl.utils.isInViewport(this.$downBuildTrace.get(0))) {
+ // User is somewhere in middle of Build Log
+
+ this.$scrollTopBtn.show();
+
+ if (this.buildStatus === 'success' || this.buildStatus === 'failed') { // Check if Build is completed
+ this.$scrollBottomBtn.show();
+ } else if (this.$buildRefreshAnimation.is(':visible') && !gl.utils.isInViewport(this.$buildRefreshAnimation.get(0))) {
+ this.$scrollBottomBtn.show();
+ } else {
+ this.$scrollBottomBtn.hide();
}
- });
+
+ // Hide Autoscroll Status Indicator
+ if (this.$scrollBottomBtn.is(':visible')) {
+ this.$autoScrollContainer.hide();
+ this.$autoScrollStatusText.removeClass('animate');
+ } else {
+ this.$autoScrollContainer.css({ top: this.$body.outerHeight() - AUTO_SCROLL_OFFSET }).show();
+ this.$autoScrollStatusText.addClass('animate');
+ }
+ } else if (gl.utils.isInViewport(this.$upBuildTrace.get(0)) && !gl.utils.isInViewport(this.$downBuildTrace.get(0))) {
+ // User is at Top of Build Log
+
+ this.$scrollTopBtn.hide();
+ this.$scrollBottomBtn.show();
+
+ this.$autoScrollContainer.hide();
+ this.$autoScrollStatusText.removeClass('animate');
+ } else if ((!gl.utils.isInViewport(this.$upBuildTrace.get(0)) && gl.utils.isInViewport(this.$downBuildTrace.get(0))) ||
+ (this.$buildRefreshAnimation.is(':visible') && gl.utils.isInViewport(this.$buildRefreshAnimation.get(0)))) {
+ // User is at Bottom of Build Log
+
+ this.$scrollTopBtn.show();
+ this.$scrollBottomBtn.hide();
+
+ // Show and Reposition Autoscroll Status Indicator
+ this.$autoScrollContainer.css({ top: this.$body.outerHeight() - AUTO_SCROLL_OFFSET }).show();
+ this.$autoScrollStatusText.addClass('animate');
+ } else if (gl.utils.isInViewport(this.$upBuildTrace.get(0)) && gl.utils.isInViewport(this.$downBuildTrace.get(0))) {
+ // Build Log height is small
+
+ this.$scrollTopBtn.hide();
+ this.$scrollBottomBtn.hide();
+
+ // Hide Autoscroll Status Indicator
+ this.$autoScrollContainer.hide();
+ this.$autoScrollStatusText.removeClass('animate');
+ }
+
+ if (this.buildStatus === "running" || this.buildStatus === "pending") {
+ // Check if Refresh Animation is in Viewport and enable Autoscroll, disable otherwise.
+ this.$autoScrollStatus.data("state", gl.utils.isInViewport(this.$buildRefreshAnimation.get(0)) ? 'enabled' : 'disabled');
+ }
};
Build.prototype.shouldHideSidebarForViewport = function() {
@@ -205,7 +290,5 @@
};
return Build;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/build_artifacts.js b/app/assets/javascripts/build_artifacts.js
index c423a548a30..083448552b6 100644
--- a/app/assets/javascripts/build_artifacts.js
+++ b/app/assets/javascripts/build_artifacts.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, no-unused-vars, no-return-assign, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, no-unused-vars, no-return-assign, max-len */
(function() {
this.BuildArtifacts = (function() {
function BuildArtifacts() {
@@ -22,7 +22,5 @@
};
return BuildArtifacts;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/build_variables.js.es6 b/app/assets/javascripts/build_variables.js.es6
index 993424d422f..99082b412e2 100644
--- a/app/assets/javascripts/build_variables.js.es6
+++ b/app/assets/javascripts/build_variables.js.es6
@@ -1,7 +1,7 @@
-/* eslint-disable func-names, prefer-arrow-callback, space-before-blocks, space-before-function-paren, comma-spacing, max-len */
+/* eslint-disable func-names, prefer-arrow-callback, space-before-function-paren */
-$(function(){
- $('.reveal-variables').off('click').on('click',function(){
+$(function() {
+ $('.reveal-variables').off('click').on('click', function() {
$('.js-build').toggle().niceScroll();
$(this).hide();
});
diff --git a/app/assets/javascripts/ci_lint_editor.js.es6 b/app/assets/javascripts/ci_lint_editor.js.es6
new file mode 100644
index 00000000000..56ffaa765a8
--- /dev/null
+++ b/app/assets/javascripts/ci_lint_editor.js.es6
@@ -0,0 +1,18 @@
+(() => {
+ window.gl = window.gl || {};
+
+ class CILintEditor {
+ constructor() {
+ this.editor = window.ace.edit('ci-editor');
+ this.textarea = document.querySelector('#content');
+
+ this.editor.getSession().setMode('ace/mode/yaml');
+ this.editor.on('input', () => {
+ const content = this.editor.getSession().getValue();
+ this.textarea.value = content;
+ });
+ }
+ }
+
+ gl.CILintEditor = CILintEditor;
+})();
diff --git a/app/assets/javascripts/commit.js b/app/assets/javascripts/commit.js
index 67b33a4d7ee..c656ae4e241 100644
--- a/app/assets/javascripts/commit.js
+++ b/app/assets/javascripts/commit.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, padded-blocks */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife */
/* global CommitFile */
(function() {
@@ -10,7 +10,5 @@
}
return Commit;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/commit/file.js b/app/assets/javascripts/commit/file.js
index 27512312c7c..184b4561d2e 100644
--- a/app/assets/javascripts/commit/file.js
+++ b/app/assets/javascripts/commit/file.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-new, padded-blocks */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-new */
/* global ImageFile */
(function() {
@@ -10,7 +10,5 @@
}
return CommitFile;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/commit/image_file.js b/app/assets/javascripts/commit/image_file.js
index fd8910e916f..f09a6b1e676 100644
--- a/app/assets/javascripts/commit/image_file.js
+++ b/app/assets/javascripts/commit/image_file.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-use-before-define, prefer-arrow-callback, no-else-return, consistent-return, prefer-template, quotes, one-var, one-var-declaration-per-line, no-unused-vars, no-return-assign, comma-dangle, quote-props, no-unused-expressions, no-sequences, object-shorthand, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-use-before-define, prefer-arrow-callback, no-else-return, consistent-return, prefer-template, quotes, one-var, one-var-declaration-per-line, no-unused-vars, no-return-assign, comma-dangle, quote-props, no-unused-expressions, no-sequences, object-shorthand, max-len */
(function() {
gl.ImageFile = (function() {
var prepareFrames;
@@ -172,7 +172,5 @@
};
return ImageFile;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js.es6 b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js.es6
new file mode 100644
index 00000000000..fbfec7743c7
--- /dev/null
+++ b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js.es6
@@ -0,0 +1,26 @@
+/* eslint-disable no-new, no-param-reassign */
+/* global Vue, CommitsPipelineStore, PipelinesService, Flash */
+
+window.Vue = require('vue');
+require('./pipelines_table');
+/**
+ * Commits View > Pipelines Tab > Pipelines Table.
+ * Merge Request View > Pipelines Tab > Pipelines Table.
+ *
+ * Renders Pipelines table in pipelines tab in the commits show view.
+ * Renders Pipelines table in pipelines tab in the merge request show view.
+ */
+
+$(() => {
+ window.gl = window.gl || {};
+ gl.commits = gl.commits || {};
+ gl.commits.pipelines = gl.commits.pipelines || {};
+
+ if (gl.commits.PipelinesTableBundle) {
+ gl.commits.PipelinesTableBundle.$destroy(true);
+ }
+
+ gl.commits.pipelines.PipelinesTableBundle = new gl.commits.pipelines.PipelinesTableView({
+ el: document.querySelector('#commit-pipeline-table-view'),
+ });
+});
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_service.js.es6 b/app/assets/javascripts/commit/pipelines/pipelines_service.js.es6
new file mode 100644
index 00000000000..483b414126a
--- /dev/null
+++ b/app/assets/javascripts/commit/pipelines/pipelines_service.js.es6
@@ -0,0 +1,29 @@
+/* globals Vue */
+/* eslint-disable no-unused-vars, no-param-reassign */
+
+/**
+ * Pipelines service.
+ *
+ * Used to fetch the data used to render the pipelines table.
+ * Uses Vue.Resource
+ */
+class PipelinesService {
+ constructor(endpoint) {
+ this.pipelines = Vue.resource(endpoint);
+ }
+
+ /**
+ * Given the root param provided when the class is initialized, will
+ * make a GET request.
+ *
+ * @return {Promise}
+ */
+ all() {
+ return this.pipelines.get();
+ }
+}
+
+window.gl = window.gl || {};
+gl.commits = gl.commits || {};
+gl.commits.pipelines = gl.commits.pipelines || {};
+gl.commits.pipelines.PipelinesService = PipelinesService;
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_store.js.es6 b/app/assets/javascripts/commit/pipelines/pipelines_store.js.es6
new file mode 100644
index 00000000000..11a3449d99a
--- /dev/null
+++ b/app/assets/javascripts/commit/pipelines/pipelines_store.js.es6
@@ -0,0 +1,50 @@
+/* eslint-disable no-underscore-dangle*/
+/**
+ * Pipelines' Store for commits view.
+ *
+ * Used to store the Pipelines rendered in the commit view in the pipelines table.
+ */
+
+class PipelinesStore {
+ constructor() {
+ this.state = {};
+ this.state.pipelines = [];
+ }
+
+ storePipelines(pipelines = []) {
+ this.state.pipelines = pipelines;
+
+ return pipelines;
+ }
+
+ /**
+ * Once the data is received we will start the time ago loops.
+ *
+ * Everytime a request is made like retry or cancel a pipeline, every 10 seconds we
+ * update the time to show how long as passed.
+ *
+ */
+ startTimeAgoLoops() {
+ const startTimeLoops = () => {
+ this.timeLoopInterval = setInterval(function timeloopInterval() {
+ this.$children[0].$children.reduce((acc, component) => {
+ const timeAgoComponent = component.$children.filter(el => el.$options._componentTag === 'time-ago')[0];
+ acc.push(timeAgoComponent);
+ return acc;
+ }, []).forEach(e => e.changeTime());
+ }, 10000);
+ };
+
+ startTimeLoops();
+
+ const removeIntervals = () => clearInterval(this.timeLoopInterval);
+ const startIntervals = () => startTimeLoops();
+
+ gl.VueRealtimeListener(removeIntervals, startIntervals);
+ }
+}
+
+window.gl = window.gl || {};
+gl.commits = gl.commits || {};
+gl.commits.pipelines = gl.commits.pipelines || {};
+gl.commits.pipelines.PipelinesStore = PipelinesStore;
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.js.es6 b/app/assets/javascripts/commit/pipelines/pipelines_table.js.es6
new file mode 100644
index 00000000000..ce0dbd4d56b
--- /dev/null
+++ b/app/assets/javascripts/commit/pipelines/pipelines_table.js.es6
@@ -0,0 +1,107 @@
+/* eslint-disable no-new, no-param-reassign */
+/* global Vue, CommitsPipelineStore, PipelinesService, Flash */
+
+window.Vue = require('vue');
+window.Vue.use(require('vue-resource'));
+require('../../lib/utils/common_utils');
+require('../../vue_shared/vue_resource_interceptor');
+require('../../vue_shared/components/pipelines_table');
+require('../../vue_realtime_listener/index');
+require('./pipelines_service');
+require('./pipelines_store');
+
+/**
+ *
+ * Uses `pipelines-table-component` to render Pipelines table with an API call.
+ * Endpoint is provided in HTML and passed as `endpoint`.
+ * We need a store to store the received environemnts.
+ * We need a service to communicate with the server.
+ *
+ * Necessary SVG in the table are provided as props. This should be refactored
+ * as soon as we have Webpack and can load them directly into JS files.
+ */
+
+(() => {
+ window.gl = window.gl || {};
+ gl.commits = gl.commits || {};
+ gl.commits.pipelines = gl.commits.pipelines || {};
+
+ gl.commits.pipelines.PipelinesTableView = Vue.component('pipelines-table', {
+
+ components: {
+ 'pipelines-table-component': gl.pipelines.PipelinesTableComponent,
+ },
+
+ /**
+ * Accesses the DOM to provide the needed data.
+ * Returns the necessary props to render `pipelines-table-component` component.
+ *
+ * @return {Object}
+ */
+ data() {
+ const pipelinesTableData = document.querySelector('#commit-pipeline-table-view').dataset;
+ const svgsData = document.querySelector('.pipeline-svgs').dataset;
+ const store = new gl.commits.pipelines.PipelinesStore();
+
+ // Transform svgs DOMStringMap to a plain Object.
+ const svgsObject = gl.utils.DOMStringMapToObject(svgsData);
+
+ return {
+ endpoint: pipelinesTableData.endpoint,
+ svgs: svgsObject,
+ store,
+ state: store.state,
+ isLoading: false,
+ };
+ },
+
+ /**
+ * When the component is created the service to fetch the data will be
+ * initialized with the correct endpoint.
+ *
+ * A request to fetch the pipelines will be made.
+ * In case of a successfull response we will store the data in the provided
+ * store, in case of a failed response we need to warn the user.
+ *
+ */
+ created() {
+ const pipelinesService = new gl.commits.pipelines.PipelinesService(this.endpoint);
+
+ this.isLoading = true;
+ return pipelinesService.all()
+ .then(response => response.json())
+ .then((json) => {
+ this.store.storePipelines(json);
+ this.store.startTimeAgoLoops.call(this, Vue);
+ this.isLoading = false;
+ })
+ .catch(() => {
+ this.isLoading = false;
+ new Flash('An error occurred while fetching the pipelines, please reload the page again.', 'alert');
+ });
+ },
+
+ template: `
+ <div>
+ <div class="pipelines realtime-loading" v-if="isLoading">
+ <i class="fa fa-spinner fa-spin"></i>
+ </div>
+
+ <div class="blank-state blank-state-no-icon"
+ v-if="!isLoading && state.pipelines.length === 0">
+ <h2 class="blank-state-title js-blank-state-title">
+ No pipelines to show
+ </h2>
+ </div>
+
+ <div class="table-holder pipelines"
+ v-if="!isLoading && state.pipelines.length > 0">
+ <pipelines-table-component
+ :pipelines="state.pipelines"
+ :svgs="svgs">
+ </pipelines-table-component>
+ </div>
+ </div>
+ `,
+ });
+})();
diff --git a/app/assets/javascripts/commits.js b/app/assets/javascripts/commits.js
index 24a6e4ff0e9..c6fdfbcaa10 100644
--- a/app/assets/javascripts/commits.js
+++ b/app/assets/javascripts/commits.js
@@ -1,9 +1,9 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, consistent-return, no-return-assign, no-param-reassign, one-var, no-var, one-var-declaration-per-line, no-unused-vars, prefer-template, object-shorthand, comma-dangle, padded-blocks, max-len, prefer-arrow-callback */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, consistent-return, no-return-assign, no-param-reassign, one-var, no-var, one-var-declaration-per-line, no-unused-vars, prefer-template, object-shorthand, comma-dangle, max-len, prefer-arrow-callback */
/* global Pager */
(function() {
this.CommitsList = (function() {
- function CommitsList() {}
+ var CommitsList = {};
CommitsList.timer = null;
@@ -20,6 +20,7 @@
});
this.content = $("#commits-list");
this.searchField = $("#commits-search");
+ this.lastSearch = this.searchField.val();
return this.initSearch();
};
@@ -37,6 +38,7 @@
var commitsUrl, form, search;
form = $(".commits-search-form");
search = CommitsList.searchField.val();
+ if (search === CommitsList.lastSearch) return;
commitsUrl = form.attr("action") + '?' + form.serialize();
CommitsList.content.fadeTo('fast', 0.5);
return $.ajax({
@@ -47,18 +49,20 @@
return CommitsList.content.fadeTo('fast', 1.0);
},
success: function(data) {
+ CommitsList.lastSearch = search;
CommitsList.content.html(data.html);
return history.replaceState({
page: commitsUrl
// Change url so if user reload a page - search results are saved
}, document.title, commitsUrl);
},
+ error: function() {
+ CommitsList.lastSearch = null;
+ },
dataType: "json"
});
};
return CommitsList;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/compare.js b/app/assets/javascripts/compare.js
index d4243baadb5..9591df70e9c 100644
--- a/app/assets/javascripts/compare.js
+++ b/app/assets/javascripts/compare.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, no-var, object-shorthand, consistent-return, no-unused-vars, comma-dangle, vars-on-top, prefer-template, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, no-var, object-shorthand, consistent-return, no-unused-vars, comma-dangle, vars-on-top, prefer-template, max-len */
(function() {
this.Compare = (function() {
function Compare(opts) {
@@ -87,7 +87,5 @@
};
return Compare;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/compare_autocomplete.js.es6 b/app/assets/javascripts/compare_autocomplete.js.es6
index 45c974b2b68..3587431ab69 100644
--- a/app/assets/javascripts/compare_autocomplete.js.es6
+++ b/app/assets/javascripts/compare_autocomplete.js.es6
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, object-shorthand, comma-dangle, prefer-arrow-callback, no-else-return, newline-per-chained-call, no-dupe-keys, wrap-iife, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, object-shorthand, comma-dangle, prefer-arrow-callback, no-else-return, newline-per-chained-call, wrap-iife, max-len */
(function() {
this.CompareAutocomplete = (function() {
@@ -28,7 +28,6 @@
selectable: true,
filterable: true,
filterByText: true,
- toggleLabel: true,
fieldName: $dropdown.data('field-name'),
filterInput: 'input[type="search"]',
renderRow: function(ref) {
@@ -55,11 +54,16 @@
$('.dropdown-toggle-text', $dropdown).text(text);
$dropdownContainer.removeClass('open');
});
+
+ $dropdownContainer.on('click', '.dropdown-content a', (e) => {
+ $dropdown.prop('title', e.target.text.replace(/_+?/g, '-'));
+ if ($dropdown.hasClass('has-tooltip')) {
+ $dropdown.tooltip('fixTitle');
+ }
+ });
});
};
return CompareAutocomplete;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/confirm_danger_modal.js b/app/assets/javascripts/confirm_danger_modal.js
index 686a48486f3..35d98492012 100644
--- a/app/assets/javascripts/confirm_danger_modal.js
+++ b/app/assets/javascripts/confirm_danger_modal.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, camelcase, one-var-declaration-per-line, no-else-return, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, camelcase, one-var-declaration-per-line, no-else-return, max-len */
(function() {
this.ConfirmDangerModal = (function() {
function ConfirmDangerModal(form, text) {
@@ -27,7 +27,5 @@
}
return ConfirmDangerModal;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/copy_as_gfm.js.es6 b/app/assets/javascripts/copy_as_gfm.js.es6
new file mode 100644
index 00000000000..2bfe57b4100
--- /dev/null
+++ b/app/assets/javascripts/copy_as_gfm.js.es6
@@ -0,0 +1,355 @@
+/* eslint-disable class-methods-use-this, object-shorthand, no-unused-vars, no-use-before-define, no-new, max-len, no-restricted-syntax, guard-for-in, no-continue */
+/* jshint esversion: 6 */
+
+require('./lib/utils/common_utils');
+
+(() => {
+ const gfmRules = {
+ // The filters referenced in lib/banzai/pipeline/gfm_pipeline.rb convert
+ // GitLab Flavored Markdown (GFM) to HTML.
+ // These handlers consequently convert that same HTML to GFM to be copied to the clipboard.
+ // Every filter in lib/banzai/pipeline/gfm_pipeline.rb that generates HTML
+ // from GFM should have a handler here, in reverse order.
+ // The GFM-to-HTML-to-GFM cycle is tested in spec/features/copy_as_gfm_spec.rb.
+ InlineDiffFilter: {
+ 'span.idiff.addition'(el, text) {
+ return `{+${text}+}`;
+ },
+ 'span.idiff.deletion'(el, text) {
+ return `{-${text}-}`;
+ },
+ },
+ TaskListFilter: {
+ 'input[type=checkbox].task-list-item-checkbox'(el, text) {
+ return `[${el.checked ? 'x' : ' '}]`;
+ },
+ },
+ ReferenceFilter: {
+ 'a.gfm:not([data-link=true])'(el, text) {
+ return el.dataset.original || text;
+ },
+ },
+ AutolinkFilter: {
+ 'a'(el, text) {
+ // Fallback on the regular MarkdownFilter's `a` handler.
+ if (text !== el.getAttribute('href')) return false;
+
+ return text;
+ },
+ },
+ TableOfContentsFilter: {
+ 'ul.section-nav'(el, text) {
+ return '[[_TOC_]]';
+ },
+ },
+ EmojiFilter: {
+ 'img.emoji'(el, text) {
+ return el.getAttribute('alt');
+ },
+ },
+ ImageLinkFilter: {
+ 'a.no-attachment-icon'(el, text) {
+ return text;
+ },
+ },
+ VideoLinkFilter: {
+ '.video-container'(el, text) {
+ const videoEl = el.querySelector('video');
+ if (!videoEl) return false;
+
+ return CopyAsGFM.nodeToGFM(videoEl);
+ },
+ 'video'(el, text) {
+ return `![${el.dataset.title}](${el.getAttribute('src')})`;
+ },
+ },
+ MathFilter: {
+ 'pre.code.math[data-math-style=display]'(el, text) {
+ return `\`\`\`math\n${text.trim()}\n\`\`\``;
+ },
+ 'code.code.math[data-math-style=inline]'(el, text) {
+ return `$\`${text}\`$`;
+ },
+ 'span.katex-display span.katex-mathml'(el, text) {
+ const mathAnnotation = el.querySelector('annotation[encoding="application/x-tex"]');
+ if (!mathAnnotation) return false;
+
+ return `\`\`\`math\n${CopyAsGFM.nodeToGFM(mathAnnotation)}\n\`\`\``;
+ },
+ 'span.katex-mathml'(el, text) {
+ const mathAnnotation = el.querySelector('annotation[encoding="application/x-tex"]');
+ if (!mathAnnotation) return false;
+
+ return `$\`${CopyAsGFM.nodeToGFM(mathAnnotation)}\`$`;
+ },
+ 'span.katex-html'(el, text) {
+ // We don't want to include the content of this element in the copied text.
+ return '';
+ },
+ 'annotation[encoding="application/x-tex"]'(el, text) {
+ return text.trim();
+ },
+ },
+ SanitizationFilter: {
+ 'dl'(el, text) {
+ let lines = text.trim().split('\n');
+ // Add two spaces to the front of subsequent list items lines,
+ // or leave the line entirely blank.
+ lines = lines.map((l) => {
+ const line = l.trim();
+ if (line.length === 0) return '';
+
+ return ` ${line}`;
+ });
+
+ return `<dl>\n${lines.join('\n')}\n</dl>`;
+ },
+ 'sub, dt, dd, kbd, q, samp, var, ruby, rt, rp, abbr'(el, text) {
+ const tag = el.nodeName.toLowerCase();
+ return `<${tag}>${text}</${tag}>`;
+ },
+ },
+ SyntaxHighlightFilter: {
+ 'pre.code.highlight'(el, t) {
+ const text = t.trim();
+
+ let lang = el.getAttribute('lang');
+ if (lang === 'plaintext') {
+ lang = '';
+ }
+
+ // Prefixes lines with 4 spaces if the code contains triple backticks
+ if (lang === '' && text.match(/^```/gm)) {
+ return text.split('\n').map((l) => {
+ const line = l.trim();
+ if (line.length === 0) return '';
+
+ return ` ${line}`;
+ }).join('\n');
+ }
+
+ return `\`\`\`${lang}\n${text}\n\`\`\``;
+ },
+ 'pre > code'(el, text) {
+ // Don't wrap code blocks in ``
+ return text;
+ },
+ },
+ MarkdownFilter: {
+ 'br'(el, text) {
+ // Two spaces at the end of a line are turned into a BR
+ return ' ';
+ },
+ 'code'(el, text) {
+ let backtickCount = 1;
+ const backtickMatch = text.match(/`+/);
+ if (backtickMatch) {
+ backtickCount = backtickMatch[0].length + 1;
+ }
+
+ const backticks = Array(backtickCount + 1).join('`');
+ const spaceOrNoSpace = backtickCount > 1 ? ' ' : '';
+
+ return backticks + spaceOrNoSpace + text + spaceOrNoSpace + backticks;
+ },
+ 'blockquote'(el, text) {
+ return text.trim().split('\n').map(s => `> ${s}`.trim()).join('\n');
+ },
+ 'img'(el, text) {
+ return `![${el.getAttribute('alt')}](${el.getAttribute('src')})`;
+ },
+ 'a.anchor'(el, text) {
+ // Don't render a Markdown link for the anchor link inside a heading
+ return text;
+ },
+ 'a'(el, text) {
+ return `[${text}](${el.getAttribute('href')})`;
+ },
+ 'li'(el, text) {
+ const lines = text.trim().split('\n');
+ const firstLine = `- ${lines.shift()}`;
+ // Add four spaces to the front of subsequent list items lines,
+ // or leave the line entirely blank.
+ const nextLines = lines.map((s) => {
+ if (s.trim().length === 0) return '';
+
+ return ` ${s}`;
+ });
+
+ return `${firstLine}\n${nextLines.join('\n')}`;
+ },
+ 'ul'(el, text) {
+ return text;
+ },
+ 'ol'(el, text) {
+ // LIs get a `- ` prefix by default, which we replace by `1. ` for ordered lists.
+ return text.replace(/^- /mg, '1. ');
+ },
+ 'h1'(el, text) {
+ return `# ${text.trim()}`;
+ },
+ 'h2'(el, text) {
+ return `## ${text.trim()}`;
+ },
+ 'h3'(el, text) {
+ return `### ${text.trim()}`;
+ },
+ 'h4'(el, text) {
+ return `#### ${text.trim()}`;
+ },
+ 'h5'(el, text) {
+ return `##### ${text.trim()}`;
+ },
+ 'h6'(el, text) {
+ return `###### ${text.trim()}`;
+ },
+ 'strong'(el, text) {
+ return `**${text}**`;
+ },
+ 'em'(el, text) {
+ return `_${text}_`;
+ },
+ 'del'(el, text) {
+ return `~~${text}~~`;
+ },
+ 'sup'(el, text) {
+ return `^${text}`;
+ },
+ 'hr'(el, text) {
+ return '-----';
+ },
+ 'table'(el, text) {
+ const theadEl = el.querySelector('thead');
+ const tbodyEl = el.querySelector('tbody');
+ if (!theadEl || !tbodyEl) return false;
+
+ const theadText = CopyAsGFM.nodeToGFM(theadEl);
+ const tbodyText = CopyAsGFM.nodeToGFM(tbodyEl);
+
+ return theadText + tbodyText;
+ },
+ 'thead'(el, text) {
+ const cells = _.map(el.querySelectorAll('th'), (cell) => {
+ let chars = CopyAsGFM.nodeToGFM(cell).trim().length + 2;
+
+ let before = '';
+ let after = '';
+ switch (cell.style.textAlign) {
+ case 'center':
+ before = ':';
+ after = ':';
+ chars -= 2;
+ break;
+ case 'right':
+ after = ':';
+ chars -= 1;
+ break;
+ default:
+ break;
+ }
+
+ chars = Math.max(chars, 3);
+
+ const middle = Array(chars + 1).join('-');
+
+ return before + middle + after;
+ });
+
+ return `${text}|${cells.join('|')}|`;
+ },
+ 'tr'(el, text) {
+ const cells = _.map(el.querySelectorAll('td, th'), cell => CopyAsGFM.nodeToGFM(cell).trim());
+ return `| ${cells.join(' | ')} |`;
+ },
+ },
+ };
+
+ class CopyAsGFM {
+ constructor() {
+ $(document).on('copy', '.md, .wiki', this.handleCopy);
+ $(document).on('paste', '.js-gfm-input', this.handlePaste);
+ }
+
+ handleCopy(e) {
+ const clipboardData = e.originalEvent.clipboardData;
+ if (!clipboardData) return;
+
+ const documentFragment = window.gl.utils.getSelectedFragment();
+ if (!documentFragment) return;
+
+ // If the documentFragment contains more than just Markdown, don't copy as GFM.
+ if (documentFragment.querySelector('.md, .wiki')) return;
+
+ e.preventDefault();
+ clipboardData.setData('text/plain', documentFragment.textContent);
+
+ const gfm = CopyAsGFM.nodeToGFM(documentFragment);
+ clipboardData.setData('text/x-gfm', gfm);
+ }
+
+ handlePaste(e) {
+ const clipboardData = e.originalEvent.clipboardData;
+ if (!clipboardData) return;
+
+ const gfm = clipboardData.getData('text/x-gfm');
+ if (!gfm) return;
+
+ e.preventDefault();
+
+ window.gl.utils.insertText(e.target, gfm);
+ }
+
+ static nodeToGFM(node) {
+ if (node.nodeType === Node.TEXT_NODE) {
+ return node.textContent;
+ }
+
+ const text = this.innerGFM(node);
+
+ if (node.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
+ return text;
+ }
+
+ for (const filter in gfmRules) {
+ const rules = gfmRules[filter];
+
+ for (const selector in rules) {
+ const func = rules[selector];
+
+ if (!window.gl.utils.nodeMatchesSelector(node, selector)) continue;
+
+ const result = func(node, text);
+ if (result === false) continue;
+
+ return result;
+ }
+ }
+
+ return text;
+ }
+
+ static innerGFM(parentNode) {
+ const nodes = parentNode.childNodes;
+
+ const clonedParentNode = parentNode.cloneNode(true);
+ const clonedNodes = Array.prototype.slice.call(clonedParentNode.childNodes, 0);
+
+ for (let i = 0; i < nodes.length; i += 1) {
+ const node = nodes[i];
+ const clonedNode = clonedNodes[i];
+
+ const text = this.nodeToGFM(node);
+
+ // `clonedNode.replaceWith(text)` is not yet widely supported
+ clonedNode.parentNode.replaceChild(document.createTextNode(text), clonedNode);
+ }
+
+ return clonedParentNode.innerText || clonedParentNode.textContent;
+ }
+ }
+
+ window.gl = window.gl || {};
+ window.gl.CopyAsGFM = CopyAsGFM;
+
+ new CopyAsGFM();
+})();
diff --git a/app/assets/javascripts/copy_to_clipboard.js b/app/assets/javascripts/copy_to_clipboard.js
index 6a13f38588d..0029c59e550 100644
--- a/app/assets/javascripts/copy_to_clipboard.js
+++ b/app/assets/javascripts/copy_to_clipboard.js
@@ -1,7 +1,7 @@
-/* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, prefer-template, quotes, no-unused-vars, prefer-arrow-callback, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, prefer-template, quotes, no-unused-vars, prefer-arrow-callback, max-len */
/* global Clipboard */
-/*= require clipboard */
+window.Clipboard = require('vendor/clipboard');
(function() {
var genericError, genericSuccess, showTooltip;
@@ -46,5 +46,4 @@
clipboard.on('success', genericSuccess);
return clipboard.on('error', genericError);
});
-
}).call(this);
diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6 b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6
index 2f810a69758..c41c57c1dcd 100644
--- a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6
+++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6
@@ -2,9 +2,12 @@
/* global Cookies */
/* global Flash */
-//= require vue
-//= require_tree ./svg
-//= require_tree .
+window.Vue = require('vue');
+window.Cookies = require('vendor/js.cookie');
+
+function requireAll(context) { return context.keys().map(context); }
+requireAll(require.context('./svg', false, /^\.\/.*\.(js|es6)$/));
+requireAll(require.context('.', true, /^\.\/(?!cycle_analytics_bundle).*\.(js|es6)$/));
$(() => {
const OVERVIEW_DIALOG_COOKIE = 'cycle_analytics_help_dismissed';
diff --git a/app/assets/javascripts/diff.js.es6 b/app/assets/javascripts/diff.js.es6
index 9cf33e62958..c39e30fb7e0 100644
--- a/app/assets/javascripts/diff.js.es6
+++ b/app/assets/javascripts/diff.js.es6
@@ -1,7 +1,10 @@
/* eslint-disable class-methods-use-this */
+require('./lib/utils/url_utility');
+
(() => {
const UNFOLD_COUNT = 20;
+ let isBound = false;
class Diff {
constructor() {
@@ -15,12 +18,14 @@
$('.content-wrapper .container-fluid').removeClass('container-limited');
}
- $(document)
- .off('click', '.js-unfold, .diff-line-num a')
- .on('click', '.js-unfold', this.handleClickUnfold.bind(this))
- .on('click', '.diff-line-num a', this.handleClickLineNum.bind(this));
+ if (!isBound) {
+ $(document)
+ .on('click', '.js-unfold', this.handleClickUnfold.bind(this))
+ .on('click', '.diff-line-num a', this.handleClickLineNum.bind(this));
+ isBound = true;
+ }
- this.highlighSelectedLine();
+ this.openAnchoredDiff();
}
handleClickUnfold(e) {
@@ -61,13 +66,22 @@
$.get(link, params, response => $target.parent().replaceWith(response));
}
- openAnchoredDiff(anchoredDiff, cb) {
- const diffTitle = $(`#file-path-${anchoredDiff}`);
+ openAnchoredDiff(cb) {
+ const locationHash = gl.utils.getLocationHash();
+ const anchoredDiff = locationHash && locationHash.split('_')[0];
+
+ if (!anchoredDiff) return;
+
+ const diffTitle = $(`#${anchoredDiff}`);
const diffFile = diffTitle.closest('.diff-file');
const nothingHereBlock = $('.nothing-here-block:visible', diffFile);
if (nothingHereBlock.length) {
- diffFile.singleFileDiff(true, cb);
- } else {
+ const clickTarget = $('.file-title, .click-to-expand', diffFile);
+ diffFile.data('singleFileDiff').toggleDiff(clickTarget, () => {
+ this.highlighSelectedLine();
+ if (cb) cb();
+ });
+ } else if (cb) {
cb();
}
}
@@ -95,11 +109,11 @@
}
highlighSelectedLine() {
+ const hash = gl.utils.getLocationHash();
const $diffFiles = $('.diff-file');
$diffFiles.find('.hll').removeClass('hll');
- if (window.location.hash !== '') {
- const hash = window.location.hash.replace('#', '');
+ if (hash) {
$diffFiles
.find(`tr#${hash}:not(.match) td, td#${hash}, td[data-line-code="${hash}"]`)
.addClass('hll');
diff --git a/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es6 b/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es6
index c59d3996fab..2514459e65e 100644
--- a/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es6
+++ b/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es6
@@ -1,4 +1,4 @@
-/* eslint-disable comma-dangle, object-shorthand, func-names, no-else-return, quotes, no-lonely-if, semi, max-len */
+/* eslint-disable comma-dangle, object-shorthand, func-names, no-else-return, quotes, no-lonely-if, max-len */
/* global Vue */
/* global CommentsStore */
@@ -10,7 +10,7 @@
data() {
return {
textareaIsEmpty: true
- }
+ };
},
computed: {
discussion: function () {
diff --git a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es6 b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es6
index f47867fc3b0..c3898873eaa 100644
--- a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es6
+++ b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es6
@@ -1,4 +1,4 @@
-/* eslint-disable comma-dangle, object-shorthand, func-names, no-else-return, guard-for-in, no-restricted-syntax, one-var, indent, space-before-function-paren, no-plusplus, no-lonely-if, no-continue, brace-style, max-len, quotes, semi */
+/* eslint-disable comma-dangle, object-shorthand, func-names, no-else-return, guard-for-in, no-restricted-syntax, one-var, space-before-function-paren, no-lonely-if, no-continue, brace-style, max-len, quotes */
/* global Vue */
/* global DiscussionMixins */
/* global CommentsStore */
@@ -46,13 +46,13 @@
},
methods: {
jumpToNextUnresolvedDiscussion: function () {
- let discussionsSelector,
- discussionIdsInScope,
- firstUnresolvedDiscussionId,
- nextUnresolvedDiscussionId,
- activeTab = window.mrTabs.currentAction,
- hasDiscussionsToJumpTo = true,
- jumpToFirstDiscussion = !this.discussionId;
+ let discussionsSelector;
+ let discussionIdsInScope;
+ let firstUnresolvedDiscussionId;
+ let nextUnresolvedDiscussionId;
+ let activeTab = window.mrTabs.currentAction;
+ let hasDiscussionsToJumpTo = true;
+ let jumpToFirstDiscussion = !this.discussionId;
const discussionIdsForElements = function(elements) {
return elements.map(function() {
@@ -68,11 +68,11 @@
let unresolvedDiscussionCount = 0;
- for (let i = 0; i < discussionIdsInScope.length; i++) {
+ for (let i = 0; i < discussionIdsInScope.length; i += 1) {
const discussionId = discussionIdsInScope[i];
const discussion = discussions[discussionId];
if (discussion && !discussion.isResolved()) {
- unresolvedDiscussionCount++;
+ unresolvedDiscussionCount += 1;
}
}
@@ -109,7 +109,7 @@
}
let currentDiscussionFound = false;
- for (let i = 0; i < discussionIdsInScope.length; i++) {
+ for (let i = 0; i < discussionIdsInScope.length; i += 1) {
const discussionId = discussionIdsInScope[i];
const discussion = discussions[discussionId];
@@ -156,7 +156,7 @@
// If the next discussion is closed, toggle it open.
if ($target.find('.js-toggle-content').is(':hidden')) {
- $target.find('.js-toggle-button i').trigger('click')
+ $target.find('.js-toggle-button i').trigger('click');
}
} else if (activeTab === 'diffs') {
// Resolved discussions are hidden in the diffs tab by default.
@@ -170,7 +170,7 @@
// If we are on the diffs tab, we don't scroll to the discussion itself, but to
// 4 diff lines above it: the line the discussion was in response to + 3 context
let prevEl;
- for (let i = 0; i < 4; i++) {
+ for (let i = 0; i < 4; i += 1) {
prevEl = $target.prev();
// If the discussion doesn't have 4 lines above it, we'll have to do with fewer.
diff --git a/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6 b/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6
index 88a19fc6e1d..5852b8bbdb7 100644
--- a/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6
+++ b/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6
@@ -59,9 +59,11 @@
},
methods: {
updateTooltip: function () {
- $(this.$refs.button)
- .tooltip('hide')
- .tooltip('fixTitle');
+ this.$nextTick(() => {
+ $(this.$refs.button)
+ .tooltip('hide')
+ .tooltip('fixTitle');
+ });
},
resolve: function () {
if (!this.canResolve) return;
@@ -90,7 +92,7 @@
new Flash('An error occurred when trying to resolve a comment. Please try again.', 'alert');
}
- this.$nextTick(this.updateTooltip);
+ this.updateTooltip();
});
}
},
diff --git a/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6 b/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6
index 840b5aa922e..f0edfb8aaf1 100644
--- a/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6
+++ b/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6
@@ -1,14 +1,13 @@
-/* eslint-disable func-names, comma-dangle, new-cap, no-new */
+/* eslint-disable func-names, comma-dangle, new-cap, no-new, import/newline-after-import, no-multi-spaces, max-len */
/* global Vue */
/* global ResolveCount */
-//= require vue
-//= require vue-resource
-//= require_directory ./models
-//= require_directory ./stores
-//= require_directory ./services
-//= require_directory ./mixins
-//= require_directory ./components
+function requireAll(context) { return context.keys().map(context); }
+requireAll(require.context('./models', false, /^\.\/.*\.(js|es6)$/));
+requireAll(require.context('./stores', false, /^\.\/.*\.(js|es6)$/));
+requireAll(require.context('./services', false, /^\.\/.*\.(js|es6)$/));
+requireAll(require.context('./mixins', false, /^\.\/.*\.(js|es6)$/));
+requireAll(require.context('./components', false, /^\.\/.*\.(js|es6)$/));
$(() => {
const COMPONENT_SELECTOR = 'resolve-btn, resolve-discussion-btn, jump-to-discussion, comment-and-resolve-btn';
diff --git a/app/assets/javascripts/diff_notes/mixins/discussion.js.es6 b/app/assets/javascripts/diff_notes/mixins/discussion.js.es6
index a9ea18bf82b..3c08c222f46 100644
--- a/app/assets/javascripts/diff_notes/mixins/discussion.js.es6
+++ b/app/assets/javascripts/diff_notes/mixins/discussion.js.es6
@@ -1,4 +1,4 @@
-/* eslint-disable object-shorthand, func-names, guard-for-in, no-restricted-syntax, comma-dangle, no-plusplus, no-param-reassign, max-len */
+/* eslint-disable object-shorthand, func-names, guard-for-in, no-restricted-syntax, comma-dangle, no-param-reassign, max-len */
((w) => {
w.DiscussionMixins = {
@@ -13,7 +13,7 @@
const discussion = this.discussions[discussionId];
if (discussion.isResolved()) {
- resolvedCount++;
+ resolvedCount += 1;
}
}
@@ -26,7 +26,7 @@
const discussion = this.discussions[discussionId];
if (!discussion.isResolved()) {
- unresolvedCount++;
+ unresolvedCount += 1;
}
}
diff --git a/app/assets/javascripts/diff_notes/models/discussion.js.es6 b/app/assets/javascripts/diff_notes/models/discussion.js.es6
index efcd46680a7..fa518ba4d33 100644
--- a/app/assets/javascripts/diff_notes/models/discussion.js.es6
+++ b/app/assets/javascripts/diff_notes/models/discussion.js.es6
@@ -92,3 +92,5 @@ class DiscussionModel {
return false;
}
}
+
+window.DiscussionModel = DiscussionModel;
diff --git a/app/assets/javascripts/diff_notes/models/note.js.es6 b/app/assets/javascripts/diff_notes/models/note.js.es6
index e3bce1d2038..f3a7cba5ef6 100644
--- a/app/assets/javascripts/diff_notes/models/note.js.es6
+++ b/app/assets/javascripts/diff_notes/models/note.js.es6
@@ -9,3 +9,5 @@ class NoteModel {
this.resolved_by = resolved_by;
}
}
+
+window.NoteModel = NoteModel;
diff --git a/app/assets/javascripts/diff_notes/services/resolve.js.es6 b/app/assets/javascripts/diff_notes/services/resolve.js.es6
index 78c74985f78..a52c476352d 100644
--- a/app/assets/javascripts/diff_notes/services/resolve.js.es6
+++ b/app/assets/javascripts/diff_notes/services/resolve.js.es6
@@ -1,4 +1,4 @@
-/* eslint-disable class-methods-use-this, one-var, indent, camelcase, no-new, comma-dangle, semi, no-param-reassign, max-len */
+/* eslint-disable class-methods-use-this, one-var, camelcase, no-new, comma-dangle, no-param-reassign, max-len */
/* global Vue */
/* global Flash */
/* global CommentsStore */
@@ -32,8 +32,8 @@
}
toggleResolveForDiscussion(projectPath, mergeRequestId, discussionId) {
- const discussion = CommentsStore.state[discussionId],
- isResolved = discussion.isResolved();
+ const discussion = CommentsStore.state[discussionId];
+ const isResolved = discussion.isResolved();
let promise;
if (isResolved) {
@@ -59,7 +59,7 @@
} else {
new Flash('An error occurred when trying to resolve a discussion. Please try again.', 'alert');
}
- })
+ });
}
resolveAll(projectPath, mergeRequestId, discussionId) {
diff --git a/app/assets/javascripts/diff_notes/stores/comments.js.es6 b/app/assets/javascripts/diff_notes/stores/comments.js.es6
index 1a7abbc6f75..c80d979b977 100644
--- a/app/assets/javascripts/diff_notes/stores/comments.js.es6
+++ b/app/assets/javascripts/diff_notes/stores/comments.js.es6
@@ -1,4 +1,4 @@
-/* eslint-disable object-shorthand, func-names, camelcase, prefer-const, no-restricted-syntax, guard-for-in, comma-dangle, max-len, no-param-reassign */
+/* eslint-disable object-shorthand, func-names, camelcase, no-restricted-syntax, guard-for-in, comma-dangle, max-len, no-param-reassign */
/* global Vue */
/* global DiscussionModel */
@@ -41,7 +41,7 @@
}
},
unresolvedDiscussionIds: function () {
- let ids = [];
+ const ids = [];
for (const discussionId in this.state) {
const discussion = this.state[discussionId];
diff --git a/app/assets/javascripts/dispatcher.js.es6 b/app/assets/javascripts/dispatcher.js.es6
index 752f35e6356..f8efca76b13 100644
--- a/app/assets/javascripts/dispatcher.js.es6
+++ b/app/assets/javascripts/dispatcher.js.es6
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, prefer-arrow-callback, wrap-iife, no-shadow, consistent-return, one-var, one-var-declaration-per-line, camelcase, default-case, no-new, quotes, no-duplicate-case, no-case-declarations, no-fallthrough, max-len, padded-blocks */
+/* eslint-disable func-names, space-before-function-paren, no-var, prefer-arrow-callback, wrap-iife, no-shadow, consistent-return, one-var, one-var-declaration-per-line, camelcase, default-case, no-new, quotes, no-duplicate-case, no-case-declarations, no-fallthrough, max-len */
/* global UsernameValidator */
/* global ActiveTabMemoizer */
/* global ShortcutsNavigation */
@@ -8,7 +8,6 @@
/* global ShortcutsIssuable */
/* global ZenMode */
/* global Milestone */
-/* global GLForm */
/* global IssuableForm */
/* global LabelsSelect */
/* global MilestoneSelect */
@@ -20,7 +19,6 @@
/* global UsersSelect */
/* global GroupAvatar */
/* global LineHighlighter */
-/* global ShortcutsBlob */
/* global ProjectFork */
/* global BuildArtifacts */
/* global GroupsSelect */
@@ -37,6 +35,8 @@
/* global Labels */
/* global Shortcuts */
+const ShortcutsBlob = require('./shortcuts_blob');
+
(function() {
var Dispatcher;
@@ -73,6 +73,9 @@
break;
case 'projects:merge_requests:index':
case 'projects:issues:index':
+ if (gl.FilteredSearchManager) {
+ new gl.FilteredSearchManager();
+ }
Issuable.init();
new gl.IssuableBulkActions({
prefixId: page === 'projects:merge_requests:index' ? 'merge_request_' : 'issue_',
@@ -96,7 +99,7 @@
case 'projects:milestones:edit':
new ZenMode();
new gl.DueDateSelectors();
- new GLForm($('.milestone-form'));
+ new gl.GLForm($('.milestone-form'));
break;
case 'groups:milestones:new':
new ZenMode();
@@ -107,7 +110,7 @@
case 'projects:issues:new':
case 'projects:issues:edit':
shortcut_handler = new ShortcutsNavigation();
- new GLForm($('.issue-form'));
+ new gl.GLForm($('.issue-form'));
new IssuableForm($('.issue-form'));
new LabelsSelect();
new MilestoneSelect();
@@ -117,7 +120,7 @@
case 'projects:merge_requests:edit':
new gl.Diff();
shortcut_handler = new ShortcutsNavigation();
- new GLForm($('.merge-request-form'));
+ new gl.GLForm($('.merge-request-form'));
new IssuableForm($('.merge-request-form'));
new LabelsSelect();
new MilestoneSelect();
@@ -125,11 +128,11 @@
break;
case 'projects:tags:new':
new ZenMode();
- new GLForm($('.tag-form'));
+ new gl.GLForm($('.tag-form'));
break;
case 'projects:releases:edit':
new ZenMode();
- new GLForm($('.release-form'));
+ new gl.GLForm($('.release-form'));
break;
case 'projects:merge_requests:show':
new gl.Diff();
@@ -138,14 +141,8 @@
new MergedButtons();
break;
case 'projects:merge_requests:commits':
- case 'projects:merge_requests:builds':
new MergedButtons();
break;
- case 'projects:merge_requests:pipelines':
- new gl.MiniPipelineGraph({
- container: '.js-pipeline-table',
- });
- break;
case "projects:merge_requests:diffs":
new gl.Diff();
new ZenMode();
@@ -166,10 +163,7 @@
case 'projects:commit:pipelines':
new gl.MiniPipelineGraph({
container: '.js-pipeline-table',
- });
- break;
- case 'projects:commit:builds':
- new gl.Pipelines();
+ }).bindEvents();
break;
case 'projects:commits:show':
case 'projects:activity':
@@ -182,11 +176,6 @@
new TreeView();
}
break;
- case 'projects:pipelines:index':
- new gl.MiniPipelineGraph({
- container: '.js-pipeline-table',
- });
- break;
case 'projects:pipelines:builds':
case 'projects:pipelines:show':
const { controllerAction } = document.querySelector('.js-pipeline-container').dataset;
@@ -213,7 +202,9 @@
new gl.Members();
new UsersSelect();
break;
- case 'projects:project_members:index':
+ case 'projects:members:show':
+ new gl.MemberExpirationDate('.js-access-expiration-date-groups');
+ new GroupsSelect();
new gl.MemberExpirationDate();
new gl.Members();
new UsersSelect();
@@ -235,7 +226,12 @@
case 'projects:blame:show':
new LineHighlighter();
shortcut_handler = new ShortcutsNavigation();
- new ShortcutsBlob(true);
+ const fileBlobPermalinkUrlElement = document.querySelector('.js-data-file-blob-permalink-url');
+ const fileBlobPermalinkUrl = fileBlobPermalinkUrlElement && fileBlobPermalinkUrlElement.getAttribute('href');
+ new ShortcutsBlob({
+ skipResetBindings: true,
+ fileBlobPermalinkUrl,
+ });
break;
case 'groups:labels:new':
case 'groups:labels:edit':
@@ -259,9 +255,8 @@
case 'projects:artifacts:browse':
new BuildArtifacts();
break;
- case 'projects:group_links:index':
- new gl.MemberExpirationDate();
- new GroupsSelect();
+ case 'help:index':
+ gl.VersionCheckImage.bindErrorEvent($('img.js-version-status-badge'));
break;
case 'search:show':
new Search();
@@ -270,11 +265,26 @@
new gl.ProtectedBranchCreate();
new gl.ProtectedBranchEditList();
break;
- case 'projects:variables:index':
+ case 'projects:ci_cd:show':
new gl.ProjectVariables();
break;
+ case 'ci:lints:create':
+ case 'ci:lints:show':
+ new gl.CILintEditor();
+ break;
}
switch (path.first()) {
+ case 'sessions':
+ case 'omniauth_callbacks':
+ if (!gon.u2f) break;
+ gl.u2fAuthenticate = new gl.U2FAuthenticate(
+ $('#js-authenticate-u2f'),
+ '#js-login-u2f-form',
+ gon.u2f,
+ document.querySelector('#js-login-2fa-device'),
+ document.querySelector('.js-2fa-form'),
+ );
+ gl.u2fAuthenticate.start();
case 'admin':
new Admin();
switch (path[1]) {
@@ -327,7 +337,7 @@
new gl.Wikis();
shortcut_handler = new ShortcutsNavigation();
new ZenMode();
- new GLForm($('.wiki-form'));
+ new gl.GLForm($('.wiki-form'));
break;
case 'snippets':
shortcut_handler = new ShortcutsNavigation();
@@ -352,7 +362,7 @@
}
// If we haven't installed a custom shortcut handler, install the default one
if (!shortcut_handler) {
- return new Shortcuts();
+ new Shortcuts();
}
};
@@ -370,7 +380,5 @@
};
return Dispatcher;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/droplab/droplab.js b/app/assets/javascripts/droplab/droplab.js
new file mode 100644
index 00000000000..8b14191395b
--- /dev/null
+++ b/app/assets/javascripts/droplab/droplab.js
@@ -0,0 +1,741 @@
+/* eslint-disable */
+// Determine where to place this
+if (typeof Object.assign != 'function') {
+ Object.assign = function (target, varArgs) { // .length of function is 2
+ 'use strict';
+ if (target == null) { // TypeError if undefined or null
+ throw new TypeError('Cannot convert undefined or null to object');
+ }
+
+ var to = Object(target);
+
+ for (var index = 1; index < arguments.length; index++) {
+ var nextSource = arguments[index];
+
+ if (nextSource != null) { // Skip over if undefined or null
+ for (var nextKey in nextSource) {
+ // Avoid bugs when hasOwnProperty is shadowed
+ if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
+ to[nextKey] = nextSource[nextKey];
+ }
+ }
+ }
+ }
+ return to;
+ };
+}
+
+(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.droplab = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
+var DATA_TRIGGER = 'data-dropdown-trigger';
+var DATA_DROPDOWN = 'data-dropdown';
+
+module.exports = {
+ DATA_TRIGGER: DATA_TRIGGER,
+ DATA_DROPDOWN: DATA_DROPDOWN,
+}
+
+},{}],2:[function(require,module,exports){
+// Custom event support for IE
+if ( typeof CustomEvent === "function" ) {
+ module.exports = CustomEvent;
+} else {
+ require('./window')(function(w){
+ var CustomEvent = function ( event, params ) {
+ params = params || { bubbles: false, cancelable: false, detail: undefined };
+ var evt = document.createEvent( 'CustomEvent' );
+ evt.initCustomEvent( event, params.bubbles, params.cancelable, params.detail );
+ return evt;
+ }
+ CustomEvent.prototype = w.Event.prototype;
+
+ w.CustomEvent = CustomEvent;
+ });
+ module.exports = CustomEvent;
+}
+
+},{"./window":11}],3:[function(require,module,exports){
+var CustomEvent = require('./custom_event_polyfill');
+var utils = require('./utils');
+
+var DropDown = function(list) {
+ this.currentIndex = 0;
+ this.hidden = true;
+ this.list = list;
+ this.items = [];
+ this.getItems();
+ this.initTemplateString();
+ this.addEvents();
+ this.initialState = list.innerHTML;
+};
+
+Object.assign(DropDown.prototype, {
+ getItems: function() {
+ this.items = [].slice.call(this.list.querySelectorAll('li'));
+ return this.items;
+ },
+
+ initTemplateString: function() {
+ var items = this.items || this.getItems();
+
+ var templateString = '';
+ if(items.length > 0) {
+ templateString = items[items.length - 1].outerHTML;
+ }
+ this.templateString = templateString;
+ return this.templateString;
+ },
+
+ clickEvent: function(e) {
+ // climb up the tree to find the LI
+ var selected = utils.closest(e.target, 'LI');
+
+ if(selected) {
+ e.preventDefault();
+ this.hide();
+ var listEvent = new CustomEvent('click.dl', {
+ detail: {
+ list: this,
+ selected: selected,
+ data: e.target.dataset,
+ },
+ });
+ this.list.dispatchEvent(listEvent);
+ }
+ },
+
+ addEvents: function() {
+ this.clickWrapper = this.clickEvent.bind(this);
+ // event delegation.
+ this.list.addEventListener('click', this.clickWrapper);
+ },
+
+ toggle: function() {
+ if(this.hidden) {
+ this.show();
+ } else {
+ this.hide();
+ }
+ },
+
+ setData: function(data) {
+ this.data = data;
+ this.render(data);
+ },
+
+ addData: function(data) {
+ this.data = (this.data || []).concat(data);
+ this.render(this.data);
+ },
+
+ // call render manually on data;
+ render: function(data){
+ // debugger
+ // empty the list first
+ var templateString = this.templateString;
+ var newChildren = [];
+ var toAppend;
+
+ newChildren = (data ||[]).map(function(dat){
+ var html = utils.t(templateString, dat);
+ var template = document.createElement('div');
+ template.innerHTML = html;
+
+ // Help set the image src template
+ var imageTags = template.querySelectorAll('img[data-src]');
+ // debugger
+ for(var i = 0; i < imageTags.length; i++) {
+ var imageTag = imageTags[i];
+ imageTag.src = imageTag.getAttribute('data-src');
+ imageTag.removeAttribute('data-src');
+ }
+
+ if(dat.hasOwnProperty('droplab_hidden') && dat.droplab_hidden){
+ template.firstChild.style.display = 'none'
+ }else{
+ template.firstChild.style.display = 'block';
+ }
+ return template.firstChild.outerHTML;
+ });
+ toAppend = this.list.querySelector('ul[data-dynamic]');
+ if(toAppend) {
+ toAppend.innerHTML = newChildren.join('');
+ } else {
+ this.list.innerHTML = newChildren.join('');
+ }
+ },
+
+ show: function() {
+ if (this.hidden) {
+ // debugger
+ this.list.style.display = 'block';
+ this.currentIndex = 0;
+ this.hidden = false;
+ }
+ },
+
+ hide: function() {
+ if (!this.hidden) {
+ // debugger
+ this.list.style.display = 'none';
+ this.currentIndex = 0;
+ this.hidden = true;
+ }
+ },
+
+ destroy: function() {
+ this.hide();
+ this.list.removeEventListener('click', this.clickWrapper);
+ }
+});
+
+module.exports = DropDown;
+
+},{"./custom_event_polyfill":2,"./utils":10}],4:[function(require,module,exports){
+require('./window')(function(w){
+ module.exports = function(deps) {
+ deps = deps || {};
+ var window = deps.window || w;
+ var document = deps.document || window.document;
+ var CustomEvent = deps.CustomEvent || require('./custom_event_polyfill');
+ var HookButton = deps.HookButton || require('./hook_button');
+ var HookInput = deps.HookInput || require('./hook_input');
+ var utils = deps.utils || require('./utils');
+ var DATA_TRIGGER = require('./constants').DATA_TRIGGER;
+
+ var DropLab = function(hook){
+ if (!(this instanceof DropLab)) return new DropLab(hook);
+ this.ready = false;
+ this.hooks = [];
+ this.queuedData = [];
+ this.config = {};
+ this.loadWrapper;
+ if(typeof hook !== 'undefined'){
+ this.addHook(hook);
+ }
+ };
+
+
+ Object.assign(DropLab.prototype, {
+ load: function() {
+ this.loadWrapper();
+ },
+
+ loadWrapper: function(){
+ var dropdownTriggers = [].slice.apply(document.querySelectorAll('['+DATA_TRIGGER+']'));
+ this.addHooks(dropdownTriggers).init();
+ },
+
+ addData: function () {
+ var args = [].slice.apply(arguments);
+ this.applyArgs(args, '_addData');
+ },
+
+ setData: function() {
+ var args = [].slice.apply(arguments);
+ this.applyArgs(args, '_setData');
+ },
+
+ destroy: function() {
+ for(var i = 0; i < this.hooks.length; i++) {
+ this.hooks[i].destroy();
+ }
+ this.hooks = [];
+ this.removeEvents();
+ },
+
+ applyArgs: function(args, methodName) {
+ if(this.ready) {
+ this[methodName].apply(this, args);
+ } else {
+ this.queuedData = this.queuedData || [];
+ this.queuedData.push(args);
+ }
+ },
+
+ _addData: function(trigger, data) {
+ this._processData(trigger, data, 'addData');
+ },
+
+ _setData: function(trigger, data) {
+ this._processData(trigger, data, 'setData');
+ },
+
+ _processData: function(trigger, data, methodName) {
+ for(var i = 0; i < this.hooks.length; i++) {
+ var hook = this.hooks[i];
+ if(hook.trigger.dataset.hasOwnProperty('id')) {
+ if(hook.trigger.dataset.id === trigger) {
+ hook.list[methodName](data);
+ }
+ }
+ }
+ },
+
+ addEvents: function() {
+ var self = this;
+ this.windowClickedWrapper = function(e){
+ var thisTag = e.target;
+ if(thisTag.tagName !== 'UL'){
+ // climb up the tree to find the UL
+ thisTag = utils.closest(thisTag, 'UL');
+ }
+ if(utils.isDropDownParts(thisTag)){ return }
+ if(utils.isDropDownParts(e.target)){ return }
+ for(var i = 0; i < self.hooks.length; i++) {
+ self.hooks[i].list.hide();
+ }
+ }.bind(this);
+ document.addEventListener('click', this.windowClickedWrapper);
+ },
+
+ removeEvents: function(){
+ w.removeEventListener('click', this.windowClickedWrapper);
+ w.removeEventListener('load', this.loadWrapper);
+ },
+
+ changeHookList: function(trigger, list, plugins, config) {
+ trigger = document.querySelector('[data-id="'+trigger+'"]');
+ // list = document.querySelector(list);
+ this.hooks.every(function(hook, i) {
+ if(hook.trigger === trigger) {
+ hook.destroy();
+ this.hooks.splice(i, 1);
+ this.addHook(trigger, list, plugins, config);
+ return false;
+ }
+ return true
+ }.bind(this));
+ },
+
+ addHook: function(hook, list, plugins, config) {
+ if(!(hook instanceof HTMLElement) && typeof hook === 'string'){
+ hook = document.querySelector(hook);
+ }
+ if(!list){
+ list = document.querySelector(hook.dataset[utils.toDataCamelCase(DATA_TRIGGER)]);
+ }
+
+ if(hook) {
+ if(hook.tagName === 'A' || hook.tagName === 'BUTTON') {
+ this.hooks.push(new HookButton(hook, list, plugins, config));
+ } else if(hook.tagName === 'INPUT') {
+ this.hooks.push(new HookInput(hook, list, plugins, config));
+ }
+ }
+ return this;
+ },
+
+ addHooks: function(hooks, plugins, config) {
+ for(var i = 0; i < hooks.length; i++) {
+ var hook = hooks[i];
+ this.addHook(hook, null, plugins, config);
+ }
+ return this;
+ },
+
+ setConfig: function(obj){
+ this.config = obj;
+ },
+
+ init: function () {
+ this.addEvents();
+ var readyEvent = new CustomEvent('ready.dl', {
+ detail: {
+ dropdown: this,
+ },
+ });
+ window.dispatchEvent(readyEvent);
+ this.ready = true;
+ for(var i = 0; i < this.queuedData.length; i++) {
+ this.addData.apply(this, this.queuedData[i]);
+ }
+ this.queuedData = [];
+ return this;
+ },
+ });
+
+ return DropLab;
+ };
+});
+
+},{"./constants":1,"./custom_event_polyfill":2,"./hook_button":6,"./hook_input":7,"./utils":10,"./window":11}],5:[function(require,module,exports){
+var DropDown = require('./dropdown');
+
+var Hook = function(trigger, list, plugins, config){
+ this.trigger = trigger;
+ this.list = new DropDown(list);
+ this.type = 'Hook';
+ this.event = 'click';
+ this.plugins = plugins || [];
+ this.config = config || {};
+ this.id = trigger.dataset.id;
+};
+
+Object.assign(Hook.prototype, {
+
+ addEvents: function(){},
+
+ constructor: Hook,
+});
+
+module.exports = Hook;
+
+},{"./dropdown":3}],6:[function(require,module,exports){
+var CustomEvent = require('./custom_event_polyfill');
+var Hook = require('./hook');
+
+var HookButton = function(trigger, list, plugins, config) {
+ Hook.call(this, trigger, list, plugins, config);
+ this.type = 'button';
+ this.event = 'click';
+ this.addEvents();
+ this.addPlugins();
+};
+
+HookButton.prototype = Object.create(Hook.prototype);
+
+Object.assign(HookButton.prototype, {
+ addPlugins: function() {
+ for(var i = 0; i < this.plugins.length; i++) {
+ this.plugins[i].init(this);
+ }
+ },
+
+ clicked: function(e){
+ var buttonEvent = new CustomEvent('click.dl', {
+ detail: {
+ hook: this,
+ },
+ bubbles: true,
+ cancelable: true
+ });
+ this.list.show();
+ e.target.dispatchEvent(buttonEvent);
+ },
+
+ addEvents: function(){
+ this.clickedWrapper = this.clicked.bind(this);
+ this.trigger.addEventListener('click', this.clickedWrapper);
+ },
+
+ removeEvents: function(){
+ this.trigger.removeEventListener('click', this.clickedWrapper);
+ },
+
+ restoreInitialState: function() {
+ this.list.list.innerHTML = this.list.initialState;
+ },
+
+ removePlugins: function() {
+ for(var i = 0; i < this.plugins.length; i++) {
+ this.plugins[i].destroy();
+ }
+ },
+
+ destroy: function() {
+ this.restoreInitialState();
+ this.removeEvents();
+ this.removePlugins();
+ },
+
+
+ constructor: HookButton,
+});
+
+
+module.exports = HookButton;
+
+},{"./custom_event_polyfill":2,"./hook":5}],7:[function(require,module,exports){
+var CustomEvent = require('./custom_event_polyfill');
+var Hook = require('./hook');
+
+var HookInput = function(trigger, list, plugins, config) {
+ Hook.call(this, trigger, list, plugins, config);
+ this.type = 'input';
+ this.event = 'input';
+ this.addPlugins();
+ this.addEvents();
+};
+
+Object.assign(HookInput.prototype, {
+ addPlugins: function() {
+ var self = this;
+ for(var i = 0; i < this.plugins.length; i++) {
+ this.plugins[i].init(self);
+ }
+ },
+
+ addEvents: function(){
+ var self = this;
+
+ this.mousedown = function mousedown(e) {
+ if(self.hasRemovedEvents) return;
+
+ var mouseEvent = new CustomEvent('mousedown.dl', {
+ detail: {
+ hook: self,
+ text: e.target.value,
+ },
+ bubbles: true,
+ cancelable: true
+ });
+ e.target.dispatchEvent(mouseEvent);
+ }
+
+ this.input = function input(e) {
+ if(self.hasRemovedEvents) return;
+
+ self.list.show();
+
+ var inputEvent = new CustomEvent('input.dl', {
+ detail: {
+ hook: self,
+ text: e.target.value,
+ },
+ bubbles: true,
+ cancelable: true
+ });
+ e.target.dispatchEvent(inputEvent);
+ }
+
+ this.keyup = function keyup(e) {
+ if(self.hasRemovedEvents) return;
+
+ keyEvent(e, 'keyup.dl');
+ }
+
+ this.keydown = function keydown(e) {
+ if(self.hasRemovedEvents) return;
+
+ keyEvent(e, 'keydown.dl');
+ }
+
+ function keyEvent(e, keyEventName){
+ self.list.show();
+
+ var keyEvent = new CustomEvent(keyEventName, {
+ detail: {
+ hook: self,
+ text: e.target.value,
+ which: e.which,
+ key: e.key,
+ },
+ bubbles: true,
+ cancelable: true
+ });
+ e.target.dispatchEvent(keyEvent);
+ }
+
+ this.events = this.events || {};
+ this.events.mousedown = this.mousedown;
+ this.events.input = this.input;
+ this.events.keyup = this.keyup;
+ this.events.keydown = this.keydown;
+ this.trigger.addEventListener('mousedown', this.mousedown);
+ this.trigger.addEventListener('input', this.input);
+ this.trigger.addEventListener('keyup', this.keyup);
+ this.trigger.addEventListener('keydown', this.keydown);
+ },
+
+ removeEvents: function() {
+ this.hasRemovedEvents = true;
+ this.trigger.removeEventListener('mousedown', this.mousedown);
+ this.trigger.removeEventListener('input', this.input);
+ this.trigger.removeEventListener('keyup', this.keyup);
+ this.trigger.removeEventListener('keydown', this.keydown);
+ },
+
+ restoreInitialState: function() {
+ this.list.list.innerHTML = this.list.initialState;
+ },
+
+ removePlugins: function() {
+ for(var i = 0; i < this.plugins.length; i++) {
+ this.plugins[i].destroy();
+ }
+ },
+
+ destroy: function() {
+ this.restoreInitialState();
+ this.removeEvents();
+ this.removePlugins();
+ this.list.destroy();
+ }
+});
+
+module.exports = HookInput;
+
+},{"./custom_event_polyfill":2,"./hook":5}],8:[function(require,module,exports){
+var DropLab = require('./droplab')();
+var DATA_TRIGGER = require('./constants').DATA_TRIGGER;
+var keyboard = require('./keyboard')();
+var setup = function() {
+ window.DropLab = DropLab;
+};
+
+
+module.exports = setup();
+
+},{"./constants":1,"./droplab":4,"./keyboard":9}],9:[function(require,module,exports){
+require('./window')(function(w){
+ module.exports = function(){
+ var currentKey;
+ var currentFocus;
+ var isUpArrow = false;
+ var isDownArrow = false;
+ var removeHighlight = function removeHighlight(list) {
+ var listItems = Array.prototype.slice.call(list.list.querySelectorAll('li:not(.divider)'), 0);
+ var listItemsTmp = [];
+ for(var i = 0; i < listItems.length; i++) {
+ var listItem = listItems[i];
+ listItem.classList.remove('dropdown-active');
+
+ if (listItem.style.display !== 'none') {
+ listItemsTmp.push(listItem);
+ }
+ }
+ return listItemsTmp;
+ };
+
+ var setMenuForArrows = function setMenuForArrows(list) {
+ var listItems = removeHighlight(list);
+ if(list.currentIndex>0){
+ if(!listItems[list.currentIndex-1]){
+ list.currentIndex = list.currentIndex-1;
+ }
+
+ if (listItems[list.currentIndex-1]) {
+ var el = listItems[list.currentIndex-1];
+ var filterDropdownEl = el.closest('.filter-dropdown');
+ el.classList.add('dropdown-active');
+
+ if (filterDropdownEl) {
+ var filterDropdownBottom = filterDropdownEl.offsetHeight;
+ var elOffsetTop = el.offsetTop - 30;
+
+ if (elOffsetTop > filterDropdownBottom) {
+ filterDropdownEl.scrollTop = elOffsetTop - filterDropdownBottom;
+ }
+ }
+ }
+ }
+ };
+
+ var mousedown = function mousedown(e) {
+ var list = e.detail.hook.list;
+ removeHighlight(list);
+ list.show();
+ list.currentIndex = 0;
+ isUpArrow = false;
+ isDownArrow = false;
+ };
+ var selectItem = function selectItem(list) {
+ var listItems = removeHighlight(list);
+ var currentItem = listItems[list.currentIndex-1];
+ var listEvent = new CustomEvent('click.dl', {
+ detail: {
+ list: list,
+ selected: currentItem,
+ data: currentItem.dataset,
+ },
+ });
+ list.list.dispatchEvent(listEvent);
+ list.hide();
+ }
+
+ var keydown = function keydown(e){
+ var typedOn = e.target;
+ var list = e.detail.hook.list;
+ var currentIndex = list.currentIndex;
+ isUpArrow = false;
+ isDownArrow = false;
+
+ if(e.detail.which){
+ currentKey = e.detail.which;
+ if(currentKey === 13){
+ selectItem(e.detail.hook.list);
+ return;
+ }
+ if(currentKey === 38) {
+ isUpArrow = true;
+ }
+ if(currentKey === 40) {
+ isDownArrow = true;
+ }
+ } else if(e.detail.key) {
+ currentKey = e.detail.key;
+ if(currentKey === 'Enter'){
+ selectItem(e.detail.hook.list);
+ return;
+ }
+ if(currentKey === 'ArrowUp') {
+ isUpArrow = true;
+ }
+ if(currentKey === 'ArrowDown') {
+ isDownArrow = true;
+ }
+ }
+ if(isUpArrow){ currentIndex--; }
+ if(isDownArrow){ currentIndex++; }
+ if(currentIndex < 0){ currentIndex = 0; }
+ list.currentIndex = currentIndex;
+ setMenuForArrows(e.detail.hook.list);
+ };
+
+ w.addEventListener('mousedown.dl', mousedown);
+ w.addEventListener('keydown.dl', keydown);
+ };
+});
+},{"./window":11}],10:[function(require,module,exports){
+var DATA_TRIGGER = require('./constants').DATA_TRIGGER;
+var DATA_DROPDOWN = require('./constants').DATA_DROPDOWN;
+
+var toDataCamelCase = function(attr){
+ return this.camelize(attr.split('-').slice(1).join(' '));
+};
+
+// the tiniest damn templating I can do
+var t = function(s,d){
+ for(var p in d)
+ s=s.replace(new RegExp('{{'+p+'}}','g'), d[p]);
+ return s;
+};
+
+var camelize = function(str) {
+ return str.replace(/(?:^\w|[A-Z]|\b\w)/g, function(letter, index) {
+ return index == 0 ? letter.toLowerCase() : letter.toUpperCase();
+ }).replace(/\s+/g, '');
+};
+
+var closest = function(thisTag, stopTag) {
+ while(thisTag && thisTag.tagName !== stopTag && thisTag.tagName !== 'HTML'){
+ thisTag = thisTag.parentNode;
+ }
+ return thisTag;
+};
+
+var isDropDownParts = function(target) {
+ if(!target || target.tagName === 'HTML') { return false; }
+ return (
+ target.hasAttribute(DATA_TRIGGER) ||
+ target.hasAttribute(DATA_DROPDOWN)
+ );
+};
+
+module.exports = {
+ toDataCamelCase: toDataCamelCase,
+ t: t,
+ camelize: camelize,
+ closest: closest,
+ isDropDownParts: isDropDownParts,
+};
+
+},{"./constants":1}],11:[function(require,module,exports){
+module.exports = function(callback) {
+ return (function() {
+ callback(this);
+ }).call(null);
+};
+
+},{}]},{},[8])(8)
+});
diff --git a/app/assets/javascripts/droplab/droplab_ajax.js b/app/assets/javascripts/droplab/droplab_ajax.js
new file mode 100644
index 00000000000..5cdf11c6a2c
--- /dev/null
+++ b/app/assets/javascripts/droplab/droplab_ajax.js
@@ -0,0 +1,96 @@
+/* eslint-disable */
+(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g=(g.droplab||(g.droplab = {}));g=(g.ajax||(g.ajax = {}));g=(g.datasource||(g.datasource = {}));g.js = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
+/* global droplab */
+
+require('../window')(function(w){
+ function droplabAjaxException(message) {
+ this.message = message;
+ }
+
+ w.droplabAjax = {
+ _loadUrlData: function _loadUrlData(url) {
+ var self = this;
+ return new Promise(function(resolve, reject) {
+ var xhr = new XMLHttpRequest;
+ xhr.open('GET', url, true);
+ xhr.onreadystatechange = function () {
+ if(xhr.readyState === XMLHttpRequest.DONE) {
+ if (xhr.status === 200) {
+ var data = JSON.parse(xhr.responseText);
+ self.cache[url] = data;
+ return resolve(data);
+ } else {
+ return reject([xhr.responseText, xhr.status]);
+ }
+ }
+ };
+ xhr.send();
+ });
+ },
+
+ _loadData: function _loadData(data, config, self) {
+ if (config.loadingTemplate) {
+ var dataLoadingTemplate = self.hook.list.list.querySelector('[data-loading-template]');
+
+ if (dataLoadingTemplate) {
+ dataLoadingTemplate.outerHTML = self.listTemplate;
+ }
+ }
+
+ self.hook.list[config.method].call(self.hook.list, data);
+ },
+
+ init: function init(hook) {
+ var self = this;
+ self.cache = self.cache || {};
+ var config = hook.config.droplabAjax;
+ this.hook = hook;
+
+ if (!config || !config.endpoint || !config.method) {
+ return;
+ }
+
+ if (config.method !== 'setData' && config.method !== 'addData') {
+ return;
+ }
+
+ if (config.loadingTemplate) {
+ var dynamicList = hook.list.list.querySelector('[data-dynamic]');
+
+ var loadingTemplate = document.createElement('div');
+ loadingTemplate.innerHTML = config.loadingTemplate;
+ loadingTemplate.setAttribute('data-loading-template', '');
+
+ this.listTemplate = dynamicList.outerHTML;
+ dynamicList.outerHTML = loadingTemplate.outerHTML;
+ }
+
+ if (self.cache[config.endpoint]) {
+ self._loadData(self.cache[config.endpoint], config, self);
+ } else {
+ this._loadUrlData(config.endpoint)
+ .then(function(d) {
+ self._loadData(d, config, self);
+ }).catch(function(e) {
+ throw new droplabAjaxException(e.message || e);
+ });
+ }
+ },
+
+ destroy: function() {
+ var dynamicList = this.hook.list.list.querySelector('[data-dynamic]');
+ if (this.listTemplate && dynamicList) {
+ dynamicList.outerHTML = this.listTemplate;
+ }
+ }
+ };
+});
+},{"../window":2}],2:[function(require,module,exports){
+module.exports = function(callback) {
+ return (function() {
+ callback(this);
+ }).call(null);
+};
+
+},{}]},{},[1])(1)
+});
diff --git a/app/assets/javascripts/droplab/droplab_ajax_filter.js b/app/assets/javascripts/droplab/droplab_ajax_filter.js
new file mode 100644
index 00000000000..b63d73066cb
--- /dev/null
+++ b/app/assets/javascripts/droplab/droplab_ajax_filter.js
@@ -0,0 +1,161 @@
+/* eslint-disable */
+(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g=(g.droplab||(g.droplab = {}));g=(g.ajax||(g.ajax = {}));g=(g.datasource||(g.datasource = {}));g.js = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
+/* global droplab */
+
+require('../window')(function(w){
+ w.droplabAjaxFilter = {
+ init: function(hook) {
+ this.destroyed = false;
+ this.hook = hook;
+ this.notLoading();
+
+ this.debounceTriggerWrapper = this.debounceTrigger.bind(this);
+ this.hook.trigger.addEventListener('keydown.dl', this.debounceTriggerWrapper);
+ this.hook.trigger.addEventListener('focus', this.debounceTriggerWrapper);
+ this.trigger(true);
+ },
+
+ notLoading: function notLoading() {
+ this.loading = false;
+ },
+
+ debounceTrigger: function debounceTrigger(e) {
+ var NON_CHARACTER_KEYS = [16, 17, 18, 20, 37, 38, 39, 40, 91, 93];
+ var invalidKeyPressed = NON_CHARACTER_KEYS.indexOf(e.detail.which || e.detail.keyCode) > -1;
+ var focusEvent = e.type === 'focus';
+
+ if (invalidKeyPressed || this.loading) {
+ return;
+ }
+
+ if (this.timeout) {
+ clearTimeout(this.timeout);
+ }
+
+ this.timeout = setTimeout(this.trigger.bind(this, focusEvent), 200);
+ },
+
+ trigger: function trigger(getEntireList) {
+ var config = this.hook.config.droplabAjaxFilter;
+ var searchValue = this.trigger.value;
+
+ if (!config || !config.endpoint || !config.searchKey) {
+ return;
+ }
+
+ if (config.searchValueFunction) {
+ searchValue = config.searchValueFunction();
+ }
+
+ if (config.loadingTemplate && this.hook.list.data === undefined ||
+ this.hook.list.data.length === 0) {
+ var dynamicList = this.hook.list.list.querySelector('[data-dynamic]');
+
+ var loadingTemplate = document.createElement('div');
+ loadingTemplate.innerHTML = config.loadingTemplate;
+ loadingTemplate.setAttribute('data-loading-template', true);
+
+ this.listTemplate = dynamicList.outerHTML;
+ dynamicList.outerHTML = loadingTemplate.outerHTML;
+ }
+
+ if (getEntireList) {
+ searchValue = '';
+ }
+
+ if (config.searchKey === searchValue) {
+ return this.list.show();
+ }
+
+ this.loading = true;
+
+ var params = config.params || {};
+ params[config.searchKey] = searchValue;
+ var self = this;
+ self.cache = self.cache || {};
+ var url = config.endpoint + this.buildParams(params);
+ var urlCachedData = self.cache[url];
+
+ if (urlCachedData) {
+ self._loadData(urlCachedData, config, self);
+ } else {
+ this._loadUrlData(url)
+ .then(function(data) {
+ self._loadData(data, config, self);
+ });
+ }
+ },
+
+ _loadUrlData: function _loadUrlData(url) {
+ var self = this;
+ return new Promise(function(resolve, reject) {
+ var xhr = new XMLHttpRequest;
+ xhr.open('GET', url, true);
+ xhr.onreadystatechange = function () {
+ if(xhr.readyState === XMLHttpRequest.DONE) {
+ if (xhr.status === 200) {
+ var data = JSON.parse(xhr.responseText);
+ self.cache[url] = data;
+ return resolve(data);
+ } else {
+ return reject([xhr.responseText, xhr.status]);
+ }
+ }
+ };
+ xhr.send();
+ });
+ },
+
+ _loadData: function _loadData(data, config, self) {
+ if (config.loadingTemplate && self.hook.list.data === undefined ||
+ self.hook.list.data.length === 0) {
+ const dataLoadingTemplate = self.hook.list.list.querySelector('[data-loading-template]');
+
+ if (dataLoadingTemplate) {
+ dataLoadingTemplate.outerHTML = self.listTemplate;
+ }
+ }
+
+ if (!self.destroyed) {
+ var hookListChildren = self.hook.list.list.children;
+ var onlyDynamicList = hookListChildren.length === 1 && hookListChildren[0].hasAttribute('data-dynamic');
+
+ if (onlyDynamicList && data.length === 0) {
+ self.hook.list.hide();
+ }
+
+ self.hook.list.setData.call(self.hook.list, data);
+ }
+ self.notLoading();
+ self.hook.list.currentIndex = 0;
+ },
+
+ buildParams: function(params) {
+ if (!params) return '';
+ var paramsArray = Object.keys(params).map(function(param) {
+ return param + '=' + (params[param] || '');
+ });
+ return '?' + paramsArray.join('&');
+ },
+
+ destroy: function destroy() {
+ if (this.timeout) {
+ clearTimeout(this.timeout);
+ }
+
+ this.destroyed = true;
+
+ this.hook.trigger.removeEventListener('keydown.dl', this.debounceTriggerWrapper);
+ this.hook.trigger.removeEventListener('focus', this.debounceTriggerWrapper);
+ }
+ };
+});
+},{"../window":2}],2:[function(require,module,exports){
+module.exports = function(callback) {
+ return (function() {
+ callback(this);
+ }).call(null);
+};
+
+},{}]},{},[1])(1)
+});
diff --git a/app/assets/javascripts/droplab/droplab_filter.js b/app/assets/javascripts/droplab/droplab_filter.js
new file mode 100644
index 00000000000..9b40a3f20a4
--- /dev/null
+++ b/app/assets/javascripts/droplab/droplab_filter.js
@@ -0,0 +1,74 @@
+/* eslint-disable */
+(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g=(g.droplab||(g.droplab = {}));g=(g.filter||(g.filter = {}));g.js = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
+/* global droplab */
+
+require('../window')(function(w){
+ w.droplabFilter = {
+
+ keydownWrapper: function(e){
+ var hiddenCount = 0;
+ var dataHiddenCount = 0;
+ var list = e.detail.hook.list;
+ var data = list.data;
+ var value = e.detail.hook.trigger.value.toLowerCase();
+ var config = e.detail.hook.config.droplabFilter;
+ var matches = [];
+ var filterFunction;
+ // will only work on dynamically set data
+ if(!data){
+ return;
+ }
+
+ if (config && config.filterFunction && typeof config.filterFunction === 'function') {
+ filterFunction = config.filterFunction;
+ } else {
+ filterFunction = function(o){
+ // cheap string search
+ o.droplab_hidden = o[config.template].toLowerCase().indexOf(value) === -1;
+ return o;
+ };
+ }
+
+ dataHiddenCount = data.filter(function(o) {
+ return !o.droplab_hidden;
+ }).length;
+
+ matches = data.map(function(o) {
+ return filterFunction(o, value);
+ });
+
+ hiddenCount = matches.filter(function(o) {
+ return !o.droplab_hidden;
+ }).length;
+
+ if (dataHiddenCount !== hiddenCount) {
+ list.render(matches);
+ list.currentIndex = 0;
+ }
+ },
+
+ init: function init(hookInput) {
+ var config = hookInput.config.droplabFilter;
+
+ if (!config || (!config.template && !config.filterFunction)) {
+ return;
+ }
+
+ this.hookInput = hookInput;
+ this.hookInput.trigger.addEventListener('keyup.dl', this.keydownWrapper);
+ },
+
+ destroy: function destroy(){
+ this.hookInput.trigger.removeEventListener('keyup.dl', this.keydownWrapper);
+ }
+ };
+});
+},{"../window":2}],2:[function(require,module,exports){
+module.exports = function(callback) {
+ return (function() {
+ callback(this);
+ }).call(null);
+};
+
+},{}]},{},[1])(1)
+});
diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js
index 56cb39be642..a510eebae1a 100644
--- a/app/assets/javascripts/dropzone_input.js
+++ b/app/assets/javascripts/dropzone_input.js
@@ -1,7 +1,7 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, max-len, one-var, no-var, one-var-declaration-per-line, no-unused-vars, camelcase, quotes, no-useless-concat, prefer-template, quote-props, comma-dangle, object-shorthand, consistent-return, no-plusplus, prefer-arrow-callback, padded-blocks */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, max-len, one-var, no-var, one-var-declaration-per-line, no-unused-vars, camelcase, quotes, no-useless-concat, prefer-template, quote-props, comma-dangle, object-shorthand, consistent-return, prefer-arrow-callback */
/* global Dropzone */
-/*= require preview_markdown */
+require('./preview_markdown');
(function() {
this.DropzoneInput = (function() {
@@ -120,7 +120,7 @@
if (item.type.indexOf("image") !== -1) {
return item;
}
- i++;
+ i += 1;
}
return false;
};
@@ -215,7 +215,5 @@
}
return DropzoneInput;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/due_date_select.js.es6 b/app/assets/javascripts/due_date_select.js.es6
index 2b7d57d86c6..d81d4cf8425 100644
--- a/app/assets/javascripts/due_date_select.js.es6
+++ b/app/assets/javascripts/due_date_select.js.es6
@@ -1,4 +1,4 @@
-/* eslint-disable wrap-iife, func-names, space-before-function-paren, comma-dangle, prefer-template, consistent-return, class-methods-use-this, arrow-body-style, prefer-const, padded-blocks, no-unused-vars, no-underscore-dangle, no-new, max-len, semi, no-sequences, no-unused-expressions, no-param-reassign */
+/* eslint-disable wrap-iife, func-names, space-before-function-paren, comma-dangle, prefer-template, consistent-return, class-methods-use-this, arrow-body-style, no-unused-vars, no-underscore-dangle, no-new, max-len, no-sequences, no-unused-expressions, no-param-reassign */
(function(global) {
class DueDateSelect {
@@ -16,7 +16,7 @@
this.$sidebarValue = $('.js-due-date-sidebar-value', $block);
this.fieldName = $dropdown.data('field-name'),
this.abilityName = $dropdown.data('ability-name'),
- this.issueUpdateURL = $dropdown.data('issue-update')
+ this.issueUpdateURL = $dropdown.data('issue-update');
this.rawSelectedDate = null;
this.displayedDate = null;
@@ -80,9 +80,12 @@
}
parseSelectedDate() {
- this.rawSelectedDate = $("input[name='" + this.fieldName + "']").val();
+ this.rawSelectedDate = $(`input[name='${this.fieldName}']`).val();
+
if (this.rawSelectedDate.length) {
- let dateObj = new Date(this.rawSelectedDate);
+ // Construct Date object manually to avoid buggy dateString support within Date constructor
+ const dateArray = this.rawSelectedDate.split('-').map(v => parseInt(v, 10));
+ const dateObj = new Date(dateArray[0], dateArray[1] - 1, dateArray[2]);
this.displayedDate = $.datepicker.formatDate('M d, yy', dateObj);
} else {
this.displayedDate = 'No due date';
@@ -132,7 +135,6 @@
return selectedDateValue.length ?
$('.js-remove-due-date-holder').removeClass('hidden') :
$('.js-remove-due-date-holder').addClass('hidden');
-
}
}).done((data) => {
if (isDropdown) {
@@ -176,5 +178,4 @@
}
global.DueDateSelectors = DueDateSelectors;
-
})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/environments/components/environment.js.es6 b/app/assets/javascripts/environments/components/environment.js.es6
index facd653fd72..91553bda4dc 100644
--- a/app/assets/javascripts/environments/components/environment.js.es6
+++ b/app/assets/javascripts/environments/components/environment.js.es6
@@ -1,51 +1,17 @@
-/* eslint-disable no-param-reassign */
+/* eslint-disable no-param-reassign, no-new */
/* global Vue */
/* global EnvironmentsService */
+/* global Flash */
-//= require vue
-//= require vue-resource
-//= require_tree ../services/
-//= require ./environment_item
+window.Vue = require('vue');
+window.Vue.use(require('vue-resource'));
+require('../services/environments_service');
+require('./environment_item');
(() => {
window.gl = window.gl || {};
- /**
- * Given the visibility prop provided by the url query parameter and which
- * changes according to the active tab we need to filter which environments
- * should be visible.
- *
- * The environments array is a recursive tree structure and we need to filter
- * both root level environments and children environments.
- *
- * In order to acomplish that, both `filterState` and `filterEnvironmentsByState`
- * functions work together.
- * The first one works as the filter that verifies if the given environment matches
- * the given state.
- * The second guarantees both root level and children elements are filtered as well.
- */
-
- const filterState = state => environment => environment.state === state && environment;
- /**
- * Given the filter function and the array of environments will return only
- * the environments that match the state provided to the filter function.
- *
- * @param {Function} fn
- * @param {Array} array
- * @return {Array}
- */
- const filterEnvironmentsByState = (fn, arr) => arr.map((item) => {
- if (item.children) {
- const filteredChildren = filterEnvironmentsByState(fn, item.children).filter(Boolean);
- if (filteredChildren.length) {
- item.children = filteredChildren;
- return item;
- }
- }
- return fn(item);
- }).filter(Boolean);
-
- window.gl.environmentsList.EnvironmentsComponent = Vue.component('environment-component', {
+ gl.environmentsList.EnvironmentsComponent = Vue.component('environment-component', {
props: {
store: {
type: Object,
@@ -55,7 +21,7 @@
},
components: {
- 'environment-item': window.gl.environmentsList.EnvironmentItem,
+ 'environment-item': gl.environmentsList.EnvironmentItem,
},
data() {
@@ -81,10 +47,6 @@
},
computed: {
- filteredEnvironments() {
- return filterEnvironmentsByState(filterState(this.visibility), this.state.environments);
- },
-
scope() {
return this.$options.getQueryParameter('scope');
},
@@ -111,7 +73,7 @@
const scope = this.$options.getQueryParameter('scope');
if (scope) {
- this.visibility = scope;
+ this.store.storeVisibility(scope);
}
this.isLoading = true;
@@ -121,6 +83,10 @@
.then((json) => {
this.store.storeEnvironments(json);
this.isLoading = false;
+ })
+ .catch(() => {
+ this.isLoading = false;
+ new Flash('An error occurred while fetching the environments.', 'alert');
});
},
@@ -188,7 +154,7 @@
<div class="blank-state blank-state-no-icon"
v-if="!isLoading && state.environments.length === 0">
- <h2 class="blank-state-title">
+ <h2 class="blank-state-title js-blank-state-title">
You don't have any environments right now.
</h2>
<p class="blank-state-text">
@@ -202,26 +168,26 @@
<a
v-if="canCreateEnvironmentParsed"
:href="newEnvironmentPath"
- class="btn btn-create">
+ class="btn btn-create js-new-environment-button">
New Environment
</a>
</div>
<div class="table-holder"
- v-if="!isLoading && state.environments.length > 0">
+ v-if="!isLoading && state.filteredEnvironments.length > 0">
<table class="table ci-table environments">
<thead>
<tr>
<th class="environments-name">Environment</th>
<th class="environments-deploy">Last deployment</th>
- <th class="environments-build">Build</th>
+ <th class="environments-build">Job</th>
<th class="environments-commit">Commit</th>
- <th class="environments-date"></th>
+ <th class="environments-date">Updated</th>
<th class="hidden-xs environments-actions"></th>
</tr>
</thead>
<tbody>
- <template v-for="model in filteredEnvironments"
+ <template v-for="model in state.filteredEnvironments"
v-bind:model="model">
<tr
diff --git a/app/assets/javascripts/environments/components/environment_actions.js.es6 b/app/assets/javascripts/environments/components/environment_actions.js.es6
index 7c743705d51..ed1c78945db 100644
--- a/app/assets/javascripts/environments/components/environment_actions.js.es6
+++ b/app/assets/javascripts/environments/components/environment_actions.js.es6
@@ -1,11 +1,12 @@
-/*= require vue */
/* global Vue */
+window.Vue = require('vue');
+
(() => {
window.gl = window.gl || {};
window.gl.environmentsList = window.gl.environmentsList || {};
- window.gl.environmentsList.ActionsComponent = Vue.component('actions-component', {
+ gl.environmentsList.ActionsComponent = Vue.component('actions-component', {
props: {
actions: {
type: Array,
diff --git a/app/assets/javascripts/environments/components/environment_external_url.js.es6 b/app/assets/javascripts/environments/components/environment_external_url.js.es6
index aed65b33c04..28cc0022d17 100644
--- a/app/assets/javascripts/environments/components/environment_external_url.js.es6
+++ b/app/assets/javascripts/environments/components/environment_external_url.js.es6
@@ -1,11 +1,12 @@
-/*= require vue */
/* global Vue */
+window.Vue = require('vue');
+
(() => {
window.gl = window.gl || {};
window.gl.environmentsList = window.gl.environmentsList || {};
- window.gl.environmentsList.ExternalUrlComponent = Vue.component('external-url-component', {
+ gl.environmentsList.ExternalUrlComponent = Vue.component('external-url-component', {
props: {
externalUrl: {
type: String,
diff --git a/app/assets/javascripts/environments/components/environment_item.js.es6 b/app/assets/javascripts/environments/components/environment_item.js.es6
index b26a40aa268..33a99231315 100644
--- a/app/assets/javascripts/environments/components/environment_item.js.es6
+++ b/app/assets/javascripts/environments/components/environment_item.js.es6
@@ -1,14 +1,15 @@
/* global Vue */
/* global timeago */
-/*= require timeago */
-/*= require lib/utils/text_utility */
-/*= require vue_common_component/commit */
-/*= require ./environment_actions */
-/*= require ./environment_external_url */
-/*= require ./environment_stop */
-/*= require ./environment_rollback */
-/*= require ./environment_terminal_button */
+window.Vue = require('vue');
+window.timeago = require('vendor/timeago');
+require('../../lib/utils/text_utility');
+require('../../vue_shared/components/commit');
+require('./environment_actions');
+require('./environment_external_url');
+require('./environment_stop');
+require('./environment_rollback');
+require('./environment_terminal_button');
(() => {
/**
@@ -29,12 +30,12 @@
gl.environmentsList.EnvironmentItem = Vue.component('environment-item', {
components: {
- 'commit-component': window.gl.CommitComponent,
- 'actions-component': window.gl.environmentsList.ActionsComponent,
- 'external-url-component': window.gl.environmentsList.ExternalUrlComponent,
- 'stop-component': window.gl.environmentsList.StopComponent,
- 'rollback-component': window.gl.environmentsList.RollbackComponent,
- 'terminal-button-component': window.gl.environmentsList.TerminalButtonComponent,
+ 'commit-component': gl.CommitComponent,
+ 'actions-component': gl.environmentsList.ActionsComponent,
+ 'external-url-component': gl.environmentsList.ExternalUrlComponent,
+ 'stop-component': gl.environmentsList.StopComponent,
+ 'rollback-component': gl.environmentsList.RollbackComponent,
+ 'terminal-button-component': gl.environmentsList.TerminalButtonComponent,
},
props: {
@@ -146,12 +147,12 @@
},
/**
- * Returns the value of the `stoppable?` key provided in the response.
+ * Returns the value of the `stop_action?` key provided in the response.
*
* @returns {Boolean}
*/
- isStoppable() {
- return this.model['stoppable?'];
+ hasStopAction() {
+ return this.model['stop_action?'];
},
/**
@@ -183,7 +184,7 @@
* @returns {String}
*/
createdDate() {
- return window.gl.environmentsList.timeagoInstance.format(
+ return gl.environmentsList.timeagoInstance.format(
this.model.last_deployment.deployable.created_at,
);
},
@@ -507,7 +508,7 @@
</external-url-component>
</div>
- <div v-if="isStoppable && canCreateDeployment"
+ <div v-if="hasStopAction && canCreateDeployment"
class="inline js-stop-component-container">
<stop-component
:stop-url="model.stop_path">
diff --git a/app/assets/javascripts/environments/components/environment_rollback.js.es6 b/app/assets/javascripts/environments/components/environment_rollback.js.es6
index 6d4e8fad604..5938340a128 100644
--- a/app/assets/javascripts/environments/components/environment_rollback.js.es6
+++ b/app/assets/javascripts/environments/components/environment_rollback.js.es6
@@ -1,11 +1,12 @@
-/*= require vue */
/* global Vue */
+window.Vue = require('vue');
+
(() => {
window.gl = window.gl || {};
window.gl.environmentsList = window.gl.environmentsList || {};
- window.gl.environmentsList.RollbackComponent = Vue.component('rollback-component', {
+ gl.environmentsList.RollbackComponent = Vue.component('rollback-component', {
props: {
retryUrl: {
type: String,
diff --git a/app/assets/javascripts/environments/components/environment_stop.js.es6 b/app/assets/javascripts/environments/components/environment_stop.js.es6
index 7292f924e5c..be9526989a0 100644
--- a/app/assets/javascripts/environments/components/environment_stop.js.es6
+++ b/app/assets/javascripts/environments/components/environment_stop.js.es6
@@ -1,11 +1,12 @@
-/*= require vue */
/* global Vue */
+window.Vue = require('vue');
+
(() => {
window.gl = window.gl || {};
window.gl.environmentsList = window.gl.environmentsList || {};
- window.gl.environmentsList.StopComponent = Vue.component('stop-component', {
+ gl.environmentsList.StopComponent = Vue.component('stop-component', {
props: {
stopUrl: {
type: String,
diff --git a/app/assets/javascripts/environments/components/environment_terminal_button.js.es6 b/app/assets/javascripts/environments/components/environment_terminal_button.js.es6
index 25e6ac7f3c9..a3ad063f7cb 100644
--- a/app/assets/javascripts/environments/components/environment_terminal_button.js.es6
+++ b/app/assets/javascripts/environments/components/environment_terminal_button.js.es6
@@ -1,11 +1,12 @@
-/*= require vue */
/* global Vue */
+window.Vue = require('vue');
+
(() => {
window.gl = window.gl || {};
window.gl.environmentsList = window.gl.environmentsList || {};
- window.gl.environmentsList.TerminalButtonComponent = Vue.component('terminal-button-component', {
+ gl.environmentsList.TerminalButtonComponent = Vue.component('terminal-button-component', {
props: {
terminalPath: {
type: String,
diff --git a/app/assets/javascripts/environments/environments_bundle.js.es6 b/app/assets/javascripts/environments/environments_bundle.js.es6
index 20eee7976ec..05c59d92fd4 100644
--- a/app/assets/javascripts/environments/environments_bundle.js.es6
+++ b/app/assets/javascripts/environments/environments_bundle.js.es6
@@ -1,21 +1,22 @@
-//= require vue
-//= require_tree ./stores/
-//= require ./components/environment
-//= require ./vue_resource_interceptor
-
+window.Vue = require('vue');
+require('./stores/environments_store');
+require('./components/environment');
+require('../vue_shared/vue_resource_interceptor');
$(() => {
window.gl = window.gl || {};
- if (window.gl.EnvironmentsListApp) {
- window.gl.EnvironmentsListApp.$destroy(true);
+ if (gl.EnvironmentsListApp) {
+ gl.EnvironmentsListApp.$destroy(true);
}
- const Store = window.gl.environmentsList.EnvironmentsStore;
+ const Store = gl.environmentsList.EnvironmentsStore;
- window.gl.EnvironmentsListApp = new window.gl.environmentsList.EnvironmentsComponent({
+ gl.EnvironmentsListApp = new gl.environmentsList.EnvironmentsComponent({
el: document.querySelector('#environments-list-view'),
+
propsData: {
store: Store.create(),
},
+
});
});
diff --git a/app/assets/javascripts/environments/services/environments_service.js.es6 b/app/assets/javascripts/environments/services/environments_service.js.es6
index 15ec7b76c3d..fab8d977f58 100644
--- a/app/assets/javascripts/environments/services/environments_service.js.es6
+++ b/app/assets/javascripts/environments/services/environments_service.js.es6
@@ -1,5 +1,6 @@
/* globals Vue */
/* eslint-disable no-unused-vars, no-param-reassign */
+
class EnvironmentsService {
constructor(root) {
@@ -20,3 +21,5 @@ class EnvironmentsService {
return this.environments.get();
}
}
+
+window.EnvironmentsService = EnvironmentsService;
diff --git a/app/assets/javascripts/environments/stores/environments_store.js.es6 b/app/assets/javascripts/environments/stores/environments_store.js.es6
index 0204a903ab5..9b4090100da 100644
--- a/app/assets/javascripts/environments/stores/environments_store.js.es6
+++ b/app/assets/javascripts/environments/stores/environments_store.js.es6
@@ -10,6 +10,8 @@
this.state.environments = [];
this.state.stoppedCounter = 0;
this.state.availableCounter = 0;
+ this.state.visibility = 'available';
+ this.state.filteredEnvironments = [];
return this;
},
@@ -59,7 +61,7 @@
if (occurs.length) {
acc[acc.indexOf(occurs[0])].children.push(environment);
- acc[acc.indexOf(occurs[0])].children.sort(this.sortByName);
+ acc[acc.indexOf(occurs[0])].children.slice().sort(this.sortByName);
} else {
acc.push({
name: environment.environment_type,
@@ -73,13 +75,70 @@
}
return acc;
- }, []).sort(this.sortByName);
+ }, []).slice().sort(this.sortByName);
this.state.environments = environmentsTree;
+ this.filterEnvironmentsByVisibility(this.state.environments);
+
return environmentsTree;
},
+ storeVisibility(visibility) {
+ this.state.visibility = visibility;
+ },
+ /**
+ * Given the visibility prop provided by the url query parameter and which
+ * changes according to the active tab we need to filter which environments
+ * should be visible.
+ *
+ * The environments array is a recursive tree structure and we need to filter
+ * both root level environments and children environments.
+ *
+ * In order to acomplish that, both `filterState` and `filterEnvironmentsByVisibility`
+ * functions work together.
+ * The first one works as the filter that verifies if the given environment matches
+ * the given state.
+ * The second guarantees both root level and children elements are filtered as well.
+ *
+ * Given array of environments will return only
+ * the environments that match the state stored.
+ *
+ * @param {Array} array
+ * @return {Array}
+ */
+ filterEnvironmentsByVisibility(arr) {
+ const filteredEnvironments = arr.map((item) => {
+ if (item.children) {
+ const filteredChildren = this.filterEnvironmentsByVisibility(
+ item.children,
+ ).filter(Boolean);
+
+ if (filteredChildren.length) {
+ item.children = filteredChildren;
+ return item;
+ }
+ }
+
+ return this.filterState(this.state.visibility, item);
+ }).filter(Boolean);
+
+ this.state.filteredEnvironments = filteredEnvironments;
+ return filteredEnvironments;
+ },
+
+ /**
+ * Given the state and the environment,
+ * returns only if the environment state matches the one provided.
+ *
+ * @param {String} state
+ * @param {Object} environment
+ * @return {Object}
+ */
+ filterState(state, environment) {
+ return environment.state === state && environment;
+ },
+
/**
* Toggles folder open property given the environment type.
*
diff --git a/app/assets/javascripts/environments/vue_resource_interceptor.js.es6 b/app/assets/javascripts/environments/vue_resource_interceptor.js.es6
deleted file mode 100644
index 406bdbc1c7d..00000000000
--- a/app/assets/javascripts/environments/vue_resource_interceptor.js.es6
+++ /dev/null
@@ -1,12 +0,0 @@
-/* global Vue */
-Vue.http.interceptors.push((request, next) => {
- Vue.activeResources = Vue.activeResources ? Vue.activeResources + 1 : 1;
-
- next((response) => {
- if (typeof response.data === 'string') {
- response.data = JSON.parse(response.data); // eslint-disable-line
- }
-
- Vue.activeResources--; // eslint-disable-line
- });
-});
diff --git a/app/assets/javascripts/extensions/array.js.es6 b/app/assets/javascripts/extensions/array.js.es6
index 717566a4715..f8256a8d26d 100644
--- a/app/assets/javascripts/extensions/array.js.es6
+++ b/app/assets/javascripts/extensions/array.js.es6
@@ -1,11 +1,14 @@
-/* eslint-disable no-extend-native, func-names, space-before-function-paren, semi, space-infix-ops, max-len */
+/* eslint-disable no-extend-native, func-names, space-before-function-paren, space-infix-ops, strict, max-len */
+
+'use strict';
+
Array.prototype.first = function() {
return this[0];
-}
+};
Array.prototype.last = function() {
return this[this.length-1];
-}
+};
Array.prototype.find = Array.prototype.find || function(predicate, ...args) {
if (!this) throw new TypeError('Array.prototype.find called on null or undefined');
diff --git a/app/assets/javascripts/extensions/custom_event.js.es6 b/app/assets/javascripts/extensions/custom_event.js.es6
new file mode 100644
index 00000000000..abedae4c1c7
--- /dev/null
+++ b/app/assets/javascripts/extensions/custom_event.js.es6
@@ -0,0 +1,12 @@
+/* global CustomEvent */
+/* eslint-disable no-global-assign */
+
+// Custom event support for IE
+CustomEvent = function CustomEvent(event, parameters) {
+ const params = parameters || { bubbles: false, cancelable: false, detail: undefined };
+ const evt = document.createEvent('CustomEvent');
+ evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail);
+ return evt;
+};
+
+CustomEvent.prototype = window.Event.prototype;
diff --git a/app/assets/javascripts/extensions/element.js.es6 b/app/assets/javascripts/extensions/element.js.es6
index 3f12ad9ff9f..90ab79305a7 100644
--- a/app/assets/javascripts/extensions/element.js.es6
+++ b/app/assets/javascripts/extensions/element.js.es6
@@ -1,5 +1,5 @@
/* global Element */
-/* eslint-disable consistent-return, max-len, no-empty, no-plusplus, func-names */
+/* eslint-disable consistent-return, max-len, no-empty, func-names */
Element.prototype.closest = Element.prototype.closest || function closest(selector, selectedElement = this) {
if (!selectedElement) return;
@@ -14,7 +14,7 @@ Element.prototype.matches = Element.prototype.matches ||
Element.prototype.webkitMatchesSelector ||
function (s) {
const matches = (this.document || this.ownerDocument).querySelectorAll(s);
- let i = matches.length;
- while (--i >= 0 && matches.item(i) !== this) {}
+ let i = matches.length - 1;
+ while (i >= 0 && matches.item(i) !== this) { i -= 1; }
return i > -1;
};
diff --git a/app/assets/javascripts/extensions/jquery.js b/app/assets/javascripts/extensions/jquery.js
index cdedc865d1b..d3b58b2707a 100644
--- a/app/assets/javascripts/extensions/jquery.js
+++ b/app/assets/javascripts/extensions/jquery.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, object-shorthand, comma-dangle, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, object-shorthand, comma-dangle, max-len */
// Disable an element and add the 'disabled' Bootstrap class
(function() {
$.fn.extend({
@@ -13,5 +13,4 @@
return $(this).removeAttr('disabled').removeClass('disabled');
}
});
-
}).call(this);
diff --git a/app/assets/javascripts/files_comment_button.js b/app/assets/javascripts/files_comment_button.js
index 785f2869970..895a872568d 100644
--- a/app/assets/javascripts/files_comment_button.js
+++ b/app/assets/javascripts/files_comment_button.js
@@ -1,8 +1,8 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, max-len, one-var, one-var-declaration-per-line, quotes, prefer-template, newline-per-chained-call, comma-dangle, new-cap, no-else-return, padded-blocks, consistent-return */
+/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, max-len, one-var, one-var-declaration-per-line, quotes, prefer-template, newline-per-chained-call, comma-dangle, new-cap, no-else-return, consistent-return */
/* global FilesCommentButton */
(function() {
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+ var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
this.FilesCommentButton = (function() {
var COMMENT_BUTTON_CLASS, COMMENT_BUTTON_TEMPLATE, DEBOUNCE_TIMEOUT_DURATION, EMPTY_CELL_CLASS, LINE_COLUMN_CLASSES, LINE_CONTENT_CLASS, LINE_HOLDER_CLASS, LINE_NUMBER_CLASS, OLD_LINE_CLASS, TEXT_FILE_SELECTOR, UNFOLDABLE_LINE_CLASS;
@@ -132,7 +132,6 @@
};
return FilesCommentButton;
-
})();
$.fn.filesCommentButton = function() {
@@ -145,5 +144,4 @@
}
});
};
-
}).call(this);
diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6
new file mode 100644
index 00000000000..572c221929a
--- /dev/null
+++ b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6
@@ -0,0 +1,69 @@
+require('./filtered_search_dropdown');
+
+/* global droplabFilter */
+
+(() => {
+ class DropdownHint extends gl.FilteredSearchDropdown {
+ constructor(droplab, dropdown, input, filter) {
+ super(droplab, dropdown, input, filter);
+ this.config = {
+ droplabFilter: {
+ template: 'hint',
+ filterFunction: gl.DropdownUtils.filterHint.bind(null, input),
+ },
+ };
+ }
+
+ itemClicked(e) {
+ const { selected } = e.detail;
+
+ if (selected.tagName === 'LI') {
+ if (selected.hasAttribute('data-value')) {
+ this.dismissDropdown();
+ } else if (selected.getAttribute('data-action') === 'submit') {
+ this.dismissDropdown();
+ this.dispatchFormSubmitEvent();
+ } else {
+ const token = selected.querySelector('.js-filter-hint').innerText.trim();
+ const tag = selected.querySelector('.js-filter-tag').innerText.trim();
+
+ if (tag.length) {
+ gl.FilteredSearchDropdownManager.addWordToInput(token.replace(':', ''));
+ }
+ this.dismissDropdown();
+ this.dispatchInputEvent();
+ }
+ }
+ }
+
+ renderContent() {
+ const dropdownData = [{
+ icon: 'fa-pencil',
+ hint: 'author:',
+ tag: '&lt;@author&gt;',
+ }, {
+ icon: 'fa-user',
+ hint: 'assignee:',
+ tag: '&lt;@assignee&gt;',
+ }, {
+ icon: 'fa-clock-o',
+ hint: 'milestone:',
+ tag: '&lt;%milestone&gt;',
+ }, {
+ icon: 'fa-tag',
+ hint: 'label:',
+ tag: '&lt;~label&gt;',
+ }];
+
+ this.droplab.changeHookList(this.hookId, this.dropdown, [droplabFilter], this.config);
+ this.droplab.setData(this.hookId, dropdownData);
+ }
+
+ init() {
+ this.droplab.addHook(this.input, this.dropdown, [droplabFilter], this.config).init();
+ }
+ }
+
+ window.gl = window.gl || {};
+ gl.DropdownHint = DropdownHint;
+})();
diff --git a/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 b/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6
new file mode 100644
index 00000000000..b3dc3e502c5
--- /dev/null
+++ b/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6
@@ -0,0 +1,44 @@
+require('./filtered_search_dropdown');
+
+/* global droplabAjax */
+/* global droplabFilter */
+
+(() => {
+ class DropdownNonUser extends gl.FilteredSearchDropdown {
+ constructor(droplab, dropdown, input, filter, endpoint, symbol) {
+ super(droplab, dropdown, input, filter);
+ this.symbol = symbol;
+ this.config = {
+ droplabAjax: {
+ endpoint,
+ method: 'setData',
+ loadingTemplate: this.loadingTemplate,
+ },
+ droplabFilter: {
+ filterFunction: gl.DropdownUtils.filterWithSymbol.bind(null, this.symbol, input),
+ },
+ };
+ }
+
+ itemClicked(e) {
+ super.itemClicked(e, (selected) => {
+ const title = selected.querySelector('.js-data-value').innerText.trim();
+ return `${this.symbol}${gl.DropdownUtils.getEscapedText(title)}`;
+ });
+ }
+
+ renderContent(forceShowList = false) {
+ this.droplab
+ .changeHookList(this.hookId, this.dropdown, [droplabAjax, droplabFilter], this.config);
+ super.renderContent(forceShowList);
+ }
+
+ init() {
+ this.droplab
+ .addHook(this.input, this.dropdown, [droplabAjax, droplabFilter], this.config).init();
+ }
+ }
+
+ window.gl = window.gl || {};
+ gl.DropdownNonUser = DropdownNonUser;
+})();
diff --git a/app/assets/javascripts/filtered_search/dropdown_user.js.es6 b/app/assets/javascripts/filtered_search/dropdown_user.js.es6
new file mode 100644
index 00000000000..7e9c6f74aa5
--- /dev/null
+++ b/app/assets/javascripts/filtered_search/dropdown_user.js.es6
@@ -0,0 +1,60 @@
+require('./filtered_search_dropdown');
+
+/* global droplabAjaxFilter */
+
+(() => {
+ class DropdownUser extends gl.FilteredSearchDropdown {
+ constructor(droplab, dropdown, input, filter) {
+ super(droplab, dropdown, input, filter);
+ this.config = {
+ droplabAjaxFilter: {
+ endpoint: `${gon.relative_url_root || ''}/autocomplete/users.json`,
+ searchKey: 'search',
+ params: {
+ per_page: 20,
+ active: true,
+ project_id: this.getProjectId(),
+ current_user: true,
+ },
+ searchValueFunction: this.getSearchInput.bind(this),
+ loadingTemplate: this.loadingTemplate,
+ },
+ };
+ }
+
+ itemClicked(e) {
+ super.itemClicked(e,
+ selected => selected.querySelector('.dropdown-light-content').innerText.trim());
+ }
+
+ renderContent(forceShowList = false) {
+ this.droplab.changeHookList(this.hookId, this.dropdown, [droplabAjaxFilter], this.config);
+ super.renderContent(forceShowList);
+ }
+
+ getProjectId() {
+ return this.input.getAttribute('data-project-id');
+ }
+
+ getSearchInput() {
+ const query = gl.DropdownUtils.getSearchInput(this.input);
+ const { lastToken } = gl.FilteredSearchTokenizer.processTokens(query);
+ let value = lastToken.value || '';
+
+ // Removes the first character if it is a quotation so that we can search
+ // with multiple words
+ if (value[0] === '"' || value[0] === '\'') {
+ value = value.slice(1);
+ }
+
+ return value;
+ }
+
+ init() {
+ this.droplab.addHook(this.input, this.dropdown, [droplabAjaxFilter], this.config).init();
+ }
+ }
+
+ window.gl = window.gl || {};
+ gl.DropdownUser = DropdownUser;
+})();
diff --git a/app/assets/javascripts/filtered_search/dropdown_utils.js.es6 b/app/assets/javascripts/filtered_search/dropdown_utils.js.es6
new file mode 100644
index 00000000000..de3fa116717
--- /dev/null
+++ b/app/assets/javascripts/filtered_search/dropdown_utils.js.es6
@@ -0,0 +1,126 @@
+(() => {
+ class DropdownUtils {
+ static getEscapedText(text) {
+ let escapedText = text;
+ const hasSpace = text.indexOf(' ') !== -1;
+ const hasDoubleQuote = text.indexOf('"') !== -1;
+
+ // Encapsulate value with quotes if it has spaces
+ // Known side effect: values's with both single and double quotes
+ // won't escape properly
+ if (hasSpace) {
+ if (hasDoubleQuote) {
+ escapedText = `'${text}'`;
+ } else {
+ // Encapsulate singleQuotes or if it hasSpace
+ escapedText = `"${text}"`;
+ }
+ }
+
+ return escapedText;
+ }
+
+ static filterWithSymbol(filterSymbol, input, item) {
+ const updatedItem = item;
+ const query = gl.DropdownUtils.getSearchInput(input);
+ const { lastToken, searchToken } = gl.FilteredSearchTokenizer.processTokens(query);
+
+ if (lastToken !== searchToken) {
+ const title = updatedItem.title.toLowerCase();
+ let value = lastToken.value.toLowerCase();
+
+ // Removes the first character if it is a quotation so that we can search
+ // with multiple words
+ if ((value[0] === '"' || value[0] === '\'') && title.indexOf(' ') !== -1) {
+ value = value.slice(1);
+ }
+
+ // Eg. filterSymbol = ~ for labels
+ const matchWithoutSymbol = lastToken.symbol === filterSymbol && title.indexOf(value) !== -1;
+ const match = title.indexOf(`${lastToken.symbol}${value}`) !== -1;
+
+ updatedItem.droplab_hidden = !match && !matchWithoutSymbol;
+ } else {
+ updatedItem.droplab_hidden = false;
+ }
+
+ return updatedItem;
+ }
+
+ static filterHint(input, item) {
+ const updatedItem = item;
+ const query = gl.DropdownUtils.getSearchInput(input);
+ let { lastToken } = gl.FilteredSearchTokenizer.processTokens(query);
+ lastToken = lastToken.key || lastToken || '';
+
+ if (!lastToken || query.split('').last() === ' ') {
+ updatedItem.droplab_hidden = false;
+ } else if (lastToken) {
+ const split = lastToken.split(':');
+ const tokenName = split[0].split(' ').last();
+
+ const match = updatedItem.hint.indexOf(tokenName.toLowerCase()) === -1;
+ updatedItem.droplab_hidden = tokenName ? match : false;
+ }
+
+ return updatedItem;
+ }
+
+ static setDataValueIfSelected(filter, selected) {
+ const dataValue = selected.getAttribute('data-value');
+
+ if (dataValue) {
+ gl.FilteredSearchDropdownManager.addWordToInput(filter, dataValue);
+ }
+
+ // Return boolean based on whether it was set
+ return dataValue !== null;
+ }
+
+ static getSearchInput(filteredSearchInput) {
+ const inputValue = filteredSearchInput.value;
+ const { right } = gl.DropdownUtils.getInputSelectionPosition(filteredSearchInput);
+
+ return inputValue.slice(0, right);
+ }
+
+ static getInputSelectionPosition(input) {
+ const selectionStart = input.selectionStart;
+ let inputValue = input.value;
+ // Replace all spaces inside quote marks with underscores
+ // (will continue to match entire string until an end quote is found if any)
+ // This helps with matching the beginning & end of a token:key
+ inputValue = inputValue.replace(/(('[^']*'{0,1})|("[^"]*"{0,1})|:\s+)/g, str => str.replace(/\s/g, '_'));
+
+ // Get the right position for the word selected
+ // Regex matches first space
+ let right = inputValue.slice(selectionStart).search(/\s/);
+
+ if (right >= 0) {
+ right += selectionStart;
+ } else if (right < 0) {
+ right = inputValue.length;
+ }
+
+ // Get the left position for the word selected
+ // Regex matches last non-whitespace character
+ let left = inputValue.slice(0, right).search(/\S+$/);
+
+ if (selectionStart === 0) {
+ left = 0;
+ } else if (selectionStart === inputValue.length && left < 0) {
+ left = inputValue.length;
+ } else if (left < 0) {
+ left = selectionStart;
+ }
+
+ return {
+ left,
+ right,
+ };
+ }
+ }
+
+ window.gl = window.gl || {};
+ gl.DropdownUtils = DropdownUtils;
+})();
diff --git a/app/assets/javascripts/filtered_search/filtered_search_bundle.js b/app/assets/javascripts/filtered_search/filtered_search_bundle.js
new file mode 100644
index 00000000000..392f1835966
--- /dev/null
+++ b/app/assets/javascripts/filtered_search/filtered_search_bundle.js
@@ -0,0 +1,3 @@
+function requireAll(context) { return context.keys().map(context); }
+
+requireAll(require.context('./', true, /^\.\/(?!filtered_search_bundle).*\.(js|es6)$/));
diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6
new file mode 100644
index 00000000000..859d6515531
--- /dev/null
+++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6
@@ -0,0 +1,112 @@
+(() => {
+ const DATA_DROPDOWN_TRIGGER = 'data-dropdown-trigger';
+
+ class FilteredSearchDropdown {
+ constructor(droplab, dropdown, input, filter) {
+ this.droplab = droplab;
+ this.hookId = input.getAttribute('data-id');
+ this.input = input;
+ this.filter = filter;
+ this.dropdown = dropdown;
+ this.loadingTemplate = `<div class="filter-dropdown-loading">
+ <i class="fa fa-spinner fa-spin"></i>
+ </div>`;
+ this.bindEvents();
+ }
+
+ bindEvents() {
+ this.itemClickedWrapper = this.itemClicked.bind(this);
+ this.dropdown.addEventListener('click.dl', this.itemClickedWrapper);
+ }
+
+ unbindEvents() {
+ this.dropdown.removeEventListener('click.dl', this.itemClickedWrapper);
+ }
+
+ getCurrentHook() {
+ return this.droplab.hooks.filter(h => h.id === this.hookId)[0] || null;
+ }
+
+ itemClicked(e, getValueFunction) {
+ const { selected } = e.detail;
+
+ if (selected.tagName === 'LI' && selected.innerHTML) {
+ const dataValueSet = gl.DropdownUtils.setDataValueIfSelected(this.filter, selected);
+
+ if (!dataValueSet) {
+ const value = getValueFunction(selected);
+ gl.FilteredSearchDropdownManager.addWordToInput(this.filter, value);
+ }
+
+ this.dismissDropdown();
+ this.dispatchInputEvent();
+ }
+ }
+
+ setAsDropdown() {
+ this.input.setAttribute(DATA_DROPDOWN_TRIGGER, `#${this.dropdown.id}`);
+ }
+
+ setOffset(offset = 0) {
+ this.dropdown.style.left = `${offset}px`;
+ }
+
+ renderContent(forceShowList = false) {
+ if (forceShowList && this.getCurrentHook().list.hidden) {
+ this.getCurrentHook().list.show();
+ }
+ }
+
+ render(forceRenderContent = false, forceShowList = false) {
+ this.setAsDropdown();
+
+ const currentHook = this.getCurrentHook();
+ const firstTimeInitialized = currentHook === null;
+
+ if (firstTimeInitialized || forceRenderContent) {
+ this.renderContent(forceShowList);
+ } else if (currentHook.list.list.id !== this.dropdown.id) {
+ this.renderContent(forceShowList);
+ }
+ }
+
+ dismissDropdown() {
+ // Focusing on the input will dismiss dropdown
+ // (default droplab functionality)
+ this.input.focus();
+ }
+
+ dispatchInputEvent() {
+ // Propogate input change to FilteredSearchDropdownManager
+ // so that it can determine which dropdowns to open
+ this.input.dispatchEvent(new CustomEvent('input', {
+ bubbles: true,
+ cancelable: true,
+ }));
+ }
+
+ dispatchFormSubmitEvent() {
+ // dispatchEvent() is necessary as form.submit() does not
+ // trigger event handlers
+ this.input.form.dispatchEvent(new Event('submit'));
+ }
+
+ hideDropdown() {
+ this.getCurrentHook().list.hide();
+ }
+
+ resetFilters() {
+ const hook = this.getCurrentHook();
+ const data = hook.list.data;
+ const results = data.map((o) => {
+ const updated = o;
+ updated.droplab_hidden = false;
+ return updated;
+ });
+ hook.list.render(results);
+ }
+ }
+
+ window.gl = window.gl || {};
+ gl.FilteredSearchDropdown = FilteredSearchDropdown;
+})();
diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6
new file mode 100644
index 00000000000..547989a6ff5
--- /dev/null
+++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6
@@ -0,0 +1,207 @@
+/* global DropLab */
+
+(() => {
+ class FilteredSearchDropdownManager {
+ constructor() {
+ this.tokenizer = gl.FilteredSearchTokenizer;
+ this.filteredSearchInput = document.querySelector('.filtered-search');
+
+ this.setupMapping();
+
+ this.cleanupWrapper = this.cleanup.bind(this);
+ document.addEventListener('beforeunload', this.cleanupWrapper);
+ }
+
+ cleanup() {
+ if (this.droplab) {
+ this.droplab.destroy();
+ this.droplab = null;
+ }
+
+ this.setupMapping();
+
+ document.removeEventListener('beforeunload', this.cleanupWrapper);
+ }
+
+ setupMapping() {
+ this.mapping = {
+ author: {
+ reference: null,
+ gl: 'DropdownUser',
+ element: document.querySelector('#js-dropdown-author'),
+ },
+ assignee: {
+ reference: null,
+ gl: 'DropdownUser',
+ element: document.querySelector('#js-dropdown-assignee'),
+ },
+ milestone: {
+ reference: null,
+ gl: 'DropdownNonUser',
+ extraArguments: ['milestones.json', '%'],
+ element: document.querySelector('#js-dropdown-milestone'),
+ },
+ label: {
+ reference: null,
+ gl: 'DropdownNonUser',
+ extraArguments: ['labels.json', '~'],
+ element: document.querySelector('#js-dropdown-label'),
+ },
+ hint: {
+ reference: null,
+ gl: 'DropdownHint',
+ element: document.querySelector('#js-dropdown-hint'),
+ },
+ };
+ }
+
+ static addWordToInput(tokenName, tokenValue = '') {
+ const input = document.querySelector('.filtered-search');
+ const inputValue = input.value;
+ const word = `${tokenName}:${tokenValue}`;
+
+ // Get the string to replace
+ let newCaretPosition = input.selectionStart;
+ const { left, right } = gl.DropdownUtils.getInputSelectionPosition(input);
+
+ input.value = `${inputValue.substr(0, left)}${word}${inputValue.substr(right)}`;
+
+ // If we have added a tokenValue at the end of the input,
+ // add a space and set selection to the end
+ if (right >= inputValue.length && tokenValue !== '') {
+ input.value += ' ';
+ newCaretPosition = input.value.length;
+ }
+
+ gl.FilteredSearchDropdownManager.updateInputCaretPosition(newCaretPosition, input);
+ }
+
+ static updateInputCaretPosition(selectionStart, input) {
+ // Reset the position
+ // Sometimes can end up at end of input
+ input.setSelectionRange(selectionStart, selectionStart);
+
+ const { right } = gl.DropdownUtils.getInputSelectionPosition(input);
+
+ input.setSelectionRange(right, right);
+ }
+
+ updateCurrentDropdownOffset() {
+ this.updateDropdownOffset(this.currentDropdown);
+ }
+
+ updateDropdownOffset(key) {
+ if (!this.font) {
+ this.font = window.getComputedStyle(this.filteredSearchInput).font;
+ }
+
+ const input = this.filteredSearchInput;
+ const inputText = input.value.slice(0, input.selectionStart);
+ const filterIconPadding = 27;
+ let offset = gl.text.getTextWidth(inputText, this.font) + filterIconPadding;
+
+ const currentDropdownWidth = this.mapping[key].element.clientWidth === 0 ? 200 :
+ this.mapping[key].element.clientWidth;
+ const offsetMaxWidth = this.filteredSearchInput.clientWidth - currentDropdownWidth;
+
+ if (offsetMaxWidth < offset) {
+ offset = offsetMaxWidth;
+ }
+
+ this.mapping[key].reference.setOffset(offset);
+ }
+
+ load(key, firstLoad = false) {
+ const mappingKey = this.mapping[key];
+ const glClass = mappingKey.gl;
+ const element = mappingKey.element;
+ let forceShowList = false;
+
+ if (!mappingKey.reference) {
+ const dl = this.droplab;
+ const defaultArguments = [null, dl, element, this.filteredSearchInput, key];
+ const glArguments = defaultArguments.concat(mappingKey.extraArguments || []);
+
+ // Passing glArguments to `new gl[glClass](<arguments>)`
+ mappingKey.reference = new (Function.prototype.bind.apply(gl[glClass], glArguments))();
+ }
+
+ if (firstLoad) {
+ mappingKey.reference.init();
+ }
+
+ if (this.currentDropdown === 'hint') {
+ // Force the dropdown to show if it was clicked from the hint dropdown
+ forceShowList = true;
+ }
+
+ this.updateDropdownOffset(key);
+ mappingKey.reference.render(firstLoad, forceShowList);
+
+ this.currentDropdown = key;
+ }
+
+ loadDropdown(dropdownName = '') {
+ let firstLoad = false;
+
+ if (!this.droplab) {
+ firstLoad = true;
+ this.droplab = new DropLab();
+ }
+
+ const match = gl.FilteredSearchTokenKeys.searchByKey(dropdownName.toLowerCase());
+ const shouldOpenFilterDropdown = match && this.currentDropdown !== match.key
+ && this.mapping[match.key];
+ const shouldOpenHintDropdown = !match && this.currentDropdown !== 'hint';
+
+ if (shouldOpenFilterDropdown || shouldOpenHintDropdown) {
+ const key = match && match.key ? match.key : 'hint';
+ this.load(key, firstLoad);
+ }
+ }
+
+ setDropdown() {
+ const { lastToken, searchToken } = this.tokenizer
+ .processTokens(gl.DropdownUtils.getSearchInput(this.filteredSearchInput));
+
+ if (this.currentDropdown) {
+ this.updateCurrentDropdownOffset();
+ }
+
+ if (lastToken === searchToken && lastToken !== null) {
+ // Token is not fully initialized yet because it has no value
+ // Eg. token = 'label:'
+
+ const split = lastToken.split(':');
+ const dropdownName = split[0].split(' ').last();
+ this.loadDropdown(split.length > 1 ? dropdownName : '');
+ } else if (lastToken) {
+ // Token has been initialized into an object because it has a value
+ this.loadDropdown(lastToken.key);
+ } else {
+ this.loadDropdown('hint');
+ }
+ }
+
+ resetDropdowns() {
+ // Force current dropdown to hide
+ this.mapping[this.currentDropdown].reference.hideDropdown();
+
+ // Re-Load dropdown
+ this.setDropdown();
+
+ // Reset filters for current dropdown
+ this.mapping[this.currentDropdown].reference.resetFilters();
+
+ // Reposition dropdown so that it is aligned with cursor
+ this.updateDropdownOffset(this.currentDropdown);
+ }
+
+ destroyDroplab() {
+ this.droplab.destroy();
+ }
+ }
+
+ window.gl = window.gl || {};
+ gl.FilteredSearchDropdownManager = FilteredSearchDropdownManager;
+})();
diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6
new file mode 100644
index 00000000000..4e02ab7c8c1
--- /dev/null
+++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6
@@ -0,0 +1,230 @@
+(() => {
+ class FilteredSearchManager {
+ constructor() {
+ this.filteredSearchInput = document.querySelector('.filtered-search');
+ this.clearSearchButton = document.querySelector('.clear-search');
+
+ if (this.filteredSearchInput) {
+ this.tokenizer = gl.FilteredSearchTokenizer;
+ this.dropdownManager = new gl.FilteredSearchDropdownManager();
+
+ this.bindEvents();
+ this.loadSearchParamsFromURL();
+ this.dropdownManager.setDropdown();
+
+ this.cleanupWrapper = this.cleanup.bind(this);
+ document.addEventListener('beforeunload', this.cleanupWrapper);
+ }
+ }
+
+ cleanup() {
+ this.unbindEvents();
+ document.removeEventListener('beforeunload', this.cleanupWrapper);
+ }
+
+ bindEvents() {
+ this.handleFormSubmit = this.handleFormSubmit.bind(this);
+ this.setDropdownWrapper = this.dropdownManager.setDropdown.bind(this.dropdownManager);
+ this.toggleClearSearchButtonWrapper = this.toggleClearSearchButton.bind(this);
+ this.checkForEnterWrapper = this.checkForEnter.bind(this);
+ this.clearSearchWrapper = this.clearSearch.bind(this);
+ this.checkForBackspaceWrapper = this.checkForBackspace.bind(this);
+ this.tokenChange = this.tokenChange.bind(this);
+
+ this.filteredSearchInput.form.addEventListener('submit', this.handleFormSubmit);
+ this.filteredSearchInput.addEventListener('input', this.setDropdownWrapper);
+ this.filteredSearchInput.addEventListener('input', this.toggleClearSearchButtonWrapper);
+ this.filteredSearchInput.addEventListener('keydown', this.checkForEnterWrapper);
+ this.filteredSearchInput.addEventListener('keyup', this.checkForBackspaceWrapper);
+ this.filteredSearchInput.addEventListener('click', this.tokenChange);
+ this.filteredSearchInput.addEventListener('keyup', this.tokenChange);
+ this.clearSearchButton.addEventListener('click', this.clearSearchWrapper);
+ }
+
+ unbindEvents() {
+ this.filteredSearchInput.form.removeEventListener('submit', this.handleFormSubmit);
+ this.filteredSearchInput.removeEventListener('input', this.setDropdownWrapper);
+ this.filteredSearchInput.removeEventListener('input', this.toggleClearSearchButtonWrapper);
+ this.filteredSearchInput.removeEventListener('keydown', this.checkForEnterWrapper);
+ this.filteredSearchInput.removeEventListener('keyup', this.checkForBackspaceWrapper);
+ this.filteredSearchInput.removeEventListener('click', this.tokenChange);
+ this.filteredSearchInput.removeEventListener('keyup', this.tokenChange);
+ this.clearSearchButton.removeEventListener('click', this.clearSearchWrapper);
+ }
+
+ checkForBackspace(e) {
+ // 8 = Backspace Key
+ // 46 = Delete Key
+ if (e.keyCode === 8 || e.keyCode === 46) {
+ // Reposition dropdown so that it is aligned with cursor
+ this.dropdownManager.updateCurrentDropdownOffset();
+ }
+ }
+
+ checkForEnter(e) {
+ if (e.keyCode === 38 || e.keyCode === 40) {
+ const selectionStart = this.filteredSearchInput.selectionStart;
+
+ e.preventDefault();
+ this.filteredSearchInput.setSelectionRange(selectionStart, selectionStart);
+ }
+
+ if (e.keyCode === 13) {
+ const dropdown = this.dropdownManager.mapping[this.dropdownManager.currentDropdown];
+ const dropdownEl = dropdown.element;
+ const activeElements = dropdownEl.querySelectorAll('.dropdown-active');
+
+ e.preventDefault();
+
+ if (!activeElements.length) {
+ // Prevent droplab from opening dropdown
+ this.dropdownManager.destroyDroplab();
+
+ this.search();
+ }
+ }
+ }
+
+ toggleClearSearchButton(e) {
+ if (e.target.value) {
+ this.clearSearchButton.classList.remove('hidden');
+ } else {
+ this.clearSearchButton.classList.add('hidden');
+ }
+ }
+
+ clearSearch(e) {
+ e.preventDefault();
+
+ this.filteredSearchInput.value = '';
+ this.clearSearchButton.classList.add('hidden');
+
+ this.dropdownManager.resetDropdowns();
+ }
+
+ handleFormSubmit(e) {
+ e.preventDefault();
+ this.search();
+ }
+
+ loadSearchParamsFromURL() {
+ const params = gl.utils.getUrlParamsArray();
+ const usernameParams = this.getUsernameParams();
+ const inputValues = [];
+
+ params.forEach((p) => {
+ const split = p.split('=');
+ const keyParam = decodeURIComponent(split[0]);
+ const value = split[1];
+
+ // Check if it matches edge conditions listed in gl.FilteredSearchTokenKeys
+ const condition = gl.FilteredSearchTokenKeys.searchByConditionUrl(p);
+
+ if (condition) {
+ inputValues.push(`${condition.tokenKey}:${condition.value}`);
+ } else {
+ // Sanitize value since URL converts spaces into +
+ // Replace before decode so that we know what was originally + versus the encoded +
+ const sanitizedValue = value ? decodeURIComponent(value.replace(/\+/g, ' ')) : value;
+ const match = gl.FilteredSearchTokenKeys.searchByKeyParam(keyParam);
+
+ if (match) {
+ const indexOf = keyParam.indexOf('_');
+ const sanitizedKey = indexOf !== -1 ? keyParam.slice(0, keyParam.indexOf('_')) : keyParam;
+ const symbol = match.symbol;
+ let quotationsToUse = '';
+
+ if (sanitizedValue.indexOf(' ') !== -1) {
+ // Prefer ", but use ' if required
+ quotationsToUse = sanitizedValue.indexOf('"') === -1 ? '"' : '\'';
+ }
+
+ inputValues.push(`${sanitizedKey}:${symbol}${quotationsToUse}${sanitizedValue}${quotationsToUse}`);
+ } else if (!match && keyParam === 'assignee_id') {
+ const id = parseInt(value, 10);
+ if (usernameParams[id]) {
+ inputValues.push(`assignee:@${usernameParams[id]}`);
+ }
+ } else if (!match && keyParam === 'author_id') {
+ const id = parseInt(value, 10);
+ if (usernameParams[id]) {
+ inputValues.push(`author:@${usernameParams[id]}`);
+ }
+ } else if (!match && keyParam === 'search') {
+ inputValues.push(sanitizedValue);
+ }
+ }
+ });
+
+ // Trim the last space value
+ this.filteredSearchInput.value = inputValues.join(' ');
+
+ if (inputValues.length > 0) {
+ this.clearSearchButton.classList.remove('hidden');
+ }
+ }
+
+ search() {
+ const paths = [];
+ const { tokens, searchToken } = this.tokenizer.processTokens(this.filteredSearchInput.value);
+ const currentState = gl.utils.getParameterByName('state') || 'opened';
+ paths.push(`state=${currentState}`);
+
+ tokens.forEach((token) => {
+ const condition = gl.FilteredSearchTokenKeys
+ .searchByConditionKeyValue(token.key, token.value.toLowerCase());
+ const { param } = gl.FilteredSearchTokenKeys.searchByKey(token.key);
+ const keyParam = param ? `${token.key}_${param}` : token.key;
+ let tokenPath = '';
+
+ if (condition) {
+ tokenPath = condition.url;
+ } else {
+ let tokenValue = token.value;
+
+ if ((tokenValue[0] === '\'' && tokenValue[tokenValue.length - 1] === '\'') ||
+ (tokenValue[0] === '"' && tokenValue[tokenValue.length - 1] === '"')) {
+ tokenValue = tokenValue.slice(1, tokenValue.length - 1);
+ }
+
+ tokenPath = `${keyParam}=${encodeURIComponent(tokenValue)}`;
+ }
+
+ paths.push(tokenPath);
+ });
+
+ if (searchToken) {
+ const sanitized = searchToken.split(' ').map(t => encodeURIComponent(t)).join('+');
+ paths.push(`search=${sanitized}`);
+ }
+
+ const parameterizedUrl = `?scope=all&utf8=✓&${paths.join('&')}`;
+
+ gl.utils.visitUrl(parameterizedUrl);
+ }
+
+ getUsernameParams() {
+ const usernamesById = {};
+ try {
+ const attribute = this.filteredSearchInput.getAttribute('data-username-params');
+ JSON.parse(attribute).forEach((user) => {
+ usernamesById[user.id] = user.username;
+ });
+ } catch (e) {
+ // do nothing
+ }
+ return usernamesById;
+ }
+
+ tokenChange() {
+ const dropdown = this.dropdownManager.mapping[this.dropdownManager.currentDropdown];
+ const currentDropdownRef = dropdown.reference;
+
+ this.setDropdownWrapper();
+ currentDropdownRef.dispatchInputEvent();
+ }
+ }
+
+ window.gl = window.gl || {};
+ gl.FilteredSearchManager = FilteredSearchManager;
+})();
diff --git a/app/assets/javascripts/filtered_search/filtered_search_token_keys.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js.es6
new file mode 100644
index 00000000000..e6b53cd4b55
--- /dev/null
+++ b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js.es6
@@ -0,0 +1,96 @@
+(() => {
+ const tokenKeys = [{
+ key: 'author',
+ type: 'string',
+ param: 'username',
+ symbol: '@',
+ }, {
+ key: 'assignee',
+ type: 'string',
+ param: 'username',
+ symbol: '@',
+ }, {
+ key: 'milestone',
+ type: 'string',
+ param: 'title',
+ symbol: '%',
+ }, {
+ key: 'label',
+ type: 'array',
+ param: 'name[]',
+ symbol: '~',
+ }];
+
+ const alternativeTokenKeys = [{
+ key: 'label',
+ type: 'string',
+ param: 'name',
+ symbol: '~',
+ }];
+
+ const tokenKeysWithAlternative = tokenKeys.concat(alternativeTokenKeys);
+
+ const conditions = [{
+ url: 'assignee_id=0',
+ tokenKey: 'assignee',
+ value: 'none',
+ }, {
+ url: 'milestone_title=No+Milestone',
+ tokenKey: 'milestone',
+ value: 'none',
+ }, {
+ url: 'milestone_title=%23upcoming',
+ tokenKey: 'milestone',
+ value: 'upcoming',
+ }, {
+ url: 'label_name[]=No+Label',
+ tokenKey: 'label',
+ value: 'none',
+ }];
+
+ class FilteredSearchTokenKeys {
+ static get() {
+ return tokenKeys;
+ }
+
+ static getAlternatives() {
+ return alternativeTokenKeys;
+ }
+
+ static getConditions() {
+ return conditions;
+ }
+
+ static searchByKey(key) {
+ return tokenKeys.find(tokenKey => tokenKey.key === key) || null;
+ }
+
+ static searchBySymbol(symbol) {
+ return tokenKeys.find(tokenKey => tokenKey.symbol === symbol) || null;
+ }
+
+ static searchByKeyParam(keyParam) {
+ return tokenKeysWithAlternative.find((tokenKey) => {
+ let tokenKeyParam = tokenKey.key;
+
+ if (tokenKey.param) {
+ tokenKeyParam += `_${tokenKey.param}`;
+ }
+
+ return keyParam === tokenKeyParam;
+ }) || null;
+ }
+
+ static searchByConditionUrl(url) {
+ return conditions.find(condition => condition.url === url) || null;
+ }
+
+ static searchByConditionKeyValue(key, value) {
+ return conditions
+ .find(condition => condition.tokenKey === key && condition.value === value) || null;
+ }
+ }
+
+ window.gl = window.gl || {};
+ gl.FilteredSearchTokenKeys = FilteredSearchTokenKeys;
+})();
diff --git a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js.es6
new file mode 100644
index 00000000000..cf53845a48b
--- /dev/null
+++ b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js.es6
@@ -0,0 +1,45 @@
+(() => {
+ class FilteredSearchTokenizer {
+ static processTokens(input) {
+ // Regex extracts `(token):(symbol)(value)`
+ // Values that start with a double quote must end in a double quote (same for single)
+ const tokenRegex = /(\w+):([~%@]?)(?:('[^']*'{0,1})|("[^"]*"{0,1})|(\S+))/g;
+ const tokens = [];
+ let lastToken = null;
+ const searchToken = input.replace(tokenRegex, (match, key, symbol, v1, v2, v3) => {
+ let tokenValue = v1 || v2 || v3;
+ let tokenSymbol = symbol;
+
+ if (tokenValue === '~' || tokenValue === '%' || tokenValue === '@') {
+ tokenSymbol = tokenValue;
+ tokenValue = '';
+ }
+
+ tokens.push({
+ key,
+ value: tokenValue || '',
+ symbol: tokenSymbol || '',
+ });
+ return '';
+ }).replace(/\s{2,}/g, ' ').trim() || '';
+
+ if (tokens.length > 0) {
+ const last = tokens[tokens.length - 1];
+ const lastString = `${last.key}:${last.symbol}${last.value}`;
+ lastToken = input.lastIndexOf(lastString) ===
+ input.length - lastString.length ? last : searchToken;
+ } else {
+ lastToken = searchToken;
+ }
+
+ return {
+ tokens,
+ lastToken,
+ searchToken,
+ };
+ }
+ }
+
+ window.gl = window.gl || {};
+ gl.FilteredSearchTokenizer = FilteredSearchTokenizer;
+})();
diff --git a/app/assets/javascripts/flash.js b/app/assets/javascripts/flash.js
index 804d7d9c4ab..249fe23d4cb 100644
--- a/app/assets/javascripts/flash.js
+++ b/app/assets/javascripts/flash.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, no-param-reassign, quotes, quote-props, prefer-template, comma-dangle, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, no-param-reassign, quotes, quote-props, prefer-template, comma-dangle, max-len */
(function() {
this.Flash = (function() {
var hideFlash;
@@ -38,7 +38,5 @@
}
return Flash;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/gfm_auto_complete.js.es6 b/app/assets/javascripts/gfm_auto_complete.js.es6
index 17d03c87bf5..7f1f2a5d278 100644
--- a/app/assets/javascripts/gfm_auto_complete.js.es6
+++ b/app/assets/javascripts/gfm_auto_complete.js.es6
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, no-template-curly-in-string, comma-dangle, object-shorthand, quotes, dot-notation, no-else-return, one-var, no-var, no-underscore-dangle, one-var-declaration-per-line, no-param-reassign, no-useless-escape, prefer-template, consistent-return, wrap-iife, prefer-arrow-callback, camelcase, no-unused-vars, no-useless-return, padded-blocks, vars-on-top, indent, no-extra-semi, no-multi-spaces, semi, max-len */
+/* eslint-disable func-names, space-before-function-paren, no-template-curly-in-string, comma-dangle, object-shorthand, quotes, dot-notation, no-else-return, one-var, no-var, no-underscore-dangle, one-var-declaration-per-line, no-param-reassign, no-useless-escape, prefer-template, consistent-return, wrap-iife, prefer-arrow-callback, camelcase, no-unused-vars, no-useless-return, vars-on-top, max-len */
// Creates the variables for setting up GFM auto-completion
(function() {
@@ -48,18 +48,18 @@
},
DefaultOptions: {
sorter: function(query, items, searchKey) {
+ this.setting.highlightFirst = this.setting.alwaysHighlightFirst || query.length > 0;
if (gl.GfmAutoComplete.isLoading(items)) {
+ this.setting.highlightFirst = false;
return items;
}
return $.fn.atwho["default"].callbacks.sorter(query, items, searchKey);
},
filter: function(query, data, searchKey) {
if (gl.GfmAutoComplete.isLoading(data)) {
- gl.GfmAutoComplete.togglePreventSelection.call(this, true);
gl.GfmAutoComplete.fetchData(this.$inputor, this.at);
return data;
} else {
- gl.GfmAutoComplete.togglePreventSelection.call(this, false);
return $.fn.atwho["default"].callbacks.filter(query, data, searchKey);
}
},
@@ -74,20 +74,21 @@
// The below is taken from At.js source
// Tweaked to commands to start without a space only if char before is a non-word character
// https://github.com/ichord/At.js
- var _a, _y, regexp, match, atSymbols;
- atSymbols = Object.keys(this.app.controllers).join('|');
- subtext = subtext.split(' ').pop();
+ var _a, _y, regexp, match, atSymbolsWithBar, atSymbolsWithoutBar;
+ atSymbolsWithBar = Object.keys(this.app.controllers).join('|');
+ atSymbolsWithoutBar = Object.keys(this.app.controllers).join('');
+ subtext = subtext.split(/\s+/g).pop();
flag = flag.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
_a = decodeURI("%C3%80");
_y = decodeURI("%C3%BF");
- regexp = new RegExp("(?:\\B|\\W|\\s)" + flag + "(?![" + atSymbols + "])([A-Za-z" + _a + "-" + _y + "0-9_\'\.\+\-]*)$", 'gi');
+ regexp = new RegExp("^(?:\\B|[^a-zA-Z0-9_" + atSymbolsWithoutBar + "]|\\s)" + flag + "(?![" + atSymbolsWithBar + "])(([A-Za-z" + _a + "-" + _y + "0-9_\'\.\+\-]|[^\\x00-\\x7a])*)$", 'gi');
match = regexp.exec(subtext);
if (match) {
- return match[2] || match[1];
+ return (match[1] || match[1] === "") ? match[1] : match[2];
} else {
return null;
}
@@ -112,7 +113,6 @@
return value.path != null ? this.Emoji.template : this.Loading.template;
}.bind(this),
insertTpl: ':${name}:',
- startWithSpace: false,
skipSpecialCharacterTest: true,
data: this.defaultLoadingData,
callbacks: {
@@ -129,7 +129,6 @@
}.bind(this),
insertTpl: '${atwho-at}${username}',
searchKey: 'search',
- startWithSpace: false,
alwaysHighlightFirst: true,
skipSpecialCharacterTest: true,
data: this.defaultLoadingData,
@@ -155,7 +154,7 @@
return {
username: m.username,
- avatarTag: autoCompleteAvatar.length === 1 ? txtAvatar : imgAvatar,
+ avatarTag: autoCompleteAvatar.length === 1 ? txtAvatar : imgAvatar,
title: sanitize(title),
search: sanitize(m.username + " " + m.name)
};
@@ -172,7 +171,6 @@
}.bind(this),
data: this.defaultLoadingData,
insertTpl: '${atwho-at}${id}',
- startWithSpace: false,
callbacks: {
sorter: this.DefaultOptions.sorter,
filter: this.DefaultOptions.filter,
@@ -200,7 +198,6 @@
displayTpl: function(value) {
return value.title != null ? this.Milestones.template : this.Loading.template;
}.bind(this),
- startWithSpace: false,
data: this.defaultLoadingData,
callbacks: {
matcher: this.DefaultOptions.matcher,
@@ -225,7 +222,6 @@
at: '!',
alias: 'mergerequests',
searchKey: 'search',
- startWithSpace: false,
displayTpl: function(value) {
return value.title != null ? this.Issues.template : this.Loading.template;
}.bind(this),
@@ -259,12 +255,11 @@
return this.isLoading(value) ? this.Loading.template : this.Labels.template;
}.bind(this),
insertTpl: '${atwho-at}${title}',
- startWithSpace: false,
callbacks: {
matcher: this.DefaultOptions.matcher,
- sorter: this.DefaultOptions.sorter,
beforeInsert: this.DefaultOptions.beforeInsert,
filter: this.DefaultOptions.filter,
+ sorter: this.DefaultOptions.sorter,
beforeSave: function(merges) {
if (gl.GfmAutoComplete.isLoading(merges)) return merges;
var sanitizeLabelTitle;
@@ -340,7 +335,7 @@
});
},
matcher: function(flag, subtext, should_startWithSpace, acceptSpaceBar) {
- var regexp = /(?:^|\n)\/([A-Za-z_]*)$/gi
+ var regexp = /(?:^|\n)\/([A-Za-z_]*)$/gi;
var match = regexp.exec(subtext);
if (match) {
return match[1];
@@ -372,21 +367,14 @@
return $input.trigger('keyup');
},
isLoading(data) {
- if (!data) return false;
- if (Array.isArray(data)) data = data[0];
- return data === this.defaultLoadingData[0] || data.name === this.defaultLoadingData[0];
- },
- togglePreventSelection(isPrevented = !!this.setting.tabSelectsMatch) {
- this.setting.tabSelectsMatch = !isPrevented;
- this.setting.spaceSelectsMatch = !isPrevented;
- const eventListenerAction = `${isPrevented ? 'add' : 'remove'}EventListener`;
- this.$inputor[0][eventListenerAction]('keydown', gl.GfmAutoComplete.preventSpaceTabEnter);
- },
- preventSpaceTabEnter(e) {
- const key = e.which || e.keyCode;
- const preventables = [9, 13, 32];
- if (preventables.indexOf(key) > -1) e.preventDefault();
+ var dataToInspect = data;
+ if (data && data.length > 0) {
+ dataToInspect = data[0];
+ }
+
+ var loadingState = this.defaultLoadingData[0];
+ return dataToInspect &&
+ (dataToInspect === loadingState || dataToInspect.name === loadingState);
}
};
-
}).call(this);
diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js
index bb516b3d2df..d9101b55c7f 100644
--- a/app/assets/javascripts/gl_dropdown.js
+++ b/app/assets/javascripts/gl_dropdown.js
@@ -1,11 +1,10 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, one-var, one-var-declaration-per-line, space-before-blocks, prefer-rest-params, max-len, vars-on-top, no-plusplus, wrap-iife, no-unused-vars, quotes, no-shadow, no-cond-assign, prefer-arrow-callback, semi, no-return-assign, no-else-return, camelcase, comma-dangle, no-lonely-if, guard-for-in, no-restricted-syntax, consistent-return, padded-blocks, prefer-template, no-param-reassign, no-loop-func, no-extra-semi, keyword-spacing, no-mixed-operators */
+/* eslint-disable func-names, space-before-function-paren, no-var, one-var, one-var-declaration-per-line, prefer-rest-params, max-len, vars-on-top, wrap-iife, no-unused-vars, quotes, no-shadow, no-cond-assign, prefer-arrow-callback, no-return-assign, no-else-return, camelcase, comma-dangle, no-lonely-if, guard-for-in, no-restricted-syntax, consistent-return, prefer-template, no-param-reassign, no-loop-func, no-mixed-operators */
/* global fuzzaldrinPlus */
-/* global Turbolinks */
(function() {
var GitLabDropdown, GitLabDropdownFilter, GitLabDropdownRemote,
- bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; },
- indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; };
+ bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; },
+ indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i += 1) { if (i in this && this[i] === item) return i; } return -1; };
GitLabDropdownFilter = (function() {
var ARROW_KEY_CODES, BLUR_KEYCODES, HAS_VALUE_CLASS;
@@ -37,7 +36,7 @@
.on('keydown', function (e) {
var keyCode = e.which;
if (keyCode === 13 && !options.elIsInput) {
- e.preventDefault()
+ e.preventDefault();
}
})
.on('input', function() {
@@ -133,7 +132,6 @@
};
return GitLabDropdownFilter;
-
})();
GitLabDropdownRemote = (function() {
@@ -186,7 +184,6 @@
};
return GitLabDropdownRemote;
-
})();
GitLabDropdown = (function() {
@@ -206,7 +203,7 @@
SELECTABLE_CLASSES = ".dropdown-content li:not(" + NON_SELECTABLE_CLASSES + ", .option-hidden)";
- CURSOR_SELECT_SCROLL_PADDING = 5
+ CURSOR_SELECT_SCROLL_PADDING = 5;
FILTER_INPUT = '.dropdown-input .dropdown-input-field';
@@ -223,7 +220,7 @@
this.dropdown = selector != null ? $(selector) : $(this.el).parent();
// Set Defaults
this.filterInput = this.options.filterInput || this.getElement(FILTER_INPUT);
- this.highlight = !!this.options.highlight
+ this.highlight = !!this.options.highlight;
this.filterInputBlur = this.options.filterInputBlur != null
? this.options.filterInputBlur
: true;
@@ -251,7 +248,7 @@
_this.fullData = data;
_this.parseData(_this.fullData);
_this.focusTextInput();
- if (_this.options.filterable && _this.filter && _this.filter.input && _this.filter.input.val().trim() !== '') {
+ if (_this.options.filterable && _this.filter && _this.filter.input && _this.filter.input.val() && _this.filter.input.val().trim() !== '') {
return _this.filter.input.trigger('input');
}
};
@@ -476,7 +473,7 @@
this.removeArrayKeyEvent();
$input = this.dropdown.find(".dropdown-input-field");
if (this.options.filterable) {
- $input.blur().val("");
+ $input.blur();
}
if (this.dropdown.find(".dropdown-toggle-page").length) {
$('.dropdown-menu', this.dropdown).removeClass(PAGE_TWO_CLASS);
@@ -494,7 +491,7 @@
} else {
var ul = document.createElement('ul');
- for (var i = 0; i < html.length; i++) {
+ for (var i = 0; i < html.length; i += 1) {
var el = html[i];
if (el instanceof jQuery) {
@@ -514,12 +511,17 @@
// Append the menu into the dropdown
GitLabDropdown.prototype.appendMenu = function(html) {
+ return this.clearMenu().append(html);
+ };
+
+ GitLabDropdown.prototype.clearMenu = function() {
var selector;
selector = '.dropdown-content';
if (this.dropdown.find(".dropdown-toggle-page").length) {
selector = ".dropdown-page-one .dropdown-content";
}
- return $(selector, this.dropdown).empty().append(html);
+
+ return $(selector, this.dropdown).empty();
};
GitLabDropdown.prototype.renderItem = function(data, group, index) {
@@ -550,7 +552,7 @@
value = this.options.id ? this.options.id(data) : data.id;
fieldName = this.options.fieldName;
- if (value) { value = value.toString().replace(/'/g, '\\\'') };
+ if (value) { value = value.toString().replace(/'/g, '\\\''); }
field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value + "']");
if (field.length) {
@@ -641,7 +643,7 @@
: selectedObject.id;
if (isInput) {
field = $(this.el);
- } else if(value) {
+ } else if (value) {
field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value.toString().replace(/'/g, '\\\'') + "']");
}
@@ -653,18 +655,14 @@
isMarking = false;
el.removeClass(ACTIVE_CLASS);
if (field && field.length) {
- if (isInput) {
- field.val('');
- } else {
- field.remove();
- }
+ this.clearField(field, isInput);
}
} else if (el.hasClass(INDETERMINATE_CLASS)) {
isMarking = true;
el.addClass(ACTIVE_CLASS);
el.removeClass(INDETERMINATE_CLASS);
if (field && field.length && value == null) {
- field.remove();
+ this.clearField(field, isInput);
}
if ((!field || !field.length) && fieldName) {
this.addInput(fieldName, value, selectedObject);
@@ -678,7 +676,7 @@
}
}
if (field && field.length && value == null) {
- field.remove();
+ this.clearField(field, isInput);
}
// Toggle active class for the tick mark
el.addClass(ACTIVE_CLASS);
@@ -695,8 +693,8 @@
};
GitLabDropdown.prototype.focusTextInput = function() {
- if (this.options.filterable) { this.filterInput.focus() }
- }
+ if (this.options.filterable) { this.filterInput.focus(); }
+ };
GitLabDropdown.prototype.addInput = function(fieldName, value, selectedObject) {
var $input;
@@ -724,7 +722,7 @@
if ($el.length) {
var href = $el.attr('href');
if (href && href !== '#') {
- Turbolinks.visit(href);
+ gl.utils.visitUrl(href);
} else {
$el.first().trigger('click');
}
@@ -802,7 +800,7 @@
listItemBottom = listItemTop + listItemHeight;
if (!index) {
// Scroll the dropdown content to the top
- $dropdownContent.scrollTop(0)
+ $dropdownContent.scrollTop(0);
} else if (index === ($listItems.length - 1)) {
// Scroll the dropdown content to the bottom
$dropdownContent.scrollTop($dropdownContent.prop('scrollHeight'));
@@ -828,8 +826,11 @@
return $(this.el).find(".dropdown-toggle-text").text(this.options.toggleLabel(selected, el, instance));
};
- return GitLabDropdown;
+ GitLabDropdown.prototype.clearField = function(field, isInput) {
+ return isInput ? field.val('') : field.remove();
+ };
+ return GitLabDropdown;
})();
$.fn.glDropdown = function(opts) {
@@ -839,5 +840,4 @@
}
});
};
-
}).call(this);
diff --git a/app/assets/javascripts/gl_field_errors.js.es6 b/app/assets/javascripts/gl_field_errors.js.es6
index 63f9cafa8d0..e9add115429 100644
--- a/app/assets/javascripts/gl_field_errors.js.es6
+++ b/app/assets/javascripts/gl_field_errors.js.es6
@@ -1,6 +1,6 @@
-/* eslint-disable comma-dangle, class-methods-use-this, max-len, space-before-function-paren, arrow-parens, no-param-reassign, padded-blocks */
+/* eslint-disable comma-dangle, class-methods-use-this, max-len, space-before-function-paren, arrow-parens, no-param-reassign */
-//= require gl_field_error
+require('./gl_field_error');
((global) => {
const customValidationFlag = 'gl-field-error-ignore';
@@ -45,5 +45,4 @@
}
global.GlFieldErrors = GlFieldErrors;
-
})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/gl_form.js b/app/assets/javascripts/gl_form.js
deleted file mode 100644
index 7dc2d13e5d8..00000000000
--- a/app/assets/javascripts/gl_form.js
+++ /dev/null
@@ -1,64 +0,0 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-new, padded-blocks, max-len */
-/* global GitLab */
-/* global DropzoneInput */
-/* global autosize */
-
-(function() {
- this.GLForm = (function() {
- function GLForm(form) {
- this.form = form;
- this.textarea = this.form.find('textarea.js-gfm-input');
- // Before we start, we should clean up any previous data for this form
- this.destroy();
- // Setup the form
- this.setupForm();
- this.form.data('gl-form', this);
- }
-
- GLForm.prototype.destroy = function() {
- // Clean form listeners
- this.clearEventListeners();
- return this.form.data('gl-form', null);
- };
-
- GLForm.prototype.setupForm = function() {
- var isNewForm;
- isNewForm = this.form.is(':not(.gfm-form)');
- this.form.removeClass('js-new-note-form');
- if (isNewForm) {
- this.form.find('.div-dropzone').remove();
- this.form.addClass('gfm-form');
- // remove notify commit author checkbox for non-commit notes
- gl.utils.disableButtonIfEmptyField(this.form.find('.js-note-text'), this.form.find('.js-comment-button'));
- gl.GfmAutoComplete.setup(this.form.find('.js-gfm-input'));
- new DropzoneInput(this.form);
- autosize(this.textarea);
- // form and textarea event listeners
- this.addEventListeners();
- gl.text.init(this.form);
- }
- // hide discard button
- this.form.find('.js-note-discard').hide();
- return this.form.show();
- };
-
- GLForm.prototype.clearEventListeners = function() {
- this.textarea.off('focus');
- this.textarea.off('blur');
- return gl.text.removeListeners(this.form);
- };
-
- GLForm.prototype.addEventListeners = function() {
- this.textarea.on('focus', function() {
- return $(this).closest('.md-area').addClass('is-focused');
- });
- return this.textarea.on('blur', function() {
- return $(this).closest('.md-area').removeClass('is-focused');
- });
- };
-
- return GLForm;
-
- })();
-
-}).call(this);
diff --git a/app/assets/javascripts/gl_form.js.es6 b/app/assets/javascripts/gl_form.js.es6
new file mode 100644
index 00000000000..0b446ff364a
--- /dev/null
+++ b/app/assets/javascripts/gl_form.js.es6
@@ -0,0 +1,92 @@
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-new, max-len */
+/* global GitLab */
+/* global DropzoneInput */
+/* global autosize */
+
+(() => {
+ const global = window.gl || (window.gl = {});
+
+ function GLForm(form) {
+ this.form = form;
+ this.textarea = this.form.find('textarea.js-gfm-input');
+ // Before we start, we should clean up any previous data for this form
+ this.destroy();
+ // Setup the form
+ this.setupForm();
+ this.form.data('gl-form', this);
+ }
+
+ GLForm.prototype.destroy = function() {
+ // Clean form listeners
+ this.clearEventListeners();
+ return this.form.data('gl-form', null);
+ };
+
+ GLForm.prototype.setupForm = function() {
+ var isNewForm;
+ isNewForm = this.form.is(':not(.gfm-form)');
+ this.form.removeClass('js-new-note-form');
+ if (isNewForm) {
+ this.form.find('.div-dropzone').remove();
+ this.form.addClass('gfm-form');
+ // remove notify commit author checkbox for non-commit notes
+ gl.utils.disableButtonIfEmptyField(this.form.find('.js-note-text'), this.form.find('.js-comment-button'));
+ gl.GfmAutoComplete.setup(this.form.find('.js-gfm-input'));
+ new DropzoneInput(this.form);
+ autosize(this.textarea);
+ // form and textarea event listeners
+ this.addEventListeners();
+ }
+ gl.text.init(this.form);
+ // hide discard button
+ this.form.find('.js-note-discard').hide();
+ this.form.show();
+ if (this.isAutosizeable) this.setupAutosize();
+ };
+
+ GLForm.prototype.setupAutosize = function () {
+ this.textarea.off('autosize:resized')
+ .on('autosize:resized', this.setHeightData.bind(this));
+
+ this.textarea.off('mouseup.autosize')
+ .on('mouseup.autosize', this.destroyAutosize.bind(this));
+
+ setTimeout(() => {
+ autosize(this.textarea);
+ this.textarea.css('resize', 'vertical');
+ }, 0);
+ };
+
+ GLForm.prototype.setHeightData = function () {
+ this.textarea.data('height', this.textarea.outerHeight());
+ };
+
+ GLForm.prototype.destroyAutosize = function () {
+ const outerHeight = this.textarea.outerHeight();
+
+ if (this.textarea.data('height') === outerHeight) return;
+
+ autosize.destroy(this.textarea);
+
+ this.textarea.data('height', outerHeight);
+ this.textarea.outerHeight(outerHeight);
+ this.textarea.css('max-height', window.outerHeight);
+ };
+
+ GLForm.prototype.clearEventListeners = function() {
+ this.textarea.off('focus');
+ this.textarea.off('blur');
+ return gl.text.removeListeners(this.form);
+ };
+
+ GLForm.prototype.addEventListeners = function() {
+ this.textarea.on('focus', function() {
+ return $(this).closest('.md-area').addClass('is-focused');
+ });
+ return this.textarea.on('blur', function() {
+ return $(this).closest('.md-area').removeClass('is-focused');
+ });
+ };
+
+ global.GLForm = GLForm;
+})();
diff --git a/app/assets/javascripts/graphs/graphs_bundle.js b/app/assets/javascripts/graphs/graphs_bundle.js
index 32c26349da0..4f7777aa5bc 100644
--- a/app/assets/javascripts/graphs/graphs_bundle.js
+++ b/app/assets/javascripts/graphs/graphs_bundle.js
@@ -1,12 +1,3 @@
-/* eslint-disable func-names, space-before-function-paren */
-// This is a manifest file that'll be compiled into including all the files listed below.
-// Add new JavaScript code in separate files in this directory and they'll automatically
-// be included in the compiled file accessible from http://example.com/assets/application.js
-// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
-// the compiled file.
-//
-/*= require_tree . */
-
-(function() {
-
-}).call(this);
+// require everything else in this directory
+function requireAll(context) { return context.keys().map(context); }
+requireAll(require.context('.', false, /^\.\/(?!graphs_bundle).*\.(js|es6)$/));
diff --git a/app/assets/javascripts/graphs/stat_graph.js b/app/assets/javascripts/graphs/stat_graph.js
index 3273bf3a263..2e6da5750de 100644
--- a/app/assets/javascripts/graphs/stat_graph.js
+++ b/app/assets/javascripts/graphs/stat_graph.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-return-assign, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-return-assign, max-len */
(function() {
this.StatGraph = (function() {
function StatGraph() {}
@@ -14,7 +14,5 @@
};
return StatGraph;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/graphs/stat_graph_contributors.js b/app/assets/javascripts/graphs/stat_graph_contributors.js
index 2d08a7c6ac3..d06a1a5dae4 100644
--- a/app/assets/javascripts/graphs/stat_graph_contributors.js
+++ b/app/assets/javascripts/graphs/stat_graph_contributors.js
@@ -1,11 +1,11 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, camelcase, one-var-declaration-per-line, quotes, no-param-reassign, quote-props, comma-dangle, prefer-template, max-len, no-return-assign, padded-blocks */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, camelcase, one-var-declaration-per-line, quotes, no-param-reassign, quote-props, comma-dangle, prefer-template, max-len, no-return-assign */
/* global ContributorsGraph */
/* global ContributorsAuthorGraph */
/* global ContributorsMasterGraph */
/* global ContributorsStatGraphUtil */
/* global d3 */
-/*= require d3 */
+window.d3 = require('d3');
(function() {
this.ContributorsStatGraph = (function() {
@@ -112,7 +112,5 @@
};
return ContributorsStatGraph;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js
index 9c5e9381e52..241249fae63 100644
--- a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js
+++ b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js
@@ -1,11 +1,11 @@
-/* eslint-disable func-names, space-before-function-paren, one-var, no-var, space-before-blocks, prefer-rest-params, max-len, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, comma-dangle, no-return-assign, prefer-arrow-callback, quotes, prefer-template, padded-blocks, newline-per-chained-call, no-else-return */
+/* eslint-disable func-names, space-before-function-paren, one-var, no-var, prefer-rest-params, max-len, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, comma-dangle, no-return-assign, prefer-arrow-callback, quotes, prefer-template, newline-per-chained-call, no-else-return */
/* global d3 */
/* global ContributorsGraph */
-/*= require d3 */
+window.d3 = require('d3');
(function() {
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; },
+ var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; },
extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
hasProp = {}.hasOwnProperty;
@@ -91,7 +91,6 @@
};
return ContributorsGraph;
-
})();
this.ContributorsMasterGraph = (function(superClass) {
@@ -196,7 +195,6 @@
};
return ContributorsMasterGraph;
-
})(ContributorsGraph);
this.ContributorsAuthorGraph = (function(superClass) {
@@ -274,7 +272,5 @@
};
return ContributorsAuthorGraph;
-
})(ContributorsGraph);
-
}).call(this);
diff --git a/app/assets/javascripts/graphs/stat_graph_contributors_util.js b/app/assets/javascripts/graphs/stat_graph_contributors_util.js
index 1982f4af939..29c3163328f 100644
--- a/app/assets/javascripts/graphs/stat_graph_contributors_util.js
+++ b/app/assets/javascripts/graphs/stat_graph_contributors_util.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, object-shorthand, no-var, one-var, camelcase, one-var-declaration-per-line, no-plusplus, comma-dangle, no-param-reassign, no-return-assign, quotes, prefer-arrow-callback, wrap-iife, consistent-return, no-unused-vars, max-len, no-cond-assign, no-else-return, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, object-shorthand, no-var, one-var, camelcase, one-var-declaration-per-line, comma-dangle, no-param-reassign, no-return-assign, quotes, prefer-arrow-callback, wrap-iife, consistent-return, no-unused-vars, max-len, no-cond-assign, no-else-return, max-len */
(function() {
window.ContributorsStatGraphUtil = {
parse_log: function(log) {
@@ -6,7 +6,7 @@
total = {};
by_author = {};
by_email = {};
- for (i = 0, len = log.length; i < len; i++) {
+ for (i = 0, len = log.length; i < len; i += 1) {
entry = log[i];
if (total[entry.date] == null) {
this.add_date(entry.date, total);
@@ -135,5 +135,4 @@
}
}
};
-
}).call(this);
diff --git a/app/assets/javascripts/group_avatar.js b/app/assets/javascripts/group_avatar.js
index 17a76168a79..10dfd05fe3c 100644
--- a/app/assets/javascripts/group_avatar.js
+++ b/app/assets/javascripts/group_avatar.js
@@ -1,13 +1,13 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, no-var, one-var, one-var-declaration-per-line, no-useless-escape, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, no-var, one-var, one-var-declaration-per-line, no-useless-escape, max-len */
(function() {
this.GroupAvatar = (function() {
function GroupAvatar() {
- $('.js-choose-group-avatar-button').bind("click", function() {
+ $('.js-choose-group-avatar-button').on("click", function() {
var form;
form = $(this).closest("form");
return form.find(".js-group-avatar-input").click();
});
- $('.js-group-avatar-input').bind("change", function() {
+ $('.js-group-avatar-input').on("change", function() {
var filename, form;
form = $(this).closest("form");
filename = $(this).val().replace(/^.*[\\\/]/, '');
@@ -16,7 +16,5 @@
}
return GroupAvatar;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/group_label_subscription.js.es6 b/app/assets/javascripts/group_label_subscription.js.es6
index 8e10e424412..15e695e81cf 100644
--- a/app/assets/javascripts/group_label_subscription.js.es6
+++ b/app/assets/javascripts/group_label_subscription.js.es6
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, object-shorthand, comma-dangle, wrap-iife, space-before-function-paren, no-param-reassign, padded-blocks, max-len */
+/* eslint-disable func-names, object-shorthand, comma-dangle, wrap-iife, space-before-function-paren, no-param-reassign, max-len */
(function(global) {
class GroupLabelSubscription {
@@ -50,5 +50,4 @@
}
global.GroupLabelSubscription = GroupLabelSubscription;
-
})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/groups_select.js b/app/assets/javascripts/groups_select.js
index 99700e7562a..bc88dc2d092 100644
--- a/app/assets/javascripts/groups_select.js
+++ b/app/assets/javascripts/groups_select.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, wrap-iife, one-var, camelcase, one-var-declaration-per-line, quotes, object-shorthand, prefer-arrow-callback, comma-dangle, consistent-return, yoda, prefer-rest-params, prefer-spread, no-unused-vars, prefer-template, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, no-var, wrap-iife, one-var, camelcase, one-var-declaration-per-line, quotes, object-shorthand, prefer-arrow-callback, comma-dangle, consistent-return, yoda, prefer-rest-params, prefer-spread, no-unused-vars, prefer-template, max-len */
/* global Api */
(function() {
@@ -59,15 +59,13 @@
} else {
avatar = gon.default_avatar_url;
}
- return "<div class='group-result'> <div class='group-name'>" + group.name + "</div> <div class='group-path'>" + group.path + "</div> </div>";
+ return "<div class='group-result'> <div class='group-name'>" + group.full_name + "</div> <div class='group-path'>" + group.full_path + "</div> </div>";
};
GroupsSelect.prototype.formatSelection = function(group) {
- return group.name;
+ return group.full_name;
};
return GroupsSelect;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/header.js b/app/assets/javascripts/header.js
index c7cbf9ca44b..fa85f9a6c86 100644
--- a/app/assets/javascripts/header.js
+++ b/app/assets/javascripts/header.js
@@ -1,10 +1,8 @@
-/* eslint-disable wrap-iife, func-names, space-before-function-paren, padded-blocks, prefer-arrow-callback, no-var, max-len */
+/* eslint-disable wrap-iife, func-names, space-before-function-paren, prefer-arrow-callback, no-var, max-len */
(function() {
-
$(document).on('todo:toggle', function(e, count) {
var $todoPendingCount = $('.todos-pending-count');
$todoPendingCount.text(gl.text.addDelimiter(count));
$todoPendingCount.toggleClass('hidden', count === 0);
});
-
})();
diff --git a/app/assets/javascripts/importer_status.js b/app/assets/javascripts/importer_status.js
index fa795be07ed..9390136d3d8 100644
--- a/app/assets/javascripts/importer_status.js
+++ b/app/assets/javascripts/importer_status.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, camelcase, no-var, one-var, one-var-declaration-per-line, prefer-template, quotes, object-shorthand, comma-dangle, no-unused-vars, prefer-arrow-callback, no-else-return, padded-blocks, vars-on-top, no-new, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, camelcase, no-var, one-var, one-var-declaration-per-line, prefer-template, quotes, object-shorthand, comma-dangle, no-unused-vars, prefer-arrow-callback, no-else-return, vars-on-top, no-new, max-len */
(function() {
window.ImporterStatus = (function() {
@@ -68,7 +68,6 @@
};
return ImporterStatus;
-
})();
$(function() {
diff --git a/app/assets/javascripts/issuable.js.es6 b/app/assets/javascripts/issuable.js.es6
index 1c10a7445bb..8df86f68218 100644
--- a/app/assets/javascripts/issuable.js.es6
+++ b/app/assets/javascripts/issuable.js.es6
@@ -1,13 +1,12 @@
-/* eslint-disable func-names, no-var, camelcase, no-unused-vars, object-shorthand, space-before-function-paren, no-return-assign, comma-dangle, consistent-return, one-var, one-var-declaration-per-line, quotes, prefer-template, prefer-arrow-callback, prefer-const, padded-blocks, wrap-iife, max-len */
+/* eslint-disable no-param-reassign, func-names, no-var, camelcase, no-unused-vars, object-shorthand, space-before-function-paren, no-return-assign, comma-dangle, consistent-return, one-var, one-var-declaration-per-line, quotes, prefer-template, prefer-arrow-callback, wrap-iife, max-len */
/* global Issuable */
-/* global Turbolinks */
-(function() {
+((global) => {
var issuable_created;
issuable_created = false;
- this.Issuable = {
+ global.Issuable = {
init: function() {
Issuable.initTemplates();
Issuable.initSearch();
@@ -34,7 +33,6 @@
e.preventDefault();
debouncedExecSearch(e);
});
-
},
initSearchState: function($searchInput) {
const currentSearchVal = $searchInput.val();
@@ -111,12 +109,16 @@
filterResults: (function(_this) {
return function(form) {
var formAction, formData, issuesUrl;
- formData = form.serialize();
+ formData = form.serializeArray();
+ formData = formData.filter(function(data) {
+ return data.value !== '';
+ });
+ formData = $.param(formData);
formAction = form.attr('action');
issuesUrl = formAction;
issuesUrl += "" + (formAction.indexOf('?') < 0 ? '?' : '&');
issuesUrl += formData;
- return Turbolinks.visit(issuesUrl);
+ return gl.utils.visitUrl(issuesUrl);
};
})(this),
initResetFilters: function() {
@@ -127,7 +129,7 @@
const baseIssuesUrl = target.href;
$form.attr('action', baseIssuesUrl);
- Turbolinks.visit(baseIssuesUrl);
+ gl.utils.visitUrl(baseIssuesUrl);
});
},
initChecks: function() {
@@ -148,7 +150,7 @@
this.issuableBulkActions.setOriginalDropdownData();
if ($checkedIssues.length > 0) {
- let ids = $.map($checkedIssues, function(value) {
+ const ids = $.map($checkedIssues, function(value) {
return $(value).data('id');
});
$updateIssuesIds.val(ids);
@@ -183,5 +185,4 @@
});
}
};
-
-}).call(this);
+})(window);
diff --git a/app/assets/javascripts/issuable/issuable_bundle.js.es6 b/app/assets/javascripts/issuable/issuable_bundle.js.es6
new file mode 100644
index 00000000000..e927cc0077c
--- /dev/null
+++ b/app/assets/javascripts/issuable/issuable_bundle.js.es6
@@ -0,0 +1 @@
+require('./time_tracking/time_tracking_bundle');
diff --git a/app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js.es6 b/app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js.es6
new file mode 100644
index 00000000000..bf27fbac5d7
--- /dev/null
+++ b/app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js.es6
@@ -0,0 +1,41 @@
+/* global Vue */
+require('../../../lib/utils/pretty_time');
+
+(() => {
+ Vue.component('time-tracking-collapsed-state', {
+ name: 'time-tracking-collapsed-state',
+ props: [
+ 'showComparisonState',
+ 'showSpentOnlyState',
+ 'showEstimateOnlyState',
+ 'showNoTimeTrackingState',
+ 'timeSpentHumanReadable',
+ 'timeEstimateHumanReadable',
+ 'stopwatchSvg',
+ ],
+ methods: {
+ abbreviateTime(timeStr) {
+ return gl.utils.prettyTime.abbreviateTime(timeStr);
+ },
+ },
+ template: `
+ <div class='sidebar-collapsed-icon'>
+ <div v-html='stopwatchSvg'></div>
+ <div class='time-tracking-collapsed-summary'>
+ <div class='compare' v-if='showComparisonState'>
+ <span>{{ abbreviateTime(timeSpentHumanReadable) }} / {{ abbreviateTime(timeEstimateHumanReadable) }}</span>
+ </div>
+ <div class='estimate-only' v-if='showEstimateOnlyState'>
+ <span class='bold'>-- / {{ abbreviateTime(timeEstimateHumanReadable) }}</span>
+ </div>
+ <div class='spend-only' v-if='showSpentOnlyState'>
+ <span class='bold'>{{ abbreviateTime(timeSpentHumanReadable) }} / --</span>
+ </div>
+ <div class='no-tracking' v-if='showNoTimeTrackingState'>
+ <span class='no-value'>None</span>
+ </div>
+ </div>
+ </div>
+ `,
+ });
+})();
diff --git a/app/assets/javascripts/issuable/time_tracking/components/comparison_pane.js.es6 b/app/assets/javascripts/issuable/time_tracking/components/comparison_pane.js.es6
new file mode 100644
index 00000000000..750468c679b
--- /dev/null
+++ b/app/assets/javascripts/issuable/time_tracking/components/comparison_pane.js.es6
@@ -0,0 +1,69 @@
+/* global Vue */
+require('../../../lib/utils/pretty_time');
+
+(() => {
+ const prettyTime = gl.utils.prettyTime;
+
+ Vue.component('time-tracking-comparison-pane', {
+ name: 'time-tracking-comparison-pane',
+ props: [
+ 'timeSpent',
+ 'timeEstimate',
+ 'timeSpentHumanReadable',
+ 'timeEstimateHumanReadable',
+ ],
+ computed: {
+ parsedRemaining() {
+ const diffSeconds = this.timeEstimate - this.timeSpent;
+ return prettyTime.parseSeconds(diffSeconds);
+ },
+ timeRemainingHumanReadable() {
+ return prettyTime.stringifyTime(this.parsedRemaining);
+ },
+ timeRemainingTooltip() {
+ const prefix = this.timeRemainingMinutes < 0 ? 'Over by' : 'Time remaining:';
+ return `${prefix} ${this.timeRemainingHumanReadable}`;
+ },
+ /* Diff values for comparison meter */
+ timeRemainingMinutes() {
+ return this.timeEstimate - this.timeSpent;
+ },
+ timeRemainingPercent() {
+ return `${Math.floor((this.timeSpent / this.timeEstimate) * 100)}%`;
+ },
+ timeRemainingStatusClass() {
+ return this.timeEstimate >= this.timeSpent ? 'within_estimate' : 'over_estimate';
+ },
+ /* Parsed time values */
+ parsedEstimate() {
+ return prettyTime.parseSeconds(this.timeEstimate);
+ },
+ parsedSpent() {
+ return prettyTime.parseSeconds(this.timeSpent);
+ },
+ },
+ template: `
+ <div class='time-tracking-comparison-pane'>
+ <div class='compare-meter' data-toggle='tooltip' data-placement='top' role='timeRemainingDisplay'
+ :aria-valuenow='timeRemainingTooltip'
+ :title='timeRemainingTooltip'
+ :data-original-title='timeRemainingTooltip'
+ :class='timeRemainingStatusClass'>
+ <div class='meter-container' role='timeSpentPercent' :aria-valuenow='timeRemainingPercent'>
+ <div :style='{ width: timeRemainingPercent }' class='meter-fill'></div>
+ </div>
+ <div class='compare-display-container'>
+ <div class='compare-display pull-left'>
+ <span class='compare-label'>Spent</span>
+ <span class='compare-value spent'>{{ timeSpentHumanReadable }}</span>
+ </div>
+ <div class='compare-display estimated pull-right'>
+ <span class='compare-label'>Est</span>
+ <span class='compare-value'>{{ timeEstimateHumanReadable }}</span>
+ </div>
+ </div>
+ </div>
+ </div>
+ `,
+ });
+})();
diff --git a/app/assets/javascripts/issuable/time_tracking/components/estimate_only_pane.js.es6 b/app/assets/javascripts/issuable/time_tracking/components/estimate_only_pane.js.es6
new file mode 100644
index 00000000000..309e9f2f9ef
--- /dev/null
+++ b/app/assets/javascripts/issuable/time_tracking/components/estimate_only_pane.js.es6
@@ -0,0 +1,13 @@
+/* global Vue */
+(() => {
+ Vue.component('time-tracking-estimate-only-pane', {
+ name: 'time-tracking-estimate-only-pane',
+ props: ['timeEstimateHumanReadable'],
+ template: `
+ <div class='time-tracking-estimate-only-pane'>
+ <span class='bold'>Estimated:</span>
+ {{ timeEstimateHumanReadable }}
+ </div>
+ `,
+ });
+})();
diff --git a/app/assets/javascripts/issuable/time_tracking/components/help_state.js.es6 b/app/assets/javascripts/issuable/time_tracking/components/help_state.js.es6
new file mode 100644
index 00000000000..d7ced6d7151
--- /dev/null
+++ b/app/assets/javascripts/issuable/time_tracking/components/help_state.js.es6
@@ -0,0 +1,24 @@
+/* global Vue */
+(() => {
+ Vue.component('time-tracking-help-state', {
+ name: 'time-tracking-help-state',
+ props: ['docsUrl'],
+ template: `
+ <div class='time-tracking-help-state'>
+ <div class='time-tracking-info'>
+ <h4>Track time with slash commands</h4>
+ <p>Slash commands can be used in the issues description and comment boxes.</p>
+ <p>
+ <code>/estimate</code>
+ will update the estimated time with the latest command.
+ </p>
+ <p>
+ <code>/spend</code>
+ will update the sum of the time spent.
+ </p>
+ <a class='btn btn-default learn-more-button' :href='docsUrl'>Learn more</a>
+ </div>
+ </div>
+ `,
+ });
+})();
diff --git a/app/assets/javascripts/issuable/time_tracking/components/no_tracking_pane.js.es6 b/app/assets/javascripts/issuable/time_tracking/components/no_tracking_pane.js.es6
new file mode 100644
index 00000000000..1d2ca643b5b
--- /dev/null
+++ b/app/assets/javascripts/issuable/time_tracking/components/no_tracking_pane.js.es6
@@ -0,0 +1,11 @@
+/* global Vue */
+(() => {
+ Vue.component('time-tracking-no-tracking-pane', {
+ name: 'time-tracking-no-tracking-pane',
+ template: `
+ <div class='time-tracking-no-tracking-pane'>
+ <span class='no-value'>No estimate or time spent</span>
+ </div>
+ `,
+ });
+})();
diff --git a/app/assets/javascripts/issuable/time_tracking/components/spent_only_pane.js.es6 b/app/assets/javascripts/issuable/time_tracking/components/spent_only_pane.js.es6
new file mode 100644
index 00000000000..ed283fec3c3
--- /dev/null
+++ b/app/assets/javascripts/issuable/time_tracking/components/spent_only_pane.js.es6
@@ -0,0 +1,13 @@
+/* global Vue */
+(() => {
+ Vue.component('time-tracking-spent-only-pane', {
+ name: 'time-tracking-spent-only-pane',
+ props: ['timeSpentHumanReadable'],
+ template: `
+ <div class='time-tracking-spend-only-pane'>
+ <span class='bold'>Spent:</span>
+ {{ timeSpentHumanReadable }}
+ </div>
+ `,
+ });
+})();
diff --git a/app/assets/javascripts/issuable/time_tracking/components/time_tracker.js.es6 b/app/assets/javascripts/issuable/time_tracking/components/time_tracker.js.es6
new file mode 100644
index 00000000000..e38f7852b1c
--- /dev/null
+++ b/app/assets/javascripts/issuable/time_tracking/components/time_tracker.js.es6
@@ -0,0 +1,119 @@
+/* global Vue */
+
+require('./help_state');
+require('./collapsed_state');
+require('./spent_only_pane');
+require('./no_tracking_pane');
+require('./estimate_only_pane');
+require('./comparison_pane');
+
+(() => {
+ Vue.component('issuable-time-tracker', {
+ name: 'issuable-time-tracker',
+ props: [
+ 'time_estimate',
+ 'time_spent',
+ 'human_time_estimate',
+ 'human_time_spent',
+ 'stopwatchSvg',
+ 'docsUrl',
+ ],
+ data() {
+ return {
+ showHelp: false,
+ };
+ },
+ computed: {
+ timeSpent() {
+ return this.time_spent;
+ },
+ timeEstimate() {
+ return this.time_estimate;
+ },
+ timeEstimateHumanReadable() {
+ return this.human_time_estimate;
+ },
+ timeSpentHumanReadable() {
+ return this.human_time_spent;
+ },
+ hasTimeSpent() {
+ return !!this.timeSpent;
+ },
+ hasTimeEstimate() {
+ return !!this.timeEstimate;
+ },
+ showComparisonState() {
+ return this.hasTimeEstimate && this.hasTimeSpent;
+ },
+ showEstimateOnlyState() {
+ return this.hasTimeEstimate && !this.hasTimeSpent;
+ },
+ showSpentOnlyState() {
+ return this.hasTimeSpent && !this.hasTimeEstimate;
+ },
+ showNoTimeTrackingState() {
+ return !this.hasTimeEstimate && !this.hasTimeSpent;
+ },
+ showHelpState() {
+ return !!this.showHelp;
+ },
+ },
+ methods: {
+ toggleHelpState(show) {
+ this.showHelp = show;
+ },
+ },
+ template: `
+ <div class='time_tracker time-tracking-component-wrap' v-cloak>
+ <time-tracking-collapsed-state
+ :show-comparison-state='showComparisonState'
+ :show-help-state='showHelpState'
+ :show-spent-only-state='showSpentOnlyState'
+ :show-estimate-only-state='showEstimateOnlyState'
+ :time-spent-human-readable='timeSpentHumanReadable'
+ :time-estimate-human-readable='timeEstimateHumanReadable'
+ :stopwatch-svg='stopwatchSvg'>
+ </time-tracking-collapsed-state>
+ <div class='title hide-collapsed'>
+ Time tracking
+ <div class='help-button pull-right'
+ v-if='!showHelpState'
+ @click='toggleHelpState(true)'>
+ <i class='fa fa-question-circle'></i>
+ </div>
+ <div class='close-help-button pull-right'
+ v-if='showHelpState'
+ @click='toggleHelpState(false)'>
+ <i class='fa fa-close'></i>
+ </div>
+ </div>
+ <div class='time-tracking-content hide-collapsed'>
+ <time-tracking-estimate-only-pane
+ v-if='showEstimateOnlyState'
+ :time-estimate-human-readable='timeEstimateHumanReadable'>
+ </time-tracking-estimate-only-pane>
+ <time-tracking-spent-only-pane
+ v-if='showSpentOnlyState'
+ :time-spent-human-readable='timeSpentHumanReadable'>
+ </time-tracking-spent-only-pane>
+ <time-tracking-no-tracking-pane
+ v-if='showNoTimeTrackingState'>
+ </time-tracking-no-tracking-pane>
+ <time-tracking-comparison-pane
+ v-if='showComparisonState'
+ :time-estimate='timeEstimate'
+ :time-spent='timeSpent'
+ :time-spent-human-readable='timeSpentHumanReadable'
+ :time-estimate-human-readable='timeEstimateHumanReadable'>
+ </time-tracking-comparison-pane>
+ <transition name='help-state-toggle'>
+ <time-tracking-help-state
+ v-if='showHelpState'
+ :docs-url='docsUrl'>
+ </time-tracking-help-state>
+ </transition>
+ </div>
+ </div>
+ `,
+ });
+})();
diff --git a/app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js.es6 b/app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js.es6
new file mode 100644
index 00000000000..1ca01d3bdb9
--- /dev/null
+++ b/app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js.es6
@@ -0,0 +1,62 @@
+/* global Vue */
+
+require('./components/time_tracker');
+require('../../smart_interval');
+require('../../subbable_resource');
+
+(() => {
+ /* This Vue instance represents what will become the parent instance for the
+ * sidebar. It will be responsible for managing `issuable` state and propagating
+ * changes to sidebar components. We will want to create a separate service to
+ * interface with the server at that point.
+ */
+
+ class IssuableTimeTracking {
+ constructor(issuableJSON) {
+ const parsedIssuable = JSON.parse(issuableJSON);
+ return this.initComponent(parsedIssuable);
+ }
+
+ initComponent(parsedIssuable) {
+ this.parentInstance = new Vue({
+ el: '#issuable-time-tracker',
+ data: {
+ issuable: parsedIssuable,
+ },
+ methods: {
+ fetchIssuable() {
+ return gl.IssuableResource.get.call(gl.IssuableResource, {
+ type: 'GET',
+ url: gl.IssuableResource.endpoint,
+ });
+ },
+ updateState(data) {
+ this.issuable = data;
+ },
+ subscribeToUpdates() {
+ gl.IssuableResource.subscribe(data => this.updateState(data));
+ },
+ listenForSlashCommands() {
+ $(document).on('ajax:success', '.gfm-form', (e, data) => {
+ const subscribedCommands = ['spend_time', 'time_estimate'];
+ const changedCommands = data.commands_changes;
+
+ if (changedCommands && _.intersection(subscribedCommands, changedCommands).length) {
+ this.fetchIssuable();
+ }
+ });
+ },
+ },
+ created() {
+ this.fetchIssuable();
+ },
+ mounted() {
+ this.subscribeToUpdates();
+ this.listenForSlashCommands();
+ },
+ });
+ }
+ }
+
+ gl.IssuableTimeTracking = IssuableTimeTracking;
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/issuable_context.js b/app/assets/javascripts/issuable_context.js
index 4aaad111082..c77fbb6a1c7 100644
--- a/app/assets/javascripts/issuable_context.js
+++ b/app/assets/javascripts/issuable_context.js
@@ -1,5 +1,7 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-new, comma-dangle, quotes, prefer-arrow-callback, consistent-return, one-var, no-var, one-var-declaration-per-line, no-underscore-dangle, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-new, comma-dangle, quotes, prefer-arrow-callback, consistent-return, one-var, no-var, one-var-declaration-per-line, no-underscore-dangle, max-len */
/* global UsersSelect */
+/* global Cookies */
+/* global bp */
(function() {
this.IssuableContext = (function() {
@@ -37,6 +39,13 @@
}, 0);
}
});
+ window.addEventListener('beforeunload', function() {
+ // collapsed_gutter cookie hides the sidebar
+ var bpBreakpoint = bp.getBreakpointSize();
+ if (bpBreakpoint === 'xs' || bpBreakpoint === 'sm') {
+ Cookies.set('collapsed_gutter', true);
+ }
+ });
$(".right-sidebar").niceScroll();
}
@@ -66,7 +75,5 @@
};
return IssuableContext;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/issuable_form.js b/app/assets/javascripts/issuable_form.js
index 1c4086517fe..293b856dc4d 100644
--- a/app/assets/javascripts/issuable_form.js
+++ b/app/assets/javascripts/issuable_form.js
@@ -1,11 +1,11 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-use-before-define, no-useless-escape, no-new, quotes, object-shorthand, no-unused-vars, comma-dangle, radix, no-alert, consistent-return, no-else-return, prefer-template, one-var, one-var-declaration-per-line, curly, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-use-before-define, no-useless-escape, no-new, quotes, object-shorthand, no-unused-vars, comma-dangle, no-alert, consistent-return, no-else-return, prefer-template, one-var, one-var-declaration-per-line, curly, max-len */
/* global GitLab */
/* global UsersSelect */
/* global ZenMode */
/* global Autosave */
(function() {
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+ var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
this.IssuableForm = (function() {
IssuableForm.prototype.issueMoveConfirmMsg = 'Are you sure you want to move this issue to another project?';
@@ -51,7 +51,7 @@
IssuableForm.prototype.handleSubmit = function() {
var fieldId = (this.issueMoveField != null) ? this.issueMoveField.val() : null;
- if ((parseInt(fieldId) || 0) > 0) {
+ if ((parseInt(fieldId, 10) || 0) > 0) {
if (!confirm(this.issueMoveConfirmMsg)) {
return false;
}
@@ -150,7 +150,5 @@
};
return IssuableForm;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js
index 63b70d4be17..6c08b1b8e61 100644
--- a/app/assets/javascripts/issue.js
+++ b/app/assets/javascripts/issue.js
@@ -1,12 +1,12 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, one-var, no-underscore-dangle, one-var-declaration-per-line, object-shorthand, no-unused-vars, no-new, comma-dangle, consistent-return, quotes, dot-notation, quote-props, prefer-arrow-callback, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, one-var, no-underscore-dangle, one-var-declaration-per-line, object-shorthand, no-unused-vars, no-new, comma-dangle, consistent-return, quotes, dot-notation, quote-props, prefer-arrow-callback, max-len */
/* global Flash */
-/*= require flash */
-/*= require jquery.waitforimages */
-/*= require task_list */
+require('./flash');
+require('vendor/jquery.waitforimages');
+require('vendor/task_list');
(function() {
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+ var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
this.Issue = (function() {
function Issue() {
@@ -139,22 +139,17 @@
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();
} else {
- $container.find('.checking').hide();
return $container.find('.unavailable').show();
}
});
};
return Issue;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/issue_status_select.js b/app/assets/javascripts/issue_status_select.js
index b39d8274e13..1d6eff11403 100644
--- a/app/assets/javascripts/issue_status_select.js
+++ b/app/assets/javascripts/issue_status_select.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, object-shorthand, no-unused-vars, no-shadow, one-var, one-var-declaration-per-line, comma-dangle, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, object-shorthand, no-unused-vars, no-shadow, one-var, one-var-declaration-per-line, comma-dangle, max-len */
(function() {
this.IssueStatusSelect = (function() {
function IssueStatusSelect() {
@@ -30,7 +30,5 @@
}
return IssueStatusSelect;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/issues_bulk_assignment.js.es6 b/app/assets/javascripts/issues_bulk_assignment.js.es6
index 52fd5d71b18..e0ebd36a65c 100644
--- a/app/assets/javascripts/issues_bulk_assignment.js.es6
+++ b/app/assets/javascripts/issues_bulk_assignment.js.es6
@@ -1,9 +1,8 @@
-/* eslint-disable comma-dangle, quotes, consistent-return, func-names, array-callback-return, space-before-function-paren, prefer-arrow-callback, radix, max-len, padded-blocks, no-unused-expressions, no-sequences, no-underscore-dangle, no-unused-vars, no-param-reassign */
+/* eslint-disable comma-dangle, quotes, consistent-return, func-names, array-callback-return, space-before-function-paren, prefer-arrow-callback, max-len, no-unused-expressions, no-sequences, no-underscore-dangle, no-unused-vars, no-param-reassign */
/* global Issuable */
/* global Flash */
((global) => {
-
class IssuableBulkActions {
constructor({ container, form, issues, prefixId } = {}) {
this.prefixId = prefixId || 'issue_';
@@ -62,7 +61,6 @@
return labels;
}
-
/**
* Will return only labels that were marked previously and the user has unmarked
* @return {Array} Label IDs
@@ -81,7 +79,6 @@
return result;
}
-
/**
* Simple form serialization, it will return just what we need
* Returns key/value pairs from form data
@@ -163,5 +160,4 @@
}
global.IssuableBulkActions = IssuableBulkActions;
-
})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/label_manager.js.es6 b/app/assets/javascripts/label_manager.js.es6
index 33c5e35324d..2a50b72c8aa 100644
--- a/app/assets/javascripts/label_manager.js.es6
+++ b/app/assets/javascripts/label_manager.js.es6
@@ -1,14 +1,14 @@
-/* eslint-disable comma-dangle, class-methods-use-this, no-underscore-dangle, no-param-reassign, no-unused-vars, consistent-return, func-names, space-before-function-paren, padded-blocks, max-len */
+/* eslint-disable comma-dangle, class-methods-use-this, no-underscore-dangle, no-param-reassign, no-unused-vars, consistent-return, func-names, space-before-function-paren, max-len */
/* global Flash */
((global) => {
-
class LabelManager {
constructor({ togglePriorityButton, prioritizedLabels, otherLabels } = {}) {
this.togglePriorityButton = togglePriorityButton || $('.js-toggle-priority');
this.prioritizedLabels = prioritizedLabels || $('.js-prioritized-labels');
this.otherLabels = otherLabels || $('.js-other-labels');
this.errorMessage = 'Unable to update label prioritization at this time';
+ this.emptyState = document.querySelector('#js-priority-labels-empty-state');
this.prioritizedLabels.sortable({
items: 'li',
placeholder: 'list-placeholder',
@@ -30,7 +30,12 @@
const action = $btn.parents('.js-prioritized-labels').length ? 'remove' : 'add';
const $tooltip = $(`#${$btn.find('.has-tooltip:visible').attr('aria-describedby')}`);
$tooltip.tooltip('destroy');
- return _this.toggleLabelPriority($label, action);
+ _this.toggleLabelPriority($label, action);
+ _this.toggleEmptyState($label, $btn, action);
+ }
+
+ toggleEmptyState($label, $btn, action) {
+ this.emptyState.classList.toggle('hidden', !!this.prioritizedLabels[0].querySelector(':scope > li'));
}
toggleLabelPriority($label, action, persistState) {
@@ -104,5 +109,4 @@
}
gl.LabelManager = LabelManager;
-
})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/labels.js b/app/assets/javascripts/labels.js
index 10de13c9a8a..40ad6fc348e 100644
--- a/app/assets/javascripts/labels.js
+++ b/app/assets/javascripts/labels.js
@@ -1,6 +1,6 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, vars-on-top, no-unused-vars, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, vars-on-top, no-unused-vars, max-len */
(function() {
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+ var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
this.Labels = (function() {
function Labels() {
@@ -42,7 +42,5 @@
};
return Labels;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js
index ec2fc87bece..e4cf9057e6d 100644
--- a/app/assets/javascripts/labels_select.js
+++ b/app/assets/javascripts/labels_select.js
@@ -1,13 +1,20 @@
-/* eslint-disable no-useless-return, func-names, space-before-function-paren, wrap-iife, no-var, no-underscore-dangle, prefer-arrow-callback, max-len, one-var, no-unused-vars, one-var-declaration-per-line, prefer-template, no-new, consistent-return, object-shorthand, comma-dangle, no-shadow, no-param-reassign, brace-style, vars-on-top, quotes, no-lonely-if, no-else-return, semi, dot-notation, no-empty, no-return-assign, camelcase, prefer-spread, padded-blocks */
+/* eslint-disable no-useless-return, func-names, space-before-function-paren, wrap-iife, no-var, no-underscore-dangle, prefer-arrow-callback, max-len, one-var, no-unused-vars, one-var-declaration-per-line, prefer-template, no-new, consistent-return, object-shorthand, comma-dangle, no-shadow, no-param-reassign, brace-style, vars-on-top, quotes, no-lonely-if, no-else-return, dot-notation, no-empty, no-return-assign, camelcase, prefer-spread */
/* global Issuable */
/* global ListLabel */
(function() {
this.LabelsSelect = (function() {
- function LabelsSelect() {
- var _this;
+ function LabelsSelect(els) {
+ var _this, $els;
_this = this;
- $('.js-label-select').each(function(i, dropdown) {
+
+ $els = $(els);
+
+ if (!els) {
+ $els = $('.js-label-select');
+ }
+
+ $els.each(function(i, dropdown) {
var $block, $colorPreview, $dropdown, $form, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelHTMLTemplate, labelNoneHTMLTemplate, labelUrl, namespacePath, projectPath, saveLabelData, selectedLabel, showAny, showNo, $sidebarLabelTooltip, initialSelected, $toggleText, fieldName, useId, propertyName, showMenuAbove, $container, $dropdownContainer;
$dropdown = $(dropdown);
$dropdownContainer = $dropdown.closest('.labels-filter');
@@ -324,7 +331,7 @@
multiSelect: $dropdown.hasClass('js-multiselect'),
vue: $dropdown.hasClass('js-issue-board-sidebar'),
clicked: function(label, $el, e, isMarking) {
- var isIssueIndex, isMRIndex, page;
+ var isIssueIndex, isMRIndex, page, boardsModel;
page = $('body').data('page');
isIssueIndex = page === 'projects:issues:index';
@@ -333,31 +340,44 @@
if ($dropdown.parent().find('.is-active:not(.dropdown-clear-active)').length) {
$dropdown.parent()
.find('.dropdown-clear-active')
- .removeClass('is-active')
+ .removeClass('is-active');
}
- if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) {
+ if ($dropdown.hasClass('js-issuable-form-dropdown')) {
+ return;
+ }
+
+ if ($dropdown.hasClass('js-filter-bulk-update')) {
_this.enableBulkLabelDropdown();
_this.setDropdownData($dropdown, isMarking, this.id(label));
return;
}
- if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar')) {
+ if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar') &&
+ !$dropdown.closest('.add-issues-modal').length) {
+ boardsModel = gl.issueBoards.BoardsStore.state.filters;
+ } else if ($dropdown.closest('.add-issues-modal').length) {
+ boardsModel = gl.issueBoards.ModalStore.store.filter;
+ }
+
+ if (boardsModel) {
if (label.isAny) {
- gl.issueBoards.BoardsStore.state.filters['label_name'] = [];
+ boardsModel['label_name'] = [];
}
else if ($el.hasClass('is-active')) {
- gl.issueBoards.BoardsStore.state.filters['label_name'].push(label.title);
+ boardsModel['label_name'].push(label.title);
}
else {
- var filters = gl.issueBoards.BoardsStore.state.filters['label_name'];
+ var filters = boardsModel['label_name'];
filters = filters.filter(function (filteredLabel) {
return filteredLabel !== label.title;
});
- gl.issueBoards.BoardsStore.state.filters['label_name'] = filters;
+ boardsModel['label_name'] = filters;
}
- gl.issueBoards.BoardsStore.updateFiltersUrl();
+ if (!$dropdown.closest('.add-issues-modal').length) {
+ gl.issueBoards.BoardsStore.updateFiltersUrl();
+ }
e.preventDefault();
return;
}
@@ -484,5 +504,4 @@
return LabelsSelect;
})();
-
}).call(this);
diff --git a/app/assets/javascripts/layout_nav.js b/app/assets/javascripts/layout_nav.js
index 2b700539c2b..1c0ea317c1a 100644
--- a/app/assets/javascripts/layout_nav.js
+++ b/app/assets/javascripts/layout_nav.js
@@ -1,4 +1,5 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, prefer-arrow-callback, no-unused-vars, one-var, one-var-declaration-per-line, indent, vars-on-top, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, no-var, prefer-arrow-callback, no-unused-vars, one-var, one-var-declaration-per-line, vars-on-top, max-len */
+
(function() {
var hideEndFade;
@@ -27,10 +28,10 @@
});
$scrollingTabs.each(function () {
- var $this = $(this),
- scrollingTabWidth = $this.width(),
- $active = $this.find('.active'),
- activeWidth = $active.width();
+ var $this = $(this);
+ var scrollingTabWidth = $this.width();
+ var $active = $this.find('.active');
+ var activeWidth = $active.width();
if ($active.length) {
var offset = $active.offset().left + activeWidth;
@@ -43,5 +44,4 @@
}
});
});
-
}).call(this);
diff --git a/app/assets/javascripts/lib/ace.js b/app/assets/javascripts/lib/ace.js
index 4cdf99cae72..9cdc0309503 100644
--- a/app/assets/javascripts/lib/ace.js
+++ b/app/assets/javascripts/lib/ace.js
@@ -1,2 +1,3 @@
-/*= require ace-rails-ap */
+/*= require ace/ace */
/*= require ace/ext-searchbox */
+/*= require ./ace/ace_config_paths */
diff --git a/app/assets/javascripts/lib/ace/ace_config_paths.js.erb b/app/assets/javascripts/lib/ace/ace_config_paths.js.erb
new file mode 100644
index 00000000000..976769ba84a
--- /dev/null
+++ b/app/assets/javascripts/lib/ace/ace_config_paths.js.erb
@@ -0,0 +1,34 @@
+<%
+ace_gem_path = Bundler.rubygems.find_name('ace-rails-ap').first.full_gem_path
+ace_workers = Dir[ace_gem_path + '/vendor/assets/javascripts/ace/worker-*.js'].sort.map do |file|
+ File.basename(file, '.js').sub(/^worker-/, '')
+end
+ace_modes = Dir[ace_gem_path + '/vendor/assets/javascripts/ace/mode-*.js'].sort.map do |file|
+ File.basename(file, '.js').sub(/^mode-/, '')
+end
+%>
+// Lazy-load configuration when ace.edit is called
+(function() {
+ var basePath;
+ var ace = window.ace;
+ var edit = ace.edit;
+ ace.edit = function() {
+ window.gon = window.gon || {};
+ basePath = (window.gon.relative_url_root || '').replace(/\/$/, '') + '/assets/ace';
+ ace.config.set('basePath', basePath);
+
+ // configure paths for all worker modules
+<% ace_workers.each do |worker| %>
+ ace.config.setModuleUrl('ace/mode/<%= worker %>_worker', basePath + '/<%= File.basename(asset_path("ace/worker-#{worker}.js")) %>');
+<% end %>
+
+ // configure paths for all mode modules
+<% ace_modes.each do |mode| %>
+ ace.config.setModuleUrl('ace/mode/<%= mode %>', basePath + '/<%= File.basename(asset_path("ace/mode-#{mode}.js")) %>');
+<% end %>
+
+ // restore original method
+ ace.edit = edit;
+ return ace.edit.apply(ace, arguments);
+ };
+})();
diff --git a/app/assets/javascripts/lib/chart.js b/app/assets/javascripts/lib/chart.js
index d8ad5aaeffe..9b011d89e93 100644
--- a/app/assets/javascripts/lib/chart.js
+++ b/app/assets/javascripts/lib/chart.js
@@ -1,7 +1,3 @@
/* eslint-disable func-names, space-before-function-paren */
-/*= require Chart */
-
-(function() {
-
-}).call(this);
+window.Chart = require('vendor/Chart');
diff --git a/app/assets/javascripts/lib/d3.js b/app/assets/javascripts/lib/d3.js
index 57e7986576c..a9dd32edbed 100644
--- a/app/assets/javascripts/lib/d3.js
+++ b/app/assets/javascripts/lib/d3.js
@@ -1,7 +1,3 @@
/* eslint-disable func-names, space-before-function-paren */
-/*= require d3 */
-
-(function() {
-
-}).call(this);
+window.d3 = require('d3');
diff --git a/app/assets/javascripts/lib/utils/animate.js b/app/assets/javascripts/lib/utils/animate.js
index 83957af94f3..ce090a2e4fd 100644
--- a/app/assets/javascripts/lib/utils/animate.js
+++ b/app/assets/javascripts/lib/utils/animate.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-param-reassign, no-void, prefer-template, no-var, new-cap, prefer-arrow-callback, consistent-return, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-param-reassign, no-void, prefer-template, no-var, new-cap, prefer-arrow-callback, consistent-return, max-len */
(function() {
(function(w) {
if (w.gl == null) {
@@ -46,5 +46,4 @@
return dfd.promise();
};
})(window);
-
}).call(this);
diff --git a/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js.es6 b/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js.es6
index e810ee85bd3..2955bda1a36 100644
--- a/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js.es6
+++ b/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js.es6
@@ -95,7 +95,6 @@
const newState = `${copySource}${this.currentLocation.search}${this.currentLocation.hash}`;
history.replaceState({
- turbolinks: true,
url: newState,
}, document.title, newState);
return newState;
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
deleted file mode 100644
index 8fa80502d92..00000000000
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ /dev/null
@@ -1,119 +0,0 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-unused-expressions, no-param-reassign, no-else-return, quotes, object-shorthand, comma-dangle, camelcase, one-var, vars-on-top, one-var-declaration-per-line, no-return-assign, consistent-return, padded-blocks, max-len, prefer-template */
-(function() {
- (function(w) {
- var base;
- 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;
- }
- };
-
- w.gl.utils.ajaxGet = function(url) {
- return $.ajax({
- type: "GET",
- url: url,
- dataType: "script"
- });
- };
-
- w.gl.utils.extractLast = function(term) {
- return this.split(term).pop();
- };
-
- w.gl.utils.rstrip = function rstrip(val) {
- if (val) {
- return val.replace(/\s+$/, '');
- } else {
- return val;
- }
- };
-
- w.gl.utils.disableButtonIfEmptyField = function(field_selector, button_selector, event_name) {
- event_name = event_name || 'input';
- var closest_submit, field, that;
- that = this;
- field = $(field_selector);
- closest_submit = field.closest('form').find(button_selector);
- if (this.rstrip(field.val()) === "") {
- closest_submit.disable();
- }
- return field.on(event_name, function() {
- if (that.rstrip($(this).val()) === "") {
- return closest_submit.disable();
- } else {
- return closest_submit.enable();
- }
- });
- };
-
- // automatically adjust scroll position for hash urls taking the height of the navbar into account
- // https://github.com/twitter/bootstrap/issues/1768
- w.gl.utils.handleLocationHash = function() {
- var hash = w.gl.utils.getLocationHash();
- if (!hash) return;
-
- var navbar = document.querySelector('.navbar-gitlab');
- var subnav = document.querySelector('.layout-nav');
- var fixedTabs = document.querySelector('.js-tabs-affix');
-
- var adjustment = 0;
- if (navbar) adjustment -= navbar.offsetHeight;
- if (subnav) adjustment -= subnav.offsetHeight;
-
- // scroll to user-generated markdown anchor if we cannot find a match
- if (document.getElementById(hash) === null) {
- var target = document.getElementById('user-content-' + hash);
- if (target && target.scrollIntoView) {
- target.scrollIntoView(true);
- window.scrollBy(0, adjustment);
- }
- } else {
- // only adjust for fixedTabs when not targeting user-generated content
- if (fixedTabs) {
- adjustment -= fixedTabs.offsetHeight;
- }
- window.scrollBy(0, adjustment);
- }
- };
-
- gl.utils.getPagePath = function() {
- return $('body').data('page').split(':')[0];
- };
-
- gl.utils.parseUrl = function (url) {
- var parser = document.createElement('a');
- parser.href = url;
- return parser;
- };
-
- gl.utils.parseUrlPathname = function (url) {
- var parsedUrl = gl.utils.parseUrl(url);
- // parsedUrl.pathname will return an absolute path for Firefox and a relative path for IE11
- // We have to make sure we always have an absolute path.
- return parsedUrl.pathname.charAt(0) === '/' ? parsedUrl.pathname : '/' + parsedUrl.pathname;
- };
-
- gl.utils.isMetaKey = function(e) {
- return e.metaKey || e.ctrlKey || e.altKey || e.shiftKey;
- };
-
- })(window);
-
-}).call(this);
diff --git a/app/assets/javascripts/lib/utils/common_utils.js.es6 b/app/assets/javascripts/lib/utils/common_utils.js.es6
new file mode 100644
index 00000000000..5becf688652
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/common_utils.js.es6
@@ -0,0 +1,256 @@
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-unused-expressions, no-param-reassign, no-else-return, quotes, object-shorthand, comma-dangle, camelcase, one-var, vars-on-top, one-var-declaration-per-line, no-return-assign, consistent-return, max-len, prefer-template */
+(function() {
+ (function(w) {
+ var base;
+ 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;
+ }
+ };
+
+ w.gl.utils.ajaxGet = function(url) {
+ return $.ajax({
+ type: "GET",
+ url: url,
+ dataType: "script"
+ });
+ };
+
+ w.gl.utils.extractLast = function(term) {
+ return this.split(term).pop();
+ };
+
+ w.gl.utils.rstrip = function rstrip(val) {
+ if (val) {
+ return val.replace(/\s+$/, '');
+ } else {
+ return val;
+ }
+ };
+
+ w.gl.utils.disableButtonIfEmptyField = function(field_selector, button_selector, event_name) {
+ event_name = event_name || 'input';
+ var closest_submit, field, that;
+ that = this;
+ field = $(field_selector);
+ closest_submit = field.closest('form').find(button_selector);
+ if (this.rstrip(field.val()) === "") {
+ closest_submit.disable();
+ }
+ return field.on(event_name, function() {
+ if (that.rstrip($(this).val()) === "") {
+ return closest_submit.disable();
+ } else {
+ return closest_submit.enable();
+ }
+ });
+ };
+
+ // automatically adjust scroll position for hash urls taking the height of the navbar into account
+ // https://github.com/twitter/bootstrap/issues/1768
+ w.gl.utils.handleLocationHash = function() {
+ var hash = w.gl.utils.getLocationHash();
+ if (!hash) return;
+
+ // This is required to handle non-unicode characters in hash
+ hash = decodeURIComponent(hash);
+
+ var navbar = document.querySelector('.navbar-gitlab');
+ var subnav = document.querySelector('.layout-nav');
+ var fixedTabs = document.querySelector('.js-tabs-affix');
+
+ var adjustment = 0;
+ if (navbar) adjustment -= navbar.offsetHeight;
+ if (subnav) adjustment -= subnav.offsetHeight;
+
+ // scroll to user-generated markdown anchor if we cannot find a match
+ if (document.getElementById(hash) === null) {
+ var target = document.getElementById('user-content-' + hash);
+ if (target && target.scrollIntoView) {
+ target.scrollIntoView(true);
+ window.scrollBy(0, adjustment);
+ }
+ } else {
+ // only adjust for fixedTabs when not targeting user-generated content
+ if (fixedTabs) {
+ adjustment -= fixedTabs.offsetHeight;
+ }
+ window.scrollBy(0, adjustment);
+ }
+ };
+
+ // Check if element scrolled into viewport from above or below
+ // Courtesy http://stackoverflow.com/a/7557433/414749
+ w.gl.utils.isInViewport = function(el) {
+ var rect = el.getBoundingClientRect();
+
+ return (
+ rect.top >= 0 &&
+ rect.left >= 0 &&
+ rect.bottom <= window.innerHeight &&
+ rect.right <= window.innerWidth
+ );
+ };
+
+ gl.utils.getPagePath = function(index) {
+ index = index || 0;
+ return $('body').data('page').split(':')[index];
+ };
+
+ gl.utils.parseUrl = function (url) {
+ var parser = document.createElement('a');
+ parser.href = url;
+ return parser;
+ };
+
+ gl.utils.parseUrlPathname = function (url) {
+ var parsedUrl = gl.utils.parseUrl(url);
+ // parsedUrl.pathname will return an absolute path for Firefox and a relative path for IE11
+ // We have to make sure we always have an absolute path.
+ return parsedUrl.pathname.charAt(0) === '/' ? parsedUrl.pathname : '/' + parsedUrl.pathname;
+ };
+
+ gl.utils.getUrlParamsArray = function () {
+ // We can trust that each param has one & since values containing & will be encoded
+ // Remove the first character of search as it is always ?
+ return window.location.search.slice(1).split('&');
+ };
+
+ gl.utils.isMetaKey = function(e) {
+ return e.metaKey || e.ctrlKey || e.altKey || e.shiftKey;
+ };
+
+ gl.utils.isMetaClick = function(e) {
+ // Identify following special clicks
+ // 1) Cmd + Click on Mac (e.metaKey)
+ // 2) Ctrl + Click on PC (e.ctrlKey)
+ // 3) Middle-click or Mouse Wheel Click (e.which is 2)
+ return e.metaKey || e.ctrlKey || e.which === 2;
+ };
+
+ gl.utils.scrollToElement = function($el) {
+ var top = $el.offset().top;
+ gl.navBarHeight = gl.navBarHeight || $('.navbar-gitlab').height();
+ gl.navLinksHeight = gl.navLinksHeight || $('.nav-links').height();
+ gl.mrTabsHeight = gl.mrTabsHeight || $('.merge-request-tabs').height();
+
+ return $('body, html').animate({
+ scrollTop: top - (gl.navBarHeight + gl.navLinksHeight + gl.mrTabsHeight)
+ }, 200);
+ };
+
+ /**
+ this will take in the `name` of the param you want to parse in the url
+ if the name does not exist this function will return `null`
+ otherwise it will return the value of the param key provided
+ */
+ w.gl.utils.getParameterByName = (name) => {
+ const url = window.location.href;
+ name = name.replace(/[[\]]/g, '\\$&');
+ const regex = new RegExp(`[?&]${name}(=([^&#]*)|&|#|$)`);
+ const results = regex.exec(url);
+ if (!results) return null;
+ if (!results[2]) return '';
+ return decodeURIComponent(results[2].replace(/\+/g, ' '));
+ };
+
+ w.gl.utils.getSelectedFragment = () => {
+ const selection = window.getSelection();
+ if (selection.rangeCount === 0) return null;
+ const documentFragment = selection.getRangeAt(0).cloneContents();
+ if (documentFragment.textContent.length === 0) return null;
+
+ return documentFragment;
+ };
+
+ w.gl.utils.insertText = (target, text) => {
+ // Firefox doesn't support `document.execCommand('insertText', false, text)` on textareas
+
+ const selectionStart = target.selectionStart;
+ const selectionEnd = target.selectionEnd;
+ const value = target.value;
+
+ const textBefore = value.substring(0, selectionStart);
+ const textAfter = value.substring(selectionEnd, value.length);
+ const newText = textBefore + text + textAfter;
+
+ target.value = newText;
+ target.selectionStart = target.selectionEnd = selectionStart + text.length;
+
+ // Trigger autosave
+ $(target).trigger('input');
+
+ // Trigger autosize
+ var event = document.createEvent('Event');
+ event.initEvent('autosize:update', true, false);
+ target.dispatchEvent(event);
+ };
+
+ w.gl.utils.nodeMatchesSelector = (node, selector) => {
+ const matches = Element.prototype.matches ||
+ Element.prototype.matchesSelector ||
+ Element.prototype.mozMatchesSelector ||
+ Element.prototype.msMatchesSelector ||
+ Element.prototype.oMatchesSelector ||
+ Element.prototype.webkitMatchesSelector;
+
+ if (matches) {
+ return matches.call(node, selector);
+ }
+
+ // IE11 doesn't support `node.matches(selector)`
+
+ let parentNode = node.parentNode;
+ if (!parentNode) {
+ parentNode = document.createElement('div');
+ node = node.cloneNode(true);
+ parentNode.appendChild(node);
+ }
+
+ const matchingNodes = parentNode.querySelectorAll(selector);
+ return Array.prototype.indexOf.call(matchingNodes, node) !== -1;
+ };
+
+ /**
+ this will take in the headers from an API response and normalize them
+ this way we don't run into production issues when nginx gives us lowercased header keys
+ */
+ w.gl.utils.normalizeHeaders = (headers) => {
+ const upperCaseHeaders = {};
+
+ Object.keys(headers).forEach((e) => {
+ upperCaseHeaders[e.toUpperCase()] = headers[e];
+ });
+
+ return upperCaseHeaders;
+ };
+
+ /**
+ * Transforms a DOMStringMap into a plain object.
+ *
+ * @param {DOMStringMap} DOMStringMapObject
+ * @returns {Object}
+ */
+ w.gl.utils.DOMStringMapToObject = DOMStringMapObject => Object.keys(DOMStringMapObject).reduce((acc, element) => {
+ acc[element] = DOMStringMapObject[element];
+ return acc;
+ }, {});
+ })(window);
+}).call(this);
diff --git a/app/assets/javascripts/lib/utils/custom_event_polyfill.js.es6 b/app/assets/javascripts/lib/utils/custom_event_polyfill.js.es6
deleted file mode 100644
index 5ae978010c9..00000000000
--- a/app/assets/javascripts/lib/utils/custom_event_polyfill.js.es6
+++ /dev/null
@@ -1,12 +0,0 @@
-/**
- * CustomEvent support for IE
- */
-if (typeof window.CustomEvent !== 'function') {
- window.CustomEvent = function CustomEvent(e, params) {
- const options = params || { bubbles: false, cancelable: false, detail: undefined };
- const evt = document.createEvent('CustomEvent');
- evt.initCustomEvent(e, options.bubbles, options.cancelable, options.detail);
- return evt;
- };
- window.CustomEvent.prototype = window.Event.prototype;
-}
diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js
deleted file mode 100644
index e8e502694d6..00000000000
--- a/app/assets/javascripts/lib/utils/datetime_utility.js
+++ /dev/null
@@ -1,103 +0,0 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, comma-dangle, no-unused-expressions, prefer-template, padded-blocks, max-len */
-/* global timeago */
-/* global dateFormat */
-
-/*= require timeago */
-/*= require date.format */
-
-(function() {
- (function(w) {
- var base;
- if (w.gl == null) {
- w.gl = {};
- }
- if ((base = w.gl).utils == null) {
- base.utils = {};
- }
- w.gl.utils.days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
-
- w.gl.utils.formatDate = function(datetime) {
- return dateFormat(datetime, 'mmm d, yyyy h:MMtt Z');
- };
-
- w.gl.utils.getDayName = function(date) {
- return this.days[date.getDay()];
- };
-
- w.gl.utils.localTimeAgo = function($timeagoEls, setTimeago) {
- if (setTimeago == null) {
- setTimeago = true;
- }
-
- $timeagoEls.filter(':not([data-timeago-rendered])').each(function() {
- var $el = $(this);
- $el.attr('title', gl.utils.formatDate($el.attr('datetime')));
-
- if (setTimeago) {
- // Recreate with custom template
- $el.tooltip({
- template: '<div class="tooltip local-timeago" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>'
- });
- }
-
- $el.attr('data-timeago-rendered', true);
- gl.utils.renderTimeago($el);
- });
- };
-
- w.gl.utils.getTimeago = function() {
- var locale = function(number, index) {
- return [
- ['less than a minute ago', 'a while'],
- ['less than a minute ago', 'in %s seconds'],
- ['about a minute ago', 'in 1 minute'],
- ['%s minutes ago', 'in %s minutes'],
- ['about an hour ago', 'in 1 hour'],
- ['about %s hours ago', 'in %s hours'],
- ['a day ago', 'in 1 day'],
- ['%s days ago', 'in %s days'],
- ['a week ago', 'in 1 week'],
- ['%s weeks ago', 'in %s weeks'],
- ['a month ago', 'in 1 month'],
- ['%s months ago', 'in %s months'],
- ['a year ago', 'in 1 year'],
- ['%s years ago', 'in %s years']
- ][index];
- };
-
- timeago.register('gl_en', locale);
- return timeago();
- };
-
- w.gl.utils.timeFor = function(time, suffix, expiredLabel) {
- var timefor;
- if (!time) {
- return '';
- }
- suffix || (suffix = 'remaining');
- expiredLabel || (expiredLabel = 'Past due');
- timefor = gl.utils.getTimeago().format(time).replace('in', '');
- if (timefor.indexOf('ago') > -1) {
- timefor = expiredLabel;
- } else {
- timefor = timefor.trim() + ' ' + suffix;
- }
- return timefor;
- };
-
- w.gl.utils.renderTimeago = function($element) {
- var timeagoInstance = gl.utils.getTimeago();
- timeagoInstance.render($element, 'gl_en');
- };
-
- w.gl.utils.getDayDifference = function(a, b) {
- var millisecondsPerDay = 1000 * 60 * 60 * 24;
- var date1 = Date.UTC(a.getFullYear(), a.getMonth(), a.getDate());
- var date2 = Date.UTC(b.getFullYear(), b.getMonth(), b.getDate());
-
- return Math.floor((date2 - date1) / millisecondsPerDay);
- };
-
- })(window);
-
-}).call(this);
diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js.es6 b/app/assets/javascripts/lib/utils/datetime_utility.js.es6
new file mode 100644
index 00000000000..56300926188
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/datetime_utility.js.es6
@@ -0,0 +1,126 @@
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, comma-dangle, no-unused-expressions, prefer-template, max-len */
+/* global timeago */
+/* global dateFormat */
+
+window.timeago = require('vendor/timeago');
+window.dateFormat = require('vendor/date.format');
+
+(function() {
+ (function(w) {
+ var base;
+ var timeagoInstance;
+
+ if (w.gl == null) {
+ w.gl = {};
+ }
+ if ((base = w.gl).utils == null) {
+ base.utils = {};
+ }
+ w.gl.utils.days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
+
+ w.gl.utils.formatDate = function(datetime) {
+ return dateFormat(datetime, 'mmm d, yyyy h:MMtt Z');
+ };
+
+ w.gl.utils.getDayName = function(date) {
+ return this.days[date.getDay()];
+ };
+
+ w.gl.utils.localTimeAgo = function($timeagoEls, setTimeago = true) {
+ $timeagoEls.each((i, el) => {
+ el.setAttribute('title', gl.utils.formatDate(el.getAttribute('datetime')));
+
+ if (setTimeago) {
+ // Recreate with custom template
+ $(el).tooltip({
+ template: '<div class="tooltip local-timeago" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>'
+ });
+ }
+
+ el.classList.add('js-timeago-render');
+ });
+
+ gl.utils.renderTimeago($timeagoEls);
+ };
+
+ w.gl.utils.getTimeago = function() {
+ var locale;
+
+ if (!timeagoInstance) {
+ locale = function(number, index) {
+ return [
+ ['less than a minute ago', 'a while'],
+ ['less than a minute ago', 'in %s seconds'],
+ ['about a minute ago', 'in 1 minute'],
+ ['%s minutes ago', 'in %s minutes'],
+ ['about an hour ago', 'in 1 hour'],
+ ['about %s hours ago', 'in %s hours'],
+ ['a day ago', 'in 1 day'],
+ ['%s days ago', 'in %s days'],
+ ['a week ago', 'in 1 week'],
+ ['%s weeks ago', 'in %s weeks'],
+ ['a month ago', 'in 1 month'],
+ ['%s months ago', 'in %s months'],
+ ['a year ago', 'in 1 year'],
+ ['%s years ago', 'in %s years']
+ ][index];
+ };
+
+ timeago.register('gl_en', locale);
+ timeagoInstance = timeago();
+ }
+
+ return timeagoInstance;
+ };
+
+ w.gl.utils.timeFor = function(time, suffix, expiredLabel) {
+ var timefor;
+ if (!time) {
+ return '';
+ }
+ suffix || (suffix = 'remaining');
+ expiredLabel || (expiredLabel = 'Past due');
+ timefor = gl.utils.getTimeago().format(time).replace('in', '');
+ if (timefor.indexOf('ago') > -1) {
+ timefor = expiredLabel;
+ } else {
+ timefor = timefor.trim() + ' ' + suffix;
+ }
+ return timefor;
+ };
+
+ w.gl.utils.cachedTimeagoElements = [];
+ w.gl.utils.renderTimeago = function($els) {
+ if (!$els && !w.gl.utils.cachedTimeagoElements.length) {
+ w.gl.utils.cachedTimeagoElements = [].slice.call(document.querySelectorAll('.js-timeago-render'));
+ } else if ($els) {
+ w.gl.utils.cachedTimeagoElements = w.gl.utils.cachedTimeagoElements.concat($els.toArray());
+ }
+
+ w.gl.utils.cachedTimeagoElements.forEach(gl.utils.updateTimeagoText);
+ };
+
+ w.gl.utils.updateTimeagoText = function(el) {
+ const timeago = gl.utils.getTimeago();
+ const formattedDate = timeago.format(el.getAttribute('datetime'), 'gl_en');
+
+ if (el.textContent !== formattedDate) {
+ el.textContent = formattedDate;
+ }
+ };
+
+ w.gl.utils.initTimeagoTimeout = function() {
+ gl.utils.renderTimeago();
+
+ gl.utils.timeagoTimeout = setTimeout(gl.utils.initTimeagoTimeout, 1000);
+ };
+
+ w.gl.utils.getDayDifference = function(a, b) {
+ var millisecondsPerDay = 1000 * 60 * 60 * 24;
+ var date1 = Date.UTC(a.getFullYear(), a.getMonth(), a.getDate());
+ var date2 = Date.UTC(b.getFullYear(), b.getMonth(), b.getDate());
+
+ return Math.floor((date2 - date1) / millisecondsPerDay);
+ };
+ })(window);
+}).call(this);
diff --git a/app/assets/javascripts/lib/utils/emoji_aliases.js.erb b/app/assets/javascripts/lib/utils/emoji_aliases.js.erb
deleted file mode 100644
index aeb86c9fa5b..00000000000
--- a/app/assets/javascripts/lib/utils/emoji_aliases.js.erb
+++ /dev/null
@@ -1,6 +0,0 @@
-(function() {
- gl.emojiAliases = function() {
- return JSON.parse('<%= Gitlab::AwardEmoji.aliases.to_json %>');
- };
-
-}).call(this);
diff --git a/app/assets/javascripts/lib/utils/notify.js b/app/assets/javascripts/lib/utils/notify.js
index 3c9ad0e67c8..6d5979603b9 100644
--- a/app/assets/javascripts/lib/utils/notify.js
+++ b/app/assets/javascripts/lib/utils/notify.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, consistent-return, prefer-arrow-callback, no-return-assign, object-shorthand, comma-dangle, no-param-reassign, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, consistent-return, prefer-arrow-callback, no-return-assign, object-shorthand, comma-dangle, no-param-reassign, max-len */
(function() {
(function(w) {
@@ -44,5 +44,4 @@
w.notify = notifyMe;
return w.notifyPermissions = notifyPermissions;
})(window);
-
}).call(this);
diff --git a/app/assets/javascripts/lib/utils/pretty_time.js.es6 b/app/assets/javascripts/lib/utils/pretty_time.js.es6
index ccaf447eb0b..ae397212e55 100644
--- a/app/assets/javascripts/lib/utils/pretty_time.js.es6
+++ b/app/assets/javascripts/lib/utils/pretty_time.js.es6
@@ -4,13 +4,13 @@
* stringifyTime condensed or non-condensed, abbreviateTimelengths)
* */
- class PrettyTime {
-
+ const utils = window.gl.utils = gl.utils || {};
+ const prettyTime = utils.prettyTime = {
/*
* Accepts seconds and returns a timeObject { weeks: #, days: #, hours: #, minutes: # }
* Seconds can be negative or positive, zero or non-zero.
*/
- static parseSeconds(seconds) {
+ parseSeconds(seconds) {
const DAYS_PER_WEEK = 5;
const HOURS_PER_DAY = 8;
const MINUTES_PER_HOUR = 60;
@@ -24,7 +24,7 @@
minutes: 1,
};
- let unorderedMinutes = PrettyTime.secondsToMinutes(seconds);
+ let unorderedMinutes = prettyTime.secondsToMinutes(seconds);
return _.mapObject(timePeriodConstraints, (minutesPerPeriod) => {
const periodCount = Math.floor(unorderedMinutes / minutesPerPeriod);
@@ -33,35 +33,33 @@
return periodCount;
});
- }
+ },
/*
* Accepts a timeObject and returns a condensed string representation of it
* (e.g. '1w 2d 3h 1m' or '1h 30m'). Zero value units are not included.
*/
- static stringifyTime(timeObject) {
+ stringifyTime(timeObject) {
const reducedTime = _.reduce(timeObject, (memo, unitValue, unitName) => {
const isNonZero = !!unitValue;
return isNonZero ? `${memo} ${unitValue}${unitName.charAt(0)}` : memo;
}, '').trim();
return reducedTime.length ? reducedTime : '0m';
- }
+ },
/*
* Accepts a time string of any size (e.g. '1w 2d 3h 5m' or '1w 2d') and returns
* the first non-zero unit/value pair.
*/
- static abbreviateTime(timeStr) {
+ abbreviateTime(timeStr) {
return timeStr.split(' ')
.filter(unitStr => unitStr.charAt(0) !== '0')[0];
- }
+ },
- static secondsToMinutes(seconds) {
+ secondsToMinutes(seconds) {
return Math.abs(seconds / 60);
- }
- }
-
- gl.PrettyTime = PrettyTime;
+ },
+ };
})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js
index ac44b81ee22..d9370db0cf2 100644
--- a/app/assets/javascripts/lib/utils/text_utility.js
+++ b/app/assets/javascripts/lib/utils/text_utility.js
@@ -1,4 +1,5 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, quotes, semi, one-var, one-var-declaration-per-line, operator-assignment, no-else-return, prefer-template, prefer-arrow-callback, no-empty, max-len, consistent-return, no-unused-vars, no-return-assign, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, quotes, one-var, one-var-declaration-per-line, operator-assignment, no-else-return, prefer-template, prefer-arrow-callback, no-empty, max-len, consistent-return, no-unused-vars, no-return-assign, max-len */
+
(function() {
(function(w) {
var base;
@@ -10,13 +11,28 @@
}
gl.text.addDelimiter = function(text) {
return text ? text.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") : text;
- }
+ };
gl.text.randomString = function() {
return Math.random().toString(36).substring(7);
};
gl.text.replaceRange = function(s, start, end, substitute) {
return s.substring(0, start) + substitute + s.substring(end);
};
+ gl.text.getTextWidth = function(text, font) {
+ /**
+ * Uses canvas.measureText to compute and return the width of the given text of given font in pixels.
+ *
+ * @param {String} text The text to be rendered.
+ * @param {String} font The css font descriptor that text is to be rendered with (e.g. "bold 14px verdana").
+ *
+ * @see http://stackoverflow.com/questions/118241/calculate-text-width-with-javascript/21015393#21015393
+ */
+ // re-use canvas object for better performance
+ var canvas = gl.text.getTextWidth.canvas || (gl.text.getTextWidth.canvas = document.createElement('canvas'));
+ var context = canvas.getContext('2d');
+ context.font = font;
+ return context.measureText(text).width;
+ };
gl.text.selectedText = function(text, textarea) {
return text.substring(textarea.selectionStart, textarea.selectionEnd);
};
@@ -44,9 +60,25 @@
}
};
gl.text.insertText = function(textArea, text, tag, blockTag, selected, wrap) {
- var insertText, inserted, selectedSplit, startChar;
+ var insertText, inserted, selectedSplit, startChar, removedLastNewLine, removedFirstNewLine;
+ removedLastNewLine = false;
+ removedFirstNewLine = false;
+
+ // Remove the first newline
+ if (selected.indexOf('\n') === 0) {
+ removedFirstNewLine = true;
+ selected = selected.replace(/\n+/, '');
+ }
+
+ // Remove the last newline
+ if (textArea.selectionEnd - textArea.selectionStart > selected.replace(/\n$/, '').length) {
+ removedLastNewLine = true;
+ selected = selected.replace(/\n$/, '');
+ }
+
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);
@@ -62,6 +94,15 @@
} else {
insertText = "" + startChar + tag + selected + (wrap ? tag : ' ');
}
+
+ if (removedFirstNewLine) {
+ insertText = '\n' + insertText;
+ }
+
+ if (removedLastNewLine) {
+ insertText += '\n';
+ }
+
if (document.queryCommandSupported('insertText')) {
inserted = document.execCommand('insertText', false, insertText);
}
@@ -74,9 +115,9 @@
document.execCommand("ms-endUndoUnit");
} catch (error) {}
}
- return this.moveCursor(textArea, tag, wrap);
+ return this.moveCursor(textArea, tag, wrap, removedLastNewLine);
};
- gl.text.moveCursor = function(textArea, tag, wrapped) {
+ gl.text.moveCursor = function(textArea, tag, wrapped, removedLastNewLine) {
var pos;
if (!textArea.setSelectionRange) {
return;
@@ -87,6 +128,11 @@
} else {
pos = textArea.selectionStart;
}
+
+ if (removedLastNewLine) {
+ pos -= 1;
+ }
+
return textArea.setSelectionRange(pos, pos);
}
};
@@ -114,10 +160,12 @@
};
gl.text.humanize = function(string) {
return string.charAt(0).toUpperCase() + string.replace(/_/g, ' ').slice(1);
- }
+ };
+ gl.text.pluralize = function(str, count) {
+ return str + (count > 1 || count === 0 ? 's' : '');
+ };
return gl.text.truncate = function(string, maxLength) {
return string.substr(0, (maxLength - 3)) + '...';
};
})(window);
-
}).call(this);
diff --git a/app/assets/javascripts/lib/utils/type_utility.js b/app/assets/javascripts/lib/utils/type_utility.js
index 961859dfb5b..6d813d61601 100644
--- a/app/assets/javascripts/lib/utils/type_utility.js
+++ b/app/assets/javascripts/lib/utils/type_utility.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, no-return-assign, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, no-return-assign, max-len */
(function() {
(function(w) {
var base;
@@ -12,5 +12,4 @@
return (obj != null) && (obj.constructor === Object);
};
})(window);
-
}).call(this);
diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js
deleted file mode 100644
index 6872186cd7f..00000000000
--- a/app/assets/javascripts/lib/utils/url_utility.js
+++ /dev/null
@@ -1,81 +0,0 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, one-var, one-var-declaration-per-line, no-void, no-plusplus, guard-for-in, no-restricted-syntax, prefer-template, quotes, padded-blocks, max-len */
-(function() {
- (function(w) {
- var base;
- if (w.gl == null) {
- w.gl = {};
- }
- if ((base = w.gl).utils == null) {
- base.utils = {};
- }
- // Returns an array containing the value(s) of the
- // of the key passed as an argument
- 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].replace(/\+/g, ' '));
- }
- i++;
- }
- return values;
- };
- // @param {Object} params - url keys and value to merge
- // @param {String} url
- 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;
- }
- }
- // Remove a trailing ampersand
- lastChar = newUrl[newUrl.length - 1];
- if (lastChar === '&') {
- newUrl = newUrl.slice(0, -1);
- }
- return newUrl;
- };
- // removes parameter query string from url. returns the modified url
- 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.es6 b/app/assets/javascripts/lib/utils/url_utility.js.es6
new file mode 100644
index 00000000000..a1558b371f0
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/url_utility.js.es6
@@ -0,0 +1,86 @@
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, one-var, one-var-declaration-per-line, no-void, guard-for-in, no-restricted-syntax, prefer-template, quotes, max-len */
+(function() {
+ (function(w) {
+ var base;
+ if (w.gl == null) {
+ w.gl = {};
+ }
+ if ((base = w.gl).utils == null) {
+ base.utils = {};
+ }
+ // Returns an array containing the value(s) of the
+ // of the key passed as an argument
+ 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].replace(/\+/g, ' '));
+ }
+ i += 1;
+ }
+ return values;
+ };
+ // @param {Object} params - url keys and value to merge
+ // @param {String} url
+ 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;
+ }
+ }
+ // Remove a trailing ampersand
+ lastChar = newUrl[newUrl.length - 1];
+ if (lastChar === '&') {
+ newUrl = newUrl.slice(0, -1);
+ }
+ return newUrl;
+ };
+ // removes parameter query string from url. returns the modified url
+ 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 += 1) {
+ 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);
+ };
+
+ w.gl.utils.refreshCurrentPage = () => gl.utils.visitUrl(document.location.href);
+
+ w.gl.utils.visitUrl = (url) => {
+ document.location.href = url;
+ };
+ })(window);
+}).call(this);
diff --git a/app/assets/javascripts/lib/vue_resource.js.es6 b/app/assets/javascripts/lib/vue_resource.js.es6
new file mode 100644
index 00000000000..49babdea2e1
--- /dev/null
+++ b/app/assets/javascripts/lib/vue_resource.js.es6
@@ -0,0 +1,2 @@
+window.Vue = require('vue');
+window.Vue.use(require('vue-resource'));
diff --git a/app/assets/javascripts/line_highlighter.js b/app/assets/javascripts/line_highlighter.js
index 9af89b79f84..d7137ec63e4 100644
--- a/app/assets/javascripts/line_highlighter.js
+++ b/app/assets/javascripts/line_highlighter.js
@@ -1,10 +1,10 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-use-before-define, no-underscore-dangle, no-param-reassign, prefer-template, quotes, comma-dangle, prefer-arrow-callback, consistent-return, one-var, one-var-declaration-per-line, spaced-comment, radix, no-else-return, max-len, no-plusplus, padded-blocks */
+/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-use-before-define, no-underscore-dangle, no-param-reassign, prefer-template, quotes, comma-dangle, prefer-arrow-callback, consistent-return, one-var, one-var-declaration-per-line, no-else-return, max-len */
// LineHighlighter
//
// Handles single- and multi-line selection and highlight for blob views.
//
-/*= require jquery.scrollTo */
+require('vendor/jquery.scrollTo');
//
// ### Example Markup
@@ -31,7 +31,7 @@
// </div>
//
(function() {
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+ var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
this.LineHighlighter = (function() {
// CSS class applied to highlighted lines
@@ -74,8 +74,9 @@
// 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.
- return $('#blob-content-holder').on('click', 'a[data-line-number]', function(event) {
- return event.preventDefault();
+ $('#blob-content-holder').on('click', 'a[data-line-number]', function(event) {
+ event.preventDefault();
+ event.stopPropagation();
});
};
@@ -119,11 +120,11 @@
// Returns an Array
LineHighlighter.prototype.hashToRange = function(hash) {
var first, last, matches;
- //?L(\d+)(?:-(\d+))?$/)
+ // ?L(\d+)(?:-(\d+))?$/)
matches = hash.match(/^#?L(\d+)(?:-(\d+))?$/);
if (matches && matches.length) {
- first = parseInt(matches[1]);
- last = matches[2] ? parseInt(matches[2]) : null;
+ first = parseInt(matches[1], 10);
+ last = matches[2] ? parseInt(matches[2], 10) : null;
return [first, last];
} else {
return [null, null];
@@ -144,7 +145,7 @@
var i, lineNumber, ref, ref1, results;
if (range[1]) {
results = [];
- for (lineNumber = i = ref = range[0], ref1 = range[1]; ref <= ref1 ? i <= ref1 : i >= ref1; lineNumber = ref <= ref1 ? ++i : --i) {
+ for (lineNumber = i = ref = range[0], ref1 = range[1]; ref <= ref1 ? i <= ref1 : i >= ref1; lineNumber = ref <= ref1 ? (i += 1) : (i -= 1)) {
results.push(this.highlightLine(lineNumber));
}
return results;
@@ -170,7 +171,6 @@
// This method is stubbed in tests.
LineHighlighter.prototype.__setLocationHash__ = function(value) {
return history.pushState({
- turbolinks: false,
url: value
// We're using pushState instead of assigning location.hash directly to
// prevent the page from scrolling on the hashchange event
@@ -178,7 +178,5 @@
};
return LineHighlighter;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/logo.js b/app/assets/javascripts/logo.js
index 0ae6df311bb..1b0d0768db8 100644
--- a/app/assets/javascripts/logo.js
+++ b/app/assets/javascripts/logo.js
@@ -1,15 +1,7 @@
-/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, padded-blocks */
-/* global Turbolinks */
+/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback */
(function() {
- Turbolinks.enableProgressBar();
-
- $(document).on('page:fetch', function() {
+ window.addEventListener('beforeunload', function() {
$('.tanuki-logo').addClass('animate');
});
-
- $(document).on('page:change', function() {
- $('.tanuki-logo').removeClass('animate');
- });
-
}).call(this);
diff --git a/app/assets/javascripts/member_expiration_date.js b/app/assets/javascripts/member_expiration_date.js
deleted file mode 100644
index 7741cd29793..00000000000
--- a/app/assets/javascripts/member_expiration_date.js
+++ /dev/null
@@ -1,37 +0,0 @@
-/* eslint-disable func-names, space-before-function-paren, vars-on-top, no-var, object-shorthand, comma-dangle, max-len */
-(function() {
- // Add datepickers to all `js-access-expiration-date` elements. If those elements are
- // children of an element with the `clearable-input` class, and have a sibling
- // `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: function () {
- $(this).trigger('change');
- toggleClearInput.call(this);
- }
- });
-
- 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)
- .trigger('change');
- toggleClearInput.call(input);
- });
-
- inputs.on('blur', toggleClearInput);
-
- inputs.each(toggleClearInput);
- };
-}).call(this);
diff --git a/app/assets/javascripts/member_expiration_date.js.es6 b/app/assets/javascripts/member_expiration_date.js.es6
new file mode 100644
index 00000000000..bf6c0ec2798
--- /dev/null
+++ b/app/assets/javascripts/member_expiration_date.js.es6
@@ -0,0 +1,36 @@
+(() => {
+ // 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.
+ //
+ window.gl = window.gl || {};
+ gl.MemberExpirationDate = (selector = '.js-access-expiration-date') => {
+ function toggleClearInput() {
+ $(this).closest('.clearable-input').toggleClass('has-value', $(this).val() !== '');
+ }
+ const inputs = $(selector);
+
+ inputs.datepicker({
+ dateFormat: 'yy-mm-dd',
+ minDate: 1,
+ onSelect: function onSelect() {
+ $(this).trigger('change');
+ toggleClearInput.call(this);
+ },
+ });
+
+ inputs.next('.js-clear-input').on('click', function clicked(event) {
+ event.preventDefault();
+
+ const input = $(this).closest('.clearable-input').find(selector);
+ input.datepicker('setDate', null)
+ .trigger('change');
+ toggleClearInput.call(input);
+ });
+
+ inputs.on('blur', toggleClearInput);
+
+ inputs.each(toggleClearInput);
+ };
+}).call(this);
diff --git a/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js.es6 b/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js.es6
index f95b079c972..c7e78fed8fe 100644
--- a/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js.es6
+++ b/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js.es6
@@ -1,10 +1,9 @@
-/* eslint-disable comma-dangle, quote-props, no-useless-computed-key, object-shorthand, prefer-const, no-new, padded-blocks, no-param-reassign, semi, max-len */
+/* eslint-disable comma-dangle, quote-props, no-useless-computed-key, object-shorthand, no-new, no-param-reassign, max-len */
/* global Vue */
/* global ace */
/* global Flash */
((global) => {
-
global.mergeConflicts = global.mergeConflicts || {};
global.mergeConflicts.diffFileEditor = Vue.extend({
@@ -19,7 +18,7 @@
loading: false,
fileLoaded: false,
originalContent: '',
- }
+ };
},
computed: {
classObject() {
@@ -51,8 +50,8 @@
$.get(this.file.content_path)
.done((file) => {
- let content = this.$el.querySelector('pre');
- let fileContent = document.createTextNode(file.content);
+ const content = this.$el.querySelector('pre');
+ const fileContent = document.createTextNode(file.content);
content.textContent = fileContent.textContent;
@@ -94,5 +93,4 @@
}
}
});
-
})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js.es6 b/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js.es6
index 74544b7d0c7..240c8f98932 100644
--- a/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js.es6
+++ b/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js.es6
@@ -1,8 +1,7 @@
-/* eslint-disable padded-blocks, no-param-reassign, comma-dangle */
+/* eslint-disable no-param-reassign, comma-dangle */
/* global Vue */
((global) => {
-
global.mergeConflicts = global.mergeConflicts || {};
global.mergeConflicts.inlineConflictLines = Vue.extend({
@@ -11,5 +10,4 @@
},
mixins: [global.mergeConflicts.utils, global.mergeConflicts.actions],
});
-
})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js.es6 b/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js.es6
index 78c00c31c16..97753c50b60 100644
--- a/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js.es6
+++ b/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js.es6
@@ -1,8 +1,7 @@
-/* eslint-disable padded-blocks, no-param-reassign, comma-dangle */
+/* eslint-disable no-param-reassign, comma-dangle */
/* global Vue */
((global) => {
-
global.mergeConflicts = global.mergeConflicts || {};
global.mergeConflicts.parallelConflictLines = Vue.extend({
@@ -26,5 +25,4 @@
</table>
`,
});
-
})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/merge_conflicts/merge_conflict_service.js.es6 b/app/assets/javascripts/merge_conflicts/merge_conflict_service.js.es6
index 8df3170edac..c012b77e0bf 100644
--- a/app/assets/javascripts/merge_conflicts/merge_conflict_service.js.es6
+++ b/app/assets/javascripts/merge_conflicts/merge_conflict_service.js.es6
@@ -1,4 +1,4 @@
-/* eslint-disable no-param-reassign, comma-dangle, no-extra-semi, padded-blocks */
+/* eslint-disable no-param-reassign, comma-dangle */
((global) => {
global.mergeConflicts = global.mergeConflicts || {};
@@ -25,8 +25,7 @@
method: 'POST'
});
}
- };
+ }
global.mergeConflicts.mergeConflictsService = mergeConflictsService;
-
})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6 b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6
index 53b44007510..74587df22c5 100644
--- a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6
+++ b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6
@@ -1,4 +1,4 @@
-/* eslint-disable comma-dangle, object-shorthand, no-dupe-keys, no-param-reassign, no-plusplus, camelcase, prefer-const, no-nested-ternary, no-continue, semi, func-call-spacing, no-spaced-func, padded-blocks, max-len */
+/* eslint-disable comma-dangle, object-shorthand, no-param-reassign, camelcase, no-nested-ternary, no-continue, max-len */
/* global Cookies */
/* global Vue */
@@ -40,7 +40,6 @@
commitMessage: data.commit_message,
sourceBranch: data.source_branch,
targetBranch: data.target_branch,
- commitMessage: data.commit_message,
shortCommitSha: data.commit_sha.slice(0, 7),
};
},
@@ -89,7 +88,7 @@
this.decorateLineForInlineView(line, id, conflict);
file.inlineLines.push(line);
- })
+ });
if (conflict) {
file.inlineLines.push(this.getOriginHeaderLine(id));
@@ -121,7 +120,7 @@
} else {
const lineType = type || 'context';
- linesObj.left.push (this.getLineForParallelView(line, id, lineType));
+ linesObj.left.push(this.getLineForParallelView(line, id, lineType));
linesObj.right.push(this.getLineForParallelView(line, id, lineType, true));
}
});
@@ -129,7 +128,7 @@
this.checkLineLengths(linesObj);
});
- for (let i = 0, len = linesObj.left.length; i < len; i++) {
+ for (let i = 0, len = linesObj.left.length; i < len; i += 1) {
file.parallelLines.push([
linesObj.right[i],
linesObj.left[i]
@@ -162,11 +161,11 @@
if (file.type === CONFLICT_TYPES.TEXT) {
file.sections.forEach((section) => {
if (section.conflict) {
- count++;
+ count += 1;
}
});
} else {
- count++;
+ count += 1;
}
});
@@ -252,17 +251,17 @@
},
checkLineLengths(linesObj) {
- let { left, right } = linesObj;
+ const { left, right } = linesObj;
if (left.length !== right.length) {
if (left.length > right.length) {
const diff = left.length - right.length;
- for (let i = 0; i < diff; i++) {
+ for (let i = 0; i < diff; i += 1) {
right.push({ lineType: 'emptyLine', richText: '' });
}
} else {
const diff = right.length - left.length;
- for (let i = 0; i < diff; i++) {
+ for (let i = 0; i < diff; i += 1) {
left.push({ lineType: 'emptyLine', richText: '' });
}
}
@@ -316,32 +315,31 @@
const hasCommitMessage = $.trim(this.state.conflictsData.commitMessage).length;
let unresolved = 0;
- for (let i = 0, l = files.length; i < l; i++) {
- let file = files[i];
+ for (let i = 0, l = files.length; i < l; i += 1) {
+ const file = files[i];
if (file.resolveMode === INTERACTIVE_RESOLVE_MODE) {
let numberConflicts = 0;
- let resolvedConflicts = Object.keys(file.resolutionData).length
+ const resolvedConflicts = Object.keys(file.resolutionData).length;
// We only check for conflicts type 'text'
// since conflicts `text_editor` can´t be resolved in interactive mode
if (file.type === CONFLICT_TYPES.TEXT) {
- for (let j = 0, k = file.sections.length; j < k; j++) {
+ for (let j = 0, k = file.sections.length; j < k; j += 1) {
if (file.sections[j].conflict) {
- numberConflicts++;
+ numberConflicts += 1;
}
}
if (resolvedConflicts !== numberConflicts) {
- unresolved++;
+ unresolved += 1;
}
}
} else if (file.resolveMode === EDIT_RESOLVE_MODE) {
-
// Unlikely to happen since switching to Edit mode saves content automatically.
// Checking anyway in case the save strategy changes in the future
if (!file.content) {
- unresolved++;
+ unresolved += 1;
continue;
}
}
@@ -366,15 +364,12 @@
};
this.state.conflictsData.files.forEach((file) => {
- let addFile;
-
- addFile = {
+ const addFile = {
old_path: file.old_path,
new_path: file.new_path
};
if (file.type === CONFLICT_TYPES.TEXT) {
-
// Submit only one data for type of editing
if (file.resolveMode === INTERACTIVE_RESOLVE_MODE) {
addFile.sections = file.resolutionData;
@@ -435,5 +430,4 @@
return this.state.conflictsData.files.some(f => f.type === CONFLICT_TYPES.TEXT);
}
};
-
})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6 b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6
index 83520702f9b..653e52fb6bf 100644
--- a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6
+++ b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6
@@ -1,15 +1,15 @@
-/* eslint-disable new-cap, comma-dangle, no-new, semi */
+/* eslint-disable new-cap, comma-dangle, no-new */
/* global Vue */
/* global Flash */
-//= require vue
-//= require ./merge_conflict_store
-//= require ./merge_conflict_service
-//= require ./mixins/line_conflict_utils
-//= require ./mixins/line_conflict_actions
-//= require ./components/diff_file_editor
-//= require ./components/inline_conflict_lines
-//= require ./components/parallel_conflict_lines
+window.Vue = require('vue');
+require('./merge_conflict_store');
+require('./merge_conflict_service');
+require('./mixins/line_conflict_utils');
+require('./mixins/line_conflict_actions');
+require('./components/diff_file_editor');
+require('./components/inline_conflict_lines');
+require('./components/parallel_conflict_lines');
$(() => {
const INTERACTIVE_RESOLVE_MODE = 'interactive';
@@ -29,10 +29,10 @@ $(() => {
'parallel-conflict-lines': gl.mergeConflicts.parallelConflictLines
},
computed: {
- conflictsCountText() { return mergeConflictsStore.getConflictsCountText() },
- readyToCommit() { return mergeConflictsStore.isReadyToCommit() },
- commitButtonText() { return mergeConflictsStore.getCommitButtonText() },
- showDiffViewTypeSwitcher() { return mergeConflictsStore.fileTextTypePresent() }
+ conflictsCountText() { return mergeConflictsStore.getConflictsCountText(); },
+ readyToCommit() { return mergeConflictsStore.isReadyToCommit(); },
+ commitButtonText() { return mergeConflictsStore.getCommitButtonText(); },
+ showDiffViewTypeSwitcher() { return mergeConflictsStore.fileTextTypePresent(); }
},
created() {
mergeConflictsService
@@ -88,5 +88,5 @@ $(() => {
});
}
}
- })
+ });
});
diff --git a/app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js.es6 b/app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js.es6
index e89b35d5407..53e000d7e9e 100644
--- a/app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js.es6
+++ b/app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js.es6
@@ -1,4 +1,4 @@
-/* eslint-disable no-param-reassign, comma-dangle, padded-blocks */
+/* eslint-disable no-param-reassign, comma-dangle */
((global) => {
global.mergeConflicts = global.mergeConflicts || {};
@@ -10,5 +10,4 @@
}
}
};
-
})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js.es6 b/app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js.es6
index a4aca85d460..0f475f62ee6 100644
--- a/app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js.es6
+++ b/app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js.es6
@@ -1,4 +1,4 @@
-/* eslint-disable no-param-reassign, quote-props, comma-dangle, padded-blocks */
+/* eslint-disable no-param-reassign, quote-props, comma-dangle */
((global) => {
global.mergeConflicts = global.mergeConflicts || {};
@@ -16,5 +16,4 @@
}
}
};
-
})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js
index 244c2f6746c..8762ec35b80 100644
--- a/app/assets/javascripts/merge_request.js
+++ b/app/assets/javascripts/merge_request.js
@@ -1,12 +1,12 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, quotes, no-underscore-dangle, one-var, one-var-declaration-per-line, consistent-return, dot-notation, quote-props, comma-dangle, object-shorthand, padded-blocks, max-len, prefer-arrow-callback */
+/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, quotes, no-underscore-dangle, one-var, one-var-declaration-per-line, consistent-return, dot-notation, quote-props, comma-dangle, object-shorthand, max-len, prefer-arrow-callback */
/* global MergeRequestTabs */
-/*= require jquery.waitforimages */
-/*= require task_list */
-/*= require merge_request_tabs */
+require('vendor/jquery.waitforimages');
+require('vendor/task_list');
+require('./merge_request_tabs');
(function() {
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+ var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
this.MergeRequest = (function() {
function MergeRequest(opts) {
@@ -110,9 +110,8 @@
};
MergeRequest.prototype.initCommitMessageListeners = function() {
- var textarea = $('textarea.js-commit-message');
-
- $('a.js-with-description-link').on('click', function(e) {
+ $(document).on('click', 'a.js-with-description-link', function(e) {
+ var textarea = $('textarea.js-commit-message');
e.preventDefault();
textarea.val(textarea.data('messageWithDescription'));
@@ -120,7 +119,8 @@
$('p.js-without-description-hint').show();
});
- $('a.js-without-description-link').on('click', function(e) {
+ $(document).on('click', 'a.js-without-description-link', function(e) {
+ var textarea = $('textarea.js-commit-message');
e.preventDefault();
textarea.val(textarea.data('messageWithoutDescription'));
@@ -130,7 +130,5 @@
};
return MergeRequest;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/merge_request_tabs.js.es6 b/app/assets/javascripts/merge_request_tabs.js.es6
index 3ec0f1fd613..af1ba9ecaf3 100644
--- a/app/assets/javascripts/merge_request_tabs.js.es6
+++ b/app/assets/javascripts/merge_request_tabs.js.es6
@@ -1,11 +1,11 @@
/* eslint-disable no-new, class-methods-use-this */
/* global Breakpoints */
/* global Cookies */
-/* global DiffNotesApp */
/* global Flash */
-/*= require js.cookie */
-/*= require breakpoints */
+require('./breakpoints');
+window.Cookies = require('vendor/js.cookie');
+require('./flash');
/* eslint-disable max-len */
// MergeRequestTabs
@@ -59,16 +59,12 @@
class MergeRequestTabs {
- constructor({ action, setUrl, buildsLoaded, stubLocation } = {}) {
+ constructor({ action, setUrl, stubLocation } = {}) {
this.diffsLoaded = false;
- this.buildsLoaded = false;
- this.pipelinesLoaded = false;
this.commitsLoaded = false;
this.fixedLayoutPref = null;
this.setUrl = setUrl !== undefined ? setUrl : true;
- this.buildsLoaded = buildsLoaded || false;
-
this.setCurrentAction = this.setCurrentAction.bind(this);
this.tabShown = this.tabShown.bind(this);
this.showTab = this.showTab.bind(this);
@@ -86,12 +82,18 @@
$(document)
.on('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown)
.on('click', '.js-show-tab', this.showTab);
+
+ $('.merge-request-tabs a[data-toggle="tab"]')
+ .on('click', this.clickTab);
}
unbindEvents() {
$(document)
.off('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown)
.off('click', '.js-show-tab', this.showTab);
+
+ $('.merge-request-tabs a[data-toggle="tab"]')
+ .off('click', this.clickTab);
}
showTab(e) {
@@ -99,6 +101,14 @@
this.activateTab($(e.target).data('action'));
}
+ clickTab(e) {
+ if (e.target && gl.utils.isMetaClick(e)) {
+ const targetLink = e.target.getAttribute('href');
+ e.stopImmediatePropagation();
+ window.open(targetLink, '_blank');
+ }
+ }
+
tabShown(e) {
const $target = $(e.target);
const action = $target.data('action');
@@ -119,14 +129,6 @@
$.scrollTo('.merge-request-details .merge-request-tabs', {
offset: -navBarHeight,
});
- } else if (action === 'builds') {
- this.loadBuilds($target.attr('href'));
- this.expandView();
- this.resetViewContainer();
- } else if (action === 'pipelines') {
- this.loadPipelines($target.attr('href'));
- this.expandView();
- this.resetViewContainer();
} else {
this.expandView();
this.resetViewContainer();
@@ -180,8 +182,8 @@
setCurrentAction(action) {
this.currentAction = action === 'show' ? 'notes' : action;
- // Remove a trailing '/commits' '/diffs' '/builds' '/pipelines' '/new' '/new/diffs'
- let newState = location.pathname.replace(/\/(commits|diffs|builds|pipelines|new|new\/diffs)(\.html)?\/?$/, '');
+ // Remove a trailing '/commits' '/diffs' '/pipelines' '/new' '/new/diffs'
+ let newState = location.pathname.replace(/\/(commits|diffs|pipelines|new|new\/diffs)(\.html)?\/?$/, '');
// Append the new action if we're on a tab other than 'notes'
if (this.currentAction !== 'notes') {
@@ -191,12 +193,13 @@
// Ensure parameters and hash come along for the ride
newState += location.search + location.hash;
+ // TODO: Consider refactoring in light of turbolinks removal.
+
// Replace the current history state with the new one without breaking
// Turbolinks' history.
//
// See https://github.com/rails/turbolinks/issues/363
window.history.replaceState({
- turbolinks: true,
url: newState,
}, document.title, newState);
@@ -244,44 +247,8 @@
}
this.diffsLoaded = true;
- const diffPage = new gl.Diff();
-
- const locationHash = gl.utils.getLocationHash();
- const anchoredDiff = locationHash && locationHash.split('_')[0];
- if (anchoredDiff) {
- diffPage.openAnchoredDiff(anchoredDiff, () => this.scrollToElement('#diffs'));
- }
- },
- });
- }
-
- loadBuilds(source) {
- if (this.buildsLoaded) {
- return;
- }
- this.ajaxGet({
- url: `${source}.json`,
- success: (data) => {
- document.querySelector('div#builds').innerHTML = data.html;
- gl.utils.localTimeAgo($('.js-timeago', 'div#builds'));
- this.buildsLoaded = true;
- new gl.Pipelines();
- this.scrollToElement('#builds');
- },
- });
- }
-
- loadPipelines(source) {
- if (this.pipelinesLoaded) {
- return;
- }
- this.ajaxGet({
- url: `${source}.json`,
- success: (data) => {
- $('#pipelines').html(data.html);
- gl.utils.localTimeAgo($('.js-timeago', '#pipelines'));
- this.pipelinesLoaded = true;
- this.scrollToElement('#pipelines');
+ new gl.Diff();
+ this.scrollToElement('#diffs');
},
});
}
diff --git a/app/assets/javascripts/merge_request_widget.js.es6 b/app/assets/javascripts/merge_request_widget.js.es6
index e47047c4cca..e5d2d706fc7 100644
--- a/app/assets/javascripts/merge_request_widget.js.es6
+++ b/app/assets/javascripts/merge_request_widget.js.es6
@@ -1,11 +1,12 @@
-/* eslint-disable max-len, no-var, func-names, space-before-function-paren, vars-on-top, no-plusplus, comma-dangle, no-return-assign, consistent-return, no-param-reassign, one-var, one-var-declaration-per-line, quotes, prefer-template, no-else-return, prefer-arrow-callback, no-unused-vars, no-underscore-dangle, no-shadow, no-mixed-operators, template-curly-spacing, camelcase, default-case, wrap-iife, semi, padded-blocks */
+/* eslint-disable max-len, no-var, func-names, space-before-function-paren, vars-on-top, comma-dangle, no-return-assign, consistent-return, no-param-reassign, one-var, one-var-declaration-per-line, quotes, prefer-template, no-else-return, prefer-arrow-callback, no-unused-vars, no-underscore-dangle, no-shadow, no-mixed-operators, camelcase, default-case, wrap-iife */
/* global notify */
/* global notifyPermissions */
/* global merge_request_widget */
-/* global Turbolinks */
+
+require('./smart_interval');
((global) => {
- var indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; };
+ var indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i += 1) { if (i in this && this[i] === item) return i; } return -1; };
const DEPLOYMENT_TEMPLATE = `<div class="mr-widget-heading" id="<%- id %>">
<div class="ci_widget ci-success">
@@ -50,6 +51,8 @@
this.getCIStatus(false);
this.retrieveSuccessIcon();
+ this.initMiniPipelineGraph();
+
this.ciStatusInterval = new global.SmartInterval({
callback: this.getCIStatus.bind(this, true),
startingInterval: 10000,
@@ -65,17 +68,18 @@
incrementByFactorOf: 15000,
immediateExecution: true,
});
+
notifyPermissions();
}
MergeRequestWidget.prototype.clearEventListeners = function() {
- return $(document).off('page:change.merge_request');
+ return $(document).off('DOMContentLoaded');
};
MergeRequestWidget.prototype.addEventListeners = function() {
var allowedPages;
- allowedPages = ['show', 'commits', 'builds', 'pipelines', 'changes'];
- $(document).on('page:change.merge_request', (function(_this) {
+ allowedPages = ['show', 'commits', 'pipelines', 'changes'];
+ $(document).on('DOMContentLoaded', (function(_this) {
return function() {
var page;
page = $('body').data('page').split(':').last();
@@ -90,7 +94,7 @@
const $ciSuccessIcon = $('.js-success-icon');
this.$ciSuccessIcon = $ciSuccessIcon.html();
$ciSuccessIcon.remove();
- }
+ };
MergeRequestWidget.prototype.mergeInProgress = function(deleteSourceBranch) {
if (deleteSourceBranch == null) {
@@ -126,7 +130,9 @@
MergeRequestWidget.prototype.getMergeStatus = function() {
return $.get(this.opts.merge_check_url, function(data) {
- return $('.mr-state-widget').replaceWith(data);
+ var $html = $(data);
+ $('.mr-widget-body').replaceWith($html.find('.mr-widget-body'));
+ $('.mr-widget-footer').replaceWith($html.find('.mr-widget-footer'));
});
};
@@ -152,12 +158,22 @@
return;
}
if (data.environments && data.environments.length) _this.renderEnvironments(data.environments);
- if (data.status !== _this.opts.ci_status && (data.status != null)) {
+ if (data.status !== _this.opts.ci_status ||
+ data.sha !== _this.opts.ci_sha ||
+ data.pipeline !== _this.opts.ci_pipeline) {
_this.opts.ci_status = data.status;
_this.showCIStatus(data.status);
if (data.coverage) {
_this.showCICoverage(data.coverage);
}
+ if (data.pipeline) {
+ _this.opts.ci_pipeline = data.pipeline;
+ _this.updatePipelineUrls(data.pipeline);
+ }
+ if (data.sha) {
+ _this.opts.ci_sha = data.sha;
+ _this.updateCommitUrls(data.sha);
+ }
if (showNotification) {
status = _this.ciLabelForStatus(data.status);
if (status === "preparing") {
@@ -173,7 +189,6 @@
message = message.replace('{{title}}', data.title);
notify(title, message, _this.opts.gitlab_icon, function() {
this.close();
- return Turbolinks.visit(_this.opts.builds_path);
});
}
}
@@ -188,9 +203,9 @@
};
MergeRequestWidget.prototype.renderEnvironments = function(environments) {
- for (let i = 0; i < environments.length; i++) {
+ for (let i = 0; i < environments.length; i += 1) {
const environment = environments[i];
- if ($(`.mr-state-widget #${ environment.id }`).length) return;
+ if ($(`.mr-state-widget #${environment.id}`).length) return;
const $template = $(DEPLOYMENT_TEMPLATE);
if (!environment.external_url || !environment.external_url_formatted) $('.js-environment-link', $template).remove();
@@ -206,7 +221,7 @@
}
environment.ci_success_icon = this.$ciSuccessIcon;
const templateString = _.unescape($template[0].outerHTML);
- const template = _.template(templateString)(environment)
+ const template = _.template(templateString)(environment);
this.$widgetBody.before(template);
}
};
@@ -224,17 +239,20 @@
case "failed":
case "canceled":
case "not_found":
- return this.setMergeButtonClass('btn-danger');
+ this.setMergeButtonClass('btn-danger');
+ break;
case "running":
- return this.setMergeButtonClass('btn-info');
+ this.setMergeButtonClass('btn-info');
+ break;
case "success":
case "success_with_warnings":
- return this.setMergeButtonClass('btn-create');
+ this.setMergeButtonClass('btn-create');
}
} else {
$('.ci_widget.ci-error').show();
- return this.setMergeButtonClass('btn-danger');
+ this.setMergeButtonClass('btn-danger');
}
+ this.initMiniPipelineGraph();
};
MergeRequestWidget.prototype.showCICoverage = function(coverage) {
@@ -247,8 +265,22 @@
return $('.js-merge-button,.accept-action .dropdown-toggle').removeClass('btn-danger btn-info btn-create').addClass(css_class);
};
- return MergeRequestWidget;
+ MergeRequestWidget.prototype.updatePipelineUrls = function(id) {
+ const pipelineUrl = this.opts.pipeline_path;
+ $('.pipeline').text(`#${id}`).attr('href', [pipelineUrl, id].join('/'));
+ };
- })();
+ MergeRequestWidget.prototype.updateCommitUrls = function(id) {
+ const commitsUrl = this.opts.commits_path;
+ $('.js-commit-link').text(`#${id}`).attr('href', [commitsUrl, id].join('/'));
+ };
+ MergeRequestWidget.prototype.initMiniPipelineGraph = function() {
+ new gl.MiniPipelineGraph({
+ container: '.js-pipeline-inline-mr-widget-graph:visible',
+ }).bindEvents();
+ };
+
+ return MergeRequestWidget;
+ })();
})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/merge_request_widget/ci_bundle.js.es6 b/app/assets/javascripts/merge_request_widget/ci_bundle.js.es6
new file mode 100644
index 00000000000..5840916846b
--- /dev/null
+++ b/app/assets/javascripts/merge_request_widget/ci_bundle.js.es6
@@ -0,0 +1,53 @@
+/* global merge_request_widget */
+
+(() => {
+ $(() => {
+ /* TODO: This needs a better home, or should be refactored. It was previously contained
+ * in a script tag in app/views/projects/merge_requests/widget/open/_accept.html.haml,
+ * but Vue chokes on script tags and prevents their execution. So it was moved here
+ * temporarily.
+ * */
+
+ $(document)
+ .off('ajax:send', '.accept-mr-form')
+ .on('ajax:send', '.accept-mr-form', () => {
+ $('.accept-mr-form :input').disable();
+ });
+
+ $(document)
+ .off('click', '.accept_merge_request')
+ .on('click', '.accept_merge_request', () => {
+ $('.js-merge-button').html('<i class="fa fa-spinner fa-spin"></i> Merge in progress');
+ });
+
+ $(document)
+ .off('click', '.merge_when_build_succeeds')
+ .on('click', '.merge_when_build_succeeds', () => {
+ $('#merge_when_build_succeeds').val('1');
+ });
+
+ $(document)
+ .off('click', '.js-merge-dropdown a')
+ .on('click', '.js-merge-dropdown a', (e) => {
+ e.preventDefault();
+ $(e.target).closest('form').submit();
+ });
+ if ($('.rebase-in-progress').length) {
+ merge_request_widget.rebaseInProgress();
+ } else if ($('.rebase-mr-form').length) {
+ $(document)
+ .off('ajax:send', '.rebase-mr-form')
+ .on('ajax:send', '.rebase-mr-form', () => {
+ $('.rebase-mr-form :input').disable();
+ });
+
+ $(document)
+ .off('click', '.js-rebase-button')
+ .on('click', '.js-rebase-button', () => {
+ $('.js-rebase-button').html("<i class='fa fa-spinner fa-spin'></i> Rebase in progress");
+ });
+ } else {
+ setTimeout(() => merge_request_widget.getMergeStatus(), 200);
+ }
+ });
+})();
diff --git a/app/assets/javascripts/merged_buttons.js b/app/assets/javascripts/merged_buttons.js
index 9f8af46c715..527cdc9b698 100644
--- a/app/assets/javascripts/merged_buttons.js
+++ b/app/assets/javascripts/merged_buttons.js
@@ -1,7 +1,7 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, max-len */
(function() {
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+ var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
this.MergedButtons = (function() {
function MergedButtons() {
@@ -41,7 +41,5 @@
};
return MergedButtons;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/milestone.js b/app/assets/javascripts/milestone.js
index 42152362e60..7ce1259e015 100644
--- a/app/assets/javascripts/milestone.js
+++ b/app/assets/javascripts/milestone.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-use-before-define, camelcase, quotes, object-shorthand, no-shadow, no-unused-vars, comma-dangle, no-var, prefer-template, no-underscore-dangle, consistent-return, one-var, one-var-declaration-per-line, default-case, prefer-arrow-callback, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-use-before-define, camelcase, quotes, object-shorthand, no-shadow, no-unused-vars, comma-dangle, no-var, prefer-template, no-underscore-dangle, consistent-return, one-var, one-var-declaration-per-line, default-case, prefer-arrow-callback, max-len */
/* global Flash */
(function() {
@@ -193,7 +193,5 @@
};
return Milestone;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js
index 28054b78249..2f08aa7fe8b 100644
--- a/app/assets/javascripts/milestone_select.js
+++ b/app/assets/javascripts/milestone_select.js
@@ -1,17 +1,24 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-underscore-dangle, prefer-arrow-callback, max-len, one-var, one-var-declaration-per-line, no-unused-vars, object-shorthand, comma-dangle, no-else-return, no-self-compare, consistent-return, no-param-reassign, no-shadow, padded-blocks */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-underscore-dangle, prefer-arrow-callback, max-len, one-var, one-var-declaration-per-line, no-unused-vars, object-shorthand, comma-dangle, no-else-return, no-self-compare, consistent-return, no-param-reassign, no-shadow */
/* global Vue */
/* global Issuable */
/* global ListMilestone */
(function() {
this.MilestoneSelect = (function() {
- function MilestoneSelect(currentProject) {
- var _this;
+ function MilestoneSelect(currentProject, els) {
+ var _this, $els;
if (currentProject != null) {
_this = this;
this.currentProject = JSON.parse(currentProject);
}
- $('.js-milestone-select').each(function(i, dropdown) {
+
+ $els = $(els);
+
+ if (!els) {
+ $els = $('.js-milestone-select');
+ }
+
+ $els.each(function(i, dropdown) {
var $block, $dropdown, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, collapsedSidebarLabelTemplate, defaultLabel, issuableId, issueUpdateURL, milestoneLinkNoneTemplate, milestoneLinkTemplate, milestonesUrl, projectId, selectedMilestone, showAny, showNo, showUpcoming, useId, showMenuAbove;
$dropdown = $(dropdown);
projectId = $dropdown.data('project-id');
@@ -108,7 +115,7 @@
},
vue: $dropdown.hasClass('js-issue-board-sidebar'),
clicked: function(selected, $el, e) {
- var data, isIssueIndex, isMRIndex, page;
+ var data, isIssueIndex, isMRIndex, page, boardsStore;
page = $('body').data('page');
isIssueIndex = page === 'projects:issues:index';
isMRIndex = (page === page && page === 'projects:merge_requests:index');
@@ -116,9 +123,19 @@
e.preventDefault();
return;
}
- if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar')) {
- gl.issueBoards.BoardsStore.state.filters[$dropdown.data('field-name')] = selected.name;
- gl.issueBoards.BoardsStore.updateFiltersUrl();
+
+ if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar') &&
+ !$dropdown.closest('.add-issues-modal').length) {
+ boardsStore = gl.issueBoards.BoardsStore.state.filters;
+ } else if ($dropdown.closest('.add-issues-modal').length) {
+ boardsStore = gl.issueBoards.ModalStore.store.filter;
+ }
+
+ if (boardsStore) {
+ boardsStore[$dropdown.data('field-name')] = selected.name;
+ if (!$dropdown.closest('.add-issues-modal').length) {
+ gl.issueBoards.BoardsStore.updateFiltersUrl();
+ }
e.preventDefault();
} else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
if (selected.name != null) {
@@ -181,7 +198,5 @@
}
return MilestoneSelect;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6 b/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6
index 90b3366f14b..4becbc32681 100644
--- a/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6
+++ b/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6
@@ -10,9 +10,9 @@
* The container should be the table element.
*
* The stage icon clicked needs to have the following HTML structure:
- * <div>
- * <button class="dropdown js-builds-dropdown-button"></button>
- * <div class="js-builds-dropdown-container"></div>
+ * <div class="dropdown">
+ * <button class="dropdown js-builds-dropdown-button" data-toggle="dropdown"></button>
+ * <div class="js-builds-dropdown-container dropdown-menu"></div>
* </div>
*/
(() => {
@@ -21,18 +21,14 @@
this.container = opts.container || '';
this.dropdownListSelector = '.js-builds-dropdown-container';
this.getBuildsList = this.getBuildsList.bind(this);
-
- this.bindEvents();
}
/**
- * Adds and removes the event listener.
+ * Adds the event listener when the dropdown is opened.
+ * All dropdown events are fired at the .dropdown-menu's parent element.
*/
bindEvents() {
- const dropdownButtonSelector = 'button.js-builds-dropdown-button';
-
- $(this.container).off('click', dropdownButtonSelector, this.getBuildsList)
- .on('click', dropdownButtonSelector, this.getBuildsList);
+ $(this.container).on('shown.bs.dropdown', this.getBuildsList);
}
/**
@@ -52,11 +48,14 @@
/**
* For the clicked stage, gets the list of builds.
*
- * @param {Object} e
+ * All dropdown events have a relatedTarget property,
+ * whose value is the toggling anchor element.
+ *
+ * @param {Object} e bootstrap dropdown event
* @return {Promise}
*/
getBuildsList(e) {
- const button = e.currentTarget;
+ const button = e.relatedTarget;
const endpoint = button.dataset.stageEndpoint;
return $.ajax({
diff --git a/app/assets/javascripts/namespace_select.js b/app/assets/javascripts/namespace_select.js
index 6633f2c2709..514556ade0b 100644
--- a/app/assets/javascripts/namespace_select.js
+++ b/app/assets/javascripts/namespace_select.js
@@ -1,8 +1,8 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, one-var, vars-on-top, one-var-declaration-per-line, comma-dangle, object-shorthand, no-else-return, prefer-template, quotes, prefer-arrow-callback, padded-blocks, no-param-reassign, no-cond-assign, max-len */
+/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, one-var, vars-on-top, one-var-declaration-per-line, comma-dangle, object-shorthand, no-else-return, prefer-template, quotes, prefer-arrow-callback, no-param-reassign, no-cond-assign, max-len */
/* global Api */
(function() {
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+ var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
window.NamespaceSelect = (function() {
function NamespaceSelect(opts) {
@@ -63,7 +63,6 @@
};
return NamespaceSelect;
-
})();
window.NamespaceSelects = (function() {
@@ -83,7 +82,5 @@
}
return NamespaceSelects;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/network/branch_graph.js b/app/assets/javascripts/network/branch_graph.js
index 64b19a54893..a7ccd03b60c 100644
--- a/app/assets/javascripts/network/branch_graph.js
+++ b/app/assets/javascripts/network/branch_graph.js
@@ -1,8 +1,8 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, quotes, comma-dangle, one-var, one-var-declaration-per-line, no-mixed-operators, new-cap, no-plusplus, no-loop-func, no-floating-decimal, consistent-return, no-unused-vars, prefer-template, prefer-arrow-callback, camelcase, max-len, padded-blocks */
+/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, quotes, comma-dangle, one-var, one-var-declaration-per-line, no-mixed-operators, new-cap, no-loop-func, no-floating-decimal, consistent-return, no-unused-vars, prefer-template, prefer-arrow-callback, camelcase, max-len */
/* global Raphael */
(function() {
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+ var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
this.BranchGraph = (function() {
function BranchGraph(element1, options1) {
@@ -53,7 +53,7 @@
this.top = this.r.set();
this.barHeight = Math.max(this.graphHeight, this.unitTime * this.days.length + 320);
ref = this.commits;
- for (j = 0, len = ref.length; j < len; j++) {
+ for (j = 0, len = ref.length; j < len; j += 1) {
c = ref[j];
if (c.id in this.parents) {
c.isParent = true;
@@ -68,7 +68,7 @@
var c, j, len, p, ref, results;
ref = this.commits;
results = [];
- for (j = 0, len = ref.length; j < len; j++) {
+ for (j = 0, len = ref.length; j < len; j += 1) {
c = ref[j];
this.mtime = Math.max(this.mtime, c.time);
this.mspace = Math.max(this.mspace, c.space);
@@ -76,7 +76,7 @@
var l, len1, ref1, results1;
ref1 = c.parents;
results1 = [];
- for (l = 0, len1 = ref1.length; l < len1; l++) {
+ for (l = 0, len1 = ref1.length; l < len1; l += 1) {
p = ref1[l];
this.parents[p[0]] = true;
results1.push(this.mspace = Math.max(this.mspace, p[1]));
@@ -96,7 +96,7 @@
// Skipping a few colors in the spectrum to get more contrast between colors
Raphael.getColor();
Raphael.getColor();
- results.push(k++);
+ results.push(k += 1);
}
return results;
};
@@ -113,7 +113,7 @@
fill: "#444"
});
ref = this.days;
- for (mm = j = 0, len = ref.length; j < len; mm = ++j) {
+ for (mm = j = 0, len = ref.length; j < len; mm = (j += 1)) {
day = ref[mm];
if (cuday !== day[0] || cumonth !== day[1]) {
// Dates
@@ -286,7 +286,7 @@
r = this.r;
ref = commit.parents;
results = [];
- for (i = j = 0, len = ref.length; j < len; i = ++j) {
+ for (i = j = 0, len = ref.length; j < len; i = (j += 1)) {
parent = ref[i];
parentCommit = this.preparedCommits[parent[0]];
parentY = this.offsetY + this.unitTime * parentCommit.time;
@@ -346,7 +346,6 @@
};
return BranchGraph;
-
})();
Raphael.prototype.commitTooltip = function(x, y, commit) {
@@ -356,7 +355,7 @@
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);
+ messageText = this.text(x, y + 50, commit.message.replace(/\r?\n/g, " \n "));
textSet = this.set(icon, nameText, idText, messageText).attr({
"text-anchor": "start",
font: "12px Monaco, monospace"
@@ -368,6 +367,7 @@
idText.attr({
fill: "#AAA"
});
+ messageText.node.style["white-space"] = "pre";
this.textWrap(messageText, boxWidth - 50);
rect = this.rect(x - 10, y - 10, boxWidth, 100, 4).attr({
fill: "#FFF",
@@ -398,23 +398,27 @@
words = content.split(" ");
x = 0;
s = [];
- for (j = 0, len = words.length; j < len; j++) {
+ for (j = 0, len = words.length; j < len; j += 1) {
word = words[j];
if (x + (word.length * letterWidth) > width) {
s.push("\n");
x = 0;
}
- x += word.length * letterWidth;
- s.push(word + " ");
+ if (word === "\n") {
+ s.push("\n");
+ x = 0;
+ } else {
+ s.push(word + " ");
+ x += word.length * letterWidth;
+ }
}
t.attr({
- text: s.join("")
+ text: s.join("").trim()
});
b = t.getBBox();
- h = Math.abs(b.y2) - Math.abs(b.y) + 1;
+ h = Math.abs(b.y2) + 1;
return t.attr({
- y: b.y + h
+ y: h
});
};
-
}).call(this);
diff --git a/app/assets/javascripts/network/network.js b/app/assets/javascripts/network/network.js
index 2367d2497b2..37bf6436fd1 100644
--- a/app/assets/javascripts/network/network.js
+++ b/app/assets/javascripts/network/network.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, quote-props, prefer-template, comma-dangle, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, quote-props, prefer-template, comma-dangle, max-len */
/* global BranchGraph */
(function() {
@@ -16,7 +16,5 @@
}
return Network;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/network/network_bundle.js b/app/assets/javascripts/network/network_bundle.js
index 17833d3419a..b4491354472 100644
--- a/app/assets/javascripts/network/network_bundle.js
+++ b/app/assets/javascripts/network/network_bundle.js
@@ -1,14 +1,10 @@
-/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, quotes, no-var, vars-on-top, camelcase, comma-dangle, consistent-return, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, quotes, no-var, vars-on-top, camelcase, comma-dangle, consistent-return, max-len */
/* global Network */
/* global ShortcutsNetwork */
-// This is a manifest file that'll be compiled into including all the files listed below.
-// Add new JavaScript code in separate files in this directory and they'll automatically
-// be included in the compiled file accessible from http://example.com/assets/application.js
-// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
-// the compiled file.
-//
-/*= require_tree . */
+// require everything else in this directory
+function requireAll(context) { return context.keys().map(context); }
+requireAll(require.context('.', false, /^\.\/(?!network_bundle).*\.(js|es6)$/));
(function() {
$(function() {
@@ -23,5 +19,4 @@
});
return new ShortcutsNetwork(network_graph.branch_graph);
});
-
}).call(this);
diff --git a/app/assets/javascripts/new_branch_form.js b/app/assets/javascripts/new_branch_form.js
index 29a323dd4c6..7f763c13b50 100644
--- a/app/assets/javascripts/new_branch_form.js
+++ b/app/assets/javascripts/new_branch_form.js
@@ -1,7 +1,7 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, one-var, space-before-blocks, prefer-rest-params, max-len, vars-on-top, no-plusplus, wrap-iife, consistent-return, comma-dangle, one-var-declaration-per-line, quotes, no-return-assign, prefer-arrow-callback, prefer-template, no-shadow, no-else-return, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, no-var, one-var, prefer-rest-params, max-len, vars-on-top, wrap-iife, consistent-return, comma-dangle, one-var-declaration-per-line, quotes, no-return-assign, prefer-arrow-callback, prefer-template, no-shadow, no-else-return, max-len */
(function() {
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; },
- indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; };
+ var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; },
+ indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i += 1) { if (i in this && this[i] === item) return i; } return -1; };
this.NewBranchForm = (function() {
function NewBranchForm(form, availableRefs) {
@@ -99,7 +99,5 @@
};
return NewBranchForm;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/new_commit_form.js b/app/assets/javascripts/new_commit_form.js
index 8fb8f3e4a5f..41eea78a3e6 100644
--- a/app/assets/javascripts/new_commit_form.js
+++ b/app/assets/javascripts/new_commit_form.js
@@ -1,6 +1,6 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-return-assign, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-return-assign, max-len */
(function() {
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+ var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
this.NewCommitForm = (function() {
function NewCommitForm(form) {
@@ -29,7 +29,5 @@
};
return NewCommitForm;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index a8b9a352870..d108da29af7 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -1,19 +1,20 @@
-/* eslint-disable no-restricted-properties, func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-use-before-define, camelcase, no-unused-expressions, quotes, max-len, one-var, one-var-declaration-per-line, default-case, prefer-template, consistent-return, no-alert, no-return-assign, no-param-reassign, prefer-arrow-callback, no-else-return, comma-dangle, no-new, brace-style, no-lonely-if, vars-on-top, no-unused-vars, semi, indent, no-sequences, no-shadow, newline-per-chained-call, no-useless-escape, radix, padded-blocks */
+/* eslint-disable no-restricted-properties, func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-use-before-define, camelcase, no-unused-expressions, quotes, max-len, one-var, one-var-declaration-per-line, default-case, prefer-template, consistent-return, no-alert, no-return-assign, no-param-reassign, prefer-arrow-callback, no-else-return, comma-dangle, no-new, brace-style, no-lonely-if, vars-on-top, no-unused-vars, no-sequences, no-shadow, newline-per-chained-call, no-useless-escape */
/* global Flash */
-/* global GLForm */
/* global Autosave */
/* global ResolveService */
+/* global mrRefreshWidgetUrl */
-/*= require autosave */
-/*= require autosize */
-/*= require dropzone */
-/*= require dropzone_input */
-/*= require gfm_auto_complete */
-/*= require jquery.atwho */
-/*= require task_list */
+require('./autosave');
+window.autosize = require('vendor/autosize');
+window.Dropzone = require('dropzone');
+require('./dropzone_input');
+require('./gfm_auto_complete');
+require('vendor/jquery.caret'); // required by jquery.atwho
+require('vendor/jquery.atwho');
+require('vendor/task_list');
(function() {
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+ var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
this.Notes = (function() {
const MAX_VISIBLE_COMMIT_LIST_COUNT = 3;
@@ -52,6 +53,12 @@
this.setupMainTargetNoteForm();
this.initTaskList();
this.collapseLongCommitList();
+
+ // We are in the Merge Requests page so we need another edit form for Changes tab
+ if (gl.utils.getPagePath(1) === 'merge_requests') {
+ $('.note-edit-form').clone()
+ .addClass('mr-note-edit-form').insertAfter('.note-edit-form');
+ }
}
Notes.prototype.addBinding = function() {
@@ -63,7 +70,7 @@
// change note in UI after update
$(document).on("ajax:success", "form.edit-note", this.updateNote);
// Edit note link
- $(document).on("click", ".js-note-edit", this.showEditForm);
+ $(document).on("click", ".js-note-edit", this.showEditForm.bind(this));
$(document).on("click", ".note-edit-cancel", this.cancelEdit);
// Reopen and close actions for Issue/MR combined with note form submit
$(document).on("click", ".js-comment-button", this.updateCloseButton);
@@ -213,7 +220,6 @@
})(this));
};
-
/*
Increase @pollingInterval up to 120 seconds on every function call,
if `shouldReset` has a truthy value, 'null' or 'undefined' the variable
@@ -237,6 +243,15 @@
return this.initRefresh();
};
+ Notes.prototype.handleCreateChanges = function(note) {
+ if (typeof note === 'undefined') {
+ return;
+ }
+
+ if (note.commands_changes && note.commands_changes.indexOf('merge') !== -1) {
+ $.get(mrRefreshWidgetUrl);
+ }
+ };
/*
Render note in main comments area.
@@ -277,7 +292,6 @@
}
};
-
/*
Check if note does not exists on page
*/
@@ -290,7 +304,6 @@
return this.view === 'parallel';
};
-
/*
Render note in discussion area.
@@ -341,7 +354,6 @@
return this.updateNotesCount(1);
};
-
/*
Called in response the main target form has been successfully submitted.
@@ -373,7 +385,6 @@
return form.find(".js-note-text").trigger("input");
};
-
/*
Shows the main form and does some setup on it.
@@ -398,7 +409,6 @@
return this.parentTimeline = form.parents('.timeline');
};
-
/*
General note form setup.
@@ -410,12 +420,11 @@
Notes.prototype.setupNoteForm = function(form) {
var textarea;
- new GLForm(form);
+ new gl.GLForm(form);
textarea = form.find(".js-note-text");
return new Autosave(textarea, ["Note", form.find("#note_noteable_type").val(), form.find("#note_noteable_id").val(), form.find("#note_commit_id").val(), form.find("#note_type").val(), form.find("#note_line_code").val(), form.find("#note_position").val()]);
};
-
/*
Called in response to the new note form being submitted
@@ -423,6 +432,7 @@
*/
Notes.prototype.addNote = function(xhr, note, status) {
+ this.handleCreateChanges(note);
return this.renderNote(note);
};
@@ -430,7 +440,6 @@
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
@@ -455,7 +464,6 @@
this.removeDiscussionNoteForm($form);
};
-
/*
Called in response to the edit note form being submitted
@@ -466,6 +474,7 @@
var $html, $note_li;
// Convert returned HTML to a jQuery object so we can modify it further
$html = $(note.html);
+ this.revertNoteEditForm();
gl.utils.localTimeAgo($('.js-timeago', $html));
$html.renderGFM();
$html.find('.js-task-list-container').taskList('enable');
@@ -479,51 +488,56 @@
}
};
+ Notes.prototype.checkContentToAllowEditing = function($el) {
+ var initialContent = $el.find('.original-note-content').text().trim();
+ var currentContent = $el.find('.note-textarea').val();
+ var isAllowed = true;
+
+ if (currentContent === initialContent) {
+ this.removeNoteEditForm($el);
+ }
+ else {
+ var $buttons = $el.find('.note-form-actions');
+ var isWidgetVisible = gl.utils.isInViewport($el.get(0));
+
+ if (!isWidgetVisible) {
+ gl.utils.scrollToElement($el);
+ }
+
+ $el.find('.js-edit-warning').show();
+ isAllowed = false;
+ }
+
+ return isAllowed;
+ };
/*
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');
- // Show the attachment delete link
- note.find(".js-note-attachment-delete").show();
- done = function($noteText) {
- var noteTextVal;
- // 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);
- return $noteText.val('').val(noteTextVal);
- };
- new GLForm(form);
- if ((scrollTo != null) && (myLastNote != null)) {
- // scroll to the bottom
- // so the open of the last element doesn't make a jump
- $('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);
+
+ var $target = $(e.target);
+ var $editForm = $(this.getEditFormSelector($target));
+ var $note = $target.closest('.note');
+ var $currentlyEditing = $('.note.is-editting:visible');
+
+ if ($currentlyEditing.length) {
+ var isEditAllowed = this.checkContentToAllowEditing($currentlyEditing);
+
+ if (!isEditAllowed) {
+ return;
+ }
}
- };
+ $note.find('.js-note-attachment-delete').show();
+ $editForm.addClass('current-note-edit-form');
+ $note.addClass('is-editting');
+ this.putEditFormInPlace($target);
+ };
/*
Called in response to clicking the edit note link
@@ -532,22 +546,43 @@
*/
Notes.prototype.cancelEdit = function(e) {
- var note;
e.preventDefault();
- note = $(e.target).closest('.note');
+ var $target = $(e.target);
+ var note = $target.closest('.note');
+ note.find('.js-edit-warning').hide();
+ this.revertNoteEditForm($target);
return this.removeNoteEditForm(note);
};
+ Notes.prototype.revertNoteEditForm = function($target) {
+ $target = $target || $('.note.is-editting:visible');
+ var selector = this.getEditFormSelector($target);
+ var $editForm = $(selector);
+
+ $editForm.insertBefore('.notes-form');
+ $editForm.find('.js-comment-button').enable();
+ $editForm.find('.js-edit-warning').hide();
+ };
+
+ Notes.prototype.getEditFormSelector = function($el) {
+ var selector = '.note-edit-form:not(.mr-note-edit-form)';
+
+ if ($el.parents('#diffs').length) {
+ selector = '.note-edit-form.mr-note-edit-form';
+ }
+
+ return selector;
+ };
+
Notes.prototype.removeNoteEditForm = function(note) {
- var form;
- form = note.find(".current-note-edit-form");
- note.removeClass("is-editting");
- form.removeClass("current-note-edit-form");
+ var form = note.find('.current-note-edit-form');
+ note.removeClass('is-editting');
+ form.removeClass('current-note-edit-form');
+ form.find('.js-edit-warning').hide();
// Replace markdown textarea text with original note text.
- return form.find(".js-note-text").val(form.find('form.edit-note').data('original-note'));
+ 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.
@@ -587,7 +622,6 @@
return this.updateNotesCount(-1);
};
-
/*
Called in response to clicking the delete attachment link
@@ -604,7 +638,6 @@
return note.find(".current-note-edit-form").remove();
};
-
/*
Called when clicking on the "reply" button for a diff line.
@@ -624,7 +657,6 @@
return this.setupDiscussionNoteForm(replyLink, form);
};
-
/*
Shows the diff or discussion form and does some setup on it.
@@ -666,7 +698,6 @@
.addClass("discussion-form js-discussion-note-form");
};
-
/*
Called when clicking on the "add a comment" button on the side of a diff line.
@@ -723,7 +754,6 @@
}
};
-
/*
Called in response to "cancel" on a diff note form.
@@ -757,7 +787,6 @@
return this.removeDiscussionNoteForm(form);
};
-
/*
Called after an attachment file has been selected.
@@ -772,7 +801,6 @@
return form.find(".js-attachment-filename").text(filename);
};
-
/*
Called when the tab visibility changes
*/
@@ -837,19 +865,50 @@
Notes.prototype.initTaskList = function() {
this.enableTaskList();
- return $(document).on('tasklist:changed', '.note .js-task-list-container', this.updateTaskList);
+ return $(document).on('tasklist:changed', '.note .js-task-list-container', this.updateTaskList.bind(this));
};
Notes.prototype.enableTaskList = function() {
return $('.note .js-task-list-container').taskList('enable');
};
- Notes.prototype.updateTaskList = function() {
- return $('form', this).submit();
+ Notes.prototype.putEditFormInPlace = function($el) {
+ var $editForm = $(this.getEditFormSelector($el));
+ var $note = $el.closest('.note');
+
+ $editForm.insertAfter($note.find('.note-text'));
+
+ var $originalContentEl = $note.find('.original-note-content');
+ var originalContent = $originalContentEl.text().trim();
+ var postUrl = $originalContentEl.data('post-url');
+ var targetId = $originalContentEl.data('target-id');
+ var targetType = $originalContentEl.data('target-type');
+
+ new gl.GLForm($editForm.find('form'));
+
+ $editForm.find('form')
+ .attr('action', postUrl)
+ .attr('data-remote', 'true');
+ $editForm.find('.js-form-target-id').val(targetId);
+ $editForm.find('.js-form-target-type').val(targetType);
+ $editForm.find('.js-note-text').focus().val(originalContent);
+ $editForm.find('.js-md-write-button').trigger('click');
+ $editForm.find('.referenced-users').hide();
+ };
+
+ Notes.prototype.updateTaskList = function(e) {
+ var $target = $(e.target);
+ var $list = $target.closest('.js-task-list-container');
+ var $editForm = $(this.getEditFormSelector($target));
+ var $note = $list.closest('.note');
+
+ this.putEditFormInPlace($list);
+ $editForm.find('#note_note').val($note.find('.original-task-list').val());
+ $('form', $list).submit();
};
Notes.prototype.updateNotesCount = function(updateCount) {
- return this.notesCountBadge.text(parseInt(this.notesCountBadge.text()) + updateCount);
+ return this.notesCountBadge.text(parseInt(this.notesCountBadge.text(), 10) + updateCount);
};
Notes.prototype.resolveDiscussion = function() {
@@ -894,7 +953,5 @@
};
return Notes;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/notifications_dropdown.js b/app/assets/javascripts/notifications_dropdown.js
index 5d0d594073d..926dc35fee8 100644
--- a/app/assets/javascripts/notifications_dropdown.js
+++ b/app/assets/javascripts/notifications_dropdown.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, no-unused-vars, consistent-return, prefer-arrow-callback, no-else-return, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, no-unused-vars, consistent-return, prefer-arrow-callback, no-else-return, max-len */
/* global Flash */
(function() {
@@ -27,7 +27,5 @@
}
return NotificationsDropdown;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/notifications_form.js b/app/assets/javascripts/notifications_form.js
index 2034f9a748a..c3d7cc0adfb 100644
--- a/app/assets/javascripts/notifications_form.js
+++ b/app/assets/javascripts/notifications_form.js
@@ -1,6 +1,6 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, one-var, one-var-declaration-per-line, newline-per-chained-call, comma-dangle, consistent-return, prefer-arrow-callback, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, one-var, one-var-declaration-per-line, newline-per-chained-call, comma-dangle, consistent-return, prefer-arrow-callback, max-len */
(function() {
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+ var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
this.NotificationsForm = (function() {
function NotificationsForm() {
@@ -53,7 +53,5 @@
};
return NotificationsForm;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/pipelines.js.es6 b/app/assets/javascripts/pipelines.js.es6
index 0b09ad113a3..9203abefbbc 100644
--- a/app/assets/javascripts/pipelines.js.es6
+++ b/app/assets/javascripts/pipelines.js.es6
@@ -1,12 +1,10 @@
-/* eslint-disable no-new, guard-for-in, no-restricted-syntax, no-continue, padded-blocks, no-param-reassign, max-len */
+/* eslint-disable no-new, guard-for-in, no-restricted-syntax, no-continue, no-param-reassign, max-len */
-//= require lib/utils/bootstrap_linked_tabs
+require('./lib/utils/bootstrap_linked_tabs');
((global) => {
-
class Pipelines {
constructor(options = {}) {
-
if (options.initTabs && options.tabsOptions) {
new global.LinkedTabs(options.tabsOptions);
}
@@ -37,5 +35,4 @@
}
global.Pipelines = Pipelines;
-
})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/preview_markdown.js b/app/assets/javascripts/preview_markdown.js
index 1e261cd49c2..07eea98e737 100644
--- a/app/assets/javascripts/preview_markdown.js
+++ b/app/assets/javascripts/preview_markdown.js
@@ -1,14 +1,18 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, one-var, one-var-declaration-per-line, wrap-iife, no-else-return, consistent-return, object-shorthand, comma-dangle, no-param-reassign, padded-blocks, camelcase, prefer-arrow-callback, max-len */
+/* eslint-disable func-names, no-var, object-shorthand, comma-dangle, prefer-arrow-callback */
// MarkdownPreview
//
// Handles toggling the "Write" and "Preview" tab clicks, rendering the preview,
// and showing a warning when more than `x` users are referenced.
//
-(function() {
- var lastTextareaPreviewed, markdownPreview, previewButtonSelector, writeButtonSelector;
-
- window.MarkdownPreview = (function() {
+(function () {
+ var lastTextareaPreviewed;
+ var lastTextareaHeight = null;
+ var markdownPreview;
+ var previewButtonSelector;
+ var writeButtonSelector;
+
+ window.MarkdownPreview = (function () {
function MarkdownPreview() {}
// Minimum number of users referenced before triggering a warning
@@ -16,73 +20,71 @@
MarkdownPreview.prototype.ajaxCache = {};
- MarkdownPreview.prototype.showPreview = function(form) {
- var mdText, preview;
- preview = form.find('.js-md-preview');
- mdText = form.find('textarea.markdown-area').val();
+ MarkdownPreview.prototype.showPreview = function ($form) {
+ var mdText;
+ var preview = $form.find('.js-md-preview');
+ if (preview.hasClass('md-preview-loading')) {
+ return;
+ }
+ mdText = $form.find('textarea.markdown-area').val();
+
if (mdText.trim().length === 0) {
preview.text('Nothing to preview.');
- return this.hideReferencedUsers(form);
+ this.hideReferencedUsers($form);
} else {
- preview.text('Loading...');
- return this.renderMarkdown(mdText, (function(_this) {
- return function(response) {
- preview.html(response.body);
- preview.renderGFM();
- return _this.renderReferencedUsers(response.references.users, form);
- };
- })(this));
+ preview.addClass('md-preview-loading').text('Loading...');
+ this.fetchMarkdownPreview(mdText, (function (response) {
+ preview.removeClass('md-preview-loading').html(response.body);
+ preview.renderGFM();
+ this.renderReferencedUsers(response.references.users, $form);
+ }).bind(this));
}
};
- MarkdownPreview.prototype.renderMarkdown = function(text, success) {
+ MarkdownPreview.prototype.fetchMarkdownPreview = function (text, success) {
if (!window.preview_markdown_path) {
return;
}
if (text === this.ajaxCache.text) {
- return success(this.ajaxCache.response);
+ success(this.ajaxCache.response);
+ return;
}
- return $.ajax({
+ $.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);
+ success: (function (response) {
+ this.ajaxCache = {
+ text: text,
+ response: response
};
- })(this)
+ success(response);
+ }).bind(this)
});
};
- MarkdownPreview.prototype.hideReferencedUsers = function(form) {
- var referencedUsers;
- referencedUsers = form.find('.referenced-users');
- return referencedUsers.hide();
+ MarkdownPreview.prototype.hideReferencedUsers = function ($form) {
+ $form.find('.referenced-users').hide();
};
- MarkdownPreview.prototype.renderReferencedUsers = function(users, form) {
+ MarkdownPreview.prototype.renderReferencedUsers = function (users, $form) {
var referencedUsers;
- referencedUsers = form.find('.referenced-users');
+ 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);
+ referencedUsers.find('.js-referenced-users-count').text(users.length);
} else {
- return referencedUsers.hide();
+ referencedUsers.hide();
}
}
};
return MarkdownPreview;
-
- })();
+ }());
markdownPreview = new window.MarkdownPreview();
@@ -92,71 +94,75 @@
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);
+ $.fn.setupMarkdownPreview = function () {
+ var $form = $(this);
+ $form.find('textarea.markdown-area').on('input', function () {
+ markdownPreview.hideReferencedUsers($form);
});
};
- $(document).on('markdown-preview:show', function(e, $form) {
+ $(document).on('markdown-preview:show', function (e, $form) {
if (!$form) {
return;
}
+
lastTextareaPreviewed = $form.find('textarea.markdown-area');
+ lastTextareaHeight = lastTextareaPreviewed.height();
+
// toggle tabs
$form.find(writeButtonSelector).parent().removeClass('active');
$form.find(previewButtonSelector).parent().addClass('active');
+
// toggle content
$form.find('.md-write-holder').hide();
$form.find('.md-preview-holder').show();
- return markdownPreview.showPreview($form);
+ markdownPreview.showPreview($form);
});
- $(document).on('markdown-preview:hide', function(e, $form) {
+ $(document).on('markdown-preview:hide', function (e, $form) {
if (!$form) {
return;
}
lastTextareaPreviewed = null;
+
+ if (lastTextareaHeight) {
+ $form.find('textarea.markdown-area').height(lastTextareaHeight);
+ }
+
// toggle tabs
$form.find(writeButtonSelector).parent().addClass('active');
$form.find(previewButtonSelector).parent().removeClass('active');
+
// toggle content
$form.find('.md-write-holder').show();
$form.find('textarea.markdown-area').focus();
- return $form.find('.md-preview-holder').hide();
+ $form.find('.md-preview-holder').hide();
});
- $(document).on('markdown-preview:toggle', function(e, keyboardEvent) {
+ $(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();
+ keyboardEvent.preventDefault();
} else if (lastTextareaPreviewed) {
$target = lastTextareaPreviewed;
$(document).triggerHandler('markdown-preview:hide', [$target.closest('form')]);
- return keyboardEvent.preventDefault();
+ keyboardEvent.preventDefault();
}
});
- $(document).on('click', previewButtonSelector, function(e) {
+ $(document).on('click', previewButtonSelector, function (e) {
var $form;
e.preventDefault();
$form = $(this).closest('form');
- return $(document).triggerHandler('markdown-preview:show', [$form]);
+ $(document).triggerHandler('markdown-preview:show', [$form]);
});
- $(document).on('click', writeButtonSelector, function(e) {
+ $(document).on('click', writeButtonSelector, function (e) {
var $form;
e.preventDefault();
$form = $(this).closest('form');
- return $(document).triggerHandler('markdown-preview:hide', [$form]);
+ $(document).triggerHandler('markdown-preview:hide', [$form]);
});
-
-}).call(this);
+}());
diff --git a/app/assets/javascripts/profile/gl_crop.js.es6 b/app/assets/javascripts/profile/gl_crop.js.es6
index b4b6da41f63..42e9847af91 100644
--- a/app/assets/javascripts/profile/gl_crop.js.es6
+++ b/app/assets/javascripts/profile/gl_crop.js.es6
@@ -1,14 +1,12 @@
-/* eslint-disable no-useless-escape, max-len, padded-blocks, quotes, no-var, no-underscore-dangle, func-names, space-before-function-paren, no-unused-vars, no-return-assign, object-shorthand, one-var, one-var-declaration-per-line, comma-dangle, consistent-return, class-methods-use-this, no-plusplus, new-parens, semi */
+/* eslint-disable no-useless-escape, max-len, quotes, no-var, no-underscore-dangle, func-names, space-before-function-paren, no-unused-vars, no-return-assign, object-shorthand, one-var, one-var-declaration-per-line, comma-dangle, consistent-return, class-methods-use-this, new-parens */
((global) => {
-
// Matches everything but the file name
const FILENAMEREGEX = /^.*[\\\/]/;
class GitLabCrop {
constructor(input, { filename, previewImage, modalCrop, pickImageEl, uploadImageBtn, modalCropImg,
exportWidth = 200, exportHeight = 200, cropBoxWidth = 200, cropBoxHeight = 200 } = {}) {
-
this.onUploadImageBtnClick = this.onUploadImageBtnClick.bind(this);
this.onModalHide = this.onModalHide.bind(this);
this.onModalShow = this.onModalShow.bind(this);
@@ -136,7 +134,7 @@
var array, binary, i, k, len, v;
binary = atob(dataURL.split(',')[1]);
array = [];
- for (k = i = 0, len = binary.length; i < len; k = ++i) {
+ for (k = i = 0, len = binary.length; i < len; k = (i += 1)) {
v = binary[k];
array.push(binary.charCodeAt(k));
}
@@ -169,6 +167,5 @@
return this.each(function() {
return $(this).data('glcrop', new GitLabCrop(this, opts));
});
- }
-
+ };
})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/profile/profile.js.es6 b/app/assets/javascripts/profile/profile.js.es6
index eb2fe3477a2..5aec9c813fe 100644
--- a/app/assets/javascripts/profile/profile.js.es6
+++ b/app/assets/javascripts/profile/profile.js.es6
@@ -1,8 +1,7 @@
-/* eslint-disable comma-dangle, no-unused-vars, class-methods-use-this, quotes, consistent-return, func-names, prefer-arrow-callback, space-before-function-paren, max-len, padded-blocks */
+/* eslint-disable comma-dangle, no-unused-vars, class-methods-use-this, quotes, consistent-return, func-names, prefer-arrow-callback, space-before-function-paren, max-len */
/* global Flash */
((global) => {
-
class Profile {
constructor({ form } = {}) {
this.onSubmitForm = this.onSubmitForm.bind(this);
@@ -37,19 +36,17 @@
}
onSubmitForm(e) {
+ e.preventDefault();
return this.saveForm();
}
beforeUpdateUsername() {
- $('.loading-username').show();
- $(this).find('.update-success').hide();
- return $(this).find('.update-failed').hide();
+ $('.loading-username', this).removeClass('hidden');
}
afterUpdateUsername() {
- $('.loading-username').hide();
- $(this).find('.btn-save').enable();
- return $(this).find('.loading-gif').hide();
+ $('.loading-username', this).addClass('hidden');
+ $('button[type=submit]', this).enable();
}
onUpdateNotifs(e, data) {
@@ -98,5 +95,4 @@
return new Profile();
}
});
-
})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/profile/profile_bundle.js b/app/assets/javascripts/profile/profile_bundle.js
index f50802bdf2e..d7f3c9fd37e 100644
--- a/app/assets/javascripts/profile/profile_bundle.js
+++ b/app/assets/javascripts/profile/profile_bundle.js
@@ -1,7 +1,3 @@
-/* eslint-disable func-names, space-before-function-paren */
-
-/*= require_tree . */
-
-(function() {
-
-}).call(this);
+// require everything else in this directory
+function requireAll(context) { return context.keys().map(context); }
+requireAll(require.context('.', false, /^\.\/(?!profile_bundle).*\.(js|es6)$/));
diff --git a/app/assets/javascripts/project.js b/app/assets/javascripts/project.js
index fcf3a4af956..71719917d0c 100644
--- a/app/assets/javascripts/project.js
+++ b/app/assets/javascripts/project.js
@@ -1,6 +1,5 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, consistent-return, no-new, prefer-arrow-callback, no-return-assign, one-var, one-var-declaration-per-line, object-shorthand, comma-dangle, no-else-return, newline-per-chained-call, no-shadow, semi, vars-on-top, indent, prefer-template, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, consistent-return, no-new, prefer-arrow-callback, no-return-assign, one-var, one-var-declaration-per-line, object-shorthand, comma-dangle, no-else-return, newline-per-chained-call, no-shadow, vars-on-top, prefer-template, max-len */
/* global Cookies */
-/* global Turbolinks */
/* global ProjectSelect */
(function() {
@@ -58,6 +57,11 @@
};
Project.prototype.initRefSwitcher = function() {
+ var refListItem = document.createElement('li');
+ var refLink = document.createElement('a');
+
+ refLink.href = '#';
+
return $('.js-project-refs-dropdown').each(function() {
var $dropdown, selected;
$dropdown = $(this);
@@ -67,7 +71,8 @@
return $.ajax({
url: $dropdown.data('refs-url'),
data: {
- ref: $dropdown.data('ref')
+ ref: $dropdown.data('ref'),
+ search: term
},
dataType: "json"
}).done(function(refs) {
@@ -76,16 +81,29 @@
},
selectable: true,
filterable: true,
+ filterRemote: true,
filterByText: true,
fieldName: $dropdown.data('field-name'),
renderRow: function(ref) {
- var link;
+ var li = refListItem.cloneNode(false);
+
if (ref.header != null) {
- return $('<li />').addClass('dropdown-header').text(ref.header);
+ li.className = 'dropdown-header';
+ li.textContent = ref.header;
} else {
- link = $('<a />').attr('href', '#').addClass(ref === selected ? 'is-active' : '').text(ref).attr('data-ref', ref);
- return $('<li />').append(link);
+ var link = refLink.cloneNode(false);
+
+ if (ref === selected) {
+ link.className = 'is-active';
+ }
+
+ link.textContent = ref;
+ link.dataset.ref = ref;
+
+ li.appendChild(link);
}
+
+ return li;
},
id: function(obj, $el) {
return $el.attr('data-ref');
@@ -94,12 +112,12 @@
return $el.text().trim();
},
clicked: function(selected, $el, e) {
- e.preventDefault()
+ e.preventDefault();
if ($('input[name="ref"]').length) {
- var $form = $dropdown.closest('form'),
- action = $form.attr('action'),
- divider = action.indexOf('?') < 0 ? '?' : '&';
- Turbolinks.visit(action + '' + divider + '' + $form.serialize());
+ var $form = $dropdown.closest('form');
+ var action = $form.attr('action');
+ var divider = action.indexOf('?') < 0 ? '?' : '&';
+ gl.utils.visitUrl(action + '' + divider + '' + $form.serialize());
}
}
});
@@ -107,7 +125,5 @@
};
return Project;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/project_avatar.js b/app/assets/javascripts/project_avatar.js
index 84f28ede4bf..a6d3ba9eb86 100644
--- a/app/assets/javascripts/project_avatar.js
+++ b/app/assets/javascripts/project_avatar.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, no-useless-escape, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, no-useless-escape, max-len */
(function() {
this.ProjectAvatar = (function() {
function ProjectAvatar() {
@@ -16,7 +16,5 @@
}
return ProjectAvatar;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/project_find_file.js b/app/assets/javascripts/project_find_file.js
index 1bd232314d0..04fe84683f3 100644
--- a/app/assets/javascripts/project_find_file.js
+++ b/app/assets/javascripts/project_find_file.js
@@ -1,8 +1,8 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, quotes, consistent-return, one-var, one-var-declaration-per-line, no-cond-assign, max-len, object-shorthand, no-param-reassign, comma-dangle, no-plusplus, prefer-template, no-unused-vars, no-return-assign, padded-blocks */
+/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, quotes, consistent-return, one-var, one-var-declaration-per-line, no-cond-assign, max-len, object-shorthand, no-param-reassign, comma-dangle, prefer-template, no-unused-vars, no-return-assign */
/* global fuzzaldrinPlus */
(function() {
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+ var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
this.ProjectFindFile = (function() {
var highlighter;
@@ -71,7 +71,7 @@
var blobItemUrl, filePath, html, i, j, len, matches, results;
this.element.find(".tree-table > tbody").empty();
results = [];
- for (i = j = 0, len = filePaths.length; j < len; i = ++j) {
+ for (i = j = 0, len = filePaths.length; j < len; i = (j += 1)) {
filePath = filePaths[i];
if (i === 20) {
break;
@@ -92,7 +92,7 @@
lastIndex = 0;
highlightText = "";
matchedChars = [];
- for (j = 0, len = matches.length; j < len; j++) {
+ for (j = 0, len = matches.length; j < len; j += 1) {
matchIndex = matches[j];
unmatched = text.substring(lastIndex, matchIndex);
if (unmatched) {
@@ -167,7 +167,5 @@
};
return ProjectFindFile;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/project_fork.js b/app/assets/javascripts/project_fork.js
index 4aedc9a2330..208f25a0e33 100644
--- a/app/assets/javascripts/project_fork.js
+++ b/app/assets/javascripts/project_fork.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, max-len */
(function() {
this.ProjectFork = (function() {
function ProjectFork() {
@@ -9,7 +9,5 @@
}
return ProjectFork;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/project_import.js b/app/assets/javascripts/project_import.js
index 02dafcfb865..d7943959238 100644
--- a/app/assets/javascripts/project_import.js
+++ b/app/assets/javascripts/project_import.js
@@ -1,16 +1,13 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, padded-blocks, max-len */
-/* global Turbolinks */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, max-len */
(function() {
this.ProjectImport = (function() {
function ProjectImport() {
setTimeout(function() {
- return Turbolinks.visit(location.href);
+ return gl.utils.visitUrl(location.href);
}, 5000);
}
return ProjectImport;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/project_label_subscription.js.es6 b/app/assets/javascripts/project_label_subscription.js.es6
index b8d6a198996..8365f7118d5 100644
--- a/app/assets/javascripts/project_label_subscription.js.es6
+++ b/app/assets/javascripts/project_label_subscription.js.es6
@@ -1,4 +1,4 @@
-/* eslint-disable wrap-iife, func-names, space-before-function-paren, object-shorthand, comma-dangle, one-var, one-var-declaration-per-line, no-restricted-syntax, prefer-const, max-len, no-param-reassign, padded-blocks */
+/* eslint-disable wrap-iife, func-names, space-before-function-paren, object-shorthand, comma-dangle, one-var, one-var-declaration-per-line, no-restricted-syntax, max-len, no-param-reassign */
(function(global) {
class ProjectLabelSubscription {
@@ -38,8 +38,8 @@
this.$buttons.attr('data-status', newStatus);
this.$buttons.find('> span').text(newAction);
- for (let button of this.$buttons) {
- let $button = $(button);
+ for (const button of this.$buttons) {
+ const $button = $(button);
if ($button.attr('data-original-title')) {
$button.tooltip('hide').attr('data-original-title', newAction).tooltip('fixTitle');
@@ -50,5 +50,4 @@
}
global.ProjectLabelSubscription = ProjectLabelSubscription;
-
})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/project_new.js b/app/assets/javascripts/project_new.js
index 7fc611d0dad..3aa6f6771ce 100644
--- a/app/assets/javascripts/project_new.js
+++ b/app/assets/javascripts/project_new.js
@@ -1,6 +1,7 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-unused-vars, one-var, indent, no-underscore-dangle, prefer-template, no-else-return, prefer-arrow-callback, radix, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-unused-vars, one-var, no-underscore-dangle, prefer-template, no-else-return, prefer-arrow-callback, max-len */
+
(function() {
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+ var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
this.ProjectNew = (function() {
function ProjectNew() {
@@ -14,18 +15,29 @@
return $('.save-project-loader').show();
};
})(this));
+
+ this.initVisibilitySelect();
+
this.toggleSettings();
this.toggleSettingsOnclick();
this.toggleRepoVisibility();
}
+ ProjectNew.prototype.initVisibilitySelect = function() {
+ const visibilityContainer = document.querySelector('.js-visibility-select');
+ if (!visibilityContainer) return;
+ const visibilitySelect = new gl.VisibilitySelect(visibilityContainer);
+ visibilitySelect.init();
+ };
+
ProjectNew.prototype.toggleSettings = function() {
var self = this;
this.$selects.each(function () {
- var $select = $(this),
- className = $select.data('field').replace(/_/g, '-')
- .replace('access-level', 'feature');
+ var $select = $(this);
+ var className = $select.data('field')
+ .replace(/_/g, '-')
+ .replace('access-level', 'feature');
self._showOrHide($select, '.' + className);
});
};
@@ -45,9 +57,9 @@
};
ProjectNew.prototype.toggleRepoVisibility = function () {
- var $repoAccessLevel = $('.js-repo-access-level select'),
- containerRegistry = document.querySelectorAll('.js-container-registry')[0],
- containerRegistryCheckbox = document.getElementById('project_container_registry_enabled');
+ var $repoAccessLevel = $('.js-repo-access-level select');
+ var containerRegistry = document.querySelectorAll('.js-container-registry')[0];
+ var containerRegistryCheckbox = document.getElementById('project_container_registry_enabled');
this.$repoSelects.find("option[value='" + $repoAccessLevel.val() + "']")
.nextAll()
@@ -55,11 +67,11 @@
$repoAccessLevel.off('change')
.on('change', function () {
- var selectedVal = parseInt($repoAccessLevel.val());
+ var selectedVal = parseInt($repoAccessLevel.val(), 10);
this.$repoSelects.each(function () {
- var $this = $(this),
- repoSelectVal = parseInt($this.val());
+ var $this = $(this);
+ var repoSelectVal = parseInt($this.val(), 10);
$this.find('option').show();
@@ -88,7 +100,5 @@
};
return ProjectNew;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/project_select.js b/app/assets/javascripts/project_select.js
index 650996700ba..7b5e9953598 100644
--- a/app/assets/javascripts/project_select.js
+++ b/app/assets/javascripts/project_select.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, no-var, comma-dangle, object-shorthand, one-var, one-var-declaration-per-line, no-else-return, quotes, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, no-var, comma-dangle, object-shorthand, one-var, one-var-declaration-per-line, no-else-return, quotes, max-len */
/* global Api */
(function() {
@@ -15,6 +15,7 @@
},
data: function(term, callback) {
var finalCallback, projectsCallback;
+ var orderBy = $dropdown.data('order-by');
finalCallback = function(projects) {
return callback(projects);
};
@@ -34,7 +35,7 @@
if (this.groupId) {
return Api.groupProjects(this.groupId, term, projectsCallback);
} else {
- return Api.projects(term, this.orderBy, projectsCallback);
+ return Api.projects(term, orderBy, projectsCallback);
}
},
url: function(project) {
@@ -99,7 +100,5 @@
}
return ProjectSelect;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/project_show.js b/app/assets/javascripts/project_show.js
index eaf4c03d573..aad130cf267 100644
--- a/app/assets/javascripts/project_show.js
+++ b/app/assets/javascripts/project_show.js
@@ -1,12 +1,11 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, padded-blocks */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife */
+
(function() {
this.ProjectShow = (function() {
function ProjectShow() {}
return ProjectShow;
-
})();
-
}).call(this);
// I kept class for future
diff --git a/app/assets/javascripts/projects_list.js b/app/assets/javascripts/projects_list.js
index 4548dc68fe1..69a11dfaf39 100644
--- a/app/assets/javascripts/projects_list.js
+++ b/app/assets/javascripts/projects_list.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, object-shorthand, quotes, no-var, one-var, one-var-declaration-per-line, prefer-arrow-callback, consistent-return, no-unused-vars, camelcase, prefer-template, comma-dangle, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, object-shorthand, quotes, no-var, one-var, one-var-declaration-per-line, prefer-arrow-callback, consistent-return, no-unused-vars, camelcase, prefer-template, comma-dangle, max-len */
(function() {
window.ProjectsList = {
@@ -47,5 +47,4 @@
});
}
};
-
}).call(this);
diff --git a/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js.es6
index 4aef1c84b56..e7fff57ff45 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js.es6
+++ b/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js.es6
@@ -1,7 +1,7 @@
-/* eslint-disable arrow-parens, no-param-reassign, no-irregular-whitespace, object-shorthand, no-else-return, comma-dangle, semi, padded-blocks, max-len */
+/* eslint-disable arrow-parens, no-param-reassign, object-shorthand, no-else-return, comma-dangle, max-len */
(global => {
- global.gl = global.gl || {};
+ global.gl = global.gl || {};
gl.ProtectedBranchAccessDropdown = class {
constructor(options) {
@@ -25,6 +25,5 @@
}
});
}
- }
-
+ };
})(window);
diff --git a/app/assets/javascripts/protected_branches/protected_branch_create.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_create.js.es6
index f26fba979a4..57ea2f52814 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_create.js.es6
+++ b/app/assets/javascripts/protected_branches/protected_branch_create.js.es6
@@ -1,8 +1,8 @@
-/* eslint-disable no-new, arrow-parens, no-param-reassign, no-irregular-whitespace, comma-dangle, padded-blocks, semi, max-len */
+/* eslint-disable no-new, arrow-parens, no-param-reassign, comma-dangle, max-len */
/* global ProtectedBranchDropdown */
(global => {
- global.gl = global.gl || {};
+ global.gl = global.gl || {};
gl.ProtectedBranchCreate = class {
constructor() {
@@ -44,7 +44,6 @@
// This will run after clicked callback
onSelect() {
-
// Enable submit button
const $branchInput = this.$wrap.find('input[name="protected_branch[name]"]');
const $allowedToMergeInput = this.$wrap.find('input[name="protected_branch[merge_access_levels_attributes][0][access_level]"]');
@@ -52,6 +51,5 @@
this.$form.find('input[type="submit"]').attr('disabled', !($branchInput.val() && $allowedToMergeInput.length && $allowedToPushInput.length));
}
- }
-
+ };
})(window);
diff --git a/app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es6
index 08264ad3d2f..5cf28aa7a73 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es6
+++ b/app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es6
@@ -49,7 +49,7 @@ class ProtectedBranchDropdown {
onClickCreateWildcard() {
// Refresh the dropdown's data, which ends up calling `getProtectedBranches`
this.$dropdown.data('glDropdown').remote.execute();
- this.$dropdown.data('glDropdown').selectRowAtIndex(0);
+ this.$dropdown.data('glDropdown').selectRowAtIndex();
}
getProtectedBranches(term, callback) {
@@ -76,3 +76,5 @@ class ProtectedBranchDropdown {
this.$dropdownFooter.toggleClass('hidden', !branchName);
}
}
+
+window.ProtectedBranchDropdown = ProtectedBranchDropdown;
diff --git a/app/assets/javascripts/protected_branches/protected_branch_edit.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_edit.js.es6
index 4ff2fa5a80f..149e511451e 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_edit.js.es6
+++ b/app/assets/javascripts/protected_branches/protected_branch_edit.js.es6
@@ -1,8 +1,8 @@
-/* eslint-disable no-new, arrow-parens, no-param-reassign, no-irregular-whitespace, padded-blocks, comma-dangle, no-trailing-spaces, semi, max-len */
+/* eslint-disable no-new, arrow-parens, no-param-reassign, comma-dangle, max-len */
/* global Flash */
(global => {
- global.gl = global.gl || {};
+ global.gl = global.gl || {};
gl.ProtectedBranchEdit = class {
constructor(options) {
@@ -14,7 +14,6 @@
}
buildDropdowns() {
-
// Allowed to merge dropdown
new gl.ProtectedBranchAccessDropdown({
$dropdown: this.$allowedToMergeDropdown,
@@ -63,6 +62,5 @@
}
});
}
- }
-
+ };
})(window);
diff --git a/app/assets/javascripts/protected_branches/protected_branch_edit_list.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_edit_list.js.es6
index b6972ef2e16..336fa6c57a7 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_edit_list.js.es6
+++ b/app/assets/javascripts/protected_branches/protected_branch_edit_list.js.es6
@@ -1,10 +1,10 @@
-/* eslint-disable arrow-parens, no-param-reassign, no-irregular-whitespace, no-new, comma-dangle, semi, padded-blocks, max-len */
+/* eslint-disable arrow-parens, no-param-reassign, no-new, comma-dangle */
(global => {
- global.gl = global.gl || {};
+ global.gl = global.gl || {};
gl.ProtectedBranchEditList = class {
- constructor() {
+ constructor() {
this.$wrap = $('.protected-branches-list');
// Build edit forms
@@ -14,6 +14,5 @@
});
});
}
- }
-
+ };
})(window);
diff --git a/app/assets/javascripts/protected_branches/protected_branches_bundle.js b/app/assets/javascripts/protected_branches/protected_branches_bundle.js
index 15b3affd469..ffb66caf5f4 100644
--- a/app/assets/javascripts/protected_branches/protected_branches_bundle.js
+++ b/app/assets/javascripts/protected_branches/protected_branches_bundle.js
@@ -1 +1,3 @@
-/*= require_tree . */
+// require everything else in this directory
+function requireAll(context) { return context.keys().map(context); }
+requireAll(require.context('.', false, /^\.\/(?!protected_branches_bundle).*\.(js|es6)$/));
diff --git a/app/assets/javascripts/render_gfm.js b/app/assets/javascripts/render_gfm.js
index bbb2f186655..bdbad93ad04 100644
--- a/app/assets/javascripts/render_gfm.js
+++ b/app/assets/javascripts/render_gfm.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, consistent-return, no-var, no-undef, no-else-return, prefer-arrow-callback, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, consistent-return, no-var, no-undef, no-else-return, prefer-arrow-callback, max-len */
// Render Gitlab flavoured Markdown
//
// Delegates to syntax highlight and render math
@@ -9,8 +9,7 @@
this.find('.js-render-math').renderMath();
};
- $(document).on('ready page:load', function() {
+ $(document).on('ready load', function() {
return $('body').renderGFM();
});
-
}).call(this);
diff --git a/app/assets/javascripts/render_math.js b/app/assets/javascripts/render_math.js
index 209e7a8661d..6cef449babf 100644
--- a/app/assets/javascripts/render_math.js
+++ b/app/assets/javascripts/render_math.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, consistent-return, no-var, no-undef, no-else-return, prefer-arrow-callback, padded-blocks, max-len, no-console */
+/* eslint-disable func-names, space-before-function-paren, consistent-return, no-var, no-undef, no-else-return, prefer-arrow-callback, max-len, no-console */
// Renders math using KaTeX in any element with the
// `js-render-math` class
//
@@ -51,5 +51,4 @@
});
}
};
-
}).call(this);
diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js
index b1e844b7302..76a0f993ea0 100644
--- a/app/assets/javascripts/right_sidebar.js
+++ b/app/assets/javascripts/right_sidebar.js
@@ -1,8 +1,8 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-unused-vars, semi, consistent-return, one-var, one-var-declaration-per-line, quotes, prefer-template, object-shorthand, comma-dangle, no-else-return, no-param-reassign, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-unused-vars, consistent-return, one-var, one-var-declaration-per-line, quotes, prefer-template, object-shorthand, comma-dangle, no-else-return, no-param-reassign, max-len */
/* global Cookies */
(function() {
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+ var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
this.Sidebar = (function() {
function Sidebar(currentUser) {
@@ -18,7 +18,7 @@
$('.dropdown').off('loading.gl.dropdown');
$('.dropdown').off('loaded.gl.dropdown');
$(document).off('click', '.js-sidebar-toggle');
- }
+ };
Sidebar.prototype.addEventListeners = function() {
this.sidebar.on('click', '.sidebar-collapsed-icon', this, this.sidebarCollapseClicked);
@@ -200,7 +200,5 @@
};
return Sidebar;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/search.js b/app/assets/javascripts/search.js
index 4b6ebadeac7..b1c0dc37b4d 100644
--- a/app/assets/javascripts/search.js
+++ b/app/assets/javascripts/search.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, object-shorthand, prefer-arrow-callback, comma-dangle, prefer-template, quotes, no-else-return, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, object-shorthand, prefer-arrow-callback, comma-dangle, prefer-template, quotes, no-else-return, max-len */
/* global Api */
(function() {
@@ -12,10 +12,13 @@
selectable: true,
filterable: true,
fieldName: 'group_id',
+ search: {
+ fields: ['full_name']
+ },
data: function(term, callback) {
return Api.groups(term, {}, function(data) {
data.unshift({
- name: 'Any'
+ full_name: 'Any'
});
data.splice(1, 0, 'divider');
return callback(data);
@@ -25,10 +28,10 @@
return obj.id;
},
text: function(obj) {
- return obj.name;
+ return obj.full_name;
},
toggleLabel: function(obj) {
- return ($groupDropdown.data('default-label')) + " " + obj.name;
+ return ($groupDropdown.data('default-label')) + " " + obj.full_name;
},
clicked: (function(_this) {
return function() {
@@ -40,6 +43,9 @@
selectable: true,
filterable: true,
fieldName: 'project_id',
+ search: {
+ fields: ['name']
+ },
data: function(term, callback) {
return Api.projects(term, 'id', function(data) {
data.unshift({
@@ -90,7 +96,5 @@
};
return Search;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/search_autocomplete.js.es6 b/app/assets/javascripts/search_autocomplete.js.es6
index 437f5dbbf7d..6250e75d407 100644
--- a/app/assets/javascripts/search_autocomplete.js.es6
+++ b/app/assets/javascripts/search_autocomplete.js.es6
@@ -1,7 +1,6 @@
-/* eslint-disable comma-dangle, no-return-assign, one-var, no-var, no-underscore-dangle, one-var-declaration-per-line, no-unused-vars, no-cond-assign, consistent-return, object-shorthand, prefer-arrow-callback, func-names, space-before-function-paren, no-plusplus, prefer-template, quotes, class-methods-use-this, no-unused-expressions, no-sequences, wrap-iife, no-lonely-if, no-else-return, no-param-reassign, vars-on-top, padded-blocks, no-extra-semi, indent, max-len */
+/* eslint-disable comma-dangle, no-return-assign, one-var, no-var, no-underscore-dangle, one-var-declaration-per-line, no-unused-vars, no-cond-assign, consistent-return, object-shorthand, prefer-arrow-callback, func-names, space-before-function-paren, prefer-template, quotes, class-methods-use-this, no-unused-expressions, no-sequences, wrap-iife, no-lonely-if, no-else-return, no-param-reassign, vars-on-top, max-len */
((global) => {
-
const KEYCODE = {
ESCAPE: 27,
BACKSPACE: 8,
@@ -70,12 +69,17 @@
search: {
fields: ['text']
},
+ id: this.getSearchText,
data: this.getData.bind(this),
selectable: true,
clicked: this.onClick.bind(this)
});
}
+ getSearchText(selectedObject, el) {
+ return selectedObject.id ? selectedObject.text : '';
+ }
+
getData(term, callback) {
var _this, contents, jqXHR;
_this = this;
@@ -105,7 +109,7 @@
data = [];
// List results
firstCategory = true;
- for (i = 0, len = response.length; i < len; i++) {
+ for (i = 0, len = response.length; i < len; i += 1) {
suggestion = response[i];
// Add group header before list each group
if (lastCategory !== suggestion.category) {
@@ -142,8 +146,9 @@
}
getCategoryContents() {
- var dashboardOptions, groupOptions, issuesPath, items, mrPath, name, options, projectOptions, userId, utils;
+ var dashboardOptions, groupOptions, issuesPath, items, mrPath, name, options, projectOptions, userId, userName, utils;
userId = gon.current_user_id;
+ userName = gon.current_username;
utils = gl.utils, projectOptions = gl.projectOptions, groupOptions = gl.groupOptions, dashboardOptions = gl.dashboardOptions;
if (utils.isInGroupsPage() && groupOptions) {
options = groupOptions[utils.getGroupSlug()];
@@ -158,10 +163,10 @@
header: "" + name
}, {
text: 'Issues assigned to me',
- url: issuesPath + "/?assignee_id=" + userId
+ url: issuesPath + "/?assignee_username=" + userName
}, {
text: "Issues I've created",
- url: issuesPath + "/?author_id=" + userId
+ url: issuesPath + "/?author_username=" + userName
}, 'separator', {
text: 'Merge requests assigned to me',
url: mrPath + "/?assignee_id=" + userId
@@ -215,7 +220,7 @@
this.dropdown.addClass('open').trigger('shown.bs.dropdown');
return this.searchInput.removeClass('disabled');
}
- };
+ }
// Saves last length of the entered text
onSearchInputKeyDown() {
@@ -279,12 +284,12 @@
return this.searchInput.val();
}
- onClearInputClick(e) {
+ onClearInputClick(e) {
e.preventDefault();
return this.searchInput.val('').focus();
}
- onSearchInputBlur(e) {
+ onSearchInputBlur(e) {
this.isFocused = false;
this.wrap.removeClass('search-active');
// If input is blank then restore state
@@ -304,12 +309,12 @@
hasLocationBadge() {
return this.wrap.is('.has-location-badge');
- };
+ }
restoreOriginalState() {
var i, input, inputs, len;
inputs = Object.keys(this.originalState);
- for (i = 0, len = inputs.length; i < len; i++) {
+ for (i = 0, len = inputs.length; i < len; i += 1) {
input = inputs[i];
this.getElement("#" + input).val(this.originalState[input]);
}
@@ -330,7 +335,7 @@
var i, input, inputs, len, results;
inputs = Object.keys(this.originalState);
results = [];
- for (i = 0, len = inputs.length; i < len; i++) {
+ for (i = 0, len = inputs.length; i < len; i += 1) {
input = inputs[i];
// _location isnt a input
if (input === '_location') {
@@ -360,11 +365,11 @@
var html;
html = "<ul> <li><a class='dropdown-menu-empty-link is-focused'>Loading...</a></li> </ul>";
return this.dropdownContent.html(html);
- };
+ }
onClick(item, $el, e) {
if (location.pathname.indexOf(item.url) !== -1) {
- e.preventDefault();
+ if (!e.metaKey) e.preventDefault();
if (!this.badgePresent) {
if (item.category === 'Projects') {
this.projectInputEl.val(item.id);
@@ -383,8 +388,7 @@
this.disableAutocomplete();
return this.searchInput.val('').focus();
}
- };
-
+ }
}
global.SearchAutocomplete = SearchAutocomplete;
@@ -425,5 +429,4 @@
};
}
});
-
})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/shortcuts.js b/app/assets/javascripts/shortcuts.js
index 5ea00f408f4..c6d9b007ad1 100644
--- a/app/assets/javascripts/shortcuts.js
+++ b/app/assets/javascripts/shortcuts.js
@@ -1,10 +1,9 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, quotes, prefer-arrow-callback, consistent-return, object-shorthand, no-unused-vars, one-var, one-var-declaration-per-line, no-plusplus, no-else-return, comma-dangle, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, quotes, prefer-arrow-callback, consistent-return, object-shorthand, no-unused-vars, one-var, one-var-declaration-per-line, no-else-return, comma-dangle, max-len */
/* global Mousetrap */
-/* global Turbolinks */
/* global findFileURL */
(function() {
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+ var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
this.Shortcuts = (function() {
function Shortcuts(skipResetBindings) {
@@ -23,7 +22,7 @@
Mousetrap.bind(['ctrl+shift+p', 'command+shift+p'], this.toggleMarkdownPreview);
if (typeof findFileURL !== "undefined" && findFileURL !== null) {
Mousetrap.bind('t', function() {
- return Turbolinks.visit(findFileURL);
+ return gl.utils.visitUrl(findFileURL);
});
}
}
@@ -51,7 +50,7 @@
var i, l, len, results;
if (location && location.length > 0) {
results = [];
- for (i = 0, len = location.length; i < len; i++) {
+ for (i = 0, len = location.length; i < len; i += 1) {
l = location[i];
results.push($(l).show());
}
@@ -78,7 +77,6 @@
};
return Shortcuts;
-
})();
$(document).on('click.more_help', '.js-more-help-button', function(e) {
@@ -99,5 +97,4 @@
}
};
})();
-
}).call(this);
diff --git a/app/assets/javascripts/shortcuts_blob.js b/app/assets/javascripts/shortcuts_blob.js
deleted file mode 100644
index c26903038b4..00000000000
--- a/app/assets/javascripts/shortcuts_blob.js
+++ /dev/null
@@ -1,31 +0,0 @@
-/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, consistent-return, padded-blocks */
-/* global Shortcuts */
-/* global Mousetrap */
-
-/*= require shortcuts */
-
-(function() {
- var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
- hasProp = {}.hasOwnProperty;
-
- this.ShortcutsBlob = (function(superClass) {
- extend(ShortcutsBlob, superClass);
-
- function ShortcutsBlob(skipResetBindings) {
- ShortcutsBlob.__super__.constructor.call(this, skipResetBindings);
- Mousetrap.bind('y', ShortcutsBlob.copyToClipboard);
- }
-
- ShortcutsBlob.copyToClipboard = function() {
- var clipboardButton;
- clipboardButton = $('.btn-clipboard');
- if (clipboardButton) {
- return clipboardButton.click();
- }
- };
-
- return ShortcutsBlob;
-
- })(Shortcuts);
-
-}).call(this);
diff --git a/app/assets/javascripts/shortcuts_blob.js.es6 b/app/assets/javascripts/shortcuts_blob.js.es6
new file mode 100644
index 00000000000..bfe90aef71e
--- /dev/null
+++ b/app/assets/javascripts/shortcuts_blob.js.es6
@@ -0,0 +1,29 @@
+/* global Mousetrap */
+/* global Shortcuts */
+
+require('./shortcuts');
+
+const defaults = {
+ skipResetBindings: false,
+ fileBlobPermalinkUrl: null,
+};
+
+class ShortcutsBlob extends Shortcuts {
+ constructor(opts) {
+ const options = Object.assign({}, defaults, opts);
+ super(options.skipResetBindings);
+ this.options = options;
+
+ Mousetrap.bind('y', this.moveToFilePermalink.bind(this));
+ }
+
+ moveToFilePermalink() {
+ if (this.options.fileBlobPermalinkUrl) {
+ const hash = gl.utils.getLocationHash();
+ const hashUrlString = hash ? `#${hash}` : '';
+ gl.utils.visitUrl(`${this.options.fileBlobPermalinkUrl}${hashUrlString}`);
+ }
+ }
+}
+
+module.exports = ShortcutsBlob;
diff --git a/app/assets/javascripts/shortcuts_dashboard_navigation.js b/app/assets/javascripts/shortcuts_dashboard_navigation.js
index 4549742bbcb..7378b322426 100644
--- a/app/assets/javascripts/shortcuts_dashboard_navigation.js
+++ b/app/assets/javascripts/shortcuts_dashboard_navigation.js
@@ -1,8 +1,8 @@
-/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-arrow-callback, consistent-return, no-return-assign, padded-blocks */
+/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-arrow-callback, consistent-return, no-return-assign */
/* global Mousetrap */
/* global Shortcuts */
-/*= require shortcuts */
+require('./shortcuts');
(function() {
var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
@@ -36,7 +36,5 @@
};
return ShortcutsDashboardNavigation;
-
})(Shortcuts);
-
}).call(this);
diff --git a/app/assets/javascripts/shortcuts_find_file.js b/app/assets/javascripts/shortcuts_find_file.js
index 3a81380eef0..36e379d634d 100644
--- a/app/assets/javascripts/shortcuts_find_file.js
+++ b/app/assets/javascripts/shortcuts_find_file.js
@@ -1,8 +1,8 @@
-/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, padded-blocks */
+/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife */
/* global Mousetrap */
/* global ShortcutsNavigation */
-/*= require shortcuts_navigation */
+require('./shortcuts_navigation');
(function() {
var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
@@ -34,7 +34,5 @@
}
return ShortcutsFindFile;
-
})(ShortcutsNavigation);
-
}).call(this);
diff --git a/app/assets/javascripts/shortcuts_issuable.js b/app/assets/javascripts/shortcuts_issuable.js
index b892fbc3393..b841abb754d 100644
--- a/app/assets/javascripts/shortcuts_issuable.js
+++ b/app/assets/javascripts/shortcuts_issuable.js
@@ -1,11 +1,10 @@
-/* eslint-disable func-names, space-before-function-paren, max-len, no-var, one-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, one-var-declaration-per-line, quotes, prefer-arrow-callback, consistent-return, prefer-template, no-mixed-operators, padded-blocks */
+/* eslint-disable func-names, space-before-function-paren, max-len, no-var, one-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, one-var-declaration-per-line, quotes, prefer-arrow-callback, consistent-return, prefer-template, no-mixed-operators */
/* global Mousetrap */
-/* global Turbolinks */
/* global ShortcutsNavigation */
/* global sidebar */
-/*= require mousetrap */
-/*= require shortcuts_navigation */
+require('mousetrap');
+require('./shortcuts_navigation');
(function() {
var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
@@ -39,35 +38,48 @@
}
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;
- }
- // Put a '>' character before each non-empty line in the selection
- quote = _.map(selected.split("\n"), function(val) {
- if (val.trim() !== '') {
- return "> " + val + "\n";
- }
- });
- // If replyField already has some content, add a newline before our quote
- separator = replyField.val().trim() !== "" && "\n" || '';
- replyField.val(function(_, current) {
- return current + separator + quote.join('') + "\n";
- });
- // Trigger autosave for the added text
- replyField.trigger('input');
- // Focus the input field
- return replyField.focus();
+ var quote, documentFragment, selected, separator;
+ var replyField = $('.js-main-target-form #note_note');
+
+ documentFragment = window.gl.utils.getSelectedFragment();
+ if (!documentFragment) {
+ replyField.focus();
+ return;
+ }
+
+ // If the documentFragment contains more than just Markdown, don't copy as GFM.
+ if (documentFragment.querySelector('.md, .wiki')) return;
+
+ selected = window.gl.CopyAsGFM.nodeToGFM(documentFragment);
+
+ if (selected.trim() === "") {
+ return;
}
+ quote = _.map(selected.split("\n"), function(val) {
+ return ("> " + val).trim() + "\n";
+ });
+ // If replyField already has some content, add a newline before our quote
+ separator = replyField.val().trim() !== "" && "\n\n" || '';
+ replyField.val(function(_, current) {
+ return current + separator + quote.join('') + "\n";
+ });
+
+ // Trigger autosave
+ replyField.trigger('input');
+
+ // Trigger autosize
+ var event = document.createEvent('Event');
+ event.initEvent('autosize:update', true, false);
+ replyField.get(0).dispatchEvent(event);
+
+ // Focus the input field
+ return replyField.focus();
};
ShortcutsIssuable.prototype.editIssue = function() {
var $editBtn;
$editBtn = $('.issuable-edit');
- return Turbolinks.visit($editBtn.attr('href'));
+ return gl.utils.visitUrl($editBtn.attr('href'));
};
ShortcutsIssuable.prototype.openSidebarDropdown = function(name) {
@@ -76,7 +88,5 @@
};
return ShortcutsIssuable;
-
})(ShortcutsNavigation);
-
}).call(this);
diff --git a/app/assets/javascripts/shortcuts_navigation.js b/app/assets/javascripts/shortcuts_navigation.js
index 0776d0a9b67..cb5f2c53ea6 100644
--- a/app/assets/javascripts/shortcuts_navigation.js
+++ b/app/assets/javascripts/shortcuts_navigation.js
@@ -1,8 +1,8 @@
-/* eslint-disable func-names, space-before-function-paren, max-len, no-var, one-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-arrow-callback, consistent-return, no-return-assign, padded-blocks */
+/* eslint-disable func-names, space-before-function-paren, max-len, no-var, one-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-arrow-callback, consistent-return, no-return-assign */
/* global Mousetrap */
/* global Shortcuts */
-/*= require shortcuts */
+require('./shortcuts');
(function() {
var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
@@ -64,7 +64,5 @@
};
return ShortcutsNavigation;
-
})(Shortcuts);
-
}).call(this);
diff --git a/app/assets/javascripts/shortcuts_network.js b/app/assets/javascripts/shortcuts_network.js
index ecc3fab84c3..651957f5325 100644
--- a/app/assets/javascripts/shortcuts_network.js
+++ b/app/assets/javascripts/shortcuts_network.js
@@ -1,8 +1,8 @@
-/* eslint-disable func-names, space-before-function-paren, max-len, no-var, one-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, max-len, no-var, one-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, max-len */
/* global Mousetrap */
/* global ShortcutsNavigation */
-/*= require shortcuts_navigation */
+require('./shortcuts_navigation');
(function() {
var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
@@ -24,7 +24,5 @@
}
return ShortcutsNetwork;
-
})(ShortcutsNavigation);
-
}).call(this);
diff --git a/app/assets/javascripts/sidebar.js.es6 b/app/assets/javascripts/sidebar.js.es6
index 9790a44972d..cbb2ae9f1bd 100644
--- a/app/assets/javascripts/sidebar.js.es6
+++ b/app/assets/javascripts/sidebar.js.es6
@@ -1,9 +1,7 @@
-/* eslint-disable arrow-parens, class-methods-use-this, no-param-reassign, padded-blocks */
+/* eslint-disable arrow-parens, class-methods-use-this, no-param-reassign */
/* global Cookies */
-((global) => {
- let singleton;
-
+(() => {
const pinnedStateCookie = 'pin_nav';
const sidebarBreakpoint = 1024;
@@ -23,11 +21,12 @@
class Sidebar {
constructor() {
- if (!singleton) {
- singleton = this;
- singleton.init();
+ if (!Sidebar.singleton) {
+ Sidebar.singleton = this;
+ Sidebar.singleton.init();
}
- return singleton;
+
+ return Sidebar.singleton;
}
init() {
@@ -39,8 +38,8 @@
$(document)
.on('click', sidebarToggleSelector, () => this.toggleSidebar())
.on('click', pinnedToggleSelector, () => this.togglePinnedState())
- .on('click', 'html, body', (e) => this.handleClickEvent(e))
- .on('page:change', () => this.renderState())
+ .on('click', 'html, body, a, button', (e) => this.handleClickEvent(e))
+ .on('DOMContentLoaded', () => this.renderState())
.on('todo:toggle', (e, count) => this.updateTodoCount(count));
this.renderState();
}
@@ -88,11 +87,12 @@
$pinnedToggle.attr('title', tooltipText).tooltip('fixTitle').tooltip(tooltipState);
if (this.isExpanded) {
- setTimeout(() => $(sidebarContentSelector).niceScroll().updateScrollBar(), 200);
+ const sidebarContent = $(sidebarContentSelector);
+ setTimeout(() => { sidebarContent.niceScroll().updateScrollBar(); }, 200);
}
}
}
- global.Sidebar = Sidebar;
-
-})(window.gl || (window.gl = {}));
+ window.gl = window.gl || {};
+ gl.Sidebar = Sidebar;
+})();
diff --git a/app/assets/javascripts/single_file_diff.js b/app/assets/javascripts/single_file_diff.js
index ac8603ccd10..5b20c63384c 100644
--- a/app/assets/javascripts/single_file_diff.js
+++ b/app/assets/javascripts/single_file_diff.js
@@ -1,7 +1,7 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, one-var, one-var-declaration-per-line, consistent-return, no-param-reassign, padded-blocks, max-len */
+/* eslint-disable func-names, prefer-arrow-callback, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, one-var, one-var-declaration-per-line, consistent-return, no-param-reassign, max-len */
(function() {
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+ var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
window.SingleFileDiff = (function() {
var COLLAPSED_HTML, ERROR_HTML, LOADING_HTML, WRAPPER;
@@ -14,8 +14,7 @@
COLLAPSED_HTML = '<div class="nothing-here-block diff-collapsed">This diff is collapsed. <a class="click-to-expand">Click to expand it.</a></div>';
- function SingleFileDiff(file, forceLoad, cb) {
- var clickTarget;
+ function SingleFileDiff(file) {
this.file = file;
this.toggleDiff = bind(this.toggleDiff, this);
this.content = $('.diff-content', this.file);
@@ -33,14 +32,13 @@
this.content.after(this.collapsedContent);
this.$toggleIcon.addClass('fa-caret-down');
}
- clickTarget = $('.file-title, .click-to-expand', this.file).on('click', this.toggleDiff);
- if (forceLoad) {
- this.toggleDiff({ target: clickTarget }, cb);
- }
+
+ $('.file-title, .click-to-expand', this.file).on('click', (function (e) {
+ this.toggleDiff($(e.target));
+ }).bind(this));
}
- SingleFileDiff.prototype.toggleDiff = function(e, cb) {
- var $target = $(e.target);
+ SingleFileDiff.prototype.toggleDiff = function($target, cb) {
if (!$target.hasClass('file-title') && !$target.hasClass('click-to-expand') && !$target.hasClass('diff-toggle-caret')) return;
this.isOpen = !this.isOpen;
if (!this.isOpen && !this.hasError) {
@@ -88,15 +86,13 @@
};
return SingleFileDiff;
-
})();
- $.fn.singleFileDiff = function(forceLoad, cb) {
+ $.fn.singleFileDiff = function() {
return this.each(function() {
- if (!$.data(this, 'singleFileDiff') || forceLoad) {
- return $.data(this, 'singleFileDiff', new window.SingleFileDiff(this, forceLoad, cb));
+ if (!$.data(this, 'singleFileDiff')) {
+ return $.data(this, 'singleFileDiff', new window.SingleFileDiff(this));
}
});
};
-
}).call(this);
diff --git a/app/assets/javascripts/smart_interval.js.es6 b/app/assets/javascripts/smart_interval.js.es6
index 40f67637c7c..d1bdc353be2 100644
--- a/app/assets/javascripts/smart_interval.js.es6
+++ b/app/assets/javascripts/smart_interval.js.es6
@@ -89,7 +89,7 @@
destroy() {
this.cancel();
document.removeEventListener('visibilitychange', this.handleVisibilityChange);
- $(document).off('visibilitychange').off('page:before-unload');
+ $(document).off('visibilitychange').off('beforeunload');
}
/* private */
@@ -111,8 +111,9 @@
}
initPageUnloadHandling() {
+ // TODO: Consider refactoring in light of turbolinks removal.
// prevent interval continuing after page change, when kept in cache by Turbolinks
- $(document).on('page:before-unload', () => this.cancel());
+ $(document).on('beforeunload', () => this.cancel());
}
handleVisibilityChange(e) {
diff --git a/app/assets/javascripts/snippet/snippet_bundle.js b/app/assets/javascripts/snippet/snippet_bundle.js
index 18512d179b3..64f9065be42 100644
--- a/app/assets/javascripts/snippet/snippet_bundle.js
+++ b/app/assets/javascripts/snippet/snippet_bundle.js
@@ -1,15 +1,16 @@
-/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, quotes, semi, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, quotes, max-len */
/* global ace */
-/*= require_tree . */
+// require everything else in this directory
+function requireAll(context) { return context.keys().map(context); }
+requireAll(require.context('.', false, /^\.\/(?!snippet_bundle).*\.(js|es6)$/));
(function() {
$(function() {
- var editor = ace.edit("editor")
+ var editor = ace.edit("editor");
$(".snippet-form-holder form").on('submit', function() {
$(".snippet-file-content").val(editor.getValue());
});
});
-
}).call(this);
diff --git a/app/assets/javascripts/snippets_list.js.es6 b/app/assets/javascripts/snippets_list.js.es6
index 6f913326a3a..2128007113f 100644
--- a/app/assets/javascripts/snippets_list.js.es6
+++ b/app/assets/javascripts/snippets_list.js.es6
@@ -1,4 +1,4 @@
-/* eslint-disable arrow-parens, no-param-reassign, space-before-function-paren, func-names, no-var, semi, max-len */
+/* eslint-disable arrow-parens, no-param-reassign, space-before-function-paren, func-names, no-var, max-len */
(global => {
global.gl = global.gl || {};
@@ -9,5 +9,5 @@
$holder.find('.pagination').on('ajax:success', (e, data) => {
$holder.replaceWith(data.html);
});
- }
+ };
})(window);
diff --git a/app/assets/javascripts/star.js b/app/assets/javascripts/star.js
index f1fc526bf2e..531fd0e9c32 100644
--- a/app/assets/javascripts/star.js
+++ b/app/assets/javascripts/star.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-unused-vars, one-var, no-var, one-var-declaration-per-line, prefer-arrow-callback, no-new, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-unused-vars, one-var, no-var, one-var-declaration-per-line, prefer-arrow-callback, no-new, max-len */
/* global Flash */
(function() {
@@ -26,7 +26,5 @@
}
return Star;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/subbable_resource.js.es6 b/app/assets/javascripts/subbable_resource.js.es6
index 932120157a3..d8191605128 100644
--- a/app/assets/javascripts/subbable_resource.js.es6
+++ b/app/assets/javascripts/subbable_resource.js.es6
@@ -1,6 +1,3 @@
-//= require vue
-//= require vue-resource
-
(() => {
/*
* SubbableResource can be extended to provide a pubsub-style service for one-off REST
diff --git a/app/assets/javascripts/subscription_select.js b/app/assets/javascripts/subscription_select.js
index 185d20775d0..187356f0bf9 100644
--- a/app/assets/javascripts/subscription_select.js
+++ b/app/assets/javascripts/subscription_select.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, object-shorthand, no-unused-vars, no-shadow, one-var, one-var-declaration-per-line, comma-dangle, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, object-shorthand, no-unused-vars, no-shadow, one-var, one-var-declaration-per-line, comma-dangle, max-len */
(function() {
this.SubscriptionSelect = (function() {
function SubscriptionSelect() {
@@ -30,7 +30,5 @@
}
return SubscriptionSelect;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/syntax_highlight.js b/app/assets/javascripts/syntax_highlight.js
index 5d0fa62c50a..115716bff6a 100644
--- a/app/assets/javascripts/syntax_highlight.js
+++ b/app/assets/javascripts/syntax_highlight.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, consistent-return, no-var, no-else-return, prefer-arrow-callback, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, consistent-return, no-var, no-else-return, prefer-arrow-callback, max-len */
// Syntax Highlighter
//
@@ -10,7 +10,6 @@
// <div class="js-syntax-highlight"></div>
//
(function() {
-
$.fn.syntaxHighlight = function() {
var $children;
@@ -25,5 +24,4 @@
}
}
};
-
}).call(this);
diff --git a/app/assets/javascripts/templates/issuable_template_selector.js.es6 b/app/assets/javascripts/templates/issuable_template_selector.js.es6
index d2b152045b4..e9e9aafd71a 100644
--- a/app/assets/javascripts/templates/issuable_template_selector.js.es6
+++ b/app/assets/javascripts/templates/issuable_template_selector.js.es6
@@ -1,7 +1,7 @@
-/* eslint-disable prefer-const, comma-dangle, max-len, no-useless-return, object-curly-spacing, no-param-reassign, max-len */
+/* eslint-disable comma-dangle, max-len, no-useless-return, no-param-reassign, max-len */
/* global Api */
-/*= require ../blob/template_selector */
+require('../blob/template_selector');
((global) => {
class IssuableTemplateSelector extends gl.TemplateSelector {
@@ -12,7 +12,7 @@
this.issuableType = this.wrapper.data('issuable-type');
this.titleInput = $(`#${this.issuableType}_title`);
- let initialQuery = {
+ const initialQuery = {
name: this.dropdown.data('selected')
};
@@ -23,7 +23,7 @@
});
$('.no-template', this.dropdown.parent()).on('click', () => {
- this.currentTemplate = '';
+ this.currentTemplate.content = '';
this.setInputValueToTemplateContent();
$('.dropdown-toggle-text', this.dropdown).text('Choose a template');
});
@@ -47,10 +47,10 @@
// If the title has not yet been set, focus the title input and
// skip focusing the description input by setting `true` as the
// `skipFocus` option to `requestFileSuccess`.
- this.requestFileSuccess(this.currentTemplate, {skipFocus: true});
+ this.requestFileSuccess(this.currentTemplate, { skipFocus: true });
this.titleInput.focus();
} else {
- this.requestFileSuccess(this.currentTemplate, {skipFocus: false});
+ this.requestFileSuccess(this.currentTemplate, { skipFocus: false });
}
return;
}
diff --git a/app/assets/javascripts/templates/issuable_template_selectors.js.es6 b/app/assets/javascripts/templates/issuable_template_selectors.js.es6
index 7310b9de074..97f6d37364d 100644
--- a/app/assets/javascripts/templates/issuable_template_selectors.js.es6
+++ b/app/assets/javascripts/templates/issuable_template_selectors.js.es6
@@ -1,4 +1,4 @@
-/* eslint-disable no-new, comma-dangle, class-methods-use-this, prefer-const, no-param-reassign */
+/* eslint-disable no-new, comma-dangle, class-methods-use-this, no-param-reassign */
((global) => {
class IssuableTemplateSelectors {
@@ -19,7 +19,7 @@
}
initEditor() {
- let editor = $('.markdown-area');
+ const editor = $('.markdown-area');
// Proxy ace-editor's .setValue to jQuery's .val
editor.setValue = editor.val;
editor.getValue = editor.val;
diff --git a/app/assets/javascripts/terminal/terminal_bundle.js.es6 b/app/assets/javascripts/terminal/terminal_bundle.js.es6
index ded7ee6e9fe..13cf3a10a38 100644
--- a/app/assets/javascripts/terminal/terminal_bundle.js.es6
+++ b/app/assets/javascripts/terminal/terminal_bundle.js.es6
@@ -1,5 +1,7 @@
-//= require xterm/xterm.js
-//= require xterm/fit.js
-//= require ./terminal.js
+require('vendor/xterm/encoding-indexes.js');
+require('vendor/xterm/encoding.js');
+window.Terminal = require('vendor/xterm/xterm.js');
+require('vendor/xterm/fit.js');
+require('./terminal.js');
$(() => new gl.Terminal({ selector: '#terminal' }));
diff --git a/app/assets/javascripts/todos.js.es6 b/app/assets/javascripts/todos.js.es6
index d8713600030..b07e62a8c30 100644
--- a/app/assets/javascripts/todos.js.es6
+++ b/app/assets/javascripts/todos.js.es6
@@ -1,9 +1,7 @@
-/* eslint-disable padded-blocks, class-methods-use-this, no-new, func-names, prefer-template, no-unneeded-ternary, object-shorthand, space-before-function-paren, comma-dangle, quote-props, consistent-return, no-else-return, semi, no-param-reassign, max-len */
+/* eslint-disable class-methods-use-this, no-new, func-names, prefer-template, no-unneeded-ternary, object-shorthand, space-before-function-paren, comma-dangle, quote-props, consistent-return, no-else-return, no-param-reassign, max-len */
/* global UsersSelect */
-/* global Turbolinks */
((global) => {
-
class Todos {
constructor({ el } = {}) {
this.allDoneClicked = this.allDoneClicked.bind(this);
@@ -35,7 +33,7 @@
$('form.filter-form').on('submit', function (event) {
event.preventDefault();
- Turbolinks.visit(this.action + '&' + $(this).serialize());
+ gl.utils.visitUrl(this.action + '&' + $(this).serialize());
});
}
@@ -49,7 +47,7 @@
clicked: function() {
return $dropdown.closest('form.filter-form').submit();
}
- })
+ });
}
doneClicked(e) {
@@ -86,7 +84,7 @@
},
success: (data) => {
$target.remove();
- $('.prepend-top-default').html('<div class="nothing-here-block">You\'re all done!</div>');
+ $('.js-todos-all').html('<div class="nothing-here-block">You\'re all done!</div>');
return this.updateBadges(data);
}
});
@@ -143,21 +141,33 @@
};
url = gl.utils.mergeUrlParams(pageParams, url);
}
- return Turbolinks.visit(url);
+ return gl.utils.visitUrl(url);
}
}
goToTodoUrl(e) {
- const todoLink = $(this).data('url');
+ const todoLink = this.dataset.url;
+ let targetLink = e.target.getAttribute('href');
+
+ if (e.target.tagName === 'IMG') { // See if clicked target was Avatar
+ targetLink = e.target.parentElement.getAttribute('href'); // Parent of Avatar is link
+ }
+
if (!todoLink) {
return;
}
- // Allow Meta-Click or Mouse3-click to open in a new tab
- if (e.metaKey || e.which === 2) {
+
+ if (gl.utils.isMetaClick(e)) {
e.preventDefault();
- return window.open(todoLink, '_blank');
+ // Meta-Click on username leads to different URL than todoLink.
+ // Turbolinks can resolve that URL, but window.open requires URL manually.
+ if (targetLink !== todoLink) {
+ return window.open(targetLink, '_blank');
+ } else {
+ return window.open(todoLink, '_blank');
+ }
} else {
- return Turbolinks.visit(todoLink);
+ return gl.utils.visitUrl(todoLink);
}
}
}
diff --git a/app/assets/javascripts/tree.js b/app/assets/javascripts/tree.js
index f48a7ee0f55..b1b35fdbd6c 100644
--- a/app/assets/javascripts/tree.js
+++ b/app/assets/javascripts/tree.js
@@ -1,5 +1,5 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, max-len, quotes, consistent-return, no-var, one-var, one-var-declaration-per-line, no-else-return, prefer-arrow-callback, padded-blocks, max-len */
-/* global Turbolinks */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, max-len, quotes, consistent-return, no-var, one-var, one-var-declaration-per-line, no-else-return, prefer-arrow-callback, max-len */
+
(function() {
this.TreeView = (function() {
function TreeView() {
@@ -15,7 +15,7 @@
e.preventDefault();
return window.open(path, '_blank');
} else {
- return Turbolinks.visit(path);
+ return gl.utils.visitUrl(path);
}
}
});
@@ -57,14 +57,12 @@
} else if (e.which === 13) {
path = $('.tree-item.selected .tree-item-file-name a').attr('href');
if (path) {
- return Turbolinks.visit(path);
+ return gl.utils.visitUrl(path);
}
}
});
};
return TreeView;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/u2f/authenticate.js b/app/assets/javascripts/u2f/authenticate.js
deleted file mode 100644
index d2aa3c7a841..00000000000
--- a/app/assets/javascripts/u2f/authenticate.js
+++ /dev/null
@@ -1,112 +0,0 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, prefer-arrow-callback, no-else-return, quotes, quote-props, comma-dangle, one-var, one-var-declaration-per-line, padded-blocks, max-len */
-/* global u2f */
-/* global U2FError */
-/* global U2FUtil */
-
-// Authenticate U2F (universal 2nd factor) devices for users to authenticate with.
-//
-// State Flow #1: setup -> in_progress -> authenticated -> POST to server
-// State Flow #2: setup -> in_progress -> error -> setup
-(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) {
- // The U2F Javascript API v1.1 requires a single challenge, with
- // _no challenges per-request_. The U2F Javascript API v1.0 requires a
- // challenge per-request, which is done by copying the single challenge
- // into every request.
- //
- // In either case, we don't need the per-request challenges that the server
- // has generated, so we can remove them.
- //
- // Note: The server library fixes this behaviour in (unreleased) version 1.0.0.
- // This can be removed once we upgrade.
- // https://github.com/castle/ruby-u2f/commit/103f428071a81cd3d5f80c2e77d522d5029946a4
- 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);
- };
-
- // Rendering #
- 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');
- // Prefer to do this instead of interpolating using Underscore templates
- // because of JSON escaping issues.
- 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.es6 b/app/assets/javascripts/u2f/authenticate.js.es6
new file mode 100644
index 00000000000..500b78fc5d8
--- /dev/null
+++ b/app/assets/javascripts/u2f/authenticate.js.es6
@@ -0,0 +1,118 @@
+/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, prefer-arrow-callback, no-else-return, quotes, quote-props, comma-dangle, one-var, one-var-declaration-per-line, max-len */
+/* global u2f */
+/* global U2FError */
+/* global U2FUtil */
+
+// 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
+(function() {
+ const global = window.gl || (window.gl = {});
+
+ var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
+
+ global.U2FAuthenticate = (function() {
+ function U2FAuthenticate(container, form, u2fParams, fallbackButton, fallbackUI) {
+ 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.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.form = form;
+ this.fallbackButton = fallbackButton;
+ this.fallbackUI = fallbackUI;
+ if (this.fallbackButton) this.fallbackButton.addEventListener('click', this.switchToFallbackUI.bind(this));
+ this.signRequests = u2fParams.sign_requests.map(function(request) {
+ // The U2F Javascript API v1.1 requires a single challenge, with
+ // _no challenges per-request_. The U2F Javascript API v1.0 requires a
+ // challenge per-request, which is done by copying the single challenge
+ // into every request.
+ //
+ // In either case, we don't need the per-request challenges that the server
+ // has generated, so we can remove them.
+ //
+ // Note: The server library fixes this behaviour in (unreleased) version 1.0.0.
+ // This can be removed once we upgrade.
+ // https://github.com/castle/ruby-u2f/commit/103f428071a81cd3d5f80c2e77d522d5029946a4
+ return _(request).omit('challenge');
+ });
+ }
+
+ U2FAuthenticate.prototype.start = function() {
+ if (U2FUtil.isU2FSupported()) {
+ return this.renderInProgress();
+ } 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, 'authenticate');
+ return _this.renderError(error);
+ } else {
+ return _this.renderAuthenticated(JSON.stringify(response));
+ }
+ };
+ })(this), 10);
+ };
+
+ // Rendering #
+ 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.renderInProgress = function() {
+ this.renderTemplate('inProgress');
+ return this.authenticate();
+ };
+
+ U2FAuthenticate.prototype.renderError = function(error) {
+ this.renderTemplate('error', {
+ error_message: error.message(),
+ error_code: error.errorCode
+ });
+ return this.container.find('#js-u2f-try-again').on('click', this.renderInProgress);
+ };
+
+ U2FAuthenticate.prototype.renderAuthenticated = function(deviceResponse) {
+ this.renderTemplate('authenticated');
+ const container = this.container[0];
+ container.querySelector('#js-device-response').value = deviceResponse;
+ container.querySelector(this.form).submit();
+ this.fallbackButton.classList.add('hidden');
+ };
+
+ U2FAuthenticate.prototype.renderNotSupported = function() {
+ return this.renderTemplate('notSupported');
+ };
+
+ U2FAuthenticate.prototype.switchToFallbackUI = function() {
+ this.fallbackButton.classList.add('hidden');
+ this.container[0].classList.add('hidden');
+ this.fallbackUI.classList.remove('hidden');
+ };
+
+ return U2FAuthenticate;
+ })();
+})();
diff --git a/app/assets/javascripts/u2f/error.js b/app/assets/javascripts/u2f/error.js
index 69f98c9c0ad..86b459e1866 100644
--- a/app/assets/javascripts/u2f/error.js
+++ b/app/assets/javascripts/u2f/error.js
@@ -1,30 +1,27 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-console, quotes, prefer-template, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-console, quotes, prefer-template, max-len */
/* global u2f */
(function() {
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+ var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
this.U2FError = (function() {
- function U2FError(errorCode) {
+ function U2FError(errorCode, u2fFlowType) {
this.errorCode = errorCode;
this.message = bind(this.message, this);
this.httpsDisabled = window.location.protocol !== 'https:';
- console.error("U2F Error Code: " + this.errorCode);
+ this.u2fFlowType = u2fFlowType;
}
U2FError.prototype.message = function() {
- switch (false) {
- case !(this.errorCode === u2f.ErrorCodes.BAD_REQUEST && this.httpsDisabled):
- return "U2F only works with HTTPS-enabled websites. Contact your administrator for more details.";
- case this.errorCode !== u2f.ErrorCodes.DEVICE_INELIGIBLE:
- return "This device has already been registered with us.";
- default:
- return "There was a problem communicating with your device.";
+ if (this.errorCode === u2f.ErrorCodes.BAD_REQUEST && this.httpsDisabled) {
+ return 'U2F only works with HTTPS-enabled websites. Contact your administrator for more details.';
+ } else if (this.errorCode === u2f.ErrorCodes.DEVICE_INELIGIBLE) {
+ if (this.u2fFlowType === 'authenticate') return 'This device has not been registered with us.';
+ if (this.u2fFlowType === 'register') return 'This device has already been registered with us.';
}
+ return "There was a problem communicating with your device.";
};
return U2FError;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/u2f/register.js b/app/assets/javascripts/u2f/register.js
index 4f5d68f546b..69d1ff3a39e 100644
--- a/app/assets/javascripts/u2f/register.js
+++ b/app/assets/javascripts/u2f/register.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-else-return, quotes, quote-props, comma-dangle, one-var, one-var-declaration-per-line, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-else-return, quotes, quote-props, comma-dangle, one-var, one-var-declaration-per-line, max-len */
/* global u2f */
/* global U2FError */
/* global U2FUtil */
@@ -8,7 +8,7 @@
// State Flow #1: setup -> in_progress -> registered -> POST to server
// State Flow #2: setup -> in_progress -> error -> setup
(function() {
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+ var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
this.U2FRegister = (function() {
function U2FRegister(container, u2fParams) {
@@ -39,7 +39,7 @@
return function(response) {
var error;
if (response.errorCode) {
- error = new U2FError(response.errorCode);
+ error = new U2FError(response.errorCode, 'register');
return _this.renderError(error);
} else {
return _this.renderRegistered(JSON.stringify(response));
@@ -76,7 +76,8 @@
U2FRegister.prototype.renderError = function(error) {
this.renderTemplate('error', {
- error_message: error.message()
+ error_message: error.message(),
+ error_code: error.errorCode
});
return this.container.find('#js-u2f-try-again').on('click', this.renderSetup);
};
@@ -93,7 +94,5 @@
};
return U2FRegister;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/u2f/util.js b/app/assets/javascripts/u2f/util.js
index eedd3bcd5a1..34e88220b12 100644
--- a/app/assets/javascripts/u2f/util.js
+++ b/app/assets/javascripts/u2f/util.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, padded-blocks */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife */
(function() {
this.U2FUtil = (function() {
function U2FUtil() {}
@@ -8,7 +8,5 @@
};
return U2FUtil;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/user.js.es6 b/app/assets/javascripts/user.js.es6
index 0a2db7c05fe..059e6c628b3 100644
--- a/app/assets/javascripts/user.js.es6
+++ b/app/assets/javascripts/user.js.es6
@@ -1,4 +1,4 @@
-/* eslint-disable class-methods-use-this, comma-dangle, arrow-parens, no-param-reassign, semi */
+/* eslint-disable class-methods-use-this, comma-dangle, arrow-parens, no-param-reassign */
/* global Cookies */
((global) => {
@@ -30,5 +30,5 @@
$(this).parents('.project-limit-message').remove();
});
}
- }
+ };
})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/user_tabs.js.es6 b/app/assets/javascripts/user_tabs.js.es6
index b9c23b51b4d..465618e3d53 100644
--- a/app/assets/javascripts/user_tabs.js.es6
+++ b/app/assets/javascripts/user_tabs.js.es6
@@ -1,4 +1,4 @@
-/* eslint-disable max-len, space-before-function-paren, no-underscore-dangle, array-bracket-spacing, consistent-return, comma-dangle, no-unused-vars, dot-notation, no-new, no-return-assign, camelcase, semi, no-param-reassign */
+/* eslint-disable max-len, space-before-function-paren, no-underscore-dangle, consistent-return, comma-dangle, no-unused-vars, dot-notation, no-new, no-return-assign, camelcase, no-param-reassign */
/*
UserTabs
@@ -107,7 +107,7 @@ content on the Users#show page.
this.loadActivities(source);
}
- const loadableActions = [ 'groups', 'contributed', 'projects', 'snippets' ];
+ const loadableActions = ['groups', 'contributed', 'projects', 'snippets'];
if (loadableActions.indexOf(action) > -1) {
return this.loadTab(source, action);
}
@@ -145,11 +145,10 @@ content on the Users#show page.
}
setCurrentAction(source, action) {
- let new_state = source
+ let new_state = source;
new_state = new_state.replace(/\/+$/, '');
new_state += this._location.search + this._location.hash;
history.replaceState({
- turbolinks: true,
url: new_state
}, document.title, new_state);
return new_state;
diff --git a/app/assets/javascripts/users/calendar.js b/app/assets/javascripts/users/calendar.js
index 578be7c3590..6e40dfdf3d8 100644
--- a/app/assets/javascripts/users/calendar.js
+++ b/app/assets/javascripts/users/calendar.js
@@ -1,9 +1,8 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, camelcase, vars-on-top, semi, keyword-spacing, no-plusplus, object-shorthand, comma-dangle, eqeqeq, no-mixed-operators, no-return-assign, newline-per-chained-call, prefer-arrow-callback, consistent-return, one-var, one-var-declaration-per-line, prefer-template, quotes, no-unused-vars, no-else-return, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, camelcase, vars-on-top, object-shorthand, comma-dangle, eqeqeq, no-mixed-operators, no-return-assign, newline-per-chained-call, prefer-arrow-callback, consistent-return, one-var, one-var-declaration-per-line, prefer-template, quotes, no-unused-vars, no-else-return, max-len */
/* global d3 */
-/* global dateFormat */
(function() {
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+ var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
this.Calendar = (function() {
function Calendar(timestamps, calendar_activities_path) {
@@ -20,7 +19,7 @@
this.timestampsTmp = [];
var group = 0;
- var today = new Date()
+ var today = new Date();
today.setHours(0, 0, 0, 0, 0);
var oneYearAgo = new Date(today);
@@ -28,18 +27,18 @@
var days = gl.utils.getDayDifference(oneYearAgo, today);
- for(var i = 0; i <= days; i++) {
+ for (var i = 0; i <= days; i += 1) {
var date = new Date(oneYearAgo);
date.setDate(date.getDate() + i);
var day = date.getDay();
- var count = timestamps[dateFormat(date, 'yyyy-mm-dd')];
+ var count = timestamps[date.format('yyyy-mm-dd')];
// Create a new group array if this is the first day of the week
// or if is first object
if ((day === 0 && i !== 0) || i === 0) {
this.timestampsTmp.push([]);
- group++;
+ group += 1;
}
var innerArray = this.timestampsTmp[group - 1];
@@ -74,7 +73,7 @@
}
return extraWidthPadding;
- }
+ };
Calendar.prototype.renderSvg = function(group) {
var width = (group + 1) * this.daySizeWithSpace + this.getExtraWidthPadding(group);
@@ -122,7 +121,7 @@
if (stamp.count > 0) {
contribText = stamp.count + " contribution" + (stamp.count > 1 ? 's' : '');
}
- dateText = dateFormat(date, 'mmm d, yyyy');
+ dateText = date.format('mmm d, yyyy');
return contribText + "<br />" + (gl.utils.getDayName(date)) + " " + dateText;
};
})(this)).attr('class', 'user-contrib-cell js-tooltip').attr('fill', (function(_this) {
@@ -158,7 +157,7 @@
};
Calendar.prototype.renderMonths = function() {
- return this.svg.append('g').selectAll('text').data(this.months).enter().append('text').attr('x', function(date) {
+ return this.svg.append('g').attr('direction', 'ltr').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) {
@@ -221,7 +220,5 @@
};
return Calendar;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/users/users_bundle.js b/app/assets/javascripts/users/users_bundle.js
index f50802bdf2e..4cad60a59b1 100644
--- a/app/assets/javascripts/users/users_bundle.js
+++ b/app/assets/javascripts/users/users_bundle.js
@@ -1,7 +1,3 @@
-/* eslint-disable func-names, space-before-function-paren */
-
-/*= require_tree . */
-
-(function() {
-
-}).call(this);
+// require everything else in this directory
+function requireAll(context) { return context.keys().map(context); }
+requireAll(require.context('.', false, /^\.\/(?!users_bundle).*\.(js|es6)$/));
diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js
index d4b5e03aa35..d4b24d13299 100644
--- a/app/assets/javascripts/users_select.js
+++ b/app/assets/javascripts/users_select.js
@@ -1,14 +1,15 @@
-/* eslint-disable func-names, space-before-function-paren, one-var, no-var, space-before-blocks, prefer-rest-params, wrap-iife, quotes, max-len, one-var-declaration-per-line, vars-on-top, prefer-arrow-callback, consistent-return, comma-dangle, object-shorthand, no-shadow, no-unused-vars, no-plusplus, no-else-return, no-self-compare, prefer-template, no-unused-expressions, no-lonely-if, yoda, prefer-spread, no-void, camelcase, keyword-spacing, no-param-reassign, padded-blocks */
+/* eslint-disable func-names, space-before-function-paren, one-var, no-var, prefer-rest-params, wrap-iife, quotes, max-len, one-var-declaration-per-line, vars-on-top, prefer-arrow-callback, consistent-return, comma-dangle, object-shorthand, no-shadow, no-unused-vars, no-else-return, no-self-compare, prefer-template, no-unused-expressions, no-lonely-if, yoda, prefer-spread, no-void, camelcase, no-param-reassign */
/* global Vue */
/* global Issuable */
/* global ListUser */
(function() {
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; },
+ var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; },
slice = [].slice;
this.UsersSelect = (function() {
- function UsersSelect(currentUser) {
+ function UsersSelect(currentUser, els) {
+ var $els;
this.users = bind(this.users, this);
this.user = bind(this.user, this);
this.usersPath = "/autocomplete/users.json";
@@ -20,7 +21,14 @@
this.currentUser = JSON.parse(currentUser);
}
}
- $('.js-user-search').each((function(_this) {
+
+ $els = $(els);
+
+ if (!els) {
+ $els = $('.js-user-search');
+ }
+
+ $els.each((function(_this) {
return function(i, dropdown) {
var options = {};
var $block, $collapsedSidebar, $dropdown, $loading, $selectbox, $value, abilityName, assignTo, assigneeTemplate, collapsedAssigneeTemplate, defaultLabel, firstUser, issueURL, selectedId, showAnyUser, showNullUser, showMenuAbove;
@@ -116,7 +124,7 @@
showDivider = 0;
if (firstUser) {
// Move current user to the front of the list
- for (index = j = 0, len = users.length; j < len; index = ++j) {
+ for (index = j = 0, len = users.length; j < len; index = (j += 1)) {
obj = users[index];
if (obj.username === firstUser) {
users.splice(index, 1);
@@ -193,7 +201,9 @@
selectedId = user.id;
return;
}
- if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar')) {
+ if ($el.closest('.add-issues-modal').length) {
+ gl.issueBoards.ModalStore.store.filter[$dropdown.data('field-name')] = user.id;
+ } else if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar')) {
selectedId = user.id;
gl.issueBoards.BoardsStore.state.filters[$dropdown.data('field-name')] = user.id;
gl.issueBoards.BoardsStore.updateFiltersUrl();
@@ -278,7 +288,7 @@
if (firstUser) {
// Move current user to the front of the list
ref = data.results;
- for (index = j = 0, len = ref.length; j < len; index = ++j) {
+ for (index = j = 0, len = ref.length; j < len; index = (j += 1)) {
obj = ref[index];
if (obj.username === firstUser) {
data.results.splice(index, 1);
@@ -371,7 +381,7 @@
};
UsersSelect.prototype.user = function(user_id, callback) {
- if(!/^\d+$/.test(user_id)) {
+ if (!/^\d+$/.test(user_id)) {
return false;
}
@@ -421,7 +431,5 @@
};
return UsersSelect;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/version_check_image.js.es6 b/app/assets/javascripts/version_check_image.js.es6
new file mode 100644
index 00000000000..1fa2b5ac399
--- /dev/null
+++ b/app/assets/javascripts/version_check_image.js.es6
@@ -0,0 +1,10 @@
+(() => {
+ class VersionCheckImage {
+ static bindErrorEvent(imageElement) {
+ imageElement.off('error').on('error', () => imageElement.hide());
+ }
+ }
+
+ window.gl = window.gl || {};
+ gl.VersionCheckImage = VersionCheckImage;
+})();
diff --git a/app/assets/javascripts/visibility_select.js.es6 b/app/assets/javascripts/visibility_select.js.es6
new file mode 100644
index 00000000000..f712d7ba930
--- /dev/null
+++ b/app/assets/javascripts/visibility_select.js.es6
@@ -0,0 +1,27 @@
+(() => {
+ const gl = window.gl || (window.gl = {});
+
+ class VisibilitySelect {
+ constructor(container) {
+ if (!container) throw new Error('VisibilitySelect requires a container element as argument 1');
+ this.container = container;
+ this.helpBlock = this.container.querySelector('.help-block');
+ this.select = this.container.querySelector('select');
+ }
+
+ init() {
+ if (this.select) {
+ this.updateHelpText();
+ this.select.addEventListener('change', this.updateHelpText.bind(this));
+ } else {
+ this.helpBlock.textContent = this.container.querySelector('.js-locked').dataset.helpBlock;
+ }
+ }
+
+ updateHelpText() {
+ this.helpBlock.textContent = this.select.querySelector('option:checked').dataset.description;
+ }
+ }
+
+ gl.VisibilitySelect = VisibilitySelect;
+})();
diff --git a/app/assets/javascripts/vue_common_component/commit.js.es6 b/app/assets/javascripts/vue_common_component/commit.js.es6
deleted file mode 100644
index 62a22e39a3b..00000000000
--- a/app/assets/javascripts/vue_common_component/commit.js.es6
+++ /dev/null
@@ -1,163 +0,0 @@
-/*= require vue */
-/* global Vue */
-(() => {
- window.gl = window.gl || {};
-
- window.gl.CommitComponent = Vue.component('commit-component', {
-
- props: {
- /**
- * Indicates the existance of a tag.
- * Used to render the correct icon, if true will render `fa-tag` icon,
- * if false will render `fa-code-fork` icon.
- */
- tag: {
- type: Boolean,
- required: false,
- default: false,
- },
-
- /**
- * If provided is used to render the branch name and url.
- * Should contain the following properties:
- * name
- * ref_url
- */
- commitRef: {
- type: Object,
- required: false,
- default: () => ({}),
- },
-
- /**
- * Used to link to the commit sha.
- */
- commitUrl: {
- type: String,
- required: false,
- default: '',
- },
-
- /**
- * Used to show the commit short sha that links to the commit url.
- */
- shortSha: {
- type: String,
- required: false,
- default: '',
- },
-
- /**
- * If provided shows the commit tile.
- */
- title: {
- type: String,
- required: false,
- default: '',
- },
-
- /**
- * If provided renders information about the author of the commit.
- * When provided should include:
- * `avatar_url` to render the avatar icon
- * `web_url` to link to user profile
- * `username` to render alt and title tags
- */
- author: {
- type: Object,
- required: false,
- default: () => ({}),
- },
-
- commitIconSvg: {
- type: String,
- required: false,
- },
- },
-
- computed: {
- /**
- * Used to verify if all the properties needed to render the commit
- * ref section were provided.
- *
- * TODO: Improve this! Use lodash _.has when we have it.
- *
- * @returns {Boolean}
- */
- hasCommitRef() {
- return this.commitRef && this.commitRef.name && this.commitRef.ref_url;
- },
-
- /**
- * Used to verify if all the properties needed to render the commit
- * author section were provided.
- *
- * TODO: Improve this! Use lodash _.has when we have it.
- *
- * @returns {Boolean}
- */
- hasAuthor() {
- return this.author &&
- this.author.avatar_url &&
- this.author.web_url &&
- this.author.username;
- },
-
- /**
- * If information about the author is provided will return a string
- * to be rendered as the alt attribute of the img tag.
- *
- * @returns {String}
- */
- userImageAltDescription() {
- return this.author &&
- this.author.username ? `${this.author.username}'s avatar` : null;
- },
- },
-
- template: `
- <div class="branch-commit">
-
- <div v-if="hasCommitRef" class="icon-container">
- <i v-if="tag" class="fa fa-tag"></i>
- <i v-if="!tag" class="fa fa-code-fork"></i>
- </div>
-
- <a v-if="hasCommitRef"
- class="monospace branch-name"
- :href="commitRef.ref_url">
- {{commitRef.name}}
- </a>
-
- <div v-html="commitIconSvg" class="commit-icon js-commit-icon"></div>
-
- <a class="commit-id monospace"
- :href="commitUrl">
- {{shortSha}}
- </a>
-
- <p class="commit-title">
- <span v-if="title">
- <a v-if="hasAuthor"
- class="avatar-image-container"
- :href="author.web_url">
- <img
- class="avatar has-tooltip s20"
- :src="author.avatar_url"
- :alt="userImageAltDescription"
- :title="author.username" />
- </a>
-
- <a class="commit-row-message"
- :href="commitUrl">
- {{title}}
- </a>
- </span>
- <span v-else>
- Cant find HEAD commit for this branch
- </span>
- </p>
- </div>
- `,
- });
-})();
diff --git a/app/assets/javascripts/vue_pipelines_index/index.js.es6 b/app/assets/javascripts/vue_pipelines_index/index.js.es6
new file mode 100644
index 00000000000..e7432afb56e
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/index.js.es6
@@ -0,0 +1,36 @@
+/* eslint-disable no-param-reassign */
+/* global Vue, VueResource, gl */
+window.Vue = require('vue');
+window.Vue.use(require('vue-resource'));
+require('../lib/utils/common_utils');
+require('../vue_shared/vue_resource_interceptor');
+require('./pipelines');
+
+$(() => new Vue({
+ el: document.querySelector('.vue-pipelines-index'),
+
+ data() {
+ const project = document.querySelector('.pipelines');
+ const svgs = document.querySelector('.pipeline-svgs').dataset;
+
+ // Transform svgs DOMStringMap to a plain Object.
+ const svgsObject = gl.utils.DOMStringMapToObject(svgs);
+
+ return {
+ scope: project.dataset.url,
+ store: new gl.PipelineStore(),
+ svgs: svgsObject,
+ };
+ },
+ components: {
+ 'vue-pipelines': gl.VuePipelines,
+ },
+ template: `
+ <vue-pipelines
+ :scope='scope'
+ :store='store'
+ :svgs='svgs'
+ >
+ </vue-pipelines>
+ `,
+}));
diff --git a/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 b/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6
new file mode 100644
index 00000000000..8106934e864
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6
@@ -0,0 +1,104 @@
+/* global Vue, Flash, gl */
+/* eslint-disable no-param-reassign */
+
+((gl) => {
+ gl.VuePipelineActions = Vue.extend({
+ props: ['pipeline', 'svgs'],
+ computed: {
+ actions() {
+ return this.pipeline.details.manual_actions.length > 0;
+ },
+ artifacts() {
+ return this.pipeline.details.artifacts.length > 0;
+ },
+ },
+ methods: {
+ download(name) {
+ return `Download ${name} artifacts`;
+ },
+ },
+ template: `
+ <td class="pipeline-actions hidden-xs">
+ <div class="controls pull-right">
+ <div class="btn-group inline">
+ <div class="btn-group">
+ <button
+ v-if='actions'
+ class="dropdown-toggle btn btn-default has-tooltip js-pipeline-dropdown-manual-actions"
+ data-toggle="dropdown"
+ title="Manual job"
+ data-placement="top"
+ aria-label="Manual job"
+ >
+ <span v-html='svgs.iconPlay' aria-hidden="true"></span>
+ <i class="fa fa-caret-down" aria-hidden="true"></i>
+ </button>
+ <ul class="dropdown-menu dropdown-menu-align-right">
+ <li v-for='action in pipeline.details.manual_actions'>
+ <a
+ rel="nofollow"
+ data-method="post"
+ :href='action.path'
+ >
+ <span v-html='svgs.iconPlay' aria-hidden="true"></span>
+ <span>{{action.name}}</span>
+ </a>
+ </li>
+ </ul>
+ </div>
+ <div class="btn-group">
+ <button
+ v-if='artifacts'
+ class="dropdown-toggle btn btn-default build-artifacts has-tooltip js-pipeline-dropdown-download"
+ title="Artifacts"
+ data-placement="top"
+ data-toggle="dropdown"
+ aria-label="Artifacts"
+ >
+ <i class="fa fa-download" aria-hidden="true"></i>
+ <i class="fa fa-caret-down" aria-hidden="true"></i>
+ </button>
+ <ul class="dropdown-menu dropdown-menu-align-right">
+ <li v-for='artifact in pipeline.details.artifacts'>
+ <a
+ rel="nofollow"
+ :href='artifact.path'
+ >
+ <i class="fa fa-download" aria-hidden="true"></i>
+ <span>{{download(artifact.name)}}</span>
+ </a>
+ </li>
+ </ul>
+ </div>
+ </div>
+ <div class="cancel-retry-btns inline">
+ <a
+ v-if='pipeline.flags.retryable'
+ class="btn has-tooltip"
+ title="Retry"
+ rel="nofollow"
+ data-method="post"
+ data-placement="top"
+ data-toggle="dropdown"
+ :href='pipeline.retry_path'
+ aria-label="Retry">
+ <i class="fa fa-repeat" aria-hidden="true"></i>
+ </a>
+ <a
+ v-if='pipeline.flags.cancelable'
+ class="btn btn-remove has-tooltip"
+ title="Cancel"
+ rel="nofollow"
+ data-method="post"
+ data-placement="top"
+ data-toggle="dropdown"
+ :href='pipeline.cancel_path'
+ aria-label="Cancel">
+ <i class="fa fa-remove" aria-hidden="true"></i>
+ </a>
+ </div>
+ </div>
+ </td>
+ `,
+ });
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/vue_pipelines_index/pipeline_url.js.es6 b/app/assets/javascripts/vue_pipelines_index/pipeline_url.js.es6
new file mode 100644
index 00000000000..ae5649f0519
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/pipeline_url.js.es6
@@ -0,0 +1,63 @@
+/* global Vue, gl */
+/* eslint-disable no-param-reassign */
+
+((gl) => {
+ gl.VuePipelineUrl = Vue.extend({
+ props: [
+ 'pipeline',
+ ],
+ computed: {
+ user() {
+ return !!this.pipeline.user;
+ },
+ },
+ template: `
+ <td>
+ <a :href='pipeline.path'>
+ <span class="pipeline-id">#{{pipeline.id}}</span>
+ </a>
+ <span>by</span>
+ <a
+ v-if='user'
+ :href='pipeline.user.web_url'
+ >
+ <img
+ v-if='user'
+ class="avatar has-tooltip s20 "
+ :title='pipeline.user.name'
+ data-container="body"
+ :src='pipeline.user.avatar_url'
+ >
+ </a>
+ <span
+ v-if='!user'
+ class="api monospace"
+ >
+ API
+ </span>
+ <span
+ v-if='pipeline.flags.latest'
+ class="label label-success has-tooltip"
+ title="Latest pipeline for this branch"
+ data-original-title="Latest pipeline for this branch"
+ >
+ latest
+ </span>
+ <span
+ v-if='pipeline.flags.yaml_errors'
+ class="label label-danger has-tooltip"
+ :title='pipeline.yaml_errors'
+ :data-original-title='pipeline.yaml_errors'
+ >
+ yaml invalid
+ </span>
+ <span
+ v-if='pipeline.flags.stuck'
+ class="label label-warning"
+ >
+ stuck
+ </span>
+ </td>
+ `,
+ });
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6 b/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6
new file mode 100644
index 00000000000..e47dc6935d6
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6
@@ -0,0 +1,73 @@
+/* global Vue, gl */
+/* eslint-disable no-param-reassign */
+
+window.Vue = require('vue');
+require('../vue_shared/components/table_pagination');
+require('./store');
+require('../vue_shared/components/pipelines_table');
+
+((gl) => {
+ gl.VuePipelines = Vue.extend({
+
+ components: {
+ 'gl-pagination': gl.VueGlPagination,
+ 'pipelines-table-component': gl.pipelines.PipelinesTableComponent,
+ },
+
+ data() {
+ return {
+ pipelines: [],
+ timeLoopInterval: '',
+ intervalId: '',
+ apiScope: 'all',
+ pageInfo: {},
+ pagenum: 1,
+ count: { all: 0, running_or_pending: 0 },
+ pageRequest: false,
+ };
+ },
+ props: ['scope', 'store', 'svgs'],
+ created() {
+ const pagenum = gl.utils.getParameterByName('p');
+ const scope = gl.utils.getParameterByName('scope');
+ if (pagenum) this.pagenum = pagenum;
+ if (scope) this.apiScope = scope;
+ this.store.fetchDataLoop.call(this, Vue, this.pagenum, this.scope, this.apiScope);
+ },
+ methods: {
+ change(pagenum, apiScope) {
+ gl.utils.visitUrl(`?scope=${apiScope}&p=${pagenum}`);
+ },
+ },
+ template: `
+ <div>
+ <div class="pipelines realtime-loading" v-if='pageRequest'>
+ <i class="fa fa-spinner fa-spin"></i>
+ </div>
+
+ <div class="blank-state blank-state-no-icon"
+ v-if="!pageRequest && pipelines.length === 0">
+ <h2 class="blank-state-title js-blank-state-title">
+ No pipelines to show
+ </h2>
+ </div>
+
+ <div class="table-holder" v-if='!pageRequest && pipelines.length'>
+ <pipelines-table-component
+ :pipelines='pipelines'
+ :svgs='svgs'>
+ </pipelines-table-component>
+ </div>
+
+ <gl-pagination
+ v-if='!pageRequest && pipelines.length && pageInfo.total > pageInfo.perPage'
+ :pagenum='pagenum'
+ :change='change'
+ :count='count.all'
+ :pageInfo='pageInfo'
+ >
+ </gl-pagination>
+ </div>
+ `,
+ });
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/vue_pipelines_index/stage.js.es6 b/app/assets/javascripts/vue_pipelines_index/stage.js.es6
new file mode 100644
index 00000000000..8cc417a9966
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/stage.js.es6
@@ -0,0 +1,103 @@
+/* global Vue, Flash, gl */
+/* eslint-disable no-param-reassign */
+
+((gl) => {
+ gl.VueStage = Vue.extend({
+ data() {
+ return {
+ builds: '',
+ spinner: '<span class="fa fa-spinner fa-spin"></span>',
+ };
+ },
+ props: {
+ stage: {
+ type: Object,
+ required: true,
+ },
+ svgs: {
+ type: Object,
+ required: true,
+ },
+ match: {
+ type: Function,
+ required: true,
+ },
+ },
+ methods: {
+ fetchBuilds(e) {
+ const areaExpanded = e.currentTarget.attributes['aria-expanded'];
+
+ if (areaExpanded && (areaExpanded.textContent === 'true')) return null;
+
+ return this.$http.get(this.stage.dropdown_path)
+ .then((response) => {
+ this.builds = JSON.parse(response.body).html;
+ }, () => {
+ const flash = new Flash('Something went wrong on our end.');
+ return flash;
+ });
+ },
+ keepGraph(e) {
+ const { target } = e;
+
+ if (target.className.indexOf('js-ci-action-icon') >= 0) return null;
+
+ if (
+ target.parentElement &&
+ (target.parentElement.className.indexOf('js-ci-action-icon') >= 0)
+ ) return null;
+
+ return e.stopPropagation();
+ },
+ },
+ computed: {
+ buildsOrSpinner() {
+ return this.builds ? this.builds : this.spinner;
+ },
+ dropdownClass() {
+ if (this.builds) return 'js-builds-dropdown-container';
+ return 'js-builds-dropdown-loading builds-dropdown-loading';
+ },
+ buildStatus() {
+ return `Build: ${this.stage.status.label}`;
+ },
+ tooltip() {
+ return `has-tooltip ci-status-icon ci-status-icon-${this.stage.status.group}`;
+ },
+ svg() {
+ const { icon } = this.stage.status;
+ const stageIcon = icon.replace(/icon/i, 'stage_icon');
+ return this.svgs[this.match(stageIcon)];
+ },
+ triggerButtonClass() {
+ return `mini-pipeline-graph-dropdown-toggle has-tooltip js-builds-dropdown-button ci-status-icon-${this.stage.status.group}`;
+ },
+ },
+ template: `
+ <div>
+ <button
+ @click='fetchBuilds($event)'
+ :class="triggerButtonClass"
+ :title='stage.title'
+ data-placement="top"
+ data-toggle="dropdown"
+ type="button"
+ :aria-label='stage.title'
+ >
+ <span v-html="svg" aria-hidden="true"></span>
+ <i class="fa fa-caret-down" aria-hidden="true"></i>
+ </button>
+ <ul class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container">
+ <div class="arrow-up" aria-hidden="true"></div>
+ <div
+ @click='keepGraph($event)'
+ :class="dropdownClass"
+ class="js-builds-dropdown-list scrollable-menu"
+ v-html="buildsOrSpinner"
+ >
+ </div>
+ </ul>
+ </div>
+ `,
+ });
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/vue_pipelines_index/status.js.es6 b/app/assets/javascripts/vue_pipelines_index/status.js.es6
new file mode 100644
index 00000000000..05175082704
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/status.js.es6
@@ -0,0 +1,34 @@
+/* global Vue, gl */
+/* eslint-disable no-param-reassign */
+
+((gl) => {
+ gl.VueStatusScope = Vue.extend({
+ props: [
+ 'pipeline', 'svgs', 'match',
+ ],
+ computed: {
+ cssClasses() {
+ const cssObject = { 'ci-status': true };
+ cssObject[`ci-${this.pipeline.details.status.group}`] = true;
+ return cssObject;
+ },
+ svg() {
+ return this.svgs[this.match(this.pipeline.details.status.icon)];
+ },
+ detailsPath() {
+ const { status } = this.pipeline.details;
+ return status.has_details ? status.details_path : false;
+ },
+ },
+ template: `
+ <td class="commit-link">
+ <a
+ :class='cssClasses'
+ :href='detailsPath'
+ v-html='svg + pipeline.details.status.text'
+ >
+ </a>
+ </td>
+ `,
+ });
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/vue_pipelines_index/store.js.es6 b/app/assets/javascripts/vue_pipelines_index/store.js.es6
new file mode 100644
index 00000000000..0ee21f00fdc
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/store.js.es6
@@ -0,0 +1,68 @@
+/* global gl, Flash */
+/* eslint-disable no-param-reassign, no-underscore-dangle */
+require('../vue_realtime_listener');
+
+((gl) => {
+ const pageValues = (headers) => {
+ const normalized = gl.utils.normalizeHeaders(headers);
+
+ const paginationInfo = {
+ perPage: +normalized['X-PER-PAGE'],
+ page: +normalized['X-PAGE'],
+ total: +normalized['X-TOTAL'],
+ totalPages: +normalized['X-TOTAL-PAGES'],
+ nextPage: +normalized['X-NEXT-PAGE'],
+ previousPage: +normalized['X-PREV-PAGE'],
+ };
+
+ return paginationInfo;
+ };
+
+ gl.PipelineStore = class {
+ fetchDataLoop(Vue, pageNum, url, apiScope) {
+ this.pageRequest = true;
+ const updatePipelineNums = (count) => {
+ const { all } = count;
+ const running = count.running_or_pending;
+ document.querySelector('.js-totalbuilds-count').innerHTML = all;
+ document.querySelector('.js-running-count').innerHTML = running;
+ };
+
+ const goFetch = () =>
+ this.$http.get(`${url}?scope=${apiScope}&page=${pageNum}`)
+ .then((response) => {
+ const pageInfo = pageValues(response.headers);
+ this.pageInfo = Object.assign({}, this.pageInfo, pageInfo);
+
+ const res = JSON.parse(response.body);
+ this.count = Object.assign({}, this.count, res.count);
+ this.pipelines = Object.assign([], this.pipelines, res.pipelines);
+
+ updatePipelineNums(this.count);
+ this.pageRequest = false;
+ }, () => {
+ this.pageRequest = false;
+ return new Flash('An error occurred while fetching the pipelines, please reload the page again.');
+ });
+
+ goFetch();
+
+ const startTimeLoops = () => {
+ this.timeLoopInterval = setInterval(() => {
+ this.$children[0].$children.reduce((acc, component) => {
+ const timeAgoComponent = component.$children.filter(el => el.$options._componentTag === 'time-ago')[0];
+ acc.push(timeAgoComponent);
+ return acc;
+ }, []).forEach(e => e.changeTime());
+ }, 10000);
+ };
+
+ startTimeLoops();
+
+ const removeIntervals = () => clearInterval(this.timeLoopInterval);
+ const startIntervals = () => startTimeLoops();
+
+ gl.VueRealtimeListener(removeIntervals, startIntervals);
+ }
+ };
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/vue_pipelines_index/time_ago.js.es6 b/app/assets/javascripts/vue_pipelines_index/time_ago.js.es6
new file mode 100644
index 00000000000..3598da11573
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/time_ago.js.es6
@@ -0,0 +1,76 @@
+/* global Vue, gl */
+/* eslint-disable no-param-reassign */
+
+window.Vue = require('vue');
+require('../lib/utils/datetime_utility');
+
+((gl) => {
+ gl.VueTimeAgo = Vue.extend({
+ data() {
+ return {
+ currentTime: new Date(),
+ };
+ },
+ props: ['pipeline', 'svgs'],
+ computed: {
+ timeAgo() {
+ return gl.utils.getTimeago();
+ },
+ localTimeFinished() {
+ return gl.utils.formatDate(this.pipeline.details.finished_at);
+ },
+ timeStopped() {
+ const changeTime = this.currentTime;
+ const options = {
+ weekday: 'long',
+ year: 'numeric',
+ month: 'short',
+ day: 'numeric',
+ };
+ options.timeZoneName = 'short';
+ const finished = this.pipeline.details.finished_at;
+ if (!finished && changeTime) return false;
+ return ({ words: this.timeAgo.format(finished) });
+ },
+ duration() {
+ const { duration } = this.pipeline.details;
+ const date = new Date(duration * 1000);
+
+ let hh = date.getUTCHours();
+ let mm = date.getUTCMinutes();
+ let ss = date.getSeconds();
+
+ if (hh < 10) hh = `0${hh}`;
+ if (mm < 10) mm = `0${mm}`;
+ if (ss < 10) ss = `0${ss}`;
+
+ if (duration !== null) return `${hh}:${mm}:${ss}`;
+ return false;
+ },
+ },
+ methods: {
+ changeTime() {
+ this.currentTime = new Date();
+ },
+ },
+ template: `
+ <td>
+ <p class="duration" v-if='duration'>
+ <span v-html='svgs.iconTimer'></span>
+ {{duration}}
+ </p>
+ <p class="finished-at" v-if='timeStopped'>
+ <i class="fa fa-calendar"></i>
+ <time
+ data-toggle="tooltip"
+ data-placement="top"
+ data-container="body"
+ :data-original-title='localTimeFinished'
+ >
+ {{timeStopped.words}}
+ </time>
+ </p>
+ </td>
+ `,
+ });
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/vue_realtime_listener/index.js.es6 b/app/assets/javascripts/vue_realtime_listener/index.js.es6
new file mode 100644
index 00000000000..95564152cce
--- /dev/null
+++ b/app/assets/javascripts/vue_realtime_listener/index.js.es6
@@ -0,0 +1,18 @@
+/* eslint-disable no-param-reassign */
+
+((gl) => {
+ gl.VueRealtimeListener = (removeIntervals, startIntervals) => {
+ const removeAll = () => {
+ removeIntervals();
+ window.removeEventListener('beforeunload', removeIntervals);
+ window.removeEventListener('focus', startIntervals);
+ window.removeEventListener('blur', removeIntervals);
+ document.removeEventListener('beforeunload', removeAll);
+ };
+
+ window.addEventListener('beforeunload', removeIntervals);
+ window.addEventListener('focus', startIntervals);
+ window.addEventListener('blur', removeIntervals);
+ document.addEventListener('beforeunload', removeAll);
+ };
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/vue_shared/components/commit.js.es6 b/app/assets/javascripts/vue_shared/components/commit.js.es6
new file mode 100644
index 00000000000..7f7c18ddeb1
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/commit.js.es6
@@ -0,0 +1,163 @@
+/* global Vue */
+
+(() => {
+ window.gl = window.gl || {};
+
+ window.gl.CommitComponent = Vue.component('commit-component', {
+
+ props: {
+ /**
+ * Indicates the existance of a tag.
+ * Used to render the correct icon, if true will render `fa-tag` icon,
+ * if false will render `fa-code-fork` icon.
+ */
+ tag: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+
+ /**
+ * If provided is used to render the branch name and url.
+ * Should contain the following properties:
+ * name
+ * ref_url
+ */
+ commitRef: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+
+ /**
+ * Used to link to the commit sha.
+ */
+ commitUrl: {
+ type: String,
+ required: false,
+ default: '',
+ },
+
+ /**
+ * Used to show the commit short sha that links to the commit url.
+ */
+ shortSha: {
+ type: String,
+ required: false,
+ default: '',
+ },
+
+ /**
+ * If provided shows the commit tile.
+ */
+ title: {
+ type: String,
+ required: false,
+ default: '',
+ },
+
+ /**
+ * If provided renders information about the author of the commit.
+ * When provided should include:
+ * `avatar_url` to render the avatar icon
+ * `web_url` to link to user profile
+ * `username` to render alt and title tags
+ */
+ author: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+
+ commitIconSvg: {
+ type: String,
+ required: false,
+ },
+ },
+
+ computed: {
+ /**
+ * Used to verify if all the properties needed to render the commit
+ * ref section were provided.
+ *
+ * TODO: Improve this! Use lodash _.has when we have it.
+ *
+ * @returns {Boolean}
+ */
+ hasCommitRef() {
+ return this.commitRef && this.commitRef.name && this.commitRef.ref_url;
+ },
+
+ /**
+ * Used to verify if all the properties needed to render the commit
+ * author section were provided.
+ *
+ * TODO: Improve this! Use lodash _.has when we have it.
+ *
+ * @returns {Boolean}
+ */
+ hasAuthor() {
+ return this.author &&
+ this.author.avatar_url &&
+ this.author.web_url &&
+ this.author.username;
+ },
+
+ /**
+ * If information about the author is provided will return a string
+ * to be rendered as the alt attribute of the img tag.
+ *
+ * @returns {String}
+ */
+ userImageAltDescription() {
+ return this.author &&
+ this.author.username ? `${this.author.username}'s avatar` : null;
+ },
+ },
+
+ template: `
+ <div class="branch-commit">
+
+ <div v-if="hasCommitRef" class="icon-container">
+ <i v-if="tag" class="fa fa-tag"></i>
+ <i v-if="!tag" class="fa fa-code-fork"></i>
+ </div>
+
+ <a v-if="hasCommitRef"
+ class="monospace branch-name"
+ :href="commitRef.ref_url">
+ {{commitRef.name}}
+ </a>
+
+ <div v-html="commitIconSvg" class="commit-icon js-commit-icon"></div>
+
+ <a class="commit-id monospace"
+ :href="commitUrl">
+ {{shortSha}}
+ </a>
+
+ <p class="commit-title">
+ <span v-if="title">
+ <a v-if="hasAuthor"
+ class="avatar-image-container"
+ :href="author.web_url">
+ <img
+ class="avatar has-tooltip s20"
+ :src="author.avatar_url"
+ :alt="userImageAltDescription"
+ :title="author.username" />
+ </a>
+
+ <a class="commit-row-message"
+ :href="commitUrl">
+ {{title}}
+ </a>
+ </span>
+ <span v-else>
+ Cant find HEAD commit for this branch
+ </span>
+ </p>
+ </div>
+ `,
+ });
+})();
diff --git a/app/assets/javascripts/vue_shared/components/pipelines_table.js.es6 b/app/assets/javascripts/vue_shared/components/pipelines_table.js.es6
new file mode 100644
index 00000000000..4bdaef31ee9
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/pipelines_table.js.es6
@@ -0,0 +1,61 @@
+/* eslint-disable no-param-reassign */
+/* global Vue */
+
+require('./pipelines_table_row');
+/**
+ * Pipelines Table Component.
+ *
+ * Given an array of objects, renders a table.
+ */
+
+(() => {
+ window.gl = window.gl || {};
+ gl.pipelines = gl.pipelines || {};
+
+ gl.pipelines.PipelinesTableComponent = Vue.component('pipelines-table-component', {
+
+ props: {
+ pipelines: {
+ type: Array,
+ required: true,
+ default: () => ([]),
+ },
+
+ /**
+ * TODO: Remove this when we have webpack.
+ */
+ svgs: {
+ type: Object,
+ required: true,
+ default: () => ({}),
+ },
+ },
+
+ components: {
+ 'pipelines-table-row-component': gl.pipelines.PipelinesTableRowComponent,
+ },
+
+ template: `
+ <table class="table ci-table">
+ <thead>
+ <tr>
+ <th class="js-pipeline-status pipeline-status">Status</th>
+ <th class="js-pipeline-info pipeline-info">Pipeline</th>
+ <th class="js-pipeline-commit pipeline-commit">Commit</th>
+ <th class="js-pipeline-stages pipeline-stages">Stages</th>
+ <th class="js-pipeline-date pipeline-date"></th>
+ <th class="js-pipeline-actions pipeline-actions hidden-xs"></th>
+ </tr>
+ </thead>
+ <tbody>
+ <template v-for="model in pipelines"
+ v-bind:model="model">
+ <tr is="pipelines-table-row-component"
+ :pipeline="model"
+ :svgs="svgs"></tr>
+ </template>
+ </tbody>
+ </table>
+ `,
+ });
+})();
diff --git a/app/assets/javascripts/vue_shared/components/pipelines_table_row.js.es6 b/app/assets/javascripts/vue_shared/components/pipelines_table_row.js.es6
new file mode 100644
index 00000000000..c819f0dd7cd
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/pipelines_table_row.js.es6
@@ -0,0 +1,234 @@
+/* eslint-disable no-param-reassign */
+/* global Vue */
+
+require('../../vue_pipelines_index/status');
+require('../../vue_pipelines_index/pipeline_url');
+require('../../vue_pipelines_index/stage');
+require('../../vue_pipelines_index/pipeline_actions');
+require('../../vue_pipelines_index/time_ago');
+require('./commit');
+/**
+ * Pipeline table row.
+ *
+ * Given the received object renders a table row in the pipelines' table.
+ */
+(() => {
+ window.gl = window.gl || {};
+ gl.pipelines = gl.pipelines || {};
+
+ gl.pipelines.PipelinesTableRowComponent = Vue.component('pipelines-table-row-component', {
+
+ props: {
+ pipeline: {
+ type: Object,
+ required: true,
+ default: () => ({}),
+ },
+
+ /**
+ * TODO: Remove this when we have webpack;
+ */
+ svgs: {
+ type: Object,
+ required: true,
+ default: () => ({}),
+ },
+ },
+
+ components: {
+ 'commit-component': gl.CommitComponent,
+ 'pipeline-actions': gl.VuePipelineActions,
+ 'dropdown-stage': gl.VueStage,
+ 'pipeline-url': gl.VuePipelineUrl,
+ 'status-scope': gl.VueStatusScope,
+ 'time-ago': gl.VueTimeAgo,
+ },
+
+ computed: {
+ /**
+ * If provided, returns the commit tag.
+ * Needed to render the commit component column.
+ *
+ * This field needs a lot of verification, because of different possible cases:
+ *
+ * 1. person who is an author of a commit might be a GitLab user
+ * 2. if person who is an author of a commit is a GitLab user he/she can have a GitLab avatar
+ * 3. If GitLab user does not have avatar he/she might have a Gravatar
+ * 4. If committer is not a GitLab User he/she can have a Gravatar
+ * 5. We do not have consistent API object in this case
+ * 6. We should improve API and the code
+ *
+ * @returns {Object|Undefined}
+ */
+ commitAuthor() {
+ let commitAuthorInformation;
+
+ // 1. person who is an author of a commit might be a GitLab user
+ if (this.pipeline &&
+ this.pipeline.commit &&
+ this.pipeline.commit.author) {
+ // 2. if person who is an author of a commit is a GitLab user
+ // he/she can have a GitLab avatar
+ if (this.pipeline.commit.author.avatar_url) {
+ commitAuthorInformation = this.pipeline.commit.author;
+
+ // 3. If GitLab user does not have avatar he/she might have a Gravatar
+ } else if (this.pipeline.commit.author_gravatar_url) {
+ commitAuthorInformation = Object.assign({}, this.pipeline.commit.author, {
+ avatar_url: this.pipeline.commit.author_gravatar_url,
+ });
+ }
+ }
+
+ // 4. If committer is not a GitLab User he/she can have a Gravatar
+ if (this.pipeline &&
+ this.pipeline.commit) {
+ commitAuthorInformation = {
+ avatar_url: this.pipeline.commit.author_gravatar_url,
+ web_url: `mailto:${this.pipeline.commit.author_email}`,
+ username: this.pipeline.commit.author_name,
+ };
+ }
+
+ return commitAuthorInformation;
+ },
+
+ /**
+ * If provided, returns the commit tag.
+ * Needed to render the commit component column.
+ *
+ * @returns {String|Undefined}
+ */
+ commitTag() {
+ if (this.pipeline.ref &&
+ this.pipeline.ref.tag) {
+ return this.pipeline.ref.tag;
+ }
+ return undefined;
+ },
+
+ /**
+ * If provided, returns the commit ref.
+ * Needed to render the commit component column.
+ *
+ * Matched `url` prop sent in the API to `path` prop needed
+ * in the commit component.
+ *
+ * @returns {Object|Undefined}
+ */
+ commitRef() {
+ if (this.pipeline.ref) {
+ return Object.keys(this.pipeline.ref).reduce((accumulator, prop) => {
+ if (prop === 'url') {
+ accumulator.path = this.pipeline.ref[prop];
+ } else {
+ accumulator[prop] = this.pipeline.ref[prop];
+ }
+ return accumulator;
+ }, {});
+ }
+
+ return undefined;
+ },
+
+ /**
+ * If provided, returns the commit url.
+ * Needed to render the commit component column.
+ *
+ * @returns {String|Undefined}
+ */
+ commitUrl() {
+ if (this.pipeline.commit &&
+ this.pipeline.commit.commit_path) {
+ return this.pipeline.commit.commit_path;
+ }
+ return undefined;
+ },
+
+ /**
+ * If provided, returns the commit short sha.
+ * Needed to render the commit component column.
+ *
+ * @returns {String|Undefined}
+ */
+ commitShortSha() {
+ if (this.pipeline.commit &&
+ this.pipeline.commit.short_id) {
+ return this.pipeline.commit.short_id;
+ }
+ return undefined;
+ },
+
+ /**
+ * If provided, returns the commit title.
+ * Needed to render the commit component column.
+ *
+ * @returns {String|Undefined}
+ */
+ commitTitle() {
+ if (this.pipeline.commit &&
+ this.pipeline.commit.title) {
+ return this.pipeline.commit.title;
+ }
+ return undefined;
+ },
+ },
+
+ methods: {
+ /**
+ * FIXME: This should not be in this component but in the components that
+ * need this function.
+ *
+ * Used to render SVGs in the following components:
+ * - status-scope
+ * - dropdown-stage
+ *
+ * @param {String} string
+ * @return {String}
+ */
+ match(string) {
+ return string.replace(/_([a-z])/g, (m, w) => w.toUpperCase());
+ },
+ },
+
+ template: `
+ <tr class="commit">
+ <status-scope
+ :pipeline="pipeline"
+ :svgs="svgs"
+ :match="match">
+ </status-scope>
+
+ <pipeline-url :pipeline="pipeline"></pipeline-url>
+
+ <td>
+ <commit-component
+ :tag="commitTag"
+ :commit-ref="commitRef"
+ :commit-url="commitUrl"
+ :short-sha="commitShortSha"
+ :title="commitTitle"
+ :author="commitAuthor"
+ :commit-icon-svg="svgs.commitIconSvg">
+ </commit-component>
+ </td>
+
+ <td class="stage-cell">
+ <div class="stage-container dropdown js-mini-pipeline-graph"
+ v-if="pipeline.details.stages.length > 0"
+ v-for="stage in pipeline.details.stages">
+ <dropdown-stage
+ :stage="stage"
+ :svgs="svgs"
+ :match="match">
+ </dropdown-stage>
+ </div>
+ </td>
+
+ <time-ago :pipeline="pipeline" :svgs="svgs"></time-ago>
+
+ <pipeline-actions :pipeline="pipeline" :svgs="svgs"></pipeline-actions>
+ </tr>
+ `,
+ });
+})();
diff --git a/app/assets/javascripts/vue_shared/components/table_pagination.js.es6 b/app/assets/javascripts/vue_shared/components/table_pagination.js.es6
new file mode 100644
index 00000000000..67c6cb73761
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/table_pagination.js.es6
@@ -0,0 +1,152 @@
+/* global Vue, gl */
+/* eslint-disable no-param-reassign, no-plusplus */
+
+window.Vue = require('vue');
+
+((gl) => {
+ const PAGINATION_UI_BUTTON_LIMIT = 4;
+ const UI_LIMIT = 6;
+ const SPREAD = '...';
+ const PREV = 'Prev';
+ const NEXT = 'Next';
+ const FIRST = '<< First';
+ const LAST = 'Last >>';
+
+ gl.VueGlPagination = Vue.extend({
+ props: {
+
+ // TODO: Consider refactoring in light of turbolinks removal.
+
+ /**
+ This function will take the information given by the pagination component
+ And make a new Turbolinks call
+
+ Here is an example `change` method:
+
+ change(pagenum, apiScope) {
+ gl.utils.visitUrl(`?scope=${apiScope}&p=${pagenum}`);
+ },
+ */
+
+ change: {
+ type: Function,
+ required: true,
+ },
+
+ /**
+ pageInfo will come from the headers of the API call
+ in the `.then` clause of the VueResource API call
+ there should be a function that contructs the pageInfo for this component
+
+ This is an example:
+
+ const pageInfo = headers => ({
+ perPage: +headers['X-Per-Page'],
+ page: +headers['X-Page'],
+ total: +headers['X-Total'],
+ totalPages: +headers['X-Total-Pages'],
+ nextPage: +headers['X-Next-Page'],
+ previousPage: +headers['X-Prev-Page'],
+ });
+ */
+
+ pageInfo: {
+ type: Object,
+ required: true,
+ },
+ },
+ methods: {
+ changePage(e) {
+ let apiScope = gl.utils.getParameterByName('scope');
+
+ if (!apiScope) apiScope = 'all';
+
+ const text = e.target.innerText;
+ const { totalPages, nextPage, previousPage } = this.pageInfo;
+
+ switch (text) {
+ case SPREAD:
+ break;
+ case LAST:
+ this.change(totalPages, apiScope);
+ break;
+ case NEXT:
+ this.change(nextPage, apiScope);
+ break;
+ case PREV:
+ this.change(previousPage, apiScope);
+ break;
+ case FIRST:
+ this.change(1, apiScope);
+ break;
+ default:
+ this.change(+text, apiScope);
+ break;
+ }
+ },
+ },
+ computed: {
+ prev() {
+ return this.pageInfo.previousPage;
+ },
+ next() {
+ return this.pageInfo.nextPage;
+ },
+ getItems() {
+ const total = this.pageInfo.totalPages;
+ const page = this.pageInfo.page;
+ const items = [];
+
+ if (page > 1) items.push({ title: FIRST });
+
+ if (page > 1) {
+ items.push({ title: PREV, prev: true });
+ } else {
+ items.push({ title: PREV, disabled: true, prev: true });
+ }
+
+ if (page > UI_LIMIT) items.push({ title: SPREAD, separator: true });
+
+ const start = Math.max(page - PAGINATION_UI_BUTTON_LIMIT, 1);
+ const end = Math.min(page + PAGINATION_UI_BUTTON_LIMIT, total);
+
+ for (let i = start; i <= end; i++) {
+ const isActive = i === page;
+ items.push({ title: i, active: isActive, page: true });
+ }
+
+ if (total - page > PAGINATION_UI_BUTTON_LIMIT) {
+ items.push({ title: SPREAD, separator: true, page: true });
+ }
+
+ if (page === total) {
+ items.push({ title: NEXT, disabled: true, next: true });
+ } else if (total - page >= 1) {
+ items.push({ title: NEXT, next: true });
+ }
+
+ if (total - page >= 1) items.push({ title: LAST, last: true });
+
+ return items;
+ },
+ },
+ template: `
+ <div class="gl-pagination">
+ <ul class="pagination clearfix">
+ <li v-for='item in getItems'
+ :class='{
+ page: item.page,
+ prev: item.prev,
+ next: item.next,
+ separator: item.separator,
+ active: item.active,
+ disabled: item.disabled
+ }'
+ >
+ <a @click="changePage($event)">{{item.title}}</a>
+ </li>
+ </ul>
+ </div>
+ `,
+ });
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/vue_shared/vue_resource_interceptor.js.es6 b/app/assets/javascripts/vue_shared/vue_resource_interceptor.js.es6
new file mode 100644
index 00000000000..d3229f9f730
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/vue_resource_interceptor.js.es6
@@ -0,0 +1,23 @@
+/* eslint-disable func-names, prefer-arrow-callback, no-unused-vars,
+no-param-reassign, no-plusplus */
+/* global Vue */
+
+Vue.http.interceptors.push((request, next) => {
+ Vue.activeResources = Vue.activeResources ? Vue.activeResources + 1 : 1;
+
+ next((response) => {
+ if (typeof response.data === 'string') {
+ response.data = JSON.parse(response.data);
+ }
+
+ Vue.activeResources--;
+ });
+});
+
+Vue.http.interceptors.push((request, next) => {
+ // needed in order to not break the tests.
+ if ($.rails) {
+ request.headers['X-CSRF-Token'] = $.rails.csrfToken();
+ }
+ next();
+});
diff --git a/app/assets/javascripts/wikis.js.es6 b/app/assets/javascripts/wikis.js.es6
index ecff5fd5bf4..ef99b2e92f0 100644
--- a/app/assets/javascripts/wikis.js.es6
+++ b/app/assets/javascripts/wikis.js.es6
@@ -1,9 +1,9 @@
/* eslint-disable no-param-reassign */
/* global Breakpoints */
-/*= require latinise */
-/*= require breakpoints */
-/*= require jquery.nicescroll */
+require('vendor/latinise');
+require('./breakpoints');
+require('vendor/jquery.nicescroll');
((global) => {
const dasherize = str => str.replace(/[_\s]+/g, '-');
diff --git a/app/assets/javascripts/zen_mode.js b/app/assets/javascripts/zen_mode.js
index e09b59dd5aa..d9261cda1b1 100644
--- a/app/assets/javascripts/zen_mode.js
+++ b/app/assets/javascripts/zen_mode.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, no-unused-vars, consistent-return, camelcase, comma-dangle, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, no-unused-vars, consistent-return, camelcase, comma-dangle, max-len */
/* global Dropzone */
/* global Mousetrap */
@@ -6,11 +6,11 @@
//
/*= provides zen_mode:enter */
/*= provides zen_mode:leave */
-//
-/*= require jquery.scrollTo */
-/*= require dropzone */
-/*= require mousetrap */
-/*= require mousetrap/pause */
+
+require('vendor/jquery.scrollTo');
+window.Dropzone = require('dropzone');
+require('mousetrap');
+require('mousetrap/plugins/pause/mousetrap-pause');
//
// ### Events
@@ -93,7 +93,5 @@
};
return ZenMode;
-
})();
-
}).call(this);
diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss
index 40bc0579393..08f203a1bf6 100644
--- a/app/assets/stylesheets/framework.scss
+++ b/app/assets/stylesheets/framework.scss
@@ -9,6 +9,7 @@
@import "framework/asciidoctor.scss";
@import "framework/blocks.scss";
@import "framework/buttons.scss";
+@import "framework/badges.scss";
@import "framework/calendar.scss";
@import "framework/callout.scss";
@import "framework/common.scss";
@@ -30,7 +31,6 @@
@import "framework/modal.scss";
@import "framework/nav.scss";
@import "framework/pagination.scss";
-@import "framework/progress.scss";
@import "framework/panels.scss";
@import "framework/selects.scss";
@import "framework/sidebar.scss";
diff --git a/app/assets/stylesheets/framework/animations.scss b/app/assets/stylesheets/framework/animations.scss
index f1d36efb3de..0a26b4c6a8c 100644
--- a/app/assets/stylesheets/framework/animations.scss
+++ b/app/assets/stylesheets/framework/animations.scss
@@ -50,3 +50,100 @@
.pulse {
@include webkit-prefix(animation-name, pulse);
}
+
+/*
+* General hover animations
+*/
+
+
+// Sass multiple transitions mixin | https://gist.github.com/tobiasahlin/7a421fb9306a4f518aab
+// Usage: @include transition(width, height 0.3s ease-in-out);
+// Output: -webkit-transition(width 0.2s, height 0.3s ease-in-out);
+// transition(width 0.2s, height 0.3s ease-in-out);
+//
+// Pass in any number of transitions
+@mixin transition($transitions...) {
+ $unfoldedTransitions: ();
+ @each $transition in $transitions {
+ $unfoldedTransitions: append($unfoldedTransitions, unfoldTransition($transition), comma);
+ }
+
+ transition: $unfoldedTransitions;
+}
+
+@mixin disableAllAnimation {
+ /*CSS transitions*/
+ -o-transition-property: none !important;
+ -moz-transition-property: none !important;
+ -ms-transition-property: none !important;
+ -webkit-transition-property: none !important;
+ transition-property: none !important;
+ /*CSS transforms*/
+ -o-transform: none !important;
+ -moz-transform: none !important;
+ -ms-transform: none !important;
+ -webkit-transform: none !important;
+ transform: none !important;
+ /*CSS animations*/
+ -webkit-animation: none !important;
+ -moz-animation: none !important;
+ -o-animation: none !important;
+ -ms-animation: none !important;
+ animation: none !important;
+}
+
+@function unfoldTransition ($transition) {
+ // Default values
+ $property: all;
+ $duration: $general-hover-transition-duration;
+ $easing: $general-hover-transition-curve; // Browser default is ease, which is what we want
+ $delay: null; // Browser default is 0, which is what we want
+ $defaultProperties: ($property, $duration, $easing, $delay);
+
+ // Grab transition properties if they exist
+ $unfoldedTransition: ();
+ @for $i from 1 through length($defaultProperties) {
+ $p: null;
+ @if $i <= length($transition) {
+ $p: nth($transition, $i);
+ } @else {
+ $p: nth($defaultProperties, $i);
+ }
+ $unfoldedTransition: append($unfoldedTransition, $p);
+ }
+
+ @return $unfoldedTransition;
+}
+
+.btn,
+.side-nav-toggle {
+ @include transition(background-color, border-color, color, box-shadow);
+}
+
+.dropdown-menu-toggle,
+.avatar-circle,
+.header-user-avatar {
+ @include transition(border-color);
+}
+
+.note-action-button .link-highlight,
+.toolbar-btn,
+.dropdown-toggle-caret,
+.fa:not(.fa-bell) {
+ @include transition(color);
+}
+
+a {
+ @include transition(background-color, color, border);
+}
+
+.stage-nav-item {
+ @include transition(background-color, box-shadow);
+}
+
+.nav-sidebar a,
+.dropdown-menu a,
+.dropdown-menu button,
+.dropdown-menu-nav a {
+ transition: none;
+}
diff --git a/app/assets/stylesheets/framework/avatar.scss b/app/assets/stylesheets/framework/avatar.scss
index 48827578d94..1d59700543c 100644
--- a/app/assets/stylesheets/framework/avatar.scss
+++ b/app/assets/stylesheets/framework/avatar.scss
@@ -37,6 +37,8 @@
display: inline-block;
margin-left: 4px;
margin-bottom: 2px;
+ flex-shrink: 0;
+ -webkit-flex-shrink: 0;
&.s16 { margin-right: 4px; }
&.s24 { margin-right: 4px; }
@@ -52,6 +54,10 @@
border-radius: 0;
border: none;
}
+
+ &:not([href]):hover {
+ border-color: rgba($avatar-border, .2);
+ }
}
.identicon {
diff --git a/app/assets/stylesheets/framework/awards.scss b/app/assets/stylesheets/framework/awards.scss
index 9fc9bcebc44..49907417e26 100644
--- a/app/assets/stylesheets/framework/awards.scss
+++ b/app/assets/stylesheets/framework/awards.scss
@@ -97,8 +97,20 @@
padding: 5px 6px;
outline: 0;
- &:hover,
+ &.disabled {
+ cursor: default;
+
+ &:hover,
+ &:focus,
+ &:active {
+ background-color: $white-light;
+ border-color: $border-color;
+ box-shadow: none;
+ }
+ }
+
&.active,
+ &:hover,
&:active {
background-color: $row-hover;
border-color: $row-hover-border;
diff --git a/app/assets/stylesheets/framework/badges.scss b/app/assets/stylesheets/framework/badges.scss
new file mode 100644
index 00000000000..47a8f44c709
--- /dev/null
+++ b/app/assets/stylesheets/framework/badges.scss
@@ -0,0 +1,6 @@
+.badge {
+ font-weight: normal;
+ background-color: $badge-bg;
+ color: $badge-color;
+ vertical-align: baseline;
+}
diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss
index 9f02749f5ab..0f9213b98e3 100644
--- a/app/assets/stylesheets/framework/blocks.scss
+++ b/app/assets/stylesheets/framework/blocks.scss
@@ -1,15 +1,15 @@
.centered-light-block {
text-align: center;
- color: $gl-gray;
+ color: $gl-text-color;
margin: 20px;
}
.nothing-here-block {
text-align: center;
padding: 20px;
- color: $gl-gray;
+ color: $gl-text-color;
font-weight: normal;
- font-size: 16px;
+ font-size: 14px;
line-height: 36px;
&.diff-collapsed {
@@ -29,7 +29,7 @@
margin-bottom: 0;
border-top: 1px solid $white-dark;
border-bottom: 1px solid $white-dark;
- color: $gl-gray;
+ color: $gl-text-color;
&.oneline-block {
line-height: 42px;
@@ -82,7 +82,12 @@
}
.block-controls {
- float: right;
+ display: -webkit-flex;
+ display: flex;
+ -webkit-justify-content: flex-end;
+ justify-content: flex-end;
+ -webkit-flex: 1;
+ flex: 1;
.control {
float: left;
@@ -135,11 +140,11 @@
}
.cover-title {
- color: $gl-header-color;
+ color: $gl-text-color;
font-size: 23px;
h1 {
- color: $gl-gray-dark;
+ color: $gl-text-color;
margin-bottom: 6px;
font-size: 23px;
}
@@ -153,7 +158,7 @@
p {
padding: 0 $gl-padding;
- color: $gl-text-color-dark;
+ color: $gl-text-color;
}
}
@@ -211,7 +216,7 @@
display: inline;
font-weight: normal;
font-size: 24px;
- color: $gl-title-color;
+ color: $gl-text-color;
}
}
}
@@ -273,6 +278,10 @@
display: inline-block;
}
+ .btn {
+ margin: $btn-side-margin $btn-side-margin 0 0;
+ }
+
@media(max-width: $screen-xs-max) {
margin-top: 50px;
text-align: center;
@@ -281,4 +290,15 @@
width: 100%;
}
}
+
+ @media(min-width: $screen-xs-max) {
+ &.labels .text-content {
+ margin-top: 70px;
+ }
+ }
+}
+
+.flex-container-block {
+ display: -webkit-flex;
+ display: flex;
}
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index a11f1cd7735..cda46223492 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -88,7 +88,7 @@
}
@mixin btn-gray {
- @include btn-color($gray-light, $border-gray-normal, $gray-normal, $border-gray-normal, $gray-dark, $border-gray-dark, $gl-gray-dark);
+ @include btn-color($gray-light, $border-gray-normal, $gray-normal, $border-gray-normal, $gray-dark, $border-gray-dark, $gl-text-color);
}
@mixin btn-white {
@@ -242,7 +242,7 @@
}
.btn-transparent {
- color: $gl-gray-light;
+ color: $gl-text-color-secondary;
background-color: transparent;
border: 0;
@@ -324,21 +324,17 @@
&:focus {
cursor: text;
box-shadow: none;
- border-color: $border-color;
+ border-color: lighten($dropdown-input-focus-border, 20%);
color: $gray-darkest;
background-color: $gray-light;
}
}
-.btn-file-option {
- background: linear-gradient(180deg, $white-light 25%, $gray-light 100%);
-}
-
.btn-build {
margin-left: 10px;
i {
- color: $gl-gray-light;
+ color: $gl-text-color-secondary;
}
}
diff --git a/app/assets/stylesheets/framework/calendar.scss b/app/assets/stylesheets/framework/calendar.scss
index ef921a8c6a9..1d2d1bfc0d7 100644
--- a/app/assets/stylesheets/framework/calendar.scss
+++ b/app/assets/stylesheets/framework/calendar.scss
@@ -1,6 +1,7 @@
.calender-block {
padding-left: 0;
padding-right: 0;
+ direction: rtl;
@media (min-width: $screen-sm-min) and (max-width: $screen-md-max) {
overflow-x: scroll;
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index 251e43d2edd..a4b38723bbd 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -26,6 +26,7 @@
.append-bottom-default { margin-bottom: $gl-padding; }
.inline { display: inline-block; }
.center { text-align: center; }
+.vertical-align-middle { vertical-align: middle; }
.underlined-link { text-decoration: underline; }
.hint { font-style: italic; color: $hint-color; }
@@ -57,16 +58,33 @@ pre {
border-radius: 0;
color: $well-pre-color;
}
+
+ &.wrap {
+ word-break: break-word;
+ white-space: pre-wrap;
+ }
}
hr {
margin: $gl-padding 0;
+ border-top: 1px solid darken($gray-normal, 8%);
}
.str-truncated {
@include str-truncated;
}
+.block-truncated {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+
+ > div,
+ .str-truncated {
+ display: inline;
+ }
+}
+
.item-title { font-weight: 600; }
/** FLASH message **/
@@ -235,6 +253,8 @@ li.note {
.progress {
margin-bottom: 0;
margin-top: 4px;
+ box-shadow: none;
+ background-color: $border-gray-light;
}
}
@@ -394,7 +414,7 @@ table {
padding: 0 10px;
clip: auto;
text-decoration: none;
- color: $gl-title-color;
+ color: $gl-text-color;
background: $gray-light;
z-index: 1;
}
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index 889366d6ddf..ca5861bf3e6 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -125,7 +125,8 @@
top: 100%;
left: 0;
z-index: 9;
- width: 240px;
+ max-width: 280px;
+ min-width: 240px;
margin-top: 2px;
margin-bottom: 0;
font-size: 14px;
@@ -201,7 +202,7 @@
}
.icon-play {
- fill: $gl-gray-light;
+ fill: $gl-text-color-secondary;
margin-right: 6px;
height: 12px;
width: 11px;
@@ -209,7 +210,7 @@
}
.dropdown-header {
- color: $gl-gray-light;
+ color: $gl-text-color-secondary;
font-size: 13px;
line-height: 22px;
padding: 0 10px;
@@ -222,10 +223,15 @@
.unclickable {
cursor: not-allowed;
padding: 5px 8px;
- color: $gl-gray-light;
+ color: $gl-text-color-secondary;
}
}
+.dropdown-menu-drop-up {
+ top: auto;
+ bottom: 100%;
+}
+
.dropdown-menu-large {
width: 340px;
}
@@ -592,7 +598,7 @@
}
.ui-datepicker-title {
- color: $gl-gray;
+ color: $gl-text-color;
font-size: 14px;
line-height: 1;
font-weight: normal;
@@ -614,17 +620,17 @@
.dropdown-menu-inner-title {
display: block;
- color: $gl-title-color;
+ color: $gl-text-color;
font-weight: 600;
}
.dropdown-menu-inner-content {
display: block;
- color: $gl-gray-light;
+ color: $gl-text-color-secondary;
}
.dropdown-toggle-text {
&.is-default {
- color: $gl-gray-light;
+ color: $gl-text-color-secondary;
}
}
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index 88ed0a4a17e..c51912b4ac4 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -182,3 +182,52 @@ span.idiff {
border-bottom-right-radius: 2px;
}
}
+
+.file-stats {
+ ul {
+ list-style: none;
+ margin: 0;
+ padding: 10px 0;
+
+ li {
+ padding: 3px 0;
+ line-height: 20px;
+ }
+ }
+
+ .new-file {
+ a {
+ color: $gl-text-green;
+ }
+ }
+
+ .renamed-file {
+ a {
+ color: $gl-text-orange;
+ }
+ }
+
+ .deleted-file {
+ a {
+ color: $gl-text-red;
+ }
+ }
+
+ .edit-file {
+ a {
+ color: $gl-text-color;
+ }
+ }
+
+ a {
+ text-decoration: none;
+
+ .new-file {
+ color: $notify-new-file;
+ }
+
+ .deleted-file {
+ color: $notify-deleted-file;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss
index 19827943385..e3da467a27c 100644
--- a/app/assets/stylesheets/framework/filters.scss
+++ b/app/assets/stylesheets/framework/filters.scss
@@ -23,3 +23,129 @@
}
}
+.filtered-search-container {
+ display: -webkit-flex;
+ display: flex;
+}
+
+.filtered-search-input-container {
+ display: -webkit-flex;
+ display: flex;
+ position: relative;
+ width: 100%;
+
+ .form-control {
+ padding-left: 25px;
+ padding-right: 25px;
+
+ &:focus ~ .fa-filter {
+ color: $common-gray-dark;
+ }
+ }
+
+ .fa-filter {
+ position: absolute;
+ top: 10px;
+ left: 10px;
+ color: $gray-darkest;
+ }
+
+ .fa-times {
+ right: 10px;
+ color: $gray-darkest;
+ }
+
+ .clear-search {
+ width: 35px;
+ background-color: transparent;
+ border: none;
+ position: absolute;
+ right: 0;
+ height: 100%;
+ outline: none;
+
+ &:hover .fa-times {
+ color: $common-gray-dark;
+ }
+ }
+}
+
+.dropdown-menu .filter-dropdown-item {
+ padding: 0;
+}
+
+.filter-dropdown {
+ max-height: 215px;
+ overflow: auto;
+}
+
+%filter-dropdown-item-btn-hover {
+ background-color: $dropdown-hover-color;
+ color: $white-light;
+ text-decoration: none;
+
+ .avatar {
+ border-color: $white-light;
+ }
+}
+
+.filter-dropdown-item {
+ .btn {
+ border: none;
+ width: 100%;
+ text-align: left;
+ padding: 8px 16px;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ border-radius: 0;
+
+ .fa {
+ width: 15px;
+ }
+
+ .dropdown-label-box {
+ border-color: $white-light;
+ border-style: solid;
+ border-width: 1px;
+ width: 17px;
+ height: 17px;
+ }
+
+ &:hover,
+ &:focus {
+ @extend %filter-dropdown-item-btn-hover;
+ }
+ }
+
+ .dropdown-light-content {
+ font-size: 14px;
+ font-weight: 400;
+ }
+
+ .dropdown-user {
+ display: -webkit-flex;
+ display: flex;
+ }
+
+ .dropdown-user-details {
+ display: -webkit-flex;
+ display: flex;
+ -webkit-flex-direction: column;
+ flex-direction: column;
+
+ &> span {
+ white-space: normal;
+ word-break: break-all;
+ }
+ }
+}
+
+.filter-dropdown-item.dropdown-active {
+ .btn {
+ @extend %filter-dropdown-item-btn-hover;
+ }
+}
+
+.filter-dropdown-loading {
+ padding: 8px 16px;
+}
diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss
index 8726a69867b..25d6fbe465a 100644
--- a/app/assets/stylesheets/framework/forms.scss
+++ b/app/assets/stylesheets/framework/forms.scss
@@ -153,7 +153,7 @@ label {
}
.form-control::-webkit-input-placeholder {
- color: $gl-gray-light;
+ color: $gl-text-color-secondary;
}
.input-group {
diff --git a/app/assets/stylesheets/framework/gitlab-theme.scss b/app/assets/stylesheets/framework/gitlab-theme.scss
index 5cd242af91d..d6566dc4ec9 100644
--- a/app/assets/stylesheets/framework/gitlab-theme.scss
+++ b/app/assets/stylesheets/framework/gitlab-theme.scss
@@ -80,29 +80,32 @@
}
}
+ .about-gitlab {
+ color: $color-light;
+ }
}
}
}
-$theme-charcoal: #3d454d;
-$theme-charcoal-light: #485157;
-$theme-charcoal-dark: #383f45;
-$theme-charcoal-text: #b9bbbe;
+$theme-charcoal-light: #b9bbbe;
+$theme-charcoal: #485157;
+$theme-charcoal-dark: #3d454d;
+$theme-charcoal-darker: #383f45;
$theme-blue-light: #becde9;
$theme-blue: #2980b9;
$theme-blue-dark: #1970a9;
$theme-blue-darker: #096099;
-$theme-graphite-lighter: #ccc;
-$theme-graphite-light: #777;
-$theme-graphite: #666;
-$theme-graphite-dark: #555;
+$theme-graphite-light: #ccc;
+$theme-graphite: #777;
+$theme-graphite-dark: #666;
+$theme-graphite-darker: #555;
-$theme-gray-light: #979797;
-$theme-gray: #373737;
-$theme-gray-dark: #272727;
-$theme-gray-darker: #222;
+$theme-black-light: #979797;
+$theme-black: #373737;
+$theme-black-dark: #272727;
+$theme-black-darker: #222;
$theme-green-light: #adc;
$theme-green: #019875;
@@ -120,15 +123,15 @@ body {
}
&.ui_charcoal {
- @include gitlab-theme($theme-charcoal-text, $theme-charcoal-light, $theme-charcoal, $theme-charcoal-dark);
+ @include gitlab-theme($theme-charcoal-light, $theme-charcoal, $theme-charcoal-dark, $theme-charcoal-darker);
}
&.ui_graphite {
- @include gitlab-theme($theme-graphite-lighter, $theme-graphite-light, $theme-graphite, $theme-graphite-dark);
+ @include gitlab-theme($theme-graphite-light, $theme-graphite, $theme-graphite-dark, $theme-graphite-darker);
}
- &.ui_gray {
- @include gitlab-theme($theme-gray-light, $theme-gray, $theme-gray-dark, $theme-gray-darker);
+ &.ui_black {
+ @include gitlab-theme($theme-black-light, $theme-black, $theme-black-dark, $theme-black-darker);
}
&.ui_green {
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index 971940773f7..2a01bc4d44d 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -45,7 +45,7 @@ header {
padding: 0;
.nav > li > a {
- color: $gl-gray-light;
+ color: $gl-text-color-secondary;
font-size: 18px;
padding: 0;
margin: ($header-height - 28) / 2 0;
@@ -57,13 +57,21 @@ header {
&.header-user-dropdown-toggle {
margin-left: 14px;
+
+ &:hover,
+ &:focus,
+ &:active {
+ .header-user-avatar {
+ border-color: rgba($avatar-border, .2);
+ }
+ }
}
&:hover,
&:focus,
&:active {
background-color: $gray-light;
- color: darken($gl-gray-light, 30%);
+ color: $gl-text-color;
.todos-pending-count {
background: darken($todo-alert-blue, 10%);
@@ -88,7 +96,7 @@ header {
}
&.active {
- color: $gl-gray-light;
+ color: $gl-text-color-secondary;
}
}
}
@@ -104,6 +112,7 @@ header {
&:hover {
background-color: $white-normal;
+ color: $gl-header-nav-hover-color;
}
}
}
@@ -180,6 +189,7 @@ header {
&:hover {
text-decoration: underline;
+ color: $gl-header-nav-hover-color;
}
}
@@ -198,7 +208,7 @@ header {
cursor: pointer;
&:hover {
- color: darken($color: $gl-text-color, $amount: 30%);
+ color: $gl-header-nav-hover-color;
}
}
@@ -271,4 +281,5 @@ header {
float: left;
margin-right: 5px;
border-radius: 50%;
+ border: 1px solid $avatar-border;
}
diff --git a/app/assets/stylesheets/framework/icons.scss b/app/assets/stylesheets/framework/icons.scss
index 8624a25c052..db8d231a82a 100644
--- a/app/assets/stylesheets/framework/icons.scss
+++ b/app/assets/stylesheets/framework/icons.scss
@@ -15,6 +15,7 @@
}
.ci-status-icon-pending,
+.ci-status-icon-failed_with_warnings,
.ci-status-icon-success_with_warnings {
color: $gl-warning;
@@ -34,10 +35,10 @@
.ci-status-icon-canceled,
.ci-status-icon-disabled,
.ci-status-icon-not-found {
- color: $gl-gray;
+ color: $gl-text-color;
svg {
- fill: $gl-gray;
+ fill: $gl-text-color;
}
}
@@ -57,3 +58,9 @@
fill: $gl-text-color;
}
}
+
+.icon-link {
+ &:hover {
+ text-decoration: none;
+ }
+}
diff --git a/app/assets/stylesheets/framework/issue_box.scss b/app/assets/stylesheets/framework/issue_box.scss
index 298913108ee..46632f15f35 100644
--- a/app/assets/stylesheets/framework/issue_box.scss
+++ b/app/assets/stylesheets/framework/issue_box.scss
@@ -41,6 +41,6 @@
}
&.status-box-upcoming {
- background: $gl-gray-light;
+ background: $gl-text-color-secondary;
}
}
diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss
index 59fae61a44f..29d55c44699 100644
--- a/app/assets/stylesheets/framework/layout.scss
+++ b/app/assets/stylesheets/framework/layout.scss
@@ -33,10 +33,27 @@ body {
}
.alert-wrapper {
- margin-bottom: $gl-padding;
-
.alert {
margin-bottom: 0;
+
+ &:last-child {
+ margin-bottom: $gl-padding;
+ }
+ }
+
+ .alert-link-group {
+ float: right;
+ }
+
+ /* Center alert text and alert action links on smaller screens */
+ @media (max-width: $screen-sm-max) {
+ .alert {
+ text-align: center;
+ }
+
+ .alert-link-group {
+ float: none;
+ }
}
/* Stripe the background colors so that adjacent alert-warnings are distinct from one another */
diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss
index e96cd671e34..2bfdb9f9601 100644
--- a/app/assets/stylesheets/framework/lists.scss
+++ b/app/assets/stylesheets/framework/lists.scss
@@ -128,7 +128,7 @@ ul.content-list {
}
a {
- color: $gl-dark-link-color;
+ color: $gl-text-color;
}
.member-group-link {
@@ -155,7 +155,8 @@ ul.content-list {
}
> .btn,
- > .btn-group {
+ > .btn-group,
+ > .dropdown.inline {
margin-right: $gl-padding-top;
display: inline-block;
margin-top: 3px;
@@ -163,6 +164,10 @@ ul.content-list {
&:last-child {
margin-right: 0;
+
+ @media(max-width: $screen-xs-max) {
+ margin: 0 auto;
+ }
}
}
@@ -199,6 +204,7 @@ ul.content-list {
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
+ align-items: center;
white-space: nowrap;
}
@@ -208,6 +214,11 @@ ul.content-list {
padding-right: 8px;
}
+ .row-fixed-content {
+ flex: 0 0 auto;
+ margin-left: auto;
+ }
+
.row-title {
font-weight: 600;
}
@@ -224,22 +235,53 @@ ul.content-list {
}
.label-default {
- color: $gl-gray-light;
+ color: $gl-text-color-secondary;
}
}
-.panel > .content-list > li {
- padding: $gl-padding-top $gl-padding;
+// Table list
+.table-list {
+ display: table;
+ width: 100%;
+
+ .table-list-row {
+ display: table-row;
+ }
+
+ .table-list-cell {
+ display: table-cell;
+ vertical-align: top;
+ padding: 10px 16px;
+ border-bottom: 1px solid $gray-darker;
+
+ &.avatar-cell {
+ width: 36px;
+ padding-right: 0;
+
+ img {
+ margin-right: 0;
+ }
+ }
+ }
- &.commit {
- @media (min-width: $screen-sm-min) {
- padding-left: 46px + $gl-padding;
+ &.table-wide {
+ .table-list-cell {
+ &:last-of-type {
+ padding-right: 0;
+ }
+
+ &:first-of-type {
+ padding-left: 0;
+ }
}
}
}
+.panel > .content-list > li {
+ padding: $gl-padding-top $gl-padding;
+}
+
ul.controls {
- padding-top: 1px;
float: right;
list-style: none;
@@ -265,3 +307,7 @@ ul.controls {
}
}
}
+
+ul.indent-list {
+ padding: 10px 0 0 30px;
+}
diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss
index e30d81d09f0..d4758d90352 100644
--- a/app/assets/stylesheets/framework/markdown_area.scss
+++ b/app/assets/stylesheets/framework/markdown_area.scss
@@ -73,7 +73,7 @@
}
.referenced-users {
- color: $gl-header-color;
+ color: $gl-text-color;
padding-top: 10px;
}
@@ -135,7 +135,7 @@
.toolbar-btn {
float: left;
padding: 0 5px;
- color: $gl-gray-light;
+ color: $gl-text-color-secondary;
background: transparent;
border: 0;
outline: 0;
@@ -159,6 +159,7 @@
.cur {
.avatar {
border: 1px solid $white-light;
+ @include disableAllAnimation;
}
}
}
diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss
index 4f2ac77f228..1acd06122a3 100644
--- a/app/assets/stylesheets/framework/mixins.scss
+++ b/app/assets/stylesheets/framework/mixins.scss
@@ -46,7 +46,7 @@
&.light {
a {
- color: $gl-gray;
+ color: $gl-text-color;
}
}
}
diff --git a/app/assets/stylesheets/framework/mobile.scss b/app/assets/stylesheets/framework/mobile.scss
index 7eb9962ba33..8e2c56a8488 100644
--- a/app/assets/stylesheets/framework/mobile.scss
+++ b/app/assets/stylesheets/framework/mobile.scss
@@ -23,21 +23,21 @@
margin-right: 0;
}
- .issues-details-filters,
+ .issues-details-filters:not(.filtered-search-block),
.dash-projects-filters,
.check-all-holder {
display: none;
}
- .rss-btn {
+ .issues-holder .issue-check {
display: none;
}
- .project-home-links {
+ .rss-btn {
display: none;
}
- .project-avatar {
+ .project-home-links {
display: none;
}
diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss
index e4affbb1be1..fd081c2d7e1 100644
--- a/app/assets/stylesheets/framework/nav.scss
+++ b/app/assets/stylesheets/framework/nav.scss
@@ -51,7 +51,7 @@
margin-bottom: -1px;
font-size: 14px;
line-height: 28px;
- color: $gl-gray-light;
+ color: $gl-text-color-secondary;
border-bottom: 2px solid transparent;
&:hover,
@@ -76,13 +76,6 @@
color: $black;
}
}
-
- .badge {
- font-weight: normal;
- background-color: $nav-badge-bg;
- color: $gl-gray-light;
- vertical-align: baseline;
- }
}
&.sub-nav {
@@ -108,7 +101,7 @@
&:hover,
&:active,
&:focus {
- border-bottom: none;
+ border-color: transparent;
}
}
}
@@ -123,8 +116,8 @@
padding-top: 16px;
padding-bottom: 11px;
display: inline-block;
- width: 50%;
line-height: 28px;
+ white-space: normal;
/* Small devices (phones, tablets, 768px and lower) */
@media (max-width: $screen-xs-max) {
@@ -165,30 +158,24 @@
}
.nav-controls {
- width: 50%;
display: inline-block;
float: right;
text-align: right;
padding: 11px 0;
margin-bottom: 0;
- > .dropdown {
- margin-right: $gl-padding-top;
- display: inline-block;
- vertical-align: top;
-
- &:last-child {
- margin-right: 0;
- }
- }
-
- > .btn {
+ > .btn,
+ > .btn-container,
+ > .dropdown,
+ > input,
+ > form {
margin-right: $gl-padding-top;
display: inline-block;
vertical-align: top;
&:last-child {
margin-right: 0;
+ float: right;
}
}
@@ -196,19 +183,21 @@
float: none;
}
- > form {
- display: inline-block;
- }
-
.icon-label {
display: none;
}
- input {
+ .btn,
+ .dropdown,
+ .dropdown-toggle,
+ input,
+ form {
height: 35px;
+ }
+
+ input {
display: inline-block;
position: relative;
- margin-right: $gl-padding-top;
/* Medium devices (desktops, 992px and up) */
@media (min-width: $screen-md-min) { width: 200px; }
@@ -232,6 +221,7 @@
.btn,
form,
.dropdown,
+ .dropdown-toggle,
.dropdown-menu-toggle,
.form-control {
margin: 0 0 10px;
@@ -270,6 +260,10 @@
.nav-text,
.nav-controls {
width: auto;
+
+ @media (max-width: $screen-xs-max) {
+ width: 100%;
+ }
}
}
@@ -282,6 +276,10 @@
padding: 17px 0;
}
}
+
+ pre {
+ width: 100%;
+ }
}
.layout-nav {
@@ -296,16 +294,18 @@
.container-fluid {
position: relative;
+
+ .nav-control {
+ @media (max-width: $screen-sm-max) {
+ margin-right: 75px;
+ }
+ }
}
.controls {
float: right;
padding: 7px 0 0;
- @media (max-width: $screen-sm-max) {
- display: none;
- }
-
i {
color: $layout-link-gray;
}
@@ -317,7 +317,7 @@
.fa-caret-down {
margin-left: 5px;
- color: $gl-gray-light;
+ color: $gl-text-color-secondary;
}
.dropdown {
@@ -363,6 +363,7 @@
.fade-left {
@include fade(right, $gray-light);
left: -5px;
+ text-align: center;
.fa {
left: -7px;
@@ -435,3 +436,40 @@
}
}
}
+
+@media (max-width: $screen-xs-max) {
+ .top-area {
+ flex-flow: row wrap;
+
+ .nav-controls {
+ $controls-margin: $btn-xs-side-margin - 2px;
+ flex: 0 0 100%;
+
+ &.controls-flex {
+ display: flex;
+ flex-flow: row wrap;
+ align-items: center;
+ justify-content: center;
+ padding: 0 0 $gl-padding-top;
+ }
+
+ .controls-item,
+ .controls-item-full,
+ .controls-item:last-child {
+ flex: 1 1 35%;
+ display: block;
+ width: 100%;
+ margin: $controls-margin;
+
+ .btn,
+ .dropdown {
+ margin: 0;
+ }
+ }
+
+ .controls-item-full {
+ flex: 1 1 100%;
+ }
+ }
+ }
+}
diff --git a/app/assets/stylesheets/framework/page-header.scss b/app/assets/stylesheets/framework/page-header.scss
index 625bea96aaa..5f4211147f3 100644
--- a/app/assets/stylesheets/framework/page-header.scss
+++ b/app/assets/stylesheets/framework/page-header.scss
@@ -14,7 +14,7 @@
.header-action-buttons {
i {
- color: $gl-gray-light;
+ color: $gl-text-color-secondary;
font-size: 13px;
margin-right: 3px;
}
@@ -42,14 +42,10 @@
.commit-committer-link,
.commit-author-link {
- color: $gl-gray;
+ color: $gl-text-color;
font-weight: bold;
}
- .fa-clipboard {
- color: $dropdown-title-btn-color;
- }
-
.commit-info {
&.branches {
margin-left: 8px;
diff --git a/app/assets/stylesheets/framework/pagination.scss b/app/assets/stylesheets/framework/pagination.scss
index b37c1d0d670..c3ec9db0f07 100644
--- a/app/assets/stylesheets/framework/pagination.scss
+++ b/app/assets/stylesheets/framework/pagination.scss
@@ -6,8 +6,22 @@
.pagination {
padding: 0;
+
+ a {
+ cursor: pointer;
+ }
+
+ .separator,
+ .separator:hover {
+ a {
+ cursor: default;
+ background-color: $gray-light;
+ padding: $gl-vert-padding;
+ }
+ }
}
+
.gap,
.gap:hover {
background-color: $gray-light;
diff --git a/app/assets/stylesheets/framework/panels.scss b/app/assets/stylesheets/framework/panels.scss
index 9d8d08dff88..efe93724013 100644
--- a/app/assets/stylesheets/framework/panels.scss
+++ b/app/assets/stylesheets/framework/panels.scss
@@ -48,3 +48,11 @@
line-height: inherit;
}
}
+
+.panel-default {
+ .table-list-row:last-child {
+ .table-list-cell {
+ border-bottom: 0;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/framework/progress.scss b/app/assets/stylesheets/framework/progress.scss
deleted file mode 100644
index e9800bd24b5..00000000000
--- a/app/assets/stylesheets/framework/progress.scss
+++ /dev/null
@@ -1,5 +0,0 @@
-html.turbolinks-progress-bar::before {
- background-color: $progress-color!important;
- height: 2px!important;
- box-shadow: 0 0 10px $progress-color, 0 0 5px $progress-color;
-}
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index 46a06cd7eab..f0b03710c79 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -101,6 +101,17 @@
padding: 0 8px;
border-radius: 6px;
}
+
+ .about-gitlab {
+ padding: 7px $gl-sidebar-padding;
+ font-size: $gl-font-size;
+ line-height: 24px;
+ display: block;
+ text-decoration: none;
+ font-weight: normal;
+ position: absolute;
+ bottom: 10px;
+ }
}
.sidebar-action-buttons {
@@ -172,7 +183,9 @@
&.right-sidebar-expanded {
.line-resolve-all-container {
- display: none;
+ @media (min-width: $sidebar-breakpoint) {
+ display: none;
+ }
}
}
}
@@ -223,9 +236,13 @@ header.header-sidebar-pinned {
@media (min-width: $screen-md-min) {
padding-right: $gutter_width;
- .merge-request-tabs-holder.affix {
+ &:not(.with-overlay) .merge-request-tabs-holder.affix {
right: $gutter_width;
}
+
+ &.with-overlay .merge-request-tabs-holder.affix {
+ right: $sidebar_collapsed_width;
+ }
}
&.with-overlay {
diff --git a/app/assets/stylesheets/framework/timeline.scss b/app/assets/stylesheets/framework/timeline.scss
index 6078505807e..ff185cd8767 100644
--- a/app/assets/stylesheets/framework/timeline.scss
+++ b/app/assets/stylesheets/framework/timeline.scss
@@ -7,7 +7,7 @@
.timeline-entry {
padding: $gl-padding $gl-btn-padding 11px;
border-color: $white-normal;
- color: $gl-gray;
+ color: $gl-text-color;
border-bottom: 1px solid $border-white-light;
&:target {
@@ -32,7 +32,7 @@
.system-note {
.note-text {
- color: $gl-gray !important;
+ color: $gl-text-color !important;
}
}
diff --git a/app/assets/stylesheets/framework/tw_bootstrap.scss b/app/assets/stylesheets/framework/tw_bootstrap.scss
index 718dbbfea27..ea2d26dd5a0 100644
--- a/app/assets/stylesheets/framework/tw_bootstrap.scss
+++ b/app/assets/stylesheets/framework/tw_bootstrap.scss
@@ -33,7 +33,7 @@
@import "bootstrap/labels";
@import "bootstrap/badges";
@import "bootstrap/alerts";
-// @import "bootstrap/progress-bars";
+@import "bootstrap/progress-bars";
@import "bootstrap/list-group";
@import "bootstrap/wells";
@import "bootstrap/close";
@@ -91,14 +91,14 @@
// Labels
.label {
padding: 4px 5px;
- font-size: 13px;
+ font-size: 12px;
font-style: normal;
font-weight: normal;
display: inline-block;
&.label-gray {
background-color: $label-gray-bg;
- color: $gl-gray;
+ color: $gl-text-color;
text-shadow: none;
}
@@ -162,6 +162,10 @@
}
}
}
+
+ &.panel-without-border {
+ border: 0;
+ }
}
.panel-succes .panel-heading,
diff --git a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss
index 876adf7f712..0fc89d5976a 100644
--- a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss
+++ b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss
@@ -65,11 +65,11 @@ $legend-color: $text-color;
//
//##
-$pagination-color: $gl-gray;
+$pagination-color: $gl-text-color;
$pagination-bg: $white-light;
$pagination-border: $border-color;
-$pagination-hover-color: $gl-gray;
+$pagination-hover-color: $gl-text-color;
$pagination-hover-bg: $row-hover;
$pagination-hover-border: $border-color;
@@ -121,6 +121,9 @@ $panel-default-heading-bg: $gray-light;
$panel-footer-bg: $gray-light;
$panel-inner-border: $border-color;
+$badge-bg: $badge-bg;
+$badge-color: $badge-color;
+
//== Wells
//
//##
@@ -154,7 +157,7 @@ $nav-link-padding: 13px $gl-padding;
//
//##
$pre-bg: $gray-light !default;
-$pre-color: $gl-gray !default;
+$pre-color: $gl-text-color !default;
$pre-border-color: $border-color;
$table-bg-accent: $gray-light;
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index d906d26bba9..54958973f15 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -1,5 +1,5 @@
@mixin md-typography {
- color: $md-text-color;
+ color: $gl-text-color;
word-wrap: break-word;
a {
@@ -10,7 +10,7 @@
max-width: 100%;
}
- *:first-child {
+ *:first-child:not(.katex-display) {
margin-top: 0;
}
@@ -50,14 +50,14 @@
margin: 16px 0 10px;
padding: 0 0 0.3em;
border-bottom: 1px solid $white-dark;
- color: $gl-gray-dark;
+ color: $gl-text-color;
}
h2 {
font-size: 1.5em;
font-weight: 600;
margin: 16px 0 10px;
- color: $gl-gray-dark;
+ color: $gl-text-color;
}
h3 {
@@ -100,7 +100,7 @@
}
p {
- color: $gl-text-color-dark;
+ color: $gl-text-color;
margin: 6px 0 0;
}
@@ -108,7 +108,7 @@
@extend .table;
@extend .table-bordered;
margin: 12px 0;
- color: $gl-text-color-dark;
+ color: $gl-text-color;
th {
background: $label-gray-bg;
@@ -230,7 +230,7 @@ h3,
h4,
h5,
h6 {
- color: $gl-title-color;
+ color: $gl-text-color;
font-weight: 600;
}
@@ -292,7 +292,7 @@ h2,
h3,
h4 {
small {
- color: $gl-gray;
+ color: $gl-text-color;
}
}
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 460c5d995be..7809d4866f1 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -5,7 +5,7 @@ $sidebar_collapsed_width: 62px;
$sidebar_width: 220px;
$gutter_collapsed_width: 62px;
$gutter_width: 290px;
-$gutter_inner_width: 258px;
+$gutter_inner_width: 250px;
$sidebar-transition-duration: .15s;
$sidebar-breakpoint: 1024px;
@@ -56,6 +56,7 @@ $black-transparent: rgba(0, 0, 0, 0.3);
$border-white-light: darken($white-light, $darken-border-factor);
$border-white-normal: darken($white-normal, $darken-border-factor);
+$border-gray-light: darken($gray-light, $darken-border-factor);
$border-gray-normal: darken($gray-normal, $darken-border-factor);
$border-gray-dark: darken($white-normal, $darken-border-factor);
@@ -85,6 +86,7 @@ $warning-message-border: #f0e2bb;
*/
$border-color: #e5e5e5;
$focus-border-color: #3aabf0;
+$sidebar-collapsed-icon-color: #999;
$well-expand-item: #e8f2f7;
$well-inner-border: #eef0f2;
$well-light-border: #f1f1f1;
@@ -94,29 +96,26 @@ $well-light-text-color: #5b6169;
* Text
*/
$gl-font-size: 14px;
-$gl-title-color: #333;
-$gl-text-color: #5c5c5c;
-$gl-text-color-dark: #5c5d5e;
-$gl-text-color-light: #8c8c8c;
+$gl-text-color: rgba(0, 0, 0, .85);
+$gl-text-color-secondary: rgba(0, 0, 0, .55);
+$gl-text-color-disabled: rgba(0, 0, 0, .35);
$gl-text-green: #4a2;
$gl-text-red: #d12f19;
$gl-text-orange: #d90;
$gl-link-color: #3777b0;
-$gl-diff-text-color: #555;
-$gl-dark-link-color: #333;
-$gl-gray-light: #8f8f8f;
$gl-grayish-blue: #7f8fa4;
$gl-gray: $gl-text-color;
$gl-gray-dark: #313236;
$gl-header-color: #4c4e54;
+$gl-header-nav-hover-color: #434343;
/*
* Lists
*/
$list-font-size: $gl-font-size;
-$list-title-color: $gl-title-color;
+$list-title-color: $gl-text-color;
$list-text-color: $gl-text-color;
-$list-text-disabled-color: #888;
+$list-text-disabled-color: $gl-text-color-disabled;
$list-border-light: #eee;
$list-border: rgba(0, 0, 0, 0.05);
$list-text-height: 42px;
@@ -127,15 +126,14 @@ $list-warning-row-color: #8a6d3b;
/*
* Markdown
*/
-$md-text-color: $gl-text-color;
$md-link-color: $gl-link-color;
$md-area-border: #ddd;
/*
* Code
*/
-$code_font_size: 13px;
-$code_line_height: 1.5;
+$code_font_size: 12px;
+$code_line_height: 1.6;
/*
* Padding
@@ -168,9 +166,7 @@ $btn-side-margin: 10px;
$btn-sm-side-margin: 7px;
$btn-xs-side-margin: 5px;
$issue-status-expired: #cea61b;
-$issuable-sidebar-color: #999;
-$issuable-avatar-hover-border: #999;
-$issuable-clipboard-color: #999;
+$issuable-sidebar-color: $gl-text-color-secondary;
$show-aside-bg: #eee;
$show-aside-color: #777;
$show-aside-shadow: #ddd;
@@ -182,6 +178,9 @@ $count-arrow-border: #dce0e5;
$save-project-loader-color: #555;
$divergence-graph-bar-bg: #ccc;
$divergence-graph-separator-bg: #ccc;
+$general-hover-transition-duration: 100ms;
+$general-hover-transition-curve: linear;
+
/*
* Common component specific colors
@@ -274,10 +273,22 @@ $dropdown-toggle-active-border-color: darken($border-color, 14%);
/*
+* Filtered Search
+*/
+$dropdown-hover-color: #3b86ff;
+
+/*
* Buttons
*/
$btn-active-gray: #ececec;
$btn-active-gray-light: e4e7ed;
+$btn-white-active: #848484;
+
+/*
+* Badges
+*/
+$badge-bg: #eee;
+$badge-color: $gl-text-color-secondary;
/*
* Award emoji
@@ -296,8 +307,8 @@ $location-icon-color: #e7e9ed;
/*
* Notes
*/
-$notes-light-color: #8e8e8e;
-$notes-role-color: #8e8e8e;
+$notes-light-color: $gl-text-color-secondary;
+$notes-role-color: $gl-text-color-secondary;
$note-disabled-comment-color: #b2b2b2;
$note-targe3-outside: #fffff0;
$note-targe3-inside: #ffffd3;
@@ -322,7 +333,7 @@ $calendar-user-contrib-text: #959494;
$cycle-analytics-box-padding: 30px;
$cycle-analytics-box-text-color: #8c8c8c;
$cycle-analytics-big-font: 19px;
-$cycle-analytics-dark-text: $gl-title-color;
+$cycle-analytics-dark-text: $gl-text-color;
$cycle-analytics-light-gray: #bfbfbf;
$cycle-analytics-dismiss-icon-color: #b2b2b2;
@@ -368,14 +379,13 @@ $callout-success-color: #3c763d;
/*
* Commit Page
*/
-$commit-committer-color: #999;
$commit-max-width-marker-color: rgba(0, 0, 0, 0.0);
$commit-message-text-area-bg: rgba(0, 0, 0, 0.0);
/*
* Common
*/
-$common-gray: $gl-gray;
+$common-gray: $gl-text-color;
$common-gray-light: #bbb;
$common-gray-dark: #444;
$common-red: $gl-text-red;
@@ -426,6 +436,7 @@ $help-shortcut-header-color: #333;
*/
$issues-today-bg: #f3fff2;
$issues-today-border: #e1e8d5;
+$compare-display-color: #888;
/*
* jQuery UI
@@ -481,9 +492,9 @@ $project-network-controls-color: #888;
*/
$runner-state-shared-bg: #32b186;
$runner-state-specific-bg: #3498db;
-$runner-status-online-color: green;
-$runner-status-offline-color: gray;
-$runner-status-paused-color: red;
+$runner-status-online-color: $green-normal;
+$runner-status-offline-color: $gray-darkest;
+$runner-status-paused-color: $red-normal;
/*
Stat Graph
@@ -530,3 +541,4 @@ Pipeline Graph
*/
$stage-hover-bg: #eaf3fc;
$stage-hover-border: #d1e7fc;
+$action-icon-color: #d6d6d6; \ No newline at end of file
diff --git a/app/assets/stylesheets/framework/wells.scss b/app/assets/stylesheets/framework/wells.scss
index f9c850ebdc8..32eb750180f 100644
--- a/app/assets/stylesheets/framework/wells.scss
+++ b/app/assets/stylesheets/framework/wells.scss
@@ -1,6 +1,6 @@
.info-well {
background: $gray-light;
- color: $gl-gray;
+ color: $gl-text-color;
border: 1px solid $border-color;
border-radius: $border-radius-default;
diff --git a/app/assets/stylesheets/framework/zen.scss b/app/assets/stylesheets/framework/zen.scss
index 84b639fabf5..97ade638db6 100644
--- a/app/assets/stylesheets/framework/zen.scss
+++ b/app/assets/stylesheets/framework/zen.scss
@@ -40,7 +40,7 @@
}
.zen-control-full {
- color: $gl-gray-light;
+ color: $gl-text-color-secondary;
&:hover {
color: $gl-link-color;
diff --git a/app/assets/stylesheets/highlight/dark.scss b/app/assets/stylesheets/highlight/dark.scss
index cb923166b25..6f2e746d4b0 100644
--- a/app/assets/stylesheets/highlight/dark.scss
+++ b/app/assets/stylesheets/highlight/dark.scss
@@ -13,6 +13,8 @@ $dark-main-bg: #1d1f21;
$dark-main-color: #1d1f21;
$dark-line-color: #c5c8c6;
$dark-line-num-color: rgba(255, 255, 255, 0.3);
+$dark-line-num-color-new: #627165;
+$dark-line-num-color-old: #806565;
$dark-diff-not-empty-bg: #557;
$dark-highlight-bg: #ffe792;
$dark-highlight-color: $black;
@@ -89,7 +91,6 @@ $dark-il: #de935f;
.diff-line-num,
.diff-line-num a {
- color: $dark-main-color;
color: $dark-line-num-color;
}
@@ -121,11 +122,21 @@ $dark-il: #de935f;
.diff-line-num.new,
.line_content.new {
@include diff_background($dark-new-bg, $dark-new-idiff, $dark-border);
+
+ &::before,
+ a {
+ color: $dark-line-num-color-new;
+ }
}
.diff-line-num.old,
.line_content.old {
@include diff_background($dark-old-bg, $dark-old-idiff, $dark-border);
+
+ &::before,
+ a {
+ color: $dark-line-num-color-old;
+ }
}
.line_content.match {
diff --git a/app/assets/stylesheets/highlight/monokai.scss b/app/assets/stylesheets/highlight/monokai.scss
index d8510baad8a..2144a5f7466 100644
--- a/app/assets/stylesheets/highlight/monokai.scss
+++ b/app/assets/stylesheets/highlight/monokai.scss
@@ -7,6 +7,8 @@ $monokai-bg: #272822;
$monokai-border: #555;
$monokai-text-color: #f8f8f2;
$monokai-line-num-color: rgba(255, 255, 255, 0.3);
+$monokai-line-num-color-new: #707565;
+$monokai-line-num-color-old: #7e736f;
$monokai-line-empty-bg: #49483e;
$monokai-line-empty-border: darken($monokai-line-empty-bg, 15%);
$monokai-diff-border: #808080;
@@ -120,11 +122,21 @@ $monokai-gi: #a6e22e;
.diff-line-num.new,
.line_content.new {
@include diff_background($monokai-new-bg, $monokai-new-idiff, $monokai-diff-border);
+
+ &::before,
+ a {
+ color: $monokai-line-num-color-new;
+ }
}
.diff-line-num.old,
.line_content.old {
@include diff_background($monokai-old-bg, $monokai-old-idiff, $monokai-diff-border);
+
+ &::before,
+ a {
+ color: $monokai-line-num-color-old;
+ }
}
.line_content.match {
diff --git a/app/assets/stylesheets/highlight/solarized_dark.scss b/app/assets/stylesheets/highlight/solarized_dark.scss
index 874aecb5e16..2cb1d18f12f 100644
--- a/app/assets/stylesheets/highlight/solarized_dark.scss
+++ b/app/assets/stylesheets/highlight/solarized_dark.scss
@@ -13,6 +13,8 @@ $solarized-dark-pre-color: #93a1a1;
$solarized-dark-pre-border: #113b46;
$solarized-dark-line-bg: #002b36;
$solarized-dark-line-color: rgba(255, 255, 255, 0.3);
+$solarized-dark-line-color-new: #5a766c;
+$solarized-dark-line-color-old: #7a6c71;
$solarized-dark-highlight: #094554;
$solarized-dark-hll-bg: #174652;
$solarized-dark-c: #586e75;
@@ -124,11 +126,21 @@ $solarized-dark-il: #2aa198;
.diff-line-num.new,
.line_content.new {
@include diff_background($solarized-dark-new-bg, $solarized-dark-new-idiff, $solarized-dark-border);
+
+ &::before,
+ a {
+ color: $solarized-dark-line-color-new;
+ }
}
.diff-line-num.old,
.line_content.old {
@include diff_background($solarized-dark-old-bg, $solarized-dark-old-idiff, $solarized-dark-border);
+
+ &::before,
+ a {
+ color: $solarized-dark-line-color-old;
+ }
}
.line_content.match {
diff --git a/app/assets/stylesheets/highlight/solarized_light.scss b/app/assets/stylesheets/highlight/solarized_light.scss
index 499a1c108b8..b72c4326730 100644
--- a/app/assets/stylesheets/highlight/solarized_light.scss
+++ b/app/assets/stylesheets/highlight/solarized_light.scss
@@ -13,6 +13,9 @@ $solarized-light-pre-bg: #002b36;
$solarized-light-pre-bg: #fdf6e3;
$solarized-light-pre-color: #586e75;
$solarized-light-line-bg: #fdf6e3;
+$solarized-light-line-color: rgba(0, 0, 0, 0.3);
+$solarized-light-line-color-new: #a1a080;
+$solarized-light-line-color-old: #ad9186;
$solarized-light-highlight: #eee8d5;
$solarized-light-hll-bg: #ddd8c5;
$solarized-light-c: #93a1a1;
@@ -98,7 +101,7 @@ $solarized-light-il: #2aa198;
.diff-line-num,
.diff-line-num a {
- color: $black-transparent;
+ color: $solarized-light-line-color;
}
// Code itself
@@ -130,11 +133,21 @@ $solarized-light-il: #2aa198;
.line_content.new {
@include diff_background($solarized-light-new-bg,
$solarized-light-new-idiff, $solarized-light-border);
+
+ &::before,
+ a {
+ color: $solarized-light-line-color-new;
+ }
}
.diff-line-num.old,
.line_content.old {
@include diff_background($solarized-light-old-bg, $solarized-light-old-idiff, $solarized-light-border);
+
+ &::before,
+ a {
+ color: $solarized-light-line-color-old;
+ }
}
.line_content.match {
diff --git a/app/assets/stylesheets/highlight/white.scss b/app/assets/stylesheets/highlight/white.scss
index 54a5664a874..398fbfd3b18 100644
--- a/app/assets/stylesheets/highlight/white.scss
+++ b/app/assets/stylesheets/highlight/white.scss
@@ -3,7 +3,7 @@
/*
* White Syntax Colors
*/
-$white-code-color: #333;
+$white-code-color: $gl-text-color;
$white-highlight: #fafe3d;
$white-pre-hll-bg: #f8eec7;
$white-hll-bg: #f8f8f8;
@@ -108,11 +108,19 @@ $white-gc-bg: #eaf2f5;
&.old {
background-color: $line-number-old;
border-color: $line-removed-dark;
+
+ a {
+ color: scale-color($line-number-old,$red: -30%, $green: -30%, $blue: -30%);
+ }
}
&.new {
background-color: $line-number-new;
border-color: $line-added-dark;
+
+ a {
+ color: scale-color($line-number-new,$red: -30%, $green: -30%, $blue: -30%);
+ }
}
&.hll:not(.empty-cell) {
@@ -125,6 +133,10 @@ $white-gc-bg: #eaf2f5;
&.old {
background-color: $line-removed;
+ &::before {
+ color: scale-color($line-number-old,$red: -30%, $green: -30%, $blue: -30%);
+ }
+
span.idiff {
background-color: $line-removed-dark;
}
@@ -133,6 +145,10 @@ $white-gc-bg: #eaf2f5;
&.new {
background-color: $line-added;
+ &::before {
+ color: scale-color($line-number-new,$red: -30%, $green: -30%, $blue: -30%);
+ }
+
span.idiff {
background-color: $line-added-dark;
}
diff --git a/app/assets/stylesheets/notify.scss b/app/assets/stylesheets/notify.scss
index ddc382362f7..a81e5eb5ebf 100644
--- a/app/assets/stylesheets/notify.scss
+++ b/app/assets/stylesheets/notify.scss
@@ -18,15 +18,3 @@ p.details {
pre.commit-message {
white-space: pre-wrap;
}
-
-.file-stats > a {
- text-decoration: none;
-
- > .new-file {
- color: $notify-new-file;
- }
-
- > .deleted-file {
- color: $notify-deleted-file;
- }
-}
diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss
index c735f104c20..b362cc758cc 100644
--- a/app/assets/stylesheets/pages/boards.scss
+++ b/app/assets/stylesheets/pages/boards.scss
@@ -31,7 +31,7 @@
.dropdown-content {
max-height: 150px;
- }
+ }
}
.issue-board-dropdown-content {
@@ -74,6 +74,7 @@
height: 475px; // Needed for PhantomJS
height: calc(100vh - 220px);
min-height: 475px;
+ transition: width .2s;
&.is-compact {
width: calc(100% - 290px);
@@ -109,6 +110,12 @@
&.has-border {
border-top: 3px solid;
+ margin-top: -1px;
+ margin-right: -1px;
+ margin-left: -1px;
+ padding-top: 1px;
+ padding-right: 1px;
+ padding-left: 1px;
.board-title {
padding-top: ($gl-padding - 3px);
@@ -243,7 +250,7 @@
}
.issue-boards-search {
- width: 290px;
+ width: 395px;
.form-control {
display: inline-block;
@@ -253,7 +260,7 @@
.board-list-count {
padding: 10px 0;
- color: $gl-gray-light;
+ color: $gl-text-color-secondary;
font-size: 13px;
> .fa {
@@ -332,3 +339,186 @@
}
}
}
+
+.right-sidebar.right-sidebar-expanded {
+ &.boards-sidebar-slide-enter-active,
+ &.boards-sidebar-slide-leave-active {
+ transition: width .2s,
+ padding .2s;
+ }
+
+ &.boards-sidebar-slide-enter,
+ &.boards-sidebar-slide-leave-active {
+ width: 0;
+ padding-left: 0;
+ padding-right: 0;
+ }
+}
+
+.add-issues-modal {
+ display: -webkit-flex;
+ display: flex;
+ position: fixed;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ background-color: rgba($black, .3);
+ z-index: 9999;
+}
+
+.add-issues-container {
+ display: -webkit-flex;
+ display: flex;
+ -webkit-flex-direction: column;
+ flex-direction: column;
+ width: 90vw;
+ height: 85vh;
+ max-width: 1100px;
+ min-height: 500px;
+ margin: auto;
+ padding: 25px 15px 0;
+ background-color: $white-light;
+ border-radius: $border-radius-default;
+ box-shadow: 0 2px 12px rgba($black, .5);
+
+ .empty-state {
+ display: -webkit-flex;
+ display: flex;
+ -webkit-flex: 1;
+ flex: 1;
+ margin-top: 0;
+
+ &.add-issues-empty-state-filter {
+ -webkit-flex-direction: column;
+ flex-direction: column;
+ -webkit-justify-content: center;
+ justify-content: center;
+ }
+
+ > .row {
+ width: 100%;
+ margin: auto 0;
+ }
+
+ .svg-content {
+ margin-top: -40px;
+ }
+ }
+}
+
+.add-issues-header {
+ margin: -25px -15px -5px;
+ border-top: 0;
+ border-bottom: 1px solid $border-color;
+ border-top-right-radius: $border-radius-default;
+ border-top-left-radius: $border-radius-default;
+
+ > h2 {
+ margin: 0;
+ font-size: 18px;
+ }
+}
+
+.add-issues-search {
+ display: -webkit-flex;
+ display: flex;
+
+ .form-control {
+ margin-left: auto;
+
+ @media (min-width: $screen-sm-min) {
+ max-width: 200px;
+ }
+ }
+}
+
+.add-issues-list-column {
+ width: 100%;
+
+ @media (min-width: $screen-sm-min) {
+ width: 50%;
+ }
+
+ @media (min-width: $screen-md-min) {
+ width: (100% / 3);
+ }
+}
+
+.add-issues-list {
+ display: -webkit-flex;
+ display: flex;
+ -webkit-flex: 1;
+ flex: 1;
+ padding-top: 3px;
+ margin-left: -$gl-vert-padding;
+ margin-right: -$gl-vert-padding;
+ overflow-y: scroll;
+
+ .card-parent {
+ padding: 0 5px 5px;
+ }
+
+ .card {
+ border: 1px solid $border-gray-dark;
+ box-shadow: 0 1px 2px rgba($issue-boards-card-shadow, .3);
+ cursor: pointer;
+ }
+}
+
+.add-issues-list-loading {
+ -webkit-align-self: center;
+ align-self: center;
+ width: 100%;
+ padding-left: $gl-vert-padding;
+ padding-right: $gl-vert-padding;
+ font-size: 35px;
+}
+
+.add-issues-footer {
+ margin: auto -15px 0;
+ padding-left: 15px;
+ padding-right: 15px;
+ border-bottom-right-radius: $border-radius-default;
+ border-bottom-left-radius: $border-radius-default;
+}
+
+.add-issues-footer-to-list {
+ padding-left: $gl-vert-padding;
+ padding-right: $gl-vert-padding;
+ line-height: 34px;
+}
+
+.issue-card-selected {
+ position: absolute;
+ right: -3px;
+ top: -3px;
+ width: 17px;
+ background-color: $blue-light;
+ color: $white-light;
+ border: 1px solid $border-blue-light;
+ font-size: 9px;
+ line-height: 15px;
+ border-radius: 50%;
+}
+
+.modal-filters {
+ display: flex;
+
+ > .dropdown {
+ display: none;
+ margin-right: 10px;
+
+ @media (min-width: $screen-sm-min) {
+ display: block;
+ }
+ }
+
+ .dropdown-menu-toggle {
+ width: 100px;
+
+ @media (min-width: $screen-md-min) {
+ width: 140px;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/pages/branches.scss b/app/assets/stylesheets/pages/branches.scss
new file mode 100644
index 00000000000..3e2fa8ca88d
--- /dev/null
+++ b/app/assets/stylesheets/pages/branches.scss
@@ -0,0 +1,55 @@
+.divergence-graph {
+ padding: 12px 12px 0 0;
+ float: right;
+
+ .graph-side {
+ position: relative;
+ width: 80px;
+ height: 22px;
+ padding: 5px 0 13px;
+ float: left;
+
+ .bar {
+ position: absolute;
+ height: 4px;
+ background-color: $divergence-graph-bar-bg;
+ }
+
+ .bar-behind {
+ right: 0;
+ border-radius: 3px 0 0 3px;
+ }
+
+ .bar-ahead {
+ left: 0;
+ border-radius: 0 3px 3px 0;
+ }
+
+ .count {
+ padding-top: 6px;
+ padding-bottom: 0;
+ font-size: 12px;
+ color: $gl-text-color;
+ display: block;
+ }
+
+ .count-behind {
+ padding-right: 4px;
+ text-align: right;
+ }
+
+ .count-ahead {
+ padding-left: 4px;
+ text-align: left;
+ }
+ }
+
+ .graph-separator {
+ position: relative;
+ width: 1px;
+ height: 18px;
+ margin: 5px 0 0;
+ float: left;
+ background-color: $divergence-graph-separator-bg;
+ }
+}
diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss
index 66f7e7f97c8..fd101d43b5b 100644
--- a/app/assets/stylesheets/pages/builds.scss
+++ b/app/assets/stylesheets/pages/builds.scss
@@ -1,3 +1,34 @@
+@keyframes fade-out-status {
+ 0%, 50% { opacity: 1; }
+ 100% { opacity: 0; }
+}
+
+@keyframes blinking-dots {
+ 0% {
+ background-color: rgba($white-light, 1);
+ box-shadow: 12px 0 0 0 rgba($white-light,0.2),
+ 24px 0 0 0 rgba($white-light,0.2);
+ }
+
+ 25% {
+ background-color: rgba($white-light, 0.4);
+ box-shadow: 12px 0 0 0 rgba($white-light,2),
+ 24px 0 0 0 rgba($white-light,0.2);
+ }
+
+ 75% {
+ background-color: rgba($white-light, 0.4);
+ box-shadow: 12px 0 0 0 rgba($white-light,0.2),
+ 24px 0 0 0 rgba($white-light,1);
+ }
+
+ 100% {
+ background-color: rgba($white-light, 1);
+ box-shadow: 12px 0 0 0 rgba($white-light,0.2),
+ 24px 0 0 0 rgba($white-light,0.2);
+ }
+}
+
.build-page {
pre.trace {
background: $builds-trace-bg;
@@ -14,45 +45,99 @@
}
}
- .scroll-controls {
- .scroll-step {
- width: 31px;
- margin: 0 0 0 auto;
+ .environment-information {
+ background-color: $gray-light;
+ border: 1px solid $border-color;
+ padding: 12px $gl-padding;
+ border-radius: $border-radius-default;
+
+ svg {
+ position: relative;
+ top: 1px;
+ margin-right: 5px;
+ }
+ }
+}
+
+.scroll-controls {
+ height: 100%;
+
+ .scroll-step {
+ width: 31px;
+ margin: 0 0 0 auto;
+ }
+
+ .scroll-link,
+ .autoscroll-container {
+ right: 25px;
+ z-index: 1;
+ }
+
+ .scroll-link {
+ position: fixed;
+ display: block;
+ margin-bottom: 10px;
+
+ &.scroll-top .gitlab-icon-scroll-up-hover,
+ &.scroll-top:hover .gitlab-icon-scroll-up,
+ &.scroll-bottom .gitlab-icon-scroll-down-hover,
+ &.scroll-bottom:hover .gitlab-icon-scroll-down {
+ display: none;
}
- &.affix-bottom {
- position: absolute;
- right: 25px;
+ &.scroll-top:hover .gitlab-icon-scroll-up-hover,
+ &.scroll-bottom:hover .gitlab-icon-scroll-down-hover {
+ display: inline-block;
}
- &.affix {
- right: 25px;
- bottom: 15px;
- z-index: 1;
+ &.scroll-top {
+ top: 110px;
}
- &.sidebar-expanded {
- right: #{$gutter_width + ($gl-padding * 2)};
+ &.scroll-bottom {
+ bottom: -2px;
}
+ }
- a {
- display: block;
- margin-bottom: 10px;
+ .autoscroll-container {
+ position: absolute;
+ }
+
+ &.sidebar-expanded {
+
+ .scroll-link,
+ .autoscroll-container {
+ right: ($gutter_width + ($gl-padding * 2));
}
}
+}
- .environment-information {
- background-color: $gray-light;
- border: 1px solid $border-color;
- padding: 12px $gl-padding;
- border-radius: $border-radius-default;
+.status-message {
+ display: inline-block;
+ color: $white-light;
- svg {
- position: relative;
- top: 1px;
- margin-right: 5px;
+ .status-icon {
+ display: inline-block;
+ width: 16px;
+ height: 33px;
+ }
+
+ .status-text {
+ float: left;
+ opacity: 0;
+ margin-right: 10px;
+ font-weight: normal;
+ line-height: 1.8;
+ transition: opacity 1s ease-out;
+
+ &.animate {
+ animation: fade-out-status 2s ease;
}
}
+
+ &:hover .status-text {
+ opacity: 1;
+ }
}
.build-header {
@@ -75,7 +160,7 @@
flex: 1;
a {
- color: $gl-gray;
+ color: $gl-text-color;
&:hover {
color: $gl-link-color;
@@ -109,6 +194,15 @@
.bash {
display: block;
}
+
+ .build-loader-animation {
+ position: relative;
+ width: 6px;
+ height: 6px;
+ margin: auto auto 12px 2px;
+ border-radius: 50%;
+ animation: blinking-dots 1s linear infinite;
+ }
}
.right-sidebar.build-sidebar {
@@ -248,6 +342,12 @@
}
}
+.build-sidebar {
+ .container-fluid.container-limited {
+ max-width: 100%;
+ }
+}
+
.build-detail-row {
margin-bottom: 5px;
@@ -257,7 +357,7 @@
}
.build-light-text {
- color: $gl-gray-light;
+ color: $gl-text-color-secondary;
}
.build-gutter-toggle {
diff --git a/app/assets/stylesheets/pages/ci_projects.scss b/app/assets/stylesheets/pages/ci_projects.scss
index d1cd1e5d848..90643832390 100644
--- a/app/assets/stylesheets/pages/ci_projects.scss
+++ b/app/assets/stylesheets/pages/ci_projects.scss
@@ -18,7 +18,7 @@
}
td {
- color: $gl-gray;
+ color: $gl-text-color;
vertical-align: middle !important;
a {
diff --git a/app/assets/stylesheets/pages/commit.scss b/app/assets/stylesheets/pages/commit.scss
deleted file mode 100644
index bf656d0e28e..00000000000
--- a/app/assets/stylesheets/pages/commit.scss
+++ /dev/null
@@ -1,132 +0,0 @@
-.commit-title {
- display: block;
-}
-
-.commit-author,
-.commit-committer {
- display: block;
- color: $commit-committer-color;
- font-weight: normal;
- font-style: italic;
-}
-
-.commit-author strong,
-.commit-committer strong {
- font-weight: bold;
- font-style: normal;
-}
-
-.commit-description {
- background: none;
- border: none;
- margin: 0;
- padding: 0;
- margin-top: 10px;
- word-break: normal;
- white-space: pre-wrap;
-}
-
-.js-details-expand {
- &:hover {
- text-decoration: none;
- }
-}
-
-.ci-status-link {
- svg {
- overflow: visible;
- }
-}
-
-.commit-box {
- border-top: 1px solid $border-color;
- padding: $gl-padding 0;
-
- .commit-title {
- margin: 0;
- font-size: 23px;
- color: $gl-gray-dark;
- }
-
- .commit-description {
- margin-top: 15px;
- }
-}
-
-.commit-hash-full {
- @media (max-width: $screen-sm-max) {
- width: 80px;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- display: inline-block;
- vertical-align: bottom;
- }
-}
-
-.file-stats {
- ul {
- list-style: none;
- margin: 0;
- padding: 10px 0;
-
- li {
- padding: 3px 0;
- line-height: 20px;
- }
- }
-
- .new-file {
- a {
- color: $gl-text-green;
- }
- }
-
- .renamed-file {
- a {
- color: $gl-text-orange;
- }
- }
-
- .deleted-file {
- a {
- color: $gl-text-red;
- }
- }
-
- .edit-file {
- a {
- color: $gl-text-color;
- }
- }
-}
-
-/*
- * Commit message textarea for web editor and
- * custom merge request message
- */
-.commit-message-container {
- background-color: $body-bg;
- position: relative;
- font-family: $monospace_font;
- $left: 12px;
- overflow: hidden; // See https://gitlab.com/gitlab-org/gitlab-ce/issues/13987
- .max-width-marker {
- width: 72ch;
- color: $commit-max-width-marker-color;
- font-family: inherit;
- left: $left;
- height: 100%;
- border-right: 1px solid mix($input-border, $white-light);
- position: absolute;
- z-index: 1;
- }
-
- > textarea {
- background-color: $commit-message-text-area-bg;
- font-family: inherit;
- padding-left: $left;
- position: relative;
- z-index: 2;
- }
-}
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index e76e1a73b25..c3d45d708c1 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -1,6 +1,75 @@
+.commit-description {
+ background: none;
+ border: none;
+ padding: 0;
+ margin-top: 10px;
+ word-break: normal;
+ white-space: pre-wrap;
+}
+
+.js-details-expand {
+ &:hover {
+ text-decoration: none;
+ }
+}
+
+.commit-box {
+ border-top: 1px solid $border-color;
+ padding: $gl-padding 0;
+
+ .commit-title {
+ margin: 0;
+ color: $gl-text-color;
+ }
+
+ .commit-description {
+ margin-top: 15px;
+ }
+}
+
+.commit-hash-full {
+ @media (max-width: $screen-sm-max) {
+ width: 80px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ display: inline-block;
+ vertical-align: bottom;
+ }
+}
+
+/*
+ * Commit message textarea for web editor and
+ * custom merge request message
+ */
+.commit-message-container {
+ background-color: $body-bg;
+ position: relative;
+ font-family: $monospace_font;
+ $left: 12px;
+ overflow: hidden; // See https://gitlab.com/gitlab-org/gitlab-ce/issues/13987
+ .max-width-marker {
+ width: 72ch;
+ color: $commit-max-width-marker-color;
+ font-family: inherit;
+ left: $left;
+ height: 100%;
+ border-right: 1px solid mix($input-border, $white-light);
+ position: absolute;
+ z-index: 1;
+ }
+
+ textarea {
+ background-color: $commit-message-text-area-bg;
+ font-family: inherit;
+ padding-left: $left;
+ position: relative;
+ z-index: 2;
+ }
+}
+
+
.commits-compare-switch {
- @include btn-default;
- @include btn-white;
float: left;
margin-right: 9px;
}
@@ -8,7 +77,6 @@
.commit-header {
padding: 5px 10px;
background-color: $gray-light;
- border-top: 1px solid $gray-darker;
border-bottom: 1px solid $gray-darker;
font-size: 14px;
@@ -18,8 +86,6 @@
}
.commit-row-title {
- line-height: 1.35;
-
.notes_count {
float: right;
margin-right: 10px;
@@ -30,15 +96,14 @@
}
.commit-row-message {
- color: $gl-dark-link-color;
+ color: $gl-text-color;
}
-
}
.text-expander {
display: inline-block;
background: $gray-light;
- color: $gl-gray-light;
+ color: $gl-text-color-secondary;
padding: 0 5px;
cursor: pointer;
border: 1px solid $border-gray-dark;
@@ -54,9 +119,8 @@
.commit-actions {
@media (min-width: $screen-sm-min) {
- float: right;
- margin-left: $gl-padding;
- margin-top: 2px;
+ width: 300px;
+ text-align: right;
font-size: 0;
}
@@ -86,38 +150,15 @@
.commit,
.generic_commit_status {
- padding: 10px 0;
- position: relative;
-
- @media (min-width: $screen-sm-min) {
- padding-left: 46px;
- }
-
- &:not(:last-child) {
- border-bottom: 1px solid $gray-darker;
- }
a,
button {
- color: $gl-dark-link-color;
+ color: $gl-text-color;
vertical-align: baseline;
}
- .avatar {
- margin-left: -46px;
- }
-
- .item-title {
- display: inline-block;
-
- @media (min-width: $screen-sm-min) {
- max-width: 70%;
- }
- }
-
.commit-row-description {
font-size: 14px;
- border-left: 1px solid $white-normal;
padding: 10px 15px;
margin: 10px 0;
background: $gray-light;
@@ -134,20 +175,7 @@
}
a {
- color: $gl-dark-link-color;
- }
- }
-
- .commit-row-info {
- color: $gl-gray;
- line-height: 1.35;
-
- a {
- color: $gl-gray;
- }
-
- .avatar {
- margin-right: 8px;
+ color: $gl-text-color;
}
}
@@ -164,7 +192,7 @@
}
.branch-commit {
- color: $gl-gray;
+ color: $gl-text-color;
.commit-icon {
text-align: center;
@@ -174,7 +202,7 @@
height: 14px;
width: 14px;
vertical-align: middle;
- fill: $gl-gray-light;
+ fill: $gl-text-color-secondary;
}
}
@@ -183,62 +211,6 @@
}
.commit-row-message {
- color: $gl-gray;
- }
-}
-
-.divergence-graph {
- padding: 12px 12px 0 0;
- float: right;
-
- .graph-side {
- position: relative;
- width: 80px;
- height: 22px;
- padding: 5px 0 13px;
- float: left;
-
- .bar {
- position: absolute;
- height: 4px;
- background-color: $divergence-graph-bar-bg;
- }
-
- .bar-behind {
- right: 0;
- border-radius: 3px 0 0 3px;
- }
-
- .bar-ahead {
- left: 0;
- border-radius: 0 3px 3px 0;
- }
-
- .count {
- padding-top: 6px;
- padding-bottom: 0;
- font-size: 12px;
- color: $gl-title-color;
- display: block;
- }
-
- .count-behind {
- padding-right: 4px;
- text-align: right;
- }
-
- .count-ahead {
- padding-left: 4px;
- text-align: left;
- }
- }
-
- .graph-separator {
- position: relative;
- width: 1px;
- height: 18px;
- margin: 5px 0 0;
- float: left;
- background-color: $divergence-graph-separator-bg;
+ color: $gl-text-color;
}
}
diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss
index 5aef31724e1..cda069e6c0e 100644
--- a/app/assets/stylesheets/pages/cycle_analytics.scss
+++ b/app/assets/stylesheets/pages/cycle_analytics.scss
@@ -20,15 +20,19 @@
.fa {
color: $cycle-analytics-light-gray;
+
+ &:hover {
+ color: $gl-text-color;
+ }
}
.stage-header {
- width: 28%;
+ width: 26%;
padding-left: $gl-padding;
}
.median-header {
- width: 12%;
+ width: 14%;
}
.event-header {
@@ -111,14 +115,14 @@
line-height: 19px;
font-size: 14px;
font-weight: 600;
- color: $gl-title-color;
+ color: $gl-text-color;
}
&.text {
color: $layout-link-gray;
&.value-col {
- color: $gl-title-color;
+ color: $gl-text-color;
}
}
}
@@ -141,7 +145,7 @@
.dismiss-icon {
position: absolute;
- right: $cycle-analytics-dismiss-icon-color;
+ right: $cycle-analytics-box-padding;
cursor: pointer;
color: $cycle-analytics-dismiss-icon-color;
}
@@ -215,7 +219,6 @@
border-bottom: 1px solid transparent;
border-right: 1px solid $border-color;
background-color: $gray-light;
- cursor: default;
&.active {
background-color: transparent;
@@ -247,11 +250,11 @@
float: left;
&.stage-name {
- width: 70%;
+ width: 65%;
}
&.stage-median {
- width: 30%;
+ width: 35%;
}
}
@@ -261,7 +264,7 @@
.stage-empty,
.not-available {
- color: $gl-text-color-light;
+ color: $gl-text-color-secondary;
}
}
}
@@ -328,7 +331,7 @@
@include text-overflow();
a {
- color: $gl-dark-link-color;
+ color: $gl-text-color;
}
}
}
@@ -356,7 +359,7 @@
.issue-link,
.commit-author-link,
.issue-author-link {
- color: $gl-dark-link-color;
+ color: $gl-text-color;
}
// Custom CSS for components
@@ -397,11 +400,11 @@
}
.item-build-name {
- color: $gl-title-color;
+ color: $gl-text-color;
}
.pipeline-id {
- color: $gl-title-color;
+ color: $gl-text-color;
padding: 0 3px 0 0;
}
@@ -424,7 +427,7 @@
}
.fa {
- color: $gl-text-color-light;
+ color: $gl-text-color-secondary;
font-size: $code_font_size;
}
}
@@ -436,7 +439,7 @@
width: 75%;
margin: 0 auto;
padding-top: 130px;
- color: $gl-text-color-light;
+ color: $gl-text-color-secondary;
h4 {
color: $gl-text-color;
diff --git a/app/assets/stylesheets/pages/deploy_keys.scss b/app/assets/stylesheets/pages/deploy_keys.scss
new file mode 100644
index 00000000000..2fafe052106
--- /dev/null
+++ b/app/assets/stylesheets/pages/deploy_keys.scss
@@ -0,0 +1,13 @@
+.deploy-keys-list {
+ width: 100%;
+ overflow: auto;
+
+ table {
+ border: 1px solid $table-border-color;
+ }
+}
+
+.deploy-keys-title {
+ padding-bottom: 2px;
+ line-height: 2;
+}
diff --git a/app/assets/stylesheets/pages/detail_page.scss b/app/assets/stylesheets/pages/detail_page.scss
index 9b28df1afc5..46fd19c93f9 100644
--- a/app/assets/stylesheets/pages/detail_page.scss
+++ b/app/assets/stylesheets/pages/detail_page.scss
@@ -1,15 +1,15 @@
.detail-page-header {
padding: $gl-padding-top 0;
border-bottom: 1px solid $border-color;
- color: $gl-text-color-dark;
+ color: $gl-text-color;
line-height: 34px;
.author {
- color: $gl-text-color-dark;
+ color: $gl-text-color;
}
.identifier {
- color: $gl-text-color-dark;
+ color: $gl-text-color;
}
.issue_created_ago,
@@ -22,7 +22,7 @@
.title {
margin: 0 0 16px;
font-size: 2em;
- color: $gl-gray-dark;
+ color: $gl-text-color;
padding: 0 0 0.3em;
border-bottom: 1px solid $white-dark;
}
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index f30795fd2c2..96ba7c40634 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -14,7 +14,7 @@
background: $gray-light;
border-bottom: 1px solid $border-color;
padding: 10px 16px;
- color: $gl-diff-text-color;
+ color: $gl-text-color;
z-index: 10;
border-radius: 3px 3px 0 0;
@@ -50,7 +50,7 @@
overflow: auto;
overflow-y: hidden;
background: $white-light;
- color: $gl-title-color;
+ color: $gl-text-color;
border-radius: 0 0 3px 3px;
.unfold {
@@ -380,7 +380,7 @@
}
cursor: default;
- color: $gl-title-color;
+ color: $gl-text-color;
}
&.disabled {
diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss
index 3d60426de01..778ef01430e 100644
--- a/app/assets/stylesheets/pages/environments.scss
+++ b/app/assets/stylesheets/pages/environments.scss
@@ -72,25 +72,25 @@
.external-url,
.dropdown-new {
- color: $gl-gray-light;
+ color: $gl-text-color-secondary;
}
.dropdown-menu {
.fa {
margin-right: 6px;
- color: $gl-gray-light;
+ color: $gl-text-color-secondary;
}
}
.build-link,
.branch-name {
- color: $gl-dark-link-color;
+ color: $gl-text-color;
}
.stop-env-link,
.external-url {
- color: $gl-gray-light;
+ color: $gl-text-color-secondary;
.stop-env-icon {
font-size: 14px;
@@ -101,7 +101,7 @@
.build-column {
.build-link {
- color: $gl-dark-link-color;
+ color: $gl-text-color;
}
.avatar {
@@ -121,13 +121,6 @@
.folder-name {
cursor: pointer;
-
- .badge {
- font-weight: normal;
- background-color: $gray-darker;
- color: $gl-gray-light;
- vertical-align: baseline;
- }
}
}
@@ -142,4 +135,4 @@
margin-right: 0;
}
}
-}
+} \ No newline at end of file
diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss
index 98925c2d0cb..b989d72ce1c 100644
--- a/app/assets/stylesheets/pages/events.scss
+++ b/app/assets/stylesheets/pages/events.scss
@@ -21,7 +21,7 @@
}
a {
- color: $gl-dark-link-color;
+ color: $gl-text-color;
}
.avatar {
diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss
index 16bff5f1e03..d377526e655 100644
--- a/app/assets/stylesheets/pages/groups.scss
+++ b/app/assets/stylesheets/pages/groups.scss
@@ -13,7 +13,7 @@
.stats {
float: right;
line-height: $list-text-height;
- color: $gl-gray;
+ color: $gl-text-color;
span {
margin-right: 15px;
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index 4fac0cfb0ba..9174976c4c6 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -1,9 +1,13 @@
// Limit MR description for side-by-side diff view
+.fixed-width-container {
+ max-width: $limited-layout-width - ($gl-padding * 2);
+ margin-left: auto;
+ margin-right: auto;
+}
+
.limit-container-width {
.detail-page-header {
- max-width: calc(#{$limited-layout-width} - (#{$gl-padding} * 2));
- margin-left: auto;
- margin-right: auto;
+ @extend .fixed-width-container;
}
.issuable-details {
@@ -11,9 +15,7 @@
.mr-source-target,
.mr-state-widget,
.merge-manually {
- max-width: calc(#{$limited-layout-width} - (#{$gl-padding} * 2));
- margin-left: auto;
- margin-right: auto;
+ @extend .fixed-width-container;
}
.merge-request-tabs-holder {
@@ -24,23 +26,19 @@
border: 0;
}
}
+ }
+ }
- .container-fluid {
- padding-left: 0;
- padding-right: 0;
- max-width: calc(#{$limited-layout-width} - (#{$gl-padding} * 2));
- margin-left: auto;
- margin-right: auto;
- }
+ .merge-request-details {
+ .emoji-list-container {
+ @extend .fixed-width-container;
}
}
.diffs {
.mr-version-controls,
.files-changed {
- max-width: calc(#{$limited-layout-width} - (#{$gl-padding} * 2));
- margin-left: auto;
- margin-right: auto;
+ @extend .fixed-width-container;
}
}
}
@@ -52,6 +50,12 @@
}
}
+ .title {
+ padding: 0;
+ margin: 0;
+ border-bottom: none;
+ }
+
// Border around images in issue and MR descriptions.
.description img:not(.emoji) {
border: 1px solid $white-normal;
@@ -144,10 +148,10 @@
}
.edit-link {
- color: $gl-gray;
+ color: $gl-text-color;
- &:hover {
- color: $md-link-color;
+ &:not([href]):hover {
+ color: rgba($avatar-border, .2);
}
}
}
@@ -180,7 +184,7 @@
}
.btn-clipboard:hover {
- color: $gl-gray;
+ color: $gl-text-color;
}
}
@@ -189,7 +193,7 @@
top: $header-height;
bottom: 0;
right: 0;
- z-index: 10;
+ z-index: 8;
transition: width .3s;
background: $gray-light;
padding: 10px 20px;
@@ -215,7 +219,7 @@
}
.no-value {
- color: $gl-gray-light;
+ color: $gl-text-color-secondary;
}
.sidebar-collapsed-icon {
@@ -283,7 +287,7 @@
color: $issuable-sidebar-color;
&:hover {
- color: $gl-gray;
+ color: $gl-text-color;
}
span {
@@ -296,16 +300,16 @@
}
.avatar:hover {
- border-color: $issuable-avatar-hover-border;
+ border-color: $issuable-sidebar-color;
}
.btn-clipboard {
border: none;
- color: $issuable-clipboard-color;
+ color: $issuable-sidebar-color;
&:hover {
background: transparent;
- color: $gl-gray;
+ color: $gl-text-color;
}
}
}
@@ -324,6 +328,10 @@
&:hover {
color: $md-link-color;
text-decoration: none;
+
+ .avatar {
+ border-color: rgba($avatar-border, .2);
+ }
}
}
@@ -365,6 +373,10 @@
display: inline-block;
padding: 5px;
+ &:nth-of-type(7n) {
+ padding-right: 0;
+ }
+
.author_link {
display: block;
}
@@ -379,7 +391,7 @@
margin-left: 5px;
a {
- color: $gl-gray-light;
+ color: $gl-text-color-secondary;
}
}
@@ -434,6 +446,7 @@
.issuable-meta {
display: inline-block;
line-height: 18px;
+ font-size: 14px;
}
.js-issuable-selector-wrap {
@@ -460,3 +473,102 @@
}
}
}
+
+.time_tracker {
+ padding-bottom: 0;
+ border-bottom: 0;
+
+
+ .sidebar-collapsed-icon {
+
+ > .stopwatch-svg {
+ display: inline-block;
+ }
+
+ svg {
+ width: 16px;
+ height: 16px;
+ fill: $sidebar-collapsed-icon-color;
+ }
+
+ &:hover svg {
+ fill: $gl-text-color;
+ }
+ }
+
+ .help-button,
+ .close-help-button {
+ cursor: pointer;
+ }
+
+ .compare-meter {
+ &.within_estimate {
+ .meter-fill {
+ background: $gl-primary;
+ }
+ }
+
+ &.over_estimate {
+ .meter-fill {
+ background: $red-light;
+ }
+
+ .time-remaining,
+ .compare-value.spent {
+ color: $red-light;
+ }
+ }
+ }
+
+ .meter-container {
+ background: $border-gray-light;
+ border-radius: 3px;
+
+ .meter-fill {
+ max-width: 100%;
+ height: 5px;
+ border-radius: 3px;
+ background: $gl-primary;
+ }
+ }
+
+ .compare-display-container {
+ display: flex;
+ justify-content: space-between;
+ margin-top: 5px;
+
+ .compare-display {
+ font-size: 13px;
+ color: $compare-display-color;
+
+ .compare-value {
+ color: $gl-text-color;
+ }
+ }
+ }
+
+ .time-tracking-help-state {
+ background: $white-light;
+ margin: 16px -20px 0;
+ padding: 16px 20px;
+ border-top: 1px solid $border-gray-light;
+ border-bottom: 1px solid $border-gray-light;
+
+ a:hover {
+ color: $btn-white-active;
+ }
+ }
+
+ .help-state-toggle-enter-active {
+ transition: all .8s ease;
+ }
+
+ .help-state-toggle-leave-active {
+ transition: all .5s ease;
+ }
+
+ .help-state-toggle-enter,
+ .help-state-toggle-leave-active {
+ opacity: 0;
+ }
+}
diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss
index 8734a3b1598..1e605337f09 100644
--- a/app/assets/stylesheets/pages/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -148,3 +148,7 @@ ul.related-merge-requests > li {
border: 1px solid $border-gray-normal;
}
}
+
+.recaptcha {
+ margin-bottom: 30px;
+}
diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss
index d129eb12a45..762b95a657c 100644
--- a/app/assets/stylesheets/pages/labels.scss
+++ b/app/assets/stylesheets/pages/labels.scss
@@ -117,7 +117,7 @@
.manage-labels-list {
.btn-action {
- color: $gl-dark-link-color;
+ color: $gl-text-color;
.fa {
font-size: 18px;
@@ -203,6 +203,10 @@
z-index: 3;
border-radius: $label-border-radius;
padding: 6px 10px 6px 9px;
+
+ &:hover {
+ box-shadow: inset 0 0 0 80px $label-remove-border;
+ }
}
.btn {
@@ -255,3 +259,8 @@
}
}
}
+
+.label-link {
+ display: inline-block;
+ vertical-align: text-top;
+}
diff --git a/app/assets/stylesheets/pages/lint.scss b/app/assets/stylesheets/pages/lint.scss
index a7c80dce424..68b6c5ecbd4 100644
--- a/app/assets/stylesheets/pages/lint.scss
+++ b/app/assets/stylesheets/pages/lint.scss
@@ -9,3 +9,13 @@
color: $lint-correct-color;
}
}
+
+.ci-linter {
+ .ci-editor {
+ height: 400px;
+ }
+
+ .ci-template pre {
+ white-space: pre-wrap;
+ }
+}
diff --git a/app/assets/stylesheets/pages/login.scss b/app/assets/stylesheets/pages/login.scss
index dd27a06fcd2..71ed5b1361a 100644
--- a/app/assets/stylesheets/pages/login.scss
+++ b/app/assets/stylesheets/pages/login.scss
@@ -17,14 +17,19 @@
line-height: 1.5;
p {
- font-size: 18px;
+ font-size: 16px;
color: $login-brand-holder-color;
}
h1:first-child {
font-weight: normal;
- margin-bottom: 30px;
+ margin-bottom: 0.68em;
margin-top: 0;
+ font-size: 34px;
+ }
+
+ h3 {
+ font-size: 22px;
}
img {
@@ -105,19 +110,19 @@
li {
flex: 1;
text-align: center;
+ border-left: 1px solid $border-color;
&:first-of-type {
+ border-left: none;
border-top-left-radius: $border-radius-default;
}
&:last-of-type {
- border-left: 1px solid $border-color;
border-top-right-radius: $border-radius-default;
}
&:not(.active) {
background-color: $gray-light;
- border-left: 1px solid $border-color;
}
a {
diff --git a/app/assets/stylesheets/pages/members.scss b/app/assets/stylesheets/pages/members.scss
index 36ee5d17211..be7193bae04 100644
--- a/app/assets/stylesheets/pages/members.scss
+++ b/app/assets/stylesheets/pages/members.scss
@@ -25,7 +25,7 @@
}
.form-horizontal {
- margin-top: 5px;
+ margin-top: 20px;
@media (min-width: $screen-sm-min) {
display: -webkit-flex;
@@ -98,6 +98,10 @@
padding-right: 35px;
@media (min-width: $screen-sm-min) {
+ width: 250px;
+ }
+
+ @media (min-width: $screen-md-min) {
width: 350px;
}
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index e779e65eca3..b01d8d695d6 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -4,7 +4,7 @@
*/
.mr-state-widget {
background: $gray-light;
- color: $gl-gray;
+ color: $gl-text-color;
border: 1px solid $border-color;
border-radius: 2px;
@@ -21,6 +21,14 @@
display: inline-block;
float: left;
+ .btn-success.dropdown-toggle .fa {
+ color: inherit;
+ }
+
+ .btn-success.dropdown-toggle:disabled {
+ background-color: $gl-success;
+ }
+
.accept_merge_request {
&.ci-pending,
&.ci-running {
@@ -48,21 +56,34 @@
&.right {
float: right;
padding-right: 0;
+ }
- a {
- color: $gl-gray;
- }
+ .modify-merge-commit-link {
+ color: $gl-text-color;
}
- .remove_source_checkbox {
+ .merge-param-checkbox {
margin: 0;
}
+
+ a .fa-question-circle {
+ color: $gl-text-color-secondary;
+
+ &:hover,
+ &:focus {
+ color: $link-hover-color;
+ }
+ }
}
}
.ci_widget {
border-bottom: 1px solid $well-inner-border;
- color: $gl-gray;
+ color: $gl-text-color;
+ display: -webkit-flex;
+ display: flex;
+ -webkit-align-items: center;
+ align-items: center;
svg {
margin-right: 4px;
@@ -71,12 +92,20 @@
overflow: visible;
}
+ &> span {
+ padding-right: 4px;
+ }
+
&.ci-success_with_warnings {
i {
color: $gl-warning;
}
}
+
+ @media (max-width: $screen-xs-max) {
+ flex-wrap: wrap;
+ }
}
.mr-widget-body,
@@ -85,8 +114,39 @@
padding: $gl-padding;
}
+ .mr-widget-pipeline-graph {
+ flex-shrink: 0;
+
+ .dropdown-menu {
+ margin-top: 11px;
+ }
+
+ .ci-action-icon-wrapper {
+ line-height: 16px;
+ }
+
+ @media (max-width: $screen-xs-max) {
+ order: 1;
+ margin-top: $gl-padding-top;
+ border-radius: 3px;
+ background-color: $white-light;
+ border: 1px solid $gray-darker;
+ width: 100%;
+ text-align: center;
+
+ .dropdown-menu {
+ margin-left: -97.5px;
+ }
+
+ .arrow-up::before,
+ .arrow-up::after, {
+ margin-left: 97.5px;
+ }
+ }
+ }
+
.normal {
- color: $gl-text-color-dark;
+ color: $gl-text-color;
}
.js-deployment-link {
@@ -96,9 +156,9 @@
.mr-widget-body {
h4 {
font-weight: 600;
- font-size: 17px;
+ font-size: 16px;
margin: 5px 0;
- color: $gl-gray-dark;
+ color: $gl-text-color;
&.has-conflicts .fa-exclamation-triangle {
color: $gl-warning;
@@ -182,7 +242,7 @@
}
.label-branch {
- color: $gl-gray-dark;
+ color: $gl-text-color;
font-family: $monospace_font;
font-weight: bold;
overflow: hidden;
@@ -302,10 +362,6 @@
left: 0;
top: 2px;
}
-
- .commit-row-info {
- line-height: 20px;
- }
}
.btn-clipboard {
@@ -359,7 +415,7 @@
th {
background-color: $white-light;
- color: $gl-gray-light;
+ color: $gl-text-color-secondary;
}
}
}
@@ -421,10 +477,30 @@
left: 0;
z-index: 10;
transition: right .15s;
+
+ @media (max-width: $screen-xs-max) {
+ right: 0;
+ }
+
+ .merge-request-tabs-container {
+ padding-left: $gl-padding;
+ padding-right: $gl-padding;
+ }
+ }
+}
+
+.limit-container-width {
+ .merge-request-tabs-container {
+ max-width: $limited-layout-width;
+ margin-left: auto;
+ margin-right: auto;
}
+}
- &:not(.affix) .container-fluid {
- padding-left: 0;
- padding-right: 0;
+.limit-container-width:not(.container-limited) {
+ .merge-request-tabs-holder:not(.affix) {
+ .merge-request-tabs-container {
+ max-width: $limited-layout-width - ($gl-padding * 2);
+ }
}
}
diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss
index 77c523d7310..686b64cdd24 100644
--- a/app/assets/stylesheets/pages/milestone.scss
+++ b/app/assets/stylesheets/pages/milestone.scss
@@ -25,12 +25,6 @@
}
.issuable-row {
- .color-label {
- border-radius: 2px;
- padding: 3px !important;
- margin-right: 7px;
- }
-
span a {
color: $gl-text-color;
word-wrap: break-word;
@@ -108,13 +102,17 @@
margin-top: 7px;
.issuable-number {
- color: $gl-gray-light;
+ color: $gl-text-color-secondary;
margin-right: 5px;
}
.avatar {
float: none;
}
+
+ > a:not(:last-of-type) {
+ margin-right: 5px;
+ }
}
}
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index 074abec7692..f984b469609 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -27,6 +27,7 @@
.new-note,
.note-edit-form {
.note-form-actions {
+ position: relative;
margin-top: $gl-padding;
}
@@ -44,7 +45,7 @@
.note-textarea {
display: block;
padding: 10px 0;
- color: $gl-gray;
+ color: $gl-text-color;
font-family: $regular_font;
border: 0;
@@ -109,7 +110,7 @@
margin: auto;
margin-top: 0;
text-align: center;
- font-size: 13px;
+ font-size: 12px;
@media (max-width: $screen-sm-max) {
// On smaller devices the warning becomes the fourth item in the list,
@@ -204,7 +205,7 @@
.comment-toolbar {
padding-top: $gl-padding-top;
- color: $gl-gray-light;
+ color: $gl-text-color-secondary;
border-top: 1px solid $border-color;
}
@@ -265,3 +266,18 @@
}
}
}
+
+.note-edit-warning.settings-message {
+ display: none;
+ padding: 5px 10px;
+ position: absolute;
+ left: 127px;
+ top: 2px;
+
+ @media (max-width: $screen-xs-max) {
+ position: relative;
+ top: 0;
+ left: 0;
+ margin-bottom: 10px;
+ }
+}
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 106c5d4d390..f310cc72da0 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -43,7 +43,7 @@ ul.notes {
}
.system-note-message {
- display: inline-block;
+ display: inline;
&::first-letter {
text-transform: lowercase;
@@ -55,7 +55,7 @@ ul.notes {
}
p {
- display: inline-block;
+ display: inline;
margin: 0;
&::first-letter {
@@ -195,10 +195,10 @@ ul.notes {
}
.note-body {
- overflow: auto;
+ overflow-x: auto;
+ overflow-y: hidden;
.note-text {
- overflow: auto;
word-wrap: break-word;
@include md-typography;
// Reset ul style types since we're nested inside a ul already
@@ -345,7 +345,7 @@ ul.notes {
}
.author_link {
- color: $gl-gray;
+ color: $gl-text-color;
}
}
@@ -353,6 +353,14 @@ ul.notes {
font-size: 14px;
}
+.note-headline-light {
+ display: inline;
+
+ @media (max-width: $screen-xs-min) {
+ display: block;
+ }
+}
+
.note-headline-light,
.discussion-headline-light {
color: $notes-light-color;
@@ -448,11 +456,6 @@ ul.notes {
border-radius: $border-radius-base;
}
-.diff-file .note .note-actions {
- right: 0;
- top: 0;
-}
-
/**
* Line note button on the side of diffs
@@ -464,7 +467,7 @@ ul.notes {
}
.add-diff-note {
- margin-top: -4px;
+ margin-top: -8px;
border-radius: 40px;
background: $white-light;
padding: 4px;
@@ -512,7 +515,6 @@ ul.notes {
.line-resolve-all-container {
.btn-group {
- margin-top: -1px;
margin-left: -4px;
}
@@ -523,8 +525,9 @@ ul.notes {
}
.line-resolve-all {
+ vertical-align: middle;
display: inline-block;
- padding: 5px 10px;
+ padding: 6px 10px;
background-color: $gray-light;
border: 1px solid $border-color;
border-radius: $border-radius-default;
@@ -532,18 +535,14 @@ ul.notes {
&.has-next-btn {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
+ border-right: 0;
}
.line-resolve-btn {
- vertical-align: middle;
margin-right: 5px;
}
}
-.line-resolve-text {
- vertical-align: middle;
-}
-
.line-resolve-btn {
display: inline-block;
position: relative;
@@ -562,18 +561,14 @@ ul.notes {
&.is-active {
color: $gl-text-green;
- svg path {
+ svg {
fill: $gl-text-green;
}
}
svg {
position: relative;
- color: $gray-darkest;
-
- path {
- fill: $gray-darkest;
- }
+ fill: $gray-darkest;
}
}
@@ -586,3 +581,17 @@ ul.notes {
}
}
}
+
+// Merge request notes in diffs
+.diff-file {
+ // Diff is side by side
+ .notes_content.parallel .note-header .note-headline-light {
+ display: block;
+ position: relative;
+ }
+ // Diff is inline
+ .notes_content .note-header .note-headline-light {
+ display: inline-block;
+ position: relative;
+ }
+}
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index 621b780ce4d..974100bdff0 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -1,4 +1,9 @@
.pipelines {
+ .realtime-loading {
+ font-size: 40px;
+ text-align: center;
+ }
+
.stage {
max-width: 90px;
width: 90px;
@@ -24,6 +29,10 @@
min-width: 1200px;
table-layout: fixed;
+ .label {
+ margin-bottom: 3px;
+ }
+
.pipeline-id {
color: $black;
}
@@ -35,8 +44,8 @@
.pipeline-info,
.pipeline-commit,
- .pipeline-actions,
- .pipeline-stages {
+ .pipeline-stages,
+ .pipeline-actions {
width: 20%;
}
}
@@ -52,6 +61,10 @@
}
}
+.content-list.pipelines .table-holder {
+ min-height: 300px;
+}
+
.pipeline-holder {
width: 100%;
overflow: auto;
@@ -80,6 +93,10 @@
td {
padding: 10px 8px;
}
+
+ .commit-link {
+ padding: 9px 8px 10px;
+ }
}
tbody {
@@ -123,7 +140,7 @@
height: 14px;
width: 14px;
vertical-align: middle;
- fill: $gl-gray-light;
+ fill: $gl-text-color-secondary;
}
.fa {
@@ -166,44 +183,11 @@
}
}
- .stage-cell {
- font-size: 0;
-
- svg {
- height: 18px;
- width: 18px;
- position: relative;
- z-index: 2;
- vertical-align: middle;
- overflow: visible;
- }
-
- .stage-container {
- display: inline-block;
- position: relative;
- margin-right: 6px;
-
- .tooltip {
- white-space: nowrap;
- }
-
- &:not(:last-child) {
- &::after {
- content: '';
- width: 8px;
- position: absolute;
- right: -7px;
- bottom: 9px;
- border-bottom: 2px solid $border-color;
- }
- }
- }
- }
-
.duration,
.finished-at {
- color: $gl-gray-light;
+ color: $gl-text-color-secondary;
margin: 4px 0;
+ white-space: nowrap;
.fa {
font-size: 12px;
@@ -223,7 +207,7 @@
.btn {
margin: 0;
- color: $gl-gray-light;
+ color: $gl-text-color-secondary;
}
.cancel-retry-btns {
@@ -236,10 +220,10 @@
.dropdown-toggle,
.dropdown-menu {
- color: $gl-gray-light;
+ color: $gl-text-color-secondary;
.fa {
- color: $gl-gray-light;
+ color: $gl-text-color-secondary;
font-size: 14px;
}
@@ -268,12 +252,16 @@
}
}
}
+
+ .tooltip {
+ white-space: nowrap;
+ }
}
.build-link {
a {
- color: $gl-dark-link-color;
+ color: $gl-text-color;
}
}
@@ -282,6 +270,48 @@
}
}
+.stage-cell {
+ font-size: 0;
+ padding: 10px 4px;
+
+ > .stage-container > div > button > span > svg,
+ > .stage-container > button > svg {
+ height: 22px;
+ width: 22px;
+ position: absolute;
+ top: -1px;
+ left: -1px;
+ z-index: 2;
+ overflow: visible;
+ }
+
+ .stage-container {
+ display: inline-block;
+ position: relative;
+ height: 22px;
+ margin: 3px 6px 3px 0;
+
+ .tooltip {
+ white-space: nowrap;
+ }
+
+ .tooltip-inner {
+ padding: 3px 4px;
+ }
+
+ &:not(:last-child) {
+ &::after {
+ content: '';
+ width: 7px;
+ position: absolute;
+ right: -7px;
+ top: 10px;
+ border-bottom: 2px solid $border-color;
+ }
+ }
+ }
+}
+
.admin-builds-table {
.ci-table td:last-child {
min-width: 120px;
@@ -327,17 +357,18 @@
width: 100%;
background-color: $gray-light;
padding: $gl-padding;
- overflow: auto;
white-space: nowrap;
transition: max-height 0.3s, padding 0.3s;
+ overflow: auto;
- ul {
+ .stage-column-list,
+ .builds-container > ul {
padding: 0;
}
a {
text-decoration: none;
- color: $gl-text-color-light;
+ color: $gl-text-color-secondary;
}
svg {
@@ -461,13 +492,50 @@
width: 186px;
margin-bottom: 10px;
white-space: normal;
- color: $gl-text-color-light;
+ color: $gl-text-color-secondary;
+
+ // Action Icons in big pipeline-graph nodes
+ > .ci-action-icon-container .ci-action-icon-wrapper {
+ height: 30px;
+ width: 30px;
+ background: $white-light;
+ border: 1px solid $border-color;
+ border-radius: 100%;
+ display: block;
+
+ &:hover {
+ background-color: $stage-hover-bg;
+ border: 1px solid $stage-hover-bg;
+ }
+
+ svg {
+ fill: $border-color;
+ position: relative;
+ left: -1px;
+ top: -1px;
+ }
+
+ &:hover svg {
+ fill: $gl-text-color;
+ }
+ }
+
+ > .ci-action-icon-container {
+ position: absolute;
+ right: 5px;
+ top: 5px;
+ }
+
+ .ci-status-icon svg {
+ height: 20px;
+ width: 20px;
+ }
.dropdown-menu-toggle {
background-color: transparent;
border: none;
padding: 0;
- color: $gl-text-color-light;
+ color: $gl-text-color-secondary;
&:focus {
outline: none;
@@ -497,21 +565,6 @@
}
}
- > .ci-action-icon-container {
- position: absolute;
- right: 4px;
- top: 5px;
- }
-
- .ci-status-icon {
- position: relative;
- top: 1px;
- }
-
- .ci-status-icon svg {
- height: 20px;
- width: 20px;
- }
.arrow {
&::before,
@@ -596,44 +649,14 @@
}
}
+// Triggers the dropdown in the big pipeline graph
.dropdown-counter-badge {
- float: right;
color: $border-color;
font-weight: 100;
font-size: 15px;
- margin-right: 2px;
-}
-
-.grouped-pipeline-dropdown {
- padding: 0;
- width: 191px;
- left: auto;
- right: -195px;
- top: -4px;
- box-shadow: 0 1px 5px $black-transparent;
-
- a {
- display: inline-block;
-
- &:hover {
- background-color: $stage-hover-bg;
- }
- }
-
- ul {
- max-height: 245px;
- overflow: auto;
- margin: 5px 0;
-
- li {
- padding-top: 2px;
- margin: 0 5px;
- padding-left: 0;
- padding-bottom: 0;
- margin-bottom: 0;
- line-height: 1.2;
- }
- }
+ position: absolute;
+ right: 13px;
+ top: 8px;
}
.ci-status-text {
@@ -644,117 +667,248 @@
vertical-align: bottom;
display: inline-block;
position: relative;
- font-weight: 100;
+ font-weight: normal;
}
-// Action Icons
-.ci-action-icon-container .ci-action-icon-wrapper {
- i {
- color: $border-color;
- border-radius: 100%;
- border: 1px solid $border-color;
- padding: 5px 6px;
- font-size: 13px;
- background: $white-light;
- height: 30px;
- width: 30px;
+// Dropdown button in mini pipeline graph
+.mini-pipeline-graph-dropdown-toggle {
+ border-radius: 100px;
+ background-color: $white-light;
+ border-width: 1px;
+ border-style: solid;
+ width: 22px;
+ height: 22px;
+ margin: 0;
+ padding: 0;
+ transition: all 0.2s linear;
+ position: relative;
- &::before {
- position: relative;
- top: 3px;
- left: 3px;
+ > .fa.fa-caret-down {
+ position: absolute;
+ left: 20px;
+ top: 5px;
+ display: inline-block;
+ visibility: hidden;
+ opacity: 0;
+ color: inherit;
+ font-size: 12px;
+ transition: visibility 0.1s, opacity 0.1s linear;
+ }
+
+ &:active,
+ &:focus,
+ &:hover {
+ outline: none;
+ width: 35px;
+
+ .fa.fa-caret-down {
+ visibility: visible;
+ opacity: 1;
}
+ }
- &:hover {
- color: $gl-text-color;
- background-color: $stage-hover-bg;
- border: 1px solid $stage-hover-bg;
+ // Dropdown button animation in mini pipeline graph
+ &.ci-status-icon-success {
+ border-color: $gl-success;
+ color: $gl-success;
+
+ &:hover,
+ &:focus,
+ &:active {
+ background-color: rgba($gl-success, 0.1);
+ border-color: $gl-success;
}
}
- .ci-play-icon {
- padding: 5px 5px 5px 7px;
+ &.ci-status-icon-failed {
+ border-color: $gl-danger;
+ color: $gl-danger;
+
+ &:hover,
+ &:focus,
+ &:active {
+ background-color: rgba($gl-danger, 0.1);
+ border-color: $gl-danger;
+ }
}
-}
-.dropdown-build {
- color: $gl-text-color-light;
+ &.ci-status-icon-pending,
+ &.ci-status-icon-success_with_warnings {
+ border-color: $gl-warning;
+ color: $gl-warning;
- .ci-action-icon-container {
- padding: 0;
- font-size: 11px;
- float: right;
- margin-top: 4px;
- display: inline-block;
- position: relative;
+ &:hover,
+ &:focus,
+ &:active {
+ background-color: rgba($gl-warning, 0.1);
+ border-color: $gl-warning;
+ }
+ }
+
+ &.ci-status-icon-running {
+ border-color: $blue-normal;
+ color: $blue-normal;
- i {
- font-size: 11px;
- margin-top: 0;
+ &:hover,
+ &:focus,
+ &:active {
+ background-color: rgba($blue-normal, 0.1);
+ border-color: $blue-normal;
}
}
- &:hover {
- background-color: $stage-hover-bg;
- border-radius: 3px;
+ &.ci-status-icon-canceled,
+ &.ci-status-icon-disabled,
+ &.ci-status-icon-not-found,
+ &.ci-status-icon-manual {
+ border-color: $gl-text-color;
color: $gl-text-color;
+
+ &:hover,
+ &:focus,
+ &:active {
+ background-color: rgba($gl-text-color, 0.1);
+ border-color: $gl-text-color;
+ }
}
- .ci-action-icon-container {
- i {
- width: 25px;
- height: 25px;
+ &.ci-status-icon-created,
+ &.ci-status-icon-skipped {
+ border-color: $gray-darkest;
+ color: $gray-darkest;
- &::before {
- top: 1px;
- left: 1px;
- }
+ &:hover,
+ &:focus,
+ &:active {
+ background-color: rgba($gray-darkest, 0.1);
+ border-color: $gray-darkest;
}
}
+}
- .stage {
- max-width: 100px;
- width: 100px;
- }
+// dropdown content for big and mini pipeline
+.big-pipeline-graph-dropdown-menu,
+.mini-pipeline-graph-dropdown-menu {
+ width: 195px;
+ max-width: 195px;
- .ci-status-icon svg {
- height: 18px;
- width: 18px;
+ li {
+ padding: 2px 3px;
}
- .ci-status-text {
- max-width: 95px;
+ .scrollable-menu {
+ max-height: 245px;
+ overflow: auto;
}
-}
-/**
- * Builds dropdown in mini pipeline
- */
-.mini-pipeline-graph {
- .builds-dropdown {
- background-color: transparent;
- border: none;
- padding: 0;
- color: $gl-text-color-light;
- border: none;
- margin: 0;
+ // Loading icon
+ .builds-dropdown-loading {
+ margin: 0 auto;
+ width: 20px;
+ }
+
+ // Action icon on the right
+ a.ci-action-icon-wrapper {
+ color: $action-icon-color;
+ border: 1px solid $action-icon-color;
+ border-radius: 20px;
+ width: 22px;
+ height: 22px;
+ padding: 2px 0 0 5px;
+ cursor: pointer;
+ float: right;
+ margin: -26px 9px 0 0;
+ font-size: 12px;
+ background-color: $white-light;
+
+ &:hover,
+ &:focus {
+ background-color: $stage-hover-bg;
+ border: 1px solid transparent;
+ }
+
+ svg {
+ width: 22px;
+ height: 22px;
+ left: -6px;
+ position: relative;
+ top: -3px;
+ fill: $action-icon-color;
+ }
+
+ &:hover svg,
+ &:focus svg {
+ fill: $gl-text-color;
+ }
}
- .builds-dropdown-loading {
- margin: 10px auto;
- width: 18px;
+ // link to the build
+ .mini-pipeline-graph-dropdown-item {
+ padding: 3px 7px 4px;
+ clear: both;
+ font-weight: normal;
+ line-height: 1.428571429;
+ white-space: nowrap;
+ margin: 0 5px;
+ border-radius: 3px;
+
+ // build name
+ .ci-build-text {
+ font-weight: 200;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ width: 90px;
+ color: $gl-text-color-secondary;
+ margin-left: 2px;
+ display: inline-block;
+ top: 1px;
+ vertical-align: text-bottom;
+ position: relative;
+ }
+
+ // status icon on the left
+ .ci-status-icon {
+ top: 3px;
+ position: relative;
+
+ > svg {
+ overflow: visible;
+ width: 18px;
+ height: 18px;
+ }
+ }
+
+ &:hover,
+ &:focus {
+ outline: none;
+ text-decoration: none;
+ color: $gl-text-color;
+ background-color: $stage-hover-bg;
+ }
}
+}
- .grouped-pipeline-dropdown {
- right: -172px;
- top: 23px;
- min-height: 50px;
+// Dropdown in the big pipeline graph
+.big-pipeline-graph-dropdown-menu {
+ width: 195px;
+ min-width: 195px;
+ left: auto;
+ right: -195px;
+ top: -4px;
+ box-shadow: 0 1px 5px $black-transparent;
- a {
- color: $gl-text-color-light;
+ .mini-pipeline-graph-dropdown-item {
+ .ci-status-icon {
+ top: -1px;
}
}
+}
+/**
+ * Top arrow in the dropdown in the mini pipeline graph
+ */
+.mini-pipeline-graph-dropdown-menu {
.arrow-up {
&::before,
&::after {
@@ -783,94 +937,8 @@
}
/**
- * Icons in mini pipeline graph
+ * Terminal
*/
-.mini-pipeline-graph-icon-container .ci-status-icon {
- display: inline-block;
- border: 1px solid;
- border-radius: 20px;
- margin-right: 1px;
- width: 20px;
- height: 20px;
- position: relative;
- z-index: 2;
- transition: all 0.2s cubic-bezier(0.25, 0, 1, 1);
-
- svg {
- top: -1px;
- }
-}
-
-.builds-dropdown {
- &:focus {
- outline: none;
- margin-right: -8px;
-
- .ci-status-icon {
- width: 28px;
- padding: 0 8px 0 0;
- transition: width 0.2s cubic-bezier(0.25, 0, 1, 1);
-
- + .dropdown-caret {
- display: inline-block;
- }
- }
- }
-
- &:focus,
- &:active {
- .ci-status-icon-success {
- background-color: rgba($gl-success, .1);
- }
-
- .ci-status-icon-failed {
- background-color: rgba($gl-danger, .1);
- }
-
- .ci-status-icon-pending,
- .ci-status-icon-success_with_warnings {
- background-color: rgba($gl-warning, .1);
- }
-
- .ci-status-icon-running {
- background-color: rgba($blue-normal, .1);
- }
-
- .ci-status-icon-canceled,
- .ci-status-icon-disabled,
- .ci-status-icon-not-found {
- background-color: rgba($gl-gray, .1);
- }
-
- .ci-status-icon-created,
- .ci-status-icon-skipped {
- background-color: rgba($gray-darkest, .1);
- }
- }
-
- .mini-pipeline-graph-icon-container {
- .ci-status-icon:hover,
- .ci-status-icon:focus {
- width: 28px;
- padding: 0 8px 0 0;
-
- + .dropdown-caret {
- display: inline-block;
- }
- }
-
- .dropdown-caret {
- font-size: 11px;
- position: relative;
- top: 3px;
- left: -11px;
- margin-right: -6px;
- display: none;
- z-index: 2;
- }
- }
-}
-
.terminal-icon {
margin-left: 3px;
}
diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss
index 8b1976bd925..722b3006f7c 100644
--- a/app/assets/stylesheets/pages/profile.scss
+++ b/app/assets/stylesheets/pages/profile.scss
@@ -216,8 +216,8 @@
}
}
-.user-profile {
+.user-profile {
.cover-controls a {
margin-left: 5px;
}
@@ -231,8 +231,11 @@
}
}
- @media (max-width: $screen-xs-max) {
+ .user-profile-nav {
+ font-size: 0;
+ }
+ @media (max-width: $screen-xs-max) {
.cover-block {
padding-top: 20px;
}
@@ -253,6 +256,12 @@
}
}
}
+
+ .user-profile-nav {
+ a {
+ margin-right: 0;
+ }
+ }
}
}
@@ -271,4 +280,4 @@ table.u2f-registrations {
.scopes-list {
padding-left: 18px;
}
-} \ No newline at end of file
+}
diff --git a/app/assets/stylesheets/pages/profiles/preferences.scss b/app/assets/stylesheets/pages/profiles/preferences.scss
index f8da0983b77..100ace41f2a 100644
--- a/app/assets/stylesheets/pages/profiles/preferences.scss
+++ b/app/assets/stylesheets/pages/profiles/preferences.scss
@@ -22,8 +22,8 @@
background: $theme-graphite;
}
- &.ui_gray {
- background: $theme-gray;
+ &.ui_black {
+ background: $theme-black;
}
&.ui_green {
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index e16a553bcda..8b59c20cb65 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -9,24 +9,24 @@
.new_project,
.edit-project {
- fieldset {
-
- &.features {
+ .sharing-and-permissions {
+ .header {
+ padding-top: $gl-vert-padding;
+ }
- .label-light {
- margin-bottom: 0;
- }
+ .label-light {
+ margin-bottom: 0;
+ }
- .help-block {
- margin-top: 0;
- }
+ .help-block {
+ margin-top: 0;
}
.form-group {
margin-bottom: 5px;
}
- &> .form-group {
+ & > .form-group {
padding-left: 0;
}
}
@@ -73,7 +73,7 @@
border: 1px solid $border-color;
}
- &+ .select2 a {
+ & + .select2 a {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
@@ -292,7 +292,7 @@
.option-title {
font-weight: normal;
display: inline-block;
- color: $gl-gray-dark;
+ color: $gl-text-color;
}
.option-descr {
@@ -331,7 +331,7 @@
a.deploy-project-label {
padding: 5px;
margin-right: 5px;
- color: $gl-gray;
+ color: $gl-text-color;
background-color: $row-hover;
&:hover {
@@ -372,7 +372,7 @@ a.deploy-project-label {
}
a {
- color: $gl-dark-link-color;
+ color: $gl-text-color;
}
.dropdown-menu {
@@ -426,7 +426,7 @@ a.deploy-project-label {
width: 100%;
height: 100%;
padding-top: $gl-padding;
- color: $gl-gray;
+ color: $gl-text-color;
.caption {
min-height: 30px;
@@ -523,7 +523,7 @@ a.deploy-project-label {
&:hover,
&:focus {
- color: darken($notes-light-color, 15%);
+ color: $gl-text-color;
}
}
@@ -552,7 +552,7 @@ pre.light-well {
margin: 0 7px 7px;
h5 {
- color: $gl-text-color-dark;
+ color: $gl-text-color;
}
.light-well {
@@ -587,11 +587,21 @@ pre.light-well {
.project-full-name {
@include str-truncated;
+
+ @media (max-width: $screen-xs-max) {
+ max-width: 50%;
+ }
}
.controls {
line-height: $list-text-height;
+ .badge {
+ @media (max-width: $screen-xs-max) {
+ display: none;
+ }
+ }
+
a:hover {
text-decoration: none;
}
@@ -605,6 +615,12 @@ pre.light-well {
top: 2px;
}
}
+
+ .description p {
+ @media (max-width: $screen-xs-max) {
+ max-width: 50%;
+ }
+ }
}
.bottom {
@@ -618,7 +634,6 @@ pre.light-well {
margin: 0;
}
-
.activity-filter-block {
.controls {
padding-bottom: 7px;
@@ -627,6 +642,12 @@ pre.light-well {
}
}
+.commits-search-form {
+ .input-short {
+ min-width: 200px;
+ }
+}
+
.project-last-commit {
@media (min-width: $screen-sm-min) {
margin-top: $gl-padding;
@@ -657,7 +678,7 @@ pre.light-well {
}
.commit-row-message {
- color: $gl-gray;
+ color: $gl-text-color;
}
.commit_short_id {
@@ -745,7 +766,7 @@ pre.light-well {
.protected-branches-list {
a {
- color: $gl-gray;
+ color: $gl-text-color;
&:hover {
color: $gl-link-color;
@@ -805,7 +826,31 @@ pre.light-well {
.compare-form-group {
.dropdown-menu {
- width: 300px;
+ width: 100%;
+
+ @media (min-width: $screen-sm-min) {
+ width: 300px;
+ }
+ }
+
+ + .compare-ellipsis {
+ width: 100%;
+ vertical-align: middle;
+ text-align: center;
+ margin-top: -20px;
+
+ @media (min-width: $screen-sm-min) {
+ margin-top: 0;
+ width: auto;
+ }
+ }
+
+ .inline-input-group {
+ width: 100%;
+
+ @media (min-width: $screen-sm-min) {
+ width: 250px;
+ }
}
}
@@ -860,10 +905,18 @@ pre.light-well {
}
}
-.project-feature-nested {
+.project-feature {
+ padding-top: 10px;
+
@media (min-width: $screen-sm-min) {
padding-left: 45px;
}
+
+ &.nested {
+ @media (min-width: $screen-sm-min) {
+ padding-left: 90px;
+ }
+ }
}
.project-repo-select {
@@ -876,7 +929,51 @@ pre.light-well {
.variables-table {
table-layout: fixed;
+ &.table-responsive {
+ border: none;
+ }
+
.variable-key {
- width: 30%;
+ width: 300px;
+ max-width: 300px;
+ overflow: hidden;
+ word-wrap: break-word;
+
+ // override bootstrap
+ white-space: normal!important;
+
+ @media (max-width: $screen-sm-max) {
+ width: 150px;
+ max-width: 150px;
+ }
+ }
+
+ .variable-value {
+ @media(max-width: $screen-xs-max) {
+ width: 150px;
+ max-width: 150px;
+ overflow: hidden;
+ word-wrap: break-word;
+ }
+ }
+}
+
+.services-installation-info .row {
+ margin-bottom: 10px;
+}
+
+.service-installation {
+ padding: 32px;
+ margin: 32px;
+ border-radius: 3px;
+ background-color: $white-light;
+
+ h3 {
+ margin-top: 0;
+ }
+
+ hr {
+ margin: 32px 0;
+ border-color: $border-color;
}
}
diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss
index cedd4cb2987..88ea92c5afb 100644
--- a/app/assets/stylesheets/pages/search.scss
+++ b/app/assets/stylesheets/pages/search.scss
@@ -14,6 +14,19 @@
}
}
+.search form:hover,
+.file-finder-input:hover,
+.issuable-search-form:hover,
+.search-text-input:hover,
+.form-control:hover {
+ border-color: lighten($dropdown-input-focus-border, 20%);
+ box-shadow: 0 0 4px lighten($search-input-focus-shadow-color, 20%);
+}
+
+input[type="checkbox"]:hover {
+ box-shadow: 0 0 2px 2px lighten($search-input-focus-shadow-color, 20%), 0 0 0 1px lighten($search-input-focus-shadow-color, 20%);
+}
+
.search {
margin-right: 10px;
margin-left: 10px;
diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss
index ddee2c95247..a28a87ed4f8 100644
--- a/app/assets/stylesheets/pages/settings.scss
+++ b/app/assets/stylesheets/pages/settings.scss
@@ -1,5 +1,5 @@
.settings-list-icon {
- color: $gl-gray-light;
+ color: $gl-text-color-secondary;
font-size: $settings-icon-size;
line-height: 42px;
}
diff --git a/app/assets/stylesheets/pages/status.scss b/app/assets/stylesheets/pages/status.scss
index a810ed32327..6f31d4ed789 100644
--- a/app/assets/stylesheets/pages/status.scss
+++ b/app/assets/stylesheets/pages/status.scss
@@ -1,6 +1,6 @@
.container-fluid {
.ci-status {
- padding: 2px 7px;
+ padding: 2px 7px 4px;
margin-right: 10px;
border: 1px solid $gray-darker;
white-space: nowrap;
@@ -19,7 +19,8 @@
overflow: visible;
}
- &.ci-failed {
+ &.ci-failed,
+ &.ci-failed_with_warnings {
color: $gl-danger;
border-color: $gl-danger;
@@ -61,15 +62,15 @@
&.ci-canceled,
&.ci-disabled {
- color: $gl-gray;
- border-color: $gl-gray;
+ color: $gl-text-color;
+ border-color: $gl-text-color;
&:not(span):hover {
- background-color: rgba($gl-gray, .07);
+ background-color: rgba($gl-text-color, .07);
}
svg {
- fill: $gl-gray;
+ fill: $gl-text-color;
}
}
@@ -101,15 +102,15 @@
&.ci-created,
&.ci-skipped {
- color: $gl-gray-light;
- border-color: $gl-gray-light;
+ color: $gl-text-color-secondary;
+ border-color: $gl-text-color-secondary;
&:not(span):hover {
- background-color: rgba($gl-gray-light, .07);
+ background-color: rgba($gl-text-color-secondary, .07);
}
svg {
- fill: $gl-gray-light;
+ fill: $gl-text-color-secondary;
}
}
@@ -135,3 +136,9 @@
left: 5px;
}
}
+
+.ci-status-link {
+ svg {
+ overflow: visible;
+ }
+}
diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss
index 508b30f3947..0d5604aae69 100644
--- a/app/assets/stylesheets/pages/todos.scss
+++ b/app/assets/stylesheets/pages/todos.scss
@@ -76,6 +76,10 @@
font-size: 14px;
}
+ .action-name {
+ font-weight: normal;
+ }
+
.todo-body {
.todo-note {
word-wrap: break-word;
@@ -90,7 +94,7 @@
}
p {
- color: $gl-text-color-dark;
+ color: $gl-text-color;
}
}
diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss
index cad4e149845..948921efc0b 100644
--- a/app/assets/stylesheets/pages/tree.scss
+++ b/app/assets/stylesheets/pages/tree.scss
@@ -32,6 +32,10 @@
.last-commit {
@include str-truncated(506px);
+ .fa-angle-right {
+ margin-left: 5px;
+ }
+
@media (min-width: $screen-md-min) and (max-width: $screen-md-max) {
@include str-truncated(450px);
}
@@ -78,7 +82,7 @@
i,
a {
- color: $gl-dark-link-color;
+ color: $gl-text-color;
}
img {
@@ -104,21 +108,21 @@
padding-right: 8px;
.commit-author-name {
- color: $gl-gray;
+ color: $gl-text-color;
}
}
.tree-time-ago {
min-width: 135px;
- color: $gl-gray-light;
+ color: $gl-text-color-secondary;
}
.tree-commit {
max-width: 320px;
- color: $gl-gray-light;
+ color: $gl-text-color-secondary;
.tree-commit-link {
- color: $gl-gray-light;
+ color: $gl-text-color-secondary;
&:hover {
text-decoration: underline;
@@ -134,21 +138,18 @@
.blob-commit-info {
list-style: none;
- padding: $gl-padding;
background: $gray-light;
+ padding: 6px 0;
border: 1px solid $border-color;
border-bottom: none;
margin: 0;
- .commit {
- padding-top: 0;
- padding-bottom: 0;
+ .table-list-cell {
+ border-bottom: none;
+ }
- .commit-row-title {
- .commit-row-message {
- font-weight: normal;
- }
- }
+ .commit-actions {
+ width: 200px;
}
}
diff --git a/app/assets/stylesheets/pages/wiki.scss b/app/assets/stylesheets/pages/wiki.scss
index b9f81533150..d5783e14b21 100644
--- a/app/assets/stylesheets/pages/wiki.scss
+++ b/app/assets/stylesheets/pages/wiki.scss
@@ -15,7 +15,7 @@
}
.wiki-last-edit-by {
- color: $gl-gray-light;
+ color: $gl-text-color-secondary;
strong {
color: $gl-text-color;
@@ -24,7 +24,7 @@
.light {
font-weight: normal;
- color: $gl-gray-light;
+ color: $gl-text-color-secondary;
}
.git-access-header {
diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb
index c2bb8464824..b0f5d4a9933 100644
--- a/app/controllers/admin/application_settings_controller.rb
+++ b/app/controllers/admin/application_settings_controller.rb
@@ -5,7 +5,11 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
end
def update
- if @application_setting.update_attributes(application_setting_params)
+ successful = ApplicationSettings::UpdateService
+ .new(@application_setting, current_user, application_setting_params)
+ .execute
+
+ if successful
redirect_to admin_application_settings_path,
notice: 'Application settings saved successfully'
else
@@ -67,69 +71,80 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
params.delete(:domain_blacklist_raw) if params[:domain_blacklist_file]
params.require(:application_setting).permit(
- :default_projects_limit,
- :default_branch_protection,
- :signup_enabled,
- :signin_enabled,
- :require_two_factor_authentication,
- :two_factor_grace_period,
- :gravatar_enabled,
- :sign_in_text,
- :after_sign_up_text,
- :help_page_text,
- :home_page_url,
+ application_setting_params_ce
+ )
+ end
+
+ def application_setting_params_ce
+ [
+ :admin_notification_email,
:after_sign_out_path,
- :max_attachment_size,
- :session_expire_delay,
+ :after_sign_up_text,
+ :akismet_api_key,
+ :akismet_enabled,
+ :container_registry_token_expire_delay,
+ :default_branch_protection,
+ :default_group_visibility,
:default_project_visibility,
+ :default_projects_limit,
:default_snippet_visibility,
- :default_group_visibility,
- :domain_whitelist_raw,
:domain_blacklist_enabled,
- :domain_blacklist_raw,
:domain_blacklist_file,
- :version_check_enabled,
- :admin_notification_email,
- :user_oauth_applications,
- :user_default_external,
- :shared_runners_enabled,
- :shared_runners_text,
+ :domain_blacklist_raw,
+ :domain_whitelist_raw,
+ :email_author_in_body,
+ :enabled_git_access_protocol,
+ :gravatar_enabled,
+ :help_page_text,
+ :home_page_url,
+ :housekeeping_bitmaps_enabled,
+ :housekeeping_enabled,
+ :housekeeping_full_repack_period,
+ :housekeeping_gc_period,
+ :housekeeping_incremental_repack_period,
+ :html_emails_enabled,
+ :koding_enabled,
+ :koding_url,
+ :plantuml_enabled,
+ :plantuml_url,
:max_artifacts_size,
+ :max_attachment_size,
+ :max_pages_size,
:metrics_enabled,
:metrics_host,
- :metrics_port,
- :metrics_pool_size,
- :metrics_timeout,
:metrics_method_call_threshold,
+ :metrics_packet_size,
+ :metrics_pool_size,
+ :metrics_port,
:metrics_sample_interval,
+ :metrics_timeout,
:recaptcha_enabled,
- :recaptcha_site_key,
:recaptcha_private_key,
- :sentry_enabled,
- :sentry_dsn,
- :akismet_enabled,
- :akismet_api_key,
- :koding_enabled,
- :koding_url,
- :email_author_in_body,
- :html_emails_enabled,
+ :recaptcha_site_key,
:repository_checks_enabled,
- :metrics_packet_size,
+ :require_two_factor_authentication,
+ :session_expire_delay,
+ :sign_in_text,
+ :signin_enabled,
+ :signup_enabled,
+ :sentry_dsn,
+ :sentry_enabled,
:send_user_confirmation_email,
- :container_registry_token_expire_delay,
- :enabled_git_access_protocol,
+ :shared_runners_enabled,
+ :shared_runners_text,
:sidekiq_throttling_enabled,
:sidekiq_throttling_factor,
- :housekeeping_enabled,
- :housekeeping_bitmaps_enabled,
- :housekeeping_incremental_repack_period,
- :housekeeping_full_repack_period,
- :housekeeping_gc_period,
+ :two_factor_grace_period,
+ :user_default_external,
+ :user_oauth_applications,
+ :version_check_enabled,
+ :terminal_max_session_time,
+
+ disabled_oauth_sign_in_sources: [],
+ import_sources: [],
repository_storages: [],
restricted_visibility_levels: [],
- import_sources: [],
- disabled_oauth_sign_in_sources: [],
sidekiq_throttling_queues: []
- )
+ ]
end
end
diff --git a/app/controllers/admin/deploy_keys_controller.rb b/app/controllers/admin/deploy_keys_controller.rb
index 285e8495342..4f6a7e9e2cb 100644
--- a/app/controllers/admin/deploy_keys_controller.rb
+++ b/app/controllers/admin/deploy_keys_controller.rb
@@ -10,7 +10,7 @@ class Admin::DeployKeysController < Admin::ApplicationController
end
def create
- @deploy_key = deploy_keys.new(deploy_key_params)
+ @deploy_key = deploy_keys.new(deploy_key_params.merge(user: current_user))
if @deploy_key.save
redirect_to admin_deploy_keys_path
@@ -39,6 +39,6 @@ class Admin::DeployKeysController < Admin::ApplicationController
end
def deploy_key_params
- params.require(:deploy_key).permit(:key, :title)
+ params.require(:deploy_key).permit(:key, :title, :can_push)
end
end
diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb
index 1e3d194e9f9..b7722a1d15d 100644
--- a/app/controllers/admin/groups_controller.rb
+++ b/app/controllers/admin/groups_controller.rb
@@ -1,17 +1,18 @@
class Admin::GroupsController < Admin::ApplicationController
- before_action :group, only: [:edit, :show, :update, :destroy, :project_update, :members_update]
+ before_action :group, only: [:edit, :update, :destroy, :project_update, :members_update]
def index
- @groups = Group.all
+ @groups = Group.with_statistics
@groups = @groups.sort(@sort = params[:sort])
@groups = @groups.search(params[:name]) if params[:name].present?
@groups = @groups.page(params[:page])
end
def show
+ @group = Group.with_statistics.joins(:route).group('routes.path').find_by_full_path(params[:id])
@members = @group.members.order("access_level DESC").page(params[:members_page])
@requesters = AccessRequestsFinder.new(@group).execute(current_user)
- @projects = @group.projects.page(params[:projects_page])
+ @projects = @group.projects.with_statistics.page(params[:projects_page])
end
def new
@@ -60,7 +61,11 @@ class Admin::GroupsController < Admin::ApplicationController
end
def group_params
- params.require(:group).permit(
+ params.require(:group).permit(group_params_ce)
+ end
+
+ def group_params_ce
+ [
:avatar,
:description,
:lfs_enabled,
@@ -68,6 +73,6 @@ class Admin::GroupsController < Admin::ApplicationController
:path,
:request_access_enabled,
:visibility_level
- )
+ ]
end
end
diff --git a/app/controllers/admin/projects_controller.rb b/app/controllers/admin/projects_controller.rb
index 1d963bdf7d5..39c8c6d8a0c 100644
--- a/app/controllers/admin/projects_controller.rb
+++ b/app/controllers/admin/projects_controller.rb
@@ -3,7 +3,7 @@ class Admin::ProjectsController < Admin::ApplicationController
before_action :group, only: [:show, :transfer]
def index
- @projects = Project.all
+ @projects = Project.with_statistics
@projects = @projects.in_namespace(params[:namespace_id]) if params[:namespace_id].present?
@projects = @projects.where(visibility_level: params[:visibility_level]) if params[:visibility_level].present?
@projects = @projects.with_push if params[:with_push].present?
@@ -45,7 +45,7 @@ class Admin::ProjectsController < Admin::ApplicationController
protected
def project
- @project = Project.find_with_namespace(
+ @project = Project.find_by_full_path(
[params[:namespace_id], '/', params[:id]].join('')
)
@project || render_404
diff --git a/app/controllers/admin/runner_projects_controller.rb b/app/controllers/admin/runner_projects_controller.rb
index bc65dcc33d3..70ac6a75434 100644
--- a/app/controllers/admin/runner_projects_controller.rb
+++ b/app/controllers/admin/runner_projects_controller.rb
@@ -24,7 +24,7 @@ class Admin::RunnerProjectsController < Admin::ApplicationController
private
def project
- @project = Project.find_with_namespace(
+ @project = Project.find_by_full_path(
[params[:namespace_id], '/', params[:project_id]].join('')
)
@project || render_404
diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb
index bb912ed10cc..1cd50852e89 100644
--- a/app/controllers/admin/users_controller.rb
+++ b/app/controllers/admin/users_controller.rb
@@ -16,9 +16,6 @@ class Admin::UsersController < Admin::ApplicationController
@joined_projects = user.projects.joined(@user)
end
- def groups
- end
-
def keys
@keys = user.keys
end
@@ -164,15 +161,6 @@ class Admin::UsersController < Admin::ApplicationController
@user ||= User.find_by!(username: params[:id])
end
- def user_params
- params.require(:user).permit(
- :email, :remember_me, :bio, :name, :username,
- :skype, :linkedin, :twitter, :website_url, :color_scheme_id, :theme_id, :force_random_password,
- :extern_uid, :provider, :password_expires_at, :avatar, :hide_no_ssh_key, :hide_no_password,
- :projects_limit, :can_create_group, :admin, :key_id, :external
- )
- end
-
def redirect_back_or_admin_user(options = {})
redirect_back_or_default(default: default_route, options: options)
end
@@ -180,4 +168,36 @@ class Admin::UsersController < Admin::ApplicationController
def default_route
[:admin, @user]
end
+
+ def user_params
+ params.require(:user).permit(user_params_ce)
+ end
+
+ def user_params_ce
+ [
+ :access_level,
+ :avatar,
+ :bio,
+ :can_create_group,
+ :color_scheme_id,
+ :email,
+ :extern_uid,
+ :external,
+ :force_random_password,
+ :hide_no_password,
+ :hide_no_ssh_key,
+ :key_id,
+ :linkedin,
+ :name,
+ :password_expires_at,
+ :projects_limit,
+ :provider,
+ :remember_me,
+ :skype,
+ :theme_id,
+ :twitter,
+ :username,
+ :website_url
+ ]
+ end
end
diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb
index 5f13353baa1..d7a45bacd35 100644
--- a/app/controllers/autocomplete_controller.rb
+++ b/app/controllers/autocomplete_controller.rb
@@ -18,15 +18,14 @@ class AutocompleteController < ApplicationController
if params[:search].blank?
# Include current user if available to filter by "Me"
if params[:current_user].present? && current_user
- @users = [*@users, current_user]
+ @users = @users.where.not(id: current_user.id)
+ @users = [current_user, *@users]
end
if params[:author_id].present?
author = User.find_by_id(params[:author_id])
- @users = [author, *@users] if author
+ @users = [author, *@users].uniq if author
end
-
- @users.uniq!
end
render json: @users, only: [:name, :username, :id], methods: [:avatar_url]
diff --git a/app/controllers/concerns/creates_commit.rb b/app/controllers/concerns/creates_commit.rb
index 936d9bab57e..6286d67d30c 100644
--- a/app/controllers/concerns/creates_commit.rb
+++ b/app/controllers/concerns/creates_commit.rb
@@ -4,13 +4,15 @@ module CreatesCommit
def create_commit(service, success_path:, failure_path:, failure_view: nil, success_notice: nil)
set_commit_variables
+ start_branch = @mr_target_branch unless initial_commit?
commit_params = @commit_params.merge(
- source_project: @project,
- source_branch: @ref,
- target_branch: @target_branch
+ start_project: @mr_target_project,
+ start_branch: start_branch,
+ target_branch: @mr_source_branch
)
- result = service.new(@tree_edit_project, current_user, commit_params).execute
+ result = service.new(
+ @mr_source_project, current_user, commit_params).execute
if result[:status] == :success
update_flash_notice(success_notice)
@@ -82,27 +84,25 @@ module CreatesCommit
return @merge_request if defined?(@merge_request)
@merge_request = MergeRequestsFinder.new(current_user, project_id: @mr_target_project.id).execute.opened.
- find_by(source_branch: @mr_source_branch, target_branch: @mr_target_branch)
+ find_by(source_branch: @mr_source_branch, target_branch: @mr_target_branch, source_project_id: @mr_source_project)
end
def different_project?
@mr_source_project != @mr_target_project
end
- def different_branch?
- @mr_source_branch != @mr_target_branch || different_project?
- end
-
def create_merge_request?
- params[:create_merge_request].present? && different_branch?
+ # XXX: Even if the field is set, if we're checking the same branch
+ # as the target branch in the same project,
+ # we don't want to create a merge request.
+ params[:create_merge_request].present? &&
+ (different_project? || @ref != @target_branch)
end
+ # TODO: We should really clean this up
def set_commit_variables
- @mr_source_branch ||= @target_branch
-
if can?(current_user, :push_code, @project)
# Edit file in this project
- @tree_edit_project = @project
@mr_source_project = @project
if @project.forked?
@@ -112,15 +112,34 @@ module CreatesCommit
else
# Merge request to this project
@mr_target_project = @project
- @mr_target_branch ||= @ref
+ @mr_target_branch = @ref || @target_branch
end
else
- # Edit file in fork
- @tree_edit_project = current_user.fork_of(@project)
# Merge request from fork to this project
- @mr_source_project = @tree_edit_project
+ @mr_source_project = current_user.fork_of(@project)
@mr_target_project = @project
- @mr_target_branch ||= @ref
+ @mr_target_branch = @ref || @target_branch
end
+
+ @mr_source_branch = guess_mr_source_branch
+ end
+
+ def initial_commit?
+ @mr_target_branch.nil? ||
+ !@mr_target_project.repository.branch_exists?(@mr_target_branch)
+ end
+
+ def guess_mr_source_branch
+ # XXX: Happens when viewing a commit without a branch. In this case,
+ # @target_branch would be the default branch for @mr_source_project,
+ # however we want a generated new branch here. Thus we can't use
+ # @target_branch, but should pass nil to indicate that we want a new
+ # branch instead of @target_branch.
+ return if
+ create_merge_request? &&
+ # XXX: Don't understand why rubocop prefers this indention
+ @mr_source_project.repository.branch_exists?(@target_branch)
+
+ @target_branch
end
end
diff --git a/app/controllers/concerns/cycle_analytics_params.rb b/app/controllers/concerns/cycle_analytics_params.rb
index 2aaf8f2b451..52e06f4945a 100644
--- a/app/controllers/concerns/cycle_analytics_params.rb
+++ b/app/controllers/concerns/cycle_analytics_params.rb
@@ -1,6 +1,10 @@
module CycleAnalyticsParams
extend ActiveSupport::Concern
+ def options(params)
+ @options ||= { from: start_date(params), current_user: current_user }
+ end
+
def start_date(params)
params[:start_date] == '30' ? 30.days.ago : 90.days.ago
end
diff --git a/app/controllers/concerns/global_milestones.rb b/app/controllers/concerns/global_milestones.rb
deleted file mode 100644
index 5c503c5b698..00000000000
--- a/app/controllers/concerns/global_milestones.rb
+++ /dev/null
@@ -1,20 +0,0 @@
-module GlobalMilestones
- extend ActiveSupport::Concern
-
- def milestones
- epoch = DateTime.parse('1970-01-01')
- @milestones = MilestonesFinder.new.execute(@projects, params)
- @milestones = GlobalMilestone.build_collection(@milestones)
- @milestones = @milestones.sort_by { |x| x.due_date.nil? ? epoch : x.due_date }
- end
-
- def milestone
- milestones = Milestone.of_projects(@projects).where(title: params[:title])
-
- if milestones.present?
- @milestone = GlobalMilestone.new(params[:title], milestones)
- else
- render_404
- end
- end
-end
diff --git a/app/controllers/concerns/service_params.rb b/app/controllers/concerns/service_params.rb
index 549a8526715..d7f5a4e4682 100644
--- a/app/controllers/concerns/service_params.rb
+++ b/app/controllers/concerns/service_params.rb
@@ -1,31 +1,72 @@
module ServiceParams
extend ActiveSupport::Concern
- ALLOWED_PARAMS = [:title, :token, :type, :active, :api_key, :api_url, :api_version, :subdomain,
- :room, :recipients, :project_url, :webhook,
- :user_key, :device, :priority, :sound, :bamboo_url, :username, :password,
- :build_key, :server, :teamcity_url, :drone_url, :build_type,
- :description, :issues_url, :new_issue_url, :restrict_to_branch, :channel,
- :colorize_messages, :channels,
- # We're using `issues_events` and `merge_requests_events`
- # in the view so we still need to explicitly state them
- # here. `Service#event_names` would only give
- # `issue_events` and `merge_request_events` (singular!)
- # See app/helpers/services_helper.rb for how we
- # make those event names plural as special case.
- :issues_events, :confidential_issues_events, :merge_requests_events,
- :notify_only_broken_builds, :notify_only_broken_pipelines,
- :add_pusher, :send_from_committer_email, :disable_diffs,
- :external_wiki_url, :notify, :color,
- :server_host, :server_port, :default_irc_uri, :enable_ssl_verification,
- :jira_issue_transition_id, :url, :project_key, :ca_pem, :namespace]
+ ALLOWED_PARAMS_CE = [
+ :active,
+ :add_pusher,
+ :api_key,
+ :api_url,
+ :api_version,
+ :bamboo_url,
+ :build_key,
+ :build_type,
+ :ca_pem,
+ :channel,
+ :channels,
+ :color,
+ :colorize_messages,
+ :confidential_issues_events,
+ :default_irc_uri,
+ :description,
+ :device,
+ :disable_diffs,
+ :drone_url,
+ :enable_ssl_verification,
+ :external_wiki_url,
+ # We're using `issues_events` and `merge_requests_events`
+ # in the view so we still need to explicitly state them
+ # here. `Service#event_names` would only give
+ # `issue_events` and `merge_request_events` (singular!)
+ # See app/helpers/services_helper.rb for how we
+ # make those event names plural as special case.
+ :issues_events,
+ :issues_url,
+ :jira_issue_transition_id,
+ :merge_requests_events,
+ :namespace,
+ :new_issue_url,
+ :notify,
+ :notify_only_broken_builds,
+ :notify_only_broken_pipelines,
+ :password,
+ :priority,
+ :project_key,
+ :project_url,
+ :recipients,
+ :restrict_to_branch,
+ :room,
+ :send_from_committer_email,
+ :server,
+ :server_host,
+ :server_port,
+ :sound,
+ :subdomain,
+ :teamcity_url,
+ :title,
+ :token,
+ :type,
+ :url,
+ :user_key,
+ :username,
+ :webhook
+ ]
# Parameters to ignore if no value is specified
FILTER_BLANK_PARAMS = [:password]
def service_params
dynamic_params = @service.event_channel_names + @service.event_names
- service_params = params.permit(:id, service: ALLOWED_PARAMS + dynamic_params)
+ service_params = params.permit(:id, service: ALLOWED_PARAMS_CE + dynamic_params)
if service_params[:service].is_a?(Hash)
FILTER_BLANK_PARAMS.each do |param|
diff --git a/app/controllers/concerns/spammable_actions.rb b/app/controllers/concerns/spammable_actions.rb
index 99acd98ae13..a6891149bfa 100644
--- a/app/controllers/concerns/spammable_actions.rb
+++ b/app/controllers/concerns/spammable_actions.rb
@@ -1,13 +1,15 @@
module SpammableActions
extend ActiveSupport::Concern
+ include Recaptcha::Verify
+
included do
before_action :authorize_submit_spammable!, only: :mark_as_spam
end
def mark_as_spam
if SpamService.new(spammable).mark_as_spam!
- redirect_to spammable, notice: "#{spammable.class} was submitted to Akismet successfully."
+ redirect_to spammable, notice: "#{spammable.spammable_entity_type.titlecase} was submitted to Akismet successfully."
else
redirect_to spammable, alert: 'Error with Akismet. Please check the logs for more info.'
end
@@ -15,6 +17,15 @@ module SpammableActions
private
+ def recaptcha_params
+ return {} unless params[:recaptcha_verification] && Gitlab::Recaptcha.load_configurations! && verify_recaptcha
+
+ {
+ recaptcha_verified: true,
+ spam_log_id: params[:spam_log_id]
+ }
+ end
+
def spammable
raise NotImplementedError, "#{self.class} does not implement #{__method__}"
end
@@ -22,4 +33,11 @@ module SpammableActions
def authorize_submit_spammable!
access_denied! unless current_user.admin?
end
+
+ def render_recaptcha?
+ return false if spammable.errors.count > 1 # re-render "new" template in case there are other errors
+ return false unless Gitlab::Recaptcha.enabled?
+
+ spammable.spam
+ end
end
diff --git a/app/controllers/confirmations_controller.rb b/app/controllers/confirmations_controller.rb
index 3da44b9b888..306afb65f10 100644
--- a/app/controllers/confirmations_controller.rb
+++ b/app/controllers/confirmations_controller.rb
@@ -14,12 +14,8 @@ class ConfirmationsController < Devise::ConfirmationsController
if signed_in?(resource_name)
after_sign_in_path_for(resource)
else
- sign_in(resource)
- if signed_in?(resource_name)
- after_sign_in_path_for(resource)
- else
- new_session_path(resource_name)
- end
+ flash[:notice] += " Please sign in."
+ new_session_path(resource_name)
end
end
end
diff --git a/app/controllers/dashboard/milestones_controller.rb b/app/controllers/dashboard/milestones_controller.rb
index fa9c6c054f0..7f506db583f 100644
--- a/app/controllers/dashboard/milestones_controller.rb
+++ b/app/controllers/dashboard/milestones_controller.rb
@@ -1,6 +1,4 @@
class Dashboard::MilestonesController < Dashboard::ApplicationController
- include GlobalMilestones
-
before_action :projects
before_action :milestone, only: [:show]
@@ -17,4 +15,15 @@ class Dashboard::MilestonesController < Dashboard::ApplicationController
def show
end
+
+ private
+
+ def milestones
+ @milestones = DashboardMilestone.build_collection(@projects, params)
+ end
+
+ def milestone
+ @milestone = DashboardMilestone.build(@projects, params[:title])
+ render_404 unless @milestone
+ end
end
diff --git a/app/controllers/dashboard/projects_controller.rb b/app/controllers/dashboard/projects_controller.rb
index c08eb811532..3ba8c2f8bb9 100644
--- a/app/controllers/dashboard/projects_controller.rb
+++ b/app/controllers/dashboard/projects_controller.rb
@@ -10,10 +10,8 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
@projects = @projects.sort(@sort = params[:sort])
@projects = @projects.page(params[:page])
- @last_push = current_user.recent_push
-
respond_to do |format|
- format.html
+ format.html { @last_push = current_user.recent_push }
format.atom do
event_filter
load_events
diff --git a/app/controllers/dashboard/todos_controller.rb b/app/controllers/dashboard/todos_controller.rb
index d425d0f9014..e3933e3d7b1 100644
--- a/app/controllers/dashboard/todos_controller.rb
+++ b/app/controllers/dashboard/todos_controller.rb
@@ -4,6 +4,9 @@ class Dashboard::TodosController < Dashboard::ApplicationController
def index
@sort = params[:sort]
@todos = @todos.page(params[:page])
+ if @todos.out_of_range? && @todos.total_pages != 0
+ redirect_to url_for(params.merge(page: @todos.total_pages))
+ end
end
def destroy
diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb
index 4dda4e51f6a..79d420a32d3 100644
--- a/app/controllers/dashboard_controller.rb
+++ b/app/controllers/dashboard_controller.rb
@@ -4,6 +4,7 @@ class DashboardController < Dashboard::ApplicationController
before_action :event_filter, only: :activity
before_action :projects, only: [:issues, :merge_requests]
+ before_action :set_show_full_reference, only: [:issues, :merge_requests]
respond_to :html
@@ -34,4 +35,8 @@ class DashboardController < Dashboard::ApplicationController
@events = @event_filter.apply_filter(@events).with_associations
@events = @events.limit(20).offset(params[:offset] || 0)
end
+
+ def set_show_full_reference
+ @show_full_reference = true
+ end
end
diff --git a/app/controllers/explore/projects_controller.rb b/app/controllers/explore/projects_controller.rb
index a62c6211372..26e17a7553e 100644
--- a/app/controllers/explore/projects_controller.rb
+++ b/app/controllers/explore/projects_controller.rb
@@ -22,6 +22,7 @@ class Explore::ProjectsController < Explore::ApplicationController
def trending
@projects = filter_projects(Project.trending)
+ @projects = @projects.sort(@sort = params[:sort])
@projects = @projects.page(params[:page])
respond_to do |format|
diff --git a/app/controllers/groups/milestones_controller.rb b/app/controllers/groups/milestones_controller.rb
index 24ec4eec3f2..0d872c86c8a 100644
--- a/app/controllers/groups/milestones_controller.rb
+++ b/app/controllers/groups/milestones_controller.rb
@@ -1,6 +1,4 @@
class Groups::MilestonesController < Groups::ApplicationController
- include GlobalMilestones
-
before_action :group_projects
before_action :milestone, only: [:show, :update]
before_action :authorize_admin_milestones!, only: [:new, :create, :update]
@@ -73,4 +71,13 @@ class Groups::MilestonesController < Groups::ApplicationController
def milestone_path(title)
group_milestone_path(@group, title.to_slug.to_s, title: title)
end
+
+ def milestones
+ @milestones = GroupMilestone.build_collection(@group, @projects, params)
+ end
+
+ def milestone
+ @milestone = GroupMilestone.build(@group, @projects, params[:title])
+ render_404 unless @milestone
+ end
end
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index b83c3a872cf..264b14713fb 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -42,6 +42,8 @@ class GroupsController < Groups::ApplicationController
@notification_setting = current_user.notification_settings_for(group)
end
+ @nested_groups = group.children
+
setup_projects
respond_to do |format|
@@ -75,13 +77,15 @@ class GroupsController < Groups::ApplicationController
end
def projects
- @projects = @group.projects.page(params[:page])
+ @projects = @group.projects.with_statistics.page(params[:page])
end
def update
if Groups::UpdateService.new(@group, current_user, group_params).execute
redirect_to edit_group_path(@group), notice: "Group '#{@group.name}' was successfully updated."
else
+ @group.restore_path!
+
render action: "edit"
end
end
@@ -121,7 +125,11 @@ class GroupsController < Groups::ApplicationController
end
def group_params
- params.require(:group).permit(
+ params.require(:group).permit(group_params_ce)
+ end
+
+ def group_params_ce
+ [
:avatar,
:description,
:lfs_enabled,
@@ -131,7 +139,7 @@ class GroupsController < Groups::ApplicationController
:request_access_enabled,
:share_with_group_lock,
:visibility_level
- )
+ ]
end
def load_events
diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb
index b2ff36f6538..db33b60b229 100644
--- a/app/controllers/projects/application_controller.rb
+++ b/app/controllers/projects/application_controller.rb
@@ -18,13 +18,13 @@ class Projects::ApplicationController < ApplicationController
# to
# localhost/group/project
#
- if id =~ /\.git\Z/
+ if params[:format] == 'git'
redirect_to request.original_url.gsub(/\.git\/?\Z/, '')
return
end
project_path = "#{namespace}/#{id}"
- @project = Project.find_with_namespace(project_path)
+ @project = Project.find_by_full_path(project_path)
if can?(current_user, :read_project, @project) && !@project.pending_delete?
if @project.path_with_namespace != project_path
diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb
index 9940263ae24..4c39fe98028 100644
--- a/app/controllers/projects/blob_controller.rb
+++ b/app/controllers/projects/blob_controller.rb
@@ -30,6 +30,8 @@ class Projects::BlobController < Projects::ApplicationController
end
def show
+ environment_params = @repository.branch_exists?(@ref) ? { ref: @ref } : { commit: @commit }
+ @environment = EnvironmentsFinder.new(@project, current_user, environment_params).execute.last
end
def edit
diff --git a/app/controllers/projects/boards/issues_controller.rb b/app/controllers/projects/boards/issues_controller.rb
index dc33e1405f2..61fef4dc133 100644
--- a/app/controllers/projects/boards/issues_controller.rb
+++ b/app/controllers/projects/boards/issues_controller.rb
@@ -7,7 +7,7 @@ module Projects
def index
issues = ::Boards::Issues::ListService.new(project, current_user, filter_params).execute
- issues = issues.page(params[:page])
+ issues = issues.page(params[:page]).per(params[:per] || 20)
render json: {
issues: serialize_as_json(issues),
@@ -59,7 +59,7 @@ module Projects
end
def filter_params
- params.merge(board_id: params[:board_id], id: params[:list_id])
+ params.merge(board_id: params[:board_id], id: params[:list_id]).compact
end
def move_params
@@ -73,7 +73,7 @@ module Projects
def serialize_as_json(resource)
resource.as_json(
labels: true,
- only: [:iid, :title, :confidential, :due_date],
+ only: [:id, :iid, :title, :confidential, :due_date],
include: {
assignee: { only: [:id, :name, :username], methods: [:avatar_url] },
milestone: { only: [:id, :title] }
diff --git a/app/controllers/projects/builds_controller.rb b/app/controllers/projects/builds_controller.rb
index fbe391fc58c..886934a3f67 100644
--- a/app/controllers/projects/builds_controller.rb
+++ b/app/controllers/projects/builds_controller.rb
@@ -94,7 +94,7 @@ class Projects::BuildsController < Projects::ApplicationController
private
def build
- @build ||= project.builds.find_by!(id: params[:id])
+ @build ||= project.builds.find_by!(id: params[:id]).present(current_user: current_user)
end
def build_path(build)
diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb
index 8197d9e4c99..e10d7992db7 100644
--- a/app/controllers/projects/commit_controller.rb
+++ b/app/controllers/projects/commit_controller.rb
@@ -8,13 +8,10 @@ class Projects::CommitController < Projects::ApplicationController
# Authorize
before_action :require_non_empty_project
- before_action :authorize_download_code!, except: [:cancel_builds, :retry_builds]
- before_action :authorize_update_build!, only: [:cancel_builds, :retry_builds]
+ before_action :authorize_download_code!
before_action :authorize_read_pipeline!, only: [:pipelines]
- before_action :authorize_read_commit_status!, only: [:builds]
before_action :commit
- before_action :define_commit_vars, only: [:show, :diff_for_path, :builds, :pipelines]
- before_action :define_status_vars, only: [:show, :builds, :pipelines]
+ before_action :define_commit_vars, only: [:show, :diff_for_path, :pipelines]
before_action :define_note_vars, only: [:show, :diff_for_path]
before_action :authorize_edit_tree!, only: [:revert, :cherry_pick]
@@ -33,25 +30,16 @@ class Projects::CommitController < Projects::ApplicationController
end
def pipelines
- end
-
- def builds
- end
-
- def cancel_builds
- ci_builds.running_or_pending.each(&:cancel)
+ @pipelines = @commit.pipelines.order(id: :desc)
- redirect_back_or_default default: builds_namespace_project_commit_path(project.namespace, project, commit.sha)
- end
-
- def retry_builds
- ci_builds.latest.failed.each do |build|
- if build.retryable?
- Ci::Build.retry(build, current_user)
+ respond_to do |format|
+ format.html
+ format.json do
+ render json: PipelineSerializer
+ .new(project: @project, user: @current_user)
+ .represent(@pipelines)
end
end
-
- redirect_back_or_default default: builds_namespace_project_commit_path(project.namespace, project, commit.sha)
end
def branches
@@ -61,7 +49,7 @@ class Projects::CommitController < Projects::ApplicationController
end
def revert
- assign_change_commit_vars(@commit.revert_branch_name)
+ assign_change_commit_vars
return render_404 if @target_branch.blank?
@@ -70,7 +58,7 @@ class Projects::CommitController < Projects::ApplicationController
end
def cherry_pick
- assign_change_commit_vars(@commit.cherry_pick_branch_name)
+ assign_change_commit_vars
return render_404 if @target_branch.blank?
@@ -98,10 +86,6 @@ class Projects::CommitController < Projects::ApplicationController
@noteable = @commit ||= @project.commit(params[:id])
end
- def ci_builds
- @ci_builds ||= Ci::Build.where(pipeline: pipelines)
- end
-
def define_commit_vars
return git_not_found! unless commit
@@ -110,6 +94,8 @@ class Projects::CommitController < Projects::ApplicationController
@diffs = commit.diffs(opts)
@notes_count = commit.notes.count
+
+ @environment = EnvironmentsFinder.new(@project, current_user, commit: @commit).execute.last
end
def define_note_vars
@@ -131,17 +117,9 @@ class Projects::CommitController < Projects::ApplicationController
}
end
- def define_status_vars
- @ci_pipelines = project.pipelines.where(sha: commit.sha)
- @statuses = CommitStatus.where(pipeline: @ci_pipelines).relevant
- @builds = Ci::Build.where(pipeline: @ci_pipelines).relevant
- end
-
- def assign_change_commit_vars(mr_source_branch)
+ def assign_change_commit_vars
@commit = project.commit(params[:id])
@target_branch = params[:target_branch]
- @mr_source_branch = mr_source_branch
- @mr_target_branch = @target_branch
@commit_params = {
commit: @commit,
create_merge_request: params[:create_merge_request].present? || different_project?
diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb
index ec02fc15d35..c6651254d70 100644
--- a/app/controllers/projects/compare_controller.rb
+++ b/app/controllers/projects/compare_controller.rb
@@ -25,8 +25,17 @@ class Projects::CompareController < Projects::ApplicationController
end
def create
- redirect_to namespace_project_compare_path(@project.namespace, @project,
+ if params[:from].blank? || params[:to].blank?
+ flash[:alert] = "You must select from and to branches"
+ from_to_vars = {
+ from: params[:from].presence,
+ to: params[:to].presence
+ }
+ redirect_to namespace_project_compare_index_path(@project.namespace, @project, from_to_vars)
+ else
+ redirect_to namespace_project_compare_path(@project.namespace, @project,
params[:from], params[:to])
+ end
end
private
@@ -37,7 +46,8 @@ class Projects::CompareController < Projects::ApplicationController
end
def define_diff_vars
- @compare = CompareService.new.execute(@project, @head_ref, @project, @start_ref)
+ @compare = CompareService.new(@project, @head_ref)
+ .execute(@project, @start_ref)
if @compare
@commits = @compare.commits
@@ -47,6 +57,9 @@ class Projects::CompareController < Projects::ApplicationController
@diffs = @compare.diffs(diff_options)
+ environment_params = @repository.branch_exists?(@head_ref) ? { ref: @head_ref } : { commit: @commit }
+ @environment = EnvironmentsFinder.new(@project, current_user, environment_params).execute.last
+
@diff_notes_disabled = true
@grouped_diff_discussions = {}
end
diff --git a/app/controllers/projects/cycle_analytics/events_controller.rb b/app/controllers/projects/cycle_analytics/events_controller.rb
index 13b3eec761f..b69d46f2c41 100644
--- a/app/controllers/projects/cycle_analytics/events_controller.rb
+++ b/app/controllers/projects/cycle_analytics/events_controller.rb
@@ -9,56 +9,52 @@ module Projects
before_action :authorize_read_merge_request!, only: [:code, :review]
def issue
- render_events(events.issue_events)
+ render_events(cycle_analytics[:issue].events)
end
def plan
- render_events(events.plan_events)
+ render_events(cycle_analytics[:plan].events)
end
def code
- render_events(events.code_events)
+ render_events(cycle_analytics[:code].events)
end
def test
- options[:branch] = events_params[:branch_name]
+ options(events_params)[:branch] = events_params[:branch_name]
- render_events(events.test_events)
+ render_events(cycle_analytics[:test].events)
end
def review
- render_events(events.review_events)
+ render_events(cycle_analytics[:review].events)
end
def staging
- render_events(events.staging_events)
+ render_events(cycle_analytics[:staging].events)
end
def production
- render_events(events.production_events)
+ render_events(cycle_analytics[:production].events)
end
private
-
- def render_events(events_list)
+
+ def render_events(events)
respond_to do |format|
format.html
- format.json { render json: { events: events_list } }
+ format.json { render json: { events: events } }
end
end
- def events
- @events ||= Gitlab::CycleAnalytics::Events.new(project: project, options: options)
- end
-
- def options
- @options ||= { from: start_date(events_params), current_user: current_user }
+ def cycle_analytics
+ @cycle_analytics ||= ::CycleAnalytics.new(project, options(events_params))
end
def events_params
return {} unless params[:events].present?
- params[:events].slice(:start_date, :branch_name)
+ params[:events].permit(:start_date, :branch_name)
end
end
end
diff --git a/app/controllers/projects/cycle_analytics_controller.rb b/app/controllers/projects/cycle_analytics_controller.rb
index ac639ef015b..88ac3ad046b 100644
--- a/app/controllers/projects/cycle_analytics_controller.rb
+++ b/app/controllers/projects/cycle_analytics_controller.rb
@@ -6,11 +6,9 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController
before_action :authorize_read_cycle_analytics!
def show
- @cycle_analytics = ::CycleAnalytics.new(@project, current_user, from: start_date(cycle_analytics_params))
+ @cycle_analytics = ::CycleAnalytics.new(@project, options(cycle_analytics_params))
- stats_values, cycle_analytics_json = generate_cycle_analytics_data
-
- @cycle_analytics_no_data = stats_values.blank?
+ @cycle_analytics_no_data = @cycle_analytics.no_stats?
respond_to do |format|
format.html
@@ -23,50 +21,14 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController
def cycle_analytics_params
return {} unless params[:cycle_analytics].present?
- { start_date: params[:cycle_analytics][:start_date] }
+ params[:cycle_analytics].permit(:start_date)
end
- def generate_cycle_analytics_data
- stats_values = []
-
- cycle_analytics_view_data = [[:issue, "Issue", "Related Issues", "Time before an issue gets scheduled"],
- [:plan, "Plan", "Related Commits", "Time before an issue starts implementation"],
- [:code, "Code", "Related Merge Requests", "Time spent coding"],
- [:test, "Test", "Relative Builds Trigger by Commits", "The time taken to build and test the application"],
- [:review, "Review", "Relative Merged Requests", "The time taken to review the code"],
- [:staging, "Staging", "Relative Deployed Builds", "The time taken in staging"],
- [:production, "Production", "Related Issues", "The total time taken from idea to production"]]
-
- stats = cycle_analytics_view_data.reduce([]) do |stats, (stage_method, stage_text, stage_legend, stage_description)|
- value = @cycle_analytics.send(stage_method).presence
-
- stats_values << value.abs if value
-
- stats << {
- title: stage_text,
- description: stage_description,
- legend: stage_legend,
- value: value && !value.zero? ? distance_of_time_in_words(value) : nil
- }
-
- stats
- end
-
- issues = @cycle_analytics.summary.new_issues
- commits = @cycle_analytics.summary.commits
- deploys = @cycle_analytics.summary.deploys
-
- summary = [
- { title: "New Issue".pluralize(issues), value: issues },
- { title: "Commit".pluralize(commits), value: commits },
- { title: "Deploy".pluralize(deploys), value: deploys }
- ]
-
- cycle_analytics_hash = { summary: summary,
- stats: stats,
- permissions: @cycle_analytics.permissions(user: current_user)
+ def cycle_analytics_json
+ {
+ summary: @cycle_analytics.summary,
+ stats: @cycle_analytics.stats,
+ permissions: @cycle_analytics.permissions(user: current_user)
}
-
- [stats_values, cycle_analytics_hash]
end
end
diff --git a/app/controllers/projects/deploy_keys_controller.rb b/app/controllers/projects/deploy_keys_controller.rb
index 529e0aa2d33..b094491e006 100644
--- a/app/controllers/projects/deploy_keys_controller.rb
+++ b/app/controllers/projects/deploy_keys_controller.rb
@@ -16,7 +16,7 @@ class Projects::DeployKeysController < Projects::ApplicationController
end
def create
- @key = DeployKey.new(deploy_key_params)
+ @key = DeployKey.new(deploy_key_params.merge(user: current_user))
set_index_vars
if @key.valid? && @project.deploy_keys << @key
@@ -53,6 +53,6 @@ class Projects::DeployKeysController < Projects::ApplicationController
end
def deploy_key_params
- params.require(:deploy_key).permit(:key, :title)
+ params.require(:deploy_key).permit(:key, :title, :can_push)
end
end
diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb
index 87cc36253f1..0ec8f5bd64a 100644
--- a/app/controllers/projects/environments_controller.rb
+++ b/app/controllers/projects/environments_controller.rb
@@ -10,7 +10,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController
def index
@scope = params[:scope]
- @environments = project.environments
+ @environments = project.environments.includes(:last_deployment)
respond_to do |format|
format.html
@@ -52,10 +52,15 @@ class Projects::EnvironmentsController < Projects::ApplicationController
end
def stop
- return render_404 unless @environment.stoppable?
+ return render_404 unless @environment.available?
- new_action = @environment.stop!(current_user)
- redirect_to polymorphic_path([project.namespace.becomes(Namespace), project, new_action])
+ stop_action = @environment.stop_with_action!(current_user)
+
+ if stop_action
+ redirect_to polymorphic_path([project.namespace.becomes(Namespace), project, stop_action])
+ else
+ redirect_to namespace_project_environment_path(project.namespace, project, @environment)
+ end
end
def terminal
diff --git a/app/controllers/projects/git_http_client_controller.rb b/app/controllers/projects/git_http_client_controller.rb
index 8714349e27f..216c158e41e 100644
--- a/app/controllers/projects/git_http_client_controller.rb
+++ b/app/controllers/projects/git_http_client_controller.rb
@@ -79,7 +79,7 @@ class Projects::GitHttpClientController < Projects::ApplicationController
if project_id.blank?
@project = nil
else
- @project = Project.find_with_namespace("#{params[:namespace_id]}/#{project_id}")
+ @project = Project.find_by_full_path("#{params[:namespace_id]}/#{project_id}")
end
end
@@ -109,12 +109,14 @@ class Projects::GitHttpClientController < Projects::ApplicationController
end
def repository
+ wiki? ? project.wiki.repository : project.repository
+ end
+
+ def wiki?
+ return @wiki if defined?(@wiki)
+
_, suffix = project_id_with_suffix
- if suffix == '.wiki.git'
- project.wiki.repository
- else
- project.repository
- end
+ @wiki = suffix == '.wiki.git'
end
def render_not_found
diff --git a/app/controllers/projects/git_http_controller.rb b/app/controllers/projects/git_http_controller.rb
index 9184dcccac5..278098fcc58 100644
--- a/app/controllers/projects/git_http_controller.rb
+++ b/app/controllers/projects/git_http_controller.rb
@@ -84,7 +84,7 @@ class Projects::GitHttpController < Projects::GitHttpClientController
end
def access
- @access ||= Gitlab::GitAccess.new(user, project, 'http', authentication_abilities: authentication_abilities)
+ @access ||= access_klass.new(user, project, 'http', authentication_abilities: authentication_abilities)
end
def access_check
@@ -102,4 +102,8 @@ class Projects::GitHttpController < Projects::GitHttpClientController
access_check.allowed?
end
+
+ def access_klass
+ @access_klass ||= wiki? ? Gitlab::GitAccessWiki : Gitlab::GitAccess
+ end
end
diff --git a/app/controllers/projects/group_links_controller.rb b/app/controllers/projects/group_links_controller.rb
index 9eaf26a0dbf..66b7bdbd988 100644
--- a/app/controllers/projects/group_links_controller.rb
+++ b/app/controllers/projects/group_links_controller.rb
@@ -4,10 +4,7 @@ class Projects::GroupLinksController < Projects::ApplicationController
before_action :authorize_admin_project_member!, only: [:update]
def index
- @group_links = project.project_group_links.all
-
- @skip_groups = @group_links.pluck(:group_id)
- @skip_groups << project.namespace_id unless project.personal?
+ redirect_to namespace_project_settings_members_path
end
def create
@@ -25,7 +22,7 @@ class Projects::GroupLinksController < Projects::ApplicationController
flash[:alert] = 'Please select a group.'
end
- redirect_to namespace_project_group_links_path(project.namespace, project)
+ redirect_to namespace_project_settings_members_path(project.namespace, project)
end
def update
@@ -39,7 +36,7 @@ class Projects::GroupLinksController < Projects::ApplicationController
respond_to do |format|
format.html do
- redirect_to namespace_project_group_links_path(project.namespace, project)
+ redirect_to namespace_project_settings_members_path(project.namespace, project)
end
format.js { head :ok }
end
diff --git a/app/controllers/projects/hooks_controller.rb b/app/controllers/projects/hooks_controller.rb
index 0ae8ff98009..b668a9331e7 100644
--- a/app/controllers/projects/hooks_controller.rb
+++ b/app/controllers/projects/hooks_controller.rb
@@ -6,21 +6,15 @@ class Projects::HooksController < Projects::ApplicationController
layout "project_settings"
- def index
- @hooks = @project.hooks
- @hook = ProjectHook.new
- end
-
def create
@hook = @project.hooks.new(hook_params)
@hook.save
- if @hook.valid?
- redirect_to namespace_project_hooks_path(@project.namespace, @project)
- else
+ unless @hook.valid?
@hooks = @project.hooks.select(&:persisted?)
- render :index
+ flash[:alert] = @hook.errors.full_messages.join.html_safe
end
+ redirect_to namespace_project_settings_integrations_path(@project.namespace, @project)
end
def test
@@ -44,7 +38,7 @@ class Projects::HooksController < Projects::ApplicationController
def destroy
hook.destroy
- redirect_to namespace_project_hooks_path(@project.namespace, @project)
+ redirect_to namespace_project_settings_integrations_path(@project.namespace, @project)
end
private
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index 4f66e01e0f7..c75b8987a4b 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -25,11 +25,26 @@ class Projects::IssuesController < Projects::ApplicationController
def index
@issues = issues_collection
@issues = @issues.page(params[:page])
+ if @issues.out_of_range? && @issues.total_pages != 0
+ return redirect_to url_for(params.merge(page: @issues.total_pages))
+ end
if params[:label_name].present?
@labels = LabelsFinder.new(current_user, project_id: @project.id, title: params[:label_name]).execute
end
+ @users = []
+
+ if params[:assignee_id].present?
+ assignee = User.find_by_id(params[:assignee_id])
+ @users.push(assignee) if assignee
+ end
+
+ if params[:author_id].present?
+ author = User.find_by_id(params[:author_id])
+ @users.push(author) if author
+ end
+
respond_to do |format|
format.html
format.atom { render layout: false }
@@ -78,15 +93,13 @@ class Projects::IssuesController < Projects::ApplicationController
def create
extra_params = { request: request,
merge_request_for_resolving_discussions: merge_request_for_resolving_discussions }
+ extra_params.merge!(recaptcha_params)
+
@issue = Issues::CreateService.new(project, current_user, issue_params.merge(extra_params)).execute
respond_to do |format|
format.html do
- if @issue.valid?
- redirect_to issue_path(@issue)
- else
- render :new
- end
+ html_response_create
end
format.js do
@link = @issue.attachment.url.to_js
@@ -163,6 +176,20 @@ class Projects::IssuesController < Projects::ApplicationController
protected
+ def html_response_create
+ if @issue.valid?
+ redirect_to issue_path(@issue)
+ elsif render_recaptcha?
+ if params[:recaptcha_verification]
+ flash[:alert] = 'There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.'
+ end
+
+ render :verify
+ else
+ render :new
+ end
+ end
+
def issue
# The Sortable default scope causes performance issues when used with find_by
@noteable = @issue ||= @project.issues.where(iid: params[:id]).reorder(nil).take || redirect_old
diff --git a/app/controllers/projects/labels_controller.rb b/app/controllers/projects/labels_controller.rb
index 824ed7be73e..1593b5c1afb 100644
--- a/app/controllers/projects/labels_controller.rb
+++ b/app/controllers/projects/labels_controller.rb
@@ -2,12 +2,13 @@ class Projects::LabelsController < Projects::ApplicationController
include ToggleSubscriptionAction
before_action :module_enabled
- before_action :label, only: [:edit, :update, :destroy]
+ before_action :label, only: [:edit, :update, :destroy, :promote]
before_action :find_labels, only: [:index, :set_priorities, :remove_priority, :toggle_subscription]
before_action :authorize_read_label!
before_action :authorize_admin_labels!, only: [:new, :create, :edit, :update,
:generate, :destroy, :remove_priority,
:set_priorities]
+ before_action :authorize_admin_group!, only: [:promote]
respond_to :js, :html
@@ -71,13 +72,7 @@ class Projects::LabelsController < Projects::ApplicationController
@label.destroy
@labels = find_labels
- respond_to do |format|
- format.html do
- redirect_to(namespace_project_labels_path(@project.namespace, @project),
- notice: 'Label was removed')
- end
- format.js
- end
+ redirect_to(namespace_project_labels_path(@project.namespace, @project), notice: 'Label was removed')
end
def remove_priority
@@ -108,6 +103,32 @@ class Projects::LabelsController < Projects::ApplicationController
end
end
+ def promote
+ promote_service = Labels::PromoteService.new(@project, @current_user)
+
+ begin
+ return render_404 unless promote_service.execute(@label)
+ respond_to do |format|
+ format.html do
+ redirect_to(namespace_project_labels_path(@project.namespace, @project),
+ notice: 'Label was promoted to a Group Label')
+ end
+ format.js
+ end
+ rescue ActiveRecord::RecordInvalid => e
+ Gitlab::AppLogger.error "Failed to promote label \"#{@label.title}\" to group label"
+ Gitlab::AppLogger.error e
+
+ respond_to do |format|
+ format.html do
+ redirect_to(namespace_project_labels_path(@project.namespace, @project),
+ notice: 'Failed to promote label due to internal error. Please contact administrators.')
+ end
+ format.js
+ end
+ end
+ end
+
protected
def module_enabled
@@ -135,4 +156,8 @@ class Projects::LabelsController < Projects::ApplicationController
def authorize_admin_labels!
return render_404 unless can?(current_user, :admin_label, @project)
end
+
+ def authorize_admin_group!
+ return render_404 unless can?(current_user, :admin_group, @project.group)
+ end
end
diff --git a/app/controllers/projects/lfs_api_controller.rb b/app/controllers/projects/lfs_api_controller.rb
index 440259b643c..8a5a645ed0e 100644
--- a/app/controllers/projects/lfs_api_controller.rb
+++ b/app/controllers/projects/lfs_api_controller.rb
@@ -48,6 +48,10 @@ class Projects::LfsApiController < Projects::GitHttpClientController
objects.each do |object|
if existing_oids.include?(object[:oid])
object[:actions] = download_actions(object)
+
+ if Guest.can?(:download_code, project)
+ object[:authenticated] = true
+ end
else
object[:error] = {
code: 404,
diff --git a/app/controllers/projects/mattermosts_controller.rb b/app/controllers/projects/mattermosts_controller.rb
new file mode 100644
index 00000000000..38f7e6eb5e9
--- /dev/null
+++ b/app/controllers/projects/mattermosts_controller.rb
@@ -0,0 +1,43 @@
+class Projects::MattermostsController < Projects::ApplicationController
+ include TriggersHelper
+ include ActionView::Helpers::AssetUrlHelper
+
+ layout 'project_settings'
+
+ before_action :authorize_admin_project!
+ before_action :service
+ before_action :teams, only: [:new]
+
+ def new
+ end
+
+ def create
+ result, message = @service.configure(current_user, configure_params)
+
+ if result
+ flash[:notice] = 'This service is now configured'
+ redirect_to edit_namespace_project_service_path(
+ @project.namespace, @project, service)
+ else
+ flash[:alert] = message || 'Failed to configure service'
+ redirect_to new_namespace_project_mattermost_path(
+ @project.namespace, @project)
+ end
+ end
+
+ private
+
+ def configure_params
+ params.require(:mattermost).permit(:trigger, :team_id).merge(
+ url: service_trigger_url(@service),
+ icon_url: asset_url('slash-command-logo.png'))
+ end
+
+ def teams
+ @teams, @teams_error_message = @service.list_teams(current_user)
+ end
+
+ def service
+ @service ||= @project.find_or_initialize_service('mattermost_slash_commands')
+ end
+end
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index f0cb5a9d4b4..fbad66c5c40 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -9,10 +9,10 @@ class Projects::MergeRequestsController < Projects::ApplicationController
before_action :module_enabled
before_action :merge_request, only: [
- :edit, :update, :show, :diffs, :commits, :conflicts, :conflict_for_path, :builds, :pipelines, :merge, :merge_check,
+ :edit, :update, :show, :diffs, :commits, :conflicts, :conflict_for_path, :pipelines, :merge, :merge_check,
:ci_status, :ci_environments_status, :toggle_subscription, :cancel_merge_when_build_succeeds, :remove_wip, :resolve_conflicts, :assign_related_issues
]
- before_action :validates_merge_request, only: [:show, :diffs, :commits, :builds, :pipelines]
+ before_action :validates_merge_request, only: [:show, :diffs, :commits, :pipelines]
before_action :define_show_vars, only: [:show, :diffs, :commits, :conflicts, :conflict_for_path, :builds, :pipelines]
before_action :define_widget_vars, only: [:merge, :cancel_merge_when_build_succeeds, :merge_check]
before_action :define_commit_vars, only: [:diffs]
@@ -38,6 +38,9 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def index
@merge_requests = merge_requests_collection
@merge_requests = @merge_requests.page(params[:page])
+ if @merge_requests.out_of_range? && @merge_requests.total_pages != 0
+ return redirect_to url_for(params.merge(page: @merge_requests.total_pages))
+ end
if params[:label_name].present?
labels_params = { project_id: @project.id, title: params[:label_name] }
@@ -95,10 +98,13 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@start_version = @comparable_diffs.find { |diff| diff.head_commit_sha == @start_sha }
unless @start_version
- render_404
+ @start_sha = @merge_request_diff.head_commit_sha
+ @start_version = @merge_request_diff
end
end
+ @environment = @merge_request.environments_for(current_user).last
+
respond_to do |format|
format.html { define_discussion_vars }
format.json do
@@ -201,17 +207,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
end
- def builds
- 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/_builds') } }
- end
- end
-
def pipelines
@pipelines = @merge_request.all_pipelines
@@ -221,12 +216,26 @@ class Projects::MergeRequestsController < Projects::ApplicationController
render 'show'
end
- format.json { render json: { html: view_to_html_string('projects/merge_requests/show/_pipelines') } }
+
+ format.json do
+ render json: PipelineSerializer
+ .new(project: @project, user: @current_user)
+ .represent(@pipelines)
+ end
end
end
def new
- define_new_vars
+ respond_to do |format|
+ format.html { define_new_vars }
+ format.json do
+ define_pipelines_vars
+
+ render json: PipelineSerializer
+ .new(project: @project, user: @current_user)
+ .represent(@pipelines)
+ end
+ end
end
def new_diffs
@@ -243,7 +252,9 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
@diff_notes_disabled = true
- render json: { html: view_to_html_string('projects/merge_requests/_new_diffs', diffs: @diffs) }
+ @environment = @merge_request.environments_for(current_user).last
+
+ render json: { html: view_to_html_string('projects/merge_requests/_new_diffs', diffs: @diffs, environment: @environment) }
end
end
end
@@ -354,6 +365,16 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
end
+ def merge_widget_refresh
+ if merge_request.in_progress_merge_commit_sha || merge_request.state == 'merged'
+ @status = :success
+ elsif merge_request.merge_when_build_succeeds
+ @status = :merge_when_build_succeeds
+ end
+
+ render 'merge'
+ end
+
def branch_from
# This is always source
@source_project = @merge_request.nil? ? @project : @merge_request.source_project
@@ -416,17 +437,14 @@ class Projects::MergeRequestsController < Projects::ApplicationController
else
ci_service = @merge_request.source_project.try(:ci_service)
status = ci_service.commit_status(merge_request.diff_head_sha, merge_request.source_branch) if ci_service
-
- if ci_service.respond_to?(:commit_coverage)
- coverage = ci_service.commit_coverage(merge_request.diff_head_sha, merge_request.source_branch)
- end
end
response = {
title: merge_request.title,
sha: (merge_request.diff_head_commit.short_id if merge_request.diff_head_sha),
status: status,
- coverage: coverage
+ coverage: coverage,
+ pipeline: pipeline.try(:id)
}
render json: response
@@ -435,14 +453,12 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def ci_environments_status
environments =
begin
- @merge_request.environments.map do |environment|
- next unless can?(current_user, :read_environment, environment)
-
+ @merge_request.environments_for(current_user).map do |environment|
project = environment.project
deployment = environment.first_deployment_for(@merge_request.diff_head_commit)
stop_url =
- if environment.stoppable? && can?(current_user, :create_deployment, environment)
+ if environment.stop_action? && can?(current_user, :create_deployment, environment)
stop_namespace_project_environment_path(project.namespace, project, environment)
end
diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb
index b71509f2c9b..c5d93ce25bc 100644
--- a/app/controllers/projects/notes_controller.rb
+++ b/app/controllers/projects/notes_controller.rb
@@ -23,7 +23,8 @@ class Projects::NotesController < Projects::ApplicationController
end
def create
- @note = Notes::CreateService.new(project, current_user, note_params).execute
+ create_params = note_params.merge(merge_request_diff_head_sha: params[:merge_request_diff_head_sha])
+ @note = Notes::CreateService.new(project, current_user, create_params).execute
if @note.is_a?(Note)
Banzai::NoteRenderer.render([@note], @project, current_user)
diff --git a/app/controllers/projects/pages_controller.rb b/app/controllers/projects/pages_controller.rb
new file mode 100644
index 00000000000..fbd18b68141
--- /dev/null
+++ b/app/controllers/projects/pages_controller.rb
@@ -0,0 +1,22 @@
+class Projects::PagesController < Projects::ApplicationController
+ layout 'project_settings'
+
+ before_action :authorize_read_pages!, only: [:show]
+ before_action :authorize_update_pages!, except: [:show]
+
+ def show
+ @domains = @project.pages_domains.order(:domain)
+ end
+
+ def destroy
+ project.remove_pages
+ project.pages_domains.destroy_all
+
+ respond_to do |format|
+ format.html do
+ redirect_to(namespace_project_pages_path(@project.namespace, @project),
+ notice: 'Pages were removed')
+ end
+ end
+ end
+end
diff --git a/app/controllers/projects/pages_domains_controller.rb b/app/controllers/projects/pages_domains_controller.rb
new file mode 100644
index 00000000000..b8c253f6ae3
--- /dev/null
+++ b/app/controllers/projects/pages_domains_controller.rb
@@ -0,0 +1,49 @@
+class Projects::PagesDomainsController < Projects::ApplicationController
+ layout 'project_settings'
+
+ before_action :authorize_update_pages!, except: [:show]
+ before_action :domain, only: [:show, :destroy]
+
+ def show
+ end
+
+ def new
+ @domain = @project.pages_domains.new
+ end
+
+ def create
+ @domain = @project.pages_domains.create(pages_domain_params)
+
+ if @domain.valid?
+ redirect_to namespace_project_pages_path(@project.namespace, @project)
+ else
+ render 'new'
+ end
+ end
+
+ def destroy
+ @domain.destroy
+
+ respond_to do |format|
+ format.html do
+ redirect_to(namespace_project_pages_path(@project.namespace, @project),
+ notice: 'Domain was removed')
+ end
+ format.js
+ end
+ end
+
+ private
+
+ def pages_domain_params
+ params.require(:pages_domain).permit(
+ :certificate,
+ :key,
+ :domain
+ )
+ end
+
+ def domain
+ @domain ||= @project.pages_domains.find_by(domain: params[:id].to_s)
+ end
+end
diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb
index cc347922c6a..84451257b98 100644
--- a/app/controllers/projects/pipelines_controller.rb
+++ b/app/controllers/projects/pipelines_controller.rb
@@ -7,11 +7,33 @@ class Projects::PipelinesController < Projects::ApplicationController
def index
@scope = params[:scope]
- @pipelines = PipelinesFinder.new(project).execute(scope: @scope).page(params[:page]).per(30)
- @pipelines = @pipelines.includes(project: :namespace)
+ @pipelines = PipelinesFinder
+ .new(project)
+ .execute(scope: @scope)
+ .page(params[:page])
+ .per(30)
- @running_or_pending_count = PipelinesFinder.new(project).execute(scope: 'running').count
- @pipelines_count = PipelinesFinder.new(project).execute.count
+ @running_or_pending_count = PipelinesFinder
+ .new(project).execute(scope: 'running').count
+
+ @pipelines_count = PipelinesFinder
+ .new(project).execute.count
+
+ respond_to do |format|
+ format.html
+ format.json do
+ render json: {
+ pipelines: PipelineSerializer
+ .new(project: @project, user: @current_user)
+ .with_pagination(request, response)
+ .represent(@pipelines),
+ count: {
+ all: @pipelines_count,
+ running_or_pending: @running_or_pending_count
+ }
+ }
+ end
+ end
end
def new
diff --git a/app/controllers/projects/pipelines_settings_controller.rb b/app/controllers/projects/pipelines_settings_controller.rb
index 53ce23221ed..c8c80551ac9 100644
--- a/app/controllers/projects/pipelines_settings_controller.rb
+++ b/app/controllers/projects/pipelines_settings_controller.rb
@@ -2,20 +2,13 @@ class Projects::PipelinesSettingsController < Projects::ApplicationController
before_action :authorize_admin_pipeline!
def show
- @ref = params[:ref] || @project.default_branch || 'master'
-
- @badges = [Gitlab::Badge::Build::Status,
- Gitlab::Badge::Coverage::Report]
-
- @badges.map! do |badge|
- badge.new(@project, @ref).metadata
- end
+ redirect_to namespace_project_settings_ci_cd_path(@project.namespace, @project, params: params)
end
def update
if @project.update_attributes(update_params)
flash[:notice] = "CI/CD Pipelines settings for '#{@project.name}' were successfully updated."
- redirect_to namespace_project_pipelines_settings_path(@project.namespace, @project)
+ redirect_to namespace_project_settings_ci_cd_path(@project.namespace, @project)
else
render 'show'
end
diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb
index 3aec6f18e27..6e158e685e9 100644
--- a/app/controllers/projects/project_members_controller.rb
+++ b/app/controllers/projects/project_members_controller.rb
@@ -6,54 +6,14 @@ class Projects::ProjectMembersController < Projects::ApplicationController
before_action :authorize_admin_project_member!, except: [:index, :leave, :request_access]
def index
- @sort = params[:sort].presence || sort_value_name
- @group_links = @project.project_group_links
-
- @project_members = @project.project_members
- @project_members = @project_members.non_invite unless can?(current_user, :admin_project, @project)
-
- group = @project.group
-
- if group
- # We need `.where.not(user_id: nil)` here otherwise when a group has an
- # invitee, it would make the following query return 0 rows since a NULL
- # user_id would be present in the subquery
- # See http://stackoverflow.com/questions/129077/not-in-clause-and-null-values
- # FIXME: This whole logic should be moved to a finder!
- non_null_user_ids = @project_members.where.not(user_id: nil).select(:user_id)
- group_members = group.group_members.where.not(user_id: non_null_user_ids)
- group_members = group_members.non_invite unless can?(current_user, :admin_group, @group)
- end
-
- if params[:search].present?
- user_ids = @project.users.search(params[:search]).select(:id)
- @project_members = @project_members.where(user_id: user_ids)
-
- if group_members
- user_ids = group.users.search(params[:search]).select(:id)
- group_members = group_members.where(user_id: user_ids)
- end
-
- @group_links = @project.project_group_links.where(group_id: @project.invited_groups.search(params[:search]).select(:id))
- end
-
- wheres = ["members.id IN (#{@project_members.select(:id).to_sql})"]
- wheres << "members.id IN (#{group_members.select(:id).to_sql})" if group_members
-
- @project_members = Member.
- where(wheres.join(' OR ')).
- sort(@sort).
- page(params[:page])
-
- @requesters = AccessRequestsFinder.new(@project).execute(current_user)
-
- @project_member = @project.project_members.new
+ sort = params[:sort].presence || sort_value_name
+ redirect_to namespace_project_settings_members_path(@project.namespace, @project, sort: sort)
end
def create
status = Members::CreateService.new(@project, current_user, params).execute
- redirect_url = namespace_project_project_members_path(@project.namespace, @project)
+ redirect_url = namespace_project_settings_members_path(@project.namespace, @project)
if status
redirect_to redirect_url, notice: 'Users were successfully added.'
@@ -76,14 +36,14 @@ class Projects::ProjectMembersController < Projects::ApplicationController
respond_to do |format|
format.html do
- redirect_to namespace_project_project_members_path(@project.namespace, @project)
+ redirect_to namespace_project_settings_members_path(@project.namespace, @project)
end
format.js { head :ok }
end
end
def resend_invite
- redirect_path = namespace_project_project_members_path(@project.namespace, @project)
+ redirect_path = namespace_project_settings_members_path(@project.namespace, @project)
@project_member = @project.project_members.find(params[:id])
@@ -106,7 +66,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController
return render_404
end
- redirect_to(namespace_project_project_members_path(project.namespace, project),
+ redirect_to(namespace_project_settings_members_path(project.namespace, project),
notice: notice)
end
diff --git a/app/controllers/projects/refs_controller.rb b/app/controllers/projects/refs_controller.rb
index 3602b3d5e58..667f4870c7a 100644
--- a/app/controllers/projects/refs_controller.rb
+++ b/app/controllers/projects/refs_controller.rb
@@ -32,12 +32,6 @@ class Projects::RefsController < Projects::ApplicationController
redirect_to new_path
end
- format.js do
- @ref = params[:ref]
- define_tree_vars
- tree
- render "tree"
- end
end
end
diff --git a/app/controllers/projects/runners_controller.rb b/app/controllers/projects/runners_controller.rb
index 53c36635efe..74c54037ba9 100644
--- a/app/controllers/projects/runners_controller.rb
+++ b/app/controllers/projects/runners_controller.rb
@@ -5,11 +5,7 @@ class Projects::RunnersController < Projects::ApplicationController
layout 'project_settings'
def index
- @project_runners = project.runners.ordered
- @assignable_runners = current_user.ci_authorized_runners.
- assignable_for(project).ordered.page(params[:page]).per(20)
- @shared_runners = Ci::Runner.shared.active
- @shared_runners_count = @shared_runners.count(:all)
+ redirect_to namespace_project_settings_ci_cd_path(@project.namespace, @project)
end
def edit
@@ -53,7 +49,7 @@ class Projects::RunnersController < Projects::ApplicationController
def toggle_shared_runners
project.toggle!(:shared_runners_enabled)
- redirect_to namespace_project_runners_path(project.namespace, project)
+ redirect_to namespace_project_settings_ci_cd_path(@project.namespace, @project)
end
protected
diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb
index 30c2a5d9982..17cb1d5be24 100644
--- a/app/controllers/projects/services_controller.rb
+++ b/app/controllers/projects/services_controller.rb
@@ -9,10 +9,6 @@ class Projects::ServicesController < Projects::ApplicationController
layout "project_settings"
- def index
- @services = @project.find_or_initialize_services
- end
-
def edit
end
diff --git a/app/controllers/projects/settings/ci_cd_controller.rb b/app/controllers/projects/settings/ci_cd_controller.rb
new file mode 100644
index 00000000000..6f009d61950
--- /dev/null
+++ b/app/controllers/projects/settings/ci_cd_controller.rb
@@ -0,0 +1,44 @@
+module Projects
+ module Settings
+ class CiCdController < Projects::ApplicationController
+ before_action :authorize_admin_pipeline!
+
+ def show
+ define_runners_variables
+ define_secret_variables
+ define_triggers_variables
+ define_badges_variables
+ end
+
+ private
+
+ def define_runners_variables
+ @project_runners = @project.runners.ordered
+ @assignable_runners = current_user.ci_authorized_runners.
+ assignable_for(project).ordered.page(params[:page]).per(20)
+ @shared_runners = Ci::Runner.shared.active
+ @shared_runners_count = @shared_runners.count(:all)
+ end
+
+ def define_secret_variables
+ @variable = Ci::Variable.new
+ end
+
+ def define_triggers_variables
+ @triggers = @project.triggers
+ @trigger = Ci::Trigger.new
+ end
+
+ def define_badges_variables
+ @ref = params[:ref] || @project.default_branch || 'master'
+
+ @badges = [Gitlab::Badge::Build::Status,
+ Gitlab::Badge::Coverage::Report]
+
+ @badges.map! do |badge|
+ badge.new(@project, @ref).metadata
+ end
+ end
+ end
+ end
+end
diff --git a/app/controllers/projects/settings/integrations_controller.rb b/app/controllers/projects/settings/integrations_controller.rb
new file mode 100644
index 00000000000..fb2a4837735
--- /dev/null
+++ b/app/controllers/projects/settings/integrations_controller.rb
@@ -0,0 +1,18 @@
+module Projects
+ module Settings
+ class IntegrationsController < Projects::ApplicationController
+ include ServiceParams
+
+ before_action :authorize_admin_project!
+ layout "project_settings"
+
+ def show
+ @hooks = @project.hooks
+ @hook = ProjectHook.new
+
+ # Services
+ @services = @project.find_or_initialize_services
+ end
+ end
+ end
+end
diff --git a/app/controllers/projects/settings/members_controller.rb b/app/controllers/projects/settings/members_controller.rb
new file mode 100644
index 00000000000..5735e281f66
--- /dev/null
+++ b/app/controllers/projects/settings/members_controller.rb
@@ -0,0 +1,55 @@
+module Projects
+ module Settings
+ class MembersController < Projects::ApplicationController
+ include SortingHelper
+
+ def show
+ @sort = params[:sort].presence || sort_value_name
+ @group_links = @project.project_group_links
+
+ @project_members = @project.project_members
+ @project_members = @project_members.non_invite unless can?(current_user, :admin_project, @project)
+
+ group = @project.group
+
+ # group links
+ @group_links = @project.project_group_links.all
+
+ @skip_groups = @group_links.pluck(:group_id)
+ @skip_groups << @project.namespace_id unless @project.personal?
+
+ if group
+ # We need `.where.not(user_id: nil)` here otherwise when a group has an
+ # invitee, it would make the following query return 0 rows since a NULL
+ # user_id would be present in the subquery
+ # See http://stackoverflow.com/questions/129077/not-in-clause-and-null-values
+ group_members = MembersFinder.new(@project_members, group).execute(current_user)
+ end
+
+ if params[:search].present?
+ user_ids = @project.users.search(params[:search]).select(:id)
+ @project_members = @project_members.where(user_id: user_ids)
+
+ if group_members
+ user_ids = group.users.search(params[:search]).select(:id)
+ group_members = group_members.where(user_id: user_ids)
+ end
+
+ @group_links = @project.project_group_links.where(group_id: @project.invited_groups.search(params[:search]).select(:id))
+ end
+
+ wheres = ["members.id IN (#{@project_members.select(:id).to_sql})"]
+ wheres << "members.id IN (#{group_members.select(:id).to_sql})" if group_members
+
+ @project_members = Member.
+ where(wheres.join(' OR ')).
+ sort(@sort).
+ page(params[:page])
+
+ @requesters = AccessRequestsFinder.new(@project).execute(current_user)
+
+ @project_member = @project.project_members.new
+ end
+ end
+ end
+end
diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb
index 0720be2e55d..5d193f26a8e 100644
--- a/app/controllers/projects/snippets_controller.rb
+++ b/app/controllers/projects/snippets_controller.rb
@@ -1,8 +1,9 @@
class Projects::SnippetsController < Projects::ApplicationController
include ToggleAwardEmoji
+ include SpammableActions
before_action :module_enabled
- before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :toggle_award_emoji]
+ before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :toggle_award_emoji, :mark_as_spam]
# Allow read any snippet
before_action :authorize_read_project_snippet!, except: [:new, :create, :index]
@@ -26,6 +27,9 @@ class Projects::SnippetsController < Projects::ApplicationController
scope: params[:scope]
)
@snippets = @snippets.page(params[:page])
+ if @snippets.out_of_range? && @snippets.total_pages != 0
+ redirect_to namespace_project_snippets_path(page: @snippets.total_pages)
+ end
end
def new
@@ -33,8 +37,8 @@ class Projects::SnippetsController < Projects::ApplicationController
end
def create
- @snippet = CreateSnippetService.new(@project, current_user,
- snippet_params).execute
+ create_params = snippet_params.merge(request: request)
+ @snippet = CreateSnippetService.new(@project, current_user, create_params).execute
if @snippet.valid?
respond_with(@snippet,
@@ -85,6 +89,7 @@ class Projects::SnippetsController < Projects::ApplicationController
@snippet ||= @project.snippets.find(params[:id])
end
alias_method :awardable, :snippet
+ alias_method :spammable, :snippet
def authorize_read_project_snippet!
return render_404 unless can?(current_user, :read_project_snippet, @snippet)
diff --git a/app/controllers/projects/tags_controller.rb b/app/controllers/projects/tags_controller.rb
index 953091492ae..e2d9d5ed460 100644
--- a/app/controllers/projects/tags_controller.rb
+++ b/app/controllers/projects/tags_controller.rb
@@ -8,7 +8,7 @@ class Projects::TagsController < Projects::ApplicationController
before_action :authorize_admin_project!, only: [:destroy]
def index
- params[:sort] = params[:sort].presence || 'name'
+ params[:sort] = params[:sort].presence || sort_value_recently_updated
@sort = params[:sort]
@tags = TagsFinder.new(@repository, params).execute
diff --git a/app/controllers/projects/triggers_controller.rb b/app/controllers/projects/triggers_controller.rb
index 92359745cec..b2c11ea4156 100644
--- a/app/controllers/projects/triggers_controller.rb
+++ b/app/controllers/projects/triggers_controller.rb
@@ -4,8 +4,7 @@ class Projects::TriggersController < Projects::ApplicationController
layout 'project_settings'
def index
- @triggers = project.triggers
- @trigger = Ci::Trigger.new
+ redirect_to namespace_project_settings_ci_cd_path(@project.namespace, @project)
end
def create
@@ -13,17 +12,18 @@ class Projects::TriggersController < Projects::ApplicationController
@trigger.save
if @trigger.valid?
- redirect_to namespace_project_triggers_path(@project.namespace, @project)
+ redirect_to namespace_project_variables_path(project.namespace, project), notice: 'Trigger was created successfully.'
else
@triggers = project.triggers.select(&:persisted?)
- render :index
+ render action: "show"
end
end
def destroy
trigger.destroy
+ flash[:alert] = "Trigger removed"
- redirect_to namespace_project_triggers_path(@project.namespace, @project)
+ redirect_to namespace_project_settings_ci_cd_path(@project.namespace, @project)
end
private
diff --git a/app/controllers/projects/uploads_controller.rb b/app/controllers/projects/uploads_controller.rb
index e617be8f9fb..50ba33ed570 100644
--- a/app/controllers/projects/uploads_controller.rb
+++ b/app/controllers/projects/uploads_controller.rb
@@ -36,7 +36,7 @@ class Projects::UploadsController < Projects::ApplicationController
namespace = params[:namespace_id]
id = params[:project_id]
- file_project = Project.find_with_namespace("#{namespace}/#{id}")
+ file_project = Project.find_by_full_path("#{namespace}/#{id}")
if file_project.nil?
@uploader = nil
diff --git a/app/controllers/projects/variables_controller.rb b/app/controllers/projects/variables_controller.rb
index 6f068729390..a4d1b1ee69b 100644
--- a/app/controllers/projects/variables_controller.rb
+++ b/app/controllers/projects/variables_controller.rb
@@ -4,7 +4,7 @@ class Projects::VariablesController < Projects::ApplicationController
layout 'project_settings'
def index
- @variable = Ci::Variable.new
+ redirect_to namespace_project_settings_ci_cd_path(@project.namespace, @project)
end
def show
@@ -25,9 +25,10 @@ class Projects::VariablesController < Projects::ApplicationController
@variable = Ci::Variable.new(project_params)
if @variable.valid? && @project.variables << @variable
- redirect_to namespace_project_variables_path(project.namespace, project), notice: 'Variables were successfully updated.'
+ flash[:notice] = 'Variables were successfully updated.'
+ redirect_to namespace_project_settings_ci_cd_path(project.namespace, project)
else
- render action: "index"
+ render "show"
end
end
@@ -35,7 +36,7 @@ class Projects::VariablesController < Projects::ApplicationController
@key = @project.variables.find(params[:id])
@key.destroy
- redirect_to namespace_project_variables_path(project.namespace, project), notice: 'Variable was successfully removed.'
+ redirect_to namespace_project_settings_ci_cd_path(project.namespace, project), notice: 'Variable was successfully removed.'
end
private
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index d5ee503c44c..acca821782c 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -42,19 +42,16 @@ class ProjectsController < Projects::ApplicationController
end
def update
- status = ::Projects::UpdateService.new(@project, current_user, project_params).execute
+ result = ::Projects::UpdateService.new(@project, current_user, project_params).execute
# Refresh the repo in case anything changed
- @repository = project.repository
+ @repository = @project.repository
respond_to do |format|
- if status
+ if result[:status] == :success
flash[:notice] = "Project '#{@project.name}' was successfully updated."
format.html do
- redirect_to(
- edit_project_path(@project),
- notice: "Project '#{@project.name}' was successfully updated."
- )
+ redirect_to(edit_project_path(@project))
end
else
format.html { render 'edit' }
@@ -234,12 +231,16 @@ class ProjectsController < Projects::ApplicationController
end
def refs
+ branches = BranchesFinder.new(@repository, params).execute.map(&:name)
+
options = {
- 'Branches' => @repository.branch_names,
+ 'Branches' => branches.take(100),
}
unless @repository.tag_count.zero?
- options['Tags'] = VersionSorter.rsort(@repository.tag_names)
+ tags = TagsFinder.new(@repository, params).execute.map(&:name)
+
+ options['Tags'] = tags.take(100)
end
# If reference is commit id - we should add it to branch/tag selectbox
diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb
index c45196cc3e9..68bf01ba08d 100644
--- a/app/controllers/registrations_controller.rb
+++ b/app/controllers/registrations_controller.rb
@@ -7,17 +7,17 @@ class RegistrationsController < Devise::RegistrationsController
end
def create
- if !Gitlab::Recaptcha.load_configurations! || verify_recaptcha
- # To avoid duplicate form fields on the login page, the registration form
- # names fields using `new_user`, but Devise still wants the params in
- # `user`.
- if params["new_#{resource_name}"].present? && params[resource_name].blank?
- params[resource_name] = params.delete(:"new_#{resource_name}")
- end
+ # To avoid duplicate form fields on the login page, the registration form
+ # names fields using `new_user`, but Devise still wants the params in
+ # `user`.
+ if params["new_#{resource_name}"].present? && params[resource_name].blank?
+ params[resource_name] = params.delete(:"new_#{resource_name}")
+ end
+ if !Gitlab::Recaptcha.load_configurations! || verify_recaptcha
super
else
- flash[:alert] = "There was an error with the reCAPTCHA code below. Please re-enter the code."
+ flash[:alert] = 'There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.'
flash.delete :recaptcha_error
render action: 'new'
end
@@ -30,7 +30,7 @@ class RegistrationsController < Devise::RegistrationsController
format.html do
session.try(:destroy)
redirect_to new_user_session_path, notice: "Account successfully removed."
- end
+ end
end
end
@@ -57,7 +57,7 @@ class RegistrationsController < Devise::RegistrationsController
end
def sign_up_params
- params.require(:user).permit(:username, :email, :name, :password, :password_confirmation)
+ params.require(:user).permit(:username, :email, :email_confirmation, :name, :password)
end
def resource_name
diff --git a/app/controllers/root_controller.rb b/app/controllers/root_controller.rb
index 627be74a38f..db2817fadf6 100644
--- a/app/controllers/root_controller.rb
+++ b/app/controllers/root_controller.rb
@@ -7,6 +7,7 @@
# For users who haven't customized the setting, we simply delegate to
# `DashboardController#show`, which is the default.
class RootController < Dashboard::ProjectsController
+ skip_before_action :authenticate_user!, only: [:index]
before_action :redirect_to_custom_dashboard, only: [:index]
def index
@@ -16,7 +17,7 @@ class RootController < Dashboard::ProjectsController
private
def redirect_to_custom_dashboard
- return unless current_user
+ return redirect_to new_user_session_path unless current_user
case current_user.dashboard
when 'stars'
diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb
index b666aa01d6b..6576ebd5235 100644
--- a/app/controllers/search_controller.rb
+++ b/app/controllers/search_controller.rb
@@ -45,6 +45,8 @@ class SearchController < ApplicationController
end
@search_objects = @search_results.objects(@scope, params[:page])
+
+ check_single_commit_result
end
def autocomplete
@@ -59,4 +61,16 @@ class SearchController < ApplicationController
render json: search_autocomplete_opts(term).to_json
end
+
+ private
+
+ def check_single_commit_result
+ if @search_results.single_commit_result?
+ only_commit = @search_results.objects('commits').first
+ query = params[:search].strip.downcase
+ found_by_commit_sha = Commit.valid_hash?(query) && only_commit.sha.start_with?(query)
+
+ redirect_to namespace_project_commit_path(@project.namespace, @project, only_commit) if found_by_commit_sha
+ end
+ end
end
diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb
index dee57e4a388..b169d993688 100644
--- a/app/controllers/snippets_controller.rb
+++ b/app/controllers/snippets_controller.rb
@@ -1,5 +1,6 @@
class SnippetsController < ApplicationController
include ToggleAwardEmoji
+ include SpammableActions
before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :download]
@@ -40,8 +41,8 @@ class SnippetsController < ApplicationController
end
def create
- @snippet = CreateSnippetService.new(nil, current_user,
- snippet_params).execute
+ create_params = snippet_params.merge(request: request)
+ @snippet = CreateSnippetService.new(nil, current_user, create_params).execute
respond_with @snippet.becomes(Snippet)
end
@@ -96,6 +97,7 @@ class SnippetsController < ApplicationController
end
end
alias_method :awardable, :snippet
+ alias_method :spammable, :snippet
def authorize_read_snippet!
authenticate_user! unless can?(current_user, :read_personal_snippet, @snippet)
diff --git a/app/finders/environments_finder.rb b/app/finders/environments_finder.rb
new file mode 100644
index 00000000000..a59f8c1efa3
--- /dev/null
+++ b/app/finders/environments_finder.rb
@@ -0,0 +1,55 @@
+class EnvironmentsFinder
+ attr_reader :project, :current_user, :params
+
+ def initialize(project, current_user, params = {})
+ @project, @current_user, @params = project, current_user, params
+ end
+
+ def execute
+ deployments = project.deployments
+ deployments =
+ if ref
+ deployments_query = params[:with_tags] ? 'ref = :ref OR tag IS TRUE' : 'ref = :ref'
+ deployments.where(deployments_query, ref: ref.to_s)
+ elsif commit
+ deployments.where(sha: commit.sha)
+ else
+ deployments.none
+ end
+
+ environment_ids = deployments
+ .group(:environment_id)
+ .select(:environment_id)
+
+ environments = project.environments.available
+ .where(id: environment_ids).order_by_last_deployed_at.to_a
+
+ environments.select! do |environment|
+ Ability.allowed?(current_user, :read_environment, environment)
+ end
+
+ if ref && commit
+ environments.select! do |environment|
+ environment.includes_commit?(commit)
+ end
+ end
+
+ if ref && params[:recently_updated]
+ environments.select! do |environment|
+ environment.recently_updated_on_branch?(ref)
+ end
+ end
+
+ environments
+ end
+
+ private
+
+ def ref
+ params[:ref].try(:to_s)
+ end
+
+ def commit
+ params[:commit]
+ end
+end
diff --git a/app/finders/group_projects_finder.rb b/app/finders/group_projects_finder.rb
index aa8f4c1d0e4..3b9a421b118 100644
--- a/app/finders/group_projects_finder.rb
+++ b/app/finders/group_projects_finder.rb
@@ -18,7 +18,7 @@ class GroupProjectsFinder < UnionFinder
projects = []
if current_user
- if @group.users.include?(current_user) || current_user.admin?
+ if @group.users.include?(current_user)
projects << @group.projects unless only_shared
projects << @group.shared_projects unless only_owned
else
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index b4c14d05eaf..1576fc80a6b 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -165,31 +165,53 @@ class IssuableFinder
end
end
- def assignee?
- params[:assignee_id].present?
+ def assignee_id?
+ params[:assignee_id].present? && params[:assignee_id] != NONE
+ end
+
+ def assignee_username?
+ params[:assignee_username].present? && params[:assignee_username] != NONE
+ end
+
+ def no_assignee?
+ # Assignee_id takes precedence over assignee_username
+ params[:assignee_id] == NONE || params[:assignee_username] == NONE
end
def assignee
return @assignee if defined?(@assignee)
@assignee =
- if assignee? && params[:assignee_id] != NONE
- User.find(params[:assignee_id])
+ if assignee_id?
+ User.find_by(id: params[:assignee_id])
+ elsif assignee_username?
+ User.find_by(username: params[:assignee_username])
else
nil
end
end
- def author?
- params[:author_id].present?
+ def author_id?
+ params[:author_id].present? && params[:author_id] != NONE
+ end
+
+ def author_username?
+ params[:author_username].present? && params[:author_username] != NONE
+ end
+
+ def no_author?
+ # author_id takes precedence over author_username
+ params[:author_id] == NONE || params[:author_username] == NONE
end
def author
return @author if defined?(@author)
@author =
- if author? && params[:author_id] != NONE
- User.find(params[:author_id])
+ if author_id?
+ User.find_by(id: params[:author_id])
+ elsif author_username?
+ User.find_by(username: params[:author_username])
else
nil
end
@@ -263,16 +285,24 @@ class IssuableFinder
end
def by_assignee(items)
- if assignee?
- items = items.where(assignee_id: assignee.try(:id))
+ if assignee
+ items = items.where(assignee_id: assignee.id)
+ elsif no_assignee?
+ items = items.where(assignee_id: nil)
+ elsif assignee_id? || assignee_username? # assignee not found
+ items = items.none
end
items
end
def by_author(items)
- if author?
- items = items.where(author_id: author.try(:id))
+ if author
+ items = items.where(author_id: author.id)
+ elsif no_author?
+ items = items.where(author_id: nil)
+ elsif author_id? || author_username? # author not found
+ items = items.none
end
items
diff --git a/app/finders/members_finder.rb b/app/finders/members_finder.rb
new file mode 100644
index 00000000000..702944404f5
--- /dev/null
+++ b/app/finders/members_finder.rb
@@ -0,0 +1,13 @@
+class MembersFinder < Projects::ApplicationController
+ def initialize(project_members, project_group)
+ @project_members = project_members
+ @project_group = project_group
+ end
+
+ def execute(current_user)
+ non_null_user_ids = @project_members.where.not(user_id: nil).select(:user_id)
+ group_members = @project_group.group_members.where.not(user_id: non_null_user_ids)
+ group_members = group_members.non_invite unless can?(current_user, :admin_group, @project_group)
+ group_members
+ end
+end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index c816b616631..bee323993a0 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -37,7 +37,7 @@ module ApplicationHelper
if project_id.is_a?(Project)
project_id
else
- Project.find_with_namespace(project_id)
+ Project.find_by_full_path(project_id)
end
if project.avatar_url
@@ -244,7 +244,9 @@ module ApplicationHelper
scope: params[:scope],
milestone_title: params[:milestone_title],
assignee_id: params[:assignee_id],
+ assignee_username: params[:assignee_username],
author_id: params[:author_id],
+ author_username: params[:author_username],
search: params[:search],
label_name: params[:label_name]
}
diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb
index 92bac149313..1ee6c1d3afa 100644
--- a/app/helpers/auth_helper.rb
+++ b/app/helpers/auth_helper.rb
@@ -1,5 +1,5 @@
module AuthHelper
- PROVIDERS_WITH_ICONS = %w(twitter github gitlab bitbucket google_oauth2 facebook azure_oauth2).freeze
+ PROVIDERS_WITH_ICONS = %w(twitter github gitlab bitbucket google_oauth2 facebook azure_oauth2 authentiq).freeze
FORM_BASED_PROVIDERS = [/\Aldap/, 'crowd'].freeze
def ldap_enabled?
diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb
index f31d4fb897d..311a70725ab 100644
--- a/app/helpers/blob_helper.rb
+++ b/app/helpers/blob_helper.rb
@@ -21,7 +21,7 @@ module BlobHelper
options[:link_opts])
if !on_top_of_branch?(project, ref)
- button_tag "Edit", class: "btn disabled has-tooltip btn-file-option", title: "You can only edit files when you are on a branch", data: { container: 'body' }
+ button_tag "Edit", class: "btn disabled has-tooltip", title: "You can only edit files when you are on a branch", data: { container: 'body' }
elsif can_edit_blob?(blob, project, ref)
link_to "Edit", edit_path, class: 'btn btn-sm'
elsif can?(current_user, :fork_project, project)
@@ -32,7 +32,7 @@ module BlobHelper
}
fork_path = namespace_project_forks_path(project.namespace, project, namespace_key: current_user.namespace.id, continue: continue_params)
- link_to "Edit", fork_path, class: 'btn btn-file-option', method: :post
+ link_to "Edit", fork_path, class: 'btn', method: :post
end
end
@@ -188,7 +188,7 @@ module BlobHelper
end
def gitlab_ci_ymls
- @gitlab_ci_ymls ||= Gitlab::Template::GitlabCiYmlTemplate.dropdown_names
+ @gitlab_ci_ymls ||= Gitlab::Template::GitlabCiYmlTemplate.dropdown_names(params[:context])
end
def dockerfile_names
diff --git a/app/helpers/boards_helper.rb b/app/helpers/boards_helper.rb
index 38c586ccd31..f43827da446 100644
--- a/app/helpers/boards_helper.rb
+++ b/app/helpers/boards_helper.rb
@@ -6,7 +6,9 @@ module BoardsHelper
endpoint: namespace_project_boards_path(@project.namespace, @project),
board_id: board.id,
disabled: "#{!can?(current_user, :admin_list, @project)}",
- issue_link_base: namespace_project_issues_path(@project.namespace, @project)
+ issue_link_base: namespace_project_issues_path(@project.namespace, @project),
+ root_path: root_path,
+ bulk_update_path: bulk_update_namespace_project_issues_path(@project.namespace, @project),
}
end
end
diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb
index d9f5e01f0dc..94f3b480178 100644
--- a/app/helpers/ci_status_helper.rb
+++ b/app/helpers/ci_status_helper.rb
@@ -1,7 +1,7 @@
module CiStatusHelper
def ci_status_path(pipeline)
project = pipeline.project
- builds_namespace_project_commit_path(project.namespace, project, pipeline.sha)
+ namespace_project_pipeline_path(project.namespace, project, pipeline)
end
# Is used by Commit and Merge Request Widget
diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb
index 66a720a9426..8aad39e148b 100644
--- a/app/helpers/commits_helper.rb
+++ b/app/helpers/commits_helper.rb
@@ -128,50 +128,11 @@ module CommitsHelper
end
def revert_commit_link(commit, continue_to_path, btn_class: nil, has_tooltip: true)
- return unless current_user
-
- tooltip = "Revert this #{commit.change_type_title(current_user)} in a new merge request" if has_tooltip
-
- if can_collaborate_with_project?
- btn_class = "btn btn-warning btn-#{btn_class}" unless btn_class.nil?
- link_to 'Revert', '#modal-revert-commit', 'data-toggle' => 'modal', 'data-container' => 'body', title: (tooltip if has_tooltip), class: "#{btn_class} #{'has-tooltip' if has_tooltip}"
- elsif can?(current_user, :fork_project, @project)
- continue_params = {
- to: continue_to_path,
- notice: edit_in_new_fork_notice + ' Try to revert this commit again.',
- notice_now: edit_in_new_fork_notice_now
- }
- fork_path = namespace_project_forks_path(@project.namespace, @project,
- namespace_key: current_user.namespace.id,
- continue: continue_params)
-
- btn_class = "btn btn-grouped btn-warning" unless btn_class.nil?
-
- link_to 'Revert', fork_path, class: btn_class, method: :post, 'data-toggle' => 'tooltip', 'data-container' => 'body', title: (tooltip if has_tooltip)
- end
+ commit_action_link('revert', commit, continue_to_path, btn_class: btn_class, has_tooltip: has_tooltip)
end
def cherry_pick_commit_link(commit, continue_to_path, btn_class: nil, has_tooltip: true)
- return unless current_user
-
- tooltip = "Cherry-pick this #{commit.change_type_title(current_user)} in a new merge request"
-
- if can_collaborate_with_project?
- btn_class = "btn btn-default btn-#{btn_class}" unless btn_class.nil?
- link_to 'Cherry-pick', '#modal-cherry-pick-commit', 'data-toggle' => 'modal', 'data-container' => 'body', title: (tooltip if has_tooltip), class: "#{btn_class} #{'has-tooltip' if has_tooltip}"
- elsif can?(current_user, :fork_project, @project)
- continue_params = {
- to: continue_to_path,
- notice: edit_in_new_fork_notice + ' Try to cherry-pick this commit again.',
- notice_now: edit_in_new_fork_notice_now
- }
- fork_path = namespace_project_forks_path(@project.namespace, @project,
- namespace_key: current_user.namespace.id,
- continue: continue_params)
-
- btn_class = "btn btn-grouped btn-close" unless btn_class.nil?
- link_to 'Cherry-pick', fork_path, class: "#{btn_class}", method: :post, 'data-toggle' => 'tooltip', 'data-container' => 'body', title: (tooltip if has_tooltip)
- end
+ commit_action_link('cherry-pick', commit, continue_to_path, btn_class: btn_class, has_tooltip: has_tooltip)
end
protected
@@ -211,17 +172,50 @@ module CommitsHelper
end
end
- def view_file_btn(commit_sha, diff_new_path, project)
+ def commit_action_link(action, commit, continue_to_path, btn_class: nil, has_tooltip: true)
+ return unless current_user
+
+ tooltip = "#{action.capitalize} this #{commit.change_type_title(current_user)} in a new merge request" if has_tooltip
+ btn_class = "btn btn-#{btn_class}" unless btn_class.nil?
+
+ if can_collaborate_with_project?
+ link_to action.capitalize, "#modal-#{action}-commit", 'data-toggle' => 'modal', 'data-container' => 'body', title: (tooltip if has_tooltip), class: "#{btn_class} #{'has-tooltip' if has_tooltip}"
+ elsif can?(current_user, :fork_project, @project)
+ continue_params = {
+ to: continue_to_path,
+ notice: "#{edit_in_new_fork_notice} Try to #{action} this commit again.",
+ notice_now: edit_in_new_fork_notice_now
+ }
+ fork_path = namespace_project_forks_path(@project.namespace, @project,
+ namespace_key: current_user.namespace.id,
+ continue: continue_params)
+
+ link_to action.capitalize, fork_path, class: btn_class, method: :post, 'data-toggle' => 'tooltip', 'data-container' => 'body', title: (tooltip if has_tooltip)
+ end
+ end
+
+ def view_file_button(commit_sha, diff_new_path, project)
link_to(
namespace_project_blob_path(project.namespace, project,
tree_join(commit_sha, diff_new_path)),
- class: 'btn view-file js-view-file btn-file-option'
+ class: 'btn view-file js-view-file'
) do
raw('View file @') + content_tag(:span, commit_sha[0..6],
class: 'commit-short-id')
end
end
+ def view_on_environment_button(commit_sha, diff_new_path, environment)
+ return unless environment && commit_sha
+
+ external_url = environment.external_url_for(diff_new_path, commit_sha)
+ return unless external_url
+
+ link_to(external_url, class: 'btn btn-file-option has-tooltip', target: '_blank', title: "View on #{environment.formatted_external_url}", data: { container: 'body' }) do
+ icon('external-link')
+ end
+ end
+
def truncate_sha(sha)
Commit.truncate_sha(sha)
end
diff --git a/app/helpers/compare_helper.rb b/app/helpers/compare_helper.rb
index aa54ee07bdc..2aa0449c46e 100644
--- a/app/helpers/compare_helper.rb
+++ b/app/helpers/compare_helper.rb
@@ -3,7 +3,7 @@ module CompareHelper
from.present? &&
to.present? &&
from != to &&
- project.feature_available?(:merge_requests, current_user) &&
+ can?(current_user, :create_merge_request, project) &&
project.repository.branch_names.include?(from) &&
project.repository.branch_names.include?(to)
end
diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb
index c35d6611ab0..aed1d7c839f 100644
--- a/app/helpers/diff_helper.rb
+++ b/app/helpers/diff_helper.rb
@@ -165,4 +165,10 @@ module DiffHelper
link_to "#{hide_whitespace? ? 'Show' : 'Hide'} whitespace changes", url, class: options[:class]
end
+
+ def render_overflow_warning?(diff_files)
+ diffs = @merge_request_diff.presence || diff_files
+
+ diffs.overflow?
+ end
end
diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb
index eb435cc1783..6d365ea9251 100644
--- a/app/helpers/gitlab_markdown_helper.rb
+++ b/app/helpers/gitlab_markdown_helper.rb
@@ -110,6 +110,28 @@ module GitlabMarkdownHelper
end
end
+ # Returns the text necessary to reference `entity` across projects
+ #
+ # project - Project to reference
+ # entity - Object that responds to `to_reference`
+ #
+ # Examples:
+ #
+ # cross_project_reference(project, project.issues.first)
+ # # => 'namespace1/project1#123'
+ #
+ # cross_project_reference(project, project.merge_requests.first)
+ # # => 'namespace1/project1!345'
+ #
+ # Returns a String
+ def cross_project_reference(project, entity)
+ if entity.respond_to?(:to_reference)
+ entity.to_reference(project, full: true)
+ else
+ ''
+ end
+ end
+
private
# Return +text+, truncated to +max_chars+ characters, excluding any HTML
@@ -158,28 +180,6 @@ module GitlabMarkdownHelper
end
end
- # Returns the text necessary to reference `entity` across projects
- #
- # project - Project to reference
- # entity - Object that responds to `to_reference`
- #
- # Examples:
- #
- # cross_project_reference(project, project.issues.first)
- # # => 'namespace1/project1#123'
- #
- # cross_project_reference(project, project.merge_requests.first)
- # # => 'namespace1/project1!345'
- #
- # Returns a String
- def cross_project_reference(project, entity)
- if entity.respond_to?(:to_reference)
- entity.to_reference(project)
- else
- ''
- end
- end
-
def markdown_toolbar_button(options = {})
data = options[:data].merge({ container: "body" })
content_tag :button,
diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb
index 99db73c9ee0..f16a63e2178 100644
--- a/app/helpers/gitlab_routing_helper.rb
+++ b/app/helpers/gitlab_routing_helper.rb
@@ -206,4 +206,17 @@ module GitlabRoutingHelper
file_namespace_project_build_artifacts_path(*args)
end
end
+
+ # Settings
+ def project_settings_integrations_path(project, *args)
+ namespace_project_settings_integrations_path(project.namespace, project, *args)
+ end
+
+ def project_settings_members_path(project, *args)
+ namespace_project_settings_members_path(project.namespace, project, *args)
+ end
+
+ def project_settings_ci_cd_path(project, *args)
+ namespace_project_settings_ci_cd_path(project.namespace, project, *args)
+ end
end
diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb
index 77dc9e7d538..926c9703628 100644
--- a/app/helpers/groups_helper.rb
+++ b/app/helpers/groups_helper.rb
@@ -14,7 +14,7 @@ module GroupsHelper
def group_title(group, name = nil, url = nil)
full_title = ''
- group.parents.each do |parent|
+ group.ancestors.each do |parent|
full_title += link_to(simple_sanitize(parent.name), group_path(parent))
full_title += ' / '.html_safe
end
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index 8231f8fa334..03354c235eb 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -30,6 +30,15 @@ module IssuablesHelper
end
end
+ def serialize_issuable(issuable)
+ case issuable
+ when Issue
+ IssueSerializer.new.represent(issuable).to_json
+ when MergeRequest
+ MergeRequestSerializer.new.represent(issuable).to_json
+ end
+ end
+
def template_dropdown_tag(issuable, &block)
title = selected_template(issuable) || "Choose a template"
options = {
@@ -96,8 +105,8 @@ module IssuablesHelper
if issuable.tasks?
output << "&ensp;".html_safe
- output << content_tag(:span, issuable.task_status, id: "task_status", class: "hidden-xs")
- output << content_tag(:span, issuable.task_status_short, id: "task_status_short", class: "hidden-sm hidden-md hidden-lg")
+ output << content_tag(:span, issuable.task_status, id: "task_status", class: "hidden-xs hidden-sm")
+ output << content_tag(:span, issuable.task_status_short, id: "task_status_short", class: "hidden-md hidden-lg")
end
output
@@ -153,6 +162,10 @@ module IssuablesHelper
]
end
+ def issuable_reference(issuable)
+ @show_full_reference ? issuable.to_reference(full: true) : issuable.to_reference(@group || @project)
+ end
+
def issuable_filter_present?
issuable_filter_params.any? { |k| params.key?(k) }
end
diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb
index a8a49e43b05..a2d21b67a77 100644
--- a/app/helpers/issues_helper.rb
+++ b/app/helpers/issues_helper.rb
@@ -58,13 +58,13 @@ module IssuesHelper
end
def status_box_class(item)
- if item.respond_to?(:expired?) && item.expired?
+ if item.try(:expired?)
'status-box-expired'
- elsif item.respond_to?(:merged?) && item.merged?
+ elsif item.try(:merged?)
'status-box-merged'
elsif item.closed?
'status-box-closed'
- elsif item.respond_to?(:upcoming?) && item.upcoming?
+ elsif item.try(:upcoming?)
'status-box-upcoming'
else
'status-box-open'
@@ -128,8 +128,10 @@ module IssuesHelper
names.to_sentence
end
- def award_active_class(awards, current_user)
- if current_user && awards.find { |a| a.user_id == current_user.id }
+ def award_state_class(awards, current_user)
+ if !current_user
+ "disabled"
+ elsif current_user && awards.find { |a| a.user_id == current_user.id }
"active"
else
""
diff --git a/app/helpers/javascript_helper.rb b/app/helpers/javascript_helper.rb
index 0e456214d37..320dd89c9d3 100644
--- a/app/helpers/javascript_helper.rb
+++ b/app/helpers/javascript_helper.rb
@@ -1,5 +1,8 @@
module JavascriptHelper
def page_specific_javascript_tag(js)
- javascript_include_tag asset_path(js), { "data-turbolinks-track" => true }
+ javascript_include_tag asset_path(js)
+ end
+ def page_specific_javascript_bundle_tag(js)
+ javascript_include_tag(*webpack_asset_paths(js))
end
end
diff --git a/app/helpers/mattermost_helper.rb b/app/helpers/mattermost_helper.rb
new file mode 100644
index 00000000000..49ac12db832
--- /dev/null
+++ b/app/helpers/mattermost_helper.rb
@@ -0,0 +1,9 @@
+module MattermostHelper
+ def mattermost_teams_options(teams)
+ teams_options = teams.map do |id, options|
+ [options['display_name'] || options['name'], id]
+ end
+
+ teams_options.compact.unshift(['Select team...', '0'])
+ end
+end
diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb
index 20218775659..b5f8c23a667 100644
--- a/app/helpers/merge_requests_helper.rb
+++ b/app/helpers/merge_requests_helper.rb
@@ -19,6 +19,14 @@ module MergeRequestsHelper
}
end
+ def mr_widget_refresh_url(mr)
+ if mr && mr.target_project
+ merge_widget_refresh_namespace_project_merge_request_url(mr.target_project.namespace, mr.target_project, mr)
+ else
+ ''
+ end
+ end
+
def mr_css_classes(mr)
classes = "merge-request"
classes << " closed" if mr.closed?
@@ -56,11 +64,11 @@ module MergeRequestsHelper
end
def mr_closes_issues
- @mr_closes_issues ||= @merge_request.closes_issues
+ @mr_closes_issues ||= @merge_request.closes_issues(current_user)
end
def mr_issues_mentioned_but_not_closing
- @mr_issues_mentioned_but_not_closing ||= @merge_request.issues_mentioned_but_not_closing
+ @mr_issues_mentioned_but_not_closing ||= @merge_request.issues_mentioned_but_not_closing(current_user)
end
def mr_change_branches_path(merge_request)
@@ -135,4 +143,16 @@ module MergeRequestsHelper
def different_base?(version1, version2)
version1 && version2 && version1.base_commit_sha != version2.base_commit_sha
end
+
+ def merge_params(merge_request)
+ {
+ merge_when_build_succeeds: true,
+ should_remove_source_branch: true,
+ sha: merge_request.diff_head_sha
+ }.merge(merge_params_ee(merge_request))
+ end
+
+ def merge_params_ee(merge_request)
+ {}
+ end
end
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index d2177f683a1..eb98204285d 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -61,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 << button_tag(type: 'button', class: "dropdown-toggle-caret js-projects-dropdown-toggle", aria: { label: "Toggle switch project dropdown" }, data: { target: ".js-dropdown-menu-projects", toggle: "dropdown" }) do
+ project_link << button_tag(type: 'button', class: 'dropdown-toggle-caret js-projects-dropdown-toggle', aria: { label: 'Toggle switch project dropdown' }, data: { target: '.js-dropdown-menu-projects', toggle: 'dropdown', order_by: 'last_activity_at' }) do
icon("chevron-down")
end
end
@@ -90,10 +90,12 @@ module ProjectsHelper
end
def project_for_deploy_key(deploy_key)
- if deploy_key.projects.include?(@project)
+ if deploy_key.has_access_to?(@project)
@project
else
- deploy_key.projects.find { |project| can?(current_user, :read_project, project) }
+ deploy_key.projects.find do |project|
+ can?(current_user, :read_project, project)
+ end
end
end
@@ -171,48 +173,27 @@ module ProjectsHelper
nav_tabs << :merge_requests
end
- if can?(current_user, :read_pipeline, project)
- nav_tabs << :pipelines
- end
-
- if can?(current_user, :read_build, project)
- nav_tabs << :builds
- end
-
if Gitlab.config.registry.enabled && can?(current_user, :read_container_image, project)
nav_tabs << :container_registry
end
- if can?(current_user, :read_environment, project)
- nav_tabs << :environments
- end
-
- if can?(current_user, :admin_project, project)
- nav_tabs << :settings
- end
-
- if can?(current_user, :read_project_member, project)
- nav_tabs << :team
- end
-
- if can?(current_user, :read_issue, project)
- nav_tabs << :issues
- end
-
- if can?(current_user, :read_wiki, project)
- nav_tabs << :wiki
- end
-
- if can?(current_user, :read_project_snippet, project)
- nav_tabs << :snippets
- end
-
- if can?(current_user, :read_label, project)
- nav_tabs << :labels
- end
+ tab_ability_map = {
+ environments: :read_environment,
+ milestones: :read_milestone,
+ pipelines: :read_pipeline,
+ snippets: :read_project_snippet,
+ settings: :admin_project,
+ builds: :read_build,
+ labels: :read_label,
+ issues: :read_issue,
+ team: :read_project_member,
+ wiki: :read_wiki
+ }
- if can?(current_user, :read_milestone, project)
- nav_tabs << :milestones
+ tab_ability_map.each do |tab, ability|
+ if can?(current_user, ability, project)
+ nav_tabs << tab
+ end
end
nav_tabs.flatten
@@ -246,11 +227,6 @@ module ProjectsHelper
end
end
- def repository_size(project = @project)
- size_in_bytes = project.repository_size * 1.megabyte
- number_to_human_size(size_in_bytes, delimiter: ',', precision: 2)
- end
-
def default_url_to_repo(project = @project)
case default_clone_protocol
when 'ssh'
@@ -280,13 +256,15 @@ module ProjectsHelper
end
end
- def add_special_file_path(project, file_name:, commit_message: nil)
+ def add_special_file_path(project, file_name:, commit_message: nil, target_branch: nil, context: nil)
namespace_project_new_blob_path(
project.namespace,
project,
project.default_branch || 'master',
file_name: file_name,
- commit_message: commit_message || "Add #{file_name.downcase}"
+ commit_message: commit_message || "Add #{file_name.downcase}",
+ target_branch: target_branch,
+ context: context
)
end
@@ -396,20 +374,6 @@ module ProjectsHelper
[@project.path_with_namespace, sha, "readme"].join('-')
end
- def round_commit_count(project)
- count = project.commit_count
-
- if count > 10000
- '10000+'
- elsif count > 5000
- '5000+'
- elsif count > 1000
- '1000+'
- else
- count
- end
- end
-
def current_ref
@ref || @repository.try(:root_ref)
end
@@ -445,4 +409,15 @@ module ProjectsHelper
def project_issues(project)
IssuesFinder.new(current_user, project_id: project.id).execute
end
+
+ def visibility_select_options(project, selected_level)
+ levels_options_array = Gitlab::VisibilityLevel.values.map do |level|
+ [
+ visibility_level_label(level),
+ { data: { description: visibility_level_description(level, project) } },
+ level
+ ]
+ end
+ options_for_select(levels_options_array, selected_level)
+ end
end
diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb
index cdb9663877c..8ff8db16514 100644
--- a/app/helpers/search_helper.rb
+++ b/app/helpers/search_helper.rb
@@ -39,7 +39,7 @@ module SearchHelper
# Autocomplete results for various settings pages
def default_autocomplete
[
- { category: "Settings", label: "Profile settings", url: profile_path },
+ { category: "Settings", label: "User settings", url: profile_path },
{ category: "Settings", label: "SSH Keys", url: profile_keys_path },
{ category: "Settings", label: "Dashboard", url: root_path },
{ category: "Settings", label: "Admin Section", url: admin_root_path },
@@ -56,7 +56,7 @@ module SearchHelper
{ category: "Help", label: "Rake Tasks Help", url: help_page_path("raketasks/README") },
{ category: "Help", label: "SSH Keys Help", url: help_page_path("ssh/README") },
{ category: "Help", label: "System Hooks Help", url: help_page_path("system_hooks/system_hooks") },
- { category: "Help", label: "Webhooks Help", url: help_page_path("web_hooks/web_hooks") },
+ { category: "Help", label: "Webhooks Help", url: help_page_path("user/project/integrations/webhooks") },
{ category: "Help", label: "Workflow Help", url: help_page_path("workflow/README") },
]
end
@@ -75,7 +75,7 @@ module SearchHelper
{ category: "Current Project", label: "Merge Requests", url: namespace_project_merge_requests_path(@project.namespace, @project) },
{ category: "Current Project", label: "Milestones", url: namespace_project_milestones_path(@project.namespace, @project) },
{ category: "Current Project", label: "Snippets", url: namespace_project_snippets_path(@project.namespace, @project) },
- { category: "Current Project", label: "Members", url: namespace_project_project_members_path(@project.namespace, @project) },
+ { category: "Current Project", label: "Members", url: namespace_project_settings_members_path(@project.namespace, @project) },
{ category: "Current Project", label: "Wiki", url: namespace_project_wikis_path(@project.namespace, @project) },
]
else
@@ -89,7 +89,7 @@ module SearchHelper
{
category: "Groups",
id: group.id,
- label: "#{search_result_sanitize(group.name)}",
+ label: "#{search_result_sanitize(group.full_name)}",
url: group_path(group)
}
end
diff --git a/app/helpers/services_helper.rb b/app/helpers/services_helper.rb
index 9bab140e60a..715e5893a2c 100644
--- a/app/helpers/services_helper.rb
+++ b/app/helpers/services_helper.rb
@@ -1,23 +1,23 @@
module ServicesHelper
def service_event_description(event)
case event
- when "push"
+ when "push", "push_events"
"Event will be triggered by a push to the repository"
- when "tag_push"
+ when "tag_push", "tag_push_events"
"Event will be triggered when a new tag is pushed to the repository"
- when "note"
+ when "note", "note_events"
"Event will be triggered when someone adds a comment"
- when "issue"
+ when "issue", "issue_events"
"Event will be triggered when an issue is created/updated/closed"
- when "confidential_issue"
+ when "confidential_issue", "confidential_issue_events"
"Event will be triggered when a confidential issue is created/updated/closed"
- when "merge_request"
+ when "merge_request", "merge_request_events"
"Event will be triggered when a merge request is created/updated/merged"
- when "build"
+ when "build", "build_events"
"Event will be triggered when a build status changes"
- when "wiki_page"
+ when "wiki_page", "wiki_page_events"
"Event will be triggered when a wiki page is created/updated"
- when "commit"
+ when "commit", "commit_events"
"Event will be triggered when a commit is created/updated"
end
end
@@ -26,4 +26,6 @@ module ServicesHelper
event = event.pluralize if %w[merge_request issue confidential_issue].include?(event)
"#{event}_events"
end
+
+ extend self
end
diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb
index f03c4627050..ff787fb4131 100644
--- a/app/helpers/sorting_helper.rb
+++ b/app/helpers/sorting_helper.rb
@@ -11,6 +11,7 @@ module SortingHelper
sort_value_due_date_soon => sort_title_due_date_soon,
sort_value_due_date_later => sort_title_due_date_later,
sort_value_largest_repo => sort_title_largest_repo,
+ sort_value_largest_group => sort_title_largest_group,
sort_value_recently_signin => sort_title_recently_signin,
sort_value_oldest_signin => sort_title_oldest_signin,
sort_value_downvotes => sort_title_downvotes,
@@ -92,6 +93,10 @@ module SortingHelper
'Largest repository'
end
+ def sort_title_largest_group
+ 'Largest group'
+ end
+
def sort_title_recently_signin
'Recent sign in'
end
@@ -193,7 +198,11 @@ module SortingHelper
end
def sort_value_largest_repo
- 'repository_size_desc'
+ 'storage_size_desc'
+ end
+
+ def sort_value_largest_group
+ 'storage_size_desc'
end
def sort_value_recently_signin
diff --git a/app/helpers/storage_helper.rb b/app/helpers/storage_helper.rb
new file mode 100644
index 00000000000..e19c67a37ca
--- /dev/null
+++ b/app/helpers/storage_helper.rb
@@ -0,0 +1,7 @@
+module StorageHelper
+ def storage_counter(size_in_bytes)
+ precision = size_in_bytes < 1.megabyte ? 0 : 1
+
+ number_to_human_size(size_in_bytes, delimiter: ',', precision: precision, significant: false)
+ end
+end
diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb
index 09c69786791..c568cca9e5e 100644
--- a/app/helpers/todos_helper.rb
+++ b/app/helpers/todos_helper.rb
@@ -11,9 +11,10 @@ module TodosHelper
case todo.action
when Todo::ASSIGNED then 'assigned you'
when Todo::MENTIONED then 'mentioned you on'
- when Todo::BUILD_FAILED then 'The build failed for your'
+ when Todo::BUILD_FAILED then 'The build failed for'
when Todo::MARKED then 'added a todo for'
when Todo::APPROVAL_REQUIRED then 'set you as an approver for'
+ when Todo::UNMERGEABLE then 'Could not merge'
end
end
@@ -35,7 +36,7 @@ module TodosHelper
else
path = [todo.project.namespace.becomes(Namespace), todo.project, todo.target]
- path.unshift(:builds) if todo.build_failed?
+ path.unshift(:pipelines) if todo.build_failed?
polymorphic_path(path, anchor: anchor)
end
diff --git a/app/helpers/version_check_helper.rb b/app/helpers/version_check_helper.rb
index a674564c4ec..456598b4c28 100644
--- a/app/helpers/version_check_helper.rb
+++ b/app/helpers/version_check_helper.rb
@@ -1,7 +1,8 @@
module VersionCheckHelper
def version_status_badge
if Rails.env.production? && current_application_settings.version_check_enabled
- image_tag VersionCheck.new.url
+ image_url = VersionCheck.new.url
+ image_tag image_url, class: 'js-version-status-badge'
end
end
end
diff --git a/app/helpers/visibility_level_helper.rb b/app/helpers/visibility_level_helper.rb
index 3a83ae15dd8..fc93acfe63e 100644
--- a/app/helpers/visibility_level_helper.rb
+++ b/app/helpers/visibility_level_helper.rb
@@ -93,10 +93,6 @@ module VisibilityLevelHelper
current_application_settings.default_project_visibility
end
- def default_snippet_visibility
- current_application_settings.default_snippet_visibility
- end
-
def default_group_visibility
current_application_settings.default_group_visibility
end
diff --git a/app/mailers/emails/notes.rb b/app/mailers/emails/notes.rb
index 0d20c9092c4..46fa6fd9f6d 100644
--- a/app/mailers/emails/notes.rb
+++ b/app/mailers/emails/notes.rb
@@ -38,6 +38,14 @@ module Emails
mail_answer_thread(@snippet, note_thread_options(recipient_id))
end
+ def note_personal_snippet_email(recipient_id, note_id)
+ setup_note_mail(note_id, recipient_id)
+
+ @snippet = @note.noteable
+ @target_url = snippet_url(@note.noteable)
+ mail_answer_thread(@snippet, note_thread_options(recipient_id))
+ end
+
private
def note_target_url_options
diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb
index 0bc1c19e9cd..0cd3456b4de 100644
--- a/app/mailers/notify.rb
+++ b/app/mailers/notify.rb
@@ -107,15 +107,11 @@ class Notify < BaseMailer
def mail_thread(model, headers = {})
add_project_headers
+ add_unsubscription_headers_and_links
+
headers["X-GitLab-#{model.class.name}-ID"] = model.id
headers['X-GitLab-Reply-Key'] = reply_key
- if !@labels_url && @sent_notification && @sent_notification.unsubscribable?
- headers['List-Unsubscribe'] = "<#{unsubscribe_sent_notification_url(@sent_notification, force: true)}>"
-
- @sent_notification_url = unsubscribe_sent_notification_url(@sent_notification)
- end
-
if Gitlab::IncomingEmail.enabled?
address = Mail::Address.new(Gitlab::IncomingEmail.reply_address(reply_key))
address.display_name = @project.name_with_namespace
@@ -171,4 +167,16 @@ class Notify < BaseMailer
headers['X-GitLab-Project-Id'] = @project.id
headers['X-GitLab-Project-Path'] = @project.path_with_namespace
end
+
+ def add_unsubscription_headers_and_links
+ return unless !@labels_url && @sent_notification && @sent_notification.unsubscribable?
+
+ list_unsubscribe_methods = [unsubscribe_sent_notification_url(@sent_notification, force: true)]
+ if Gitlab::IncomingEmail.enabled? && Gitlab::IncomingEmail.supports_wildcard?
+ list_unsubscribe_methods << "mailto:#{Gitlab::IncomingEmail.unsubscribe_address(reply_key)}"
+ end
+
+ headers['List-Unsubscribe'] = list_unsubscribe_methods.map { |e| "<#{e}>" }.join(',')
+ @sent_notification_url = unsubscribe_sent_notification_url(@sent_notification)
+ end
end
diff --git a/app/models/ability.rb b/app/models/ability.rb
index fa8f8bc3a5f..ad6c588202e 100644
--- a/app/models/ability.rb
+++ b/app/models/ability.rb
@@ -22,6 +22,17 @@ class Ability
end
end
+ # Given a list of users and a snippet this method returns the users that can
+ # read the given snippet.
+ def users_that_can_read_personal_snippet(users, snippet)
+ case snippet.visibility_level
+ when Snippet::INTERNAL, Snippet::PUBLIC
+ users
+ when Snippet::PRIVATE
+ users.include?(snippet.author) ? [snippet.author] : []
+ 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.
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index bf463a3b6bb..9a4557524c4 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -68,6 +68,10 @@ class ApplicationSetting < ActiveRecord::Base
presence: true,
if: :koding_enabled
+ validates :plantuml_url,
+ presence: true,
+ if: :plantuml_enabled
+
validates :max_attachment_size,
presence: true,
numericality: { only_integer: true, greater_than: 0 }
@@ -107,6 +111,10 @@ class ApplicationSetting < ActiveRecord::Base
presence: true,
numericality: { only_integer: true, greater_than: :housekeeping_full_repack_period }
+ validates :terminal_max_session_time,
+ presence: true,
+ numericality: { only_integer: true, greater_than_or_equal_to: 0 }
+
validates_each :restricted_visibility_levels do |record, attr, value|
unless value.nil?
value.each do |level|
@@ -152,51 +160,65 @@ class ApplicationSetting < ActiveRecord::Base
def self.expire
Rails.cache.delete(CACHE_KEY)
+ rescue
+ # Gracefully handle when Redis is not available. For example,
+ # omnibus may fail here during gitlab:assets:compile.
end
def self.cached
Rails.cache.fetch(CACHE_KEY)
end
- def self.create_from_defaults
- create(
- default_projects_limit: Settings.gitlab['default_projects_limit'],
- default_branch_protection: Settings.gitlab['default_branch_protection'],
- signup_enabled: Settings.gitlab['signup_enabled'],
- signin_enabled: Settings.gitlab['signin_enabled'],
- gravatar_enabled: Settings.gravatar['enabled'],
- sign_in_text: nil,
+ def self.defaults_ce
+ {
after_sign_up_text: nil,
- help_page_text: nil,
- shared_runners_text: nil,
- restricted_visibility_levels: Settings.gitlab['restricted_visibility_levels'],
- max_attachment_size: Settings.gitlab['max_attachment_size'],
- session_expire_delay: Settings.gitlab['session_expire_delay'],
+ akismet_enabled: false,
+ container_registry_token_expire_delay: 5,
+ default_branch_protection: Settings.gitlab['default_branch_protection'],
default_project_visibility: Settings.gitlab.default_projects_features['visibility_level'],
+ default_projects_limit: Settings.gitlab['default_projects_limit'],
default_snippet_visibility: Settings.gitlab.default_projects_features['visibility_level'],
+ disabled_oauth_sign_in_sources: [],
domain_whitelist: Settings.gitlab['domain_whitelist'],
+ gravatar_enabled: Settings.gravatar['enabled'],
+ help_page_text: nil,
+ housekeeping_bitmaps_enabled: true,
+ housekeeping_enabled: true,
+ housekeeping_full_repack_period: 50,
+ housekeeping_gc_period: 200,
+ housekeeping_incremental_repack_period: 10,
import_sources: Gitlab::ImportSources.values,
- 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,
+ max_artifacts_size: Settings.artifacts['max_size'],
+ max_attachment_size: Settings.gitlab['max_attachment_size'],
+ plantuml_enabled: false,
+ plantuml_url: nil,
+ recaptcha_enabled: false,
repository_checks_enabled: true,
- disabled_oauth_sign_in_sources: [],
- send_user_confirmation_email: false,
- container_registry_token_expire_delay: 5,
repository_storages: ['default'],
- user_default_external: false,
+ require_two_factor_authentication: false,
+ restricted_visibility_levels: Settings.gitlab['restricted_visibility_levels'],
+ session_expire_delay: Settings.gitlab['session_expire_delay'],
+ send_user_confirmation_email: false,
+ shared_runners_enabled: Settings.gitlab_ci['shared_runners_enabled'],
+ shared_runners_text: nil,
sidekiq_throttling_enabled: false,
- housekeeping_enabled: true,
- housekeeping_bitmaps_enabled: true,
- housekeeping_incremental_repack_period: 10,
- housekeeping_full_repack_period: 50,
- housekeeping_gc_period: 200,
- )
+ sign_in_text: nil,
+ signin_enabled: Settings.gitlab['signin_enabled'],
+ signup_enabled: Settings.gitlab['signup_enabled'],
+ two_factor_grace_period: 48,
+ user_default_external: false,
+ terminal_max_session_time: 0
+ }
+ end
+
+ def self.defaults
+ defaults_ce
+ end
+
+ def self.create_from_defaults
+ create(defaults)
end
def home_page_url_column_exist
diff --git a/app/models/board.rb b/app/models/board.rb
index c56422914a9..2780acc67c0 100644
--- a/app/models/board.rb
+++ b/app/models/board.rb
@@ -5,10 +5,6 @@ class Board < ActiveRecord::Base
validates :project, presence: true
- def backlog_list
- lists.merge(List.backlog).take
- end
-
def done_list
lists.merge(List.done).take
end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index cb76cdf5981..8c1b076c2d7 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -2,12 +2,14 @@ module Ci
class Build < CommitStatus
include TokenAuthenticatable
include AfterCommitQueue
+ include Presentable
belongs_to :runner
belongs_to :trigger_request
belongs_to :erased_by, class_name: 'User'
has_many :deployments, as: :deployable
+ has_one :last_deployment, -> { order('deployments.id DESC') }, as: :deployable, class_name: 'Deployment'
# The "environment" field for builds is a String, and is the unexpanded name
def persisted_environment
@@ -18,7 +20,7 @@ module Ci
end
serialize :options
- serialize :yaml_variables, Gitlab::Serialize::Ci::Variables
+ serialize :yaml_variables, Gitlab::Serializer::Ci::Variables
validates :coverage, numericality: true, allow_blank: true
validates_presence_of :ref
@@ -40,9 +42,11 @@ module Ci
before_save :update_artifacts_size, if: :artifacts_file_changed?
before_save :ensure_token
- before_destroy { project }
+ before_destroy { unscoped_project }
after_create :execute_hooks
+ after_save :update_project_statistics, if: :artifacts_size_changed?
+ after_destroy :update_project_statistics
class << self
def first_pending
@@ -89,6 +93,12 @@ module Ci
end
state_machine :status do
+ after_transition any => [:pending] do |build|
+ build.run_after_commit do
+ BuildQueueWorker.perform_async(id)
+ end
+ end
+
after_transition pending: :running do |build|
build.run_after_commit do
BuildHooksWorker.perform_async(id)
@@ -174,10 +184,6 @@ module Ci
success? && !last_deployment.try(:last?)
end
- def last_deployment
- deployments.last
- end
-
def depends_on_builds
# Get builds of the same type
latest_builds = self.pipeline.builds.latest
@@ -247,7 +253,7 @@ module Ci
end
def project_id
- pipeline.project_id
+ gl_project_id
end
def project_name
@@ -266,29 +272,23 @@ module Ci
end
def update_coverage
- return unless project
- coverage_regex = project.build_coverage_regex
- return unless coverage_regex
coverage = extract_coverage(trace, coverage_regex)
-
- if coverage.is_a? Numeric
- update_attributes(coverage: coverage)
- end
+ update_attributes(coverage: coverage) if coverage.present?
end
def extract_coverage(text, regex)
- begin
- matches = text.scan(Regexp.new(regex)).last
- matches = matches.last if matches.kind_of?(Array)
- coverage = matches.gsub(/\d+(\.\d+)?/).first
+ return unless regex
- if coverage.present?
- coverage.to_f
- end
- rescue
- # if bad regex or something goes wrong we dont want to interrupt transition
- # so we just silentrly ignore error for now
+ matches = text.scan(Regexp.new(regex)).last
+ matches = matches.last if matches.kind_of?(Array)
+ coverage = matches.gsub(/\d+(\.\d+)?/).first
+
+ if coverage.present?
+ coverage.to_f
end
+ rescue
+ # if bad regex or something goes wrong we dont want to interrupt transition
+ # so we just silentrly ignore error for now
end
def has_trace_file?
@@ -413,16 +413,23 @@ module Ci
# This method returns old path to artifacts only if it already exists.
#
def artifacts_path
+ # We need the project even if it's soft deleted, because whenever
+ # we're really deleting the project, we'll also delete the builds,
+ # and in order to delete the builds, we need to know where to find
+ # the artifacts, which is depending on the data of the project.
+ # We need to retain the project in this case.
+ the_project = project || unscoped_project
+
old = File.join(created_at.utc.strftime('%Y_%m'),
- project.ci_id.to_s,
+ the_project.ci_id.to_s,
id.to_s)
old_store = File.join(ArtifactUploader.artifacts_path, old)
- return old if project.ci_id && File.directory?(old_store)
+ return old if the_project.ci_id && File.directory?(old_store)
File.join(
created_at.utc.strftime('%Y_%m'),
- project.id.to_s,
+ the_project.id.to_s,
id.to_s
)
end
@@ -448,6 +455,7 @@ module Ci
build_data = Gitlab::DataBuilder::Build.build(self)
project.execute_hooks(build_data.dup, :build_hooks)
project.execute_services(build_data.dup, :build_hooks)
+ PagesService.new(build_data).execute
project.running_or_pending_build_count(force: true)
end
@@ -505,10 +513,18 @@ module Ci
end
end
+ def has_expiring_artifacts?
+ artifacts_expire_at.present?
+ end
+
def keep_artifacts!
self.update(artifacts_expire_at: nil)
end
+ def coverage_regex
+ super || project.try(:build_coverage_regex)
+ end
+
def when
read_attribute(:when) || build_attributes_from_config[:when] || 'on_success'
end
@@ -548,6 +564,10 @@ module Ci
self.update(erased_by: user, erased_at: Time.now, artifacts_expire_at: nil)
end
+ def unscoped_project
+ @unscoped_project ||= Project.unscoped.find_by(id: gl_project_id)
+ end
+
def predefined_variables
variables = [
{ key: 'CI', value: 'true', public: true },
@@ -584,5 +604,11 @@ module Ci
Ci::MaskSecret.mask!(trace, token)
trace
end
+
+ def update_project_statistics
+ return unless project
+
+ ProjectCacheWorker.perform_async(project_id, [], [:build_artifacts_size])
+ end
end
end
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index f2f6453b3b9..bbc358adb83 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -93,11 +93,8 @@ module Ci
.select("max(#{quoted_table_name}.id)")
.group(:ref, :sha)
- if ref
- where(id: max_id, ref: ref)
- else
- where(id: max_id)
- end
+ relation = ref ? where(ref: ref) : self
+ relation.where(id: max_id)
end
def self.latest_status(ref = nil)
@@ -105,7 +102,7 @@ module Ci
end
def self.latest_successful_for(ref)
- success.latest(ref).first
+ success.latest(ref).order(id: :desc).first
end
def self.truncate_sha(sha)
@@ -131,21 +128,26 @@ module Ci
end
def stages
+ # TODO, this needs refactoring, see gitlab-ce#26481.
+
+ stages_query = statuses
+ .group('stage').select(:stage).order('max(stage_idx)')
+
status_sql = statuses.latest.where('stage=sg.stage').status_sql
- stages_query = statuses.group('stage').select(:stage)
- .order('max(stage_idx)')
+ warnings_sql = statuses.latest.select('COUNT(*) > 0')
+ .where('stage=sg.stage').failed_but_allowed.to_sql
- stages_with_statuses = CommitStatus.from(stages_query, :sg).
- pluck('sg.stage', status_sql)
+ stages_with_statuses = CommitStatus.from(stages_query, :sg)
+ .pluck('sg.stage', status_sql, "(#{warnings_sql})")
stages_with_statuses.map do |stage|
- Ci::Stage.new(self, name: stage.first, status: stage.last)
+ Ci::Stage.new(self, Hash[%i[name status warnings].zip(stage)])
end
end
def artifacts
- builds.latest.with_artifacts_not_expired
+ builds.latest.with_artifacts_not_expired.includes(project: [:namespace])
end
def project_id
@@ -194,7 +196,11 @@ module Ci
end
def manual_actions
- builds.latest.manual_actions
+ builds.latest.manual_actions.includes(project: [:namespace])
+ end
+
+ def stuck?
+ builds.pending.any?(&:stuck?)
end
def retryable?
@@ -277,13 +283,11 @@ module Ci
def ci_yaml_file
return @ci_yaml_file if defined?(@ci_yaml_file)
- @ci_yaml_file ||= begin
- blob = project.repository.blob_at(sha, '.gitlab-ci.yml')
- blob.load_all_data!(project.repository)
- blob.data
- rescue
- nil
- end
+ @ci_yaml_file = project.repository.gitlab_ci_yml_for(sha) rescue nil
+ end
+
+ def has_yaml_errors?
+ yaml_errors.present?
end
def environments
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index 123930273e0..ed1843ba005 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -2,6 +2,7 @@ module Ci
class Runner < ActiveRecord::Base
extend Ci::Model
+ RUNNER_QUEUE_EXPIRY_TIME = 60.minutes
LAST_CONTACT_TIME = 1.hour.ago
AVAILABLE_SCOPES = %w[specific shared active paused online]
FORM_EDITABLE = %i[description tag_list active run_untagged locked]
@@ -21,6 +22,8 @@ module Ci
scope :online, ->() { where('contacted_at > ?', LAST_CONTACT_TIME) }
scope :ordered, ->() { order(id: :desc) }
+ after_save :tick_runner_queue, if: :form_editable_changed?
+
scope :owned_or_shared, ->(project_id) do
joins('LEFT JOIN ci_runner_projects ON ci_runner_projects.runner_id = ci_runners.id')
.where("ci_runner_projects.gl_project_id = :project_id OR ci_runners.is_shared = true", project_id: project_id)
@@ -122,8 +125,38 @@ module Ci
]
end
+ def tick_runner_queue
+ SecureRandom.hex.tap do |new_update|
+ Gitlab::Redis.with do |redis|
+ redis.set(runner_queue_key, new_update, ex: RUNNER_QUEUE_EXPIRY_TIME)
+ end
+ end
+ end
+
+ def ensure_runner_queue_value
+ Gitlab::Redis.with do |redis|
+ value = SecureRandom.hex
+ redis.set(runner_queue_key, value, ex: RUNNER_QUEUE_EXPIRY_TIME, nx: true)
+ redis.get(runner_queue_key)
+ end
+ end
+
+ def is_runner_queue_value_latest?(value)
+ ensure_runner_queue_value == value if value.present?
+ end
+
private
+ def runner_queue_key
+ "runner:build_queue:#{self.token}"
+ end
+
+ def form_editable_changed?
+ FORM_EDITABLE.any? do |editable|
+ public_send("#{editable}_changed?")
+ end
+ end
+
def tag_constraints
unless has_tags? || run_untagged?
errors.add(:tags_list,
diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb
index d035eda6df5..ca74c91b062 100644
--- a/app/models/ci/stage.rb
+++ b/app/models/ci/stage.rb
@@ -8,10 +8,11 @@ module Ci
delegate :project, to: :pipeline
- def initialize(pipeline, name:, status: nil)
+ def initialize(pipeline, name:, status: nil, warnings: nil)
@pipeline = pipeline
@name = name
@status = status
+ @warnings = warnings
end
def to_param
@@ -39,5 +40,17 @@ module Ci
def builds
@builds ||= pipeline.builds.where(stage: name)
end
+
+ def success?
+ status.to_s == 'success'
+ end
+
+ def has_warnings?
+ if @warnings.nil?
+ statuses.latest.failed_but_allowed.any?
+ else
+ @warnings
+ end
+ end
end
end
diff --git a/app/models/commit.rb b/app/models/commit.rb
index 69cfc47f5bf..46f06733da1 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -21,6 +21,9 @@ class Commit
DIFF_HARD_LIMIT_FILES = 1000
DIFF_HARD_LIMIT_LINES = 50000
+ # The SHA can be between 7 and 40 hex characters.
+ COMMIT_SHA_PATTERN = '\h{7,40}'
+
class << self
def decorate(commits, project)
commits.map do |commit|
@@ -52,6 +55,10 @@ class Commit
def from_hash(hash, project)
new(Gitlab::Git::Commit.new(hash), project)
end
+
+ def valid_hash?(key)
+ !!(/\A#{COMMIT_SHA_PATTERN}\z/ =~ key)
+ end
end
attr_accessor :raw
@@ -77,8 +84,6 @@ class Commit
# Pattern used to extract commit references from text
#
- # The SHA can be between 7 and 40 hex characters.
- #
# This pattern supports cross-project references.
def self.reference_pattern
@reference_pattern ||= %r{
@@ -88,15 +93,15 @@ class Commit
end
def self.link_reference_pattern
- @link_reference_pattern ||= super("commit", /(?<commit>\h{7,40})/)
+ @link_reference_pattern ||= super("commit", /(?<commit>#{COMMIT_SHA_PATTERN})/)
end
- def to_reference(from_project = nil)
- commit_reference(from_project, id)
+ def to_reference(from_project = nil, full: false)
+ commit_reference(from_project, id, full: full)
end
- def reference_link_text(from_project = nil)
- commit_reference(from_project, short_id)
+ def reference_link_text(from_project = nil, full: false)
+ commit_reference(from_project, short_id, full: full)
end
def diff_line_count
@@ -318,10 +323,24 @@ class Commit
Gitlab::Diff::FileCollection::Commit.new(self, diff_options: diff_options)
end
+ def persisted?
+ true
+ end
+
+ def touch
+ # no-op but needs to be defined since #persisted? is defined
+ end
+
+ WIP_REGEX = /\A\s*(((?i)(\[WIP\]|WIP:|WIP)\s|WIP$))|(fixup!|squash!)\s/.freeze
+
+ def work_in_progress?
+ !!(title =~ WIP_REGEX)
+ end
+
private
- def commit_reference(from_project, referable_commit_id)
- reference = project.to_reference(from_project)
+ def commit_reference(from_project, referable_commit_id, full: false)
+ reference = project.to_reference(from_project, full: full)
if reference.present?
"#{reference}#{self.class.reference_prefix}#{referable_commit_id}"
diff --git a/app/models/commit_range.rb b/app/models/commit_range.rb
index d9af7f6c139..84e2e8a5dd5 100644
--- a/app/models/commit_range.rb
+++ b/app/models/commit_range.rb
@@ -89,8 +89,8 @@ class CommitRange
alias_method :id, :to_s
- def to_reference(from_project = nil)
- project_reference = project.to_reference(from_project)
+ def to_reference(from_project = nil, full: false)
+ project_reference = project.to_reference(from_project, full: full)
if project_reference.present?
project_reference + self.class.reference_prefix + self.id
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index 31cd381dcd2..9547c57b2ae 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -137,4 +137,10 @@ class CommitStatus < ActiveRecord::Base
.new(self, current_user)
.fabricate!
end
+
+ def sortable_name
+ name.split(/(\d+)/).map do |v|
+ v =~ /\d+/ ? v.to_i : v
+ end
+ end
end
diff --git a/app/models/concerns/cache_markdown_field.rb b/app/models/concerns/cache_markdown_field.rb
index 90bd6490a02..a600f9c14c5 100644
--- a/app/models/concerns/cache_markdown_field.rb
+++ b/app/models/concerns/cache_markdown_field.rb
@@ -51,6 +51,10 @@ module CacheMarkdownField
CACHING_CLASSES.map(&:constantize)
end
+ def skip_project_check?
+ false
+ end
+
extend ActiveSupport::Concern
included do
@@ -112,7 +116,8 @@ module CacheMarkdownField
invalidation_method = "#{html_field}_invalidated?".to_sym
define_method(cache_method) do
- html = Banzai::Renderer.cacheless_render_field(self, markdown_field)
+ options = { skip_project_check: skip_project_check? }
+ html = Banzai::Renderer.cacheless_render_field(self, markdown_field, options)
__send__("#{html_field}=", html)
true
end
diff --git a/app/models/concerns/has_status.rb b/app/models/concerns/has_status.rb
index 90432fc4050..431c0354969 100644
--- a/app/models/concerns/has_status.rb
+++ b/app/models/concerns/has_status.rb
@@ -1,6 +1,7 @@
module HasStatus
extend ActiveSupport::Concern
+ DEFAULT_STATUS = 'created'
AVAILABLE_STATUSES = %w[created pending running success failed canceled skipped]
STARTED_STATUSES = %w[running success failed skipped]
ACTIVE_STATUSES = %w[pending running]
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 0ea7b1b1098..3517969eabc 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -13,6 +13,7 @@ module Issuable
include StripAttribute
include Awardable
include Taskable
+ include TimeTrackable
included do
cache_markdown_field :title, pipeline: :single_line
@@ -92,8 +93,9 @@ module Issuable
after_save :record_metrics
def update_assignee_cache_counts
- # make sure we flush the cache for both the old *and* new assignee
- User.find(assignee_id_was).update_cache_counts if assignee_id_was
+ # make sure we flush the cache for both the old *and* new assignees(if they exist)
+ previous_assignee = User.find_by_id(assignee_id_was) if assignee_id_was
+ previous_assignee.update_cache_counts if previous_assignee
assignee.update_cache_counts if assignee
end
diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb
index 8ab0401d288..ef2c1e5d414 100644
--- a/app/models/concerns/mentionable.rb
+++ b/app/models/concerns/mentionable.rb
@@ -49,7 +49,11 @@ module Mentionable
self.class.mentionable_attrs.each do |attr, options|
text = __send__(attr)
- options = options.merge(cache_key: [self, attr], author: author)
+ options = options.merge(
+ cache_key: [self, attr],
+ author: author,
+ skip_project_check: skip_project_check?
+ )
extractor.analyze(text, options)
end
@@ -121,4 +125,8 @@ module Mentionable
def cross_reference_exists?(target)
SystemNoteService.cross_reference_exists?(target, local_reference)
end
+
+ def skip_project_check?
+ false
+ end
end
diff --git a/app/models/concerns/milestoneish.rb b/app/models/concerns/milestoneish.rb
index 8f02c226f0b..e9450dd0c26 100644
--- a/app/models/concerns/milestoneish.rb
+++ b/app/models/concerns/milestoneish.rb
@@ -7,11 +7,14 @@ module Milestoneish
def total_items_count(user)
memoize_per_user(user, :total_items_count) do
- issues_count = count_issues_by_state(user).values.sum
- issues_count + merge_requests.size
+ total_issues_count(user) + merge_requests.size
end
end
+ def total_issues_count(user)
+ count_issues_by_state(user).values.sum
+ end
+
def complete?(user)
total_items_count(user) > 0 && total_items_count(user) == closed_items_count(user)
end
@@ -36,8 +39,8 @@ module Milestoneish
def issues_visible_to_user(user)
memoize_per_user(user, :issues_visible_to_user) do
- params = try(:project_id) ? { project_id: project_id } : {}
- IssuesFinder.new(user, params).execute.where(milestone_id: milestoneish_ids)
+ IssuesFinder.new(user, issues_finder_params)
+ .execute.where(milestone_id: milestoneish_ids)
end
end
@@ -72,4 +75,10 @@ module Milestoneish
@memoized[method_name] ||= {}
@memoized[method_name][user.try!(:id)] ||= yield
end
+
+ # override in a class that includes this module to get a faster query
+ # from IssuesFinder
+ def issues_finder_params
+ {}
+ end
end
diff --git a/app/models/concerns/participable.rb b/app/models/concerns/participable.rb
index 70740c76e43..4865c0a14b1 100644
--- a/app/models/concerns/participable.rb
+++ b/app/models/concerns/participable.rb
@@ -96,6 +96,11 @@ module Participable
participants.merge(ext.users)
- Ability.users_that_can_read_project(participants.to_a, project)
+ case self
+ when PersonalSnippet
+ Ability.users_that_can_read_personal_snippet(participants.to_a, self)
+ else
+ Ability.users_that_can_read_project(participants.to_a, project)
+ end
end
end
diff --git a/app/models/concerns/presentable.rb b/app/models/concerns/presentable.rb
new file mode 100644
index 00000000000..7b33b837004
--- /dev/null
+++ b/app/models/concerns/presentable.rb
@@ -0,0 +1,7 @@
+module Presentable
+ def present(**attributes)
+ Gitlab::View::Presenter::Factory
+ .new(self, attributes)
+ .fabricate!
+ end
+end
diff --git a/app/models/concerns/project_features_compatibility.rb b/app/models/concerns/project_features_compatibility.rb
index 6d88951c713..60734bc6660 100644
--- a/app/models/concerns/project_features_compatibility.rb
+++ b/app/models/concerns/project_features_compatibility.rb
@@ -32,6 +32,6 @@ module ProjectFeaturesCompatibility
build_project_feature unless project_feature
access_level = Gitlab::Utils.to_boolean(value) ? ProjectFeature::ENABLED : ProjectFeature::DISABLED
- project_feature.update_attribute(field, access_level)
+ project_feature.send(:write_attribute, field, access_level)
end
end
diff --git a/app/models/concerns/reactive_caching.rb b/app/models/concerns/reactive_caching.rb
index 944519a3070..2589215ad19 100644
--- a/app/models/concerns/reactive_caching.rb
+++ b/app/models/concerns/reactive_caching.rb
@@ -55,30 +55,30 @@ module ReactiveCaching
self.reactive_cache_refresh_interval = 1.minute
self.reactive_cache_lifetime = 10.minutes
- def calculate_reactive_cache
+ def calculate_reactive_cache(*args)
raise NotImplementedError
end
- def with_reactive_cache(&blk)
- within_reactive_cache_lifetime do
- data = Rails.cache.read(full_reactive_cache_key)
+ def with_reactive_cache(*args, &blk)
+ within_reactive_cache_lifetime(*args) do
+ data = Rails.cache.read(full_reactive_cache_key(*args))
yield data if data.present?
end
ensure
- Rails.cache.write(full_reactive_cache_key('alive'), true, expires_in: self.class.reactive_cache_lifetime)
- ReactiveCachingWorker.perform_async(self.class, id)
+ Rails.cache.write(alive_reactive_cache_key(*args), true, expires_in: self.class.reactive_cache_lifetime)
+ ReactiveCachingWorker.perform_async(self.class, id, *args)
end
- def clear_reactive_cache!
- Rails.cache.delete(full_reactive_cache_key)
+ def clear_reactive_cache!(*args)
+ Rails.cache.delete(full_reactive_cache_key(*args))
end
- def exclusively_update_reactive_cache!
- locking_reactive_cache do
- within_reactive_cache_lifetime do
- enqueuing_update do
- value = calculate_reactive_cache
- Rails.cache.write(full_reactive_cache_key, value)
+ def exclusively_update_reactive_cache!(*args)
+ locking_reactive_cache(*args) do
+ within_reactive_cache_lifetime(*args) do
+ enqueuing_update(*args) do
+ value = calculate_reactive_cache(*args)
+ Rails.cache.write(full_reactive_cache_key(*args), value)
end
end
end
@@ -93,22 +93,26 @@ module ReactiveCaching
([prefix].flatten + qualifiers).join(':')
end
- def locking_reactive_cache
- lease = Gitlab::ExclusiveLease.new(full_reactive_cache_key, timeout: reactive_cache_lease_timeout)
+ def alive_reactive_cache_key(*qualifiers)
+ full_reactive_cache_key(*(qualifiers + ['alive']))
+ end
+
+ def locking_reactive_cache(*args)
+ lease = Gitlab::ExclusiveLease.new(full_reactive_cache_key(*args), timeout: reactive_cache_lease_timeout)
uuid = lease.try_obtain
yield if uuid
ensure
- Gitlab::ExclusiveLease.cancel(full_reactive_cache_key, uuid)
+ Gitlab::ExclusiveLease.cancel(full_reactive_cache_key(*args), uuid)
end
- def within_reactive_cache_lifetime
- yield if Rails.cache.read(full_reactive_cache_key('alive'))
+ def within_reactive_cache_lifetime(*args)
+ yield if Rails.cache.read(alive_reactive_cache_key(*args))
end
- def enqueuing_update
+ def enqueuing_update(*args)
yield
ensure
- ReactiveCachingWorker.perform_in(self.class.reactive_cache_refresh_interval, self.class, id)
+ ReactiveCachingWorker.perform_in(self.class.reactive_cache_refresh_interval, self.class, id, *args)
end
end
end
diff --git a/app/models/concerns/reactive_service.rb b/app/models/concerns/reactive_service.rb
new file mode 100644
index 00000000000..e1f868a299b
--- /dev/null
+++ b/app/models/concerns/reactive_service.rb
@@ -0,0 +1,10 @@
+module ReactiveService
+ extend ActiveSupport::Concern
+
+ included do
+ include ReactiveCaching
+
+ # Default cache key: class name + project_id
+ self.reactive_cache_key = ->(service) { [ service.class.model_name.singular, service.project_id ] }
+ end
+end
diff --git a/app/models/concerns/referable.rb b/app/models/concerns/referable.rb
index 8ba009fe04f..da803c7f481 100644
--- a/app/models/concerns/referable.rb
+++ b/app/models/concerns/referable.rb
@@ -17,7 +17,7 @@ module Referable
# Issue.last.to_reference(other_project) # => "cross-project#1"
#
# Returns a String
- def to_reference(_from_project = nil)
+ def to_reference(_from_project = nil, full:)
''
end
diff --git a/app/models/concerns/routable.rb b/app/models/concerns/routable.rb
index 1108a64c59e..2b93aa30c0f 100644
--- a/app/models/concerns/routable.rb
+++ b/app/models/concerns/routable.rb
@@ -60,6 +60,21 @@ module Routable
joins(:route).where(wheres.join(' OR '))
end
end
+
+ # Builds a relation to find multiple objects that are nested under user membership
+ #
+ # Usage:
+ #
+ # Klass.member_descendants(1)
+ #
+ # Returns an ActiveRecord::Relation.
+ def member_descendants(user_id)
+ joins(:route).
+ joins("INNER JOIN routes r2 ON routes.path LIKE CONCAT(r2.path, '/%')
+ INNER JOIN members ON members.source_id = r2.source_id
+ AND members.source_type = r2.source_type").
+ where('members.user_id = ?', user_id)
+ end
end
private
diff --git a/app/models/concerns/spammable.rb b/app/models/concerns/spammable.rb
index 1aa97debe42..423ae98a60e 100644
--- a/app/models/concerns/spammable.rb
+++ b/app/models/concerns/spammable.rb
@@ -11,6 +11,7 @@ module Spammable
has_one :user_agent_detail, as: :subject, dependent: :destroy
attr_accessor :spam
+ attr_accessor :spam_log
after_validation :check_for_spam, on: :create
@@ -34,7 +35,18 @@ module Spammable
end
def check_for_spam
- self.errors.add(:base, "Your #{self.class.name.underscore} has been recognized as spam and has been discarded.") if spam?
+ error_msg = if Gitlab::Recaptcha.enabled?
+ "Your #{spammable_entity_type} has been recognized as spam. "\
+ "You can still submit it by solving Captcha."
+ else
+ "Your #{spammable_entity_type} has been recognized as spam and has been discarded."
+ end
+
+ self.errors.add(:base, error_msg) if spam?
+ end
+
+ def spammable_entity_type
+ self.class.name.underscore
end
def spam_title
diff --git a/app/models/concerns/taskable.rb b/app/models/concerns/taskable.rb
index ebc75100a54..25e2d8ea24e 100644
--- a/app/models/concerns/taskable.rb
+++ b/app/models/concerns/taskable.rb
@@ -11,10 +11,10 @@ module Taskable
INCOMPLETE = 'incomplete'.freeze
ITEM_PATTERN = /
^
- (?:\s*[-+*]|(?:\d+\.))? # optional list prefix
- \s* # optional whitespace prefix
- (\[\s\]|\[[xX]\]) # checkbox
- (\s.+) # followed by whitespace and some text.
+ \s*(?:[-+*]|(?:\d+\.)) # list prefix required - task item has to be always in a list
+ \s+ # whitespace prefix has to be always presented for a list item
+ (\[\s\]|\[[xX]\]) # checkbox
+ (\s.+) # followed by whitespace and some text.
/x
def self.get_tasks(content)
diff --git a/app/models/concerns/time_trackable.rb b/app/models/concerns/time_trackable.rb
new file mode 100644
index 00000000000..9cf83440784
--- /dev/null
+++ b/app/models/concerns/time_trackable.rb
@@ -0,0 +1,72 @@
+# == TimeTrackable concern
+#
+# Contains functionality related to objects that support time tracking.
+#
+# Used by Issue and MergeRequest.
+#
+
+module TimeTrackable
+ extend ActiveSupport::Concern
+
+ included do
+ attr_reader :time_spent, :time_spent_user
+
+ alias_method :time_spent?, :time_spent
+
+ default_value_for :time_estimate, value: 0, allows_nil: false
+
+ validates :time_estimate, numericality: { message: 'has an invalid format' }, allow_nil: false
+ validate :check_negative_time_spent
+
+ has_many :timelogs, dependent: :destroy
+ end
+
+ def spend_time(options)
+ @time_spent = options[:duration]
+ @time_spent_user = options[:user]
+ @original_total_time_spent = nil
+
+ return if @time_spent == 0
+
+ if @time_spent == :reset
+ reset_spent_time
+ else
+ add_or_subtract_spent_time
+ end
+ end
+ alias_method :spend_time=, :spend_time
+
+ def total_time_spent
+ timelogs.sum(:time_spent)
+ end
+
+ def human_total_time_spent
+ Gitlab::TimeTrackingFormatter.output(total_time_spent)
+ end
+
+ def human_time_estimate
+ Gitlab::TimeTrackingFormatter.output(time_estimate)
+ end
+
+ private
+
+ def reset_spent_time
+ timelogs.new(time_spent: total_time_spent * -1, user: @time_spent_user)
+ end
+
+ def add_or_subtract_spent_time
+ timelogs.new(time_spent: time_spent, user: @time_spent_user)
+ end
+
+ def check_negative_time_spent
+ return if time_spent.nil? || time_spent == :reset
+
+ # we need to cache the total time spent so multiple calls to #valid?
+ # doesn't give a false error
+ @original_total_time_spent ||= total_time_spent
+
+ if time_spent < 0 && (time_spent.abs > @original_total_time_spent)
+ errors.add(:time_spent, 'Time to subtract exceeds the total time spent')
+ end
+ end
+end
diff --git a/app/models/concerns/valid_attribute.rb b/app/models/concerns/valid_attribute.rb
new file mode 100644
index 00000000000..8c35cea8d58
--- /dev/null
+++ b/app/models/concerns/valid_attribute.rb
@@ -0,0 +1,10 @@
+module ValidAttribute
+ extend ActiveSupport::Concern
+
+ # Checks whether an attribute has failed validation or not
+ #
+ # +attribute+ The symbolised name of the attribute i.e :name
+ def valid_attribute?(attribute)
+ self.errors.empty? || self.errors.messages[attribute].nil?
+ end
+end
diff --git a/app/models/cycle_analytics.rb b/app/models/cycle_analytics.rb
index ba4ee6fcf9d..d2e626c22e8 100644
--- a/app/models/cycle_analytics.rb
+++ b/app/models/cycle_analytics.rb
@@ -1,62 +1,38 @@
class CycleAnalytics
STAGES = %i[issue plan code test review staging production].freeze
- def initialize(project, current_user, from:)
+ def initialize(project, options)
@project = project
- @current_user = current_user
- @from = from
- @fetcher = Gitlab::CycleAnalytics::MetricsFetcher.new(project: project, from: from, branch: nil)
+ @options = options
end
def summary
- @summary ||= Summary.new(@project, @current_user, from: @from)
+ @summary ||= ::Gitlab::CycleAnalytics::StageSummary.new(@project,
+ from: @options[:from],
+ current_user: @options[:current_user]).data
end
- def permissions(user:)
- Gitlab::CycleAnalytics::Permissions.get(user: user, project: @project)
+ def stats
+ @stats ||= stats_per_stage
end
- def issue
- @fetcher.calculate_metric(:issue,
- Issue.arel_table[:created_at],
- [Issue::Metrics.arel_table[:first_associated_with_milestone_at],
- Issue::Metrics.arel_table[:first_added_to_board_at]])
+ def no_stats?
+ stats.all? { |hash| hash[:value].nil? }
end
- def plan
- @fetcher.calculate_metric(:plan,
- [Issue::Metrics.arel_table[:first_associated_with_milestone_at],
- Issue::Metrics.arel_table[:first_added_to_board_at]],
- Issue::Metrics.arel_table[:first_mentioned_in_commit_at])
- end
-
- def code
- @fetcher.calculate_metric(:code,
- Issue::Metrics.arel_table[:first_mentioned_in_commit_at],
- MergeRequest.arel_table[:created_at])
- end
-
- def test
- @fetcher.calculate_metric(:test,
- MergeRequest::Metrics.arel_table[:latest_build_started_at],
- MergeRequest::Metrics.arel_table[:latest_build_finished_at])
+ def permissions(user:)
+ Gitlab::CycleAnalytics::Permissions.get(user: user, project: @project)
end
- def review
- @fetcher.calculate_metric(:review,
- MergeRequest.arel_table[:created_at],
- MergeRequest::Metrics.arel_table[:merged_at])
+ def [](stage_name)
+ Gitlab::CycleAnalytics::Stage[stage_name].new(project: @project, options: @options)
end
- def staging
- @fetcher.calculate_metric(:staging,
- MergeRequest::Metrics.arel_table[:merged_at],
- MergeRequest::Metrics.arel_table[:first_deployed_to_production_at])
- end
+ private
- def production
- @fetcher.calculate_metric(:production,
- Issue.arel_table[:created_at],
- MergeRequest::Metrics.arel_table[:first_deployed_to_production_at])
+ def stats_per_stage
+ STAGES.map do |stage_name|
+ self[stage_name].as_json
+ end
end
end
diff --git a/app/models/cycle_analytics/summary.rb b/app/models/cycle_analytics/summary.rb
index 82f53d17ddd..e69de29bb2d 100644
--- a/app/models/cycle_analytics/summary.rb
+++ b/app/models/cycle_analytics/summary.rb
@@ -1,43 +0,0 @@
-class CycleAnalytics
- class Summary
- def initialize(project, current_user, from:)
- @project = project
- @current_user = current_user
- @from = from
- end
-
- def new_issues
- IssuesFinder.new(@current_user, project_id: @project.id).execute.created_after(@from).count
- end
-
- def commits
- ref = @project.default_branch.presence
- count_commits_for(ref)
- end
-
- def deploys
- @project.deployments.where("created_at > ?", @from).count
- end
-
- private
-
- # Don't use the `Gitlab::Git::Repository#log` method, because it enforces
- # a limit. Since we need a commit count, we _can't_ enforce a limit, so
- # the easiest way forward is to replicate the relevant portions of the
- # `log` function here.
- def count_commits_for(ref)
- return unless ref
-
- repository = @project.repository.raw_repository
- sha = @project.repository.commit(ref).sha
-
- cmd = %W(git --git-dir=#{repository.path} log)
- cmd << '--format=%H'
- cmd << "--after=#{@from.iso8601}"
- cmd << sha
-
- raw_output = IO.popen(cmd) { |io| io.read }
- raw_output.lines.count
- end
- end
-end
diff --git a/app/models/dashboard_milestone.rb b/app/models/dashboard_milestone.rb
new file mode 100644
index 00000000000..646c1e5ce1a
--- /dev/null
+++ b/app/models/dashboard_milestone.rb
@@ -0,0 +1,5 @@
+class DashboardMilestone < GlobalMilestone
+ def issues_finder_params
+ { authorized_only: true }
+ end
+end
diff --git a/app/models/deploy_key.rb b/app/models/deploy_key.rb
index 2c525d4cd7a..053f2a11aa0 100644
--- a/app/models/deploy_key.rb
+++ b/app/models/deploy_key.rb
@@ -20,4 +20,18 @@ class DeployKey < Key
def destroyed_when_orphaned?
self.private?
end
+
+ def has_access_to?(project)
+ projects.include?(project)
+ end
+
+ def can_push_to?(project)
+ can_push? && has_access_to?(project)
+ end
+
+ private
+
+ # we don't want to notify the user for deploy keys
+ def notify_user
+ end
end
diff --git a/app/models/deployment.rb b/app/models/deployment.rb
index 91d85c2279b..afad001d50f 100644
--- a/app/models/deployment.rb
+++ b/app/models/deployment.rb
@@ -91,7 +91,7 @@ class Deployment < ActiveRecord::Base
@stop_action ||= manual_actions.find_by(name: on_stop)
end
- def stoppable?
+ def stop_action?
stop_action.present?
end
diff --git a/app/models/environment.rb b/app/models/environment.rb
index 5cde94b3509..803060b3979 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -1,11 +1,13 @@
class Environment < ActiveRecord::Base
# Used to generate random suffixes for the slug
+ LETTERS = 'a'..'z'
NUMBERS = '0'..'9'
- SUFFIX_CHARS = ('a'..'z').to_a + NUMBERS.to_a
+ SUFFIX_CHARS = LETTERS.to_a + NUMBERS.to_a
belongs_to :project, required: true, validate: true
- has_many :deployments
+ has_many :deployments, dependent: :destroy
+ has_one :last_deployment, -> { order('deployments.id DESC') }, class_name: 'Deployment'
before_validation :nullify_external_url
before_validation :generate_slug, if: ->(env) { env.slug.blank? }
@@ -36,6 +38,13 @@ class Environment < ActiveRecord::Base
scope :available, -> { with_state(:available) }
scope :stopped, -> { with_state(:stopped) }
+ scope :order_by_last_deployed_at, -> do
+ max_deployment_id_sql =
+ Deployment.select(Deployment.arel_table[:id].maximum).
+ where(Deployment.arel_table[:environment_id].eq(arel_table[:id])).
+ to_sql
+ order(Gitlab::Database.nulls_first_order("(#{max_deployment_id_sql})", 'ASC'))
+ end
state_machine :state, initial: :available do
event :start do
@@ -61,10 +70,6 @@ class Environment < ActiveRecord::Base
ref.to_s == last_deployment.try(:ref)
end
- def last_deployment
- deployments.last
- end
-
def nullify_external_url
self.external_url = nil if self.external_url.blank?
end
@@ -86,8 +91,12 @@ class Environment < ActiveRecord::Base
last_deployment.includes_commit?(commit)
end
+ def last_deployed_at
+ last_deployment.try(:created_at)
+ end
+
def update_merge_request_metrics?
- self.name == "production"
+ (environment_type || name) == "production"
end
def first_deployment_for(commit)
@@ -109,15 +118,15 @@ class Environment < ActiveRecord::Base
external_url.gsub(/\A.*?:\/\//, '')
end
- def stoppable?
+ def stop_action?
available? && stop_action.present?
end
- def stop!(current_user)
- return unless stoppable?
+ def stop_with_action!(current_user)
+ return unless available?
- stop
- stop_action.play(current_user)
+ stop!
+ stop_action.play(current_user) if stop_action
end
def actions_for(environment)
@@ -148,21 +157,37 @@ class Environment < ActiveRecord::Base
slugified = name.to_s.downcase.gsub(/[^a-z0-9]/, '-')
# Must start with a letter
- slugified = "env-" + slugified if NUMBERS.cover?(slugified[0])
+ slugified = 'env-' + slugified unless LETTERS.cover?(slugified[0])
+
+ # Repeated dashes are invalid (OpenShift limitation)
+ slugified.gsub!(/\-+/, '-')
# Maximum length: 24 characters (OpenShift limitation)
slugified = slugified[0..23]
- # Cannot end with a "-" character (Kubernetes label limitation)
- slugified = slugified[0..-2] if slugified[-1] == "-"
+ # Cannot end with a dash (Kubernetes label limitation)
+ slugified.chop! if slugified.end_with?('-')
# Add a random suffix, shortening the current string if necessary, if it
# has been slugified. This ensures uniqueness.
- slugified = slugified[0..16] + "-" + random_suffix if slugified != name
+ if slugified != name
+ slugified = slugified[0..16]
+ slugified << '-' unless slugified.end_with?('-')
+ slugified << random_suffix
+ end
self.slug = slugified
end
+ def external_url_for(path, commit_sha)
+ return unless self.external_url
+
+ public_path = project.public_path_for_source_path(path, commit_sha)
+ return unless public_path
+
+ [external_url, public_path].join('/')
+ end
+
private
# Slugifying a name may remove the uniqueness guarantee afforded by it being
diff --git a/app/models/external_issue.rb b/app/models/external_issue.rb
index 91b508eb325..26712c19b5a 100644
--- a/app/models/external_issue.rb
+++ b/app/models/external_issue.rb
@@ -38,7 +38,7 @@ class ExternalIssue
@reference_pattern ||= %r{(?<issue>\b([A-Z][A-Z0-9_]+-)\d+)}
end
- def to_reference(_from_project = nil)
+ def to_reference(_from_project = nil, full: nil)
id
end
diff --git a/app/models/forked_project_link.rb b/app/models/forked_project_link.rb
index 9803bae0bee..36cf7ad6a28 100644
--- a/app/models/forked_project_link.rb
+++ b/app/models/forked_project_link.rb
@@ -1,4 +1,4 @@
class ForkedProjectLink < ActiveRecord::Base
- belongs_to :forked_to_project, class_name: Project
- belongs_to :forked_from_project, class_name: Project
+ belongs_to :forked_to_project, class_name: 'Project'
+ belongs_to :forked_from_project, class_name: 'Project'
end
diff --git a/app/models/generic_commit_status.rb b/app/models/generic_commit_status.rb
index fa54e3540d0..8867ba0d2ff 100644
--- a/app/models/generic_commit_status.rb
+++ b/app/models/generic_commit_status.rb
@@ -1,6 +1,10 @@
class GenericCommitStatus < CommitStatus
before_validation :set_default_values
+ validates :target_url, addressable_url: true,
+ length: { maximum: 255 },
+ allow_nil: true
+
# GitHub compatible API
alias_attribute :context, :name
@@ -12,4 +16,10 @@ class GenericCommitStatus < CommitStatus
def tags
[:external]
end
+
+ def detailed_status(current_user)
+ Gitlab::Ci::Status::External::Factory
+ .new(self, current_user)
+ .fabricate!
+ end
end
diff --git a/app/models/global_milestone.rb b/app/models/global_milestone.rb
index a54e478f5f8..b991d78e27f 100644
--- a/app/models/global_milestone.rb
+++ b/app/models/global_milestone.rb
@@ -1,6 +1,8 @@
class GlobalMilestone
include Milestoneish
+ EPOCH = DateTime.parse('1970-01-01')
+
attr_accessor :title, :milestones
alias_attribute :name, :title
@@ -8,13 +10,22 @@ class GlobalMilestone
@first_milestone
end
- def self.build_collection(milestones)
- milestones = milestones.group_by(&:title)
+ def self.build_collection(projects, params)
+ child_milestones = MilestonesFinder.new.execute(projects, params)
- milestones.map do |title, milestones|
- milestones_relation = Milestone.where(id: milestones.map(&:id))
+ milestones = child_milestones.select(:id, :title).group_by(&:title).map do |title, grouped|
+ milestones_relation = Milestone.where(id: grouped.map(&:id))
new(title, milestones_relation)
end
+
+ milestones.sort_by { |milestone| milestone.due_date || EPOCH }
+ end
+
+ def self.build(projects, title)
+ child_milestones = Milestone.of_projects(projects).where(title: title)
+ return if child_milestones.blank?
+
+ new(title, child_milestones)
end
def initialize(title, milestones)
diff --git a/app/models/group.rb b/app/models/group.rb
index ac8a82c8c1e..a5b92283daa 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -48,7 +48,13 @@ class Group < Namespace
end
def sort(method)
- order_by(method)
+ if method == 'storage_size_desc'
+ # storage_size is a virtual column so we need to
+ # pass a string to avoid AR adding the table name
+ reorder('storage_size DESC, namespaces.id DESC')
+ else
+ order_by(method)
+ end
end
def reference_prefix
@@ -74,7 +80,7 @@ class Group < Namespace
end
end
- def to_reference(_from_project = nil)
+ def to_reference(_from_project = nil, full: nil)
"#{self.class.reference_prefix}#{name}"
end
@@ -155,15 +161,17 @@ class Group < Namespace
end
def has_owner?(user)
- owners.include?(user)
+ members_with_parents.owners.where(user_id: user).any?
end
def has_master?(user)
- members.masters.where(user_id: user).any?
+ members_with_parents.masters.where(user_id: user).any?
end
+ # Check if user is a last owner of the group.
+ # Parent owners are ignored for nested groups.
def last_owner?(user)
- has_owner?(user) && owners.size == 1
+ owners.include?(user) && owners.size == 1
end
def avatar_type
@@ -189,6 +197,19 @@ class Group < Namespace
end
def refresh_members_authorized_projects
- UserProjectAccessChangedService.new(users.pluck(:id)).execute
+ UserProjectAccessChangedService.new(user_ids_for_project_authorizations).
+ execute
+ end
+
+ def user_ids_for_project_authorizations
+ users_with_parents.pluck(:id)
+ end
+
+ def members_with_parents
+ GroupMember.where(requested_at: nil, source_id: ancestors.map(&:id).push(id))
+ end
+
+ def users_with_parents
+ User.where(id: members_with_parents.select(:user_id))
end
end
diff --git a/app/models/group_label.rb b/app/models/group_label.rb
index 68841ace2e6..92c83b54861 100644
--- a/app/models/group_label.rb
+++ b/app/models/group_label.rb
@@ -8,8 +8,4 @@ class GroupLabel < Label
def subject_foreign_key
'group_id'
end
-
- def to_reference(source_project = nil, target_project = nil, format: :id)
- super(source_project, target_project, format: format)
- end
end
diff --git a/app/models/group_milestone.rb b/app/models/group_milestone.rb
new file mode 100644
index 00000000000..7b6db2634b7
--- /dev/null
+++ b/app/models/group_milestone.rb
@@ -0,0 +1,19 @@
+class GroupMilestone < GlobalMilestone
+ attr_accessor :group
+
+ def self.build_collection(group, projects, params)
+ super(projects, params).each do |milestone|
+ milestone.group = group
+ end
+ end
+
+ def self.build(group, projects, title)
+ super(projects, title).tap do |milestone|
+ milestone.group = group if milestone
+ end
+ end
+
+ def issues_finder_params
+ { group_id: group.id }
+ end
+end
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 6825553512f..d8826b65fcc 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -97,10 +97,11 @@ class Issue < ActiveRecord::Base
end
end
- def to_reference(from_project = nil)
+ # `from` argument can be a Namespace or Project.
+ def to_reference(from = nil, full: false)
reference = "#{self.class.reference_prefix}#{iid}"
- "#{project.to_reference(from_project)}#{reference}"
+ "#{project.to_reference(from, full: full)}#{reference}"
end
def referenced_merge_requests(current_user = nil)
diff --git a/app/models/key.rb b/app/models/key.rb
index a5d25409730..9c74ca84753 100644
--- a/app/models/key.rb
+++ b/app/models/key.rb
@@ -4,6 +4,8 @@ class Key < ActiveRecord::Base
include AfterCommitQueue
include Sortable
+ LAST_USED_AT_REFRESH_TIME = 1.day.to_i
+
belongs_to :user
before_validation :generate_fingerprint
@@ -49,6 +51,13 @@ class Key < ActiveRecord::Base
"key-#{id}"
end
+ def update_last_used_at
+ lease = Gitlab::ExclusiveLease.new("key_update_last_used_at:#{id}", timeout: LAST_USED_AT_REFRESH_TIME)
+ return unless lease.try_obtain
+
+ UseKeyWorker.perform_async(id)
+ end
+
def add_to_shell
GitlabShellWorker.perform_async(
:add_key,
@@ -57,10 +66,6 @@ class Key < ActiveRecord::Base
)
end
- def notify_user
- run_after_commit { NotificationService.new.new_key(self) }
- end
-
def post_create_hook
SystemHooksService.new.execute_hooks_for(self, :create)
end
@@ -86,4 +91,8 @@ class Key < ActiveRecord::Base
self.fingerprint = Gitlab::KeyFingerprint.new(self.key).fingerprint
end
+
+ def notify_user
+ run_after_commit { NotificationService.new.new_key(self) }
+ end
end
diff --git a/app/models/label.rb b/app/models/label.rb
index d38c37344c9..5b6b9a7a736 100644
--- a/app/models/label.rb
+++ b/app/models/label.rb
@@ -26,6 +26,7 @@ class Label < ActiveRecord::Base
# Don't allow ',' for label titles
validates :title, presence: true, format: { with: /\A[^,]+\z/ }
validates :title, uniqueness: { scope: [:group_id, :project_id] }
+ validates :title, length: { maximum: 255 }
default_scope { order(title: :asc) }
@@ -146,17 +147,17 @@ class Label < ActiveRecord::Base
#
# Label.first.to_reference # => "~1"
# Label.first.to_reference(format: :name) # => "~\"bug\""
- # Label.first.to_reference(project, same_namespace_project) # => "gitlab-ce~1"
- # Label.first.to_reference(project, another_namespace_project) # => "gitlab-org/gitlab-ce~1"
+ # Label.first.to_reference(project, target_project: same_namespace_project) # => "gitlab-ce~1"
+ # Label.first.to_reference(project, target_project: another_namespace_project) # => "gitlab-org/gitlab-ce~1"
#
# Returns a String
#
- def to_reference(source_project = nil, target_project = nil, format: :id)
+ def to_reference(from_project = nil, target_project: nil, format: :id, full: false)
format_reference = label_format_reference(format)
reference = "#{self.class.reference_prefix}#{format_reference}"
- if source_project
- "#{source_project.to_reference(target_project)}#{reference}"
+ if from_project
+ "#{from_project.to_reference(target_project, full: full)}#{reference}"
else
reference
end
diff --git a/app/models/lfs_objects_project.rb b/app/models/lfs_objects_project.rb
index 0fd5f089db9..007eed5600a 100644
--- a/app/models/lfs_objects_project.rb
+++ b/app/models/lfs_objects_project.rb
@@ -5,4 +5,13 @@ class LfsObjectsProject < ActiveRecord::Base
validates :lfs_object_id, presence: true
validates :lfs_object_id, uniqueness: { scope: [:project_id], message: "already exists in project" }
validates :project_id, presence: true
+
+ after_create :update_project_statistics
+ after_destroy :update_project_statistics
+
+ private
+
+ def update_project_statistics
+ ProjectCacheWorker.perform_async(project_id, [], [:lfs_objects_size])
+ end
end
diff --git a/app/models/list.rb b/app/models/list.rb
index 065d75bd1dc..1e5da7f4dd4 100644
--- a/app/models/list.rb
+++ b/app/models/list.rb
@@ -2,7 +2,7 @@ class List < ActiveRecord::Base
belongs_to :board
belongs_to :label
- enum list_type: { backlog: 0, label: 1, done: 2 }
+ enum list_type: { label: 1, done: 2 }
validates :board, :list_type, presence: true
validates :label, :position, presence: true, if: :label?
diff --git a/app/models/member.rb b/app/models/member.rb
index c585e0b450e..26a6054e00d 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -68,9 +68,9 @@ class Member < ActiveRecord::Base
after_create :send_request, if: :request?, unless: :importing?
after_create :create_notification_setting, unless: [:pending?, :importing?]
after_create :post_create_hook, unless: [:pending?, :importing?]
- after_create :refresh_member_authorized_projects, if: :importing?
after_update :post_update_hook, unless: [:pending?, :importing?]
after_destroy :post_destroy_hook, unless: :pending?
+ after_commit :refresh_member_authorized_projects
delegate :name, :username, :email, to: :user, prefix: true
@@ -147,8 +147,6 @@ class Member < ActiveRecord::Base
member.save
end
- UserProjectAccessChangedService.new(user.id).execute if user.is_a?(User)
-
member
end
@@ -275,23 +273,27 @@ class Member < ActiveRecord::Base
end
def post_create_hook
- UserProjectAccessChangedService.new(user.id).execute
system_hook_service.execute_hooks_for(self, :create)
end
def post_update_hook
- UserProjectAccessChangedService.new(user.id).execute if access_level_changed?
+ # override in sub class
end
def post_destroy_hook
- refresh_member_authorized_projects
system_hook_service.execute_hooks_for(self, :destroy)
end
+ # Refreshes authorizations of the current member.
+ #
+ # This method schedules a job using Sidekiq and as such **must not** be called
+ # in a transaction. Doing so can lead to the job running before the
+ # transaction has been committed, resulting in the job either throwing an
+ # error or not doing any meaningful work.
def refresh_member_authorized_projects
- # If user/source is being destroyed, project access are gonna be destroyed eventually
- # because of DB foreign keys, so we shouldn't bother with refreshing after each
- # member is destroyed through association
+ # If user/source is being destroyed, project access are going to be
+ # destroyed eventually because of DB foreign keys, so we shouldn't bother
+ # with refreshing after each member is destroyed through association
return if destroyed_by_association.present?
UserProjectAccessChangedService.new(user_id).execute
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 5ce20cc43d6..c0d4dd0197f 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -91,6 +91,10 @@ class MergeRequest < ActiveRecord::Base
around_transition do |merge_request, transition, block|
Gitlab::Timeless.timeless(merge_request, &block)
end
+
+ after_transition unchecked: :cannot_be_merged do |merge_request, transition|
+ TodoService.new.merge_request_became_unmergeable(merge_request)
+ end
end
validates :source_project, presence: true, unless: [:allow_broken, :importing?, :closed_without_fork?]
@@ -175,10 +179,11 @@ class MergeRequest < ActiveRecord::Base
work_in_progress?(title) ? title : "WIP: #{title}"
end
- def to_reference(from_project = nil)
+ # `from` argument can be a Namespace or Project.
+ def to_reference(from = nil, full: false)
reference = "#{self.class.reference_prefix}#{iid}"
- "#{project.to_reference(from_project)}#{reference}"
+ "#{project.to_reference(from, full: full)}#{reference}"
end
def first_commit
@@ -198,7 +203,9 @@ class MergeRequest < ActiveRecord::Base
end
def diff_size
- diffs(diff_options).size
+ opts = diff_options || {}
+
+ raw_diffs(opts).size
end
def diff_base_commit
@@ -219,7 +226,7 @@ class MergeRequest < ActiveRecord::Base
# true base commit, so we can't simply have `#diff_base_commit` fall back on
# this method.
def likely_diff_base_commit
- first_commit.parent || first_commit
+ first_commit.try(:parent) || first_commit
end
def diff_start_commit
@@ -539,7 +546,7 @@ class MergeRequest < ActiveRecord::Base
# Calculating this information for a number of merge requests requires
# running `ReferenceExtractor` on each of them separately.
# This optimization does not apply to issues from external sources.
- def cache_merge_request_closes_issues!(current_user = self.author)
+ def cache_merge_request_closes_issues!(current_user)
return if project.has_external_issue_tracker?
transaction do
@@ -551,10 +558,6 @@ class MergeRequest < ActiveRecord::Base
end
end
- def closes_issue?(issue)
- closes_issues.include?(issue)
- end
-
# Return the set of issues that will be closed if this merge request is accepted.
def closes_issues(current_user = self.author)
if target_branch == project.default_branch
@@ -568,17 +571,13 @@ class MergeRequest < ActiveRecord::Base
end
end
- def issues_mentioned_but_not_closing(current_user = self.author)
+ def issues_mentioned_but_not_closing(current_user)
return [] unless target_branch == project.default_branch
ext = Gitlab::ReferenceExtractor.new(project, current_user)
ext.analyze(description)
- issues = ext.issues
- closing_issues = Gitlab::ClosingIssueExtractor.new(project, current_user).
- closed_by_message(description)
-
- issues - closing_issues
+ ext.issues - closes_issues(current_user)
end
def target_project_path
@@ -712,18 +711,22 @@ class MergeRequest < ActiveRecord::Base
!head_pipeline || head_pipeline.success? || head_pipeline.skipped?
end
- def environments
+ def environments_for(current_user)
return [] unless diff_head_commit
- @environments ||= begin
- target_envs = target_project.environments_for(
- target_branch, commit: diff_head_commit, with_tags: true)
+ @environments ||= Hash.new do |h, current_user|
+ envs = EnvironmentsFinder.new(target_project, current_user,
+ ref: target_branch, commit: diff_head_commit, with_tags: true).execute
- source_envs = source_project.environments_for(
- source_branch, commit: diff_head_commit) if source_project
+ if source_project
+ envs.concat EnvironmentsFinder.new(source_project, current_user,
+ ref: source_branch, commit: diff_head_commit).execute
+ end
- (target_envs.to_a + source_envs.to_a).uniq
+ h[current_user] = envs.uniq
end
+
+ @environments[current_user]
end
def state_human_name
@@ -863,9 +866,11 @@ class MergeRequest < ActiveRecord::Base
paths: paths
)
- active_diff_notes.each do |note|
- service.execute(note)
- Gitlab::Timeless.timeless(note, &:save)
+ transaction do
+ active_diff_notes.each do |note|
+ service.execute(note)
+ Gitlab::Timeless.timeless(note, &:save)
+ end
end
end
@@ -900,10 +905,22 @@ class MergeRequest < ActiveRecord::Base
end
def has_commits?
- commits_count > 0
+ merge_request_diff && commits_count > 0
end
def has_no_commits?
!has_commits?
end
+
+ def mergeable_with_slash_command?(current_user, autocomplete_precheck: false, last_diff_sha: nil)
+ return false unless can_be_merged_by?(current_user)
+
+ return true if autocomplete_precheck
+
+ return false unless mergeable?(skip_ci_check: true)
+ return false if head_pipeline && !(head_pipeline.success? || head_pipeline.active?)
+ return false if last_diff_sha != diff_head_sha
+
+ true
+ end
end
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index b8f36a2c958..70bad2a4396 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -1,7 +1,7 @@
class MergeRequestDiff < ActiveRecord::Base
include Sortable
include Importable
- include EncodingHelper
+ include Gitlab::Git::EncodingHelper
# Prevent store of diff if commits amount more then 500
COMMITS_SAFE_SIZE = 100
@@ -169,7 +169,8 @@ class MergeRequestDiff < ActiveRecord::Base
# When compare merge request versions we want diff A..B instead of A...B
# so we handle cases when user does squash and rebase of the commits between versions.
# For this reason we set straight to true by default.
- CompareService.new.execute(project, head_commit_sha, project, sha, straight: straight)
+ CompareService.new(project, head_commit_sha)
+ .execute(project, sha, straight: straight)
end
def commits_count
@@ -234,28 +235,28 @@ class MergeRequestDiff < ActiveRecord::Base
# and save it as array of hashes in st_diffs db field
def save_diffs
new_attributes = {}
- new_diffs = []
if commits.size.zero?
new_attributes[:state] = :empty
else
diff_collection = compare.diffs(Commit.max_diff_options)
-
- if diff_collection.overflow?
- # Set our state to 'overflow' to make the #empty? and #collected?
- # methods (generated by StateMachine) return false.
- new_attributes[:state] = :overflow
- end
-
- new_attributes[:real_size] = diff_collection.real_size
+ new_attributes[:real_size] = compare.diffs.real_size
if diff_collection.any?
new_diffs = dump_diffs(diff_collection)
new_attributes[:state] = :collected
end
+
+ new_attributes[:st_diffs] = new_diffs || []
+
+ # Set our state to 'overflow' to make the #empty? and #collected?
+ # methods (generated by StateMachine) return false.
+ #
+ # This attribution has to come at the end of the method so 'overflow'
+ # state does not get overridden by 'collected'.
+ new_attributes[:state] = :overflow if diff_collection.overflow?
end
- new_attributes[:st_diffs] = new_diffs
update_columns_serialized(new_attributes)
end
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index 0dcfec89f14..7331000a9f2 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -118,11 +118,11 @@ class Milestone < ActiveRecord::Base
# Milestone.first.to_reference(cross_namespace_project) # => "gitlab-org/gitlab-ce%1"
# Milestone.first.to_reference(same_namespace_project) # => "gitlab-ce%1"
#
- def to_reference(from_project = nil, format: :iid)
+ def to_reference(from_project = nil, format: :iid, full: false)
format_reference = milestone_format_reference(format)
reference = "#{self.class.reference_prefix}#{format_reference}"
- "#{project.to_reference(from_project)}#{reference}"
+ "#{project.to_reference(from_project, full: full)}#{reference}"
end
def reference_link_text(from_project = nil)
@@ -202,4 +202,8 @@ class Milestone < ActiveRecord::Base
errors.add(:start_date, "Can't be greater than due date")
end
end
+
+ def issues_finder_params
+ { project_id: project_id }
+ end
end
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index fd42f2328d8..c5713fb7818 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -4,11 +4,13 @@ class Namespace < ActiveRecord::Base
include CacheMarkdownField
include Sortable
include Gitlab::ShellAdapter
+ include Gitlab::CurrentSettings
include Routable
cache_markdown_field :description, pipeline: :description
has_many :projects, dependent: :destroy
+ has_many :project_statistics
belongs_to :owner, class_name: "User"
belongs_to :parent, class_name: "Namespace"
@@ -38,6 +40,18 @@ class Namespace < ActiveRecord::Base
scope :root, -> { where('type IS NULL') }
+ scope :with_statistics, -> do
+ joins('LEFT JOIN project_statistics ps ON ps.namespace_id = namespaces.id')
+ .group('namespaces.id')
+ .select(
+ 'namespaces.*',
+ 'COALESCE(SUM(ps.storage_size), 0) AS storage_size',
+ 'COALESCE(SUM(ps.repository_size), 0) AS repository_size',
+ 'COALESCE(SUM(ps.lfs_objects_size), 0) AS lfs_objects_size',
+ 'COALESCE(SUM(ps.build_artifacts_size), 0) AS build_artifacts_size',
+ )
+ end
+
class << self
def by_path(path)
find_by('lower(path) = :value', value: path.downcase)
@@ -98,7 +112,7 @@ class Namespace < ActiveRecord::Base
def move_dir
if any_project_has_container_registry_tags?
- raise Exception.new('Namespace cannot be moved, because at least one project has tags in container registry')
+ raise Gitlab::UpdatePathError.new('Namespace cannot be moved, because at least one project has tags in container registry')
end
# Move the namespace directory in all storages paths used by member projects
@@ -111,11 +125,14 @@ class Namespace < ActiveRecord::Base
# if we cannot move namespace directory we should rollback
# db changes in order to prevent out of sync between db and fs
- raise Exception.new('namespace directory cannot be moved')
+ raise Gitlab::UpdatePathError.new('namespace directory cannot be moved')
end
end
Gitlab::UploadsTransfer.new.rename_namespace(path_was, path)
+ Gitlab::PagesTransfer.new.rename_namespace(path_was, path)
+
+ remove_exports!
# If repositories moved successfully we need to
# send update instructions to users.
@@ -161,6 +178,10 @@ class Namespace < ActiveRecord::Base
end
end
+ def shared_runners_enabled?
+ projects.with_shared_runners.any?
+ end
+
def full_name
@full_name ||=
if parent
@@ -170,8 +191,30 @@ class Namespace < ActiveRecord::Base
end
end
- def parents
- @parents ||= parent ? parent.parents + [parent] : []
+ # Scopes the model on ancestors of the record
+ def ancestors
+ if parent_id
+ path = route.path
+ paths = []
+
+ until path.blank?
+ path = path.rpartition('/').first
+ paths << path
+ end
+
+ self.class.joins(:route).where('routes.path IN (?)', paths).reorder('routes.path ASC')
+ else
+ self.class.none
+ end
+ end
+
+ # Scopes the model on direct and indirect children of the record
+ def descendants
+ self.class.joins(:route).where('routes.path LIKE ?', "#{route.path}/%").reorder('routes.path ASC')
+ end
+
+ def user_ids_for_project_authorizations
+ [owner_id]
end
private
@@ -201,6 +244,8 @@ class Namespace < ActiveRecord::Base
GitlabShellWorker.perform_in(5.minutes, :rm_namespace, repository_storage_path, new_path)
end
end
+
+ remove_exports!
end
def refresh_access_of_projects_invited_groups
@@ -213,4 +258,20 @@ class Namespace < ActiveRecord::Base
def full_path_changed?
path_changed? || parent_id_changed?
end
+
+ def remove_exports!
+ Gitlab::Popen.popen(%W(find #{export_path} -not -path #{export_path} -delete))
+ end
+
+ def export_path
+ File.join(Gitlab::ImportExport.storage_path, full_path_was)
+ end
+
+ def full_path_was
+ if parent
+ parent.full_path + '/' + path_was
+ else
+ path_was
+ end
+ end
end
diff --git a/app/models/note.rb b/app/models/note.rb
index 0c1b05dabf2..bf090a0438c 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -43,7 +43,8 @@ class Note < ActiveRecord::Base
delegate :name, :email, to: :author, prefix: true
delegate :title, to: :noteable, allow_nil: true
- validates :note, :project, presence: true
+ validates :note, presence: true
+ validates :project, presence: true, unless: :for_personal_snippet?
# Attachments are deprecated and are handled by Markdown uploader
validates :attachment, file_size: { maximum: :max_attachment_size }
@@ -53,7 +54,7 @@ class Note < ActiveRecord::Base
validates :commit_id, presence: true, if: :for_commit?
validates :author, presence: true
- validate unless: [:for_commit?, :importing?] do |note|
+ validate unless: [:for_commit?, :importing?, :for_personal_snippet?] do |note|
unless note.noteable.try(:project) == note.project
errors.add(:invalid_project, 'Note and noteable project mismatch')
end
@@ -83,7 +84,7 @@ class Note < ActiveRecord::Base
after_initialize :ensure_discussion_id
before_validation :nullify_blank_type, :nullify_blank_line_code
before_validation :set_discussion_id
- after_save :keep_around_commit
+ after_save :keep_around_commit, unless: :for_personal_snippet?
class << self
def model_name
@@ -165,6 +166,14 @@ class Note < ActiveRecord::Base
noteable_type == "Snippet"
end
+ def for_personal_snippet?
+ noteable.is_a?(PersonalSnippet)
+ end
+
+ def skip_project_check?
+ for_personal_snippet?
+ end
+
# override to return commits, which are not active record
def noteable
if for_commit?
@@ -220,6 +229,10 @@ class Note < ActiveRecord::Base
note.match(Banzai::Filter::EmojiFilter.emoji_pattern)[1]
end
+ def to_ability_name
+ for_personal_snippet? ? 'personal_snippet' : noteable_type.underscore
+ end
+
private
def keep_around_commit
diff --git a/app/models/notification_setting.rb b/app/models/notification_setting.rb
index 43fc218de2b..58f6214bea7 100644
--- a/app/models/notification_setting.rb
+++ b/app/models/notification_setting.rb
@@ -37,6 +37,10 @@ class NotificationSetting < ActiveRecord::Base
:success_pipeline
]
+ EXCLUDED_WATCHER_EVENTS = [
+ :success_pipeline
+ ]
+
store :events, accessors: EMAIL_EVENTS, coder: JSON
before_create :set_events
diff --git a/app/models/pages_domain.rb b/app/models/pages_domain.rb
new file mode 100644
index 00000000000..0b9ebf1ffe2
--- /dev/null
+++ b/app/models/pages_domain.rb
@@ -0,0 +1,119 @@
+class PagesDomain < ActiveRecord::Base
+ belongs_to :project
+
+ validates :domain, hostname: true
+ validates_uniqueness_of :domain, case_sensitive: false
+ validates :certificate, certificate: true, allow_nil: true, allow_blank: true
+ validates :key, certificate_key: true, allow_nil: true, allow_blank: true
+
+ validate :validate_pages_domain
+ validate :validate_matching_key, if: ->(domain) { domain.certificate.present? || domain.key.present? }
+ validate :validate_intermediates, if: ->(domain) { domain.certificate.present? }
+
+ attr_encrypted :key,
+ mode: :per_attribute_iv_and_salt,
+ insecure_mode: true,
+ key: Gitlab::Application.secrets.db_key_base,
+ algorithm: 'aes-256-cbc'
+
+ after_create :update
+ after_save :update
+ after_destroy :update
+
+ def to_param
+ domain
+ end
+
+ def url
+ return unless domain
+
+ if certificate
+ "https://#{domain}"
+ else
+ "http://#{domain}"
+ end
+ end
+
+ def has_matching_key?
+ return false unless x509
+ return false unless pkey
+
+ # We compare the public key stored in certificate with public key from certificate key
+ x509.check_private_key(pkey)
+ end
+
+ def has_intermediates?
+ return false unless x509
+
+ # self-signed certificates doesn't have the certificate chain
+ return true if x509.verify(x509.public_key)
+
+ store = OpenSSL::X509::Store.new
+ store.set_default_paths
+
+ # This forces to load all intermediate certificates stored in `certificate`
+ Tempfile.open('certificate_chain') do |f|
+ f.write(certificate)
+ f.flush
+ store.add_file(f.path)
+ end
+
+ store.verify(x509)
+ rescue OpenSSL::X509::StoreError
+ false
+ end
+
+ def expired?
+ return false unless x509
+ current = Time.new
+ current < x509.not_before || x509.not_after < current
+ end
+
+ def subject
+ return unless x509
+ x509.subject.to_s
+ end
+
+ def certificate_text
+ @certificate_text ||= x509.try(:to_text)
+ end
+
+ private
+
+ def update
+ ::Projects::UpdatePagesConfigurationService.new(project).execute
+ end
+
+ def validate_matching_key
+ unless has_matching_key?
+ self.errors.add(:key, "doesn't match the certificate")
+ end
+ end
+
+ def validate_intermediates
+ unless has_intermediates?
+ self.errors.add(:certificate, 'misses intermediates')
+ end
+ end
+
+ def validate_pages_domain
+ return unless domain
+ if domain.downcase.ends_with?(".#{Settings.pages.host}".downcase)
+ self.errors.add(:domain, "*.#{Settings.pages.host} is restricted")
+ end
+ end
+
+ def x509
+ return unless certificate
+ @x509 ||= OpenSSL::X509::Certificate.new(certificate)
+ rescue OpenSSL::X509::CertificateError
+ nil
+ end
+
+ def pkey
+ return unless key
+ @pkey ||= OpenSSL::PKey::RSA.new(key)
+ rescue OpenSSL::PKey::PKeyError, OpenSSL::Cipher::CipherError
+ nil
+ end
+end
diff --git a/app/models/project.rb b/app/models/project.rb
index 72d3da64f2d..b45f22d94d9 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -12,6 +12,7 @@ class Project < ActiveRecord::Base
include AfterCommitQueue
include CaseSensitivity
include TokenAuthenticatable
+ include ValidAttribute
include ProjectFeaturesCompatibility
include SelectForProjectAuthorization
include Routable
@@ -44,6 +45,7 @@ class Project < ActiveRecord::Base
after_create :ensure_dir_exist
after_create :create_project_feature, unless: :project_feature
after_save :ensure_dir_exist, if: :namespace_id_changed?
+ after_save :update_project_statistics, if: :namespace_id_changed?
# set last_activity_at to the same as created_at
after_create :set_last_activity_at
@@ -51,6 +53,8 @@ class Project < ActiveRecord::Base
update_column(:last_activity_at, self.created_at)
end
+ after_destroy :remove_pages
+
# update visibility_level of forks
after_update :update_forks_visibility_level
def update_forks_visibility_level
@@ -64,6 +68,8 @@ class Project < ActiveRecord::Base
end
end
+ after_validation :check_pending_delete
+
ActsAsTaggableOn.strict_case_match = true
acts_as_taggable_on :tags
@@ -94,9 +100,9 @@ class Project < ActiveRecord::Base
has_one :asana_service, dependent: :destroy
has_one :gemnasium_service, dependent: :destroy
has_one :mattermost_slash_commands_service, dependent: :destroy
- has_one :mattermost_notification_service, dependent: :destroy
+ has_one :mattermost_service, dependent: :destroy
has_one :slack_slash_commands_service, dependent: :destroy
- has_one :slack_notification_service, dependent: :destroy
+ has_one :slack_service, dependent: :destroy
has_one :buildkite_service, dependent: :destroy
has_one :bamboo_service, dependent: :destroy
has_one :teamcity_service, dependent: :destroy
@@ -117,8 +123,6 @@ class Project < ActiveRecord::Base
# Merge Requests for target project should be removed with it
has_many :merge_requests, dependent: :destroy, foreign_key: 'target_project_id'
- # Merge requests from source project should be kept when source project was removed
- has_many :fork_merge_requests, foreign_key: 'source_project_id', class_name: MergeRequest
has_many :issues, dependent: :destroy
has_many :labels, dependent: :destroy, class_name: 'ProjectLabel'
has_many :services, dependent: :destroy
@@ -129,7 +133,7 @@ class Project < ActiveRecord::Base
has_many :hooks, dependent: :destroy, class_name: 'ProjectHook'
has_many :protected_branches, dependent: :destroy
- has_many :project_authorizations, dependent: :destroy
+ has_many :project_authorizations
has_many :authorized_users, through: :project_authorizations, source: :user, class_name: 'User'
has_many :project_members, -> { where(requested_at: nil) }, dependent: :destroy, as: :source
alias_method :members, :project_members
@@ -146,11 +150,13 @@ class Project < ActiveRecord::Base
has_many :lfs_objects, through: :lfs_objects_projects
has_many :project_group_links, dependent: :destroy
has_many :invited_groups, through: :project_group_links, source: :group
+ has_many :pages_domains, dependent: :destroy
has_many :todos, dependent: :destroy
has_many :notification_settings, dependent: :destroy, as: :source
has_one :import_data, dependent: :destroy, class_name: "ProjectImportData"
has_one :project_feature, dependent: :destroy
+ has_one :statistics, class_name: 'ProjectStatistics', dependent: :delete
has_many :commit_statuses, dependent: :destroy, foreign_key: :gl_project_id
has_many :pipelines, dependent: :destroy, class_name: 'Ci::Pipeline', foreign_key: :gl_project_id
@@ -220,6 +226,9 @@ class Project < ActiveRecord::Base
scope :with_push, -> { joins(:events).where('events.action = ?', Event::PUSHED) }
scope :with_project_feature, -> { joins('LEFT JOIN project_features ON projects.id = project_features.project_id') }
+ scope :with_statistics, -> { includes(:statistics) }
+ scope :with_shared_runners, -> { where(shared_runners_enabled: true) }
+ scope :inside_path, ->(path) { joins(:route).where('routes.path LIKE ?', "#{path}/%") }
# "enabled" here means "not disabled". It includes private features!
scope :with_feature_enabled, ->(feature) {
@@ -332,8 +341,10 @@ class Project < ActiveRecord::Base
end
def sort(method)
- if method == 'repository_size_desc'
- reorder(repository_size: :desc, id: :desc)
+ if method == 'storage_size_desc'
+ # storage_size is a joined column so we need to
+ # pass a string to avoid AR adding the table name
+ reorder('project_statistics.storage_size DESC, projects.id DESC')
else
order_by(method)
end
@@ -362,10 +373,6 @@ class Project < ActiveRecord::Base
def group_ids
joins(:namespace).where(namespaces: { type: 'Group' }).select(:namespace_id)
end
-
- # Add alias for Routable method for compatibility with old code.
- # In future all calls `find_with_namespace` should be replaced with `find_by_full_path`
- alias_method :find_with_namespace, :find_by_full_path
end
def lfs_enabled?
@@ -584,10 +591,11 @@ class Project < ActiveRecord::Base
end
end
- def to_reference(from_project = nil)
- if cross_namespace_reference?(from_project)
+ # `from` argument can be a Namespace or Project.
+ def to_reference(from = nil, full: false)
+ if full || cross_namespace_reference?(from)
path_with_namespace
- elsif cross_project_reference?(from_project)
+ elsif cross_project_reference?(from)
path
end
end
@@ -604,10 +612,6 @@ class Project < ActiveRecord::Base
Gitlab::Routing.url_helpers.namespace_project_url(self.namespace, self)
end
- def web_url_without_protocol
- web_url.split('://')[1]
- end
-
def new_issue_address(author)
return unless Gitlab::IncomingEmail.supports_issue_creation? && author
@@ -921,7 +925,7 @@ class Project < ActiveRecord::Base
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')
+ raise StandardError.new('Project cannot be renamed, because tags are present in its container registry')
end
if gitlab_shell.mv_repository(repository_storage_path, old_path_with_namespace, new_path_with_namespace)
@@ -948,12 +952,13 @@ class Project < ActiveRecord::Base
# 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')
+ raise StandardError.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)
+ Gitlab::PagesTransfer.new.rename_project(path_was, path, namespace.path)
end
# Expires various caches before a project is renamed.
@@ -1028,7 +1033,7 @@ class Project < ActiveRecord::Base
"refs/heads/#{branch}",
force: true)
repository.copy_gitattributes(branch)
- repository.expire_avatar_cache
+ repository.after_change_head
reload_default_branch
end
@@ -1036,14 +1041,6 @@ class Project < ActiveRecord::Base
forked? && project == forked_from_project
end
- def update_repository_size
- update_attribute(:repository_size, repository.size)
- end
-
- def update_commit_count
- update_attribute(:commit_count, repository.commit_count)
- end
-
def forks_count
forks.count
end
@@ -1102,12 +1099,20 @@ class Project < ActiveRecord::Base
project_feature.update_attribute(:builds_access_level, ProjectFeature::ENABLED)
end
+ def shared_runners_available?
+ shared_runners_enabled?
+ end
+
+ def shared_runners
+ shared_runners_available? ? Ci::Runner.shared : Ci::Runner.none
+ end
+
def any_runners?(&block)
if runners.active.any?(&block)
return true
end
- shared_runners_enabled? && Ci::Runner.shared.active.any?(&block)
+ shared_runners.active.any?(&block)
end
def valid_runners_token?(token)
@@ -1155,6 +1160,45 @@ class Project < ActiveRecord::Base
ensure_runners_token!
end
+ def pages_deployed?
+ Dir.exist?(public_pages_path)
+ end
+
+ def pages_url
+ # The hostname always needs to be in downcased
+ # All web servers convert hostname to lowercase
+ host = "#{namespace.path}.#{Settings.pages.host}".downcase
+
+ # The host in URL always needs to be downcased
+ url = Gitlab.config.pages.url.sub(/^https?:\/\//) do |prefix|
+ "#{prefix}#{namespace.path}."
+ end.downcase
+
+ # If the project path is the same as host, we serve it as group page
+ return url if host == path
+
+ "#{url}/#{path}"
+ end
+
+ def pages_path
+ File.join(Settings.pages.path, path_with_namespace)
+ end
+
+ def public_pages_path
+ File.join(pages_path, 'public')
+ end
+
+ def remove_pages
+ # 1. We rename pages to temporary directory
+ # 2. We wait 5 minutes, due to NFS caching
+ # 3. We asynchronously remove pages with force
+ temp_path = "#{path}.#{SecureRandom.hex}.deleted"
+
+ if Gitlab::PagesTransfer.new.rename_project(path, temp_path, namespace.path)
+ PagesWorker.perform_in(5.minutes, :remove, namespace.path, temp_path)
+ end
+ end
+
def wiki
@wiki ||= ProjectWiki.new(self, self.owner)
end
@@ -1262,47 +1306,50 @@ class Project < ActiveRecord::Base
Gitlab::Redis.with { |redis| redis.del(pushes_since_gc_redis_key) }
end
- def environments_for(ref, commit: nil, with_tags: false)
- deployments_query = with_tags ? 'ref = ? OR tag IS TRUE' : 'ref = ?'
+ def route_map_for(commit_sha)
+ @route_maps_by_commit ||= Hash.new do |h, sha|
+ h[sha] = begin
+ data = repository.route_map_for(sha)
+ next unless data
- environment_ids = deployments
- .where(deployments_query, ref.to_s)
- .group(:environment_id)
- .select(:environment_id)
+ Gitlab::RouteMap.new(data)
+ rescue Gitlab::RouteMap::FormatError
+ nil
+ end
+ end
- environments_found = environments.available
- .where(id: environment_ids).to_a
+ @route_maps_by_commit[commit_sha]
+ end
- return environments_found unless commit
+ def public_path_for_source_path(path, commit_sha)
+ map = route_map_for(commit_sha)
+ return unless map
- environments_found.select do |environment|
- environment.includes_commit?(commit)
- end
+ map.public_path_for_source_path(path)
end
- def environments_recently_updated_on_branch(branch)
- environments_for(branch).select do |environment|
- environment.recently_updated_on_branch?(branch)
+ private
+
+ def cross_namespace_reference?(from)
+ case from
+ when Project
+ namespace != from.namespace
+ when Namespace
+ namespace != from
end
end
- private
-
# Check if a reference is being done cross-project
- #
- # from_project - Refering Project object
- def cross_project_reference?(from_project)
- from_project && self != from_project
+ def cross_project_reference?(from)
+ return true if from.is_a?(Namespace)
+
+ from && self != from
end
def pushes_since_gc_redis_key
"projects/#{id}/pushes_since_gc"
end
- def cross_namespace_reference?(from_project)
- from_project && namespace != from_project.namespace
- end
-
def default_branch_protected?
current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_FULL ||
current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_MERGE
@@ -1322,4 +1369,26 @@ class Project < ActiveRecord::Base
def full_path_changed?
path_changed? || namespace_id_changed?
end
+
+ def update_project_statistics
+ stats = statistics || build_statistics
+ stats.update(namespace_id: namespace_id)
+ end
+
+ def check_pending_delete
+ return if valid_attribute?(:name) && valid_attribute?(:path)
+ return unless pending_delete_twin
+
+ %i[route route.path name path].each do |error|
+ errors.delete(error)
+ end
+
+ errors.add(:base, "The project is still being deleted. Please try again later.")
+ end
+
+ def pending_delete_twin
+ return false unless path
+
+ Project.unscoped.where(pending_delete: true).find_by_full_path(path_with_namespace)
+ end
end
diff --git a/app/models/project_group_link.rb b/app/models/project_group_link.rb
index 6149c35cc61..5cb6b0c527d 100644
--- a/app/models/project_group_link.rb
+++ b/app/models/project_group_link.rb
@@ -16,8 +16,7 @@ class ProjectGroupLink < ActiveRecord::Base
validates :group_access, inclusion: { in: Gitlab::Access.values }, presence: true
validate :different_group
- after_create :refresh_group_members_authorized_projects
- after_destroy :refresh_group_members_authorized_projects
+ after_commit :refresh_group_members_authorized_projects
def self.access_options
Gitlab::Access.options
diff --git a/app/models/project_label.rb b/app/models/project_label.rb
index 82f47f0e8fd..313815e5869 100644
--- a/app/models/project_label.rb
+++ b/app/models/project_label.rb
@@ -16,8 +16,8 @@ class ProjectLabel < Label
'project_id'
end
- def to_reference(target_project = nil, format: :id)
- super(project, target_project, format: format)
+ def to_reference(target_project = nil, format: :id, full: false)
+ super(project, target_project: target_project, format: format, full: full)
end
private
diff --git a/app/models/project_services/asana_service.rb b/app/models/project_services/asana_service.rb
index 7c23b766763..3728f5642e4 100644
--- a/app/models/project_services/asana_service.rb
+++ b/app/models/project_services/asana_service.rb
@@ -25,7 +25,7 @@ You can create a Personal Access Token here:
http://app.asana.com/-/account_api'
end
- def to_param
+ def self.to_param
'asana'
end
@@ -44,7 +44,7 @@ http://app.asana.com/-/account_api'
]
end
- def supported_events
+ def self.supported_events
%w(push)
end
diff --git a/app/models/project_services/assembla_service.rb b/app/models/project_services/assembla_service.rb
index d839221d315..aeeff8917bf 100644
--- a/app/models/project_services/assembla_service.rb
+++ b/app/models/project_services/assembla_service.rb
@@ -12,7 +12,7 @@ class AssemblaService < Service
'Project Management Software (Source Commits Endpoint)'
end
- def to_param
+ def self.to_param
'assembla'
end
@@ -23,7 +23,7 @@ class AssemblaService < Service
]
end
- def supported_events
+ def self.supported_events
%w(push)
end
diff --git a/app/models/project_services/bamboo_service.rb b/app/models/project_services/bamboo_service.rb
index b5c76e4d4fe..400020ee04a 100644
--- a/app/models/project_services/bamboo_service.rb
+++ b/app/models/project_services/bamboo_service.rb
@@ -1,4 +1,6 @@
class BambooService < CiService
+ include ReactiveService
+
prop_accessor :bamboo_url, :build_key, :username, :password
validates :bamboo_url, presence: true, url: true, if: :activated?
@@ -38,7 +40,7 @@ class BambooService < CiService
'You must set up automatic revision labeling and a repository trigger in Bamboo.'
end
- def to_param
+ def self.to_param
'bamboo'
end
@@ -54,35 +56,46 @@ class BambooService < CiService
]
end
- def supported_events
- %w(push)
+ def build_page(sha, ref)
+ with_reactive_cache(sha, ref) {|cached| cached[:build_page] }
end
- def build_info(sha)
- @response = get_path("rest/api/latest/result?label=#{sha}")
+ def commit_status(sha, ref)
+ with_reactive_cache(sha, ref) {|cached| cached[:commit_status] }
end
- def build_page(sha, ref)
- build_info(sha) if @response.nil? || !@response.code
+ def execute(data)
+ return unless supported_events.include?(data[:object_kind])
+
+ get_path("updateAndBuild.action?buildKey=#{build_key}")
+ end
+
+ def calculate_reactive_cache(sha, ref)
+ response = get_path("rest/api/latest/result?label=#{sha}")
+
+ { build_page: read_build_page(response), commit_status: read_commit_status(response) }
+ end
- if @response.code != 200 || @response['results']['results']['size'] == '0'
+ private
+
+ def read_build_page(response)
+ if response.code != 200 || response['results']['results']['size'] == '0'
# If actual build link can't be determined, send user to build summary page.
URI.join("#{bamboo_url}/", "browse/#{build_key}").to_s
else
# If actual build link is available, go to build result page.
- result_key = @response['results']['results']['result']['planResultKey']['key']
+ result_key = response['results']['results']['result']['planResultKey']['key']
URI.join("#{bamboo_url}/", "browse/#{result_key}").to_s
end
end
- def commit_status(sha, ref)
- build_info(sha) if @response.nil? || !@response.code
- return :error unless @response.code == 200 || @response.code == 404
+ def read_commit_status(response)
+ return :error unless response.code == 200 || response.code == 404
- status = if @response.code == 404 || @response['results']['results']['size'] == '0'
+ status = if response.code == 404 || response['results']['results']['size'] == '0'
'Pending'
else
- @response['results']['results']['result']['buildState']
+ response['results']['results']['result']['buildState']
end
if status.include?('Success')
@@ -96,14 +109,6 @@ class BambooService < CiService
end
end
- def execute(data)
- return unless supported_events.include?(data[:object_kind])
-
- get_path("updateAndBuild.action?buildKey=#{build_key}")
- end
-
- private
-
def build_url(path)
URI.join("#{bamboo_url}/", path).to_s
end
diff --git a/app/models/project_services/bugzilla_service.rb b/app/models/project_services/bugzilla_service.rb
index 338e685339a..046e2809f45 100644
--- a/app/models/project_services/bugzilla_service.rb
+++ b/app/models/project_services/bugzilla_service.rb
@@ -19,7 +19,7 @@ class BugzillaService < IssueTrackerService
end
end
- def to_param
+ def self.to_param
'bugzilla'
end
end
diff --git a/app/models/project_services/buildkite_service.rb b/app/models/project_services/buildkite_service.rb
index fe6d7aabb22..0956c4a4ede 100644
--- a/app/models/project_services/buildkite_service.rb
+++ b/app/models/project_services/buildkite_service.rb
@@ -1,6 +1,8 @@
require "addressable/uri"
class BuildkiteService < CiService
+ include ReactiveService
+
ENDPOINT = "https://buildkite.com"
prop_accessor :project_url, :token
@@ -22,10 +24,6 @@ class BuildkiteService < CiService
hook.save
end
- def supported_events
- %w(push)
- end
-
def execute(data)
return unless supported_events.include?(data[:object_kind])
@@ -33,13 +31,7 @@ class BuildkiteService < CiService
end
def commit_status(sha, ref)
- response = HTTParty.get(commit_status_path(sha), verify: false)
-
- if response.code == 200 && response['status']
- response['status']
- else
- :error
- end
+ with_reactive_cache(sha, ref) {|cached| cached[:commit_status] }
end
def commit_status_path(sha)
@@ -58,7 +50,7 @@ class BuildkiteService < CiService
'Continuous integration and deployments'
end
- def to_param
+ def self.to_param
'buildkite'
end
@@ -78,6 +70,19 @@ class BuildkiteService < CiService
]
end
+ def calculate_reactive_cache(sha, ref)
+ response = HTTParty.get(commit_status_path(sha), verify: false)
+
+ status =
+ if response.code == 200 && response['status']
+ response['status']
+ else
+ :error
+ end
+
+ { commit_status: status }
+ end
+
private
def webhook_token
diff --git a/app/models/project_services/builds_email_service.rb b/app/models/project_services/builds_email_service.rb
index 201b94b065b..ebd21e37189 100644
--- a/app/models/project_services/builds_email_service.rb
+++ b/app/models/project_services/builds_email_service.rb
@@ -19,11 +19,11 @@ class BuildsEmailService < Service
'Email the builds status to a list of recipients.'
end
- def to_param
+ def self.to_param
'builds_email'
end
- def supported_events
+ def self.supported_events
%w(build)
end
diff --git a/app/models/project_services/campfire_service.rb b/app/models/project_services/campfire_service.rb
index 5af93860d09..0de59af5652 100644
--- a/app/models/project_services/campfire_service.rb
+++ b/app/models/project_services/campfire_service.rb
@@ -12,7 +12,7 @@ class CampfireService < Service
'Simple web-based real-time group chat'
end
- def to_param
+ def self.to_param
'campfire'
end
@@ -24,7 +24,7 @@ class CampfireService < Service
]
end
- def supported_events
+ def self.supported_events
%w(push)
end
diff --git a/app/models/project_services/chat_notification_service.rb b/app/models/project_services/chat_notification_service.rb
index b0556987721..8468934425f 100644
--- a/app/models/project_services/chat_notification_service.rb
+++ b/app/models/project_services/chat_notification_service.rb
@@ -25,7 +25,7 @@ class ChatNotificationService < Service
valid?
end
- def supported_events
+ def self.supported_events
%w[push issue confidential_issue merge_request note tag_push
build pipeline wiki_page]
end
@@ -49,11 +49,13 @@ class ChatNotificationService < Service
return false unless message
- opt = {}
+ channel_name = get_channel_field(object_kind).presence || channel
- opt[:channel] = get_channel_field(object_kind).presence || channel || default_channel
- opt[:username] = username if username
- notifier = Slack::Notifier.new(webhook, opt)
+ opts = {}
+ opts[:channel] = channel_name if channel_name
+ opts[:username] = username if username
+
+ notifier = Slack::Notifier.new(webhook, opts)
notifier.ping(message.pretext, attachments: message.attachments, fallback: message.fallback)
true
@@ -71,7 +73,7 @@ class ChatNotificationService < Service
fields.reject { |field| field[:name].end_with?('channel') }
end
- def default_channel
+ def default_channel_placeholder
raise NotImplementedError
end
@@ -80,19 +82,19 @@ class ChatNotificationService < Service
def get_message(object_kind, data)
case object_kind
when "push", "tag_push"
- PushMessage.new(data)
+ ChatMessage::PushMessage.new(data)
when "issue"
- IssueMessage.new(data) unless is_update?(data)
+ ChatMessage::IssueMessage.new(data) unless is_update?(data)
when "merge_request"
- MergeMessage.new(data) unless is_update?(data)
+ ChatMessage::MergeMessage.new(data) unless is_update?(data)
when "note"
- NoteMessage.new(data)
+ ChatMessage::NoteMessage.new(data)
when "build"
- BuildMessage.new(data) if should_build_be_notified?(data)
+ ChatMessage::BuildMessage.new(data) if should_build_be_notified?(data)
when "pipeline"
- PipelineMessage.new(data) if should_pipeline_be_notified?(data)
+ ChatMessage::PipelineMessage.new(data) if should_pipeline_be_notified?(data)
when "wiki_page"
- WikiPageMessage.new(data)
+ ChatMessage::WikiPageMessage.new(data)
end
end
@@ -103,7 +105,7 @@ class ChatNotificationService < Service
def build_event_channels
supported_events.reduce([]) do |channels, event|
- channels << { type: 'text', name: event_channel_name(event), placeholder: default_channel }
+ channels << { type: 'text', name: event_channel_name(event), placeholder: default_channel_placeholder }
end
end
diff --git a/app/models/project_services/chat_slash_commands_service.rb b/app/models/project_services/chat_slash_commands_service.rb
index 0bc160af604..8b5bc24fd3c 100644
--- a/app/models/project_services/chat_slash_commands_service.rb
+++ b/app/models/project_services/chat_slash_commands_service.rb
@@ -13,8 +13,8 @@ class ChatSlashCommandsService < Service
ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.token)
end
- def supported_events
- []
+ def self.supported_events
+ %w()
end
def can_test?
@@ -23,7 +23,7 @@ class ChatSlashCommandsService < Service
def fields
[
- { type: 'text', name: 'token', placeholder: '' }
+ { type: 'text', name: 'token', placeholder: 'XXxxXXxxXXxxXXxxXXxxXXxx' }
]
end
@@ -31,13 +31,13 @@ class ChatSlashCommandsService < Service
return unless valid_token?(params[:token])
user = find_chat_user(params)
- unless user
+
+ if user
+ Gitlab::ChatCommands::Command.new(project, user, params).execute
+ else
url = authorize_chat_name_url(params)
- return presenter.authorize_chat_name(url)
+ Gitlab::ChatCommands::Presenters::Access.new(url).authorize
end
-
- Gitlab::ChatCommands::Command.new(project, user,
- params).execute
end
private
@@ -49,8 +49,4 @@ class ChatSlashCommandsService < Service
def authorize_chat_name_url(params)
ChatNames::AuthorizeUserService.new(self, params).execute
end
-
- def presenter
- Gitlab::ChatCommands::Presenter.new
- end
end
diff --git a/app/models/project_services/ci_service.rb b/app/models/project_services/ci_service.rb
index 596c00705ad..82979c8bd34 100644
--- a/app/models/project_services/ci_service.rb
+++ b/app/models/project_services/ci_service.rb
@@ -8,19 +8,11 @@ class CiService < Service
self.respond_to?(:token) && self.token.present? && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.token)
end
- def supported_events
+ def self.supported_events
%w(push)
end
- def merge_request_page(iid, sha, ref)
- commit_page(sha, ref)
- end
-
- def commit_page(sha, ref)
- build_page(sha, ref)
- end
-
- # Return complete url to merge_request page
+ # Return complete url to build page
#
# Ex.
# http://jenkins.example.com:8888/job/test1/scm/bySHA1/12d65c
@@ -35,23 +27,6 @@ class CiService < Service
#
#
# Ex.
- # @service.merge_request_status(9, '13be4ac', 'dev')
- # # => 'success'
- #
- # @service.merge_request_status(10, '2abe4ac', 'dev)
- # # => 'running'
- #
- #
- def merge_request_status(iid, sha, ref)
- commit_status(sha, ref)
- end
-
- # Return string with build status or :error symbol
- #
- # Allowed states: 'success', 'failed', 'running', 'pending', 'skipped'
- #
- #
- # Ex.
# @service.commit_status('13be4ac', 'master')
# # => 'success'
#
diff --git a/app/models/project_services/custom_issue_tracker_service.rb b/app/models/project_services/custom_issue_tracker_service.rb
index b2f426dc2ac..dea915a4d05 100644
--- a/app/models/project_services/custom_issue_tracker_service.rb
+++ b/app/models/project_services/custom_issue_tracker_service.rb
@@ -23,7 +23,7 @@ class CustomIssueTrackerService < IssueTrackerService
end
end
- def to_param
+ def self.to_param
'custom_issue_tracker'
end
diff --git a/app/models/project_services/deployment_service.rb b/app/models/project_services/deployment_service.rb
index ab353a1abe6..91a55514a9a 100644
--- a/app/models/project_services/deployment_service.rb
+++ b/app/models/project_services/deployment_service.rb
@@ -5,8 +5,8 @@
class DeploymentService < Service
default_value_for :category, 'deployment'
- def supported_events
- []
+ def self.supported_events
+ %w()
end
def predefined_variables
diff --git a/app/models/project_services/drone_ci_service.rb b/app/models/project_services/drone_ci_service.rb
index adc78a427ee..0a217d8caba 100644
--- a/app/models/project_services/drone_ci_service.rb
+++ b/app/models/project_services/drone_ci_service.rb
@@ -1,4 +1,6 @@
class DroneCiService < CiService
+ include ReactiveService
+
prop_accessor :drone_url, :token
boolean_accessor :enable_ssl_verification
@@ -30,18 +32,10 @@ class DroneCiService < CiService
true
end
- def supported_events
+ def self.supported_events
%w(push merge_request tag_push)
end
- def merge_request_status_path(iid, sha = nil, ref = nil)
- url = [drone_url,
- "gitlab/#{project.namespace.path}/#{project.path}/pulls/#{iid}",
- "?access_token=#{token}"]
-
- URI.join(*url).to_s
- end
-
def commit_status_path(sha, ref)
url = [drone_url,
"gitlab/#{project.namespace.path}/#{project.path}/commits/#{sha}",
@@ -50,54 +44,34 @@ class DroneCiService < CiService
URI.join(*url).to_s
end
- def merge_request_status(iid, sha, ref)
- response = HTTParty.get(merge_request_status_path(iid), verify: enable_ssl_verification)
-
- if response.code == 200 and response['status']
- case response['status']
- when 'killed'
- :canceled
- when 'failure', 'error'
- # Because drone return error if some test env failed
- :failed
- else
- response["status"]
- end
- else
- :error
- end
- rescue Errno::ECONNREFUSED
- :error
+ def commit_status(sha, ref)
+ with_reactive_cache(sha, ref) {|cached| cached[:commit_status] }
end
- def commit_status(sha, ref)
+ def calculate_reactive_cache(sha, ref)
response = HTTParty.get(commit_status_path(sha, ref), verify: enable_ssl_verification)
- if response.code == 200 and response['status']
- case response['status']
- when 'killed'
- :canceled
- when 'failure', 'error'
- # Because drone return error if some test env failed
- :failed
+ status =
+ if response.code == 200 and response['status']
+ case response['status']
+ when 'killed'
+ :canceled
+ when 'failure', 'error'
+ # Because drone return error if some test env failed
+ :failed
+ else
+ response["status"]
+ end
else
- response["status"]
+ :error
end
- else
- :error
- end
- rescue Errno::ECONNREFUSED
- :error
- end
- def merge_request_page(iid, sha, ref)
- url = [drone_url,
- "gitlab/#{project.namespace.path}/#{project.path}/redirect/pulls/#{iid}"]
-
- URI.join(*url).to_s
+ { commit_status: status }
+ rescue Errno::ECONNREFUSED
+ { commit_status: :error }
end
- def commit_page(sha, ref)
+ def build_page(sha, ref)
url = [drone_url,
"gitlab/#{project.namespace.path}/#{project.path}/redirect/commits/#{sha}",
"?branch=#{URI::encode(ref.to_s)}"]
@@ -105,14 +79,6 @@ class DroneCiService < CiService
URI.join(*url).to_s
end
- def commit_coverage(sha, ref)
- nil
- end
-
- def build_page(sha, ref)
- commit_page(sha, ref)
- end
-
def title
'Drone CI'
end
@@ -121,7 +87,7 @@ class DroneCiService < CiService
'Drone is a Continuous Integration platform built on Docker, written in Go'
end
- def to_param
+ def self.to_param
'drone_ci'
end
diff --git a/app/models/project_services/emails_on_push_service.rb b/app/models/project_services/emails_on_push_service.rb
index 79285cbd26d..f4f913ee0b6 100644
--- a/app/models/project_services/emails_on_push_service.rb
+++ b/app/models/project_services/emails_on_push_service.rb
@@ -12,11 +12,11 @@ class EmailsOnPushService < Service
'Email the commits and diff of each push to a list of recipients.'
end
- def to_param
+ def self.to_param
'emails_on_push'
end
- def supported_events
+ def self.supported_events
%w(push tag_push)
end
diff --git a/app/models/project_services/external_wiki_service.rb b/app/models/project_services/external_wiki_service.rb
index d7b6e505191..bdf6fa6a586 100644
--- a/app/models/project_services/external_wiki_service.rb
+++ b/app/models/project_services/external_wiki_service.rb
@@ -13,7 +13,7 @@ class ExternalWikiService < Service
'Replaces the link to the internal wiki with a link to an external wiki.'
end
- def to_param
+ def self.to_param
'external_wiki'
end
@@ -29,4 +29,8 @@ class ExternalWikiService < Service
nil
end
end
+
+ def self.supported_events
+ %w()
+ end
end
diff --git a/app/models/project_services/flowdock_service.rb b/app/models/project_services/flowdock_service.rb
index dd00275187f..10a13c3fbdc 100644
--- a/app/models/project_services/flowdock_service.rb
+++ b/app/models/project_services/flowdock_service.rb
@@ -12,7 +12,7 @@ class FlowdockService < Service
'Flowdock is a collaboration web app for technical teams.'
end
- def to_param
+ def self.to_param
'flowdock'
end
@@ -22,7 +22,7 @@ class FlowdockService < Service
]
end
- def supported_events
+ def self.supported_events
%w(push)
end
diff --git a/app/models/project_services/gemnasium_service.rb b/app/models/project_services/gemnasium_service.rb
index 598aca5e06d..f271e1f1739 100644
--- a/app/models/project_services/gemnasium_service.rb
+++ b/app/models/project_services/gemnasium_service.rb
@@ -12,7 +12,7 @@ class GemnasiumService < Service
'Gemnasium monitors your project dependencies and alerts you about updates and security vulnerabilities.'
end
- def to_param
+ def self.to_param
'gemnasium'
end
@@ -23,7 +23,7 @@ class GemnasiumService < Service
]
end
- def supported_events
+ def self.supported_events
%w(push)
end
diff --git a/app/models/project_services/gitlab_issue_tracker_service.rb b/app/models/project_services/gitlab_issue_tracker_service.rb
index 6bd8d4ec568..ad4eb9536e1 100644
--- a/app/models/project_services/gitlab_issue_tracker_service.rb
+++ b/app/models/project_services/gitlab_issue_tracker_service.rb
@@ -7,7 +7,7 @@ class GitlabIssueTrackerService < IssueTrackerService
default_value_for :default, true
- def to_param
+ def self.to_param
'gitlab'
end
diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb
index 915f6fed74c..72da219df28 100644
--- a/app/models/project_services/hipchat_service.rb
+++ b/app/models/project_services/hipchat_service.rb
@@ -27,7 +27,7 @@ class HipchatService < Service
'Private group chat and IM'
end
- def to_param
+ def self.to_param
'hipchat'
end
@@ -45,7 +45,7 @@ class HipchatService < Service
]
end
- def supported_events
+ def self.supported_events
%w(push issue confidential_issue merge_request note tag_push build)
end
diff --git a/app/models/project_services/irker_service.rb b/app/models/project_services/irker_service.rb
index 7355918feab..5d93064f9b3 100644
--- a/app/models/project_services/irker_service.rb
+++ b/app/models/project_services/irker_service.rb
@@ -17,11 +17,11 @@ class IrkerService < Service
'gateway.'
end
- def to_param
+ def self.to_param
'irker'
end
- def supported_events
+ def self.supported_events
%w(push)
end
diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb
index bce2cdd5516..9e65fdbf9d6 100644
--- a/app/models/project_services/issue_tracker_service.rb
+++ b/app/models/project_services/issue_tracker_service.rb
@@ -57,7 +57,7 @@ class IssueTrackerService < Service
end
end
- def supported_events
+ def self.supported_events
%w(push)
end
diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb
index 2d969d2fcb6..80d002f9c32 100644
--- a/app/models/project_services/jira_service.rb
+++ b/app/models/project_services/jira_service.rb
@@ -12,7 +12,7 @@ class JiraService < IssueTrackerService
# This is confusing, but JiraService does not really support these events.
# The values here are required to display correct options in the service
# configuration screen.
- def supported_events
+ def self.supported_events
%w(commit merge_request)
end
@@ -60,9 +60,9 @@ class JiraService < IssueTrackerService
end
def help
- 'You need to configure JIRA before enabling this service. For more details
+ "You need to configure JIRA before enabling this service. For more details
read the
- [JIRA service documentation](https://docs.gitlab.com/ce/project_services/jira.html).'
+ [JIRA service documentation](#{help_page_url('project_services/jira')})."
end
def title
@@ -81,7 +81,7 @@ class JiraService < IssueTrackerService
end
end
- def to_param
+ def self.to_param
'jira'
end
diff --git a/app/models/project_services/kubernetes_service.rb b/app/models/project_services/kubernetes_service.rb
index 085125ca9dc..f2f019c43bb 100644
--- a/app/models/project_services/kubernetes_service.rb
+++ b/app/models/project_services/kubernetes_service.rb
@@ -1,4 +1,5 @@
class KubernetesService < DeploymentService
+ include Gitlab::CurrentSettings
include Gitlab::Kubernetes
include ReactiveCaching
@@ -52,7 +53,7 @@ class KubernetesService < DeploymentService
'deployments with `app=$CI_ENVIRONMENT_SLUG`'
end
- def to_param
+ def self.to_param
'kubernetes'
end
@@ -110,7 +111,7 @@ class KubernetesService < DeploymentService
pods = data.fetch(:pods, nil)
filter_pods(pods, app: environment.slug).
flat_map { |pod| terminals_for_pod(api_url, namespace, pod) }.
- map { |terminal| add_terminal_auth(terminal, token, ca_pem) }
+ each { |terminal| add_terminal_auth(terminal, terminal_auth) }
end
end
@@ -170,4 +171,12 @@ class KubernetesService < DeploymentService
url.to_s
end
+
+ def terminal_auth
+ {
+ token: token,
+ ca_pem: ca_pem,
+ max_session_time: current_application_settings.terminal_max_session_time
+ }
+ end
end
diff --git a/app/models/project_services/mattermost_notification_service.rb b/app/models/project_services/mattermost_notification_service.rb
deleted file mode 100644
index de18c4b1f00..00000000000
--- a/app/models/project_services/mattermost_notification_service.rb
+++ /dev/null
@@ -1,41 +0,0 @@
-class MattermostNotificationService < ChatNotificationService
- def title
- 'Mattermost notifications'
- end
-
- def description
- 'Receive event notifications in Mattermost'
- end
-
- def to_param
- 'mattermost_notification'
- end
-
- def help
- 'This service sends notifications about projects events to Mattermost channels.<br />
- To set up this service:
- <ol>
- <li><a href="https://docs.mattermost.com/developer/webhooks-incoming.html#enabling-incoming-webhooks">Enable incoming webhooks</a> in your Mattermost installation. </li>
- <li><a href="https://docs.mattermost.com/developer/webhooks-incoming.html#creating-integrations-using-incoming-webhooks">Add an incoming webhook</a> in your Mattermost team. The default channel can be overridden for each event. </li>
- <li>Paste the webhook <strong>URL</strong> into the field bellow. </li>
- <li>Select events below to enable notifications. The channel and username are optional. </li>
- </ol>'
- end
-
- def fields
- default_fields + build_event_channels
- end
-
- def default_fields
- [
- { type: 'text', name: 'webhook', placeholder: 'http://mattermost_host/hooks/...' },
- { type: 'text', name: 'username', placeholder: 'username' },
- { type: 'checkbox', name: 'notify_only_broken_builds' },
- { type: 'checkbox', name: 'notify_only_broken_pipelines' },
- ]
- end
-
- def default_channel
- "#town-square"
- end
-end
diff --git a/app/models/project_services/mattermost_service.rb b/app/models/project_services/mattermost_service.rb
new file mode 100644
index 00000000000..4ebc5318da1
--- /dev/null
+++ b/app/models/project_services/mattermost_service.rb
@@ -0,0 +1,41 @@
+class MattermostService < ChatNotificationService
+ def title
+ 'Mattermost notifications'
+ end
+
+ def description
+ 'Receive event notifications in Mattermost'
+ end
+
+ def self.to_param
+ 'mattermost'
+ end
+
+ def help
+ 'This service sends notifications about projects events to Mattermost channels.<br />
+ To set up this service:
+ <ol>
+ <li><a href="https://docs.mattermost.com/developer/webhooks-incoming.html#enabling-incoming-webhooks">Enable incoming webhooks</a> in your Mattermost installation. </li>
+ <li><a href="https://docs.mattermost.com/developer/webhooks-incoming.html#creating-integrations-using-incoming-webhooks">Add an incoming webhook</a> in your Mattermost team. The default channel can be overridden for each event. </li>
+ <li>Paste the webhook <strong>URL</strong> into the field bellow. </li>
+ <li>Select events below to enable notifications. The channel and username are optional. </li>
+ </ol>'
+ end
+
+ def fields
+ default_fields + build_event_channels
+ end
+
+ def default_fields
+ [
+ { type: 'text', name: 'webhook', placeholder: 'http://mattermost_host/hooks/...' },
+ { type: 'text', name: 'username', placeholder: 'username' },
+ { type: 'checkbox', name: 'notify_only_broken_builds' },
+ { type: 'checkbox', name: 'notify_only_broken_pipelines' },
+ ]
+ end
+
+ def default_channel_placeholder
+ "town-square"
+ end
+end
diff --git a/app/models/project_services/mattermost_slash_commands_service.rb b/app/models/project_services/mattermost_slash_commands_service.rb
index 10740275669..56f42d63b2d 100644
--- a/app/models/project_services/mattermost_slash_commands_service.rb
+++ b/app/models/project_services/mattermost_slash_commands_service.rb
@@ -8,14 +8,44 @@ class MattermostSlashCommandsService < ChatSlashCommandsService
end
def title
- 'Mattermost Command'
+ 'Mattermost slash commands'
end
def description
- "Perform common operations on GitLab in Mattermost"
+ "Perform common operations in Mattermost"
end
- def to_param
+ def self.to_param
'mattermost_slash_commands'
end
+
+ def configure(user, params)
+ token = Mattermost::Command.new(user).
+ create(command(params))
+
+ update(active: true, token: token) if token
+ rescue Mattermost::Error => e
+ [false, e.message]
+ end
+
+ def list_teams(current_user)
+ [Mattermost::Team.new(current_user).all, nil]
+ rescue Mattermost::Error => e
+ [[], e.message]
+ end
+
+ private
+
+ def command(params)
+ pretty_project_name = project.name_with_namespace
+
+ params.merge(
+ auto_complete: true,
+ auto_complete_desc: "Perform common operations on: #{pretty_project_name}",
+ auto_complete_hint: '[help]',
+ description: "Perform common operations on: #{pretty_project_name}",
+ display_name: "GitLab / #{pretty_project_name}",
+ method: 'P',
+ username: 'GitLab')
+ end
end
diff --git a/app/models/project_services/pipelines_email_service.rb b/app/models/project_services/pipelines_email_service.rb
index 745f9bd1b43..ac617f409d9 100644
--- a/app/models/project_services/pipelines_email_service.rb
+++ b/app/models/project_services/pipelines_email_service.rb
@@ -15,11 +15,11 @@ class PipelinesEmailService < Service
'Email the pipelines status to a list of recipients.'
end
- def to_param
+ def self.to_param
'pipelines_email'
end
- def supported_events
+ def self.supported_events
%w[pipeline]
end
diff --git a/app/models/project_services/pivotaltracker_service.rb b/app/models/project_services/pivotaltracker_service.rb
index 5301f9fa0ff..9cc642591f4 100644
--- a/app/models/project_services/pivotaltracker_service.rb
+++ b/app/models/project_services/pivotaltracker_service.rb
@@ -14,7 +14,7 @@ class PivotaltrackerService < Service
'Project Management Software (Source Commits Endpoint)'
end
- def to_param
+ def self.to_param
'pivotaltracker'
end
@@ -34,7 +34,7 @@ class PivotaltrackerService < Service
]
end
- def supported_events
+ def self.supported_events
%w(push)
end
diff --git a/app/models/project_services/pushover_service.rb b/app/models/project_services/pushover_service.rb
index 3dd878e4c7d..a963d27a376 100644
--- a/app/models/project_services/pushover_service.rb
+++ b/app/models/project_services/pushover_service.rb
@@ -13,7 +13,7 @@ class PushoverService < Service
'Pushover makes it easy to get real-time notifications on your Android device, iPhone, iPad, and Desktop.'
end
- def to_param
+ def self.to_param
'pushover'
end
@@ -61,7 +61,7 @@ class PushoverService < Service
]
end
- def supported_events
+ def self.supported_events
%w(push)
end
diff --git a/app/models/project_services/redmine_service.rb b/app/models/project_services/redmine_service.rb
index f9da273cf08..6acf611eba5 100644
--- a/app/models/project_services/redmine_service.rb
+++ b/app/models/project_services/redmine_service.rb
@@ -19,7 +19,7 @@ class RedmineService < IssueTrackerService
end
end
- def to_param
+ def self.to_param
'redmine'
end
end
diff --git a/app/models/project_services/slack_notification_service.rb b/app/models/project_services/slack_notification_service.rb
deleted file mode 100644
index 3cbf89efba4..00000000000
--- a/app/models/project_services/slack_notification_service.rb
+++ /dev/null
@@ -1,40 +0,0 @@
-class SlackNotificationService < ChatNotificationService
- def title
- 'Slack notifications'
- end
-
- def description
- 'Receive event notifications in Slack'
- end
-
- def to_param
- 'slack_notification'
- end
-
- def help
- 'This service sends notifications about projects events to Slack channels.<br />
- To setup this service:
- <ol>
- <li><a href="https://slack.com/apps/A0F7XDUAZ-incoming-webhooks">Add an incoming webhook</a> in your Slack team. The default channel can be overridden for each event. </li>
- <li>Paste the <strong>Webhook URL</strong> into the field below. </li>
- <li>Select events below to enable notifications. The channel and username are optional. </li>
- </ol>'
- end
-
- def fields
- default_fields + build_event_channels
- end
-
- def default_fields
- [
- { type: 'text', name: 'webhook', placeholder: 'https://hooks.slack.com/services/...' },
- { type: 'text', name: 'username', placeholder: 'username' },
- { type: 'checkbox', name: 'notify_only_broken_builds' },
- { type: 'checkbox', name: 'notify_only_broken_pipelines' },
- ]
- end
-
- def default_channel
- "#general"
- end
-end
diff --git a/app/models/project_services/slack_service.rb b/app/models/project_services/slack_service.rb
new file mode 100644
index 00000000000..f77d2d7c60b
--- /dev/null
+++ b/app/models/project_services/slack_service.rb
@@ -0,0 +1,40 @@
+class SlackService < ChatNotificationService
+ def title
+ 'Slack notifications'
+ end
+
+ def description
+ 'Receive event notifications in Slack'
+ end
+
+ def self.to_param
+ 'slack'
+ end
+
+ def help
+ 'This service sends notifications about projects events to Slack channels.<br />
+ To setup this service:
+ <ol>
+ <li><a href="https://slack.com/apps/A0F7XDUAZ-incoming-webhooks">Add an incoming webhook</a> in your Slack team. The default channel can be overridden for each event. </li>
+ <li>Paste the <strong>Webhook URL</strong> into the field below. </li>
+ <li>Select events below to enable notifications. The channel and username are optional. </li>
+ </ol>'
+ end
+
+ def fields
+ default_fields + build_event_channels
+ end
+
+ def default_fields
+ [
+ { type: 'text', name: 'webhook', placeholder: 'https://hooks.slack.com/services/...' },
+ { type: 'text', name: 'username', placeholder: 'username' },
+ { type: 'checkbox', name: 'notify_only_broken_builds' },
+ { type: 'checkbox', name: 'notify_only_broken_pipelines' },
+ ]
+ end
+
+ def default_channel_placeholder
+ "#general"
+ end
+end
diff --git a/app/models/project_services/slack_slash_commands_service.rb b/app/models/project_services/slack_slash_commands_service.rb
index cb19ebf4cad..2182c1c7e4b 100644
--- a/app/models/project_services/slack_slash_commands_service.rb
+++ b/app/models/project_services/slack_slash_commands_service.rb
@@ -2,21 +2,21 @@ class SlackSlashCommandsService < ChatSlashCommandsService
include TriggersHelper
def title
- 'Slack Command'
+ 'Slack slash commands'
end
def description
- "Perform common operations on GitLab in Slack"
+ "Perform common operations in Slack"
end
- def to_param
+ def self.to_param
'slack_slash_commands'
end
def trigger(params)
# Format messages to be Slack-compatible
super.tap do |result|
- result[:text] = format(result[:text])
+ result[:text] = format(result[:text]) if result.is_a?(Hash)
end
end
diff --git a/app/models/project_services/teamcity_service.rb b/app/models/project_services/teamcity_service.rb
index a4a967c9bc9..cbaffb8ce48 100644
--- a/app/models/project_services/teamcity_service.rb
+++ b/app/models/project_services/teamcity_service.rb
@@ -1,4 +1,6 @@
class TeamcityService < CiService
+ include ReactiveService
+
prop_accessor :teamcity_url, :build_type, :username, :password
validates :teamcity_url, presence: true, url: true, if: :activated?
@@ -41,14 +43,10 @@ class TeamcityService < CiService
'requests build, that setting is in the vsc root advanced settings.'
end
- def to_param
+ def self.to_param
'teamcity'
end
- def supported_events
- %w(push)
- end
-
def fields
[
{ type: 'text', name: 'teamcity_url',
@@ -61,43 +59,18 @@ class TeamcityService < CiService
]
end
- def build_info(sha)
- @response = get_path("httpAuth/app/rest/builds/branch:unspecified:any,number:#{sha}")
- end
-
def build_page(sha, ref)
- build_info(sha) if @response.nil? || !@response.code
-
- if @response.code != 200
- # If actual build link can't be determined,
- # send user to build summary page.
- build_url("viewLog.html?buildTypeId=#{build_type}")
- else
- # If actual build link is available, go to build result page.
- built_id = @response['build']['id']
- build_url("viewLog.html?buildId=#{built_id}&buildTypeId=#{build_type}")
- end
+ with_reactive_cache(sha, ref) {|cached| cached[:build_page] }
end
def commit_status(sha, ref)
- build_info(sha) if @response.nil? || !@response.code
- return :error unless @response.code == 200 || @response.code == 404
+ with_reactive_cache(sha, ref) {|cached| cached[:commit_status] }
+ end
- status = if @response.code == 404
- 'Pending'
- else
- @response['build']['status']
- end
+ def calculate_reactive_cache(sha, ref)
+ response = get_path("httpAuth/app/rest/builds/branch:unspecified:any,number:#{sha}")
- if status.include?('SUCCESS')
- 'success'
- elsif status.include?('FAILURE')
- 'failed'
- elsif status.include?('Pending')
- 'pending'
- else
- :error
- end
+ { build_page: read_build_page(response), commit_status: read_commit_status(response) }
end
def execute(data)
@@ -122,6 +95,40 @@ class TeamcityService < CiService
private
+ def read_build_page(response)
+ if response.code != 200
+ # If actual build link can't be determined,
+ # send user to build summary page.
+ build_url("viewLog.html?buildTypeId=#{build_type}")
+ else
+ # If actual build link is available, go to build result page.
+ built_id = response['build']['id']
+ build_url("viewLog.html?buildId=#{built_id}&buildTypeId=#{build_type}")
+ end
+ end
+
+ def read_commit_status(response)
+ return :error unless response.code == 200 || response.code == 404
+
+ status = if response.code == 404
+ 'Pending'
+ else
+ response['build']['status']
+ end
+
+ return :error unless status.present?
+
+ if status.include?('SUCCESS')
+ 'success'
+ elsif status.include?('FAILURE')
+ 'failed'
+ elsif status.include?('Pending')
+ 'pending'
+ else
+ :error
+ end
+ end
+
def build_url(path)
URI.join("#{teamcity_url}/", path).to_s
end
diff --git a/app/models/project_snippet.rb b/app/models/project_snippet.rb
index 25b5d777641..9bb456eee24 100644
--- a/app/models/project_snippet.rb
+++ b/app/models/project_snippet.rb
@@ -9,4 +9,8 @@ class ProjectSnippet < Snippet
participant :author
participant :notes_with_associations
+
+ def check_for_spam?
+ super && project.public?
+ end
end
diff --git a/app/models/project_statistics.rb b/app/models/project_statistics.rb
new file mode 100644
index 00000000000..06abd406523
--- /dev/null
+++ b/app/models/project_statistics.rb
@@ -0,0 +1,44 @@
+class ProjectStatistics < ActiveRecord::Base
+ belongs_to :project
+ belongs_to :namespace
+
+ before_save :update_storage_size
+
+ STORAGE_COLUMNS = [:repository_size, :lfs_objects_size, :build_artifacts_size]
+ STATISTICS_COLUMNS = [:commit_count] + STORAGE_COLUMNS
+
+ def total_repository_size
+ repository_size + lfs_objects_size
+ end
+
+ def refresh!(only: nil)
+ STATISTICS_COLUMNS.each do |column, generator|
+ if only.blank? || only.include?(column)
+ public_send("update_#{column}")
+ end
+ end
+
+ save!
+ end
+
+ def update_commit_count
+ self.commit_count = project.repository.commit_count
+ end
+
+ # Repository#size needs to be converted from MB to Byte.
+ def update_repository_size
+ self.repository_size = project.repository.size * 1.megabyte
+ end
+
+ def update_lfs_objects_size
+ self.lfs_objects_size = project.lfs_objects.sum(:size)
+ end
+
+ def update_build_artifacts_size
+ self.build_artifacts_size = project.builds.sum(:artifacts_size)
+ end
+
+ def update_storage_size
+ self.storage_size = STORAGE_COLUMNS.sum(&method(:read_attribute))
+ end
+end
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 1ccabdb7c1f..d2d92a064a4 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -5,7 +5,7 @@ class Repository
attr_accessor :path_with_namespace, :project
- class CommitError < StandardError; end
+ CommitError = Class.new(StandardError)
# Methods that cache data from the Git repository.
#
@@ -64,10 +64,6 @@ class Repository
@raw_repository ||= Gitlab::Git::Repository.new(path_to_repo)
end
- def update_autocrlf_option
- raw_repository.autocrlf = :input if raw_repository.autocrlf != :input
- end
-
# Return absolute path to repository
def path_to_repo
@path_to_repo ||= File.expand_path(
@@ -168,63 +164,46 @@ class Repository
tags.find { |tag| tag.name == name }
end
- def add_branch(user, branch_name, target)
- oldrev = Gitlab::Git::BLANK_SHA
- ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name
- target = commit(target).try(:id)
+ def add_branch(user, branch_name, ref)
+ newrev = commit(ref).try(:sha)
- return false unless target
+ return false unless newrev
- GitHooksService.new.execute(user, path_to_repo, oldrev, target, ref) do
- update_ref!(ref, target, oldrev)
- end
+ GitOperationService.new(user, self).add_branch(branch_name, newrev)
after_create_branch
find_branch(branch_name)
end
def add_tag(user, tag_name, target, message = nil)
- oldrev = Gitlab::Git::BLANK_SHA
- ref = Gitlab::Git::TAG_REF_PREFIX + tag_name
- target = commit(target).try(:id)
-
- return false unless target
-
+ newrev = commit(target).try(:id)
options = { message: message, tagger: user_to_committer(user) } if message
- GitHooksService.new.execute(user, path_to_repo, oldrev, target, ref) do |service|
- raw_tag = rugged.tags.create(tag_name, target, options)
- service.newrev = raw_tag.target_id
- end
+ return false unless newrev
+
+ GitOperationService.new(user, self).add_tag(tag_name, newrev, options)
find_tag(tag_name)
end
def rm_branch(user, branch_name)
before_remove_branch
-
branch = find_branch(branch_name)
- oldrev = branch.try(:dereferenced_target).try(:id)
- newrev = Gitlab::Git::BLANK_SHA
- ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name
- GitHooksService.new.execute(user, path_to_repo, oldrev, newrev, ref) do
- update_ref!(ref, newrev, oldrev)
- end
+ GitOperationService.new(user, self).rm_branch(branch)
after_remove_branch
true
end
- def rm_tag(tag_name)
+ def rm_tag(user, tag_name)
before_remove_tag
+ tag = find_tag(tag_name)
- begin
- rugged.tags.delete(tag_name)
- true
- rescue Rugged::ReferenceError
- false
- end
+ GitOperationService.new(user, self).rm_tag(tag)
+
+ after_remove_tag
+ true
end
def ref_names
@@ -241,21 +220,6 @@ class Repository
false
end
- def update_ref!(name, newrev, oldrev)
- # We use 'git update-ref' because libgit2/rugged currently does not
- # offer 'compare and swap' ref updates. Without compare-and-swap we can
- # (and have!) accidentally reset the ref to an earlier state, clobbering
- # commits. See also https://github.com/libgit2/libgit2/issues/1534.
- command = %W(#{Gitlab.config.git.bin_path} update-ref --stdin -z)
- _, status = Gitlab::Popen.popen(command, path_to_repo) do |stdin|
- stdin.write("update #{name}\x00#{newrev}\x00#{oldrev}\x00")
- end
-
- return if status.zero?
-
- raise CommitError.new("Could not update branch #{name.sub('refs/heads/', '')}. Please refresh and try again.")
- end
-
# Makes sure a commit is kept around when Git garbage collection runs.
# Git GC will delete commits from the repository that are no longer in any
# branches or tags, but we want to keep some of these commits around, for
@@ -435,10 +399,20 @@ class Repository
repository_event(:remove_tag)
end
+ # Runs code after removing a tag.
+ def after_remove_tag
+ expire_tags_cache
+ end
+
def before_import
expire_content_cache
end
+ # Runs code after the HEAD of a repository is changed.
+ def after_change_head
+ expire_method_caches(METHOD_CACHES_FOR_FILE_TYPES.keys)
+ end
+
# Runs code after a repository has been forked/imported.
def after_import
expire_content_cache
@@ -490,6 +464,8 @@ class Repository
unless Gitlab::Git.blank_ref?(sha)
Blob.decorate(Gitlab::Git::Blob.find(self, sha, path))
end
+ rescue Gitlab::Git::Repository::NoRepository
+ nil
end
def blob_by_oid(oid)
@@ -654,11 +630,19 @@ class Repository
end
def last_commit_for_path(sha, path)
- args = %W(#{Gitlab.config.git.bin_path} rev-list --max-count=1 #{sha} -- #{path})
- sha = Gitlab::Popen.popen(args, path_to_repo).first.strip
+ sha = last_commit_id_for_path(sha, path)
commit(sha)
end
+ def last_commit_id_for_path(sha, path)
+ key = path.blank? ? "last_commit_id_for_path:#{sha}" : "last_commit_id_for_path:#{sha}:#{Digest::SHA1.hexdigest(path)}"
+
+ cache.fetch(key) do
+ args = %W(#{Gitlab.config.git.bin_path} rev-list --max-count=1 #{sha} -- #{path})
+ Gitlab::Popen.popen(args, path_to_repo).first.strip
+ end
+ end
+
def next_branch(name, opts = {})
branch_ids = self.branch_names.map do |n|
next 1 if n == name
@@ -766,121 +750,132 @@ class Repository
@tags ||= raw_repository.tags
end
- def commit_dir(user, path, message, branch, author_email: nil, author_name: nil)
- update_branch_with_hooks(user, branch) do |ref|
- options = {
- commit: {
- branch: ref,
- message: message,
- update_ref: false
- }
- }
-
- options.merge!(get_committer_and_author(user, email: author_email, name: author_name))
-
- raw_repository.mkdir(path, options)
- end
- end
-
- def commit_file(user, path, content, message, branch, update, author_email: nil, author_name: nil)
- update_branch_with_hooks(user, branch) do |ref|
- options = {
- commit: {
- branch: ref,
- message: message,
- update_ref: false
- },
- file: {
- content: content,
- path: path,
- update: update
- }
- }
-
- options.merge!(get_committer_and_author(user, email: author_email, name: author_name))
+ # rubocop:disable Metrics/ParameterLists
+ def commit_dir(
+ user, path,
+ message:, branch_name:,
+ author_email: nil, author_name: nil,
+ start_branch_name: nil, start_project: project)
+ check_tree_entry_for_dir(branch_name, path)
- Gitlab::Git::Blob.commit(raw_repository, options)
+ if start_branch_name
+ start_project.repository.
+ check_tree_entry_for_dir(start_branch_name, path)
end
- end
-
- def update_file(user, path, content, branch:, previous_path:, message:, author_email: nil, author_name: nil)
- update_branch_with_hooks(user, branch) do |ref|
- options = {
- commit: {
- branch: ref,
- message: message,
- update_ref: false
- },
- file: {
- content: content,
- path: path,
- update: true
- }
- }
- options.merge!(get_committer_and_author(user, email: author_email, name: author_name))
-
- if previous_path && previous_path != path
- options[:file][:previous_path] = previous_path
- Gitlab::Git::Blob.rename(raw_repository, options)
- else
- Gitlab::Git::Blob.commit(raw_repository, options)
+ commit_file(
+ user,
+ "#{path}/.gitkeep",
+ '',
+ message: message,
+ branch_name: branch_name,
+ update: false,
+ author_email: author_email,
+ author_name: author_name,
+ start_branch_name: start_branch_name,
+ start_project: start_project)
+ end
+ # rubocop:enable Metrics/ParameterLists
+
+ # rubocop:disable Metrics/ParameterLists
+ def commit_file(
+ user, path, content,
+ message:, branch_name:, update: true,
+ author_email: nil, author_name: nil,
+ start_branch_name: nil, start_project: project)
+ unless update
+ error_message = "Filename already exists; update not allowed"
+
+ if tree_entry_at(branch_name, path)
+ raise Gitlab::Git::Repository::InvalidBlobName.new(error_message)
end
- end
- end
-
- def remove_file(user, path, message, branch, author_email: nil, author_name: nil)
- update_branch_with_hooks(user, branch) do |ref|
- options = {
- commit: {
- branch: ref,
- message: message,
- update_ref: false
- },
- file: {
- path: path
- }
- }
-
- options.merge!(get_committer_and_author(user, email: author_email, name: author_name))
- Gitlab::Git::Blob.remove(raw_repository, options)
+ if start_branch_name &&
+ start_project.repository.tree_entry_at(start_branch_name, path)
+ raise Gitlab::Git::Repository::InvalidBlobName.new(error_message)
+ end
end
- end
- def multi_action(user:, branch:, message:, actions:, author_email: nil, author_name: nil)
- update_branch_with_hooks(user, branch) do |ref|
+ multi_action(
+ user: user,
+ message: message,
+ branch_name: branch_name,
+ author_email: author_email,
+ author_name: author_name,
+ start_branch_name: start_branch_name,
+ start_project: start_project,
+ actions: [{ action: :create,
+ file_path: path,
+ content: content }])
+ end
+ # rubocop:enable Metrics/ParameterLists
+
+ # rubocop:disable Metrics/ParameterLists
+ def update_file(
+ user, path, content,
+ message:, branch_name:, previous_path:,
+ author_email: nil, author_name: nil,
+ start_branch_name: nil, start_project: project)
+ action = if previous_path && previous_path != path
+ :move
+ else
+ :update
+ end
+
+ multi_action(
+ user: user,
+ message: message,
+ branch_name: branch_name,
+ author_email: author_email,
+ author_name: author_name,
+ start_branch_name: start_branch_name,
+ start_project: start_project,
+ actions: [{ action: action,
+ file_path: path,
+ content: content,
+ previous_path: previous_path }])
+ end
+ # rubocop:enable Metrics/ParameterLists
+
+ # rubocop:disable Metrics/ParameterLists
+ def remove_file(
+ user, path,
+ message:, branch_name:,
+ author_email: nil, author_name: nil,
+ start_branch_name: nil, start_project: project)
+ multi_action(
+ user: user,
+ message: message,
+ branch_name: branch_name,
+ author_email: author_email,
+ author_name: author_name,
+ start_branch_name: start_branch_name,
+ start_project: start_project,
+ actions: [{ action: :delete,
+ file_path: path }])
+ end
+ # rubocop:enable Metrics/ParameterLists
+
+ # rubocop:disable Metrics/ParameterLists
+ def multi_action(
+ user:, branch_name:, message:, actions:,
+ author_email: nil, author_name: nil,
+ start_branch_name: nil, start_project: project)
+ GitOperationService.new(user, self).with_branch(
+ branch_name,
+ start_branch_name: start_branch_name,
+ start_project: start_project) do |start_commit|
index = rugged.index
- parents = []
- branch = find_branch(ref)
- if branch
- last_commit = branch.dereferenced_target
- index.read_tree(last_commit.raw_commit.tree)
- parents = [last_commit.sha]
- end
+ parents = if start_commit
+ index.read_tree(start_commit.raw_commit.tree)
+ [start_commit.sha]
+ else
+ []
+ end
- actions.each do |action|
- case action[:action]
- when :create, :update, :move
- mode =
- case action[:action]
- when :update
- index.get(action[:file_path])[:mode]
- when :move
- index.get(action[:previous_path])[:mode]
- end
- mode ||= 0o100644
-
- index.remove(action[:previous_path]) if action[:action] == :move
-
- content = action[:encoding] == 'base64' ? Base64.decode64(action[:content]) : action[:content]
- oid = rugged.write(content, :blob)
-
- index.add(path: action[:file_path], oid: oid, mode: mode)
- when :delete
- index.remove(action[:file_path])
- end
+ actions.each do |act|
+ git_action(index, act)
end
options = {
@@ -893,6 +888,7 @@ class Repository
Rugged::Commit.create(rugged, options)
end
end
+ # rubocop:enable Metrics/ParameterLists
def get_committer_and_author(user, email: nil, name: nil)
committer = user_to_committer(user)
@@ -905,7 +901,7 @@ class Repository
end
def user_to_committer(user)
- Gitlab::Git::committer_hash(email: user.email, name: user.name)
+ Gitlab::Git.committer_hash(email: user.email, name: user.name)
end
def can_be_merged?(source_sha, target_branch)
@@ -919,17 +915,18 @@ class Repository
end
end
- def merge(user, merge_request, options = {})
- our_commit = rugged.branches[merge_request.target_branch].target
- their_commit = rugged.lookup(merge_request.diff_head_sha)
+ def merge(user, source, merge_request, options = {})
+ GitOperationService.new(user, self).with_branch(
+ merge_request.target_branch) do |start_commit|
+ our_commit = start_commit.sha
+ their_commit = source
- raise "Invalid merge target" if our_commit.nil?
- raise "Invalid merge source" if their_commit.nil?
+ raise 'Invalid merge target' unless our_commit
+ raise 'Invalid merge source' unless their_commit
- merge_index = rugged.merge_commits(our_commit, their_commit)
- return false if merge_index.conflicts?
+ merge_index = rugged.merge_commits(our_commit, their_commit)
+ break if merge_index.conflicts?
- update_branch_with_hooks(user, merge_request.target_branch) do
actual_options = options.merge(
parents: [our_commit, their_commit],
tree: merge_index.write_tree(rugged),
@@ -939,34 +936,48 @@ class Repository
merge_request.update(in_progress_merge_commit_sha: commit_id)
commit_id
end
+ rescue Repository::CommitError # when merge_index.conflicts?
+ false
end
- def revert(user, commit, base_branch, revert_tree_id = nil)
- source_sha = find_branch(base_branch).dereferenced_target.sha
- revert_tree_id ||= check_revert_content(commit, base_branch)
+ def revert(
+ user, commit, branch_name, revert_tree_id = nil,
+ start_branch_name: nil, start_project: project)
+ revert_tree_id ||= check_revert_content(commit, branch_name)
return false unless revert_tree_id
- update_branch_with_hooks(user, base_branch) do
+ GitOperationService.new(user, self).with_branch(
+ branch_name,
+ start_branch_name: start_branch_name,
+ start_project: start_project) do |start_commit|
+
committer = user_to_committer(user)
- source_sha = Rugged::Commit.create(rugged,
+
+ Rugged::Commit.create(rugged,
message: commit.revert_message(user),
author: committer,
committer: committer,
tree: revert_tree_id,
- parents: [rugged.lookup(source_sha)])
+ parents: [start_commit.sha])
end
end
- def cherry_pick(user, commit, base_branch, cherry_pick_tree_id = nil)
- source_sha = find_branch(base_branch).dereferenced_target.sha
- cherry_pick_tree_id ||= check_cherry_pick_content(commit, base_branch)
+ def cherry_pick(
+ user, commit, branch_name, cherry_pick_tree_id = nil,
+ start_branch_name: nil, start_project: project)
+ cherry_pick_tree_id ||= check_cherry_pick_content(commit, branch_name)
return false unless cherry_pick_tree_id
- update_branch_with_hooks(user, base_branch) do
+ GitOperationService.new(user, self).with_branch(
+ branch_name,
+ start_branch_name: start_branch_name,
+ start_project: start_project) do |start_commit|
+
committer = user_to_committer(user)
- source_sha = Rugged::Commit.create(rugged,
+
+ Rugged::Commit.create(rugged,
message: commit.message,
author: {
email: commit.author_email,
@@ -975,22 +986,22 @@ class Repository
},
committer: committer,
tree: cherry_pick_tree_id,
- parents: [rugged.lookup(source_sha)])
+ parents: [start_commit.sha])
end
end
- def resolve_conflicts(user, branch, params)
- update_branch_with_hooks(user, branch) do
+ def resolve_conflicts(user, branch_name, params)
+ GitOperationService.new(user, self).with_branch(branch_name) do
committer = user_to_committer(user)
Rugged::Commit.create(rugged, params.merge(author: committer, committer: committer))
end
end
- def check_revert_content(commit, base_branch)
- source_sha = find_branch(base_branch).dereferenced_target.sha
- args = [commit.id, source_sha]
- args << { mainline: 1 } if commit.merge_commit?
+ def check_revert_content(target_commit, branch_name)
+ source_sha = commit(branch_name).sha
+ args = [target_commit.sha, source_sha]
+ args << { mainline: 1 } if target_commit.merge_commit?
revert_index = rugged.revert_commit(*args)
return false if revert_index.conflicts?
@@ -1001,10 +1012,10 @@ class Repository
tree_id
end
- def check_cherry_pick_content(commit, base_branch)
- source_sha = find_branch(base_branch).dereferenced_target.sha
- args = [commit.id, source_sha]
- args << 1 if commit.merge_commit?
+ def check_cherry_pick_content(target_commit, branch_name)
+ source_sha = commit(branch_name).sha
+ args = [target_commit.sha, source_sha]
+ args << 1 if target_commit.merge_commit?
cherry_pick_index = rugged.cherrypick_commit(*args)
return false if cherry_pick_index.conflicts?
@@ -1062,6 +1073,28 @@ class Repository
Gitlab::Popen.popen(args, path_to_repo).first.lines.map(&:strip)
end
+ def with_repo_branch_commit(start_repository, start_branch_name)
+ branch_name_or_sha =
+ if start_repository == self
+ start_branch_name
+ else
+ tmp_ref = "refs/tmp/#{SecureRandom.hex}/head"
+
+ fetch_ref(
+ start_repository.path_to_repo,
+ "#{Gitlab::Git::BRANCH_REF_PREFIX}#{start_branch_name}",
+ tmp_ref
+ )
+
+ start_repository.commit(start_branch_name).sha
+ end
+
+ yield(commit(branch_name_or_sha))
+
+ ensure
+ rugged.references.delete(tmp_ref) if tmp_ref
+ end
+
def fetch_ref(source_path, source_ref, target_ref)
args = %W(#{Gitlab.config.git.bin_path} fetch --no-tags -f #{source_path} #{source_ref}:#{target_ref})
Gitlab::Popen.popen(args, path_to_repo)
@@ -1071,39 +1104,6 @@ class Repository
fetch_ref(path_to_repo, ref, ref_path)
end
- def update_branch_with_hooks(current_user, branch)
- update_autocrlf_option
-
- ref = Gitlab::Git::BRANCH_REF_PREFIX + branch
- target_branch = find_branch(branch)
- was_empty = empty?
-
- # Make commit
- newrev = yield(ref)
-
- unless newrev
- raise CommitError.new('Failed to create commit')
- end
-
- if rugged.lookup(newrev).parent_ids.empty? || target_branch.nil?
- oldrev = Gitlab::Git::BLANK_SHA
- else
- oldrev = rugged.merge_base(newrev, target_branch.dereferenced_target.sha)
- end
-
- GitHooksService.new.execute(current_user, path_to_repo, oldrev, newrev, ref) do
- update_ref!(ref, newrev, oldrev)
-
- if was_empty || !target_branch
- # If repo was empty expire cache
- after_create if was_empty
- after_create_branch
- end
- end
-
- newrev
- end
-
def ls_files(ref)
actual_ref = ref || root_ref
raw_repository.ls_files(actual_ref)
@@ -1162,8 +1162,92 @@ class Repository
end
end
+ def route_map_for(sha)
+ blob_data_at(sha, '.gitlab/route-map.yml')
+ end
+
+ def gitlab_ci_yml_for(sha)
+ blob_data_at(sha, '.gitlab-ci.yml')
+ end
+
+ protected
+
+ def tree_entry_at(branch_name, path)
+ branch_exists?(branch_name) &&
+ # tree_entry is private
+ raw_repository.send(:tree_entry, commit(branch_name), path)
+ end
+
+ def check_tree_entry_for_dir(branch_name, path)
+ return unless branch_exists?(branch_name)
+
+ entry = tree_entry_at(branch_name, path)
+
+ return unless entry
+
+ if entry[:type] == :blob
+ raise Gitlab::Git::Repository::InvalidBlobName.new(
+ "Directory already exists as a file")
+ else
+ raise Gitlab::Git::Repository::InvalidBlobName.new(
+ "Directory already exists")
+ end
+ end
+
private
+ def blob_data_at(sha, path)
+ blob = blob_at(sha, path)
+ return unless blob
+
+ blob.load_all_data!(self)
+ blob.data
+ end
+
+ def git_action(index, action)
+ path = normalize_path(action[:file_path])
+
+ if action[:action] == :move
+ previous_path = normalize_path(action[:previous_path])
+ end
+
+ case action[:action]
+ when :create, :update, :move
+ mode =
+ case action[:action]
+ when :update
+ index.get(path)[:mode]
+ when :move
+ index.get(previous_path)[:mode]
+ end
+ mode ||= 0o100644
+
+ index.remove(previous_path) if action[:action] == :move
+
+ content = if action[:encoding] == 'base64'
+ Base64.decode64(action[:content])
+ else
+ action[:content]
+ end
+
+ oid = rugged.write(content, :blob)
+
+ index.add(path: path, oid: oid, mode: mode)
+ when :delete
+ index.remove(path)
+ end
+ end
+
+ def normalize_path(path)
+ pathname = Gitlab::Git::PathHelper.normalize_path(path)
+
+ if pathname.each_filename.include?('..')
+ raise Gitlab::Git::Repository::InvalidBlobName.new('Invalid path')
+ end
+
+ pathname.to_s
+ end
+
def refs_directory_exists?
return false unless path_with_namespace
@@ -1175,7 +1259,18 @@ class Repository
end
def tags_sorted_by_committed_date
- tags.sort_by { |tag| tag.dereferenced_target.committed_date }
+ tags.sort_by do |tag|
+ # Annotated tags can point to any object (e.g. a blob), but generally
+ # tags point to a commit. If we don't have a commit, then just default
+ # to putting the tag at the end of the list.
+ target = tag.dereferenced_target
+
+ if target
+ target.committed_date
+ else
+ Time.now
+ end
+ end
end
def keep_around_ref_name(sha)
diff --git a/app/models/route.rb b/app/models/route.rb
index caf596efa79..dd171fdb069 100644
--- a/app/models/route.rb
+++ b/app/models/route.rb
@@ -8,15 +8,16 @@ class Route < ActiveRecord::Base
presence: true,
uniqueness: { case_sensitive: false }
- after_update :rename_children, if: :path_changed?
+ after_update :rename_descendants, if: :path_changed?
- def rename_children
+ def rename_descendants
# We update each row separately because MySQL does not have regexp_replace.
# rubocop:disable Rails/FindEach
Route.where('path LIKE ?', "#{path_was}/%").each do |route|
# Note that update column skips validation and callbacks.
- # We need this to avoid recursive call of rename_children method
+ # We need this to avoid recursive call of rename_descendants method
route.update_column(:path, route.path.sub(path_was, path))
end
+ # rubocop:enable Rails/FindEach
end
end
diff --git a/app/models/service.rb b/app/models/service.rb
index 8abd8e73e43..043be222f3a 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -76,6 +76,11 @@ class Service < ActiveRecord::Base
def to_param
# implement inside child
+ self.class.to_param
+ end
+
+ def self.to_param
+ raise NotImplementedError
end
def fields
@@ -92,7 +97,11 @@ class Service < ActiveRecord::Base
end
def event_names
- supported_events.map { |event| "#{event}_events" }
+ self.class.event_names
+ end
+
+ def self.event_names
+ self.supported_events.map { |event| "#{event}_events" }
end
def event_field(event)
@@ -104,6 +113,10 @@ class Service < ActiveRecord::Base
end
def supported_events
+ self.class.supported_events
+ end
+
+ def self.supported_events
%w(push tag_push issue confidential_issue merge_request wiki_page)
end
@@ -216,13 +229,13 @@ class Service < ActiveRecord::Base
jira
kubernetes
mattermost_slash_commands
- mattermost_notification
+ mattermost
pipelines_email
pivotaltracker
pushover
redmine
slack_slash_commands
- slack_notification
+ slack
teamcity
]
end
diff --git a/app/models/snippet.rb b/app/models/snippet.rb
index 98ccf5f331f..2665a7249a3 100644
--- a/app/models/snippet.rb
+++ b/app/models/snippet.rb
@@ -7,6 +7,7 @@ class Snippet < ActiveRecord::Base
include Sortable
include Awardable
include Mentionable
+ include Spammable
cache_markdown_field :title, pipeline: :single_line
cache_markdown_field :content
@@ -17,7 +18,7 @@ class Snippet < ActiveRecord::Base
default_content_html_invalidator || file_name_changed?
end
- default_value_for :visibility_level, Snippet::PRIVATE
+ default_value_for(:visibility_level) { current_application_settings.default_snippet_visibility }
belongs_to :author, class_name: 'User'
belongs_to :project
@@ -46,6 +47,9 @@ class Snippet < ActiveRecord::Base
participant :author
participant :notes_with_associations
+ attr_spammable :title, spam_title: true
+ attr_spammable :content, spam_description: true
+
def self.reference_prefix
'$'
end
@@ -64,11 +68,11 @@ class Snippet < ActiveRecord::Base
@link_reference_pattern ||= super("snippets", /(?<snippet>\d+)/)
end
- def to_reference(from_project = nil)
+ def to_reference(from_project = nil, full: false)
reference = "#{self.class.reference_prefix}#{id}"
if project.present?
- "#{project.to_reference(from_project)}#{reference}"
+ "#{project.to_reference(from_project, full: full)}#{reference}"
else
reference
end
@@ -127,6 +131,14 @@ class Snippet < ActiveRecord::Base
notes.includes(:author)
end
+ def check_for_spam?
+ public?
+ end
+
+ def spammable_entity_type
+ 'snippet'
+ end
+
class << self
# Searches for snippets with a matching title or file name.
#
diff --git a/app/models/timelog.rb b/app/models/timelog.rb
new file mode 100644
index 00000000000..e166cf69703
--- /dev/null
+++ b/app/models/timelog.rb
@@ -0,0 +1,22 @@
+class Timelog < ActiveRecord::Base
+ validates :time_spent, :user, presence: true
+ validate :issuable_id_is_present
+
+ belongs_to :issue
+ belongs_to :merge_request
+ belongs_to :user
+
+ def issuable
+ issue || merge_request
+ end
+
+ private
+
+ def issuable_id_is_present
+ if issue_id && merge_request_id
+ errors.add(:base, 'Only Issue ID or Merge Request ID is required')
+ elsif issuable.nil?
+ errors.add(:base, 'Issue or Merge Request ID is required')
+ end
+ end
+end
diff --git a/app/models/todo.rb b/app/models/todo.rb
index f5ade1cc293..2adf494ce11 100644
--- a/app/models/todo.rb
+++ b/app/models/todo.rb
@@ -6,13 +6,15 @@ class Todo < ActiveRecord::Base
BUILD_FAILED = 3
MARKED = 4
APPROVAL_REQUIRED = 5 # This is an EE-only feature
+ UNMERGEABLE = 6
ACTION_NAMES = {
ASSIGNED => :assigned,
MENTIONED => :mentioned,
BUILD_FAILED => :build_failed,
MARKED => :marked,
- APPROVAL_REQUIRED => :approval_required
+ APPROVAL_REQUIRED => :approval_required,
+ UNMERGEABLE => :unmergeable
}
belongs_to :author, class_name: "User"
@@ -66,6 +68,10 @@ class Todo < ActiveRecord::Base
end
end
+ def unmergeable?
+ action == UNMERGEABLE
+ end
+
def build_failed?
action == BUILD_FAILED
end
@@ -97,9 +103,9 @@ class Todo < ActiveRecord::Base
def target_reference
if for_commit?
- target.short_id
+ target.reference_link_text(full: true)
else
- target.to_reference
+ target.to_reference(full: true)
end
end
diff --git a/app/models/user.rb b/app/models/user.rb
index 899a89a4eaa..f64d0c17a45 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -73,7 +73,7 @@ class User < ActiveRecord::Base
has_many :created_projects, foreign_key: :creator_id, class_name: 'Project'
has_many :users_star_projects, dependent: :destroy
has_many :starred_projects, through: :users_star_projects, source: :project
- has_many :project_authorizations, dependent: :destroy
+ has_many :project_authorizations
has_many :authorized_projects, through: :project_authorizations, source: :project
has_many :snippets, dependent: :destroy, foreign_key: :author_id
@@ -83,8 +83,6 @@ class User < ActiveRecord::Base
has_many :events, dependent: :destroy, foreign_key: :author_id
has_many :subscriptions, dependent: :destroy
has_many :recent_events, -> { order "id DESC" }, foreign_key: :author_id, class_name: "Event"
- has_many :assigned_issues, dependent: :destroy, foreign_key: :assignee_id, class_name: "Issue"
- has_many :assigned_merge_requests, dependent: :destroy, foreign_key: :assignee_id, class_name: "MergeRequest"
has_many :oauth_applications, class_name: 'Doorkeeper::Application', as: :owner, dependent: :destroy
has_one :abuse_report, dependent: :destroy
has_many :spam_logs, dependent: :destroy
@@ -94,11 +92,15 @@ class User < ActiveRecord::Base
has_many :notification_settings, dependent: :destroy
has_many :award_emoji, dependent: :destroy
+ has_many :assigned_issues, dependent: :nullify, foreign_key: :assignee_id, class_name: "Issue"
+ has_many :assigned_merge_requests, dependent: :nullify, foreign_key: :assignee_id, class_name: "MergeRequest"
+
#
# Validations
#
# Note: devise :validatable above adds validations for :email and :password
validates :name, presence: true
+ validates_confirmation_of :email
validates :notification_email, presence: true
validates :notification_email, email: true, if: ->(user) { user.notification_email != user.email }
validates :public_email, presence: true, uniqueness: true, email: true, allow_blank: true
@@ -178,8 +180,8 @@ class User < ActiveRecord::Base
scope :not_in_project, ->(project) { project.users.present? ? where("id not in (:ids)", ids: project.users.map(&:id) ) : all }
scope :without_projects, -> { where('id NOT IN (SELECT DISTINCT(user_id) FROM members WHERE user_id IS NOT NULL AND requested_at IS NULL)') }
scope :todo_authors, ->(user_id, state) { where(id: Todo.where(user_id: user_id, state: state).select(:author_id)) }
- scope :order_recent_sign_in, -> { reorder(last_sign_in_at: :desc) }
- scope :order_oldest_sign_in, -> { reorder(last_sign_in_at: :asc) }
+ scope :order_recent_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('last_sign_in_at', 'DESC')) }
+ scope :order_oldest_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('last_sign_in_at', 'ASC')) }
def self.with_two_factor
joins("LEFT OUTER JOIN u2f_registrations AS u2f ON u2f.user_id = users.id").
@@ -332,7 +334,7 @@ class User < ActiveRecord::Base
username
end
- def to_reference(_from_project = nil, _target_project = nil)
+ def to_reference(_from_project = nil, target_project: nil, full: nil)
"#{self.class.reference_prefix}#{username}"
end
@@ -438,12 +440,21 @@ class User < ActiveRecord::Base
Group.where("namespaces.id IN (#{union.to_sql})")
end
+ def nested_groups
+ Group.member_descendants(id)
+ end
+
+ def nested_projects
+ Project.joins(:namespace).where('namespaces.parent_id IS NOT NULL').
+ member_descendants(id)
+ end
+
def refresh_authorized_projects
Users::RefreshAuthorizedProjectsService.new(self).execute
end
def remove_project_authorizations(project_ids)
- project_authorizations.where(id: project_ids).delete_all
+ project_authorizations.where(project_id: project_ids).delete_all
end
def set_authorized_projects_column
@@ -893,6 +904,21 @@ class User < ActiveRecord::Base
end
end
+ def access_level
+ if admin?
+ :admin
+ else
+ :regular
+ end
+ end
+
+ def access_level=(new_level)
+ new_level = new_level.to_s
+ return unless %w(admin regular).include?(new_level)
+
+ self.admin = (new_level == 'admin')
+ end
+
private
def ci_projects_union
diff --git a/app/policies/base_policy.rb b/app/policies/base_policy.rb
index 118c100ca11..b9f1c29c32e 100644
--- a/app/policies/base_policy.rb
+++ b/app/policies/base_policy.rb
@@ -53,6 +53,10 @@ class BasePolicy
def self.class_for(subject)
return GlobalPolicy if subject.nil?
+ if subject.class.try(:presenter?)
+ subject = subject.subject
+ end
+
subject.class.ancestors.each do |klass|
next unless klass.name
diff --git a/app/policies/ci/build_policy.rb b/app/policies/ci/build_policy.rb
index 7b1752df0e1..8b25332b73c 100644
--- a/app/policies/ci/build_policy.rb
+++ b/app/policies/ci/build_policy.rb
@@ -1,8 +1,6 @@
module Ci
class BuildPolicy < CommitStatusPolicy
def rules
- can! :read_build if @subject.project.public_builds?
-
super
# If we can't read build we should also not have that
diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb
index 6f943feb2a7..0be6e113655 100644
--- a/app/policies/group_policy.rb
+++ b/app/policies/group_policy.rb
@@ -4,7 +4,7 @@ class GroupPolicy < BasePolicy
return unless @user
globally_viewable = @subject.public? || (@subject.internal? && !@user.external?)
- member = @subject.users.include?(@user)
+ member = @subject.users_with_parents.include?(@user)
owner = @user.admin? || @subject.has_owner?(@user)
master = owner || @subject.has_master?(@user)
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index b5db9c12622..f8594e29547 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -110,6 +110,9 @@ class ProjectPolicy < BasePolicy
can! :admin_pipeline
can! :admin_environment
can! :admin_deployment
+ can! :admin_pages
+ can! :read_pages
+ can! :update_pages
end
def public_access!
@@ -136,6 +139,7 @@ class ProjectPolicy < BasePolicy
can! :remove_fork_project
can! :destroy_merge_request
can! :destroy_issue
+ can! :remove_pages
end
def team_member_owner_access!
@@ -171,9 +175,7 @@ class ProjectPolicy < BasePolicy
def disabled_features!
repository_enabled = project.feature_available?(:repository, user)
- unless project.feature_available?(:issues, user)
- cannot!(*named_abilities(:issue))
- end
+ block_issues_abilities
unless project.feature_available?(:merge_requests, user) && repository_enabled
cannot!(*named_abilities(:merge_request))
@@ -216,25 +218,7 @@ class ProjectPolicy < BasePolicy
def anonymous_rules
return unless project.public?
- can! :read_project
- can! :read_board
- can! :read_list
- can! :read_wiki
- can! :read_label
- can! :read_milestone
- can! :read_project_snippet
- can! :read_project_member
- can! :read_merge_request
- can! :read_note
- can! :read_pipeline
- can! :read_commit_status
- can! :read_container_image
- can! :download_code
- can! :download_wiki_code
- can! :read_cycle_analytics
-
- # NOTE: may be overridden by IssuePolicy
- can! :read_issue
+ base_readonly_access!
# Allow to read builds by anonymous user if guests are allowed
can! :read_build if project.public_builds?
@@ -245,11 +229,20 @@ class ProjectPolicy < BasePolicy
def project_group_member?(user)
project.group &&
(
- project.group.members.exists?(user_id: user.id) ||
+ project.group.members_with_parents.exists?(user_id: user.id) ||
project.group.requesters.exists?(user_id: user.id)
)
end
+ def block_issues_abilities
+ unless project.feature_available?(:issues, user)
+ cannot! :read_issue if project.default_issues_tracker?
+ cannot! :create_issue
+ cannot! :update_issue
+ cannot! :admin_issue
+ end
+ end
+
def named_abilities(name)
[
:"read_#{name}",
@@ -258,4 +251,31 @@ class ProjectPolicy < BasePolicy
:"admin_#{name}"
]
end
+
+ private
+
+ # A base set of abilities for read-only users, which
+ # is then augmented as necessary for anonymous and other
+ # read-only users.
+ def base_readonly_access!
+ can! :read_project
+ can! :read_board
+ can! :read_list
+ can! :read_wiki
+ can! :read_label
+ can! :read_milestone
+ can! :read_project_snippet
+ can! :read_project_member
+ can! :read_merge_request
+ can! :read_note
+ can! :read_pipeline
+ can! :read_commit_status
+ can! :read_container_image
+ can! :download_code
+ can! :download_wiki_code
+ can! :read_cycle_analytics
+
+ # NOTE: may be overridden by IssuePolicy
+ can! :read_issue
+ end
end
diff --git a/app/policies/project_snippet_policy.rb b/app/policies/project_snippet_policy.rb
index 57acccfafd9..3a96836917e 100644
--- a/app/policies/project_snippet_policy.rb
+++ b/app/policies/project_snippet_policy.rb
@@ -3,7 +3,7 @@ class ProjectSnippetPolicy < BasePolicy
can! :read_project_snippet if @subject.public?
return unless @user
- if @user && @subject.author == @user || @user.admin?
+ if @user && (@subject.author == @user || @user.admin?)
can! :read_project_snippet
can! :update_project_snippet
can! :admin_project_snippet
diff --git a/app/presenters/README.md b/app/presenters/README.md
new file mode 100644
index 00000000000..a4d592b54d6
--- /dev/null
+++ b/app/presenters/README.md
@@ -0,0 +1,154 @@
+# Presenters
+
+This type of class is responsible for giving the view an object which defines
+**view-related logic/data methods**. It is usually useful to extract such
+methods from models to presenters.
+
+## When to use a presenter?
+
+### When your view is full of logic
+
+When your view is full of logic (`if`, `else`, `select` on arrays etc.), it's
+time to create a presenter!
+
+### When your model has a lot of view-related logic/data methods
+
+When your model has a lot of view-related logic/data methods, you can easily
+move them to a presenter.
+
+## Why are we using presenters instead of helpers?
+
+We don't use presenters to generate complex view output that would rely on helpers.
+
+Presenters should be used for:
+
+- Data and logic methods that can be pulled & combined into single methods from
+ view. This can include loops extracted from views too. A good example is
+ https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7073/diffs.
+- Data and logic methods that can be pulled from models.
+- Simple text output methods: it's ok if the method returns a string, but not a
+ whole DOM element for which we'd need HAML, a view context, helpers etc.
+
+## Why use presenters instead of model concerns?
+
+We should strive to follow the single-responsibility principle, and view-related
+logic/data methods are definitely not the responsibility of models!
+
+Another reason is as follows:
+
+> Avoid using concerns and use presenters instead. Why? After all, concerns seem
+to be a core part of Rails and can DRY up code when shared among multiple models.
+Nonetheless, the main issue is that concerns don’t make the model object more
+cohesive. The code is just better organized. In other words, there’s no real
+change to the API of the model.
+
+– https://www.toptal.com/ruby-on-rails/decoupling-rails-components
+
+## Benefits
+
+By moving pure view-related logic/data methods from models & views to presenters,
+we gain the following benefits:
+
+- rules are more explicit and centralized in the presenter => improves security
+- testing is easier and faster as presenters are Plain Old Ruby Object (PORO)
+- views are more readable and maintainable
+- decreases number of CE -> EE merge conflicts since code is in separate files
+- moves the conflicts from views (not always obvious) to presenters (a lot easier to resolve)
+
+## What not to do with presenters?
+
+- Don't use helpers in presenters. Presenters are not aware of the view context.
+- Don't generate complex DOM elements, forms etc. with presenters. Presenters
+ can return simple data as texts, and URLs using URL helpers from
+ `Gitlab::Routing` but nothing much more fancy.
+
+## Implementation
+
+### Presenter definition
+
+Every presenter should inherit from `Gitlab::View::Presenter::Simple`, which
+provides a `.presents` method which allows you to define an accessor for the
+presented object. It also includes common helpers like `Gitlab::Routing` and
+`Gitlab::Allowable`.
+
+```ruby
+class LabelPresenter < Gitlab::View::Presenter::Simple
+ presents :label
+
+ def text_color
+ label.color.to_s
+ end
+
+ def to_partial_path
+ 'projects/labels/show'
+ end
+end
+```
+
+In some cases, it can be more practical to transparently delegate all missing
+method calls to the presented object, in these cases, you can make your
+presenter inherit from `Gitlab::View::Presenter::Delegated`:
+
+```ruby
+class LabelPresenter < Gitlab::View::Presenter::Delegated
+ presents :label
+
+ def text_color
+ # color is delegated to label
+ color.to_s
+ end
+
+ def to_partial_path
+ 'projects/labels/show'
+ end
+end
+```
+
+### Presenter instantiation
+
+Instantiation must be done via the `Gitlab::View::Presenter::Factory` class which
+detects the presenter based on the presented subject's class.
+
+```ruby
+class Projects::LabelsController < Projects::ApplicationController
+ def edit
+ @label = Gitlab::View::Presenter::Factory
+ .new(@label, current_user: current_user)
+ .fabricate!
+ end
+end
+```
+
+You can also include the `Presentable` concern in the model:
+
+```ruby
+class Label
+ include Presentable
+end
+```
+
+and then in the controller:
+
+```ruby
+class Projects::LabelsController < Projects::ApplicationController
+ def edit
+ @label = @label.present(current_user: current_user)
+ end
+end
+```
+
+### Presenter usage
+
+```ruby
+%div{ class: @label.text_color }
+ = render partial: @label, label: @label
+```
+
+You can also present the model in the view:
+
+```ruby
+- label = @label.present(current_user: current_user)
+
+%div{ class: label.text_color }
+ = render partial: label, label: label
+```
diff --git a/app/presenters/ci/build_presenter.rb b/app/presenters/ci/build_presenter.rb
new file mode 100644
index 00000000000..ed72ed14d72
--- /dev/null
+++ b/app/presenters/ci/build_presenter.rb
@@ -0,0 +1,15 @@
+module Ci
+ class BuildPresenter < Gitlab::View::Presenter::Delegated
+ presents :build
+
+ def erased_by_user?
+ # Build can be erased through API, therefore it does not have
+ # `erased_by` user assigned in that case.
+ erased? && erased_by
+ end
+
+ def erased_by_name
+ erased_by.name if erased_by_user?
+ end
+ end
+end
diff --git a/app/serializers/analytics_stage_entity.rb b/app/serializers/analytics_stage_entity.rb
new file mode 100644
index 00000000000..a559d0850c4
--- /dev/null
+++ b/app/serializers/analytics_stage_entity.rb
@@ -0,0 +1,10 @@
+class AnalyticsStageEntity < Grape::Entity
+ include EntityDateHelper
+
+ expose :title
+ expose :description
+
+ expose :median, as: :value do |stage|
+ stage.median && !stage.median.zero? ? distance_of_time_in_words(stage.median) : nil
+ end
+end
diff --git a/app/serializers/analytics_stage_serializer.rb b/app/serializers/analytics_stage_serializer.rb
new file mode 100644
index 00000000000..613cf6874d8
--- /dev/null
+++ b/app/serializers/analytics_stage_serializer.rb
@@ -0,0 +1,3 @@
+class AnalyticsStageSerializer < BaseSerializer
+ entity AnalyticsStageEntity
+end
diff --git a/app/serializers/analytics_summary_entity.rb b/app/serializers/analytics_summary_entity.rb
new file mode 100644
index 00000000000..91803ec07f5
--- /dev/null
+++ b/app/serializers/analytics_summary_entity.rb
@@ -0,0 +1,7 @@
+class AnalyticsSummaryEntity < Grape::Entity
+ expose :value, safe: true
+
+ expose :title do |object|
+ object.title.pluralize(object.value)
+ end
+end
diff --git a/app/serializers/analytics_summary_serializer.rb b/app/serializers/analytics_summary_serializer.rb
new file mode 100644
index 00000000000..c87a24aa47c
--- /dev/null
+++ b/app/serializers/analytics_summary_serializer.rb
@@ -0,0 +1,3 @@
+class AnalyticsSummarySerializer < BaseSerializer
+ entity AnalyticsSummaryEntity
+end
diff --git a/app/serializers/base_serializer.rb b/app/serializers/base_serializer.rb
index de9a181db90..311ee9c96be 100644
--- a/app/serializers/base_serializer.rb
+++ b/app/serializers/base_serializer.rb
@@ -6,6 +6,7 @@ class BaseSerializer
def represent(resource, opts = {})
self.class.entity_class
.represent(resource, opts.merge(request: @request))
+ .as_json
end
def self.entity(entity_class)
diff --git a/app/serializers/build_action_entity.rb b/app/serializers/build_action_entity.rb
new file mode 100644
index 00000000000..184f5fd4b52
--- /dev/null
+++ b/app/serializers/build_action_entity.rb
@@ -0,0 +1,14 @@
+class BuildActionEntity < Grape::Entity
+ include RequestAwareEntity
+
+ expose :name do |build|
+ build.name
+ end
+
+ expose :path do |build|
+ play_namespace_project_build_path(
+ build.project.namespace,
+ build.project,
+ build)
+ end
+end
diff --git a/app/serializers/build_artifact_entity.rb b/app/serializers/build_artifact_entity.rb
new file mode 100644
index 00000000000..8b643d8e783
--- /dev/null
+++ b/app/serializers/build_artifact_entity.rb
@@ -0,0 +1,14 @@
+class BuildArtifactEntity < Grape::Entity
+ include RequestAwareEntity
+
+ expose :name do |build|
+ build.name
+ end
+
+ expose :path do |build|
+ download_namespace_project_build_artifacts_path(
+ build.project.namespace,
+ build.project,
+ build)
+ end
+end
diff --git a/app/serializers/commit_entity.rb b/app/serializers/commit_entity.rb
index acc20f6dc52..31763955f97 100644
--- a/app/serializers/commit_entity.rb
+++ b/app/serializers/commit_entity.rb
@@ -3,17 +3,21 @@ class CommitEntity < API::Entities::RepoCommit
expose :author, using: UserEntity
+ expose :author_gravatar_url do |commit|
+ GravatarService.new.execute(commit.author_email)
+ end
+
expose :commit_url do |commit|
- namespace_project_tree_url(
+ namespace_project_commit_url(
request.project.namespace,
request.project,
- id: commit.id)
+ commit)
end
expose :commit_path do |commit|
- namespace_project_tree_path(
+ namespace_project_commit_path(
request.project.namespace,
request.project,
- id: commit.id)
+ commit)
end
end
diff --git a/app/serializers/environment_entity.rb b/app/serializers/environment_entity.rb
index 5d15eb8d3d3..4c017960628 100644
--- a/app/serializers/environment_entity.rb
+++ b/app/serializers/environment_entity.rb
@@ -7,7 +7,7 @@ class EnvironmentEntity < Grape::Entity
expose :external_url
expose :environment_type
expose :last_deployment, using: DeploymentEntity
- expose :stoppable?
+ expose :stop_action?
expose :environment_path do |environment|
namespace_project_environment_path(
diff --git a/app/serializers/environment_serializer.rb b/app/serializers/environment_serializer.rb
index 91955542f25..fe16a3784c4 100644
--- a/app/serializers/environment_serializer.rb
+++ b/app/serializers/environment_serializer.rb
@@ -1,3 +1,50 @@
class EnvironmentSerializer < BaseSerializer
+ Item = Struct.new(:name, :size, :latest)
+
entity EnvironmentEntity
+
+ def within_folders
+ tap { @itemize = true }
+ end
+
+ def with_pagination(request, response)
+ tap { @paginator = Gitlab::Serializer::Pagination.new(request, response) }
+ end
+
+ def itemized?
+ @itemize
+ end
+
+ def paginated?
+ @paginator.present?
+ end
+
+ def represent(resource, opts = {})
+ resource = @paginator.paginate(resource) if paginated?
+
+ if itemized?
+ itemize(resource).map do |item|
+ { name: item.name,
+ size: item.size,
+ latest: super(item.latest, opts) }
+ end
+ else
+ super(resource, opts)
+ end
+ end
+
+ private
+
+ def itemize(resource)
+ items = resource.group(:item_name).order('item_name ASC')
+ .pluck('COALESCE(environment_type, name) AS item_name',
+ 'COUNT(*) AS environments_count',
+ 'MAX(id) AS last_environment_id')
+
+ environments = resource.where(id: items.map(&:last)).index_by(&:id)
+
+ items.map do |name, size, id|
+ Item.new(name, size, environments[id])
+ end
+ end
end
diff --git a/app/serializers/issuable_entity.rb b/app/serializers/issuable_entity.rb
index 17c9160cb19..29aecb50849 100644
--- a/app/serializers/issuable_entity.rb
+++ b/app/serializers/issuable_entity.rb
@@ -13,4 +13,8 @@ class IssuableEntity < Grape::Entity
expose :created_at
expose :updated_at
expose :deleted_at
+ expose :time_estimate
+ expose :total_time_spent
+ expose :human_time_estimate
+ expose :human_total_time_spent
end
diff --git a/app/serializers/pipeline_entity.rb b/app/serializers/pipeline_entity.rb
new file mode 100644
index 00000000000..61f0f11d7d2
--- /dev/null
+++ b/app/serializers/pipeline_entity.rb
@@ -0,0 +1,85 @@
+class PipelineEntity < Grape::Entity
+ include RequestAwareEntity
+
+ expose :id
+ expose :user, using: UserEntity
+
+ expose :path do |pipeline|
+ namespace_project_pipeline_path(
+ pipeline.project.namespace,
+ pipeline.project,
+ pipeline)
+ end
+
+ expose :details do
+ expose :status do |pipeline, options|
+ StatusEntity.represent(
+ pipeline.detailed_status(request.user),
+ options)
+ end
+
+ expose :duration
+ expose :finished_at
+ expose :stages, using: StageEntity
+ expose :artifacts, using: BuildArtifactEntity
+ expose :manual_actions, using: BuildActionEntity
+ end
+
+ expose :flags do
+ expose :latest?, as: :latest
+ expose :triggered?, as: :triggered
+ expose :stuck?, as: :stuck
+ expose :has_yaml_errors?, as: :yaml_errors
+ expose :can_retry?, as: :retryable
+ expose :can_cancel?, as: :cancelable
+ end
+
+ expose :ref do
+ expose :name do |pipeline|
+ pipeline.ref
+ end
+
+ expose :path do |pipeline|
+ if pipeline.ref
+ namespace_project_tree_path(
+ pipeline.project.namespace,
+ pipeline.project,
+ id: pipeline.ref)
+ end
+ end
+
+ expose :tag?, as: :tag
+ expose :branch?, as: :branch
+ end
+
+ expose :commit, using: CommitEntity
+ expose :yaml_errors, if: ->(pipeline, _) { pipeline.has_yaml_errors? }
+
+ expose :retry_path, if: proc { can_retry? } do |pipeline|
+ retry_namespace_project_pipeline_path(pipeline.project.namespace,
+ pipeline.project,
+ pipeline.id)
+ end
+
+ expose :cancel_path, if: proc { can_cancel? } do |pipeline|
+ cancel_namespace_project_pipeline_path(pipeline.project.namespace,
+ pipeline.project,
+ pipeline.id)
+ end
+
+ expose :created_at, :updated_at
+
+ private
+
+ alias_method :pipeline, :object
+
+ def can_retry?
+ pipeline.retryable? &&
+ can?(request.user, :update_pipeline, pipeline)
+ end
+
+ def can_cancel?
+ pipeline.cancelable? &&
+ can?(request.user, :update_pipeline, pipeline)
+ end
+end
diff --git a/app/serializers/pipeline_serializer.rb b/app/serializers/pipeline_serializer.rb
new file mode 100644
index 00000000000..2bc6cf3266e
--- /dev/null
+++ b/app/serializers/pipeline_serializer.rb
@@ -0,0 +1,25 @@
+class PipelineSerializer < BaseSerializer
+ class InvalidResourceError < StandardError; end
+
+ entity PipelineEntity
+
+ def with_pagination(request, response)
+ tap { @paginator = Gitlab::Serializer::Pagination.new(request, response) }
+ end
+
+ def paginated?
+ @paginator.present?
+ end
+
+ def represent(resource, opts = {})
+ if resource.is_a?(ActiveRecord::Relation)
+ resource = resource.includes(project: :namespace)
+ end
+
+ if paginated?
+ super(@paginator.paginate(resource), opts)
+ else
+ super(resource, opts)
+ end
+ end
+end
diff --git a/app/serializers/request_aware_entity.rb b/app/serializers/request_aware_entity.rb
index e159d750cb7..3039014aaaa 100644
--- a/app/serializers/request_aware_entity.rb
+++ b/app/serializers/request_aware_entity.rb
@@ -2,14 +2,11 @@ module RequestAwareEntity
extend ActiveSupport::Concern
included do
- include Gitlab::Routing.url_helpers
+ include Gitlab::Routing
+ include Gitlab::Allowable
end
def request
- @options.fetch(:request)
- end
-
- def can?(object, action, subject)
- Ability.allowed?(object, action, subject)
+ options.fetch(:request)
end
end
diff --git a/app/serializers/stage_entity.rb b/app/serializers/stage_entity.rb
new file mode 100644
index 00000000000..7a047bdc712
--- /dev/null
+++ b/app/serializers/stage_entity.rb
@@ -0,0 +1,38 @@
+class StageEntity < Grape::Entity
+ include RequestAwareEntity
+
+ expose :name
+
+ expose :title do |stage|
+ "#{stage.name}: #{detailed_status.label}"
+ end
+
+ expose :detailed_status,
+ as: :status,
+ with: StatusEntity
+
+ expose :path do |stage|
+ namespace_project_pipeline_path(
+ stage.pipeline.project.namespace,
+ stage.pipeline.project,
+ stage.pipeline,
+ anchor: stage.name)
+ end
+
+ expose :dropdown_path do |stage|
+ stage_namespace_project_pipeline_path(
+ stage.pipeline.project.namespace,
+ stage.pipeline.project,
+ stage.pipeline,
+ stage: stage.name,
+ format: :json)
+ end
+
+ private
+
+ alias_method :stage, :object
+
+ def detailed_status
+ stage.detailed_status(request.user)
+ end
+end
diff --git a/app/serializers/status_entity.rb b/app/serializers/status_entity.rb
new file mode 100644
index 00000000000..47066bebfb1
--- /dev/null
+++ b/app/serializers/status_entity.rb
@@ -0,0 +1,8 @@
+class StatusEntity < Grape::Entity
+ include RequestAwareEntity
+
+ expose :icon, :text, :label, :group
+
+ expose :has_details?, as: :has_details
+ expose :details_path
+end
diff --git a/app/services/application_settings/base_service.rb b/app/services/application_settings/base_service.rb
new file mode 100644
index 00000000000..2bcc7d7c08b
--- /dev/null
+++ b/app/services/application_settings/base_service.rb
@@ -0,0 +1,7 @@
+module ApplicationSettings
+ class BaseService < ::BaseService
+ def initialize(application_setting, user, params = {})
+ @application_setting, @current_user, @params = application_setting, user, params.dup
+ end
+ end
+end
diff --git a/app/services/application_settings/update_service.rb b/app/services/application_settings/update_service.rb
new file mode 100644
index 00000000000..61589a07250
--- /dev/null
+++ b/app/services/application_settings/update_service.rb
@@ -0,0 +1,7 @@
+module ApplicationSettings
+ class UpdateService < ApplicationSettings::BaseService
+ def execute
+ @application_setting.update(@params)
+ end
+ end
+end
diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb
index c00c5aebf57..5cb7a86a5ee 100644
--- a/app/services/auth/container_registry_authentication_service.rb
+++ b/app/services/auth/container_registry_authentication_service.rb
@@ -61,7 +61,7 @@ module Auth
end
def process_repository_access(type, name, actions)
- requested_project = Project.find_with_namespace(name)
+ requested_project = Project.find_by_full_path(name)
return unless requested_project
actions = actions.select do |action|
diff --git a/app/services/boards/create_service.rb b/app/services/boards/create_service.rb
index 9bdd7b6f0cf..f6275a63109 100644
--- a/app/services/boards/create_service.rb
+++ b/app/services/boards/create_service.rb
@@ -12,7 +12,6 @@ module Boards
def create_board!
board = project.boards.create
- board.lists.create(list_type: :backlog)
board.lists.create(list_type: :done)
board
diff --git a/app/services/boards/issues/list_service.rb b/app/services/boards/issues/list_service.rb
index fd4a462c7b2..8a94c54b6ab 100644
--- a/app/services/boards/issues/list_service.rb
+++ b/app/services/boards/issues/list_service.rb
@@ -3,8 +3,8 @@ module Boards
class ListService < BaseService
def execute
issues = IssuesFinder.new(current_user, filter_params).execute
- issues = without_board_labels(issues) unless list.movable?
- issues = with_list_label(issues) if list.movable?
+ issues = without_board_labels(issues) unless movable_list?
+ issues = with_list_label(issues) if movable_list?
issues
end
@@ -15,7 +15,13 @@ module Boards
end
def list
- @list ||= board.lists.find(params[:id])
+ return @list if defined?(@list)
+
+ @list = board.lists.find(params[:id]) if params.key?(:id)
+ end
+
+ def movable_list?
+ @movable_list ||= list.present? && list.movable?
end
def filter_params
@@ -40,7 +46,7 @@ module Boards
end
def set_state
- params[:state] = list.done? ? 'closed' : 'opened'
+ params[:state] = list && list.done? ? 'closed' : 'opened'
end
def board_label_ids
diff --git a/app/services/ci/register_build_service.rb b/app/services/ci/register_build_service.rb
index 74b5ebf372b..6f03bf2be13 100644
--- a/app/services/ci/register_build_service.rb
+++ b/app/services/ci/register_build_service.rb
@@ -2,48 +2,72 @@ module Ci
# This class responsible for assigning
# proper pending build to runner on runner API request
class RegisterBuildService
- def execute(current_runner)
- builds = Ci::Build.pending.unstarted
+ include Gitlab::CurrentSettings
+ attr_reader :runner
+
+ Result = Struct.new(:build, :valid?)
+
+ def initialize(runner)
+ @runner = runner
+ end
+
+ def execute
builds =
- if current_runner.shared?
- builds.
- # don't run projects which have not enabled shared runners and builds
- joins(:project).where(projects: { shared_runners_enabled: true }).
- joins('LEFT JOIN project_features ON ci_builds.gl_project_id = project_features.project_id').
-
- # this returns builds that are ordered by number of running builds
- # we prefer projects that don't use shared runners at all
- joins("LEFT JOIN (#{running_builds_for_shared_runners.to_sql}) AS project_builds ON ci_builds.gl_project_id=project_builds.gl_project_id").
- where('project_features.builds_access_level IS NULL or project_features.builds_access_level > 0').
- order('COALESCE(project_builds.running_builds, 0) ASC', 'ci_builds.id ASC')
+ if runner.shared?
+ builds_for_shared_runner
else
- # do run projects which are only assigned to this runner (FIFO)
- builds.where(project: current_runner.projects.with_builds_enabled).order('created_at ASC')
+ builds_for_specific_runner
end
build = builds.find do |build|
- current_runner.can_pick?(build)
+ runner.can_pick?(build)
end
if build
# In case when 2 runners try to assign the same build, second runner will be declined
# with StateMachines::InvalidTransition or StaleObjectError when doing run! or save method.
- build.runner_id = current_runner.id
+ build.runner_id = runner.id
build.run!
end
- build
+ Result.new(build, true)
rescue StateMachines::InvalidTransition, ActiveRecord::StaleObjectError
- nil
+ Result.new(build, false)
end
private
+ def builds_for_shared_runner
+ new_builds.
+ # don't run projects which have not enabled shared runners and builds
+ joins(:project).where(projects: { shared_runners_enabled: true }).
+ joins('LEFT JOIN project_features ON ci_builds.gl_project_id = project_features.project_id').
+ where('project_features.builds_access_level IS NULL or project_features.builds_access_level > 0').
+
+ # Implement fair scheduling
+ # this returns builds that are ordered by number of running builds
+ # we prefer projects that don't use shared runners at all
+ joins("LEFT JOIN (#{running_builds_for_shared_runners.to_sql}) AS project_builds ON ci_builds.gl_project_id=project_builds.gl_project_id").
+ order('COALESCE(project_builds.running_builds, 0) ASC', 'ci_builds.id ASC')
+ end
+
+ def builds_for_specific_runner
+ new_builds.where(project: runner.projects.with_builds_enabled).order('created_at ASC')
+ end
+
def running_builds_for_shared_runners
Ci::Build.running.where(runner: Ci::Runner.shared).
group(:gl_project_id).select(:gl_project_id, 'count(*) AS running_builds')
end
+
+ def new_builds
+ Ci::Build.pending.unstarted
+ end
+
+ def shared_runner_build_limits_feature_enabled?
+ ENV['DISABLE_SHARED_RUNNER_BUILD_MINUTES_LIMIT'].to_s != 'true'
+ end
end
end
diff --git a/app/services/ci/stop_environments_service.rb b/app/services/ci/stop_environments_service.rb
index cf590459cb2..42c72aba7dd 100644
--- a/app/services/ci/stop_environments_service.rb
+++ b/app/services/ci/stop_environments_service.rb
@@ -8,10 +8,9 @@ module Ci
return unless has_ref?
environments.each do |environment|
- next unless environment.stoppable?
next unless can?(current_user, :create_deployment, project)
- environment.stop!(current_user)
+ environment.stop_with_action!(current_user)
end
end
@@ -22,8 +21,8 @@ module Ci
end
def environments
- @environments ||= project
- .environments_recently_updated_on_branch(@ref)
+ @environments ||=
+ EnvironmentsFinder.new(project, current_user, ref: @ref, recently_updated: true).execute
end
end
end
diff --git a/app/services/ci/update_build_queue_service.rb b/app/services/ci/update_build_queue_service.rb
new file mode 100644
index 00000000000..152c8ae5006
--- /dev/null
+++ b/app/services/ci/update_build_queue_service.rb
@@ -0,0 +1,19 @@
+module Ci
+ class UpdateBuildQueueService
+ def execute(build)
+ build.project.runners.each do |runner|
+ if runner.can_pick?(build)
+ runner.tick_runner_queue
+ end
+ end
+
+ return unless build.project.shared_runners_enabled?
+
+ Ci::Runner.shared.each do |runner|
+ if runner.can_pick?(build)
+ runner.tick_runner_queue
+ end
+ end
+ end
+ end
+end
diff --git a/app/services/commits/change_service.rb b/app/services/commits/change_service.rb
index 4d410f66c55..25e22f14e60 100644
--- a/app/services/commits/change_service.rb
+++ b/app/services/commits/change_service.rb
@@ -4,7 +4,8 @@ module Commits
class ChangeError < StandardError; end
def execute
- @source_project = params[:source_project] || @project
+ @start_project = params[:start_project] || @project
+ @start_branch = params[:start_branch]
@target_branch = params[:target_branch]
@commit = params[:commit]
@create_merge_request = params[:create_merge_request].present?
@@ -25,13 +26,28 @@ module Commits
def commit_change(action)
raise NotImplementedError unless repository.respond_to?(action)
- into = @create_merge_request ? @commit.public_send("#{action}_branch_name") : @target_branch
- tree_id = repository.public_send("check_#{action}_content", @commit, @target_branch)
+ if @create_merge_request
+ into = @commit.public_send("#{action}_branch_name")
+ tree_branch = @start_branch
+ else
+ into = tree_branch = @target_branch
+ end
+
+ tree_id = repository.public_send(
+ "check_#{action}_content", @commit, tree_branch)
if tree_id
- create_target_branch(into) if @create_merge_request
+ validate_target_branch(into) if @create_merge_request
+
+ repository.public_send(
+ action,
+ current_user,
+ @commit,
+ into,
+ tree_id,
+ start_project: @start_project,
+ start_branch_name: @start_branch)
- repository.public_send(action, current_user, @commit, into, tree_id)
success
else
error_msg = "Sorry, we cannot #{action.to_s.dasherize} this #{@commit.change_type_title(current_user)} automatically.
@@ -50,12 +66,12 @@ module Commits
true
end
- def create_target_branch(new_branch)
+ def validate_target_branch(new_branch)
# Temporary branch exists and contains the change commit
- return success if repository.find_branch(new_branch)
+ return if repository.find_branch(new_branch)
- result = CreateBranchService.new(@project, current_user)
- .execute(new_branch, @target_branch, source_project: @source_project)
+ result = ValidateNewBranchService.new(@project, current_user)
+ .execute(new_branch)
if result[:status] == :error
raise ChangeError, "There was an error creating the source branch: #{result[:message]}"
diff --git a/app/services/compare_service.rb b/app/services/compare_service.rb
index 5e8fafca98c..ab4c02a97a0 100644
--- a/app/services/compare_service.rb
+++ b/app/services/compare_service.rb
@@ -3,23 +3,27 @@ require 'securerandom'
# Compare 2 branches for one repo or between repositories
# and return Gitlab::Git::Compare object that responds to commits and diffs
class CompareService
- def execute(source_project, source_branch, target_project, target_branch, straight: false)
- source_commit = source_project.commit(source_branch)
- return unless source_commit
+ attr_reader :start_project, :start_branch_name
- source_sha = source_commit.sha
+ def initialize(new_start_project, new_start_branch_name)
+ @start_project = new_start_project
+ @start_branch_name = new_start_branch_name
+ end
+ def execute(target_project, target_branch, straight: false)
# If compare with other project we need to fetch ref first
- unless target_project == source_project
- random_string = SecureRandom.hex
+ target_project.repository.with_repo_branch_commit(
+ start_project.repository,
+ start_branch_name) do |commit|
+ break unless commit
- target_project.repository.fetch_ref(
- source_project.repository.path_to_repo,
- "refs/heads/#{source_branch}",
- "refs/tmp/#{random_string}/head"
- )
+ compare(commit.sha, target_project, target_branch, straight)
end
+ end
+
+ private
+ def compare(source_sha, target_project, target_branch, straight)
raw_compare = Gitlab::Git::Compare.new(
target_project.repository.raw_repository,
target_branch,
diff --git a/app/services/create_branch_service.rb b/app/services/create_branch_service.rb
index e004a303496..77459d8779d 100644
--- a/app/services/create_branch_service.rb
+++ b/app/services/create_branch_service.rb
@@ -1,31 +1,11 @@
class CreateBranchService < BaseService
- def execute(branch_name, ref, source_project: @project)
- valid_branch = Gitlab::GitRefValidator.validate(branch_name)
+ def execute(branch_name, ref)
+ result = ValidateNewBranchService.new(project, current_user)
+ .execute(branch_name)
- unless valid_branch
- return error('Branch name is invalid')
- end
-
- repository = project.repository
- existing_branch = repository.find_branch(branch_name)
-
- if existing_branch
- return error('Branch already exists')
- end
-
- new_branch = if source_project != @project
- repository.fetch_ref(
- source_project.repository.path_to_repo,
- "refs/heads/#{ref}",
- "refs/heads/#{branch_name}"
- )
-
- repository.after_create_branch
+ return result if result[:status] == :error
- repository.find_branch(branch_name)
- else
- repository.add_branch(current_user, branch_name, ref)
- end
+ new_branch = repository.add_branch(current_user, branch_name, ref)
if new_branch
success(new_branch)
diff --git a/app/services/create_snippet_service.rb b/app/services/create_snippet_service.rb
index 95cc9baf406..14f5ba064ff 100644
--- a/app/services/create_snippet_service.rb
+++ b/app/services/create_snippet_service.rb
@@ -1,5 +1,8 @@
class CreateSnippetService < BaseService
def execute
+ request = params.delete(:request)
+ api = params.delete(:api)
+
snippet = if project
project.snippets.build(params)
else
@@ -12,8 +15,12 @@ class CreateSnippetService < BaseService
end
snippet.author = current_user
+ snippet.spam = SpamService.new(snippet, request).check(api)
+
+ if snippet.save
+ UserAgentDetailService.new(snippet, request).create
+ end
- snippet.save
snippet
end
end
diff --git a/app/services/delete_tag_service.rb b/app/services/delete_tag_service.rb
index a44dee14a0f..9d4bffb93e9 100644
--- a/app/services/delete_tag_service.rb
+++ b/app/services/delete_tag_service.rb
@@ -7,7 +7,7 @@ class DeleteTagService < BaseService
return error('No such tag', 404)
end
- if repository.rm_tag(tag_name)
+ if repository.rm_tag(current_user, tag_name)
release = project.releases.find_by(tag: tag_name)
release.destroy if release
diff --git a/app/services/files/base_service.rb b/app/services/files/base_service.rb
index 9bd4bd464f7..0a25f56d24c 100644
--- a/app/services/files/base_service.rb
+++ b/app/services/files/base_service.rb
@@ -3,9 +3,9 @@ module Files
class ValidationError < StandardError; end
def execute
- @source_project = params[:source_project] || @project
- @source_branch = params[:source_branch]
- @target_branch = params[:target_branch]
+ @start_project = params[:start_project] || @project
+ @start_branch = params[:start_branch]
+ @target_branch = params[:target_branch]
@commit_message = params[:commit_message]
@file_path = params[:file_path]
@@ -22,10 +22,8 @@ module Files
# Validate parameters
validate
- # Create new branch if it different from source_branch
- if different_branch?
- create_target_branch
- end
+ # Create new branch if it different from start_branch
+ validate_target_branch if different_branch?
result = commit
if result
@@ -40,7 +38,7 @@ module Files
private
def different_branch?
- @source_branch != @target_branch || @source_project != @project
+ @start_branch != @target_branch || @start_project != @project
end
def file_has_changed?
@@ -61,22 +59,23 @@ module Files
end
unless project.empty_repo?
- unless @source_project.repository.branch_names.include?(@source_branch)
+ unless @start_project.repository.branch_exists?(@start_branch)
raise_error('You can only create or edit files when you are on a branch')
end
if different_branch?
- if repository.branch_names.include?(@target_branch)
+ if repository.branch_exists?(@target_branch)
raise_error('Branch with such name already exists. You need to switch to this branch in order to make changes')
end
end
end
end
- def create_target_branch
- result = CreateBranchService.new(project, current_user).execute(@target_branch, @source_branch, source_project: @source_project)
+ def validate_target_branch
+ result = ValidateNewBranchService.new(project, current_user).
+ execute(@target_branch)
- unless result[:status] == :success
+ if result[:status] == :error
raise_error("Something went wrong when we tried to create #{@target_branch} for you: #{result[:message]}")
end
end
diff --git a/app/services/files/create_dir_service.rb b/app/services/files/create_dir_service.rb
index e5b4d60e467..858de5f0538 100644
--- a/app/services/files/create_dir_service.rb
+++ b/app/services/files/create_dir_service.rb
@@ -1,7 +1,15 @@
module Files
class CreateDirService < Files::BaseService
def commit
- repository.commit_dir(current_user, @file_path, @commit_message, @target_branch, author_email: @author_email, author_name: @author_name)
+ repository.commit_dir(
+ current_user,
+ @file_path,
+ message: @commit_message,
+ branch_name: @target_branch,
+ author_email: @author_email,
+ author_name: @author_name,
+ start_project: @start_project,
+ start_branch_name: @start_branch)
end
def validate
diff --git a/app/services/files/create_service.rb b/app/services/files/create_service.rb
index b23576b9a28..88dd7bbaedb 100644
--- a/app/services/files/create_service.rb
+++ b/app/services/files/create_service.rb
@@ -1,7 +1,17 @@
module Files
class CreateService < Files::BaseService
def commit
- repository.commit_file(current_user, @file_path, @file_content, @commit_message, @target_branch, false, author_email: @author_email, author_name: @author_name)
+ repository.commit_file(
+ current_user,
+ @file_path,
+ @file_content,
+ message: @commit_message,
+ branch_name: @target_branch,
+ update: false,
+ author_email: @author_email,
+ author_name: @author_name,
+ start_project: @start_project,
+ start_branch_name: @start_branch)
end
def validate
@@ -24,7 +34,7 @@ module Files
unless project.empty_repo?
@file_path.slice!(0) if @file_path.start_with?('/')
- blob = repository.blob_at_branch(@source_branch, @file_path)
+ blob = repository.blob_at_branch(@start_branch, @file_path)
if blob
raise_error('Your changes could not be committed because a file with the same name already exists')
diff --git a/app/services/files/delete_service.rb b/app/services/files/delete_service.rb
index 4f7e7a5baaa..50f0ffcac9f 100644
--- a/app/services/files/delete_service.rb
+++ b/app/services/files/delete_service.rb
@@ -1,7 +1,15 @@
module Files
class DeleteService < Files::BaseService
def commit
- repository.remove_file(current_user, @file_path, @commit_message, @target_branch, author_email: @author_email, author_name: @author_name)
+ repository.remove_file(
+ current_user,
+ @file_path,
+ message: @commit_message,
+ branch_name: @target_branch,
+ author_email: @author_email,
+ author_name: @author_name,
+ start_project: @start_project,
+ start_branch_name: @start_branch)
end
end
end
diff --git a/app/services/files/multi_service.rb b/app/services/files/multi_service.rb
index 54446e90007..6ba868df04d 100644
--- a/app/services/files/multi_service.rb
+++ b/app/services/files/multi_service.rb
@@ -5,11 +5,13 @@ module Files
def commit
repository.multi_action(
user: current_user,
- branch: @target_branch,
message: @commit_message,
+ branch_name: @target_branch,
actions: params[:actions],
author_email: @author_email,
- author_name: @author_name
+ author_name: @author_name,
+ start_project: @start_project,
+ start_branch_name: @start_branch
)
end
@@ -61,7 +63,7 @@ module Files
end
def last_commit
- Gitlab::Git::Commit.last_for_path(repository, @source_branch, @file_path)
+ Gitlab::Git::Commit.last_for_path(repository, @start_branch, @file_path)
end
def regex_check(file)
diff --git a/app/services/files/update_service.rb b/app/services/files/update_service.rb
index 47a18e3e132..a71fe61a4b6 100644
--- a/app/services/files/update_service.rb
+++ b/app/services/files/update_service.rb
@@ -4,11 +4,13 @@ module Files
def commit
repository.update_file(current_user, @file_path, @file_content,
- branch: @target_branch,
- previous_path: @previous_path,
message: @commit_message,
+ branch_name: @target_branch,
+ previous_path: @previous_path,
author_email: @author_email,
- author_name: @author_name)
+ author_name: @author_name,
+ start_project: @start_project,
+ start_branch_name: @start_branch)
end
private
@@ -23,7 +25,7 @@ module Files
def last_commit
@last_commit ||= Gitlab::Git::Commit.
- last_for_path(@source_project.repository, @source_branch, @file_path)
+ last_for_path(@start_project.repository, @start_branch, @file_path)
end
end
end
diff --git a/app/services/git_hooks_service.rb b/app/services/git_hooks_service.rb
index 6cd3908d43a..d222d1e63aa 100644
--- a/app/services/git_hooks_service.rb
+++ b/app/services/git_hooks_service.rb
@@ -18,9 +18,9 @@ class GitHooksService
end
end
- yield self
-
- run_hook('post-receive')
+ yield(self).tap do
+ run_hook('post-receive')
+ end
end
private
diff --git a/app/services/git_operation_service.rb b/app/services/git_operation_service.rb
new file mode 100644
index 00000000000..27bcc047601
--- /dev/null
+++ b/app/services/git_operation_service.rb
@@ -0,0 +1,179 @@
+class GitOperationService
+ attr_reader :user, :repository
+
+ def initialize(new_user, new_repository)
+ @user = new_user
+ @repository = new_repository
+ end
+
+ def add_branch(branch_name, newrev)
+ ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name
+ oldrev = Gitlab::Git::BLANK_SHA
+
+ update_ref_in_hooks(ref, newrev, oldrev)
+ end
+
+ def rm_branch(branch)
+ ref = Gitlab::Git::BRANCH_REF_PREFIX + branch.name
+ oldrev = branch.target
+ newrev = Gitlab::Git::BLANK_SHA
+
+ update_ref_in_hooks(ref, newrev, oldrev)
+ end
+
+ def add_tag(tag_name, newrev, options = {})
+ ref = Gitlab::Git::TAG_REF_PREFIX + tag_name
+ oldrev = Gitlab::Git::BLANK_SHA
+
+ with_hooks(ref, newrev, oldrev) do |service|
+ # We want to pass the OID of the tag object to the hooks. For an
+ # annotated tag we don't know that OID until after the tag object
+ # (raw_tag) is created in the repository. That is why we have to
+ # update the value after creating the tag object. Only the
+ # "post-receive" hook will receive the correct value in this case.
+ raw_tag = repository.rugged.tags.create(tag_name, newrev, options)
+ service.newrev = raw_tag.target_id
+ end
+ end
+
+ def rm_tag(tag)
+ ref = Gitlab::Git::TAG_REF_PREFIX + tag.name
+ oldrev = tag.target
+ newrev = Gitlab::Git::BLANK_SHA
+
+ update_ref_in_hooks(ref, newrev, oldrev) do
+ repository.rugged.tags.delete(tag_name)
+ end
+ end
+
+ # Whenever `start_branch_name` is passed, if `branch_name` doesn't exist,
+ # it would be created from `start_branch_name`.
+ # If `start_project` is passed, and the branch doesn't exist,
+ # it would try to find the commits from it instead of current repository.
+ def with_branch(
+ branch_name,
+ start_branch_name: nil,
+ start_project: repository.project,
+ &block)
+
+ check_with_branch_arguments!(
+ branch_name, start_branch_name, start_project)
+
+ update_branch_with_hooks(branch_name) do
+ repository.with_repo_branch_commit(
+ start_project.repository,
+ start_branch_name || branch_name,
+ &block)
+ end
+ end
+
+ private
+
+ def update_branch_with_hooks(branch_name)
+ update_autocrlf_option
+
+ was_empty = repository.empty?
+
+ # Make commit
+ newrev = yield
+
+ unless newrev
+ raise Repository::CommitError.new('Failed to create commit')
+ end
+
+ branch = repository.find_branch(branch_name)
+ oldrev = find_oldrev_from_branch(newrev, branch)
+
+ ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name
+ update_ref_in_hooks(ref, newrev, oldrev)
+
+ # If repo was empty expire cache
+ repository.after_create if was_empty
+ repository.after_create_branch if
+ was_empty || Gitlab::Git.blank_ref?(oldrev)
+
+ newrev
+ end
+
+ def find_oldrev_from_branch(newrev, branch)
+ return Gitlab::Git::BLANK_SHA unless branch
+
+ oldrev = branch.target
+
+ if oldrev == repository.rugged.merge_base(newrev, branch.target)
+ oldrev
+ else
+ raise Repository::CommitError.new('Branch diverged')
+ end
+ end
+
+ def update_ref_in_hooks(ref, newrev, oldrev)
+ with_hooks(ref, newrev, oldrev) do
+ update_ref(ref, newrev, oldrev)
+ end
+ end
+
+ def with_hooks(ref, newrev, oldrev)
+ GitHooksService.new.execute(
+ user,
+ repository.path_to_repo,
+ oldrev,
+ newrev,
+ ref) do |service|
+
+ yield(service)
+ end
+ end
+
+ def update_ref(ref, newrev, oldrev)
+ # We use 'git update-ref' because libgit2/rugged currently does not
+ # offer 'compare and swap' ref updates. Without compare-and-swap we can
+ # (and have!) accidentally reset the ref to an earlier state, clobbering
+ # commits. See also https://github.com/libgit2/libgit2/issues/1534.
+ command = %W[#{Gitlab.config.git.bin_path} update-ref --stdin -z]
+ _, status = Gitlab::Popen.popen(
+ command,
+ repository.path_to_repo) do |stdin|
+ stdin.write("update #{ref}\x00#{newrev}\x00#{oldrev}\x00")
+ end
+
+ unless status.zero?
+ raise Repository::CommitError.new(
+ "Could not update branch #{Gitlab::Git.branch_name(ref)}." \
+ " Please refresh and try again.")
+ end
+ end
+
+ def update_autocrlf_option
+ if repository.raw_repository.autocrlf != :input
+ repository.raw_repository.autocrlf = :input
+ end
+ end
+
+ def check_with_branch_arguments!(
+ branch_name, start_branch_name, start_project)
+ return if repository.branch_exists?(branch_name)
+
+ if repository.project != start_project
+ unless start_branch_name
+ raise ArgumentError,
+ 'Should also pass :start_branch_name if' +
+ ' :start_project is different from current project'
+ end
+
+ unless start_project.repository.branch_exists?(start_branch_name)
+ raise ArgumentError,
+ "Cannot find branch #{branch_name} nor" \
+ " #{start_branch_name} from" \
+ " #{start_project.path_with_namespace}"
+ end
+ elsif start_branch_name
+ unless repository.branch_exists?(start_branch_name)
+ raise ArgumentError,
+ "Cannot find branch #{branch_name} nor" \
+ " #{start_branch_name} from" \
+ " #{repository.project.path_with_namespace}"
+ end
+ end
+ end
+end
diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb
index 185556c12cc..dbe2fda27b5 100644
--- a/app/services/git_push_service.rb
+++ b/app/services/git_push_service.rb
@@ -3,6 +3,9 @@ class GitPushService < BaseService
include Gitlab::CurrentSettings
include Gitlab::Access
+ # The N most recent commits to process in a single push payload.
+ PROCESS_COMMIT_LIMIT = 100
+
# This method will be called after each git update
# and only if the provided user and project are present in GitLab.
#
@@ -74,7 +77,17 @@ class GitPushService < BaseService
types = []
end
- ProjectCacheWorker.perform_async(@project.id, types)
+ ProjectCacheWorker.perform_async(@project.id, types, [:commit_count, :repository_size])
+ end
+
+ # Schedules processing of commit messages.
+ def process_commit_messages
+ default = is_default_branch?
+
+ push_commits.last(PROCESS_COMMIT_LIMIT).each do |commit|
+ ProcessCommitWorker.
+ perform_async(project.id, current_user.id, commit.to_hash, default)
+ end
end
protected
@@ -128,17 +141,6 @@ class GitPushService < BaseService
end
end
- # Extract any GFM references from the pushed commit messages. If the configured issue-closing regex is matched,
- # close the referenced Issue. Create cross-reference Notes corresponding to any other referenced Mentionables.
- def process_commit_messages
- default = is_default_branch?
-
- @push_commits.each do |commit|
- ProcessCommitWorker.
- perform_async(project.id, current_user.id, commit.to_hash, default)
- end
- end
-
def build_push_data
@push_data ||= Gitlab::DataBuilder::Push.build(
@project,
diff --git a/app/services/git_tag_push_service.rb b/app/services/git_tag_push_service.rb
index 20a4445bddf..96432837481 100644
--- a/app/services/git_tag_push_service.rb
+++ b/app/services/git_tag_push_service.rb
@@ -12,7 +12,7 @@ class GitTagPushService < BaseService
project.execute_hooks(@push_data.dup, :tag_push_hooks)
project.execute_services(@push_data.dup, :tag_push_hooks)
Ci::CreatePipelineService.new(project, current_user, @push_data).execute
- ProjectCacheWorker.perform_async(project.id)
+ ProjectCacheWorker.perform_async(project.id, [], [:commit_count, :repository_size])
true
end
diff --git a/app/services/groups/create_service.rb b/app/services/groups/create_service.rb
index 2bccd584dde..febeb661fb5 100644
--- a/app/services/groups/create_service.rb
+++ b/app/services/groups/create_service.rb
@@ -12,6 +12,13 @@ module Groups
return @group
end
+ if @group.parent && !can?(current_user, :admin_group, @group.parent)
+ @group.parent = nil
+ @group.errors.add(:parent_id, 'manage access required to create subgroup')
+
+ return @group
+ end
+
@group.name ||= @group.path.dup
@group.save
@group.add_owner(current_user)
diff --git a/app/services/groups/update_service.rb b/app/services/groups/update_service.rb
index fff2273f402..4e878ec556a 100644
--- a/app/services/groups/update_service.rb
+++ b/app/services/groups/update_service.rb
@@ -14,7 +14,13 @@ module Groups
group.assign_attributes(params)
- group.save
+ begin
+ group.save
+ rescue Gitlab::UpdatePathError => e
+ group.errors.add(:base, e.message)
+
+ false
+ end
end
end
end
diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb
index ab3d2a9a0cd..5f3ced49665 100644
--- a/app/services/issuable_base_service.rb
+++ b/app/services/issuable_base_service.rb
@@ -36,14 +36,18 @@ class IssuableBaseService < BaseService
end
end
- def filter_params(issuable_ability_name = :issue)
- filter_assignee
- filter_milestone
- filter_labels
+ def create_time_estimate_note(issuable)
+ SystemNoteService.change_time_estimate(issuable, issuable.project, current_user)
+ end
+
+ def create_time_spent_note(issuable)
+ SystemNoteService.change_time_spent(issuable, issuable.project, current_user)
+ end
- ability = :"admin_#{issuable_ability_name}"
+ def filter_params(issuable)
+ ability_name = :"admin_#{issuable.to_ability_name}"
- unless can?(current_user, ability, project)
+ unless can?(current_user, ability_name, project)
params.delete(:milestone_id)
params.delete(:labels)
params.delete(:add_label_ids)
@@ -52,14 +56,35 @@ class IssuableBaseService < BaseService
params.delete(:assignee_id)
params.delete(:due_date)
end
+
+ filter_assignee(issuable)
+ filter_milestone
+ filter_labels
end
- def filter_assignee
- if params[:assignee_id] == IssuableFinder::NONE
- params[:assignee_id] = ''
+ def filter_assignee(issuable)
+ return unless params[:assignee_id].present?
+
+ assignee_id = params[:assignee_id]
+
+ if assignee_id.to_s == IssuableFinder::NONE
+ params[:assignee_id] = ""
+ else
+ params.delete(:assignee_id) unless assignee_can_read?(issuable, assignee_id)
end
end
+ def assignee_can_read?(issuable, assignee_id)
+ new_assignee = User.find_by_id(assignee_id)
+
+ return false unless new_assignee.present?
+
+ ability_name = :"read_#{issuable.to_ability_name}"
+ resource = issuable.persisted? ? issuable : project
+
+ can?(new_assignee, ability_name, resource)
+ end
+
def filter_milestone
milestone_id = params[:milestone_id]
return unless milestone_id
@@ -138,7 +163,7 @@ class IssuableBaseService < BaseService
def create(issuable)
merge_slash_commands_into_params!(issuable)
- filter_params
+ filter_params(issuable)
params.delete(:state_event)
params[:author] ||= current_user
@@ -180,7 +205,7 @@ class IssuableBaseService < BaseService
change_state(issuable)
change_subscription(issuable)
change_todo(issuable)
- filter_params
+ filter_params(issuable)
old_labels = issuable.labels.to_a
old_mentioned_users = issuable.mentioned_users.to_a
@@ -255,6 +280,14 @@ class IssuableBaseService < BaseService
create_task_status_note(issuable)
end
+ if issuable.previous_changes.include?('time_estimate')
+ create_time_estimate_note(issuable)
+ end
+
+ if issuable.time_spent?
+ create_time_spent_note(issuable)
+ end
+
create_labels_note(issuable, old_labels) if issuable.labels != old_labels
end
end
diff --git a/app/services/issues/base_service.rb b/app/services/issues/base_service.rb
index 742e834df97..35af867a098 100644
--- a/app/services/issues/base_service.rb
+++ b/app/services/issues/base_service.rb
@@ -17,10 +17,6 @@ module Issues
private
- def filter_params
- super(:issue)
- end
-
def execute_hooks(issue, action = 'open')
issue_data = hook_data(issue, action)
hooks_scope = issue.confidential? ? :confidential_issue_hooks : :issue_hooks
diff --git a/app/services/issues/create_service.rb b/app/services/issues/create_service.rb
index d2eb46ac41b..c9168f74249 100644
--- a/app/services/issues/create_service.rb
+++ b/app/services/issues/create_service.rb
@@ -3,6 +3,8 @@ module Issues
def execute
@request = params.delete(:request)
@api = params.delete(:api)
+ @recaptcha_verified = params.delete(:recaptcha_verified)
+ @spam_log_id = params.delete(:spam_log_id)
issue_attributes = params.merge(merge_request_for_resolving_discussions: merge_request_for_resolving_discussions)
@issue = BuildService.new(project, current_user, issue_attributes).execute
@@ -11,7 +13,13 @@ module Issues
end
def before_create(issuable)
- issuable.spam = spam_service.check(@api)
+ if @recaptcha_verified
+ spam_log = current_user.spam_logs.find_by(id: @spam_log_id, title: issuable.title)
+ spam_log.update!(recaptcha_verified: true) if spam_log
+ else
+ issuable.spam = spam_service.check(@api)
+ issuable.spam_log = spam_service.spam_log
+ end
end
def after_create(issuable)
@@ -35,7 +43,7 @@ module Issues
private
def spam_service
- SpamService.new(@issue, @request)
+ @spam_service ||= SpamService.new(@issue, @request)
end
def user_agent_detail_service
diff --git a/app/services/labels/promote_service.rb b/app/services/labels/promote_service.rb
new file mode 100644
index 00000000000..76d0ba67b07
--- /dev/null
+++ b/app/services/labels/promote_service.rb
@@ -0,0 +1,71 @@
+module Labels
+ class PromoteService < BaseService
+ BATCH_SIZE = 1000
+
+ def execute(label)
+ return unless project.group &&
+ label.is_a?(ProjectLabel)
+
+ Label.transaction do
+ new_label = clone_label_to_group_label(label)
+
+ label_ids_for_merge(new_label).find_in_batches(batch_size: BATCH_SIZE) do |batched_ids|
+ update_issuables(new_label, batched_ids)
+ update_issue_board_lists(new_label, batched_ids)
+ update_priorities(new_label, batched_ids)
+ # Order is important, project labels need to be last
+ update_project_labels(batched_ids)
+ end
+
+ # We skipped validations during creation. Let's run them now, after deleting conflicting labels
+ raise ActiveRecord::RecordInvalid.new(new_label) unless new_label.valid?
+ new_label
+ end
+ end
+
+ private
+
+ def label_ids_for_merge(new_label)
+ LabelsFinder.
+ new(current_user, title: new_label.title, group_id: project.group.id).
+ execute(skip_authorization: true).
+ where.not(id: new_label).
+ select(:id) # Can't use pluck() to avoid object-creation because of the batching
+ end
+
+ def update_issuables(new_label, label_ids)
+ LabelLink.
+ where(label: label_ids).
+ update_all(label_id: new_label)
+ end
+
+ def update_issue_board_lists(new_label, label_ids)
+ List.
+ where(label: label_ids).
+ update_all(label_id: new_label)
+ end
+
+ def update_priorities(new_label, label_ids)
+ LabelPriority.
+ where(label: label_ids).
+ update_all(label_id: new_label)
+ end
+
+ def update_project_labels(label_ids)
+ Label.where(id: label_ids).delete_all
+ end
+
+ def clone_label_to_group_label(label)
+ params = label.attributes.slice('title', 'description', 'color')
+ # Since the title of the new label has to be the same as the previous labels
+ # and we're merging old labels in batches we'll skip validation to omit 2-step
+ # merge process and do it in one batch
+ # We'll be forcing validation at the end of the transaction to ensure everything
+ # was merged correctly
+ new_label = GroupLabel.new(params.merge(group: project.group))
+ new_label.save(validate: false)
+
+ new_label
+ end
+ end
+end
diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb
index 800fd39c424..5a53b973059 100644
--- a/app/services/merge_requests/base_service.rb
+++ b/app/services/merge_requests/base_service.rb
@@ -38,19 +38,13 @@ module MergeRequests
private
- def filter_params
- super(:merge_request)
- end
-
- def merge_requests_for(branch)
- origin_merge_requests = @project.origin_merge_requests
- .opened.where(source_branch: branch).to_a
-
- fork_merge_requests = @project.fork_merge_requests
- .opened.where(source_branch: branch).to_a
-
- (origin_merge_requests + fork_merge_requests)
- .uniq.select(&:source_project)
+ # Returns all origin and fork merge requests from `@project` satisfying passed arguments.
+ def merge_requests_for(source_branch, mr_states: [:opened])
+ MergeRequest
+ .with_state(mr_states)
+ .where(source_branch: source_branch, source_project_id: @project.id)
+ .preload(:source_project) # we don't need a #includes since we're just preloading for the #select
+ .select(&:source_project)
end
def pipeline_merge_requests(pipeline)
diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb
index 6a7393a9921..f4d52e3ebbd 100644
--- a/app/services/merge_requests/build_service.rb
+++ b/app/services/merge_requests/build_service.rb
@@ -1,62 +1,89 @@
module MergeRequests
class BuildService < MergeRequests::BaseService
def execute
- merge_request = MergeRequest.new(params)
-
- # Set MR attributes
- merge_request.can_be_created = true
+ self.merge_request = MergeRequest.new(params)
+ merge_request.can_be_created = true
merge_request.compare_commits = []
- merge_request.source_project = project unless merge_request.source_project
+ merge_request.source_project = find_source_project
+ merge_request.target_project = find_target_project
+ merge_request.target_branch = find_target_branch
- merge_request.target_project = nil unless can?(current_user, :read_project, merge_request.target_project)
+ if branches_specified? && branches_valid?
+ compare_branches
+ assign_title_and_description
+ else
+ merge_request.can_be_created = false
+ end
- merge_request.target_project ||= (project.forked_from_project || project)
- merge_request.target_branch ||= merge_request.target_project.default_branch
+ merge_request
+ end
- messages = validate_branches(merge_request)
- return build_failed(merge_request, messages) unless messages.empty?
+ private
- compare = CompareService.new.execute(
- merge_request.source_project,
- merge_request.source_branch,
- merge_request.target_project,
- merge_request.target_branch,
- )
+ attr_accessor :merge_request
- merge_request.compare_commits = compare.commits
- merge_request.compare = compare
+ delegate :target_branch, :source_branch, :source_project, :target_project, :compare_commits, :wip_title, :description, :errors, to: :merge_request
- set_title_and_description(merge_request)
+ def find_source_project
+ source_project || project
end
- private
+ def find_target_project
+ return target_project if target_project.present? && can?(current_user, :read_project, target_project)
+ project.forked_from_project || project
+ end
- def validate_branches(merge_request)
- messages = []
+ def find_target_branch
+ target_branch || target_project.default_branch
+ end
- if merge_request.target_branch.blank? || merge_request.source_branch.blank?
- messages <<
- if params[:source_branch] || params[:target_branch]
- "You must select source and target branch"
- end
- end
+ def branches_specified?
+ params[:source_branch] && params[:target_branch]
+ end
- if merge_request.source_project == merge_request.target_project &&
- merge_request.target_branch == merge_request.source_branch
+ def branches_valid?
+ validate_branches
+ errors.blank?
+ end
- messages << 'You must select different branches'
- end
+ def compare_branches
+ compare = CompareService.new(
+ source_project,
+ source_branch
+ ).execute(
+ target_project,
+ target_branch
+ )
- # See if source and target branches exist
- if merge_request.source_branch.present? && !merge_request.source_project.commit(merge_request.source_branch)
- messages << "Source branch \"#{merge_request.source_branch}\" does not exist"
- end
+ merge_request.compare_commits = compare.commits
+ merge_request.compare = compare
+ end
- if merge_request.target_branch.present? && !merge_request.target_project.commit(merge_request.target_branch)
- messages << "Target branch \"#{merge_request.target_branch}\" does not exist"
- end
+ def validate_branches
+ add_error('You must select source and target branch') unless branches_present?
+ add_error('You must select different branches') if same_source_and_target?
+ add_error("Source branch \"#{source_branch}\" does not exist") unless source_branch_exists?
+ add_error("Target branch \"#{target_branch}\" does not exist") unless target_branch_exists?
+ end
+
+ def add_error(message)
+ errors.add(:base, message)
+ end
+
+ def branches_present?
+ target_branch.present? && source_branch.present?
+ end
+
+ def same_source_and_target?
+ source_project == target_project && target_branch == source_branch
+ end
- messages
+ def source_branch_exists?
+ source_branch.blank? || source_project.commit(source_branch)
+ end
+
+ def target_branch_exists?
+ target_branch.blank? || target_project.commit(target_branch)
end
# When your branch name starts with an iid followed by a dash this pattern will be
@@ -71,17 +98,17 @@ module MergeRequests
# - Setting the title as 'Resolves "Emoji don't show up in commit title"' if there is
# more than one commit in the MR
#
- def set_title_and_description(merge_request)
- if match = merge_request.source_branch.match(/\A(\d+)-/)
+ def assign_title_and_description
+ if match = source_branch.match(/\A(\d+)-/)
iid = match[1]
end
- commits = merge_request.compare_commits
+ commits = compare_commits
if commits && commits.count == 1
commit = commits.first
merge_request.title = commit.title
merge_request.description ||= commit.description.try(:strip)
- elsif iid && issue = merge_request.target_project.get_issue(iid, current_user)
+ elsif iid && issue = target_project.get_issue(iid, current_user)
case issue
when Issue
merge_request.title = "Resolve \"#{issue.title}\""
@@ -89,31 +116,20 @@ module MergeRequests
merge_request.title = "Resolve #{issue.title}"
end
else
- merge_request.title = merge_request.source_branch.titleize.humanize
+ merge_request.title = source_branch.titleize.humanize
end
if iid
closes_issue = "Closes ##{iid}"
- if merge_request.description.present?
+ if description.present?
merge_request.description += closes_issue.prepend("\n\n")
else
merge_request.description = closes_issue
end
end
- merge_request.title = merge_request.wip_title if commits.empty?
-
- merge_request
- end
-
- def build_failed(merge_request, messages)
- messages.compact.each do |message|
- merge_request.errors.add(:base, message)
- end
- merge_request.compare_commits = []
- merge_request.can_be_created = false
- merge_request
+ merge_request.title = wip_title if commits.empty?
end
end
end
diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb
index ab9056a3250..5ca6fec962d 100644
--- a/app/services/merge_requests/merge_service.rb
+++ b/app/services/merge_requests/merge_service.rb
@@ -6,13 +6,17 @@ module MergeRequests
# Executed when you do merge via GitLab UI
#
class MergeService < MergeRequests::BaseService
- attr_reader :merge_request
+ attr_reader :merge_request, :source
def execute(merge_request)
@merge_request = merge_request
return log_merge_error('Merge request is not mergeable', true) unless @merge_request.mergeable?
+ @source = find_merge_source
+
+ return log_merge_error('No source for merge', true) unless @source
+
merge_request.in_locked_state do
if commit
after_merge
@@ -34,7 +38,7 @@ module MergeRequests
committer: committer
}
- commit_id = repository.merge(current_user, merge_request, options)
+ commit_id = repository.merge(current_user, source, merge_request, options)
if commit_id
merge_request.update(merge_commit_sha: commit_id)
@@ -73,9 +77,11 @@ module MergeRequests
end
def merge_request_info
- project = merge_request.project
+ merge_request.to_reference(full: true)
+ end
- "#{project.to_reference}#{merge_request.to_reference}"
+ def find_merge_source
+ merge_request.diff_head_sha
end
end
end
diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb
index 0a9563ed7e7..b4bfb0e5e8c 100644
--- a/app/services/merge_requests/refresh_service.rb
+++ b/app/services/merge_requests/refresh_service.rb
@@ -21,6 +21,7 @@ module MergeRequests
end
comment_mr_with_commits
+ mark_mr_as_wip_from_commits
execute_mr_web_hooks
true
@@ -41,7 +42,7 @@ module MergeRequests
commit_ids.include?(merge_request.diff_head_sha)
end
- merge_requests.uniq.select(&:source_project).each do |merge_request|
+ filter_merge_requests(merge_requests).each do |merge_request|
MergeRequests::PostMergeService.
new(merge_request.target_project, @current_user).
execute(merge_request)
@@ -57,10 +58,13 @@ module MergeRequests
def reload_merge_requests
merge_requests = @project.merge_requests.opened.
by_source_or_target_branch(@branch_name).to_a
- merge_requests += fork_merge_requests
- merge_requests = filter_merge_requests(merge_requests)
- merge_requests.each do |merge_request|
+ # Fork merge requests
+ merge_requests += MergeRequest.opened
+ .where(source_branch: @branch_name, source_project: @project)
+ .where.not(target_project: @project).to_a
+
+ filter_merge_requests(merge_requests).each do |merge_request|
if merge_request.source_branch == @branch_name || force_push?
merge_request.reload_diff
else
@@ -136,6 +140,24 @@ module MergeRequests
end
end
+ def mark_mr_as_wip_from_commits
+ return unless @commits.present?
+
+ merge_requests_for_source_branch.each do |merge_request|
+ wip_commit = @commits.detect(&:work_in_progress?)
+
+ if wip_commit && !merge_request.work_in_progress?
+ merge_request.update(title: merge_request.wip_title)
+ SystemNoteService.add_merge_request_wip_from_commit(
+ merge_request,
+ merge_request.project,
+ @current_user,
+ wip_commit
+ )
+ end
+ end
+ end
+
# Call merge request webhook with update branches
def execute_mr_web_hooks
merge_requests_for_source_branch.each do |merge_request|
@@ -156,16 +178,7 @@ module MergeRequests
end
def merge_requests_for_source_branch
- @source_merge_requests ||= begin
- merge_requests = @project.origin_merge_requests.opened.where(source_branch: @branch_name).to_a
- merge_requests += fork_merge_requests
- filter_merge_requests(merge_requests)
- end
- end
-
- def fork_merge_requests
- @fork_merge_requests ||= @project.fork_merge_requests.opened.
- where(source_branch: @branch_name).to_a
+ @source_merge_requests ||= merge_requests_for(@branch_name)
end
def branch_added?
diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb
index ad16ef8c70f..3cb9aae83f6 100644
--- a/app/services/merge_requests/update_service.rb
+++ b/app/services/merge_requests/update_service.rb
@@ -7,6 +7,8 @@ module MergeRequests
params.except!(:target_project_id)
params.except!(:source_branch)
+ merge_from_slash_command(merge_request) if params[:merge]
+
if merge_request.closed_without_fork?
params.except!(:target_branch, :force_remove_source_branch)
end
@@ -69,6 +71,19 @@ module MergeRequests
end
end
+ def merge_from_slash_command(merge_request)
+ last_diff_sha = params.delete(:merge)
+ return unless merge_request.mergeable_with_slash_command?(current_user, last_diff_sha: last_diff_sha)
+
+ merge_request.update(merge_error: nil)
+
+ if merge_request.head_pipeline && merge_request.head_pipeline.active?
+ MergeRequests::MergeWhenPipelineSucceedsService.new(project, current_user).execute(merge_request)
+ else
+ MergeWorker.perform_async(merge_request.id, current_user.id, {})
+ end
+ end
+
def reopen_service
MergeRequests::ReopenService
end
diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb
index d75592e31f3..b4f8b33d564 100644
--- a/app/services/notes/create_service.rb
+++ b/app/services/notes/create_service.rb
@@ -1,9 +1,12 @@
module Notes
class CreateService < BaseService
def execute
- note = project.notes.new(params)
- note.author = current_user
- note.system = false
+ merge_request_diff_head_sha = params.delete(:merge_request_diff_head_sha)
+
+ note = Note.new(params)
+ note.project = project
+ note.author = current_user
+ note.system = false
if note.award_emoji?
noteable = note.noteable
@@ -19,7 +22,8 @@ module Notes
slash_commands_service = SlashCommandsService.new(project, current_user)
if slash_commands_service.supported?(note)
- content, command_params = slash_commands_service.extract_commands(note)
+ options = { merge_request_diff_head_sha: merge_request_diff_head_sha }
+ content, command_params = slash_commands_service.extract_commands(note, options)
only_commands = content.empty?
@@ -41,7 +45,7 @@ module Notes
# 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!')
+ note.errors.add(:commands_only, 'Commands applied')
end
note.commands_changes = command_params.keys
diff --git a/app/services/notes/post_process_service.rb b/app/services/notes/post_process_service.rb
index e4cd3fc7833..6a10e172483 100644
--- a/app/services/notes/post_process_service.rb
+++ b/app/services/notes/post_process_service.rb
@@ -10,6 +10,9 @@ module Notes
# Skip system notes, like status changes and cross-references and awards
unless @note.system?
EventCreateService.new.leave_note(@note, @note.author)
+
+ return if @note.for_personal_snippet?
+
@note.create_cross_references!
execute_note_hooks
end
diff --git a/app/services/notes/slash_commands_service.rb b/app/services/notes/slash_commands_service.rb
index 2edbd39a9e7..56913568cae 100644
--- a/app/services/notes/slash_commands_service.rb
+++ b/app/services/notes/slash_commands_service.rb
@@ -12,17 +12,17 @@ module Notes
def self.supported?(note, current_user)
noteable_update_service(note) &&
current_user &&
- current_user.can?(:"update_#{note.noteable_type.underscore}", note.noteable)
+ current_user.can?(:"update_#{note.to_ability_name}", note.noteable)
end
def supported?(note)
self.class.supported?(note, current_user)
end
- def extract_commands(note)
+ def extract_commands(note, options = {})
return [note.note, {}] unless supported?(note)
- SlashCommands::InterpretService.new(project, current_user).
+ SlashCommands::InterpretService.new(project, current_user, options).
execute(note.note, note.noteable)
end
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index 9a7af5730d2..b2cc39763f3 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -178,8 +178,15 @@ class NotificationService
recipients = []
mentioned_users = note.mentioned_users
+
+ ability, subject = if note.for_personal_snippet?
+ [:read_personal_snippet, note.noteable]
+ else
+ [:read_project, note.project]
+ end
+
mentioned_users.select! do |user|
- user.can?(:read_project, note.project)
+ user.can?(ability, subject)
end
# Add all users participating in the thread (author, assignee, comment authors)
@@ -192,11 +199,13 @@ class NotificationService
recipients = recipients.concat(participants)
- # Merge project watchers
- recipients = add_project_watchers(recipients, note.project)
+ unless note.for_personal_snippet?
+ # Merge project watchers
+ recipients = add_project_watchers(recipients, note.project)
- # Merge project with custom notification
- recipients = add_custom_notifications(recipients, note.project, :new_note)
+ # Merge project with custom notification
+ recipients = add_custom_notifications(recipients, note.project, :new_note)
+ end
# Reject users with Mention notification level, except those mentioned in _this_ note.
recipients = reject_mention_users(recipients - mentioned_users, note.project)
@@ -211,8 +220,7 @@ class NotificationService
recipients.delete(note.author)
recipients = recipients.uniq
- # build notify method like 'note_commit_email'
- notify_method = "note_#{note.noteable_type.underscore}_email".to_sym
+ notify_method = "note_#{note.to_ability_name}_email".to_sym
recipients.each do |recipient|
mailer.send(notify_method, recipient.id, note.id).deliver_later
@@ -357,7 +365,7 @@ class NotificationService
users = users_with_global_level_watch([users_with_project_level_global, users_with_group_level_global].flatten.uniq)
users_with_project_setting = select_project_member_setting(project, users_with_project_level_global, users)
- users_with_group_setting = select_group_member_setting(project, project_members, users_with_group_level_global, users)
+ users_with_group_setting = select_group_member_setting(project.group, project_members, users_with_group_level_global, users)
User.where(id: users_with_project_setting.concat(users_with_group_setting).uniq).to_a
end
@@ -407,8 +415,8 @@ class NotificationService
end
# Build a list of users based on group notification settings
- def select_group_member_setting(project, project_members, global_setting, users_global_level_watch)
- uids = notification_settings_for(project, :watch)
+ def select_group_member_setting(group, project_members, global_setting, users_global_level_watch)
+ uids = notification_settings_for(group, :watch)
# Group setting is watch, add to users list if user is not project member
users = []
@@ -465,7 +473,7 @@ class NotificationService
setting = user.notification_settings_for(project)
- if !setting && project.group
+ if project.group && (setting.nil? || setting.global?)
setting = user.notification_settings_for(project.group)
end
@@ -591,7 +599,10 @@ class NotificationService
custom_action = build_custom_key(action, target)
recipients = target.participants(current_user)
- recipients = add_project_watchers(recipients, project)
+
+ unless NotificationSetting::EXCLUDED_WATCHER_EVENTS.include?(custom_action)
+ recipients = add_project_watchers(recipients, project)
+ end
recipients = add_custom_notifications(recipients, project, custom_action)
recipients = reject_mention_users(recipients, project)
diff --git a/app/services/pages_service.rb b/app/services/pages_service.rb
new file mode 100644
index 00000000000..446eeb34d3b
--- /dev/null
+++ b/app/services/pages_service.rb
@@ -0,0 +1,15 @@
+class PagesService
+ attr_reader :data
+
+ def initialize(data)
+ @data = data
+ end
+
+ def execute
+ return unless Settings.pages.enabled
+ return unless data[:build_name] == 'pages'
+ return unless data[:build_status] == 'success'
+
+ PagesWorker.perform_async(:deploy, data[:build_id])
+ end
+end
diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb
index 159f46cd465..c7cce0c55b9 100644
--- a/app/services/projects/create_service.rb
+++ b/app/services/projects/create_service.rb
@@ -22,17 +22,7 @@ module Projects
return @project
end
- # Set project name from path
- if @project.name.present? && @project.path.present?
- # if both name and path set - everything is ok
- elsif @project.path.present?
- # Set project name from path
- @project.name = @project.path.dup
- elsif @project.name.present?
- # For compatibility - set path from name
- # TODO: remove this in 8.0
- @project.path = @project.name.dup.parameterize
- end
+ set_project_name_from_path
# get namespace id
namespace_id = params[:namespace_id]
@@ -144,5 +134,19 @@ module Projects
service.save!
end
end
+
+ def set_project_name_from_path
+ # Set project name from path
+ if @project.name.present? && @project.path.present?
+ # if both name and path set - everything is ok
+ elsif @project.path.present?
+ # Set project name from path
+ @project.name = @project.path.dup
+ elsif @project.name.present?
+ # For compatibility - set path from name
+ # TODO: remove this in 8.0
+ @project.path = @project.name.dup.parameterize
+ end
+ end
end
end
diff --git a/app/services/projects/import_export/export_service.rb b/app/services/projects/import_export/export_service.rb
index 06252c7b625..535da706159 100644
--- a/app/services/projects/import_export/export_service.rb
+++ b/app/services/projects/import_export/export_service.rb
@@ -26,7 +26,7 @@ module Projects
end
def project_tree_saver
- Gitlab::ImportExport::ProjectTreeSaver.new(project: project, shared: @shared)
+ Gitlab::ImportExport::ProjectTreeSaver.new(project: project, current_user: @current_user, shared: @shared)
end
def uploads_saver
diff --git a/app/services/projects/participants_service.rb b/app/services/projects/participants_service.rb
index 6040391fd94..96c363c8d1a 100644
--- a/app/services/projects/participants_service.rb
+++ b/app/services/projects/participants_service.rb
@@ -36,7 +36,7 @@ module Projects
def groups
current_user.authorized_groups.sort_by(&:path).map do |group|
count = group.users.count
- { username: group.path, name: group.name, count: count, avatar_url: group.avatar.url }
+ { username: group.path, name: group.name, count: count, avatar_url: group.avatar_url }
end
end
diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb
index 34ec575e808..484700c8c29 100644
--- a/app/services/projects/transfer_service.rb
+++ b/app/services/projects/transfer_service.rb
@@ -25,9 +25,10 @@ module Projects
end
def transfer(project, new_namespace)
+ old_namespace = project.namespace
+
Project.transaction do
old_path = project.path_with_namespace
- old_namespace = project.namespace
old_group = project.group
new_path = File.join(new_namespace.try(:path) || '', project.path)
@@ -64,11 +65,17 @@ module Projects
# Move uploads
Gitlab::UploadsTransfer.new.move_project(project.path, old_namespace.path, new_namespace.path)
+ # Move pages
+ Gitlab::PagesTransfer.new.move_project(project.path, old_namespace.path, new_namespace.path)
+
project.old_path_with_namespace = old_path
SystemHooksService.new.execute_hooks_for(project, :transfer)
- true
end
+
+ refresh_permissions(old_namespace, new_namespace)
+
+ true
end
def allowed_transfer?(current_user, project, namespace)
@@ -77,5 +84,14 @@ module Projects
namespace.id != project.namespace_id &&
current_user.can?(:create_projects, namespace)
end
+
+ def refresh_permissions(old_namespace, new_namespace)
+ # This ensures we only schedule 1 job for every user that has access to
+ # the namespaces.
+ user_ids = old_namespace.user_ids_for_project_authorizations |
+ new_namespace.user_ids_for_project_authorizations
+
+ UserProjectAccessChangedService.new(user_ids).execute
+ end
end
end
diff --git a/app/services/projects/update_pages_configuration_service.rb b/app/services/projects/update_pages_configuration_service.rb
new file mode 100644
index 00000000000..eb4809afa85
--- /dev/null
+++ b/app/services/projects/update_pages_configuration_service.rb
@@ -0,0 +1,69 @@
+module Projects
+ class UpdatePagesConfigurationService < BaseService
+ attr_reader :project
+
+ def initialize(project)
+ @project = project
+ end
+
+ def execute
+ update_file(pages_config_file, pages_config.to_json)
+ reload_daemon
+ success
+ rescue => e
+ error(e.message)
+ end
+
+ private
+
+ def pages_config
+ {
+ domains: pages_domains_config
+ }
+ end
+
+ def pages_domains_config
+ project.pages_domains.map do |domain|
+ {
+ domain: domain.domain,
+ certificate: domain.certificate,
+ key: domain.key,
+ }
+ end
+ end
+
+ def reload_daemon
+ # GitLab Pages daemon constantly watches for modification time of `pages.path`
+ # It reloads configuration when `pages.path` is modified
+ update_file(pages_update_file, SecureRandom.hex(64))
+ end
+
+ def pages_path
+ @pages_path ||= project.pages_path
+ end
+
+ def pages_config_file
+ File.join(pages_path, 'config.json')
+ end
+
+ def pages_update_file
+ File.join(::Settings.pages.path, '.update')
+ end
+
+ def update_file(file, data)
+ unless data
+ FileUtils.remove(file, force: true)
+ return
+ end
+
+ temp_file = "#{file}.#{SecureRandom.hex(16)}"
+ File.open(temp_file, 'w') do |f|
+ f.write(data)
+ end
+ FileUtils.move(temp_file, file, force: true)
+ ensure
+ # In case if the updating fails
+ FileUtils.remove(temp_file, force: true)
+ end
+ end
+end
diff --git a/app/services/projects/update_pages_service.rb b/app/services/projects/update_pages_service.rb
new file mode 100644
index 00000000000..f5f9ee88912
--- /dev/null
+++ b/app/services/projects/update_pages_service.rb
@@ -0,0 +1,164 @@
+module Projects
+ class UpdatePagesService < BaseService
+ BLOCK_SIZE = 32.kilobytes
+ MAX_SIZE = 1.terabyte
+ SITE_PATH = 'public/'
+
+ attr_reader :build
+
+ def initialize(project, build)
+ @project, @build = project, build
+ end
+
+ def execute
+ # Create status notifying the deployment of pages
+ @status = create_status
+ @status.enqueue!
+ @status.run!
+
+ raise 'missing pages artifacts' unless build.artifacts_file?
+ raise 'pages are outdated' unless latest?
+
+ # Create temporary directory in which we will extract the artifacts
+ FileUtils.mkdir_p(tmp_path)
+ Dir.mktmpdir(nil, tmp_path) do |archive_path|
+ extract_archive!(archive_path)
+
+ # Check if we did extract public directory
+ archive_public_path = File.join(archive_path, 'public')
+ raise 'pages miss the public folder' unless Dir.exist?(archive_public_path)
+ raise 'pages are outdated' unless latest?
+
+ deploy_page!(archive_public_path)
+ success
+ end
+ rescue => e
+ error(e.message)
+ end
+
+ private
+
+ def success
+ @status.success
+ super
+ end
+
+ def error(message, http_status = nil)
+ @status.allow_failure = !latest?
+ @status.description = message
+ @status.drop
+ super
+ end
+
+ def create_status
+ GenericCommitStatus.new(
+ project: project,
+ pipeline: build.pipeline,
+ user: build.user,
+ ref: build.ref,
+ stage: 'deploy',
+ name: 'pages:deploy'
+ )
+ end
+
+ def extract_archive!(temp_path)
+ if artifacts.ends_with?('.tar.gz') || artifacts.ends_with?('.tgz')
+ extract_tar_archive!(temp_path)
+ elsif artifacts.ends_with?('.zip')
+ extract_zip_archive!(temp_path)
+ else
+ raise 'unsupported artifacts format'
+ end
+ end
+
+ def extract_tar_archive!(temp_path)
+ results = Open3.pipeline(%W(gunzip -c #{artifacts}),
+ %W(dd bs=#{BLOCK_SIZE} count=#{blocks}),
+ %W(tar -x -C #{temp_path} #{SITE_PATH}),
+ err: '/dev/null')
+ raise 'pages failed to extract' unless results.compact.all?(&:success?)
+ end
+
+ def extract_zip_archive!(temp_path)
+ raise 'missing artifacts metadata' unless build.artifacts_metadata?
+
+ # Calculate page size after extract
+ public_entry = build.artifacts_metadata_entry(SITE_PATH, recursive: true)
+
+ if public_entry.total_size > max_size
+ raise "artifacts for pages are too large: #{public_entry.total_size}"
+ end
+
+ # Requires UnZip at least 6.00 Info-ZIP.
+ # -n never overwrite existing files
+ # We add * to end of SITE_PATH, because we want to extract SITE_PATH and all subdirectories
+ site_path = File.join(SITE_PATH, '*')
+ unless system(*%W(unzip -n #{artifacts} #{site_path} -d #{temp_path}))
+ raise 'pages failed to extract'
+ end
+ end
+
+ def deploy_page!(archive_public_path)
+ # Do atomic move of pages
+ # Move and removal may not be atomic, but they are significantly faster then extracting and removal
+ # 1. We move deployed public to previous public path (file removal is slow)
+ # 2. We move temporary public to be deployed public
+ # 3. We remove previous public path
+ FileUtils.mkdir_p(pages_path)
+ begin
+ FileUtils.move(public_path, previous_public_path)
+ rescue
+ end
+ FileUtils.move(archive_public_path, public_path)
+ ensure
+ FileUtils.rm_r(previous_public_path, force: true)
+ end
+
+ def latest?
+ # check if sha for the ref is still the most recent one
+ # this helps in case when multiple deployments happens
+ sha == latest_sha
+ end
+
+ def blocks
+ # Calculate dd parameters: we limit the size of pages
+ 1 + max_size / BLOCK_SIZE
+ end
+
+ def max_size
+ current_application_settings.max_pages_size.megabytes || MAX_SIZE
+ end
+
+ def tmp_path
+ @tmp_path ||= File.join(::Settings.pages.path, 'tmp')
+ end
+
+ def pages_path
+ @pages_path ||= project.pages_path
+ end
+
+ def public_path
+ @public_path ||= File.join(pages_path, 'public')
+ end
+
+ def previous_public_path
+ @previous_public_path ||= File.join(pages_path, "public.#{SecureRandom.hex}")
+ end
+
+ def ref
+ build.ref
+ end
+
+ def artifacts
+ build.artifacts_file.path
+ end
+
+ def latest_sha
+ project.commit(build.ref).try(:sha).to_s
+ end
+
+ def sha
+ build.sha
+ end
+ end
+end
diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb
index 8a6af8d8ada..55d9cb13ae4 100644
--- a/app/services/projects/update_service.rb
+++ b/app/services/projects/update_service.rb
@@ -9,7 +9,7 @@ module Projects
Gitlab::VisibilityLevel.allowed_for?(current_user, new_visibility)
deny_visibility_level(project, new_visibility)
- return project
+ return error('Visibility level unallowed')
end
end
@@ -22,7 +22,13 @@ module Projects
if project.update_attributes(params.except(:default_branch))
if project.previous_changes.include?('path')
project.rename_repo
+ else
+ system_hook_service.execute_hooks_for(project, :update)
end
+
+ success
+ else
+ error('Project could not be updated')
end
end
end
diff --git a/app/services/search/global_service.rb b/app/services/search/global_service.rb
index aa9837038a6..781cd13b44b 100644
--- a/app/services/search/global_service.rb
+++ b/app/services/search/global_service.rb
@@ -9,7 +9,10 @@ module Search
def execute
group = Group.find_by(id: params[:group_id]) if params[:group_id].present?
projects = ProjectsFinder.new.execute(current_user)
- projects = projects.in_namespace(group.id) if group
+
+ if group
+ projects = projects.inside_path(group.full_path)
+ end
Gitlab::SearchResults.new(current_user, projects, params[:search])
end
diff --git a/app/services/slash_commands/interpret_service.rb b/app/services/slash_commands/interpret_service.rb
index d75c5b1800e..3e0a85cf059 100644
--- a/app/services/slash_commands/interpret_service.rb
+++ b/app/services/slash_commands/interpret_service.rb
@@ -2,7 +2,7 @@ module SlashCommands
class InterpretService < BaseService
include Gitlab::SlashCommands::Dsl
- attr_reader :issuable
+ attr_reader :issuable, :options
# Takes a text and interprets the commands that are extracted from it.
# Returns the content without commands, and hash of changes to be applied to a record.
@@ -13,7 +13,8 @@ module SlashCommands
opts = {
issuable: issuable,
current_user: current_user,
- project: project
+ project: project,
+ params: params
}
content, commands = extractor.extract_commands(content, opts)
@@ -58,6 +59,17 @@ module SlashCommands
@updates[:state_event] = 'reopen'
end
+ desc 'Merge (when build succeeds)'
+ condition do
+ last_diff_sha = params && params[:merge_request_diff_head_sha]
+ issuable.is_a?(MergeRequest) &&
+ issuable.persisted? &&
+ issuable.mergeable_with_slash_command?(current_user, autocomplete_precheck: !last_diff_sha, last_diff_sha: last_diff_sha)
+ end
+ command :merge do
+ @updates[:merge] = params[:merge_request_diff_head_sha]
+ end
+
desc 'Change title'
params '<New title>'
condition do
@@ -243,11 +255,67 @@ module SlashCommands
@updates[:wip_event] = issuable.work_in_progress? ? 'unwip' : 'wip'
end
+ desc 'Set time estimate'
+ params '<1w 3d 2h 14m>'
+ condition do
+ current_user.can?(:"admin_#{issuable.to_ability_name}", project)
+ end
+ command :estimate do |raw_duration|
+ time_estimate = Gitlab::TimeTrackingFormatter.parse(raw_duration)
+
+ if time_estimate
+ @updates[:time_estimate] = time_estimate
+ end
+ end
+
+ desc 'Add or substract spent time'
+ params '<1h 30m | -1h 30m>'
+ condition do
+ current_user.can?(:"admin_#{issuable.to_ability_name}", issuable)
+ end
+ command :spend do |raw_duration|
+ time_spent = Gitlab::TimeTrackingFormatter.parse(raw_duration)
+
+ if time_spent
+ @updates[:spend_time] = { duration: time_spent, user: current_user }
+ end
+ end
+
+ desc 'Remove time estimate'
+ condition do
+ issuable.persisted? &&
+ current_user.can?(:"admin_#{issuable.to_ability_name}", project)
+ end
+ command :remove_estimate do
+ @updates[:time_estimate] = 0
+ end
+
+ desc 'Remove spent time'
+ condition do
+ issuable.persisted? &&
+ current_user.can?(:"admin_#{issuable.to_ability_name}", project)
+ end
+ command :remove_time_spent do
+ @updates[:spend_time] = { duration: :reset, user: current_user }
+ end
+
# This is a dummy command, so that it appears in the autocomplete commands
desc 'CC'
params '@user'
command :cc
+ desc 'Defines target branch for MR'
+ params '<Local branch name>'
+ condition do
+ issuable.respond_to?(:target_branch) &&
+ (current_user.can?(:"update_#{issuable.to_ability_name}", issuable) ||
+ issuable.new_record?)
+ end
+ command :target_branch do |target_branch_param|
+ branch_name = target_branch_param.strip
+ @updates[:target_branch] = branch_name if project.repository.branch_names.include?(branch_name)
+ end
+
def find_label_ids(labels_param)
label_ids_by_reference = extract_references(labels_param, :label).map(&:id)
labels_ids_by_name = LabelsFinder.new(current_user, project_id: project.id, name: labels_param.split).execute.select(:id)
diff --git a/app/services/spam_service.rb b/app/services/spam_service.rb
index 48903291799..024a7c19d33 100644
--- a/app/services/spam_service.rb
+++ b/app/services/spam_service.rb
@@ -1,5 +1,6 @@
class SpamService
attr_accessor :spammable, :request, :options
+ attr_reader :spam_log
def initialize(spammable, request = nil)
@spammable = spammable
@@ -63,7 +64,7 @@ class SpamService
end
def create_spam_log(api)
- SpamLog.create(
+ @spam_log = SpamLog.create!(
{
user_id: spammable_owner_id,
title: spammable.spam_title,
diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb
index 7613ecd5021..87ba72cf991 100644
--- a/app/services/system_note_service.rb
+++ b/app/services/system_note_service.rb
@@ -109,6 +109,61 @@ module SystemNoteService
create_note(noteable: noteable, project: project, author: author, note: body)
end
+ # Called when the estimated time of a Noteable is changed
+ #
+ # noteable - Noteable object
+ # project - Project owning noteable
+ # author - User performing the change
+ # time_estimate - Estimated time
+ #
+ # Example Note text:
+ #
+ # "removed time estimate"
+ #
+ # "changed time estimate to 3d 5h"
+ #
+ # Returns the created Note object
+
+ def change_time_estimate(noteable, project, author)
+ parsed_time = Gitlab::TimeTrackingFormatter.output(noteable.time_estimate)
+ body = if noteable.time_estimate == 0
+ "removed time estimate"
+ else
+ "changed time estimate to #{parsed_time}"
+ end
+
+ create_note(noteable: noteable, project: project, author: author, note: body)
+ end
+
+ # Called when the spent time of a Noteable is changed
+ #
+ # noteable - Noteable object
+ # project - Project owning noteable
+ # author - User performing the change
+ # time_spent - Spent time
+ #
+ # Example Note text:
+ #
+ # "removed time spent"
+ #
+ # "added 2h 30m of time spent"
+ #
+ # Returns the created Note object
+
+ def change_time_spent(noteable, project, author)
+ time_spent = noteable.time_spent
+
+ if time_spent == :reset
+ body = "removed time spent"
+ else
+ parsed_time = Gitlab::TimeTrackingFormatter.output(time_spent.abs)
+ action = time_spent > 0 ? 'added' : 'subtracted'
+ body = "#{action} #{parsed_time} of time spent"
+ end
+
+ create_note(noteable: noteable, project: project, author: author, note: body)
+ end
+
# Called when the status of a Noteable is changed
#
# noteable - Noteable object
@@ -157,6 +212,12 @@ module SystemNoteService
create_note(noteable: noteable, project: project, author: author, note: body)
end
+ def add_merge_request_wip_from_commit(noteable, project, author, commit)
+ body = "marked as a **Work In Progress** from #{commit.to_reference(project)}"
+
+ create_note(noteable: noteable, project: project, author: author, note: body)
+ end
+
def self.resolve_all_discussions(merge_request, project, author)
body = "resolved all discussions"
@@ -164,7 +225,7 @@ module SystemNoteService
end
def discussion_continued_in_issue(discussion, project, author, issue)
- body = "Added #{issue.to_reference} to continue this discussion"
+ body = "created #{issue.to_reference} to continue this discussion"
note_attributes = discussion.reply_attributes.merge(project: project, author: author, note: body)
note_attributes[:type] = note_attributes.delete(:note_type)
@@ -203,7 +264,7 @@ module SystemNoteService
#
# Example Note text:
#
- # "made the issue confidential"
+ # "made the issue confidential"
#
# Returns the created Note object
def change_issue_confidentiality(issue, project, author)
@@ -324,6 +385,7 @@ module SystemNoteService
# Returns Boolean
def cross_reference_disallowed?(noteable, mentioner)
return true if noteable.is_a?(ExternalIssue) && !noteable.project.jira_tracker_active?
+ return true if noteable.is_a?(Issuable) && (noteable.try(:closed?) || noteable.try(:merged?))
return false unless mentioner.is_a?(MergeRequest)
return false unless noteable.is_a?(Commit)
diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb
index f8e6b2ef094..1bd6ce416ab 100644
--- a/app/services/todo_service.rb
+++ b/app/services/todo_service.rb
@@ -98,10 +98,12 @@ class TodoService
# When a build fails on the HEAD of a merge request we should:
#
- # * create a todo for that user to fix it
+ # * create a todo for author of MR to fix it
+ # * create a todo for merge_user to keep an eye on it
#
def merge_request_build_failed(merge_request)
- create_build_failed_todo(merge_request)
+ create_build_failed_todo(merge_request, merge_request.author)
+ create_build_failed_todo(merge_request, merge_request.merge_user) if merge_request.merge_when_build_succeeds?
end
# When a new commit is pushed to a merge request we should:
@@ -115,11 +117,21 @@ class TodoService
# When a build is retried to a merge request we should:
#
# * mark all pending todos related to the merge request for the author as done
+ # * mark all pending todos related to the merge request for the merge_user as done
#
def merge_request_build_retried(merge_request)
mark_pending_todos_as_done(merge_request, merge_request.author)
+ mark_pending_todos_as_done(merge_request, merge_request.merge_user) if merge_request.merge_when_build_succeeds?
end
-
+
+ # When a merge request could not be automatically merged due to its unmergeable state we should:
+ #
+ # * create a todo for a merge_user
+ #
+ def merge_request_became_unmergeable(merge_request)
+ create_unmergeable_todo(merge_request, merge_request.merge_user) if merge_request.merge_when_build_succeeds?
+ end
+
# When create a note we should:
#
# * mark all pending todos related to the noteable for the note author as done
@@ -236,10 +248,14 @@ class TodoService
create_todos(mentioned_users, attributes)
end
- def create_build_failed_todo(merge_request)
- author = merge_request.author
- attributes = attributes_for_todo(merge_request.project, merge_request, author, Todo::BUILD_FAILED)
- create_todos(author, attributes)
+ def create_build_failed_todo(merge_request, todo_author)
+ attributes = attributes_for_todo(merge_request.project, merge_request, todo_author, Todo::BUILD_FAILED)
+ create_todos(todo_author, attributes)
+ end
+
+ def create_unmergeable_todo(merge_request, merge_user)
+ attributes = attributes_for_todo(merge_request.project, merge_request, merge_user, Todo::UNMERGEABLE)
+ create_todos(merge_user, attributes)
end
def attributes_for_target(target)
diff --git a/app/services/user_project_access_changed_service.rb b/app/services/user_project_access_changed_service.rb
index 2469b4f0d7c..d7a6804ee88 100644
--- a/app/services/user_project_access_changed_service.rb
+++ b/app/services/user_project_access_changed_service.rb
@@ -4,6 +4,6 @@ class UserProjectAccessChangedService
end
def execute
- AuthorizedProjectsWorker.bulk_perform_async(@user_ids.map { |id| [id] })
+ AuthorizedProjectsWorker.bulk_perform_and_wait(@user_ids.map { |id| [id] })
end
end
diff --git a/app/services/users/refresh_authorized_projects_service.rb b/app/services/users/refresh_authorized_projects_service.rb
index 7d38ac3a374..fad741531ea 100644
--- a/app/services/users/refresh_authorized_projects_service.rb
+++ b/app/services/users/refresh_authorized_projects_service.rb
@@ -26,8 +26,26 @@ module Users
user.reload
end
- # This method returns the updated User object.
def execute
+ lease_key = "refresh_authorized_projects:#{user.id}"
+ lease = Gitlab::ExclusiveLease.new(lease_key, timeout: LEASE_TIMEOUT)
+
+ until uuid = lease.try_obtain
+ # Keep trying until we obtain the lease. If we don't do so we may end up
+ # not updating the list of authorized projects properly. To prevent
+ # hammering Redis too much we'll wait for a bit between retries.
+ sleep(1)
+ end
+
+ begin
+ execute_without_lease
+ ensure
+ Gitlab::ExclusiveLease.cancel(lease_key, uuid)
+ end
+ end
+
+ # This method returns the updated User object.
+ def execute_without_lease
current = current_authorizations_per_project
fresh = fresh_access_levels_per_project
@@ -35,7 +53,7 @@ module Users
# rows not in the new list or with a different access level should be
# removed.
if !fresh[project_id] || fresh[project_id] != row.access_level
- array << row.id
+ array << row.project_id
end
end
@@ -47,26 +65,7 @@ module Users
end
end
- update_with_lease(remove, add)
- end
-
- # Updates the list of authorizations using an exclusive lease.
- def update_with_lease(remove = [], add = [])
- lease_key = "refresh_authorized_projects:#{user.id}"
- lease = Gitlab::ExclusiveLease.new(lease_key, timeout: LEASE_TIMEOUT)
-
- until uuid = lease.try_obtain
- # Keep trying until we obtain the lease. If we don't do so we may end up
- # not updating the list of authorized projects properly. To prevent
- # hammering Redis too much we'll wait for a bit between retries.
- sleep(1)
- end
-
- begin
- update_authorizations(remove, add)
- ensure
- Gitlab::ExclusiveLease.cancel(lease_key, uuid)
- end
+ update_authorizations(remove, add)
end
# Updates the list of authorizations for the current user.
@@ -74,7 +73,7 @@ module Users
# remove - The IDs of the authorization rows to remove.
# add - Rows to insert in the form `[user id, project id, access level]`
def update_authorizations(remove = [], add = [])
- return if remove.empty? && add.empty?
+ return if remove.empty? && add.empty? && user.authorized_projects_populated
User.transaction do
user.remove_project_authorizations(remove) unless remove.empty?
@@ -100,7 +99,7 @@ module Users
end
def current_authorizations
- user.project_authorizations.select(:id, :project_id, :access_level)
+ user.project_authorizations.select(:project_id, :access_level)
end
def fresh_authorizations
@@ -119,7 +118,8 @@ module Users
user.personal_projects.select("#{user.id} AS user_id, projects.id AS project_id, #{Gitlab::Access::MASTER} AS access_level"),
user.groups_projects.select_for_project_authorization,
user.projects.select_for_project_authorization,
- user.groups.joins(:shared_projects).select_for_project_authorization
+ user.groups.joins(:shared_projects).select_for_project_authorization,
+ user.nested_projects.select_for_project_authorization
]
Gitlab::SQL::Union.new(relations)
diff --git a/app/services/validate_new_branch_service.rb b/app/services/validate_new_branch_service.rb
new file mode 100644
index 00000000000..2f61be184ce
--- /dev/null
+++ b/app/services/validate_new_branch_service.rb
@@ -0,0 +1,22 @@
+require_relative 'base_service'
+
+class ValidateNewBranchService < BaseService
+ def execute(branch_name)
+ valid_branch = Gitlab::GitRefValidator.validate(branch_name)
+
+ unless valid_branch
+ return error('Branch name is invalid')
+ end
+
+ repository = project.repository
+ existing_branch = repository.find_branch(branch_name)
+
+ if existing_branch
+ return error('Branch already exists')
+ end
+
+ success
+ rescue GitHooksService::PreReceiveError => ex
+ error(ex.message)
+ end
+end
diff --git a/app/uploaders/avatar_uploader.rb b/app/uploaders/avatar_uploader.rb
index a1ecb7bc00b..265cea2d2c6 100644
--- a/app/uploaders/avatar_uploader.rb
+++ b/app/uploaders/avatar_uploader.rb
@@ -10,4 +10,15 @@ class AvatarUploader < GitlabUploader
def exists?
model.avatar.file && model.avatar.file.exists?
end
+
+ # We set move_to_store and move_to_cache to 'false' to prevent stealing
+ # the avatar file from a project when forking it.
+ # https://gitlab.com/gitlab-org/gitlab-ce/issues/26158
+ def move_to_store
+ false
+ end
+
+ def move_to_cache
+ false
+ end
end
diff --git a/app/validators/certificate_key_validator.rb b/app/validators/certificate_key_validator.rb
new file mode 100644
index 00000000000..098b16017d2
--- /dev/null
+++ b/app/validators/certificate_key_validator.rb
@@ -0,0 +1,25 @@
+# UrlValidator
+#
+# Custom validator for private keys.
+#
+# class Project < ActiveRecord::Base
+# validates :certificate_key, certificate_key: true
+# end
+#
+class CertificateKeyValidator < ActiveModel::EachValidator
+ def validate_each(record, attribute, value)
+ unless valid_private_key_pem?(value)
+ record.errors.add(attribute, "must be a valid PEM private key")
+ end
+ end
+
+ private
+
+ def valid_private_key_pem?(value)
+ return false unless value
+ pkey = OpenSSL::PKey::RSA.new(value)
+ pkey.private?
+ rescue OpenSSL::PKey::PKeyError
+ false
+ end
+end
diff --git a/app/validators/certificate_validator.rb b/app/validators/certificate_validator.rb
new file mode 100644
index 00000000000..e3d18097f71
--- /dev/null
+++ b/app/validators/certificate_validator.rb
@@ -0,0 +1,24 @@
+# UrlValidator
+#
+# Custom validator for private keys.
+#
+# class Project < ActiveRecord::Base
+# validates :certificate_key, certificate: true
+# end
+#
+class CertificateValidator < ActiveModel::EachValidator
+ def validate_each(record, attribute, value)
+ unless valid_certificate_pem?(value)
+ record.errors.add(attribute, "must be a valid PEM certificate")
+ end
+ end
+
+ private
+
+ def valid_certificate_pem?(value)
+ return false unless value
+ OpenSSL::X509::Certificate.new(value).present?
+ rescue OpenSSL::X509::CertificateError
+ false
+ end
+end
diff --git a/app/validators/project_path_validator.rb b/app/validators/project_path_validator.rb
index 927c67b65b0..36279daa743 100644
--- a/app/validators/project_path_validator.rb
+++ b/app/validators/project_path_validator.rb
@@ -14,7 +14,8 @@ class ProjectPathValidator < ActiveModel::EachValidator
# without tree as reserved name routing can match 'group/project' as group name,
# 'tree' as project name and 'deploy_keys' as route.
#
- RESERVED = (NamespaceValidator::RESERVED +
+ RESERVED = (NamespaceValidator::RESERVED -
+ %w[dashboard help ci admin search notes services assets profile public] +
%w[tree commits wikis new edit create update logs_tree
preview blob blame raw files create_dir find_file]).freeze
diff --git a/app/views/abuse_report_mailer/notify.html.haml b/app/views/abuse_report_mailer/notify.html.haml
index 2741eb44357..d50b4071745 100644
--- a/app/views/abuse_report_mailer/notify.html.haml
+++ b/app/views/abuse_report_mailer/notify.html.haml
@@ -1,7 +1,7 @@
%p
- #{link_to @abuse_report.user.name, user_url(@abuse_report.user)}
- (@#{@abuse_report.user.username}) was reported for abuse by
- #{link_to @abuse_report.reporter.name, user_url(@abuse_report.reporter)}
+ #{link_to @abuse_report.user.name, user_url(@abuse_report.user)}
+ (@#{@abuse_report.user.username}) was reported for abuse by
+ #{link_to @abuse_report.reporter.name, user_url(@abuse_report.reporter)}
(@#{@abuse_report.reporter.username}).
%blockquote
diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml
index 7accd2529af..816035ec442 100644
--- a/app/views/admin/application_settings/_form.html.haml
+++ b/app/views/admin/application_settings/_form.html.haml
@@ -187,6 +187,14 @@
.help-block Markdown enabled
%fieldset
+ %legend Pages
+ .form-group
+ = f.label :max_pages_size, 'Maximum size of pages (MB)', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :max_pages_size, class: 'form-control'
+ .help-block Zero for unlimited
+
+ %fieldset
%legend Continuous Integration
.form-group
.col-sm-offset-2.col-sm-10
@@ -204,7 +212,7 @@
.col-sm-10
= f.number_field :max_artifacts_size, class: 'form-control'
.help-block
- Set the maximum file size each build's artifacts can have
+ Set the maximum file size each jobs's artifacts can have
= link_to "(?)", help_page_path("user/admin_area/settings/continuous_integration", anchor: "maximum-artifacts-size")
- if Gitlab.config.registry.enabled
@@ -321,7 +329,7 @@
= f.text_field :recaptcha_site_key, class: 'form-control'
.help-block
Generate site and private keys at
- %a{ href: 'http://www.google.com/recaptcha', target: 'blank'} http://www.google.com/recaptcha
+ %a{ href: 'http://www.google.com/recaptcha', target: 'blank' } http://www.google.com/recaptcha
.form-group
= f.label :recaptcha_private_key, 'reCAPTCHA Private Key', class: 'control-label col-sm-2'
@@ -342,7 +350,7 @@
= f.text_field :akismet_api_key, class: 'form-control'
.help-block
Generate API key at
- %a{ href: 'http://www.akismet.com', target: 'blank'} http://www.akismet.com
+ %a{ href: 'http://www.akismet.com', target: 'blank' } http://www.akismet.com
%fieldset
%legend Abuse reports
@@ -421,6 +429,23 @@
= link_to "Koding administration documentation", help_page_path("administration/integration/koding")
%fieldset
+ %legend PlantUML
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :plantuml_enabled do
+ = f.check_box :plantuml_enabled
+ Enable PlantUML
+ .form-group
+ = f.label :plantuml_url, 'PlantUML URL', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.text_field :plantuml_url, class: 'form-control', placeholder: 'http://gitlab.your-plantuml-instance.com:8080'
+ .help-block
+ Allow rendering of
+ = link_to "PlantUML", "http://plantuml.com"
+ diagrams in Asciidoc documents using an external PlantUML service.
+
+ %fieldset
%legend Usage statistics
.form-group
.col-sm-offset-2.col-sm-10
@@ -492,5 +517,15 @@
.help-block
Number of Git pushes after which 'git gc' is run.
+ %fieldset
+ %legend Web terminal
+ .form-group
+ = f.label :terminal_max_session_time, 'Max session time', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :terminal_max_session_time, class: 'form-control'
+ .help-block
+ Maximum time for web terminal websocket connection (in seconds).
+ Set to 0 for unlimited time.
+
.form-actions
= f.submit 'Save', class: 'btn btn-save'
diff --git a/app/views/admin/applications/_delete_form.html.haml b/app/views/admin/applications/_delete_form.html.haml
index 042971e1eed..82781f6716d 100644
--- a/app/views/admin/applications/_delete_form.html.haml
+++ b/app/views/admin/applications/_delete_form.html.haml
@@ -1,4 +1,4 @@
- submit_btn_css ||= 'btn btn-link btn-remove btn-sm'
= form_tag admin_application_path(application) do
- %input{:name => "_method", :type => "hidden", :value => "delete"}/
+ %input{ :name => "_method", :type => "hidden", :value => "delete" }/
= submit_tag 'Destroy', onclick: "return confirm('Are you sure?')", class: submit_btn_css
diff --git a/app/views/admin/applications/index.html.haml b/app/views/admin/applications/index.html.haml
index f8cd98f0ec4..b3a3b4c1d45 100644
--- a/app/views/admin/applications/index.html.haml
+++ b/app/views/admin/applications/index.html.haml
@@ -15,7 +15,7 @@
%th
%tbody.oauth-applications
- @applications.each do |application|
- %tr{:id => "application_#{application.id}"}
+ %tr{ :id => "application_#{application.id}" }
%td= link_to application.name, admin_application_path(application)
%td= application.redirect_uri
%td= application.access_tokens.map(&:resource_owner_id).uniq.count
diff --git a/app/views/admin/background_jobs/show.html.haml b/app/views/admin/background_jobs/show.html.haml
index 05855db963a..4f982a6e369 100644
--- a/app/views/admin/background_jobs/show.html.haml
+++ b/app/views/admin/background_jobs/show.html.haml
@@ -43,4 +43,4 @@
.panel.panel-default
- %iframe{src: sidekiq_path, width: '100%', height: 970, style: "border: none"}
+ %iframe{ src: sidekiq_path, width: '100%', height: 970, style: "border: none" }
diff --git a/app/views/admin/broadcast_messages/_form.html.haml b/app/views/admin/broadcast_messages/_form.html.haml
index 3132d157f29..2269fb1fd8c 100644
--- a/app/views/admin/broadcast_messages/_form.html.haml
+++ b/app/views/admin/broadcast_messages/_form.html.haml
@@ -4,7 +4,7 @@
- if @broadcast_message.message.present?
= render_broadcast_message(@broadcast_message)
- else
- = "Your message here"
+ Your message here
= form_for [:admin, @broadcast_message], html: { class: 'broadcast-message-form form-horizontal js-quick-submit js-requires-input'} do |f|
= form_errors(@broadcast_message)
diff --git a/app/views/admin/broadcast_messages/index.html.haml b/app/views/admin/broadcast_messages/index.html.haml
index c05538a393c..4f2ae081d7a 100644
--- a/app/views/admin/broadcast_messages/index.html.haml
+++ b/app/views/admin/broadcast_messages/index.html.haml
@@ -10,7 +10,7 @@
%br.clearfix
--if @broadcast_messages.any?
+- if @broadcast_messages.any?
%table.table
%thead
%tr
diff --git a/app/views/admin/builds/index.html.haml b/app/views/admin/builds/index.html.haml
index 5e3f105d41f..66d633119c2 100644
--- a/app/views/admin/builds/index.html.haml
+++ b/app/views/admin/builds/index.html.haml
@@ -12,7 +12,7 @@
= link_to 'Cancel all', cancel_all_admin_builds_path, data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post
.row-content-block.second-block
- #{(@scope || 'all').capitalize} builds
+ #{(@scope || 'all').capitalize} jobs
%ul.content-list.builds-content-list.admin-builds-table
= render "projects/builds/table", builds: @builds, admin: true
diff --git a/app/views/admin/dashboard/_head.html.haml b/app/views/admin/dashboard/_head.html.haml
index b5f96363230..7893c1dee97 100644
--- a/app/views/admin/dashboard/_head.html.haml
+++ b/app/views/admin/dashboard/_head.html.haml
@@ -20,9 +20,9 @@
%span
Groups
= nav_link path: 'builds#index' do
- = link_to admin_builds_path, title: 'Builds' do
+ = link_to admin_builds_path, title: 'Jobs' do
%span
- Builds
+ Jobs
= nav_link path: ['runners#index', 'runners#show'] do
= link_to admin_runners_path, title: 'Runners' do
%span
diff --git a/app/views/admin/deploy_keys/index.html.haml b/app/views/admin/deploy_keys/index.html.haml
index 149593e7f46..7b71bb5b287 100644
--- a/app/views/admin/deploy_keys/index.html.haml
+++ b/app/views/admin/deploy_keys/index.html.haml
@@ -1,27 +1,34 @@
- page_title "Deploy Keys"
-.panel.panel-default.prepend-top-default
- .panel-heading
- Public deploy keys (#{@deploy_keys.count})
- .controls
- = link_to 'New Deploy Key', new_admin_deploy_key_path, class: "btn btn-new btn-sm"
- - if @deploy_keys.any?
- .table-holder
- %table.table
- %thead.panel-heading
+
+%h3.page-title.deploy-keys-title
+ Public deploy keys (#{@deploy_keys.count})
+ .pull-right
+ = link_to 'New Deploy Key', new_admin_deploy_key_path, class: 'btn btn-new btn-sm btn-inverted'
+
+- if @deploy_keys.any?
+ .table-holder.deploy-keys-list
+ %table.table
+ %thead
+ %tr
+ %th.col-sm-2 Title
+ %th.col-sm-4 Fingerprint
+ %th.col-sm-2 Write access allowed
+ %th.col-sm-2 Added at
+ %th.col-sm-2
+ %tbody
+ - @deploy_keys.each do |deploy_key|
%tr
- %th Title
- %th Fingerprint
- %th Added at
- %th
- %tbody
- - @deploy_keys.each do |deploy_key|
- %tr
- %td
- %strong= deploy_key.title
- %td
- %code.key-fingerprint= deploy_key.fingerprint
- %td
- %span.cgray
- added #{time_ago_with_tooltip(deploy_key.created_at)}
- %td
- = link_to 'Remove', admin_deploy_key_path(deploy_key), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-sm btn-remove delete-key pull-right"
+ %td
+ %strong= deploy_key.title
+ %td
+ %code.key-fingerprint= deploy_key.fingerprint
+ %td
+ - if deploy_key.can_push?
+ Yes
+ - else
+ No
+ %td
+ %span.cgray
+ added #{time_ago_with_tooltip(deploy_key.created_at)}
+ %td
+ = link_to 'Remove', admin_deploy_key_path(deploy_key), data: { confirm: 'Are you sure?'}, method: :delete, class: 'btn btn-sm btn-remove delete-key pull-right'
diff --git a/app/views/admin/deploy_keys/new.html.haml b/app/views/admin/deploy_keys/new.html.haml
index 5c410a695bf..a064efc231f 100644
--- a/app/views/admin/deploy_keys/new.html.haml
+++ b/app/views/admin/deploy_keys/new.html.haml
@@ -16,6 +16,14 @@
Paste a machine public key here. Read more about how to generate it
= link_to "here", help_page_path("ssh/README")
= f.text_area :key, class: "form-control thin_area", rows: 5
+ .form-group
+ .control-label
+ .col-sm-10
+ = f.label :can_push do
+ = f.check_box :can_push
+ %strong Write access allowed
+ %p.light.append-bottom-0
+ Allow this key to push to repository as well? (Default only allows pull access.)
.form-actions
= f.submit 'Create', class: "btn-create btn"
diff --git a/app/views/admin/groups/_group.html.haml b/app/views/admin/groups/_group.html.haml
index cf28f92853e..e3a77dfdf10 100644
--- a/app/views/admin/groups/_group.html.haml
+++ b/app/views/admin/groups/_group.html.haml
@@ -5,6 +5,9 @@
= link_to 'Edit', admin_group_edit_path(group), id: "edit_#{dom_id(group)}", class: 'btn'
= link_to 'Delete', [:admin, group], data: { confirm: "Are you sure you want to remove #{group.name}?" }, method: :delete, class: 'btn btn-remove'
.stats
+ %span.badge
+ = storage_counter(group.storage_size)
+
%span
= icon('bookmark')
= number_with_delimiter(group.projects.count)
@@ -13,7 +16,7 @@
= icon('users')
= number_with_delimiter(group.users.count)
- %span.visibility-icon.has-tooltip{data: { container: 'body', placement: 'left' }, title: visibility_icon_description(group)}
+ %span.visibility-icon.has-tooltip{ data: { container: 'body', placement: 'left' }, title: visibility_icon_description(group) }
= visibility_level_icon(group.visibility_level, fw: false)
.avatar-container.s40
diff --git a/app/views/admin/groups/index.html.haml b/app/views/admin/groups/index.html.haml
index 794f910a61f..07775247cfd 100644
--- a/app/views/admin/groups/index.html.haml
+++ b/app/views/admin/groups/index.html.haml
@@ -27,6 +27,8 @@
= sort_title_recently_updated
= link_to admin_groups_path(sort: sort_value_oldest_updated, name: project_name) do
= sort_title_oldest_updated
+ = link_to admin_groups_path(sort: sort_value_largest_group, name: project_name) do
+ = sort_title_largest_group
= link_to new_admin_group_path, class: "btn btn-new" do
New Group
%ul.content-list
diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml
index 7b0175af214..30b3fabdd7e 100644
--- a/app/views/admin/groups/show.html.haml
+++ b/app/views/admin/groups/show.html.haml
@@ -39,6 +39,18 @@
= @group.created_at.to_s(:medium)
%li
+ %span.light Storage:
+ %strong= storage_counter(@group.storage_size)
+ (
+ = storage_counter(@group.repository_size)
+ repositories,
+ = storage_counter(@group.build_artifacts_size)
+ build artifacts,
+ = storage_counter(@group.lfs_objects_size)
+ LFS
+ )
+
+ %li
%span.light Group Git LFS status:
%strong
= group_lfs_status(@group)
@@ -55,8 +67,8 @@
%li
%strong
= link_to project.name_with_namespace, [:admin, project.namespace.becomes(Namespace), project]
- %span.label.label-gray
- = repository_size(project)
+ %span.badge
+ = storage_counter(project.statistics.storage_size)
%span.pull-right.light
%span.monospace= project.path_with_namespace + ".git"
.panel-footer
@@ -73,8 +85,8 @@
%li
%strong
= link_to project.name_with_namespace, [:admin, project.namespace.becomes(Namespace), project]
- %span.label.label-gray
- = repository_size(project)
+ %span.badge
+ = storage_counter(project.statistics.storage_size)
%span.pull-right.light
%span.monospace= project.path_with_namespace + ".git"
@@ -91,7 +103,7 @@
= form_tag admin_group_members_update_path(@group), id: "new_project_member", class: "bulk_import", method: :put do
%div
= users_select_tag(:user_ids, multiple: true, email_user: true, scope: :all)
- %div.prepend-top-10
+ .prepend-top-10
= select_tag :access_level, options_for_select(GroupMember.access_level_roles), class: "project-access-select select2"
%hr
= button_tag 'Add users to group', class: "btn btn-create"
diff --git a/app/views/admin/hooks/index.html.haml b/app/views/admin/hooks/index.html.haml
index c217490963f..551edf14361 100644
--- a/app/views/admin/hooks/index.html.haml
+++ b/app/views/admin/hooks/index.html.haml
@@ -29,7 +29,7 @@
System hook will be triggered on set of events like creating project
or adding ssh key. But you can also enable extra triggers like Push events.
- %div.prepend-top-default
+ .prepend-top-default
= f.check_box :push_events, class: 'pull-left'
.prepend-left-20
= f.label :push_events, class: 'list-label' do
@@ -54,7 +54,7 @@
= f.submit "Add System Hook", class: "btn btn-create"
%hr
--if @hooks.any?
+- if @hooks.any?
.panel.panel-default
.panel-heading
System hooks (#{@hooks.count})
@@ -70,4 +70,3 @@
- if hook.send(trigger)
%span.label.label-gray= trigger.titleize
%span.label.label-gray SSL Verification: #{hook.enable_ssl_verification ? "enabled" : "disabled"}
-
diff --git a/app/views/admin/identities/_identity.html.haml b/app/views/admin/identities/_identity.html.haml
index 7362d904b94..8c658905bd6 100644
--- a/app/views/admin/identities/_identity.html.haml
+++ b/app/views/admin/identities/_identity.html.haml
@@ -1,6 +1,6 @@
%tr
%td
- = "#{Gitlab::OAuth::Provider.label_for(identity.provider)} (#{identity.provider})"
+ #{Gitlab::OAuth::Provider.label_for(identity.provider)} (#{identity.provider})
%td
= identity.extern_uid
%td
diff --git a/app/views/admin/labels/_label.html.haml b/app/views/admin/labels/_label.html.haml
index be224d66855..77b174fbb27 100644
--- a/app/views/admin/labels/_label.html.haml
+++ b/app/views/admin/labels/_label.html.haml
@@ -1,4 +1,4 @@
-%li{id: dom_id(label)}
+%li{ id: dom_id(label) }
.label-row
= render_colored_label(label, tooltip: false)
= markdown_field(label, :description)
diff --git a/app/views/admin/projects/index.html.haml b/app/views/admin/projects/index.html.haml
index 8bc7dc7dd51..cf8d438670b 100644
--- a/app/views/admin/projects/index.html.haml
+++ b/app/views/admin/projects/index.html.haml
@@ -27,7 +27,7 @@
= icon("search", class: "search-icon")
.dropdown
- - toggle_text = 'Search for Namespace'
+ - toggle_text = 'Namespace'
- if params[:namespace_id].present?
- namespace = Namespace.find(params[:namespace_id])
- toggle_text = "#{namespace.kind}: #{namespace.path}"
@@ -37,8 +37,10 @@
= dropdown_filter("Search for Namespace")
= dropdown_content
= dropdown_loading
-
- = button_tag "Search", class: "btn btn-primary btn-search"
+ = render 'shared/projects/dropdown'
+ = link_to new_project_path, class: 'btn btn-new' do
+ New Project
+ = button_tag "Search", class: "btn btn-primary btn-search hide"
%ul.nav-links
- opts = params[:visibility_level].present? ? {} : { page: admin_projects_path }
@@ -56,11 +58,6 @@
= link_to admin_projects_path(visibility_level: Gitlab::VisibilityLevel::PUBLIC) do
Public
- .nav-controls
- = render 'shared/projects/dropdown'
- = link_to new_project_path, class: 'btn btn-new' do
- New Project
-
.projects-list-holder
- if @projects.any?
%ul.projects-list.content-list
@@ -69,8 +66,8 @@
.controls
- if project.archived
%span.label.label-warning archived
- %span.label.label-gray
- = repository_size(project)
+ %span.badge
+ = storage_counter(project.statistics.storage_size)
= link_to 'Edit', edit_namespace_project_path(project.namespace, project), id: "edit_#{dom_id(project)}", class: "btn"
= link_to 'Delete', [project.namespace.becomes(Namespace), project], data: { confirm: remove_project_message(project) }, method: :delete, class: "btn btn-remove"
.title
diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml
index 6c7c3c48604..2967da6e692 100644
--- a/app/views/admin/projects/show.html.haml
+++ b/app/views/admin/projects/show.html.haml
@@ -65,9 +65,16 @@
= @project.repository.path_to_repo
%li
- %span.light Size
- %strong
- = repository_size(@project)
+ %span.light Storage:
+ %strong= storage_counter(@project.statistics.storage_size)
+ (
+ = storage_counter(@project.statistics.repository_size)
+ repository,
+ = storage_counter(@project.statistics.build_artifacts_size)
+ build artifacts,
+ = storage_counter(@project.statistics.lfs_objects_size)
+ LFS
+ )
%li
%span.light last commit:
diff --git a/app/views/admin/runners/_runner.html.haml b/app/views/admin/runners/_runner.html.haml
index 64893b38c58..975bd950ae1 100644
--- a/app/views/admin/runners/_runner.html.haml
+++ b/app/views/admin/runners/_runner.html.haml
@@ -1,4 +1,4 @@
-%tr{id: dom_id(runner)}
+%tr{ id: dom_id(runner) }
%td
- if runner.shared?
%span.label.label-success shared
diff --git a/app/views/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml
index 37bb6a3b0e0..721bc77cc2f 100644
--- a/app/views/admin/runners/index.html.haml
+++ b/app/views/admin/runners/index.html.haml
@@ -11,7 +11,7 @@
that for future communication.
%br
Registration token is
- %code{ id: 'runners-token' } #{current_application_settings.runners_registration_token}
+ %code#runners-token= current_application_settings.runners_registration_token
.bs-callout.clearfix
.pull-left
@@ -26,7 +26,7 @@
.bs-callout
%p
- A 'Runner' is a process which runs a build.
+ A 'Runner' is a process which runs a job.
You can setup as many Runners as you need.
%br
Runners can be placed on separate users, servers, even on your local machine.
@@ -37,16 +37,16 @@
%ul
%li
%span.label.label-success shared
- \- Runner runs builds from all unassigned projects
+ \- Runner runs jobs from all unassigned projects
%li
%span.label.label-info specific
- \- Runner runs builds from assigned projects
+ \- Runner runs jobs from assigned projects
%li
%span.label.label-warning locked
\- Runner cannot be assigned to other projects
%li
%span.label.label-danger paused
- \- Runner will not receive any new builds
+ \- Runner will not receive any new jobs
.append-bottom-20.clearfix
.pull-left
@@ -68,7 +68,7 @@
%th Runner token
%th Description
%th Projects
- %th Builds
+ %th Jobs
%th Tags
%th Last contact
%th
diff --git a/app/views/admin/runners/show.html.haml b/app/views/admin/runners/show.html.haml
index ca503e35623..dc4116e1ce0 100644
--- a/app/views/admin/runners/show.html.haml
+++ b/app/views/admin/runners/show.html.haml
@@ -11,13 +11,13 @@
- if @runner.shared?
.bs-callout.bs-callout-success
- %h4 This Runner will process builds from ALL UNASSIGNED projects
+ %h4 This Runner will process jobs from ALL UNASSIGNED projects
%p
If you want Runners to build only specific projects, enable them in the table below.
Keep in mind that this is a one way transition.
- else
.bs-callout.bs-callout-info
- %h4 This Runner will process builds only from ASSIGNED projects
+ %h4 This Runner will process jobs only from ASSIGNED projects
%p You can't make this a shared Runner.
%hr
@@ -70,11 +70,11 @@
= paginate @projects, theme: "gitlab"
.col-md-6
- %h4 Recent builds served by this Runner
+ %h4 Recent jobs served by this Runner
%table.table.ci-table.runner-builds
%thead
%tr
- %th Build
+ %th Job
%th Status
%th Project
%th Commit
@@ -100,7 +100,7 @@
%td.build-link
- if project
= link_to ci_status_path(build.pipeline) do
- %strong #{build.pipeline.short_sha}
+ %strong= build.pipeline.short_sha
%td.timestamp
- if build.finished_at
diff --git a/app/views/admin/spam_logs/_spam_log.html.haml b/app/views/admin/spam_logs/_spam_log.html.haml
index 4ce4eab8753..33f6d847782 100644
--- a/app/views/admin/spam_logs/_spam_log.html.haml
+++ b/app/views/admin/spam_logs/_spam_log.html.haml
@@ -14,6 +14,8 @@
%td
= spam_log.via_api? ? 'Y' : 'N'
%td
+ = spam_log.recaptcha_verified ? 'Y' : 'N'
+ %td
= spam_log.noteable_type
%td
= spam_log.title
diff --git a/app/views/admin/spam_logs/index.html.haml b/app/views/admin/spam_logs/index.html.haml
index 0fdd5bd9960..8aaa6379730 100644
--- a/app/views/admin/spam_logs/index.html.haml
+++ b/app/views/admin/spam_logs/index.html.haml
@@ -10,6 +10,7 @@
%th User
%th Source IP
%th API?
+ %th Recaptcha verified?
%th Type
%th Title
%th Description
diff --git a/app/views/admin/system_info/show.html.haml b/app/views/admin/system_info/show.html.haml
index bfc6142067a..2e5f120c4e4 100644
--- a/app/views/admin/system_info/show.html.haml
+++ b/app/views/admin/system_info/show.html.haml
@@ -10,7 +10,7 @@
%h4 CPU
.data
- if @cpus
- %h1= "#{@cpus.length} cores"
+ %h1 #{@cpus.length} cores
- else
= icon('warning', class: 'text-warning')
Unable to collect CPU info
@@ -19,7 +19,7 @@
%h4 Memory
.data
- if @memory
- %h1= "#{number_to_human_size(@memory.active_bytes)} / #{number_to_human_size(@memory.total_bytes)}"
+ %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
@@ -28,6 +28,6 @@
%h4 Disks
.data
- @disks.each do |disk|
- %h1= "#{number_to_human_size(disk[:bytes_used])} / #{number_to_human_size(disk[:bytes_total])}"
- %p= "#{disk[:disk_name]}"
- %p= "#{disk[:mount_path]}"
+ %h1 #{number_to_human_size(disk[:bytes_used])} / #{number_to_human_size(disk[:bytes_total])}
+ %p= disk[:disk_name]
+ %p= disk[:mount_path]
diff --git a/app/views/admin/users/_access_levels.html.haml b/app/views/admin/users/_access_levels.html.haml
new file mode 100644
index 00000000000..7855239dfe5
--- /dev/null
+++ b/app/views/admin/users/_access_levels.html.haml
@@ -0,0 +1,37 @@
+%fieldset
+ %legend Access
+ .form-group
+ = f.label :projects_limit, class: 'control-label'
+ .col-sm-10= f.number_field :projects_limit, min: 0, class: 'form-control'
+
+ .form-group
+ = f.label :can_create_group, class: 'control-label'
+ .col-sm-10= f.check_box :can_create_group
+
+ .form-group
+ = f.label :access_level, class: 'control-label'
+ .col-sm-10
+ - editing_current_user = (current_user == @user)
+
+ = f.radio_button :access_level, :regular, disabled: editing_current_user
+ = label_tag :regular do
+ Regular
+ %p.light
+ Regular users have access to their groups and projects
+
+ = f.radio_button :access_level, :admin, disabled: editing_current_user
+ = label_tag :admin do
+ Admin
+ %p.light
+ Administrators have access to all groups, projects and users and can manage all features in this installation
+ - if editing_current_user
+ %p.light
+ You cannot remove your own admin rights.
+
+ .form-group
+ = f.label :external, class: 'control-label'
+ .col-sm-10
+ = f.check_box :external do
+ External
+ %p.light
+ External users cannot see internal or private projects unless access is explicitly granted. Also, external users cannot create projects or groups.
diff --git a/app/views/admin/users/_form.html.haml b/app/views/admin/users/_form.html.haml
index 3145212728f..e911af3f6f9 100644
--- a/app/views/admin/users/_form.html.haml
+++ b/app/views/admin/users/_form.html.haml
@@ -40,28 +40,7 @@
= f.label :password_confirmation, class: 'control-label'
.col-sm-10= f.password_field :password_confirmation, disabled: f.object.force_random_password, class: 'form-control'
- %fieldset
- %legend Access
- .form-group
- = f.label :projects_limit, class: 'control-label'
- .col-sm-10= f.number_field :projects_limit, min: 0, class: 'form-control'
-
- .form-group
- = f.label :can_create_group, class: 'control-label'
- .col-sm-10= f.check_box :can_create_group
-
- .form-group
- = f.label :admin, class: 'control-label'
- - if current_user == @user
- .col-sm-10= f.check_box :admin, disabled: true
- .col-sm-10 You cannot remove your own admin rights.
- - else
- .col-sm-10= f.check_box :admin
-
- .form-group
- = f.label :external, class: 'control-label'
- .col-sm-10= f.check_box :external
- .col-sm-10 External users cannot see internal or private projects unless access is explicitly granted. Also, external users cannot create projects or groups.
+ = render partial: 'access_levels', locals: { f: f }
%fieldset
%legend Profile
diff --git a/app/views/admin/users/_head.html.haml b/app/views/admin/users/_head.html.haml
index ce5e21e54cc..9984e733956 100644
--- a/app/views/admin/users/_head.html.haml
+++ b/app/views/admin/users/_head.html.haml
@@ -15,10 +15,8 @@
%ul.nav-links
= nav_link(path: 'users#show') do
= link_to "Account", admin_user_path(@user)
- = nav_link(path: 'users#groups') do
- = link_to "Groups", groups_admin_user_path(@user)
= nav_link(path: 'users#projects') do
- = link_to "Projects", projects_admin_user_path(@user)
+ = link_to "Groups and projects", projects_admin_user_path(@user)
= nav_link(path: 'users#keys') do
= link_to "SSH keys", keys_admin_user_path(@user)
= nav_link(controller: :identities) do
diff --git a/app/views/admin/users/_user.html.haml b/app/views/admin/users/_user.html.haml
index 2d9588f9d27..3b5c713ac2d 100644
--- a/app/views/admin/users/_user.html.haml
+++ b/app/views/admin/users/_user.html.haml
@@ -18,7 +18,7 @@
= link_to 'Edit', edit_admin_user_path(user), id: "edit_#{dom_id(user)}", class: 'btn'
- unless user == current_user
.dropdown.inline
- %a.dropdown-new.btn.btn-default#project-settings-button{href: '#', data: { toggle: 'dropdown' } }
+ %a.dropdown-new.btn.btn-default#project-settings-button{ href: '#', data: { toggle: 'dropdown' } }
= icon('cog')
= icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right
diff --git a/app/views/admin/users/groups.html.haml b/app/views/admin/users/groups.html.haml
deleted file mode 100644
index 8f6d13b881a..00000000000
--- a/app/views/admin/users/groups.html.haml
+++ /dev/null
@@ -1,20 +0,0 @@
-- page_title "Groups", @user.name, "Users"
-= render 'admin/users/head'
-
-- group_members = @user.group_members.includes(:source)
-- if group_members.any?
- .panel.panel-default
- .panel-heading Groups:
- %ul.well-list
- - group_members.each do |group_member|
- - group = group_member.group
- %li.group_member
- %span{class: ("list-item-name" unless group_member.owner?)}
- %strong= link_to group.name, admin_group_path(group)
- .pull-right
- %span.light= group_member.human_access
- - unless group_member.owner?
- = link_to group_group_member_path(group, group_member), data: { confirm: remove_member_message(group_member) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from group' do
- %i.fa.fa-times.fa-inverse
-- else
- .nothing-here-block This user has no groups.
diff --git a/app/views/admin/users/projects.html.haml b/app/views/admin/users/projects.html.haml
index dd6b7303493..15eaf1c0e67 100644
--- a/app/views/admin/users/projects.html.haml
+++ b/app/views/admin/users/projects.html.haml
@@ -1,15 +1,21 @@
-- page_title "Projects", @user.name, "Users"
+- page_title "Groups and projects", @user.name, "Users"
= render 'admin/users/head'
- if @user.groups.any?
.panel.panel-default
.panel-heading Group projects
%ul.well-list
- - @user.groups.each do |group|
- %li
+ - @user.group_members.includes(:source).each do |group_member|
+ - group = group_member.group
+ %li.group_member
%strong= link_to group.name, admin_group_path(group)
&ndash; access to
#{pluralize(group.projects.count, 'project')}
+ .pull-right
+ %span.light.vertical-align-middle= group_member.human_access
+ - unless group_member.owner?
+ = link_to group_group_member_path(group, group_member), data: { confirm: remove_member_message(group_member) }, method: :delete, remote: true, class: "btn-xs btn btn-remove prepend-left-10", title: 'Remove user from group' do
+ %i.fa.fa-times.fa-inverse
.row
.col-md-6
@@ -35,8 +41,8 @@
- if member.owner?
%span.light Owner
- else
- %span.light= member.human_access
+ %span.light.vertical-align-middle= member.human_access
- if member.respond_to? :project
- = link_to namespace_project_project_member_path(project.namespace, project, member), data: { confirm: remove_member_message(member) }, remote: true, method: :delete, class: "btn-xs btn btn-remove", title: 'Remove user from project' do
+ = link_to namespace_project_project_member_path(project.namespace, project, member), data: { confirm: remove_member_message(member) }, remote: true, method: :delete, class: "btn-xs btn btn-remove prepend-left-10", title: 'Remove user from project' do
%i.fa.fa-times
diff --git a/app/views/admin/users/show.html.haml b/app/views/admin/users/show.html.haml
index 76c9ed0ee8b..76b1291fe10 100644
--- a/app/views/admin/users/show.html.haml
+++ b/app/views/admin/users/show.html.haml
@@ -40,7 +40,7 @@
%li.two-factor-status
%span.light Two-factor Authentication:
- %strong{class: @user.two_factor_enabled? ? 'cgreen' : 'cred'}
+ %strong{ class: @user.two_factor_enabled? ? 'cgreen' : 'cred' }
- if @user.two_factor_enabled?
Enabled
= link_to 'Disable', disable_two_factor_admin_user_path(@user), data: {confirm: 'Are you sure?'}, method: :patch, class: 'btn btn-xs btn-remove pull-right', title: 'Disable Two-factor Authentication'
@@ -186,6 +186,6 @@
- if @user.solo_owned_groups.present?
%p
This user is currently an owner in these groups:
- %strong #{@user.solo_owned_groups.map(&:name).join(', ')}
+ %strong= @user.solo_owned_groups.map(&:name).join(', ')
%p
You must transfer ownership or delete these groups before you can delete this user.
diff --git a/app/views/award_emoji/_awards_block.html.haml b/app/views/award_emoji/_awards_block.html.haml
index d8912eda314..e3305e21e96 100644
--- a/app/views/award_emoji/_awards_block.html.haml
+++ b/app/views/award_emoji/_awards_block.html.haml
@@ -2,8 +2,7 @@
.awards.js-awards-block{ class: ("hidden" if !inline && grouped_emojis.empty?), data: { award_url: toggle_award_url(awardable) } }
- awards_sort(grouped_emojis).each do |emoji, awards|
%button.btn.award-control.js-emoji-btn.has-tooltip{ type: "button",
- disabled: !current_user,
- class: (award_active_class(awards, current_user)),
+ class: (award_state_class(awards, current_user)),
data: { placement: "bottom", title: award_user_list(awards, current_user) } }
= emoji_icon(emoji, sprite: false)
%span.award-control-text.js-counter
diff --git a/app/views/ci/lints/_create.html.haml b/app/views/ci/lints/_create.html.haml
index 61c7cce20b2..c91602fcff7 100644
--- a/app/views/ci/lints/_create.html.haml
+++ b/app/views/ci/lints/_create.html.haml
@@ -36,7 +36,7 @@
- if build[:allow_failure]
%b Allowed to fail
--else
+- else
%p
%b Status:
syntax is incorrect
diff --git a/app/views/ci/lints/show.html.haml b/app/views/ci/lints/show.html.haml
index 889086c62b1..b0bee1c6204 100644
--- a/app/views/ci/lints/show.html.haml
+++ b/app/views/ci/lints/show.html.haml
@@ -1,20 +1,25 @@
- page_title "CI Lint"
- page_description "Validate your GitLab CI configuration file"
+- content_for :page_specific_javascripts do
+ = page_specific_javascript_tag('lib/ace.js')
%h2 Check your .gitlab-ci.yml
-%hr
-.row
- = form_tag ci_lint_path, method: :post do
- .form-group
- = label_tag(:content, 'Content of .gitlab-ci.yml', class: 'control-label text-nowrap')
+.ci-linter
+ .row
+ = form_tag ci_lint_path, method: :post do
+ .form-group
+ .col-sm-12
+ .file-holder
+ .file-title.clearfix
+ Content of .gitlab-ci.yml
+ #ci-editor.ci-editor= @content
+ = text_area_tag(:content, @content, class: 'hidden form-control span1', rows: 7, require: true)
.col-sm-12
- = text_area_tag(:content, @content, class: 'form-control span1', rows: 7, require: true)
- .col-sm-12
- .pull-left.prepend-top-10
- = submit_tag('Validate', class: 'btn btn-success submit-yml')
+ .pull-left.prepend-top-10
+ = submit_tag('Validate', class: 'btn btn-success submit-yml')
-.row.prepend-top-20
- .col-sm-12
- .results
- = render partial: 'create' if defined?(@status)
+ .row.prepend-top-20
+ .col-sm-12
+ .results.ci-template
+ = render partial: 'create' if defined?(@status)
diff --git a/app/views/ci/status/_badge.html.haml b/app/views/ci/status/_badge.html.haml
index 601fb7f0f3f..c00c7f7407e 100644
--- a/app/views/ci/status/_badge.html.haml
+++ b/app/views/ci/status/_badge.html.haml
@@ -1,7 +1,8 @@
- status = local_assigns.fetch(:status)
+- link = local_assigns.fetch(:link, true)
- css_classes = "ci-status ci-#{status.group}"
-- if status.has_details?
+- if link && status.has_details?
= link_to status.details_path, class: css_classes do
= custom_icon(status.icon)
= status.text
diff --git a/app/views/ci/status/_dropdown_graph_badge.html.haml b/app/views/ci/status/_dropdown_graph_badge.html.haml
new file mode 100644
index 00000000000..8ed23ac4919
--- /dev/null
+++ b/app/views/ci/status/_dropdown_graph_badge.html.haml
@@ -0,0 +1,19 @@
+-# Renders the content of each li in the dropdown
+
+- subject = local_assigns.fetch(:subject)
+- status = subject.detailed_status(current_user)
+- klass = "ci-status-icon ci-status-icon-#{status.group}"
+- tooltip = "#{subject.name} - #{status.label}"
+
+- if status.has_details?
+ = link_to status.details_path, class: 'mini-pipeline-graph-dropdown-item', data: { toggle: 'tooltip', title: tooltip } do
+ %span{ class: klass }= custom_icon(status.icon)
+ %span.ci-build-text= subject.name
+- else
+ .mini-pipeline-graph-dropdown-item{ data: { toggle: 'tooltip', title: tooltip } }
+ %span{ class: klass }= custom_icon(status.icon)
+ %span.ci-build-text= subject.name
+
+- if status.has_action?
+ = link_to status.action_path, class: 'ci-action-icon-wrapper js-ci-action-icon', method: status.action_method, data: { toggle: 'tooltip', title: status.action_title } do
+ = custom_icon(status.action_icon)
diff --git a/app/views/ci/status/_graph_badge.html.haml b/app/views/ci/status/_graph_badge.html.haml
index dd2f649de9a..0530d21a7e2 100644
--- a/app/views/ci/status/_graph_badge.html.haml
+++ b/app/views/ci/status/_graph_badge.html.haml
@@ -2,7 +2,7 @@
- subject = local_assigns.fetch(:subject)
- status = subject.detailed_status(current_user)
-- klass = "ci-status-icon ci-status-icon-#{status.group}"
+- klass = "ci-status-icon ci-status-icon-#{status.group} js-ci-status-icon-#{status.group}"
- tooltip = "#{subject.name} - #{status.label}"
- if status.has_details?
@@ -16,5 +16,5 @@
- if status.has_action?
= link_to status.action_path, class: 'ci-action-icon-container has-tooltip', method: status.action_method, data: { toggle: 'tooltip', title: status.action_title } do
- %i.ci-action-icon-wrapper
- = icon(status.action_icon, class: status.action_class)
+ %i.ci-action-icon-wrapper{ class: "js-#{status.action_icon.dasherize}" }
+ = custom_icon(status.action_icon)
diff --git a/app/views/dashboard/issues.html.haml b/app/views/dashboard/issues.html.haml
index 3caaf827ff5..653052f7c54 100644
--- a/app/views/dashboard/issues.html.haml
+++ b/app/views/dashboard/issues.html.haml
@@ -15,6 +15,4 @@
= render 'shared/new_project_item_select', path: 'issues/new', label: "New Issue"
= render 'shared/issuable/filter', type: :issues
-
-.prepend-top-default
- = render 'shared/issues'
+= render 'shared/issues'
diff --git a/app/views/dashboard/merge_requests.html.haml b/app/views/dashboard/merge_requests.html.haml
index fb016599fef..e64c78c4cb8 100644
--- a/app/views/dashboard/merge_requests.html.haml
+++ b/app/views/dashboard/merge_requests.html.haml
@@ -7,6 +7,4 @@
= render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New Merge Request"
= render 'shared/issuable/filter', type: :merge_requests
-
-.prepend-top-default
- = render 'shared/merge_requests'
+= render 'shared/merge_requests'
diff --git a/app/views/dashboard/projects/_zero_authorized_projects.html.haml b/app/views/dashboard/projects/_zero_authorized_projects.html.haml
index 4a55aac0df6..1bbd4602ecf 100644
--- a/app/views/dashboard/projects/_zero_authorized_projects.html.haml
+++ b/app/views/dashboard/projects/_zero_authorized_projects.html.haml
@@ -33,7 +33,7 @@
= link_to new_project_path, class: "btn btn-new" do
New project
--if publicish_project_count > 0
+- if publicish_project_count > 0
.blank-state
.blank-state-icon
= icon("globe")
diff --git a/app/views/dashboard/todos/_todo.html.haml b/app/views/dashboard/todos/_todo.html.haml
index cc077fad32a..605bfd0cf8d 100644
--- a/app/views/dashboard/todos/_todo.html.haml
+++ b/app/views/dashboard/todos/_todo.html.haml
@@ -1,9 +1,9 @@
-%li{class: "todo todo-#{todo.done? ? 'done' : 'pending'}", id: dom_id(todo), data:{url: todo_target_path(todo)} }
+%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
.todo-title.title
- - unless todo.build_failed?
+ - unless todo.build_failed? || todo.unmergeable?
= todo_target_state_pill(todo)
%span.author-name
@@ -11,8 +11,11 @@
= link_to_author(todo)
- else
(removed)
- %span.todo-label
+
+ %span.action-name
= todo_action_name(todo)
+
+ %span.todo-label
- if todo.target
= todo_target_link(todo)
- else
diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml
index e13f404fee2..c4bf2c90cc2 100644
--- a/app/views/dashboard/todos/index.html.haml
+++ b/app/views/dashboard/todos/index.html.haml
@@ -5,14 +5,14 @@
.top-area
%ul.nav-links
- todo_pending_active = ('active' if params[:state].blank? || params[:state] == 'pending')
- %li{class: "todos-pending #{todo_pending_active}"}>
+ %li{ class: "todos-pending #{todo_pending_active}" }>
= link_to todos_filter_path(state: 'pending') do
%span
To do
%span.badge
= number_with_delimiter(todos_pending_count)
- todo_done_active = ('active' if params[:state] == 'done')
- %li{class: "todos-done #{todo_done_active}"}>
+ %li{ class: "todos-done #{todo_done_active}" }>
= link_to todos_filter_path(state: 'done') do
%span
Done
@@ -50,7 +50,7 @@
data: { data: todo_actions_options, default_label: 'Action' } })
.pull-right
.dropdown.inline.prepend-left-10
- %button.dropdown-toggle{type: 'button', 'data-toggle' => 'dropdown'}
+ %button.dropdown-toggle{ type: 'button', 'data-toggle' => 'dropdown' }
%span.light
- if @sort.present?
= sort_options_hash[@sort]
@@ -67,21 +67,17 @@
= sort_title_oldest_created
-.prepend-top-default
+.js-todos-all
- if @todos.any?
.js-todos-options{ data: {per_page: @todos.limit_value, current_page: @todos.current_page, total_pages: @todos.total_pages} }
- - @todos.group_by(&:project).each do |group|
- .panel.panel-default.panel-small
- - project = group[0]
- .panel-heading
- = link_to project.name_with_namespace, namespace_project_path(project.namespace, project)
-
+ .panel.panel-default.panel-small.panel-without-border
%ul.content-list.todos-list
- = render group[1]
+ = render @todos
= paginate @todos, theme: "gitlab"
+
- elsif current_user.todos.any?
.todos-all-done
- = render "shared/empty_states/todos_all_done.svg"
+ = render "shared/empty_states/icons/todos_all_done.svg"
- if todos_filter_empty?
%h4.text-center
= Gitlab.config.gitlab.no_todos_messages.sample
@@ -98,7 +94,7 @@
- else
.todos-empty
.todos-empty-hero
- = render "shared/empty_states/todos_empty.svg"
+ = render "shared/empty_states/icons/todos_empty.svg"
.todos-empty-content
%h4
Todos let you see what you should do next.
diff --git a/app/views/devise/sessions/_new_base.html.haml b/app/views/devise/sessions/_new_base.html.haml
index 84e13693dfd..5d359538efe 100644
--- a/app/views/devise/sessions/_new_base.html.haml
+++ b/app/views/devise/sessions/_new_base.html.haml
@@ -1,16 +1,16 @@
= form_for(resource, as: resource_name, url: session_path(resource_name), html: { class: 'new_user gl-show-field-errors', 'aria-live' => 'assertive'}) do |f|
- %div.form-group
+ .form-group
= f.label "Username or email", for: :login
= f.text_field :login, class: "form-control top", autofocus: "autofocus", autocapitalize: "off", autocorrect: "off", required: true, title: "This field is required."
- %div.form-group
+ .form-group
= f.label :password
= f.password_field :password, class: "form-control bottom", required: true, title: "This field is required."
- if devise_mapping.rememberable?
.remember-me.checkbox
- %label{for: "user_remember_me"}
+ %label{ for: "user_remember_me" }
= f.check_box :remember_me
%span Remember me
.pull-right.forgot-password
= link_to "Forgot your password?", new_password_path(resource_name)
- %div.submit-container.move-submit-down
+ .submit-container.move-submit-down
= f.submit "Sign in", class: "btn btn-save"
diff --git a/app/views/devise/sessions/_new_crowd.html.haml b/app/views/devise/sessions/_new_crowd.html.haml
index a6cadbcbdff..2556cb6f59b 100644
--- a/app/views/devise/sessions/_new_crowd.html.haml
+++ b/app/views/devise/sessions/_new_crowd.html.haml
@@ -1,13 +1,13 @@
= form_tag(omniauth_authorize_path(:user, :crowd), id: 'new_crowd_user', class: 'gl-show-field-errors') do
.form-group
= label_tag :username, 'Username or email'
- = text_field_tag :username, nil, {class: "form-control top", title: "This field is required", autofocus: "autofocus", required: true }
+ = text_field_tag :username, nil, { class: "form-control top", title: "This field is required", autofocus: "autofocus", required: true }
.form-group
= label_tag :password
= password_field_tag :password, nil, { class: "form-control bottom", title: "This field is required.", required: true }
- if devise_mapping.rememberable?
.remember-me.checkbox
- %label{for: "remember_me"}
+ %label{ for: "remember_me" }
= check_box_tag :remember_me, '1', false, id: 'remember_me'
%span Remember me
= submit_tag "Sign in", class: "btn-save btn"
diff --git a/app/views/devise/sessions/_new_ldap.html.haml b/app/views/devise/sessions/_new_ldap.html.haml
index 3ab5461f929..3159d21598a 100644
--- a/app/views/devise/sessions/_new_ldap.html.haml
+++ b/app/views/devise/sessions/_new_ldap.html.haml
@@ -1,13 +1,13 @@
= form_tag(omniauth_callback_path(:user, server['provider_name']), id: 'new_ldap_user', class: "gl-show-field-errors") do
.form-group
= label_tag :username, "#{server['label']} Username"
- = text_field_tag :username, nil, {class: "form-control top", title: "This field is required.", autofocus: "autofocus", required: true }
+ = text_field_tag :username, nil, { class: "form-control top", title: "This field is required.", autofocus: "autofocus", required: true }
.form-group
= label_tag :password
= password_field_tag :password, nil, { class: "form-control bottom", title: "This field is required.", required: true }
- if devise_mapping.rememberable?
.remember-me.checkbox
- %label{for: "remember_me"}
+ %label{ for: "remember_me" }
= check_box_tag :remember_me, '1', false, id: 'remember_me'
%span Remember me
= submit_tag "Sign in", class: "btn-save btn"
diff --git a/app/views/devise/sessions/two_factor.html.haml b/app/views/devise/sessions/two_factor.html.haml
index 2cadc424668..951f03083bf 100644
--- a/app/views/devise/sessions/two_factor.html.haml
+++ b/app/views/devise/sessions/two_factor.html.haml
@@ -7,7 +7,7 @@
.login-box
.login-body
- if @user.two_factor_otp_enabled?
- = form_for(resource, as: resource_name, url: session_path(resource_name), method: :post, html: { class: 'edit_user gl-show-field-errors' }) do |f|
+ = form_for(resource, as: resource_name, url: session_path(resource_name), method: :post, html: { class: "edit_user gl-show-field-errors js-2fa-form #{'hidden' if @user.two_factor_u2f_enabled?}" }) do |f|
- resource_params = params[resource_name].presence || params
= f.hidden_field :remember_me, value: resource_params.fetch(:remember_me, 0)
%div
diff --git a/app/views/devise/shared/_omniauth_box.html.haml b/app/views/devise/shared/_omniauth_box.html.haml
index 8908b64cdac..f92f89e73ff 100644
--- a/app/views/devise/shared/_omniauth_box.html.haml
+++ b/app/views/devise/shared/_omniauth_box.html.haml
@@ -1,4 +1,4 @@
-%div.omniauth-container
+.omniauth-container
%p
%span.light
Sign in with &nbsp;
@@ -6,4 +6,4 @@
- providers.each do |provider|
%span.light
- has_icon = provider_has_icon?(provider)
- = link_to provider_image_tag(provider), omniauth_authorize_path(:user, provider), method: :post, class: (has_icon ? 'oauth-image-link' : 'btn'), "data-no-turbolink" => "true"
+ = link_to provider_image_tag(provider), omniauth_authorize_path(:user, provider), method: :post, class: (has_icon ? 'oauth-image-link' : 'btn')
diff --git a/app/views/devise/shared/_signin_box.html.haml b/app/views/devise/shared/_signin_box.html.haml
index 86edaf14e43..eddfce363a7 100644
--- a/app/views/devise/shared/_signin_box.html.haml
+++ b/app/views/devise/shared/_signin_box.html.haml
@@ -1,18 +1,18 @@
- if form_based_providers.any?
- if crowd_enabled?
- .login-box.tab-pane.active{id: "crowd", role: 'tabpanel', class: 'tab-pane'}
+ .login-box.tab-pane.active{ id: "crowd", role: 'tabpanel' }
.login-body
= render 'devise/sessions/new_crowd'
- @ldap_servers.each_with_index do |server, i|
- .login-box.tab-pane{id: "#{server['provider_name']}", role: 'tabpanel', class: (:active if i.zero? && !crowd_enabled?)}
+ .login-box.tab-pane{ id: "#{server['provider_name']}", role: 'tabpanel', class: (:active if i.zero? && !crowd_enabled?) }
.login-body
= render 'devise/sessions/new_ldap', server: server
- if signin_enabled?
- .login-box.tab-pane{id: 'ldap-standard', role: 'tabpanel'}
+ .login-box.tab-pane{ id: 'ldap-standard', role: 'tabpanel' }
.login-body
= render 'devise/sessions/new_base'
- elsif signin_enabled?
- .login-box.tab-pane.active{id: 'login-pane', role: 'tabpanel'}
+ .login-box.tab-pane.active{ id: 'login-pane', role: 'tabpanel' }
.login-body
= render 'devise/sessions/new_base'
diff --git a/app/views/devise/shared/_signup_box.html.haml b/app/views/devise/shared/_signup_box.html.haml
index 3133f6de2e8..5a44ec45b7b 100644
--- a/app/views/devise/shared/_signup_box.html.haml
+++ b/app/views/devise/shared/_signup_box.html.haml
@@ -1,26 +1,29 @@
-#register-pane.login-box{ role: 'tabpanel', class: 'tab-pane' }
+#register-pane.tab-pane.login-box{ role: 'tabpanel' }
.login-body
= form_for(resource, as: "new_#{resource_name}", url: registration_path(resource_name), html: { class: "new_new_user gl-show-field-errors", "aria-live" => "assertive" }) do |f|
.devise-errors
= devise_error_messages!
- %div.form-group
+ .form-group
= f.label :name
= f.text_field :name, class: "form-control top", required: true, title: "This field is required."
- %div.username.form-group
+ .username.form-group
= f.label :username
= f.text_field :username, class: "form-control middle", pattern: Gitlab::Regex::NAMESPACE_REGEX_STR_SIMPLE, required: true, title: 'Please create a username with only alphanumeric characters.'
%p.validation-error.hide Username is already taken.
%p.validation-success.hide Username is available.
%p.validation-pending.hide Checking username availability...
- %div.form-group
+ .form-group
= f.label :email
= f.email_field :email, class: "form-control middle", required: true, title: "Please provide a valid email address."
+ .form-group
+ = f.label :email_confirmation
+ = f.email_field :email_confirmation, class: "form-control middle", required: true, title: "Please retype the email address."
.form-group.append-bottom-20#password-strength
= f.label :password
= f.password_field :password, class: "form-control bottom", required: true, pattern: ".{#{@minimum_password_length},}", title: "Minimum length is #{@minimum_password_length} characters."
%p.gl-field-hint Minimum length is #{@minimum_password_length} characters
%div
- - if current_application_settings.recaptcha_enabled
+ - if Gitlab::Recaptcha.enabled?
= recaptcha_tags
%div
= f.submit "Register", class: "btn-register btn"
diff --git a/app/views/devise/shared/_tabs_ldap.html.haml b/app/views/devise/shared/_tabs_ldap.html.haml
index aec1b31ce62..8c4ad30c832 100644
--- a/app/views/devise/shared/_tabs_ldap.html.haml
+++ b/app/views/devise/shared/_tabs_ldap.html.haml
@@ -3,7 +3,7 @@
%li.active
= link_to "Crowd", "#crowd", 'data-toggle' => 'tab'
- @ldap_servers.each_with_index do |server, i|
- %li{class: (:active if i.zero? && !crowd_enabled?)}
+ %li{ class: (:active if i.zero? && !crowd_enabled?) }
= link_to server['label'], "##{server['provider_name']}", 'data-toggle' => 'tab'
- if signin_enabled?
%li
diff --git a/app/views/devise/shared/_tabs_normal.html.haml b/app/views/devise/shared/_tabs_normal.html.haml
index 05246303fb6..c225d800a98 100644
--- a/app/views/devise/shared/_tabs_normal.html.haml
+++ b/app/views/devise/shared/_tabs_normal.html.haml
@@ -1,6 +1,6 @@
-%ul.nav-links.new-session-tabs.nav-tabs{ role: 'tablist'}
+%ul.nav-links.new-session-tabs.nav-tabs{ role: 'tablist' }
%li.active{ role: 'presentation' }
- %a{ href: '#login-pane', data: { toggle: 'tab' }, role: 'tab'} Sign in
+ %a{ href: '#login-pane', data: { toggle: 'tab' }, role: 'tab' } Sign in
- if signin_enabled? && signup_enabled?
- %li{ role: 'presentation'}
- %a{ href: '#register-pane', data: { toggle: 'tab' }, role: 'tab'} Register
+ %li{ role: 'presentation' }
+ %a{ href: '#register-pane', data: { toggle: 'tab' }, role: 'tab' } Register
diff --git a/app/views/discussions/_diff_discussion.html.haml b/app/views/discussions/_diff_discussion.html.haml
index 1411daeb4a6..2deadbeeceb 100644
--- a/app/views/discussions/_diff_discussion.html.haml
+++ b/app/views/discussions/_diff_discussion.html.haml
@@ -1,5 +1,5 @@
- expanded = local_assigns.fetch(:expanded, true)
-%tr.notes_holder{class: ('hide' unless expanded)}
+%tr.notes_holder{ class: ('hide' unless expanded) }
%td.notes_line{ colspan: 2 }
%td.notes_content
.content
diff --git a/app/views/discussions/_discussion.html.haml b/app/views/discussions/_discussion.html.haml
index 2bce2780484..6f5d4bf2a2f 100644
--- a/app/views/discussions/_discussion.html.haml
+++ b/app/views/discussions/_discussion.html.haml
@@ -1,6 +1,9 @@
- 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
diff --git a/app/views/discussions/_jump_to_next.html.haml b/app/views/discussions/_jump_to_next.html.haml
index 7ed09dd1a98..69bd416c4de 100644
--- a/app/views/discussions/_jump_to_next.html.haml
+++ b/app/views/discussions/_jump_to_next.html.haml
@@ -5,5 +5,5 @@
%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" }}
+ data: { container: "body" } }
= custom_icon("next_discussion")
diff --git a/app/views/discussions/_parallel_diff_discussion.html.haml b/app/views/discussions/_parallel_diff_discussion.html.haml
index f1072ce0feb..3a19e021643 100644
--- a/app/views/discussions/_parallel_diff_discussion.html.haml
+++ b/app/views/discussions/_parallel_diff_discussion.html.haml
@@ -1,21 +1,21 @@
- expanded = discussion_left.try(:expanded?) || discussion_right.try(:expanded?)
-%tr.notes_holder{class: ('hide' unless 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?)}
+ .content{ class: ('hide' unless discussion_left.expanded?) }
= render "discussions/notes", discussion: discussion_left, line_type: 'old'
- else
- %td.notes_line.old= ""
+ %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?)}
+ .content{ class: ('hide' unless discussion_right.expanded?) }
= render "discussions/notes", discussion: discussion_right, line_type: 'new'
- else
- %td.notes_line.new= ""
+ %td.notes_line.new= ("")
%td.notes_content.parallel.new
.content
diff --git a/app/views/doorkeeper/applications/_delete_form.html.haml b/app/views/doorkeeper/applications/_delete_form.html.haml
index 001a711b1dd..84b4ce5b606 100644
--- a/app/views/doorkeeper/applications/_delete_form.html.haml
+++ b/app/views/doorkeeper/applications/_delete_form.html.haml
@@ -1,6 +1,6 @@
- submit_btn_css ||= 'btn btn-link btn-remove btn-sm'
= form_tag oauth_application_path(application) do
- %input{:name => "_method", :type => "hidden", :value => "delete"}/
+ %input{ :name => "_method", :type => "hidden", :value => "delete" }/
- if defined? small
= button_tag type: "submit", class: "btn btn-transparent", data: { confirm: "Are you sure?" } do
%span.sr-only
diff --git a/app/views/doorkeeper/applications/index.html.haml b/app/views/doorkeeper/applications/index.html.haml
index 3998e66f40d..aa271150b07 100644
--- a/app/views/doorkeeper/applications/index.html.haml
+++ b/app/views/doorkeeper/applications/index.html.haml
@@ -31,7 +31,7 @@
%th.last-heading
%tbody
- @applications.each do |application|
- %tr{id: "application_#{application.id}"}
+ %tr{ id: "application_#{application.id}" }
%td= link_to application.name, oauth_application_path(application)
%td
- application.redirect_uri.split.each do |uri|
@@ -63,7 +63,7 @@
%tbody
- @authorized_apps.each do |app|
- token = app.authorized_tokens.order('created_at desc').first
- %tr{id: "application_#{app.id}"}
+ %tr{ id: "application_#{app.id}" }
%td= app.name
%td= token.created_at
%td= token.scopes
@@ -72,7 +72,7 @@
%tr
%td
Anonymous
- %div.help-block
+ .help-block
%em Authorization was granted by entering your username and password in the application.
%td= token.created_at
%td= token.scopes
diff --git a/app/views/doorkeeper/authorizations/error.html.haml b/app/views/doorkeeper/authorizations/error.html.haml
index a4c607cea60..6117b00149f 100644
--- a/app/views/doorkeeper/authorizations/error.html.haml
+++ b/app/views/doorkeeper/authorizations/error.html.haml
@@ -1,3 +1,3 @@
%h3.page-title An error has occurred
-%main{:role => "main"}
+%main{ :role => "main" }
%pre= @pre_auth.error_response.body[:error_description]
diff --git a/app/views/doorkeeper/authorizations/new.html.haml b/app/views/doorkeeper/authorizations/new.html.haml
index ce050007204..a196561f381 100644
--- a/app/views/doorkeeper/authorizations/new.html.haml
+++ b/app/views/doorkeeper/authorizations/new.html.haml
@@ -1,5 +1,5 @@
%h3.page-title Authorization required
-%main{:role => "main"}
+%main{ :role => "main" }
%p.h4
Authorize
%strong.text-info= @pre_auth.client.name
@@ -10,7 +10,7 @@
%p
= icon("exclamation-triangle fw")
You are an admin, which means granting access to
- %strong #{@pre_auth.client.name}
+ %strong= @pre_auth.client.name
will allow them to interact with GitLab as an admin as well. Proceed with caution.
- if @pre_auth.scopes
diff --git a/app/views/doorkeeper/authorizations/show.html.haml b/app/views/doorkeeper/authorizations/show.html.haml
index 01f9e46f142..44e868e6782 100644
--- a/app/views/doorkeeper/authorizations/show.html.haml
+++ b/app/views/doorkeeper/authorizations/show.html.haml
@@ -1,3 +1,3 @@
%h3.page-title Authorization code:
-%main{:role => "main"}
+%main{ :role => "main" }
%code#authorization_code= params[:code]
diff --git a/app/views/doorkeeper/authorized_applications/_delete_form.html.haml b/app/views/doorkeeper/authorized_applications/_delete_form.html.haml
index 9f02a8d2ed9..11c1e67878e 100644
--- a/app/views/doorkeeper/authorized_applications/_delete_form.html.haml
+++ b/app/views/doorkeeper/authorized_applications/_delete_form.html.haml
@@ -3,7 +3,7 @@
- path = oauth_authorized_application_path(0, token_id: token)
- else
- path = oauth_authorized_application_path(application)
-
+
= form_tag path do
- %input{:name => "_method", :type => "hidden", :value => "delete"}/
+ %input{ :name => "_method", :type => "hidden", :value => "delete" }/
= submit_tag 'Revoke', onclick: "return confirm('Are you sure?')", class: 'btn btn-remove btn-sm'
diff --git a/app/views/doorkeeper/authorized_applications/index.html.haml b/app/views/doorkeeper/authorized_applications/index.html.haml
index b184b9c01d4..c8a585560a2 100644
--- a/app/views/doorkeeper/authorized_applications/index.html.haml
+++ b/app/views/doorkeeper/authorized_applications/index.html.haml
@@ -1,6 +1,6 @@
%header.page-header
%h1 Your authorized applications
-%main{:role => "main"}
+%main{ :role => "main" }
.table-holder
%table.table.table-striped
%thead
diff --git a/app/views/emojis/index.html.haml b/app/views/emojis/index.html.haml
index 790d90ad3ee..49bd9acd2db 100644
--- a/app/views/emojis/index.html.haml
+++ b/app/views/emojis/index.html.haml
@@ -7,5 +7,5 @@
%ul.clearfix.emoji-menu-list
- emojis.each do |emoji|
%li.pull-left.text-center.emoji-menu-list-item
- %button.emoji-menu-btn.text-center.js-emoji-btn{type: "button"}
+ %button.emoji-menu-btn.text-center.js-emoji-btn{ type: "button" }
= emoji_icon(emoji["name"], emoji["unicode"], emoji["aliases"])
diff --git a/app/views/errors/access_denied.html.haml b/app/views/errors/access_denied.html.haml
index 8bddbef3562..a97cbd4d4b3 100644
--- a/app/views/errors/access_denied.html.haml
+++ b/app/views/errors/access_denied.html.haml
@@ -1,6 +1,5 @@
- content_for(:title, 'Access Denied')
-%img{:alt => "GitLab Logo",
- :src => image_path('logo.svg')}
+%img{ :alt => "GitLab Logo", :src => image_path('logo.svg') }
%h1
403
.container
diff --git a/app/views/errors/encoding.html.haml b/app/views/errors/encoding.html.haml
index 064ff14ad2c..64f7f8e0836 100644
--- a/app/views/errors/encoding.html.haml
+++ b/app/views/errors/encoding.html.haml
@@ -1,6 +1,5 @@
- content_for(:title, 'Encoding Error')
-%img{:alt => "GitLab Logo",
- :src => image_path('logo.svg')}
+%img{ :alt => "GitLab Logo", :src => image_path('logo.svg') }
%h1
500
.container
diff --git a/app/views/errors/git_not_found.html.haml b/app/views/errors/git_not_found.html.haml
index c5c12a410ac..d860957665b 100644
--- a/app/views/errors/git_not_found.html.haml
+++ b/app/views/errors/git_not_found.html.haml
@@ -1,6 +1,5 @@
- content_for(:title, 'Git Resource Not Found')
-%img{:alt => "GitLab Logo",
- :src => image_path('logo.svg')}
+%img{ :alt => "GitLab Logo", :src => image_path('logo.svg') }
%h1
404
.container
diff --git a/app/views/errors/not_found.html.haml b/app/views/errors/not_found.html.haml
index 50a54a93cb5..a0b9a632e22 100644
--- a/app/views/errors/not_found.html.haml
+++ b/app/views/errors/not_found.html.haml
@@ -1,6 +1,5 @@
- content_for(:title, 'Not Found')
-%img{:alt => "GitLab Logo",
- :src => image_path('logo.svg')}
+%img{ :alt => "GitLab Logo", :src => image_path('logo.svg') }
%h1
404
.container
diff --git a/app/views/errors/omniauth_error.html.haml b/app/views/errors/omniauth_error.html.haml
index d91f1878cb6..72508b91134 100644
--- a/app/views/errors/omniauth_error.html.haml
+++ b/app/views/errors/omniauth_error.html.haml
@@ -1,6 +1,5 @@
- content_for(:title, 'Auth Error')
-%img{:alt => "GitLab Logo",
- :src => image_path('logo.svg')}
+%img{ :alt => "GitLab Logo", :src => image_path('logo.svg') }
%h1
422
.container
diff --git a/app/views/events/_event_issue.atom.haml b/app/views/events/_event_issue.atom.haml
index 083c3936212..51585314a62 100644
--- a/app/views/events/_event_issue.atom.haml
+++ b/app/views/events/_event_issue.atom.haml
@@ -1,2 +1,2 @@
-%div{xmlns: "http://www.w3.org/1999/xhtml"}
+%div{ xmlns: "http://www.w3.org/1999/xhtml" }
= markdown(issue.description, pipeline: :atom, project: issue.project, author: issue.author)
diff --git a/app/views/events/_event_merge_request.atom.haml b/app/views/events/_event_merge_request.atom.haml
index d7e05600627..56fc8b86217 100644
--- a/app/views/events/_event_merge_request.atom.haml
+++ b/app/views/events/_event_merge_request.atom.haml
@@ -1,2 +1,2 @@
-%div{xmlns: "http://www.w3.org/1999/xhtml"}
+%div{ xmlns: "http://www.w3.org/1999/xhtml" }
= markdown(merge_request.description, pipeline: :atom, project: merge_request.project, author: merge_request.author)
diff --git a/app/views/events/_event_note.atom.haml b/app/views/events/_event_note.atom.haml
index 1154f982821..6fa2f9bd4db 100644
--- a/app/views/events/_event_note.atom.haml
+++ b/app/views/events/_event_note.atom.haml
@@ -1,2 +1,2 @@
-%div{xmlns: "http://www.w3.org/1999/xhtml"}
+%div{ xmlns: "http://www.w3.org/1999/xhtml" }
= markdown(note.note, pipeline: :atom, project: note.project, author: note.author)
diff --git a/app/views/events/_event_push.atom.haml b/app/views/events/_event_push.atom.haml
index 28bee1d0a33..f8f0bcb7608 100644
--- a/app/views/events/_event_push.atom.haml
+++ b/app/views/events/_event_push.atom.haml
@@ -1,4 +1,4 @@
-%div{xmlns: "http://www.w3.org/1999/xhtml"}
+%div{ xmlns: "http://www.w3.org/1999/xhtml" }
- event.commits.first(15).each do |commit|
%p
%strong= commit[:author][:name]
diff --git a/app/views/events/event/_common.html.haml b/app/views/events/event/_common.html.haml
index bba6e0d2c20..2fb6b5647da 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{class: event.action_name}
+ %span{ class: event.action_name }
- if event.target
= event.action_name
%strong
diff --git a/app/views/events/event/_created_project.html.haml b/app/views/events/event/_created_project.html.haml
index aba64dd17d0..80cf2344fe1 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{class: event.action_name}
+ %span{ class: event.action_name }
= event_action_name(event)
- if event.project
diff --git a/app/views/explore/groups/index.html.haml b/app/views/explore/groups/index.html.haml
index 4e5d965ccbe..73cf6e87eb4 100644
--- a/app/views/explore/groups/index.html.haml
+++ b/app/views/explore/groups/index.html.haml
@@ -17,7 +17,7 @@
.pull-right
.dropdown.inline
- %button.dropdown-toggle{type: 'button', 'data-toggle' => 'dropdown'}
+ %button.dropdown-toggle{ type: 'button', 'data-toggle' => 'dropdown' }
%span.light
- if @sort.present?
= sort_options_hash[@sort]
diff --git a/app/views/explore/projects/_filter.html.haml b/app/views/explore/projects/_filter.html.haml
index 5ea154c36b4..e3088848492 100644
--- a/app/views/explore/projects/_filter.html.haml
+++ b/app/views/explore/projects/_filter.html.haml
@@ -1,6 +1,6 @@
- if current_user
.dropdown
- %button.dropdown-toggle{href: '#', "data-toggle" => "dropdown"}
+ %button.dropdown-toggle{ href: '#', "data-toggle" => "dropdown" }
= icon('globe')
%span.light Visibility:
- if params[:visibility_level].present?
@@ -20,7 +20,7 @@
- if @tags.present?
.dropdown
- %button.dropdown-toggle{href: '#', "data-toggle" => "dropdown"}
+ %button.dropdown-toggle{ href: '#', "data-toggle" => "dropdown" }
= icon('tags')
%span.light Tags:
- if params[:tag].present?
diff --git a/app/views/groups/_group_lfs_settings.html.haml b/app/views/groups/_group_lfs_settings.html.haml
index af57065f0fc..3c622ca5c3c 100644
--- a/app/views/groups/_group_lfs_settings.html.haml
+++ b/app/views/groups/_group_lfs_settings.html.haml
@@ -8,4 +8,4 @@
Allow projects within this group to use Git LFS
= link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs')
%br/
- %span.descr This setting can be overridden in each project. \ No newline at end of file
+ %span.descr This setting can be overridden in each project.
diff --git a/app/views/groups/group_members/_new_group_member.html.haml b/app/views/groups/group_members/_new_group_member.html.haml
index b185b81db7f..5b1a4630c56 100644
--- a/app/views/groups/group_members/_new_group_member.html.haml
+++ b/app/views/groups/group_members/_new_group_member.html.haml
@@ -3,7 +3,7 @@
.col-md-4.col-lg-6
= users_select_tag(:user_ids, multiple: true, class: 'input-clamp', scope: :all, email_user: true)
.help-block.append-bottom-10
- Search for users by name, username, or email, or invite new ones using their email address.
+ Search for members by name, username, or email, or invite new ones using their email address.
.col-md-3.col-lg-2
= select_tag :access_level, options_for_select(GroupMember.access_level_roles, @group_member.access_level), class: "form-control project-access-select"
@@ -16,7 +16,7 @@
= text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Expiration date'
%i.clear-icon.js-clear-input
.help-block.append-bottom-10
- On this date, the user(s) will automatically lose access to this group and all of its projects.
+ On this date, the member(s) will automatically lose access to this group and all of its projects.
.col-md-2
= f.submit 'Add to group', class: "btn btn-create btn-block"
diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml
index bc5d3c797ac..2e4e4511bb6 100644
--- a/app/views/groups/group_members/index.html.haml
+++ b/app/views/groups/group_members/index.html.haml
@@ -7,7 +7,7 @@
- if can?(current_user, :admin_group_member, @group)
.project-members-new.append-bottom-default
%p.clearfix
- Add new user to
+ Add new member to
%strong= @group.name
= render "new_group_member"
@@ -15,7 +15,7 @@
.append-bottom-default.clearfix
%h5.member.existing-title
- Existing users
+ Existing members
= form_tag group_group_members_path(@group), method: :get, class: 'form-inline member-search-form' do
.form-group
= search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false }
@@ -24,8 +24,8 @@
= render 'shared/members/sort_dropdown'
.panel.panel-default
.panel-heading
- Users with access to
- %strong #{@group.name}
+ Members with access to
+ %strong= @group.name
%span.badge= @members.total_count
%ul.content-list
= render partial: 'shared/members/member', collection: @members, as: :member
diff --git a/app/views/groups/issues.html.haml b/app/views/groups/issues.html.haml
index b4aa4f24d9e..83edb719692 100644
--- a/app/views/groups/issues.html.haml
+++ b/app/views/groups/issues.html.haml
@@ -18,12 +18,11 @@
.row-content-block.second-block
Only issues from the
- %strong #{@group.name}
+ %strong= @group.name
group are listed here.
- if current_user
To see all issues you should visit #{link_to 'dashboard', issues_dashboard_path} page.
- .prepend-top-default
- = render 'shared/issues'
+ = render 'shared/issues'
- else
= render 'shared/empty_states/issues', project_select_button: true
diff --git a/app/views/groups/merge_requests.html.haml b/app/views/groups/merge_requests.html.haml
index dbbdb583a24..6ad76d23df5 100644
--- a/app/views/groups/merge_requests.html.haml
+++ b/app/views/groups/merge_requests.html.haml
@@ -10,10 +10,9 @@
.row-content-block.second-block
Only merge requests from
- %strong #{@group.name}
+ %strong= @group.name
group are listed here.
- if current_user
To see all merge requests you should visit #{link_to 'dashboard', merge_requests_dashboard_path} page.
-.prepend-top-default
- = render 'shared/merge_requests'
+= render 'shared/merge_requests'
diff --git a/app/views/groups/milestones/index.html.haml b/app/views/groups/milestones/index.html.haml
index a8fdbd8c426..cd5388fe402 100644
--- a/app/views/groups/milestones/index.html.haml
+++ b/app/views/groups/milestones/index.html.haml
@@ -10,7 +10,7 @@
.row-content-block
Only milestones from
- %strong #{@group.name}
+ %strong= @group.name
group are listed here.
.milestones
diff --git a/app/views/groups/new.html.haml b/app/views/groups/new.html.haml
index d19eaa6add9..38d63fd9acc 100644
--- a/app/views/groups/new.html.haml
+++ b/app/views/groups/new.html.haml
@@ -21,5 +21,5 @@
= render 'shared/group_tips'
.form-actions
- = f.submit 'Create group', class: "btn btn-create", tabindex: 3
+ = f.submit 'Create group', class: "btn btn-create"
= link_to 'Cancel', dashboard_groups_path, class: 'btn btn-cancel'
diff --git a/app/views/groups/projects.html.haml b/app/views/groups/projects.html.haml
index 33fee334d93..2e7e5e5c309 100644
--- a/app/views/groups/projects.html.haml
+++ b/app/views/groups/projects.html.haml
@@ -18,8 +18,8 @@
.pull-right
- if project.archived
%span.label.label-warning archived
- %span.label.label-gray
- = repository_size(project)
+ %span.badge
+ = storage_counter(project.statistics.storage_size)
= link_to 'Members', namespace_project_project_members_path(project.namespace, project), id: "edit_#{dom_id(project)}", class: "btn btn-sm"
= link_to 'Edit', edit_namespace_project_path(project.namespace, project), id: "edit_#{dom_id(project)}", class: "btn btn-sm"
= link_to 'Remove', project, data: { confirm: remove_project_message(project)}, method: :delete, class: "btn btn-sm btn-remove"
diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml
index 52ce26a20b1..d256d14609e 100644
--- a/app/views/groups/show.html.haml
+++ b/app/views/groups/show.html.haml
@@ -22,7 +22,7 @@
= render 'shared/members/access_request_buttons', source: @group
= render 'shared/notifications/button', notification_setting: @notification_setting
-%div.groups-header{ class: container_class }
+.groups-header{ class: container_class }
.top-area
%ul.nav-links
%li.active
@@ -32,6 +32,10 @@
%li
= link_to "#shared", 'data-toggle' => 'tab' do
Shared Projects
+ - if @nested_groups.present?
+ %li
+ = link_to "#groups", 'data-toggle' => 'tab' do
+ Subgroups
.nav-controls
= form_tag request.path, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f|
= search_field_tag :filter_projects, nil, placeholder: 'Filter by name', class: 'projects-list-filter form-control', spellcheck: false
@@ -47,3 +51,8 @@
- if @shared_projects.present?
.tab-pane#shared
= render "shared_projects", projects: @shared_projects
+
+ - if @nested_groups.present?
+ .tab-pane#groups
+ %ul.content-list
+ = render partial: 'shared/groups/group', collection: @nested_groups
diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml
index 65842a0479b..705e20112fa 100644
--- a/app/views/help/_shortcuts.html.haml
+++ b/app/views/help/_shortcuts.html.haml
@@ -1,8 +1,8 @@
-#modal-shortcuts.modal{tabindex: -1}
+#modal-shortcuts.modal{ tabindex: -1 }
.modal-dialog
.modal-content
.modal-header
- %a.close{href: "#", "data-dismiss" => "modal"} ×
+ %a.close{ href: "#", "data-dismiss" => "modal" } ×
%h4
Keyboard Shortcuts
%small
@@ -79,10 +79,18 @@
%td.shortcut
.key esc
%td Go back
+ %tbody
+ %tr
+ %th
+ %th Project File
+ %tr
+ %td.shortcut
+ .key y
+ %td Go to file permalink
.col-lg-4
%table.shortcut-mappings
- %tbody{ class: 'hidden-shortcut project', style: 'display:none' }
+ %tbody.hidden-shortcut.project{ style: 'display:none' }
%tr
%th
%th Global Dashboard
@@ -143,7 +151,7 @@
.key g
.key b
%td
- Go to builds
+ Go to jobs
%tr
%td.shortcut
.key g
@@ -190,7 +198,7 @@
%td New issue
.col-lg-4
%table.shortcut-mappings
- %tbody{ class: 'hidden-shortcut network', style: 'display:none' }
+ %tbody.hidden-shortcut.network{ style: 'display:none' }
%tr
%th
%th Network Graph
@@ -240,7 +248,7 @@
.key
shift j
%td Scroll to bottom
- %tbody{ class: 'hidden-shortcut issues', style: 'display:none' }
+ %tbody.hidden-shortcut.issues{ style: 'display:none' }
%tr
%th
%th Issues
@@ -264,7 +272,7 @@
%td.shortcut
.key l
%td Change Label
- %tbody{ class: 'hidden-shortcut merge_requests', style: 'display:none' }
+ %tbody.hidden-shortcut.merge_requests{ style: 'display:none' }
%tr
%th
%th Merge Requests
diff --git a/app/views/help/ui.html.haml b/app/views/help/ui.html.haml
index 070ed90da6d..dd1df46792b 100644
--- a/app/views/help/ui.html.haml
+++ b/app/views/help/ui.html.haml
@@ -182,7 +182,7 @@
.nav-controls
= text_field_tag 'sample', nil, class: 'form-control'
.dropdown
- %button.dropdown-menu-toggle{type: 'button', 'data-toggle' => 'dropdown'}
+ %button.dropdown-menu-toggle{ type: 'button', 'data-toggle' => 'dropdown' }
%span Sort by name
= icon('chevron-down')
%ul.dropdown-menu
@@ -205,121 +205,121 @@
%h2#buttons Buttons
.example
- %button.btn.btn-default{:type => "button"} Default
- %button.btn.btn-gray{:type => "button"} Gray
- %button.btn.btn-primary{:type => "button"} Primary
- %button.btn.btn-success{:type => "button"} Success
- %button.btn.btn-info{:type => "button"} Info
- %button.btn.btn-warning{:type => "button"} Warning
- %button.btn.btn-danger{:type => "button"} Danger
- %button.btn.btn-link{:type => "button"} Link
+ %button.btn.btn-default{ :type => "button" } Default
+ %button.btn.btn-gray{ :type => "button" } Gray
+ %button.btn.btn-primary{ :type => "button" } Primary
+ %button.btn.btn-success{ :type => "button" } Success
+ %button.btn.btn-info{ :type => "button" } Info
+ %button.btn.btn-warning{ :type => "button" } Warning
+ %button.btn.btn-danger{ :type => "button" } Danger
+ %button.btn.btn-link{ :type => "button" } Link
%h2#dropdowns Dropdowns
.example
.clearfix
.dropdown.inline.pull-left
- %button.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}}
+ %button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown' } }
Dropdown
= icon('chevron-down')
%ul.dropdown-menu
%li
- %a{href: "#"}
+ %a{ href: "#" }
Dropdown Option
.dropdown.inline.pull-right
- %button.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}}
+ %button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown' } }
Dropdown
= icon('chevron-down')
%ul.dropdown-menu.dropdown-menu-align-right
%li
- %a{href: "#"}
+ %a{ href: "#" }
Dropdown Option
.example
%div
.dropdown.inline
- %button.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}}
+ %button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown' } }
Dropdown
= icon('chevron-down')
%ul.dropdown-menu.dropdown-menu-selectable
%li
- %a.is-active{href: "#"}
+ %a.is-active{ href: "#" }
Dropdown Option
.example
%div
.dropdown.inline
- %button.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}}
+ %button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown' } }
Dropdown
= icon('chevron-down')
.dropdown-menu.dropdown-select.dropdown-menu-selectable
.dropdown-title
%span Dropdown Title
- %button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}}
+ %button.dropdown-title-button.dropdown-menu-close{ aria: { label: "Close" } }
= icon('times')
.dropdown-input
- %input.dropdown-input-field{type: "search", placeholder: "Filter results"}
+ %input.dropdown-input-field{ type: "search", placeholder: "Filter results" }
= icon('search')
.dropdown-content
%ul
%li
- %a.is-active{href: "#"}
+ %a.is-active{ href: "#" }
Dropdown Option
%li
- %a{href: "#"}
+ %a{ href: "#" }
Dropdown Option
%li.divider
%li
- %a{href: "#"}
+ %a{ href: "#" }
Dropdown Option
%li
- %a{href: "#"}
+ %a{ href: "#" }
Dropdown Option
%li
- %a{href: "#"}
+ %a{ href: "#" }
Dropdown Option
%li
- %a{href: "#"}
+ %a{ href: "#" }
Dropdown Option
%li
- %a{href: "#"}
+ %a{ href: "#" }
Dropdown Option
.dropdown-footer
%strong Tip:
If an author is not a member of this project, you can still filter by his name while using the search field.
.dropdown.inline
- %button.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}}
+ %button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown' } }
Dropdown loading
= icon('chevron-down')
.dropdown-menu.dropdown-select.dropdown-menu-selectable.is-loading
.dropdown-title
%span Dropdown Title
- %button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}}
+ %button.dropdown-title-button.dropdown-menu-close{ aria: { label: "Close" } }
= icon('times')
.dropdown-input
- %input.dropdown-input-field{type: "search", placeholder: "Filter results"}
+ %input.dropdown-input-field{ type: "search", placeholder: "Filter results" }
= icon('search')
.dropdown-content
%ul
%li
- %a.is-active{href: "#"}
+ %a.is-active{ href: "#" }
Dropdown Option
%li
- %a{href: "#"}
+ %a{ href: "#" }
Dropdown Option
%li.divider
%li
- %a{href: "#"}
+ %a{ href: "#" }
Dropdown Option
%li
- %a{href: "#"}
+ %a{ href: "#" }
Dropdown Option
%li
- %a{href: "#"}
+ %a{ href: "#" }
Dropdown Option
%li
- %a{href: "#"}
+ %a{ href: "#" }
Dropdown Option
%li
- %a{href: "#"}
+ %a{ href: "#" }
Dropdown Option
.dropdown-footer
%strong Tip:
@@ -330,21 +330,21 @@
.example
%div
.dropdown.inline
- %button.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}}
+ %button.dropdown-menu-toggle{ type: 'button', data: {toggle: 'dropdown' } }
Dropdown user
= icon('chevron-down')
.dropdown-menu.dropdown-select.dropdown-menu-selectable.dropdown-menu-user
.dropdown-title
%span Dropdown Title
- %button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}}
+ %button.dropdown-title-button.dropdown-menu-close{ aria: { label: "Close" } }
= icon('times')
.dropdown-input
- %input.dropdown-input-field{type: "search", placeholder: "Filter results"}
+ %input.dropdown-input-field{ type: "search", placeholder: "Filter results" }
= icon('search')
.dropdown-content
%ul
%li
- %a.dropdown-menu-user-link.is-active{href: "#"}
+ %a.dropdown-menu-user-link.is-active{ href: "#" }
= link_to_member_avatar(@user, size: 30)
%strong.dropdown-menu-user-full-name
= @user.name
@@ -354,24 +354,24 @@
.example
%div
.dropdown.inline
- %button.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}}
+ %button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown' } }
Dropdown page 2
= icon('chevron-down')
.dropdown-menu.dropdown-select.dropdown-menu-selectable.dropdown-menu-user.dropdown-menu-paging.is-page-two
.dropdown-page-one
.dropdown-title
- %button.dropdown-title-button.dropdown-menu-back{aria: {label: "Go back"}}
+ %button.dropdown-title-button.dropdown-menu-back{ aria: { label: "Go back" } }
= icon('arrow-left')
%span Dropdown Title
- %button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}}
+ %button.dropdown-title-button.dropdown-menu-close{ aria: { label: "Close" } }
= icon('times')
.dropdown-input
- %input.dropdown-input-field{type: "search", placeholder: "Filter results"}
+ %input.dropdown-input-field{ type: "search", placeholder: "Filter results" }
= icon('search')
.dropdown-content
%ul
%li
- %a.dropdown-menu-user-link.is-active{href: "#"}
+ %a.dropdown-menu-user-link.is-active{ href: "#" }
= link_to_member_avatar(@user, size: 30)
%strong.dropdown-menu-user-full-name
= @user.name
@@ -379,13 +379,13 @@
= @user.to_reference
.dropdown-page-two
.dropdown-title
- %button.dropdown-title-button.dropdown-menu-back{aria: {label: "Go back"}}
+ %button.dropdown-title-button.dropdown-menu-back{ aria: { label: "Go back" } }
= icon('arrow-left')
%span Create label
- %button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}}
+ %button.dropdown-title-button.dropdown-menu-close{ aria: { label: "Close" } }
= icon('times')
.dropdown-input
- %input.dropdown-input-field{type: "search", placeholder: "Name new label"}
+ %input.dropdown-input-field{ type: "search", placeholder: "Name new label" }
.dropdown-content
%button.btn.btn-primary
Create
@@ -393,16 +393,16 @@
.example
%div
.dropdown.inline
- %button#js-project-dropdown.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}}
+ %button#js-project-dropdown.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown' } }
Projects
= icon('chevron-down')
.dropdown-menu.dropdown-select.dropdown-menu-selectable
.dropdown-title
%span Go to project
- %button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}}
+ %button.dropdown-title-button.dropdown-menu-close{ aria: { label: "Close" } }
= icon('times')
.dropdown-input
- %input.dropdown-input-field{type: "search", placeholder: "Filter results"}
+ %input.dropdown-input-field{ type: "search", placeholder: "Filter results" }
= icon('search')
.dropdown-content
.dropdown-loading
@@ -486,22 +486,22 @@
.example
%form.form-horizontal
.form-group
- %label.col-sm-2.control-label{:for => "inputEmail3"} Email
+ %label.col-sm-2.control-label{ :for => "inputEmail3" } Email
.col-sm-10
- %input#inputEmail3.form-control{:placeholder => "Email", :type => "email"}/
+ %input#inputEmail3.form-control{ :placeholder => "Email", :type => "email" }/
.form-group
- %label.col-sm-2.control-label{:for => "inputPassword3"} Password
+ %label.col-sm-2.control-label{ :for => "inputPassword3" } Password
.col-sm-10
- %input#inputPassword3.form-control{:placeholder => "Password", :type => "password"}/
+ %input#inputPassword3.form-control{ :placeholder => "Password", :type => "password" }/
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
%label
- %input{:type => "checkbox"}/
+ %input{ :type => "checkbox" }/
Remember me
.form-group
.col-sm-offset-2.col-sm-10
- %button.btn.btn-default{:type => "submit"} Sign in
+ %button.btn.btn-default{ :type => "submit" } Sign in
.lead
Form when label rendered above input
@@ -510,16 +510,16 @@
.example
%form
.form-group
- %label{:for => "exampleInputEmail1"} Email address
- %input#exampleInputEmail1.form-control{:placeholder => "Enter email", :type => "email"}/
+ %label{ :for => "exampleInputEmail1" } Email address
+ %input#exampleInputEmail1.form-control{ :placeholder => "Enter email", :type => "email" }/
.form-group
- %label{:for => "exampleInputPassword1"} Password
- %input#exampleInputPassword1.form-control{:placeholder => "Password", :type => "password"}/
+ %label{ :for => "exampleInputPassword1" } Password
+ %input#exampleInputPassword1.form-control{ :placeholder => "Password", :type => "password" }/
.checkbox
%label
- %input{:type => "checkbox"}/
+ %input{ :type => "checkbox" }/
Remember me
- %button.btn.btn-default{:type => "submit"} Sign in
+ %button.btn.btn-default{ :type => "submit" } Sign in
%h2#file File
%h4
diff --git a/app/views/import/_githubish_status.html.haml b/app/views/import/_githubish_status.html.haml
index f12f9482a51..0e7f0b5ed4f 100644
--- a/app/views/import/_githubish_status.html.haml
+++ b/app/views/import/_githubish_status.html.haml
@@ -16,12 +16,12 @@
%colgroup.import-jobs-status-col
%thead
%tr
- %th= "From #{provider_title}"
+ %th From #{provider_title}
%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)}"}
+ %tr{ id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}" }
%td
= provider_project_link(provider, project.import_source)
%td
@@ -38,7 +38,7 @@
= project.human_import_status_name
- @repos.each do |repo|
- %tr{id: "repo_#{repo.id}"}
+ %tr{ id: "repo_#{repo.id}" }
%td
= provider_project_link(provider, repo.full_name)
%td.import-target
diff --git a/app/views/import/bitbucket/status.html.haml b/app/views/import/bitbucket/status.html.haml
index ac09b71ae89..e18bd47798b 100644
--- a/app/views/import/bitbucket/status.html.haml
+++ b/app/views/import/bitbucket/status.html.haml
@@ -31,7 +31,7 @@
%th Status
%tbody
- @already_added_projects.each do |project|
- %tr{id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}"}
+ %tr{ id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}" }
%td
= link_to project.import_source, "https://bitbucket.org/#{project.import_source}", target: '_blank'
%td
@@ -48,7 +48,7 @@
= project.human_import_status_name
- @repos.each do |repo|
- %tr{id: "repo_#{repo.owner}___#{repo.slug}"}
+ %tr{ id: "repo_#{repo.owner}___#{repo.slug}" }
%td
= link_to repo.full_name, "https://bitbucket.org/#{repo.full_name}", target: "_blank"
%td.import-target
@@ -68,7 +68,7 @@
Import
= icon('spinner spin', class: 'loading-icon')
- @incompatible_repos.each do |repo|
- %tr{id: "repo_#{repo.owner}___#{repo.slug}"}
+ %tr{ id: "repo_#{repo.owner}___#{repo.slug}" }
%td
= link_to repo.full_name, "https://bitbucket.org/#{repo.full_name}", target: '_blank'
%td.import-target
@@ -82,7 +82,7 @@
rather than Git. Please convert
= link_to 'them to Git,', 'https://www.atlassian.com/git/tutorials/migrating-overview'
and go through the
- = link_to 'import flow', status_import_bitbucket_path, 'data-no-turbolink' => 'true'
+ = link_to 'import flow', status_import_bitbucket_path
again.
.js-importer-status{ data: { jobs_import_path: "#{jobs_import_bitbucket_path}", import_path: "#{import_bitbucket_path}" } }
diff --git a/app/views/import/fogbugz/new_user_map.html.haml b/app/views/import/fogbugz/new_user_map.html.haml
index 07338736bac..9999a4362c6 100644
--- a/app/views/import/fogbugz/new_user_map.html.haml
+++ b/app/views/import/fogbugz/new_user_map.html.haml
@@ -37,7 +37,7 @@
%tbody
- @user_map.each do |id, user|
%tr
- %td= id
+ %td= (id)
%td= text_field_tag "users[#{id}][name]", user[:name], class: 'form-control'
%td= text_field_tag "users[#{id}][email]", user[:email], class: 'form-control'
%td
diff --git a/app/views/import/fogbugz/status.html.haml b/app/views/import/fogbugz/status.html.haml
index c8a6fa1aa9e..5de5da5e6a2 100644
--- a/app/views/import/fogbugz/status.html.haml
+++ b/app/views/import/fogbugz/status.html.haml
@@ -29,7 +29,7 @@
%th Status
%tbody
- @already_added_projects.each do |project|
- %tr{id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}"}
+ %tr{ id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}" }
%td
= project.import_source
%td
@@ -46,11 +46,11 @@
= project.human_import_status_name
- @repos.each do |repo|
- %tr{id: "repo_#{repo.id}"}
+ %tr{ id: "repo_#{repo.id}" }
%td
= repo.name
%td.import-target
- = "#{current_user.username}/#{repo.name}"
+ #{current_user.username}/#{repo.name}
%td.import-actions.job-status
= button_tag class: "btn btn-import js-add-to-import" do
Import
diff --git a/app/views/import/gitlab/status.html.haml b/app/views/import/gitlab/status.html.haml
index d31fc2e6adb..d5b88709a34 100644
--- a/app/views/import/gitlab/status.html.haml
+++ b/app/views/import/gitlab/status.html.haml
@@ -24,7 +24,7 @@
%th Status
%tbody
- @already_added_projects.each do |project|
- %tr{id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}"}
+ %tr{ id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}" }
%td
= link_to project.import_source, "https://gitlab.com/#{project.import_source}", target: "_blank"
%td
@@ -41,7 +41,7 @@
= project.human_import_status_name
- @repos.each do |repo|
- %tr{id: "repo_#{repo["id"]}"}
+ %tr{ id: "repo_#{repo["id"]}" }
%td
= link_to repo["path_with_namespace"], "https://gitlab.com/#{repo["path_with_namespace"]}", target: "_blank"
%td.import-target
diff --git a/app/views/import/google_code/new.html.haml b/app/views/import/google_code/new.html.haml
index 5d2f149cd5f..336becd229e 100644
--- a/app/views/import/google_code/new.html.haml
+++ b/app/views/import/google_code/new.html.haml
@@ -45,7 +45,7 @@
%p
Upload <code>GoogleCodeProjectHosting.json</code> here:
%p
- %input{type: "file", name: "dump_file", id: "dump_file"}
+ %input{ type: "file", name: "dump_file", id: "dump_file" }
%li
%p
Do you want to customize how Google Code email addresses and usernames are imported into GitLab?
diff --git a/app/views/import/google_code/status.html.haml b/app/views/import/google_code/status.html.haml
index e79f122940a..5e01af008be 100644
--- a/app/views/import/google_code/status.html.haml
+++ b/app/views/import/google_code/status.html.haml
@@ -34,7 +34,7 @@
%th Status
%tbody
- @already_added_projects.each do |project|
- %tr{id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}"}
+ %tr{ id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}" }
%td
= link_to project.import_source, "https://code.google.com/p/#{project.import_source}", target: "_blank"
%td
@@ -51,17 +51,17 @@
= project.human_import_status_name
- @repos.each do |repo|
- %tr{id: "repo_#{repo.id}"}
+ %tr{ id: "repo_#{repo.id}" }
%td
= link_to repo.name, "https://code.google.com/p/#{repo.name}", target: "_blank"
%td.import-target
- = "#{current_user.username}/#{repo.name}"
+ #{current_user.username}/#{repo.name}
%td.import-actions.job-status
= button_tag class: "btn btn-import js-add-to-import" do
Import
= icon("spinner spin", class: "loading-icon")
- @incompatible_repos.each do |repo|
- %tr{id: "repo_#{repo.id}"}
+ %tr{ id: "repo_#{repo.id}" }
%td
= link_to repo.name, "https://code.google.com/p/#{repo.name}", target: "_blank"
%td.import-target
diff --git a/app/views/kaminari/gitlab/_next_page.html.haml b/app/views/kaminari/gitlab/_next_page.html.haml
index 125f09777ba..c93dc7a50e8 100644
--- a/app/views/kaminari/gitlab/_next_page.html.haml
+++ b/app/views/kaminari/gitlab/_next_page.html.haml
@@ -6,8 +6,8 @@
-# per_page: number of items to fetch per page
-# remote: data-remote
- if current_page.last?
- %li{ class: "next disabled" }
+ %li.next.disabled
%span= raw(t 'views.pagination.next')
- else
- %li{ class: "next" }
+ %li.next
= link_to raw(t 'views.pagination.next'), url, rel: 'next', remote: remote
diff --git a/app/views/kaminari/gitlab/_page.html.haml b/app/views/kaminari/gitlab/_page.html.haml
index 750aed8f329..cefe0066a8f 100644
--- a/app/views/kaminari/gitlab/_page.html.haml
+++ b/app/views/kaminari/gitlab/_page.html.haml
@@ -6,5 +6,5 @@
-# total_pages: total number of pages
-# per_page: number of items to fetch per page
-# remote: data-remote
-%li{class: "page#{' active' if page.current?}#{' sibling' if page.next? || page.prev?}"}
- = link_to page, url, {remote: remote, rel: page.next? ? 'next' : page.prev? ? 'prev' : nil}
+%li{ class: "page#{' active' if page.current?}#{' sibling' if page.next? || page.prev?}" }
+ = link_to page, url, { remote: remote, rel: page.next? ? 'next' : page.prev? ? 'prev' : nil }
diff --git a/app/views/kaminari/gitlab/_paginator.html.haml b/app/views/kaminari/gitlab/_paginator.html.haml
index f5e0d2ed3f3..8fe6bd653ae 100644
--- a/app/views/kaminari/gitlab/_paginator.html.haml
+++ b/app/views/kaminari/gitlab/_paginator.html.haml
@@ -6,7 +6,7 @@
-# remote: data-remote
-# paginator: the paginator that renders the pagination tags inside
= paginator.render do
- %div.gl-pagination
+ .gl-pagination
%ul.pagination.clearfix
- unless current_page.first?
= first_page_tag unless total_pages < 5 # As kaminari will always show the first 5 pages
@@ -19,4 +19,3 @@
= next_page_tag
- unless current_page.last?
= last_page_tag unless total_pages < 5
-
diff --git a/app/views/kaminari/gitlab/_prev_page.html.haml b/app/views/kaminari/gitlab/_prev_page.html.haml
index 7edf10498a8..b7c6caf7ff4 100644
--- a/app/views/kaminari/gitlab/_prev_page.html.haml
+++ b/app/views/kaminari/gitlab/_prev_page.html.haml
@@ -6,8 +6,8 @@
-# per_page: number of items to fetch per page
-# remote: data-remote
- if current_page.first?
- %li{ class: "prev disabled" }
+ %li.prev.disabled
%span= raw(t 'views.pagination.previous')
- else
- %li{ class: "prev" }
+ %li.prev
= link_to raw(t 'views.pagination.previous'), url, rel: 'prev', remote: remote
diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml
index 3e488cf73b9..f2d355587bd 100644
--- a/app/views/layouts/_head.html.haml
+++ b/app/views/layouts/_head.html.haml
@@ -1,55 +1,57 @@
- page_description brand_title unless page_description
- site_name = "GitLab"
-%head{prefix: "og: http://ogp.me/ns#"}
- %meta{charset: "utf-8"}
- %meta{'http-equiv' => 'X-UA-Compatible', content: 'IE=edge'}
+%head{ prefix: "og: http://ogp.me/ns#" }
+ %meta{ charset: "utf-8" }
+ %meta{ 'http-equiv' => 'X-UA-Compatible', content: 'IE=edge' }
-# Open Graph - http://ogp.me/
- %meta{property: 'og:type', content: "object"}
- %meta{property: 'og:site_name', content: site_name}
- %meta{property: 'og:title', content: page_title}
- %meta{property: 'og:description', content: page_description}
- %meta{property: 'og:image', content: page_image}
- %meta{property: 'og:url', content: request.base_url + request.fullpath}
+ %meta{ property: 'og:type', content: "object" }
+ %meta{ property: 'og:site_name', content: site_name }
+ %meta{ property: 'og:title', content: page_title }
+ %meta{ property: 'og:description', content: page_description }
+ %meta{ property: 'og:image', content: page_image }
+ %meta{ property: 'og:url', content: request.base_url + request.fullpath }
-# Twitter Card - https://dev.twitter.com/cards/types/summary
- %meta{property: 'twitter:card', content: "summary"}
- %meta{property: 'twitter:title', content: page_title}
- %meta{property: 'twitter:description', content: page_description}
- %meta{property: 'twitter:image', content: page_image}
+ %meta{ property: 'twitter:card', content: "summary" }
+ %meta{ property: 'twitter:title', content: page_title }
+ %meta{ property: 'twitter:description', content: page_description }
+ %meta{ property: 'twitter:image', content: page_image }
= page_card_meta_tags
%title= page_title(site_name)
- %meta{name: "description", content: page_description}
+ %meta{ name: "description", content: page_description }
= favicon_link_tag 'favicon.ico'
= stylesheet_link_tag "application", media: "all"
= stylesheet_link_tag "print", media: "print"
- = javascript_include_tag "application"
+ = javascript_include_tag(*webpack_asset_paths("application"))
- if content_for?(:page_specific_javascripts)
= yield :page_specific_javascripts
+ = yield :project_javascripts
+
= csrf_meta_tags
- unless browser.safari?
- %meta{name: 'referrer', content: 'origin-when-cross-origin'}
- %meta{name: 'viewport', content: 'width=device-width, initial-scale=1, maximum-scale=1'}
- %meta{name: 'theme-color', content: '#474D57'}
+ %meta{ name: 'referrer', content: 'origin-when-cross-origin' }
+ %meta{ name: 'viewport', content: 'width=device-width, initial-scale=1, maximum-scale=1' }
+ %meta{ name: 'theme-color', content: '#474D57' }
-# Apple Safari/iOS home screen icons
= favicon_link_tag 'touch-icon-iphone.png', rel: 'apple-touch-icon'
= favicon_link_tag 'touch-icon-ipad.png', rel: 'apple-touch-icon', sizes: '76x76'
= favicon_link_tag 'touch-icon-iphone-retina.png', rel: 'apple-touch-icon', sizes: '120x120'
= favicon_link_tag 'touch-icon-ipad-retina.png', rel: 'apple-touch-icon', sizes: '152x152'
- %link{rel: 'mask-icon', href: image_path('logo.svg'), color: 'rgb(226, 67, 41)'}
+ %link{ rel: 'mask-icon', href: image_path('logo.svg'), color: 'rgb(226, 67, 41)' }
-# Windows 8 pinned site tile
- %meta{name: 'msapplication-TileImage', content: image_path('msapplication-tile.png')}
- %meta{name: 'msapplication-TileColor', content: '#30353E'}
+ %meta{ name: 'msapplication-TileImage', content: image_path('msapplication-tile.png') }
+ %meta{ name: 'msapplication-TileColor', content: '#30353E' }
= yield :meta_tags
diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml
index 8e65bd12c56..0e64ebd71b8 100644
--- a/app/views/layouts/_search.html.haml
+++ b/app/views/layouts/_search.html.haml
@@ -6,7 +6,7 @@
- group_data_attrs = { group_path: j(@group.path), name: @group.name, issues_path: issues_group_path(j(@group.path)), mr_path: merge_requests_group_path(j(@group.path)) }
- if @project && @project.persisted?
- project_data_attrs = { project_path: j(@project.path), name: j(@project.name), issues_path: namespace_project_issues_path(@project.namespace, @project), mr_path: namespace_project_merge_requests_path(@project.namespace, @project) }
-.search.search-form{class: "#{'has-location-badge' if label.present?}"}
+.search.search-form{ class: "#{'has-location-badge' if label.present?}" }
= form_tag search_path, method: :get, class: 'navbar-form' do |f|
.search-input-container
- if label.present?
@@ -44,4 +44,4 @@
= hidden_field_tag :snippets, true
= hidden_field_tag :repository_ref, @ref
= button_tag 'Go' if ENV['RAILS_ENV'] == 'test'
- .search-autocomplete-opts.hide{:'data-autocomplete-path' => search_autocomplete_path, :'data-autocomplete-project-id' => @project.try(:id), :'data-autocomplete-project-ref' => @ref }
+ .search-autocomplete-opts.hide{ :'data-autocomplete-path' => search_autocomplete_path, :'data-autocomplete-project-id' => @project.try(:id), :'data-autocomplete-project-ref' => @ref }
diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml
index 6c2285fa2b6..248d439cd05 100644
--- a/app/views/layouts/application.html.haml
+++ b/app/views/layouts/application.html.haml
@@ -1,12 +1,9 @@
!!! 5
%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}"}}
+ %body{ class: "#{user_application_theme}", data: { page: body_data_page, project: "#{@project.path if @project}", group: "#{@group.path if @group}" } }
= Gon::Base.render_data
- -# Ideally this would be inside the head, but turbolinks only evaluates page-specific JS in the body.
- = yield :scripts_body_top
-
= render "layouts/header/default", title: header_title
= render 'layouts/page', sidebar: sidebar, nav: nav
diff --git a/app/views/layouts/devise.html.haml b/app/views/layouts/devise.html.haml
index afd9958f073..3368a9beb29 100644
--- a/app/views/layouts/devise.html.haml
+++ b/app/views/layouts/devise.html.haml
@@ -1,7 +1,7 @@
!!! 5
-%html{ lang: "en", class: "devise-layout-html"}
+%html.devise-layout-html
= render "layouts/head"
- %body.ui_charcoal.login-page.application.navless{ data: { page: body_data_page }}
+ %body.ui_charcoal.login-page.application.navless{ data: { page: body_data_page } }
.page-wrap
= Gon::Base.render_data
= render "layouts/header/empty"
diff --git a/app/views/layouts/devise_empty.html.haml b/app/views/layouts/devise_empty.html.haml
index 6bd427b02ac..7466423a934 100644
--- a/app/views/layouts/devise_empty.html.haml
+++ b/app/views/layouts/devise_empty.html.haml
@@ -1,5 +1,5 @@
!!! 5
-%html{ lang: "en"}
+%html{ lang: "en" }
= render "layouts/head"
%body.ui_charcoal.login-page.application.navless
= Gon::Base.render_data
diff --git a/app/views/layouts/devise_mailer.html.haml b/app/views/layouts/devise_mailer.html.haml
index c258eafdd51..e1e1f9ae516 100644
--- a/app/views/layouts/devise_mailer.html.haml
+++ b/app/views/layouts/devise_mailer.html.haml
@@ -1,7 +1,7 @@
!!! 5
%html
%head
- %meta(content='text/html; charset=UTF-8' http-equiv='Content-Type')
+ %meta{ content: 'text/html; charset=UTF-8', 'http-equiv'=> 'Content-Type' }
= stylesheet_link_tag 'mailers/devise'
%body
@@ -9,7 +9,7 @@
%tr
%td
%table#header
- %td{valign: "top"}
+ %td{ valign: "top" }
= image_tag('mailers/gitlab_header_logo.png', id: 'logo', alt: 'GitLab Wordmark')
%table#body
diff --git a/app/views/layouts/errors.html.haml b/app/views/layouts/errors.html.haml
index a3b925f6afd..6d9ec043590 100644
--- a/app/views/layouts/errors.html.haml
+++ b/app/views/layouts/errors.html.haml
@@ -1,7 +1,7 @@
!!! 5
-%html{ lang: "en"}
+%html{ lang: "en" }
%head
- %meta{:content => "width=device-width, initial-scale=1, maximum-scale=1", :name => "viewport"}
+ %meta{ :content => "width=device-width, initial-scale=1, maximum-scale=1", :name => "viewport" }
%title= yield(:title)
:css
body {
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index 5456be77aab..9ecc0d11c95 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -1,11 +1,11 @@
%header.navbar.navbar-fixed-top.navbar-gitlab{ class: nav_header_class }
- %a{ href: "#content-body", tabindex: "1", class: "sr-only gl-accessibility" } Skip to content
- %div{ class: "container-fluid" }
+ %a.sr-only.gl-accessibility{ href: "#content-body", tabindex: "1" } Skip to content
+ .container-fluid
.header-content
%button.side-nav-toggle{ type: 'button', "aria-label" => "Toggle global navigation" }
%span.sr-only Toggle navigation
= icon('bars')
- %button.navbar-toggle{type: 'button'}
+ %button.navbar-toggle{ type: 'button' }
%span.sr-only Toggle navigation
= icon('ellipsis-v')
@@ -44,7 +44,7 @@
%li
= link_to "Profile", current_user, class: 'profile-link', aria: { label: "Profile" }, data: { user: current_user.username }
%li
- = link_to "Profile Settings", profile_path, aria: { label: "Profile Settings" }
+ = link_to "Settings", profile_path, aria: { label: "Settings" }
%li
= link_to "Help", help_path, aria: { label: "Help" }
%li.divider
diff --git a/app/views/layouts/nav/_admin.html.haml b/app/views/layouts/nav/_admin.html.haml
index ac04f57e217..19a947af4ca 100644
--- a/app/views/layouts/nav/_admin.html.haml
+++ b/app/views/layouts/nav/_admin.html.haml
@@ -1,5 +1,5 @@
+= render 'layouts/nav/admin_settings'
.scrolling-tabs-container{ class: nav_control_class }
- = render 'layouts/nav/admin_settings'
.fade-left
= icon('angle-left')
.fade-right
diff --git a/app/views/layouts/nav/_admin_settings.html.haml b/app/views/layouts/nav/_admin_settings.html.haml
index 38e9b80d129..9de0e12a826 100644
--- a/app/views/layouts/nav/_admin_settings.html.haml
+++ b/app/views/layouts/nav/_admin_settings.html.haml
@@ -1,6 +1,6 @@
.controls
.dropdown.admin-settings-dropdown
- %a.dropdown-new.btn.btn-default{href: '#', 'data-toggle' => 'dropdown'}
+ %a.dropdown-new.btn.btn-default{ href: '#', 'data-toggle' => 'dropdown' }
= icon('cog')
= icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right
diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml
index 817e4bebb05..205d23178d2 100644
--- a/app/views/layouts/nav/_dashboard.html.haml
+++ b/app/views/layouts/nav/_dashboard.html.haml
@@ -35,3 +35,7 @@
= link_to dashboard_snippets_path, title: 'Snippets' do
%span
Snippets
+
+ = link_to help_path, title: 'About GitLab CE', class: 'about-gitlab' do
+ %span
+ About GitLab CE
diff --git a/app/views/layouts/nav/_group_settings.html.haml b/app/views/layouts/nav/_group_settings.html.haml
index 1579d8f1662..30feb6813b4 100644
--- a/app/views/layouts/nav/_group_settings.html.haml
+++ b/app/views/layouts/nav/_group_settings.html.haml
@@ -5,7 +5,7 @@
- if can_admin_group || can_edit
.controls
.dropdown.group-settings-dropdown
- %a.dropdown-new.btn.btn-default#group-settings-button{href: '#', 'data-toggle' => 'dropdown'}
+ %a.dropdown-new.btn.btn-default#group-settings-button{ href: '#', 'data-toggle' => 'dropdown' }
= icon('cog')
= icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right
diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml
index 904d11c2cf4..7883823b21e 100644
--- a/app/views/layouts/nav/_project.html.haml
+++ b/app/views/layouts/nav/_project.html.haml
@@ -1,7 +1,7 @@
- if current_user
.controls
.dropdown.project-settings-dropdown
- %a.dropdown-new.btn.btn-default#project-settings-button{href: '#', 'data-toggle' => 'dropdown'}
+ %a.dropdown-new.btn.btn-default#project-settings-button{ href: '#', 'data-toggle' => 'dropdown' }
= icon('cog')
= icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right
@@ -96,8 +96,8 @@
-# Shortcut to builds page
- if project_nav_tab? :builds
%li.hidden
- = link_to project_builds_path(@project), title: 'Builds', class: 'shortcuts-builds' do
- Builds
+ = link_to project_builds_path(@project), title: 'Jobs', class: 'shortcuts-builds' do
+ Jobs
-# Shortcut to commits page
- if project_nav_tab? :commits
diff --git a/app/views/layouts/nav/_project_settings.html.haml b/app/views/layouts/nav/_project_settings.html.haml
index 613b8b7d301..665725f6862 100644
--- a/app/views/layouts/nav/_project_settings.html.haml
+++ b/app/views/layouts/nav/_project_settings.html.haml
@@ -1,45 +1,28 @@
- if project_nav_tab? :team
- = nav_link(controller: [:project_members, :teams]) do
- = link_to namespace_project_project_members_path(@project.namespace, @project), title: 'Members', class: 'team-tab tab' do
+ = nav_link(controller: [:members, :teams]) do
+ = link_to namespace_project_settings_members_path(@project.namespace, @project), title: 'Members', class: 'team-tab tab' do
%span
Members
- if can_edit
- - if @project.allowed_to_share_with_group?
- = nav_link(controller: :group_links) do
- = link_to namespace_project_group_links_path(@project.namespace, @project), title: "Groups" do
- %span
- Groups
= nav_link(controller: :deploy_keys) do
= link_to namespace_project_deploy_keys_path(@project.namespace, @project), title: 'Deploy Keys' do
%span
Deploy Keys
- = nav_link(controller: :hooks) do
- = link_to namespace_project_hooks_path(@project.namespace, @project), title: 'Webhooks' do
- %span
- Webhooks
- = nav_link(controller: :services) do
- = link_to namespace_project_services_path(@project.namespace, @project), title: 'Services' do
+ = nav_link(controller: :integrations) do
+ = link_to namespace_project_settings_integrations_path(@project.namespace, @project), title: 'Integrations' do
%span
- Services
+ Integrations
= nav_link(controller: :protected_branches) do
= link_to namespace_project_protected_branches_path(@project.namespace, @project), title: 'Protected Branches' do
%span
Protected Branches
- if @project.feature_available?(:builds, current_user)
- = nav_link(controller: :runners) do
- = link_to namespace_project_runners_path(@project.namespace, @project), title: 'Runners' do
- %span
- Runners
- = nav_link(controller: :variables) do
- = link_to namespace_project_variables_path(@project.namespace, @project), title: 'Variables' do
- %span
- Variables
- = nav_link(controller: :triggers) do
- = link_to namespace_project_triggers_path(@project.namespace, @project), title: 'Triggers' do
- %span
- Triggers
- = nav_link(controller: :pipelines_settings) do
- = link_to namespace_project_pipelines_settings_path(@project.namespace, @project), title: 'CI/CD Pipelines' do
+ = nav_link(controller: :ci_cd) do
+ = link_to namespace_project_settings_ci_cd_path(@project.namespace, @project), title: 'CI/CD Pipelines' do
%span
CI/CD Pipelines
+ = nav_link(controller: :pages) do
+ = link_to namespace_project_pages_path(@project.namespace, @project), title: 'Pages', data: {placement: 'right'} do
+ %span
+ Pages
diff --git a/app/views/layouts/notify.html.haml b/app/views/layouts/notify.html.haml
index 1ec4c3f0c67..76268c1b705 100644
--- a/app/views/layouts/notify.html.haml
+++ b/app/views/layouts/notify.html.haml
@@ -1,14 +1,14 @@
-%html{lang: "en"}
+%html{ lang: "en" }
%head
- %meta{content: "text/html; charset=utf-8", "http-equiv" => "Content-Type"}
+ %meta{ content: "text/html; charset=utf-8", "http-equiv" => "Content-Type" }
%title
GitLab
= stylesheet_link_tag 'notify'
= yield :head
%body
- %div.content
+ .content
= yield
- %div.footer{style: "margin-top: 10px;"}
+ .footer{ style: "margin-top: 10px;" }
%p
&mdash;
%br
diff --git a/app/views/layouts/profile.html.haml b/app/views/layouts/profile.html.haml
index b77d3402a2e..0ee8a57dbd4 100644
--- a/app/views/layouts/profile.html.haml
+++ b/app/views/layouts/profile.html.haml
@@ -1,5 +1,5 @@
-- page_title "Profile Settings"
-- header_title "Profile Settings", profile_path unless header_title
+- page_title "User Settings"
+- header_title "User Settings", profile_path unless header_title
- sidebar "dashboard"
- nav "profile"
diff --git a/app/views/layouts/project.html.haml b/app/views/layouts/project.html.haml
index 277eb71ea73..f5e7ea7710d 100644
--- a/app/views/layouts/project.html.haml
+++ b/app/views/layouts/project.html.haml
@@ -3,7 +3,7 @@
- header_title project_title(@project) unless header_title
- nav "project"
-- content_for :scripts_body_top do
+- content_for :project_javascripts do
- project = @target_project || @project
- if @project_wiki && @page
- preview_markdown_path = namespace_project_wiki_preview_markdown_path(project.namespace, project, @page.slug)
diff --git a/app/views/notify/_reassigned_issuable_email.html.haml b/app/views/notify/_reassigned_issuable_email.html.haml
index 56d81b2ed2e..fd35713f79c 100644
--- a/app/views/notify/_reassigned_issuable_email.html.haml
+++ b/app/views/notify/_reassigned_issuable_email.html.haml
@@ -2,9 +2,9 @@
Assignee changed
- if @previous_assignee
from
- %strong #{@previous_assignee.name}
+ %strong= @previous_assignee.name
to
- if issuable.assignee_id
- %strong #{issuable.assignee_name}
+ %strong= issuable.assignee_name
- else
%strong Unassigned
diff --git a/app/views/notify/build_fail_email.html.haml b/app/views/notify/build_fail_email.html.haml
index 4bf7c1f4d64..060b50ffc69 100644
--- a/app/views/notify/build_fail_email.html.haml
+++ b/app/views/notify/build_fail_email.html.haml
@@ -1,6 +1,6 @@
- content_for :header do
- %h1{style: "background: #c40834; color: #FFF; font: normal 20px Helvetica, Arial, sans-serif; margin: 0; padding: 5px 10px; line-height: 32px; font-size: 16px;"}
- GitLab (build failed)
+ %h1{ style: "background: #c40834; color: #FFF; font: normal 20px Helvetica, Arial, sans-serif; margin: 0; padding: 5px 10px; line-height: 32px; font-size: 16px;" }
+ GitLab (job failed)
%h3
Project:
@@ -21,4 +21,4 @@
Message: #{@build.pipeline.git_commit_message}
%p
- Build details: #{link_to "Build #{@build.id}", namespace_project_build_url(@build.project.namespace, @build.project, @build)}
+ Job details: #{link_to "Job #{@build.id}", namespace_project_build_url(@build.project.namespace, @build.project, @build)}
diff --git a/app/views/notify/build_fail_email.text.erb b/app/views/notify/build_fail_email.text.erb
index 9d497983498..2a94688a6b0 100644
--- a/app/views/notify/build_fail_email.text.erb
+++ b/app/views/notify/build_fail_email.text.erb
@@ -1,4 +1,4 @@
-Build failed for <%= @project.name %>
+Job failed for <%= @project.name %>
Status: <%= @build.status %>
Commit: <%= @build.pipeline.short_sha %>
diff --git a/app/views/notify/build_success_email.html.haml b/app/views/notify/build_success_email.html.haml
index 252a5b7152c..ca0eaa96a9d 100644
--- a/app/views/notify/build_success_email.html.haml
+++ b/app/views/notify/build_success_email.html.haml
@@ -1,6 +1,6 @@
- content_for :header do
- %h1{style: "background: #38CF5B; color: #FFF; font: normal 20px Helvetica, Arial, sans-serif; margin: 0; padding: 5px 10px; line-height: 32px; font-size: 16px;"}
- GitLab (build successful)
+ %h1{ style: "background: #38CF5B; color: #FFF; font: normal 20px Helvetica, Arial, sans-serif; margin: 0; padding: 5px 10px; line-height: 32px; font-size: 16px;" }
+ GitLab (job successful)
%h3
Project:
@@ -21,4 +21,4 @@
Message: #{@build.pipeline.git_commit_message}
%p
- Build details: #{link_to "Build #{@build.id}", namespace_project_build_url(@build.project.namespace, @build.project, @build)}
+ Job details: #{link_to "Job #{@build.id}", namespace_project_build_url(@build.project.namespace, @build.project, @build)}
diff --git a/app/views/notify/build_success_email.text.erb b/app/views/notify/build_success_email.text.erb
index c5ed4f84861..445cd46e64f 100644
--- a/app/views/notify/build_success_email.text.erb
+++ b/app/views/notify/build_success_email.text.erb
@@ -1,4 +1,4 @@
-Build successful for <%= @project.name %>
+Job successful for <%= @project.name %>
Status: <%= @build.status %>
Commit: <%= @build.pipeline.short_sha %>
diff --git a/app/views/notify/closed_issue_email.html.haml b/app/views/notify/closed_issue_email.html.haml
index 56c18cd83cd..b7284dd819b 100644
--- a/app/views/notify/closed_issue_email.html.haml
+++ b/app/views/notify/closed_issue_email.html.haml
@@ -1,2 +1,2 @@
%p
- = "Issue was closed by #{@updated_by.name}"
+ Issue was closed by #{@updated_by.name}
diff --git a/app/views/notify/closed_issue_email.text.haml b/app/views/notify/closed_issue_email.text.haml
index ac703b31edd..bc12e38675f 100644
--- a/app/views/notify/closed_issue_email.text.haml
+++ b/app/views/notify/closed_issue_email.text.haml
@@ -1,3 +1,3 @@
-= "Issue was closed by #{@updated_by.name}"
+Issue was closed by #{@updated_by.name}
Issue ##{@issue.iid}: #{namespace_project_issue_url(@issue.project.namespace, @issue.project, @issue)}
diff --git a/app/views/notify/closed_merge_request_email.html.haml b/app/views/notify/closed_merge_request_email.html.haml
index 81c7c88fc96..44e018304e1 100644
--- a/app/views/notify/closed_merge_request_email.html.haml
+++ b/app/views/notify/closed_merge_request_email.html.haml
@@ -1,2 +1,2 @@
%p
- = "Merge Request #{@merge_request.to_reference} was closed by #{@updated_by.name}"
+ Merge Request #{@merge_request.to_reference} was closed by #{@updated_by.name}
diff --git a/app/views/notify/closed_merge_request_email.text.haml b/app/views/notify/closed_merge_request_email.text.haml
index b435067d5a6..d0c96b83976 100644
--- a/app/views/notify/closed_merge_request_email.text.haml
+++ b/app/views/notify/closed_merge_request_email.text.haml
@@ -1,4 +1,4 @@
-= "Merge Request #{@merge_request.to_reference} was closed by #{@updated_by.name}"
+Merge Request #{@merge_request.to_reference} was closed by #{@updated_by.name}
Merge Request url: #{namespace_project_merge_request_url(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request)}
diff --git a/app/views/notify/issue_status_changed_email.html.haml b/app/views/notify/issue_status_changed_email.html.haml
index 482c884a9db..b6051b11cea 100644
--- a/app/views/notify/issue_status_changed_email.html.haml
+++ b/app/views/notify/issue_status_changed_email.html.haml
@@ -1,2 +1,2 @@
%p
- = "Issue was #{@issue_status} by #{@updated_by.name}"
+ Issue was #{@issue_status} by #{@updated_by.name}
diff --git a/app/views/notify/links/ci/builds/_build.html.haml b/app/views/notify/links/ci/builds/_build.html.haml
index 38cd4e5e145..d35b3839171 100644
--- a/app/views/notify/links/ci/builds/_build.html.haml
+++ b/app/views/notify/links/ci/builds/_build.html.haml
@@ -1,2 +1,2 @@
-%a{href: pipeline_build_url(pipeline, build), style: "color:#3777b0;text-decoration:none;"}
+%a{ href: pipeline_build_url(pipeline, build), style: "color:#3777b0;text-decoration:none;" }
= build.name
diff --git a/app/views/notify/links/ci/builds/_build.text.erb b/app/views/notify/links/ci/builds/_build.text.erb
index f495a2e5486..741c7f344c8 100644
--- a/app/views/notify/links/ci/builds/_build.text.erb
+++ b/app/views/notify/links/ci/builds/_build.text.erb
@@ -1 +1 @@
-Build #<%= build.id %> ( <%= pipeline_build_url(pipeline, build) %> )
+Job #<%= build.id %> ( <%= pipeline_build_url(pipeline, build) %> )
diff --git a/app/views/notify/links/generic_commit_statuses/_generic_commit_status.text.erb b/app/views/notify/links/generic_commit_statuses/_generic_commit_status.text.erb
index 8e89c52a1f3..af8924bad57 100644
--- a/app/views/notify/links/generic_commit_statuses/_generic_commit_status.text.erb
+++ b/app/views/notify/links/generic_commit_statuses/_generic_commit_status.text.erb
@@ -1 +1 @@
-Build #<%= build.id %>
+Job #<%= build.id %>
diff --git a/app/views/notify/merge_request_status_email.html.haml b/app/views/notify/merge_request_status_email.html.haml
index 41a320d6bd8..b487e26b122 100644
--- a/app/views/notify/merge_request_status_email.html.haml
+++ b/app/views/notify/merge_request_status_email.html.haml
@@ -1,2 +1,2 @@
%p
- = "Merge Request #{@merge_request.to_reference} was #{@mr_status} by #{@updated_by.name}"
+ Merge Request #{@merge_request.to_reference} was #{@mr_status} by #{@updated_by.name}
diff --git a/app/views/notify/merge_request_status_email.text.haml b/app/views/notify/merge_request_status_email.text.haml
index 7a5074a1dc3..4c9719ba732 100644
--- a/app/views/notify/merge_request_status_email.text.haml
+++ b/app/views/notify/merge_request_status_email.text.haml
@@ -1,4 +1,4 @@
-= "Merge Request #{@merge_request.to_reference} was #{@mr_status} by #{@updated_by.name}"
+Merge Request #{@merge_request.to_reference} was #{@mr_status} by #{@updated_by.name}
Merge Request url: #{namespace_project_merge_request_url(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request)}
diff --git a/app/views/notify/merged_merge_request_email.html.haml b/app/views/notify/merged_merge_request_email.html.haml
index fbe506d4f4d..0fe54e73313 100644
--- a/app/views/notify/merged_merge_request_email.html.haml
+++ b/app/views/notify/merged_merge_request_email.html.haml
@@ -1,2 +1,2 @@
%p
- = "Merge Request #{@merge_request.to_reference} was merged"
+ Merge Request #{@merge_request.to_reference} was merged
diff --git a/app/views/notify/merged_merge_request_email.text.haml b/app/views/notify/merged_merge_request_email.text.haml
index bfbae01094f..46c1c9dee0b 100644
--- a/app/views/notify/merged_merge_request_email.text.haml
+++ b/app/views/notify/merged_merge_request_email.text.haml
@@ -1,4 +1,4 @@
-= "Merge Request #{@merge_request.to_reference} was merged"
+Merge Request #{@merge_request.to_reference} was merged
Merge Request url: #{namespace_project_merge_request_url(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request)}
diff --git a/app/views/notify/new_issue_email.html.haml b/app/views/notify/new_issue_email.html.haml
index f42b150c0d6..d1855568215 100644
--- a/app/views/notify/new_issue_email.html.haml
+++ b/app/views/notify/new_issue_email.html.haml
@@ -1,7 +1,7 @@
- if current_application_settings.email_author_in_body
%div
#{link_to @issue.author_name, user_url(@issue.author)} wrote:
--if @issue.description
+- if @issue.description
= markdown(@issue.description, pipeline: :email, author: @issue.author)
- if @issue.assignee_id.present?
diff --git a/app/views/notify/new_mention_in_issue_email.html.haml b/app/views/notify/new_mention_in_issue_email.html.haml
index 4f3d36bd9ca..02f21baa368 100644
--- a/app/views/notify/new_mention_in_issue_email.html.haml
+++ b/app/views/notify/new_mention_in_issue_email.html.haml
@@ -4,7 +4,7 @@
- if current_application_settings.email_author_in_body
%div
#{link_to @issue.author_name, user_url(@issue.author)} wrote:
--if @issue.description
+- if @issue.description
= markdown(@issue.description, pipeline: :email, author: @issue.author)
- if @issue.assignee_id.present?
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
index 32aedb9e6b9..cbd434be02a 100644
--- a/app/views/notify/new_mention_in_merge_request_email.html.haml
+++ b/app/views/notify/new_mention_in_merge_request_email.html.haml
@@ -11,5 +11,5 @@
%p
Assignee: #{@merge_request.author_name} &rarr; #{@merge_request.assignee_name}
--if @merge_request.description
+- if @merge_request.description
= markdown(@merge_request.description, pipeline: :email, author: @merge_request.author)
diff --git a/app/views/notify/new_merge_request_email.html.haml b/app/views/notify/new_merge_request_email.html.haml
index 158404de396..8890b300f7d 100644
--- a/app/views/notify/new_merge_request_email.html.haml
+++ b/app/views/notify/new_merge_request_email.html.haml
@@ -8,5 +8,5 @@
%p
Assignee: #{@merge_request.author_name} &rarr; #{@merge_request.assignee_name}
--if @merge_request.description
+- if @merge_request.description
= markdown(@merge_request.description, pipeline: :email, author: @merge_request.author)
diff --git a/app/views/notify/note_personal_snippet_email.html.haml b/app/views/notify/note_personal_snippet_email.html.haml
new file mode 100644
index 00000000000..2fa2f784661
--- /dev/null
+++ b/app/views/notify/note_personal_snippet_email.html.haml
@@ -0,0 +1 @@
+= render 'note_message'
diff --git a/app/views/notify/note_personal_snippet_email.text.erb b/app/views/notify/note_personal_snippet_email.text.erb
new file mode 100644
index 00000000000..b2a8809a23b
--- /dev/null
+++ b/app/views/notify/note_personal_snippet_email.text.erb
@@ -0,0 +1,8 @@
+New comment for Snippet <%= @snippet.id %>
+
+<%= url_for(snippet_url(@snippet, anchor: "note_#{@note.id}")) %>
+
+
+Author: <%= @note.author_name %>
+
+<%= @note.note %>
diff --git a/app/views/notify/pipeline_failed_email.html.haml b/app/views/notify/pipeline_failed_email.html.haml
index 001d9c48555..d9ebbaa2704 100644
--- a/app/views/notify/pipeline_failed_email.html.haml
+++ b/app/views/notify/pipeline_failed_email.html.haml
@@ -1,9 +1,9 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional //EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
-%html{lang: "en"}
+%html{ lang: "en" }
%head
- %meta{content: "text/html; charset=UTF-8", "http-equiv" => "Content-Type"}/
- %meta{content: "width=device-width, initial-scale=1", name: "viewport"}/
- %meta{content: "IE=edge", "http-equiv" => "X-UA-Compatible"}/
+ %meta{ content: "text/html; charset=UTF-8", "http-equiv" => "Content-Type" }/
+ %meta{ content: "width=device-width, initial-scale=1", name: "viewport" }/
+ %meta{ content: "IE=edge", "http-equiv" => "X-UA-Compatible" }/
%title= message.subject
:css
/* CLIENT-SPECIFIC STYLES */
@@ -41,139 +41,139 @@
padding-right: 10px !important;
}
}
- %body{style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;height:100%;font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;"}
- %table#body{border: "0", cellpadding: "0", cellspacing: "0", style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;"}
+ %body{ style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;height:100%;font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;" }
+ %table#body{ border: "0", cellpadding: "0", cellspacing: "0", style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;" }
%tbody
%tr.line
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#6b4fbb;height:4px;font-size:4px;line-height:4px;"}  
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#6b4fbb;height:4px;font-size:4px;line-height:4px;" }  
%tr.header
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;"}
- %img{alt: "GitLab", height: "50", src: image_url('mailers/ci_pipeline_notif_v1/gitlab-logo.gif'), width: "55"}/
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" }
+ %img{ alt: "GitLab", height: "50", src: image_url('mailers/ci_pipeline_notif_v1/gitlab-logo.gif'), width: "55" }/
%tr
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;"}
- %table.wrapper{border: "0", cellpadding: "0", cellspacing: "0", style: "width:640px;margin:0 auto;border-collapse:separate;border-spacing:0;"}
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;" }
+ %table.wrapper{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:640px;margin:0 auto;border-collapse:separate;border-spacing:0;" }
%tbody
%tr
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#ffffff;text-align:left;padding:18px 25px;border:1px solid #ededed;border-radius:3px;overflow:hidden;"}
- %table.content{border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;border-collapse:separate;border-spacing:0;"}
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#ffffff;text-align:left;padding:18px 25px;border:1px solid #ededed;border-radius:3px;overflow:hidden;" }
+ %table.content{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;border-collapse:separate;border-spacing:0;" }
%tbody
%tr.alert
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:10px;border-radius:3px;font-size:14px;line-height:1.3;text-align:center;overflow:hidden;background-color:#d22f57;color:#ffffff;"}
- %table.img{border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;margin:0 auto;"}
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:10px;border-radius:3px;font-size:14px;line-height:1.3;text-align:center;overflow:hidden;background-color:#d22f57;color:#ffffff;" }
+ %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;margin:0 auto;" }
%tbody
%tr
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;padding-right:5px;"}
- %img{alt: "x", height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-x-red-inverted.gif'), style: "display:block;", width: "13"}/
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;"}
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;padding-right:5px;" }
+ %img{ alt: "x", height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-x-red-inverted.gif'), style: "display:block;", width: "13" }/
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;" }
Your pipeline has failed.
%tr.spacer
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;"}
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;" }
&nbsp;
%tr.section
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;"}
- %table.info{border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;"}
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;" }
+ %table.info{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;" }
%tbody
%tr
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;"} Project
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;"}
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;" } Project
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;" }
- namespace_name = @project.group ? @project.group.name : @project.namespace.owner.name
- namespace_url = @project.group ? group_url(@project.group) : user_url(@project.namespace.owner)
- %a.muted{href: namespace_url, style: "color:#333333;text-decoration:none;"}
+ %a.muted{ href: namespace_url, style: "color:#333333;text-decoration:none;" }
= namespace_name
\/
- %a.muted{href: project_url(@project), style: "color:#333333;text-decoration:none;"}
+ %a.muted{ href: project_url(@project), style: "color:#333333;text-decoration:none;" }
= @project.name
%tr
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;"} Branch
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;"}
- %table.img{border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;"}
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Branch
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" }
+ %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" }
%tbody
%tr
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;"}
- %img{height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-branch-gray.gif'), style: "display:block;", width: "13"}/
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;"}
- %a.muted{href: commits_url(@pipeline), style: "color:#333333;text-decoration:none;"}
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
+ %img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-branch-gray.gif'), style: "display:block;", width: "13", alt: "Branch icon" }/
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
+ %a.muted{ href: commits_url(@pipeline), style: "color:#333333;text-decoration:none;" }
= @pipeline.ref
%tr
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;"} Commit
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;"}
- %table.img{border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;"}
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Commit
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" }
+ %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" }
%tbody
%tr
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;"}
- %img{height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-commit-gray.gif'), style: "display:block;", width: "13"}/
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;"}
- %a{href: commit_url(@pipeline), style: "color:#3777b0;text-decoration:none;"}
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
+ %img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-commit-gray.gif'), style: "display:block;", width: "13", alt: "Commit icon" }/
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
+ %a{ href: commit_url(@pipeline), style: "color:#3777b0;text-decoration:none;" }
= @pipeline.short_sha
- if @merge_request
in
- %a{href: merge_request_url(@merge_request), style: "color:#3777b0;text-decoration:none;"}
+ %a{ href: merge_request_url(@merge_request), style: "color:#3777b0;text-decoration:none;" }
= @merge_request.to_reference
- .commit{style: "color:#5c5c5c;font-weight:300;"}
+ .commit{ style: "color:#5c5c5c;font-weight:300;" }
= @pipeline.git_commit_message.truncate(50)
%tr
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;"} Author
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;"}
- %table.img{border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;"}
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Author
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" }
+ %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" }
%tbody
%tr
- commit = @pipeline.commit
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;"}
- %img.avatar{height: "24", src: avatar_icon(commit.author || commit.author_email, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24"}/
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;"}
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
+ %img.avatar{ height: "24", src: avatar_icon(commit.author || commit.author_email, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
- if commit.author
- %a.muted{href: user_url(commit.author), style: "color:#333333;text-decoration:none;"}
+ %a.muted{ href: user_url(commit.author), style: "color:#333333;text-decoration:none;" }
= commit.author.name
- else
%span
= commit.author_name
%tr.spacer
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;"}
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;" }
&nbsp;
- failed = @pipeline.statuses.latest.failed
%tr.pre-section
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#333333;font-size:15px;font-weight:400;line-height:1.4;padding:15px 0;"}
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#333333;font-size:15px;font-weight:400;line-height:1.4;padding:15px 0;" }
Pipeline
- %a{href: pipeline_url(@pipeline), style: "color:#3777b0;text-decoration:none;"}
+ %a{ href: pipeline_url(@pipeline), style: "color:#3777b0;text-decoration:none;" }
= "\##{@pipeline.id}"
had
= failed.size
failed
- = "#{'build'.pluralize(failed.size)}."
+ #{'build'.pluralize(failed.size)}.
%tr.warning
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;border:1px solid #ededed;border-bottom:0;border-radius:3px 3px 0 0;overflow:hidden;background-color:#fdf4f6;color:#d22852;font-size:14px;line-height:1.4;text-align:center;padding:8px 15px;"}
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;border:1px solid #ededed;border-bottom:0;border-radius:3px 3px 0 0;overflow:hidden;background-color:#fdf4f6;color:#d22852;font-size:14px;line-height:1.4;text-align:center;padding:8px 15px;" }
Logs may contain sensitive data. Please consider before forwarding this email.
%tr.section
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;border-top:0;border-radius:0 0 3px 3px;"}
- %table.builds{border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;border-collapse:collapse;"}
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;border-top:0;border-radius:0 0 3px 3px;" }
+ %table.builds{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;border-collapse:collapse;" }
%tbody
- failed.each do |build|
%tr.build-state
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:20px 0;color:#8c8c8c;font-weight:500;font-size:15px;"}
- %table.img{border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;"}
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:20px 0;color:#8c8c8c;font-weight:500;font-size:15px;" }
+ %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" }
%tbody
%tr
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#8c8c8c;font-weight:500;font-size:15px;vertical-align:middle;padding-right:5px;"}
- %img{alt: "x", height: "10", src: image_url('mailers/ci_pipeline_notif_v1/icon-x-red.gif'), style: "display:block;", width: "10"}/
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#8c8c8c;font-weight:500;font-size:15px;vertical-align:middle;"}
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#8c8c8c;font-weight:500;font-size:15px;vertical-align:middle;padding-right:5px;" }
+ %img{ alt: "x", height: "10", src: image_url('mailers/ci_pipeline_notif_v1/icon-x-red.gif'), style: "display:block;", width: "10" }/
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#8c8c8c;font-weight:500;font-size:15px;vertical-align:middle;" }
= build.stage
- %td{align: "right", style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:20px 0;color:#8c8c8c;font-weight:500;font-size:15px;"}
+ %td{ align: "right", style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:20px 0;color:#8c8c8c;font-weight:500;font-size:15px;" }
= render "notify/links/#{build.to_partial_path}", pipeline: @pipeline, build: build
%tr.build-log
- if build.has_trace?
- %td{colspan: "2", style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 0 15px;"}
- %pre{style: "font-family:Monaco,'Lucida Console','Courier New',Courier,monospace;background-color:#fafafa;border-radius:3px;overflow:hidden;white-space:pre-wrap;word-break:break-all;font-size:13px;line-height:1.4;padding:12px;color:#333333;margin:0;"}
+ %td{ colspan: "2", style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 0 15px;" }
+ %pre{ style: "font-family:Monaco,'Lucida Console','Courier New',Courier,monospace;background-color:#fafafa;border-radius:3px;overflow:hidden;white-space:pre-wrap;word-break:break-all;font-size:13px;line-height:1.4;padding:12px;color:#333333;margin:0;" }
= build.trace_html(last_lines: 10).html_safe
- else
- %td{colspan: "2"}
+ %td{ colspan: "2" }
%tr.footer
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;"}
- %img{alt: "GitLab", height: "33", src: image_url('mailers/ci_pipeline_notif_v1/gitlab-logo-full-horizontal.gif'), style: "display:block;margin:0 auto 1em;", width: "90"}/
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" }
+ %img{ alt: "GitLab", height: "33", src: image_url('mailers/ci_pipeline_notif_v1/gitlab-logo-full-horizontal.gif'), style: "display:block;margin:0 auto 1em;", width: "90" }/
%div
- %a{href: profile_notifications_url, style: "color:#3777b0;text-decoration:none;"} Manage all notifications
+ %a{ href: profile_notifications_url, style: "color:#3777b0;text-decoration:none;" } Manage all notifications
&middot;
- %a{href: help_url, style: "color:#3777b0;text-decoration:none;"} Help
+ %a{ href: help_url, style: "color:#3777b0;text-decoration:none;" } Help
%div
You're receiving this email because of your account on
= succeed "." do
- %a{href: root_url, style: "color:#3777b0;text-decoration:none;"}= Gitlab.config.gitlab.host
+ %a{ href: root_url, style: "color:#3777b0;text-decoration:none;" }= Gitlab.config.gitlab.host
diff --git a/app/views/notify/pipeline_success_email.html.haml b/app/views/notify/pipeline_success_email.html.haml
index 56c1949ab2b..8add2e18206 100644
--- a/app/views/notify/pipeline_success_email.html.haml
+++ b/app/views/notify/pipeline_success_email.html.haml
@@ -1,9 +1,9 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional //EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
-%html{lang: "en"}
+%html{ lang: "en" }
%head
- %meta{content: "text/html; charset=UTF-8", "http-equiv" => "Content-Type"}/
- %meta{content: "width=device-width, initial-scale=1", name: "viewport"}/
- %meta{content: "IE=edge", "http-equiv" => "X-UA-Compatible"}/
+ %meta{ content: "text/html; charset=UTF-8", "http-equiv" => "Content-Type" }/
+ %meta{ content: "width=device-width, initial-scale=1", name: "viewport" }/
+ %meta{ content: "IE=edge", "http-equiv" => "X-UA-Compatible" }/
%title= message.subject
:css
/* CLIENT-SPECIFIC STYLES */
@@ -41,114 +41,114 @@
padding-right: 10px !important;
}
}
- %body{style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;height:100%;font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;"}
- %table#body{border: "0", cellpadding: "0", cellspacing: "0", style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;"}
+ %body{ style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;height:100%;font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;" }
+ %table#body{ border: "0", cellpadding: "0", cellspacing: "0", style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;" }
%tbody
%tr.line
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#6b4fbb;height:4px;font-size:4px;line-height:4px;"}  
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#6b4fbb;height:4px;font-size:4px;line-height:4px;" }  
%tr.header
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;"}
- %img{alt: "GitLab", height: "50", src: image_url('mailers/ci_pipeline_notif_v1/gitlab-logo.gif'), width: "55"}/
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" }
+ %img{ alt: "GitLab", height: "50", src: image_url('mailers/ci_pipeline_notif_v1/gitlab-logo.gif'), width: "55" }/
%tr
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;"}
- %table.wrapper{border: "0", cellpadding: "0", cellspacing: "0", style: "width:640px;margin:0 auto;border-collapse:separate;border-spacing:0;"}
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;" }
+ %table.wrapper{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:640px;margin:0 auto;border-collapse:separate;border-spacing:0;" }
%tbody
%tr
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#ffffff;text-align:left;padding:18px 25px;border:1px solid #ededed;border-radius:3px;overflow:hidden;"}
- %table.content{border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;border-collapse:separate;border-spacing:0;"}
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#ffffff;text-align:left;padding:18px 25px;border:1px solid #ededed;border-radius:3px;overflow:hidden;" }
+ %table.content{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;border-collapse:separate;border-spacing:0;" }
%tbody
%tr.success
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:10px;border-radius:3px;font-size:14px;line-height:1.3;text-align:center;overflow:hidden;color:#ffffff;background-color:#31af64;"}
- %table.img{border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;margin:0 auto;"}
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:10px;border-radius:3px;font-size:14px;line-height:1.3;text-align:center;overflow:hidden;color:#ffffff;background-color:#31af64;" }
+ %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;margin:0 auto;" }
%tbody
%tr
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;padding-right:5px;"}
- %img{alt: "✓", height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-check-green-inverted.gif'), style: "display:block;", width: "13"}/
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;"}
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;padding-right:5px;" }
+ %img{ alt: "✓", height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-check-green-inverted.gif'), style: "display:block;", width: "13" }/
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;" }
Your pipeline has passed.
%tr.spacer
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;"}
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;" }
&nbsp;
%tr.section
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;"}
- %table.info{border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;"}
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;" }
+ %table.info{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;" }
%tbody
%tr
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;"} Project
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;"}
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;" } Project
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;" }
- namespace_name = @project.group ? @project.group.name : @project.namespace.owner.name
- namespace_url = @project.group ? group_url(@project.group) : user_url(@project.namespace.owner)
- %a.muted{href: namespace_url, style: "color:#333333;text-decoration:none;"}
+ %a.muted{ href: namespace_url, style: "color:#333333;text-decoration:none;" }
= namespace_name
\/
- %a.muted{href: project_url(@project), style: "color:#333333;text-decoration:none;"}
+ %a.muted{ href: project_url(@project), style: "color:#333333;text-decoration:none;" }
= @project.name
%tr
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;"} Branch
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;"}
- %table.img{border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;"}
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Branch
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" }
+ %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" }
%tbody
%tr
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;"}
- %img{height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-branch-gray.gif'), style: "display:block;", width: "13"}/
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;"}
- %a.muted{href: commits_url(@pipeline), style: "color:#333333;text-decoration:none;"}
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
+ %img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-branch-gray.gif'), style: "display:block;", width: "13", alt: "Branch icon" }/
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
+ %a.muted{ href: commits_url(@pipeline), style: "color:#333333;text-decoration:none;" }
= @pipeline.ref
%tr
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;"} Commit
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;"}
- %table.img{border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;"}
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Commit
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" }
+ %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" }
%tbody
%tr
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;"}
- %img{height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-commit-gray.gif'), style: "display:block;", width: "13"}/
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;"}
- %a{href: commit_url(@pipeline), style: "color:#3777b0;text-decoration:none;"}
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
+ %img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-commit-gray.gif'), style: "display:block;", width: "13", alt: "Commit icon" }/
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
+ %a{ href: commit_url(@pipeline), style: "color:#3777b0;text-decoration:none;" }
= @pipeline.short_sha
- if @merge_request
in
- %a{href: merge_request_url(@merge_request), style: "color:#3777b0;text-decoration:none;"}
+ %a{ href: merge_request_url(@merge_request), style: "color:#3777b0;text-decoration:none;" }
= @merge_request.to_reference
- .commit{style: "color:#5c5c5c;font-weight:300;"}
+ .commit{ style: "color:#5c5c5c;font-weight:300;" }
= @pipeline.git_commit_message.truncate(50)
%tr
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;"} Author
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;"}
- %table.img{border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;"}
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Author
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" }
+ %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" }
%tbody
%tr
- commit = @pipeline.commit
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;"}
- %img.avatar{height: "24", src: avatar_icon(commit.author || commit.author_email, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24"}/
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;"}
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
+ %img.avatar{ height: "24", src: avatar_icon(commit.author || commit.author_email, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
- if commit.author
- %a.muted{href: user_url(commit.author), style: "color:#333333;text-decoration:none;"}
+ %a.muted{ href: user_url(commit.author), style: "color:#333333;text-decoration:none;" }
= commit.author.name
- else
%span
= commit.author_name
%tr.spacer
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;"}
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;" }
&nbsp;
%tr.success-message
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#333333;font-size:15px;font-weight:400;line-height:1.4;padding:15px 5px;text-align:center;"}
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#333333;font-size:15px;font-weight:400;line-height:1.4;padding:15px 5px;text-align:center;" }
- build_count = @pipeline.statuses.latest.size
- stage_count = @pipeline.stages_count
Pipeline
- %a{href: pipeline_url(@pipeline), style: "color:#3777b0;text-decoration:none;"}
+ %a{ href: pipeline_url(@pipeline), style: "color:#3777b0;text-decoration:none;" }
= "\##{@pipeline.id}"
successfully completed
- = "#{build_count} #{'build'.pluralize(build_count)}"
+ #{build_count} #{'build'.pluralize(build_count)}
in
- = "#{stage_count} #{'stage'.pluralize(stage_count)}."
+ #{stage_count} #{'stage'.pluralize(stage_count)}.
%tr.footer
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;"}
- %img{alt: "GitLab", height: "33", src: image_url('mailers/ci_pipeline_notif_v1/gitlab-logo-full-horizontal.gif'), style: "display:block;margin:0 auto 1em;", width: "90"}/
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" }
+ %img{ alt: "GitLab", height: "33", src: image_url('mailers/ci_pipeline_notif_v1/gitlab-logo-full-horizontal.gif'), style: "display:block;margin:0 auto 1em;", width: "90" }/
%div
- %a{href: profile_notifications_url, style: "color:#3777b0;text-decoration:none;"} Manage all notifications
+ %a{ href: profile_notifications_url, style: "color:#3777b0;text-decoration:none;" } Manage all notifications
&middot;
- %a{href: help_url, style: "color:#3777b0;text-decoration:none;"} Help
+ %a{ href: help_url, style: "color:#3777b0;text-decoration:none;" } Help
%div
You're receiving this email because of your account on
= succeed "." do
- %a{href: root_url, style: "color:#3777b0;text-decoration:none;"}= Gitlab.config.gitlab.host
+ %a{ href: root_url, style: "color:#3777b0;text-decoration:none;" }= Gitlab.config.gitlab.host
diff --git a/app/views/notify/project_was_not_exported_email.text.haml b/app/views/notify/project_was_not_exported_email.text.haml
index b27cb620b9e..6c6902994a1 100644
--- a/app/views/notify/project_was_not_exported_email.text.haml
+++ b/app/views/notify/project_was_not_exported_email.text.haml
@@ -1,6 +1,6 @@
-= "Project #{@project.name} couldn't be exported."
+Project #{@project.name} couldn't be exported.
-= "The errors we encountered were:"
+The errors we encountered were:
- @errors.each do |error|
- #{error} \ No newline at end of file
+ #{error}
diff --git a/app/views/notify/repository_push_email.html.haml b/app/views/notify/repository_push_email.html.haml
index 25883de257c..c6b1db17f91 100644
--- a/app/views/notify/repository_push_email.html.haml
+++ b/app/views/notify/repository_push_email.html.haml
@@ -17,7 +17,7 @@
%ul
- @message.commits.each do |commit|
%li
- %strong #{link_to(commit.short_id, namespace_project_commit_url(@message.project_namespace, @message.project, commit))}
+ %strong= link_to(commit.short_id, namespace_project_commit_url(@message.project_namespace, @message.project, commit))
%div
%span by #{commit.author_name}
%i at #{commit.committed_date.to_s(:iso8601)}
@@ -29,7 +29,7 @@
%ul
- @message.diffs.each do |diff|
%li.file-stats
- %a{href: "#{@message.target_url if @message.disable_diffs?}##{hexdigest(diff.file_path)}" }
+ %a{ href: "#{@message.target_url if @message.disable_diffs?}##{hexdigest(diff.file_path)}" }
- if diff.deleted_file
%span.deleted-file
&minus;
@@ -54,8 +54,8 @@
%h4 Changes:
- diff_files.each do |diff_file|
- file_hash = hexdigest(diff_file.file_path)
- %li{id: file_hash}
- %a{href: @message.target_url + "##{file_hash}"}<
+ %li{ id: file_hash }
+ %a{ href: @message.target_url + "##{file_hash}" }<
- if diff_file.deleted_file
%strong<
= diff_file.old_path
diff --git a/app/views/profiles/_head.html.haml b/app/views/profiles/_head.html.haml
index 943ebdaeffe..1df04ea614e 100644
--- a/app/views/profiles/_head.html.haml
+++ b/app/views/profiles/_head.html.haml
@@ -1,3 +1,3 @@
- content_for :page_specific_javascripts do
= page_specific_javascript_tag('lib/cropper.js')
- = page_specific_javascript_tag('profile/profile_bundle.js')
+ = page_specific_javascript_bundle_tag('profile')
diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml
index 72f658d1b68..a4f4079d556 100644
--- a/app/views/profiles/accounts/show.html.haml
+++ b/app/views/profiles/accounts/show.html.haml
@@ -82,7 +82,7 @@
= link_to unlink_profile_account_path(provider: provider), method: :delete, class: 'provider-btn' do
Disconnect
- else
- = link_to omniauth_authorize_path(:user, provider), method: :post, class: 'provider-btn not-active', "data-no-turbolink" => "true" do
+ = link_to omniauth_authorize_path(:user, provider), method: :post, class: 'provider-btn not-active' do
Connect
%hr
- if current_user.can_change_username?
@@ -102,7 +102,7 @@
= f.text_field :username, required: true, class: 'form-control'
.help-block
Current path:
- = "#{root_url}#{current_user.username}"
+ #{root_url}#{current_user.username}
.prepend-top-default
= f.button class: "btn btn-warning", type: "submit" do
= icon "spinner spin", class: "hidden loading-username"
@@ -128,7 +128,7 @@
- if @user.solo_owned_groups.present?
%p
Your account is currently an owner in these groups:
- %strong #{@user.solo_owned_groups.map(&:name).join(', ')}
+ %strong= @user.solo_owned_groups.map(&:name).join(', ')
%p
You must transfer ownership or delete these groups before you can delete your account.
.append-bottom-default
diff --git a/app/views/profiles/chat_names/new.html.haml b/app/views/profiles/chat_names/new.html.haml
index f635acf96e2..5bf22aa94f1 100644
--- a/app/views/profiles/chat_names/new.html.haml
+++ b/app/views/profiles/chat_names/new.html.haml
@@ -1,5 +1,5 @@
%h3.page-title Authorization required
-%main{:role => "main"}
+%main{ :role => "main" }
%p.h4
Authorize
%strong.text-info= @chat_name_params[:chat_name]
diff --git a/app/views/profiles/keys/_key.html.haml b/app/views/profiles/keys/_key.html.haml
index 3276db6692c..d2a60ac2867 100644
--- a/app/views/profiles/keys/_key.html.haml
+++ b/app/views/profiles/keys/_key.html.haml
@@ -6,6 +6,9 @@
= key.title
.description
= key.fingerprint
+ .last-used-at
+ last used:
+ = key.last_used_at ? time_ago_with_tooltip(key.last_used_at) : 'n/a'
.pull-right
%span.key-created-at
created #{time_ago_with_tooltip(key.created_at)}
diff --git a/app/views/profiles/keys/_key_details.html.haml b/app/views/profiles/keys/_key_details.html.haml
index dd7615400dc..d44603c638c 100644
--- a/app/views/profiles/keys/_key_details.html.haml
+++ b/app/views/profiles/keys/_key_details.html.haml
@@ -11,6 +11,9 @@
%li
%span.light Created on:
%strong= @key.created_at.to_s(:medium)
+ %li
+ %span.light Last used on:
+ %strong= @key.last_used_at.try(:to_s, :medium) || 'N/A'
.col-md-8
%p
diff --git a/app/views/profiles/keys/index.html.haml b/app/views/profiles/keys/index.html.haml
index 93187873501..71b224a413b 100644
--- a/app/views/profiles/keys/index.html.haml
+++ b/app/views/profiles/keys/index.html.haml
@@ -17,5 +17,5 @@
%hr
%h5
Your SSH keys (#{@keys.count})
- %div.append-bottom-default
+ .append-bottom-default
= render 'key_table'
diff --git a/app/views/profiles/notifications/show.html.haml b/app/views/profiles/notifications/show.html.haml
index d79a1a9f368..5c5e5940365 100644
--- a/app/views/profiles/notifications/show.html.haml
+++ b/app/views/profiles/notifications/show.html.haml
@@ -3,7 +3,7 @@
%div
- if @user.errors.any?
- %div.alert.alert-danger
+ .alert.alert-danger
%ul
- @user.errors.full_messages.each do |msg|
%li= msg
diff --git a/app/views/profiles/personal_access_tokens/index.html.haml b/app/views/profiles/personal_access_tokens/index.html.haml
index bb4effeeeb1..60a561c9f9c 100644
--- a/app/views/profiles/personal_access_tokens/index.html.haml
+++ b/app/views/profiles/personal_access_tokens/index.html.haml
@@ -19,7 +19,7 @@
Your New Personal Access Token
.form-group
= text_field_tag 'created-personal-access-token', flash[:personal_access_token], readonly: true, class: "form-control", 'aria-describedby' => "created-personal-access-token-help-block"
- = clipboard_button(clipboard_text: flash[:personal_access_token])
+ = clipboard_button(clipboard_text: flash[:personal_access_token], title: "Copy personal access token to clipboard", placement: "left")
%span#created-personal-access-token-help-block.help-block.text-danger Make sure you save it - you won't be able to access it again.
%hr
diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml
index 2afa026847a..feadd863b00 100644
--- a/app/views/profiles/preferences/show.html.haml
+++ b/app/views/profiles/preferences/show.html.haml
@@ -1,7 +1,7 @@
- page_title 'Preferences'
= render 'profiles/head'
-= form_for @user, url: profile_preferences_path, remote: true, method: :put, html: {class: 'row prepend-top-default js-preferences-form'} do |f|
+= form_for @user, url: profile_preferences_path, remote: true, method: :put, html: { class: 'row prepend-top-default js-preferences-form' } do |f|
.col-lg-3.profile-settings-sidebar
%h4.prepend-top-0
Application theme
@@ -10,7 +10,7 @@
.col-lg-9.application-theme
- Gitlab::Themes.each do |theme|
= label_tag do
- .preview{class: theme.css_class}
+ .preview{ class: theme.css_class }
= f.radio_button :theme_id, theme.id
= theme.name
.col-sm-12
diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml
index 578af9fe98d..d551754a2e5 100644
--- a/app/views/profiles/show.html.haml
+++ b/app/views/profiles/show.html.haml
@@ -18,7 +18,8 @@
or change it at #{link_to Gitlab.config.gravatar.host, "http://" + Gitlab.config.gravatar.host}
.col-lg-9
.clearfix.avatar-image.append-bottom-default
- = image_tag avatar_icon(@user, 160), alt: '', class: 'avatar s160'
+ = link_to avatar_icon(@user, 400), target: '_blank' do
+ = image_tag avatar_icon(@user, 160), alt: '', class: 'avatar s160'
%h5.prepend-top-0
Upload new avatar
.prepend-top-5.append-bottom-10
@@ -30,7 +31,7 @@
The maximum file size allowed is 200KB.
- if @user.avatar?
%hr
- = link_to 'Remove avatar', profile_avatar_path, data: { confirm: "Avatar will be removed. Are you sure?"}, method: :delete, class: "btn btn-gray"
+ = link_to 'Remove avatar', profile_avatar_path, data: { confirm: "Avatar will be removed. Are you sure?" }, method: :delete, class: "btn btn-gray"
%hr
.row
.col-lg-3.profile-settings-sidebar
@@ -61,7 +62,7 @@
%span.help-block
Please click the link in the confirmation email before continuing. It was sent to
= succeed "." do
- %strong #{@user.unconfirmed_email}
+ %strong= @user.unconfirmed_email
%p
= link_to "Resend confirmation e-mail", user_confirmation_path(user: { email: @user.unconfirmed_email }), method: :post
@@ -69,7 +70,7 @@
%span.help-block We also use email for avatar detection if no avatar is uploaded.
.form-group
= f.label :public_email, class: "label-light"
- = f.select :public_email, options_for_select(@user.all_emails, selected: @user.public_email), {include_blank: 'Do not show on profile'}, class: "select2"
+ = f.select :public_email, options_for_select(@user.all_emails, selected: @user.public_email), { include_blank: 'Do not show on profile' }, class: "select2"
%span.help-block This email will be displayed on your public profile.
.form-group
= f.label :skype, class: "label-light"
@@ -101,14 +102,14 @@
.modal-dialog
.modal-content
.modal-header
- %button.close{:type => "button", :'data-dismiss' => "modal"}
+ %button.close{ :type => "button", :'data-dismiss' => "modal" }
%span
&times;
%h4.modal-title
Position and size your new avatar
.modal-body
.profile-crop-image-container
- %img.modal-profile-crop-image
+ %img.modal-profile-crop-image{ alt: "Avatar cropper" }
.crop-controls
.btn-group
%button.btn.btn-primary{ data: { method: "zoom", option: "0.1" } }
@@ -116,5 +117,5 @@
%button.btn.btn-primary{ data: { method: "zoom", option: "-0.1" } }
%span.fa.fa-search-minus
.modal-footer
- %button.btn.btn-primary.js-upload-user-avatar{:type => "button"}
+ %button.btn.btn-primary.js-upload-user-avatar{ :type => "button" }
Set new profile picture
diff --git a/app/views/projects/_activity.html.haml b/app/views/projects/_activity.html.haml
index 4f15f2997fb..0ea733cb978 100644
--- a/app/views/projects/_activity.html.haml
+++ b/app/views/projects/_activity.html.haml
@@ -9,7 +9,7 @@
= render 'shared/event_filter'
- .content_list.project-activity{:"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 e74fd5b93ea..c24a496486c 100644
--- a/app/views/projects/_bitbucket_import_modal.html.haml
+++ b/app/views/projects/_bitbucket_import_modal.html.haml
@@ -1,8 +1,8 @@
-%div#bitbucket_import_modal.modal
+#bitbucket_import_modal.modal
.modal-dialog
.modal-content
.modal-header
- %a.close{href: "#", "data-dismiss" => "modal"} ×
+ %a.close{ href: "#", "data-dismiss" => "modal" } ×
%h3 Import projects from Bitbucket
.modal-body
To enable importing projects from Bitbucket,
@@ -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/_customize_workflow.html.haml b/app/views/projects/_customize_workflow.html.haml
index d2c1e943db1..a41791f0eca 100644
--- a/app/views/projects/_customize_workflow.html.haml
+++ b/app/views/projects/_customize_workflow.html.haml
@@ -1,8 +1,8 @@
.row-content-block.project-home-empty
- %div.text-center{ class: container_class }
+ .text-center{ class: container_class }
%h4
Customize your workflow!
%p
- Get started with GitLab by enabling features that work best for your project. From issues and wikis, to merge requests and builds, GitLab can help manage your workflow from idea to production!
+ Get started with GitLab by enabling features that work best for your project. From issues and wikis, to merge requests and pipelines, GitLab can help manage your workflow from idea to production!
- if can?(current_user, :admin_project, @project)
= link_to "Get started", edit_project_path(@project), class: "btn btn-success"
diff --git a/app/views/projects/_find_file_link.html.haml b/app/views/projects/_find_file_link.html.haml
index 08e2fc48be7..dbb33090670 100644
--- a/app/views/projects/_find_file_link.html.haml
+++ b/app/views/projects/_find_file_link.html.haml
@@ -1,3 +1,3 @@
-= link_to namespace_project_find_file_path(@project.namespace, @project, @ref), class: 'btn btn-grouped shortcuts-find-file', rel: 'nofollow' do
- = icon('search')
- %span Find File
+= link_to namespace_project_find_file_path(@project.namespace, @project, @ref), class: 'btn btn-grouped shortcuts-find-file', rel: 'nofollow' do
+ = icon('search')
+ %span Find File
diff --git a/app/views/projects/_gitlab_import_modal.html.haml b/app/views/projects/_gitlab_import_modal.html.haml
index e9f39b16aa7..00aef66e1f8 100644
--- a/app/views/projects/_gitlab_import_modal.html.haml
+++ b/app/views/projects/_gitlab_import_modal.html.haml
@@ -1,8 +1,8 @@
-%div#gitlab_import_modal.modal
+#gitlab_import_modal.modal
.modal-dialog
.modal-content
.modal-header
- %a.close{href: "#", "data-dismiss" => "modal"} ×
+ %a.close{ href: "#", "data-dismiss" => "modal" } ×
%h3 Import projects from GitLab.com
.modal-body
To enable importing projects from GitLab.com,
@@ -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 0d1f2b70018..1b9d87e9969 100644
--- a/app/views/projects/_home_panel.html.haml
+++ b/app/views/projects/_home_panel.html.haml
@@ -5,7 +5,7 @@
= project_icon(@project, alt: @project.name, class: 'avatar s70 avatar-tile')
%h1.project-title
= @project.name
- %span.visibility-icon.has-tooltip{data: { container: 'body' }, title: visibility_icon_description(@project)}
+ %span.visibility-icon.has-tooltip{ data: { container: 'body' }, title: visibility_icon_description(@project) }
= visibility_level_icon(@project.visibility_level, fw: false)
.project-home-desc
diff --git a/app/views/projects/_last_commit.html.haml b/app/views/projects/_last_commit.html.haml
index 7f530708947..e1fea8ccf3d 100644
--- a/app/views/projects/_last_commit.html.haml
+++ b/app/views/projects/_last_commit.html.haml
@@ -1,7 +1,8 @@
+
- ref = local_assigns.fetch(:ref)
- status = commit.status(ref)
- if status
- = link_to builds_namespace_project_commit_path(commit.project.namespace, commit.project, commit), class: "ci-status ci-#{status}" do
+ = link_to pipelines_namespace_project_commit_path(commit.project.namespace, commit.project, commit), class: "ci-status ci-#{status}" do
= ci_icon_for_status(status)
= ci_label_for_status(status)
diff --git a/app/views/projects/_md_preview.html.haml b/app/views/projects/_md_preview.html.haml
index 58d961d93ca..23e27c1105c 100644
--- a/app/views/projects/_md_preview.html.haml
+++ b/app/views/projects/_md_preview.html.haml
@@ -15,23 +15,23 @@
%li.pull-right
.toolbar-group
- = markdown_toolbar_button({icon: "bold fw", data: { "md-tag" => "**" }, title: "Add bold text" })
- = markdown_toolbar_button({icon: "italic fw", data: { "md-tag" => "*" }, title: "Add italic text" })
- = markdown_toolbar_button({icon: "quote-right fw", data: { "md-tag" => "> ", "md-prepend" => true }, title: "Insert a quote" })
- = markdown_toolbar_button({icon: "code fw", data: { "md-tag" => "`", "md-block" => "```" }, title: "Insert code" })
- = markdown_toolbar_button({icon: "list-ul fw", data: { "md-tag" => "* ", "md-prepend" => true }, title: "Add a bullet list" })
- = markdown_toolbar_button({icon: "list-ol fw", data: { "md-tag" => "1. ", "md-prepend" => true }, title: "Add a numbered list" })
- = markdown_toolbar_button({icon: "check-square-o fw", data: { "md-tag" => "* [ ] ", "md-prepend" => true }, title: "Add a task list" })
+ = markdown_toolbar_button({ icon: "bold fw", data: { "md-tag" => "**" }, title: "Add bold text" })
+ = markdown_toolbar_button({ icon: "italic fw", data: { "md-tag" => "*" }, title: "Add italic text" })
+ = markdown_toolbar_button({ icon: "quote-right fw", data: { "md-tag" => "> ", "md-prepend" => true }, title: "Insert a quote" })
+ = markdown_toolbar_button({ icon: "code fw", data: { "md-tag" => "`", "md-block" => "```" }, title: "Insert code" })
+ = markdown_toolbar_button({ icon: "list-ul fw", data: { "md-tag" => "* ", "md-prepend" => true }, title: "Add a bullet list" })
+ = markdown_toolbar_button({ icon: "list-ol fw", data: { "md-tag" => "1. ", "md-prepend" => true }, title: "Add a numbered list" })
+ = markdown_toolbar_button({ icon: "check-square-o fw", data: { "md-tag" => "* [ ] ", "md-prepend" => true }, title: "Add a task list" })
.toolbar-group
- %button.toolbar-btn.js-zen-enter.has-tooltip.hidden-xs{ type: "button", tabindex: -1, aria: { label: "Go full screen" }, title: "Go full screen", data: { container: "body" } }
- =icon("arrows-alt fw")
+ %button.toolbar-btn.js-zen-enter.has-tooltip{ type: "button", tabindex: -1, aria: { label: "Go full screen" }, title: "Go full screen", data: { container: "body" } }
+ = icon("arrows-alt fw")
.md-write-holder
= yield
- .md.md-preview-holder.js-md-preview.hide{class: (preview_class if defined?(preview_class))}
+ .md.md-preview-holder.js-md-preview.hide{ class: (preview_class if defined?(preview_class)) }
- if defined?(referenced_users) && referenced_users
- %div.referenced-users.hide
+ .referenced-users.hide
%span
= icon("exclamation-triangle")
You are about to add
diff --git a/app/views/projects/_merge_request_merge_settings.html.haml b/app/views/projects/_merge_request_merge_settings.html.haml
index afe2fd7fd7b..27d25a6b682 100644
--- a/app/views/projects/_merge_request_merge_settings.html.haml
+++ b/app/views/projects/_merge_request_merge_settings.html.haml
@@ -4,11 +4,11 @@
.checkbox.builds-feature
= form.label :only_allow_merge_if_build_succeeds do
= form.check_box :only_allow_merge_if_build_succeeds
- %strong Only allow merge requests to be merged if the build succeeds
+ %strong Only allow merge requests to be merged if the pipeline succeeds
%br
%span.descr
- Builds need to be configured to enable this feature.
- = link_to icon('question-circle'), help_page_path('user/project/merge_requests/merge_when_build_succeeds', anchor: 'only-allow-merge-requests-to-be-merged-if-the-build-succeeds')
+ Pipelines need to be configured to enable this feature.
+ = link_to icon('question-circle'), help_page_path('user/project/merge_requests/merge_when_pipeline_succeeds', anchor: 'only-allow-merge-requests-to-be-merged-if-the-pipeline-succeeds')
.checkbox
= form.label :only_allow_merge_if_all_discussions_are_resolved do
= form.check_box :only_allow_merge_if_all_discussions_are_resolved
diff --git a/app/views/projects/_visibility_select.html.haml b/app/views/projects/_visibility_select.html.haml
new file mode 100644
index 00000000000..65fc0a36ca9
--- /dev/null
+++ b/app/views/projects/_visibility_select.html.haml
@@ -0,0 +1,7 @@
+- if can_change_visibility_level?(@project, current_user)
+ = form.select(model_method, visibility_select_options(@project, selected_level), {}, class: 'form-control visibility-select')
+- else
+ .info.js-locked{ data: { help_block: visibility_level_description(@project.visibility_level, @project) } }
+ = visibility_level_icon(@project.visibility_level)
+ %strong
+ = visibility_level_label(@project.visibility_level)
diff --git a/app/views/projects/_wiki.html.haml b/app/views/projects/_wiki.html.haml
index f00422dd7c0..41d42740f61 100644
--- a/app/views/projects/_wiki.html.haml
+++ b/app/views/projects/_wiki.html.haml
@@ -7,7 +7,7 @@
- else
- can_create_wiki = can?(current_user, :create_wiki, @project)
.project-home-empty{ class: [('row-content-block' if can_create_wiki), ('content-block' unless can_create_wiki)] }
- %div.text-center{ class: container_class }
+ .text-center{ class: container_class }
%h4
This project does not have a wiki homepage yet
- if can_create_wiki
diff --git a/app/views/projects/artifacts/_tree_directory.html.haml b/app/views/projects/artifacts/_tree_directory.html.haml
index def493c56f5..9e49c93388a 100644
--- a/app/views/projects/artifacts/_tree_directory.html.haml
+++ b/app/views/projects/artifacts/_tree_directory.html.haml
@@ -1,6 +1,6 @@
- path_to_directory = browse_namespace_project_build_artifacts_path(@project.namespace, @project, @build, path: directory.path)
-%tr.tree-item{ 'data-link' => path_to_directory}
+%tr.tree-item{ 'data-link' => path_to_directory }
%td.tree-item-file-name
= tree_icon('folder', '755', directory.name)
%span.str-truncated
diff --git a/app/views/projects/artifacts/browse.html.haml b/app/views/projects/artifacts/browse.html.haml
index ede01dcc1aa..edf55d59f28 100644
--- a/app/views/projects/artifacts/browse.html.haml
+++ b/app/views/projects/artifacts/browse.html.haml
@@ -1,4 +1,4 @@
-- page_title 'Artifacts', "#{@build.name} (##{@build.id})", 'Builds'
+- page_title 'Artifacts', "#{@build.name} (##{@build.id})", 'Jobs'
.top-block.row-content-block.clearfix
.pull-right
@@ -8,7 +8,7 @@
Download artifacts archive
.tree-holder
- %div.tree-content-holder
+ .tree-content-holder
%table.table.tree-table
%thead
%tr
diff --git a/app/views/projects/blame/show.html.haml b/app/views/projects/blame/show.html.haml
index f63802ac88b..23f54553014 100644
--- a/app/views/projects/blame/show.html.haml
+++ b/app/views/projects/blame/show.html.haml
@@ -36,7 +36,7 @@
%td.line-numbers
- line_count = blame_group[:lines].count
- (current_line...(current_line + line_count)).each do |i|
- %a.diff-line-num{href: "#L#{i}", id: "L#{i}", 'data-line-number' => i}
+ %a.diff-line-num{ href: "#L#{i}", id: "L#{i}", 'data-line-number' => i }
= icon("link")
= i
\
diff --git a/app/views/projects/blob/_actions.html.haml b/app/views/projects/blob/_actions.html.haml
index ff893ea74e1..7b9cfbbd067 100644
--- a/app/views/projects/blob/_actions.html.haml
+++ b/app/views/projects/blob/_actions.html.haml
@@ -1,3 +1,6 @@
+.btn-group
+ = view_on_environment_button(@commit.sha, @path, @environment) if @environment
+
.btn-group.tree-btn-group
= link_to 'Raw', namespace_project_raw_path(@project.namespace, @project, @id),
class: 'btn btn-sm', target: '_blank'
@@ -12,7 +15,7 @@
= link_to 'History', namespace_project_commits_path(@project.namespace, @project, @id),
class: 'btn btn-sm'
= link_to 'Permalink', namespace_project_blob_path(@project.namespace, @project,
- tree_join(@commit.sha, @path)), class: 'btn btn-sm'
+ tree_join(@commit.sha, @path)), class: 'btn btn-sm js-data-file-blob-permalink-url'
- if current_user
.btn-group{ role: "group" }
diff --git a/app/views/projects/blob/_blob.html.haml b/app/views/projects/blob/_blob.html.haml
index 149ee7c59d6..f75f438ee4f 100644
--- a/app/views/projects/blob/_blob.html.haml
+++ b/app/views/projects/blob/_blob.html.haml
@@ -18,11 +18,11 @@
- else
= link_to title, '#'
-%ul.blob-commit-info.hidden-xs
+%ul.blob-commit-info.table-list.hidden-xs
- blob_commit = @repository.last_commit_for_path(@commit.id, blob.path)
= render blob_commit, project: @project, ref: @ref
-%div#blob-content-holder.blob-content-holder
+#blob-content-holder.blob-content-holder
%article.file-holder
.file-title
= blob_icon blob.mode, blob.name
diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml
index 1d058daa094..228ac61fc8c 100644
--- a/app/views/projects/blob/_editor.html.haml
+++ b/app/views/projects/blob/_editor.html.haml
@@ -34,7 +34,7 @@
= select_tag :encoding, options_for_select([ "base64", "text" ], "text"), class: 'select2'
.file-editor.code
- %pre.js-edit-mode-pane#editor #{params[:content] || local_assigns[:blob_data]}
+ %pre.js-edit-mode-pane#editor= params[:content] || local_assigns[:blob_data]
- if local_assigns[:path]
.js-edit-mode-pane#preview.hide
.center
diff --git a/app/views/projects/blob/_image.html.haml b/app/views/projects/blob/_image.html.haml
index 4c356d1f07f..f864702d862 100644
--- a/app/views/projects/blob/_image.html.haml
+++ b/app/views/projects/blob/_image.html.haml
@@ -1,15 +1,15 @@
.file-content.image_file
- if blob.svg?
- 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.
+ -# 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)}"}
+ %img{ src: "data:#{blob.mime_type};base64,#{Base64.encode64(blob.data)}", alt: "#{blob.name}" }
- 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))}
+ %img{ src: namespace_project_raw_path(@project.namespace, @project, tree_join(@commit.id, blob.path)), alt: "#{blob.name}" }
diff --git a/app/views/projects/blob/_new_dir.html.haml b/app/views/projects/blob/_new_dir.html.haml
index 84694203d7d..7f470b890ba 100644
--- a/app/views/projects/blob/_new_dir.html.haml
+++ b/app/views/projects/blob/_new_dir.html.haml
@@ -2,7 +2,7 @@
.modal-dialog
.modal-content
.modal-header
- %a.close{href: "#", "data-dismiss" => "modal"} ×
+ %a.close{ href: "#", "data-dismiss" => "modal" } ×
%h3.page-title Create New Directory
.modal-body
= form_tag namespace_project_create_dir_path(@project.namespace, @project, @id), method: :post, remote: false, class: 'form-horizontal js-create-dir-form js-quick-submit js-requires-input' do
diff --git a/app/views/projects/blob/_remove.html.haml b/app/views/projects/blob/_remove.html.haml
index 2e1f32fd15e..db6662a95ac 100644
--- a/app/views/projects/blob/_remove.html.haml
+++ b/app/views/projects/blob/_remove.html.haml
@@ -2,7 +2,7 @@
.modal-dialog
.modal-content
.modal-header
- %a.close{href: "#", "data-dismiss" => "modal"} ×
+ %a.close{ href: "#", "data-dismiss" => "modal" } ×
%h3.page-title Delete #{@blob.name}
.modal-body
diff --git a/app/views/projects/blob/_upload.html.haml b/app/views/projects/blob/_upload.html.haml
index 57a27ec904e..4924c73cf8e 100644
--- a/app/views/projects/blob/_upload.html.haml
+++ b/app/views/projects/blob/_upload.html.haml
@@ -2,8 +2,8 @@
.modal-dialog
.modal-content
.modal-header
- %a.close{href: "#", "data-dismiss" => "modal"} ×
- %h3.page-title #{title}
+ %a.close{ href: "#", "data-dismiss" => "modal" } ×
+ %h3.page-title= title
.modal-body
= form_tag form_path, method: method, class: 'js-quick-submit js-upload-blob-form form-horizontal' do
.dropzone
@@ -12,7 +12,7 @@
Attach a file by drag &amp; drop or
= link_to 'click to upload', '#', class: "markdown-selector"
%br
- .dropzone-alerts{class: "alert alert-danger data", style: "display:none"}
+ .dropzone-alerts.alert.alert-danger.data{ style: "display:none" }
= render 'shared/new_commit_form', placeholder: placeholder
diff --git a/app/views/projects/blob/diff.html.haml b/app/views/projects/blob/diff.html.haml
index a79ae53c780..3b1a2e54ec2 100644
--- a/app/views/projects/blob/diff.html.haml
+++ b/app/views/projects/blob/diff.html.haml
@@ -13,18 +13,18 @@
- case diff_view
- when :inline
%td.old_line.diff-line-num{ data: { linenumber: line_old } }
- %a{href: "##{line_old}", 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 }}
+ %a{ href: "##{line_new}", data: { linenumber: line_new } }
= line_content
- when :parallel
- %td.old_line.diff-line-num{data: { linenumber: line_old }}
+ %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 }}
+ %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, class: line_class }
- = diff_match_line @form.to, @form.to, text: @match_line, view: diff_view, bottom: true
+ = diff_match_line @form.to - @form.offset, @form.to, text: @match_line, view: diff_view, bottom: true
diff --git a/app/views/projects/blob/edit.html.haml b/app/views/projects/blob/edit.html.haml
index a5dcd93f42e..8853801016b 100644
--- a/app/views/projects/blob/edit.html.haml
+++ b/app/views/projects/blob/edit.html.haml
@@ -2,7 +2,7 @@
- page_title "Edit", @blob.path, @ref
- content_for :page_specific_javascripts do
= page_specific_javascript_tag('lib/ace.js')
- = page_specific_javascript_tag('blob_edit/blob_edit_bundle.js')
+ = page_specific_javascript_bundle_tag('blob_edit')
= render "projects/commits/head"
%div{ class: container_class }
diff --git a/app/views/projects/blob/new.html.haml b/app/views/projects/blob/new.html.haml
index b6ed9518c48..e0ce8cc9601 100644
--- a/app/views/projects/blob/new.html.haml
+++ b/app/views/projects/blob/new.html.haml
@@ -1,7 +1,7 @@
- page_title "New File", @path.presence, @ref
- content_for :page_specific_javascripts do
= page_specific_javascript_tag('lib/ace.js')
- = page_specific_javascript_tag('blob_edit/blob_edit_bundle.js')
+ = page_specific_javascript_bundle_tag('blob_edit')
%h3.page-title
New File
diff --git a/app/views/projects/blob/preview.html.haml b/app/views/projects/blob/preview.html.haml
index 541dc96c45f..5cafb644b40 100644
--- a/app/views/projects/blob/preview.html.haml
+++ b/app/views/projects/blob/preview.html.haml
@@ -20,6 +20,6 @@
- else
%td.old_line.diff-line-num
%td.new_line.diff-line-num
- %td.line_content{class: "#{line.type}"}= diff_line_content(line.text)
+ %td.line_content{ class: "#{line.type}" }= diff_line_content(line.text)
- else
.nothing-here-block No changes.
diff --git a/app/views/projects/blob/show.html.haml b/app/views/projects/blob/show.html.haml
index 0ab78a39cf9..b6738c3380f 100644
--- a/app/views/projects/blob/show.html.haml
+++ b/app/views/projects/blob/show.html.haml
@@ -5,7 +5,7 @@
%div{ class: container_class }
= render 'projects/last_push'
- %div#tree-holder.tree-holder
+ #tree-holder.tree-holder
= render 'blob', blob: @blob
- if can_edit_blob?(@blob)
diff --git a/app/views/projects/boards/_show.html.haml b/app/views/projects/boards/_show.html.haml
index 356bd50f7f3..05fe504d1c9 100644
--- a/app/views/projects/boards/_show.html.haml
+++ b/app/views/projects/boards/_show.html.haml
@@ -3,8 +3,8 @@
- page_title "Boards"
- content_for :page_specific_javascripts do
- = page_specific_javascript_tag('boards/boards_bundle.js')
- = page_specific_javascript_tag('boards/test_utils/simulate_drag.js') if Rails.env.test?
+ = page_specific_javascript_bundle_tag('boards')
+ = page_specific_javascript_bundle_tag('boards_test') if Rails.env.test?
%script#js-board-template{ type: "text/x-template" }= render "projects/boards/components/board"
%script#js-board-list-template{ type: "text/x-template" }= render "projects/boards/components/board_list"
@@ -24,5 +24,13 @@
":list" => "list",
":disabled" => "disabled",
":issue-link-base" => "issueLinkBase",
+ ":root-path" => "rootPath",
":key" => "_uid" }
= render "projects/boards/components/sidebar"
+ %board-add-issues-modal{ "blank-state-image" => render('shared/empty_states/icons/issues.svg'),
+ "new-issue-path" => new_namespace_project_issue_path(@project.namespace, @project),
+ "milestone-path" => milestones_filter_dropdown_path,
+ "label-path" => labels_filter_path,
+ ":issue-link-base" => "issueLinkBase",
+ ":root-path" => "rootPath",
+ ":project-id" => @project.try(:id) }
diff --git a/app/views/projects/boards/components/_board.html.haml b/app/views/projects/boards/components/_board.html.haml
index a2e5118a9f3..72bce4049de 100644
--- a/app/views/projects/boards/components/_board.html.haml
+++ b/app/views/projects/boards/components/_board.html.haml
@@ -29,6 +29,7 @@
":loading" => "list.loading",
":disabled" => "disabled",
":issue-link-base" => "issueLinkBase",
+ ":root-path" => "rootPath",
"ref" => "board-list" }
- if can?(current_user, :admin_list, @project)
= render "projects/boards/components/blank_state"
diff --git a/app/views/projects/boards/components/_board_list.html.haml b/app/views/projects/boards/components/_board_list.html.haml
index 34fdb1f6a74..f413a5e94c1 100644
--- a/app/views/projects/boards/components/_board_list.html.haml
+++ b/app/views/projects/boards/components/_board_list.html.haml
@@ -34,6 +34,7 @@
":list" => "list",
":issue" => "issue",
":issue-link-base" => "issueLinkBase",
+ ":root-path" => "rootPath",
":disabled" => "disabled",
":key" => "issue.id" }
%li.board-list-count.text-center{ "v-if" => "showCount" }
diff --git a/app/views/projects/boards/components/_card.html.haml b/app/views/projects/boards/components/_card.html.haml
index 1f31496e73f..891c2c46251 100644
--- a/app/views/projects/boards/components/_card.html.haml
+++ b/app/views/projects/boards/components/_card.html.haml
@@ -4,25 +4,7 @@
"@mousedown" => "mouseDown",
"@mousemove" => "mouseMove",
"@mouseup" => "showIssue($event)" }
- %h4.card-title
- = icon("eye-slash", class: "confidential-icon", "v-if" => "issue.confidential")
- %a{ ":href" => 'issueLinkBase + "/" + issue.id',
- ":title" => "issue.title" }
- {{ issue.title }}
- .card-footer
- %span.card-number{ "v-if" => "issue.id" }
- = precede '#' do
- {{ issue.id }}
- %a.has-tooltip{ ":href" => "\"#{root_path}\" + issue.assignee.username",
- ":title" => '"Assigned to " + issue.assignee.name',
- "v-if" => "issue.assignee",
- data: { container: 'body' } }
- %img.avatar.avatar-inline.s20{ ":src" => "issue.assignee.avatar", width: 20, height: 20 }
- %button.label.color-label.has-tooltip{ "v-for" => "label in issue.labels",
- type: "button",
- "v-if" => "(!list.label || label.id !== list.label.id)",
- "@click" => "filterByLabel(label, $event)",
- ":style" => "{ backgroundColor: label.color, color: label.textColor }",
- ":title" => "label.description",
- data: { container: 'body' } }
- {{ label.title }}
+ %issue-card-inner{ ":list" => "list",
+ ":issue" => "issue",
+ ":issue-link-base" => "issueLinkBase",
+ ":root-path" => "rootPath" }
diff --git a/app/views/projects/boards/components/_sidebar.html.haml b/app/views/projects/boards/components/_sidebar.html.haml
index 2125c3387c4..24d76da6f06 100644
--- a/app/views/projects/boards/components/_sidebar.html.haml
+++ b/app/views/projects/boards/components/_sidebar.html.haml
@@ -1,23 +1,26 @@
%board-sidebar{ "inline-template" => true,
":current-user" => "#{current_user ? current_user.to_json(only: [:username, :id, :name], methods: [:avatar_url]) : {}}" }
- %aside.right-sidebar.right-sidebar-expanded.issue-boards-sidebar{ "v-show" => "showSidebar" }
- .issuable-sidebar
- .block.issuable-sidebar-header
- %span.issuable-header-text.hide-collapsed.pull-left
- %strong
- {{ issue.title }}
- %br/
- %span
- = precede "#" do
- {{ issue.id }}
- %a.gutter-toggle.pull-right{ role: "button",
- href: "#",
- "@click.prevent" => "closeSidebar",
- "aria-label" => "Toggle sidebar" }
- = custom_icon("icon_close", size: 15)
- .js-issuable-update
- = render "projects/boards/components/sidebar/assignee"
- = render "projects/boards/components/sidebar/milestone"
- = render "projects/boards/components/sidebar/due_date"
- = render "projects/boards/components/sidebar/labels"
- = render "projects/boards/components/sidebar/notifications"
+ %transition{ name: "boards-sidebar-slide" }
+ %aside.right-sidebar.right-sidebar-expanded.issue-boards-sidebar{ "v-show" => "showSidebar" }
+ .issuable-sidebar
+ .block.issuable-sidebar-header
+ %span.issuable-header-text.hide-collapsed.pull-left
+ %strong
+ {{ issue.title }}
+ %br/
+ %span
+ = precede "#" do
+ {{ issue.id }}
+ %a.gutter-toggle.pull-right{ role: "button",
+ href: "#",
+ "@click.prevent" => "closeSidebar",
+ "aria-label" => "Toggle sidebar" }
+ = custom_icon("icon_close", size: 15)
+ .js-issuable-update
+ = render "projects/boards/components/sidebar/assignee"
+ = render "projects/boards/components/sidebar/milestone"
+ = render "projects/boards/components/sidebar/due_date"
+ = render "projects/boards/components/sidebar/labels"
+ = render "projects/boards/components/sidebar/notifications"
+ %remove-btn{ ":issue" => "issue",
+ ":list" => "list" }
diff --git a/app/views/projects/boards/components/sidebar/_assignee.html.haml b/app/views/projects/boards/components/sidebar/_assignee.html.haml
index 8fe1b832071..e75ce305440 100644
--- a/app/views/projects/boards/components/sidebar/_assignee.html.haml
+++ b/app/views/projects/boards/components/sidebar/_assignee.html.haml
@@ -14,7 +14,7 @@
%a.author_link.bold{ ":href" => "'#{root_url}' + issue.assignee.username",
"v-if" => "issue.assignee" }
%img.avatar.avatar-inline.s32{ ":src" => "issue.assignee.avatar",
- width: "32" }
+ width: "32", alt: "Avatar" }
%span.author
{{ issue.assignee.name }}
%span.username
diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml
index 9135cee8364..19ffe73a08d 100644
--- a/app/views/projects/branches/_branch.html.haml
+++ b/app/views/projects/branches/_branch.html.haml
@@ -4,7 +4,7 @@
- number_commits_behind = diverging_commit_counts[:behind]
- number_commits_ahead = diverging_commit_counts[:ahead]
- merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project))
-%li(class="js-branch-#{branch.name}")
+%li{ class: "js-branch-#{branch.name}" }
%div
= link_to namespace_project_tree_path(@project.namespace, @project, branch.name), class: 'item-title str-truncated' do
= branch.name
@@ -12,14 +12,13 @@
- if branch.name == @repository.root_ref
%span.label.label-primary default
- elsif @repository.merged_to_root_ref? branch.name
- %span.label.label-info.has-tooltip(title="Merged into #{@repository.root_ref}")
+ %span.label.label-info.has-tooltip{ title: "Merged into #{@repository.root_ref}" }
merged
- if @project.protected_branch? branch.name
%span.label.label-success
- %i.fa.fa-lock
protected
- .controls.hidden-xs
+ .controls.hidden-xs<
- if merge_project && create_mr_button?(@repository.root_ref, branch.name)
= link_to create_mr_path(@repository.root_ref, branch.name), class: 'btn btn-default' do
Merge Request
diff --git a/app/views/projects/branches/index.html.haml b/app/views/projects/branches/index.html.haml
index 5fd664c7a93..bd1f2d96f56 100644
--- a/app/views/projects/branches/index.html.haml
+++ b/app/views/projects/branches/index.html.haml
@@ -3,16 +3,17 @@
= render "projects/commits/head"
%div{ class: container_class }
- .top-area
+ .top-area.adjust
.nav-text
- Protected branches can be managed in project settings
+ Protected branches can be managed in
+ = link_to 'project settings', namespace_project_protected_branches_path(@project.namespace, @project)
.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{type: 'button', 'data-toggle' => 'dropdown'}
+ .dropdown.inline>
+ %button.dropdown-menu-toggle{ type: 'button', 'data-toggle' => 'dropdown' }
%span.light
= projects_sort_options_hash[@sort]
= icon('chevron-down')
diff --git a/app/views/projects/branches/new.html.haml b/app/views/projects/branches/new.html.haml
index 5a6c8c243fa..e63bdb38bd8 100644
--- a/app/views/projects/branches/new.html.haml
+++ b/app/views/projects/branches/new.html.haml
@@ -2,7 +2,7 @@
- if @error
.alert.alert-danger
- %button{ type: "button", class: "close", "data-dismiss" => "alert"} &times;
+ %button.close{ type: "button", "data-dismiss" => "alert" } &times;
= @error
%h3.page-title
New Branch
diff --git a/app/views/projects/builds/_header.html.haml b/app/views/projects/builds/_header.html.haml
index 057a720a54a..27e81c2bec3 100644
--- a/app/views/projects/builds/_header.html.haml
+++ b/app/views/projects/builds/_header.html.haml
@@ -1,13 +1,13 @@
.content-block.build-header
.header-content
- = render 'ci/status/badge', status: @build.detailed_status(current_user)
- Build
- %strong ##{@build.id}
+ = render 'ci/status/badge', status: @build.detailed_status(current_user), link: false
+ Job
+ %strong.js-build-id ##{@build.id}
in pipeline
= link_to pipeline_path(@build.pipeline) do
%strong ##{@build.pipeline.id}
for commit
- = link_to ci_status_path(@build.pipeline) do
+ = link_to namespace_project_commit_path(@project.namespace, @project, @build.pipeline.sha) do
%strong= @build.pipeline.short_sha
from
= link_to namespace_project_commits_path(@project.namespace, @project, @build.ref) do
@@ -17,6 +17,6 @@
= render "user"
= time_ago_with_tooltip(@build.created_at)
- if can?(current_user, :update_build, @build) && @build.retryable?
- = link_to "Retry build", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-inverted-secondary pull-right', method: :post
+ = link_to "Retry job", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-inverted-secondary pull-right', method: :post
%button.btn.btn-default.pull-right.visible-xs-block.visible-sm-block.build-gutter-toggle.js-sidebar-build-toggle{ role: "button", type: "button" }
= icon('angle-double-left')
diff --git a/app/views/projects/builds/_sidebar.html.haml b/app/views/projects/builds/_sidebar.html.haml
index ce8b66b1945..56fc5f5e68b 100644
--- a/app/views/projects/builds/_sidebar.html.haml
+++ b/app/views/projects/builds/_sidebar.html.haml
@@ -2,7 +2,7 @@
%aside.right-sidebar.right-sidebar-expanded.build-sidebar.js-build-sidebar
.block.build-sidebar-header.visible-xs-block.visible-sm-block.append-bottom-default
- Build
+ Job
%strong ##{@build.id}
%a.gutter-toggle.pull-right.js-sidebar-build-toggle{ href: "#" }
= icon('angle-double-right')
@@ -17,19 +17,19 @@
- if can?(current_user, :read_build, @project) && (@build.artifacts? || @build.artifacts_expired?)
.block{ class: ("block-first" if !@build.coverage) }
.title
- Build artifacts
+ Job artifacts
- if @build.artifacts_expired?
%p.build-detail-row
The artifacts were removed
#{time_ago_with_tooltip(@build.artifacts_expire_at)}
- - elsif @build.artifacts_expire_at
+ - elsif @build.has_expiring_artifacts?
%p.build-detail-row
The artifacts will be removed in
%span.js-artifacts-remove= @build.artifacts_expire_at
- if @build.artifacts?
.btn-group.btn-group-justified{ role: :group }
- - if @build.artifacts_expire_at
+ - if @build.has_expiring_artifacts? && can?(current_user, :update_build, @build)
= link_to keep_namespace_project_build_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default', method: :post do
Keep
@@ -42,9 +42,9 @@
.block{ class: ("block-first" if !@build.coverage && !(can?(current_user, :read_build, @project) && (@build.artifacts? || @build.artifacts_expired?))) }
.title
- Build details
+ Job details
- if can?(current_user, :update_build, @build) && @build.retryable?
- = link_to "Retry build", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'pull-right retry-link', method: :post
+ = link_to "Retry job", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'pull-right retry-link', method: :post
- if @build.merge_request
%p.build-detail-row
%span.build-light-text Merge Request:
@@ -114,7 +114,7 @@
- if @build.pipeline.stages_count > 1
.dropdown.build-dropdown
.title Stage
- %button.dropdown-menu-toggle{type: 'button', 'data-toggle' => 'dropdown'}
+ %button.dropdown-menu-toggle{ type: 'button', 'data-toggle' => 'dropdown' }
%span.stage-selection More
= icon('chevron-down')
%ul.dropdown-menu
@@ -125,10 +125,10 @@
.builds-container
- HasStatus::ORDERED_STATUSES.each do |build_status|
- builds.select{|build| build.status == build_status}.each do |build|
- .build-job{class: sidebar_build_class(build, @build), data: {stage: build.stage}}
+ .build-job{ class: sidebar_build_class(build, @build), data: { stage: build.stage } }
= link_to namespace_project_build_path(@project.namespace, @project, build) do
= icon('arrow-right')
- %span{class: "ci-status-icon-#{build.status}"}
+ %span{ class: "ci-status-icon-#{build.status}" }
= ci_icon_for_status(build.status)
%span
- if build.name
@@ -136,4 +136,4 @@
- else
= build.id
- if build.retried?
- %i.fa.fa-refresh.has-tooltip{data: { container: 'body', placement: 'bottom' }, title: 'Build was retried'}
+ %i.fa.fa-refresh.has-tooltip{ data: { container: 'body', placement: 'bottom' }, title: 'Job was retried' }
diff --git a/app/views/projects/builds/_table.html.haml b/app/views/projects/builds/_table.html.haml
index 028664f5bba..acfdb250aff 100644
--- a/app/views/projects/builds/_table.html.haml
+++ b/app/views/projects/builds/_table.html.haml
@@ -2,14 +2,14 @@
- if builds.blank?
%div
- .nothing-here-block No builds to show
+ .nothing-here-block No jobs to show
- else
.table-holder
%table.table.ci-table.builds-page
%thead
%tr
%th Status
- %th Build
+ %th Job
%th Pipeline
- if admin
%th Project
diff --git a/app/views/projects/builds/index.html.haml b/app/views/projects/builds/index.html.haml
index 06070f12bbd..5ffc0e20d10 100644
--- a/app/views/projects/builds/index.html.haml
+++ b/app/views/projects/builds/index.html.haml
@@ -1,5 +1,5 @@
- @no_container = true
-- page_title "Builds"
+- page_title "Jobs"
= render "projects/pipelines/head"
%div{ class: container_class }
@@ -14,10 +14,10 @@
data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post
- unless @repository.gitlab_ci_yml
- = link_to 'Get started with Builds', help_page_path('ci/quick_start/README'), class: 'btn btn-info'
+ = link_to 'Get started with CI/CD Pipelines', help_page_path('ci/quick_start/README'), class: 'btn btn-info'
= link_to ci_lint_path, class: 'btn btn-default' do
%span CI Lint
- %div.content-list.builds-content-list
+ .content-list.builds-content-list
= render "table", builds: @builds, project: @project
diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml
index cdeb81372ee..228dad528ab 100644
--- a/app/views/projects/builds/show.html.haml
+++ b/app/views/projects/builds/show.html.haml
@@ -1,5 +1,5 @@
- @no_container = true
-- page_title "#{@build.name} (##{@build.id})", "Builds"
+- page_title "#{@build.name} (##{@build.id})", "Jobs"
- trace_with_state = @build.trace_with_state
= render "projects/pipelines/head", build_subnav: true
@@ -12,14 +12,14 @@
.bs-callout.bs-callout-warning
%p
- if no_runners_for_project?(@build.project)
- This build is stuck, because the project doesn't have any runners online assigned to it.
+ This job is stuck, because the project doesn't have any runners online assigned to it.
- elsif @build.tags.any?
- This build is stuck, because you don't have any active runners online with any of these tags assigned to them:
+ This job is stuck, because you don't have any active runners online with any of these tags assigned to them:
- @build.tags.each do |tag|
%span.label.label-primary
= tag
- else
- This build is stuck, because you don't have any active runners that can run this build.
+ This job is stuck, because you don't have any active runners that can run this job.
%br
Go to
@@ -37,36 +37,43 @@
- environment = environment_for_build(@build.project, @build)
- if @build.success? && @build.last_deployment.present?
- if @build.last_deployment.last?
- This build is the most recent deployment to #{environment_link_for_build(@build.project, @build)}.
+ This job is the most recent deployment to #{environment_link_for_build(@build.project, @build)}.
- else
- This build is an out-of-date deployment to #{environment_link_for_build(@build.project, @build)}.
+ This job is an out-of-date deployment to #{environment_link_for_build(@build.project, @build)}.
View the most recent deployment #{deployment_link(environment.last_deployment)}.
- elsif @build.complete? && !@build.success?
- The deployment of this build to #{environment_link_for_build(@build.project, @build)} did not succeed.
+ The deployment of this job to #{environment_link_for_build(@build.project, @build)} did not succeed.
- else
- This build is creating a deployment to #{environment_link_for_build(@build.project, @build)}
+ This job is creating a deployment to #{environment_link_for_build(@build.project, @build)}
- if environment.try(:last_deployment)
and will overwrite the #{deployment_link(environment.last_deployment, text: 'latest deployment')}
.prepend-top-default
- if @build.erased?
.erased.alert.alert-warning
- - erased_by = "by #{link_to @build.erased_by.name, user_path(@build.erased_by)}" if @build.erased_by
- Build has been erased #{erased_by.html_safe} #{time_ago_with_tooltip(@build.erased_at)}
+ - if @build.erased_by_user?
+ Job has been erased by #{link_to(@build.erased_by_name, user_path(@build.erased_by))} #{time_ago_with_tooltip(@build.erased_at)}
+ - else
+ Job has been erased #{time_ago_with_tooltip(@build.erased_at)}
- else
#js-build-scroll.scroll-controls
.scroll-step
- = link_to '#build-trace', class: 'btn' do
- %i.fa.fa-angle-up
- = link_to '#down-build-trace', class: 'btn' do
- %i.fa.fa-angle-down
+ %a.scroll-link.scroll-top{ href: '#up-build-trace', id: 'scroll-top', title: 'Scroll to top' }
+ = custom_icon('scroll_up')
+ = custom_icon('scroll_up_hover_active')
+ %a.scroll-link.scroll-bottom{ href: '#down-build-trace', id: 'scroll-bottom', title: 'Scroll to bottom' }
+ = custom_icon('scroll_down')
+ = custom_icon('scroll_down_hover_active')
- if @build.active?
.autoscroll-container
- %button.btn.btn-sm#autoscroll-button{:type => "button", :data => {:state => 'disabled'}}
- Enable autoscroll
+ %span.status-message#autoscroll-status{ data: { state: 'disabled' } }
+ %span.status-text Autoscroll active
+ %i.status-icon
+ = custom_icon('scroll_down_hover_active')
+ #up-build-trace
%pre.build-trace#build-trace
%code.bash.js-build-output
- = icon("refresh spin", class: "js-build-refresh")
+ .build-loader-animation.js-build-refresh
#down-build-trace
diff --git a/app/views/projects/buttons/_download.html.haml b/app/views/projects/buttons/_download.html.haml
index 324a7f8cd3f..762ff34a9ec 100644
--- a/app/views/projects/buttons/_download.html.haml
+++ b/app/views/projects/buttons/_download.html.haml
@@ -1,5 +1,5 @@
- if !project.empty_repo? && can?(current_user, :download_code, project)
- .project-action-button.dropdown.inline
+ .project-action-button.dropdown.inline>
%button.btn{ 'data-toggle' => 'dropdown' }
= icon('download')
= icon("caret-down")
diff --git a/app/views/projects/buttons/_dropdown.html.haml b/app/views/projects/buttons/_dropdown.html.haml
index f5659be25f0..67de8699b2e 100644
--- a/app/views/projects/buttons/_dropdown.html.haml
+++ b/app/views/projects/buttons/_dropdown.html.haml
@@ -1,6 +1,6 @@
- if current_user
.project-action-button.dropdown.inline
- %a.btn.dropdown-toggle{href: '#', "data-toggle" => "dropdown"}
+ %a.btn.dropdown-toggle{ href: '#', "data-toggle" => "dropdown" }
= icon('plus')
= icon("caret-down")
%ul.dropdown-menu.dropdown-menu-align-right.project-home-dropdown
diff --git a/app/views/projects/buttons/_fork.html.haml b/app/views/projects/buttons/_fork.html.haml
index 27da86b9efe..851fe44a86d 100644
--- a/app/views/projects/buttons/_fork.html.haml
+++ b/app/views/projects/buttons/_fork.html.haml
@@ -8,7 +8,7 @@
= link_to new_namespace_project_fork_path(@project.namespace, @project), title: 'Fork project', class: 'btn' do
= custom_icon('icon_fork')
%span Fork
- %div.count-with-arrow
+ .count-with-arrow
%span.arrow
= link_to namespace_project_forks_path(@project.namespace, @project), title: 'Forks', class: 'count' do
= @project.forks_count
diff --git a/app/views/projects/buttons/_star.html.haml b/app/views/projects/buttons/_star.html.haml
index 12d35101770..d57eb2cbfbc 100644
--- a/app/views/projects/buttons/_star.html.haml
+++ b/app/views/projects/buttons/_star.html.haml
@@ -6,7 +6,7 @@
- else
= icon('star-o')
%span Star
- %div.count-with-arrow
+ .count-with-arrow
%span.arrow
%span.count.star-count
= @project.star_count
@@ -15,7 +15,7 @@
= link_to new_user_session_path, class: 'btn has-tooltip star-btn', title: 'You must sign in to star a project' do
= icon('star')
Star
- %div.count-with-arrow
+ .count-with-arrow
%span.arrow
%span.count
= @project.star_count
diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml
index f1cb0201032..5ea85f9fd4c 100644
--- a/app/views/projects/ci/builds/_build.html.haml
+++ b/app/views/projects/ci/builds/_build.html.haml
@@ -7,7 +7,7 @@
- coverage = local_assigns.fetch(:coverage, false)
- allow_retry = local_assigns.fetch(:allow_retry, false)
-%tr.build.commit{class: ('retried' if retried)}
+%tr.build.commit{ class: ('retried' if retried) }
%td.status
= render "ci/status/badge", status: build.detailed_status(current_user)
@@ -32,10 +32,10 @@
= link_to build.short_sha, namespace_project_commit_path(build.project.namespace, build.project, build.sha), class: "commit-id monospace"
- if build.stuck?
- = icon('warning', class: 'text-warning has-tooltip', title: 'Build is stuck. Check runners.')
+ = icon('warning', class: 'text-warning has-tooltip', title: 'Job is stuck. Check runners.')
- if retried
- = icon('refresh', class: 'text-warning has-tooltip', title: 'Build was retried')
+ = icon('refresh', class: 'text-warning has-tooltip', title: 'Job was retried')
.label-container
- if build.tags.any?
@@ -85,7 +85,7 @@
- if build.finished_at
%p.finished-at
= icon("calendar")
- %span #{time_ago_with_tooltip(build.finished_at)}
+ %span= time_ago_with_tooltip(build.finished_at)
%td.coverage
- if coverage && build.try(:coverage)
diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml
index 2f8f153f9a9..ac0fd87fd8d 100644
--- a/app/views/projects/ci/pipelines/_pipeline.html.haml
+++ b/app/views/projects/ci/pipelines/_pipeline.html.haml
@@ -15,7 +15,7 @@
- else
%span.api.monospace API
- if pipeline.latest?
- %span.label.label-success.has-tooltip{ title: 'Latest build for this branch' } latest
+ %span.label.label-success.has-tooltip{ title: 'Latest job for this branch' } latest
- if pipeline.triggered?
%span.label.label-primary triggered
- if pipeline.yaml_errors.present?
@@ -40,28 +40,8 @@
- else
Cant find HEAD commit for this branch
- %td.stage-cell
- - pipeline.stages.each do |stage|
- - if stage.status
- - detailed_status = stage.detailed_status(current_user)
- - icon_status = "#{detailed_status.icon}_borderless"
- - status_klass = "ci-status-icon ci-status-icon-#{detailed_status.group}"
-
- .stage-container.mini-pipeline-graph
- .dropdown.inline.build-content
- %button.has-tooltip.builds-dropdown.js-builds-dropdown-button{ type: 'button', data: { toggle: 'dropdown', title: "#{stage.name}: #{detailed_status.label}", placement: 'top', "stage-endpoint" => stage_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline, stage: stage.name)}}
- %span.has-tooltip{ class: status_klass }
- %span.mini-pipeline-graph-icon-container
- %span{ class: status_klass }= custom_icon(icon_status)
- = icon('caret-down', class: 'dropdown-caret')
-
- .js-builds-dropdown-container
- .dropdown-menu.grouped-pipeline-dropdown
- .arrow-up
- .js-builds-dropdown-list
-
- .js-builds-dropdown-loading.builds-dropdown-loading.hidden
- %span.fa.fa-spinner.fa-spin
+ %td
+ = render 'shared/mini_pipeline_graph', pipeline: pipeline, klass: 'js-mini-pipeline-graph'
%td
- if pipeline.duration
@@ -81,18 +61,18 @@
.btn-group.inline
- if actions.any?
.btn-group
- %a.dropdown-toggle.btn.btn-default.js-pipeline-dropdown-manual-actions{type: 'button', 'data-toggle' => 'dropdown'}
+ %button.dropdown-toggle.btn.btn-default.has-tooltip.js-pipeline-dropdown-manual-actions{ type: 'button', title: 'Manual job', data: { toggle: 'dropdown', placement: 'top' }, 'aria-label' => 'Manual job' }
= custom_icon('icon_play')
- = icon('caret-down')
+ = icon('caret-down', 'aria-hidden' => 'true')
%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
= custom_icon('icon_play')
- %span= build.name.humanize
+ %span= build.name
- if artifacts.present?
.btn-group
- %a.dropdown-toggle.btn.btn-default.build-artifacts.js-pipeline-dropdown-download{type: 'button', 'data-toggle' => 'dropdown'}
+ %button.dropdown-toggle.btn.btn-default.build-artifacts.has-tooltip.js-pipeline-dropdown-download{ type: 'button', title: 'Artifacts', data: { toggle: 'dropdown', placement: 'top' }, 'aria-label' => 'Artifacts' }
= icon("download")
= icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right
@@ -105,8 +85,8 @@
- 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
+ = link_to retry_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: 'btn has-tooltip', title: 'Retry', data: { toggle: 'dropdown', placement: 'top' }, 'aria-label' => '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
+ = link_to cancel_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: 'btn btn-remove has-tooltip', title: 'Cancel', data: { toggle: 'dropdown', placement: 'top' }, 'aria-label' => 'Cancel' , method: :post do
= icon("remove")
diff --git a/app/views/projects/commit/_builds.html.haml b/app/views/projects/commit/_builds.html.haml
deleted file mode 100644
index b7087749428..00000000000
--- a/app/views/projects/commit/_builds.html.haml
+++ /dev/null
@@ -1,2 +0,0 @@
-- @ci_pipelines.each do |pipeline|
- = render "pipeline", pipeline: pipeline, pipeline_details: true
diff --git a/app/views/projects/commit/_change.html.haml b/app/views/projects/commit/_change.html.haml
index 782f558e8b0..421b3db342d 100644
--- a/app/views/projects/commit/_change.html.haml
+++ b/app/views/projects/commit/_change.html.haml
@@ -6,19 +6,19 @@
- label = 'Cherry-pick'
- target_label = 'Pick into branch'
-.modal{id: "modal-#{type}-commit"}
+.modal{ id: "modal-#{type}-commit" }
.modal-dialog
.modal-content
.modal-header
- %a.close{href: "#", "data-dismiss" => "modal"} ×
+ %a.close{ href: "#", "data-dismiss" => "modal" } ×
%h3.page-title== #{label} this #{commit.change_type_title(current_user)}
.modal-body
- = form_tag send("#{type.underscore}_namespace_project_commit_path", @project.namespace, @project, commit.id), method: :post, remote: false, class: "form-horizontal js-#{type}-form js-requires-input" do
+ = form_tag [type.underscore, @project.namespace.becomes(Namespace), @project, commit], method: :post, remote: false, class: "form-horizontal js-#{type}-form js-requires-input" do
.form-group.branch
= label_tag 'target_branch', target_label, class: 'control-label'
.col-sm-10
= 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 }})
+ = 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
diff --git a/app/views/projects/commit/_ci_menu.html.haml b/app/views/projects/commit/_ci_menu.html.haml
index cbfd99ca448..8aed88da38b 100644
--- a/app/views/projects/commit/_ci_menu.html.haml
+++ b/app/views/projects/commit/_ci_menu.html.haml
@@ -7,8 +7,4 @@
= nav_link(path: 'commit#pipelines') do
= link_to pipelines_namespace_project_commit_path(@project.namespace, @project, @commit.id) do
Pipelines
- %span.badge= @ci_pipelines.count
- = nav_link(path: 'commit#builds') do
- = link_to builds_namespace_project_commit_path(@project.namespace, @project, @commit.id) do
- Builds
- %span.badge= @statuses.count
+ %span.badge= @commit.pipelines.size
diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml
index c08ed8f6c16..4d0b7a5ca85 100644
--- a/app/views/projects/commit/_commit_box.html.haml
+++ b/app/views/projects/commit/_commit_box.html.haml
@@ -1,8 +1,7 @@
.page-content-header
.header-main-content
- %strong
- = clipboard_button(clipboard_text: @commit.id)
- = @commit.short_id
+ %strong Commit #{@commit.short_id}
+ = clipboard_button(clipboard_text: @commit.id, title: "Copy commit SHA to clipboard")
%span.hidden-xs authored
#{time_ago_with_tooltip(@commit.authored_date)}
%span by
@@ -63,10 +62,11 @@
- if @commit.status
.well-segment.pipeline-info
- %div{class: "icon-container ci-status-icon-#{@commit.status}"}
- = ci_icon_for_status(@commit.status)
+ %div{ class: "icon-container ci-status-icon-#{@commit.status}" }
+ = link_to namespace_project_pipeline_path(@project.namespace, @project, @commit.pipelines.last.id) do
+ = ci_icon_for_status(@commit.status)
Pipeline
- = link_to "##{@commit.pipelines.last.id}", pipelines_namespace_project_commit_path(@project.namespace, @project, @commit.id), class: "monospace"
+ = link_to "##{@commit.pipelines.last.id}", namespace_project_pipeline_path(@project.namespace, @project, @commit.pipelines.last.id), class: "monospace"
for
= link_to @commit.short_id, namespace_project_commit_path(@project.namespace, @project, @commit), class: "monospace"
%span.ci-status-label
diff --git a/app/views/projects/commit/_pipeline.html.haml b/app/views/projects/commit/_pipeline.html.haml
index 08d3443b3d0..6abff6aaf95 100644
--- a/app/views/projects/commit/_pipeline.html.haml
+++ b/app/views/projects/commit/_pipeline.html.haml
@@ -13,7 +13,7 @@
Pipeline
= link_to "##{pipeline.id}", namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: "monospace"
with
- = pluralize pipeline.statuses.count(:id), "build"
+ = pluralize pipeline.statuses.count(:id), "job"
- if pipeline.ref
for
= link_to pipeline.ref, namespace_project_commits_path(pipeline.project.namespace, pipeline.project, pipeline.ref), class: "monospace"
@@ -44,7 +44,7 @@
%thead
%tr
%th Status
- %th Build ID
+ %th Job ID
%th Name
%th
- if pipeline.project.build_coverage_enabled?
diff --git a/app/views/projects/commit/_pipelines_list.haml b/app/views/projects/commit/_pipelines_list.haml
index 5a9f7295135..aae2cb8a04b 100644
--- a/app/views/projects/commit/_pipelines_list.haml
+++ b/app/views/projects/commit/_pipelines_list.haml
@@ -1,15 +1,25 @@
-%ul.content-list.pipelines
- - if pipelines.blank?
- %li
- .nothing-here-block No pipelines to show
- - else
- .table-holder
- %table.table.ci-table.js-pipeline-table
- %thead
- %th.pipeline-status Status
- %th.pipeline-info Pipeline
- %th.pipeline-commit Commit
- %th.pipeline-stages Stages
- %th.pipeline-date
- %th.pipeline-actions
- = render pipelines, commit_sha: true, stage: true, allow_retry: true, show_commit: false
+#commit-pipeline-table-view{ data: { endpoint: endpoint } }
+.pipeline-svgs{ data: { "commit_icon_svg" => custom_icon("icon_commit"),
+ "icon_status_canceled" => custom_icon("icon_status_canceled"),
+ "icon_status_running" => custom_icon("icon_status_running"),
+ "icon_status_skipped" => custom_icon("icon_status_skipped"),
+ "icon_status_created" => custom_icon("icon_status_created"),
+ "icon_status_pending" => custom_icon("icon_status_pending"),
+ "icon_status_success" => custom_icon("icon_status_success"),
+ "icon_status_failed" => custom_icon("icon_status_failed"),
+ "icon_status_warning" => custom_icon("icon_status_warning"),
+ "stage_icon_status_canceled" => custom_icon("icon_status_canceled_borderless"),
+ "stage_icon_status_running" => custom_icon("icon_status_running_borderless"),
+ "stage_icon_status_skipped" => custom_icon("icon_status_skipped_borderless"),
+ "stage_icon_status_created" => custom_icon("icon_status_created_borderless"),
+ "stage_icon_status_pending" => custom_icon("icon_status_pending_borderless"),
+ "stage_icon_status_success" => custom_icon("icon_status_success_borderless"),
+ "stage_icon_status_failed" => custom_icon("icon_status_failed_borderless"),
+ "stage_icon_status_warning" => custom_icon("icon_status_warning_borderless"),
+ "icon_play" => custom_icon("icon_play"),
+ "icon_timer" => custom_icon("icon_timer"),
+ "icon_status_manual" => custom_icon("icon_status_manual"),
+} }
+
+- content_for :page_specific_javascripts do
+ = page_specific_javascript_bundle_tag('commit_pipelines')
diff --git a/app/views/projects/commit/builds.html.haml b/app/views/projects/commit/builds.html.haml
deleted file mode 100644
index 077b2d2725b..00000000000
--- a/app/views/projects/commit/builds.html.haml
+++ /dev/null
@@ -1,9 +0,0 @@
-- @no_container = true
-- page_title "Builds", "#{@commit.title} (#{@commit.short_id})", "Commits"
-= render "projects/commits/head"
-
-%div{ class: container_class }
- = render "commit_box"
-
- = render "ci_menu"
- = render "builds"
diff --git a/app/views/projects/commit/pipelines.html.haml b/app/views/projects/commit/pipelines.html.haml
index 8233e26e4e7..ac93eac41ac 100644
--- a/app/views/projects/commit/pipelines.html.haml
+++ b/app/views/projects/commit/pipelines.html.haml
@@ -1,6 +1,5 @@
-- page_title "Pipelines", "#{@commit.title} (#{@commit.short_id})", "Commits"
+- page_title 'Pipelines', "#{@commit.title} (#{@commit.short_id})", 'Commits'
-= render "commit_box"
-
-= render "ci_menu"
-= render "pipelines_list", pipelines: @ci_pipelines
+= render 'commit_box'
+= render 'ci_menu'
+= render 'projects/commit/pipelines_list', endpoint: pipelines_namespace_project_commit_path(@project.namespace, @project, @commit.id)
diff --git a/app/views/projects/commit/show.html.haml b/app/views/projects/commit/show.html.haml
index b8c64d1f13e..d5fc283aa8d 100644
--- a/app/views/projects/commit/show.html.haml
+++ b/app/views/projects/commit/show.html.haml
@@ -8,8 +8,8 @@
- if @commit.status
= render "ci_menu"
- else
- %div.block-connector
- = render "projects/diffs/diffs", diffs: @diffs
+ .block-connector
+ = render "projects/diffs/diffs", diffs: @diffs, environment: @environment
= render "projects/notes/notes_with_form"
- if can_collaborate_with_project?
- %w(revert cherry-pick).each do |type|
diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml
index a940515fadf..002e3d345dc 100644
--- a/app/views/projects/commits/_commit.html.haml
+++ b/app/views/projects/commits/_commit.html.haml
@@ -9,33 +9,33 @@
- cache_key.push(commit.status(ref)) if commit.status(ref)
= cache(cache_key, expires_in: 1.day) do
- %li.commit.js-toggle-container{ id: "commit-#{commit.short_id}" }
- = author_avatar(commit, size: 36)
+ %li.commit.table-list-row.js-toggle-container{ id: "commit-#{commit.short_id}" }
- .commit-info-block
- .commit-row-title
- %span.item-title
- = link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit.id), class: "commit-row-message"
- %span.commit-row-message.visible-xs-inline
- &middot;
- = commit.short_id
- - if commit.status(ref)
- .visible-xs-inline
- = render_commit_status(commit, ref: ref)
- - if commit.description?
- %a.text-expander.hidden-xs.js-toggle-button ...
+ .table-list-cell.avatar-cell.hidden-xs
+ = author_avatar(commit, size: 36)
- .commit-actions.hidden-xs
- - if commit.status(ref)
- = render_commit_status(commit, ref: ref)
- = 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)
+ .table-list-cell.commit-content
+ = link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit.id), class: "commit-row-message item-title"
+ %span.commit-row-message.visible-xs-inline
+ &middot;
+ = commit.short_id
+ - if commit.status(ref)
+ .visible-xs-inline
+ = render_commit_status(commit, ref: ref)
+ - if commit.description?
+ %a.text-expander.hidden-xs.js-toggle-button ...
- if commit.description?
%pre.commit-row-description.js-toggle-content
= preserve(markdown(commit.description, pipeline: :single_line, author: commit.author))
+ .commiter
+ = commit_author_link(commit, avatar: false, size: 24)
+ committed
+ #{time_ago_with_tooltip(commit.committed_date)}
- = commit_author_link(commit, avatar: false, size: 24)
- committed
- #{time_ago_with_tooltip(commit.committed_date)}
+ .table-list-cell.commit-actions.hidden-xs
+ - if commit.status(ref)
+ = render_commit_status(commit, ref: ref)
+ = clipboard_button(clipboard_text: commit.id, title: "Copy commit SHA to clipboard")
+ = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit-short-id btn btn-transparent"
+ = link_to_browse_code(project, commit)
diff --git a/app/views/projects/commits/_commit_list.html.haml b/app/views/projects/commits/_commit_list.html.haml
index ce416caa494..64d93e4141c 100644
--- a/app/views/projects/commits/_commit_list.html.haml
+++ b/app/views/projects/commits/_commit_list.html.haml
@@ -1,7 +1,7 @@
- commits, hidden = limited_commits(@commits)
- commits = Commit.decorate(commits, @project)
-%div.panel.panel-default
+.panel.panel-default
.panel-heading
Commits (#{@commits.count})
- if hidden > 0
@@ -11,4 +11,4 @@
%li.warning-row.unstyled
#{number_with_delimiter(hidden)} additional commits have been omitted to prevent performance issues.
- else
- %ul.content-list= render commits, project: @project, ref: @ref
+ %ul.content-list.table-list= render commits, project: @project, ref: @ref
diff --git a/app/views/projects/commits/_commits.html.haml b/app/views/projects/commits/_commits.html.haml
index 48756c68941..904cdb5767f 100644
--- a/app/views/projects/commits/_commits.html.haml
+++ b/app/views/projects/commits/_commits.html.haml
@@ -2,9 +2,9 @@
- commits, hidden = limited_commits(@commits)
- commits.chunk { |c| c.committed_date.in_time_zone.to_date }.each do |day, commits|
- %li.commit-header= "#{day.strftime('%d %b, %Y')} #{pluralize(commits.count, 'commit')}"
+ %li.commit-header #{day.strftime('%d %b, %Y')} #{pluralize(commits.count, 'commit')}
%li.commits-row
- %ul.list-unstyled.commit-list
+ %ul.content-list.commit-list.table-list.table-wide
= render commits, project: project, ref: ref
- if hidden > 0
diff --git a/app/views/projects/commits/show.html.haml b/app/views/projects/commits/show.html.haml
index 9628cbd1634..d94f23f5a38 100644
--- a/app/views/projects/commits/show.html.haml
+++ b/app/views/projects/commits/show.html.haml
@@ -9,10 +9,13 @@
= render "head"
%div{ class: container_class }
- .row-content-block.second-block.content-component-block
+ .row-content-block.second-block.content-component-block.flex-container-block
.tree-ref-holder
= render 'shared/ref_switcher', destination: 'commits'
+ %ul.breadcrumb.repo-breadcrumb
+ = commits_breadcrumbs
+
.block-controls.hidden-xs.hidden-sm
- if @merge_request.present?
.control
@@ -28,12 +31,10 @@
= search_field_tag :search, params[:search], { placeholder: 'Filter by commit message', id: 'commits-search', class: 'form-control search-text-input input-short', spellcheck: false }
- if current_user && current_user.private_token
.control
- = link_to namespace_project_commits_path(@project.namespace, @project, @ref, {format: :atom, private_token: current_user.private_token}), title: "Commits Feed", class: 'btn' do
+ = link_to namespace_project_commits_path(@project.namespace, @project, @ref, { format: :atom, private_token: current_user.private_token }), title: "Commits Feed", class: 'btn' do
= icon("rss")
- %ul.breadcrumb.repo-breadcrumb
- = commits_breadcrumbs
- %div{id: dom_id(@project)}
+ %div{ id: dom_id(@project) }
%ol#commits-list.list-unstyled.content_list
= render 'commits', project: @project, ref: @ref
= spinner
diff --git a/app/views/projects/compare/_form.html.haml b/app/views/projects/compare/_form.html.haml
index 7bde20c3286..d76d48187cd 100644
--- a/app/views/projects/compare/_form.html.haml
+++ b/app/views/projects/compare/_form.html.haml
@@ -2,21 +2,21 @@
.clearfix
- if params[:to] && params[:from]
.compare-switch-container
- = link_to icon('exchange'), {from: params[:to], to: params[:from]}, {class: 'commits-compare-switch has-tooltip', title: 'Switch base of comparison'}
+ = link_to icon('exchange'), {from: params[:to], to: params[:from]}, {class: 'commits-compare-switch has-tooltip btn btn-white', title: 'Switch base of comparison'}
.form-group.dropdown.compare-form-group.from.js-compare-from-dropdown
.input-group.inline-input-group
%span.input-group-addon from
= hidden_field_tag :from, params[:from]
- = button_tag type: 'button', class: "form-control compare-dropdown-toggle 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], field_name: :from } do
- .dropdown-toggle-text= params[:from] || 'Select branch/tag'
+ = button_tag type: 'button', title: params[:from], class: "form-control compare-dropdown-toggle js-compare-dropdown has-tooltip", required: true, data: { refs_url: refs_namespace_project_path(@project.namespace, @project), toggle: "dropdown", target: ".js-compare-from-dropdown", selected: params[:from], field_name: :from } do
+ .dropdown-toggle-text.str-truncated= params[:from] || 'Select branch/tag'
= render "ref_dropdown"
.compare-ellipsis.inline ...
.form-group.dropdown.compare-form-group.to.js-compare-to-dropdown
.input-group.inline-input-group
%span.input-group-addon to
= hidden_field_tag :to, params[:to]
- = button_tag type: 'button', class: "form-control compare-dropdown-toggle 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], field_name: :to } do
- .dropdown-toggle-text= params[:to] || 'Select branch/tag'
+ = button_tag type: 'button', title: params[:to], class: "form-control compare-dropdown-toggle js-compare-dropdown has-tooltip", required: true, data: { refs_url: refs_namespace_project_path(@project.namespace, @project), toggle: "dropdown", target: ".js-compare-to-dropdown", selected: params[:to], field_name: :to } do
+ .dropdown-toggle-text.str-truncated= params[:to] || 'Select branch/tag'
= render "ref_dropdown"
&nbsp;
= button_tag "Compare", class: "btn btn-create commits-compare-btn"
diff --git a/app/views/projects/compare/show.html.haml b/app/views/projects/compare/show.html.haml
index 819e9bc15ae..0dfc9fe20ed 100644
--- a/app/views/projects/compare/show.html.haml
+++ b/app/views/projects/compare/show.html.haml
@@ -8,7 +8,7 @@
- if @commits.present?
= render "projects/commits/commit_list"
- = render "projects/diffs/diffs", diffs: @diffs
+ = render "projects/diffs/diffs", diffs: @diffs, environment: @environment
- else
.light-well
.center
@@ -16,9 +16,9 @@
There isn't anything to compare.
%p.slead
- if params[:to] == params[:from]
- %span.label-branch #{params[:from]}
+ %span.label-branch= params[:from]
and
- %span.label-branch #{params[:to]}
+ %span.label-branch= params[:to]
are the same.
- else
You'll need to use different branch names to get a valid comparison.
diff --git a/app/views/projects/cycle_analytics/_empty_stage.html.haml b/app/views/projects/cycle_analytics/_empty_stage.html.haml
index b200ce22970..c3f95860e92 100644
--- a/app/views/projects/cycle_analytics/_empty_stage.html.haml
+++ b/app/views/projects/cycle_analytics/_empty_stage.html.haml
@@ -2,6 +2,6 @@
.empty-stage
.icon-no-data
= custom_icon ('icon_no_data')
- %h4 We don’t have enough data to show this stage.
+ %h4 We don't have enough data to show this stage.
%p
{{currentStage.emptyStageText}}
diff --git a/app/views/projects/cycle_analytics/show.html.haml b/app/views/projects/cycle_analytics/show.html.haml
index ef1b38d5e21..5405ff16bea 100644
--- a/app/views/projects/cycle_analytics/show.html.haml
+++ b/app/views/projects/cycle_analytics/show.html.haml
@@ -1,13 +1,13 @@
- @no_container = true
- page_title "Cycle Analytics"
- content_for :page_specific_javascripts do
- = page_specific_javascript_tag("cycle_analytics/cycle_analytics_bundle.js")
+ = page_specific_javascript_bundle_tag('cycle_analytics')
= render "projects/pipelines/head"
#cycle-analytics{ class: container_class, "v-cloak" => "true", data: { request_path: project_cycle_analytics_path(@project) } }
- if @cycle_analytics_no_data
- .bordered-box.landing.content-block{"v-if" => "!isOverviewDialogDismissed"}
+ .bordered-box.landing.content-block{ "v-if" => "!isOverviewDialogDismissed" }
= icon("times", class: "dismiss-icon", "@click" => "dismissOverviewDialog()")
.row
.col-sm-3.col-xs-12.svg-container
@@ -20,19 +20,19 @@
= link_to "Read more", help_page_path('user/project/cycle_analytics'), target: '_blank', class: 'btn'
= icon("spinner spin", "v-show" => "isLoading")
- .wrapper{"v-show" => "!isLoading && !hasError"}
+ .wrapper{ "v-show" => "!isLoading && !hasError" }
.panel.panel-default
.panel-heading
Pipeline Health
.content-block
.container-fluid
.row
- .col-sm-3.col-xs-12.column{"v-for" => "item in state.summary"}
- %h3.header {{item.value}}
- %p.text {{item.title}}
+ .col-sm-3.col-xs-12.column{ "v-for" => "item in state.summary" }
+ %h3.header {{ item.value }}
+ %p.text {{ item.title }}
.col-sm-3.col-xs-12.column
.dropdown.inline.js-ca-dropdown
- %button.dropdown-menu-toggle{"data-toggle" => "dropdown", :type => "button"}
+ %button.dropdown-menu-toggle{ "data-toggle" => "dropdown", :type => "button" }
%span.dropdown-label Last 30 days
%i.fa.fa-chevron-down
%ul.dropdown-menu.dropdown-menu-align-right
diff --git a/app/views/projects/deploy_keys/_deploy_key.html.haml b/app/views/projects/deploy_keys/_deploy_key.html.haml
index 450aaeb367c..d1e3cb14022 100644
--- a/app/views/projects/deploy_keys/_deploy_key.html.haml
+++ b/app/views/projects/deploy_keys/_deploy_key.html.haml
@@ -6,6 +6,9 @@
= deploy_key.title
.description
= deploy_key.fingerprint
+ - if deploy_key.can_push?
+ .write-access-allowed
+ Write access allowed
.deploy-key-content.prepend-left-default.deploy-key-projects
- deploy_key.projects.each do |project|
- if can?(current_user, :read_project, project)
diff --git a/app/views/projects/deploy_keys/_form.html.haml b/app/views/projects/deploy_keys/_form.html.haml
index 901605f7ca3..c91bb9c255a 100644
--- a/app/views/projects/deploy_keys/_form.html.haml
+++ b/app/views/projects/deploy_keys/_form.html.haml
@@ -10,4 +10,13 @@
%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")
+ .form-group
+ .checkbox
+ = f.label :can_push do
+ = f.check_box :can_push
+ %strong Write access allowed
+ .form-group
+ %p.light.append-bottom-0
+ Allow this key to push to repository as well? (Default only allows pull access.)
+
= f.submit "Add key", class: "btn-create btn"
diff --git a/app/views/projects/deployments/_actions.haml b/app/views/projects/deployments/_actions.haml
index 58a214bdbd1..a680b1ca017 100644
--- a/app/views/projects/deployments/_actions.haml
+++ b/app/views/projects/deployments/_actions.haml
@@ -3,7 +3,7 @@
- if actions.present?
.inline
.dropdown
- %a.dropdown-new.btn.btn-default{type: 'button', 'data-toggle' => 'dropdown'}
+ %a.dropdown-new.btn.btn-default{ type: 'button', 'data-toggle' => 'dropdown' }
= custom_icon('icon_play')
= icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right
diff --git a/app/views/projects/deployments/_commit.html.haml b/app/views/projects/deployments/_commit.html.haml
index ff250eeca50..170d786ecbf 100644
--- a/app/views/projects/deployments/_commit.html.haml
+++ b/app/views/projects/deployments/_commit.html.haml
@@ -1,4 +1,4 @@
-%div.branch-commit
+.branch-commit
- if deployment.ref
.icon-container
= deployment.tag? ? icon('tag') : icon('code-fork')
diff --git a/app/views/projects/deployments/_deployment.html.haml b/app/views/projects/deployments/_deployment.html.haml
index 9238f232c7e..c468202569f 100644
--- a/app/views/projects/deployments/_deployment.html.haml
+++ b/app/views/projects/deployments/_deployment.html.haml
@@ -1,6 +1,6 @@
%tr.deployment
%td
- %strong= "##{deployment.iid}"
+ %strong ##{deployment.iid}
%td
= render 'projects/deployments/commit', deployment: deployment
@@ -8,7 +8,7 @@
%td.build-column
- if deployment.deployable
= link_to [@project.namespace.becomes(Namespace), @project, deployment.deployable], class: 'build-link' do
- = "#{deployment.deployable.name} (##{deployment.deployable.id})"
+ #{deployment.deployable.name} (##{deployment.deployable.id})
- if deployment.user
by
= user_avatar(user: deployment.user, size: 20)
diff --git a/app/views/projects/diffs/_content.html.haml b/app/views/projects/diffs/_content.html.haml
index 6120b2191dd..b87b79b170e 100644
--- a/app/views/projects/diffs/_content.html.haml
+++ b/app/views/projects/diffs/_content.html.haml
@@ -1,5 +1,5 @@
.diff-content.diff-wrap-lines
- - # Skip all non non-supported blobs
+ -# 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.
@@ -10,7 +10,7 @@
.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, file_identifier: diff_file.file_identifier))
- .nothing-here-block.diff-collapsed{data: { diff_for_path: url } }
+ .nothing-here-block.diff-collapsed{ data: { diff_for_path: url } }
This diff is collapsed.
%a.click-to-expand
Click to expand it.
diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml
index ab4a2dc36e5..4b49bed835f 100644
--- a/app/views/projects/diffs/_diffs.html.haml
+++ b/app/views/projects/diffs/_diffs.html.haml
@@ -1,3 +1,4 @@
+- environment = local_assigns.fetch(:environment, nil)
- show_whitespace_toggle = local_assigns.fetch(:show_whitespace_toggle, true)
- can_create_note = !@diff_notes_disabled && can?(current_user, :create_note, diffs.project)
- diff_files = diffs.diff_files
@@ -18,8 +19,8 @@
= parallel_diff_btn
= render 'projects/diffs/stats', diff_files: diff_files
-- if diff_files.overflow?
- = render 'projects/diffs/warning', diff_files: diff_files
+- if render_overflow_warning?(diff_files)
+ = render 'projects/diffs/warning', diff_files: diffs
.files{ data: { can_create_note: can_create_note } }
- diff_files.each_with_index do |diff_file|
@@ -30,4 +31,4 @@
- file_hash = hexdigest(diff_file.file_path)
= render 'projects/diffs/file', file_hash: file_hash, project: diffs.project,
- diff_file: diff_file, diff_commit: diff_commit, blob: blob
+ diff_file: diff_file, diff_commit: diff_commit, blob: blob, environment: environment
diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml
index 6c33d80becd..75885badac9 100644
--- a/app/views/projects/diffs/_file.html.haml
+++ b/app/views/projects/diffs/_file.html.haml
@@ -1,11 +1,12 @@
-.diff-file.file-holder{id: file_hash, data: diff_file_html_data(project, diff_file.file_path, diff_commit.id)}
- .file-title{id: "file-path-#{hexdigest(diff_file.file_path)}"}
+- environment = local_assigns.fetch(:environment, nil)
+.diff-file.file-holder{ id: file_hash, data: diff_file_html_data(project, diff_file.file_path, diff_commit.id) }
+ .file-title
= render "projects/diffs/file_header", diff_file: diff_file, blob: blob, diff_commit: diff_commit, project: project, url: "##{file_hash}"
- unless diff_file.submodule?
.file-actions.hidden-xs
- if blob_text_viewable?(blob)
- = link_to '#', class: 'js-toggle-diff-comments btn active has-tooltip btn-file-option', title: "Toggle comments for this file", disabled: @diff_notes_disabled do
+ = link_to '#', class: 'js-toggle-diff-comments btn active has-tooltip', title: "Toggle comments for this file", disabled: @diff_notes_disabled do
= icon('comment')
\
- if editable_diff?(diff_file)
@@ -13,6 +14,7 @@
= edit_blob_link(@merge_request.source_project, @merge_request.source_branch, diff_file.new_path,
blob: blob, link_opts: link_opts)
- = view_file_btn(diff_commit.id, diff_file.new_path, project)
+ = view_file_button(diff_commit.id, diff_file.new_path, project)
+ = view_on_environment_button(diff_commit.id, diff_file.new_path, environment) if environment
= render 'projects/diffs/content', diff_file: diff_file, diff_commit: diff_commit, blob: blob, project: project
diff --git a/app/views/projects/diffs/_file_header.html.haml b/app/views/projects/diffs/_file_header.html.haml
index d3ed8e1bf38..ddec775b789 100644
--- a/app/views/projects/diffs/_file_header.html.haml
+++ b/app/views/projects/diffs/_file_header.html.haml
@@ -21,8 +21,8 @@
- if diff_file.deleted_file
deleted
- = clipboard_button(clipboard_text: diff_file.new_path, class: 'btn-clipboard btn-transparent prepend-left-5', title: 'Copy filename to clipboard')
+ = clipboard_button(clipboard_text: diff_file.new_path, class: 'btn-clipboard btn-transparent prepend-left-5', title: 'Copy file path to clipboard')
- if diff_file.mode_changed?
%small
- = "#{diff_file.a_mode} → #{diff_file.b_mode}"
+ #{diff_file.a_mode} → #{diff_file.b_mode}
diff --git a/app/views/projects/diffs/_image.html.haml b/app/views/projects/diffs/_image.html.haml
index 9ec6a7aa5cd..ca10921c5e2 100644
--- a/app/views/projects/diffs/_image.html.haml
+++ b/app/views/projects/diffs/_image.html.haml
@@ -7,18 +7,18 @@
- if diff.renamed_file || diff.new_file || diff.deleted_file
.image
%span.wrap
- .frame{class: image_diff_class(diff)}
- %img{src: diff.deleted_file ? old_file_raw_path : file_raw_path}
- %p.image-info= "#{number_to_human_size file.size}"
+ .frame{ class: image_diff_class(diff) }
+ %img{ src: diff.deleted_file ? old_file_raw_path : file_raw_path, alt: diff.new_path }
+ %p.image-info= number_to_human_size(file.size)
- else
.image
- %div.two-up.view
+ .two-up.view
%span.wrap
.frame.deleted
- %a{href: namespace_project_blob_path(@project.namespace, @project, tree_join(diff_file.old_ref, diff.old_path))}
- %img{src: old_file_raw_path}
+ %a{ href: namespace_project_blob_path(@project.namespace, @project, tree_join(diff_file.old_ref, diff.old_path)) }
+ %img{ src: old_file_raw_path, alt: diff.old_path }
%p.image-info.hide
- %span.meta-filesize= "#{number_to_human_size old_file.size}"
+ %span.meta-filesize= number_to_human_size(old_file.size)
|
%b W:
%span.meta-width
@@ -27,10 +27,10 @@
%span.meta-height
%span.wrap
.frame.added
- %a{href: namespace_project_blob_path(@project.namespace, @project, tree_join(diff_file.new_ref, diff.new_path))}
- %img{src: file_raw_path}
+ %a{ href: namespace_project_blob_path(@project.namespace, @project, tree_join(diff_file.new_ref, diff.new_path)) }
+ %img{ src: file_raw_path, alt: diff.new_path }
%p.image-info.hide
- %span.meta-filesize= "#{number_to_human_size file.size}"
+ %span.meta-filesize= number_to_human_size(file.size)
|
%b W:
%span.meta-width
@@ -38,32 +38,32 @@
%b H:
%span.meta-height
- %div.swipe.view.hide
+ .swipe.view.hide
.swipe-frame
.frame.deleted
- %img{src: old_file_raw_path}
+ %img{ src: old_file_raw_path, alt: diff.old_path }
.swipe-wrap
.frame.added
- %img{src: file_raw_path}
+ %img{ src: file_raw_path, alt: diff.new_path }
%span.swipe-bar
%span.top-handle
%span.bottom-handle
- %div.onion-skin.view.hide
+ .onion-skin.view.hide
.onion-skin-frame
.frame.deleted
- %img{src: old_file_raw_path}
+ %img{ src: old_file_raw_path, alt: diff.old_path }
.frame.added
- %img{src: file_raw_path}
+ %img{ src: file_raw_path, alt: diff.new_path }
.controls
.transparent
.drag-track
- .dragger{:style => "left: 0px;"}
+ .dragger{ :style => "left: 0px;" }
.opaque
.view-modes.hide
%ul.view-modes-menu
- %li.two-up{data: {mode: 'two-up'}} 2-up
- %li.swipe{data: {mode: 'swipe'}} Swipe
- %li.onion-skin{data: {mode: 'onion-skin'}} Onion skin
+ %li.two-up{ data: { mode: 'two-up' } } 2-up
+ %li.swipe{ data: { mode: 'swipe' } } Swipe
+ %li.onion-skin{ data: { mode: 'onion-skin' } } Onion skin
diff --git a/app/views/projects/diffs/_line.html.haml b/app/views/projects/diffs/_line.html.haml
index 16c96b66714..cd18ba2ed00 100644
--- a/app/views/projects/diffs/_line.html.haml
+++ b/app/views/projects/diffs/_line.html.haml
@@ -16,13 +16,13 @@
- if plain
= link_text
- else
- %a{href: "##{line_code}", data: { linenumber: link_text }}
+ %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" ? " " : line.new_pos
- if plain
= link_text
- else
- %a{href: "##{line_code}", data: { linenumber: link_text }}
+ %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= line.text
diff --git a/app/views/projects/diffs/_parallel_view.html.haml b/app/views/projects/diffs/_parallel_view.html.haml
index 78aa9fb7391..074f1f634ae 100644
--- a/app/views/projects/diffs/_parallel_view.html.haml
+++ b/app/views/projects/diffs/_parallel_view.html.haml
@@ -1,34 +1,40 @@
/ Side-by-side diff view
-%div.text-file.diff-wrap-lines.code.js-syntax-highlight{ data: diff_view_data }
+.text-file.diff-wrap-lines.code.js-syntax-highlight{ data: diff_view_data }
%table
- - last_line = 0
- diff_file.parallel_diff_lines.each do |line|
- left = line[:left]
- right = line[:right]
- - last_line = right.new_pos if right
%tr.line_holder.parallel
- if left
- - if left.meta?
+ - case left.type
+ - when 'match'
= diff_match_line left.old_pos, nil, text: left.text, view: :parallel
+ - when 'nonewline'
+ %td.old_line.diff-line-num
+ %td.line_content.match= left.text
- else
- left_line_code = diff_file.line_code(left)
- left_position = diff_file.position(left)
- %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)
+ %td.old_line.diff-line-num{ id: left_line_code, class: left.type, data: { linenumber: left.old_pos } }
+ %a{ href: "##{left_line_code}", data: { linenumber: 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.empty-cell
%td.line_content.parallel
- if right
- - if right.meta?
+ - case right.type
+ - when 'match'
= diff_match_line nil, right.new_pos, text: left.text, view: :parallel
+ - when 'nonewline'
+ %td.new_line.diff-line-num
+ %td.line_content.match= right.text
- else
- right_line_code = diff_file.line_code(right)
- right_position = diff_file.position(right)
- %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)
+ %td.new_line.diff-line-num{ id: right_line_code, class: right.type, data: { linenumber: right.new_pos } }
+ %a{ href: "##{right_line_code}", data: { linenumber: 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
@@ -37,5 +43,7 @@
- discussion_left, discussion_right = parallel_diff_discussions(left, right, diff_file)
- if discussion_left || discussion_right
= render "discussions/parallel_diff_discussion", discussion_left: discussion_left, discussion_right: discussion_right
- - if !diff_file.new_file && last_line > 0
- = diff_match_line last_line, last_line, bottom: true, view: :parallel
+ - if !diff_file.new_file && diff_file.diff_lines.any?
+ - last_line = diff_file.diff_lines.last
+ %tr.line_holder.parallel
+ = diff_match_line last_line.old_pos, last_line.new_pos, bottom: true, view: :parallel
diff --git a/app/views/projects/diffs/_stats.html.haml b/app/views/projects/diffs/_stats.html.haml
index 66d6254aa1e..8e24e28765f 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.size, "changed file")}
+ %strong= pluralize(diff_files.size, "changed file")
with
%strong.cgreen #{diff_files.sum(&:added_lines)} additions
and
@@ -14,24 +14,23 @@
%li
- if diff_file.deleted_file
%span.deleted-file
- %a{href: "##{file_hash}"}
+ %a{ href: "##{file_hash}" }
%i.fa.fa-minus
= diff_file.old_path
- elsif diff_file.renamed_file
%span.renamed-file
- %a{href: "##{file_hash}"}
+ %a{ href: "##{file_hash}" }
%i.fa.fa-minus
= diff_file.old_path
&rarr;
= diff_file.new_path
- elsif diff_file.new_file
%span.new-file
- %a{href: "##{file_hash}"}
+ %a{ href: "##{file_hash}" }
%i.fa.fa-plus
= diff_file.new_path
- else
%span.edit-file
- %a{href: "##{file_hash}"}
+ %a{ href: "##{file_hash}" }
%i.fa.fa-adjust
= diff_file.new_path
-
diff --git a/app/views/projects/diffs/_text_file.html.haml b/app/views/projects/diffs/_text_file.html.haml
index f1d2d4bf268..2eea1db169a 100644
--- a/app/views/projects/diffs/_text_file.html.haml
+++ b/app/views/projects/diffs/_text_file.html.haml
@@ -4,13 +4,13 @@
%a.show-suppressed-diff.js-show-suppressed-diff Changes suppressed. Click to show.
%table.text-file.code.js-syntax-highlight{ data: diff_view_data, class: too_big ? 'hide' : '' }
- - last_line = 0
- discussions = @grouped_diff_discussions unless @diff_notes_disabled
= render partial: "projects/diffs/line",
collection: diff_file.highlighted_diff_lines,
as: :line,
locals: { diff_file: diff_file, discussions: discussions }
- - last_line = diff_file.highlighted_diff_lines.last.new_pos
- - if !diff_file.new_file && last_line > 0
- = diff_match_line last_line, last_line, bottom: true
+ - if !diff_file.new_file && diff_file.highlighted_diff_lines.any?
+ - last_line = diff_file.highlighted_diff_lines.last
+ %tr.line_holder
+ = diff_match_line last_line.old_pos, last_line.new_pos, bottom: true
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index 38e7fc4279c..9c5c1a6d707 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -7,10 +7,17 @@
.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
- = f.label :name, class: 'label-light' do
- Project name
- = f.text_field :name, class: "form-control", id: "project_name_edit"
+ .row
+ .form-group.col-md-9
+ = f.label :name, class: 'label-light', for: 'project_name_edit' do
+ Project name
+ = f.text_field :name, class: "form-control", id: "project_name_edit"
+
+ .form-group.col-md-3
+ = f.label :id, class: 'label-light' do
+ Project ID
+ = f.text_field :id, class: 'form-control', readonly: true
+
.form-group
= f.label :description, class: 'label-light' do
Project description
@@ -21,76 +28,68 @@
.form-group
= f.label :default_branch, "Default Branch", class: 'label-light'
= f.select(:default_branch, @project.repository.branch_names, {}, {class: 'select2 select-wide'})
- .form-group.project-visibility-level-holder
- = f.label :visibility_level, class: 'label-light' do
- Visibility Level
- = link_to "(?)", help_page_path("public_access/public_access")
- - if can_change_visibility_level?(@project, current_user)
- = render('shared/visibility_radios', model_method: :visibility_level, form: f, selected_level: @project.visibility_level, form_model: @project)
- - else
- .info
- = visibility_level_icon(@project.visibility_level)
- %strong
- = visibility_level_label(@project.visibility_level)
- .light= visibility_level_description(@project.visibility_level, @project)
-
- .form-group
- = render 'shared/allow_request_access', form: f
-
.form-group
= f.label :tag_list, "Tags", class: 'label-light'
= f.text_field :tag_list, value: @project.tag_list.to_s, maxlength: 2000, class: "form-control"
%p.help-block Separate tags with commas.
%hr
- %fieldset.features.append-bottom-0
+ %fieldset.append-bottom-0
%h5.prepend-top-0
- Feature Visibility
-
- = f.fields_for :project_feature do |feature_fields|
- .form_group.prepend-top-20
- .row
- .col-md-9
- = feature_fields.label :repository_access_level, "Repository", class: 'label-light'
- %span.help-block Push files to be stored in this project
- .col-md-3.js-repo-access-level
- = project_feature_access_select(:repository_access_level)
-
- .col-sm-12
- .row
- .col-md-9.project-feature-nested
- = feature_fields.label :merge_requests_access_level, "Merge requests", class: 'label-light'
- %span.help-block Submit changes to be merged upstream
- .col-md-3
- = project_feature_access_select(:merge_requests_access_level)
+ Sharing &amp; Permissions
+ .form_group.prepend-top-20.sharing-and-permissions
+ .row.js-visibility-select
+ .col-md-9
+ %label.label-light
+ = label_tag :project_visibility, 'Project Visibility', class: 'label-light'
+ = link_to "(?)", help_page_path("public_access/public_access")
+ %span.help-block
+ .col-md-3.visibility-select-container
+ = render('projects/visibility_select', model_method: :visibility_level, form: f, selected_level: @project.visibility_level)
+ = f.fields_for :project_feature do |feature_fields|
+ %fieldset.features
+ .row
+ .col-md-9.project-feature
+ = feature_fields.label :repository_access_level, "Repository", class: 'label-light'
+ %span.help-block View and edit files in this project
+ .col-md-3.js-repo-access-level
+ = project_feature_access_select(:repository_access_level)
- .row
- .col-md-9.project-feature-nested
- = feature_fields.label :builds_access_level, "Builds", class: 'label-light'
- %span.help-block Submit, test and deploy your changes before merge
- .col-md-3
- = project_feature_access_select(:builds_access_level)
+ .row
+ .col-md-9.project-feature.nested
+ = feature_fields.label :merge_requests_access_level, "Merge requests", class: 'label-light'
+ %span.help-block Submit changes to be merged upstream
+ .col-md-3
+ = project_feature_access_select(:merge_requests_access_level)
- .row
- .col-md-9
- = feature_fields.label :snippets_access_level, "Snippets", class: 'label-light'
- %span.help-block Share code pastes with others out of Git repository
- .col-md-3
- = project_feature_access_select(:snippets_access_level)
+ .row
+ .col-md-9.project-feature.nested
+ = feature_fields.label :builds_access_level, "Pipelines", class: 'label-light'
+ %span.help-block Submit, test and deploy your changes before merge
+ .col-md-3
+ = project_feature_access_select(:builds_access_level)
- .row
- .col-md-9
- = feature_fields.label :issues_access_level, "Issues", class: 'label-light'
- %span.help-block Lightweight issue tracking system for this project
- .col-md-3
- = project_feature_access_select(:issues_access_level)
+ .row
+ .col-md-9.project-feature
+ = feature_fields.label :snippets_access_level, "Snippets", class: 'label-light'
+ %span.help-block Share code pastes with others out of Git repository
+ .col-md-3
+ = project_feature_access_select(:snippets_access_level)
- .row
- .col-md-9
- = feature_fields.label :wiki_access_level, "Wiki", class: 'label-light'
- %span.help-block Pages for project documentation
- .col-md-3
- = project_feature_access_select(:wiki_access_level)
+ .row
+ .col-md-9.project-feature
+ = feature_fields.label :issues_access_level, "Issues", class: 'label-light'
+ %span.help-block Lightweight issue tracking system for this project
+ .col-md-3
+ = project_feature_access_select(:issues_access_level)
+ .row
+ .col-md-9.project-feature
+ = feature_fields.label :wiki_access_level, "Wiki", class: 'label-light'
+ %span.help-block Pages for project documentation
+ .col-md-3
+ = project_feature_access_select(:wiki_access_level)
+ .form-group
+ = render 'shared/allow_request_access', form: f
- if Gitlab.config.lfs.enabled && current_user.admin?
.row
.col-md-9
@@ -134,6 +133,7 @@
%hr
= link_to 'Remove avatar', namespace_project_avatar_path(@project.namespace, @project), data: { confirm: "Project avatar will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-sm remove-avatar"
= f.submit 'Save changes', class: "btn btn-save"
+
.row.prepend-top-default
%hr
.row.prepend-top-default
@@ -181,11 +181,13 @@
%p
The following items will NOT be exported:
%ul
- %li Build traces and artifacts
+ %li Job traces and artifacts
%li LFS objects
%li Container registry images
- %hr
+ %li CI variables
+ %li Any encrypted tokens
- if can? current_user, :archive_project, @project
+ %hr
.row.prepend-top-default
.col-lg-3
%h4.warning-title.prepend-top-0
diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml
index c0a83091c8c..58c085cdb9d 100644
--- a/app/views/projects/empty.html.haml
+++ b/app/views/projects/empty.html.haml
@@ -32,7 +32,7 @@
.empty_wrapper
%h3.page-title-empty
Command line instructions
- %div.git-empty
+ .git-empty
%fieldset
%h5 Git global setup
%pre.light-well
@@ -52,7 +52,7 @@
git push -u origin master
%fieldset
- %h5 Existing folder or Git repository
+ %h5 Existing folder
%pre.light-well
:preserve
cd existing_folder
@@ -62,6 +62,15 @@
git commit
git push -u origin master
+ %fieldset
+ %h5 Existing Git repository
+ %pre.light-well
+ :preserve
+ cd existing_repo
+ git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')}
+ git push -u origin --all
+ git push -u origin --tags
+
- if can? current_user, :remove_project, @project
.prepend-top-20
= link_to 'Remove project', [@project.namespace.becomes(Namespace), @project], data: { confirm: remove_project_message(@project)}, method: :delete, class: "btn btn-remove pull-right"
diff --git a/app/views/projects/environments/_stop.html.haml b/app/views/projects/environments/_stop.html.haml
index 69848123c17..14a2d627203 100644
--- a/app/views/projects/environments/_stop.html.haml
+++ b/app/views/projects/environments/_stop.html.haml
@@ -1,4 +1,4 @@
-- if can?(current_user, :create_deployment, environment) && environment.stoppable?
+- if can?(current_user, :create_deployment, environment) && environment.stop_action?
.inline
= link_to stop_namespace_project_environment_path(@project.namespace, @project, environment), method: :post,
class: 'btn stop-env-link', rel: 'nofollow', data: { confirm: 'Are you sure you want to stop this environment?' } do
diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml
index 0c6f696f5b9..1f27d41ddd9 100644
--- a/app/views/projects/environments/index.html.haml
+++ b/app/views/projects/environments/index.html.haml
@@ -3,7 +3,7 @@
= render "projects/pipelines/head"
- content_for :page_specific_javascripts do
- = page_specific_javascript_tag("environments/environments_bundle.js")
+ = page_specific_javascript_bundle_tag("environments")
#environments-list-view{ data: { environments_data: environments_list_data,
"can-create-deployment" => can?(current_user, :create_deployment, @project).to_s,
@@ -16,4 +16,4 @@
"css-class" => container_class,
"commit-icon-svg" => custom_icon("icon_commit"),
"terminal-icon-svg" => custom_icon("icon_terminal"),
- "play-icon-svg" => custom_icon("icon_play")}}
+ "play-icon-svg" => custom_icon("icon_play") } }
diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml
index 6e0d9456900..7036325fff8 100644
--- a/app/views/projects/environments/show.html.haml
+++ b/app/views/projects/environments/show.html.haml
@@ -5,14 +5,14 @@
%div{ class: container_class }
.top-area.adjust
.col-md-9
- %h3.page-title= @environment.name.capitalize
+ %h3.page-title= @environment.name
.col-md-3
.nav-controls
= render 'projects/environments/terminal_button', environment: @environment
= render 'projects/environments/external_url', environment: @environment
- if can?(current_user, :update_environment, @environment)
= link_to 'Edit', edit_namespace_project_environment_path(@project.namespace, @project, @environment), class: 'btn'
- - if can?(current_user, :create_deployment, @environment) && @environment.stoppable?
+ - if can?(current_user, :create_deployment, @environment) && @environment.can_stop?
= link_to 'Stop', stop_namespace_project_environment_path(@project.namespace, @project, @environment), data: { confirm: 'Are you sure you want to stop this environment?' }, class: 'btn btn-danger', method: :post
.deployments-container
@@ -32,8 +32,8 @@
%tr
%th ID
%th Commit
- %th Build
- %th
+ %th Job
+ %th Created
%th.hidden-xs
= render @deployments
diff --git a/app/views/projects/environments/terminal.html.haml b/app/views/projects/environments/terminal.html.haml
index a6726e509e0..1d49e9cbaf7 100644
--- a/app/views/projects/environments/terminal.html.haml
+++ b/app/views/projects/environments/terminal.html.haml
@@ -4,9 +4,9 @@
- content_for :page_specific_javascripts do
= stylesheet_link_tag "xterm/xterm"
- = page_specific_javascript_tag("terminal/terminal_bundle.js")
+ = page_specific_javascript_bundle_tag("terminal")
-%div{class: container_class}
+%div{ class: container_class }
.top-area
.row
.col-sm-6
@@ -18,5 +18,5 @@
.nav-controls
= render 'projects/deployments/actions', deployment: @environment.last_deployment
-.terminal-container{class: container_class}
- #terminal{data:{project_path: "#{terminal_namespace_project_environment_path(@project.namespace, @project, @environment)}.ws"}}
+.terminal-container{ class: container_class }
+ #terminal{ data: { project_path: "#{terminal_namespace_project_environment_path(@project.namespace, @project, @environment)}.ws" } }
diff --git a/app/views/projects/find_file/show.html.haml b/app/views/projects/find_file/show.html.haml
index 9322c82904f..4cdb44325b3 100644
--- a/app/views/projects/find_file/show.html.haml
+++ b/app/views/projects/find_file/show.html.haml
@@ -9,11 +9,11 @@
= link_to namespace_project_tree_path(@project.namespace, @project, @ref) do
= @project.path
%li.file-finder
- %input#file_find.form-control.file-finder-input{type: "text", placeholder: 'Find by path', autocomplete: 'off'}
+ %input#file_find.form-control.file-finder-input{ type: "text", placeholder: 'Find by path', autocomplete: 'off' }
- %div.tree-content-holder
+ .tree-content-holder
.table-holder
- %table.table.files-slider{class: "table_#{@hex_path} tree-table table-striped" }
+ %table.table.files-slider{ class: "table_#{@hex_path} tree-table table-striped" }
%tbody
= spinner nil, true
diff --git a/app/views/projects/forks/index.html.haml b/app/views/projects/forks/index.html.haml
index 5ee3979c7e7..f4aa523b32d 100644
--- a/app/views/projects/forks/index.html.haml
+++ b/app/views/projects/forks/index.html.haml
@@ -1,7 +1,7 @@
.top-area
.nav-text
- full_count_title = "#{@public_forks_count} public and #{@private_forks_count} private"
- == #{pluralize(@total_forks_count, 'fork')}: #{full_count_title}
+ #{pluralize(@total_forks_count, 'fork')}: #{full_count_title}
.nav-controls
= form_tag request.original_url, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f|
@@ -9,7 +9,7 @@
spellcheck: false, data: { 'filter-selector' => 'span.namespace-name' }
.dropdown
- %button.dropdown-toggle{type: 'button', 'data-toggle' => 'dropdown'}
+ %button.dropdown-toggle{ type: 'button', 'data-toggle' => 'dropdown' }
%span.light sort:
- if @sort.present?
= sort_options_hash[@sort]
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 9f444f076c0..07fb80750d6 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
@@ -6,7 +6,7 @@
- stage = local_assigns.fetch(:stage, false)
- coverage = local_assigns.fetch(:coverage, false)
-%tr.generic_commit_status{class: ('retried' if retried)}
+%tr.generic_commit_status{ class: ('retried' if retried) }
%td.status
= render 'ci/status/badge', status: generic_commit_status.detailed_status(current_user)
@@ -77,7 +77,7 @@
- if generic_commit_status.finished_at
%p.finished-at
= icon("calendar")
- %span #{time_ago_with_tooltip(generic_commit_status.finished_at)}
+ %span= time_ago_with_tooltip(generic_commit_status.finished_at)
%td.coverage
- if coverage && generic_commit_status.try(:coverage)
diff --git a/app/views/projects/graphs/_head.html.haml b/app/views/projects/graphs/_head.html.haml
index 1a62a6a809c..67018aaa2ac 100644
--- a/app/views/projects/graphs/_head.html.haml
+++ b/app/views/projects/graphs/_head.html.haml
@@ -5,8 +5,8 @@
%ul{ class: (container_class) }
- content_for :page_specific_javascripts do
- = page_specific_javascript_tag('lib/chart.js')
- = page_specific_javascript_tag('graphs/graphs_bundle.js')
+ = page_specific_javascript_bundle_tag('lib_chart')
+ = page_specific_javascript_bundle_tag('graphs')
= nav_link(action: :show) do
= link_to 'Contributors', namespace_project_graph_path
= nav_link(action: :commits) do
diff --git a/app/views/projects/graphs/ci/_build_times.haml b/app/views/projects/graphs/ci/_build_times.haml
index 195f18afc76..bb0975a9535 100644
--- a/app/views/projects/graphs/ci/_build_times.haml
+++ b/app/views/projects/graphs/ci/_build_times.haml
@@ -2,7 +2,7 @@
%p.light
Commit duration in minutes for last 30 commits
- %canvas#build_timesChart{height: 200}
+ %canvas#build_timesChart{ height: 200 }
:javascript
var data = {
diff --git a/app/views/projects/graphs/ci/_builds.haml b/app/views/projects/graphs/ci/_builds.haml
index 1fbf6ca2c1c..b6f453b9736 100644
--- a/app/views/projects/graphs/ci/_builds.haml
+++ b/app/views/projects/graphs/ci/_builds.haml
@@ -1,4 +1,4 @@
-%h4 Build charts
+%h4 Pipelines charts
%p
&nbsp;
%span.cgreen
@@ -11,20 +11,20 @@
.prepend-top-default
%p.light
- Builds for last week
+ Jobs for last week
(#{date_from_to(Date.today - 7.days, Date.today)})
- %canvas#weekChart{height: 200}
+ %canvas#weekChart{ height: 200 }
.prepend-top-default
%p.light
- Builds for last month
+ Jobs for last month
(#{date_from_to(Date.today - 30.days, Date.today)})
- %canvas#monthChart{height: 200}
+ %canvas#monthChart{ height: 200 }
.prepend-top-default
%p.light
- Builds for last year
- %canvas#yearChart.padded{height: 250}
+ Jobs for last year
+ %canvas#yearChart.padded{ height: 250 }
- [:week, :month, :year].each do |scope|
:javascript
diff --git a/app/views/projects/graphs/commits.html.haml b/app/views/projects/graphs/commits.html.haml
index 7e34a89f9ae..c8a82f7bca3 100644
--- a/app/views/projects/graphs/commits.html.haml
+++ b/app/views/projects/graphs/commits.html.haml
@@ -11,7 +11,7 @@
%p.lead
Commit statistics for
- %strong #{@ref}
+ %strong= @ref
#{@commits_graph.start_date.strftime('%b %d')} - #{@commits_graph.end_date.strftime('%b %d')}
.row
@@ -19,19 +19,19 @@
%ul
%li
%p.lead
- %strong #{@commits_graph.commits.size}
+ %strong= @commits_graph.commits.size
commits during
- %strong #{@commits_graph.duration}
+ %strong= @commits_graph.duration
days
%li
%p.lead
Average
- %strong #{@commits_graph.commit_per_day}
+ %strong= @commits_graph.commit_per_day
commits per day
%li
%p.lead
Contributed by
- %strong #{@commits_graph.authors}
+ %strong= @commits_graph.authors
authors
.col-md-6
%div
diff --git a/app/views/projects/graphs/show.html.haml b/app/views/projects/graphs/show.html.haml
index ac5f792d140..5ebb939a109 100644
--- a/app/views/projects/graphs/show.html.haml
+++ b/app/views/projects/graphs/show.html.haml
@@ -21,7 +21,7 @@
%h3#date_header.page-title
%p.light
Commits to #{@ref}, excluding merge commits. Limited to 6,000 commits.
- %input#brush_change{:type => "hidden"}
+ %input#brush_change{ :type => "hidden" }
.graphs.row
#contributors-master
#contributors.clearfix
diff --git a/app/views/projects/group_links/_index.html.haml b/app/views/projects/group_links/_index.html.haml
new file mode 100644
index 00000000000..99d0df2ac34
--- /dev/null
+++ b/app/views/projects/group_links/_index.html.haml
@@ -0,0 +1,55 @@
+- page_title "Groups"
+.row.prepend-top-default
+ .col-lg-3.settings-sidebar
+ %h4.prepend-top-0
+ Share project with other groups
+ %p
+ Projects can be stored in only one group at once. However you can share a project with other groups here.
+ .col-lg-9
+ %h5.prepend-top-0
+ Set a group to share
+ = form_tag namespace_project_group_links_path(@project.namespace, @project), class: 'js-requires-input', method: :post do
+ .form-group
+ = label_tag :link_group_id, "Group", class: "label-light"
+ = groups_select_tag(:link_group_id, data: { skip_groups: @skip_groups }, required: true)
+ .form-group
+ = label_tag :link_group_access, "Max access level", class: "label-light"
+ .select-wrapper
+ = select_tag :link_group_access, options_for_select(ProjectGroupLink.access_options, ProjectGroupLink.default_access), class: "form-control select-control"
+ = icon('caret-down')
+ .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-groups', placeholder: 'Select access expiration date', id: 'expires_at_groups'
+ %i.clear-icon.js-clear-input
+ .help-block
+ On this date, all members 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
+ .col-lg-9.col-lg-offset-3.append-bottom-default.enabled-groups
+ %h5.prepend-top-0
+ Groups you share with (#{@group_links.size})
+ - if @group_links.present?
+ %ul.well-list
+ - @group_links.each do |group_link|
+ - group = group_link.group
+ %li
+ .pull-left.append-right-10.hidden-xs
+ = icon("folder-open-o", class: "settings-list-icon")
+ .pull-left
+ = link_to group do
+ = 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
+ = icon("trash")
+ - else
+ .settings-message.text-center
+ There are no groups with access to your project, add one in the form above
diff --git a/app/views/projects/group_links/index.html.haml b/app/views/projects/group_links/index.html.haml
deleted file mode 100644
index 1b0dbbb8111..00000000000
--- a/app/views/projects/group_links/index.html.haml
+++ /dev/null
@@ -1,55 +0,0 @@
-- page_title "Groups"
-.row.prepend-top-default
- .col-lg-3.settings-sidebar
- %h4.prepend-top-0
- Share project with other groups
- %p
- Projects can be stored in only one group at once. However you can share a project with other groups here.
- .col-lg-9
- %h5.prepend-top-0
- Set a group to share
- = form_tag namespace_project_group_links_path(@project.namespace, @project), class: 'js-requires-input', method: :post do
- .form-group
- = label_tag :link_group_id, "Group", class: "label-light"
- = groups_select_tag(:link_group_id, data: { skip_groups: @skip_groups }, required: true)
- .form-group
- = label_tag :link_group_access, "Max access level", class: "label-light"
- .select-wrapper
- = select_tag :link_group_access, options_for_select(ProjectGroupLink.access_options, ProjectGroupLink.default_access), class: "form-control select-control"
- = icon('caret-down')
- .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
- .col-lg-9.col-lg-offset-3.append-bottom-default.enabled-groups
- %h5.prepend-top-0
- Groups you share with (#{@group_links.size})
- - if @group_links.present?
- %ul.well-list
- - @group_links.each do |group_link|
- - group = group_link.group
- %li
- .pull-left.append-right-10.hidden-xs
- = icon("folder-open-o", class: "settings-list-icon")
- .pull-left
- = link_to group do
- = 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
- = icon("trash")
- - else
- .settings-message.text-center
- There are no groups with access to your project, add one in the form above
diff --git a/app/views/projects/hooks/index.html.haml b/app/views/projects/hooks/_index.html.haml
index 8faad351463..8faad351463 100644
--- a/app/views/projects/hooks/index.html.haml
+++ b/app/views/projects/hooks/_index.html.haml
diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml
index c2f4457b60b..5d4e593e4ef 100644
--- a/app/views/projects/issues/_discussion.html.haml
+++ b/app/views/projects/issues/_discussion.html.haml
@@ -1,7 +1,7 @@
- content_for :note_actions do
- if can?(current_user, :update_issue, @issue)
- = link_to 'Reopen issue', issue_path(@issue, issue: {state_event: :reopen}, status_only: true, format: 'json'), data: {no_turbolink: true, original_text: "Reopen issue", alternative_text: "Comment & reopen issue"}, class: "btn btn-nr btn-reopen btn-comment js-note-target-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
- = link_to 'Close issue', issue_path(@issue, issue: {state_event: :close}, status_only: true, format: 'json'), data: {no_turbolink: true, original_text: "Close issue", alternative_text: "Comment & close issue"}, class: "btn btn-nr btn-close btn-comment js-note-target-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
+ = link_to 'Reopen issue', issue_path(@issue, issue: {state_event: :reopen}, format: 'json'), data: {no_turbolink: true, original_text: "Reopen issue", alternative_text: "Comment & reopen issue"}, class: "btn btn-nr btn-reopen btn-comment js-note-target-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
+ = link_to 'Close issue', issue_path(@issue, issue: {state_event: :close}, format: 'json'), data: {no_turbolink: true, original_text: "Close issue", alternative_text: "Comment & close issue"}, class: "btn btn-nr btn-close btn-comment js-note-target-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
#notes
= render 'projects/notes/notes_with_form'
diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml
index c80210d6ff4..085b2fc2814 100644
--- a/app/views/projects/issues/_issue.html.haml
+++ b/app/views/projects/issues/_issue.html.haml
@@ -34,7 +34,7 @@
= note_count
.issue-info
- #{issue.to_reference} &middot;
+ #{issuable_reference(issue)} &middot;
opened #{time_ago_with_tooltip(issue.created_at, placement: 'bottom')}
by #{link_to_member(@project, issue.author, avatar: false)}
- if issue.milestone
@@ -43,14 +43,14 @@
= icon('clock-o')
= issue.milestone.title
- if issue.due_date
- %span{class: "#{'cred' if issue.overdue?}"}
+ %span{ class: "#{'cred' if issue.overdue?}" }
&nbsp;
= icon('calendar')
= issue.due_date.to_s(:medium)
- if issue.labels.any?
&nbsp;
- issue.labels.each do |label|
- = link_to_label(label, subject: issue.project)
+ = link_to_label(label, subject: issue.project, css_class: 'label-link')
- if issue.tasks?
&nbsp;
%span.task-status
diff --git a/app/views/projects/issues/_new_branch.html.haml b/app/views/projects/issues/_new_branch.html.haml
index c56b6cc11f5..13e2150f997 100644
--- a/app/views/projects/issues/_new_branch.html.haml
+++ b/app/views/projects/issues/_new_branch.html.haml
@@ -1,9 +1,6 @@
- if can?(current_user, :push_code, @project)
.pull-right
- #new-branch.new-branch{'data-path' => can_create_branch_namespace_project_issue_path(@project.namespace, @project, @issue)}
- = link_to '#', class: 'checking btn btn-grouped', disabled: 'disabled' do
- = icon('spinner spin')
- Checking branches
+ #new-branch.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 btn-new btn-inverted btn-grouped has-tooltip available hide', title: @issue.to_branch_name do
New branch
diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml
index 26f3f0ac292..8ea1a3a45e1 100644
--- a/app/views/projects/issues/index.html.haml
+++ b/app/views/projects/issues/index.html.haml
@@ -6,6 +6,9 @@
= content_for :sub_nav do
= render "projects/issues/head"
+- content_for :page_specific_javascripts do
+ = page_specific_javascript_bundle_tag('filtered_search')
+
= content_for :meta_tags do
- if current_user
= auto_discovery_link_tag(:atom, url_for(params.merge(format: :atom, private_token: current_user.private_token)), title: "#{@project.name} issues")
@@ -16,11 +19,8 @@
= render 'shared/issuable/nav', type: :issues
.nav-controls
- if current_user
- = link_to url_for(params.merge(format: :atom, private_token: current_user.private_token)), class: 'btn append-right-10' do
+ = link_to url_for(params.merge(format: :atom, private_token: current_user.private_token)), class: 'btn append-right-10 has-tooltip', title: 'Subscribe' do
= icon('rss')
- %span.icon-label
- Subscribe
- = render 'shared/issuable/search_form', path: namespace_project_issues_path(@project.namespace, @project)
- if can? current_user, :create_issue, @project
= link_to new_namespace_project_issue_path(@project.namespace,
@project,
@@ -30,7 +30,7 @@
title: "New Issue",
id: "new_issue_link" do
New Issue
- = render 'shared/issuable/filter', type: :issues
+ = render 'shared/issuable/search_bar', type: :issues
.issues-holder
= render 'issues'
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index 981bf640a6b..d3eb3b7055b 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -1,7 +1,9 @@
-- @content_class = "limit-container-width"
+- @content_class = "limit-container-width" unless fluid_layout
- page_title "#{@issue.title} (#{@issue.to_reference})", "Issues"
- page_description @issue.description
- page_card_attributes @issue.card_attributes
+- content_for :page_specific_javascripts do
+ = page_specific_javascript_bundle_tag('lib_vue')
.clearfix.detail-page-header
.issuable-header
@@ -33,9 +35,9 @@
= link_to 'New issue', new_namespace_project_issue_path(@project.namespace, @project), title: 'New issue', id: 'new_issue_link'
- if can?(current_user, :update_issue, @issue)
%li
- = link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
+ = link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, format: 'json'), data: {no_turbolink: true}, class: "btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
%li
- = link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
+ = link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, format: 'json'), data: {no_turbolink: true}, class: "btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
%li
= link_to 'Edit', edit_namespace_project_issue_path(@project.namespace, @project, @issue)
- if @issue.submittable_as_spam? && current_user.admin?
@@ -46,8 +48,8 @@
= link_to new_namespace_project_issue_path(@project.namespace, @project), class: 'hidden-xs hidden-sm btn btn-grouped new-issue-link btn-new btn-inverted', title: 'New issue', id: 'new_issue_link' do
New issue
- if can?(current_user, :update_issue, @issue)
- = link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-grouped btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
- = link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-grouped btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
+ = link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-grouped btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
+ = link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-grouped btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
- if @issue.submittable_as_spam? && current_user.admin?
= link_to 'Submit as spam', mark_as_spam_namespace_project_issue_path(@project.namespace, @project, @issue), method: :post, class: 'hidden-xs hidden-sm btn btn-grouped btn-spam', title: 'Submit as spam'
= link_to 'Edit', edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: 'hidden-xs hidden-sm btn btn-grouped issuable-edit'
@@ -73,7 +75,7 @@
// This element is filled in using JavaScript.
.content-block.content-block-small
- = render 'new_branch'
+ = render 'new_branch' unless @issue.confidential?
= render 'award_emoji/awards_block', awardable: @issue, inline: true
%section.issuable-discussion
diff --git a/app/views/projects/issues/verify.html.haml b/app/views/projects/issues/verify.html.haml
new file mode 100644
index 00000000000..1934b18c086
--- /dev/null
+++ b/app/views/projects/issues/verify.html.haml
@@ -0,0 +1,20 @@
+- page_title "Anti-spam verification"
+
+%h3.page-title
+ Anti-spam verification
+%hr
+
+%p
+ We detected potential spam in the issue description. Please verify that you are not a robot to submit the issue.
+
+= form_for [@project.namespace.becomes(Namespace), @project, @issue] do |f|
+ .recaptcha
+ - params[:issue].each do |field, value|
+ = hidden_field(:issue, field, value: value)
+ = hidden_field_tag(:merge_request_for_resolving_discussions, params[:merge_request_for_resolving_discussions])
+ = hidden_field_tag(:spam_log_id, @issue.spam_log.id)
+ = hidden_field_tag(:recaptcha_verification, true)
+ = recaptcha_tags
+
+ .row-content-block.footer-block
+ = f.submit "Submit #{@issue.class.model_name.human.downcase}", class: 'btn btn-create'
diff --git a/app/views/projects/labels/destroy.js.haml b/app/views/projects/labels/destroy.js.haml
deleted file mode 100644
index 8d09e2bda11..00000000000
--- a/app/views/projects/labels/destroy.js.haml
+++ /dev/null
@@ -1,2 +0,0 @@
-- if @labels.empty?
- $('.labels').load(document.URL + ' .nothing-here-block').hide().fadeIn(1000)
diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml
index 05a8475dcd6..29f861c09c6 100644
--- a/app/views/projects/labels/index.html.haml
+++ b/app/views/projects/labels/index.html.haml
@@ -3,37 +3,35 @@
- hide_class = ''
= render "projects/issues/head"
-%div{ class: container_class }
- .top-area.adjust
- .nav-text
- Labels can be applied to issues and merge requests. Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging.
+- if @labels.exists? || @prioritized_labels.exists?
+ %div{ class: container_class }
+ .top-area.adjust
+ .nav-text
+ Labels can be applied to issues and merge requests. Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging.
- .nav-controls
- - if can?(current_user, :admin_label, @project)
- = link_to new_namespace_project_label_path(@project.namespace, @project), class: "btn btn-new" do
- New label
-
- .labels
- - if can?(current_user, :admin_label, @project)
- -# Only show it in the first page
- - hide = @available_labels.empty? || (params[:page].present? && params[:page] != '1')
- .prioritized-labels{ class: ('hide' if hide) }
- %h5 Prioritized Labels
- %ul.content-list.manage-labels-list.js-prioritized-labels{ "data-url" => set_priorities_namespace_project_labels_path(@project.namespace, @project) }
- %p.empty-message{ class: ('hidden' unless @prioritized_labels.empty?) } No prioritized labels yet
- - if @prioritized_labels.present?
- = render partial: 'shared/label', subject: @project, collection: @prioritized_labels, as: :label
+ .nav-controls
+ - if can?(current_user, :admin_label, @project)
+ = link_to new_namespace_project_label_path(@project.namespace, @project), class: "btn btn-new" do
+ New label
- .other-labels
+ .labels
- if can?(current_user, :admin_label, @project)
- %h5{ class: ('hide' if hide) } Other Labels
- %ul.content-list.manage-labels-list.js-other-labels
- - if @labels.present?
- = render partial: 'shared/label', subject: @project, collection: @labels, as: :label
- = paginate @labels, theme: 'gitlab'
- - if @labels.blank?
- .nothing-here-block
+ -# Only show it in the first page
+ - hide = @available_labels.empty? || (params[:page].present? && params[:page] != '1')
+ .prioritized-labels{ class: ('hide' if hide) }
+ %h5 Prioritized Labels
+ %ul.content-list.manage-labels-list.js-prioritized-labels{ "data-url" => set_priorities_namespace_project_labels_path(@project.namespace, @project) }
+ #js-priority-labels-empty-state{ class: "#{'hidden' unless @prioritized_labels.empty?}" }
+ = render 'shared/empty_states/priority_labels'
+ - if @prioritized_labels.present?
+ = render partial: 'shared/label', subject: @project, collection: @prioritized_labels, as: :label
+
+ - if @labels.present?
+ .other-labels
- if can?(current_user, :admin_label, @project)
- Create a label or #{link_to 'generate a default set of labels', generate_namespace_project_labels_path(@project.namespace, @project), method: :post}.
- - else
- No labels created
+ %h5{ class: ('hide' if hide) } Other Labels
+ %ul.content-list.manage-labels-list.js-other-labels
+ = render partial: 'shared/label', subject: @project, collection: @labels, as: :label
+ = paginate @labels, theme: 'gitlab'
+- else
+ = render 'shared/empty_states/labels'
diff --git a/app/views/projects/mattermosts/_no_teams.html.haml b/app/views/projects/mattermosts/_no_teams.html.haml
new file mode 100644
index 00000000000..aac74a25b75
--- /dev/null
+++ b/app/views/projects/mattermosts/_no_teams.html.haml
@@ -0,0 +1,16 @@
+- if @teams_error_message
+ = content_for :flash_message do
+ .alert.alert-danger= @teams_error_message
+
+%p
+ You aren’t a member of any team on the Mattermost instance at
+ %strong= Gitlab.config.mattermost.host
+%p
+ To install this service,
+ = link_to "#{Gitlab.config.mattermost.host}/select_team", target: '__blank' do
+ join a team
+ = icon('external-link')
+ and try again.
+%hr
+.clearfix
+ = link_to 'Go back', edit_namespace_project_service_path(@project.namespace, @project, @service), class: 'btn btn-lg pull-right'
diff --git a/app/views/projects/mattermosts/_team_selection.html.haml b/app/views/projects/mattermosts/_team_selection.html.haml
new file mode 100644
index 00000000000..a80f9aa4c4a
--- /dev/null
+++ b/app/views/projects/mattermosts/_team_selection.html.haml
@@ -0,0 +1,47 @@
+%p
+ This service will be installed on the Mattermost instance at
+ %strong= link_to Gitlab.config.mattermost.host, Gitlab.config.mattermost.host
+%hr
+= form_for(:mattermost, method: :post, url: namespace_project_mattermost_path(@project.namespace, @project)) do |f|
+ %h4 Team
+ %p
+ = @teams.one? ? 'The team' : 'Select the team'
+ where the slash commands will be used in
+ - selected_id = @teams.one? ? @teams.keys.first : 0
+ - options = mattermost_teams_options(@teams)
+ - options = options_for_select(options, selected_id)
+ = f.select(:team_id, options, {}, { class: 'form-control', disabled: @teams.one?, selected: selected_id })
+ = f.hidden_field(:team_id, value: selected_id) if @teams.one?
+ .help-block
+ - if @teams.one?
+ This is the only available team.
+ - else
+ The list shows all available teams.
+ To create a team,
+ = link_to "#{Gitlab.config.mattermost.host}/create_team" do
+ use Mattermost's interface
+ = icon('external-link')
+ or ask your Mattermost system administrator.
+ %hr
+ %h4 Command trigger word
+ %p Choose the word that will trigger commands
+ = f.text_field(:trigger, value: @project.path, class: 'form-control')
+ .help-block
+ %p
+ Trigger word must be unique, and can't begin with a slash or contain any spaces.
+ Use the word that works best for your team.
+ %p
+ Suggestions:
+ %code= 'gitlab'
+ %code= @project.path # Path contains no spaces, but dashes
+ %code= @project.path_with_namespace
+ %p
+ Reserved:
+ = link_to 'https://docs.mattermost.com/help/messaging/executing-commands.html#built-in-commands', target: '__blank' do
+ see list of built-in slash commands
+ = icon('external-link')
+ %hr
+ .clearfix
+ .pull-right
+ = link_to 'Cancel', edit_namespace_project_service_path(@project.namespace, @project, @service), class: 'btn btn-lg'
+ = f.submit 'Install', class: 'btn btn-save btn-lg'
diff --git a/app/views/projects/mattermosts/new.html.haml b/app/views/projects/mattermosts/new.html.haml
new file mode 100644
index 00000000000..96b1d2aee61
--- /dev/null
+++ b/app/views/projects/mattermosts/new.html.haml
@@ -0,0 +1,8 @@
+.service-installation
+ .inline.pull-right
+ = custom_icon('mattermost_logo', size: 48)
+ %h3 Install Mattermost Command
+ - if @teams.empty?
+ = render 'no_teams'
+ - else
+ = render 'team_selection'
diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml
index b3c43286a50..4dbb97b3228 100644
--- a/app/views/projects/merge_requests/_merge_request.html.haml
+++ b/app/views/projects/merge_requests/_merge_request.html.haml
@@ -46,7 +46,7 @@
= note_count
.merge-request-info
- #{merge_request.to_reference} &middot;
+ #{issuable_reference(merge_request)} &middot;
opened #{time_ago_with_tooltip(merge_request.created_at, placement: 'bottom')}
by #{link_to_member(@project, merge_request.author, avatar: false)}
- if merge_request.target_project.default_branch != merge_request.target_branch
@@ -64,8 +64,8 @@
- if merge_request.labels.any?
&nbsp;
- merge_request.labels.each do |label|
- = link_to_label(label, subject: merge_request.project, type: :merge_request)
-
+ = link_to_label(label, subject: merge_request.project, type: :merge_request, css_class: 'label-link')
+
- if merge_request.tasks?
&nbsp;
%span.task-status
diff --git a/app/views/projects/merge_requests/_new_diffs.html.haml b/app/views/projects/merge_requests/_new_diffs.html.haml
index 74367ab9b7b..627fc4e9671 100644
--- a/app/views/projects/merge_requests/_new_diffs.html.haml
+++ b/app/views/projects/merge_requests/_new_diffs.html.haml
@@ -1 +1 @@
-= render "projects/diffs/diffs", diffs: @diffs, show_whitespace_toggle: false
+= render "projects/diffs/diffs", diffs: @diffs, environment: @environment, show_whitespace_toggle: false
diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml
index 4a08ed045f4..bd72310c16b 100644
--- a/app/views/projects/merge_requests/_new_submit.html.haml
+++ b/app/views/projects/merge_requests/_new_submit.html.haml
@@ -3,15 +3,15 @@
%p.slead
- source_title, target_title = format_mr_branch_names(@merge_request)
From
- %strong.label-branch #{source_title}
+ %strong.label-branch= source_title
%span into
- %strong.label-branch #{target_title}
+ %strong.label-branch= target_title
%span.pull-right
= link_to 'Change branches', mr_change_branches_path(@merge_request)
%hr
-= form_for [@project.namespace.becomes(Namespace), @project, @merge_request], html: { class: 'merge-request-form form-horizontal common-note-form js-requires-input' } do |f|
- = render 'shared/issuable/form', f: f, issuable: @merge_request
+= form_for [@project.namespace.becomes(Namespace), @project, @merge_request], html: { class: 'merge-request-form form-horizontal common-note-form js-requires-input js-quick-submit' } do |f|
+ = render 'shared/issuable/form', f: f, issuable: @merge_request, commits: @commits
= f.hidden_field :source_project_id
= f.hidden_field :source_branch
= f.hidden_field :target_project_id
@@ -34,11 +34,6 @@
= link_to url_for(params), data: {target: 'div#pipelines', action: 'pipelines', toggle: 'tab'} do
Pipelines
%span.badge= @pipelines.size
- - if @pipeline.present?
- %li.builds-tab
- = link_to url_for(params), data: {target: 'div#builds', action: 'builds', toggle: 'tab'} do
- Builds
- %span.badge= @statuses_count
%li.diffs-tab
= link_to url_for(params.merge(action: 'new_diffs')), data: {target: 'div#diffs', action: 'new/diffs', toggle: 'tab'} do
Changes
@@ -48,13 +43,10 @@
#commits.commits.tab-pane.active
= render "projects/merge_requests/show/commits"
#diffs.diffs.tab-pane
- - # This tab is always loaded via AJAX
- - if @pipeline.present?
- #builds.builds.tab-pane
- = render "projects/merge_requests/show/builds"
+ -# This tab is always loaded via AJAX
- if @pipelines.any?
#pipelines.pipelines.tab-pane
- = render "projects/merge_requests/show/pipelines"
+ = render 'projects/merge_requests/show/pipelines', endpoint: url_for(params.merge(format: :json))
.mr-loading-status
= spinner
@@ -66,6 +58,5 @@
});
:javascript
var merge_request = new MergeRequest({
- action: "#{(@show_changes_tab ? 'new/diffs' : 'new')}",
- buildsLoaded: "#{@pipeline.present? ? 'true' : 'false'}"
+ action: "#{(@show_changes_tab ? 'new/diffs' : 'new')}"
});
diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml
index 7725558518f..83250443bea 100644
--- a/app/views/projects/merge_requests/_show.html.haml
+++ b/app/views/projects/merge_requests/_show.html.haml
@@ -1,14 +1,15 @@
-- @content_class = "limit-container-width"
+- @content_class = "limit-container-width" unless fluid_layout
- page_title "#{@merge_request.title} (#{@merge_request.to_reference})", "Merge Requests"
- page_description @merge_request.description
- page_card_attributes @merge_request.card_attributes
- content_for :page_specific_javascripts do
- = page_specific_javascript_tag('diff_notes/diff_notes_bundle.js')
+ = page_specific_javascript_bundle_tag('lib_vue')
+ = page_specific_javascript_bundle_tag('diff_notes')
-.merge-request{'data-url' => merge_request_path(@merge_request)}
+.merge-request{ 'data-url' => merge_request_path(@merge_request) }
= render "projects/merge_requests/show/mr_title"
- .merge-request-details.issuable-details{data: {id: @merge_request.project.id}}
+ .merge-request-details.issuable-details{ data: { id: @merge_request.project.id } }
= render "projects/merge_requests/show/mr_box"
.append-bottom-default.mr-source-target.prepend-top-default
- if @merge_request.open?
@@ -42,71 +43,64 @@
= render "projects/merge_requests/widget/show.html.haml"
- if @merge_request.source_branch_exists? && @merge_request.mergeable? && @merge_request.can_be_merged_by?(current_user)
- .merge-manually.light.prepend-top-default.append-bottom-default
+ .merge-manually.light.prepend-top-default
You can also accept this merge request manually using the
= succeed '.' do
= link_to "command line", "#modal_merge_info", class: "how_to_merge_link vlink", title: "How To Merge", "data-toggle" => "modal"
- - if @commits_count.nonzero?
- .merge-request-tabs-holder{ class: ("js-tabs-affix" unless ENV['RAILS_ENV'] == 'test') }
- %div{ class: container_class }
- %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
- Discussion
- %span.badge= @merge_request.related_notes.user.count
- - if @merge_request.source_project
- %li.commits-tab
- = link_to commits_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#commits', action: 'commits', toggle: 'tab' } do
- Commits
- %span.badge= @commits_count
- - if @pipelines.any?
- %li.pipelines-tab
- = link_to pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: '#pipelines', action: 'pipelines', toggle: 'tab' } do
- Pipelines
- %span.badge= @pipelines.size
- - if @pipeline.present?
- %li.builds-tab
- = link_to builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: '#builds', action: 'builds', toggle: 'tab' } do
- Builds
- %span.badge= @statuses_count
- %li.diffs-tab
- = link_to diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#diffs', action: 'diffs', toggle: 'tab' } do
- Changes
- %span.badge= @merge_request.diff_size
- %li#resolve-count-app.line-resolve-all-container.pull-right.prepend-top-10.hidden-xs{ "v-cloak" => true }
- %resolve-count{ "inline-template" => true, ":logged-out" => "#{current_user.nil?}" }
- %div
- .line-resolve-all{ "v-show" => "discussionCount > 0",
- ":class" => "{ 'has-next-btn': !loggedOut && resolvedDiscussionCount !== discussionCount }" }
- %span.line-resolve-btn.is-disabled{ type: "button",
- ":class" => "{ 'is-active': resolvedDiscussionCount === discussionCount }" }
- = render "shared/icons/icon_status_success.svg"
- %span.line-resolve-text
- {{ resolvedDiscussionCount }}/{{ discussionCount }} {{ resolvedCountText }} resolved
- = render "discussions/jump_to_next"
+ .content-block.content-block-small.emoji-list-container
+ = render 'award_emoji/awards_block', awardable: @merge_request, inline: true
- .tab-content#diff-notes-app
- #notes.notes.tab-pane.voting_notes
- .content-block.content-block-small
- = render 'award_emoji/awards_block', awardable: @merge_request, inline: true
+ .merge-request-tabs-holder{ class: ("js-tabs-affix" unless ENV['RAILS_ENV'] == 'test') }
+ .merge-request-tabs-container
+ %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
+ Discussion
+ %span.badge= @merge_request.related_notes.user.count
+ - if @merge_request.source_project
+ %li.commits-tab
+ = link_to commits_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#commits', action: 'commits', toggle: 'tab' } do
+ Commits
+ %span.badge= @commits_count
+ - if @pipelines.any?
+ %li.pipelines-tab
+ = link_to pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: '#pipelines', action: 'pipelines', toggle: 'tab' } do
+ Pipelines
+ %span.badge= @pipelines.size
+ %li.diffs-tab
+ = link_to diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#diffs', action: 'diffs', toggle: 'tab' } do
+ Changes
+ %span.badge= @merge_request.diff_size
+ %li#resolve-count-app.line-resolve-all-container.pull-right.prepend-top-10.hidden-xs{ "v-cloak" => true }
+ %resolve-count{ "inline-template" => true, ":logged-out" => "#{current_user.nil?}" }
+ %div
+ .line-resolve-all{ "v-show" => "discussionCount > 0",
+ ":class" => "{ 'has-next-btn': !loggedOut && resolvedDiscussionCount !== discussionCount }" }
+ %span.line-resolve-btn.is-disabled{ type: "button",
+ ":class" => "{ 'is-active': resolvedDiscussionCount === discussionCount }" }
+ = render "shared/icons/icon_status_success.svg"
+ %span.line-resolve-text
+ {{ resolvedDiscussionCount }}/{{ discussionCount }} {{ resolvedCountText }} resolved
+ = render "discussions/jump_to_next"
- .row
- %section.col-md-12
- .issuable-discussion
- = render "projects/merge_requests/discussion"
+ .tab-content#diff-notes-app
+ #notes.notes.tab-pane.voting_notes
+ .row
+ %section.col-md-12
+ .issuable-discussion
+ = render "projects/merge_requests/discussion"
- #commits.commits.tab-pane
- - # 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
+ #commits.commits.tab-pane
+ -# This tab is always loaded via AJAX
+ #pipelines.pipelines.tab-pane
+ - if @pipelines.any?
+ = render 'projects/commit/pipelines_list', endpoint: pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)
+ #diffs.diffs.tab-pane
+ -# This tab is always loaded via AJAX
- .mr-loading-status
- = spinner
+ .mr-loading-status
+ = spinner
= render 'shared/issuable/sidebar', issuable: @merge_request
- if @merge_request.can_be_reverted?(current_user)
@@ -115,8 +109,10 @@
= render "projects/commit/change", type: 'cherry-pick', commit: @merge_request.merge_commit, title: @merge_request.title
:javascript
- var merge_request;
-
- merge_request = new MergeRequest({
- action: "#{controller.action_name}"
+ $(function () {
+ new MergeRequest({
+ action: "#{controller.action_name}"
+ });
});
+
+ var mrRefreshWidgetUrl = "#{mr_widget_refresh_url(@merge_request)}";
diff --git a/app/views/projects/merge_requests/conflicts.html.haml b/app/views/projects/merge_requests/conflicts.html.haml
index 16789f68f70..dcf578b85f9 100644
--- a/app/views/projects/merge_requests/conflicts.html.haml
+++ b/app/views/projects/merge_requests/conflicts.html.haml
@@ -1,6 +1,7 @@
- page_title "Merge Conflicts", "#{@merge_request.title} (#{@merge_request.to_reference}", "Merge Requests"
- content_for :page_specific_javascripts do
- = page_specific_javascript_tag('merge_conflicts/merge_conflicts_bundle.js')
+ = page_specific_javascript_bundle_tag('lib_vue')
+ = page_specific_javascript_bundle_tag('merge_conflicts')
= page_specific_javascript_tag('lib/ace.js')
= render "projects/merge_requests/show/mr_title"
@@ -9,29 +10,29 @@
= 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),
+#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"}
+ .loading{ "v-if" => "isLoading" }
%i.fa.fa-spinner.fa-spin
- .nothing-here-block{"v-if" => "hasError"}
+ .nothing-here-block{ "v-if" => "hasError" }
{{conflictsData.errorMessage}}
= render partial: "projects/merge_requests/conflicts/commit_stats"
- .files-wrapper{"v-if" => "!isLoading && !hasError"}
+ .files-wrapper{ "v-if" => "!isLoading && !hasError" }
.files
- .diff-file.file-holder.conflict{"v-for" => "file in conflictsData.files"}
+ .diff-file.file-holder.conflict{ "v-for" => "file in conflictsData.files" }
.file-title
- %i.fa.fa-fw{":class" => "file.iconClass"}
+ %i.fa.fa-fw{ ":class" => "file.iconClass" }
%strong {{file.filePath}}
= render partial: 'projects/merge_requests/conflicts/file_actions'
.diff-content.diff-wrap-lines
- .diff-wrap-lines.code.file-content.js-syntax-highlight{"v-show" => "!isParallel && file.resolveMode === 'interactive' && file.type === 'text'" }
+ .diff-wrap-lines.code.file-content.js-syntax-highlight{ "v-show" => "!isParallel && file.resolveMode === 'interactive' && file.type === 'text'" }
= render partial: "projects/merge_requests/conflicts/components/inline_conflict_lines"
- .diff-wrap-lines.code.file-content.js-syntax-highlight{"v-show" => "isParallel && file.resolveMode === 'interactive' && file.type === 'text'" }
+ .diff-wrap-lines.code.file-content.js-syntax-highlight{ "v-show" => "isParallel && file.resolveMode === 'interactive' && file.type === 'text'" }
%parallel-conflict-lines{ ":file" => "file" }
- %div{"v-show" => "file.resolveMode === 'edit' || file.type === 'text-editor'"}
+ %div{ "v-show" => "file.resolveMode === 'edit' || file.type === 'text-editor'" }
= render partial: "projects/merge_requests/conflicts/components/diff_file_editor"
= render partial: "projects/merge_requests/conflicts/submit_form"
diff --git a/app/views/projects/merge_requests/conflicts/_commit_stats.html.haml b/app/views/projects/merge_requests/conflicts/_commit_stats.html.haml
index 5ab3cd96163..964dc40a213 100644
--- a/app/views/projects/merge_requests/conflicts/_commit_stats.html.haml
+++ b/app/views/projects/merge_requests/conflicts/_commit_stats.html.haml
@@ -1,9 +1,9 @@
-.content-block.oneline-block.files-changed{"v-if" => "!isLoading && !hasError"}
- .inline-parallel-buttons{"v-if" => "showDiffViewTypeSwitcher"}
+.content-block.oneline-block.files-changed{ "v-if" => "!isLoading && !hasError" }
+ .inline-parallel-buttons{ "v-if" => "showDiffViewTypeSwitcher" }
.btn-group
- %button.btn{":class" => "{'active': !isParallel}", "@click" => "handleViewTypeChange('inline')"}
+ %button.btn{ ":class" => "{'active': !isParallel}", "@click" => "handleViewTypeChange('inline')" }
Inline
- %button.btn{":class" => "{'active': isParallel}", "@click" => "handleViewTypeChange('parallel')"}
+ %button.btn{ ":class" => "{'active': isParallel}", "@click" => "handleViewTypeChange('parallel')" }
Side-by-side
.js-toggle-container
diff --git a/app/views/projects/merge_requests/conflicts/_file_actions.html.haml b/app/views/projects/merge_requests/conflicts/_file_actions.html.haml
index 05af57acf03..0839880713f 100644
--- a/app/views/projects/merge_requests/conflicts/_file_actions.html.haml
+++ b/app/views/projects/merge_requests/conflicts/_file_actions.html.haml
@@ -1,5 +1,5 @@
.file-actions
- .btn-group{"v-if" => "file.type === 'text'"}
+ .btn-group{ "v-if" => "file.type === 'text'" }
%button.btn{ ":class" => "{ 'active': file.resolveMode == 'interactive' }",
'@click' => "onClickResolveModeButton(file, 'interactive')",
type: 'button' }
@@ -8,5 +8,5 @@
'@click' => "onClickResolveModeButton(file, 'edit')",
type: 'button' }
Edit inline
- %a.btn.view-file.btn-file-option{":href" => "file.blobPath"}
+ %a.btn.view-file{ ":href" => "file.blobPath" }
View file @{{conflictsData.shortCommitSha}}
diff --git a/app/views/projects/merge_requests/conflicts/_submit_form.html.haml b/app/views/projects/merge_requests/conflicts/_submit_form.html.haml
index 6ffaa9ad4d2..62c9748c510 100644
--- a/app/views/projects/merge_requests/conflicts/_submit_form.html.haml
+++ b/app/views/projects/merge_requests/conflicts/_submit_form.html.haml
@@ -10,7 +10,7 @@
.col-sm-offset-2.col-sm-10
.row
.col-xs-6
- %button{ type: "button", class: "btn btn-success js-submit-button", "@click" => "commit()", ":disabled" => "!readyToCommit" }
+ %button.btn.btn-success.js-submit-button{ type: "button", "@click" => "commit()", ":disabled" => "!readyToCommit" }
%span {{commitButtonText}}
.col-xs-6.text-right
= 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/conflicts/components/_diff_file_editor.html.haml b/app/views/projects/merge_requests/conflicts/components/_diff_file_editor.html.haml
index 3c927d362c2..aff3fb82fa6 100644
--- a/app/views/projects/merge_requests/conflicts/components/_diff_file_editor.html.haml
+++ b/app/views/projects/merge_requests/conflicts/components/_diff_file_editor.html.haml
@@ -1,4 +1,4 @@
-%diff-file-editor{"inline-template" => "true", ":file" => "file", ":on-cancel-discard-confirmation" => "cancelDiscardConfirmation", ":on-accept-discard-confirmation" => "acceptDiscardConfirmation"}
+%diff-file-editor{ "inline-template" => "true", ":file" => "file", ":on-cancel-discard-confirmation" => "cancelDiscardConfirmation", ":on-accept-discard-confirmation" => "acceptDiscardConfirmation" }
.diff-editor-wrap{ "v-show" => "file.showEditor" }
.discard-changes-alert-wrap{ "v-if" => "file.promptDiscardConfirmation" }
.discard-changes-alert
diff --git a/app/views/projects/merge_requests/conflicts/components/_inline_conflict_lines.html.haml b/app/views/projects/merge_requests/conflicts/components/_inline_conflict_lines.html.haml
index d35c7bee163..d828cb6cf9e 100644
--- a/app/views/projects/merge_requests/conflicts/components/_inline_conflict_lines.html.haml
+++ b/app/views/projects/merge_requests/conflicts/components/_inline_conflict_lines.html.haml
@@ -1,14 +1,14 @@
-%inline-conflict-lines{ "inline-template" => "true", ":file" => "file"}
+%inline-conflict-lines{ "inline-template" => "true", ":file" => "file" }
%table
- %tr.line_holder.diff-inline{"v-for" => "line in file.inlineLines"}
- %td.diff-line-num.new_line{":class" => "lineCssClass(line)", "v-if" => "!line.isHeader"}
+ %tr.line_holder.diff-inline{ "v-for" => "line in file.inlineLines" }
+ %td.diff-line-num.new_line{ ":class" => "lineCssClass(line)", "v-if" => "!line.isHeader" }
%a {{line.new_line}}
- %td.diff-line-num.old_line{":class" => "lineCssClass(line)", "v-if" => "!line.isHeader"}
+ %td.diff-line-num.old_line{ ":class" => "lineCssClass(line)", "v-if" => "!line.isHeader" }
%a {{line.old_line}}
- %td.line_content{":class" => "lineCssClass(line)", "v-if" => "!line.isHeader", "v-html" => "line.richText"}
- %td.diff-line-num.header{":class" => "lineCssClass(line)", "v-if" => "line.isHeader"}
- %td.diff-line-num.header{":class" => "lineCssClass(line)", "v-if" => "line.isHeader"}
- %td.line_content.header{":class" => "lineCssClass(line)", "v-if" => "line.isHeader"}
- %strong{"v-html" => "line.richText"}
+ %td.line_content{ ":class" => "lineCssClass(line)", "v-if" => "!line.isHeader", "v-html" => "line.richText" }
+ %td.diff-line-num.header{ ":class" => "lineCssClass(line)", "v-if" => "line.isHeader" }
+ %td.diff-line-num.header{ ":class" => "lineCssClass(line)", "v-if" => "line.isHeader" }
+ %td.line_content.header{ ":class" => "lineCssClass(line)", "v-if" => "line.isHeader" }
+ %strong{ "v-html" => "line.richText" }
%button.btn{ "@click" => "handleSelected(file, line.id, line.section)" }
{{line.buttonTitle}}
diff --git a/app/views/projects/merge_requests/show/_builds.html.haml b/app/views/projects/merge_requests/show/_builds.html.haml
deleted file mode 100644
index 808ef7fed27..00000000000
--- a/app/views/projects/merge_requests/show/_builds.html.haml
+++ /dev/null
@@ -1 +0,0 @@
-= render "projects/commit/pipeline", pipeline: @pipeline, link_to_commit: true
diff --git a/app/views/projects/merge_requests/show/_commits.html.haml b/app/views/projects/merge_requests/show/_commits.html.haml
index baa1ade5eee..11793919ff7 100644
--- a/app/views/projects/merge_requests/show/_commits.html.haml
+++ b/app/views/projects/merge_requests/show/_commits.html.haml
@@ -1,2 +1,8 @@
-%ol#commits-list.list-unstyled
- = render "projects/commits/commits", project: @merge_request.source_project, ref: @merge_request.source_branch
+- if @commits.empty?
+ .commits-empty
+ %h4
+ There are no commits yet.
+ = custom_icon ('illustration_no_commits')
+- else
+ %ol#commits-list.list-unstyled
+ = render "projects/commits/commits", project: @merge_request.source_project, ref: @merge_request.source_branch
diff --git a/app/views/projects/merge_requests/show/_diffs.html.haml b/app/views/projects/merge_requests/show/_diffs.html.haml
index 99c71e1454a..7f0913ea516 100644
--- a/app/views/projects/merge_requests/show/_diffs.html.haml
+++ b/app/views/projects/merge_requests/show/_diffs.html.haml
@@ -1,13 +1,5 @@
-- if @merge_request_diff.collected?
+- if @merge_request_diff.collected? || @merge_request_diff.overflow?
= render 'projects/merge_requests/show/versions'
- = render "projects/diffs/diffs", diffs: @diffs
+ = render "projects/diffs/diffs", diffs: @diffs, environment: @environment
- elsif @merge_request_diff.empty?
.nothing-here-block Nothing to merge from #{@merge_request.source_branch} into #{@merge_request.target_branch}
-- else
- .alert.alert-warning
- %h4
- Changes view for this comparison is extremely large.
- %p
- You can
- = link_to "download it", merge_request_path(@merge_request, format: :diff), class: "vlink"
- instead.
diff --git a/app/views/projects/merge_requests/show/_how_to_merge.html.haml b/app/views/projects/merge_requests/show/_how_to_merge.html.haml
index f1d5441f9dd..93ed4b68e0e 100644
--- a/app/views/projects/merge_requests/show/_how_to_merge.html.haml
+++ b/app/views/projects/merge_requests/show/_how_to_merge.html.haml
@@ -1,14 +1,14 @@
-%div#modal_merge_info.modal
+#modal_merge_info.modal
.modal-dialog
.modal-content
.modal-header
- %a.close{href: "#", "data-dismiss" => "modal"} ×
+ %a.close{ href: "#", "data-dismiss" => "modal" } ×
%h3 Check out, review, and merge locally
.modal-body
%p
%strong Step 1.
Fetch and check out the branch for this merge request
- = clipboard_button(clipboard_target: "pre#merge-info-1")
+ = clipboard_button(clipboard_target: "pre#merge-info-1", title: "Copy commands to clipboard")
%pre.dark#merge-info-1
- if @merge_request.for_fork?
:preserve
@@ -25,7 +25,7 @@
%p
%strong Step 3.
Merge the branch and fix any conflicts that come up
- = clipboard_button(clipboard_target: "pre#merge-info-3")
+ = clipboard_button(clipboard_target: "pre#merge-info-3", title: "Copy commands to clipboard")
%pre.dark#merge-info-3
- if @merge_request.for_fork?
:preserve
@@ -38,7 +38,7 @@
%p
%strong Step 4.
Push the result of the merge to GitLab
- = clipboard_button(clipboard_target: "pre#merge-info-4")
+ = clipboard_button(clipboard_target: "pre#merge-info-4", title: "Copy commands to clipboard")
%pre.dark#merge-info-4
:preserve
git push origin #{h @merge_request.target_branch}
diff --git a/app/views/projects/merge_requests/show/_mr_box.html.haml b/app/views/projects/merge_requests/show/_mr_box.html.haml
index ed23d06ee5e..683cb8a5a27 100644
--- a/app/views/projects/merge_requests/show/_mr_box.html.haml
+++ b/app/views/projects/merge_requests/show/_mr_box.html.haml
@@ -4,7 +4,7 @@
%div
- if @merge_request.description.present?
- .description{class: can?(current_user, :update_merge_request, @merge_request) ? 'js-task-list-container' : ''}
+ .description{ class: can?(current_user, :update_merge_request, @merge_request) ? 'js-task-list-container' : '' }
.wiki
= preserve do
= markdown_field(@merge_request, :description)
diff --git a/app/views/projects/merge_requests/show/_pipelines.html.haml b/app/views/projects/merge_requests/show/_pipelines.html.haml
index afe3f3430c6..de4aa255bbd 100644
--- a/app/views/projects/merge_requests/show/_pipelines.html.haml
+++ b/app/views/projects/merge_requests/show/_pipelines.html.haml
@@ -1 +1,3 @@
-= render "projects/commit/pipelines_list", pipelines: @pipelines, link_to_commit: true
+- endpoint_path = local_assigns[:endpoint] || pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request, format: :json)
+
+= render 'projects/commit/pipelines_list', endpoint: endpoint_path
diff --git a/app/views/projects/merge_requests/show/_versions.html.haml b/app/views/projects/merge_requests/show/_versions.html.haml
index 5cc92595fe0..74a7b1dc498 100644
--- a/app/views/projects/merge_requests/show/_versions.html.haml
+++ b/app/views/projects/merge_requests/show/_versions.html.haml
@@ -1,6 +1,6 @@
- if @merge_request_diffs.size > 1
.mr-version-controls
- %div.mr-version-menus-container.content-block
+ .mr-version-menus-container.content-block
Changes between
%span.dropdown.inline.mr-version-dropdown
%a.dropdown-toggle.btn.btn-default{ data: {toggle: :dropdown} }
@@ -13,19 +13,19 @@
.dropdown-menu.dropdown-select.dropdown-menu-selectable
.dropdown-title
%span Version:
- %button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}}
+ %button.dropdown-title-button.dropdown-menu-close{ aria: { label: "Close" } }
= icon('times', class: 'dropdown-menu-close-icon')
.dropdown-content
%ul
- @merge_request_diffs.each do |merge_request_diff|
%li
- = link_to merge_request_version_path(@project, @merge_request, merge_request_diff), class: ('is-active' if merge_request_diff == @merge_request_diff) do
+ = link_to merge_request_version_path(@project, @merge_request, merge_request_diff, @start_sha), class: ('is-active' if merge_request_diff == @merge_request_diff) do
%strong
- if merge_request_diff.latest?
latest version
- else
version #{version_index(merge_request_diff)}
- .monospace #{short_sha(merge_request_diff.head_commit_sha)}
+ .monospace= short_sha(merge_request_diff.head_commit_sha)
%small
#{number_with_delimiter(merge_request_diff.commits_count)} #{'commit'.pluralize(merge_request_diff.commits_count)},
= time_ago_with_tooltip(merge_request_diff.created_at)
@@ -43,7 +43,7 @@
.dropdown-menu.dropdown-select.dropdown-menu-selectable
.dropdown-title
%span Compared with:
- %button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}}
+ %button.dropdown-title-button.dropdown-menu-close{ aria: { label: "Close" } }
= icon('times', class: 'dropdown-menu-close-icon')
.dropdown-content
%ul
@@ -55,14 +55,14 @@
latest version
- else
version #{version_index(merge_request_diff)}
- .monospace #{short_sha(merge_request_diff.head_commit_sha)}
+ .monospace= short_sha(merge_request_diff.head_commit_sha)
%small
= time_ago_with_tooltip(merge_request_diff.created_at)
%li
= link_to merge_request_version_path(@project, @merge_request, @merge_request_diff), class: ('is-active' unless @start_sha) do
%strong
#{@merge_request.target_branch} (base)
- .monospace #{short_sha(@merge_request_diff.base_commit_sha)}
+ .monospace= short_sha(@merge_request_diff.base_commit_sha)
- if different_base?(@start_version, @merge_request_diff)
.content-block
@@ -72,7 +72,7 @@
= link_to namespace_project_compare_path(@project.namespace, @project, from: @start_version.base_commit_sha, to: @merge_request_diff.base_commit_sha) do
new commits
from
- %code #{@merge_request.target_branch}
+ %code= @merge_request.target_branch
- unless @merge_request_diff.latest? && !@start_sha
.comments-disabled-notif.content-block
diff --git a/app/views/projects/merge_requests/widget/_closed.html.haml b/app/views/projects/merge_requests/widget/_closed.html.haml
index f3cc0e7e8a1..15f47ecf210 100644
--- a/app/views/projects/merge_requests/widget/_closed.html.haml
+++ b/app/views/projects/merge_requests/widget/_closed.html.haml
@@ -6,7 +6,7 @@
- if @merge_request.closed_event
by #{link_to_member(@project, @merge_request.closed_event.author, avatar: true)}
#{time_ago_with_tooltip(@merge_request.closed_event.created_at)}
- %p
+ %p
= succeed '.' do
The changes were not merged into
%span.label-branch= @merge_request.target_branch
diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml
index 9ab7971b56c..bef76f16ca7 100644
--- a/app/views/projects/merge_requests/widget/_heading.html.haml
+++ b/app/views/projects/merge_requests/widget/_heading.html.haml
@@ -1,26 +1,31 @@
- if @pipeline
.mr-widget-heading
- %w[success success_with_warnings skipped canceled failed running pending].each do |status|
- .ci_widget{ class: "ci-#{status} ci-status-icon-#{status}", style: ("display:none" unless @pipeline.status == status) }
- = ci_icon_for_status(status)
+ .ci_widget{ class: "ci-#{status}", style: ("display:none" unless @pipeline.status == status) }
+ %div{ class: "ci-status-icon-#{status}" }
+ = link_to namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), class: 'icon-link' do
+ = ci_icon_for_status(status)
%span
Pipeline
= link_to "##{@pipeline.id}", namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), class: 'pipeline'
= ci_label_for_status(status)
- for
- = succeed "." do
- = link_to @pipeline.short_sha, namespace_project_commit_path(@merge_request.source_project.namespace, @merge_request.source_project, @pipeline.sha), class: "monospace"
+ .mr-widget-pipeline-graph
+ = render 'shared/mini_pipeline_graph', pipeline: @pipeline, klass: 'js-pipeline-inline-mr-widget-graph'
+ %span
+ for
+ = succeed "." do
+ = link_to @pipeline.short_sha, namespace_project_commit_path(@merge_request.source_project.namespace, @merge_request.source_project, @pipeline.sha), class: "monospace js-commit-link"
%span.ci-coverage
- elsif @merge_request.has_ci?
- - # Compatibility with old CI integrations (ex jenkins) when you request status from CI server via AJAX
- - # TODO, remove in later versions when services like Jenkins will set CI status via Commit status API
+ -# Compatibility with old CI integrations (ex jenkins) when you request status from CI server via AJAX
+ -# TODO, remove in later versions when services like Jenkins will set CI status via Commit status API
.mr-widget-heading
- %w[success skipped canceled failed running pending].each do |status|
- .ci_widget{class: "ci-#{status}", style: "display:none"}
+ .ci_widget{ class: "ci-#{status} ci-status-icon-#{status}", style: "display:none" }
= ci_icon_for_status(status)
%span
- CI build
+ CI job
= ci_label_for_status(status)
for
- commit = @merge_request.diff_head_commit
@@ -32,11 +37,11 @@
= icon("spinner spin")
Checking CI status for #{@merge_request.diff_head_commit.short_id}&hellip;
- .ci_widget.ci-not_found{style: "display:none"}
+ .ci_widget.ci-not_found{ style: "display:none" }
= icon("times-circle")
Could not find CI status for #{@merge_request.diff_head_commit.short_id}.
- .ci_widget.ci-error{style: "display:none"}
+ .ci_widget.ci-error{ style: "display:none" }
= icon("times-circle")
Could not connect to the CI server. Please check your settings and try again.
diff --git a/app/views/projects/merge_requests/widget/_merged_buttons.haml b/app/views/projects/merge_requests/widget/_merged_buttons.haml
index d836a253507..9eef011b591 100644
--- a/app/views/projects/merge_requests/widget/_merged_buttons.haml
+++ b/app/views/projects/merge_requests/widget/_merged_buttons.haml
@@ -5,10 +5,10 @@
- if can_remove_source_branch || mr_can_be_reverted || mr_can_be_cherry_picked
.clearfix.merged-buttons
- if can_remove_source_branch
- = link_to namespace_project_branch_path(@merge_request.source_project.namespace, @merge_request.source_project, @merge_request.source_branch), remote: true, method: :delete, class: "btn btn-default btn-sm remove_source_branch" do
+ = link_to namespace_project_branch_path(@merge_request.source_project.namespace, @merge_request.source_project, @merge_request.source_branch), remote: true, method: :delete, class: "btn btn-default remove_source_branch" do
= icon('trash-o')
Remove Source Branch
- if mr_can_be_reverted
- = revert_commit_link(@merge_request.merge_commit, namespace_project_merge_request_path(@project.namespace, @project, @merge_request), btn_class: 'sm')
+ = revert_commit_link(@merge_request.merge_commit, namespace_project_merge_request_path(@project.namespace, @project, @merge_request), btn_class: "warning")
- if mr_can_be_cherry_picked
- = cherry_pick_commit_link(@merge_request.merge_commit, namespace_project_merge_request_path(@project.namespace, @project, @merge_request), btn_class: 'sm')
+ = cherry_pick_commit_link(@merge_request.merge_commit, namespace_project_merge_request_path(@project.namespace, @project, @merge_request), btn_class: "default")
diff --git a/app/views/projects/merge_requests/widget/_open.html.haml b/app/views/projects/merge_requests/widget/_open.html.haml
index f4aa1609a1b..c0d6ab669b8 100644
--- a/app/views/projects/merge_requests/widget/_open.html.haml
+++ b/app/views/projects/merge_requests/widget/_open.html.haml
@@ -42,6 +42,4 @@
- if mr_issues_mentioned_but_not_closing.present?
#{"Issue".pluralize(mr_issues_mentioned_but_not_closing.size)}
!= markdown issues_sentence(mr_issues_mentioned_but_not_closing), pipeline: :gfm, author: @merge_request.author
- #{mr_issues_mentioned_but_not_closing.size > 1 ? 'are' : 'is'} mentioned but will not closed.
-
-
+ #{mr_issues_mentioned_but_not_closing.size > 1 ? 'are' : 'is'} mentioned but will not be closed.
diff --git a/app/views/projects/merge_requests/widget/_show.html.haml b/app/views/projects/merge_requests/widget/_show.html.haml
index a8918c85dde..5de59473840 100644
--- a/app/views/projects/merge_requests/widget/_show.html.haml
+++ b/app/views/projects/merge_requests/widget/_show.html.haml
@@ -16,20 +16,22 @@
gitlab_icon: "#{asset_path 'gitlab_logo.png'}",
ci_status: "#{@merge_request.head_pipeline ? @merge_request.head_pipeline.status : ''}",
ci_message: {
- normal: "Build {{status}} for \"{{title}}\"",
- preparing: "{{status}} build for \"{{title}}\""
+ normal: "Job {{status}} for \"{{title}}\"",
+ preparing: "{{status}} job for \"{{title}}\""
},
ci_enable: #{@project.ci_service ? "true" : "false"},
ci_title: {
- preparing: "{{status}} build",
- normal: "Build {{status}}"
+ preparing: "{{status}} job",
+ normal: "Job {{status}}"
},
- builds_path: "#{builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}",
+ ci_sha: "#{@merge_request.head_pipeline ? @merge_request.head_pipeline.short_sha : ''}",
+ ci_pipeline: #{@merge_request.head_pipeline.try(:id).to_json},
+ commits_path: "#{project_commits_path(@project)}",
+ pipeline_path: "#{project_pipelines_path(@project)}",
pipelines_path: "#{pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}"
};
if (typeof merge_request_widget !== 'undefined') {
- clearInterval(merge_request_widget.fetchBuildStatusInterval);
merge_request_widget.cancelPolling();
merge_request_widget.clearEventListeners();
}
diff --git a/app/views/projects/merge_requests/widget/open/_accept.html.haml b/app/views/projects/merge_requests/widget/open/_accept.html.haml
index d6f7f23533c..b730ced4214 100644
--- a/app/views/projects/merge_requests/widget/open/_accept.html.haml
+++ b/app/views/projects/merge_requests/widget/open/_accept.html.haml
@@ -1,3 +1,6 @@
+- content_for :page_specific_javascripts do
+ = page_specific_javascript_bundle_tag('merge_request_widget')
+
- status_class = @pipeline ? " ci-#{@pipeline.status}" : nil
= form_for [:merge, @project.namespace.becomes(Namespace), @project, @merge_request], remote: true, method: :post, html: { class: 'accept-mr-form js-quick-submit js-requires-input' } do |f|
@@ -32,10 +35,10 @@
The source branch will be removed.
- elsif @merge_request.can_remove_source_branch?(current_user)
.accept-control.checkbox
- = label_tag :should_remove_source_branch, class: "remove_source_checkbox" do
+ = label_tag :should_remove_source_branch, class: "merge-param-checkbox" do
= check_box_tag :should_remove_source_branch
Remove source branch
- .accept-control.right
+ .accept-control
= link_to "#", class: "modify-merge-commit-link js-toggle-button" do
= icon('edit')
Modify commit message
@@ -47,21 +50,3 @@
rows: 14, hint: true
= hidden_field_tag :merge_when_build_succeeds, "", autocomplete: "off"
-
- :javascript
- $('.accept-mr-form').on('ajax:send', function() {
- $(".accept-mr-form :input").disable();
- });
-
- $('.accept_merge_request').on('click', function() {
- $('.js-merge-button').html("<i class='fa fa-spinner fa-spin'></i> Merge in progress");
- });
-
- $('.merge_when_build_succeeds').on('click', function() {
- $("#merge_when_build_succeeds").val("1");
- });
-
- $('.js-merge-dropdown a').on('click', function(e) {
- e.preventDefault();
- $(this).closest("form").submit();
- });
diff --git a/app/views/projects/merge_requests/widget/open/_archived.html.haml b/app/views/projects/merge_requests/widget/open/_archived.html.haml
index ab30fa6b243..0d61e56d8fb 100644
--- a/app/views/projects/merge_requests/widget/open/_archived.html.haml
+++ b/app/views/projects/merge_requests/widget/open/_archived.html.haml
@@ -1,4 +1,4 @@
-%h4
+%h4
Project is archived
%p
This merge request cannot be merged because archived projects cannot be written to.
diff --git a/app/views/projects/merge_requests/widget/open/_build_failed.html.haml b/app/views/projects/merge_requests/widget/open/_build_failed.html.haml
index 14f51af5360..a18c2ad768f 100644
--- a/app/views/projects/merge_requests/widget/open/_build_failed.html.haml
+++ b/app/views/projects/merge_requests/widget/open/_build_failed.html.haml
@@ -1,6 +1,6 @@
%h4
= icon('exclamation-triangle')
- The build for this merge request failed
+ The job for this merge request failed
%p
- Please retry the build or push a new commit to fix the failure.
+ Please retry the job or push a new commit to fix the failure.
diff --git a/app/views/projects/merge_requests/widget/open/_check.html.haml b/app/views/projects/merge_requests/widget/open/_check.html.haml
index e16878ba513..909dc52fc06 100644
--- a/app/views/projects/merge_requests/widget/open/_check.html.haml
+++ b/app/views/projects/merge_requests/widget/open/_check.html.haml
@@ -1,9 +1,6 @@
+- content_for :page_specific_javascripts do
+ = page_specific_javascript_bundle_tag('merge_request_widget')
+
%strong
= icon("spinner spin")
Checking ability to merge automatically&hellip;
-
-:javascript
- $(function() {
- merge_request_widget.getMergeStatus();
- });
-
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 af3096f04d9..c98b2c42597 100644
--- a/app/views/projects/merge_requests/widget/open/_conflicts.html.haml
+++ b/app/views/projects/merge_requests/widget/open/_conflicts.html.haml
@@ -1,21 +1,23 @@
+- can_resolve = @merge_request.conflicts_can_be_resolved_by?(current_user)
+- can_resolve_in_ui = @merge_request.conflicts_can_be_resolved_in_ui?
+- can_merge = @merge_request.can_be_merged_via_command_line_by?(current_user)
+
%h4.has-conflicts
= icon("exclamation-triangle")
This merge request contains merge conflicts
%p
- 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
-
+ To merge this request, resolve these conflicts
+ - if can_resolve && !can_resolve_in_ui
+ locally
or
+ - unless can_merge
+ ask someone with write access to this repository to
+ merge it locally.
- - 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.
+- if (can_resolve && can_resolve_in_ui) || can_merge
+ .btn-group
+ - if can_resolve && can_resolve_in_ui
+ = link_to "Resolve conflicts", conflicts_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: "btn"
+ - if can_merge
+ = link_to "Merge locally", "#modal_merge_info", class: "btn how_to_merge_link vlink", "data-toggle" => "modal"
diff --git a/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml b/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml
index 072d01d144e..cf7abf3756c 100644
--- a/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml
+++ b/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml
@@ -1,3 +1,6 @@
+- content_for :page_specific_javascripts do
+ = page_specific_javascript_bundle_tag('merge_request_widget')
+
%h4
Set by #{link_to_member(@project, @merge_request.merge_user, avatar: true)}
to be merged automatically when the pipeline succeeds.
@@ -16,7 +19,7 @@
- if remove_source_branch_button || user_can_cancel_automatic_merge
.clearfix.prepend-top-10
- if remove_source_branch_button
- = link_to merge_namespace_project_merge_request_path(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request, merge_when_build_succeeds: true, should_remove_source_branch: true, sha: @merge_request.diff_head_sha), remote: true, method: :post, class: "btn btn-grouped btn-primary btn-sm remove_source_branch" do
+ = link_to merge_namespace_project_merge_request_path(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request, merge_params(@merge_request)), remote: true, method: :post, class: "btn btn-grouped btn-primary btn-sm remove_source_branch" do
= icon('times')
Remove Source Branch When Merged
diff --git a/app/views/projects/merge_requests/widget/open/_nothing.html.haml b/app/views/projects/merge_requests/widget/open/_nothing.html.haml
index 35626b624b7..7af8c01c134 100644
--- a/app/views/projects/merge_requests/widget/open/_nothing.html.haml
+++ b/app/views/projects/merge_requests/widget/open/_nothing.html.haml
@@ -1,4 +1,4 @@
-%h4
+%h4
= icon("exclamation-triangle")
Nothing to merge from
%span.label-branch= source_branch_with_namespace(@merge_request)
diff --git a/app/views/projects/milestones/_form.html.haml b/app/views/projects/milestones/_form.html.haml
index 513710e8e66..0f4a8508751 100644
--- a/app/views/projects/milestones/_form.html.haml
+++ b/app/views/projects/milestones/_form.html.haml
@@ -20,6 +20,6 @@
- if @milestone.new_record?
= f.submit 'Create milestone', class: "btn-create btn"
= link_to "Cancel", namespace_project_milestones_path(@project.namespace, @project), class: "btn btn-cancel"
- -else
+ - else
= f.submit 'Save changes', class: "btn-save btn"
= link_to "Cancel", namespace_project_milestone_path(@project.namespace, @project, @milestone), class: "btn btn-cancel"
diff --git a/app/views/projects/network/show.html.haml b/app/views/projects/network/show.html.haml
index d8951e69242..b88eef65cef 100644
--- a/app/views/projects/network/show.html.haml
+++ b/app/views/projects/network/show.html.haml
@@ -1,7 +1,7 @@
- page_title "Network", @ref
- content_for :page_specific_javascripts do
= page_specific_javascript_tag('lib/raphael.js')
- = page_specific_javascript_tag('network/network_bundle.js')
+ = page_specific_javascript_bundle_tag('network')
= render "projects/commits/head"
= render "head"
%div{ class: container_class }
diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml
index 866b278ce57..cd685f7d0eb 100644
--- a/app/views/projects/new.html.haml
+++ b/app/views/projects/new.html.haml
@@ -50,7 +50,7 @@
= icon('github', text: 'GitHub')
%div
- if bitbucket_import_enabled?
- = link_to status_import_bitbucket_path, class: "btn import_bitbucket #{'how_to_import_link' unless bitbucket_import_configured?}", "data-no-turbolink" => "true" do
+ = link_to status_import_bitbucket_path, class: "btn import_bitbucket #{'how_to_import_link' unless bitbucket_import_configured?}" do
= icon('bitbucket', text: 'Bitbucket')
- unless bitbucket_import_configured?
= render 'bitbucket_import_modal'
@@ -77,7 +77,7 @@
- if git_import_enabled?
= link_to "#", class: 'btn js-toggle-button import_git' do
= icon('git', text: 'Repo by URL')
- %div{ class: 'import_gitlab_project' }
+ .import_gitlab_project
- if gitlab_project_import_enabled?
= link_to new_import_gitlab_project_path, class: 'btn btn_import_gitlab_project project-submit' do
= icon('gitlab', text: 'GitLab export')
diff --git a/app/views/projects/notes/_edit_form.html.haml b/app/views/projects/notes/_edit_form.html.haml
index 8620f492282..e8e450742b5 100644
--- a/app/views/projects/notes/_edit_form.html.haml
+++ b/app/views/projects/notes/_edit_form.html.haml
@@ -1,11 +1,14 @@
.note-edit-form
- = form_for note, url: namespace_project_note_path(@project.namespace, @project, note), method: :put, remote: true, authenticity_token: true, html: { class: 'edit-note common-note-form js-quick-submit' } do |f|
- = note_target_fields(note)
- = render layout: 'projects/md_preview', locals: { preview_class: 'md-preview' } do
- = render 'projects/zen', f: f, attr: :note, classes: 'note-textarea js-note-text js-task-list-field', placeholder: "Write a comment or drag your files here..."
+ = form_tag '#', method: :put, class: 'edit-note common-note-form js-quick-submit' do
+ = hidden_field_tag :target_id, '', class: 'js-form-target-id'
+ = hidden_field_tag :target_type, '', class: 'js-form-target-type'
+ = render layout: 'projects/md_preview', locals: { preview_class: 'md-preview', referenced_users: true } do
+ = render 'projects/zen', attr: 'note[note]', classes: 'note-textarea js-note-text js-task-list-field', placeholder: "Write a comment or drag your files here..."
= render 'projects/notes/hints'
.note-form-actions.clearfix
- = f.submit 'Save Comment', class: 'btn btn-nr btn-save js-comment-button'
+ .settings-message.note-edit-warning.js-edit-warning
+ Finish editing this message first!
+ = submit_tag 'Save Comment', class: 'btn btn-nr btn-save js-comment-button'
%button.btn.btn-nr.btn-cancel.note-edit-cancel{ type: 'button' }
Cancel
diff --git a/app/views/projects/notes/_form.html.haml b/app/views/projects/notes/_form.html.haml
index 46b402545cd..b561052e721 100644
--- a/app/views/projects/notes/_form.html.haml
+++ b/app/views/projects/notes/_form.html.haml
@@ -3,6 +3,7 @@
= form_for [@project.namespace.becomes(Namespace), @project, @note], remote: true, html: { :'data-type' => 'json', multipart: true, id: nil, class: "new-note js-new-note-form js-quick-submit common-note-form", "data-noteable-iid" => @note.noteable.try(:iid), }, authenticity_token: true do |f|
= hidden_field_tag :view, diff_view
= hidden_field_tag :line_type
+ = hidden_field_tag :merge_request_diff_head_sha, @note.noteable.try(:diff_head_sha)
= note_target_fields(@note)
= f.hidden_field :commit_id
= f.hidden_field :line_code
@@ -23,5 +24,5 @@
.note-form-actions.clearfix
= f.submit 'Comment', class: "btn btn-nr btn-create append-right-10 comment-btn js-comment-button"
= yield(:note_actions)
- %a.btn.btn-cancel.js-note-discard{role: "button", data: {cancel_text: "Cancel"}}
+ %a.btn.btn-cancel.js-note-discard{ role: "button", data: {cancel_text: "Cancel" } }
Discard draft
diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml
index 778a32e6345..4b1da9c73e5 100644
--- a/app/views/projects/notes/_note.html.haml
+++ b/app/views/projects/notes/_note.html.haml
@@ -5,17 +5,20 @@
%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} }
.timeline-entry-inner
.timeline-icon
- %a{href: user_path(note.author)}
+ %a{ href: user_path(note.author) }
= image_tag avatar_icon(note.author), alt: '', class: 'avatar s40'
.timeline-content
.note-header
- = link_to_member(note.project, note.author, avatar: false)
- .inline.note-headline-light
+ %a.visible-xs{ href: user_path(note.author) }
= note.author.to_reference
+ = link_to_member(note.project, note.author, avatar: false, extra_class: 'hidden-xs')
+ .note-headline-light
+ %span.hidden-xs
+ = note.author.to_reference
- unless note.system
commented
- if note.system
- %span{class: 'system-note-message'}
+ %span.system-note-message
= note.redacted_note_html
%a{ href: "##{dom_id(note)}" }
= time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note-created-ago')
@@ -23,7 +26,7 @@
.note-actions
- access = note_max_access_for_user(note)
- if access
- %span.note-role.hidden-xs= access
+ %span.note-role= access
- if note.resolvable?
- can_resolve = can?(current_user, :resolve_note, note)
@@ -59,15 +62,17 @@
- if note_editable
= link_to '#', title: 'Edit comment', class: 'note-action-button js-note-edit' do
= icon('pencil', class: 'link-highlight')
- = link_to namespace_project_note_path(note.project.namespace, note.project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'note-action-button hidden-xs js-note-delete danger' do
+ = link_to namespace_project_note_path(note.project.namespace, note.project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'note-action-button js-note-delete danger' do
= icon('trash-o', class: 'danger-highlight')
- .note-body{class: note_editable ? 'js-task-list-container' : ''}
+ .note-body{ class: note_editable ? 'js-task-list-container' : '' }
.note-text.md
= preserve do
= note.redacted_note_html
= edited_time_ago_with_tooltip(note, placement: 'bottom', html_class: 'note_edited_ago', include_author: true)
- if note_editable
- = render 'projects/notes/edit_form', note: note
+ .original-note-content.hidden{ data: { post_url: namespace_project_note_path(@project.namespace, @project, note), target_id: note.noteable.id, target_type: note.noteable.class.name.underscore } }
+ #{note.note}
+ %textarea.hidden.js-task-list-field.original-task-list= note.note
.note-awards
= render 'award_emoji/awards_block', awardable: note, inline: false
- if note.system
diff --git a/app/views/projects/notes/_notes_with_form.html.haml b/app/views/projects/notes/_notes_with_form.html.haml
index 00b62a595ff..08c73d94a09 100644
--- a/app/views/projects/notes/_notes_with_form.html.haml
+++ b/app/views/projects/notes/_notes_with_form.html.haml
@@ -1,5 +1,8 @@
%ul#notes-list.notes.main-notes-list.timeline
= render "projects/notes/notes"
+
+= render 'projects/notes/edit_form'
+
%ul.notes.notes-form.timeline
%li.timeline-entry
.flash-container.timeline-content
@@ -10,7 +13,7 @@
= image_tag avatar_icon(current_user), alt: current_user.to_reference, class: 'avatar s40'
.timeline-content.timeline-content-form
= render "projects/notes/form", view: diff_view
- - else
+ - elsif !current_user
.disabled-comment.text-center
.disabled-comment-text.inline
Please
@@ -20,4 +23,4 @@
to post a comment
:javascript
- var notes = new Notes("#{namespace_project_notes_path(namespace_id: @project.namespace, target_id: @noteable.id, target_type: @noteable.class.name.underscore)}", #{@notes.map(&:id).to_json}, #{Time.now.to_i}, "#{diff_view}")
+ var notes = new Notes("#{namespace_project_notes_path(namespace_id: @project.namespace, project_id: @project, target_id: @noteable.id, target_type: @noteable.class.name.underscore)}", #{@notes.map(&:id).to_json}, #{Time.now.to_i}, "#{diff_view}")
diff --git a/app/views/projects/pages/_access.html.haml b/app/views/projects/pages/_access.html.haml
new file mode 100644
index 00000000000..82e20eeebb3
--- /dev/null
+++ b/app/views/projects/pages/_access.html.haml
@@ -0,0 +1,13 @@
+- if @project.pages_deployed?
+ .panel.panel-default
+ .panel-heading
+ Access pages
+ .panel-body
+ %p
+ %strong
+ Congratulations! Your pages are served under:
+
+ %p= link_to @project.pages_url, @project.pages_url
+
+ - @project.pages_domains.each do |domain|
+ %p= link_to domain.url, domain.url
diff --git a/app/views/projects/pages/_destroy.haml b/app/views/projects/pages/_destroy.haml
new file mode 100644
index 00000000000..42d9ef5ccba
--- /dev/null
+++ b/app/views/projects/pages/_destroy.haml
@@ -0,0 +1,12 @@
+- if @project.pages_deployed?
+ - if can?(current_user, :remove_pages, @project)
+ .panel.panel-default.panel.panel-danger
+ .panel-heading Remove pages
+ .errors-holder
+ .panel-body
+ %p
+ Removing the pages will prevent from exposing them to outside world.
+ .form-actions
+ = link_to 'Remove pages', namespace_project_pages_path(@project.namespace, @project), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-remove"
+ - else
+ .nothing-here-block Only the project owner can remove pages
diff --git a/app/views/projects/pages/_disabled.html.haml b/app/views/projects/pages/_disabled.html.haml
new file mode 100644
index 00000000000..ad51fbc6cab
--- /dev/null
+++ b/app/views/projects/pages/_disabled.html.haml
@@ -0,0 +1,4 @@
+.panel.panel-default
+ .nothing-here-block
+ GitLab Pages are disabled.
+ Ask your system's administrator to enable it.
diff --git a/app/views/projects/pages/_list.html.haml b/app/views/projects/pages/_list.html.haml
new file mode 100644
index 00000000000..4f2dd1a1398
--- /dev/null
+++ b/app/views/projects/pages/_list.html.haml
@@ -0,0 +1,17 @@
+- if can?(current_user, :update_pages, @project) && @domains.any?
+ .panel.panel-default
+ .panel-heading
+ Domains (#{@domains.count})
+ %ul.well-list
+ - @domains.each do |domain|
+ %li
+ .pull-right
+ = link_to 'Details', namespace_project_pages_domain_path(@project.namespace, @project, domain), class: "btn btn-sm btn-grouped"
+ = link_to 'Remove', namespace_project_pages_domain_path(@project.namespace, @project, domain), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-remove btn-sm btn-grouped"
+ .clearfix
+ %span= link_to domain.domain, domain.url
+ %p
+ - if domain.subject
+ %span.label.label-gray Certificate: #{domain.subject}
+ - if domain.expired?
+ %span.label.label-danger Expired
diff --git a/app/views/projects/pages/_no_domains.html.haml b/app/views/projects/pages/_no_domains.html.haml
new file mode 100644
index 00000000000..7cea5f3e70b
--- /dev/null
+++ b/app/views/projects/pages/_no_domains.html.haml
@@ -0,0 +1,7 @@
+- if can?(current_user, :update_pages, @project)
+ .panel.panel-default
+ .panel-heading
+ Domains
+ .nothing-here-block
+ Support for domains and certificates is disabled.
+ Ask your system's administrator to enable it.
diff --git a/app/views/projects/pages/_use.html.haml b/app/views/projects/pages/_use.html.haml
new file mode 100644
index 00000000000..9db46f0b1fc
--- /dev/null
+++ b/app/views/projects/pages/_use.html.haml
@@ -0,0 +1,8 @@
+- unless @project.pages_deployed?
+ .panel.panel-info
+ .panel-heading
+ Configure pages
+ .panel-body
+ %p
+ Learn how to upload your static site and have it served by
+ GitLab by following the #{link_to "documentation on GitLab Pages", "http://doc.gitlab.com/ee/pages/README.html", target: :blank}.
diff --git a/app/views/projects/pages/show.html.haml b/app/views/projects/pages/show.html.haml
new file mode 100644
index 00000000000..b6595269b06
--- /dev/null
+++ b/app/views/projects/pages/show.html.haml
@@ -0,0 +1,26 @@
+- page_title 'Pages'
+%h3.page_title
+ Pages
+
+ - if can?(current_user, :update_pages, @project) && (Gitlab.config.pages.external_http || Gitlab.config.pages.external_https)
+ = link_to new_namespace_project_pages_domain_path(@project.namespace, @project), class: 'btn btn-new pull-right', title: 'New Domain' do
+ %i.fa.fa-plus
+ New Domain
+
+%p.light
+ With GitLab Pages you can host your static websites on GitLab.
+ Combined with the power of GitLab CI and the help of GitLab Runner
+ you can deploy static pages for your individual projects, your user or your group.
+
+%hr.clearfix
+
+- if Gitlab.config.pages.enabled
+ = render 'access'
+ = render 'use'
+ - if Gitlab.config.pages.external_http || Gitlab.config.pages.external_https
+ = render 'list'
+ - else
+ = render 'no_domains'
+ = render 'destroy'
+- else
+ = render 'disabled'
diff --git a/app/views/projects/pages_domains/_form.html.haml b/app/views/projects/pages_domains/_form.html.haml
new file mode 100644
index 00000000000..ca1b41b140a
--- /dev/null
+++ b/app/views/projects/pages_domains/_form.html.haml
@@ -0,0 +1,34 @@
+= form_for [@project.namespace.becomes(Namespace), @project, @domain], html: { class: 'form-horizontal fieldset-form' } do |f|
+ - if @domain.errors.any?
+ #error_explanation
+ .alert.alert-danger
+ - @domain.errors.full_messages.each do |msg|
+ %p= msg
+
+ .form-group
+ = f.label :domain, class: 'control-label' do
+ Domain
+ .col-sm-10
+ = f.text_field :domain, required: true, autocomplete: 'off', class: 'form-control'
+
+ - if Gitlab.config.pages.external_https
+ .form-group
+ = f.label :certificate, class: 'control-label' do
+ Certificate (PEM)
+ .col-sm-10
+ = f.text_area :certificate, rows: 5, class: 'form-control'
+ %span.help-inline Upload a certificate for your domain with all intermediates
+
+ .form-group
+ = f.label :key, class: 'control-label' do
+ Key (PEM)
+ .col-sm-10
+ = f.text_area :key, rows: 5, class: 'form-control'
+ %span.help-inline Upload a private key for your certificate
+ - else
+ .nothing-here-block
+ Support for custom certificates is disabled.
+ Ask your system's administrator to enable it.
+
+ .form-actions
+ = f.submit 'Create New Domain', class: "btn btn-save"
diff --git a/app/views/projects/pages_domains/new.html.haml b/app/views/projects/pages_domains/new.html.haml
new file mode 100644
index 00000000000..e1477c71d06
--- /dev/null
+++ b/app/views/projects/pages_domains/new.html.haml
@@ -0,0 +1,6 @@
+- page_title 'New Pages Domain'
+%h3.page_title
+ New Pages Domain
+%hr.clearfix
+%div
+ = render 'form'
diff --git a/app/views/projects/pages_domains/show.html.haml b/app/views/projects/pages_domains/show.html.haml
new file mode 100644
index 00000000000..52dddb052a7
--- /dev/null
+++ b/app/views/projects/pages_domains/show.html.haml
@@ -0,0 +1,30 @@
+- page_title "#{@domain.domain}", 'Pages Domains'
+
+%h3.page-title
+ Pages Domain
+
+.table-holder
+ %table.table
+ %tr
+ %td
+ Domain
+ %td
+ = link_to @domain.domain, @domain.url
+ %tr
+ %td
+ DNS
+ %td
+ %p
+ To access the domain create a new DNS record:
+ %pre
+ #{@domain.domain} CNAME #{@domain.project.namespace.path}.#{Settings.pages.host}.
+ %tr
+ %td
+ Certificate
+ %td
+ - if @domain.certificate_text
+ %pre
+ = @domain.certificate_text
+ - else
+ .light
+ missing
diff --git a/app/views/projects/pipelines/_head.html.haml b/app/views/projects/pipelines/_head.html.haml
index b10dd47709f..721a9b6beb5 100644
--- a/app/views/projects/pipelines/_head.html.haml
+++ b/app/views/projects/pipelines/_head.html.haml
@@ -11,9 +11,9 @@
- if project_nav_tab? :builds
= nav_link(controller: %w(builds)) do
- = link_to project_builds_path(@project), title: 'Builds', class: 'shortcuts-builds' do
+ = link_to project_builds_path(@project), title: 'Jobs', class: 'shortcuts-builds' do
%span
- Builds
+ Jobs
- if project_nav_tab? :environments
= nav_link(controller: %w(environments)) do
diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml
index b00ba2d5307..a6cd2d83bd5 100644
--- a/app/views/projects/pipelines/_info.html.haml
+++ b/app/views/projects/pipelines/_info.html.haml
@@ -23,9 +23,9 @@
.info-well
- if @commit.status
.well-segment.pipeline-info
- %div{class: "icon-container ci-status-icon-#{@commit.status}"}
- = ci_icon_for_status(@commit.status)
- = pluralize @pipeline.statuses.count(:id), "build"
+ .icon-container
+ = icon('clock-o')
+ = pluralize @pipeline.statuses.count(:id), "job"
- if @pipeline.ref
from
= link_to @pipeline.ref, namespace_project_commits_path(@project.namespace, @project, @pipeline.ref), class: "monospace"
diff --git a/app/views/projects/pipelines/_stage.html.haml b/app/views/projects/pipelines/_stage.html.haml
index 20456e792e7..a0b14a7274a 100644
--- a/app/views/projects/pipelines/_stage.html.haml
+++ b/app/views/projects/pipelines/_stage.html.haml
@@ -1,4 +1,3 @@
-%ul
- - @stage.statuses.each do |status|
- %li.dropdown-build
- = render 'ci/status/graph_badge', subject: status
+- @stage.statuses.latest.each do |status|
+ %li
+ = render 'ci/status/dropdown_graph_badge', subject: status
diff --git a/app/views/projects/pipelines/_with_tabs.html.haml b/app/views/projects/pipelines/_with_tabs.html.haml
index 88af41aa835..53067cdcba4 100644
--- a/app/views/projects/pipelines/_with_tabs.html.haml
+++ b/app/views/projects/pipelines/_with_tabs.html.haml
@@ -5,7 +5,7 @@
Pipeline
%li.js-builds-tab-link
= link_to builds_namespace_project_pipeline_path(@project.namespace, @project, @pipeline), data: {target: 'div#js-tab-builds', action: 'builds', toggle: 'tab' }, class: 'builds-tab' do
- Builds
+ Jobs
%span.badge.js-builds-counter= pipeline.statuses.count
@@ -33,7 +33,7 @@
%thead
%tr
%th Status
- %th Build ID
+ %th Job ID
%th Name
%th
- if pipeline.project.build_coverage_enabled?
diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml
index 28026ccf861..81e393d7626 100644
--- a/app/views/projects/pipelines/index.html.haml
+++ b/app/views/projects/pipelines/index.html.haml
@@ -5,23 +5,23 @@
%div{ class: container_class }
.top-area
%ul.nav-links
- %li{class: ('active' if @scope.nil?)}>
+ %li{ class: ('active' if @scope.nil?) }>
= link_to project_pipelines_path(@project) do
All
%span.badge.js-totalbuilds-count
= number_with_delimiter(@pipelines_count)
- %li{class: ('active' if @scope == 'running')}>
+ %li{ class: ('active' if @scope == 'running') }>
= link_to project_pipelines_path(@project, scope: :running) do
Running
%span.badge.js-running-count
= number_with_delimiter(@running_or_pending_count)
- %li{class: ('active' if @scope == 'branches')}>
+ %li{ class: ('active' if @scope == 'branches') }>
= link_to project_pipelines_path(@project, scope: :branches) do
Branches
- %li{class: ('active' if @scope == 'tags')}>
+ %li{ class: ('active' if @scope == 'tags') }>
= link_to project_pipelines_path(@project, scope: :tags) do
Tags
@@ -35,21 +35,29 @@
= link_to ci_lint_path, class: 'btn btn-default' do
%span CI Lint
-
- %div.content-list.pipelines
- - if @pipelines.blank?
- %div
- .nothing-here-block No pipelines to show
- - else
- .table-holder
- %table.table.ci-table.js-pipeline-table
- %thead
- %th.pipeline-status Status
- %th.pipeline-info Pipeline
- %th.pipeline-commit Commit
- %th.pipeline-stages Stages
- %th.pipeline-date
- %th.pipeline-actions.hidden-xs
- = render @pipelines, commit_sha: true, stage: true, allow_retry: true
-
- = paginate @pipelines, theme: 'gitlab'
+ .content-list.pipelines{ data: { url: namespace_project_pipelines_path(@project.namespace, @project, format: :json) } }
+ .pipeline-svgs{ "data" => {"commit_icon_svg" => custom_icon("icon_commit"),
+ "icon_status_canceled" => custom_icon("icon_status_canceled"),
+ "icon_status_running" => custom_icon("icon_status_running"),
+ "icon_status_skipped" => custom_icon("icon_status_skipped"),
+ "icon_status_created" => custom_icon("icon_status_created"),
+ "icon_status_pending" => custom_icon("icon_status_pending"),
+ "icon_status_success" => custom_icon("icon_status_success"),
+ "icon_status_failed" => custom_icon("icon_status_failed"),
+ "icon_status_warning" => custom_icon("icon_status_warning"),
+ "stage_icon_status_canceled" => custom_icon("icon_status_canceled_borderless"),
+ "stage_icon_status_running" => custom_icon("icon_status_running_borderless"),
+ "stage_icon_status_skipped" => custom_icon("icon_status_skipped_borderless"),
+ "stage_icon_status_created" => custom_icon("icon_status_created_borderless"),
+ "stage_icon_status_pending" => custom_icon("icon_status_pending_borderless"),
+ "stage_icon_status_success" => custom_icon("icon_status_success_borderless"),
+ "stage_icon_status_failed" => custom_icon("icon_status_failed_borderless"),
+ "stage_icon_status_warning" => custom_icon("icon_status_warning_borderless"),
+ "icon_play" => custom_icon("icon_play"),
+ "icon_timer" => custom_icon("icon_timer"),
+ "icon_status_manual" => custom_icon("icon_status_manual"),
+ } }
+
+ .vue-pipelines-index
+
+= page_specific_javascript_bundle_tag('vue_pipelines')
diff --git a/app/views/projects/pipelines/show.html.haml b/app/views/projects/pipelines/show.html.haml
index 29a41bc664b..49c1d886423 100644
--- a/app/views/projects/pipelines/show.html.haml
+++ b/app/views/projects/pipelines/show.html.haml
@@ -2,7 +2,7 @@
- page_title "Pipeline"
= render "projects/pipelines/head"
-%div.js-pipeline-container{ class: container_class, data: { controller_action: "#{controller.action_name}" } }
+.js-pipeline-container{ class: container_class, data: { controller_action: "#{controller.action_name}" } }
- if @commit
= render "projects/pipelines/info"
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..8024fb8979d
--- /dev/null
+++ b/app/views/projects/pipelines_settings/_show.html.haml
@@ -0,0 +1,96 @@
+.row.prepend-top-default
+ .col-lg-3.profile-settings-sidebar
+ %h4.prepend-top-0
+ CI/CD Pipelines
+ .col-lg-9
+ = form_for @project, url: namespace_project_pipelines_settings_path(@project.namespace.becomes(Namespace), @project) do |f|
+ %fieldset.builds-feature
+ - 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'
+ %hr
+ .form-group.append-bottom-default
+ = f.label :runners_token, "Runner token", class: 'label-light'
+ = f.text_field :runners_token, class: "form-control", placeholder: 'xEeFCaDAB89'
+ %p.help-block The secure token used by the Runner to checkout the project
+
+ %hr
+ .form-group
+ %h5.prepend-top-0
+ Git strategy for pipelines
+ %p
+ Choose between <code>clone</code> or <code>fetch</code> to get the recent application code
+ = link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'git-strategy')
+ .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 the project workspace is pristine as it clones the repository from scratch for every job
+ .radio
+ = f.label :build_allow_git_fetch_true do
+ = f.radio_button :build_allow_git_fetch, 'true'
+ %strong git fetch
+ %br
+ %span.descr
+ Faster as it re-uses the project workspace (falling back to clone if it doesn't exist)
+
+ %hr
+ .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 job in minutes. If a job passes this threshold, it will be marked as failed.
+ = link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'timeout')
+
+ %hr
+ .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
+ = link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'visibility-of-pipelines')
+
+ %hr
+ .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
+ A regular expression that will be used to find the test coverage
+ output in the job trace. Leave blank to disable
+ = link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'test-coverage-parsing')
+ .bs-callout.bs-callout-info
+ %p Below are examples of regex for existing tools:
+ %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 (NodeJS) -
+ %code ^Statements\s*:\s*([^%]+)
+ %li
+ excoveralls (Elixir) -
+ %code \[TOTAL\]\s+(\d+\.\d+)%
+
+ = f.submit 'Save changes', class: "btn btn-save"
+
+%hr
+
+.row.prepend-top-default
+ = render partial: 'projects/pipelines_settings/badge', collection: @badges
diff --git a/app/views/projects/pipelines_settings/show.html.haml b/app/views/projects/pipelines_settings/show.html.haml
deleted file mode 100644
index 1f698558bce..00000000000
--- a/app/views/projects/pipelines_settings/show.html.haml
+++ /dev/null
@@ -1,98 +0,0 @@
-- page_title "CI/CD Pipelines"
-
-.row.prepend-top-default
- .col-lg-3.profile-settings-sidebar
- %h4.prepend-top-0
- = page_title
- .col-lg-9
- = form_for @project, url: namespace_project_pipelines_settings_path(@project.namespace.becomes(Namespace), @project) 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'
- %hr
- .form-group.append-bottom-default
- = f.label :runners_token, "Runner token", class: 'label-light'
- = f.text_field :runners_token, class: "form-control", placeholder: 'xEeFCaDAB89'
- %p.help-block The secure token used by the Runner to checkout the project
-
- %hr
- .form-group
- %h5.prepend-top-0
- Git strategy for pipelines
- %p
- Choose between <code>clone</code> or <code>fetch</code> to get the recent application code
- = link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'git-strategy')
- .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 the project workspace is pristine as it clones the repository from scratch for every job
- .radio
- = f.label :build_allow_git_fetch_true do
- = f.radio_button :build_allow_git_fetch, 'true'
- %strong git fetch
- %br
- %span.descr
- Faster as it re-uses the project workspace (falling back to clone if it doesn't exist)
-
- %hr
- .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 job in minutes. If a job passes this threshold, it will be marked as failed.
- = link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'timeout')
-
- %hr
- .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
- = link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'visibility-of-pipelines')
-
- %hr
- .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
- A regular expression that will be used to find the test coverage
- output in the build trace. Leave blank to disable
- = link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'test-coverage-parsing')
- .bs-callout.bs-callout-info
- %p Below are examples of regex for existing tools:
- %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 (NodeJS) -
- %code ^Statements\s*:\s*([^%]+)
- %li
- excoveralls (Elixir) -
- %code \[TOTAL\]\s+(\d+\.\d+)%
-
- = 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/_group_members.html.haml b/app/views/projects/project_members/_group_members.html.haml
index 9738f369a35..c7996077bc7 100644
--- a/app/views/projects/project_members/_group_members.html.haml
+++ b/app/views/projects/project_members/_group_members.html.haml
@@ -1,7 +1,7 @@
.panel.panel-default
.panel-heading
Group members with access to
- %strong #{@group.name}
+ %strong= @group.name
%span.badge= members.size
- if can?(current_user, :admin_group_member, @group)
.controls
diff --git a/app/views/projects/project_members/_groups.html.haml b/app/views/projects/project_members/_groups.html.haml
index d7f5fa96527..fdeb5f21fbe 100644
--- a/app/views/projects/project_members/_groups.html.haml
+++ b/app/views/projects/project_members/_groups.html.haml
@@ -1,7 +1,7 @@
.panel.panel-default.project-members-groups
.panel-heading
Groups with access to
- %strong #{@project.name}
+ %strong= @project.name
%span.badge= group_links.size
%ul.content-list
= render partial: 'shared/members/group', collection: group_links, as: :group_link
diff --git a/app/views/projects/project_members/_index.html.haml b/app/views/projects/project_members/_index.html.haml
new file mode 100644
index 00000000000..ab0771b5751
--- /dev/null
+++ b/app/views/projects/project_members/_index.html.haml
@@ -0,0 +1,22 @@
+.row.prepend-top-default
+ .col-lg-3.settings-sidebar
+ %h4.prepend-top-0
+ Members
+ - if can?(current_user, :admin_project_member, @project)
+ %p
+ Add a new member to
+ %strong= @project.name
+ .col-lg-9
+ .light.prepend-top-default
+ - if can?(current_user, :admin_project_member, @project)
+ = render "projects/project_members/new_project_member"
+
+ = render 'shared/members/requests', membership_source: @project, requesters: @requesters
+ .append-bottom-default.clearfix
+ %h5.member.existing-title
+ Existing members and groups
+ - if @group_links.any?
+ = render 'projects/project_members/groups', group_links: @group_links
+
+ = render 'projects/project_members/team', members: @project_members
+ = paginate @project_members, theme: "gitlab"
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 79dcd7a6ee9..2b1c23f7dda 100644
--- a/app/views/projects/project_members/_new_project_member.html.haml
+++ b/app/views/projects/project_members/_new_project_member.html.haml
@@ -1,22 +1,18 @@
= form_for @project_member, as: :project_member, url: namespace_project_project_members_path(@project.namespace, @project), html: { class: 'users-project-form' } do |f|
- .row
- .col-md-4.col-lg-6
- = users_select_tag(:user_ids, multiple: true, class: "input-clamp", scope: :all, email_user: true)
- .help-block.append-bottom-10
- Search for users by name, username, or email, or invite new ones using their email address.
-
- .col-md-3.col-lg-2
- = select_tag :access_level, options_for_select(ProjectMember.access_level_roles, @project_member.access_level), class: "form-control project-access-select"
- .help-block.append-bottom-10
- = link_to "Read more", help_page_path("user/permissions"), class: "vlink"
- about role permissions
-
- .col-md-3.col-lg-2
- .clearable-input
- = text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Expiration date'
- %i.clear-icon.js-clear-input
- .help-block.append-bottom-10
- On this date, the user(s) will automatically lose access to this project.
-
- .col-md-2
- = f.submit "Add to project", class: "btn btn-create btn-block"
+ .form-group
+ = users_select_tag(:user_ids, multiple: true, class: "input-clamp", scope: :all, email_user: true, placeholder: "Search for members to update or invite")
+ .help-block.append-bottom-10
+ Search for members by name, username, or email, or invite new ones using their email address.
+ .form-group
+ = select_tag :access_level, options_for_select(ProjectMember.access_level_roles, @project_member.access_level), class: "form-control project-access-select"
+ .help-block.append-bottom-10
+ = link_to "Read more", help_page_path("user/permissions"), class: "vlink"
+ about role permissions
+ .form-group
+ .clearable-input
+ = text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Expiration date'
+ %i.clear-icon.js-clear-input
+ .help-block.append-bottom-10
+ On this date, the member(s) will automatically lose access to this project.
+ = f.submit "Add to project", class: "btn btn-create"
+ = link_to "Import", import_namespace_project_project_members_path(@project.namespace, @project), class: "btn btn-default", title: "Import members from another project"
diff --git a/app/views/projects/project_members/_shared_group_members.html.haml b/app/views/projects/project_members/_shared_group_members.html.haml
index 77370c14def..7902ddb1ae9 100644
--- a/app/views/projects/project_members/_shared_group_members.html.haml
+++ b/app/views/projects/project_members/_shared_group_members.html.haml
@@ -5,9 +5,9 @@
.panel.panel-default
.panel-heading
Shared with
- %strong #{shared_group.name}
+ %strong= shared_group.name
group, members with
- %strong #{group_links.human_access}
+ %strong= group_links.human_access
role (#{shared_group_users_count})
- if can?(current_user, :admin_group, shared_group)
.panel-head-actions
diff --git a/app/views/projects/project_members/_team.html.haml b/app/views/projects/project_members/_team.html.haml
index c1e894d8f40..81d57c77edf 100644
--- a/app/views/projects/project_members/_team.html.haml
+++ b/app/views/projects/project_members/_team.html.haml
@@ -1,7 +1,13 @@
.panel.panel-default
.panel-heading
- Users with access to
- %strong #{@project.name}
+ Members with access to
+ %strong= @project.name
%span.badge= @project_members.total_count
+ = form_tag namespace_project_settings_members_path(@project.namespace, @project), method: :get, class: 'form-inline member-search-form' do
+ .form-group
+ = search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false }
+ %button.member-search-btn{ type: "submit", "aria-label" => "Submit search" }
+ = icon("search")
+ = render 'shared/members/sort_dropdown'
%ul.content-list
= render partial: 'shared/members/member', collection: members, as: :member
diff --git a/app/views/projects/project_members/import.html.haml b/app/views/projects/project_members/import.html.haml
index eef97107d77..42ce4f8001b 100644
--- a/app/views/projects/project_members/import.html.haml
+++ b/app/views/projects/project_members/import.html.haml
@@ -12,5 +12,4 @@
.form-actions
= button_tag 'Import project members', class: "btn btn-create"
- = link_to "Cancel", namespace_project_project_members_path(@project.namespace, @project), class: "btn btn-cancel"
-
+ = link_to "Cancel", namespace_project_settings_members_path(@project.namespace, @project), class: "btn btn-cancel"
diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml
deleted file mode 100644
index 4f1cec20f85..00000000000
--- a/app/views/projects/project_members/index.html.haml
+++ /dev/null
@@ -1,29 +0,0 @@
-- page_title "Members"
-
-.project-members-page.prepend-top-default
- %h4.project-members-title.clearfix
- Members
- = link_to "Import", import_namespace_project_project_members_path(@project.namespace, @project), class: "btn btn-default pull-right hidden-xs", title: "Import members from another project"
- - if can?(current_user, :admin_project_member, @project)
- .project-members-new.append-bottom-default
- %p.clearfix
- Add new user to
- %strong= @project.name
- = render "new_project_member"
-
- = render 'shared/members/requests', membership_source: @project, requesters: @requesters
-
- .append-bottom-default.clearfix
- %h5.member.existing-title
- Existing users and groups
- = form_tag namespace_project_project_members_path(@project.namespace, @project), method: :get, class: 'form-inline member-search-form' do
- .form-group
- = search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false }
- %button.member-search-btn{ type: "submit", "aria-label" => "Submit search" }
- = icon("search")
- = render 'shared/members/sort_dropdown'
- - if @group_links.any?
- = render 'groups', group_links: @group_links
-
- = render 'team', members: @project_members
- = paginate @project_members, theme: "gitlab"
diff --git a/app/views/projects/protected_branches/index.html.haml b/app/views/projects/protected_branches/index.html.haml
index 42e9bdbd30e..b3b419bd92d 100644
--- a/app/views/projects/protected_branches/index.html.haml
+++ b/app/views/projects/protected_branches/index.html.haml
@@ -1,6 +1,6 @@
- page_title "Protected branches"
- content_for :page_specific_javascripts do
- = page_specific_javascript_tag('protected_branches/protected_branches_bundle.js')
+ = page_specific_javascript_bundle_tag('protected_branches')
.row.prepend-top-default.append-bottom-default
.col-lg-3
diff --git a/app/views/projects/releases/edit.html.haml b/app/views/projects/releases/edit.html.haml
index 33d5cbff420..79d8d721aa9 100644
--- a/app/views/projects/releases/edit.html.haml
+++ b/app/views/projects/releases/edit.html.haml
@@ -7,7 +7,7 @@
.oneline
.title
Release notes for tag
- %strong #{@tag.name}
+ %strong= @tag.name
= 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|
diff --git a/app/views/projects/runners/_form.html.haml b/app/views/projects/runners/_form.html.haml
index 33a9a96183c..98e72f6c547 100644
--- a/app/views/projects/runners/_form.html.haml
+++ b/app/views/projects/runners/_form.html.haml
@@ -5,7 +5,7 @@
.col-sm-10
.checkbox
= f.check_box :active
- %span.light Paused Runners don't accept new builds
+ %span.light Paused Runners don't accept new jobs
.form-group
= label :run_untagged, 'Run untagged jobs', class: 'control-label'
.col-sm-10
diff --git a/app/views/projects/runners/_index.html.haml b/app/views/projects/runners/_index.html.haml
new file mode 100644
index 00000000000..f9808f7c990
--- /dev/null
+++ b/app/views/projects/runners/_index.html.haml
@@ -0,0 +1,25 @@
+.light.prepend-top-default
+ %p
+ A 'Runner' is a process which runs a job.
+ You can setup as many Runners as you need.
+ %br
+ Runners can be placed on separate users, servers, and even on your local machine.
+
+ %p Each Runner can be in one of the following states:
+ %div
+ %ul
+ %li
+ %span.label.label-success active
+ \- Runner is active and can process any new jobs
+ %li
+ %span.label.label-danger paused
+ \- Runner is paused and will not receive any new jobs
+
+%hr
+
+%p.lead To start serving your jobs you can either add specific Runners to your project or use shared Runners
+.row
+ .col-sm-6
+ = render 'projects/runners/specific_runners'
+ .col-sm-6
+ = render 'projects/runners/shared_runners'
diff --git a/app/views/projects/runners/_runner.html.haml b/app/views/projects/runners/_runner.html.haml
index 6e58e5a0c78..7036b8a5ccc 100644
--- a/app/views/projects/runners/_runner.html.haml
+++ b/app/views/projects/runners/_runner.html.haml
@@ -1,4 +1,4 @@
-%li.runner{id: dom_id(runner)}
+%li.runner{ id: dom_id(runner) }
%h4
= runner_status_icon(runner)
%span.monospace
diff --git a/app/views/projects/runners/_shared_runners.html.haml b/app/views/projects/runners/_shared_runners.html.haml
index 5afa193357e..0671dd66e78 100644
--- a/app/views/projects/runners/_shared_runners.html.haml
+++ b/app/views/projects/runners/_shared_runners.html.haml
@@ -22,7 +22,7 @@
- else
%h4.underlined-title Available shared Runners : #{@shared_runners_count}
%ul.bordered-list.available-shared-runners
- = render partial: 'runner', collection: @shared_runners, as: :runner
+ = render partial: 'projects/runners/runner', collection: @shared_runners, as: :runner
- if @shared_runners_count > 10
.light
and #{@shared_runners_count - 10} more...
diff --git a/app/views/projects/runners/_specific_runners.html.haml b/app/views/projects/runners/_specific_runners.html.haml
index 51b0939564e..6b8e6bd4fee 100644
--- a/app/views/projects/runners/_specific_runners.html.haml
+++ b/app/views/projects/runners/_specific_runners.html.haml
@@ -9,10 +9,10 @@
(checkout the #{link_to 'GitLab Runner section', 'https://about.gitlab.com/gitlab-ci/#gitlab-runner', target: '_blank'} for information on how to install it).
%li
Specify the following URL during the Runner setup:
- %code #{ci_root_url(only_path: false)}
+ %code= ci_root_url(only_path: false)
%li
Use the following registration token during setup:
- %code #{@project.runners_token}
+ %code= @project.runners_token
%li
Start the Runner!
@@ -20,10 +20,10 @@
- if @project_runners.any?
%h4.underlined-title Runners activated for this project
%ul.bordered-list.activated-specific-runners
- = render partial: 'runner', collection: @project_runners, as: :runner
+ = render partial: 'projects/runners/runner', collection: @project_runners, as: :runner
- if @assignable_runners.any?
%h4.underlined-title Available specific runners
%ul.bordered-list.available-specific-runners
- = render partial: 'runner', collection: @assignable_runners, as: :runner
+ = render partial: 'projects/runners/runner', collection: @assignable_runners, as: :runner
= paginate @assignable_runners, theme: "gitlab"
diff --git a/app/views/projects/runners/index.html.haml b/app/views/projects/runners/index.html.haml
deleted file mode 100644
index 92957470070..00000000000
--- a/app/views/projects/runners/index.html.haml
+++ /dev/null
@@ -1,27 +0,0 @@
-- page_title "Runners"
-
-.light.prepend-top-default
- %p
- A 'Runner' is a process which runs a build.
- You can setup as many Runners as you need.
- %br
- Runners can be placed on separate users, servers, and even on your local machine.
-
- %p Each Runner can be in one of the following states:
- %div
- %ul
- %li
- %span.label.label-success active
- \- Runner is active and can process any new builds
- %li
- %span.label.label-danger paused
- \- Runner is paused and will not receive any new builds
-
-%hr
-
-%p.lead To start serving your builds you can either add specific Runners to your project or use shared Runners
-.row
- .col-sm-6
- = render 'specific_runners'
- .col-sm-6
- = render 'shared_runners'
diff --git a/app/views/projects/services/_form.html.haml b/app/views/projects/services/_form.html.haml
index db51c4f8a4e..f1a80f1d5e1 100644
--- a/app/views/projects/services/_form.html.haml
+++ b/app/views/projects/services/_form.html.haml
@@ -8,7 +8,6 @@
.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, subject: @service
-
.footer-block.row-content-block
= form.submit 'Save changes', class: 'btn btn-save'
&nbsp;
@@ -18,4 +17,4 @@
- disabled_title = @service.disabled_title
= link_to 'Test settings', test_namespace_project_service_path(@project.namespace, @project, @service), class: "btn #{disabled_class}", title: disabled_title
- = link_to "Cancel", namespace_project_services_path(@project.namespace, @project), class: "btn btn-cancel"
+ = link_to "Cancel", namespace_project_settings_integrations_path(@project.namespace, @project), class: "btn btn-cancel"
diff --git a/app/views/projects/services/_index.html.haml b/app/views/projects/services/_index.html.haml
new file mode 100644
index 00000000000..964133504e6
--- /dev/null
+++ b/app/views/projects/services/_index.html.haml
@@ -0,0 +1,31 @@
+.row.prepend-top-default.append-bottom-default
+ .col-lg-3
+ %h4.prepend-top-0
+ Project services
+ %p Project services allow you to integrate GitLab with other applications
+ .col-lg-9
+ %table.table
+ %colgroup
+ %col
+ %col
+ %col.hidden-xs
+ %col{ width: "120" }
+ %thead
+ %tr
+ %th
+ %th Service
+ %th.hidden-xs Description
+ %th Last edit
+ - @services.sort_by(&:title).each do |service|
+ %tr
+ %td
+ = boolean_to_icon service.activated?
+ %td
+ = link_to edit_namespace_project_service_path(@project.namespace, @project, service.to_param) do
+ %strong= service.title
+ %td.hidden-xs
+ = service.description
+ %td.light
+ - if service.updated_at.present?
+ = time_ago_in_words service.updated_at
+ ago
diff --git a/app/views/projects/services/index.html.haml b/app/views/projects/services/index.html.haml
deleted file mode 100644
index 66fd3029dc9..00000000000
--- a/app/views/projects/services/index.html.haml
+++ /dev/null
@@ -1,33 +0,0 @@
-- page_title "Services"
-
-.row.prepend-top-default.append-bottom-default
- .col-lg-3
- %h4.prepend-top-0
- Project services
- %p Project services allow you to integrate GitLab with other applications
- .col-lg-9
- %table.table
- %colgroup
- %col
- %col
- %col.hidden-xs
- %col{ width: "120" }
- %thead
- %tr
- %th
- %th Service
- %th.hidden-xs Description
- %th Last edit
- - @services.sort_by(&:title).each do |service|
- %tr
- %td
- = boolean_to_icon service.activated?
- %td
- = link_to edit_namespace_project_service_path(@project.namespace, @project, service.to_param) do
- %strong= service.title
- %td.hidden-xs
- = service.description
- %td.light
- - if service.updated_at.present?
- = time_ago_in_words service.updated_at
- ago
diff --git a/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml b/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml
new file mode 100644
index 00000000000..3a323d94cc2
--- /dev/null
+++ b/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml
@@ -0,0 +1,99 @@
+- run_actions_text = "Perform common operations on GitLab project: #{@project.name_with_namespace}"
+
+%p To setup this service:
+%ul.list-unstyled.indent-list
+ %li
+ 1.
+ = link_to 'https://docs.mattermost.com/developer/slash-commands.html#enabling-custom-commands', target: '_blank', rel: 'noreferrer noopener nofollow' do
+ Enable custom slash commands
+ = icon('external-link')
+ on your Mattermost installation
+ %li
+ 2.
+ = link_to 'https://docs.mattermost.com/developer/slash-commands.html#set-up-a-custom-command', target: '_blank', rel: 'noreferrer noopener nofollow' do
+ Add a slash command
+ = icon('external-link')
+ in your Mattermost team with these options:
+%hr
+
+.help-form
+ .form-group
+ = label_tag :display_name, 'Display name', class: 'col-sm-2 col-xs-12 control-label'
+ .col-sm-10.col-xs-12.input-group
+ = text_field_tag :display_name, "GitLab / #{@project.name_with_namespace}", class: 'form-control input-sm', readonly: 'readonly'
+ .input-group-btn
+ = clipboard_button(clipboard_target: '#display_name')
+
+ .form-group
+ = label_tag :description, 'Description', class: 'col-sm-2 col-xs-12 control-label'
+ .col-sm-10.col-xs-12.input-group
+ = text_field_tag :description, run_actions_text, class: 'form-control input-sm', readonly: 'readonly'
+ .input-group-btn
+ = clipboard_button(clipboard_target: '#description')
+
+ .form-group
+ = label_tag nil, 'Command trigger word', class: 'col-sm-2 col-xs-12 control-label'
+ .col-sm-10.col-xs-12.text-block
+ %p Fill in the word that works best for your team.
+ %p
+ Suggestions:
+ %code= 'gitlab'
+ %code= @project.path # Path contains no spaces, but dashes
+ %code= @project.path_with_namespace
+
+ .form-group
+ = label_tag :request_url, 'Request URL', class: 'col-sm-2 col-xs-12 control-label'
+ .col-sm-10.col-xs-12.input-group
+ = text_field_tag :request_url, service_trigger_url(subject), class: 'form-control input-sm', readonly: 'readonly'
+ .input-group-btn
+ = clipboard_button(clipboard_target: '#request_url')
+
+ .form-group
+ = label_tag nil, 'Request method', class: 'col-sm-2 col-xs-12 control-label'
+ .col-sm-10.col-xs-12.text-block POST
+
+ .form-group
+ = label_tag :response_username, 'Response username', class: 'col-sm-2 col-xs-12 control-label'
+ .col-sm-10.col-xs-12.input-group
+ = text_field_tag :response_username, 'GitLab', class: 'form-control input-sm', readonly: 'readonly'
+ .input-group-btn
+ = clipboard_button(clipboard_target: '#response_username')
+
+ .form-group
+ = label_tag :response_icon, 'Response icon', class: 'col-sm-2 col-xs-12 control-label'
+ .col-sm-10.col-xs-12.input-group
+ = text_field_tag :response_icon, asset_url('gitlab_logo.png'), class: 'form-control input-sm', readonly: 'readonly'
+ .input-group-btn
+ = clipboard_button(clipboard_target: '#response_icon')
+
+ .form-group
+ = label_tag nil, 'Autocomplete', class: 'col-sm-2 col-xs-12 control-label'
+ .col-sm-10.col-xs-12.text-block Yes
+
+ .form-group
+ = label_tag :autocomplete_hint, 'Autocomplete hint', class: 'col-sm-2 col-xs-12 control-label'
+ .col-sm-10.col-xs-12.input-group
+ = text_field_tag :autocomplete_hint, '[help]', class: 'form-control input-sm', readonly: 'readonly'
+ .input-group-btn
+ = clipboard_button(clipboard_target: '#autocomplete_hint')
+
+ .form-group
+ = label_tag :autocomplete_description, 'Autocomplete description', class: 'col-sm-2 col-xs-12 control-label'
+ .col-sm-10.col-xs-12.input-group
+ = text_field_tag :autocomplete_description, run_actions_text, class: 'form-control input-sm', readonly: 'readonly'
+ .input-group-btn
+ = clipboard_button(clipboard_target: '#autocomplete_description')
+
+%hr
+
+%ul.list-unstyled.indent-list
+ %li
+ 3. Paste the
+ %strong Token
+ into the field below
+ %li
+ 4. Select the
+ %strong Active
+ checkbox, press
+ %strong Save changes
+ and start using GitLab inside Mattermost!
diff --git a/app/views/projects/services/mattermost_slash_commands/_help.html.haml b/app/views/projects/services/mattermost_slash_commands/_help.html.haml
index 01a77a952d1..a04fd5035a6 100644
--- a/app/views/projects/services/mattermost_slash_commands/_help.html.haml
+++ b/app/views/projects/services/mattermost_slash_commands/_help.html.haml
@@ -1,99 +1,18 @@
-- run_actions_text = "Perform common operations on this project: #{@project.name_with_namespace}"
+- enabled = Gitlab.config.mattermost.enabled
.well
- This service allows GitLab users to perform common operations on this
- project by entering slash commands in Mattermost.
- %br
- See list of available commands in Mattermost after setting up this service,
- by entering
- %code /&lt;command_trigger_word&gt; help
- %br
- %br
- To setup this service:
- %ul.list-unstyled
- %li
- 1.
- = link_to 'Enable custom slash commands', 'https://docs.mattermost.com/developer/slash-commands.html#enabling-custom-commands'
- on your Mattermost installation
- %li
- 2.
- = link_to 'Add a slash command', 'https://docs.mattermost.com/developer/slash-commands.html#set-up-a-custom-command'
- in Mattermost with these options:
-
- %hr
-
- .help-form
- .form-group
- = label_tag :display_name, 'Display name', class: 'col-sm-2 col-xs-12 control-label'
- .col-sm-10.col-xs-12.input-group
- = text_field_tag :display_name, "GitLab / #{@project.name_with_namespace}", class: 'form-control input-sm', readonly: 'readonly'
- .input-group-btn
- = clipboard_button(clipboard_target: '#display_name')
-
- .form-group
- = label_tag :description, 'Description', class: 'col-sm-2 col-xs-12 control-label'
- .col-sm-10.col-xs-12.input-group
- = text_field_tag :description, run_actions_text, class: 'form-control input-sm', readonly: 'readonly'
- .input-group-btn
- = clipboard_button(clipboard_target: '#description')
-
- .form-group
- = label_tag nil, 'Command trigger word', class: 'col-sm-2 col-xs-12 control-label'
- .col-sm-10.col-xs-12.text-block
- %p Fill in the word that works best for your team.
- %p
- Suggestions:
- %code= 'gitlab'
- %code= @project.path # Path contains no spaces, but dashes
- %code= @project.path_with_namespace
-
- .form-group
- = label_tag :request_url, 'Request URL', class: 'col-sm-2 col-xs-12 control-label'
- .col-sm-10.col-xs-12.input-group
- = text_field_tag :request_url, service_trigger_url(subject), class: 'form-control input-sm', readonly: 'readonly'
- .input-group-btn
- = clipboard_button(clipboard_target: '#request_url')
-
- .form-group
- = label_tag nil, 'Request method', class: 'col-sm-2 col-xs-12 control-label'
- .col-sm-10.col-xs-12.text-block POST
-
- .form-group
- = label_tag :response_username, 'Response username', class: 'col-sm-2 col-xs-12 control-label'
- .col-sm-10.col-xs-12.input-group
- = text_field_tag :response_username, 'GitLab', class: 'form-control input-sm', readonly: 'readonly'
- .input-group-btn
- = clipboard_button(clipboard_target: '#response_username')
-
- .form-group
- = label_tag :response_icon, 'Response icon', class: 'col-sm-2 col-xs-12 control-label'
- .col-sm-10.col-xs-12.input-group
- = text_field_tag :response_icon, asset_url('gitlab_logo.png'), class: 'form-control input-sm', readonly: 'readonly'
- .input-group-btn
- = clipboard_button(clipboard_target: '#response_icon')
-
- .form-group
- = label_tag nil, 'Autocomplete', class: 'col-sm-2 col-xs-12 control-label'
- .col-sm-10.col-xs-12.text-block Yes
-
- .form-group
- = label_tag :autocomplete_hint, 'Autocomplete hint', class: 'col-sm-2 col-xs-12 control-label'
- .col-sm-10.col-xs-12.input-group
- = text_field_tag :autocomplete_hint, '[help]', class: 'form-control input-sm', readonly: 'readonly'
- .input-group-btn
- = clipboard_button(clipboard_target: '#autocomplete_hint')
-
- .form-group
- = label_tag :autocomplete_description, 'Autocomplete description', class: 'col-sm-2 col-xs-12 control-label'
- .col-sm-10.col-xs-12.input-group
- = text_field_tag :autocomplete_description, run_actions_text, class: 'form-control input-sm', readonly: 'readonly'
- .input-group-btn
- = clipboard_button(clipboard_target: '#autocomplete_description')
-
- %hr
-
- %ul.list-unstyled
- %li
- 3. After adding the slash command, paste the
- %strong token
- into the field below
+ %p
+ This service allows users to perform common operations on this
+ project by entering slash commands in Mattermost.
+ = link_to help_page_path('user/project/integrations/mattermost_slash_commands.md'), target: '_blank', ref: 'noreferrer nofollow noopener' do
+ View documentation
+ = icon('external-link')
+ %p.inline
+ See list of available commands in Mattermost after setting up this service,
+ by entering
+ %kbd.inline /&lt;trigger&gt; help
+ - unless enabled || @service.template?
+ = render 'projects/services/mattermost_slash_commands/detailed_help', subject: @service
+
+- if enabled && !@service.template?
+ = render 'projects/services/mattermost_slash_commands/installation_info', subject: @service
diff --git a/app/views/projects/services/mattermost_slash_commands/_installation_info.html.haml b/app/views/projects/services/mattermost_slash_commands/_installation_info.html.haml
new file mode 100644
index 00000000000..fcc91be11cd
--- /dev/null
+++ b/app/views/projects/services/mattermost_slash_commands/_installation_info.html.haml
@@ -0,0 +1,7 @@
+.services-installation-info
+ - unless @service.activated?
+ .row
+ .col-sm-9.col-sm-offset-3
+ = link_to new_namespace_project_mattermost_path(@project.namespace, @project), class: 'btn btn-lg' do
+ = custom_icon('mattermost_logo', size: 15)
+ Add to Mattermost
diff --git a/app/views/projects/services/slack_slash_commands/_help.html.haml b/app/views/projects/services/slack_slash_commands/_help.html.haml
index c45052e3954..0d973a20d4c 100644
--- a/app/views/projects/services/slack_slash_commands/_help.html.haml
+++ b/app/views/projects/services/slack_slash_commands/_help.html.haml
@@ -1,93 +1,99 @@
-- run_actions_text = "Perform common operations on this project: #{@project.name_with_namespace}"
+- pretty_name = defined?(@project) ? @project.name_with_namespace : 'namespace / path'
+- run_actions_text = "Perform common operations on GitLab project: #{pretty_name}"
.well
- This service allows GitLab users to perform common operations on this
- project by entering slash commands in Slack.
- %br
- See list of available commands in Slack after setting up this service,
- by entering
- %code /&lt;command&gt; help
- %br
- %br
- To setup this service:
- %ul.list-unstyled
- %li
- 1.
- = link_to 'Add a slash command', 'https://my.slack.com/services/new/slash-commands'
- in your Slack team with these options:
+ %p
+ This service allows users to perform common operations on this
+ project by entering slash commands in Slack.
+ = link_to help_page_path('user/project/integrations/slack_slash_commands.md'), target: '_blank', ref: 'noreferrer nofollow noopener' do
+ View documentation
+ = icon('external-link')
+ %p.inline
+ See list of available commands in Slack after setting up this service,
+ by entering
+ %kbd.inline /&lt;command&gt; help
+ - unless @service.template?
+ %p To setup this service:
+ %ul.list-unstyled.indent-list
+ %li
+ 1.
+ = link_to 'https://my.slack.com/services/new/slash-commands', target: '_blank', rel: 'noreferrer noopener nofollow' do
+ Add a slash command
+ = icon('external-link')
+ in your Slack team with these options:
- %hr
+ %hr
- .help-form
- .form-group
- = label_tag nil, 'Command', class: 'col-sm-2 col-xs-12 control-label'
- .col-sm-10.col-xs-12.text-block
- %p Fill in the word that works best for your team.
- %p
- Suggestions:
- %code= 'gitlab'
- %code= @project.path # Path contains no spaces, but dashes
- %code= @project.path_with_namespace
+ .help-form
+ .form-group
+ = label_tag nil, 'Command', class: 'col-sm-2 col-xs-12 control-label'
+ .col-sm-10.col-xs-12.text-block
+ %p Fill in the word that works best for your team.
+ %p
+ Suggestions:
+ %code= 'gitlab'
+ %code= @project.path # Path contains no spaces, but dashes
+ %code= @project.path_with_namespace
- .form-group
- = label_tag :url, 'URL', class: 'col-sm-2 col-xs-12 control-label'
- .col-sm-10.col-xs-12.input-group
- = text_field_tag :url, service_trigger_url(subject), class: 'form-control input-sm', readonly: 'readonly'
- .input-group-btn
- = clipboard_button(clipboard_target: '#url')
+ .form-group
+ = label_tag :url, 'URL', class: 'col-sm-2 col-xs-12 control-label'
+ .col-sm-10.col-xs-12.input-group
+ = text_field_tag :url, service_trigger_url(subject), class: 'form-control input-sm', readonly: 'readonly'
+ .input-group-btn
+ = clipboard_button(clipboard_target: '#url')
- .form-group
- = label_tag nil, 'Method', class: 'col-sm-2 col-xs-12 control-label'
- .col-sm-10.col-xs-12.text-block POST
+ .form-group
+ = label_tag nil, 'Method', class: 'col-sm-2 col-xs-12 control-label'
+ .col-sm-10.col-xs-12.text-block POST
- .form-group
- = label_tag :customize_name, 'Customize name', class: 'col-sm-2 col-xs-12 control-label'
- .col-sm-10.col-xs-12.input-group
- = text_field_tag :customize_name, 'GitLab', class: 'form-control input-sm', readonly: 'readonly'
- .input-group-btn
- = clipboard_button(clipboard_target: '#customize_name')
+ .form-group
+ = label_tag :customize_name, 'Customize name', class: 'col-sm-2 col-xs-12 control-label'
+ .col-sm-10.col-xs-12.input-group
+ = text_field_tag :customize_name, 'GitLab', class: 'form-control input-sm', readonly: 'readonly'
+ .input-group-btn
+ = clipboard_button(clipboard_target: '#customize_name')
- .form-group
- = label_tag nil, 'Customize icon', class: 'col-sm-2 col-xs-12 control-label'
- .col-sm-10.col-xs-12.text-block
- = image_tag(asset_url('gitlab_logo.png'), width: 36, height: 36)
- = link_to('Download image', asset_url('gitlab_logo.png'), class: 'btn btn-sm', target: '_blank')
+ .form-group
+ = label_tag nil, 'Customize icon', class: 'col-sm-2 col-xs-12 control-label'
+ .col-sm-10.col-xs-12.text-block
+ = image_tag(asset_url('slash-command-logo.png'), width: 36, height: 36)
+ = link_to('Download image', asset_url('gitlab_logo.png'), class: 'btn btn-sm', target: '_blank')
- .form-group
- = label_tag nil, 'Autocomplete', class: 'col-sm-2 col-xs-12 control-label'
- .col-sm-10.col-xs-12.text-block Show this command in the autocomplete list
+ .form-group
+ = label_tag nil, 'Autocomplete', class: 'col-sm-2 col-xs-12 control-label'
+ .col-sm-10.col-xs-12.text-block Show this command in the autocomplete list
- .form-group
- = label_tag :autocomplete_description, 'Autocomplete description', class: 'col-sm-2 col-xs-12 control-label'
- .col-sm-10.col-xs-12.input-group
- = text_field_tag :autocomplete_description, run_actions_text, class: 'form-control input-sm', readonly: 'readonly'
- .input-group-btn
- = clipboard_button(clipboard_target: '#autocomplete_description')
+ .form-group
+ = label_tag :autocomplete_description, 'Autocomplete description', class: 'col-sm-2 col-xs-12 control-label'
+ .col-sm-10.col-xs-12.input-group
+ = text_field_tag :autocomplete_description, run_actions_text, class: 'form-control input-sm', readonly: 'readonly'
+ .input-group-btn
+ = clipboard_button(clipboard_target: '#autocomplete_description')
- .form-group
- = label_tag :autocomplete_usage_hint, 'Autocomplete usage hint', class: 'col-sm-2 col-xs-12 control-label'
- .col-sm-10.col-xs-12.input-group
- = text_field_tag :autocomplete_usage_hint, '[help]', class: 'form-control input-sm', readonly: 'readonly'
- .input-group-btn
- = clipboard_button(clipboard_target: '#autocomplete_usage_hint')
+ .form-group
+ = label_tag :autocomplete_usage_hint, 'Autocomplete usage hint', class: 'col-sm-2 col-xs-12 control-label'
+ .col-sm-10.col-xs-12.input-group
+ = text_field_tag :autocomplete_usage_hint, '[help]', class: 'form-control input-sm', readonly: 'readonly'
+ .input-group-btn
+ = clipboard_button(clipboard_target: '#autocomplete_usage_hint')
- .form-group
- = label_tag :descriptive_label, 'Descriptive label', class: 'col-sm-2 col-xs-12 control-label'
- .col-sm-10.col-xs-12.input-group
- = text_field_tag :descriptive_label, 'Perform common operations on GitLab project', class: 'form-control input-sm', readonly: 'readonly'
- .input-group-btn
- = clipboard_button(clipboard_target: '#descriptive_label')
+ .form-group
+ = label_tag :descriptive_label, 'Descriptive label', class: 'col-sm-2 col-xs-12 control-label'
+ .col-sm-10.col-xs-12.input-group
+ = text_field_tag :descriptive_label, 'Perform common operations on GitLab project', class: 'form-control input-sm', readonly: 'readonly'
+ .input-group-btn
+ = clipboard_button(clipboard_target: '#descriptive_label')
- %hr
+ %hr
- %ul.list-unstyled
- %li
- 2. Paste the
- %strong Token
- into the field below
- %li
- 3. Select the
- %strong Active
- checkbox, press
- %strong Save changes
- and start using GitLab inside Slack!
+ %ul.list-unstyled.indent-list
+ %li
+ 2. Paste the
+ %strong Token
+ into the field below
+ %li
+ 3. Select the
+ %strong Active
+ checkbox, press
+ %strong Save changes
+ and start using GitLab inside Slack!
diff --git a/app/views/projects/settings/ci_cd/show.html.haml b/app/views/projects/settings/ci_cd/show.html.haml
new file mode 100644
index 00000000000..52f5f7b81e2
--- /dev/null
+++ b/app/views/projects/settings/ci_cd/show.html.haml
@@ -0,0 +1,6 @@
+- page_title "CI/CD Pipelines"
+
+= render 'projects/runners/index'
+= render 'projects/variables/index'
+= render 'projects/triggers/index'
+= render 'projects/pipelines_settings/show'
diff --git a/app/views/projects/hooks/_project_hook.html.haml b/app/views/projects/settings/integrations/_project_hook.html.haml
index ceabe2eab3d..ceabe2eab3d 100644
--- a/app/views/projects/hooks/_project_hook.html.haml
+++ b/app/views/projects/settings/integrations/_project_hook.html.haml
diff --git a/app/views/projects/settings/integrations/show.html.haml b/app/views/projects/settings/integrations/show.html.haml
new file mode 100644
index 00000000000..aa38a889cdd
--- /dev/null
+++ b/app/views/projects/settings/integrations/show.html.haml
@@ -0,0 +1,3 @@
+- page_title 'Integrations'
+= render 'projects/hooks/index'
+= render 'projects/services/index'
diff --git a/app/views/projects/settings/members/show.html.haml b/app/views/projects/settings/members/show.html.haml
new file mode 100644
index 00000000000..d81ed7bb609
--- /dev/null
+++ b/app/views/projects/settings/members/show.html.haml
@@ -0,0 +1,6 @@
+- page_title "Members"
+
+= render "projects/project_members/index"
+- if can?(current_user, :admin_project, @project)
+ - if @project.allowed_to_share_with_group?
+ = render "projects/group_links/index"
diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml
index 8a214e1de58..80d4081dd7b 100644
--- a/app/views/projects/show.html.haml
+++ b/app/views/projects/show.html.haml
@@ -17,10 +17,10 @@
%ul.nav
%li
= link_to project_files_path(@project) do
- Files (#{repository_size})
+ Files (#{storage_counter(@project.statistics.total_repository_size)})
%li
= link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do
- #{'Commit'.pluralize(@project.commit_count)} (#{number_with_delimiter(@project.commit_count)})
+ #{'Commit'.pluralize(@project.statistics.commit_count)} (#{number_with_delimiter(@project.statistics.commit_count)})
%li
= link_to namespace_project_branches_path(@project.namespace, @project) do
#{'Branch'.pluralize(@repository.branch_count)} (#{number_with_delimiter(@repository.branch_count)})
@@ -68,6 +68,10 @@
- if koding_enabled? && @repository.koding_yml.blank?
%li.missing
= link_to 'Set up Koding', add_koding_stack_path(@project)
+ - if @repository.gitlab_ci_yml.blank? && @project.deployment_service.present?
+ %li.missing
+ = link_to add_special_file_path(@project, file_name: '.gitlab-ci.yml', commit_message: 'Set up auto deploy', target_branch: 'auto-deploy', context: 'autodeploy') do
+ Set up auto deploy
- if @repository.commit
.project-last-commit{ class: container_class }
diff --git a/app/views/projects/snippets/_actions.html.haml b/app/views/projects/snippets/_actions.html.haml
index 068a6610350..dde2e2b644d 100644
--- a/app/views/projects/snippets/_actions.html.haml
+++ b/app/views/projects/snippets/_actions.html.haml
@@ -1,3 +1,5 @@
+- return unless current_user
+
.hidden-xs
- if can?(current_user, :update_project_snippet, @snippet)
= link_to edit_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-grouped" do
@@ -8,6 +10,8 @@
- if can?(current_user, :create_project_snippet, @project)
= link_to new_namespace_project_snippet_path(@project.namespace, @project), class: 'btn btn-grouped btn-inverted btn-create', title: "New snippet" do
New snippet
+ - if @snippet.submittable_as_spam? && current_user.admin?
+ = link_to 'Submit as spam', mark_as_spam_namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :post, class: 'btn btn-grouped btn-spam', title: 'Submit as spam'
- if can?(current_user, :create_project_snippet, @project) || can?(current_user, :update_project_snippet, @snippet)
.visible-xs-block.dropdown
%button.btn.btn-default.btn-block.append-bottom-0.prepend-top-5{ data: { toggle: "dropdown" } }
@@ -27,3 +31,6 @@
%li
= link_to edit_namespace_project_snippet_path(@project.namespace, @project, @snippet) do
Edit
+ - if @snippet.submittable_as_spam? && current_user.admin?
+ %li
+ = link_to 'Submit as spam', mark_as_spam_namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :post
diff --git a/app/views/projects/snippets/edit.html.haml b/app/views/projects/snippets/edit.html.haml
index 216f70f5605..fb39028529d 100644
--- a/app/views/projects/snippets/edit.html.haml
+++ b/app/views/projects/snippets/edit.html.haml
@@ -3,4 +3,4 @@
%h3.page-title
Edit Snippet
%hr
-= render "shared/snippets/form", url: namespace_project_snippet_path(@project.namespace, @project, @snippet), visibility_level: @snippet.visibility_level
+= render "shared/snippets/form", url: namespace_project_snippet_path(@project.namespace, @project, @snippet)
diff --git a/app/views/projects/snippets/new.html.haml b/app/views/projects/snippets/new.html.haml
index 772a594269c..cfed3a79bc5 100644
--- a/app/views/projects/snippets/new.html.haml
+++ b/app/views/projects/snippets/new.html.haml
@@ -3,4 +3,4 @@
%h3.page-title
New Snippet
%hr
-= render "shared/snippets/form", url: namespace_project_snippets_path(@project.namespace, @project, @snippet), visibility_level: default_snippet_visibility
+= render "shared/snippets/form", url: namespace_project_snippets_path(@project.namespace, @project, @snippet)
diff --git a/app/views/projects/snippets/show.html.haml b/app/views/projects/snippets/show.html.haml
index 79d87b7db12..485b23815bc 100644
--- a/app/views/projects/snippets/show.html.haml
+++ b/app/views/projects/snippets/show.html.haml
@@ -15,4 +15,4 @@
.row-content-block.top-block.content-component-block
= render 'award_emoji/awards_block', awardable: @snippet, inline: true
- %div#notes= render "projects/notes/notes_with_form"
+ #notes= render "projects/notes/notes_with_form"
diff --git a/app/views/projects/stage/_graph.html.haml b/app/views/projects/stage/_graph.html.haml
index 6919b210a00..4ee30b023ac 100644
--- a/app/views/projects/stage/_graph.html.haml
+++ b/app/views/projects/stage/_graph.html.haml
@@ -1,6 +1,6 @@
- stage = local_assigns.fetch(:stage)
- statuses = stage.statuses.latest
-- status_groups = statuses.sort_by(&:name).group_by(&:group_name)
+- status_groups = statuses.sort_by(&:sortable_name).group_by(&:group_name)
%li.stage-column
.stage-name
%a{ name: stage.name }
@@ -10,11 +10,10 @@
- status_groups.each do |group_name, grouped_statuses|
- if grouped_statuses.one?
- status = grouped_statuses.first
- %li.build{ 'id' => "ci-badge-#{group_name}" }
+ %li.build{ 'id' => "ci-badge-#{group_name}" }
.curve
= render 'ci/status/graph_badge', subject: status
- else
- %li.build{ 'id' => "ci-badge-#{group_name}" }
+ %li.build{ 'id' => "ci-badge-#{group_name}" }
.curve
= render 'projects/stage/in_stage_group', name: group_name, subject: grouped_statuses
-
diff --git a/app/views/projects/stage/_in_stage_group.html.haml b/app/views/projects/stage/_in_stage_group.html.haml
index b15f7eaeab2..9c5eb501174 100644
--- a/app/views/projects/stage/_in_stage_group.html.haml
+++ b/app/views/projects/stage/_in_stage_group.html.haml
@@ -1,13 +1,14 @@
- group_status = CommitStatus.where(id: subject).status
-%button.dropdown-menu-toggle.build-content.has-tooltip{ type: 'button', data: { toggle: 'dropdown'} }
- %span{class: "ci-status-icon ci-status-icon-#{group_status}"}
+%button.dropdown-menu-toggle.build-content.has-tooltip{ type: 'button', data: { toggle: 'dropdown', title: "#{name} - #{group_status}" } }
+ %span{ class: "ci-status-icon ci-status-icon-#{group_status}" }
= ci_icon_for_status(group_status)
- %span.ci-status-text{ 'data-toggle' => 'tooltip', 'data-title' => "#{name} - #{group_status}" }
+ %span.ci-status-text
= name
%span.dropdown-counter-badge= subject.size
-.dropdown-menu.grouped-pipeline-dropdown
+
+%ul.dropdown-menu.big-pipeline-graph-dropdown-menu.js-grouped-pipeline-dropdown
.arrow
- %ul
+ .scrollable-menu
- subject.each do |status|
- %li.dropdown-build
- = render 'ci/status/graph_badge', subject: status
+ %li
+ = render 'ci/status/dropdown_graph_badge', subject: status
diff --git a/app/views/projects/stage/_stage.html.haml b/app/views/projects/stage/_stage.html.haml
index 1684e02fbad..28e1c060875 100644
--- a/app/views/projects/stage/_stage.html.haml
+++ b/app/views/projects/stage/_stage.html.haml
@@ -1,13 +1,13 @@
%tr
- %th{colspan: 10}
+ %th{ colspan: 10 }
%strong
%a{ name: stage.name }
- %span{class: "ci-status-link ci-status-icon-#{stage.status}"}
+ %span{ class: "ci-status-link ci-status-icon-#{stage.status}" }
= ci_icon_for_status(stage.status)
&nbsp;
= stage.name.titleize
= render stage.statuses.latest_ordered, coverage: @project.build_coverage_enabled?, stage: false, ref: false, pipeline_link: false, allow_retry: true
= render stage.statuses.retried_ordered, coverage: @project.build_coverage_enabled?, stage: false, ref: false, pipeline_link: false, retried: true
%tr
- %td{colspan: 10}
+ %td{ colspan: 10 }
&nbsp;
diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml
index c42641afea0..8ef069b9e05 100644
--- a/app/views/projects/tags/_tag.html.haml
+++ b/app/views/projects/tags/_tag.html.haml
@@ -1,7 +1,7 @@
- commit = @repository.commit(tag.dereferenced_target)
- release = @releases.find { |release| release.tag == tag.name }
-%li
- %div
+%li.flex-row
+ .row-main-content.str-truncated
= link_to namespace_project_tag_path(@project.namespace, @project, tag.name) do
%span.item-title
= icon('tag')
@@ -10,24 +10,25 @@
&nbsp;
= strip_gpg_signature(tag.message)
- .controls
- = render 'projects/buttons/download', project: @project, ref: tag.name
+ - if commit
+ .block-truncated
+ = render 'projects/branches/commit', commit: commit, project: @project
+ - else
+ %p
+ Cant find HEAD commit for this tag
+ - if release && release.description.present?
+ .description.prepend-top-default
+ .wiki
+ = preserve do
+ = markdown_field(release, :description)
- - 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", data: { container: "body" } do
- = icon("pencil")
+ .row-fixed-content.controls
+ = render 'projects/buttons/download', project: @project, ref: tag.name
- - if can?(current_user, :admin_project, @project)
- = link_to namespace_project_tag_path(@project.namespace, @project, tag.name), class: 'btn btn-remove remove-row has-tooltip', title: "Delete tag", method: :delete, data: { confirm: "Deleting the '#{tag.name}' tag cannot be undone. Are you sure?", container: 'body' }, remote: true do
- = icon("trash-o")
+ - 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", data: { container: "body" } do
+ = icon("pencil")
- - if commit
- = render 'projects/branches/commit', commit: commit, project: @project
- - else
- %p
- Cant find HEAD commit for this tag
- - if release && release.description.present?
- .description.prepend-top-default
- .wiki
- = preserve do
- = markdown_field(release, :description)
+ - if can?(current_user, :admin_project, @project)
+ = link_to namespace_project_tag_path(@project.namespace, @project, tag.name), class: 'btn btn-remove remove-row has-tooltip', title: "Delete tag", method: :delete, data: { confirm: "Deleting the '#{tag.name}' tag cannot be undone. Are you sure?", container: 'body' }, remote: true do
+ = icon("trash-o")
diff --git a/app/views/projects/tags/index.html.haml b/app/views/projects/tags/index.html.haml
index 1d39f3a7534..e2f132f7742 100644
--- a/app/views/projects/tags/index.html.haml
+++ b/app/views/projects/tags/index.html.haml
@@ -2,16 +2,16 @@
- page_title "Tags"
= render "projects/commits/head"
-%div{ class: container_class }
- .top-area
- .nav-text
+.flex-list{ class: container_class }
+ .top-area.flex-row
+ .nav-text.row-main-content
Tags give the ability to mark specific points in history as being important
- .nav-controls
+ .nav-controls.row-fixed-content
= form_tag(filter_tags_path, method: :get) do
= search_field_tag :search, params[:search], { placeholder: 'Filter by tag name', id: 'tag-search', class: 'form-control search-text-input input-short', spellcheck: false }
- .dropdown.inline
+ .dropdown
%button.dropdown-toggle{ type: 'button', data: { toggle: 'dropdown'} }
%span.light
= projects_sort_options_hash[@sort]
@@ -30,7 +30,7 @@
.tags
- if @tags.any?
- %ul.content-list
+ %ul.flex-list.content-list
= render partial: 'tag', collection: @tags
= paginate @tags, theme: 'gitlab'
diff --git a/app/views/projects/tags/new.html.haml b/app/views/projects/tags/new.html.haml
index c06a413eb2f..160d4c7a223 100644
--- a/app/views/projects/tags/new.html.haml
+++ b/app/views/projects/tags/new.html.haml
@@ -2,7 +2,7 @@
- if @error
.alert.alert-danger
- %button{ type: "button", class: "close", "data-dismiss" => "alert"} &times;
+ %button.close{ type: "button", "data-dismiss" => "alert" } &times;
= @error
%h3.page-title
diff --git a/app/views/projects/tags/show.html.haml b/app/views/projects/tags/show.html.haml
index 12facb6eb73..fad3c5c2173 100644
--- a/app/views/projects/tags/show.html.haml
+++ b/app/views/projects/tags/show.html.haml
@@ -12,21 +12,23 @@
- else
Cant find HEAD commit for this tag
- .nav-controls
+ .nav-controls.controls-flex
- 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
+ = link_to edit_namespace_project_tag_release_path(@project.namespace, @project, @tag.name), class: 'btn controls-item 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
+ = link_to namespace_project_tree_path(@project.namespace, @project, @tag.name), class: 'btn controls-item 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
+ = link_to namespace_project_commits_path(@project.namespace, @project, @tag.name), class: 'btn controls-item has-tooltip', title: 'Browse commits' do
= icon('history')
- = render 'projects/buttons/download', project: @project, ref: @tag.name
+ .btn-container.controls-item
+ = render 'projects/buttons/download', project: @project, ref: @tag.name
- if can?(current_user, :admin_project, @project)
- .pull-right
+ .btn-container.controls-item-full
= link_to namespace_project_tag_path(@project.namespace, @project, @tag.name), class: 'btn btn-remove remove-row has-tooltip', title: "Delete tag", method: :delete, data: { confirm: "Deleting the '#{@tag.name}' tag cannot be undone. Are you sure?" } do
%i.fa.fa-trash-o
+
- if @tag.message.present?
- %pre.body
+ %pre.wrap
= strip_gpg_signature(@tag.message)
.append-bottom-default.prepend-top-default
diff --git a/app/views/projects/tree/_blob_item.html.haml b/app/views/projects/tree/_blob_item.html.haml
index ee417b58cbf..425b460eb09 100644
--- a/app/views/projects/tree/_blob_item.html.haml
+++ b/app/views/projects/tree/_blob_item.html.haml
@@ -6,4 +6,4 @@
%span.str-truncated= file_name
%td.hidden-xs.tree-commit
%td.tree-time-ago.cgray.text-right
- = render 'projects/tree/spinner' \ No newline at end of file
+ = render 'projects/tree/spinner'
diff --git a/app/views/projects/tree/_submodule_item.html.haml b/app/views/projects/tree/_submodule_item.html.haml
index 2b5f671c09e..04d52361db0 100644
--- a/app/views/projects/tree/_submodule_item.html.haml
+++ b/app/views/projects/tree/_submodule_item.html.haml
@@ -1,4 +1,4 @@
-%tr{ class: "tree-item" }
+%tr.tree-item
%td.tree-item-file-name
%i.fa.fa-archive.fa-fw
= submodule_link(submodule_item, @ref)
diff --git a/app/views/projects/tree/_tree_content.html.haml b/app/views/projects/tree/_tree_content.html.haml
index d37c376c36b..6855c463c6d 100644
--- a/app/views/projects/tree/_tree_content.html.haml
+++ b/app/views/projects/tree/_tree_content.html.haml
@@ -1,16 +1,18 @@
-%div.tree-content-holder
+.tree-content-holder
.table-holder
- %table.table#tree-slider{class: "table_#{@hex_path} tree-table" }
+ %table.table#tree-slider{ class: "table_#{@hex_path} tree-table" }
%thead
%tr
%th Name
%th.hidden-xs
.pull-left Last commit
.last-commit.hidden-sm.pull-left
- %small.light
- = clipboard_button(clipboard_text: @commit.id)
+ %i.fa.fa-angle-right
+ %small.light
= link_to @commit.short_id, namespace_project_commit_path(@project.namespace, @project, @commit), class: "monospace"
+ = clipboard_button(clipboard_text: @commit.id, title: "Copy commit SHA to clipboard")
= time_ago_with_tooltip(@commit.committed_date)
+ \-
= @commit.full_title
%small.commit-history-link-spacer &#124;
= link_to 'History', namespace_project_commits_path(@project.namespace, @project, @id), class: 'commit-history-link'
diff --git a/app/views/projects/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml
index 1c5f8b3928b..259207a6dfd 100644
--- a/app/views/projects/tree/_tree_header.html.haml
+++ b/app/views/projects/tree/_tree_header.html.haml
@@ -15,11 +15,11 @@
- if current_user
%li
- if !on_top_of_branch?
- %span.btn.add-to-tree.disabled.has-tooltip{title: "You can only add files when you are on a branch", data: { container: 'body' }}
+ %span.btn.add-to-tree.disabled.has-tooltip{ title: "You can only add files when you are on a branch", data: { container: 'body' } }
= icon('plus')
- else
%span.dropdown
- %a.dropdown-toggle.btn.add-to-tree{href: '#', "data-toggle" => "dropdown"}
+ %a.dropdown-toggle.btn.add-to-tree{ href: '#', "data-toggle" => "dropdown" }
= icon('plus')
%ul.dropdown-menu
- if can_edit_tree?
@@ -28,11 +28,11 @@
= icon('pencil fw')
New file
%li
- = link_to '#modal-upload-blob', { 'data-target' => '#modal-upload-blob', 'data-toggle' => 'modal'} do
+ = link_to '#modal-upload-blob', { 'data-target' => '#modal-upload-blob', 'data-toggle' => 'modal' } do
= icon('file fw')
Upload file
%li
- = link_to '#modal-create-new-dir', { 'data-target' => '#modal-create-new-dir', 'data-toggle' => 'modal'} do
+ = link_to '#modal-create-new-dir', { 'data-target' => '#modal-create-new-dir', 'data-toggle' => 'modal' } do
= icon('folder fw')
New directory
- elsif can?(current_user, :fork_project, @project)
diff --git a/app/views/projects/tree/_tree_item.html.haml b/app/views/projects/tree/_tree_item.html.haml
index 1ccef6d52ab..15c9536133c 100644
--- a/app/views/projects/tree/_tree_item.html.haml
+++ b/app/views/projects/tree/_tree_item.html.haml
@@ -6,4 +6,4 @@
%span.str-truncated= path
%td.hidden-xs.tree-commit
%td.tree-time-ago.text-right
- = render 'projects/tree/spinner' \ No newline at end of file
+ = render 'projects/tree/spinner'
diff --git a/app/views/projects/triggers/_index.html.haml b/app/views/projects/triggers/_index.html.haml
new file mode 100644
index 00000000000..5cb1818ae54
--- /dev/null
+++ b/app/views/projects/triggers/_index.html.haml
@@ -0,0 +1,108 @@
+.row.prepend-top-default.append-bottom-default
+ .col-lg-3
+ %h4.prepend-top-0
+ Triggers
+ %p.prepend-top-20
+ Triggers can force a specific branch or tag to get rebuilt with an API call.
+ %p.append-bottom-0
+ = succeed '.' do
+ Learn more in the
+ = link_to 'triggers documentation', help_page_path('ci/triggers/README'), target: '_blank'
+ .col-lg-9
+ .panel.panel-default
+ .panel-heading
+ %h4.panel-title
+ Manage your project's triggers
+ .panel-body
+ - if @triggers.any?
+ .table-responsive
+ %table.table
+ %thead
+ %th
+ %strong Token
+ %th
+ %strong Last used
+ %th
+ = render partial: 'projects/triggers/trigger', collection: @triggers, as: :trigger
+ - else
+ %p.settings-message.text-center.append-bottom-default
+ No triggers have been created yet. Add one using the button below.
+
+ = form_for @trigger, url: url_for(controller: '/projects/triggers', action: 'create') do |f|
+ = f.submit "Add trigger", class: 'btn btn-success'
+
+ .panel-footer
+
+ %p
+ In the following examples, you can see the exact API call you need to
+ make in order to rebuild a specific
+ %code ref
+ (branch or tag) with a trigger token.
+ %p
+ All you need to do is replace the
+ %code TOKEN
+ and
+ %code REF_NAME
+ with the trigger token and the branch or tag name respectively.
+
+ %h5.prepend-top-default
+ Use cURL
+
+ %p.light
+ Copy one of the tokens above, set your branch or tag name, and that
+ reference will be rebuilt.
+
+ %pre
+ :plain
+ curl -X POST \
+ -F token=TOKEN \
+ -F ref=REF_NAME \
+ #{builds_trigger_url(@project.id)}
+ %h5.prepend-top-default
+ Use .gitlab-ci.yml
+
+ %p.light
+ In the
+ %code .gitlab-ci.yml
+ of another project, include the following snippet.
+ The project will be rebuilt at the end of the job.
+
+ %pre
+ :plain
+ trigger_build:
+ stage: deploy
+ script:
+ - "curl -X POST -F token=TOKEN -F ref=REF_NAME #{builds_trigger_url(@project.id)}"
+ %h5.prepend-top-default
+ Use webhook
+
+ %p.light
+ Add the following webhook to another project for Push and Tag push events.
+ The project will be rebuilt at the corresponding event.
+
+ %pre
+ :plain
+ #{builds_trigger_url(@project.id, ref: 'REF_NAME')}?token=TOKEN
+ %h5.prepend-top-default
+ Pass job variables
+
+ %p.light
+ Add
+ %code variables[VARIABLE]=VALUE
+ to an API request. Variable values can be used to distinguish between triggered jobs and normal jobs.
+
+ With cURL:
+
+ %pre
+ :plain
+ curl -X POST \
+ -F token=TOKEN \
+ -F "ref=REF_NAME" \
+ -F "variables[RUN_NIGHTLY_BUILD]=true" \
+ #{builds_trigger_url(@project.id)}
+ %p.light
+ With webhook:
+
+ %pre.append-bottom-0
+ :plain
+ #{builds_trigger_url(@project.id, ref: 'REF_NAME')}?token=TOKEN&variables[RUN_NIGHTLY_BUILD]=true
diff --git a/app/views/projects/triggers/index.html.haml b/app/views/projects/triggers/index.html.haml
deleted file mode 100644
index 6e5dd1b196d..00000000000
--- a/app/views/projects/triggers/index.html.haml
+++ /dev/null
@@ -1,110 +0,0 @@
-- page_title "Triggers"
-
-.row.prepend-top-default.append-bottom-default
- .col-lg-3
- %h4.prepend-top-0
- = page_title
- %p.prepend-top-20
- Triggers can force a specific branch or tag to get rebuilt with an API call.
- %p.append-bottom-0
- = succeed '.' do
- Learn more in the
- = link_to 'triggers documentation', help_page_path('ci/triggers/README'), target: '_blank'
- .col-lg-9
- .panel.panel-default
- .panel-heading
- %h4.panel-title
- Manage your project's triggers
- .panel-body
- - if @triggers.any?
- .table-responsive
- %table.table
- %thead
- %th
- %strong Token
- %th
- %strong Last used
- %th
- = render partial: 'trigger', collection: @triggers, as: :trigger
- - else
- %p.settings-message.text-center.append-bottom-default
- No triggers have been created yet. Add one using the button below.
-
- = form_for @trigger, url: url_for(controller: 'projects/triggers', action: 'create') do |f|
- = f.submit "Add trigger", class: 'btn btn-success'
-
- .panel-footer
-
- %p
- In the following examples, you can see the exact API call you need to
- make in order to rebuild a specific
- %code ref
- (branch or tag) with a trigger token.
- %p
- All you need to do is replace the
- %code TOKEN
- and
- %code REF_NAME
- with the trigger token and the branch or tag name respectively.
-
- %h5.prepend-top-default
- Use cURL
-
- %p.light
- Copy one of the tokens above, set your branch or tag name, and that
- reference will be rebuilt.
-
- %pre
- :plain
- curl -X POST \
- -F token=TOKEN \
- -F ref=REF_NAME \
- #{builds_trigger_url(@project.id)}
- %h5.prepend-top-default
- Use .gitlab-ci.yml
-
- %p.light
- In the
- %code .gitlab-ci.yml
- of another project, include the following snippet.
- The project will be rebuilt at the end of the build.
-
- %pre
- :plain
- trigger_build:
- stage: deploy
- script:
- - "curl -X POST -F token=TOKEN -F ref=REF_NAME #{builds_trigger_url(@project.id)}"
- %h5.prepend-top-default
- Use webhook
-
- %p.light
- Add the following webhook to another project for Push and Tag push events.
- The project will be rebuilt at the corresponding event.
-
- %pre
- :plain
- #{builds_trigger_url(@project.id, ref: 'REF_NAME')}?token=TOKEN
- %h5.prepend-top-default
- Pass build variables
-
- %p.light
- Add
- %code variables[VARIABLE]=VALUE
- to an API request. Variable values can be used to distinguish between triggered builds and normal builds.
-
- With cURL:
-
- %pre
- :plain
- curl -X POST \
- -F token=TOKEN \
- -F "ref=REF_NAME" \
- -F "variables[RUN_NIGHTLY_BUILD]=true" \
- #{builds_trigger_url(@project.id)}
- %p.light
- With webhook:
-
- %pre.append-bottom-0
- :plain
- #{builds_trigger_url(@project.id, ref: 'REF_NAME')}?token=TOKEN&variables[RUN_NIGHTLY_BUILD]=true
diff --git a/app/views/projects/variables/_content.html.haml b/app/views/projects/variables/_content.html.haml
index 0249e0c1bf1..06477aba103 100644
--- a/app/views/projects/variables/_content.html.haml
+++ b/app/views/projects/variables/_content.html.haml
@@ -5,4 +5,4 @@
%p
So you can use them for passwords, secret keys or whatever you want.
%p
- The value of the variable can be visible in build log if explicitly asked to do so.
+ The value of the variable can be visible in job log if explicitly asked to do so.
diff --git a/app/views/projects/variables/_index.html.haml b/app/views/projects/variables/_index.html.haml
new file mode 100644
index 00000000000..1b852a9c5b3
--- /dev/null
+++ b/app/views/projects/variables/_index.html.haml
@@ -0,0 +1,16 @@
+.row.prepend-top-default.append-bottom-default
+ .col-lg-3
+ = render "projects/variables/content"
+ .col-lg-9
+ %h5.prepend-top-0
+ Add a variable
+ = render "projects/variables/form", btn_text: "Add new variable"
+ %hr
+ %h5.prepend-top-0
+ Your variables (#{@project.variables.size})
+ - if @project.variables.empty?
+ %p.settings-message.text-center.append-bottom-0
+ No variables found, add one with the form above.
+ - else
+ = render "projects/variables/table"
+ %button.btn.btn-info.js-btn-toggle-reveal-values{ "data-status" => 'hidden' } Reveal Values
diff --git a/app/views/projects/variables/index.html.haml b/app/views/projects/variables/index.html.haml
deleted file mode 100644
index 39303700131..00000000000
--- a/app/views/projects/variables/index.html.haml
+++ /dev/null
@@ -1,18 +0,0 @@
-- page_title "Variables"
-
-.row.prepend-top-default.append-bottom-default
- .col-lg-3
- = render "content"
- .col-lg-9
- %h5.prepend-top-0
- Add a variable
- = render "form", btn_text: "Add new variable"
- %hr
- %h5.prepend-top-0
- Your variables (#{@project.variables.size})
- - if @project.variables.empty?
- %p.settings-message.text-center.append-bottom-0
- No variables found, add one with the form above.
- - else
- = render "table"
- %button.btn.btn-info.js-btn-toggle-reveal-values{"data-status" => 'hidden'} Reveal Values
diff --git a/app/views/projects/wikis/_new.html.haml b/app/views/projects/wikis/_new.html.haml
index c32cb122c26..c74f53b4c39 100644
--- a/app/views/projects/wikis/_new.html.haml
+++ b/app/views/projects/wikis/_new.html.haml
@@ -1,11 +1,11 @@
- @no_container = true
%div{ class: container_class }
- %div#modal-new-wiki.modal
+ #modal-new-wiki.modal
.modal-dialog
.modal-content
.modal-header
- %a.close{href: "#", "data-dismiss" => "modal"} ×
+ %a.close{ href: "#", "data-dismiss" => "modal" } ×
%h3.page-title New Wiki Page
.modal-body
%form.new-wiki-page
diff --git a/app/views/projects/wikis/git_access.html.haml b/app/views/projects/wikis/git_access.html.haml
index e25d6a48573..fb0efd85dcd 100644
--- a/app/views/projects/wikis/git_access.html.haml
+++ b/app/views/projects/wikis/git_access.html.haml
@@ -17,6 +17,13 @@
%pre.dark
:preserve
gem install gollum
+ %p
+ It is recommended to install
+ %code github-markdown
+ so that GFM features render locally:
+ %pre.dark
+ :preserve
+ gem install github-markdown
%h3 Clone your wiki
%pre.dark
diff --git a/app/views/search/_category.html.haml b/app/views/search/_category.html.haml
index 2c378231237..8cbecb725b5 100644
--- a/app/views/search/_category.html.haml
+++ b/app/views/search/_category.html.haml
@@ -1,70 +1,70 @@
%ul.nav-links.search-filter
- if @project
- %li{class: ("active" if @scope == 'blobs')}
+ %li{ class: ("active" if @scope == 'blobs') }
= link_to search_filter_path(scope: 'blobs') do
Code
%span.badge
= @search_results.blobs_count
- %li{class: ("active" if @scope == 'issues')}
+ %li{ class: ("active" if @scope == 'issues') }
= link_to search_filter_path(scope: 'issues') do
Issues
%span.badge
= @search_results.issues_count
- %li{class: ("active" if @scope == 'merge_requests')}
+ %li{ class: ("active" if @scope == 'merge_requests') }
= link_to search_filter_path(scope: 'merge_requests') do
Merge requests
%span.badge
= @search_results.merge_requests_count
- %li{class: ("active" if @scope == 'milestones')}
+ %li{ class: ("active" if @scope == 'milestones') }
= link_to search_filter_path(scope: 'milestones') do
Milestones
%span.badge
= @search_results.milestones_count
- %li{class: ("active" if @scope == 'notes')}
+ %li{ class: ("active" if @scope == 'notes') }
= link_to search_filter_path(scope: 'notes') do
Comments
%span.badge
= @search_results.notes_count
- %li{class: ("active" if @scope == 'wiki_blobs')}
+ %li{ class: ("active" if @scope == 'wiki_blobs') }
= link_to search_filter_path(scope: 'wiki_blobs') do
Wiki
%span.badge
= @search_results.wiki_blobs_count
- %li{class: ("active" if @scope == 'commits')}
+ %li{ class: ("active" if @scope == 'commits') }
= link_to search_filter_path(scope: 'commits') do
Commits
%span.badge
= @search_results.commits_count
- elsif @show_snippets
- %li{class: ("active" if @scope == 'snippet_blobs')}
+ %li{ class: ("active" if @scope == 'snippet_blobs') }
= link_to search_filter_path(scope: 'snippet_blobs', snippets: true, group_id: nil, project_id: nil) do
Snippet Contents
%span.badge
= @search_results.snippet_blobs_count
- %li{class: ("active" if @scope == 'snippet_titles')}
+ %li{ class: ("active" if @scope == 'snippet_titles') }
= link_to search_filter_path(scope: 'snippet_titles', snippets: true, group_id: nil, project_id: nil) do
Titles and Filenames
%span.badge
= @search_results.snippet_titles_count
- else
- %li{class: ("active" if @scope == 'projects')}
+ %li{ class: ("active" if @scope == 'projects') }
= link_to search_filter_path(scope: 'projects') do
Projects
%span.badge
= @search_results.projects_count
- %li{class: ("active" if @scope == 'issues')}
+ %li{ class: ("active" if @scope == 'issues') }
= link_to search_filter_path(scope: 'issues') do
Issues
%span.badge
= @search_results.issues_count
- %li{class: ("active" if @scope == 'merge_requests')}
+ %li{ class: ("active" if @scope == 'merge_requests') }
= link_to search_filter_path(scope: 'merge_requests') do
Merge requests
%span.badge
= @search_results.merge_requests_count
- %li{class: ("active" if @scope == 'milestones')}
+ %li{ class: ("active" if @scope == 'milestones') }
= link_to search_filter_path(scope: 'milestones') do
Milestones
%span.badge
diff --git a/app/views/search/results/_empty.html.haml b/app/views/search/results/_empty.html.haml
index 05a63016c09..821a39d61f5 100644
--- a/app/views/search/results/_empty.html.haml
+++ b/app/views/search/results/_empty.html.haml
@@ -3,4 +3,4 @@
%h4
= icon('search')
We couldn't find any results matching
- %code #{@search_term}
+ %code= @search_term
diff --git a/app/views/search/results/_merge_request.html.haml b/app/views/search/results/_merge_request.html.haml
index 07b17bc69c0..2e6adf3027c 100644
--- a/app/views/search/results/_merge_request.html.haml
+++ b/app/views/search/results/_merge_request.html.haml
@@ -2,7 +2,7 @@
%h4
= link_to [merge_request.target_project.namespace.becomes(Namespace), merge_request.target_project, merge_request] do
%span.term.str-truncated= merge_request.title
- .pull-right #{merge_request.to_reference}
+ .pull-right= merge_request.to_reference
- if merge_request.description.present?
.description.term
= preserve do
diff --git a/app/views/search/results/_snippet_blob.html.haml b/app/views/search/results/_snippet_blob.html.haml
index c9b7bd154af..23ca6479414 100644
--- a/app/views/search/results/_snippet_blob.html.haml
+++ b/app/views/search/results/_snippet_blob.html.haml
@@ -9,7 +9,7 @@
= link_to user_snippets_path(snippet.author) do
= image_tag avatar_icon(snippet.author_email), class: "avatar avatar-inline s16", alt: ''
= snippet.author_name
- %span.light #{time_ago_with_tooltip(snippet.created_at)}
+ %span.light= time_ago_with_tooltip(snippet.created_at)
%h4.snippet-title
- snippet_path = reliable_snippet_path(snippet)
= link_to snippet_path do
diff --git a/app/views/search/results/_snippet_title.html.haml b/app/views/search/results/_snippet_title.html.haml
index 027d42396b4..704d1d01a81 100644
--- a/app/views/search/results/_snippet_title.html.haml
+++ b/app/views/search/results/_snippet_title.html.haml
@@ -20,4 +20,4 @@
= link_to user_snippets_path(snippet_title.author) do
= image_tag avatar_icon(snippet_title.author_email), class: "avatar avatar-inline s16", alt: ''
= snippet_title.author_name
- %span.light #{time_ago_with_tooltip(snippet_title.created_at)}
+ %span.light= time_ago_with_tooltip(snippet_title.created_at)
diff --git a/app/views/shared/_choose_group_avatar_button.html.haml b/app/views/shared/_choose_group_avatar_button.html.haml
index 000532b1c9a..94295970acf 100644
--- a/app/views/shared/_choose_group_avatar_button.html.haml
+++ b/app/views/shared/_choose_group_avatar_button.html.haml
@@ -1,4 +1,4 @@
-%a.choose-btn.btn.btn-sm.js-choose-group-avatar-button
+%button.choose-btn.btn.btn-sm.js-choose-group-avatar-button{ type: 'button' }
%i.fa.fa-paperclip
%span Choose File ...
&nbsp;
diff --git a/app/views/shared/_clone_panel.html.haml b/app/views/shared/_clone_panel.html.haml
index 3b82d8e686f..03684389742 100644
--- a/app/views/shared/_clone_panel.html.haml
+++ b/app/views/shared/_clone_panel.html.haml
@@ -2,12 +2,12 @@
.git-clone-holder.input-group
.input-group-btn
- -if allowed_protocols_present?
+ - if allowed_protocols_present?
.clone-dropdown-btn.btn.btn-static
%span
= enabled_project_button(project, enabled_protocol)
- else
- %a#clone-dropdown.clone-dropdown-btn.btn{href: '#', data: { toggle: 'dropdown' }}
+ %a#clone-dropdown.clone-dropdown-btn.btn{ href: '#', data: { toggle: 'dropdown' } }
%span
= default_clone_protocol.upcase
= icon('caret-down')
@@ -19,7 +19,7 @@
= text_field_tag :project_clone, default_url_to_repo(project), class: "js-select-on-focus form-control", readonly: true
.input-group-btn
- = clipboard_button(clipboard_target: '#project_clone')
+ = clipboard_button(clipboard_target: '#project_clone', title: "Copy URL to clipboard")
:javascript
$('ul.clone-options-dropdown a').on('click',function(e){
diff --git a/app/views/shared/_confirm_modal.html.haml b/app/views/shared/_confirm_modal.html.haml
index b0fc60573f7..f94f8ffc604 100644
--- a/app/views/shared/_confirm_modal.html.haml
+++ b/app/views/shared/_confirm_modal.html.haml
@@ -1,8 +1,8 @@
-#modal-confirm-danger.modal{tabindex: -1}
+#modal-confirm-danger.modal{ tabindex: -1 }
.modal-dialog
.modal-content
.modal-header
- %a.close{href: "#", "data-dismiss" => "modal"} ×
+ %a.close{ href: "#", "data-dismiss" => "modal" } ×
%h3.page-title
Confirmation required
@@ -14,7 +14,7 @@
To prevent accidental actions we ask you to confirm your intention.
%br
Please type
- %code.js-confirm-danger-match #{phrase}
+ %code.js-confirm-danger-match= phrase
to proceed or close this modal to cancel.
.form-group
diff --git a/app/views/shared/_file_highlight.html.haml b/app/views/shared/_file_highlight.html.haml
index e26693bf5b9..8d64cb5d698 100644
--- a/app/views/shared/_file_highlight.html.haml
+++ b/app/views/shared/_file_highlight.html.haml
@@ -9,8 +9,8 @@
- offset = defined?(first_line_number) ? first_line_number : 1
- i = index + offset
-# We're not using `link_to` because it is too slow once we get to thousands of lines.
- %a.diff-line-num{href: "#{link}#L#{i}", id: "L#{i}", 'data-line-number' => i}
+ %a.diff-line-num{ href: "#{link}#L#{i}", id: "L#{i}", 'data-line-number' => i }
= link_icon
= i
- .blob-content{data: {blob_id: blob.id}}
+ .blob-content{ data: { blob_id: blob.id } }
= highlight(blob.path, blob.data, repository: repository, plain: blob.no_highlighting?)
diff --git a/app/views/shared/_import_form.html.haml b/app/views/shared/_import_form.html.haml
index 65a3a6bddab..54b5ae2402e 100644
--- a/app/views/shared/_import_form.html.haml
+++ b/app/views/shared/_import_form.html.haml
@@ -2,7 +2,7 @@
= f.label :import_url, class: 'control-label' do
%span Git repository URL
.col-sm-10
- = f.text_field :import_url, class: 'form-control', placeholder: 'https://username:password@gitlab.company.com/group/project.git', disabled: true
+ = f.text_field :import_url, autocomplete: 'off', class: 'form-control', placeholder: 'https://username:password@gitlab.company.com/group/project.git', disabled: true
.well.prepend-top-20
%ul
@@ -13,4 +13,4 @@
%li
The import will time out after 15 minutes. For repositories that take longer, use a clone/push combination.
%li
- To migrate an SVN repository, check out #{link_to "this document", "http://doc.gitlab.com/ce/workflow/importing/migrating_from_svn.html"}.
+ To migrate an SVN repository, check out #{link_to "this document", help_page_path('workflow/importing/migrating_from_svn')}.
diff --git a/app/views/shared/_issues.html.haml b/app/views/shared/_issues.html.haml
index 26b349e8a62..3a49227961f 100644
--- a/app/views/shared/_issues.html.haml
+++ b/app/views/shared/_issues.html.haml
@@ -1,16 +1,7 @@
- if @issues.to_a.any?
- - @issues.group_by(&:project).each do |group|
- .panel.panel-default.panel-small
- - project = group[0]
- .panel-heading
- = link_to project.name_with_namespace, namespace_project_issues_path(project.namespace, project)
- - if can?(current_user, :create_issue, project)
- .pull-right
- = link_to 'New issue', new_namespace_project_issue_path(project.namespace, project)
-
- %ul.content-list.issues-list
- - group[1].each do |issue|
- = render 'projects/issues/issue', issue: issue
+ .panel.panel-default.panel-small.panel-without-border
+ %ul.content-list.issues-list
+ = render partial: 'projects/issues/issue', collection: @issues
= paginate @issues, theme: "gitlab"
- else
= render 'shared/empty_states/issues'
diff --git a/app/views/shared/_label.html.haml b/app/views/shared/_label.html.haml
index db324d8868e..ead9b84b991 100644
--- a/app/views/shared/_label.html.haml
+++ b/app/views/shared/_label.html.haml
@@ -4,7 +4,7 @@
- status = label_subscription_status(label, @project).inquiry if current_user
- subject = local_assigns[:subject]
-%li{id: label_css_id, data: { id: label.id } }
+%li{ id: label_css_id, data: { id: label.id } }
= render "shared/label_row", label: label
.visible-xs.visible-sm-inline-block.visible-md-inline-block.dropdown
@@ -36,7 +36,7 @@
%li
= link_to 'Edit', edit_label_path(label)
%li
- = link_to 'Delete', destroy_label_path(label), title: 'Delete', method: :delete, remote: true, data: {confirm: 'Remove this label? Are you sure?'}
+ = link_to 'Delete', destroy_label_path(label), title: 'Delete', method: :delete, data: {confirm: 'Remove this label? Are you sure?'}
.pull-right.hidden-xs.hidden-sm.hidden-md
= link_to_label(label, subject: subject, type: :merge_request, css_class: 'btn btn-transparent btn-action') do
@@ -56,7 +56,7 @@
= icon('spinner spin', class: 'label-subscribe-button-loading')
.dropdown.dropdown-group-label{ class: ('hidden' unless status.unsubscribed?) }
- %button.dropdown-menu-toggle{type: 'button', 'data-toggle' => 'dropdown'}
+ %button.dropdown-menu-toggle{ type: 'button', 'data-toggle' => 'dropdown' }
%span Subscribe
= icon('chevron-down')
%ul.dropdown-menu
@@ -66,11 +66,15 @@
%a.js-subscribe-button{ data: { url: toggle_subscription_group_label_path(label.group, label) } }
Group level
+ - if label.is_a?(ProjectLabel) && label.project.group && can?(current_user, :admin_group, label.project.group)
+ = link_to promote_namespace_project_label_path(label.project.namespace, label.project, label), title: "Promote to Group Label", class: 'btn btn-transparent btn-action', data: {confirm: "Promoting this label will make this label available to all projects inside this group. Existing project labels with the same name will be merged. Are you sure?", toggle: "tooltip"}, method: :post do
+ %span.sr-only Promote to Group
+ = icon('level-up')
- if can?(current_user, :admin_label, label)
= link_to edit_label_path(label), title: "Edit", class: 'btn btn-transparent btn-action', data: {toggle: "tooltip"} do
%span.sr-only Edit
= icon('pencil-square-o')
- = link_to destroy_label_path(label), title: "Delete", class: 'btn btn-transparent btn-action remove-row', method: :delete, remote: true, data: {confirm: label_deletion_confirm_text(label), toggle: "tooltip"} do
+ = link_to destroy_label_path(label), title: "Delete", class: 'btn btn-transparent btn-action remove-row', method: :delete, data: {confirm: label_deletion_confirm_text(label), toggle: "tooltip"} do
%span.sr-only Delete
= icon('trash-o')
diff --git a/app/views/shared/_merge_requests.html.haml b/app/views/shared/_merge_requests.html.haml
index 2f3605b4d27..b7982b7fe9b 100644
--- a/app/views/shared/_merge_requests.html.haml
+++ b/app/views/shared/_merge_requests.html.haml
@@ -1,16 +1,8 @@
- if @merge_requests.to_a.any?
- - @merge_requests.group_by(&:target_project).each do |group|
- .panel.panel-default.panel-small
- - project = group[0]
- .panel-heading
- = link_to project.name_with_namespace, namespace_project_merge_requests_path(project.namespace, project)
- - if can?(current_user, :create_merge_request, project)
- .pull-right
- = link_to 'New merge request', new_namespace_project_merge_request_path(project.namespace, project)
+ .panel.panel-default.panel-small.panel-without-border
+ %ul.content-list.mr-list
+ = render partial: 'projects/merge_requests/merge_request', collection: @merge_requests
- %ul.content-list.mr-list
- - group[1].each do |merge_request|
- = render 'projects/merge_requests/merge_request', merge_request: merge_request
= paginate @merge_requests, theme: "gitlab"
- else
diff --git a/app/views/shared/_milestones_filter.html.haml b/app/views/shared/_milestones_filter.html.haml
index 186ed4a7c8b..704893b4d5b 100644
--- a/app/views/shared/_milestones_filter.html.haml
+++ b/app/views/shared/_milestones_filter.html.haml
@@ -2,18 +2,18 @@
- counts = milestone_counts(@project.milestones)
%ul.nav-links
- %li{class: milestone_class_for_state(params[:state], 'opened', true)}>
+ %li{ class: milestone_class_for_state(params[:state], 'opened', true) }>
= link_to milestones_filter_path(state: 'opened') do
Open
- if @project
- %span.badge #{counts[:opened]}
- %li{class: milestone_class_for_state(params[:state], 'closed')}>
+ %span.badge= counts[:opened]
+ %li{ class: milestone_class_for_state(params[:state], 'closed') }>
= link_to milestones_filter_path(state: 'closed') do
Closed
- if @project
- %span.badge #{counts[:closed]}
- %li{class: milestone_class_for_state(params[:state], 'all')}>
+ %span.badge= counts[:closed]
+ %li{ class: milestone_class_for_state(params[:state], 'all') }>
= link_to milestones_filter_path(state: 'all') do
All
- if @project
- %span.badge #{counts[:all]}
+ %span.badge= counts[:all]
diff --git a/app/views/shared/_mini_pipeline_graph.html.haml b/app/views/shared/_mini_pipeline_graph.html.haml
new file mode 100644
index 00000000000..b0778653d4e
--- /dev/null
+++ b/app/views/shared/_mini_pipeline_graph.html.haml
@@ -0,0 +1,18 @@
+.stage-cell
+ - pipeline.stages.each do |stage|
+ - if stage.status
+ - detailed_status = stage.detailed_status(current_user)
+ - icon_status = "#{detailed_status.icon}_borderless"
+ - status_klass = "ci-status-icon ci-status-icon-#{detailed_status.group}"
+
+ .stage-container.dropdown{ class: klass }
+ %button.mini-pipeline-graph-dropdown-toggle.has-tooltip.js-builds-dropdown-button{ class: "ci-status-icon-#{detailed_status.group}", type: 'button', data: { toggle: 'dropdown', title: "#{stage.name}: #{detailed_status.label}", placement: 'top', "stage-endpoint" => stage_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline, stage: stage.name) } }
+ = custom_icon(icon_status)
+ = icon('caret-down')
+
+ %ul.dropdown-menu.mini-pipeline-graph-dropdown-menu.js-builds-dropdown-container
+ .arrow-up
+ .js-builds-dropdown-list.scrollable-menu
+
+ .js-builds-dropdown-loading.builds-dropdown-loading.hidden
+ %span.fa.fa-spinner.fa-spin
diff --git a/app/views/shared/_nav_scroll.html.haml b/app/views/shared/_nav_scroll.html.haml
index 4e3b1b3a571..61646f150c1 100644
--- a/app/views/shared/_nav_scroll.html.haml
+++ b/app/views/shared/_nav_scroll.html.haml
@@ -1,4 +1,4 @@
.fade-left
= icon('angle-left')
.fade-right
- = icon('angle-right') \ No newline at end of file
+ = icon('angle-right')
diff --git a/app/views/shared/_no_password.html.haml b/app/views/shared/_no_password.html.haml
index a43bf33751a..ed6fc76c61e 100644
--- a/app/views/shared/_no_password.html.haml
+++ b/app/views/shared/_no_password.html.haml
@@ -1,8 +1,8 @@
- if cookies[:hide_no_password_message].blank? && !current_user.hide_no_password && current_user.require_password?
- .no-password-message.alert.alert-warning.hidden-xs
+ .no-password-message.alert.alert-warning
You won't be able to pull or push project code via #{gitlab_config.protocol.upcase} until you #{link_to 'set a password', edit_profile_password_path} on your account
- .pull-right
+ .alert-link-group
= link_to "Don't show again", profile_path(user: {hide_no_password: true}), method: :put
|
= link_to 'Remind later', '#', class: 'hide-no-password-message'
diff --git a/app/views/shared/_no_ssh.html.haml b/app/views/shared/_no_ssh.html.haml
index bb5fff2d3bb..d663fa13d10 100644
--- a/app/views/shared/_no_ssh.html.haml
+++ b/app/views/shared/_no_ssh.html.haml
@@ -1,8 +1,8 @@
- if cookies[:hide_no_ssh_message].blank? && !current_user.hide_no_ssh_key && current_user.require_ssh_key?
- .no-ssh-key-message.alert.alert-warning.hidden-xs
+ .no-ssh-key-message.alert.alert-warning
You won't be able to pull or push project code via SSH until you #{link_to 'add an SSH key', profile_keys_path, class: 'alert-link'} to your profile
- .pull-right
+ .alert-link-group
= link_to "Don't show again", profile_path(user: {hide_no_ssh_key: true}), method: :put, class: 'alert-link'
|
= link_to 'Remind later', '#', class: 'hide-no-ssh-message alert-link'
diff --git a/app/views/shared/_outdated_browser.html.haml b/app/views/shared/_outdated_browser.html.haml
index 0eba1fe075f..c06d1ffa59b 100644
--- a/app/views/shared/_outdated_browser.html.haml
+++ b/app/views/shared/_outdated_browser.html.haml
@@ -1,8 +1,7 @@
- if outdated_browser?
- - link = "https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/install/requirements.md#supported-web-browsers"
.browser-alert
GitLab may not work properly because you are using an outdated web browser.
%br
Please install a
- = link_to 'supported web browser', link
+ = link_to 'supported web browser', help_page_url('install/requirements', anchor: 'supported-web-browsers')
for a better experience.
diff --git a/app/views/shared/_sort_dropdown.html.haml b/app/views/shared/_sort_dropdown.html.haml
index ede3c7090d7..0ce0d759e86 100644
--- a/app/views/shared/_sort_dropdown.html.haml
+++ b/app/views/shared/_sort_dropdown.html.haml
@@ -1,5 +1,5 @@
.dropdown.inline.prepend-left-10
- %button.dropdown-toggle{type: 'button', data: {toggle: 'dropdown'}}
+ %button.dropdown-toggle{ type: 'button', data: {toggle: 'dropdown' } }
%span.light
- if @sort.present?
= sort_options_hash[@sort]
diff --git a/app/views/shared/empty_states/_issues.html.haml b/app/views/shared/empty_states/_issues.html.haml
index 07d4927b6c9..e2033654018 100644
--- a/app/views/shared/empty_states/_issues.html.haml
+++ b/app/views/shared/empty_states/_issues.html.haml
@@ -10,10 +10,10 @@
.text-content
- if has_button && current_user
%h4
- The Issue Tracker is a good place to add things that need to be improved or solved in a project!
+ The Issue Tracker is the place to add things that need to be improved or solved in a project
%p
- An issue can be a bug, a todo or a feature request that needs to be discussed in a project.
- Besides, issues are searchable and filterable.
+ Issues can be bugs, tasks or ideas to be discussed.
+ Also, issues are searchable and filterable.
- if project_select_button
= render 'shared/new_project_item_select', path: 'issues/new', label: 'New issue'
- else
diff --git a/app/views/shared/empty_states/_labels.html.haml b/app/views/shared/empty_states/_labels.html.haml
new file mode 100644
index 00000000000..ba5c2dae09d
--- /dev/null
+++ b/app/views/shared/empty_states/_labels.html.haml
@@ -0,0 +1,11 @@
+.row.empty-state.labels
+ .pull-right.col-xs-12.col-sm-6
+ .svg-content
+ = render 'shared/empty_states/icons/labels.svg'
+ .col-xs-12.col-sm-6
+ .text-content
+ %h4 Labels can be applied to issues and merge requests to categorize them.
+ %p You can also star label to make it a priority label.
+ - if can?(current_user, :admin_label, @project)
+ = link_to 'New label', new_namespace_project_label_path(@project.namespace, @project), class: 'btn btn-new', title: 'New label', id: 'new_label_link'
+ = link_to 'Generate a default set of labels', generate_namespace_project_labels_path(@project.namespace, @project), method: :post, class: 'btn btn-success btn-inverted', title: 'Generate a default set of labels', id: 'generate_labels_link'
diff --git a/app/views/shared/empty_states/_priority_labels.html.haml b/app/views/shared/empty_states/_priority_labels.html.haml
new file mode 100644
index 00000000000..bc268301a97
--- /dev/null
+++ b/app/views/shared/empty_states/_priority_labels.html.haml
@@ -0,0 +1,3 @@
+.text-center
+ = render 'shared/empty_states/icons/priority_labels.svg'
+ %p Star labels to start sorting by priority
diff --git a/app/views/shared/empty_states/icons/_labels.svg b/app/views/shared/empty_states/icons/_labels.svg
new file mode 100644
index 00000000000..dc041a4a78b
--- /dev/null
+++ b/app/views/shared/empty_states/icons/_labels.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="787 240 386 274" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><circle id="a" cx="37" cy="107" r="8"/><mask id="e" width="16" height="16" x="0" y="0" fill="#fff"><use xlink:href="#a"/></mask><circle id="b" cx="37" cy="75" r="8"/><mask id="f" width="16" height="16" x="0" y="0" fill="#fff"><use xlink:href="#b"/></mask><circle id="c" cx="42" cy="93" r="8"/><mask id="g" width="16" height="16" x="0" y="0" fill="#fff"><use xlink:href="#c"/></mask><circle id="d" cx="43" cy="75" r="8"/><mask id="h" width="16" height="16" x="0" y="0" fill="#fff"><use xlink:href="#d"/></mask></defs><g fill="none" fill-rule="evenodd" transform="translate(791 244)"><g transform="rotate(30 49.554 229.722)"><rect width="74" height="124" x="8.6" y="95.9" fill="#FAFAFA" rx="8"/><rect width="74" height="124" y="87" fill="#FFF" stroke="#EEE" stroke-width="4" stroke-linecap="round" rx="8"/><circle cx="26.5" cy="178.5" r="3.5" fill="#FC8A51"/><circle cx="47.5" cy="178.5" r="3.5" fill="#FC8A51"/><rect width="50" height="4" x="12" y="127" fill="#E5E5E5" rx="2"/><rect width="38" height="4" x="18" y="139" fill="#E5E5E5" rx="2"/><use stroke="#E5E5E5" stroke-width="8" mask="url(#e)" stroke-linecap="round" xlink:href="#a"/><path stroke="#EEE" stroke-width="4" d="M37.3 107S10.5 18.3 81 .6" stroke-linecap="round"/><path fill="#FDE5D8" d="M31 189c0 3.3 2.7 6 6 6s6-2.7 6-6"/></g><g transform="translate(105 47)"><rect width="74" height="124" y="64" fill="#FAFAFA" rx="8"/><rect width="74" height="124" y="55" fill="#FFF" stroke="#EEE" stroke-width="4" stroke-linecap="round" rx="8"/><rect width="50" height="4" x="12" y="95" fill="#E5E5E5" rx="2"/><rect width="38" height="4" x="18" y="107" fill="#E5E5E5" rx="2"/><use stroke="#E5E5E5" stroke-width="8" mask="url(#f)" stroke-linecap="round" xlink:href="#b"/><path fill="#B5A7DD" d="M56 149.7c-.6-1-.2-2 .7-2.7l1.8-1c1-.6 2-.2 2.7.7.5 1 .2 2.2-.7 2.8l-1.8 1c-1 .5-2 .2-2.7-.8zm-37.8 0c.5-1 .2-2-.7-2.7l-1.8-1c-1-.6-2-.2-2.7.7-.6 1-.2 2.2.7 2.8l1.8 1c1 .5 2 .2 2.7-.8zM33 151h9v4h-9v-4z"/><path fill="#6B4FBB" d="M59 153c0-5.5-4.6-10-10-10-5.7 0-10 4.5-10 10s4.3 10 10 10c5.4 0 10-4.5 10-10zm-16 0c0-3.3 2.6-6 6-6 3.2 0 6 2.7 6 6s-2.8 6-6 6c-3.4 0-6-2.7-6-6zM35 153c0-5.5-4.6-10-10-10-5.7 0-10 4.5-10 10s4.3 10 10 10c5.4 0 10-4.5 10-10zm-16 0c0-3.3 2.6-6 6-6 3.2 0 6 2.7 6 6s-2.8 6-6 6c-3.4 0-6-2.7-6-6z"/><path stroke="#EEE" stroke-width="4" d="M37 75S30 0 80 0" stroke-linecap="round"/></g><g transform="rotate(15 -82.507 752.644)"><rect width="74" height="124" x="14.6" y="81.8" fill="#FAFAFA" rx="8"/><rect width="74" height="124" x="5" y="73" fill="#FFF" stroke="#EEE" stroke-width="4" stroke-linecap="round" rx="8"/><path fill="#FDE5D8" d="M41 147c0-1 1-2 2-2s2 1 2 2v3c0 1-1 2-2 2s-2-1-2-2v-3zm16.8 6.2c.8-.7 2-.6 2.8.3.7.8.5 2-.3 2.8L58 158c-1 .8-2.2.7-3 0-.6-1-.4-2.3.4-3l2.4-1.8zm-32 3c-1-.6-1-2-.4-2.7.7-1 2-1 2.8-.3l2.4 1.8c.8.7 1 2 .3 3-.8.7-2 1-3 0l-2.3-1.7z"/><rect width="2" height="7" x="39" y="168" fill="#FC8A51" rx="1"/><rect width="2" height="7" x="45" y="168" fill="#FC8A51" rx="1"/><circle cx="40" cy="169" r="2" fill="#FC8A51"/><circle cx="46" cy="169" r="2" fill="#FC8A51"/><rect width="22" height="18" x="32" y="158" stroke="#FC8A51" stroke-width="4" rx="8"/><rect width="34" height="5" x="26" y="174" fill="#FC8A51" rx="2.5"/><rect width="50" height="4" x="17" y="113" fill="#E5E5E5" rx="2"/><rect width="38" height="4" x="23" y="125" fill="#E5E5E5" rx="2"/><use stroke="#E5E5E5" stroke-width="8" mask="url(#g)" stroke-linecap="round" xlink:href="#c"/><path stroke="#EEE" stroke-width="4" d="M42 93S50 0 0 0" stroke-linecap="round"/></g><g transform="rotate(-15 276.18 -697.744)"><rect width="74" height="124" x="18.7" y="65.6" fill="#FAFAFA" rx="8"/><rect width="74" height="124" x="6" y="55" fill="#FFF" stroke="#EEE" stroke-width="4" stroke-linecap="round" rx="8"/><g transform="translate(25 129)"><path stroke="#B5A7DD" stroke-width="4" d="M32 14c0-7.7-6.3-14-14-14S4 6.3 4 14" stroke-linecap="round"/><path stroke="#B5A7DD" stroke-width="2" d="M33 15v13c0 4.4-3.6 8-8 8" stroke-linecap="round"/><rect width="7" height="4" x="20" y="34" fill="#6B4FBB" rx="2"/><rect width="7" height="13" y="15" fill="#FFF" stroke="#6B4FBB" stroke-width="3" stroke-linejoin="round" rx="3.5"/><rect width="7" height="13" x="29" y="15" fill="#FFF" stroke="#6B4FBB" stroke-width="3" stroke-linejoin="round" transform="matrix(-1 0 0 1 65 0)" rx="3.5"/></g><rect width="50" height="4" x="18" y="95" fill="#E5E5E5" rx="2"/><rect width="38" height="4" x="24" y="107" fill="#E5E5E5" rx="2"/><use stroke="#E5E5E5" stroke-width="8" mask="url(#h)" stroke-linecap="round" xlink:href="#d"/><path stroke="#EEE" stroke-width="4" d="M43 75S50 0 0 0" stroke-linecap="round"/></g><circle cx="193" cy="47" r="12" fill="#FFF" stroke="#FDE5D8" stroke-width="4"/><circle cx="193" cy="47" r="5" fill="#FFF" stroke="#FDE5D8" stroke-width="4"/><g opacity=".2"><path fill="#FC8A51" d="M30.7 254.8l-2.6 1c-1 .5-1.7 0-1.7-1v-3l-1-2.7c-.4-1 .2-1.7 1.2-1.7h3l2.6-1c1.2-.4 2 .2 2 1.2l-.2 3 1 2.6c.5 1.2 0 2-1 2l-3-.2zM374.7 133.8l-2.6 1c-1 .5-1.7 0-1.7-1v-3l-1-2.7c-.4-1 .2-1.7 1.2-1.7h3l2.6-1c1.2-.4 2 .2 2 1.2l-.2 3 1 2.6c.5 1.2 0 2-1 2l-3-.2zM5.6 95H1.8c-1.3.2-2-.8-1.4-2l1.4-3.4-.2-3.8c0-1.3 1-2 2-1.4l3.6 1.4 3.7-.2c1.2 0 2 1 1.4 2L11 91.3V95c.2 1.2-.8 2-2 1.4L5.6 95z"/><path fill="#6B4FBB" d="M308.8 62l-2-2.3c-.7-.8-.5-1.7.6-2l2.8-1 2-2c1-.6 1.8-.4 2.2.7l.8 2.8 2 2c.8 1 .5 1.8-.5 2.2l-2.8.8-2.3 2c-.8.8-1.7.5-2-.5l-1-2.8zM318 226.6h-3c-1-.2-1.4-1-1-2l1.4-2.5v-3c.2-1 1-1.4 2-1l2.6 1.4h3c1 .2 1.5 1 1 2l-1.4 2.6v3c-.2 1-1 1.5-2 1l-2.5-1.4zM121.8 8l-2-2.3c-.7-.8-.5-1.7.6-2l2.8-1 2-2c1-.6 1.8-.4 2.2.7l.8 2.8 2 2c.8 1 .5 1.8-.5 2.2l-2.8.8-2.3 2c-.8.8-1.7.5-2-.5l-1-2.8z"/></g></g></svg>
diff --git a/app/views/shared/empty_states/icons/_priority_labels.svg b/app/views/shared/empty_states/icons/_priority_labels.svg
new file mode 100644
index 00000000000..7282c2b215e
--- /dev/null
+++ b/app/views/shared/empty_states/icons/_priority_labels.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="116" height="68" viewBox="181 0 116 68"><g fill="none" fill-rule="evenodd" transform="translate(182)"><rect width="78" height="34" x="37" y="34" fill="#FAFAFA" rx="3"/><rect width="78" height="34" x="31" y="28" fill="#FFF" stroke="#EEE" stroke-width="4" stroke-linecap="round" rx="3"/><path fill="#FFF" stroke="#FC6D26" stroke-width="3" d="M34 35.8c-.6 0-1.4 0-1.8.4L29 38.8c-1 .7-1.7.4-2-.7l-.6-4c0-.5-.5-1.2-1-1.5L22 30.2c-1-.6-1-1.5 0-2l3.7-2c.5-.2 1-.8 1.2-1.3l1-4.2c.3-1 1-1.3 2-.5l3 3c.3.3 1 .6 1.6.6l4.2-.3c1 0 1.5.7 1 1.7L38 29c-.3.6-.3 1.4 0 2l1.3 3.8c.4 1 0 1.8-1.2 1.6l-4-.6z" stroke-linecap="round"/><path fill="#FDE5D8" d="M51.6 14.3c-.2-.2-.8-.4-1-.3l-2.8.5c-.7 0-1-.4-.8-1l1-2.8V9.5L46.6 7c-.3-.7 0-1.2.8-1h2.7c.3 0 .8-.2 1-.5l2-2c.6-.5 1-.4 1.3.3l.7 2.8c0 .3.4.8.7 1l2.3 1.2c.7.3.7 1 0 1.3l-2.2 1.7-.6 1-.4 3c-.2.6-.7.8-1.3.4l-2-1.7zM5.4 43.2c-.2-.2-.5-.2-.7-.2l-1.8.3c-.6 0-1-.2-.7-.7l.7-1.8V40l-1-1.7c0-.4 0-.7.6-.7h1.8c.3 0 .6 0 .8-.2L6.5 36c.3-.3.7-.2.8.2l.5 2 .5.5 1.6.8c.3.2.3.7 0 1l-1.6 1c-.2 0-.4.4-.4.7l-.4 2c0 .3-.4.5-.8.2l-1.4-1.2zM10.4 9.2C10.2 9 10 9 9.7 9L8 9.3c-.6 0-1-.2-.7-.7L8 6.8V6L7 4.3c0-.4 0-.7.6-.7h1.8c.3 0 .6 0 .8-.2L11.5 2c.3-.3.7-.2.8.2l.5 2 .5.5 1.6.8c.3.2.3.7 0 1l-1.6 1c-.2 0-.4.4-.4.7l-.4 2c0 .3-.4.5-.8.2l-1.4-1.2z"/><rect width="52" height="4" x="43" y="38" fill="#EEE" rx="2"/><rect width="36" height="4" x="43" y="48" fill="#EEE" rx="2"/></g></svg>
diff --git a/app/views/shared/empty_states/_todos_all_done.svg b/app/views/shared/empty_states/icons/_todos_all_done.svg
index 94b5c2e0ea0..94b5c2e0ea0 100644
--- a/app/views/shared/empty_states/_todos_all_done.svg
+++ b/app/views/shared/empty_states/icons/_todos_all_done.svg
diff --git a/app/views/shared/empty_states/_todos_empty.svg b/app/views/shared/empty_states/icons/_todos_empty.svg
index b1e661268fb..b1e661268fb 100644
--- a/app/views/shared/empty_states/_todos_empty.svg
+++ b/app/views/shared/empty_states/icons/_todos_empty.svg
diff --git a/app/views/shared/groups/_group.html.haml b/app/views/shared/groups/_group.html.haml
index 8164f61797c..dd9e433491b 100644
--- a/app/views/shared/groups/_group.html.haml
+++ b/app/views/shared/groups/_group.html.haml
@@ -21,7 +21,7 @@
= icon('users')
= number_with_delimiter(group.users.count)
- %span.visibility-icon.has-tooltip{data: { container: 'body', placement: 'left' }, title: visibility_icon_description(group)}
+ %span.visibility-icon.has-tooltip{ data: { container: 'body', placement: 'left' }, title: visibility_icon_description(group) }
= visibility_level_icon(group.visibility_level, fw: false)
.avatar-container.s40
@@ -32,7 +32,7 @@
- if group_member
as
- %span #{group_member.human_access}
+ %span= group_member.human_access
- if group.description.present?
.description
diff --git a/app/views/shared/icons/_icon_action_cancel.svg b/app/views/shared/icons/_icon_action_cancel.svg
new file mode 100644
index 00000000000..a1f700eb0ff
--- /dev/null
+++ b/app/views/shared/icons/_icon_action_cancel.svg
@@ -0,0 +1 @@
+<svg width="30px" height="30px" viewBox="0 0 30 30" xmlns="http://www.w3.org/2000/svg"><path d="M19.25,14.9765625 C19.25,14.1380166 19.0234398,13.3697952 18.5703125,12.671875 L12.6796875,18.5546875 C13.3932327,19.0182315 14.1666625,19.25 15,19.25 C15.5781279,19.25 16.1289036,19.1367199 16.6523438,18.9101562 C17.1757839,18.6835926 17.6276023,18.3802102 18.0078125,18 C18.3880227,17.6197898 18.690103,17.1653672 18.9140625,16.6367188 C19.138022,16.1080703 19.25,15.5546904 19.25,14.9765625 Z M11.4453125,17.3125 L17.34375,11.421875 C16.6406215,10.9479143 15.8593793,10.7109375 15,10.7109375 C14.2291628,10.7109375 13.5182324,10.9010398 12.8671875,11.28125 C12.2161426,11.6614602 11.7005227,12.1796842 11.3203125,12.8359375 C10.9401023,13.4921908 10.75,14.2057253 10.75,14.9765625 C10.75,15.8203167 10.9817685,16.5989548 11.4453125,17.3125 Z M21,14.9765625 C21,15.7942749 20.8411474,16.5755171 20.5234375,17.3203125 C20.2057276,18.0651079 19.7799506,18.7057265 19.2460938,19.2421875 C18.7122369,19.7786485 18.0742225,20.2057276 17.3320312,20.5234375 C16.58984,20.8411474 15.8125041,21 15,21 C14.1874959,21 13.41016,20.8411474 12.6679688,20.5234375 C11.9257775,20.2057276 11.2877631,19.7786485 10.7539062,19.2421875 C10.2200494,18.7057265 9.79427242,18.0651079 9.4765625,17.3203125 C9.15885258,16.5755171 9,15.7942749 9,14.9765625 C9,14.1588501 9.15885258,13.37891 9.4765625,12.6367188 C9.79427242,11.8945275 10.2200494,11.255211 10.7539062,10.71875 C11.2877631,10.182289 11.9257775,9.75520992 12.6679688,9.4375 C13.41016,9.11979008 14.1874959,8.9609375 15,8.9609375 C15.8125041,8.9609375 16.58984,9.11979008 17.3320312,9.4375 C18.0742225,9.75520992 18.7122369,10.182289 19.2460938,10.71875 C19.7799506,11.255211 20.2057276,11.8945275 20.5234375,12.6367188 C20.8411474,13.37891 21,14.1588501 21,14.9765625 Z"></path></svg>
diff --git a/app/views/shared/icons/_icon_action_play.svg b/app/views/shared/icons/_icon_action_play.svg
new file mode 100644
index 00000000000..6ac192cd7e9
--- /dev/null
+++ b/app/views/shared/icons/_icon_action_play.svg
@@ -0,0 +1 @@
+<svg width="30px" height="30px" viewBox="0 0 30 30" xmlns="http://www.w3.org/2000/svg"><path d="M21.5401786,15.2320328 L11.90625,20.5858274 C11.7950143,20.6486998 11.6994982,20.6559541 11.6196987,20.6075908 C11.5398992,20.5592275 11.5,20.4721748 11.5,20.3464301 L11.5,9.66785867 C11.5,9.54211399 11.5398992,9.45506129 11.6196987,9.40669795 C11.6994982,9.35833462 11.7950143,9.36558901 11.90625,9.42846135 L21.5401786,14.782256 C21.6514142,14.8451283 21.7070312,14.9200904 21.7070312,15.0071444 C21.7070312,15.0941984 21.6514142,15.1691604 21.5401786,15.2320328 Z"></path></svg>
diff --git a/app/views/shared/icons/_icon_action_retry.svg b/app/views/shared/icons/_icon_action_retry.svg
new file mode 100644
index 00000000000..0fa0243f3c0
--- /dev/null
+++ b/app/views/shared/icons/_icon_action_retry.svg
@@ -0,0 +1 @@
+<svg width="30px" height="30px" viewBox="0 0 30 30" xmlns="http://www.w3.org/2000/svg"><path d="M20.6114971,16.0821413 C20.6114971,16.106323 20.6090789,16.1232499 20.6042426,16.1329226 C20.2947172,17.42906 19.6466582,18.4797378 18.6600462,19.2849873 C17.6734341,20.0902369 16.5175677,20.4928556 15.1924122,20.4928556 C14.4863075,20.4928556 13.8031856,20.3598584 13.1430261,20.0938601 C12.4828665,19.8278617 11.8940517,19.4482152 11.376564,18.9549092 L10.4407381,19.8907351 C10.3488478,19.9826254 10.2400319,20.0285699 10.1142872,20.0285699 C9.98854256,20.0285699 9.87972669,19.9826254 9.78783635,19.8907351 C9.69594601,19.7988447 9.65000153,19.6900289 9.65000153,19.5642842 L9.65000153,16.3142842 C9.65000153,16.1885395 9.69594601,16.0797236 9.78783635,15.9878333 C9.87972669,15.895943 9.98854256,15.8499985 10.1142872,15.8499985 L13.3642872,15.8499985 C13.4900319,15.8499985 13.5988478,15.895943 13.6907381,15.9878333 C13.7826285,16.0797236 13.828573,16.1885395 13.828573,16.3142842 C13.828573,16.4400289 13.7826285,16.5488447 13.6907381,16.6407351 L12.6968765,17.6345967 C13.0402562,17.9537947 13.4295752,18.200444 13.8648453,18.374552 C14.3001153,18.5486601 14.7523057,18.6357128 15.2214301,18.6357128 C15.8694988,18.6357128 16.4740315,18.4785343 17.0350462,18.1641726 C17.5960609,17.8498109 18.0458332,17.4169655 18.3843765,16.8656235 C18.4375762,16.7834058 18.5657371,16.5004845 18.7688631,16.0168512 C18.8075538,15.9056155 18.8800977,15.8499985 18.9864971,15.8499985 L20.3793542,15.8499985 C20.4422265,15.8499985 20.4966345,15.8729707 20.5425797,15.9189159 C20.5885248,15.9648611 20.6114971,16.019269 20.6114971,16.0821413 Z M20.7928587,10.2785699 L20.7928587,13.5285699 C20.7928587,13.6543146 20.7469142,13.7631305 20.6550238,13.8550208 C20.5631335,13.9469111 20.4543176,13.9928556 20.328573,13.9928556 L17.078573,13.9928556 C16.9528283,13.9928556 16.8440124,13.9469111 16.7521221,13.8550208 C16.6602317,13.7631305 16.6142872,13.6543146 16.6142872,13.5285699 C16.6142872,13.4028252 16.6602317,13.2940094 16.7521221,13.202119 L17.7532381,12.2010029 C17.0374607,11.5384252 16.1935332,11.2071413 15.2214301,11.2071413 C14.5733614,11.2071413 13.9688287,11.3643198 13.407814,11.6786815 C12.8467993,11.9930432 12.397027,12.4258886 12.0584837,12.9772306 C12.005284,13.0594483 11.8771231,13.3423696 11.6739971,13.8260029 C11.6353064,13.9372386 11.5627625,13.9928556 11.4563631,13.9928556 L10.0127247,13.9928556 C9.9498524,13.9928556 9.89544446,13.9698834 9.84949929,13.9239382 C9.80355412,13.877993 9.78058188,13.8235851 9.78058188,13.7607128 L9.78058188,13.7099315 C10.0949436,12.4137941 10.7478388,11.3631163 11.7392872,10.5578668 C12.7307356,9.75261722 13.8914383,9.34999847 15.2214301,9.34999847 C15.9275348,9.34999847 16.6142839,9.48420472 17.281698,9.75262124 C17.949112,10.0210378 18.541554,10.3994752 19.0590417,10.8879449 L20.0021221,9.95211901 C20.0940124,9.86022867 20.2028283,9.81428419 20.328573,9.81428419 C20.4543176,9.81428419 20.5631335,9.86022867 20.6550238,9.95211901 C20.7469142,10.0440094 20.7928587,10.1528252 20.7928587,10.2785699 Z"></path></svg>
diff --git a/app/views/shared/icons/_icon_action_stop.svg b/app/views/shared/icons/_icon_action_stop.svg
new file mode 100644
index 00000000000..1c8e2fe4156
--- /dev/null
+++ b/app/views/shared/icons/_icon_action_stop.svg
@@ -0,0 +1 @@
+<svg width="30px" height="30px" viewBox="0 0 30 30" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M20.1357204,10.2985704 L20.1357204,19.7271418 C20.1357204,19.8432138 20.0933101,19.9436592 20.0084882,20.0284811 C19.9236664,20.1133029 19.823221,20.1557132 19.707149,20.1557132 L10.2785775,20.1557132 C10.1625055,20.1557132 10.0620601,20.1133029 9.97723825,20.0284811 C9.89241639,19.9436592 9.8500061,19.8432138 9.8500061,19.7271418 L9.8500061,10.2985704 C9.8500061,10.1824984 9.89241639,10.0820529 9.97723825,9.99723107 C10.0620601,9.91240922 10.1625055,9.86999893 10.2785775,9.86999893 L19.707149,9.86999893 C19.823221,9.86999893 19.9236664,9.91240922 20.0084882,9.99723107 C20.0933101,10.0820529 20.1357204,10.1824984 20.1357204,10.2985704 Z"></path></svg>
diff --git a/app/views/shared/icons/_icon_stopwatch.svg b/app/views/shared/icons/_icon_stopwatch.svg
new file mode 100644
index 00000000000..f20de04538e
--- /dev/null
+++ b/app/views/shared/icons/_icon_stopwatch.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 14" enable-background="new 0 0 12 14"><path d="m11.5 2.4l-1.3-1.1-1 1.1 1.4 1.1.9-1.1"/><path d="m6.8 2v-.5h.5v-1.5h-2.6v1.5h.5v.5c-2.9.4-5.2 2.9-5.2 6 0 3.3 2.7 6 6 6s6-2.7 6-6c0-3-2.3-5.6-5.2-6m-.8 10.5c-2.5 0-4.5-2-4.5-4.5s2-4.5 4.5-4.5 4.5 2 4.5 4.5-2 4.5-4.5 4.5"/><path d="m6.2 8.9h-.5c-.1 0-.2-.1-.2-.2v-3.5c0-.1.1-.2.2-.2h.5c.1 0 .2.1.2.2v3.5c0 .1-.1.2-.2.2"/></svg> \ No newline at end of file
diff --git a/app/views/shared/icons/_mattermost_logo.svg.erb b/app/views/shared/icons/_mattermost_logo.svg.erb
new file mode 100644
index 00000000000..83fbd1a407d
--- /dev/null
+++ b/app/views/shared/icons/_mattermost_logo.svg.erb
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="<%= size %>" height="<%= size %>" version="1" viewBox="0 0 501 501"><path d="M236 .7C137.7 7.5 54 68.2 18.2 158.5c-32 81-19.6 172.8 33 242.5 39.8 53 97.2 87 164.3 97 16.5 2.7 48 3.2 63.5 1.2 48.7-6.3 92.2-24.6 129-54.2 13-10.5 33-31.2 42.2-43.7 26.4-35.5 42.8-75.8 49-120.3 1.6-12.3 1.6-48.7 0-61-4-28.3-12-54.8-24.2-79.5-12.8-26-26.5-45.3-46.8-65.8C417.8 64 400.2 49 398.4 49c-.6 0-.4 10.5.3 26l1.3 26 7 8.7c19 23.7 32.8 53.5 38.2 83 2.5 14 3 43 1 55.8-4.5 27.8-15.2 54-31 76.5-8.6 12.2-28 31.6-40.2 40.2-24 17-50 27.6-80 33-10 1.8-49 1.8-59 0-43-7.7-78.8-26-107.2-54.8-29.3-29.7-46.5-64-52.4-104.4-2-14-1.5-42 1-55C90 121.4 132 72 192 49.7c8-3 18.4-5.8 29.5-8.2 1.7-.4 34.4-38 35.3-40.6.3-1-10.2-1-20.8-.4z"/><path d="M322.2 24.6c-1.3.8-8.4 9.3-16 18.7-7.4 9.5-22.4 28-33.2 41.2-51 62.2-66 81.6-70.6 91-6 12-8.4 21-9 33-1.2 19.8 5 36 19 50C222 268 230 273 243 277.2c9 3 10.4 3.2 24 3.2 13.8 0 15 0 22.6-3 23.2-9 39-28.4 45-55.7 2-8.2 2-28.7.4-79.7l-2-72c-1-36.8-1.4-41.8-3-44-2-3-4.8-3.6-7.8-1.4z"/></svg>
diff --git a/app/views/shared/icons/_scroll_down.svg b/app/views/shared/icons/_scroll_down.svg
new file mode 100644
index 00000000000..acf22ac9314
--- /dev/null
+++ b/app/views/shared/icons/_scroll_down.svg
@@ -0,0 +1,3 @@
+<svg width="16" height="33" class="gitlab-icon-scroll-down" viewBox="0 0 16 33" xmlns="http://www.w3.org/2000/svg">
+ <path fill="#ffffff" d="M1.385 5.534v12.47a4.145 4.145 0 0 0 4.144 4.15h4.942a4.151 4.151 0 0 0 4.144-4.15V5.535a4.145 4.145 0 0 0-4.144-4.15H5.53a4.151 4.151 0 0 0-4.144 4.15zM8.88 30.27v-4.351a.688.688 0 0 0-.69-.688.687.687 0 0 0-.69.688v4.334l-1.345-1.346a.69.69 0 0 0-.976.976l2.526 2.526a.685.685 0 0 0 .494.2.685.685 0 0 0 .493-.2l2.526-2.526a.69.69 0 1 0-.976-.976L8.88 30.27zM0 5.534A5.536 5.536 0 0 1 5.529 0h4.942A5.53 5.53 0 0 1 16 5.534v12.47a5.536 5.536 0 0 1-5.529 5.534H5.53A5.53 5.53 0 0 1 0 18.005V5.534zm7 1.01a1 1 0 1 1 2 0v2.143a1 1 0 1 1-2 0V6.544z" fill-rule="evenodd"/>
+</svg>
diff --git a/app/views/shared/icons/_scroll_down_hover_active.svg b/app/views/shared/icons/_scroll_down_hover_active.svg
new file mode 100644
index 00000000000..262576acf54
--- /dev/null
+++ b/app/views/shared/icons/_scroll_down_hover_active.svg
@@ -0,0 +1,3 @@
+<svg width="16" height="33" class="gitlab-icon-scroll-down-hover" viewBox="0 0 16 33" xmlns="http://www.w3.org/2000/svg">
+ <path fill="#ffffff" d="M8.88 30.27v-4.351a.688.688 0 0 0-.69-.688.687.687 0 0 0-.69.688v4.334l-1.345-1.346a.69.69 0 0 0-.976.976l2.526 2.526a.685.685 0 0 0 .494.2.685.685 0 0 0 .493-.2l2.526-2.526a.69.69 0 1 0-.976-.976L8.88 30.27zM0 5.534A5.536 5.536 0 0 1 5.529 0h4.942A5.53 5.53 0 0 1 16 5.534v12.47a5.536 5.536 0 0 1-5.529 5.534H5.53A5.53 5.53 0 0 1 0 18.005V5.534zm7 1.01a1 1 0 1 1 2 0v2.143a1 1 0 1 1-2 0V6.544z" fill-rule="evenodd"/>
+</svg>
diff --git a/app/views/shared/icons/_scroll_up.svg b/app/views/shared/icons/_scroll_up.svg
new file mode 100644
index 00000000000..f11288fd59c
--- /dev/null
+++ b/app/views/shared/icons/_scroll_up.svg
@@ -0,0 +1,3 @@
+<svg width="16" height="33" class="gitlab-icon-scroll-up" viewBox="0 0 16 33" xmlns="http://www.w3.org/2000/svg">
+ <path fill="#ffffff" d="M1.385 14.534v12.47a4.145 4.145 0 0 0 4.144 4.15h4.942a4.151 4.151 0 0 0 4.144-4.15v-12.47a4.145 4.145 0 0 0-4.144-4.15H5.53a4.151 4.151 0 0 0-4.144 4.15zM8.88 2.609V6.96a.688.688 0 0 1-.69.688.687.687 0 0 1-.69-.688V2.627L6.155 3.972a.69.69 0 0 1-.976-.976L7.705.47a.685.685 0 0 1 .494-.2.685.685 0 0 1 .493.2l2.526 2.526a.69.69 0 1 1-.976.976L8.88 2.609zM0 14.534A5.536 5.536 0 0 1 5.529 9h4.942A5.53 5.53 0 0 1 16 14.534v12.47a5.536 5.536 0 0 1-5.529 5.534H5.53A5.53 5.53 0 0 1 0 27.005V14.534zm7 1.01a1 1 0 1 1 2 0v2.143a1 1 0 1 1-2 0v-2.143z" fill-rule="evenodd"/>
+</svg>
diff --git a/app/views/shared/icons/_scroll_up_hover_active.svg b/app/views/shared/icons/_scroll_up_hover_active.svg
new file mode 100644
index 00000000000..4658dbb1bb7
--- /dev/null
+++ b/app/views/shared/icons/_scroll_up_hover_active.svg
@@ -0,0 +1,3 @@
+<svg width="16" height="33" class="gitlab-icon-scroll-up-hover" viewBox="0 0 16 33" xmlns="http://www.w3.org/2000/svg">
+ <path fill="#ffffff" d="M8.88 2.646l1.362 1.362a.69.69 0 0 0 .976-.976L8.692.507A.685.685 0 0 0 8.2.306a.685.685 0 0 0-.494.2L5.179 3.033a.69.69 0 1 0 .976.976L7.5 2.663v4.179c0 .38.306.688.69.688.381 0 .69-.306.69-.688V2.646zM0 14.534A5.536 5.536 0 0 1 5.529 9h4.942A5.53 5.53 0 0 1 16 14.534v12.47a5.536 5.536 0 0 1-5.529 5.534H5.53A5.53 5.53 0 0 1 0 27.005V14.534zm7 1.01a1 1 0 1 1 2 0v2.143a1 1 0 1 1-2 0v-2.143z" fill-rule="evenodd"/>
+</svg>
diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml
index e3503981afe..f17ae9f28eb 100644
--- a/app/views/shared/issuable/_filter.html.haml
+++ b/app/views/shared/issuable/_filter.html.haml
@@ -31,15 +31,16 @@
- if issuable_filter_present?
.filter-item.inline.reset-filters
- %a{href: page_filter_path(without: issuable_filter_params)} Reset filters
+ %a{ href: page_filter_path(without: issuable_filter_params) } Reset filters
.pull-right
- if boards_page
- #js-boards-seach.issue-boards-search
+ #js-boards-search.issue-boards-search
%input.pull-left.form-control{ type: "search", placeholder: "Filter by name...", "v-model" => "filters.search", "debounce" => "250" }
- if can?(current_user, :admin_list, @project)
+ #js-add-issues-btn.pull-right.prepend-left-10
.dropdown.pull-right
- %button.btn.btn-create.js-new-board-list{ type: "button", data: { toggle: "dropdown", labels: labels_filter_path, namespace_path: @project.try(:namespace).try(:path), project_path: @project.try(:path) } }
+ %button.btn.btn-create.btn-inverted.js-new-board-list{ type: "button", data: { toggle: "dropdown", labels: labels_filter_path, namespace_path: @project.try(:namespace).try(:path), project_path: @project.try(:path) } }
Add list
.dropdown-menu.dropdown-menu-paging.dropdown-menu-align-right.dropdown-menu-issues-board-new.dropdown-menu-selectable
= render partial: "shared/issuable/label_page_default", locals: { show_footer: true, show_create: true, show_boards_content: true, title: "Add list" }
@@ -53,26 +54,26 @@
.issues_bulk_update.hide
= form_tag [:bulk_update, @project.namespace.becomes(Namespace), @project, type], method: :post, class: 'bulk-update' do
.filter-item.inline
- = dropdown_tag("Status", options: { toggle_class: "js-issue-status", title: "Change status", dropdown_class: "dropdown-menu-status dropdown-menu-selectable", data: { field_name: "update[state_event]" } } ) do
+ = dropdown_tag("Status", options: { toggle_class: "js-issue-status", title: "Change status", dropdown_class: "dropdown-menu-status dropdown-menu-selectable", data: { field_name: "update[state_event]", default_label: "Status" } } ) do
%ul
%li
- %a{href: "#", data: {id: "reopen"}} Open
+ %a{ href: "#", data: { id: "reopen" } } Open
%li
- %a{href: "#", data: {id: "close"}} Closed
+ %a{ href: "#", data: {id: "close" } } Closed
.filter-item.inline
= dropdown_tag("Assignee", options: { toggle_class: "js-user-search js-update-assignee js-filter-submit js-filter-bulk-update", title: "Assign to", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable",
- placeholder: "Search authors", data: { first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: @project.id, field_name: "update[assignee_id]" } })
+ placeholder: "Search authors", data: { first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: @project.id, field_name: "update[assignee_id]", default_label: "Assignee" } })
.filter-item.inline
- = dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select js-extra-options js-filter-submit js-filter-bulk-update', filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), use_id: true } })
+ = dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select js-extra-options js-filter-submit js-filter-bulk-update', filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", default_label: "Milestone", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), use_id: true } })
.filter-item.inline.labels-filter
- = render "shared/issuable/label_dropdown", classes: ['js-filter-bulk-update', 'js-multiselect'], show_create: false, show_footer: false, extra_options: false, filter_submit: false, data_options: { persist_when_hide: "true", field_name: "update[label_ids][]", show_no: false, show_any: false, use_id: true }
+ = render "shared/issuable/label_dropdown", classes: ['js-filter-bulk-update', 'js-multiselect'], dropdown_title: 'Apply a label', show_create: false, show_footer: false, extra_options: false, filter_submit: false, data_options: { persist_when_hide: "true", field_name: "update[label_ids][]", show_no: false, show_any: false, use_id: true }
.filter-item.inline
- = dropdown_tag("Subscription", options: { toggle_class: "js-subscription-event", title: "Change subscription", dropdown_class: "dropdown-menu-selectable", data: { field_name: "update[subscription_event]" } } ) do
+ = dropdown_tag("Subscription", options: { toggle_class: "js-subscription-event", title: "Change subscription", dropdown_class: "dropdown-menu-selectable", data: { field_name: "update[subscription_event]", default_label: "Subscription" } } ) do
%ul
%li
- %a{href: "#", data: {id: "subscribe"}} Subscribe
+ %a{ href: "#", data: { id: "subscribe" } } Subscribe
%li
- %a{href: "#", data: {id: "unsubscribe"}} Unsubscribe
+ %a{ href: "#", data: { id: "unsubscribe" } } Unsubscribe
= hidden_field_tag 'update[issuable_ids]', []
= hidden_field_tag :state_event, params[:state_event]
@@ -91,5 +92,5 @@
new SubscriptionSelect();
$('form.filter-form').on('submit', function (event) {
event.preventDefault();
- Turbolinks.visit(this.action + '&' + $(this).serialize());
+ gl.utils.visitUrl(this.action + '&' + $(this).serialize());
});
diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml
index bdb00bfa33c..cb92b2e97a7 100644
--- a/app/views/shared/issuable/_form.html.haml
+++ b/app/views/shared/issuable/_form.html.haml
@@ -1,4 +1,5 @@
- form = local_assigns.fetch(:f)
+- commits = local_assigns[:commits]
- project = @target_project || @project
= form_errors(issuable)
@@ -14,7 +15,7 @@
= form.label :title, class: 'control-label'
= render 'shared/issuable/form/template_selector', issuable: issuable
- = render 'shared/issuable/form/title', issuable: issuable, form: form
+ = render 'shared/issuable/form/title', issuable: issuable, form: form, has_wip_commits: commits && commits.detect(&:work_in_progress?)
= render 'shared/issuable/form/description', issuable: issuable, form: form
@@ -42,6 +43,8 @@
= render 'shared/issuable/form/branch_chooser', issuable: issuable, form: form
+= render 'shared/issuable/form/merge_params', issuable: issuable
+
- if @merge_request_for_resolving_discussions
.form-group
.col-sm-10.col-sm-offset-2
@@ -58,7 +61,7 @@
as resolved. Ask someone with sufficient rights to resolve the them.
- is_footer = !(issuable.is_a?(MergeRequest) && issuable.new_record?)
-.row-content-block{class: (is_footer ? "footer-block" : "middle-block")}
+.row-content-block{ class: (is_footer ? "footer-block" : "middle-block") }
- if issuable.new_record?
= form.submit "Submit #{issuable.class.model_name.human.downcase}", class: 'btn btn-create'
- else
@@ -67,7 +70,7 @@
- if !issuable.persisted? && !issuable.project.empty_repo? && (guide_url = contribution_guide_path(issuable.project))
.inline.prepend-left-10
Please review the
- %strong #{link_to 'contribution guidelines', guide_url}
+ %strong= link_to('contribution guidelines', guide_url)
for this project.
- if issuable.new_record?
diff --git a/app/views/shared/issuable/_label_dropdown.html.haml b/app/views/shared/issuable/_label_dropdown.html.haml
index 22b5a6aa11b..93c7fa0c7d6 100644
--- a/app/views/shared/issuable/_label_dropdown.html.haml
+++ b/app/views/shared/issuable/_label_dropdown.html.haml
@@ -19,7 +19,7 @@
= hidden_field_tag data_options[:field_name], use_id ? label.try(:id) : label.try(:title), id: nil
.dropdown
- %button.dropdown-menu-toggle.js-label-select.js-multiselect{class: classes.join(' '), type: "button", data: dropdown_data}
+ %button.dropdown-menu-toggle.js-label-select.js-multiselect{ class: classes.join(' '), type: "button", data: dropdown_data }
%span.dropdown-toggle-text{ class: ("is-default" if selected.nil? || selected.empty?) }
= multi_label_name(selected, "Labels")
= icon('chevron-down')
diff --git a/app/views/shared/issuable/_label_page_default.html.haml b/app/views/shared/issuable/_label_page_default.html.haml
index a8f01026ca5..9a8529c6cbb 100644
--- a/app/views/shared/issuable/_label_page_default.html.haml
+++ b/app/views/shared/issuable/_label_page_default.html.haml
@@ -17,7 +17,7 @@
%ul.dropdown-footer-list
- if can?(current_user, :admin_label, @project)
%li
- %a.dropdown-toggle-page{href: "#"}
+ %a.dropdown-toggle-page{ href: "#" }
Create new label
%li
= link_to namespace_project_labels_path(@project.namespace, @project), :"data-is-link" => true do
diff --git a/app/views/shared/issuable/_milestone_dropdown.html.haml b/app/views/shared/issuable/_milestone_dropdown.html.haml
index 40fe53e6a8d..415361f8fbf 100644
--- a/app/views/shared/issuable/_milestone_dropdown.html.haml
+++ b/app/views/shared/issuable/_milestone_dropdown.html.haml
@@ -3,7 +3,7 @@
- show_menu_above = show_menu_above || false
- selected_text = selected.try(:title) || params[:milestone_title]
- dropdown_title = local_assigns.fetch(:dropdown_title, "Filter by milestone")
-- if selected.present?
+- if selected.present? || params[:milestone_title].present?
= hidden_field_tag(name, name == :milestone_title ? selected_text : selected.id)
= dropdown_tag(milestone_dropdown_label(selected_text), options: { title: dropdown_title, toggle_class: "js-milestone-select js-filter-submit #{extra_class}", filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone",
placeholder: "Search milestones", footer_content: project.present?, data: { show_no: true, show_menu_above: show_menu_above, show_any: show_any, show_upcoming: show_upcoming, field_name: name, selected: selected.try(:title), project_id: project.try(:id), milestones: milestones_filter_dropdown_path, default_label: "Milestone" } }) do
diff --git a/app/views/shared/issuable/_nav.html.haml b/app/views/shared/issuable/_nav.html.haml
index d938edf4dbd..1154316c03f 100644
--- a/app/views/shared/issuable/_nav.html.haml
+++ b/app/views/shared/issuable/_nav.html.haml
@@ -3,23 +3,23 @@
- issuables = @issues || @merge_requests
%ul.nav-links.issues-state-filters
- %li{class: ("active" if params[:state] == 'opened')}>
+ %li{ class: ("active" if params[:state] == 'opened') }>
= link_to page_filter_path(state: 'opened', label: true), id: 'state-opened', title: "Filter by #{page_context_word} that are currently opened." do
#{issuables_state_counter_text(type, :opened)}
- if type == :merge_requests
- %li{class: ("active" if params[:state] == 'merged')}>
+ %li{ class: ("active" if params[:state] == 'merged') }>
= link_to page_filter_path(state: 'merged', label: true), id: 'state-merged', title: 'Filter by merge requests that are currently merged.' do
#{issuables_state_counter_text(type, :merged)}
- %li{class: ("active" if params[:state] == 'closed')}>
+ %li{ class: ("active" if params[:state] == 'closed') }>
= link_to page_filter_path(state: 'closed', label: true), id: 'state-closed', title: 'Filter by merge requests that are currently closed and unmerged.' do
#{issuables_state_counter_text(type, :closed)}
- else
- %li{class: ("active" if params[:state] == 'closed')}>
+ %li{ class: ("active" if params[:state] == 'closed') }>
= link_to page_filter_path(state: 'closed', label: true), id: 'state-all', title: 'Filter by issues that are currently closed.' do
#{issuables_state_counter_text(type, :closed)}
- %li{class: ("active" if params[:state] == 'all')}>
+ %li{ class: ("active" if params[:state] == 'all') }>
= link_to page_filter_path(state: 'all', label: true), id: 'state-all', title: "Show all #{page_context_word}." do
#{issuables_state_counter_text(type, :all)}
diff --git a/app/views/shared/issuable/_participants.html.haml b/app/views/shared/issuable/_participants.html.haml
index 33a9a494857..171da899937 100644
--- a/app/views/shared/issuable/_participants.html.haml
+++ b/app/views/shared/issuable/_participants.html.haml
@@ -13,8 +13,8 @@
.participants-author.js-participants-author
= link_to_member(@project, participant, name: false, size: 24)
- if participants_extra > 0
- %div.participants-more
- %a.js-participants-more{href: "#", data: {original_text: "+ #{participants_size - 7} more", less_text: "- show less"}}
+ .participants-more
+ %a.js-participants-more{ href: "#", data: { original_text: "+ #{participants_size - 7} more", less_text: "- show less" } }
+ #{participants_extra} more
:javascript
IssuableContext.prototype.PARTICIPANTS_ROW_COUNT = #{participants_row};
diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml
new file mode 100644
index 00000000000..173fa922f56
--- /dev/null
+++ b/app/views/shared/issuable/_search_bar.html.haml
@@ -0,0 +1,133 @@
+- type = local_assigns.fetch(:type)
+
+.issues-filters
+ .issues-details-filters.row-content-block.second-block.filtered-search-block
+ = form_tag page_filter_path(without: [:assignee_id, :author_id, :milestone_title, :label_name, :search]), method: :get, class: 'filter-form js-filter-form' do
+ - if params[:search].present?
+ = hidden_field_tag :search, params[:search]
+ - if @bulk_edit
+ .check-all-holder
+ = check_box_tag "check_all_issues", nil, false,
+ class: "check_all_issues left"
+ .issues-other-filters.filtered-search-container
+ .filtered-search-input-container
+ %input.form-control.filtered-search{ placeholder: 'Search or filter results...', 'data-id' => 'filtered-search', 'data-project-id' => @project.id, 'data-username-params' => @users.to_json(only: [:id, :username]) }
+ = icon('filter')
+ %button.clear-search.hidden{ type: 'button' }
+ = icon('times')
+ #js-dropdown-hint.dropdown-menu.hint-dropdown
+ %ul{ 'data-dropdown' => true }
+ %li.filter-dropdown-item{ 'data-action' => 'submit' }
+ %button.btn.btn-link
+ = icon('search')
+ %span
+ Keep typing and press Enter
+ %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true }
+ %li.filter-dropdown-item
+ %button.btn.btn-link
+ -# Encapsulate static class name `{{icon}}` inside #{} to bypass
+ -# haml lint's ClassAttributeWithStaticValue
+ %i.fa{ class: "#{'{{icon}}'}" }
+ %span.js-filter-hint
+ {{hint}}
+ %span.js-filter-tag.dropdown-light-content
+ {{tag}}
+ #js-dropdown-author.dropdown-menu
+ %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true }
+ %li.filter-dropdown-item
+ %button.btn.btn-link.dropdown-user
+ %img.avatar.avatar-inline{ 'data-src' => '{{avatar_url}}', alt: '{{name}}\'s avatar', width: '30' }
+ .dropdown-user-details
+ %span
+ {{name}}
+ %span.dropdown-light-content
+ @{{username}}
+ #js-dropdown-assignee.dropdown-menu
+ %ul{ 'data-dropdown' => true }
+ %li.filter-dropdown-item{ 'data-value' => 'none' }
+ %button.btn.btn-link
+ No Assignee
+ %li.divider
+ %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true }
+ %li.filter-dropdown-item
+ %button.btn.btn-link.dropdown-user
+ %img.avatar.avatar-inline{ 'data-src' => '{{avatar_url}}', alt: '{{name}}\'s avatar', width: '30' }
+ .dropdown-user-details
+ %span
+ {{name}}
+ %span.dropdown-light-content
+ @{{username}}
+ #js-dropdown-milestone.dropdown-menu{ 'data-dropdown' => true }
+ %ul{ 'data-dropdown' => true }
+ %li.filter-dropdown-item{ 'data-value' => 'none' }
+ %button.btn.btn-link
+ No Milestone
+ %li.filter-dropdown-item{ 'data-value' => 'upcoming' }
+ %button.btn.btn-link
+ Upcoming
+ %li.divider
+ %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true }
+ %li.filter-dropdown-item
+ %button.btn.btn-link.js-data-value
+ {{title}}
+ #js-dropdown-label.dropdown-menu{ 'data-dropdown' => true }
+ %ul{ 'data-dropdown' => true }
+ %li.filter-dropdown-item{ 'data-value' => 'none' }
+ %button.btn.btn-link
+ No Label
+ %li.divider
+ %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true }
+ %li.filter-dropdown-item
+ %button.btn.btn-link
+ %span.dropdown-label-box{ style: 'background: {{color}}' }
+ %span.label-title.js-data-value
+ {{title}}
+ .pull-right
+ = render 'shared/sort_dropdown'
+
+ - if @bulk_edit
+ .issues_bulk_update.hide
+ = form_tag [:bulk_update, @project.namespace.becomes(Namespace), @project, type], method: :post, class: 'bulk-update' do
+ .filter-item.inline
+ = dropdown_tag("Status", options: { toggle_class: "js-issue-status", title: "Change status", dropdown_class: "dropdown-menu-status dropdown-menu-selectable", data: { field_name: "update[state_event]" } } ) do
+ %ul
+ %li
+ %a{ href: "#", data: { id: "reopen" } } Open
+ %li
+ %a{ href: "#", data: { id: "close" } } Closed
+ .filter-item.inline
+ = dropdown_tag("Assignee", options: { toggle_class: "js-user-search js-update-assignee js-filter-submit js-filter-bulk-update", title: "Assign to", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable",
+ placeholder: "Search authors", data: { first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: @project.id, field_name: "update[assignee_id]" } })
+ .filter-item.inline
+ = dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select js-extra-options js-filter-submit js-filter-bulk-update', filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), use_id: true } })
+ .filter-item.inline.labels-filter
+ = render "shared/issuable/label_dropdown", classes: ['js-filter-bulk-update', 'js-multiselect'], dropdown_title: 'Apply a label', show_create: false, show_footer: false, extra_options: false, filter_submit: false, data_options: { persist_when_hide: "true", field_name: "update[label_ids][]", show_no: false, show_any: false, use_id: true }
+ .filter-item.inline
+ = dropdown_tag("Subscription", options: { toggle_class: "js-subscription-event", title: "Change subscription", dropdown_class: "dropdown-menu-selectable", data: { field_name: "update[subscription_event]" } } ) do
+ %ul
+ %li
+ %a{ href: "#", data: { id: "subscribe" } } Subscribe
+ %li
+ %a{ href: "#", data: { id: "unsubscribe" } } Unsubscribe
+
+ = hidden_field_tag 'update[issuable_ids]', []
+ = hidden_field_tag :state_event, params[:state_event]
+ .filter-item.inline
+ = button_tag "Update #{type.to_s.humanize(capitalize: false)}", class: "btn update_selected_issues btn-save"
+
+:javascript
+ new UsersSelect();
+ new LabelsSelect();
+ new MilestoneSelect();
+ new IssueStatusSelect();
+ new SubscriptionSelect();
+
+ $(document).off('page:restore').on('page:restore', function (event) {
+ if (gl.FilteredSearchManager) {
+ new gl.FilteredSearchManager();
+ }
+ Issuable.init();
+ new gl.IssuableBulkActions({
+ prefixId: 'issue_',
+ });
+ });
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 9fe1be5a597..77fc44fa5cc 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -1,5 +1,8 @@
- todo = issuable_todo(issuable)
-%aside.right-sidebar{ class: sidebar_gutter_collapsed_class }
+- content_for :page_specific_javascripts do
+ = page_specific_javascript_bundle_tag('issuable')
+
+%aside.right-sidebar{ class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' }
.issuable-sidebar
- can_edit_issuable = can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
.block.issuable-sidebar-header
@@ -17,9 +20,9 @@
Add todo
= icon('spin spinner', class: 'hidden js-issuable-todo-loading')
- = form_for [@project.namespace.becomes(Namespace), @project, issuable], remote: true, format: :json, html: {class: 'issuable-context-form inline-update js-issuable-update'} do |f|
+ = 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.name 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
@@ -54,7 +57,7 @@
= icon('clock-o')
%span
- if issuable.milestone
- %span.has-tooltip{title: milestone_remaining_days(issuable.milestone), data: {container: 'body', html: 1, placement: 'left'}}
+ %span.has-tooltip{ title: milestone_remaining_days(issuable.milestone), data: { container: 'body', html: 1, placement: 'left' } }
= issuable.milestone.title
- else
None
@@ -72,7 +75,13 @@
.selectbox.hide-collapsed
= f.hidden_field 'milestone_id', value: issuable.milestone_id, id: nil
= dropdown_tag('Milestone', options: { title: 'Assign milestone', toggle_class: 'js-milestone-select js-extra-options', filter: true, dropdown_class: 'dropdown-menu-selectable', placeholder: 'Search milestones', data: { show_no: true, field_name: "#{issuable.to_ability_name}[milestone_id]", project_id: @project.id, issuable_id: issuable.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), ability_name: issuable.to_ability_name, issue_update: issuable_json_path(issuable), use_id: true }})
-
+ - if issuable.has_attribute?(:time_estimate)
+ #issuable-time-tracker.block
+ %issuable-time-tracker{ ':time_estimate' => 'issuable.time_estimate', ':time_spent' => 'issuable.total_time_spent', ':human_time_estimate' => 'issuable.human_time_estimate', ':human_time_spent' => 'issuable.human_total_time_spent', 'stopwatch-svg' => custom_icon('icon_stopwatch'), 'docs-url' => help_page_path('workflow/time_tracking.md') }
+ // Fallback while content is loading
+ .title.hide-collapsed
+ Time tracking
+ = icon('spinner spin')
- if issuable.has_attribute?(:due_date)
.block.due_date
.sidebar-collapsed-icon
@@ -97,7 +106,7 @@
remove due date
- if can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
.selectbox.hide-collapsed
- = f.hidden_field :due_date, value: issuable.due_date
+ = f.hidden_field :due_date, value: issuable.due_date.try(:strftime, 'yy-mm-dd')
.dropdown
%button.dropdown-menu-toggle.js-due-date-select{ type: 'button', data: { toggle: 'dropdown', field_name: "#{issuable.to_ability_name}[due_date]", ability_name: issuable.to_ability_name, issue_update: issuable_json_path(issuable) } }
%span.dropdown-toggle-text Due date
@@ -122,15 +131,15 @@
.value.issuable-show-labels.hide-collapsed{ class: ("has-labels" if selected_labels.any?) }
- if selected_labels.any?
- selected_labels.each do |label|
- = link_to_label(label, type: issuable.to_ability_name)
+ = link_to_label(label, subject: issuable.project, type: issuable.to_ability_name)
- else
%span.no-value None
.selectbox.hide-collapsed
- selected_labels.each do |label|
= hidden_field_tag "#{issuable.to_ability_name}[label_names][]", label.id, id: nil
.dropdown
- %button.dropdown-menu-toggle.js-label-select.js-multiselect.js-label-sidebar-dropdown{type: "button", data: {toggle: "dropdown", default_label: "Labels", field_name: "#{issuable.to_ability_name}[label_names][]", ability_name: issuable.to_ability_name, show_no: "true", show_any: "true", namespace_path: @project.try(:namespace).try(:path), project_path: @project.try(:path), issue_update: issuable_json_path(issuable), labels: (namespace_project_labels_path(@project.namespace, @project, :json) if @project)}}
- %span.dropdown-toggle-text{ class: ("is-default" if selected_labels.empty?)}
+ %button.dropdown-menu-toggle.js-label-select.js-multiselect.js-label-sidebar-dropdown{ type: "button", data: {toggle: "dropdown", default_label: "Labels", field_name: "#{issuable.to_ability_name}[label_names][]", ability_name: issuable.to_ability_name, show_no: "true", show_any: "true", namespace_path: @project.try(:namespace).try(:path), project_path: @project.try(:path), issue_update: issuable_json_path(issuable), labels: (namespace_project_labels_path(@project.namespace, @project, :json) if @project) } }
+ %span.dropdown-toggle-text{ class: ("is-default" if selected_labels.empty?) }
= multi_label_name(selected_labels, "Labels")
= icon('chevron-down')
.dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable
@@ -141,7 +150,7 @@
= render "shared/issuable/participants", participants: issuable.participants(current_user)
- if current_user
- subscribed = issuable.subscribed?(current_user, @project)
- .block.light.subscription{data: {url: toggle_subscription_path(issuable)}}
+ .block.light.subscription{ data: { url: toggle_subscription_path(issuable) } }
.sidebar-collapsed-icon
= icon('rss')
%span.issuable-header-text.hide-collapsed.pull-left
@@ -153,15 +162,17 @@
- project_ref = cross_project_reference(@project, issuable)
.block.project-reference
.sidebar-collapsed-icon.dont-change-state
- = clipboard_button(clipboard_text: project_ref)
+ = clipboard_button(clipboard_text: project_ref, title: "Copy reference to clipboard", placement: "left")
.cross-project-reference.hide-collapsed
%span
Reference:
- %cite{title: project_ref}
+ %cite{ title: project_ref }
= project_ref
- = clipboard_button(clipboard_text: project_ref)
+ = clipboard_button(clipboard_text: project_ref, title: "Copy reference to clipboard", placement: "left")
:javascript
+ gl.IssuableResource = new gl.SubbableResource('#{issuable_json_path(issuable)}');
+ new gl.IssuableTimeTracking("#{escape_javascript(serialize_issuable(issuable))}");
new MilestoneSelect('{"namespace":"#{@project.namespace.path}","path":"#{@project.path}"}');
new LabelsSelect();
new IssuableContext('#{escape_javascript(current_user.to_json(only: [:username, :id, :name]))}');
diff --git a/app/views/shared/issuable/form/_branch_chooser.html.haml b/app/views/shared/issuable/form/_branch_chooser.html.haml
index b757893ea04..2793e7bcff4 100644
--- a/app/views/shared/issuable/form/_branch_chooser.html.haml
+++ b/app/views/shared/issuable/form/_branch_chooser.html.haml
@@ -19,12 +19,3 @@
- if issuable.new_record?
&nbsp;
= link_to 'Change branches', mr_change_branches_path(issuable)
-
-- if issuable.can_remove_source_branch?(current_user)
- .form-group
- .col-sm-10.col-sm-offset-2
- .checkbox
- = label_tag 'merge_request[force_remove_source_branch]' do
- = hidden_field_tag 'merge_request[force_remove_source_branch]', '0', id: nil
- = check_box_tag 'merge_request[force_remove_source_branch]', '1', issuable.force_remove_source_branch?
- Remove source branch when merge request is accepted.
diff --git a/app/views/shared/issuable/form/_merge_params.html.haml b/app/views/shared/issuable/form/_merge_params.html.haml
new file mode 100644
index 00000000000..03309722326
--- /dev/null
+++ b/app/views/shared/issuable/form/_merge_params.html.haml
@@ -0,0 +1,16 @@
+- issuable = local_assigns.fetch(:issuable)
+
+- return unless issuable.is_a?(MergeRequest)
+- return if issuable.closed_without_fork?
+
+-# This check is duplicated below, to avoid conflicts with EE.
+- return unless issuable.can_remove_source_branch?(current_user)
+
+.form-group
+ .col-sm-10.col-sm-offset-2
+ - if issuable.can_remove_source_branch?(current_user)
+ .checkbox
+ = label_tag 'merge_request[force_remove_source_branch]' do
+ = hidden_field_tag 'merge_request[force_remove_source_branch]', '0', id: nil
+ = check_box_tag 'merge_request[force_remove_source_branch]', '1', issuable.force_remove_source_branch?
+ Remove source branch when merge request is accepted.
diff --git a/app/views/shared/issuable/form/_title.html.haml b/app/views/shared/issuable/form/_title.html.haml
index 83efdc7c8f7..64826d41d60 100644
--- a/app/views/shared/issuable/form/_title.html.haml
+++ b/app/views/shared/issuable/form/_title.html.haml
@@ -1,4 +1,5 @@
- issuable = local_assigns.fetch(:issuable)
+- has_wip_commits = local_assigns.fetch(:has_wip_commits)
- form = local_assigns.fetch(:form)
- no_issuable_templates = issuable_templates(issuable).empty?
- div_class = no_issuable_templates ? 'col-sm-10' : 'col-sm-7 col-lg-8'
@@ -18,6 +19,9 @@
%strong Work In Progress
merge request to be merged when it's ready.
.js-no-wip-explanation
+ - if has_wip_commits
+ It looks like you have some WIP commits in this branch.
+ %br
%a.js-toggle-wip{ href: '', tabindex: -1 }
Start the title with
%code WIP:
diff --git a/app/views/shared/members/_group.html.haml b/app/views/shared/members/_group.html.haml
index 8928de9097b..81b5bc1de30 100644
--- a/app/views/shared/members/_group.html.haml
+++ b/app/views/shared/members/_group.html.haml
@@ -3,7 +3,7 @@
- can_admin_member = can?(current_user, :admin_project_member, @project)
- dom_id = "group_member_#{group_link.id}"
%li.member.group_member{ id: dom_id }
- %span{ class: "list-item-name" }
+ %span.list-item-name
= image_tag group_icon(group), class: "avatar s40", alt: ''
%strong
= link_to group.name, group_path(group)
@@ -37,7 +37,6 @@
%i.clear-icon.js-clear-input
- if can_admin_member
= link_to namespace_project_group_link_path(@project.namespace, @project, group_link),
- remote: true,
method: :delete,
data: { confirm: "Are you sure you want to remove #{group.name}?" },
class: 'btn btn-remove prepend-left-10' do
diff --git a/app/views/shared/milestones/_issuable.html.haml b/app/views/shared/milestones/_issuable.html.haml
index 9e1b0379428..28935c8b598 100644
--- a/app/views/shared/milestones/_issuable.html.haml
+++ b/app/views/shared/milestones/_issuable.html.haml
@@ -14,15 +14,15 @@
- if issuable.is_a?(Issue)
= confidential_icon(issuable)
= link_to_gfm issuable.title, [project.namespace.becomes(Namespace), project, issuable], title: issuable.title
- %div{class: 'issuable-detail'}
+ .issuable-detail
= link_to [project.namespace.becomes(Namespace), project, issuable] do
- %span{ class: 'issuable-number' }>= issuable.to_reference
+ %span.issuable-number= issuable.to_reference
- issuable.labels.each do |label|
= link_to polymorphic_path(base_url_args, { milestone_title: @milestone.title, label_name: label.title, state: 'all' }) do
- render_colored_label(label)
- %span{ class: "assignee-icon" }
+ %span.assignee-icon
- if assignee
= link_to polymorphic_path(base_url_args, { milestone_title: @milestone.title, assignee_id: issuable.assignee_id, state: 'all' }),
class: 'has-tooltip', title: "Assigned to #{assignee.name}", data: { container: 'body' } do
diff --git a/app/views/shared/milestones/_issuables.html.haml b/app/views/shared/milestones/_issuables.html.haml
index 15ff5b8a27e..31eb07ca666 100644
--- a/app/views/shared/milestones/_issuables.html.haml
+++ b/app/views/shared/milestones/_issuables.html.haml
@@ -8,7 +8,7 @@
= title
- if show_counter
.right
- = issuables.size
+ = number_with_delimiter(issuables.size)
- class_prefix = dom_class(issuables).pluralize
%ul{ class: "well-list #{class_prefix}-sortable-list", id: "#{class_prefix}-list-#{id}", "data-state" => id }
diff --git a/app/views/shared/milestones/_milestone.html.haml b/app/views/shared/milestones/_milestone.html.haml
index 3dccfb147bf..9e6a76e1ddb 100644
--- a/app/views/shared/milestones/_milestone.html.haml
+++ b/app/views/shared/milestones/_milestone.html.haml
@@ -1,7 +1,7 @@
- dashboard = local_assigns[:dashboard]
- custom_dom_id = dom_id(@project ? milestone : milestone.milestones.first)
-%li{class: "milestone milestone-#{milestone.closed? ? 'closed' : 'open'}", id: custom_dom_id }
+%li{ class: "milestone milestone-#{milestone.closed? ? 'closed' : 'open'}", id: custom_dom_id }
.row
.col-sm-6
%strong= link_to_gfm truncate(milestone.title, length: 100), milestone_path
@@ -9,7 +9,7 @@
.pull-right.light #{milestone.percent_complete(current_user)}% complete
.row
.col-sm-6
- = link_to pluralize(milestone.issues_visible_to_user(current_user).size, 'Issue'), issues_path
+ = link_to pluralize(milestone.total_issues_count(current_user), 'Issue'), issues_path
&middot;
= link_to pluralize(milestone.merge_requests.size, 'Merge Request'), merge_requests_path
.col-sm-6= milestone_progress_bar(milestone)
diff --git a/app/views/shared/notifications/_custom_notifications.html.haml b/app/views/shared/notifications/_custom_notifications.html.haml
index a82fc95df84..a736bfd91e2 100644
--- a/app/views/shared/notifications/_custom_notifications.html.haml
+++ b/app/views/shared/notifications/_custom_notifications.html.haml
@@ -18,7 +18,7 @@
%p
Custom notification levels are the same as participating levels. With custom notification levels you will also receive notifications for select events. To find out more, check out
= succeed "." do
- %a{ href: "http://docs.gitlab.com/ce/workflow/notifications.html", target: "_blank"} notification emails
+ %a{ href: help_page_path('workflow/notifications'), target: "_blank" } notification emails
.col-lg-8
- NotificationSetting::EMAIL_EVENTS.each_with_index do |event, index|
- field_id = "#{notifications_menu_identifier("modal", notification_setting)}_notification_setting[#{event}]"
diff --git a/app/views/shared/projects/_dropdown.html.haml b/app/views/shared/projects/_dropdown.html.haml
index b7f8551153b..ac028f18e50 100644
--- a/app/views/shared/projects/_dropdown.html.haml
+++ b/app/views/shared/projects/_dropdown.html.haml
@@ -2,7 +2,7 @@
- personal = params[:personal]
- archived = params[:archived]
- namespace_id = params[:namespace_id]
-.dropdown.inline
+.dropdown
- toggle_text = projects_sort_options_hash[@sort]
= dropdown_toggle(toggle_text, { toggle: 'dropdown' }, { id: 'sort-projects-dropdown' })
%ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-selectable
diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml
index 264391fe84f..4a27965754d 100644
--- a/app/views/shared/projects/_project.html.haml
+++ b/app/views/shared/projects/_project.html.haml
@@ -25,7 +25,7 @@
%span
= icon('star')
= number_with_delimiter(project.star_count)
- %span.visibility-icon.has-tooltip{data: { container: 'body', placement: 'left' }, title: visibility_icon_description(project)}
+ %span.visibility-icon.has-tooltip{ data: { container: 'body', placement: 'left' }, title: visibility_icon_description(project) }
= visibility_level_icon(project.visibility_level, fw: true)
.title
diff --git a/app/views/shared/snippets/_blob.html.haml b/app/views/shared/snippets/_blob.html.haml
index dcdba01aee9..ad5c0c2d8c8 100644
--- a/app/views/shared/snippets/_blob.html.haml
+++ b/app/views/shared/snippets/_blob.html.haml
@@ -1,6 +1,6 @@
- unless @snippet.content.empty?
- if markup?(@snippet.file_name)
- %textarea.markdown-snippet-copy.blob-content{data: {blob_id: @snippet.id}}
+ %textarea.markdown-snippet-copy.blob-content{ data: { blob_id: @snippet.id } }
= @snippet.content
.file-content.wiki
- if gitlab_markdown?(@snippet.file_name)
diff --git a/app/views/shared/snippets/_form.html.haml b/app/views/shared/snippets/_form.html.haml
index 0c788032020..56c0f7390a5 100644
--- a/app/views/shared/snippets/_form.html.haml
+++ b/app/views/shared/snippets/_form.html.haml
@@ -1,6 +1,6 @@
- content_for :page_specific_javascripts do
= page_specific_javascript_tag('lib/ace.js')
- = page_specific_javascript_tag('snippet/snippet_bundle.js')
+ = page_specific_javascript_bundle_tag('snippet')
.snippet-form-holder
= form_for @snippet, url: url, html: { class: "form-horizontal snippet-form js-requires-input" } do |f|
@@ -11,7 +11,7 @@
.col-sm-10
= f.text_field :title, class: 'form-control', required: true, autofocus: true
- = render 'shared/visibility_level', f: f, visibility_level: visibility_level, can_change_visibility_level: true, form_model: @snippet
+ = render 'shared/visibility_level', f: f, visibility_level: @snippet.visibility_level, can_change_visibility_level: true, form_model: @snippet
.file-editor
.form-group
@@ -34,4 +34,3 @@
= link_to "Cancel", namespace_project_snippets_path(@project.namespace, @project), class: "btn btn-cancel"
- else
= link_to "Cancel", snippets_path(@project), class: "btn btn-cancel"
-
diff --git a/app/views/shared/tokens/_scopes_form.html.haml b/app/views/shared/tokens/_scopes_form.html.haml
index 5074afb63a1..8bbaf431536 100644
--- a/app/views/shared/tokens/_scopes_form.html.haml
+++ b/app/views/shared/tokens/_scopes_form.html.haml
@@ -5,5 +5,5 @@
- scopes.each do |scope|
%fieldset
= check_box_tag "#{prefix}[scopes][]", scope, token.scopes.include?(scope), id: "#{prefix}_scopes_#{scope}"
- = label_tag "#{prefix}_scopes_#{scope}", scope
- %span= "(#{t(scope, scope: [:doorkeeper, :scopes])})"
+ = label_tag ("#{prefix}_scopes_#{scope}"), scope
+ %span= t(scope, scope: [:doorkeeper, :scopes])
diff --git a/app/views/shared/web_hooks/_form.html.haml b/app/views/shared/web_hooks/_form.html.haml
index 5d659eb83a9..37e2a377a69 100644
--- a/app/views/shared/web_hooks/_form.html.haml
+++ b/app/views/shared/web_hooks/_form.html.haml
@@ -1,12 +1,9 @@
-- page_title "Webhooks"
-- context_title = @project ? 'project' : 'group'
-
.row.prepend-top-default
.col-lg-3
%h4.prepend-top-0
= page_title
%p
- #{link_to "Webhooks", help_page_path("web_hooks/web_hooks")} can be
+ #{link_to "Webhooks", help_page_path("user/project/integrations/webhooks")} can be
used for binding events when something is happening within the project.
.col-lg-9.append-bottom-default
= form_for hook, as: :hook, url: polymorphic_path(url_components + [:hooks]) do |f|
@@ -69,9 +66,9 @@
= f.check_box :build_events, class: 'pull-left'
.prepend-left-20
= f.label :build_events, class: 'list-label' do
- %strong Build events
+ %strong Jobs events
%p.light
- This URL will be triggered when the build status changes
+ This URL will be triggered when the job status changes
%li
= f.check_box :pipeline_events, class: 'pull-left'
.prepend-left-20
diff --git a/app/views/sherlock/file_samples/show.html.haml b/app/views/sherlock/file_samples/show.html.haml
index 94d4dd4fa7d..92151176fce 100644
--- a/app/views/sherlock/file_samples/show.html.haml
+++ b/app/views/sherlock/file_samples/show.html.haml
@@ -41,7 +41,7 @@
%th= t('sherlock.percent')
%tbody
- @file_sample.line_samples.each_with_index do |sample, index|
- %tr{class: sample.majority_of?(@file_sample.duration) ? 'slow' : ''}
+ %tr{ class: sample.majority_of?(@file_sample.duration) ? 'slow' : '' }
%td= index + 1
%td= sample.events
%td
diff --git a/app/views/sherlock/queries/_general.html.haml b/app/views/sherlock/queries/_general.html.haml
index 7073c0f4d90..5a447f791dc 100644
--- a/app/views/sherlock/queries/_general.html.haml
+++ b/app/views/sherlock/queries/_general.html.haml
@@ -26,7 +26,7 @@
.panel.panel-default
.panel-heading
.pull-right
- %button.js-clipboard-trigger.btn.btn-xs{title: t('sherlock.copy_to_clipboard'), type: :button}
+ %button.js-clipboard-trigger.btn.btn-xs{ title: t('sherlock.copy_to_clipboard'), type: :button }
%i.fa.fa-clipboard
%pre.hidden
= @query.formatted_query
@@ -41,7 +41,7 @@
.panel.panel-default
.panel-heading
.pull-right
- %button.js-clipboard-trigger.btn.btn-xs{title: t('sherlock.copy_to_clipboard'), type: :button}
+ %button.js-clipboard-trigger.btn.btn-xs{ title: t('sherlock.copy_to_clipboard'), type: :button }
%i.fa.fa-clipboard
%pre.hidden
= @query.explain
diff --git a/app/views/sherlock/queries/show.html.haml b/app/views/sherlock/queries/show.html.haml
index fc2863dca8e..c45da6ee9a4 100644
--- a/app/views/sherlock/queries/show.html.haml
+++ b/app/views/sherlock/queries/show.html.haml
@@ -3,10 +3,10 @@
%ul.nav-links
%li.active
- %a(href="#tab-general" data-toggle="tab")
+ %a{ href: "#tab-general", data: { toggle: "tab" } }
= t('sherlock.general')
%li
- %a(href="#tab-backtrace" data-toggle="tab")
+ %a{ href: "#tab-backtrace", data: { toggle: "tab" } }
= t('sherlock.backtrace')
.row-content-block
diff --git a/app/views/sherlock/transactions/index.html.haml b/app/views/sherlock/transactions/index.html.haml
index da969c02765..bc05659dfa8 100644
--- a/app/views/sherlock/transactions/index.html.haml
+++ b/app/views/sherlock/transactions/index.html.haml
@@ -28,7 +28,7 @@
%tr
%td= trans.type
%td
- %span{title: trans.path}
+ %span{ title: trans.path }
= truncate(trans.path, length: 70)
%td
= trans.duration.round(2)
diff --git a/app/views/sherlock/transactions/show.html.haml b/app/views/sherlock/transactions/show.html.haml
index 8aa6b437d95..eab91e8fbe4 100644
--- a/app/views/sherlock/transactions/show.html.haml
+++ b/app/views/sherlock/transactions/show.html.haml
@@ -3,15 +3,15 @@
%ul.nav-links
%li.active
- %a(href="#tab-general" data-toggle="tab")
+ %a{ href: "#tab-general", data: { toggle: "tab" } }
= t('sherlock.general')
%li
- %a(href="#tab-queries" data-toggle="tab")
+ %a{ href: "#tab-queries", data: { toggle: "tab" } }
= t('sherlock.queries')
%span.badge
#{@transaction.queries.length}
%li
- %a(href="#tab-file-samples" data-toggle="tab")
+ %a{ href: "#tab-file-samples", data: { toggle: "tab" } }
= t('sherlock.file_samples')
%span.badge
#{@transaction.file_samples.length}
diff --git a/app/views/snippets/_actions.html.haml b/app/views/snippets/_actions.html.haml
index 95fc7198104..855a995afa9 100644
--- a/app/views/snippets/_actions.html.haml
+++ b/app/views/snippets/_actions.html.haml
@@ -1,3 +1,5 @@
+- return unless current_user
+
.hidden-xs
- if can?(current_user, :update_personal_snippet, @snippet)
= link_to edit_snippet_path(@snippet), class: "btn btn-grouped" do
@@ -5,24 +7,27 @@
- if can?(current_user, :admin_personal_snippet, @snippet)
= link_to snippet_path(@snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-inverted btn-remove", title: 'Delete Snippet' do
Delete
- - if current_user
- = link_to new_snippet_path, class: "btn btn-grouped btn-inverted btn-create", title: "New snippet" do
- New snippet
-- if current_user
- .visible-xs-block.dropdown
- %button.btn.btn-default.btn-block.append-bottom-0.prepend-top-5{ data: { toggle: "dropdown" } }
- Options
- = icon('caret-down')
- .dropdown-menu.dropdown-menu-full-width
- %ul
+ = link_to new_snippet_path, class: "btn btn-grouped btn-inverted btn-create", title: "New snippet" do
+ New snippet
+ - if @snippet.submittable_as_spam? && current_user.admin?
+ = link_to 'Submit as spam', mark_as_spam_snippet_path(@snippet), method: :post, class: 'btn btn-grouped btn-spam', title: 'Submit as spam'
+.visible-xs-block.dropdown
+ %button.btn.btn-default.btn-block.append-bottom-0.prepend-top-5{ data: { toggle: "dropdown" } }
+ Options
+ = icon('caret-down')
+ .dropdown-menu.dropdown-menu-full-width
+ %ul
+ %li
+ = link_to new_snippet_path, title: "New snippet" do
+ New snippet
+ - if can?(current_user, :admin_personal_snippet, @snippet)
%li
- = link_to new_snippet_path, title: "New snippet" do
- New snippet
- - if can?(current_user, :admin_personal_snippet, @snippet)
- %li
- = link_to snippet_path(@snippet), method: :delete, data: { confirm: "Are you sure?" }, title: 'Delete Snippet' do
- Delete
- - if can?(current_user, :update_personal_snippet, @snippet)
- %li
- = link_to edit_snippet_path(@snippet) do
- Edit
+ = link_to snippet_path(@snippet), method: :delete, data: { confirm: "Are you sure?" }, title: 'Delete Snippet' do
+ Delete
+ - if can?(current_user, :update_personal_snippet, @snippet)
+ %li
+ = link_to edit_snippet_path(@snippet) do
+ Edit
+ - if @snippet.submittable_as_spam? && current_user.admin?
+ %li
+ = link_to 'Submit as spam', mark_as_spam_snippet_path(@snippet), method: :post
diff --git a/app/views/snippets/edit.html.haml b/app/views/snippets/edit.html.haml
index 82f44a9a5c3..915bf98eb3e 100644
--- a/app/views/snippets/edit.html.haml
+++ b/app/views/snippets/edit.html.haml
@@ -2,4 +2,4 @@
%h3.page-title
Edit Snippet
%hr
-= render 'shared/snippets/form', url: snippet_path(@snippet), visibility_level: @snippet.visibility_level
+= render 'shared/snippets/form', url: snippet_path(@snippet)
diff --git a/app/views/snippets/new.html.haml b/app/views/snippets/new.html.haml
index 79e2392490d..ca8afb4bb6a 100644
--- a/app/views/snippets/new.html.haml
+++ b/app/views/snippets/new.html.haml
@@ -2,4 +2,4 @@
%h3.page-title
New Snippet
%hr
-= render "shared/snippets/form", url: snippets_path(@snippet), visibility_level: default_snippet_visibility
+= render "shared/snippets/form", url: snippets_path(@snippet)
diff --git a/app/views/u2f/_authenticate.html.haml b/app/views/u2f/_authenticate.html.haml
index 232ca26c1af..f878bece2fa 100644
--- a/app/views/u2f/_authenticate.html.haml
+++ b/app/views/u2f/_authenticate.html.haml
@@ -1,30 +1,21 @@
#js-authenticate-u2f
+%a.btn.btn-block.btn-info#js-login-2fa-device{ href: '#' } Sign in via 2FA code
%script#js-authenticate-u2f-not-supported{ type: "text/template" }
%p Your browser doesn't support U2F. Please use Google Chrome desktop (version 41 or newer).
-%script#js-authenticate-u2f-setup{ type: "text/template" }
- %div
- %p Insert your security key (if you haven't already), and press the button below.
- %a.btn.btn-info#js-login-u2f-device{ href: 'javascript:void(0)' } Sign in via U2F device
-
%script#js-authenticate-u2f-in-progress{ type: "text/template" }
%p Trying to communicate with your device. Plug it in (if you haven't already) and press the button on the device now.
%script#js-authenticate-u2f-error{ type: "text/template" }
%div
- %p <%= error_message %>
- %a.btn.btn-warning#js-u2f-try-again Try again?
+ %p <%= error_message %> (error code: <%= error_code %>)
+ %a.btn.btn-block.btn-warning#js-u2f-try-again Try again?
%script#js-authenticate-u2f-authenticated{ type: "text/template" }
%div
- %p We heard back from your U2F device. Click this button to authenticate with the GitLab server.
- = form_tag(new_user_session_path, method: :post) do |f|
+ %p We heard back from your U2F device. You have been authenticated.
+ = form_tag(new_user_session_path, method: :post, id: 'js-login-u2f-form') do |f|
- resource_params = params[resource_name].presence || params
= hidden_field_tag 'user[remember_me]', resource_params.fetch(:remember_me, 0)
= hidden_field_tag 'user[device_response]', nil, class: 'form-control', required: true, id: "js-device-response"
- = submit_tag "Authenticate via U2F Device", class: "btn btn-success"
-
-:javascript
- var u2fAuthenticate = new U2FAuthenticate($("#js-authenticate-u2f"), gon.u2f);
- u2fAuthenticate.start();
diff --git a/app/views/u2f/_register.html.haml b/app/views/u2f/_register.html.haml
index 8f7b42eb351..adc07bcba73 100644
--- a/app/views/u2f/_register.html.haml
+++ b/app/views/u2f/_register.html.haml
@@ -23,11 +23,11 @@
%script#js-register-u2f-error{ type: "text/template" }
%div
%p
- %span <%= error_message %>
+ %span <%= error_message %> (error code: <%= error_code %>)
%a.btn.btn-warning#js-u2f-try-again Try again?
%script#js-register-u2f-registered{ type: "text/template" }
- %div.row.append-bottom-10
+ .row.append-bottom-10
.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
diff --git a/app/views/users/calendar.html.haml b/app/views/users/calendar.html.haml
index 09ff8a76d27..6228245d8d0 100644
--- a/app/views/users/calendar.html.haml
+++ b/app/views/users/calendar.html.haml
@@ -6,4 +6,4 @@
new Calendar(
#{@activity_dates.to_json},
'#{user_calendar_activities_path}'
- ); \ No newline at end of file
+ );
diff --git a/app/views/users/calendar_activities.html.haml b/app/views/users/calendar_activities.html.haml
index f51599212db..b09782749f5 100644
--- a/app/views/users/calendar_activities.html.haml
+++ b/app/views/users/calendar_activities.html.haml
@@ -1,6 +1,6 @@
%h4.prepend-top-20
Contributions for
- %strong #{@calendar_date.to_s(:short)}
+ %strong= @calendar_date.to_s(:short)
- if @events.any?
%ul.bordered-list
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index 1e0752bd3c3..44254040e4e 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -1,8 +1,8 @@
- page_title @user.name
- page_description @user.bio
- content_for :page_specific_javascripts do
- = page_specific_javascript_tag('lib/d3.js')
- = page_specific_javascript_tag('users/users_bundle.js')
+ = page_specific_javascript_bundle_tag('lib_d3')
+ = page_specific_javascript_bundle_tag('users')
- header_title @user.name, user_path(@user)
- @no_container = true
@@ -18,11 +18,11 @@
- elsif current_user
- if @user.abuse_report
%button.btn.btn-danger{ title: 'Already reported for abuse',
- data: { toggle: 'tooltip', placement: 'bottom', container: 'body' }}
+ data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } }
= icon('exclamation-circle')
- else
= link_to new_abuse_report_path(user_id: @user.id, ref_url: request.referrer), class: 'btn btn-gray',
- title: 'Report abuse', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
+ title: 'Report abuse', data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do
= icon('exclamation-circle')
- if current_user
= link_to user_path(@user, :atom, { private_token: current_user.private_token }), class: 'btn btn-gray' do
@@ -101,25 +101,25 @@
.tab-content
#activity.tab-pane
.row-content-block.calender-block.white.second-block.hidden-xs
- .user-calendar{data: {href: user_calendar_path}}
+ .user-calendar{ data: { href: user_calendar_path } }
%h4.center.light
%i.fa.fa-spinner.fa-spin
.user-calendar-activities
- .content_list{ data: {href: user_path} }
+ .content_list{ data: { href: user_path } }
= spinner
#groups.tab-pane
- - # This tab is always loaded via AJAX
+ -# This tab is always loaded via AJAX
#contributed.tab-pane
- - # This tab is always loaded via AJAX
+ -# This tab is always loaded via AJAX
#projects.tab-pane
- - # This tab is always loaded via AJAX
+ -# This tab is always loaded via AJAX
#snippets.tab-pane
- - # This tab is always loaded via AJAX
+ -# This tab is always loaded via AJAX
.loading-status
= spinner
diff --git a/app/workers/authorized_projects_worker.rb b/app/workers/authorized_projects_worker.rb
index 2badd0680fb..6abbb5a5250 100644
--- a/app/workers/authorized_projects_worker.rb
+++ b/app/workers/authorized_projects_worker.rb
@@ -2,6 +2,13 @@ class AuthorizedProjectsWorker
include Sidekiq::Worker
include DedicatedSidekiqQueue
+ # Schedules multiple jobs and waits for them to be completed.
+ def self.bulk_perform_and_wait(args_list)
+ job_ids = bulk_perform_async(args_list)
+
+ Gitlab::JobWaiter.new(job_ids).wait
+ end
+
def self.bulk_perform_async(args_list)
Sidekiq::Client.push_bulk('class' => self, 'args' => args_list)
end
diff --git a/app/workers/build_queue_worker.rb b/app/workers/build_queue_worker.rb
new file mode 100644
index 00000000000..fa9e097e40a
--- /dev/null
+++ b/app/workers/build_queue_worker.rb
@@ -0,0 +1,10 @@
+class BuildQueueWorker
+ include Sidekiq::Worker
+ include BuildQueue
+
+ def perform(build_id)
+ Ci::Build.find_by(id: build_id).try do |build|
+ Ci::UpdateBuildQueueService.new.execute(build)
+ end
+ end
+end
diff --git a/app/workers/emails_on_push_worker.rb b/app/workers/emails_on_push_worker.rb
index b9cd49985dc..f5ccc84c160 100644
--- a/app/workers/emails_on_push_worker.rb
+++ b/app/workers/emails_on_push_worker.rb
@@ -33,13 +33,15 @@ class EmailsOnPushWorker
reverse_compare = false
if action == :push
- compare = CompareService.new.execute(project, after_sha, project, before_sha)
+ compare = CompareService.new(project, after_sha)
+ .execute(project, before_sha)
diff_refs = compare.diff_refs
return false if compare.same
if compare.commits.empty?
- compare = CompareService.new.execute(project, before_sha, project, after_sha)
+ compare = CompareService.new(project, before_sha)
+ .execute(project, after_sha)
diff_refs = compare.diff_refs
reverse_compare = true
diff --git a/app/workers/pages_worker.rb b/app/workers/pages_worker.rb
new file mode 100644
index 00000000000..4eeb9666bb0
--- /dev/null
+++ b/app/workers/pages_worker.rb
@@ -0,0 +1,23 @@
+class PagesWorker
+ include Sidekiq::Worker
+
+ sidekiq_options queue: :pages, retry: false
+
+ def perform(action, *arg)
+ send(action, *arg)
+ end
+
+ def deploy(build_id)
+ build = Ci::Build.find_by(id: build_id)
+ result = Projects::UpdatePagesService.new(build.project, build).execute
+ if result[:status] == :success
+ result = Projects::UpdatePagesConfigurationService.new(build.project).execute
+ end
+ result
+ end
+
+ def remove(namespace_path, project_path)
+ full_path = File.join(Settings.pages.path, namespace_path, project_path)
+ FileUtils.rm_r(full_path, force: true)
+ end
+end
diff --git a/app/workers/project_cache_worker.rb b/app/workers/project_cache_worker.rb
index 27d7e652721..8ff9d07860f 100644
--- a/app/workers/project_cache_worker.rb
+++ b/app/workers/project_cache_worker.rb
@@ -6,26 +6,27 @@ class ProjectCacheWorker
LEASE_TIMEOUT = 15.minutes.to_i
# project_id - The ID of the project for which to flush the cache.
- # refresh - An Array containing extra types of data to refresh such as
- # `:readme` to flush the README and `:changelog` to flush the
- # CHANGELOG.
- def perform(project_id, refresh = [])
+ # files - An Array containing extra types of files to refresh such as
+ # `:readme` to flush the README and `:changelog` to flush the
+ # CHANGELOG.
+ # statistics - An Array containing columns from ProjectStatistics to
+ # refresh, if empty all columns will be refreshed
+ def perform(project_id, files = [], statistics = [])
project = Project.find_by(id: project_id)
return unless project && project.repository.exists?
- update_repository_size(project)
- project.update_commit_count
+ update_statistics(project, statistics.map(&:to_sym))
- project.repository.refresh_method_caches(refresh.map(&:to_sym))
+ project.repository.refresh_method_caches(files.map(&:to_sym))
end
- def update_repository_size(project)
- return unless try_obtain_lease_for(project.id, :update_repository_size)
+ def update_statistics(project, statistics = [])
+ return unless try_obtain_lease_for(project.id, :update_statistics)
- Rails.logger.info("Updating repository size for project #{project.id}")
+ Rails.logger.info("Updating statistics for project #{project.id}")
- project.update_repository_size
+ project.statistics.refresh!(only: statistics)
end
private
diff --git a/app/workers/reactive_caching_worker.rb b/app/workers/reactive_caching_worker.rb
index 9af9dae04f0..18b8daf4e1e 100644
--- a/app/workers/reactive_caching_worker.rb
+++ b/app/workers/reactive_caching_worker.rb
@@ -2,7 +2,7 @@ class ReactiveCachingWorker
include Sidekiq::Worker
include DedicatedSidekiqQueue
- def perform(class_name, id)
+ def perform(class_name, id, *args)
klass = begin
Kernel.const_get(class_name)
rescue NameError
@@ -10,6 +10,6 @@ class ReactiveCachingWorker
end
return unless klass
- klass.find_by(id: id).try(:exclusively_update_reactive_cache!)
+ klass.find_by(id: id).try(:exclusively_update_reactive_cache!, *args)
end
end
diff --git a/app/workers/use_key_worker.rb b/app/workers/use_key_worker.rb
new file mode 100644
index 00000000000..c9d382cc5d6
--- /dev/null
+++ b/app/workers/use_key_worker.rb
@@ -0,0 +1,13 @@
+class UseKeyWorker
+ include Sidekiq::Worker
+ include DedicatedSidekiqQueue
+
+ def perform(key_id)
+ key = Key.find(key_id)
+ key.touch(:last_used_at)
+ rescue ActiveRecord::RecordNotFound
+ Rails.logger.error("UseKeyWorker: couldn't find key with ID=#{key_id}, skipping job")
+
+ false
+ end
+end
diff --git a/bin/teaspoon b/bin/teaspoon
deleted file mode 100755
index 7c3b8dfc4ed..00000000000
--- a/bin/teaspoon
+++ /dev/null
@@ -1,8 +0,0 @@
-#!/usr/bin/env ruby
-begin
- load File.expand_path('../spring', __FILE__)
-rescue LoadError => e
- raise unless e.message.include?('spring')
-end
-require 'bundler/setup'
-load Gem.bin_path('teaspoon', 'teaspoon')
diff --git a/changelogs/unreleased/15081-wrong-login-tab-ldap-frontend.yml b/changelogs/unreleased/15081-wrong-login-tab-ldap-frontend.yml
deleted file mode 100644
index 19c76b5b437..00000000000
--- a/changelogs/unreleased/15081-wrong-login-tab-ldap-frontend.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix wrong tab selected when loggin fails and multiple login tabs exists
-merge_request: 7314
-author: Jacopo Beschi @jacopo-beschi
diff --git a/changelogs/unreleased/17662-rename-builds.yml b/changelogs/unreleased/17662-rename-builds.yml
new file mode 100644
index 00000000000..12f2998d1c8
--- /dev/null
+++ b/changelogs/unreleased/17662-rename-builds.yml
@@ -0,0 +1,4 @@
+---
+title: Rename Builds to Pipelines, CI/CD Pipelines, or Jobs everywhere
+merge_request: 8787
+author:
diff --git a/changelogs/unreleased/18435-autocomplete-is-not-performant.yml b/changelogs/unreleased/18435-autocomplete-is-not-performant.yml
deleted file mode 100644
index 019c55e27dc..00000000000
--- a/changelogs/unreleased/18435-autocomplete-is-not-performant.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Made comment autocomplete more performant and removed some loading bugs
-merge_request: 6856
-author:
diff --git a/changelogs/unreleased/18546-update-wiki-page-design.yml b/changelogs/unreleased/18546-update-wiki-page-design.yml
deleted file mode 100644
index c76e17340f2..00000000000
--- a/changelogs/unreleased/18546-update-wiki-page-design.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Update wiki page design
-merge_request: 7429
-author:
diff --git a/changelogs/unreleased/19550-fix-contributer-graph-duplicates.yml b/changelogs/unreleased/19550-fix-contributer-graph-duplicates.yml
deleted file mode 100644
index 742b10e72aa..00000000000
--- a/changelogs/unreleased/19550-fix-contributer-graph-duplicates.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: group authors in contribution graph with case insensitive email handle comparison
-merge_request: 8021
-author:
diff --git a/changelogs/unreleased/19703-direct-link-pipelines.yml b/changelogs/unreleased/19703-direct-link-pipelines.yml
deleted file mode 100644
index d846ad41e0f..00000000000
--- a/changelogs/unreleased/19703-direct-link-pipelines.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Adds Direct link from pipeline list to builds
-merge_request: 8097
-author:
diff --git a/changelogs/unreleased/20052-actions-table-vscroll.yml b/changelogs/unreleased/20052-actions-table-vscroll.yml
deleted file mode 100644
index 779cd08de09..00000000000
--- a/changelogs/unreleased/20052-actions-table-vscroll.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Prevent overflow with vertical scroll when we have space to show content
-merge_request: 8061
-author:
diff --git a/changelogs/unreleased/20452-remove-require-from-request_profiler-initializer.yml b/changelogs/unreleased/20452-remove-require-from-request_profiler-initializer.yml
new file mode 100644
index 00000000000..965d0648adf
--- /dev/null
+++ b/changelogs/unreleased/20452-remove-require-from-request_profiler-initializer.yml
@@ -0,0 +1,4 @@
+---
+title: Don't require lib/gitlab/request_profiler/middleware.rb in config/initializers/request_profiler.rb
+merge_request:
+author:
diff --git a/changelogs/unreleased/20492-access-token-scopes.yml b/changelogs/unreleased/20492-access-token-scopes.yml
deleted file mode 100644
index a9424ded662..00000000000
--- a/changelogs/unreleased/20492-access-token-scopes.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add scopes for personal access tokens and OAuth tokens
-merge_request: 5951
-author:
diff --git a/changelogs/unreleased/20852-getting-started-project-better-blank-state-for-labels-view.yml b/changelogs/unreleased/20852-getting-started-project-better-blank-state-for-labels-view.yml
new file mode 100644
index 00000000000..eda872049fd
--- /dev/null
+++ b/changelogs/unreleased/20852-getting-started-project-better-blank-state-for-labels-view.yml
@@ -0,0 +1,4 @@
+---
+title: Added labels empty state
+merge_request: 7443
+author:
diff --git a/changelogs/unreleased/21518_recaptcha_spam_issues.yml b/changelogs/unreleased/21518_recaptcha_spam_issues.yml
new file mode 100644
index 00000000000..bd6c9d7521e
--- /dev/null
+++ b/changelogs/unreleased/21518_recaptcha_spam_issues.yml
@@ -0,0 +1,4 @@
+---
+title: Use reCaptcha when an issue is identified as a spam
+merge_request: 8846
+author:
diff --git a/changelogs/unreleased/22007-unify-projects-search.yml b/changelogs/unreleased/22007-unify-projects-search.yml
new file mode 100644
index 00000000000..f43c1925ad0
--- /dev/null
+++ b/changelogs/unreleased/22007-unify-projects-search.yml
@@ -0,0 +1,4 @@
+---
+title: Unify projects search by removing /projects/:search endpoint
+merge_request: 8877
+author:
diff --git a/changelogs/unreleased/22348-gitea-importer.yml b/changelogs/unreleased/22348-gitea-importer.yml
deleted file mode 100644
index 2aeefb0b259..00000000000
--- a/changelogs/unreleased/22348-gitea-importer.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: New Gitea importer
-merge_request: 8116
-author:
diff --git a/changelogs/unreleased/22373-reduce-queries-in-api-helpers-find_project.yml b/changelogs/unreleased/22373-reduce-queries-in-api-helpers-find_project.yml
deleted file mode 100644
index 7f1d40e7c21..00000000000
--- a/changelogs/unreleased/22373-reduce-queries-in-api-helpers-find_project.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: 'Make API::Helpers find a project with only one query'
-merge_request: 7714
-author:
diff --git a/changelogs/unreleased/22604-manual-actions.yml b/changelogs/unreleased/22604-manual-actions.yml
deleted file mode 100644
index 7335e597292..00000000000
--- a/changelogs/unreleased/22604-manual-actions.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Resolve "Manual actions on pipeline graph"
-merge_request: 7931
-author:
diff --git a/changelogs/unreleased/22619-add-an-email-address-to-unsubscribe-list-header-in-email b/changelogs/unreleased/22619-add-an-email-address-to-unsubscribe-list-header-in-email
new file mode 100644
index 00000000000..f4011b756a5
--- /dev/null
+++ b/changelogs/unreleased/22619-add-an-email-address-to-unsubscribe-list-header-in-email
@@ -0,0 +1,4 @@
+---
+title: Handle unsubscribe from email notifications via replying to reply+%{key}+unsubscribe@ address
+merge_request: 6597
+author:
diff --git a/changelogs/unreleased/22638-creating-a-branch-matching-a-wildcard-fails.yml b/changelogs/unreleased/22638-creating-a-branch-matching-a-wildcard-fails.yml
new file mode 100644
index 00000000000..2c6883bcf7b
--- /dev/null
+++ b/changelogs/unreleased/22638-creating-a-branch-matching-a-wildcard-fails.yml
@@ -0,0 +1,4 @@
+---
+title: Allow creating protected branches when user can merge to such branch
+merge_request: 8458
+author:
diff --git a/changelogs/unreleased/22719-provide-a-new-gitlab-workhorse-install-rake-task-similar-to-gitlab-shell-install.yml b/changelogs/unreleased/22719-provide-a-new-gitlab-workhorse-install-rake-task-similar-to-gitlab-shell-install.yml
deleted file mode 100644
index 54bd313f075..00000000000
--- a/changelogs/unreleased/22719-provide-a-new-gitlab-workhorse-install-rake-task-similar-to-gitlab-shell-install.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: New `gitlab:workhorse:install` rake task
-merge_request: 6574
-author:
diff --git a/changelogs/unreleased/22781-user-generated-permalinks.yml b/changelogs/unreleased/22781-user-generated-permalinks.yml
deleted file mode 100644
index e46739e48e3..00000000000
--- a/changelogs/unreleased/22781-user-generated-permalinks.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Prevent DOM ID collisions resulting from user-generated content anchors
-merge_request: 7631
-author:
diff --git a/changelogs/unreleased/22849-ci-build-ref-slug.yml b/changelogs/unreleased/22849-ci-build-ref-slug.yml
deleted file mode 100644
index b159ecca6d8..00000000000
--- a/changelogs/unreleased/22849-ci-build-ref-slug.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Introduce $CI_BUILD_REF_SLUG
-merge_request: 8072
-author:
diff --git a/changelogs/unreleased/22864-add-environment-slug.yml b/changelogs/unreleased/22864-add-environment-slug.yml
deleted file mode 100644
index f90f79337d5..00000000000
--- a/changelogs/unreleased/22864-add-environment-slug.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add a slug to environments
-merge_request: 7983
-author:
diff --git a/changelogs/unreleased/22864-kubernetes-service.yml b/changelogs/unreleased/22864-kubernetes-service.yml
deleted file mode 100644
index ea1323cbeb0..00000000000
--- a/changelogs/unreleased/22864-kubernetes-service.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Introduce deployment services, starting with a KubernetesService
-merge_request: 7994
-author:
diff --git a/changelogs/unreleased/22974-trigger-service-events-through-api.yml b/changelogs/unreleased/22974-trigger-service-events-through-api.yml
new file mode 100644
index 00000000000..57106e8c676
--- /dev/null
+++ b/changelogs/unreleased/22974-trigger-service-events-through-api.yml
@@ -0,0 +1,4 @@
+---
+title: Adds service trigger events to api
+merge_request: 8324
+author:
diff --git a/changelogs/unreleased/23500-enable-colorvariable.yml b/changelogs/unreleased/23500-enable-colorvariable.yml
deleted file mode 100644
index 98e22a934b8..00000000000
--- a/changelogs/unreleased/23500-enable-colorvariable.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Enable ColorVariable in scss-lint
-merge_request:
-author: Sam Rose
diff --git a/changelogs/unreleased/23524-notify-automerge-user-of-failed-build.yml b/changelogs/unreleased/23524-notify-automerge-user-of-failed-build.yml
new file mode 100644
index 00000000000..268be6b9b83
--- /dev/null
+++ b/changelogs/unreleased/23524-notify-automerge-user-of-failed-build.yml
@@ -0,0 +1,4 @@
+---
+title: Create a TODO for user who set auto-merge when a build fails, merge conflict occurs
+merge_request: 8056
+author: twonegatives
diff --git a/changelogs/unreleased/23532-define-common-helper-for-describe-pagination-params-in-api.yml b/changelogs/unreleased/23532-define-common-helper-for-describe-pagination-params-in-api.yml
deleted file mode 100644
index bb9e96d7581..00000000000
--- a/changelogs/unreleased/23532-define-common-helper-for-describe-pagination-params-in-api.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Define common helper for describe pagination params in api
-merge_request: 7646
-author: Semyon Pupkov
diff --git a/changelogs/unreleased/23573-sort-functionality-for-project-member.yml b/changelogs/unreleased/23573-sort-functionality-for-project-member.yml
deleted file mode 100644
index 73de0a6351b..00000000000
--- a/changelogs/unreleased/23573-sort-functionality-for-project-member.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add sorting functionality for group/project members
-merge_request: 7032
-author:
diff --git a/changelogs/unreleased/23589-open-issue-for-mr.yml b/changelogs/unreleased/23589-open-issue-for-mr.yml
deleted file mode 100644
index cea48b85254..00000000000
--- a/changelogs/unreleased/23589-open-issue-for-mr.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Resolve all discussions in a merge request by creating an issue collecting
- them
-merge_request: 7180
-author: Bob Van Landuyt
diff --git a/changelogs/unreleased/23634-remove-project-grouping.yml b/changelogs/unreleased/23634-remove-project-grouping.yml
new file mode 100644
index 00000000000..dde8b2d1815
--- /dev/null
+++ b/changelogs/unreleased/23634-remove-project-grouping.yml
@@ -0,0 +1,4 @@
+---
+title: Don't group issues by project on group-level and dashboard issue indexes.
+merge_request: 8111
+author: Bernardo Castro
diff --git a/changelogs/unreleased/23718-backup-rake-task-human-readable.yml b/changelogs/unreleased/23718-backup-rake-task-human-readable.yml
deleted file mode 100644
index 2e7583244ac..00000000000
--- a/changelogs/unreleased/23718-backup-rake-task-human-readable.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add Human Readable format for rake backup
-merge_request: 7188
-author: David Gerő
diff --git a/changelogs/unreleased/23767-disable-storing-of-sensitive-information.yml b/changelogs/unreleased/23767-disable-storing-of-sensitive-information.yml
new file mode 100644
index 00000000000..587ef4f9a73
--- /dev/null
+++ b/changelogs/unreleased/23767-disable-storing-of-sensitive-information.yml
@@ -0,0 +1,4 @@
+---
+title: Fix disable storing of sensitive information when importing a new repo
+merge_request: 8885
+author: Bernard Pietraga
diff --git a/changelogs/unreleased/24135-new-project-should-be-below-new-group-on-the-welcome-screen.yml b/changelogs/unreleased/24135-new-project-should-be-below-new-group-on-the-welcome-screen.yml
deleted file mode 100644
index 855e4e1ba1d..00000000000
--- a/changelogs/unreleased/24135-new-project-should-be-below-new-group-on-the-welcome-screen.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Moved new projects button below new group button on the welcome screen
-merge_request: 7770
-author:
diff --git a/changelogs/unreleased/24147-delete-env-button.yml b/changelogs/unreleased/24147-delete-env-button.yml
new file mode 100644
index 00000000000..14e80cacbfb
--- /dev/null
+++ b/changelogs/unreleased/24147-delete-env-button.yml
@@ -0,0 +1,4 @@
+---
+title: Adds back ability to stop all environments
+merge_request: 7379
+author:
diff --git a/changelogs/unreleased/24150-consistent-dropdown-styles.yml b/changelogs/unreleased/24150-consistent-dropdown-styles.yml
deleted file mode 100644
index a328d796c43..00000000000
--- a/changelogs/unreleased/24150-consistent-dropdown-styles.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Homogenize filter and sort dropdown look'n'feel
-merge_request: 7583
-author: David Wagner
diff --git a/changelogs/unreleased/24281-issue-merge-request-sidebar-subscribe-button-style-improvement.yml b/changelogs/unreleased/24281-issue-merge-request-sidebar-subscribe-button-style-improvement.yml
deleted file mode 100644
index 2227c81bd34..00000000000
--- a/changelogs/unreleased/24281-issue-merge-request-sidebar-subscribe-button-style-improvement.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Remove the help text under the sidebar subscribe button and style it inline
-merge_request: 7389
-author:
diff --git a/changelogs/unreleased/24413-show-unconfirmed-email-status.yml b/changelogs/unreleased/24413-show-unconfirmed-email-status.yml
deleted file mode 100644
index 972eaed95e0..00000000000
--- a/changelogs/unreleased/24413-show-unconfirmed-email-status.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Shows unconfirmed email status in profile
-merge_request: 7611
-author:
diff --git a/changelogs/unreleased/24462-reduce_ldap_queries_for_lfs.yml b/changelogs/unreleased/24462-reduce_ldap_queries_for_lfs.yml
new file mode 100644
index 00000000000..05fbd8f0bf2
--- /dev/null
+++ b/changelogs/unreleased/24462-reduce_ldap_queries_for_lfs.yml
@@ -0,0 +1,4 @@
+---
+title: Reduce hits to LDAP on Git HTTP auth by reordering auth mechanisms
+merge_request: 8752
+author:
diff --git a/changelogs/unreleased/24507_remove_deleted_branch_link_in_merge_request.yml b/changelogs/unreleased/24507_remove_deleted_branch_link_in_merge_request.yml
deleted file mode 100644
index 34999480d4a..00000000000
--- a/changelogs/unreleased/24507_remove_deleted_branch_link_in_merge_request.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: 'Remove unnecessary target branch link from MR page in case of deleted target branch'
-merge_request: 7916
-author: Rydkin Maxim
diff --git a/changelogs/unreleased/24576_cant_stop_impersonating.yml b/changelogs/unreleased/24576_cant_stop_impersonating.yml
deleted file mode 100644
index 8fa6eeca756..00000000000
--- a/changelogs/unreleased/24576_cant_stop_impersonating.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Allow admins to stop impersonating users without e-mail addresses
-merge_request: 7550
-author: Oren Kanner
diff --git a/changelogs/unreleased/24606-force-password-reset-on-next-login.yml b/changelogs/unreleased/24606-force-password-reset-on-next-login.yml
new file mode 100644
index 00000000000..fd671d04a9f
--- /dev/null
+++ b/changelogs/unreleased/24606-force-password-reset-on-next-login.yml
@@ -0,0 +1,4 @@
+---
+title: Force new password after password reset via API
+merge_request:
+author: George Andrinopoulos
diff --git a/changelogs/unreleased/24710-fix-generic-commit-status-table-row.yml b/changelogs/unreleased/24710-fix-generic-commit-status-table-row.yml
deleted file mode 100644
index 07cb53d5278..00000000000
--- a/changelogs/unreleased/24710-fix-generic-commit-status-table-row.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Update generic/external build status to match normal build status template
-merge_request: 7811
-author:
diff --git a/changelogs/unreleased/24716-fix-ctrl-click-links.yml b/changelogs/unreleased/24716-fix-ctrl-click-links.yml
new file mode 100644
index 00000000000..13de5db5e41
--- /dev/null
+++ b/changelogs/unreleased/24716-fix-ctrl-click-links.yml
@@ -0,0 +1,4 @@
+---
+title: Fix Ctrl+Click support for Todos and Merge Request page tabs
+merge_request: 8898
+author:
diff --git a/changelogs/unreleased/24726-remove-across-gitlab.yml b/changelogs/unreleased/24726-remove-across-gitlab.yml
deleted file mode 100644
index 6436e4b688f..00000000000
--- a/changelogs/unreleased/24726-remove-across-gitlab.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: 24726 Remove Across GitLab from side navigation
-merge_request:
-author:
diff --git a/changelogs/unreleased/24733-archived-project-merge-request-count.yml b/changelogs/unreleased/24733-archived-project-merge-request-count.yml
deleted file mode 100644
index 2bc7e91825a..00000000000
--- a/changelogs/unreleased/24733-archived-project-merge-request-count.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix Archived project merge requests add to group's Merge Requests
-merge_request: 7790
-author: Jacopo Beschi @jacopo-beschi
diff --git a/changelogs/unreleased/24795_refactor_merge_request_build_service.yml b/changelogs/unreleased/24795_refactor_merge_request_build_service.yml
new file mode 100644
index 00000000000..b735fb57649
--- /dev/null
+++ b/changelogs/unreleased/24795_refactor_merge_request_build_service.yml
@@ -0,0 +1,4 @@
+---
+title: Refactor MergeRequests::BuildService
+merge_request: 8462
+author: Rydkin Maxim
diff --git a/changelogs/unreleased/24803-change-cursor-for-ca-stages.yml b/changelogs/unreleased/24803-change-cursor-for-ca-stages.yml
deleted file mode 100644
index b9d84c0ce31..00000000000
--- a/changelogs/unreleased/24803-change-cursor-for-ca-stages.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Changed cursor icon to pointer when mousing over stages on the Cycle Analytics
- pages
-merge_request:
-author: Ryan Harris
diff --git a/changelogs/unreleased/24807-stop-ddosing-ourselves.yml b/changelogs/unreleased/24807-stop-ddosing-ourselves.yml
deleted file mode 100644
index 49e6c5e56e5..00000000000
--- a/changelogs/unreleased/24807-stop-ddosing-ourselves.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Use SmartInterval for MR widget and improve visibilitychange functionality
-merge_request: 7762
-author:
diff --git a/changelogs/unreleased/24824-dropdown-items-focus.yml b/changelogs/unreleased/24824-dropdown-items-focus.yml
deleted file mode 100644
index 66970c2a9a5..00000000000
--- a/changelogs/unreleased/24824-dropdown-items-focus.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add focus state to dropdown items
-merge_request:
-author:
diff --git a/changelogs/unreleased/24833-Allow-to-search-by-commit-hash-within-project.yml b/changelogs/unreleased/24833-Allow-to-search-by-commit-hash-within-project.yml
new file mode 100644
index 00000000000..be66c370f36
--- /dev/null
+++ b/changelogs/unreleased/24833-Allow-to-search-by-commit-hash-within-project.yml
@@ -0,0 +1,4 @@
+---
+title: 'Allows to search within project by commit hash'
+merge_request:
+author: YarNayar
diff --git a/changelogs/unreleased/24844-environments-date.yml b/changelogs/unreleased/24844-environments-date.yml
deleted file mode 100644
index 2bc23d40a68..00000000000
--- a/changelogs/unreleased/24844-environments-date.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fixes Environments displaying incorrect date since 8.14 upgrade
-merge_request:
-author:
diff --git a/changelogs/unreleased/24877-bulk-edit-only-keeps-common-labels-when-searching.yml b/changelogs/unreleased/24877-bulk-edit-only-keeps-common-labels-when-searching.yml
deleted file mode 100644
index cc7c2604824..00000000000
--- a/changelogs/unreleased/24877-bulk-edit-only-keeps-common-labels-when-searching.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Improve bulk assignment for issuables
-merge_request:
-author:
diff --git a/changelogs/unreleased/24921-hide-prompt-to-add-ssh-key-if-ssh-protocol-is-disabled.yml b/changelogs/unreleased/24921-hide-prompt-to-add-ssh-key-if-ssh-protocol-is-disabled.yml
deleted file mode 100644
index 4d4019e770e..00000000000
--- a/changelogs/unreleased/24921-hide-prompt-to-add-ssh-key-if-ssh-protocol-is-disabled.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Don't display prompt to add SSH keys if SSH protocol is disabled
-merge_request: 7840
-author: Andrew Smith (EspadaV8)
diff --git a/changelogs/unreleased/24923_nested_tasks.yml b/changelogs/unreleased/24923_nested_tasks.yml
new file mode 100644
index 00000000000..de35cad3dd6
--- /dev/null
+++ b/changelogs/unreleased/24923_nested_tasks.yml
@@ -0,0 +1,4 @@
+---
+title: Fix nested tasks in ordered list
+merge_request: 8626
+author:
diff --git a/changelogs/unreleased/24927-custom-event-polyfill-test.yml b/changelogs/unreleased/24927-custom-event-polyfill-test.yml
deleted file mode 100644
index 879c28a951e..00000000000
--- a/changelogs/unreleased/24927-custom-event-polyfill-test.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Adds tests for custom event polyfill
-merge_request: 7996
-author:
diff --git a/changelogs/unreleased/24949-view-2-up-swipe-onion-skin-controls-for-merge-request-diff-containing-an-image.yml b/changelogs/unreleased/24949-view-2-up-swipe-onion-skin-controls-for-merge-request-diff-containing-an-image.yml
deleted file mode 100644
index b8ba9391530..00000000000
--- a/changelogs/unreleased/24949-view-2-up-swipe-onion-skin-controls-for-merge-request-diff-containing-an-image.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add image controls to MR diffs
-merge_request: 7919
-author:
diff --git a/changelogs/unreleased/24982-ux-improvement-sign-in-success-message.yml b/changelogs/unreleased/24982-ux-improvement-sign-in-success-message.yml
deleted file mode 100644
index 12ea08e3815..00000000000
--- a/changelogs/unreleased/24982-ux-improvement-sign-in-success-message.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: 'fix: 24982- Remove''Signed in successfully'' message After this change the
- sign-in-success flash message will not be shown'
-merge_request: 7837
-author: jnoortheen
diff --git a/changelogs/unreleased/24999-fix-project-avatar-alignment.yml b/changelogs/unreleased/24999-fix-project-avatar-alignment.yml
deleted file mode 100644
index 7af812e7359..00000000000
--- a/changelogs/unreleased/24999-fix-project-avatar-alignment.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Adjust the width of project avatars to fix alignment within their container
-merge_request:
-author: Ryan Harris
diff --git a/changelogs/unreleased/25002-sentence-case-dashboard-tabs.yml b/changelogs/unreleased/25002-sentence-case-dashboard-tabs.yml
deleted file mode 100644
index cc8b0e28277..00000000000
--- a/changelogs/unreleased/25002-sentence-case-dashboard-tabs.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Sentence cased the nav tab headers on the project dashboard page
-merge_request:
-author: Ryan Harris
diff --git a/changelogs/unreleased/25011-hoverstates-for-collapsed-issue-merge-request-sidebar.yml b/changelogs/unreleased/25011-hoverstates-for-collapsed-issue-merge-request-sidebar.yml
deleted file mode 100644
index 2c3ba1dfe44..00000000000
--- a/changelogs/unreleased/25011-hoverstates-for-collapsed-issue-merge-request-sidebar.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Adds hoverstates for collapsed Issue/Merge Request sidebar
-merge_request: !7777
-author:
diff --git a/changelogs/unreleased/25026-authenticate-user-for-new-snippet.yml b/changelogs/unreleased/25026-authenticate-user-for-new-snippet.yml
deleted file mode 100644
index a7b5810f1bf..00000000000
--- a/changelogs/unreleased/25026-authenticate-user-for-new-snippet.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Redirect to sign-in page when unauthenticated user tries to create a snippet
-merge_request: 7786
-author:
diff --git a/changelogs/unreleased/25031-do-not-raise-error-in-autocomplete.yml b/changelogs/unreleased/25031-do-not-raise-error-in-autocomplete.yml
deleted file mode 100644
index 862de7c5db1..00000000000
--- a/changelogs/unreleased/25031-do-not-raise-error-in-autocomplete.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Do not raise error in AutocompleteController#users when not authorized
-merge_request: 7817
-author: Semyon Pupkov
diff --git a/changelogs/unreleased/25093-hide-new-issue-btn-non-loggedin-user.yml b/changelogs/unreleased/25093-hide-new-issue-btn-non-loggedin-user.yml
deleted file mode 100644
index 18836e7a90b..00000000000
--- a/changelogs/unreleased/25093-hide-new-issue-btn-non-loggedin-user.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Hides new issue button for non loggedin user
-merge_request: 8175
-author:
diff --git a/changelogs/unreleased/25098-header-margins-on-pipeline-settings.yml b/changelogs/unreleased/25098-header-margins-on-pipeline-settings.yml
deleted file mode 100644
index 1799fad1631..00000000000
--- a/changelogs/unreleased/25098-header-margins-on-pipeline-settings.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Adjusted margins for Build Status and Coverage Report rows to match those of
- the CI/CD Pipeline row
-merge_request:
-author: Ryan Harris
diff --git a/changelogs/unreleased/25106-hide-issue-mr-button-for-not-loggedin.yml b/changelogs/unreleased/25106-hide-issue-mr-button-for-not-loggedin.yml
deleted file mode 100644
index 62030d3fc45..00000000000
--- a/changelogs/unreleased/25106-hide-issue-mr-button-for-not-loggedin.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Prevent user creating issue or MR without signing in for a group
-merge_request: 7902
-author:
diff --git a/changelogs/unreleased/25134-mobile-issue-view-doesn-t-show-organization-membership.yml b/changelogs/unreleased/25134-mobile-issue-view-doesn-t-show-organization-membership.yml
new file mode 100644
index 00000000000..d35ad0be0db
--- /dev/null
+++ b/changelogs/unreleased/25134-mobile-issue-view-doesn-t-show-organization-membership.yml
@@ -0,0 +1,4 @@
+---
+title: Show organisation membership and delete comment on smaller viewports, plus change comment author name to username
+merge_request:
+author:
diff --git a/changelogs/unreleased/25136-last-deployment-link.yml b/changelogs/unreleased/25136-last-deployment-link.yml
deleted file mode 100644
index eab1534aa66..00000000000
--- a/changelogs/unreleased/25136-last-deployment-link.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix Latest deployment link is broken
-merge_request: 7839
-author:
diff --git a/changelogs/unreleased/25144-gitlab-ce-mattermost-slash-command-for-issue-create-needs-better-documentation.yml b/changelogs/unreleased/25144-gitlab-ce-mattermost-slash-command-for-issue-create-needs-better-documentation.yml
deleted file mode 100644
index 531b0f83099..00000000000
--- a/changelogs/unreleased/25144-gitlab-ce-mattermost-slash-command-for-issue-create-needs-better-documentation.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Improve help message for issue create slash command
-merge_request: 7850
-author:
diff --git a/changelogs/unreleased/25202-fix-mr-widget-content-wrapping.yml b/changelogs/unreleased/25202-fix-mr-widget-content-wrapping.yml
deleted file mode 100644
index 7afc794866b..00000000000
--- a/changelogs/unreleased/25202-fix-mr-widget-content-wrapping.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Centered Accept Merge Request button within MR widget and added padding for
- viewports smaller than 768px
-merge_request:
-author: Ryan Harris
diff --git a/changelogs/unreleased/25207-text-overflow-env-table.yml b/changelogs/unreleased/25207-text-overflow-env-table.yml
deleted file mode 100644
index 69348281a50..00000000000
--- a/changelogs/unreleased/25207-text-overflow-env-table.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Prevent enviroment table to overflow when name has underscores
-merge_request: 8142
-author:
diff --git a/changelogs/unreleased/25221-fix-build-status-overflow-mobile.yml b/changelogs/unreleased/25221-fix-build-status-overflow-mobile.yml
deleted file mode 100644
index 52de34478f0..00000000000
--- a/changelogs/unreleased/25221-fix-build-status-overflow-mobile.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Added top margin to Build status page header for mobile views
-merge_request:
-author: Ryan Harris
diff --git a/changelogs/unreleased/25251-actionview-template-error-undefined-method-text-for-nil-nilclass.yml b/changelogs/unreleased/25251-actionview-template-error-undefined-method-text-for-nil-nilclass.yml
deleted file mode 100644
index 7f1c417bc77..00000000000
--- a/changelogs/unreleased/25251-actionview-template-error-undefined-method-text-for-nil-nilclass.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: 'Fixes "ActionView::Template::Error: undefined method `text?` for nil:NilClass"
- on MR pages'
-merge_request:
-author:
diff --git a/changelogs/unreleased/25264-ref-commit.yml b/changelogs/unreleased/25264-ref-commit.yml
deleted file mode 100644
index 13a33da9801..00000000000
--- a/changelogs/unreleased/25264-ref-commit.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Change ref property to commitRef in vue commit component
-merge_request: 7901
-author:
diff --git a/changelogs/unreleased/25272_fix_comments_tab_disappearing.yml b/changelogs/unreleased/25272_fix_comments_tab_disappearing.yml
deleted file mode 100644
index 79cb2c6d843..00000000000
--- a/changelogs/unreleased/25272_fix_comments_tab_disappearing.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: 'Fix comments activity tab visibility condition'
-merge_request: 7913
-author: Rydkin Maxim
diff --git a/changelogs/unreleased/25294-remove-signed-out-msg.yml b/changelogs/unreleased/25294-remove-signed-out-msg.yml
deleted file mode 100644
index 567294fe5f7..00000000000
--- a/changelogs/unreleased/25294-remove-signed-out-msg.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: 'fix: removed signed_out notification'
-merge_request: 7958
-author: jnoortheen
diff --git a/changelogs/unreleased/25301-git-2-11-force-push-bug.yml b/changelogs/unreleased/25301-git-2-11-force-push-bug.yml
deleted file mode 100644
index afe57729c48..00000000000
--- a/changelogs/unreleased/25301-git-2-11-force-push-bug.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Accept environment variables from the `pre-receive` script
-merge_request: 7967
-author:
diff --git a/changelogs/unreleased/25312-search-input-cmd-click-issue.yml b/changelogs/unreleased/25312-search-input-cmd-click-issue.yml
new file mode 100644
index 00000000000..56e03a48692
--- /dev/null
+++ b/changelogs/unreleased/25312-search-input-cmd-click-issue.yml
@@ -0,0 +1,4 @@
+---
+title: Prevent removal of input fields if it is the parent dropdown element
+merge_request: 8397
+author:
diff --git a/changelogs/unreleased/25324-change-housekeeping-btn-to-default.yml b/changelogs/unreleased/25324-change-housekeeping-btn-to-default.yml
deleted file mode 100644
index 0770f9752a0..00000000000
--- a/changelogs/unreleased/25324-change-housekeeping-btn-to-default.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Changed Housekeeping button on project settings page to default styling
-merge_request:
-author: Ryan Harris
diff --git a/changelogs/unreleased/25339-2-webhooks-fired-for-issue-closed-and-reopened.yml b/changelogs/unreleased/25339-2-webhooks-fired-for-issue-closed-and-reopened.yml
deleted file mode 100644
index b12eab26b67..00000000000
--- a/changelogs/unreleased/25339-2-webhooks-fired-for-issue-closed-and-reopened.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Ensure issuable state changes only fire webhooks once
-merge_request:
-author:
diff --git a/changelogs/unreleased/25360-remove-flash-warning-from-login-page.yml b/changelogs/unreleased/25360-remove-flash-warning-from-login-page.yml
new file mode 100644
index 00000000000..50a5c879446
--- /dev/null
+++ b/changelogs/unreleased/25360-remove-flash-warning-from-login-page.yml
@@ -0,0 +1,4 @@
+---
+title: Remove flash warning from login page
+merge_request: 8864
+author: Gerald J. Padilla
diff --git a/changelogs/unreleased/25374-svg-as-prop.yml b/changelogs/unreleased/25374-svg-as-prop.yml
deleted file mode 100644
index 45a71b55b3b..00000000000
--- a/changelogs/unreleased/25374-svg-as-prop.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Resolve "Provide SVG as a prop instead of hiding and copy them in environments table"
-merge_request: 7992
-author:
diff --git a/changelogs/unreleased/25460-replace-word-users-with-members.yml b/changelogs/unreleased/25460-replace-word-users-with-members.yml
new file mode 100644
index 00000000000..dac90eaa34d
--- /dev/null
+++ b/changelogs/unreleased/25460-replace-word-users-with-members.yml
@@ -0,0 +1,4 @@
+---
+title: Replace word user with member
+merge_request: 8872
+author:
diff --git a/changelogs/unreleased/25482-fix-api-sudo.yml b/changelogs/unreleased/25482-fix-api-sudo.yml
deleted file mode 100644
index 4c11fe1622e..00000000000
--- a/changelogs/unreleased/25482-fix-api-sudo.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: 'API: Memoize the current_user so that sudo can work properly'
-merge_request: 8017
-author:
diff --git a/changelogs/unreleased/25483-broken-tabs.yml b/changelogs/unreleased/25483-broken-tabs.yml
deleted file mode 100644
index d6c92014bea..00000000000
--- a/changelogs/unreleased/25483-broken-tabs.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix TypeError: Cannot read property 'initTabs' on commit builds tab
-merge_request: 8009
-author:
diff --git a/changelogs/unreleased/25534-adding-a-way-to-go-back-on-error-pages.yml b/changelogs/unreleased/25534-adding-a-way-to-go-back-on-error-pages.yml
deleted file mode 100644
index c6a92547c5c..00000000000
--- a/changelogs/unreleased/25534-adding-a-way-to-go-back-on-error-pages.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Added go back anchor on error pages.
-merge_request: 8087
-author:
diff --git a/changelogs/unreleased/25617-todos-filter-placeholder.yml b/changelogs/unreleased/25617-todos-filter-placeholder.yml
deleted file mode 100644
index 5d0adb04ef3..00000000000
--- a/changelogs/unreleased/25617-todos-filter-placeholder.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: 25617 Fix placeholder color of todo filters
-merge_request:
-author:
diff --git a/changelogs/unreleased/25624-anticipate-obstacles-to-removing-turbolinks.yml b/changelogs/unreleased/25624-anticipate-obstacles-to-removing-turbolinks.yml
new file mode 100644
index 00000000000..d7f950d7be9
--- /dev/null
+++ b/changelogs/unreleased/25624-anticipate-obstacles-to-removing-turbolinks.yml
@@ -0,0 +1,4 @@
+---
+title: Remove turbolinks.
+merge_request: !8570
+author:
diff --git a/changelogs/unreleased/25678-remove-user-build.yml b/changelogs/unreleased/25678-remove-user-build.yml
deleted file mode 100644
index 873e637d670..00000000000
--- a/changelogs/unreleased/25678-remove-user-build.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: remove build_user
-merge_request: 8162
-author: Arsenev Vladislav
diff --git a/changelogs/unreleased/25740-fix-new-branch-button-padding.yml b/changelogs/unreleased/25740-fix-new-branch-button-padding.yml
deleted file mode 100644
index 7da8f9357a7..00000000000
--- a/changelogs/unreleased/25740-fix-new-branch-button-padding.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Made the padding on the plus button in the breadcrumb menu even
-merge_request:
-author: Ryan Harris
diff --git a/changelogs/unreleased/25743-clean-up-css-for-project-alerts-and-flash-notifications.yml b/changelogs/unreleased/25743-clean-up-css-for-project-alerts-and-flash-notifications.yml
deleted file mode 100644
index 0a81124de0d..00000000000
--- a/changelogs/unreleased/25743-clean-up-css-for-project-alerts-and-flash-notifications.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: fix colors and margins for adjacent alert banners
-merge_request: 8151
-author:
diff --git a/changelogs/unreleased/25811-pipeline-number-and-url-do-not-update-when-new-pipeline-is-triggered.yml b/changelogs/unreleased/25811-pipeline-number-and-url-do-not-update-when-new-pipeline-is-triggered.yml
new file mode 100644
index 00000000000..f74e9fa8b6d
--- /dev/null
+++ b/changelogs/unreleased/25811-pipeline-number-and-url-do-not-update-when-new-pipeline-is-triggered.yml
@@ -0,0 +1,4 @@
+---
+title: Update pipeline and commit links when CI status is updated
+merge_request: 8351
+author:
diff --git a/changelogs/unreleased/25910-convert-manual-action-icons-to-svg-to-propperly-position-them.yml b/changelogs/unreleased/25910-convert-manual-action-icons-to-svg-to-propperly-position-them.yml
new file mode 100644
index 00000000000..9506692dd40
--- /dev/null
+++ b/changelogs/unreleased/25910-convert-manual-action-icons-to-svg-to-propperly-position-them.yml
@@ -0,0 +1,4 @@
+---
+title: Convert pipeline action icons to svg to have them propperly positioned
+merge_request:
+author:
diff --git a/changelogs/unreleased/25989-fix-rogue-scrollbars-on-comments.yml b/changelogs/unreleased/25989-fix-rogue-scrollbars-on-comments.yml
new file mode 100644
index 00000000000..e67a9c0da15
--- /dev/null
+++ b/changelogs/unreleased/25989-fix-rogue-scrollbars-on-comments.yml
@@ -0,0 +1,4 @@
+---
+title: Remove rogue scrollbars for issue comments with inline elements
+merge_request:
+author:
diff --git a/changelogs/unreleased/26059-segoe-ui-vertical.yml b/changelogs/unreleased/26059-segoe-ui-vertical.yml
new file mode 100644
index 00000000000..fc3f1af5b61
--- /dev/null
+++ b/changelogs/unreleased/26059-segoe-ui-vertical.yml
@@ -0,0 +1,4 @@
+---
+title: Align Segoe UI label text
+merge_request:
+author:
diff --git a/changelogs/unreleased/26068_tasklist_issue.yml b/changelogs/unreleased/26068_tasklist_issue.yml
new file mode 100644
index 00000000000..c938351b8a7
--- /dev/null
+++ b/changelogs/unreleased/26068_tasklist_issue.yml
@@ -0,0 +1,4 @@
+---
+title: Don’t count tasks that are not defined as list items correctly
+merge_request: 8526
+author:
diff --git a/changelogs/unreleased/26117-sort-pipeline-for-commit.yml b/changelogs/unreleased/26117-sort-pipeline-for-commit.yml
new file mode 100644
index 00000000000..b2f5294d380
--- /dev/null
+++ b/changelogs/unreleased/26117-sort-pipeline-for-commit.yml
@@ -0,0 +1,4 @@
+---
+title: Add sorting pipeline for a commit
+merge_request: 8319
+author: Takuya Noguchi
diff --git a/changelogs/unreleased/26186-diff-view-plus-and-minus-signs-as-part-of-line-number.yml b/changelogs/unreleased/26186-diff-view-plus-and-minus-signs-as-part-of-line-number.yml
new file mode 100644
index 00000000000..565672917b2
--- /dev/null
+++ b/changelogs/unreleased/26186-diff-view-plus-and-minus-signs-as-part-of-line-number.yml
@@ -0,0 +1,4 @@
+---
+title: Color + and - signs in diffs to increase code legibility
+merge_request:
+author:
diff --git a/changelogs/unreleased/26445-accessible-piplelines-buttons.yml b/changelogs/unreleased/26445-accessible-piplelines-buttons.yml
new file mode 100644
index 00000000000..fb5274e5253
--- /dev/null
+++ b/changelogs/unreleased/26445-accessible-piplelines-buttons.yml
@@ -0,0 +1,4 @@
+---
+title: Improve button accessibility on pipelines page
+merge_request: 8561
+author:
diff --git a/changelogs/unreleased/26447-fix-tab-list-order.yml b/changelogs/unreleased/26447-fix-tab-list-order.yml
new file mode 100644
index 00000000000..351c53bd076
--- /dev/null
+++ b/changelogs/unreleased/26447-fix-tab-list-order.yml
@@ -0,0 +1,4 @@
+---
+title: Fix tab index order on branch commits list page
+merge_request:
+author: Ryan Harris
diff --git a/changelogs/unreleased/26468-fix-users-sort-in-admin-area.yml b/changelogs/unreleased/26468-fix-users-sort-in-admin-area.yml
new file mode 100644
index 00000000000..87ae8233c4a
--- /dev/null
+++ b/changelogs/unreleased/26468-fix-users-sort-in-admin-area.yml
@@ -0,0 +1,4 @@
+---
+title: Fix Sort by Recent Sign-in in Admin Area
+merge_request: 8637
+author: Poornima M
diff --git a/changelogs/unreleased/26787-add-copy-icon-hover-state.yml b/changelogs/unreleased/26787-add-copy-icon-hover-state.yml
new file mode 100644
index 00000000000..31f1812c6f8
--- /dev/null
+++ b/changelogs/unreleased/26787-add-copy-icon-hover-state.yml
@@ -0,0 +1,4 @@
+---
+title: Add hover style to copy icon on commit page header
+merge_request:
+author: Ryan Harris
diff --git a/changelogs/unreleased/26852-fix-slug-for-openshift.yml b/changelogs/unreleased/26852-fix-slug-for-openshift.yml
new file mode 100644
index 00000000000..fb65b068b23
--- /dev/null
+++ b/changelogs/unreleased/26852-fix-slug-for-openshift.yml
@@ -0,0 +1,4 @@
+---
+title: Avoid repeated dashes in $CI_ENVIRONMENT_SLUG
+merge_request: 8638
+author:
diff --git a/changelogs/unreleased/26863-Remove-hover-animation-from-row-elements.yml b/changelogs/unreleased/26863-Remove-hover-animation-from-row-elements.yml
new file mode 100644
index 00000000000..8dfabf87c2a
--- /dev/null
+++ b/changelogs/unreleased/26863-Remove-hover-animation-from-row-elements.yml
@@ -0,0 +1,4 @@
+---
+title: Remove hover animation from row elements
+merge_request:
+author:
diff --git a/changelogs/unreleased/26908-make-timelogs-use-foreign-keys b/changelogs/unreleased/26908-make-timelogs-use-foreign-keys
new file mode 100644
index 00000000000..0e8f7093b34
--- /dev/null
+++ b/changelogs/unreleased/26908-make-timelogs-use-foreign-keys
@@ -0,0 +1,4 @@
+---
+title: Refactor Timelogs structure to use foreign keys.
+merge_request: 8769
+author:
diff --git a/changelogs/unreleased/26920-hover-cursor-on-pagination-element.yml b/changelogs/unreleased/26920-hover-cursor-on-pagination-element.yml
new file mode 100644
index 00000000000..ea567437ac2
--- /dev/null
+++ b/changelogs/unreleased/26920-hover-cursor-on-pagination-element.yml
@@ -0,0 +1,4 @@
+---
+title: Fixes hover cursor on pipeline pagenation
+merge_request: 9003
+author:
diff --git a/changelogs/unreleased/26947-build-status-self-link.yml b/changelogs/unreleased/26947-build-status-self-link.yml
new file mode 100644
index 00000000000..15c5821874e
--- /dev/null
+++ b/changelogs/unreleased/26947-build-status-self-link.yml
@@ -0,0 +1,4 @@
+---
+title: Add link verification to badge partial in order to render a badge without a link
+merge_request: 8740
+author:
diff --git a/changelogs/unreleased/26982-improve-pipeline-status-icon-linking-in-widgets.yml b/changelogs/unreleased/26982-improve-pipeline-status-icon-linking-in-widgets.yml
new file mode 100644
index 00000000000..c5c57af5aaf
--- /dev/null
+++ b/changelogs/unreleased/26982-improve-pipeline-status-icon-linking-in-widgets.yml
@@ -0,0 +1,4 @@
+---
+title: Improve pipeline status icon linking in widgets
+merge_request:
+author:
diff --git a/changelogs/unreleased/27013-regression-in-commit-title-bar.yml b/changelogs/unreleased/27013-regression-in-commit-title-bar.yml
new file mode 100644
index 00000000000..7cb5e4b273d
--- /dev/null
+++ b/changelogs/unreleased/27013-regression-in-commit-title-bar.yml
@@ -0,0 +1,4 @@
+---
+title: Fix commit title bar and repository view copy clipboard button order on last commit in repository view
+merge_request:
+author:
diff --git a/changelogs/unreleased/27014-fix-pipeline-tooltip-wrapping.yml b/changelogs/unreleased/27014-fix-pipeline-tooltip-wrapping.yml
new file mode 100644
index 00000000000..f0301c849b6
--- /dev/null
+++ b/changelogs/unreleased/27014-fix-pipeline-tooltip-wrapping.yml
@@ -0,0 +1,4 @@
+---
+title: Fix mini-pipeline stage tooltip text wrapping
+merge_request:
+author:
diff --git a/changelogs/unreleased/27021-line-numbers-now-in-copy-pasta-data.yml b/changelogs/unreleased/27021-line-numbers-now-in-copy-pasta-data.yml
new file mode 100644
index 00000000000..b5584749098
--- /dev/null
+++ b/changelogs/unreleased/27021-line-numbers-now-in-copy-pasta-data.yml
@@ -0,0 +1,4 @@
+---
+title: Prevent copying of line numbers in parallel diff view
+merge_request: 8706
+author:
diff --git a/changelogs/unreleased/27178-update-builds-link-in-project-settings.yml b/changelogs/unreleased/27178-update-builds-link-in-project-settings.yml
new file mode 100644
index 00000000000..52406bba464
--- /dev/null
+++ b/changelogs/unreleased/27178-update-builds-link-in-project-settings.yml
@@ -0,0 +1,4 @@
+---
+title: Updated builds info link on the project settings page
+merge_request:
+author: Ryan Harris
diff --git a/changelogs/unreleased/27240-make-progress-bars-consistent.yml b/changelogs/unreleased/27240-make-progress-bars-consistent.yml
new file mode 100644
index 00000000000..3f902fb324e
--- /dev/null
+++ b/changelogs/unreleased/27240-make-progress-bars-consistent.yml
@@ -0,0 +1,4 @@
+---
+title: 27240 Make progress bars consistent
+merge_request:
+author:
diff --git a/changelogs/unreleased/27267-unnecessary-queries-from-projects-dashboard-atom-and-json.yml b/changelogs/unreleased/27267-unnecessary-queries-from-projects-dashboard-atom-and-json.yml
new file mode 100644
index 00000000000..7b307b501f4
--- /dev/null
+++ b/changelogs/unreleased/27267-unnecessary-queries-from-projects-dashboard-atom-and-json.yml
@@ -0,0 +1,4 @@
+---
+title: Remove unnecessary queries for .atom and .json in Dashboard::ProjectsController#index
+merge_request: 8956
+author:
diff --git a/changelogs/unreleased/27277-small-mini-pipeline-graph-glitch-upon-hover.yml b/changelogs/unreleased/27277-small-mini-pipeline-graph-glitch-upon-hover.yml
new file mode 100644
index 00000000000..9456251025b
--- /dev/null
+++ b/changelogs/unreleased/27277-small-mini-pipeline-graph-glitch-upon-hover.yml
@@ -0,0 +1,4 @@
+---
+title: fixed small mini pipeline graph line glitch
+merge_request: 8804
+author:
diff --git a/changelogs/unreleased/27291-unify-mr-diff-file-buttons.yml b/changelogs/unreleased/27291-unify-mr-diff-file-buttons.yml
new file mode 100644
index 00000000000..293aab67d39
--- /dev/null
+++ b/changelogs/unreleased/27291-unify-mr-diff-file-buttons.yml
@@ -0,0 +1,4 @@
+---
+title: Unify MR diff file button style
+merge_request: 8874
+author:
diff --git a/changelogs/unreleased/27321-double-separator-line-in-edit-projects-settings.yml b/changelogs/unreleased/27321-double-separator-line-in-edit-projects-settings.yml
new file mode 100644
index 00000000000..502927cd160
--- /dev/null
+++ b/changelogs/unreleased/27321-double-separator-line-in-edit-projects-settings.yml
@@ -0,0 +1,4 @@
+---
+title: Only render hr when user can't archive project.
+merge_request: !8917
+author:
diff --git a/changelogs/unreleased/27332-mini-pipeline-graph-with-many-stages-has-no-line-spacing-in-firefox-and-safari.yml b/changelogs/unreleased/27332-mini-pipeline-graph-with-many-stages-has-no-line-spacing-in-firefox-and-safari.yml
new file mode 100644
index 00000000000..79316abbaf7
--- /dev/null
+++ b/changelogs/unreleased/27332-mini-pipeline-graph-with-many-stages-has-no-line-spacing-in-firefox-and-safari.yml
@@ -0,0 +1,4 @@
+---
+title: Fix pipeline graph vertical spacing in Firefox and Safari
+merge_request: 8886
+author:
diff --git a/changelogs/unreleased/27343-autocomplete-post-to-wrong-url-when-not-hosting-in-root.yml b/changelogs/unreleased/27343-autocomplete-post-to-wrong-url-when-not-hosting-in-root.yml
new file mode 100644
index 00000000000..8f061a34ac0
--- /dev/null
+++ b/changelogs/unreleased/27343-autocomplete-post-to-wrong-url-when-not-hosting-in-root.yml
@@ -0,0 +1,5 @@
+---
+title: Fix filtered search user autocomplete for gitlab instances that are hosted
+ on a subdirectory
+merge_request: 8891
+author:
diff --git a/changelogs/unreleased/27352-search-label-filter-header.yml b/changelogs/unreleased/27352-search-label-filter-header.yml
new file mode 100644
index 00000000000..191b530aee8
--- /dev/null
+++ b/changelogs/unreleased/27352-search-label-filter-header.yml
@@ -0,0 +1,4 @@
+---
+title: 27352-search-label-filter-header
+merge_request:
+author:
diff --git a/changelogs/unreleased/27484-environment-show-name.yml b/changelogs/unreleased/27484-environment-show-name.yml
new file mode 100644
index 00000000000..dc400d65006
--- /dev/null
+++ b/changelogs/unreleased/27484-environment-show-name.yml
@@ -0,0 +1,4 @@
+---
+title: Don't capitalize environment name in show page
+merge_request:
+author:
diff --git a/changelogs/unreleased/27488-fix-jwt-version.yml b/changelogs/unreleased/27488-fix-jwt-version.yml
new file mode 100644
index 00000000000..5135ff0fd60
--- /dev/null
+++ b/changelogs/unreleased/27488-fix-jwt-version.yml
@@ -0,0 +1,4 @@
+---
+title: Update and pin the `jwt` gem to ~> 1.5.6
+merge_request:
+author:
diff --git a/changelogs/unreleased/27494-environment-list-column-headers.yml b/changelogs/unreleased/27494-environment-list-column-headers.yml
new file mode 100644
index 00000000000..798c01f3238
--- /dev/null
+++ b/changelogs/unreleased/27494-environment-list-column-headers.yml
@@ -0,0 +1,4 @@
+---
+title: Edited the column header for the environments list from created to updated and added created to environments detail page colum header titles
+merge_request:
+author:
diff --git a/changelogs/unreleased/27516-fix-wrong-call-to-project_cache_worker-method.yml b/changelogs/unreleased/27516-fix-wrong-call-to-project_cache_worker-method.yml
new file mode 100644
index 00000000000..bc990c66866
--- /dev/null
+++ b/changelogs/unreleased/27516-fix-wrong-call-to-project_cache_worker-method.yml
@@ -0,0 +1,4 @@
+---
+title: Fix wrong call to ProjectCacheWorker.perform
+merge_request: 8910
+author:
diff --git a/changelogs/unreleased/27602-fix-avatar-border-flicker-mention-dropdown.yml b/changelogs/unreleased/27602-fix-avatar-border-flicker-mention-dropdown.yml
new file mode 100644
index 00000000000..a5bb37ec8a9
--- /dev/null
+++ b/changelogs/unreleased/27602-fix-avatar-border-flicker-mention-dropdown.yml
@@ -0,0 +1,4 @@
+---
+title: Fixes flickering of avatar border in mention dropdown
+merge_request: 8950
+author:
diff --git a/changelogs/unreleased/27632_fix_mr_widget_url.yml b/changelogs/unreleased/27632_fix_mr_widget_url.yml
new file mode 100644
index 00000000000..958621a43a1
--- /dev/null
+++ b/changelogs/unreleased/27632_fix_mr_widget_url.yml
@@ -0,0 +1,4 @@
+---
+title: Fix MR widget url
+merge_request: 8989
+author:
diff --git a/changelogs/unreleased/27639-emoji-panel-under-the-side-panel-in-the-merge-request.yml b/changelogs/unreleased/27639-emoji-panel-under-the-side-panel-in-the-merge-request.yml
new file mode 100644
index 00000000000..0531ef2c038
--- /dev/null
+++ b/changelogs/unreleased/27639-emoji-panel-under-the-side-panel-in-the-merge-request.yml
@@ -0,0 +1,4 @@
+---
+title: Layer award emoji dropdown over the right sidebar
+merge_request: 9004
+author:
diff --git a/changelogs/unreleased/27774-text-color-contrast-is-barely-readable-for-pipelines-visualization-graph-page-with-roboto-fonts.yml b/changelogs/unreleased/27774-text-color-contrast-is-barely-readable-for-pipelines-visualization-graph-page-with-roboto-fonts.yml
new file mode 100644
index 00000000000..aa89d9f9850
--- /dev/null
+++ b/changelogs/unreleased/27774-text-color-contrast-is-barely-readable-for-pipelines-visualization-graph-page-with-roboto-fonts.yml
@@ -0,0 +1,4 @@
+---
+title: Give ci status text on pipeline graph a better font-weight
+merge_request:
+author:
diff --git a/changelogs/unreleased/27822-default-bulk-assign-labels.yml b/changelogs/unreleased/27822-default-bulk-assign-labels.yml
new file mode 100644
index 00000000000..ee2431869f0
--- /dev/null
+++ b/changelogs/unreleased/27822-default-bulk-assign-labels.yml
@@ -0,0 +1,4 @@
+---
+title: Add default labels to bulk assign dropdowns
+merge_request:
+author:
diff --git a/changelogs/unreleased/395-fix-notification-when-group-set-to-watch.yml b/changelogs/unreleased/395-fix-notification-when-group-set-to-watch.yml
new file mode 100644
index 00000000000..11d1f55172b
--- /dev/null
+++ b/changelogs/unreleased/395-fix-notification-when-group-set-to-watch.yml
@@ -0,0 +1,4 @@
+---
+title: Fix notifications when set at group level
+merge_request: 6813
+author: Alexandre Maia
diff --git a/changelogs/unreleased/4269-public-api.yml b/changelogs/unreleased/4269-public-api.yml
deleted file mode 100644
index 9de739d0cad..00000000000
--- a/changelogs/unreleased/4269-public-api.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Allow unauthenticated access to some Project API GET endpoints
-merge_request: 7843
-author:
diff --git a/changelogs/unreleased/4269-public-files-api.yml b/changelogs/unreleased/4269-public-files-api.yml
deleted file mode 100644
index e8f9e9b5ed3..00000000000
--- a/changelogs/unreleased/4269-public-files-api.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Allow unauthenticated access to Repositories Files API GET endpoints
-merge_request:
-author:
diff --git a/changelogs/unreleased/4269-public-repositories-api.yml b/changelogs/unreleased/4269-public-repositories-api.yml
deleted file mode 100644
index 38984eed904..00000000000
--- a/changelogs/unreleased/4269-public-repositories-api.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Allow unauthenticated access to Repositories API GET endpoints
-merge_request: 8148
-author:
diff --git a/changelogs/unreleased/7749-add-setting-to-disable-html-emails.yml b/changelogs/unreleased/7749-add-setting-to-disable-html-emails.yml
deleted file mode 100644
index 9dd04d3f089..00000000000
--- a/changelogs/unreleased/7749-add-setting-to-disable-html-emails.yml
+++ /dev/null
@@ -1,3 +0,0 @@
-title: Add setting to enable/disable HTML emails
-merge_request: 7749
-author:
diff --git a/changelogs/unreleased/8-15-stable.yml b/changelogs/unreleased/8-15-stable.yml
new file mode 100644
index 00000000000..75502e139e7
--- /dev/null
+++ b/changelogs/unreleased/8-15-stable.yml
@@ -0,0 +1,4 @@
+---
+title: Ensure export files are removed after a namespace is deleted
+merge_request:
+author:
diff --git a/changelogs/unreleased/8003-katex-math.yml b/changelogs/unreleased/8003-katex-math.yml
deleted file mode 100644
index a40dcde1393..00000000000
--- a/changelogs/unreleased/8003-katex-math.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Added support for math rendering, using KaTeX, in Markdown and asciidoc
-merge_request: 8003
-author: Munken
diff --git a/changelogs/unreleased/8082-permalink-to-file.yml b/changelogs/unreleased/8082-permalink-to-file.yml
new file mode 100644
index 00000000000..136d2108c63
--- /dev/null
+++ b/changelogs/unreleased/8082-permalink-to-file.yml
@@ -0,0 +1,4 @@
+---
+title: Add `y` keyboard shortcut to move to file permalink
+merge_request:
+author:
diff --git a/changelogs/unreleased/9-0-api-changes.yml b/changelogs/unreleased/9-0-api-changes.yml
new file mode 100644
index 00000000000..2f0f1887257
--- /dev/null
+++ b/changelogs/unreleased/9-0-api-changes.yml
@@ -0,0 +1,4 @@
+---
+title: Remove deprecated MR and Issue endpoints and preserve V3 namespace
+merge_request: 8967
+author:
diff --git a/changelogs/unreleased/Add-a-shash-command-for-target-merge-request-branch.yml b/changelogs/unreleased/Add-a-shash-command-for-target-merge-request-branch.yml
new file mode 100644
index 00000000000..9fd6ea5bc52
--- /dev/null
+++ b/changelogs/unreleased/Add-a-shash-command-for-target-merge-request-branch.yml
@@ -0,0 +1,4 @@
+---
+title: Adds /target_branch slash command functionality for merge requests
+merge_request:
+author: YarNayar
diff --git a/changelogs/unreleased/abuse_report-fixture.yml b/changelogs/unreleased/abuse_report-fixture.yml
deleted file mode 100644
index 47478a2048b..00000000000
--- a/changelogs/unreleased/abuse_report-fixture.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Replace static fixture for abuse_reports_spec
-merge_request: 7644
-author: winniehell
diff --git a/changelogs/unreleased/add_info_to_qr.yml b/changelogs/unreleased/add_info_to_qr.yml
deleted file mode 100644
index a4b0354a9c9..00000000000
--- a/changelogs/unreleased/add_info_to_qr.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add GitLab host to 2FA QR code and manual info
-merge_request: 6941
-author:
diff --git a/changelogs/unreleased/add_project_update_hook.yml b/changelogs/unreleased/add_project_update_hook.yml
new file mode 100644
index 00000000000..915c9538843
--- /dev/null
+++ b/changelogs/unreleased/add_project_update_hook.yml
@@ -0,0 +1,4 @@
+---
+title: Add system hook for when a project is updated (other than rename/transfer)
+merge_request: 5711
+author: Tommy Beadle
diff --git a/changelogs/unreleased/allow-more-filenames.yml b/changelogs/unreleased/allow-more-filenames.yml
deleted file mode 100644
index 7989f94e528..00000000000
--- a/changelogs/unreleased/allow-more-filenames.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Allow all alphanumeric characters in file names
-merge_request: 8002
-author: winniehell
diff --git a/changelogs/unreleased/api-branch-status.yml b/changelogs/unreleased/api-branch-status.yml
deleted file mode 100644
index c5763345a22..00000000000
--- a/changelogs/unreleased/api-branch-status.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: 'API: Expose merge status for branch API'
-merge_request:
-author: Robert Schilling
diff --git a/changelogs/unreleased/api-cherry-pick.yml b/changelogs/unreleased/api-cherry-pick.yml
deleted file mode 100644
index 5f4cee450b9..00000000000
--- a/changelogs/unreleased/api-cherry-pick.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: 'API: Ability to cherry pick a commit'
-merge_request: 8047
-author: Robert Schilling
diff --git a/changelogs/unreleased/api-delete-group-share.yml b/changelogs/unreleased/api-delete-group-share.yml
deleted file mode 100644
index 26cfb35bba3..00000000000
--- a/changelogs/unreleased/api-delete-group-share.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: 'API: Add ability to unshare a project from a group'
-merge_request: 7662
-author: Robert Schilling
diff --git a/changelogs/unreleased/api-expose-commiter-details.yml b/changelogs/unreleased/api-expose-commiter-details.yml
deleted file mode 100644
index 5ee34adc5c9..00000000000
--- a/changelogs/unreleased/api-expose-commiter-details.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: 'API: Expose committer details for commits'
-merge_request:
-author: Robert Schilling
diff --git a/changelogs/unreleased/api-fix-files.yml b/changelogs/unreleased/api-fix-files.yml
new file mode 100644
index 00000000000..8a9e29109a8
--- /dev/null
+++ b/changelogs/unreleased/api-fix-files.yml
@@ -0,0 +1,4 @@
+---
+title: 'API: Fix file downloading'
+merge_request: Robert Schilling
+author: 8267
diff --git a/changelogs/unreleased/api-remove-snippets-expires-at.yml b/changelogs/unreleased/api-remove-snippets-expires-at.yml
new file mode 100644
index 00000000000..67603bfab3b
--- /dev/null
+++ b/changelogs/unreleased/api-remove-snippets-expires-at.yml
@@ -0,0 +1,4 @@
+---
+title: 'API: Remove deprecated ''expires_at'' from project snippets'
+merge_request: 8723
+author: Robert Schilling
diff --git a/changelogs/unreleased/api-remove-source-branch.yml b/changelogs/unreleased/api-remove-source-branch.yml
deleted file mode 100644
index d1b6507aedb..00000000000
--- a/changelogs/unreleased/api-remove-source-branch.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: 'API: Ability to set ''should_remove_source_branch'' on merge requests'
-merge_request:
-author: Robert Schilling
diff --git a/changelogs/unreleased/api-simple-group-project.yml b/changelogs/unreleased/api-simple-group-project.yml
deleted file mode 100644
index 54c8de610a6..00000000000
--- a/changelogs/unreleased/api-simple-group-project.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: 'API: Simple representation of group''s projects'
-merge_request: 8060
-author: Robert Schilling
diff --git a/changelogs/unreleased/awards_handler.yml b/changelogs/unreleased/awards_handler.yml
deleted file mode 100644
index 1f9904c0691..00000000000
--- a/changelogs/unreleased/awards_handler.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Replace static fixture for awards_handler_spec
-merge_request: 7661
-author: winniehell
diff --git a/changelogs/unreleased/babel-all-the-things.yml b/changelogs/unreleased/babel-all-the-things.yml
new file mode 100644
index 00000000000..fda1c3bd562
--- /dev/null
+++ b/changelogs/unreleased/babel-all-the-things.yml
@@ -0,0 +1,5 @@
+---
+title: use babel to transpile all non-vendor javascript assets regardless of file
+ extension
+merge_request: 8988
+author:
diff --git a/changelogs/unreleased/bitbucket-oauth2.yml b/changelogs/unreleased/bitbucket-oauth2.yml
deleted file mode 100644
index 97d82518b7b..00000000000
--- a/changelogs/unreleased/bitbucket-oauth2.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Refactor Bitbucket importer to use BitBucket API Version 2
-merge_request:
-author:
diff --git a/changelogs/unreleased/broken_iamge_when_doing_offline_update_check-help_page-.yml b/changelogs/unreleased/broken_iamge_when_doing_offline_update_check-help_page-.yml
new file mode 100644
index 00000000000..77750b55e7e
--- /dev/null
+++ b/changelogs/unreleased/broken_iamge_when_doing_offline_update_check-help_page-.yml
@@ -0,0 +1,4 @@
+---
+title: Hide version check image if there is no internet connection
+merge_request: 8355
+author: Ken Ding
diff --git a/changelogs/unreleased/change_development_build_fixtures.yml b/changelogs/unreleased/change_development_build_fixtures.yml
deleted file mode 100644
index b5dc3792745..00000000000
--- a/changelogs/unreleased/change_development_build_fixtures.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Ci::Builds have same ref as Ci::Pipeline in dev fixtures
-merge_request:
-author: twonegatives
diff --git a/changelogs/unreleased/chomp-git-status-message.yml b/changelogs/unreleased/chomp-git-status-message.yml
deleted file mode 100644
index f70607df7a1..00000000000
--- a/changelogs/unreleased/chomp-git-status-message.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: For single line git commit messages, the close quote should be on the same
- line as the open quote
-merge_request:
-author:
diff --git a/changelogs/unreleased/cleanup-common_utils-js.yml b/changelogs/unreleased/cleanup-common_utils-js.yml
deleted file mode 100644
index 54d81b76c28..00000000000
--- a/changelogs/unreleased/cleanup-common_utils-js.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Clean up common_utils.js
-merge_request: 7318
-author: winniehell
diff --git a/changelogs/unreleased/clipboard-button-commit-sha.yml b/changelogs/unreleased/clipboard-button-commit-sha.yml
new file mode 100644
index 00000000000..6aa4a5664e7
--- /dev/null
+++ b/changelogs/unreleased/clipboard-button-commit-sha.yml
@@ -0,0 +1,3 @@
+---
+title: 'Copy commit SHA to clipboard'
+merge_request: 8547
diff --git a/changelogs/unreleased/comments-fixture.yml b/changelogs/unreleased/comments-fixture.yml
deleted file mode 100644
index 824c1c88a60..00000000000
--- a/changelogs/unreleased/comments-fixture.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Replace static fixture for notes_spec
-merge_request: 7683
-author: winniehell
diff --git a/changelogs/unreleased/contribution-calendar-scroll.yml b/changelogs/unreleased/contribution-calendar-scroll.yml
new file mode 100644
index 00000000000..a504d59e61c
--- /dev/null
+++ b/changelogs/unreleased/contribution-calendar-scroll.yml
@@ -0,0 +1,4 @@
+---
+title: contribution calendar scrolls from right to left
+merge_request:
+author:
diff --git a/changelogs/unreleased/cop-gem-fetcher.yml b/changelogs/unreleased/cop-gem-fetcher.yml
new file mode 100644
index 00000000000..506815a5b54
--- /dev/null
+++ b/changelogs/unreleased/cop-gem-fetcher.yml
@@ -0,0 +1,4 @@
+---
+title: Cop for gem fetched from a git source
+merge_request: 8856
+author: Adam Pahlevi
diff --git a/changelogs/unreleased/copy-as-md.yml b/changelogs/unreleased/copy-as-md.yml
new file mode 100644
index 00000000000..637e9dc36e2
--- /dev/null
+++ b/changelogs/unreleased/copy-as-md.yml
@@ -0,0 +1,4 @@
+---
+title: Copying a rendered issue/comment will paste into GFM textareas as actual GFM
+merge_request:
+author:
diff --git a/changelogs/unreleased/create-dynamic-fixture-for-build_spec.yml b/changelogs/unreleased/create-dynamic-fixture-for-build_spec.yml
deleted file mode 100644
index f0d9ff0c34f..00000000000
--- a/changelogs/unreleased/create-dynamic-fixture-for-build_spec.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Create dynamic fixture for build_spec
-merge_request: 7589
-author: winniehell
diff --git a/changelogs/unreleased/destroy-session.yml b/changelogs/unreleased/destroy-session.yml
deleted file mode 100644
index e713e2dc424..00000000000
--- a/changelogs/unreleased/destroy-session.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Destroy a user's session when they delete their own account
-merge_request:
-author:
diff --git a/changelogs/unreleased/dev-issue-24554.yml b/changelogs/unreleased/dev-issue-24554.yml
deleted file mode 100644
index 0bb362b9325..00000000000
--- a/changelogs/unreleased/dev-issue-24554.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Edit help text to clarify annotated tag creation.
-merge_request:
-author: Liz Lam
diff --git a/changelogs/unreleased/disable-autologin-on-email-confirmation-links.yml b/changelogs/unreleased/disable-autologin-on-email-confirmation-links.yml
new file mode 100644
index 00000000000..6dd0d748001
--- /dev/null
+++ b/changelogs/unreleased/disable-autologin-on-email-confirmation-links.yml
@@ -0,0 +1,4 @@
+---
+title: Disable automatic login after clicking email confirmation links
+merge_request: 7472
+author:
diff --git a/changelogs/unreleased/display-project-id.yml b/changelogs/unreleased/display-project-id.yml
new file mode 100644
index 00000000000..8705ed28400
--- /dev/null
+++ b/changelogs/unreleased/display-project-id.yml
@@ -0,0 +1,4 @@
+---
+title: Display project ID in project settings
+merge_request: 8572
+author: winniehell
diff --git a/changelogs/unreleased/do-not-refresh-main-when-fork-target-branch-updated.yml b/changelogs/unreleased/do-not-refresh-main-when-fork-target-branch-updated.yml
deleted file mode 100644
index 12b1460f388..00000000000
--- a/changelogs/unreleased/do-not-refresh-main-when-fork-target-branch-updated.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Do not reload diff for merge request made from fork when target branch in fork is updated
-merge_request: 7973
-author:
diff --git a/changelogs/unreleased/dockerfile-templates.yml b/changelogs/unreleased/dockerfile-templates.yml
deleted file mode 100644
index e4db46cdf9a..00000000000
--- a/changelogs/unreleased/dockerfile-templates.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add support for Dockerfile templates
-merge_request: 7247
-author:
diff --git a/changelogs/unreleased/document-how-to-vue.yml b/changelogs/unreleased/document-how-to-vue.yml
new file mode 100644
index 00000000000..863e41b6413
--- /dev/null
+++ b/changelogs/unreleased/document-how-to-vue.yml
@@ -0,0 +1,4 @@
+---
+title: Adds documentation for how to use Vue.js
+merge_request: 8866
+author:
diff --git a/changelogs/unreleased/dont-delete-assigned-issuables.yml b/changelogs/unreleased/dont-delete-assigned-issuables.yml
new file mode 100644
index 00000000000..fb589a053c0
--- /dev/null
+++ b/changelogs/unreleased/dont-delete-assigned-issuables.yml
@@ -0,0 +1,4 @@
+---
+title: Don't delete assigned MRs/issues when user is deleted
+merge_request:
+author:
diff --git a/changelogs/unreleased/dz-allow-nested-group-routing.yml b/changelogs/unreleased/dz-allow-nested-group-routing.yml
deleted file mode 100644
index 9d8e6e17914..00000000000
--- a/changelogs/unreleased/dz-allow-nested-group-routing.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add nested groups support to the routing
-merge_request: 7459
-author:
diff --git a/changelogs/unreleased/dz-fix-route-rename.yml b/changelogs/unreleased/dz-fix-route-rename.yml
deleted file mode 100644
index a649fb169a5..00000000000
--- a/changelogs/unreleased/dz-fix-route-rename.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix Route#rename_children behavior
-merge_request:
-author:
diff --git a/changelogs/unreleased/dz-nested-groups-improvements-2.yml b/changelogs/unreleased/dz-nested-groups-improvements-2.yml
new file mode 100644
index 00000000000..8e4eb7f1fff
--- /dev/null
+++ b/changelogs/unreleased/dz-nested-groups-improvements-2.yml
@@ -0,0 +1,4 @@
+---
+title: Add read-only full_path and full_name attributes to Group API
+merge_request: 8827
+author:
diff --git a/changelogs/unreleased/dz-nested-groups.yml b/changelogs/unreleased/dz-nested-groups.yml
deleted file mode 100644
index c227c5a8ea5..00000000000
--- a/changelogs/unreleased/dz-nested-groups.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add nested groups support on data level
-merge_request:
-author:
diff --git a/changelogs/unreleased/emoji-btn-disabled.yml b/changelogs/unreleased/emoji-btn-disabled.yml
deleted file mode 100644
index a18b553d513..00000000000
--- a/changelogs/unreleased/emoji-btn-disabled.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Disabled emoji buttons when user is not logged in
-merge_request:
-author:
diff --git a/changelogs/unreleased/empty-selection-reply-shortcut.yml b/changelogs/unreleased/empty-selection-reply-shortcut.yml
new file mode 100644
index 00000000000..5a42c98a800
--- /dev/null
+++ b/changelogs/unreleased/empty-selection-reply-shortcut.yml
@@ -0,0 +1,4 @@
+---
+title: Change the reply shortcut to focus the field even without a selection.
+merge_request: 8873
+author: Brian Hall
diff --git a/changelogs/unreleased/enable-asciidoctor-admonition-icons.yml b/changelogs/unreleased/enable-asciidoctor-admonition-icons.yml
deleted file mode 100644
index 9c52e53c3b4..00000000000
--- a/changelogs/unreleased/enable-asciidoctor-admonition-icons.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Enable AsciiDoctor admonition icons
-merge_request: 7812
-author: Horacio Sanson
diff --git a/changelogs/unreleased/expose-deployment-variables.yml b/changelogs/unreleased/expose-deployment-variables.yml
deleted file mode 100644
index 7663d5b6ae5..00000000000
--- a/changelogs/unreleased/expose-deployment-variables.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Pass variables from deployment project services to CI runner
-merge_request: 8107
-author:
diff --git a/changelogs/unreleased/fe-commit-mr-pipelines.yml b/changelogs/unreleased/fe-commit-mr-pipelines.yml
new file mode 100644
index 00000000000..b5cc6bbf8b6
--- /dev/null
+++ b/changelogs/unreleased/fe-commit-mr-pipelines.yml
@@ -0,0 +1,4 @@
+---
+title: Use vue.js Pipelines table in commit and merge request view
+merge_request: 8844
+author:
diff --git a/changelogs/unreleased/feature-admin-user-groups-link.yml b/changelogs/unreleased/feature-admin-user-groups-link.yml
deleted file mode 100644
index b89c08f82d7..00000000000
--- a/changelogs/unreleased/feature-admin-user-groups-link.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: The admin user projects view now has a clickable group link
-merge_request: 7620
-author: James Gregory
diff --git a/changelogs/unreleased/feature-success-warning-icons-in-stages-builds.yml b/changelogs/unreleased/feature-success-warning-icons-in-stages-builds.yml
new file mode 100644
index 00000000000..5fba0332881
--- /dev/null
+++ b/changelogs/unreleased/feature-success-warning-icons-in-stages-builds.yml
@@ -0,0 +1,4 @@
+---
+title: Use warning icon in mini-graph if stage passed conditionally
+merge_request: 8503
+author:
diff --git a/changelogs/unreleased/features-api-snippets.yml b/changelogs/unreleased/features-api-snippets.yml
deleted file mode 100644
index 80c7bb75359..00000000000
--- a/changelogs/unreleased/features-api-snippets.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: 'API: Endpoint to expose personal snippets as /snippets'
-merge_request: 6373
-author: Bernard Guyzmo Pratz
diff --git a/changelogs/unreleased/file-template-dropwdown-proper-position.yml b/changelogs/unreleased/file-template-dropwdown-proper-position.yml
deleted file mode 100644
index cf2a622b7e6..00000000000
--- a/changelogs/unreleased/file-template-dropwdown-proper-position.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fixed file template dropdown for the "New File" editor for smaller/zoomed screens
-merge_request:
-author:
diff --git a/changelogs/unreleased/fix-27479.yml b/changelogs/unreleased/fix-27479.yml
new file mode 100644
index 00000000000..cc72a830695
--- /dev/null
+++ b/changelogs/unreleased/fix-27479.yml
@@ -0,0 +1,4 @@
+---
+title: Remove new branch button for confidential issues
+merge_request:
+author:
diff --git a/changelogs/unreleased/fix-anchor-scrolling.yml b/changelogs/unreleased/fix-anchor-scrolling.yml
new file mode 100644
index 00000000000..43b3b9bf96e
--- /dev/null
+++ b/changelogs/unreleased/fix-anchor-scrolling.yml
@@ -0,0 +1,4 @@
+---
+title: Fix broken anchor links when special characters are used
+merge_request: 8961
+author: Andrey Krivko
diff --git a/changelogs/unreleased/fix-api-mr-permissions.yml b/changelogs/unreleased/fix-api-mr-permissions.yml
new file mode 100644
index 00000000000..33b677b1f29
--- /dev/null
+++ b/changelogs/unreleased/fix-api-mr-permissions.yml
@@ -0,0 +1,4 @@
+---
+title: Don't allow project guests to subscribe to merge requests through the API
+merge_request:
+author: Robert Schilling
diff --git a/changelogs/unreleased/fix-blame-500.yml b/changelogs/unreleased/fix-blame-500.yml
deleted file mode 100644
index 379d81aaa44..00000000000
--- a/changelogs/unreleased/fix-blame-500.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix blame 500 error on invalid path.
-merge_request: 25761
-author: Jeff Stubler
diff --git a/changelogs/unreleased/fix-cancelling-pipelines.yml b/changelogs/unreleased/fix-cancelling-pipelines.yml
deleted file mode 100644
index c21e663093a..00000000000
--- a/changelogs/unreleased/fix-cancelling-pipelines.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix cancelling created or external pipelines
-merge_request: 7508
-author:
diff --git a/changelogs/unreleased/fix-ci-build-policy.yml b/changelogs/unreleased/fix-ci-build-policy.yml
new file mode 100644
index 00000000000..26003713ed4
--- /dev/null
+++ b/changelogs/unreleased/fix-ci-build-policy.yml
@@ -0,0 +1,4 @@
+---
+title: Improve build policy and access abilities
+merge_request: 8711
+author:
diff --git a/changelogs/unreleased/fix-create-pipeline-with-builds-in-transaction.yml b/changelogs/unreleased/fix-create-pipeline-with-builds-in-transaction.yml
deleted file mode 100644
index e37841e80c3..00000000000
--- a/changelogs/unreleased/fix-create-pipeline-with-builds-in-transaction.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Create builds in transaction to avoid empty pipelines
-merge_request: 7742
-author:
diff --git a/changelogs/unreleased/fix-deleting-project-again.yml b/changelogs/unreleased/fix-deleting-project-again.yml
new file mode 100644
index 00000000000..e13215f22a7
--- /dev/null
+++ b/changelogs/unreleased/fix-deleting-project-again.yml
@@ -0,0 +1,4 @@
+---
+title: Fix deleting projects with pipelines and builds
+merge_request: 8960
+author:
diff --git a/changelogs/unreleased/fix-depr-warn.yml b/changelogs/unreleased/fix-depr-warn.yml
new file mode 100644
index 00000000000..61817027720
--- /dev/null
+++ b/changelogs/unreleased/fix-depr-warn.yml
@@ -0,0 +1,4 @@
+---
+title: resolve deprecation warnings
+merge_request: 8855
+author: Adam Pahlevi
diff --git a/changelogs/unreleased/fix-drop-project-authorized-for-user.yml b/changelogs/unreleased/fix-drop-project-authorized-for-user.yml
deleted file mode 100644
index 0d11969575a..00000000000
--- a/changelogs/unreleased/fix-drop-project-authorized-for-user.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Use authorized projects in ProjectTeam
-merge_request:
-author:
diff --git a/changelogs/unreleased/fix-gb-backwards-compatibility-coverage-ci-yml.yml b/changelogs/unreleased/fix-gb-backwards-compatibility-coverage-ci-yml.yml
new file mode 100644
index 00000000000..df7e3776700
--- /dev/null
+++ b/changelogs/unreleased/fix-gb-backwards-compatibility-coverage-ci-yml.yml
@@ -0,0 +1,4 @@
+---
+title: Preserve backward compatibility CI/CD and disallow setting `coverage` regexp in global context
+merge_request: 8981
+author:
diff --git a/changelogs/unreleased/fix-guest-access-posting-to-notes.yml b/changelogs/unreleased/fix-guest-access-posting-to-notes.yml
new file mode 100644
index 00000000000..81377c0c6f0
--- /dev/null
+++ b/changelogs/unreleased/fix-guest-access-posting-to-notes.yml
@@ -0,0 +1,4 @@
+---
+title: Prevent users from creating notes on resources they can't access
+merge_request:
+author:
diff --git a/changelogs/unreleased/fix-import-encrypt-atts.yml b/changelogs/unreleased/fix-import-encrypt-atts.yml
new file mode 100644
index 00000000000..e34d895570b
--- /dev/null
+++ b/changelogs/unreleased/fix-import-encrypt-atts.yml
@@ -0,0 +1,4 @@
+---
+title: Ignore encrypted attributes in Import/Export
+merge_request:
+author:
diff --git a/changelogs/unreleased/fix-import-export-build-token.yml b/changelogs/unreleased/fix-import-export-build-token.yml
deleted file mode 100644
index 622487e6829..00000000000
--- a/changelogs/unreleased/fix-import-export-build-token.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix Import/Export duplicated builds error
-merge_request:
-author:
diff --git a/changelogs/unreleased/fix-import-export-ee-services.yml b/changelogs/unreleased/fix-import-export-ee-services.yml
deleted file mode 100644
index c0aacbc96f8..00000000000
--- a/changelogs/unreleased/fix-import-export-ee-services.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix missing service error importing from EE to CE
-merge_request: 8144
-author:
diff --git a/changelogs/unreleased/fix-import-export-mr-error.yml b/changelogs/unreleased/fix-import-export-mr-error.yml
deleted file mode 100644
index e1137bca131..00000000000
--- a/changelogs/unreleased/fix-import-export-mr-error.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix Import/Export merge requests error while importing
-merge_request:
-author:
diff --git a/changelogs/unreleased/fix-import-group-members.yml b/changelogs/unreleased/fix-import-group-members.yml
new file mode 100644
index 00000000000..fe580af31b3
--- /dev/null
+++ b/changelogs/unreleased/fix-import-group-members.yml
@@ -0,0 +1,4 @@
+---
+title: Add ability to export project inherited group members to Import/Export
+merge_request: 8923
+author:
diff --git a/changelogs/unreleased/fix-references-header-parsing.yml b/changelogs/unreleased/fix-references-header-parsing.yml
new file mode 100644
index 00000000000..b927279cdf4
--- /dev/null
+++ b/changelogs/unreleased/fix-references-header-parsing.yml
@@ -0,0 +1,5 @@
+---
+title: Fix reply by email without sub-addressing for some clients from
+ Microsoft and Apple
+merge_request: 8620
+author:
diff --git a/changelogs/unreleased/fix-rename-mwbs-to-merge-when-pipeline-succeeds.yml b/changelogs/unreleased/fix-rename-mwbs-to-merge-when-pipeline-succeeds.yml
deleted file mode 100644
index f8acc6ef8ad..00000000000
--- a/changelogs/unreleased/fix-rename-mwbs-to-merge-when-pipeline-succeeds.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Rename Merge When Build Succeeds to Merge When Pipeline Succeeds
-merge_request: 7135
-author:
diff --git a/changelogs/unreleased/fix-scroll-test.yml b/changelogs/unreleased/fix-scroll-test.yml
new file mode 100644
index 00000000000..e98ac755b88
--- /dev/null
+++ b/changelogs/unreleased/fix-scroll-test.yml
@@ -0,0 +1,4 @@
+---
+title: Change rspec test to guarantee window is resized before visiting page
+merge_request:
+author:
diff --git a/changelogs/unreleased/fix-slack-pipeline-message-by-api.yml b/changelogs/unreleased/fix-slack-pipeline-message-by-api.yml
deleted file mode 100644
index aa5ad5cd8d6..00000000000
--- a/changelogs/unreleased/fix-slack-pipeline-message-by-api.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix Slack pipeline message from pipelines made by API
-merge_request: 8059
-author:
diff --git a/changelogs/unreleased/fix-users-deleting-public-deployment-keys.yml b/changelogs/unreleased/fix-users-deleting-public-deployment-keys.yml
new file mode 100644
index 00000000000..c9edd1de86c
--- /dev/null
+++ b/changelogs/unreleased/fix-users-deleting-public-deployment-keys.yml
@@ -0,0 +1,4 @@
+---
+title: Prevent users from deleting system deploy keys via the project deploy key API
+merge_request:
+author:
diff --git a/changelogs/unreleased/fix-yaml-variables.yml b/changelogs/unreleased/fix-yaml-variables.yml
deleted file mode 100644
index 3abff1e3b08..00000000000
--- a/changelogs/unreleased/fix-yaml-variables.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Convert CI YAML variables keys into strings
-merge_request: 8088
-author:
diff --git a/changelogs/unreleased/fix_broken_diff_discussions.yml b/changelogs/unreleased/fix_broken_diff_discussions.yml
new file mode 100644
index 00000000000..4551212759f
--- /dev/null
+++ b/changelogs/unreleased/fix_broken_diff_discussions.yml
@@ -0,0 +1,4 @@
+---
+title: Make MR-review-discussions more reliable
+merge_request:
+author:
diff --git a/changelogs/unreleased/fwn-to-find-by-full-path.yml b/changelogs/unreleased/fwn-to-find-by-full-path.yml
new file mode 100644
index 00000000000..1427e4e7624
--- /dev/null
+++ b/changelogs/unreleased/fwn-to-find-by-full-path.yml
@@ -0,0 +1,4 @@
+---
+title: replace `find_with_namespace` with `find_by_full_path`
+merge_request: 8949
+author: Adam Pahlevi
diff --git a/changelogs/unreleased/gem-update-grape.yml b/changelogs/unreleased/gem-update-grape.yml
deleted file mode 100644
index 46b6702d9fd..00000000000
--- a/changelogs/unreleased/gem-update-grape.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: 'Gem update: Update grape to 0.18.0'
-merge_request:
-author: Robert Schilling
diff --git a/changelogs/unreleased/get-rid-of-water-from-notification_service_spec-to-make-it-DRY.yml b/changelogs/unreleased/get-rid-of-water-from-notification_service_spec-to-make-it-DRY.yml
new file mode 100644
index 00000000000..f60417d185e
--- /dev/null
+++ b/changelogs/unreleased/get-rid-of-water-from-notification_service_spec-to-make-it-DRY.yml
@@ -0,0 +1,4 @@
+---
+title: Make notification_service spec DRYer by making test reusable
+merge_request:
+author: YarNayar
diff --git a/changelogs/unreleased/git_to_html_redirection.yml b/changelogs/unreleased/git_to_html_redirection.yml
new file mode 100644
index 00000000000..b2959c02c07
--- /dev/null
+++ b/changelogs/unreleased/git_to_html_redirection.yml
@@ -0,0 +1,4 @@
+---
+title: Redirect http://someproject.git to http://someproject
+merge_request:
+author: blackst0ne
diff --git a/changelogs/unreleased/gitlab-workhorse-multipart.yml b/changelogs/unreleased/gitlab-workhorse-multipart.yml
deleted file mode 100644
index 23c2139cf93..00000000000
--- a/changelogs/unreleased/gitlab-workhorse-multipart.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Replace Rack::Multipart with GitLab-Workhorse based solution
-merge_request: 5867
-author:
diff --git a/changelogs/unreleased/glm-shorthand-reference.yml b/changelogs/unreleased/glm-shorthand-reference.yml
deleted file mode 100644
index 6d60f23c798..00000000000
--- a/changelogs/unreleased/glm-shorthand-reference.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add shorthand support to gitlab markdown references
-merge_request: 7255
-author: Oswaldo Ferreira
diff --git a/changelogs/unreleased/go-go-gadget-webpack.yml b/changelogs/unreleased/go-go-gadget-webpack.yml
new file mode 100644
index 00000000000..7f372ccb428
--- /dev/null
+++ b/changelogs/unreleased/go-go-gadget-webpack.yml
@@ -0,0 +1,4 @@
+---
+title: use webpack to bundle frontend assets and use karma for frontend testing
+merge_request: 7288
+author:
diff --git a/changelogs/unreleased/group-label-sidebar-link.yml b/changelogs/unreleased/group-label-sidebar-link.yml
new file mode 100644
index 00000000000..c11c2d4ede1
--- /dev/null
+++ b/changelogs/unreleased/group-label-sidebar-link.yml
@@ -0,0 +1,4 @@
+---
+title: Fixed group label links in issue/merge request sidebar
+merge_request:
+author:
diff --git a/changelogs/unreleased/hardcode-title-system-note.yml b/changelogs/unreleased/hardcode-title-system-note.yml
new file mode 100644
index 00000000000..1b0a63efa51
--- /dev/null
+++ b/changelogs/unreleased/hardcode-title-system-note.yml
@@ -0,0 +1,4 @@
+---
+title: Ensure autogenerated title does not cause failing spec
+merge_request: 8963
+author: brian m. carlson
diff --git a/changelogs/unreleased/hoopes-gitlab-ce-21027-add-diff-hunks-to-notification-emails.yml b/changelogs/unreleased/hoopes-gitlab-ce-21027-add-diff-hunks-to-notification-emails.yml
deleted file mode 100644
index 73d8a52e001..00000000000
--- a/changelogs/unreleased/hoopes-gitlab-ce-21027-add-diff-hunks-to-notification-emails.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add git diff context to notifications of new notes on merge requests
-merge_request:
-author: Heidi Hoopes
diff --git a/changelogs/unreleased/html-safe-diff-line-content.yml b/changelogs/unreleased/html-safe-diff-line-content.yml
deleted file mode 100644
index 8f8bbc51963..00000000000
--- a/changelogs/unreleased/html-safe-diff-line-content.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Don't accidentally mark unsafe diff lines as HTML safe
-merge_request:
-author:
diff --git a/changelogs/unreleased/improve-ci-example-php-doc.yml b/changelogs/unreleased/improve-ci-example-php-doc.yml
new file mode 100644
index 00000000000..39a85e3d261
--- /dev/null
+++ b/changelogs/unreleased/improve-ci-example-php-doc.yml
@@ -0,0 +1,4 @@
+---
+title: Changed composer installer script in the CI PHP example doc
+merge_request: 4342
+author: Jeffrey Cafferata
diff --git a/changelogs/unreleased/improve-handleLocationHash-tests.yml b/changelogs/unreleased/improve-handleLocationHash-tests.yml
new file mode 100644
index 00000000000..8ae3dfe079c
--- /dev/null
+++ b/changelogs/unreleased/improve-handleLocationHash-tests.yml
@@ -0,0 +1,4 @@
+---
+title: Improve gl.utils.handleLocationHash tests
+merge_request:
+author:
diff --git a/changelogs/unreleased/improve-invite-accept-page.yml b/changelogs/unreleased/improve-invite-accept-page.yml
deleted file mode 100644
index 8a09a5ae42f..00000000000
--- a/changelogs/unreleased/improve-invite-accept-page.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add note to the invite page when the logged in user email is not the same as the invitation
-merge_request:
-author:
diff --git a/changelogs/unreleased/issuable-sidebar-bug.yml b/changelogs/unreleased/issuable-sidebar-bug.yml
new file mode 100644
index 00000000000..4086292eb89
--- /dev/null
+++ b/changelogs/unreleased/issuable-sidebar-bug.yml
@@ -0,0 +1,4 @@
+---
+title: Fixed Issuable sidebar not closing on smaller/mobile sized screens
+merge_request:
+author:
diff --git a/changelogs/unreleased/issuable_filters_present-refactor.yml b/changelogs/unreleased/issuable_filters_present-refactor.yml
deleted file mode 100644
index c131f9cb68e..00000000000
--- a/changelogs/unreleased/issuable_filters_present-refactor.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Refactor issuable_filters_present to reduce duplications
-merge_request: 7776
-author: Semyon Pupkov
diff --git a/changelogs/unreleased/issue-20428.yml b/changelogs/unreleased/issue-20428.yml
new file mode 100644
index 00000000000..60da1c14702
--- /dev/null
+++ b/changelogs/unreleased/issue-20428.yml
@@ -0,0 +1,4 @@
+---
+title: Add ability to define a coverage regex in the .gitlab-ci.yml
+merge_request: 7447
+author: Leandro Camargo
diff --git a/changelogs/unreleased/issue-24534.yml b/changelogs/unreleased/issue-24534.yml
deleted file mode 100644
index 14d6730d3f6..00000000000
--- a/changelogs/unreleased/issue-24534.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Remove unnecessary sentences for status codes in the API documentation
-merge_request:
-author: Luis Alonso Chavez Armendariz
diff --git a/changelogs/unreleased/issue-boards-scrollable-element.yml b/changelogs/unreleased/issue-boards-scrollable-element.yml
deleted file mode 100644
index 90edc30e791..00000000000
--- a/changelogs/unreleased/issue-boards-scrollable-element.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fixed issue boards scrolling with a lot of lists & issues
-merge_request:
-author:
diff --git a/changelogs/unreleased/issue-events-filter.yml b/changelogs/unreleased/issue-events-filter.yml
deleted file mode 100644
index a3b08bde6e7..00000000000
--- a/changelogs/unreleased/issue-events-filter.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add issue events filter and make all really show all events
-merge_request: 7673
-author: Oxan van Leeuwen
diff --git a/changelogs/unreleased/issue-sidebar-empty-assignee.yml b/changelogs/unreleased/issue-sidebar-empty-assignee.yml
new file mode 100644
index 00000000000..263af75b9e9
--- /dev/null
+++ b/changelogs/unreleased/issue-sidebar-empty-assignee.yml
@@ -0,0 +1,4 @@
+---
+title: Resets assignee dropdown when sidebar is open
+merge_request:
+author:
diff --git a/changelogs/unreleased/issue_13270.yml b/changelogs/unreleased/issue_13270.yml
deleted file mode 100644
index 9c15c436876..00000000000
--- a/changelogs/unreleased/issue_13270.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Allow to delete tag release note
-merge_request:
-author:
diff --git a/changelogs/unreleased/issue_19262.yml b/changelogs/unreleased/issue_19262.yml
new file mode 100644
index 00000000000..5dea1493f23
--- /dev/null
+++ b/changelogs/unreleased/issue_19262.yml
@@ -0,0 +1,4 @@
+---
+title: Disallow system notes for closed issuables
+merge_request:
+author:
diff --git a/changelogs/unreleased/issue_22269.yml b/changelogs/unreleased/issue_22269.yml
deleted file mode 100644
index 6b7164aff77..00000000000
--- a/changelogs/unreleased/issue_22269.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Create mattermost service
-merge_request:
-author:
diff --git a/changelogs/unreleased/issue_24363.yml b/changelogs/unreleased/issue_24363.yml
deleted file mode 100644
index 0298890b477..00000000000
--- a/changelogs/unreleased/issue_24363.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix appearance in error pages
-merge_request:
-author: Luis Alonso Chavez Armendariz
diff --git a/changelogs/unreleased/issue_24748.yml b/changelogs/unreleased/issue_24748.yml
deleted file mode 100644
index 4c1df542c53..00000000000
--- a/changelogs/unreleased/issue_24748.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix title case to sentence case
-merge_request:
-author: Luis Alonso Chavez Armendariz
diff --git a/changelogs/unreleased/issue_24958.yml b/changelogs/unreleased/issue_24958.yml
deleted file mode 100644
index dbbbbf9d28d..00000000000
--- a/changelogs/unreleased/issue_24958.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix bad selection on dropdown menu for tags filter
-merge_request:
-author: Luis Alonso Chavez Armendariz
diff --git a/changelogs/unreleased/issue_27211.yml b/changelogs/unreleased/issue_27211.yml
new file mode 100644
index 00000000000..ad48fec5d85
--- /dev/null
+++ b/changelogs/unreleased/issue_27211.yml
@@ -0,0 +1,4 @@
+---
+title: Remove unused js response from refs controller
+merge_request:
+author:
diff --git a/changelogs/unreleased/issues-1608-text.yml b/changelogs/unreleased/issues-1608-text.yml
deleted file mode 100644
index bef427a1e1e..00000000000
--- a/changelogs/unreleased/issues-1608-text.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: change text around timestamps to make it clear which timestamp is displayed
-merge_request: 7860
-author: BM5k
diff --git a/changelogs/unreleased/jej-22869.yml b/changelogs/unreleased/jej-22869.yml
deleted file mode 100644
index 9d2edcfee42..00000000000
--- a/changelogs/unreleased/jej-22869.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix information disclosure in `Projects::BlobController#update`
-merge_request:
-author:
diff --git a/changelogs/unreleased/jej-23867-use-mr-finder-instead-of-access-check.yml b/changelogs/unreleased/jej-23867-use-mr-finder-instead-of-access-check.yml
deleted file mode 100644
index 5a4a44b9562..00000000000
--- a/changelogs/unreleased/jej-23867-use-mr-finder-instead-of-access-check.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Replace MR access checks with use of MergeRequestsFinder
-merge_request:
-author:
diff --git a/changelogs/unreleased/jej-24637-move-issue-visible_to_user-to-finder.yml b/changelogs/unreleased/jej-24637-move-issue-visible_to_user-to-finder.yml
deleted file mode 100644
index db1389e2024..00000000000
--- a/changelogs/unreleased/jej-24637-move-issue-visible_to_user-to-finder.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Issue#visible_to_user moved to IssuesFinder to prevent accidental use
-merge_request:
-author:
diff --git a/changelogs/unreleased/jej-fix-missing-access-check-on-issues.yml b/changelogs/unreleased/jej-fix-missing-access-check-on-issues.yml
deleted file mode 100644
index 844fba9a107..00000000000
--- a/changelogs/unreleased/jej-fix-missing-access-check-on-issues.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix missing access checks on issue lookup using IssuableFinder
-merge_request:
-author:
diff --git a/changelogs/unreleased/jej-fix-n-1-queries-milestones-show.yml b/changelogs/unreleased/jej-fix-n-1-queries-milestones-show.yml
deleted file mode 100644
index ad6eba3faf2..00000000000
--- a/changelogs/unreleased/jej-fix-n-1-queries-milestones-show.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix N+1 queries on milestone show pages
-merge_request: 8185
-author:
diff --git a/changelogs/unreleased/jej-memoize-milestoneish-visible-to-user.yml b/changelogs/unreleased/jej-memoize-milestoneish-visible-to-user.yml
deleted file mode 100644
index ab7f39a4178..00000000000
--- a/changelogs/unreleased/jej-memoize-milestoneish-visible-to-user.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Milestoneish SQL performance partially improved and memoized
-merge_request: 8146
-author:
diff --git a/changelogs/unreleased/jej-note-search-uses-finder.yml b/changelogs/unreleased/jej-note-search-uses-finder.yml
deleted file mode 100644
index 1768bdfd487..00000000000
--- a/changelogs/unreleased/jej-note-search-uses-finder.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix missing Note access checks by moving Note#search to updated NoteFinder
-merge_request:
-author:
diff --git a/changelogs/unreleased/jej-pages-picked-from-ee.yml b/changelogs/unreleased/jej-pages-picked-from-ee.yml
new file mode 100644
index 00000000000..ee4a43a93db
--- /dev/null
+++ b/changelogs/unreleased/jej-pages-picked-from-ee.yml
@@ -0,0 +1,4 @@
+---
+title: Added GitLab Pages to CE
+merge_request: 8463
+author:
diff --git a/changelogs/unreleased/jej-use-issuable-finder-instead-of-access-check.yml b/changelogs/unreleased/jej-use-issuable-finder-instead-of-access-check.yml
deleted file mode 100644
index c0b6f50052c..00000000000
--- a/changelogs/unreleased/jej-use-issuable-finder-instead-of-access-check.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Replace issue access checks with use of IssuableFinder
-merge_request:
-author:
diff --git a/changelogs/unreleased/label-promotion.yml b/changelogs/unreleased/label-promotion.yml
new file mode 100644
index 00000000000..2ab997bf420
--- /dev/null
+++ b/changelogs/unreleased/label-promotion.yml
@@ -0,0 +1,4 @@
+---
+title: "Project labels can now be promoted to group labels"
+merge_request: 7242
+author: Olaf Tomalka
diff --git a/changelogs/unreleased/ldap_maint_task.yml b/changelogs/unreleased/ldap_maint_task.yml
deleted file mode 100644
index 8acffba0ce5..00000000000
--- a/changelogs/unreleased/ldap_maint_task.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add LDAP Rake task to rename a provider
-merge_request: 2181
-author:
diff --git a/changelogs/unreleased/leave-project-btn.yml b/changelogs/unreleased/leave-project-btn.yml
deleted file mode 100644
index 2aa553d7b97..00000000000
--- a/changelogs/unreleased/leave-project-btn.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Move all action buttons to project header
-merge_request:
-author:
diff --git a/changelogs/unreleased/lfs-noauth-public-repo.yml b/changelogs/unreleased/lfs-noauth-public-repo.yml
new file mode 100644
index 00000000000..60f62d7691b
--- /dev/null
+++ b/changelogs/unreleased/lfs-noauth-public-repo.yml
@@ -0,0 +1,4 @@
+---
+title: Support unauthenticated LFS object downloads for public projects
+merge_request: 8824
+author: Ben Boeckel
diff --git a/changelogs/unreleased/markdown-plantuml.yml b/changelogs/unreleased/markdown-plantuml.yml
new file mode 100644
index 00000000000..c855f0cbcf7
--- /dev/null
+++ b/changelogs/unreleased/markdown-plantuml.yml
@@ -0,0 +1,4 @@
+---
+title: PlantUML support for Markdown
+merge_request: 8588
+author: Horacio Sanson
diff --git a/changelogs/unreleased/members-dropdowns.yml b/changelogs/unreleased/members-dropdowns.yml
deleted file mode 100644
index b15403d6d62..00000000000
--- a/changelogs/unreleased/members-dropdowns.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Updated members dropdowns
-merge_request:
-author:
diff --git a/changelogs/unreleased/milestone_start_date.yml b/changelogs/unreleased/milestone_start_date.yml
deleted file mode 100644
index 39ac1344329..00000000000
--- a/changelogs/unreleased/milestone_start_date.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add a starting date to milestones
-merge_request:
-author:
diff --git a/changelogs/unreleased/misalinged-discussion-with-no-avatar-26854.yml b/changelogs/unreleased/misalinged-discussion-with-no-avatar-26854.yml
new file mode 100644
index 00000000000..f32b3aea3c8
--- /dev/null
+++ b/changelogs/unreleased/misalinged-discussion-with-no-avatar-26854.yml
@@ -0,0 +1,4 @@
+---
+title: adds avatar for discussion note
+merge_request: 8734
+author:
diff --git a/changelogs/unreleased/move-abuse-report-spinach-test-to-rspec.yml b/changelogs/unreleased/move-abuse-report-spinach-test-to-rspec.yml
deleted file mode 100644
index 9de7477c200..00000000000
--- a/changelogs/unreleased/move-abuse-report-spinach-test-to-rspec.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Move abuse report spinach test to rspec
-merge_request: 7659
-author: Semyon Pupkov
diff --git a/changelogs/unreleased/move-admin-abuse-report-spinach-test-to-rspec.yml b/changelogs/unreleased/move-admin-abuse-report-spinach-test-to-rspec.yml
deleted file mode 100644
index fb70fa2955a..00000000000
--- a/changelogs/unreleased/move-admin-abuse-report-spinach-test-to-rspec.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Move admin abuse report spinach test to rspec
-merge_request: 7691
-author: Semyon Pupkov
diff --git a/changelogs/unreleased/move-admin-active-tab-spinach-tests-to-rspec.yml b/changelogs/unreleased/move-admin-active-tab-spinach-tests-to-rspec.yml
deleted file mode 100644
index 11250643a23..00000000000
--- a/changelogs/unreleased/move-admin-active-tab-spinach-tests-to-rspec.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Move admin active tab spinach tests to rspec
-merge_request: 8037
-author: Semyon Pupkov
diff --git a/changelogs/unreleased/move-admin-hooks-spinach-test-to-rspec.yml b/changelogs/unreleased/move-admin-hooks-spinach-test-to-rspec.yml
deleted file mode 100644
index 7dfd741985a..00000000000
--- a/changelogs/unreleased/move-admin-hooks-spinach-test-to-rspec.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Move admin hooks spinach to rspec
-merge_request: 7942
-author: Semyon Pupkov
diff --git a/changelogs/unreleased/move-admin-logs-spinach-test-to-rspec.yml b/changelogs/unreleased/move-admin-logs-spinach-test-to-rspec.yml
deleted file mode 100644
index 696aa8510a0..00000000000
--- a/changelogs/unreleased/move-admin-logs-spinach-test-to-rspec.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Move admin logs spinach test to rspec
-merge_request: 7945
-author: Semyon Pupkov
diff --git a/changelogs/unreleased/move-admin-spam-spinach-test-to-rspec.yml b/changelogs/unreleased/move-admin-spam-spinach-test-to-rspec.yml
deleted file mode 100644
index a7ec2c20554..00000000000
--- a/changelogs/unreleased/move-admin-spam-spinach-test-to-rspec.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Move admin spam spinach test to Rspec
-merge_request: 7708
-author: Semyon Pupkov
diff --git a/changelogs/unreleased/mr-origin-7855.yml b/changelogs/unreleased/mr-origin-7855.yml
deleted file mode 100644
index 0fdc6153d55..00000000000
--- a/changelogs/unreleased/mr-origin-7855.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Provides a sensible default message when adding a README to a project
-merge_request: 7903
-author:
diff --git a/changelogs/unreleased/mr-tabs-container-offset.yml b/changelogs/unreleased/mr-tabs-container-offset.yml
new file mode 100644
index 00000000000..c5df8abfcf2
--- /dev/null
+++ b/changelogs/unreleased/mr-tabs-container-offset.yml
@@ -0,0 +1,4 @@
+---
+title: Fixed merge requests tab extra margin when fixed to window
+merge_request:
+author:
diff --git a/changelogs/unreleased/newline-eslint-rule.yml b/changelogs/unreleased/newline-eslint-rule.yml
new file mode 100644
index 00000000000..5ce080b6912
--- /dev/null
+++ b/changelogs/unreleased/newline-eslint-rule.yml
@@ -0,0 +1,4 @@
+---
+title: Flag multiple empty lines in eslint, fix offenses.
+merge_request: 8137
+author:
diff --git a/changelogs/unreleased/nick-thomas-gitlab-ce-22864-kubernetes-deploy-with-terminal.yml b/changelogs/unreleased/nick-thomas-gitlab-ce-22864-kubernetes-deploy-with-terminal.yml
deleted file mode 100644
index bb4edf80d94..00000000000
--- a/changelogs/unreleased/nick-thomas-gitlab-ce-22864-kubernetes-deploy-with-terminal.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add online terminal support for Kubernetes
-merge_request: 7690
-author:
diff --git a/changelogs/unreleased/no-sidebar-on-action-btn-click.yml b/changelogs/unreleased/no-sidebar-on-action-btn-click.yml
new file mode 100644
index 00000000000..09e0b3a12d8
--- /dev/null
+++ b/changelogs/unreleased/no-sidebar-on-action-btn-click.yml
@@ -0,0 +1,4 @@
+---
+title: dismiss sidebar on repo buttons click
+merge_request: 8798
+author: Adam Pahlevi
diff --git a/changelogs/unreleased/no_project_notes.yml b/changelogs/unreleased/no_project_notes.yml
new file mode 100644
index 00000000000..6106c027360
--- /dev/null
+++ b/changelogs/unreleased/no_project_notes.yml
@@ -0,0 +1,4 @@
+---
+title: Support notes when a project is not specified (personal snippet notes)
+merge_request: 8468
+author:
diff --git a/changelogs/unreleased/nuke-ugly-spaces-in-changelog-generator.yml b/changelogs/unreleased/nuke-ugly-spaces-in-changelog-generator.yml
deleted file mode 100644
index fd173031107..00000000000
--- a/changelogs/unreleased/nuke-ugly-spaces-in-changelog-generator.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Remove trailing whitespace when generating changelog entry
-merge_request: 7948
-author:
diff --git a/changelogs/unreleased/pass in current_user in MergeRequest and MergeRequestsHelper.yml b/changelogs/unreleased/pass in current_user in MergeRequest and MergeRequestsHelper.yml
new file mode 100644
index 00000000000..0751047c3c0
--- /dev/null
+++ b/changelogs/unreleased/pass in current_user in MergeRequest and MergeRequestsHelper.yml
@@ -0,0 +1,4 @@
+---
+title: pass in current_user in MergeRequest and MergeRequestsHelper
+merge_request: 8624
+author: Dongqing Hu
diff --git a/changelogs/unreleased/pipeline-build-hitbox.yml b/changelogs/unreleased/pipeline-build-hitbox.yml
deleted file mode 100644
index 051b538a9a3..00000000000
--- a/changelogs/unreleased/pipeline-build-hitbox.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Make CI badge hitboxes match parent
-merge_request:
-author:
diff --git a/changelogs/unreleased/pms-lowercase-system-notes.yml b/changelogs/unreleased/pms-lowercase-system-notes.yml
new file mode 100644
index 00000000000..c2fa1a7fad0
--- /dev/null
+++ b/changelogs/unreleased/pms-lowercase-system-notes.yml
@@ -0,0 +1,4 @@
+---
+title: Make all system notes lowercase
+merge_request: 8807
+author:
diff --git a/changelogs/unreleased/post_receive-any-email.yml b/changelogs/unreleased/post_receive-any-email.yml
deleted file mode 100644
index 3710b1b4b46..00000000000
--- a/changelogs/unreleased/post_receive-any-email.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: "post_receive: accept any user email from last commit"
-merge_request: 7225
-author: Elan Ruusamäe
diff --git a/changelogs/unreleased/process-commit-worker-migration-encoding.yml b/changelogs/unreleased/process-commit-worker-migration-encoding.yml
deleted file mode 100644
index 26aabd9b647..00000000000
--- a/changelogs/unreleased/process-commit-worker-migration-encoding.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Encode input when migrating ProcessCommitWorker jobs to prevent migration errors
-merge_request:
-author:
diff --git a/changelogs/unreleased/public-tags-api.yml b/changelogs/unreleased/public-tags-api.yml
deleted file mode 100644
index f5e844470b2..00000000000
--- a/changelogs/unreleased/public-tags-api.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Allow public access to some Tag API endpoints
-merge_request:
-author:
diff --git a/changelogs/unreleased/readme-link-fix.yml b/changelogs/unreleased/readme-link-fix.yml
deleted file mode 100644
index 211d3b80c3a..00000000000
--- a/changelogs/unreleased/readme-link-fix.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix broken README.md UX guide link.
-merge_request:
-author:
diff --git a/changelogs/unreleased/redesign-searchbar-admin-project-26794.yml b/changelogs/unreleased/redesign-searchbar-admin-project-26794.yml
new file mode 100644
index 00000000000..547a7c6755c
--- /dev/null
+++ b/changelogs/unreleased/redesign-searchbar-admin-project-26794.yml
@@ -0,0 +1,4 @@
+---
+title: Redesign searchbar in admin project list
+merge_request: 8776
+author:
diff --git a/changelogs/unreleased/redirect-to-commit-when-only-commit-found.yml b/changelogs/unreleased/redirect-to-commit-when-only-commit-found.yml
new file mode 100644
index 00000000000..e0f7e11b6d1
--- /dev/null
+++ b/changelogs/unreleased/redirect-to-commit-when-only-commit-found.yml
@@ -0,0 +1,5 @@
+---
+title: 'Search feature: redirects to commit page if query is commit sha and only commit
+ found'
+merge_request: 8028
+author: YarNayar
diff --git a/changelogs/unreleased/refactor-create-service-spec.yml b/changelogs/unreleased/refactor-create-service-spec.yml
deleted file mode 100644
index 148a0fee02c..00000000000
--- a/changelogs/unreleased/refactor-create-service-spec.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Refactor create service spec
-merge_request: 7609
-author: Semyon Pupkov
diff --git a/changelogs/unreleased/refresh-permissions-when-moving-projects.yml b/changelogs/unreleased/refresh-permissions-when-moving-projects.yml
new file mode 100644
index 00000000000..a94bcdaa9a3
--- /dev/null
+++ b/changelogs/unreleased/refresh-permissions-when-moving-projects.yml
@@ -0,0 +1,4 @@
+---
+title: Refresh authorizations when transferring projects
+merge_request:
+author:
diff --git a/changelogs/unreleased/relative-url-assets.yml b/changelogs/unreleased/relative-url-assets.yml
new file mode 100644
index 00000000000..0877664aca4
--- /dev/null
+++ b/changelogs/unreleased/relative-url-assets.yml
@@ -0,0 +1,4 @@
+---
+title: allow relative url change without recompiling frontend assets
+merge_request: 8831
+author:
diff --git a/changelogs/unreleased/removal_of_unused_parameter.yml b/changelogs/unreleased/removal_of_unused_parameter.yml
new file mode 100644
index 00000000000..26bffafd9d9
--- /dev/null
+++ b/changelogs/unreleased/removal_of_unused_parameter.yml
@@ -0,0 +1,4 @@
+---
+title: 'removed unused parameter ''status_only: true'''
+merge_request:
+author:
diff --git a/changelogs/unreleased/remove-backup-strategies.yml b/changelogs/unreleased/remove-backup-strategies.yml
deleted file mode 100644
index 9f034613c2c..00000000000
--- a/changelogs/unreleased/remove-backup-strategies.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Stop supporting Google and Azure as backup strategies
-merge_request:
-author:
diff --git a/changelogs/unreleased/remove-deploy-key-endpoint.yml b/changelogs/unreleased/remove-deploy-key-endpoint.yml
new file mode 100644
index 00000000000..3ff69adb4d3
--- /dev/null
+++ b/changelogs/unreleased/remove-deploy-key-endpoint.yml
@@ -0,0 +1,4 @@
+---
+title: 'API: Remove /projects/:id/keys/.. endpoints'
+merge_request: 8716
+author: Robert Schilling
diff --git a/changelogs/unreleased/remove-jsx-react-eslint-plugins.yml b/changelogs/unreleased/remove-jsx-react-eslint-plugins.yml
deleted file mode 100644
index 6e02998b3a8..00000000000
--- a/changelogs/unreleased/remove-jsx-react-eslint-plugins.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Changed eslint airbnb config to the base airbnb config and corrected eslintrc
- plugins and envs
-merge_request: 7470
-author: Luke "Jared" Bennett
diff --git a/changelogs/unreleased/remove-require-from-services.yml b/changelogs/unreleased/remove-require-from-services.yml
deleted file mode 100644
index 400512e0314..00000000000
--- a/changelogs/unreleased/remove-require-from-services.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: 'Remove unnecessary require_relative calls from service classes'
-merge_request: '7601'
-author: Semyon Pupkov
diff --git a/changelogs/unreleased/remove-sidekiq-backup-ar-threads.yml b/changelogs/unreleased/remove-sidekiq-backup-ar-threads.yml
new file mode 100644
index 00000000000..f42aa6fae79
--- /dev/null
+++ b/changelogs/unreleased/remove-sidekiq-backup-ar-threads.yml
@@ -0,0 +1,4 @@
+---
+title: Don't use backup Active Record connections for Sidekiq
+merge_request:
+author:
diff --git a/changelogs/unreleased/remove-unnecessary-message-mr-commits-tab.yml b/changelogs/unreleased/remove-unnecessary-message-mr-commits-tab.yml
deleted file mode 100644
index 754af641add..00000000000
--- a/changelogs/unreleased/remove-unnecessary-message-mr-commits-tab.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Remove unnecessary commits order message
-merge_request: 8004
-author:
diff --git a/changelogs/unreleased/remove-unnecessary-self-from-user-model.yml b/changelogs/unreleased/remove-unnecessary-self-from-user-model.yml
deleted file mode 100644
index bef11c63675..00000000000
--- a/changelogs/unreleased/remove-unnecessary-self-from-user-model.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Remove unnecessary self from user model
-merge_request: 7551
-author: Semyon Pupkov
diff --git a/changelogs/unreleased/removing_unnecessary_indexes.yml b/changelogs/unreleased/removing_unnecessary_indexes.yml
deleted file mode 100644
index 01314ab5585..00000000000
--- a/changelogs/unreleased/removing_unnecessary_indexes.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Remove unnecessary database indices
-merge_request:
-author:
diff --git a/changelogs/unreleased/render-svg-in-diffs-and-notes.yml b/changelogs/unreleased/render-svg-in-diffs-and-notes.yml
deleted file mode 100644
index 827b0dbb1d3..00000000000
--- a/changelogs/unreleased/render-svg-in-diffs-and-notes.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Render SVG images in diffs and notes
-merge_request: 7747
-author: andrebsguedes
diff --git a/changelogs/unreleased/right-sidebar-fixture.yml b/changelogs/unreleased/right-sidebar-fixture.yml
deleted file mode 100644
index 46a3e459fef..00000000000
--- a/changelogs/unreleased/right-sidebar-fixture.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Replace static fixture for right_sidebar_spec
-merge_request: 7687
-author: winniehell
diff --git a/changelogs/unreleased/rounded-labels-fixes.yml b/changelogs/unreleased/rounded-labels-fixes.yml
deleted file mode 100644
index e0fbc6e3b5a..00000000000
--- a/changelogs/unreleased/rounded-labels-fixes.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Additional rounded label fixes
-merge_request:
-author:
diff --git a/changelogs/unreleased/route-map.yml b/changelogs/unreleased/route-map.yml
new file mode 100644
index 00000000000..9b6df0c54af
--- /dev/null
+++ b/changelogs/unreleased/route-map.yml
@@ -0,0 +1,4 @@
+---
+title: Add 'View on [env]' link to blobs and individual files in diffs
+merge_request: 8867
+author:
diff --git a/changelogs/unreleased/rs-project-team-helpers.yml b/changelogs/unreleased/rs-project-team-helpers.yml
deleted file mode 100644
index 79abcbce1e3..00000000000
--- a/changelogs/unreleased/rs-project-team-helpers.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add shortcuts for adding users to a project team with a specific role
-merge_request:
-author: Nikolay Ponomarev and Dino M
diff --git a/changelogs/unreleased/seed-runner-token.yml b/changelogs/unreleased/seed-runner-token.yml
deleted file mode 100644
index e8153be043a..00000000000
--- a/changelogs/unreleased/seed-runner-token.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add support for setting the GitLab Runners Registration Token during initial
- database seeding
-merge_request: 6642
-author:
diff --git a/changelogs/unreleased/shortcuts-issuable-fixture.yml b/changelogs/unreleased/shortcuts-issuable-fixture.yml
deleted file mode 100644
index 88945600886..00000000000
--- a/changelogs/unreleased/shortcuts-issuable-fixture.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Replace static fixture for shortcuts_issuable_spec
-merge_request: 7685
-author: winniehell
diff --git a/changelogs/unreleased/show-commit-status-from-latest-pipeline.yml b/changelogs/unreleased/show-commit-status-from-latest-pipeline.yml
deleted file mode 100644
index bbd7a217493..00000000000
--- a/changelogs/unreleased/show-commit-status-from-latest-pipeline.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Show commit status from latest pipeline
-merge_request: 7333
-author:
diff --git a/changelogs/unreleased/simplify-create-new-list-issue-boards.yml b/changelogs/unreleased/simplify-create-new-list-issue-boards.yml
deleted file mode 100644
index ca11e3b94a7..00000000000
--- a/changelogs/unreleased/simplify-create-new-list-issue-boards.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Simplify copy on "Create a new list" dropdown in Issue Boards
-merge_request: 7605
-author: Victor Rodrigues
diff --git a/changelogs/unreleased/slash-commands-typo.yml b/changelogs/unreleased/slash-commands-typo.yml
new file mode 100644
index 00000000000..e6ffb94bd08
--- /dev/null
+++ b/changelogs/unreleased/slash-commands-typo.yml
@@ -0,0 +1,4 @@
+---
+title: Fixed "substract" typo on /help/user/project/slash_commands
+merge_request: 8976
+author: Jason Aquino
diff --git a/changelogs/unreleased/small-emoji-adjustments.yml b/changelogs/unreleased/small-emoji-adjustments.yml
deleted file mode 100644
index 804bd05b613..00000000000
--- a/changelogs/unreleased/small-emoji-adjustments.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Various small emoji positioning adjustments
-merge_request:
-author:
diff --git a/changelogs/unreleased/small-screen-fullscreen-button.yml b/changelogs/unreleased/small-screen-fullscreen-button.yml
new file mode 100644
index 00000000000..f4c269bc473
--- /dev/null
+++ b/changelogs/unreleased/small-screen-fullscreen-button.yml
@@ -0,0 +1,4 @@
+---
+title: Display fullscreen button on small screens
+merge_request: 5302
+author: winniehell
diff --git a/changelogs/unreleased/tc-only-mr-button-if-allowed.yml b/changelogs/unreleased/tc-only-mr-button-if-allowed.yml
new file mode 100644
index 00000000000..a7f5dcb560c
--- /dev/null
+++ b/changelogs/unreleased/tc-only-mr-button-if-allowed.yml
@@ -0,0 +1,4 @@
+---
+title: Only show Merge Request button when user can create a MR
+merge_request: 8639
+author:
diff --git a/changelogs/unreleased/terminal-max-session-time.yml b/changelogs/unreleased/terminal-max-session-time.yml
new file mode 100644
index 00000000000..db1e66770d1
--- /dev/null
+++ b/changelogs/unreleased/terminal-max-session-time.yml
@@ -0,0 +1,4 @@
+---
+title: Introduce maximum session time for terminal websocket connection
+merge_request: 8413
+author:
diff --git a/changelogs/unreleased/update-api-spec-files.yml b/changelogs/unreleased/update-api-spec-files.yml
deleted file mode 100644
index 349d866cf22..00000000000
--- a/changelogs/unreleased/update-api-spec-files.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Update API spec files to describe the correct class
-merge_request:
-author: Livier
diff --git a/changelogs/unreleased/update-button-font-weight.yml b/changelogs/unreleased/update-button-font-weight.yml
deleted file mode 100644
index ddb3c1c8da4..00000000000
--- a/changelogs/unreleased/update-button-font-weight.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Updates the font weight of button styles because of the change to system fonts
-merge_request:
-author:
diff --git a/changelogs/unreleased/update-git-version-in-doc.yml b/changelogs/unreleased/update-git-version-in-doc.yml
deleted file mode 100644
index cb3260f71cd..00000000000
--- a/changelogs/unreleased/update-git-version-in-doc.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Bump Git version requirement to 2.8.4
-merge_request:
-author:
diff --git a/changelogs/unreleased/upgrade-omniauth.yml b/changelogs/unreleased/upgrade-omniauth.yml
new file mode 100644
index 00000000000..7e0334566dc
--- /dev/null
+++ b/changelogs/unreleased/upgrade-omniauth.yml
@@ -0,0 +1,4 @@
+---
+title: Upgrade omniauth gem to 1.3.2
+merge_request:
+author:
diff --git a/changelogs/unreleased/use-st-commits-where-possible.yml b/changelogs/unreleased/use-st-commits-where-possible.yml
deleted file mode 100644
index e4395461560..00000000000
--- a/changelogs/unreleased/use-st-commits-where-possible.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Replace references to MergeRequestDiff#commits with st_commits when we care
- only about the number of commits
-merge_request: 7668
-author:
diff --git a/changelogs/unreleased/username-exists-root.yml b/changelogs/unreleased/username-exists-root.yml
deleted file mode 100644
index 1ffb3eb435c..00000000000
--- a/changelogs/unreleased/username-exists-root.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Username exists check respects relative root path
-merge_request:
-author:
diff --git a/changelogs/unreleased/validate-state-param-when-filtering-issuables.yml b/changelogs/unreleased/validate-state-param-when-filtering-issuables.yml
deleted file mode 100644
index 3fb025806b0..00000000000
--- a/changelogs/unreleased/validate-state-param-when-filtering-issuables.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Validate state param when filtering issuables
-merge_request:
-author:
diff --git a/changelogs/unreleased/wip-mr-from-commits.yml b/changelogs/unreleased/wip-mr-from-commits.yml
new file mode 100644
index 00000000000..0083798be08
--- /dev/null
+++ b/changelogs/unreleased/wip-mr-from-commits.yml
@@ -0,0 +1,4 @@
+---
+title: Mark MR as WIP when pushing WIP commits
+merge_request: 8124
+author: Jurre Stender @jurre
diff --git a/changelogs/unreleased/zen-mode-fixture.yml b/changelogs/unreleased/zen-mode-fixture.yml
deleted file mode 100644
index bec6f6e6dba..00000000000
--- a/changelogs/unreleased/zen-mode-fixture.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Replace static fixture for zen_mode_spec
-merge_request: 7686
-author: winniehell
diff --git a/changelogs/unreleased/zj-expose-coverage-pipelines.yml b/changelogs/unreleased/zj-expose-coverage-pipelines.yml
deleted file mode 100644
index 34e4926e58a..00000000000
--- a/changelogs/unreleased/zj-expose-coverage-pipelines.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: 'API: expose pipeline coverage'
-merge_request:
-author:
diff --git a/changelogs/unreleased/zj-fix-label-creation-non-members.yml b/changelogs/unreleased/zj-fix-label-creation-non-members.yml
deleted file mode 100644
index ae4824f82fa..00000000000
--- a/changelogs/unreleased/zj-fix-label-creation-non-members.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Non members cannot create labels through the API
-merge_request:
-author:
diff --git a/changelogs/unreleased/zj-format-chat-messages.yml b/changelogs/unreleased/zj-format-chat-messages.yml
new file mode 100644
index 00000000000..2494884f5c9
--- /dev/null
+++ b/changelogs/unreleased/zj-format-chat-messages.yml
@@ -0,0 +1,4 @@
+---
+title: Reformat messages ChatOps
+merge_request: 8528
+author:
diff --git a/changelogs/unreleased/zj-guest-reads-public-builds.yml b/changelogs/unreleased/zj-guest-reads-public-builds.yml
deleted file mode 100644
index 1859addd606..00000000000
--- a/changelogs/unreleased/zj-guest-reads-public-builds.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Guests can read builds when public
-merge_request: 6842
-author:
diff --git a/changelogs/unreleased/zj-issue-new-over-issue-create.yml b/changelogs/unreleased/zj-issue-new-over-issue-create.yml
deleted file mode 100644
index 9dd463e4efa..00000000000
--- a/changelogs/unreleased/zj-issue-new-over-issue-create.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Accept issue new as command to create an issue
-merge_request:
-author:
diff --git a/changelogs/unreleased/zj-issue-search-slash-command.yml b/changelogs/unreleased/zj-issue-search-slash-command.yml
deleted file mode 100644
index de41c39d545..00000000000
--- a/changelogs/unreleased/zj-issue-search-slash-command.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add issue search slash command
-merge_request:
-author:
diff --git a/changelogs/unreleased/zj-requeue-pending-delete.yml b/changelogs/unreleased/zj-requeue-pending-delete.yml
new file mode 100644
index 00000000000..464c5948f8c
--- /dev/null
+++ b/changelogs/unreleased/zj-requeue-pending-delete.yml
@@ -0,0 +1,4 @@
+---
+title: Requeue pending deletion projects
+merge_request:
+author:
diff --git a/changelogs/unreleased/zj-slack-slash-commands.yml b/changelogs/unreleased/zj-slack-slash-commands.yml
deleted file mode 100644
index 9f4c8681ad0..00000000000
--- a/changelogs/unreleased/zj-slack-slash-commands.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Refactor presenters ChatCommands
-merge_request: 7846
-author:
diff --git a/changelogs/unreleased/zj-use-ruby-2-3-3.yml b/changelogs/unreleased/zj-use-ruby-2-3-3.yml
deleted file mode 100644
index 0d1a0fcd79d..00000000000
--- a/changelogs/unreleased/zj-use-ruby-2-3-3.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Bump ruby version to 2.3.3
-merge_request: 7904
-author:
diff --git a/config/application.rb b/config/application.rb
index 057d60ca869..9088d3c432b 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -80,9 +80,18 @@ module Gitlab
# like if you have constraints or database-specific column types
# config.active_record.schema_format = :sql
+ # Configure webpack
+ config.webpack.config_file = "config/webpack.config.js"
+ config.webpack.output_dir = "public/assets/webpack"
+ config.webpack.public_path = "assets/webpack"
+
+ # Webpack dev server configuration is handled in initializers/static_files.rb
+ config.webpack.dev_server.enabled = false
+
# Enable the asset pipeline
config.assets.enabled = true
config.assets.paths << Gemojione.images_path
+ config.assets.paths << "vendor/assets/fonts"
config.assets.precompile << "*.png"
config.assets.precompile << "print.css"
config.assets.precompile << "notify.css"
@@ -90,23 +99,11 @@ module Gitlab
config.assets.precompile << "katex.css"
config.assets.precompile << "katex.js"
config.assets.precompile << "xterm/xterm.css"
- config.assets.precompile << "graphs/graphs_bundle.js"
- config.assets.precompile << "users/users_bundle.js"
- config.assets.precompile << "network/network_bundle.js"
- config.assets.precompile << "profile/profile_bundle.js"
- config.assets.precompile << "protected_branches/protected_branches_bundle.js"
- config.assets.precompile << "diff_notes/diff_notes_bundle.js"
- config.assets.precompile << "boards/boards_bundle.js"
- config.assets.precompile << "cycle_analytics/cycle_analytics_bundle.js"
- config.assets.precompile << "merge_conflicts/merge_conflicts_bundle.js"
- config.assets.precompile << "boards/test_utils/simulate_drag.js"
- config.assets.precompile << "environments/environments_bundle.js"
- config.assets.precompile << "blob_edit/blob_edit_bundle.js"
- config.assets.precompile << "snippet/snippet_bundle.js"
- config.assets.precompile << "terminal/terminal_bundle.js"
- config.assets.precompile << "lib/utils/*.js"
- config.assets.precompile << "lib/*.js"
+ config.assets.precompile << "lib/ace.js"
+ config.assets.precompile << "lib/cropper.js"
+ config.assets.precompile << "lib/raphael.js"
config.assets.precompile << "u2f.js"
+ config.assets.precompile << "vendor/assets/fonts/*"
# Version of your assets, change this if you want to expire all your assets
config.assets.version = '1.0'
diff --git a/config/database.yml.mysql b/config/database.yml.mysql
index d9702870249..a33e40e8eb3 100644
--- a/config/database.yml.mysql
+++ b/config/database.yml.mysql
@@ -3,8 +3,8 @@
#
production:
adapter: mysql2
- encoding: utf8mb4
- collation: utf8mb4_general_ci
+ encoding: utf8
+ collation: utf8_general_ci
reconnect: false
database: gitlabhq_production
pool: 10
@@ -18,8 +18,8 @@ production:
#
development:
adapter: mysql2
- encoding: utf8mb4
- collation: utf8mb4_general_ci
+ encoding: utf8
+ collation: utf8_general_ci
reconnect: false
database: gitlabhq_development
pool: 5
diff --git a/config/dependency_decisions.yml b/config/dependency_decisions.yml
index c11296975b7..aabe859730a 100644
--- a/config/dependency_decisions.yml
+++ b/config/dependency_decisions.yml
@@ -1,9 +1,9 @@
---
-# IGNORED GROUPS AND GEMS
- - :ignore_group
- development
- :who: Connor Shea
- :why: Development gems are not distributed with the final product and are therefore exempt.
+ :why: Development gems are not distributed with the final product and are therefore
+ exempt.
:versions: []
:when: 2016-04-17 21:27:01.054140000 Z
- - :ignore_group
@@ -18,8 +18,6 @@
:why: Bundler is MIT licensed but will sometimes fail in CI.
:versions: []
:when: 2016-05-02 06:42:08.045090000 Z
-
-# LICENSE WHITELIST
- - :whitelist
- MIT
- :who: Connor Shea
@@ -86,9 +84,6 @@
:why: https://opensource.org/licenses/BSD-2-Clause
:versions: []
:when: 2016-07-26 21:24:07.248480000 Z
-
-
-# LICENSE BLACKLIST
- - :blacklist
- GPLv2
- :who: Connor Shea
@@ -107,9 +102,6 @@
:why: The OSL license is a copyleft license
:versions: []
:when: 2016-10-28 11:02:15.540105000 Z
-
-
-# GEM LICENSES
- - :license
- raphael-rails
- MIT
@@ -201,3 +193,112 @@
:why: https://github.com/jmcnevin/rubypants/blob/master/LICENSE.rdoc
:versions: []
:when: 2016-05-02 05:56:50.696858000 Z
+- - :approve
+ - after
+ - :who: Matt Lee
+ :why: https://github.com/Raynos/after/blob/master/LICENCE
+ :versions: []
+ :when: 2017-01-14 20:00:32.473125000 Z
+- - :approve
+ - amdefine
+ - :who: Matt Lee
+ :why: MIT License
+ :versions: []
+ :when: 2017-01-14 20:08:31.810633000 Z
+- - :approve
+ - base64id
+ - :who: Matt Lee
+ :why: https://github.com/faeldt/base64id/blob/master/LICENSE
+ :versions: []
+ :when: 2017-01-14 20:08:33.174760000 Z
+- - :approve
+ - blob
+ - :who: Matt Lee
+ :why: https://github.com/webmodules/blob/blob/master/LICENSE
+ :versions: []
+ :when: 2017-01-14 20:08:34.564048000 Z
+- - :approve
+ - callsite
+ - :who: Matt Lee
+ :why: https://github.com/tj/callsite/blob/master/LICENSE
+ :versions: []
+ :when: 2017-01-14 20:08:35.976025000 Z
+- - :approve
+ - component-bind
+ - :who: Matt Lee
+ :why: https://github.com/component/bind/blob/master/LICENSE
+ :versions: []
+ :when: 2017-01-14 20:08:37.291219000 Z
+- - :approve
+ - component-inherit
+ - :who: Matt Lee
+ :why: https://github.com/component/inherit/blob/master/LICENSE
+ :versions: []
+ :when: 2017-01-14 20:10:41.804804000 Z
+- - :approve
+ - fsevents
+ - :who: Matt Lee
+ :why: https://github.com/strongloop/fsevents/blob/master/LICENSE
+ :versions: []
+ :when: 2017-01-14 20:50:20.037775000 Z
+- - :approve
+ - indexof
+ - :who: Matt Lee
+ :why: https://github.com/component/indexof/blob/master/LICENSE
+ :versions: []
+ :when: 2017-01-14 20:10:43.209900000 Z
+- - :approve
+ - is-integer
+ - :who: Matt Lee
+ :why: https://github.com/parshap/js-is-integer/blob/master/LICENSE
+ :versions: []
+ :when: 2017-01-14 20:10:44.540916000 Z
+- - :approve
+ - jsonify
+ - :who: Matt Lee
+ :why: Public Domain - no formal license on this one. probably okay as its been
+ the same for along time. would prefer to see CC0
+ :versions: []
+ :when: 2017-01-14 20:10:45.857261000 Z
+- - :approve
+ - object-component
+ - :who: Matt Lee
+ :why: https://github.com/component/object/blob/master/LICENSE
+ :versions: []
+ :when: 2017-01-14 20:10:47.190148000 Z
+- - :approve
+ - optimist
+ - :who: Matt Lee
+ :why: https://github.com/substack/node-optimist/blob/master/LICENSE
+ :versions: []
+ :when: 2017-01-14 20:10:48.563077000 Z
+- - :approve
+ - path-is-inside
+ - :who: Matt Lee
+ :why: https://github.com/domenic/path-is-inside/blob/master/LICENSE.txt
+ :versions: []
+ :when: 2017-01-14 20:10:49.910497000 Z
+- - :approve
+ - rc
+ - :who: Matt Lee
+ :why: https://github.com/dominictarr/rc/blob/master/LICENSE.MIT
+ :versions: []
+ :when: 2017-01-14 20:10:51.244695000 Z
+- - :approve
+ - ripemd160
+ - :who: Matt Lee
+ :why: https://github.com/crypto-browserify/ripemd160/blob/master/LICENSE.md
+ :versions: []
+ :when: 2017-01-14 20:10:52.560282000 Z
+- - :approve
+ - select2
+ - :who: Matt Lee
+ :why: https://github.com/select2/select2/blob/master/LICENSE.md
+ :versions: []
+ :when: 2017-01-14 20:10:53.909618000 Z
+- - :approve
+ - tweetnacl
+ - :who: Matt Lee
+ :why: https://github.com/dchest/tweetnacl-js/blob/master/LICENSE
+ :versions: []
+ :when: 2017-01-14 20:10:57.812077000 Z
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index b8b41a0d86c..cc1af77a1de 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -153,6 +153,21 @@ production: &base
# The location where LFS objects are stored (default: shared/lfs-objects).
# storage_path: shared/lfs-objects
+ ## GitLab Pages
+ pages:
+ enabled: false
+ # The location where pages are stored (default: shared/pages).
+ # path: shared/pages
+
+ # The domain under which the pages are served:
+ # http://group.example.com/project
+ # or project path can be a group page: group.example.com
+ host: example.com
+ port: 80 # Set to 443 if you serve the pages with HTTPS
+ https: false # Set to true if you serve the pages with HTTPS
+ # external_http: "1.1.1.1:80" # If defined, enables custom domain support in GitLab Pages
+ # external_https: "1.1.1.1:443" # If defined, enables custom domain and certificate support in GitLab Pages
+
## Mattermost
## For enabling Add to Mattermost button
mattermost:
@@ -368,6 +383,16 @@ production: &base
# login_url: '/cas/login',
# service_validate_url: '/cas/p3/serviceValidate',
# logout_url: '/cas/logout'} }
+ # - { name: 'authentiq',
+ # # for client credentials (client ID and secret), go to https://www.authentiq.com/
+ # app_id: 'YOUR_CLIENT_ID',
+ # app_secret: 'YOUR_CLIENT_SECRET',
+ # args: {
+ # scope: 'aq:name email~rs address aq:push'
+ # # redirect_uri parameter is optional except when 'gitlab.host' in this file is set to 'localhost'
+ # # redirect_uri: 'YOUR_REDIRECT_URI'
+ # }
+ # }
# - { name: 'github',
# app_id: 'YOUR_APP_ID',
# app_secret: 'YOUR_APP_SECRET',
@@ -495,6 +520,16 @@ production: &base
# Git timeout to read a commit, in seconds
timeout: 10
+ ## Webpack settings
+ # If enabled, this will tell rails to serve frontend assets from the webpack-dev-server running
+ # on a given port instead of serving directly from /assets/webpack. This is only indended for use
+ # in development.
+ webpack:
+ # dev_server:
+ # enabled: true
+ # host: localhost
+ # port: 3808
+
#
# 5. Extra customization
# ==========================
@@ -576,4 +611,4 @@ test:
admin_group: ''
staging:
- <<: *base
+ <<: *base \ No newline at end of file
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index ee97b4e42b9..ab59394cb0c 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -6,7 +6,7 @@ class Settings < Settingslogic
class << self
def gitlab_on_standard_port?
- gitlab.port.to_i == (gitlab.https ? 443 : 80)
+ on_standard_port?(gitlab)
end
def host_without_www(url)
@@ -14,7 +14,7 @@ class Settings < Settingslogic
end
def build_gitlab_ci_url
- if gitlab_on_standard_port?
+ if on_standard_port?(gitlab)
custom_port = nil
else
custom_port = ":#{gitlab.port}"
@@ -27,6 +27,10 @@ class Settings < Settingslogic
].join('')
end
+ def build_pages_url
+ base_url(pages).join('')
+ end
+
def build_gitlab_shell_ssh_path_prefix
user_host = "#{gitlab_shell.ssh_user}@#{gitlab_shell.ssh_host}"
@@ -42,11 +46,11 @@ class Settings < Settingslogic
end
def build_base_gitlab_url
- base_gitlab_url.join('')
+ base_url(gitlab).join('')
end
def build_gitlab_url
- (base_gitlab_url + [gitlab.relative_url_root]).join('')
+ (base_url(gitlab) + [gitlab.relative_url_root]).join('')
end
# check that values in `current` (string or integer) is a contant in `modul`.
@@ -74,15 +78,19 @@ class Settings < Settingslogic
private
- def base_gitlab_url
- custom_port = gitlab_on_standard_port? ? nil : ":#{gitlab.port}"
- [ gitlab.protocol,
+ def base_url(config)
+ custom_port = on_standard_port?(config) ? nil : ":#{config.port}"
+ [ config.protocol,
"://",
- gitlab.host,
+ config.host,
custom_port
]
end
+ def on_standard_port?(config)
+ config.port.to_i == (config.https ? 443 : 80)
+ end
+
# Extract the host part of the given +url+.
def host(url)
url = url.downcase
@@ -255,6 +263,20 @@ Settings.registry['host_port'] ||= [Settings.registry['host'], Settings.regi
Settings.registry['path'] = File.expand_path(Settings.registry['path'] || File.join(Settings.shared['path'], 'registry'), Rails.root)
#
+# Pages
+#
+Settings['pages'] ||= Settingslogic.new({})
+Settings.pages['enabled'] = false if Settings.pages['enabled'].nil?
+Settings.pages['path'] = File.expand_path(Settings.pages['path'] || File.join(Settings.shared['path'], "pages"), Rails.root)
+Settings.pages['https'] = false if Settings.pages['https'].nil?
+Settings.pages['host'] ||= "example.com"
+Settings.pages['port'] ||= Settings.pages.https ? 443 : 80
+Settings.pages['protocol'] ||= Settings.pages.https ? "https" : "http"
+Settings.pages['url'] ||= Settings.send(:build_pages_url)
+Settings.pages['external_http'] ||= false if Settings.pages['external_http'].nil?
+Settings.pages['external_https'] ||= false if Settings.pages['external_https'].nil?
+
+#
# Git LFS
#
Settings['lfs'] ||= Settingslogic.new({})
@@ -404,6 +426,21 @@ Settings.rack_attack.git_basic_auth['findtime'] ||= 1.minute
Settings.rack_attack.git_basic_auth['bantime'] ||= 1.hour
#
+# Gitaly
+#
+Settings['gitaly'] ||= Settingslogic.new({})
+Settings.gitaly['socket_path'] ||= ENV['GITALY_SOCKET_PATH']
+
+#
+# Webpack settings
+#
+Settings['webpack'] ||= Settingslogic.new({})
+Settings.webpack['dev_server'] ||= Settingslogic.new({})
+Settings.webpack.dev_server['enabled'] ||= false
+Settings.webpack.dev_server['host'] ||= 'localhost'
+Settings.webpack.dev_server['port'] ||= 3808
+
+#
# Testing settings
#
if Rails.env.test?
@@ -413,10 +450,4 @@ if Rails.env.test?
end
# Force a refresh of application settings at startup
-begin
- ApplicationSetting.expire
- Ci::ApplicationSetting.expire
-rescue
- # Gracefully handle when Redis is not available. For example,
- # omnibus may fail here during assets:precompile.
-end
+ApplicationSetting.expire
diff --git a/config/initializers/5_backend.rb b/config/initializers/5_backend.rb
index ed88c8ee1b8..2bd159ca7f1 100644
--- a/config/initializers/5_backend.rb
+++ b/config/initializers/5_backend.rb
@@ -1,9 +1,3 @@
-# GIT over SSH
-require_dependency Rails.root.join('lib/gitlab/backend/shell')
-
-# GitLab shell adapter
-require_dependency Rails.root.join('lib/gitlab/backend/shell_adapter')
-
required_version = Gitlab::VersionInfo.parse(Gitlab::Shell.version_required)
current_version = Gitlab::VersionInfo.parse(Gitlab::Shell.new.version)
diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb
index 3d1a41a4652..d4197da3fa9 100644
--- a/config/initializers/inflections.rb
+++ b/config/initializers/inflections.rb
@@ -10,5 +10,5 @@
# end
#
ActiveSupport::Inflector.inflections do |inflect|
- inflect.uncountable %w(award_emoji)
+ inflect.uncountable %w(award_emoji project_statistics)
end
diff --git a/config/initializers/metrics.rb b/config/initializers/metrics.rb
index 3b8771543e4..e0702e06cc9 100644
--- a/config/initializers/metrics.rb
+++ b/config/initializers/metrics.rb
@@ -1,3 +1,117 @@
+# Autoload all classes that we want to instrument, and instrument the methods we
+# need. This takes the Gitlab::Metrics::Instrumentation module as an argument so
+# that we can stub it for testing, as it is only called when metrics are
+# enabled.
+#
+# rubocop:disable Metrics/AbcSize
+def instrument_classes(instrumentation)
+ instrumentation.instrument_instance_methods(Gitlab::Shell)
+
+ instrumentation.instrument_methods(Gitlab::Git)
+
+ Gitlab::Git.constants.each do |name|
+ const = Gitlab::Git.const_get(name)
+
+ next unless const.is_a?(Module)
+
+ instrumentation.instrument_methods(const)
+ instrumentation.instrument_instance_methods(const)
+ end
+
+ # Path to search => prefix to strip from constant
+ paths_to_instrument = {
+ ['app', 'finders'] => ['app', 'finders'],
+ ['app', 'mailers', 'emails'] => ['app', 'mailers'],
+ ['app', 'services', '**'] => ['app', 'services'],
+ ['lib', 'gitlab', 'conflicts'] => ['lib'],
+ ['lib', 'gitlab', 'diff'] => ['lib'],
+ ['lib', 'gitlab', 'email', 'message'] => ['lib'],
+ ['lib', 'gitlab', 'checks'] => ['lib']
+ }
+
+ paths_to_instrument.each do |(path, prefix)|
+ prefix = Rails.root.join(*prefix)
+
+ Dir[Rails.root.join(*path + ['*.rb'])].each do |file_path|
+ path = Pathname.new(file_path).relative_path_from(prefix)
+ const = path.to_s.sub('.rb', '').camelize.constantize
+
+ instrumentation.instrument_methods(const)
+ instrumentation.instrument_instance_methods(const)
+ end
+ end
+
+ instrumentation.instrument_methods(Premailer::Adapter::Nokogiri)
+ instrumentation.instrument_instance_methods(Premailer::Adapter::Nokogiri)
+
+ [
+ :Blame, :Branch, :BranchCollection, :Blob, :Commit, :Diff, :Repository,
+ :Tag, :TagCollection, :Tree
+ ].each do |name|
+ const = Rugged.const_get(name)
+
+ instrumentation.instrument_methods(const)
+ instrumentation.instrument_instance_methods(const)
+ end
+
+ # Instruments all Banzai filters and reference parsers
+ {
+ Filter: Rails.root.join('lib', 'banzai', 'filter', '*.rb'),
+ ReferenceParser: Rails.root.join('lib', 'banzai', 'reference_parser', '*.rb')
+ }.each do |const_name, path|
+ Dir[path].each do |file|
+ klass = File.basename(file, File.extname(file)).camelize
+ const = Banzai.const_get(const_name).const_get(klass)
+
+ instrumentation.instrument_methods(const)
+ instrumentation.instrument_instance_methods(const)
+ end
+ end
+
+ instrumentation.instrument_methods(Banzai::Renderer)
+ instrumentation.instrument_methods(Banzai::Querying)
+
+ instrumentation.instrument_instance_methods(Banzai::ObjectRenderer)
+ instrumentation.instrument_instance_methods(Banzai::Redactor)
+ instrumentation.instrument_methods(Banzai::NoteRenderer)
+
+ [Issuable, Mentionable, Participable].each do |klass|
+ instrumentation.instrument_instance_methods(klass)
+ instrumentation.instrument_instance_methods(klass::ClassMethods)
+ end
+
+ instrumentation.instrument_methods(Gitlab::ReferenceExtractor)
+ instrumentation.instrument_instance_methods(Gitlab::ReferenceExtractor)
+
+ # Instrument the classes used for checking if somebody has push access.
+ instrumentation.instrument_instance_methods(Gitlab::GitAccess)
+ instrumentation.instrument_instance_methods(Gitlab::GitAccessWiki)
+
+ instrumentation.instrument_instance_methods(API::Helpers)
+
+ instrumentation.instrument_instance_methods(RepositoryCheck::SingleRepositoryWorker)
+
+ instrumentation.instrument_instance_methods(Rouge::Plugins::Redcarpet)
+ instrumentation.instrument_instance_methods(Rouge::Formatters::HTMLGitlab)
+
+ [:XML, :HTML].each do |namespace|
+ namespace_mod = Nokogiri.const_get(namespace)
+
+ instrumentation.instrument_methods(namespace_mod)
+ instrumentation.instrument_methods(namespace_mod::Document)
+ end
+
+ instrumentation.instrument_methods(Rinku)
+ instrumentation.instrument_instance_methods(Repository)
+
+ instrumentation.instrument_methods(Gitlab::Highlight)
+ instrumentation.instrument_instance_methods(Gitlab::Highlight)
+
+ # This is a Rails scope so we have to instrument it manually.
+ instrumentation.instrument_method(Project, :visible_to_user)
+end
+# rubocop:enable Metrics/AbcSize
+
if Gitlab::Metrics.enabled?
require 'pathname'
require 'influxdb'
@@ -49,110 +163,7 @@ if Gitlab::Metrics.enabled?
end
Gitlab::Metrics::Instrumentation.configure do |config|
- config.instrument_instance_methods(Gitlab::Shell)
-
- config.instrument_methods(Gitlab::Git)
-
- Gitlab::Git.constants.each do |name|
- const = Gitlab::Git.const_get(name)
-
- next unless const.is_a?(Module)
-
- config.instrument_methods(const)
- config.instrument_instance_methods(const)
- end
-
- # Path to search => prefix to strip from constant
- paths_to_instrument = {
- ['app', 'finders'] => ['app', 'finders'],
- ['app', 'mailers', 'emails'] => ['app', 'mailers'],
- ['app', 'services', '**'] => ['app', 'services'],
- ['lib', 'gitlab', 'conflicts'] => ['lib'],
- ['lib', 'gitlab', 'diff'] => ['lib'],
- ['lib', 'gitlab', 'email', 'message'] => ['lib'],
- ['lib', 'gitlab', 'checks'] => ['lib']
- }
-
- paths_to_instrument.each do |(path, prefix)|
- prefix = Rails.root.join(*prefix)
-
- Dir[Rails.root.join(*path + ['*.rb'])].each do |file_path|
- path = Pathname.new(file_path).relative_path_from(prefix)
- const = path.to_s.sub('.rb', '').camelize.constantize
-
- config.instrument_methods(const)
- config.instrument_instance_methods(const)
- end
- end
-
- config.instrument_methods(Premailer::Adapter::Nokogiri)
- config.instrument_instance_methods(Premailer::Adapter::Nokogiri)
-
- [
- :Blame, :Branch, :BranchCollection, :Blob, :Commit, :Diff, :Repository,
- :Tag, :TagCollection, :Tree
- ].each do |name|
- const = Rugged.const_get(name)
-
- config.instrument_methods(const)
- config.instrument_instance_methods(const)
- end
-
- # Instruments all Banzai filters and reference parsers
- {
- Filter: Rails.root.join('lib', 'banzai', 'filter', '*.rb'),
- ReferenceParser: Rails.root.join('lib', 'banzai', 'reference_parser', '*.rb')
- }.each do |const_name, path|
- Dir[path].each do |file|
- klass = File.basename(file, File.extname(file)).camelize
- const = Banzai.const_get(const_name).const_get(klass)
-
- config.instrument_methods(const)
- config.instrument_instance_methods(const)
- end
- end
-
- config.instrument_methods(Banzai::Renderer)
- config.instrument_methods(Banzai::Querying)
-
- config.instrument_instance_methods(Banzai::ObjectRenderer)
- config.instrument_instance_methods(Banzai::Redactor)
- config.instrument_methods(Banzai::NoteRenderer)
-
- [Issuable, Mentionable, Participable].each do |klass|
- config.instrument_instance_methods(klass)
- config.instrument_instance_methods(klass::ClassMethods)
- end
-
- config.instrument_methods(Gitlab::ReferenceExtractor)
- config.instrument_instance_methods(Gitlab::ReferenceExtractor)
-
- # Instrument the classes used for checking if somebody has push access.
- config.instrument_instance_methods(Gitlab::GitAccess)
- config.instrument_instance_methods(Gitlab::GitAccessWiki)
-
- config.instrument_instance_methods(API::Helpers)
-
- config.instrument_instance_methods(RepositoryCheck::SingleRepositoryWorker)
-
- 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)
+ instrument_classes(config)
end
GC::Profiler.enable
diff --git a/config/initializers/plantuml_lexer.rb b/config/initializers/plantuml_lexer.rb
new file mode 100644
index 00000000000..e8a77b146fa
--- /dev/null
+++ b/config/initializers/plantuml_lexer.rb
@@ -0,0 +1,2 @@
+# Touch the lexers so it is registered with Rouge
+Rouge::Lexers::Plantuml
diff --git a/config/initializers/request_profiler.rb b/config/initializers/request_profiler.rb
index a9aa802681a..fb5a7b8372e 100644
--- a/config/initializers/request_profiler.rb
+++ b/config/initializers/request_profiler.rb
@@ -1,5 +1,3 @@
-require 'gitlab/request_profiler/middleware'
-
Rails.application.configure do |config|
config.middleware.use(Gitlab::RequestProfiler::Middleware)
end
diff --git a/config/initializers/rspec_profiling.rb b/config/initializers/rspec_profiling.rb
new file mode 100644
index 00000000000..f462e654b2c
--- /dev/null
+++ b/config/initializers/rspec_profiling.rb
@@ -0,0 +1,14 @@
+module RspecProfilingConnection
+ def establish_connection
+ ::RspecProfiling::Collectors::PSQL::Result.establish_connection(ENV['RSPEC_PROFILING_POSTGRES_URL'])
+ end
+end
+
+if Rails.env.test?
+ RspecProfiling.configure do |config|
+ if ENV['RSPEC_PROFILING_POSTGRES_URL']
+ RspecProfiling::Collectors::PSQL.prepend(RspecProfilingConnection)
+ config.collector = RspecProfiling::Collectors::PSQL
+ end
+ end
+end
diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb
index 5a7365bb0f6..0c4516b70f0 100644
--- a/config/initializers/sidekiq.rb
+++ b/config/initializers/sidekiq.rb
@@ -12,6 +12,11 @@ Sidekiq.configure_server do |config|
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'
+ chain.add Gitlab::SidekiqStatus::ServerMiddleware
+ end
+
+ config.client_middleware do |chain|
+ chain.add Gitlab::SidekiqStatus::ClientMiddleware
end
# Sidekiq-cron: load recurring jobs from gitlab.yml
@@ -31,11 +36,9 @@ Sidekiq.configure_server do |config|
Gitlab::SidekiqThrottler.execute!
- # Database pool should be at least `sidekiq_concurrency` + 2
- # For more info, see: https://github.com/mperham/sidekiq/blob/master/4.0-Upgrade.md
config = ActiveRecord::Base.configurations[Rails.env] ||
Rails.application.config.database_configuration[Rails.env]
- config['pool'] = Sidekiq.options[:concurrency] + 2
+ config['pool'] = Sidekiq.options[:concurrency]
ActiveRecord::Base.establish_connection(config)
Rails.logger.debug("Connection Pool size for Sidekiq Server is now: #{ActiveRecord::Base.connection.pool.instance_variable_get('@size')}")
@@ -46,6 +49,10 @@ end
Sidekiq.configure_client do |config|
config.redis = redis_config_hash
+
+ config.client_middleware do |chain|
+ chain.add Gitlab::SidekiqStatus::ClientMiddleware
+ end
end
# The Sidekiq client API always adds the queue to the Sidekiq queue
diff --git a/config/initializers/static_files.rb b/config/initializers/static_files.rb
index d6dbf8b9fbf..74aba6c5d06 100644
--- a/config/initializers/static_files.rb
+++ b/config/initializers/static_files.rb
@@ -12,4 +12,35 @@ if app.config.serve_static_files
app.paths["public"].first,
app.config.static_cache_control
)
+
+ # If webpack-dev-server is configured, proxy webpack's public directory
+ # instead of looking for static assets
+ dev_server = Gitlab.config.webpack.dev_server
+
+ if dev_server.enabled
+ settings = {
+ enabled: true,
+ host: dev_server.host,
+ port: dev_server.port,
+ manifest_host: dev_server.host,
+ manifest_port: dev_server.port,
+ }
+
+ if Rails.env.development?
+ settings.merge!(
+ host: Gitlab.config.gitlab.host,
+ port: Gitlab.config.gitlab.port,
+ https: Gitlab.config.gitlab.https,
+ )
+ app.config.middleware.insert_before(
+ Gitlab::Middleware::Static,
+ Gitlab::Middleware::WebpackProxy,
+ proxy_path: app.config.webpack.public_path,
+ proxy_host: dev_server.host,
+ proxy_port: dev_server.port,
+ )
+ end
+
+ app.config.webpack.dev_server.merge!(settings)
+ end
end
diff --git a/config/initializers/workhorse_multipart.rb b/config/initializers/workhorse_multipart.rb
index 3e2f25c354a..84d809741c4 100644
--- a/config/initializers/workhorse_multipart.rb
+++ b/config/initializers/workhorse_multipart.rb
@@ -1,3 +1,25 @@
Rails.application.configure do |config|
config.middleware.use(Gitlab::Middleware::Multipart)
end
+
+# The Gitlab::Middleware::Multipart middleware inserts instances of our
+# own ::UploadedFile class in the Rack env of requests. These instances
+# will be blocked by the 'strong parameters' feature of ActionController
+# unless we somehow whitelist them. At the moment it seems the only way
+# to do that is by monkey-patching.
+#
+module Gitlab
+ module StrongParameterScalars
+ GITLAB_PERMITTED_SCALAR_TYPES = [::UploadedFile]
+
+ def permitted_scalar?(value)
+ super || GITLAB_PERMITTED_SCALAR_TYPES.any? { |type| value.is_a?(type) }
+ end
+ end
+end
+
+module ActionController
+ class Parameters
+ prepend Gitlab::StrongParameterScalars
+ end
+end
diff --git a/config/karma.config.js b/config/karma.config.js
new file mode 100644
index 00000000000..44229e2ee88
--- /dev/null
+++ b/config/karma.config.js
@@ -0,0 +1,21 @@
+var path = require('path');
+var webpackConfig = require('./webpack.config.js');
+var ROOT_PATH = path.resolve(__dirname, '..');
+
+// Karma configuration
+module.exports = function(config) {
+ config.set({
+ basePath: ROOT_PATH,
+ browsers: ['PhantomJS'],
+ frameworks: ['jasmine'],
+ files: [
+ { pattern: 'spec/javascripts/test_bundle.js', watched: false },
+ { pattern: 'spec/javascripts/fixtures/**/*@(.json|.html|.html.raw)', included: false },
+ ],
+ preprocessors: {
+ 'spec/javascripts/**/*.js?(.es6)': ['webpack', 'sourcemap'],
+ },
+ webpack: webpackConfig,
+ webpackMiddleware: { stats: 'errors-only' },
+ });
+};
diff --git a/config/routes/admin.rb b/config/routes/admin.rb
index 0dd2c8f7aef..8e99239f350 100644
--- a/config/routes/admin.rb
+++ b/config/routes/admin.rb
@@ -6,7 +6,6 @@ namespace :admin do
member do
get :projects
get :keys
- get :groups
put :block
put :unblock
put :unlock
@@ -32,7 +31,7 @@ namespace :admin do
scope(path: 'groups/*id',
controller: :groups,
- constraints: { id: Gitlab::Regex.namespace_route_regex }) do
+ constraints: { id: Gitlab::Regex.namespace_route_regex, format: /(html|json|atom)/ }) do
scope(as: :group) do
put :members_update
diff --git a/config/routes/group.rb b/config/routes/group.rb
index 776c31c9dac..60a1175fe80 100644
--- a/config/routes/group.rb
+++ b/config/routes/group.rb
@@ -19,13 +19,11 @@ end
scope(path: 'groups/*id',
controller: :groups,
- constraints: { id: Gitlab::Regex.namespace_route_regex }) do
+ constraints: { id: Gitlab::Regex.namespace_route_regex, format: /(html|json|atom)/ }) do
get :edit, as: :edit_group
get :issues, as: :issues_group
get :merge_requests, as: :merge_requests_group
get :projects, as: :projects_group
get :activity, as: :activity_group
+ get '/', action: :show, as: :group_canonical
end
-
-# Must be last route in this file
-get 'groups/*id' => 'groups#show', as: :group_canonical, constraints: { id: Gitlab::Regex.namespace_route_regex }
diff --git a/config/routes/project.rb b/config/routes/project.rb
index 335fccb617b..2ac98cf3842 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -32,16 +32,17 @@ constraints(ProjectUrlConstrainer.new) do
resources :commit, only: [:show], constraints: { id: /\h{7,40}/ } do
member do
get :branches
- get :builds
get :pipelines
- post :cancel_builds
- post :retry_builds
post :revert
post :cherry_pick
get :diff_for_path
end
end
+ resource :pages, only: [:show, :destroy] do
+ resources :domains, only: [:show, :new, :create, :destroy], controller: 'pages_domains', constraints: { id: /[^\/]+/ }
+ end
+
resources :compare, only: [:index, :create] do
collection do
get :diff_for_path
@@ -67,6 +68,7 @@ constraints(ProjectUrlConstrainer.new) do
resources :snippets, concerns: :awardable, constraints: { id: /\d+/ } do
member do
get 'raw'
+ post :mark_as_spam
end
end
@@ -76,6 +78,8 @@ constraints(ProjectUrlConstrainer.new) do
end
end
+ resource :mattermost, only: [:new, :create]
+
resources :deploy_keys, constraints: { id: /\d+/ }, only: [:index, :new, :create] do
member do
put :enable
@@ -92,10 +96,10 @@ constraints(ProjectUrlConstrainer.new) do
get :diffs
get :conflicts
get :conflict_for_path
- get :builds
get :pipelines
get :merge_check
post :merge
+ get :merge_widget_refresh
post :cancel_merge_when_build_succeeds
get :ci_status
get :ci_environments_status
@@ -221,6 +225,7 @@ constraints(ProjectUrlConstrainer.new) do
end
member do
+ post :promote
post :toggle_subscription
delete :remove_priority
end
@@ -266,7 +271,7 @@ constraints(ProjectUrlConstrainer.new) do
resources :boards, only: [:index, :show] do
scope module: :boards do
- resources :issues, only: [:update]
+ resources :issues, only: [:index, :update]
resources :lists, only: [:index, :create, :update, :destroy] do
collection do
@@ -308,6 +313,11 @@ constraints(ProjectUrlConstrainer.new) do
end
end
end
+ namespace :settings do
+ resource :members, only: [:show]
+ resource :ci_cd, only: [:show], controller: 'ci_cd'
+ resource :integrations, only: [:show]
+ end
# Since both wiki and repository routing contains wildcard characters
# its preferable to keep it below all other project routes
diff --git a/config/routes/snippets.rb b/config/routes/snippets.rb
index 3ca096f31ba..ce0d1314292 100644
--- a/config/routes/snippets.rb
+++ b/config/routes/snippets.rb
@@ -2,6 +2,7 @@ resources :snippets, concerns: :awardable do
member do
get 'raw'
get 'download'
+ post :mark_as_spam
end
end
diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
index c22964179d9..56bf4e6b1de 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -29,6 +29,7 @@
- [email_receiver, 2]
- [emails_on_push, 2]
- [mailers, 2]
+ - [use_key, 1]
- [repository_fork, 1]
- [repository_import, 1]
- [project_service, 1]
@@ -49,3 +50,4 @@
- [reactive_caching, 1]
- [cronjob, 1]
- [default, 1]
+ - [pages, 1]
diff --git a/config/webpack.config.js b/config/webpack.config.js
new file mode 100644
index 00000000000..953ae463c86
--- /dev/null
+++ b/config/webpack.config.js
@@ -0,0 +1,126 @@
+'use strict';
+
+var fs = require('fs');
+var path = require('path');
+var webpack = require('webpack');
+var StatsPlugin = require('stats-webpack-plugin');
+var CompressionPlugin = require('compression-webpack-plugin');
+
+var ROOT_PATH = path.resolve(__dirname, '..');
+var IS_PRODUCTION = process.env.NODE_ENV === 'production';
+var IS_DEV_SERVER = process.argv[1].indexOf('webpack-dev-server') !== -1;
+var DEV_SERVER_PORT = parseInt(process.env.DEV_SERVER_PORT, 10) || 3808;
+
+var config = {
+ context: path.join(ROOT_PATH, 'app/assets/javascripts'),
+ entry: {
+ application: './application.js',
+ blob_edit: './blob_edit/blob_edit_bundle.js',
+ boards: './boards/boards_bundle.js',
+ boards_test: './boards/test_utils/simulate_drag.js',
+ cycle_analytics: './cycle_analytics/cycle_analytics_bundle.js',
+ commit_pipelines: './commit/pipelines/pipelines_bundle.js',
+ diff_notes: './diff_notes/diff_notes_bundle.js',
+ environments: './environments/environments_bundle.js',
+ filtered_search: './filtered_search/filtered_search_bundle.js',
+ graphs: './graphs/graphs_bundle.js',
+ issuable: './issuable/issuable_bundle.js',
+ merge_conflicts: './merge_conflicts/merge_conflicts_bundle.js',
+ merge_request_widget: './merge_request_widget/ci_bundle.js',
+ network: './network/network_bundle.js',
+ profile: './profile/profile_bundle.js',
+ protected_branches: './protected_branches/protected_branches_bundle.js',
+ snippet: './snippet/snippet_bundle.js',
+ terminal: './terminal/terminal_bundle.js',
+ users: './users/users_bundle.js',
+ lib_chart: './lib/chart.js',
+ lib_d3: './lib/d3.js',
+ lib_vue: './lib/vue_resource.js',
+ vue_pipelines: './vue_pipelines_index/index.js',
+ },
+
+ output: {
+ path: path.join(ROOT_PATH, 'public/assets/webpack'),
+ publicPath: '/assets/webpack/',
+ filename: IS_PRODUCTION ? '[name]-[chunkhash].js' : '[name].js'
+ },
+
+ devtool: 'inline-source-map',
+
+ module: {
+ loaders: [
+ {
+ test: /\.(js|es6)$/,
+ exclude: /(node_modules|vendor\/assets)/,
+ loader: 'babel-loader',
+ query: {
+ // 'use strict' was broken in sprockets-es6 due to sprockets concatination method.
+ // many es5 strict errors which were never caught ended up in our es6 assets as a result.
+ // this hack is necessary until they can be fixed.
+ blacklist: ['useStrict']
+ }
+ },
+ {
+ test: /\.(js|es6)$/,
+ loader: 'imports-loader',
+ query: 'this=>window'
+ },
+ {
+ test: /\.json$/,
+ loader: 'json-loader'
+ }
+ ]
+ },
+
+ plugins: [
+ // manifest filename must match config.webpack.manifest_filename
+ // webpack-rails only needs assetsByChunkName to function properly
+ new StatsPlugin('manifest.json', {
+ chunkModules: false,
+ source: false,
+ chunks: false,
+ modules: false,
+ assets: true
+ }),
+ new CompressionPlugin({
+ asset: '[path].gz[query]',
+ }),
+ ],
+
+ resolve: {
+ extensions: ['', '.js', '.es6', '.js.es6'],
+ alias: {
+ '~': path.join(ROOT_PATH, 'app/assets/javascripts'),
+ 'bootstrap/js': 'bootstrap-sass/assets/javascripts/bootstrap',
+ 'emoji-aliases$': path.join(ROOT_PATH, 'fixtures/emojis/aliases.json'),
+ 'vendor': path.join(ROOT_PATH, 'vendor/assets/javascripts'),
+ 'vue$': 'vue/dist/vue.js',
+ 'vue-resource$': 'vue-resource/dist/vue-resource.js'
+ }
+ }
+}
+
+if (IS_PRODUCTION) {
+ config.devtool = 'source-map';
+ config.plugins.push(
+ new webpack.NoErrorsPlugin(),
+ new webpack.optimize.UglifyJsPlugin({
+ compress: { warnings: false }
+ }),
+ new webpack.DefinePlugin({
+ 'process.env': { NODE_ENV: JSON.stringify('production') }
+ }),
+ new webpack.optimize.DedupePlugin(),
+ new webpack.optimize.OccurrenceOrderPlugin()
+ );
+}
+
+if (IS_DEV_SERVER) {
+ config.devServer = {
+ port: DEV_SERVER_PORT,
+ headers: { 'Access-Control-Allow-Origin': '*' }
+ };
+ config.output.publicPath = '//localhost:' + DEV_SERVER_PORT + config.output.publicPath;
+}
+
+module.exports = config;
diff --git a/db/fixtures/development/01_admin.rb b/db/fixtures/development/01_admin.rb
index bba2fc4b186..6f241f6fa4a 100644
--- a/db/fixtures/development/01_admin.rb
+++ b/db/fixtures/development/01_admin.rb
@@ -1,3 +1,5 @@
+require './spec/support/sidekiq'
+
Gitlab::Seeder.quiet do
User.seed do |s|
s.id = 1
diff --git a/db/fixtures/development/04_project.rb b/db/fixtures/development/04_project.rb
index a984eda5ab5..c2b8f7ba819 100644
--- a/db/fixtures/development/04_project.rb
+++ b/db/fixtures/development/04_project.rb
@@ -1,4 +1,4 @@
-require 'sidekiq/testing'
+require './spec/support/sidekiq'
Sidekiq::Testing.inline! do
Gitlab::Seeder.quiet do
diff --git a/db/fixtures/development/05_users.rb b/db/fixtures/development/05_users.rb
index 03da29c4c68..101ff3a1209 100644
--- a/db/fixtures/development/05_users.rb
+++ b/db/fixtures/development/05_users.rb
@@ -1,3 +1,5 @@
+require './spec/support/sidekiq'
+
Gitlab::Seeder.quiet do
20.times do |i|
begin
diff --git a/db/fixtures/development/06_teams.rb b/db/fixtures/development/06_teams.rb
index 5c2a03fec3f..86e0a38aae1 100644
--- a/db/fixtures/development/06_teams.rb
+++ b/db/fixtures/development/06_teams.rb
@@ -1,4 +1,4 @@
-require 'sidekiq/testing'
+require './spec/support/sidekiq'
Sidekiq::Testing.inline! do
Gitlab::Seeder.quiet do
diff --git a/db/fixtures/development/07_milestones.rb b/db/fixtures/development/07_milestones.rb
index 540e4e68259..271bfbc97e0 100644
--- a/db/fixtures/development/07_milestones.rb
+++ b/db/fixtures/development/07_milestones.rb
@@ -1,3 +1,5 @@
+require './spec/support/sidekiq'
+
Gitlab::Seeder.quiet do
Project.all.each do |project|
5.times do |i|
diff --git a/db/fixtures/development/09_issues.rb b/db/fixtures/development/09_issues.rb
index 4fa572fca9b..d93d133d157 100644
--- a/db/fixtures/development/09_issues.rb
+++ b/db/fixtures/development/09_issues.rb
@@ -1,3 +1,5 @@
+require './spec/support/sidekiq'
+
Gitlab::Seeder.quiet do
Project.all.each do |project|
10.times do
diff --git a/db/fixtures/development/10_merge_requests.rb b/db/fixtures/development/10_merge_requests.rb
index 87fb8e3300d..c304e0706dc 100644
--- a/db/fixtures/development/10_merge_requests.rb
+++ b/db/fixtures/development/10_merge_requests.rb
@@ -1,3 +1,5 @@
+require './spec/support/sidekiq'
+
Gitlab::Seeder.quiet do
# Limit the number of merge requests per project to avoid long seeds
MAX_NUM_MERGE_REQUESTS = 10
@@ -24,7 +26,7 @@ Gitlab::Seeder.quiet do
end
end
- project = Project.find_with_namespace('gitlab-org/gitlab-test')
+ project = Project.find_by_full_path('gitlab-org/gitlab-test')
params = {
source_branch: 'feature',
diff --git a/db/fixtures/development/11_keys.rb b/db/fixtures/development/11_keys.rb
index 8b4bee384e1..51e22137d6f 100644
--- a/db/fixtures/development/11_keys.rb
+++ b/db/fixtures/development/11_keys.rb
@@ -1,12 +1,18 @@
-Gitlab::Seeder.quiet do
- User.first(10).each do |user|
- key = "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt#{user.id + 100}6k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0="
+require './spec/support/sidekiq'
- user.keys.create(
- title: "Sample key #{user.id}",
- key: key
- )
+# Creating keys runs a gitlab-shell worker. Since we may not have the right
+# gitlab-shell path set (yet) we need to disable this for these fixtures.
+Sidekiq::Testing.disable! do
+ Gitlab::Seeder.quiet do
+ User.first(10).each do |user|
+ key = "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt#{user.id + 100}6k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0="
- print '.'
+ user.keys.create(
+ title: "Sample key #{user.id}",
+ key: key
+ )
+
+ print '.'
+ end
end
end
diff --git a/db/fixtures/development/12_snippets.rb b/db/fixtures/development/12_snippets.rb
index 74898544a69..4f3bdba043d 100644
--- a/db/fixtures/development/12_snippets.rb
+++ b/db/fixtures/development/12_snippets.rb
@@ -1,3 +1,5 @@
+require './spec/support/sidekiq'
+
Gitlab::Seeder.quiet do
content =<<eos
class Member < ActiveRecord::Base
diff --git a/db/fixtures/development/13_comments.rb b/db/fixtures/development/13_comments.rb
index 566c0705638..29b8081055d 100644
--- a/db/fixtures/development/13_comments.rb
+++ b/db/fixtures/development/13_comments.rb
@@ -1,3 +1,5 @@
+require './spec/support/sidekiq'
+
Gitlab::Seeder.quiet do
Issue.all.each do |issue|
project = issue.project
diff --git a/db/fixtures/development/14_pipelines.rb b/db/fixtures/development/14_pipelines.rb
index be95d788850..534847a7107 100644
--- a/db/fixtures/development/14_pipelines.rb
+++ b/db/fixtures/development/14_pipelines.rb
@@ -1,3 +1,5 @@
+require './spec/support/sidekiq'
+
class Gitlab::Seeder::Pipelines
STAGES = %w[build test deploy notify]
BUILDS = [
diff --git a/db/fixtures/development/15_award_emoji.rb b/db/fixtures/development/15_award_emoji.rb
index baac32f2d10..ea343c26b69 100644
--- a/db/fixtures/development/15_award_emoji.rb
+++ b/db/fixtures/development/15_award_emoji.rb
@@ -1,3 +1,5 @@
+require './spec/support/sidekiq'
+
Gitlab::Seeder.quiet do
emoji = Gitlab::AwardEmoji.emojis.keys
diff --git a/db/fixtures/development/16_protected_branches.rb b/db/fixtures/development/16_protected_branches.rb
index 103c7f9445c..39d466fb43f 100644
--- a/db/fixtures/development/16_protected_branches.rb
+++ b/db/fixtures/development/16_protected_branches.rb
@@ -1,3 +1,5 @@
+require './spec/support/sidekiq'
+
Gitlab::Seeder.quiet do
admin_user = User.find(1)
diff --git a/db/fixtures/development/17_cycle_analytics.rb b/db/fixtures/development/17_cycle_analytics.rb
index 916ee8dbac8..747901dd634 100644
--- a/db/fixtures/development/17_cycle_analytics.rb
+++ b/db/fixtures/development/17_cycle_analytics.rb
@@ -1,4 +1,4 @@
-require 'sidekiq/testing'
+require './spec/support/sidekiq'
require './spec/support/test_env'
class Gitlab::Seeder::CycleAnalytics
diff --git a/db/migrate/20141006143943_move_slack_service_to_webhook.rb b/db/migrate/20141006143943_move_slack_service_to_webhook.rb
index 42e88d6d6e3..561184615cc 100644
--- a/db/migrate/20141006143943_move_slack_service_to_webhook.rb
+++ b/db/migrate/20141006143943_move_slack_service_to_webhook.rb
@@ -5,7 +5,7 @@ class MoveSlackServiceToWebhook < ActiveRecord::Migration
DOWNTIME_REASON = 'Move old fields "token" and "subdomain" to one single field "webhook"'
def change
- SlackNotificationService.all.each do |slack_service|
+ SlackService.all.each do |slack_service|
if ["token", "subdomain"].all? { |property| slack_service.properties.key? property }
token = slack_service.properties['token']
subdomain = slack_service.properties['subdomain']
diff --git a/db/migrate/20151215132013_add_pages_size_to_application_settings.rb b/db/migrate/20151215132013_add_pages_size_to_application_settings.rb
new file mode 100644
index 00000000000..f3a663f805b
--- /dev/null
+++ b/db/migrate/20151215132013_add_pages_size_to_application_settings.rb
@@ -0,0 +1,14 @@
+class AddPagesSizeToApplicationSettings < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+ disable_ddl_transaction!
+
+ DOWNTIME = false
+
+ def up
+ add_column_with_default :application_settings, :max_pages_size, :integer, default: 100, allow_null: false
+ end
+
+ def down
+ remove_column(:application_settings, :max_pages_size)
+ end
+end
diff --git a/db/migrate/20160210105555_create_pages_domain.rb b/db/migrate/20160210105555_create_pages_domain.rb
new file mode 100644
index 00000000000..0e8507c7e9a
--- /dev/null
+++ b/db/migrate/20160210105555_create_pages_domain.rb
@@ -0,0 +1,16 @@
+class CreatePagesDomain < ActiveRecord::Migration
+ DOWNTIME = false
+
+ def change
+ create_table :pages_domains do |t|
+ t.integer :project_id
+ t.text :certificate
+ t.text :encrypted_key
+ t.string :encrypted_key_iv
+ t.string :encrypted_key_salt
+ t.string :domain
+ end
+
+ add_index :pages_domains, :domain, unique: true
+ end
+end
diff --git a/db/migrate/20160811172945_add_can_push_to_keys.rb b/db/migrate/20160811172945_add_can_push_to_keys.rb
new file mode 100644
index 00000000000..5fd303fe8fb
--- /dev/null
+++ b/db/migrate/20160811172945_add_can_push_to_keys.rb
@@ -0,0 +1,14 @@
+class AddCanPushToKeys < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+ disable_ddl_transaction!
+
+ DOWNTIME = false
+
+ def up
+ add_column_with_default(:keys, :can_push, :boolean, default: false, allow_null: false)
+ end
+
+ def down
+ remove_column(:keys, :can_push)
+ end
+end
diff --git a/db/migrate/20161114024742_add_coverage_regex_to_builds.rb b/db/migrate/20161114024742_add_coverage_regex_to_builds.rb
new file mode 100644
index 00000000000..88aa5d52b39
--- /dev/null
+++ b/db/migrate/20161114024742_add_coverage_regex_to_builds.rb
@@ -0,0 +1,13 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddCoverageRegexToBuilds < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ def change
+ add_column :ci_builds, :coverage_regex, :string
+ end
+end
diff --git a/db/migrate/20161117114805_remove_undeleted_groups.rb b/db/migrate/20161117114805_remove_undeleted_groups.rb
index 696914f8e4d..29040583aa2 100644
--- a/db/migrate/20161117114805_remove_undeleted_groups.rb
+++ b/db/migrate/20161117114805_remove_undeleted_groups.rb
@@ -5,47 +5,87 @@ class RemoveUndeletedGroups < ActiveRecord::Migration
DOWNTIME = false
def up
+ is_ee = defined?(Gitlab::License)
+
+ if is_ee
+ execute <<-EOF.strip_heredoc
+ DELETE FROM path_locks
+ WHERE project_id IN (
+ SELECT project_id
+ FROM projects
+ WHERE namespace_id IN (#{namespaces_pending_removal})
+ );
+ EOF
+
+ execute <<-EOF.strip_heredoc
+ DELETE FROM remote_mirrors
+ WHERE project_id IN (
+ SELECT project_id
+ FROM projects
+ WHERE namespace_id IN (#{namespaces_pending_removal})
+ );
+ EOF
+ end
+
execute <<-EOF.strip_heredoc
- DELETE FROM projects
- WHERE namespace_id IN (
- SELECT id FROM (
- SELECT id
- FROM namespaces
- WHERE deleted_at IS NOT NULL
- ) namespace_ids
+ DELETE FROM lists
+ WHERE label_id IN (
+ SELECT id
+ FROM labels
+ WHERE group_id IN (#{namespaces_pending_removal})
+ );
+ EOF
+
+ execute <<-EOF.strip_heredoc
+ DELETE FROM lists
+ WHERE board_id IN (
+ SELECT id
+ FROM boards
+ WHERE project_id IN (
+ SELECT project_id
+ FROM projects
+ WHERE namespace_id IN (#{namespaces_pending_removal})
+ )
);
EOF
- if defined?(Gitlab::License)
+ execute <<-EOF.strip_heredoc
+ DELETE FROM labels
+ WHERE group_id IN (#{namespaces_pending_removal});
+ EOF
+
+ execute <<-EOF.strip_heredoc
+ DELETE FROM boards
+ WHERE project_id IN (
+ SELECT project_id
+ FROM projects
+ WHERE namespace_id IN (#{namespaces_pending_removal})
+ )
+ EOF
+
+ execute <<-EOF.strip_heredoc
+ DELETE FROM projects
+ WHERE namespace_id IN (#{namespaces_pending_removal});
+ EOF
+
+ if is_ee
# EE adds these columns but we have to make sure this data is cleaned up
# here before we run the DELETE below. An alternative would be patching
# this migration in EE but this will only result in a mess and confusing
# migrations.
execute <<-EOF.strip_heredoc
DELETE FROM protected_branch_push_access_levels
- WHERE group_id IN (
- SELECT id FROM (
- SELECT id
- FROM namespaces
- WHERE deleted_at IS NOT NULL
- ) namespace_ids
- );
+ WHERE group_id IN (#{namespaces_pending_removal});
EOF
execute <<-EOF.strip_heredoc
DELETE FROM protected_branch_merge_access_levels
- WHERE group_id IN (
- SELECT id FROM (
- SELECT id
- FROM namespaces
- WHERE deleted_at IS NOT NULL
- ) namespace_ids
- );
+ WHERE group_id IN (#{namespaces_pending_removal});
EOF
end
- # This removes namespaces that were supposed to be soft deleted but still
- # reside in the database.
+ # This removes namespaces that were supposed to be deleted but still reside
+ # in the database.
execute "DELETE FROM namespaces WHERE deleted_at IS NOT NULL;"
end
@@ -54,4 +94,12 @@ class RemoveUndeletedGroups < ActiveRecord::Migration
# If someone is trying to rollback for other reasons, we should not throw an Exception.
# raise ActiveRecord::IrreversibleMigration
end
+
+ def namespaces_pending_removal
+ "SELECT id FROM (
+ SELECT id
+ FROM namespaces
+ WHERE deleted_at IS NOT NULL
+ ) namespace_ids"
+ end
end
diff --git a/db/migrate/20161201001911_add_plant_uml_url_to_application_settings.rb b/db/migrate/20161201001911_add_plant_uml_url_to_application_settings.rb
new file mode 100644
index 00000000000..b8d8742ae40
--- /dev/null
+++ b/db/migrate/20161201001911_add_plant_uml_url_to_application_settings.rb
@@ -0,0 +1,12 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddPlantUmlUrlToApplicationSettings < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ add_column :application_settings, :plantuml_url, :string
+ end
+end
diff --git a/db/migrate/20161201155511_create_project_statistics.rb b/db/migrate/20161201155511_create_project_statistics.rb
new file mode 100644
index 00000000000..26e6d3623eb
--- /dev/null
+++ b/db/migrate/20161201155511_create_project_statistics.rb
@@ -0,0 +1,20 @@
+class CreateProjectStatistics < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ # use bigint columns to support values >2GB
+ counter_column = { limit: 8, null: false, default: 0 }
+
+ create_table :project_statistics do |t|
+ t.references :project, null: false, index: { unique: true }, foreign_key: { on_delete: :cascade }
+ t.references :namespace, null: false, index: true
+ t.integer :commit_count, counter_column
+ t.integer :storage_size, counter_column
+ t.integer :repository_size, counter_column
+ t.integer :lfs_objects_size, counter_column
+ t.integer :build_artifacts_size, counter_column
+ end
+ end
+end
diff --git a/db/migrate/20161201160452_migrate_project_statistics.rb b/db/migrate/20161201160452_migrate_project_statistics.rb
new file mode 100644
index 00000000000..3ae3f2c159b
--- /dev/null
+++ b/db/migrate/20161201160452_migrate_project_statistics.rb
@@ -0,0 +1,23 @@
+class MigrateProjectStatistics < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = true
+ DOWNTIME_REASON = 'Removes two columns from the projects table'
+
+ def up
+ # convert repository_size in float (megabytes) to integer (bytes),
+ # initialize total storage_size with repository_size
+ execute <<-EOF
+ INSERT INTO project_statistics (project_id, namespace_id, commit_count, storage_size, repository_size)
+ SELECT id, namespace_id, commit_count, (repository_size * 1024 * 1024), (repository_size * 1024 * 1024) FROM projects
+ EOF
+
+ remove_column :projects, :repository_size
+ remove_column :projects, :commit_count
+ end
+
+ def down
+ add_column_with_default :projects, :repository_size, :float, default: 0.0
+ add_column_with_default :projects, :commit_count, :integer, default: 0
+ end
+end
diff --git a/db/migrate/20161206003819_add_plant_uml_enabled_to_application_settings.rb b/db/migrate/20161206003819_add_plant_uml_enabled_to_application_settings.rb
new file mode 100644
index 00000000000..3677f978cc2
--- /dev/null
+++ b/db/migrate/20161206003819_add_plant_uml_enabled_to_application_settings.rb
@@ -0,0 +1,12 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddPlantUmlEnabledToApplicationSettings < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ add_column :application_settings, :plantuml_enabled, :boolean
+ end
+end
diff --git a/db/migrate/20161207231626_add_environment_slug.rb b/db/migrate/20161207231626_add_environment_slug.rb
index 7153e6a32b1..8e98ee5b9ba 100644
--- a/db/migrate/20161207231626_add_environment_slug.rb
+++ b/db/migrate/20161207231626_add_environment_slug.rb
@@ -8,8 +8,9 @@ class AddEnvironmentSlug < ActiveRecord::Migration
DOWNTIME_REASON = 'Adding NOT NULL column environments.slug with dependent data'
# Used to generate random suffixes for the slug
+ LETTERS = 'a'..'z'
NUMBERS = '0'..'9'
- SUFFIX_CHARS = ('a'..'z').to_a + NUMBERS.to_a
+ SUFFIX_CHARS = LETTERS.to_a + NUMBERS.to_a
def up
environments = Arel::Table.new(:environments)
@@ -39,17 +40,24 @@ class AddEnvironmentSlug < ActiveRecord::Migration
slugified = name.to_s.downcase.gsub(/[^a-z0-9]/, '-')
# Must start with a letter
- slugified = "env-" + slugified if NUMBERS.cover?(slugified[0])
+ slugified = 'env-' + slugified unless LETTERS.cover?(slugified[0])
+
+ # Repeated dashes are invalid (OpenShift limitation)
+ slugified.gsub!(/\-+/, '-')
# Maximum length: 24 characters (OpenShift limitation)
slugified = slugified[0..23]
- # Cannot end with a "-" character (Kubernetes label limitation)
- slugified = slugified[0..-2] if slugified[-1] == "-"
+ # Cannot end with a dash (Kubernetes label limitation)
+ slugified.chop! if slugified.end_with?('-')
# Add a random suffix, shortening the current string if necessary, if it
# has been slugified. This ensures uniqueness.
- slugified = slugified[0..16] + "-" + random_suffix if slugified != name
+ if slugified != name
+ slugified = slugified[0..16]
+ slugified << '-' unless slugified.end_with?('-')
+ slugified << random_suffix
+ end
slugified
end
diff --git a/db/migrate/20161213172958_change_slack_service_to_slack_notification_service.rb b/db/migrate/20161213172958_change_slack_service_to_slack_notification_service.rb
index a7278d7b5a6..dc38d0ac906 100644
--- a/db/migrate/20161213172958_change_slack_service_to_slack_notification_service.rb
+++ b/db/migrate/20161213172958_change_slack_service_to_slack_notification_service.rb
@@ -1,14 +1,11 @@
class ChangeSlackServiceToSlackNotificationService < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
- DOWNTIME = true
- DOWNTIME_REASON = 'Rename SlackService to SlackNotificationService'
+ DOWNTIME = false
- def up
- execute("UPDATE services SET type = 'SlackNotificationService' WHERE type = 'SlackService'")
- end
-
- def down
- execute("UPDATE services SET type = 'SlackService' WHERE type = 'SlackNotificationService'")
+ # This migration is a no-op, as it existed in an RC but we renamed
+ # SlackNotificationService back to SlackService:
+ # https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8191#note_20310845
+ def change
end
end
diff --git a/db/migrate/20161220141214_remove_dot_git_from_group_names.rb b/db/migrate/20161220141214_remove_dot_git_from_group_names.rb
new file mode 100644
index 00000000000..241afc6b097
--- /dev/null
+++ b/db/migrate/20161220141214_remove_dot_git_from_group_names.rb
@@ -0,0 +1,82 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class RemoveDotGitFromGroupNames < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+ include Gitlab::ShellAdapter
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ def up
+ invalid_groups.each do |group|
+ path_was = group['path']
+ path_was_wildcard = quote_string("#{path_was}/%")
+ path = quote_string(rename_path(path_was))
+
+ move_namespace(group['id'], path_was, path)
+
+ execute "UPDATE routes SET path = '#{path}' WHERE source_type = 'Namespace' AND source_id = #{group['id']}"
+ execute "UPDATE namespaces SET path = '#{path}' WHERE id = #{group['id']}"
+
+ select_all("SELECT id, path FROM routes WHERE path LIKE '#{path_was_wildcard}'").each do |route|
+ new_path = "#{path}/#{route['path'].split('/').last}"
+ execute "UPDATE routes SET path = '#{new_path}' WHERE id = #{route['id']}"
+ end
+ end
+ end
+
+ def down
+ # nothing to do here
+ end
+
+ private
+
+ def invalid_groups
+ select_all("SELECT id, path FROM namespaces WHERE type = 'Group' AND path LIKE '%.git'")
+ end
+
+ def route_exists?(path)
+ select_all("SELECT id, path FROM routes WHERE path = '#{quote_string(path)}'").present?
+ end
+
+ # Accepts invalid path like test.git and returns test_git or
+ # test_git1 if test_git already taken
+ def rename_path(path)
+ # To stay closer with original name and reduce risk of duplicates
+ # we rename suffix instead of removing it
+ path = path.sub(/\.git\z/, '_git')
+
+ counter = 0
+ base = path
+
+ while route_exists?(path)
+ counter += 1
+ path = "#{base}#{counter}"
+ end
+
+ path
+ end
+
+ def move_namespace(group_id, path_was, path)
+ repository_storage_paths = select_all("SELECT distinct(repository_storage) FROM projects WHERE namespace_id = #{group_id}").map do |row|
+ Gitlab.config.repositories.storages[row['repository_storage']]
+ end.compact
+
+ # Move the namespace directory in all storages paths used by member projects
+ repository_storage_paths.each do |repository_storage_path|
+ # Ensure old directory exists before moving it
+ gitlab_shell.add_namespace(repository_storage_path, path_was)
+
+ unless gitlab_shell.mv_namespace(repository_storage_path, path_was, path)
+ Rails.logger.error "Exception moving path #{repository_storage_path} from #{path_was} to #{path}"
+
+ # if we cannot move namespace directory we should rollback
+ # db changes in order to prevent out of sync between db and fs
+ raise Exception.new('namespace directory cannot be moved')
+ end
+ end
+
+ Gitlab::UploadsTransfer.new.rename_namespace(path_was, path)
+ end
+end
diff --git a/db/migrate/20161221152132_add_last_used_at_to_key.rb b/db/migrate/20161221152132_add_last_used_at_to_key.rb
new file mode 100644
index 00000000000..fb2b15817de
--- /dev/null
+++ b/db/migrate/20161221152132_add_last_used_at_to_key.rb
@@ -0,0 +1,9 @@
+class AddLastUsedAtToKey < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ add_column :keys, :last_used_at, :datetime
+ end
+end
diff --git a/db/migrate/20161223034433_add_estimate_to_issuables_ce.rb b/db/migrate/20161223034433_add_estimate_to_issuables_ce.rb
new file mode 100644
index 00000000000..d5116dfab49
--- /dev/null
+++ b/db/migrate/20161223034433_add_estimate_to_issuables_ce.rb
@@ -0,0 +1,25 @@
+class AddEstimateToIssuablesCe < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ unless column_exists?(:issues, :time_estimate)
+ add_column :issues, :time_estimate, :integer
+ end
+
+ unless column_exists?(:merge_requests, :time_estimate)
+ add_column :merge_requests, :time_estimate, :integer
+ end
+ end
+
+ def down
+ if column_exists?(:issues, :time_estimate)
+ remove_column :issues, :time_estimate
+ end
+
+ if column_exists?(:merge_requests, :time_estimate)
+ remove_column :merge_requests, :time_estimate
+ end
+ end
+end
diff --git a/db/migrate/20161223034646_create_timelogs_ce.rb b/db/migrate/20161223034646_create_timelogs_ce.rb
new file mode 100644
index 00000000000..66d9cd823fb
--- /dev/null
+++ b/db/migrate/20161223034646_create_timelogs_ce.rb
@@ -0,0 +1,24 @@
+class CreateTimelogsCe < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ unless table_exists?(:timelogs)
+ create_table :timelogs do |t|
+ t.integer :time_spent, null: false
+ t.references :trackable, polymorphic: true
+ t.references :user
+
+ t.timestamps null: false
+ end
+
+ add_index :timelogs, [:trackable_type, :trackable_id]
+ add_index :timelogs, :user_id
+ end
+ end
+
+ def down
+ drop_table :timelogs if table_exists?(:timelogs)
+ end
+end
diff --git a/db/migrate/20161226122833_remove_dot_git_from_usernames.rb b/db/migrate/20161226122833_remove_dot_git_from_usernames.rb
new file mode 100644
index 00000000000..a0ce927161f
--- /dev/null
+++ b/db/migrate/20161226122833_remove_dot_git_from_usernames.rb
@@ -0,0 +1,114 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class RemoveDotGitFromUsernames < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+ include Gitlab::ShellAdapter
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ def up
+ invalid_users.each do |user|
+ id = user['id']
+ namespace_id = user['namespace_id']
+ path_was = user['username']
+ path_was_wildcard = quote_string("#{path_was}/%")
+ path = quote_string(new_path(path_was))
+
+ move_namespace(namespace_id, path_was, path)
+
+ begin
+ execute "UPDATE routes SET path = '#{path}' WHERE source_type = 'Namespace' AND source_id = #{namespace_id}"
+ execute "UPDATE namespaces SET path = '#{path}' WHERE id = #{namespace_id}"
+ execute "UPDATE users SET username = '#{path}' WHERE id = #{id}"
+
+ select_all("SELECT id, path FROM routes WHERE path LIKE '#{path_was_wildcard}'").each do |route|
+ new_path = "#{path}/#{route['path'].split('/').last}"
+ execute "UPDATE routes SET path = '#{new_path}' WHERE id = #{route['id']}"
+ end
+ rescue => e
+ say("Couldn't update routes for path #{path_was} to #{path}")
+ # Move namespace back
+ move_namespace(namespace_id, path, path_was)
+
+ raise e
+ end
+ end
+ end
+
+ def down
+ # nothing to do here
+ end
+
+ private
+
+ def invalid_users
+ select_all("SELECT u.id, u.username, n.path AS namespace_path, n.id AS namespace_id FROM users u
+ INNER JOIN namespaces n ON n.owner_id = u.id
+ WHERE n.type is NULL AND n.path LIKE '%.git'")
+ end
+
+ def route_exists?(path)
+ select_all("SELECT id, path FROM routes WHERE path = '#{quote_string(path)}'").present?
+ end
+
+ def path_exists?(path, repository_storage_path)
+ repository_storage_path && gitlab_shell.exists?(repository_storage_path, path)
+ end
+
+ # Accepts invalid path like test.git and returns test_git or
+ # test_git1 if test_git already taken
+ def new_path(path)
+ # To stay closer with original name and reduce risk of duplicates
+ # we rename suffix instead of removing it
+ path = path.sub(/\.git\z/, '_git')
+
+ check_routes(path.dup, 0, path)
+ end
+
+ def check_routes(base, counter, path)
+ route_exists = route_exists?(path)
+
+ Gitlab.config.repositories.storages.each_value do |storage|
+ if route_exists || path_exists?(path, storage)
+ counter += 1
+ path = "#{base}#{counter}"
+
+ return check_routes(base, counter, path)
+ end
+ end
+
+ path
+ end
+
+ def move_namespace(namespace_id, path_was, path)
+ repository_storage_paths = select_all("SELECT distinct(repository_storage) FROM projects WHERE namespace_id = #{namespace_id}").map do |row|
+ Gitlab.config.repositories.storages[row['repository_storage']]
+ end.compact
+
+ # Move the namespace directory in all storages paths used by member projects
+ repository_storage_paths.each do |repository_storage_path|
+ # Ensure old directory exists before moving it
+ gitlab_shell.add_namespace(repository_storage_path, path_was)
+
+ unless gitlab_shell.mv_namespace(repository_storage_path, path_was, path)
+ Rails.logger.error "Exception moving path #{repository_storage_path} from #{path_was} to #{path}"
+
+ # if we cannot move namespace directory we should rollback
+ # db changes in order to prevent out of sync between db and fs
+ raise Exception.new('namespace directory cannot be moved')
+ end
+ end
+
+ begin
+ Gitlab::UploadsTransfer.new.rename_namespace(path_was, path)
+ rescue => e
+ if path.nil?
+ say("Couldn't find a storage path for #{namespace_id}, #{path_was} -- skipping")
+ else
+ raise e
+ end
+ end
+ end
+end
diff --git a/db/migrate/20161227192806_rename_slack_and_mattermost_notification_services.rb b/db/migrate/20161227192806_rename_slack_and_mattermost_notification_services.rb
new file mode 100644
index 00000000000..50ad7437227
--- /dev/null
+++ b/db/migrate/20161227192806_rename_slack_and_mattermost_notification_services.rb
@@ -0,0 +1,25 @@
+class RenameSlackAndMattermostNotificationServices < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ update_column_in_batches(:services, :type, 'SlackService') do |table, query|
+ query.where(table[:type].eq('SlackNotificationService'))
+ end
+
+ update_column_in_batches(:services, :type, 'MattermostService') do |table, query|
+ query.where(table[:type].eq('MattermostNotificationService'))
+ end
+ end
+
+ def down
+ update_column_in_batches(:services, :type, 'SlackNotificationService') do |table, query|
+ query.where(table[:type].eq('SlackService'))
+ end
+
+ update_column_in_batches(:services, :type, 'MattermostNotificationService') do |table, query|
+ query.where(table[:type].eq('MattermostService'))
+ end
+ end
+end
diff --git a/db/migrate/20170121123724_add_index_to_ci_builds_for_status_runner_id_and_type.rb b/db/migrate/20170121123724_add_index_to_ci_builds_for_status_runner_id_and_type.rb
new file mode 100644
index 00000000000..4ea953f2b78
--- /dev/null
+++ b/db/migrate/20170121123724_add_index_to_ci_builds_for_status_runner_id_and_type.rb
@@ -0,0 +1,17 @@
+class AddIndexToCiBuildsForStatusRunnerIdAndType < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :ci_builds, [:status, :type, :runner_id]
+ end
+
+ def down
+ if index_exists?(:ci_builds, [:status, :type, :runner_id])
+ remove_index :ci_builds, column: [:status, :type, :runner_id]
+ end
+ end
+end
diff --git a/db/migrate/20170121130655_add_index_to_ci_runners_for_is_shared.rb b/db/migrate/20170121130655_add_index_to_ci_runners_for_is_shared.rb
new file mode 100644
index 00000000000..620befcf4d7
--- /dev/null
+++ b/db/migrate/20170121130655_add_index_to_ci_runners_for_is_shared.rb
@@ -0,0 +1,17 @@
+class AddIndexToCiRunnersForIsShared < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :ci_runners, :is_shared
+ end
+
+ def down
+ if index_exists?(:ci_runners, :is_shared)
+ remove_index :ci_runners, :is_shared
+ end
+ end
+end
diff --git a/db/migrate/20170124174637_add_foreign_keys_to_timelogs.rb b/db/migrate/20170124174637_add_foreign_keys_to_timelogs.rb
new file mode 100644
index 00000000000..69bfa2d3fc4
--- /dev/null
+++ b/db/migrate/20170124174637_add_foreign_keys_to_timelogs.rb
@@ -0,0 +1,54 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddForeignKeysToTimelogs < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+ # When a migration requires downtime you **must** uncomment the following
+ # constant and define a short and easy to understand explanation as to why the
+ # migration requires downtime.
+ DOWNTIME_REASON = ''
+
+ # When using the methods "add_concurrent_index" or "add_column_with_default"
+ # you must disable the use of transactions as these methods can not run in an
+ # existing transaction. When using "add_concurrent_index" make sure that this
+ # method is the _only_ method called in the migration, any other changes
+ # should go in a separate migration. This ensures that upon failure _only_ the
+ # index creation fails and can be retried or reverted easily.
+ #
+ # To disable transactions uncomment the following line and remove these
+ # comments:
+ disable_ddl_transaction!
+
+ def up
+ change_table :timelogs do |t|
+ t.column :issue_id, :integer
+ t.column :merge_request_id, :integer
+ end
+
+ add_concurrent_index :timelogs, :issue_id
+ add_concurrent_index :timelogs, :merge_request_id
+
+ if Gitlab::Database.postgresql?
+ execute <<-EOF
+ ALTER TABLE timelogs ADD CONSTRAINT "fk_timelogs_issues_issue_id" FOREIGN KEY (issue_id) REFERENCES "issues" (id) ON DELETE CASCADE NOT VALID;
+ ALTER TABLE timelogs ADD CONSTRAINT "fk_timelogs_merge_requests_merge_request_id" FOREIGN KEY (merge_request_id) REFERENCES "merge_requests" (id) ON DELETE CASCADE NOT VALID;
+ EOF
+ else
+ execute "ALTER TABLE timelogs ADD CONSTRAINT fk_timelogs_issues_issue_id FOREIGN KEY (issue_id) REFERENCES issues(id) ON DELETE CASCADE;"
+ execute "ALTER TABLE timelogs ADD CONSTRAINT fk_timelogs_merge_requests_merge_request_id FOREIGN KEY (merge_request_id) REFERENCES merge_requests(id) ON DELETE CASCADE;"
+ end
+
+ Timelog.where(trackable_type: 'Issue').update_all("issue_id = trackable_id")
+ Timelog.where(trackable_type: 'MergeRequest').update_all("merge_request_id = trackable_id")
+ end
+
+ def down
+ Timelog.where('issue_id IS NOT NULL').update_all("trackable_id = issue_id, trackable_type = 'Issue'")
+ Timelog.where('merge_request_id IS NOT NULL').update_all("trackable_id = merge_request_id, trackable_type = 'MergeRequest'")
+
+ remove_columns :timelogs, :issue_id, :merge_request_id
+ end
+end
diff --git a/db/migrate/20170126174819_add_terminal_max_session_time_to_application_settings.rb b/db/migrate/20170126174819_add_terminal_max_session_time_to_application_settings.rb
new file mode 100644
index 00000000000..334f53f9145
--- /dev/null
+++ b/db/migrate/20170126174819_add_terminal_max_session_time_to_application_settings.rb
@@ -0,0 +1,33 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddTerminalMaxSessionTimeToApplicationSettings < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ # When a migration requires downtime you **must** uncomment the following
+ # constant and define a short and easy to understand explanation as to why the
+ # migration requires downtime.
+ # DOWNTIME_REASON = ''
+
+ # When using the methods "add_concurrent_index" or "add_column_with_default"
+ # you must disable the use of transactions as these methods can not run in an
+ # existing transaction. When using "add_concurrent_index" make sure that this
+ # method is the _only_ method called in the migration, any other changes
+ # should go in a separate migration. This ensures that upon failure _only_ the
+ # index creation fails and can be retried or reverted easily.
+ #
+ # To disable transactions uncomment the following line and remove these
+ # comments:
+ disable_ddl_transaction!
+
+ def up
+ add_column_with_default :application_settings, :terminal_max_session_time, :integer, default: 0, allow_null: false
+ end
+
+ def down
+ remove_column :application_settings, :terminal_max_session_time
+ end
+end
diff --git a/db/migrate/20170127032550_remove_backlog_lists_from_boards.rb b/db/migrate/20170127032550_remove_backlog_lists_from_boards.rb
new file mode 100644
index 00000000000..0ee4229d1f8
--- /dev/null
+++ b/db/migrate/20170127032550_remove_backlog_lists_from_boards.rb
@@ -0,0 +1,17 @@
+class RemoveBacklogListsFromBoards < ActiveRecord::Migration
+ DOWNTIME = false
+
+ def up
+ execute <<-SQL
+ DELETE FROM lists WHERE list_type = 0;
+ SQL
+ end
+
+ def down
+ execute <<-SQL
+ INSERT INTO lists (board_id, list_type, created_at, updated_at)
+ SELECT boards.id, 0, NOW(), NOW()
+ FROM boards;
+ SQL
+ end
+end
diff --git a/db/migrate/20170130204620_add_index_to_project_authorizations.rb b/db/migrate/20170130204620_add_index_to_project_authorizations.rb
new file mode 100644
index 00000000000..e9a0aee4d6a
--- /dev/null
+++ b/db/migrate/20170130204620_add_index_to_project_authorizations.rb
@@ -0,0 +1,11 @@
+class AddIndexToProjectAuthorizations < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index(:project_authorizations, :project_id)
+ end
+end
diff --git a/db/migrate/20170204181513_add_index_to_labels_for_type_and_project.rb b/db/migrate/20170204181513_add_index_to_labels_for_type_and_project.rb
new file mode 100644
index 00000000000..8f944930807
--- /dev/null
+++ b/db/migrate/20170204181513_add_index_to_labels_for_type_and_project.rb
@@ -0,0 +1,11 @@
+class AddIndexToLabelsForTypeAndProject < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def change
+ add_concurrent_index :labels, [:type, :project_id]
+ end
+end
diff --git a/db/migrate/20170206071414_add_recaptcha_verified_to_spam_logs.rb b/db/migrate/20170206071414_add_recaptcha_verified_to_spam_logs.rb
new file mode 100644
index 00000000000..44372334d21
--- /dev/null
+++ b/db/migrate/20170206071414_add_recaptcha_verified_to_spam_logs.rb
@@ -0,0 +1,15 @@
+class AddRecaptchaVerifiedToSpamLogs < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ disable_ddl_transaction!
+
+ DOWNTIME = false
+
+ def up
+ add_column_with_default(:spam_logs, :recaptcha_verified, :boolean, default: false)
+ end
+
+ def down
+ remove_column(:spam_logs, :recaptcha_verified)
+ end
+end
diff --git a/db/post_migrate/20161221140236_remove_unneeded_services.rb b/db/post_migrate/20161221140236_remove_unneeded_services.rb
new file mode 100644
index 00000000000..6b7e94c8641
--- /dev/null
+++ b/db/post_migrate/20161221140236_remove_unneeded_services.rb
@@ -0,0 +1,15 @@
+class RemoveUnneededServices < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ disable_statement_timeout
+
+ execute("DELETE FROM services WHERE active = false AND properties = '{}';")
+ end
+
+ def down
+ # noop
+ end
+end
diff --git a/db/post_migrate/20161221153951_rename_reserved_project_names.rb b/db/post_migrate/20161221153951_rename_reserved_project_names.rb
new file mode 100644
index 00000000000..282837be1fa
--- /dev/null
+++ b/db/post_migrate/20161221153951_rename_reserved_project_names.rb
@@ -0,0 +1,130 @@
+require 'thread'
+
+class RenameReservedProjectNames < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+ include Gitlab::ShellAdapter
+
+ DOWNTIME = false
+
+ THREAD_COUNT = 8
+
+ KNOWN_PATHS = %w(.well-known
+ all
+ blame
+ blob
+ commits
+ create
+ create_dir
+ edit
+ files
+ find_file
+ groups
+ hooks
+ issues
+ logs_tree
+ merge_requests
+ new
+ preview
+ projects
+ raw
+ repository
+ robots.txt
+ s
+ snippets
+ teams
+ tree
+ u
+ unsubscribes
+ update
+ users
+ wikis)
+
+ def up
+ queues = Array.new(THREAD_COUNT) { Queue.new }
+ start = false
+
+ threads = Array.new(THREAD_COUNT) do |index|
+ Thread.new do
+ queue = queues[index]
+
+ # Wait until we have input to process.
+ until start; end
+
+ rename_projects(queue.pop) until queue.empty?
+ end
+ end
+
+ enum = queues.each
+
+ reserved_projects.each_slice(100) do |slice|
+ begin
+ queue = enum.next
+ rescue StopIteration
+ enum.rewind
+ retry
+ end
+
+ queue << slice
+ end
+
+ start = true
+
+ threads.each(&:join)
+ end
+
+ def down
+ # nothing to do here
+ end
+
+ private
+
+ def reserved_projects
+ Project.unscoped.
+ includes(:namespace).
+ where('EXISTS (SELECT 1 FROM namespaces WHERE projects.namespace_id = namespaces.id)').
+ where('projects.path' => KNOWN_PATHS)
+ end
+
+ def route_exists?(full_path)
+ quoted_path = ActiveRecord::Base.connection.quote_string(full_path)
+
+ ActiveRecord::Base.connection.
+ select_all("SELECT id, path FROM routes WHERE path = '#{quoted_path}'").present?
+ end
+
+ # Adds number to the end of the path that is not taken by other route
+ def rename_path(namespace_path, path_was)
+ counter = 0
+ path = "#{path_was}#{counter}"
+
+ while route_exists?("#{namespace_path}/#{path}")
+ counter += 1
+ path = "#{path_was}#{counter}"
+ end
+
+ path
+ end
+
+ def rename_projects(projects)
+ projects.each do |project|
+ id = project.id
+ path_was = project.path
+ namespace_path = project.namespace.path
+ path = rename_path(namespace_path, path_was)
+
+ begin
+ # Because project path update is quite complex operation we can't safely
+ # copy-paste all code from GitLab. As exception we use Rails code here
+ project.rename_repo if rename_project_row(project, path)
+ rescue Exception => e # rubocop: disable Lint/RescueException
+ Rails.logger.error "Exception when renaming project #{id}: #{e.message}"
+ end
+ end
+ end
+
+ def rename_project_row(project, path)
+ project.respond_to?(:update_attributes) &&
+ project.update_attributes(path: path) &&
+ project.respond_to?(:rename_repo)
+ end
+end
diff --git a/db/post_migrate/20170104150317_requeue_pending_delete_projects.rb b/db/post_migrate/20170104150317_requeue_pending_delete_projects.rb
new file mode 100644
index 00000000000..f399950bd5e
--- /dev/null
+++ b/db/post_migrate/20170104150317_requeue_pending_delete_projects.rb
@@ -0,0 +1,49 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class RequeuePendingDeleteProjects < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ admin = User.find_by(admin: true)
+ return unless admin
+
+ @offset = 0
+
+ loop do
+ ids = pending_delete_batch
+
+ break if ids.rows.count.zero?
+
+ args = ids.map { |id| [id['id'], admin.id, {}] }
+
+ Sidekiq::Client.push_bulk('class' => "ProjectDestroyWorker", 'args' => args)
+
+ @offset += 1
+ end
+ end
+
+ def down
+ # noop
+ end
+
+ private
+
+ def pending_delete_batch
+ connection.exec_query(find_batch)
+ end
+
+ BATCH_SIZE = 5000
+
+ def find_batch
+ projects = Arel::Table.new(:projects)
+ projects.project(projects[:id]).
+ where(projects[:pending_delete].eq(true)).
+ where(projects[:namespace_id].not_eq(nil)).
+ skip(@offset * BATCH_SIZE).
+ take(BATCH_SIZE).
+ to_sql
+ end
+end
diff --git a/db/post_migrate/20170106142508_fill_authorized_projects.rb b/db/post_migrate/20170106142508_fill_authorized_projects.rb
new file mode 100644
index 00000000000..314c8440c8b
--- /dev/null
+++ b/db/post_migrate/20170106142508_fill_authorized_projects.rb
@@ -0,0 +1,30 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class FillAuthorizedProjects < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ class User < ActiveRecord::Base
+ self.table_name = 'users'
+ end
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ # We're not inserting any data so we don't need to start a transaction.
+ disable_ddl_transaction!
+
+ def up
+ relation = User.select(:id).
+ where('authorized_projects_populated IS NOT TRUE')
+
+ relation.find_in_batches(batch_size: 1_000) do |rows|
+ args = rows.map { |row| [row.id] }
+
+ Sidekiq::Client.push_bulk('class' => 'AuthorizedProjectsWorker', 'args' => args)
+ end
+ end
+
+ def down
+ end
+end
diff --git a/db/post_migrate/20170106172224_remove_project_authorizations_id_column.rb b/db/post_migrate/20170106172224_remove_project_authorizations_id_column.rb
new file mode 100644
index 00000000000..7c788160022
--- /dev/null
+++ b/db/post_migrate/20170106172224_remove_project_authorizations_id_column.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 RemoveProjectAuthorizationsIdColumn < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ remove_column :project_authorizations, :id, :primary_key
+ end
+end
diff --git a/db/post_migrate/20170206101007_remove_trackable_columns_from_timelogs.rb b/db/post_migrate/20170206101007_remove_trackable_columns_from_timelogs.rb
new file mode 100644
index 00000000000..89aa753646c
--- /dev/null
+++ b/db/post_migrate/20170206101007_remove_trackable_columns_from_timelogs.rb
@@ -0,0 +1,23 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class RemoveTrackableColumnsFromTimelogs < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ # When using the methods "add_concurrent_index" or "add_column_with_default"
+ # you must disable the use of transactions as these methods can not run in an
+ # existing transaction. When using "add_concurrent_index" make sure that this
+ # method is the _only_ method called in the migration, any other changes
+ # should go in a separate migration. This ensures that upon failure _only_ the
+ # index creation fails and can be retried or reverted easily.
+ #
+ # To disable transactions uncomment the following line and remove these
+ # comments:
+ # disable_ddl_transaction!
+
+ def change
+ remove_columns :timelogs, :trackable_id, :trackable_type
+ end
+end
diff --git a/db/post_migrate/20170206101030_validate_foreign_keys_on_timelogs.rb b/db/post_migrate/20170206101030_validate_foreign_keys_on_timelogs.rb
new file mode 100644
index 00000000000..f397ef919cc
--- /dev/null
+++ b/db/post_migrate/20170206101030_validate_foreign_keys_on_timelogs.rb
@@ -0,0 +1,32 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class ValidateForeignKeysOnTimelogs < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ # When using the methods "add_concurrent_index" or "add_column_with_default"
+ # you must disable the use of transactions as these methods can not run in an
+ # existing transaction. When using "add_concurrent_index" make sure that this
+ # method is the _only_ method called in the migration, any other changes
+ # should go in a separate migration. This ensures that upon failure _only_ the
+ # index creation fails and can be retried or reverted easily.
+ #
+ # To disable transactions uncomment the following line and remove these
+ # comments:
+ disable_ddl_transaction!
+
+ def up
+ if Gitlab::Database.postgresql?
+ execute <<-EOF
+ ALTER TABLE timelogs VALIDATE CONSTRAINT "fk_timelogs_issues_issue_id";
+ ALTER TABLE timelogs VALIDATE CONSTRAINT "fk_timelogs_merge_requests_merge_request_id";
+ EOF
+ end
+ end
+
+ def down
+ # noop
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 14801b581e6..850772ba356 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: 20161213172958) do
+ActiveRecord::Schema.define(version: 20170206101030) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -98,15 +98,19 @@ ActiveRecord::Schema.define(version: 20161213172958) do
t.text "help_page_text_html"
t.text "shared_runners_text_html"
t.text "after_sign_up_text_html"
- t.boolean "sidekiq_throttling_enabled", default: false
- t.string "sidekiq_throttling_queues"
- t.decimal "sidekiq_throttling_factor"
t.boolean "housekeeping_enabled", default: true, null: false
t.boolean "housekeeping_bitmaps_enabled", default: true, null: false
t.integer "housekeeping_incremental_repack_period", default: 10, null: false
t.integer "housekeeping_full_repack_period", default: 50, null: false
t.integer "housekeeping_gc_period", default: 200, null: false
+ t.boolean "sidekiq_throttling_enabled", default: false
+ t.string "sidekiq_throttling_queues"
+ t.decimal "sidekiq_throttling_factor"
t.boolean "html_emails_enabled", default: true
+ t.string "plantuml_url"
+ t.boolean "plantuml_enabled"
+ t.integer "max_pages_size", default: 100, null: false
+ t.integer "terminal_max_session_time", default: 0, null: false
end
create_table "audit_events", force: :cascade do |t|
@@ -213,6 +217,7 @@ ActiveRecord::Schema.define(version: 20161213172958) do
t.datetime "queued_at"
t.string "token"
t.integer "lock_version"
+ t.string "coverage_regex"
end
add_index "ci_builds", ["commit_id", "stage_idx", "created_at"], name: "index_ci_builds_on_commit_id_and_stage_idx_and_created_at", using: :btree
@@ -222,6 +227,7 @@ ActiveRecord::Schema.define(version: 20161213172958) do
add_index "ci_builds", ["gl_project_id"], name: "index_ci_builds_on_gl_project_id", using: :btree
add_index "ci_builds", ["project_id"], name: "index_ci_builds_on_project_id", using: :btree
add_index "ci_builds", ["runner_id"], name: "index_ci_builds_on_runner_id", using: :btree
+ add_index "ci_builds", ["status", "type", "runner_id"], name: "index_ci_builds_on_status_and_type_and_runner_id", using: :btree
add_index "ci_builds", ["status"], name: "index_ci_builds_on_status", using: :btree
add_index "ci_builds", ["token"], name: "index_ci_builds_on_token", unique: true, using: :btree
@@ -325,6 +331,7 @@ ActiveRecord::Schema.define(version: 20161213172958) do
t.boolean "locked", default: false, null: false
end
+ add_index "ci_runners", ["is_shared"], name: "index_ci_runners_on_is_shared", using: :btree
add_index "ci_runners", ["locked"], name: "index_ci_runners_on_locked", using: :btree
add_index "ci_runners", ["token"], name: "index_ci_runners_on_token", using: :btree
@@ -504,6 +511,7 @@ ActiveRecord::Schema.define(version: 20161213172958) do
t.integer "lock_version"
t.text "title_html"
t.text "description_html"
+ t.integer "time_estimate"
end
add_index "issues", ["assignee_id"], name: "index_issues_on_assignee_id", using: :btree
@@ -527,6 +535,8 @@ ActiveRecord::Schema.define(version: 20161213172958) do
t.string "type"
t.string "fingerprint"
t.boolean "public", default: false, null: false
+ t.boolean "can_push", default: false, null: false
+ t.datetime "last_used_at"
end
add_index "keys", ["fingerprint"], name: "index_keys_on_fingerprint", unique: true, using: :btree
@@ -568,6 +578,7 @@ ActiveRecord::Schema.define(version: 20161213172958) do
end
add_index "labels", ["group_id", "project_id", "title"], name: "index_labels_on_group_id_and_project_id_and_title", unique: true, using: :btree
+ add_index "labels", ["type", "project_id"], name: "index_labels_on_type_and_project_id", using: :btree
create_table "lfs_objects", force: :cascade do |t|
t.string "oid", null: false
@@ -681,6 +692,7 @@ ActiveRecord::Schema.define(version: 20161213172958) do
t.integer "lock_version"
t.text "title_html"
t.text "description_html"
+ t.integer "time_estimate"
end
add_index "merge_requests", ["assignee_id"], name: "index_merge_requests_on_assignee_id", using: :btree
@@ -846,6 +858,17 @@ ActiveRecord::Schema.define(version: 20161213172958) do
add_index "oauth_applications", ["owner_id", "owner_type"], name: "index_oauth_applications_on_owner_id_and_owner_type", using: :btree
add_index "oauth_applications", ["uid"], name: "index_oauth_applications_on_uid", unique: true, using: :btree
+ create_table "pages_domains", force: :cascade do |t|
+ t.integer "project_id"
+ t.text "certificate"
+ t.text "encrypted_key"
+ t.string "encrypted_key_iv"
+ t.string "encrypted_key_salt"
+ t.string "domain"
+ end
+
+ add_index "pages_domains", ["domain"], name: "index_pages_domains_on_domain", unique: true, using: :btree
+
create_table "personal_access_tokens", force: :cascade do |t|
t.integer "user_id", null: false
t.string "token", null: false
@@ -854,18 +877,19 @@ ActiveRecord::Schema.define(version: 20161213172958) do
t.datetime "expires_at"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
- t.string "scopes", default: "--- []\n", null: false
+ t.string "scopes", default: "--- []\n", null: false
end
add_index "personal_access_tokens", ["token"], name: "index_personal_access_tokens_on_token", unique: true, using: :btree
add_index "personal_access_tokens", ["user_id"], name: "index_personal_access_tokens_on_user_id", using: :btree
- create_table "project_authorizations", force: :cascade do |t|
+ create_table "project_authorizations", id: false, force: :cascade do |t|
t.integer "user_id"
t.integer "project_id"
t.integer "access_level"
end
+ add_index "project_authorizations", ["project_id"], name: "index_project_authorizations_on_project_id", using: :btree
add_index "project_authorizations", ["user_id", "project_id", "access_level"], name: "index_project_authorizations_on_user_id_project_id_access_level", unique: true, using: :btree
create_table "project_features", force: :cascade do |t|
@@ -901,6 +925,19 @@ ActiveRecord::Schema.define(version: 20161213172958) do
add_index "project_import_data", ["project_id"], name: "index_project_import_data_on_project_id", using: :btree
+ create_table "project_statistics", force: :cascade do |t|
+ t.integer "project_id", null: false
+ t.integer "namespace_id", null: false
+ t.integer "commit_count", limit: 8, default: 0, null: false
+ t.integer "storage_size", limit: 8, default: 0, null: false
+ t.integer "repository_size", limit: 8, default: 0, null: false
+ t.integer "lfs_objects_size", limit: 8, default: 0, null: false
+ t.integer "build_artifacts_size", limit: 8, default: 0, null: false
+ end
+
+ add_index "project_statistics", ["namespace_id"], name: "index_project_statistics_on_namespace_id", using: :btree
+ add_index "project_statistics", ["project_id"], name: "index_project_statistics_on_project_id", unique: true, using: :btree
+
create_table "projects", force: :cascade do |t|
t.string "name"
t.string "path"
@@ -915,11 +952,9 @@ ActiveRecord::Schema.define(version: 20161213172958) do
t.boolean "archived", default: false, null: false
t.string "avatar"
t.string "import_status"
- t.float "repository_size", default: 0.0
t.integer "star_count", default: 0, null: false
t.string "import_type"
t.string "import_source"
- t.integer "commit_count", default: 0
t.text "import_error"
t.integer "ci_id"
t.boolean "shared_runners_enabled", default: true, null: false
@@ -1079,6 +1114,7 @@ ActiveRecord::Schema.define(version: 20161213172958) do
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.boolean "submitted_as_ham", default: false, null: false
+ t.boolean "recaptcha_verified", default: false, null: false
end
create_table "subscriptions", force: :cascade do |t|
@@ -1113,6 +1149,19 @@ ActiveRecord::Schema.define(version: 20161213172958) do
add_index "tags", ["name"], name: "index_tags_on_name", unique: true, using: :btree
+ create_table "timelogs", force: :cascade do |t|
+ t.integer "time_spent", null: false
+ t.integer "user_id"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.integer "issue_id"
+ t.integer "merge_request_id"
+ end
+
+ add_index "timelogs", ["issue_id"], name: "index_timelogs_on_issue_id", using: :btree
+ add_index "timelogs", ["merge_request_id"], name: "index_timelogs_on_merge_request_id", using: :btree
+ add_index "timelogs", ["user_id"], name: "index_timelogs_on_user_id", using: :btree
+
create_table "todos", force: :cascade do |t|
t.integer "user_id", null: false
t.integer "project_id", null: false
@@ -1288,9 +1337,12 @@ ActiveRecord::Schema.define(version: 20161213172958) do
add_foreign_key "personal_access_tokens", "users"
add_foreign_key "project_authorizations", "projects", on_delete: :cascade
add_foreign_key "project_authorizations", "users", on_delete: :cascade
+ add_foreign_key "project_statistics", "projects", on_delete: :cascade
add_foreign_key "protected_branch_merge_access_levels", "protected_branches"
add_foreign_key "protected_branch_push_access_levels", "protected_branches"
add_foreign_key "subscriptions", "projects", on_delete: :cascade
+ add_foreign_key "timelogs", "issues", name: "fk_timelogs_issues_issue_id", on_delete: :cascade
+ add_foreign_key "timelogs", "merge_requests", name: "fk_timelogs_merge_requests_merge_request_id", on_delete: :cascade
add_foreign_key "trending_projects", "projects", on_delete: :cascade
add_foreign_key "u2f_registrations", "users"
end
diff --git a/doc/README.md b/doc/README.md
index ee69684b53b..78030ce4e72 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -1,25 +1,29 @@
# GitLab Community Edition documentation
+## University
+
+[University](university/README.md) contain guides to learn Git and GitLab through courses and videos.
+
## User documentation
-- [Account Security](user/account/security.md) Securing your account via two-factor authentication, etc.
+- [Account Security](user/profile/account/two_factor_authentication.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.
- [Container Registry](user/project/container_registry.md) Learn how to use GitLab Container Registry.
- [GitLab basics](gitlab-basics/README.md) Find step by step how to start working on your commandline and on GitLab.
+- [GitLab Pages](user/project/pages/index.md) Using GitLab Pages.
- [Importing to GitLab](workflow/importing/README.md) Import your projects from GitHub, Bitbucket, GitLab.com, FogBugz and SVN into GitLab.
- [Importing and exporting projects between instances](user/project/settings/import_export.md).
- [Markdown](user/markdown.md) GitLab's advanced formatting system.
- [Migrating from SVN](workflow/importing/migrating_from_svn.md) Convert a SVN repository to Git and GitLab.
- [Permissions](user/permissions.md) Learn what each role in a project (external/guest/reporter/developer/master/owner) can do.
- [Profile Settings](profile/README.md)
-- [Project Services](project_services/project_services.md) Integrate a project with external services, such as CI and chat.
+- [Project Services](user/project/integrations//project_services.md) Integrate a project with external services, such as CI and chat.
- [Public access](public_access/public_access.md) Learn how you can allow public and internal access to projects.
- [SSH](ssh/README.md) Setup your ssh keys and deploy keys for secure access to your projects.
-- [Webhooks](web_hooks/web_hooks.md) Let GitLab notify you when new code has been pushed to your project.
+- [Webhooks](user/project/integrations/webhooks.md) Let GitLab notify you when new code has been pushed to your project.
- [Workflow](workflow/README.md) Using GitLab functionality and importing projects from GitHub and SVN.
-- [University](university/README.md) Learn Git and GitLab through videos and courses.
- [Git Attributes](user/project/git_attributes.md) Managing Git attributes using a `.gitattributes` file.
- [Git cheatsheet](https://gitlab.com/gitlab-com/marketing/raw/master/design/print/git-cheatsheet/print-pdf/git-cheatsheet.pdf) Download a PDF describing the most used Git operations.
@@ -41,7 +45,7 @@
- [Operations](administration/operations.md) Keeping GitLab up and running.
- [Raketasks](raketasks/README.md) Backups, maintenance, automatic webhook setup and the importing of projects.
- [Repository checks](administration/repository_checks.md) Periodic Git repository checks.
-- [Repository storages](administration/repository_storages.md) Manage the paths used to store repositories.
+- [Repository storage paths](administration/repository_storage_paths.md) Manage the paths used to store repositories.
- [Security](security/README.md) Learn what you can do to further secure your GitLab instance.
- [System hooks](system_hooks/system_hooks.md) Notifications when users, projects and keys are changed.
- [Update](update/README.md) Update guides to upgrade your installation.
@@ -50,14 +54,15 @@
- [Migrate GitLab CI to CE/EE](migrate_ci_to_ce/README.md) Follow this guide to migrate your existing GitLab CI data to GitLab CE/EE.
- [Git LFS configuration](workflow/lfs/lfs_administration.md)
- [Housekeeping](administration/housekeeping.md) Keep your Git repository tidy and fast.
-- [GitLab Performance Monitoring](administration/monitoring/performance/introduction.md) Configure GitLab and InfluxDB for measuring performance metrics.
+- [GitLab Pages configuration](administration/pages/index.md) Configure GitLab Pages.
+- [GitLab performance monitoring with InfluxDB](administration/monitoring/performance/introduction.md) Configure GitLab and InfluxDB for measuring performance metrics.
+- [GitLab performance monitoring with Prometheus](administration/monitoring/performance/prometheus.md) Configure GitLab and Prometheus for measuring performance metrics.
- [Request Profiling](administration/monitoring/performance/request_profiling.md) Get a detailed profile on slow requests.
- [Monitoring uptime](user/admin_area/monitoring/health_check.md) Check the server status using the health check endpoint.
- [Debugging Tips](administration/troubleshooting/debug.md) Tips to debug problems when things go wrong
- [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
diff --git a/doc/administration/auth/README.md b/doc/administration/auth/README.md
index 2fc5d0355b5..13bd501e397 100644
--- a/doc/administration/auth/README.md
+++ b/doc/administration/auth/README.md
@@ -6,7 +6,7 @@ providers.
- [LDAP](ldap.md) Includes Active Directory, Apple Open Directory, Open LDAP,
and 389 Server
- [OmniAuth](../../integration/omniauth.md) Sign in via Twitter, GitHub, GitLab.com, Google,
- Bitbucket, Facebook, Shibboleth, Crowd and Azure
+ Bitbucket, Facebook, Shibboleth, Crowd, Azure and Authentiq ID
- [CAS](../../integration/cas.md) Configure GitLab to sign in using CAS
- [SAML](../../integration/saml.md) Configure GitLab as a SAML 2.0 Service Provider
- [Okta](okta.md) Configure GitLab to sign in using Okta
diff --git a/doc/administration/auth/authentiq.md b/doc/administration/auth/authentiq.md
new file mode 100644
index 00000000000..3f39539da95
--- /dev/null
+++ b/doc/administration/auth/authentiq.md
@@ -0,0 +1,69 @@
+# Authentiq OmniAuth Provider
+
+To enable the Authentiq OmniAuth provider for passwordless authentication you must register an application with Authentiq.
+
+Authentiq will generate a Client ID and the accompanying Client Secret for you to use.
+
+1. Get your Client credentials (Client ID and Client Secret) at [Authentiq](https://www.authentiq.com/register).
+
+2. On your GitLab server, open the configuration file:
+
+ For omnibus installation
+ ```sh
+ sudo editor /etc/gitlab/gitlab.rb
+ ```
+
+ For installations from source:
+
+ ```sh
+ sudo -u git -H editor /home/git/gitlab/config/gitlab.yml
+ ```
+
+3. See [Initial OmniAuth Configuration](../../integration/omniauth.md#initial-omniauth-configuration) for initial settings to enable single sign-on and add Authentiq as an OAuth provider.
+
+4. Add the provider configuration for Authentiq:
+
+ For Omnibus packages:
+
+ ```ruby
+ gitlab_rails['omniauth_providers'] = [
+ {
+ "name" => "authentiq",
+ "app_id" => "YOUR_CLIENT_ID",
+ "app_secret" => "YOUR_CLIENT_SECRET",
+ "args" => {
+ scope: 'aq:name email~rs aq:push'
+ }
+ }
+ ]
+ ```
+
+ For installations from source:
+
+ ```yaml
+ - { name: 'authentiq',
+ app_id: 'YOUR_CLIENT_ID',
+ app_secret: 'YOUR_CLIENT_SECRET',
+ args: {
+ scope: 'aq:name email~rs aq:push'
+ }
+ }
+ ```
+
+
+5. The `scope` is set to request the user's name, email (required and signed), and permission to send push notifications to sign in on subsequent visits.
+See [OmniAuth Authentiq strategy](https://github.com/AuthentiqID/omniauth-authentiq#scopes-and-redirect-uri-configuration) for more information on scopes and modifiers.
+
+6. Change 'YOUR_CLIENT_ID' and 'YOUR_CLIENT_SECRET' to the Client credentials you received in step 1.
+
+7. Save the configuration file.
+
+8. [Reconfigure](../restart_gitlab.md#omnibus-gitlab-reconfigure) or [restart GitLab](../restart_gitlab.md#installations-from-source)
+ for the changes to take effect if you installed GitLab via Omnibus or from source respectively.
+
+On the sign in page there should now be an Authentiq icon below the regular sign in form. Click the icon to begin the authentication process.
+
+- If the user has the Authentiq ID app installed in their iOS or Android device, they can scan the QR code, decide what personal details to share and sign in to your GitLab installation.
+- If not they will be prompted to download the app and then follow the procedure above.
+
+If everything goes right, the user will be returned to GitLab and will be signed in. \ No newline at end of file
diff --git a/doc/administration/auth/ldap.md b/doc/administration/auth/ldap.md
index b8b63df091e..f6027b2f99e 100644
--- a/doc/administration/auth/ldap.md
+++ b/doc/administration/auth/ldap.md
@@ -65,11 +65,15 @@ main: # 'main' is the GitLab 'provider ID' of this LDAP server
#
# Example: 'Paris' or 'Acme, Ltd.'
label: 'LDAP'
-
+
+ # Example: 'ldap.mydomain.com'
host: '_your_ldap_server'
+ # This port is an example, it is sometimes different but it is always an integer and not a string
port: 389
uid: 'sAMAccountName'
method: 'plain' # "tls" or "ssl" or "plain"
+
+ # Examples: 'america\\momo' or 'CN=Gitlab Git,CN=Users,DC=mydomain,DC=com'
bind_dn: '_the_full_dn_of_the_user_you_will_bind_with'
password: '_the_password_of_the_bind_user'
@@ -101,7 +105,7 @@ main: # 'main' is the GitLab 'provider ID' of this LDAP server
# Base where we can search for users
#
- # Ex. ou=People,dc=gitlab,dc=example
+ # Ex. 'ou=People,dc=gitlab,dc=example' or 'DC=mydomain,DC=com'
#
base: ''
@@ -112,6 +116,9 @@ main: # 'main' is the GitLab 'provider ID' of this LDAP server
#
# Note: GitLab does not support omniauth-ldap's custom filter syntax.
#
+ # Below an example for get only specific users
+ # Example: '(&(objectclass=user)(|(samaccountname=momo)(samaccountname=toto)))'
+ #
user_filter: ''
# LDAP attributes that GitLab will use to create an account for the LDAP user.
@@ -298,8 +305,11 @@ LDAP server please double-check the LDAP `port` and `method` settings used by
GitLab. Common combinations are `method: 'plain'` and `port: 389`, OR
`method: 'ssl'` and `port: 636`.
-### Login with valid credentials rejected
+### Troubleshooting
+
+If a user account is blocked or unblocked due to the LDAP configuration, a
+message will be logged to `application.log`.
-If there is an unexpected error while authenticating the user with the LDAP
-backend, the login is rejected and details about the error are logged to
+If there is an unexpected error during an LDAP lookup (configuration error,
+timeout), the login is rejected and a message will be logged to
`production.log`.
diff --git a/doc/administration/build_artifacts.md b/doc/administration/build_artifacts.md
index 3ba8387c7f0..cca422892ec 100644
--- a/doc/administration/build_artifacts.md
+++ b/doc/administration/build_artifacts.md
@@ -88,3 +88,9 @@ artifacts through the [Admin area settings](../user/admin_area/settings/continuo
[reconfigure gitlab]: restart_gitlab.md "How to restart GitLab"
[restart gitlab]: restart_gitlab.md "How to restart GitLab"
+
+## Storage statistics
+
+You can see the total storage used for build artifacts on groups and projects
+in the administration area, as well as through the [groups](../api/groups.md)
+and [projects APIs](../api/projects.md).
diff --git a/doc/administration/container_registry.md b/doc/administration/container_registry.md
index d7cfb464f74..a6300e18dc0 100644
--- a/doc/administration/container_registry.md
+++ b/doc/administration/container_registry.md
@@ -379,6 +379,10 @@ Read more about the individual driver's config options in the
filesystem. Remember to enable backups with your object storage provider if
desired.
+> **Important** Enabling storage driver other than `filesystem` would mean
+that your Docker client needs to be able to access the storage backend directly.
+So you must use an address that resolves and is accessible outside GitLab server.
+
---
**Omnibus GitLab installations**
diff --git a/doc/administration/custom_hooks.md b/doc/administration/custom_hooks.md
index 80e5d80aa41..4d35b20d0c3 100644
--- a/doc/administration/custom_hooks.md
+++ b/doc/administration/custom_hooks.md
@@ -3,7 +3,7 @@
>
**Note:** Custom Git hooks must be configured on the filesystem of the GitLab
server. Only GitLab server administrators will be able to complete these tasks.
-Please explore [webhooks](../web_hooks/web_hooks.md) as an option if you do not
+Please explore [webhooks] as an option if you do not
have filesystem access. For a user configurable Git hook interface, please see
[GitLab Enterprise Edition Git Hooks](http://docs.gitlab.com/ee/git_hooks/git_hooks.html).
@@ -80,5 +80,6 @@ STDERR takes precedence over STDOUT.
![Custom message from custom Git hook](img/custom_hooks_error_msg.png)
[hooks]: https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks#Server-Side-Hooks
+[webhooks]: ../user/project/integrations/webhooks.md
[5073]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5073
[93]: https://gitlab.com/gitlab-org/gitlab-shell/merge_requests/93
diff --git a/doc/administration/high_availability/load_balancer.md b/doc/administration/high_availability/load_balancer.md
index 1824829903c..dad8e956c0e 100644
--- a/doc/administration/high_availability/load_balancer.md
+++ b/doc/administration/high_availability/load_balancer.md
@@ -66,4 +66,4 @@ Read more on high-availability configuration:
configure custom domains with custom SSL, which would not be possible
if SSL was terminated at the load balancer.
-[gitlab-pages]: http://docs.gitlab.com/ee/pages/administration.html
+[gitlab-pages]: ../pages/index.md
diff --git a/doc/administration/high_availability/nfs.md b/doc/administration/high_availability/nfs.md
index 5602d70f1ef..3893d837006 100644
--- a/doc/administration/high_availability/nfs.md
+++ b/doc/administration/high_availability/nfs.md
@@ -47,13 +47,13 @@ When using default Omnibus configuration you will need to share 5 data locations
between all GitLab cluster nodes. No other locations should be shared. The
following are the 5 locations you need to mount:
-| Location | Description |
-| -------- | ----------- |
-| `/var/opt/gitlab/git-data` | Git repository data. This will account for a large portion of your data
-| `/var/opt/gitlab/.ssh` | SSH `authorized_keys` file and keys used to import repositories from some other Git services
-| `/var/opt/gitlab/gitlab-rails/uploads` | User uploaded attachments
-| `/var/opt/gitlab/gitlab-rails/shared` | Build artifacts, GitLab Pages, LFS objects, temp files, etc. If you're using LFS this may also account for a large portion of your data
-| `/var/opt/gitlab/gitlab-ci/builds` | GitLab CI build traces
+| Location | Description | Default configuration |
+| -------- | ----------- | --------------------- |
+| `/var/opt/gitlab/git-data` | Git repository data. This will account for a large portion of your data | `git_data_dirs({"default" => "/var/opt/gitlab/git-data"})`
+| `/var/opt/gitlab/.ssh` | SSH `authorized_keys` file and keys used to import repositories from some other Git services | `user['home'] = '/var/opt/gitlab/'`
+| `/var/opt/gitlab/gitlab-rails/uploads` | User uploaded attachments | `gitlab_rails['uploads_directory'] = '/var/opt/gitlab/gitlab-rails/uploads'`
+| `/var/opt/gitlab/gitlab-rails/shared` | Build artifacts, GitLab Pages, LFS objects, temp files, etc. If you're using LFS this may also account for a large portion of your data | `gitlab_rails['shared_path'] = '/var/opt/gitlab/gitlab-rails/shared'`
+| `/var/opt/gitlab/gitlab-ci/builds` | GitLab CI build traces | `gitlab_ci['builds_directory'] = '/var/opt/gitlab/gitlab-ci/builds'`
Other GitLab directories should not be shared between nodes. They contain
node-specific files and GitLab code that does not need to be shared. To ship
@@ -73,10 +73,10 @@ as subdirectories. Mount `/gitlab-data` then use the following Omnibus
configuration to move each data location to a subdirectory:
```ruby
+git_data_dirs({"default" => "/gitlab-data/git-data"})
user['home'] = '/gitlab-data/home'
-git_data_dir '/gitlab-data/git-data'
-gitlab_rails['shared_path'] = '/gitlab-data/shared'
gitlab_rails['uploads_directory'] = '/gitlab-data/uploads'
+gitlab_rails['shared_path'] = '/gitlab-data/shared'
gitlab_ci['builds_directory'] = '/gitlab-data/builds'
```
diff --git a/doc/administration/housekeeping.md b/doc/administration/housekeeping.md
index f846c06ca42..bb621b788f1 100644
--- a/doc/administration/housekeeping.md
+++ b/doc/administration/housekeeping.md
@@ -12,13 +12,24 @@ to turn it off, go to **Admin area > Settings**
## Manual housekeeping
-The housekeeping function runs `git gc` ([man page][man]) on the current
-project Git repository.
+The housekeeping function will run a `repack` or `gc` depending on the
+"Automatic Git repository housekeeping" settings configured in **Admin area > Settings**
-`git gc` runs a number of housekeeping tasks, such as compressing file
-revisions (to reduce disk space and increase performance) and removing
-unreachable objects which may have been created from prior invocations of
-`git add`.
+For example in the following scenario a `git repack -d` will be executed:
+
++ Project: pushes since gc counter (`pushes_since_gc`) = `10`
++ Git GC period = `200`
++ Full repack period = `50`
+
+When the `pushes_since_gc` value is 50 a `repack -A -d --pack-kept-objects` will run, similarly when
+the `pushes_since_gc` value is 200 a `git gc` will be run.
+
++ `git gc` ([man page][man-gc]) runs a number of housekeeping tasks,
+such as compressing filerevisions (to reduce disk space and increase performance)
+and removing unreachable objects which may have been created from prior invocations of
+`git add`.
+
++ `git repack` ([man page][man-repack]) re-organize existing packs into a single, more efficient pack.
You can find this option under your **[Project] > Edit Project**.
@@ -27,4 +38,5 @@ You can find this option under your **[Project] > Edit Project**.
![Housekeeping settings](img/housekeeping_settings.png)
[ce-2371]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2371 "Housekeeping merge request"
-[man]: https://www.kernel.org/pub/software/scm/git/docs/git-gc.html "git gc man page"
+[man-gc]: https://www.kernel.org/pub/software/scm/git/docs/git-gc.html "git gc man page"
+[man-repack]: https://www.kernel.org/pub/software/scm/git/docs/git-repack.html \ No newline at end of file
diff --git a/doc/administration/img/integration/plantuml-example.png b/doc/administration/img/integration/plantuml-example.png
new file mode 100644
index 00000000000..cb64eca1a8a
--- /dev/null
+++ b/doc/administration/img/integration/plantuml-example.png
Binary files differ
diff --git a/doc/administration/integration/plantuml.md b/doc/administration/integration/plantuml.md
new file mode 100644
index 00000000000..6515b1a264a
--- /dev/null
+++ b/doc/administration/integration/plantuml.md
@@ -0,0 +1,95 @@
+# PlantUML & GitLab
+
+> [Introduced][ce-7810] in GitLab 8.16.
+
+When [PlantUML](http://plantuml.com) integration is enabled and configured in
+GitLab we are able to create simple diagrams in AsciiDoc and Markdown documents
+created in snippets, wikis, and repos.
+
+## PlantUML Server
+
+Before you can enable PlantUML in GitLab; you need to set up your own PlantUML
+server that will generate the diagrams. Installing and configuring your
+own PlantUML server is easy in Debian/Ubuntu distributions using Tomcat.
+
+First you need to create a `plantuml.war` file from the source code:
+
+```
+sudo apt-get install graphviz openjdk-7-jdk git-core maven
+git clone https://github.com/plantuml/plantuml-server.git
+cd plantuml-server
+mvn package
+```
+
+The above sequence of commands will generate a WAR file that can be deployed
+using Tomcat:
+
+```
+sudo apt-get install tomcat7
+sudo cp target/plantuml.war /var/lib/tomcat7/webapps/plantuml.war
+sudo chown tomcat7:tomcat7 /var/lib/tomcat7/webapps/plantuml.war
+sudo service restart tomcat7
+```
+
+Once the Tomcat service restarts the PlantUML service will be ready and
+listening for requests on port 8080:
+
+```
+http://localhost:8080/plantuml
+```
+
+you can change these defaults by editing the `/etc/tomcat7/server.xml` file.
+
+
+## GitLab
+
+You need to enable PlantUML integration from Settings under Admin Area. To do
+that, login with an Admin account and do following:
+
+ - in GitLab go to **Admin Area** and then **Settings**
+ - scroll to bottom of the page until PlantUML section
+ - check **Enable PlantUML** checkbox
+ - set the PlantUML instance as **PlantUML URL**
+
+## Creating Diagrams
+
+With PlantUML integration enabled and configured, we can start adding diagrams to
+our AsciiDoc snippets, wikis and repos using delimited blocks:
+
+```
+[plantuml, format="png", id="myDiagram", width="200px"]
+--
+Bob->Alice : hello
+Alice -> Bob : Go Away
+--
+```
+
+And in Markdown using fenced code blocks:
+
+ ```plantuml
+ Bob -> Alice : hello
+ Alice -> Bob : Go Away
+ ```
+
+The above blocks will be converted to an HTML img tag with source pointing to the
+PlantUML instance. If the PlantUML server is correctly configured, this should
+render a nice diagram instead of the block:
+
+![PlantUML Integration](../img/integration/plantuml-example.png)
+
+Inside the block you can add any of the supported diagrams by PlantUML such as
+[Sequence](http://plantuml.com/sequence-diagram), [Use Case](http://plantuml.com/use-case-diagram),
+[Class](http://plantuml.com/class-diagram), [Activity](http://plantuml.com/activity-diagram-legacy),
+[Component](http://plantuml.com/component-diagram), [State](http://plantuml.com/state-diagram),
+and [Object](http://plantuml.com/object-diagram) diagrams. You do not need to use the PlantUML
+diagram delimiters `@startuml`/`@enduml` as these are replaced by the AsciiDoc `plantuml` block.
+
+Some parameters can be added to the AsciiDoc block definition:
+
+ - *format*: Can be either `png` or `svg`. Note that `svg` is not supported by
+ all browsers so use with care. The default is `png`.
+ - *id*: A CSS id added to the diagram HTML tag.
+ - *width*: Width attribute added to the img tag.
+ - *height*: Height attribute added to the img tag.
+
+Markdown does not support any parameters and will always use PNG format.
diff --git a/doc/administration/integration/terminal.md b/doc/administration/integration/terminal.md
index a1d1bb03b50..3b5ee86b68b 100644
--- a/doc/administration/integration/terminal.md
+++ b/doc/administration/integration/terminal.md
@@ -1,13 +1,12 @@
# Web terminals
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7690)
-in GitLab 8.15. Only project masters and owners can access web terminals.
+> [Introduced][ce-7690] in GitLab 8.15. Only project masters and owners can
+ access web terminals.
-With the introduction of the [Kubernetes](../../project_services/kubernetes.md)
-project service, GitLab gained the ability to store and use credentials for a
-Kubernetes cluster. One of the things it uses these credentials for is providing
-access to [web terminals](../../ci/environments.html#web-terminals)
-for environments.
+With the introduction of the [Kubernetes project service][kubservice], GitLab
+gained the ability to store and use credentials for a Kubernetes cluster. One
+of the things it uses these credentials for is providing access to
+[web terminals](../../ci/environments.html#web-terminals) for environments.
## How it works
@@ -71,3 +70,16 @@ by the above guides.
When these headers are not passed through, Workhorse will return a
`400 Bad Request` response to users attempting to use a web terminal. In turn,
they will receive a `Connection failed` message.
+
+## Limiting WebSocket connection time
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8413)
+in GitLab 8.17.
+
+Terminal sessions use long-lived connections; by default, these may last
+forever. You can configure a maximum session time in the Admin area of your
+GitLab instance if you find this undesirable from a scalability or security
+point of view.
+
+[ce-7690]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7690
+[kubservice]: ../../user/project/integrations/kubernetes.md
diff --git a/doc/administration/monitoring/performance/introduction.md b/doc/administration/monitoring/performance/introduction.md
index 79904916b7e..8b106e89cc2 100644
--- a/doc/administration/monitoring/performance/introduction.md
+++ b/doc/administration/monitoring/performance/introduction.md
@@ -12,6 +12,11 @@ documents in order to understand and properly configure GitLab Performance Monit
- [InfluxDB Schema](influxdb_schema.md)
- [Grafana Install/Configuration](grafana_configuration.md)
+>**Note:**
+Omnibus GitLab 8.16 includes Prometheus as an additional tool to collect
+metrics. It will eventually replace InfluxDB when their metrics collection is
+on par. Read more in the [Prometheus documentation](prometheus.md).
+
## Introduction to GitLab Performance Monitoring
GitLab Performance Monitoring makes it possible to measure a wide variety of statistics
diff --git a/doc/administration/monitoring/performance/prometheus.md b/doc/administration/monitoring/performance/prometheus.md
new file mode 100644
index 00000000000..51c63325064
--- /dev/null
+++ b/doc/administration/monitoring/performance/prometheus.md
@@ -0,0 +1,102 @@
+# GitLab Prometheus
+
+>**Notes:**
+- Prometheus and the node exporter are bundled in the Omnibus GitLab package
+ since GitLab 8.16. For installations from source you will have to install
+ them yourself. Over subsequent releases additional GitLab metrics will be
+ captured.
+- Prometheus services are off by default but will be on starting with GitLab 9.0.
+
+[Prometheus] is a powerful time-series monitoring service, providing a flexible
+platform for monitoring GitLab and other software products.
+GitLab provides out of the box monitoring with Prometheus, providing easy
+access to high quality time-series monitoring of GitLab services.
+
+## Overview
+
+Prometheus works by periodically connecting to data sources and collecting their
+performance metrics. To view and work with the monitoring data, you can either
+connect directly to Prometheus or utilize a dashboard tool like [Grafana].
+
+## Configuring Prometheus
+
+>**Note:**
+Available since Omnibus GitLab 8.16. For installations from source you'll
+have to install and configure it yourself.
+
+To enable Prometheus:
+
+1. Edit `/etc/gitlab/gitlab.rb`
+1. Find and uncomment the following line, making sure it's set to `true`:
+
+ ```ruby
+ prometheus['enable'] = true
+ ```
+
+1. Save the file and [reconfigure GitLab][reconfigure] for the changes to
+ take effect
+
+By default, Prometheus will run as the `gitlab-prometheus` user and listen on
+TCP port `9090` under localhost. If the [node exporter](#node-exporter) service
+has been enabled, it will automatically be set up as a monitoring target for
+Prometheus.
+
+## Viewing Performance Metrics
+
+After you have [enabled Prometheus](#configuring-prometheus), you can visit
+`<your_domain_name>:9090` for the dashboard that Prometheus offers by default.
+
+The performance data collected by Prometheus can be viewed directly in the
+Prometheus console or through a compatible dashboard tool.
+The Prometheus interface provides a [flexible query language][prom-query] to work
+with the collected data where you can visualize their output.
+For a more fully featured dashboard, Grafana can be used and has
+[official support for Prometheus][prom-grafana].
+
+## Prometheus exporters
+
+There are a number of libraries and servers which help in exporting existing
+metrics from third-party systems as Prometheus metrics. This is useful for cases
+where it is not feasible to instrument a given system with Prometheus metrics
+directly (for example, HAProxy or Linux system stats). You can read more in the
+[Prometheus exporters and integrations documentation][prom-exporters].
+
+While you can use any exporter you like with your GitLab installation, the
+following ones documented here are bundled in the Omnibus GitLab packages
+making it easy to configure and use.
+
+### Node exporter
+
+>**Note:**
+Available since Omnibus GitLab 8.16. For installations from source you'll
+have to install and configure it yourself.
+
+The [node exporter] allows you to measure various machine resources such as
+memory, disk and CPU utilization.
+
+To enable the node exporter:
+
+1. [Enable Prometheus](#configuring-prometheus)
+1. Edit `/etc/gitlab/gitlab.rb`
+1. Find and uncomment the following line, making sure it's set to `true`:
+
+ ```ruby
+ node_exporter['enable'] = true
+ ```
+
+1. Save the file and [reconfigure GitLab][reconfigure] for the changes to
+ take effect
+
+Prometheus it will now automatically begin collecting performance data from
+the node exporter. You can visit `<your_domain_name>:9100/metrics` for a real
+time representation of the metrics that are collected. Refresh the page and
+you will see the data change.
+
+[grafana]: https://grafana.net
+[node exporter]: https://github.com/prometheus/node_exporter
+[prometheus]: https://prometheus.io
+[prom-query]: https://prometheus.io/docs/querying/basics
+[prom-grafana]: https://prometheus.io/docs/visualization/grafana/
+[scrape-config]: https://prometheus.io/docs/operating/configuration/#%3Cscrape_config%3E
+[prom-exporters]: https://prometheus.io/docs/instrumenting/exporters/
+[reconfigure]: ../../restart_gitlab.md#omnibus-gitlab-reconfigure
diff --git a/doc/administration/pages/index.md b/doc/administration/pages/index.md
new file mode 100644
index 00000000000..c352caf1115
--- /dev/null
+++ b/doc/administration/pages/index.md
@@ -0,0 +1,249 @@
+# GitLab Pages Administration
+
+> **Notes:**
+- [Introduced][ee-80] in GitLab EE 8.3.
+- Custom CNAMEs with TLS support were [introduced][ee-173] in GitLab EE 8.5.
+- GitLab Pages [were ported][ce-14605] to Community Edition in GitLab 8.17.
+- This guide is for Omnibus GitLab installations. If you have installed
+ GitLab from source, follow the [Pages source installation document](source.md).
+
+---
+
+This document describes how to set up the _latest_ GitLab Pages feature. Make
+sure to read the [changelog](#changelog) if you are upgrading to a new GitLab
+version as it may include new features and changes needed to be made in your
+configuration.
+
+If you are looking for ways to upload your static content in GitLab Pages, you
+probably want to read the [user documentation][pages-userguide].
+
+## Overview
+
+GitLab Pages makes use of the [GitLab Pages daemon], a simple HTTP server
+written in Go that can listen on an external IP address and provide support for
+custom domains and custom certificates. It supports dynamic certificates through
+SNI and exposes pages using HTTP2 by default.
+You are encouraged to read its [README][pages-readme] to fully understand how
+it works.
+
+---
+
+In the case of custom domains, the Pages daemon needs to listen on ports `80`
+and/or `443`. For that reason, there is some flexibility in the way which you
+can set it up:
+
+1. Run the pages daemon in the same server as GitLab, listening on a secondary IP
+1. Run the pages daemon in a separate server. In that case, the
+ [Pages path](#change-storage-path) must also be present in the server that
+ the pages daemon is installed, so you will have to share it via network.
+1. Run the pages daemon in the same server as GitLab, listening on the same IP
+ but on different ports. In that case, you will have to proxy the traffic with
+ a loadbalancer. If you choose that route note that you should use TCP load
+ balancing for HTTPS. If you use TLS-termination (HTTPS-load balancing) the
+ pages will not be able to be served with user provided certificates. For
+ HTTP it's OK to use HTTP or TCP load balancing.
+
+In this document, we will proceed assuming the first option.
+
+## Prerequisites
+
+Before proceeding with the Pages configuration, you will need to:
+
+1. Have a separate domain under which the GitLab Pages will be served. In this
+ document we assume that to be `example.io`.
+1. Configure a **wildcard DNS record**.
+1. (Optional) Have a **wildcard certificate** for that domain if you decide to
+ serve Pages under HTTPS.
+1. (Optional but recommended) Enable [Shared runners](../ci/runners/README.md)
+ so that your users don't have to bring their own.
+
+### DNS configuration
+
+GitLab Pages expect to run on their own virtual host. In your DNS server/provider
+you need to add a [wildcard DNS A record][wiki-wildcard-dns] pointing to the
+host that GitLab runs. For example, an entry would look like this:
+
+```
+*.example.io. 1800 IN A 1.2.3.4
+```
+
+where `example.io` is the domain under which GitLab Pages will be served
+and `1.2.3.4` is the IP address of your GitLab instance.
+
+> **Note:**
+You should not use the GitLab domain to serve user pages. For more information
+see the [security section](#security).
+
+[wiki-wildcard-dns]: https://en.wikipedia.org/wiki/Wildcard_DNS_record
+
+## Configuration
+
+Depending on your needs, you can install GitLab Pages in four different ways.
+
+### Option 1. Custom domains with HTTPS support
+
+| URL scheme | Wildcard certificate | Custom domain with HTTP support | Custom domain with HTTPS support | Secondary IP |
+| --- |:---:|:---:|:---:|:---:|:---:|:---:|:---:|
+| `https://page.example.io` and `https://page.com` | yes | redirects to HTTPS | yes | yes |
+
+Pages enabled, daemon is enabled AND pages has external IP support enabled.
+In that case, the pages daemon is running, NGINX still proxies requests to
+the daemon but the daemon is also able to receive requests from the outside
+world. Custom domains and TLS are supported.
+
+1. Edit `/etc/gitlab/gitlab.rb`:
+
+ ```ruby
+ pages_external_url "https://example.io"
+ nginx['listen_addresses'] = ['1.1.1.1']
+ pages_nginx['enable'] = false
+ gitlab_pages['cert'] = "/etc/gitlab/ssl/example.io.crt"
+ gitlab_pages['cert_key'] = "/etc/gitlab/ssl/example.io.key"
+ gitlab_pages['external_http'] = '1.1.1.2:80'
+ gitlab_pages['external_https'] = '1.1.1.2:443'
+ ```
+
+ where `1.1.1.1` is the primary IP address that GitLab is listening to and
+ `1.1.1.2` the secondary IP where the GitLab Pages daemon listens to.
+
+1. [Reconfigure GitLab][reconfigure]
+
+### Option 2. Custom domains without HTTPS support
+
+| URL scheme | Wildcard certificate | Custom domain with HTTP support | Custom domain with HTTPS support | Secondary IP |
+| --- |:---:|:---:|:---:|:---:|:---:|:---:|:---:|
+| `http://page.example.io` and `http://page.com` | no | yes | no | yes |
+
+Pages enabled, daemon is enabled AND pages has external IP support enabled.
+In that case, the pages daemon is running, NGINX still proxies requests to
+the daemon but the daemon is also able to receive requests from the outside
+world. Custom domains and TLS are supported.
+
+1. Edit `/etc/gitlab/gitlab.rb`:
+
+ ```ruby
+ pages_external_url "http://example.io"
+ nginx['listen_addresses'] = ['1.1.1.1']
+ pages_nginx['enable'] = false
+ gitlab_pages['external_http'] = '1.1.1.2:80'
+ ```
+
+ where `1.1.1.1` is the primary IP address that GitLab is listening to and
+ `1.1.1.2` the secondary IP where the GitLab Pages daemon listens to.
+
+1. [Reconfigure GitLab][reconfigure]
+
+### Option 3. Wildcard HTTPS domain without custom domains
+
+| URL scheme | Wildcard certificate | Custom domain with HTTP support | Custom domain with HTTPS support | Secondary IP |
+| --- |:---:|:---:|:---:|:---:|:---:|:---:|:---:|
+| `https://page.example.io` | yes | no | no | no |
+
+Pages enabled, daemon is enabled and NGINX will proxy all requests to the
+daemon. Pages daemon doesn't listen to the outside world.
+
+1. Place the certificate and key inside `/etc/gitlab/ssl`
+1. In `/etc/gitlab/gitlab.rb` specify the following configuration:
+
+ ```ruby
+ pages_external_url 'https://example.io'
+
+ pages_nginx['redirect_http_to_https'] = true
+ pages_nginx['ssl_certificate'] = "/etc/gitlab/ssl/pages-nginx.crt"
+ pages_nginx['ssl_certificate_key'] = "/etc/gitlab/ssl/pages-nginx.key"
+ ```
+
+ where `pages-nginx.crt` and `pages-nginx.key` are the SSL cert and key,
+ respectively.
+
+1. [Reconfigure GitLab][reconfigure]
+
+### Option 4. Wildcard HTTP domain without custom domains
+
+| URL scheme | Wildcard certificate | Custom domain with HTTP support | Custom domain with HTTPS support | Secondary IP |
+| --- |:---:|:---:|:---:|:---:|:---:|:---:|:---:|
+| `http://page.example.io` | no | no | no | no |
+
+Pages enabled, daemon is enabled and NGINX will proxy all requests to the
+daemon. Pages daemon doesn't listen to the outside world.
+
+1. Set the external URL for GitLab Pages in `/etc/gitlab/gitlab.rb`:
+
+ ```ruby
+ pages_external_url 'http://example.io'
+ ```
+
+1. [Reconfigure GitLab][reconfigure]
+
+## Change storage path
+
+Follow the steps below to change the default path where GitLab Pages' contents
+are stored.
+
+1. Pages are stored by default in `/var/opt/gitlab/gitlab-rails/shared/pages`.
+ If you wish to store them in another location you must set it up in
+ `/etc/gitlab/gitlab.rb`:
+
+ ```ruby
+ gitlab_rails['pages_path'] = "/mnt/storage/pages"
+ ```
+
+1. [Reconfigure GitLab][reconfigure]
+
+## Set maximum pages size
+
+The maximum size of the unpacked archive per project can be configured in the
+Admin area under the Application settings in the **Maximum size of pages (MB)**.
+The default is 100MB.
+
+## Backup
+
+Pages are part of the [regular backup][backup] so there is nothing to configure.
+
+## Security
+
+You should strongly consider running GitLab pages under a different hostname
+than GitLab to prevent XSS attacks.
+
+## Changelog
+
+GitLab Pages were first introduced in GitLab EE 8.3. Since then, many features
+where added, like custom CNAME and TLS support, and many more are likely to
+come. Below is a brief changelog. If no changes were introduced or a version is
+missing from the changelog, assume that the documentation is the same as the
+latest previous version.
+
+---
+
+**GitLab 8.17 ([documentation][8-17-docs])**
+
+- GitLab Pages were ported to Community Edition in GitLab 8.17.
+- Documentation was refactored to be more modular and easy to follow.
+
+**GitLab 8.5 ([documentation][8-5-docs])**
+
+- In GitLab 8.5 we introduced the [gitlab-pages][] daemon which is now the
+ recommended way to set up GitLab Pages.
+- The [NGINX configs][] have changed to reflect this change. So make sure to
+ update them.
+- Custom CNAME and TLS certificates support.
+- Documentation was moved to one place.
+
+**GitLab 8.3 ([documentation][8-3-docs])**
+
+- GitLab Pages feature was introduced.
+
+[8-3-docs]: https://gitlab.com/gitlab-org/gitlab-ee/blob/8-3-stable-ee/doc/pages/administration.md
+[8-5-docs]: https://gitlab.com/gitlab-org/gitlab-ee/blob/8-5-stable-ee/doc/pages/administration.md
+[8-17-docs]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-17-stable-ce/doc/administration/pages/index.md
+[backup]: ../raketasks/backup_restore.md
+[ce-14605]: https://gitlab.com/gitlab-org/gitlab-ce/issues/14605
+[ee-80]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/80
+[ee-173]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/173
+[gitlab pages daemon]: https://gitlab.com/gitlab-org/gitlab-pages
+[NGINX configs]: https://gitlab.com/gitlab-org/gitlab-ee/tree/8-5-stable-ee/lib/support/nginx
+[pages-readme]: https://gitlab.com/gitlab-org/gitlab-pages/blob/master/README.md
+[pages-userguide]: ../../user/project/pages/index.md
+[reconfigure]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure
+[restart]: ../administration/restart_gitlab.md#installations-from-source
+[gitlab-pages]: https://gitlab.com/gitlab-org/gitlab-pages/tree/v0.2.4
diff --git a/doc/administration/pages/source.md b/doc/administration/pages/source.md
new file mode 100644
index 00000000000..d4468b99992
--- /dev/null
+++ b/doc/administration/pages/source.md
@@ -0,0 +1,323 @@
+# GitLab Pages administration for source installations
+
+This is the documentation for configuring a GitLab Pages when you have installed
+GitLab from source and not using the Omnibus packages.
+
+You are encouraged to read the [Omnibus documentation](index.md) as it provides
+some invaluable information to the configuration of GitLab Pages. Please proceed
+to read it before going forward with this guide.
+
+We also highly recommend that you use the Omnibus GitLab packages, as we
+optimize them specifically for GitLab, and we will take care of upgrading GitLab
+Pages to the latest supported version.
+
+## Overview
+
+[Read the Omnibus overview section.](index.md#overview)
+
+## Prerequisites
+
+[Read the Omnibus prerequisites section.](index.md#prerequisites)
+
+## Configuration
+
+Depending on your needs, you can install GitLab Pages in four different ways.
+
+### Option 1. Custom domains with HTTPS support
+
+| URL scheme | Wildcard certificate | Custom domain with HTTP support | Custom domain with HTTPS support | Secondary IP |
+| --- |:---:|:---:|:---:|:---:|:---:|:---:|:---:|
+| `https://page.example.io` and `https://page.com` | yes | redirects to HTTPS | yes | yes |
+
+Pages enabled, daemon is enabled AND pages has external IP support enabled.
+In that case, the pages daemon is running, NGINX still proxies requests to
+the daemon but the daemon is also able to receive requests from the outside
+world. Custom domains and TLS are supported.
+
+1. Install the Pages daemon:
+
+ ```
+ cd /home/git
+ sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-pages.git
+ cd gitlab-pages
+ sudo -u git -H git checkout v0.2.4
+ sudo -u git -H make
+ ```
+
+1. Edit `gitlab.yml` to look like the example below. You need to change the
+ `host` to the FQDN under which GitLab Pages will be served. Set
+ `external_http` and `external_https` to the secondary IP on which the pages
+ daemon will listen for connections:
+
+ ```yaml
+ ## GitLab Pages
+ pages:
+ enabled: true
+ # The location where pages are stored (default: shared/pages).
+ # path: shared/pages
+
+ host: example.io
+ port: 443
+ https: true
+
+ external_http: 1.1.1.2:80
+ external_https: 1.1.1.2:443
+ ```
+
+1. Edit `/etc/default/gitlab` and set `gitlab_pages_enabled` to `true` in
+ order to enable the pages daemon. In `gitlab_pages_options` the
+ `-pages-domain`, `-listen-http` and `-listen-https` must match the `host`,
+ `external_http` and `external_https` settings that you set above respectively.
+ The `-root-cert` and `-root-key` settings are the wildcard TLS certificates
+ of the `example.io` domain:
+
+ ```
+ gitlab_pages_enabled=true
+ gitlab_pages_options="-pages-domain example.io -pages-root $app_root/shared/pages -listen-proxy 127.0.0.1:8090 -listen-http 1.1.1.2:80 -listen-https 1.1.1.2:443 -root-cert /path/to/example.io.crt -root-key /path/to/example.io.key
+ ```
+
+1. Copy the `gitlab-pages-ssl` Nginx configuration file:
+
+ ```bash
+ sudo cp lib/support/nginx/gitlab-pages-ssl /etc/nginx/sites-available/gitlab-pages-ssl.conf
+ sudo ln -sf /etc/nginx/sites-{available,enabled}/gitlab-pages-ssl.conf
+ ```
+
+ Replace `gitlab-pages-ssl` with `gitlab-pages` if you are not using SSL.
+
+1. Edit all GitLab related configs in `/etc/nginx/site-available/` and replace
+ `0.0.0.0` with `1.1.1.1`, where `1.1.1.1` the primary IP where GitLab
+ listens to.
+1. Restart NGINX
+1. [Restart GitLab][restart]
+
+### Option 2. Custom domains without HTTPS support
+
+| URL scheme | Wildcard certificate | Custom domain with HTTP support | Custom domain with HTTPS support | Secondary IP |
+| --- |:---:|:---:|:---:|:---:|:---:|:---:|:---:|
+| `http://page.example.io` and `http://page.com` | no | yes | no | yes |
+
+Pages enabled, daemon is enabled AND pages has external IP support enabled.
+In that case, the pages daemon is running, NGINX still proxies requests to
+the daemon but the daemon is also able to receive requests from the outside
+world. Custom domains and TLS are supported.
+
+1. Install the Pages daemon:
+
+ ```
+ cd /home/git
+ sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-pages.git
+ cd gitlab-pages
+ sudo -u git -H git checkout v0.2.4
+ sudo -u git -H make
+ ```
+
+1. Edit `gitlab.yml` to look like the example below. You need to change the
+ `host` to the FQDN under which GitLab Pages will be served. Set
+ `external_http` to the secondary IP on which the pages daemon will listen
+ for connections:
+
+ ```yaml
+ pages:
+ enabled: true
+ # The location where pages are stored (default: shared/pages).
+ # path: shared/pages
+
+ host: example.io
+ port: 80
+ https: false
+
+ external_http: 1.1.1.2:80
+ ```
+
+1. Edit `/etc/default/gitlab` and set `gitlab_pages_enabled` to `true` in
+ order to enable the pages daemon. In `gitlab_pages_options` the
+ `-pages-domain` and `-listen-http` must match the `host` and `external_http`
+ settings that you set above respectively:
+
+ ```
+ gitlab_pages_enabled=true
+ gitlab_pages_options="-pages-domain example.io -pages-root $app_root/shared/pages -listen-proxy 127.0.0.1:8090 -listen-http 1.1.1.2:80"
+ ```
+
+1. Copy the `gitlab-pages-ssl` Nginx configuration file:
+
+ ```bash
+ sudo cp lib/support/nginx/gitlab-pages-ssl /etc/nginx/sites-available/gitlab-pages-ssl.conf
+ sudo ln -sf /etc/nginx/sites-{available,enabled}/gitlab-pages-ssl.conf
+ ```
+
+ Replace `gitlab-pages-ssl` with `gitlab-pages` if you are not using SSL.
+
+1. Edit all GitLab related configs in `/etc/nginx/site-available/` and replace
+ `0.0.0.0` with `1.1.1.1`, where `1.1.1.1` the primary IP where GitLab
+ listens to.
+1. Restart NGINX
+1. [Restart GitLab][restart]
+
+### Option 3. Wildcard HTTPS domain without custom domains
+
+| URL scheme | Wildcard certificate | Custom domain with HTTP support | Custom domain with HTTPS support | Secondary IP |
+| --- |:---:|:---:|:---:|:---:|:---:|:---:|:---:|
+| `https://page.example.io` | yes | no | no | no |
+
+Pages enabled, daemon is enabled and NGINX will proxy all requests to the
+daemon. Pages daemon doesn't listen to the outside world.
+
+1. Install the Pages daemon:
+
+ ```
+ cd /home/git
+ sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-pages.git
+ cd gitlab-pages
+ sudo -u git -H git checkout v0.2.4
+ sudo -u git -H make
+ ```
+1. In `gitlab.yml`, set the port to `443` and https to `true`:
+
+ ```bash
+ ## GitLab Pages
+ pages:
+ enabled: true
+ # The location where pages are stored (default: shared/pages).
+ # path: shared/pages
+
+ host: example.io
+ port: 443
+ https: true
+ ```
+
+1. Copy the `gitlab-pages-ssl` Nginx configuration file:
+
+ ```bash
+ sudo cp lib/support/nginx/gitlab-pages-ssl /etc/nginx/sites-available/gitlab-pages-ssl.conf
+ sudo ln -sf /etc/nginx/sites-{available,enabled}/gitlab-pages-ssl.conf
+ ```
+
+ Replace `gitlab-pages-ssl` with `gitlab-pages` if you are not using SSL.
+
+1. Restart NGINX
+1. [Restart GitLab][restart]
+
+### Option 4. Wildcard HTTP domain without custom domains
+
+| URL scheme | Wildcard certificate | Custom domain with HTTP support | Custom domain with HTTPS support | Secondary IP |
+| --- |:---:|:---:|:---:|:---:|:---:|:---:|:---:|
+| `http://page.example.io` | no | no | no | no |
+
+Pages enabled, daemon is enabled and NGINX will proxy all requests to the
+daemon. Pages daemon doesn't listen to the outside world.
+
+1. Install the Pages daemon:
+
+ ```
+ cd /home/git
+ sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-pages.git
+ cd gitlab-pages
+ sudo -u git -H git checkout v0.2.4
+ sudo -u git -H make
+ ```
+
+1. Go to the GitLab installation directory:
+
+ ```bash
+ cd /home/git/gitlab
+ ```
+
+1. Edit `gitlab.yml` and under the `pages` setting, set `enabled` to `true` and
+ the `host` to the FQDN under which GitLab Pages will be served:
+
+ ```yaml
+ ## GitLab Pages
+ pages:
+ enabled: true
+ # The location where pages are stored (default: shared/pages).
+ # path: shared/pages
+
+ host: example.io
+ port: 80
+ https: false
+ ```
+
+1. Copy the `gitlab-pages-ssl` Nginx configuration file:
+
+ ```bash
+ sudo cp lib/support/nginx/gitlab-pages-ssl /etc/nginx/sites-available/gitlab-pages-ssl.conf
+ sudo ln -sf /etc/nginx/sites-{available,enabled}/gitlab-pages-ssl.conf
+ ```
+
+ Replace `gitlab-pages-ssl` with `gitlab-pages` if you are not using SSL.
+
+1. Restart NGINX
+1. [Restart GitLab][restart]
+
+## NGINX caveats
+
+>**Note:**
+The following information applies only for installations from source.
+
+Be extra careful when setting up the domain name in the NGINX config. You must
+not remove the backslashes.
+
+If your GitLab pages domain is `example.io`, replace:
+
+```bash
+server_name ~^.*\.YOUR_GITLAB_PAGES\.DOMAIN$;
+```
+
+with:
+
+```
+server_name ~^.*\.example\.io$;
+```
+
+If you are using a subdomain, make sure to escape all dots (`.`) except from
+the first one with a backslash (\). For example `pages.example.io` would be:
+
+```
+server_name ~^.*\.pages\.example\.io$;
+```
+
+## Change storage path
+
+Follow the steps below to change the default path where GitLab Pages' contents
+are stored.
+
+1. Pages are stored by default in `/home/git/gitlab/shared/pages`.
+ If you wish to store them in another location you must set it up in
+ `gitlab.yml` under the `pages` section:
+
+ ```yaml
+ pages:
+ enabled: true
+ # The location where pages are stored (default: shared/pages).
+ path: /mnt/storage/pages
+ ```
+
+1. [Restart GitLab][restart]
+
+## Set maximum Pages size
+
+The maximum size of the unpacked archive per project can be configured in the
+Admin area under the Application settings in the **Maximum size of pages (MB)**.
+The default is 100MB.
+
+## Backup
+
+Pages are part of the [regular backup][backup] so there is nothing to configure.
+
+## Security
+
+You should strongly consider running GitLab pages under a different hostname
+than GitLab to prevent XSS attacks.
+
+[backup]: ../raketasks/backup_restore.md
+[ee-80]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/80
+[ee-173]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/173
+[gitlab pages daemon]: https://gitlab.com/gitlab-org/gitlab-pages
+[NGINX configs]: https://gitlab.com/gitlab-org/gitlab-ee/tree/8-5-stable-ee/lib/support/nginx
+[pages-readme]: https://gitlab.com/gitlab-org/gitlab-pages/blob/master/README.md
+[pages-userguide]: ../../user/project/pages/index.md
+[reconfigure]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure
+[restart]: ../administration/restart_gitlab.md#installations-from-source
+[gitlab-pages]: https://gitlab.com/gitlab-org/gitlab-pages/tree/v0.2.4
diff --git a/doc/administration/raketasks/maintenance.md b/doc/administration/raketasks/maintenance.md
index f3c2e72341f..5b6ee354887 100644
--- a/doc/administration/raketasks/maintenance.md
+++ b/doc/administration/raketasks/maintenance.md
@@ -27,6 +27,7 @@ Ruby Version: 2.1.5p273
Gem Version: 2.4.3
Bundler Version: 1.7.6
Rake Version: 10.3.2
+Redis Version: 3.2.5
Sidekiq Version: 2.17.8
GitLab information
@@ -171,14 +172,14 @@ Omnibus packages.
```
cd /home/git/gitlab
-sudo -u git -H bundle exec rake assets:precompile RAILS_ENV=production
+sudo -u git -H bundle exec rake gitlab:assets:compile RAILS_ENV=production
```
For omnibus versions, the unoptimized assets (JavaScript, CSS) are frozen at
the release of upstream GitLab. The omnibus version includes optimized versions
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
+rake gitlab:assets:compile on the production machine. If you suspect that assets
have been corrupted, you should reinstall the omnibus package.
## Tracking Deployments
diff --git a/doc/administration/reply_by_email.md b/doc/administration/reply_by_email.md
index 14cd7a03826..00494e7e9d6 100644
--- a/doc/administration/reply_by_email.md
+++ b/doc/administration/reply_by_email.md
@@ -71,8 +71,8 @@ If you want to use Gmail / Google Apps with Reply by email, make sure you have
[IMAP access enabled](https://support.google.com/mail/troubleshooter/1668960?hl=en#ts=1665018)
and [allowed less secure apps to access the account](https://support.google.com/accounts/answer/6010255).
-To set up a basic Postfix mail server with IMAP access on Ubuntu, follow
-[these instructions](./postfix.md).
+To set up a basic Postfix mail server with IMAP access on Ubuntu, follow the
+[Postfix setup documentation](reply_by_email_postfix_setup.md).
### Omnibus package installations
diff --git a/doc/administration/reply_by_email_postfix_setup.md b/doc/administration/reply_by_email_postfix_setup.md
index 22f10489a6c..3b8c716eff5 100644
--- a/doc/administration/reply_by_email_postfix_setup.md
+++ b/doc/administration/reply_by_email_postfix_setup.md
@@ -315,7 +315,7 @@ Courier, which we will install later to add IMAP authentication, requires mailbo
## Done!
-If all the tests were successful, Postfix is all set up and ready to receive email! Continue with the [Reply by email](./README.md) guide to configure GitLab.
+If all the tests were successful, Postfix is all set up and ready to receive email! Continue with the [Reply by email](./reply_by_email.md) guide to configure GitLab.
---
diff --git a/doc/administration/repository_checks.md b/doc/administration/repository_checks.md
index bc2b1f20ed3..ee37ea49874 100644
--- a/doc/administration/repository_checks.md
+++ b/doc/administration/repository_checks.md
@@ -13,12 +13,12 @@ checks failed you can see their output on the admin log page under
## Periodic checks
-GitLab periodically runs a repository check on all project repositories and
-wiki repositories in order to detect data corruption problems. A
-project will be checked no more than once per week. If any projects
+When enabled, GitLab periodically runs a repository check on all project
+repositories and wiki repositories in order to detect data corruption problems.
+A project will be checked no more than once per month. If any projects
fail their repository checks all GitLab administrators will receive an email
-notification of the situation. This notification is sent out no more
-than once a day.
+notification of the situation. This notification is sent out once a week on
+Sunday, by default.
## Disabling periodic checks
diff --git a/doc/administration/repository_storage_paths.md b/doc/administration/repository_storage_paths.md
new file mode 100644
index 00000000000..d6aa6101026
--- /dev/null
+++ b/doc/administration/repository_storage_paths.md
@@ -0,0 +1,102 @@
+# Repository storage paths
+
+> [Introduced][ce-4578] in GitLab 8.10.
+
+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`.
+- 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. 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.
+
+![Choose repository storage path in Admin area](img/repository_storages_admin_ui.png)
+
+Beginning with GitLab 8.13.4, multiple paths can be chosen. New projects will be
+randomly placed on one of the selected paths.
+
+[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/administration/repository_storages.md b/doc/administration/repository_storages.md
index ab70557b69a..9d41ba77f34 100644
--- a/doc/administration/repository_storages.md
+++ b/doc/administration/repository_storages.md
@@ -1,102 +1,3 @@
# Repository storages
-> [Introduced][ce-4578] in GitLab 8.10.
-
-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`.
-- 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. 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.
-
-![Choose repository storage path in Admin area](img/repository_storages_admin_ui.png)
-
-Beginning with GitLab 8.13.4, multiple paths can be chosen. New projects will be
-randomly placed on one of the selected paths.
-
-[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
+This document was moved to a [new location](repository_storage_paths.md).
diff --git a/doc/api/README.md b/doc/api/README.md
index f65b934b9db..b334ca46caf 100644
--- a/doc/api/README.md
+++ b/doc/api/README.md
@@ -49,6 +49,7 @@ following locations:
- [Todos](todos.md)
- [Users](users.md)
- [Validate CI configuration](ci/lint.md)
+- [V3 to V4](v3_to_v4.md)
- [Version](version.md)
### Internal CI API
@@ -104,6 +105,13 @@ that needs access to the GitLab API.
Once you have your token, pass it to the API using either the `private_token`
parameter or the `PRIVATE-TOKEN` header.
+> [Introduced][ce-5951] in GitLab 8.15.
+
+Personal Access Tokens can be created with one or more scopes that allow various actions
+that a given token can perform. Although there are only two scopes available at the
+moment – `read_user` and `api` – the groundwork has been laid to add more scopes easily.
+
+At any time you can revoke any personal access token by just clicking **Revoke**.
### Session Cookie
@@ -380,3 +388,4 @@ programming languages. Visit the [GitLab website] for a complete list.
[GitLab website]: https://about.gitlab.com/applications/#api-clients "Clients using the GitLab API"
[lib-api-url]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/lib/api/api.rb
[ce-3749]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3749
+[ce-5951]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5951
diff --git a/doc/api/commits.md b/doc/api/commits.md
index 5c11d0f83bb..53ce381c8ae 100644
--- a/doc/api/commits.md
+++ b/doc/api/commits.md
@@ -245,7 +245,7 @@ Example response:
```json
[
{
- "diff": "--- a/doc/update/5.4-to-6.0.md\n+++ b/doc/update/5.4-to-6.0.md\n@@ -71,6 +71,8 @@\n sudo -u git -H bundle exec rake migrate_keys RAILS_ENV=production\n sudo -u git -H bundle exec rake migrate_inline_notes RAILS_ENV=production\n \n+sudo -u git -H bundle exec rake assets:precompile RAILS_ENV=production\n+\n ```\n \n ### 6. Update config files",
+ "diff": "--- a/doc/update/5.4-to-6.0.md\n+++ b/doc/update/5.4-to-6.0.md\n@@ -71,6 +71,8 @@\n sudo -u git -H bundle exec rake migrate_keys RAILS_ENV=production\n sudo -u git -H bundle exec rake migrate_inline_notes RAILS_ENV=production\n \n+sudo -u git -H bundle exec rake gitlab:assets:compile RAILS_ENV=production\n+\n ```\n \n ### 6. Update config files",
"new_path": "doc/update/5.4-to-6.0.md",
"old_path": "doc/update/5.4-to-6.0.md",
"a_mode": null,
diff --git a/doc/api/deploy_keys.md b/doc/api/deploy_keys.md
index 5f248ab6f91..284d5f88c55 100644
--- a/doc/api/deploy_keys.md
+++ b/doc/api/deploy_keys.md
@@ -20,12 +20,14 @@ Example response:
"id": 1,
"title": "Public key",
"key": "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4596k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=",
+ "can_push": false,
"created_at": "2013-10-02T10:12:29Z"
},
{
"id": 3,
"title": "Another Public key",
"key": "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4596k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=",
+ "can_push": true,
"created_at": "2013-10-02T11:12:29Z"
}
]
@@ -55,12 +57,14 @@ Example response:
"id": 1,
"title": "Public key",
"key": "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4596k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=",
+ "can_push": false,
"created_at": "2013-10-02T10:12:29Z"
},
{
"id": 3,
"title": "Another Public key",
"key": "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4596k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=",
+ "can_push": false,
"created_at": "2013-10-02T11:12:29Z"
}
]
@@ -92,6 +96,7 @@ Example response:
"id": 1,
"title": "Public key",
"key": "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4596k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=",
+ "can_push": false,
"created_at": "2013-10-02T10:12:29Z"
}
```
@@ -107,14 +112,15 @@ project only if original one was is accessible by the same user.
POST /projects/:id/deploy_keys
```
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of the project |
-| `title` | string | yes | New deploy key's title |
-| `key` | string | yes | New deploy key |
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of the project |
+| `title` | string | yes | New deploy key's title |
+| `key` | string | yes | New deploy key |
+| `can_push` | boolean | no | Can deploy key push to the project's repository |
```bash
-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/"
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --header "Content-Type: application/json" --data '{"title": "My deploy key", "key": "ssh-rsa AAAA...", "can_push": "true"}' "https://gitlab.example.com/api/v3/projects/5/deploy_keys/"
```
Example response:
@@ -124,6 +130,7 @@ Example response:
"key" : "ssh-rsa AAAA...",
"id" : 12,
"title" : "My deploy key",
+ "can_push": true,
"created_at" : "2015-08-29T12:44:31.550Z"
}
```
diff --git a/doc/api/enviroments.md b/doc/api/enviroments.md
index 1299aca8c45..e0ee20d9610 100644
--- a/doc/api/enviroments.md
+++ b/doc/api/enviroments.md
@@ -78,7 +78,7 @@ PUT /projects/:id/environments/:environments_id
| `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"
+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/environments/1"
```
Example response:
@@ -106,7 +106,7 @@ DELETE /projects/:id/environments/:environment_id
| `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"
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/environments/1"
```
Example response:
diff --git a/doc/api/groups.md b/doc/api/groups.md
index 134d7bda22f..3b38e3e1bee 100644
--- a/doc/api/groups.md
+++ b/doc/api/groups.md
@@ -13,6 +13,7 @@ Parameters:
| `search` | string | no | Return list of authorized groups matching the search criteria |
| `order_by` | string | no | Order groups by `name` or `path`. Default is `name` |
| `sort` | string | no | Order groups in `asc` or `desc` order. Default is `asc` |
+| `statistics` | boolean | no | Include group statistics (admins only) |
```
GET /groups
@@ -24,14 +25,20 @@ GET /groups
"id": 1,
"name": "Foobar Group",
"path": "foo-bar",
- "description": "An interesting group"
+ "description": "An interesting group",
+ "visibility_level": 20,
+ "lfs_enabled": true,
+ "avatar_url": "http://localhost:3000/uploads/group/avatar/1/foo.jpg",
+ "web_url": "http://localhost:3000/groups/foo-bar",
+ "request_access_enabled": false,
+ "full_name": "Foobar Group",
+ "full_path": "foo-bar"
}
]
```
You can search for groups by name or path, see below.
-=======
## List owned groups
Get a list of groups which are owned by the authenticated user.
@@ -40,6 +47,12 @@ Get a list of groups which are owned by the authenticated user.
GET /groups/owned
```
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `statistics` | boolean | no | Include group statistics |
+
## List a group's projects
Get a list of projects in this group.
@@ -143,6 +156,8 @@ Example response:
"avatar_url": null,
"web_url": "https://gitlab.example.com/groups/twitter",
"request_access_enabled": false,
+ "full_name": "Foobar Group",
+ "full_path": "foo-bar",
"projects": [
{
"id": 7,
@@ -329,7 +344,7 @@ POST /groups/:id/projects/:project_id
Parameters:
- `id` (required) - The ID or path of a group
-- `project_id` (required) - The ID of a project
+- `project_id` (required) - The ID or path of a project
## Update group
@@ -366,6 +381,8 @@ Example response:
"avatar_url": null,
"web_url": "http://gitlab.example.com/groups/h5bp",
"request_access_enabled": false,
+ "full_name": "Foobar Group",
+ "full_path": "foo-bar",
"projects": [
{
"id": 9,
diff --git a/doc/api/issues.md b/doc/api/issues.md
index 119125bcd3d..7c0a444d4fa 100644
--- a/doc/api/issues.md
+++ b/doc/api/issues.md
@@ -23,12 +23,15 @@ GET /issues?state=closed
GET /issues?labels=foo
GET /issues?labels=foo,bar
GET /issues?labels=foo,bar&state=opened
+GET /issues?milestone=1.0.0
+GET /issues?milestone=1.0.0&state=opened
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `state` | string | no | Return all issues or just those that are `opened` or `closed`|
| `labels` | string | no | Comma-separated list of label names, issues with any of the labels will be returned |
+| `milestone` | string| no | The milestone title |
| `order_by`| string | no | Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at` |
| `sort` | string | no | Return requests sorted in `asc` or `desc` order. Default is `desc` |
@@ -178,7 +181,6 @@ GET /projects/:id/issues?labels=foo,bar
GET /projects/:id/issues?labels=foo,bar&state=opened
GET /projects/:id/issues?milestone=1.0.0
GET /projects/:id/issues?milestone=1.0.0&state=opened
-GET /projects/:id/issues?iid=42
```
| Attribute | Type | Required | Description |
@@ -709,6 +711,146 @@ Example response:
}
```
+## Set a time estimate for an issue
+
+Sets an estimated time of work for this issue.
+
+```
+POST /projects/:id/issues/:issue_id/time_estimate
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `issue_id` | integer | yes | The ID of a project's issue |
+| `duration` | string | yes | The duration in human format. e.g: 3h30m |
+
+```bash
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/time_estimate?duration=3h30m
+```
+
+Example response:
+
+```json
+{
+ "human_time_estimate": "3h 30m",
+ "human_total_time_spent": null,
+ "time_estimate": 12600,
+ "total_time_spent": 0
+}
+```
+
+## Reset the time estimate for an issue
+
+Resets the estimated time for this issue to 0 seconds.
+
+```
+POST /projects/:id/issues/:issue_id/reset_time_estimate
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `issue_id` | integer | yes | The ID of a project's issue |
+
+```bash
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/reset_time_estimate
+```
+
+Example response:
+
+```json
+{
+ "human_time_estimate": null,
+ "human_total_time_spent": null,
+ "time_estimate": 0,
+ "total_time_spent": 0
+}
+```
+
+## Add spent time for an issue
+
+Adds spent time for this issue
+
+```
+POST /projects/:id/issues/:issue_id/add_spent_time
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `issue_id` | integer | yes | The ID of a project's issue |
+| `duration` | string | yes | The duration in human format. e.g: 3h30m |
+
+```bash
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/add_spent_time?duration=1h
+```
+
+Example response:
+
+```json
+{
+ "human_time_estimate": null,
+ "human_total_time_spent": "1h",
+ "time_estimate": 0,
+ "total_time_spent": 3600
+}
+```
+
+## Reset spent time for an issue
+
+Resets the total spent time for this issue to 0 seconds.
+
+```
+POST /projects/:id/issues/:issue_id/reset_spent_time
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `issue_id` | integer | yes | The ID of a project's issue |
+
+```bash
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/reset_spent_time
+```
+
+Example response:
+
+```json
+{
+ "human_time_estimate": null,
+ "human_total_time_spent": null,
+ "time_estimate": 0,
+ "total_time_spent": 0
+}
+```
+
+## Get time tracking stats
+
+```
+GET /projects/:id/issues/:issue_id/time_stats
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `issue_id` | integer | yes | The ID of a project's issue |
+
+```bash
+curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/time_stats
+```
+
+Example response:
+
+```json
+{
+ "human_time_estimate": "2h",
+ "human_total_time_spent": "1h",
+ "time_estimate": 7200,
+ "total_time_spent": 3600
+}
+```
+
## Comments on issues
Comments are done via the [notes](notes.md) resource.
diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md
index 662cc9da733..1cf7632d60c 100644
--- a/doc/api/merge_requests.md
+++ b/doc/api/merge_requests.md
@@ -10,8 +10,7 @@ The pagination parameters `page` and `per_page` can be used to restrict the list
GET /projects/:id/merge_requests
GET /projects/:id/merge_requests?state=opened
GET /projects/:id/merge_requests?state=all
-GET /projects/:id/merge_requests?iid=42
-GET /projects/:id/merge_requests?iid[]=42&iid[]=43
+GET /projects/:id/merge_requests?iids[]=42&iids[]=43
```
Parameters:
@@ -1018,3 +1017,142 @@ Example response:
}]
}
```
+## Set a time estimate for a merge request
+
+Sets an estimated time of work for this merge request.
+
+```
+POST /projects/:id/merge_requests/:merge_request_id/time_estimate
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `merge_request_id` | integer | yes | The ID of a project's merge request |
+| `duration` | string | yes | The duration in human format. e.g: 3h30m |
+
+```bash
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/93/time_estimate?duration=3h30m
+```
+
+Example response:
+
+```json
+{
+ "human_time_estimate": "3h 30m",
+ "human_total_time_spent": null,
+ "time_estimate": 12600,
+ "total_time_spent": 0
+}
+```
+
+## Reset the time estimate for a merge request
+
+Resets the estimated time for this merge request to 0 seconds.
+
+```
+POST /projects/:id/merge_requests/:merge_request_id/reset_time_estimate
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `merge_request_id` | integer | yes | The ID of a project's merge_request |
+
+```bash
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/93/reset_time_estimate
+```
+
+Example response:
+
+```json
+{
+ "human_time_estimate": null,
+ "human_total_time_spent": null,
+ "time_estimate": 0,
+ "total_time_spent": 0
+}
+```
+
+## Add spent time for a merge request
+
+Adds spent time for this merge request
+
+```
+POST /projects/:id/merge_requests/:merge_request_id/add_spent_time
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `merge_request_id` | integer | yes | The ID of a project's merge request |
+| `duration` | string | yes | The duration in human format. e.g: 3h30m |
+
+```bash
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/93/add_spent_time?duration=1h
+```
+
+Example response:
+
+```json
+{
+ "human_time_estimate": null,
+ "human_total_time_spent": "1h",
+ "time_estimate": 0,
+ "total_time_spent": 3600
+}
+```
+
+## Reset spent time for a merge request
+
+Resets the total spent time for this merge request to 0 seconds.
+
+```
+POST /projects/:id/merge_requests/:merge_request_id/reset_spent_time
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `merge_request_id` | integer | yes | The ID of a project's merge_request |
+
+```bash
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/93/reset_spent_time
+```
+
+Example response:
+
+```json
+{
+ "human_time_estimate": null,
+ "human_total_time_spent": null,
+ "time_estimate": 0,
+ "total_time_spent": 0
+}
+```
+
+## Get time tracking stats
+
+```
+GET /projects/:id/merge_requests/:merge_request_id/time_stats
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `merge_request_id` | integer | yes | The ID of a project's merge request |
+
+```bash
+curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/93/time_stats
+```
+
+Example response:
+
+```json
+{
+ "human_time_estimate": "2h",
+ "human_total_time_spent": "1h",
+ "time_estimate": 7200,
+ "total_time_spent": 3600
+}
+```
diff --git a/doc/api/project_snippets.md b/doc/api/project_snippets.md
index c6685f54a9d..404876f6237 100644
--- a/doc/api/project_snippets.md
+++ b/doc/api/project_snippets.md
@@ -51,7 +51,6 @@ Parameters:
"state": "active",
"created_at": "2012-05-23T08:00:58Z"
},
- "expires_at": null,
"updated_at": "2012-06-28T10:52:04Z",
"created_at": "2012-06-28T10:52:04Z",
"web_url": "http://example.com/example/example/snippets/1"
diff --git a/doc/api/projects.md b/doc/api/projects.md
index edffad555a5..122075bbd11 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -307,6 +307,8 @@ Parameters:
| `order_by` | string | no | Return projects ordered by `id`, `name`, `path`, `created_at`, `updated_at`, or `last_activity_at` fields. Default is `created_at` |
| `sort` | string | no | Return projects sorted in `asc` or `desc` order. Default is `desc` |
| `search` | string | no | Return list of authorized projects matching the search criteria |
+| `simple` | boolean | no | Return only the ID, URL, name, and path of each project |
+| `statistics` | boolean | no | Include project statistics |
### List starred projects
@@ -325,6 +327,7 @@ Parameters:
| `order_by` | string | no | Return projects ordered by `id`, `name`, `path`, `created_at`, `updated_at`, or `last_activity_at` fields. Default is `created_at` |
| `sort` | string | no | Return projects sorted in `asc` or `desc` order. Default is `desc` |
| `search` | string | no | Return list of authorized projects matching the search criteria |
+| `simple` | boolean | no | Return only the ID, URL, name, and path of each project |
### List ALL projects
@@ -343,6 +346,7 @@ Parameters:
| `order_by` | string | no | Return projects ordered by `id`, `name`, `path`, `created_at`, `updated_at`, or `last_activity_at` fields. Default is `created_at` |
| `sort` | string | no | Return projects sorted in `asc` or `desc` order. Default is `desc` |
| `search` | string | no | Return list of authorized projects matching the search criteria |
+| `statistics` | boolean | no | Include project statistics |
### Get single project
diff --git a/doc/api/repository_files.md b/doc/api/repository_files.md
index c6ad78754a1..dbb3c1113e8 100644
--- a/doc/api/repository_files.md
+++ b/doc/api/repository_files.md
@@ -53,7 +53,7 @@ Example response:
```json
{
- "file_name": "app/project.rb",
+ "file_path": "app/project.rb",
"branch_name": "master"
}
```
@@ -82,7 +82,7 @@ Example response:
```json
{
- "file_name": "app/project.rb",
+ "file_path": "app/project.rb",
"branch_name": "master"
}
```
@@ -120,7 +120,7 @@ Example response:
```json
{
- "file_name": "app/project.rb",
+ "file_path": "app/project.rb",
"branch_name": "master"
}
```
diff --git a/doc/api/services.md b/doc/api/services.md
index 1466b8189b0..fba5da6587d 100644
--- a/doc/api/services.md
+++ b/doc/api/services.md
@@ -808,5 +808,5 @@ Get JetBrains TeamCity CI service settings for a project.
GET /projects/:id/services/teamcity
```
-[jira-doc]: ../project_services/jira.md
+[jira-doc]: ../user/project/integrations/jira.md
[old-jira-api]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-13-stable/doc/api/services.md#jira
diff --git a/doc/api/settings.md b/doc/api/settings.md
index 218546aafea..ca6b9347877 100644
--- a/doc/api/settings.md
+++ b/doc/api/settings.md
@@ -44,7 +44,10 @@ Example response:
"repository_storage": "default",
"repository_storages": ["default"],
"koding_enabled": false,
- "koding_url": null
+ "koding_url": null,
+ "plantuml_enabled": false,
+ "plantuml_url": null,
+ "terminal_max_session_time": 0
}
```
@@ -79,6 +82,10 @@ 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. |
| `koding_enabled` | boolean | no | Enable Koding integration. Default is `false`. |
| `koding_url` | string | yes (if `koding_enabled` is `true`) | The Koding instance URL for integration. |
+| `disabled_oauth_sign_in_sources` | Array of strings | no | Disabled OAuth sign-in sources |
+| `plantuml_enabled` | boolean | no | Enable PlantUML integration. Default is `false`. |
+| `plantuml_url` | string | yes (if `plantuml_enabled` is `true`) | The PlantUML instance URL for integration. |
+| `terminal_max_session_time` | integer | no | Maximum time for web terminal websocket connection (in seconds). Set to 0 for unlimited time. |
```bash
curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/application/settings?signup_enabled=false&default_project_visibility=1
@@ -111,6 +118,9 @@ Example response:
"container_registry_token_expire_delay": 5,
"repository_storage": "default",
"koding_enabled": false,
- "koding_url": null
+ "koding_url": null,
+ "plantuml_enabled": false,
+ "plantuml_url": null,
+ "terminal_max_session_time": 0
}
```
diff --git a/doc/api/users.md b/doc/api/users.md
index 28b6c7bd491..fea9bdf9639 100644
--- a/doc/api/users.md
+++ b/doc/api/users.md
@@ -271,6 +271,7 @@ Parameters:
- `can_create_group` (optional) - User can create groups - true or false
- `external` (optional) - Flags the user as external - true or false(default)
+On password update, user will be forced to change it upon next login.
Note, at the moment this method does only return a `404` error,
even in cases where a `409` (Conflict) would be more appropriate,
e.g. when renaming the email address to some existing one.
diff --git a/doc/api/v3_to_v4.md b/doc/api/v3_to_v4.md
new file mode 100644
index 00000000000..707f0437b7e
--- /dev/null
+++ b/doc/api/v3_to_v4.md
@@ -0,0 +1,14 @@
+# V3 to V4 version
+
+Our V4 API version is currently available as *Beta*! It means that V3
+will still be supported and remain unchanged for now, but be aware that the following
+changes are in V4:
+
+### Changes
+
+- Removed `/projects/:search` (use: `/projects?search=x`)
+- `iid` filter has been removed from `projects/:id/issues`
+- `projects/:id/merge_requests?iid[]=x&iid[]=y` array filter has been renamed to `iids`
+- Endpoints under `projects/merge_request/:id` have been removed (use: `projects/merge_requests/:id`)
+- Project snippets do not return deprecated field `expires_at`
+- Endpoints under `projects/:id/keys` have been removed (use `projects/:id/deploy_keys`)
diff --git a/doc/ci/README.md b/doc/ci/README.md
index 73bd2516d46..dd14698e9cd 100644
--- a/doc/ci/README.md
+++ b/doc/ci/README.md
@@ -23,6 +23,7 @@
- [CI/CD pipelines settings](../user/project/pipelines/settings.md)
- [Review Apps](review_apps/index.md)
- [Git submodules](git_submodules.md) Using Git submodules in your CI jobs
+- [Auto deploy](autodeploy/index.md)
## Breaking changes
diff --git a/doc/ci/autodeploy/img/auto_deploy_button.png b/doc/ci/autodeploy/img/auto_deploy_button.png
new file mode 100644
index 00000000000..423e76a6cda
--- /dev/null
+++ b/doc/ci/autodeploy/img/auto_deploy_button.png
Binary files differ
diff --git a/doc/ci/autodeploy/img/auto_deploy_dropdown.png b/doc/ci/autodeploy/img/auto_deploy_dropdown.png
new file mode 100644
index 00000000000..957870ec8c7
--- /dev/null
+++ b/doc/ci/autodeploy/img/auto_deploy_dropdown.png
Binary files differ
diff --git a/doc/ci/autodeploy/index.md b/doc/ci/autodeploy/index.md
new file mode 100644
index 00000000000..4028a5efa9e
--- /dev/null
+++ b/doc/ci/autodeploy/index.md
@@ -0,0 +1,41 @@
+# Auto deploy
+
+> [Introduced][mr-8135] in GitLab 8.15. Currently requires a [Public project][project-settings].
+
+Auto deploy is an easy way to configure GitLab CI for the deployment of your
+application. GitLab Community maintains a list of `.gitlab-ci.yml`
+templates for various infrastructure providers and deployment scripts
+powering them. These scripts are responsible for packaging your application,
+setting up the infrastructure and spinning up necessary services (for
+example a database).
+
+You can use [project services][project-services] to store credentials to
+your infrastructure provider and they will be available during the
+deployment.
+
+## Supported templates
+
+The list of supported auto deploy templates is available [here][auto-deploy-templates].
+
+## Configuration
+
+1. Enable a deployment [project service][project-services] to store your
+credentials. For example, if you want to deploy to OpenShift you have to
+enable [Kubernetes service][kubernetes-service].
+1. Configure GitLab Runner to use Docker or Kubernetes executor with
+[privileged mode enabled][docker-in-docker].
+1. Navigate to the "Project" tab and click "Set up auto deploy" button.
+ ![Auto deploy button](img/auto_deploy_button.png)
+1. Select a template.
+ ![Dropdown with auto deploy templates](img/auto_deploy_dropdown.png)
+1. Commit your changes and create a merge request.
+1. Test your deployment configuration using a [Review App][review-app] that was
+created automatically for you.
+
+[mr-8135]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8135
+[project-settings]: https://docs.gitlab.com/ce/public_access/public_access.html
+[project-services]: ../../user/project/integrations/project_services.md
+[auto-deploy-templates]: https://gitlab.com/gitlab-org/gitlab-ci-yml/tree/master/autodeploy
+[kubernetes-service]: ../../user/project/integrations/kubernetes.md
+[docker-in-docker]: ../docker/using_docker_build.md#use-docker-in-docker-executor
+[review-app]: ../review_apps/index.md
diff --git a/doc/ci/environments.md b/doc/ci/environments.md
index 98cd29c9567..cb62ed723f0 100644
--- a/doc/ci/environments.md
+++ b/doc/ci/environments.md
@@ -1,7 +1,6 @@
# Introduction to environments and deployments
->**Note:**
-Introduced in GitLab 8.9.
+> Introduced in GitLab 8.9.
During the development of software, there can be many stages until it's ready
for public consumption. You sure want to first test your code and then deploy it
@@ -242,7 +241,7 @@ Web terminals were added in GitLab 8.15 and are only available to project
masters and owners.
If you deploy to your environments with the help of a deployment service (e.g.,
-the [Kubernetes](../project_services/kubernetes.md) service), GitLab can open
+the [Kubernetes service][kubernetes-service], GitLab can open
a terminal session to your environment! This is a very powerful feature that
allows you to debug issues without leaving the comfort of your web browser. To
enable it, just follow the instructions given in the service documentation.
@@ -297,7 +296,7 @@ deploy_review:
- echo "Deploy a review app"
environment:
name: review/$CI_BUILD_REF_NAME
- url: https://$CI_BUILD_REF_SLUG.review.example.com
+ url: https://$CI_ENVIRONMENT_SLUG.example.com
only:
- branches
except:
@@ -318,15 +317,15 @@ also contain `/`, or other characters that would be invalid in a domain name or
URL, we use `$CI_ENVIRONMENT_SLUG` in the `environment:url` so that the
environment can get a specific and distinct URL for each branch. In this case,
given a `$CI_BUILD_REF_NAME` of `100-Do-The-Thing`, the URL will be something
-like `https://review-100-do-the-4f99a2.example.com`. Again, the way you set up
+like `https://100-do-the-4f99a2.example.com`. Again, the way you set up
the web server to serve these requests is based on your setup.
You could also use `$CI_BUILD_REF_SLUG` in `environment:url`, e.g.:
-`https://$CI_BUILD_REF_SLUG.review.example.com`. We use `$CI_ENVIRONMENT_SLUG`
+`https://$CI_BUILD_REF_SLUG.example.com`. We use `$CI_ENVIRONMENT_SLUG`
here because it is guaranteed to be unique, but if you're using a workflow like
[GitLab Flow][gitlab-flow], collisions are very unlikely, and you may prefer
environment names to be more closely based on the branch name - the example
-above would give you an URL like `https://100-do-the-thing.review.example.com`
+above would give you an URL like `https://100-do-the-thing.example.com`
Last but not least, we tell the job to run [`only`][only] on branches
[`except`][only] master.
@@ -443,6 +442,57 @@ and/or `production`) you can see this information in the merge request itself.
![Environment URLs in merge request](img/environments_link_url_mr.png)
+### Go directly from source files to public pages on the environment
+
+> Introduced in GitLab 8.17.
+
+To go one step further, we can specify a Route Map to get GitLab to show us "View on [environment URL]" buttons to go directly from a file to that file's representation on the deployed website. It will be exposed in a few places:
+
+| In the diff for a merge request, comparison or commit | In the file view |
+| ------ | ------ |
+| !["View on env" button in merge request diff](img/view_on_env_mr.png) | !["View on env" button in file view](img/view_on_env_blob.png) |
+
+To get this to work, you need to tell GitLab how the paths of files in your repository map to paths of pages on your website, using a Route Map.
+
+A Route Map is a file inside the repository at `.gitlab/route-map.yml`, which contains a YAML array that maps `source` paths (in the repository) to `public` paths (on the website).
+
+This is an example of a route map for [Middleman](https://middlemanapp.com) static websites like [http://about.gitlab.com](https://gitlab.com/gitlab-com/www-gitlab-com):
+
+```yaml
+# Team data
+- source: 'data/team.yml' # data/team.yml
+ public: 'team/' # team/
+
+# Blogposts
+- source: /source\/posts\/([0-9]{4})-([0-9]{2})-([0-9]{2})-(.+?)\..*/ # source/posts/2017-01-30-around-the-world-in-6-releases.html.md.erb
+ public: '\1/\2/\3/\4/' # 2017/01/30/around-the-world-in-6-releases/
+
+# HTML files
+- source: /source\/(.+?\.html).*/ # source/index.html.haml
+ public: '\1' # index.html
+
+# Other files
+- source: /source\/(.*)/ # source/images/blogimages/around-the-world-in-6-releases-cover.png
+ public: '\1' # images/blogimages/around-the-world-in-6-releases-cover.png
+```
+
+Mappings are defined as entries in the root YAML array, and are identified by a `-` prefix. Within an entry, we have a hash map with two keys:
+
+- `source`
+ - a string, starting and ending with `'`, for an exact match
+ - a regular expression, starting and ending with `/`, for a pattern match
+ - The regular expression needs to match the entire source path - `^` and `$` anchors are implied.
+ - Can include capture groups denoted by `()` that can be referred to in the `public` path.
+ - Slashes (`/`) can, but don't have to, be escaped as `\/`.
+ - Literal periods (`.`) should be escaped as `\.`.
+- `public`
+ - a string, starting and ending with `'`.
+ - Can include `\N` expressions to refer to capture groups in the `source` regular expression in order of their occurence, starting with `\1`.
+
+The public path for a source path is determined by finding the first `source` expression that matches it, and returning the corresponding `public` path, replacing the `\N` expressions with the values of the `()` capture groups if appropriate.
+
+In the example above, the fact that mappings are evaluated in order of their definition is used to ensure that `source/index.html.haml` will match `/source\/(.+?\.html).*/` instead of `/source\/(.*)/`, and will result in a public path of `index.html`, instead of `index.html.haml`.
+
---
We now have a full development cycle, where our app is tested, built, deployed
@@ -566,7 +616,7 @@ Below are some links you may find interesting:
[Pipelines]: pipelines.md
[jobs]: yaml/README.md#jobs
[yaml]: yaml/README.md
-[kubernetes-service]: ../project_services/kubernetes.md]
+[kubernetes-service]: ../user/project/integrations/kubernetes.md
[environments]: #environments
[deployments]: #deployments
[permissions]: ../user/permissions.md
diff --git a/doc/ci/examples/README.md b/doc/ci/examples/README.md
index ffc310ec8c7..5377bf9ee80 100644
--- a/doc/ci/examples/README.md
+++ b/doc/ci/examples/README.md
@@ -14,6 +14,12 @@ Apart from those, here is an collection of tutorials and guides on setting up yo
- [Test a Phoenix application](test-phoenix-application.md)
- [Using `dpl` as deployment tool](deployment/README.md)
- [Example project that shows how to use Review Apps](https://gitlab.com/gitlab-examples/review-apps-nginx/)
+- [Run PHP Composer & NPM scripts then deploy them to a staging server](deployment/composer-npm-deploy.md)
+- Help your favorite programming language and GitLab by sending a merge request
+ with a guide for that language.
+
+## Outside the documentation
+
- [Blog post about using GitLab CI for iOS projects](https://about.gitlab.com/2016/03/10/setting-up-gitlab-ci-for-ios-projects/)
- [Repositories with examples for various languages](https://gitlab.com/groups/gitlab-examples)
- [The .gitlab-ci.yml file for GitLab itself](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.gitlab-ci.yml)
diff --git a/doc/ci/examples/deployment/composer-npm-deploy.md b/doc/ci/examples/deployment/composer-npm-deploy.md
new file mode 100644
index 00000000000..5334a73e1f5
--- /dev/null
+++ b/doc/ci/examples/deployment/composer-npm-deploy.md
@@ -0,0 +1,156 @@
+## Running Composer and NPM scripts with deployment via SCP
+
+This guide covers the building dependencies of a PHP project while compiling assets via an NPM script.
+
+While is possible to create your own image with custom PHP and Node JS versions, for brevity, we will use an existing [Docker image](https://hub.docker.com/r/tetraweb/php/) that contains both PHP and NodeJS installed.
+
+
+```yaml
+image: tetraweb/php
+```
+
+The next step is to install zip/unzip packages and make composer available. We will place these in the `before_script` section:
+
+```yaml
+before_script:
+ - apt-get update
+ - apt-get install zip unzip
+ - php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
+ - php composer-setup.php
+ - php -r "unlink('composer-setup.php');"
+```
+
+This will make sure we have all requirements ready. Next, we want to run `composer update` to fetch all PHP dependencies and `npm install` to load node packages, then run the `npm` script. We need to append them into `before_script` section:
+
+```yaml
+before_script:
+ # ...
+ - php composer.phar update
+ - npm install
+ - npm run deploy
+```
+
+In this particular case, the `npm deploy` script is a Gulp script that does the following:
+
+1. Compile CSS & JS
+2. Create sprites
+3. Copy various assets (images, fonts) around
+4. Replace some strings
+
+All these operations will put all files into a `build` folder, which is ready to be deployed to a live server.
+
+### How to transfer files to a live server?
+
+You have multiple options: rsync, scp, sftp and so on. For now, we will use scp.
+
+To make this work, you need to add a GitLab Secret Variable (accessible on _gitlab.example/your-project-name/variables_). That variable will be called `STAGING_PRIVATE_KEY` and it's the **private** ssh key of your server.
+
+#### Security tip
+
+Create a user that has access **only** to the folder that needs to be updated!
+
+After you create that variable, you need to make sure that key will be added to the docker container on run:
+
+```yaml
+before_script:
+ # - ....
+ - 'which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )'
+ - mkdir -p ~/.ssh
+ - eval $(ssh-agent -s)
+ - '[[ -f /.dockerenv ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config'
+```
+
+In order, this means that:
+
+1. We check if the `ssh-agent` is available and we install it if it's not;
+2. We create the `~/.ssh` folder;
+3. We make sure we're running bash;
+4. We disable host checking (we don't ask for user accept when we first connect to a server; and since every build will equal a first connect, we kind of need this)
+
+And this is basically all you need in the `before_script` section.
+
+## How to deploy things?
+
+As we stated above, we need to deploy the `build` folder from the docker image to our server. To do so, we create a new job:
+
+```yaml
+stage_deploy:
+ artifacts:
+ paths:
+ - build/
+ only:
+ - dev
+ script:
+ - ssh-add <(echo "$STAGING_PRIVATE_KEY")
+ - ssh -p22 server_user@server_host "mkdir htdocs/wp-content/themes/_tmp"
+ - scp -P22 -r build/* server_user@server_host:htdocs/wp-content/themes/_tmp
+ - ssh -p22 server_user@server_host "mv htdocs/wp-content/themes/live htdocs/wp-content/themes/_old && mv htdocs/wp-content/themes/_tmp htdocs/wp-content/themes/live"
+ - ssh -p22 server_user@server_host "rm -rf htdocs/wp-content/themes/_old"
+```
+
+### What's going on here?
+
+1. `only:dev` means that this build will run only when something is pushed to the `dev` branch. You can remove this block completely and have everything be ran on every push (but probably this is something you don't want)
+2. `ssh-add ...` we will add that private key you added on the web UI to the docker container
+3. We will connect via `ssh` and create a new `_tmp` folder
+4. We will connect via `scp` and upload the `build` folder (which was generated by a `npm` script) to our previously created `_tmp` folder
+5. We will connect again to `ssh` and move the `live` folder to an `_old` folder, then move `_tmp` to `live`.
+6. We connect to ssh and remove the `_old` folder
+
+What's the deal with the artifacts? We just tell GitLab CI to keep the `build` directory (later on, you can download that as needed).
+
+#### Why we do it this way?
+
+If you're using this only for stage server, you could do this in two steps:
+
+```yaml
+- ssh -p22 server_user@server_host "rm -rf htdocs/wp-content/themes/live/*"
+- scp -P22 -r build/* server_user@server_host:htdocs/wp-content/themes/live
+```
+
+The problem is that there will be a small period of time when you won't have the app on your server.
+
+So we use so many steps because we want to make sure that at any given time we have a functional app in place.
+
+## Where to go next?
+
+Since this was a WordPress project, I gave real life code snippets. Some ideas you can pursuit:
+
+- Having a slightly different script for `master` branch will allow you to deploy to a production server from that branch and to a stage server from any other branches;
+- Instead of pushing it live, you can push it to WordPress official repo (with creating a SVN commit & stuff);
+- You could generate i18n text domains on the fly.
+
+---
+
+Our final `.gitlab-ci.yml` will look like this:
+
+```yaml
+image: tetraweb/php
+
+before_script:
+ - apt-get update
+ - apt-get install zip unzip
+ - php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
+ - php composer-setup.php
+ - php -r "unlink('composer-setup.php');"
+ - php composer.phar update
+ - npm install
+ - npm run deploy
+ - 'which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )'
+ - mkdir -p ~/.ssh
+ - eval $(ssh-agent -s)
+ - '[[ -f /.dockerenv ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config'
+
+stage_deploy:
+ artifacts:
+ paths:
+ - build/
+ only:
+ - dev
+ script:
+ - ssh-add <(echo "$STAGING_PRIVATE_KEY")
+ - ssh -p22 server_user@server_host "mkdir htdocs/wp-content/themes/_tmp"
+ - scp -P22 -r build/* server_user@server_host:htdocs/wp-content/themes/_tmp
+ - ssh -p22 server_user@server_host "mv htdocs/wp-content/themes/live htdocs/wp-content/themes/_old && mv htdocs/wp-content/themes/_tmp htdocs/wp-content/themes/live"
+ - ssh -p22 server_user@server_host "rm -rf htdocs/wp-content/themes/_old"
+``` \ No newline at end of file
diff --git a/doc/ci/examples/php.md b/doc/ci/examples/php.md
index 82ffb841729..5eeec92d976 100644
--- a/doc/ci/examples/php.md
+++ b/doc/ci/examples/php.md
@@ -235,7 +235,11 @@ cache:
before_script:
# Install composer dependencies
-- curl --silent --show-error https://getcomposer.org/installer | php
+- wget https://composer.github.io/installer.sig -O - -q | tr -d '\n' > installer.sig
+- php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
+- php -r "if (hash_file('SHA384', 'composer-setup.php') === file_get_contents('installer.sig')) { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;"
+- php composer-setup.php
+- php -r "unlink('composer-setup.php'); unlink('installer.sig');"
- php composer.phar install
...
diff --git a/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md b/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md
index 08c10d391ea..42f15a27f12 100644
--- a/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md
+++ b/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md
@@ -34,7 +34,7 @@ production:
This project has three jobs:
1. `test` - used to test Rails application,
2. `staging` - used to automatically deploy staging environment every push to `master` branch
-3. `production` - used to automatically deploy production environmnet for every created tag
+3. `production` - used to automatically deploy production environment for every created tag
### Store API keys
You'll need to create two variables in `Project > Variables`:
diff --git a/doc/ci/git_submodules.md b/doc/ci/git_submodules.md
index 1d782200cca..869743ce80a 100644
--- a/doc/ci/git_submodules.md
+++ b/doc/ci/git_submodules.md
@@ -61,7 +61,18 @@ correctly with your CI builds:
1. First, make sure you have used [relative URLs](#configuring-the-gitmodules-file)
for the submodules located in the same GitLab server.
-1. Then, use `git submodule sync/update` in `before_script`:
+1. Next, if you are using `gitlab-ci-multi-runner` v1.10+, you can set the
+ `GIT_SUBMODULE_STRATEGY` variable to either `normal` or `recursive` to tell
+ the runner to fetch your submodules before the build:
+ ```yaml
+ variables:
+ GIT_SUBMODULE_STRATEGY: recursive
+ ```
+ See the [`.gitlab-ci.yml` reference](yaml/README.md#git-submodule-strategy)
+ for more details about `GIT_SUBMODULE_STRATEGY`.
+
+1. If you are using an older version of `gitlab-ci-multi-runner`, then use
+ `git submodule sync/update` in `before_script`:
```yaml
before_script:
diff --git a/doc/ci/img/view_on_env_blob.png b/doc/ci/img/view_on_env_blob.png
new file mode 100644
index 00000000000..f4fe99046f0
--- /dev/null
+++ b/doc/ci/img/view_on_env_blob.png
Binary files differ
diff --git a/doc/ci/img/view_on_env_mr.png b/doc/ci/img/view_on_env_mr.png
new file mode 100644
index 00000000000..47ddb40bdc1
--- /dev/null
+++ b/doc/ci/img/view_on_env_mr.png
Binary files differ
diff --git a/doc/ci/pipelines.md b/doc/ci/pipelines.md
index f91b9d350f7..2c7c3ef3c18 100644
--- a/doc/ci/pipelines.md
+++ b/doc/ci/pipelines.md
@@ -75,5 +75,5 @@ respective link in the [Pipelines settings] page.
[builds]: #builds
[jobs]: yaml/README.md#jobs
[stages]: yaml/README.md#stages
-[runners]: runners/READM
+[runners]: runners/README.html
[pipelines settings]: ../user/project/pipelines/settings.md
diff --git a/doc/ci/quick_start/README.md b/doc/ci/quick_start/README.md
index c40cdd55ea5..1104edaabe9 100644
--- a/doc/ci/quick_start/README.md
+++ b/doc/ci/quick_start/README.md
@@ -217,7 +217,7 @@ builds, you should explicitly enable the **Builds Emails** service under your
project's settings.
For more information read the
-[Builds emails service documentation](../../project_services/builds_emails.md).
+[Builds emails service documentation](../../user/project/integrations/builds_emails.md).
## Examples
diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md
index d3b9611b02e..49fca884f35 100644
--- a/doc/ci/variables/README.md
+++ b/doc/ci/variables/README.md
@@ -157,14 +157,14 @@ Once you set them, they will be available for all subsequent builds.
>**Note:**
This feature requires GitLab CI 8.15 or higher.
-[Project services](../../project_services/project_services.md) that are
+[Project services](../../user/project/integrations/project_services.md) that are
responsible for deployment configuration may define their own variables that
are set in the build environment. These variables are only defined for
[deployment builds](../environments.md). Please consult the documentation of
the project services that you are using to learn which variables they define.
An example project service that defines deployment variables is
-[Kubernetes Service](../../project_services/kubernetes.md).
+[Kubernetes Service](../../user/project/integrations/kubernetes.md).
## Debug tracing
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index 7158b2e7895..cd492d16747 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -86,7 +86,7 @@ used for time of the build. The configuration of this feature is covered in
### before_script
`before_script` is used to define the command that should be run before all
-builds, including deploy builds. This can be an array or a multi-line string.
+builds, including deploy builds, but after the restoration of artifacts. This can be an array or a multi-line string.
### after_script
@@ -319,6 +319,7 @@ job_name:
| before_script | no | Override a set of commands that are executed before build |
| after_script | no | Override a set of commands that are executed after build |
| environment | no | Defines a name of environment to which deployment is done by this build |
+| coverage | no | Define code coverage settings for a given job |
### script
@@ -993,6 +994,23 @@ job:
- execute this after my script
```
+### coverage
+
+`coverage` allows you to configure how code coverage will be extracted from the
+job output.
+
+Regular expressions are the only valid kind of value expected here. So, using
+surrounding `/` is mandatory in order to consistently and explicitly represent
+a regular expression string. You must escape special characters if you want to
+match them literally.
+
+A simple example:
+
+```yaml
+job1:
+ coverage: /Code coverage: \d+\.\d+/
+```
+
## Git Strategy
> Introduced in GitLab 8.9 as an experimental feature. May change or be removed
@@ -1034,6 +1052,41 @@ variables:
GIT_STRATEGY: none
```
+## Git Submodule Strategy
+
+> Requires GitLab Runner v1.10+.
+
+The `GIT_SUBMODULE_STRATEGY` variable is used to control if / how Git
+submodules are included when fetching the code before a build. Like
+`GIT_STRATEGY`, it can be set in either the global [`variables`](#variables)
+section or the [`variables`](#job-variables) section for individual jobs.
+
+There are three posible values: `none`, `normal`, and `recursive`:
+
+- `none` means that submodules will not be included when fetching the project
+ code. This is the default, which matches the pre-v1.10 behavior.
+
+- `normal` means that only the top-level submodules will be included. It is
+ equivalent to:
+ ```
+ $ git submodule sync
+ $ git submodule update --init
+ ```
+
+- `recursive` means that all submodules (including submodules of submodules)
+ will be included. It is equivalent to:
+ ```
+ $ git submodule sync --recursive
+ $ git submodule update --init --recursive
+ ```
+
+Note that for this feature to work correctly, the submodules must be configured
+(in `.gitmodules`) with either:
+- the HTTP(S) URL of a publicly-accessible repository, or
+- a relative path to another repository on the same GitLab server. See the
+ [Git submodules](../git_submodules.md) documentation.
+
+
## Build stages attempts
> Introduced in GitLab, it requires GitLab Runner v1.9+.
@@ -1246,6 +1299,35 @@ with an API call.
[Read more in the triggers documentation.](../triggers/README.md)
+### pages
+
+`pages` is a special job that is used to upload static content to GitLab that
+can be used to serve your website. It has a special syntax, so the two
+requirements below must be met:
+
+1. Any static content must be placed under a `public/` directory
+1. `artifacts` with a path to the `public/` directory must be defined
+
+The example below simply moves all files from the root of the project to the
+`public/` directory. The `.public` workaround is so `cp` doesn't also copy
+`public/` to itself in an infinite loop:
+
+```
+pages:
+ stage: deploy
+ script:
+ - mkdir .public
+ - cp -r * .public
+ - mv .public public
+ artifacts:
+ paths:
+ - public
+ only:
+ - master
+```
+
+Read more on [GitLab Pages user documentation](../../pages/README.md).
+
## Validate the .gitlab-ci.yml
Each instance of GitLab CI has an embedded debug tool called Lint.
diff --git a/doc/development/README.md b/doc/development/README.md
index 6f2ca7b8590..265df98fb87 100644
--- a/doc/development/README.md
+++ b/doc/development/README.md
@@ -18,6 +18,7 @@
- [Frontend guidelines](frontend.md)
- [SQL guidelines](sql.md) for working with SQL queries
- [Sidekiq guidelines](sidekiq_style_guide.md) for working with Sidekiq workers
+- [`Gemfile` guidelines](gemfile.md)
## Process
diff --git a/doc/development/architecture.md b/doc/development/architecture.md
index 33fd50f4c11..4eb7a8eee48 100644
--- a/doc/development/architecture.md
+++ b/doc/development/architecture.md
@@ -6,7 +6,7 @@ There are two editions of GitLab: [Enterprise Edition](https://about.gitlab.com/
EE releases are available not long after CE releases. To obtain the GitLab EE there is a [repository at gitlab.com](https://gitlab.com/subscribers/gitlab-ee). For more information about the release process see the section 'New versions and upgrading' in the readme.
-Both EE and CE require an add-on component called gitlab-shell. It is obtained from the [gitlab-shell repository](https://gitlab.com/gitlab-org/gitlab-shell/tree/master). New versions are usually tags but staying on the master branch will give you the latest stable version. New releases are generally around the same time as GitLab CE releases with exception for informal security updates deemed critical.
+Both EE and CE require some add-on components called gitlab-shell and Gitaly. These components are available from the [gitlab-shell](https://gitlab.com/gitlab-org/gitlab-shell/tree/master) and [gitaly](https://gitlab.com/gitlab-org/gitaly/tree/master) repositories respectively. New versions are usually tags but staying on the master branch will give you the latest stable version. New releases are generally around the same time as GitLab CE releases with exception for informal security updates deemed critical.
## Physical office analogy
@@ -35,8 +35,10 @@ Their job description:
- make tasks for Sidekiq;
- fetch stuff from the warehouse or move things around in there;
-**Gitlab-shell** is a third kind of worker that takes orders from a fax machine (SSH) instead of the front desk (HTTP).
-Gitlab-shell communicates with Sidekiq via the “communication board” (Redis), and asks quick questions of the Unicorn workers either directly or via the front desk.
+**GitLab-shell** is a third kind of worker that takes orders from a fax machine (SSH) instead of the front desk (HTTP).
+GitLab-shell communicates with Sidekiq via the “communication board” (Redis), and asks quick questions of the Unicorn workers either directly or via the front desk.
+
+**Gitaly** is a back desk that is specialized on reaching the disks to perform git operations efficiently and keep a copy of the result of costly operations. All git operations go through Gitaly.
**GitLab Enterprise Edition (the application)** is the collection of processes and business practices that the office is run by.
@@ -53,7 +55,7 @@ To serve repositories over SSH there's an add-on application called gitlab-shell
### Components
![GitLab Diagram Overview](gitlab_architecture_diagram.png)
-
+
_[edit diagram (for GitLab team members only)](https://docs.google.com/drawings/d/1fBzAyklyveF-i-2q-OHUIqDkYfjjxC4mq5shwKSZHLs/edit)_
A typical install of GitLab will be on GNU/Linux. It uses Nginx or Apache as a web front end to proxypass the Unicorn web server. By default, communication between Unicorn and the front end is via a Unix domain socket but forwarding requests via TCP is also supported. The web front end accesses `/home/git/gitlab/public` bypassing the Unicorn server to serve static pages, uploads (e.g. avatar images or attachments), and precompiled assets. GitLab serves web pages and a [GitLab API](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/api) using the Unicorn web server. It uses Sidekiq as a job queue which, in turn, uses redis as a non-persistent database backend for job information, meta data, and incoming jobs.
@@ -62,7 +64,9 @@ The GitLab web app uses MySQL or PostgreSQL for persistent database information
When serving repositories over HTTP/HTTPS GitLab utilizes the GitLab API to resolve authorization and access as well as serving git objects.
-The add-on component gitlab-shell serves repositories over SSH. It manages the SSH keys within `/home/git/.ssh/authorized_keys` which should not be manually edited. gitlab-shell accesses the bare repositories directly to serve git objects and communicates with redis to submit jobs to Sidekiq for GitLab to process. gitlab-shell queries the GitLab API to determine authorization and access.
+The add-on component gitlab-shell serves repositories over SSH. It manages the SSH keys within `/home/git/.ssh/authorized_keys` which should not be manually edited. gitlab-shell accesses the bare repositories through Gitaly to serve git objects and communicates with redis to submit jobs to Sidekiq for GitLab to process. gitlab-shell queries the GitLab API to determine authorization and access.
+
+Gitaly executes git operations from gitlab-shell and Workhorse, and provides an API to the GitLab web app to get attributes from git (e.g. title, branches, tags, other meta data), and to get blobs (e.g. diffs, commits, files)
### Installation Folder Summary
diff --git a/doc/development/changelog.md b/doc/development/changelog.md
index 6a97fae9cac..c71858c6a24 100644
--- a/doc/development/changelog.md
+++ b/doc/development/changelog.md
@@ -40,6 +40,15 @@ Its simplest usage is to provide the value for `title`:
```text
$ bin/changelog 'Hey DZ, I added a feature to GitLab!'
+```
+
+The entry filename is based on the name of the current Git branch. If you run
+the command above on a branch called `feature/hey-dz`, it will generate a
+`changelogs/unreleased/feature-hey-dz.yml` file.
+
+The command will output the path of the generated file and its contents:
+
+```text
create changelogs/unreleased/my-feature.yml
---
title: Hey DZ, I added a feature to GitLab!
@@ -47,10 +56,6 @@ merge_request:
author:
```
-The entry filename is based on the name of the current Git branch. If you run
-the command above on a branch called `feature/hey-dz`, it will generate a
-`changelogs/unreleased/feature-hey-dz.yml` file.
-
### Arguments
| Argument | Shorthand | Purpose |
@@ -139,7 +144,7 @@ Use the **`--git-username`** or **`-u`** argument to automatically fill in the
$ git config user.name
Jane Doe
-$ bin/changelog --u 'Hey DZ, I added a feature to GitLab!'
+$ bin/changelog -u 'Hey DZ, I added a feature to GitLab!'
create changelogs/unreleased/feature-hey-dz.yml
---
title: Hey DZ, I added a feature to GitLab!
diff --git a/doc/development/code_review.md b/doc/development/code_review.md
index 1ef34c79971..e4a0e0b92bc 100644
--- a/doc/development/code_review.md
+++ b/doc/development/code_review.md
@@ -9,7 +9,7 @@ code is effective, understandable, and maintainable.
Any developer can, and is encouraged to, perform code review on merge requests
of colleagues and contributors. However, the final decision to accept a merge
-request is up to one of our merge request "endbosses", denoted on the
+request is up to one the project's maintainers, denoted on the
[team page](https://about.gitlab.com/team).
## Everyone
@@ -81,15 +81,15 @@ balance in how deep the reviewer can interfere with the code created by a
reviewee.
- Learning how to find the right balance takes time; that is why we have
- minibosses that become merge request endbosses after some time spent on
- reviewing merge requests.
+ reviewers that become maintainers after some time spent on reviewing merge
+ requests.
- Finding bugs and improving code style is important, but thinking about good
design is important as well. Building abstractions and good design is what
makes it possible to hide complexity and makes future changes easier.
- Asking the reviewee to change the design sometimes means the complete rewrite
- of the contributed code. It's usually a good idea to ask another merge
- request endboss before doing it, but have the courage to do it when you
- believe it is important.
+ of the contributed code. It's usually a good idea to ask another maintainer or
+ reviewer before doing it, but have the courage to do it when you believe it is
+ important.
- There is a difference in doing things right and doing things right now.
Ideally, we should do the former, but in the real world we need the latter as
well. A good example is a security fix which should be released as soon as
diff --git a/doc/development/frontend.md b/doc/development/frontend.md
index 9e782ab977f..75fdf3d8e63 100644
--- a/doc/development/frontend.md
+++ b/doc/development/frontend.md
@@ -23,6 +23,69 @@ some ideas with React.js as well as Angular.
To get started with Vue, read through [their documentation][vue-docs].
+#### How to build a new feature with Vue.js
+**Components, Stores and Services**
+
+In some features implemented with Vue.js, like the [issue board][issue-boards]
+or [environments table][environments-table]
+you can find a clear separation of concerns:
+
+```
+new_feature
+├── components
+│ └── component.js.es6
+│ └── ...
+├── store
+│ └── new_feature_store.js.es6
+├── service
+│ └── new_feature_service.js.es6
+├── new_feature_bundle.js.es6
+```
+_For consistency purposes, we recommend you to follow the same structure._
+
+Let's look into each of them:
+
+**A `*_bundle.js` file**
+
+This is the index file of your new feature. This is where the root Vue instance
+of the new feature should be.
+
+Don't forget to follow [these steps.][page-specific-javascript]
+
+**A folder for Components**
+
+This folder holds all components that are specific of this new feature.
+If you need to use or create a component that will probably be used somewhere
+else, please refer to `vue_shared/components`.
+
+A good thumb rule to know when you should create a component is to think if
+it will be reusable elsewhere.
+
+For example, tables are used in a quite amount of places across GitLab, a table
+would be a good fit for a component.
+On the other hand, a table cell used only in on table, would not be a good use
+of this pattern.
+
+You can read more about components in Vue.js site, [Component System][component-system]
+
+**A folder for the Store**
+
+The Store is a simple object that allows us to manage the state in a single
+source of truth.
+
+The concept we are trying to follow is better explained by Vue documentation
+itself, please read this guide: [State Management][state-management]
+
+**A folder for the Service**
+
+The Service is used only to communicate with the server.
+It does not store or manipulate any data.
+We use [vue-resource][vue-resource-repo] to
+communicate with the server.
+
+The [issue boards service][issue-boards-service]
+is a good example of this pattern.
+
## Performance
### Resources
@@ -198,8 +261,8 @@ As long as the fixtures don't change, `rake teaspoon:tests` is sufficient
If you need to debug your tests and/or application code while they're
running, navigate to [localhost:3000/teaspoon](http://localhost:3000/teaspoon)
-in your browser, open DevTools, and run tests for individual files by clicking
-on them. This is also much faster than setting up and running tests from the
+in your browser, open DevTools, and run tests for individual files by clicking
+on them. This is also much faster than setting up and running tests from the
command line.
Please note: Not all of the frontend fixtures are generated. Some are still static
@@ -294,19 +357,84 @@ For our currently-supported browsers, see our [requirements][requirements].
[xss]: https://en.wikipedia.org/wiki/Cross-site_scripting
[scss-style-guide]: scss_styleguide.md
[requirements]: ../install/requirements.md#supported-web-browsers
+[issue-boards]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/assets/javascripts/boards
+[environments-table]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/assets/javascripts/environments
+[page_specific_javascript]: https://docs.gitlab.com/ce/development/frontend.html#page-specific-javascript
+[component-system]: https://vuejs.org/v2/guide/#Composing-with-Components
+[state-management]: https://vuejs.org/v2/guide/state-management.html#Simple-State-Management-from-Scratch
+[vue-resource-repo]: https://github.com/pagekit/vue-resource
+[issue-boards-service]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/app/assets/javascripts/boards/services/board_service.js.es6
## Gotchas
-### Phantom.JS (used by Teaspoon & Rspec) chokes, returning vague JavaScript errors
-
-If you see very generic JavaScript errors (e.g. `jQuery is undefined`) being thrown in tests, but
-can't reproduce them manually, you may have included `ES6`-style JavaScript in files that don't
-have the `.js.es6` file extension. Either use ES5-friendly JavaScript or rename the file you're
-working in (`git mv <file.js> <file.js.es6>`).
-
-Similar errors will be thrown if you're using
-any of the [array methods introduced in ES6](http://www.2ality.com/2014/05/es6-array-methods.html)
-whether or not you've updated the file extension.
+### Spec errors due to use of ES6 features in `.js` files
+
+If you see very generic JavaScript errors (e.g. `jQuery is undefined`) being
+thrown in Teaspoon, Spinach, or Rspec tests but can't reproduce them manually,
+you may have included `ES6`-style JavaScript in files that don't have the
+`.js.es6` file extension. Either use ES5-friendly JavaScript or rename the file
+you're working in (`git mv <file.js> <file.js.es6>`).
+
+### Spec errors due to use of unsupported JavaScript
+
+Similar errors will be thrown if you're using JavaScript features not yet
+supported by our test runner's version of webkit, whether or not you've updated
+the file extension. Examples of unsupported JavaScript features are:
+
+- Array.from
+- Array.find
+- Array.first
+- Object.assign
+- Async functions
+- Generators
+- Array destructuring
+- For Of
+- Symbol/Symbol.iterator
+- Spread
+
+Until these are polyfilled or transpiled appropriately, they should not be used.
+Please update this list with additional unsupported features or when any of
+these are made usable.
+
+### Spec errors due to JavaScript not enabled
+
+If, as a result of a change you've made, a feature now depends on JavaScript to
+run correctly, you need to make sure a JavaScript web driver is enabled when
+specs are run. If you don't you'll see vague error messages from the spec
+runner, and an explosion of vague console errors in the HTML snapshot.
+
+To enable a JavaScript driver in an `rspec` test, add `js: true` to the
+individual spec or the context block containing multiple specs that need
+JavaScript enabled:
+
+```ruby
+
+# For one spec
+it 'presents information about abuse report', js: true do
+ # assertions...
+end
+
+describe "Admin::AbuseReports", js: true do
+ it 'presents information about abuse report' do
+ # assertions...
+ end
+ it 'shows buttons for adding to abuse report' do
+ # assertions...
+ end
+end
+```
+In Spinach, the JavaScript driver is enabled differently. In the `*.feature`
+file for the failing spec, add the `@javascript` flag above the Scenario:
+```
+@javascript
+Scenario: Developer can approve merge request
+ Given I am a "Shop" developer
+ And I visit project "Shop" merge requests page
+ And merge request 'Bug NS-04' must be approved
+ And I click link "Bug NS-04"
+ When I click link "Approve"
+ Then I should see approved merge request "Bug NS-04"
+```
diff --git a/doc/development/gemfile.md b/doc/development/gemfile.md
new file mode 100644
index 00000000000..ec9718cea71
--- /dev/null
+++ b/doc/development/gemfile.md
@@ -0,0 +1,14 @@
+# `Gemfile` guidelines
+
+When adding a new entry to `Gemfile` or upgrading an existing dependency pay
+attention to the following rules.
+
+## No gems fetched from git repositories
+
+We do not allow gems that are fetched from git repositories. All gems have
+to be available in the RubyGems index. We want to minimize external build
+dependencies and build times.
+
+## License compliance
+
+Refer to [licensing guidelines](licensing.md) for ensuring license compliance.
diff --git a/doc/development/gitlab_architecture_diagram.png b/doc/development/gitlab_architecture_diagram.png
index cda5ce254ce..378f7384574 100644
--- a/doc/development/gitlab_architecture_diagram.png
+++ b/doc/development/gitlab_architecture_diagram.png
Binary files differ
diff --git a/doc/development/merge_request_performance_guidelines.md b/doc/development/merge_request_performance_guidelines.md
index 0363bf8c1d5..8232a0a113c 100644
--- a/doc/development/merge_request_performance_guidelines.md
+++ b/doc/development/merge_request_performance_guidelines.md
@@ -3,7 +3,7 @@
To ensure a merge request does not negatively impact performance of GitLab
_every_ merge request **must** adhere to the guidelines outlined in this
document. There are no exceptions to this rule unless specifically discussed
-with and agreed upon by merge request endbosses and performance specialists.
+with and agreed upon by backend maintainers and performance specialists.
To measure the impact of a merge request you can use
[Sherlock](profiling.md#sherlock). It's also highly recommended that you read
@@ -40,9 +40,9 @@ section below for more information.
about the impact.
Sometimes it's hard to assess the impact of a merge request. In this case you
-should ask one of the merge request (mini) endbosses to review your changes. You
-can find a list of these endbosses at <https://about.gitlab.com/team/>. An
-endboss in turn can request a performance specialist to review the changes.
+should ask one of the merge request reviewers to review your changes. You can
+find a list of these reviewers at <https://about.gitlab.com/team/>. A reviewer
+in turn can request a performance specialist to review the changes.
## Query Counts
diff --git a/doc/development/performance.md b/doc/development/performance.md
index f936a49a2aa..c1f129e576c 100644
--- a/doc/development/performance.md
+++ b/doc/development/performance.md
@@ -211,6 +211,41 @@ suite first. See the
[StackProf documentation](https://github.com/tmm1/stackprof/blob/master/README.md)
for details.
+## RSpec profiling
+
+GitLab's development environment also includes the
+[rspec_profiling](https://github.com/foraker/rspec_profiling) gem, which is used
+to collect data on spec execution times. This is useful for analyzing the
+performance of the test suite itself, or seeing how the performance of a spec
+may have changed over time.
+
+To activate profiling in your local environment, run the following:
+
+```
+$ export RSPEC_PROFILING=yes
+$ rake rspec_profiling:install
+```
+
+This creates an SQLite3 database in `tmp/rspec_profiling`, into which statistics
+are saved every time you run specs with the `RSPEC_PROFILING` environment
+variable set.
+
+Ad-hoc investigation of the collected results can be performed in an interactive
+shell:
+
+```
+$ rake rspec_profiling:console
+irb(main):001:0> results.count
+=> 231
+irb(main):002:0> results.last.attributes.keys
+=> ["id", "commit", "date", "file", "line_number", "description", "time", "status", "exception", "query_count", "query_time", "request_count", "request_time", "created_at", "updated_at"]
+irb(main):003:0> results.where(status: "passed").average(:time).to_s
+=> "0.211340155844156"
+```
+These results can also be placed into a PostgreSQL database by setting the
+`RSPEC_PROFILING_POSTGRES_URL` variable. This is used to profile the test suite
+when running in the CI environment.
+
## Importance of Changes
When working on performance improvements, it's important to always ask yourself
diff --git a/doc/development/sidekiq_debugging.md b/doc/development/sidekiq_debugging.md
index cea11e5f126..d6d770e27c1 100644
--- a/doc/development/sidekiq_debugging.md
+++ b/doc/development/sidekiq_debugging.md
@@ -3,12 +3,15 @@
## Log arguments to Sidekiq jobs
If you want to see what arguments are being passed to Sidekiq jobs you can set
-the SIDEKIQ_LOG_ARGUMENTS environment variable.
+the `SIDEKIQ_LOG_ARGUMENTS` [environment variable]
+(https://docs.gitlab.com/omnibus/settings/environment-variables.html) to `1` (true).
+
+Example:
```
-SIDEKIQ_LOG_ARGUMENTS=1 bundle exec foreman start
+gitlab_rails['env'] = {"SIDEKIQ_LOG_ARGUMENTS" => "1"}
```
-It is not recommend to enable this setting in production because some Sidekiq
-jobs (such as sending a password reset email) take secret arguments (for
-example the password reset token).
+Please note: It is not recommend to enable this setting in production because some
+Sidekiq jobs (such as sending a password reset email) take secret arguments (for
+example the password reset token). \ No newline at end of file
diff --git a/doc/development/ui_guide.md b/doc/development/ui_guide.md
index 2d1d504202c..df6ac452300 100644
--- a/doc/development/ui_guide.md
+++ b/doc/development/ui_guide.md
@@ -20,8 +20,8 @@ The content section contains a header and the content itself. The header describ
available to the user in this area. Depending on the area (project, group, profile setting) the header name and navigation may change. For example, when the user visits one of the
project pages the header will contain the project's name and navigation for that project. When the user visits a group page it will contain the group's name and navigation related to this group.
-You can see a visual representation of the navigation in GitLab in the GitLab Product Map, which is located in the [Design Repository](gitlab-map-graffle)
-along with [PDF](gitlab-map-pdf) and [PNG](gitlab-map-png) exports.
+You can see a visual representation of the navigation in GitLab in the GitLab Product Map, which is located in the [Design Repository][gitlab-map-graffle]
+along with [PDF][gitlab-map-pdf] and [PNG][gitlab-map-png] exports.
### Adding new tab to header navigation
@@ -104,4 +104,4 @@ Do not use both green and blue button in one form.
[number_with_delimiter]: http://api.rubyonrails.org/classes/ActionView/Helpers/NumberHelper.html#method-i-number_with_delimiter
[gitlab-map-graffle]: https://gitlab.com/gitlab-org/gitlab-design/blob/master/production/resources/gitlab-map.graffle
[gitlab-map-pdf]: https://gitlab.com/gitlab-org/gitlab-design/raw/master/production/resources/gitlab-map.pdf
-[gitlab-map-png]: https://gitlab.com/gitlab-org/gitlab-design/raw/master/production/resources/gitlab-map.png \ No newline at end of file
+[gitlab-map-png]: https://gitlab.com/gitlab-org/gitlab-design/raw/master/production/resources/gitlab-map.png
diff --git a/doc/development/ux_guide/animation.md b/doc/development/ux_guide/animation.md
index daeb15460c2..5dae4bcc905 100644
--- a/doc/development/ux_guide/animation.md
+++ b/doc/development/ux_guide/animation.md
@@ -19,7 +19,7 @@ Easing specifies the rate of change of a parameter over time (see [easings.net](
### Hover
-Interactive elements (links, buttons, etc.) should have a hover state. A subtle animation for this transition adds a polished feel. We should target a `200ms linear` transition for a color hover effect.
+Interactive elements (links, buttons, etc.) should have a hover state. A subtle animation for this transition adds a polished feel. We should target a `100ms - 150ms linear` transition for a color hover effect.
View the [interactive example](http://codepen.io/awhildy/full/GNyEvM/) here.
@@ -39,4 +39,19 @@ When information is updating in place, a quick, subtle animation is needed. The
![Quick update animation](img/animation-quickupdate.gif)
-> TODO: Add guidance for other kinds of animation \ No newline at end of file
+### Moving transitions
+
+When elements move on screen, there should be a quick animation so it is clear to users what moved where. The timing of this animation differs based on the amount of movement and change. Consider animations between `200ms` and `400ms`.
+
+#### Shifting elements on reorder
+An example of a moving transition is when elements have to move out of the way when you drag an element.
+
+View the [interactive example](http://codepen.io/awhildy/full/ALyKPE/) here.
+
+![Reorder animation](img/animation-reorder.gif)
+
+#### Autoscroll the page
+Another example of a moving transition is when you have to autoscroll the page to keep an active element visible.
+
+View the [interactive example](http://codepen.io/awhildy/full/PbxgVo/) here.
+![Autoscroll animation](img/animation-autoscroll.gif) \ No newline at end of file
diff --git a/doc/development/ux_guide/basics.md b/doc/development/ux_guide/basics.md
index e81729556d8..259b214bd59 100644
--- a/doc/development/ux_guide/basics.md
+++ b/doc/development/ux_guide/basics.md
@@ -50,13 +50,20 @@ GitLab uses Font Awesome icons throughout our interface.
## Color
-| | |
-| :------: | :------- |
-| ![Blue](img/color-blue.png) | Blue is used to highlight primary active elements (such as the current tab), as well as other organizational and managing commands.|
-| ![Green](img/color-green.png) | Green is for actions that create new objects. |
-| ![Orange](img/color-orange.png) | Orange is used for warnings |
-| ![Red](img/color-red.png) | Red is reserved for delete and other destructive commands |
-| ![Grey](img/color-grey.png) | Grey is used for neutral secondary elements. Depending on context, white is sometimes used instead. |
+| | State | Action |
+| :------: | :------- | :------- |
+| ![Blue](img/color-blue.png) | Primary and active (such as the current tab) | Organizational, managing, and retry commands|
+| ![Green](img/color-green.png) | Opened | Create new objects |
+| ![Orange](img/color-orange.png) | Warning | Non destructive action |
+| ![Red](img/color-red.png) | Closed | Delete and other destructive commands |
+| ![Grey](img/color-grey.png) | Neutral | Neutral secondary commands |
+
+### Text colors
+
+|||
+| :---: | :--- |
+| ![Text primary](img/color-textprimary.png) | Used for primary body text, such as issue description and comment |
+| ![Text secondary](img/color-textsecondary.png) | Used for secondary body text, such as username and date |
> TODO: Establish a perspective for color in terms of our personality and rationalize with Marketing usage.
diff --git a/doc/development/ux_guide/components.md b/doc/development/ux_guide/components.md
index 28383ad7dfc..18d0647c798 100644
--- a/doc/development/ux_guide/components.md
+++ b/doc/development/ux_guide/components.md
@@ -12,6 +12,7 @@
* [Panels](#panels)
* [Alerts](#alerts)
* [Forms](#forms)
+* [Search box](#search-box)
* [File holders](#file-holders)
* [Data formats](#data-formats)
@@ -95,6 +96,20 @@ Since secondary buttons only have a border on their resting state, their hover a
| Background: `$color-light` <br> Border: `$border-color-light` | ![](img/button-success-secondary--hover.png) | ![](img/button-close--hover.png) | ![](img/button-spam--hover.png) |
| Background: `$color-normal` <br> Border: `$border-color-normal` | ![](img/button-success-secondary--active.png) | ![](img/button-close--active.png) | ![](img/button-spam--active.png) |
+### Placement
+
+When there are a group of buttons in a dialog or a form, we need to be consistent with the placement.
+
+#### Dismissive actions on the left
+The dismissive action returns the user to the previous state.
+
+> Example: Cancel
+
+#### Affirmative actions on the right
+Affirmative actions continue to progress towards the user goal that triggered the dialog or form.
+
+> Example: Submit, Ok, Delete
+
---
@@ -108,7 +123,7 @@ Dropdowns are used to allow users to choose one (or many) options from a list of
### Max size
-The max height for dropdowns should target **10-15 items**. If the height of the dropdown is too large, the list becomes very hard to parse and it is easy to visually lose track of the item you are looking for. Usability also suffers as more mouse movement is required, and you have a larger area in which you hijack the scroll away from the page level. While it may initially seem counterintuitive to not show as many items as you can, it is actually quicker and easier to process the information when it is cropped at a reasonable height.
+The max height for dropdowns should target **10-15** single line items, or **7-10** multi-line items. If the height of the dropdown is too large, the list becomes very hard to parse and it is easy to visually lose track of the item you are looking for. Usability also suffers as more mouse movement is required, and you have a larger area in which you hijack the scroll away from the page level. While it may initially seem counterintuitive to not show as many items as you can, it is actually quicker and easier to process the information when it is cropped at a reasonable height.
---
@@ -215,6 +230,18 @@ Horizontal form (`form.horizontal-form`) with label rendered inline with input.
---
+## Search box
+
+Search boxes across GitLab have a consistent rest, active and text entered state. When text isn't entered in the box, there should be a magnifying glass right aligned with the input field. When text is entered, the magnifying glass should become a x, allowing users to clear their text.
+
+![Search box](img/components-searchbox.png)
+
+If needed, we indicate the scope of the search in the search box.
+
+![Scoped Search box](img/components-searchboxscoped.png)
+
+---
+
## File holders
A file holder (`.file-holder`) is used to show the contents of a file inline on a page of GitLab.
diff --git a/doc/development/ux_guide/copy.md b/doc/development/ux_guide/copy.md
index 8896d450f14..5b65d531e54 100644
--- a/doc/development/ux_guide/copy.md
+++ b/doc/development/ux_guide/copy.md
@@ -2,14 +2,16 @@
The copy for GitLab is clear and direct. We strike a clear balance between professional and friendly. We can empathesize with users (such as celebrating completing all Todos), and remain respectful of the importance of the work. We are that trusted, friendly coworker that is helpful and understanding.
-The copy and messaging is a core part of the experience of GitLab and the conversation with our users. Follow the below conventions throughout GitLab.
+The copy and messaging is a core part of the experience of GitLab and the conversation with our users. Follow the below conventions throughout GitLab.
+
+Portions of this page are inspired by work found in the [Material Design guidelines][material design].
>**Note:**
We are currently inconsistent with this guidance. Images below are created to illustrate the point. As this guidance is refined, we will ensure that our experiences align.
## Contents
* [Brevity](#brevity)
-* [Sentence case](#sentence-case)
+* [Capitalization and punctuation](#capitalization-and-punctuation)
* [Terminology](#terminology)
---
@@ -29,14 +31,83 @@ Preferrably use context and placement of controls to make it obvious what clicki
---
-## Sentence case
-Use sentence case for all titles, headings, labels, menu items, and buttons.
+## Capitalization and punctuation
+
+### Case
+Use sentence case for titles, headings, labels, menu items, and buttons. Use title case when referring to [features][features] or [products][products]. Note that some features are also objects (e.g. “Merge Requests” and “merge requests”).
+
+| :white_check_mark: Do | :no_entry_sign: Don’t |
+| --- | --- |
+| Add issues to the Issue Board feature in GitLab Hosted | Add Issues To The Issue Board Feature In GitLab Hosted |
+
+### Avoid periods
+Avoid using periods in solitary sentences in these elements:
+
+* Labels
+* Hover text
+* Bulleted lists
+* Dialog body text
+
+Periods should be used for:
+
+* Lists or dialogs with multiple sentences
+* Any sentence followed by a link
+
+| :white_check_mark: **Do** place periods after sentences followed by a link | :no_entry_sign: **Don’t** place periods after a link if it‘s not followed by a sentence |
+| --- | --- |
+| Mention someone to notify them. [Learn more](#) | Mention someone to notify them. [Learn more](#). |
+
+| :white_check_mark: **Do** skip periods after solo sentences of body text | :no_entry_sign: **Don’t** place periods after body text if there is only a single sentence |
+| --- | --- |
+| To see the available commands, enter `/gitlab help` | To see the available commands, enter `/gitlab help`. |
+
+### Use contractions
+Don’t make a sentence harder to understand just to follow this rule. For example, “do not” can give more emphasis than “don’t” when needed.
+
+| :white_check_mark: Do | :no_entry_sign: Don’t |
+| --- | --- |
+| it’s, can’t, wouldn’t, you’re, you’ve, haven’t, don’t | it is, cannot, would not, it’ll, should’ve |
+
+### Use numerals for numbers
+Use “1, 2, 3” instead of “one, two, three” for numbers. One exception is when mixing uses of numbers, such as “Enter two 3s.”
+
+| :white_check_mark: Do | :no_entry_sign: Don’t |
+| --- | --- |
+| 3 new commits | Three new commits |
+
+### Punctuation
+Omit punctuation after phrases and labels to create a cleaner and more readable interface. Use punctuation to add clarity or be grammatically correct.
+
+| Punctuation mark | Copy and paste | HTML entity | Unicode | Mac shortcut | Windows shortcut | Description |
+|---|---|---|---|---|---|---|
+| Period | **.** | | | | | Omit for single sentences in affordances like labels, hover text, bulleted lists, and dialog body text.<br><br>Use in lists or dialogs with multiple sentences, and any sentence followed by a link or inline code.<br><br>Place inside quotation marks unless you’re telling the reader what to enter and it’s ambiguous whether to include the period. |
+| Comma | **,** | | | | | Place inside quotation marks.<br><br>Use a [serial comma][serial comma] in lists of three or more terms. |
+| Exclamation point | **!** | | | | | Avoid exclamation points as they tend to come across as shouting. Some exceptions include greetings or congratulatory messages. |
+| Colon | **:** | `&#58;` | `\u003A` | | | Omit from labels, for example, in the labels for fields in a form. |
+| Apostrophe | **’** | `&rsquo;` | `\u2019` | <kbd>⌥ Option</kbd>+<kbd>⇧ Shift</kbd>+<kbd>]</kbd> | <kbd>Alt</kbd>+<kbd>0 1 4 6</kbd> | Use for contractions (I’m, you’re, ’89) and to show possession.<br><br>To show possession, add an *’s* to all single nouns and names, even if they already end in an *s*: “Your issues’s status was changed.” For singular proper names ending in *s*, use only an apostrophe: “James’ commits.” For plurals of a single letter, add an *’s*: “Dot your i’s and cross your t’s.”<br><br>Omit for decades or acronyms: “the 1990s”, “MRs.” |
+| Quotation marks | **“**<br><br>**”**<br><br>**‘**<br><br>**’** | `&ldquo;`<br><br>`&rdquo;`<br><br>`&lsquo;`<br><br>`&rsquo;` | `\u201C`<br><br>`\u201D`<br><br>`\u2018`<br><br>`\u2019` | <kbd>⌥ Option</kbd>+<kbd>[</kbd><br><br><kbd>⌥ Option</kbd>+<kbd>⇧ Shift</kbd>+<kbd>[</kbd><br><br><kbd>⌥ Option</kbd>+<kbd>]</kbd><br><br><kbd>⌥ Option</kbd>+<kbd>⇧ Shift</kbd>+<kbd>]</kbd> | <kbd>Alt</kbd>+<kbd>0 1 4 7</kbd><br><br><kbd>Alt</kbd>+<kbd>0 1 4 8</kbd><br><br><kbd>Alt</kbd>+<kbd>0 1 4 5</kbd><br><br><kbd>Alt</kbd>+<kbd>0 1 4 6</kbd> | Use proper quotation marks (also known as smart quotes, curly quotes, or typographer’s quotes) for quotes. Single quotation marks are used for quotes inside of quotes.<br><br>The right single quotation mark symbol is also used for apostrophes.<br><br>Don’t use primes, straight quotes, or free-standing accents for quotation marks. |
+| Primes | **′**<br><br>**″** | `&prime;`<br><br>`&Prime;` | `\u2032`<br><br>`\u2033` | | <kbd>Alt</kbd>+<kbd>8 2 4 2</kbd><br><br><kbd>Alt</kbd>+<kbd>8 2 4 3</kbd> | Use prime (′) only in abbreviations for feet, arcminutes, and minutes: 3° 15′<br><br>Use double-prime (″) only in abbreviations for inches, arcseconds, and seconds: 3° 15′ 35″<br><br>Don’t use quotation marks, straight quotes, or free-standing accents for primes. |
+| Straight quotes and accents | **"**<br><br>**'**<br><br>**`**<br><br>**´** | `&quot;`<br><br>`&#39;`<br><br>`&#96;`<br><br>`&acute;` | `\u0022`<br><br>`\u0027`<br><br>`\u0060`<br><br>`\u00B4` | | | Don’t use straight quotes or free-standing accents for primes or quotation marks.<br><br>Proper typography never uses straight quotes. They are left over from the age of typewriters and their only modern use is for code. |
+| Ellipsis | **…** | `&hellip;` | | <kbd>⌥ Option</kbd>+<kbd>;</kbd> | <kbd>Alt</kbd>+<kbd>0 1 3 3</kbd> | Use to indicate an action in progress (“Downloading…”) or incomplete or truncated text. No space before the ellipsis.<br><br>Omit from menu items or buttons that open a dialog or start some other process. |
+| Chevrons | **«**<br><br>**»**<br><br>**‹**<br><br>**›**<br><br>**<**<br><br>**>** | `&#171;`<br><br>`&#187;`<br><br>`&#8249;`<br><br>`&#8250;`<br><br>`&lt;`<br><br>`&gt;` | `\u00AB`<br><br>`\u00BB`<br><br>`\u2039`<br><br>`\u203A`<br><br>`\u003C`<br><br>`\u003E`<br><br> | | | Omit from links or buttons that open another page or move to the next or previous step in a process. Also known as angle brackets, angular quote brackets, or guillemets. |
+| Em dash | **—** | `&mdash;` | `\u2014` | <kbd>⌥ Option</kbd>+<kbd>⇧ Shift</kbd>+<kbd>-</kbd> | <kbd>Alt</kbd>+<kbd>0 1 5 1</kbd> | Avoid using dashes to separate text. If you must use dashes for this purpose — like this — use an em dash surrounded by spaces. |
+| En dash | **–** | `&ndash;` | `\u2013` | <kbd>⌥ Option</kbd>+<kbd>-</kbd> | <kbd>Alt</kbd>+<kbd>0 1 5 0</kbd> | Use an en dash without spaces instead of a hyphen to indicate a range of values, such as numbers, times, and dates: “3–5 kg”, “8:00 AM–12:30 PM”, “10–17 Jan” |
+| Hyphen | **-** | | | | | Use to represent negative numbers, or to avoid ambiguity in adjective-noun or noun-participle pairs. Example: “anti-inflammatory”; “5-mile walk.”<br><br>Omit in commonly understood terms and adverbs that end in *ly*: “frontend”, “greatly improved performance.”<br><br>Omit in the term “open source.” |
+| Parentheses | **( )** | | | | | Use only to define acronyms or jargon: “Secure web connections are based on a technology called SSL (the secure sockets layer).”<br><br>Avoid other uses and instead rewrite the text, or use dashes or commas to set off the information. If parentheses are required: If the parenthetical is a complete, independent sentence, place the period inside the parentheses; if not, the period goes outside. |
+
+When using the <kbd>Alt</kbd> keystrokes in Windows, use the numeric keypad, not the row of numbers above the alphabet, and be sure Num Lock is turned on.
---
## Terminology
Only use the terms in the tables below.
+### Projects and Groups
+
+| Term | Use | :no_entry_sign: Don't |
+| ---- | --- | ----- |
+| Members | When discussing the people who are a part of a project or a group. | Don't use `users`. |
+
### Issues
#### Adjectives (states)
@@ -48,15 +119,15 @@ Only use the terms in the tables below.
| Deleted |
>**Example:**
-Use `5 open issues` and don't use `5 pending issues`.
+Use `5 open issues` and don’t use `5 pending issues`.
#### Verbs (actions)
-| Term | Use | Don't |
+| Term | Use | :no_entry_sign: Don’t |
| ---- | --- | --- |
-| Add | Add an issue | Don't use `create` or `new` |
+| Add | Add an issue | Don’t use `create` or `new` |
| View | View an open or closed issue ||
-| Edit | Edit an open or closed issue | Don't use `update` |
+| Edit | Edit an open or closed issue | Don’t use `update` |
| Close | Close an open issue ||
| Re-open | Re-open a closed issue | There should never be a need to use `open` as a verb |
| Delete | Delete an open or closed issue ||
@@ -67,7 +138,7 @@ When viewing a list of issues, there is a button that is labeled `Add`. Given th
![Add issue button](img/copy-form-addissuebutton.png)
-The form should be titled `Add issue`. The submit button should be labeled `Submit`. Don't use `Add`, `Create`, `New`, or `Save changes`. The cancel button should be labeled `Cancel`. Don't use `Back`.
+The form should be titled `Add issue`. The submit button should be labeled `Submit`. Don’t use `Add`, `Create`, `New`, or `Save changes`. The cancel button should be labeled `Cancel`. Don’t use `Back`.
![Add issue form](img/copy-form-addissueform.png)
@@ -77,7 +148,7 @@ When in context of an issue, the affordance to edit it is labeled `Edit`. If the
![Edit issue button](img/copy-form-editissuebutton.png)
-The form should be titled `Edit issue`. The submit button should be labeled `Save`. Don't use `Edit`, `Update`, `Submit`, or `Save changes`. The cancel button should be labeled `Cancel`. Don't use `Back`.
+The form should be titled `Edit issue`. The submit button should be labeled `Save`. Don’t use `Edit`, `Update`, `Submit`, or `Save changes`. The cancel button should be labeled `Cancel`. Don’t use `Back`.
![Edit issue form](img/copy-form-editissueform.png)
@@ -93,7 +164,7 @@ The form should be titled `Edit issue`. The submit button should be labeled `Sav
#### Verbs (actions)
-| Term | Use | Don't |
+| Term | Use | :no_entry_sign: Don’t |
| ---- | --- | --- |
| Add | Add a merge request | Do not use `create` or `new` |
| View | View an open or merged merge request ||
@@ -101,3 +172,22 @@ The form should be titled `Edit issue`. The submit button should be labeled `Sav
| Approve | Approve an open merge request ||
| Remove approval, unapproved | Remove approval of an open merge request | Do not use `unapprove` as that is not an English word|
| Merge | Merge an open merge request ||
+
+### Comments & Discussions
+
+#### Comment
+A **comment** is a written piece of text that users of GitLab can create. Comments have the meta data of author and timestamp. Comments can be added in a variety of contexts, such as issues, merge requests, and discussions.
+
+#### Discussion
+A **discussion** is a group of 1 or more comments. A discussion can include subdiscussions. Some discussions have the special capability of being able to be **resolved**. Both the comments in the discussion and the discussion itself can be resolved.
+
+---
+
+Portions of this page are modifications based on work created and shared by the [Android Open Source Project][android project] and used according to terms described in the [Creative Commons 2.5 Attribution License][creative commons].
+
+[material design]: https://material.io/guidelines/
+[features]: https://about.gitlab.com/features/ "GitLab features page"
+[products]: https://about.gitlab.com/products/ "GitLab products page"
+[serial comma]: https://en.wikipedia.org/wiki/Serial_comma "“Serial comma” in Wikipedia"
+[android project]: http://source.android.com/
+[creative commons]: http://creativecommons.org/licenses/by/2.5/ \ No newline at end of file
diff --git a/doc/development/ux_guide/img/animation-autoscroll.gif b/doc/development/ux_guide/img/animation-autoscroll.gif
new file mode 100644
index 00000000000..155b0234c64
--- /dev/null
+++ b/doc/development/ux_guide/img/animation-autoscroll.gif
Binary files differ
diff --git a/doc/development/ux_guide/img/animation-reorder.gif b/doc/development/ux_guide/img/animation-reorder.gif
new file mode 100644
index 00000000000..ccdeb3d396f
--- /dev/null
+++ b/doc/development/ux_guide/img/animation-reorder.gif
Binary files differ
diff --git a/doc/development/ux_guide/img/color-textprimary.png b/doc/development/ux_guide/img/color-textprimary.png
new file mode 100644
index 00000000000..90f2821f0cf
--- /dev/null
+++ b/doc/development/ux_guide/img/color-textprimary.png
Binary files differ
diff --git a/doc/development/ux_guide/img/color-textsecondary.png b/doc/development/ux_guide/img/color-textsecondary.png
new file mode 100644
index 00000000000..61cb8a13c45
--- /dev/null
+++ b/doc/development/ux_guide/img/color-textsecondary.png
Binary files differ
diff --git a/doc/development/ux_guide/img/components-searchbox.png b/doc/development/ux_guide/img/components-searchbox.png
new file mode 100644
index 00000000000..a25189296ba
--- /dev/null
+++ b/doc/development/ux_guide/img/components-searchbox.png
Binary files differ
diff --git a/doc/development/ux_guide/img/components-searchboxscoped.png b/doc/development/ux_guide/img/components-searchboxscoped.png
new file mode 100644
index 00000000000..b116d714848
--- /dev/null
+++ b/doc/development/ux_guide/img/components-searchboxscoped.png
Binary files differ
diff --git a/doc/gitlab-basics/add-image.md b/doc/gitlab-basics/add-image.md
index 476b48a217c..1a44123aa81 100644
--- a/doc/gitlab-basics/add-image.md
+++ b/doc/gitlab-basics/add-image.md
@@ -1,62 +1,56 @@
# How to add an image
-The following are the steps to add images to your repository in
-GitLab:
+Using your standard tool for copying files (e.g. Finder in Mac OS, or Explorer
+in Windows, or...), put the image file into the GitLab project. You can find the
+project as a regular folder in your files.
-Find the image that you’d like to add.
+Go to your [shell](command-line-commands.md), and move into the folder of your
+Gitlab project. This usually means running the following command until you get
+to the desired destination:
-In your computer files, find the GitLab project to which you'd like to add the image
-(you'll find it as a regular file). Click on every file until you find exactly where you'd
-like to add the image. There, paste the image.
-
-Go to your [shell](command-line-commands.md), and add the following commands:
-
-Add this command for every directory that you'd like to open:
```
-cd NAME-OF-FILE-YOU'D-LIKE-TO-OPEN
+cd NAME-OF-FOLDER-YOU'D-LIKE-TO-OPEN
```
-Create a new branch:
-```
-git checkout -b NAME-OF-BRANCH
-```
+Check if your image is actually present in the directory (if you are in Windows,
+use `dir` instead):
-Check if your image was correctly added to the directory:
```
ls
```
You should see the name of the image in the list shown.
-Move up the hierarchy through directories:
-```
-cd ../
-```
+Check the status:
-Check the status and you should see your image’s name in red:
```
git status
```
-Add your changes:
+Your image's name should appear in red, so `git` took notice of it! Now add it
+to the repository:
+
```
git add NAME-OF-YOUR-IMAGE
```
-Check the status and you should see your image’s name in green:
+Check the status again, your image's name should have turned green:
+
```
git status
```
-Add the commit:
+Commit:
+
```
-git commit -m “DESCRIBE COMMIT IN A FEW WORDS”
+git commit -m "DESCRIBE COMMIT IN A FEW WORDS"
```
-Now you can push (send) your changes (in the branch NAME-OF-BRANCH) to GitLab (the git remote named 'origin'):
+Now you can push (send) your changes (in the branch NAME-OF-BRANCH) to GitLab
+(the git remote named 'origin'):
+
```
git push origin NAME-OF-BRANCH
```
-Your image will be added to your branch in your repository in GitLab. Create a [Merge Request](add-merge-request.md)
-to integrate your changes to your project.
+Your image will be added to your branch in your repository in GitLab.
diff --git a/doc/install/README.md b/doc/install/README.md
index 239f5f301ec..2d2fd8cb380 100644
--- a/doc/install/README.md
+++ b/doc/install/README.md
@@ -4,3 +4,6 @@
- [Requirements](requirements.md)
- [Structure](structure.md)
- [Database MySQL](database_mysql.md)
+- [Digital Ocean and Docker](digitaloceandocker.md)
+- [Docker](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/docker)
+- [All installation methods](https://about.gitlab.com/installation/)
diff --git a/doc/install/database_mysql.md b/doc/install/database_mysql.md
index 322680f0cf4..65bfb0f7d6e 100644
--- a/doc/install/database_mysql.md
+++ b/doc/install/database_mysql.md
@@ -4,7 +4,7 @@
We do not recommend using MySQL due to various issues. For example, case [(in)sensitivity](https://dev.mysql.com/doc/refman/5.0/en/case-sensitivity.html) and [problems](https://bugs.mysql.com/bug.php?id=65830) that [suggested](https://bugs.mysql.com/bug.php?id=50909) [fixes](https://bugs.mysql.com/bug.php?id=65830) [have](https://bugs.mysql.com/bug.php?id=63164).
-## MySQL
+## Initial database setup
# Install the database packages
sudo apt-get install -y mysql-server mysql-client libmysqlclient-dev
@@ -32,8 +32,11 @@ We do not recommend using MySQL due to various issues. For example, case [(in)se
# If this fails, check your MySQL config files (e.g. `/etc/mysql/*.cnf`, `/etc/mysql/conf.d/*`) for the setting "innodb = off"
mysql> SET storage_engine=INNODB;
+ # If you have MySQL < 5.7.7 and want to enable utf8mb4 character set support with your GitLab install, you must set the following NOW:
+ mysql> SET GLOBAL innodb_file_per_table=1, innodb_file_format=Barracuda, innodb_large_prefix=1;
+
# Create the GitLab production database
- mysql> CREATE DATABASE IF NOT EXISTS `gitlabhq_production` DEFAULT CHARACTER SET `utf8` COLLATE `utf8_unicode_ci`;
+ mysql> CREATE DATABASE IF NOT EXISTS `gitlabhq_production` DEFAULT CHARACTER SET `utf8` COLLATE `utf8_general_ci`;
# Grant the GitLab user necessary permissions on the database
mysql> GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, CREATE TEMPORARY TABLES, DROP, INDEX, ALTER, LOCK TABLES, REFERENCES ON `gitlabhq_production`.* TO 'git'@'localhost';
@@ -51,7 +54,203 @@ We do not recommend using MySQL due to various issues. For example, case [(in)se
# Quit the database session
mysql> \q
- # You are done installing the database and can go back to the rest of the installation.
+ # You are done installing the database for now and can go back to the rest of the installation.
+
+Please proceed to the rest of the installation before running through the utf8mb4 support section.
+
+
+### MySQL utf8mb4 support
+
+After installation or upgrade, remember to [convert any new tables](#convert) to `utf8mb4`/`utf8mb4_general_ci`.
+
+---
+
+GitLab 8.14 has introduced [a feature](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7420) requiring `utf8mb4` encoding to be supported in your GitLab MySQL Database, which is not the case if you have setup your database before GitLab 8.16.
+
+Follow the below instructions to ensure you use the most up to date requirements for your GitLab MySQL Database.
+
+**We are about to do the following:**
+- Ensure you can enable `utf8mb4` encoding and `utf8mb4_general_ci` collation for your GitLab DB, tables and data.
+- Convert your GitLab tables and data from `utf8`/`utf8_general_ci` to `utf8mb4`/`utf8mb4_general_ci`
+
+### Check for utf8mb4 support
+
+#### Check for InnoDB File-Per-Table Tablespaces
+
+We need to check, enable and maybe convert your existing GitLab DB tables to the [InnoDB File-Per-Table Tablespaces](http://dev.mysql.com/doc/refman/5.7/en/innodb-multiple-tablespaces.html) as a prerequise for supporting **utfb8mb4 with long indexes** required by recent GitLab databases.
+
+ # Login to MySQL
+ mysql -u root -p
+
+ # Type the MySQL root password
+ mysql > use gitlabhq_production;
+
+ # Check your MySQL version is >= 5.5.3 (GitLab requires 5.5.14+)
+ mysql > SHOW VARIABLES LIKE 'version';
+ +---------------+-----------------+
+ | Variable_name | Value |
+ +---------------+-----------------+
+ | version | 5.5.53-0+deb8u1 |
+ +---------------+-----------------+
+
+ # Note where is your MySQL data dir for later:
+ mysql > select @@datadir;
+ +----------------+
+ | @@datadir |
+ +----------------+
+ | /var/lib/mysql |
+ +----------------+
+
+ # Note whether your MySQL server runs with innodb_file_per_table ON or OFF:
+ mysql> SELECT @@innodb_file_per_table;
+ +-------------------------+
+ | @@innodb_file_per_table |
+ +-------------------------+
+ | 1 |
+ +-------------------------+
+
+ # You can now quit the database session
+ mysql> \q
+
+> You need **MySQL 5.5.3 or later** to perform this update.
+
+Whatever the results of your checks above, we now need to check if your GitLab database has been created using [InnoDB File-Per-Table Tablespaces](http://dev.mysql.com/doc/refman/5.7/en/innodb-multiple-tablespaces.html) (i.e. `innodb_file_per_table` was set to **1** at initial setup time).
+
+> Note: This setting is [enabled by default](http://dev.mysql.com/doc/refman/5.7/en/innodb-parameters.html#sysvar_innodb_file_per_table) since MySQL 5.6.6.
+
+ # Run this command with root privileges, replace the data dir if different:
+ sudo ls -lh /var/lib/mysql/gitlabhq_production/*.ibd | wc -l
+
+ # Run this command with root privileges, replace the data dir if different:
+ sudo ls -lh /var/lib/mysql/gitlabhq_production/*.frm | wc -l
+
+
+- **Case 1: a result > 0 for both commands**
+
+Congrats, your GitLab database uses the right InnoDB tablespace format.
+
+However, you must still ensure that any **future tables** created by GitLab will still use the right format:
+
+- If `SELECT @@innodb_file_per_table` returned **1** previously, your server is running correctly.
+> It's however a requirement to check *now* that this setting is indeed persisted in your [my.cnf](https://dev.mysql.com/doc/refman/5.7/en/tablespace-enabling.html) file!
+
+- If `SELECT @@innodb_file_per_table` returned **0** previously, your server is not running correctly.
+> [Enable innodb_file_per_table](https://dev.mysql.com/doc/refman/5.7/en/tablespace-enabling.html) by running in a MySQL session as root the command `SET GLOBAL innodb_file_per_table=1, innodb_file_format=Barracuda;` and persist the two settings in your [my.cnf](https://dev.mysql.com/doc/refman/5.7/en/tablespace-enabling.html) file
+
+Now, if you have a **different result** returned by the 2 commands above, it means you have a **mix of tables format** uses in your GitLab database. This can happen if your MySQL server had different values for `innodb_file_per_table` in its life and you updated GitLab at different moments with those inconsistent values. So keep reading.
+
+- **Case 2: a result equals to "0" OR not the same result for both commands**
+
+Unfortunately, none or only some of your GitLab database tables use the GitLab requirement of [InnoDB File-Per-Table Tablespaces](http://dev.mysql.com/doc/refman/5.7/en/innodb-multiple-tablespaces.html).
+
+Let's enable what we need on the running server:
+
+ # Login to MySQL
+ mysql -u root -p
+
+ # Type the MySQL root password
+
+ # Enable innodb_file_per_table and set innodb_file_format on the running server:
+ mysql > SET GLOBAL innodb_file_per_table=1, innodb_file_format=Barracuda;
+
+ # You can now quit the database session
+ mysql> \q
+
+> Now, **persist** [innodb_file_per_table](https://dev.mysql.com/doc/refman/5.6/en/tablespace-enabling.html) and [innodb_file_format](https://dev.mysql.com/doc/refman/5.6/en/innodb-file-format-enabling.html) in your `my.cnf` file.
+
+Ensure at this stage that your GitLab instance is indeed **stopped**.
+
+Now, let's convert all the GitLab database tables to the new tablespace format:
+
+ # Login to MySQL
+ mysql -u root -p
+
+ # Type the MySQL root password
+ mysql > use gitlabhq_production;
+
+ # Safety check: you should still have those values set as follow:
+ mysql> SELECT @@innodb_file_per_table, @@innodb_file_format;
+ +-------------------------+----------------------+
+ | @@innodb_file_per_table | @@innodb_file_format |
+ +-------------------------+----------------------+
+ | 1 | Barracuda |
+ +-------------------------+----------------------+
+
+ mysql > SELECT CONCAT('ALTER TABLE `', TABLE_NAME,'` ENGINE=InnoDB;') AS 'Copy & run these SQL statements:' FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA="gitlabhq_production" AND TABLE_TYPE="BASE TABLE";
+
+ # If previous query returned results, copy & run all shown SQL statements
+
+ # You can now quit the database session
+ mysql> \q
+
+---
+
+#### Check for proper InnoDB File Format, Row Format, Large Prefix and tables conversion
+
+We need to check, enable and probably convert your existing GitLab DB tables to use the [Barracuda InnoDB file format](https://dev.mysql.com/doc/refman/5.6/en/innodb-file-format.html), the [DYNAMIC row format](https://dev.mysql.com/doc/refman/5.6/en/glossary.html#glos_dynamic_row_format) and [innodb_large_prefix](http://dev.mysql.com/doc/refman/5.7/en/innodb-parameters.html#sysvar_innodb_large_prefix) as a second prerequisite for supporting **utfb8mb4 with long indexes** used by recent GitLab databases.
+
+ # Login to MySQL
+ mysql -u root -p
+
+ # Type the MySQL root password
+ mysql > use gitlabhq_production;
+
+ # Set innodb_file_format and innodb_large_prefix on the running server:
+ # Note: These are the default settings only for MySQL 5.7.7 and later.
+
+ mysql > SET GLOBAL innodb_file_format=Barracuda, innodb_large_prefix=1;
+
+ # Your DB must be (still) using utf8/utf8_general_ci as default encoding and collation.
+ # We will NOT change the default encoding and collation on the DB in order to support future GitLab migrations creating tables
+ # that require "long indexes support" on installations using MySQL <= 5.7.9.
+ # However, when such migrations occur, you will have to follow this guide again to convert the newly created tables to the proper encoding/collation.
+
+ # This should return the following:
+ mysql> SELECT @@character_set_database, @@collation_database;
+ +--------------------------+----------------------+
+ | @@character_set_database | @@collation_database |
+ +--------------------------+----------------------+
+ | utf8 | utf8_general_ci |
+ +--------------------------+----------------------+
+
+> Now, ensure that [innodb_file_format](https://dev.mysql.com/doc/refman/5.6/en/tablespace-enabling.html) and [innodb_large_prefix](http://dev.mysql.com/doc/refman/5.7/en/innodb-parameters.html#sysvar_innodb_large_prefix) are **persisted** in your `my.cnf` file.
+
+#### Tables and data conversion to utf8mb4
+<a name="convert"></a>
+
+Now that you have a persistent MySQL setup, you can safely upgrade tables after setup or upgrade time:
+
+ # Convert tables not using ROW_FORMAT DYNAMIC:
+
+ mysql> SELECT CONCAT('ALTER TABLE `', TABLE_NAME,'` ROW_FORMAT=DYNAMIC;') AS 'Copy & run these SQL statements:' FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA="gitlabhq_production" AND TABLE_TYPE="BASE TABLE" AND ROW_FORMAT!="Dynamic";
+
+ # !! If previous query returned results, copy & run all shown SQL statements
+
+ # Convert tables/columns not using utf8mb4/utf8mb4_general_ci as encoding/collation:
+
+ mysql > SET foreign_key_checks = 0;
+
+ mysql > SELECT CONCAT('ALTER TABLE `', TABLE_NAME,'` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;') AS 'Copy & run these SQL statements:' FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA="gitlabhq_production" AND TABLE_COLLATION != "utf8mb4_general_ci" AND TABLE_TYPE="BASE TABLE";
+
+ # !! If previous query returned results, copy & run all shown SQL statements
+
+ # Turn foreign key checks back on
+ mysql > SET foreign_key_checks = 1;
+
+ # You can now quit the database session
+ mysql> \q
+
+Ensure your GitLab database configuration file uses a proper connection encoding and collation:
+
+```sudo -u git -H editor config/database.yml```
+
+ production:
+ adapter: mysql2
+ encoding: utf8mb4
+ collation: utf8mb4_general_ci
+
+[Restart your GitLab instance](../administration/restart_gitlab.md).
+
## MySQL strings limits
diff --git a/doc/install/digitaloceandocker.md b/doc/install/digitaloceandocker.md
new file mode 100644
index 00000000000..820060a489b
--- /dev/null
+++ b/doc/install/digitaloceandocker.md
@@ -0,0 +1,136 @@
+# Digital Ocean and Docker
+
+## Initial setup
+
+In this guide you'll configure a Digital Ocean droplet and set up Docker
+locally on either macOS or Linux.
+
+### On macOS
+
+#### Install Docker Toolbox
+
+1. [https://www.docker.com/products/docker-toolbox](https://www.docker.com/products/docker-toolbox)
+
+### On Linux
+
+#### Install Docker Engine
+
+1. [https://docs.docker.com/engine/installation/linux](https://docs.docker.com/engine/installation/linux/)
+
+#### Install Docker Machine
+
+1. [https://docs.docker.com/machine/install-machine](https://docs.docker.com/machine/install-machine/)
+
+_The rest of the steps are identical for macOS and Linux_
+
+### Create new docker host
+
+1. Login to Digital Ocean
+1. Generate a new API token at https://cloud.digitalocean.com/settings/api/tokens
+
+
+This command will create a new DO droplet called `gitlab-test-env-do` that will act as a docker host.
+
+**Note: 4GB is the minimum requirement for a Docker host that will run more then one GitLab instance**
+
++ RAM: 4GB
++ Name: `gitlab-test-env-do`
++ Driver: `digitalocean`
+
+
+**Set the DO token** - Replace the string below with your generated token
+
+```
+export DOTOKEN=cf3dfd0662933203005c4a73396214b7879d70aabc6352573fe178d340a80248
+```
+
+**Create the machine**
+
+```
+docker-machine create \
+ --driver digitalocean \
+ --digitalocean-access-token=$DOTOKEN \
+ --digitalocean-size "4gb" \
+ gitlab-test-env-do
+```
+
++ Resource: https://docs.docker.com/machine/drivers/digital-ocean/
+
+
+### Creating GitLab test instance
+
+
+#### Connect your shell to the new machine
+
+
+In this example we'll create a GitLab EE 8.10.8 instance.
+
+
+First connect the docker client to the docker host you created previously.
+
+```
+eval "$(docker-machine env gitlab-test-env-do)"
+```
+
+You can add this to your `~/.bash_profile` file to ensure the `docker` client uses the `gitlab-test-env-do` docker host
+
+
+#### Create new GitLab container
+
++ HTTP port: `8888`
++ SSH port: `2222`
+ + Set `gitlab_shell_ssh_port` using `--env GITLAB_OMNIBUS_CONFIG `
++ Hostname: IP of docker host
++ Container name: `gitlab-test-8.10`
++ GitLab version: **EE** `8.10.8-ee.0`
+
+##### Setup container settings
+
+```
+export SSH_PORT=2222
+export HTTP_PORT=8888
+export VERSION=8.10.8-ee.0
+export NAME=gitlab-test-8.10
+```
+
+##### Create container
+```
+docker run --detach \
+--env GITLAB_OMNIBUS_CONFIG="external_url 'http://$(docker-machine ip gitlab-test-env-do):$HTTP_PORT'; gitlab_rails['gitlab_shell_ssh_port'] = $SSH_PORT;" \
+--hostname $(docker-machine ip gitlab-test-env-do) \
+-p $HTTP_PORT:$HTTP_PORT -p $SSH_PORT:22 \
+--name $NAME \
+gitlab/gitlab-ee:$VERSION
+```
+
+#### Connect to the GitLab container
+
+##### Retrieve the docker host IP
+
+```
+docker-machine ip gitlab-test-env-do
+# example output: 192.168.151.134
+```
+
+
++ Browse to: http://192.168.151.134:8888/
+
+
+##### Execute interactive shell/edit configuration
+
+
+```
+docker exec -it $NAME /bin/bash
+```
+
+```
+# example commands
+root@192:/# vi /etc/gitlab/gitlab.rb
+root@192:/# gitlab-ctl reconfigure
+```
+
+#### Resources
+
++ [https://docs.gitlab.com/omnibus/docker/](https://docs.gitlab.com/omnibus/docker/)
++ [https://docs.docker.com/machine/get-started/](https://docs.docker.com/machine/get-started/)
++ [https://docs.docker.com/machine/reference/ip/](https://docs.docker.com/machine/reference/ip/)+
diff --git a/doc/install/installation.md b/doc/install/installation.md
index 2740b2982b9..355179960b3 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -124,7 +124,7 @@ Download Ruby and compile it:
mkdir /tmp/ruby && cd /tmp/ruby
curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.3/ruby-2.3.3.tar.gz
- echo 'a8db9ce7f9110320f33b8325200e3ecfbd2b534b ruby-2.3.3.tar.gz' | shasum -c - && tar xzf ruby-2.3.3.tar.gz
+ echo '1014ee699071aa2ddd501907d18cbe15399c997d ruby-2.3.3.tar.gz' | shasum -c - && tar xzf ruby-2.3.3.tar.gz
cd ruby-2.3.3
./configure --disable-install-rdoc
make
@@ -271,9 +271,9 @@ sudo usermod -aG redis git
### Clone the Source
# Clone GitLab repository
- sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-15-stable gitlab
+ sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-17-stable gitlab
-**Note:** You can change `8-15-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
+**Note:** You can change `8-17-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
### Configure It
@@ -313,6 +313,9 @@ sudo usermod -aG redis git
# Change the permissions of the directory where CI artifacts are stored
sudo chmod -R u+rwX shared/artifacts/
+ # Change the permissions of the directory where GitLab Pages are stored
+ sudo chmod -R ug+rwX shared/pages/
+
# Copy the example Unicorn config
sudo -u git -H cp config/unicorn.rb.example config/unicorn.rb
@@ -400,16 +403,10 @@ GitLab-Workhorse uses [GNU Make](https://www.gnu.org/software/make/). The
following command-line will install GitLab-Workhorse in `/home/git/gitlab-workhorse`
which is the recommended location.
- cd /home/git/gitlab
-
sudo -u git -H bundle exec rake "gitlab:workhorse:install[/home/git/gitlab-workhorse]" RAILS_ENV=production
### Initialize Database and Activate Advanced Features
- # Go to GitLab installation folder
-
- cd /home/git/gitlab
-
sudo -u git -H bundle exec rake gitlab:setup RAILS_ENV=production
# Type 'yes' to create the database tables.
@@ -454,7 +451,7 @@ Check if GitLab and its environment are configured correctly:
### Compile Assets
- sudo -u git -H bundle exec rake assets:precompile RAILS_ENV=production
+ sudo -u git -H bundle exec rake gitlab:assets:compile RAILS_ENV=production
### Start Your GitLab Instance
@@ -490,6 +487,10 @@ Make sure to edit the config file to match your setup. Also, ensure that you mat
# or else sudo rm -f /etc/nginx/sites-enabled/default
sudo editor /etc/nginx/sites-available/gitlab
+If you intend to enable GitLab pages, there is a separate Nginx config you need
+to use. Read all about the needed configuration at the
+[GitLab Pages administration guide](../administration/pages/index.md).
+
**Note:** If you want to use HTTPS, replace the `gitlab` Nginx config with `gitlab-ssl`. See [Using HTTPS](#using-https) for HTTPS configuration details.
### Test Configuration
@@ -607,6 +608,12 @@ If you want to connect the Redis server via socket, then use the "unix:" URL sch
production:
url: unix:/path/to/redis/socket
+Also you can use environment variables in the `config/resque.yml` file:
+
+ # example
+ production:
+ url: <%= ENV.fetch('GITLAB_REDIS_URL') %>
+
### Custom SSH Connection
If you are running SSH on a non-standard port, you must change the GitLab user's SSH config.
diff --git a/doc/install/relative_url.md b/doc/install/relative_url.md
index 44d2a14f366..713d11b75e4 100644
--- a/doc/install/relative_url.md
+++ b/doc/install/relative_url.md
@@ -113,14 +113,6 @@ Make sure to follow all steps below:
If you are using a custom init script, make sure to edit the above
gitlab-workhorse setting as needed.
-1. After all the above changes recompile the assets. This is an important task
- and will take some time to complete depending on the server resources:
-
- ```
- cd /home/git/gitlab
- sudo -u git -H bundle exec rake assets:clean assets:precompile RAILS_ENV=production
- ```
-
1. [Restart GitLab][] for the changes to take effect.
### Disable relative URL in GitLab
diff --git a/doc/integration/README.md b/doc/integration/README.md
index f8ffa6dcb7f..22bdf33443d 100644
--- a/doc/integration/README.md
+++ b/doc/integration/README.md
@@ -5,10 +5,10 @@ trackers and external authentication.
See the documentation below for details on how to configure these services.
-- [JIRA](../project_services/jira.md) Integrate with the JIRA issue tracker
+- [JIRA](../user/project/integrations/jira.md) Integrate with the JIRA issue tracker
- [External issue tracker](external-issue-tracker.md) Redmine, JIRA, etc.
- [LDAP](ldap.md) Set up sign in via LDAP
-- [OmniAuth](omniauth.md) Sign in via Twitter, GitHub, GitLab.com, Google, Bitbucket, Facebook, Shibboleth, SAML, Crowd and Azure
+- [OmniAuth](omniauth.md) Sign in via Twitter, GitHub, GitLab.com, Google, Bitbucket, Facebook, Shibboleth, SAML, Crowd, Azure and Authentiq ID
- [SAML](saml.md) Configure GitLab as a SAML 2.0 Service Provider
- [CAS](cas.md) Configure GitLab to sign in using CAS
- [OAuth2 provider](oauth_provider.md) OAuth2 application creation
@@ -16,18 +16,16 @@ See the documentation below for details on how to configure these services.
- [reCAPTCHA](recaptcha.md) Configure GitLab to use Google reCAPTCHA for new users
- [Akismet](akismet.md) Configure Akismet to stop spam
- [Koding](../administration/integration/koding.md) Configure Koding to use IDE integration
+- [PlantUML](../administration/integration/plantuml.md) Configure PlantUML to use diagrams in AsciiDoc documents.
-GitLab Enterprise Edition contains [advanced Jenkins support][jenkins].
-
-[jenkins]: http://docs.gitlab.com/ee/integration/jenkins.html
-
+> GitLab Enterprise Edition contains [advanced Jenkins support][jenkins].
## Project services
Integration with services such as Campfire, Flowdock, Gemnasium, HipChat,
Pivotal Tracker, and Slack are available in the form of a [Project Service][].
-[Project Service]: ../project_services/project_services.md
+[Project Service]: ../user/project/integrations/project_services.md
## SSL certificate errors
@@ -63,3 +61,5 @@ After that restart GitLab with:
```bash
sudo gitlab-ctl restart
```
+
+[jenkins]: http://docs.gitlab.com/ee/integration/jenkins.html
diff --git a/doc/integration/bitbucket.md b/doc/integration/bitbucket.md
index 1dfc985eaea..2a14c0397ca 100644
--- a/doc/integration/bitbucket.md
+++ b/doc/integration/bitbucket.md
@@ -40,9 +40,13 @@ you to use.
| :--- | :---------- |
| **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. |
- | **Callback URL** | Leave blank. |
+ | **Callback URL** | The URL to your GitLab installation, e.g., `https://gitlab.example.com`. |
| **URL** | The URL to your GitLab installation, e.g., `https://gitlab.example.com`. |
+ NOTE: Starting in GitLab 8.15, you MUST specify a callback URL, or you will
+ see an "Invalid redirect_uri" message. For more details, see [the
+ Bitbucket documentation](https://confluence.atlassian.com/bitbucket/oauth-faq-338365710.html).
+
And grant at least the following permissions:
```
diff --git a/doc/integration/chat_commands.md b/doc/integration/chat_commands.md
new file mode 100644
index 00000000000..4b0084678d9
--- /dev/null
+++ b/doc/integration/chat_commands.md
@@ -0,0 +1,14 @@
+# Chat Commands
+
+Chat commands allow user to perform common operations on GitLab right from there chat client.
+Right now both Mattermost and Slack are supported.
+
+## Available commands
+
+The trigger is configurable, but for the sake of this example, we'll use `/trigger`
+
+* `/trigger help` - Displays all available commands for this user
+* `/trigger issue new <title> <shift+return> <description>` - creates a new issue on the project
+* `/trigger issue show <id>` - Shows the issue with the given ID, if you've got access
+* `/trigger issue search <query>` - Shows a maximum of 5 items matching the query
+* `/trigger deploy <from> to <to>` - Deploy from an environment to another
diff --git a/doc/integration/external-issue-tracker.md b/doc/integration/external-issue-tracker.md
index 8d2c6351fb8..265c891cf83 100644
--- a/doc/integration/external-issue-tracker.md
+++ b/doc/integration/external-issue-tracker.md
@@ -18,9 +18,9 @@ The configuration is done via a project's **Services**.
To enable an external issue tracker you must configure the appropriate **Service**.
Visit the links below for details:
-- [Redmine](../project_services/redmine.md)
-- [Jira](../project_services/jira.md)
-- [Bugzilla](../project_services/bugzilla.md)
+- [Redmine](../user/project/integrations/redmine.md)
+- [Jira](../user/project/integrations/jira.md)
+- [Bugzilla](../user/project/integrations/bugzilla.md)
### Service Template
@@ -28,4 +28,4 @@ To save you the hassle from configuring each project's service individually,
GitLab provides the ability to set Service Templates which can then be
overridden in each project's settings.
-Read more on [Services Templates](../project_services/services_templates.md).
+Read more on [Services Templates](../user/project/integrations/services_templates.md).
diff --git a/doc/integration/jira.md b/doc/integration/jira.md
index e2f136bcc35..b6923f74e28 100644
--- a/doc/integration/jira.md
+++ b/doc/integration/jira.md
@@ -1,3 +1 @@
-# GitLab JIRA integration
-
-This document was moved to [project_services/jira](../project_services/jira.md).
+This document was moved to [integrations/jira](../user/project/integrations/jira.md).
diff --git a/doc/integration/oauth_provider.md b/doc/integration/oauth_provider.md
index 0c53584d201..af8a1c4e5ed 100644
--- a/doc/integration/oauth_provider.md
+++ b/doc/integration/oauth_provider.md
@@ -74,8 +74,10 @@ in the **Authorized applications** section under **Profile Settings > Applicatio
---
-As you can see, the default scope `api` is used, which is the only scope that
-GitLab supports so far. At any time you can revoke any access by just clicking
-**Revoke**.
+GitLab's OAuth applications support scopes, which allow various actions that any given
+application can perform. Although there are only two scopes available at the
+moment – `read_user` and `api` – the groundwork has been laid to add more scopes easily.
+
+At any time you can revoke any access by just clicking **Revoke**.
[oauth]: http://oauth.net/2/ "OAuth website"
diff --git a/doc/integration/omniauth.md b/doc/integration/omniauth.md
index 8a55fce96fe..98a680d0dbe 100644
--- a/doc/integration/omniauth.md
+++ b/doc/integration/omniauth.md
@@ -30,6 +30,7 @@ contains some settings that are common for all providers.
- [Crowd](crowd.md)
- [Azure](azure.md)
- [Auth0](auth0.md)
+- [Authentiq](../administration/auth/authentiq.md)
## Initial OmniAuth Configuration
@@ -40,6 +41,9 @@ that are in common for all providers that we need to consider.
- `allow_single_sign_on` allows you to specify the providers you want to allow to
automatically create an account. It defaults to `false`. If `false` users must
be created manually or they will not be able to sign in via OmniAuth.
+- `auto_link_ldap_user` can be used if you have [LDAP / ActiveDirectory](ldap.md)
+ integration enabled. It defaults to false. When enabled, users automatically
+ created through OmniAuth will be linked to their LDAP entry as well.
- `block_auto_created_users` defaults to `true`. If `true` auto created users will
be blocked by default and will have to be unblocked by an administrator before
they are able to sign in.
@@ -51,6 +55,10 @@ SAML, Shibboleth, Crowd or Google, or set it to `false` otherwise any user on
the Internet will be able to successfully sign in to your GitLab without
administrative approval.
+>**Note:**
+`auto_link_ldap_user` requires the `uid` of the user to be the same in both LDAP
+and the OmniAuth provider.
+
To change these settings:
* **For omnibus package**
@@ -71,6 +79,7 @@ To change these settings:
# using an array, e.g. ["saml", "twitter"], or as true/false to allow all providers or none.
# User accounts will be created automatically when authentication was successful.
gitlab_rails['omniauth_allow_single_sign_on'] = ['saml', 'twitter']
+ gitlab_rails['omniauth_auto_link_ldap_user'] = true
gitlab_rails['omniauth_block_auto_created_users'] = true
```
@@ -98,6 +107,8 @@ To change these settings:
# User accounts will be created automatically when authentication was successful.
allow_single_sign_on: ["saml", "twitter"]
+ auto_link_ldap_user: true
+
# Locks down those users until they have been cleared by the admin (default: true).
block_auto_created_users: true
```
diff --git a/doc/pages/README.md b/doc/pages/README.md
new file mode 100644
index 00000000000..c9715eed598
--- /dev/null
+++ b/doc/pages/README.md
@@ -0,0 +1 @@
+This document was moved to [user/project/pages](../user/project/pages/index.md).
diff --git a/doc/pages/administration.md b/doc/pages/administration.md
new file mode 100644
index 00000000000..4eb3bb32c77
--- /dev/null
+++ b/doc/pages/administration.md
@@ -0,0 +1 @@
+This document was moved to [administration/pages](../administration/pages/index.md).
diff --git a/doc/profile/2fa_u2f_authenticate.png b/doc/profile/2fa_u2f_authenticate.png
deleted file mode 100644
index b224ab14195..00000000000
--- a/doc/profile/2fa_u2f_authenticate.png
+++ /dev/null
Binary files differ
diff --git a/doc/profile/two_factor_authentication.md b/doc/profile/two_factor_authentication.md
index 3f6dfe03d14..60918a0339c 100644
--- a/doc/profile/two_factor_authentication.md
+++ b/doc/profile/two_factor_authentication.md
@@ -1,143 +1 @@
-# Two-factor Authentication (2FA)
-
-Two-factor Authentication (2FA) provides an additional level of security to your
-GitLab account. Once enabled, in addition to supplying your username and
-password to login, you'll be prompted for a code generated by an application on
-your phone.
-
-By enabling 2FA, the only way someone other than you can log into your account
-is to know your username and password *and* have access to your phone.
-
-> **Note:**
-When you enable 2FA, don't forget to back up your recovery codes. For your safety, if you
-lose your codes for GitLab.com, we can't disable or recover them.
-
-In addition to a phone application, GitLab supports U2F (universal 2nd factor) devices as
-the second factor of authentication. Once enabled, in addition to supplying your username and
-password to login, you'll be prompted to activate your U2F device (usually by pressing
-a button on it), and it will perform secure authentication on your behalf.
-
-> **Note:** Support for U2F devices was added in version 8.8
-
-The U2F workflow is only supported by Google Chrome at this point, so we _strongly_ recommend
-that you set up both methods of two-factor authentication, so you can still access your account
-from other browsers.
-
-> **Note:** GitLab officially only supports [Yubikey] U2F devices.
-
-## Enabling 2FA
-
-### Enable 2FA via mobile application
-
-**In GitLab:**
-
-1. Log in to your GitLab account.
-1. Go to your **Profile Settings**.
-1. Go to **Account**.
-1. Click **Enable Two-factor Authentication**.
-
-![Two-factor setup](2fa.png)
-
-**On your phone:**
-
-1. Install a compatible application. We recommend [Google Authenticator]
-\(proprietary\) or [FreeOTP] \(open source\).
-1. In the application, add a new entry in one of two ways:
- * Scan the code with your phone's camera to add the entry automatically.
- * Enter the details provided to add the entry manually.
-
-**In GitLab:**
-
-1. Enter the six-digit pin number from the entry on your phone into the **Pin
- code** field.
-1. Click **Submit**.
-
-If the pin you entered was correct, you'll see a message indicating that
-Two-Factor Authentication has been enabled, and you'll be presented with a list
-of recovery codes.
-
-### Enable 2FA via U2F device
-
-**In GitLab:**
-
-1. Log in to your GitLab account.
-1. Go to your **Profile Settings**.
-1. Go to **Account**.
-1. Click **Enable Two-Factor Authentication**.
-1. Plug in your U2F device.
-1. Click on **Setup New U2F Device**.
-1. A light will start blinking on your device. Activate it by pressing its button.
-
-You will see a message indicating that your device was successfully set up.
-Click on **Register U2F Device** to complete the process.
-
-![Two-Factor U2F Setup](2fa_u2f_register.png)
-
-## Recovery Codes
-
-Should you ever lose access to your phone, you can use one of the ten provided
-backup codes to login to your account. We suggest copying or printing them for
-storage in a safe place. **Each code can be used only once** to log in to your
-account.
-
-If you lose the recovery codes or just want to generate new ones, you can do so
-from the **Profile Settings** > **Account** page where you first enabled 2FA.
-
-> **Note:** Recovery codes are not generated for U2F devices.
-
-## Logging in with 2FA Enabled
-
-Logging in with 2FA enabled is only slightly different than a normal login.
-Enter your username and password credentials as you normally would, and you'll
-be presented with a second prompt, depending on which type of 2FA you've enabled.
-
-### Log in via mobile application
-
-Enter the pin from your phone's application or a recovery code to log in.
-
-![Two-Factor Authentication on sign in via OTP](2fa_auth.png)
-
-### Log in via U2F device
-
-1. Click **Login via U2F Device**
-1. A light will start blinking on your device. Activate it by pressing its button.
-
-You will see a message indicating that your device responded to the authentication request.
-Click on **Authenticate via U2F Device** to complete the process.
-
-![Two-Factor Authentication on sign in via U2F device](2fa_u2f_authenticate.png)
-
-## Disabling 2FA
-
-1. Log in to your GitLab account.
-1. Go to your **Profile Settings**.
-1. Go to **Account**.
-1. Click **Disable**, under **Two-Factor Authentication**.
-
-This will clear all your two-factor authentication registrations, including mobile
-applications and U2F devices.
-
-## Personal access tokens
-
-When 2FA is enabled, you can no longer use your normal account password to
-authenticate with Git over HTTPS on the command line, you must use a personal
-access token instead.
-
-1. Log in to your GitLab account.
-1. Go to your **Profile Settings**.
-1. Go to **Access Tokens**.
-1. Choose a name and expiry date for the token.
-1. Click on **Create Personal Access Token**.
-1. Save the personal access token somewhere safe.
-
-When using git over HTTPS on the command line, enter the personal access token
-into the password field.
-
-## Note to GitLab administrators
-
-You need to take special care to that 2FA keeps working after
-[restoring a GitLab backup](../raketasks/backup_restore.md).
-
-[Google Authenticator]: https://support.google.com/accounts/answer/1066447?hl=en
-[FreeOTP]: https://fedorahosted.org/freeotp/
-[YubiKey]: https://www.yubico.com/products/yubikey-hardware/
+This document was moved to [user/profile/account](../user/profile/account/two_factor_authentication.md).
diff --git a/doc/project_services/bamboo.md b/doc/project_services/bamboo.md
index 51668128c62..5b171080c72 100644
--- a/doc/project_services/bamboo.md
+++ b/doc/project_services/bamboo.md
@@ -1,60 +1 @@
-# Atlassian Bamboo CI Service
-
-GitLab provides integration with Atlassian Bamboo for continuous integration.
-When configured, pushes to a project will trigger a build in Bamboo automatically.
-Merge requests will also display CI status showing whether the build is pending,
-failed, or completed successfully. It also provides a link to the Bamboo build
-page for more information.
-
-Bamboo doesn't quite provide the same features as a traditional build system when
-it comes to accepting webhooks and commit data. There are a few things that
-need to be configured in a Bamboo build plan before GitLab can integrate.
-
-## Setup
-
-### Complete these steps in Bamboo:
-
-1. Navigate to a Bamboo build plan and choose 'Configure plan' from the 'Actions'
-dropdown.
-1. Select the 'Triggers' tab.
-1. Click 'Add trigger'.
-1. Enter a description such as 'GitLab trigger'
-1. Choose 'Repository triggers the build when changes are committed'
-1. Check one or more repositories checkboxes
-1. Enter the GitLab IP address in the 'Trigger IP addresses' box. This is a
-whitelist of IP addresses that are allowed to trigger Bamboo builds.
-1. Save the trigger.
-1. In the left pane, select a build stage. If you have multiple build stages
-you want to select the last stage that contains the git checkout task.
-1. Select the 'Miscellaneous' tab.
-1. Under 'Pattern Match Labelling' put '${bamboo.repository.revision.number}'
-in the 'Labels' box.
-1. Save
-
-Bamboo is now ready to accept triggers from GitLab. Next, set up the Bamboo
-service in GitLab
-
-### Complete these steps in GitLab:
-
-1. Navigate to the project you want to configure to trigger builds.
-1. Select 'Settings' in the top navigation.
-1. Select 'Services' in the left navigation.
-1. Click 'Atlassian Bamboo CI'
-1. Select the 'Active' checkbox.
-1. Enter the base URL of your Bamboo server. 'https://bamboo.example.com'
-1. Enter the build key from your Bamboo build plan. Build keys are a short,
-all capital letter, identifier that is unique. It will be something like PR-BLD
-1. If necessary, enter username and password for a Bamboo user that has
-access to trigger the build plan. Leave these fields blank if you do not require
-authentication.
-1. Save or optionally click 'Test Settings'. Please note that 'Test Settings'
-will actually trigger a build in Bamboo.
-
-## Troubleshooting
-
-If builds are not triggered, these are a couple of things to keep in mind.
-
-1. Ensure you entered the right GitLab IP address in Bamboo under 'Trigger
-IP addresses'.
-1. Remember that GitLab only triggers builds on push events. A commit via the
-web interface will not trigger CI currently.
+This document was moved to [user/project/integrations/bamboo.md](../user/project/integrations/bamboo.md).
diff --git a/doc/project_services/bugzilla.md b/doc/project_services/bugzilla.md
index 215ed6fe9cc..e67055d5616 100644
--- a/doc/project_services/bugzilla.md
+++ b/doc/project_services/bugzilla.md
@@ -1,17 +1 @@
-# Bugzilla Service
-
-Go to your project's **Settings > Services > Bugzilla** and fill in the required
-details as described in the table below.
-
-| Field | Description |
-| ----- | ----------- |
-| `description` | A name for the issue tracker (to differentiate between instances, for example) |
-| `project_url` | The URL to the project in Bugzilla which is being linked to this GitLab project. Note that the `project_url` requires PRODUCT_NAME to be updated with the product/project name in Bugzilla. |
-| `issues_url` | The URL to the issue in Bugzilla project that is linked to this GitLab project. Note that the `issues_url` requires `:id` in the URL. This ID is used by GitLab as a placeholder to replace the issue number. |
-| `new_issue_url` | This is the URL to create a new issue in Bugzilla for the project linked to this GitLab project. Note that the `new_issue_url` requires PRODUCT_NAME to be updated with the product/project name in Bugzilla. |
-
-Once you have configured and enabled Bugzilla:
-
-- the **Issues** link on the GitLab project pages takes you to the appropriate
- Bugzilla product page
-- clicking **New issue** on the project dashboard takes you to Bugzilla for entering a new issue
+This document was moved to [user/project/integrations/bugzilla.md](../user/project/integrations/bugzilla.md).
diff --git a/doc/project_services/builds_emails.md b/doc/project_services/builds_emails.md
index af0b1a287c7..ee54d865225 100644
--- a/doc/project_services/builds_emails.md
+++ b/doc/project_services/builds_emails.md
@@ -1,16 +1 @@
-## Enabling build emails
-
-To receive e-mail notifications about the result status of your builds, visit
-your project's **Settings > Services > Builds emails** and activate the service.
-
-In the _Recipients_ area, provide a list of e-mails separated by comma.
-
-Check the _Add pusher_ checkbox if you want the committer to also receive
-e-mail notifications about each build's status.
-
-If you enable the _Notify only broken builds_ option, e-mail notifications will
-be sent only for failed builds.
-
----
-
-![Builds emails service settings](img/builds_emails_service.png)
+This document was moved to [user/project/integrations/builds_emails.md](../user/project/integrations/builds_emails.md).
diff --git a/doc/project_services/emails_on_push.md b/doc/project_services/emails_on_push.md
index 2f9f36f962e..a2e831ada34 100644
--- a/doc/project_services/emails_on_push.md
+++ b/doc/project_services/emails_on_push.md
@@ -1,17 +1 @@
-## Enabling emails on push
-
-To receive email notifications for every change that is pushed to the project, visit
-your project's **Settings > Services > Emails on push** and activate the service.
-
-In the _Recipients_ area, provide a list of emails separated by commas.
-
-You can configure any of the following settings depending on your preference.
-
-+ **Push events** - Email will be triggered when a push event is recieved
-+ **Tag push events** - Email will be triggered when a tag is created and pushed
-+ **Send from committer** - Send notifications from the committer's email address if the domain is part of the domain GitLab is running on (e.g. `user@gitlab.com`).
-+ **Disable code diffs** - Don't include possibly sensitive code diffs in notification body.
-
----
-
-![Email on push service settings](img/emails_on_push_service.png)
+This document was moved to [user/project/integrations/emails_on_push.md](../user/project/integrations/emails_on_push.md).
diff --git a/doc/project_services/hipchat.md b/doc/project_services/hipchat.md
index 021a93a288f..4ae9f6c6b2e 100644
--- a/doc/project_services/hipchat.md
+++ b/doc/project_services/hipchat.md
@@ -1,54 +1 @@
-# Atlassian HipChat
-
-GitLab provides a way to send HipChat notifications upon a number of events,
-such as when a user pushes code, creates a branch or tag, adds a comment, and
-creates a merge request.
-
-## Setup
-
-GitLab requires the use of a HipChat v2 API token to work. v1 tokens are
-not supported at this time. Note the differences between v1 and v2 tokens:
-
-HipChat v1 API (legacy) supports "API Auth Tokens" in the Group API menu. A v1
-token is allowed to send messages to *any* room.
-
-HipChat v2 API has tokens that are can be created using the Integrations tab
-in the Group or Room admin page. By design, these are lightweight tokens that
-allow GitLab to send messages only to *one* room.
-
-### Complete these steps in HipChat:
-
-1. Go to: https://admin.hipchat.com/admin
-1. Click on "Group Admin" -> "Integrations".
-1. Find "Build Your Own!" and click "Create".
-1. Select the desired room, name the integration "GitLab", and click "Create".
-1. In the "Send messages to this room by posting this URL" column, you should
-see a URL in the format:
-
-```
- https://api.hipchat.com/v2/room/<room>/notification?auth_token=<token>
-```
-
-HipChat is now ready to accept messages from GitLab. Next, set up the HipChat
-service in GitLab.
-
-### Complete these steps in GitLab:
-
-1. Navigate to the project you want to configure for notifications.
-1. Select "Settings" in the top navigation.
-1. Select "Services" in the left navigation.
-1. Click "HipChat".
-1. Select the "Active" checkbox.
-1. Insert the `token` field from the URL into the `Token` field on the Web page.
-1. Insert the `room` field from the URL into the `Room` field on the Web page.
-1. Save or optionally click "Test Settings".
-
-## Troubleshooting
-
-If you do not see notifications, make sure you are using a HipChat v2 API
-token, not a v1 token.
-
-Note that the v2 token is tied to a specific room. If you want to be able to
-specify arbitrary rooms, you can create an API token for a specific user in
-HipChat under "Account settings" and "API access". Use the `XXX` value under
-`auth_token=XXX`.
+This document was moved to [user/project/integrations/hipchat.md](../user/project/integrations/hipchat.md).
diff --git a/doc/project_services/img/builds_emails_service.png b/doc/project_services/img/builds_emails_service.png
deleted file mode 100644
index 9dbbed03833..00000000000
--- a/doc/project_services/img/builds_emails_service.png
+++ /dev/null
Binary files differ
diff --git a/doc/project_services/img/mattermost_config_help.png b/doc/project_services/img/mattermost_config_help.png
deleted file mode 100644
index a62e4b792f9..00000000000
--- a/doc/project_services/img/mattermost_config_help.png
+++ /dev/null
Binary files differ
diff --git a/doc/project_services/irker.md b/doc/project_services/irker.md
index 25c0c3ad2a6..7f0850dcc24 100644
--- a/doc/project_services/irker.md
+++ b/doc/project_services/irker.md
@@ -1,51 +1 @@
-# Irker IRC Gateway
-
-GitLab provides a way to push update messages to an Irker server. When
-configured, pushes to a project will trigger the service to send data directly
-to the Irker server.
-
-See the project homepage for further info: https://gitlab.com/esr/irker
-
-## Needed setup
-
-You will first need an Irker daemon. You can download the Irker code from its
-repository on https://gitlab.com/esr/irker:
-
-```
-git clone https://gitlab.com/esr/irker.git
-```
-
-Once you have downloaded the code, you can run the python script named `irkerd`.
-This script is the gateway script, it acts both as an IRC client, for sending
-messages to an IRC server obviously, and as a TCP server, for receiving messages
-from the GitLab service.
-
-If the Irker server runs on the same machine, you are done. If not, you will
-need to follow the firsts steps of the next section.
-
-## Complete these steps in GitLab:
-
-1. Navigate to the project you want to configure for notifications.
-1. Select "Settings" in the top navigation.
-1. Select "Services" in the left navigation.
-1. Click "Irker".
-1. Select the "Active" checkbox.
-1. Enter the server host address where `irkerd` runs (defaults to `localhost`)
-in the `Server host` field on the Web page
-1. Enter the server port of `irkerd` (e.g. defaults to 6659) in the
-`Server port` field on the Web page.
-1. Optional: if `Default IRC URI` is set, it has to be in the format
-`irc[s]://domain.name` and will be prepend to each and every channel provided
-by the user which is not a full URI.
-1. Specify the recipients (e.g. #channel1, user1, etc.)
-1. Save or optionally click "Test Settings".
-
-## Note on Irker recipients
-
-Irker accepts channel names of the form `chan` and `#chan`, both for the
-`#chan` channel. If you want to send messages in query, you will need to add
-`,isnick` after the channel name, in this form: `Aorimn,isnick`. In this latter
-case, `Aorimn` is treated as a nick and no more as a channel name.
-
-Irker can also join password-protected channels. Users need to append
-`?key=thesecretpassword` to the chan name.
+This document was moved to [user/project/integrations/irker.md](../user/project/integrations/irker.md).
diff --git a/doc/project_services/jira.md b/doc/project_services/jira.md
index 390066c9989..63614feba82 100644
--- a/doc/project_services/jira.md
+++ b/doc/project_services/jira.md
@@ -1,208 +1 @@
-# GitLab JIRA integration
-
-GitLab can be configured to interact with JIRA. Configuration happens via
-user name and password. Connecting to a JIRA server via CAS is not possible.
-
-Each project can be configured to connect to a different JIRA instance, see the
-[configuration](#configuration) section. If you have one JIRA instance you can
-pre-fill the settings page with a default template. To configure the template
-see the [Services Templates][services-templates] document.
-
-Once the project is connected to JIRA, you can reference and close the issues
-in JIRA directly from GitLab.
-
-## Configuration
-
-In order to enable the JIRA service in GitLab, you need to first configure the
-project in JIRA and then enter the correct values in GitLab.
-
-### Configuring JIRA
-
-We need to create a user in JIRA which will have access to all projects that
-need to integrate with GitLab. Login to your JIRA instance as admin and under
-Administration go to User Management and create a new user.
-
-As an example, we'll create a user named `gitlab` and add it to `JIRA-developers`
-group.
-
-**It is important that the user `GitLab` has write-access to projects in JIRA**
-
-We have split this stage in steps so it is easier to follow.
-
----
-
-1. Login to your JIRA instance as an administrator and under **Administration**
- go to **User Management** to create a new user.
-
- ![JIRA user management link](img/jira_user_management_link.png)
-
- ---
-
-1. The next step is to create a new user (e.g., `gitlab`) who has write access
- to projects in JIRA. Enter the user's name and a _valid_ e-mail address
- since JIRA sends a verification e-mail to set-up the password.
- _**Note:** JIRA creates the username automatically by using the e-mail
- prefix. You can change it later if you want._
-
- ![JIRA create new user](img/jira_create_new_user.png)
-
- ---
-
-1. Now, let's create a `gitlab-developers` group which will have write access
- to projects in JIRA. Go to the **Groups** tab and select **Create group**.
-
- ![JIRA create new user](img/jira_create_new_group.png)
-
- ---
-
- Give it an optional description and hit **Create group**.
-
- ![jira create new group](img/jira_create_new_group_name.png)
-
- ---
-
-1. Give the newly-created group write access by going to
- **Application access ➔ View configuration** and adding the `gitlab-developers`
- group to JIRA Core.
-
- ![JIRA group access](img/jira_group_access.png)
-
- ---
-
-1. Add the `gitlab` user to the `gitlab-developers` group by going to
- **Users ➔ GitLab user ➔ Add group** and selecting the `gitlab-developers`
- group from the dropdown menu. Notice that the group says _Access_ which is
- what we aim for.
-
- ![JIRA add user to group](img/jira_add_user_to_group.png)
-
----
-
-The JIRA configuration is over. Write down the new JIRA username and its
-password as they will be needed when configuring GitLab in the next section.
-
-### Configuring GitLab
-
->**Notes:**
-- The currently supported JIRA versions are `v6.x` and `v7.x.`. GitLab 7.8 or
- higher is required.
-- GitLab 8.14 introduced a new way to integrate with JIRA which greatly simplified
- the configuration options you have to enter. If you are using an older version,
- [follow this documentation][jira-repo-docs].
-
-To enable JIRA integration in a project, navigate to your project's
-**Services ➔ JIRA** and fill in the required details on the page as described
-in the table below.
-
-| Field | Description |
-| ----- | ----------- |
-| `URL` | The base URL to the JIRA project which is being linked to this GitLab project. E.g., `https://jira.example.com`. |
-| `Project key` | The short identifier for your JIRA project, all uppercase, e.g., `PROJ`. |
-| `Username` | The user name created in [configuring JIRA step](#configuring-jira). |
-| `Password` |The password of the user created in [configuring JIRA step](#configuring-jira). |
-| `JIRA issue transition` | This is the ID of a transition that moves issues to a closed state. You can find this number under JIRA workflow administration ([see screenshot](img/jira_workflow_screenshot.png)). |
-
-After saving the configuration, your GitLab project will be able to interact
-with the linked JIRA project.
-
-![JIRA service page](img/jira_service_page.png)
-
----
-
-## JIRA issues
-
-By now you should have [configured JIRA](#configuring-jira) and enabled the
-[JIRA service in GitLab](#configuring-gitlab). If everything is set up correctly
-you should be able to reference and close JIRA issues by just mentioning their
-ID in GitLab commits and merge requests.
-
-### Referencing JIRA Issues
-
-When GitLab project has JIRA issue tracker configured and enabled, mentioning
-JIRA issue in GitLab will automatically add a comment in JIRA issue with the
-link back to GitLab. This means that in comments in merge requests and commits
-referencing an issue, e.g., `PROJECT-7`, will add a comment in JIRA issue in the
-format:
-
-```
-USER mentioned this issue in RESOURCE_NAME of [PROJECT_NAME|LINK_TO_COMMENT]:
-ENTITY_TITLE
-```
-
-* `USER` A user that mentioned the issue. This is the link to the user profile in GitLab.
-* `LINK_TO_THE_COMMENT` Link to the origin of mention with a name of the entity where JIRA issue was mentioned.
-* `RESOURCE_NAME` Kind of resource which referenced the issue. Can be a commit or merge request.
-* `PROJECT_NAME` GitLab project name.
-* `ENTITY_TITLE` Merge request title or commit message first line.
-
-![example of mentioning or closing the JIRA issue](img/jira_issue_reference.png)
-
----
-
-### Closing JIRA Issues
-
-JIRA issues can be closed directly from GitLab by using trigger words in
-commits and merge requests. When a commit which contains the trigger word
-followed by the JIRA issue ID in the commit message is pushed, GitLab will
-add a comment in the mentioned JIRA issue and immediately close it (provided
-the transition ID was set up correctly).
-
-There are currently three trigger words, and you can use either one to achieve
-the same goal:
-
-- `Resolves PROJECT-1`
-- `Closes PROJECT-1`
-- `Fixes PROJECT-1`
-
-where `PROJECT-1` is the issue ID of the JIRA project.
-
-### JIRA issue closing example
-
-Let's consider the following example:
-
-1. For the project named `PROJECT` in JIRA, we implemented a new feature
- and created a merge request in GitLab.
-1. This feature was requested in JIRA issue `PROJECT-7` and the merge request
- in GitLab contains the improvement
-1. In the merge request description we use the issue closing trigger
- `Closes PROJECT-7`.
-1. Once the merge request is merged, the JIRA issue will be automatically closed
- with a comment and an associated link to the commit that resolved the issue.
-
----
-
-In the following screenshot you can see what the link references to the JIRA
-issue look like.
-
-![A Git commit that causes the JIRA issue to be closed](img/jira_merge_request_close.png)
-
----
-
-Once this merge request is merged, the JIRA issue will be automatically closed
-with a link to the commit that resolved the issue.
-
-![The GitLab integration closes JIRA issue](img/jira_service_close_issue.png)
-
----
-
-![The GitLab integration creates a comment and a link on JIRA issue.](img/jira_service_close_comment.png)
-
-## Troubleshooting
-
-If things don't work as expected that's usually because you have configured
-incorrectly the JIRA-GitLab integration.
-
-### GitLab is unable to comment on a ticket
-
-Make sure that the user you set up for GitLab to communicate with JIRA has the
-correct access permission to post comments on a ticket and to also transition
-the ticket, if you'd like GitLab to also take care of closing them.
-JIRA issue references and update comments will not work if the GitLab issue tracker is disabled.
-
-### GitLab is unable to close a ticket
-
-Make sure the `Transition ID` you set within the JIRA settings matches the one
-your project needs to close a ticket.
-
-[services-templates]: ../project_services/services_templates.md
-[jira-repo-docs]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-13-stable/doc/project_services/jira.md
+This document was moved to [user/project/integrations/jira.md](../user/project/integrations/jira.md).
diff --git a/doc/project_services/kubernetes.md b/doc/project_services/kubernetes.md
index 59d5da702f8..0497a13c2b7 100644
--- a/doc/project_services/kubernetes.md
+++ b/doc/project_services/kubernetes.md
@@ -1,63 +1 @@
-# GitLab Kubernetes / OpenShift integration
-
-GitLab can be configured to interact with Kubernetes, or other systems using the
-Kubernetes API (such as OpenShift).
-
-Each project can be configured to connect to a different Kubernetes cluster, see
-the [configuration](#configuration) section.
-
-If you have a single cluster that you want to use for all your projects,
-you can pre-fill the settings page with a default template. To configure the
-template, see the [Services Templates](services-templates.md) document.
-
-## Configuration
-
-![Kubernetes configuration settings](img/kubernetes_configuration.png)
-
-The Kubernetes service takes the following arguments:
-
-1. Kubernetes namespace
-1. API URL
-1. Service token
-1. Custom CA bundle
-
-The API URL is the URL that GitLab uses to access the Kubernetes API. Kubernetes
-exposes several APIs - we want the "base" URL that is common to all of them,
-e.g., `https://kubernetes.example.com` rather than `https://kubernetes.example.com/api/v1`.
-
-GitLab authenticates against Kubernetes using service tokens, which are
-scoped to a particular `namespace`. If you don't have a service token yet,
-you can follow the
-[Kubernetes documentation](http://kubernetes.io/docs/user-guide/service-accounts/)
-to create one. You can also view or create service tokens in the
-[Kubernetes dashboard](http://kubernetes.io/docs/user-guide/ui/) - visit
-`Config -> Secrets`.
-
-Fill in the service token and namespace according to the values you just got.
-If the API is using a self-signed TLS certificate, you'll also need to include
-the `ca.crt` contents as the `Custom CA bundle`.
-
-## Deployment variables
-
-The Kubernetes service exposes following
-[deployment variables](../ci/variables/README.md#deployment-variables) in the
-GitLab CI build environment:
-
-- `KUBE_URL` - equal to the API URL
-- `KUBE_TOKEN`
-- `KUBE_NAMESPACE`
-- `KUBE_CA_PEM` - only if a custom CA bundle was specified
-
-## Web terminals
-
->**NOTE:**
-Added in GitLab 8.15. You must be the project owner or have `master` permissions
-to use terminals. Support is currently limited to the first container in the
-first pod of your environment.
-
-When enabled, the Kubernetes service adds [web terminal](../ci/environments.md#web-terminals)
-support to your environments. This is based on the `exec` functionality found in
-Docker and Kubernetes, so you get a new shell session within your existing
-containers. To use this integration, you should deploy to Kubernetes using
-the deployment variables above, ensuring any pods you create are labelled with
-`app=$CI_ENVIRONMENT_SLUG`. GitLab will do the rest!
+This document was moved to [user/project/integrations/kubernetes.md](../user/project/integrations/kubernetes.md).
diff --git a/doc/project_services/mattermost.md b/doc/project_services/mattermost.md
index fbc7dfeee6d..554a028853e 100644
--- a/doc/project_services/mattermost.md
+++ b/doc/project_services/mattermost.md
@@ -1,45 +1 @@
-# Mattermost Notifications Service
-
-## On Mattermost
-
-To enable Mattermost integration you must create an incoming webhook integration:
-
-1. Sign in to your Mattermost instance
-1. Visit incoming webhooks, that will be something like: https://mattermost.example/your_team_name/integrations/incoming_webhooks/add
-1. Choose a display name, description and channel, those can be overridden on GitLab
-1. Save it, copy the **Webhook URL**, we'll need this later for GitLab.
-
-There might be some cases that Incoming Webhooks are blocked by admin, ask your mattermost admin to enable
-it on https://mattermost.example/admin_console/integrations/custom.
-
-Display name override is not enabled by default, you need to ask your admin to enable it on that same section.
-
-## On GitLab
-
-After you set up Mattermost, it's time to set up GitLab.
-
-Go to your project's **Settings > Services > Mattermost Notifications** and you will see a
-checkbox with the following events that can be triggered:
-
-- Push
-- Issue
-- Merge request
-- Note
-- Tag push
-- Build
-- Wiki page
-
-Bellow each of these event checkboxes, you will have an input field to insert
-which Mattermost channel you want to send that event message, with `#town-square`
-being the default. The hash sign is optional.
-
-At the end, fill in your Mattermost details:
-
-| Field | Description |
-| ----- | ----------- |
-| **Webhook** | The incoming webhooks which you have to setup on Mattermost, it will be something like: http://mattermost.example/hooks/5xo... |
-| **Username** | Optional username which can be on messages sent to Mattermost. Fill this in if you want to change the username of the bot. |
-| **Notify only broken builds** | If you choose to enable the **Build** event and you want to be only notified about failed builds. |
-
-
-![Mattermost configuration](img/mattermost_configuration.png)
+This document was moved to [user/project/integrations/mattermost.md](../user/project/integrations/mattermost.md).
diff --git a/doc/project_services/mattermost_slash_commands.md b/doc/project_services/mattermost_slash_commands.md
index 1a7c13a29b4..7c238b5dc37 100644
--- a/doc/project_services/mattermost_slash_commands.md
+++ b/doc/project_services/mattermost_slash_commands.md
@@ -1,157 +1 @@
-# Mattermost slash commands
-
-> Introduced in GitLab 8.14
-
-Mattermost commands give users an extra interface to perform common operations
-from the chat environment. This allows one to, for example, create an issue as
-soon as the idea was discussed in Mattermost.
-
-## Prerequisites
-
-Mattermost 3.4 and up is required.
-
-If you have the Omnibus GitLab package installed, Mattermost is already bundled
-in it. All you have to do is configure it. Read more in the
-[Omnibus GitLab Mattermost documentation][omnimmdocs].
-
-## Configuration
-
-The configuration consists of two parts. First you need to enable the slash
-commands in Mattermost and then enable the service in GitLab.
-
-
-### Step 1. Enable custom slash commands in Mattermost
-
-This step is only required when using a source install, omnibus installs will be
-preconfigured with the right settings.
-
-The first thing to do in Mattermost is to enable custom slash commands from
-the administrator console.
-
-1. Log in with an account that has admin privileges and navigate to the system
- console.
-
- ![Mattermost go to console](img/mattermost_goto_console.png)
-
- ---
-
-1. Click **Custom integrations** and set **Enable Custom Slash Commands**,
- **Enable custom integrations to override usernames**, and **Override
- custom integrations to override profile picture icons** to true
-
- ![Mattermost console](img/mattermost_console_integrations.png)
-
- ---
-
-1. Click **Save** at the bottom to save the changes.
-
-### Step 2. Open the Mattermost slash commands service in GitLab
-
-1. Open a new tab for GitLab and go to your project's settings
- **Services ➔ Mattermost command**. A screen will appear with all the values you
- need to copy in Mattermost as described in the next step. Leave the window open.
-
- >**Note:**
- GitLab will propose some values for the Mattermost settings. The only one
- required to copy-paste as-is is the **Request URL**, all the others are just
- suggestions.
-
- ![Mattermost setup instructions](img/mattermost_config_help.png)
-
- ---
-
-1. Proceed to the next step and create a slash command in Mattermost with the
- above values.
-
-### Step 3. Create a new custom slash command in Mattermost
-
-Now that you have enabled custom slash commands in Mattermost and opened
-the Mattermost slash commands service in GitLab, it's time to copy these values
-in a new slash command.
-
-1. Back to Mattermost, under your team page settings, you should see the
- **Integrations** option.
-
- ![Mattermost team integrations](img/mattermost_team_integrations.png)
-
- ---
-
-1. Go to the **Slash Commands** integration and add a new one by clicking the
- **Add Slash Command** button.
-
- ![Mattermost add command](img/mattermost_add_slash_command.png)
-
- ---
-
-1. Fill in the options for the custom command as described in
- [step 2](#step-2-open-the-mattermost-slash-commands-service-in-gitlab).
-
- >**Note:**
- If you plan on connecting multiple projects, pick a slash command trigger
- word that relates to your projects such as `/gitlab-project-name` or even
- just `/project-name`. Only use `/gitlab` if you will only connect a single
- project to your Mattermost team.
-
- ![Mattermost add command configuration](img/mattermost_slash_command_configuration.png)
-
-1. After you setup all the values, copy the token (we will use it below) and
- click **Done**.
-
- ![Mattermost slash command token](img/mattermost_slash_command_token.png)
-
-### Step 4. Copy the Mattermost token into the Mattermost slash command service
-
-1. In GitLab, paste the Mattermost token you copied in the previous step and
- check the **Active** checkbox.
-
- ![Mattermost copy token to GitLab](img/mattermost_gitlab_token.png)
-
-1. Click **Save changes** for the changes to take effect.
-
----
-
-You are now set to start using slash commands in Mattermost that talk to the
-GitLab project you configured.
-
-## Authorizing Mattermost to interact with GitLab
-
-The first time a user will interact with the newly created slash commands,
-Mattermost will trigger an authorization process.
-
-![Mattermost bot authorize](img/mattermost_bot_auth.png)
-
-This will connect your Mattermost user with your GitLab user. You can
-see all authorized chat accounts in your profile's page under **Chat**.
-
-When the authorization process is complete, you can start interacting with
-GitLab using the Mattermost commands.
-
-## Available slash commands
-
-The available slash commands are:
-
-| Command | Description | Example |
-| ------- | ----------- | ------- |
-| <kbd>/&lt;trigger&gt; issue new &lt;title&gt; <kbd>⇧ Shift</kbd>+<kbd>↵ Enter</kbd> &lt;description&gt;</kbd> | Create a new issue in the project that `<trigger>` is tied to. `<description>` is optional. | <samp>/gitlab issue new We need to change the homepage</samp> |
-| <kbd>/&lt;trigger&gt; issue show &lt;issue-number&gt;</kbd> | Show the issue with ID `<issue-number>` from the project that `<trigger>` is tied to. | <samp>/gitlab issue show 42</samp> |
-| <kbd>/&lt;trigger&gt; deploy &lt;environment&gt; to &lt;environment&gt;</kbd> | Start the CI job that deploys from one environment to another, for example `staging` to `production`. CI/CD must be [properly configured][ciyaml]. | <samp>/gitlab deploy staging to production</samp> |
-
-To see a list of available commands to interact with GitLab, type the
-trigger word followed by <kbd>help</kbd>. Example: <samp>/gitlab help</samp>
-
-![Mattermost bot available commands](img/mattermost_bot_available_commands.png)
-
-## Permissions
-
-The permissions to run the [available commands](#available-commands) derive from
-the [permissions you have on the project](../user/permissions.md#project).
-
-## Further reading
-
-- [Mattermost slash commands documentation][mmslashdocs]
-- [Omnibus GitLab Mattermost][omnimmdocs]
-
-
-[omnimmdocs]: https://docs.gitlab.com/omnibus/gitlab-mattermost/
-[mmslashdocs]: https://docs.mattermost.com/developer/slash-commands.html
-[ciyaml]: ../ci/yaml/README.md
+This document was moved to [user/project/integrations/mattermost_slash_commands.md](../user/project/integrations/mattermost_slash_commands.md).
diff --git a/doc/project_services/project_services.md b/doc/project_services/project_services.md
index 0f398874b8f..2c555c4edae 100644
--- a/doc/project_services/project_services.md
+++ b/doc/project_services/project_services.md
@@ -1,58 +1 @@
-# Project Services
-
-Project services allow you to integrate GitLab with other applications. Below
-is list of the currently supported ones.
-
-You can find these within GitLab in the Services page under Project Settings if
-you are at least a master on the project.
-Project Services are a bit like plugins in that they allow a lot of freedom in
-adding functionality to GitLab. For example there is also a service that can
-send an email every time someone pushes new commits.
-
-Because GitLab is open source we can ship with the code and tests for all
-plugins. This allows the community to keep the plugins up to date so that they
-always work in newer GitLab versions.
-
-For an overview of what projects services are available without logging in,
-please see the [project_services directory][projects-code].
-
-[projects-code]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/models/project_services
-
-Click on the service links to see
-further configuration instructions and details. Contributions are welcome.
-
-## Services
-
-| Service | Description |
-| ------- | ----------- |
-| Asana | Asana - Teamwork without email |
-| Assembla | Project Management Software (Source Commits Endpoint) |
-| [Atlassian Bamboo CI](bamboo.md) | A continuous integration and build server |
-| Buildkite | Continuous integration and deployments |
-| [Builds emails](builds_emails.md) | Email the builds status to a list of recipients |
-| [Bugzilla](bugzilla.md) | Bugzilla issue tracker |
-| Campfire | Simple web-based real-time group chat |
-| Custom Issue Tracker | Custom issue tracker |
-| Drone CI | Continuous Integration platform built on Docker, written in Go |
-| [Emails on push](emails_on_push.md) | Email the commits and diff of each push to a list of recipients |
-| External Wiki | Replaces the link to the internal wiki with a link to an external wiki |
-| Flowdock | Flowdock is a collaboration web app for technical teams |
-| Gemnasium | Gemnasium monitors your project dependencies and alerts you about updates and security vulnerabilities |
-| [HipChat](hipchat.md) | Private group chat and IM |
-| [Irker (IRC gateway)](irker.md) | Send IRC messages, on update, to a list of recipients through an Irker gateway |
-| [JIRA](jira.md) | JIRA issue tracker |
-| JetBrains TeamCity CI | A continuous integration and build server |
-| [Kubernetes](kubernetes.md) | A containerized deployment service |
-| [Mattermost slash commands](mattermost_slash_commands.md) | Mattermost chat and ChatOps slash commands |
-| [Mattermost Notifications](mattermost.md) | Receive event notifications in Mattermost |
-| [Slack Notifications](slack.md) | Receive event notifications in Slack |
-| PivotalTracker | Project Management Software (Source Commits Endpoint) |
-| Pushover | Pushover makes it easy to get real-time notifications on your Android device, iPhone, iPad, and Desktop |
-| [Redmine](redmine.md) | Redmine issue tracker |
-
-## Services Templates
-
-Services templates is a way to set some predefined values in the Service of
-your liking which will then be pre-filled on each project's Service.
-
-Read more about [Services Templates in this document](services_templates.md).
+This document was moved to [user/project/integrations/project_services.md](../user/project/integrations/project_services.md).
diff --git a/doc/project_services/redmine.md b/doc/project_services/redmine.md
index b9830ea7c38..6010aa4dc75 100644
--- a/doc/project_services/redmine.md
+++ b/doc/project_services/redmine.md
@@ -1,21 +1 @@
-# Redmine Service
-
-Go to your project's **Settings > Services > Redmine** and fill in the required
-details as described in the table below.
-
-| Field | Description |
-| ----- | ----------- |
-| `description` | A name for the issue tracker (to differentiate between instances, for example) |
-| `project_url` | The URL to the project in Redmine which is being linked to this GitLab project |
-| `issues_url` | The URL to the issue in Redmine project that is linked to this GitLab project. Note that the `issues_url` requires `:id` in the URL. This ID is used by GitLab as a placeholder to replace the issue number. |
-| `new_issue_url` | This is the URL to create a new issue in Redmine for the project linked to this GitLab project |
-
-Once you have configured and enabled Redmine:
-
-- the **Issues** link on the GitLab project pages takes you to the appropriate
- Redmine issue index
-- clicking **New issue** on the project dashboard creates a new Redmine issue
-
-As an example, below is a configuration for a project named gitlab-ci.
-
-![Redmine configuration](img/redmine_configuration.png)
+This document was moved to [user/project/integrations/redmine.md](../user/project/integrations/redmine.md).
diff --git a/doc/project_services/services_templates.md b/doc/project_services/services_templates.md
index be6d13b6d2b..8905d667c5a 100644
--- a/doc/project_services/services_templates.md
+++ b/doc/project_services/services_templates.md
@@ -1,25 +1 @@
-# Services Templates
-
-A GitLab administrator can add a service template that sets a default for each
-project. This makes it much easier to configure individual projects.
-
-After the template is created, the template details will be pre-filled on a
-project's Service page.
-
-## Enable a Service template
-
-In GitLab's Admin area, navigate to **Service Templates** and choose the
-service template you wish to create.
-
-For example, in the image below you can see Redmine.
-
-![Redmine service template](img/services_templates_redmine_example.png)
-
----
-
-**NOTE:** For each project, you will still need to configure the issue tracking
-URLs by replacing `:issues_tracker_id` in the above screenshot with the ID used
-by your external issue tracker. Prior to GitLab v7.8, this ID was configured in
-the project settings, and GitLab would automatically update the URL configured
-in `gitlab.yml`. This behavior is now deprecated and all issue tracker URLs
-must be configured directly within the project's **Services** settings.
+This document was moved to [user/project/integrations/services_templates.md](../user/project/integrations/services_templates.md).
diff --git a/doc/project_services/slack.md b/doc/project_services/slack.md
index 0b682b43810..1d3f98705e3 100644
--- a/doc/project_services/slack.md
+++ b/doc/project_services/slack.md
@@ -1,50 +1 @@
-# Slack Notifications Service
-
-## On Slack
-
-To enable Slack integration you must create an incoming webhook integration on
-Slack:
-
-1. [Sign in to Slack](https://slack.com/signin)
-1. Visit [Incoming WebHooks](https://my.slack.com/services/new/incoming-webhook/)
-1. Choose the channel name you want to send notifications to.
-1. Click **Add Incoming WebHooks Integration**
-1. Copy the **Webhook URL**, we'll need this later for GitLab.
-
-## On GitLab
-
-After you set up Slack, it's time to set up GitLab.
-
-Go to your project's **Settings > Services > Slack Notifications** and you will see a
-checkbox with the following events that can be triggered:
-
-- Push
-- Issue
-- Merge request
-- Note
-- Tag push
-- Build
-- Wiki page
-
-Bellow each of these event checkboxes, you will have an input field to insert
-which Slack channel you want to send that event message, with `#general`
-being the default. Enter your preferred channel **without** the hash sign (`#`).
-
-At the end, fill in your Slack details:
-
-| Field | Description |
-| ----- | ----------- |
-| **Webhook** | The [incoming webhook URL][slackhook] which you have to setup on Slack. |
-| **Username** | Optional username which can be on messages sent to slack. Fill this in if you want to change the username of the bot. |
-| **Notify only broken builds** | If you choose to enable the **Build** event and you want to be only notified about failed builds. |
-
-After you are all done, click **Save changes** for the changes to take effect.
-
->**Note:**
-You can set "branch,pushed,Compare changes" as highlight words on your Slack
-profile settings, so that you can be aware of new commits when somebody pushes
-them.
-
-![Slack configuration](img/slack_configuration.png)
-
-[slackhook]: https://my.slack.com/services/new/incoming-webhook
+This document was moved to [user/project/integrations/slack.md](../user/project/integrations/slack.md).
diff --git a/doc/project_services/slack_slash_commands.md b/doc/project_services/slack_slash_commands.md
new file mode 100644
index 00000000000..9554c8decc8
--- /dev/null
+++ b/doc/project_services/slack_slash_commands.md
@@ -0,0 +1 @@
+This document was moved to [user/project/integrations/slack_slash_commands.md](../user/project/integrations/slack_slash_commands.md).
diff --git a/doc/public_access/img/restrict_visibility_levels.png b/doc/public_access/img/restrict_visibility_levels.png
new file mode 100644
index 00000000000..c7d4d87981f
--- /dev/null
+++ b/doc/public_access/img/restrict_visibility_levels.png
Binary files differ
diff --git a/doc/public_access/public_access.md b/doc/public_access/public_access.md
index a3921f1b89f..e8f4c73120c 100644
--- a/doc/public_access/public_access.md
+++ b/doc/public_access/public_access.md
@@ -52,7 +52,7 @@ for anonymous users. The group page now has a visibility level icon.
## Visibility of users
-The public page of a user, located at `/u/username`, is always visible whether
+The public page of a user, located at `/username`, is always visible whether
you are logged in or not.
When visiting the public page of a user, you can only see the projects which
@@ -60,10 +60,13 @@ you are privileged to.
If the public level is restricted, user profiles are only visible to logged in users.
-
## Restricting the use of public or internal projects
In the Admin area under **Settings** (`/admin/application_settings`), you can
restrict the use of visibility levels for users when they create a project or a
-snippet. This is useful to prevent people exposing their repositories to public
+snippet:
+
+![Restrict visibility levels](img/restrict_visibility_levels.png)
+
+This is useful to prevent people exposing their repositories to public
by accident. The restricted visibility settings do not apply to admin users.
diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md
index f42bb6a81a2..0fb69d63dbe 100644
--- a/doc/raketasks/backup_restore.md
+++ b/doc/raketasks/backup_restore.md
@@ -9,6 +9,9 @@ This archive will be saved in `backup_path`, which is specified in the
The filename will be `[TIMESTAMP]_gitlab_backup.tar`, where `TIMESTAMP`
identifies the time at which each backup was created.
+> In GitLab 8.15 we changed the timestamp format from `EPOCH` (`1393513186`)
+> to `EPOCH_YYYY_MM_DD` (`1393513186_2014_02_27`)
+
You can only restore a backup to exactly the same version of GitLab on which it
was created. The best way to migrate your repositories from one server to
another is through backup restore.
@@ -81,6 +84,29 @@ Deleting tmp directories...[DONE]
Deleting old backups... [SKIPPING]
```
+## Exclude specific directories from the backup
+
+You can choose what should be backed up by adding the environment variable `SKIP`.
+The available options are:
+
+* `db`
+* `uploads` (attachments)
+* `repositories`
+* `builds` (CI build output logs)
+* `artifacts` (CI build artifacts)
+* `lfs` (LFS objects)
+* `pages` (pages content)
+
+Use a comma to specify several options at the same time:
+
+```
+# use this command if you've installed GitLab with the Omnibus package
+sudo gitlab-rake gitlab:backup:create SKIP=db,uploads
+
+# if you've installed GitLab from source
+sudo -u git -H bundle exec rake gitlab:backup:create SKIP=db,uploads RAILS_ENV=production
+```
+
## Upload backups to remote (cloud) storage
Starting with GitLab 7.4 you can let the backup script upload the '.tar' file it creates.
@@ -88,10 +114,10 @@ It uses the [Fog library](http://fog.io/) to perform the upload.
In the example below we use Amazon S3 for storage, but Fog also lets you use
[other storage providers](http://fog.io/storage/). GitLab
[imports cloud drivers](https://gitlab.com/gitlab-org/gitlab-ce/blob/30f5b9a5b711b46f1065baf755e413ceced5646b/Gemfile#L88)
-for AWS, OpenStack Swift and Rackspace as well. A local driver is
+for AWS, Google, OpenStack Swift and Rackspace as well. A local driver is
[also available](#uploading-to-locally-mounted-shares).
-For omnibus packages:
+For omnibus packages, add the following to `/etc/gitlab/gitlab.rb`:
```ruby
gitlab_rails['backup_upload_connection'] = {
@@ -106,6 +132,8 @@ gitlab_rails['backup_upload_connection'] = {
gitlab_rails['backup_upload_remote_directory'] = 'my.s3.bucket'
```
+Make sure to run `sudo gitlab-ctl reconfigure` after editing `/etc/gitlab/gitlab.rb` to reflect the changes.
+
For installations from source:
```yaml
@@ -223,7 +251,8 @@ For installations from source:
## Backup archive permissions
-The backup archives created by GitLab (123456_gitlab_backup.tar) will have owner/group git:git and 0600 permissions by default.
+The backup archives created by GitLab (`1393513186_2014_02_27_gitlab_backup.tar`)
+will have owner/group git:git and 0600 permissions by default.
This is meant to avoid other system users reading GitLab's data.
If you need the backup archives to have different permissions you can use the 'archive_permissions' setting.
@@ -335,7 +364,7 @@ First make sure your backup tar file is in the backup directory described in the
`/var/opt/gitlab/backups`.
```shell
-sudo cp 1393513186_gitlab_backup.tar /var/opt/gitlab/backups/
+sudo cp 1393513186_2014_02_27_gitlab_backup.tar /var/opt/gitlab/backups/
```
Stop the processes that are connected to the database. Leave the rest of GitLab
diff --git a/doc/raketasks/features.md b/doc/raketasks/features.md
index f9a46193547..fee49cc27cc 100644
--- a/doc/raketasks/features.md
+++ b/doc/raketasks/features.md
@@ -7,7 +7,7 @@ This command will enable the namespaces feature introduced in v4.0. It will move
Note:
- Because the **repository location will change**, you will need to **update all your git URLs** to point to the new location.
-- Username can be changed at [Profile / Account](/profile/account)
+- Username can be changed at **Profile ➔ Account**.
**Example:**
diff --git a/doc/security/webhooks.md b/doc/security/webhooks.md
index bb46aebf4b5..faabc53ce72 100644
--- a/doc/security/webhooks.md
+++ b/doc/security/webhooks.md
@@ -2,7 +2,7 @@
If you have non-GitLab web services running on your GitLab server or within its local network, these may be vulnerable to exploitation via Webhooks.
-With [Webhooks](../web_hooks/web_hooks.md), you and your project masters and owners can set up URLs to be triggered when specific things happen to projects. Normally, these requests are sent to external web services specifically set up for this purpose, that process the request and its attached data in some appropriate way.
+With [Webhooks](../user/project/integrations/webhooks.md), you and your project masters and owners can set up URLs to be triggered when specific things happen to projects. Normally, these requests are sent to external web services specifically set up for this purpose, that process the request and its attached data in some appropriate way.
Things get hairy, however, when a Webhook is set up with a URL that doesn't point to an external, but to an internal service, that may do something completely unintended when the webhook is triggered and the POST request is sent.
@@ -10,4 +10,4 @@ Because Webhook requests are made by the GitLab server itself, these have comple
If a web service does not require authentication, Webhooks can be used to trigger destructive commands by getting the GitLab server to make POST requests to endpoints like "http://localhost:123/some-resource/delete".
-To prevent this type of exploitation from happening, make sure that you are aware of every web service GitLab could potentially have access to, and that all of these are set up to require authentication for every potentially destructive command. Enabling authentication but leaving a default password is not enough. \ No newline at end of file
+To prevent this type of exploitation from happening, make sure that you are aware of every web service GitLab could potentially have access to, and that all of these are set up to require authentication for every potentially destructive command. Enabling authentication but leaving a default password is not enough.
diff --git a/doc/ssh/README.md b/doc/ssh/README.md
index 9803937fcf9..9e391d647a8 100644
--- a/doc/ssh/README.md
+++ b/doc/ssh/README.md
@@ -4,10 +4,12 @@ Git is a distributed version control system, which means you can work locally
but you can also share or "push" your changes to other servers.
Before you can push your changes to a GitLab server
you need a secure communication channel for sharing information.
-GitLab uses Public-key or asymmetric cryptography
-which encrypts a communication channel by locking it with your "private key"
-and allows trusted parties to unlock it with your "public key".
-If someone does not have your public key they cannot access the unencrypted message.
+
+The SSH protocol provides this security and allows you to authenticate to the
+GitLab remote server without supplying your username or password each time.
+
+For a more detailed explanation of how the SSH protocol works, we advise you to
+read [this nice tutorial by DigitalOcean](https://www.digitalocean.com/community/tutorials/understanding-the-ssh-encryption-and-connection-process).
## Locating an existing SSH key pair
diff --git a/doc/system_hooks/system_hooks.md b/doc/system_hooks/system_hooks.md
index c44930a4ceb..ec13c2446ef 100644
--- a/doc/system_hooks/system_hooks.md
+++ b/doc/system_hooks/system_hooks.md
@@ -1,6 +1,6 @@
# System hooks
-Your GitLab instance can perform HTTP POST requests on the following events: `project_create`, `project_destroy`, `project_rename`, `project_transfer`, `user_add_to_team`, `user_remove_from_team`, `user_create`, `user_destroy`, `key_create`, `key_destroy`, `group_create`, `group_destroy`, `user_add_to_group` and `user_remove_from_group`.
+Your GitLab instance can perform HTTP POST requests on the following events: `project_create`, `project_destroy`, `project_rename`, `project_transfer`, `project_update`, `user_add_to_team`, `user_remove_from_team`, `user_create`, `user_destroy`, `key_create`, `key_destroy`, `group_create`, `group_destroy`, `user_add_to_group` and `user_remove_from_group`.
System hooks can be used, e.g. for logging or changing information in a LDAP server.
@@ -88,6 +88,23 @@ X-Gitlab-Event: System Hook
}
```
+**Project updated:**
+
+```json
+{
+ "created_at": "2012-07-21T07:30:54Z",
+ "updated_at": "2012-07-21T07:38:22Z",
+ "event_name": "project_update",
+ "name": "StoreCloud",
+ "owner_email": "johnsmith@gmail.com",
+ "owner_name": "John Smith",
+ "path": "storecloud",
+ "path_with_namespace": "jsmith/storecloud",
+ "project_id": 74,
+ "project_visibility": "private",
+}
+```
+
**New Team Member:**
```json
diff --git a/doc/university/README.md b/doc/university/README.md
index 12727e9d56f..e9f14703789 100644
--- a/doc/university/README.md
+++ b/doc/university/README.md
@@ -91,7 +91,7 @@ The curriculum is composed of GitLab videos, screencasts, presentations, project
1. [Using any Static Site Generator with GitLab Pages](https://about.gitlab.com/2016/06/17/ssg-overview-gitlab-pages-part-3-examples-ci/)
1. [Securing GitLab Pages with SSL](https://about.gitlab.com/2016/06/24/secure-gitlab-pages-with-startssl/)
-1. [GitLab Pages Documentation](https://docs.gitlab.com/ee/pages/README.html)
+1. [GitLab Pages Documentation](https://docs.gitlab.com/ce/user/project/pages/)
#### 2.2. GitLab Issues
@@ -189,10 +189,10 @@ The curriculum is composed of GitLab videos, screencasts, presentations, project
#### 3.9. Integrations
1. [How to Integrate JIRA and Jenkins with GitLab - Video](https://gitlabmeetings.webex.com/gitlabmeetings/ldr.php?RCID=44b548147a67ab4d8a62274047146415)
-1. [How to Integrate Jira with GitLab](https://docs.gitlab.com/ee/integration/jira.html)
+1. [How to Integrate Jira with GitLab](https://docs.gitlab.com/ce/user/project/integrations/jira.html)
1. [How to Integrate Jenkins with GitLab](https://docs.gitlab.com/ee/integration/jenkins.html)
-1. [How to Integrate Bamboo with GitLab](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/project_services/bamboo.md)
-1. [How to Integrate Slack with GitLab](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/integration/slack.md)
+1. [How to Integrate Bamboo with GitLab](https://docs.gitlab.com/ce/user/project/integrations/bamboo.html)
+1. [How to Integrate Slack with GitLab](https://docs.gitlab.com/ce/user/project/integrations/slack.html)
1. [How to Integrate Convox with GitLab](https://about.gitlab.com/2016/06/09/continuous-delivery-with-gitlab-and-convox/)
1. [Getting Started with GitLab and Shippable CI](https://about.gitlab.com/2016/05/05/getting-started-gitlab-and-shippable/)
diff --git a/doc/university/glossary/README.md b/doc/university/glossary/README.md
index 20e7ea1987f..979a1c5d310 100644
--- a/doc/university/glossary/README.md
+++ b/doc/university/glossary/README.md
@@ -573,7 +573,7 @@ A [model](http://www.umsl.edu/~hugheyd/is6840/waterfall.html) of building softwa
### Webhooks
-A way for for an app to [provide](https://docs.gitlab.com/ce/web_hooks/web_hooks.html) other applications with real-time information (e.g., send a message to a slack channel when a commit is pushed.) Read about setting up [custom git hooks](https://gitlab.com/help/administration/custom_hooks.md) for when webhooks are insufficient.
+A way for for an app to [provide](https://docs.gitlab.com/ce/user/project/integrations/webhooks.html) other applications with real-time information (e.g., send a message to a slack channel when a commit is pushed.) Read about setting up [custom git hooks](https://gitlab.com/help/administration/custom_hooks.md) for when webhooks are insufficient.
### Wiki
diff --git a/doc/university/support/README.md b/doc/university/support/README.md
index 6e415e4d219..ca538ef6dc3 100644
--- a/doc/university/support/README.md
+++ b/doc/university/support/README.md
@@ -172,7 +172,7 @@ Move on to understanding some of GitLab's more advanced features. You can make u
- Get to know the [GitLab API](https://docs.gitlab.com/ee/api/README.html), its capabilities and shortcomings
- Learn how to [migrate from SVN to Git](https://docs.gitlab.com/ee/workflow/importing/migrating_from_svn.html)
- Set up [GitLab CI](https://docs.gitlab.com/ee/ci/quick_start/README.html)
-- Create your first [GitLab Page](https://docs.gitlab.com/ee/pages/administration.html)
+- Create your first [GitLab Page](https://docs.gitlab.com/ce/administration/pages/)
- Get to know the GitLab Codebase by reading through the source code:
- Find the differences between the [EE codebase](https://gitlab.com/gitlab-org/gitlab-ce)
and the [CE codebase](https://gitlab.com/gitlab-org/gitlab-ce)
diff --git a/doc/university/training/topics/additional_resources.md b/doc/university/training/topics/additional_resources.md
index 1ee615432aa..3ed601625cf 100755
--- a/doc/university/training/topics/additional_resources.md
+++ b/doc/university/training/topics/additional_resources.md
@@ -5,4 +5,4 @@
3. Pro git book [http://git-scm.com/book](http://git-scm.com/book)
4. Platzi Course [https://courses.platzi.com/courses/git-gitlab/](https://courses.platzi.com/courses/git-gitlab/)
5. Code School tutorial [http://try.github.io/](http://try.github.io/)
-6. Contact Us - [subscribers@gitlab.com](subscribers@gitlab.com)
+6. Contact Us at `subscribers@gitlab.com`
diff --git a/doc/university/training/user_training.md b/doc/university/training/user_training.md
index 35afe73708f..9e38df26b6a 100755
--- a/doc/university/training/user_training.md
+++ b/doc/university/training/user_training.md
@@ -389,4 +389,4 @@ GUI Clients [http://git-scm.com/downloads/guis](http://git-scm.com/downloads/gui
Pro git book [http://git-scm.com/book](http://git-scm.com/book)
Platzi Course [https://courses.platzi.com/courses/git-gitlab/](https://courses.platzi.com/courses/git-gitlab/)
Code School tutorial [http://try.github.io/](http://try.github.io/)
-Contact Us - [subscribers@gitlab.com](subscribers@gitlab.com)
+Contact Us at `subscribers@gitlab.com`
diff --git a/doc/update/2.6-to-3.0.md b/doc/update/2.6-to-3.0.md
index fb70eaacbc9..97cd277b424 100644
--- a/doc/update/2.6-to-3.0.md
+++ b/doc/update/2.6-to-3.0.md
@@ -1,5 +1,5 @@
# From 2.6 to 3.0
-*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/2.6-to-3.0.md) for the most up to date instructions.*
+*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/2.6-to-3.0.md) for the most up to date instructions.*
## 1. Stop server & resque
diff --git a/doc/update/2.9-to-3.0.md b/doc/update/2.9-to-3.0.md
index ce46b57c09a..a890aa885d5 100644
--- a/doc/update/2.9-to-3.0.md
+++ b/doc/update/2.9-to-3.0.md
@@ -1,5 +1,5 @@
# From 2.9 to 3.0
-*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/2.9-to-3.0.md) for the most up to date instructions.*
+*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/2.9-to-3.0.md) for the most up to date instructions.*
## 1. Stop server & resque
diff --git a/doc/update/3.0-to-3.1.md b/doc/update/3.0-to-3.1.md
index 6ac83f3b60d..e32508745a2 100644
--- a/doc/update/3.0-to-3.1.md
+++ b/doc/update/3.0-to-3.1.md
@@ -1,5 +1,5 @@
# From 3.0 to 3.1
-*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/3.0-to-3.1.md) for the most up to date instructions.*
+*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/3.0-to-3.1.md) for the most up to date instructions.*
**IMPORTANT!**
diff --git a/doc/update/3.1-to-4.0.md b/doc/update/3.1-to-4.0.md
index df53ed6de83..b370464390e 100644
--- a/doc/update/3.1-to-4.0.md
+++ b/doc/update/3.1-to-4.0.md
@@ -1,5 +1,5 @@
# From 3.1 to 4.0
-*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/3.1-to-4.0.md) for the most up to date instructions.*
+*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/3.1-to-4.0.md) for the most up to date instructions.*
## Important changes
diff --git a/doc/update/4.0-to-4.1.md b/doc/update/4.0-to-4.1.md
index c66c6dd0fd8..7124424bb60 100644
--- a/doc/update/4.0-to-4.1.md
+++ b/doc/update/4.0-to-4.1.md
@@ -1,5 +1,5 @@
# From 4.0 to 4.1
-*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/4.0-to-4.1.md) for the most up to date instructions.*
+*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/4.0-to-4.1.md) for the most up to date instructions.*
## Important changes
diff --git a/doc/update/4.1-to-4.2.md b/doc/update/4.1-to-4.2.md
index 97367c5f347..8ed5b333a2e 100644
--- a/doc/update/4.1-to-4.2.md
+++ b/doc/update/4.1-to-4.2.md
@@ -1,5 +1,5 @@
# From 4.1 to 4.2
-*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/4.1-to-4.2.md) for the most up to date instructions.*
+*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/4.1-to-4.2.md) for the most up to date instructions.*
## 1. Stop server & Resque
diff --git a/doc/update/4.2-to-5.0.md b/doc/update/4.2-to-5.0.md
index 7654f4a0131..1ec39218ba8 100644
--- a/doc/update/4.2-to-5.0.md
+++ b/doc/update/4.2-to-5.0.md
@@ -1,5 +1,5 @@
# From 4.2 to 5.0
-*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/4.2-to-5.0.md) for the most up to date instructions.*
+*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/4.2-to-5.0.md) for the most up to date instructions.*
## Warning
diff --git a/doc/update/5.0-to-5.1.md b/doc/update/5.0-to-5.1.md
index c19a819ab5a..9c9950fb2c6 100644
--- a/doc/update/5.0-to-5.1.md
+++ b/doc/update/5.0-to-5.1.md
@@ -1,5 +1,5 @@
# From 5.0 to 5.1
-*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/5.0-to-5.1.md) for the most up to date instructions.*
+*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/5.0-to-5.1.md) for the most up to date instructions.*
## Warning
diff --git a/doc/update/5.1-to-5.2.md b/doc/update/5.1-to-5.2.md
index 625fcc33852..2aab47d2d7c 100644
--- a/doc/update/5.1-to-5.2.md
+++ b/doc/update/5.1-to-5.2.md
@@ -1,5 +1,5 @@
# From 5.1 to 5.2
-*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/5.1-to-5.2.md) for the most up to date instructions.*
+*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/5.1-to-5.2.md) for the most up to date instructions.*
## Warning
diff --git a/doc/update/5.1-to-5.4.md b/doc/update/5.1-to-5.4.md
index 547d453914c..e80f1b89c63 100644
--- a/doc/update/5.1-to-5.4.md
+++ b/doc/update/5.1-to-5.4.md
@@ -1,5 +1,5 @@
# From 5.1 to 5.4
-*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/5.1-to-5.4.md) for the most up to date instructions.*
+*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/5.1-to-5.4.md) for the most up to date instructions.*
Also works starting from 5.2.
diff --git a/doc/update/5.1-to-6.0.md b/doc/update/5.1-to-6.0.md
index c992c69678e..1ee175383da 100644
--- a/doc/update/5.1-to-6.0.md
+++ b/doc/update/5.1-to-6.0.md
@@ -1,5 +1,5 @@
# From 5.1 to 6.0
-*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/5.1-to-6.0.md) for the most up to date instructions.*
+*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/5.1-to-6.0.md) for the most up to date instructions.*
## Warning
diff --git a/doc/update/5.2-to-5.3.md b/doc/update/5.2-to-5.3.md
index fe8990b6843..2ae50510f63 100644
--- a/doc/update/5.2-to-5.3.md
+++ b/doc/update/5.2-to-5.3.md
@@ -1,5 +1,5 @@
# From 5.2 to 5.3
-*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/5.2-to-5.3.md) for the most up to date instructions.*
+*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/5.2-to-5.3.md) for the most up to date instructions.*
## Warning
diff --git a/doc/update/5.3-to-5.4.md b/doc/update/5.3-to-5.4.md
index 5f82ad7d444..842e3bb6791 100644
--- a/doc/update/5.3-to-5.4.md
+++ b/doc/update/5.3-to-5.4.md
@@ -1,5 +1,5 @@
# From 5.3 to 5.4
-*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/5.3-to-5.4.md) for the most up to date instructions.*
+*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/5.3-to-5.4.md) for the most up to date instructions.*
## 0. Backup
diff --git a/doc/update/5.4-to-6.0.md b/doc/update/5.4-to-6.0.md
index f0fee634322..44715984f0c 100644
--- a/doc/update/5.4-to-6.0.md
+++ b/doc/update/5.4-to-6.0.md
@@ -1,5 +1,5 @@
# From 5.4 to 6.0
-*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/5.4-to-6.0.md) for the most up to date instructions.*
+*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/5.4-to-6.0.md) for the most up to date instructions.*
## Warning
diff --git a/doc/update/6.0-to-6.1.md b/doc/update/6.0-to-6.1.md
index 409faf30902..0c672abeb05 100644
--- a/doc/update/6.0-to-6.1.md
+++ b/doc/update/6.0-to-6.1.md
@@ -1,5 +1,5 @@
# From 6.0 to 6.1
-*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/6.0-to-6.1.md) for the most up to date instructions.*
+*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/6.0-to-6.1.md) for the most up to date instructions.*
## Warning
diff --git a/doc/update/6.1-to-6.2.md b/doc/update/6.1-to-6.2.md
index 150c7ae1c83..d3760cf0619 100644
--- a/doc/update/6.1-to-6.2.md
+++ b/doc/update/6.1-to-6.2.md
@@ -1,5 +1,5 @@
# From 6.1 to 6.2
-*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/6.1-to-6.2.md) for the most up to date instructions.*
+*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/6.1-to-6.2.md) for the most up to date instructions.*
**You should update to 6.1 before installing 6.2 so all the necessary conversions are run.**
diff --git a/doc/update/6.2-to-6.3.md b/doc/update/6.2-to-6.3.md
index b96dfb8add7..91105de2e29 100644
--- a/doc/update/6.2-to-6.3.md
+++ b/doc/update/6.2-to-6.3.md
@@ -1,5 +1,5 @@
# From 6.2 to 6.3
-*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/6.2-to-6.3.md) for the most up to date instructions.*
+*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/6.2-to-6.3.md) for the most up to date instructions.*
**Requires version: 6.1 or 6.2.**
diff --git a/doc/update/6.3-to-6.4.md b/doc/update/6.3-to-6.4.md
index 37028be055f..20b58ed8b25 100644
--- a/doc/update/6.3-to-6.4.md
+++ b/doc/update/6.3-to-6.4.md
@@ -1,5 +1,5 @@
# From 6.3 to 6.4
-*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/6.3-to-6.4.md) for the most up to date instructions.*
+*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/6.3-to-6.4.md) for the most up to date instructions.*
## 0. Backup
diff --git a/doc/update/6.4-to-6.5.md b/doc/update/6.4-to-6.5.md
index 982381a4db0..5ee0f040b5d 100644
--- a/doc/update/6.4-to-6.5.md
+++ b/doc/update/6.4-to-6.5.md
@@ -1,5 +1,5 @@
# From 6.4 to 6.5
-*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/6.4-to-6.5.md) for the most up to date instructions.*
+*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/6.4-to-6.5.md) for the most up to date instructions.*
## 0. Backup
diff --git a/doc/update/6.5-to-6.6.md b/doc/update/6.5-to-6.6.md
index bbed2b30215..fa3712f83ad 100644
--- a/doc/update/6.5-to-6.6.md
+++ b/doc/update/6.5-to-6.6.md
@@ -1,5 +1,5 @@
# From 6.5 to 6.6
-*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/6.5-to-6.6.md) for the most up to date instructions.*
+*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/6.5-to-6.6.md) for the most up to date instructions.*
## 0. Backup
diff --git a/doc/update/6.6-to-6.7.md b/doc/update/6.6-to-6.7.md
index 8e82942a1a0..9c85ed091c5 100644
--- a/doc/update/6.6-to-6.7.md
+++ b/doc/update/6.6-to-6.7.md
@@ -1,5 +1,5 @@
# From 6.6 to 6.7
-*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/6.6-to-6.7.md) for the most up to date instructions.*
+*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/6.6-to-6.7.md) for the most up to date instructions.*
## 0. Backup
diff --git a/doc/update/6.7-to-6.8.md b/doc/update/6.7-to-6.8.md
index 4fb90639f16..687c1265d9b 100644
--- a/doc/update/6.7-to-6.8.md
+++ b/doc/update/6.7-to-6.8.md
@@ -1,5 +1,5 @@
# From 6.7 to 6.8
-*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/6.7-to-6.8.md) for the most up to date instructions.*
+*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/6.7-to-6.8.md) for the most up to date instructions.*
## 0. Backup
diff --git a/doc/update/6.8-to-6.9.md b/doc/update/6.8-to-6.9.md
index b9b8b63f652..0205b0c896a 100644
--- a/doc/update/6.8-to-6.9.md
+++ b/doc/update/6.8-to-6.9.md
@@ -1,5 +1,5 @@
# From 6.8 to 6.9
-*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/6.8-to-6.9.md) for the most up to date instructions.*
+*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/6.8-to-6.9.md) for the most up to date instructions.*
### 0. Backup
diff --git a/doc/update/6.9-to-7.0.md b/doc/update/6.9-to-7.0.md
index 5352fd52f93..4b6e3989893 100644
--- a/doc/update/6.9-to-7.0.md
+++ b/doc/update/6.9-to-7.0.md
@@ -1,5 +1,5 @@
# From 6.9 to 7.0
-*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/6.9-to-7.0.md) for the most up to date instructions.*
+*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/6.9-to-7.0.md) for the most up to date instructions.*
### 0. Backup
diff --git a/doc/update/6.x-or-7.x-to-7.14.md b/doc/update/6.x-or-7.x-to-7.14.md
index f170a0021b7..1e39fe47ef9 100644
--- a/doc/update/6.x-or-7.x-to-7.14.md
+++ b/doc/update/6.x-or-7.x-to-7.14.md
@@ -1,5 +1,5 @@
# From 6.x or 7.x to 7.14
-*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/6.x-or-7.x-to-7.14.md) for the most up to date instructions.*
+*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/6.x-or-7.x-to-7.14.md) for the most up to date instructions.*
This allows you to upgrade any version of GitLab from 6.0 and up (including 7.0 and up) to 7.14.
@@ -222,7 +222,7 @@ If all items are green, then congratulations upgrade complete!
When using Google omniauth login, changes of the Google account required.
Ensure that `Contacts API` and the `Google+ API` are enabled in the [Google Developers Console](https://console.developers.google.com/).
-More details can be found at the [integration documentation](../../../master/doc/integration/google.md).
+More details can be found at the [integration documentation](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/integration/google.md).
## 12. Optional optimizations for GitLab setups with MySQL databases
diff --git a/doc/update/7.0-to-7.1.md b/doc/update/7.0-to-7.1.md
index 71f39c44077..c717affebd3 100644
--- a/doc/update/7.0-to-7.1.md
+++ b/doc/update/7.0-to-7.1.md
@@ -1,5 +1,5 @@
# From 7.0 to 7.1
-*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/7.0-to-7.1.md) for the most up to date instructions.*
+*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/7.0-to-7.1.md) for the most up to date instructions.*
### 0. Backup
diff --git a/doc/update/7.1-to-7.2.md b/doc/update/7.1-to-7.2.md
index 88cb63d7d41..d01f8528e14 100644
--- a/doc/update/7.1-to-7.2.md
+++ b/doc/update/7.1-to-7.2.md
@@ -1,5 +1,5 @@
# From 7.1 to 7.2
-*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/7.1-to-7.2.md) for the most up to date instructions.*
+*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/7.1-to-7.2.md) for the most up to date instructions.*
## Editable labels
diff --git a/doc/update/7.2-to-7.3.md b/doc/update/7.2-to-7.3.md
index 18f77d6396e..0e91e682175 100644
--- a/doc/update/7.2-to-7.3.md
+++ b/doc/update/7.2-to-7.3.md
@@ -1,5 +1,5 @@
# From 7.2 to 7.3
-*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/7.2-to-7.3.md) for the most up to date instructions.*
+*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/7.2-to-7.3.md) for the most up to date instructions.*
### 0. Backup
diff --git a/doc/update/7.3-to-7.4.md b/doc/update/7.3-to-7.4.md
index 53e739c06fb..4df9127dd5f 100644
--- a/doc/update/7.3-to-7.4.md
+++ b/doc/update/7.3-to-7.4.md
@@ -1,5 +1,5 @@
# From 7.3 to 7.4
-*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/7.3-to-7.4.md) for the most up to date instructions.*
+*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/7.3-to-7.4.md) for the most up to date instructions.*
### 0. Stop server
diff --git a/doc/update/8.14-to-8.15.md b/doc/update/8.14-to-8.15.md
index 8d4bfd913bd..b1e3b116548 100644
--- a/doc/update/8.14-to-8.15.md
+++ b/doc/update/8.14-to-8.15.md
@@ -11,12 +11,15 @@ guide links by version.
### 1. Stop server
- sudo service gitlab stop
+```bash
+sudo service gitlab stop
+```
### 2. Backup
```bash
cd /home/git/gitlab
+
sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production
```
@@ -49,6 +52,8 @@ sudo gem install bundler --no-ri --no-rdoc
### 4. Get latest code
```bash
+cd /home/git/gitlab
+
sudo -u git -H git fetch --all
sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically
```
@@ -56,6 +61,8 @@ sudo -u git -H git checkout -- db/schema.rb # local changes will be restored aut
For GitLab Community Edition:
```bash
+cd /home/git/gitlab
+
sudo -u git -H git checkout 8-15-stable
```
@@ -64,28 +71,12 @@ OR
For GitLab Enterprise Edition:
```bash
-sudo -u git -H git checkout 8-15-stable-ee
-```
-
-### 5. Update gitlab-shell
-
-```bash
-cd /home/git/gitlab-shell
-sudo -u git -H git fetch --all --tags
-sudo -u git -H git checkout v4.1.1
-```
-
-### 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.
+cd /home/git/gitlab
-```bash
-sudo -u git -H bundle exec rake "gitlab:workhorse:install[/home/git/gitlab-workhorse]" RAILS_ENV=production
+sudo -u git -H git checkout 8-15-stable-ee
```
-### 7. Install libs, migrations, etc.
+### 5. Install libs, migrations, etc.
```bash
cd /home/git/gitlab
@@ -106,6 +97,27 @@ sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production
sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS_ENV=production
```
+### 6. Update gitlab-workhorse
+
+Install and compile gitlab-workhorse. This requires
+[Go 1.5](https://golang.org/dl) which should already be on your system from
+GitLab 8.1.
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake "gitlab:workhorse:install[/home/git/gitlab-workhorse]" RAILS_ENV=production
+```
+
+### 7. Update gitlab-shell
+
+```bash
+cd /home/git/gitlab-shell
+
+sudo -u git -H git fetch --all --tags
+sudo -u git -H git checkout v4.1.1
+```
+
### 8. Update configuration files
#### New configuration options for `gitlab.yml`
@@ -113,6 +125,8 @@ sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS
There are new configuration options available for [`gitlab.yml`](config/gitlab.yml.example). View them with the command below and apply them manually to your current `gitlab.yml`:
```sh
+cd /home/git/gitlab
+
git diff origin/8-14-stable:config/gitlab.yml.example origin/8-15-stable:config/gitlab.yml.example
```
@@ -122,6 +136,8 @@ Configure Git to generate packfile bitmaps (introduced in Git 2.0) on
the GitLab server during `git gc`.
```sh
+cd /home/git/gitlab
+
sudo -u git -H git config --global repack.writeBitmaps true
```
@@ -130,6 +146,8 @@ sudo -u git -H git config --global repack.writeBitmaps true
Ensure you're still up-to-date with the latest NGINX configuration changes:
```sh
+cd /home/git/gitlab
+
# For HTTPS configurations
git diff origin/8-14-stable:lib/support/nginx/gitlab-ssl origin/8-15-stable:lib/support/nginx/gitlab-ssl
@@ -162,26 +180,42 @@ See [smtp_settings.rb.sample] as an example.
Ensure you're still up-to-date with the latest init script changes:
- sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
+```bash
+cd /home/git/gitlab
+
+sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
+```
For Ubuntu 16.04.1 LTS:
- sudo systemctl daemon-reload
+```bash
+sudo systemctl daemon-reload
+```
### 9. Start application
- sudo service gitlab start
- sudo service nginx restart
+```bash
+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
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production
+```
To make sure you didn't miss anything run a more thorough check:
- sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production
+```
If all items are green, then congratulations, the upgrade is complete!
@@ -196,6 +230,7 @@ database migration (the backup is already migrated to the previous version).
```bash
cd /home/git/gitlab
+
sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production
```
diff --git a/doc/update/8.15-to-8.16.md b/doc/update/8.15-to-8.16.md
new file mode 100644
index 00000000000..2695a16ac0b
--- /dev/null
+++ b/doc/update/8.15-to-8.16.md
@@ -0,0 +1,239 @@
+# From 8.15 to 8.16
+
+Make sure you view this update guide from the tag (version) of GitLab you would
+like to install. In most cases this should be the highest numbered production
+tag (without rc in it). You can select the tag in the version dropdown at the
+top left corner of GitLab (below the menu bar).
+
+If the highest number stable branch is unclear please check the
+[GitLab Blog](https://about.gitlab.com/blog/archives.html) for installation
+guide links by version.
+
+### 1. Stop server
+
+```bash
+sudo service gitlab stop
+```
+
+### 2. Backup
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production
+```
+
+### 3. Update Ruby
+
+We will continue supporting Ruby < 2.3 for the time being but we recommend you
+upgrade to Ruby 2.3 if you're running a source installation, as this is the same
+version that ships with our Omnibus package.
+
+You can check which version you are running with `ruby -v`.
+
+Download and compile Ruby:
+
+```bash
+mkdir /tmp/ruby && cd /tmp/ruby
+curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.3/ruby-2.3.3.tar.gz
+echo '1014ee699071aa2ddd501907d18cbe15399c997d ruby-2.3.3.tar.gz' | shasum -c - && tar xzf ruby-2.3.3.tar.gz
+cd ruby-2.3.3
+./configure --disable-install-rdoc
+make
+sudo make install
+```
+
+Install Bundler:
+
+```bash
+sudo gem install bundler --no-ri --no-rdoc
+```
+
+### 4. Get latest code
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H git fetch --all
+sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically
+```
+
+For GitLab Community Edition:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H git checkout 8-16-stable
+```
+
+OR
+
+For GitLab Enterprise Edition:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H git checkout 8-16-stable-ee
+```
+
+### 5. Install libs, migrations, etc.
+
+```bash
+cd /home/git/gitlab
+
+# MySQL installations (note: the line below states '--without postgres')
+sudo -u git -H bundle install --without postgres development test --deployment
+
+# PostgreSQL installations (note: the line below states '--without mysql')
+sudo -u git -H bundle install --without mysql development test --deployment
+
+# Optional: clean up old gems
+sudo -u git -H bundle clean
+
+# Run database migrations
+sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production
+
+# Clean up assets and cache
+sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS_ENV=production
+```
+
+**MySQL installations**: Run through the `MySQL strings limits` and `Tables and data conversion to utf8mb4` [tasks](../install/database_mysql.md).
+
+### 6. Update gitlab-workhorse
+
+Install and compile gitlab-workhorse. This requires
+[Go 1.5](https://golang.org/dl) which should already be on your system from
+GitLab 8.1.
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake "gitlab:workhorse:install[/home/git/gitlab-workhorse]" RAILS_ENV=production
+```
+
+### 7. Update gitlab-shell
+
+```bash
+cd /home/git/gitlab-shell
+
+sudo -u git -H git fetch --all --tags
+sudo -u git -H git checkout v4.1.1
+```
+
+### 8. Update configuration files
+
+#### New configuration options for `gitlab.yml`
+
+There are new configuration options available for [`gitlab.yml`](config/gitlab.yml.example). View them with the command below and apply them manually to your current `gitlab.yml`:
+
+```sh
+cd /home/git/gitlab
+
+git diff origin/8-15-stable:config/gitlab.yml.example origin/8-16-stable:config/gitlab.yml.example
+```
+
+#### Git configuration
+
+Configure Git to generate packfile bitmaps (introduced in Git 2.0) on
+the GitLab server during `git gc`.
+
+```sh
+cd /home/git/gitlab
+
+sudo -u git -H git config --global repack.writeBitmaps true
+```
+
+#### Nginx configuration
+
+Ensure you're still up-to-date with the latest NGINX configuration changes:
+
+```sh
+cd /home/git/gitlab
+
+# For HTTPS configurations
+git diff origin/8-15-stable:lib/support/nginx/gitlab-ssl origin/8-16-stable:lib/support/nginx/gitlab-ssl
+
+# For HTTP configurations
+git diff origin/8-15-stable:lib/support/nginx/gitlab origin/8-16-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-16-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-16-stable/config/initializers/smtp_settings.rb.sample#L13
+
+#### Init script
+
+Ensure you're still up-to-date with the latest init script changes:
+
+```bash
+cd /home/git/gitlab
+
+sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
+```
+
+For Ubuntu 16.04.1 LTS:
+
+```bash
+sudo systemctl daemon-reload
+```
+
+### 9. Start application
+
+```bash
+sudo service gitlab start
+sudo service nginx restart
+```
+
+### 10. Check application status
+
+Check if GitLab and its environment are configured correctly:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production
+```
+
+To make sure you didn't miss anything run a more thorough check:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production
+```
+
+If all items are green, then congratulations, the upgrade is complete!
+
+## Things went south? Revert to previous version (8.15)
+
+### 1. Revert the code to the previous version
+
+Follow the [upgrade guide from 8.14 to 8.15](8.14-to-8.15.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.16-to-8.17.md b/doc/update/8.16-to-8.17.md
new file mode 100644
index 00000000000..1808232c59a
--- /dev/null
+++ b/doc/update/8.16-to-8.17.md
@@ -0,0 +1,239 @@
+# From 8.16 to 8.17
+
+Make sure you view this update guide from the tag (version) of GitLab you would
+like to install. In most cases this should be the highest numbered production
+tag (without rc in it). You can select the tag in the version dropdown at the
+top left corner of GitLab (below the menu bar).
+
+If the highest number stable branch is unclear please check the
+[GitLab Blog](https://about.gitlab.com/blog/archives.html) for installation
+guide links by version.
+
+### 1. Stop server
+
+```bash
+sudo service gitlab stop
+```
+
+### 2. Backup
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production
+```
+
+### 3. Update Ruby
+
+We will continue supporting Ruby < 2.3 for the time being but we recommend you
+upgrade to Ruby 2.3 if you're running a source installation, as this is the same
+version that ships with our Omnibus package.
+
+You can check which version you are running with `ruby -v`.
+
+Download and compile Ruby:
+
+```bash
+mkdir /tmp/ruby && cd /tmp/ruby
+curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.3/ruby-2.3.3.tar.gz
+echo '1014ee699071aa2ddd501907d18cbe15399c997d ruby-2.3.3.tar.gz' | shasum -c - && tar xzf ruby-2.3.3.tar.gz
+cd ruby-2.3.3
+./configure --disable-install-rdoc
+make
+sudo make install
+```
+
+Install Bundler:
+
+```bash
+sudo gem install bundler --no-ri --no-rdoc
+```
+
+### 4. Get latest code
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H git fetch --all
+sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically
+```
+
+For GitLab Community Edition:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H git checkout 8-17-stable
+```
+
+OR
+
+For GitLab Enterprise Edition:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H git checkout 8-17-stable-ee
+```
+
+### 5. Install libs, migrations, etc.
+
+```bash
+cd /home/git/gitlab
+
+# MySQL installations (note: the line below states '--without postgres')
+sudo -u git -H bundle install --without postgres development test --deployment
+
+# PostgreSQL installations (note: the line below states '--without mysql')
+sudo -u git -H bundle install --without mysql development test --deployment
+
+# Optional: clean up old gems
+sudo -u git -H bundle clean
+
+# Run database migrations
+sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production
+
+# Clean up assets and cache
+sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS_ENV=production
+```
+
+**MySQL installations**: Run through the `MySQL strings limits` and `Tables and data conversion to utf8mb4` [tasks](../install/database_mysql.md).
+
+### 6. Update gitlab-workhorse
+
+Install and compile gitlab-workhorse. This requires
+[Go 1.5](https://golang.org/dl) which should already be on your system from
+GitLab 8.1.
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake "gitlab:workhorse:install[/home/git/gitlab-workhorse]" RAILS_ENV=production
+```
+
+### 7. Update gitlab-shell
+
+```bash
+cd /home/git/gitlab-shell
+
+sudo -u git -H git fetch --all --tags
+sudo -u git -H git checkout v4.1.1
+```
+
+### 8. Update configuration files
+
+#### New configuration options for `gitlab.yml`
+
+There are new configuration options available for [`gitlab.yml`](config/gitlab.yml.example). View them with the command below and apply them manually to your current `gitlab.yml`:
+
+```sh
+cd /home/git/gitlab
+
+git diff origin/8-16-stable:config/gitlab.yml.example origin/8-17-stable:config/gitlab.yml.example
+```
+
+#### Git configuration
+
+Configure Git to generate packfile bitmaps (introduced in Git 2.0) on
+the GitLab server during `git gc`.
+
+```sh
+cd /home/git/gitlab
+
+sudo -u git -H git config --global repack.writeBitmaps true
+```
+
+#### Nginx configuration
+
+Ensure you're still up-to-date with the latest NGINX configuration changes:
+
+```sh
+cd /home/git/gitlab
+
+# For HTTPS configurations
+git diff origin/8-16-stable:lib/support/nginx/gitlab-ssl origin/8-17-stable:lib/support/nginx/gitlab-ssl
+
+# For HTTP configurations
+git diff origin/8-16-stable:lib/support/nginx/gitlab origin/8-17-stable:lib/support/nginx/gitlab
+```
+
+If you are using Apache instead of NGINX please see the updated [Apache templates].
+Also note that because Apache does not support upstreams behind Unix sockets you
+will need to let gitlab-workhorse listen on a TCP port. You can do this
+via [/etc/default/gitlab].
+
+[Apache templates]: https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server/apache
+[/etc/default/gitlab]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-17-stable/lib/support/init.d/gitlab.default.example#L38
+
+#### SMTP configuration
+
+If you're installing from source and use SMTP to deliver mail, you will need to add the following line
+to config/initializers/smtp_settings.rb:
+
+```ruby
+ActionMailer::Base.delivery_method = :smtp
+```
+
+See [smtp_settings.rb.sample] as an example.
+
+[smtp_settings.rb.sample]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-17-stable/config/initializers/smtp_settings.rb.sample#L13
+
+#### Init script
+
+Ensure you're still up-to-date with the latest init script changes:
+
+```bash
+cd /home/git/gitlab
+
+sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
+```
+
+For Ubuntu 16.04.1 LTS:
+
+```bash
+sudo systemctl daemon-reload
+```
+
+### 9. Start application
+
+```bash
+sudo service gitlab start
+sudo service nginx restart
+```
+
+### 10. Check application status
+
+Check if GitLab and its environment are configured correctly:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production
+```
+
+To make sure you didn't miss anything run a more thorough check:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production
+```
+
+If all items are green, then congratulations, the upgrade is complete!
+
+## Things went south? Revert to previous version (8.16)
+
+### 1. Revert the code to the previous version
+
+Follow the [upgrade guide from 8.15 to 8.16](8.15-to-8.16.md), except for the
+database migration (the backup is already migrated to the previous version).
+
+### 2. Restore from the backup
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production
+```
+
+If you have more than one backup `*.tar` file(s) please add `BACKUP=timestamp_of_backup` to the command above.
diff --git a/doc/update/patch_versions.md b/doc/update/patch_versions.md
index 685972cfb41..154a0f817da 100644
--- a/doc/update/patch_versions.md
+++ b/doc/update/patch_versions.md
@@ -14,6 +14,7 @@ user on the database version)
```bash
cd /home/git/gitlab
+
sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production
```
@@ -32,28 +33,13 @@ current version with `cat VERSION`).
```bash
cd /home/git/gitlab
+
sudo -u git -H git fetch --all
sudo -u git -H git checkout -- Gemfile.lock db/schema.rb
sudo -u git -H git checkout LATEST_TAG -b LATEST_TAG
```
-### 3. Update gitlab-shell to the corresponding version
-
-```bash
-cd /home/git/gitlab-shell
-sudo -u git -H git fetch
-sudo -u git -H git checkout v`cat /home/git/gitlab/GITLAB_SHELL_VERSION` -b v`cat /home/git/gitlab/GITLAB_SHELL_VERSION`
-```
-
-### 4. Update gitlab-workhorse to the corresponding version
-
-```bash
-cd /home/git/gitlab
-
-sudo -u git -H bundle exec rake "gitlab:workhorse:install[/home/git/gitlab-workhorse]" RAILS_ENV=production
-```
-
-### 5. Install libs, migrations, etc.
+### 3. Install libs, migrations, etc.
```bash
cd /home/git/gitlab
@@ -71,7 +57,24 @@ sudo -u git -H bundle clean
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
+sudo -u git -H bundle exec rake gitlab:assets:clean gitlab:assets:compile cache:clear RAILS_ENV=production
+```
+
+### 4. Update gitlab-workhorse to the corresponding version
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake "gitlab:workhorse:install[/home/git/gitlab-workhorse]" RAILS_ENV=production
+```
+
+### 5. Update gitlab-shell to the corresponding version
+
+```bash
+cd /home/git/gitlab-shell
+
+sudo -u git -H git fetch --all --tags
+sudo -u git -H git checkout v`cat /home/git/gitlab/GITLAB_SHELL_VERSION` -b v`cat /home/git/gitlab/GITLAB_SHELL_VERSION`
```
### 6. Start application
@@ -86,6 +89,8 @@ sudo service nginx restart
Check if GitLab and its environment are configured correctly:
```bash
+cd /home/git/gitlab
+
sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production
```
diff --git a/doc/user/account/security.md b/doc/user/account/security.md
index 816094bf8d2..2459f913583 100644
--- a/doc/user/account/security.md
+++ b/doc/user/account/security.md
@@ -1,3 +1 @@
-# Account Security
-
-- [Two-Factor Authentication](two_factor_authentication.md)
+This document was moved to [profile](../profile/index.md).
diff --git a/doc/user/account/two_factor_authentication.md b/doc/user/account/two_factor_authentication.md
index 881358ed94d..ea2c8307860 100644
--- a/doc/user/account/two_factor_authentication.md
+++ b/doc/user/account/two_factor_authentication.md
@@ -1,68 +1 @@
-# 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.
+This document was moved to [profile/account/two_factor_authentication](../profile/account/two_factor_authentication.md).
diff --git a/doc/user/markdown.md b/doc/user/markdown.md
index f6484688721..008872b59a7 100644
--- a/doc/user/markdown.md
+++ b/doc/user/markdown.md
@@ -300,6 +300,20 @@ You can add task lists to issues, merge requests and comments. To create a task
- [x] Sub-task 2
- [ ] Sub-task 3
+Tasks formatted as ordered lists are supported as well:
+
+```no-highlight
+1. [x] Completed task
+1. [ ] Incomplete task
+ 1. [ ] Sub-task 1
+ 1. [x] Sub-task 2
+```
+
+1. [x] Completed task
+1. [ ] Incomplete task
+ 1. [ ] Sub-task 1
+ 1. [x] Sub-task 2
+
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
@@ -650,7 +664,7 @@ This line is separated from the one above by two newlines, so it will be a *sepa
This line is also a separate paragraph, but...
This line is only separated by a single newline, so it's a separate line in the *same paragraph*.
-This line is also a separate paragraph, and...
+This line is also a separate paragraph, and...
This line is on its own line, because the previous line ends with two
spaces.
```
@@ -662,7 +676,7 @@ This line is separated from the one above by two newlines, so it will be a *sepa
This line is also begins a separate paragraph, but...
This line is only separated by a single newline, so it's a separate line in the *same paragraph*.
-This line is also a separate paragraph, and...
+This line is also a separate paragraph, and...
This line is on its own line, because the previous line ends with two
spaces.
@@ -800,4 +814,4 @@ A link starting with a `/` is relative to the wiki root.
[redcarpet]: https://github.com/vmg/redcarpet "Redcarpet website"
[katex]: https://github.com/Khan/KaTeX "KaTeX website"
[katex-subset]: https://github.com/Khan/KaTeX/wiki/Function-Support-in-KaTeX "Macros supported by KaTeX"
-[asciidoctor-manual]: http://asciidoctor.org/docs/user-manual/#activating-stem-support "Asciidoctor user manual" \ No newline at end of file
+[asciidoctor-manual]: http://asciidoctor.org/docs/user-manual/#activating-stem-support "Asciidoctor user manual"
diff --git a/doc/user/permissions.md b/doc/user/permissions.md
index 5ada8748d85..e87cae092a5 100644
--- a/doc/user/permissions.md
+++ b/doc/user/permissions.md
@@ -19,10 +19,12 @@ The following table depicts the various user permission levels in a project.
| Action | Guest | Reporter | Developer | Master | Owner |
|---------------------------------------|---------|------------|-------------|----------|--------|
| Create new issue | ✓ | ✓ | ✓ | ✓ | ✓ |
+| Create confidential issue | ✓ | ✓ | ✓ | ✓ | ✓ |
+| View confidential issues | (✓) [^1] | ✓ | ✓ | ✓ | ✓ |
| Leave comments | ✓ | ✓ | ✓ | ✓ | ✓ |
-| See a list of builds | ✓ [^1] | ✓ | ✓ | ✓ | ✓ |
-| See a build log | ✓ [^1] | ✓ | ✓ | ✓ | ✓ |
-| Download and browse build artifacts | ✓ [^1] | ✓ | ✓ | ✓ | ✓ |
+| See a list of builds | ✓ [^2] | ✓ | ✓ | ✓ | ✓ |
+| See a build log | ✓ [^2] | ✓ | ✓ | ✓ | ✓ |
+| Download and browse build artifacts | ✓ [^2] | ✓ | ✓ | ✓ | ✓ |
| View wiki pages | ✓ | ✓ | ✓ | ✓ | ✓ |
| Pull project code | | ✓ | ✓ | ✓ | ✓ |
| Download project | | ✓ | ✓ | ✓ | ✓ |
@@ -60,14 +62,14 @@ The following table depicts the various user permission levels in a project.
| Manage runners | | | | ✓ | ✓ |
| Manage build triggers | | | | ✓ | ✓ |
| Manage variables | | | | ✓ | ✓ |
+| Manage pages | | | | ✓ | ✓ |
+| Manage pages domains and certificates | | | | ✓ | ✓ |
| Switch visibility level | | | | | ✓ |
| Transfer project to another namespace | | | | | ✓ |
| Remove project | | | | | ✓ |
-| Force push to protected branches [^2] | | | | | |
-| Remove protected branches [^2] | | | | | |
-
-[^1]: If **Public pipelines** is enabled in **Project Settings > CI/CD Pipelines**
-[^2]: Not allowed for Guest, Reporter, Developer, Master, or Owner
+| Force push to protected branches [^3] | | | | | |
+| Remove protected branches [^3] | | | | | |
+| Remove pages | | | | | ✓ |
## Group
@@ -156,17 +158,20 @@ users:
| Run CI build | | ✓ | ✓ | ✓ |
| Clone source and LFS from current project | | ✓ | ✓ | ✓ |
| Clone source and LFS from public projects | | ✓ | ✓ | ✓ |
-| Clone source and LFS from internal projects | | ✓ [^3] | ✓ [^3] | ✓ |
-| Clone source and LFS from private projects | | ✓ [^4] | ✓ [^4] | ✓ [^4] |
+| Clone source and LFS from internal projects | | ✓ [^4] | ✓ [^4] | ✓ |
+| Clone source and LFS from private projects | | ✓ [^5] | ✓ [^5] | ✓ [^5] |
| Push source and LFS | | | | |
| Pull container images from current project | | ✓ | ✓ | ✓ |
| Pull container images from public projects | | ✓ | ✓ | ✓ |
-| Pull container images from internal projects| | ✓ [^3] | ✓ [^3] | ✓ |
-| Pull container images from private projects | | ✓ [^4] | ✓ [^4] | ✓ [^4] |
+| Pull container images from internal projects| | ✓ [^4] | ✓ [^4] | ✓ |
+| Pull container images from private projects | | ✓ [^5] | ✓ [^5] | ✓ [^5] |
| Push container images to current project | | ✓ | ✓ | ✓ |
| Push container images to other projects | | | | |
-[^3]: Only if user is not external one.
-[^4]: Only if user is a member of the project.
+[^1]: Guest users can only view the confidential issues they created themselves
+[^2]: If **Public pipelines** is enabled in **Project Settings > CI/CD Pipelines**
+[^3]: Not allowed for Guest, Reporter, Developer, Master, or Owner
+[^4]: Only if user is not external one.
+[^5]: Only if user is a member of the project.
[ce-18994]: https://gitlab.com/gitlab-org/gitlab-ce/issues/18994
[new-mod]: project/new_ci_build_permissions_model.md
diff --git a/doc/profile/2fa.png b/doc/user/profile/account/img/2fa.png
index bb464efa685..bb464efa685 100644
--- a/doc/profile/2fa.png
+++ b/doc/user/profile/account/img/2fa.png
Binary files differ
diff --git a/doc/profile/2fa_auth.png b/doc/user/profile/account/img/2fa_auth.png
index 0caaed10805..0caaed10805 100644
--- a/doc/profile/2fa_auth.png
+++ b/doc/user/profile/account/img/2fa_auth.png
Binary files differ
diff --git a/doc/user/profile/account/img/2fa_u2f_authenticate.png b/doc/user/profile/account/img/2fa_u2f_authenticate.png
new file mode 100644
index 00000000000..ff2e936764d
--- /dev/null
+++ b/doc/user/profile/account/img/2fa_u2f_authenticate.png
Binary files differ
diff --git a/doc/profile/2fa_u2f_register.png b/doc/user/profile/account/img/2fa_u2f_register.png
index 1cc142aa851..1cc142aa851 100644
--- a/doc/profile/2fa_u2f_register.png
+++ b/doc/user/profile/account/img/2fa_u2f_register.png
Binary files differ
diff --git a/doc/user/profile/account/index.md b/doc/user/profile/account/index.md
new file mode 100644
index 00000000000..764354e1e96
--- /dev/null
+++ b/doc/user/profile/account/index.md
@@ -0,0 +1,5 @@
+# Profile settings
+
+## Account
+
+Set up [two-factor authentication](two_factor_authentication.md).
diff --git a/doc/user/profile/account/two_factor_authentication.md b/doc/user/profile/account/two_factor_authentication.md
new file mode 100644
index 00000000000..a23ad79ae1d
--- /dev/null
+++ b/doc/user/profile/account/two_factor_authentication.md
@@ -0,0 +1,217 @@
+# Two-Factor Authentication
+
+Two-factor Authentication (2FA) provides an additional level of security to your
+GitLab account. Once enabled, in addition to supplying your username and
+password to login, you'll be prompted for a code generated by an application on
+your phone.
+
+By enabling 2FA, the only way someone other than you can log into your account
+is to know your username and password *and* have access to your phone.
+
+## Overview
+
+> **Note:**
+When you enable 2FA, don't forget to back up your recovery codes.
+
+In addition to a phone application, GitLab supports U2F (universal 2nd factor) devices as
+the second factor of authentication. Once enabled, in addition to supplying your username and
+password to login, you'll be prompted to activate your U2F device (usually by pressing
+a button on it), and it will perform secure authentication on your behalf.
+
+The U2F workflow is only supported by Google Chrome at this point, so we _strongly_ recommend
+that you set up both methods of two-factor authentication, so you can still access your account
+from other browsers.
+
+## Enabling 2FA
+
+There are two ways to enable two-factor authentication: via a mobile application
+or a U2F device.
+
+### Enable 2FA via mobile application
+
+**In GitLab:**
+
+1. Log in to your GitLab account.
+1. Go to your **Profile Settings**.
+1. Go to **Account**.
+1. Click **Enable Two-factor Authentication**.
+
+![Two-factor setup](img/2fa.png)
+
+**On your phone:**
+
+1. Install a compatible application. We recommend [Google Authenticator]
+\(proprietary\) or [FreeOTP] \(open source\).
+1. In the application, add a new entry in one of two ways:
+ * Scan the code with your phone's camera to add the entry automatically.
+ * Enter the details provided to add the entry manually.
+
+**In GitLab:**
+
+1. Enter the six-digit pin number from the entry on your phone into the **Pin
+ code** field.
+1. Click **Submit**.
+
+If the pin you entered was correct, you'll see a message indicating that
+Two-Factor Authentication has been enabled, and you'll be presented with a list
+of recovery codes.
+
+### Enable 2FA via U2F device
+
+> **Notes:**
+- GitLab officially only supports [Yubikey] U2F devices.
+- Support for U2F devices was added in GitLab 8.8.
+
+**In GitLab:**
+
+1. Log in to your GitLab account.
+1. Go to your **Profile Settings**.
+1. Go to **Account**.
+1. Click **Enable Two-Factor Authentication**.
+1. Plug in your U2F device.
+1. Click on **Setup New U2F Device**.
+1. A light will start blinking on your device. Activate it by pressing its button.
+
+You will see a message indicating that your device was successfully set up.
+Click on **Register U2F Device** to complete the process.
+
+![Two-Factor U2F Setup](img/2fa_u2f_register.png)
+
+## Recovery Codes
+
+> **Note:**
+Recovery codes are not generated for U2F devices.
+
+Should you ever lose access to your phone, you can use one of the ten provided
+backup codes to login to your account. We suggest copying or printing them for
+storage in a safe place. **Each code can be used only once** to log in to your
+account.
+
+If you lose the recovery codes or just want to generate new ones, you can do so
+from the **Profile settings ➔ Account** page where you first enabled 2FA.
+
+## Logging in with 2FA Enabled
+
+Logging in with 2FA enabled is only slightly different than a normal login.
+Enter your username and password credentials as you normally would, and you'll
+be presented with a second prompt, depending on which type of 2FA you've enabled.
+
+### Log in via mobile application
+
+Enter the pin from your phone's application or a recovery code to log in.
+
+![Two-Factor Authentication on sign in via OTP](img/2fa_auth.png)
+
+### Log in via U2F device
+
+1. Click **Login via U2F Device**
+1. A light will start blinking on your device. Activate it by pressing its button.
+
+You will see a message indicating that your device responded to the authentication request.
+Click on **Authenticate via U2F Device** to complete the process.
+
+![Two-Factor Authentication on sign in via U2F device](img/2fa_u2f_authenticate.png)
+
+## Disabling 2FA
+
+1. Log in to your GitLab account.
+1. Go to your **Profile Settings**.
+1. Go to **Account**.
+1. Click **Disable**, under **Two-Factor Authentication**.
+
+This will clear all your two-factor authentication registrations, including mobile
+applications and U2F devices.
+
+## Personal access tokens
+
+When 2FA is enabled, you can no longer use your normal account password to
+authenticate with Git over HTTPS on the command line, you must use a personal
+access token instead.
+
+1. Log in to your GitLab account.
+1. Go to your **Profile Settings**.
+1. Go to **Access Tokens**.
+1. Choose a name and expiry date for the token.
+1. Click on **Create Personal Access Token**.
+1. Save the personal access token somewhere safe.
+
+When using Git over HTTPS on the command line, enter the personal access token
+into the password field.
+
+## Recovery options
+
+To disable two-factor authentication on your account (for example, if you
+have lost your code generation device) you can:
+* [Use a saved recovery code](#use-a-saved-recovery-code)
+* [Generate new recovery codes using SSH](#generate-new-recovery-codes-using-SSH)
+* [Ask a GitLab administrator to disable two-factor authentication on your account](#ask-a-gitlab-administrator-to-disable-two-factor-authentication-on-your-account)
+
+### Use a saved recovery code
+
+Enabling two-factor authentication for your account generated several recovery
+codes. If you saved these codes, you can use one of them to sign in.
+
+To use a recovery code, enter your username/email and password on the GitLab
+sign-in page. When prompted for a two-factor code, enter the recovery code.
+
+> **Note:** Once you use a recovery code, you cannot re-use it. You can still
+ use the other recovery codes you saved.
+
+### Generate new recovery codes using SSH
+
+Users often forget to save their recovery codes when enabling two-factor
+authentication. If an SSH key is added to your GitLab account, you can generate
+a new set of recovery codes with SSH.
+
+1. Run `ssh git@gitlab.example.com 2fa_recovery_codes`.
+2. You are prompted to confirm that you want to generate new codes. Continuing this process invalidates previously saved codes.
+ ```
+ 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.
+ ```
+3. 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, visit your **Profile Settings -> Account** immediately to set up two-factor authentication with a new
+ device.
+
+### Ask a GitLab administrator to disable two-factor authentication on your account
+
+If you cannot use a saved recovery code or generate new recovery codes, ask a
+GitLab global administrator to disable two-factor authentication for your
+account. This will temporarily leave your account in a less secure state.
+Sign in and re-enable two-factor authentication as soon as possible.
+
+## Note to GitLab administrators
+
+- You need to take special care to that 2FA keeps working after
+[restoring a GitLab backup](../../../raketasks/backup_restore.md).
+
+- To ensure 2FA authorizes correctly with TOTP server, you may want to ensure
+your GitLab server's time is synchronized via a service like NTP. Otherwise,
+you may have cases where authorization always fails because of time differences.
+
+[Google Authenticator]: https://support.google.com/accounts/answer/1066447?hl=en
+[FreeOTP]: https://fedorahosted.org/freeotp/
+[YubiKey]: https://www.yubico.com/products/yubikey-hardware/
diff --git a/doc/user/project/container_registry.md b/doc/user/project/container_registry.md
index 47a4a3f85d0..91b35c73b34 100644
--- a/doc/user/project/container_registry.md
+++ b/doc/user/project/container_registry.md
@@ -1,10 +1,7 @@
# GitLab Container Registry
-> [Introduced][ce-4040] in GitLab 8.8.
-
----
-
>**Notes:**
+> [Introduced][ce-4040] in GitLab 8.8.
- Docker Registry manifest `v1` support was added in GitLab 8.9 to support Docker
versions earlier than 1.10.
- This document is about the user guide. To learn how to enable GitLab Container
@@ -98,8 +95,8 @@ delete them.
This feature requires GitLab 8.8 and GitLab Runner 1.2.
Make sure that your GitLab Runner is configured to allow building Docker images by
-following the [Using Docker Build](../ci/docker/using_docker_build.md)
-and [Using the GitLab Container Registry documentation](../ci/docker/using_docker_build.md#using-the-gitlab-container-registry).
+following the [Using Docker Build](../../ci/docker/using_docker_build.md)
+and [Using the GitLab Container Registry documentation](../../ci/docker/using_docker_build.md#using-the-gitlab-container-registry).
## Limitations
@@ -252,4 +249,4 @@ Once the right permissions were set, the error will go away.
[ce-4040]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4040
[docker-docs]: https://docs.docker.com/engine/userguide/intro/
-[private-docker]: https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/blob/master/docs/configuration/advanced-configuration.md#using-a-private-docker-registry
+[private-docker]: https://docs.gitlab.com/runner/configuration/advanced-configuration.html#using-a-private-docker-registry
diff --git a/doc/user/project/cycle_analytics.md b/doc/user/project/cycle_analytics.md
index 86fe52ef4ff..62afd8cf247 100644
--- a/doc/user/project/cycle_analytics.md
+++ b/doc/user/project/cycle_analytics.md
@@ -50,7 +50,7 @@ exception of the staging and production stages, where only data deployed to
production are measured.
Specifically, if your CI is not set up and you have not defined a `production`
-[environment], then you will not have any data for those stages.
+or `production/*` [environment], then you will not have any data for those stages.
Below you can see in more detail what the various stages of Cycle Analytics mean.
@@ -61,7 +61,7 @@ Below you can see in more detail what the various stages of Cycle Analytics mean
| Code | Measures the median time between pushing a first commit (previous stage) and creating a merge request (MR) related to that commit. The key to keep the process tracked is to include the [issue closing pattern] to the description of the merge request (for example, `Closes #xxx`, where `xxx` is the number of the issue related to this merge request). If the issue closing pattern is not present in the merge request description, the MR is not considered to the measurement time of the stage. |
| Test | Measures the median time to run the entire pipeline for that project. It's related to the time GitLab CI takes to run every job for the commits pushed to that merge request defined in the previous stage. It is basically the start->finish time for all pipelines. `master` is not excluded. It does not attempt to track time for any particular stages. |
| Review | Measures the median time taken to review the merge request, between its creation and until it's merged. |
-| Staging | Measures the median time between merging the merge request until the very first deployment to production. It's tracked by the [environment] set to `production` (case-sensitive, `Production` won't work) in your GitLab CI configuration. If there isn't a `production` environment, this is not tracked. |
+| Staging | Measures the median time between merging the merge request until the very first deployment to production. It's tracked by the [environment] set to `production` or matching `production/*` (case-sensitive, `Production` won't work) in your GitLab CI configuration. If there isn't a production environment, this is not tracked. |
| Production| The sum of all time (medians) taken to run the entire process, from issue creation to deploying the code to production. |
---
@@ -79,10 +79,13 @@ Here's a little explanation of how this works behind the scenes:
etc.
To sum up, anything that doesn't follow the [GitLab flow] won't be tracked at all.
-So, if a merge request doesn't close an issue or an issue is not labeled with a
-label present in the Issue Board or assigned a milestone or a project has no
-`production` environment (for staging and production stages), the Cycle Analytics
-dashboard won't present any data at all.
+So, the Cycle Analytics dashboard won't present any data:
+- For merge requests that do not close an issue.
+- For issues not labeled with a label present in the Issue Board.
+- For issues not assigned a milestone.
+- For staging and production stages, if the project has no `production` or `production/*`
+ environment.
+
## Example workflow
diff --git a/doc/user/project/integrations/bamboo.md b/doc/user/project/integrations/bamboo.md
new file mode 100644
index 00000000000..cad4757f287
--- /dev/null
+++ b/doc/user/project/integrations/bamboo.md
@@ -0,0 +1,59 @@
+# Atlassian Bamboo CI Service
+
+GitLab provides integration with Atlassian Bamboo for continuous integration.
+When configured, pushes to a project will trigger a build in Bamboo automatically.
+Merge requests will also display CI status showing whether the build is pending,
+failed, or completed successfully. It also provides a link to the Bamboo build
+page for more information.
+
+Bamboo doesn't quite provide the same features as a traditional build system when
+it comes to accepting webhooks and commit data. There are a few things that
+need to be configured in a Bamboo build plan before GitLab can integrate.
+
+## Setup
+
+### Complete these steps in Bamboo
+
+1. Navigate to a Bamboo build plan and choose 'Configure plan' from the 'Actions'
+ dropdown.
+1. Select the 'Triggers' tab.
+1. Click 'Add trigger'.
+1. Enter a description such as 'GitLab trigger'
+1. Choose 'Repository triggers the build when changes are committed'
+1. Check one or more repositories checkboxes
+1. Enter the GitLab IP address in the 'Trigger IP addresses' box. This is a
+ whitelist of IP addresses that are allowed to trigger Bamboo builds.
+1. Save the trigger.
+1. In the left pane, select a build stage. If you have multiple build stages
+ you want to select the last stage that contains the git checkout task.
+1. Select the 'Miscellaneous' tab.
+1. Under 'Pattern Match Labelling' put '${bamboo.repository.revision.number}'
+ in the 'Labels' box.
+1. Save
+
+Bamboo is now ready to accept triggers from GitLab. Next, set up the Bamboo
+service in GitLab.
+
+### Complete these steps in GitLab
+
+1. Navigate to the project you want to configure to trigger builds.
+1. Navigate to the [Integrations page](project_services.md#accessing-the-project-services)
+1. Click 'Atlassian Bamboo CI'
+1. Select the 'Active' checkbox.
+1. Enter the base URL of your Bamboo server. 'https://bamboo.example.com'
+1. Enter the build key from your Bamboo build plan. Build keys are a short,
+ all capital letter, identifier that is unique. It will be something like PR-BLD
+1. If necessary, enter username and password for a Bamboo user that has
+ access to trigger the build plan. Leave these fields blank if you do not require
+ authentication.
+1. Save or optionally click 'Test Settings'. Please note that 'Test Settings'
+ will actually trigger a build in Bamboo.
+
+## Troubleshooting
+
+If builds are not triggered, these are a couple of things to keep in mind.
+
+1. Ensure you entered the right GitLab IP address in Bamboo under 'Trigger
+ IP addresses'.
+1. Remember that GitLab only triggers builds on push events. A commit via the
+ web interface will not trigger CI currently.
diff --git a/doc/user/project/integrations/bugzilla.md b/doc/user/project/integrations/bugzilla.md
new file mode 100644
index 00000000000..0b219e84478
--- /dev/null
+++ b/doc/user/project/integrations/bugzilla.md
@@ -0,0 +1,18 @@
+# Bugzilla Service
+
+Navigate to the [Integrations page](project_services.md#accessing-the-project-services),
+select the **Bugzilla** service and fill in the required details as described
+in the table below.
+
+| Field | Description |
+| ----- | ----------- |
+| `description` | A name for the issue tracker (to differentiate between instances, for example) |
+| `project_url` | The URL to the project in Bugzilla which is being linked to this GitLab project. Note that the `project_url` requires PRODUCT_NAME to be updated with the product/project name in Bugzilla. |
+| `issues_url` | The URL to the issue in Bugzilla project that is linked to this GitLab project. Note that the `issues_url` requires `:id` in the URL. This ID is used by GitLab as a placeholder to replace the issue number. |
+| `new_issue_url` | This is the URL to create a new issue in Bugzilla for the project linked to this GitLab project. Note that the `new_issue_url` requires PRODUCT_NAME to be updated with the product/project name in Bugzilla. |
+
+Once you have configured and enabled Bugzilla:
+
+- the **Issues** link on the GitLab project pages takes you to the appropriate
+ Bugzilla product page
+- clicking **New issue** on the project dashboard takes you to Bugzilla for entering a new issue
diff --git a/doc/user/project/integrations/builds_emails.md b/doc/user/project/integrations/builds_emails.md
new file mode 100644
index 00000000000..f769dece242
--- /dev/null
+++ b/doc/user/project/integrations/builds_emails.md
@@ -0,0 +1,15 @@
+# Enabling build emails
+
+By enabling this service, you will be able to receive e-mail notifications about
+the result status of your builds.
+
+Navigate to the [Integrations page](project_services.md#accessing-the-project-services)
+and select the **Builds emails** service to configure it.
+
+In the _Recipients_ area, provide a list of e-mails separated by comma.
+
+Check the _Add pusher_ checkbox if you want the committer to also receive
+e-mail notifications about each build's status.
+
+If you enable the _Notify only broken builds_ option, e-mail notifications will
+be sent only for failed builds.
diff --git a/doc/user/project/integrations/emails_on_push.md b/doc/user/project/integrations/emails_on_push.md
new file mode 100644
index 00000000000..18109fc049c
--- /dev/null
+++ b/doc/user/project/integrations/emails_on_push.md
@@ -0,0 +1,20 @@
+# Enabling emails on push
+
+By enabling this service, you will be able to receive email notifications for
+every change that is pushed to your project.
+
+Navigate to the [Integrations page](project_services.md#accessing-the-project-services)
+and select the **Emails on push** service to configure it.
+
+In the _Recipients_ area, provide a list of emails separated by commas.
+
+You can configure any of the following settings depending on your preference.
+
++ **Push events** - Email will be triggered when a push event is recieved
++ **Tag push events** - Email will be triggered when a tag is created and pushed
++ **Send from committer** - Send notifications from the committer's email address if the domain is part of the domain GitLab is running on (e.g. `user@gitlab.com`).
++ **Disable code diffs** - Don't include possibly sensitive code diffs in notification body.
+
+---
+
+![Email on push service settings](img/emails_on_push_service.png)
diff --git a/doc/user/project/integrations/hipchat.md b/doc/user/project/integrations/hipchat.md
new file mode 100644
index 00000000000..eee779c50d4
--- /dev/null
+++ b/doc/user/project/integrations/hipchat.md
@@ -0,0 +1,53 @@
+# Atlassian HipChat
+
+GitLab provides a way to send HipChat notifications upon a number of events,
+such as when a user pushes code, creates a branch or tag, adds a comment, and
+creates a merge request.
+
+## Setup
+
+GitLab requires the use of a HipChat v2 API token to work. v1 tokens are
+not supported at this time. Note the differences between v1 and v2 tokens:
+
+HipChat v1 API (legacy) supports "API Auth Tokens" in the Group API menu. A v1
+token is allowed to send messages to *any* room.
+
+HipChat v2 API has tokens that are can be created using the Integrations tab
+in the Group or Room admin page. By design, these are lightweight tokens that
+allow GitLab to send messages only to *one* room.
+
+### Complete these steps in HipChat
+
+1. Go to: https://admin.hipchat.com/admin
+1. Click on "Group Admin" -> "Integrations".
+1. Find "Build Your Own!" and click "Create".
+1. Select the desired room, name the integration "GitLab", and click "Create".
+1. In the "Send messages to this room by posting this URL" column, you should
+see a URL in the format:
+
+```
+https://api.hipchat.com/v2/room/<room>/notification?auth_token=<token>
+```
+
+HipChat is now ready to accept messages from GitLab. Next, set up the HipChat
+service in GitLab.
+
+### Complete these steps in GitLab
+
+1. Navigate to the project you want to configure for notifications.
+1. Navigate to the [Integrations page](project_services.md#accessing-the-project-services)
+1. Click "HipChat".
+1. Select the "Active" checkbox.
+1. Insert the `token` field from the URL into the `Token` field on the Web page.
+1. Insert the `room` field from the URL into the `Room` field on the Web page.
+1. Save or optionally click "Test Settings".
+
+## Troubleshooting
+
+If you do not see notifications, make sure you are using a HipChat v2 API
+token, not a v1 token.
+
+Note that the v2 token is tied to a specific room. If you want to be able to
+specify arbitrary rooms, you can create an API token for a specific user in
+HipChat under "Account settings" and "API access". Use the `XXX` value under
+`auth_token=XXX`.
diff --git a/doc/user/project/integrations/img/accessing_integrations.png b/doc/user/project/integrations/img/accessing_integrations.png
new file mode 100644
index 00000000000..3b941f64998
--- /dev/null
+++ b/doc/user/project/integrations/img/accessing_integrations.png
Binary files differ
diff --git a/doc/project_services/img/emails_on_push_service.png b/doc/user/project/integrations/img/emails_on_push_service.png
index df301aa1eeb..df301aa1eeb 100644
--- a/doc/project_services/img/emails_on_push_service.png
+++ b/doc/user/project/integrations/img/emails_on_push_service.png
Binary files differ
diff --git a/doc/project_services/img/jira_add_user_to_group.png b/doc/user/project/integrations/img/jira_add_user_to_group.png
index 27dac49260c..27dac49260c 100644
--- a/doc/project_services/img/jira_add_user_to_group.png
+++ b/doc/user/project/integrations/img/jira_add_user_to_group.png
Binary files differ
diff --git a/doc/project_services/img/jira_create_new_group.png b/doc/user/project/integrations/img/jira_create_new_group.png
index 06c4e84fc61..06c4e84fc61 100644
--- a/doc/project_services/img/jira_create_new_group.png
+++ b/doc/user/project/integrations/img/jira_create_new_group.png
Binary files differ
diff --git a/doc/project_services/img/jira_create_new_group_name.png b/doc/user/project/integrations/img/jira_create_new_group_name.png
index bfc0dc6b2e9..bfc0dc6b2e9 100644
--- a/doc/project_services/img/jira_create_new_group_name.png
+++ b/doc/user/project/integrations/img/jira_create_new_group_name.png
Binary files differ
diff --git a/doc/project_services/img/jira_create_new_user.png b/doc/user/project/integrations/img/jira_create_new_user.png
index e9c03ed770d..e9c03ed770d 100644
--- a/doc/project_services/img/jira_create_new_user.png
+++ b/doc/user/project/integrations/img/jira_create_new_user.png
Binary files differ
diff --git a/doc/project_services/img/jira_group_access.png b/doc/user/project/integrations/img/jira_group_access.png
index 9d64cc57269..9d64cc57269 100644
--- a/doc/project_services/img/jira_group_access.png
+++ b/doc/user/project/integrations/img/jira_group_access.png
Binary files differ
diff --git a/doc/project_services/img/jira_issue_reference.png b/doc/user/project/integrations/img/jira_issue_reference.png
index 72c81460df7..72c81460df7 100644
--- a/doc/project_services/img/jira_issue_reference.png
+++ b/doc/user/project/integrations/img/jira_issue_reference.png
Binary files differ
diff --git a/doc/project_services/img/jira_merge_request_close.png b/doc/user/project/integrations/img/jira_merge_request_close.png
index 0f82ceba557..0f82ceba557 100644
--- a/doc/project_services/img/jira_merge_request_close.png
+++ b/doc/user/project/integrations/img/jira_merge_request_close.png
Binary files differ
diff --git a/doc/project_services/img/jira_project_name.png b/doc/user/project/integrations/img/jira_project_name.png
index 8540a427461..8540a427461 100644
--- a/doc/project_services/img/jira_project_name.png
+++ b/doc/user/project/integrations/img/jira_project_name.png
Binary files differ
diff --git a/doc/project_services/img/jira_service.png b/doc/user/project/integrations/img/jira_service.png
index 8e073b84ff9..8e073b84ff9 100644
--- a/doc/project_services/img/jira_service.png
+++ b/doc/user/project/integrations/img/jira_service.png
Binary files differ
diff --git a/doc/project_services/img/jira_service_close_comment.png b/doc/user/project/integrations/img/jira_service_close_comment.png
index bb9cd7e3d13..bb9cd7e3d13 100644
--- a/doc/project_services/img/jira_service_close_comment.png
+++ b/doc/user/project/integrations/img/jira_service_close_comment.png
Binary files differ
diff --git a/doc/project_services/img/jira_service_close_issue.png b/doc/user/project/integrations/img/jira_service_close_issue.png
index c85b1d1dd97..c85b1d1dd97 100644
--- a/doc/project_services/img/jira_service_close_issue.png
+++ b/doc/user/project/integrations/img/jira_service_close_issue.png
Binary files differ
diff --git a/doc/project_services/img/jira_service_page.png b/doc/user/project/integrations/img/jira_service_page.png
index c74351b57b8..c74351b57b8 100644
--- a/doc/project_services/img/jira_service_page.png
+++ b/doc/user/project/integrations/img/jira_service_page.png
Binary files differ
diff --git a/doc/project_services/img/jira_user_management_link.png b/doc/user/project/integrations/img/jira_user_management_link.png
index f81c5b5fc87..f81c5b5fc87 100644
--- a/doc/project_services/img/jira_user_management_link.png
+++ b/doc/user/project/integrations/img/jira_user_management_link.png
Binary files differ
diff --git a/doc/project_services/img/jira_workflow_screenshot.png b/doc/user/project/integrations/img/jira_workflow_screenshot.png
index e62fb202613..e62fb202613 100644
--- a/doc/project_services/img/jira_workflow_screenshot.png
+++ b/doc/user/project/integrations/img/jira_workflow_screenshot.png
Binary files differ
diff --git a/doc/project_services/img/kubernetes_configuration.png b/doc/user/project/integrations/img/kubernetes_configuration.png
index 349a2dc8456..349a2dc8456 100644
--- a/doc/project_services/img/kubernetes_configuration.png
+++ b/doc/user/project/integrations/img/kubernetes_configuration.png
Binary files differ
diff --git a/doc/project_services/img/mattermost_add_slash_command.png b/doc/user/project/integrations/img/mattermost_add_slash_command.png
index 7759efa183c..7759efa183c 100644
--- a/doc/project_services/img/mattermost_add_slash_command.png
+++ b/doc/user/project/integrations/img/mattermost_add_slash_command.png
Binary files differ
diff --git a/doc/project_services/img/mattermost_bot_auth.png b/doc/user/project/integrations/img/mattermost_bot_auth.png
index 830b7849f3d..830b7849f3d 100644
--- a/doc/project_services/img/mattermost_bot_auth.png
+++ b/doc/user/project/integrations/img/mattermost_bot_auth.png
Binary files differ
diff --git a/doc/project_services/img/mattermost_bot_available_commands.png b/doc/user/project/integrations/img/mattermost_bot_available_commands.png
index b51798cf10d..b51798cf10d 100644
--- a/doc/project_services/img/mattermost_bot_available_commands.png
+++ b/doc/user/project/integrations/img/mattermost_bot_available_commands.png
Binary files differ
diff --git a/doc/user/project/integrations/img/mattermost_config_help.png b/doc/user/project/integrations/img/mattermost_config_help.png
new file mode 100644
index 00000000000..dd3481bc1f6
--- /dev/null
+++ b/doc/user/project/integrations/img/mattermost_config_help.png
Binary files differ
diff --git a/doc/project_services/img/mattermost_configuration.png b/doc/user/project/integrations/img/mattermost_configuration.png
index 3c5ff5ee317..3c5ff5ee317 100644
--- a/doc/project_services/img/mattermost_configuration.png
+++ b/doc/user/project/integrations/img/mattermost_configuration.png
Binary files differ
diff --git a/doc/project_services/img/mattermost_console_integrations.png b/doc/user/project/integrations/img/mattermost_console_integrations.png
index 92a30da5be0..92a30da5be0 100644
--- a/doc/project_services/img/mattermost_console_integrations.png
+++ b/doc/user/project/integrations/img/mattermost_console_integrations.png
Binary files differ
diff --git a/doc/project_services/img/mattermost_gitlab_token.png b/doc/user/project/integrations/img/mattermost_gitlab_token.png
index 257018914d2..257018914d2 100644
--- a/doc/project_services/img/mattermost_gitlab_token.png
+++ b/doc/user/project/integrations/img/mattermost_gitlab_token.png
Binary files differ
diff --git a/doc/project_services/img/mattermost_goto_console.png b/doc/user/project/integrations/img/mattermost_goto_console.png
index 3354c2a24b4..3354c2a24b4 100644
--- a/doc/project_services/img/mattermost_goto_console.png
+++ b/doc/user/project/integrations/img/mattermost_goto_console.png
Binary files differ
diff --git a/doc/project_services/img/mattermost_slash_command_configuration.png b/doc/user/project/integrations/img/mattermost_slash_command_configuration.png
index 12766ab2b34..12766ab2b34 100644
--- a/doc/project_services/img/mattermost_slash_command_configuration.png
+++ b/doc/user/project/integrations/img/mattermost_slash_command_configuration.png
Binary files differ
diff --git a/doc/project_services/img/mattermost_slash_command_token.png b/doc/user/project/integrations/img/mattermost_slash_command_token.png
index c38f37c203c..c38f37c203c 100644
--- a/doc/project_services/img/mattermost_slash_command_token.png
+++ b/doc/user/project/integrations/img/mattermost_slash_command_token.png
Binary files differ
diff --git a/doc/project_services/img/mattermost_team_integrations.png b/doc/user/project/integrations/img/mattermost_team_integrations.png
index 69d4a231e5a..69d4a231e5a 100644
--- a/doc/project_services/img/mattermost_team_integrations.png
+++ b/doc/user/project/integrations/img/mattermost_team_integrations.png
Binary files differ
diff --git a/doc/user/project/integrations/img/project_services.png b/doc/user/project/integrations/img/project_services.png
new file mode 100644
index 00000000000..25b6cd5690b
--- /dev/null
+++ b/doc/user/project/integrations/img/project_services.png
Binary files differ
diff --git a/doc/project_services/img/redmine_configuration.png b/doc/user/project/integrations/img/redmine_configuration.png
index 7b6dd271401..7b6dd271401 100644
--- a/doc/project_services/img/redmine_configuration.png
+++ b/doc/user/project/integrations/img/redmine_configuration.png
Binary files differ
diff --git a/doc/project_services/img/services_templates_redmine_example.png b/doc/user/project/integrations/img/services_templates_redmine_example.png
index 50d20510daf..50d20510daf 100644
--- a/doc/project_services/img/services_templates_redmine_example.png
+++ b/doc/user/project/integrations/img/services_templates_redmine_example.png
Binary files differ
diff --git a/doc/project_services/img/slack_configuration.png b/doc/user/project/integrations/img/slack_configuration.png
index fc8e58e686b..fc8e58e686b 100644
--- a/doc/project_services/img/slack_configuration.png
+++ b/doc/user/project/integrations/img/slack_configuration.png
Binary files differ
diff --git a/doc/user/project/integrations/img/slack_setup.png b/doc/user/project/integrations/img/slack_setup.png
new file mode 100644
index 00000000000..7928fb7d495
--- /dev/null
+++ b/doc/user/project/integrations/img/slack_setup.png
Binary files differ
diff --git a/doc/web_hooks/ssl.png b/doc/user/project/integrations/img/webhooks_ssl.png
index 21ddec4ebdf..21ddec4ebdf 100644
--- a/doc/web_hooks/ssl.png
+++ b/doc/user/project/integrations/img/webhooks_ssl.png
Binary files differ
diff --git a/doc/user/project/integrations/index.md b/doc/user/project/integrations/index.md
new file mode 100644
index 00000000000..99093ebaed5
--- /dev/null
+++ b/doc/user/project/integrations/index.md
@@ -0,0 +1,26 @@
+# Project integrations
+
+You can find the available integrations under the **Integrations** page by
+navigating to the cog icon in the upper right corner of your project. You need
+to have at least [master permission][permissions] on the project.
+
+![Accessing the integrations](img/accessing_integrations.png)
+
+## Project services
+
+Project services allow you to integrate GitLab with other applications.
+They are a bit like plugins in that they allow a lot of freedom in
+adding functionality to GitLab.
+
+[Learn more about project services.](project_services.md)
+
+## Project webhooks
+
+Project webhooks allow you to trigger a URL if for example new code is pushed or
+a new issue is created. You can configure webhooks to listen for specific events
+like pushes, issues or merge requests. GitLab will send a POST request with data
+to the webhook URL.
+
+[Learn more about webhooks.](webhooks.md)
+
+[permissions]: ../../permissions.md
diff --git a/doc/user/project/integrations/irker.md b/doc/user/project/integrations/irker.md
new file mode 100644
index 00000000000..c63ea1316fe
--- /dev/null
+++ b/doc/user/project/integrations/irker.md
@@ -0,0 +1,50 @@
+# Irker IRC Gateway
+
+GitLab provides a way to push update messages to an Irker server. When
+configured, pushes to a project will trigger the service to send data directly
+to the Irker server.
+
+See the project homepage for further info: https://gitlab.com/esr/irker
+
+## Needed setup
+
+You will first need an Irker daemon. You can download the Irker code from its
+repository on https://gitlab.com/esr/irker:
+
+```
+git clone https://gitlab.com/esr/irker.git
+```
+
+Once you have downloaded the code, you can run the python script named `irkerd`.
+This script is the gateway script, it acts both as an IRC client, for sending
+messages to an IRC server obviously, and as a TCP server, for receiving messages
+from the GitLab service.
+
+If the Irker server runs on the same machine, you are done. If not, you will
+need to follow the firsts steps of the next section.
+
+## Complete these steps in GitLab
+
+1. Navigate to the project you want to configure for notifications.
+1. Navigate to the [Integrations page](project_services.md#accessing-the-project-services)
+1. Click "Irker".
+1. Select the "Active" checkbox.
+1. Enter the server host address where `irkerd` runs (defaults to `localhost`)
+in the `Server host` field on the Web page
+1. Enter the server port of `irkerd` (e.g. defaults to 6659) in the
+`Server port` field on the Web page.
+1. Optional: if `Default IRC URI` is set, it has to be in the format
+`irc[s]://domain.name` and will be prepend to each and every channel provided
+by the user which is not a full URI.
+1. Specify the recipients (e.g. #channel1, user1, etc.)
+1. Save or optionally click "Test Settings".
+
+## Note on Irker recipients
+
+Irker accepts channel names of the form `chan` and `#chan`, both for the
+`#chan` channel. If you want to send messages in query, you will need to add
+`,isnick` after the channel name, in this form: `Aorimn,isnick`. In this latter
+case, `Aorimn` is treated as a nick and no more as a channel name.
+
+Irker can also join password-protected channels. Users need to append
+`?key=thesecretpassword` to the chan name.
diff --git a/doc/user/project/integrations/jira.md b/doc/user/project/integrations/jira.md
new file mode 100644
index 00000000000..4c64d1e0907
--- /dev/null
+++ b/doc/user/project/integrations/jira.md
@@ -0,0 +1,209 @@
+# GitLab JIRA integration
+
+GitLab can be configured to interact with JIRA. Configuration happens via
+user name and password. Connecting to a JIRA server via CAS is not possible.
+
+Each project can be configured to connect to a different JIRA instance, see the
+[configuration](#configuration) section. If you have one JIRA instance you can
+pre-fill the settings page with a default template. To configure the template
+see the [Services Templates][services-templates] document.
+
+Once the project is connected to JIRA, you can reference and close the issues
+in JIRA directly from GitLab.
+
+## Configuration
+
+In order to enable the JIRA service in GitLab, you need to first configure the
+project in JIRA and then enter the correct values in GitLab.
+
+### Configuring JIRA
+
+We need to create a user in JIRA which will have access to all projects that
+need to integrate with GitLab. Login to your JIRA instance as admin and under
+Administration go to User Management and create a new user.
+
+As an example, we'll create a user named `gitlab` and add it to `JIRA-developers`
+group.
+
+**It is important that the user `GitLab` has write-access to projects in JIRA**
+
+We have split this stage in steps so it is easier to follow.
+
+---
+
+1. Login to your JIRA instance as an administrator and under **Administration**
+ go to **User Management** to create a new user.
+
+ ![JIRA user management link](img/jira_user_management_link.png)
+
+ ---
+
+1. The next step is to create a new user (e.g., `gitlab`) who has write access
+ to projects in JIRA. Enter the user's name and a _valid_ e-mail address
+ since JIRA sends a verification e-mail to set-up the password.
+ _**Note:** JIRA creates the username automatically by using the e-mail
+ prefix. You can change it later if you want._
+
+ ![JIRA create new user](img/jira_create_new_user.png)
+
+ ---
+
+1. Now, let's create a `gitlab-developers` group which will have write access
+ to projects in JIRA. Go to the **Groups** tab and select **Create group**.
+
+ ![JIRA create new user](img/jira_create_new_group.png)
+
+ ---
+
+ Give it an optional description and hit **Create group**.
+
+ ![jira create new group](img/jira_create_new_group_name.png)
+
+ ---
+
+1. Give the newly-created group write access by going to
+ **Application access ➔ View configuration** and adding the `gitlab-developers`
+ group to JIRA Core.
+
+ ![JIRA group access](img/jira_group_access.png)
+
+ ---
+
+1. Add the `gitlab` user to the `gitlab-developers` group by going to
+ **Users ➔ GitLab user ➔ Add group** and selecting the `gitlab-developers`
+ group from the dropdown menu. Notice that the group says _Access_ which is
+ what we aim for.
+
+ ![JIRA add user to group](img/jira_add_user_to_group.png)
+
+---
+
+The JIRA configuration is over. Write down the new JIRA username and its
+password as they will be needed when configuring GitLab in the next section.
+
+### Configuring GitLab
+
+>**Notes:**
+- The currently supported JIRA versions are `v6.x` and `v7.x.`. GitLab 7.8 or
+ higher is required.
+- GitLab 8.14 introduced a new way to integrate with JIRA which greatly simplified
+ the configuration options you have to enter. If you are using an older version,
+ [follow this documentation][jira-repo-old-docs].
+
+To enable JIRA integration in a project, navigate to the
+[Integrations page](project_services.md#accessing-the-project-services), click
+the **JIRA** service, and fill in the required details on the page as described
+in the table below.
+
+| Field | Description |
+| ----- | ----------- |
+| `URL` | The base URL to the JIRA project which is being linked to this GitLab project. E.g., `https://jira.example.com`. |
+| `Project key` | The short identifier for your JIRA project, all uppercase, e.g., `PROJ`. |
+| `Username` | The user name created in [configuring JIRA step](#configuring-jira). |
+| `Password` |The password of the user created in [configuring JIRA step](#configuring-jira). |
+| `JIRA issue transition` | This is the ID of a transition that moves issues to a closed state. You can find this number under JIRA workflow administration ([see screenshot](img/jira_workflow_screenshot.png)). |
+
+After saving the configuration, your GitLab project will be able to interact
+with the linked JIRA project.
+
+![JIRA service page](img/jira_service_page.png)
+
+---
+
+## JIRA issues
+
+By now you should have [configured JIRA](#configuring-jira) and enabled the
+[JIRA service in GitLab](#configuring-gitlab). If everything is set up correctly
+you should be able to reference and close JIRA issues by just mentioning their
+ID in GitLab commits and merge requests.
+
+### Referencing JIRA Issues
+
+When GitLab project has JIRA issue tracker configured and enabled, mentioning
+JIRA issue in GitLab will automatically add a comment in JIRA issue with the
+link back to GitLab. This means that in comments in merge requests and commits
+referencing an issue, e.g., `PROJECT-7`, will add a comment in JIRA issue in the
+format:
+
+```
+USER mentioned this issue in RESOURCE_NAME of [PROJECT_NAME|LINK_TO_COMMENT]:
+ENTITY_TITLE
+```
+
+* `USER` A user that mentioned the issue. This is the link to the user profile in GitLab.
+* `LINK_TO_THE_COMMENT` Link to the origin of mention with a name of the entity where JIRA issue was mentioned.
+* `RESOURCE_NAME` Kind of resource which referenced the issue. Can be a commit or merge request.
+* `PROJECT_NAME` GitLab project name.
+* `ENTITY_TITLE` Merge request title or commit message first line.
+
+![example of mentioning or closing the JIRA issue](img/jira_issue_reference.png)
+
+---
+
+### Closing JIRA Issues
+
+JIRA issues can be closed directly from GitLab by using trigger words in
+commits and merge requests. When a commit which contains the trigger word
+followed by the JIRA issue ID in the commit message is pushed, GitLab will
+add a comment in the mentioned JIRA issue and immediately close it (provided
+the transition ID was set up correctly).
+
+There are currently three trigger words, and you can use either one to achieve
+the same goal:
+
+- `Resolves PROJECT-1`
+- `Closes PROJECT-1`
+- `Fixes PROJECT-1`
+
+where `PROJECT-1` is the issue ID of the JIRA project.
+
+### JIRA issue closing example
+
+Let's consider the following example:
+
+1. For the project named `PROJECT` in JIRA, we implemented a new feature
+ and created a merge request in GitLab.
+1. This feature was requested in JIRA issue `PROJECT-7` and the merge request
+ in GitLab contains the improvement
+1. In the merge request description we use the issue closing trigger
+ `Closes PROJECT-7`.
+1. Once the merge request is merged, the JIRA issue will be automatically closed
+ with a comment and an associated link to the commit that resolved the issue.
+
+---
+
+In the following screenshot you can see what the link references to the JIRA
+issue look like.
+
+![A Git commit that causes the JIRA issue to be closed](img/jira_merge_request_close.png)
+
+---
+
+Once this merge request is merged, the JIRA issue will be automatically closed
+with a link to the commit that resolved the issue.
+
+![The GitLab integration closes JIRA issue](img/jira_service_close_issue.png)
+
+---
+
+![The GitLab integration creates a comment and a link on JIRA issue.](img/jira_service_close_comment.png)
+
+## Troubleshooting
+
+If things don't work as expected that's usually because you have configured
+incorrectly the JIRA-GitLab integration.
+
+### GitLab is unable to comment on a ticket
+
+Make sure that the user you set up for GitLab to communicate with JIRA has the
+correct access permission to post comments on a ticket and to also transition
+the ticket, if you'd like GitLab to also take care of closing them.
+JIRA issue references and update comments will not work if the GitLab issue tracker is disabled.
+
+### GitLab is unable to close a ticket
+
+Make sure the `Transition ID` you set within the JIRA settings matches the one
+your project needs to close a ticket.
+
+[services-templates]: services_templates.md
+[jira-repo-old-docs]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-13-stable/doc/project_services/jira.md
diff --git a/doc/user/project/integrations/kubernetes.md b/doc/user/project/integrations/kubernetes.md
new file mode 100644
index 00000000000..cc67e667472
--- /dev/null
+++ b/doc/user/project/integrations/kubernetes.md
@@ -0,0 +1,66 @@
+# GitLab Kubernetes / OpenShift integration
+
+GitLab can be configured to interact with Kubernetes, or other systems using the
+Kubernetes API (such as OpenShift).
+
+Each project can be configured to connect to a different Kubernetes cluster, see
+the [configuration](#configuration) section.
+
+If you have a single cluster that you want to use for all your projects,
+you can pre-fill the settings page with a default template. To configure the
+template, see the [Services Templates](services_templates.md) document.
+
+## Configuration
+
+Navigate to the [Integrations page](project_services.md#accessing-the-project-services)
+of your project and select the **Kubernetes** service to configure it.
+
+![Kubernetes configuration settings](img/kubernetes_configuration.png)
+
+The Kubernetes service takes the following arguments:
+
+1. Kubernetes namespace
+1. API URL
+1. Service token
+1. Custom CA bundle
+
+The API URL is the URL that GitLab uses to access the Kubernetes API. Kubernetes
+exposes several APIs - we want the "base" URL that is common to all of them,
+e.g., `https://kubernetes.example.com` rather than `https://kubernetes.example.com/api/v1`.
+
+GitLab authenticates against Kubernetes using service tokens, which are
+scoped to a particular `namespace`. If you don't have a service token yet,
+you can follow the
+[Kubernetes documentation](http://kubernetes.io/docs/user-guide/service-accounts/)
+to create one. You can also view or create service tokens in the
+[Kubernetes dashboard](http://kubernetes.io/docs/user-guide/ui/) - visit
+`Config -> Secrets`.
+
+Fill in the service token and namespace according to the values you just got.
+If the API is using a self-signed TLS certificate, you'll also need to include
+the `ca.crt` contents as the `Custom CA bundle`.
+
+## Deployment variables
+
+The Kubernetes service exposes following
+[deployment variables](../../../ci/variables/README.md#deployment-variables) in the
+GitLab CI build environment:
+
+- `KUBE_URL` - equal to the API URL
+- `KUBE_TOKEN`
+- `KUBE_NAMESPACE`
+- `KUBE_CA_PEM` - only if a custom CA bundle was specified
+
+## Web terminals
+
+>**NOTE:**
+Added in GitLab 8.15. You must be the project owner or have `master` permissions
+to use terminals. Support is currently limited to the first container in the
+first pod of your environment.
+
+When enabled, the Kubernetes service adds [web terminal](../../../ci/environments.md#web-terminals)
+support to your environments. This is based on the `exec` functionality found in
+Docker and Kubernetes, so you get a new shell session within your existing
+containers. To use this integration, you should deploy to Kubernetes using
+the deployment variables above, ensuring any pods you create are labelled with
+`app=$CI_ENVIRONMENT_SLUG`. GitLab will do the rest!
diff --git a/doc/user/project/integrations/mattermost.md b/doc/user/project/integrations/mattermost.md
new file mode 100644
index 00000000000..09ba9994d3a
--- /dev/null
+++ b/doc/user/project/integrations/mattermost.md
@@ -0,0 +1,46 @@
+# Mattermost Notifications Service
+
+## On Mattermost
+
+To enable Mattermost integration you must create an incoming webhook integration:
+
+1. Sign in to your Mattermost instance
+1. Visit incoming webhooks, that will be something like: https://mattermost.example/your_team_name/integrations/incoming_webhooks/add
+1. Choose a display name, description and channel, those can be overridden on GitLab
+1. Save it, copy the **Webhook URL**, we'll need this later for GitLab.
+
+There might be some cases that Incoming Webhooks are blocked by admin, ask your mattermost admin to enable
+it on https://mattermost.example/admin_console/integrations/custom.
+
+Display name override is not enabled by default, you need to ask your admin to enable it on that same section.
+
+## On GitLab
+
+After you set up Mattermost, it's time to set up GitLab.
+
+Navigate to the [Integrations page](project_services.md#accessing-the-project-services)
+and select the **Mattermost notifications** service to configure it.
+There, you will see a checkbox with the following events that can be triggered:
+
+- Push
+- Issue
+- Merge request
+- Note
+- Tag push
+- Build
+- Wiki page
+
+Bellow each of these event checkboxes, you will have an input field to insert
+which Mattermost channel you want to send that event message, with `#town-square`
+being the default. The hash sign is optional.
+
+At the end, fill in your Mattermost details:
+
+| Field | Description |
+| ----- | ----------- |
+| **Webhook** | The incoming webhooks which you have to setup on Mattermost, it will be something like: http://mattermost.example/hooks/5xo... |
+| **Username** | Optional username which can be on messages sent to Mattermost. Fill this in if you want to change the username of the bot. |
+| **Notify only broken builds** | If you choose to enable the **Build** event and you want to be only notified about failed builds. |
+
+
+![Mattermost configuration](img/mattermost_configuration.png)
diff --git a/doc/user/project/integrations/mattermost_slash_commands.md b/doc/user/project/integrations/mattermost_slash_commands.md
new file mode 100644
index 00000000000..488f61c77a3
--- /dev/null
+++ b/doc/user/project/integrations/mattermost_slash_commands.md
@@ -0,0 +1,164 @@
+# Mattermost slash commands
+
+> Introduced in GitLab 8.14
+
+Mattermost commands give users an extra interface to perform common operations
+from the chat environment. This allows one to, for example, create an issue as
+soon as the idea was discussed in Mattermost.
+
+## Prerequisites
+
+Mattermost 3.4 and up is required.
+
+If you have the Omnibus GitLab package installed, Mattermost is already bundled
+in it. All you have to do is configure it. Read more in the
+[Omnibus GitLab Mattermost documentation][omnimmdocs].
+
+## Automated Configuration
+
+If Mattermost is installed on the same server as GitLab, the configuration process can be
+done for you by GitLab.
+
+Go to the Mattermost Slash Command service on your project and click the 'Add to Mattermost' button.
+
+## Manual Configuration
+
+The configuration consists of two parts. First you need to enable the slash
+commands in Mattermost and then enable the service in GitLab.
+
+### Step 1. Enable custom slash commands in Mattermost
+
+This step is only required when using a source install, omnibus installs will be
+preconfigured with the right settings.
+
+The first thing to do in Mattermost is to enable custom slash commands from
+the administrator console.
+
+1. Log in with an account that has admin privileges and navigate to the system
+ console.
+
+ ![Mattermost go to console](img/mattermost_goto_console.png)
+
+ ---
+
+1. Click **Custom integrations** and set **Enable Custom Slash Commands**,
+ **Enable custom integrations to override usernames**, and **Override
+ custom integrations to override profile picture icons** to true
+
+ ![Mattermost console](img/mattermost_console_integrations.png)
+
+ ---
+
+1. Click **Save** at the bottom to save the changes.
+
+### Step 2. Open the Mattermost slash commands service in GitLab
+
+1. Open a new tab for GitLab, go to your project's
+ [Integrations page](project_services.md#accessing-the-project-services)
+ and select the **Mattermost command** service to configure it.
+ A screen will appear with all the values you need to copy in Mattermost as
+ described in the next step. Leave the window open.
+
+ >**Note:**
+ GitLab will propose some values for the Mattermost settings. The only one
+ required to copy-paste as-is is the **Request URL**, all the others are just
+ suggestions.
+
+ ![Mattermost setup instructions](img/mattermost_config_help.png)
+
+ ---
+
+1. Proceed to the next step and create a slash command in Mattermost with the
+ above values.
+
+### Step 3. Create a new custom slash command in Mattermost
+
+Now that you have enabled custom slash commands in Mattermost and opened
+the Mattermost slash commands service in GitLab, it's time to copy these values
+in a new slash command.
+
+1. Back to Mattermost, under your team page settings, you should see the
+ **Integrations** option.
+
+ ![Mattermost team integrations](img/mattermost_team_integrations.png)
+
+ ---
+
+1. Go to the **Slash Commands** integration and add a new one by clicking the
+ **Add Slash Command** button.
+
+ ![Mattermost add command](img/mattermost_add_slash_command.png)
+
+ ---
+
+1. Fill in the options for the custom command as described in
+ [step 2](#step-2-open-the-mattermost-slash-commands-service-in-gitlab).
+
+ >**Note:**
+ If you plan on connecting multiple projects, pick a slash command trigger
+ word that relates to your projects such as `/gitlab-project-name` or even
+ just `/project-name`. Only use `/gitlab` if you will only connect a single
+ project to your Mattermost team.
+
+ ![Mattermost add command configuration](img/mattermost_slash_command_configuration.png)
+
+1. After you setup all the values, copy the token (we will use it below) and
+ click **Done**.
+
+ ![Mattermost slash command token](img/mattermost_slash_command_token.png)
+
+### Step 4. Copy the Mattermost token into the Mattermost slash command service
+
+1. In GitLab, paste the Mattermost token you copied in the previous step and
+ check the **Active** checkbox.
+
+ ![Mattermost copy token to GitLab](img/mattermost_gitlab_token.png)
+
+1. Click **Save changes** for the changes to take effect.
+
+---
+
+You are now set to start using slash commands in Mattermost that talk to the
+GitLab project you configured.
+
+## Authorizing Mattermost to interact with GitLab
+
+The first time a user will interact with the newly created slash commands,
+Mattermost will trigger an authorization process.
+
+![Mattermost bot authorize](img/mattermost_bot_auth.png)
+
+This will connect your Mattermost user with your GitLab user. You can
+see all authorized chat accounts in your profile's page under **Chat**.
+
+When the authorization process is complete, you can start interacting with
+GitLab using the Mattermost commands.
+
+## Available slash commands
+
+The available slash commands are:
+
+| Command | Description | Example |
+| ------- | ----------- | ------- |
+| <kbd>/&lt;trigger&gt; issue new &lt;title&gt; <kbd>⇧ Shift</kbd>+<kbd>↵ Enter</kbd> &lt;description&gt;</kbd> | Create a new issue in the project that `<trigger>` is tied to. `<description>` is optional. | <samp>/gitlab issue new We need to change the homepage</samp> |
+| <kbd>/&lt;trigger&gt; issue show &lt;issue-number&gt;</kbd> | Show the issue with ID `<issue-number>` from the project that `<trigger>` is tied to. | <samp>/gitlab issue show 42</samp> |
+| <kbd>/&lt;trigger&gt; deploy &lt;environment&gt; to &lt;environment&gt;</kbd> | Start the CI job that deploys from one environment to another, for example `staging` to `production`. CI/CD must be [properly configured][ciyaml]. | <samp>/gitlab deploy staging to production</samp> |
+
+To see a list of available commands to interact with GitLab, type the
+trigger word followed by <kbd>help</kbd>. Example: <samp>/gitlab help</samp>
+
+![Mattermost bot available commands](img/mattermost_bot_available_commands.png)
+
+## Permissions
+
+The permissions to run the [available commands](#available-slash-commands) derive from
+the [permissions you have on the project](../../permissions.md#project).
+
+## Further reading
+
+- [Mattermost slash commands documentation][mmslashdocs]
+- [Omnibus GitLab Mattermost][omnimmdocs]
+
+[omnimmdocs]: https://docs.gitlab.com/omnibus/gitlab-mattermost/
+[mmslashdocs]: https://docs.mattermost.com/developer/slash-commands.html
+[ciyaml]: ../../../ci/yaml/README.md
diff --git a/doc/user/project/integrations/project_services.md b/doc/user/project/integrations/project_services.md
new file mode 100644
index 00000000000..a3a163a4c6b
--- /dev/null
+++ b/doc/user/project/integrations/project_services.md
@@ -0,0 +1,76 @@
+# Project services
+
+Project services allow you to integrate GitLab with other applications. They
+are a bit like plugins in that they allow a lot of freedom in adding
+functionality to GitLab.
+
+## Accessing the project services
+
+You can find the available services under the **Integrations** page in your
+project's settings.
+
+1. Navigate to the cog icon in the upper right corner of your project. You need
+ to have at least [master permission][permissions] on the project.
+
+ ![Accessing the services](img/accessing_integrations.png)
+
+1. There are more than 20 services to integrate with. Click on the one that you
+ want to configure.
+
+ ![Project services list](img/project_services.png)
+
+Below, you will find a list of the currently supported ones accompanied with
+comprehensive documentation.
+
+## Services
+
+Click on the service links to see further configuration instructions and details.
+
+| Service | Description |
+| ------- | ----------- |
+| Asana | Asana - Teamwork without email |
+| Assembla | Project Management Software (Source Commits Endpoint) |
+| [Atlassian Bamboo CI](bamboo.md) | A continuous integration and build server |
+| Buildkite | Continuous integration and deployments |
+| [Builds emails](builds_emails.md) | Email the builds status to a list of recipients |
+| [Bugzilla](bugzilla.md) | Bugzilla issue tracker |
+| Campfire | Simple web-based real-time group chat |
+| Custom Issue Tracker | Custom issue tracker |
+| Drone CI | Continuous Integration platform built on Docker, written in Go |
+| [Emails on push](emails_on_push.md) | Email the commits and diff of each push to a list of recipients |
+| External Wiki | Replaces the link to the internal wiki with a link to an external wiki |
+| Flowdock | Flowdock is a collaboration web app for technical teams |
+| Gemnasium | Gemnasium monitors your project dependencies and alerts you about updates and security vulnerabilities |
+| [HipChat](hipchat.md) | Private group chat and IM |
+| [Irker (IRC gateway)](irker.md) | Send IRC messages, on update, to a list of recipients through an Irker gateway |
+| [JIRA](jira.md) | JIRA issue tracker |
+| JetBrains TeamCity CI | A continuous integration and build server |
+| [Kubernetes](kubernetes.md) | A containerized deployment service |
+| [Mattermost slash commands](mattermost_slash_commands.md) | Mattermost chat and ChatOps slash commands |
+| [Mattermost Notifications](mattermost.md) | Receive event notifications in Mattermost |
+| [Slack Notifications](slack.md) | Receive event notifications in Slack |
+| [Slack slash commands](slack_slash_commands.md) | Slack chat and ChatOps slash commands |
+| PivotalTracker | Project Management Software (Source Commits Endpoint) |
+| Pushover | Pushover makes it easy to get real-time notifications on your Android device, iPhone, iPad, and Desktop |
+| [Redmine](redmine.md) | Redmine issue tracker |
+
+## Services templates
+
+Services templates is a way to set some predefined values in the Service of
+your liking which will then be pre-filled on each project's Service.
+
+Read more about [Services templates in this document](services_templates.md).
+
+## Contributing to project services
+
+Because GitLab is open source we can ship with the code and tests for all
+plugins. This allows the community to keep the plugins up to date so that they
+always work in newer GitLab versions.
+
+For an overview of what projects services are available, please see the
+[project_services source directory][projects-code].
+
+Contributions are welcome!
+
+[projects-code]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/models/project_services
+[permissions]: ../../permissions.md
diff --git a/doc/user/project/integrations/redmine.md b/doc/user/project/integrations/redmine.md
new file mode 100644
index 00000000000..89c0312d3c2
--- /dev/null
+++ b/doc/user/project/integrations/redmine.md
@@ -0,0 +1,23 @@
+# Redmine Service
+
+To enable the Redmine integration in a project, navigate to the
+[Integrations page](project_services.md#accessing-the-project-services), click
+the **Redmine** service, and fill in the required details on the page as described
+in the table below.
+
+| Field | Description |
+| ----- | ----------- |
+| `description` | A name for the issue tracker (to differentiate between instances, for example) |
+| `project_url` | The URL to the project in Redmine which is being linked to this GitLab project |
+| `issues_url` | The URL to the issue in Redmine project that is linked to this GitLab project. Note that the `issues_url` requires `:id` in the URL. This ID is used by GitLab as a placeholder to replace the issue number. |
+| `new_issue_url` | This is the URL to create a new issue in Redmine for the project linked to this GitLab project |
+
+Once you have configured and enabled Redmine:
+
+- the **Issues** link on the GitLab project pages takes you to the appropriate
+ Redmine issue index
+- clicking **New issue** on the project dashboard creates a new Redmine issue
+
+As an example, below is a configuration for a project named gitlab-ci.
+
+![Redmine configuration](img/redmine_configuration.png)
diff --git a/doc/user/project/integrations/services_templates.md b/doc/user/project/integrations/services_templates.md
new file mode 100644
index 00000000000..be6d13b6d2b
--- /dev/null
+++ b/doc/user/project/integrations/services_templates.md
@@ -0,0 +1,25 @@
+# Services Templates
+
+A GitLab administrator can add a service template that sets a default for each
+project. This makes it much easier to configure individual projects.
+
+After the template is created, the template details will be pre-filled on a
+project's Service page.
+
+## Enable a Service template
+
+In GitLab's Admin area, navigate to **Service Templates** and choose the
+service template you wish to create.
+
+For example, in the image below you can see Redmine.
+
+![Redmine service template](img/services_templates_redmine_example.png)
+
+---
+
+**NOTE:** For each project, you will still need to configure the issue tracking
+URLs by replacing `:issues_tracker_id` in the above screenshot with the ID used
+by your external issue tracker. Prior to GitLab v7.8, this ID was configured in
+the project settings, and GitLab would automatically update the URL configured
+in `gitlab.yml`. This behavior is now deprecated and all issue tracker URLs
+must be configured directly within the project's **Services** settings.
diff --git a/doc/user/project/integrations/slack.md b/doc/user/project/integrations/slack.md
new file mode 100644
index 00000000000..57a9492044b
--- /dev/null
+++ b/doc/user/project/integrations/slack.md
@@ -0,0 +1,51 @@
+# Slack Notifications Service
+
+## On Slack
+
+To enable Slack integration you must create an incoming webhook integration on
+Slack:
+
+1. [Sign in to Slack](https://slack.com/signin)
+1. Visit [Incoming WebHooks](https://my.slack.com/services/new/incoming-webhook/)
+1. Choose the channel name you want to send notifications to.
+1. Click **Add Incoming WebHooks Integration**
+1. Copy the **Webhook URL**, we'll need this later for GitLab.
+
+## On GitLab
+
+After you set up Slack, it's time to set up GitLab.
+
+Navigate to the [Integrations page](project_services.md#accessing-the-project-services)
+and select the **Slack notifications** service to configure it.
+There, you will see a checkbox with the following events that can be triggered:
+
+- Push
+- Issue
+- Merge request
+- Note
+- Tag push
+- Build
+- Wiki page
+
+Bellow each of these event checkboxes, you will have an input field to insert
+which Slack channel you want to send that event message, with `#general`
+being the default. Enter your preferred channel **without** the hash sign (`#`).
+
+At the end, fill in your Slack details:
+
+| Field | Description |
+| ----- | ----------- |
+| **Webhook** | The [incoming webhook URL][slackhook] which you have to setup on Slack. |
+| **Username** | Optional username which can be on messages sent to slack. Fill this in if you want to change the username of the bot. |
+| **Notify only broken builds** | If you choose to enable the **Build** event and you want to be only notified about failed builds. |
+
+After you are all done, click **Save changes** for the changes to take effect.
+
+>**Note:**
+You can set "branch,pushed,Compare changes" as highlight words on your Slack
+profile settings, so that you can be aware of new commits when somebody pushes
+them.
+
+![Slack configuration](img/slack_configuration.png)
+
+[slackhook]: https://my.slack.com/services/new/incoming-webhook
diff --git a/doc/user/project/integrations/slack_slash_commands.md b/doc/user/project/integrations/slack_slash_commands.md
new file mode 100644
index 00000000000..56f1ba7311e
--- /dev/null
+++ b/doc/user/project/integrations/slack_slash_commands.md
@@ -0,0 +1,24 @@
+# Slack slash commands
+
+> Introduced in GitLab 8.15
+
+Slack commands give users an extra interface to perform common operations
+from the chat environment. This allows one to, for example, create an issue as
+soon as the idea was discussed in chat.
+For all available commands try the help subcommand, for example: `/gitlab help`,
+all review the [full list of commands](../../../integration/chat_commands.md).
+
+## Prerequisites
+
+A [team](https://get.slack.help/hc/en-us/articles/217608418-Creating-a-team) in
+Slack should be created beforehand, GitLab cannot create it for you.
+
+## Configuration
+
+Go to your project's [Integrations page](project_services.md#accessing-the-project-services)
+and select the **Slack slash commands** service to configure it.
+
+![Slack setup instructions](img/slack_setup.png)
+
+Once you've followed the instructions, mark the service as active and insert the token
+you've received from Slack. After saving the service you are good to go!
diff --git a/doc/user/project/integrations/webhooks.md b/doc/user/project/integrations/webhooks.md
new file mode 100644
index 00000000000..9df0c765f84
--- /dev/null
+++ b/doc/user/project/integrations/webhooks.md
@@ -0,0 +1,1028 @@
+# Webhooks
+
+>**Note:**
+Starting from GitLab 8.5:
+- the `repository` key is deprecated in favor of the `project` key
+- the `project.ssh_url` key is deprecated in favor of the `project.git_ssh_url` key
+- the `project.http_url` key is deprecated in favor of the `project.git_http_url` key
+
+Project webhooks allow you to trigger a URL if for example new code is pushed or
+a new issue is created. You can configure webhooks to listen for specific events
+like pushes, issues or merge requests. GitLab will send a POST request with data
+to the webhook URL.
+
+Webhooks can be used to update an external issue tracker, trigger CI builds,
+update a backup mirror, or even deploy to your production server.
+
+Navigate to the webhooks page by going to the **Integrations** page from your
+project's settings which can be found under the wheel icon in the upper right
+corner.
+
+![Accessing the integrations](img/accessing_integrations.png)
+
+## Webhook endpoint tips
+
+If you are writing your own endpoint (web server) that will receive
+GitLab webhooks keep in mind the following things:
+
+- Your endpoint should send its HTTP response as fast as possible. If
+ you wait too long, GitLab may decide the hook failed and retry it.
+- Your endpoint should ALWAYS return a valid HTTP response. If you do
+ not do this then GitLab will think the hook failed and retry it.
+ Most HTTP libraries take care of this for you automatically but if
+ you are writing a low-level hook this is important to remember.
+- GitLab ignores the HTTP status code returned by your endpoint.
+
+## Secret token
+
+If you specify a secret token, it will be sent with the hook request in the
+`X-Gitlab-Token` HTTP header. Your webhook endpoint can check that to verify
+that the request is legitimate.
+
+## SSL verification
+
+By default, the SSL certificate of the webhook endpoint is verified based on
+an internal list of Certificate Authorities, which means the certificate cannot
+be self-signed.
+
+You can turn this off in the webhook settings in your GitLab projects.
+
+![SSL Verification](img/webhooks_ssl.png)
+
+## Events
+
+Below are described the supported events.
+
+### Push events
+
+Triggered when you push to the repository except when pushing tags.
+
+**Request header**:
+
+```
+X-Gitlab-Event: Push Hook
+```
+
+**Request body:**
+
+```json
+{
+ "object_kind": "push",
+ "before": "95790bf891e76fee5e1747ab589903a6a1f80f22",
+ "after": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
+ "ref": "refs/heads/master",
+ "checkout_sha": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
+ "user_id": 4,
+ "user_name": "John Smith",
+ "user_email": "john@example.com",
+ "user_avatar": "https://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=8://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=80",
+ "project_id": 15,
+ "project":{
+ "name":"Diaspora",
+ "description":"",
+ "web_url":"http://example.com/mike/diaspora",
+ "avatar_url":null,
+ "git_ssh_url":"git@example.com:mike/diaspora.git",
+ "git_http_url":"http://example.com/mike/diaspora.git",
+ "namespace":"Mike",
+ "visibility_level":0,
+ "path_with_namespace":"mike/diaspora",
+ "default_branch":"master",
+ "homepage":"http://example.com/mike/diaspora",
+ "url":"git@example.com:mike/diaspora.git",
+ "ssh_url":"git@example.com:mike/diaspora.git",
+ "http_url":"http://example.com/mike/diaspora.git"
+ },
+ "repository":{
+ "name": "Diaspora",
+ "url": "git@example.com:mike/diaspora.git",
+ "description": "",
+ "homepage": "http://example.com/mike/diaspora",
+ "git_http_url":"http://example.com/mike/diaspora.git",
+ "git_ssh_url":"git@example.com:mike/diaspora.git",
+ "visibility_level":0
+ },
+ "commits": [
+ {
+ "id": "b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327",
+ "message": "Update Catalan translation to e38cb41.",
+ "timestamp": "2011-12-12T14:27:31+02:00",
+ "url": "http://example.com/mike/diaspora/commit/b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327",
+ "author": {
+ "name": "Jordi Mallach",
+ "email": "jordi@softcatala.org"
+ },
+ "added": ["CHANGELOG"],
+ "modified": ["app/controller/application.rb"],
+ "removed": []
+ },
+ {
+ "id": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
+ "message": "fixed readme",
+ "timestamp": "2012-01-03T23:36:29+02:00",
+ "url": "http://example.com/mike/diaspora/commit/da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
+ "author": {
+ "name": "GitLab dev user",
+ "email": "gitlabdev@dv6700.(none)"
+ },
+ "added": ["CHANGELOG"],
+ "modified": ["app/controller/application.rb"],
+ "removed": []
+ }
+ ],
+ "total_commits_count": 4
+}
+```
+
+### Tag events
+
+Triggered when you create (or delete) tags to the repository.
+
+**Request header**:
+
+```
+X-Gitlab-Event: Tag Push Hook
+```
+
+**Request body:**
+
+```json
+{
+ "object_kind": "tag_push",
+ "before": "0000000000000000000000000000000000000000",
+ "after": "82b3d5ae55f7080f1e6022629cdb57bfae7cccc7",
+ "ref": "refs/tags/v1.0.0",
+ "checkout_sha": "82b3d5ae55f7080f1e6022629cdb57bfae7cccc7",
+ "user_id": 1,
+ "user_name": "John Smith",
+ "user_avatar": "https://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=8://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=80",
+ "project_id": 1,
+ "project":{
+ "name":"Example",
+ "description":"",
+ "web_url":"http://example.com/jsmith/example",
+ "avatar_url":null,
+ "git_ssh_url":"git@example.com:jsmith/example.git",
+ "git_http_url":"http://example.com/jsmith/example.git",
+ "namespace":"Jsmith",
+ "visibility_level":0,
+ "path_with_namespace":"jsmith/example",
+ "default_branch":"master",
+ "homepage":"http://example.com/jsmith/example",
+ "url":"git@example.com:jsmith/example.git",
+ "ssh_url":"git@example.com:jsmith/example.git",
+ "http_url":"http://example.com/jsmith/example.git"
+ },
+ "repository":{
+ "name": "Example",
+ "url": "ssh://git@example.com/jsmith/example.git",
+ "description": "",
+ "homepage": "http://example.com/jsmith/example",
+ "git_http_url":"http://example.com/jsmith/example.git",
+ "git_ssh_url":"git@example.com:jsmith/example.git",
+ "visibility_level":0
+ },
+ "commits": [],
+ "total_commits_count": 0
+}
+```
+
+### Issues events
+
+Triggered when a new issue is created or an existing issue was updated/closed/reopened.
+
+**Request header**:
+
+```
+X-Gitlab-Event: Issue Hook
+```
+
+**Request body:**
+
+```json
+{
+ "object_kind": "issue",
+ "user": {
+ "name": "Administrator",
+ "username": "root",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
+ },
+ "project":{
+ "name":"Gitlab Test",
+ "description":"Aut reprehenderit ut est.",
+ "web_url":"http://example.com/gitlabhq/gitlab-test",
+ "avatar_url":null,
+ "git_ssh_url":"git@example.com:gitlabhq/gitlab-test.git",
+ "git_http_url":"http://example.com/gitlabhq/gitlab-test.git",
+ "namespace":"GitlabHQ",
+ "visibility_level":20,
+ "path_with_namespace":"gitlabhq/gitlab-test",
+ "default_branch":"master",
+ "homepage":"http://example.com/gitlabhq/gitlab-test",
+ "url":"http://example.com/gitlabhq/gitlab-test.git",
+ "ssh_url":"git@example.com:gitlabhq/gitlab-test.git",
+ "http_url":"http://example.com/gitlabhq/gitlab-test.git"
+ },
+ "repository":{
+ "name": "Gitlab Test",
+ "url": "http://example.com/gitlabhq/gitlab-test.git",
+ "description": "Aut reprehenderit ut est.",
+ "homepage": "http://example.com/gitlabhq/gitlab-test"
+ },
+ "object_attributes": {
+ "id": 301,
+ "title": "New API: create/update/delete file",
+ "assignee_id": 51,
+ "author_id": 51,
+ "project_id": 14,
+ "created_at": "2013-12-03T17:15:43Z",
+ "updated_at": "2013-12-03T17:15:43Z",
+ "position": 0,
+ "branch_name": null,
+ "description": "Create new API for manipulations with repository",
+ "milestone_id": null,
+ "state": "opened",
+ "iid": 23,
+ "url": "http://example.com/diaspora/issues/23",
+ "action": "open"
+ },
+ "assignee": {
+ "name": "User1",
+ "username": "user1",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
+ }
+}
+```
+### Comment events
+
+Triggered when a new comment is made on commits, merge requests, issues, and code snippets.
+The note data will be stored in `object_attributes` (e.g. `note`, `noteable_type`). The
+payload will also include information about the target of the comment. For example,
+a comment on a issue will include the specific issue information under the `issue` key.
+Valid target types:
+
+1. `commit`
+2. `merge_request`
+3. `issue`
+4. `snippet`
+
+#### Comment on commit
+
+**Request header**:
+
+```
+X-Gitlab-Event: Note Hook
+```
+
+**Request body:**
+
+```json
+{
+ "object_kind": "note",
+ "user": {
+ "name": "Administrator",
+ "username": "root",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
+ },
+ "project_id": 5,
+ "project":{
+ "name":"Gitlab Test",
+ "description":"Aut reprehenderit ut est.",
+ "web_url":"http://example.com/gitlabhq/gitlab-test",
+ "avatar_url":null,
+ "git_ssh_url":"git@example.com:gitlabhq/gitlab-test.git",
+ "git_http_url":"http://example.com/gitlabhq/gitlab-test.git",
+ "namespace":"GitlabHQ",
+ "visibility_level":20,
+ "path_with_namespace":"gitlabhq/gitlab-test",
+ "default_branch":"master",
+ "homepage":"http://example.com/gitlabhq/gitlab-test",
+ "url":"http://example.com/gitlabhq/gitlab-test.git",
+ "ssh_url":"git@example.com:gitlabhq/gitlab-test.git",
+ "http_url":"http://example.com/gitlabhq/gitlab-test.git"
+ },
+ "repository":{
+ "name": "Gitlab Test",
+ "url": "http://example.com/gitlab-org/gitlab-test.git",
+ "description": "Aut reprehenderit ut est.",
+ "homepage": "http://example.com/gitlab-org/gitlab-test"
+ },
+ "object_attributes": {
+ "id": 1243,
+ "note": "This is a commit comment. How does this work?",
+ "noteable_type": "Commit",
+ "author_id": 1,
+ "created_at": "2015-05-17 18:08:09 UTC",
+ "updated_at": "2015-05-17 18:08:09 UTC",
+ "project_id": 5,
+ "attachment":null,
+ "line_code": "bec9703f7a456cd2b4ab5fb3220ae016e3e394e3_0_1",
+ "commit_id": "cfe32cf61b73a0d5e9f13e774abde7ff789b1660",
+ "noteable_id": null,
+ "system": false,
+ "st_diff": {
+ "diff": "--- /dev/null\n+++ b/six\n@@ -0,0 +1 @@\n+Subproject commit 409f37c4f05865e4fb208c771485f211a22c4c2d\n",
+ "new_path": "six",
+ "old_path": "six",
+ "a_mode": "0",
+ "b_mode": "160000",
+ "new_file": true,
+ "renamed_file": false,
+ "deleted_file": false
+ },
+ "url": "http://example.com/gitlab-org/gitlab-test/commit/cfe32cf61b73a0d5e9f13e774abde7ff789b1660#note_1243"
+ },
+ "commit": {
+ "id": "cfe32cf61b73a0d5e9f13e774abde7ff789b1660",
+ "message": "Add submodule\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
+ "timestamp": "2014-02-27T10:06:20+02:00",
+ "url": "http://example.com/gitlab-org/gitlab-test/commit/cfe32cf61b73a0d5e9f13e774abde7ff789b1660",
+ "author": {
+ "name": "Dmitriy Zaporozhets",
+ "email": "dmitriy.zaporozhets@gmail.com"
+ }
+ }
+}
+```
+
+#### Comment on merge request
+
+**Request header**:
+
+```
+X-Gitlab-Event: Note Hook
+```
+
+**Request body:**
+
+```json
+{
+ "object_kind": "note",
+ "user": {
+ "name": "Administrator",
+ "username": "root",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
+ },
+ "project_id": 5,
+ "project":{
+ "name":"Gitlab Test",
+ "description":"Aut reprehenderit ut est.",
+ "web_url":"http://example.com/gitlab-org/gitlab-test",
+ "avatar_url":null,
+ "git_ssh_url":"git@example.com:gitlab-org/gitlab-test.git",
+ "git_http_url":"http://example.com/gitlab-org/gitlab-test.git",
+ "namespace":"Gitlab Org",
+ "visibility_level":10,
+ "path_with_namespace":"gitlab-org/gitlab-test",
+ "default_branch":"master",
+ "homepage":"http://example.com/gitlab-org/gitlab-test",
+ "url":"http://example.com/gitlab-org/gitlab-test.git",
+ "ssh_url":"git@example.com:gitlab-org/gitlab-test.git",
+ "http_url":"http://example.com/gitlab-org/gitlab-test.git"
+ },
+ "repository":{
+ "name": "Gitlab Test",
+ "url": "http://localhost/gitlab-org/gitlab-test.git",
+ "description": "Aut reprehenderit ut est.",
+ "homepage": "http://example.com/gitlab-org/gitlab-test"
+ },
+ "object_attributes": {
+ "id": 1244,
+ "note": "This MR needs work.",
+ "noteable_type": "MergeRequest",
+ "author_id": 1,
+ "created_at": "2015-05-17 18:21:36 UTC",
+ "updated_at": "2015-05-17 18:21:36 UTC",
+ "project_id": 5,
+ "attachment": null,
+ "line_code": null,
+ "commit_id": "",
+ "noteable_id": 7,
+ "system": false,
+ "st_diff": null,
+ "url": "http://example.com/gitlab-org/gitlab-test/merge_requests/1#note_1244"
+ },
+ "merge_request": {
+ "id": 7,
+ "target_branch": "markdown",
+ "source_branch": "master",
+ "source_project_id": 5,
+ "author_id": 8,
+ "assignee_id": 28,
+ "title": "Tempora et eos debitis quae laborum et.",
+ "created_at": "2015-03-01 20:12:53 UTC",
+ "updated_at": "2015-03-21 18:27:27 UTC",
+ "milestone_id": 11,
+ "state": "opened",
+ "merge_status": "cannot_be_merged",
+ "target_project_id": 5,
+ "iid": 1,
+ "description": "Et voluptas corrupti assumenda temporibus. Architecto cum animi eveniet amet asperiores. Vitae numquam voluptate est natus sit et ad id.",
+ "position": 0,
+ "locked_at": null,
+ "source":{
+ "name":"Gitlab Test",
+ "description":"Aut reprehenderit ut est.",
+ "web_url":"http://example.com/gitlab-org/gitlab-test",
+ "avatar_url":null,
+ "git_ssh_url":"git@example.com:gitlab-org/gitlab-test.git",
+ "git_http_url":"http://example.com/gitlab-org/gitlab-test.git",
+ "namespace":"Gitlab Org",
+ "visibility_level":10,
+ "path_with_namespace":"gitlab-org/gitlab-test",
+ "default_branch":"master",
+ "homepage":"http://example.com/gitlab-org/gitlab-test",
+ "url":"http://example.com/gitlab-org/gitlab-test.git",
+ "ssh_url":"git@example.com:gitlab-org/gitlab-test.git",
+ "http_url":"http://example.com/gitlab-org/gitlab-test.git"
+ },
+ "target": {
+ "name":"Gitlab Test",
+ "description":"Aut reprehenderit ut est.",
+ "web_url":"http://example.com/gitlab-org/gitlab-test",
+ "avatar_url":null,
+ "git_ssh_url":"git@example.com:gitlab-org/gitlab-test.git",
+ "git_http_url":"http://example.com/gitlab-org/gitlab-test.git",
+ "namespace":"Gitlab Org",
+ "visibility_level":10,
+ "path_with_namespace":"gitlab-org/gitlab-test",
+ "default_branch":"master",
+ "homepage":"http://example.com/gitlab-org/gitlab-test",
+ "url":"http://example.com/gitlab-org/gitlab-test.git",
+ "ssh_url":"git@example.com:gitlab-org/gitlab-test.git",
+ "http_url":"http://example.com/gitlab-org/gitlab-test.git"
+ },
+ "last_commit": {
+ "id": "562e173be03b8ff2efb05345d12df18815438a4b",
+ "message": "Merge branch 'another-branch' into 'master'\n\nCheck in this test\n",
+ "timestamp": "2015-04-08T21: 00:25-07:00",
+ "url": "http://example.com/gitlab-org/gitlab-test/commit/562e173be03b8ff2efb05345d12df18815438a4b",
+ "author": {
+ "name": "John Smith",
+ "email": "john@example.com"
+ }
+ },
+ "work_in_progress": false,
+ "assignee": {
+ "name": "User1",
+ "username": "user1",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
+ }
+ }
+}
+```
+
+#### Comment on issue
+
+**Request header**:
+
+```
+X-Gitlab-Event: Note Hook
+```
+
+**Request body:**
+
+```json
+{
+ "object_kind": "note",
+ "user": {
+ "name": "Administrator",
+ "username": "root",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
+ },
+ "project_id": 5,
+ "project":{
+ "name":"Gitlab Test",
+ "description":"Aut reprehenderit ut est.",
+ "web_url":"http://example.com/gitlab-org/gitlab-test",
+ "avatar_url":null,
+ "git_ssh_url":"git@example.com:gitlab-org/gitlab-test.git",
+ "git_http_url":"http://example.com/gitlab-org/gitlab-test.git",
+ "namespace":"Gitlab Org",
+ "visibility_level":10,
+ "path_with_namespace":"gitlab-org/gitlab-test",
+ "default_branch":"master",
+ "homepage":"http://example.com/gitlab-org/gitlab-test",
+ "url":"http://example.com/gitlab-org/gitlab-test.git",
+ "ssh_url":"git@example.com:gitlab-org/gitlab-test.git",
+ "http_url":"http://example.com/gitlab-org/gitlab-test.git"
+ },
+ "repository":{
+ "name":"diaspora",
+ "url":"git@example.com:mike/diaspora.git",
+ "description":"",
+ "homepage":"http://example.com/mike/diaspora"
+ },
+ "object_attributes": {
+ "id": 1241,
+ "note": "Hello world",
+ "noteable_type": "Issue",
+ "author_id": 1,
+ "created_at": "2015-05-17 17:06:40 UTC",
+ "updated_at": "2015-05-17 17:06:40 UTC",
+ "project_id": 5,
+ "attachment": null,
+ "line_code": null,
+ "commit_id": "",
+ "noteable_id": 92,
+ "system": false,
+ "st_diff": null,
+ "url": "http://example.com/gitlab-org/gitlab-test/issues/17#note_1241"
+ },
+ "issue": {
+ "id": 92,
+ "title": "test",
+ "assignee_id": null,
+ "author_id": 1,
+ "project_id": 5,
+ "created_at": "2015-04-12 14:53:17 UTC",
+ "updated_at": "2015-04-26 08:28:42 UTC",
+ "position": 0,
+ "branch_name": null,
+ "description": "test",
+ "milestone_id": null,
+ "state": "closed",
+ "iid": 17
+ }
+}
+```
+
+#### Comment on code snippet
+
+**Request header**:
+
+```
+X-Gitlab-Event: Note Hook
+```
+
+**Request body:**
+
+```json
+{
+ "object_kind": "note",
+ "user": {
+ "name": "Administrator",
+ "username": "root",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
+ },
+ "project_id": 5,
+ "project":{
+ "name":"Gitlab Test",
+ "description":"Aut reprehenderit ut est.",
+ "web_url":"http://example.com/gitlab-org/gitlab-test",
+ "avatar_url":null,
+ "git_ssh_url":"git@example.com:gitlab-org/gitlab-test.git",
+ "git_http_url":"http://example.com/gitlab-org/gitlab-test.git",
+ "namespace":"Gitlab Org",
+ "visibility_level":10,
+ "path_with_namespace":"gitlab-org/gitlab-test",
+ "default_branch":"master",
+ "homepage":"http://example.com/gitlab-org/gitlab-test",
+ "url":"http://example.com/gitlab-org/gitlab-test.git",
+ "ssh_url":"git@example.com:gitlab-org/gitlab-test.git",
+ "http_url":"http://example.com/gitlab-org/gitlab-test.git"
+ },
+ "repository":{
+ "name":"Gitlab Test",
+ "url":"http://example.com/gitlab-org/gitlab-test.git",
+ "description":"Aut reprehenderit ut est.",
+ "homepage":"http://example.com/gitlab-org/gitlab-test"
+ },
+ "object_attributes": {
+ "id": 1245,
+ "note": "Is this snippet doing what it's supposed to be doing?",
+ "noteable_type": "Snippet",
+ "author_id": 1,
+ "created_at": "2015-05-17 18:35:50 UTC",
+ "updated_at": "2015-05-17 18:35:50 UTC",
+ "project_id": 5,
+ "attachment": null,
+ "line_code": null,
+ "commit_id": "",
+ "noteable_id": 53,
+ "system": false,
+ "st_diff": null,
+ "url": "http://example.com/gitlab-org/gitlab-test/snippets/53#note_1245"
+ },
+ "snippet": {
+ "id": 53,
+ "title": "test",
+ "content": "puts 'Hello world'",
+ "author_id": 1,
+ "project_id": 5,
+ "created_at": "2015-04-09 02:40:38 UTC",
+ "updated_at": "2015-04-09 02:40:38 UTC",
+ "file_name": "test.rb",
+ "expires_at": null,
+ "type": "ProjectSnippet",
+ "visibility_level": 0
+ }
+}
+```
+
+### Merge request events
+
+Triggered when a new merge request is created, an existing merge request was updated/merged/closed or a commit is added in the source branch.
+
+**Request header**:
+
+```
+X-Gitlab-Event: Merge Request Hook
+```
+
+**Request body:**
+
+```json
+{
+ "object_kind": "merge_request",
+ "user": {
+ "name": "Administrator",
+ "username": "root",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
+ },
+ "object_attributes": {
+ "id": 99,
+ "target_branch": "master",
+ "source_branch": "ms-viewport",
+ "source_project_id": 14,
+ "author_id": 51,
+ "assignee_id": 6,
+ "title": "MS-Viewport",
+ "created_at": "2013-12-03T17:23:34Z",
+ "updated_at": "2013-12-03T17:23:34Z",
+ "st_commits": null,
+ "st_diffs": null,
+ "milestone_id": null,
+ "state": "opened",
+ "merge_status": "unchecked",
+ "target_project_id": 14,
+ "iid": 1,
+ "description": "",
+ "source":{
+ "name":"Awesome Project",
+ "description":"Aut reprehenderit ut est.",
+ "web_url":"http://example.com/awesome_space/awesome_project",
+ "avatar_url":null,
+ "git_ssh_url":"git@example.com:awesome_space/awesome_project.git",
+ "git_http_url":"http://example.com/awesome_space/awesome_project.git",
+ "namespace":"Awesome Space",
+ "visibility_level":20,
+ "path_with_namespace":"awesome_space/awesome_project",
+ "default_branch":"master",
+ "homepage":"http://example.com/awesome_space/awesome_project",
+ "url":"http://example.com/awesome_space/awesome_project.git",
+ "ssh_url":"git@example.com:awesome_space/awesome_project.git",
+ "http_url":"http://example.com/awesome_space/awesome_project.git"
+ },
+ "target": {
+ "name":"Awesome Project",
+ "description":"Aut reprehenderit ut est.",
+ "web_url":"http://example.com/awesome_space/awesome_project",
+ "avatar_url":null,
+ "git_ssh_url":"git@example.com:awesome_space/awesome_project.git",
+ "git_http_url":"http://example.com/awesome_space/awesome_project.git",
+ "namespace":"Awesome Space",
+ "visibility_level":20,
+ "path_with_namespace":"awesome_space/awesome_project",
+ "default_branch":"master",
+ "homepage":"http://example.com/awesome_space/awesome_project",
+ "url":"http://example.com/awesome_space/awesome_project.git",
+ "ssh_url":"git@example.com:awesome_space/awesome_project.git",
+ "http_url":"http://example.com/awesome_space/awesome_project.git"
+ },
+ "last_commit": {
+ "id": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
+ "message": "fixed readme",
+ "timestamp": "2012-01-03T23:36:29+02:00",
+ "url": "http://example.com/awesome_space/awesome_project/commits/da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
+ "author": {
+ "name": "GitLab dev user",
+ "email": "gitlabdev@dv6700.(none)"
+ }
+ },
+ "work_in_progress": false,
+ "url": "http://example.com/diaspora/merge_requests/1",
+ "action": "open",
+ "assignee": {
+ "name": "User1",
+ "username": "user1",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
+ }
+ }
+}
+```
+
+### Wiki Page events
+
+Triggered when a wiki page is created or edited.
+
+**Request Header**:
+
+```
+X-Gitlab-Event: Wiki Page Hook
+```
+
+**Request Body**:
+
+```json
+{
+ "object_kind": "wiki_page",
+ "user": {
+ "name": "Administrator",
+ "username": "root",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon"
+ },
+ "project": {
+ "name": "awesome-project",
+ "description": "This is awesome",
+ "web_url": "http://example.com/root/awesome-project",
+ "avatar_url": null,
+ "git_ssh_url": "git@example.com:root/awesome-project.git",
+ "git_http_url": "http://example.com/root/awesome-project.git",
+ "namespace": "root",
+ "visibility_level": 0,
+ "path_with_namespace": "root/awesome-project",
+ "default_branch": "master",
+ "homepage": "http://example.com/root/awesome-project",
+ "url": "git@example.com:root/awesome-project.git",
+ "ssh_url": "git@example.com:root/awesome-project.git",
+ "http_url": "http://example.com/root/awesome-project.git"
+ },
+ "wiki": {
+ "web_url": "http://example.com/root/awesome-project/wikis/home",
+ "git_ssh_url": "git@example.com:root/awesome-project.wiki.git",
+ "git_http_url": "http://example.com/root/awesome-project.wiki.git",
+ "path_with_namespace": "root/awesome-project.wiki",
+ "default_branch": "master"
+ },
+ "object_attributes": {
+ "title": "Awesome",
+ "content": "awesome content goes here",
+ "format": "markdown",
+ "message": "adding an awesome page to the wiki",
+ "slug": "awesome",
+ "url": "http://example.com/root/awesome-project/wikis/awesome",
+ "action": "create"
+ }
+}
+```
+
+### Pipeline events
+
+Triggered on status change of Pipeline.
+
+**Request Header**:
+
+```
+X-Gitlab-Event: Pipeline Hook
+```
+
+**Request Body**:
+
+```json
+{
+ "object_kind": "pipeline",
+ "object_attributes":{
+ "id": 31,
+ "ref": "master",
+ "tag": false,
+ "sha": "bcbb5ec396a2c0f828686f14fac9b80b780504f2",
+ "before_sha": "bcbb5ec396a2c0f828686f14fac9b80b780504f2",
+ "status": "success",
+ "stages":[
+ "build",
+ "test",
+ "deploy"
+ ],
+ "created_at": "2016-08-12 15:23:28 UTC",
+ "finished_at": "2016-08-12 15:26:29 UTC",
+ "duration": 63
+ },
+ "user":{
+ "name": "Administrator",
+ "username": "root",
+ "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon"
+ },
+ "project":{
+ "name": "Gitlab Test",
+ "description": "Atque in sunt eos similique dolores voluptatem.",
+ "web_url": "http://192.168.64.1:3005/gitlab-org/gitlab-test",
+ "avatar_url": null,
+ "git_ssh_url": "git@192.168.64.1:gitlab-org/gitlab-test.git",
+ "git_http_url": "http://192.168.64.1:3005/gitlab-org/gitlab-test.git",
+ "namespace": "Gitlab Org",
+ "visibility_level": 20,
+ "path_with_namespace": "gitlab-org/gitlab-test",
+ "default_branch": "master"
+ },
+ "commit":{
+ "id": "bcbb5ec396a2c0f828686f14fac9b80b780504f2",
+ "message": "test\n",
+ "timestamp": "2016-08-12T17:23:21+02:00",
+ "url": "http://example.com/gitlab-org/gitlab-test/commit/bcbb5ec396a2c0f828686f14fac9b80b780504f2",
+ "author":{
+ "name": "User",
+ "email": "user@gitlab.com"
+ }
+ },
+ "builds":[
+ {
+ "id": 380,
+ "stage": "deploy",
+ "name": "production",
+ "status": "skipped",
+ "created_at": "2016-08-12 15:23:28 UTC",
+ "started_at": null,
+ "finished_at": null,
+ "when": "manual",
+ "manual": true,
+ "user":{
+ "name": "Administrator",
+ "username": "root",
+ "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon"
+ },
+ "runner": null,
+ "artifacts_file":{
+ "filename": null,
+ "size": null
+ }
+ },
+ {
+ "id": 377,
+ "stage": "test",
+ "name": "test-image",
+ "status": "success",
+ "created_at": "2016-08-12 15:23:28 UTC",
+ "started_at": "2016-08-12 15:26:12 UTC",
+ "finished_at": null,
+ "when": "on_success",
+ "manual": false,
+ "user":{
+ "name": "Administrator",
+ "username": "root",
+ "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon"
+ },
+ "runner": null,
+ "artifacts_file":{
+ "filename": null,
+ "size": null
+ }
+ },
+ {
+ "id": 378,
+ "stage": "test",
+ "name": "test-build",
+ "status": "success",
+ "created_at": "2016-08-12 15:23:28 UTC",
+ "started_at": "2016-08-12 15:26:12 UTC",
+ "finished_at": "2016-08-12 15:26:29 UTC",
+ "when": "on_success",
+ "manual": false,
+ "user":{
+ "name": "Administrator",
+ "username": "root",
+ "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon"
+ },
+ "runner": null,
+ "artifacts_file":{
+ "filename": null,
+ "size": null
+ }
+ },
+ {
+ "id": 376,
+ "stage": "build",
+ "name": "build-image",
+ "status": "success",
+ "created_at": "2016-08-12 15:23:28 UTC",
+ "started_at": "2016-08-12 15:24:56 UTC",
+ "finished_at": "2016-08-12 15:25:26 UTC",
+ "when": "on_success",
+ "manual": false,
+ "user":{
+ "name": "Administrator",
+ "username": "root",
+ "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon"
+ },
+ "runner": null,
+ "artifacts_file":{
+ "filename": null,
+ "size": null
+ }
+ },
+ {
+ "id": 379,
+ "stage": "deploy",
+ "name": "staging",
+ "status": "created",
+ "created_at": "2016-08-12 15:23:28 UTC",
+ "started_at": null,
+ "finished_at": null,
+ "when": "on_success",
+ "manual": false,
+ "user":{
+ "name": "Administrator",
+ "username": "root",
+ "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon"
+ },
+ "runner": null,
+ "artifacts_file":{
+ "filename": null,
+ "size": null
+ }
+ }
+ ]
+}
+```
+
+### Build events
+
+Triggered on status change of a Build.
+
+**Request Header**:
+
+```
+X-Gitlab-Event: Build Hook
+```
+
+**Request Body**:
+
+```json
+{
+ "object_kind": "build",
+ "ref": "gitlab-script-trigger",
+ "tag": false,
+ "before_sha": "2293ada6b400935a1378653304eaf6221e0fdb8f",
+ "sha": "2293ada6b400935a1378653304eaf6221e0fdb8f",
+ "build_id": 1977,
+ "build_name": "test",
+ "build_stage": "test",
+ "build_status": "created",
+ "build_started_at": null,
+ "build_finished_at": null,
+ "build_duration": null,
+ "build_allow_failure": false,
+ "project_id": 380,
+ "project_name": "gitlab-org/gitlab-test",
+ "user": {
+ "id": 3,
+ "name": "User",
+ "email": "user@gitlab.com"
+ },
+ "commit": {
+ "id": 2366,
+ "sha": "2293ada6b400935a1378653304eaf6221e0fdb8f",
+ "message": "test\n",
+ "author_name": "User",
+ "author_email": "user@gitlab.com",
+ "status": "created",
+ "duration": null,
+ "started_at": null,
+ "finished_at": null
+ },
+ "repository": {
+ "name": "gitlab_test",
+ "git_ssh_url": "git@192.168.64.1:gitlab-org/gitlab-test.git",
+ "description": "Atque in sunt eos similique dolores voluptatem.",
+ "homepage": "http://192.168.64.1:3005/gitlab-org/gitlab-test",
+ "git_ssh_url": "git@192.168.64.1:gitlab-org/gitlab-test.git",
+ "git_http_url": "http://192.168.64.1:3005/gitlab-org/gitlab-test.git",
+ "visibility_level": 20
+ }
+}
+```
+
+## Example webhook receiver
+
+If you want to see GitLab's webhooks in action for testing purposes you can use
+a simple echo script running in a console session. For the following script to
+work you need to have Ruby installed.
+
+Save the following file as `print_http_body.rb`:
+
+```ruby
+require 'webrick'
+
+server = WEBrick::HTTPServer.new(:Port => ARGV.first)
+server.mount_proc '/' do |req, res|
+ puts req.body
+end
+
+trap 'INT' do
+ server.shutdown
+end
+server.start
+```
+
+Pick an unused port (e.g. 8000) and start the script: `ruby print_http_body.rb
+8000`. Then add your server as a webhook receiver in GitLab as
+`http://my.host:8000/`.
+
+When you press 'Test Hook' in GitLab, you should see something like this in the
+console:
+
+```
+{"before":"077a85dd266e6f3573ef7e9ef8ce3343ad659c4e","after":"95cd4a99e93bc4bbabacfa2cd10e6725b1403c60",<SNIP>}
+example.com - - [14/May/2014:07:45:26 EDT] "POST / HTTP/1.1" 200 0
+- -> /
+```
diff --git a/doc/user/project/issues/confidential_issues.md b/doc/user/project/issues/confidential_issues.md
new file mode 100644
index 00000000000..1760b182114
--- /dev/null
+++ b/doc/user/project/issues/confidential_issues.md
@@ -0,0 +1,68 @@
+# Confidential issues
+
+> [Introduced][ce-3282] in GitLab 8.6.
+
+Confidential issues are issues visible only to members of a project with
+[sufficient permissions](#permissions-and-access-to-confidential-issues).
+Confidential issues can be used by open source projects and companies alike to
+keep security vulnerabilities private or prevent surprises from leaking out.
+
+## Making an issue confidential
+
+You can make an issue confidential either by creating a new issue or editing
+an existing one.
+
+When you create a new issue, a checkbox right below the text area is available
+to mark the issue as confidential. Check that box and hit the **Submit issue**
+button to create the issue. For existing issues, edit them, check the
+confidential checkbox and hit **Save changes**.
+
+![Creating a new confidential issue](img/confidential_issues_create.png)
+
+## Making an issue non-confidential
+
+To make an issue non-confidential, all you have to do is edit it and unmark
+the confidential checkbox. Once you save the issue, it will gain the default
+visibility level you have chosen for your project.
+
+Every change from regular to confidential and vice versa, is indicated by a
+system note in the issue's comments.
+
+![Confidential issues system notes](img/confidential_issues_system_notes.png)
+
+## Indications of a confidential issue
+
+>**Note:** If you don't have [enough permissions](#permissions-and-access-to-confidential-issues),
+you won't be able to see the confidential issues at all.
+
+There are a few things that visually separate a confidential issue from a
+regular one. In the issues index page view, you can see the eye-slash icon
+next to the issues that are marked as confidential.
+
+![Confidential issues index page](img/confidential_issues_index_page.png)
+
+---
+
+Likewise, while inside the issue, you can see the eye-slash icon right next to
+the issue number, but there is also an indicator in the comment area that the
+issue you are commenting on is confidential.
+
+![Confidential issue page](img/confidential_issues_issue_page.png)
+
+## Permissions and access to confidential issues
+
+There are two kinds of level access for confidential issues. The general rule
+is that confidential issues are visible only to members of a project with at
+least [Reporter access][permissions]. However, a guest user can also create
+confidential issues, but can only view the ones that they created themselves.
+
+Confidential issues are also hidden in search results for unprivileged users.
+For example, here's what a user with Master and Guest access sees in the
+project's search results respectively.
+
+| Master access | Guest access |
+| :-----------: | :----------: |
+| ![Confidential issues search master](img/confidential_issues_search_master.png) | ![Confidential issues search guest](img/confidential_issues_search_guest.png) |
+
+[permissions]: ../../permissions.md#project
+[ce-3282]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3282
diff --git a/doc/user/project/issues/due_dates.md b/doc/user/project/issues/due_dates.md
new file mode 100644
index 00000000000..b516d47ffa3
--- /dev/null
+++ b/doc/user/project/issues/due_dates.md
@@ -0,0 +1,37 @@
+# Due dates
+
+> [Introduced][ce-3614] in GitLab 8.7.
+
+Due dates can be used in issues to keep track of deadlines and make sure
+features are shipped on time. Due dates require at least [Reporter permissions][permissions]
+to be able to edit them. On the contrary, they can be seen by everybody.
+
+## Setting a due date
+
+When creating or editing an issue, you can see the due date field from where
+a calendar will appear to help you choose the date you want. To remove it,
+select the date text and delete it.
+
+![Create a due date](img/due_dates_create.png)
+
+A quicker way to set a due date is via the issue sidebar. Simply expand the
+sidebar and select **Edit** to pick a due date or remove the existing one.
+Changes are saved immediately.
+
+![Edit a due date via the sidebar](img/due_dates_edit_sidebar.png)
+
+## Making use of due dates
+
+Issues that have a due date can be distinctively seen in the issues index page
+with a calendar icon next to them. Issues where the date is past due will have
+the icon and the date colored red. You can sort issues by those that are
+_Due soon_ or _Due later_ from the dropdown menu in the right.
+
+![Issues with due dates in the issues index page](img/due_dates_issues_index_page.png)
+
+Due dates also appear in your [todos list](../../../workflow/todos.md).
+
+![Issues with due dates in the todos](img/due_dates_todos.png)
+
+[ce-3614]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3614
+[permissions]: ../../permissions.md#project
diff --git a/doc/user/project/issues/img/confidential_issues_create.png b/doc/user/project/issues/img/confidential_issues_create.png
new file mode 100644
index 00000000000..d259255599d
--- /dev/null
+++ b/doc/user/project/issues/img/confidential_issues_create.png
Binary files differ
diff --git a/doc/user/project/issues/img/confidential_issues_index_page.png b/doc/user/project/issues/img/confidential_issues_index_page.png
new file mode 100644
index 00000000000..042461e2451
--- /dev/null
+++ b/doc/user/project/issues/img/confidential_issues_index_page.png
Binary files differ
diff --git a/doc/user/project/issues/img/confidential_issues_issue_page.png b/doc/user/project/issues/img/confidential_issues_issue_page.png
new file mode 100644
index 00000000000..b3568e9303a
--- /dev/null
+++ b/doc/user/project/issues/img/confidential_issues_issue_page.png
Binary files differ
diff --git a/doc/user/project/issues/img/confidential_issues_search_guest.png b/doc/user/project/issues/img/confidential_issues_search_guest.png
new file mode 100644
index 00000000000..b85de90b4d5
--- /dev/null
+++ b/doc/user/project/issues/img/confidential_issues_search_guest.png
Binary files differ
diff --git a/doc/user/project/issues/img/confidential_issues_search_master.png b/doc/user/project/issues/img/confidential_issues_search_master.png
new file mode 100644
index 00000000000..bf2b9428875
--- /dev/null
+++ b/doc/user/project/issues/img/confidential_issues_search_master.png
Binary files differ
diff --git a/doc/user/project/issues/img/confidential_issues_system_notes.png b/doc/user/project/issues/img/confidential_issues_system_notes.png
new file mode 100644
index 00000000000..4005f9350f7
--- /dev/null
+++ b/doc/user/project/issues/img/confidential_issues_system_notes.png
Binary files differ
diff --git a/doc/user/project/issues/img/due_dates_create.png b/doc/user/project/issues/img/due_dates_create.png
new file mode 100644
index 00000000000..d2fe1172bab
--- /dev/null
+++ b/doc/user/project/issues/img/due_dates_create.png
Binary files differ
diff --git a/doc/user/project/issues/img/due_dates_edit_sidebar.png b/doc/user/project/issues/img/due_dates_edit_sidebar.png
new file mode 100644
index 00000000000..6b37150e7db
--- /dev/null
+++ b/doc/user/project/issues/img/due_dates_edit_sidebar.png
Binary files differ
diff --git a/doc/user/project/issues/img/due_dates_issues_index_page.png b/doc/user/project/issues/img/due_dates_issues_index_page.png
new file mode 100644
index 00000000000..defcd5eca39
--- /dev/null
+++ b/doc/user/project/issues/img/due_dates_issues_index_page.png
Binary files differ
diff --git a/doc/user/project/issues/img/due_dates_todos.png b/doc/user/project/issues/img/due_dates_todos.png
new file mode 100644
index 00000000000..92c9fd4021b
--- /dev/null
+++ b/doc/user/project/issues/img/due_dates_todos.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/merge_conflict_editor.png b/doc/user/project/merge_requests/img/merge_conflict_editor.png
new file mode 100644
index 00000000000..6660920c191
--- /dev/null
+++ b/doc/user/project/merge_requests/img/merge_conflict_editor.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/resolve_discussion_issue_notice.png b/doc/user/project/merge_requests/img/resolve_discussion_issue_notice.png
new file mode 100644
index 00000000000..8c7ce215ae0
--- /dev/null
+++ b/doc/user/project/merge_requests/img/resolve_discussion_issue_notice.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/resolve_discussion_open_issue.png b/doc/user/project/merge_requests/img/resolve_discussion_open_issue.png
new file mode 100644
index 00000000000..98d63278326
--- /dev/null
+++ b/doc/user/project/merge_requests/img/resolve_discussion_open_issue.png
Binary files differ
diff --git a/doc/user/project/merge_requests/merge_request_discussion_resolution.md b/doc/user/project/merge_requests/merge_request_discussion_resolution.md
index f37f1ce4d21..d4b85676d19 100644
--- a/doc/user/project/merge_requests/merge_request_discussion_resolution.md
+++ b/doc/user/project/merge_requests/merge_request_discussion_resolution.md
@@ -51,13 +51,15 @@ are resolved.
![Only allow merge if all the discussions are resolved message](img/only_allow_merge_if_all_discussions_are_resolved_msg.png)
-### Move all unresolved discussions in a merge request to an issue
+## Move all unresolved discussions in a merge request to an issue
-> [Introduced][ce-7180] (Currently on Backlog)
+> [Introduced][ce-7180] in GitLab 8.15.
To delegate unresolved discussions to a new issue you can click the link **open
an issue to resolve them later**.
+![Open new issue from unresolved discussions](img/resolve_discussion_open_issue.png)
+
This will prepare an issue with content referring to the merge request and
discussions.
@@ -66,6 +68,8 @@ discussions.
Hitting **Submit issue** will cause all discussions to be marked as resolved and
add a note referring to the newly created issue.
+![Mark discussions as resolved notice](img/resolve_discussion_issue_notice.png)
+
You can now proceed to merge the merge request from the UI.
[ce-5022]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5022
diff --git a/doc/user/project/merge_requests/resolve_conflicts.md b/doc/user/project/merge_requests/resolve_conflicts.md
index 4d7225bd820..68c49054e47 100644
--- a/doc/user/project/merge_requests/resolve_conflicts.md
+++ b/doc/user/project/merge_requests/resolve_conflicts.md
@@ -21,6 +21,18 @@ 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.
+## Merge conflict editor
+
+> Introduced in GitLab 8.13.
+
+The merge conflict resolution editor allows for more complex merge conflicts,
+which require the user to manually modify a file in order to resolve a conflict,
+to be solved right form the GitLab interface. Use the **Edit inline** button
+to open the editor. Once you're sure about your changes, hit the
+**Commit conflict resolution** button.
+
+![Merge conflict editor](img/merge_conflict_editor.png)
+
## Conflicts available for resolution
GitLab allows resolving conflicts in a file where all of the below are true:
diff --git a/doc/user/project/pages/img/pages_create_project.png b/doc/user/project/pages/img/pages_create_project.png
new file mode 100644
index 00000000000..a936d8e5dbd
--- /dev/null
+++ b/doc/user/project/pages/img/pages_create_project.png
Binary files differ
diff --git a/doc/user/project/pages/img/pages_create_user_page.png b/doc/user/project/pages/img/pages_create_user_page.png
new file mode 100644
index 00000000000..3f615d3757d
--- /dev/null
+++ b/doc/user/project/pages/img/pages_create_user_page.png
Binary files differ
diff --git a/doc/user/project/pages/img/pages_dns_details.png b/doc/user/project/pages/img/pages_dns_details.png
new file mode 100644
index 00000000000..8d34f3b7f38
--- /dev/null
+++ b/doc/user/project/pages/img/pages_dns_details.png
Binary files differ
diff --git a/doc/user/project/pages/img/pages_multiple_domains.png b/doc/user/project/pages/img/pages_multiple_domains.png
new file mode 100644
index 00000000000..2bc7cee07a6
--- /dev/null
+++ b/doc/user/project/pages/img/pages_multiple_domains.png
Binary files differ
diff --git a/doc/user/project/pages/img/pages_new_domain_button.png b/doc/user/project/pages/img/pages_new_domain_button.png
new file mode 100644
index 00000000000..c3640133bb2
--- /dev/null
+++ b/doc/user/project/pages/img/pages_new_domain_button.png
Binary files differ
diff --git a/doc/user/project/pages/img/pages_remove.png b/doc/user/project/pages/img/pages_remove.png
new file mode 100644
index 00000000000..adbfb654877
--- /dev/null
+++ b/doc/user/project/pages/img/pages_remove.png
Binary files differ
diff --git a/doc/user/project/pages/img/pages_upload_cert.png b/doc/user/project/pages/img/pages_upload_cert.png
new file mode 100644
index 00000000000..06d85ab1971
--- /dev/null
+++ b/doc/user/project/pages/img/pages_upload_cert.png
Binary files differ
diff --git a/doc/user/project/pages/index.md b/doc/user/project/pages/index.md
new file mode 100644
index 00000000000..b814e3fccb2
--- /dev/null
+++ b/doc/user/project/pages/index.md
@@ -0,0 +1,435 @@
+# GitLab Pages
+
+> **Notes:**
+> - This feature was [introduced][ee-80] in GitLab EE 8.3.
+> - Custom CNAMEs with TLS support were [introduced][ee-173] in GitLab EE 8.5.
+> - GitLab Pages [were ported][ce-14605] to Community Edition in GitLab 8.17.
+> - This document is about the user guide. To learn how to enable GitLab Pages
+> across your GitLab instance, visit the [administrator documentation](../../../administration/pages/index.md).
+
+With GitLab Pages you can host for free your static websites on GitLab.
+Combined with the power of [GitLab CI] and the help of [GitLab Runner] you can
+deploy static pages for your individual projects, your user or your group.
+
+Read [GitLab Pages on GitLab.com](#gitlab-pages-on-gitlab-com) for specific
+information, if you are using GitLab.com to host your website.
+
+## Getting started with GitLab Pages
+
+> **Note:**
+> In the rest of this document we will assume that the general domain name that
+> is used for GitLab Pages is `example.io`.
+
+In general there are two types of pages one might create:
+
+- Pages per user (`username.example.io`) or per group (`groupname.example.io`)
+- Pages per project (`username.example.io/projectname` or `groupname.example.io/projectname`)
+
+In GitLab, usernames and groupnames are unique and we often refer to them
+as namespaces. There can be only one namespace in a GitLab instance. Below you
+can see the connection between the type of GitLab Pages, what the project name
+that is created on GitLab looks like and the website URL it will be ultimately
+be served on.
+
+| Type of GitLab Pages | The name of the project created in GitLab | Website URL |
+| -------------------- | ------------ | ----------- |
+| User pages | `username.example.io` | `http(s)://username.example.io` |
+| Group pages | `groupname.example.io` | `http(s)://groupname.example.io` |
+| Project pages owned by a user | `projectname` | `http(s)://username.example.io/projectname` |
+| Project pages owned by a group | `projectname` | `http(s)://groupname.example.io/projectname`|
+
+> **Warning:**
+> There are some known [limitations](#limitations) regarding namespaces served
+> under the general domain name and HTTPS. Make sure to read that section.
+
+### GitLab Pages requirements
+
+In brief, this is what you need to upload your website in GitLab Pages:
+
+1. Find out the general domain name that is used for GitLab Pages
+ (ask your administrator). This is very important, so you should first make
+ sure you get that right.
+1. Create a project
+1. Push a [`.gitlab-ci.yml` file](../ci/yaml/README.md) in the root directory
+ of your repository with a specific job named [`pages`][pages]
+1. Set up a GitLab Runner to build your website
+
+> **Note:**
+> If [shared runners](../ci/runners/README.md) are enabled by your GitLab
+> administrator, you should be able to use them instead of bringing your own.
+
+### User or group Pages
+
+For user and group pages, the name of the project should be specific to the
+username or groupname and the general domain name that is used for GitLab Pages.
+Head over your GitLab instance that supports GitLab Pages and create a
+repository named `username.example.io`, where `username` is your username on
+GitLab. If the first part of the project name doesn't match exactly your
+username, it won’t work, so make sure to get it right.
+
+To create a group page, the steps are the same like when creating a website for
+users. Just make sure that you are creating the project within the group's
+namespace.
+
+![Create a user-based pages project](img/pages_create_user_page.png)
+
+---
+
+After you push some static content to your repository and GitLab Runner uploads
+the artifacts to GitLab CI, you will be able to access your website under
+`http(s)://username.example.io`. Keep reading to find out how.
+
+>**Note:**
+If your username/groupname contains a dot, for example `foo.bar`, you will not
+be able to use the wildcard domain HTTPS, read more at [limitations](#limitations).
+
+### Project Pages
+
+GitLab Pages for projects can be created by both user and group accounts.
+The steps to create a project page for a user or a group are identical:
+
+1. Create a new project
+1. Push a [`.gitlab-ci.yml` file](../ci/yaml/README.md) in the root directory
+ of your repository with a specific job named [`pages`][pages].
+1. Set up a GitLab Runner to build your website
+
+A user's project will be served under `http(s)://username.example.io/projectname`
+whereas a group's project under `http(s)://groupname.example.io/projectname`.
+
+### Explore the contents of `.gitlab-ci.yml`
+
+The key thing about GitLab Pages is the `.gitlab-ci.yml` file, something that
+gives you absolute control over the build process. You can actually watch your
+website being built live by following the CI build traces.
+
+> **Note:**
+> Before reading this section, make sure you familiarize yourself with GitLab CI
+> and the specific syntax of[`.gitlab-ci.yml`](../ci/yaml/README.md) by
+> following our [quick start guide](../ci/quick_start/README.md).
+
+To make use of GitLab Pages, the contents of `.gitlab-ci.yml` must follow the
+rules below:
+
+1. A special job named [`pages`][pages] must be defined
+1. Any static content which will be served by GitLab Pages must be placed under
+ a `public/` directory
+1. `artifacts` with a path to the `public/` directory must be defined
+
+In its simplest form, `.gitlab-ci.yml` looks like:
+
+```yaml
+pages:
+ script:
+ - my_commands
+ artifacts:
+ paths:
+ - public
+```
+
+When the Runner reaches to build the `pages` job, it executes whatever is
+defined in the `script` parameter and if the build completes with a non-zero
+exit status, it then uploads the `public/` directory to GitLab Pages.
+
+The `public/` directory should contain all the static content of your website.
+Depending on how you plan to publish your website, the steps defined in the
+[`script` parameter](../ci/yaml/README.md#script) may differ.
+
+Be aware that Pages are by default branch/tag agnostic and their deployment
+relies solely on what you specify in `.gitlab-ci.yml`. If you don't limit the
+`pages` job with the [`only` parameter](../ci/yaml/README.md#only-and-except),
+whenever a new commit is pushed to whatever branch or tag, the Pages will be
+overwritten. In the example below, we limit the Pages to be deployed whenever
+a commit is pushed only on the `master` branch:
+
+```yaml
+pages:
+ script:
+ - my_commands
+ artifacts:
+ paths:
+ - public
+ only:
+ - master
+```
+
+We then tell the Runner to treat the `public/` directory as `artifacts` and
+upload it to GitLab. And since all these parameters were all under a `pages`
+job, the contents of the `public` directory will be served by GitLab Pages.
+
+#### How `.gitlab-ci.yml` looks like when the static content is in your repository
+
+Supposedly your repository contained the following files:
+
+```
+├── index.html
+├── css
+│   └── main.css
+└── js
+ └── main.js
+```
+
+Then the `.gitlab-ci.yml` example below simply moves all files from the root
+directory of the project to the `public/` directory. The `.public` workaround
+is so `cp` doesn't also copy `public/` to itself in an infinite loop:
+
+```yaml
+pages:
+ script:
+ - mkdir .public
+ - cp -r * .public
+ - mv .public public
+ artifacts:
+ paths:
+ - public
+ only:
+ - master
+```
+
+#### How `.gitlab-ci.yml` looks like when using a static generator
+
+In general, GitLab Pages support any kind of [static site generator][staticgen],
+since `.gitlab-ci.yml` can be configured to run any possible command.
+
+In the root directory of your Git repository, place the source files of your
+favorite static generator. Then provide a `.gitlab-ci.yml` file which is
+specific to your static generator.
+
+The example below, uses [Jekyll] to build the static site:
+
+```yaml
+image: ruby:2.1 # the script will run in Ruby 2.1 using the Docker image ruby:2.1
+
+pages: # the build job must be named pages
+ script:
+ - gem install jekyll # we install jekyll
+ - jekyll build -d public/ # we tell jekyll to build the site for us
+ artifacts:
+ paths:
+ - public # this is where the site will live and the Runner uploads it in GitLab
+ only:
+ - master # this script is only affecting the master branch
+```
+
+Here, we used the Docker executor and in the first line we specified the base
+image against which our builds will run.
+
+You have to make sure that the generated static files are ultimately placed
+under the `public` directory, that's why in the `script` section we run the
+`jekyll` command that builds the website and puts all content in the `public/`
+directory. Depending on the static generator of your choice, this command will
+differ. Search in the documentation of the static generator you will use if
+there is an option to explicitly set the output directory. If there is not
+such an option, you can always add one more line under `script` to rename the
+resulting directory in `public/`.
+
+We then tell the Runner to treat the `public/` directory as `artifacts` and
+upload it to GitLab.
+
+---
+
+See the [jekyll example project][pages-jekyll] to better understand how this
+works.
+
+For a list of Pages projects, see the [example projects](#example-projects) to
+get you started.
+
+#### How to set up GitLab Pages in a repository where there's also actual code
+
+Remember that GitLab Pages are by default branch/tag agnostic and their
+deployment relies solely on what you specify in `.gitlab-ci.yml`. You can limit
+the `pages` job with the [`only` parameter](../ci/yaml/README.md#only-and-except),
+whenever a new commit is pushed to a branch that will be used specifically for
+your pages.
+
+That way, you can have your project's code in the `master` branch and use an
+orphan branch (let's name it `pages`) that will host your static generator site.
+
+You can create a new empty branch like this:
+
+```bash
+git checkout --orphan pages
+```
+
+The first commit made on this new branch will have no parents and it will be
+the root of a new history totally disconnected from all the other branches and
+commits. Push the source files of your static generator in the `pages` branch.
+
+Below is a copy of `.gitlab-ci.yml` where the most significant line is the last
+one, specifying to execute everything in the `pages` branch:
+
+```
+image: ruby:2.1
+
+pages:
+ script:
+ - gem install jekyll
+ - jekyll build -d public/
+ artifacts:
+ paths:
+ - public
+ only:
+ - pages
+```
+
+See an example that has different files in the [`master` branch][jekyll-master]
+and the source files for Jekyll are in a [`pages` branch][jekyll-pages] which
+also includes `.gitlab-ci.yml`.
+
+[jekyll-master]: https://gitlab.com/pages/jekyll-branched/tree/master
+[jekyll-pages]: https://gitlab.com/pages/jekyll-branched/tree/pages
+
+## Next steps
+
+So you have successfully deployed your website, congratulations! Let's check
+what more you can do with GitLab Pages.
+
+### Example projects
+
+Below is a list of example projects for GitLab Pages with a plain HTML website
+or various static site generators. Contributions are very welcome.
+
+- [Plain HTML](https://gitlab.com/pages/plain-html)
+- [Jekyll](https://gitlab.com/pages/jekyll)
+- [Hugo](https://gitlab.com/pages/hugo)
+- [Middleman](https://gitlab.com/pages/middleman)
+- [Hexo](https://gitlab.com/pages/hexo)
+- [Brunch](https://gitlab.com/pages/brunch)
+- [Metalsmith](https://gitlab.com/pages/metalsmith)
+- [Harp](https://gitlab.com/pages/harp)
+
+Visit the GitLab Pages group for a full list of example projects:
+<https://gitlab.com/groups/pages>.
+
+### Add a custom domain to your Pages website
+
+If this setting is enabled by your GitLab administrator, you should be able to
+see the **New Domain** button when visiting your project's settings through the
+gear icon in the top right and then navigating to **Pages**.
+
+![New domain button](img/pages_new_domain_button.png)
+
+---
+
+You can add multiple domains pointing to your website hosted under GitLab.
+Once the domain is added, you can see it listed under the **Domains** section.
+
+![Pages multiple domains](img/pages_multiple_domains.png)
+
+---
+
+As a last step, you need to configure your DNS and add a CNAME pointing to your
+user/group page. Click on the **Details** button of a domain for further
+instructions.
+
+![Pages DNS details](img/pages_dns_details.png)
+
+---
+
+>**Note:**
+Currently there is support only for custom domains on per-project basis. That
+means that if you add a custom domain (`example.com`) for your user website
+(`username.example.io`), a project that is served under `username.example.io/foo`,
+will not be accessible under `example.com/foo`.
+
+### Secure your custom domain website with TLS
+
+When you add a new custom domain, you also have the chance to add a TLS
+certificate. If this setting is enabled by your GitLab administrator, you
+should be able to see the option to upload the public certificate and the
+private key when adding a new domain.
+
+![Pages upload cert](img/pages_upload_cert.png)
+
+### Custom error codes pages
+
+You can provide your own 403 and 404 error pages by creating the `403.html` and
+`404.html` files respectively in the root directory of the `public/` directory
+that will be included in the artifacts. Usually this is the root directory of
+your project, but that may differ depending on your static generator
+configuration.
+
+If the case of `404.html`, there are different scenarios. For example:
+
+- If you use project Pages (served under `/projectname/`) and try to access
+ `/projectname/non/exsiting_file`, GitLab Pages will try to serve first
+ `/projectname/404.html`, and then `/404.html`.
+- If you use user/group Pages (served under `/`) and try to access
+ `/non/existing_file` GitLab Pages will try to serve `/404.html`.
+- If you use a custom domain and try to access `/non/existing_file`, GitLab
+ Pages will try to serve only `/404.html`.
+
+### Remove the contents of your pages
+
+If you ever feel the need to purge your Pages content, you can do so by going
+to your project's settings through the gear icon in the top right, and then
+navigating to **Pages**. Hit the **Remove pages** button and your Pages website
+will be deleted. Simple as that.
+
+![Remove pages](img/pages_remove.png)
+
+## GitLab Pages on GitLab.com
+
+If you are using GitLab.com to host your website, then:
+
+- The general domain name for GitLab Pages on GitLab.com is `gitlab.io`.
+- Custom domains and TLS support are enabled.
+- Shared runners are enabled by default, provided for free and can be used to
+ build your website. If you want you can still bring your own Runner.
+
+The rest of the guide still applies.
+
+## Limitations
+
+When using Pages under the general domain of a GitLab instance (`*.example.io`),
+you _cannot_ use HTTPS with sub-subdomains. That means that if your
+username/groupname contains a dot, for example `foo.bar`, the domain
+`https://foo.bar.example.io` will _not_ work. This is a limitation of the
+[HTTP Over TLS protocol][rfc]. HTTP pages will continue to work provided you
+don't redirect HTTP to HTTPS.
+
+[rfc]: https://tools.ietf.org/html/rfc2818#section-3.1 "HTTP Over TLS RFC"
+
+## Redirects in GitLab Pages
+
+Since you cannot use any custom server configuration files, like `.htaccess` or
+any `.conf` file for that matter, if you want to redirect a web page to another
+location, you can use the [HTTP meta refresh tag][metarefresh].
+
+Some static site generators provide plugins for that functionality so that you
+don't have to create and edit HTML files manually. For example, Jekyll has the
+[redirect-from plugin](https://github.com/jekyll/jekyll-redirect-from).
+
+## Frequently Asked Questions
+
+### Can I download my generated pages?
+
+Sure. All you need to do is download the artifacts archive from the build page.
+
+### Can I use GitLab Pages if my project is private?
+
+Yes. GitLab Pages don't care whether you set your project's visibility level
+to private, internal or public.
+
+### Do I need to create a user/group website before creating a project website?
+
+No, you don't. You can create your project first and it will be accessed under
+`http(s)://namespace.example.io/projectname`.
+
+## Known issues
+
+For a list of known issues, visit GitLab's [public issue tracker].
+
+---
+
+[jekyll]: http://jekyllrb.com/
+[ee-80]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/80
+[ee-173]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/173
+[pages-daemon]: https://gitlab.com/gitlab-org/gitlab-pages
+[gitlab ci]: https://about.gitlab.com/gitlab-ci
+[gitlab runner]: https://gitlab.com/gitlab-org/gitlab-ci-multi-runner
+[pages]: ../ci/yaml/README.md#pages
+[staticgen]: https://www.staticgen.com/
+[pages-jekyll]: https://gitlab.com/pages/jekyll
+[metarefresh]: https://en.wikipedia.org/wiki/Meta_refresh
+[public issue tracker]: https://gitlab.com/gitlab-org/gitlab-ee/issues?label_name=Pages
+[ce-14605]: https://gitlab.com/gitlab-org/gitlab-ce/issues/14605
diff --git a/doc/user/project/repository/web_editor.md b/doc/user/project/repository/web_editor.md
index 675e89e4247..c415d566a7c 100644
--- a/doc/user/project/repository/web_editor.md
+++ b/doc/user/project/repository/web_editor.md
@@ -170,6 +170,5 @@ you commit the changes you will be taken to a new merge request form.
![Start a new merge request with these changes](img/web_editor_start_new_merge_request.png)
-![New file button](basicsimages/file_button.png)
[ce-2808]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2808
-[issue closing pattern]: ../user/project/issues/automatic_issue_closing.md
+[issue closing pattern]: ../issues/automatic_issue_closing.md
diff --git a/doc/user/project/settings/import_export.md b/doc/user/project/settings/import_export.md
index dfc762fe1d3..be042ddf623 100644
--- a/doc/user/project/settings/import_export.md
+++ b/doc/user/project/settings/import_export.md
@@ -14,6 +14,11 @@
> raketask.
> - The exports are stored in a temporary [shared directory][tmp] and are deleted
> every 24 hours by a specific worker.
+> - Group members will get exported as project members, as long as the user has
+> master or admin access to the group where the exported project lives. An admin
+> in the import side is required to map the users, based on email or username.
+> Otherwise, a supplementary comment is left to mention the original author and
+> the MRs, notes or issues will be owned by the importer.
Existing projects running on any GitLab instance or GitLab.com can be exported
with all their related data and be moved into a new GitLab instance.
@@ -22,7 +27,8 @@ with all their related data and be moved into a new GitLab instance.
| GitLab version | Import/Export version |
| -------- | -------- |
-| 8.13.0 to current | 0.1.5 |
+| 8.17.0 to current | 0.1.6 |
+| 8.13.0 | 0.1.5 |
| 8.12.0 | 0.1.4 |
| 8.10.3 | 0.1.3 |
| 8.10.0 | 0.1.2 |
@@ -47,6 +53,9 @@ The following items will NOT be exported:
- Build traces and artifacts
- LFS objects
+- Container registry images
+- CI variables
+- Any encrypted tokens
## Exporting a project and its data
diff --git a/doc/user/project/slash_commands.md b/doc/user/project/slash_commands.md
index 5f6a6c6503e..2fddd7c6503 100644
--- a/doc/user/project/slash_commands.md
+++ b/doc/user/project/slash_commands.md
@@ -14,6 +14,7 @@ do.
|:---------------------------|:-------------|
| `/close` | Close the issue or merge request |
| `/reopen` | Reopen the issue or merge request |
+| `/merge` | Merge (when build succeeds) |
| `/title <New title>` | Change title |
| `/assign @username` | Assign |
| `/unassign` | Remove assignee |
@@ -29,3 +30,8 @@ do.
| <code>/due &lt;in 2 days &#124; this Friday &#124; December 31st&gt;</code> | Set due date |
| `/remove_due_date` | Remove due date |
| `/wip` | Toggle the Work In Progress status |
+| <code>/estimate &lt;1w 3d 2h 14m&gt;</code> | Set time estimate |
+| `/remove_estimate` | Remove estimated time |
+| <code>/spend &lt;1h 30m &#124; -1h 5m&gt;</code> | Add or subtract spent time |
+| `/remove_time_spent` | Remove time spent |
+| `/target_branch <Branch Name>` | Set target branch for current merge request |
diff --git a/doc/web_hooks/web_hooks.md b/doc/web_hooks/web_hooks.md
index 1659dd1f6cb..0ebe5eea173 100644
--- a/doc/web_hooks/web_hooks.md
+++ b/doc/web_hooks/web_hooks.md
@@ -1,1025 +1 @@
-# Webhooks
-
->**Note:**
-Starting from GitLab 8.5:
-- the `repository` key is deprecated in favor of the `project` key
-- the `project.ssh_url` key is deprecated in favor of the `project.git_ssh_url` key
-- the `project.http_url` key is deprecated in favor of the `project.git_http_url` key
-
-Project webhooks allow you to trigger a URL if for example new code is pushed or
-a new issue is created. You can configure webhooks to listen for specific events
-like pushes, issues or merge requests. GitLab will send a POST request with data
-to the webhook URL.
-
-Webhooks can be used to update an external issue tracker, trigger CI builds,
-update a backup mirror, or even deploy to your production server.
-
-Navigate to the webhooks page by choosing **Webhooks** from your project's
-settings which can be found under the wheel icon in the upper right corner.
-
-## Webhook endpoint tips
-
-If you are writing your own endpoint (web server) that will receive
-GitLab webhooks keep in mind the following things:
-
-- Your endpoint should send its HTTP response as fast as possible. If
- you wait too long, GitLab may decide the hook failed and retry it.
-- Your endpoint should ALWAYS return a valid HTTP response. If you do
- not do this then GitLab will think the hook failed and retry it.
- Most HTTP libraries take care of this for you automatically but if
- you are writing a low-level hook this is important to remember.
-- GitLab ignores the HTTP status code returned by your endpoint.
-
-## Secret token
-
-If you specify a secret token, it will be sent with the hook request in the
-`X-Gitlab-Token` HTTP header. Your webhook endpoint can check that to verify
-that the request is legitimate.
-
-## SSL verification
-
-By default, the SSL certificate of the webhook endpoint is verified based on
-an internal list of Certificate Authorities, which means the certificate cannot
-be self-signed.
-
-You can turn this off in the webhook settings in your GitLab projects.
-
-![SSL Verification](ssl.png)
-
-## Events
-
-Below are described the supported events.
-
-### Push events
-
-Triggered when you push to the repository except when pushing tags.
-
-**Request header**:
-
-```
-X-Gitlab-Event: Push Hook
-```
-
-**Request body:**
-
-```json
-{
- "object_kind": "push",
- "before": "95790bf891e76fee5e1747ab589903a6a1f80f22",
- "after": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
- "ref": "refs/heads/master",
- "checkout_sha": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
- "user_id": 4,
- "user_name": "John Smith",
- "user_email": "john@example.com",
- "user_avatar": "https://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=8://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=80",
- "project_id": 15,
- "project":{
- "name":"Diaspora",
- "description":"",
- "web_url":"http://example.com/mike/diaspora",
- "avatar_url":null,
- "git_ssh_url":"git@example.com:mike/diaspora.git",
- "git_http_url":"http://example.com/mike/diaspora.git",
- "namespace":"Mike",
- "visibility_level":0,
- "path_with_namespace":"mike/diaspora",
- "default_branch":"master",
- "homepage":"http://example.com/mike/diaspora",
- "url":"git@example.com:mike/diaspora.git",
- "ssh_url":"git@example.com:mike/diaspora.git",
- "http_url":"http://example.com/mike/diaspora.git"
- },
- "repository":{
- "name": "Diaspora",
- "url": "git@example.com:mike/diaspora.git",
- "description": "",
- "homepage": "http://example.com/mike/diaspora",
- "git_http_url":"http://example.com/mike/diaspora.git",
- "git_ssh_url":"git@example.com:mike/diaspora.git",
- "visibility_level":0
- },
- "commits": [
- {
- "id": "b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327",
- "message": "Update Catalan translation to e38cb41.",
- "timestamp": "2011-12-12T14:27:31+02:00",
- "url": "http://example.com/mike/diaspora/commit/b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327",
- "author": {
- "name": "Jordi Mallach",
- "email": "jordi@softcatala.org"
- },
- "added": ["CHANGELOG"],
- "modified": ["app/controller/application.rb"],
- "removed": []
- },
- {
- "id": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
- "message": "fixed readme",
- "timestamp": "2012-01-03T23:36:29+02:00",
- "url": "http://example.com/mike/diaspora/commit/da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
- "author": {
- "name": "GitLab dev user",
- "email": "gitlabdev@dv6700.(none)"
- },
- "added": ["CHANGELOG"],
- "modified": ["app/controller/application.rb"],
- "removed": []
- }
- ],
- "total_commits_count": 4
-}
-```
-
-### Tag events
-
-Triggered when you create (or delete) tags to the repository.
-
-**Request header**:
-
-```
-X-Gitlab-Event: Tag Push Hook
-```
-
-**Request body:**
-
-```json
-{
- "object_kind": "tag_push",
- "before": "0000000000000000000000000000000000000000",
- "after": "82b3d5ae55f7080f1e6022629cdb57bfae7cccc7",
- "ref": "refs/tags/v1.0.0",
- "checkout_sha": "82b3d5ae55f7080f1e6022629cdb57bfae7cccc7",
- "user_id": 1,
- "user_name": "John Smith",
- "user_avatar": "https://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=8://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=80",
- "project_id": 1,
- "project":{
- "name":"Example",
- "description":"",
- "web_url":"http://example.com/jsmith/example",
- "avatar_url":null,
- "git_ssh_url":"git@example.com:jsmith/example.git",
- "git_http_url":"http://example.com/jsmith/example.git",
- "namespace":"Jsmith",
- "visibility_level":0,
- "path_with_namespace":"jsmith/example",
- "default_branch":"master",
- "homepage":"http://example.com/jsmith/example",
- "url":"git@example.com:jsmith/example.git",
- "ssh_url":"git@example.com:jsmith/example.git",
- "http_url":"http://example.com/jsmith/example.git"
- },
- "repository":{
- "name": "Example",
- "url": "ssh://git@example.com/jsmith/example.git",
- "description": "",
- "homepage": "http://example.com/jsmith/example",
- "git_http_url":"http://example.com/jsmith/example.git",
- "git_ssh_url":"git@example.com:jsmith/example.git",
- "visibility_level":0
- },
- "commits": [],
- "total_commits_count": 0
-}
-```
-
-### Issues events
-
-Triggered when a new issue is created or an existing issue was updated/closed/reopened.
-
-**Request header**:
-
-```
-X-Gitlab-Event: Issue Hook
-```
-
-**Request body:**
-
-```json
-{
- "object_kind": "issue",
- "user": {
- "name": "Administrator",
- "username": "root",
- "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
- },
- "project":{
- "name":"Gitlab Test",
- "description":"Aut reprehenderit ut est.",
- "web_url":"http://example.com/gitlabhq/gitlab-test",
- "avatar_url":null,
- "git_ssh_url":"git@example.com:gitlabhq/gitlab-test.git",
- "git_http_url":"http://example.com/gitlabhq/gitlab-test.git",
- "namespace":"GitlabHQ",
- "visibility_level":20,
- "path_with_namespace":"gitlabhq/gitlab-test",
- "default_branch":"master",
- "homepage":"http://example.com/gitlabhq/gitlab-test",
- "url":"http://example.com/gitlabhq/gitlab-test.git",
- "ssh_url":"git@example.com:gitlabhq/gitlab-test.git",
- "http_url":"http://example.com/gitlabhq/gitlab-test.git"
- },
- "repository":{
- "name": "Gitlab Test",
- "url": "http://example.com/gitlabhq/gitlab-test.git",
- "description": "Aut reprehenderit ut est.",
- "homepage": "http://example.com/gitlabhq/gitlab-test"
- },
- "object_attributes": {
- "id": 301,
- "title": "New API: create/update/delete file",
- "assignee_id": 51,
- "author_id": 51,
- "project_id": 14,
- "created_at": "2013-12-03T17:15:43Z",
- "updated_at": "2013-12-03T17:15:43Z",
- "position": 0,
- "branch_name": null,
- "description": "Create new API for manipulations with repository",
- "milestone_id": null,
- "state": "opened",
- "iid": 23,
- "url": "http://example.com/diaspora/issues/23",
- "action": "open"
- },
- "assignee": {
- "name": "User1",
- "username": "user1",
- "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
- }
-}
-```
-### Comment events
-
-Triggered when a new comment is made on commits, merge requests, issues, and code snippets.
-The note data will be stored in `object_attributes` (e.g. `note`, `noteable_type`). The
-payload will also include information about the target of the comment. For example,
-a comment on a issue will include the specific issue information under the `issue` key.
-Valid target types:
-
-1. `commit`
-2. `merge_request`
-3. `issue`
-4. `snippet`
-
-#### Comment on commit
-
-**Request header**:
-
-```
-X-Gitlab-Event: Note Hook
-```
-
-**Request body:**
-
-```json
-{
- "object_kind": "note",
- "user": {
- "name": "Administrator",
- "username": "root",
- "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
- },
- "project_id": 5,
- "project":{
- "name":"Gitlab Test",
- "description":"Aut reprehenderit ut est.",
- "web_url":"http://example.com/gitlabhq/gitlab-test",
- "avatar_url":null,
- "git_ssh_url":"git@example.com:gitlabhq/gitlab-test.git",
- "git_http_url":"http://example.com/gitlabhq/gitlab-test.git",
- "namespace":"GitlabHQ",
- "visibility_level":20,
- "path_with_namespace":"gitlabhq/gitlab-test",
- "default_branch":"master",
- "homepage":"http://example.com/gitlabhq/gitlab-test",
- "url":"http://example.com/gitlabhq/gitlab-test.git",
- "ssh_url":"git@example.com:gitlabhq/gitlab-test.git",
- "http_url":"http://example.com/gitlabhq/gitlab-test.git"
- },
- "repository":{
- "name": "Gitlab Test",
- "url": "http://example.com/gitlab-org/gitlab-test.git",
- "description": "Aut reprehenderit ut est.",
- "homepage": "http://example.com/gitlab-org/gitlab-test"
- },
- "object_attributes": {
- "id": 1243,
- "note": "This is a commit comment. How does this work?",
- "noteable_type": "Commit",
- "author_id": 1,
- "created_at": "2015-05-17 18:08:09 UTC",
- "updated_at": "2015-05-17 18:08:09 UTC",
- "project_id": 5,
- "attachment":null,
- "line_code": "bec9703f7a456cd2b4ab5fb3220ae016e3e394e3_0_1",
- "commit_id": "cfe32cf61b73a0d5e9f13e774abde7ff789b1660",
- "noteable_id": null,
- "system": false,
- "st_diff": {
- "diff": "--- /dev/null\n+++ b/six\n@@ -0,0 +1 @@\n+Subproject commit 409f37c4f05865e4fb208c771485f211a22c4c2d\n",
- "new_path": "six",
- "old_path": "six",
- "a_mode": "0",
- "b_mode": "160000",
- "new_file": true,
- "renamed_file": false,
- "deleted_file": false
- },
- "url": "http://example.com/gitlab-org/gitlab-test/commit/cfe32cf61b73a0d5e9f13e774abde7ff789b1660#note_1243"
- },
- "commit": {
- "id": "cfe32cf61b73a0d5e9f13e774abde7ff789b1660",
- "message": "Add submodule\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
- "timestamp": "2014-02-27T10:06:20+02:00",
- "url": "http://example.com/gitlab-org/gitlab-test/commit/cfe32cf61b73a0d5e9f13e774abde7ff789b1660",
- "author": {
- "name": "Dmitriy Zaporozhets",
- "email": "dmitriy.zaporozhets@gmail.com"
- }
- }
-}
-```
-
-#### Comment on merge request
-
-**Request header**:
-
-```
-X-Gitlab-Event: Note Hook
-```
-
-**Request body:**
-
-```json
-{
- "object_kind": "note",
- "user": {
- "name": "Administrator",
- "username": "root",
- "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
- },
- "project_id": 5,
- "project":{
- "name":"Gitlab Test",
- "description":"Aut reprehenderit ut est.",
- "web_url":"http://example.com/gitlab-org/gitlab-test",
- "avatar_url":null,
- "git_ssh_url":"git@example.com:gitlab-org/gitlab-test.git",
- "git_http_url":"http://example.com/gitlab-org/gitlab-test.git",
- "namespace":"Gitlab Org",
- "visibility_level":10,
- "path_with_namespace":"gitlab-org/gitlab-test",
- "default_branch":"master",
- "homepage":"http://example.com/gitlab-org/gitlab-test",
- "url":"http://example.com/gitlab-org/gitlab-test.git",
- "ssh_url":"git@example.com:gitlab-org/gitlab-test.git",
- "http_url":"http://example.com/gitlab-org/gitlab-test.git"
- },
- "repository":{
- "name": "Gitlab Test",
- "url": "http://localhost/gitlab-org/gitlab-test.git",
- "description": "Aut reprehenderit ut est.",
- "homepage": "http://example.com/gitlab-org/gitlab-test"
- },
- "object_attributes": {
- "id": 1244,
- "note": "This MR needs work.",
- "noteable_type": "MergeRequest",
- "author_id": 1,
- "created_at": "2015-05-17 18:21:36 UTC",
- "updated_at": "2015-05-17 18:21:36 UTC",
- "project_id": 5,
- "attachment": null,
- "line_code": null,
- "commit_id": "",
- "noteable_id": 7,
- "system": false,
- "st_diff": null,
- "url": "http://example.com/gitlab-org/gitlab-test/merge_requests/1#note_1244"
- },
- "merge_request": {
- "id": 7,
- "target_branch": "markdown",
- "source_branch": "master",
- "source_project_id": 5,
- "author_id": 8,
- "assignee_id": 28,
- "title": "Tempora et eos debitis quae laborum et.",
- "created_at": "2015-03-01 20:12:53 UTC",
- "updated_at": "2015-03-21 18:27:27 UTC",
- "milestone_id": 11,
- "state": "opened",
- "merge_status": "cannot_be_merged",
- "target_project_id": 5,
- "iid": 1,
- "description": "Et voluptas corrupti assumenda temporibus. Architecto cum animi eveniet amet asperiores. Vitae numquam voluptate est natus sit et ad id.",
- "position": 0,
- "locked_at": null,
- "source":{
- "name":"Gitlab Test",
- "description":"Aut reprehenderit ut est.",
- "web_url":"http://example.com/gitlab-org/gitlab-test",
- "avatar_url":null,
- "git_ssh_url":"git@example.com:gitlab-org/gitlab-test.git",
- "git_http_url":"http://example.com/gitlab-org/gitlab-test.git",
- "namespace":"Gitlab Org",
- "visibility_level":10,
- "path_with_namespace":"gitlab-org/gitlab-test",
- "default_branch":"master",
- "homepage":"http://example.com/gitlab-org/gitlab-test",
- "url":"http://example.com/gitlab-org/gitlab-test.git",
- "ssh_url":"git@example.com:gitlab-org/gitlab-test.git",
- "http_url":"http://example.com/gitlab-org/gitlab-test.git"
- },
- "target": {
- "name":"Gitlab Test",
- "description":"Aut reprehenderit ut est.",
- "web_url":"http://example.com/gitlab-org/gitlab-test",
- "avatar_url":null,
- "git_ssh_url":"git@example.com:gitlab-org/gitlab-test.git",
- "git_http_url":"http://example.com/gitlab-org/gitlab-test.git",
- "namespace":"Gitlab Org",
- "visibility_level":10,
- "path_with_namespace":"gitlab-org/gitlab-test",
- "default_branch":"master",
- "homepage":"http://example.com/gitlab-org/gitlab-test",
- "url":"http://example.com/gitlab-org/gitlab-test.git",
- "ssh_url":"git@example.com:gitlab-org/gitlab-test.git",
- "http_url":"http://example.com/gitlab-org/gitlab-test.git"
- },
- "last_commit": {
- "id": "562e173be03b8ff2efb05345d12df18815438a4b",
- "message": "Merge branch 'another-branch' into 'master'\n\nCheck in this test\n",
- "timestamp": "2015-04-08T21: 00:25-07:00",
- "url": "http://example.com/gitlab-org/gitlab-test/commit/562e173be03b8ff2efb05345d12df18815438a4b",
- "author": {
- "name": "John Smith",
- "email": "john@example.com"
- }
- },
- "work_in_progress": false,
- "assignee": {
- "name": "User1",
- "username": "user1",
- "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
- }
- }
-}
-```
-
-#### Comment on issue
-
-**Request header**:
-
-```
-X-Gitlab-Event: Note Hook
-```
-
-**Request body:**
-
-```json
-{
- "object_kind": "note",
- "user": {
- "name": "Administrator",
- "username": "root",
- "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
- },
- "project_id": 5,
- "project":{
- "name":"Gitlab Test",
- "description":"Aut reprehenderit ut est.",
- "web_url":"http://example.com/gitlab-org/gitlab-test",
- "avatar_url":null,
- "git_ssh_url":"git@example.com:gitlab-org/gitlab-test.git",
- "git_http_url":"http://example.com/gitlab-org/gitlab-test.git",
- "namespace":"Gitlab Org",
- "visibility_level":10,
- "path_with_namespace":"gitlab-org/gitlab-test",
- "default_branch":"master",
- "homepage":"http://example.com/gitlab-org/gitlab-test",
- "url":"http://example.com/gitlab-org/gitlab-test.git",
- "ssh_url":"git@example.com:gitlab-org/gitlab-test.git",
- "http_url":"http://example.com/gitlab-org/gitlab-test.git"
- },
- "repository":{
- "name":"diaspora",
- "url":"git@example.com:mike/diaspora.git",
- "description":"",
- "homepage":"http://example.com/mike/diaspora"
- },
- "object_attributes": {
- "id": 1241,
- "note": "Hello world",
- "noteable_type": "Issue",
- "author_id": 1,
- "created_at": "2015-05-17 17:06:40 UTC",
- "updated_at": "2015-05-17 17:06:40 UTC",
- "project_id": 5,
- "attachment": null,
- "line_code": null,
- "commit_id": "",
- "noteable_id": 92,
- "system": false,
- "st_diff": null,
- "url": "http://example.com/gitlab-org/gitlab-test/issues/17#note_1241"
- },
- "issue": {
- "id": 92,
- "title": "test",
- "assignee_id": null,
- "author_id": 1,
- "project_id": 5,
- "created_at": "2015-04-12 14:53:17 UTC",
- "updated_at": "2015-04-26 08:28:42 UTC",
- "position": 0,
- "branch_name": null,
- "description": "test",
- "milestone_id": null,
- "state": "closed",
- "iid": 17
- }
-}
-```
-
-#### Comment on code snippet
-
-**Request header**:
-
-```
-X-Gitlab-Event: Note Hook
-```
-
-**Request body:**
-
-```json
-{
- "object_kind": "note",
- "user": {
- "name": "Administrator",
- "username": "root",
- "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
- },
- "project_id": 5,
- "project":{
- "name":"Gitlab Test",
- "description":"Aut reprehenderit ut est.",
- "web_url":"http://example.com/gitlab-org/gitlab-test",
- "avatar_url":null,
- "git_ssh_url":"git@example.com:gitlab-org/gitlab-test.git",
- "git_http_url":"http://example.com/gitlab-org/gitlab-test.git",
- "namespace":"Gitlab Org",
- "visibility_level":10,
- "path_with_namespace":"gitlab-org/gitlab-test",
- "default_branch":"master",
- "homepage":"http://example.com/gitlab-org/gitlab-test",
- "url":"http://example.com/gitlab-org/gitlab-test.git",
- "ssh_url":"git@example.com:gitlab-org/gitlab-test.git",
- "http_url":"http://example.com/gitlab-org/gitlab-test.git"
- },
- "repository":{
- "name":"Gitlab Test",
- "url":"http://example.com/gitlab-org/gitlab-test.git",
- "description":"Aut reprehenderit ut est.",
- "homepage":"http://example.com/gitlab-org/gitlab-test"
- },
- "object_attributes": {
- "id": 1245,
- "note": "Is this snippet doing what it's supposed to be doing?",
- "noteable_type": "Snippet",
- "author_id": 1,
- "created_at": "2015-05-17 18:35:50 UTC",
- "updated_at": "2015-05-17 18:35:50 UTC",
- "project_id": 5,
- "attachment": null,
- "line_code": null,
- "commit_id": "",
- "noteable_id": 53,
- "system": false,
- "st_diff": null,
- "url": "http://example.com/gitlab-org/gitlab-test/snippets/53#note_1245"
- },
- "snippet": {
- "id": 53,
- "title": "test",
- "content": "puts 'Hello world'",
- "author_id": 1,
- "project_id": 5,
- "created_at": "2015-04-09 02:40:38 UTC",
- "updated_at": "2015-04-09 02:40:38 UTC",
- "file_name": "test.rb",
- "expires_at": null,
- "type": "ProjectSnippet",
- "visibility_level": 0
- }
-}
-```
-
-### Merge request events
-
-Triggered when a new merge request is created, an existing merge request was updated/merged/closed or a commit is added in the source branch.
-
-**Request header**:
-
-```
-X-Gitlab-Event: Merge Request Hook
-```
-
-**Request body:**
-
-```json
-{
- "object_kind": "merge_request",
- "user": {
- "name": "Administrator",
- "username": "root",
- "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
- },
- "object_attributes": {
- "id": 99,
- "target_branch": "master",
- "source_branch": "ms-viewport",
- "source_project_id": 14,
- "author_id": 51,
- "assignee_id": 6,
- "title": "MS-Viewport",
- "created_at": "2013-12-03T17:23:34Z",
- "updated_at": "2013-12-03T17:23:34Z",
- "st_commits": null,
- "st_diffs": null,
- "milestone_id": null,
- "state": "opened",
- "merge_status": "unchecked",
- "target_project_id": 14,
- "iid": 1,
- "description": "",
- "source":{
- "name":"Awesome Project",
- "description":"Aut reprehenderit ut est.",
- "web_url":"http://example.com/awesome_space/awesome_project",
- "avatar_url":null,
- "git_ssh_url":"git@example.com:awesome_space/awesome_project.git",
- "git_http_url":"http://example.com/awesome_space/awesome_project.git",
- "namespace":"Awesome Space",
- "visibility_level":20,
- "path_with_namespace":"awesome_space/awesome_project",
- "default_branch":"master",
- "homepage":"http://example.com/awesome_space/awesome_project",
- "url":"http://example.com/awesome_space/awesome_project.git",
- "ssh_url":"git@example.com:awesome_space/awesome_project.git",
- "http_url":"http://example.com/awesome_space/awesome_project.git"
- },
- "target": {
- "name":"Awesome Project",
- "description":"Aut reprehenderit ut est.",
- "web_url":"http://example.com/awesome_space/awesome_project",
- "avatar_url":null,
- "git_ssh_url":"git@example.com:awesome_space/awesome_project.git",
- "git_http_url":"http://example.com/awesome_space/awesome_project.git",
- "namespace":"Awesome Space",
- "visibility_level":20,
- "path_with_namespace":"awesome_space/awesome_project",
- "default_branch":"master",
- "homepage":"http://example.com/awesome_space/awesome_project",
- "url":"http://example.com/awesome_space/awesome_project.git",
- "ssh_url":"git@example.com:awesome_space/awesome_project.git",
- "http_url":"http://example.com/awesome_space/awesome_project.git"
- },
- "last_commit": {
- "id": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
- "message": "fixed readme",
- "timestamp": "2012-01-03T23:36:29+02:00",
- "url": "http://example.com/awesome_space/awesome_project/commits/da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
- "author": {
- "name": "GitLab dev user",
- "email": "gitlabdev@dv6700.(none)"
- }
- },
- "work_in_progress": false,
- "url": "http://example.com/diaspora/merge_requests/1",
- "action": "open",
- "assignee": {
- "name": "User1",
- "username": "user1",
- "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
- }
- }
-}
-```
-
-### Wiki Page events
-
-Triggered when a wiki page is created or edited.
-
-**Request Header**:
-
-```
-X-Gitlab-Event: Wiki Page Hook
-```
-
-**Request Body**:
-
-```json
-{
- "object_kind": "wiki_page",
- "user": {
- "name": "Administrator",
- "username": "root",
- "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon"
- },
- "project": {
- "name": "awesome-project",
- "description": "This is awesome",
- "web_url": "http://example.com/root/awesome-project",
- "avatar_url": null,
- "git_ssh_url": "git@example.com:root/awesome-project.git",
- "git_http_url": "http://example.com/root/awesome-project.git",
- "namespace": "root",
- "visibility_level": 0,
- "path_with_namespace": "root/awesome-project",
- "default_branch": "master",
- "homepage": "http://example.com/root/awesome-project",
- "url": "git@example.com:root/awesome-project.git",
- "ssh_url": "git@example.com:root/awesome-project.git",
- "http_url": "http://example.com/root/awesome-project.git"
- },
- "wiki": {
- "web_url": "http://example.com/root/awesome-project/wikis/home",
- "git_ssh_url": "git@example.com:root/awesome-project.wiki.git",
- "git_http_url": "http://example.com/root/awesome-project.wiki.git",
- "path_with_namespace": "root/awesome-project.wiki",
- "default_branch": "master"
- },
- "object_attributes": {
- "title": "Awesome",
- "content": "awesome content goes here",
- "format": "markdown",
- "message": "adding an awesome page to the wiki",
- "slug": "awesome",
- "url": "http://example.com/root/awesome-project/wikis/awesome",
- "action": "create"
- }
-}
-```
-
-### Pipeline events
-
-Triggered on status change of Pipeline.
-
-**Request Header**:
-
-```
-X-Gitlab-Event: Pipeline Hook
-```
-
-**Request Body**:
-
-```json
-{
- "object_kind": "pipeline",
- "object_attributes":{
- "id": 31,
- "ref": "master",
- "tag": false,
- "sha": "bcbb5ec396a2c0f828686f14fac9b80b780504f2",
- "before_sha": "bcbb5ec396a2c0f828686f14fac9b80b780504f2",
- "status": "success",
- "stages":[
- "build",
- "test",
- "deploy"
- ],
- "created_at": "2016-08-12 15:23:28 UTC",
- "finished_at": "2016-08-12 15:26:29 UTC",
- "duration": 63
- },
- "user":{
- "name": "Administrator",
- "username": "root",
- "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon"
- },
- "project":{
- "name": "Gitlab Test",
- "description": "Atque in sunt eos similique dolores voluptatem.",
- "web_url": "http://192.168.64.1:3005/gitlab-org/gitlab-test",
- "avatar_url": null,
- "git_ssh_url": "git@192.168.64.1:gitlab-org/gitlab-test.git",
- "git_http_url": "http://192.168.64.1:3005/gitlab-org/gitlab-test.git",
- "namespace": "Gitlab Org",
- "visibility_level": 20,
- "path_with_namespace": "gitlab-org/gitlab-test",
- "default_branch": "master"
- },
- "commit":{
- "id": "bcbb5ec396a2c0f828686f14fac9b80b780504f2",
- "message": "test\n",
- "timestamp": "2016-08-12T17:23:21+02:00",
- "url": "http://example.com/gitlab-org/gitlab-test/commit/bcbb5ec396a2c0f828686f14fac9b80b780504f2",
- "author":{
- "name": "User",
- "email": "user@gitlab.com"
- }
- },
- "builds":[
- {
- "id": 380,
- "stage": "deploy",
- "name": "production",
- "status": "skipped",
- "created_at": "2016-08-12 15:23:28 UTC",
- "started_at": null,
- "finished_at": null,
- "when": "manual",
- "manual": true,
- "user":{
- "name": "Administrator",
- "username": "root",
- "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon"
- },
- "runner": null,
- "artifacts_file":{
- "filename": null,
- "size": null
- }
- },
- {
- "id": 377,
- "stage": "test",
- "name": "test-image",
- "status": "success",
- "created_at": "2016-08-12 15:23:28 UTC",
- "started_at": "2016-08-12 15:26:12 UTC",
- "finished_at": null,
- "when": "on_success",
- "manual": false,
- "user":{
- "name": "Administrator",
- "username": "root",
- "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon"
- },
- "runner": null,
- "artifacts_file":{
- "filename": null,
- "size": null
- }
- },
- {
- "id": 378,
- "stage": "test",
- "name": "test-build",
- "status": "success",
- "created_at": "2016-08-12 15:23:28 UTC",
- "started_at": "2016-08-12 15:26:12 UTC",
- "finished_at": "2016-08-12 15:26:29 UTC",
- "when": "on_success",
- "manual": false,
- "user":{
- "name": "Administrator",
- "username": "root",
- "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon"
- },
- "runner": null,
- "artifacts_file":{
- "filename": null,
- "size": null
- }
- },
- {
- "id": 376,
- "stage": "build",
- "name": "build-image",
- "status": "success",
- "created_at": "2016-08-12 15:23:28 UTC",
- "started_at": "2016-08-12 15:24:56 UTC",
- "finished_at": "2016-08-12 15:25:26 UTC",
- "when": "on_success",
- "manual": false,
- "user":{
- "name": "Administrator",
- "username": "root",
- "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon"
- },
- "runner": null,
- "artifacts_file":{
- "filename": null,
- "size": null
- }
- },
- {
- "id": 379,
- "stage": "deploy",
- "name": "staging",
- "status": "created",
- "created_at": "2016-08-12 15:23:28 UTC",
- "started_at": null,
- "finished_at": null,
- "when": "on_success",
- "manual": false,
- "user":{
- "name": "Administrator",
- "username": "root",
- "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon"
- },
- "runner": null,
- "artifacts_file":{
- "filename": null,
- "size": null
- }
- }
- ]
-}
-```
-
-### Build events
-
-Triggered on status change of a Build.
-
-**Request Header**:
-
-```
-X-Gitlab-Event: Build Hook
-```
-
-**Request Body**:
-
-```json
-{
- "object_kind": "build",
- "ref": "gitlab-script-trigger",
- "tag": false,
- "before_sha": "2293ada6b400935a1378653304eaf6221e0fdb8f",
- "sha": "2293ada6b400935a1378653304eaf6221e0fdb8f",
- "build_id": 1977,
- "build_name": "test",
- "build_stage": "test",
- "build_status": "created",
- "build_started_at": null,
- "build_finished_at": null,
- "build_duration": null,
- "build_allow_failure": false,
- "project_id": 380,
- "project_name": "gitlab-org/gitlab-test",
- "user": {
- "id": 3,
- "name": "User",
- "email": "user@gitlab.com"
- },
- "commit": {
- "id": 2366,
- "sha": "2293ada6b400935a1378653304eaf6221e0fdb8f",
- "message": "test\n",
- "author_name": "User",
- "author_email": "user@gitlab.com",
- "status": "created",
- "duration": null,
- "started_at": null,
- "finished_at": null
- },
- "repository": {
- "name": "gitlab_test",
- "git_ssh_url": "git@192.168.64.1:gitlab-org/gitlab-test.git",
- "description": "Atque in sunt eos similique dolores voluptatem.",
- "homepage": "http://192.168.64.1:3005/gitlab-org/gitlab-test",
- "git_ssh_url": "git@192.168.64.1:gitlab-org/gitlab-test.git",
- "git_http_url": "http://192.168.64.1:3005/gitlab-org/gitlab-test.git",
- "visibility_level": 20
- }
-}
-```
-
-## Example webhook receiver
-
-If you want to see GitLab's webhooks in action for testing purposes you can use
-a simple echo script running in a console session. For the following script to
-work you need to have Ruby installed.
-
-Save the following file as `print_http_body.rb`:
-
-```ruby
-require 'webrick'
-
-server = WEBrick::HTTPServer.new(:Port => ARGV.first)
-server.mount_proc '/' do |req, res|
- puts req.body
-end
-
-trap 'INT' do
- server.shutdown
-end
-server.start
-```
-
-Pick an unused port (e.g. 8000) and start the script: `ruby print_http_body.rb
-8000`. Then add your server as a webhook receiver in GitLab as
-`http://my.host:8000/`.
-
-When you press 'Test Hook' in GitLab, you should see something like this in the
-console:
-
-```
-{"before":"077a85dd266e6f3573ef7e9ef8ce3343ad659c4e","after":"95cd4a99e93bc4bbabacfa2cd10e6725b1403c60",<SNIP>}
-example.com - - [14/May/2014:07:45:26 EDT] "POST / HTTP/1.1" 200 0
-- -> /
-```
+This document was moved to [project/integrations/webhooks](../user/project/integrations/webhooks.md).
diff --git a/doc/workflow/README.md b/doc/workflow/README.md
index 59a806de210..0b6f00c6aa4 100644
--- a/doc/workflow/README.md
+++ b/doc/workflow/README.md
@@ -7,6 +7,10 @@
- [Feature branch workflow](workflow.md)
- [GitLab Flow](gitlab_flow.md)
- [Groups](groups.md)
+- Issues - The GitLab Issue Tracker is an advanced and complete tool for
+ tracking the evolution of a new idea or the process of solving a problem.
+ - [Confidential issues](../user/project/issues/confidential_issues.md)
+ - [Due date for issues](../user/project/issues/due_dates.md)
- [Issue Board](../user/project/issue_board.md)
- [Keyboard shortcuts](shortcuts.md)
- [File finder](file_finder.md)
@@ -19,6 +23,7 @@
- [Slash commands](../user/project/slash_commands.md)
- [Sharing a project with a group](share_with_group.md)
- [Share projects with other groups](share_projects_with_other_groups.md)
+- [Time tracking](time_tracking.md)
- [Web Editor](../user/project/repository/web_editor.md)
- [Releases](releases.md)
- [Milestones](milestones.md)
diff --git a/doc/workflow/importing/import_projects_from_bitbucket.md b/doc/workflow/importing/import_projects_from_bitbucket.md
index 97380bce172..f3c636ed1d5 100644
--- a/doc/workflow/importing/import_projects_from_bitbucket.md
+++ b/doc/workflow/importing/import_projects_from_bitbucket.md
@@ -28,7 +28,7 @@ to enable this if not already.
When issues/pull requests are being imported, the Bitbucket importer tries to find
the Bitbucket author/assignee in GitLab's database using the Bitbucket ID. For this
to work, the Bitbucket author/assignee should have signed in beforehand in GitLab
-and [**associated their Bitbucket account**][social sign-in]. If the user is not
+and **associated their Bitbucket account**. If the user is not
found in GitLab's database, the project creator (most of the times the current
user that started the import process) is set as the author, but a reference on
the issue about the original Bitbucket author is kept.
diff --git a/doc/workflow/importing/import_projects_from_gitea.md b/doc/workflow/importing/import_projects_from_gitea.md
index 936cee89f45..f5746a0fb31 100644
--- a/doc/workflow/importing/import_projects_from_gitea.md
+++ b/doc/workflow/importing/import_projects_from_gitea.md
@@ -5,8 +5,7 @@ Import your projects from Gitea to GitLab with minimal effort.
## Overview
>**Note:**
-As of Gitea `v1.0.0`, issue & pull-request comments cannot be imported! This is
-a [known issue][issue-401] that should be fixed in a near-future.
+This requires Gitea `v1.0.0` or newer.
- At its current state, Gitea importer can import:
- the repository description (GitLab 8.15+)
@@ -76,5 +75,3 @@ If you want, you can import all your Gitea projects in one go by hitting
You can also choose a different name for the project and a different namespace,
if you have the privileges to do so.
-
-[issue-401]: https://github.com/go-gitea/gitea/issues/401
diff --git a/doc/workflow/importing/import_projects_from_github.md b/doc/workflow/importing/import_projects_from_github.md
index 86a016fc6d6..cdacef9832f 100644
--- a/doc/workflow/importing/import_projects_from_github.md
+++ b/doc/workflow/importing/import_projects_from_github.md
@@ -28,7 +28,7 @@ still be able to import their GitHub repositories with a
When issues/pull requests are being imported, the GitHub importer tries to find
the GitHub author/assignee in GitLab's database using the GitHub ID. For this
to work, the GitHub author/assignee should have signed in beforehand in GitLab
-and [**associated their GitHub account**][social sign-in]. If the user is not
+and **associated their GitHub account**. If the user is not
found in GitLab's database, the project creator (most of the times the current
user that started the import process) is set as the author, but a reference on
the issue about the original GitHub author is kept.
diff --git a/doc/workflow/importing/import_projects_from_gitlab_com.md b/doc/workflow/importing/import_projects_from_gitlab_com.md
index dcc00074b75..b27125a44de 100644
--- a/doc/workflow/importing/import_projects_from_gitlab_com.md
+++ b/doc/workflow/importing/import_projects_from_gitlab_com.md
@@ -5,6 +5,9 @@ GitLab support is enabled on your GitLab instance.
You can read more about GitLab support [here](http://docs.gitlab.com/ce/integration/gitlab.html)
To get to the importer page you need to go to "New project" page.
+>**Note:**
+If you are interested in importing Wiki and Merge Request data to your new instance, you'll need to follow the instructions for [project export](../../user/project/settings/import_export.md)
+
![New project page](gitlab_importer/new_project_page.png)
Click on the "Import projects from GitLab.com" link and you will be redirected to GitLab.com
diff --git a/doc/workflow/importing/migrating_from_svn.md b/doc/workflow/importing/migrating_from_svn.md
index 423b095e69e..7a3628a39d7 100644
--- a/doc/workflow/importing/migrating_from_svn.md
+++ b/doc/workflow/importing/migrating_from_svn.md
@@ -79,7 +79,7 @@ Now that SubGit has configured the Git/SVN repos, run `subgit` to perform the
initial translation of existing SVN revisions into the Git repository:
```
-subgit install $GIT_REPOS_PATH
+subgit install $GIT_REPO_PATH
```
After the initial translation is completed, the Git repository and the SVN
diff --git a/doc/workflow/lfs/lfs_administration.md b/doc/workflow/lfs/lfs_administration.md
index b3c73e947f0..3a6773909d6 100644
--- a/doc/workflow/lfs/lfs_administration.md
+++ b/doc/workflow/lfs/lfs_administration.md
@@ -40,6 +40,12 @@ In `config/gitlab.yml`:
storage_path: /mnt/storage/lfs-objects
```
+## Storage statistics
+
+You can see the total storage used for LFS objects on groups and projects
+in the administration area, as well as through the [groups](../../api/groups.md)
+and [projects APIs](../../api/projects.md).
+
## Known limitations
* Currently, storing GitLab Git LFS objects on a non-local storage (like S3 buckets)
@@ -47,3 +53,5 @@ In `config/gitlab.yml`:
* Currently, removing LFS objects from GitLab Git LFS storage is not supported
* LFS authentications via SSH was added with GitLab 8.12
* Only compatible with the GitLFS client versions 1.1.0 and up, or 1.0.2.
+* The storage statistics currently count each LFS object multiple times for
+ every project linking to it
diff --git a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md
index 6a7098e79d0..8c5020bee37 100644
--- a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md
+++ b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md
@@ -134,7 +134,6 @@ This behaviour is caused by Git LFS using HTTPS connections by default when a
To prevent this from happening, set the lfs url in project Git config:
```bash
-
git config --add lfs.url "http://gitlab.example.com/group/project.git/info/lfs"
```
diff --git a/doc/workflow/notifications.md b/doc/workflow/notifications.md
index c936e8833c6..4c52974e103 100644
--- a/doc/workflow/notifications.md
+++ b/doc/workflow/notifications.md
@@ -73,7 +73,7 @@ In all of the below cases, the notification will be sent to:
...with notification level "Participating" or higher
-- Watchers: users with notification level "Watch"
+- Watchers: users with notification level "Watch" (however successful pipeline would be off for watchers)
- Subscribers: anyone who manually subscribed to the issue/merge request
- Custom: Users with notification level "custom" who turned on notifications for any of the events present in the table below
diff --git a/doc/workflow/time-tracking/time-tracking-example.png b/doc/workflow/time-tracking/time-tracking-example.png
new file mode 100644
index 00000000000..bbcabb602d6
--- /dev/null
+++ b/doc/workflow/time-tracking/time-tracking-example.png
Binary files differ
diff --git a/doc/workflow/time-tracking/time-tracking-sidebar.png b/doc/workflow/time-tracking/time-tracking-sidebar.png
new file mode 100644
index 00000000000..d1ff5571f95
--- /dev/null
+++ b/doc/workflow/time-tracking/time-tracking-sidebar.png
Binary files differ
diff --git a/doc/workflow/time_tracking.md b/doc/workflow/time_tracking.md
new file mode 100644
index 00000000000..de12994c516
--- /dev/null
+++ b/doc/workflow/time_tracking.md
@@ -0,0 +1,73 @@
+# Time Tracking
+
+> Introduced in GitLab 8.14.
+
+Time Tracking allows you to track estimates and time spent on issues and merge
+requests within GitLab.
+
+## Overview
+
+Time Tracking lets you:
+* record the time spent working on an issue or a merge request,
+* add an estimate of the amount of time needed to complete an issue or a merge
+request.
+
+You don't have to indicate an estimate to enter the time spent, and vice versa.
+
+Data about time tracking is shown on the issue/merge request sidebar, as shown
+below.
+
+![Time tracking in the sidebar](time-tracking/time-tracking-sidebar.png)
+
+## How to enter data
+
+Time Tracking uses two [slash commands] that GitLab introduced with this new
+feature: `/spend` and `/estimate`.
+
+Slash commands can be used in the body of an issue or a merge request, but also
+in a comment in both an issue or a merge request.
+
+Below is an example of how you can use those new slash commands inside a comment.
+
+![Time tracking example in a comment](time-tracking/time-tracking-example.png)
+
+Adding time entries (time spent or estimates) is limited to project members.
+
+### Estimates
+
+To enter an estimate, write `/estimate`, followed by the time. For example, if
+you need to enter an estimate of 3 days, 5 hours and 10 minutes, you would write
+`/estimate 3d 5h 10m`.
+
+Every time you enter a new time estimate, any previous time estimates will be
+overridden by this new value. There should only be one valid estimate in an
+issue or a merge request.
+
+To remove an estimation entirely, use `/remove_estimation`.
+
+### Time spent
+
+To enter a time spent, use `/spend 3d 5h 10m`.
+
+Every new time spent entry will be added to the current total time spent for the
+issue or the merge request.
+
+You can remove time by entering a negative amount: `/spend -3d` will remove 3
+days from the total time spent. You can't go below 0 minutes of time spent,
+so GitLab will automatically reset the time spent if you remove a larger amount
+of time compared to the time that was entered already.
+
+To remove all the time spent at once, use `/remove_time_spent`.
+
+## Configuration
+
+The following time units are available:
+* weeks (w)
+* days (d)
+* hours (h)
+* minutes (m)
+
+Default conversion rates are 1w = 5d and 1d = 8h.
+
+[landing]: https://about.gitlab.com/features/time-tracking
+[slash-commands]: ../user/project/slash_commands.md
diff --git a/features/admin/groups.feature b/features/admin/groups.feature
deleted file mode 100644
index 657e847cf4a..00000000000
--- a/features/admin/groups.feature
+++ /dev/null
@@ -1,49 +0,0 @@
-@admin
-Feature: Admin Groups
- Background:
- Given I sign in as an admin
- And I have group with projects
- And User "John Doe" exists
- And I visit admin groups page
-
- Scenario: See group list
- Then I should be all groups
-
- Scenario: Create a group
- When I click new group link
- And submit form with new group info
- Then I should be redirected to group page
- And I should see newly created group
-
- @javascript
- Scenario: Add user into projects in group
- When I visit admin group page
- When I select user "John Doe" from user list as "Reporter"
- Then I should see "John Doe" in team list in every project as "Reporter"
-
- Scenario: Shared projects
- Given group has shared projects
- When I visit group page
- Then I should see project shared with group
-
- @javascript
- Scenario: Invite user to a group by e-mail
- When I visit admin group page
- When I select user "johndoe@gitlab.com" from user list as "Reporter"
- Then I should see "johndoe@gitlab.com" in team list in every project as "Reporter"
-
- @javascript
- Scenario: Signed in admin should be able to add himself to a group
- Given "John Doe" is owner of group "Owned"
- When I visit group "Owned" members page
- When I select current user as "Developer"
- Then I should see current user as "Developer"
-
- @javascript
- Scenario: Signed in admin should be able to remove himself from group
- Given current user is developer of group "Owned"
- When I visit group "Owned" members page
- Then I should see current user as "Developer"
- When I click on the "Remove User From Group" button for current user
- When I visit group "Owned" members page
- Then I should not see current user as "Developer"
diff --git a/features/admin/users.feature b/features/admin/users.feature
deleted file mode 100644
index 6755645778a..00000000000
--- a/features/admin/users.feature
+++ /dev/null
@@ -1,65 +0,0 @@
-@admin
-Feature: Admin Users
- Background:
- Given I sign in as an admin
- And system has users
-
- Scenario: On Admin Users
- Given I visit admin users page
- Then I should see all users
-
- Scenario: Edit user and change username to non ascii char
- When I visit admin users page
- And Click edit
- And Input non ascii char in username
- And Click save
- Then See username error message
- And Not changed form action url
-
- Scenario: Show user attributes
- Given user "Mike" with groups and projects
- Given I visit admin users page
- And click on "Mike" link
- Then I should see user "Mike" details
-
- Scenario: Edit my user attributes
- Given I visit admin users page
- And click edit on my user
- When I submit modified user
- Then I see user attributes changed
-
- @javascript
- Scenario: Remove users secondary email
- Given I visit admin users page
- And I view the user with secondary email
- And I see the secondary email
- When I click remove secondary email
- Then I should not see secondary email anymore
-
- Scenario: Show user keys
- Given user "Pete" with ssh keys
- And I visit admin users page
- And click on user "Pete"
- And click on ssh keys tab
- Then I should see key list
- And I click on the key title
- Then I should see key details
- And I click on remove key
- Then I should see the key removed
-
- Scenario: Show user identities
- Given user "Pete" with twitter account
- And I visit "Pete" identities page in admin
- Then I should see twitter details
-
- Scenario: Update user identities
- Given user "Pete" with twitter account
- And I visit "Pete" identities page in admin
- And I modify twitter identity
- Then I should see twitter details updated
-
- Scenario: Remove user identities
- Given user "Pete" with twitter account
- And I visit "Pete" identities page in admin
- And I remove twitter identity
- Then I should not see twitter details
diff --git a/features/dashboard/active_tab.feature b/features/dashboard/active_tab.feature
deleted file mode 100644
index bd883a0ebfa..00000000000
--- a/features/dashboard/active_tab.feature
+++ /dev/null
@@ -1,24 +0,0 @@
-@dashboard
-Feature: Dashboard Active Tab
- Background:
- Given I sign in as a user
-
- Scenario: On Dashboard Home
- Given I visit dashboard page
- Then the active main tab should be Home
- And no other main tabs should be active
-
- Scenario: On Dashboard Issues
- Given I visit dashboard issues page
- Then the active main tab should be Issues
- And no other main tabs should be active
-
- Scenario: On Dashboard Merge Requests
- Given I visit dashboard merge requests page
- Then the active main tab should be Merge Requests
- And no other main tabs should be active
-
- Scenario: On Dashboard Groups
- Given I visit dashboard groups page
- Then the active main tab should be Groups
- And no other main tabs should be active
diff --git a/features/dashboard/archived_projects.feature b/features/dashboard/archived_projects.feature
deleted file mode 100644
index bed9282f1c6..00000000000
--- a/features/dashboard/archived_projects.feature
+++ /dev/null
@@ -1,17 +0,0 @@
-@dashboard
-Feature: Dashboard Archived Projects
- Background:
- Given I sign in as a user
- And I own project "Shop"
- And I own project "Forum"
- And project "Forum" is archived
- And I visit dashboard page
-
- Scenario: I should see non-archived projects on dashboard
- Then I should see "Shop" project link
- And I should not see "Forum" project link
-
- Scenario: I toggle show of archived projects on dashboard
- When I click "Show archived projects" link
- Then I should see "Shop" project link
- And I should see "Forum" project link
diff --git a/features/dashboard/group.feature b/features/dashboard/group.feature
deleted file mode 100644
index 3ae2c679dc1..00000000000
--- a/features/dashboard/group.feature
+++ /dev/null
@@ -1,13 +0,0 @@
-@dashboard
-Feature: Dashboard Group
- Background:
- Given I sign in as "John Doe"
- And "John Doe" is owner of group "Owned"
- And "John Doe" is guest of group "Guest"
-
- Scenario: Create a group from dasboard
- And I visit dashboard groups page
- And I click new group link
- And submit form with new group "Samurai" info
- Then I should be redirected to group "Samurai" page
- And I should see newly created group "Samurai"
diff --git a/features/dashboard/help.feature b/features/dashboard/help.feature
deleted file mode 100644
index bca2772897b..00000000000
--- a/features/dashboard/help.feature
+++ /dev/null
@@ -1,9 +0,0 @@
-@dashboard
-Feature: Dashboard Help
- Background:
- Given I sign in as a user
- And I visit the "Rake Tasks" help page
-
- Scenario: The markdown should be rendered correctly
- Then I should see "Rake Tasks" page markdown rendered
- And Header "Rebuild project satellites" should have correct ids and links
diff --git a/features/dashboard/shortcuts.feature b/features/dashboard/shortcuts.feature
deleted file mode 100644
index 41d79aa6ec8..00000000000
--- a/features/dashboard/shortcuts.feature
+++ /dev/null
@@ -1,21 +0,0 @@
-@dashboard
-Feature: Dashboard Shortcuts
- Background:
- Given I sign in as a user
- And I visit dashboard page
-
- @javascript
- Scenario: Navigate to projects tab
- Given I press "g" and "p"
- Then the active main tab should be Projects
-
- @javascript
- Scenario: Navigate to issue tab
- Given I press "g" and "i"
- Then the active main tab should be Issues
-
- @javascript
- Scenario: Navigate to merge requests tab
- Given I press "g" and "m"
- Then the active main tab should be Merge Requests
-
diff --git a/features/project/active_tab.feature b/features/project/active_tab.feature
index 57dda9c2234..5c14c5db665 100644
--- a/features/project/active_tab.feature
+++ b/features/project/active_tab.feature
@@ -39,10 +39,10 @@ Feature: Project Active Tab
# Sub Tabs: Settings
- Scenario: On Project Settings/Hooks
+ Scenario: On Project Settings/Integrations
Given I visit my project's settings page
- And I click the "Hooks" tab
- Then the active sub nav should be Hooks
+ And I click the "Integrations" tab
+ Then the active sub nav should be Integrations
And no other sub navs should be active
And the active main tab should be Settings
@@ -53,6 +53,13 @@ Feature: Project Active Tab
And no other sub navs should be active
And the active main tab should be Settings
+ Scenario: On Project Settings/Pages
+ Given I visit my project's settings page
+ And I click the "Pages" tab
+ Then the active sub nav should be Pages
+ And no other sub navs should be active
+ And the active main tab should be Settings
+
Scenario: On Project Members
Given I visit my project's members page
Then the active sub nav should be Members
diff --git a/features/project/commits/commits.feature b/features/project/commits/commits.feature
index 1776c07e60e..3459cce03f9 100644
--- a/features/project/commits/commits.feature
+++ b/features/project/commits/commits.feature
@@ -47,8 +47,6 @@ Feature: Project Commits
And repository contains ".gitlab-ci.yml" file
When I click on commit link
Then I see commit ci info
- And I click status link
- Then I see builds list
Scenario: I browse commit with side-by-side diff view
Given I click on commit link
diff --git a/features/project/issues/filter_labels.feature b/features/project/issues/filter_labels.feature
deleted file mode 100644
index 49d7a3b9af2..00000000000
--- a/features/project/issues/filter_labels.feature
+++ /dev/null
@@ -1,28 +0,0 @@
-@project_issues
-Feature: Project Issues Filter Labels
- Background:
- Given I sign in as a user
- And I own project "Shop"
- And project "Shop" has labels: "bug", "feature", "enhancement"
- And project "Shop" has issue "Bugfix1" with labels: "bug", "feature"
- And project "Shop" has issue "Bugfix2" with labels: "bug", "enhancement"
- And project "Shop" has issue "Feature1" with labels: "feature"
- Given I visit project "Shop" issues page
-
- @javascript
- Scenario: I filter by one label
- Given I click link "bug"
- And I click "dropdown close button"
- Then I should see "Bugfix1" in issues list
- And I should see "Bugfix2" in issues list
- And I should not see "Feature1" in issues list
-
- # TODO: make labels filter works according to this scanario
- # right now it looks for label 1 OR label 2. Old behaviour (this test) was
- # all issues that have both label 1 AND label 2
- #Scenario: I filter by two labels
- #Given I click link "bug"
- #And I click link "feature"
- #Then I should see "Bugfix1" in issues list
- #And I should not see "Bugfix2" in issues list
- #And I should not see "Feature1" in issues list
diff --git a/features/project/issues/issues.feature b/features/project/issues/issues.feature
index 80670063ea0..b2b4fe72220 100644
--- a/features/project/issues/issues.feature
+++ b/features/project/issues/issues.feature
@@ -26,12 +26,6 @@ Feature: Project Issues
Given I click link "Release 0.4"
Then I should see issue "Release 0.4"
- @javascript
- Scenario: I filter by author
- Given I add a user to project "Shop"
- And I click "author" dropdown
- Then I see current user as the first user
-
Scenario: I submit new unassigned issue
Given I click link "New Issue"
And I submit new issue "500 error on profile"
@@ -84,56 +78,6 @@ Feature: Project Issues
And I sort the list by "Least popular"
Then The list should be sorted by "Least popular"
- @javascript
- Scenario: I search issue
- Given I fill in issue search with "Re"
- Then I should see "Release 0.4" in issues
- And I should not see "Release 0.3" in issues
- And I should not see "Tweet control" in issues
-
- @javascript
- Scenario: I search issue that not exist
- Given I fill in issue search with "Bu"
- Then I should not see "Release 0.4" in issues
- And I should not see "Release 0.3" in issues
-
- @javascript
- Scenario: I search all issues
- Given I click link "All"
- And I fill in issue search with ".3"
- Then I should see "Release 0.3" in issues
- And I should not see "Release 0.4" in issues
-
- @javascript
- Scenario: Search issues when search string exactly matches issue description
- Given project 'Shop' has issue 'Bugfix1' with description: 'Description for issue1'
- And I fill in issue search with 'Description for issue1'
- Then I should see 'Bugfix1' in issues
- And I should not see "Release 0.4" in issues
- And I should not see "Release 0.3" in issues
- And I should not see "Tweet control" in issues
-
- @javascript
- Scenario: Search issues when search string partially matches issue description
- Given project 'Shop' has issue 'Bugfix1' with description: 'Description for issue1'
- And project 'Shop' has issue 'Feature1' with description: 'Feature submitted for issue1'
- And I fill in issue search with 'issue1'
- Then I should see 'Feature1' in issues
- Then I should see 'Bugfix1' in issues
- And I should not see "Release 0.4" in issues
- And I should not see "Release 0.3" in issues
- And I should not see "Tweet control" in issues
-
- @javascript
- Scenario: Search issues when search string matches no issue description
- Given project 'Shop' has issue 'Bugfix1' with description: 'Description for issue1'
- And I fill in issue search with 'Rock and roll'
- Then I should not see 'Bugfix1' in issues
- And I should not see "Release 0.4" in issues
- And I should not see "Release 0.3" in issues
- And I should not see "Tweet control" in issues
-
-
# Markdown
Scenario: Headers inside the description should have ids generated for them.
diff --git a/features/project/pages.feature b/features/project/pages.feature
new file mode 100644
index 00000000000..87d88348d09
--- /dev/null
+++ b/features/project/pages.feature
@@ -0,0 +1,82 @@
+Feature: Project Pages
+ Background:
+ Given I sign in as a user
+ And I own a project
+
+ Scenario: Pages are disabled
+ Given pages are disabled
+ When I visit the Project Pages
+ Then I should see that GitLab Pages are disabled
+
+ Scenario: I can see the pages usage if not deployed
+ Given pages are enabled
+ When I visit the Project Pages
+ Then I should see the usage of GitLab Pages
+
+ Scenario: I can access the pages if deployed
+ Given pages are enabled
+ And pages are deployed
+ When I visit the Project Pages
+ Then I should be able to access the Pages
+
+ Scenario: I should message that domains support is disabled
+ Given pages are enabled
+ And pages are deployed
+ And support for external domains is disabled
+ When I visit the Project Pages
+ Then I should see that support for domains is disabled
+
+ Scenario: I should see a new domain button
+ Given pages are enabled
+ And pages are exposed on external HTTP address
+ When I visit the Project Pages
+ And I should be able to add a New Domain
+
+ Scenario: I should be able to add a new domain
+ Given pages are enabled
+ And pages are exposed on external HTTP address
+ When I visit add a new Pages Domain
+ And I fill the domain
+ And I click on "Create New Domain"
+ Then I should see a new domain added
+
+ Scenario: I should be able to add a new domain for project in group namespace
+ Given I own a project in some group namespace
+ And pages are enabled
+ And pages are exposed on external HTTP address
+ When I visit add a new Pages Domain
+ And I fill the domain
+ And I click on "Create New Domain"
+ Then I should see a new domain added
+
+ Scenario: I should be denied to add the same domain twice
+ Given pages are enabled
+ And pages are exposed on external HTTP address
+ And pages domain is added
+ When I visit add a new Pages Domain
+ And I fill the domain
+ And I click on "Create New Domain"
+ Then I should see error message that domain already exists
+
+ Scenario: I should message that certificates support is disabled when trying to add a new domain
+ Given pages are enabled
+ And pages are exposed on external HTTP address
+ And pages domain is added
+ When I visit add a new Pages Domain
+ Then I should see that support for certificates is disabled
+
+ Scenario: I should be able to add a new domain with certificate
+ Given pages are enabled
+ And pages are exposed on external HTTPS address
+ When I visit add a new Pages Domain
+ And I fill the domain
+ And I fill the certificate and key
+ And I click on "Create New Domain"
+ Then I should see a new domain added
+
+ Scenario: I can remove the pages if deployed
+ Given pages are enabled
+ And pages are deployed
+ When I visit the Project Pages
+ And I click Remove Pages
+ Then The Pages should get removed
diff --git a/features/steps/admin/groups.rb b/features/steps/admin/groups.rb
deleted file mode 100644
index 9396a76f0a2..00000000000
--- a/features/steps/admin/groups.rb
+++ /dev/null
@@ -1,143 +0,0 @@
-class Spinach::Features::AdminGroups < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedGroup
- include SharedPaths
- include SharedUser
- include SharedActiveTab
- include Select2Helper
-
- When 'I visit admin group page' do
- visit admin_group_path(current_group)
- end
-
- When 'I click new group link' do
- click_link "New Group"
- end
-
- step 'I have group with projects' do
- @group = create(:group)
- @project = create(:project, group: @group)
- @event = create(:closed_issue_event, project: @project)
-
- @project.team << [current_user, :master]
- end
-
- step 'submit form with new group info' do
- fill_in 'group_path', with: 'gitlab'
- fill_in 'group_description', with: 'Group description'
- click_button "Create group"
- end
-
- step 'I should see newly created group' do
- expect(page).to have_content "Group: gitlab"
- expect(page).to have_content "Group description"
- end
-
- step 'I should be redirected to group page' do
- expect(current_path).to eq admin_group_path(Group.find_by(path: 'gitlab'))
- end
-
- When 'I select user "John Doe" from user list as "Reporter"' do
- select2(user_john.id, from: "#user_ids", multiple: true)
- page.within "#new_project_member" do
- select "Reporter", from: "access_level"
- end
- click_button "Add users to group"
- end
-
- When 'I select user "johndoe@gitlab.com" from user list as "Reporter"' do
- select2('johndoe@gitlab.com', from: "#user_ids", multiple: true)
- page.within "#new_project_member" do
- select "Reporter", from: "access_level"
- end
- click_button "Add users to group"
- end
-
- step 'I should see "John Doe" in team list in every project as "Reporter"' do
- page.within ".group-users-list" do
- expect(page).to have_content "John Doe"
- expect(page).to have_content "Reporter"
- end
- end
-
- step 'I should see "johndoe@gitlab.com" in team list in every project as "Reporter"' do
- page.within ".group-users-list" do
- expect(page).to have_content "johndoe@gitlab.com"
- expect(page).to have_content "Invited by"
- expect(page).to have_content "Reporter"
- end
- end
-
- step 'I should be all groups' do
- Group.all.each do |group|
- expect(page).to have_content group.name
- end
- end
-
- step 'group has shared projects' do
- share_link = shared_project.project_group_links.new(group_access: Gitlab::Access::MASTER)
- share_link.group_id = current_group.id
- share_link.save!
- end
-
- step 'I visit group page' do
- visit admin_group_path(current_group)
- end
-
- step 'I should see project shared with group' do
- expect(page).to have_content(shared_project.name_with_namespace)
- expect(page).to have_content "Projects shared with"
- end
-
- step 'we have user "John Doe" in group' do
- current_group.add_reporter(user_john)
- end
-
- step 'I should not see "John Doe" in team list' do
- page.within ".group-users-list" do
- expect(page).not_to have_content "John Doe"
- end
- end
-
- step 'I select current user as "Developer"' do
- page.within ".users-group-form" do
- select2(current_user.id, from: "#user_ids", multiple: true)
- select "Developer", from: "access_level"
- end
-
- click_button "Add to group"
- end
-
- step 'I should see current user as "Developer"' do
- page.within '.content-list' do
- expect(page).to have_content(current_user.name)
- expect(page).to have_content('Developer')
- end
- end
-
- step 'I click on the "Remove User From Group" button for current user' do
- find(:css, 'li', text: current_user.name).find(:css, 'a.btn-remove').click
- # poltergeist always confirms popups.
- end
-
- step 'I should not see current user as "Developer"' do
- page.within '.content-list' do
- expect(page).not_to have_content(current_user.name)
- expect(page).not_to have_content('Developer')
- end
- end
-
- protected
-
- def current_group
- @group ||= Group.first
- end
-
- def shared_project
- @shared_project ||= create(:empty_project)
- end
-
- def user_john
- @user_john ||= User.find_by(name: "John Doe")
- end
-end
diff --git a/features/steps/admin/users.rb b/features/steps/admin/users.rb
deleted file mode 100644
index 8fb8a86d58b..00000000000
--- a/features/steps/admin/users.rb
+++ /dev/null
@@ -1,167 +0,0 @@
-class Spinach::Features::AdminUsers < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedPaths
- include SharedAdmin
-
- before do
- allow(Gitlab::OAuth::Provider).to receive(:providers).and_return([:twitter, :twitter_updated])
- allow_any_instance_of(ApplicationHelper).to receive(:user_omniauth_authorize_path).and_return(root_path)
- end
-
- after do
- allow(Gitlab::OAuth::Provider).to receive(:providers).and_call_original
- allow_any_instance_of(ApplicationHelper).to receive(:user_omniauth_authorize_path).and_call_original
- end
-
- step 'I should see all users' do
- User.all.each do |user|
- expect(page).to have_content user.name
- end
- end
-
- step 'Click edit' do
- @user = User.first
- find("#edit_user_#{@user.id}").click
- end
-
- step 'Input non ascii char in username' do
- fill_in 'user_username', with: "\u3042\u3044"
- end
-
- step 'Click save' do
- click_button("Save")
- end
-
- step 'See username error message' do
- page.within "#error_explanation" do
- expect(page).to have_content "Username"
- end
- end
-
- step 'Not changed form action url' do
- expect(page).to have_selector %(form[action="/admin/users/#{@user.username}"])
- end
-
- step 'I submit modified user' do
- check :user_can_create_group
- click_button 'Save'
- end
-
- step 'I see user attributes changed' do
- expect(page).to have_content 'Can create groups: Yes'
- end
-
- step 'click edit on my user' do
- find("#edit_user_#{current_user.id}").click
- end
-
- step 'I view the user with secondary email' do
- @user_with_secondary_email = User.last
- @user_with_secondary_email.emails.new(email: "secondary@example.com")
- @user_with_secondary_email.save
- visit "/admin/users/#{@user_with_secondary_email.username}"
- end
-
- step 'I see the secondary email' do
- expect(page).to have_content "Secondary email: #{@user_with_secondary_email.emails.last.email}"
- end
-
- step 'I click remove secondary email' do
- find("#remove_email_#{@user_with_secondary_email.emails.last.id}").click
- end
-
- step 'I should not see secondary email anymore' do
- expect(page).not_to have_content "Secondary email:"
- end
-
- step 'user "Mike" with groups and projects' do
- user = create(:user, name: 'Mike')
-
- project = create(:empty_project)
- project.team << [user, :developer]
-
- group = create(:group)
- group.add_developer(user)
- end
-
- step 'click on "Mike" link' do
- click_link "Mike"
- end
-
- step 'I should see user "Mike" details' do
- expect(page).to have_content 'Account'
- expect(page).to have_content 'Personal projects limit'
- end
-
- step 'user "Pete" with ssh keys' do
- user = create(:user, name: 'Pete')
- create(:key, user: user, title: "ssh-rsa Key1", key: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC4FIEBXGi4bPU8kzxMefudPIJ08/gNprdNTaO9BR/ndy3+58s2HCTw2xCHcsuBmq+TsAqgEidVq4skpqoTMB+Uot5Uzp9z4764rc48dZiI661izoREoKnuRQSsRqUTHg5wrLzwxlQbl1MVfRWQpqiz/5KjBC7yLEb9AbusjnWBk8wvC1bQPQ1uLAauEA7d836tgaIsym9BrLsMVnR4P1boWD3Xp1B1T/ImJwAGHvRmP/ycIqmKdSpMdJXwxcb40efWVj0Ibbe7ii9eeoLdHACqevUZi6fwfbymdow+FeqlkPoHyGg3Cu4vD/D8+8cRc7mE/zGCWcQ15Var83Tczour Key1")
- create(:key, user: user, title: "ssh-rsa Key2", key: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDQSTWXhJAX/He+nG78MiRRRn7m0Pb0XbcgTxE0etArgoFoh9WtvDf36HG6tOSg/0UUNcp0dICsNAmhBKdncp6cIyPaXJTURPRAGvhI0/VDk4bi27bRnccGbJ/hDaUxZMLhhrzY0r22mjVf8PF6dvv5QUIQVm1/LeaWYsHHvLgiIjwrXirUZPnFrZw6VLREoBKG8uWvfSXw1L5eapmstqfsME8099oi+vWLR8MgEysZQmD28M73fgW4zek6LDQzKQyJx9nB+hJkKUDvcuziZjGmRFlNgSA2mguERwL1OXonD8WYUrBDGKroIvBT39zS5d9tQDnidEJZ9Y8gv5ViYP7x Key2")
- end
-
- step 'click on user "Pete"' do
- click_link 'Pete'
- end
-
- step 'I should see key list' do
- expect(page).to have_content 'ssh-rsa Key2'
- expect(page).to have_content 'ssh-rsa Key1'
- end
-
- step 'I click on the key title' do
- click_link 'ssh-rsa Key2'
- end
-
- step 'I should see key details' do
- expect(page).to have_content 'ssh-rsa Key2'
- expect(page).to have_content 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDQSTWXhJAX/He+nG78MiRRRn7m0Pb0XbcgTxE0etArgoFoh9WtvDf36HG6tOSg/0UUNcp0dICsNAmhBKdncp6cIyPaXJTURPRAGvhI0/VDk4bi27bRnccGbJ/hDaUxZMLhhrzY0r22mjVf8PF6dvv5QUIQVm1/LeaWYsHHvLgiIjwrXirUZPnFrZw6VLREoBKG8uWvfSXw1L5eapmstqfsME8099oi+vWLR8MgEysZQmD28M73fgW4zek6LDQzKQyJx9nB+hJkKUDvcuziZjGmRFlNgSA2mguERwL1OXonD8WYUrBDGKroIvBT39zS5d9tQDnidEJZ9Y8gv5ViYP7x Key2'
- end
-
- step 'I click on remove key' do
- click_link 'Remove'
- end
-
- step 'I should see the key removed' do
- expect(page).not_to have_content 'ssh-rsa Key2'
- end
-
- step 'user "Pete" with twitter account' do
- @user = create(:user, name: 'Pete')
- @user.identities.create!(extern_uid: '123456', provider: 'twitter')
- end
-
- step 'I visit "Pete" identities page in admin' do
- visit admin_user_identities_path(@user)
- end
-
- step 'I should see twitter details' do
- expect(page).to have_content 'Pete'
- expect(page).to have_content 'twitter'
- end
-
- step 'I modify twitter identity' do
- find('.table').find(:link, 'Edit').click
- fill_in 'identity_extern_uid', with: '654321'
- select 'twitter_updated', from: 'identity_provider'
- click_button 'Save changes'
- end
-
- step 'I should see twitter details updated' do
- expect(page).to have_content 'Pete'
- expect(page).to have_content 'twitter_updated'
- expect(page).to have_content '654321'
- end
-
- step 'I remove twitter identity' do
- click_link 'Delete'
- end
-
- step 'I should not see twitter details' do
- expect(page).to have_content 'Pete'
- expect(page).not_to have_content 'twitter'
- end
-
- step 'click on ssh keys tab' do
- click_link 'SSH keys'
- end
-end
diff --git a/features/steps/dashboard/active_tab.rb b/features/steps/dashboard/active_tab.rb
deleted file mode 100644
index 04fe96cef22..00000000000
--- a/features/steps/dashboard/active_tab.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-class Spinach::Features::DashboardActiveTab < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedPaths
- include SharedSidebarActiveTab
-end
diff --git a/features/steps/dashboard/archived_projects.rb b/features/steps/dashboard/archived_projects.rb
deleted file mode 100644
index 6510f8d9b32..00000000000
--- a/features/steps/dashboard/archived_projects.rb
+++ /dev/null
@@ -1,26 +0,0 @@
-class Spinach::Features::DashboardArchivedProjects < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedPaths
- include SharedProject
-
- When 'project "Forum" is archived' do
- project = Project.find_by(name: "Forum")
- project.update_attribute(:archived, true)
- end
-
- step 'I should see "Shop" project link' do
- expect(page).to have_link "Shop"
- end
-
- step 'I should not see "Forum" project link' do
- expect(page).not_to have_link "Forum"
- end
-
- step 'I should see "Forum" project link' do
- expect(page).to have_link "Forum"
- end
-
- step 'I click "Show archived projects" link' do
- click_link "Show archived projects"
- end
-end
diff --git a/features/steps/dashboard/dashboard.rb b/features/steps/dashboard/dashboard.rb
index b2bec369e0f..33a1c88e33c 100644
--- a/features/steps/dashboard/dashboard.rb
+++ b/features/steps/dashboard/dashboard.rb
@@ -35,7 +35,7 @@ class Spinach::Features::Dashboard < Spinach::FeatureSteps
step 'I have group with projects' do
@group = create(:group)
- @project = create(:project, namespace: @group)
+ @project = create(:empty_project, namespace: @group)
@event = create(:closed_issue_event, project: @project)
@project.team << [current_user, :master]
@@ -54,8 +54,8 @@ class Spinach::Features::Dashboard < Spinach::FeatureSteps
end
step 'group has a projects that does not belongs to me' do
- @forbidden_project1 = create(:project, group: @group)
- @forbidden_project2 = create(:project, group: @group)
+ @forbidden_project1 = create(:empty_project, group: @group)
+ @forbidden_project2 = create(:empty_project, group: @group)
end
step 'I should see 1 project at group list' do
diff --git a/features/steps/dashboard/group.rb b/features/steps/dashboard/group.rb
deleted file mode 100644
index cf679fea530..00000000000
--- a/features/steps/dashboard/group.rb
+++ /dev/null
@@ -1,25 +0,0 @@
-class Spinach::Features::DashboardGroup < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedGroup
- include SharedPaths
- include SharedUser
-
- step 'I click new group link' do
- click_link "New Group"
- end
-
- step 'submit form with new group "Samurai" info' do
- fill_in 'group_path', with: 'Samurai'
- fill_in 'group_description', with: 'Tokugawa Shogunate'
- click_button "Create group"
- end
-
- step 'I should be redirected to group "Samurai" page' do
- expect(current_path).to eq group_path(Group.find_by(name: 'Samurai'))
- end
-
- step 'I should see newly created group "Samurai"' do
- expect(page).to have_content "Samurai"
- expect(page).to have_content "Tokugawa Shogunate"
- end
-end
diff --git a/features/steps/dashboard/help.rb b/features/steps/dashboard/help.rb
deleted file mode 100644
index 3c5bf44c538..00000000000
--- a/features/steps/dashboard/help.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-class Spinach::Features::DashboardHelp < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedPaths
- include SharedMarkdown
-
- step 'I visit the help page' do
- visit help_path
- end
-
- step 'I visit the "Rake Tasks" help page' do
- visit help_page_path("administration/raketasks/maintenance")
- end
-
- step 'I should see "Rake Tasks" page markdown rendered' do
- expect(page).to have_content "Gather information about GitLab and the system it runs on"
- end
-
- step 'Header "Rebuild project satellites" should have correct ids and links' do
- header_should_have_correct_id_and_link(2, 'Check GitLab configuration', 'check-gitlab-configuration', '.documentation')
- end
-end
diff --git a/features/steps/dashboard/issues.rb b/features/steps/dashboard/issues.rb
index 39c65bb6cde..4e15d79ae74 100644
--- a/features/steps/dashboard/issues.rb
+++ b/features/steps/dashboard/issues.rb
@@ -79,13 +79,13 @@ class Spinach::Features::DashboardIssues < Spinach::FeatureSteps
def project
@project ||= begin
- project = create :project
+ project = create(:empty_project)
project.team << [current_user, :master]
project
end
end
def public_project
- @public_project ||= create :project, :public
+ @public_project ||= create(:empty_project, :public)
end
end
diff --git a/features/steps/dashboard/merge_requests.rb b/features/steps/dashboard/merge_requests.rb
index 6777101fb15..909ffec3646 100644
--- a/features/steps/dashboard/merge_requests.rb
+++ b/features/steps/dashboard/merge_requests.rb
@@ -105,14 +105,14 @@ class Spinach::Features::DashboardMergeRequests < Spinach::FeatureSteps
def project
@project ||= begin
- project = create :project
+ project = create(:project, :repository)
project.team << [current_user, :master]
project
end
end
def public_project
- @public_project ||= create :project, :public
+ @public_project ||= create(:project, :public, :repository)
end
def forked_project
diff --git a/features/steps/dashboard/shortcuts.rb b/features/steps/dashboard/shortcuts.rb
deleted file mode 100644
index 118d27888df..00000000000
--- a/features/steps/dashboard/shortcuts.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-class Spinach::Features::DashboardShortcuts < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedPaths
- include SharedProject
- include SharedSidebarActiveTab
- include SharedShortcuts
-end
diff --git a/features/steps/dashboard/todos.rb b/features/steps/dashboard/todos.rb
index 344b6fda9a6..2bbc43b491f 100644
--- a/features/steps/dashboard/todos.rb
+++ b/features/steps/dashboard/todos.rb
@@ -25,15 +25,18 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps
end
step 'I should see todos assigned to me' do
+ merge_request_reference = merge_request.to_reference(full: true)
+ issue_reference = issue.to_reference(full: true)
+
page.within('.todos-pending-count') { expect(page).to have_content '4' }
expect(page).to have_content 'To do 4'
expect(page).to have_content 'Done 0'
expect(page).to have_link project.name_with_namespace
- should_see_todo(1, "John Doe assigned you merge request #{merge_request.to_reference}", merge_request.title)
- should_see_todo(2, "John Doe mentioned you on issue #{issue.to_reference}", "#{current_user.to_reference} Wdyt?")
- should_see_todo(3, "John Doe assigned you issue #{issue.to_reference}", issue.title)
- should_see_todo(4, "Mary Jane mentioned you on issue #{issue.to_reference}", issue.title)
+ should_see_todo(1, "John Doe assigned you merge request #{merge_request_reference}", merge_request.title)
+ should_see_todo(2, "John Doe mentioned you on issue #{issue_reference}", "#{current_user.to_reference} Wdyt?")
+ should_see_todo(3, "John Doe assigned you issue #{issue_reference}", issue.title)
+ should_see_todo(4, "Mary Jane mentioned you on issue #{issue_reference}", issue.title)
end
step 'I mark the todo as done' do
@@ -44,10 +47,13 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps
page.within('.todos-pending-count') { expect(page).to have_content '3' }
expect(page).to have_content 'To do 3'
expect(page).to have_content 'Done 1'
- should_not_see_todo "John Doe assigned you merge request #{merge_request.to_reference}"
+ should_not_see_todo "John Doe assigned you merge request #{merge_request.to_reference(full: true)}"
end
step 'I mark all todos as done' do
+ merge_request_reference = merge_request.to_reference(full: true)
+ issue_reference = issue.to_reference(full: true)
+
click_link 'Mark all as done'
page.within('.todos-pending-count') { expect(page).to have_content '0' }
@@ -55,27 +61,30 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps
expect(page).to have_content 'Done 4'
expect(page).to have_content "You're all done!"
expect('.prepend-top-default').not_to have_link project.name_with_namespace
- should_not_see_todo "John Doe assigned you merge request #{merge_request.to_reference}"
- should_not_see_todo "John Doe mentioned you on issue #{issue.to_reference}"
- should_not_see_todo "John Doe assigned you issue #{issue.to_reference}"
- should_not_see_todo "Mary Jane mentioned you on issue #{issue.to_reference}"
+ should_not_see_todo "John Doe assigned you merge request #{merge_request_reference}"
+ should_not_see_todo "John Doe mentioned you on issue #{issue_reference}"
+ should_not_see_todo "John Doe assigned you issue #{issue_reference}"
+ should_not_see_todo "Mary Jane mentioned you on issue #{issue_reference}"
end
step 'I should see the todo marked as done' do
click_link 'Done 1'
expect(page).to have_link project.name_with_namespace
- should_see_todo(1, "John Doe assigned you merge request #{merge_request.to_reference}", merge_request.title, false)
+ should_see_todo(1, "John Doe assigned you merge request #{merge_request.to_reference(full: true)}", merge_request.title, false)
end
step 'I should see all todos marked as done' do
+ merge_request_reference = merge_request.to_reference(full: true)
+ issue_reference = issue.to_reference(full: true)
+
click_link 'Done 4'
expect(page).to have_link project.name_with_namespace
- should_see_todo(1, "John Doe assigned you merge request #{merge_request.to_reference}", merge_request.title, false)
- should_see_todo(2, "John Doe mentioned you on issue #{issue.to_reference}", "#{current_user.to_reference} Wdyt?", false)
- should_see_todo(3, "John Doe assigned you issue #{issue.to_reference}", issue.title, false)
- should_see_todo(4, "Mary Jane mentioned you on issue #{issue.to_reference}", issue.title, false)
+ should_see_todo(1, "John Doe assigned you merge request #{merge_request_reference}", merge_request.title, false)
+ should_see_todo(2, "John Doe mentioned you on issue #{issue_reference}", "#{current_user.to_reference} Wdyt?", false)
+ should_see_todo(3, "John Doe assigned you issue #{issue_reference}", issue.title, false)
+ should_see_todo(4, "Mary Jane mentioned you on issue #{issue_reference}", issue.title, false)
end
step 'I filter by "Enterprise"' do
@@ -111,16 +120,16 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps
end
step 'I should not see todos related to "Mary Jane" in the list' do
- should_not_see_todo "Mary Jane mentioned you on issue #{issue.to_reference}"
+ should_not_see_todo "Mary Jane mentioned you on issue #{issue.to_reference(full: true)}"
end
step 'I should not see todos related to "Merge Requests" in the list' do
- should_not_see_todo "John Doe assigned you merge request #{merge_request.to_reference}"
+ should_not_see_todo "John Doe assigned you merge request #{merge_request.to_reference(full: true)}"
end
step 'I should not see todos related to "Assignments" in the list' do
- should_not_see_todo "John Doe assigned you merge request #{merge_request.to_reference}"
- should_not_see_todo "John Doe assigned you issue #{issue.to_reference}"
+ should_not_see_todo "John Doe assigned you merge request #{merge_request.to_reference(full: true)}"
+ should_not_see_todo "John Doe assigned you issue #{issue.to_reference(full: true)}"
end
step 'I click on the todo' do
diff --git a/features/steps/group/milestones.rb b/features/steps/group/milestones.rb
index c1d1eca9116..70e23098dde 100644
--- a/features/steps/group/milestones.rb
+++ b/features/steps/group/milestones.rb
@@ -104,7 +104,7 @@ class Spinach::Features::GroupMilestones < Spinach::FeatureSteps
group = owned_group
%w(gitlabhq gitlab-ci cookbook-gitlab).each do |path|
- project = create :project, path: path, group: group
+ project = create(:empty_project, path: path, group: group)
milestone = create :milestone, title: "Version 7.2", project: project
create(:label, project: project, title: 'bug')
diff --git a/features/steps/groups.rb b/features/steps/groups.rb
index 0c88838767c..4dc87dc4d9c 100644
--- a/features/steps/groups.rb
+++ b/features/steps/groups.rb
@@ -109,7 +109,7 @@ class Spinach::Features::Groups < Spinach::FeatureSteps
step 'Group "Owned" has archived project' do
group = Group.find_by(name: 'Owned')
- @archived_project = create(:project, namespace: group, archived: true, path: "archived-project")
+ @archived_project = create(:empty_project, :archived, namespace: group, path: "archived-project")
end
step 'I should see "archived" label' do
diff --git a/features/steps/profile/profile.rb b/features/steps/profile/profile.rb
index ea480d2ad68..24cfbaad7fe 100644
--- a/features/steps/profile/profile.rb
+++ b/features/steps/profile/profile.rb
@@ -162,7 +162,7 @@ class Spinach::Features::Profile < Spinach::FeatureSteps
step 'I have group with projects' do
@group = create(:group)
@group.add_owner(current_user)
- @project = create(:project, namespace: @group)
+ @project = create(:project, :repository, namespace: @group)
@event = create(:closed_issue_event, project: @project)
@project.team << [current_user, :master]
diff --git a/features/steps/project/active_tab.rb b/features/steps/project/active_tab.rb
index 58225032859..e842d7bec2b 100644
--- a/features/steps/project/active_tab.rb
+++ b/features/steps/project/active_tab.rb
@@ -27,26 +27,34 @@ class Spinach::Features::ProjectActiveTab < Spinach::FeatureSteps
end
end
- step 'I click the "Hooks" tab' do
- click_link('Webhooks')
+ step 'I click the "Integrations" tab' do
+ click_link('Integrations')
end
step 'I click the "Deploy Keys" tab' do
click_link('Deploy Keys')
end
+ step 'I click the "Pages" tab' do
+ click_link('Pages')
+ end
+
step 'the active sub nav should be Members' do
ensure_active_sub_nav('Members')
end
- step 'the active sub nav should be Hooks' do
- ensure_active_sub_nav('Webhooks')
+ step 'the active sub nav should be Integrations' do
+ ensure_active_sub_nav('Integrations')
end
step 'the active sub nav should be Deploy Keys' do
ensure_active_sub_nav('Deploy Keys')
end
+ step 'the active sub nav should be Pages' do
+ ensure_active_sub_nav('Pages')
+ end
+
# Sub Tabs: Commits
step 'I click the "Compare" tab' do
diff --git a/features/steps/project/builds/summary.rb b/features/steps/project/builds/summary.rb
index 374eb0b0e07..19ff92f6dc6 100644
--- a/features/steps/project/builds/summary.rb
+++ b/features/steps/project/builds/summary.rb
@@ -33,7 +33,7 @@ class Spinach::Features::ProjectBuildsSummary < Spinach::FeatureSteps
step 'recent build summary contains information saying that build has been erased' do
page.within('.erased') do
- expect(page).to have_content 'Build has been erased'
+ expect(page).to have_content 'Job has been erased'
end
end
diff --git a/features/steps/project/commits/commits.rb b/features/steps/project/commits/commits.rb
index 007dfb67a77..18e267294e4 100644
--- a/features/steps/project/commits/commits.rb
+++ b/features/steps/project/commits/commits.rb
@@ -166,15 +166,6 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps
expect(page).to have_content "Pipeline #1 for 570e7b2a pending"
end
- step 'I click status link' do
- find('.commit-ci-menu').click_link "Builds"
- end
-
- step 'I see builds list' do
- expect(page).to have_content "Pipeline #1 for 570e7b2a pending"
- expect(page).to have_content "1 build"
- end
-
step 'I search "submodules" commits' do
fill_in 'commits-search', with: 'submodules'
end
diff --git a/features/steps/project/deploy_keys.rb b/features/steps/project/deploy_keys.rb
index 83b9ef48392..edf78f62f9a 100644
--- a/features/steps/project/deploy_keys.rb
+++ b/features/steps/project/deploy_keys.rb
@@ -46,11 +46,11 @@ class Spinach::Features::ProjectDeployKeys < Spinach::FeatureSteps
end
step 'other projects have deploy keys' do
- @second_project = create(:project, namespace: create(:group))
+ @second_project = create(:empty_project, namespace: create(:group))
@second_project.team << [current_user, :master]
create(:deploy_keys_project, project: @second_project)
- @third_project = create(:project, namespace: create(:group))
+ @third_project = create(:empty_project, namespace: create(:group))
@third_project.team << [current_user, :master]
create(:deploy_keys_project, project: @third_project, deploy_key: @second_project.deploy_keys.first)
end
diff --git a/features/steps/project/fork.rb b/features/steps/project/fork.rb
index 70dbd030003..9a6c04fba7a 100644
--- a/features/steps/project/fork.rb
+++ b/features/steps/project/fork.rb
@@ -9,7 +9,7 @@ class Spinach::Features::ProjectFork < Spinach::FeatureSteps
end
step 'I am a member of project "Shop"' do
- @project = create(:project, name: "Shop")
+ @project = create(:project, :repository, name: "Shop")
@project.team << [@user, :reporter]
end
@@ -18,7 +18,7 @@ class Spinach::Features::ProjectFork < Spinach::FeatureSteps
end
step 'I already have a project named "Shop" in my namespace' do
- @my_project = create(:project, name: "Shop", namespace: current_user.namespace)
+ @my_project = create(:project, :repository, name: "Shop", namespace: current_user.namespace)
end
step 'I should see a "Name has already been taken" warning' do
diff --git a/features/steps/project/forked_merge_requests.rb b/features/steps/project/forked_merge_requests.rb
index 6c14d835004..c0827ff8fc7 100644
--- a/features/steps/project/forked_merge_requests.rb
+++ b/features/steps/project/forked_merge_requests.rb
@@ -7,7 +7,7 @@ class Spinach::Features::ProjectForkedMergeRequests < Spinach::FeatureSteps
step 'I am a member of project "Shop"' do
@project = Project.find_by(name: "Shop")
- @project ||= create(:project, name: "Shop")
+ @project ||= create(:project, :repository, name: "Shop")
@project.team << [@user, :reporter]
end
diff --git a/features/steps/project/graph.rb b/features/steps/project/graph.rb
index 7490d2bc6e7..48ac7a98f0d 100644
--- a/features/steps/project/graph.rb
+++ b/features/steps/project/graph.rb
@@ -34,9 +34,9 @@ class Spinach::Features::ProjectGraph < Spinach::FeatureSteps
step 'page should have CI graphs' do
expect(page).to have_content 'Overall'
- expect(page).to have_content 'Builds for last week'
- expect(page).to have_content 'Builds for last month'
- expect(page).to have_content 'Builds for last year'
+ expect(page).to have_content 'Jobs for last week'
+ expect(page).to have_content 'Jobs for last month'
+ expect(page).to have_content 'Jobs for last year'
expect(page).to have_content 'Commit duration in minutes for last 30 commits'
end
diff --git a/features/steps/project/hooks.rb b/features/steps/project/hooks.rb
index 13c0713669a..37b608ffbd3 100644
--- a/features/steps/project/hooks.rb
+++ b/features/steps/project/hooks.rb
@@ -36,12 +36,12 @@ class Spinach::Features::ProjectHooks < Spinach::FeatureSteps
end
step 'I should see newly created hook' do
- expect(current_path).to eq namespace_project_hooks_path(current_project.namespace, current_project)
+ expect(current_path).to eq namespace_project_settings_integrations_path(current_project.namespace, current_project)
expect(page).to have_content(@url)
end
step 'I should see newly created hook with SSL verification enabled' do
- expect(current_path).to eq namespace_project_hooks_path(current_project.namespace, current_project)
+ expect(current_path).to eq namespace_project_settings_integrations_path(current_project.namespace, current_project)
expect(page).to have_content(@url)
expect(page).to have_content("SSL Verification: enabled")
end
@@ -57,7 +57,7 @@ class Spinach::Features::ProjectHooks < Spinach::FeatureSteps
end
step 'hook should be triggered' do
- expect(current_path).to eq namespace_project_hooks_path(current_project.namespace, current_project)
+ expect(current_path).to eq namespace_project_settings_integrations_path(current_project.namespace, current_project)
expect(page).to have_selector '.flash-notice',
text: 'Hook executed successfully: HTTP 200'
end
diff --git a/features/steps/project/issues/labels.rb b/features/steps/project/issues/labels.rb
index f74a9b5df47..4a35b71af2f 100644
--- a/features/steps/project/issues/labels.rb
+++ b/features/steps/project/issues/labels.rb
@@ -15,17 +15,16 @@ class Spinach::Features::ProjectIssuesLabels < Spinach::FeatureSteps
step 'I delete all labels' do
page.within '.labels' do
- page.all('.remove-row').each do |remove|
- remove.click
- sleep 0.05
+ page.all('.remove-row').each do
+ first('.remove-row').click
end
end
end
step 'I should see labels help message' do
page.within '.labels' do
- expect(page).to have_content 'Create a label or generate a default set '\
- 'of labels'
+ expect(page).to have_content 'Generate a default set of labels'
+ expect(page).to have_content 'New label'
end
end
diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb
index d2fa8cd39af..9f0057cace7 100644
--- a/features/steps/project/merge_requests.rb
+++ b/features/steps/project/merge_requests.rb
@@ -501,6 +501,9 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
step 'I fill in merge request search with "Fe"' do
fill_in 'issuable_search', with: "Fe"
+ page.within '.merge-requests-holder' do
+ find('.merge-request')
+ end
end
step 'I click the "Target branch" dropdown' do
diff --git a/features/steps/project/merge_requests/acceptance.rb b/features/steps/project/merge_requests/acceptance.rb
index 4fda0731e2f..0a3f4649870 100644
--- a/features/steps/project/merge_requests/acceptance.rb
+++ b/features/steps/project/merge_requests/acceptance.rb
@@ -28,7 +28,7 @@ class Spinach::Features::ProjectMergeRequestsAcceptance < Spinach::FeatureSteps
step 'There is an open Merge Request' do
@user = create(:user)
- @project = create(:project, :public)
+ @project = create(:project, :public, :repository)
@project_member = create(:project_member, :developer, user: @user, project: @project)
@merge_request = create(:merge_request, :with_diffs, :simple, source_project: @project)
end
diff --git a/features/steps/project/merge_requests/revert.rb b/features/steps/project/merge_requests/revert.rb
index efbc4831ce1..31f95b524b3 100644
--- a/features/steps/project/merge_requests/revert.rb
+++ b/features/steps/project/merge_requests/revert.rb
@@ -30,14 +30,13 @@ class Spinach::Features::RevertMergeRequests < Spinach::FeatureSteps
end
step 'I am signed in as a developer of the project' do
+ @user = create(:user) { |u| @project.add_developer(u) }
login_as(@user)
end
step 'There is an open Merge Request' do
- @user = create(:user)
- @project = create(:project, :public)
- @project_member = create(:project_member, :developer, user: @user, project: @project)
- @merge_request = create(:merge_request, :with_diffs, :simple, source_project: @project)
+ @merge_request = create(:merge_request, :with_diffs, :simple)
+ @project = @merge_request.source_project
end
step 'I should see a revert error' do
diff --git a/features/steps/project/pages.rb b/features/steps/project/pages.rb
new file mode 100644
index 00000000000..c80c6273807
--- /dev/null
+++ b/features/steps/project/pages.rb
@@ -0,0 +1,139 @@
+class Spinach::Features::ProjectPages < Spinach::FeatureSteps
+ include SharedAuthentication
+ include SharedPaths
+ include SharedProject
+
+ step 'pages are enabled' do
+ allow(Gitlab.config.pages).to receive(:enabled).and_return(true)
+ allow(Gitlab.config.pages).to receive(:host).and_return('example.com')
+ allow(Gitlab.config.pages).to receive(:port).and_return(80)
+ allow(Gitlab.config.pages).to receive(:https).and_return(false)
+ end
+
+ step 'pages are disabled' do
+ allow(Gitlab.config.pages).to receive(:enabled).and_return(false)
+ end
+
+ step 'I visit the Project Pages' do
+ visit namespace_project_pages_path(@project.namespace, @project)
+ end
+
+ step 'I should see that GitLab Pages are disabled' do
+ expect(page).to have_content('GitLab Pages are disabled')
+ end
+
+ step 'I should see the usage of GitLab Pages' do
+ expect(page).to have_content('Configure pages')
+ end
+
+ step 'pages are deployed' do
+ pipeline = @project.ensure_pipeline('HEAD', @project.commit('HEAD').sha)
+ build = build(:ci_build,
+ project: @project,
+ pipeline: pipeline,
+ ref: 'HEAD',
+ artifacts_file: fixture_file_upload(Rails.root + 'spec/fixtures/pages.zip'),
+ artifacts_metadata: fixture_file_upload(Rails.root + 'spec/fixtures/pages.zip.meta')
+ )
+ result = ::Projects::UpdatePagesService.new(@project, build).execute
+ expect(result[:status]).to eq(:success)
+ end
+
+ step 'I should be able to access the Pages' do
+ expect(page).to have_content('Access pages')
+ end
+
+ step 'I should see that support for domains is disabled' do
+ expect(page).to have_content('Support for domains and certificates is disabled')
+ end
+
+ step 'support for external domains is disabled' do
+ allow(Gitlab.config.pages).to receive(:external_http).and_return(nil)
+ allow(Gitlab.config.pages).to receive(:external_https).and_return(nil)
+ end
+
+ step 'pages are exposed on external HTTP address' do
+ allow(Gitlab.config.pages).to receive(:external_http).and_return('1.1.1.1:80')
+ allow(Gitlab.config.pages).to receive(:external_https).and_return(nil)
+ end
+
+ step 'pages are exposed on external HTTPS address' do
+ allow(Gitlab.config.pages).to receive(:external_http).and_return('1.1.1.1:80')
+ allow(Gitlab.config.pages).to receive(:external_https).and_return('1.1.1.1:443')
+ end
+
+ step 'I should be able to add a New Domain' do
+ expect(page).to have_content('New Domain')
+ end
+
+ step 'I visit add a new Pages Domain' do
+ visit new_namespace_project_pages_domain_path(@project.namespace, @project)
+ end
+
+ step 'I fill the domain' do
+ fill_in 'Domain', with: 'my.test.domain.com'
+ end
+
+ step 'I click on "Create New Domain"' do
+ click_button 'Create New Domain'
+ end
+
+ step 'I should see a new domain added' do
+ expect(page).to have_content('Domains (1)')
+ expect(page).to have_content('my.test.domain.com')
+ end
+
+ step 'pages domain is added' do
+ @project.pages_domains.create!(domain: 'my.test.domain.com')
+ end
+
+ step 'I should see error message that domain already exists' do
+ expect(page).to have_content('Domain has already been taken')
+ end
+
+ step 'I should see that support for certificates is disabled' do
+ expect(page).to have_content('Support for custom certificates is disabled')
+ end
+
+ step 'I fill the certificate and key' do
+ fill_in 'Certificate (PEM)', with: '-----BEGIN CERTIFICATE-----
+MIICGzCCAYSgAwIBAgIBATANBgkqhkiG9w0BAQUFADAbMRkwFwYDVQQDExB0ZXN0
+LWNlcnRpZmljYXRlMB4XDTE2MDIxMjE0MzIwMFoXDTIwMDQxMjE0MzIwMFowGzEZ
+MBcGA1UEAxMQdGVzdC1jZXJ0aWZpY2F0ZTCBnzANBgkqhkiG9w0BAQEFAAOBjQAw
+gYkCgYEApL4J9L0ZxFJ1hI1LPIflAlAGvm6ZEvoT4qKU5Xf2JgU7/2geNR1qlNFa
+SvCc08Knupp5yTgmvyK/Xi09U0N82vvp4Zvr/diSc4A/RA6Mta6egLySNT438kdT
+nY2tR5feoTLwQpX0t4IMlwGQGT5h6Of2fKmDxzuwuyffcIHqLdsCAwEAAaNvMG0w
+DAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUxl9WSxBprB0z0ibJs3rXEk0+95AwCwYD
+VR0PBAQDAgXgMBEGCWCGSAGG+EIBAQQEAwIGQDAeBglghkgBhvhCAQ0EERYPeGNh
+IGNlcnRpZmljYXRlMA0GCSqGSIb3DQEBBQUAA4GBAGC4T8SlFHK0yPSa+idGLQFQ
+joZp2JHYvNlTPkRJ/J4TcXxBTJmArcQgTIuNoBtC+0A/SwdK4MfTCUY4vNWNdese
+5A4K65Nb7Oh1AdQieTBHNXXCdyFsva9/ScfQGEl7p55a52jOPs0StPd7g64uvjlg
+YHi2yesCrOvVXt+lgPTd
+-----END CERTIFICATE-----'
+
+ fill_in 'Key (PEM)', with: '-----BEGIN PRIVATE KEY-----
+MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAKS+CfS9GcRSdYSN
+SzyH5QJQBr5umRL6E+KilOV39iYFO/9oHjUdapTRWkrwnNPCp7qaeck4Jr8iv14t
+PVNDfNr76eGb6/3YknOAP0QOjLWunoC8kjU+N/JHU52NrUeX3qEy8EKV9LeCDJcB
+kBk+Yejn9nypg8c7sLsn33CB6i3bAgMBAAECgYA2D26w80T7WZvazYr86BNMePpd
+j2mIAqx32KZHzt/lhh40J/SRtX9+Kl0Y7nBoRR5Ja9u/HkAIxNxLiUjwg9r6cpg/
+uITEF5nMt7lAk391BuI+7VOZZGbJDsq2ulPd6lO+C8Kq/PI/e4kXcIjeH6KwQsuR
+5vrXfBZ3sQfflaiN4QJBANBt8JY2LIGQF8o89qwUpRL5vbnKQ4IzZ5+TOl4RLR7O
+AQpJ81tGuINghO7aunctb6rrcKJrxmEH1whzComybrMCQQDKV49nOBudRBAIgG4K
+EnLzsRKISUHMZSJiYTYnablof8cKw1JaQduw7zgrUlLwnroSaAGX88+Jw1f5n2Lh
+Vlg5AkBDdUGnrDLtYBCDEQYZHblrkc7ZAeCllDOWjxUV+uMqlCv8A4Ey6omvY57C
+m6I8DkWVAQx8VPtozhvHjUw80rZHAkB55HWHAM3h13axKG0htCt7klhPsZHpx6MH
+EPjGlXIT+aW2XiPmK3ZlCDcWIenE+lmtbOpI159Wpk8BGXs/s/xBAkEAlAY3ymgx
+63BDJEwvOb2IaP8lDDxNsXx9XJNVvQbv5n15vNsLHbjslHfAhAbxnLQ1fLhUPqSi
+nNp/xedE1YxutQ==
+-----END PRIVATE KEY-----'
+ end
+
+ step 'I click Remove Pages' do
+ click_link 'Remove pages'
+ end
+
+ step 'The Pages should get removed' do
+ expect(@project.pages_deployed?).to be_falsey
+ end
+end
diff --git a/features/steps/project/redirects.rb b/features/steps/project/redirects.rb
index 1ffd5cb9de5..92936f27c20 100644
--- a/features/steps/project/redirects.rb
+++ b/features/steps/project/redirects.rb
@@ -4,11 +4,11 @@ class Spinach::Features::ProjectRedirects < Spinach::FeatureSteps
include SharedProject
step 'public project "Community"' do
- create :project, :public, name: 'Community'
+ create(:empty_project, :public, name: 'Community')
end
step 'private project "Enterprise"' do
- create :project, name: 'Enterprise'
+ create(:empty_project, :private, name: 'Enterprise')
end
step 'I visit project "Community" page' do
diff --git a/features/steps/project/services.rb b/features/steps/project/services.rb
index a4d29770922..772b07d0ad8 100644
--- a/features/steps/project/services.rb
+++ b/features/steps/project/services.rb
@@ -4,7 +4,7 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps
include SharedPaths
step 'I visit project "Shop" services page' do
- visit namespace_project_services_path(@project.namespace, @project)
+ visit namespace_project_settings_integrations_path(@project.namespace, @project)
end
step 'I should see list of available services' do
diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb
index 1cc9e37b075..f18adcadcce 100644
--- a/features/steps/project/source/browse_files.rb
+++ b/features/steps/project/source/browse_files.rb
@@ -6,7 +6,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
include RepoHelpers
step "I don't have write access" do
- @project = create(:project, name: "Other Project", path: "other-project")
+ @project = create(:project, :repository, name: "Other Project", path: "other-project")
@project.team << [@user, :reporter]
visit namespace_project_tree_path(@project.namespace, @project, root_ref)
end
diff --git a/features/steps/project/source/markdown_render.rb b/features/steps/project/source/markdown_render.rb
index dee6a8a5558..9183de76881 100644
--- a/features/steps/project/source/markdown_render.rb
+++ b/features/steps/project/source/markdown_render.rb
@@ -8,7 +8,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps
step 'I own project "Delta"' do
@project = Project.find_by(name: "Delta")
- @project ||= create(:project, name: "Delta", namespace: @user.namespace)
+ @project ||= create(:project, :repository, name: "Delta", namespace: @user.namespace)
@project.team << [@user, :master]
end
diff --git a/features/steps/project/team_management.rb b/features/steps/project/team_management.rb
index 22d971fadfb..6986c7ede56 100644
--- a/features/steps/project/team_management.rb
+++ b/features/steps/project/team_management.rb
@@ -113,8 +113,10 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps
project.team << [user, :reporter]
end
- step 'I click link "Import team from another project"' do
- click_link "Import"
+ step 'I click link "Import team from another project"' do
+ page.within '.users-project-form' do
+ click_link "Import"
+ end
end
When 'I submit "Website" project for import team' do
@@ -135,7 +137,7 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps
step 'I share project with group "OpenSource"' do
project = Project.find_by(name: 'Shop')
os_group = create(:group, name: 'OpenSource')
- create(:project, group: os_group)
+ create(:empty_project, group: os_group)
@os_user1 = create(:user)
@os_user2 = create(:user)
os_group.add_owner(@os_user1)
diff --git a/features/steps/shared/admin.rb b/features/steps/shared/admin.rb
index fbaa408226e..ac0a1764147 100644
--- a/features/steps/shared/admin.rb
+++ b/features/steps/shared/admin.rb
@@ -2,7 +2,7 @@ module SharedAdmin
include Spinach::DSL
step 'there are projects in system' do
- 2.times { create(:project) }
+ 2.times { create(:project, :repository) }
end
step 'system has users' do
diff --git a/features/steps/shared/builds.rb b/features/steps/shared/builds.rb
index 70e6d4836b2..d008a8a26af 100644
--- a/features/steps/shared/builds.rb
+++ b/features/steps/shared/builds.rb
@@ -47,7 +47,7 @@ module SharedBuilds
end
step 'recent build has a build trace' do
- @build.trace = 'build trace'
+ @build.trace = 'job trace'
end
step 'download of build artifacts archive starts' do
@@ -60,7 +60,7 @@ module SharedBuilds
end
step 'I see details of a build' do
- expect(page).to have_content "Build ##{@build.id}"
+ expect(page).to have_content "Job ##{@build.id}"
end
step 'I see build trace' do
diff --git a/features/steps/shared/group.rb b/features/steps/shared/group.rb
index fe6736dacd4..de119f2c6c0 100644
--- a/features/steps/shared/group.rb
+++ b/features/steps/shared/group.rb
@@ -40,7 +40,7 @@ module SharedGroup
user = User.find_by(name: username) || create(:user, name: username)
group = Group.find_by(name: groupname) || create(:group, name: groupname)
group.add_user(user, role)
- project ||= create(:project, namespace: group, path: "project#{@project_count}")
+ project ||= create(:project, :repository, namespace: group, path: "project#{@project_count}")
create(:closed_issue_event, project: project)
project.team << [user, :master]
@project_count += 1
diff --git a/features/steps/shared/paths.rb b/features/steps/shared/paths.rb
index 15b81fa529b..718cf924729 100644
--- a/features/steps/shared/paths.rb
+++ b/features/steps/shared/paths.rb
@@ -191,10 +191,6 @@ module SharedPaths
visit admin_background_jobs_path
end
- step 'I visit admin groups page' do
- visit admin_groups_path
- end
-
step 'I visit admin teams page' do
visit admin_teams_path
end
@@ -260,7 +256,7 @@ module SharedPaths
end
step 'I visit project hooks page' do
- visit namespace_project_hooks_path(@project.namespace, @project)
+ visit namespace_project_settings_integrations_path(@project.namespace, @project)
end
step 'I visit project deploy keys page' do
diff --git a/features/steps/shared/project.rb b/features/steps/shared/project.rb
index b51152c79c6..dae248b8b7e 100644
--- a/features/steps/shared/project.rb
+++ b/features/steps/shared/project.rb
@@ -3,19 +3,25 @@ module SharedProject
# Create a project without caring about what it's called
step "I own a project" do
- @project = create(:project, namespace: @user.namespace)
+ @project = create(:project, :repository, namespace: @user.namespace)
+ @project.team << [@user, :master]
+ end
+
+ step "I own a project in some group namespace" do
+ @group = create(:group, name: 'some group')
+ @project = create(:project, namespace: @group)
@project.team << [@user, :master]
end
step "project exists in some group namespace" do
@group = create(:group, name: 'some group')
- @project = create(:project, namespace: @group, public_builds: false)
+ @project = create(:project, :repository, namespace: @group, public_builds: false)
end
# Create a specific project called "Shop"
step 'I own project "Shop"' do
@project = Project.find_by(name: "Shop")
- @project ||= create(:project, name: "Shop", namespace: @user.namespace)
+ @project ||= create(:project, :repository, name: "Shop", namespace: @user.namespace)
@project.team << [@user, :master]
end
@@ -40,7 +46,7 @@ module SharedProject
# Create another specific project called "Forum"
step 'I own project "Forum"' do
@project = Project.find_by(name: "Forum")
- @project ||= create(:project, name: "Forum", namespace: @user.namespace, path: 'forum_project')
+ @project ||= create(:project, :repository, name: "Forum", namespace: @user.namespace, path: 'forum_project')
@project.build_project_feature
@project.project_feature.save
@project.team << [@user, :master]
@@ -97,7 +103,7 @@ module SharedProject
step 'I should see project settings' do
expect(current_path).to eq edit_namespace_project_path(@project.namespace, @project)
expect(page).to have_content("Project name")
- expect(page).to have_content("Feature Visibility")
+ expect(page).to have_content("Sharing & Permissions")
end
def current_project
@@ -121,7 +127,7 @@ module SharedProject
# ----------------------------------------
step 'archived project "Archive"' do
- create :project, :public, archived: true, name: 'Archive'
+ create(:project, :archived, :public, :repository, name: 'Archive')
end
step 'I should not see project "Archive"' do
@@ -144,7 +150,7 @@ module SharedProject
# ----------------------------------------
step 'private project "Enterprise"' do
- create :project, name: 'Enterprise'
+ create(:project, :private, :repository, name: 'Enterprise')
end
step 'I should see project "Enterprise"' do
@@ -156,7 +162,7 @@ module SharedProject
end
step 'internal project "Internal"' do
- create :project, :internal, name: 'Internal'
+ create(:project, :internal, :repository, name: 'Internal')
end
step 'I should see project "Internal"' do
@@ -168,7 +174,7 @@ module SharedProject
end
step 'public project "Community"' do
- create :project, :public, name: 'Community'
+ create(:project, :public, :repository, name: 'Community')
end
step 'I should see project "Community"' do
diff --git a/features/steps/user.rb b/features/steps/user.rb
index 59385a6ab59..271c9b097d4 100644
--- a/features/steps/user.rb
+++ b/features/steps/user.rb
@@ -38,6 +38,6 @@ class Spinach::Features::User < Spinach::FeatureSteps
end
def contributed_project
- @contributed_project ||= create(:project, :public)
+ @contributed_project ||= create(:empty_project, :public)
end
end
diff --git a/features/support/env.rb b/features/support/env.rb
index 8dbe3624410..f394c30d52f 100644
--- a/features/support/env.rb
+++ b/features/support/env.rb
@@ -4,7 +4,6 @@ SimpleCovEnv.start!
ENV['RAILS_ENV'] = 'test'
require './config/environment'
require 'rspec/expectations'
-require 'sidekiq/testing/inline'
require_relative 'capybara'
require_relative 'db_cleaner'
@@ -15,7 +14,7 @@ if ENV['CI']
Knapsack::Adapters::SpinachAdapter.bind
end
-%w(select2_helper test_env repo_helpers wait_for_ajax).each do |f|
+%w(select2_helper test_env repo_helpers wait_for_ajax sidekiq).each do |f|
require Rails.root.join('spec', 'support', f)
end
diff --git a/lib/api/api.rb b/lib/api/api.rb
index 9d5adffd8f4..eb9792680ff 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -1,7 +1,16 @@
module API
class API < Grape::API
include APIGuard
- version 'v3', using: :path
+
+ version %w(v3 v4), using: :path
+
+ version 'v3', using: :path do
+ mount ::API::V3::DeployKeys
+ mount ::API::V3::Issues
+ mount ::API::V3::MergeRequests
+ mount ::API::V3::Projects
+ mount ::API::V3::ProjectSnippets
+ end
before { allow_access_with_scope :api }
@@ -14,7 +23,11 @@ module API
end
# Retain 405 error rather than a 500 error for Grape 0.15.0+.
- # See: https://github.com/ruby-grape/grape/commit/252bfd27c320466ec3c0751812cf44245e97e5de
+ # https://github.com/ruby-grape/grape/blob/a3a28f5b5dfbb2797442e006dbffd750b27f2a76/UPGRADING.md#changes-to-method-not-allowed-routes
+ rescue_from Grape::Exceptions::MethodNotAllowed do |e|
+ error! e.message, e.status, e.headers
+ end
+
rescue_from Grape::Exceptions::Base do |e|
error! e.message, e.status, e.headers
end
diff --git a/lib/api/boards.rb b/lib/api/boards.rb
index 4ac491edc1b..13752eb4947 100644
--- a/lib/api/boards.rb
+++ b/lib/api/boards.rb
@@ -37,7 +37,7 @@ module API
end
desc 'Get the lists of a project board' do
- detail 'Does not include `backlog` and `done` lists. This feature was introduced in 8.13'
+ detail 'Does not include `done` list. This feature was introduced in 8.13'
success Entities::List
end
get '/lists' do
diff --git a/lib/api/branches.rb b/lib/api/branches.rb
index 0950c3d2e88..be659fa4a6a 100644
--- a/lib/api/branches.rb
+++ b/lib/api/branches.rb
@@ -129,12 +129,7 @@ module API
end
end
- # Delete all merged branches
- #
- # Parameters:
- # id (required) - The ID of a project
- # Example Request:
- # DELETE /projects/:id/repository/branches/delete_merged
+ desc 'Delete all merged branches'
delete ":id/repository/merged_branches" do
DeleteMergedBranchesService.new(user_project, current_user).async_execute
diff --git a/lib/api/builds.rb b/lib/api/builds.rb
index af61be343be..44fe0fc4a95 100644
--- a/lib/api/builds.rb
+++ b/lib/api/builds.rb
@@ -209,7 +209,7 @@ module API
build = get_build!(params[:build_id])
- bad_request!("Unplayable Build") unless build.playable?
+ bad_request!("Unplayable Job") unless build.playable?
build.play(current_user)
diff --git a/lib/api/commit_statuses.rb b/lib/api/commit_statuses.rb
index 4bbdf06a49c..b6e6820c3f4 100644
--- a/lib/api/commit_statuses.rb
+++ b/lib/api/commit_statuses.rb
@@ -78,6 +78,8 @@ module API
description: params[:description]
)
+ render_validation_error!(status) if status.invalid?
+
begin
case params[:state]
when 'pending'
diff --git a/lib/api/commits.rb b/lib/api/commits.rb
index cf2489dbb67..2fefe760d24 100644
--- a/lib/api/commits.rb
+++ b/lib/api/commits.rb
@@ -44,7 +44,6 @@ module API
detail 'This feature was introduced in GitLab 8.13'
end
params do
- requires :id, type: Integer, desc: 'The project ID'
requires :branch_name, type: String, desc: 'The name of branch'
requires :commit_message, type: String, desc: 'Commit message'
requires :actions, type: Array[Hash], desc: 'Actions to perform in commit'
@@ -55,7 +54,7 @@ module API
authorize! :push_code, user_project
attrs = declared_params
- attrs[:source_branch] = attrs[:branch_name]
+ attrs[:start_branch] = attrs[:branch_name]
attrs[:target_branch] = attrs[:branch_name]
attrs[:actions].map! do |action|
action[:action] = action[:action].to_sym
@@ -140,8 +139,6 @@ module API
commit_params = {
commit: commit,
create_merge_request: false,
- source_project: user_project,
- source_branch: commit.cherry_pick_branch_name,
target_branch: params[:branch]
}
diff --git a/lib/api/deploy_keys.rb b/lib/api/deploy_keys.rb
index 85360730841..3f5183d46a2 100644
--- a/lib/api/deploy_keys.rb
+++ b/lib/api/deploy_keys.rb
@@ -1,5 +1,4 @@
module API
- # Projects API
class DeployKeys < Grape::API
before { authenticate! }
@@ -16,104 +15,102 @@ module API
resource :projects do
before { authorize_admin_project }
- # Routing "projects/:id/keys/..." is DEPRECATED and WILL BE REMOVED in version 9.0
- # Use "projects/:id/deploy_keys/..." instead.
- #
- %w(keys deploy_keys).each do |path|
- desc "Get a specific project's deploy keys" do
- success Entities::SSHKey
- end
- get ":id/#{path}" do
- present user_project.deploy_keys, with: Entities::SSHKey
- end
+ desc "Get a specific project's deploy keys" do
+ success Entities::SSHKey
+ end
+ get ":id/deploy_keys" do
+ present user_project.deploy_keys, with: Entities::SSHKey
+ end
- desc 'Get single deploy key' do
- success Entities::SSHKey
- end
- params do
- requires :key_id, type: Integer, desc: 'The ID of the deploy key'
- end
- get ":id/#{path}/:key_id" do
- key = user_project.deploy_keys.find params[:key_id]
+ desc 'Get single deploy key' do
+ success Entities::SSHKey
+ end
+ params do
+ requires :key_id, type: Integer, desc: 'The ID of the deploy key'
+ end
+ get ":id/deploy_keys/:key_id" do
+ key = user_project.deploy_keys.find params[:key_id]
+ present key, with: Entities::SSHKey
+ end
+
+ desc 'Add new deploy key to currently authenticated user' do
+ success Entities::SSHKey
+ end
+ params do
+ requires :key, type: String, desc: 'The new deploy key'
+ requires :title, type: String, desc: 'The name of the deploy key'
+ end
+ post ":id/deploy_keys" do
+ params[:key].strip!
+
+ # Check for an existing key joined to this project
+ key = user_project.deploy_keys.find_by(key: params[:key])
+ if key
present key, with: Entities::SSHKey
+ break
end
- # 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
+ # Check for available deploy keys in other projects
+ key = current_user.accessible_deploy_keys.find_by(key: params[:key])
+ if key
+ user_project.deploy_keys << key
+ present key, with: Entities::SSHKey
+ break
end
- post ":id/#{path}" do
- attrs = attributes_for_keys [:title, :key]
- attrs[:key].strip! if attrs[:key]
- # Check for an existing key joined to this project
- key = user_project.deploy_keys.find_by(key: attrs[:key])
- if key
- present key, with: Entities::SSHKey
- break
- end
-
- # Check for available deploy keys in other projects
- key = current_user.accessible_deploy_keys.find_by(key: attrs[:key])
- if key
- user_project.deploy_keys << key
- present key, with: Entities::SSHKey
- break
- end
-
- # Create a new deploy key
- key = DeployKey.new attrs
- if key.valid? && user_project.deploy_keys << key
- present key, with: Entities::SSHKey
- else
- render_validation_error!(key)
- end
+ # Create a new deploy key
+ key = DeployKey.new(declared_params(include_missing: false))
+ if key.valid? && user_project.deploy_keys << key
+ present key, with: Entities::SSHKey
+ else
+ render_validation_error!(key)
end
+ end
- desc 'Enable a deploy key for a project' do
- detail 'This feature was added in GitLab 8.11'
- success Entities::SSHKey
- end
- params do
- requires :key_id, type: Integer, desc: 'The ID of the deploy key'
- end
- post ":id/#{path}/:key_id/enable" do
- key = ::Projects::EnableDeployKeyService.new(user_project,
- current_user, declared_params).execute
+ desc 'Enable a deploy key for a project' do
+ detail 'This feature was added in GitLab 8.11'
+ success Entities::SSHKey
+ end
+ params do
+ requires :key_id, type: Integer, desc: 'The ID of the deploy key'
+ end
+ post ":id/deploy_keys/:key_id/enable" do
+ key = ::Projects::EnableDeployKeyService.new(user_project,
+ current_user, declared_params).execute
- if key
- present key, with: Entities::SSHKey
- else
- not_found!('Deploy Key')
- end
+ if key
+ present key, with: Entities::SSHKey
+ else
+ not_found!('Deploy Key')
end
+ end
- desc 'Disable a deploy key for a project' do
- detail 'This feature was added in GitLab 8.11'
- success Entities::SSHKey
- end
- params do
- requires :key_id, type: Integer, desc: 'The ID of the deploy key'
- end
- delete ":id/#{path}/:key_id/disable" do
- key = user_project.deploy_keys_projects.find_by(deploy_key_id: params[:key_id])
- key.destroy
+ desc 'Disable a deploy key for a project' do
+ detail 'This feature was added in GitLab 8.11'
+ success Entities::SSHKey
+ end
+ params do
+ requires :key_id, type: Integer, desc: 'The ID of the deploy key'
+ end
+ delete ":id/deploy_keys/:key_id/disable" do
+ key = user_project.deploy_keys_projects.find_by(deploy_key_id: params[:key_id])
+ key.destroy
- present key.deploy_key, with: Entities::SSHKey
- end
+ present key.deploy_key, with: Entities::SSHKey
+ end
- desc 'Delete 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])
+ desc 'Delete deploy key for a project' do
+ success Key
+ end
+ params do
+ requires :key_id, type: Integer, desc: 'The ID of the deploy key'
+ end
+ delete ":id/deploy_keys/:key_id" do
+ key = user_project.deploy_keys_projects.find_by(deploy_key_id: params[:key_id])
+ if key
key.destroy
+ else
+ not_found!('Deploy Key')
end
end
end
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index dfbb3ab86dd..5d7b8e021bb 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -78,21 +78,21 @@ module API
expose :container_registry_enabled
# Expose old field names with the new permissions methods to keep API compatible
- expose(:issues_enabled) { |project, options| project.feature_available?(:issues, options[:user]) }
- expose(:merge_requests_enabled) { |project, options| project.feature_available?(:merge_requests, options[:user]) }
- expose(:wiki_enabled) { |project, options| project.feature_available?(:wiki, options[:user]) }
- expose(:builds_enabled) { |project, options| project.feature_available?(:builds, options[:user]) }
- expose(:snippets_enabled) { |project, options| project.feature_available?(:snippets, options[:user]) }
+ expose(:issues_enabled) { |project, options| project.feature_available?(:issues, options[:current_user]) }
+ expose(:merge_requests_enabled) { |project, options| project.feature_available?(:merge_requests, options[:current_user]) }
+ expose(:wiki_enabled) { |project, options| project.feature_available?(:wiki, options[:current_user]) }
+ expose(:builds_enabled) { |project, options| project.feature_available?(:builds, options[:current_user]) }
+ expose(:snippets_enabled) { |project, options| project.feature_available?(:snippets, options[:current_user]) }
expose :created_at, :last_activity_at
expose :shared_runners_enabled
expose :lfs_enabled?, as: :lfs_enabled
expose :creator_id
- expose :namespace
+ expose :namespace, using: 'API::Entities::Namespace'
expose :forked_from_project, using: Entities::BasicProjectDetails, if: lambda{ |project, options| project.forked? }
expose :avatar_url
expose :star_count, :forks_count
- expose :open_issues_count, if: lambda { |project, options| project.feature_available?(:issues, options[:user]) && project.default_issues_tracker? }
+ expose :open_issues_count, if: lambda { |project, options| project.feature_available?(:issues, options[:current_user]) && project.default_issues_tracker? }
expose :runners_token, if: lambda { |_project, options| options[:user_can_admin_project] }
expose :public_builds
expose :shared_with_groups do |project, options|
@@ -101,6 +101,16 @@ module API
expose :only_allow_merge_if_build_succeeds
expose :request_access_enabled
expose :only_allow_merge_if_all_discussions_are_resolved
+
+ expose :statistics, using: 'API::Entities::ProjectStatistics', if: :statistics
+ end
+
+ class ProjectStatistics < Grape::Entity
+ expose :commit_count
+ expose :storage_size
+ expose :repository_size
+ expose :lfs_objects_size
+ expose :build_artifacts_size
end
class Member < UserBasic
@@ -127,6 +137,16 @@ module API
expose :avatar_url
expose :web_url
expose :request_access_enabled
+ expose :full_name, :full_path
+
+ expose :statistics, if: :statistics do
+ with_options format_with: -> (value) { value.to_i } do
+ expose :storage_size
+ expose :repository_size
+ expose :lfs_objects_size
+ expose :build_artifacts_size
+ end
+ end
end
class GroupDetail < Group
@@ -193,9 +213,6 @@ module API
expose :author, using: Entities::UserBasic
expose :updated_at, :created_at
- # TODO (rspeicher): Deprecated; remove in 9.0
- expose(:expires_at) { |snippet| nil }
-
expose :web_url do |snippet, options|
Gitlab::UrlBuilder.build(snippet)
end
@@ -249,6 +266,13 @@ module API
end
end
+ class IssuableTimeStats < Grape::Entity
+ expose :time_estimate
+ expose :total_time_spent
+ expose :human_time_estimate
+ expose :human_total_time_spent
+ end
+
class ExternalIssue < Grape::Entity
expose :title
expose :id
@@ -298,7 +322,7 @@ module API
end
class SSHKey < Grape::Entity
- expose :id, :title, :key, :created_at
+ expose :id, :title, :key, :created_at, :can_push
end
class SSHKeyWithUser < SSHKey
@@ -391,7 +415,7 @@ module API
end
class Namespace < Grape::Entity
- expose :id, :path, :kind
+ expose :id, :name, :path, :kind
end
class MemberAccess < Grape::Entity
@@ -440,12 +464,12 @@ module API
class ProjectWithAccess < Project
expose :permissions do
expose :project_access, using: Entities::ProjectAccess do |project, options|
- project.project_members.find_by(user_id: options[:user].id)
+ project.project_members.find_by(user_id: options[:current_user].id)
end
expose :group_access, using: Entities::GroupAccess do |project, options|
if project.group
- project.group.group_members.find_by(user_id: options[:user].id)
+ project.group.group_members.find_by(user_id: options[:current_user].id)
end
end
end
@@ -546,6 +570,9 @@ module API
expose :repository_storages
expose :koding_enabled
expose :koding_url
+ expose :plantuml_enabled
+ expose :plantuml_url
+ expose :terminal_max_session_time
end
class Release < Grape::Entity
diff --git a/lib/api/files.rb b/lib/api/files.rb
index 532a317c89e..c58472de578 100644
--- a/lib/api/files.rb
+++ b/lib/api/files.rb
@@ -5,7 +5,7 @@ module API
def commit_params(attrs)
{
file_path: attrs[:file_path],
- source_branch: attrs[:branch_name],
+ start_branch: attrs[:branch_name],
target_branch: attrs[:branch_name],
commit_message: attrs[:commit_message],
file_content: attrs[:content],
@@ -68,7 +68,7 @@ module API
ref: params[:ref],
blob_id: blob.id,
commit_id: commit.id,
- last_commit_id: repo.last_commit_for_path(commit.sha, params[:file_path]).id
+ last_commit_id: repo.last_commit_id_for_path(commit.sha, params[:file_path])
}
end
diff --git a/lib/api/groups.rb b/lib/api/groups.rb
index 9b9d3df7435..7682d286866 100644
--- a/lib/api/groups.rb
+++ b/lib/api/groups.rb
@@ -11,6 +11,20 @@ module API
optional :lfs_enabled, type: Boolean, desc: 'Enable/disable LFS for the projects in this group'
optional :request_access_enabled, type: Boolean, desc: 'Allow users to request member access'
end
+
+ params :statistics_params do
+ optional :statistics, type: Boolean, default: false, desc: 'Include project statistics'
+ end
+
+ def present_groups(groups, options = {})
+ options = options.reverse_merge(
+ with: Entities::Group,
+ current_user: current_user,
+ )
+
+ groups = groups.with_statistics if options[:statistics]
+ present paginate(groups), options
+ end
end
resource :groups do
@@ -18,6 +32,7 @@ module API
success Entities::Group
end
params do
+ use :statistics_params
optional :skip_groups, type: Array[Integer], desc: 'Array of group ids to exclude from list'
optional :all_available, type: Boolean, desc: 'Show all group that you have access to'
optional :search, type: String, desc: 'Search for a specific group'
@@ -38,7 +53,7 @@ module API
groups = groups.where.not(id: params[:skip_groups]) if params[:skip_groups].present?
groups = groups.reorder(params[:order_by] => params[:sort])
- present paginate(groups), with: Entities::Group
+ present_groups groups, statistics: params[:statistics] && current_user.is_admin?
end
desc 'Get list of owned groups for authenticated user' do
@@ -46,10 +61,10 @@ module API
end
params do
use :pagination
+ use :statistics_params
end
get '/owned' do
- groups = current_user.owned_groups
- present paginate(groups), with: Entities::Group, user: current_user
+ present_groups current_user.owned_groups, statistics: params[:statistics]
end
desc 'Create a group. Available only for users who can create groups.' do
@@ -66,7 +81,7 @@ module API
group = ::Groups::CreateService.new(current_user, declared_params(include_missing: false)).execute
if group.persisted?
- present group, with: Entities::Group
+ present group, with: Entities::Group, current_user: current_user
else
render_api_error!("Failed to save group #{group.errors.messages}", 400)
end
@@ -92,7 +107,7 @@ module API
authorize! :admin_group, group
if ::Groups::UpdateService.new(group, current_user, declared_params(include_missing: false)).execute
- present group, with: Entities::GroupDetail
+ present group, with: Entities::GroupDetail, current_user: current_user
else
render_validation_error!(group)
end
@@ -103,7 +118,7 @@ module API
end
get ":id" do
group = find_group!(params[:id])
- present group, with: Entities::GroupDetail
+ present group, with: Entities::GroupDetail, current_user: current_user
end
desc 'Remove a group.'
@@ -134,23 +149,23 @@ module API
projects = GroupProjectsFinder.new(group).execute(current_user)
projects = filter_projects(projects)
entity = params[:simple] ? Entities::BasicProjectDetails : Entities::Project
- present paginate(projects), with: entity, user: current_user
+ present paginate(projects), with: entity, current_user: current_user
end
desc 'Transfer a project to the group namespace. Available only for admin.' do
success Entities::GroupDetail
end
params do
- requires :project_id, type: String, desc: 'The ID of the project'
+ requires :project_id, type: String, desc: 'The ID or path of the project'
end
post ":id/projects/:project_id" do
authenticated_as_admin!
- group = Group.find_by(id: params[:id])
- project = Project.find(params[:project_id])
+ group = find_group!(params[:id])
+ project = find_project!(params[:project_id])
result = ::Projects::TransferService.new(project, current_user).execute(group)
if result
- present group, with: Entities::GroupDetail
+ present group, with: Entities::GroupDetail, current_user: current_user
else
render_api_error!("Failed to transfer project #{project.errors.messages}", 400)
end
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index 4be659fc20b..dfab60f7fa5 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -1,6 +1,7 @@
module API
module Helpers
include Gitlab::Utils
+ include Helpers::Pagination
SUDO_HEADER = "HTTP_SUDO"
SUDO_PARAM = :sudo
@@ -44,7 +45,7 @@ module API
if id =~ /^\d+$/
Project.find_by(id: id)
else
- Project.find_with_namespace(id)
+ Project.find_by_full_path(id)
end
end
@@ -85,10 +86,14 @@ module API
IssuesFinder.new(current_user, project_id: user_project.id).find(id)
end
- def paginate(relation)
- relation.page(params[:page]).per(params[:per_page].to_i).tap do |data|
- add_pagination_headers(data)
- end
+ def find_project_merge_request(id)
+ MergeRequestsFinder.new(current_user, project_id: user_project.id).find(id)
+ end
+
+ def find_merge_request_with_access(id, access_level = :read_merge_request)
+ merge_request = user_project.merge_requests.find(id)
+ authorize! access_level, merge_request
+ merge_request
end
def authenticate!
@@ -96,7 +101,7 @@ module API
end
def authenticate_non_get!
- authenticate! unless %w[GET HEAD].include?(route.route_method)
+ authenticate! unless %w[GET HEAD].include?(route.request_method)
end
def authenticate_by_gitlab_shell_token!
@@ -227,7 +232,7 @@ module API
end
def render_api_error!(message, status)
- error!({ 'message' => message }, status)
+ error!({ 'message' => message }, status, header)
end
def handle_api_exception(exception)
@@ -248,7 +253,7 @@ module API
rack_response({ 'message' => '500 Internal Server Error' }.to_json, 500)
end
- # Projects helpers
+ # project helpers
def filter_projects(projects)
if params[:search].present?
@@ -299,7 +304,7 @@ module API
header['X-Sendfile'] = path
body
else
- file FileStreamer.new(path)
+ file path
end
end
@@ -361,38 +366,6 @@ module API
@sudo_identifier ||= params[SUDO_PARAM] || env[SUDO_HEADER]
end
- def add_pagination_headers(paginated_data)
- header 'X-Total', paginated_data.total_count.to_s
- header 'X-Total-Pages', paginated_data.total_pages.to_s
- header 'X-Per-Page', paginated_data.limit_value.to_s
- header 'X-Page', paginated_data.current_page.to_s
- header 'X-Next-Page', paginated_data.next_page.to_s
- header 'X-Prev-Page', paginated_data.prev_page.to_s
- header 'Link', pagination_links(paginated_data)
- end
-
- def pagination_links(paginated_data)
- request_url = request.url.split('?').first
- request_params = params.clone
- request_params[:per_page] = paginated_data.limit_value
-
- links = []
-
- request_params[:page] = paginated_data.current_page - 1
- links << %(<#{request_url}?#{request_params.to_query}>; rel="prev") unless paginated_data.first_page?
-
- request_params[:page] = paginated_data.current_page + 1
- links << %(<#{request_url}?#{request_params.to_query}>; rel="next") unless paginated_data.last_page?
-
- request_params[:page] = 1
- links << %(<#{request_url}?#{request_params.to_query}>; rel="first")
-
- request_params[:page] = paginated_data.total_pages
- links << %(<#{request_url}?#{request_params.to_query}>; rel="last")
-
- links.join(', ')
- end
-
def secret_token
Gitlab::Shell.secret_token
end
diff --git a/lib/api/helpers/internal_helpers.rb b/lib/api/helpers/internal_helpers.rb
index e8975eb57e0..080a6274957 100644
--- a/lib/api/helpers/internal_helpers.rb
+++ b/lib/api/helpers/internal_helpers.rb
@@ -30,7 +30,7 @@ module API
def wiki?
@wiki ||= project_path.end_with?('.wiki') &&
- !Project.find_with_namespace(project_path)
+ !Project.find_by_full_path(project_path)
end
def project
@@ -41,7 +41,7 @@ module API
# the wiki repository as well.
project_path.chomp!('.wiki') if wiki?
- Project.find_with_namespace(project_path)
+ Project.find_by_full_path(project_path)
end
end
diff --git a/lib/api/helpers/pagination.rb b/lib/api/helpers/pagination.rb
new file mode 100644
index 00000000000..2199eea7e5f
--- /dev/null
+++ b/lib/api/helpers/pagination.rb
@@ -0,0 +1,45 @@
+module API
+ module Helpers
+ module Pagination
+ def paginate(relation)
+ relation.page(params[:page]).per(params[:per_page].to_i).tap do |data|
+ add_pagination_headers(data)
+ end
+ end
+
+ private
+
+ def add_pagination_headers(paginated_data)
+ header 'X-Total', paginated_data.total_count.to_s
+ header 'X-Total-Pages', paginated_data.total_pages.to_s
+ header 'X-Per-Page', paginated_data.limit_value.to_s
+ header 'X-Page', paginated_data.current_page.to_s
+ header 'X-Next-Page', paginated_data.next_page.to_s
+ header 'X-Prev-Page', paginated_data.prev_page.to_s
+ header 'Link', pagination_links(paginated_data)
+ end
+
+ def pagination_links(paginated_data)
+ request_url = request.url.split('?').first
+ request_params = params.clone
+ request_params[:per_page] = paginated_data.limit_value
+
+ links = []
+
+ request_params[:page] = paginated_data.current_page - 1
+ links << %(<#{request_url}?#{request_params.to_query}>; rel="prev") unless paginated_data.first_page?
+
+ request_params[:page] = paginated_data.current_page + 1
+ links << %(<#{request_url}?#{request_params.to_query}>; rel="next") unless paginated_data.last_page?
+
+ request_params[:page] = 1
+ links << %(<#{request_url}?#{request_params.to_query}>; rel="first")
+
+ request_params[:page] = paginated_data.total_pages
+ links << %(<#{request_url}?#{request_params.to_query}>; rel="last")
+
+ links.join(', ')
+ end
+ end
+ end
+end
diff --git a/lib/api/internal.rb b/lib/api/internal.rb
index db2d18f935d..d235977fbd8 100644
--- a/lib/api/internal.rb
+++ b/lib/api/internal.rb
@@ -28,6 +28,8 @@ module API
protocol = params[:protocol]
+ actor.update_last_used_at if actor.is_a?(Key)
+
access =
if wiki?
Gitlab::GitAccessWiki.new(actor, project, protocol, authentication_abilities: ssh_authentication_abilities)
@@ -61,6 +63,8 @@ module API
status 200
key = Key.find(params[:key_id])
+ key.update_last_used_at
+
token_handler = Gitlab::LfsToken.new(key)
{
@@ -103,7 +107,9 @@ module API
key = Key.find_by(id: params[:key_id])
- unless key
+ if key
+ key.update_last_used_at
+ else
return { 'success' => false, 'message' => 'Could not find the given key' }
end
diff --git a/lib/api/issues.rb b/lib/api/issues.rb
index c9124649cbb..90fca20d4fa 100644
--- a/lib/api/issues.rb
+++ b/lib/api/issues.rb
@@ -5,28 +5,34 @@ module API
before { authenticate! }
helpers do
- def filter_issues_state(issues, state)
- case state
- when 'opened' then issues.opened
- when 'closed' then issues.closed
- else issues
- end
- end
+ def find_issues(args = {})
+ args = params.merge(args)
- def filter_issues_labels(issues, labels)
- issues.includes(:labels).where('labels.title' => labels.split(','))
- end
+ args.delete(:id)
+ args[:milestone_title] = args.delete(:milestone)
+
+ match_all_labels = args.delete(:match_all_labels)
+ labels = args.delete(:labels)
+ args[:label_name] = labels if match_all_labels
+
+ issues = IssuesFinder.new(current_user, args).execute.inc_notes_with_associations
- def filter_issues_milestone(issues, milestone)
- issues.includes(:milestone).where('milestones.title' => milestone)
+ # TODO: Remove in 9.0 pass `label_name: args.delete(:labels)` to IssuesFinder
+ if !match_all_labels && labels.present?
+ issues = issues.includes(:labels).where('labels.title' => labels.split(','))
+ end
+
+ issues.reorder(args[:order_by] => args[:sort])
end
params :issues_params do
optional :labels, type: String, desc: 'Comma-separated list of label names'
+ optional :milestone, type: String, desc: 'Milestone title'
optional :order_by, type: String, values: %w[created_at updated_at], default: 'created_at',
desc: 'Return issues ordered by `created_at` or `updated_at` fields.'
optional :sort, type: String, values: %w[asc desc], default: 'desc',
desc: 'Return issues sorted in `asc` or `desc` order.'
+ optional :milestone, type: String, desc: 'Return issues for a specific milestone'
use :pagination
end
@@ -37,8 +43,6 @@ module API
optional :labels, type: String, desc: 'Comma-separated list of label names'
optional :due_date, type: String, desc: 'Date time string in the format YEAR-MONTH-DAY'
optional :confidential, type: Boolean, desc: 'Boolean parameter if the issue should be confidential'
- optional :state_event, type: String, values: %w[open close],
- desc: 'State of the issue'
end
end
@@ -52,10 +56,7 @@ module API
use :issues_params
end
get do
- issues = current_user.issues.inc_notes_with_associations
- issues = filter_issues_state(issues, params[:state])
- issues = filter_issues_labels(issues, params[:labels]) unless params[:labels].nil?
- issues = issues.reorder(params[:order_by] => params[:sort])
+ issues = find_issues(scope: 'authored')
present paginate(issues), with: Entities::Issue, current_user: current_user
end
@@ -74,15 +75,10 @@ module API
use :issues_params
end
get ":id/issues" do
- group = find_group!(params.delete(:id))
-
- params[:group_id] = group.id
- params[:milestone_title] = params.delete(:milestone)
- params[:label_name] = params.delete(:labels)
+ group = find_group!(params[:id])
- issues = IssuesFinder.new(current_user, params).execute
+ issues = find_issues(group_id: group.id, state: params[:state] || 'opened', match_all_labels: true)
- issues = issues.reorder(params[:order_by] => params[:sort])
present paginate(issues), with: Entities::Issue, current_user: current_user
end
end
@@ -91,26 +87,21 @@ module API
requires :id, type: String, desc: 'The ID of a project'
end
resource :projects do
+ include TimeTrackingEndpoints
+
desc 'Get a list of project issues' do
success Entities::Issue
end
params do
optional :state, type: String, values: %w[opened closed all], default: 'all',
desc: 'Return opened, closed, or all issues'
- optional :iid, type: Integer, desc: 'The IID of the issue'
use :issues_params
end
get ":id/issues" do
- issues = IssuesFinder.new(current_user, project_id: user_project.id).execute.inc_notes_with_associations
- issues = filter_issues_state(issues, params[:state])
- issues = filter_issues_labels(issues, params[:labels]) unless params[:labels].nil?
- issues = filter_by_iid(issues, params[:iid]) unless params[:iid].nil?
+ project = find_project(params[:id])
- unless params[:milestone].nil?
- issues = filter_issues_milestone(issues, params[:milestone])
- end
+ issues = find_issues(project_id: project.id)
- issues = issues.reorder(params[:order_by] => params[:sort])
present paginate(issues), with: Entities::Issue, current_user: current_user, project: user_project
end
@@ -172,6 +163,7 @@ module API
optional :title, type: String, desc: 'The title of an issue'
optional :updated_at, type: DateTime,
desc: 'Date time when the issue was updated. Available only for admins and project owners.'
+ optional :state_event, type: String, values: %w[reopen close], desc: 'State of the issue'
use :issue_params
at_least_one_of :title, :description, :assignee_id, :milestone_id,
:labels, :created_at, :due_date, :confidential, :state_event
diff --git a/lib/api/merge_request_diffs.rb b/lib/api/merge_request_diffs.rb
index 07435d78468..bc3d69f6904 100644
--- a/lib/api/merge_request_diffs.rb
+++ b/lib/api/merge_request_diffs.rb
@@ -15,10 +15,8 @@ module API
end
get ":id/merge_requests/:merge_request_id/versions" do
- merge_request = user_project.merge_requests.
- find(params[:merge_request_id])
+ merge_request = find_merge_request_with_access(params[:merge_request_id])
- authorize! :read_merge_request, merge_request
present merge_request.merge_request_diffs, with: Entities::MergeRequestDiff
end
@@ -34,10 +32,8 @@ module API
end
get ":id/merge_requests/:merge_request_id/versions/:version_id" do
- merge_request = user_project.merge_requests.
- find(params[:merge_request_id])
+ merge_request = find_merge_request_with_access(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
diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb
index 5d1fe22f2df..782147883c8 100644
--- a/lib/api/merge_requests.rb
+++ b/lib/api/merge_requests.rb
@@ -2,14 +2,14 @@ module API
class MergeRequests < Grape::API
include PaginationParams
- DEPRECATION_MESSAGE = 'This endpoint is deprecated and will be removed in GitLab 9.0.'.freeze
-
before { authenticate! }
params do
requires :id, type: String, desc: 'The ID of a project'
end
resource :projects do
+ include TimeTrackingEndpoints
+
helpers do
def handle_merge_request_errors!(errors)
if errors[:project_access].any?
@@ -44,14 +44,14 @@ module API
desc: 'Return merge requests ordered by `created_at` or `updated_at` fields.'
optional :sort, type: String, values: %w[asc desc], default: 'desc',
desc: 'Return merge requests sorted in `asc` or `desc` order.'
- optional :iid, type: Array[Integer], desc: 'The IID of the merge requests'
+ optional :iids, type: Array[Integer], desc: 'The IID array of merge requests'
use :pagination
end
get ":id/merge_requests" do
authorize! :read_merge_request, user_project
merge_requests = user_project.merge_requests.inc_notes_with_associations
- merge_requests = filter_by_iid(merge_requests, params[:iid]) if params[:iid].present?
+ merge_requests = filter_by_iid(merge_requests, params[:iids]) if params[:iids].present?
merge_requests =
case params[:state]
@@ -96,188 +96,173 @@ module API
requires :merge_request_id, type: Integer, desc: 'The ID of a merge request'
end
delete ":id/merge_requests/:merge_request_id" do
- merge_request = user_project.merge_requests.find_by(id: params[:merge_request_id])
+ merge_request = find_project_merge_request(params[:merge_request_id])
authorize!(:destroy_merge_request, merge_request)
merge_request.destroy
end
- # Routing "merge_request/:merge_request_id/..." is DEPRECATED and WILL BE REMOVED in version 9.0
- # Use "merge_requests/:merge_request_id/..." instead.
- #
params do
requires :merge_request_id, type: Integer, desc: 'The ID of a merge request'
end
- { ":id/merge_request/:merge_request_id" => :deprecated, ":id/merge_requests/:merge_request_id" => :ok }.each do |path, status|
- desc 'Get a single merge request' do
- if status == :deprecated
- detail DEPRECATION_MESSAGE
- end
- success Entities::MergeRequest
- end
- get path do
- merge_request = user_project.merge_requests.find(params[:merge_request_id])
- authorize! :read_merge_request, merge_request
- present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project
- end
+ desc 'Get a single merge request' do
+ success Entities::MergeRequest
+ end
+ get ':id/merge_requests/:merge_request_id' do
+ merge_request = find_merge_request_with_access(params[:merge_request_id])
- desc 'Get the commits of a merge request' do
- success Entities::RepoCommit
- end
- get "#{path}/commits" do
- merge_request = user_project.merge_requests.find(params[:merge_request_id])
- authorize! :read_merge_request, merge_request
- present merge_request.commits, with: Entities::RepoCommit
- end
+ present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project
+ end
- desc 'Show the merge request changes' do
- success Entities::MergeRequestChanges
- end
- get "#{path}/changes" do
- merge_request = user_project.merge_requests.find(params[:merge_request_id])
- authorize! :read_merge_request, merge_request
- present merge_request, with: Entities::MergeRequestChanges, current_user: current_user
- end
+ desc 'Get the commits of a merge request' do
+ success Entities::RepoCommit
+ end
+ get ':id/merge_requests/:merge_request_id/commits' do
+ merge_request = find_merge_request_with_access(params[:merge_request_id])
- desc 'Update a merge request' do
- success Entities::MergeRequest
- end
- params do
- optional :title, type: String, allow_blank: false, desc: 'The title of the merge request'
- optional :target_branch, type: String, allow_blank: false, desc: 'The target branch'
- optional :state_event, type: String, values: %w[close reopen merge],
- desc: 'Status of the merge request'
- use :optional_params
- at_least_one_of :title, :target_branch, :description, :assignee_id,
- :milestone_id, :labels, :state_event,
- :remove_source_branch
- end
- put path do
- merge_request = user_project.merge_requests.find(params.delete(:merge_request_id))
- authorize! :update_merge_request, merge_request
+ present merge_request.commits, with: Entities::RepoCommit
+ end
- mr_params = declared_params(include_missing: false)
- mr_params[:force_remove_source_branch] = mr_params.delete(:remove_source_branch) if mr_params[:remove_source_branch].present?
+ desc 'Show the merge request changes' do
+ success Entities::MergeRequestChanges
+ end
+ get ':id/merge_requests/:merge_request_id/changes' do
+ merge_request = find_merge_request_with_access(params[:merge_request_id])
- merge_request = ::MergeRequests::UpdateService.new(user_project, current_user, mr_params).execute(merge_request)
+ present merge_request, with: Entities::MergeRequestChanges, current_user: current_user
+ end
- if merge_request.valid?
- present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project
- else
- handle_merge_request_errors! merge_request.errors
- end
- end
+ desc 'Update a merge request' do
+ success Entities::MergeRequest
+ end
+ params do
+ optional :title, type: String, allow_blank: false, desc: 'The title of the merge request'
+ optional :target_branch, type: String, allow_blank: false, desc: 'The target branch'
+ optional :state_event, type: String, values: %w[close reopen merge],
+ desc: 'Status of the merge request'
+ use :optional_params
+ at_least_one_of :title, :target_branch, :description, :assignee_id,
+ :milestone_id, :labels, :state_event,
+ :remove_source_branch
+ end
+ put ':id/merge_requests/:merge_request_id' do
+ merge_request = find_merge_request_with_access(params.delete(:merge_request_id), :update_merge_request)
- desc 'Merge a merge request' do
- success Entities::MergeRequest
- end
- params do
- optional :merge_commit_message, type: String, desc: 'Custom merge commit message'
- optional :should_remove_source_branch, type: Boolean,
- desc: 'When true, the source branch will be deleted if possible'
- optional :merge_when_build_succeeds, type: Boolean,
- desc: 'When true, this merge request will be merged when the pipeline succeeds'
- optional :sha, type: String, desc: 'When present, must have the HEAD SHA of the source branch'
- end
- put "#{path}/merge" do
- merge_request = user_project.merge_requests.find(params[:merge_request_id])
+ mr_params = declared_params(include_missing: false)
+ mr_params[:force_remove_source_branch] = mr_params.delete(:remove_source_branch) if mr_params[:remove_source_branch].present?
- # Merge request can not be merged
- # because user dont have permissions to push into target branch
- unauthorized! unless merge_request.can_be_merged_by?(current_user)
+ merge_request = ::MergeRequests::UpdateService.new(user_project, current_user, mr_params).execute(merge_request)
- not_allowed! unless merge_request.mergeable_state?
+ if merge_request.valid?
+ present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project
+ else
+ handle_merge_request_errors! merge_request.errors
+ end
+ end
- render_api_error!('Branch cannot be merged', 406) unless merge_request.mergeable?
+ desc 'Merge a merge request' do
+ success Entities::MergeRequest
+ end
+ params do
+ optional :merge_commit_message, type: String, desc: 'Custom merge commit message'
+ optional :should_remove_source_branch, type: Boolean,
+ desc: 'When true, the source branch will be deleted if possible'
+ optional :merge_when_build_succeeds, type: Boolean,
+ desc: 'When true, this merge request will be merged when the pipeline succeeds'
+ optional :sha, type: String, desc: 'When present, must have the HEAD SHA of the source branch'
+ end
+ put ':id/merge_requests/:merge_request_id/merge' do
+ merge_request = find_project_merge_request(params[:merge_request_id])
- if params[:sha] && merge_request.diff_head_sha != params[:sha]
- render_api_error!("SHA does not match HEAD of source branch: #{merge_request.diff_head_sha}", 409)
- end
+ # Merge request can not be merged
+ # because user dont have permissions to push into target branch
+ unauthorized! unless merge_request.can_be_merged_by?(current_user)
- merge_params = {
- commit_message: params[:merge_commit_message],
- should_remove_source_branch: params[:should_remove_source_branch]
- }
-
- if params[:merge_when_build_succeeds] && merge_request.head_pipeline && merge_request.head_pipeline.active?
- ::MergeRequests::MergeWhenPipelineSucceedsService
- .new(merge_request.target_project, current_user, merge_params)
- .execute(merge_request)
- else
- ::MergeRequests::MergeService
- .new(merge_request.target_project, current_user, merge_params)
- .execute(merge_request)
- end
+ not_allowed! unless merge_request.mergeable_state?
- present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project
- end
+ render_api_error!('Branch cannot be merged', 406) unless merge_request.mergeable?
- desc 'Cancel merge if "Merge When Pipeline Succeeds" is enabled' do
- success Entities::MergeRequest
+ if params[:sha] && merge_request.diff_head_sha != params[:sha]
+ render_api_error!("SHA does not match HEAD of source branch: #{merge_request.diff_head_sha}", 409)
end
- post "#{path}/cancel_merge_when_build_succeeds" do
- merge_request = user_project.merge_requests.find(params[:merge_request_id])
- unauthorized! unless merge_request.can_cancel_merge_when_build_succeeds?(current_user)
+ merge_params = {
+ commit_message: params[:merge_commit_message],
+ should_remove_source_branch: params[:should_remove_source_branch]
+ }
- ::MergeRequest::MergeWhenPipelineSucceedsService
- .new(merge_request.target_project, current_user)
- .cancel(merge_request)
+ if params[:merge_when_build_succeeds] && merge_request.head_pipeline && merge_request.head_pipeline.active?
+ ::MergeRequests::MergeWhenPipelineSucceedsService
+ .new(merge_request.target_project, current_user, merge_params)
+ .execute(merge_request)
+ else
+ ::MergeRequests::MergeService
+ .new(merge_request.target_project, current_user, merge_params)
+ .execute(merge_request)
end
- desc 'Get the comments of a merge request' do
- detail 'Duplicate. DEPRECATED and WILL BE REMOVED in 9.0'
- success Entities::MRNote
- end
- params do
- use :pagination
- end
- get "#{path}/comments" do
- merge_request = user_project.merge_requests.find(params[:merge_request_id])
+ present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project
+ end
- authorize! :read_merge_request, merge_request
+ desc 'Cancel merge if "Merge When Pipeline Succeeds" is enabled' do
+ success Entities::MergeRequest
+ end
+ post ':id/merge_requests/:merge_request_id/cancel_merge_when_build_succeeds' do
+ merge_request = find_project_merge_request(params[:merge_request_id])
- present paginate(merge_request.notes.fresh), with: Entities::MRNote
- end
+ unauthorized! unless merge_request.can_cancel_merge_when_build_succeeds?(current_user)
- desc 'Post a comment to a merge request' do
- detail 'Duplicate. DEPRECATED and WILL BE REMOVED in 9.0'
- success Entities::MRNote
- end
- params do
- requires :note, type: String, desc: 'The text of the comment'
- end
- post "#{path}/comments" do
- merge_request = user_project.merge_requests.find(params[:merge_request_id])
- authorize! :create_note, merge_request
-
- opts = {
- note: params[:note],
- noteable_type: 'MergeRequest',
- noteable_id: merge_request.id
- }
-
- note = ::Notes::CreateService.new(user_project, current_user, opts).execute
-
- if note.save
- present note, with: Entities::MRNote
- else
- render_api_error!("Failed to save note #{note.errors.messages}", 400)
- end
- end
+ ::MergeRequest::MergeWhenPipelineSucceedsService
+ .new(merge_request.target_project, current_user)
+ .cancel(merge_request)
+ end
- desc 'List issues that will be closed on merge' do
- success Entities::MRNote
- end
- params do
- use :pagination
- end
- get "#{path}/closes_issues" do
- merge_request = user_project.merge_requests.find(params[:merge_request_id])
- issues = ::Kaminari.paginate_array(merge_request.closes_issues(current_user))
- present paginate(issues), with: issue_entity(user_project), current_user: current_user
+ desc 'Get the comments of a merge request' do
+ success Entities::MRNote
+ end
+ params do
+ use :pagination
+ end
+ get ':id/merge_requests/:merge_request_id/comments' do
+ merge_request = find_merge_request_with_access(params[:merge_request_id])
+ present paginate(merge_request.notes.fresh), with: Entities::MRNote
+ end
+
+ desc 'Post a comment to a merge request' do
+ success Entities::MRNote
+ end
+ params do
+ requires :note, type: String, desc: 'The text of the comment'
+ end
+ post ':id/merge_requests/:merge_request_id/comments' do
+ merge_request = find_merge_request_with_access(params[:merge_request_id], :create_note)
+
+ opts = {
+ note: params[:note],
+ noteable_type: 'MergeRequest',
+ noteable_id: merge_request.id
+ }
+
+ note = ::Notes::CreateService.new(user_project, current_user, opts).execute
+
+ if note.save
+ present note, with: Entities::MRNote
+ else
+ render_api_error!("Failed to save note #{note.errors.messages}", 400)
end
end
+
+ desc 'List issues that will be closed on merge' do
+ success Entities::MRNote
+ end
+ params do
+ use :pagination
+ end
+ get ':id/merge_requests/:merge_request_id/closes_issues' do
+ merge_request = find_merge_request_with_access(params[:merge_request_id])
+ issues = ::Kaminari.paginate_array(merge_request.closes_issues(current_user))
+ present paginate(issues), with: issue_entity(user_project), current_user: current_user
+ end
end
end
end
diff --git a/lib/api/notes.rb b/lib/api/notes.rb
index d0faf17714b..4d2a8f48267 100644
--- a/lib/api/notes.rb
+++ b/lib/api/notes.rb
@@ -69,24 +69,28 @@ module API
optional :created_at, type: String, desc: 'The creation date of the note'
end
post ":id/#{noteables_str}/:noteable_id/notes" do
- required_attributes! [:body]
-
opts = {
- note: params[:body],
- noteable_type: noteables_str.classify,
- noteable_id: params[:noteable_id]
+ note: params[:body],
+ noteable_type: noteables_str.classify,
+ noteable_id: params[:noteable_id]
}
- if params[:created_at] && (current_user.is_admin? || user_project.owner == current_user)
- opts[:created_at] = params[:created_at]
- end
+ noteable = user_project.send(noteables_str.to_sym).find(params[:noteable_id])
- note = ::Notes::CreateService.new(user_project, current_user, opts).execute
+ if can?(current_user, noteable_read_ability_name(noteable), noteable)
+ if params[:created_at] && (current_user.is_admin? || user_project.owner == current_user)
+ opts[:created_at] = params[:created_at]
+ end
- if note.valid?
- present note, with: Entities::const_get(note.class.name)
+ note = ::Notes::CreateService.new(user_project, current_user, opts).execute
+
+ if note.valid?
+ present note, with: Entities::const_get(note.class.name)
+ else
+ not_found!("Note #{note.errors.messages}")
+ end
else
- not_found!("Note #{note.errors.messages}")
+ not_found!("Note")
end
end
diff --git a/lib/api/project_hooks.rb b/lib/api/project_hooks.rb
index dcc0fb7a911..cb679e6658a 100644
--- a/lib/api/project_hooks.rb
+++ b/lib/api/project_hooks.rb
@@ -15,7 +15,7 @@ module API
optional :note_events, type: Boolean, desc: "Trigger hook on note(comment) events"
optional :build_events, type: Boolean, desc: "Trigger hook on build events"
optional :pipeline_events, type: Boolean, desc: "Trigger hook on pipeline events"
- optional :wiki_events, type: Boolean, desc: "Trigger hook on wiki events"
+ optional :wiki_page_events, type: Boolean, desc: "Trigger hook on wiki events"
optional :enable_ssl_verification, type: Boolean, desc: "Do SSL verification when triggering the hook"
optional :token, type: String, desc: "Secret token to validate received payloads; this will not be returned in the response"
end
diff --git a/lib/api/project_snippets.rb b/lib/api/project_snippets.rb
index 9d8c5b63685..dcc0c82ee27 100644
--- a/lib/api/project_snippets.rb
+++ b/lib/api/project_snippets.rb
@@ -58,7 +58,7 @@ module API
end
post ":id/snippets" do
authorize! :create_project_snippet, user_project
- snippet_params = declared_params
+ snippet_params = declared_params.merge(request: request, api: true)
snippet_params[:content] = snippet_params.delete(:code)
snippet = CreateSnippetService.new(user_project, current_user, snippet_params).execute
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index 2929d2157dc..92a70faf1c2 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -40,6 +40,15 @@ module API
resource :projects do
helpers do
+ params :collection_params do
+ use :sort_params
+ use :filter_params
+ use :pagination
+
+ optional :simple, type: Boolean, default: false,
+ desc: 'Return only the ID, URL, name, and path of each project'
+ end
+
params :sort_params do
optional :order_by, type: String, values: %w[id name path created_at updated_at last_activity_at],
default: 'created_at', desc: 'Return projects ordered by field'
@@ -52,113 +61,94 @@ module API
optional :visibility, type: String, values: %w[public internal private],
desc: 'Limit by visibility'
optional :search, type: String, desc: 'Return list of authorized projects matching the search criteria'
- use :sort_params
+ end
+
+ params :statistics_params do
+ optional :statistics, type: Boolean, default: false, desc: 'Include project statistics'
end
params :create_params do
optional :namespace_id, type: Integer, desc: 'Namespace ID for the new project. Default to the user namespace.'
optional :import_url, type: String, desc: 'URL from which the project is imported'
end
+
+ def present_projects(projects, options = {})
+ options = options.reverse_merge(
+ with: Entities::Project,
+ current_user: current_user,
+ simple: params[:simple],
+ )
+
+ projects = filter_projects(projects)
+ projects = projects.with_statistics if options[:statistics]
+ options[:with] = Entities::BasicProjectDetails if options[:simple]
+
+ present paginate(projects), options
+ end
end
desc 'Get a list of visible projects for authenticated user' do
success Entities::BasicProjectDetails
end
params do
- optional :simple, type: Boolean, default: false,
- desc: 'Return only the ID, URL, name, and path of each project'
- use :filter_params
- use :pagination
+ use :collection_params
end
get '/visible' do
- projects = ProjectsFinder.new.execute(current_user)
- projects = filter_projects(projects)
- entity = params[:simple] || !current_user ? Entities::BasicProjectDetails : Entities::ProjectWithAccess
-
- present paginate(projects), with: entity, user: current_user
+ entity = current_user ? Entities::ProjectWithAccess : Entities::BasicProjectDetails
+ present_projects ProjectsFinder.new.execute(current_user), with: entity
end
desc 'Get a projects list for authenticated user' do
success Entities::BasicProjectDetails
end
params do
- optional :simple, type: Boolean, default: false,
- desc: 'Return only the ID, URL, name, and path of each project'
- use :filter_params
- use :pagination
+ use :collection_params
end
get do
authenticate!
- projects = current_user.authorized_projects
- projects = filter_projects(projects)
- entity = params[:simple] ? Entities::BasicProjectDetails : Entities::ProjectWithAccess
-
- present paginate(projects), with: entity, user: current_user
+ present_projects current_user.authorized_projects,
+ with: Entities::ProjectWithAccess
end
desc 'Get an owned projects list for authenticated user' do
success Entities::BasicProjectDetails
end
params do
- use :filter_params
- use :pagination
+ use :collection_params
+ use :statistics_params
end
get '/owned' do
authenticate!
- projects = current_user.owned_projects
- projects = filter_projects(projects)
-
- present paginate(projects), with: Entities::ProjectWithAccess, user: current_user
+ present_projects current_user.owned_projects,
+ with: Entities::ProjectWithAccess,
+ statistics: params[:statistics]
end
desc 'Gets starred project for the authenticated user' do
success Entities::BasicProjectDetails
end
params do
- use :filter_params
- use :pagination
+ use :collection_params
end
get '/starred' do
authenticate!
- projects = current_user.viewable_starred_projects
- projects = filter_projects(projects)
-
- present paginate(projects), with: Entities::Project, user: current_user
+ present_projects current_user.viewable_starred_projects
end
desc 'Get all projects for admin user' do
success Entities::BasicProjectDetails
end
params do
- use :filter_params
- use :pagination
+ use :collection_params
+ use :statistics_params
end
get '/all' do
authenticated_as_admin!
- projects = Project.all
- projects = filter_projects(projects)
-
- present paginate(projects), with: Entities::ProjectWithAccess, user: current_user
- end
-
- desc 'Search for projects the current user has access to' do
- success Entities::Project
- end
- params do
- requires :query, type: String, desc: 'The project name to be searched'
- use :sort_params
- use :pagination
- end
- get "/search/:query" do
- search_service = Search::GlobalService.new(current_user, search: params[:query]).execute
- projects = search_service.objects('projects', params[:page])
- projects = projects.reorder(params[:order_by] => params[:sort])
-
- present paginate(projects), with: Entities::Project
+ present_projects Project.all, with: Entities::ProjectWithAccess, statistics: params[:statistics]
end
desc 'Create new project' do
@@ -221,7 +211,7 @@ module API
end
get ":id" do
entity = current_user ? Entities::ProjectWithAccess : Entities::BasicProjectDetails
- present user_project, with: entity, user: current_user,
+ present user_project, with: entity, current_user: current_user,
user_can_admin_project: can?(current_user, :admin_project, user_project)
end
@@ -289,13 +279,13 @@ module API
authorize! :rename_project, user_project if attrs[:name].present?
authorize! :change_visibility_level, user_project if attrs[:visibility_level].present?
- ::Projects::UpdateService.new(user_project, current_user, attrs).execute
+ result = ::Projects::UpdateService.new(user_project, current_user, attrs).execute
- if user_project.errors.any?
- render_validation_error!(user_project)
- else
+ if result[:status] == :success
present user_project, with: Entities::Project,
user_can_admin_project: can?(current_user, :admin_project, user_project)
+ else
+ render_validation_error!(user_project)
end
end
diff --git a/lib/api/services.rb b/lib/api/services.rb
index aa97f6af0b2..1456fe4688b 100644
--- a/lib/api/services.rb
+++ b/lib/api/services.rb
@@ -145,7 +145,7 @@ module API
name: :room,
type: String,
desc: 'Campfire room'
- },
+ }
],
'custom-issue-tracker' => [
{
@@ -480,7 +480,7 @@ module API
desc: 'The description of the tracker'
}
],
- 'slack-notification' => [
+ 'slack' => [
{
required: true,
name: :webhook,
@@ -500,7 +500,7 @@ module API
desc: 'The channel name'
}
],
- 'mattermost-notification' => [
+ 'mattermost' => [
{
required: true,
name: :webhook,
@@ -534,7 +534,36 @@ module API
desc: 'The password of the user'
}
]
- }.freeze
+ }
+
+ service_classes = [
+ AsanaService,
+ AssemblaService,
+ BambooService,
+ BugzillaService,
+ BuildkiteService,
+ BuildsEmailService,
+ CampfireService,
+ CustomIssueTrackerService,
+ DroneCiService,
+ EmailsOnPushService,
+ ExternalWikiService,
+ FlowdockService,
+ GemnasiumService,
+ HipchatService,
+ IrkerService,
+ JiraService,
+ KubernetesService,
+ MattermostSlashCommandsService,
+ SlackSlashCommandsService,
+ PipelinesEmailService,
+ PivotaltrackerService,
+ PushoverService,
+ RedmineService,
+ SlackService,
+ MattermostService,
+ TeamcityService,
+ ].freeze
trigger_services = {
'mattermost-slash-commands' => [
@@ -543,6 +572,13 @@ module API
type: String,
desc: 'The Mattermost token'
}
+ ],
+ 'slack-slash-commands' => [
+ {
+ name: :token,
+ type: String,
+ desc: 'The Slack token'
+ }
]
}.freeze
@@ -561,6 +597,19 @@ module API
services.each do |service_slug, settings|
desc "Set #{service_slug} service for project"
params do
+ service_classes.each do |service|
+ event_names = service.try(:event_names) || []
+ event_names.each do |event_name|
+ services[service.to_param.tr("_", "-")] << {
+ required: false,
+ name: event_name.to_sym,
+ type: String,
+ desc: ServicesHelper.service_event_description(event_name)
+ }
+ end
+ end
+ services.freeze
+
settings.each do |setting|
if setting[:required]
requires setting[:name], type: setting[:type], desc: setting[:desc]
@@ -574,7 +623,7 @@ module API
service_params = declared_params(include_missing: false).merge(active: true)
if service.update_attributes(service_params)
- true
+ present service, with: Entities::ProjectService, include_passwords: current_user.is_admin?
else
render_api_error!('400 Bad Request', 400)
end
@@ -612,6 +661,14 @@ module API
end
trigger_services.each do |service_slug, settings|
+ helpers do
+ def chat_command_service(project, service_slug, params)
+ project.services.active.where(template: false).find do |service|
+ service.try(:token) == params[:token] && service.to_param == service_slug.underscore
+ end
+ end
+ end
+
params do
requires :id, type: String, desc: 'The ID of a project'
end
@@ -630,9 +687,8 @@ module API
# This is not accurate, but done to prevent leakage of the project names
not_found!('Service') unless project
- service = project.find_or_initialize_service(service_slug.underscore)
-
- result = service.try(:active?) && service.try(:trigger, params)
+ service = chat_command_service(project, service_slug, params)
+ result = service.try(:trigger, params)
if result
status result[:status] || 200
diff --git a/lib/api/settings.rb b/lib/api/settings.rb
index c4cb1c7924a..747ceb4e3e0 100644
--- a/lib/api/settings.rb
+++ b/lib/api/settings.rb
@@ -9,23 +9,123 @@ module API
end
end
- # Get current applicaiton settings
- #
- # Example Request:
- # GET /application/settings
+ desc 'Get the current application settings' do
+ success Entities::ApplicationSetting
+ end
get "application/settings" do
present current_settings, with: Entities::ApplicationSetting
end
- # Modify application settings
- #
- # Example Request:
- # PUT /application/settings
+ desc 'Modify application settings' do
+ success Entities::ApplicationSetting
+ end
+ params do
+ optional :default_branch_protection, type: Integer, values: [0, 1, 2], desc: 'Determine if developers can push to master'
+ optional :default_project_visibility, type: Integer, values: Gitlab::VisibilityLevel.values, desc: 'The default project visibility'
+ optional :default_snippet_visibility, type: Integer, values: Gitlab::VisibilityLevel.values, desc: 'The default snippet visibility'
+ optional :default_group_visibility, type: Integer, values: Gitlab::VisibilityLevel.values, desc: 'The default group visibility'
+ optional :restricted_visibility_levels, type: Array[String], desc: 'Selected levels cannot be used by non-admin users for projects or snippets. If the public level is restricted, user profiles are only visible to logged in users.'
+ optional :import_sources, type: Array[String], values: %w[github bitbucket gitlab google_code fogbugz git gitlab_project],
+ desc: 'Enabled sources for code import during project creation. OmniAuth must be configured for GitHub, Bitbucket, and GitLab.com'
+ optional :disabled_oauth_sign_in_sources, type: Array[String], desc: 'Disable certain OAuth sign-in sources'
+ optional :enabled_git_access_protocol, type: String, values: %w[ssh http nil], desc: 'Allow only the selected protocols to be used for Git access.'
+ optional :gravatar_enabled, type: Boolean, desc: 'Flag indicating if the Gravatar service is enabled'
+ optional :default_projects_limit, type: Integer, desc: 'The maximum number of personal projects'
+ optional :max_attachment_size, type: Integer, desc: 'Maximum attachment size in MB'
+ optional :session_expire_delay, type: Integer, desc: 'Session duration in minutes. GitLab restart is required to apply changes.'
+ optional :user_oauth_applications, type: Boolean, desc: 'Allow users to register any application to use GitLab as an OAuth provider'
+ optional :user_default_external, type: Boolean, desc: 'Newly registered users will by default be external'
+ optional :signup_enabled, type: Boolean, desc: 'Flag indicating if sign up is enabled'
+ optional :send_user_confirmation_email, type: Boolean, desc: 'Send confirmation email on sign-up'
+ optional :domain_whitelist, type: String, desc: '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'
+ optional :domain_blacklist_enabled, type: Boolean, desc: 'Enable domain blacklist for sign ups'
+ given domain_blacklist_enabled: ->(val) { val } do
+ requires :domain_blacklist, type: String, desc: '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'
+ end
+ optional :after_sign_up_text, type: String, desc: 'Text shown after sign up'
+ optional :signin_enabled, type: Boolean, desc: 'Flag indicating if sign in is enabled'
+ optional :require_two_factor_authentication, type: Boolean, desc: 'Require all users to setup Two-factor authentication'
+ given require_two_factor_authentication: ->(val) { val } do
+ requires :two_factor_grace_period, type: Integer, desc: 'Amount of time (in hours) that users are allowed to skip forced configuration of two-factor authentication'
+ end
+ optional :home_page_url, type: String, desc: 'We will redirect non-logged in users to this page'
+ optional :after_sign_out_path, type: String, desc: 'We will redirect users to this page after they sign out'
+ optional :sign_in_text, type: String, desc: 'The sign in text of the GitLab application'
+ optional :help_page_text, type: String, desc: 'Custom text displayed on the help page'
+ optional :shared_runners_enabled, type: Boolean, desc: 'Enable shared runners for new projects'
+ given shared_runners_enabled: ->(val) { val } do
+ requires :shared_runners_text, type: String, desc: 'Shared runners text '
+ end
+ optional :max_artifacts_size, type: Integer, desc: "Set the maximum file size each build's artifacts can have"
+ optional :max_pages_size, type: Integer, desc: 'Maximum size of pages in MB'
+ optional :container_registry_token_expire_delay, type: Integer, desc: 'Authorization token duration (minutes)'
+ optional :metrics_enabled, type: Boolean, desc: 'Enable the InfluxDB metrics'
+ given metrics_enabled: ->(val) { val } do
+ requires :metrics_host, type: String, desc: 'The InfluxDB host'
+ requires :metrics_port, type: Integer, desc: 'The UDP port to use for connecting to InfluxDB'
+ requires :metrics_pool_size, type: Integer, desc: 'The amount of InfluxDB connections to open'
+ requires :metrics_timeout, type: Integer, desc: 'The amount of seconds after which an InfluxDB connection will time out'
+ requires :metrics_method_call_threshold, type: Integer, desc: 'A method call is only tracked when it takes longer to complete than the given amount of milliseconds.'
+ requires :metrics_sample_interval, type: Integer, desc: 'The sampling interval in seconds'
+ requires :metrics_packet_size, type: Integer, desc: 'The amount of points to store in a single UDP packet'
+ end
+ optional :sidekiq_throttling_enabled, type: Boolean, desc: 'Enable Sidekiq Job Throttling'
+ given sidekiq_throttling_enabled: ->(val) { val } do
+ requires :sidekiq_throttling_queus, type: Array[String], desc: 'Choose which queues you wish to throttle'
+ requires :sidekiq_throttling_factor, type: Float, desc: 'The factor by which the queues should be throttled. A value between 0.0 and 1.0, exclusive.'
+ end
+ optional :recaptcha_enabled, type: Boolean, desc: 'Helps prevent bots from creating accounts'
+ given recaptcha_enabled: ->(val) { val } do
+ requires :recaptcha_site_key, type: String, desc: 'Generate site key at http://www.google.com/recaptcha'
+ requires :recaptcha_private_key, type: String, desc: 'Generate private key at http://www.google.com/recaptcha'
+ end
+ optional :akismet_enabled, type: Boolean, desc: 'Helps prevent bots from creating issues'
+ given akismet_enabled: ->(val) { val } do
+ requires :akismet_api_key, type: String, desc: 'Generate API key at http://www.akismet.com'
+ end
+ optional :admin_notification_email, type: String, desc: 'Abuse reports will be sent to this address if it is set. Abuse reports are always available in the admin area.'
+ optional :sentry_enabled, type: Boolean, desc: 'Sentry is an error reporting and logging tool which is currently not shipped with GitLab, get it here: https://getsentry.com'
+ given sentry_enabled: ->(val) { val } do
+ requires :sentry_dsn, type: String, desc: 'Sentry Data Source Name'
+ end
+ optional :repository_storage, type: String, desc: 'Storage paths for new projects'
+ optional :repository_checks_enabled, type: Boolean, desc: "GitLab will periodically run 'git fsck' in all project and wiki repositories to look for silent disk corruption issues."
+ optional :koding_enabled, type: Boolean, desc: 'Enable Koding'
+ given koding_enabled: ->(val) { val } do
+ requires :koding_url, type: String, desc: 'The Koding team URL'
+ end
+ optional :plantuml_enabled, type: Boolean, desc: 'Enable PlantUML'
+ given plantuml_enabled: ->(val) { val } do
+ requires :plantuml_url, type: String, desc: 'The PlantUML server URL'
+ end
+ optional :version_check_enabled, type: Boolean, desc: 'Let GitLab inform you when an update is available.'
+ optional :email_author_in_body, type: Boolean, desc: 'Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead.'
+ optional :html_emails_enabled, type: Boolean, desc: 'By default GitLab sends emails in HTML and plain text formats so mail clients can choose what format to use. Disable this option if you only want to send emails in plain text format.'
+ optional :housekeeping_enabled, type: Boolean, desc: 'Enable automatic repository housekeeping (git repack, git gc)'
+ given housekeeping_enabled: ->(val) { val } do
+ requires :housekeeping_bitmaps_enabled, type: Boolean, desc: "Creating pack file bitmaps makes housekeeping take a little longer but bitmaps should accelerate 'git clone' performance."
+ requires :housekeeping_incremental_repack_period, type: Integer, desc: "Number of Git pushes after which an incremental 'git repack' is run."
+ requires :housekeeping_full_repack_period, type: Integer, desc: "Number of Git pushes after which a full 'git repack' is run."
+ requires :housekeeping_gc_period, type: Integer, desc: "Number of Git pushes after which 'git gc' is run."
+ end
+ optional :terminal_max_session_time, type: Integer, desc: 'Maximum time for web terminal websocket connection (in seconds). Set to 0 for unlimited time.'
+ at_least_one_of :default_branch_protection, :default_project_visibility, :default_snippet_visibility,
+ :default_group_visibility, :restricted_visibility_levels, :import_sources,
+ :enabled_git_access_protocol, :gravatar_enabled, :default_projects_limit,
+ :max_attachment_size, :session_expire_delay, :disabled_oauth_sign_in_sources,
+ :user_oauth_applications, :user_default_external, :signup_enabled,
+ :send_user_confirmation_email, :domain_whitelist, :domain_blacklist_enabled,
+ :after_sign_up_text, :signin_enabled, :require_two_factor_authentication,
+ :home_page_url, :after_sign_out_path, :sign_in_text, :help_page_text,
+ :shared_runners_enabled, :max_artifacts_size, :max_pages_size, :container_registry_token_expire_delay,
+ :metrics_enabled, :sidekiq_throttling_enabled, :recaptcha_enabled,
+ :akismet_enabled, :admin_notification_email, :sentry_enabled,
+ :repository_storage, :repository_checks_enabled, :koding_enabled, :plantuml_enabled,
+ :version_check_enabled, :email_author_in_body, :html_emails_enabled,
+ :housekeeping_enabled, :terminal_max_session_time
+ end
put "application/settings" do
- attributes = ["repository_storage"] + current_settings.attributes.keys - ["id"]
- attrs = attributes_for_keys(attributes)
-
- if current_settings.update_attributes(attrs)
+ if current_settings.update_attributes(declared_params(include_missing: false))
present current_settings, with: Entities::ApplicationSetting
else
render_validation_error!(current_settings)
diff --git a/lib/api/snippets.rb b/lib/api/snippets.rb
index e096e636806..eb9ece49e7f 100644
--- a/lib/api/snippets.rb
+++ b/lib/api/snippets.rb
@@ -64,7 +64,7 @@ module API
desc: 'The visibility level of the snippet'
end
post do
- attrs = declared_params(include_missing: false)
+ attrs = declared_params(include_missing: false).merge(request: request, api: true)
snippet = CreateSnippetService.new(nil, current_user, attrs).execute
if snippet.persisted?
diff --git a/lib/api/subscriptions.rb b/lib/api/subscriptions.rb
index 10749b34004..e11d7537cc9 100644
--- a/lib/api/subscriptions.rb
+++ b/lib/api/subscriptions.rb
@@ -3,8 +3,8 @@ module API
before { authenticate! }
subscribable_types = {
- 'merge_request' => proc { |id| user_project.merge_requests.find(id) },
- 'merge_requests' => proc { |id| user_project.merge_requests.find(id) },
+ 'merge_request' => proc { |id| find_merge_request_with_access(id, :update_merge_request) },
+ 'merge_requests' => proc { |id| find_merge_request_with_access(id, :update_merge_request) },
'issues' => proc { |id| find_project_issue(id) },
'labels' => proc { |id| find_project_label(id) },
}
diff --git a/lib/api/time_tracking_endpoints.rb b/lib/api/time_tracking_endpoints.rb
new file mode 100644
index 00000000000..85b5f7d98b8
--- /dev/null
+++ b/lib/api/time_tracking_endpoints.rb
@@ -0,0 +1,114 @@
+module API
+ module TimeTrackingEndpoints
+ extend ActiveSupport::Concern
+
+ included do
+ helpers do
+ def issuable_name
+ declared_params.has_key?(:issue_id) ? 'issue' : 'merge_request'
+ end
+
+ def issuable_key
+ "#{issuable_name}_id".to_sym
+ end
+
+ def update_issuable_key
+ "update_#{issuable_name}".to_sym
+ end
+
+ def read_issuable_key
+ "read_#{issuable_name}".to_sym
+ end
+
+ def load_issuable
+ @issuable ||= begin
+ case issuable_name
+ when 'issue'
+ find_project_issue(params.delete(issuable_key))
+ when 'merge_request'
+ find_project_merge_request(params.delete(issuable_key))
+ end
+ end
+ end
+
+ def update_issuable(attrs)
+ custom_params = declared_params(include_missing: false)
+ custom_params.merge!(attrs)
+
+ issuable = update_service.new(user_project, current_user, custom_params).execute(load_issuable)
+ if issuable.valid?
+ present issuable, with: Entities::IssuableTimeStats
+ else
+ render_validation_error!(issuable)
+ end
+ end
+
+ def update_service
+ issuable_name == 'issue' ? ::Issues::UpdateService : ::MergeRequests::UpdateService
+ end
+ end
+
+ issuable_name = name.end_with?('Issues') ? 'issue' : 'merge_request'
+ issuable_collection_name = issuable_name.pluralize
+ issuable_key = "#{issuable_name}_id".to_sym
+
+ desc "Set a time estimate for a project #{issuable_name}"
+ params do
+ requires issuable_key, type: Integer, desc: "The ID of a project #{issuable_name}"
+ requires :duration, type: String, desc: 'The duration to be parsed'
+ end
+ post ":id/#{issuable_collection_name}/:#{issuable_key}/time_estimate" do
+ authorize! update_issuable_key, load_issuable
+
+ status :ok
+ update_issuable(time_estimate: Gitlab::TimeTrackingFormatter.parse(params.delete(:duration)))
+ end
+
+ desc "Reset the time estimate for a project #{issuable_name}"
+ params do
+ requires issuable_key, type: Integer, desc: "The ID of a project #{issuable_name}"
+ end
+ post ":id/#{issuable_collection_name}/:#{issuable_key}/reset_time_estimate" do
+ authorize! update_issuable_key, load_issuable
+
+ status :ok
+ update_issuable(time_estimate: 0)
+ end
+
+ desc "Add spent time for a project #{issuable_name}"
+ params do
+ requires issuable_key, type: Integer, desc: "The ID of a project #{issuable_name}"
+ requires :duration, type: String, desc: 'The duration to be parsed'
+ end
+ post ":id/#{issuable_collection_name}/:#{issuable_key}/add_spent_time" do
+ authorize! update_issuable_key, load_issuable
+
+ update_issuable(spend_time: {
+ duration: Gitlab::TimeTrackingFormatter.parse(params.delete(:duration)),
+ user: current_user
+ })
+ end
+
+ desc "Reset spent time for a project #{issuable_name}"
+ params do
+ requires issuable_key, type: Integer, desc: "The ID of a project #{issuable_name}"
+ end
+ post ":id/#{issuable_collection_name}/:#{issuable_key}/reset_spent_time" do
+ authorize! update_issuable_key, load_issuable
+
+ status :ok
+ update_issuable(spend_time: { duration: :reset, user: current_user })
+ end
+
+ desc "Show time stats for a project #{issuable_name}"
+ params do
+ requires issuable_key, type: Integer, desc: "The ID of a project #{issuable_name}"
+ end
+ get ":id/#{issuable_collection_name}/:#{issuable_key}/time_stats" do
+ authorize! read_issuable_key, load_issuable
+
+ present load_issuable, with: Entities::IssuableTimeStats
+ end
+ end
+ end
+end
diff --git a/lib/api/todos.rb b/lib/api/todos.rb
index ed8f48aa1e3..9bd077263a7 100644
--- a/lib/api/todos.rb
+++ b/lib/api/todos.rb
@@ -5,7 +5,7 @@ module API
before { authenticate! }
ISSUABLE_TYPES = {
- 'merge_requests' => ->(id) { user_project.merge_requests.find(id) },
+ 'merge_requests' => ->(id) { find_merge_request_with_access(id) },
'issues' => ->(id) { find_project_issue(id) }
}
diff --git a/lib/api/users.rb b/lib/api/users.rb
index 4c22287b5c6..0ed468626b7 100644
--- a/lib/api/users.rb
+++ b/lib/api/users.rb
@@ -16,7 +16,7 @@ module API
optional :website_url, type: String, desc: 'The website of the user'
optional :organization, type: String, desc: 'The organization of the user'
optional :projects_limit, type: Integer, desc: 'The number of projects a user can create'
- optional :extern_uid, type: Integer, desc: 'The external authentication provider UID'
+ optional :extern_uid, type: String, desc: 'The external authentication provider UID'
optional :provider, type: String, desc: 'The external provider'
optional :bio, type: String, desc: 'The biography of the user'
optional :location, type: String, desc: 'The location of the user'
@@ -91,10 +91,11 @@ module API
authenticated_as_admin!
# Filter out params which are used later
- identity_attrs = params.slice(:provider, :extern_uid)
- confirm = params.delete(:confirm)
+ user_params = declared_params(include_missing: false)
+ identity_attrs = user_params.slice(:provider, :extern_uid)
+ confirm = user_params.delete(:confirm)
- user = User.new(declared_params(include_missing: false))
+ user = User.new(user_params.except(:extern_uid, :provider))
user.skip_confirmation! unless confirm
if identity_attrs.any?
@@ -159,11 +160,9 @@ module API
end
end
- # Delete already handled parameters
- user_params.delete(:extern_uid)
- user_params.delete(:provider)
+ user_params.merge!(password_expires_at: Time.now) if user_params[:password].present?
- if user.update_attributes(user_params)
+ if user.update_attributes(user_params.except(:extern_uid, :provider))
present user, with: Entities::UserPublic
else
render_validation_error!(user)
diff --git a/lib/api/v3/deploy_keys.rb b/lib/api/v3/deploy_keys.rb
new file mode 100644
index 00000000000..5bbb167755c
--- /dev/null
+++ b/lib/api/v3/deploy_keys.rb
@@ -0,0 +1,122 @@
+module API
+ module V3
+ class DeployKeys < Grape::API
+ before { authenticate! }
+
+ get "deploy_keys" do
+ authenticated_as_admin!
+
+ keys = DeployKey.all
+ present keys, with: ::API::Entities::SSHKey
+ end
+
+ params do
+ requires :id, type: String, desc: 'The ID of the project'
+ end
+ resource :projects do
+ before { authorize_admin_project }
+
+ %w(keys deploy_keys).each do |path|
+ desc "Get a specific project's deploy keys" do
+ success ::API::Entities::SSHKey
+ end
+ get ":id/#{path}" do
+ present user_project.deploy_keys, with: ::API::Entities::SSHKey
+ end
+
+ desc 'Get single deploy key' do
+ success ::API::Entities::SSHKey
+ end
+ params do
+ requires :key_id, type: Integer, desc: 'The ID of the deploy key'
+ end
+ get ":id/#{path}/:key_id" do
+ key = user_project.deploy_keys.find params[:key_id]
+ present key, with: ::API::Entities::SSHKey
+ end
+
+ desc 'Add new deploy key to currently authenticated user' do
+ success ::API::Entities::SSHKey
+ end
+ params do
+ requires :key, type: String, desc: 'The new deploy key'
+ requires :title, type: String, desc: 'The name of the deploy key'
+ end
+ post ":id/#{path}" do
+ params[:key].strip!
+
+ # Check for an existing key joined to this project
+ key = user_project.deploy_keys.find_by(key: params[:key])
+ if key
+ present key, with: ::API::Entities::SSHKey
+ break
+ end
+
+ # Check for available deploy keys in other projects
+ key = current_user.accessible_deploy_keys.find_by(key: params[:key])
+ if key
+ user_project.deploy_keys << key
+ present key, with: ::API::Entities::SSHKey
+ break
+ end
+
+ # Create a new deploy key
+ key = DeployKey.new(declared_params(include_missing: false))
+ if key.valid? && user_project.deploy_keys << key
+ present key, with: ::API::Entities::SSHKey
+ else
+ render_validation_error!(key)
+ end
+ end
+
+ desc 'Enable a deploy key for a project' do
+ detail 'This feature was added in GitLab 8.11'
+ success ::API::Entities::SSHKey
+ end
+ params do
+ requires :key_id, type: Integer, desc: 'The ID of the deploy key'
+ end
+ post ":id/#{path}/:key_id/enable" do
+ key = ::Projects::EnableDeployKeyService.new(user_project,
+ current_user, declared_params).execute
+
+ if key
+ present key, with: ::API::Entities::SSHKey
+ else
+ not_found!('Deploy Key')
+ end
+ end
+
+ desc 'Disable a deploy key for a project' do
+ detail 'This feature was added in GitLab 8.11'
+ success ::API::Entities::SSHKey
+ end
+ params do
+ requires :key_id, type: Integer, desc: 'The ID of the deploy key'
+ end
+ delete ":id/#{path}/:key_id/disable" do
+ key = user_project.deploy_keys_projects.find_by(deploy_key_id: params[:key_id])
+ key.destroy
+
+ present key.deploy_key, with: ::API::Entities::SSHKey
+ end
+
+ desc 'Delete deploy key for a project' do
+ success Key
+ end
+ params do
+ requires :key_id, type: Integer, desc: 'The ID of the deploy key'
+ end
+ delete ":id/#{path}/:key_id" do
+ key = user_project.deploy_keys_projects.find_by(deploy_key_id: params[:key_id])
+ if key
+ key.destroy
+ else
+ not_found!('Deploy Key')
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/v3/entities.rb b/lib/api/v3/entities.rb
new file mode 100644
index 00000000000..3cc0dc968a8
--- /dev/null
+++ b/lib/api/v3/entities.rb
@@ -0,0 +1,16 @@
+module API
+ module V3
+ module Entities
+ class ProjectSnippet < Grape::Entity
+ expose :id, :title, :file_name
+ expose :author, using: ::API::Entities::UserBasic
+ expose :updated_at, :created_at
+ expose(:expires_at) { |snippet| nil }
+
+ expose :web_url do |snippet, options|
+ Gitlab::UrlBuilder.build(snippet)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/v3/issues.rb b/lib/api/v3/issues.rb
new file mode 100644
index 00000000000..081d45165e8
--- /dev/null
+++ b/lib/api/v3/issues.rb
@@ -0,0 +1,231 @@
+module API
+ module V3
+ class Issues < Grape::API
+ include PaginationParams
+
+ before { authenticate! }
+
+ helpers do
+ def find_issues(args = {})
+ args = params.merge(args)
+
+ args.delete(:id)
+ args[:milestone_title] = args.delete(:milestone)
+
+ match_all_labels = args.delete(:match_all_labels)
+ labels = args.delete(:labels)
+ args[:label_name] = labels if match_all_labels
+
+ args[:search] = "#{Issue.reference_prefix}#{args.delete(:iid)}" if args.key?(:iid)
+
+ issues = IssuesFinder.new(current_user, args).execute.inc_notes_with_associations
+
+ if !match_all_labels && labels.present?
+ issues = issues.includes(:labels).where('labels.title' => labels.split(','))
+ end
+
+ issues.reorder(args[:order_by] => args[:sort])
+ end
+
+ params :issues_params do
+ optional :labels, type: String, desc: 'Comma-separated list of label names'
+ optional :milestone, type: String, desc: 'Milestone title'
+ optional :order_by, type: String, values: %w[created_at updated_at], default: 'created_at',
+ desc: 'Return issues ordered by `created_at` or `updated_at` fields.'
+ optional :sort, type: String, values: %w[asc desc], default: 'desc',
+ desc: 'Return issues sorted in `asc` or `desc` order.'
+ optional :milestone, type: String, desc: 'Return issues for a specific milestone'
+ use :pagination
+ end
+
+ params :issue_params do
+ optional :description, type: String, desc: 'The description of an issue'
+ optional :assignee_id, type: Integer, desc: 'The ID of a user to assign issue'
+ optional :milestone_id, type: Integer, desc: 'The ID of a milestone to assign issue'
+ optional :labels, type: String, desc: 'Comma-separated list of label names'
+ optional :due_date, type: String, desc: 'Date time string in the format YEAR-MONTH-DAY'
+ optional :confidential, type: Boolean, desc: 'Boolean parameter if the issue should be confidential'
+ end
+ end
+
+ resource :issues do
+ desc "Get currently authenticated user's issues" do
+ success ::API::Entities::Issue
+ end
+ params do
+ optional :state, type: String, values: %w[opened closed all], default: 'all',
+ desc: 'Return opened, closed, or all issues'
+ use :issues_params
+ end
+ get do
+ issues = find_issues(scope: 'authored')
+
+ present paginate(issues), with: ::API::Entities::Issue, current_user: current_user
+ end
+ end
+
+ params do
+ requires :id, type: String, desc: 'The ID of a group'
+ end
+ resource :groups do
+ desc 'Get a list of group issues' do
+ success ::API::Entities::Issue
+ end
+ params do
+ optional :state, type: String, values: %w[opened closed all], default: 'opened',
+ desc: 'Return opened, closed, or all issues'
+ use :issues_params
+ end
+ get ":id/issues" do
+ group = find_group!(params[:id])
+
+ issues = find_issues(group_id: group.id, state: params[:state] || 'opened', match_all_labels: true)
+
+ present paginate(issues), with: ::API::Entities::Issue, current_user: current_user
+ end
+ end
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+ resource :projects do
+ include TimeTrackingEndpoints
+
+ desc 'Get a list of project issues' do
+ detail 'iid filter is deprecated have been removed on V4'
+ success ::API::Entities::Issue
+ end
+ params do
+ optional :state, type: String, values: %w[opened closed all], default: 'all',
+ desc: 'Return opened, closed, or all issues'
+ optional :iid, type: Integer, desc: 'Return the issue having the given `iid`'
+ use :issues_params
+ end
+ get ":id/issues" do
+ project = find_project(params[:id])
+
+ issues = find_issues(project_id: project.id)
+
+ present paginate(issues), with: ::API::Entities::Issue, current_user: current_user, project: user_project
+ end
+
+ desc 'Get a single project issue' do
+ success ::API::Entities::Issue
+ end
+ params do
+ requires :issue_id, type: Integer, desc: 'The ID of a project issue'
+ end
+ get ":id/issues/:issue_id" do
+ issue = find_project_issue(params[:issue_id])
+ present issue, with: ::API::Entities::Issue, current_user: current_user, project: user_project
+ end
+
+ desc 'Create a new project issue' do
+ success ::API::Entities::Issue
+ end
+ params do
+ requires :title, type: String, desc: 'The title of an issue'
+ optional :created_at, type: DateTime,
+ desc: 'Date time when the issue was created. Available only for admins and project owners.'
+ optional :merge_request_for_resolving_discussions, type: Integer,
+ desc: 'The IID of a merge request for which to resolve discussions'
+ use :issue_params
+ end
+ post ':id/issues' do
+ # Setting created_at time only allowed for admins and project owners
+ unless current_user.admin? || user_project.owner == current_user
+ params.delete(:created_at)
+ end
+
+ issue_params = declared_params(include_missing: false)
+
+ if merge_request_iid = params[:merge_request_for_resolving_discussions]
+ issue_params[:merge_request_for_resolving_discussions] = MergeRequestsFinder.new(current_user, project_id: user_project.id).
+ execute.
+ find_by(iid: merge_request_iid)
+ end
+
+ issue = ::Issues::CreateService.new(user_project,
+ current_user,
+ issue_params.merge(request: request, api: true)).execute
+ if issue.spam?
+ render_api_error!({ error: 'Spam detected' }, 400)
+ end
+
+ if issue.valid?
+ present issue, with: ::API::Entities::Issue, current_user: current_user, project: user_project
+ else
+ render_validation_error!(issue)
+ end
+ end
+
+ desc 'Update an existing issue' do
+ success ::API::Entities::Issue
+ end
+ params do
+ requires :issue_id, type: Integer, desc: 'The ID of a project issue'
+ optional :title, type: String, desc: 'The title of an issue'
+ optional :updated_at, type: DateTime,
+ desc: 'Date time when the issue was updated. Available only for admins and project owners.'
+ optional :state_event, type: String, values: %w[reopen close], desc: 'State of the issue'
+ use :issue_params
+ at_least_one_of :title, :description, :assignee_id, :milestone_id,
+ :labels, :created_at, :due_date, :confidential, :state_event
+ end
+ put ':id/issues/:issue_id' do
+ issue = user_project.issues.find(params.delete(:issue_id))
+ authorize! :update_issue, issue
+
+ # Setting created_at time only allowed for admins and project owners
+ unless current_user.admin? || user_project.owner == current_user
+ params.delete(:updated_at)
+ end
+
+ issue = ::Issues::UpdateService.new(user_project,
+ current_user,
+ declared_params(include_missing: false)).execute(issue)
+
+ if issue.valid?
+ present issue, with: ::API::Entities::Issue, current_user: current_user, project: user_project
+ else
+ render_validation_error!(issue)
+ end
+ end
+
+ desc 'Move an existing issue' do
+ success ::API::Entities::Issue
+ end
+ params do
+ requires :issue_id, type: Integer, desc: 'The ID of a project issue'
+ requires :to_project_id, type: Integer, desc: 'The ID of the new project'
+ end
+ post ':id/issues/:issue_id/move' do
+ issue = user_project.issues.find_by(id: params[:issue_id])
+ not_found!('Issue') unless issue
+
+ new_project = Project.find_by(id: params[:to_project_id])
+ not_found!('Project') unless new_project
+
+ begin
+ issue = ::Issues::MoveService.new(user_project, current_user).execute(issue, new_project)
+ present issue, with: ::API::Entities::Issue, current_user: current_user, project: user_project
+ rescue ::Issues::MoveService::MoveError => error
+ render_api_error!(error.message, 400)
+ end
+ end
+
+ desc 'Delete a project issue'
+ params do
+ requires :issue_id, type: Integer, desc: 'The ID of a project issue'
+ end
+ delete ":id/issues/:issue_id" do
+ issue = user_project.issues.find_by(id: params[:issue_id])
+ not_found!('Issue') unless issue
+
+ authorize!(:destroy_issue, issue)
+ issue.destroy
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/v3/merge_requests.rb b/lib/api/v3/merge_requests.rb
new file mode 100644
index 00000000000..129f9d850e9
--- /dev/null
+++ b/lib/api/v3/merge_requests.rb
@@ -0,0 +1,280 @@
+module API
+ module V3
+ class MergeRequests < Grape::API
+ include PaginationParams
+
+ DEPRECATION_MESSAGE = 'This endpoint is deprecated and has been removed on V4'.freeze
+
+ before { authenticate! }
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+ resource :projects do
+ include TimeTrackingEndpoints
+
+ helpers do
+ def handle_merge_request_errors!(errors)
+ if errors[:project_access].any?
+ error!(errors[:project_access], 422)
+ elsif errors[:branch_conflict].any?
+ error!(errors[:branch_conflict], 422)
+ elsif errors[:validate_fork].any?
+ error!(errors[:validate_fork], 422)
+ elsif errors[:validate_branches].any?
+ conflict!(errors[:validate_branches])
+ end
+
+ render_api_error!(errors, 400)
+ end
+
+ params :optional_params do
+ optional :description, type: String, desc: 'The description of the merge request'
+ optional :assignee_id, type: Integer, desc: 'The ID of a user to assign the merge request'
+ optional :milestone_id, type: Integer, desc: 'The ID of a milestone to assign the merge request'
+ optional :labels, type: String, desc: 'Comma-separated list of label names'
+ optional :remove_source_branch, type: Boolean, desc: 'Remove source branch when merging'
+ end
+ end
+
+ desc 'List merge requests' do
+ detail 'iid filter is deprecated have been removed on V4'
+ success ::API::Entities::MergeRequest
+ end
+ params do
+ optional :state, type: String, values: %w[opened closed merged all], default: 'all',
+ desc: 'Return opened, closed, merged, or all merge requests'
+ optional :order_by, type: String, values: %w[created_at updated_at], default: 'created_at',
+ desc: 'Return merge requests ordered by `created_at` or `updated_at` fields.'
+ optional :sort, type: String, values: %w[asc desc], default: 'desc',
+ desc: 'Return merge requests sorted in `asc` or `desc` order.'
+ optional :iid, type: Array[Integer], desc: 'The IID of the merge requests'
+ use :pagination
+ end
+ get ":id/merge_requests" do
+ authorize! :read_merge_request, user_project
+
+ merge_requests = user_project.merge_requests.inc_notes_with_associations
+ merge_requests = filter_by_iid(merge_requests, params[:iid]) if params[:iid].present?
+
+ merge_requests =
+ case params[:state]
+ when 'opened' then merge_requests.opened
+ when 'closed' then merge_requests.closed
+ when 'merged' then merge_requests.merged
+ else merge_requests
+ end
+
+ merge_requests = merge_requests.reorder(params[:order_by] => params[:sort])
+ present paginate(merge_requests), with: ::API::Entities::MergeRequest, current_user: current_user, project: user_project
+ end
+
+ desc 'Create a merge request' do
+ success ::API::Entities::MergeRequest
+ end
+ params do
+ requires :title, type: String, desc: 'The title of the merge request'
+ requires :source_branch, type: String, desc: 'The source branch'
+ requires :target_branch, type: String, desc: 'The target branch'
+ optional :target_project_id, type: Integer,
+ desc: 'The target project of the merge request defaults to the :id of the project'
+ use :optional_params
+ end
+ post ":id/merge_requests" do
+ authorize! :create_merge_request, user_project
+
+ mr_params = declared_params(include_missing: false)
+ mr_params[:force_remove_source_branch] = mr_params.delete(:remove_source_branch) if mr_params[:remove_source_branch].present?
+
+ merge_request = ::MergeRequests::CreateService.new(user_project, current_user, mr_params).execute
+
+ if merge_request.valid?
+ present merge_request, with: ::API::Entities::MergeRequest, current_user: current_user, project: user_project
+ else
+ handle_merge_request_errors! merge_request.errors
+ end
+ end
+
+ desc 'Delete a merge request'
+ params do
+ requires :merge_request_id, type: Integer, desc: 'The ID of a merge request'
+ end
+ delete ":id/merge_requests/:merge_request_id" do
+ merge_request = find_project_merge_request(params[:merge_request_id])
+
+ authorize!(:destroy_merge_request, merge_request)
+ merge_request.destroy
+ end
+
+ params do
+ requires :merge_request_id, type: Integer, desc: 'The ID of a merge request'
+ end
+ { ":id/merge_request/:merge_request_id" => :deprecated, ":id/merge_requests/:merge_request_id" => :ok }.each do |path, status|
+ desc 'Get a single merge request' do
+ if status == :deprecated
+ detail DEPRECATION_MESSAGE
+ end
+ success ::API::Entities::MergeRequest
+ end
+ get path do
+ merge_request = find_merge_request_with_access(params[:merge_request_id])
+
+ present merge_request, with: ::API::Entities::MergeRequest, current_user: current_user, project: user_project
+ end
+
+ desc 'Get the commits of a merge request' do
+ success ::API::Entities::RepoCommit
+ end
+ get "#{path}/commits" do
+ merge_request = find_merge_request_with_access(params[:merge_request_id])
+
+ present merge_request.commits, with: ::API::Entities::RepoCommit
+ end
+
+ desc 'Show the merge request changes' do
+ success ::API::Entities::MergeRequestChanges
+ end
+ get "#{path}/changes" do
+ merge_request = find_merge_request_with_access(params[:merge_request_id])
+
+ present merge_request, with: ::API::Entities::MergeRequestChanges, current_user: current_user
+ end
+
+ desc 'Update a merge request' do
+ success ::API::Entities::MergeRequest
+ end
+ params do
+ optional :title, type: String, allow_blank: false, desc: 'The title of the merge request'
+ optional :target_branch, type: String, allow_blank: false, desc: 'The target branch'
+ optional :state_event, type: String, values: %w[close reopen merge],
+ desc: 'Status of the merge request'
+ use :optional_params
+ at_least_one_of :title, :target_branch, :description, :assignee_id,
+ :milestone_id, :labels, :state_event,
+ :remove_source_branch
+ end
+ put path do
+ merge_request = find_merge_request_with_access(params.delete(:merge_request_id), :update_merge_request)
+
+ mr_params = declared_params(include_missing: false)
+ mr_params[:force_remove_source_branch] = mr_params.delete(:remove_source_branch) if mr_params[:remove_source_branch].present?
+
+ merge_request = ::MergeRequests::UpdateService.new(user_project, current_user, mr_params).execute(merge_request)
+
+ if merge_request.valid?
+ present merge_request, with: ::API::Entities::MergeRequest, current_user: current_user, project: user_project
+ else
+ handle_merge_request_errors! merge_request.errors
+ end
+ end
+
+ desc 'Merge a merge request' do
+ success ::API::Entities::MergeRequest
+ end
+ params do
+ optional :merge_commit_message, type: String, desc: 'Custom merge commit message'
+ optional :should_remove_source_branch, type: Boolean,
+ desc: 'When true, the source branch will be deleted if possible'
+ optional :merge_when_build_succeeds, type: Boolean,
+ desc: 'When true, this merge request will be merged when the pipeline succeeds'
+ optional :sha, type: String, desc: 'When present, must have the HEAD SHA of the source branch'
+ end
+ put "#{path}/merge" do
+ merge_request = find_project_merge_request(params[:merge_request_id])
+
+ # Merge request can not be merged
+ # because user dont have permissions to push into target branch
+ unauthorized! unless merge_request.can_be_merged_by?(current_user)
+
+ not_allowed! unless merge_request.mergeable_state?
+
+ render_api_error!('Branch cannot be merged', 406) unless merge_request.mergeable?
+
+ if params[:sha] && merge_request.diff_head_sha != params[:sha]
+ render_api_error!("SHA does not match HEAD of source branch: #{merge_request.diff_head_sha}", 409)
+ end
+
+ merge_params = {
+ commit_message: params[:merge_commit_message],
+ should_remove_source_branch: params[:should_remove_source_branch]
+ }
+
+ if params[:merge_when_build_succeeds] && merge_request.head_pipeline && merge_request.head_pipeline.active?
+ ::MergeRequests::MergeWhenPipelineSucceedsService
+ .new(merge_request.target_project, current_user, merge_params)
+ .execute(merge_request)
+ else
+ ::MergeRequests::MergeService
+ .new(merge_request.target_project, current_user, merge_params)
+ .execute(merge_request)
+ end
+
+ present merge_request, with: ::API::Entities::MergeRequest, current_user: current_user, project: user_project
+ end
+
+ desc 'Cancel merge if "Merge When Pipeline Succeeds" is enabled' do
+ success ::API::Entities::MergeRequest
+ end
+ post "#{path}/cancel_merge_when_build_succeeds" do
+ merge_request = find_project_merge_request(params[:merge_request_id])
+
+ unauthorized! unless merge_request.can_cancel_merge_when_build_succeeds?(current_user)
+
+ ::MergeRequest::MergeWhenPipelineSucceedsService
+ .new(merge_request.target_project, current_user)
+ .cancel(merge_request)
+ end
+
+ desc 'Get the comments of a merge request' do
+ detail 'Duplicate. DEPRECATED and HAS BEEN REMOVED in V4'
+ success ::API::Entities::MRNote
+ end
+ params do
+ use :pagination
+ end
+ get "#{path}/comments" do
+ merge_request = find_merge_request_with_access(params[:merge_request_id])
+ present paginate(merge_request.notes.fresh), with: ::API::Entities::MRNote
+ end
+
+ desc 'Post a comment to a merge request' do
+ detail 'Duplicate. DEPRECATED and HAS BEEN REMOVED in V4'
+ success ::API::Entities::MRNote
+ end
+ params do
+ requires :note, type: String, desc: 'The text of the comment'
+ end
+ post "#{path}/comments" do
+ merge_request = find_merge_request_with_access(params[:merge_request_id], :create_note)
+
+ opts = {
+ note: params[:note],
+ noteable_type: 'MergeRequest',
+ noteable_id: merge_request.id
+ }
+
+ note = ::Notes::CreateService.new(user_project, current_user, opts).execute
+
+ if note.save
+ present note, with: ::API::Entities::MRNote
+ else
+ render_api_error!("Failed to save note #{note.errors.messages}", 400)
+ end
+ end
+
+ desc 'List issues that will be closed on merge' do
+ success ::API::Entities::MRNote
+ end
+ params do
+ use :pagination
+ end
+ get "#{path}/closes_issues" do
+ merge_request = find_merge_request_with_access(params[:merge_request_id])
+ issues = ::Kaminari.paginate_array(merge_request.closes_issues(current_user))
+ present paginate(issues), with: issue_entity(user_project), current_user: current_user
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/v3/project_snippets.rb b/lib/api/v3/project_snippets.rb
new file mode 100644
index 00000000000..9f95d4395fa
--- /dev/null
+++ b/lib/api/v3/project_snippets.rb
@@ -0,0 +1,135 @@
+module API
+ module V3
+ class ProjectSnippets < Grape::API
+ include PaginationParams
+
+ before { authenticate! }
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+ resource :projects do
+ helpers do
+ def handle_project_member_errors(errors)
+ if errors[:project_access].any?
+ error!(errors[:project_access], 422)
+ end
+ not_found!
+ end
+
+ def snippets_for_current_user
+ finder_params = { filter: :by_project, project: user_project }
+ SnippetsFinder.new.execute(current_user, finder_params)
+ end
+ end
+
+ desc 'Get all project snippets' do
+ success ::API::V3::Entities::ProjectSnippet
+ end
+ params do
+ use :pagination
+ end
+ get ":id/snippets" do
+ present paginate(snippets_for_current_user), with: ::API::V3::Entities::ProjectSnippet
+ end
+
+ desc 'Get a single project snippet' do
+ success ::API::V3::Entities::ProjectSnippet
+ end
+ params do
+ requires :snippet_id, type: Integer, desc: 'The ID of a project snippet'
+ end
+ get ":id/snippets/:snippet_id" do
+ snippet = snippets_for_current_user.find(params[:snippet_id])
+ present snippet, with: ::API::V3::Entities::ProjectSnippet
+ end
+
+ desc 'Create a new project snippet' do
+ success ::API::V3::Entities::ProjectSnippet
+ end
+ params do
+ requires :title, type: String, desc: 'The title of the snippet'
+ requires :file_name, type: String, desc: 'The file name of the snippet'
+ requires :code, type: String, desc: 'The content of the snippet'
+ requires :visibility_level, type: Integer,
+ values: [Gitlab::VisibilityLevel::PRIVATE,
+ Gitlab::VisibilityLevel::INTERNAL,
+ Gitlab::VisibilityLevel::PUBLIC],
+ desc: 'The visibility level of the snippet'
+ end
+ post ":id/snippets" do
+ authorize! :create_project_snippet, user_project
+ snippet_params = declared_params.merge(request: request, api: true)
+ snippet_params[:content] = snippet_params.delete(:code)
+
+ snippet = CreateSnippetService.new(user_project, current_user, snippet_params).execute
+
+ if snippet.persisted?
+ present snippet, with: ::API::V3::Entities::ProjectSnippet
+ else
+ render_validation_error!(snippet)
+ end
+ end
+
+ desc 'Update an existing project snippet' do
+ success ::API::V3::Entities::ProjectSnippet
+ end
+ params do
+ requires :snippet_id, type: Integer, desc: 'The ID of a project snippet'
+ optional :title, type: String, desc: 'The title of the snippet'
+ optional :file_name, type: String, desc: 'The file name of the snippet'
+ optional :code, type: String, desc: 'The content of the snippet'
+ optional :visibility_level, type: Integer,
+ values: [Gitlab::VisibilityLevel::PRIVATE,
+ Gitlab::VisibilityLevel::INTERNAL,
+ Gitlab::VisibilityLevel::PUBLIC],
+ desc: 'The visibility level of the snippet'
+ at_least_one_of :title, :file_name, :code, :visibility_level
+ end
+ put ":id/snippets/:snippet_id" do
+ snippet = snippets_for_current_user.find_by(id: params.delete(:snippet_id))
+ not_found!('Snippet') unless snippet
+
+ authorize! :update_project_snippet, snippet
+
+ snippet_params = declared_params(include_missing: false)
+ snippet_params[:content] = snippet_params.delete(:code) if snippet_params[:code].present?
+
+ UpdateSnippetService.new(user_project, current_user, snippet,
+ snippet_params).execute
+
+ if snippet.persisted?
+ present snippet, with: ::API::V3::Entities::ProjectSnippet
+ else
+ render_validation_error!(snippet)
+ end
+ end
+
+ desc 'Delete a project snippet'
+ params do
+ requires :snippet_id, type: Integer, desc: 'The ID of a project snippet'
+ end
+ delete ":id/snippets/:snippet_id" do
+ snippet = snippets_for_current_user.find_by(id: params[:snippet_id])
+ not_found!('Snippet') unless snippet
+
+ authorize! :admin_project_snippet, snippet
+ snippet.destroy
+ end
+
+ desc 'Get a raw project snippet'
+ params do
+ requires :snippet_id, type: Integer, desc: 'The ID of a project snippet'
+ end
+ get ":id/snippets/:snippet_id/raw" do
+ snippet = snippets_for_current_user.find_by(id: params[:snippet_id])
+ not_found!('Snippet') unless snippet
+
+ env['api.format'] = :txt
+ content_type 'text/plain'
+ present snippet.content
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/v3/projects.rb b/lib/api/v3/projects.rb
new file mode 100644
index 00000000000..6796da83f07
--- /dev/null
+++ b/lib/api/v3/projects.rb
@@ -0,0 +1,458 @@
+module API
+ module V3
+ class Projects < Grape::API
+ include PaginationParams
+
+ before { authenticate_non_get! }
+
+ helpers do
+ params :optional_params do
+ optional :description, type: String, desc: 'The description of the project'
+ optional :issues_enabled, type: Boolean, desc: 'Flag indication if the issue tracker is enabled'
+ optional :merge_requests_enabled, type: Boolean, desc: 'Flag indication if merge requests are enabled'
+ optional :wiki_enabled, type: Boolean, desc: 'Flag indication if the wiki is enabled'
+ optional :builds_enabled, type: Boolean, desc: 'Flag indication if builds are enabled'
+ optional :snippets_enabled, type: Boolean, desc: 'Flag indication if snippets are enabled'
+ optional :shared_runners_enabled, type: Boolean, desc: 'Flag indication if shared runners are enabled for that project'
+ optional :container_registry_enabled, type: Boolean, desc: 'Flag indication if the container registry is enabled for that project'
+ optional :lfs_enabled, type: Boolean, desc: 'Flag indication if Git LFS is enabled for that project'
+ optional :public, type: Boolean, desc: 'Create a public project. The same as visibility_level = 20.'
+ optional :visibility_level, type: Integer, values: [
+ Gitlab::VisibilityLevel::PRIVATE,
+ Gitlab::VisibilityLevel::INTERNAL,
+ Gitlab::VisibilityLevel::PUBLIC ], desc: 'Create a public project. The same as visibility_level = 20.'
+ optional :public_builds, type: Boolean, desc: 'Perform public builds'
+ optional :request_access_enabled, type: Boolean, desc: 'Allow users to request member access'
+ optional :only_allow_merge_if_build_succeeds, type: Boolean, desc: 'Only allow to merge if builds succeed'
+ optional :only_allow_merge_if_all_discussions_are_resolved, type: Boolean, desc: 'Only allow to merge if all discussions are resolved'
+ end
+
+ def map_public_to_visibility_level(attrs)
+ publik = attrs.delete(:public)
+ if !publik.nil? && !attrs[:visibility_level].present?
+ # Since setting the public attribute to private could mean either
+ # private or internal, use the more conservative option, private.
+ attrs[:visibility_level] = (publik == true) ? Gitlab::VisibilityLevel::PUBLIC : Gitlab::VisibilityLevel::PRIVATE
+ end
+ attrs
+ end
+ end
+
+ resource :projects do
+ helpers do
+ params :collection_params do
+ use :sort_params
+ use :filter_params
+ use :pagination
+
+ optional :simple, type: Boolean, default: false,
+ desc: 'Return only the ID, URL, name, and path of each project'
+ end
+
+ params :sort_params do
+ optional :order_by, type: String, values: %w[id name path created_at updated_at last_activity_at],
+ default: 'created_at', desc: 'Return projects ordered by field'
+ optional :sort, type: String, values: %w[asc desc], default: 'desc',
+ desc: 'Return projects sorted in ascending and descending order'
+ end
+
+ params :filter_params do
+ optional :archived, type: Boolean, default: false, desc: 'Limit by archived status'
+ optional :visibility, type: String, values: %w[public internal private],
+ desc: 'Limit by visibility'
+ optional :search, type: String, desc: 'Return list of authorized projects matching the search criteria'
+ end
+
+ params :statistics_params do
+ optional :statistics, type: Boolean, default: false, desc: 'Include project statistics'
+ end
+
+ params :create_params do
+ optional :namespace_id, type: Integer, desc: 'Namespace ID for the new project. Default to the user namespace.'
+ optional :import_url, type: String, desc: 'URL from which the project is imported'
+ end
+
+ def present_projects(projects, options = {})
+ options = options.reverse_merge(
+ with: ::API::Entities::Project,
+ current_user: current_user,
+ simple: params[:simple],
+ )
+
+ projects = filter_projects(projects)
+ projects = projects.with_statistics if options[:statistics]
+ options[:with] = ::API::Entities::BasicProjectDetails if options[:simple]
+
+ present paginate(projects), options
+ end
+ end
+
+ desc 'Get a list of visible projects for authenticated user' do
+ success ::API::Entities::BasicProjectDetails
+ end
+ params do
+ use :collection_params
+ end
+ get '/visible' do
+ entity = current_user ? ::API::Entities::ProjectWithAccess : ::API::Entities::BasicProjectDetails
+ present_projects ProjectsFinder.new.execute(current_user), with: entity
+ end
+
+ desc 'Get a projects list for authenticated user' do
+ success ::API::Entities::BasicProjectDetails
+ end
+ params do
+ use :collection_params
+ end
+ get do
+ authenticate!
+
+ present_projects current_user.authorized_projects,
+ with: ::API::Entities::ProjectWithAccess
+ end
+
+ desc 'Get an owned projects list for authenticated user' do
+ success ::API::Entities::BasicProjectDetails
+ end
+ params do
+ use :collection_params
+ use :statistics_params
+ end
+ get '/owned' do
+ authenticate!
+
+ present_projects current_user.owned_projects,
+ with: ::API::Entities::ProjectWithAccess,
+ statistics: params[:statistics]
+ end
+
+ desc 'Gets starred project for the authenticated user' do
+ success ::API::Entities::BasicProjectDetails
+ end
+ params do
+ use :collection_params
+ end
+ get '/starred' do
+ authenticate!
+
+ present_projects current_user.viewable_starred_projects
+ end
+
+ desc 'Get all projects for admin user' do
+ success ::API::Entities::BasicProjectDetails
+ end
+ params do
+ use :collection_params
+ use :statistics_params
+ end
+ get '/all' do
+ authenticated_as_admin!
+
+ present_projects Project.all, with: ::API::Entities::ProjectWithAccess, statistics: params[:statistics]
+ end
+
+ desc 'Search for projects the current user has access to' do
+ success ::API::Entities::Project
+ end
+ params do
+ requires :query, type: String, desc: 'The project name to be searched'
+ use :sort_params
+ use :pagination
+ end
+ get "/search/:query", requirements: { query: /[^\/]+/ } do
+ search_service = Search::GlobalService.new(current_user, search: params[:query]).execute
+ projects = search_service.objects('projects', params[:page])
+ projects = projects.reorder(params[:order_by] => params[:sort])
+
+ present paginate(projects), with: ::API::Entities::Project
+ end
+
+ desc 'Create new project' do
+ success ::API::Entities::Project
+ end
+ params do
+ requires :name, type: String, desc: 'The name of the project'
+ optional :path, type: String, desc: 'The path of the repository'
+ use :optional_params
+ use :create_params
+ end
+ post do
+ attrs = map_public_to_visibility_level(declared_params(include_missing: false))
+ project = ::Projects::CreateService.new(current_user, attrs).execute
+
+ if project.saved?
+ present project, with: ::API::Entities::Project,
+ user_can_admin_project: can?(current_user, :admin_project, project)
+ else
+ if project.errors[:limit_reached].present?
+ error!(project.errors[:limit_reached], 403)
+ end
+ render_validation_error!(project)
+ end
+ end
+
+ desc 'Create new project for a specified user. Only available to admin users.' do
+ success ::API::Entities::Project
+ end
+ params do
+ requires :name, type: String, desc: 'The name of the project'
+ requires :user_id, type: Integer, desc: 'The ID of a user'
+ optional :default_branch, type: String, desc: 'The default branch of the project'
+ use :optional_params
+ use :create_params
+ end
+ post "user/:user_id" do
+ authenticated_as_admin!
+ user = User.find_by(id: params.delete(:user_id))
+ not_found!('User') unless user
+
+ attrs = map_public_to_visibility_level(declared_params(include_missing: false))
+ project = ::Projects::CreateService.new(user, attrs).execute
+
+ if project.saved?
+ present project, with: ::API::Entities::Project,
+ user_can_admin_project: can?(current_user, :admin_project, project)
+ else
+ render_validation_error!(project)
+ end
+ end
+ end
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+ resource :projects, requirements: { id: /[^\/]+/ } do
+ desc 'Get a single project' do
+ success ::API::Entities::ProjectWithAccess
+ end
+ get ":id" do
+ entity = current_user ? ::API::Entities::ProjectWithAccess : ::API::Entities::BasicProjectDetails
+ present user_project, with: entity, current_user: current_user,
+ user_can_admin_project: can?(current_user, :admin_project, user_project)
+ end
+
+ desc 'Get events for a single project' do
+ success ::API::Entities::Event
+ end
+ params do
+ use :pagination
+ end
+ get ":id/events" do
+ present paginate(user_project.events.recent), with: ::API::Entities::Event
+ end
+
+ desc 'Fork new project for the current user or provided namespace.' do
+ success ::API::Entities::Project
+ end
+ params do
+ optional :namespace, type: String, desc: 'The ID or name of the namespace that the project will be forked into'
+ end
+ post 'fork/:id' do
+ fork_params = declared_params(include_missing: false)
+ namespace_id = fork_params[:namespace]
+
+ if namespace_id.present?
+ fork_params[:namespace] = if namespace_id =~ /^\d+$/
+ Namespace.find_by(id: namespace_id)
+ else
+ Namespace.find_by_path_or_name(namespace_id)
+ end
+
+ unless fork_params[:namespace] && can?(current_user, :create_projects, fork_params[:namespace])
+ not_found!('Target Namespace')
+ end
+ end
+
+ forked_project = ::Projects::ForkService.new(user_project, current_user, fork_params).execute
+
+ if forked_project.errors.any?
+ conflict!(forked_project.errors.messages)
+ else
+ present forked_project, with: ::API::Entities::Project,
+ user_can_admin_project: can?(current_user, :admin_project, forked_project)
+ end
+ end
+
+ desc 'Update an existing project' do
+ success ::API::Entities::Project
+ end
+ params do
+ optional :name, type: String, desc: 'The name of the project'
+ optional :default_branch, type: String, desc: 'The default branch of the project'
+ optional :path, type: String, desc: 'The path of the repository'
+ use :optional_params
+ at_least_one_of :name, :description, :issues_enabled, :merge_requests_enabled,
+ :wiki_enabled, :builds_enabled, :snippets_enabled,
+ :shared_runners_enabled, :container_registry_enabled,
+ :lfs_enabled, :public, :visibility_level, :public_builds,
+ :request_access_enabled, :only_allow_merge_if_build_succeeds,
+ :only_allow_merge_if_all_discussions_are_resolved, :path,
+ :default_branch
+ end
+ put ':id' do
+ authorize_admin_project
+ attrs = map_public_to_visibility_level(declared_params(include_missing: false))
+ authorize! :rename_project, user_project if attrs[:name].present?
+ authorize! :change_visibility_level, user_project if attrs[:visibility_level].present?
+
+ result = ::Projects::UpdateService.new(user_project, current_user, attrs).execute
+
+ if result[:status] == :success
+ present user_project, with: ::API::Entities::Project,
+ user_can_admin_project: can?(current_user, :admin_project, user_project)
+ else
+ render_validation_error!(user_project)
+ end
+ end
+
+ desc 'Archive a project' do
+ success ::API::Entities::Project
+ end
+ post ':id/archive' do
+ authorize!(:archive_project, user_project)
+
+ user_project.archive!
+
+ present user_project, with: ::API::Entities::Project
+ end
+
+ desc 'Unarchive a project' do
+ success ::API::Entities::Project
+ end
+ post ':id/unarchive' do
+ authorize!(:archive_project, user_project)
+
+ user_project.unarchive!
+
+ present user_project, with: ::API::Entities::Project
+ end
+
+ desc 'Star a project' do
+ success ::API::Entities::Project
+ end
+ post ':id/star' do
+ if current_user.starred?(user_project)
+ not_modified!
+ else
+ current_user.toggle_star(user_project)
+ user_project.reload
+
+ present user_project, with: ::API::Entities::Project
+ end
+ end
+
+ desc 'Unstar a project' do
+ success ::API::Entities::Project
+ end
+ delete ':id/star' do
+ if current_user.starred?(user_project)
+ current_user.toggle_star(user_project)
+ user_project.reload
+
+ present user_project, with: ::API::Entities::Project
+ else
+ not_modified!
+ end
+ end
+
+ desc 'Remove a project'
+ delete ":id" do
+ authorize! :remove_project, user_project
+ ::Projects::DestroyService.new(user_project, current_user, {}).async_execute
+ end
+
+ desc 'Mark this project as forked from another'
+ params do
+ requires :forked_from_id, type: String, desc: 'The ID of the project it was forked from'
+ end
+ post ":id/fork/:forked_from_id" do
+ authenticated_as_admin!
+
+ forked_from_project = find_project!(params[:forked_from_id])
+ not_found!("Source Project") unless forked_from_project
+
+ if user_project.forked_from_project.nil?
+ user_project.create_forked_project_link(forked_to_project_id: user_project.id, forked_from_project_id: forked_from_project.id)
+ else
+ render_api_error!("Project already forked", 409)
+ end
+ end
+
+ desc 'Remove a forked_from relationship'
+ delete ":id/fork" do
+ authorize! :remove_fork_project, user_project
+
+ if user_project.forked?
+ user_project.forked_project_link.destroy
+ else
+ not_modified!
+ end
+ end
+
+ desc 'Share the project with a group' do
+ success ::API::Entities::ProjectGroupLink
+ end
+ params do
+ requires :group_id, type: Integer, desc: 'The ID of a group'
+ requires :group_access, type: Integer, values: Gitlab::Access.values, desc: 'The group access level'
+ optional :expires_at, type: Date, desc: 'Share expiration date'
+ end
+ post ":id/share" do
+ authorize! :admin_project, user_project
+ group = Group.find_by_id(params[:group_id])
+
+ unless group && can?(current_user, :read_group, group)
+ not_found!('Group')
+ end
+
+ unless user_project.allowed_to_share_with_group?
+ return render_api_error!("The project sharing with group is disabled", 400)
+ end
+
+ link = user_project.project_group_links.new(declared_params(include_missing: false))
+
+ if link.save
+ present link, with: ::API::Entities::ProjectGroupLink
+ else
+ render_api_error!(link.errors.full_messages.first, 409)
+ end
+ end
+
+ params do
+ requires :group_id, type: Integer, desc: 'The ID of the group'
+ end
+ delete ":id/share/:group_id" do
+ authorize! :admin_project, user_project
+
+ link = user_project.project_group_links.find_by(group_id: params[:group_id])
+ not_found!('Group Link') unless link
+
+ link.destroy
+ no_content!
+ end
+
+ desc 'Upload a file'
+ params do
+ requires :file, type: File, desc: 'The file to be uploaded'
+ end
+ post ":id/uploads" do
+ ::Projects::UploadService.new(user_project, params[:file]).execute
+ end
+
+ desc 'Get the users list of a project' do
+ success ::API::Entities::UserBasic
+ end
+ params do
+ optional :search, type: String, desc: 'Return list of users matching the search criteria'
+ use :pagination
+ end
+ get ':id/users' do
+ users = user_project.team.users
+ users = users.search(params[:search]) if params[:search].present?
+
+ present paginate(users), with: ::API::Entities::UserBasic
+ end
+ end
+ end
+ end
+end
diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb
index 7e6537e3d9e..f099c0651ac 100644
--- a/lib/backup/manager.rb
+++ b/lib/backup/manager.rb
@@ -1,7 +1,8 @@
module Backup
class Manager
- ARCHIVES_TO_BACKUP = %w[uploads builds artifacts lfs registry]
+ ARCHIVES_TO_BACKUP = %w[uploads builds artifacts pages lfs registry]
FOLDERS_TO_BACKUP = %w[repositories db]
+ FILE_NAME_SUFFIX = '_gitlab_backup.tar'
def pack
# Make sure there is a connection
@@ -14,7 +15,7 @@ module Backup
s[:gitlab_version] = Gitlab::VERSION
s[:tar_version] = tar_version
s[:skipped] = ENV["SKIP"]
- tar_file = s[:backup_created_at].strftime('%s_%Y_%m_%d') + '_gitlab_backup.tar'
+ tar_file = "#{s[:backup_created_at].strftime('%s_%Y_%m_%d')}#{FILE_NAME_SUFFIX}"
Dir.chdir(Gitlab.config.backup.path) do
File.open("#{Gitlab.config.backup.path}/backup_information.yml",
@@ -82,7 +83,7 @@ module Backup
removed = 0
Dir.chdir(Gitlab.config.backup.path) do
- Dir.glob('*_gitlab_backup.tar').each do |file|
+ Dir.glob("*#{FILE_NAME_SUFFIX}").each do |file|
next unless file =~ /(\d+)(?:_\d{4}_\d{2}_\d{2})?_gitlab_backup\.tar/
timestamp = $1.to_i
@@ -108,41 +109,50 @@ module Backup
Dir.chdir(Gitlab.config.backup.path)
# check for existing backups in the backup dir
- file_list = Dir.glob("*_gitlab_backup.tar")
- puts "no backups found" if file_list.count == 0
+ file_list = Dir.glob("*#{FILE_NAME_SUFFIX}")
+
+ if file_list.count == 0
+ $progress.puts "No backups found in #{Gitlab.config.backup.path}"
+ $progress.puts "Please make sure that file name ends with #{FILE_NAME_SUFFIX}"
+ exit 1
+ end
if file_list.count > 1 && ENV["BACKUP"].nil?
- puts "Found more than one backup, please specify which one you want to restore:"
- puts "rake gitlab:backup:restore BACKUP=timestamp_of_backup"
+ $progress.puts 'Found more than one backup, please specify which one you want to restore:'
+ $progress.puts 'rake gitlab:backup:restore BACKUP=timestamp_of_backup'
exit 1
end
- tar_file = ENV["BACKUP"].nil? ? file_list.first : file_list.grep(ENV['BACKUP']).first
+ if ENV['BACKUP'].present?
+ tar_file = "#{ENV['BACKUP']}#{FILE_NAME_SUFFIX}"
+ else
+ tar_file = file_list.first
+ end
unless File.exist?(tar_file)
- puts "The specified backup doesn't exist!"
+ $progress.puts "The backup file #{tar_file} does not exist!"
exit 1
end
- $progress.print "Unpacking backup ... "
+ $progress.print 'Unpacking backup ... '
unless Kernel.system(*%W(tar -xf #{tar_file}))
- puts "unpacking backup failed".color(:red)
+ $progress.puts 'unpacking backup failed'.color(:red)
exit 1
else
- $progress.puts "done".color(:green)
+ $progress.puts 'done'.color(:green)
end
ENV["VERSION"] = "#{settings[:db_version]}" if settings[:db_version].to_i > 0
# restoring mismatching backups can lead to unexpected problems
if settings[:gitlab_version] != Gitlab::VERSION
- puts "GitLab version mismatch:".color(:red)
- puts " Your current GitLab version (#{Gitlab::VERSION}) differs from the GitLab version in the backup!".color(:red)
- puts " Please switch to the following version and try again:".color(:red)
- puts " version: #{settings[:gitlab_version]}".color(:red)
- puts
- puts "Hint: git checkout v#{settings[:gitlab_version]}"
+ $progress.puts 'GitLab version mismatch:'.color(:red)
+ $progress.puts " Your current GitLab version (#{Gitlab::VERSION}) differs from the GitLab version in the backup!".color(:red)
+ $progress.puts ' Please switch to the following version and try again:'.color(:red)
+ $progress.puts " version: #{settings[:gitlab_version]}".color(:red)
+ $progress.puts
+ $progress.puts "Hint: git checkout v#{settings[:gitlab_version]}"
exit 1
end
end
diff --git a/lib/backup/pages.rb b/lib/backup/pages.rb
new file mode 100644
index 00000000000..215ded93bfe
--- /dev/null
+++ b/lib/backup/pages.rb
@@ -0,0 +1,13 @@
+require 'backup/files'
+
+module Backup
+ class Pages < Files
+ def initialize
+ super('pages', Gitlab.config.pages.path)
+ end
+
+ def create_files_dir
+ Dir.mkdir(app_files_dir, 0700)
+ end
+ end
+end
diff --git a/lib/banzai/cross_project_reference.rb b/lib/banzai/cross_project_reference.rb
index 0257848b6bc..e2b57adf611 100644
--- a/lib/banzai/cross_project_reference.rb
+++ b/lib/banzai/cross_project_reference.rb
@@ -14,7 +14,7 @@ module Banzai
def project_from_ref(ref)
return context[:project] unless ref
- Project.find_with_namespace(ref)
+ Project.find_by_full_path(ref)
end
end
end
diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb
index fd74eeaebe7..a3d495a5da0 100644
--- a/lib/banzai/filter/abstract_reference_filter.rb
+++ b/lib/banzai/filter/abstract_reference_filter.rb
@@ -153,7 +153,7 @@ module Banzai
title = object_link_title(object)
klass = reference_class(object_sym)
- data = data_attributes_for(link_content || match, project, object)
+ data = data_attributes_for(link_content || match, project, object, link: !!link_content)
if matches.names.include?("url") && matches[:url]
url = matches[:url]
@@ -172,9 +172,10 @@ module Banzai
end
end
- def data_attributes_for(text, project, object)
+ def data_attributes_for(text, project, object, link: false)
data_attribute(
original: text,
+ link: link,
project: project.id,
object_sym => object.id
)
@@ -254,15 +255,26 @@ module Banzai
# Returns projects for the given paths.
def find_projects_for_paths(paths)
if RequestStore.active?
- to_query = paths - project_refs_cache.keys
+ cache = project_refs_cache
+ to_query = paths - cache.keys
unless to_query.empty?
- projects_relation_for_paths(to_query).each do |project|
- get_or_set_cache(project_refs_cache, project.path_with_namespace) { project }
+ projects = projects_relation_for_paths(to_query)
+
+ found = []
+ projects.each do |project|
+ ref = project.path_with_namespace
+ get_or_set_cache(cache, ref) { project }
+ found << ref
+ end
+
+ not_found = to_query - found
+ not_found.each do |ref|
+ get_or_set_cache(cache, ref) { nil }
end
end
- project_refs_cache.slice(*paths).values
+ cache.slice(*paths).values.compact
else
projects_relation_for_paths(paths)
end
diff --git a/lib/banzai/filter/external_link_filter.rb b/lib/banzai/filter/external_link_filter.rb
index 2f19b59e725..d67d466bce8 100644
--- a/lib/banzai/filter/external_link_filter.rb
+++ b/lib/banzai/filter/external_link_filter.rb
@@ -10,7 +10,7 @@ module Banzai
node.set_attribute('href', href)
end
- if href =~ /\Ahttp(s)?:\/\// && external_url?(href)
+ if href =~ %r{\A(https?:)?//[^/]} && external_url?(href)
node.set_attribute('rel', 'nofollow noreferrer')
node.set_attribute('target', '_blank')
end
diff --git a/lib/banzai/filter/issue_reference_filter.rb b/lib/banzai/filter/issue_reference_filter.rb
index 4d1bc687696..fd6b9704132 100644
--- a/lib/banzai/filter/issue_reference_filter.rb
+++ b/lib/banzai/filter/issue_reference_filter.rb
@@ -62,7 +62,7 @@ module Banzai
end
end
- def data_attributes_for(text, project, object)
+ def data_attributes_for(text, project, object, link: false)
if object.is_a?(ExternalIssue)
data_attribute(
project: project.id,
diff --git a/lib/banzai/filter/math_filter.rb b/lib/banzai/filter/math_filter.rb
index cb037f89337..b6e784c886b 100644
--- a/lib/banzai/filter/math_filter.rb
+++ b/lib/banzai/filter/math_filter.rb
@@ -5,12 +5,6 @@ module Banzai
# HTML filter that adds class="code math" and removes the dollar sign in $`2+2`$.
#
class MathFilter < HTML::Pipeline::Filter
- # This picks out <code>...</code>.
- INLINE_MATH = 'descendant-or-self::code'.freeze
-
- # Pick out a code block which is declared math
- DISPLAY_MATH = "descendant-or-self::pre[contains(@class, 'math') and contains(@class, 'code')]".freeze
-
# Attribute indicating inline or display math.
STYLE_ATTRIBUTE = 'data-math-style'.freeze
@@ -22,13 +16,14 @@ module Banzai
DOLLAR_SIGN = '$'.freeze
def call
- doc.xpath(INLINE_MATH).each do |code|
+ doc.css('code').each do |code|
closing = code.next
opening = code.previous
# We need a sibling before and after.
# They should end and start with $ respectively.
if closing && opening &&
+ closing.text? && opening.text? &&
closing.content.first == DOLLAR_SIGN &&
opening.content.last == DOLLAR_SIGN
@@ -39,7 +34,7 @@ module Banzai
end
end
- doc.xpath(DISPLAY_MATH).each do |el|
+ doc.css('pre.code.math').each do |el|
el[STYLE_ATTRIBUTE] = 'display'
el[:class] += " #{TAG_CLASS}"
end
diff --git a/lib/banzai/filter/plantuml_filter.rb b/lib/banzai/filter/plantuml_filter.rb
new file mode 100644
index 00000000000..e194cf59275
--- /dev/null
+++ b/lib/banzai/filter/plantuml_filter.rb
@@ -0,0 +1,39 @@
+require "nokogiri"
+require "asciidoctor-plantuml/plantuml"
+
+module Banzai
+ module Filter
+ # HTML that replaces all `code plantuml` tags with PlantUML img tags.
+ #
+ class PlantumlFilter < HTML::Pipeline::Filter
+ def call
+ return doc unless doc.at('pre.plantuml') and settings.plantuml_enabled
+
+ plantuml_setup
+
+ doc.css('pre.plantuml').each do |el|
+ img_tag = Nokogiri::HTML::DocumentFragment.parse(
+ Asciidoctor::PlantUml::Processor.plantuml_content(el.content, {}))
+ el.replace img_tag
+ end
+
+ doc
+ end
+
+ private
+
+ def settings
+ ApplicationSetting.current || ApplicationSetting.create_from_defaults
+ end
+
+ def plantuml_setup
+ Asciidoctor::PlantUml.configure do |conf|
+ conf.url = settings.plantuml_url
+ conf.png_enable = settings.plantuml_enabled
+ conf.svg_enable = false
+ conf.txt_enable = false
+ end
+ end
+ end
+ end
+end
diff --git a/lib/banzai/filter/reference_filter.rb b/lib/banzai/filter/reference_filter.rb
index 84bfeac8041..6640168bfa2 100644
--- a/lib/banzai/filter/reference_filter.rb
+++ b/lib/banzai/filter/reference_filter.rb
@@ -20,10 +20,10 @@ module Banzai
# Examples:
#
# data_attribute(project: 1, issue: 2)
- # # => "data-reference-filter=\"SomeReferenceFilter\" data-project=\"1\" data-issue=\"2\""
+ # # => "data-reference-type=\"SomeReferenceFilter\" data-project=\"1\" data-issue=\"2\""
#
# data_attribute(project: 3, merge_request: 4)
- # # => "data-reference-filter=\"SomeReferenceFilter\" data-project=\"3\" data-merge-request=\"4\""
+ # # => "data-reference-type=\"SomeReferenceFilter\" data-project=\"3\" data-merge-request=\"4\""
#
# Returns a String
def data_attribute(attributes = {})
@@ -31,7 +31,9 @@ module Banzai
attributes[:reference_type] ||= self.class.reference_type
attributes.delete(:original) if context[:no_original_data]
- attributes.map { |key, value| %Q(data-#{key.to_s.dasherize}="#{escape_once(value)}") }.join(" ")
+ attributes.map do |key, value|
+ %Q(data-#{key.to_s.dasherize}="#{escape_once(value)}")
+ end.join(' ')
end
def escape_once(html)
@@ -51,6 +53,10 @@ module Banzai
context[:project]
end
+ def skip_project_check?
+ context[:skip_project_check]
+ end
+
def reference_class(type)
"gfm gfm-#{type} has-tooltip"
end
diff --git a/lib/banzai/filter/syntax_highlight_filter.rb b/lib/banzai/filter/syntax_highlight_filter.rb
index 026b81ac175..a447e2b8bff 100644
--- a/lib/banzai/filter/syntax_highlight_filter.rb
+++ b/lib/banzai/filter/syntax_highlight_filter.rb
@@ -20,17 +20,19 @@ module Banzai
code = node.text
css_classes = "code highlight"
lexer = lexer_for(language)
+ lang = lexer.tag
begin
code = format(lex(lexer, code))
- css_classes << " js-syntax-highlight #{lexer.tag}"
+ css_classes << " js-syntax-highlight #{lang}"
rescue
+ lang = nil
# Gracefully handle syntax highlighter bugs/errors to ensure
# users can still access an issue/comment/etc.
end
- highlighted = %(<pre class="#{css_classes}" v-pre="true"><code>#{code}</code></pre>)
+ highlighted = %(<pre class="#{css_classes}" lang="#{lang}" v-pre="true"><code>#{code}</code></pre>)
# Extracted to a method to measure it
replace_parent_pre_element(node, highlighted)
diff --git a/lib/banzai/filter/user_reference_filter.rb b/lib/banzai/filter/user_reference_filter.rb
index f842b1fb779..1aa9355b256 100644
--- a/lib/banzai/filter/user_reference_filter.rb
+++ b/lib/banzai/filter/user_reference_filter.rb
@@ -24,7 +24,7 @@ module Banzai
end
def call
- return doc if project.nil?
+ return doc if project.nil? && !skip_project_check?
ref_pattern = User.reference_pattern
ref_pattern_start = /\A#{ref_pattern}\z/
@@ -58,7 +58,7 @@ module Banzai
# have `gfm` and `gfm-project_member` class names attached for styling.
def user_link_filter(text, link_content: nil)
self.class.references_in(text) do |match, username|
- if username == 'all'
+ if username == 'all' && !skip_project_check?
link_to_all(link_content: link_content)
elsif namespace = namespaces[username]
link_to_namespace(namespace, link_content: link_content) || match
diff --git a/lib/banzai/filter/video_link_filter.rb b/lib/banzai/filter/video_link_filter.rb
index ac7bbcb0d10..b64a1287d4d 100644
--- a/lib/banzai/filter/video_link_filter.rb
+++ b/lib/banzai/filter/video_link_filter.rb
@@ -35,7 +35,8 @@ module Banzai
src: element['src'],
width: '400',
controls: true,
- 'data-setup' => '{}')
+ 'data-setup' => '{}',
+ 'data-title' => element['title'] || element['alt'])
link = doc.document.create_element(
'a',
diff --git a/lib/banzai/pipeline/gfm_pipeline.rb b/lib/banzai/pipeline/gfm_pipeline.rb
index 5a1f873496c..b25d6f18d59 100644
--- a/lib/banzai/pipeline/gfm_pipeline.rb
+++ b/lib/banzai/pipeline/gfm_pipeline.rb
@@ -1,9 +1,16 @@
module Banzai
module Pipeline
class GfmPipeline < BasePipeline
+ # These filters convert GitLab Flavored Markdown (GFM) to HTML.
+ # The handlers defined in app/assets/javascripts/copy_as_gfm.js.es6
+ # consequently convert that same HTML to GFM to be copied to the clipboard.
+ # Every filter that generates HTML from GFM should have a handler in
+ # app/assets/javascripts/copy_as_gfm.js.es6, in reverse order.
+ # The GFM-to-HTML-to-GFM cycle is tested in spec/features/copy_as_gfm_spec.rb.
def self.filters
@filters ||= FilterArray[
Filter::SyntaxHighlightFilter,
+ Filter::PlantumlFilter,
Filter::SanitizationFilter,
Filter::MathFilter,
diff --git a/lib/banzai/renderer.rb b/lib/banzai/renderer.rb
index f31fb6c3f71..74663556cbb 100644
--- a/lib/banzai/renderer.rb
+++ b/lib/banzai/renderer.rb
@@ -52,9 +52,9 @@ module Banzai
end
# Same as +render_field+, but without consulting or updating the cache field
- def cacheless_render_field(object, field)
+ def cacheless_render_field(object, field, options = {})
text = object.__send__(field)
- context = object.banzai_render_context(field)
+ context = object.banzai_render_context(field).merge(options)
cacheless_render(text, context)
end
diff --git a/lib/ci/ansi2html.rb b/lib/ci/ansi2html.rb
index 229050151d3..c10d3616f31 100644
--- a/lib/ci/ansi2html.rb
+++ b/lib/ci/ansi2html.rb
@@ -105,7 +105,7 @@ module Ci
break
elsif s.scan(/</)
@out << '&lt;'
- elsif s.scan(/\n/)
+ elsif s.scan(/\r?\n/)
@out << '<br>'
else
@out << s.scan(/./m)
diff --git a/lib/ci/api/api.rb b/lib/ci/api/api.rb
index a6b9beecded..24bb3649a76 100644
--- a/lib/ci/api/api.rb
+++ b/lib/ci/api/api.rb
@@ -8,6 +8,16 @@ module Ci
rack_response({ 'message' => '404 Not found' }.to_json, 404)
end
+ # Retain 405 error rather than a 500 error for Grape 0.15.0+.
+ # https://github.com/ruby-grape/grape/blob/a3a28f5b5dfbb2797442e006dbffd750b27f2a76/UPGRADING.md#changes-to-method-not-allowed-routes
+ rescue_from Grape::Exceptions::MethodNotAllowed do |e|
+ error! e.message, e.status, e.headers
+ end
+
+ rescue_from Grape::Exceptions::Base do |e|
+ error! e.message, e.status, e.headers
+ end
+
rescue_from :all do |exception|
handle_api_exception(exception)
end
diff --git a/lib/ci/api/builds.rb b/lib/ci/api/builds.rb
index 142bce82286..8b939663ffd 100644
--- a/lib/ci/api/builds.rb
+++ b/lib/ci/api/builds.rb
@@ -16,17 +16,33 @@ module Ci
not_found! unless current_runner.active?
update_runner_info
- build = Ci::RegisterBuildService.new.execute(current_runner)
+ if current_runner.is_runner_queue_value_latest?(params[:last_update])
+ header 'X-GitLab-Last-Update', params[:last_update]
+ Gitlab::Metrics.add_event(:build_not_found_cached)
+ return build_not_found!
+ end
- if build
- Gitlab::Metrics.add_event(:build_found,
- project: build.project.path_with_namespace)
+ new_update = current_runner.ensure_runner_queue_value
- present build, with: Entities::BuildDetails
- else
- Gitlab::Metrics.add_event(:build_not_found)
+ result = Ci::RegisterBuildService.new(current_runner).execute
+
+ if result.valid?
+ if result.build
+ Gitlab::Metrics.add_event(:build_found,
+ project: result.build.project.path_with_namespace)
- build_not_found!
+ present result.build, with: Entities::BuildDetails
+ else
+ Gitlab::Metrics.add_event(:build_not_found)
+
+ header 'X-GitLab-Last-Update', new_update
+
+ build_not_found!
+ end
+ else
+ # We received build that is invalid due to concurrency conflict
+ Gitlab::Metrics.add_event(:build_invalid)
+ conflict!
end
end
diff --git a/lib/ci/api/helpers.rb b/lib/ci/api/helpers.rb
index 31fbd1da108..5ff25a3a9b2 100644
--- a/lib/ci/api/helpers.rb
+++ b/lib/ci/api/helpers.rb
@@ -60,7 +60,7 @@ module Ci
end
def build_not_found!
- if headers['User-Agent'].match(/gitlab-ci-multi-runner \d+\.\d+\.\d+(~beta\.\d+\.g[0-9a-f]+)? /)
+ if headers['User-Agent'].to_s.match(/gitlab-ci-multi-runner \d+\.\d+\.\d+(~beta\.\d+\.g[0-9a-f]+)? /)
no_content!
else
not_found!
diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb
index 7463bd719d5..649ee4d018b 100644
--- a/lib/ci/gitlab_ci_yaml_processor.rb
+++ b/lib/ci/gitlab_ci_yaml_processor.rb
@@ -61,6 +61,7 @@ module Ci
allow_failure: job[:allow_failure] || false,
when: job[:when] || 'on_success',
environment: job[:environment_name],
+ coverage_regex: job[:coverage],
yaml_variables: yaml_variables(name),
options: {
image: job[:image],
diff --git a/lib/constraints/project_url_constrainer.rb b/lib/constraints/project_url_constrainer.rb
index 730b05bed97..a10b4657d7d 100644
--- a/lib/constraints/project_url_constrainer.rb
+++ b/lib/constraints/project_url_constrainer.rb
@@ -8,6 +8,6 @@ class ProjectUrlConstrainer
return false
end
- Project.find_with_namespace(full_path).present?
+ Project.find_by_full_path(full_path).present?
end
end
diff --git a/lib/email_template_interceptor.rb b/lib/email_template_interceptor.rb
index fb04a7824b8..63f9f8d7a5a 100644
--- a/lib/email_template_interceptor.rb
+++ b/lib/email_template_interceptor.rb
@@ -5,8 +5,8 @@ class EmailTemplateInterceptor
def self.delivering_email(message)
# Remove HTML part if HTML emails are disabled.
unless current_application_settings.html_emails_enabled
- message.part.delete_if do |part|
- part.content_type.try(:start_with?, 'text/html')
+ message.parts.delete_if do |part|
+ part.content_type.start_with?('text/html')
end
end
end
diff --git a/lib/gitlab/asciidoc.rb b/lib/gitlab/asciidoc.rb
index fa234284361..0618107e2c3 100644
--- a/lib/gitlab/asciidoc.rb
+++ b/lib/gitlab/asciidoc.rb
@@ -1,5 +1,6 @@
require 'asciidoctor'
require 'asciidoctor/converter/html5'
+require "asciidoctor-plantuml"
module Gitlab
# Parser/renderer for the AsciiDoc format that uses Asciidoctor and filters
@@ -29,6 +30,8 @@ module Gitlab
)
asciidoc_opts[:attributes].unshift(*DEFAULT_ADOC_ATTRS)
+ plantuml_setup
+
html = ::Asciidoctor.convert(input, asciidoc_opts)
html = Banzai.post_process(html, context)
@@ -36,6 +39,15 @@ module Gitlab
html.html_safe
end
+ def self.plantuml_setup
+ Asciidoctor::PlantUml.configure do |conf|
+ conf.url = ApplicationSetting.current.plantuml_url
+ conf.svg_enable = ApplicationSetting.current.plantuml_enabled
+ conf.png_enable = ApplicationSetting.current.plantuml_enabled
+ conf.txt_enable = false
+ end
+ end
+
class Html5Converter < Asciidoctor::Converter::Html5Converter
extend Asciidoctor::Converter::Config
diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb
index 8dda65c71ef..f638905a1e0 100644
--- a/lib/gitlab/auth.rb
+++ b/lib/gitlab/auth.rb
@@ -10,13 +10,16 @@ module Gitlab
def find_for_git_client(login, password, project:, ip:)
raise "Must provide an IP for rate limiting" if ip.nil?
+ # `user_with_password_for_git` should be the last check
+ # because it's the most expensive, especially when LDAP
+ # is enabled.
result =
service_request_check(login, password, project) ||
build_access_token_check(login, password) ||
- user_with_password_for_git(login, password) ||
- oauth_access_token_check(login, password) ||
lfs_token_check(login, password) ||
+ oauth_access_token_check(login, password) ||
personal_access_token_check(login, password) ||
+ user_with_password_for_git(login, password) ||
Gitlab::Auth::Result.new
rate_limit!(ip, success: result.success?, login: login)
@@ -143,7 +146,9 @@ module Gitlab
read_authentication_abilities
end
- Result.new(actor, nil, token_handler.type, authentication_abilities) if Devise.secure_compare(token_handler.token, password)
+ if Devise.secure_compare(token_handler.token, password)
+ Gitlab::Auth::Result.new(actor, nil, token_handler.type, authentication_abilities)
+ end
end
def build_access_token_check(login, password)
diff --git a/lib/gitlab/auth/result.rb b/lib/gitlab/auth/result.rb
index 6be7f690676..39b86c61a18 100644
--- a/lib/gitlab/auth/result.rb
+++ b/lib/gitlab/auth/result.rb
@@ -9,8 +9,7 @@ module Gitlab
def lfs_deploy_token?(for_project)
type == :lfs_deploy_token &&
- actor &&
- actor.projects.include?(for_project)
+ actor.try(:has_access_to?, for_project)
end
def success?
diff --git a/lib/gitlab/chat_commands/base_command.rb b/lib/gitlab/chat_commands/base_command.rb
index 4fe53ce93a9..25da8474e95 100644
--- a/lib/gitlab/chat_commands/base_command.rb
+++ b/lib/gitlab/chat_commands/base_command.rb
@@ -42,10 +42,6 @@ module Gitlab
def find_by_iid(iid)
collection.find_by(iid: iid)
end
-
- def presenter
- Gitlab::ChatCommands::Presenter.new
- end
end
end
end
diff --git a/lib/gitlab/chat_commands/command.rb b/lib/gitlab/chat_commands/command.rb
index 145086755e4..f34ed0f4cf2 100644
--- a/lib/gitlab/chat_commands/command.rb
+++ b/lib/gitlab/chat_commands/command.rb
@@ -3,7 +3,7 @@ module Gitlab
class Command < BaseCommand
COMMANDS = [
Gitlab::ChatCommands::IssueShow,
- Gitlab::ChatCommands::IssueCreate,
+ Gitlab::ChatCommands::IssueNew,
Gitlab::ChatCommands::IssueSearch,
Gitlab::ChatCommands::Deploy,
].freeze
@@ -13,51 +13,32 @@ module Gitlab
if command
if command.allowed?(project, current_user)
- present command.new(project, current_user, params).execute(match)
+ command.new(project, current_user, params).execute(match)
else
- access_denied
+ Gitlab::ChatCommands::Presenters::Access.new.access_denied
end
else
- help(help_messages)
+ Gitlab::ChatCommands::Help.new(project, current_user, params).execute(available_commands, params[:text])
end
end
def match_command
match = nil
- service = available_commands.find do |klass|
- match = klass.match(command)
- end
+ service =
+ available_commands.find do |klass|
+ match = klass.match(params[:text])
+ end
[service, match]
end
private
- def help_messages
- available_commands.map(&:help_message)
- end
-
def available_commands
COMMANDS.select do |klass|
klass.available?(project)
end
end
-
- def command
- params[:text]
- end
-
- def help(messages)
- presenter.help(messages, params[:command])
- end
-
- def access_denied
- presenter.access_denied
- end
-
- def present(resource)
- presenter.present(resource)
- end
end
end
end
diff --git a/lib/gitlab/chat_commands/deploy.rb b/lib/gitlab/chat_commands/deploy.rb
index 6bb854dc080..458d90f84e8 100644
--- a/lib/gitlab/chat_commands/deploy.rb
+++ b/lib/gitlab/chat_commands/deploy.rb
@@ -1,8 +1,6 @@
module Gitlab
module ChatCommands
class Deploy < BaseCommand
- include Gitlab::Routing.url_helpers
-
def self.match(text)
/\Adeploy\s+(?<from>\S+.*)\s+to+\s+(?<to>\S+.*)\z/.match(text)
end
@@ -24,34 +22,29 @@ module Gitlab
to = match[:to]
actions = find_actions(from, to)
- return unless actions.present?
- if actions.one?
- play!(from, to, actions.first)
+ if actions.none?
+ Gitlab::ChatCommands::Presenters::Deploy.new(nil).no_actions
+ elsif actions.one?
+ action = play!(from, to, actions.first)
+ Gitlab::ChatCommands::Presenters::Deploy.new(action).present(from, to)
else
- Result.new(:error, 'Too many actions defined')
+ Gitlab::ChatCommands::Presenters::Deploy.new(actions).too_many_actions
end
end
private
def play!(from, to, action)
- new_action = action.play(current_user)
-
- Result.new(:success, "Deployment from #{from} to #{to} started. Follow the progress: #{url(new_action)}.")
+ action.play(current_user)
end
def find_actions(from, to)
environment = project.environments.find_by(name: from)
- return unless environment
+ return [] unless environment
environment.actions_for(to).select(&:starts_environment?)
end
-
- def url(subject)
- polymorphic_url(
- [ subject.project.namespace.becomes(Namespace), subject.project, subject ])
- end
end
end
end
diff --git a/lib/gitlab/chat_commands/help.rb b/lib/gitlab/chat_commands/help.rb
new file mode 100644
index 00000000000..6c0e4d304a4
--- /dev/null
+++ b/lib/gitlab/chat_commands/help.rb
@@ -0,0 +1,28 @@
+module Gitlab
+ module ChatCommands
+ class Help < BaseCommand
+ # This class has to be used last, as it always matches. It has to match
+ # because other commands were not triggered and we want to show the help
+ # command
+ def self.match(_text)
+ true
+ end
+
+ def self.help_message
+ 'help'
+ end
+
+ def self.allowed?(_project, _user)
+ true
+ end
+
+ def execute(commands, text)
+ Gitlab::ChatCommands::Presenters::Help.new(commands).present(trigger, text)
+ end
+
+ def trigger
+ params[:command]
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/chat_commands/issue_create.rb b/lib/gitlab/chat_commands/issue_create.rb
deleted file mode 100644
index cefb6775db8..00000000000
--- a/lib/gitlab/chat_commands/issue_create.rb
+++ /dev/null
@@ -1,26 +0,0 @@
-module Gitlab
- module ChatCommands
- class IssueCreate < IssueCommand
- def self.match(text)
- # we can not match \n with the dot by passing the m modifier as than
- # the title and description are not seperated
- /\Aissue\s+(new|create)\s+(?<title>[^\n]*)\n*(?<description>(.|\n)*)/.match(text)
- end
-
- def self.help_message
- 'issue new <title> *`⇧ Shift`*+*`↵ Enter`* <description>'
- end
-
- def self.allowed?(project, user)
- can?(user, :create_issue, project)
- end
-
- def execute(match)
- title = match[:title]
- description = match[:description].to_s.rstrip
-
- Issues::CreateService.new(project, current_user, title: title, description: description).execute
- end
- end
- end
-end
diff --git a/lib/gitlab/chat_commands/issue_new.rb b/lib/gitlab/chat_commands/issue_new.rb
new file mode 100644
index 00000000000..016054ecd46
--- /dev/null
+++ b/lib/gitlab/chat_commands/issue_new.rb
@@ -0,0 +1,42 @@
+module Gitlab
+ module ChatCommands
+ class IssueNew < IssueCommand
+ def self.match(text)
+ # we can not match \n with the dot by passing the m modifier as than
+ # the title and description are not seperated
+ /\Aissue\s+(new|create)\s+(?<title>[^\n]*)\n*(?<description>(.|\n)*)/.match(text)
+ end
+
+ def self.help_message
+ 'issue new <title> *`⇧ Shift`*+*`↵ Enter`* <description>'
+ end
+
+ def self.allowed?(project, user)
+ can?(user, :create_issue, project)
+ end
+
+ def execute(match)
+ title = match[:title]
+ description = match[:description].to_s.rstrip
+
+ issue = create_issue(title: title, description: description)
+
+ if issue.persisted?
+ presenter(issue).present
+ else
+ presenter(issue).display_errors
+ end
+ end
+
+ private
+
+ def create_issue(title:, description:)
+ Issues::CreateService.new(project, current_user, title: title, description: description).execute
+ end
+
+ def presenter(issue)
+ Gitlab::ChatCommands::Presenters::IssueNew.new(issue)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/chat_commands/issue_search.rb b/lib/gitlab/chat_commands/issue_search.rb
index 51bf80c800b..3491b53093e 100644
--- a/lib/gitlab/chat_commands/issue_search.rb
+++ b/lib/gitlab/chat_commands/issue_search.rb
@@ -10,7 +10,13 @@ module Gitlab
end
def execute(match)
- collection.search(match[:query]).limit(QUERY_LIMIT)
+ issues = collection.search(match[:query]).limit(QUERY_LIMIT)
+
+ if issues.present?
+ Presenters::IssueSearch.new(issues).present
+ else
+ Presenters::Access.new(issues).not_found
+ end
end
end
end
diff --git a/lib/gitlab/chat_commands/issue_show.rb b/lib/gitlab/chat_commands/issue_show.rb
index 2a45d49cf6b..d6013f4d10c 100644
--- a/lib/gitlab/chat_commands/issue_show.rb
+++ b/lib/gitlab/chat_commands/issue_show.rb
@@ -10,7 +10,13 @@ module Gitlab
end
def execute(match)
- find_by_iid(match[:iid])
+ issue = find_by_iid(match[:iid])
+
+ if issue
+ Gitlab::ChatCommands::Presenters::IssueShow.new(issue).present
+ else
+ Gitlab::ChatCommands::Presenters::Access.new.not_found
+ end
end
end
end
diff --git a/lib/gitlab/chat_commands/presenter.rb b/lib/gitlab/chat_commands/presenter.rb
deleted file mode 100644
index caceaa25391..00000000000
--- a/lib/gitlab/chat_commands/presenter.rb
+++ /dev/null
@@ -1,131 +0,0 @@
-module Gitlab
- module ChatCommands
- class Presenter
- include Gitlab::Routing
-
- def authorize_chat_name(url)
- message = if url
- ":wave: Hi there! Before I do anything for you, please [connect your GitLab account](#{url})."
- else
- ":sweat_smile: Couldn't identify you, nor can I autorize you!"
- end
-
- ephemeral_response(message)
- end
-
- def help(commands, trigger)
- if commands.none?
- ephemeral_response("No commands configured")
- else
- commands.map! { |command| "#{trigger} #{command}" }
- message = header_with_list("Available commands", commands)
-
- ephemeral_response(message)
- end
- end
-
- def present(subject)
- return not_found unless subject
-
- if subject.is_a?(Gitlab::ChatCommands::Result)
- show_result(subject)
- elsif subject.respond_to?(:count)
- if subject.many?
- multiple_resources(subject)
- elsif subject.none?
- not_found
- else
- single_resource(subject)
- end
- else
- single_resource(subject)
- end
- end
-
- def access_denied
- ephemeral_response("Whoops! That action is not allowed. This incident will be [reported](https://xkcd.com/838/).")
- end
-
- private
-
- def show_result(result)
- case result.type
- when :success
- in_channel_response(result.message)
- else
- ephemeral_response(result.message)
- end
- end
-
- def not_found
- ephemeral_response("404 not found! GitLab couldn't find what you were looking for! :boom:")
- end
-
- def single_resource(resource)
- return error(resource) if resource.errors.any? || !resource.persisted?
-
- message = "#{title(resource)}:"
- message << "\n\n#{resource.description}" if resource.try(:description)
-
- in_channel_response(message)
- end
-
- def multiple_resources(resources)
- resources.map! { |resource| title(resource) }
-
- message = header_with_list("Multiple results were found:", resources)
-
- ephemeral_response(message)
- end
-
- def error(resource)
- message = header_with_list("The action was not successful, because:", resource.errors.messages)
-
- ephemeral_response(message)
- end
-
- def title(resource)
- reference = resource.try(:to_reference) || resource.try(:id)
- title = resource.try(:title) || resource.try(:name)
-
- "[#{reference} #{title}](#{url(resource)})"
- end
-
- def header_with_list(header, items)
- message = [header]
-
- items.each do |item|
- message << "- #{item}"
- end
-
- message.join("\n")
- end
-
- def url(resource)
- url_for(
- [
- resource.project.namespace.becomes(Namespace),
- resource.project,
- resource
- ]
- )
- end
-
- def ephemeral_response(message)
- {
- response_type: :ephemeral,
- text: message,
- status: 200
- }
- end
-
- def in_channel_response(message)
- {
- response_type: :in_channel,
- text: message,
- status: 200
- }
- end
- end
- end
-end
diff --git a/lib/gitlab/chat_commands/presenters/access.rb b/lib/gitlab/chat_commands/presenters/access.rb
new file mode 100644
index 00000000000..92f4fa17f78
--- /dev/null
+++ b/lib/gitlab/chat_commands/presenters/access.rb
@@ -0,0 +1,40 @@
+module Gitlab
+ module ChatCommands
+ module Presenters
+ class Access < Presenters::Base
+ def access_denied
+ ephemeral_response(text: "Whoops! This action is not allowed. This incident will be [reported](https://xkcd.com/838/).")
+ end
+
+ def not_found
+ ephemeral_response(text: "404 not found! GitLab couldn't find what you were looking for! :boom:")
+ end
+
+ def authorize
+ message =
+ if @resource
+ ":wave: Hi there! Before I do anything for you, please [connect your GitLab account](#{@resource})."
+ else
+ ":sweat_smile: Couldn't identify you, nor can I autorize you!"
+ end
+
+ ephemeral_response(text: message)
+ end
+
+ def unknown_command(commands)
+ ephemeral_response(text: help_message(trigger))
+ end
+
+ private
+
+ def help_message(trigger)
+ header_with_list("Command not found, these are the commands you can use", full_commands(trigger))
+ end
+
+ def full_commands(trigger)
+ @resource.map { |command| "#{trigger} #{command.help_message}" }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/chat_commands/presenters/base.rb b/lib/gitlab/chat_commands/presenters/base.rb
new file mode 100644
index 00000000000..2700a5a2ad5
--- /dev/null
+++ b/lib/gitlab/chat_commands/presenters/base.rb
@@ -0,0 +1,77 @@
+module Gitlab
+ module ChatCommands
+ module Presenters
+ class Base
+ include Gitlab::Routing.url_helpers
+
+ def initialize(resource = nil)
+ @resource = resource
+ end
+
+ def display_errors
+ message = header_with_list("The action was not successful, because:", @resource.errors.full_messages)
+
+ ephemeral_response(text: message)
+ end
+
+ private
+
+ def header_with_list(header, items)
+ message = [header]
+
+ items.each do |item|
+ message << "- #{item}"
+ end
+
+ message.join("\n")
+ end
+
+ def ephemeral_response(message)
+ response = {
+ response_type: :ephemeral,
+ status: 200
+ }.merge(message)
+
+ format_response(response)
+ end
+
+ def in_channel_response(message)
+ response = {
+ response_type: :in_channel,
+ status: 200
+ }.merge(message)
+
+ format_response(response)
+ end
+
+ def format_response(response)
+ response[:text] = format(response[:text]) if response.has_key?(:text)
+
+ if response.has_key?(:attachments)
+ response[:attachments].each do |attachment|
+ attachment[:pretext] = format(attachment[:pretext]) if attachment[:pretext]
+ attachment[:text] = format(attachment[:text]) if attachment[:text]
+ end
+ end
+
+ response
+ end
+
+ # Convert Markdown to slacks format
+ def format(string)
+ Slack::Notifier::LinkFormatter.format(string)
+ end
+
+ def resource_url
+ url_for(
+ [
+ @resource.project.namespace.becomes(Namespace),
+ @resource.project,
+ @resource
+ ]
+ )
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/chat_commands/presenters/deploy.rb b/lib/gitlab/chat_commands/presenters/deploy.rb
new file mode 100644
index 00000000000..863d0bf99ca
--- /dev/null
+++ b/lib/gitlab/chat_commands/presenters/deploy.rb
@@ -0,0 +1,21 @@
+module Gitlab
+ module ChatCommands
+ module Presenters
+ class Deploy < Presenters::Base
+ def present(from, to)
+ message = "Deployment started from #{from} to #{to}. [Follow its progress](#{resource_url})."
+
+ in_channel_response(text: message)
+ end
+
+ def no_actions
+ ephemeral_response(text: "No action found to be executed")
+ end
+
+ def too_many_actions
+ ephemeral_response(text: "Too many actions defined")
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/chat_commands/presenters/help.rb b/lib/gitlab/chat_commands/presenters/help.rb
new file mode 100644
index 00000000000..cd47b7f4c6a
--- /dev/null
+++ b/lib/gitlab/chat_commands/presenters/help.rb
@@ -0,0 +1,27 @@
+module Gitlab
+ module ChatCommands
+ module Presenters
+ class Help < Presenters::Base
+ def present(trigger, text)
+ ephemeral_response(text: help_message(trigger, text))
+ end
+
+ private
+
+ def help_message(trigger, text)
+ return "No commands available :thinking_face:" unless @resource.present?
+
+ if text.start_with?('help')
+ header_with_list("Available commands", full_commands(trigger))
+ else
+ header_with_list("Unknown command, these commands are available", full_commands(trigger))
+ end
+ end
+
+ def full_commands(trigger)
+ @resource.map { |command| "#{trigger} #{command.help_message}" }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/chat_commands/presenters/issuable.rb b/lib/gitlab/chat_commands/presenters/issuable.rb
new file mode 100644
index 00000000000..dfb1c8f6616
--- /dev/null
+++ b/lib/gitlab/chat_commands/presenters/issuable.rb
@@ -0,0 +1,43 @@
+module Gitlab
+ module ChatCommands
+ module Presenters
+ module Issuable
+ def color(issuable)
+ issuable.open? ? '#38ae67' : '#d22852'
+ end
+
+ def status_text(issuable)
+ issuable.open? ? 'Open' : 'Closed'
+ end
+
+ def project
+ @resource.project
+ end
+
+ def author
+ @resource.author
+ end
+
+ def fields
+ [
+ {
+ title: "Assignee",
+ value: @resource.assignee ? @resource.assignee.name : "_None_",
+ short: true
+ },
+ {
+ title: "Milestone",
+ value: @resource.milestone ? @resource.milestone.title : "_None_",
+ short: true
+ },
+ {
+ title: "Labels",
+ value: @resource.labels.any? ? @resource.label_names : "_None_",
+ short: true
+ }
+ ]
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/chat_commands/presenters/issue_new.rb b/lib/gitlab/chat_commands/presenters/issue_new.rb
new file mode 100644
index 00000000000..a1a3add56c9
--- /dev/null
+++ b/lib/gitlab/chat_commands/presenters/issue_new.rb
@@ -0,0 +1,50 @@
+module Gitlab
+ module ChatCommands
+ module Presenters
+ class IssueNew < Presenters::Base
+ include Presenters::Issuable
+
+ def present
+ in_channel_response(new_issue)
+ end
+
+ private
+
+ def new_issue
+ {
+ attachments: [
+ {
+ title: "#{@resource.title} · #{@resource.to_reference}",
+ title_link: resource_url,
+ author_name: author.name,
+ author_icon: author.avatar_url,
+ fallback: "New issue #{@resource.to_reference}: #{@resource.title}",
+ pretext: pretext,
+ color: color(@resource),
+ fields: fields,
+ mrkdwn_in: [
+ :title,
+ :pretext,
+ :text,
+ :fields
+ ]
+ }
+ ]
+ }
+ end
+
+ def pretext
+ "I created an issue on #{author_profile_link}'s behalf: **#{@resource.to_reference}** in #{project_link}"
+ end
+
+ def project_link
+ "[#{project.name_with_namespace}](#{projects_url(project)})"
+ end
+
+ def author_profile_link
+ "[#{author.to_reference}](#{url_for(author)})"
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/chat_commands/presenters/issue_search.rb b/lib/gitlab/chat_commands/presenters/issue_search.rb
new file mode 100644
index 00000000000..3478359b91d
--- /dev/null
+++ b/lib/gitlab/chat_commands/presenters/issue_search.rb
@@ -0,0 +1,47 @@
+module Gitlab
+ module ChatCommands
+ module Presenters
+ class IssueSearch < Presenters::Base
+ include Presenters::Issuable
+
+ def present
+ text = if @resource.count >= 5
+ "Here are the first 5 issues I found:"
+ elsif @resource.one?
+ "Here is the only issue I found:"
+ else
+ "Here are the #{@resource.count} issues I found:"
+ end
+
+ ephemeral_response(text: text, attachments: attachments)
+ end
+
+ private
+
+ def attachments
+ @resource.map do |issue|
+ url = "[#{issue.to_reference}](#{url_for([namespace, project, issue])})"
+
+ {
+ color: color(issue),
+ fallback: "#{issue.to_reference} #{issue.title}",
+ text: "#{url} · #{issue.title} (#{status_text(issue)})",
+
+ mrkdwn_in: [
+ :text
+ ]
+ }
+ end
+ end
+
+ def project
+ @project ||= @resource.first.project
+ end
+
+ def namespace
+ @namespace ||= project.namespace.becomes(Namespace)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/chat_commands/presenters/issue_show.rb b/lib/gitlab/chat_commands/presenters/issue_show.rb
new file mode 100644
index 00000000000..fe5847ccd15
--- /dev/null
+++ b/lib/gitlab/chat_commands/presenters/issue_show.rb
@@ -0,0 +1,61 @@
+module Gitlab
+ module ChatCommands
+ module Presenters
+ class IssueShow < Presenters::Base
+ include Presenters::Issuable
+
+ def present
+ if @resource.confidential?
+ ephemeral_response(show_issue)
+ else
+ in_channel_response(show_issue)
+ end
+ end
+
+ private
+
+ def show_issue
+ {
+ attachments: [
+ {
+ title: "#{@resource.title} · #{@resource.to_reference}",
+ title_link: resource_url,
+ author_name: author.name,
+ author_icon: author.avatar_url,
+ fallback: "Issue #{@resource.to_reference}: #{@resource.title}",
+ pretext: pretext,
+ text: text,
+ color: color(@resource),
+ fields: fields,
+ mrkdwn_in: [
+ :pretext,
+ :text,
+ :fields
+ ]
+ }
+ ]
+ }
+ end
+
+ def text
+ message = "**#{status_text(@resource)}**"
+
+ if @resource.upvotes.zero? && @resource.downvotes.zero? && @resource.user_notes_count.zero?
+ return message
+ end
+
+ message << " · "
+ message << ":+1: #{@resource.upvotes} " unless @resource.upvotes.zero?
+ message << ":-1: #{@resource.downvotes} " unless @resource.downvotes.zero?
+ message << ":speech_balloon: #{@resource.user_notes_count}" unless @resource.user_notes_count.zero?
+
+ message
+ end
+
+ def pretext
+ "Issue *#{@resource.to_reference}* from #{project.name_with_namespace}"
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/checks/change_access.rb b/lib/gitlab/checks/change_access.rb
index 3d203017d9f..273118135a9 100644
--- a/lib/gitlab/checks/change_access.rb
+++ b/lib/gitlab/checks/change_access.rb
@@ -1,14 +1,16 @@
module Gitlab
module Checks
class ChangeAccess
- attr_reader :user_access, :project
+ attr_reader :user_access, :project, :skip_authorization
- def initialize(change, user_access:, project:, env: {})
+ def initialize(
+ change, user_access:, project:, env: {}, skip_authorization: false)
@oldrev, @newrev, @ref = change.values_at(:oldrev, :newrev, :ref)
@branch_name = Gitlab::Git.branch_name(@ref)
@user_access = user_access
@project = project
@env = env
+ @skip_authorization = skip_authorization
end
def exec
@@ -24,12 +26,13 @@ module Gitlab
protected
def protected_branch_checks
+ return if skip_authorization
return unless @branch_name
return unless project.protected_branch?(@branch_name)
- if forced_push? && user_access.cannot_do_action?(:force_push_code_to_protected_branches)
+ if forced_push?
return "You are not allowed to force push code to a protected branch on this project."
- elsif Gitlab::Git.blank_ref?(@newrev) && user_access.cannot_do_action?(:remove_protected_branches)
+ elsif Gitlab::Git.blank_ref?(@newrev)
return "You are not allowed to delete protected branches from this project."
end
@@ -49,6 +52,8 @@ module Gitlab
end
def tag_checks
+ return if skip_authorization
+
tag_ref = Gitlab::Git.tag_name(@ref)
if tag_ref && protected_tag?(tag_ref) && user_access.cannot_do_action?(:admin_project)
@@ -57,6 +62,8 @@ module Gitlab
end
def push_checks
+ return if skip_authorization
+
if user_access.cannot_do_action?(:push_code)
"You are not allowed to push code to this project."
end
diff --git a/lib/gitlab/ci/config/entry/coverage.rb b/lib/gitlab/ci/config/entry/coverage.rb
new file mode 100644
index 00000000000..12a063059cb
--- /dev/null
+++ b/lib/gitlab/ci/config/entry/coverage.rb
@@ -0,0 +1,22 @@
+module Gitlab
+ module Ci
+ class Config
+ module Entry
+ ##
+ # Entry that represents Coverage settings.
+ #
+ class Coverage < Node
+ include Validatable
+
+ validations do
+ validates :config, regexp: true
+ end
+
+ def value
+ @config[1...-1]
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/entry/environment.rb b/lib/gitlab/ci/config/entry/environment.rb
index b7b4b91eb51..f7c530c7d9f 100644
--- a/lib/gitlab/ci/config/entry/environment.rb
+++ b/lib/gitlab/ci/config/entry/environment.rb
@@ -33,7 +33,6 @@ module Gitlab
validates :url,
length: { maximum: 255 },
- addressable_url: true,
allow_nil: true
validates :action,
diff --git a/lib/gitlab/ci/config/entry/job.rb b/lib/gitlab/ci/config/entry/job.rb
index a55362f0b6b..69a5e6f433d 100644
--- a/lib/gitlab/ci/config/entry/job.rb
+++ b/lib/gitlab/ci/config/entry/job.rb
@@ -11,7 +11,7 @@ module Gitlab
ALLOWED_KEYS = %i[tags script only except type image services allow_failure
type stage when artifacts cache dependencies before_script
- after_script variables environment]
+ after_script variables environment coverage]
validations do
validates :config, allowed_keys: ALLOWED_KEYS
@@ -71,9 +71,12 @@ module Gitlab
entry :environment, Entry::Environment,
description: 'Environment configuration for this job.'
+ entry :coverage, Entry::Coverage,
+ description: 'Coverage configuration for this job.'
+
helpers :before_script, :script, :stage, :type, :after_script,
:cache, :image, :services, :only, :except, :variables,
- :artifacts, :commands, :environment
+ :artifacts, :commands, :environment, :coverage
attributes :script, :tags, :allow_failure, :when, :dependencies
@@ -130,6 +133,7 @@ module Gitlab
variables: variables_defined? ? variables_value : nil,
environment: environment_defined? ? environment_value : nil,
environment_name: environment_defined? ? environment_value[:name] : nil,
+ coverage: coverage_defined? ? coverage_value : nil,
artifacts: artifacts_value,
after_script: after_script_value }
end
diff --git a/lib/gitlab/ci/config/entry/legacy_validation_helpers.rb b/lib/gitlab/ci/config/entry/legacy_validation_helpers.rb
index f01975aab5c..9b9a0a8125a 100644
--- a/lib/gitlab/ci/config/entry/legacy_validation_helpers.rb
+++ b/lib/gitlab/ci/config/entry/legacy_validation_helpers.rb
@@ -28,17 +28,21 @@ module Gitlab
value.is_a?(String) || value.is_a?(Symbol)
end
+ def validate_regexp(value)
+ !value.nil? && Regexp.new(value.to_s) && true
+ rescue RegexpError, TypeError
+ false
+ end
+
def validate_string_or_regexp(value)
return true if value.is_a?(Symbol)
return false unless value.is_a?(String)
if value.first == '/' && value.last == '/'
- Regexp.new(value[1...-1])
+ validate_regexp(value[1...-1])
else
true
end
- rescue RegexpError
- false
end
def validate_boolean(value)
diff --git a/lib/gitlab/ci/config/entry/trigger.rb b/lib/gitlab/ci/config/entry/trigger.rb
index 28b0a9ffe01..16b234e6c59 100644
--- a/lib/gitlab/ci/config/entry/trigger.rb
+++ b/lib/gitlab/ci/config/entry/trigger.rb
@@ -9,15 +9,7 @@ module Gitlab
include Validatable
validations do
- include LegacyValidationHelpers
-
- validate :array_of_strings_or_regexps
-
- def array_of_strings_or_regexps
- unless validate_array_of_strings_or_regexps(config)
- errors.add(:config, 'should be an array of strings or regexps')
- end
- end
+ validates :config, array_of_strings_or_regexps: true
end
end
end
diff --git a/lib/gitlab/ci/config/entry/validators.rb b/lib/gitlab/ci/config/entry/validators.rb
index 8632dd0e233..bd7428b1272 100644
--- a/lib/gitlab/ci/config/entry/validators.rb
+++ b/lib/gitlab/ci/config/entry/validators.rb
@@ -54,6 +54,51 @@ module Gitlab
end
end
+ class RegexpValidator < ActiveModel::EachValidator
+ include LegacyValidationHelpers
+
+ def validate_each(record, attribute, value)
+ unless validate_regexp(value)
+ record.errors.add(attribute, 'must be a regular expression')
+ end
+ end
+
+ private
+
+ def look_like_regexp?(value)
+ value.is_a?(String) && value.start_with?('/') &&
+ value.end_with?('/')
+ end
+
+ def validate_regexp(value)
+ look_like_regexp?(value) &&
+ Regexp.new(value.to_s[1...-1]) &&
+ true
+ rescue RegexpError
+ false
+ end
+ end
+
+ class ArrayOfStringsOrRegexpsValidator < RegexpValidator
+ def validate_each(record, attribute, value)
+ unless validate_array_of_strings_or_regexps(value)
+ record.errors.add(attribute, 'should be an array of strings or regexps')
+ end
+ end
+
+ private
+
+ def validate_array_of_strings_or_regexps(values)
+ values.is_a?(Array) && values.all?(&method(:validate_string_or_regexp))
+ end
+
+ def validate_string_or_regexp(value)
+ return false unless value.is_a?(String)
+ return validate_regexp(value) if look_like_regexp?(value)
+ true
+ end
+ end
+
class TypeValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
type = options[:with]
diff --git a/lib/gitlab/ci/status/build/cancelable.rb b/lib/gitlab/ci/status/build/cancelable.rb
index a979fe7d573..67bbc3c4849 100644
--- a/lib/gitlab/ci/status/build/cancelable.rb
+++ b/lib/gitlab/ci/status/build/cancelable.rb
@@ -10,7 +10,7 @@ module Gitlab
end
def action_icon
- 'ban'
+ 'icon_action_cancel'
end
def action_path
diff --git a/lib/gitlab/ci/status/build/factory.rb b/lib/gitlab/ci/status/build/factory.rb
index eee9a64120b..38ac6edc9f1 100644
--- a/lib/gitlab/ci/status/build/factory.rb
+++ b/lib/gitlab/ci/status/build/factory.rb
@@ -4,8 +4,11 @@ module Gitlab
module Build
class Factory < Status::Factory
def self.extended_statuses
- [Status::Build::Stop, Status::Build::Play,
- Status::Build::Cancelable, Status::Build::Retryable]
+ [[Status::Build::Cancelable,
+ Status::Build::Retryable],
+ [Status::Build::FailedAllowed,
+ Status::Build::Play,
+ Status::Build::Stop]]
end
def self.common_helpers
diff --git a/lib/gitlab/ci/status/build/failed_allowed.rb b/lib/gitlab/ci/status/build/failed_allowed.rb
new file mode 100644
index 00000000000..807afe24bd5
--- /dev/null
+++ b/lib/gitlab/ci/status/build/failed_allowed.rb
@@ -0,0 +1,27 @@
+module Gitlab
+ module Ci
+ module Status
+ module Build
+ class FailedAllowed < SimpleDelegator
+ include Status::Extended
+
+ def label
+ 'failed (allowed to fail)'
+ end
+
+ def icon
+ 'icon_status_warning'
+ end
+
+ def group
+ 'failed_with_warnings'
+ end
+
+ def self.matches?(build, user)
+ build.failed? && build.allow_failure?
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/status/build/play.rb b/lib/gitlab/ci/status/build/play.rb
index 1bf949c96dd..0f4b7b24cef 100644
--- a/lib/gitlab/ci/status/build/play.rb
+++ b/lib/gitlab/ci/status/build/play.rb
@@ -26,17 +26,13 @@ module Gitlab
end
def action_icon
- 'play'
+ 'icon_action_play'
end
def action_title
'Play'
end
- def action_class
- 'ci-play-icon'
- end
-
def action_path
play_namespace_project_build_path(subject.project.namespace,
subject.project,
diff --git a/lib/gitlab/ci/status/build/retryable.rb b/lib/gitlab/ci/status/build/retryable.rb
index 8e38d6a8523..6b362af7634 100644
--- a/lib/gitlab/ci/status/build/retryable.rb
+++ b/lib/gitlab/ci/status/build/retryable.rb
@@ -10,7 +10,7 @@ module Gitlab
end
def action_icon
- 'refresh'
+ 'icon_action_retry'
end
def action_title
diff --git a/lib/gitlab/ci/status/build/stop.rb b/lib/gitlab/ci/status/build/stop.rb
index e1dfdb76d41..90401cad0d2 100644
--- a/lib/gitlab/ci/status/build/stop.rb
+++ b/lib/gitlab/ci/status/build/stop.rb
@@ -26,7 +26,7 @@ module Gitlab
end
def action_icon
- 'stop'
+ 'icon_action_stop'
end
def action_title
diff --git a/lib/gitlab/ci/status/core.rb b/lib/gitlab/ci/status/core.rb
index 73b6ab5a635..3dd2b9e01f6 100644
--- a/lib/gitlab/ci/status/core.rb
+++ b/lib/gitlab/ci/status/core.rb
@@ -42,9 +42,6 @@ module Gitlab
raise NotImplementedError
end
- def action_class
- end
-
def action_path
raise NotImplementedError
end
diff --git a/lib/gitlab/ci/status/external/common.rb b/lib/gitlab/ci/status/external/common.rb
new file mode 100644
index 00000000000..4969a350862
--- /dev/null
+++ b/lib/gitlab/ci/status/external/common.rb
@@ -0,0 +1,22 @@
+module Gitlab
+ module Ci
+ module Status
+ module External
+ module Common
+ def has_details?
+ subject.target_url.present? &&
+ can?(user, :read_commit_status, subject)
+ end
+
+ def details_path
+ subject.target_url
+ end
+
+ def has_action?
+ false
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/status/external/factory.rb b/lib/gitlab/ci/status/external/factory.rb
new file mode 100644
index 00000000000..07b15bd8d97
--- /dev/null
+++ b/lib/gitlab/ci/status/external/factory.rb
@@ -0,0 +1,13 @@
+module Gitlab
+ module Ci
+ module Status
+ module External
+ class Factory < Status::Factory
+ def self.common_helpers
+ Status::External::Common
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/status/factory.rb b/lib/gitlab/ci/status/factory.rb
index ae9ef895df4..15836c699c7 100644
--- a/lib/gitlab/ci/status/factory.rb
+++ b/lib/gitlab/ci/status/factory.rb
@@ -5,41 +5,46 @@ module Gitlab
def initialize(subject, user)
@subject = subject
@user = user
+ @status = subject.status || HasStatus::DEFAULT_STATUS
end
def fabricate!
- if extended_status
- extended_status.new(core_status)
- else
+ if extended_statuses.none?
core_status
+ else
+ compound_extended_status
end
end
- def self.extended_statuses
- []
+ def core_status
+ Gitlab::Ci::Status
+ .const_get(@status.capitalize)
+ .new(@subject, @user)
+ .extend(self.class.common_helpers)
end
- def self.common_helpers
- Module.new
+ def compound_extended_status
+ extended_statuses.inject(core_status) do |status, extended|
+ extended.new(status)
+ end
end
- private
+ def extended_statuses
+ return @extended_statuses if defined?(@extended_statuses)
- def simple_status
- @simple_status ||= @subject.status || :created
+ groups = self.class.extended_statuses.map do |group|
+ Array(group).find { |status| status.matches?(@subject, @user) }
+ end
+
+ @extended_statuses = groups.flatten.compact
end
- def core_status
- Gitlab::Ci::Status
- .const_get(simple_status.capitalize)
- .new(@subject, @user)
- .extend(self.class.common_helpers)
+ def self.extended_statuses
+ []
end
- def extended_status
- @extended ||= self.class.extended_statuses.find do |status|
- status.matches?(@subject, @user)
- end
+ def self.common_helpers
+ Module.new
end
end
end
diff --git a/lib/gitlab/ci/status/pipeline/factory.rb b/lib/gitlab/ci/status/pipeline/factory.rb
index 16dcb326be9..13c8343b12a 100644
--- a/lib/gitlab/ci/status/pipeline/factory.rb
+++ b/lib/gitlab/ci/status/pipeline/factory.rb
@@ -4,7 +4,7 @@ module Gitlab
module Pipeline
class Factory < Status::Factory
def self.extended_statuses
- [Pipeline::SuccessWithWarnings]
+ [Status::SuccessWarning]
end
def self.common_helpers
diff --git a/lib/gitlab/ci/status/pipeline/success_with_warnings.rb b/lib/gitlab/ci/status/pipeline/success_with_warnings.rb
deleted file mode 100644
index 24bf8b869e0..00000000000
--- a/lib/gitlab/ci/status/pipeline/success_with_warnings.rb
+++ /dev/null
@@ -1,31 +0,0 @@
-module Gitlab
- module Ci
- module Status
- module Pipeline
- class SuccessWithWarnings < SimpleDelegator
- include Status::Extended
-
- def text
- 'passed'
- end
-
- def label
- 'passed with warnings'
- end
-
- def icon
- 'icon_status_warning'
- end
-
- def group
- 'success_with_warnings'
- end
-
- def self.matches?(pipeline, user)
- pipeline.success? && pipeline.has_warnings?
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/ci/status/stage/factory.rb b/lib/gitlab/ci/status/stage/factory.rb
index 689a5dd45bc..4c37f084d07 100644
--- a/lib/gitlab/ci/status/stage/factory.rb
+++ b/lib/gitlab/ci/status/stage/factory.rb
@@ -3,6 +3,10 @@ module Gitlab
module Status
module Stage
class Factory < Status::Factory
+ def self.extended_statuses
+ [Status::SuccessWarning]
+ end
+
def self.common_helpers
Status::Stage::Common
end
diff --git a/lib/gitlab/ci/status/success_warning.rb b/lib/gitlab/ci/status/success_warning.rb
new file mode 100644
index 00000000000..d4cdab6957a
--- /dev/null
+++ b/lib/gitlab/ci/status/success_warning.rb
@@ -0,0 +1,33 @@
+module Gitlab
+ module Ci
+ module Status
+ ##
+ # Extended status used when pipeline or stage passed conditionally.
+ # This means that failed jobs that are allowed to fail were present.
+ #
+ class SuccessWarning < SimpleDelegator
+ include Status::Extended
+
+ def text
+ 'passed'
+ end
+
+ def label
+ 'passed with warnings'
+ end
+
+ def icon
+ 'icon_status_warning'
+ end
+
+ def group
+ 'success_with_warnings'
+ end
+
+ def self.matches?(subject, user)
+ subject.success? && subject.has_warnings?
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/trace_reader.rb b/lib/gitlab/ci/trace_reader.rb
index 37e51536e8f..1d7ddeb3e0f 100644
--- a/lib/gitlab/ci/trace_reader.rb
+++ b/lib/gitlab/ci/trace_reader.rb
@@ -42,6 +42,7 @@ module Gitlab
end
chunks.join.lines.last(max_lines).join
+ .force_encoding(Encoding.default_external)
end
end
end
diff --git a/lib/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb
index 9d142f1b82e..e20f5f6f514 100644
--- a/lib/gitlab/current_settings.rb
+++ b/lib/gitlab/current_settings.rb
@@ -9,7 +9,9 @@ module Gitlab
end
def ensure_application_settings!
- if connect_to_db?
+ return fake_application_settings unless connect_to_db?
+
+ unless ENV['IN_MEMORY_APPLICATION_SETTINGS'] == 'true'
begin
settings = ::ApplicationSetting.current
# In case Redis isn't running or the Redis UNIX socket file is not available
@@ -20,42 +22,23 @@ module Gitlab
settings ||= ::ApplicationSetting.create_from_defaults unless ActiveRecord::Migrator.needs_migration?
end
- settings || fake_application_settings
+ settings || in_memory_application_settings
end
def sidekiq_throttling_enabled?
current_application_settings.sidekiq_throttling_enabled?
end
+ def in_memory_application_settings
+ @in_memory_application_settings ||= ::ApplicationSetting.new(::ApplicationSetting.defaults)
+ # In case migrations the application_settings table is not created yet,
+ # we fallback to a simple OpenStruct
+ rescue ActiveRecord::StatementInvalid, ActiveRecord::UnknownAttributeError
+ fake_application_settings
+ end
+
def fake_application_settings
- OpenStruct.new(
- default_projects_limit: Settings.gitlab['default_projects_limit'],
- default_branch_protection: Settings.gitlab['default_branch_protection'],
- 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,
- shared_runners_text: nil,
- restricted_visibility_levels: Settings.gitlab['restricted_visibility_levels'],
- max_attachment_size: Settings.gitlab['max_attachment_size'],
- 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'],
- domain_whitelist: Settings.gitlab['domain_whitelist'],
- import_sources: %w[gitea 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,
- akismet_enabled: false,
- repository_checks_enabled: true,
- container_registry_token_expire_delay: 5,
- user_default_external: false,
- sidekiq_throttling_enabled: false,
- )
+ OpenStruct.new(::ApplicationSetting.defaults)
end
private
diff --git a/lib/gitlab/cycle_analytics/base_event.rb b/lib/gitlab/cycle_analytics/base_event.rb
deleted file mode 100644
index 53a148ad703..00000000000
--- a/lib/gitlab/cycle_analytics/base_event.rb
+++ /dev/null
@@ -1,57 +0,0 @@
-module Gitlab
- module CycleAnalytics
- class BaseEvent
- include MetricsTables
-
- attr_reader :stage, :start_time_attrs, :end_time_attrs, :projections, :query
-
- def initialize(project:, options:)
- @query = EventsQuery.new(project: project, options: options)
- @project = project
- @options = options
- end
-
- def fetch
- update_author!
-
- event_result.map do |event|
- serialize(event) if has_permission?(event['id'])
- end.compact
- end
-
- def custom_query(_base_query); end
-
- def order
- @order || @start_time_attrs
- end
-
- private
-
- def update_author!
- return unless event_result.any? && event_result.first['author_id']
-
- Updater.update!(event_result, from: 'author_id', to: 'author', klass: User)
- end
-
- def event_result
- @event_result ||= @query.execute(self).to_a
- end
-
- def serialize(_event)
- raise NotImplementedError.new("Expected #{self.name} to implement serialize(event)")
- end
-
- def has_permission?(id)
- allowed_ids.nil? || allowed_ids.include?(id.to_i)
- end
-
- def allowed_ids
- nil
- end
-
- def event_result_ids
- event_result.map { |event| event['id'] }
- end
- end
- end
-end
diff --git a/lib/gitlab/cycle_analytics/base_event_fetcher.rb b/lib/gitlab/cycle_analytics/base_event_fetcher.rb
new file mode 100644
index 00000000000..0d8791d396b
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/base_event_fetcher.rb
@@ -0,0 +1,65 @@
+module Gitlab
+ module CycleAnalytics
+ class BaseEventFetcher
+ include BaseQuery
+
+ attr_reader :projections, :query, :stage, :order
+
+ def initialize(project:, stage:, options:)
+ @project = project
+ @stage = stage
+ @options = options
+ end
+
+ def fetch
+ update_author!
+
+ event_result.map do |event|
+ serialize(event) if has_permission?(event['id'])
+ end.compact
+ end
+
+ def order
+ @order || default_order
+ end
+
+ private
+
+ def update_author!
+ return unless event_result.any? && event_result.first['author_id']
+
+ Updater.update!(event_result, from: 'author_id', to: 'author', klass: User)
+ end
+
+ def event_result
+ @event_result ||= ActiveRecord::Base.connection.exec_query(events_query.to_sql).to_a
+ end
+
+ def events_query
+ diff_fn = subtract_datetimes_diff(base_query, @options[:start_time_attrs], @options[:end_time_attrs])
+
+ base_query.project(extract_diff_epoch(diff_fn).as('total_time'), *projections).order(order.desc)
+ end
+
+ def default_order
+ [@options[:start_time_attrs]].flatten.first
+ end
+
+ def serialize(_event)
+ raise NotImplementedError.new("Expected #{self.name} to implement serialize(event)")
+ end
+
+ def has_permission?(id)
+ allowed_ids.nil? || allowed_ids.include?(id.to_i)
+ end
+
+ def allowed_ids
+ nil
+ end
+
+ def event_result_ids
+ event_result.map { |event| event['id'] }
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cycle_analytics/base_query.rb b/lib/gitlab/cycle_analytics/base_query.rb
new file mode 100644
index 00000000000..d560dca45c8
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/base_query.rb
@@ -0,0 +1,31 @@
+module Gitlab
+ module CycleAnalytics
+ module BaseQuery
+ include MetricsTables
+ include Gitlab::Database::Median
+ include Gitlab::Database::DateTime
+
+ private
+
+ def base_query
+ @base_query ||= stage_query
+ end
+
+ def stage_query
+ query = mr_closing_issues_table.join(issue_table).on(issue_table[:id].eq(mr_closing_issues_table[:issue_id])).
+ join(issue_metrics_table).on(issue_table[:id].eq(issue_metrics_table[:issue_id])).
+ where(issue_table[:project_id].eq(@project.id)).
+ where(issue_table[:deleted_at].eq(nil)).
+ where(issue_table[:created_at].gteq(@options[:from]))
+
+ # Load merge_requests
+ query = query.join(mr_table, Arel::Nodes::OuterJoin).
+ on(mr_table[:id].eq(mr_closing_issues_table[:merge_request_id])).
+ join(mr_metrics_table).
+ on(mr_table[:id].eq(mr_metrics_table[:merge_request_id]))
+
+ query
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cycle_analytics/base_stage.rb b/lib/gitlab/cycle_analytics/base_stage.rb
new file mode 100644
index 00000000000..559e3939da6
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/base_stage.rb
@@ -0,0 +1,54 @@
+module Gitlab
+ module CycleAnalytics
+ class BaseStage
+ include BaseQuery
+
+ def initialize(project:, options:)
+ @project = project
+ @options = options
+ end
+
+ def events
+ event_fetcher.fetch
+ end
+
+ def as_json
+ AnalyticsStageSerializer.new.represent(self)
+ end
+
+ def title
+ name.to_s.capitalize
+ end
+
+ def median
+ cte_table = Arel::Table.new("cte_table_for_#{name}")
+
+ # Build a `SELECT` query. We find the first of the `end_time_attrs` that isn't `NULL` (call this end_time).
+ # Next, we find the first of the start_time_attrs that isn't `NULL` (call this start_time).
+ # We compute the (end_time - start_time) interval, and give it an alias based on the current
+ # cycle analytics stage.
+ interval_query = Arel::Nodes::As.new(
+ cte_table,
+ subtract_datetimes(base_query.dup, start_time_attrs, end_time_attrs, name.to_s))
+
+ median_datetime(cte_table, interval_query, name)
+ end
+
+ def name
+ raise NotImplementedError.new("Expected #{self.name} to implement name")
+ end
+
+ private
+
+ def event_fetcher
+ @event_fetcher ||= Gitlab::CycleAnalytics::EventFetcher[name].new(project: @project,
+ stage: name,
+ options: event_options)
+ end
+
+ def event_options
+ @options.merge(start_time_attrs: start_time_attrs, end_time_attrs: end_time_attrs)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cycle_analytics/code_event.rb b/lib/gitlab/cycle_analytics/code_event.rb
deleted file mode 100644
index 2afdf0b8518..00000000000
--- a/lib/gitlab/cycle_analytics/code_event.rb
+++ /dev/null
@@ -1,28 +0,0 @@
-module Gitlab
- module CycleAnalytics
- class CodeEvent < BaseEvent
- include MergeRequestAllowed
-
- def initialize(*args)
- @stage = :code
- @start_time_attrs = issue_metrics_table[:first_mentioned_in_commit_at]
- @end_time_attrs = mr_table[:created_at]
- @projections = [mr_table[:title],
- mr_table[:iid],
- mr_table[:id],
- mr_table[:created_at],
- mr_table[:state],
- mr_table[:author_id]]
- @order = mr_table[:created_at]
-
- super(*args)
- end
-
- private
-
- def serialize(event)
- AnalyticsMergeRequestSerializer.new(project: @project).represent(event).as_json
- end
- end
- end
-end
diff --git a/lib/gitlab/cycle_analytics/code_event_fetcher.rb b/lib/gitlab/cycle_analytics/code_event_fetcher.rb
new file mode 100644
index 00000000000..d5bf6149749
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/code_event_fetcher.rb
@@ -0,0 +1,25 @@
+module Gitlab
+ module CycleAnalytics
+ class CodeEventFetcher < BaseEventFetcher
+ include MergeRequestAllowed
+
+ def initialize(*args)
+ @projections = [mr_table[:title],
+ mr_table[:iid],
+ mr_table[:id],
+ mr_table[:created_at],
+ mr_table[:state],
+ mr_table[:author_id]]
+ @order = mr_table[:created_at]
+
+ super(*args)
+ end
+
+ private
+
+ def serialize(event)
+ AnalyticsMergeRequestSerializer.new(project: @project).represent(event)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cycle_analytics/code_stage.rb b/lib/gitlab/cycle_analytics/code_stage.rb
new file mode 100644
index 00000000000..d1bc2055ba8
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/code_stage.rb
@@ -0,0 +1,21 @@
+module Gitlab
+ module CycleAnalytics
+ class CodeStage < BaseStage
+ def start_time_attrs
+ @start_time_attrs ||= issue_metrics_table[:first_mentioned_in_commit_at]
+ end
+
+ def end_time_attrs
+ @end_time_attrs ||= mr_table[:created_at]
+ end
+
+ def name
+ :code
+ end
+
+ def description
+ "Time until first merge request"
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cycle_analytics/event_fetcher.rb b/lib/gitlab/cycle_analytics/event_fetcher.rb
new file mode 100644
index 00000000000..50e126cf00b
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/event_fetcher.rb
@@ -0,0 +1,9 @@
+module Gitlab
+ module CycleAnalytics
+ module EventFetcher
+ def self.[](stage_name)
+ CycleAnalytics.const_get("#{stage_name.to_s.camelize}EventFetcher")
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cycle_analytics/events.rb b/lib/gitlab/cycle_analytics/events.rb
deleted file mode 100644
index 2d703d76cbb..00000000000
--- a/lib/gitlab/cycle_analytics/events.rb
+++ /dev/null
@@ -1,38 +0,0 @@
-module Gitlab
- module CycleAnalytics
- class Events
- def initialize(project:, options:)
- @project = project
- @options = options
- end
-
- def issue_events
- IssueEvent.new(project: @project, options: @options).fetch
- end
-
- def plan_events
- PlanEvent.new(project: @project, options: @options).fetch
- end
-
- def code_events
- CodeEvent.new(project: @project, options: @options).fetch
- end
-
- def test_events
- TestEvent.new(project: @project, options: @options).fetch
- end
-
- def review_events
- ReviewEvent.new(project: @project, options: @options).fetch
- end
-
- def staging_events
- StagingEvent.new(project: @project, options: @options).fetch
- end
-
- def production_events
- ProductionEvent.new(project: @project, options: @options).fetch
- end
- end
- end
-end
diff --git a/lib/gitlab/cycle_analytics/events_query.rb b/lib/gitlab/cycle_analytics/events_query.rb
deleted file mode 100644
index 2418832ccc2..00000000000
--- a/lib/gitlab/cycle_analytics/events_query.rb
+++ /dev/null
@@ -1,37 +0,0 @@
-module Gitlab
- module CycleAnalytics
- class EventsQuery
- attr_reader :project
-
- def initialize(project:, options: {})
- @project = project
- @from = options[:from]
- @branch = options[:branch]
- @fetcher = Gitlab::CycleAnalytics::MetricsFetcher.new(project: project, from: @from, branch: @branch)
- end
-
- def execute(stage_class)
- @stage_class = stage_class
-
- ActiveRecord::Base.connection.exec_query(query.to_sql)
- end
-
- private
-
- def query
- base_query = @fetcher.base_query_for(@stage_class.stage)
- diff_fn = @fetcher.subtract_datetimes_diff(base_query, @stage_class.start_time_attrs, @stage_class.end_time_attrs)
-
- @stage_class.custom_query(base_query)
-
- base_query.project(extract_epoch(diff_fn).as('total_time'), *@stage_class.projections).order(@stage_class.order.desc)
- end
-
- def extract_epoch(arel_attribute)
- return arel_attribute unless Gitlab::Database.postgresql?
-
- Arel.sql(%Q{EXTRACT(EPOCH FROM (#{arel_attribute.to_sql}))})
- end
- end
- end
-end
diff --git a/lib/gitlab/cycle_analytics/issue_event.rb b/lib/gitlab/cycle_analytics/issue_event.rb
deleted file mode 100644
index 705b7e5ce24..00000000000
--- a/lib/gitlab/cycle_analytics/issue_event.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-module Gitlab
- module CycleAnalytics
- class IssueEvent < BaseEvent
- include IssueAllowed
-
- def initialize(*args)
- @stage = :issue
- @start_time_attrs = issue_table[:created_at]
- @end_time_attrs = [issue_metrics_table[:first_associated_with_milestone_at],
- issue_metrics_table[:first_added_to_board_at]]
- @projections = [issue_table[:title],
- issue_table[:iid],
- issue_table[:id],
- issue_table[:created_at],
- issue_table[:author_id]]
-
- super(*args)
- end
-
- private
-
- def serialize(event)
- AnalyticsIssueSerializer.new(project: @project).represent(event).as_json
- end
- end
- end
-end
diff --git a/lib/gitlab/cycle_analytics/issue_event_fetcher.rb b/lib/gitlab/cycle_analytics/issue_event_fetcher.rb
new file mode 100644
index 00000000000..3df9cbdcfce
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/issue_event_fetcher.rb
@@ -0,0 +1,23 @@
+module Gitlab
+ module CycleAnalytics
+ class IssueEventFetcher < BaseEventFetcher
+ include IssueAllowed
+
+ def initialize(*args)
+ @projections = [issue_table[:title],
+ issue_table[:iid],
+ issue_table[:id],
+ issue_table[:created_at],
+ issue_table[:author_id]]
+
+ super(*args)
+ end
+
+ private
+
+ def serialize(event)
+ AnalyticsIssueSerializer.new(project: @project).represent(event)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cycle_analytics/issue_stage.rb b/lib/gitlab/cycle_analytics/issue_stage.rb
new file mode 100644
index 00000000000..d2068fbc38f
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/issue_stage.rb
@@ -0,0 +1,22 @@
+module Gitlab
+ module CycleAnalytics
+ class IssueStage < BaseStage
+ def start_time_attrs
+ @start_time_attrs ||= issue_table[:created_at]
+ end
+
+ def end_time_attrs
+ @end_time_attrs ||= [issue_metrics_table[:first_associated_with_milestone_at],
+ issue_metrics_table[:first_added_to_board_at]]
+ end
+
+ def name
+ :issue
+ end
+
+ def description
+ "Time before an issue gets scheduled"
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cycle_analytics/metrics_fetcher.rb b/lib/gitlab/cycle_analytics/metrics_fetcher.rb
deleted file mode 100644
index b71e8735e27..00000000000
--- a/lib/gitlab/cycle_analytics/metrics_fetcher.rb
+++ /dev/null
@@ -1,60 +0,0 @@
-module Gitlab
- module CycleAnalytics
- class MetricsFetcher
- include Gitlab::Database::Median
- include Gitlab::Database::DateTime
- include MetricsTables
-
- DEPLOYMENT_METRIC_STAGES = %i[production staging]
-
- def initialize(project:, from:, branch:)
- @project = project
- @project = project
- @from = from
- @branch = branch
- end
-
- def calculate_metric(name, start_time_attrs, end_time_attrs)
- cte_table = Arel::Table.new("cte_table_for_#{name}")
-
- # Build a `SELECT` query. We find the first of the `end_time_attrs` that isn't `NULL` (call this end_time).
- # Next, we find the first of the start_time_attrs that isn't `NULL` (call this start_time).
- # We compute the (end_time - start_time) interval, and give it an alias based on the current
- # cycle analytics stage.
- interval_query = Arel::Nodes::As.new(
- cte_table,
- subtract_datetimes(base_query_for(name), start_time_attrs, end_time_attrs, name.to_s))
-
- median_datetime(cte_table, interval_query, name)
- end
-
- # Join table with a row for every <issue,merge_request> pair (where the merge request
- # closes the given issue) with issue and merge request metrics included. The metrics
- # are loaded with an inner join, so issues / merge requests without metrics are
- # automatically excluded.
- def base_query_for(name)
- # Load issues
- query = mr_closing_issues_table.join(issue_table).on(issue_table[:id].eq(mr_closing_issues_table[:issue_id])).
- join(issue_metrics_table).on(issue_table[:id].eq(issue_metrics_table[:issue_id])).
- where(issue_table[:project_id].eq(@project.id)).
- where(issue_table[:deleted_at].eq(nil)).
- where(issue_table[:created_at].gteq(@from))
-
- query = query.where(build_table[:ref].eq(@branch)) if name == :test && @branch
-
- # Load merge_requests
- query = query.join(mr_table, Arel::Nodes::OuterJoin).
- on(mr_table[:id].eq(mr_closing_issues_table[:merge_request_id])).
- join(mr_metrics_table).
- on(mr_table[:id].eq(mr_metrics_table[:merge_request_id]))
-
- if DEPLOYMENT_METRIC_STAGES.include?(name)
- # Limit to merge requests that have been deployed to production after `@from`
- query.where(mr_metrics_table[:first_deployed_to_production_at].gteq(@from))
- end
-
- query
- end
- end
- end
-end
diff --git a/lib/gitlab/cycle_analytics/plan_event.rb b/lib/gitlab/cycle_analytics/plan_event.rb
deleted file mode 100644
index 7c3f0e9989f..00000000000
--- a/lib/gitlab/cycle_analytics/plan_event.rb
+++ /dev/null
@@ -1,46 +0,0 @@
-module Gitlab
- module CycleAnalytics
- class PlanEvent < BaseEvent
- def initialize(*args)
- @stage = :plan
- @start_time_attrs = issue_metrics_table[:first_associated_with_milestone_at]
- @end_time_attrs = [issue_metrics_table[:first_added_to_board_at],
- issue_metrics_table[:first_mentioned_in_commit_at]]
- @projections = [mr_diff_table[:st_commits].as('commits'),
- issue_metrics_table[:first_mentioned_in_commit_at]]
-
- super(*args)
- end
-
- def custom_query(base_query)
- base_query.join(mr_diff_table).on(mr_diff_table[:merge_request_id].eq(mr_table[:id]))
- end
-
- private
-
- def serialize(event)
- st_commit = first_time_reference_commit(event.delete('commits'), event)
-
- return unless st_commit
-
- serialize_commit(event, st_commit, query)
- end
-
- def first_time_reference_commit(commits, event)
- return nil if commits.blank?
-
- YAML.load(commits).find do |commit|
- next unless commit[:committed_date] && event['first_mentioned_in_commit_at']
-
- commit[:committed_date].to_i == DateTime.parse(event['first_mentioned_in_commit_at'].to_s).to_i
- end
- end
-
- def serialize_commit(event, st_commit, query)
- commit = Commit.new(Gitlab::Git::Commit.new(st_commit), @project)
-
- AnalyticsCommitSerializer.new(project: @project, total_time: event['total_time']).represent(commit).as_json
- end
- end
- end
-end
diff --git a/lib/gitlab/cycle_analytics/plan_event_fetcher.rb b/lib/gitlab/cycle_analytics/plan_event_fetcher.rb
new file mode 100644
index 00000000000..7d342a2d2cb
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/plan_event_fetcher.rb
@@ -0,0 +1,44 @@
+module Gitlab
+ module CycleAnalytics
+ class PlanEventFetcher < BaseEventFetcher
+ def initialize(*args)
+ @projections = [mr_diff_table[:st_commits].as('commits'),
+ issue_metrics_table[:first_mentioned_in_commit_at]]
+
+ super(*args)
+ end
+
+ def events_query
+ base_query.join(mr_diff_table).on(mr_diff_table[:merge_request_id].eq(mr_table[:id]))
+
+ super
+ end
+
+ private
+
+ def serialize(event)
+ st_commit = first_time_reference_commit(event.delete('commits'), event)
+
+ return unless st_commit
+
+ serialize_commit(event, st_commit, query)
+ end
+
+ def first_time_reference_commit(commits, event)
+ return nil if commits.blank?
+
+ YAML.load(commits).find do |commit|
+ next unless commit[:committed_date] && event['first_mentioned_in_commit_at']
+
+ commit[:committed_date].to_i == DateTime.parse(event['first_mentioned_in_commit_at'].to_s).to_i
+ end
+ end
+
+ def serialize_commit(event, st_commit, query)
+ commit = Commit.new(Gitlab::Git::Commit.new(st_commit), @project)
+
+ AnalyticsCommitSerializer.new(project: @project, total_time: event['total_time']).represent(commit)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cycle_analytics/plan_stage.rb b/lib/gitlab/cycle_analytics/plan_stage.rb
new file mode 100644
index 00000000000..3b4dfc6a30e
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/plan_stage.rb
@@ -0,0 +1,22 @@
+module Gitlab
+ module CycleAnalytics
+ class PlanStage < BaseStage
+ def start_time_attrs
+ @start_time_attrs ||= [issue_metrics_table[:first_associated_with_milestone_at],
+ issue_metrics_table[:first_added_to_board_at]]
+ end
+
+ def end_time_attrs
+ @end_time_attrs ||= issue_metrics_table[:first_mentioned_in_commit_at]
+ end
+
+ def name
+ :plan
+ end
+
+ def description
+ "Time before an issue starts implementation"
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cycle_analytics/production_event.rb b/lib/gitlab/cycle_analytics/production_event.rb
deleted file mode 100644
index 4868c3c6237..00000000000
--- a/lib/gitlab/cycle_analytics/production_event.rb
+++ /dev/null
@@ -1,26 +0,0 @@
-module Gitlab
- module CycleAnalytics
- class ProductionEvent < BaseEvent
- include IssueAllowed
-
- def initialize(*args)
- @stage = :production
- @start_time_attrs = issue_table[:created_at]
- @end_time_attrs = mr_metrics_table[:first_deployed_to_production_at]
- @projections = [issue_table[:title],
- issue_table[:iid],
- issue_table[:id],
- issue_table[:created_at],
- issue_table[:author_id]]
-
- super(*args)
- end
-
- private
-
- def serialize(event)
- AnalyticsIssueSerializer.new(project: @project).represent(event).as_json
- end
- end
- end
-end
diff --git a/lib/gitlab/cycle_analytics/production_event_fetcher.rb b/lib/gitlab/cycle_analytics/production_event_fetcher.rb
new file mode 100644
index 00000000000..0fa2e87f673
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/production_event_fetcher.rb
@@ -0,0 +1,6 @@
+module Gitlab
+ module CycleAnalytics
+ class ProductionEventFetcher < IssueEventFetcher
+ end
+ end
+end
diff --git a/lib/gitlab/cycle_analytics/production_helper.rb b/lib/gitlab/cycle_analytics/production_helper.rb
new file mode 100644
index 00000000000..d693443bfa4
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/production_helper.rb
@@ -0,0 +1,9 @@
+module Gitlab
+ module CycleAnalytics
+ module ProductionHelper
+ def stage_query
+ super.where(mr_metrics_table[:first_deployed_to_production_at].gteq(@options[:from]))
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cycle_analytics/production_stage.rb b/lib/gitlab/cycle_analytics/production_stage.rb
new file mode 100644
index 00000000000..2a6bcc80116
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/production_stage.rb
@@ -0,0 +1,28 @@
+module Gitlab
+ module CycleAnalytics
+ class ProductionStage < BaseStage
+ include ProductionHelper
+
+ def start_time_attrs
+ @start_time_attrs ||= issue_table[:created_at]
+ end
+
+ def end_time_attrs
+ @end_time_attrs ||= mr_metrics_table[:first_deployed_to_production_at]
+ end
+
+ def name
+ :production
+ end
+
+ def description
+ "From issue creation until deploy to production"
+ end
+
+ def query
+ # Limit to merge requests that have been deployed to production after `@from`
+ query.where(mr_metrics_table[:first_deployed_to_production_at].gteq(@from))
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cycle_analytics/review_event.rb b/lib/gitlab/cycle_analytics/review_event.rb
deleted file mode 100644
index b394a02cc52..00000000000
--- a/lib/gitlab/cycle_analytics/review_event.rb
+++ /dev/null
@@ -1,25 +0,0 @@
-module Gitlab
- module CycleAnalytics
- class ReviewEvent < BaseEvent
- include MergeRequestAllowed
-
- def initialize(*args)
- @stage = :review
- @start_time_attrs = mr_table[:created_at]
- @end_time_attrs = mr_metrics_table[:merged_at]
- @projections = [mr_table[:title],
- mr_table[:iid],
- mr_table[:id],
- mr_table[:created_at],
- mr_table[:state],
- mr_table[:author_id]]
-
- super(*args)
- end
-
- def serialize(event)
- AnalyticsMergeRequestSerializer.new(project: @project).represent(event).as_json
- end
- end
- end
-end
diff --git a/lib/gitlab/cycle_analytics/review_event_fetcher.rb b/lib/gitlab/cycle_analytics/review_event_fetcher.rb
new file mode 100644
index 00000000000..4c7b3f4467f
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/review_event_fetcher.rb
@@ -0,0 +1,22 @@
+module Gitlab
+ module CycleAnalytics
+ class ReviewEventFetcher < BaseEventFetcher
+ include MergeRequestAllowed
+
+ def initialize(*args)
+ @projections = [mr_table[:title],
+ mr_table[:iid],
+ mr_table[:id],
+ mr_table[:created_at],
+ mr_table[:state],
+ mr_table[:author_id]]
+
+ super(*args)
+ end
+
+ def serialize(event)
+ AnalyticsMergeRequestSerializer.new(project: @project).represent(event)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cycle_analytics/review_stage.rb b/lib/gitlab/cycle_analytics/review_stage.rb
new file mode 100644
index 00000000000..fbaa3010d81
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/review_stage.rb
@@ -0,0 +1,21 @@
+module Gitlab
+ module CycleAnalytics
+ class ReviewStage < BaseStage
+ def start_time_attrs
+ @start_time_attrs ||= mr_table[:created_at]
+ end
+
+ def end_time_attrs
+ @end_time_attrs ||= mr_metrics_table[:merged_at]
+ end
+
+ def name
+ :review
+ end
+
+ def description
+ "Time between merge request creation and merge/close"
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cycle_analytics/stage.rb b/lib/gitlab/cycle_analytics/stage.rb
new file mode 100644
index 00000000000..28e0455df59
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/stage.rb
@@ -0,0 +1,9 @@
+module Gitlab
+ module CycleAnalytics
+ module Stage
+ def self.[](stage_name)
+ CycleAnalytics.const_get("#{stage_name.to_s.camelize}Stage")
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cycle_analytics/stage_summary.rb b/lib/gitlab/cycle_analytics/stage_summary.rb
new file mode 100644
index 00000000000..fc77bd86097
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/stage_summary.rb
@@ -0,0 +1,23 @@
+module Gitlab
+ module CycleAnalytics
+ class StageSummary
+ def initialize(project, from:, current_user:)
+ @project = project
+ @from = from
+ @current_user = current_user
+ end
+
+ def data
+ [serialize(Summary::Issue.new(project: @project, from: @from, current_user: @current_user)),
+ serialize(Summary::Commit.new(project: @project, from: @from)),
+ serialize(Summary::Deploy.new(project: @project, from: @from))]
+ end
+
+ private
+
+ def serialize(summary_object)
+ AnalyticsSummarySerializer.new.represent(summary_object)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cycle_analytics/staging_event.rb b/lib/gitlab/cycle_analytics/staging_event.rb
deleted file mode 100644
index a1f30b716f6..00000000000
--- a/lib/gitlab/cycle_analytics/staging_event.rb
+++ /dev/null
@@ -1,31 +0,0 @@
-module Gitlab
- module CycleAnalytics
- class StagingEvent < BaseEvent
- def initialize(*args)
- @stage = :staging
- @start_time_attrs = mr_metrics_table[:merged_at]
- @end_time_attrs = mr_metrics_table[:first_deployed_to_production_at]
- @projections = [build_table[:id]]
- @order = build_table[:created_at]
-
- super(*args)
- end
-
- def fetch
- Updater.update!(event_result, from: 'id', to: 'build', klass: ::Ci::Build)
-
- super
- end
-
- def custom_query(base_query)
- base_query.join(build_table).on(mr_metrics_table[:pipeline_id].eq(build_table[:commit_id]))
- end
-
- private
-
- def serialize(event)
- AnalyticsBuildSerializer.new.represent(event['build']).as_json
- end
- end
- end
-end
diff --git a/lib/gitlab/cycle_analytics/staging_event_fetcher.rb b/lib/gitlab/cycle_analytics/staging_event_fetcher.rb
new file mode 100644
index 00000000000..36c0260dbfe
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/staging_event_fetcher.rb
@@ -0,0 +1,30 @@
+module Gitlab
+ module CycleAnalytics
+ class StagingEventFetcher < BaseEventFetcher
+ def initialize(*args)
+ @projections = [build_table[:id]]
+ @order = build_table[:created_at]
+
+ super(*args)
+ end
+
+ def fetch
+ Updater.update!(event_result, from: 'id', to: 'build', klass: ::Ci::Build)
+
+ super
+ end
+
+ def events_query
+ base_query.join(build_table).on(mr_metrics_table[:pipeline_id].eq(build_table[:commit_id]))
+
+ super
+ end
+
+ private
+
+ def serialize(event)
+ AnalyticsBuildSerializer.new.represent(event['build'])
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cycle_analytics/staging_stage.rb b/lib/gitlab/cycle_analytics/staging_stage.rb
new file mode 100644
index 00000000000..945909a4d62
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/staging_stage.rb
@@ -0,0 +1,22 @@
+module Gitlab
+ module CycleAnalytics
+ class StagingStage < BaseStage
+ include ProductionHelper
+ def start_time_attrs
+ @start_time_attrs ||= mr_metrics_table[:merged_at]
+ end
+
+ def end_time_attrs
+ @end_time_attrs ||= mr_metrics_table[:first_deployed_to_production_at]
+ end
+
+ def name
+ :staging
+ end
+
+ def description
+ "From merge request merge until deploy to production"
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cycle_analytics/summary/base.rb b/lib/gitlab/cycle_analytics/summary/base.rb
new file mode 100644
index 00000000000..43fa3795e5c
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/summary/base.rb
@@ -0,0 +1,20 @@
+module Gitlab
+ module CycleAnalytics
+ module Summary
+ class Base
+ def initialize(project:, from:)
+ @project = project
+ @from = from
+ end
+
+ def title
+ self.class.name.demodulize
+ end
+
+ def value
+ raise NotImplementedError.new("Expected #{self.name} to implement value")
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cycle_analytics/summary/commit.rb b/lib/gitlab/cycle_analytics/summary/commit.rb
new file mode 100644
index 00000000000..7b8faa4d854
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/summary/commit.rb
@@ -0,0 +1,39 @@
+module Gitlab
+ module CycleAnalytics
+ module Summary
+ class Commit < Base
+ def value
+ @value ||= count_commits
+ end
+
+ private
+
+ # Don't use the `Gitlab::Git::Repository#log` method, because it enforces
+ # a limit. Since we need a commit count, we _can't_ enforce a limit, so
+ # the easiest way forward is to replicate the relevant portions of the
+ # `log` function here.
+ def count_commits
+ return unless ref
+
+ repository = @project.repository.raw_repository
+ sha = @project.repository.commit(ref).sha
+
+ cmd = %W(git --git-dir=#{repository.path} log)
+ cmd << '--format=%H'
+ cmd << "--after=#{@from.iso8601}"
+ cmd << sha
+
+ output, status = Gitlab::Popen.popen(cmd)
+
+ raise IOError, output unless status.zero?
+
+ output.lines.count
+ end
+
+ def ref
+ @ref ||= @project.default_branch.presence
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cycle_analytics/summary/deploy.rb b/lib/gitlab/cycle_analytics/summary/deploy.rb
new file mode 100644
index 00000000000..06032e9200e
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/summary/deploy.rb
@@ -0,0 +1,11 @@
+module Gitlab
+ module CycleAnalytics
+ module Summary
+ class Deploy < Base
+ def value
+ @value ||= @project.deployments.where("created_at > ?", @from).count
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cycle_analytics/summary/issue.rb b/lib/gitlab/cycle_analytics/summary/issue.rb
new file mode 100644
index 00000000000..008468f24b9
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/summary/issue.rb
@@ -0,0 +1,21 @@
+module Gitlab
+ module CycleAnalytics
+ module Summary
+ class Issue < Base
+ def initialize(project:, from:, current_user:)
+ @project = project
+ @from = from
+ @current_user = current_user
+ end
+
+ def title
+ 'New Issue'
+ end
+
+ def value
+ @value ||= IssuesFinder.new(@current_user, project_id: @project.id).execute.created_after(@from).count
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cycle_analytics/test_event.rb b/lib/gitlab/cycle_analytics/test_event.rb
deleted file mode 100644
index d553d0b5aec..00000000000
--- a/lib/gitlab/cycle_analytics/test_event.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-module Gitlab
- module CycleAnalytics
- class TestEvent < StagingEvent
- def initialize(*args)
- super(*args)
-
- @stage = :test
- @start_time_attrs = mr_metrics_table[:latest_build_started_at]
- @end_time_attrs = mr_metrics_table[:latest_build_finished_at]
- end
- end
- end
-end
diff --git a/lib/gitlab/cycle_analytics/test_event_fetcher.rb b/lib/gitlab/cycle_analytics/test_event_fetcher.rb
new file mode 100644
index 00000000000..a2589c6601a
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/test_event_fetcher.rb
@@ -0,0 +1,6 @@
+module Gitlab
+ module CycleAnalytics
+ class TestEventFetcher < StagingEventFetcher
+ end
+ end
+end
diff --git a/lib/gitlab/cycle_analytics/test_stage.rb b/lib/gitlab/cycle_analytics/test_stage.rb
new file mode 100644
index 00000000000..0079d56e0e4
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/test_stage.rb
@@ -0,0 +1,29 @@
+module Gitlab
+ module CycleAnalytics
+ class TestStage < BaseStage
+ def start_time_attrs
+ @start_time_attrs ||= mr_metrics_table[:latest_build_started_at]
+ end
+
+ def end_time_attrs
+ @end_time_attrs ||= mr_metrics_table[:latest_build_finished_at]
+ end
+
+ def name
+ :test
+ end
+
+ def description
+ "Total test time for all commits/merges"
+ end
+
+ def stage_query
+ if @options[:branch]
+ super.where(build_table[:ref].eq(@options[:branch]))
+ else
+ super
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database.rb b/lib/gitlab/database.rb
index 55b8f888d53..dc2537d36aa 100644
--- a/lib/gitlab/database.rb
+++ b/lib/gitlab/database.rb
@@ -35,6 +35,20 @@ module Gitlab
order
end
+ def self.nulls_first_order(field, direction = 'ASC')
+ order = "#{field} #{direction}"
+
+ if Gitlab::Database.postgresql?
+ order << ' NULLS FIRST'
+ else
+ # `field IS NULL` will be `0` for non-NULL columns and `1` for NULL
+ # columns. In the (default) ascending order, `0` comes first.
+ order.prepend("#{field} IS NULL, ") if direction == 'DESC'
+ end
+
+ order
+ end
+
def self.random
Gitlab::Database.postgresql? ? "RANDOM()" : "RAND()"
end
diff --git a/lib/gitlab/database/median.rb b/lib/gitlab/database/median.rb
index 1444d25ebc7..08607c27c09 100644
--- a/lib/gitlab/database/median.rb
+++ b/lib/gitlab/database/median.rb
@@ -103,6 +103,11 @@ module Gitlab
Arel.sql(%Q{EXTRACT(EPOCH FROM "#{arel_attribute.relation.name}"."#{arel_attribute.name}")})
end
+ def extract_diff_epoch(diff)
+ return diff unless Gitlab::Database.postgresql?
+
+ Arel.sql(%Q{EXTRACT(EPOCH FROM (#{diff.to_sql}))})
+ end
# Need to cast '0' to an INTERVAL before we can check if the interval is positive
def zero_interval
Arel::Nodes::NamedFunction.new("CAST", [Arel.sql("'0' AS INTERVAL")])
diff --git a/lib/gitlab/diff/file_collection/merge_request_diff.rb b/lib/gitlab/diff/file_collection/merge_request_diff.rb
index 56530448f36..329d12f13d1 100644
--- a/lib/gitlab/diff/file_collection/merge_request_diff.rb
+++ b/lib/gitlab/diff/file_collection/merge_request_diff.rb
@@ -61,7 +61,10 @@ module Gitlab
end
def cacheable?(diff_file)
- @merge_request_diff.present? && diff_file.blob && diff_file.blob.text?
+ @merge_request_diff.present? &&
+ diff_file.blob &&
+ diff_file.blob.text? &&
+ @project.repository.diffable?(diff_file.blob)
end
def cache_key
diff --git a/lib/gitlab/email/handler.rb b/lib/gitlab/email/handler.rb
index bd3267e2a80..bd2f5d3615e 100644
--- a/lib/gitlab/email/handler.rb
+++ b/lib/gitlab/email/handler.rb
@@ -1,10 +1,11 @@
require 'gitlab/email/handler/create_note_handler'
require 'gitlab/email/handler/create_issue_handler'
+require 'gitlab/email/handler/unsubscribe_handler'
module Gitlab
module Email
module Handler
- HANDLERS = [CreateNoteHandler, CreateIssueHandler]
+ HANDLERS = [UnsubscribeHandler, CreateNoteHandler, CreateIssueHandler]
def self.for(mail, mail_key)
HANDLERS.find do |klass|
diff --git a/lib/gitlab/email/handler/base_handler.rb b/lib/gitlab/email/handler/base_handler.rb
index 7cccf465334..3f6ace0311a 100644
--- a/lib/gitlab/email/handler/base_handler.rb
+++ b/lib/gitlab/email/handler/base_handler.rb
@@ -9,52 +9,13 @@ module Gitlab
@mail_key = mail_key
end
- def message
- @message ||= process_message
- end
-
- def author
+ def can_execute?
raise NotImplementedError
end
- def project
+ def execute
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
diff --git a/lib/gitlab/email/handler/create_issue_handler.rb b/lib/gitlab/email/handler/create_issue_handler.rb
index 9f90a3ec2b2..b8ec9138c10 100644
--- a/lib/gitlab/email/handler/create_issue_handler.rb
+++ b/lib/gitlab/email/handler/create_issue_handler.rb
@@ -5,6 +5,7 @@ module Gitlab
module Email
module Handler
class CreateIssueHandler < BaseHandler
+ include ReplyProcessing
attr_reader :project_path, :incoming_email_token
def initialize(mail, mail_key)
@@ -33,7 +34,7 @@ module Gitlab
end
def project
- @project ||= Project.find_with_namespace(project_path)
+ @project ||= Project.find_by_full_path(project_path)
end
private
diff --git a/lib/gitlab/email/handler/create_note_handler.rb b/lib/gitlab/email/handler/create_note_handler.rb
index 447c7a6a6b9..d87ba427f4b 100644
--- a/lib/gitlab/email/handler/create_note_handler.rb
+++ b/lib/gitlab/email/handler/create_note_handler.rb
@@ -1,10 +1,13 @@
require 'gitlab/email/handler/base_handler'
+require 'gitlab/email/handler/reply_processing'
module Gitlab
module Email
module Handler
class CreateNoteHandler < BaseHandler
+ include ReplyProcessing
+
def can_handle?
mail_key =~ /\A\w+\z/
end
@@ -24,6 +27,8 @@ module Gitlab
record_name: 'comment')
end
+ private
+
def author
sent_notification.recipient
end
@@ -36,8 +41,6 @@ module Gitlab
@sent_notification ||= SentNotification.for(mail_key)
end
- private
-
def create_note
Notes::CreateService.new(
project,
diff --git a/lib/gitlab/email/handler/reply_processing.rb b/lib/gitlab/email/handler/reply_processing.rb
new file mode 100644
index 00000000000..32c5caf93e8
--- /dev/null
+++ b/lib/gitlab/email/handler/reply_processing.rb
@@ -0,0 +1,54 @@
+module Gitlab
+ module Email
+ module Handler
+ module ReplyProcessing
+ private
+
+ def author
+ raise NotImplementedError
+ end
+
+ def project
+ raise NotImplementedError
+ end
+
+ def message
+ @message ||= process_message
+ 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 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 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/unsubscribe_handler.rb b/lib/gitlab/email/handler/unsubscribe_handler.rb
new file mode 100644
index 00000000000..97d7a8d65ff
--- /dev/null
+++ b/lib/gitlab/email/handler/unsubscribe_handler.rb
@@ -0,0 +1,32 @@
+require 'gitlab/email/handler/base_handler'
+
+module Gitlab
+ module Email
+ module Handler
+ class UnsubscribeHandler < BaseHandler
+ def can_handle?
+ mail_key =~ /\A\w+#{Regexp.escape(Gitlab::IncomingEmail::UNSUBSCRIBE_SUFFIX)}\z/
+ end
+
+ def execute
+ raise SentNotificationNotFoundError unless sent_notification
+ return unless sent_notification.unsubscribable?
+
+ noteable = sent_notification.noteable
+ raise NoteableNotFoundError unless noteable
+ noteable.unsubscribe(sent_notification.recipient)
+ end
+
+ private
+
+ def sent_notification
+ @sent_notification ||= SentNotification.for(reply_key)
+ end
+
+ def reply_key
+ mail_key.sub(Gitlab::IncomingEmail::UNSUBSCRIBE_SUFFIX, '')
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/email/receiver.rb b/lib/gitlab/email/receiver.rb
index a40c44eb1bc..b64db5d01ae 100644
--- a/lib/gitlab/email/receiver.rb
+++ b/lib/gitlab/email/receiver.rb
@@ -35,6 +35,8 @@ module Gitlab
handler.execute
end
+ private
+
def build_mail
Mail::Message.new(@raw)
rescue Encoding::UndefinedConversionError,
@@ -54,7 +56,24 @@ module Gitlab
end
def key_from_additional_headers(mail)
- Array(mail.references).find do |mail_id|
+ references = ensure_references_array(mail.references)
+
+ find_key_from_references(references)
+ end
+
+ def ensure_references_array(references)
+ case references
+ when Array
+ references
+ when String
+ # Handle emails from clients which append with commas,
+ # example clients are Microsoft exchange and iOS app
+ Gitlab::IncomingEmail.scan_fallback_references(references)
+ end
+ end
+
+ def find_key_from_references(references)
+ references.find do |mail_id|
key = Gitlab::IncomingEmail.key_from_fallback_message_id(mail_id)
break key if key
end
diff --git a/lib/gitlab/email/reply_parser.rb b/lib/gitlab/email/reply_parser.rb
index f586c5ab062..8c8dd1b9cef 100644
--- a/lib/gitlab/email/reply_parser.rb
+++ b/lib/gitlab/email/reply_parser.rb
@@ -13,9 +13,17 @@ module Gitlab
encoding = body.encoding
- body = discourse_email_trimmer(body)
+ body = EmailReplyTrimmer.trim(body)
- body = EmailReplyParser.parse_reply(body)
+ return '' unless body
+
+ # not using /\s+$/ here because that deletes empty lines
+ body = body.gsub(/[ \t]$/, '')
+
+ # NOTE: We currently don't support empty quotes.
+ # EmailReplyTrimmer allows this as a special case,
+ # so we detect it manually here.
+ return "" if body.lines.all? { |l| l.strip.empty? || l.start_with?('>') }
body.force_encoding(encoding).encode("UTF-8")
end
@@ -57,30 +65,6 @@ module Gitlab
rescue
nil
end
-
- REPLYING_HEADER_LABELS = %w(From Sent To Subject Reply To Cc Bcc Date)
- REPLYING_HEADER_REGEX = Regexp.union(REPLYING_HEADER_LABELS.map { |label| "#{label}:" })
-
- def discourse_email_trimmer(body)
- lines = body.scrub.lines.to_a
- range_end = 0
-
- lines.each_with_index do |l, idx|
- # This one might be controversial but so many reply lines have years, times and end with a colon.
- # Let's try it and see how well it works.
- break if (l =~ /\d{4}/ && l =~ /\d:\d\d/ && l =~ /\:$/) ||
- (l =~ /On \w+ \d+,? \d+,?.*wrote:/)
-
- # Headers on subsequent lines
- break if (0..2).all? { |off| lines[idx + off] =~ REPLYING_HEADER_REGEX }
- # Headers on the same line
- break if REPLYING_HEADER_LABELS.count { |label| l.include?(label) } >= 3
-
- range_end = idx
- end
-
- lines[0..range_end].join.strip
- end
end
end
end
diff --git a/lib/gitlab/gfm/reference_rewriter.rb b/lib/gitlab/gfm/reference_rewriter.rb
index a7c596dced0..b984492d369 100644
--- a/lib/gitlab/gfm/reference_rewriter.rb
+++ b/lib/gitlab/gfm/reference_rewriter.rb
@@ -76,7 +76,7 @@ module Gitlab
if referable.respond_to?(:project)
referable.to_reference(target_project)
else
- referable.to_reference(@source_project, target_project)
+ referable.to_reference(@source_project, target_project: target_project)
end
end
diff --git a/lib/gitlab/git.rb b/lib/gitlab/git.rb
index 3cd515e4a3a..d3df3f1bca1 100644
--- a/lib/gitlab/git.rb
+++ b/lib/gitlab/git.rb
@@ -6,7 +6,7 @@ module Gitlab
class << self
def ref_name(ref)
- ref.gsub(/\Arefs\/(tags|heads)\//, '')
+ ref.sub(/\Arefs\/(tags|heads)\//, '')
end
def branch_name(ref)
diff --git a/lib/gitlab/git/attributes.rb b/lib/gitlab/git/attributes.rb
new file mode 100644
index 00000000000..42140ecc993
--- /dev/null
+++ b/lib/gitlab/git/attributes.rb
@@ -0,0 +1,131 @@
+module Gitlab
+ module Git
+ # Class for parsing Git attribute files and extracting the attributes for
+ # file patterns.
+ #
+ # Unlike Rugged this parser only needs a single IO call (a call to `open`),
+ # vastly reducing the time spent in extracting attributes.
+ #
+ # This class _only_ supports parsing the attributes file located at
+ # `$GIT_DIR/info/attributes` as GitLab doesn't use any other files
+ # (`.gitattributes` is copied to this particular path).
+ #
+ # Basic usage:
+ #
+ # attributes = Gitlab::Git::Attributes.new(some_repo.path)
+ #
+ # attributes.attributes('README.md') # => { "eol" => "lf }
+ class Attributes
+ # path - The path to the Git repository.
+ def initialize(path)
+ @path = File.expand_path(path)
+ @patterns = nil
+ end
+
+ # Returns all the Git attributes for the given path.
+ #
+ # path - A path to a file for which to get the attributes.
+ #
+ # Returns a Hash.
+ def attributes(path)
+ full_path = File.join(@path, path)
+
+ patterns.each do |pattern, attrs|
+ return attrs if File.fnmatch?(pattern, full_path)
+ end
+
+ {}
+ end
+
+ # Returns a Hash containing the file patterns and their attributes.
+ def patterns
+ @patterns ||= parse_file
+ end
+
+ # Parses an attribute string.
+ #
+ # These strings can be in the following formats:
+ #
+ # text # => { "text" => true }
+ # -text # => { "text" => false }
+ # key=value # => { "key" => "value" }
+ #
+ # string - The string to parse.
+ #
+ # Returns a Hash containing the attributes and their values.
+ def parse_attributes(string)
+ values = {}
+ dash = '-'
+ equal = '='
+ binary = 'binary'
+
+ string.split(/\s+/).each do |chunk|
+ # Data such as "foo = bar" should be treated as "foo" and "bar" being
+ # separate boolean attributes.
+ next if chunk == equal
+
+ key = chunk
+
+ # Input: "-foo"
+ if chunk.start_with?(dash)
+ key = chunk.byteslice(1, chunk.length - 1)
+ value = false
+
+ # Input: "foo=bar"
+ elsif chunk.include?(equal)
+ key, value = chunk.split(equal, 2)
+
+ # Input: "foo"
+ else
+ value = true
+ end
+
+ values[key] = value
+
+ # When the "binary" option is set the "diff" option should be set to
+ # the inverse. If "diff" is later set it should overwrite the
+ # automatically set value.
+ values['diff'] = false if key == binary && value
+ end
+
+ values
+ end
+
+ # Iterates over every line in the attributes file.
+ def each_line
+ full_path = File.join(@path, 'info/attributes')
+
+ return unless File.exist?(full_path)
+
+ File.open(full_path, 'r') do |handle|
+ handle.each_line do |line|
+ break unless line.valid_encoding?
+
+ yield line.strip
+ end
+ end
+ end
+
+ private
+
+ # Parses the Git attributes file.
+ def parse_file
+ pairs = []
+ comment = '#'
+
+ each_line do |line|
+ next if line.start_with?(comment) || line.empty?
+
+ pattern, attrs = line.split(/\s+/, 2)
+
+ parsed = attrs ? parse_attributes(attrs) : {}
+
+ pairs << [File.join(@path, pattern), parsed]
+ end
+
+ # Newer entries take precedence over older entries.
+ pairs.reverse.to_h
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git/blame.rb b/lib/gitlab/git/blame.rb
new file mode 100644
index 00000000000..58193391926
--- /dev/null
+++ b/lib/gitlab/git/blame.rb
@@ -0,0 +1,75 @@
+module Gitlab
+ module Git
+ class Blame
+ include Gitlab::Git::EncodingHelper
+
+ attr_reader :lines, :blames
+
+ def initialize(repository, sha, path)
+ @repo = repository
+ @sha = sha
+ @path = path
+ @lines = []
+ @blames = load_blame
+ end
+
+ def each
+ @blames.each do |blame|
+ yield(
+ Gitlab::Git::Commit.new(blame.commit),
+ blame.line
+ )
+ end
+ end
+
+ private
+
+ def load_blame
+ cmd = %W(#{Gitlab.config.git.bin_path} --git-dir=#{@repo.path} blame -p #{@sha} -- #{@path})
+ # Read in binary mode to ensure ASCII-8BIT
+ raw_output = IO.popen(cmd, 'rb') {|io| io.read }
+ output = encode_utf8(raw_output)
+ process_raw_blame output
+ end
+
+ def process_raw_blame(output)
+ lines, final = [], []
+ info, commits = {}, {}
+
+ # process the output
+ output.split("\n").each do |line|
+ if line[0, 1] == "\t"
+ lines << line[1, line.size]
+ elsif m = /^(\w{40}) (\d+) (\d+)/.match(line)
+ commit_id, old_lineno, lineno = m[1], m[2].to_i, m[3].to_i
+ commits[commit_id] = nil unless commits.key?(commit_id)
+ info[lineno] = [commit_id, old_lineno]
+ end
+ end
+
+ # load all commits in single call
+ commits.keys.each do |key|
+ commits[key] = @repo.lookup(key)
+ end
+
+ # get it together
+ info.sort.each do |lineno, (commit_id, old_lineno)|
+ commit = commits[commit_id]
+ final << BlameLine.new(lineno, old_lineno, commit, lines[lineno - 1])
+ end
+
+ @lines = final
+ end
+ end
+
+ class BlameLine
+ attr_accessor :lineno, :oldlineno, :commit, :line
+ def initialize(lineno, oldlineno, commit, line)
+ @lineno = lineno
+ @oldlineno = oldlineno
+ @commit = commit
+ @line = line
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git/blob.rb b/lib/gitlab/git/blob.rb
new file mode 100644
index 00000000000..b742d9e1e4b
--- /dev/null
+++ b/lib/gitlab/git/blob.rb
@@ -0,0 +1,330 @@
+module Gitlab
+ module Git
+ class Blob
+ include Linguist::BlobHelper
+ include Gitlab::Git::EncodingHelper
+
+ # This number is the maximum amount of data that we want to display to
+ # the user. We load as much as we can for encoding detection
+ # (Linguist) and LFS pointer parsing. All other cases where we need full
+ # blob data should use load_all_data!.
+ MAX_DATA_DISPLAY_SIZE = 10485760
+
+ attr_accessor :name, :path, :size, :data, :mode, :id, :commit_id, :loaded_size, :binary
+
+ class << self
+ def find(repository, sha, path)
+ commit = repository.lookup(sha)
+ root_tree = commit.tree
+
+ blob_entry = find_entry_by_path(repository, root_tree.oid, path)
+
+ return nil unless blob_entry
+
+ if blob_entry[:type] == :commit
+ submodule_blob(blob_entry, path, sha)
+ else
+ blob = repository.lookup(blob_entry[:oid])
+
+ if blob
+ new(
+ id: blob.oid,
+ name: blob_entry[:name],
+ size: blob.size,
+ data: blob.content(MAX_DATA_DISPLAY_SIZE),
+ mode: blob_entry[:filemode].to_s(8),
+ path: path,
+ commit_id: sha,
+ binary: blob.binary?
+ )
+ end
+ end
+ end
+
+ def raw(repository, sha)
+ blob = repository.lookup(sha)
+
+ new(
+ id: blob.oid,
+ size: blob.size,
+ data: blob.content(MAX_DATA_DISPLAY_SIZE),
+ binary: blob.binary?
+ )
+ end
+
+ # Recursive search of blob id by path
+ #
+ # Ex.
+ # blog/ # oid: 1a
+ # app/ # oid: 2a
+ # models/ # oid: 3a
+ # file.rb # oid: 4a
+ #
+ #
+ # Blob.find_entry_by_path(repo, '1a', 'app/file.rb') # => '4a'
+ #
+ def find_entry_by_path(repository, root_id, path)
+ root_tree = repository.lookup(root_id)
+ # Strip leading slashes
+ path[/^\/*/] = ''
+ path_arr = path.split('/')
+
+ entry = root_tree.find do |entry|
+ entry[:name] == path_arr[0]
+ end
+
+ return nil unless entry
+
+ if path_arr.size > 1
+ return nil unless entry[:type] == :tree
+ path_arr.shift
+ find_entry_by_path(repository, entry[:oid], path_arr.join('/'))
+ else
+ [:blob, :commit].include?(entry[:type]) ? entry : nil
+ end
+ end
+
+ def submodule_blob(blob_entry, path, sha)
+ new(
+ id: blob_entry[:oid],
+ name: blob_entry[:name],
+ data: '',
+ path: path,
+ commit_id: sha,
+ )
+ end
+
+ # Commit file in repository and return commit sha
+ #
+ # options should contain next structure:
+ # file: {
+ # content: 'Lorem ipsum...',
+ # path: 'documents/story.txt',
+ # update: true
+ # },
+ # author: {
+ # email: 'user@example.com',
+ # name: 'Test User',
+ # time: Time.now
+ # },
+ # committer: {
+ # email: 'user@example.com',
+ # name: 'Test User',
+ # time: Time.now
+ # },
+ # commit: {
+ # message: 'Wow such commit',
+ # branch: 'master',
+ # update_ref: false
+ # }
+ #
+ # rubocop:disable Metrics/AbcSize
+ # rubocop:disable Metrics/CyclomaticComplexity
+ # rubocop:disable Metrics/PerceivedComplexity
+ def commit(repository, options, action = :add)
+ file = options[:file]
+ update = file[:update].nil? ? true : file[:update]
+ author = options[:author]
+ committer = options[:committer]
+ commit = options[:commit]
+ repo = repository.rugged
+ ref = commit[:branch]
+ update_ref = commit[:update_ref].nil? ? true : commit[:update_ref]
+ parents = []
+ mode = 0o100644
+
+ unless ref.start_with?('refs/')
+ ref = 'refs/heads/' + ref
+ end
+
+ path_name = Gitlab::Git::PathHelper.normalize_path(file[:path])
+ # Abort if any invalid characters remain (e.g. ../foo)
+ raise Gitlab::Git::Repository::InvalidBlobName.new("Invalid path") if path_name.each_filename.to_a.include?('..')
+
+ filename = path_name.to_s
+ index = repo.index
+
+ unless repo.empty?
+ rugged_ref = repo.references[ref]
+ raise Gitlab::Git::Repository::InvalidRef.new("Invalid branch name") unless rugged_ref
+ last_commit = rugged_ref.target
+ index.read_tree(last_commit.tree)
+ parents = [last_commit]
+ end
+
+ if action == :remove
+ index.remove(filename)
+ else
+ file_entry = index.get(filename)
+
+ if action == :rename
+ old_path_name = Gitlab::Git::PathHelper.normalize_path(file[:previous_path])
+ old_filename = old_path_name.to_s
+ file_entry = index.get(old_filename)
+ index.remove(old_filename) unless file_entry.blank?
+ end
+
+ if file_entry
+ raise Gitlab::Git::Repository::InvalidBlobName.new("Filename already exists; update not allowed") unless update
+
+ # Preserve the current file mode if one is available
+ mode = file_entry[:mode] if file_entry[:mode]
+ end
+
+ content = file[:content]
+ detect = CharlockHolmes::EncodingDetector.new.detect(content) if content
+
+ unless detect && detect[:type] == :binary
+ # When writing to the repo directly as we are doing here,
+ # the `core.autocrlf` config isn't taken into account.
+ content.gsub!("\r\n", "\n") if repository.autocrlf
+ end
+
+ oid = repo.write(content, :blob)
+ index.add(path: filename, oid: oid, mode: mode)
+ end
+
+ opts = {}
+ opts[:tree] = index.write_tree(repo)
+ opts[:author] = author
+ opts[:committer] = committer
+ opts[:message] = commit[:message]
+ opts[:parents] = parents
+ opts[:update_ref] = ref if update_ref
+
+ Rugged::Commit.create(repo, opts)
+ end
+ # rubocop:enable Metrics/AbcSize
+ # rubocop:enable Metrics/CyclomaticComplexity
+ # rubocop:enable Metrics/PerceivedComplexity
+
+ # Remove file from repository and return commit sha
+ #
+ # options should contain next structure:
+ # file: {
+ # path: 'documents/story.txt'
+ # },
+ # author: {
+ # email: 'user@example.com',
+ # name: 'Test User',
+ # time: Time.now
+ # },
+ # committer: {
+ # email: 'user@example.com',
+ # name: 'Test User',
+ # time: Time.now
+ # },
+ # commit: {
+ # message: 'Remove FILENAME',
+ # branch: 'master'
+ # }
+ #
+ def remove(repository, options)
+ commit(repository, options, :remove)
+ end
+
+ # Rename file from repository and return commit sha
+ #
+ # options should contain next structure:
+ # file: {
+ # previous_path: 'documents/old_story.txt'
+ # path: 'documents/story.txt'
+ # content: 'Lorem ipsum...',
+ # update: true
+ # },
+ # author: {
+ # email: 'user@example.com',
+ # name: 'Test User',
+ # time: Time.now
+ # },
+ # committer: {
+ # email: 'user@example.com',
+ # name: 'Test User',
+ # time: Time.now
+ # },
+ # commit: {
+ # message: 'Rename FILENAME',
+ # branch: 'master'
+ # }
+ #
+ def rename(repository, options)
+ commit(repository, options, :rename)
+ end
+ end
+
+ def initialize(options)
+ %w(id name path size data mode commit_id binary).each do |key|
+ self.send("#{key}=", options[key.to_sym])
+ end
+
+ @loaded_all_data = false
+ # Retain the actual size before it is encoded
+ @loaded_size = @data.bytesize if @data
+ end
+
+ def binary?
+ @binary.nil? ? super : @binary == true
+ end
+
+ def empty?
+ !data || data == ''
+ end
+
+ def data
+ encode! @data
+ end
+
+ # Load all blob data (not just the first MAX_DATA_DISPLAY_SIZE bytes) into
+ # memory as a Ruby string.
+ def load_all_data!(repository)
+ return if @data == '' # don't mess with submodule blobs
+ return @data if @loaded_all_data
+
+ @loaded_all_data = true
+ @data = repository.lookup(id).content
+ @loaded_size = @data.bytesize
+ end
+
+ def name
+ encode! @name
+ end
+
+ # Valid LFS object pointer is a text file consisting of
+ # version
+ # oid
+ # size
+ # see https://github.com/github/git-lfs/blob/v1.1.0/docs/spec.md#the-pointer
+ def lfs_pointer?
+ has_lfs_version_key? && lfs_oid.present? && lfs_size.present?
+ end
+
+ def lfs_oid
+ if has_lfs_version_key?
+ oid = data.match(/(?<=sha256:)([0-9a-f]{64})/)
+ return oid[1] if oid
+ end
+
+ nil
+ end
+
+ def lfs_size
+ if has_lfs_version_key?
+ size = data.match(/(?<=size )([0-9]+)/)
+ return size[1] if size
+ end
+
+ nil
+ end
+
+ def truncated?
+ size && (size > loaded_size)
+ end
+
+ private
+
+ def has_lfs_version_key?
+ !empty? && text? && data.start_with?("version https://git-lfs.github.com/spec")
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git/blob_snippet.rb b/lib/gitlab/git/blob_snippet.rb
new file mode 100644
index 00000000000..e98de57fc22
--- /dev/null
+++ b/lib/gitlab/git/blob_snippet.rb
@@ -0,0 +1,32 @@
+module Gitlab
+ module Git
+ class BlobSnippet
+ include Linguist::BlobHelper
+
+ attr_accessor :ref
+ attr_accessor :lines
+ attr_accessor :filename
+ attr_accessor :startline
+
+ def initialize(ref, lines, startline, filename)
+ @ref, @lines, @startline, @filename = ref, lines, startline, filename
+ end
+
+ def data
+ lines.join("\n") if lines
+ end
+
+ def name
+ filename
+ end
+
+ def size
+ data.length
+ end
+
+ def mode
+ nil
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git/branch.rb b/lib/gitlab/git/branch.rb
new file mode 100644
index 00000000000..586380da94a
--- /dev/null
+++ b/lib/gitlab/git/branch.rb
@@ -0,0 +1,6 @@
+module Gitlab
+ module Git
+ class Branch < Ref
+ end
+ end
+end
diff --git a/lib/gitlab/git/commit.rb b/lib/gitlab/git/commit.rb
new file mode 100644
index 00000000000..d785516ebdd
--- /dev/null
+++ b/lib/gitlab/git/commit.rb
@@ -0,0 +1,310 @@
+# Gitlab::Git::Commit is a wrapper around native Rugged::Commit object
+module Gitlab
+ module Git
+ class Commit
+ include Gitlab::Git::EncodingHelper
+
+ attr_accessor :raw_commit, :head, :refs
+
+ SERIALIZE_KEYS = [
+ :id, :message, :parent_ids,
+ :authored_date, :author_name, :author_email,
+ :committed_date, :committer_name, :committer_email
+ ].freeze
+
+ attr_accessor *SERIALIZE_KEYS # rubocop:disable Lint/AmbiguousOperator
+
+ def ==(other)
+ return false unless other.is_a?(Gitlab::Git::Commit)
+
+ methods = [:message, :parent_ids, :authored_date, :author_name,
+ :author_email, :committed_date, :committer_name,
+ :committer_email]
+
+ methods.all? do |method|
+ send(method) == other.send(method)
+ end
+ end
+
+ class << self
+ # Get commits collection
+ #
+ # Ex.
+ # Commit.where(
+ # repo: repo,
+ # ref: 'master',
+ # path: 'app/models',
+ # limit: 10,
+ # offset: 5,
+ # )
+ #
+ def where(options)
+ repo = options.delete(:repo)
+ raise 'Gitlab::Git::Repository is required' unless repo.respond_to?(:log)
+
+ repo.log(options).map { |c| decorate(c) }
+ end
+
+ # Get single commit
+ #
+ # Ex.
+ # Commit.find(repo, '29eda46b')
+ #
+ # Commit.find(repo, 'master')
+ #
+ def find(repo, commit_id = "HEAD")
+ return decorate(commit_id) if commit_id.is_a?(Rugged::Commit)
+
+ obj = if commit_id.is_a?(String)
+ repo.rev_parse_target(commit_id)
+ else
+ Gitlab::Git::Ref.dereference_object(commit_id)
+ end
+
+ return nil unless obj.is_a?(Rugged::Commit)
+
+ decorate(obj)
+ rescue Rugged::ReferenceError, Rugged::InvalidError, Rugged::ObjectError, Gitlab::Git::Repository::NoRepository
+ nil
+ end
+
+ # Get last commit for HEAD
+ #
+ # Ex.
+ # Commit.last(repo)
+ #
+ def last(repo)
+ find(repo)
+ end
+
+ # Get last commit for specified path and ref
+ #
+ # Ex.
+ # Commit.last_for_path(repo, '29eda46b', 'app/models')
+ #
+ # Commit.last_for_path(repo, 'master', 'Gemfile')
+ #
+ def last_for_path(repo, ref, path = nil)
+ where(
+ repo: repo,
+ ref: ref,
+ path: path,
+ limit: 1
+ ).first
+ end
+
+ # Get commits between two revspecs
+ # See also #repository.commits_between
+ #
+ # Ex.
+ # Commit.between(repo, '29eda46b', 'master')
+ #
+ def between(repo, base, head)
+ repo.commits_between(base, head).map do |commit|
+ decorate(commit)
+ end
+ rescue Rugged::ReferenceError
+ []
+ end
+
+ # Delegate Repository#find_commits
+ def find_all(repo, options = {})
+ repo.find_commits(options)
+ end
+
+ def decorate(commit, ref = nil)
+ Gitlab::Git::Commit.new(commit, ref)
+ end
+
+ # Returns a diff object for the changes introduced by +rugged_commit+.
+ # If +rugged_commit+ doesn't have a parent, then the diff is between
+ # this commit and an empty repo. See Repository#diff for the keys
+ # allowed in the +options+ hash.
+ def diff_from_parent(rugged_commit, options = {})
+ options ||= {}
+ break_rewrites = options[:break_rewrites]
+ actual_options = Gitlab::Git::Diff.filter_diff_options(options)
+
+ diff = if rugged_commit.parents.empty?
+ rugged_commit.diff(actual_options.merge(reverse: true))
+ else
+ rugged_commit.parents[0].diff(rugged_commit, actual_options)
+ end
+
+ diff.find_similar!(break_rewrites: break_rewrites)
+ diff
+ end
+ end
+
+ def initialize(raw_commit, head = nil)
+ raise "Nil as raw commit passed" unless raw_commit
+
+ if raw_commit.is_a?(Hash)
+ init_from_hash(raw_commit)
+ elsif raw_commit.is_a?(Rugged::Commit)
+ init_from_rugged(raw_commit)
+ else
+ raise "Invalid raw commit type: #{raw_commit.class}"
+ end
+
+ @head = head
+ end
+
+ def sha
+ id
+ end
+
+ def short_id(length = 10)
+ id.to_s[0..length]
+ end
+
+ def safe_message
+ @safe_message ||= message
+ end
+
+ def created_at
+ committed_date
+ end
+
+ # Was this commit committed by a different person than the original author?
+ def different_committer?
+ author_name != committer_name || author_email != committer_email
+ end
+
+ def parent_id
+ parent_ids.first
+ end
+
+ # Shows the diff between the commit's parent and the commit.
+ #
+ # Cuts out the header and stats from #to_patch and returns only the diff.
+ def to_diff(options = {})
+ diff_from_parent(options).patch
+ end
+
+ # Returns a diff object for the changes from this commit's first parent.
+ # If there is no parent, then the diff is between this commit and an
+ # empty repo. See Repository#diff for keys allowed in the +options+
+ # hash.
+ def diff_from_parent(options = {})
+ Commit.diff_from_parent(raw_commit, options)
+ end
+
+ def has_zero_stats?
+ stats.total.zero?
+ rescue
+ true
+ end
+
+ def no_commit_message
+ "--no commit message"
+ end
+
+ def to_hash
+ serialize_keys.map.with_object({}) do |key, hash|
+ hash[key] = send(key)
+ end
+ end
+
+ def date
+ committed_date
+ end
+
+ def diffs(options = {})
+ Gitlab::Git::DiffCollection.new(diff_from_parent(options), options)
+ end
+
+ def parents
+ raw_commit.parents.map { |c| Gitlab::Git::Commit.new(c) }
+ end
+
+ def tree
+ raw_commit.tree
+ end
+
+ def stats
+ Gitlab::Git::CommitStats.new(self)
+ end
+
+ def to_patch(options = {})
+ begin
+ raw_commit.to_mbox(options)
+ rescue Rugged::InvalidError => ex
+ if ex.message =~ /Commit \w+ is a merge commit/
+ 'Patch format is not currently supported for merge commits.'
+ end
+ end
+ end
+
+ # Get a collection of Rugged::Reference objects for this commit.
+ #
+ # Ex.
+ # commit.ref(repo)
+ #
+ def refs(repo)
+ repo.refs_hash[id]
+ end
+
+ # Get ref names collection
+ #
+ # Ex.
+ # commit.ref_names(repo)
+ #
+ def ref_names(repo)
+ refs(repo).map do |ref|
+ ref.name.sub(%r{^refs/(heads|remotes|tags)/}, "")
+ end
+ end
+
+ def message
+ encode! @message
+ end
+
+ def author_name
+ encode! @author_name
+ end
+
+ def author_email
+ encode! @author_email
+ end
+
+ def committer_name
+ encode! @committer_name
+ end
+
+ def committer_email
+ encode! @committer_email
+ end
+
+ private
+
+ def init_from_hash(hash)
+ raw_commit = hash.symbolize_keys
+
+ serialize_keys.each do |key|
+ send("#{key}=", raw_commit[key])
+ end
+ end
+
+ def init_from_rugged(commit)
+ author = commit.author
+ committer = commit.committer
+
+ @raw_commit = commit
+ @id = commit.oid
+ @message = commit.message
+ @authored_date = author[:time]
+ @committed_date = committer[:time]
+ @author_name = author[:name]
+ @author_email = author[:email]
+ @committer_name = committer[:name]
+ @committer_email = committer[:email]
+ @parent_ids = commit.parents.map(&:oid)
+ end
+
+ def serialize_keys
+ SERIALIZE_KEYS
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git/commit_stats.rb b/lib/gitlab/git/commit_stats.rb
new file mode 100644
index 00000000000..e9118bbed0e
--- /dev/null
+++ b/lib/gitlab/git/commit_stats.rb
@@ -0,0 +1,26 @@
+# Gitlab::Git::CommitStats counts the additions, deletions, and total changes
+# in a commit.
+module Gitlab
+ module Git
+ class CommitStats
+ attr_reader :id, :additions, :deletions, :total
+
+ # Instantiate a CommitStats object
+ def initialize(commit)
+ @id = commit.id
+ @additions = 0
+ @deletions = 0
+ @total = 0
+
+ diff = commit.diff_from_parent
+
+ diff.each_patch do |p|
+ # TODO: Use the new Rugged convenience methods when they're released
+ @additions += p.stat[0]
+ @deletions += p.stat[1]
+ @total += p.changes
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git/compare.rb b/lib/gitlab/git/compare.rb
new file mode 100644
index 00000000000..696a2acd5e3
--- /dev/null
+++ b/lib/gitlab/git/compare.rb
@@ -0,0 +1,43 @@
+module Gitlab
+ module Git
+ class Compare
+ attr_reader :head, :base, :straight
+
+ def initialize(repository, base, head, straight = false)
+ @repository = repository
+ @straight = straight
+
+ unless base && head
+ @commits = []
+ return
+ end
+
+ @base = Gitlab::Git::Commit.find(repository, base.try(:strip))
+ @head = Gitlab::Git::Commit.find(repository, head.try(:strip))
+
+ @commits = [] unless @base && @head
+ @commits = [] if same
+ end
+
+ def same
+ @base && @head && @base.id == @head.id
+ end
+
+ def commits
+ return @commits if defined?(@commits)
+
+ @commits = Gitlab::Git::Commit.between(@repository, @base.id, @head.id)
+ end
+
+ def diffs(options = {})
+ unless @head && @base
+ return Gitlab::Git::DiffCollection.new([])
+ end
+
+ paths = options.delete(:paths) || []
+ options[:straight] = @straight
+ Gitlab::Git::Diff.between(@repository, @head.id, @base.id, options, *paths)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git/diff.rb b/lib/gitlab/git/diff.rb
new file mode 100644
index 00000000000..d6b3b5705a9
--- /dev/null
+++ b/lib/gitlab/git/diff.rb
@@ -0,0 +1,322 @@
+# Gitlab::Git::Diff is a wrapper around native Rugged::Diff object
+module Gitlab
+ module Git
+ class Diff
+ class TimeoutError < StandardError; end
+ include Gitlab::Git::EncodingHelper
+
+ # Diff properties
+ attr_accessor :old_path, :new_path, :a_mode, :b_mode, :diff
+
+ # Stats properties
+ attr_accessor :new_file, :renamed_file, :deleted_file
+
+ attr_accessor :too_large
+
+ # The maximum size of a diff to display.
+ DIFF_SIZE_LIMIT = 102400 # 100 KB
+
+ # The maximum size before a diff is collapsed.
+ DIFF_COLLAPSE_LIMIT = 10240 # 10 KB
+
+ class << self
+ def between(repo, head, base, options = {}, *paths)
+ straight = options.delete(:straight) || false
+
+ common_commit = if straight
+ base
+ else
+ # Only show what is new in the source branch
+ # compared to the target branch, not the other way
+ # around. The linex below with merge_base is
+ # equivalent to diff with three dots (git diff
+ # branch1...branch2) From the git documentation:
+ # "git diff A...B" is equivalent to "git diff
+ # $(git-merge-base A B) B"
+ repo.merge_base_commit(head, base)
+ end
+
+ options ||= {}
+ actual_options = filter_diff_options(options)
+ repo.diff(common_commit, head, actual_options, *paths)
+ end
+
+ # Return a copy of the +options+ hash containing only keys that can be
+ # passed to Rugged. Allowed options are:
+ #
+ # :max_size ::
+ # An integer specifying the maximum byte size of a file before a it
+ # will be treated as binary. The default value is 512MB.
+ #
+ # :context_lines ::
+ # The number of unchanged lines that define the boundary of a hunk
+ # (and to display before and after the actual changes). The default is
+ # 3.
+ #
+ # :interhunk_lines ::
+ # The maximum number of unchanged lines between hunk boundaries before
+ # the hunks will be merged into a one. The default is 0.
+ #
+ # :old_prefix ::
+ # The virtual "directory" to prefix to old filenames in hunk headers.
+ # The default is "a".
+ #
+ # :new_prefix ::
+ # The virtual "directory" to prefix to new filenames in hunk headers.
+ # The default is "b".
+ #
+ # :reverse ::
+ # If true, the sides of the diff will be reversed.
+ #
+ # :force_text ::
+ # If true, all files will be treated as text, disabling binary
+ # attributes & detection.
+ #
+ # :ignore_whitespace ::
+ # If true, all whitespace will be ignored.
+ #
+ # :ignore_whitespace_change ::
+ # If true, changes in amount of whitespace will be ignored.
+ #
+ # :ignore_whitespace_eol ::
+ # If true, whitespace at end of line will be ignored.
+ #
+ # :ignore_submodules ::
+ # if true, submodules will be excluded from the diff completely.
+ #
+ # :patience ::
+ # If true, the "patience diff" algorithm will be used (currenlty
+ # unimplemented).
+ #
+ # :include_ignored ::
+ # If true, ignored files will be included in the diff.
+ #
+ # :include_untracked ::
+ # If true, untracked files will be included in the diff.
+ #
+ # :include_unmodified ::
+ # If true, unmodified files will be included in the diff.
+ #
+ # :recurse_untracked_dirs ::
+ # Even if +:include_untracked+ is true, untracked directories will
+ # only be marked with a single entry in the diff. If this flag is set
+ # to true, all files under ignored directories will be included in the
+ # diff, too.
+ #
+ # :disable_pathspec_match ::
+ # If true, the given +*paths+ will be applied as exact matches,
+ # instead of as fnmatch patterns.
+ #
+ # :deltas_are_icase ::
+ # If true, filename comparisons will be made with case-insensitivity.
+ #
+ # :include_untracked_content ::
+ # if true, untracked content will be contained in the the diff patch
+ # text.
+ #
+ # :skip_binary_check ::
+ # If true, diff deltas will be generated without spending time on
+ # binary detection. This is useful to improve performance in cases
+ # where the actual file content difference is not needed.
+ #
+ # :include_typechange ::
+ # If true, type changes for files will not be interpreted as deletion
+ # of the "old file" and addition of the "new file", but will generate
+ # typechange records.
+ #
+ # :include_typechange_trees ::
+ # Even if +:include_typechange+ is true, blob -> tree changes will
+ # still usually be handled as a deletion of the blob. If this flag is
+ # set to true, blob -> tree changes will be marked as typechanges.
+ #
+ # :ignore_filemode ::
+ # If true, file mode changes will be ignored.
+ #
+ # :recurse_ignored_dirs ::
+ # Even if +:include_ignored+ is true, ignored directories will only be
+ # marked with a single entry in the diff. If this flag is set to true,
+ # all files under ignored directories will be included in the diff,
+ # too.
+ def filter_diff_options(options, default_options = {})
+ allowed_options = [:max_size, :context_lines, :interhunk_lines,
+ :old_prefix, :new_prefix, :reverse, :force_text,
+ :ignore_whitespace, :ignore_whitespace_change,
+ :ignore_whitespace_eol, :ignore_submodules,
+ :patience, :include_ignored, :include_untracked,
+ :include_unmodified, :recurse_untracked_dirs,
+ :disable_pathspec_match, :deltas_are_icase,
+ :include_untracked_content, :skip_binary_check,
+ :include_typechange, :include_typechange_trees,
+ :ignore_filemode, :recurse_ignored_dirs, :paths,
+ :max_files, :max_lines, :all_diffs, :no_collapse]
+
+ if default_options
+ actual_defaults = default_options.dup
+ actual_defaults.keep_if do |key|
+ allowed_options.include?(key)
+ end
+ else
+ actual_defaults = {}
+ end
+
+ if options
+ filtered_opts = options.dup
+ filtered_opts.keep_if do |key|
+ allowed_options.include?(key)
+ end
+ filtered_opts = actual_defaults.merge(filtered_opts)
+ else
+ filtered_opts = actual_defaults
+ end
+
+ filtered_opts
+ end
+ end
+
+ def initialize(raw_diff, collapse: false)
+ case raw_diff
+ when Hash
+ init_from_hash(raw_diff, collapse: collapse)
+ when Rugged::Patch, Rugged::Diff::Delta
+ init_from_rugged(raw_diff, collapse: collapse)
+ when nil
+ raise "Nil as raw diff passed"
+ else
+ raise "Invalid raw diff type: #{raw_diff.class}"
+ end
+ end
+
+ def serialize_keys
+ @serialize_keys ||= %i(diff new_path old_path a_mode b_mode new_file renamed_file deleted_file too_large)
+ end
+
+ def to_hash
+ hash = {}
+
+ keys = serialize_keys
+
+ keys.each do |key|
+ hash[key] = send(key)
+ end
+
+ hash
+ end
+
+ def submodule?
+ a_mode == '160000' || b_mode == '160000'
+ end
+
+ def line_count
+ @line_count ||= Util.count_lines(@diff)
+ end
+
+ def too_large?
+ if @too_large.nil?
+ @too_large = @diff.bytesize >= DIFF_SIZE_LIMIT
+ else
+ @too_large
+ end
+ end
+
+ def collapsible?
+ @diff.bytesize >= DIFF_COLLAPSE_LIMIT
+ end
+
+ def prune_large_diff!
+ @diff = ''
+ @line_count = 0
+ @too_large = true
+ end
+
+ def collapsed?
+ return @collapsed if defined?(@collapsed)
+ false
+ end
+
+ def prune_collapsed_diff!
+ @diff = ''
+ @line_count = 0
+ @collapsed = true
+ end
+
+ private
+
+ def init_from_rugged(rugged, collapse: false)
+ if rugged.is_a?(Rugged::Patch)
+ init_from_rugged_patch(rugged, collapse: collapse)
+ d = rugged.delta
+ else
+ d = rugged
+ end
+
+ @new_path = encode!(d.new_file[:path])
+ @old_path = encode!(d.old_file[:path])
+ @a_mode = d.old_file[:mode].to_s(8)
+ @b_mode = d.new_file[:mode].to_s(8)
+ @new_file = d.added?
+ @renamed_file = d.renamed?
+ @deleted_file = d.deleted?
+ end
+
+ def init_from_rugged_patch(patch, collapse: false)
+ # Don't bother initializing diffs that are too large. If a diff is
+ # binary we're not going to display anything so we skip the size check.
+ return if !patch.delta.binary? && prune_large_patch(patch, collapse)
+
+ @diff = encode!(strip_diff_headers(patch.to_s))
+ end
+
+ def init_from_hash(hash, collapse: false)
+ raw_diff = hash.symbolize_keys
+
+ serialize_keys.each do |key|
+ send(:"#{key}=", raw_diff[key.to_sym])
+ end
+
+ prune_large_diff! if too_large?
+ prune_collapsed_diff! if collapse && collapsible?
+ end
+
+ # If the patch surpasses any of the diff limits it calls the appropiate
+ # prune method and returns true. Otherwise returns false.
+ def prune_large_patch(patch, collapse)
+ size = 0
+
+ patch.each_hunk do |hunk|
+ hunk.each_line do |line|
+ size += line.content.bytesize
+
+ if size >= DIFF_SIZE_LIMIT
+ prune_large_diff!
+ return true
+ end
+ end
+ end
+
+ if collapse && size >= DIFF_COLLAPSE_LIMIT
+ prune_collapsed_diff!
+ return true
+ end
+
+ false
+ end
+
+ # Strip out the information at the beginning of the patch's text to match
+ # Grit's output
+ def strip_diff_headers(diff_text)
+ # Delete everything up to the first line that starts with '---' or
+ # 'Binary'
+ diff_text.sub!(/\A.*?^(---|Binary)/m, '\1')
+
+ if diff_text.start_with?('---', 'Binary')
+ diff_text
+ else
+ # If the diff_text did not contain a line starting with '---' or
+ # 'Binary', return the empty string. No idea why; we are just
+ # preserving behavior from before the refactor.
+ ''
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git/diff_collection.rb b/lib/gitlab/git/diff_collection.rb
new file mode 100644
index 00000000000..65e06f5065d
--- /dev/null
+++ b/lib/gitlab/git/diff_collection.rb
@@ -0,0 +1,129 @@
+module Gitlab
+ module Git
+ class DiffCollection
+ include Enumerable
+
+ DEFAULT_LIMITS = { max_files: 100, max_lines: 5000 }.freeze
+
+ def initialize(iterator, options = {})
+ @iterator = iterator
+ @max_files = options.fetch(:max_files, DEFAULT_LIMITS[:max_files])
+ @max_lines = options.fetch(:max_lines, DEFAULT_LIMITS[:max_lines])
+ @max_bytes = @max_files * 5120 # Average 5 KB per file
+ @safe_max_files = [@max_files, DEFAULT_LIMITS[:max_files]].min
+ @safe_max_lines = [@max_lines, DEFAULT_LIMITS[:max_lines]].min
+ @safe_max_bytes = @safe_max_files * 5120 # Average 5 KB per file
+ @all_diffs = !!options.fetch(:all_diffs, false)
+ @no_collapse = !!options.fetch(:no_collapse, true)
+ @deltas_only = !!options.fetch(:deltas_only, false)
+
+ @line_count = 0
+ @byte_count = 0
+ @overflow = false
+ @array = Array.new
+ end
+
+ def each(&block)
+ if @populated
+ # @iterator.each is slower than just iterating the array in place
+ @array.each(&block)
+ elsif @deltas_only
+ each_delta(&block)
+ else
+ each_patch(&block)
+ end
+ end
+
+ def empty?
+ !@iterator.any?
+ end
+
+ def overflow?
+ populate!
+ !!@overflow
+ end
+
+ def size
+ @size ||= count # forces a loop using each method
+ end
+
+ def real_size
+ populate!
+
+ if @overflow
+ "#{size}+"
+ else
+ size.to_s
+ end
+ end
+
+ def decorate!
+ collection = each_with_index do |element, i|
+ @array[i] = yield(element)
+ end
+ @populated = true
+ collection
+ end
+
+ private
+
+ def populate!
+ return if @populated
+
+ each { nil } # force a loop through all diffs
+ @populated = true
+ nil
+ end
+
+ def over_safe_limits?(files)
+ files >= @safe_max_files || @line_count > @safe_max_lines || @byte_count >= @safe_max_bytes
+ end
+
+ def each_delta
+ @iterator.each_delta.with_index do |delta, i|
+ diff = Gitlab::Git::Diff.new(delta)
+
+ yield @array[i] = diff
+ end
+ end
+
+ def each_patch
+ @iterator.each_with_index do |raw, i|
+ # First yield cached Diff instances from @array
+ if @array[i]
+ yield @array[i]
+ next
+ end
+
+ # We have exhausted @array, time to create new Diff instances or stop.
+ break if @overflow
+
+ if !@all_diffs && i >= @max_files
+ @overflow = true
+ break
+ end
+
+ collapse = !@all_diffs && !@no_collapse
+
+ diff = Gitlab::Git::Diff.new(raw, collapse: collapse)
+
+ if collapse && over_safe_limits?(i)
+ diff.prune_collapsed_diff!
+ end
+
+ @line_count += diff.line_count
+ @byte_count += diff.diff.bytesize
+
+ if !@all_diffs && (@line_count >= @max_lines || @byte_count >= @max_bytes)
+ # This last Diff instance pushes us over the lines limit. We stop and
+ # discard it.
+ @overflow = true
+ break
+ end
+
+ yield @array[i] = diff
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git/encoding_helper.rb b/lib/gitlab/git/encoding_helper.rb
new file mode 100644
index 00000000000..e57d228e688
--- /dev/null
+++ b/lib/gitlab/git/encoding_helper.rb
@@ -0,0 +1,58 @@
+module Gitlab
+ module Git
+ module EncodingHelper
+ extend self
+
+ # This threshold is carefully tweaked to prevent usage of encodings detected
+ # by CharlockHolmes with low confidence. If CharlockHolmes confidence is low,
+ # we're better off sticking with utf8 encoding.
+ # Reason: git diff can return strings with invalid utf8 byte sequences if it
+ # truncates a diff in the middle of a multibyte character. In this case
+ # CharlockHolmes will try to guess the encoding and will likely suggest an
+ # obscure encoding with low confidence.
+ # There is a lot more info with this merge request:
+ # https://gitlab.com/gitlab-org/gitlab_git/merge_requests/77#note_4754193
+ ENCODING_CONFIDENCE_THRESHOLD = 40
+
+ def encode!(message)
+ return nil unless message.respond_to? :force_encoding
+
+ # if message is utf-8 encoding, just return it
+ message.force_encoding("UTF-8")
+ return message if message.valid_encoding?
+
+ # return message if message type is binary
+ detect = CharlockHolmes::EncodingDetector.detect(message)
+ return message.force_encoding("BINARY") if detect && detect[:type] == :binary
+
+ # force detected encoding if we have sufficient confidence.
+ if detect && detect[:encoding] && detect[:confidence] > ENCODING_CONFIDENCE_THRESHOLD
+ message.force_encoding(detect[:encoding])
+ end
+
+ # encode and clean the bad chars
+ message.replace clean(message)
+ rescue
+ encoding = detect ? detect[:encoding] : "unknown"
+ "--broken encoding: #{encoding}"
+ end
+
+ def encode_utf8(message)
+ detect = CharlockHolmes::EncodingDetector.detect(message)
+ if detect
+ CharlockHolmes::Converter.convert(message, detect[:encoding], 'UTF-8')
+ else
+ clean(message)
+ end
+ end
+
+ private
+
+ def clean(message)
+ message.encode("UTF-16BE", undef: :replace, invalid: :replace, replace: "")
+ .encode("UTF-8")
+ .gsub("\0".encode("UTF-8"), "")
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git/path_helper.rb b/lib/gitlab/git/path_helper.rb
new file mode 100644
index 00000000000..0148cd8df05
--- /dev/null
+++ b/lib/gitlab/git/path_helper.rb
@@ -0,0 +1,16 @@
+module Gitlab
+ module Git
+ class PathHelper
+ class << self
+ def normalize_path(filename)
+ # Strip all leading slashes so that //foo -> foo
+ filename[/^\/*/] = ''
+
+ # Expand relative paths (e.g. foo/../bar)
+ filename = Pathname.new(filename)
+ filename.relative_path_from(Pathname.new(''))
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git/popen.rb b/lib/gitlab/git/popen.rb
new file mode 100644
index 00000000000..df9ca3ee5ac
--- /dev/null
+++ b/lib/gitlab/git/popen.rb
@@ -0,0 +1,26 @@
+require 'open3'
+
+module Gitlab
+ module Git
+ module Popen
+ def popen(cmd, path)
+ unless cmd.is_a?(Array)
+ raise "System commands must be given as an array of strings"
+ end
+
+ vars = { "PWD" => path }
+ options = { chdir: path }
+
+ @cmd_output = ""
+ @cmd_status = 0
+ Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|
+ @cmd_output << stdout.read
+ @cmd_output << stderr.read
+ @cmd_status = wait_thr.value.exitstatus
+ end
+
+ [@cmd_output, @cmd_status]
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git/ref.rb b/lib/gitlab/git/ref.rb
new file mode 100644
index 00000000000..37ef6836742
--- /dev/null
+++ b/lib/gitlab/git/ref.rb
@@ -0,0 +1,49 @@
+module Gitlab
+ module Git
+ class Ref
+ include Gitlab::Git::EncodingHelper
+
+ # Branch or tag name
+ # without "refs/tags|heads" prefix
+ attr_reader :name
+
+ # Target sha.
+ # Usually it is commit sha but in case
+ # when tag reference on other tag it can be tag sha
+ attr_reader :target
+
+ # Dereferenced target
+ # Commit object to which the Ref points to
+ attr_reader :dereferenced_target
+
+ # Extract branch name from full ref path
+ #
+ # Ex.
+ # Ref.extract_branch_name('refs/heads/master') #=> 'master'
+ def self.extract_branch_name(str)
+ str.gsub(/\Arefs\/heads\//, '')
+ end
+
+ def self.dereference_object(object)
+ object = object.target while object.is_a?(Rugged::Tag::Annotation)
+
+ object
+ end
+
+ def initialize(repository, name, target)
+ encode! name
+ @name = name.gsub(/\Arefs\/(tags|heads)\//, '')
+ @dereferenced_target = Gitlab::Git::Commit.find(repository, target)
+ @target = if target.respond_to?(:oid)
+ target.oid
+ elsif target.respond_to?(:name)
+ target.name
+ elsif target.is_a? String
+ target
+ else
+ nil
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
new file mode 100644
index 00000000000..7068e68a855
--- /dev/null
+++ b/lib/gitlab/git/repository.rb
@@ -0,0 +1,1251 @@
+# Gitlab::Git::Repository is a wrapper around native Rugged::Repository object
+require 'forwardable'
+require 'tempfile'
+require 'forwardable'
+require "rubygems/package"
+
+module Gitlab
+ module Git
+ class Repository
+ extend Forwardable
+ include Gitlab::Git::Popen
+
+ SEARCH_CONTEXT_LINES = 3
+
+ class NoRepository < StandardError; end
+ class InvalidBlobName < StandardError; end
+ class InvalidRef < StandardError; end
+
+ # Full path to repo
+ attr_reader :path
+
+ # Directory name of repo
+ attr_reader :name
+
+ # Rugged repo object
+ attr_reader :rugged
+
+ # 'path' must be the path to a _bare_ git repository, e.g.
+ # /path/to/my-repo.git
+ def initialize(path)
+ @path = path
+ @name = path.split("/").last
+ @attributes = Gitlab::Git::Attributes.new(path)
+ end
+
+ # Default branch in the repository
+ def root_ref
+ @root_ref ||= discover_default_branch
+ end
+
+ # Alias to old method for compatibility
+ def raw
+ rugged
+ end
+
+ def rugged
+ @rugged ||= Rugged::Repository.new(path)
+ rescue Rugged::RepositoryError, Rugged::OSError
+ raise NoRepository.new('no repository for such path')
+ end
+
+ # Returns an Array of branch names
+ # sorted by name ASC
+ def branch_names
+ branches.map(&:name)
+ end
+
+ # Returns an Array of Branches
+ def branches
+ rugged.branches.map do |rugged_ref|
+ begin
+ Gitlab::Git::Branch.new(self, rugged_ref.name, rugged_ref.target)
+ rescue Rugged::ReferenceError
+ # Omit invalid branch
+ end
+ end.compact.sort_by(&:name)
+ end
+
+ def reload_rugged
+ @rugged = nil
+ end
+
+ # Directly find a branch with a simple name (e.g. master)
+ #
+ # force_reload causes a new Rugged repository to be instantiated
+ #
+ # This is to work around a bug in libgit2 that causes in-memory refs to
+ # be stale/invalid when packed-refs is changed.
+ # See https://gitlab.com/gitlab-org/gitlab-ce/issues/15392#note_14538333
+ def find_branch(name, force_reload = false)
+ reload_rugged if force_reload
+
+ rugged_ref = rugged.branches[name]
+ Gitlab::Git::Branch.new(self, rugged_ref.name, rugged_ref.target) if rugged_ref
+ end
+
+ def local_branches
+ rugged.branches.each(:local).map do |branch|
+ Gitlab::Git::Branch.new(self, branch.name, branch.target)
+ end
+ end
+
+ # Returns the number of valid branches
+ def branch_count
+ rugged.branches.count do |ref|
+ begin
+ ref.name && ref.target # ensures the branch is valid
+
+ true
+ rescue Rugged::ReferenceError
+ false
+ end
+ end
+ end
+
+ # Returns an Array of tag names
+ def tag_names
+ rugged.tags.map { |t| t.name }
+ end
+
+ # Returns an Array of Tags
+ def tags
+ rugged.references.each("refs/tags/*").map do |ref|
+ message = nil
+
+ if ref.target.is_a?(Rugged::Tag::Annotation)
+ tag_message = ref.target.message
+
+ if tag_message.respond_to?(:chomp)
+ message = tag_message.chomp
+ end
+ end
+
+ Gitlab::Git::Tag.new(self, ref.name, ref.target, message)
+ end.sort_by(&:name)
+ end
+
+ # Returns true if the given tag exists
+ #
+ # name - The name of the tag as a String.
+ def tag_exists?(name)
+ !!rugged.tags[name]
+ end
+
+ # Returns true if the given branch exists
+ #
+ # name - The name of the branch as a String.
+ def branch_exists?(name)
+ rugged.branches.exists?(name)
+
+ # If the branch name is invalid (e.g. ".foo") Rugged will raise an error.
+ # Whatever code calls this method shouldn't have to deal with that so
+ # instead we just return `false` (which is true since a branch doesn't
+ # exist when it has an invalid name).
+ rescue Rugged::ReferenceError
+ false
+ end
+
+ # Returns an Array of branch and tag names
+ def ref_names
+ branch_names + tag_names
+ end
+
+ # Deprecated. Will be removed in 5.2
+ def heads
+ rugged.references.each("refs/heads/*").map do |head|
+ Gitlab::Git::Ref.new(self, head.name, head.target)
+ end.sort_by(&:name)
+ end
+
+ def has_commits?
+ !empty?
+ end
+
+ def empty?
+ rugged.empty?
+ end
+
+ def bare?
+ rugged.bare?
+ end
+
+ def repo_exists?
+ !!rugged
+ end
+
+ # Discovers the default branch based on the repository's available branches
+ #
+ # - If no branches are present, returns nil
+ # - If one branch is present, returns its name
+ # - If two or more branches are present, returns current HEAD or master or first branch
+ def discover_default_branch
+ names = branch_names
+
+ return if names.empty?
+
+ return names[0] if names.length == 1
+
+ if rugged_head
+ extracted_name = Ref.extract_branch_name(rugged_head.name)
+
+ return extracted_name if names.include?(extracted_name)
+ end
+
+ if names.include?('master')
+ 'master'
+ else
+ names[0]
+ end
+ end
+
+ def rugged_head
+ rugged.head
+ rescue Rugged::ReferenceError
+ nil
+ end
+
+ def archive_metadata(ref, storage_path, format = "tar.gz")
+ ref ||= root_ref
+ commit = Gitlab::Git::Commit.find(self, ref)
+ return {} if commit.nil?
+
+ project_name = self.name.chomp('.git')
+ prefix = "#{project_name}-#{ref}-#{commit.id}"
+
+ {
+ 'RepoPath' => path,
+ 'ArchivePrefix' => prefix,
+ 'ArchivePath' => archive_file_path(prefix, storage_path, format),
+ 'CommitId' => commit.id,
+ }
+ end
+
+ def archive_file_path(name, storage_path, format = "tar.gz")
+ # Build file path
+ return nil unless name
+
+ extension =
+ case format
+ when "tar.bz2", "tbz", "tbz2", "tb2", "bz2"
+ "tar.bz2"
+ when "tar"
+ "tar"
+ when "zip"
+ "zip"
+ else
+ # everything else should fall back to tar.gz
+ "tar.gz"
+ end
+
+ file_name = "#{name}.#{extension}"
+ File.join(storage_path, self.name, file_name)
+ end
+
+ # Return repo size in megabytes
+ def size
+ size = popen(%w(du -sk), path).first.strip.to_i
+ (size.to_f / 1024).round(2)
+ end
+
+ # Returns an array of BlobSnippets for files at the specified +ref+ that
+ # contain the +query+ string.
+ def search_files(query, ref = nil)
+ greps = []
+ ref ||= root_ref
+
+ populated_index(ref).each do |entry|
+ # Discard submodules
+ next if submodule?(entry)
+
+ blob = Gitlab::Git::Blob.raw(self, entry[:oid])
+
+ # Skip binary files
+ next if blob.data.encoding == Encoding::ASCII_8BIT
+
+ blob.load_all_data!(self)
+ greps += build_greps(blob.data, query, ref, entry[:path])
+ end
+
+ greps
+ end
+
+ # Use the Rugged Walker API to build an array of commits.
+ #
+ # Usage.
+ # repo.log(
+ # ref: 'master',
+ # path: 'app/models',
+ # limit: 10,
+ # offset: 5,
+ # after: Time.new(2016, 4, 21, 14, 32, 10)
+ # )
+ #
+ def log(options)
+ default_options = {
+ limit: 10,
+ offset: 0,
+ path: nil,
+ follow: false,
+ skip_merges: false,
+ disable_walk: false,
+ after: nil,
+ before: nil
+ }
+
+ options = default_options.merge(options)
+ options[:limit] ||= 0
+ options[:offset] ||= 0
+ actual_ref = options[:ref] || root_ref
+ begin
+ sha = sha_from_ref(actual_ref)
+ rescue Rugged::OdbError, Rugged::InvalidError, Rugged::ReferenceError
+ # Return an empty array if the ref wasn't found
+ return []
+ end
+
+ if log_using_shell?(options)
+ log_by_shell(sha, options)
+ else
+ log_by_walk(sha, options)
+ end
+ end
+
+ def log_using_shell?(options)
+ options[:path].present? ||
+ options[:disable_walk] ||
+ options[:skip_merges] ||
+ options[:after] ||
+ options[:before]
+ end
+
+ def log_by_walk(sha, options)
+ walk_options = {
+ show: sha,
+ sort: Rugged::SORT_DATE,
+ limit: options[:limit],
+ offset: options[:offset]
+ }
+ Rugged::Walker.walk(rugged, walk_options).to_a
+ end
+
+ def log_by_shell(sha, options)
+ cmd = %W(#{Gitlab.config.git.bin_path} --git-dir=#{path} log)
+ cmd += %W(-n #{options[:limit].to_i})
+ cmd += %w(--format=%H)
+ cmd += %W(--skip=#{options[:offset].to_i})
+ cmd += %w(--follow) if options[:follow]
+ cmd += %w(--no-merges) if options[:skip_merges]
+ cmd += %W(--after=#{options[:after].iso8601}) if options[:after]
+ cmd += %W(--before=#{options[:before].iso8601}) if options[:before]
+ cmd += [sha]
+ cmd += %W(-- #{options[:path]}) if options[:path].present?
+
+ raw_output = IO.popen(cmd) {|io| io.read }
+
+ log = raw_output.lines.map do |c|
+ Rugged::Commit.new(rugged, c.strip)
+ end
+
+ log.is_a?(Array) ? log : []
+ end
+
+ def sha_from_ref(ref)
+ rev_parse_target(ref).oid
+ end
+
+ # Return the object that +revspec+ points to. If +revspec+ is an
+ # annotated tag, then return the tag's target instead.
+ def rev_parse_target(revspec)
+ obj = rugged.rev_parse(revspec)
+ Ref.dereference_object(obj)
+ end
+
+ # Return a collection of Rugged::Commits between the two revspec arguments.
+ # See http://git-scm.com/docs/git-rev-parse.html#_specifying_revisions for
+ # a detailed list of valid arguments.
+ def commits_between(from, to)
+ walker = Rugged::Walker.new(rugged)
+ walker.sorting(Rugged::SORT_DATE | Rugged::SORT_REVERSE)
+
+ sha_from = sha_from_ref(from)
+ sha_to = sha_from_ref(to)
+
+ walker.push(sha_to)
+ walker.hide(sha_from)
+
+ commits = walker.to_a
+ walker.reset
+
+ commits
+ end
+
+ # Counts the amount of commits between `from` and `to`.
+ def count_commits_between(from, to)
+ commits_between(from, to).size
+ end
+
+ # Returns the SHA of the most recent common ancestor of +from+ and +to+
+ def merge_base_commit(from, to)
+ rugged.merge_base(from, to)
+ end
+
+ # Return an array of Diff objects that represent the diff
+ # between +from+ and +to+. See Diff::filter_diff_options for the allowed
+ # diff options. The +options+ hash can also include :break_rewrites to
+ # split larger rewrites into delete/add pairs.
+ def diff(from, to, options = {}, *paths)
+ Gitlab::Git::DiffCollection.new(diff_patches(from, to, options, *paths), options)
+ end
+
+ # Returns commits collection
+ #
+ # Ex.
+ # repo.find_commits(
+ # ref: 'master',
+ # max_count: 10,
+ # skip: 5,
+ # order: :date
+ # )
+ #
+ # +options+ is a Hash of optional arguments to git
+ # :ref is the ref from which to begin (SHA1 or name)
+ # :contains is the commit contained by the refs from which to begin (SHA1 or name)
+ # :max_count is the maximum number of commits to fetch
+ # :skip is the number of commits to skip
+ # :order is the commits order and allowed value is :date(default) or :topo
+ #
+ def find_commits(options = {})
+ actual_options = options.dup
+
+ allowed_options = [:ref, :max_count, :skip, :contains, :order]
+
+ actual_options.keep_if do |key|
+ allowed_options.include?(key)
+ end
+
+ default_options = { skip: 0 }
+ actual_options = default_options.merge(actual_options)
+
+ walker = Rugged::Walker.new(rugged)
+
+ if actual_options[:ref]
+ walker.push(rugged.rev_parse_oid(actual_options[:ref]))
+ elsif actual_options[:contains]
+ branches_contains(actual_options[:contains]).each do |branch|
+ walker.push(branch.target_id)
+ end
+ else
+ rugged.references.each("refs/heads/*") do |ref|
+ walker.push(ref.target_id)
+ end
+ end
+
+ if actual_options[:order] == :topo
+ walker.sorting(Rugged::SORT_TOPO)
+ else
+ walker.sorting(Rugged::SORT_DATE)
+ end
+
+ commits = []
+ offset = actual_options[:skip]
+ limit = actual_options[:max_count]
+ walker.each(offset: offset, limit: limit) do |commit|
+ gitlab_commit = Gitlab::Git::Commit.decorate(commit)
+ commits.push(gitlab_commit)
+ end
+
+ walker.reset
+
+ commits
+ rescue Rugged::OdbError
+ []
+ end
+
+ # Returns branch names collection that contains the special commit(SHA1
+ # or name)
+ #
+ # Ex.
+ # repo.branch_names_contains('master')
+ #
+ def branch_names_contains(commit)
+ branches_contains(commit).map { |c| c.name }
+ end
+
+ # Returns branch collection that contains the special commit(SHA1 or name)
+ #
+ # Ex.
+ # repo.branch_names_contains('master')
+ #
+ def branches_contains(commit)
+ commit_obj = rugged.rev_parse(commit)
+ parent = commit_obj.parents.first unless commit_obj.parents.empty?
+
+ walker = Rugged::Walker.new(rugged)
+
+ rugged.branches.select do |branch|
+ walker.push(branch.target_id)
+ walker.hide(parent) if parent
+ result = walker.any? { |c| c.oid == commit_obj.oid }
+ walker.reset
+
+ result
+ end
+ end
+
+ # Get refs hash which key is SHA1
+ # and value is a Rugged::Reference
+ def refs_hash
+ # Initialize only when first call
+ if @refs_hash.nil?
+ @refs_hash = Hash.new { |h, k| h[k] = [] }
+
+ rugged.references.each do |r|
+ # Symbolic/remote references may not have an OID; skip over them
+ target_oid = r.target.try(:oid)
+ if target_oid
+ sha = rev_parse_target(target_oid).oid
+ @refs_hash[sha] << r
+ end
+ end
+ end
+ @refs_hash
+ end
+
+ # Lookup for rugged object by oid or ref name
+ def lookup(oid_or_ref_name)
+ rugged.rev_parse(oid_or_ref_name)
+ end
+
+ # Return hash with submodules info for this repository
+ #
+ # Ex.
+ # {
+ # "rack" => {
+ # "id" => "c67be4624545b4263184c4a0e8f887efd0a66320",
+ # "path" => "rack",
+ # "url" => "git://github.com/chneukirchen/rack.git"
+ # },
+ # "encoding" => {
+ # "id" => ....
+ # }
+ # }
+ #
+ def submodules(ref)
+ commit = rev_parse_target(ref)
+ return {} unless commit
+
+ begin
+ content = blob_content(commit, ".gitmodules")
+ rescue InvalidBlobName
+ return {}
+ end
+
+ parse_gitmodules(commit, content)
+ end
+
+ # Return total commits count accessible from passed ref
+ def commit_count(ref)
+ walker = Rugged::Walker.new(rugged)
+ walker.sorting(Rugged::SORT_TOPO | Rugged::SORT_REVERSE)
+ oid = rugged.rev_parse_oid(ref)
+ walker.push(oid)
+ walker.count
+ end
+
+ # Sets HEAD to the commit specified by +ref+; +ref+ can be a branch or
+ # tag name or a commit SHA. Valid +reset_type+ values are:
+ #
+ # [:soft]
+ # the head will be moved to the commit.
+ # [:mixed]
+ # will trigger a +:soft+ reset, plus the index will be replaced
+ # with the content of the commit tree.
+ # [:hard]
+ # will trigger a +:mixed+ reset and the working directory will be
+ # replaced with the content of the index. (Untracked and ignored files
+ # will be left alone)
+ def reset(ref, reset_type)
+ rugged.reset(ref, reset_type)
+ end
+
+ # Mimic the `git clean` command and recursively delete untracked files.
+ # Valid keys that can be passed in the +options+ hash are:
+ #
+ # :d - Remove untracked directories
+ # :f - Remove untracked directories that are managed by a different
+ # repository
+ # :x - Remove ignored files
+ #
+ # The value in +options+ must evaluate to true for an option to take
+ # effect.
+ #
+ # Examples:
+ #
+ # repo.clean(d: true, f: true) # Enable the -d and -f options
+ #
+ # repo.clean(d: false, x: true) # -x is enabled, -d is not
+ def clean(options = {})
+ strategies = [:remove_untracked]
+ strategies.push(:force) if options[:f]
+ strategies.push(:remove_ignored) if options[:x]
+
+ # TODO: implement this method
+ end
+
+ # Check out the specified ref. Valid options are:
+ #
+ # :b - Create a new branch at +start_point+ and set HEAD to the new
+ # branch.
+ #
+ # * These options are passed to the Rugged::Repository#checkout method:
+ #
+ # :progress ::
+ # A callback that will be executed for checkout progress notifications.
+ # Up to 3 parameters are passed on each execution:
+ #
+ # - The path to the last updated file (or +nil+ on the very first
+ # invocation).
+ # - The number of completed checkout steps.
+ # - The number of total checkout steps to be performed.
+ #
+ # :notify ::
+ # A callback that will be executed for each checkout notification
+ # types specified with +:notify_flags+. Up to 5 parameters are passed
+ # on each execution:
+ #
+ # - An array containing the +:notify_flags+ that caused the callback
+ # execution.
+ # - The path of the current file.
+ # - A hash describing the baseline blob (or +nil+ if it does not
+ # exist).
+ # - A hash describing the target blob (or +nil+ if it does not exist).
+ # - A hash describing the workdir blob (or +nil+ if it does not
+ # exist).
+ #
+ # :strategy ::
+ # A single symbol or an array of symbols representing the strategies
+ # to use when performing the checkout. Possible values are:
+ #
+ # :none ::
+ # Perform a dry run (default).
+ #
+ # :safe ::
+ # Allow safe updates that cannot overwrite uncommitted data.
+ #
+ # :safe_create ::
+ # Allow safe updates plus creation of missing files.
+ #
+ # :force ::
+ # Allow all updates to force working directory to look like index.
+ #
+ # :allow_conflicts ::
+ # Allow checkout to make safe updates even if conflicts are found.
+ #
+ # :remove_untracked ::
+ # Remove untracked files not in index (that are not ignored).
+ #
+ # :remove_ignored ::
+ # Remove ignored files not in index.
+ #
+ # :update_only ::
+ # Only update existing files, don't create new ones.
+ #
+ # :dont_update_index ::
+ # Normally checkout updates index entries as it goes; this stops
+ # that.
+ #
+ # :no_refresh ::
+ # Don't refresh index/config/etc before doing checkout.
+ #
+ # :disable_pathspec_match ::
+ # Treat pathspec as simple list of exact match file paths.
+ #
+ # :skip_locked_directories ::
+ # Ignore directories in use, they will be left empty.
+ #
+ # :skip_unmerged ::
+ # Allow checkout to skip unmerged files (NOT IMPLEMENTED).
+ #
+ # :use_ours ::
+ # For unmerged files, checkout stage 2 from index (NOT IMPLEMENTED).
+ #
+ # :use_theirs ::
+ # For unmerged files, checkout stage 3 from index (NOT IMPLEMENTED).
+ #
+ # :update_submodules ::
+ # Recursively checkout submodules with same options (NOT
+ # IMPLEMENTED).
+ #
+ # :update_submodules_if_changed ::
+ # Recursively checkout submodules if HEAD moved in super repo (NOT
+ # IMPLEMENTED).
+ #
+ # :disable_filters ::
+ # If +true+, filters like CRLF line conversion will be disabled.
+ #
+ # :dir_mode ::
+ # Mode for newly created directories. Default: +0755+.
+ #
+ # :file_mode ::
+ # Mode for newly created files. Default: +0755+ or +0644+.
+ #
+ # :file_open_flags ::
+ # Mode for opening files. Default:
+ # <code>IO::CREAT | IO::TRUNC | IO::WRONLY</code>.
+ #
+ # :notify_flags ::
+ # A single symbol or an array of symbols representing the cases in
+ # which the +:notify+ callback should be invoked. Possible values are:
+ #
+ # :none ::
+ # Do not invoke the +:notify+ callback (default).
+ #
+ # :conflict ::
+ # Invoke the callback for conflicting paths.
+ #
+ # :dirty ::
+ # Invoke the callback for "dirty" files, i.e. those that do not need
+ # an update but no longer match the baseline.
+ #
+ # :updated ::
+ # Invoke the callback for any file that was changed.
+ #
+ # :untracked ::
+ # Invoke the callback for untracked files.
+ #
+ # :ignored ::
+ # Invoke the callback for ignored files.
+ #
+ # :all ::
+ # Invoke the callback for all these cases.
+ #
+ # :paths ::
+ # A glob string or an array of glob strings specifying which paths
+ # should be taken into account for the checkout operation. +nil+ will
+ # match all files. Default: +nil+.
+ #
+ # :baseline ::
+ # A Rugged::Tree that represents the current, expected contents of the
+ # workdir. Default: +HEAD+.
+ #
+ # :target_directory ::
+ # A path to an alternative workdir directory in which the checkout
+ # should be performed.
+ def checkout(ref, options = {}, start_point = "HEAD")
+ if options[:b]
+ rugged.branches.create(ref, start_point)
+ options.delete(:b)
+ end
+ default_options = { strategy: [:recreate_missing, :safe] }
+ rugged.checkout(ref, default_options.merge(options))
+ end
+
+ # Delete the specified branch from the repository
+ def delete_branch(branch_name)
+ rugged.branches.delete(branch_name)
+ end
+
+ # Create a new branch named **ref+ based on **stat_point+, HEAD by default
+ #
+ # Examples:
+ # create_branch("feature")
+ # create_branch("other-feature", "master")
+ def create_branch(ref, start_point = "HEAD")
+ rugged_ref = rugged.branches.create(ref, start_point)
+ Gitlab::Git::Branch.new(self, rugged_ref.name, rugged_ref.target)
+ rescue Rugged::ReferenceError => e
+ raise InvalidRef.new("Branch #{ref} already exists") if e.to_s =~ /'refs\/heads\/#{ref}'/
+ raise InvalidRef.new("Invalid reference #{start_point}")
+ end
+
+ # Return an array of this repository's remote names
+ def remote_names
+ rugged.remotes.each_name.to_a
+ end
+
+ # Delete the specified remote from this repository.
+ def remote_delete(remote_name)
+ rugged.remotes.delete(remote_name)
+ end
+
+ # Add a new remote to this repository. Returns a Rugged::Remote object
+ def remote_add(remote_name, url)
+ rugged.remotes.create(remote_name, url)
+ end
+
+ # Update the specified remote using the values in the +options+ hash
+ #
+ # Example
+ # repo.update_remote("origin", url: "path/to/repo")
+ def remote_update(remote_name, options = {})
+ # TODO: Implement other remote options
+ rugged.remotes.set_url(remote_name, options[:url]) if options[:url]
+ end
+
+ # Fetch the specified remote
+ def fetch(remote_name)
+ rugged.remotes[remote_name].fetch
+ end
+
+ # Push +*refspecs+ to the remote identified by +remote_name+.
+ def push(remote_name, *refspecs)
+ rugged.remotes[remote_name].push(refspecs)
+ end
+
+ # Merge the +source_name+ branch into the +target_name+ branch. This is
+ # equivalent to `git merge --no_ff +source_name+`, since a merge commit
+ # is always created.
+ def merge(source_name, target_name, options = {})
+ our_commit = rugged.branches[target_name].target
+ their_commit = rugged.branches[source_name].target
+
+ raise "Invalid merge target" if our_commit.nil?
+ raise "Invalid merge source" if their_commit.nil?
+
+ merge_index = rugged.merge_commits(our_commit, their_commit)
+ return false if merge_index.conflicts?
+
+ actual_options = options.merge(
+ parents: [our_commit, their_commit],
+ tree: merge_index.write_tree(rugged),
+ update_ref: "refs/heads/#{target_name}"
+ )
+ Rugged::Commit.create(rugged, actual_options)
+ end
+
+ def commits_since(from_date)
+ walker = Rugged::Walker.new(rugged)
+ walker.sorting(Rugged::SORT_DATE | Rugged::SORT_REVERSE)
+
+ rugged.references.each("refs/heads/*") do |ref|
+ walker.push(ref.target_id)
+ end
+
+ commits = []
+ walker.each do |commit|
+ break if commit.author[:time].to_date < from_date
+ commits.push(commit)
+ end
+
+ commits
+ end
+
+ AUTOCRLF_VALUES = {
+ "true" => true,
+ "false" => false,
+ "input" => :input
+ }.freeze
+
+ def autocrlf
+ AUTOCRLF_VALUES[rugged.config['core.autocrlf']]
+ end
+
+ def autocrlf=(value)
+ rugged.config['core.autocrlf'] = AUTOCRLF_VALUES.invert[value]
+ end
+
+ # Create a new directory with a .gitkeep file. Creates
+ # all required nested directories (i.e. mkdir -p behavior)
+ #
+ # options should contain next structure:
+ # author: {
+ # email: 'user@example.com',
+ # name: 'Test User',
+ # time: Time.now
+ # },
+ # committer: {
+ # email: 'user@example.com',
+ # name: 'Test User',
+ # time: Time.now
+ # },
+ # commit: {
+ # message: 'Wow such commit',
+ # branch: 'master',
+ # update_ref: false
+ # }
+ def mkdir(path, options = {})
+ # Check if this directory exists; if it does, then don't bother
+ # adding .gitkeep file.
+ ref = options[:commit][:branch]
+ path = Gitlab::Git::PathHelper.normalize_path(path).to_s
+ rugged_ref = rugged.ref(ref)
+
+ raise InvalidRef.new("Invalid ref") if rugged_ref.nil?
+
+ target_commit = rugged_ref.target
+
+ raise InvalidRef.new("Invalid target commit") if target_commit.nil?
+
+ entry = tree_entry(target_commit, path)
+
+ if entry
+ if entry[:type] == :blob
+ raise InvalidBlobName.new("Directory already exists as a file")
+ else
+ raise InvalidBlobName.new("Directory already exists")
+ end
+ end
+
+ options[:file] = {
+ content: '',
+ path: "#{path}/.gitkeep",
+ update: true
+ }
+
+ Gitlab::Git::Blob.commit(self, options)
+ end
+
+ # Returns result like "git ls-files" , recursive and full file path
+ #
+ # Ex.
+ # repo.ls_files('master')
+ #
+ def ls_files(ref)
+ actual_ref = ref || root_ref
+
+ begin
+ sha_from_ref(actual_ref)
+ rescue Rugged::OdbError, Rugged::InvalidError, Rugged::ReferenceError
+ # Return an empty array if the ref wasn't found
+ return []
+ end
+
+ cmd = %W(#{Gitlab.config.git.bin_path} --git-dir=#{path} ls-tree)
+ cmd += %w(-r)
+ cmd += %w(--full-tree)
+ cmd += %w(--full-name)
+ cmd += %W(-- #{actual_ref})
+
+ raw_output = IO.popen(cmd, &:read).split("\n").map do |f|
+ stuff, path = f.split("\t")
+ _mode, type, _sha = stuff.split(" ")
+ path if type == "blob"
+ # Contain only blob type
+ end
+
+ raw_output.compact
+ end
+
+ def copy_gitattributes(ref)
+ begin
+ commit = lookup(ref)
+ rescue Rugged::ReferenceError
+ raise InvalidRef.new("Ref #{ref} is invalid")
+ end
+
+ # Create the paths
+ info_dir_path = File.join(path, 'info')
+ info_attributes_path = File.join(info_dir_path, 'attributes')
+
+ begin
+ # Retrieve the contents of the blob
+ gitattributes_content = blob_content(commit, '.gitattributes')
+ rescue InvalidBlobName
+ # No .gitattributes found. Should now remove any info/attributes and return
+ File.delete(info_attributes_path) if File.exist?(info_attributes_path)
+ return
+ end
+
+ # Create the info directory if needed
+ Dir.mkdir(info_dir_path) unless File.directory?(info_dir_path)
+
+ # Write the contents of the .gitattributes file to info/attributes
+ # Use binary mode to prevent Rails from converting ASCII-8BIT to UTF-8
+ File.open(info_attributes_path, "wb") do |file|
+ file.write(gitattributes_content)
+ end
+ end
+
+ # Checks if the blob should be diffable according to its attributes
+ def diffable?(blob)
+ attributes(blob.path).fetch('diff') { blob.text? }
+ end
+
+ # Returns the Git attributes for the given file path.
+ #
+ # See `Gitlab::Git::Attributes` for more information.
+ def attributes(path)
+ @attributes.attributes(path)
+ end
+
+ private
+
+ # Get the content of a blob for a given commit. If the blob is a commit
+ # (for submodules) then return the blob's OID.
+ def blob_content(commit, blob_name)
+ blob_entry = tree_entry(commit, blob_name)
+
+ unless blob_entry
+ raise InvalidBlobName.new("Invalid blob name: #{blob_name}")
+ end
+
+ case blob_entry[:type]
+ when :commit
+ blob_entry[:oid]
+ when :tree
+ raise InvalidBlobName.new("#{blob_name} is a tree, not a blob")
+ when :blob
+ rugged.lookup(blob_entry[:oid]).content
+ end
+ end
+
+ # Parses the contents of a .gitmodules file and returns a hash of
+ # submodule information.
+ def parse_gitmodules(commit, content)
+ results = {}
+
+ current = ""
+ content.split("\n").each do |txt|
+ if txt =~ /^\s*\[/
+ current = txt.match(/(?<=").*(?=")/)[0]
+ results[current] = {}
+ else
+ next unless results[current]
+ match_data = txt.match(/(\w+)\s*=\s*(.*)/)
+ next unless match_data
+ target = match_data[2].chomp
+ results[current][match_data[1]] = target
+
+ if match_data[1] == "path"
+ begin
+ results[current]["id"] = blob_content(commit, target)
+ rescue InvalidBlobName
+ results.delete(current)
+ end
+ end
+ end
+ end
+
+ results
+ end
+
+ # Returns true if +commit+ introduced changes to +path+, using commit
+ # trees to make that determination. Uses the history simplification
+ # rules that `git log` uses by default, where a commit is omitted if it
+ # is TREESAME to any parent.
+ #
+ # If the +follow+ option is true and the file specified by +path+ was
+ # renamed, then the path value is set to the old path.
+ def commit_touches_path?(commit, path, follow, walker)
+ entry = tree_entry(commit, path)
+
+ if commit.parents.empty?
+ # This is the root commit, return true if it has +path+ in its tree
+ return !entry.nil?
+ end
+
+ num_treesame = 0
+ commit.parents.each do |parent|
+ parent_entry = tree_entry(parent, path)
+
+ # Only follow the first TREESAME parent for merge commits
+ if num_treesame > 0
+ walker.hide(parent)
+ next
+ end
+
+ if entry.nil? && parent_entry.nil?
+ num_treesame += 1
+ elsif entry && parent_entry && entry[:oid] == parent_entry[:oid]
+ num_treesame += 1
+ end
+ end
+
+ case num_treesame
+ when 0
+ detect_rename(commit, commit.parents.first, path) if follow
+ true
+ else false
+ end
+ end
+
+ # Find the entry for +path+ in the tree for +commit+
+ def tree_entry(commit, path)
+ pathname = Pathname.new(path)
+ first = true
+ tmp_entry = nil
+
+ pathname.each_filename do |dir|
+ if first
+ tmp_entry = commit.tree[dir]
+ first = false
+ elsif tmp_entry.nil?
+ return nil
+ else
+ tmp_entry = rugged.lookup(tmp_entry[:oid])
+ return nil unless tmp_entry.type == :tree
+ tmp_entry = tmp_entry[dir]
+ end
+ end
+
+ tmp_entry
+ end
+
+ # Compare +commit+ and +parent+ for +path+. If +path+ is a file and was
+ # renamed in +commit+, then set +path+ to the old filename.
+ def detect_rename(commit, parent, path)
+ diff = parent.diff(commit, paths: [path], disable_pathspec_match: true)
+
+ # If +path+ is a filename, not a directory, then we should only have
+ # one delta. We don't need to follow renames for directories.
+ return nil if diff.each_delta.count > 1
+
+ delta = diff.each_delta.first
+ if delta.added?
+ full_diff = parent.diff(commit)
+ full_diff.find_similar!
+
+ full_diff.each_delta do |full_delta|
+ if full_delta.renamed? && path == full_delta.new_file[:path]
+ # Look for the old path in ancestors
+ path.replace(full_delta.old_file[:path])
+ end
+ end
+ end
+ end
+
+ def archive_to_file(treeish = 'master', filename = 'archive.tar.gz', format = nil, compress_cmd = %w(gzip -n))
+ git_archive_cmd = %W(#{Gitlab.config.git.bin_path} --git-dir=#{path} archive)
+
+ # Put files into a directory before archiving
+ prefix = "#{archive_name(treeish)}/"
+ git_archive_cmd << "--prefix=#{prefix}"
+
+ # Format defaults to tar
+ git_archive_cmd << "--format=#{format}" if format
+
+ git_archive_cmd += %W(-- #{treeish})
+
+ open(filename, 'w') do |file|
+ # Create a pipe to act as the '|' in 'git archive ... | gzip'
+ pipe_rd, pipe_wr = IO.pipe
+
+ # Get the compression process ready to accept data from the read end
+ # of the pipe
+ compress_pid = spawn(*nice(compress_cmd), in: pipe_rd, out: file)
+ # The read end belongs to the compression process now; we should
+ # close our file descriptor for it.
+ pipe_rd.close
+
+ # Start 'git archive' and tell it to write into the write end of the
+ # pipe.
+ git_archive_pid = spawn(*nice(git_archive_cmd), out: pipe_wr)
+ # The write end belongs to 'git archive' now; close it.
+ pipe_wr.close
+
+ # When 'git archive' and the compression process are finished, we are
+ # done.
+ Process.waitpid(git_archive_pid)
+ raise "#{git_archive_cmd.join(' ')} failed" unless $?.success?
+ Process.waitpid(compress_pid)
+ raise "#{compress_cmd.join(' ')} failed" unless $?.success?
+ end
+ end
+
+ def nice(cmd)
+ nice_cmd = %w(nice -n 20)
+ unless unsupported_platform?
+ nice_cmd += %w(ionice -c 2 -n 7)
+ end
+ nice_cmd + cmd
+ end
+
+ def unsupported_platform?
+ %w[darwin freebsd solaris].map { |platform| RUBY_PLATFORM.include?(platform) }.any?
+ end
+
+ # Returns true if the index entry has the special file mode that denotes
+ # a submodule.
+ def submodule?(index_entry)
+ index_entry[:mode] == 57344
+ end
+
+ # Return a Rugged::Index that has read from the tree at +ref_name+
+ def populated_index(ref_name)
+ commit = rev_parse_target(ref_name)
+ index = rugged.index
+ index.read_tree(commit.tree)
+ index
+ end
+
+ # Return an array of BlobSnippets for lines in +file_contents+ that match
+ # +query+
+ def build_greps(file_contents, query, ref, filename)
+ # The file_contents string is potentially huge so we make sure to loop
+ # through it one line at a time. This gives Ruby the chance to GC lines
+ # we are not interested in.
+ #
+ # We need to do a little extra work because we are not looking for just
+ # the lines that matches the query, but also for the context
+ # (surrounding lines). We will use Enumerable#each_cons to efficiently
+ # loop through the lines while keeping surrounding lines on hand.
+ #
+ # First, we turn "foo\nbar\nbaz" into
+ # [
+ # [nil, -3], [nil, -2], [nil, -1],
+ # ['foo', 0], ['bar', 1], ['baz', 3],
+ # [nil, 4], [nil, 5], [nil, 6]
+ # ]
+ lines_with_index = Enumerator.new do |yielder|
+ # Yield fake 'before' lines for the first line of file_contents
+ (-SEARCH_CONTEXT_LINES..-1).each do |i|
+ yielder.yield [nil, i]
+ end
+
+ # Yield the actual file contents
+ count = 0
+ file_contents.each_line do |line|
+ line.chomp!
+ yielder.yield [line, count]
+ count += 1
+ end
+
+ # Yield fake 'after' lines for the last line of file_contents
+ (count + 1..count + SEARCH_CONTEXT_LINES).each do |i|
+ yielder.yield [nil, i]
+ end
+ end
+
+ greps = []
+
+ # Loop through consecutive blocks of lines with indexes
+ lines_with_index.each_cons(2 * SEARCH_CONTEXT_LINES + 1) do |line_block|
+ # Get the 'middle' line and index from the block
+ line, _ = line_block[SEARCH_CONTEXT_LINES]
+
+ next unless line && line.match(/#{Regexp.escape(query)}/i)
+
+ # Yay, 'line' contains a match!
+ # Get an array with just the context lines (no indexes)
+ match_with_context = line_block.map(&:first)
+ # Remove 'nil' lines in case we are close to the first or last line
+ match_with_context.compact!
+
+ # Get the line number (1-indexed) of the first context line
+ first_context_line_number = line_block[0][1] + 1
+
+ greps << Gitlab::Git::BlobSnippet.new(
+ ref,
+ match_with_context,
+ first_context_line_number,
+ filename
+ )
+ end
+
+ greps
+ end
+
+ # Return the Rugged patches for the diff between +from+ and +to+.
+ def diff_patches(from, to, options = {}, *paths)
+ options ||= {}
+ break_rewrites = options[:break_rewrites]
+ actual_options = Gitlab::Git::Diff.filter_diff_options(options.merge(paths: paths))
+
+ diff = rugged.diff(from, to, actual_options)
+ diff.find_similar!(break_rewrites: break_rewrites)
+ diff.each_patch
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git/tag.rb b/lib/gitlab/git/tag.rb
new file mode 100644
index 00000000000..b5342c3d310
--- /dev/null
+++ b/lib/gitlab/git/tag.rb
@@ -0,0 +1,17 @@
+module Gitlab
+ module Git
+ class Tag < Ref
+ attr_reader :object_sha
+
+ def initialize(repository, name, target, message = nil)
+ super(repository, name, target)
+
+ @message = message
+ end
+
+ def message
+ encode! @message
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git/tree.rb b/lib/gitlab/git/tree.rb
new file mode 100644
index 00000000000..f7450e8b58f
--- /dev/null
+++ b/lib/gitlab/git/tree.rb
@@ -0,0 +1,104 @@
+module Gitlab
+ module Git
+ class Tree
+ include Gitlab::Git::EncodingHelper
+
+ attr_accessor :id, :root_id, :name, :path, :type,
+ :mode, :commit_id, :submodule_url
+
+ class << self
+ # Get list of tree objects
+ # for repository based on commit sha and path
+ # Uses rugged for raw objects
+ def where(repository, sha, path = nil)
+ path = nil if path == '' || path == '/'
+
+ commit = repository.lookup(sha)
+ root_tree = commit.tree
+
+ tree = if path
+ id = find_id_by_path(repository, root_tree.oid, path)
+ if id
+ repository.lookup(id)
+ else
+ []
+ end
+ else
+ root_tree
+ end
+
+ tree.map do |entry|
+ new(
+ id: entry[:oid],
+ root_id: root_tree.oid,
+ name: entry[:name],
+ type: entry[:type],
+ mode: entry[:filemode],
+ path: path ? File.join(path, entry[:name]) : entry[:name],
+ commit_id: sha,
+ )
+ end
+ end
+
+ # Recursive search of tree id for path
+ #
+ # Ex.
+ # blog/ # oid: 1a
+ # app/ # oid: 2a
+ # models/ # oid: 3a
+ # views/ # oid: 4a
+ #
+ #
+ # Tree.find_id_by_path(repo, '1a', 'app/models') # => '3a'
+ #
+ def find_id_by_path(repository, root_id, path)
+ root_tree = repository.lookup(root_id)
+ path_arr = path.split('/')
+
+ entry = root_tree.find do |entry|
+ entry[:name] == path_arr[0] && entry[:type] == :tree
+ end
+
+ return nil unless entry
+
+ if path_arr.size > 1
+ path_arr.shift
+ find_id_by_path(repository, entry[:oid], path_arr.join('/'))
+ else
+ entry[:oid]
+ end
+ end
+ end
+
+ def initialize(options)
+ %w(id root_id name path type mode commit_id).each do |key|
+ self.send("#{key}=", options[key.to_sym])
+ end
+ end
+
+ def name
+ encode! @name
+ end
+
+ def dir?
+ type == :tree
+ end
+
+ def file?
+ type == :blob
+ end
+
+ def submodule?
+ type == :commit
+ end
+
+ def readme?
+ name =~ /^readme/i
+ end
+
+ def contributing?
+ name =~ /^contributing/i
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git/util.rb b/lib/gitlab/git/util.rb
new file mode 100644
index 00000000000..7973da2e8f8
--- /dev/null
+++ b/lib/gitlab/git/util.rb
@@ -0,0 +1,18 @@
+module Gitlab
+ module Git
+ module Util
+ LINE_SEP = "\n".freeze
+
+ def self.count_lines(string)
+ case string[-1]
+ when nil
+ 0
+ when LINE_SEP
+ string.count(LINE_SEP)
+ else
+ string.count(LINE_SEP) + 1
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb
index c6b6efda360..7e1484613f2 100644
--- a/lib/gitlab/git_access.rb
+++ b/lib/gitlab/git_access.rb
@@ -7,7 +7,8 @@ module Gitlab
ERROR_MESSAGES = {
upload: 'You are not allowed to upload code for this project.',
download: 'You are not allowed to download code from this project.',
- deploy_key: 'Deploy keys are not allowed to push code.',
+ deploy_key_upload:
+ 'This deploy key does not have write access to this project.',
no_repo: 'A repository for this project does not exist yet.'
}
@@ -31,12 +32,13 @@ module Gitlab
check_active_user!
check_project_accessibility!
check_command_existence!(cmd)
+ check_repository_existence!
case cmd
when *DOWNLOAD_COMMANDS
- download_access_check
+ check_download_access!
when *PUSH_COMMANDS
- push_access_check(changes)
+ check_push_access!(changes)
end
build_status_object(true)
@@ -44,32 +46,10 @@ module Gitlab
build_status_object(false, ex.message)
end
- def download_access_check
- if user
- user_download_access_check
- elsif deploy_key.nil? && !guest_can_downlod_code?
- raise UnauthorizedError, ERROR_MESSAGES[:download]
- end
- end
-
- def push_access_check(changes)
- if user
- user_push_access_check(changes)
- else
- raise UnauthorizedError, ERROR_MESSAGES[deploy_key ? :deploy_key : :upload]
- end
- end
-
- def guest_can_downlod_code?
+ def guest_can_download_code?
Guest.can?(:download_code, project)
end
- def user_download_access_check
- unless user_can_download_code? || build_can_download_code?
- raise UnauthorizedError, ERROR_MESSAGES[:download]
- end
- end
-
def user_can_download_code?
authentication_abilities.include?(:download_code) && user_access.can_do_action?(:download_code)
end
@@ -78,35 +58,6 @@ module Gitlab
authentication_abilities.include?(:build_download_code) && user_access.can_do_action?(:build_download_code)
end
- def user_push_access_check(changes)
- unless authentication_abilities.include?(:push_code)
- raise UnauthorizedError, ERROR_MESSAGES[:upload]
- end
-
- if changes.blank?
- return # Allow access.
- end
-
- unless project.repository.exists?
- raise UnauthorizedError, ERROR_MESSAGES[:no_repo]
- end
-
- changes_list = Gitlab::ChangesList.new(changes)
-
- # Iterate over all changes to find if user allowed all of them to be applied
- 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
- raise UnauthorizedError, status.message
- end
- end
- end
-
- def change_access_check(change)
- Checks::ChangeAccess.new(change, user_access: user_access, project: project, env: @env).exec
- end
-
def protocol_allowed?
Gitlab::ProtocolAccess.allowed?(protocol)
end
@@ -120,6 +71,8 @@ module Gitlab
end
def check_active_user!
+ return if deploy_key?
+
if user && !user_access.allowed?
raise UnauthorizedError, "Your account has been blocked."
end
@@ -137,33 +90,92 @@ module Gitlab
end
end
- def matching_merge_request?(newrev, branch_name)
- Checks::MatchingMergeRequest.new(newrev, branch_name, project).match?
+ def check_repository_existence!
+ unless project.repository.exists?
+ raise UnauthorizedError, ERROR_MESSAGES[:no_repo]
+ end
end
- def deploy_key
- actor if actor.is_a?(DeployKey)
+ def check_download_access!
+ return if deploy_key?
+
+ passed = user_can_download_code? ||
+ build_can_download_code? ||
+ guest_can_download_code?
+
+ unless passed
+ raise UnauthorizedError, ERROR_MESSAGES[:download]
+ end
end
- def deploy_key_can_read_project?
+ def check_push_access!(changes)
if deploy_key
- return true if project.public?
- deploy_key.projects.include?(project)
+ check_deploy_key_push_access!
+ elsif user
+ check_user_push_access!
else
- false
+ raise UnauthorizedError, ERROR_MESSAGES[:upload]
end
+
+ return if changes.blank? # Allow access.
+
+ check_change_access!(changes)
end
- def can_read_project?
- if user
- user_access.can_read_project?
- elsif deploy_key
- deploy_key_can_read_project?
- else
- Guest.can?(:read_project, project)
+ def check_user_push_access!
+ unless authentication_abilities.include?(:push_code)
+ raise UnauthorizedError, ERROR_MESSAGES[:upload]
end
end
+ def check_deploy_key_push_access!
+ unless deploy_key.can_push_to?(project)
+ raise UnauthorizedError, ERROR_MESSAGES[:deploy_key_upload]
+ end
+ end
+
+ def check_change_access!(changes)
+ changes_list = Gitlab::ChangesList.new(changes)
+
+ # Iterate over all changes to find if user allowed all of them to be applied
+ changes_list.each do |change|
+ status = check_single_change_access(change)
+ unless status.allowed?
+ # If user does not have access to make at least one change - cancel all push
+ raise UnauthorizedError, status.message
+ end
+ end
+ end
+
+ def check_single_change_access(change)
+ Checks::ChangeAccess.new(
+ change,
+ user_access: user_access,
+ project: project,
+ env: @env,
+ skip_authorization: deploy_key?).exec
+ end
+
+ def matching_merge_request?(newrev, branch_name)
+ Checks::MatchingMergeRequest.new(newrev, branch_name, project).match?
+ end
+
+ def deploy_key
+ actor if deploy_key?
+ end
+
+ def deploy_key?
+ actor.is_a?(DeployKey)
+ end
+
+ def can_read_project?
+ if deploy_key
+ deploy_key.has_access_to?(project)
+ elsif user
+ user.can?(:read_project, project)
+ end || Guest.can?(:read_project, project)
+ end
+
protected
def user
diff --git a/lib/gitlab/git_access_wiki.rb b/lib/gitlab/git_access_wiki.rb
index 2c06c4ff1ef..67eaa5e088d 100644
--- a/lib/gitlab/git_access_wiki.rb
+++ b/lib/gitlab/git_access_wiki.rb
@@ -1,6 +1,6 @@
module Gitlab
class GitAccessWiki < GitAccess
- def guest_can_downlod_code?
+ def guest_can_download_code?
Guest.can?(:download_wiki_code, project)
end
@@ -8,7 +8,7 @@ module Gitlab
authentication_abilities.include?(:download_code) && user_access.can_do_action?(:download_wiki_code)
end
- def change_access_check(change)
+ def check_single_change_access(change)
if user_access.can_do_action?(:create_wiki)
build_status_object(true)
else
diff --git a/lib/gitlab/git_post_receive.rb b/lib/gitlab/git_post_receive.rb
index d32bdd86427..6babea144c7 100644
--- a/lib/gitlab/git_post_receive.rb
+++ b/lib/gitlab/git_post_receive.rb
@@ -30,11 +30,11 @@ module Gitlab
def retrieve_project_and_type
@type = :project
- @project = Project.find_with_namespace(@repo_path)
+ @project = Project.find_by_full_path(@repo_path)
if @repo_path.end_with?('.wiki') && !@project
@type = :wiki
- @project = Project.find_with_namespace(@repo_path.gsub(/\.wiki\z/, ''))
+ @project = Project.find_by_full_path(@repo_path.gsub(/\.wiki\z/, ''))
end
end
diff --git a/lib/gitlab/github_import/project_creator.rb b/lib/gitlab/github_import/project_creator.rb
index 3f635be22ba..a55adc9b1c8 100644
--- a/lib/gitlab/github_import/project_creator.rb
+++ b/lib/gitlab/github_import/project_creator.rb
@@ -1,6 +1,8 @@
module Gitlab
module GithubImport
class ProjectCreator
+ include Gitlab::CurrentSettings
+
attr_reader :repo, :name, :namespace, :current_user, :session_data, :type
def initialize(repo, name, namespace, current_user, session_data, type: 'github')
@@ -34,7 +36,7 @@ module Gitlab
end
def visibility_level
- repo.private ? Gitlab::VisibilityLevel::PRIVATE : ApplicationSetting.current.default_project_visibility
+ repo.private ? Gitlab::VisibilityLevel::PRIVATE : current_application_settings.default_project_visibility
end
#
diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb
index 4d4e04e9e35..b8a5ac907a4 100644
--- a/lib/gitlab/gon_helper.rb
+++ b/lib/gitlab/gon_helper.rb
@@ -13,6 +13,7 @@ module Gitlab
if current_user
gon.current_user_id = current_user.id
+ gon.current_username = current_user.username
end
end
end
diff --git a/lib/gitlab/import_export.rb b/lib/gitlab/import_export.rb
index eb667a85b78..d679edec36b 100644
--- a/lib/gitlab/import_export.rb
+++ b/lib/gitlab/import_export.rb
@@ -3,7 +3,7 @@ module Gitlab
extend self
# For every version update, the version history in import_export.md has to be kept up to date.
- VERSION = '0.1.5'
+ VERSION = '0.1.6'
FILENAME_LIMIT = 50
def export_path(relative_path:)
diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml
index e6ecd118609..416194e57d7 100644
--- a/lib/gitlab/import_export/import_export.yml
+++ b/lib/gitlab/import_export/import_export.yml
@@ -6,6 +6,7 @@ project_tree:
- :events
- issues:
- :events
+ - :timelogs
- notes:
- :author
- :events
@@ -27,6 +28,7 @@ project_tree:
- :events
- :merge_request_diff
- :events
+ - :timelogs
- label_links:
- label:
:priorities
@@ -37,7 +39,6 @@ project_tree:
- :author
- :events
- :statuses
- - :variables
- :triggers
- :deploy_keys
- :services
diff --git a/lib/gitlab/import_export/members_mapper.rb b/lib/gitlab/import_export/members_mapper.rb
index b790733f4a7..8b8e48aac76 100644
--- a/lib/gitlab/import_export/members_mapper.rb
+++ b/lib/gitlab/import_export/members_mapper.rb
@@ -1,13 +1,10 @@
module Gitlab
module ImportExport
class MembersMapper
- attr_reader :missing_author_ids
-
def initialize(exported_members:, user:, project:)
- @exported_members = exported_members
+ @exported_members = user.admin? ? exported_members : []
@user = user
@project = project
- @missing_author_ids = []
# This needs to run first, as second call would be from #map
# which means project members already exist.
@@ -35,16 +32,21 @@ module Gitlab
@user.id
end
+ def include?(old_author_id)
+ map.keys.include?(old_author_id) && map[old_author_id] != default_user_id
+ end
+
private
def missing_keys_tracking_hash
Hash.new do |_, key|
- @missing_author_ids << key
default_user_id
end
end
def ensure_default_member!
+ @project.project_members.destroy_all
+
ProjectMember.create!(user: @user, access_level: ProjectMember::MASTER, source_id: @project.id, importing: true)
end
@@ -64,7 +66,7 @@ module Gitlab
end
def find_project_user_query(member)
- user_arel[:username].eq(member['user']['username']).or(user_arel[:email].eq(member['user']['email']))
+ user_arel[:email].eq(member['user']['email']).or(user_arel[:username].eq(member['user']['username']))
end
def user_arel
diff --git a/lib/gitlab/import_export/project_tree_saver.rb b/lib/gitlab/import_export/project_tree_saver.rb
index 2fbf437ec26..b79be62245b 100644
--- a/lib/gitlab/import_export/project_tree_saver.rb
+++ b/lib/gitlab/import_export/project_tree_saver.rb
@@ -5,8 +5,9 @@ module Gitlab
attr_reader :full_path
- def initialize(project:, shared:)
+ def initialize(project:, current_user:, shared:)
@project = project
+ @current_user = current_user
@shared = shared
@full_path = File.join(@shared.export_path, ImportExport.project_filename)
end
@@ -24,7 +25,29 @@ module Gitlab
private
def project_json_tree
- @project.to_json(Gitlab::ImportExport::Reader.new(shared: @shared).project_tree)
+ project_json['project_members'] += group_members_json
+
+ project_json.to_json
+ end
+
+ def project_json
+ @project_json ||= @project.as_json(reader.project_tree)
+ end
+
+ def reader
+ @reader ||= Gitlab::ImportExport::Reader.new(shared: @shared)
+ end
+
+ def group_members_json
+ group_members.as_json(reader.group_members_tree).each do |group_member|
+ group_member['source_type'] = 'Project' # Make group members project members of the future import
+ end
+ end
+
+ def group_members
+ return [] unless @current_user.can?(:admin_group, @project.group)
+
+ MembersFinder.new(@project.project_members, @project.group).execute(@current_user)
end
end
end
diff --git a/lib/gitlab/import_export/reader.rb b/lib/gitlab/import_export/reader.rb
index 5021a1a14ce..a1e7159fe42 100644
--- a/lib/gitlab/import_export/reader.rb
+++ b/lib/gitlab/import_export/reader.rb
@@ -21,6 +21,10 @@ module Gitlab
false
end
+ def group_members_tree
+ @attributes_finder.find_included(:project_members).merge(include: @attributes_finder.find(:user))
+ end
+
private
# Builds a hash in the format described here: http://api.rubyonrails.org/classes/ActiveModel/Serializers/JSON.html
diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb
index 65b229ca8ff..fae792237d9 100644
--- a/lib/gitlab/import_export/relation_factory.rb
+++ b/lib/gitlab/import_export/relation_factory.rb
@@ -4,7 +4,6 @@ module Gitlab
OVERRIDES = { snippets: :project_snippets,
pipelines: 'Ci::Pipeline',
statuses: 'commit_status',
- variables: 'Ci::Variable',
triggers: 'Ci::Trigger',
builds: 'Ci::Build',
hooks: 'ProjectHook',
@@ -14,7 +13,7 @@ module Gitlab
priorities: :label_priorities,
label: :project_label }.freeze
- USER_REFERENCES = %w[author_id assignee_id updated_by_id user_id created_by_id merge_user_id].freeze
+ USER_REFERENCES = %w[author_id assignee_id updated_by_id user_id created_by_id merge_user_id resolved_by_id].freeze
PROJECT_REFERENCES = %w[project_id source_project_id gl_project_id target_project_id].freeze
@@ -22,7 +21,9 @@ module Gitlab
IMPORTED_OBJECT_MAX_RETRIES = 5.freeze
- EXISTING_OBJECT_CHECK = %i[milestone milestones label labels project_label project_labels project_label group_label].freeze
+ EXISTING_OBJECT_CHECK = %i[milestone milestones label labels project_label project_labels group_label group_labels].freeze
+
+ TOKEN_RESET_MODELS = %w[Ci::Trigger Ci::Build ProjectHook].freeze
def self.create(*args)
new(*args).create
@@ -61,7 +62,9 @@ module Gitlab
update_project_references
handle_group_label if group_label?
- reset_ci_tokens if @relation_name == 'Ci::Trigger'
+ reset_tokens!
+ remove_encrypted_attributes!
+
@relation_hash['data'].deep_symbolize_keys! if @relation_name == :events && @relation_hash['data']
set_st_diffs if @relation_name == :merge_request_diff
end
@@ -80,17 +83,13 @@ module Gitlab
# is left.
def set_note_author
old_author_id = @relation_hash['author_id']
-
- # Users with admin access can map users
- @relation_hash['author_id'] = admin_user? ? @members_mapper.map[old_author_id] : @members_mapper.default_user_id
-
author = @relation_hash.delete('author')
- update_note_for_missing_author(author['name']) if missing_author?(old_author_id)
+ update_note_for_missing_author(author['name']) unless has_author?(old_author_id)
end
- def missing_author?(old_author_id)
- !admin_user? || @members_mapper.missing_author_ids.include?(old_author_id)
+ def has_author?(old_author_id)
+ admin_user? && @members_mapper.include?(old_author_id)
end
def missing_author_note(updated_at, author_name)
@@ -144,11 +143,22 @@ module Gitlab
end
end
- def reset_ci_tokens
- return unless Gitlab::ImportExport.reset_tokens?
+ def reset_tokens!
+ return unless Gitlab::ImportExport.reset_tokens? && TOKEN_RESET_MODELS.include?(@relation_name.to_s)
# If we import/export a project to the same instance, tokens will have to be reset.
- @relation_hash['token'] = nil
+ # We also have to reset them to avoid issues when the gitlab secrets file cannot be copied across.
+ relation_class.attribute_names.select { |name| name.include?('token') }.each do |token|
+ @relation_hash[token] = nil
+ end
+ end
+
+ def remove_encrypted_attributes!
+ return unless relation_class.respond_to?(:encrypted_attributes) && relation_class.encrypted_attributes.any?
+
+ relation_class.encrypted_attributes.each_key do |key|
+ @relation_hash[key.to_s] = nil
+ end
end
def relation_class
@@ -189,7 +199,7 @@ module Gitlab
# Otherwise always create the record, skipping the extra SELECT clause.
@existing_or_new_object ||= begin
if EXISTING_OBJECT_CHECK.include?(@relation_name)
- attribute_hash = attribute_hash_for(['events', 'priorities'])
+ attribute_hash = attribute_hash_for(['events'])
existing_object.assign_attributes(attribute_hash) if attribute_hash.any?
@@ -210,9 +220,8 @@ module Gitlab
def existing_object
@existing_object ||=
begin
- finder_attributes = @relation_name == :group_label ? %w[title group_id] : %w[title project_id]
- finder_hash = parsed_relation_hash.slice(*finder_attributes)
- existing_object = relation_class.find_or_create_by(finder_hash)
+ existing_object = find_or_create_object!
+
# Done in two steps, as MySQL behaves differently than PostgreSQL using
# the +find_or_create_by+ method and does not return the ID the second time.
existing_object.update!(parsed_relation_hash)
@@ -224,6 +233,25 @@ module Gitlab
@relation_name == :services && parsed_relation_hash['type'] &&
!Object.const_defined?(parsed_relation_hash['type'])
end
+
+ def find_or_create_object!
+ finder_attributes = @relation_name == :group_label ? %w[title group_id] : %w[title project_id]
+ finder_hash = parsed_relation_hash.slice(*finder_attributes)
+
+ if label?
+ label = relation_class.find_or_initialize_by(finder_hash)
+ parsed_relation_hash.delete('priorities') if label.persisted?
+
+ label.save!
+ label
+ else
+ relation_class.find_or_create_by(finder_hash)
+ end
+ end
+
+ def label?
+ @relation_name.to_s.include?('label')
+ end
end
end
end
diff --git a/lib/gitlab/import_sources.rb b/lib/gitlab/import_sources.rb
index 45958710c13..52276cbcd9a 100644
--- a/lib/gitlab/import_sources.rb
+++ b/lib/gitlab/import_sources.rb
@@ -5,8 +5,6 @@
#
module Gitlab
module ImportSources
- extend CurrentSettings
-
ImportSource = Struct.new(:name, :title, :importer)
ImportTable = [
diff --git a/lib/gitlab/incoming_email.rb b/lib/gitlab/incoming_email.rb
index 801dfde9a36..c9122a23568 100644
--- a/lib/gitlab/incoming_email.rb
+++ b/lib/gitlab/incoming_email.rb
@@ -1,10 +1,9 @@
module Gitlab
module IncomingEmail
+ UNSUBSCRIBE_SUFFIX = '+unsubscribe'.freeze
WILDCARD_PLACEHOLDER = '%{key}'.freeze
class << self
- FALLBACK_MESSAGE_ID_REGEX = /\Areply\-(.+)@#{Gitlab.config.gitlab.host}\Z/.freeze
-
def enabled?
config.enabled && config.address
end
@@ -18,7 +17,11 @@ module Gitlab
end
def reply_address(key)
- config.address.gsub(WILDCARD_PLACEHOLDER, key)
+ config.address.sub(WILDCARD_PLACEHOLDER, key)
+ end
+
+ def unsubscribe_address(key)
+ config.address.sub(WILDCARD_PLACEHOLDER, "#{key}#{UNSUBSCRIBE_SUFFIX}")
end
def key_from_address(address)
@@ -32,10 +35,14 @@ module Gitlab
end
def key_from_fallback_message_id(mail_id)
- match = mail_id.match(FALLBACK_MESSAGE_ID_REGEX)
- return unless match
+ message_id_regexp = /\Areply\-(.+)@#{Gitlab.config.gitlab.host}\z/
- match[1]
+ mail_id[message_id_regexp, 1]
+ end
+
+ def scan_fallback_references(references)
+ # It's looking for each <...>
+ references.scan(/(?!<)[^<>]+(?=>)/)
end
def config
@@ -49,7 +56,7 @@ module Gitlab
return nil unless wildcard_address
regex = Regexp.escape(wildcard_address)
- regex = regex.gsub(Regexp.escape('%{key}'), "(.+)")
+ regex = regex.sub(Regexp.escape(WILDCARD_PLACEHOLDER), '(.+)')
Regexp.new(regex).freeze
end
end
diff --git a/lib/gitlab/job_waiter.rb b/lib/gitlab/job_waiter.rb
new file mode 100644
index 00000000000..8db91d25a4b
--- /dev/null
+++ b/lib/gitlab/job_waiter.rb
@@ -0,0 +1,27 @@
+module Gitlab
+ # JobWaiter can be used to wait for a number of Sidekiq jobs to complete.
+ class JobWaiter
+ # The sleep interval between checking keys, in seconds.
+ INTERVAL = 0.1
+
+ # jobs - The job IDs to wait for.
+ def initialize(jobs)
+ @jobs = jobs
+ end
+
+ # Waits for all the jobs to be completed.
+ #
+ # timeout - The maximum amount of seconds to block the caller for. This
+ # ensures we don't indefinitely block a caller in case a job takes
+ # long to process, or is never processed.
+ def wait(timeout = 60)
+ start = Time.current
+
+ while (Time.current - start) <= timeout
+ break if SidekiqStatus.all_completed?(@jobs)
+
+ sleep(INTERVAL) # to not overload Redis too much.
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/kubernetes.rb b/lib/gitlab/kubernetes.rb
index 288771c1c12..3a7af363548 100644
--- a/lib/gitlab/kubernetes.rb
+++ b/lib/gitlab/kubernetes.rb
@@ -43,10 +43,10 @@ module Gitlab
end
end
- def add_terminal_auth(terminal, token, ca_pem = nil)
+ def add_terminal_auth(terminal, token:, max_session_time:, ca_pem: nil)
terminal[:headers]['Authorization'] << "Bearer #{token}"
+ terminal[:max_session_time] = max_session_time
terminal[:ca_pem] = ca_pem if ca_pem.present?
- terminal
end
def container_exec_url(api_url, namespace, pod_name, container_name)
diff --git a/lib/gitlab/ldap/access.rb b/lib/gitlab/ldap/access.rb
index 7e06bd2b0fb..54a5b1d31cd 100644
--- a/lib/gitlab/ldap/access.rb
+++ b/lib/gitlab/ldap/access.rb
@@ -34,21 +34,21 @@ module Gitlab
def allowed?
if ldap_user
unless ldap_config.active_directory
- user.activate if user.ldap_blocked?
+ unblock_user(user, 'is available again') if user.ldap_blocked?
return true
end
# Block user in GitLab if he/she was blocked in AD
if Gitlab::LDAP::Person.disabled_via_active_directory?(user.ldap_identity.extern_uid, adapter)
- user.ldap_block
+ block_user(user, 'is disabled in Active Directory')
false
else
- user.activate if user.ldap_blocked?
+ unblock_user(user, 'is not disabled anymore') if user.ldap_blocked?
true
end
else
# Block the user if they no longer exist in LDAP/AD
- user.ldap_block
+ block_user(user, 'does not exist anymore')
false
end
end
@@ -64,6 +64,24 @@ module Gitlab
def ldap_user
@ldap_user ||= Gitlab::LDAP::Person.find_by_dn(user.ldap_identity.extern_uid, adapter)
end
+
+ def block_user(user, reason)
+ user.ldap_block
+
+ Gitlab::AppLogger.info(
+ "LDAP account \"#{user.ldap_identity.extern_uid}\" #{reason}, " \
+ "blocking Gitlab user \"#{user.name}\" (#{user.email})"
+ )
+ end
+
+ def unblock_user(user, reason)
+ user.activate
+
+ Gitlab::AppLogger.info(
+ "LDAP account \"#{user.ldap_identity.extern_uid}\" #{reason}, " \
+ "unblocking Gitlab user \"#{user.name}\" (#{user.email})"
+ )
+ end
end
end
end
diff --git a/lib/gitlab/ldap/auth_hash.rb b/lib/gitlab/ldap/auth_hash.rb
index bf4dd9542d5..95378e5a769 100644
--- a/lib/gitlab/ldap/auth_hash.rb
+++ b/lib/gitlab/ldap/auth_hash.rb
@@ -25,7 +25,7 @@ module Gitlab
end
def get_raw(key)
- auth_hash.extra[:raw_info][key]
+ auth_hash.extra[:raw_info][key] if auth_hash.extra
end
def ldap_config
diff --git a/lib/gitlab/ldap/config.rb b/lib/gitlab/ldap/config.rb
index de52ef3fc65..28129198438 100644
--- a/lib/gitlab/ldap/config.rb
+++ b/lib/gitlab/ldap/config.rb
@@ -107,7 +107,7 @@ module Gitlab
end
def attributes
- options['attributes']
+ default_attributes.merge(options['attributes'])
end
def timeout
@@ -130,6 +130,16 @@ module Gitlab
end
end
+ def default_attributes
+ {
+ 'username' => %w(uid userid sAMAccountName),
+ 'email' => %w(mail email userPrincipalName),
+ 'name' => 'cn',
+ 'first_name' => 'givenName',
+ 'last_name' => 'sn'
+ }
+ end
+
protected
def base_options
diff --git a/lib/gitlab/ldap/person.rb b/lib/gitlab/ldap/person.rb
index b81f3e8e8f5..7084fd1767d 100644
--- a/lib/gitlab/ldap/person.rb
+++ b/lib/gitlab/ldap/person.rb
@@ -28,7 +28,7 @@ module Gitlab
end
def name
- entry.cn.first
+ attribute_value(:name).first
end
def uid
@@ -40,7 +40,7 @@ module Gitlab
end
def email
- entry.try(:mail)
+ attribute_value(:email)
end
def dn
@@ -56,6 +56,19 @@ module Gitlab
def config
@config ||= Gitlab::LDAP::Config.new(provider)
end
+
+ # Using the LDAP attributes configuration, find and return the first
+ # attribute with a value. For example, by default, when given 'email',
+ # this method looks for 'mail', 'email' and 'userPrincipalName' and
+ # returns the first with a value.
+ def attribute_value(attribute)
+ attributes = Array(config.attributes[attribute.to_s])
+ selected_attr = attributes.find { |attr| entry.respond_to?(attr) }
+
+ return nil unless selected_attr
+
+ entry.public_send(selected_attr)
+ end
end
end
end
diff --git a/lib/gitlab/metrics/rack_middleware.rb b/lib/gitlab/metrics/rack_middleware.rb
index 01c96a6fe96..47f88727fc8 100644
--- a/lib/gitlab/metrics/rack_middleware.rb
+++ b/lib/gitlab/metrics/rack_middleware.rb
@@ -70,8 +70,19 @@ module Gitlab
def tag_endpoint(trans, env)
endpoint = env[ENDPOINT_KEY]
- path = endpoint_paths_cache[endpoint.route.route_method][endpoint.route.route_path]
- trans.action = "Grape##{endpoint.route.route_method} #{path}"
+
+ begin
+ route = endpoint.route
+ rescue
+ # endpoint.route is calling env[Grape::Env::GRAPE_ROUTING_ARGS][:route_info]
+ # but env[Grape::Env::GRAPE_ROUTING_ARGS] is nil in the case of a 405 response
+ # so we're rescuing exceptions and bailing out
+ end
+
+ if route
+ path = endpoint_paths_cache[route.request_method][route.path]
+ trans.action = "Grape##{route.request_method} #{path}"
+ end
end
private
diff --git a/lib/gitlab/middleware/multipart.rb b/lib/gitlab/middleware/multipart.rb
index 65713e73a59..dd99f9bb7d7 100644
--- a/lib/gitlab/middleware/multipart.rb
+++ b/lib/gitlab/middleware/multipart.rb
@@ -42,7 +42,7 @@ module Gitlab
key, value = parsed_field.first
if value.nil?
- value = File.open(tmp_path)
+ value = open_file(tmp_path)
@open_files << value
else
value = decorate_params_value(value, @request.params[key], tmp_path)
@@ -68,7 +68,7 @@ module Gitlab
case path_value
when nil
- value_hash[path_key] = File.open(tmp_path)
+ value_hash[path_key] = open_file(tmp_path)
@open_files << value_hash[path_key]
value_hash
when Hash
@@ -78,6 +78,10 @@ module Gitlab
raise "unexpected path value: #{path_value.inspect}"
end
end
+
+ def open_file(path)
+ ::UploadedFile.new(path, File.basename(path), 'application/octet-stream')
+ end
end
def initialize(app)
diff --git a/lib/gitlab/middleware/webpack_proxy.rb b/lib/gitlab/middleware/webpack_proxy.rb
new file mode 100644
index 00000000000..3fe32adeade
--- /dev/null
+++ b/lib/gitlab/middleware/webpack_proxy.rb
@@ -0,0 +1,24 @@
+# This Rack middleware is intended to proxy the webpack assets directory to the
+# webpack-dev-server. It is only intended for use in development.
+
+module Gitlab
+ module Middleware
+ class WebpackProxy < Rack::Proxy
+ def initialize(app = nil, opts = {})
+ @proxy_host = opts.fetch(:proxy_host, 'localhost')
+ @proxy_port = opts.fetch(:proxy_port, 3808)
+ @proxy_path = opts[:proxy_path] if opts[:proxy_path]
+ super(app, opts)
+ end
+
+ def perform_request(env)
+ unless @proxy_path && env['PATH_INFO'].start_with?("/#{@proxy_path}")
+ return @app.call(env)
+ end
+
+ env['HTTP_HOST'] = "#{@proxy_host}:#{@proxy_port}"
+ super(env)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/pages_transfer.rb b/lib/gitlab/pages_transfer.rb
new file mode 100644
index 00000000000..fb215f27cbd
--- /dev/null
+++ b/lib/gitlab/pages_transfer.rb
@@ -0,0 +1,7 @@
+module Gitlab
+ class PagesTransfer < ProjectTransfer
+ def root_dir
+ Gitlab.config.pages.path
+ end
+ end
+end
diff --git a/lib/gitlab/project_search_results.rb b/lib/gitlab/project_search_results.rb
index 6bdf3db9cb8..db325c00705 100644
--- a/lib/gitlab/project_search_results.rb
+++ b/lib/gitlab/project_search_results.rb
@@ -71,6 +71,14 @@ module Gitlab
)
end
+ def single_commit_result?
+ commits_count == 1 && total_result_count == 1
+ end
+
+ def total_result_count
+ issues_count + merge_requests_count + milestones_count + notes_count + blobs_count + wiki_blobs_count + commits_count
+ end
+
private
def blobs
@@ -114,7 +122,25 @@ module Gitlab
end
def commits
- @commits ||= project.repository.find_commits_by_message(query)
+ @commits ||= find_commits(query)
+ end
+
+ def find_commits(query)
+ return [] unless Ability.allowed?(@current_user, :download_code, @project)
+
+ commits = find_commits_by_message(query)
+ commit_by_sha = find_commit_by_sha(query)
+ commits |= [commit_by_sha] if commit_by_sha
+ commits
+ end
+
+ def find_commits_by_message(query)
+ project.repository.find_commits_by_message(query)
+ end
+
+ def find_commit_by_sha(query)
+ key = query.strip
+ project.repository.commit(key) if Commit.valid_hash?(key)
end
def project_ids_relation
diff --git a/lib/gitlab/project_transfer.rb b/lib/gitlab/project_transfer.rb
new file mode 100644
index 00000000000..1bba0b78e2f
--- /dev/null
+++ b/lib/gitlab/project_transfer.rb
@@ -0,0 +1,35 @@
+module Gitlab
+ class ProjectTransfer
+ def move_project(project_path, namespace_path_was, namespace_path)
+ new_namespace_folder = File.join(root_dir, namespace_path)
+ FileUtils.mkdir_p(new_namespace_folder) unless Dir.exist?(new_namespace_folder)
+ from = File.join(root_dir, namespace_path_was, project_path)
+ to = File.join(root_dir, namespace_path, project_path)
+ move(from, to, "")
+ end
+
+ def rename_project(path_was, path, namespace_path)
+ base_dir = File.join(root_dir, namespace_path)
+ move(path_was, path, base_dir)
+ end
+
+ def rename_namespace(path_was, path)
+ move(path_was, path)
+ end
+
+ def root_dir
+ raise NotImplementedError
+ end
+
+ private
+
+ def move(path_was, path, base_dir = nil)
+ base_dir = root_dir unless base_dir
+ from = File.join(base_dir, path_was)
+ to = File.join(base_dir, path)
+ FileUtils.mv(from, to)
+ rescue Errno::ENOENT
+ false
+ end
+ end
+end
diff --git a/lib/gitlab/recaptcha.rb b/lib/gitlab/recaptcha.rb
index 70e7f25d518..4bc76ea033f 100644
--- a/lib/gitlab/recaptcha.rb
+++ b/lib/gitlab/recaptcha.rb
@@ -10,5 +10,9 @@ module Gitlab
true
end
end
+
+ def self.enabled?
+ current_application_settings.recaptcha_enabled
+ end
end
end
diff --git a/lib/gitlab/redis.rb b/lib/gitlab/redis.rb
index 9226da2d6b1..9384102acec 100644
--- a/lib/gitlab/redis.rb
+++ b/lib/gitlab/redis.rb
@@ -42,7 +42,7 @@ module Gitlab
return @_raw_config if defined?(@_raw_config)
begin
- @_raw_config = File.read(CONFIG_FILE).freeze
+ @_raw_config = ERB.new(File.read(CONFIG_FILE)).result.freeze
rescue Errno::ENOENT
@_raw_config = false
end
diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb
index 9e0b0e5ea98..a3fa7c1331a 100644
--- a/lib/gitlab/regex.rb
+++ b/lib/gitlab/regex.rb
@@ -61,11 +61,11 @@ module Gitlab
end
def file_name_regex
- @file_name_regex ||= /\A[[[:alnum:]]_\-\.\@]*\z/.freeze
+ @file_name_regex ||= /\A[[[:alnum:]]_\-\.\@\+]*\z/.freeze
end
def file_name_regex_message
- "can contain only letters, digits, '_', '-', '@' and '.'."
+ "can contain only letters, digits, '_', '-', '@', '+' and '.'."
end
def file_path_regex
diff --git a/lib/gitlab/request_profiler/middleware.rb b/lib/gitlab/request_profiler/middleware.rb
index 786e1d49f5e..ef42b0557e0 100644
--- a/lib/gitlab/request_profiler/middleware.rb
+++ b/lib/gitlab/request_profiler/middleware.rb
@@ -1,5 +1,4 @@
require 'ruby-prof'
-require_dependency 'gitlab/request_profiler'
module Gitlab
module RequestProfiler
@@ -20,7 +19,7 @@ module Gitlab
header_token = env['HTTP_X_PROFILE_TOKEN']
return unless header_token.present?
- profile_token = RequestProfiler.profile_token
+ profile_token = Gitlab::RequestProfiler.profile_token
return unless profile_token.present?
header_token == profile_token
diff --git a/lib/gitlab/route_map.rb b/lib/gitlab/route_map.rb
new file mode 100644
index 00000000000..72d00abfcc2
--- /dev/null
+++ b/lib/gitlab/route_map.rb
@@ -0,0 +1,50 @@
+module Gitlab
+ class RouteMap
+ class FormatError < StandardError; end
+
+ def initialize(data)
+ begin
+ entries = YAML.safe_load(data)
+ rescue
+ raise FormatError, 'Route map is not valid YAML'
+ end
+
+ raise FormatError, 'Route map is not an array' unless entries.is_a?(Array)
+
+ @map = entries.map { |entry| parse_entry(entry) }
+ end
+
+ def public_path_for_source_path(path)
+ mapping = @map.find { |mapping| mapping[:source] === path }
+ return unless mapping
+
+ path.sub(mapping[:source], mapping[:public])
+ end
+
+ private
+
+ def parse_entry(entry)
+ raise FormatError, 'Route map entry is not a hash' unless entry.is_a?(Hash)
+ raise FormatError, 'Route map entry does not have a source key' unless entry.has_key?('source')
+ raise FormatError, 'Route map entry does not have a public key' unless entry.has_key?('public')
+
+ source_pattern = entry['source']
+ public_path = entry['public']
+
+ if source_pattern.start_with?('/') && source_pattern.end_with?('/')
+ source_pattern = source_pattern[1...-1].gsub('\/', '/')
+
+ begin
+ source_pattern = /\A#{source_pattern}\z/
+ rescue RegexpError => e
+ raise FormatError, "Route map entry source is not a valid regular expression: #{e}"
+ end
+ end
+
+ {
+ source: source_pattern,
+ public: public_path
+ }
+ end
+ end
+end
diff --git a/lib/gitlab/search_results.rb b/lib/gitlab/search_results.rb
index 35212992698..c9c65f76f4b 100644
--- a/lib/gitlab/search_results.rb
+++ b/lib/gitlab/search_results.rb
@@ -43,6 +43,10 @@ module Gitlab
@milestones_count ||= milestones.count
end
+ def single_commit_result?
+ false
+ end
+
private
def projects
diff --git a/lib/gitlab/serialize/ci/variables.rb b/lib/gitlab/serialize/ci/variables.rb
deleted file mode 100644
index 3a9443bfcd9..00000000000
--- a/lib/gitlab/serialize/ci/variables.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-module Gitlab
- module Serialize
- module Ci
- # This serializer could make sure our YAML variables' keys and values
- # are always strings. This is more for legacy build data because
- # from now on we convert them into strings before saving to database.
- module Variables
- extend self
-
- def load(string)
- return unless string
-
- object = YAML.safe_load(string, [Symbol])
-
- object.map do |variable|
- variable[:key] = variable[:key].to_s
- variable
- end
- end
-
- def dump(object)
- YAML.dump(object)
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/serializer/ci/variables.rb b/lib/gitlab/serializer/ci/variables.rb
new file mode 100644
index 00000000000..c059c454eac
--- /dev/null
+++ b/lib/gitlab/serializer/ci/variables.rb
@@ -0,0 +1,27 @@
+module Gitlab
+ module Serializer
+ module Ci
+ # This serializer could make sure our YAML variables' keys and values
+ # are always strings. This is more for legacy build data because
+ # from now on we convert them into strings before saving to database.
+ module Variables
+ extend self
+
+ def load(string)
+ return unless string
+
+ object = YAML.safe_load(string, [Symbol])
+
+ object.map do |variable|
+ variable[:key] = variable[:key].to_s
+ variable
+ end
+ end
+
+ def dump(object)
+ YAML.dump(object)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/serializer/pagination.rb b/lib/gitlab/serializer/pagination.rb
new file mode 100644
index 00000000000..bf2c0acc729
--- /dev/null
+++ b/lib/gitlab/serializer/pagination.rb
@@ -0,0 +1,36 @@
+module Gitlab
+ module Serializer
+ class Pagination
+ class InvalidResourceError < StandardError; end
+ include ::API::Helpers::Pagination
+
+ def initialize(request, response)
+ @request = request
+ @response = response
+ end
+
+ def paginate(resource)
+ if resource.respond_to?(:page)
+ super(resource)
+ else
+ raise InvalidResourceError
+ end
+ end
+
+ private
+
+ # Methods needed by `API::Helpers::Pagination`
+ #
+
+ attr_reader :request
+
+ def params
+ @request.query_parameters
+ end
+
+ def header(header, value)
+ @response.headers[header] = value
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/backend/shell.rb b/lib/gitlab/shell.rb
index 82e194c1af1..82e194c1af1 100644
--- a/lib/gitlab/backend/shell.rb
+++ b/lib/gitlab/shell.rb
diff --git a/lib/gitlab/backend/shell_adapter.rb b/lib/gitlab/shell_adapter.rb
index fbe2a7a0d72..fbe2a7a0d72 100644
--- a/lib/gitlab/backend/shell_adapter.rb
+++ b/lib/gitlab/shell_adapter.rb
diff --git a/lib/gitlab/sidekiq_status.rb b/lib/gitlab/sidekiq_status.rb
new file mode 100644
index 00000000000..aadc401ff8d
--- /dev/null
+++ b/lib/gitlab/sidekiq_status.rb
@@ -0,0 +1,66 @@
+module Gitlab
+ # The SidekiqStatus module and its child classes can be used for checking if a
+ # Sidekiq job has been processed or not.
+ #
+ # To check if a job has been completed, simply pass the job ID to the
+ # `completed?` method:
+ #
+ # job_id = SomeWorker.perform_async(...)
+ #
+ # if Gitlab::SidekiqStatus.completed?(job_id)
+ # ...
+ # end
+ #
+ # For each job ID registered a separate key is stored in Redis, making lookups
+ # much faster than using Sidekiq's built-in job finding/status API. These keys
+ # expire after a certain period of time to prevent storing too many keys in
+ # Redis.
+ module SidekiqStatus
+ STATUS_KEY = 'gitlab-sidekiq-status:%s'.freeze
+
+ # The default time (in seconds) after which a status key is expired
+ # automatically. The default of 30 minutes should be more than sufficient
+ # for most jobs.
+ DEFAULT_EXPIRATION = 30.minutes.to_i
+
+ # Starts tracking of the given job.
+ #
+ # jid - The Sidekiq job ID
+ # expire - The expiration time of the Redis key.
+ def self.set(jid, expire = DEFAULT_EXPIRATION)
+ Sidekiq.redis do |redis|
+ redis.set(key_for(jid), 1, ex: expire)
+ end
+ end
+
+ # Stops the tracking of the given job.
+ #
+ # jid - The Sidekiq job ID to remove.
+ def self.unset(jid)
+ Sidekiq.redis do |redis|
+ redis.del(key_for(jid))
+ end
+ end
+
+ # Returns true if all the given job have been completed.
+ #
+ # jids - The Sidekiq job IDs to check.
+ #
+ # Returns true or false.
+ def self.all_completed?(jids)
+ keys = jids.map { |jid| key_for(jid) }
+
+ responses = Sidekiq.redis do |redis|
+ redis.pipelined do
+ keys.each { |key| redis.exists(key) }
+ end
+ end
+
+ responses.all? { |value| !value }
+ end
+
+ def self.key_for(jid)
+ STATUS_KEY % jid
+ end
+ end
+end
diff --git a/lib/gitlab/sidekiq_status/client_middleware.rb b/lib/gitlab/sidekiq_status/client_middleware.rb
new file mode 100644
index 00000000000..779a9998b22
--- /dev/null
+++ b/lib/gitlab/sidekiq_status/client_middleware.rb
@@ -0,0 +1,10 @@
+module Gitlab
+ module SidekiqStatus
+ class ClientMiddleware
+ def call(_, job, _, _)
+ SidekiqStatus.set(job['jid'])
+ yield
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/sidekiq_status/server_middleware.rb b/lib/gitlab/sidekiq_status/server_middleware.rb
new file mode 100644
index 00000000000..31dfa46ff9d
--- /dev/null
+++ b/lib/gitlab/sidekiq_status/server_middleware.rb
@@ -0,0 +1,13 @@
+module Gitlab
+ module SidekiqStatus
+ class ServerMiddleware
+ def call(worker, job, queue)
+ ret = yield
+
+ SidekiqStatus.unset(job['jid'])
+
+ ret
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/template/gitlab_ci_yml_template.rb b/lib/gitlab/template/gitlab_ci_yml_template.rb
index 8d1a1ed54c9..9d2ecee9756 100644
--- a/lib/gitlab/template/gitlab_ci_yml_template.rb
+++ b/lib/gitlab/template/gitlab_ci_yml_template.rb
@@ -13,8 +13,9 @@ module Gitlab
def categories
{
- "General" => '',
- "Pages" => 'Pages'
+ 'General' => '',
+ 'Pages' => 'Pages',
+ 'Auto deploy' => 'autodeploy'
}
end
@@ -25,6 +26,11 @@ module Gitlab
def finder(project = nil)
Gitlab::Template::Finders::GlobalTemplateFinder.new(self.base_dir, self.extension, self.categories)
end
+
+ def dropdown_names(context)
+ categories = context == 'autodeploy' ? ['Auto deploy'] : ['General', 'Pages']
+ super().slice(*categories)
+ end
end
end
end
diff --git a/lib/gitlab/themes.rb b/lib/gitlab/themes.rb
index d4020af76f9..19ab76ae80f 100644
--- a/lib/gitlab/themes.rb
+++ b/lib/gitlab/themes.rb
@@ -15,7 +15,7 @@ module Gitlab
Theme.new(1, 'Graphite', 'ui_graphite'),
Theme.new(2, 'Charcoal', 'ui_charcoal'),
Theme.new(3, 'Green', 'ui_green'),
- Theme.new(4, 'Gray', 'ui_gray'),
+ Theme.new(4, 'Black', 'ui_black'),
Theme.new(5, 'Violet', 'ui_violet'),
Theme.new(6, 'Blue', 'ui_blue')
].freeze
diff --git a/lib/gitlab/time_tracking_formatter.rb b/lib/gitlab/time_tracking_formatter.rb
new file mode 100644
index 00000000000..d615c24149a
--- /dev/null
+++ b/lib/gitlab/time_tracking_formatter.rb
@@ -0,0 +1,34 @@
+module Gitlab
+ module TimeTrackingFormatter
+ extend self
+
+ def parse(string)
+ with_custom_config do
+ string.sub!(/\A-/, '')
+
+ seconds = ChronicDuration.parse(string, default_unit: 'hours') rescue nil
+ seconds *= -1 if seconds && Regexp.last_match
+ seconds
+ end
+ end
+
+ def output(seconds)
+ with_custom_config do
+ ChronicDuration.output(seconds, format: :short, limit_to_hours: false, weeks: true) rescue nil
+ end
+ end
+
+ def with_custom_config
+ # We may want to configure it through project settings in a future version.
+ ChronicDuration.hours_per_day = 8
+ ChronicDuration.days_per_week = 5
+
+ result = yield
+
+ ChronicDuration.hours_per_day = 24
+ ChronicDuration.days_per_week = 7
+
+ result
+ end
+ end
+end
diff --git a/lib/gitlab/update_path_error.rb b/lib/gitlab/update_path_error.rb
new file mode 100644
index 00000000000..ce14cc887d0
--- /dev/null
+++ b/lib/gitlab/update_path_error.rb
@@ -0,0 +1,3 @@
+module Gitlab
+ class UpdatePathError < StandardError; end
+end
diff --git a/lib/gitlab/upgrader.rb b/lib/gitlab/upgrader.rb
index f3567f3ef85..e78d0c34a02 100644
--- a/lib/gitlab/upgrader.rb
+++ b/lib/gitlab/upgrader.rb
@@ -61,7 +61,7 @@ module Gitlab
"Switch to new version" => %W(#{Gitlab.config.git.bin_path} checkout v#{latest_version}),
"Install gems" => %W(bundle),
"Migrate DB" => %W(bundle exec rake db:migrate),
- "Recompile assets" => %W(bundle exec rake assets:clean assets:precompile),
+ "Recompile assets" => %W(bundle exec rake gitlab:assets:clean gitlab:assets:compile),
"Clear cache" => %W(bundle exec rake cache:clear)
}
end
diff --git a/lib/gitlab/uploads_transfer.rb b/lib/gitlab/uploads_transfer.rb
index be8fcc7b2d2..81701831a6a 100644
--- a/lib/gitlab/uploads_transfer.rb
+++ b/lib/gitlab/uploads_transfer.rb
@@ -1,33 +1,5 @@
module Gitlab
- class UploadsTransfer
- def move_project(project_path, namespace_path_was, namespace_path)
- new_namespace_folder = File.join(root_dir, namespace_path)
- FileUtils.mkdir_p(new_namespace_folder) unless Dir.exist?(new_namespace_folder)
- from = File.join(root_dir, namespace_path_was, project_path)
- to = File.join(root_dir, namespace_path, project_path)
- move(from, to, "")
- end
-
- def rename_project(path_was, path, namespace_path)
- base_dir = File.join(root_dir, namespace_path)
- move(path_was, path, base_dir)
- end
-
- def rename_namespace(path_was, path)
- move(path_was, path)
- end
-
- private
-
- def move(path_was, path, base_dir = nil)
- base_dir = root_dir unless base_dir
- from = File.join(base_dir, path_was)
- to = File.join(base_dir, path)
- FileUtils.mv(from, to)
- rescue Errno::ENOENT
- false
- end
-
+ class UploadsTransfer < ProjectTransfer
def root_dir
File.join(Rails.root, "public", "uploads")
end
diff --git a/lib/gitlab/user_access.rb b/lib/gitlab/user_access.rb
index 9858d2e7d83..6ce9b229294 100644
--- a/lib/gitlab/user_access.rb
+++ b/lib/gitlab/user_access.rb
@@ -8,6 +8,8 @@ module Gitlab
end
def can_do_action?(action)
+ return false if no_user_or_blocked?
+
@permission_cache ||= {}
@permission_cache[action] ||= user.can?(action, project)
end
@@ -17,7 +19,7 @@ module Gitlab
end
def allowed?
- return false if user.blank? || user.blocked?
+ return false if no_user_or_blocked?
if user.requires_ldap_check? && user.try_obtain_ldap_lease
return false unless Gitlab::LDAP::Access.allowed?(user)
@@ -27,20 +29,22 @@ module Gitlab
end
def can_push_to_branch?(ref)
- return false unless user
+ return false if no_user_or_blocked?
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) }
+ has_access = access_levels.any? { |access_level| access_level.check_access(user) }
+
+ has_access || !project.repository.branch_exists?(ref) && can_merge_to_branch?(ref)
else
user.can?(:push_code, project)
end
end
def can_merge_to_branch?(ref)
- return false unless user
+ return false if no_user_or_blocked?
if project.protected_branch?(ref)
access_levels = project.protected_branches.matching(ref).map(&:merge_access_levels).flatten
@@ -51,9 +55,15 @@ module Gitlab
end
def can_read_project?
- return false unless user
+ return false if no_user_or_blocked?
user.can?(:read_project, project)
end
+
+ private
+
+ def no_user_or_blocked?
+ user.nil? || user.blocked?
+ end
end
end
diff --git a/lib/gitlab/view/presenter/base.rb b/lib/gitlab/view/presenter/base.rb
new file mode 100644
index 00000000000..dbfe0941e4d
--- /dev/null
+++ b/lib/gitlab/view/presenter/base.rb
@@ -0,0 +1,30 @@
+module Gitlab
+ module View
+ module Presenter
+ CannotOverrideMethodError = Class.new(StandardError)
+
+ module Base
+ extend ActiveSupport::Concern
+
+ include Gitlab::Routing
+ include Gitlab::Allowable
+
+ attr_reader :subject
+
+ def can?(user, action, overriden_subject = nil)
+ super(user, action, overriden_subject || subject)
+ end
+
+ class_methods do
+ def presenter?
+ true
+ end
+
+ def presents(name)
+ define_method(name) { subject }
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/view/presenter/delegated.rb b/lib/gitlab/view/presenter/delegated.rb
new file mode 100644
index 00000000000..387ff0f5d43
--- /dev/null
+++ b/lib/gitlab/view/presenter/delegated.rb
@@ -0,0 +1,23 @@
+module Gitlab
+ module View
+ module Presenter
+ class Delegated < SimpleDelegator
+ include Gitlab::View::Presenter::Base
+
+ def initialize(subject, **attributes)
+ @subject = subject
+
+ attributes.each do |key, value|
+ if subject.respond_to?(key)
+ raise CannotOverrideMethodError.new("#{subject} already respond to #{key}!")
+ end
+
+ define_singleton_method(key) { value }
+ end
+
+ super(subject)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/view/presenter/factory.rb b/lib/gitlab/view/presenter/factory.rb
new file mode 100644
index 00000000000..d172d61e2c9
--- /dev/null
+++ b/lib/gitlab/view/presenter/factory.rb
@@ -0,0 +1,24 @@
+module Gitlab
+ module View
+ module Presenter
+ class Factory
+ def initialize(subject, **attributes)
+ @subject = subject
+ @attributes = attributes
+ end
+
+ def fabricate!
+ presenter_class.new(subject, attributes)
+ end
+
+ private
+
+ attr_reader :subject, :attributes
+
+ def presenter_class
+ "#{subject.class.name}Presenter".constantize
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/view/presenter/simple.rb b/lib/gitlab/view/presenter/simple.rb
new file mode 100644
index 00000000000..b7653a0f3cc
--- /dev/null
+++ b/lib/gitlab/view/presenter/simple.rb
@@ -0,0 +1,17 @@
+module Gitlab
+ module View
+ module Presenter
+ class Simple
+ include Gitlab::View::Presenter::Base
+
+ def initialize(subject, **attributes)
+ @subject = subject
+
+ attributes.each do |key, value|
+ define_singleton_method(key) { value }
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/visibility_level.rb b/lib/gitlab/visibility_level.rb
index 9462f3368e6..a4e966e4016 100644
--- a/lib/gitlab/visibility_level.rb
+++ b/lib/gitlab/visibility_level.rb
@@ -11,8 +11,21 @@ module Gitlab
included do
scope :public_only, -> { where(visibility_level: PUBLIC) }
scope :public_and_internal_only, -> { where(visibility_level: [PUBLIC, INTERNAL] ) }
-
- scope :public_to_user, -> (user) { user && !user.external ? public_and_internal_only : public_only }
+ scope :non_public_only, -> { where.not(visibility_level: PUBLIC) }
+
+ scope :public_to_user, -> (user) do
+ if user
+ if user.admin?
+ all
+ elsif !user.external?
+ public_and_internal_only
+ else
+ public_only
+ end
+ else
+ public_only
+ end
+ end
end
PRIVATE = 0 unless const_defined?(:PRIVATE)
diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb
index d28bb583fe7..c8872df8a93 100644
--- a/lib/gitlab/workhorse.rb
+++ b/lib/gitlab/workhorse.rb
@@ -15,10 +15,17 @@ module Gitlab
class << self
def git_http_ok(repository, user)
- {
+ params = {
GL_ID: Gitlab::GlId.gl_id(user),
RepoPath: repository.path_to_repo,
}
+
+ params.merge!(
+ GitalySocketPath: Gitlab.config.gitaly.socket_path,
+ GitalyResourcePath: "/projects/#{repository.project.id}/git-http/info-refs",
+ ) if Gitlab.config.gitaly.socket_path.present?
+
+ params
end
def lfs_upload_ok(oid, size)
@@ -100,7 +107,8 @@ module Gitlab
'Terminal' => {
'Subprotocols' => terminal[:subprotocols],
'Url' => terminal[:url],
- 'Header' => terminal[:headers]
+ 'Header' => terminal[:headers],
+ 'MaxSessionTime' => terminal[:max_session_time],
}
}
details['Terminal']['CAPem'] = terminal[:ca_pem] if terminal.has_key?(:ca_pem)
diff --git a/lib/mattermost/client.rb b/lib/mattermost/client.rb
new file mode 100644
index 00000000000..e55c0d6ac49
--- /dev/null
+++ b/lib/mattermost/client.rb
@@ -0,0 +1,51 @@
+module Mattermost
+ class ClientError < Mattermost::Error; end
+
+ class Client
+ attr_reader :user
+
+ def initialize(user)
+ @user = user
+ end
+
+ def with_session(&blk)
+ Mattermost::Session.new(user).with_session(&blk)
+ end
+
+ private
+
+ # Should be used in a session manually
+ def get(session, path, options = {})
+ json_response session.get(path, options)
+ end
+
+ # Should be used in a session manually
+ def post(session, path, options = {})
+ json_response session.post(path, options)
+ end
+
+ def session_get(path, options = {})
+ with_session do |session|
+ get(session, path, options)
+ end
+ end
+
+ def session_post(path, options = {})
+ with_session do |session|
+ post(session, path, options)
+ end
+ end
+
+ def json_response(response)
+ json_response = JSON.parse(response.body)
+
+ unless response.success?
+ raise Mattermost::ClientError.new(json_response['message'] || 'Undefined error')
+ end
+
+ json_response
+ rescue JSON::JSONError
+ raise Mattermost::ClientError.new('Cannot parse response')
+ end
+ end
+end
diff --git a/lib/mattermost/command.rb b/lib/mattermost/command.rb
new file mode 100644
index 00000000000..33e450d7f0a
--- /dev/null
+++ b/lib/mattermost/command.rb
@@ -0,0 +1,10 @@
+module Mattermost
+ class Command < Client
+ def create(params)
+ response = session_post("/api/v3/teams/#{params[:team_id]}/commands/create",
+ body: params.to_json)
+
+ response['token']
+ end
+ end
+end
diff --git a/lib/mattermost/error.rb b/lib/mattermost/error.rb
new file mode 100644
index 00000000000..014df175be0
--- /dev/null
+++ b/lib/mattermost/error.rb
@@ -0,0 +1,3 @@
+module Mattermost
+ class Error < StandardError; end
+end
diff --git a/lib/mattermost/session.rb b/lib/mattermost/session.rb
index fb8d7d97f8a..377cb7b1021 100644
--- a/lib/mattermost/session.rb
+++ b/lib/mattermost/session.rb
@@ -1,5 +1,12 @@
module Mattermost
- class NoSessionError < StandardError; end
+ class NoSessionError < Mattermost::Error
+ def message
+ 'No session could be set up, is Mattermost configured with Single Sign On?'
+ end
+ end
+
+ class ConnectionError < Mattermost::Error; end
+
# This class' prime objective is to obtain a session token on a Mattermost
# instance with SSO configured where this GitLab instance is the provider.
#
@@ -17,6 +24,8 @@ module Mattermost
include Doorkeeper::Helpers::Controller
include HTTParty
+ LEASE_TIMEOUT = 60
+
base_uri Settings.mattermost.host
attr_accessor :current_resource_owner, :token
@@ -26,12 +35,16 @@ module Mattermost
end
def with_session
- raise NoSessionError unless create
-
- begin
- yield self
- ensure
- destroy
+ with_lease do
+ raise Mattermost::NoSessionError unless create
+
+ begin
+ yield self
+ rescue Errno::ECONNREFUSED
+ raise Mattermost::NoSessionError
+ ensure
+ destroy
+ end
end
end
@@ -58,11 +71,15 @@ module Mattermost
end
def get(path, options = {})
- self.class.get(path, options.merge(headers: @headers))
+ handle_exceptions do
+ self.class.get(path, options.merge(headers: @headers))
+ end
end
def post(path, options = {})
- self.class.post(path, options.merge(headers: @headers))
+ handle_exceptions do
+ self.class.post(path, options.merge(headers: @headers))
+ end
end
private
@@ -111,5 +128,33 @@ module Mattermost
response.headers['token']
end
end
+
+ def with_lease
+ lease_uuid = lease_try_obtain
+ raise NoSessionError unless lease_uuid
+
+ begin
+ yield
+ ensure
+ Gitlab::ExclusiveLease.cancel(lease_key, lease_uuid)
+ end
+ end
+
+ def lease_key
+ "mattermost:session"
+ end
+
+ def lease_try_obtain
+ lease = ::Gitlab::ExclusiveLease.new(lease_key, timeout: LEASE_TIMEOUT)
+ lease.try_obtain
+ end
+
+ def handle_exceptions
+ yield
+ rescue HTTParty::Error => e
+ raise Mattermost::ConnectionError.new(e.message)
+ rescue Errno::ECONNREFUSED
+ raise Mattermost::ConnectionError.new(e.message)
+ end
end
end
diff --git a/lib/mattermost/team.rb b/lib/mattermost/team.rb
new file mode 100644
index 00000000000..09dfd082b3a
--- /dev/null
+++ b/lib/mattermost/team.rb
@@ -0,0 +1,7 @@
+module Mattermost
+ class Team < Client
+ def all
+ session_get('/api/v3/teams/all')
+ end
+ end
+end
diff --git a/lib/rouge/lexers/plantuml.rb b/lib/rouge/lexers/plantuml.rb
new file mode 100644
index 00000000000..7d5700b7f6d
--- /dev/null
+++ b/lib/rouge/lexers/plantuml.rb
@@ -0,0 +1,21 @@
+module Rouge
+ module Lexers
+ class Plantuml < Lexer
+ title "A passthrough lexer used for PlantUML input"
+ desc "A boring lexer that doesn't highlight anything"
+
+ tag 'plantuml'
+ mimetypes 'text/plain'
+
+ default_options token: 'Text'
+
+ def token
+ @token ||= Token[option :token]
+ end
+
+ def stream_tokens(string, &b)
+ yield self.token, string
+ end
+ end
+ end
+end
diff --git a/lib/support/deploy/deploy.sh b/lib/support/deploy/deploy.sh
index adea4c7a747..ab46c47d8f5 100755
--- a/lib/support/deploy/deploy.sh
+++ b/lib/support/deploy/deploy.sh
@@ -31,8 +31,8 @@ echo 'Deploy: Bundle and migrate'
sudo -u git -H bundle --without aws development test mysql --deployment
sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production
-sudo -u git -H bundle exec rake assets:clean RAILS_ENV=production
-sudo -u git -H bundle exec rake assets:precompile RAILS_ENV=production
+sudo -u git -H bundle exec rake gitlab:assets:clean RAILS_ENV=production
+sudo -u git -H bundle exec rake gitlab:assets:compile RAILS_ENV=production
sudo -u git -H bundle exec rake cache:clear RAILS_ENV=production
# return stashed changes (if necessary)
diff --git a/lib/support/init.d/gitlab b/lib/support/init.d/gitlab
index 31b00ff128a..5fd7f0f98bd 100755
--- a/lib/support/init.d/gitlab
+++ b/lib/support/init.d/gitlab
@@ -42,6 +42,11 @@ gitlab_workhorse_dir=$(cd $app_root/../gitlab-workhorse 2> /dev/null && pwd)
gitlab_workhorse_pid_path="$pid_path/gitlab-workhorse.pid"
gitlab_workhorse_options="-listenUmask 0 -listenNetwork unix -listenAddr $socket_path/gitlab-workhorse.socket -authBackend http://127.0.0.1:8080 -authSocket $rails_socket -documentRoot $app_root/public"
gitlab_workhorse_log="$app_root/log/gitlab-workhorse.log"
+gitlab_pages_enabled=false
+gitlab_pages_dir=$(cd $app_root/../gitlab-pages 2> /dev/null && pwd)
+gitlab_pages_pid_path="$pid_path/gitlab-pages.pid"
+gitlab_pages_options="-pages-domain example.com -pages-root $app_root/shared/pages -listen-proxy 127.0.0.1:8090"
+gitlab_pages_log="$app_root/log/gitlab-pages.log"
shell_path="/bin/bash"
# Read configuration variable file if it is present
@@ -89,13 +94,20 @@ check_pids(){
mpid=0
fi
fi
+ if [ "$gitlab_pages_enabled" = true ]; then
+ if [ -f "$gitlab_pages_pid_path" ]; then
+ gppid=$(cat "$gitlab_pages_pid_path")
+ else
+ gppid=0
+ fi
+ fi
}
## Called when we have started the two processes and are waiting for their pid files.
wait_for_pids(){
# We are sleeping a bit here mostly because sidekiq is slow at writing its pid
i=0;
- while [ ! -f $web_server_pid_path ] || [ ! -f $sidekiq_pid_path ] || [ ! -f $gitlab_workhorse_pid_path ] || { [ "$mail_room_enabled" = true ] && [ ! -f $mail_room_pid_path ]; }; do
+ while [ ! -f $web_server_pid_path ] || [ ! -f $sidekiq_pid_path ] || [ ! -f $gitlab_workhorse_pid_path ] || { [ "$mail_room_enabled" = true ] && [ ! -f $mail_room_pid_path ]; } || { [ "$gitlab_pages_enabled" = true ] && [ ! -f $gitlab_pages_pid_path ]; }; do
sleep 0.1;
i=$((i+1))
if [ $((i%10)) = 0 ]; then
@@ -144,7 +156,15 @@ check_status(){
mail_room_status="-1"
fi
fi
- if [ $web_status = 0 ] && [ $sidekiq_status = 0 ] && [ $gitlab_workhorse_status = 0 ] && { [ "$mail_room_enabled" != true ] || [ $mail_room_status = 0 ]; }; then
+ if [ "$gitlab_pages_enabled" = true ]; then
+ if [ $gppid -ne 0 ]; then
+ kill -0 "$gppid" 2>/dev/null
+ gitlab_pages_status="$?"
+ else
+ gitlab_pages_status="-1"
+ fi
+ fi
+ if [ $web_status = 0 ] && [ $sidekiq_status = 0 ] && [ $gitlab_workhorse_status = 0 ] && { [ "$mail_room_enabled" != true ] || [ $mail_room_status = 0 ]; } && { [ "$gitlab_pages_enabled" != true ] || [ $gitlab_pages_status = 0 ]; }; then
gitlab_status=0
else
# http://refspecs.linuxbase.org/LSB_4.1.0/LSB-Core-generic/LSB-Core-generic/iniscrptact.html
@@ -186,12 +206,19 @@ check_stale_pids(){
exit 1
fi
fi
+ if [ "$gitlab_pages_enabled" = true ] && [ "$gppid" != "0" ] && [ "$gitlab_pages_status" != "0" ]; then
+ echo "Removing stale GitLab Pages job dispatcher pid. This is most likely caused by GitLab Pages crashing the last time it ran."
+ if ! rm "$gitlab_pages_pid_path"; then
+ echo "Unable to remove stale pid, exiting"
+ exit 1
+ fi
+ fi
}
## If no parts of the service is running, bail out.
exit_if_not_running(){
check_stale_pids
- if [ "$web_status" != "0" ] && [ "$sidekiq_status" != "0" ] && [ "$gitlab_workhorse_status" != "0" ] && { [ "$mail_room_enabled" != true ] || [ "$mail_room_status" != "0" ]; }; then
+ if [ "$web_status" != "0" ] && [ "$sidekiq_status" != "0" ] && [ "$gitlab_workhorse_status" != "0" ] && { [ "$mail_room_enabled" != true ] || [ "$mail_room_status" != "0" ]; } && { [ "$gitlab_pages_enabled" != true ] || [ "$gitlab_pages_status" != "0" ]; }; then
echo "GitLab is not running."
exit
fi
@@ -213,6 +240,9 @@ start_gitlab() {
if [ "$mail_room_enabled" = true ] && [ "$mail_room_status" != "0" ]; then
echo "Starting GitLab MailRoom"
fi
+ if [ "$gitlab_pages_enabled" = true ] && [ "$gitlab_pages_status" != "0" ]; then
+ echo "Starting GitLab Pages"
+ fi
# Then check if the service is running. If it is: don't start again.
if [ "$web_status" = "0" ]; then
@@ -252,6 +282,16 @@ start_gitlab() {
fi
fi
+ if [ "$gitlab_pages_enabled" = true ]; then
+ if [ "$gitlab_pages_status" = "0" ]; then
+ echo "The GitLab Pages is already running with pid $spid, not restarting"
+ else
+ $app_root/bin/daemon_with_pidfile $gitlab_pages_pid_path \
+ $gitlab_pages_dir/gitlab-pages $gitlab_pages_options \
+ >> $gitlab_pages_log 2>&1 &
+ fi
+ fi
+
# Wait for the pids to be planted
wait_for_pids
# Finally check the status to tell wether or not GitLab is running
@@ -278,13 +318,17 @@ stop_gitlab() {
echo "Shutting down GitLab MailRoom"
RAILS_ENV=$RAILS_ENV bin/mail_room stop
fi
+ if [ "$gitlab_pages_status" = "0" ]; then
+ echo "Shutting down gitlab-pages"
+ kill -- $(cat $gitlab_pages_pid_path)
+ fi
# If something needs to be stopped, lets wait for it to stop. Never use SIGKILL in a script.
- while [ "$web_status" = "0" ] || [ "$sidekiq_status" = "0" ] || [ "$gitlab_workhorse_status" = "0" ] || { [ "$mail_room_enabled" = true ] && [ "$mail_room_status" = "0" ]; }; do
+ while [ "$web_status" = "0" ] || [ "$sidekiq_status" = "0" ] || [ "$gitlab_workhorse_status" = "0" ] || { [ "$mail_room_enabled" = true ] && [ "$mail_room_status" = "0" ]; } || { [ "$gitlab_pages_enabled" = true ] && [ "$gitlab_pages_status" = "0" ]; }; do
sleep 1
check_status
printf "."
- if [ "$web_status" != "0" ] && [ "$sidekiq_status" != "0" ] && [ "$gitlab_workhorse_status" != "0" ] && { [ "$mail_room_enabled" != true ] || [ "$mail_room_status" != "0" ]; }; then
+ if [ "$web_status" != "0" ] && [ "$sidekiq_status" != "0" ] && [ "$gitlab_workhorse_status" != "0" ] && { [ "$mail_room_enabled" != true ] || [ "$mail_room_status" != "0" ]; } && { [ "$gitlab_pages_enabled" != true ] || [ "$gitlab_pages_status" != "0" ]; }; then
printf "\n"
break
fi
@@ -298,6 +342,7 @@ stop_gitlab() {
if [ "$mail_room_enabled" = true ]; then
rm "$mail_room_pid_path" 2>/dev/null
fi
+ rm -f "$gitlab_pages_pid_path"
print_status
}
@@ -305,7 +350,7 @@ stop_gitlab() {
## Prints the status of GitLab and its components.
print_status() {
check_status
- if [ "$web_status" != "0" ] && [ "$sidekiq_status" != "0" ] && [ "$gitlab_workhorse_status" != "0" ] && { [ "$mail_room_enabled" != true ] || [ "$mail_room_status" != "0" ]; }; then
+ if [ "$web_status" != "0" ] && [ "$sidekiq_status" != "0" ] && [ "$gitlab_workhorse_status" != "0" ] && { [ "$mail_room_enabled" != true ] || [ "$mail_room_status" != "0" ]; } && { [ "$gitlab_pages_enabled" != true ] || [ "$gitlab_pages_status" != "0" ]; }; then
echo "GitLab is not running."
return
fi
@@ -331,7 +376,14 @@ print_status() {
printf "The GitLab MailRoom email processor is \033[31mnot running\033[0m.\n"
fi
fi
- if [ "$web_status" = "0" ] && [ "$sidekiq_status" = "0" ] && [ "$gitlab_workhorse_status" = "0" ] && { [ "$mail_room_enabled" != true ] || [ "$mail_room_status" = "0" ]; }; then
+ if [ "$gitlab_pages_enabled" = true ]; then
+ if [ "$gitlab_pages_status" = "0" ]; then
+ echo "The GitLab Pages with pid $mpid is running."
+ else
+ printf "The GitLab Pages is \033[31mnot running\033[0m.\n"
+ fi
+ fi
+ if [ "$web_status" = "0" ] && [ "$sidekiq_status" = "0" ] && [ "$gitlab_workhorse_status" = "0" ] && { [ "$mail_room_enabled" != true ] || [ "$mail_room_status" = "0" ]; } && { [ "$gitlab_pages_enabled" != true ] || [ "$gitlab_pages_status" = "0" ]; }; then
printf "GitLab and all its components are \033[32mup and running\033[0m.\n"
fi
}
@@ -362,7 +414,7 @@ reload_gitlab(){
## Restarts Sidekiq and Unicorn.
restart_gitlab(){
check_status
- if [ "$web_status" = "0" ] || [ "$sidekiq_status" = "0" ] || [ "$gitlab_workhorse" = "0" ] || { [ "$mail_room_enabled" = true ] && [ "$mail_room_status" = "0" ]; }; then
+ if [ "$web_status" = "0" ] || [ "$sidekiq_status" = "0" ] || [ "$gitlab_workhorse" = "0" ] || { [ "$mail_room_enabled" = true ] && [ "$mail_room_status" = "0" ]; } || { [ "$gitlab_pages_enabled" = true ] && [ "$gitlab_pages_status" = "0" ]; }; then
stop_gitlab
fi
start_gitlab
diff --git a/lib/support/init.d/gitlab.default.example b/lib/support/init.d/gitlab.default.example
index cc8617b72ca..e5797d8fe3c 100755..100644
--- a/lib/support/init.d/gitlab.default.example
+++ b/lib/support/init.d/gitlab.default.example
@@ -47,6 +47,30 @@ gitlab_workhorse_pid_path="$pid_path/gitlab-workhorse.pid"
gitlab_workhorse_options="-listenUmask 0 -listenNetwork unix -listenAddr $socket_path/gitlab-workhorse.socket -authBackend http://127.0.0.1:8080 -authSocket $socket_path/gitlab.socket -documentRoot $app_root/public"
gitlab_workhorse_log="$app_root/log/gitlab-workhorse.log"
+# The GitLab Pages Daemon needs either a separate IP address on which it will
+# listen or use different ports than 80 or 443 that will be forwarded to GitLab
+# Pages Daemon.
+#
+# To enable HTTP support for custom domains add the `-listen-http` directive
+# in `gitlab_pages_options` below.
+# The value of -listen-http must be set to `gitlab.yml > pages > external_http`
+# as well. For example:
+#
+# -listen-http 1.1.1.1:80
+#
+# To enable HTTPS support for custom domains add the `-listen-https`,
+# `-root-cert` and `-root-key` directives in `gitlab_pages_options` below.
+# The value of -listen-https must be set to `gitlab.yml > pages > external_https`
+# as well. For example:
+#
+# -listen-https 1.1.1.1:443 -root-cert /path/to/example.com.crt -root-key /path/to/example.com.key
+#
+# The -pages-domain must be specified the same as in `gitlab.yml > pages > host`.
+# Set `gitlab_pages_enabled=true` if you want to enable the Pages feature.
+gitlab_pages_enabled=false
+gitlab_pages_options="-pages-domain example.com -pages-root $app_root/shared/pages -listen-proxy 127.0.0.1:8090"
+gitlab_pages_log="$app_root/log/gitlab-pages.log"
+
# mail_room_enabled specifies whether mail_room, which is used to process incoming email, is enabled.
# This is required for the Reply by email feature.
# The default is "false"
diff --git a/lib/support/nginx/gitlab-pages b/lib/support/nginx/gitlab-pages
new file mode 100644
index 00000000000..d9746c5c1aa
--- /dev/null
+++ b/lib/support/nginx/gitlab-pages
@@ -0,0 +1,28 @@
+## GitLab
+##
+
+## Pages serving host
+server {
+ listen 0.0.0.0:80;
+ listen [::]:80 ipv6only=on;
+
+ ## Replace this with something like pages.gitlab.com
+ server_name ~^.*\.YOUR_GITLAB_PAGES\.DOMAIN$;
+
+ ## Individual nginx logs for GitLab pages
+ access_log /var/log/nginx/gitlab_pages_access.log;
+ error_log /var/log/nginx/gitlab_pages_error.log;
+
+ location / {
+ proxy_set_header Host $http_host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ # The same address as passed to GitLab Pages: `-listen-proxy`
+ proxy_pass http://localhost:8090/;
+ }
+
+ # Define custom error pages
+ error_page 403 /403.html;
+ error_page 404 /404.html;
+}
diff --git a/lib/support/nginx/gitlab-pages-ssl b/lib/support/nginx/gitlab-pages-ssl
new file mode 100644
index 00000000000..a1ccf266835
--- /dev/null
+++ b/lib/support/nginx/gitlab-pages-ssl
@@ -0,0 +1,77 @@
+## GitLab
+##
+
+## Redirects all HTTP traffic to the HTTPS host
+server {
+ ## Either remove "default_server" from the listen line below,
+ ## or delete the /etc/nginx/sites-enabled/default file. This will cause gitlab
+ ## to be served if you visit any address that your server responds to, eg.
+ ## the ip address of the server (http://x.x.x.x/)
+ listen 0.0.0.0:80;
+ listen [::]:80 ipv6only=on;
+
+ ## Replace this with something like pages.gitlab.com
+ server_name ~^.*\.YOUR_GITLAB_PAGES\.DOMAIN$;
+ server_tokens off; ## Don't show the nginx version number, a security best practice
+
+ return 301 https://$http_host$request_uri;
+
+ access_log /var/log/nginx/gitlab_pages_access.log;
+ error_log /var/log/nginx/gitlab_pages_access.log;
+}
+
+## Pages serving host
+server {
+ listen 0.0.0.0:443 ssl;
+ listen [::]:443 ipv6only=on ssl http2;
+
+ ## Replace this with something like pages.gitlab.com
+ server_name ~^.*\.YOUR_GITLAB_PAGES\.DOMAIN$;
+ server_tokens off; ## Don't show the nginx version number, a security best practice
+
+ ## Strong SSL Security
+ ## https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html & https://cipherli.st/
+ ssl on;
+ ssl_certificate /etc/nginx/ssl/gitlab-pages.crt;
+ ssl_certificate_key /etc/nginx/ssl/gitlab-pages.key;
+
+ # GitLab needs backwards compatible ciphers to retain compatibility with Java IDEs
+ ssl_ciphers "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4";
+ ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
+ ssl_prefer_server_ciphers on;
+ ssl_session_cache shared:SSL:10m;
+ ssl_session_timeout 5m;
+
+ ## See app/controllers/application_controller.rb for headers set
+
+ ## [Optional] If your certficate has OCSP, enable OCSP stapling to reduce the overhead and latency of running SSL.
+ ## Replace with your ssl_trusted_certificate. For more info see:
+ ## - https://medium.com/devops-programming/4445f4862461
+ ## - https://www.ruby-forum.com/topic/4419319
+ ## - https://www.digitalocean.com/community/tutorials/how-to-configure-ocsp-stapling-on-apache-and-nginx
+ # ssl_stapling on;
+ # ssl_stapling_verify on;
+ # ssl_trusted_certificate /etc/nginx/ssl/stapling.trusted.crt;
+
+ ## [Optional] Generate a stronger DHE parameter:
+ ## sudo openssl dhparam -out /etc/ssl/certs/dhparam.pem 4096
+ ##
+ # ssl_dhparam /etc/ssl/certs/dhparam.pem;
+
+ ## Individual nginx logs for GitLab pages
+ access_log /var/log/nginx/gitlab_pages_access.log;
+ error_log /var/log/nginx/gitlab_pages_error.log;
+
+ location / {
+ proxy_set_header Host $http_host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ # The same address as passed to GitLab Pages: `-listen-proxy`
+ proxy_pass http://localhost:8090/;
+ }
+
+ # Define custom error pages
+ error_page 403 /403.html;
+ error_page 404 /404.html;
+}
diff --git a/lib/tasks/config_lint.rake b/lib/tasks/config_lint.rake
new file mode 100644
index 00000000000..ddbcf1e1eb8
--- /dev/null
+++ b/lib/tasks/config_lint.rake
@@ -0,0 +1,25 @@
+module ConfigLint
+ def self.run(files)
+ failures = files.reject do |file|
+ yield(file)
+ end
+
+ if failures.present?
+ puts failures
+ exit failures.count
+ end
+ end
+end
+
+desc "Checks syntax for shell scripts and nginx config files in 'lib/support/'"
+task :config_lint do
+ shell_scripts = [
+ 'lib/support/init.d/gitlab',
+ 'lib/support/init.d/gitlab.default.example',
+ 'lib/support/deploy/deploy.sh'
+ ]
+
+ ConfigLint.run(shell_scripts) do |file|
+ Kernel.system('bash', '-n', file)
+ end
+end
diff --git a/lib/tasks/dev.rake b/lib/tasks/dev.rake
index 6f27972c4e4..5e94fba97bf 100644
--- a/lib/tasks/dev.rake
+++ b/lib/tasks/dev.rake
@@ -7,9 +7,4 @@ namespace :dev do
Rake::Task["gitlab:setup"].invoke
Rake::Task["gitlab:shell:setup"].invoke
end
-
- desc 'GitLab | Start/restart foreman and watch for changes'
- task :foreman => :environment do
- sh 'rerun --dir app,config,lib -- foreman start'
- end
end
diff --git a/lib/tasks/gitlab/assets.rake b/lib/tasks/gitlab/assets.rake
new file mode 100644
index 00000000000..b6ef8260191
--- /dev/null
+++ b/lib/tasks/gitlab/assets.rake
@@ -0,0 +1,48 @@
+namespace :gitlab do
+ namespace :assets do
+ desc 'GitLab | Assets | Compile all frontend assets'
+ task :compile do
+ Rake::Task['assets:precompile'].invoke
+ Rake::Task['webpack:compile'].invoke
+ Rake::Task['gitlab:assets:fix_urls'].invoke
+ end
+
+ desc 'GitLab | Assets | Clean up old compiled frontend assets'
+ task :clean do
+ Rake::Task['assets:clean'].invoke
+ end
+
+ desc 'GitLab | Assets | Remove all compiled frontend assets'
+ task :purge do
+ Rake::Task['assets:clobber'].invoke
+ end
+
+ desc 'GitLab | Assets | Fix all absolute url references in CSS'
+ task :fix_urls do
+ css_files = Dir['public/assets/*.css']
+ css_files.each do | file |
+ # replace url(/assets/*) with url(./*)
+ puts "Fixing #{file}"
+ system "sed", "-i", "-e", 's/url(\([\"\']\?\)\/assets\//url(\1.\//g', file
+
+ # rewrite the corresponding gzip file (if it exists)
+ gzip = "#{file}.gz"
+ if File.exist?(gzip)
+ puts "Fixing #{gzip}"
+
+ FileUtils.rm(gzip)
+ mtime = File.stat(file).mtime
+
+ File.open(gzip, 'wb+') do |f|
+ gz = Zlib::GzipWriter.new(f, Zlib::BEST_COMPRESSION)
+ gz.mtime = mtime
+ gz.write IO.binread(file)
+ gz.close
+
+ File.utime(mtime, mtime, f.path)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/tasks/gitlab/backup.rake b/lib/tasks/gitlab/backup.rake
index a9f1255e8cf..1650263b98d 100644
--- a/lib/tasks/gitlab/backup.rake
+++ b/lib/tasks/gitlab/backup.rake
@@ -13,6 +13,7 @@ namespace :gitlab do
Rake::Task["gitlab:backup:uploads:create"].invoke
Rake::Task["gitlab:backup:builds:create"].invoke
Rake::Task["gitlab:backup:artifacts:create"].invoke
+ Rake::Task["gitlab:backup:pages:create"].invoke
Rake::Task["gitlab:backup:lfs:create"].invoke
Rake::Task["gitlab:backup:registry:create"].invoke
@@ -56,6 +57,7 @@ namespace :gitlab do
Rake::Task['gitlab:backup:uploads:restore'].invoke unless backup.skipped?('uploads')
Rake::Task['gitlab:backup:builds:restore'].invoke unless backup.skipped?('builds')
Rake::Task['gitlab:backup:artifacts:restore'].invoke unless backup.skipped?('artifacts')
+ Rake::Task["gitlab:backup:pages:restore"].invoke unless backup.skipped?('pages')
Rake::Task['gitlab:backup:lfs:restore'].invoke unless backup.skipped?('lfs')
Rake::Task['gitlab:backup:registry:restore'].invoke unless backup.skipped?('registry')
Rake::Task['gitlab:shell:setup'].invoke
@@ -159,6 +161,25 @@ namespace :gitlab do
end
end
+ namespace :pages do
+ task create: :environment do
+ $progress.puts "Dumping pages ... ".color(:blue)
+
+ if ENV["SKIP"] && ENV["SKIP"].include?("pages")
+ $progress.puts "[SKIPPED]".color(:cyan)
+ else
+ Backup::Pages.new.dump
+ $progress.puts "done".color(:green)
+ end
+ end
+
+ task restore: :environment do
+ $progress.puts "Restoring pages ... ".color(:blue)
+ Backup::Pages.new.restore
+ $progress.puts "done".color(:green)
+ end
+ end
+
namespace :lfs do
task create: :environment do
$progress.puts "Dumping lfs objects ... ".color(:blue)
diff --git a/lib/tasks/gitlab/cleanup.rake b/lib/tasks/gitlab/cleanup.rake
index 4a696a52b4d..967f630ef20 100644
--- a/lib/tasks/gitlab/cleanup.rake
+++ b/lib/tasks/gitlab/cleanup.rake
@@ -58,7 +58,7 @@ namespace :gitlab do
sub(%r{^/*}, '').
chomp('.git').
chomp('.wiki')
- next if Project.find_with_namespace(repo_with_namespace)
+ next if Project.find_by_full_path(repo_with_namespace)
new_path = path + move_suffix
puts path.inspect + ' -> ' + new_path.inspect
File.rename(path, new_path)
diff --git a/lib/tasks/gitlab/git.rake b/lib/tasks/gitlab/git.rake
index f9834a4dae8..a67c1fe1f27 100644
--- a/lib/tasks/gitlab/git.rake
+++ b/lib/tasks/gitlab/git.rake
@@ -3,7 +3,7 @@ namespace :gitlab do
desc "GitLab | Git | Repack"
task repack: :environment do
- failures = perform_git_cmd(%W(git repack -a --quiet), "Repacking repo")
+ failures = perform_git_cmd(%W(#{Gitlab.config.git.bin_path} repack -a --quiet), "Repacking repo")
if failures.empty?
puts "Done".color(:green)
else
@@ -13,17 +13,17 @@ namespace :gitlab do
desc "GitLab | Git | Run garbage collection on all repos"
task gc: :environment do
- failures = perform_git_cmd(%W(git gc --auto --quiet), "Garbage Collecting")
+ failures = perform_git_cmd(%W(#{Gitlab.config.git.bin_path} gc --auto --quiet), "Garbage Collecting")
if failures.empty?
puts "Done".color(:green)
else
output_failures(failures)
end
end
-
+
desc "GitLab | Git | Prune all repos"
task prune: :environment do
- failures = perform_git_cmd(%W(git prune), "Git Prune")
+ failures = perform_git_cmd(%W(#{Gitlab.config.git.bin_path} prune), "Git Prune")
if failures.empty?
puts "Done".color(:green)
else
diff --git a/lib/tasks/gitlab/import.rake b/lib/tasks/gitlab/import.rake
index dbdd4e977e8..b4015f5238e 100644
--- a/lib/tasks/gitlab/import.rake
+++ b/lib/tasks/gitlab/import.rake
@@ -29,7 +29,7 @@ namespace :gitlab do
next
end
- project = Project.find_with_namespace(path)
+ project = Project.find_by_full_path(path)
if project
puts " * #{project.name} (#{repo_path}) exists"
@@ -63,8 +63,7 @@ namespace :gitlab do
if project.persisted?
puts " * Created #{project.name} (#{repo_path})".color(:green)
- project.update_repository_size
- project.update_commit_count
+ ProjectCacheWorker.perform_async(project.id)
else
puts " * Failed trying to create #{project.name} (#{repo_path})".color(:red)
puts " Errors: #{project.errors.messages}".color(:red)
diff --git a/lib/tasks/gitlab/info.rake b/lib/tasks/gitlab/info.rake
index dffea8ed155..f7c831892ee 100644
--- a/lib/tasks/gitlab/info.rake
+++ b/lib/tasks/gitlab/info.rake
@@ -11,8 +11,10 @@ namespace :gitlab do
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
+ # check Rake version
rake_version = run_and_match(%W(rake --version), /[\d\.]+/).try(:to_s)
+ # check redis version
+ redis_version = run_and_match(%W(redis-cli --version), /redis-cli (\d+\.\d+\.\d+)/).to_a
puts ""
puts "System information".color(:yellow)
@@ -24,6 +26,7 @@ namespace :gitlab do
puts "Gem Version:\t#{gem_version || "unknown".color(:red)}"
puts "Bundler Version:#{bunder_version || "unknown".color(:red)}"
puts "Rake Version:\t#{rake_version || "unknown".color(:red)}"
+ puts "Redis Version:\t#{redis_version[1] || "unknown".color(:red)}"
puts "Sidekiq Version:#{Sidekiq::VERSION}"
diff --git a/lib/tasks/gitlab/sidekiq.rake b/lib/tasks/gitlab/sidekiq.rake
index 7e2a6668e59..f2e12d85045 100644
--- a/lib/tasks/gitlab/sidekiq.rake
+++ b/lib/tasks/gitlab/sidekiq.rake
@@ -7,7 +7,7 @@ namespace :gitlab do
unless args.project.present?
abort "Please specify the project you want to drop PostReceive jobs for:\n rake gitlab:sidekiq:drop_post_receive[group/project]"
end
- project_path = Project.find_with_namespace(args.project).repository.path_to_repo
+ project_path = Project.find_by_full_path(args.project).repository.path_to_repo
Sidekiq.redis do |redis|
unless redis.exists(QUEUE)
diff --git a/lib/tasks/gitlab/test.rake b/lib/tasks/gitlab/test.rake
index 4d4e746503a..84810b489ce 100644
--- a/lib/tasks/gitlab/test.rake
+++ b/lib/tasks/gitlab/test.rake
@@ -6,7 +6,7 @@ namespace :gitlab do
%W(rake rubocop),
%W(rake spinach),
%W(rake spec),
- %W(rake teaspoon)
+ %W(rake karma)
]
cmds.each do |cmd|
diff --git a/lib/tasks/gitlab/update_commit_count.rake b/lib/tasks/gitlab/update_commit_count.rake
deleted file mode 100644
index 3bd10b0208b..00000000000
--- a/lib/tasks/gitlab/update_commit_count.rake
+++ /dev/null
@@ -1,20 +0,0 @@
-namespace :gitlab do
- desc "GitLab | Update commit count for projects"
- task update_commit_count: :environment do
- projects = Project.where(commit_count: 0)
- puts "#{projects.size} projects need to be updated. This might take a while."
- ask_to_continue unless ENV['force'] == 'yes'
-
- projects.find_each(batch_size: 100) do |project|
- print "#{project.name_with_namespace.color(:yellow)} ... "
-
- unless project.repo_exists?
- puts "skipping, because the repo is empty".color(:magenta)
- next
- end
-
- project.update_commit_count
- puts project.commit_count.to_s.color(:green)
- end
- end
-end
diff --git a/lib/tasks/gitlab/update_templates.rake b/lib/tasks/gitlab/update_templates.rake
index 4f76dad7286..b77a5bb62d1 100644
--- a/lib/tasks/gitlab/update_templates.rake
+++ b/lib/tasks/gitlab/update_templates.rake
@@ -44,7 +44,7 @@ namespace :gitlab do
),
Template.new(
"https://gitlab.com/gitlab-org/gitlab-ci-yml.git",
- /(\.{1,2}|LICENSE|Pages|\.gitlab-ci.yml)\z/
+ /(\.{1,2}|LICENSE|Pages|autodeploy|\.gitlab-ci.yml)\z/
)
]
diff --git a/lib/tasks/grape.rake b/lib/tasks/grape.rake
index 9980e0b7984..ea2698da606 100644
--- a/lib/tasks/grape.rake
+++ b/lib/tasks/grape.rake
@@ -2,7 +2,11 @@ namespace :grape do
desc 'Print compiled grape routes'
task routes: :environment do
API::API.routes.each do |route|
- puts route
+ puts "#{route.options[:method]} #{route.path} - #{route_description(route.options)}"
end
end
+
+ def route_description(options)
+ options[:settings][:description][:description] if options[:settings][:description]
+ end
end
diff --git a/lib/tasks/karma.rake b/lib/tasks/karma.rake
new file mode 100644
index 00000000000..89812a179ec
--- /dev/null
+++ b/lib/tasks/karma.rake
@@ -0,0 +1,25 @@
+unless Rails.env.production?
+ Rake::Task['karma'].clear if Rake::Task.task_defined?('karma')
+
+ namespace :karma do
+ desc 'GitLab | Karma | Generate fixtures for JavaScript tests'
+ RSpec::Core::RakeTask.new(:fixtures) do |t|
+ ENV['NO_KNAPSACK'] = 'true'
+ t.pattern = 'spec/javascripts/fixtures/*.rb'
+ t.rspec_opts = '--format documentation'
+ end
+
+ desc 'GitLab | Karma | Run JavaScript tests'
+ task :tests do
+ sh "npm run karma" do |ok, res|
+ abort('rake karma:tests failed') unless ok
+ end
+ end
+ end
+
+ desc 'GitLab | Karma | Shortcut for karma:fixtures and karma:tests'
+ task :karma do
+ Rake::Task['karma:fixtures'].invoke
+ Rake::Task['karma:tests'].invoke
+ end
+end
diff --git a/lib/tasks/teaspoon.rake b/lib/tasks/teaspoon.rake
deleted file mode 100644
index 08caedd7ff3..00000000000
--- a/lib/tasks/teaspoon.rake
+++ /dev/null
@@ -1,25 +0,0 @@
-unless Rails.env.production?
- Rake::Task['teaspoon'].clear if Rake::Task.task_defined?('teaspoon')
-
- namespace :teaspoon do
- desc 'GitLab | Teaspoon | Generate fixtures for JavaScript tests'
- RSpec::Core::RakeTask.new(:fixtures) do |t|
- ENV['NO_KNAPSACK'] = 'true'
- t.pattern = 'spec/javascripts/fixtures/*.rb'
- t.rspec_opts = '--format documentation'
- end
-
- desc 'GitLab | Teaspoon | Run JavaScript tests'
- task :tests do
- require "teaspoon/console"
- options = {}
- abort('rake teaspoon:tests failed') if Teaspoon::Console.new(options).failures?
- end
- end
-
- desc 'GitLab | Teaspoon | Shortcut for teaspoon:fixtures and teaspoon:tests'
- task :teaspoon do
- Rake::Task['teaspoon:fixtures'].invoke
- Rake::Task['teaspoon:tests'].invoke
- end
-end
diff --git a/lib/tasks/test.rake b/lib/tasks/test.rake
index d3dcbd2c29b..3e01f91d32c 100644
--- a/lib/tasks/test.rake
+++ b/lib/tasks/test.rake
@@ -7,5 +7,5 @@ end
unless Rails.env.production?
desc "GitLab | Run all tests on CI with simplecov"
- task test_ci: [:rubocop, :brakeman, :teaspoon, :spinach, :spec]
+ task test_ci: [:rubocop, :brakeman, :karma, :spinach, :spec]
end
diff --git a/package.json b/package.json
index 49b8210e427..9581d966237 100644
--- a/package.json
+++ b/package.json
@@ -1,9 +1,37 @@
{
"private": true,
"scripts": {
+ "dev-server": "webpack-dev-server --config config/webpack.config.js",
"eslint": "eslint --max-warnings 0 --ext .js,.js.es6 .",
"eslint-fix": "npm run eslint -- --fix",
- "eslint-report": "npm run eslint -- --format html --output-file ./eslint-report.html"
+ "eslint-report": "npm run eslint -- --format html --output-file ./eslint-report.html",
+ "karma": "karma start config/karma.config.js --single-run",
+ "karma-start": "karma start config/karma.config.js",
+ "webpack": "webpack --config config/webpack.config.js",
+ "webpack-prod": "NODE_ENV=production npm run webpack"
+ },
+ "dependencies": {
+ "babel": "^5.8.38",
+ "babel-core": "^5.8.38",
+ "babel-loader": "^5.4.2",
+ "bootstrap-sass": "3.3.6",
+ "compression-webpack-plugin": "^0.3.2",
+ "d3": "3.5.11",
+ "dropzone": "4.2.0",
+ "exports-loader": "^0.6.3",
+ "imports-loader": "^0.6.5",
+ "jquery": "2.2.1",
+ "jquery-ui": "github:jquery/jquery-ui#1.11.4",
+ "jquery-ujs": "1.2.1",
+ "json-loader": "^0.5.4",
+ "mousetrap": "1.4.6",
+ "select2": "3.5.2-browserify",
+ "stats-webpack-plugin": "^0.4.2",
+ "underscore": "1.8.3",
+ "vue": "2.0.3",
+ "vue-resource": "0.9.3",
+ "webpack": "^1.14.0",
+ "webpack-dev-server": "^1.16.2"
},
"devDependencies": {
"eslint": "^3.10.1",
@@ -11,6 +39,13 @@
"eslint-plugin-filenames": "^1.1.0",
"eslint-plugin-import": "^2.2.0",
"eslint-plugin-jasmine": "^2.1.0",
- "istanbul": "^0.4.5"
+ "istanbul": "^0.4.5",
+ "jasmine-core": "^2.5.2",
+ "jasmine-jquery": "^2.1.1",
+ "karma": "^1.3.0",
+ "karma-jasmine": "^1.1.0",
+ "karma-phantomjs-launcher": "^1.0.2",
+ "karma-sourcemap-loader": "^0.3.7",
+ "karma-webpack": "^1.8.0"
}
}
diff --git a/public/robots.txt b/public/robots.txt
index 7d69fad59d1..123272a9834 100644
--- a/public/robots.txt
+++ b/public/robots.txt
@@ -4,13 +4,12 @@
# User-Agent: *
# Disallow: /
-User-Agent: *
-
# Add a 1 second delay between successive requests to the same server, limits resources used by crawler
# Only some crawlers respect this setting, e.g. Googlebot does not
# Crawl-delay: 1
# Based on details in https://gitlab.com/gitlab-org/gitlab-ce/blob/master/config/routes.rb, https://gitlab.com/gitlab-org/gitlab-ce/blob/master/spec/routing, and using application
+User-Agent: *
Disallow: /autocomplete/users
Disallow: /search
Disallow: /api
@@ -23,12 +22,14 @@ Disallow: /groups/*/edit
Disallow: /users
# Global snippets
+User-Agent: *
Disallow: /s/
Disallow: /snippets/new
Disallow: /snippets/*/edit
Disallow: /snippets/*/raw
# Project details
+User-Agent: *
Disallow: /*/*.git
Disallow: /*/*/fork/new
Disallow: /*/*/repository/archive*
diff --git a/public/slash-command-logo.png b/public/slash-command-logo.png
new file mode 100644
index 00000000000..05c8b0d0ccf
--- /dev/null
+++ b/public/slash-command-logo.png
Binary files differ
diff --git a/rubocop/cop/gem_fetcher.rb b/rubocop/cop/gem_fetcher.rb
new file mode 100644
index 00000000000..4a63c760744
--- /dev/null
+++ b/rubocop/cop/gem_fetcher.rb
@@ -0,0 +1,28 @@
+module RuboCop
+ module Cop
+ # Cop that checks for all gems specified in the Gemfile, and will
+ # alert if any gem is to be fetched not from the RubyGems index.
+ # This enforcement is done so as to minimize external build
+ # dependencies and build times.
+ class GemFetcher < RuboCop::Cop::Cop
+ MSG = 'Do not use gems from git repositories, only use gems from RubyGems.'
+
+ GIT_KEYS = [:git, :github]
+
+ def on_send(node)
+ file_path = node.location.expression.source_buffer.name
+ return unless file_path.end_with?("Gemfile")
+
+ func_name = node.children[1]
+ return unless func_name == :gem
+
+ node.children.last.each_node(:pair) do |pair|
+ key_name = pair.children[0].children[0].to_sym
+ if GIT_KEYS.include?(key_name)
+ add_offense(node, :selector)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/rubocop/rubocop.rb b/rubocop/rubocop.rb
index 7922e19768b..7f20754ee51 100644
--- a/rubocop/rubocop.rb
+++ b/rubocop/rubocop.rb
@@ -1,3 +1,4 @@
require_relative 'migration_helpers'
require_relative 'cop/migration/add_index'
require_relative 'cop/migration/column_with_default'
+require_relative 'cop/gem_fetcher'
diff --git a/scripts/notify_slack.sh b/scripts/notify_slack.sh
index 0a4239e132c..6b3bc563c7a 100755
--- a/scripts/notify_slack.sh
+++ b/scripts/notify_slack.sh
@@ -1,4 +1,4 @@
-#!/bin/bash
+#!/bin/sh
# Sends Slack notification ERROR_MSG to CHANNEL
# An env. variable CI_SLACK_WEBHOOK_URL needs to be set.
diff --git a/shared/pages/.gitkeep b/shared/pages/.gitkeep
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/shared/pages/.gitkeep
diff --git a/spec/controllers/admin/groups_controller_spec.rb b/spec/controllers/admin/groups_controller_spec.rb
index 602de72d23f..84db26a958a 100644
--- a/spec/controllers/admin/groups_controller_spec.rb
+++ b/spec/controllers/admin/groups_controller_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe Admin::GroupsController do
let(:group) { create(:group) }
- let(:project) { create(:project, namespace: group) }
+ let(:project) { create(:empty_project, namespace: group) }
let(:admin) { create(:admin) }
before do
diff --git a/spec/controllers/admin/projects_controller_spec.rb b/spec/controllers/admin/projects_controller_spec.rb
index 8eaacef2024..2c35d394b74 100644
--- a/spec/controllers/admin/projects_controller_spec.rb
+++ b/spec/controllers/admin/projects_controller_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Admin::ProjectsController do
- let!(:project) { create(:project, visibility_level: Gitlab::VisibilityLevel::PUBLIC) }
+ let!(:project) { create(:empty_project, :public) }
before do
sign_in(create(:admin))
diff --git a/spec/controllers/admin/services_controller_spec.rb b/spec/controllers/admin/services_controller_spec.rb
new file mode 100644
index 00000000000..e5cdd52307e
--- /dev/null
+++ b/spec/controllers/admin/services_controller_spec.rb
@@ -0,0 +1,26 @@
+require 'spec_helper'
+
+describe Admin::ServicesController do
+ let(:admin) { create(:admin) }
+
+ before { sign_in(admin) }
+
+ describe 'GET #edit' do
+ let!(:project) { create(:empty_project) }
+
+ Service.available_services_names.each do |service_name|
+ context "#{service_name}" do
+ let!(:service) do
+ service_template = service_name.concat("_service").camelize.constantize
+ service_template.where(template: true).first_or_create
+ end
+
+ it 'successfully displays the template' do
+ get :edit, id: service.id
+
+ expect(response).to have_http_status(200)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/controllers/autocomplete_controller_spec.rb b/spec/controllers/autocomplete_controller_spec.rb
index ea2fd90a9b0..7d2f6dd9d0a 100644
--- a/spec/controllers/autocomplete_controller_spec.rb
+++ b/spec/controllers/autocomplete_controller_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe AutocompleteController do
- let!(:project) { create(:project) }
+ let!(:project) { create(:empty_project) }
let!(:user) { create(:user) }
context 'GET users' do
diff --git a/spec/controllers/blob_controller_spec.rb b/spec/controllers/blob_controller_spec.rb
index 465013231f9..2fcb4a6a528 100644
--- a/spec/controllers/blob_controller_spec.rb
+++ b/spec/controllers/blob_controller_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Projects::BlobController do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
before do
diff --git a/spec/controllers/ci/projects_controller_spec.rb b/spec/controllers/ci/projects_controller_spec.rb
index 5022a3e2c80..86f01f437a2 100644
--- a/spec/controllers/ci/projects_controller_spec.rb
+++ b/spec/controllers/ci/projects_controller_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe Ci::ProjectsController do
let(:visibility) { :public }
- let!(:project) { create(:project, visibility, ci_id: 1) }
+ let!(:project) { create(:empty_project, visibility, ci_id: 1) }
let(:ci_id) { project.ci_id }
describe '#index' do
diff --git a/spec/controllers/dashboard/todos_controller_spec.rb b/spec/controllers/dashboard/todos_controller_spec.rb
new file mode 100644
index 00000000000..79ef3a1adad
--- /dev/null
+++ b/spec/controllers/dashboard/todos_controller_spec.rb
@@ -0,0 +1,37 @@
+require 'spec_helper'
+
+describe Dashboard::TodosController do
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project) }
+ let(:todo_service) { TodoService.new }
+
+ describe 'GET #index' do
+ before do
+ sign_in(user)
+ project.team << [user, :developer]
+ end
+
+ context 'when using pagination' do
+ let(:last_page) { user.todos.page.total_pages }
+ let!(:issues) { create_list(:issue, 2, project: project, assignee: user) }
+
+ before do
+ issues.each { |issue| todo_service.new_issue(issue, user) }
+ allow(Kaminari.config).to receive(:default_per_page).and_return(1)
+ end
+
+ it 'redirects to last_page if page number is larger than number of pages' do
+ get :index, page: (last_page + 1).to_param
+
+ expect(response).to redirect_to(dashboard_todos_path(page: last_page))
+ end
+
+ it 'redirects to correspondent page' do
+ get :index, page: last_page
+
+ expect(assigns(:todos).current_page).to eq(last_page)
+ expect(response).to have_http_status(200)
+ end
+ end
+ end
+end
diff --git a/spec/controllers/explore/projects_controller_spec.rb b/spec/controllers/explore/projects_controller_spec.rb
new file mode 100644
index 00000000000..9dceeca168d
--- /dev/null
+++ b/spec/controllers/explore/projects_controller_spec.rb
@@ -0,0 +1,27 @@
+require 'spec_helper'
+
+describe Explore::ProjectsController do
+ describe 'GET #trending' do
+ context 'sorting by update date' do
+ let(:project1) { create(:empty_project, :public, updated_at: 3.days.ago) }
+ let(:project2) { create(:empty_project, :public, updated_at: 1.day.ago) }
+
+ before do
+ create(:trending_project, project: project1)
+ create(:trending_project, project: project2)
+ end
+
+ it 'sorts by last updated' do
+ get :trending, sort: 'updated_desc'
+
+ expect(assigns(:projects)).to eq [project2, project1]
+ end
+
+ it 'sorts by oldest updated' do
+ get :trending, sort: 'updated_asc'
+
+ expect(assigns(:projects)).to eq [project1, project2]
+ end
+ end
+ end
+end
diff --git a/spec/controllers/groups/milestones_controller_spec.rb b/spec/controllers/groups/milestones_controller_spec.rb
index 8c52f615b8b..6e4b5f78e33 100644
--- a/spec/controllers/groups/milestones_controller_spec.rb
+++ b/spec/controllers/groups/milestones_controller_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe Groups::MilestonesController do
let(:group) { create(:group) }
- let(:project) { create(:project, group: group) }
+ let(:project) { create(:empty_project, group: group) }
let(:project2) { create(:empty_project, group: group) }
let(:user) { create(:user) }
let(:title) { '肯定不是中文的问题' }
diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb
index a763e2c5ba8..cad82a34fb0 100644
--- a/spec/controllers/groups_controller_spec.rb
+++ b/spec/controllers/groups_controller_spec.rb
@@ -3,7 +3,7 @@ require 'rails_helper'
describe GroupsController do
let(:user) { create(:user) }
let(:group) { create(:group) }
- let(:project) { create(:project, namespace: group) }
+ let(:project) { create(:empty_project, namespace: group) }
let!(:group_member) { create(:group_member, group: group, user: user) }
describe 'GET #index' do
@@ -105,4 +105,25 @@ describe GroupsController do
end
end
end
+
+ describe 'PUT update' do
+ before do
+ sign_in(user)
+ end
+
+ it 'updates the path succesfully' do
+ post :update, id: group.to_param, group: { path: 'new_path' }
+
+ expect(response).to have_http_status(302)
+ expect(controller).to set_flash[:notice]
+ end
+
+ it 'does not update the path on error' do
+ allow_any_instance_of(Group).to receive(:move_dir).and_raise(Gitlab::UpdatePathError)
+ post :update, id: group.to_param, group: { path: 'new_path' }
+
+ expect(assigns(:group).errors).not_to be_empty
+ expect(assigns(:group).path).not_to eq('new_path')
+ end
+ end
end
diff --git a/spec/controllers/health_check_controller_spec.rb b/spec/controllers/health_check_controller_spec.rb
index 56ecf2bb644..cfe18dd4b6c 100644
--- a/spec/controllers/health_check_controller_spec.rb
+++ b/spec/controllers/health_check_controller_spec.rb
@@ -1,10 +1,16 @@
require 'spec_helper'
describe HealthCheckController do
+ include StubENV
+
let(:token) { current_application_settings.health_check_access_token }
let(:json_response) { JSON.parse(response.body) }
let(:xml_response) { Hash.from_xml(response.body)['hash'] }
+ before do
+ stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
+ end
+
describe 'GET #index' do
context 'when services are up but NO access token' do
it 'returns a not found page' do
diff --git a/spec/controllers/import/bitbucket_controller_spec.rb b/spec/controllers/import/bitbucket_controller_spec.rb
index ce7c0b334ee..fa4cc0ebbe0 100644
--- a/spec/controllers/import/bitbucket_controller_spec.rb
+++ b/spec/controllers/import/bitbucket_controller_spec.rb
@@ -52,7 +52,7 @@ describe Import::BitbucketController do
end
it "assigns variables" do
- @project = create(:project, import_type: 'bitbucket', creator_id: user.id)
+ @project = create(:empty_project, import_type: 'bitbucket', creator_id: user.id)
allow_any_instance_of(Bitbucket::Client).to receive(:repos).and_return([@repo])
get :status
@@ -63,7 +63,7 @@ describe Import::BitbucketController do
end
it "does not show already added project" do
- @project = create(:project, import_type: 'bitbucket', creator_id: user.id, import_source: 'asd/vim')
+ @project = create(:empty_project, import_type: 'bitbucket', creator_id: user.id, import_source: 'asd/vim')
allow_any_instance_of(Bitbucket::Client).to receive(:repos).and_return([@repo])
get :status
diff --git a/spec/controllers/import/fogbugz_controller_spec.rb b/spec/controllers/import/fogbugz_controller_spec.rb
index 5f0f6dea821..fffbc805335 100644
--- a/spec/controllers/import/fogbugz_controller_spec.rb
+++ b/spec/controllers/import/fogbugz_controller_spec.rb
@@ -16,7 +16,7 @@ describe Import::FogbugzController do
end
it 'assigns variables' do
- @project = create(:project, import_type: 'fogbugz', creator_id: user.id)
+ @project = create(:empty_project, import_type: 'fogbugz', creator_id: user.id)
stub_client(repos: [@repo])
get :status
@@ -26,7 +26,7 @@ describe Import::FogbugzController do
end
it 'does not show already added project' do
- @project = create(:project, import_type: 'fogbugz', creator_id: user.id, import_source: 'vim')
+ @project = create(:empty_project, import_type: 'fogbugz', creator_id: user.id, import_source: 'vim')
stub_client(repos: [@repo])
get :status
diff --git a/spec/controllers/import/gitlab_controller_spec.rb b/spec/controllers/import/gitlab_controller_spec.rb
index 6f75ebb16c8..3f73ea000ae 100644
--- a/spec/controllers/import/gitlab_controller_spec.rb
+++ b/spec/controllers/import/gitlab_controller_spec.rb
@@ -36,7 +36,7 @@ describe Import::GitlabController do
end
it "assigns variables" do
- @project = create(:project, import_type: 'gitlab', creator_id: user.id)
+ @project = create(:empty_project, import_type: 'gitlab', creator_id: user.id)
stub_client(projects: [@repo])
get :status
@@ -46,7 +46,7 @@ describe Import::GitlabController do
end
it "does not show already added project" do
- @project = create(:project, import_type: 'gitlab', creator_id: user.id, import_source: 'asd/vim')
+ @project = create(:empty_project, import_type: 'gitlab', creator_id: user.id, import_source: 'asd/vim')
stub_client(projects: [@repo])
get :status
diff --git a/spec/controllers/import/google_code_controller_spec.rb b/spec/controllers/import/google_code_controller_spec.rb
index 4241db6e771..c96fb90f70e 100644
--- a/spec/controllers/import/google_code_controller_spec.rb
+++ b/spec/controllers/import/google_code_controller_spec.rb
@@ -27,7 +27,7 @@ describe Import::GoogleCodeController do
end
it "assigns variables" do
- @project = create(:project, import_type: 'google_code', creator_id: user.id)
+ @project = create(:empty_project, import_type: 'google_code', creator_id: user.id)
stub_client(repos: [@repo], incompatible_repos: [])
get :status
@@ -38,7 +38,7 @@ describe Import::GoogleCodeController do
end
it "does not show already added project" do
- @project = create(:project, import_type: 'google_code', creator_id: user.id, import_source: 'vim')
+ @project = create(:empty_project, import_type: 'google_code', creator_id: user.id, import_source: 'vim')
stub_client(repos: [@repo], incompatible_repos: [])
get :status
diff --git a/spec/controllers/notification_settings_controller_spec.rb b/spec/controllers/notification_settings_controller_spec.rb
index 79b819a1377..9e3a31e1a6b 100644
--- a/spec/controllers/notification_settings_controller_spec.rb
+++ b/spec/controllers/notification_settings_controller_spec.rb
@@ -93,7 +93,7 @@ describe NotificationSettingsController do
end
context 'not authorized' do
- let(:private_project) { create(:project, :private) }
+ let(:private_project) { create(:empty_project, :private) }
before { sign_in(user) }
it 'returns 404' do
diff --git a/spec/controllers/projects/avatars_controller_spec.rb b/spec/controllers/projects/avatars_controller_spec.rb
index f5ea097af8b..8b71d6518bb 100644
--- a/spec/controllers/projects/avatars_controller_spec.rb
+++ b/spec/controllers/projects/avatars_controller_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Projects::AvatarsController do
- let(:project) { create(:project, avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png")) }
+ let(:project) { create(:empty_project, avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png")) }
let(:user) { create(:user) }
before do
diff --git a/spec/controllers/projects/blame_controller_spec.rb b/spec/controllers/projects/blame_controller_spec.rb
index 4402ca43c65..addc5e7ec33 100644
--- a/spec/controllers/projects/blame_controller_spec.rb
+++ b/spec/controllers/projects/blame_controller_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Projects::BlameController do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
before do
diff --git a/spec/controllers/projects/blob_controller_spec.rb b/spec/controllers/projects/blob_controller_spec.rb
index 3efef757ae2..b36d0e69330 100644
--- a/spec/controllers/projects/blob_controller_spec.rb
+++ b/spec/controllers/projects/blob_controller_spec.rb
@@ -1,7 +1,7 @@
require 'rails_helper'
describe Projects::BlobController do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :public, :repository) }
let(:user) { create(:user) }
before do
@@ -84,5 +84,35 @@ describe Projects::BlobController do
end
end
end
+
+ context 'when user has forked project' do
+ let(:guest) { create(:user) }
+ let!(:forked_project) { Projects::ForkService.new(project, guest).execute }
+ let!(:merge_request) { create(:merge_request, source_project: project, target_project: project, source_branch: "fork-test-1", target_branch: "master") }
+
+ before { sign_in(guest) }
+
+ it "redirects to forked project new merge request" do
+ default_params[:target_branch] = "fork-test-1"
+ default_params[:create_merge_request] = 1
+
+ allow_any_instance_of(Files::UpdateService).to receive(:commit).and_return(:success)
+
+ put :update, default_params
+
+ expect(response).to redirect_to(
+ new_namespace_project_merge_request_path(
+ forked_project.namespace,
+ forked_project,
+ merge_request: {
+ source_project_id: forked_project.id,
+ target_project_id: project.id,
+ source_branch: "fork-test-1",
+ target_branch: "master"
+ }
+ )
+ )
+ end
+ end
end
end
diff --git a/spec/controllers/projects/boards/issues_controller_spec.rb b/spec/controllers/projects/boards/issues_controller_spec.rb
index 299d2c981d3..ad15e3942a5 100644
--- a/spec/controllers/projects/boards/issues_controller_spec.rb
+++ b/spec/controllers/projects/boards/issues_controller_spec.rb
@@ -18,23 +18,7 @@ describe Projects::Boards::IssuesController do
end
describe 'GET index' do
- context 'with valid list id' do
- it 'returns issues that have the list label applied' do
- johndoe = create(:user, avatar: fixture_file_upload(File.join(Rails.root, 'spec/fixtures/dk.png')))
- issue = create(:labeled_issue, project: project, labels: [planning])
- create(:labeled_issue, project: project, labels: [planning])
- create(:labeled_issue, project: project, labels: [development], due_date: Date.tomorrow)
- create(:labeled_issue, project: project, labels: [development], assignee: johndoe)
- issue.subscribe(johndoe, project)
-
- list_issues user: user, board: board, list: list2
-
- parsed_response = JSON.parse(response.body)
-
- expect(response).to match_response_schema('issues')
- expect(parsed_response.length).to eq 2
- end
- end
+ let(:johndoe) { create(:user, avatar: fixture_file_upload(File.join(Rails.root, 'spec/fixtures/dk.png'))) }
context 'with invalid board id' do
it 'returns a not found 404 response' do
@@ -44,11 +28,47 @@ describe Projects::Boards::IssuesController do
end
end
- context 'with invalid list id' do
- it 'returns a not found 404 response' do
- list_issues user: user, board: board, list: 999
+ context 'when list id is present' do
+ context 'with valid list id' do
+ it 'returns issues that have the list label applied' do
+ issue = create(:labeled_issue, project: project, labels: [planning])
+ create(:labeled_issue, project: project, labels: [planning])
+ create(:labeled_issue, project: project, labels: [development], due_date: Date.tomorrow)
+ create(:labeled_issue, project: project, labels: [development], assignee: johndoe)
+ issue.subscribe(johndoe, project)
- expect(response).to have_http_status(404)
+ list_issues user: user, board: board, list: list2
+
+ parsed_response = JSON.parse(response.body)
+
+ expect(response).to match_response_schema('issues')
+ expect(parsed_response.length).to eq 2
+ end
+ end
+
+ context 'with invalid list id' do
+ it 'returns a not found 404 response' do
+ list_issues user: user, board: board, list: 999
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ context 'when list id is missing' do
+ it 'returns opened issues without board labels applied' do
+ bug = create(:label, project: project, name: 'Bug')
+ create(:issue, project: project)
+ create(:labeled_issue, project: project, labels: [planning])
+ create(:labeled_issue, project: project, labels: [development])
+ create(:labeled_issue, project: project, labels: [bug])
+
+ list_issues user: user, board: board
+
+ parsed_response = JSON.parse(response.body)
+
+ expect(response).to match_response_schema('issues')
+ expect(parsed_response.length).to eq 2
end
end
@@ -65,13 +85,17 @@ describe Projects::Boards::IssuesController do
end
end
- def list_issues(user:, board:, list:)
+ def list_issues(user:, board:, list: nil)
sign_in(user)
- get :index, namespace_id: project.namespace.to_param,
- project_id: project.to_param,
- board_id: board.to_param,
- list_id: list.to_param
+ params = {
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ board_id: board.to_param,
+ list_id: list.try(:to_param)
+ }
+
+ get :index, params.compact
end
end
diff --git a/spec/controllers/projects/boards/lists_controller_spec.rb b/spec/controllers/projects/boards/lists_controller_spec.rb
index 34d6119429d..b3f9f76a50c 100644
--- a/spec/controllers/projects/boards/lists_controller_spec.rb
+++ b/spec/controllers/projects/boards/lists_controller_spec.rb
@@ -27,7 +27,7 @@ describe Projects::Boards::ListsController do
parsed_response = JSON.parse(response.body)
expect(response).to match_response_schema('lists')
- expect(parsed_response.length).to eq 3
+ expect(parsed_response.length).to eq 2
end
context 'with unauthorized user' do
diff --git a/spec/controllers/projects/branches_controller_spec.rb b/spec/controllers/projects/branches_controller_spec.rb
index b88586b8678..9de03876755 100644
--- a/spec/controllers/projects/branches_controller_spec.rb
+++ b/spec/controllers/projects/branches_controller_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Projects::BranchesController do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
let(:developer) { create(:user) }
diff --git a/spec/controllers/projects/commit_controller_spec.rb b/spec/controllers/projects/commit_controller_spec.rb
index 646b097d74e..ebd2d0e092b 100644
--- a/spec/controllers/projects/commit_controller_spec.rb
+++ b/spec/controllers/projects/commit_controller_spec.rb
@@ -1,9 +1,9 @@
require 'spec_helper'
describe Projects::CommitController do
- let(:project) { create(:project) }
- let(:user) { create(:user) }
- let(:commit) { project.commit("master") }
+ let(:project) { create(:project, :repository) }
+ let(:user) { create(:user) }
+ let(:commit) { project.commit("master") }
let(:master_pickable_sha) { '7d3b0f7cff5f37573aea97cebfd5692ea1689924' }
let(:master_pickable_commit) { project.commit(master_pickable_sha) }
@@ -309,4 +309,50 @@ describe Projects::CommitController do
end
end
end
+
+ describe 'GET pipelines' do
+ def get_pipelines(extra_params = {})
+ params = {
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param
+ }
+
+ get :pipelines, params.merge(extra_params)
+ end
+
+ context 'when the commit exists' do
+ context 'when the commit has pipelines' do
+ before do
+ create(:ci_pipeline, project: project, sha: commit.id)
+ end
+
+ context 'when rendering a HTML format' do
+ it 'shows pipelines' do
+ get_pipelines(id: commit.id)
+
+ expect(response).to be_ok
+ end
+ end
+
+ context 'when rendering a JSON format' do
+ it 'responds with serialized pipelines' do
+ get_pipelines(id: commit.id, format: :json)
+
+ expect(response).to be_ok
+ expect(JSON.parse(response.body)).not_to be_empty
+ end
+ end
+ end
+ end
+
+ context 'when the commit does not exist' do
+ before do
+ get_pipelines(id: 'e7a412c8da9f6d0081a633a4a402dde1c4694ebd')
+ end
+
+ it 'returns a 404' do
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
end
diff --git a/spec/controllers/projects/commits_controller_spec.rb b/spec/controllers/projects/commits_controller_spec.rb
index 1ac7e03a2db..54b8d1108a5 100644
--- a/spec/controllers/projects/commits_controller_spec.rb
+++ b/spec/controllers/projects/commits_controller_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Projects::CommitsController do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
before do
diff --git a/spec/controllers/projects/compare_controller_spec.rb b/spec/controllers/projects/compare_controller_spec.rb
index 7a57801c437..e811c76fb31 100644
--- a/spec/controllers/projects/compare_controller_spec.rb
+++ b/spec/controllers/projects/compare_controller_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Projects::CompareController do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
let(:ref_from) { "improve%2Fawesome" }
let(:ref_to) { "feature" }
@@ -64,6 +64,36 @@ describe Projects::CompareController do
expect(assigns(:diffs)).to eq(nil)
expect(assigns(:commits)).to eq(nil)
end
+
+ it 'redirects back to index when params[:from] is empty and preserves params[:to]' do
+ post(:create,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ from: '',
+ to: 'master')
+
+ expect(response).to redirect_to(namespace_project_compare_index_path(project.namespace, project, to: 'master'))
+ end
+
+ it 'redirects back to index when params[:to] is empty and preserves params[:from]' do
+ post(:create,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ from: 'master',
+ to: '')
+
+ expect(response).to redirect_to(namespace_project_compare_index_path(project.namespace, project, from: 'master'))
+ end
+
+ it 'redirects back to index when params[:from] and params[:to] are empty' do
+ post(:create,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ from: '',
+ to: '')
+
+ expect(response).to redirect_to(namespace_project_compare_index_path)
+ end
end
describe 'GET diff_for_path' do
diff --git a/spec/controllers/projects/cycle_analytics_controller_spec.rb b/spec/controllers/projects/cycle_analytics_controller_spec.rb
index a971adf0539..6a6d71a16ee 100644
--- a/spec/controllers/projects/cycle_analytics_controller_spec.rb
+++ b/spec/controllers/projects/cycle_analytics_controller_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Projects::CycleAnalyticsController do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
before do
diff --git a/spec/controllers/projects/discussions_controller_spec.rb b/spec/controllers/projects/discussions_controller_spec.rb
index ff617fea847..79ab364a6f3 100644
--- a/spec/controllers/projects/discussions_controller_spec.rb
+++ b/spec/controllers/projects/discussions_controller_spec.rb
@@ -1,9 +1,9 @@
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(:user) { create(:user) }
+ let(:merge_request) { create(:merge_request) }
+ let(:project) { merge_request.source_project }
let(:note) { create(:diff_note_on_merge_request, noteable: merge_request, project: project) }
let(:discussion) { note.discussion }
diff --git a/spec/controllers/projects/find_file_controller_spec.rb b/spec/controllers/projects/find_file_controller_spec.rb
index 038dfeb8466..a4884256c92 100644
--- a/spec/controllers/projects/find_file_controller_spec.rb
+++ b/spec/controllers/projects/find_file_controller_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Projects::FindFileController do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
before do
diff --git a/spec/controllers/projects/forks_controller_spec.rb b/spec/controllers/projects/forks_controller_spec.rb
index 028ea067a97..a867668d97b 100644
--- a/spec/controllers/projects/forks_controller_spec.rb
+++ b/spec/controllers/projects/forks_controller_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe Projects::ForksController do
let(:user) { create(:user) }
- let(:project) { create(:project, :public) }
+ let(:project) { create(:project, :public, :repository) }
let(:forked_project) { Projects::ForkService.new(project, user).execute }
let(:group) { create(:group, owner: forked_project.creator) }
diff --git a/spec/controllers/projects/graphs_controller_spec.rb b/spec/controllers/projects/graphs_controller_spec.rb
index 74e6603b0cb..bbe8e4bf6b2 100644
--- a/spec/controllers/projects/graphs_controller_spec.rb
+++ b/spec/controllers/projects/graphs_controller_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Projects::GraphsController do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
before do
diff --git a/spec/controllers/projects/group_links_controller_spec.rb b/spec/controllers/projects/group_links_controller_spec.rb
index b9d9117c928..a976a9c27ab 100644
--- a/spec/controllers/projects/group_links_controller_spec.rb
+++ b/spec/controllers/projects/group_links_controller_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe Projects::GroupLinksController do
let(:group) { create(:group, :private) }
let(:group2) { create(:group, :private) }
- let(:project) { create(:project, :private, group: group2) }
+ let(:project) { create(:empty_project, :private, group: group2) }
let(:user) { create(:user) }
before do
@@ -31,7 +31,7 @@ describe Projects::GroupLinksController do
it 'redirects to project group links page' do
expect(response).to redirect_to(
- namespace_project_group_links_path(project.namespace, project)
+ namespace_project_settings_members_path(project.namespace, project)
)
end
end
@@ -62,7 +62,7 @@ describe Projects::GroupLinksController do
it 'redirects to project group links page' do
expect(response).to redirect_to(
- namespace_project_group_links_path(project.namespace, project)
+ namespace_project_settings_members_path(project.namespace, project)
)
end
end
@@ -76,7 +76,7 @@ describe Projects::GroupLinksController do
it 'redirects to project group links page' do
expect(response).to redirect_to(
- namespace_project_group_links_path(project.namespace, project)
+ namespace_project_settings_members_path(project.namespace, project)
)
expect(flash[:alert]).to eq('Please select a group.')
end
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index dbe5ddccbcf..4b89381eb96 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -52,6 +52,36 @@ describe Projects::IssuesController do
expect(response).to have_http_status(404)
end
end
+
+ context 'with page param' do
+ let(:last_page) { project.issues.page().total_pages }
+ let!(:issue_list) { create_list(:issue, 2, project: project) }
+
+ before do
+ sign_in(user)
+ project.team << [user, :developer]
+ allow(Kaminari.config).to receive(:default_per_page).and_return(1)
+ end
+
+ it 'redirects to last_page if page number is larger than number of pages' do
+ get :index,
+ namespace_id: project.namespace.path.to_param,
+ project_id: project.path.to_param,
+ page: (last_page + 1).to_param
+
+ expect(response).to redirect_to(namespace_project_issues_path(page: last_page, state: controller.params[:state], scope: controller.params[:scope]))
+ end
+
+ it 'redirects to specified page' do
+ get :index,
+ namespace_id: project.namespace.path.to_param,
+ project_id: project.path.to_param,
+ page: last_page.to_param
+
+ expect(assigns(:issues).current_page).to eq(last_page)
+ expect(response).to have_http_status(200)
+ end
+ end
end
describe 'GET #new' do
@@ -68,7 +98,7 @@ describe Projects::IssuesController do
end
it 'fills in an issue for a merge request' do
- project_with_repository = create(:project)
+ project_with_repository = create(:project, :repository)
project_with_repository.team << [user, :developer]
mr = create(:merge_request_with_diff_notes, source_project: project_with_repository)
@@ -94,7 +124,7 @@ describe Projects::IssuesController do
describe 'PUT #update' do
context 'when moving issue to another private project' do
- let(:another_project) { create(:project, :private) }
+ let(:another_project) { create(:empty_project, :private) }
before do
sign_in(user)
@@ -296,6 +326,20 @@ describe Projects::IssuesController do
end
describe 'POST #create' do
+ def post_new_issue(issue_attrs = {}, additional_params = {})
+ sign_in(user)
+ project = create(:empty_project, :public)
+ project.team << [user, :developer]
+
+ post :create, {
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ issue: { title: 'Title', description: 'Description' }.merge(issue_attrs)
+ }.merge(additional_params)
+
+ project.issues.first
+ end
+
context 'resolving discussions in MergeRequest' do
let(:discussion) { Discussion.for_diff_notes([create(:diff_note_on_merge_request)]).first }
let(:merge_request) { discussion.noteable }
@@ -334,30 +378,81 @@ describe Projects::IssuesController do
context 'Akismet is enabled' do
before do
+ stub_application_setting(recaptcha_enabled: true)
allow_any_instance_of(SpamService).to receive(:check_for_spam?).and_return(true)
- allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true)
end
- def post_spam_issue
- 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
+ context 'when an issue is not identified as a spam' do
+ before do
+ allow_any_instance_of(described_class).to receive(:verify_recaptcha).and_return(false)
+ allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(false)
+ end
- it 'rejects an issue recognized as spam' do
- expect{ post_spam_issue }.not_to change(Issue, :count)
- expect(response).to render_template(:new)
+ it 'does not create an issue' do
+ expect { post_new_issue(title: '') }.not_to change(Issue, :count)
+ end
end
- it 'creates a spam log' do
- post_spam_issue
- spam_logs = SpamLog.all
- expect(spam_logs.count).to eq(1)
- expect(spam_logs[0].title).to eq('Spam Title')
+ context 'when an issue is identified as a spam' do
+ before { allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true) }
+
+ context 'when captcha is not verified' do
+ def post_spam_issue
+ post_new_issue(title: 'Spam Title', description: 'Spam lives here')
+ end
+
+ before { allow_any_instance_of(described_class).to receive(:verify_recaptcha).and_return(false) }
+
+ it 'rejects an issue recognized as a spam' do
+ expect { post_spam_issue }.not_to change(Issue, :count)
+ end
+
+ it 'creates a spam log' do
+ post_spam_issue
+ spam_logs = SpamLog.all
+
+ expect(spam_logs.count).to eq(1)
+ expect(spam_logs.first.title).to eq('Spam Title')
+ expect(spam_logs.first.recaptcha_verified).to be_falsey
+ end
+
+ it 'does not create an issue when it is not valid' do
+ expect { post_new_issue(title: '') }.not_to change(Issue, :count)
+ end
+
+ it 'does not create an issue when recaptcha is not enabled' do
+ stub_application_setting(recaptcha_enabled: false)
+
+ expect { post_spam_issue }.not_to change(Issue, :count)
+ end
+ end
+
+ context 'when captcha is verified' do
+ let!(:spam_logs) { create_list(:spam_log, 2, user: user, title: 'Title') }
+
+ def post_verified_issue
+ post_new_issue({}, { spam_log_id: spam_logs.last.id, recaptcha_verification: true } )
+ end
+
+ before do
+ allow_any_instance_of(described_class).to receive(:verify_recaptcha).and_return(true)
+ end
+
+ it 'accepts an issue after recaptcha is verified' do
+ expect { post_verified_issue }.to change(Issue, :count)
+ end
+
+ it 'marks spam log as recaptcha_verified' do
+ expect { post_verified_issue }.to change { SpamLog.last.recaptcha_verified }.from(false).to(true)
+ end
+
+ it 'does not mark spam log as recaptcha_verified when it does not belong to current_user' do
+ spam_log = create(:spam_log)
+
+ expect { post_new_issue({}, { spam_log_id: spam_log.id, recaptcha_verification: true } ) }.
+ not_to change { SpamLog.last.recaptcha_verified }
+ end
+ end
end
end
@@ -366,18 +461,26 @@ describe Projects::IssuesController do
request.env['action_dispatch.remote_ip'] = '127.0.0.1'
end
- def post_new_issue
+ it 'creates a user agent detail' do
+ expect { post_new_issue }.to change(UserAgentDetail, :count).by(1)
+ end
+ end
+
+ context 'when description has slash commands' do
+ before do
sign_in(user)
- project = create(:empty_project, :public)
- post :create, {
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
- issue: { title: 'Title', description: 'Description' }
- }
end
- it 'creates a user agent detail' do
- expect{ post_new_issue }.to change(UserAgentDetail, :count).by(1)
+ it 'can add spent time' do
+ issue = post_new_issue(description: '/spend 1h')
+
+ expect(issue.total_time_spent).to eq(3600)
+ end
+
+ it 'can set the time estimate' do
+ issue = post_new_issue(description: '/estimate 2h')
+
+ expect(issue.time_estimate).to eq(7200)
end
end
end
@@ -420,7 +523,7 @@ describe Projects::IssuesController do
context "when the user is owner" do
let(:owner) { create(:user) }
let(:namespace) { create(:namespace, owner: owner) }
- let(:project) { create(:project, namespace: namespace) }
+ let(:project) { create(:empty_project, namespace: namespace) }
before { sign_in(owner) }
diff --git a/spec/controllers/projects/labels_controller_spec.rb b/spec/controllers/projects/labels_controller_spec.rb
index ec6cea5c0f4..3e0326dd47d 100644
--- a/spec/controllers/projects/labels_controller_spec.rb
+++ b/spec/controllers/projects/labels_controller_spec.rb
@@ -112,4 +112,49 @@ describe Projects::LabelsController do
post :toggle_subscription, namespace_id: project.namespace.to_param, project_id: project.to_param, id: label.to_param
end
end
+
+ describe 'POST #promote' do
+ let!(:promoted_label_name) { "Promoted Label" }
+ let!(:label_1) { create(:label, title: promoted_label_name, project: project) }
+
+ context 'not group owner' do
+ it 'denies access' do
+ post :promote, namespace_id: project.namespace.to_param, project_id: project.to_param, id: label_1.to_param
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'group owner' do
+ before do
+ GroupMember.add_users_to_group(group, [user], :owner)
+ end
+
+ it 'gives access' do
+ post :promote, namespace_id: project.namespace.to_param, project_id: project.to_param, id: label_1.to_param
+
+ expect(response).to redirect_to(namespace_project_labels_path)
+ end
+
+ it 'promotes the label' do
+ post :promote, namespace_id: project.namespace.to_param, project_id: project.to_param, id: label_1.to_param
+
+ expect(Label.where(id: label_1.id)).to be_empty
+ expect(GroupLabel.find_by(title: promoted_label_name)).not_to be_nil
+ end
+
+ context 'service raising InvalidRecord' do
+ before do
+ expect_any_instance_of(Labels::PromoteService).to receive(:execute) do |label|
+ raise ActiveRecord::RecordInvalid.new(label_1)
+ end
+ end
+
+ it 'returns to label list' do
+ post :promote, namespace_id: project.namespace.to_param, project_id: project.to_param, id: label_1.to_param
+ expect(response).to redirect_to(namespace_project_labels_path)
+ end
+ end
+ end
+ end
end
diff --git a/spec/controllers/projects/mattermosts_controller_spec.rb b/spec/controllers/projects/mattermosts_controller_spec.rb
new file mode 100644
index 00000000000..cae733f0cfb
--- /dev/null
+++ b/spec/controllers/projects/mattermosts_controller_spec.rb
@@ -0,0 +1,58 @@
+require 'spec_helper'
+
+describe Projects::MattermostsController do
+ let!(:project) { create(:empty_project) }
+ let!(:user) { create(:user) }
+
+ before do
+ project.team << [user, :master]
+ sign_in(user)
+ end
+
+ describe 'GET #new' do
+ before do
+ allow_any_instance_of(MattermostSlashCommandsService).
+ to receive(:list_teams).and_return([])
+ end
+
+ it 'accepts the request' do
+ get(:new,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param)
+
+ expect(response).to have_http_status(200)
+ end
+ end
+
+ describe 'POST #create' do
+ let(:mattermost_params) { { trigger: 'http://localhost:3000/trigger', team_id: 'abc' } }
+
+ subject do
+ post(:create,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ mattermost: mattermost_params)
+ end
+
+ context 'no request can be made to mattermost' do
+ it 'shows the error' do
+ allow_any_instance_of(MattermostSlashCommandsService).to receive(:configure).and_return([false, "error message"])
+
+ expect(subject).to redirect_to(new_namespace_project_mattermost_url(project.namespace, project))
+ end
+ end
+
+ context 'the request is succesull' do
+ before do
+ allow_any_instance_of(Mattermost::Command).to receive(:create).and_return('token')
+ end
+
+ it 'redirects to the new page' do
+ subject
+ service = project.services.last
+
+ expect(subject).to redirect_to(edit_namespace_project_service_url(project.namespace, project, service))
+ end
+ end
+ end
+end
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index 9e0b80205d8..63780802cfa 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe Projects::MergeRequestsController do
+ include ApiHelpers
+
let(:project) { create(:project) }
let(:user) { create(:user) }
let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) }
@@ -20,23 +22,41 @@ describe Projects::MergeRequestsController do
render_views
let(:fork_project) { create(:forked_project_with_submodules) }
+ before { fork_project.team << [user, :master] }
- before do
- fork_project.team << [user, :master]
+ context 'when rendering HTML response' do
+ it 'renders new merge request widget template' do
+ submit_new_merge_request
+
+ expect(response).to be_success
+ end
end
- it 'renders it' do
- get :new,
- namespace_id: fork_project.namespace.to_param,
- project_id: fork_project.to_param,
- merge_request: {
- source_branch: 'remove-submodule',
- target_branch: 'master'
- }
+ context 'when rendering JSON response' do
+ before do
+ create(:ci_pipeline, sha: fork_project.commit('remove-submodule').id,
+ ref: 'remove-submodule',
+ project: fork_project)
+ end
- expect(response).to be_success
+ it 'renders JSON including serialized pipelines' do
+ submit_new_merge_request(format: :json)
+
+ expect(response).to be_ok
+ expect(json_response).not_to be_empty
+ end
end
end
+
+ def submit_new_merge_request(format: :html)
+ get :new,
+ namespace_id: fork_project.namespace.to_param,
+ project_id: fork_project.to_param,
+ merge_request: {
+ source_branch: 'remove-submodule',
+ target_branch: 'master' },
+ format: format
+ end
end
shared_examples "loads labels" do |action|
@@ -127,11 +147,29 @@ describe Projects::MergeRequestsController do
end
describe 'GET index' do
- def get_merge_requests
+ def get_merge_requests(page = nil)
get :index,
namespace_id: project.namespace.to_param,
project_id: project.to_param,
- state: 'opened'
+ state: 'opened', page: page.to_param
+ end
+
+ context 'when page param' do
+ let(:last_page) { project.merge_requests.page().total_pages }
+ let!(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) }
+
+ it 'redirects to last_page if page number is larger than number of pages' do
+ get_merge_requests(last_page + 1)
+
+ expect(response).to redirect_to(namespace_project_merge_requests_path(page: last_page, state: controller.params[:state], scope: controller.params[:scope]))
+ end
+
+ it 'redirects to specified page' do
+ get_merge_requests(last_page)
+
+ expect(assigns(:merge_requests).current_page).to eq(last_page)
+ expect(response).to have_http_status(200)
+ end
end
context 'when filtering by opened state' do
@@ -437,7 +475,7 @@ describe Projects::MergeRequestsController do
it 'renders the diffs template to a string' do
expect(response).to render_template('projects/merge_requests/show/_diffs')
- expect(JSON.parse(response.body)).to have_key('html')
+ expect(json_response).to have_key('html')
end
end
@@ -476,7 +514,7 @@ describe Projects::MergeRequestsController do
it 'renders the diffs template to a string' do
expect(response).to render_template('projects/merge_requests/show/_diffs')
- expect(JSON.parse(response.body)).to have_key('html')
+ expect(json_response).to have_key('html')
end
end
end
@@ -644,22 +682,38 @@ describe Projects::MergeRequestsController do
go format: 'json'
expect(response).to render_template('projects/merge_requests/show/_commits')
- expect(JSON.parse(response.body)).to have_key('html')
+ expect(json_response).to have_key('html')
end
end
end
- describe 'GET builds' do
- it_behaves_like "loads labels", :builds
- end
-
describe 'GET pipelines' do
- it_behaves_like "loads labels", :pipelines
+ before do
+ create(:ci_pipeline, project: merge_request.source_project,
+ ref: merge_request.source_branch,
+ sha: merge_request.diff_head_sha)
+ end
+
+ context 'when using HTML format' do
+ it_behaves_like "loads labels", :pipelines
+ end
+
+ context 'when using JSON format' do
+ before do
+ get :pipelines,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ id: merge_request.iid,
+ format: :json
+ end
+
+ it 'responds with serialized pipelines' do
+ expect(json_response).not_to be_empty
+ end
+ end
end
describe 'GET conflicts' do
- let(:json_response) { JSON.parse(response.body) }
-
context 'when the conflicts cannot be resolved in the UI' do
before do
allow_any_instance_of(Gitlab::Conflict::Parser).
@@ -756,8 +810,6 @@ describe Projects::MergeRequestsController do
end
describe 'GET conflict_for_path' do
- let(:json_response) { JSON.parse(response.body) }
-
def conflict_for_path(path)
get :conflict_for_path,
namespace_id: merge_request_with_conflicts.project.namespace.to_param,
@@ -812,7 +864,6 @@ describe Projects::MergeRequestsController do
end
context 'POST resolve_conflicts' do
- let(:json_response) { JSON.parse(response.body) }
let!(:original_head_sha) { merge_request_with_conflicts.diff_head_sha }
def resolve_conflicts(files)
@@ -1010,7 +1061,6 @@ describe Projects::MergeRequestsController do
let!(:forked) { create(:project) }
let!(:environment) { create(:environment, project: forked) }
let!(:deployment) { create(:deployment, environment: environment, sha: forked.commit.id, ref: 'master') }
- let(:json_response) { JSON.parse(response.body) }
let(:admin) { create(:admin) }
let(:merge_request) do
@@ -1034,4 +1084,72 @@ describe Projects::MergeRequestsController do
end
end
end
+
+ describe 'GET merge_widget_refresh' do
+ let(:params) do
+ {
+ namespace_id: project.namespace.path,
+ project_id: project.path,
+ id: merge_request.iid,
+ format: :raw
+ }
+ end
+
+ before do
+ project.team << [user, :developer]
+ xhr :get, :merge_widget_refresh, params
+ end
+
+ context 'when merge in progress' do
+ let(:merge_request) { create(:merge_request, source_project: project, in_progress_merge_commit_sha: 'sha') }
+
+ it 'returns an OK response' do
+ expect(response).to have_http_status(:ok)
+ end
+
+ it 'sets status to :success' do
+ expect(assigns(:status)).to eq(:success)
+ expect(response).to render_template('merge')
+ end
+ end
+
+ context 'when merge request was merged already' do
+ let(:merge_request) { create(:merge_request, source_project: project, state: :merged) }
+
+ it 'returns an OK response' do
+ expect(response).to have_http_status(:ok)
+ end
+
+ it 'sets status to :success' do
+ expect(assigns(:status)).to eq(:success)
+ expect(response).to render_template('merge')
+ end
+ end
+
+ context 'when waiting for build' do
+ let(:merge_request) { create(:merge_request, source_project: project, merge_when_build_succeeds: true, merge_user: user) }
+
+ it 'returns an OK response' do
+ expect(response).to have_http_status(:ok)
+ end
+
+ it 'sets status to :merge_when_build_succeeds' do
+ expect(assigns(:status)).to eq(:merge_when_build_succeeds)
+ expect(response).to render_template('merge')
+ end
+ end
+
+ context 'when no special status for MR' do
+ let(:merge_request) { create(:merge_request, source_project: project) }
+
+ it 'returns an OK response' do
+ expect(response).to have_http_status(:ok)
+ end
+
+ it 'sets status to nil' do
+ expect(assigns(:status)).to be_nil
+ expect(response).to render_template('merge')
+ end
+ end
+ end
end
diff --git a/spec/controllers/projects/milestones_controller_spec.rb b/spec/controllers/projects/milestones_controller_spec.rb
index 6d30d085056..14207bf6b7a 100644
--- a/spec/controllers/projects/milestones_controller_spec.rb
+++ b/spec/controllers/projects/milestones_controller_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Projects::MilestonesController do
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project) }
let(:user) { create(:user) }
let(:milestone) { create(:milestone, project: project) }
let(:issue) { create(:issue, project: project, milestone: milestone) }
diff --git a/spec/controllers/projects/notes_controller_spec.rb b/spec/controllers/projects/notes_controller_spec.rb
index 92e38b02615..dc597202050 100644
--- a/spec/controllers/projects/notes_controller_spec.rb
+++ b/spec/controllers/projects/notes_controller_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe Projects::NotesController do
let(:user) { create(:user) }
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project) }
let(:issue) { create(:issue, project: project) }
let(:note) { create(:note, noteable: issue, project: project) }
@@ -14,6 +14,55 @@ describe Projects::NotesController do
}
end
+ describe 'POST create' do
+ let(:merge_request) { create(:merge_request) }
+ let(:project) { merge_request.source_project }
+ let(:request_params) do
+ {
+ note: { note: 'some note', noteable_id: merge_request.id, noteable_type: 'MergeRequest' },
+ namespace_id: project.namespace,
+ project_id: project,
+ merge_request_diff_head_sha: 'sha'
+ }
+ end
+
+ before do
+ sign_in(user)
+ project.team << [user, :developer]
+ end
+
+ it "returns status 302 for html" do
+ post :create, request_params
+
+ expect(response).to have_http_status(302)
+ end
+
+ it "returns status 200 for json" do
+ post :create, request_params.merge(format: :json)
+
+ expect(response).to have_http_status(200)
+ end
+
+ context 'when merge_request_diff_head_sha present' do
+ before do
+ service_params = {
+ note: 'some note',
+ noteable_id: merge_request.id.to_s,
+ noteable_type: 'MergeRequest',
+ merge_request_diff_head_sha: 'sha'
+ }
+
+ expect(Notes::CreateService).to receive(:new).with(project, user, service_params).and_return(double(execute: true))
+ end
+
+ it "returns status 302 for html" do
+ post :create, request_params
+
+ expect(response).to have_http_status(302)
+ end
+ end
+ end
+
describe 'POST toggle_award_emoji' do
before do
sign_in(user)
@@ -40,6 +89,7 @@ describe Projects::NotesController do
end
describe "resolving and unresolving" do
+ let(:project) { create(:project, :repository) }
let(:merge_request) { create(:merge_request, source_project: project) }
let(:note) { create(:diff_note_on_merge_request, noteable: merge_request, project: project) }
diff --git a/spec/controllers/projects/pages_domains_controller_spec.rb b/spec/controllers/projects/pages_domains_controller_spec.rb
new file mode 100644
index 00000000000..2362df895a8
--- /dev/null
+++ b/spec/controllers/projects/pages_domains_controller_spec.rb
@@ -0,0 +1,64 @@
+require 'spec_helper'
+
+describe Projects::PagesDomainsController do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+
+ let(:request_params) do
+ {
+ namespace_id: project.namespace,
+ project_id: project
+ }
+ end
+
+ before do
+ sign_in(user)
+ project.team << [user, :master]
+ end
+
+ describe 'GET show' do
+ let!(:pages_domain) { create(:pages_domain, project: project) }
+
+ it "displays the 'show' page" do
+ get(:show, request_params.merge(id: pages_domain.domain))
+
+ expect(response).to have_http_status(200)
+ expect(response).to render_template('show')
+ end
+ end
+
+ describe 'GET new' do
+ it "displays the 'new' page" do
+ get(:new, request_params)
+
+ expect(response).to have_http_status(200)
+ expect(response).to render_template('new')
+ end
+ end
+
+ describe 'POST create' do
+ let(:pages_domain_params) do
+ build(:pages_domain, :with_certificate, :with_key).slice(:key, :certificate, :domain)
+ end
+
+ it "creates a new pages domain" do
+ expect do
+ post(:create, request_params.merge(pages_domain: pages_domain_params))
+ end.to change { PagesDomain.count }.by(1)
+
+ expect(response).to redirect_to(namespace_project_pages_path(project.namespace, project))
+ end
+ end
+
+ describe 'DELETE destroy' do
+ let!(:pages_domain) { create(:pages_domain, project: project) }
+
+ it "deletes the pages domain" do
+ expect do
+ delete(:destroy, request_params.merge(id: pages_domain.domain))
+ end.to change { PagesDomain.count }.by(-1)
+
+ expect(response).to redirect_to(namespace_project_pages_path(project.namespace, project))
+ end
+ end
+end
diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb
index 5fe7e6407cc..1ed2ee3ab4a 100644
--- a/spec/controllers/projects/pipelines_controller_spec.rb
+++ b/spec/controllers/projects/pipelines_controller_spec.rb
@@ -5,13 +5,33 @@ describe Projects::PipelinesController do
let(:user) { create(:user) }
let(:project) { create(:empty_project, :public) }
- let(:pipeline) { create(:ci_pipeline, project: project) }
before do
sign_in(user)
end
+ describe 'GET index.json' do
+ before do
+ create_list(:ci_empty_pipeline, 2, project: project)
+
+ get :index, namespace_id: project.namespace.path,
+ project_id: project.path,
+ format: :json
+ end
+
+ it 'returns JSON with serialized pipelines' do
+ expect(response).to have_http_status(:ok)
+
+ expect(json_response).to include('pipelines')
+ expect(json_response['pipelines'].count).to eq 2
+ expect(json_response['count']['all']).to eq 2
+ expect(json_response['count']['running_or_pending']).to eq 2
+ end
+ end
+
describe 'GET stages.json' do
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+
context 'when accessing existing stage' do
before do
create(:ci_build, pipeline: pipeline, stage: 'build')
diff --git a/spec/controllers/projects/project_members_controller_spec.rb b/spec/controllers/projects/project_members_controller_spec.rb
index b52137fbe7e..416eaa0037e 100644
--- a/spec/controllers/projects/project_members_controller_spec.rb
+++ b/spec/controllers/projects/project_members_controller_spec.rb
@@ -5,11 +5,11 @@ describe Projects::ProjectMembersController do
let(:project) { create(:empty_project, :public, :access_requestable) }
describe 'GET index' do
- it 'renders index with 200 status code' do
+ it 'should have the settings/members address with a 302 status code' do
get :index, namespace_id: project.namespace, project_id: project
- expect(response).to have_http_status(200)
- expect(response).to render_template(:index)
+ expect(response).to have_http_status(302)
+ expect(response.location).to include namespace_project_settings_members_path(project.namespace, project)
end
end
@@ -44,7 +44,7 @@ describe Projects::ProjectMembersController do
access_level: Gitlab::Access::GUEST
expect(response).to set_flash.to 'Users were successfully added.'
- expect(response).to redirect_to(namespace_project_project_members_path(project.namespace, project))
+ expect(response).to redirect_to(namespace_project_settings_members_path(project.namespace, project))
end
it 'adds no user to members' do
@@ -56,7 +56,7 @@ describe Projects::ProjectMembersController do
access_level: Gitlab::Access::GUEST
expect(response).to set_flash.to 'No users or groups specified.'
- expect(response).to redirect_to(namespace_project_project_members_path(project.namespace, project))
+ expect(response).to redirect_to(namespace_project_settings_members_path(project.namespace, project))
end
end
end
@@ -99,7 +99,7 @@ describe Projects::ProjectMembersController do
id: member
expect(response).to redirect_to(
- namespace_project_project_members_path(project.namespace, project)
+ namespace_project_settings_members_path(project.namespace, project)
)
expect(project.members).not_to include member
end
@@ -143,7 +143,7 @@ describe Projects::ProjectMembersController do
end
context 'and is an owner' do
- let(:project) { create(:project, namespace: user.namespace) }
+ let(:project) { create(:empty_project, namespace: user.namespace) }
before { project.team << [user, :master] }
@@ -234,7 +234,7 @@ describe Projects::ProjectMembersController do
end
describe 'POST apply_import' do
- let(:another_project) { create(:project, :private) }
+ let(:another_project) { create(:empty_project, :private) }
let(:member) { create(:user) }
before do
@@ -259,7 +259,7 @@ describe Projects::ProjectMembersController do
expect(project.team_members).to include member
expect(response).to set_flash.to 'Successfully imported'
expect(response).to redirect_to(
- namespace_project_project_members_path(project.namespace, project)
+ namespace_project_settings_members_path(project.namespace, project)
)
end
end
diff --git a/spec/controllers/projects/raw_controller_spec.rb b/spec/controllers/projects/raw_controller_spec.rb
index 04bd9a01f7b..b23d6e257ba 100644
--- a/spec/controllers/projects/raw_controller_spec.rb
+++ b/spec/controllers/projects/raw_controller_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Projects::RawController do
- let(:public_project) { create(:project, :public) }
+ let(:public_project) { create(:project, :public, :repository) }
describe "#show" do
context 'regular filename' do
diff --git a/spec/controllers/projects/refs_controller_spec.rb b/spec/controllers/projects/refs_controller_spec.rb
index abd45a74e2d..d8fb4667c67 100644
--- a/spec/controllers/projects/refs_controller_spec.rb
+++ b/spec/controllers/projects/refs_controller_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Projects::RefsController do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
before do
diff --git a/spec/controllers/projects/releases_controller_spec.rb b/spec/controllers/projects/releases_controller_spec.rb
index 9fd5c3b85f6..69fcc26c77e 100644
--- a/spec/controllers/projects/releases_controller_spec.rb
+++ b/spec/controllers/projects/releases_controller_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Projects::ReleasesController do
- let!(:project) { create(:project) }
+ let!(:project) { create(:project, :repository) }
let!(:user) { create(:user) }
let!(:release) { create(:release, project: project) }
let!(:tag) { release.tag }
diff --git a/spec/controllers/projects/repositories_controller_spec.rb b/spec/controllers/projects/repositories_controller_spec.rb
index 38e02a46626..04e88879fb8 100644
--- a/spec/controllers/projects/repositories_controller_spec.rb
+++ b/spec/controllers/projects/repositories_controller_spec.rb
@@ -1,7 +1,7 @@
require "spec_helper"
describe Projects::RepositoriesController do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
describe "GET archive" do
context 'as a guest' do
diff --git a/spec/controllers/projects/services_controller_spec.rb b/spec/controllers/projects/services_controller_spec.rb
index 2e44b5128b4..16365642a34 100644
--- a/spec/controllers/projects/services_controller_spec.rb
+++ b/spec/controllers/projects/services_controller_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Projects::ServicesController do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
let(:service) { create(:service, project: project) }
@@ -54,6 +54,7 @@ describe Projects::ServicesController do
context 'on successful update' do
it 'sets the flash' do
expect(service).to receive(:to_param).and_return('hipchat')
+ expect(service).to receive(:event_names).and_return(HipchatService.event_names)
put :update,
namespace_id: project.namespace.id,
diff --git a/spec/controllers/projects/settings/ci_cd_controller_spec.rb b/spec/controllers/projects/settings/ci_cd_controller_spec.rb
new file mode 100644
index 00000000000..e9a91cff1b3
--- /dev/null
+++ b/spec/controllers/projects/settings/ci_cd_controller_spec.rb
@@ -0,0 +1,20 @@
+require('spec_helper')
+
+describe Projects::Settings::CiCdController do
+ let(:project) { create(:empty_project, :public, :access_requestable) }
+ let(:user) { create(:user) }
+
+ before do
+ project.team << [user, :master]
+ sign_in(user)
+ end
+
+ describe 'GET show' do
+ it 'renders show with 200 status code' do
+ get :show, namespace_id: project.namespace, project_id: project
+
+ expect(response).to have_http_status(200)
+ expect(response).to render_template(:show)
+ end
+ end
+end
diff --git a/spec/controllers/projects/settings/integrations_controller_spec.rb b/spec/controllers/projects/settings/integrations_controller_spec.rb
new file mode 100644
index 00000000000..65f7bb34f4a
--- /dev/null
+++ b/spec/controllers/projects/settings/integrations_controller_spec.rb
@@ -0,0 +1,20 @@
+require 'spec_helper'
+
+describe Projects::Settings::IntegrationsController do
+ let(:project) { create(:empty_project, :public) }
+ let(:user) { create(:user) }
+
+ before do
+ project.team << [user, :master]
+ sign_in(user)
+ end
+
+ describe 'GET show' do
+ it 'renders show with 200 status code' do
+ get :show, namespace_id: project.namespace, project_id: project
+
+ expect(response).to have_http_status(200)
+ expect(response).to render_template(:show)
+ end
+ end
+end
diff --git a/spec/controllers/projects/settings/members_controller_spec.rb b/spec/controllers/projects/settings/members_controller_spec.rb
new file mode 100644
index 00000000000..076d6cd9c6e
--- /dev/null
+++ b/spec/controllers/projects/settings/members_controller_spec.rb
@@ -0,0 +1,14 @@
+require('spec_helper')
+
+describe Projects::Settings::MembersController do
+ let(:project) { create(:empty_project, :public, :access_requestable) }
+
+ describe 'GET show' do
+ it 'renders show with 200 status code' do
+ get :show, namespace_id: project.namespace, project_id: project
+
+ expect(response).to have_http_status(200)
+ expect(response).to render_template(:show)
+ end
+ end
+end
diff --git a/spec/controllers/projects/snippets_controller_spec.rb b/spec/controllers/projects/snippets_controller_spec.rb
index 72a3ebf2ebd..19e948d8fb8 100644
--- a/spec/controllers/projects/snippets_controller_spec.rb
+++ b/spec/controllers/projects/snippets_controller_spec.rb
@@ -6,11 +6,33 @@ describe Projects::SnippetsController do
let(:user2) { create(:user) }
before do
- project.team << [user, :master]
- project.team << [user2, :master]
+ project.add_master(user)
+ project.add_master(user2)
end
describe 'GET #index' do
+ context 'when page param' do
+ let(:last_page) { project.snippets.page().total_pages }
+ let!(:project_snippet) { create(:project_snippet, :public, project: project, author: user) }
+
+ it 'redirects to last_page if page number is larger than number of pages' do
+ get :index,
+ namespace_id: project.namespace.path,
+ project_id: project.path, page: (last_page + 1).to_param
+
+ expect(response).to redirect_to(namespace_project_snippets_path(page: last_page))
+ end
+
+ it 'redirects to specified page' do
+ get :index,
+ namespace_id: project.namespace.path,
+ project_id: project.path, page: last_page.to_param
+
+ expect(assigns(:snippets).current_page).to eq(last_page)
+ expect(response).to have_http_status(200)
+ end
+ end
+
context 'when the project snippet is private' do
let!(:project_snippet) { create(:project_snippet, :private, project: project, author: user) }
@@ -47,6 +69,86 @@ describe Projects::SnippetsController do
end
end
+ describe 'POST #create' do
+ def create_snippet(project, snippet_params = {})
+ sign_in(user)
+
+ project.add_developer(user)
+
+ post :create, {
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ project_snippet: { title: 'Title', content: 'Content' }.merge(snippet_params)
+ }
+ end
+
+ context 'when the snippet is spam' do
+ before do
+ allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true)
+ end
+
+ context 'when the project is private' do
+ let(:private_project) { create(:project_empty_repo, :private) }
+
+ context 'when the snippet is public' do
+ it 'creates the snippet' do
+ expect { create_snippet(private_project, visibility_level: Snippet::PUBLIC) }.
+ to change { Snippet.count }.by(1)
+ end
+ end
+ end
+
+ context 'when the project is public' do
+ context 'when the snippet is private' do
+ it 'creates the snippet' do
+ expect { create_snippet(project, visibility_level: Snippet::PRIVATE) }.
+ to change { Snippet.count }.by(1)
+ end
+ end
+
+ context 'when the snippet is public' do
+ it 'rejects the shippet' do
+ expect { create_snippet(project, visibility_level: Snippet::PUBLIC) }.
+ not_to change { Snippet.count }
+ expect(response).to render_template(:new)
+ end
+
+ it 'creates a spam log' do
+ expect { create_snippet(project, visibility_level: Snippet::PUBLIC) }.
+ to change { SpamLog.count }.by(1)
+ end
+ end
+ end
+ end
+ end
+
+ describe 'POST #mark_as_spam' do
+ let(:snippet) { create(:project_snippet, :private, project: project, author: user) }
+
+ before do
+ allow_any_instance_of(AkismetService).to receive_messages(submit_spam: true)
+ stub_application_setting(akismet_enabled: true)
+ end
+
+ def mark_as_spam
+ admin = create(:admin)
+ create(:user_agent_detail, subject: snippet)
+ project.add_master(admin)
+ sign_in(admin)
+
+ post :mark_as_spam,
+ namespace_id: project.namespace.path,
+ project_id: project.path,
+ id: snippet.id
+ end
+
+ it 'updates the snippet' do
+ mark_as_spam
+
+ expect(snippet.reload).not_to be_submittable_as_spam
+ end
+ end
+
%w[show raw].each do |action|
describe "GET ##{action}" do
context 'when the project snippet is private' do
diff --git a/spec/controllers/projects/tags_controller_spec.rb b/spec/controllers/projects/tags_controller_spec.rb
index 5e661c2c41d..c36a5fdd66c 100644
--- a/spec/controllers/projects/tags_controller_spec.rb
+++ b/spec/controllers/projects/tags_controller_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Projects::TagsController do
- let(:project) { create(:project, :public) }
+ let(:project) { create(:project, :public, :repository) }
let!(:release) { create(:release, project: project) }
let!(:invalid_release) { create(:release, project: project, tag: 'does-not-exist') }
diff --git a/spec/controllers/projects/templates_controller_spec.rb b/spec/controllers/projects/templates_controller_spec.rb
index 19a152bcb05..80f84a388ce 100644
--- a/spec/controllers/projects/templates_controller_spec.rb
+++ b/spec/controllers/projects/templates_controller_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Projects::TemplatesController do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
let(:user2) { create(:user) }
let(:file_path_1) { '.gitlab/issue_templates/bug.md' }
@@ -14,7 +14,8 @@ describe Projects::TemplatesController do
before do
project.add_user(user, Gitlab::Access::MASTER)
- project.repository.commit_file(user, file_path_1, "something valid", "test 3", "master", false)
+ project.repository.commit_file(user, file_path_1, 'something valid',
+ message: 'test 3', branch_name: 'master', update: false)
end
describe '#show' do
diff --git a/spec/controllers/projects/tree_controller_spec.rb b/spec/controllers/projects/tree_controller_spec.rb
index 1cc050247c6..b81645a3d2d 100644
--- a/spec/controllers/projects/tree_controller_spec.rb
+++ b/spec/controllers/projects/tree_controller_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Projects::TreeController do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
before do
diff --git a/spec/controllers/projects/uploads_controller_spec.rb b/spec/controllers/projects/uploads_controller_spec.rb
index 71d0e4be834..f1c8891e87a 100644
--- a/spec/controllers/projects/uploads_controller_spec.rb
+++ b/spec/controllers/projects/uploads_controller_spec.rb
@@ -1,7 +1,7 @@
require('spec_helper')
describe Projects::UploadsController do
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project) }
let(:user) { create(:user) }
let(:jpg) { fixture_file_upload(Rails.root + 'spec/fixtures/rails_sample.jpg', 'image/jpg') }
let(:txt) { fixture_file_upload(Rails.root + 'spec/fixtures/doc_sample.txt', 'text/plain') }
diff --git a/spec/controllers/projects/variables_controller_spec.rb b/spec/controllers/projects/variables_controller_spec.rb
new file mode 100644
index 00000000000..9fa358f7d62
--- /dev/null
+++ b/spec/controllers/projects/variables_controller_spec.rb
@@ -0,0 +1,59 @@
+require 'spec_helper'
+
+describe Projects::VariablesController do
+ let(:project) { create(:empty_project) }
+ let(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+ project.team << [user, :master]
+ end
+
+ describe 'POST #create' do
+ context 'variable is valid' do
+ it 'shows a success flash message' do
+ post :create, namespace_id: project.namespace.to_param, project_id: project.to_param,
+ variable: { key: "one", value: "two" }
+
+ expect(flash[:notice]).to include 'Variables were successfully updated.'
+ expect(response).to redirect_to(namespace_project_settings_ci_cd_path(project.namespace, project))
+ end
+ end
+
+ context 'variable is invalid' do
+ it 'shows an alert flash message' do
+ post :create, namespace_id: project.namespace.to_param, project_id: project.to_param,
+ variable: { key: "..one", value: "two" }
+
+ expect(response).to render_template("projects/variables/show")
+ end
+ end
+ end
+
+ describe 'POST #update' do
+ let(:variable) { create(:ci_variable) }
+
+ context 'updating a variable with valid characters' do
+ before do
+ variable.gl_project_id = project.id
+ project.variables << variable
+ end
+
+ it 'shows a success flash message' do
+ post :update, namespace_id: project.namespace.to_param, project_id: project.to_param,
+ id: variable.id, variable: { key: variable.key, value: 'two' }
+
+ expect(flash[:notice]).to include 'Variable was successfully updated.'
+ expect(response).to redirect_to(namespace_project_variables_path(project.namespace, project))
+ end
+
+ it 'renders the action #show if the variable key is invalid' do
+ post :update, namespace_id: project.namespace.to_param, project_id: project.to_param,
+ id: variable.id, variable: { key: '?', value: variable.value }
+
+ expect(response).to have_http_status(200)
+ expect(response).to render_template :show
+ end
+ end
+ end
+end
diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb
index 5ddcaa60dc6..e7aa8745b99 100644
--- a/spec/controllers/projects_controller_spec.rb
+++ b/spec/controllers/projects_controller_spec.rb
@@ -1,11 +1,11 @@
require('spec_helper')
describe ProjectsController do
- let(:project) { create(:project) }
- let(:public_project) { create(:project, :public) }
- let(:user) { create(:user) }
- let(:jpg) { fixture_file_upload(Rails.root + 'spec/fixtures/rails_sample.jpg', 'image/jpg') }
- let(:txt) { fixture_file_upload(Rails.root + 'spec/fixtures/doc_sample.txt', 'text/plain') }
+ let(:project) { create(:empty_project) }
+ let(:public_project) { create(:empty_project, :public) }
+ let(:user) { create(:user) }
+ let(:jpg) { fixture_file_upload(Rails.root + 'spec/fixtures/rails_sample.jpg', 'image/jpg') }
+ let(:txt) { fixture_file_upload(Rails.root + 'spec/fixtures/doc_sample.txt', 'text/plain') }
describe 'GET index' do
context 'as a user' do
@@ -32,7 +32,7 @@ describe ProjectsController do
before { sign_in(user) }
context "user does not have access to project" do
- let(:private_project) { create(:project, :private) }
+ let(:private_project) { create(:empty_project, :private) }
it "does not initialize notification setting" do
get :show, namespace_id: private_project.namespace.path, id: private_project.path
@@ -146,6 +146,8 @@ describe ProjectsController do
end
context "rendering default project view" do
+ let(:public_project) { create(:project, :public, :repository) }
+
render_views
it "renders the activity view" do
@@ -190,25 +192,11 @@ describe ProjectsController do
expect(assigns(:project)).to eq(public_project)
expect(response).to redirect_to("/#{public_project.path_with_namespace}")
end
-
- # MySQL queries are case insensitive by default, so this spec would fail.
- if Gitlab::Database.postgresql?
- context "when there is also a match with the same casing" do
- let!(:other_project) { create(:project, :public, namespace: public_project.namespace, path: public_project.path.upcase) }
-
- it "loads the exactly matched project" do
- get :show, namespace_id: public_project.namespace.path, id: public_project.path.upcase
-
- expect(assigns(:project)).to eq(other_project)
- expect(response).to have_http_status(200)
- end
- end
- end
end
end
context "when the url contains .atom" do
- let(:public_project_with_dot_atom) { build(:project, :public, name: 'my.atom', path: 'my.atom') }
+ let(:public_project_with_dot_atom) { build(:empty_project, :public, name: 'my.atom', path: 'my.atom') }
it 'expects an error creating the project' do
expect(public_project_with_dot_atom).not_to be_valid
@@ -217,7 +205,7 @@ describe ProjectsController do
context 'when the project is pending deletions' do
it 'renders a 404 error' do
- project = create(:project, pending_delete: true)
+ project = create(:empty_project, pending_delete: true)
sign_in(user)
get :show, namespace_id: project.namespace.path, id: project.path
@@ -225,6 +213,17 @@ describe ProjectsController do
expect(response.status).to eq 404
end
end
+
+ context "redirection from http://someproject.git" do
+ it 'redirects to project page (format.html)' do
+ project = create(:project, :public)
+
+ get :show, namespace_id: project.namespace.path, id: project.path, format: :git
+
+ expect(response).to have_http_status(302)
+ expect(response).to redirect_to(namespace_project_path)
+ end
+ end
end
describe "#update" do
@@ -233,6 +232,7 @@ describe ProjectsController do
let(:admin) { create(:admin) }
it "sets the repository to the right path after a rename" do
+ project = create(:project, :repository)
new_path = 'renamed_path'
project_params = { path: new_path }
controller.instance_variable_set(:@project, project)
@@ -245,7 +245,7 @@ describe ProjectsController do
expect(project.repository.path).to include(new_path)
expect(assigns(:repository).path).to eq(project.repository.path)
- expect(response).to have_http_status(200)
+ expect(response).to have_http_status(302)
end
end
@@ -384,6 +384,8 @@ describe ProjectsController do
end
describe "GET refs" do
+ let(:public_project) { create(:project, :public) }
+
it "gets a list of branches and tags" do
get :refs, namespace_id: public_project.namespace.path, id: public_project.path
diff --git a/spec/controllers/registrations_controller_spec.rb b/spec/controllers/registrations_controller_spec.rb
index 026f41c926b..8cc216445eb 100644
--- a/spec/controllers/registrations_controller_spec.rb
+++ b/spec/controllers/registrations_controller_spec.rb
@@ -2,30 +2,60 @@ require 'spec_helper'
describe RegistrationsController do
describe '#create' do
- around(:each) do |example|
- perform_enqueued_jobs do
- example.run
+ let(:user_params) { { user: { name: 'new_user', username: 'new_username', email: 'new@user.com', password: 'Any_password' } } }
+
+ context 'email confirmation' do
+ around(:each) do |example|
+ perform_enqueued_jobs do
+ example.run
+ end
end
- end
- let(:user_params) { { user: { name: "new_user", username: "new_username", email: "new@user.com", password: "Any_password" } } }
+ context 'when send_user_confirmation_email is false' do
+ it 'signs the user in' do
+ allow_any_instance_of(ApplicationSetting).to receive(:send_user_confirmation_email).and_return(false)
+
+ expect { post(:create, user_params) }.not_to change{ ActionMailer::Base.deliveries.size }
+ expect(subject.current_user).not_to be_nil
+ end
+ end
- context 'when sending email confirmation' do
- before { allow_any_instance_of(ApplicationSetting).to receive(:send_user_confirmation_email).and_return(false) }
+ context 'when send_user_confirmation_email is true' do
+ it 'does not authenticate user and sends confirmation email' do
+ allow_any_instance_of(ApplicationSetting).to receive(:send_user_confirmation_email).and_return(true)
- it 'logs user in directly' do
- expect { post(:create, user_params) }.not_to change{ ActionMailer::Base.deliveries.size }
- expect(subject.current_user).not_to be_nil
+ post(:create, user_params)
+
+ expect(ActionMailer::Base.deliveries.last.to.first).to eq(user_params[:user][:email])
+ expect(subject.current_user).to be_nil
+ end
end
end
- context 'when not sending email confirmation' do
- before { allow_any_instance_of(ApplicationSetting).to receive(:send_user_confirmation_email).and_return(true) }
+ context 'when reCAPTCHA is enabled' do
+ before do
+ stub_application_setting(recaptcha_enabled: true)
+ end
+
+ it 'displays an error when the reCAPTCHA is not solved' do
+ # Without this, `verify_recaptcha` arbitraily returns true in test env
+ Recaptcha.configuration.skip_verify_env.delete('test')
- it 'does not authenticate user and sends confirmation email' do
post(:create, user_params)
- expect(ActionMailer::Base.deliveries.last.to.first).to eq(user_params[:user][:email])
- expect(subject.current_user).to be_nil
+
+ expect(response).to render_template(:new)
+ expect(flash[:alert]).to include 'There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.'
+ end
+
+ it 'redirects to the dashboard when the recaptcha is solved' do
+ # Avoid test ordering issue and ensure `verify_recaptcha` returns true
+ unless Recaptcha.configuration.skip_verify_env.include?('test')
+ Recaptcha.configuration.skip_verify_env << 'test'
+ end
+
+ post(:create, user_params)
+
+ expect(flash[:notice]).to include 'Welcome! You have signed up successfully.'
end
end
end
diff --git a/spec/controllers/search_controller_spec.rb b/spec/controllers/search_controller_spec.rb
index b7bb9290712..3173aae664c 100644
--- a/spec/controllers/search_controller_spec.rb
+++ b/spec/controllers/search_controller_spec.rb
@@ -2,7 +2,6 @@ require 'spec_helper'
describe SearchController do
let(:user) { create(:user) }
- let(:project) { create(:empty_project, :public) }
before do
sign_in(user)
@@ -22,7 +21,7 @@ describe SearchController do
before { sign_out(user) }
it "doesn't expose comments on issues" do
- project = create(:empty_project, :public, issues_access_level: ProjectFeature::PRIVATE)
+ project = create(:empty_project, :public, :issues_private)
note = create(:note_on_issue, project: project)
get :show, project_id: project.id, scope: 'notes', search: note.note
@@ -31,17 +30,8 @@ describe SearchController do
end
end
- it "doesn't expose comments on issues" do
- project = create(:empty_project, :public, issues_access_level: ProjectFeature::PRIVATE)
- note = create(:note_on_issue, project: project)
-
- get :show, project_id: project.id, scope: 'notes', search: note.note
-
- expect(assigns[:search_objects].count).to eq(0)
- end
-
it "doesn't expose comments on merge_requests" do
- project = create(:empty_project, :public, merge_requests_access_level: ProjectFeature::PRIVATE)
+ project = create(:empty_project, :public, :merge_requests_private)
note = create(:note_on_merge_request, project: project)
get :show, project_id: project.id, scope: 'notes', search: note.note
@@ -50,7 +40,7 @@ describe SearchController do
end
it "doesn't expose comments on snippets" do
- project = create(:empty_project, :public, snippets_access_level: ProjectFeature::PRIVATE)
+ project = create(:empty_project, :public, :snippets_private)
note = create(:note_on_project_snippet, project: project)
get :show, project_id: project.id, scope: 'notes', search: note.note
diff --git a/spec/controllers/snippets_controller_spec.rb b/spec/controllers/snippets_controller_spec.rb
index d76fe9f580f..dadcb90cfc2 100644
--- a/spec/controllers/snippets_controller_spec.rb
+++ b/spec/controllers/snippets_controller_spec.rb
@@ -138,6 +138,65 @@ describe SnippetsController do
end
end
+ describe 'POST #create' do
+ def create_snippet(snippet_params = {})
+ sign_in(user)
+
+ post :create, {
+ personal_snippet: { title: 'Title', content: 'Content' }.merge(snippet_params)
+ }
+ end
+
+ context 'when the snippet is spam' do
+ before do
+ allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true)
+ end
+
+ context 'when the snippet is private' do
+ it 'creates the snippet' do
+ expect { create_snippet(visibility_level: Snippet::PRIVATE) }.
+ to change { Snippet.count }.by(1)
+ end
+ end
+
+ context 'when the snippet is public' do
+ it 'rejects the shippet' do
+ expect { create_snippet(visibility_level: Snippet::PUBLIC) }.
+ not_to change { Snippet.count }
+ expect(response).to render_template(:new)
+ end
+
+ it 'creates a spam log' do
+ expect { create_snippet(visibility_level: Snippet::PUBLIC) }.
+ to change { SpamLog.count }.by(1)
+ end
+ end
+ end
+ end
+
+ describe 'POST #mark_as_spam' do
+ let(:snippet) { create(:personal_snippet, :public, author: user) }
+
+ before do
+ allow_any_instance_of(AkismetService).to receive_messages(submit_spam: true)
+ stub_application_setting(akismet_enabled: true)
+ end
+
+ def mark_as_spam
+ admin = create(:admin)
+ create(:user_agent_detail, subject: snippet)
+ sign_in(admin)
+
+ post :mark_as_spam, id: snippet.id
+ end
+
+ it 'updates the snippet' do
+ mark_as_spam
+
+ expect(snippet.reload).not_to be_submittable_as_spam
+ end
+ end
+
%w(raw download).each do |action|
describe "GET #{action}" do
context 'when the personal snippet is private' do
diff --git a/spec/controllers/uploads_controller_spec.rb b/spec/controllers/uploads_controller_spec.rb
index 69124ab06bf..570d9fa43f8 100644
--- a/spec/controllers/uploads_controller_spec.rb
+++ b/spec/controllers/uploads_controller_spec.rb
@@ -41,7 +41,7 @@ describe UploadsController do
end
context "when viewing a project avatar" do
- let!(:project) { create(:project, avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png")) }
+ let!(:project) { create(:empty_project, avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png")) }
context "when the project is public" do
before do
diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb
index 19a8b1fe524..bbe9aaf737f 100644
--- a/spec/controllers/users_controller_spec.rb
+++ b/spec/controllers/users_controller_spec.rb
@@ -73,7 +73,7 @@ describe UsersController do
end
context 'forked project' do
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project) }
let(:forked_project) { Projects::ForkService.new(project, user).execute }
before do
@@ -91,7 +91,7 @@ describe UsersController do
end
describe 'GET #calendar_activities' do
- let!(:project) { create(:project) }
+ let!(:project) { create(:empty_project) }
let!(:user) { create(:user) }
before do
diff --git a/spec/db/production/settings.rb b/spec/db/production/settings.rb
index a7c5283df94..007b35bbb77 100644
--- a/spec/db/production/settings.rb
+++ b/spec/db/production/settings.rb
@@ -2,10 +2,11 @@ require 'spec_helper'
require 'rainbow/ext/string'
describe 'seed production settings', lib: true do
+ include StubENV
+
context 'GITLAB_SHARED_RUNNERS_REGISTRATION_TOKEN is set in the environment' do
before do
- allow(ENV).to receive(:[]).and_call_original
- allow(ENV).to receive(:[]).with('GITLAB_SHARED_RUNNERS_REGISTRATION_TOKEN').and_return('013456789')
+ stub_env('GITLAB_SHARED_RUNNERS_REGISTRATION_TOKEN', '013456789')
end
it 'writes the token to the database' do
diff --git a/spec/factories/boards.rb b/spec/factories/boards.rb
index ec46146d9b5..a581725245a 100644
--- a/spec/factories/boards.rb
+++ b/spec/factories/boards.rb
@@ -3,7 +3,6 @@ FactoryGirl.define do
project factory: :empty_project
after(:create) do |board|
- board.lists.create(list_type: :backlog)
board.lists.create(list_type: :done)
end
end
diff --git a/spec/factories/ci/pipelines.rb b/spec/factories/ci/pipelines.rb
index 1735791f644..77404f46c92 100644
--- a/spec/factories/ci/pipelines.rb
+++ b/spec/factories/ci/pipelines.rb
@@ -31,6 +31,14 @@ FactoryGirl.define do
File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml'))
end
end
+
+ # Populates pipeline with errors
+ #
+ pipeline.config_processor if evaluator.config
+ end
+
+ trait :invalid do
+ config(rspec: nil)
end
end
end
diff --git a/spec/factories/ci/runners.rb b/spec/factories/ci/runners.rb
index e3b73e29987..c3b4aff55ba 100644
--- a/spec/factories/ci/runners.rb
+++ b/spec/factories/ci/runners.rb
@@ -8,10 +8,18 @@ FactoryGirl.define do
is_shared false
active true
+ trait :online do
+ contacted_at Time.now
+ end
+
trait :shared do
is_shared true
end
+ trait :specific do
+ is_shared false
+ end
+
trait :inactive do
active false
end
diff --git a/spec/factories/ci/stages.rb b/spec/factories/ci/stages.rb
index ee3b17b8bf1..7f557b25ccb 100644
--- a/spec/factories/ci/stages.rb
+++ b/spec/factories/ci/stages.rb
@@ -3,11 +3,12 @@ FactoryGirl.define do
transient do
name 'test'
status nil
+ warnings nil
pipeline factory: :ci_empty_pipeline
end
initialize_with do
- Ci::Stage.new(pipeline, name: name, status: status)
+ Ci::Stage.new(pipeline, name: name, status: status, warnings: warnings)
end
end
end
diff --git a/spec/factories/deploy_keys_projects.rb b/spec/factories/deploy_keys_projects.rb
index 27cece487bd..75f8982ecd9 100644
--- a/spec/factories/deploy_keys_projects.rb
+++ b/spec/factories/deploy_keys_projects.rb
@@ -1,6 +1,6 @@
FactoryGirl.define do
factory :deploy_keys_project do
deploy_key
- project
+ project factory: :empty_project
end
end
diff --git a/spec/factories/events.rb b/spec/factories/events.rb
index 8820d527c61..55727d6b62c 100644
--- a/spec/factories/events.rb
+++ b/spec/factories/events.rb
@@ -1,8 +1,20 @@
FactoryGirl.define do
factory :event do
- project
+ project factory: :empty_project
author factory: :user
+ trait(:created) { action Event::CREATED }
+ trait(:updated) { action Event::UPDATED }
+ trait(:closed) { action Event::CLOSED }
+ trait(:reopened) { action Event::REOPENED }
+ trait(:pushed) { action Event::PUSHED }
+ trait(:commented) { action Event::COMMENTED }
+ trait(:merged) { action Event::MERGED }
+ trait(:joined) { action Event::JOINED }
+ trait(:left) { action Event::LEFT }
+ trait(:destroyed) { action Event::DESTROYED }
+ trait(:expired) { action Event::EXPIRED }
+
factory :closed_issue_event do
action { Event::CLOSED }
target factory: :closed_issue
diff --git a/spec/factories/file_uploader.rb b/spec/factories/file_uploader.rb
index 1b36e21f2b0..bc74aeecc3b 100644
--- a/spec/factories/file_uploader.rb
+++ b/spec/factories/file_uploader.rb
@@ -1,6 +1,6 @@
FactoryGirl.define do
factory :file_uploader do
- project
+ project factory: :empty_project
secret nil
transient do
diff --git a/spec/factories/groups.rb b/spec/factories/groups.rb
index ece6beb9fa9..86f51ffca99 100644
--- a/spec/factories/groups.rb
+++ b/spec/factories/groups.rb
@@ -1,8 +1,9 @@
FactoryGirl.define do
- factory :group do
+ factory :group, class: Group, parent: :namespace do
sequence(:name) { |n| "group#{n}" }
path { name.downcase.gsub(/\s/, '_') }
type 'Group'
+ owner nil
trait :public do
visibility_level Gitlab::VisibilityLevel::PUBLIC
diff --git a/spec/factories/issues.rb b/spec/factories/issues.rb
index 2b4670be468..7e09f1ba8ea 100644
--- a/spec/factories/issues.rb
+++ b/spec/factories/issues.rb
@@ -6,7 +6,7 @@ FactoryGirl.define do
factory :issue do
title
author
- project
+ project factory: :empty_project
trait :confidential do
confidential true
diff --git a/spec/factories/labels.rb b/spec/factories/labels.rb
index 3e8822faf97..5ba8443c62c 100644
--- a/spec/factories/labels.rb
+++ b/spec/factories/labels.rb
@@ -2,7 +2,7 @@ FactoryGirl.define do
factory :label, class: ProjectLabel do
sequence(:title) { |n| "label#{n}" }
color "#990000"
- project
+ project factory: :empty_project
transient do
priority nil
diff --git a/spec/factories/lfs_objects.rb b/spec/factories/lfs_objects.rb
index a81645acd2b..477fab9e964 100644
--- a/spec/factories/lfs_objects.rb
+++ b/spec/factories/lfs_objects.rb
@@ -2,7 +2,7 @@ include ActionDispatch::TestProcess
FactoryGirl.define do
factory :lfs_object do
- oid "b68143e6463773b1b6c6fd009a76c32aeec041faff32ba2ed42fd7f708a17f80"
+ sequence(:oid) { |n| "b68143e6463773b1b6c6fd009a76c32aeec041faff32ba2ed42fd7f708a%05x" % n }
size 499013
end
diff --git a/spec/factories/lists.rb b/spec/factories/lists.rb
index 9e3f06c682c..2a2f3cca91c 100644
--- a/spec/factories/lists.rb
+++ b/spec/factories/lists.rb
@@ -6,12 +6,6 @@ FactoryGirl.define do
sequence(:position)
end
- factory :backlog_list, parent: :list do
- list_type :backlog
- label nil
- position nil
- end
-
factory :done_list, parent: :list do
list_type :done
label nil
diff --git a/spec/factories/merge_requests.rb b/spec/factories/merge_requests.rb
index 37eb49c94df..22f84150bb3 100644
--- a/spec/factories/merge_requests.rb
+++ b/spec/factories/merge_requests.rb
@@ -2,7 +2,7 @@ FactoryGirl.define do
factory :merge_request do
title
author
- source_project factory: :project
+ association :source_project, :repository, factory: :project
target_project { source_project }
# $ git log --pretty=oneline feature..master
diff --git a/spec/factories/milestones.rb b/spec/factories/milestones.rb
index 84da71ed6dc..841ab3c73b8 100644
--- a/spec/factories/milestones.rb
+++ b/spec/factories/milestones.rb
@@ -1,7 +1,7 @@
FactoryGirl.define do
factory :milestone do
title
- project
+ project factory: :empty_project
trait :active do
state "active"
diff --git a/spec/factories/notes.rb b/spec/factories/notes.rb
index a10ba629760..a21da7074f9 100644
--- a/spec/factories/notes.rb
+++ b/spec/factories/notes.rb
@@ -4,7 +4,7 @@ include ActionDispatch::TestProcess
FactoryGirl.define do
factory :note do
- project
+ project factory: :empty_project
note "Note"
author
on_issue
@@ -13,12 +13,19 @@ FactoryGirl.define do
factory :note_on_issue, traits: [:on_issue], aliases: [:votable_note]
factory :note_on_merge_request, traits: [:on_merge_request]
factory :note_on_project_snippet, traits: [:on_project_snippet]
+ factory :note_on_personal_snippet, traits: [:on_personal_snippet]
factory :system_note, traits: [:system]
- factory :legacy_diff_note_on_commit, traits: [:on_commit, :legacy_diff_note], class: LegacyDiffNote
- factory :legacy_diff_note_on_merge_request, traits: [:on_merge_request, :legacy_diff_note], class: LegacyDiffNote
+ factory :legacy_diff_note_on_commit, traits: [:on_commit, :legacy_diff_note], class: LegacyDiffNote do
+ association :project, :repository
+ end
+
+ factory :legacy_diff_note_on_merge_request, traits: [:on_merge_request, :legacy_diff_note], class: LegacyDiffNote do
+ association :project, :repository
+ end
factory :diff_note_on_merge_request, traits: [:on_merge_request], class: DiffNote do
+ association :project, :repository
position do
Gitlab::Diff::Position.new(
old_path: "files/ruby/popen.rb",
@@ -36,6 +43,7 @@ FactoryGirl.define do
end
factory :diff_note_on_commit, traits: [:on_commit], class: DiffNote do
+ association :project, :repository
position do
Gitlab::Diff::Position.new(
old_path: "files/ruby/popen.rb",
@@ -48,6 +56,7 @@ FactoryGirl.define do
end
trait :on_commit do
+ association :project, :repository
noteable nil
noteable_type 'Commit'
noteable_id nil
@@ -70,6 +79,11 @@ FactoryGirl.define do
noteable { create(:project_snippet, project: project) }
end
+ trait :on_personal_snippet do
+ noteable { create(:personal_snippet) }
+ project nil
+ end
+
trait :system do
system true
end
diff --git a/spec/factories/pages_domains.rb b/spec/factories/pages_domains.rb
new file mode 100644
index 00000000000..6d2e45f41ba
--- /dev/null
+++ b/spec/factories/pages_domains.rb
@@ -0,0 +1,153 @@
+FactoryGirl.define do
+ factory :pages_domain, class: 'PagesDomain' do
+ domain 'my.domain.com'
+
+ trait :with_certificate do
+ certificate '-----BEGIN CERTIFICATE-----
+MIICGzCCAYSgAwIBAgIBATANBgkqhkiG9w0BAQUFADAbMRkwFwYDVQQDExB0ZXN0
+LWNlcnRpZmljYXRlMB4XDTE2MDIxMjE0MzIwMFoXDTIwMDQxMjE0MzIwMFowGzEZ
+MBcGA1UEAxMQdGVzdC1jZXJ0aWZpY2F0ZTCBnzANBgkqhkiG9w0BAQEFAAOBjQAw
+gYkCgYEApL4J9L0ZxFJ1hI1LPIflAlAGvm6ZEvoT4qKU5Xf2JgU7/2geNR1qlNFa
+SvCc08Knupp5yTgmvyK/Xi09U0N82vvp4Zvr/diSc4A/RA6Mta6egLySNT438kdT
+nY2tR5feoTLwQpX0t4IMlwGQGT5h6Of2fKmDxzuwuyffcIHqLdsCAwEAAaNvMG0w
+DAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUxl9WSxBprB0z0ibJs3rXEk0+95AwCwYD
+VR0PBAQDAgXgMBEGCWCGSAGG+EIBAQQEAwIGQDAeBglghkgBhvhCAQ0EERYPeGNh
+IGNlcnRpZmljYXRlMA0GCSqGSIb3DQEBBQUAA4GBAGC4T8SlFHK0yPSa+idGLQFQ
+joZp2JHYvNlTPkRJ/J4TcXxBTJmArcQgTIuNoBtC+0A/SwdK4MfTCUY4vNWNdese
+5A4K65Nb7Oh1AdQieTBHNXXCdyFsva9/ScfQGEl7p55a52jOPs0StPd7g64uvjlg
+YHi2yesCrOvVXt+lgPTd
+-----END CERTIFICATE-----'
+ end
+
+ trait :with_key do
+ key '-----BEGIN PRIVATE KEY-----
+MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAKS+CfS9GcRSdYSN
+SzyH5QJQBr5umRL6E+KilOV39iYFO/9oHjUdapTRWkrwnNPCp7qaeck4Jr8iv14t
+PVNDfNr76eGb6/3YknOAP0QOjLWunoC8kjU+N/JHU52NrUeX3qEy8EKV9LeCDJcB
+kBk+Yejn9nypg8c7sLsn33CB6i3bAgMBAAECgYA2D26w80T7WZvazYr86BNMePpd
+j2mIAqx32KZHzt/lhh40J/SRtX9+Kl0Y7nBoRR5Ja9u/HkAIxNxLiUjwg9r6cpg/
+uITEF5nMt7lAk391BuI+7VOZZGbJDsq2ulPd6lO+C8Kq/PI/e4kXcIjeH6KwQsuR
+5vrXfBZ3sQfflaiN4QJBANBt8JY2LIGQF8o89qwUpRL5vbnKQ4IzZ5+TOl4RLR7O
+AQpJ81tGuINghO7aunctb6rrcKJrxmEH1whzComybrMCQQDKV49nOBudRBAIgG4K
+EnLzsRKISUHMZSJiYTYnablof8cKw1JaQduw7zgrUlLwnroSaAGX88+Jw1f5n2Lh
+Vlg5AkBDdUGnrDLtYBCDEQYZHblrkc7ZAeCllDOWjxUV+uMqlCv8A4Ey6omvY57C
+m6I8DkWVAQx8VPtozhvHjUw80rZHAkB55HWHAM3h13axKG0htCt7klhPsZHpx6MH
+EPjGlXIT+aW2XiPmK3ZlCDcWIenE+lmtbOpI159Wpk8BGXs/s/xBAkEAlAY3ymgx
+63BDJEwvOb2IaP8lDDxNsXx9XJNVvQbv5n15vNsLHbjslHfAhAbxnLQ1fLhUPqSi
+nNp/xedE1YxutQ==
+-----END PRIVATE KEY-----'
+ end
+
+ trait :with_missing_chain do
+ # This certificate is signed with different key
+ # And misses the CA to build trust chain
+ certificate '-----BEGIN CERTIFICATE-----
+MIIDGTCCAgGgAwIBAgIBAjANBgkqhkiG9w0BAQUFADASMRAwDgYDVQQDEwdUZXN0
+IENBMB4XDTE2MDIxMjE0MjMwMFoXDTE3MDIxMTE0MjMwMFowHTEbMBkGA1UEAxMS
+dGVzdC1jZXJ0aWZpY2F0ZS0yMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
+AQEAw8RWetIUT0YymSuKvBpClzDv/jQdX0Ch+2iF7f4Lm3lcmoUuXgyhl/WRe5K9
+ONuMHPQlZbeavEbvWb0BsU7geInhsjd/zAu3EP17jfSIXToUdSD20wcSG/yclLdZ
+qhb6NCtHTJKFUI8BktoS7kafkdvmeem/UJFzlvcA6VMyGDkS8ZN39a45R1jGmPEl
+Yk0g1jW7lSKcBLjU1O/Csv59LyWXqBP6jR1vB8ijlUf1IyK8gOk7NHF13GHl7Z3A
+/8zwuEt/pB3yK92o71P+FnSEcJ23zcAalz6H9ajVTzRr/AXttineBNVYnEuPXW+V
+Rsboe+bBO/e4pVKXnQ1F3aMT7QIDAQABo28wbTAMBgNVHRMBAf8EAjAAMB0GA1Ud
+DgQWBBSFwo3rhc26lD8ZVaBVcUY1NyCOLDALBgNVHQ8EBAMCBeAwEQYJYIZIAYb4
+QgEBBAQDAgZAMB4GCWCGSAGG+EIBDQQRFg94Y2EgY2VydGlmaWNhdGUwDQYJKoZI
+hvcNAQEFBQADggEBABppUhunuT7qArM9gZ2gLgcOK8qyZWU8AJulvloaCZDvqGVs
+Qom0iEMBrrt5+8bBevNiB49Tz7ok8NFgLzrlEnOw6y6QGjiI/g8sRKEiXl+ZNX8h
+s8VN6arqT348OU8h2BixaXDmBF/IqZVApGhR8+B4fkCt0VQmdzVuHGbOQXMWJCpl
+WlU8raZoPIqf6H/8JA97pM/nk/3CqCoHsouSQv+jGY4pSL22RqsO0ylIM0LDBbmF
+m4AEaojTljX1tMJAF9Rbiw/omam5bDPq2JWtosrz/zB69y5FaQjc6FnCk0M4oN/+
+VM+d42lQAgoq318A84Xu5vRh1KCAJuztkhNbM+w=
+-----END CERTIFICATE-----'
+ end
+
+ trait :with_trusted_chain do
+ # This contains
+ # [Intermediate #2 (SHA-2)] 'Comodo RSA Domain Validation Secure Server CA'
+ # [Intermediate #1 (SHA-2)] 'COMODO RSA Certification Authority'
+ certificate '-----BEGIN CERTIFICATE-----
+MIIGCDCCA/CgAwIBAgIQKy5u6tl1NmwUim7bo3yMBzANBgkqhkiG9w0BAQwFADCB
+hTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G
+A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNV
+BAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTQwMjEy
+MDAwMDAwWhcNMjkwMjExMjM1OTU5WjCBkDELMAkGA1UEBhMCR0IxGzAZBgNVBAgT
+EkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMR
+Q09NT0RPIENBIExpbWl0ZWQxNjA0BgNVBAMTLUNPTU9ETyBSU0EgRG9tYWluIFZh
+bGlkYXRpb24gU2VjdXJlIFNlcnZlciBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEP
+ADCCAQoCggEBAI7CAhnhoFmk6zg1jSz9AdDTScBkxwtiBUUWOqigwAwCfx3M28Sh
+bXcDow+G+eMGnD4LgYqbSRutA776S9uMIO3Vzl5ljj4Nr0zCsLdFXlIvNN5IJGS0
+Qa4Al/e+Z96e0HqnU4A7fK31llVvl0cKfIWLIpeNs4TgllfQcBhglo/uLQeTnaG6
+ytHNe+nEKpooIZFNb5JPJaXyejXdJtxGpdCsWTWM/06RQ1A/WZMebFEh7lgUq/51
+UHg+TLAchhP6a5i84DuUHoVS3AOTJBhuyydRReZw3iVDpA3hSqXttn7IzW3uLh0n
+c13cRTCAquOyQQuvvUSH2rnlG51/ruWFgqUCAwEAAaOCAWUwggFhMB8GA1UdIwQY
+MBaAFLuvfgI9+qbxPISOre44mOzZMjLUMB0GA1UdDgQWBBSQr2o6lFoL2JDqElZz
+30O0Oija5zAOBgNVHQ8BAf8EBAMCAYYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNV
+HSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwGwYDVR0gBBQwEjAGBgRVHSAAMAgG
+BmeBDAECATBMBgNVHR8ERTBDMEGgP6A9hjtodHRwOi8vY3JsLmNvbW9kb2NhLmNv
+bS9DT01PRE9SU0FDZXJ0aWZpY2F0aW9uQXV0aG9yaXR5LmNybDBxBggrBgEFBQcB
+AQRlMGMwOwYIKwYBBQUHMAKGL2h0dHA6Ly9jcnQuY29tb2RvY2EuY29tL0NPTU9E
+T1JTQUFkZFRydXN0Q0EuY3J0MCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5jb21v
+ZG9jYS5jb20wDQYJKoZIhvcNAQEMBQADggIBAE4rdk+SHGI2ibp3wScF9BzWRJ2p
+mj6q1WZmAT7qSeaiNbz69t2Vjpk1mA42GHWx3d1Qcnyu3HeIzg/3kCDKo2cuH1Z/
+e+FE6kKVxF0NAVBGFfKBiVlsit2M8RKhjTpCipj4SzR7JzsItG8kO3KdY3RYPBps
+P0/HEZrIqPW1N+8QRcZs2eBelSaz662jue5/DJpmNXMyYE7l3YphLG5SEXdoltMY
+dVEVABt0iN3hxzgEQyjpFv3ZBdRdRydg1vs4O2xyopT4Qhrf7W8GjEXCBgCq5Ojc
+2bXhc3js9iPc0d1sjhqPpepUfJa3w/5Vjo1JXvxku88+vZbrac2/4EjxYoIQ5QxG
+V/Iz2tDIY+3GH5QFlkoakdH368+PUq4NCNk+qKBR6cGHdNXJ93SrLlP7u3r7l+L4
+HyaPs9Kg4DdbKDsx5Q5XLVq4rXmsXiBmGqW5prU5wfWYQ//u+aen/e7KJD2AFsQX
+j4rBYKEMrltDR5FL1ZoXX/nUh8HCjLfn4g8wGTeGrODcQgPmlKidrv0PJFGUzpII
+0fxQ8ANAe4hZ7Q7drNJ3gjTcBpUC2JD5Leo31Rpg0Gcg19hCC0Wvgmje3WYkN5Ap
+lBlGGSW4gNfL1IYoakRwJiNiqZ+Gb7+6kHDSVneFeO/qJakXzlByjAA6quPbYzSf
++AZxAeKCINT+b72x
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIFdDCCBFygAwIBAgIQJ2buVutJ846r13Ci/ITeIjANBgkqhkiG9w0BAQwFADBv
+MQswCQYDVQQGEwJTRTEUMBIGA1UEChMLQWRkVHJ1c3QgQUIxJjAkBgNVBAsTHUFk
+ZFRydXN0IEV4dGVybmFsIFRUUCBOZXR3b3JrMSIwIAYDVQQDExlBZGRUcnVzdCBF
+eHRlcm5hbCBDQSBSb290MB4XDTAwMDUzMDEwNDgzOFoXDTIwMDUzMDEwNDgzOFow
+gYUxCzAJBgNVBAYTAkdCMRswGQYDVQQIExJHcmVhdGVyIE1hbmNoZXN0ZXIxEDAO
+BgNVBAcTB1NhbGZvcmQxGjAYBgNVBAoTEUNPTU9ETyBDQSBMaW1pdGVkMSswKQYD
+VQQDEyJDT01PRE8gUlNBIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIICIjANBgkq
+hkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAkehUktIKVrGsDSTdxc9EZ3SZKzejfSNw
+AHG8U9/E+ioSj0t/EFa9n3Byt2F/yUsPF6c947AEYe7/EZfH9IY+Cvo+XPmT5jR6
+2RRr55yzhaCCenavcZDX7P0N+pxs+t+wgvQUfvm+xKYvT3+Zf7X8Z0NyvQwA1onr
+ayzT7Y+YHBSrfuXjbvzYqOSSJNpDa2K4Vf3qwbxstovzDo2a5JtsaZn4eEgwRdWt
+4Q08RWD8MpZRJ7xnw8outmvqRsfHIKCxH2XeSAi6pE6p8oNGN4Tr6MyBSENnTnIq
+m1y9TBsoilwie7SrmNnu4FGDwwlGTm0+mfqVF9p8M1dBPI1R7Qu2XK8sYxrfV8g/
+vOldxJuvRZnio1oktLqpVj3Pb6r/SVi+8Kj/9Lit6Tf7urj0Czr56ENCHonYhMsT
+8dm74YlguIwoVqwUHZwK53Hrzw7dPamWoUi9PPevtQ0iTMARgexWO/bTouJbt7IE
+IlKVgJNp6I5MZfGRAy1wdALqi2cVKWlSArvX31BqVUa/oKMoYX9w0MOiqiwhqkfO
+KJwGRXa/ghgntNWutMtQ5mv0TIZxMOmm3xaG4Nj/QN370EKIf6MzOi5cHkERgWPO
+GHFrK+ymircxXDpqR+DDeVnWIBqv8mqYqnK8V0rSS527EPywTEHl7R09XiidnMy/
+s1Hap0flhFMCAwEAAaOB9DCB8TAfBgNVHSMEGDAWgBStvZh6NLQm9/rEJlTvA73g
+JMtUGjAdBgNVHQ4EFgQUu69+Aj36pvE8hI6t7jiY7NkyMtQwDgYDVR0PAQH/BAQD
+AgGGMA8GA1UdEwEB/wQFMAMBAf8wEQYDVR0gBAowCDAGBgRVHSAAMEQGA1UdHwQ9
+MDswOaA3oDWGM2h0dHA6Ly9jcmwudXNlcnRydXN0LmNvbS9BZGRUcnVzdEV4dGVy
+bmFsQ0FSb290LmNybDA1BggrBgEFBQcBAQQpMCcwJQYIKwYBBQUHMAGGGWh0dHA6
+Ly9vY3NwLnVzZXJ0cnVzdC5jb20wDQYJKoZIhvcNAQEMBQADggEBAGS/g/FfmoXQ
+zbihKVcN6Fr30ek+8nYEbvFScLsePP9NDXRqzIGCJdPDoCpdTPW6i6FtxFQJdcfj
+Jw5dhHk3QBN39bSsHNA7qxcS1u80GH4r6XnTq1dFDK8o+tDb5VCViLvfhVdpfZLY
+Uspzgb8c8+a4bmYRBbMelC1/kZWSWfFMzqORcUx8Rww7Cxn2obFshj5cqsQugsv5
+B5a6SE2Q8pTIqXOi6wZ7I53eovNNVZ96YUWYGGjHXkBrI/V5eu+MtWuLt29G9Hvx
+PUsE2JOAWVrgQSQdso8VYFhH2+9uRv0V9dlfmrPb2LjkQLPNlzmuhbsdjrzch5vR
+pu/xO28QOG8=
+-----END CERTIFICATE-----'
+ end
+
+ trait :with_expired_certificate do
+ certificate '-----BEGIN CERTIFICATE-----
+MIIBsDCCARmgAwIBAgIBATANBgkqhkiG9w0BAQUFADAeMRwwGgYDVQQDExNleHBp
+cmVkLWNlcnRpZmljYXRlMB4XDTE1MDIxMjE0MzMwMFoXDTE2MDIwMTE0MzMwMFow
+HjEcMBoGA1UEAxMTZXhwaXJlZC1jZXJ0aWZpY2F0ZTCBnzANBgkqhkiG9w0BAQEF
+AAOBjQAwgYkCgYEApL4J9L0ZxFJ1hI1LPIflAlAGvm6ZEvoT4qKU5Xf2JgU7/2ge
+NR1qlNFaSvCc08Knupp5yTgmvyK/Xi09U0N82vvp4Zvr/diSc4A/RA6Mta6egLyS
+NT438kdTnY2tR5feoTLwQpX0t4IMlwGQGT5h6Of2fKmDxzuwuyffcIHqLdsCAwEA
+ATANBgkqhkiG9w0BAQUFAAOBgQBNj+vWvneyW1KkbVK+b/cVmnYPSfbkHrYK6m8X
+Hq9LkWn6WP4EHsesHyslgTQZF8C7kVLTbLn2noLnOE+Mp3vcWlZxl3Yk6aZMhKS+
+Iy6oRpHaCF/2obZdIdgf9rlyz0fkqyHJc9GkioSoOhJZxEV2SgAkap8yS0sX2tJ9
+ZDXgrA==
+-----END CERTIFICATE-----'
+ end
+ end
+end
diff --git a/spec/factories/project_group_links.rb b/spec/factories/project_group_links.rb
index e73cc05f9d7..50341d943f5 100644
--- a/spec/factories/project_group_links.rb
+++ b/spec/factories/project_group_links.rb
@@ -1,6 +1,6 @@
FactoryGirl.define do
factory :project_group_link do
- project
+ project factory: :empty_project
group
end
end
diff --git a/spec/factories/project_members.rb b/spec/factories/project_members.rb
index c21927640d1..d62799a5a47 100644
--- a/spec/factories/project_members.rb
+++ b/spec/factories/project_members.rb
@@ -1,7 +1,7 @@
FactoryGirl.define do
factory :project_member do
user
- project
+ project factory: :empty_project
master
trait(:guest) { access_level ProjectMember::GUEST }
diff --git a/spec/factories/project_snippets.rb b/spec/factories/project_snippets.rb
index d681a2c8483..e0fe1b36fd3 100644
--- a/spec/factories/project_snippets.rb
+++ b/spec/factories/project_snippets.rb
@@ -1,5 +1,5 @@
FactoryGirl.define do
factory :project_snippet, parent: :snippet, class: :ProjectSnippet do
- project
+ project factory: :empty_project
end
end
diff --git a/spec/factories/project_statistics.rb b/spec/factories/project_statistics.rb
new file mode 100644
index 00000000000..72d43096216
--- /dev/null
+++ b/spec/factories/project_statistics.rb
@@ -0,0 +1,6 @@
+FactoryGirl.define do
+ factory :project_statistics do
+ project { create :project }
+ namespace { project.namespace }
+ end
+end
diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb
index f7fa834d7a2..c80b09e9b9d 100644
--- a/spec/factories/projects.rb
+++ b/spec/factories/projects.rb
@@ -24,10 +24,18 @@ FactoryGirl.define do
visibility_level Gitlab::VisibilityLevel::PRIVATE
end
+ trait :archived do
+ archived true
+ end
+
trait :access_requestable do
request_access_enabled true
end
+ trait :repository do
+ # no-op... for now!
+ end
+
trait :empty_repo do
after(:create) do |project|
project.create_repository
@@ -48,6 +56,25 @@ FactoryGirl.define do
end
end
+ trait(:wiki_enabled) { wiki_access_level ProjectFeature::ENABLED }
+ trait(:wiki_disabled) { wiki_access_level ProjectFeature::DISABLED }
+ trait(:wiki_private) { wiki_access_level ProjectFeature::PRIVATE }
+ trait(:builds_enabled) { builds_access_level ProjectFeature::ENABLED }
+ trait(:builds_disabled) { builds_access_level ProjectFeature::DISABLED }
+ trait(:builds_private) { builds_access_level ProjectFeature::PRIVATE }
+ trait(:snippets_enabled) { snippets_access_level ProjectFeature::ENABLED }
+ trait(:snippets_disabled) { snippets_access_level ProjectFeature::DISABLED }
+ trait(:snippets_private) { snippets_access_level ProjectFeature::PRIVATE }
+ trait(:issues_disabled) { issues_access_level ProjectFeature::DISABLED }
+ trait(:issues_enabled) { issues_access_level ProjectFeature::ENABLED }
+ trait(:issues_private) { issues_access_level ProjectFeature::PRIVATE }
+ trait(:merge_requests_enabled) { merge_requests_access_level ProjectFeature::ENABLED }
+ trait(:merge_requests_disabled) { merge_requests_access_level ProjectFeature::DISABLED }
+ trait(:merge_requests_private) { merge_requests_access_level ProjectFeature::PRIVATE }
+ trait(:repository_enabled) { repository_access_level ProjectFeature::ENABLED }
+ trait(:repository_disabled) { repository_access_level ProjectFeature::DISABLED }
+ trait(:repository_private) { repository_access_level ProjectFeature::PRIVATE }
+
# Nest Project Feature attributes
transient do
wiki_access_level ProjectFeature::ENABLED
@@ -98,6 +125,42 @@ FactoryGirl.define do
path { 'gitlabhq' }
test_repo
+
+ transient do
+ create_template nil
+ end
+
+ after :create do |project, evaluator|
+ TestEnv.copy_repo(project)
+
+ if evaluator.create_template
+ args = evaluator.create_template
+
+ project.add_user(args[:user], args[:access])
+
+ project.repository.commit_file(
+ args[:user],
+ ".gitlab/#{args[:path]}/bug.md",
+ 'something valid',
+ message: 'test 3',
+ branch_name: 'master',
+ update: false)
+ project.repository.commit_file(
+ args[:user],
+ ".gitlab/#{args[:path]}/template_test.md",
+ 'template_test',
+ message: 'test 1',
+ branch_name: 'master',
+ update: false)
+ project.repository.commit_file(
+ args[:user],
+ ".gitlab/#{args[:path]}/feature_proposal.md",
+ 'feature_proposal',
+ message: 'test 2',
+ branch_name: 'master',
+ update: false)
+ end
+ end
end
factory :forked_project_with_submodules, parent: :empty_project do
diff --git a/spec/factories/releases.rb b/spec/factories/releases.rb
index 74497dc82c0..6a6d6fa171f 100644
--- a/spec/factories/releases.rb
+++ b/spec/factories/releases.rb
@@ -2,6 +2,6 @@ FactoryGirl.define do
factory :release do
tag "v1.1.0"
description "Awesome release"
- project
+ project factory: :empty_project
end
end
diff --git a/spec/factories/sent_notifications.rb b/spec/factories/sent_notifications.rb
index 78eb929c6e7..6287c40afe9 100644
--- a/spec/factories/sent_notifications.rb
+++ b/spec/factories/sent_notifications.rb
@@ -1,6 +1,6 @@
FactoryGirl.define do
factory :sent_notification do
- project
+ project factory: :empty_project
recipient factory: :user
noteable factory: :issue
reply_key "0123456789abcdef" * 2
diff --git a/spec/factories/services.rb b/spec/factories/services.rb
index 9de78d68280..a14a46c803e 100644
--- a/spec/factories/services.rb
+++ b/spec/factories/services.rb
@@ -1,5 +1,5 @@
FactoryGirl.define do
factory :service do
- project
+ project factory: :empty_project
end
end
diff --git a/spec/factories/timelogs.rb b/spec/factories/timelogs.rb
new file mode 100644
index 00000000000..6f1545418eb
--- /dev/null
+++ b/spec/factories/timelogs.rb
@@ -0,0 +1,9 @@
+# Read about factories at https://github.com/thoughtbot/factory_girl
+
+FactoryGirl.define do
+ factory :timelog do
+ time_spent 3600
+ user
+ issue
+ end
+end
diff --git a/spec/factories/todos.rb b/spec/factories/todos.rb
index 866e663f026..91d6f39a5bf 100644
--- a/spec/factories/todos.rb
+++ b/spec/factories/todos.rb
@@ -1,6 +1,6 @@
FactoryGirl.define do
factory :todo do
- project
+ project factory: :empty_project
author
user
target factory: :issue
@@ -21,12 +21,17 @@ FactoryGirl.define do
trait :build_failed do
action { Todo::BUILD_FAILED }
+ target factory: :merge_request
end
trait :approval_required do
action { Todo::APPROVAL_REQUIRED }
end
+ trait :unmergeable do
+ action { Todo::UNMERGEABLE }
+ end
+
trait :done do
state :done
end
diff --git a/spec/factories/trending_project.rb b/spec/factories/trending_project.rb
new file mode 100644
index 00000000000..246176611dc
--- /dev/null
+++ b/spec/factories/trending_project.rb
@@ -0,0 +1,6 @@
+FactoryGirl.define do
+ # TrendingProject
+ factory :trending_project, class: 'TrendingProject' do
+ project
+ end
+end
diff --git a/spec/features/admin/admin_builds_spec.rb b/spec/features/admin/admin_builds_spec.rb
index e177059d959..9d5ce876c29 100644
--- a/spec/features/admin/admin_builds_spec.rb
+++ b/spec/features/admin/admin_builds_spec.rb
@@ -9,8 +9,8 @@ describe 'Admin Builds' do
let(:pipeline) { create(:ci_pipeline) }
context 'All tab' do
- context 'when have builds' do
- it 'shows all builds' do
+ context 'when have jobs' do
+ it 'shows all jobs' do
create(:ci_build, pipeline: pipeline, status: :pending)
create(:ci_build, pipeline: pipeline, status: :running)
create(:ci_build, pipeline: pipeline, status: :success)
@@ -19,26 +19,26 @@ describe 'Admin Builds' do
visit admin_builds_path
expect(page).to have_selector('.nav-links li.active', text: 'All')
- expect(page).to have_selector('.row-content-block', text: 'All builds')
+ expect(page).to have_selector('.row-content-block', text: 'All jobs')
expect(page.all('.build-link').size).to eq(4)
expect(page).to have_link 'Cancel all'
end
end
- context 'when have no builds' do
+ context 'when have no jobs' do
it 'shows a message' do
visit admin_builds_path
expect(page).to have_selector('.nav-links li.active', text: 'All')
- expect(page).to have_content 'No builds to show'
+ expect(page).to have_content 'No jobs to show'
expect(page).not_to have_link 'Cancel all'
end
end
end
context 'Pending tab' do
- context 'when have pending builds' do
- it 'shows pending builds' do
+ context 'when have pending jobs' do
+ it 'shows pending jobs' do
build1 = create(:ci_build, pipeline: pipeline, status: :pending)
build2 = create(:ci_build, pipeline: pipeline, status: :running)
build3 = create(:ci_build, pipeline: pipeline, status: :success)
@@ -55,22 +55,22 @@ describe 'Admin Builds' do
end
end
- context 'when have no builds pending' do
+ context 'when have no jobs pending' do
it 'shows a message' do
create(:ci_build, pipeline: pipeline, status: :success)
visit admin_builds_path(scope: :pending)
expect(page).to have_selector('.nav-links li.active', text: 'Pending')
- expect(page).to have_content 'No builds to show'
+ expect(page).to have_content 'No jobs to show'
expect(page).not_to have_link 'Cancel all'
end
end
end
context 'Running tab' do
- context 'when have running builds' do
- it 'shows running builds' do
+ context 'when have running jobs' do
+ it 'shows running jobs' do
build1 = create(:ci_build, pipeline: pipeline, status: :running)
build2 = create(:ci_build, pipeline: pipeline, status: :success)
build3 = create(:ci_build, pipeline: pipeline, status: :failed)
@@ -87,22 +87,22 @@ describe 'Admin Builds' do
end
end
- context 'when have no builds running' do
+ context 'when have no jobs running' do
it 'shows a message' do
create(:ci_build, pipeline: pipeline, status: :success)
visit admin_builds_path(scope: :running)
expect(page).to have_selector('.nav-links li.active', text: 'Running')
- expect(page).to have_content 'No builds to show'
+ expect(page).to have_content 'No jobs to show'
expect(page).not_to have_link 'Cancel all'
end
end
end
context 'Finished tab' do
- context 'when have finished builds' do
- it 'shows finished builds' do
+ context 'when have finished jobs' do
+ it 'shows finished jobs' do
build1 = create(:ci_build, pipeline: pipeline, status: :pending)
build2 = create(:ci_build, pipeline: pipeline, status: :running)
build3 = create(:ci_build, pipeline: pipeline, status: :success)
@@ -117,14 +117,14 @@ describe 'Admin Builds' do
end
end
- context 'when have no builds finished' do
+ context 'when have no jobs finished' do
it 'shows a message' do
create(:ci_build, pipeline: pipeline, status: :running)
visit admin_builds_path(scope: :finished)
expect(page).to have_selector('.nav-links li.active', text: 'Finished')
- expect(page).to have_content 'No builds to show'
+ expect(page).to have_content 'No jobs to show'
expect(page).to have_link 'Cancel all'
end
end
diff --git a/spec/features/admin/admin_deploy_keys_spec.rb b/spec/features/admin/admin_deploy_keys_spec.rb
index 8bf68480785..7ce6cce0a5c 100644
--- a/spec/features/admin/admin_deploy_keys_spec.rb
+++ b/spec/features/admin/admin_deploy_keys_spec.rb
@@ -15,15 +15,36 @@ RSpec.describe 'admin deploy keys', type: :feature do
expect(page).to have_content(another_deploy_key.title)
end
- it 'creates new deploy key' do
- visit admin_deploy_keys_path
+ describe 'create new deploy key' do
+ before do
+ visit admin_deploy_keys_path
+ click_link 'New Deploy Key'
+ end
+
+ it 'creates new deploy key' do
+ fill_deploy_key
+ click_button 'Create'
+
+ expect_renders_new_key
+ end
+
+ it 'creates new deploy key with write access' do
+ fill_deploy_key
+ check "deploy_key_can_push"
+ click_button "Create"
+
+ expect_renders_new_key
+ expect(page).to have_content('Yes')
+ end
- click_link 'New Deploy Key'
- fill_in 'deploy_key_title', with: 'laptop'
- fill_in 'deploy_key_key', with: 'ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAzrEJUIR6Y03TCE9rIJ+GqTBvgb8t1jI9h5UBzCLuK4VawOmkLornPqLDrGbm6tcwM/wBrrLvVOqi2HwmkKEIecVO0a64A4rIYScVsXIniHRS6w5twyn1MD3sIbN+socBDcaldECQa2u1dI3tnNVcs8wi77fiRe7RSxePsJceGoheRQgC8AZ510UdIlO+9rjIHUdVN7LLyz512auAfYsgx1OfablkQ/XJcdEwDNgi9imI6nAXhmoKUm1IPLT2yKajTIC64AjLOnE0YyCh6+7RFMpiMyu1qiOCpdjYwTgBRiciNRZCH8xIedyCoAmiUgkUT40XYHwLuwiPJICpkAzp7Q== user@laptop'
- click_button 'Create'
+ def expect_renders_new_key
+ expect(current_path).to eq admin_deploy_keys_path
+ expect(page).to have_content('laptop')
+ end
- expect(current_path).to eq admin_deploy_keys_path
- expect(page).to have_content('laptop')
+ def fill_deploy_key
+ fill_in 'deploy_key_title', with: 'laptop'
+ fill_in 'deploy_key_key', with: 'ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAzrEJUIR6Y03TCE9rIJ+GqTBvgb8t1jI9h5UBzCLuK4VawOmkLornPqLDrGbm6tcwM/wBrrLvVOqi2HwmkKEIecVO0a64A4rIYScVsXIniHRS6w5twyn1MD3sIbN+socBDcaldECQa2u1dI3tnNVcs8wi77fiRe7RSxePsJceGoheRQgC8AZ510UdIlO+9rjIHUdVN7LLyz512auAfYsgx1OfablkQ/XJcdEwDNgi9imI6nAXhmoKUm1IPLT2yKajTIC64AjLOnE0YyCh6+7RFMpiMyu1qiOCpdjYwTgBRiciNRZCH8xIedyCoAmiUgkUT40XYHwLuwiPJICpkAzp7Q== user@laptop'
+ end
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 66044b44495..e8e080ce3e2 100644
--- a/spec/features/admin/admin_disables_git_access_protocol_spec.rb
+++ b/spec/features/admin/admin_disables_git_access_protocol_spec.rb
@@ -1,10 +1,13 @@
require 'rails_helper'
feature 'Admin disables Git access protocol', feature: true do
+ include StubENV
+
let(:project) { create(:empty_project, :empty_repo) }
let(:admin) { create(:admin) }
background do
+ stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
login_as(admin)
end
diff --git a/spec/features/admin/admin_groups_spec.rb b/spec/features/admin/admin_groups_spec.rb
index 0aa01fc499a..a871e370ba2 100644
--- a/spec/features/admin/admin_groups_spec.rb
+++ b/spec/features/admin/admin_groups_spec.rb
@@ -1,15 +1,39 @@
require 'spec_helper'
feature 'Admin Groups', feature: true do
+ include Select2Helper
+
let(:internal) { Gitlab::VisibilityLevel::INTERNAL }
+ let(:user) { create :user }
+ let!(:group) { create :group }
+ let!(:current_user) { login_as :admin }
before do
- login_as(:admin)
-
stub_application_setting(default_group_visibility: internal)
end
+ describe 'list' do
+ it 'renders groups' do
+ visit admin_groups_path
+
+ expect(page).to have_content(group.name)
+ end
+ end
+
describe 'create a group' do
+ it 'creates new group' do
+ visit admin_groups_path
+
+ click_link "New Group"
+ fill_in 'group_path', with: 'gitlab'
+ fill_in 'group_description', with: 'Group description'
+ click_button "Create group"
+
+ expect(current_path).to eq admin_group_path(Group.find_by(path: 'gitlab'))
+ expect(page).to have_content('Group: gitlab')
+ expect(page).to have_content('Group description')
+ end
+
scenario 'shows the visibility level radio populated with the default value' do
visit new_admin_group_path
@@ -17,6 +41,16 @@ feature 'Admin Groups', feature: true do
end
end
+ describe 'show a group' do
+ scenario 'shows the group' do
+ group = create(:group, :private)
+
+ visit admin_group_path(group)
+
+ expect(page).to have_content("Group: #{group.name}")
+ end
+ end
+
describe 'group edit' do
scenario 'shows the visibility level radio populated with the group visibility_level value' do
group = create(:group, :private)
@@ -27,6 +61,91 @@ feature 'Admin Groups', feature: true do
end
end
+ describe 'add user into a group', js: true do
+ shared_context 'adds user into a group' do
+ it do
+ visit admin_group_path(group)
+
+ select2(user_selector, from: '#user_ids', multiple: true)
+ page.within '#new_project_member' do
+ select2(Gitlab::Access::REPORTER, from: '#access_level')
+ end
+ click_button "Add users to group"
+ page.within ".group-users-list" do
+ expect(page).to have_content(user.name)
+ expect(page).to have_content('Reporter')
+ end
+ end
+ end
+
+ it_behaves_like 'adds user into a group' do
+ let(:user_selector) { user.id }
+ end
+
+ it_behaves_like 'adds user into a group' do
+ let(:user_selector) { user.email }
+ end
+ end
+
+ describe 'add admin himself to a group' do
+ before do
+ group.add_user(:user, Gitlab::Access::OWNER)
+ end
+
+ it 'adds admin a to a group as developer', js: true do
+ visit group_group_members_path(group)
+
+ page.within '.users-group-form' do
+ select2(current_user.id, from: '#user_ids', multiple: true)
+ select 'Developer', from: 'access_level'
+ end
+
+ click_button 'Add to group'
+
+ page.within '.content-list' do
+ expect(page).to have_content(current_user.name)
+ expect(page).to have_content('Developer')
+ end
+ end
+ end
+
+ describe 'admin remove himself from a group', js: true do
+ it 'removes admin from the group' do
+ group.add_user(current_user, Gitlab::Access::DEVELOPER)
+
+ visit group_group_members_path(group)
+
+ page.within '.content-list' do
+ expect(page).to have_content(current_user.name)
+ expect(page).to have_content('Developer')
+ end
+
+ find(:css, 'li', text: current_user.name).find(:css, 'a.btn-remove').click
+
+ visit group_group_members_path(group)
+
+ page.within '.content-list' do
+ expect(page).not_to have_content(current_user.name)
+ expect(page).not_to have_content('Developer')
+ end
+ end
+ end
+
+ describe 'shared projects' do
+ it 'renders shared project' do
+ empty_project = create(:empty_project)
+ empty_project.project_group_links.create!(
+ group_access: Gitlab::Access::MASTER,
+ group: group
+ )
+
+ visit admin_group_path(group)
+
+ expect(page).to have_content(empty_project.name_with_namespace)
+ expect(page).to have_content('Projects shared with')
+ end
+ end
+
def expect_selected_visibility(level)
selector = "#group_visibility_level_#{level}[checked=checked]"
diff --git a/spec/features/admin/admin_health_check_spec.rb b/spec/features/admin/admin_health_check_spec.rb
index dec2dedf2b5..f7e49a56deb 100644
--- a/spec/features/admin/admin_health_check_spec.rb
+++ b/spec/features/admin/admin_health_check_spec.rb
@@ -1,9 +1,11 @@
require 'spec_helper'
feature "Admin Health Check", feature: true do
+ include StubENV
include WaitForAjax
before do
+ stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
login_as :admin
end
@@ -12,11 +14,12 @@ feature "Admin Health Check", feature: true do
visit admin_health_check_path
end
- it { page.has_text? 'Health Check' }
- it { page.has_text? 'Health information can be retrieved' }
-
it 'has a health check access token' do
+ page.has_text? 'Health Check'
+ page.has_text? 'Health information can be retrieved'
+
token = current_application_settings.health_check_access_token
+
expect(page).to have_content("Access token is #{token}")
expect(page).to have_selector('#health-check-token', text: token)
end
diff --git a/spec/features/admin/admin_projects_spec.rb b/spec/features/admin/admin_projects_spec.rb
index a5b88812b75..87a8f62687a 100644
--- a/spec/features/admin/admin_projects_spec.rb
+++ b/spec/features/admin/admin_projects_spec.rb
@@ -10,7 +10,7 @@ describe "Admin::Projects", feature: true do
end
describe "GET /admin/projects" do
- let!(:archived_project) { create :project, :public, archived: true }
+ let!(:archived_project) { create :project, :public, :archived }
before do
visit admin_projects_path
diff --git a/spec/features/admin/admin_runners_spec.rb b/spec/features/admin/admin_runners_spec.rb
index d92c66b689d..f05fbe3d062 100644
--- a/spec/features/admin/admin_runners_spec.rb
+++ b/spec/features/admin/admin_runners_spec.rb
@@ -1,7 +1,10 @@
require 'spec_helper'
describe "Admin Runners" do
+ include StubENV
+
before do
+ stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
login_as :admin
end
diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb
index 47fa2f14307..de42ab81fac 100644
--- a/spec/features/admin/admin_settings_spec.rb
+++ b/spec/features/admin/admin_settings_spec.rb
@@ -1,7 +1,10 @@
require 'spec_helper'
feature 'Admin updates settings', feature: true do
- before(:each) do
+ include StubENV
+
+ before do
+ stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
login_as :admin
visit admin_application_settings_path
end
diff --git a/spec/features/admin/admin_users_spec.rb b/spec/features/admin/admin_users_spec.rb
index e31325ce47b..c0807b8c507 100644
--- a/spec/features/admin/admin_users_spec.rb
+++ b/spec/features/admin/admin_users_spec.rb
@@ -1,7 +1,13 @@
require 'spec_helper'
-describe "Admin::Users", feature: true do
- before { login_as :admin }
+describe "Admin::Users", feature: true do
+ include WaitForAjax
+
+ let!(:user) do
+ create(:omniauth_user, provider: 'twitter', extern_uid: '123456')
+ end
+
+ let!(:current_user) { login_as :admin }
describe "GET /admin/users" do
before do
@@ -13,8 +19,10 @@ describe "Admin::Users", feature: true do
end
it "has users list" do
- expect(page).to have_content(@user.email)
- expect(page).to have_content(@user.name)
+ expect(page).to have_content(current_user.email)
+ expect(page).to have_content(current_user.name)
+ expect(page).to have_content(user.email)
+ expect(page).to have_content(user.name)
end
describe 'Two-factor Authentication filters' do
@@ -38,8 +46,6 @@ describe "Admin::Users", feature: true do
end
it 'counts users who have not enabled 2FA' do
- create(:user)
-
visit admin_users_path
page.within('.filter-two-factor-disabled small') do
@@ -48,8 +54,6 @@ describe "Admin::Users", feature: true do
end
it 'filters by users who have not enabled 2FA' do
- user = create(:user)
-
visit admin_users_path
click_link '2FA Disabled'
@@ -108,10 +112,10 @@ describe "Admin::Users", feature: true do
describe "GET /admin/users/:id" do
it "has user info" do
visit admin_users_path
- click_link @user.name
+ click_link user.name
- expect(page).to have_content(@user.email)
- expect(page).to have_content(@user.name)
+ expect(page).to have_content(user.email)
+ expect(page).to have_content(user.name)
end
describe 'Impersonation' do
@@ -124,7 +128,7 @@ describe "Admin::Users", feature: true do
end
it 'does not show impersonate button for admin itself' do
- visit admin_user_path(@user)
+ visit admin_user_path(current_user)
expect(page).not_to have_content('Impersonate')
end
@@ -156,7 +160,7 @@ describe "Admin::Users", feature: true do
it 'logs out of impersonated user back to original user' do
find(:css, 'li.impersonation a').click
- expect(page.find(:css, '.header-user .profile-link')['data-user']).to eql(@user.username)
+ expect(page.find(:css, '.header-user .profile-link')['data-user']).to eql(current_user.username)
end
it 'is redirected back to the impersonated users page in the admin after stopping' do
@@ -169,15 +173,15 @@ describe "Admin::Users", feature: true do
describe 'Two-factor Authentication status' do
it 'shows when enabled' do
- @user.update_attribute(:otp_required_for_login, true)
+ user.update_attribute(:otp_required_for_login, true)
- visit admin_user_path(@user)
+ visit admin_user_path(user)
expect_two_factor_status('Enabled')
end
it 'shows when disabled' do
- visit admin_user_path(@user)
+ visit admin_user_path(user)
expect_two_factor_status('Disabled')
end
@@ -192,9 +196,8 @@ describe "Admin::Users", feature: true do
describe "GET /admin/users/:id/edit" do
before do
- @simple_user = create(:user)
visit admin_users_path
- click_link "edit_user_#{@simple_user.id}"
+ click_link "edit_user_#{user.id}"
end
it "has user edit page" do
@@ -208,49 +211,172 @@ describe "Admin::Users", feature: true do
fill_in "user_email", with: "bigbang@mail.com"
fill_in "user_password", with: "AValidPassword1"
fill_in "user_password_confirmation", with: "AValidPassword1"
- check "user_admin"
+ choose "user_access_level_admin"
click_button "Save changes"
end
- it "shows page with new data" do
+ it "shows page with new data" do
expect(page).to have_content('bigbang@mail.com')
expect(page).to have_content('Big Bang')
end
it "changes user entry" do
- @simple_user.reload
- expect(@simple_user.name).to eq('Big Bang')
- expect(@simple_user.is_admin?).to be_truthy
- expect(@simple_user.password_expires_at).to be <= Time.now
+ user.reload
+ expect(user.name).to eq('Big Bang')
+ expect(user.is_admin?).to be_truthy
+ expect(user.password_expires_at).to be <= Time.now
+ end
+ end
+
+ describe 'update username to non ascii char' do
+ it do
+ fill_in 'user_username', with: '\u3042\u3044'
+ click_button('Save')
+
+ page.within '#error_explanation' do
+ expect(page).to have_content('Username')
+ end
+
+ expect(page).to have_selector(%(form[action="/admin/users/#{user.username}"]))
end
end
end
describe "GET /admin/users/:id/projects" do
+ let(:group) { create(:group) }
+ let!(:project) { create(:project, group: group) }
+
before do
- @group = create(:group)
- @project = create(:project, group: @group)
- @simple_user = create(:user)
- @group.add_developer(@simple_user)
+ group.add_developer(user)
- visit projects_admin_user_path(@simple_user)
+ visit projects_admin_user_path(user)
end
it "lists group projects" do
within(:css, '.append-bottom-default + .panel') do
expect(page).to have_content 'Group projects'
- expect(page).to have_link @group.name, admin_group_path(@group)
+ expect(page).to have_link group.name, admin_group_path(group)
end
end
it 'allows navigation to the group details' do
within(:css, '.append-bottom-default + .panel') do
- click_link @group.name
+ click_link group.name
end
within(:css, 'h3.page-title') do
- expect(page).to have_content "Group: #{@group.name}"
+ expect(page).to have_content "Group: #{group.name}"
+ end
+ expect(page).to have_content project.name
+ end
+
+ it 'shows the group access level' do
+ within(:css, '.append-bottom-default + .panel') do
+ expect(page).to have_content 'Developer'
end
- expect(page).to have_content @project.name
+ end
+
+ it 'allows group membership to be revoked', js: true do
+ page.within(first('.group_member')) do
+ find('.btn-remove').click
+ end
+ wait_for_ajax
+
+ expect(page).not_to have_selector('.group_member')
+ end
+ end
+
+ describe 'show user attributes' do
+ it do
+ visit admin_users_path
+
+ click_link user.name
+
+ expect(page).to have_content 'Account'
+ expect(page).to have_content 'Personal projects limit'
+ end
+ end
+
+ describe 'remove users secondary email', js: true do
+ let!(:secondary_email) do
+ create :email, email: 'secondary@example.com', user: user
+ end
+
+ it do
+ visit admin_user_path(user.username)
+
+ expect(page).to have_content("Secondary email: #{secondary_email.email}")
+
+ find("#remove_email_#{secondary_email.id}").click
+
+ expect(page).not_to have_content(secondary_email.email)
+ end
+ end
+
+ describe 'show user keys' do
+ let!(:key1) do
+ create(:key, user: user, title: "ssh-rsa Key1", key: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC4FIEBXGi4bPU8kzxMefudPIJ08/gNprdNTaO9BR/ndy3+58s2HCTw2xCHcsuBmq+TsAqgEidVq4skpqoTMB+Uot5Uzp9z4764rc48dZiI661izoREoKnuRQSsRqUTHg5wrLzwxlQbl1MVfRWQpqiz/5KjBC7yLEb9AbusjnWBk8wvC1bQPQ1uLAauEA7d836tgaIsym9BrLsMVnR4P1boWD3Xp1B1T/ImJwAGHvRmP/ycIqmKdSpMdJXwxcb40efWVj0Ibbe7ii9eeoLdHACqevUZi6fwfbymdow+FeqlkPoHyGg3Cu4vD/D8+8cRc7mE/zGCWcQ15Var83Tczour Key1")
+ end
+
+ let!(:key2) do
+ create(:key, user: user, title: "ssh-rsa Key2", key: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDQSTWXhJAX/He+nG78MiRRRn7m0Pb0XbcgTxE0etArgoFoh9WtvDf36HG6tOSg/0UUNcp0dICsNAmhBKdncp6cIyPaXJTURPRAGvhI0/VDk4bi27bRnccGbJ/hDaUxZMLhhrzY0r22mjVf8PF6dvv5QUIQVm1/LeaWYsHHvLgiIjwrXirUZPnFrZw6VLREoBKG8uWvfSXw1L5eapmstqfsME8099oi+vWLR8MgEysZQmD28M73fgW4zek6LDQzKQyJx9nB+hJkKUDvcuziZjGmRFlNgSA2mguERwL1OXonD8WYUrBDGKroIvBT39zS5d9tQDnidEJZ9Y8gv5ViYP7x Key2")
+ end
+
+ it do
+ visit admin_users_path
+
+ click_link user.name
+ click_link 'SSH keys'
+
+ expect(page).to have_content(key1.title)
+ expect(page).to have_content(key2.title)
+
+ click_link key2.title
+
+ expect(page).to have_content(key2.title)
+ expect(page).to have_content(key2.key)
+
+ click_link 'Remove'
+
+ expect(page).not_to have_content(key2.title)
+ end
+ end
+
+ describe 'show user identities' do
+ it 'shows user identities' do
+ visit admin_user_identities_path(user)
+
+ expect(page).to have_content(user.name)
+ expect(page).to have_content('twitter')
+ end
+ end
+
+ describe 'update user identities' do
+ before do
+ allow(Gitlab::OAuth::Provider).to receive(:providers).and_return([:twitter, :twitter_updated])
+ end
+
+ it 'modifies twitter identity' do
+ visit admin_user_identities_path(user)
+
+ find('.table').find(:link, 'Edit').click
+ fill_in 'identity_extern_uid', with: '654321'
+ select 'twitter_updated', from: 'identity_provider'
+ click_button 'Save changes'
+
+ expect(page).to have_content(user.name)
+ expect(page).to have_content('twitter_updated')
+ expect(page).to have_content('654321')
+ end
+ end
+
+ describe 'remove user with identities' do
+ it 'removes user with twitter identity' do
+ visit admin_user_identities_path(user)
+
+ click_link 'Delete'
+
+ expect(page).to have_content(user.name)
+ expect(page).not_to have_content('twitter')
end
end
end
diff --git a/spec/features/admin/admin_uses_repository_checks_spec.rb b/spec/features/admin/admin_uses_repository_checks_spec.rb
index 661fb761809..855247de2ea 100644
--- a/spec/features/admin/admin_uses_repository_checks_spec.rb
+++ b/spec/features/admin/admin_uses_repository_checks_spec.rb
@@ -1,7 +1,12 @@
require 'rails_helper'
feature 'Admin uses repository checks', feature: true do
- before { login_as :admin }
+ include StubENV
+
+ before do
+ stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
+ login_as :admin
+ end
scenario 'to trigger a single check' do
project = create(:empty_project)
@@ -29,7 +34,7 @@ feature 'Admin uses repository checks', feature: true do
scenario 'to clear all repository checks', js: true do
visit admin_application_settings_path
-
+
expect(RepositoryCheck::ClearWorker).to receive(:perform_async)
click_link 'Clear all repository checks'
diff --git a/spec/features/auto_deploy_spec.rb b/spec/features/auto_deploy_spec.rb
new file mode 100644
index 00000000000..ea7a97d1d4f
--- /dev/null
+++ b/spec/features/auto_deploy_spec.rb
@@ -0,0 +1,64 @@
+require 'spec_helper'
+
+describe 'Auto deploy' do
+ include WaitForAjax
+
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+
+ before do
+ project.create_kubernetes_service(
+ active: true,
+ properties: {
+ namespace: project.path,
+ api_url: 'https://kubernetes.example.com',
+ token: 'a' * 40,
+ }
+ )
+ project.team << [user, :master]
+ login_as user
+ end
+
+ context 'when no deployment service is active' do
+ before do
+ project.kubernetes_service.update!(active: false)
+ end
+
+ it 'does not show a button to set up auto deploy' do
+ visit namespace_project_path(project.namespace, project)
+ expect(page).to have_no_content('Set up auto deploy')
+ end
+ end
+
+ context 'when a deployment service is active' do
+ before do
+ project.kubernetes_service.update!(active: true)
+ visit namespace_project_path(project.namespace, project)
+ end
+
+ it 'shows a button to set up auto deploy' do
+ expect(page).to have_link('Set up auto deploy')
+ end
+
+ it 'includes OpenShift as an available template', js: true do
+ click_link 'Set up auto deploy'
+ click_button 'Choose a GitLab CI Yaml template'
+
+ within '.gitlab-ci-yml-selector' do
+ expect(page).to have_content('OpenShift')
+ end
+ end
+
+ it 'creates a merge request using "auto-deploy" branch', js: true do
+ click_link 'Set up auto deploy'
+ click_button 'Choose a GitLab CI Yaml template'
+ within '.gitlab-ci-yml-selector' do
+ click_on 'OpenShift'
+ end
+ wait_for_ajax
+ click_button 'Commit Changes'
+
+ expect(page).to have_content('New Merge Request From auto-deploy into master')
+ end
+ end
+end
diff --git a/spec/features/boards/add_issues_modal_spec.rb b/spec/features/boards/add_issues_modal_spec.rb
new file mode 100644
index 00000000000..2875fc1e533
--- /dev/null
+++ b/spec/features/boards/add_issues_modal_spec.rb
@@ -0,0 +1,233 @@
+require 'rails_helper'
+
+describe 'Issue Boards add issue modal', :feature, :js do
+ include WaitForAjax
+ include WaitForVueResource
+
+ let(:project) { create(:empty_project, :public) }
+ let(:board) { create(:board, project: project) }
+ let(:user) { create(:user) }
+ let!(:planning) { create(:label, project: project, name: 'Planning') }
+ let!(:label) { create(:label, project: project) }
+ let!(:list1) { create(:list, board: board, label: planning, position: 0) }
+ let!(:list2) { create(:list, board: board, label: label, position: 1) }
+ let!(:issue) { create(:issue, project: project) }
+ let!(:issue2) { create(:issue, project: project) }
+
+ before do
+ project.team << [user, :master]
+
+ login_as(user)
+
+ visit namespace_project_board_path(project.namespace, project, board)
+ wait_for_vue_resource
+ end
+
+ context 'modal interaction' do
+ it 'opens modal' do
+ click_button('Add issues')
+
+ expect(page).to have_selector('.add-issues-modal')
+ end
+
+ it 'closes modal' do
+ click_button('Add issues')
+
+ page.within('.add-issues-modal') do
+ find('.close').click
+ end
+
+ expect(page).not_to have_selector('.add-issues-modal')
+ end
+
+ it 'closes modal if cancel button clicked' do
+ click_button('Add issues')
+
+ page.within('.add-issues-modal') do
+ click_button 'Cancel'
+ end
+
+ expect(page).not_to have_selector('.add-issues-modal')
+ end
+ end
+
+ context 'issues list' do
+ before do
+ click_button('Add issues')
+
+ wait_for_vue_resource
+ end
+
+ it 'loads issues' do
+ page.within('.add-issues-modal') do
+ page.within('.nav-links') do
+ expect(page).to have_content('2')
+ end
+
+ expect(page).to have_selector('.card', count: 2)
+ end
+ end
+
+ it 'shows selected issues' do
+ page.within('.add-issues-modal') do
+ click_link 'Selected issues'
+
+ expect(page).not_to have_selector('.card')
+ end
+ end
+
+ context 'list dropdown' do
+ it 'resets after deleting list' do
+ page.within('.add-issues-modal') do
+ expect(find('.add-issues-footer')).to have_button(planning.title)
+
+ click_button 'Cancel'
+ end
+
+ first('.board-delete').click
+
+ click_button('Add issues')
+
+ wait_for_vue_resource
+
+ page.within('.add-issues-modal') do
+ expect(find('.add-issues-footer')).not_to have_button(planning.title)
+ expect(find('.add-issues-footer')).to have_button(label.title)
+ end
+ end
+ end
+
+ context 'search' do
+ it 'returns issues' do
+ page.within('.add-issues-modal') do
+ find('.form-control').native.send_keys(issue.title)
+
+ expect(page).to have_selector('.card', count: 1)
+ end
+ end
+
+ it 'returns no issues' do
+ page.within('.add-issues-modal') do
+ find('.form-control').native.send_keys('testing search')
+
+ expect(page).not_to have_selector('.card')
+ expect(page).not_to have_content("You haven't added any issues to your project yet")
+ end
+ end
+ end
+
+ context 'selecing issues' do
+ it 'selects single issue' do
+ page.within('.add-issues-modal') do
+ first('.card').click
+
+ page.within('.nav-links') do
+ expect(page).to have_content('Selected issues 1')
+ end
+ end
+ end
+
+ it 'changes button text' do
+ page.within('.add-issues-modal') do
+ first('.card').click
+
+ expect(first('.add-issues-footer .btn')).to have_content('Add 1 issue')
+ end
+ end
+
+ it 'changes button text with plural' do
+ page.within('.add-issues-modal') do
+ all('.card').each do |el|
+ el.click
+ end
+
+ expect(first('.add-issues-footer .btn')).to have_content('Add 2 issues')
+ end
+ end
+
+ it 'shows only selected issues on selected tab' do
+ page.within('.add-issues-modal') do
+ first('.card').click
+
+ click_link 'Selected issues'
+
+ expect(page).to have_selector('.card', count: 1)
+ end
+ end
+
+ it 'selects all issues' do
+ page.within('.add-issues-modal') do
+ click_button 'Select all'
+
+ expect(page).to have_selector('.is-active', count: 2)
+ end
+ end
+
+ it 'deselects all issues' do
+ page.within('.add-issues-modal') do
+ click_button 'Select all'
+
+ expect(page).to have_selector('.is-active', count: 2)
+
+ click_button 'Deselect all'
+
+ expect(page).not_to have_selector('.is-active')
+ end
+ end
+
+ it 'selects all that arent already selected' do
+ page.within('.add-issues-modal') do
+ first('.card').click
+
+ expect(page).to have_selector('.is-active', count: 1)
+
+ click_button 'Select all'
+
+ expect(page).to have_selector('.is-active', count: 2)
+ end
+ end
+
+ it 'unselects from selected tab' do
+ page.within('.add-issues-modal') do
+ first('.card').click
+
+ click_link 'Selected issues'
+
+ first('.card').click
+
+ expect(page).not_to have_selector('.is-active')
+ end
+ end
+ end
+
+ context 'adding issues' do
+ it 'adds to board' do
+ page.within('.add-issues-modal') do
+ first('.card').click
+
+ click_button 'Add 1 issue'
+ end
+
+ page.within(first('.board')) do
+ expect(page).to have_selector('.card')
+ end
+ end
+
+ it 'adds to second list' do
+ page.within('.add-issues-modal') do
+ first('.card').click
+
+ click_button planning.title
+
+ click_link label.title
+
+ click_button 'Add 1 issue'
+ end
+
+ page.within(find('.board:nth-child(2)')) do
+ expect(page).to have_selector('.card')
+ end
+ end
+ end
+ end
+end
diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb
index 973d5b286e9..34f47daf0e5 100644
--- a/spec/features/boards/boards_spec.rb
+++ b/spec/features/boards/boards_spec.rb
@@ -20,7 +20,7 @@ describe 'Issue Boards', feature: true, js: true do
before do
visit namespace_project_board_path(project.namespace, project, board)
wait_for_vue_resource
- expect(page).to have_selector('.board', count: 3)
+ expect(page).to have_selector('.board', count: 2)
end
it 'shows blank state' do
@@ -31,18 +31,18 @@ describe 'Issue Boards', feature: true, js: true do
page.within(find('.board-blank-state')) do
click_button("Nevermind, I'll use my own")
end
- expect(page).to have_selector('.board', count: 2)
+ expect(page).to have_selector('.board', count: 1)
end
it 'creates default lists' do
- lists = ['Backlog', 'To Do', 'Doing', 'Done']
+ lists = ['To Do', 'Doing', 'Done']
page.within(find('.board-blank-state')) do
click_button('Add default lists')
end
wait_for_vue_resource
- expect(page).to have_selector('.board', count: 4)
+ expect(page).to have_selector('.board', count: 3)
page.all('.board').each_with_index do |list, i|
expect(list.find('.board-title')).to have_content(lists[i])
@@ -64,42 +64,41 @@ describe 'Issue Boards', feature: true, js: true do
let!(:list1) { create(:list, board: board, label: planning, position: 0) }
let!(:list2) { create(:list, board: board, label: development, position: 1) }
- let!(:confidential_issue) { create(:issue, :confidential, project: project, author: user) }
- let!(:issue1) { create(:issue, project: project, assignee: user) }
- let!(:issue2) { create(:issue, project: project, author: user2) }
- let!(:issue3) { create(:issue, project: project) }
- let!(:issue4) { create(:issue, project: project) }
+ let!(:confidential_issue) { create(:labeled_issue, :confidential, project: project, author: user, labels: [planning]) }
+ let!(:issue1) { create(:labeled_issue, project: project, assignee: user, labels: [planning]) }
+ let!(:issue2) { create(:labeled_issue, project: project, author: user2, labels: [planning]) }
+ let!(:issue3) { create(:labeled_issue, project: project, labels: [planning]) }
+ let!(:issue4) { create(:labeled_issue, project: project, labels: [planning]) }
let!(:issue5) { create(:labeled_issue, project: project, labels: [planning], milestone: milestone) }
let!(:issue6) { create(:labeled_issue, project: project, labels: [planning, development]) }
let!(:issue7) { create(:labeled_issue, project: project, labels: [development]) }
let!(:issue8) { create(:closed_issue, project: project) }
- let!(:issue9) { create(:labeled_issue, project: project, labels: [testing, bug, accepting]) }
+ let!(:issue9) { create(:labeled_issue, project: project, labels: [planning, testing, bug, accepting]) }
before do
visit namespace_project_board_path(project.namespace, project, board)
wait_for_vue_resource
- expect(page).to have_selector('.board', count: 4)
+ expect(page).to have_selector('.board', count: 3)
expect(find('.board:nth-child(1)')).to have_selector('.card')
expect(find('.board:nth-child(2)')).to have_selector('.card')
expect(find('.board:nth-child(3)')).to have_selector('.card')
- expect(find('.board:nth-child(4)')).to have_selector('.card')
end
it 'shows lists' do
- expect(page).to have_selector('.board', count: 4)
+ expect(page).to have_selector('.board', count: 3)
end
it 'shows description tooltip on list title' do
- page.within('.board:nth-child(2)') do
+ page.within('.board:nth-child(1)') do
expect(find('.board-title span.has-tooltip')[:title]).to eq('Test')
end
end
it 'shows issues in lists' do
+ wait_for_board_cards(1, 8)
wait_for_board_cards(2, 2)
- wait_for_board_cards(3, 2)
end
it 'shows confidential issues with icon' do
@@ -108,21 +107,8 @@ describe 'Issue Boards', feature: true, js: true do
end
end
- it 'search backlog list' do
- page.within('#js-boards-seach') do
- find('.form-control').set(issue1.title)
- end
-
- wait_for_vue_resource
-
- expect(find('.board:nth-child(1)')).to have_selector('.card', count: 1)
- expect(find('.board:nth-child(2)')).to have_selector('.card', count: 0)
- expect(find('.board:nth-child(3)')).to have_selector('.card', count: 0)
- expect(find('.board:nth-child(4)')).to have_selector('.card', count: 0)
- end
-
it 'search done list' do
- page.within('#js-boards-seach') do
+ page.within('#js-boards-search') do
find('.form-control').set(issue8.title)
end
@@ -130,168 +116,145 @@ describe 'Issue Boards', feature: true, js: true do
expect(find('.board:nth-child(1)')).to have_selector('.card', count: 0)
expect(find('.board:nth-child(2)')).to have_selector('.card', count: 0)
- expect(find('.board:nth-child(3)')).to have_selector('.card', count: 0)
- expect(find('.board:nth-child(4)')).to have_selector('.card', count: 1)
+ expect(find('.board:nth-child(3)')).to have_selector('.card', count: 1)
end
it 'search list' do
- page.within('#js-boards-seach') do
+ page.within('#js-boards-search') do
find('.form-control').set(issue5.title)
end
wait_for_vue_resource
- expect(find('.board:nth-child(1)')).to have_selector('.card', count: 0)
- expect(find('.board:nth-child(2)')).to have_selector('.card', count: 1)
+ expect(find('.board:nth-child(1)')).to have_selector('.card', count: 1)
+ expect(find('.board:nth-child(2)')).to have_selector('.card', count: 0)
expect(find('.board:nth-child(3)')).to have_selector('.card', count: 0)
- expect(find('.board:nth-child(4)')).to have_selector('.card', count: 0)
end
it 'allows user to delete board' do
- page.within(find('.board:nth-child(2)')) do
+ page.within(find('.board:nth-child(1)')) do
find('.board-delete').click
end
wait_for_vue_resource
- expect(page).to have_selector('.board', count: 3)
+ expect(page).to have_selector('.board', count: 2)
end
it 'removes checkmark in new list dropdown after deleting' do
click_button 'Add list'
wait_for_ajax
- page.within(find('.board:nth-child(2)')) do
+ page.within(find('.board:nth-child(1)')) do
find('.board-delete').click
end
wait_for_vue_resource
- expect(page).to have_selector('.board', count: 3)
- expect(find(".js-board-list-#{planning.id}", visible: false)).not_to have_css('.is-active')
+ expect(page).to have_selector('.board', count: 2)
end
it 'infinite scrolls list' do
50.times do
- create(:issue, project: project)
+ create(:labeled_issue, project: project, labels: [planning])
end
visit namespace_project_board_path(project.namespace, project, board)
wait_for_vue_resource
page.within(find('.board', match: :first)) do
- expect(page.find('.board-header')).to have_content('56')
+ expect(page.find('.board-header')).to have_content('58')
expect(page).to have_selector('.card', count: 20)
- expect(page).to have_content('Showing 20 of 56 issues')
+ expect(page).to have_content('Showing 20 of 58 issues')
evaluate_script("document.querySelectorAll('.board .board-list')[0].scrollTop = document.querySelectorAll('.board .board-list')[0].scrollHeight")
wait_for_vue_resource
expect(page).to have_selector('.card', count: 40)
- expect(page).to have_content('Showing 40 of 56 issues')
+ expect(page).to have_content('Showing 40 of 58 issues')
evaluate_script("document.querySelectorAll('.board .board-list')[0].scrollTop = document.querySelectorAll('.board .board-list')[0].scrollHeight")
wait_for_vue_resource
- expect(page).to have_selector('.card', count: 56)
+ expect(page).to have_selector('.card', count: 58)
expect(page).to have_content('Showing all issues')
end
end
- context 'backlog' do
- it 'shows issues in backlog with no labels' do
- wait_for_board_cards(1, 6)
- end
-
- it 'moves issue from backlog into list' do
- drag_to(list_to_index: 1)
-
- wait_for_vue_resource
- wait_for_board_cards(1, 5)
- wait_for_board_cards(2, 3)
- end
- end
-
context 'done' do
it 'shows list of done issues' do
- wait_for_board_cards(4, 1)
+ wait_for_board_cards(3, 1)
wait_for_ajax
end
it 'moves issue to done' do
- drag_to(list_from_index: 0, list_to_index: 3)
+ drag_to(list_from_index: 0, list_to_index: 2)
- wait_for_board_cards(1, 5)
+ wait_for_board_cards(1, 7)
wait_for_board_cards(2, 2)
wait_for_board_cards(3, 2)
- wait_for_board_cards(4, 2)
expect(find('.board:nth-child(1)')).not_to have_content(issue9.title)
- expect(find('.board:nth-child(4)')).to have_selector('.card', count: 2)
- expect(find('.board:nth-child(4)')).to have_content(issue9.title)
- expect(find('.board:nth-child(4)')).not_to have_content(planning.title)
+ expect(find('.board:nth-child(3)')).to have_selector('.card', count: 2)
+ expect(find('.board:nth-child(3)')).to have_content(issue9.title)
+ expect(find('.board:nth-child(3)')).not_to have_content(planning.title)
end
it 'removes all of the same issue to done' do
- drag_to(list_from_index: 1, list_to_index: 3)
+ drag_to(list_from_index: 0, list_to_index: 2)
- wait_for_board_cards(1, 6)
- wait_for_board_cards(2, 1)
- wait_for_board_cards(3, 1)
- wait_for_board_cards(4, 2)
+ wait_for_board_cards(1, 7)
+ wait_for_board_cards(2, 2)
+ wait_for_board_cards(3, 2)
- expect(find('.board:nth-child(2)')).not_to have_content(issue6.title)
- expect(find('.board:nth-child(4)')).to have_content(issue6.title)
- expect(find('.board:nth-child(4)')).not_to have_content(planning.title)
+ expect(find('.board:nth-child(1)')).not_to have_content(issue9.title)
+ expect(find('.board:nth-child(3)')).to have_content(issue9.title)
+ expect(find('.board:nth-child(3)')).not_to have_content(planning.title)
end
end
context 'lists' do
it 'changes position of list' do
- drag_to(list_from_index: 1, list_to_index: 2, selector: '.board-header')
+ drag_to(list_from_index: 1, list_to_index: 0, selector: '.board-header')
- wait_for_board_cards(1, 6)
- wait_for_board_cards(2, 2)
- wait_for_board_cards(3, 2)
- wait_for_board_cards(4, 1)
+ wait_for_board_cards(1, 2)
+ wait_for_board_cards(2, 8)
+ wait_for_board_cards(3, 1)
- expect(find('.board:nth-child(2)')).to have_content(development.title)
- expect(find('.board:nth-child(2)')).to have_content(planning.title)
+ expect(find('.board:nth-child(1)')).to have_content(development.title)
+ expect(find('.board:nth-child(1)')).to have_content(planning.title)
end
it 'issue moves between lists' do
- drag_to(list_from_index: 1, card_index: 1, list_to_index: 2)
+ drag_to(list_from_index: 0, card_index: 1, list_to_index: 1)
- wait_for_board_cards(1, 6)
- wait_for_board_cards(2, 1)
- wait_for_board_cards(3, 3)
- wait_for_board_cards(4, 1)
+ wait_for_board_cards(1, 7)
+ wait_for_board_cards(2, 2)
+ wait_for_board_cards(3, 1)
- expect(find('.board:nth-child(3)')).to have_content(issue6.title)
- expect(find('.board:nth-child(3)').all('.card').last).not_to have_content(development.title)
+ expect(find('.board:nth-child(2)')).to have_content(issue6.title)
+ expect(find('.board:nth-child(2)').all('.card').last).not_to have_content(development.title)
end
it 'issue moves between lists' do
- drag_to(list_from_index: 2, list_to_index: 1)
+ drag_to(list_from_index: 1, list_to_index: 0)
- wait_for_board_cards(1, 6)
- wait_for_board_cards(2, 3)
+ wait_for_board_cards(1, 9)
+ wait_for_board_cards(2, 1)
wait_for_board_cards(3, 1)
- wait_for_board_cards(4, 1)
- expect(find('.board:nth-child(2)')).to have_content(issue7.title)
- expect(find('.board:nth-child(2)').all('.card').first).not_to have_content(planning.title)
+ expect(find('.board:nth-child(1)')).to have_content(issue7.title)
+ expect(find('.board:nth-child(1)').all('.card').first).not_to have_content(planning.title)
end
it 'issue moves from done' do
- drag_to(list_from_index: 3, list_to_index: 1)
+ drag_to(list_from_index: 2, list_to_index: 1)
expect(find('.board:nth-child(2)')).to have_content(issue8.title)
- wait_for_board_cards(1, 6)
+ wait_for_board_cards(1, 8)
wait_for_board_cards(2, 3)
- wait_for_board_cards(3, 2)
- wait_for_board_cards(4, 0)
+ wait_for_board_cards(3, 0)
end
context 'issue card' do
@@ -324,7 +287,7 @@ describe 'Issue Boards', feature: true, js: true do
wait_for_vue_resource
- expect(page).to have_selector('.board', count: 5)
+ expect(page).to have_selector('.board', count: 4)
end
it 'creates new list for Backlog label' do
@@ -337,7 +300,7 @@ describe 'Issue Boards', feature: true, js: true do
wait_for_vue_resource
- expect(page).to have_selector('.board', count: 5)
+ expect(page).to have_selector('.board', count: 4)
end
it 'creates new list for Done label' do
@@ -350,7 +313,7 @@ describe 'Issue Boards', feature: true, js: true do
wait_for_vue_resource
- expect(page).to have_selector('.board', count: 5)
+ expect(page).to have_selector('.board', count: 4)
end
it 'keeps dropdown open after adding new list' do
@@ -366,21 +329,6 @@ describe 'Issue Boards', feature: true, js: true do
expect(find('.issue-boards-search')).to have_selector('.open')
end
- it 'moves issues from backlog into new list' do
- wait_for_board_cards(1, 6)
-
- click_button 'Add list'
- wait_for_ajax
-
- page.within('.dropdown-menu-issues-board-new') do
- click_link testing.title
- end
-
- wait_for_vue_resource
-
- wait_for_board_cards(1, 5)
- end
-
it 'creates new list from a new label' do
click_button 'Add list'
@@ -397,7 +345,7 @@ describe 'Issue Boards', feature: true, js: true do
wait_for_ajax
wait_for_vue_resource
- expect(page).to have_selector('.board', count: 5)
+ expect(page).to have_selector('.board', count: 4)
end
end
end
@@ -418,7 +366,7 @@ describe 'Issue Boards', feature: true, js: true do
wait_for_vue_resource
wait_for_board_cards(1, 1)
- wait_for_empty_boards((2..4))
+ wait_for_empty_boards((2..3))
end
it 'filters by assignee' do
@@ -437,7 +385,7 @@ describe 'Issue Boards', feature: true, js: true do
wait_for_vue_resource
wait_for_board_cards(1, 1)
- wait_for_empty_boards((2..4))
+ wait_for_empty_boards((2..3))
end
it 'filters by milestone' do
@@ -454,10 +402,9 @@ describe 'Issue Boards', feature: true, js: true do
end
wait_for_vue_resource
- wait_for_board_cards(1, 0)
- wait_for_board_cards(2, 1)
+ wait_for_board_cards(1, 1)
+ wait_for_board_cards(2, 0)
wait_for_board_cards(3, 0)
- wait_for_board_cards(4, 0)
end
it 'filters by label' do
@@ -474,7 +421,7 @@ describe 'Issue Boards', feature: true, js: true do
wait_for_vue_resource
wait_for_board_cards(1, 1)
- wait_for_empty_boards((2..4))
+ wait_for_empty_boards((2..3))
end
it 'filters by label with space after reload' do
@@ -530,7 +477,7 @@ describe 'Issue Boards', feature: true, js: true do
it 'infinite scrolls list with label filter' do
50.times do
- create(:labeled_issue, project: project, labels: [testing])
+ create(:labeled_issue, project: project, labels: [planning, testing])
end
page.within '.issues-filters' do
@@ -580,32 +527,12 @@ describe 'Issue Boards', feature: true, js: true do
wait_for_vue_resource
wait_for_board_cards(1, 1)
- wait_for_empty_boards((2..4))
- end
-
- it 'filters by no label' do
- page.within '.issues-filters' do
- click_button('Label')
- wait_for_ajax
-
- page.within '.dropdown-menu-labels' do
- click_link("No Label")
- wait_for_vue_resource
- find('.dropdown-menu-close').click
- end
- end
-
- wait_for_vue_resource
-
- wait_for_board_cards(1, 5)
- wait_for_board_cards(2, 0)
- wait_for_board_cards(3, 0)
- wait_for_board_cards(4, 1)
+ wait_for_empty_boards((2..3))
end
it 'filters by clicking label button on issue' do
page.within(find('.board', match: :first)) do
- expect(page).to have_selector('.card', count: 6)
+ expect(page).to have_selector('.card', count: 8)
expect(find('.card', match: :first)).to have_content(bug.title)
click_button(bug.title)
wait_for_vue_resource
@@ -614,7 +541,7 @@ describe 'Issue Boards', feature: true, js: true do
wait_for_vue_resource
wait_for_board_cards(1, 1)
- wait_for_empty_boards((2..4))
+ wait_for_empty_boards((2..3))
page.within('.labels-filter') do
expect(find('.dropdown-toggle-text')).to have_content(bug.title)
diff --git a/spec/features/boards/modal_filter_spec.rb b/spec/features/boards/modal_filter_spec.rb
new file mode 100644
index 00000000000..1cf0d11d448
--- /dev/null
+++ b/spec/features/boards/modal_filter_spec.rb
@@ -0,0 +1,259 @@
+require 'rails_helper'
+
+describe 'Issue Boards add issue modal filtering', :feature, :js do
+ include WaitForAjax
+ include WaitForVueResource
+
+ let(:project) { create(:empty_project, :public) }
+ let(:board) { create(:board, project: project) }
+ let(:planning) { create(:label, project: project, name: 'Planning') }
+ let!(:list1) { create(:list, board: board, label: planning, position: 0) }
+ let(:user) { create(:user) }
+ let(:user2) { create(:user) }
+ let!(:issue1) { create(:issue, project: project) }
+
+ before do
+ project.team << [user, :master]
+
+ login_as(user)
+ end
+
+ it 'shows empty state when no results found' do
+ visit_board
+
+ page.within('.add-issues-modal') do
+ find('.form-control').native.send_keys('testing empty state')
+
+ wait_for_vue_resource
+
+ expect(page).to have_content('There are no issues to show.')
+ end
+ end
+
+ it 'restores filters when closing' do
+ visit_board
+
+ page.within('.add-issues-modal') do
+ click_button 'Milestone'
+
+ wait_for_ajax
+
+ click_link 'Upcoming'
+
+ wait_for_vue_resource
+
+ expect(page).to have_selector('.card', count: 0)
+
+ click_button 'Cancel'
+ end
+
+ click_button('Add issues')
+
+ page.within('.add-issues-modal') do
+ wait_for_vue_resource
+
+ expect(page).to have_selector('.card', count: 1)
+ end
+ end
+
+ context 'author' do
+ let!(:issue) { create(:issue, project: project, author: user2) }
+
+ before do
+ project.team << [user2, :developer]
+
+ visit_board
+ end
+
+ it 'filters by any author' do
+ page.within('.add-issues-modal') do
+ click_button 'Author'
+
+ wait_for_ajax
+
+ click_link 'Any Author'
+
+ wait_for_vue_resource
+
+ expect(page).to have_selector('.card', count: 2)
+ end
+ end
+
+ it 'filters by selected user' do
+ page.within('.add-issues-modal') do
+ click_button 'Author'
+
+ wait_for_ajax
+
+ click_link user2.name
+
+ wait_for_vue_resource
+
+ expect(page).to have_selector('.card', count: 1)
+ end
+ end
+ end
+
+ context 'assignee' do
+ let!(:issue) { create(:issue, project: project, assignee: user2) }
+
+ before do
+ project.team << [user2, :developer]
+
+ visit_board
+ end
+
+ it 'filters by any assignee' do
+ page.within('.add-issues-modal') do
+ click_button 'Assignee'
+
+ wait_for_ajax
+
+ click_link 'Any Assignee'
+
+ wait_for_vue_resource
+
+ expect(page).to have_selector('.card', count: 2)
+ end
+ end
+
+ it 'filters by unassigned' do
+ page.within('.add-issues-modal') do
+ click_button 'Assignee'
+
+ wait_for_ajax
+
+ click_link 'Unassigned'
+
+ wait_for_vue_resource
+
+ expect(page).to have_selector('.card', count: 1)
+ end
+ end
+
+ it 'filters by selected user' do
+ page.within('.add-issues-modal') do
+ click_button 'Assignee'
+
+ wait_for_ajax
+
+ page.within '.dropdown-menu-user' do
+ click_link user2.name
+ end
+
+ wait_for_vue_resource
+
+ expect(page).to have_selector('.card', count: 1)
+ end
+ end
+ end
+
+ context 'milestone' do
+ let(:milestone) { create(:milestone, project: project) }
+ let!(:issue) { create(:issue, project: project, milestone: milestone) }
+
+ before do
+ visit_board
+ end
+
+ it 'filters by any milestone' do
+ page.within('.add-issues-modal') do
+ click_button 'Milestone'
+
+ wait_for_ajax
+
+ click_link 'Any Milestone'
+
+ wait_for_vue_resource
+
+ expect(page).to have_selector('.card', count: 2)
+ end
+ end
+
+ it 'filters by upcoming milestone' do
+ page.within('.add-issues-modal') do
+ click_button 'Milestone'
+
+ wait_for_ajax
+
+ click_link 'Upcoming'
+
+ wait_for_vue_resource
+
+ expect(page).to have_selector('.card', count: 0)
+ end
+ end
+
+ it 'filters by selected milestone' do
+ page.within('.add-issues-modal') do
+ click_button 'Milestone'
+
+ wait_for_ajax
+
+ click_link milestone.name
+
+ wait_for_vue_resource
+
+ expect(page).to have_selector('.card', count: 1)
+ end
+ end
+ end
+
+ context 'label' do
+ let(:label) { create(:label, project: project) }
+ let!(:issue) { create(:labeled_issue, project: project, labels: [label]) }
+
+ before do
+ visit_board
+ end
+
+ it 'filters by any label' do
+ page.within('.add-issues-modal') do
+ click_button 'Label'
+
+ wait_for_ajax
+
+ click_link 'Any Label'
+
+ wait_for_vue_resource
+
+ expect(page).to have_selector('.card', count: 2)
+ end
+ end
+
+ it 'filters by no label' do
+ page.within('.add-issues-modal') do
+ click_button 'Label'
+
+ wait_for_ajax
+
+ click_link 'No Label'
+
+ wait_for_vue_resource
+
+ expect(page).to have_selector('.card', count: 1)
+ end
+ end
+
+ it 'filters by label' do
+ page.within('.add-issues-modal') do
+ click_button 'Label'
+
+ wait_for_ajax
+
+ click_link label.title
+
+ wait_for_vue_resource
+
+ expect(page).to have_selector('.card', count: 1)
+ end
+ end
+ end
+
+ def visit_board
+ visit namespace_project_board_path(project.namespace, project, board)
+ wait_for_vue_resource
+
+ click_button('Add issues')
+ end
+end
diff --git a/spec/features/boards/new_issue_spec.rb b/spec/features/boards/new_issue_spec.rb
index a03cd6fbf2d..6d14a8cf483 100644
--- a/spec/features/boards/new_issue_spec.rb
+++ b/spec/features/boards/new_issue_spec.rb
@@ -6,6 +6,7 @@ describe 'Issue Boards new issue', feature: true, js: true do
let(:project) { create(:empty_project, :public) }
let(:board) { create(:board, project: project) }
+ let!(:list) { create(:list, board: board, position: 0) }
let(:user) { create(:user) }
context 'authorized user' do
@@ -17,7 +18,7 @@ describe 'Issue Boards new issue', feature: true, js: true do
visit namespace_project_board_path(project.namespace, project, board)
wait_for_vue_resource
- expect(page).to have_selector('.board', count: 3)
+ expect(page).to have_selector('.board', count: 2)
end
it 'displays new issue button' do
@@ -25,7 +26,7 @@ describe 'Issue Boards new issue', feature: true, js: true do
end
it 'does not display new issue button in done list' do
- page.within('.board:nth-child(3)') do
+ page.within('.board:nth-child(2)') do
expect(page).not_to have_selector('.board-issue-count-holder .btn')
end
end
diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb
index c16aafa1470..9cc50167395 100644
--- a/spec/features/boards/sidebar_spec.rb
+++ b/spec/features/boards/sidebar_spec.rb
@@ -4,14 +4,17 @@ describe 'Issue Boards', feature: true, js: true do
include WaitForAjax
include WaitForVueResource
- let(:project) { create(:empty_project, :public) }
- let(:board) { create(:board, project: project) }
- let(:user) { create(:user) }
- let!(:label) { create(:label, project: project) }
- let!(:label2) { create(:label, project: project) }
- let!(:milestone) { create(:milestone, project: project) }
- let!(:issue2) { create(:labeled_issue, project: project, assignee: user, milestone: milestone, labels: [label]) }
- let!(:issue) { create(:issue, project: project) }
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project, :public) }
+ let!(:milestone) { create(:milestone, project: project) }
+ let!(:development) { create(:label, project: project, name: 'Development') }
+ let!(:bug) { create(:label, project: project, name: 'Bug') }
+ let!(:regression) { create(:label, project: project, name: 'Regression') }
+ let!(:stretch) { create(:label, project: project, name: 'Stretch') }
+ let!(:issue1) { create(:labeled_issue, project: project, assignee: user, milestone: milestone, labels: [development]) }
+ let!(:issue2) { create(:labeled_issue, project: project, labels: [development, stretch]) }
+ let(:board) { create(:board, project: project) }
+ let!(:list) { create(:list, board: board, label: development, position: 0) }
before do
project.team << [user, :master]
@@ -62,8 +65,22 @@ describe 'Issue Boards', feature: true, js: true do
end
page.within('.issue-boards-sidebar') do
- expect(page).to have_content(issue.title)
- expect(page).to have_content(issue.to_reference)
+ expect(page).to have_content(issue2.title)
+ expect(page).to have_content(issue2.to_reference)
+ end
+ end
+
+ it 'removes card from board when clicking remove button' do
+ page.within(first('.board')) do
+ first('.card').click
+ end
+
+ page.within('.issue-boards-sidebar') do
+ click_button 'Remove from board'
+ end
+
+ page.within(first('.board')) do
+ expect(page).to have_selector('.card', count: 1)
end
end
@@ -125,7 +142,9 @@ describe 'Issue Boards', feature: true, js: true do
first('.card').click
end
- page.within('.assignee') do
+ page.within(find('.assignee')) do
+ expect(page).to have_content('No assignee')
+
click_link 'assign yourself'
wait_for_vue_resource
@@ -139,6 +158,36 @@ describe 'Issue Boards', feature: true, js: true do
end
end
end
+
+ it 'resets assignee dropdown' do
+ page.within(first('.board')) do
+ first('.card').click
+ end
+
+ page.within('.assignee') do
+ click_link 'Edit'
+
+ wait_for_ajax
+
+ page.within('.dropdown-menu-user') do
+ click_link user.name
+
+ wait_for_vue_resource
+ end
+
+ expect(page).to have_content(user.name)
+ end
+
+ page.within(first('.board')) do
+ find('.card:nth-child(2)').click
+ end
+
+ page.within('.assignee') do
+ click_link 'Edit'
+
+ expect(page).not_to have_selector('.is-active')
+ end
+ end
end
context 'milestone' do
@@ -212,22 +261,22 @@ describe 'Issue Boards', feature: true, js: true do
wait_for_ajax
- click_link label.title
+ click_link bug.title
wait_for_vue_resource
find('.dropdown-menu-close-icon').click
page.within('.value') do
- expect(page).to have_selector('.label', count: 1)
- expect(page).to have_content(label.title)
+ expect(page).to have_selector('.label', count: 3)
+ expect(page).to have_content(bug.title)
end
end
page.within(first('.board')) do
page.within(first('.card')) do
- expect(page).to have_selector('.label', count: 1)
- expect(page).to have_content(label.title)
+ expect(page).to have_selector('.label', count: 2)
+ expect(page).to have_content(bug.title)
end
end
end
@@ -242,32 +291,32 @@ describe 'Issue Boards', feature: true, js: true do
wait_for_ajax
- click_link label.title
- click_link label2.title
+ click_link bug.title
+ click_link regression.title
wait_for_vue_resource
find('.dropdown-menu-close-icon').click
page.within('.value') do
- expect(page).to have_selector('.label', count: 2)
- expect(page).to have_content(label.title)
- expect(page).to have_content(label2.title)
+ expect(page).to have_selector('.label', count: 4)
+ expect(page).to have_content(bug.title)
+ expect(page).to have_content(regression.title)
end
end
page.within(first('.board')) do
page.within(first('.card')) do
- expect(page).to have_selector('.label', count: 2)
- expect(page).to have_content(label.title)
- expect(page).to have_content(label2.title)
+ expect(page).to have_selector('.label', count: 3)
+ expect(page).to have_content(bug.title)
+ expect(page).to have_content(regression.title)
end
end
end
it 'removes a label' do
page.within(first('.board')) do
- find('.card:nth-child(2)').click
+ first('.card').click
end
page.within('.labels') do
@@ -275,22 +324,22 @@ describe 'Issue Boards', feature: true, js: true do
wait_for_ajax
- click_link label.title
+ click_link stretch.title
wait_for_vue_resource
find('.dropdown-menu-close-icon').click
page.within('.value') do
- expect(page).to have_selector('.label', count: 0)
- expect(page).not_to have_content(label.title)
+ expect(page).to have_selector('.label', count: 1)
+ expect(page).not_to have_content(stretch.title)
end
end
page.within(first('.board')) do
- page.within(find('.card:nth-child(2)')) do
- expect(page).not_to have_selector('.label', count: 1)
- expect(page).not_to have_content(label.title)
+ page.within(first('.card')) do
+ expect(page).not_to have_selector('.label')
+ expect(page).not_to have_content(stretch.title)
end
end
end
diff --git a/spec/features/ci_lint_spec.rb b/spec/features/ci_lint_spec.rb
index 81077f4b005..3ebc432206a 100644
--- a/spec/features/ci_lint_spec.rb
+++ b/spec/features/ci_lint_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe 'CI Lint' do
+describe 'CI Lint', js: true do
before do
login_as :user
end
@@ -8,7 +8,10 @@ describe 'CI Lint' do
describe 'YAML parsing' do
before do
visit ci_lint_path
- fill_in 'content', with: yaml_content
+ # Ace editor updates a hidden textarea and it happens asynchronously
+ # `sleep 0.1` is actually needed here because of this
+ execute_script("ace.edit('ci-editor').setValue(" + yaml_content.to_json + ");")
+ sleep 0.1
click_on 'Validate'
end
@@ -40,7 +43,7 @@ describe 'CI Lint' do
let(:yaml_content) { 'my yaml content' }
it 'loads previous YAML content after validation' do
- expect(page).to have_field('content', with: 'my yaml content', type: 'textarea')
+ expect(page).to have_field('content', with: 'my yaml content', visible: false, type: 'textarea')
end
end
end
diff --git a/spec/features/copy_as_gfm_spec.rb b/spec/features/copy_as_gfm_spec.rb
new file mode 100644
index 00000000000..f3a5b565122
--- /dev/null
+++ b/spec/features/copy_as_gfm_spec.rb
@@ -0,0 +1,432 @@
+require 'spec_helper'
+
+describe 'Copy as GFM', feature: true, js: true do
+ include GitlabMarkdownHelper
+ include ActionView::Helpers::JavaScriptHelper
+
+ before do
+ @feat = MarkdownFeature.new
+
+ # `markdown` helper expects a `@project` variable
+ @project = @feat.project
+
+ visit namespace_project_issue_path(@project.namespace, @project, @feat.issue)
+ end
+
+ # The filters referenced in lib/banzai/pipeline/gfm_pipeline.rb convert GitLab Flavored Markdown (GFM) to HTML.
+ # The handlers defined in app/assets/javascripts/copy_as_gfm.js.es6 consequently convert that same HTML to GFM.
+ # To make sure these filters and handlers are properly aligned, this spec tests the GFM-to-HTML-to-GFM cycle
+ # by verifying (`html_to_gfm(gfm_to_html(gfm)) == gfm`) for a number of examples of GFM for every filter, using the `verify` helper.
+
+ # These are all in a single `it` for performance reasons.
+ it 'works', :aggregate_failures do
+ verify(
+ 'nesting',
+
+ '> 1. [x] **[$`2 + 2`$ {-=-}{+=+} 2^2 ~~:thumbsup:~~](http://google.com)**'
+ )
+
+ verify(
+ 'a real world example from the gitlab-ce README',
+
+ <<-GFM.strip_heredoc
+ # GitLab
+
+ [![Build status](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/build.svg)](https://gitlab.com/gitlab-org/gitlab-ce/commits/master)
+ [![CE coverage report](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg?job=coverage)](https://gitlab-org.gitlab.io/gitlab-ce/coverage-ruby)
+ [![Code Climate](https://codeclimate.com/github/gitlabhq/gitlabhq.svg)](https://codeclimate.com/github/gitlabhq/gitlabhq)
+ [![Core Infrastructure Initiative Best Practices](https://bestpractices.coreinfrastructure.org/projects/42/badge)](https://bestpractices.coreinfrastructure.org/projects/42)
+
+ ## Canonical source
+
+ The canonical source of GitLab Community Edition is [hosted on GitLab.com](https://gitlab.com/gitlab-org/gitlab-ce/).
+
+ ## Open source software to collaborate on code
+
+ To see how GitLab looks please see the [features page on our website](https://about.gitlab.com/features/).
+
+
+ - Manage Git repositories with fine grained access controls that keep your code secure
+
+ - Perform code reviews and enhance collaboration with merge requests
+
+ - Complete continuous integration (CI) and CD pipelines to builds, test, and deploy your applications
+
+ - Each project can also have an issue tracker, issue board, and a wiki
+
+ - Used by more than 100,000 organizations, GitLab is the most popular solution to manage Git repositories on-premises
+
+ - Completely free and open source (MIT Expat license)
+ GFM
+ )
+
+ verify(
+ 'InlineDiffFilter',
+
+ '{-Deleted text-}',
+ '{+Added text+}'
+ )
+
+ verify(
+ 'TaskListFilter',
+
+ '- [ ] Unchecked task',
+ '- [x] Checked task',
+ '1. [ ] Unchecked numbered task',
+ '1. [x] Checked numbered task'
+ )
+
+ verify(
+ 'ReferenceFilter',
+
+ # issue reference
+ @feat.issue.to_reference,
+ # full issue reference
+ @feat.issue.to_reference(full: true),
+ # issue URL
+ namespace_project_issue_url(@project.namespace, @project, @feat.issue),
+ # issue URL with note anchor
+ namespace_project_issue_url(@project.namespace, @project, @feat.issue, anchor: 'note_123'),
+ # issue link
+ "[Issue](#{namespace_project_issue_url(@project.namespace, @project, @feat.issue)})",
+ # issue link with note anchor
+ "[Issue](#{namespace_project_issue_url(@project.namespace, @project, @feat.issue, anchor: 'note_123')})",
+ )
+
+ verify(
+ 'AutolinkFilter',
+
+ 'https://example.com'
+ )
+
+ verify(
+ 'TableOfContentsFilter',
+
+ '[[_TOC_]]'
+ )
+
+ verify(
+ 'EmojiFilter',
+
+ ':thumbsup:'
+ )
+
+ verify(
+ 'ImageLinkFilter',
+
+ '![Image](https://example.com/image.png)'
+ )
+
+ verify(
+ 'VideoLinkFilter',
+
+ '![Video](https://example.com/video.mp4)'
+ )
+
+ verify(
+ 'MathFilter: math as converted from GFM to HTML',
+
+ '$`c = \pm\sqrt{a^2 + b^2}`$',
+
+ # math block
+ <<-GFM.strip_heredoc
+ ```math
+ c = \pm\sqrt{a^2 + b^2}
+ ```
+ GFM
+ )
+
+ aggregate_failures('MathFilter: math as transformed from HTML to KaTeX') do
+ gfm = '$`c = \pm\sqrt{a^2 + b^2}`$'
+
+ html = <<-HTML.strip_heredoc
+ <span class="katex">
+ <span class="katex-mathml">
+ <math>
+ <semantics>
+ <mrow>
+ <mi>c</mi>
+ <mo>=</mo>
+ <mo>±</mo>
+ <msqrt>
+ <mrow>
+ <msup>
+ <mi>a</mi>
+ <mn>2</mn>
+ </msup>
+ <mo>+</mo>
+ <msup>
+ <mi>b</mi>
+ <mn>2</mn>
+ </msup>
+ </mrow>
+ </msqrt>
+ </mrow>
+ <annotation encoding="application/x-tex">c = \\pm\\sqrt{a^2 + b^2}</annotation>
+ </semantics>
+ </math>
+ </span>
+ <span class="katex-html" aria-hidden="true">
+ <span class="strut" style="height: 0.913389em;"></span>
+ <span class="strut bottom" style="height: 1.04em; vertical-align: -0.126611em;"></span>
+ <span class="base textstyle uncramped">
+ <span class="mord mathit">c</span>
+ <span class="mrel">=</span>
+ <span class="mord">±</span>
+ <span class="sqrt mord"><span class="sqrt-sign" style="top: -0.073389em;">
+ <span class="style-wrap reset-textstyle textstyle uncramped">√</span>
+ </span>
+ <span class="vlist">
+ <span class="" style="top: 0em;">
+ <span class="fontsize-ensurer reset-size5 size5">
+ <span class="" style="font-size: 1em;">​</span>
+ </span>
+ <span class="mord textstyle cramped">
+ <span class="mord">
+ <span class="mord mathit">a</span>
+ <span class="msupsub">
+ <span class="vlist">
+ <span class="" style="top: -0.289em; margin-right: 0.05em;">
+ <span class="fontsize-ensurer reset-size5 size5">
+ <span class="" style="font-size: 0em;">​</span>
+ </span>
+ <span class="reset-textstyle scriptstyle cramped">
+ <span class="mord mathrm">2</span>
+ </span>
+ </span>
+ <span class="baseline-fix">
+ <span class="fontsize-ensurer reset-size5 size5">
+ <span class="" style="font-size: 0em;">​</span>
+ </span>
+ ​</span>
+ </span>
+ </span>
+ </span>
+ <span class="mbin">+</span>
+ <span class="mord">
+ <span class="mord mathit">b</span>
+ <span class="msupsub">
+ <span class="vlist">
+ <span class="" style="top: -0.289em; margin-right: 0.05em;">
+ <span class="fontsize-ensurer reset-size5 size5">
+ <span class="" style="font-size: 0em;">​</span>
+ </span>
+ <span class="reset-textstyle scriptstyle cramped">
+ <span class="mord mathrm">2</span>
+ </span>
+ </span>
+ <span class="baseline-fix">
+ <span class="fontsize-ensurer reset-size5 size5">
+ <span class="" style="font-size: 0em;">​</span>
+ </span>
+ ​</span>
+ </span>
+ </span>
+ </span>
+ </span>
+ </span>
+ <span class="" style="top: -0.833389em;">
+ <span class="fontsize-ensurer reset-size5 size5">
+ <span class="" style="font-size: 1em;">​</span>
+ </span>
+ <span class="reset-textstyle textstyle uncramped sqrt-line"></span>
+ </span>
+ <span class="baseline-fix">
+ <span class="fontsize-ensurer reset-size5 size5">
+ <span class="" style="font-size: 1em;">​</span>
+ </span>
+ ​</span>
+ </span>
+ </span>
+ </span>
+ </span>
+ </span>
+ HTML
+
+ output_gfm = html_to_gfm(html)
+ expect(output_gfm.strip).to eq(gfm.strip)
+ end
+
+ verify(
+ 'SanitizationFilter',
+
+ <<-GFM.strip_heredoc
+ <sub>sub</sub>
+
+ <dl>
+ <dt>dt</dt>
+ <dd>dd</dd>
+ </dl>
+
+ <kbd>kbd</kbd>
+
+ <q>q</q>
+
+ <samp>samp</samp>
+
+ <var>var</var>
+
+ <ruby>ruby</ruby>
+
+ <rt>rt</rt>
+
+ <rp>rp</rp>
+
+ <abbr>abbr</abbr>
+ GFM
+ )
+
+ verify(
+ 'SanitizationFilter',
+
+ <<-GFM.strip_heredoc,
+ ```
+ Plain text
+ ```
+ GFM
+
+ <<-GFM.strip_heredoc,
+ ```ruby
+ def foo
+ bar
+ end
+ ```
+ GFM
+
+ <<-GFM.strip_heredoc
+ Foo
+
+ This is an example of GFM
+
+ ```js
+ Code goes here
+ ```
+ GFM
+ )
+
+ verify(
+ 'MarkdownFilter',
+
+ "Line with two spaces at the end \nto insert a linebreak",
+
+ '`code`',
+ '`` code with ` ticks ``',
+
+ '> Quote',
+
+ # multiline quote
+ <<-GFM.strip_heredoc,
+ > Multiline
+ > Quote
+ >
+ > With multiple paragraphs
+ GFM
+
+ '![Image](https://example.com/image.png)',
+
+ '# Heading with no anchor link',
+
+ '[Link](https://example.com)',
+
+ '- List item',
+
+ # multiline list item
+ <<-GFM.strip_heredoc,
+ - Multiline
+ List item
+ GFM
+
+ # nested lists
+ <<-GFM.strip_heredoc,
+ - Nested
+
+
+ - Lists
+ GFM
+
+ # list with blockquote
+ <<-GFM.strip_heredoc,
+ - List
+
+ > Blockquote
+ GFM
+
+ '1. Numbered list item',
+
+ # multiline numbered list item
+ <<-GFM.strip_heredoc,
+ 1. Multiline
+ Numbered list item
+ GFM
+
+ # nested numbered list
+ <<-GFM.strip_heredoc,
+ 1. Nested
+
+
+ 1. Numbered lists
+ GFM
+
+ '# Heading',
+ '## Heading',
+ '### Heading',
+ '#### Heading',
+ '##### Heading',
+ '###### Heading',
+
+ '**Bold**',
+
+ '_Italics_',
+
+ '~~Strikethrough~~',
+
+ '2^2',
+
+ '-----',
+
+ # table
+ <<-GFM.strip_heredoc,
+ | Centered | Right | Left |
+ |:--------:|------:|------|
+ | Foo | Bar | **Baz** |
+ | Foo | Bar | **Baz** |
+ GFM
+
+ # table with empty heading
+ <<-GFM.strip_heredoc,
+ | | x | y |
+ |---|---|---|
+ | a | 1 | 0 |
+ | b | 0 | 1 |
+ GFM
+ )
+ end
+
+ alias_method :gfm_to_html, :markdown
+
+ def html_to_gfm(html)
+ js = <<-JS.strip_heredoc
+ (function(html) {
+ var node = document.createElement('div');
+ node.innerHTML = html;
+ return window.gl.CopyAsGFM.nodeToGFM(node);
+ })("#{escape_javascript(html)}")
+ JS
+ page.evaluate_script(js)
+ end
+
+ def verify(label, *gfms)
+ aggregate_failures(label) do
+ gfms.each do |gfm|
+ html = gfm_to_html(gfm)
+ output_gfm = html_to_gfm(html)
+ expect(output_gfm.strip).to eq(gfm.strip)
+ end
+ end
+ end
+
+ # Fake a `current_user` helper
+ def current_user
+ @feat.user
+ end
+end
diff --git a/spec/features/cycle_analytics_spec.rb b/spec/features/cycle_analytics_spec.rb
new file mode 100644
index 00000000000..0648c89a5c7
--- /dev/null
+++ b/spec/features/cycle_analytics_spec.rb
@@ -0,0 +1,125 @@
+require 'spec_helper'
+
+feature 'Cycle Analytics', feature: true, js: true do
+ include WaitForAjax
+
+ let(:user) { create(:user) }
+ let(:guest) { create(:user) }
+ let(:project) { create(:project) }
+ let(:issue) { create(:issue, project: project, created_at: 2.days.ago) }
+ let(:milestone) { create(:milestone, project: project) }
+ let(:mr) { create_merge_request_closing_issue(issue) }
+ let(:pipeline) { create(:ci_empty_pipeline, status: 'created', project: project, ref: mr.source_branch, sha: mr.source_branch_sha) }
+
+ context 'as an allowed user' do
+ context 'when project is new' do
+ before do
+ project.team << [user, :master]
+ login_as(user)
+ visit namespace_project_cycle_analytics_path(project.namespace, project)
+ wait_for_ajax
+ end
+
+ it 'shows introductory message' do
+ expect(page).to have_content('Introducing Cycle Analytics')
+ end
+
+ it 'shows active stage with empty message' do
+ expect(page).to have_selector('.stage-nav-item.active', text: 'Issue')
+ expect(page).to have_content("We don't have enough data to show this stage.")
+ end
+ end
+
+ context "when there's cycle analytics data" do
+ before do
+ project.team << [user, :master]
+
+ allow_any_instance_of(Gitlab::ReferenceExtractor).to receive(:issues).and_return([issue])
+ create_cycle
+ deploy_master
+
+ login_as(user)
+ visit namespace_project_cycle_analytics_path(project.namespace, project)
+ end
+
+ it 'shows data on each stage' do
+ expect_issue_to_be_present
+
+ click_stage('Plan')
+ expect(find('.stage-events')).to have_content(mr.commits.last.title)
+
+ click_stage('Code')
+ expect_merge_request_to_be_present
+
+ click_stage('Test')
+ expect_build_to_be_present
+
+ click_stage('Review')
+ expect_merge_request_to_be_present
+
+ click_stage('Staging')
+ expect_build_to_be_present
+
+ click_stage('Production')
+ expect_issue_to_be_present
+ end
+ end
+ end
+
+ context "as a guest" do
+ before do
+ project.team << [guest, :guest]
+
+ allow_any_instance_of(Gitlab::ReferenceExtractor).to receive(:issues).and_return([issue])
+ create_cycle
+ deploy_master
+
+ login_as(guest)
+ visit namespace_project_cycle_analytics_path(project.namespace, project)
+ wait_for_ajax
+ end
+
+ it 'needs permissions to see restricted stages' do
+ expect(find('.stage-events')).to have_content(issue.title)
+
+ click_stage('Code')
+ expect(find('.stage-events')).to have_content('You need permission.')
+
+ click_stage('Review')
+ expect(find('.stage-events')).to have_content('You need permission.')
+ end
+ end
+
+ def expect_issue_to_be_present
+ expect(find('.stage-events')).to have_content(issue.title)
+ expect(find('.stage-events')).to have_content(issue.author.name)
+ expect(find('.stage-events')).to have_content("##{issue.iid}")
+ end
+
+ def expect_build_to_be_present
+ expect(find('.stage-events')).to have_content(@build.ref)
+ expect(find('.stage-events')).to have_content(@build.short_sha)
+ expect(find('.stage-events')).to have_content("##{@build.id}")
+ end
+
+ def expect_merge_request_to_be_present
+ expect(find('.stage-events')).to have_content(mr.title)
+ expect(find('.stage-events')).to have_content(mr.author.name)
+ expect(find('.stage-events')).to have_content("!#{mr.iid}")
+ end
+
+ def create_cycle
+ issue.update(milestone: milestone)
+ pipeline.run
+
+ @build = create(:ci_build, pipeline: pipeline, status: :success, author: user)
+
+ merge_merge_requests_closing_issue(issue)
+ ProcessCommitWorker.new.perform(project.id, user.id, mr.commits.last.to_hash)
+ end
+
+ def click_stage(stage_name)
+ find('.stage-nav li', text: stage_name).click
+ wait_for_ajax
+ end
+end
diff --git a/spec/features/dashboard/active_tab_spec.rb b/spec/features/dashboard/active_tab_spec.rb
new file mode 100644
index 00000000000..7d59fcac517
--- /dev/null
+++ b/spec/features/dashboard/active_tab_spec.rb
@@ -0,0 +1,46 @@
+require 'spec_helper'
+
+RSpec.describe 'Dashboard Active Tab', feature: true do
+ before do
+ login_as :user
+ end
+
+ shared_examples 'page has active tab' do |title|
+ it "#{title} tab" do
+ expect(page).to have_selector('.nav-sidebar li.active', count: 1)
+ expect(find('.nav-sidebar li.active')).to have_content(title)
+ end
+ end
+
+ context 'on dashboard projects' do
+ before do
+ visit dashboard_projects_path
+ end
+
+ it_behaves_like 'page has active tab', 'Projects'
+ end
+
+ context 'on dashboard issues' do
+ before do
+ visit issues_dashboard_path
+ end
+
+ it_behaves_like 'page has active tab', 'Issues'
+ end
+
+ context 'on dashboard merge requests' do
+ before do
+ visit merge_requests_dashboard_path
+ end
+
+ it_behaves_like 'page has active tab', 'Merge Requests'
+ end
+
+ context 'on dashboard groups' do
+ before do
+ visit dashboard_groups_path
+ end
+
+ it_behaves_like 'page has active tab', 'Groups'
+ end
+end
diff --git a/spec/features/dashboard/archived_projects_spec.rb b/spec/features/dashboard/archived_projects_spec.rb
new file mode 100644
index 00000000000..038c1641be9
--- /dev/null
+++ b/spec/features/dashboard/archived_projects_spec.rb
@@ -0,0 +1,28 @@
+require 'spec_helper'
+
+RSpec.describe 'Dashboard Archived Project', feature: true do
+ let(:user) { create :user }
+ let(:project) { create :project}
+ let(:archived_project) { create(:project, :archived) }
+
+ before do
+ project.team << [user, :master]
+ archived_project.team << [user, :master]
+
+ login_as(user)
+
+ visit dashboard_projects_path
+ end
+
+ it 'renders non archived projects' do
+ expect(page).to have_link(project.name)
+ expect(page).not_to have_link(archived_project.name)
+ end
+
+ it 'renders all projects' do
+ click_link 'Show archived projects'
+
+ expect(page).to have_link(project.name)
+ expect(page).to have_link(archived_project.name)
+ end
+end
diff --git a/spec/features/dashboard/datetime_on_tooltips_spec.rb b/spec/features/dashboard/datetime_on_tooltips_spec.rb
index 44dfc2dff45..dc9d09fa396 100644
--- a/spec/features/dashboard/datetime_on_tooltips_spec.rb
+++ b/spec/features/dashboard/datetime_on_tooltips_spec.rb
@@ -6,7 +6,7 @@ feature 'Tooltips on .timeago dates', feature: true, js: true do
let(:user) { create(:user) }
let(:project) { create(:project, name: 'test', namespace: user.namespace) }
let(:created_date) { Date.yesterday.to_time }
- let(:expected_format) { created_date.strftime('%b %-d, %Y %l:%M%P UTC') }
+ let(:expected_format) { created_date.strftime('%b %-d, %Y %l:%M%P') }
context 'on the activity tab' do
before do
diff --git a/spec/features/dashboard/group_spec.rb b/spec/features/dashboard/group_spec.rb
new file mode 100644
index 00000000000..d5f8470fab0
--- /dev/null
+++ b/spec/features/dashboard/group_spec.rb
@@ -0,0 +1,20 @@
+require 'spec_helper'
+
+RSpec.describe 'Dashboard Group', feature: true do
+ before do
+ login_as(:user)
+ end
+
+ it 'creates new grpup' do
+ visit dashboard_groups_path
+ click_link 'New Group'
+
+ fill_in 'group_path', with: 'Samurai'
+ fill_in 'group_description', with: 'Tokugawa Shogunate'
+ click_button 'Create group'
+
+ expect(current_path).to eq group_path(Group.find_by(name: 'Samurai'))
+ expect(page).to have_content('Samurai')
+ expect(page).to have_content('Tokugawa Shogunate')
+ end
+end
diff --git a/spec/features/dashboard/help_spec.rb b/spec/features/dashboard/help_spec.rb
new file mode 100644
index 00000000000..2803f7ec62b
--- /dev/null
+++ b/spec/features/dashboard/help_spec.rb
@@ -0,0 +1,17 @@
+require 'spec_helper'
+
+RSpec.describe 'Dashboard Help', feature: true do
+ before do
+ login_as(:user)
+ end
+
+ it 'renders correctly markdown' do
+ visit help_page_path("administration/raketasks/maintenance")
+
+ expect(page).to have_content('Gather information about GitLab and the system it runs on')
+
+ node = find('.documentation h2 a#user-content-check-gitlab-configuration')
+ expect(node[:href]).to eq '#check-gitlab-configuration'
+ expect(find(:xpath, "#{node.path}/..").text).to eq 'Check GitLab configuration'
+ end
+end
diff --git a/spec/features/dashboard/shortcuts_spec.rb b/spec/features/dashboard/shortcuts_spec.rb
new file mode 100644
index 00000000000..d9be4e5dbdd
--- /dev/null
+++ b/spec/features/dashboard/shortcuts_spec.rb
@@ -0,0 +1,29 @@
+require 'spec_helper'
+
+feature 'Dashboard shortcuts', feature: true, js: true do
+ before do
+ login_as :user
+ visit dashboard_projects_path
+ end
+
+ scenario 'Navigate to tabs' do
+ find('body').native.send_key('g')
+ find('body').native.send_key('p')
+
+ ensure_active_main_tab('Projects')
+
+ find('body').native.send_key('g')
+ find('body').native.send_key('i')
+
+ ensure_active_main_tab('Issues')
+
+ find('body').native.send_key('g')
+ find('body').native.send_key('m')
+
+ ensure_active_main_tab('Merge Requests')
+ end
+
+ def ensure_active_main_tab(content)
+ expect(find('.nav-sidebar li.active')).to have_content(content)
+ end
+end
diff --git a/spec/features/environment_spec.rb b/spec/features/environment_spec.rb
index 56f6cd2e095..2f49e89b4e4 100644
--- a/spec/features/environment_spec.rb
+++ b/spec/features/environment_spec.rb
@@ -19,6 +19,10 @@ feature 'Environment', :feature do
visit_environment(environment)
end
+ scenario 'shows environment name' do
+ expect(page).to have_content(environment.name)
+ end
+
context 'without deployments' do
scenario 'does show no deployments' do
expect(page).to have_content('You don\'t have any deployments right now.')
@@ -60,10 +64,6 @@ feature 'Environment', :feature do
expect(page).to have_link('Re-deploy')
end
- scenario 'does not show stop button' do
- expect(page).not_to have_link('Stop')
- end
-
scenario 'does not show terminal button' do
expect(page).not_to have_terminal_button
end
@@ -112,27 +112,43 @@ feature 'Environment', :feature do
end
end
- context 'with stop action' do
- given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'close_app') }
- given(:deployment) { create(:deployment, environment: environment, deployable: build, on_stop: 'close_app') }
+ context 'when environment is available' do
+ context 'with stop action' do
+ given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'close_app') }
+ given(:deployment) { create(:deployment, environment: environment, deployable: build, on_stop: 'close_app') }
- scenario 'does show stop button' do
- expect(page).to have_link('Stop')
- end
+ scenario 'does show stop button' do
+ expect(page).to have_link('Stop')
+ end
- scenario 'does allow to stop environment' do
- click_link('Stop')
+ scenario 'does allow to stop environment' do
+ click_link('Stop')
- expect(page).to have_content('close_app')
- end
+ expect(page).to have_content('close_app')
+ end
- context 'for reporter' do
- let(:role) { :reporter }
+ context 'for reporter' do
+ let(:role) { :reporter }
- scenario 'does not show stop button' do
- expect(page).not_to have_link('Stop')
+ scenario 'does not show stop button' do
+ expect(page).not_to have_link('Stop')
+ end
end
end
+
+ context 'without stop action' do
+ scenario 'does allow to stop environment' do
+ click_link('Stop')
+ end
+ end
+ end
+
+ context 'when environment is stopped' do
+ given(:environment) { create(:environment, project: project, state: :stopped) }
+
+ scenario 'does not show stop button' do
+ expect(page).not_to have_link('Stop')
+ end
end
end
end
diff --git a/spec/features/environments_spec.rb b/spec/features/environments_spec.rb
index 72b984cfab8..78be7d36f47 100644
--- a/spec/features/environments_spec.rb
+++ b/spec/features/environments_spec.rb
@@ -52,6 +52,22 @@ feature 'Environments page', :feature, :js do
scenario 'does show no deployments' do
expect(page).to have_content('No deployments yet')
end
+
+ context 'for available environment' do
+ given(:environment) { create(:environment, project: project, state: :available) }
+
+ scenario 'does not shows stop button' do
+ expect(page).not_to have_selector('.stop-env-link')
+ end
+ end
+
+ context 'for stopped environment' do
+ given(:environment) { create(:environment, project: project, state: :stopped) }
+
+ scenario 'does not shows stop button' do
+ expect(page).not_to have_selector('.stop-env-link')
+ end
+ end
end
context 'with deployments' do
@@ -194,7 +210,7 @@ feature 'Environments page', :feature, :js do
end
scenario 'does create a new pipeline' do
- expect(page).to have_content('Production')
+ expect(page).to have_content('production')
end
end
diff --git a/spec/features/expand_collapse_diffs_spec.rb b/spec/features/expand_collapse_diffs_spec.rb
index 3934c936f20..8b3e2fa93a2 100644
--- a/spec/features/expand_collapse_diffs_spec.rb
+++ b/spec/features/expand_collapse_diffs_spec.rb
@@ -4,10 +4,10 @@ feature 'Expand and collapse diffs', js: true, feature: true do
include WaitForAjax
let(:branch) { 'expand-collapse-diffs' }
+ let(:project) { create(:project) }
before do
login_as :admin
- project = create(:project)
# Ensure that undiffable.md is in .gitattributes
project.repository.copy_gitattributes(branch)
@@ -31,6 +31,33 @@ feature 'Expand and collapse diffs', js: true, feature: true do
define_method(file.split('.').first) { file_container(file) }
end
+ it 'should show the diff content with a highlighted line when linking to line' do
+ expect(large_diff).not_to have_selector('.code')
+ expect(large_diff).to have_selector('.nothing-here-block')
+
+ visit namespace_project_commit_path(project.namespace, project, project.commit(branch), anchor: "#{large_diff[:id]}_0_1")
+ execute_script('window.location.reload()')
+
+ wait_for_ajax
+
+ expect(large_diff).to have_selector('.code')
+ expect(large_diff).not_to have_selector('.nothing-here-block')
+ expect(large_diff).to have_selector('.hll')
+ end
+
+ it 'should show the diff content when linking to file' do
+ expect(large_diff).not_to have_selector('.code')
+ expect(large_diff).to have_selector('.nothing-here-block')
+
+ visit namespace_project_commit_path(project.namespace, project, project.commit(branch), anchor: large_diff[:id])
+ execute_script('window.location.reload()')
+
+ wait_for_ajax
+
+ expect(large_diff).to have_selector('.code')
+ expect(large_diff).not_to have_selector('.nothing-here-block')
+ end
+
context 'visiting a commit with collapsed diffs' do
it 'shows small diffs immediately' do
expect(small_diff).to have_selector('.code')
diff --git a/spec/features/groups/merge_requests_spec.rb b/spec/features/groups/merge_requests_spec.rb
index 30b80aa82b0..b55078c3bf6 100644
--- a/spec/features/groups/merge_requests_spec.rb
+++ b/spec/features/groups/merge_requests_spec.rb
@@ -7,7 +7,7 @@ feature 'Group merge requests page', feature: true do
include_examples 'project features apply to issuables', MergeRequest
context 'archived issuable' do
- let(:project_archived) { create(:project, group: group, merge_requests_access_level: ProjectFeature::ENABLED, archived: true) }
+ let(:project_archived) { create(:project, :archived, :merge_requests_enabled, group: group) }
let(:issuable_archived) { create(:merge_request, source_project: project_archived, target_project: project_archived, title: 'issuable of an archived project') }
let(:access_level) { ProjectFeature::ENABLED }
let(:user) { user_in_group }
diff --git a/spec/features/groups_spec.rb b/spec/features/groups_spec.rb
index 4b19886274e..a515c92db37 100644
--- a/spec/features/groups_spec.rb
+++ b/spec/features/groups_spec.rb
@@ -107,4 +107,17 @@ feature 'Group', feature: true do
expect(page).to have_css('.group-home-desc a[rel]')
end
end
+
+ describe 'group page with nested groups', js: true do
+ let!(:group) { create(:group) }
+ let!(:nested_group) { create(:group, parent: group) }
+ let!(:path) { group_path(group) }
+
+ it 'has nested groups tab with nested groups inside' do
+ visit path
+ click_link 'Subgroups'
+
+ expect(page).to have_content(nested_group.full_name)
+ end
+ end
end
diff --git a/spec/features/issuables/default_sort_order_spec.rb b/spec/features/issuables/default_sort_order_spec.rb
index 9a2b879e789..73553f97d6f 100644
--- a/spec/features/issuables/default_sort_order_spec.rb
+++ b/spec/features/issuables/default_sort_order_spec.rb
@@ -180,16 +180,10 @@ describe 'Projects > Issuables > Default sort order', feature: true do
end
def visit_merge_requests_with_state(project, state)
- visit_merge_requests project
- visit_issuables_with_state state
+ visit_merge_requests project, 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 }
+ visit_issues project, state: state
end
end
diff --git a/spec/features/issues/award_emoji_spec.rb b/spec/features/issues/award_emoji_spec.rb
index efb53026449..73e43316dc7 100644
--- a/spec/features/issues/award_emoji_spec.rb
+++ b/spec/features/issues/award_emoji_spec.rb
@@ -76,7 +76,7 @@ describe 'Awards Emoji', feature: true do
end
it 'has disabled emoji button' do
- expect(first('.award-control')[:disabled]).to be(true)
+ expect(first('.award-control')[:class]).to have_text('disabled')
end
end
diff --git a/spec/features/issues/filter_by_labels_spec.rb b/spec/features/issues/filter_by_labels_spec.rb
deleted file mode 100644
index 0253629f753..00000000000
--- a/spec/features/issues/filter_by_labels_spec.rb
+++ /dev/null
@@ -1,152 +0,0 @@
-require 'rails_helper'
-
-feature 'Issue filtering by Labels', feature: true, js: true do
- include WaitForAjax
-
- let(:project) { create(:project, :public) }
- let!(:user) { create(:user) }
- let!(:label) { create(:label, project: project) }
-
- before do
- bug = create(:label, project: project, title: 'bug')
- feature = create(:label, project: project, title: 'feature')
- enhancement = create(:label, project: project, title: 'enhancement')
-
- issue1 = create(:issue, title: "Bugfix1", project: project)
- issue1.labels << bug
-
- issue2 = create(:issue, title: "Bugfix2", project: project)
- issue2.labels << bug
- issue2.labels << enhancement
-
- issue3 = create(:issue, title: "Feature1", project: project)
- issue3.labels << feature
-
- project.team << [user, :master]
- login_as(user)
-
- visit namespace_project_issues_path(project.namespace, project)
- end
-
- context 'filter by label bug' do
- before do
- select_labels('bug')
- end
-
- it 'apply the filter' do
- expect(page).to have_content "Bugfix1"
- expect(page).to have_content "Bugfix2"
- expect(page).not_to have_content "Feature1"
- expect(find('.filtered-labels')).to have_content "bug"
- expect(find('.filtered-labels')).not_to have_content "feature"
- expect(find('.filtered-labels')).not_to have_content "enhancement"
-
- find('.js-label-filter-remove').click
- wait_for_ajax
- expect(find('.filtered-labels', visible: false)).to have_no_content "bug"
- end
- end
-
- context 'filter by label feature' do
- before do
- select_labels('feature')
- end
-
- it 'applies the filter' do
- expect(page).to have_content "Feature1"
- expect(page).not_to have_content "Bugfix2"
- expect(page).not_to have_content "Bugfix1"
- expect(find('.filtered-labels')).to have_content "feature"
- expect(find('.filtered-labels')).not_to have_content "bug"
- expect(find('.filtered-labels')).not_to have_content "enhancement"
- end
- end
-
- context 'filter by label enhancement' do
- before do
- select_labels('enhancement')
- end
-
- it 'applies the filter' do
- expect(page).to have_content "Bugfix2"
- expect(page).not_to have_content "Feature1"
- expect(page).not_to have_content "Bugfix1"
- expect(find('.filtered-labels')).to have_content "enhancement"
- expect(find('.filtered-labels')).not_to have_content "bug"
- expect(find('.filtered-labels')).not_to have_content "feature"
- end
- end
-
- context 'filter by label enhancement and bug in issues list' do
- before do
- select_labels('bug', 'enhancement')
- end
-
- it 'applies the filters' do
- expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
- expect(page).to have_content "Bugfix2"
- expect(page).not_to have_content "Feature1"
- expect(find('.filtered-labels')).to have_content "bug"
- expect(find('.filtered-labels')).to have_content "enhancement"
- expect(find('.filtered-labels')).not_to have_content "feature"
-
- find('.js-label-filter-remove', match: :first).click
- wait_for_ajax
-
- expect(page).to have_content "Bugfix2"
- expect(page).not_to have_content "Feature1"
- expect(page).not_to have_content "Bugfix1"
- expect(find('.filtered-labels')).not_to have_content "bug"
- expect(find('.filtered-labels')).to have_content "enhancement"
- expect(find('.filtered-labels')).not_to have_content "feature"
- end
- end
-
- context 'remove filtered labels' do
- before do
- page.within '.labels-filter' do
- click_button 'Label'
- wait_for_ajax
- click_link 'bug'
- find('.dropdown-menu-close').click
- end
-
- page.within '.filtered-labels' do
- expect(page).to have_content 'bug'
- end
- end
-
- it 'allows user to remove filtered labels' do
- first('.js-label-filter-remove').click
- wait_for_ajax
-
- expect(find('.filtered-labels', visible: false)).not_to have_content 'bug'
- expect(find('.labels-filter')).not_to have_content 'bug'
- end
- end
-
- context 'dropdown filtering' do
- it 'filters by label name' do
- page.within '.labels-filter' do
- click_button 'Label'
- wait_for_ajax
- find('.dropdown-input input').set 'bug'
-
- page.within '.dropdown-content' do
- expect(page).not_to have_content 'enhancement'
- expect(page).to have_content 'bug'
- end
- end
- end
- end
-
- def select_labels(*labels)
- page.find('.js-label-select').click
- wait_for_ajax
- labels.each do |label|
- execute_script("$('.dropdown-menu-labels li:contains(\"#{label}\") a').click()")
- end
- page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click
- wait_for_ajax
- end
-end
diff --git a/spec/features/issues/filter_by_milestone_spec.rb b/spec/features/issues/filter_by_milestone_spec.rb
deleted file mode 100644
index 9dfa5d1de19..00000000000
--- a/spec/features/issues/filter_by_milestone_spec.rb
+++ /dev/null
@@ -1,91 +0,0 @@
-require 'rails_helper'
-
-feature 'Issue filtering by Milestone', feature: true do
- let(:project) { create(:project, :public) }
- let(:milestone) { create(:milestone, project: project) }
-
- scenario 'filters by no Milestone', js: true do
- create(:issue, project: project)
- create(:issue, project: project, milestone: milestone)
-
- visit_issues(project)
- filter_by_milestone(Milestone::None.title)
-
- expect(page).to have_css('.milestone-filter .dropdown-toggle-text', text: 'No Milestone')
- expect(page).to have_css('.issue', count: 1)
- end
-
- context 'filters by upcoming milestone', js: true do
- it 'does not show issues with no expiry' do
- create(:issue, project: project)
- create(:issue, project: project, milestone: milestone)
-
- visit_issues(project)
- filter_by_milestone(Milestone::Upcoming.title)
-
- expect(page).to have_css('.milestone-filter .dropdown-toggle-text', text: 'Upcoming')
- expect(page).to have_css('.issue', count: 0)
- end
-
- it 'shows issues in future' do
- milestone = create(:milestone, project: project, due_date: Date.tomorrow)
- create(:issue, project: project)
- create(:issue, project: project, milestone: milestone)
-
- visit_issues(project)
- filter_by_milestone(Milestone::Upcoming.title)
-
- expect(page).to have_css('.milestone-filter .dropdown-toggle-text', text: 'Upcoming')
- expect(page).to have_css('.issue', count: 1)
- end
-
- it 'does not show issues in past' do
- milestone = create(:milestone, project: project, due_date: Date.yesterday)
- create(:issue, project: project)
- create(:issue, project: project, milestone: milestone)
-
- visit_issues(project)
- filter_by_milestone(Milestone::Upcoming.title)
-
- expect(page).to have_css('.milestone-filter .dropdown-toggle-text', text: 'Upcoming')
- expect(page).to have_css('.issue', count: 0)
- end
- end
-
- scenario 'filters by a specific Milestone', js: true do
- create(:issue, project: project, milestone: milestone)
- create(:issue, project: project)
-
- visit_issues(project)
- filter_by_milestone(milestone.title)
-
- expect(page).to have_css('.milestone-filter .dropdown-toggle-text', text: milestone.title)
- expect(page).to have_css('.issue', count: 1)
- end
-
- context 'when milestone has single quotes in title' do
- background do
- milestone.update(name: "rock 'n' roll")
- end
-
- scenario 'filters by a specific Milestone', js: true do
- create(:issue, project: project, milestone: milestone)
- create(:issue, project: project)
-
- visit_issues(project)
- filter_by_milestone(milestone.title)
-
- expect(page).to have_css('.milestone-filter .dropdown-toggle-text', text: milestone.title)
- expect(page).to have_css('.issue', count: 1)
- end
- end
-
- def visit_issues(project)
- visit namespace_project_issues_path(project.namespace, project)
- end
-
- def filter_by_milestone(title)
- find(".js-milestone-select").click
- find(".milestone-filter .dropdown-content a", text: title).click
- end
-end
diff --git a/spec/features/issues/filter_issues_spec.rb b/spec/features/issues/filter_issues_spec.rb
deleted file mode 100644
index 0d19563d628..00000000000
--- a/spec/features/issues/filter_issues_spec.rb
+++ /dev/null
@@ -1,384 +0,0 @@
-require 'rails_helper'
-
-describe 'Filter issues', feature: true do
- include WaitForAjax
-
- let!(:group) { create(:group) }
- let!(:project) { create(:project, group: group) }
- let!(:user) { create(:user)}
- let!(:milestone) { create(:milestone, project: project) }
- let!(:label) { create(:label, project: project) }
- let!(:wontfix) { create(:label, project: project, title: "Won't fix") }
-
- before do
- project.team << [user, :master]
- group.add_developer(user)
- login_as(user)
- create(:issue, project: project)
- end
-
- describe 'for assignee from issues#index' do
- before do
- visit namespace_project_issues_path(project.namespace, project)
-
- find('.js-assignee-search').click
-
- find('.dropdown-menu-user-link', text: user.username).click
-
- wait_for_ajax
- end
-
- context 'assignee', js: true do
- it 'updates to current user' do
- expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name)
- end
-
- 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 '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)
- end
- end
- end
-
- describe 'for milestone from issues#index' do
- before do
- visit namespace_project_issues_path(project.namespace, project)
-
- find('.js-milestone-select').click
-
- find('.milestone-filter .dropdown-content a', text: milestone.title).click
-
- wait_for_ajax
- end
-
- context 'milestone', js: true do
- it 'updates to current milestone' do
- expect(find('.js-milestone-select .dropdown-toggle-text')).to have_content(milestone.title)
- end
-
- 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 '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)
- end
- end
- end
-
- describe 'for label from issues#index', js: true do
- before do
- visit namespace_project_issues_path(project.namespace, project)
- find('.js-label-select').click
- wait_for_ajax
- end
-
- 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
-
- expect(find('.labels-filter')).to have_content 'Label'
- end
-
- 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
-
- page.within '.labels-filter' do
- expect(page).to have_content 'Labels'
- end
- expect(find('.js-label-select .dropdown-toggle-text')).to have_content('Labels')
- end
-
- it 'filters by a label' do
- find('.dropdown-menu-labels a', text: label.title).click
- page.within '.labels-filter' do
- expect(page).to have_content label.title
- end
- expect(find('.js-label-select .dropdown-toggle-text')).to have_content(label.title)
- end
-
- it "filters by `won't fix` and another label" do
- page.within '.labels-filter' do
- click_link wontfix.title
- expect(page).to have_content wontfix.title
- click_link label.title
- end
-
- expect(find('.js-label-select .dropdown-toggle-text')).to have_content("#{wontfix.title} +1 more")
- end
-
- it "filters by `won't fix` label followed by another label after page load" do
- page.within '.labels-filter' do
- click_link wontfix.title
- expect(page).to have_content wontfix.title
- end
-
- find('.dropdown-menu-close-icon').click
-
- expect(find('.filtered-labels')).to have_content(wontfix.title)
-
- find('.js-label-select').click
- wait_for_ajax
- find('.dropdown-menu-labels a', text: label.title).click
-
- find('.dropdown-menu-close-icon').click
-
- expect(find('.filtered-labels')).to have_content(wontfix.title)
- expect(find('.filtered-labels')).to have_content(label.title)
-
- find('.js-label-select').click
- wait_for_ajax
-
- expect(find('.dropdown-menu-labels li', text: wontfix.title)).to have_css('.is-active')
- expect(find('.dropdown-menu-labels li', text: label.title)).to have_css('.is-active')
- end
-
- it "selects and unselects `won't fix`" do
- find('.dropdown-menu-labels a', text: wontfix.title).click
- find('.dropdown-menu-labels a', text: wontfix.title).click
-
- find('.dropdown-menu-close-icon').click
- expect(page).not_to have_css('.filtered-labels')
- end
- end
-
- describe 'for assignee and label from issues#index' do
- before do
- visit namespace_project_issues_path(project.namespace, project)
-
- find('.js-assignee-search').click
-
- find('.dropdown-menu-user-link', text: user.username).click
-
- expect(page).not_to have_selector('.issues-list .issue')
-
- find('.js-label-select').click
-
- find('.dropdown-menu-labels .dropdown-content a', text: label.title).click
- page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click
-
- wait_for_ajax
- end
-
- context 'assignee and label', js: true 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 '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 '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)
- expect(find('.js-label-select .dropdown-toggle-text')).to have_content(label.title)
- end
- end
- end
-
- describe 'filter issues by text' do
- before do
- create(:issue, title: "Bug", project: project)
-
- bug_label = create(:label, project: project, title: 'bug')
- milestone = create(:milestone, title: "8", project: project)
-
- issue = create(:issue,
- title: "Bug 2",
- project: project,
- milestone: milestone,
- author: user,
- assignee: user)
- issue.labels << bug_label
-
- visit namespace_project_issues_path(project.namespace, project)
- end
-
- context 'only text', js: true do
- it 'filters issues by searched text' do
- fill_in 'issuable_search', with: 'Bug'
-
- page.within '.issues-list' do
- expect(page).to have_selector('.issue', count: 2)
- end
- end
-
- it 'does not show any issues' do
- fill_in 'issuable_search', with: 'testing'
-
- page.within '.issues-list' do
- expect(page).not_to have_selector('.issue')
- end
- end
- end
-
- context 'text and dropdown options', js: true do
- it 'filters by text and label' do
- fill_in 'issuable_search', with: 'Bug'
-
- expect(page).to have_issuable_counts(open: 2, closed: 0, all: 2)
- page.within '.issues-list' do
- expect(page).to have_selector('.issue', count: 2)
- end
-
- click_button 'Label'
- page.within '.labels-filter' do
- click_link 'bug'
- end
- find('.dropdown-menu-close-icon').click
-
- expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
- page.within '.issues-list' do
- expect(page).to have_selector('.issue', count: 1)
- end
- end
-
- it 'filters by text and milestone' do
- fill_in 'issuable_search', with: 'Bug'
-
- expect(page).to have_issuable_counts(open: 2, closed: 0, all: 2)
- page.within '.issues-list' do
- expect(page).to have_selector('.issue', count: 2)
- end
-
- click_button 'Milestone'
- page.within '.milestone-filter' do
- click_link '8'
- end
-
- expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
- page.within '.issues-list' do
- expect(page).to have_selector('.issue', count: 1)
- end
- end
-
- it 'filters by text and assignee' do
- fill_in 'issuable_search', with: 'Bug'
-
- expect(page).to have_issuable_counts(open: 2, closed: 0, all: 2)
- page.within '.issues-list' do
- expect(page).to have_selector('.issue', count: 2)
- end
-
- click_button 'Assignee'
- page.within '.dropdown-menu-assignee' do
- click_link user.name
- end
-
- expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
- page.within '.issues-list' do
- expect(page).to have_selector('.issue', count: 1)
- end
- end
-
- it 'filters by text and author' do
- fill_in 'issuable_search', with: 'Bug'
-
- expect(page).to have_issuable_counts(open: 2, closed: 0, all: 2)
- page.within '.issues-list' do
- expect(page).to have_selector('.issue', count: 2)
- end
-
- click_button 'Author'
- page.within '.dropdown-menu-author' do
- click_link user.name
- end
-
- expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
- page.within '.issues-list' do
- expect(page).to have_selector('.issue', count: 1)
- end
- end
- end
- end
-
- describe 'filter issues and sort', js: true do
- before do
- bug_label = create(:label, project: project, title: 'bug')
- bug_one = create(:issue, title: "Frontend", project: project)
- bug_two = create(:issue, title: "Bug 2", project: project)
-
- bug_one.labels << bug_label
- bug_two.labels << bug_label
-
- visit namespace_project_issues_path(project.namespace, project)
- end
-
- it 'is able to filter and sort issues' do
- click_button 'Label'
- wait_for_ajax
- page.within '.labels-filter' do
- click_link 'bug'
- end
- find('.dropdown-menu-close-icon').click
- wait_for_ajax
-
- expect(page).to have_issuable_counts(open: 2, closed: 0, all: 2)
- page.within '.issues-list' do
- expect(page).to have_selector('.issue', count: 2)
- end
-
- click_button 'Last created'
- page.within '.dropdown-menu-sort' do
- click_link 'Oldest created'
- end
- wait_for_ajax
-
- page.within '.issues-list' do
- expect(page).to have_content('Frontend')
- end
- end
- end
-
- it 'updates atom feed link for project issues' do
- visit namespace_project_issues_path(project.namespace, project, milestone_title: '', assignee_id: user.id)
-
- link = find('.nav-controls a', text: 'Subscribe')
- params = CGI::parse(URI.parse(link[:href]).query)
- auto_discovery_link = find('link[type="application/atom+xml"]', visible: false)
- auto_discovery_params = CGI::parse(URI.parse(auto_discovery_link[:href]).query)
-
- expect(params).to include('private_token' => [user.private_token])
- expect(params).to include('milestone_title' => [''])
- expect(params).to include('assignee_id' => [user.id.to_s])
- expect(auto_discovery_params).to include('private_token' => [user.private_token])
- expect(auto_discovery_params).to include('milestone_title' => [''])
- expect(auto_discovery_params).to include('assignee_id' => [user.id.to_s])
- end
-
- it 'updates atom feed link for group issues' do
- visit issues_group_path(group, milestone_title: '', assignee_id: user.id)
-
- link = find('.nav-controls a', text: 'Subscribe')
- params = CGI::parse(URI.parse(link[:href]).query)
- auto_discovery_link = find('link[type="application/atom+xml"]', visible: false)
- auto_discovery_params = CGI::parse(URI.parse(auto_discovery_link[:href]).query)
-
- expect(params).to include('private_token' => [user.private_token])
- expect(params).to include('milestone_title' => [''])
- expect(params).to include('assignee_id' => [user.id.to_s])
- expect(auto_discovery_params).to include('private_token' => [user.private_token])
- expect(auto_discovery_params).to include('milestone_title' => [''])
- expect(auto_discovery_params).to include('assignee_id' => [user.id.to_s])
- end
-end
diff --git a/spec/features/issues/filtered_search/dropdown_assignee_spec.rb b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb
new file mode 100644
index 00000000000..93763f092fb
--- /dev/null
+++ b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb
@@ -0,0 +1,190 @@
+require 'rails_helper'
+
+describe 'Dropdown assignee', js: true, feature: true do
+ include WaitForAjax
+
+ let!(:project) { create(:empty_project) }
+ let!(:user) { create(:user, name: 'administrator', username: 'root') }
+ let!(:user_john) { create(:user, name: 'John', username: 'th0mas') }
+ let!(:user_jacob) { create(:user, name: 'Jacob', username: 'otter32') }
+ let(:filtered_search) { find('.filtered-search') }
+ let(:js_dropdown_assignee) { '#js-dropdown-assignee' }
+
+ def send_keys_to_filtered_search(input)
+ input.split("").each do |i|
+ filtered_search.send_keys(i)
+ sleep 5
+ wait_for_ajax
+ end
+ end
+
+ def dropdown_assignee_size
+ page.all('#js-dropdown-assignee .filter-dropdown .filter-dropdown-item').size
+ end
+
+ def click_assignee(text)
+ find('#js-dropdown-assignee .filter-dropdown .filter-dropdown-item', text: text).click
+ end
+
+ before do
+ project.team << [user, :master]
+ project.team << [user_john, :master]
+ project.team << [user_jacob, :master]
+ login_as(user)
+ create(:issue, project: project)
+
+ visit namespace_project_issues_path(project.namespace, project)
+ end
+
+ describe 'behavior' do
+ it 'opens when the search bar has assignee:' do
+ filtered_search.set('assignee:')
+
+ expect(page).to have_css(js_dropdown_assignee, visible: true)
+ end
+
+ it 'closes when the search bar is unfocused' do
+ find('body').click()
+
+ expect(page).to have_css(js_dropdown_assignee, visible: false)
+ end
+
+ it 'should show loading indicator when opened' do
+ filtered_search.set('assignee:')
+
+ expect(page).to have_css('#js-dropdown-assignee .filter-dropdown-loading', visible: true)
+ end
+
+ it 'should hide loading indicator when loaded' do
+ send_keys_to_filtered_search('assignee:')
+
+ expect(page).not_to have_css('#js-dropdown-assignee .filter-dropdown-loading')
+ end
+
+ it 'should load all the assignees when opened' do
+ send_keys_to_filtered_search('assignee:')
+
+ expect(dropdown_assignee_size).to eq(3)
+ end
+
+ it 'shows current user at top of dropdown' do
+ send_keys_to_filtered_search('assignee:')
+
+ expect(first('#js-dropdown-assignee .filter-dropdown li')).to have_content(user.name)
+ end
+ end
+
+ describe 'filtering' do
+ before do
+ send_keys_to_filtered_search('assignee:')
+ end
+
+ it 'filters by name' do
+ send_keys_to_filtered_search('j')
+
+ expect(dropdown_assignee_size).to eq(2)
+ end
+
+ it 'filters by case insensitive name' do
+ send_keys_to_filtered_search('J')
+
+ expect(dropdown_assignee_size).to eq(2)
+ end
+
+ it 'filters by username with symbol' do
+ send_keys_to_filtered_search('@ot')
+
+ expect(dropdown_assignee_size).to eq(2)
+ end
+
+ it 'filters by case insensitive username with symbol' do
+ send_keys_to_filtered_search('@OT')
+
+ expect(dropdown_assignee_size).to eq(2)
+ end
+
+ it 'filters by username without symbol' do
+ send_keys_to_filtered_search('ot')
+
+ expect(dropdown_assignee_size).to eq(2)
+ end
+
+ it 'filters by case insensitive username without symbol' do
+ send_keys_to_filtered_search('OT')
+
+ expect(dropdown_assignee_size).to eq(2)
+ end
+ end
+
+ describe 'selecting from dropdown' do
+ before do
+ filtered_search.set('assignee:')
+ end
+
+ it 'fills in the assignee username when the assignee has not been filtered' do
+ click_assignee(user_jacob.name)
+
+ expect(page).to have_css(js_dropdown_assignee, visible: false)
+ expect(filtered_search.value).to eq("assignee:@#{user_jacob.username} ")
+ end
+
+ it 'fills in the assignee username when the assignee has been filtered' do
+ send_keys_to_filtered_search('roo')
+ click_assignee(user.name)
+
+ expect(page).to have_css(js_dropdown_assignee, visible: false)
+ expect(filtered_search.value).to eq("assignee:@#{user.username} ")
+ end
+
+ it 'selects `no assignee`' do
+ find('#js-dropdown-assignee .filter-dropdown-item', text: 'No Assignee').click
+
+ expect(page).to have_css(js_dropdown_assignee, visible: false)
+ expect(filtered_search.value).to eq("assignee:none ")
+ end
+ end
+
+ describe 'input has existing content' do
+ it 'opens assignee dropdown with existing search term' do
+ filtered_search.set('searchTerm assignee:')
+
+ expect(page).to have_css(js_dropdown_assignee, visible: true)
+ end
+
+ it 'opens assignee dropdown with existing author' do
+ filtered_search.set('author:@user assignee:')
+
+ expect(page).to have_css(js_dropdown_assignee, visible: true)
+ end
+
+ it 'opens assignee dropdown with existing label' do
+ filtered_search.set('label:~bug assignee:')
+
+ expect(page).to have_css(js_dropdown_assignee, visible: true)
+ end
+
+ it 'opens assignee dropdown with existing milestone' do
+ filtered_search.set('milestone:%v1.0 assignee:')
+
+ expect(page).to have_css(js_dropdown_assignee, visible: true)
+ end
+ end
+
+ describe 'caching requests' do
+ it 'caches requests after the first load' do
+ filtered_search.set('assignee')
+ send_keys_to_filtered_search(':')
+ initial_size = dropdown_assignee_size
+
+ expect(initial_size).to be > 0
+
+ new_user = create(:user)
+ project.team << [new_user, :master]
+ find('.filtered-search-input-container .clear-search').click
+ filtered_search.set('assignee')
+ send_keys_to_filtered_search(':')
+
+ expect(dropdown_assignee_size).to eq(initial_size)
+ end
+ end
+end
diff --git a/spec/features/issues/filtered_search/dropdown_author_spec.rb b/spec/features/issues/filtered_search/dropdown_author_spec.rb
new file mode 100644
index 00000000000..59e302f0e2d
--- /dev/null
+++ b/spec/features/issues/filtered_search/dropdown_author_spec.rb
@@ -0,0 +1,178 @@
+require 'rails_helper'
+
+describe 'Dropdown author', js: true, feature: true do
+ include WaitForAjax
+
+ let!(:project) { create(:empty_project) }
+ let!(:user) { create(:user, name: 'administrator', username: 'root') }
+ let!(:user_john) { create(:user, name: 'John', username: 'th0mas') }
+ let!(:user_jacob) { create(:user, name: 'Jacob', username: 'otter32') }
+ let(:filtered_search) { find('.filtered-search') }
+ let(:js_dropdown_author) { '#js-dropdown-author' }
+
+ def send_keys_to_filtered_search(input)
+ input.split("").each do |i|
+ filtered_search.send_keys(i)
+ sleep 5
+ wait_for_ajax
+ end
+ end
+
+ def dropdown_author_size
+ page.all('#js-dropdown-author .filter-dropdown .filter-dropdown-item').size
+ end
+
+ def click_author(text)
+ find('#js-dropdown-author .filter-dropdown .filter-dropdown-item', text: text).click
+ end
+
+ before do
+ project.team << [user, :master]
+ project.team << [user_john, :master]
+ project.team << [user_jacob, :master]
+ login_as(user)
+ create(:issue, project: project)
+
+ visit namespace_project_issues_path(project.namespace, project)
+ end
+
+ describe 'behavior' do
+ it 'opens when the search bar has author:' do
+ filtered_search.set('author:')
+
+ expect(page).to have_css(js_dropdown_author, visible: true)
+ end
+
+ it 'closes when the search bar is unfocused' do
+ find('body').click()
+
+ expect(page).to have_css(js_dropdown_author, visible: false)
+ end
+
+ it 'should show loading indicator when opened' do
+ filtered_search.set('author:')
+
+ expect(page).to have_css('#js-dropdown-author .filter-dropdown-loading', visible: true)
+ end
+
+ it 'should hide loading indicator when loaded' do
+ send_keys_to_filtered_search('author:')
+
+ expect(page).not_to have_css('#js-dropdown-author .filter-dropdown-loading')
+ end
+
+ it 'should load all the authors when opened' do
+ send_keys_to_filtered_search('author:')
+
+ expect(dropdown_author_size).to eq(3)
+ end
+
+ it 'shows current user at top of dropdown' do
+ send_keys_to_filtered_search('author:')
+
+ expect(first('#js-dropdown-author li')).to have_content(user.name)
+ end
+ end
+
+ describe 'filtering' do
+ before do
+ filtered_search.set('author')
+ send_keys_to_filtered_search(':')
+ end
+
+ it 'filters by name' do
+ send_keys_to_filtered_search('ja')
+
+ expect(dropdown_author_size).to eq(1)
+ end
+
+ it 'filters by case insensitive name' do
+ send_keys_to_filtered_search('Ja')
+
+ expect(dropdown_author_size).to eq(1)
+ end
+
+ it 'filters by username with symbol' do
+ send_keys_to_filtered_search('@ot')
+
+ expect(dropdown_author_size).to eq(2)
+ end
+
+ it 'filters by username without symbol' do
+ send_keys_to_filtered_search('ot')
+
+ expect(dropdown_author_size).to eq(2)
+ end
+
+ it 'filters by case insensitive username without symbol' do
+ send_keys_to_filtered_search('OT')
+
+ expect(dropdown_author_size).to eq(2)
+ end
+ end
+
+ describe 'selecting from dropdown' do
+ before do
+ filtered_search.set('author')
+ send_keys_to_filtered_search(':')
+ end
+
+ it 'fills in the author username when the author has not been filtered' do
+ click_author(user_jacob.name)
+
+ expect(page).to have_css(js_dropdown_author, visible: false)
+ expect(filtered_search.value).to eq("author:@#{user_jacob.username} ")
+ end
+
+ it 'fills in the author username when the author has been filtered' do
+ click_author(user.name)
+
+ expect(page).to have_css(js_dropdown_author, visible: false)
+ expect(filtered_search.value).to eq("author:@#{user.username} ")
+ end
+ end
+
+ describe 'input has existing content' do
+ it 'opens author dropdown with existing search term' do
+ filtered_search.set('searchTerm author:')
+
+ expect(page).to have_css(js_dropdown_author, visible: true)
+ end
+
+ it 'opens author dropdown with existing assignee' do
+ filtered_search.set('assignee:@user author:')
+
+ expect(page).to have_css(js_dropdown_author, visible: true)
+ end
+
+ it 'opens author dropdown with existing label' do
+ filtered_search.set('label:~bug author:')
+
+ expect(page).to have_css(js_dropdown_author, visible: true)
+ end
+
+ it 'opens author dropdown with existing milestone' do
+ filtered_search.set('milestone:%v1.0 author:')
+
+ expect(page).to have_css(js_dropdown_author, visible: true)
+ end
+ end
+
+ describe 'caching requests' do
+ it 'caches requests after the first load' do
+ filtered_search.set('author')
+ send_keys_to_filtered_search(':')
+ initial_size = dropdown_author_size
+
+ expect(initial_size).to be > 0
+
+ new_user = create(:user)
+ project.team << [new_user, :master]
+ find('.filtered-search-input-container .clear-search').click
+ filtered_search.set('author')
+ send_keys_to_filtered_search(':')
+
+ expect(dropdown_author_size).to eq(initial_size)
+ end
+ end
+end
diff --git a/spec/features/issues/filtered_search/dropdown_hint_spec.rb b/spec/features/issues/filtered_search/dropdown_hint_spec.rb
new file mode 100644
index 00000000000..04dd54ab459
--- /dev/null
+++ b/spec/features/issues/filtered_search/dropdown_hint_spec.rb
@@ -0,0 +1,134 @@
+require 'rails_helper'
+
+describe 'Dropdown hint', js: true, feature: true do
+ include WaitForAjax
+
+ let!(:project) { create(:empty_project) }
+ let!(:user) { create(:user) }
+ let(:filtered_search) { find('.filtered-search') }
+ let(:js_dropdown_hint) { '#js-dropdown-hint' }
+
+ def dropdown_hint_size
+ page.all('#js-dropdown-hint .filter-dropdown .filter-dropdown-item').size
+ end
+
+ def click_hint(text)
+ find('#js-dropdown-hint .filter-dropdown .filter-dropdown-item', text: text).click
+ end
+
+ before do
+ project.team << [user, :master]
+ login_as(user)
+ create(:issue, project: project)
+
+ visit namespace_project_issues_path(project.namespace, project)
+ end
+
+ describe 'behavior' do
+ before do
+ expect(page).to have_css(js_dropdown_hint, visible: false)
+ filtered_search.click
+ end
+
+ it 'opens when the search bar is first focused' do
+ expect(page).to have_css(js_dropdown_hint, visible: true)
+ end
+
+ it 'closes when the search bar is unfocused' do
+ find('body').click
+
+ expect(page).to have_css(js_dropdown_hint, visible: false)
+ end
+ end
+
+ describe 'filtering' do
+ it 'does not filter `Keep typing and press Enter`' do
+ filtered_search.set('randomtext')
+
+ expect(page).to have_css(js_dropdown_hint, text: 'Keep typing and press Enter', visible: false)
+ expect(dropdown_hint_size).to eq(0)
+ end
+
+ it 'filters with text' do
+ filtered_search.set('a')
+
+ expect(dropdown_hint_size).to eq(3)
+ end
+ end
+
+ describe 'selecting from dropdown with no input' do
+ before do
+ filtered_search.click
+ end
+
+ it 'opens the author dropdown when you click on author' do
+ click_hint('author')
+
+ expect(page).to have_css(js_dropdown_hint, visible: false)
+ expect(page).to have_css('#js-dropdown-author', visible: true)
+ expect(filtered_search.value).to eq('author:')
+ end
+
+ it 'opens the assignee dropdown when you click on assignee' do
+ click_hint('assignee')
+
+ expect(page).to have_css(js_dropdown_hint, visible: false)
+ expect(page).to have_css('#js-dropdown-assignee', visible: true)
+ expect(filtered_search.value).to eq('assignee:')
+ end
+
+ it 'opens the milestone dropdown when you click on milestone' do
+ click_hint('milestone')
+
+ expect(page).to have_css(js_dropdown_hint, visible: false)
+ expect(page).to have_css('#js-dropdown-milestone', visible: true)
+ expect(filtered_search.value).to eq('milestone:')
+ end
+
+ it 'opens the label dropdown when you click on label' do
+ click_hint('label')
+
+ expect(page).to have_css(js_dropdown_hint, visible: false)
+ expect(page).to have_css('#js-dropdown-label', visible: true)
+ expect(filtered_search.value).to eq('label:')
+ end
+ end
+
+ describe 'selecting from dropdown with some input' do
+ it 'opens the author dropdown when you click on author' do
+ filtered_search.set('auth')
+ click_hint('author')
+
+ expect(page).to have_css(js_dropdown_hint, visible: false)
+ expect(page).to have_css('#js-dropdown-author', visible: true)
+ expect(filtered_search.value).to eq('author:')
+ end
+
+ it 'opens the assignee dropdown when you click on assignee' do
+ filtered_search.set('assign')
+ click_hint('assignee')
+
+ expect(page).to have_css(js_dropdown_hint, visible: false)
+ expect(page).to have_css('#js-dropdown-assignee', visible: true)
+ expect(filtered_search.value).to eq('assignee:')
+ end
+
+ it 'opens the milestone dropdown when you click on milestone' do
+ filtered_search.set('mile')
+ click_hint('milestone')
+
+ expect(page).to have_css(js_dropdown_hint, visible: false)
+ expect(page).to have_css('#js-dropdown-milestone', visible: true)
+ expect(filtered_search.value).to eq('milestone:')
+ end
+
+ it 'opens the label dropdown when you click on label' do
+ filtered_search.set('lab')
+ click_hint('label')
+
+ expect(page).to have_css(js_dropdown_hint, visible: false)
+ expect(page).to have_css('#js-dropdown-label', visible: true)
+ expect(filtered_search.value).to eq('label:')
+ end
+ end
+end
diff --git a/spec/features/issues/filtered_search/dropdown_label_spec.rb b/spec/features/issues/filtered_search/dropdown_label_spec.rb
new file mode 100644
index 00000000000..c6a88e1b7b0
--- /dev/null
+++ b/spec/features/issues/filtered_search/dropdown_label_spec.rb
@@ -0,0 +1,287 @@
+require 'spec_helper'
+
+describe 'Dropdown label', js: true, feature: true do
+ let(:project) { create(:empty_project) }
+ let(:user) { create(:user) }
+ let(:filtered_search) { find('.filtered-search') }
+ let(:js_dropdown_label) { '#js-dropdown-label' }
+ let(:filter_dropdown) { find("#{js_dropdown_label} .filter-dropdown") }
+
+ shared_context 'with labels' do
+ let!(:bug_label) { create(:label, project: project, title: 'bug-label') }
+ let!(:uppercase_label) { create(:label, project: project, title: 'BUG-LABEL') }
+ let!(:two_words_label) { create(:label, project: project, title: 'High Priority') }
+ let!(:wont_fix_label) { create(:label, project: project, title: 'Won"t Fix') }
+ let!(:wont_fix_single_label) { create(:label, project: project, title: 'Won\'t Fix') }
+ let!(:special_label) { create(:label, project: project, title: '!@#$%^+&*()') }
+ let!(:long_label) { create(:label, project: project, title: 'this is a very long title this is a very long title this is a very long title this is a very long title this is a very long title') }
+ end
+
+ def init_label_search
+ filtered_search.set('label:')
+ # This ensures the dropdown is shown
+ expect(find(js_dropdown_label)).not_to have_css('.filter-dropdown-loading')
+ end
+
+ def search_for_label(label)
+ init_label_search
+ filtered_search.send_keys(label)
+ end
+
+ def click_label(text)
+ filter_dropdown.find('.filter-dropdown-item', text: text).click
+ end
+
+ def dropdown_label_size
+ filter_dropdown.all('.filter-dropdown-item').size
+ end
+
+ def clear_search_field
+ find('.filtered-search-input-container .clear-search').click
+ end
+
+ before do
+ project.add_master(user)
+ login_as(user)
+ create(:issue, project: project)
+
+ visit namespace_project_issues_path(project.namespace, project)
+ end
+
+ describe 'keyboard navigation' do
+ it 'selects label' do
+ bug_label = create(:label, project: project, title: 'bug-label')
+ init_label_search
+
+ filtered_search.native.send_keys(:down, :down, :enter)
+
+ expect(filtered_search.value).to eq("label:~#{bug_label.title} ")
+ end
+ end
+
+ describe 'behavior' do
+ it 'opens when the search bar has label:' do
+ filtered_search.set('label:')
+
+ expect(page).to have_css(js_dropdown_label)
+ end
+
+ it 'closes when the search bar is unfocused' do
+ find('body').click
+
+ expect(page).not_to have_css(js_dropdown_label)
+ end
+
+ it 'shows loading indicator when opened and hides it when loaded' do
+ filtered_search.set('label:')
+
+ expect(find(js_dropdown_label)).to have_css('.filter-dropdown-loading')
+ expect(find(js_dropdown_label)).not_to have_css('.filter-dropdown-loading')
+ end
+
+ it 'loads all the labels when opened' do
+ bug_label = create(:label, project: project, title: 'bug-label')
+ filtered_search.set('label:')
+
+ expect(filter_dropdown).to have_content(bug_label.title)
+ expect(dropdown_label_size).to eq(1)
+ end
+ end
+
+ describe 'filtering' do
+ include_context 'with labels'
+
+ before do
+ init_label_search
+ end
+
+ it 'filters by case-insensitive name with or without symbol' do
+ search_for_label('b')
+
+ expect(filter_dropdown.find('.filter-dropdown-item', text: bug_label.title)).to be_visible
+ expect(filter_dropdown.find('.filter-dropdown-item', text: uppercase_label.title)).to be_visible
+ expect(dropdown_label_size).to eq(2)
+
+ clear_search_field
+ init_label_search
+
+ search_for_label('~bu')
+
+ expect(filter_dropdown.find('.filter-dropdown-item', text: bug_label.title)).to be_visible
+ expect(filter_dropdown.find('.filter-dropdown-item', text: uppercase_label.title)).to be_visible
+ expect(dropdown_label_size).to eq(2)
+ end
+
+ it 'filters by multiple words with or without symbol' do
+ filtered_search.send_keys('Hig')
+
+ expect(filter_dropdown.find('.filter-dropdown-item', text: two_words_label.title)).to be_visible
+ expect(dropdown_label_size).to eq(1)
+
+ clear_search_field
+ init_label_search
+
+ filtered_search.send_keys('~Hig')
+
+ expect(filter_dropdown.find('.filter-dropdown-item', text: two_words_label.title)).to be_visible
+ expect(dropdown_label_size).to eq(1)
+ end
+
+ it 'filters by multiple words containing single quotes with or without symbol' do
+ filtered_search.send_keys('won\'t')
+
+ expect(filter_dropdown.find('.filter-dropdown-item', text: wont_fix_single_label.title)).to be_visible
+ expect(dropdown_label_size).to eq(1)
+
+ clear_search_field
+ init_label_search
+
+ filtered_search.send_keys('~won\'t')
+
+ expect(filter_dropdown.find('.filter-dropdown-item', text: wont_fix_single_label.title)).to be_visible
+ expect(dropdown_label_size).to eq(1)
+ end
+
+ it 'filters by multiple words containing double quotes with or without symbol' do
+ filtered_search.send_keys('won"t')
+
+ expect(filter_dropdown.find('.filter-dropdown-item', text: wont_fix_label.title)).to be_visible
+ expect(dropdown_label_size).to eq(1)
+
+ clear_search_field
+ init_label_search
+
+ filtered_search.send_keys('~won"t')
+
+ expect(filter_dropdown.find('.filter-dropdown-item', text: wont_fix_label.title)).to be_visible
+ expect(dropdown_label_size).to eq(1)
+ end
+
+ it 'filters by special characters with or without symbol' do
+ filtered_search.send_keys('^+')
+
+ expect(filter_dropdown.find('.filter-dropdown-item', text: special_label.title)).to be_visible
+ expect(dropdown_label_size).to eq(1)
+
+ clear_search_field
+ init_label_search
+
+ filtered_search.send_keys('~^+')
+
+ expect(filter_dropdown.find('.filter-dropdown-item', text: special_label.title)).to be_visible
+ expect(dropdown_label_size).to eq(1)
+ end
+ end
+
+ describe 'selecting from dropdown' do
+ include_context 'with labels'
+
+ before do
+ init_label_search
+ end
+
+ it 'fills in the label name when the label has not been filled' do
+ click_label(bug_label.title)
+
+ expect(page).not_to have_css(js_dropdown_label)
+ expect(filtered_search.value).to eq("label:~#{bug_label.title} ")
+ end
+
+ it 'fills in the label name when the label is partially filled' do
+ filtered_search.send_keys('bu')
+ click_label(bug_label.title)
+
+ expect(page).not_to have_css(js_dropdown_label)
+ expect(filtered_search.value).to eq("label:~#{bug_label.title} ")
+ end
+
+ it 'fills in the label name that contains multiple words' do
+ click_label(two_words_label.title)
+
+ expect(page).not_to have_css(js_dropdown_label)
+ expect(filtered_search.value).to eq("label:~\"#{two_words_label.title}\" ")
+ end
+
+ it 'fills in the label name that contains multiple words and is very long' do
+ click_label(long_label.title)
+
+ expect(page).not_to have_css(js_dropdown_label)
+ expect(filtered_search.value).to eq("label:~\"#{long_label.title}\" ")
+ end
+
+ it 'fills in the label name that contains double quotes' do
+ click_label(wont_fix_label.title)
+
+ expect(page).not_to have_css(js_dropdown_label)
+ expect(filtered_search.value).to eq("label:~'#{wont_fix_label.title}' ")
+ end
+
+ it 'fills in the label name with the correct capitalization' do
+ click_label(uppercase_label.title)
+
+ expect(page).not_to have_css(js_dropdown_label)
+ expect(filtered_search.value).to eq("label:~#{uppercase_label.title} ")
+ end
+
+ it 'fills in the label name with special characters' do
+ click_label(special_label.title)
+
+ expect(page).not_to have_css(js_dropdown_label)
+ expect(filtered_search.value).to eq("label:~#{special_label.title} ")
+ end
+
+ it 'selects `no label`' do
+ find("#{js_dropdown_label} .filter-dropdown-item", text: 'No Label').click
+
+ expect(page).not_to have_css(js_dropdown_label)
+ expect(filtered_search.value).to eq("label:none ")
+ end
+ end
+
+ describe 'input has existing content' do
+ it 'opens label dropdown with existing search term' do
+ filtered_search.set('searchTerm label:')
+
+ expect(page).to have_css(js_dropdown_label)
+ end
+
+ it 'opens label dropdown with existing author' do
+ filtered_search.set('author:@person label:')
+
+ expect(page).to have_css(js_dropdown_label)
+ end
+
+ it 'opens label dropdown with existing assignee' do
+ filtered_search.set('assignee:@person label:')
+
+ expect(page).to have_css(js_dropdown_label)
+ end
+
+ it 'opens label dropdown with existing label' do
+ filtered_search.set('label:~urgent label:')
+
+ expect(page).to have_css(js_dropdown_label)
+ end
+
+ it 'opens label dropdown with existing milestone' do
+ filtered_search.set('milestone:%v2.0 label:')
+
+ expect(page).to have_css(js_dropdown_label)
+ end
+ end
+
+ describe 'caching requests' do
+ it 'caches requests after the first load' do
+ create(:label, project: project, title: 'bug-label')
+ init_label_search
+
+ expect(dropdown_label_size).to eq(1)
+
+ create(:label, project: project)
+ clear_search_field
+ init_label_search
+
+ expect(dropdown_label_size).to eq(1)
+ end
+ end
+end
diff --git a/spec/features/issues/filtered_search/dropdown_milestone_spec.rb b/spec/features/issues/filtered_search/dropdown_milestone_spec.rb
new file mode 100644
index 00000000000..0ce16715b86
--- /dev/null
+++ b/spec/features/issues/filtered_search/dropdown_milestone_spec.rb
@@ -0,0 +1,239 @@
+require 'rails_helper'
+
+describe 'Dropdown milestone', js: true, feature: true do
+ include WaitForAjax
+
+ let!(:project) { create(:empty_project) }
+ let!(:user) { create(:user) }
+ let!(:milestone) { create(:milestone, title: 'v1.0', project: project) }
+ let!(:uppercase_milestone) { create(:milestone, title: 'CAP_MILESTONE', project: project) }
+ let!(:two_words_milestone) { create(:milestone, title: 'Future Plan', project: project) }
+ let!(:wont_fix_milestone) { create(:milestone, title: 'Won"t Fix', project: project) }
+ let!(:special_milestone) { create(:milestone, title: '!@#$%^&*(+)', project: project) }
+ let!(:long_milestone) { create(:milestone, title: 'this is a very long title this is a very long title this is a very long title this is a very long title this is a very long title', project: project) }
+
+ let(:filtered_search) { find('.filtered-search') }
+ let(:js_dropdown_milestone) { '#js-dropdown-milestone' }
+
+ def send_keys_to_filtered_search(input)
+ input.split("").each do |i|
+ filtered_search.send_keys(i)
+ sleep 3
+ wait_for_ajax
+ sleep 3
+ end
+ end
+
+ def dropdown_milestone_size
+ page.all('#js-dropdown-milestone .filter-dropdown .filter-dropdown-item').size
+ end
+
+ def click_milestone(text)
+ find('#js-dropdown-milestone .filter-dropdown .filter-dropdown-item', text: text).click
+ end
+
+ def click_static_milestone(text)
+ find('#js-dropdown-milestone .filter-dropdown-item', text: text).click
+ end
+
+ before do
+ project.team << [user, :master]
+ login_as(user)
+ create(:issue, project: project)
+
+ visit namespace_project_issues_path(project.namespace, project)
+ end
+
+ describe 'behavior' do
+ it 'opens when the search bar has milestone:' do
+ filtered_search.set('milestone:')
+
+ expect(page).to have_css(js_dropdown_milestone, visible: true)
+ end
+
+ it 'closes when the search bar is unfocused' do
+ find('body').click()
+
+ expect(page).to have_css(js_dropdown_milestone, visible: false)
+ end
+
+ it 'should show loading indicator when opened' do
+ filtered_search.set('milestone:')
+
+ expect(page).to have_css('#js-dropdown-milestone .filter-dropdown-loading', visible: true)
+ end
+
+ it 'should hide loading indicator when loaded' do
+ send_keys_to_filtered_search('milestone:')
+
+ expect(page).not_to have_css('#js-dropdown-milestone .filter-dropdown-loading')
+ end
+
+ it 'should load all the milestones when opened' do
+ send_keys_to_filtered_search('milestone:')
+
+ expect(dropdown_milestone_size).to be > 0
+ end
+ end
+
+ describe 'filtering' do
+ before do
+ filtered_search.set('milestone')
+ end
+
+ it 'filters by name' do
+ send_keys_to_filtered_search(':v1')
+
+ expect(dropdown_milestone_size).to eq(1)
+ end
+
+ it 'filters by case insensitive name' do
+ send_keys_to_filtered_search(':V1')
+
+ expect(dropdown_milestone_size).to eq(1)
+ end
+
+ it 'filters by name with symbol' do
+ send_keys_to_filtered_search(':%v1')
+
+ expect(dropdown_milestone_size).to eq(1)
+ end
+
+ it 'filters by case insensitive name with symbol' do
+ send_keys_to_filtered_search(':%V1')
+
+ expect(dropdown_milestone_size).to eq(1)
+ end
+
+ it 'filters by special characters' do
+ send_keys_to_filtered_search(':(+')
+
+ expect(dropdown_milestone_size).to eq(1)
+ end
+
+ it 'filters by special characters with symbol' do
+ send_keys_to_filtered_search(':%(+')
+
+ expect(dropdown_milestone_size).to eq(1)
+ end
+ end
+
+ describe 'selecting from dropdown' do
+ before do
+ filtered_search.set('milestone:')
+ end
+
+ it 'fills in the milestone name when the milestone has not been filled' do
+ click_milestone(milestone.title)
+
+ expect(page).to have_css(js_dropdown_milestone, visible: false)
+ expect(filtered_search.value).to eq("milestone:%#{milestone.title} ")
+ end
+
+ it 'fills in the milestone name when the milestone is partially filled' do
+ send_keys_to_filtered_search('v')
+ click_milestone(milestone.title)
+
+ expect(page).to have_css(js_dropdown_milestone, visible: false)
+ expect(filtered_search.value).to eq("milestone:%#{milestone.title} ")
+ end
+
+ it 'fills in the milestone name that contains multiple words' do
+ click_milestone(two_words_milestone.title)
+
+ expect(page).to have_css(js_dropdown_milestone, visible: false)
+ expect(filtered_search.value).to eq("milestone:%\"#{two_words_milestone.title}\" ")
+ end
+
+ it 'fills in the milestone name that contains multiple words and is very long' do
+ click_milestone(long_milestone.title)
+
+ expect(page).to have_css(js_dropdown_milestone, visible: false)
+ expect(filtered_search.value).to eq("milestone:%\"#{long_milestone.title}\" ")
+ end
+
+ it 'fills in the milestone name that contains double quotes' do
+ click_milestone(wont_fix_milestone.title)
+
+ expect(page).to have_css(js_dropdown_milestone, visible: false)
+ expect(filtered_search.value).to eq("milestone:%'#{wont_fix_milestone.title}' ")
+ end
+
+ it 'fills in the milestone name with the correct capitalization' do
+ click_milestone(uppercase_milestone.title)
+
+ expect(page).to have_css(js_dropdown_milestone, visible: false)
+ expect(filtered_search.value).to eq("milestone:%#{uppercase_milestone.title} ")
+ end
+
+ it 'fills in the milestone name with special characters' do
+ click_milestone(special_milestone.title)
+
+ expect(page).to have_css(js_dropdown_milestone, visible: false)
+ expect(filtered_search.value).to eq("milestone:%#{special_milestone.title} ")
+ end
+
+ it 'selects `no milestone`' do
+ click_static_milestone('No Milestone')
+
+ expect(page).to have_css(js_dropdown_milestone, visible: false)
+ expect(filtered_search.value).to eq("milestone:none ")
+ end
+
+ it 'selects `upcoming milestone`' do
+ click_static_milestone('Upcoming')
+
+ expect(page).to have_css(js_dropdown_milestone, visible: false)
+ expect(filtered_search.value).to eq("milestone:upcoming ")
+ end
+ end
+
+ describe 'input has existing content' do
+ it 'opens milestone dropdown with existing search term' do
+ filtered_search.set('searchTerm milestone:')
+
+ expect(page).to have_css(js_dropdown_milestone, visible: true)
+ end
+
+ it 'opens milestone dropdown with existing author' do
+ filtered_search.set('author:@john milestone:')
+
+ expect(page).to have_css(js_dropdown_milestone, visible: true)
+ end
+
+ it 'opens milestone dropdown with existing assignee' do
+ filtered_search.set('assignee:@john milestone:')
+
+ expect(page).to have_css(js_dropdown_milestone, visible: true)
+ end
+
+ it 'opens milestone dropdown with existing label' do
+ filtered_search.set('label:~important milestone:')
+
+ expect(page).to have_css(js_dropdown_milestone, visible: true)
+ end
+
+ it 'opens milestone dropdown with existing milestone' do
+ filtered_search.set('milestone:%100 milestone:')
+
+ expect(page).to have_css(js_dropdown_milestone, visible: true)
+ end
+ end
+
+ describe 'caching requests' do
+ it 'caches requests after the first load' do
+ filtered_search.set('milestone')
+ send_keys_to_filtered_search(':')
+ initial_size = dropdown_milestone_size
+
+ expect(initial_size).to be > 0
+
+ create(:milestone, project: project)
+ find('.filtered-search-input-container .clear-search').click
+ filtered_search.set('milestone')
+ send_keys_to_filtered_search(':')
+
+ expect(dropdown_milestone_size).to eq(initial_size)
+ end
+ end
+end
diff --git a/spec/features/issues/filtered_search/filter_issues_spec.rb b/spec/features/issues/filtered_search/filter_issues_spec.rb
new file mode 100644
index 00000000000..3f70a6aa75f
--- /dev/null
+++ b/spec/features/issues/filtered_search/filter_issues_spec.rb
@@ -0,0 +1,804 @@
+require 'rails_helper'
+
+describe 'Filter issues', js: true, feature: true do
+ include WaitForAjax
+
+ let!(:group) { create(:group) }
+ let!(:project) { create(:project, group: group) }
+ let!(:user) { create(:user) }
+ let!(:user2) { create(:user) }
+ let!(:milestone) { create(:milestone, project: project) }
+ let!(:label) { create(:label, project: project) }
+ let!(:wontfix) { create(:label, project: project, title: "Won't fix") }
+
+ let!(:bug_label) { create(:label, project: project, title: 'bug') }
+ let!(:caps_sensitive_label) { create(:label, project: project, title: 'CAPS_sensitive') }
+ let!(:milestone) { create(:milestone, title: "8", project: project) }
+ let!(:multiple_words_label) { create(:label, project: project, title: "Two words") }
+
+ let!(:closed_issue) { create(:issue, title: 'bug that is closed', project: project, state: :closed) }
+ let(:filtered_search) { find('.filtered-search') }
+
+ def input_filtered_search(search_term, submit: true)
+ filtered_search.set(search_term)
+
+ if submit
+ filtered_search.send_keys(:enter)
+ end
+ end
+
+ def expect_filtered_search_input(input)
+ expect(find('.filtered-search').value).to eq(input)
+ end
+
+ def expect_no_issues_list
+ page.within '.issues-list' do
+ expect(page).not_to have_selector('.issue')
+ end
+ end
+
+ def expect_issues_list_count(open_count, closed_count = 0)
+ all_count = open_count + closed_count
+
+ expect(page).to have_issuable_counts(open: open_count, closed: closed_count, all: all_count)
+ page.within '.issues-list' do
+ expect(page).to have_selector('.issue', count: open_count)
+ end
+ end
+
+ def select_search_at_index(pos)
+ evaluate_script("el = document.querySelector('.filtered-search'); el.focus(); el.setSelectionRange(#{pos}, #{pos});")
+ end
+
+ before do
+ project.team << [user, :master]
+ project.team << [user2, :master]
+ group.add_developer(user)
+ group.add_developer(user2)
+ login_as(user)
+ create(:issue, project: project)
+
+ create(:issue, title: "Bug report 1", project: project)
+ create(:issue, title: "Bug report 2", project: project)
+ create(:issue, title: "issue with 'single quotes'", project: project)
+ create(:issue, title: "issue with \"double quotes\"", project: project)
+ create(:issue, title: "issue with !@\#{$%^&*()-+", project: project)
+ create(:issue, title: "issue by assignee", project: project, milestone: milestone, author: user, assignee: user)
+ create(:issue, title: "issue by assignee with searchTerm", project: project, milestone: milestone, author: user, assignee: user)
+
+ issue = create(:issue,
+ title: "Bug 2",
+ project: project,
+ milestone: milestone,
+ author: user,
+ assignee: user)
+ issue.labels << bug_label
+
+ issue_with_caps_label = create(:issue,
+ title: "issue by assignee with searchTerm and label",
+ project: project,
+ milestone: milestone,
+ author: user,
+ assignee: user)
+ issue_with_caps_label.labels << caps_sensitive_label
+
+ issue_with_everything = create(:issue,
+ title: "Bug report with everything you thought was possible",
+ project: project,
+ milestone: milestone,
+ author: user,
+ assignee: user)
+ issue_with_everything.labels << bug_label
+ issue_with_everything.labels << caps_sensitive_label
+
+ multiple_words_label_issue = create(:issue, title: "Issue with multiple words label", project: project)
+ multiple_words_label_issue.labels << multiple_words_label
+
+ future_milestone = create(:milestone, title: "future", project: project, due_date: Time.now + 1.month)
+
+ create(:issue,
+ title: "Issue with future milestone",
+ milestone: future_milestone,
+ project: project)
+
+ visit namespace_project_issues_path(project.namespace, project)
+ end
+
+ describe 'filter issues by author' do
+ context 'only author' do
+ it 'filters issues by searched author' do
+ input_filtered_search("author:@#{user.username}")
+
+ expect_issues_list_count(5)
+ end
+
+ it 'filters issues by invalid author' do
+ pending('to be tested, issue #26546')
+ expect(true).to be(false)
+ end
+
+ it 'filters issues by multiple authors' do
+ pending('to be tested, issue #26546')
+ expect(true).to be(false)
+ end
+ end
+
+ context 'author with other filters' do
+ it 'filters issues by searched author and text' do
+ search = "author:@#{user.username} issue"
+ input_filtered_search(search)
+
+ expect_issues_list_count(3)
+ expect_filtered_search_input(search)
+ end
+
+ it 'filters issues by searched author, assignee and text' do
+ search = "author:@#{user.username} assignee:@#{user.username} issue"
+ input_filtered_search(search)
+
+ expect_issues_list_count(3)
+ expect_filtered_search_input(search)
+ end
+
+ it 'filters issues by searched author, assignee, label, and text' do
+ search = "author:@#{user.username} assignee:@#{user.username} label:~#{caps_sensitive_label.title} issue"
+ input_filtered_search(search)
+
+ expect_issues_list_count(1)
+ expect_filtered_search_input(search)
+ end
+
+ it 'filters issues by searched author, assignee, label, milestone and text' do
+ search = "author:@#{user.username} assignee:@#{user.username} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} issue"
+ input_filtered_search(search)
+
+ expect_issues_list_count(1)
+ expect_filtered_search_input(search)
+ end
+ end
+
+ it 'sorting' do
+ pending('to be tested, issue #26546')
+ expect(true).to be(false)
+ end
+ end
+
+ describe 'filter issues by assignee' do
+ context 'only assignee' do
+ it 'filters issues by searched assignee' do
+ search = "assignee:@#{user.username}"
+ input_filtered_search(search)
+
+ expect_issues_list_count(5)
+ expect_filtered_search_input(search)
+ end
+
+ it 'filters issues by no assignee' do
+ search = "assignee:none"
+ input_filtered_search(search)
+
+ expect_issues_list_count(8, 1)
+ expect_filtered_search_input(search)
+ end
+
+ it 'filters issues by invalid assignee' do
+ pending('to be tested, issue #26546')
+ expect(true).to be(false)
+ end
+
+ it 'filters issues by multiple assignees' do
+ pending('to be tested, issue #26546')
+ expect(true).to be(false)
+ end
+ end
+
+ context 'assignee with other filters' do
+ it 'filters issues by searched assignee and text' do
+ search = "assignee:@#{user.username} searchTerm"
+ input_filtered_search(search)
+
+ expect_issues_list_count(2)
+ expect_filtered_search_input(search)
+ end
+
+ it 'filters issues by searched assignee, author and text' do
+ search = "assignee:@#{user.username} author:@#{user.username} searchTerm"
+ input_filtered_search(search)
+
+ expect_issues_list_count(2)
+ expect_filtered_search_input(search)
+ end
+
+ it 'filters issues by searched assignee, author, label, text' do
+ search = "assignee:@#{user.username} author:@#{user.username} label:~#{caps_sensitive_label.title} searchTerm"
+ input_filtered_search(search)
+
+ expect_issues_list_count(1)
+ expect_filtered_search_input(search)
+ end
+
+ it 'filters issues by searched assignee, author, label, milestone and text' do
+ search = "assignee:@#{user.username} author:@#{user.username} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} searchTerm"
+ input_filtered_search(search)
+
+ expect_issues_list_count(1)
+ expect_filtered_search_input(search)
+ end
+ end
+
+ context 'sorting' do
+ it 'sorts' do
+ pending('to be tested, issue #26546')
+ expect(true).to be(false)
+ end
+ end
+ end
+
+ describe 'filter issues by label' do
+ context 'only label' do
+ it 'filters issues by searched label' do
+ search = "label:~#{bug_label.title}"
+ input_filtered_search(search)
+
+ expect_issues_list_count(2)
+ expect_filtered_search_input(search)
+ end
+
+ it 'filters issues by no label' do
+ search = "label:none"
+ input_filtered_search(search)
+
+ expect_issues_list_count(9, 1)
+ expect_filtered_search_input(search)
+ end
+
+ it 'filters issues by invalid label' do
+ pending('to be tested, issue #26546')
+ expect(true).to be(false)
+ end
+
+ it 'filters issues by multiple labels' do
+ search = "label:~#{bug_label.title} label:~#{caps_sensitive_label.title}"
+ input_filtered_search(search)
+
+ expect_issues_list_count(1)
+ expect_filtered_search_input(search)
+ end
+
+ it 'filters issues by label containing special characters' do
+ special_label = create(:label, project: project, title: '!@#{$%^&*()-+[]<>?/:{}|\}')
+ special_issue = create(:issue, title: "Issue with special character label", project: project)
+ special_issue.labels << special_label
+
+ search = "label:~#{special_label.title}"
+ input_filtered_search(search)
+
+ expect_issues_list_count(1)
+ expect_filtered_search_input(search)
+ end
+
+ it 'does not show issues' do
+ new_label = create(:label, project: project, title: "new_label")
+
+ search = "label:~#{new_label.title}"
+ input_filtered_search(search)
+
+ expect_no_issues_list()
+ expect_filtered_search_input(search)
+ end
+ end
+
+ context 'label with multiple words' do
+ it 'special characters' do
+ special_multiple_label = create(:label, project: project, title: "Utmost |mp0rt@nce")
+ special_multiple_issue = create(:issue, title: "Issue with special character multiple words label", project: project)
+ special_multiple_issue.labels << special_multiple_label
+
+ search = "label:~'#{special_multiple_label.title}'"
+ input_filtered_search(search)
+
+ expect_issues_list_count(1)
+
+ # filtered search defaults quotations to double quotes
+ expect_filtered_search_input("label:~\"#{special_multiple_label.title}\"")
+ end
+
+ it 'single quotes' do
+ search = "label:~'#{multiple_words_label.title}'"
+ input_filtered_search(search)
+
+ expect_issues_list_count(1)
+ expect_filtered_search_input("label:~\"#{multiple_words_label.title}\"")
+ end
+
+ it 'double quotes' do
+ search = "label:~\"#{multiple_words_label.title}\""
+ input_filtered_search(search)
+
+ expect_issues_list_count(1)
+ expect_filtered_search_input(search)
+ end
+
+ it 'single quotes containing double quotes' do
+ double_quotes_label = create(:label, project: project, title: 'won"t fix')
+ double_quotes_label_issue = create(:issue, title: "Issue with double quotes label", project: project)
+ double_quotes_label_issue.labels << double_quotes_label
+
+ search = "label:~'#{double_quotes_label.title}'"
+ input_filtered_search(search)
+
+ expect_issues_list_count(1)
+ expect_filtered_search_input(search)
+ end
+
+ it 'double quotes containing single quotes' do
+ single_quotes_label = create(:label, project: project, title: "won't fix")
+ single_quotes_label_issue = create(:issue, title: "Issue with single quotes label", project: project)
+ single_quotes_label_issue.labels << single_quotes_label
+
+ search = "label:~\"#{single_quotes_label.title}\""
+ input_filtered_search(search)
+
+ expect_issues_list_count(1)
+ expect_filtered_search_input(search)
+ end
+ end
+
+ context 'label with other filters' do
+ it 'filters issues by searched label and text' do
+ search = "label:~#{caps_sensitive_label.title} bug"
+ input_filtered_search(search)
+
+ expect_issues_list_count(1)
+ expect_filtered_search_input(search)
+ end
+
+ it 'filters issues by searched label, author and text' do
+ search = "label:~#{caps_sensitive_label.title} author:@#{user.username} bug"
+ input_filtered_search(search)
+
+ expect_issues_list_count(1)
+ expect_filtered_search_input(search)
+ end
+
+ it 'filters issues by searched label, author, assignee and text' do
+ search = "label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} bug"
+ input_filtered_search(search)
+
+ expect_issues_list_count(1)
+ expect_filtered_search_input(search)
+ end
+
+ it 'filters issues by searched label, author, assignee, milestone and text' do
+ search = "label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} milestone:%#{milestone.title} bug"
+ input_filtered_search(search)
+
+ expect_issues_list_count(1)
+ expect_filtered_search_input(search)
+ end
+ end
+
+ context 'multiple labels with other filters' do
+ it 'filters issues by searched label, label2, and text' do
+ search = "label:~#{bug_label.title} label:~#{caps_sensitive_label.title} bug"
+ input_filtered_search(search)
+
+ expect_issues_list_count(1)
+ expect_filtered_search_input(search)
+ end
+
+ it 'filters issues by searched label, label2, author and text' do
+ search = "label:~#{bug_label.title} label:~#{caps_sensitive_label.title} author:@#{user.username} bug"
+ input_filtered_search(search)
+
+ expect_issues_list_count(1)
+ expect_filtered_search_input(search)
+ end
+
+ it 'filters issues by searched label, label2, author, assignee and text' do
+ search = "label:~#{bug_label.title} label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} bug"
+ input_filtered_search(search)
+
+ expect_issues_list_count(1)
+ expect_filtered_search_input(search)
+ end
+
+ it 'filters issues by searched label, label2, author, assignee, milestone and text' do
+ search = "label:~#{bug_label.title} label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} milestone:%#{milestone.title} bug"
+ input_filtered_search(search)
+
+ expect_issues_list_count(1)
+ expect_filtered_search_input(search)
+ end
+ end
+
+ context 'issue label clicked' do
+ before do
+ find('.issues-list .issue .issue-info a .label', text: multiple_words_label.title).click
+ sleep 1
+ end
+
+ it 'filters' do
+ expect_issues_list_count(1)
+ end
+
+ it 'displays in search bar' do
+ expect(find('.filtered-search').value).to eq("label:~\"#{multiple_words_label.title}\"")
+ end
+ end
+
+ context 'sorting' do
+ it 'sorts' do
+ pending('to be tested, issue #26546')
+ expect(true).to be(false)
+ end
+ end
+ end
+
+ describe 'filter issues by milestone' do
+ context 'only milestone' do
+ it 'filters issues by searched milestone' do
+ input_filtered_search("milestone:%#{milestone.title}")
+
+ expect_issues_list_count(5)
+ end
+
+ it 'filters issues by no milestone' do
+ input_filtered_search("milestone:none")
+
+ expect_issues_list_count(7, 1)
+ end
+
+ it 'filters issues by upcoming milestones' do
+ input_filtered_search("milestone:upcoming")
+
+ expect_issues_list_count(1)
+ end
+
+ it 'filters issues by invalid milestones' do
+ pending('to be tested, issue #26546')
+ expect(true).to be(false)
+ end
+
+ it 'filters issues by multiple milestones' do
+ pending('to be tested, issue #26546')
+ expect(true).to be(false)
+ end
+
+ it 'filters issues by milestone containing special characters' do
+ special_milestone = create(:milestone, title: '!@\#{$%^&*()}', project: project)
+ create(:issue, title: "Issue with special character milestone", project: project, milestone: special_milestone)
+
+ search = "milestone:%#{special_milestone.title}"
+ input_filtered_search(search)
+
+ expect_issues_list_count(1)
+ expect_filtered_search_input(search)
+ end
+
+ it 'does not show issues' do
+ new_milestone = create(:milestone, title: "new", project: project)
+
+ search = "milestone:%#{new_milestone.title}"
+ input_filtered_search(search)
+
+ expect_no_issues_list()
+ expect_filtered_search_input(search)
+ end
+ end
+
+ context 'milestone with other filters' do
+ it 'filters issues by searched milestone and text' do
+ search = "milestone:%#{milestone.title} bug"
+ input_filtered_search(search)
+
+ expect_issues_list_count(2)
+ expect_filtered_search_input(search)
+ end
+
+ it 'filters issues by searched milestone, author and text' do
+ search = "milestone:%#{milestone.title} author:@#{user.username} bug"
+ input_filtered_search(search)
+
+ expect_issues_list_count(2)
+ expect_filtered_search_input(search)
+ end
+
+ it 'filters issues by searched milestone, author, assignee and text' do
+ search = "milestone:%#{milestone.title} author:@#{user.username} assignee:@#{user.username} bug"
+ input_filtered_search(search)
+
+ expect_issues_list_count(2)
+ expect_filtered_search_input(search)
+ end
+
+ it 'filters issues by searched milestone, author, assignee, label and text' do
+ search = "milestone:%#{milestone.title} author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} bug"
+ input_filtered_search(search)
+
+ expect_issues_list_count(2)
+ expect_filtered_search_input(search)
+ end
+ end
+
+ context 'sorting' do
+ it 'sorts' do
+ pending('to be tested, issue #26546')
+ expect(true).to be(false)
+ end
+ end
+ end
+
+ describe 'overwrites selected filter' do
+ it 'changes author' do
+ input_filtered_search("author:@#{user.username}", submit: false)
+
+ select_search_at_index(3)
+
+ page.within '#js-dropdown-author' do
+ click_button user2.username
+ end
+
+ expect(filtered_search.value).to eq("author:@#{user2.username} ")
+ end
+
+ it 'changes label' do
+ input_filtered_search("author:@#{user.username} label:~#{bug_label.title}", submit: false)
+
+ select_search_at_index(27)
+
+ page.within '#js-dropdown-label' do
+ click_button label.name
+ end
+
+ expect(filtered_search.value).to eq("author:@#{user.username} label:~#{label.name} ")
+ end
+
+ it 'changes label correctly space is in previous label' do
+ input_filtered_search("label:~\"#{multiple_words_label.title}\"", submit: false)
+
+ select_search_at_index(0)
+
+ page.within '#js-dropdown-label' do
+ click_button label.name
+ end
+
+ expect(filtered_search.value).to eq("label:~#{label.name} ")
+ end
+ end
+
+ describe 'filter issues by text' do
+ context 'only text' do
+ it 'filters issues by searched text' do
+ search = 'Bug'
+ input_filtered_search(search)
+
+ expect_issues_list_count(4, 1)
+ expect_filtered_search_input(search)
+ end
+
+ it 'filters issues by multiple searched text' do
+ search = 'Bug report'
+ input_filtered_search(search)
+
+ expect_issues_list_count(3)
+ expect_filtered_search_input(search)
+ end
+
+ it 'filters issues by case insensitive searched text' do
+ search = 'bug report'
+ input_filtered_search(search)
+
+ expect_issues_list_count(3)
+ expect_filtered_search_input(search)
+ end
+
+ it 'filters issues by searched text containing single quotes' do
+ search = '\'single quotes\''
+ input_filtered_search(search)
+
+ expect_issues_list_count(1)
+ expect_filtered_search_input(search)
+ end
+
+ it 'filters issues by searched text containing double quotes' do
+ search = '"double quotes"'
+ input_filtered_search(search)
+
+ expect_issues_list_count(1)
+ expect_filtered_search_input(search)
+ end
+
+ it 'filters issues by searched text containing special characters' do
+ search = '!@#{$%^&*()-+'
+ input_filtered_search(search)
+
+ expect_issues_list_count(1)
+ expect_filtered_search_input(search)
+ end
+
+ it 'does not show any issues' do
+ search = 'testing'
+ input_filtered_search(search)
+
+ expect_no_issues_list()
+ expect_filtered_search_input(search)
+ end
+ end
+
+ context 'searched text with other filters' do
+ it 'filters issues by searched text and author' do
+ input_filtered_search("bug author:@#{user.username}")
+
+ expect_issues_list_count(2)
+ expect_filtered_search_input("author:@#{user.username} bug")
+ end
+
+ it 'filters issues by searched text, author and more text' do
+ input_filtered_search("bug author:@#{user.username} report")
+
+ expect_issues_list_count(1)
+ expect_filtered_search_input("author:@#{user.username} bug report")
+ end
+
+ it 'filters issues by searched text, author and assignee' do
+ input_filtered_search("bug author:@#{user.username} assignee:@#{user.username}")
+
+ expect_issues_list_count(2)
+ expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} bug")
+ end
+
+ it 'filters issues by searched text, author, more text and assignee' do
+ input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username}")
+
+ expect_issues_list_count(1)
+ expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} bug report")
+ end
+
+ it 'filters issues by searched text, author, more text, assignee and even more text' do
+ input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username} with")
+
+ expect_issues_list_count(1)
+ expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} bug report with")
+ end
+
+ it 'filters issues by searched text, author, assignee and label' do
+ input_filtered_search("bug author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title}")
+
+ expect_issues_list_count(2)
+ expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} bug")
+ end
+
+ it 'filters issues by searched text, author, text, assignee, text, label and text' do
+ input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username} with label:~#{bug_label.title} everything")
+
+ expect_issues_list_count(1)
+ expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} bug report with everything")
+ end
+
+ it 'filters issues by searched text, author, assignee, label and milestone' do
+ input_filtered_search("bug author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} milestone:%#{milestone.title}")
+
+ expect_issues_list_count(2)
+ expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} milestone:%#{milestone.title} bug")
+ end
+
+ it 'filters issues by searched text, author, text, assignee, text, label, text, milestone and text' do
+ input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username} with label:~#{bug_label.title} everything milestone:%#{milestone.title} you")
+
+ expect_issues_list_count(1)
+ expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} milestone:%#{milestone.title} bug report with everything you")
+ end
+
+ it 'filters issues by searched text, author, assignee, multiple labels and milestone' do
+ input_filtered_search("bug author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title}")
+
+ expect_issues_list_count(1)
+ expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} bug")
+ end
+
+ it 'filters issues by searched text, author, text, assignee, text, label1, text, label2, text, milestone and text' do
+ input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username} with label:~#{bug_label.title} everything label:~#{caps_sensitive_label.title} you milestone:%#{milestone.title} thought")
+
+ expect_issues_list_count(1)
+ expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} bug report with everything you thought")
+ end
+ end
+
+ context 'sorting' do
+ it 'sorts by oldest updated' do
+ create(:issue,
+ title: '3 days ago',
+ project: project,
+ author: user,
+ created_at: 3.days.ago,
+ updated_at: 3.days.ago)
+
+ old_issue = create(:issue,
+ title: '5 days ago',
+ project: project,
+ author: user,
+ created_at: 5.days.ago,
+ updated_at: 5.days.ago)
+
+ input_filtered_search('days ago')
+
+ expect_issues_list_count(2)
+
+ sort_toggle = find('.filtered-search-container .dropdown-toggle')
+ sort_toggle.click
+
+ find('.filtered-search-container .dropdown-menu li a', text: 'Oldest updated').click
+ wait_for_ajax
+
+ expect(find('.issues-list .issue:first-of-type .issue-title-text a')).to have_content(old_issue.title)
+ end
+ end
+ end
+
+ describe 'retains filter when switching issue states' do
+ before do
+ input_filtered_search('bug')
+
+ # Wait for search results to load
+ sleep 2
+ end
+
+ it 'open state' do
+ find('.issues-state-filters a', text: 'Closed').click
+ wait_for_ajax
+
+ find('.issues-state-filters a', text: 'Open').click
+ wait_for_ajax
+
+ expect(page).to have_selector('.issues-list .issue', count: 4)
+ end
+
+ it 'closed state' do
+ find('.issues-state-filters a', text: 'Closed').click
+ wait_for_ajax
+
+ expect(page).to have_selector('.issues-list .issue', count: 1)
+ expect(find('.issues-list .issue:first-of-type .issue-title-text a')).to have_content(closed_issue.title)
+ end
+
+ it 'all state' do
+ find('.issues-state-filters a', text: 'All').click
+ wait_for_ajax
+
+ expect(page).to have_selector('.issues-list .issue', count: 5)
+ end
+ end
+
+ describe 'RSS feeds' do
+ it 'updates atom feed link for project issues' do
+ visit namespace_project_issues_path(project.namespace, project, milestone_title: milestone.title, assignee_id: user.id)
+ link = find_link('Subscribe')
+ params = CGI.parse(URI.parse(link[:href]).query)
+ auto_discovery_link = find('link[type="application/atom+xml"]', visible: false)
+ auto_discovery_params = CGI.parse(URI.parse(auto_discovery_link[:href]).query)
+
+ expect(params).to include('private_token' => [user.private_token])
+ expect(params).to include('milestone_title' => [milestone.title])
+ expect(params).to include('assignee_id' => [user.id.to_s])
+ expect(auto_discovery_params).to include('private_token' => [user.private_token])
+ expect(auto_discovery_params).to include('milestone_title' => [milestone.title])
+ expect(auto_discovery_params).to include('assignee_id' => [user.id.to_s])
+ end
+
+ it 'updates atom feed link for group issues' do
+ visit issues_group_path(group, milestone_title: milestone.title, assignee_id: user.id)
+ link = find('.nav-controls a', text: 'Subscribe')
+ params = CGI.parse(URI.parse(link[:href]).query)
+ auto_discovery_link = find('link[type="application/atom+xml"]', visible: false)
+ auto_discovery_params = CGI.parse(URI.parse(auto_discovery_link[:href]).query)
+
+ expect(params).to include('private_token' => [user.private_token])
+ expect(params).to include('milestone_title' => [milestone.title])
+ expect(params).to include('assignee_id' => [user.id.to_s])
+ expect(auto_discovery_params).to include('private_token' => [user.private_token])
+ expect(auto_discovery_params).to include('milestone_title' => [milestone.title])
+ expect(auto_discovery_params).to include('assignee_id' => [user.id.to_s])
+ end
+ end
+end
diff --git a/spec/features/issues/filtered_search/search_bar_spec.rb b/spec/features/issues/filtered_search/search_bar_spec.rb
new file mode 100644
index 00000000000..90eb60eb337
--- /dev/null
+++ b/spec/features/issues/filtered_search/search_bar_spec.rb
@@ -0,0 +1,104 @@
+require 'rails_helper'
+
+describe 'Search bar', js: true, feature: true do
+ include WaitForAjax
+
+ let!(:project) { create(:empty_project) }
+ let!(:user) { create(:user) }
+ let(:filtered_search) { find('.filtered-search') }
+
+ before do
+ project.team << [user, :master]
+ login_as(user)
+ create(:issue, project: project)
+
+ visit namespace_project_issues_path(project.namespace, project)
+ end
+
+ def get_left_style(style)
+ left_style = /left:\s\d*[.]\d*px/.match(style)
+ left_style.to_s.gsub('left: ', '').to_f
+ end
+
+ describe 'keyboard navigation' do
+ it 'makes item active' do
+ filtered_search.native.send_keys(:down)
+
+ page.within '#js-dropdown-hint' do
+ expect(page).to have_selector('.dropdown-active')
+ end
+ end
+
+ it 'selects item' do
+ filtered_search.native.send_keys(:down, :down, :enter)
+
+ expect(filtered_search.value).to eq('author:')
+ end
+ end
+
+ describe 'clear search button' do
+ it 'clears text' do
+ search_text = 'search_text'
+ filtered_search.set(search_text)
+
+ expect(filtered_search.value).to eq(search_text)
+ find('.filtered-search-input-container .clear-search').click
+
+ expect(filtered_search.value).to eq('')
+ end
+
+ it 'hides by default' do
+ expect(page).to have_css('.clear-search', visible: false)
+ end
+
+ it 'hides after clicked' do
+ filtered_search.set('a')
+ find('.filtered-search-input-container .clear-search').click
+
+ expect(page).to have_css('.clear-search', visible: false)
+ end
+
+ it 'hides when there is no text' do
+ filtered_search.set('a')
+ filtered_search.set('')
+
+ expect(page).to have_css('.clear-search', visible: false)
+ end
+
+ it 'shows when there is text' do
+ filtered_search.set('a')
+
+ expect(page).to have_css('.clear-search', visible: true)
+ end
+
+ it 'resets the dropdown hint filter' do
+ filtered_search.click
+ original_size = page.all('#js-dropdown-hint .filter-dropdown .filter-dropdown-item').size
+
+ filtered_search.set('author')
+
+ expect(page.all('#js-dropdown-hint .filter-dropdown .filter-dropdown-item').size).to eq(1)
+
+ find('.filtered-search-input-container .clear-search').click
+ filtered_search.click
+
+ expect(page.all('#js-dropdown-hint .filter-dropdown .filter-dropdown-item').size).to eq(original_size)
+ end
+
+ it 'resets the dropdown filters' do
+ filtered_search.set('a')
+ hint_style = page.find('#js-dropdown-hint')['style']
+ hint_offset = get_left_style(hint_style)
+
+ filtered_search.set('author:')
+
+ expect(page.all('#js-dropdown-hint .filter-dropdown .filter-dropdown-item').size).to eq(0)
+
+ find('.filtered-search-input-container .clear-search').click
+ filtered_search.click
+
+ expect(page.all('#js-dropdown-hint .filter-dropdown .filter-dropdown-item').size).to be > 0
+ expect(get_left_style(page.find('#js-dropdown-hint')['style'])).to eq(hint_offset)
+ end
+ end
+end
diff --git a/spec/features/issues/form_spec.rb b/spec/features/issues/form_spec.rb
index 8771cc8e157..741ca95f1ca 100644
--- a/spec/features/issues/form_spec.rb
+++ b/spec/features/issues/form_spec.rb
@@ -68,6 +68,22 @@ describe 'New/edit issue', feature: true, js: true do
end
end
end
+
+ it 'correctly updates the dropdown toggle when removing a label' do
+ click_button 'Labels'
+
+ page.within '.dropdown-menu-labels' do
+ click_link label.title
+ end
+
+ expect(find('.js-label-select')).to have_content(label.title)
+
+ page.within '.dropdown-menu-labels' do
+ click_link label.title
+ end
+
+ expect(find('.js-label-select')).to have_content('Labels')
+ end
end
context 'edit issue' do
diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb
index da64827b377..93139dc9e94 100644
--- a/spec/features/issues/gfm_autocomplete_spec.rb
+++ b/spec/features/issues/gfm_autocomplete_spec.rb
@@ -2,7 +2,7 @@ require 'rails_helper'
feature 'GFM autocomplete', feature: true, js: true do
include WaitForAjax
- let(:user) { create(:user, username: 'someone.special') }
+ let(:user) { create(:user, name: '💃speciąl someone💃', username: 'someone.special') }
let(:project) { create(:project) }
let(:label) { create(:label, project: project, title: 'special+') }
let(:issue) { create(:issue, project: project) }
@@ -33,13 +33,64 @@ feature 'GFM autocomplete', feature: true, js: true do
expect(page).not_to have_selector('.atwho-view')
end
+ it 'doesnt select the first item for non-assignee dropdowns' do
+ page.within '.timeline-content-form' do
+ find('#note_note').native.send_keys('')
+ find('#note_note').native.send_keys(':')
+ end
+
+ expect(page).to have_selector('.atwho-container')
+
+ wait_for_ajax
+
+ expect(find('#at-view-58')).not_to have_selector('.cur:first-of-type')
+ end
+
+ it 'selects the first item for assignee dropdowns' do
+ page.within '.timeline-content-form' do
+ find('#note_note').native.send_keys('')
+ find('#note_note').native.send_keys('@')
+ end
+
+ expect(page).to have_selector('.atwho-container')
+
+ wait_for_ajax
+
+ expect(find('#at-view-64')).to have_selector('.cur:first-of-type')
+ end
+
+ it 'includes items for assignee dropdowns with non-ASCII characters in name' do
+ page.within '.timeline-content-form' do
+ find('#note_note').native.send_keys('')
+ find('#note_note').native.send_keys("@#{user.name[0...8]}")
+ end
+
+ expect(page).to have_selector('.atwho-container')
+
+ wait_for_ajax
+
+ expect(find('#at-view-64')).to have_content(user.name)
+ end
+
+ it 'selects the first item for non-assignee dropdowns if a query is entered' do
+ page.within '.timeline-content-form' do
+ find('#note_note').native.send_keys('')
+ find('#note_note').native.send_keys(':1')
+ end
+
+ expect(page).to have_selector('.atwho-container')
+
+ wait_for_ajax
+
+ expect(find('#at-view-58')).to have_selector('.cur:first-of-type')
+ end
+
context 'if a selected value has special characters' do
it 'wraps the result in double quotes' do
note = find('#note_note')
page.within '.timeline-content-form' do
note.native.send_keys('')
note.native.send_keys("~#{label.title[0]}")
- sleep 1
note.click
end
@@ -48,12 +99,52 @@ feature 'GFM autocomplete', feature: true, js: true do
expect_to_wrap(true, label_item, note, label.title)
end
+ it "shows dropdown after a new line" do
+ note = find('#note_note')
+ page.within '.timeline-content-form' do
+ note.native.send_keys('test')
+ note.native.send_keys(:enter)
+ note.native.send_keys(:enter)
+ note.native.send_keys('@')
+ end
+
+ expect(page).to have_selector('.atwho-container')
+ end
+
+ it "does not show dropdown when preceded with a special character" do
+ note = find('#note_note')
+ page.within '.timeline-content-form' do
+ note.native.send_keys('')
+ note.native.send_keys("@")
+ note.click
+ end
+
+ expect(page).to have_selector('.atwho-container')
+
+ page.within '.timeline-content-form' do
+ note.native.send_keys("@")
+ note.click
+ end
+
+ expect(page).to have_selector('.atwho-container', visible: false)
+ end
+
+ it "does not throw an error if no labels exist" do
+ note = find('#note_note')
+ page.within '.timeline-content-form' do
+ note.native.send_keys('')
+ note.native.send_keys('~')
+ note.click
+ end
+
+ expect(page).to have_selector('.atwho-container', visible: false)
+ end
+
it 'doesn\'t wrap for assignee values' do
note = find('#note_note')
page.within '.timeline-content-form' do
note.native.send_keys('')
note.native.send_keys("@#{user.username[0]}")
- sleep 1
note.click
end
@@ -67,7 +158,6 @@ feature 'GFM autocomplete', feature: true, js: true do
page.within '.timeline-content-form' do
note.native.send_keys('')
note.native.send_keys(":cartwheel")
- sleep 1
note.click
end
@@ -76,6 +166,22 @@ feature 'GFM autocomplete', feature: true, js: true do
expect_to_wrap(false, emoji_item, note, 'cartwheel_tone1')
end
+ it 'doesn\'t open autocomplete after non-word character' do
+ page.within '.timeline-content-form' do
+ find('#note_note').native.send_keys("@#{user.username[0..2]}!")
+ end
+
+ expect(page).not_to have_selector('.atwho-view')
+ end
+
+ it 'doesn\'t open autocomplete if there is no space before' do
+ page.within '.timeline-content-form' do
+ find('#note_note').native.send_keys("hello:#{user.username[0..2]}")
+ end
+
+ expect(page).not_to have_selector('.atwho-view')
+ end
+
def expect_to_wrap(should_wrap, item, note, value)
expect(item).to have_content(value)
expect(item).not_to have_content("\"#{value}\"")
@@ -89,12 +195,4 @@ feature 'GFM autocomplete', feature: true, js: true do
end
end
end
-
- it 'doesnt open autocomplete after non-word character' do
- page.within '.timeline-content-form' do
- find('#note_note').native.send_keys("@#{user.username[0..2]}!")
- end
-
- expect(page).not_to have_selector('.atwho-view')
- end
end
diff --git a/spec/features/issues/group_label_sidebar_spec.rb b/spec/features/issues/group_label_sidebar_spec.rb
new file mode 100644
index 00000000000..fc8515cfe9b
--- /dev/null
+++ b/spec/features/issues/group_label_sidebar_spec.rb
@@ -0,0 +1,21 @@
+require 'rails_helper'
+
+describe 'Group label on issue', :feature do
+ it 'renders link to the project issues page' do
+ group = create(:group)
+ project = create(:empty_project, :public, namespace: group)
+ feature = create(:group_label, group: group, title: 'feature')
+ issue = create(:labeled_issue, project: project, labels: [feature])
+ label_link = namespace_project_issues_path(
+ project.namespace,
+ project,
+ label_name: [feature.name]
+ )
+
+ visit namespace_project_issue_path(project.namespace, project, issue)
+
+ link = find('.issuable-show-labels a')
+
+ expect(link[:href]).to eq(label_link)
+ end
+end
diff --git a/spec/features/issues/issue_sidebar_spec.rb b/spec/features/issues/issue_sidebar_spec.rb
index bc068b5e7e0..1eb981942ea 100644
--- a/spec/features/issues/issue_sidebar_spec.rb
+++ b/spec/features/issues/issue_sidebar_spec.rb
@@ -2,6 +2,7 @@ require 'rails_helper'
feature 'Issue Sidebar', feature: true do
include WaitForAjax
+ include MobileHelpers
let(:project) { create(:project, :public) }
let(:issue) { create(:issue, project: project) }
@@ -59,6 +60,23 @@ feature 'Issue Sidebar', feature: true do
end
end
+ context 'sidebar', js: true do
+ it 'changes size when the screen size is smaller' do
+ sidebar_selector = 'aside.right-sidebar.right-sidebar-collapsed'
+ # Resize the window
+ resize_screen_sm
+ # Make sure the sidebar is collapsed
+ expect(page).to have_css(sidebar_selector)
+ # Once is collapsed let's open the sidebard and reload
+ open_issue_sidebar
+ refresh
+ expect(page).to have_css(sidebar_selector)
+ # Restore the window size as it was including the sidebar
+ restore_window_size
+ open_issue_sidebar
+ end
+ end
+
context 'creating a new label', js: true do
it 'shows option to crate a new label is present' do
page.within('.block.labels') do
@@ -109,4 +127,11 @@ feature 'Issue Sidebar', feature: true do
def visit_issue(project, issue)
visit namespace_project_issue_path(project.namespace, project, issue)
end
+
+ def open_issue_sidebar
+ page.within('aside.right-sidebar.right-sidebar-collapsed') do
+ find('.js-sidebar-toggle').click
+ sleep 1
+ end
+ end
end
diff --git a/spec/features/issues/markdown_toolbar_spec.rb b/spec/features/issues/markdown_toolbar_spec.rb
new file mode 100644
index 00000000000..c8c9c50396b
--- /dev/null
+++ b/spec/features/issues/markdown_toolbar_spec.rb
@@ -0,0 +1,37 @@
+require 'rails_helper'
+
+feature 'Issue markdown toolbar', feature: true, js: true do
+ let(:project) { create(:project, :public) }
+ let(:issue) { create(:issue, project: project) }
+ let(:user) { create(:user) }
+
+ before do
+ login_as(user)
+
+ visit namespace_project_issue_path(project.namespace, project, issue)
+ end
+
+ it "doesn't include first new line when adding bold" do
+ find('#note_note').native.send_keys('test')
+ find('#note_note').native.send_key(:enter)
+ find('#note_note').native.send_keys('bold')
+
+ page.evaluate_script('document.querySelectorAll(".js-main-target-form #note_note")[0].setSelectionRange(4, 9)')
+
+ first('.toolbar-btn').click
+
+ expect(find('#note_note')[:value]).to eq("test\n**bold**\n")
+ end
+
+ it "doesn't include first new line when adding underline" do
+ find('#note_note').native.send_keys('test')
+ find('#note_note').native.send_key(:enter)
+ find('#note_note').native.send_keys('underline')
+
+ page.evaluate_script('document.querySelectorAll(".js-main-target-form #note_note")[0].setSelectionRange(4, 50)')
+
+ find('.toolbar-btn:nth-child(2)').click
+
+ expect(find('#note_note')[:value]).to eq("test\n*underline*\n")
+ end
+end
diff --git a/spec/features/issues/new_branch_button_spec.rb b/spec/features/issues/new_branch_button_spec.rb
index a4d3053d10c..c0ab42c6822 100644
--- a/spec/features/issues/new_branch_button_spec.rb
+++ b/spec/features/issues/new_branch_button_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-feature 'Start new branch from an issue', feature: true do
+feature 'Start new branch from an issue', feature: true, js: true do
let!(:project) { create(:project) }
let!(:issue) { create(:issue, project: project) }
let!(:user) { create(:user)}
@@ -11,7 +11,7 @@ feature 'Start new branch from an issue', feature: true do
login_as(user)
end
- it 'shows the new branch button', js: true do
+ it 'shows the new branch button' do
visit namespace_project_issue_path(project.namespace, project, issue)
expect(page).to have_css('#new-branch .available')
@@ -34,16 +34,26 @@ feature 'Start new branch from an issue', feature: true do
visit namespace_project_issue_path(project.namespace, project, issue)
end
- it "hides the new branch button", js: true do
+ it "hides the new branch button" do
expect(page).to have_css('#new-branch .unavailable')
expect(page).not_to have_css('#new-branch .available')
expect(page).to have_content /1 Related Merge Request/
end
end
+
+ context 'when issue is confidential' do
+ it 'hides the new branch button' do
+ issue = create(:issue, :confidential, project: project)
+
+ visit namespace_project_issue_path(project.namespace, project, issue)
+
+ expect(page).not_to have_css('#new-branch')
+ end
+ end
end
- context "for visiters" do
- it 'shows no buttons', js: true do
+ context 'for visitors' do
+ it 'shows no buttons' do
visit namespace_project_issue_path(project.namespace, project, issue)
expect(page).not_to have_css('#new-branch')
diff --git a/spec/features/issues/reset_filters_spec.rb b/spec/features/issues/reset_filters_spec.rb
deleted file mode 100644
index c9a3ecf16ea..00000000000
--- a/spec/features/issues/reset_filters_spec.rb
+++ /dev/null
@@ -1,89 +0,0 @@
-require 'rails_helper'
-
-feature 'Issues filter reset button', feature: true, js: true do
- include WaitForAjax
- include IssueHelpers
-
- let!(:project) { create(:project, :public) }
- let!(:user) { create(:user)}
- let!(:milestone) { create(:milestone, project: project) }
- let!(:bug) { create(:label, project: project, name: 'bug')}
- let!(:issue1) { create(:issue, project: project, milestone: milestone, author: user, assignee: user, title: 'Feature')}
- let!(:issue2) { create(:labeled_issue, project: project, labels: [bug], title: 'Bugfix1')}
-
- before do
- project.team << [user, :developer]
- end
-
- context 'when a milestone filter has been applied' do
- it 'resets the milestone filter' do
- visit_issues(project, milestone_title: milestone.title)
- expect(page).to have_css('.issue', count: 1)
-
- reset_filters
- expect(page).to have_css('.issue', count: 2)
- end
- end
-
- context 'when a label filter has been applied' do
- it 'resets the label filter' do
- visit_issues(project, label_name: bug.name)
- expect(page).to have_css('.issue', count: 1)
-
- reset_filters
- expect(page).to have_css('.issue', count: 2)
- end
- end
-
- context 'when a text search has been conducted' do
- it 'resets the text search filter' do
- visit_issues(project, search: 'Bug')
- expect(page).to have_css('.issue', count: 1)
-
- reset_filters
- expect(page).to have_css('.issue', count: 2)
- end
- end
-
- context 'when author filter has been applied' do
- it 'resets the author filter' do
- visit_issues(project, author_id: user.id)
- expect(page).to have_css('.issue', count: 1)
-
- reset_filters
- expect(page).to have_css('.issue', count: 2)
- end
- end
-
- context 'when assignee filter has been applied' do
- it 'resets the assignee filter' do
- visit_issues(project, assignee_id: user.id)
- expect(page).to have_css('.issue', count: 1)
-
- reset_filters
- expect(page).to have_css('.issue', count: 2)
- end
- end
-
- context 'when all filters have been applied' do
- it 'resets all filters' do
- visit_issues(project, assignee_id: user.id, author_id: user.id, milestone_title: milestone.title, label_name: bug.name, search: 'Bug')
- expect(page).to have_css('.issue', count: 0)
-
- reset_filters
- expect(page).to have_css('.issue', count: 2)
- end
- end
-
- context 'when no filters have been applied' do
- it 'the reset link should not be visible' do
- visit_issues(project)
- expect(page).to have_css('.issue', count: 2)
- expect(page).not_to have_css '.reset_filters'
- end
- end
-
- def reset_filters
- find('.reset-filters').click
- end
-end
diff --git a/spec/features/issues/spam_issues_spec.rb b/spec/features/issues/spam_issues_spec.rb
new file mode 100644
index 00000000000..4bc9b49f889
--- /dev/null
+++ b/spec/features/issues/spam_issues_spec.rb
@@ -0,0 +1,66 @@
+require 'rails_helper'
+
+describe 'New issue', feature: true do
+ include StubENV
+
+ let(:project) { create(:project, :public) }
+ let(:user) { create(:user)}
+
+ before do
+ stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
+
+ current_application_settings.update!(
+ akismet_enabled: true,
+ akismet_api_key: 'testkey',
+ recaptcha_enabled: true,
+ recaptcha_site_key: 'test site key',
+ recaptcha_private_key: 'test private key'
+ )
+
+ project.team << [user, :master]
+ login_as(user)
+ end
+
+ context 'when identified as a spam' do
+ before do
+ WebMock.stub_request(:any, /.*akismet.com.*/).to_return(body: "true", status: 200)
+
+ visit new_namespace_project_issue_path(project.namespace, project)
+ end
+
+ it 'creates an issue after solving reCaptcha' do
+ fill_in 'issue_title', with: 'issue title'
+ fill_in 'issue_description', with: 'issue description'
+
+ click_button 'Submit issue'
+
+ # it is impossible to test recaptcha automatically and there is no possibility to fill in recaptcha
+ # recaptcha verification is skipped in test environment and it always returns true
+ expect(page).not_to have_content('issue title')
+ expect(page).to have_css('.recaptcha')
+
+ click_button 'Submit issue'
+
+ expect(page.find('.issue-details h2.title')).to have_content('issue title')
+ expect(page.find('.issue-details .description')).to have_content('issue description')
+ end
+ end
+
+ context 'when not identified as a spam' do
+ before do
+ WebMock.stub_request(:any, /.*akismet.com.*/).to_return(body: 'false', status: 200)
+
+ visit new_namespace_project_issue_path(project.namespace, project)
+ end
+
+ it 'creates an issue' do
+ fill_in 'issue_title', with: 'issue title'
+ fill_in 'issue_description', with: 'issue description'
+
+ click_button 'Submit issue'
+
+ expect(page.find('.issue-details h2.title')).to have_content('issue title')
+ expect(page.find('.issue-details .description')).to have_content('issue description')
+ end
+ end
+end
diff --git a/spec/features/issues/user_uses_slash_commands_spec.rb b/spec/features/issues/user_uses_slash_commands_spec.rb
index 3f2da1c380c..0a9cd11ad6e 100644
--- a/spec/features/issues/user_uses_slash_commands_spec.rb
+++ b/spec/features/issues/user_uses_slash_commands_spec.rb
@@ -30,7 +30,7 @@ feature 'Issues > User uses slash commands', feature: true, js: true do
write_note("/due 2016-08-28")
expect(page).not_to have_content '/due 2016-08-28'
- expect(page).to have_content 'Your commands have been executed!'
+ expect(page).to have_content 'Commands applied'
issue.reload
@@ -51,7 +51,7 @@ feature 'Issues > User uses slash commands', feature: true, js: true do
write_note("/due 2016-08-28")
expect(page).to have_content '/due 2016-08-28'
- expect(page).not_to have_content 'Your commands have been executed!'
+ expect(page).not_to have_content 'Commands applied'
issue.reload
@@ -70,7 +70,7 @@ feature 'Issues > User uses slash commands', feature: true, js: true do
write_note("/remove_due_date")
expect(page).not_to have_content '/remove_due_date'
- expect(page).to have_content 'Your commands have been executed!'
+ expect(page).to have_content 'Commands applied'
issue.reload
@@ -91,7 +91,7 @@ feature 'Issues > User uses slash commands', feature: true, js: true do
write_note("/remove_due_date")
expect(page).to have_content '/remove_due_date'
- expect(page).not_to have_content 'Your commands have been executed!'
+ expect(page).not_to have_content 'Commands applied'
issue.reload
@@ -100,6 +100,58 @@ feature 'Issues > User uses slash commands', feature: true, js: true do
end
end
+ describe 'Issuable time tracking' do
+ let(:issue) { create(:issue, project: project) }
+
+ before do
+ project.team << [user, :developer]
+ end
+
+ context 'Issue' do
+ before do
+ visit namespace_project_issue_path(project.namespace, project, issue)
+ end
+
+ it_behaves_like 'issuable time tracker'
+ end
+
+ context 'Merge Request' do
+ let(:merge_request) { create(:merge_request, source_project: project) }
+
+ before do
+ visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+ end
+
+ it_behaves_like 'issuable time tracker'
+ end
+ end
+
+ describe 'Issuable time tracking' do
+ let(:issue) { create(:issue, project: project) }
+
+ before do
+ project.team << [user, :developer]
+ end
+
+ context 'Issue' do
+ before do
+ visit namespace_project_issue_path(project.namespace, project, issue)
+ end
+
+ it_behaves_like 'issuable time tracker'
+ end
+
+ context 'Merge Request' do
+ let(:merge_request) { create(:merge_request, source_project: project) }
+
+ before do
+ visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+ end
+
+ it_behaves_like 'issuable time tracker'
+ end
+ end
+
describe 'toggling the WIP prefix from the title from note' do
let(:issue) { create(:issue, project: project) }
diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb
index 5c958455604..394eb31aff8 100644
--- a/spec/features/issues_spec.rb
+++ b/spec/features/issues_spec.rb
@@ -371,23 +371,25 @@ describe 'Issues', feature: true do
describe 'when I want to reset my incoming email token' do
let(:project1) { create(:project, namespace: @user.namespace) }
- let(:issue) { create(:issue, project: project1) }
+ let!(:issue) { create(:issue, project: project1) }
before do
- allow(Gitlab.config.incoming_email).to receive(:enabled).and_return(true)
+ stub_incoming_email_setting(enabled: true, address: "p+%{key}@gl.ab")
project1.team << [@user, :master]
- project1.issues << issue
visit namespace_project_issues_path(@user.namespace, project1)
end
it 'changes incoming email address token', js: true do
find('.issue-email-modal-btn').click
previous_token = find('input#issue_email').value
-
find('.incoming-email-token-reset').click
- wait_for_ajax
- expect(find('input#issue_email').value).not_to eq(previous_token)
+ expect(page).to have_no_field('issue_email', with: previous_token)
+ new_token = project1.new_issue_address(@user.reload)
+ expect(page).to have_field(
+ 'issue_email',
+ with: new_token
+ )
end
end
@@ -617,14 +619,18 @@ describe 'Issues', feature: true do
end
it 'adds due date to issue' do
+ date = Date.today.at_beginning_of_month + 2.days
+
page.within '.due_date' do
click_link 'Edit'
page.within '.ui-datepicker-calendar' do
- first('.ui-state-default').click
+ click_link date.day
end
- expect(page).to have_no_content 'None'
+ wait_for_ajax
+
+ expect(find('.value').text).to have_content date.strftime('%b %-d, %Y')
end
end
@@ -636,6 +642,8 @@ describe 'Issues', feature: true do
first('.ui-state-default').click
end
+ wait_for_ajax
+
expect(page).to have_no_content 'No due date'
click_link 'remove due date'
diff --git a/spec/features/login_spec.rb b/spec/features/login_spec.rb
index 76bcfbe523a..ab7d89306db 100644
--- a/spec/features/login_spec.rb
+++ b/spec/features/login_spec.rb
@@ -25,6 +25,11 @@ feature 'Login', feature: true do
expect(current_path).to eq root_path
end
+
+ it 'does not show flash messages when login page' do
+ visit root_path
+ expect(page).not_to have_content('You need to sign in or sign up before continuing.')
+ end
end
describe 'with two-factor authentication' do
diff --git a/spec/features/merge_requests/cherry_pick_spec.rb b/spec/features/merge_requests/cherry_pick_spec.rb
index 82bc5226d07..dfe7c910a10 100644
--- a/spec/features/merge_requests/cherry_pick_spec.rb
+++ b/spec/features/merge_requests/cherry_pick_spec.rb
@@ -2,7 +2,8 @@ require 'spec_helper'
describe 'Cherry-pick Merge Requests' do
let(:user) { create(:user) }
- let(:project) { create(:project) }
+ let(:group) { create(:group) }
+ let(:project) { create(:project, namespace: group) }
let(:merge_request) { create(:merge_request_with_diffs, source_project: project, author: user) }
before do
diff --git a/spec/features/merge_requests/closes_issues_spec.rb b/spec/features/merge_requests/closes_issues_spec.rb
index dc32c8f7373..c73065cdce1 100644
--- a/spec/features/merge_requests/closes_issues_spec.rb
+++ b/spec/features/merge_requests/closes_issues_spec.rb
@@ -41,7 +41,7 @@ feature 'Merge Request closing issues message', feature: true do
let(:merge_request_description) { "Description\n\nRefers to #{issue_1.to_reference} and #{issue_2.to_reference}" }
it 'does not display closing issue message' do
- expect(page).to have_content("Issues #{issue_1.to_reference} and #{issue_2.to_reference} are mentioned but will not closed.")
+ expect(page).to have_content("Issues #{issue_1.to_reference} and #{issue_2.to_reference} are mentioned but will not be closed.")
end
end
@@ -49,7 +49,7 @@ feature 'Merge Request closing issues message', feature: true do
let(:merge_request_description) { "Description\n\ncloses #{issue_1.to_reference}\n\n refers to #{issue_2.to_reference}" }
it 'does not display closing issue message' do
- expect(page).to have_content("Accepting this merge request will close issue #{issue_1.to_reference}. Issue #{issue_2.to_reference} is mentioned but will not closed.")
+ expect(page).to have_content("Accepting this merge request will close issue #{issue_1.to_reference}. Issue #{issue_2.to_reference} is mentioned but will not be closed.")
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 f1b68a39343..e853fb7e016 100644
--- a/spec/features/merge_requests/create_new_mr_spec.rb
+++ b/spec/features/merge_requests/create_new_mr_spec.rb
@@ -84,4 +84,24 @@ feature 'Create New Merge Request', feature: true, js: true do
expect(page).not_to have_selector('#error_explanation')
expect(page).not_to have_content('The form contains the following error')
end
+
+ context 'when a new merge request has a pipeline' do
+ let!(:pipeline) do
+ create(:ci_pipeline, sha: project.commit('fix').id,
+ ref: 'fix',
+ project: project)
+ end
+
+ it 'shows pipelines for a new merge request' do
+ visit new_namespace_project_merge_request_path(
+ project.namespace, project,
+ merge_request: { target_branch: 'master', source_branch: 'fix' })
+
+ page.within('.merge-request') do
+ click_link 'Pipelines'
+
+ expect(page).to have_content "##{pipeline.id}"
+ end
+ end
+ end
end
diff --git a/spec/features/merge_requests/created_from_fork_spec.rb b/spec/features/merge_requests/created_from_fork_spec.rb
index 142649297cc..73c5ef31edc 100644
--- a/spec/features/merge_requests/created_from_fork_spec.rb
+++ b/spec/features/merge_requests/created_from_fork_spec.rb
@@ -54,14 +54,14 @@ feature 'Merge request created from fork' do
scenario 'user visits a pipelines page', js: true do
visit_merge_request(merge_request)
- page.within('.merge-request-tabs') { click_link 'Builds' }
+ page.within('.merge-request-tabs') { click_link 'Pipelines' }
page.within('table.ci-table') do
- expect(page).to have_content 'rspec'
- expect(page).to have_content 'spinach'
+ expect(page).to have_content pipeline.status
+ expect(page).to have_content pipeline.id
end
- expect(find_link('Cancel running')[:href])
+ expect(page.find('a.btn-remove')[:href])
.to include fork_project.path_with_namespace
end
end
diff --git a/spec/features/merge_requests/deleted_source_branch_spec.rb b/spec/features/merge_requests/deleted_source_branch_spec.rb
index d5c9ed8a3b7..0952b17b63e 100644
--- a/spec/features/merge_requests/deleted_source_branch_spec.rb
+++ b/spec/features/merge_requests/deleted_source_branch_spec.rb
@@ -4,6 +4,8 @@ require 'spec_helper'
# message to be shown by JavaScript when the source branch was deleted.
# Please do not remove "js: true".
describe 'Deleted source branch', feature: true, js: true do
+ include WaitForAjax
+
let(:user) { create(:user) }
let(:merge_request) { create(:merge_request) }
@@ -13,7 +15,8 @@ describe 'Deleted source branch', feature: true, js: true do
merge_request.update!(source_branch: 'this-branch-does-not-exist')
visit namespace_project_merge_request_path(
merge_request.project.namespace,
- merge_request.project, merge_request
+ merge_request.project,
+ merge_request
)
end
@@ -23,11 +26,17 @@ describe 'Deleted source branch', feature: true, js: true do
)
end
- it 'hides Discussion, Commits and Changes tabs' do
+ it 'still contains Discussion, Commits and Changes tabs' do
within '.merge-request-details' do
- expect(page).to have_no_content('Discussion')
- expect(page).to have_no_content('Commits')
- expect(page).to have_no_content('Changes')
+ expect(page).to have_content('Discussion')
+ expect(page).to have_content('Commits')
+ expect(page).to have_content('Changes')
end
+
+ click_on 'Changes'
+ wait_for_ajax
+
+ expect(page).to have_selector('.diffs.tab-pane .nothing-here-block')
+ expect(page).to have_content('Nothing to merge from this-branch-does-not-exist into feature')
end
end
diff --git a/spec/features/merge_requests/diffs_spec.rb b/spec/features/merge_requests/diffs_spec.rb
index c9a0059645d..4a6c76a5caf 100644
--- a/spec/features/merge_requests/diffs_spec.rb
+++ b/spec/features/merge_requests/diffs_spec.rb
@@ -22,4 +22,18 @@ feature 'Diffs URL', js: true, feature: true do
expect(page).to have_css('.diffs.tab-pane.active')
end
end
+
+ context 'when merge request has overflow' do
+ it 'displays warning' do
+ allow_any_instance_of(MergeRequestDiff).to receive(:overflow?).and_return(true)
+ allow(Commit).to receive(:max_diff_options).and_return(max_files: 20, max_lines: 20)
+
+ visit diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)
+
+ page.within('.alert') do
+ expect(page).to have_text("Too many changes to show. Plain diff Email patch To preserve
+ performance only 3 of 3+ files are displayed.")
+ end
+ end
+ end
end
diff --git a/spec/features/merge_requests/edit_mr_spec.rb b/spec/features/merge_requests/edit_mr_spec.rb
index c46bd8d449f..cb3bc392903 100644
--- a/spec/features/merge_requests/edit_mr_spec.rb
+++ b/spec/features/merge_requests/edit_mr_spec.rb
@@ -40,5 +40,32 @@ feature 'Edit Merge Request', feature: true do
expect(page).to have_content 'Remove source branch'
end
+
+ it 'should preserve description textarea height', js: true do
+ long_description = %q(
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam ac ornare ligula, ut tempus arcu. Etiam ultricies accumsan dolor vitae faucibus. Donec at elit lacus. Mauris orci ante, aliquam quis lorem eget, convallis faucibus arcu. Aenean at pulvinar lacus. Ut viverra quam massa, molestie ornare tortor dignissim a. Suspendisse tristique pellentesque tellus, id lacinia metus elementum id. Nam tristique, arcu rhoncus faucibus viverra, lacus ipsum sagittis ligula, vitae convallis odio lacus a nibh. Ut tincidunt est purus, ac vestibulum augue maximus in. Suspendisse vel erat et mi ultricies semper. Pellentesque volutpat pellentesque consequat.
+
+ Cras congue nec ligula tristique viverra. Curabitur fringilla fringilla fringilla. Donec rhoncus dignissim orci ut accumsan. Ut rutrum urna a rhoncus varius. Maecenas blandit, mauris nec accumsan gravida, augue nibh finibus magna, sed maximus turpis libero nec neque. Suspendisse at semper est. Nunc imperdiet dapibus dui, varius sollicitudin erat luctus non. Sed pellentesque ligula eget posuere facilisis. Donec dictum commodo volutpat. Donec egestas dui ac magna sollicitudin bibendum. Vivamus purus neque, ullamcorper ac feugiat et, tempus sit amet metus. Praesent quis viverra neque. Sed bibendum viverra est, eu aliquam mi ornare vitae. Proin et dapibus ipsum. Nunc tortor diam, malesuada nec interdum vel, placerat quis justo. Ut viverra at erat eu laoreet.
+
+ Pellentesque commodo, diam sit amet dignissim condimentum, tortor justo pretium est, non venenatis metus eros ut nunc. Etiam ut neque eget sem dapibus aliquam. Curabitur vel elit lorem. Nulla nec enim elit. Sed ut ex id justo facilisis convallis at ac augue. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Nullam cursus egestas turpis non tristique. Suspendisse in erat sem. Fusce libero elit, fermentum gravida mauris id, auctor iaculis felis. Nullam vulputate tempor laoreet.
+
+ Nam tempor et magna sed convallis. Fusce sit amet sollicitudin risus, a ullamcorper lacus. Morbi gravida quis sem eget porttitor. Donec eu egestas mauris, in elementum tortor. Sed eget ex mi. Mauris iaculis tortor ut est auctor, nec dignissim quam sagittis. Suspendisse vel metus non quam suscipit tincidunt. Cras molestie lacus non justo finibus sodales quis vitae erat. In a porttitor nisi, id sollicitudin urna. Ut at felis tellus. Suspendisse potenti.
+
+ Maecenas leo ligula, varius at neque vitae, ornare maximus justo. Nullam convallis luctus risus et vulputate. Duis suscipit faucibus iaculis. Etiam quis tortor faucibus, tristique tellus sit amet, sodales neque. Nulla dapibus nisi vel aliquet consequat. Etiam faucibus, metus eget condimentum iaculis, enim urna lobortis sem, id efficitur eros sapien nec nisi. Aenean ut finibus ex.
+ )
+
+ fill_in 'merge_request_description', with: long_description
+
+ height = get_textarea_height
+ find('.js-md-preview-button').click
+ find('.js-md-write-button').click
+ new_height = get_textarea_height
+
+ expect(height).to eq(new_height)
+ end
+
+ def get_textarea_height
+ page.evaluate_script('document.getElementById("merge_request_description").offsetHeight')
+ end
end
end
diff --git a/spec/features/merge_requests/filter_by_labels_spec.rb b/spec/features/merge_requests/filter_by_labels_spec.rb
new file mode 100644
index 00000000000..4c60329865c
--- /dev/null
+++ b/spec/features/merge_requests/filter_by_labels_spec.rb
@@ -0,0 +1,154 @@
+require 'rails_helper'
+
+feature 'Issue filtering by Labels', feature: true, js: true do
+ include WaitForAjax
+
+ let(:project) { create(:project, :public) }
+ let!(:user) { create(:user) }
+ let!(:label) { create(:label, project: project) }
+
+ let!(:bug) { create(:label, project: project, title: 'bug') }
+ let!(:feature) { create(:label, project: project, title: 'feature') }
+ let!(:enhancement) { create(:label, project: project, title: 'enhancement') }
+
+ let!(:mr1) { create(:merge_request, title: "Bugfix1", source_project: project, target_project: project, source_branch: "bugfix1") }
+ let!(:mr2) { create(:merge_request, title: "Bugfix2", source_project: project, target_project: project, source_branch: "bugfix2") }
+ let!(:mr3) { create(:merge_request, title: "Feature1", source_project: project, target_project: project, source_branch: "feature1") }
+
+ before do
+ mr1.labels << bug
+
+ mr2.labels << bug
+ mr2.labels << enhancement
+
+ mr3.title = "Feature1"
+ mr3.labels << feature
+
+ project.team << [user, :master]
+ login_as(user)
+
+ visit namespace_project_merge_requests_path(project.namespace, project)
+ end
+
+ context 'filter by label bug' do
+ before do
+ select_labels('bug')
+ end
+
+ it 'apply the filter' do
+ expect(page).to have_content "Bugfix1"
+ expect(page).to have_content "Bugfix2"
+ expect(page).not_to have_content "Feature1"
+ expect(find('.filtered-labels')).to have_content "bug"
+ expect(find('.filtered-labels')).not_to have_content "feature"
+ expect(find('.filtered-labels')).not_to have_content "enhancement"
+
+ find('.js-label-filter-remove').click
+ wait_for_ajax
+ expect(find('.filtered-labels', visible: false)).to have_no_content "bug"
+ end
+ end
+
+ context 'filter by label feature' do
+ before do
+ select_labels('feature')
+ end
+
+ it 'applies the filter' do
+ expect(page).to have_content "Feature1"
+ expect(page).not_to have_content "Bugfix2"
+ expect(page).not_to have_content "Bugfix1"
+ expect(find('.filtered-labels')).to have_content "feature"
+ expect(find('.filtered-labels')).not_to have_content "bug"
+ expect(find('.filtered-labels')).not_to have_content "enhancement"
+ end
+ end
+
+ context 'filter by label enhancement' do
+ before do
+ select_labels('enhancement')
+ end
+
+ it 'applies the filter' do
+ expect(page).to have_content "Bugfix2"
+ expect(page).not_to have_content "Feature1"
+ expect(page).not_to have_content "Bugfix1"
+ expect(find('.filtered-labels')).to have_content "enhancement"
+ expect(find('.filtered-labels')).not_to have_content "bug"
+ expect(find('.filtered-labels')).not_to have_content "feature"
+ end
+ end
+
+ context 'filter by label enhancement and bug in issues list' do
+ before do
+ select_labels('bug', 'enhancement')
+ end
+
+ it 'applies the filters' do
+ expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
+ expect(page).to have_content "Bugfix2"
+ expect(page).not_to have_content "Feature1"
+ expect(find('.filtered-labels')).to have_content "bug"
+ expect(find('.filtered-labels')).to have_content "enhancement"
+ expect(find('.filtered-labels')).not_to have_content "feature"
+
+ find('.js-label-filter-remove', match: :first).click
+ wait_for_ajax
+
+ expect(page).to have_content "Bugfix2"
+ expect(page).not_to have_content "Feature1"
+ expect(page).not_to have_content "Bugfix1"
+ expect(find('.filtered-labels')).not_to have_content "bug"
+ expect(find('.filtered-labels')).to have_content "enhancement"
+ expect(find('.filtered-labels')).not_to have_content "feature"
+ end
+ end
+
+ context 'remove filtered labels' do
+ before do
+ page.within '.labels-filter' do
+ click_button 'Label'
+ wait_for_ajax
+ click_link 'bug'
+ find('.dropdown-menu-close').click
+ end
+
+ page.within '.filtered-labels' do
+ expect(page).to have_content 'bug'
+ end
+ end
+
+ it 'allows user to remove filtered labels' do
+ first('.js-label-filter-remove').click
+ wait_for_ajax
+
+ expect(find('.filtered-labels', visible: false)).not_to have_content 'bug'
+ expect(find('.labels-filter')).not_to have_content 'bug'
+ end
+ end
+
+ context 'dropdown filtering' do
+ it 'filters by label name' do
+ page.within '.labels-filter' do
+ click_button 'Label'
+ wait_for_ajax
+ find('.dropdown-input input').set 'bug'
+
+ page.within '.dropdown-content' do
+ expect(page).not_to have_content 'enhancement'
+ expect(page).to have_content 'bug'
+ end
+ end
+ end
+ end
+
+ def select_labels(*labels)
+ page.find('.js-label-select').click
+ wait_for_ajax
+ labels.each do |label|
+ execute_script("$('.dropdown-menu-labels li:contains(\"#{label}\") a').click()")
+ end
+ page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click
+ wait_for_ajax
+ end
+end
diff --git a/spec/features/merge_requests/filter_merge_requests_spec.rb b/spec/features/merge_requests/filter_merge_requests_spec.rb
new file mode 100644
index 00000000000..4642b5a530d
--- /dev/null
+++ b/spec/features/merge_requests/filter_merge_requests_spec.rb
@@ -0,0 +1,355 @@
+require 'rails_helper'
+
+describe 'Filter merge requests', feature: true do
+ include WaitForAjax
+
+ let!(:project) { create(:project) }
+ let!(:group) { create(:group) }
+ let!(:user) { create(:user)}
+ let!(:milestone) { create(:milestone, project: project) }
+ let!(:label) { create(:label, project: project) }
+ let!(:wontfix) { create(:label, project: project, title: "Won't fix") }
+
+ before do
+ project.team << [user, :master]
+ group.add_developer(user)
+ login_as(user)
+ create(:merge_request, source_project: project, target_project: project)
+ end
+
+ describe 'for assignee from mr#index' do
+ before do
+ visit namespace_project_merge_requests_path(project.namespace, project)
+
+ find('.js-assignee-search').click
+
+ find('.dropdown-menu-user-link', text: user.username).click
+
+ wait_for_ajax
+ end
+
+ context 'assignee', js: true do
+ it 'updates to current user' do
+ expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name)
+ end
+
+ 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 '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)
+ end
+ end
+ end
+
+ describe 'for milestone from mr#index' do
+ before do
+ visit namespace_project_merge_requests_path(project.namespace, project)
+
+ find('.js-milestone-select').click
+
+ find('.milestone-filter .dropdown-content a', text: milestone.title).click
+
+ wait_for_ajax
+ end
+
+ context 'milestone', js: true do
+ it 'updates to current milestone' do
+ expect(find('.js-milestone-select .dropdown-toggle-text')).to have_content(milestone.title)
+ end
+
+ 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 '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)
+ end
+ end
+ end
+
+ describe 'for label from mr#index', js: true do
+ before do
+ visit namespace_project_merge_requests_path(project.namespace, project)
+ find('.js-label-select').click
+ wait_for_ajax
+ end
+
+ 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
+
+ expect(find('.labels-filter')).to have_content 'Label'
+ end
+
+ 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
+
+ page.within '.labels-filter' do
+ expect(page).to have_content 'Labels'
+ end
+ expect(find('.js-label-select .dropdown-toggle-text')).to have_content('Labels')
+ end
+
+ it 'filters by a label' do
+ find('.dropdown-menu-labels a', text: label.title).click
+ page.within '.labels-filter' do
+ expect(page).to have_content label.title
+ end
+ expect(find('.js-label-select .dropdown-toggle-text')).to have_content(label.title)
+ end
+
+ it "filters by `won't fix` and another label" do
+ page.within '.labels-filter' do
+ click_link wontfix.title
+ expect(page).to have_content wontfix.title
+ click_link label.title
+ end
+
+ expect(find('.js-label-select .dropdown-toggle-text')).to have_content("#{wontfix.title} +1 more")
+ end
+
+ it "filters by `won't fix` label followed by another label after page load" do
+ page.within '.labels-filter' do
+ click_link wontfix.title
+ expect(page).to have_content wontfix.title
+ end
+
+ find('body').click
+
+ expect(find('.filtered-labels')).to have_content(wontfix.title)
+
+ find('.js-label-select').click
+ wait_for_ajax
+ find('.dropdown-menu-labels a', text: label.title).click
+
+ find('body').click
+
+ expect(find('.filtered-labels')).to have_content(wontfix.title)
+ expect(find('.filtered-labels')).to have_content(label.title)
+
+ find('.js-label-select').click
+ wait_for_ajax
+
+ expect(find('.dropdown-menu-labels li', text: wontfix.title)).to have_css('.is-active')
+ expect(find('.dropdown-menu-labels li', text: label.title)).to have_css('.is-active')
+ end
+
+ it "selects and unselects `won't fix`" do
+ find('.dropdown-menu-labels a', text: wontfix.title).click
+ find('.dropdown-menu-labels a', text: wontfix.title).click
+ # Close label dropdown to load
+ find('body').click
+ expect(page).not_to have_css('.filtered-labels')
+ end
+ end
+
+ describe 'for assignee and label from issues#index' do
+ before do
+ visit namespace_project_merge_requests_path(project.namespace, project)
+
+ find('.js-assignee-search').click
+
+ find('.dropdown-menu-user-link', text: user.username).click
+
+ expect(page).not_to have_selector('.mr-list .merge-request')
+
+ find('.js-label-select').click
+
+ find('.dropdown-menu-labels .dropdown-content a', text: label.title).click
+ page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click
+
+ wait_for_ajax
+ end
+
+ context 'assignee and label', js: true 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 '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 '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)
+ expect(find('.js-label-select .dropdown-toggle-text')).to have_content(label.title)
+ end
+ end
+ end
+
+ describe 'filter merge requests by text' do
+ before do
+ create(:merge_request, title: "Bug", source_project: project, target_project: project, source_branch: "bug")
+
+ bug_label = create(:label, project: project, title: 'bug')
+ milestone = create(:milestone, title: "8", project: project)
+
+ mr = create(:merge_request,
+ title: "Bug 2",
+ source_project: project,
+ target_project: project,
+ source_branch: "bug2",
+ milestone: milestone,
+ author: user,
+ assignee: user)
+ mr.labels << bug_label
+
+ visit namespace_project_merge_requests_path(project.namespace, project)
+ end
+
+ context 'only text', js: true do
+ it 'filters merge requests by searched text' do
+ fill_in 'issuable_search', with: 'Bug'
+
+ page.within '.mr-list' do
+ expect(page).to have_selector('.merge-request', count: 2)
+ end
+ end
+
+ it 'does not show any merge requests' do
+ fill_in 'issuable_search', with: 'testing'
+
+ page.within '.mr-list' do
+ expect(page).not_to have_selector('.merge-request')
+ end
+ end
+ end
+
+ context 'text and dropdown options', js: true do
+ it 'filters by text and label' do
+ fill_in 'issuable_search', with: 'Bug'
+
+ expect(page).to have_issuable_counts(open: 2, closed: 0, all: 2)
+ page.within '.mr-list' do
+ expect(page).to have_selector('.merge-request', count: 2)
+ end
+
+ click_button 'Label'
+ page.within '.labels-filter' do
+ click_link 'bug'
+ end
+ find('.dropdown-menu-close-icon').click
+
+ expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
+ page.within '.mr-list' do
+ expect(page).to have_selector('.merge-request', count: 1)
+ end
+ end
+
+ it 'filters by text and milestone' do
+ fill_in 'issuable_search', with: 'Bug'
+
+ expect(page).to have_issuable_counts(open: 2, closed: 0, all: 2)
+ page.within '.mr-list' do
+ expect(page).to have_selector('.merge-request', count: 2)
+ end
+
+ click_button 'Milestone'
+ page.within '.milestone-filter' do
+ click_link '8'
+ end
+
+ expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
+ page.within '.mr-list' do
+ expect(page).to have_selector('.merge-request', count: 1)
+ end
+ end
+
+ it 'filters by text and assignee' do
+ fill_in 'issuable_search', with: 'Bug'
+
+ expect(page).to have_issuable_counts(open: 2, closed: 0, all: 2)
+ page.within '.mr-list' do
+ expect(page).to have_selector('.merge-request', count: 2)
+ end
+
+ click_button 'Assignee'
+ page.within '.dropdown-menu-assignee' do
+ click_link user.name
+ end
+
+ expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
+ page.within '.mr-list' do
+ expect(page).to have_selector('.merge-request', count: 1)
+ end
+ end
+
+ it 'filters by text and author' do
+ fill_in 'issuable_search', with: 'Bug'
+
+ expect(page).to have_issuable_counts(open: 2, closed: 0, all: 2)
+ page.within '.mr-list' do
+ expect(page).to have_selector('.merge-request', count: 2)
+ end
+
+ click_button 'Author'
+ page.within '.dropdown-menu-author' do
+ click_link user.name
+ end
+
+ expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
+ page.within '.mr-list' do
+ expect(page).to have_selector('.merge-request', count: 1)
+ end
+ end
+ end
+ end
+
+ describe 'filter merge requests and sort', js: true do
+ before do
+ bug_label = create(:label, project: project, title: 'bug')
+
+ mr1 = create(:merge_request, title: "Frontend", source_project: project, target_project: project, source_branch: "Frontend")
+ mr2 = create(:merge_request, title: "Bug 2", source_project: project, target_project: project, source_branch: "bug2")
+
+ mr1.labels << bug_label
+ mr2.labels << bug_label
+
+ visit namespace_project_merge_requests_path(project.namespace, project)
+ end
+
+ it 'is able to filter and sort merge requests' do
+ click_button 'Label'
+ wait_for_ajax
+ page.within '.labels-filter' do
+ click_link 'bug'
+ end
+ find('.dropdown-menu-close-icon').click
+ wait_for_ajax
+
+ expect(page).to have_issuable_counts(open: 2, closed: 0, all: 2)
+ page.within '.mr-list' do
+ expect(page).to have_selector('.merge-request', count: 2)
+ end
+
+ click_button 'Last created'
+ page.within '.dropdown-menu-sort' do
+ click_link 'Oldest created'
+ end
+ wait_for_ajax
+
+ page.within '.mr-list' do
+ expect(page).to have_content('Frontend')
+ end
+ end
+ end
+end
diff --git a/spec/features/merge_requests/merge_immediately_with_pipeline_spec.rb b/spec/features/merge_requests/merge_immediately_with_pipeline_spec.rb
new file mode 100644
index 00000000000..f2f8f11ab28
--- /dev/null
+++ b/spec/features/merge_requests/merge_immediately_with_pipeline_spec.rb
@@ -0,0 +1,41 @@
+require 'spec_helper'
+
+feature 'Merge immediately', :feature, :js do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :public) }
+
+ let(:merge_request) do
+ create(:merge_request_with_diffs, source_project: project,
+ author: user,
+ title: 'Bug NS-04')
+ end
+
+ let(:pipeline) do
+ create(:ci_pipeline, project: project,
+ sha: merge_request.diff_head_sha,
+ ref: merge_request.source_branch)
+ end
+
+ before { project.team << [user, :master] }
+
+ context 'when there is active pipeline for merge request' do
+ background do
+ create(:ci_build, pipeline: pipeline)
+ end
+
+ before do
+ login_as user
+ visit namespace_project_merge_request_path(merge_request.project.namespace, merge_request.project, merge_request)
+ end
+
+ it 'enables merge immediately' do
+ page.within '.mr-widget-body' do
+ find('.dropdown-toggle').click
+
+ click_link 'Merge Immediately'
+
+ expect(find('.js-merge-button')).to have_content('Merge in progress')
+ end
+ end
+ end
+end
diff --git a/spec/features/merge_requests/merge_request_versions_spec.rb b/spec/features/merge_requests/merge_request_versions_spec.rb
index 09451f41de4..04e85ed3f73 100644
--- a/spec/features/merge_requests/merge_request_versions_spec.rb
+++ b/spec/features/merge_requests/merge_request_versions_spec.rb
@@ -24,7 +24,7 @@ feature 'Merge Request versions', js: true, feature: true do
before do
page.within '.mr-version-dropdown' do
find('.btn-default').click
- click_link 'version 1'
+ find(:link, 'version 1').trigger('click')
end
end
@@ -45,7 +45,7 @@ feature 'Merge Request versions', js: true, feature: true do
before do
page.within '.mr-version-compare-dropdown' do
find('.btn-default').click
- click_link 'version 1'
+ find(:link, 'version 1').trigger('click')
end
end
@@ -81,4 +81,52 @@ feature 'Merge Request versions', js: true, feature: true do
expect(page).to have_content '8 changed files'
end
end
+
+ describe 'compare with same version' do
+ before do
+ page.within '.mr-version-compare-dropdown' do
+ find('.btn-default').click
+ click_link 'version 1'
+ end
+ end
+
+ it 'should have 0 chages between versions' do
+ page.within '.mr-version-compare-dropdown' do
+ expect(page).to have_content 'version 1'
+ end
+
+ page.within '.mr-version-dropdown' do
+ find('.btn-default').click
+ find(:link, 'version 1').trigger('click')
+ end
+
+ expect(page).to have_content '0 changed files'
+ end
+ end
+
+ describe 'compare with newer version' do
+ before do
+ page.within '.mr-version-compare-dropdown' do
+ find('.btn-default').click
+ click_link 'version 2'
+ end
+ end
+
+ it 'should set the compared versions to be the same' do
+ page.within '.mr-version-compare-dropdown' do
+ expect(page).to have_content 'version 2'
+ end
+
+ page.within '.mr-version-dropdown' do
+ find('.btn-default').click
+ find(:link, 'version 1').trigger('click')
+ end
+
+ page.within '.mr-version-compare-dropdown' do
+ expect(page).to have_content 'version 1'
+ end
+
+ expect(page).to have_content '0 changed files'
+ end
+ end
end
diff --git a/spec/features/merge_requests/merge_when_pipeline_succeeds_spec.rb b/spec/features/merge_requests/merge_when_pipeline_succeeds_spec.rb
index aa24a905001..2ea9c317bd1 100644
--- a/spec/features/merge_requests/merge_when_pipeline_succeeds_spec.rb
+++ b/spec/features/merge_requests/merge_when_pipeline_succeeds_spec.rb
@@ -32,19 +32,61 @@ feature 'Merge When Pipeline Succeeds', :feature, :js do
expect(page).to have_button "Merge When Pipeline Succeeds"
end
- context "Merge When Pipeline Succeeds enabled" do
- before do
- click_button "Merge When Pipeline Succeeds"
+ describe 'enabling Merge When Pipeline Succeeds' do
+ shared_examples 'Merge When Pipeline Succeeds activator' do
+ it 'activates the Merge When Pipeline Succeeds feature' do
+ click_button "Merge When Pipeline Succeeds"
+
+ expect(page).to have_content "Set by #{user.name} to be merged automatically when the pipeline succeeds."
+ expect(page).to have_content "The source branch will not be removed."
+ expect(page).to have_link "Cancel Automatic Merge"
+ visit_merge_request(merge_request) # Needed to refresh the page
+ expect(page).to have_content /enabled an automatic merge when the pipeline for \h{8} succeeds/i
+ end
end
- it 'activates Merge When Pipeline Succeeds feature' do
- expect(page).to have_link "Cancel Automatic Merge"
+ context "when enabled immediately" do
+ it_behaves_like 'Merge When Pipeline Succeeds activator'
+ end
+
+ context 'when enabled after pipeline status changed' do
+ before do
+ pipeline.run!
- expect(page).to have_content "Set by #{user.name} to be merged automatically when the pipeline succeeds."
- expect(page).to have_content "The source branch will not be removed."
+ # We depend on merge request widget being reloaded
+ # so we have to wait for asynchronous call to reload it
+ # and have_content expectation handles that.
+ #
+ expect(page).to have_content "Pipeline ##{pipeline.id} running"
+ end
+
+ it_behaves_like 'Merge When Pipeline Succeeds activator'
+ end
+
+ context 'when enabled after it was previously canceled' do
+ before do
+ click_button "Merge When Pipeline Succeeds"
+ click_link "Cancel Automatic Merge"
+ end
+
+ it_behaves_like 'Merge When Pipeline Succeeds activator'
+ end
- visit_merge_request(merge_request) # Needed to refresh the page
- expect(page).to have_content /enabled an automatic merge when the pipeline for \h{8} succeeds/i
+ context 'when it was enabled and then canceled' do
+ let(:merge_request) do
+ create(:merge_request_with_diffs,
+ :merge_when_build_succeeds,
+ source_project: project,
+ title: 'Bug NS-04',
+ author: user,
+ merge_user: user)
+ end
+
+ before do
+ click_link "Cancel Automatic Merge"
+ end
+
+ it_behaves_like 'Merge When Pipeline Succeeds activator'
end
end
end
diff --git a/spec/features/merge_requests/mini_pipeline_graph_spec.rb b/spec/features/merge_requests/mini_pipeline_graph_spec.rb
new file mode 100644
index 00000000000..b08bd36bde9
--- /dev/null
+++ b/spec/features/merge_requests/mini_pipeline_graph_spec.rb
@@ -0,0 +1,100 @@
+require 'rails_helper'
+
+feature 'Mini Pipeline Graph', :js, :feature do
+ include WaitForAjax
+
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :public) }
+ let(:merge_request) { create(:merge_request, source_project: project) }
+
+ let(:pipeline) { create(:ci_empty_pipeline, project: project, ref: 'master', status: 'running', sha: project.commit.id) }
+ let(:build) { create(:ci_build, pipeline: pipeline, stage: 'test', commands: 'test') }
+
+ before do
+ build.run
+
+ login_as(user)
+ visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+ end
+
+ it 'should display a mini pipeline graph' do
+ expect(page).to have_selector('.mr-widget-pipeline-graph')
+ end
+
+ describe 'build list toggle' do
+ let(:toggle) do
+ find('.mini-pipeline-graph-dropdown-toggle')
+ first('.mini-pipeline-graph-dropdown-toggle')
+ end
+
+ it 'should expand when hovered' do
+ before_width = evaluate_script("$('.mini-pipeline-graph-dropdown-toggle:visible').outerWidth();")
+
+ toggle.hover
+
+ after_width = evaluate_script("$('.mini-pipeline-graph-dropdown-toggle:visible').outerWidth();")
+
+ expect(before_width).to be < after_width
+ end
+
+ it 'should show dropdown caret when hovered' do
+ toggle.hover
+
+ expect(toggle).to have_selector('.fa-caret-down')
+ end
+
+ it 'should show tooltip when hovered' do
+ toggle.hover
+
+ expect(toggle.find(:xpath, '..')).to have_selector('.tooltip')
+ end
+ end
+
+ describe 'builds list menu' do
+ let(:toggle) do
+ find('.mini-pipeline-graph-dropdown-toggle')
+ first('.mini-pipeline-graph-dropdown-toggle')
+ end
+
+ before do
+ toggle.click
+ wait_for_ajax
+ end
+
+ it 'should open when toggle is clicked' do
+ expect(toggle.find(:xpath, '..')).to have_selector('.mini-pipeline-graph-dropdown-menu')
+ end
+
+ it 'should close when toggle is clicked again' do
+ toggle.click
+
+ expect(toggle.find(:xpath, '..')).not_to have_selector('.mini-pipeline-graph-dropdown-menu')
+ end
+
+ it 'should close when clicking somewhere else' do
+ find('body').click
+
+ expect(toggle.find(:xpath, '..')).not_to have_selector('.mini-pipeline-graph-dropdown-menu')
+ end
+
+ describe 'build list build item' do
+ let(:build_item) do
+ find('.mini-pipeline-graph-dropdown-item')
+ first('.mini-pipeline-graph-dropdown-item')
+ end
+
+ it 'should visit the build page when clicked' do
+ build_item.click
+ find('.build-page')
+
+ expect(current_path).to eql(namespace_project_build_path(project.namespace, project, build))
+ end
+
+ it 'should show tooltip when hovered' do
+ build_item.hover
+
+ expect(build_item.find(:xpath, '..')).to have_selector('.tooltip')
+ end
+ end
+ end
+end
diff --git a/spec/features/merge_requests/only_allow_merge_if_build_succeeds_spec.rb b/spec/features/merge_requests/only_allow_merge_if_build_succeeds_spec.rb
index 7e2907cd26f..d2f5c4afc93 100644
--- a/spec/features/merge_requests/only_allow_merge_if_build_succeeds_spec.rb
+++ b/spec/features/merge_requests/only_allow_merge_if_build_succeeds_spec.rb
@@ -50,7 +50,7 @@ feature 'Only allow merge requests to be merged if the build succeeds', feature:
visit_merge_request(merge_request)
expect(page).not_to have_button 'Accept Merge Request'
- expect(page).to have_content('Please retry the build or push a new commit to fix the failure.')
+ expect(page).to have_content('Please retry the job or push a new commit to fix the failure.')
end
end
@@ -61,7 +61,7 @@ feature 'Only allow merge requests to be merged if the build succeeds', feature:
visit_merge_request(merge_request)
expect(page).not_to have_button 'Accept Merge Request'
- expect(page).to have_content('Please retry the build or push a new commit to fix the failure.')
+ expect(page).to have_content('Please retry the job or push a new commit to fix the failure.')
end
end
diff --git a/spec/features/merge_requests/reset_filters_spec.rb b/spec/features/merge_requests/reset_filters_spec.rb
new file mode 100644
index 00000000000..3a7ece7e1d6
--- /dev/null
+++ b/spec/features/merge_requests/reset_filters_spec.rb
@@ -0,0 +1,96 @@
+require 'rails_helper'
+
+feature 'Issues filter reset button', feature: true, js: true do
+ include WaitForAjax
+ include IssueHelpers
+
+ let!(:project) { create(:project, :public) }
+ let!(:user) { create(:user)}
+ let!(:milestone) { create(:milestone, project: project) }
+ let!(:bug) { create(:label, project: project, name: 'bug')}
+ let!(:mr1) { create(:merge_request, title: "Feature", source_project: project, target_project: project, source_branch: "Feature", milestone: milestone, author: user, assignee: user) }
+ let!(:mr2) { create(:merge_request, title: "Bugfix1", source_project: project, target_project: project, source_branch: "Bugfix1") }
+
+ let(:merge_request_css) { '.merge-request' }
+
+ before do
+ mr2.labels << bug
+ project.team << [user, :developer]
+ end
+
+ context 'when a milestone filter has been applied' do
+ it 'resets the milestone filter' do
+ visit_merge_requests(project, milestone_title: milestone.title)
+ expect(page).to have_css(merge_request_css, count: 1)
+
+ reset_filters
+ expect(page).to have_css(merge_request_css, count: 2)
+ end
+ end
+
+ context 'when a label filter has been applied' do
+ it 'resets the label filter' do
+ visit_merge_requests(project, label_name: bug.name)
+ expect(page).to have_css(merge_request_css, count: 1)
+
+ reset_filters
+ expect(page).to have_css(merge_request_css, count: 2)
+ end
+ end
+
+ context 'when a text search has been conducted' do
+ it 'resets the text search filter' do
+ visit_merge_requests(project, search: 'Bug')
+ expect(page).to have_css(merge_request_css, count: 1)
+
+ reset_filters
+ expect(page).to have_css(merge_request_css, count: 2)
+ end
+ end
+
+ context 'when author filter has been applied' do
+ it 'resets the author filter' do
+ visit_merge_requests(project, author_id: user.id)
+ expect(page).to have_css(merge_request_css, count: 1)
+
+ reset_filters
+ expect(page).to have_css(merge_request_css, count: 2)
+ end
+ end
+
+ context 'when assignee filter has been applied' do
+ it 'resets the assignee filter' do
+ visit_merge_requests(project, assignee_id: user.id)
+ expect(page).to have_css(merge_request_css, count: 1)
+
+ reset_filters
+ expect(page).to have_css(merge_request_css, count: 2)
+ end
+ end
+
+ context 'when all filters have been applied' do
+ it 'resets all filters' do
+ visit_merge_requests(project, assignee_id: user.id, author_id: user.id, milestone_title: milestone.title, label_name: bug.name, search: 'Bug')
+ expect(page).to have_css(merge_request_css, count: 0)
+
+ reset_filters
+ expect(page).to have_css(merge_request_css, count: 2)
+ end
+ end
+
+ context 'when no filters have been applied' do
+ it 'the reset link should not be visible' do
+ visit_merge_requests(project)
+ expect(page).to have_css(merge_request_css, count: 2)
+ expect(page).not_to have_css '.reset_filters'
+ end
+ end
+
+ def visit_merge_requests(project, opts = {})
+ visit namespace_project_merge_requests_path project.namespace, project, opts
+ end
+
+ def reset_filters
+ find('.reset-filters').click
+ end
+end
diff --git a/spec/features/merge_requests/toggler_behavior_spec.rb b/spec/features/merge_requests/toggler_behavior_spec.rb
new file mode 100644
index 00000000000..a2cf9b18bf2
--- /dev/null
+++ b/spec/features/merge_requests/toggler_behavior_spec.rb
@@ -0,0 +1,28 @@
+require 'spec_helper'
+
+feature 'toggler_behavior', js: true, feature: true do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+ let(:merge_request) { create(:merge_request, source_project: project, author: user) }
+ let(:note) { create(:diff_note_on_merge_request, noteable: merge_request, project: project) }
+ let(:fragment_id) { "#note_#{note.id}" }
+
+ before do
+ login_as :admin
+ project = merge_request.source_project
+ page.current_window.resize_to(1000, 300)
+ visit "#{namespace_project_merge_request_path(project.namespace, project, merge_request)}#{fragment_id}"
+ end
+
+ describe 'scroll position' do
+ it 'should be scrolled down to fragment' do
+ page_height = page.current_window.size[1]
+ page_scroll_y = page.evaluate_script("window.scrollY")
+ fragment_position_top = page.evaluate_script("$('#{fragment_id}').offset().top")
+ expect(find('.js-toggle-content').visible?).to eq true
+ expect(find(fragment_id).visible?).to eq true
+ expect(fragment_position_top).to be >= page_scroll_y
+ expect(fragment_position_top).to be < (page_scroll_y + page_height)
+ end
+ end
+end
diff --git a/spec/features/merge_requests/user_uses_slash_commands_spec.rb b/spec/features/merge_requests/user_uses_slash_commands_spec.rb
index 7b8af555f0e..2f3c3e45ae6 100644
--- a/spec/features/merge_requests/user_uses_slash_commands_spec.rb
+++ b/spec/features/merge_requests/user_uses_slash_commands_spec.rb
@@ -11,7 +11,7 @@ feature 'Merge Requests > User uses slash commands', feature: true, js: true do
it_behaves_like 'issuable record that supports slash commands in its description and notes', :merge_request do
let(:issuable) { create(:merge_request, source_project: project) }
- let(:new_url_opts) { { merge_request: { source_branch: 'feature' } } }
+ let(:new_url_opts) { { merge_request: { source_branch: 'feature', target_branch: 'master' } } }
end
describe 'merge-request-only commands' do
@@ -31,7 +31,7 @@ feature 'Merge Requests > User uses slash commands', feature: true, js: true do
write_note("/wip")
expect(page).not_to have_content '/wip'
- expect(page).to have_content 'Your commands have been executed!'
+ expect(page).to have_content 'Commands applied'
expect(merge_request.reload.work_in_progress?).to eq true
end
@@ -42,7 +42,7 @@ feature 'Merge Requests > User uses slash commands', feature: true, js: true do
write_note("/wip")
expect(page).not_to have_content '/wip'
- expect(page).to have_content 'Your commands have been executed!'
+ expect(page).to have_content 'Commands applied'
expect(merge_request.reload.work_in_progress?).to eq false
end
@@ -61,13 +61,58 @@ feature 'Merge Requests > User uses slash commands', feature: true, js: true do
write_note("/wip")
expect(page).not_to have_content '/wip'
- expect(page).not_to have_content 'Your commands have been executed!'
+ expect(page).not_to have_content 'Commands applied'
expect(merge_request.reload.work_in_progress?).to eq false
end
end
end
+ describe 'merging the MR from the note' do
+ context 'when the current user can merge the MR' do
+ it 'merges the MR' do
+ write_note("/merge")
+
+ expect(page).to have_content 'Commands applied'
+
+ expect(merge_request.reload).to be_merged
+ end
+ end
+
+ context 'when the head diff changes in the meanwhile' do
+ before do
+ merge_request.source_branch = 'another_branch'
+ merge_request.save
+ end
+
+ it 'does not merge the MR' do
+ write_note("/merge")
+
+ expect(page).not_to have_content 'Your commands have been executed!'
+
+ expect(merge_request.reload).not_to be_merged
+ end
+ end
+
+ context 'when the current user cannot merge the MR' do
+ let(:guest) { create(:user) }
+ before do
+ project.team << [guest, :guest]
+ logout
+ login_with(guest)
+ visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+ end
+
+ it 'does not merge the MR' do
+ write_note("/merge")
+
+ expect(page).not_to have_content 'Your commands have been executed!'
+
+ expect(merge_request.reload).not_to be_merged
+ end
+ end
+ end
+
describe 'adding a due date from note' do
it 'does not recognize the command nor create a note' do
write_note('/due 2016-08-28')
@@ -75,5 +120,81 @@ feature 'Merge Requests > User uses slash commands', feature: true, js: true do
expect(page).not_to have_content '/due 2016-08-28'
end
end
+
+ describe '/target_branch command in merge request' do
+ let(:another_project) { create(:project, :public) }
+ let(:new_url_opts) { { merge_request: { source_branch: 'feature' } } }
+
+ before do
+ logout
+ another_project.team << [user, :master]
+ login_with(user)
+ end
+
+ it 'changes target_branch in new merge_request' do
+ visit new_namespace_project_merge_request_path(another_project.namespace, another_project, new_url_opts)
+ click_button "Compare branches and continue"
+
+ fill_in "merge_request_title", with: 'My brand new feature'
+ fill_in "merge_request_description", with: "le feature \n/target_branch fix\nFeature description:"
+ click_button "Submit merge request"
+
+ merge_request = another_project.merge_requests.first
+ expect(merge_request.description).to eq "le feature \nFeature description:"
+ expect(merge_request.target_branch).to eq 'fix'
+ end
+
+ it 'does not change target branch when merge request is edited' do
+ new_merge_request = create(:merge_request, source_project: another_project)
+
+ visit edit_namespace_project_merge_request_path(another_project.namespace, another_project, new_merge_request)
+ fill_in "merge_request_description", with: "Want to update target branch\n/target_branch fix\n"
+ click_button "Save changes"
+
+ new_merge_request = another_project.merge_requests.first
+ expect(new_merge_request.description).to include('/target_branch')
+ expect(new_merge_request.target_branch).not_to eq('fix')
+ end
+ end
+
+ describe '/target_branch command from note' do
+ context 'when the current user can change target branch' do
+ it 'changes target branch from a note' do
+ write_note("message start \n/target_branch merge-test\n message end.")
+
+ expect(page).not_to have_content('/target_branch')
+ expect(page).to have_content('message start')
+ expect(page).to have_content('message end.')
+
+ expect(merge_request.reload.target_branch).to eq 'merge-test'
+ end
+
+ it 'does not fail when target branch does not exists' do
+ write_note('/target_branch totally_not_existing_branch')
+
+ expect(page).not_to have_content('/target_branch')
+
+ expect(merge_request.target_branch).to eq 'feature'
+ end
+ end
+
+ context 'when current user can not change target branch' do
+ let(:guest) { create(:user) }
+ before do
+ project.team << [guest, :guest]
+ logout
+ login_with(guest)
+ visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+ end
+
+ it 'does not change target branch' do
+ write_note('/target_branch merge-test')
+
+ expect(page).not_to have_content '/target_branch merge-test'
+
+ expect(merge_request.target_branch).to eq 'feature'
+ end
+ end
+ end
end
end
diff --git a/spec/features/merge_requests/widget_spec.rb b/spec/features/merge_requests/widget_spec.rb
new file mode 100644
index 00000000000..fb3a1ae4bd0
--- /dev/null
+++ b/spec/features/merge_requests/widget_spec.rb
@@ -0,0 +1,57 @@
+require 'rails_helper'
+
+describe 'Merge request', :feature, :js do
+ include WaitForAjax
+
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+ let(:merge_request) { create(:merge_request, source_project: project) }
+
+ before do
+ project.team << [user, :master]
+ login_as(user)
+ end
+
+ context 'new merge request' do
+ before do
+ visit new_namespace_project_merge_request_path(
+ project.namespace,
+ project,
+ merge_request: {
+ source_project_id: project.id,
+ target_project_id: project.id,
+ source_branch: 'feature',
+ target_branch: 'master'
+ }
+ )
+ end
+
+ it 'shows widget status after creating new merge request' do
+ click_button 'Submit merge request'
+
+ expect(find('.mr-state-widget')).to have_content('Checking ability to merge automatically')
+
+ wait_for_ajax
+
+ expect(page).to have_selector('.accept_merge_request')
+ end
+ end
+
+ context 'view merge request' do
+ let!(:environment) { create(:environment, project: project) }
+ let!(:deployment) { create(:deployment, environment: environment, ref: 'feature', sha: merge_request.diff_head_sha) }
+
+ before do
+ visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+ end
+
+ it 'shows environments link' do
+ wait_for_ajax
+
+ page.within('.mr-widget-heading') do
+ expect(page).to have_content("Deployed to #{environment.name}")
+ expect(find('.js-environment-link')[:href]).to include(environment.formatted_external_url)
+ end
+ end
+ end
+end
diff --git a/spec/features/merge_requests/wip_message_spec.rb b/spec/features/merge_requests/wip_message_spec.rb
new file mode 100644
index 00000000000..3311731b33b
--- /dev/null
+++ b/spec/features/merge_requests/wip_message_spec.rb
@@ -0,0 +1,63 @@
+require 'spec_helper'
+
+feature 'Work In Progress help message', feature: true do
+ let!(:project) { create(:project, visibility_level: Gitlab::VisibilityLevel::PUBLIC) }
+ let!(:user) { create(:user) }
+
+ before do
+ project.team << [user, :master]
+ login_as(user)
+ end
+
+ context 'with WIP commits' do
+ it 'shows a specific WIP hint' do
+ visit new_namespace_project_merge_request_path(
+ project.namespace,
+ project,
+ merge_request: {
+ source_project_id: project.id,
+ target_project_id: project.id,
+ source_branch: 'wip',
+ target_branch: 'master'
+ }
+ )
+
+ within_wip_explanation do
+ expect(page).to have_text(
+ 'It looks like you have some WIP commits in this branch'
+ )
+ end
+ end
+ end
+
+ context 'without WIP commits' do
+ it 'shows the regular WIP message' do
+ visit new_namespace_project_merge_request_path(
+ project.namespace,
+ project,
+ merge_request: {
+ source_project_id: project.id,
+ target_project_id: project.id,
+ source_branch: 'fix',
+ target_branch: 'master'
+ }
+ )
+
+ within_wip_explanation do
+ expect(page).not_to have_text(
+ 'It looks like you have some WIP commits in this branch'
+ )
+ expect(page).to have_text(
+ "Start the title with WIP: to prevent a Work In Progress merge \
+request from being merged before it's ready"
+ )
+ end
+ end
+ end
+
+ def within_wip_explanation(&block)
+ page.within '.js-no-wip-explanation' do
+ yield
+ end
+ end
+end
diff --git a/spec/features/milestones/milestones_spec.rb b/spec/features/milestones/milestones_spec.rb
index 8b603f51545..aadd72a9f8e 100644
--- a/spec/features/milestones/milestones_spec.rb
+++ b/spec/features/milestones/milestones_spec.rb
@@ -1,6 +1,8 @@
require 'rails_helper'
describe 'Milestone draggable', feature: true, js: true do
+ include WaitForAjax
+
let(:milestone) { create(:milestone, project: project, title: 8.14) }
let(:project) { create(:empty_project, :public) }
let(:user) { create(:user) }
@@ -74,6 +76,8 @@ describe 'Milestone draggable', feature: true, js: true do
visit namespace_project_milestone_path(project.namespace, project, milestone)
issue.drag_to(issue_target)
+
+ wait_for_ajax
end
def create_and_drag_merge_request(params = {})
@@ -82,5 +86,7 @@ describe 'Milestone draggable', feature: true, js: true do
visit namespace_project_milestone_path(project.namespace, project, milestone)
page.find("a[href='#tab-merge-requests']").click
merge_request.drag_to(merge_request_target)
+
+ wait_for_ajax
end
end
diff --git a/spec/features/notes_on_merge_requests_spec.rb b/spec/features/notes_on_merge_requests_spec.rb
index 9fffbb43e87..fab2d532e06 100644
--- a/spec/features/notes_on_merge_requests_spec.rb
+++ b/spec/features/notes_on_merge_requests_spec.rb
@@ -70,16 +70,15 @@ describe 'Comments', feature: true do
end
describe 'when editing a note', js: true 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
+ it 'there should be a hidden edit form' do
+ is_expected.to have_css('.note-edit-form:not(.mr-note-edit-form)', visible: false, count: 1)
+ is_expected.to have_css('.note-edit-form.mr-note-edit-form', visible: false, count: 1)
end
describe 'editing the note' do
before do
find('.note').hover
- find(".js-note-edit").click
+ find('.js-note-edit').click
end
it 'shows the note edit form and hide the note body' do
@@ -90,14 +89,29 @@ describe 'Comments', feature: true do
end
end
- # TODO: fix after 7.7 release
- # it "should reset the edit note form textarea with the original content of the note if cancelled" do
- # within(".current-note-edit-form") do
- # fill_in "note[note]", with: "Some new content"
- # find(".btn-cancel").click
- # expect(find(".js-note-text", visible: false).text).to eq note.note
- # end
- # end
+ it 'resets the edit note form textarea with the original content of the note if cancelled' do
+ within('.current-note-edit-form') do
+ fill_in 'note[note]', with: 'Some new content'
+ find('.btn-cancel').click
+ expect(find('.js-note-text', visible: false).text).to eq ''
+ end
+ end
+
+ it 'allows using markdown buttons after saving a note and then trying to edit it again' do
+ page.within('.current-note-edit-form') do
+ fill_in 'note[note]', with: 'This is the new content'
+ find('.btn-save').click
+ end
+
+ find('.note').hover
+ find('.js-note-edit').click
+
+ page.within('.current-note-edit-form') do
+ expect(find('#note_note').value).to eq('This is the new content')
+ find('.js-md:first-child').click
+ expect(find('#note_note').value).to eq('This is the new content****')
+ end
+ end
it 'appends the edited at time to the note' do
page.within('.current-note-edit-form') do
@@ -184,7 +198,7 @@ describe 'Comments', feature: true do
end
describe 'the note form' do
- it "shouldn't add a second form for same row" do
+ it "does not add a second form for same row" do
click_diff_line
is_expected.
@@ -192,7 +206,7 @@ describe 'Comments', feature: true do
count: 1)
end
- it 'should be removed when canceled' do
+ it 'is removed when canceled' do
is_expected.to have_css('.js-temp-notes-holder')
page.within("form[data-line-code='#{line_code}']") do
diff --git a/spec/features/projects/blobs/shortcuts_blob_spec.rb b/spec/features/projects/blobs/shortcuts_blob_spec.rb
new file mode 100644
index 00000000000..30e2d587267
--- /dev/null
+++ b/spec/features/projects/blobs/shortcuts_blob_spec.rb
@@ -0,0 +1,37 @@
+require 'spec_helper'
+
+feature 'Blob shortcuts', feature: true do
+ include TreeHelper
+ let(:project) { create(:project, :public, :repository) }
+ let(:path) { project.repository.ls_files(project.repository.root_ref)[0] }
+ let(:sha) { project.repository.commit.sha }
+
+ describe 'On a file(blob)', js: true do
+ def get_absolute_url(path = "")
+ "http://#{page.server.host}:#{page.server.port}#{path}"
+ end
+
+ def visit_blob(fragment = nil)
+ visit namespace_project_blob_path(project.namespace, project, tree_join('master', path), anchor: fragment)
+ end
+
+ describe 'pressing "y"' do
+ it 'redirects to permalink with commit sha' do
+ visit_blob
+
+ find('body').native.send_key('y')
+
+ expect(page).to have_current_path(get_absolute_url(namespace_project_blob_path(project.namespace, project, tree_join(sha, path))), url: true)
+ end
+
+ it 'maintains fragment hash when redirecting' do
+ fragment = "L1"
+ visit_blob(fragment)
+
+ find('body').native.send_key('y')
+
+ expect(page).to have_current_path(get_absolute_url(namespace_project_blob_path(project.namespace, project, tree_join(sha, path), anchor: fragment)), url: true)
+ end
+ end
+ end
+end
diff --git a/spec/features/projects/builds_spec.rb b/spec/features/projects/builds_spec.rb
index 8c4d4320dc5..f7e0115643e 100644
--- a/spec/features/projects/builds_spec.rb
+++ b/spec/features/projects/builds_spec.rb
@@ -3,6 +3,7 @@ require 'tempfile'
feature 'Builds', :feature do
let(:user) { create(:user) }
+ let(:user_access_level) { :developer }
let(:project) { create(:project) }
let(:pipeline) { create(:ci_pipeline, project: project) }
@@ -14,7 +15,7 @@ feature 'Builds', :feature do
end
before do
- project.team << [user, :developer]
+ project.team << [user, user_access_level]
login_as(user)
end
@@ -26,7 +27,7 @@ feature 'Builds', :feature do
visit namespace_project_builds_path(project.namespace, project, scope: :pending)
end
- it "shows Pending tab builds" do
+ it "shows Pending tab jobs" do
expect(page).to have_link 'Cancel running'
expect(page).to have_selector('.nav-links li.active', text: 'Pending')
expect(page).to have_content build.short_sha
@@ -41,7 +42,7 @@ feature 'Builds', :feature do
visit namespace_project_builds_path(project.namespace, project, scope: :running)
end
- it "shows Running tab builds" do
+ it "shows Running tab jobs" do
expect(page).to have_selector('.nav-links li.active', text: 'Running')
expect(page).to have_link 'Cancel running'
expect(page).to have_content build.short_sha
@@ -56,20 +57,20 @@ feature 'Builds', :feature do
visit namespace_project_builds_path(project.namespace, project, scope: :finished)
end
- it "shows Finished tab builds" do
+ it "shows Finished tab jobs" do
expect(page).to have_selector('.nav-links li.active', text: 'Finished')
- expect(page).to have_content 'No builds to show'
+ expect(page).to have_content 'No jobs to show'
expect(page).to have_link 'Cancel running'
end
end
- context "All builds" do
+ context "All jobs" do
before do
project.builds.running_or_pending.each(&:success)
visit namespace_project_builds_path(project.namespace, project)
end
- it "shows All tab builds" do
+ it "shows All tab jobs" do
expect(page).to have_selector('.nav-links li.active', text: 'All')
expect(page).to have_content build.short_sha
expect(page).to have_content build.ref
@@ -97,7 +98,7 @@ feature 'Builds', :feature do
end
describe "GET /:project/builds/:id" do
- context "Build from project" do
+ context "Job from project" do
before do
visit namespace_project_build_path(project.namespace, project, build)
end
@@ -110,7 +111,7 @@ feature 'Builds', :feature do
end
end
- context "Build from other project" do
+ context "Job from other project" do
before do
visit namespace_project_build_path(project.namespace, project, build2)
end
@@ -131,7 +132,9 @@ feature 'Builds', :feature do
context 'Artifacts expire date' do
before do
- build.update_attributes(artifacts_file: artifacts_file, artifacts_expire_at: expire_at)
+ build.update_attributes(artifacts_file: artifacts_file,
+ artifacts_expire_at: expire_at)
+
visit namespace_project_build_path(project.namespace, project, build)
end
@@ -146,12 +149,23 @@ feature 'Builds', :feature do
context 'when expire date is defined' do
let(:expire_at) { Time.now + 7.days }
- it 'keeps artifacts when Keep button is clicked' do
- expect(page).to have_content 'The artifacts will be removed'
- click_link 'Keep'
+ context 'when user has ability to update job' do
+ it 'keeps artifacts when keep button is clicked' do
+ expect(page).to have_content 'The artifacts will be removed'
- expect(page).not_to have_link 'Keep'
- expect(page).not_to have_content 'The artifacts will be removed'
+ click_link 'Keep'
+
+ expect(page).to have_no_link 'Keep'
+ expect(page).to have_no_content 'The artifacts will be removed'
+ end
+ end
+
+ context 'when user does not have ability to update job' do
+ let(:user_access_level) { :guest }
+
+ it 'does not have keep button' do
+ expect(page).to have_no_link 'Keep'
+ end
end
end
@@ -183,8 +197,8 @@ feature 'Builds', :feature do
visit namespace_project_build_path(project.namespace, project, build)
end
- context 'when build has an initial trace' do
- it 'loads build trace' do
+ context 'when job has an initial trace' do
+ it 'loads job trace' do
expect(page).to have_content 'BUILD TRACE'
build.append_trace(' and more trace', 11)
@@ -228,32 +242,32 @@ feature 'Builds', :feature do
end
end
- context 'when build starts environment' do
+ context 'when job starts environment' do
let(:environment) { create(:environment, project: project) }
let(:pipeline) { create(:ci_pipeline, project: project) }
- context 'build is successfull and has deployment' do
+ context 'job is successfull and has deployment' do
let(:deployment) { create(:deployment) }
let(:build) { create(:ci_build, :success, environment: environment.name, deployments: [deployment], pipeline: pipeline) }
- it 'shows a link for the build' do
+ it 'shows a link for the job' do
visit namespace_project_build_path(project.namespace, project, build)
expect(page).to have_link environment.name
end
end
- context 'build is complete and not successfull' do
+ context 'job is complete and not successfull' do
let(:build) { create(:ci_build, :failed, environment: environment.name, pipeline: pipeline) }
- it 'shows a link for the build' do
+ it 'shows a link for the job' do
visit namespace_project_build_path(project.namespace, project, build)
expect(page).to have_link environment.name
end
end
- context 'build creates a new deployment' do
+ context 'job creates a new deployment' do
let!(:deployment) { create(:deployment, environment: environment, sha: project.commit.id) }
let(:build) { create(:ci_build, :success, environment: environment.name, pipeline: pipeline) }
@@ -267,7 +281,7 @@ feature 'Builds', :feature do
end
describe "POST /:project/builds/:id/cancel" do
- context "Build from project" do
+ context "Job from project" do
before do
build.run!
visit namespace_project_build_path(project.namespace, project, build)
@@ -281,7 +295,7 @@ feature 'Builds', :feature do
end
end
- context "Build from other project" do
+ context "Job from other project" do
before do
build.run!
visit namespace_project_build_path(project.namespace, project, build)
@@ -293,13 +307,13 @@ feature 'Builds', :feature do
end
describe "POST /:project/builds/:id/retry" do
- context "Build from project" do
+ context "Job from project" do
before do
build.run!
visit namespace_project_build_path(project.namespace, project, build)
click_link 'Cancel'
page.within('.build-header') do
- click_link 'Retry build'
+ click_link 'Retry job'
end
end
diff --git a/spec/features/projects/commit/builds_spec.rb b/spec/features/projects/commit/builds_spec.rb
index fcdf7870f34..268d420c594 100644
--- a/spec/features/projects/commit/builds_spec.rb
+++ b/spec/features/projects/commit/builds_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'project commit builds' do
+feature 'project commit pipelines', js: true do
given(:project) { create(:project) }
background do
@@ -16,11 +16,13 @@ feature 'project commit builds' do
ref: 'master')
end
- scenario 'user views commit builds page' do
- visit builds_namespace_project_commit_path(project.namespace,
- project, project.commit.sha)
+ scenario 'user views commit pipelines page' do
+ visit pipelines_namespace_project_commit_path(project.namespace, project, project.commit.sha)
- expect(page).to have_content('Builds')
+ page.within('.table-holder') do
+ expect(page).to have_content project.pipelines[0].status # pipeline status
+ expect(page).to have_content project.pipelines[0].id # pipeline ids
+ end
end
end
end
diff --git a/spec/features/projects/commit/cherry_pick_spec.rb b/spec/features/projects/commit/cherry_pick_spec.rb
new file mode 100644
index 00000000000..7baf7913424
--- /dev/null
+++ b/spec/features/projects/commit/cherry_pick_spec.rb
@@ -0,0 +1,89 @@
+require 'spec_helper'
+include WaitForAjax
+
+describe 'Cherry-pick Commits' do
+ let(:group) { create(:group) }
+ let(:project) { create(:project, namespace: group) }
+ let(:master_pickable_commit) { project.commit('7d3b0f7cff5f37573aea97cebfd5692ea1689924') }
+ let(:master_pickable_merge) { project.commit('e56497bb5f03a90a51293fc6d516788730953899') }
+
+ before do
+ login_as :user
+ project.team << [@user, :master]
+ visit namespace_project_commit_path(project.namespace, project, master_pickable_commit.id)
+ end
+
+ context "I cherry-pick a commit" do
+ it do
+ 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
+ uncheck 'create_merge_request'
+ click_button 'Cherry-pick'
+ end
+ expect(page).to have_content('The commit has been successfully cherry-picked.')
+ end
+ end
+
+ context "I cherry-pick a merge commit" do
+ it do
+ find("a[href='#modal-cherry-pick-commit']").click
+ 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
+
+ context "I cherry-pick a commit that was previously cherry-picked" do
+ it do
+ find("a[href='#modal-cherry-pick-commit']").click
+ page.within('#modal-cherry-pick-commit') do
+ uncheck 'create_merge_request'
+ click_button 'Cherry-pick'
+ end
+ 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'
+ click_button 'Cherry-pick'
+ end
+ expect(page).to have_content('Sorry, we cannot cherry-pick this commit automatically.')
+ end
+ end
+
+ context "I cherry-pick a commit in a new merge request" do
+ it do
+ find("a[href='#modal-cherry-pick-commit']").click
+ page.within('#modal-cherry-pick-commit') do
+ click_button 'Cherry-pick'
+ end
+ 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('.header-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/commits/cherry_pick_spec.rb b/spec/features/projects/commits/cherry_pick_spec.rb
deleted file mode 100644
index d46d9e9399e..00000000000
--- a/spec/features/projects/commits/cherry_pick_spec.rb
+++ /dev/null
@@ -1,88 +0,0 @@
-require 'spec_helper'
-include WaitForAjax
-
-describe 'Cherry-pick Commits' do
- let(:project) { create(:project) }
- let(:master_pickable_commit) { project.commit('7d3b0f7cff5f37573aea97cebfd5692ea1689924') }
- let(:master_pickable_merge) { project.commit('e56497bb5f03a90a51293fc6d516788730953899') }
-
- before do
- login_as :user
- project.team << [@user, :master]
- visit namespace_project_commit_path(project.namespace, project, master_pickable_commit.id)
- end
-
- context "I cherry-pick a commit" do
- it do
- 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
- uncheck 'create_merge_request'
- click_button 'Cherry-pick'
- end
- expect(page).to have_content('The commit has been successfully cherry-picked.')
- end
- end
-
- context "I cherry-pick a merge commit" do
- it do
- find("a[href='#modal-cherry-pick-commit']").click
- 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
-
- context "I cherry-pick a commit that was previously cherry-picked" do
- it do
- find("a[href='#modal-cherry-pick-commit']").click
- page.within('#modal-cherry-pick-commit') do
- uncheck 'create_merge_request'
- click_button 'Cherry-pick'
- end
- 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'
- click_button 'Cherry-pick'
- end
- expect(page).to have_content('Sorry, we cannot cherry-pick this commit automatically.')
- end
- end
-
- context "I cherry-pick a commit in a new merge request" do
- it do
- find("a[href='#modal-cherry-pick-commit']").click
- page.within('#modal-cherry-pick-commit') do
- click_button 'Cherry-pick'
- end
- 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('.header-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/compare_spec.rb b/spec/features/projects/compare_spec.rb
index 43eb4000e58..43eb4000e58 100644
--- a/spec/features/compare_spec.rb
+++ b/spec/features/projects/compare_spec.rb
diff --git a/spec/features/projects/features_visibility_spec.rb b/spec/features/projects/features_visibility_spec.rb
index 3bb33394be7..9079350186d 100644
--- a/spec/features/projects/features_visibility_spec.rb
+++ b/spec/features/projects/features_visibility_spec.rb
@@ -42,6 +42,17 @@ describe 'Edit Project Settings', feature: true do
end
end
+ context "When external issue tracker is enabled" do
+ it "does not hide issues tab" do
+ project.project_feature.update(issues_access_level: ProjectFeature::DISABLED)
+ allow_any_instance_of(Project).to receive(:external_issue_tracker).and_return(JiraService.new)
+
+ visit namespace_project_path(project.namespace, project)
+
+ expect(page).to have_selector(".shortcuts-issues")
+ end
+ end
+
context "pipelines subtabs" do
it "shows builds when enabled" do
visit namespace_project_pipelines_path(project.namespace, project)
diff --git a/spec/features/projects/files/editing_a_file_spec.rb b/spec/features/projects/files/editing_a_file_spec.rb
index fe047e00409..36a80d7575d 100644
--- a/spec/features/projects/files/editing_a_file_spec.rb
+++ b/spec/features/projects/files/editing_a_file_spec.rb
@@ -7,7 +7,7 @@ feature 'User wants to edit a file', feature: true do
let(:user) { create(:user) }
let(:commit_params) do
{
- source_branch: project.default_branch,
+ start_branch: project.default_branch,
target_branch: project.default_branch,
commit_message: "Committing First Update",
file_path: ".gitignore",
diff --git a/spec/features/projects/files/project_owner_creates_license_file_spec.rb b/spec/features/projects/files/project_owner_creates_license_file_spec.rb
index a521ce50f35..64094af29c0 100644
--- a/spec/features/projects/files/project_owner_creates_license_file_spec.rb
+++ b/spec/features/projects/files/project_owner_creates_license_file_spec.rb
@@ -6,7 +6,8 @@ feature 'project owner creates a license file', feature: true, js: true do
let(:project_master) { create(:user) }
let(:project) { create(:project) }
background do
- project.repository.remove_file(project_master, 'LICENSE', 'Remove LICENSE', 'master')
+ project.repository.remove_file(project_master, 'LICENSE',
+ message: 'Remove LICENSE', branch_name: 'master')
project.team << [project_master, :master]
login_as(project_master)
visit namespace_project_path(project.namespace, project)
diff --git a/spec/features/projects/group_links_spec.rb b/spec/features/projects/group_links_spec.rb
index 1a71a03fbd9..8b302a6aa23 100644
--- a/spec/features/projects/group_links_spec.rb
+++ b/spec/features/projects/group_links_spec.rb
@@ -14,10 +14,10 @@ feature 'Project group links', feature: true, js: true do
context 'setting an expiration date for a group link' do
before do
- visit namespace_project_group_links_path(project.namespace, project)
+ visit namespace_project_settings_members_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')
+ fill_in 'expires_at_groups', with: (Time.current + 4.5.days).strftime('%Y-%m-%d')
page.find('body').click
click_on 'Share'
end
diff --git a/spec/features/projects/import_export/export_file_spec.rb b/spec/features/projects/import_export/export_file_spec.rb
index 52d08982c7a..16dddb2a86b 100644
--- a/spec/features/projects/import_export/export_file_spec.rb
+++ b/spec/features/projects/import_export/export_file_spec.rb
@@ -74,6 +74,9 @@ feature 'Import/Export - project export integration test', feature: true, js: tr
Otherwise, please add the exception to +safe_list+ in CURRENT_SPEC using #{sensitive_word} as the key and the
correspondent hash or model as the value.
+ Also, if the attribute is a generated unique token, please add it to RelationFactory::TOKEN_RESET_MODELS if it needs to be
+ reset (to prevent duplicate column problems while importing to the same instance).
+
IMPORT_EXPORT_CONFIG: #{Gitlab::ImportExport.config_file}
CURRENT_SPEC: #{__FILE__}
MSG
diff --git a/spec/features/projects/import_export/namespace_export_file_spec.rb b/spec/features/projects/import_export/namespace_export_file_spec.rb
new file mode 100644
index 00000000000..d0bafc6168c
--- /dev/null
+++ b/spec/features/projects/import_export/namespace_export_file_spec.rb
@@ -0,0 +1,62 @@
+require 'spec_helper'
+
+feature 'Import/Export - Namespace export file cleanup', feature: true, js: true do
+ let(:export_path) { "#{Dir::tmpdir}/import_file_spec" }
+ let(:config_hash) { YAML.load_file(Gitlab::ImportExport.config_file).deep_stringify_keys }
+
+ let(:project) { create(:empty_project) }
+
+ background do
+ allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
+ end
+
+ after do
+ FileUtils.rm_rf(export_path, secure: true)
+ end
+
+ context 'admin user' do
+ before do
+ login_as(:admin)
+ end
+
+ context 'moving the namespace' do
+ scenario 'removes the export file' do
+ setup_export_project
+
+ old_export_path = project.export_path.dup
+
+ expect(File).to exist(old_export_path)
+
+ project.namespace.update(path: 'new_path')
+
+ expect(File).not_to exist(old_export_path)
+ end
+ end
+
+ context 'deleting the namespace' do
+ scenario 'removes the export file' do
+ setup_export_project
+
+ old_export_path = project.export_path.dup
+
+ expect(File).to exist(old_export_path)
+
+ project.namespace.destroy
+
+ expect(File).not_to exist(old_export_path)
+ end
+ end
+
+ def setup_export_project
+ visit edit_namespace_project_path(project.namespace, project)
+
+ expect(page).to have_content('Export project')
+
+ click_link 'Export project'
+
+ visit edit_namespace_project_path(project.namespace, project)
+
+ expect(page).to have_content('Download export')
+ end
+ end
+end
diff --git a/spec/features/projects/import_export/test_project_export.tar.gz b/spec/features/projects/import_export/test_project_export.tar.gz
index d3165d07d7b..20cdfbae24f 100644
--- a/spec/features/projects/import_export/test_project_export.tar.gz
+++ b/spec/features/projects/import_export/test_project_export.tar.gz
Binary files differ
diff --git a/spec/features/projects/issuable_templates_spec.rb b/spec/features/projects/issuable_templates_spec.rb
index 2f377312ea5..e90a033b8c4 100644
--- a/spec/features/projects/issuable_templates_spec.rb
+++ b/spec/features/projects/issuable_templates_spec.rb
@@ -18,8 +18,20 @@ feature 'issuable templates', feature: true, js: true do
let(:description_addition) { ' appending to description' }
background do
- project.repository.commit_file(user, '.gitlab/issue_templates/bug.md', template_content, 'added issue template', 'master', false)
- project.repository.commit_file(user, '.gitlab/issue_templates/test.md', longtemplate_content, 'added issue template', 'master', false)
+ project.repository.commit_file(
+ user,
+ '.gitlab/issue_templates/bug.md',
+ template_content,
+ message: 'added issue template',
+ branch_name: 'master',
+ update: false)
+ project.repository.commit_file(
+ user,
+ '.gitlab/issue_templates/test.md',
+ longtemplate_content,
+ message: 'added issue template',
+ branch_name: 'master',
+ update: false)
visit edit_namespace_project_issue_path project.namespace, project, issue
fill_in :'issue[title]', with: 'test issue title'
end
@@ -27,7 +39,7 @@ feature 'issuable templates', feature: true, js: true do
scenario 'user selects "bug" template' do
select_template 'bug'
wait_for_ajax
- preview_template
+ assert_template
save_changes
end
@@ -35,8 +47,7 @@ feature 'issuable templates', feature: true, js: true do
select_template 'bug'
wait_for_ajax
select_option 'No template'
- wait_for_ajax
- preview_template('')
+ assert_template('')
save_changes('')
end
@@ -44,9 +55,9 @@ feature 'issuable templates', feature: true, js: true do
select_template 'bug'
wait_for_ajax
find_field('issue_description').send_keys(description_addition)
- preview_template(template_content + description_addition)
+ assert_template(template_content + description_addition)
select_option 'Reset template'
- preview_template
+ assert_template
save_changes
end
@@ -68,7 +79,13 @@ feature 'issuable templates', feature: true, js: true do
let(:issue) { create(:issue, author: user, assignee: user, project: project) }
background do
- project.repository.commit_file(user, '.gitlab/issue_templates/bug.md', template_content, 'added issue template', 'master', false)
+ project.repository.commit_file(
+ user,
+ '.gitlab/issue_templates/bug.md',
+ template_content,
+ message: 'added issue template',
+ branch_name: 'master',
+ update: false)
visit edit_namespace_project_issue_path project.namespace, project, issue
fill_in :'issue[title]', with: 'test issue title'
fill_in :'issue[description]', with: prior_description
@@ -77,7 +94,7 @@ feature 'issuable templates', feature: true, js: true do
scenario 'user selects "bug" template' do
select_template 'bug'
wait_for_ajax
- preview_template("#{template_content}")
+ assert_template("#{template_content}")
save_changes
end
end
@@ -87,7 +104,13 @@ feature 'issuable templates', feature: true, js: true do
let(:merge_request) { create(:merge_request, :with_diffs, source_project: project) }
background do
- project.repository.commit_file(user, '.gitlab/merge_request_templates/feature-proposal.md', template_content, 'added merge request template', 'master', false)
+ project.repository.commit_file(
+ user,
+ '.gitlab/merge_request_templates/feature-proposal.md',
+ template_content,
+ message: 'added merge request template',
+ branch_name: 'master',
+ update: false)
visit edit_namespace_project_merge_request_path project.namespace, project, merge_request
fill_in :'merge_request[title]', with: 'test merge request title'
end
@@ -95,7 +118,7 @@ feature 'issuable templates', feature: true, js: true do
scenario 'user selects "feature-proposal" template' do
select_template 'feature-proposal'
wait_for_ajax
- preview_template
+ assert_template
save_changes
end
end
@@ -112,7 +135,13 @@ feature 'issuable templates', feature: true, js: true do
fork_project.team << [fork_user, :master]
create(:forked_project_link, forked_to_project: fork_project, forked_from_project: project)
login_as fork_user
- project.repository.commit_file(fork_user, '.gitlab/merge_request_templates/feature-proposal.md', template_content, 'added merge request template', 'master', false)
+ project.repository.commit_file(
+ fork_user,
+ '.gitlab/merge_request_templates/feature-proposal.md',
+ template_content,
+ message: 'added merge request template',
+ branch_name: 'master',
+ update: false)
visit edit_namespace_project_merge_request_path project.namespace, project, merge_request
fill_in :'merge_request[title]', with: 'test merge request title'
end
@@ -122,17 +151,15 @@ feature 'issuable templates', feature: true, js: true do
scenario 'user selects template' do
select_template 'feature-proposal'
wait_for_ajax
- preview_template
+ assert_template
save_changes
end
end
end
end
- def preview_template(expected_content = template_content)
- click_link 'Preview'
- expect(page).to have_content expected_content
- click_link 'Write'
+ def assert_template(expected_content = template_content)
+ expect(find('textarea')['value']).to eq(expected_content)
end
def save_changes(expected_content = template_content)
diff --git a/spec/features/projects/labels/update_prioritization_spec.rb b/spec/features/projects/labels/update_prioritization_spec.rb
index c9fa8315e79..97ce9cdfd87 100644
--- a/spec/features/projects/labels/update_prioritization_spec.rb
+++ b/spec/features/projects/labels/update_prioritization_spec.rb
@@ -20,7 +20,7 @@ feature 'Prioritize labels', feature: true do
scenario 'user can prioritize a group label', js: true do
visit namespace_project_labels_path(project.namespace, project)
- expect(page).to have_content('No prioritized labels yet')
+ expect(page).to have_content('Star labels to start sorting by priority')
page.within('.other-labels') do
all('.js-toggle-priority')[1].click
@@ -29,7 +29,7 @@ feature 'Prioritize labels', feature: true do
end
page.within('.prioritized-labels') do
- expect(page).not_to have_content('No prioritized labels yet')
+ expect(page).not_to have_content('Star labels to start sorting by priority')
expect(page).to have_content('feature')
end
end
@@ -55,7 +55,7 @@ feature 'Prioritize labels', feature: true do
scenario 'user can prioritize a project label', js: true do
visit namespace_project_labels_path(project.namespace, project)
- expect(page).to have_content('No prioritized labels yet')
+ expect(page).to have_content('Star labels to start sorting by priority')
page.within('.other-labels') do
first('.js-toggle-priority').click
@@ -64,7 +64,7 @@ feature 'Prioritize labels', feature: true do
end
page.within('.prioritized-labels') do
- expect(page).not_to have_content('No prioritized labels yet')
+ expect(page).not_to have_content('Star labels to start sorting by priority')
expect(page).to have_content('bug')
end
end
diff --git a/spec/features/projects/members/anonymous_user_sees_members_spec.rb b/spec/features/projects/members/anonymous_user_sees_members_spec.rb
index c5e3d143d91..d82cf53c690 100644
--- a/spec/features/projects/members/anonymous_user_sees_members_spec.rb
+++ b/spec/features/projects/members/anonymous_user_sees_members_spec.rb
@@ -11,10 +11,10 @@ feature 'Projects > Members > Anonymous user sees members', feature: true do
end
scenario "anonymous user visits the project's members page and sees the list of members" do
- visit namespace_project_project_members_path(project.namespace, project)
+ visit namespace_project_settings_members_path(project.namespace, project)
expect(current_path).to eq(
- namespace_project_project_members_path(project.namespace, project))
+ namespace_project_settings_members_path(project.namespace, project))
expect(page).to have_content(user.name)
end
end
diff --git a/spec/features/projects/members/group_links_spec.rb b/spec/features/projects/members/group_links_spec.rb
index 94995f7cf95..cffb935ad5a 100644
--- a/spec/features/projects/members/group_links_spec.rb
+++ b/spec/features/projects/members/group_links_spec.rb
@@ -12,7 +12,7 @@ feature 'Projects > Members > Anonymous user sees members', feature: true, js: t
@group_link = create(:project_group_link, project: project, group: group)
login_as(user)
- visit namespace_project_project_members_path(project.namespace, project)
+ visit namespace_project_settings_members_path(project.namespace, project)
end
it 'updates group access level' do
@@ -24,7 +24,7 @@ feature 'Projects > Members > Anonymous user sees members', feature: true, js: t
wait_for_ajax
- visit namespace_project_project_members_path(project.namespace, project)
+ visit namespace_project_settings_members_path(project.namespace, project)
expect(first('.group_member')).to have_content('Guest')
end
diff --git a/spec/features/projects/members/group_members_spec.rb b/spec/features/projects/members/group_members_spec.rb
index 7d0065ee2c4..3385e5972ff 100644
--- a/spec/features/projects/members/group_members_spec.rb
+++ b/spec/features/projects/members/group_members_spec.rb
@@ -19,7 +19,7 @@ feature 'Projects members', feature: true do
context 'with a group invitee' do
before do
group_invitee
- visit namespace_project_project_members_path(project.namespace, project)
+ visit namespace_project_settings_members_path(project.namespace, project)
end
scenario 'does not appear in the project members page' do
@@ -33,7 +33,7 @@ feature 'Projects members', feature: true do
before do
group_invitee
project_invitee
- visit namespace_project_project_members_path(project.namespace, project)
+ visit namespace_project_settings_members_path(project.namespace, project)
end
scenario 'shows the project invitee, the project developer, and the group owner' do
@@ -54,7 +54,7 @@ feature 'Projects members', feature: true do
context 'with a group requester' do
before do
group.request_access(group_requester)
- visit namespace_project_project_members_path(project.namespace, project)
+ visit namespace_project_settings_members_path(project.namespace, project)
end
scenario 'does not appear in the project members page' do
@@ -68,7 +68,7 @@ feature 'Projects members', feature: true do
before do
group.request_access(group_requester)
project.request_access(project_requester)
- visit namespace_project_project_members_path(project.namespace, project)
+ visit namespace_project_settings_members_path(project.namespace, project)
end
scenario 'shows the project requester, the project developer, and the group owner' do
diff --git a/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb b/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb
index b7273021c95..f136d9ce0fa 100644
--- a/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb
+++ b/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb
@@ -14,15 +14,15 @@ feature 'Projects > Members > Master adds member with expiration date', feature:
login_as(master)
end
- scenario 'expiration date is displayed in the members list' do
+ scenario 'expiration date is displayed in the members list', js: true do
travel_to Time.zone.parse('2016-08-06 08:00') do
- visit namespace_project_project_members_path(project.namespace, project)
-
+ visit namespace_project_settings_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 to project'
end
+ find('.users-project-form').click
+ click_on 'Add to project'
page.within "#project_member_#{new_member.project_members.first.id}" do
expect(page).to have_content('Expires in 4 days')
diff --git a/spec/features/projects/members/user_requests_access_spec.rb b/spec/features/projects/members/user_requests_access_spec.rb
index 97c42bd7f01..0b4dcaa39c6 100644
--- a/spec/features/projects/members/user_requests_access_spec.rb
+++ b/spec/features/projects/members/user_requests_access_spec.rb
@@ -39,7 +39,7 @@ feature 'Projects > Members > User requests access', feature: true do
open_project_settings_menu
click_link 'Members'
- visit namespace_project_project_members_path(project.namespace, project)
+ visit namespace_project_settings_members_path(project.namespace, project)
page.within('.content') do
expect(page).not_to have_content(user.name)
end
diff --git a/spec/features/projects/merge_request_button_spec.rb b/spec/features/projects/merge_request_button_spec.rb
new file mode 100644
index 00000000000..b6728960fb8
--- /dev/null
+++ b/spec/features/projects/merge_request_button_spec.rb
@@ -0,0 +1,108 @@
+require 'spec_helper'
+
+feature 'Merge Request button', feature: true do
+ shared_examples 'Merge Request button only shown when allowed' do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :public) }
+ let(:forked_project) { create(:project, :public, forked_from_project: project) }
+
+ context 'not logged in' do
+ it 'does not show Create Merge Request button' do
+ visit url
+
+ within("#content-body") do
+ expect(page).not_to have_link(label)
+ end
+ end
+ end
+
+ context 'logged in as developer' do
+ before do
+ login_as(user)
+ project.team << [user, :developer]
+ end
+
+ it 'shows Create Merge Request button' do
+ href = new_namespace_project_merge_request_path(project.namespace,
+ project,
+ merge_request: { source_branch: 'feature',
+ target_branch: 'master' })
+
+ visit url
+
+ within("#content-body") do
+ expect(page).to have_link(label, href: href)
+ end
+ end
+
+ context 'merge requests are disabled' do
+ before do
+ project.project_feature.update!(merge_requests_access_level: ProjectFeature::DISABLED)
+ end
+
+ it 'does not show Create Merge Request button' do
+ visit url
+
+ within("#content-body") do
+ expect(page).not_to have_link(label)
+ end
+ end
+ end
+ end
+
+ context 'logged in as non-member' do
+ before do
+ login_as(user)
+ end
+
+ it 'does not show Create Merge Request button' do
+ visit url
+
+ within("#content-body") do
+ expect(page).not_to have_link(label)
+ end
+ end
+
+ context 'on own fork of project' do
+ let(:user) { forked_project.owner }
+
+ it 'shows Create Merge Request button' do
+ href = new_namespace_project_merge_request_path(forked_project.namespace,
+ forked_project,
+ merge_request: { source_branch: 'feature',
+ target_branch: 'master' })
+
+ visit fork_url
+
+ within("#content-body") do
+ expect(page).to have_link(label, href: href)
+ end
+ end
+ end
+ end
+ end
+
+ context 'on branches page' do
+ it_behaves_like 'Merge Request button only shown when allowed' do
+ let(:label) { 'Merge Request' }
+ let(:url) { namespace_project_branches_path(project.namespace, project) }
+ let(:fork_url) { namespace_project_branches_path(forked_project.namespace, forked_project) }
+ end
+ end
+
+ context 'on compare page' do
+ it_behaves_like 'Merge Request button only shown when allowed' do
+ let(:label) { 'Create Merge Request' }
+ let(:url) { namespace_project_compare_path(project.namespace, project, from: 'master', to: 'feature') }
+ let(:fork_url) { namespace_project_compare_path(forked_project.namespace, forked_project, from: 'master', to: 'feature') }
+ end
+ end
+
+ context 'on commits page' do
+ it_behaves_like 'Merge Request button only shown when allowed' do
+ let(:label) { 'Create Merge Request' }
+ let(:url) { namespace_project_commits_path(project.namespace, project, 'feature') }
+ let(:fork_url) { namespace_project_commits_path(forked_project.namespace, forked_project, 'feature') }
+ end
+ end
+end
diff --git a/spec/features/projects/new_project_spec.rb b/spec/features/projects/new_project_spec.rb
index abfc46601fb..b56e562b2b6 100644
--- a/spec/features/projects/new_project_spec.rb
+++ b/spec/features/projects/new_project_spec.rb
@@ -1,11 +1,13 @@
require "spec_helper"
feature "New project", feature: true do
- context "Visibility level selector" do
- let(:user) { create(:admin) }
+ let(:user) { create(:admin) }
- before { login_as(user) }
+ before do
+ login_as(user)
+ end
+ context "Visibility level selector" do
Gitlab::VisibilityLevel.options.each do |key, level|
it "sets selector to #{key}" do
stub_application_setting(default_project_visibility: level)
@@ -16,4 +18,16 @@ feature "New project", feature: true do
end
end
end
+
+ context 'Import project options' do
+ before do
+ visit new_project_path
+ end
+
+ it 'does not autocomplete sensitive git repo URL' do
+ autocomplete = find('#project_import_url')['autocomplete']
+
+ expect(autocomplete).to eq('off')
+ end
+ end
end
diff --git a/spec/features/projects/pages_spec.rb b/spec/features/projects/pages_spec.rb
new file mode 100644
index 00000000000..11793c0f303
--- /dev/null
+++ b/spec/features/projects/pages_spec.rb
@@ -0,0 +1,60 @@
+require 'spec_helper'
+
+feature 'Pages', feature: true do
+ given(:project) { create(:empty_project) }
+ given(:user) { create(:user) }
+ given(:role) { :master }
+
+ background do
+ allow(Gitlab.config.pages).to receive(:enabled).and_return(true)
+
+ project.team << [user, role]
+
+ login_as(user)
+ end
+
+ shared_examples 'no pages deployed' do
+ scenario 'does not see anything to destroy' do
+ visit namespace_project_pages_path(project.namespace, project)
+
+ expect(page).not_to have_link('Remove pages')
+ expect(page).not_to have_text('Only the project owner can remove pages')
+ end
+ end
+
+ context 'when user is the owner' do
+ background do
+ project.namespace.update(owner: user)
+ end
+
+ context 'when pages deployed' do
+ background do
+ allow_any_instance_of(Project).to receive(:pages_deployed?) { true }
+ end
+
+ scenario 'sees "Remove pages" link' do
+ visit namespace_project_pages_path(project.namespace, project)
+
+ expect(page).to have_link('Remove pages')
+ end
+ end
+
+ it_behaves_like 'no pages deployed'
+ end
+
+ context 'when the user is not the owner' do
+ context 'when pages deployed' do
+ background do
+ allow_any_instance_of(Project).to receive(:pages_deployed?) { true }
+ end
+
+ scenario 'sees "Only the project owner can remove pages" text' do
+ visit namespace_project_pages_path(project.namespace, project)
+
+ expect(page).to have_text('Only the project owner can remove pages')
+ end
+ end
+
+ it_behaves_like 'no pages deployed'
+ end
+end
diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb
index 14e009daba8..0b5ccc8c515 100644
--- a/spec/features/projects/pipelines/pipeline_spec.rb
+++ b/spec/features/projects/pipelines/pipeline_spec.rb
@@ -11,18 +11,42 @@ describe 'Pipeline', :feature, :js do
project.team << [user, :developer]
end
+ shared_context 'pipeline builds' do
+ let!(:build_passed) do
+ create(:ci_build, :success,
+ pipeline: pipeline, stage: 'build', name: 'build')
+ end
+
+ let!(:build_failed) do
+ create(:ci_build, :failed,
+ pipeline: pipeline, stage: 'test', name: 'test', commands: 'test')
+ end
+
+ let!(:build_running) do
+ create(:ci_build, :running,
+ pipeline: pipeline, stage: 'deploy', name: 'deploy')
+ end
+
+ let!(:build_manual) do
+ create(:ci_build, :manual,
+ pipeline: pipeline, stage: 'deploy', name: 'manual-build')
+ end
+
+ let!(:build_external) do
+ create(:generic_commit_status, status: 'success',
+ pipeline: pipeline,
+ name: 'jenkins',
+ stage: 'external',
+ target_url: 'http://gitlab.com/status')
+ end
+ end
+
describe 'GET /:project/pipelines/:id' do
+ include_context 'pipeline builds'
+
let(:project) { create(:project) }
let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master', sha: project.commit.id) }
- before do
- @success = create(:ci_build, :success, pipeline: pipeline, stage: 'build', name: 'build')
- @failed = create(:ci_build, :failed, pipeline: pipeline, stage: 'test', name: 'test', commands: 'test')
- @running = create(:ci_build, :running, pipeline: pipeline, stage: 'deploy', name: 'deploy')
- @manual = create(:ci_build, :manual, pipeline: pipeline, stage: 'deploy', name: 'manual-build')
- @external = create(:generic_commit_status, status: 'success', pipeline: pipeline, name: 'jenkins', stage: 'external')
- end
-
before { visit namespace_project_pipeline_path(project.namespace, project, pipeline) }
it 'shows the pipeline graph' do
@@ -42,8 +66,8 @@ describe 'Pipeline', :feature, :js do
context 'when pipeline has running builds' do
it 'shows a running icon and a cancel action for the running build' do
page.within('#ci-badge-deploy') do
- expect(page).to have_selector('.ci-status-icon-running')
- expect(page).to have_selector('.ci-action-icon-container .fa-ban')
+ expect(page).to have_selector('.js-ci-status-icon-running')
+ expect(page).to have_selector('.js-icon-action-cancel')
expect(page).to have_content('deploy')
end
end
@@ -58,75 +82,76 @@ describe 'Pipeline', :feature, :js do
context 'when pipeline has successful builds' do
it 'shows the success icon and a retry action for the successful build' do
page.within('#ci-badge-build') do
- expect(page).to have_selector('.ci-status-icon-success')
+ expect(page).to have_selector('.js-ci-status-icon-success')
expect(page).to have_content('build')
end
page.within('#ci-badge-build .ci-action-icon-container') do
- expect(page).to have_selector('.ci-action-icon-container .fa-refresh')
+ expect(page).to have_selector('.js-icon-action-retry')
end
end
- it 'should be possible to retry the success build' do
+ it 'should be possible to retry the success job' do
find('#ci-badge-build .ci-action-icon-container').trigger('click')
- expect(page).not_to have_content('Retry build')
+ expect(page).not_to have_content('Retry job')
end
end
context 'when pipeline has failed builds' do
it 'shows the failed icon and a retry action for the failed build' do
page.within('#ci-badge-test') do
- expect(page).to have_selector('.ci-status-icon-failed')
+ expect(page).to have_selector('.js-ci-status-icon-failed')
expect(page).to have_content('test')
end
page.within('#ci-badge-test .ci-action-icon-container') do
- expect(page).to have_selector('.ci-action-icon-container .fa-refresh')
+ expect(page).to have_selector('.js-icon-action-retry')
end
end
it 'should be possible to retry the failed build' do
find('#ci-badge-test .ci-action-icon-container').trigger('click')
- expect(page).not_to have_content('Retry build')
+ expect(page).not_to have_content('Retry job')
end
end
- context 'when pipeline has manual builds' do
+ context 'when pipeline has manual jobs' do
it 'shows the skipped icon and a play action for the manual build' do
page.within('#ci-badge-manual-build') do
- expect(page).to have_selector('.ci-status-icon-manual')
+ expect(page).to have_selector('.js-ci-status-icon-manual')
expect(page).to have_content('manual')
end
page.within('#ci-badge-manual-build .ci-action-icon-container') do
- expect(page).to have_selector('.ci-action-icon-container .fa-play')
+ expect(page).to have_selector('.js-icon-action-play')
end
end
- it 'should be possible to play the manual build' do
+ it 'should be possible to play the manual job' do
find('#ci-badge-manual-build .ci-action-icon-container').trigger('click')
- expect(page).not_to have_content('Play build')
+ expect(page).not_to have_content('Play job')
end
end
- context 'when pipeline has external build' do
+ context 'when pipeline has external job' do
it 'shows the success icon and the generic comit status build' do
- expect(page).to have_selector('.ci-status-icon-success')
+ expect(page).to have_selector('.js-ci-status-icon-success')
expect(page).to have_content('jenkins')
+ expect(page).to have_link('jenkins', href: 'http://gitlab.com/status')
end
end
end
context 'page tabs' do
- it 'shows Pipeline and Builds tabs with link' do
+ it 'shows Pipeline and Jobs tabs with link' do
expect(page).to have_link('Pipeline')
- expect(page).to have_link('Builds')
+ expect(page).to have_link('Jobs')
end
- it 'shows counter in Builds tab' do
+ it 'shows counter in Jobs tab' do
expect(page.find('.js-builds-counter').text).to eq(pipeline.statuses.count.to_s)
end
@@ -135,7 +160,7 @@ describe 'Pipeline', :feature, :js do
end
end
- context 'retrying builds' do
+ context 'retrying jobs' do
it { expect(page).not_to have_content('retried') }
context 'when retrying' do
@@ -145,7 +170,7 @@ describe 'Pipeline', :feature, :js do
end
end
- context 'canceling builds' do
+ context 'canceling jobs' do
it { expect(page).not_to have_selector('.ci-canceled') }
context 'when canceling' do
@@ -157,51 +182,47 @@ describe 'Pipeline', :feature, :js do
end
describe 'GET /:project/pipelines/:id/builds' do
+ include_context 'pipeline builds'
+
let(:project) { create(:project) }
let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master', sha: project.commit.id) }
before do
- @success = create(:ci_build, :success, pipeline: pipeline, stage: 'build', name: 'build')
- @failed = create(:ci_build, :failed, pipeline: pipeline, stage: 'test', name: 'test', commands: 'test')
- @running = create(:ci_build, :running, pipeline: pipeline, stage: 'deploy', name: 'deploy')
- @manual = create(:ci_build, :manual, pipeline: pipeline, stage: 'deploy', name: 'manual-build')
- @external = create(:generic_commit_status, status: 'success', pipeline: pipeline, name: 'jenkins', stage: 'external')
+ visit builds_namespace_project_pipeline_path(project.namespace, project, pipeline)
end
- before { visit builds_namespace_project_pipeline_path(project.namespace, project, pipeline)}
-
- it 'shows a list of builds' do
+ it 'shows a list of jobs' do
expect(page).to have_content('Test')
- expect(page).to have_content(@success.id)
+ expect(page).to have_content(build_passed.id)
expect(page).to have_content('Deploy')
- expect(page).to have_content(@failed.id)
- expect(page).to have_content(@running.id)
- expect(page).to have_content(@external.id)
+ expect(page).to have_content(build_failed.id)
+ expect(page).to have_content(build_running.id)
+ expect(page).to have_content(build_external.id)
expect(page).to have_content('Retry failed')
expect(page).to have_content('Cancel running')
expect(page).to have_link('Play')
end
- it 'shows Builds tab pane as active' do
+ it 'shows jobs tab pane as active' do
expect(page).to have_css('#js-tab-builds.active')
end
context 'page tabs' do
- it 'shows Pipeline and Builds tabs with link' do
+ it 'shows Pipeline and Jobs tabs with link' do
expect(page).to have_link('Pipeline')
- expect(page).to have_link('Builds')
+ expect(page).to have_link('Jobs')
end
- it 'shows counter in Builds tab' do
+ it 'shows counter in Jobs tab' do
expect(page.find('.js-builds-counter').text).to eq(pipeline.statuses.count.to_s)
end
- it 'shows Builds tab as active' do
+ it 'shows Jobs tab as active' do
expect(page).to have_css('li.js-builds-tab-link.active')
end
end
- context 'retrying builds' do
+ context 'retrying jobs' do
it { expect(page).not_to have_content('retried') }
context 'when retrying' do
@@ -212,7 +233,7 @@ describe 'Pipeline', :feature, :js do
end
end
- context 'canceling builds' do
+ context 'canceling jobs' do
it { expect(page).not_to have_selector('.ci-canceled') }
context 'when canceling' do
@@ -223,14 +244,14 @@ describe 'Pipeline', :feature, :js do
end
end
- context 'playing manual build' do
+ context 'playing manual job' do
before do
within '.pipeline-holder' do
click_link('Play')
end
end
- it { expect(@manual.reload).to be_pending }
+ it { expect(build_manual.reload).to be_pending }
end
end
end
diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb
index 1ff57f92c4c..ca18ac073d8 100644
--- a/spec/features/projects/pipelines/pipelines_spec.rb
+++ b/spec/features/projects/pipelines/pipelines_spec.rb
@@ -1,267 +1,364 @@
require 'spec_helper'
describe 'Pipelines', :feature, :js do
- include GitlabRoutingHelper
- include WaitForAjax
+ include WaitForVueResource
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) }
+ context 'when user is logged in' do
+ let(:user) { create(:user) }
- it { expect(page).to have_http_status(:success) }
+ before do
+ login_as(user)
+ project.team << [user, :developer]
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)
+ describe 'GET /:project/pipelines' do
+ let(:project) { create(:project) }
+
+ let!(:pipeline) do
+ create(
+ :ci_empty_pipeline,
+ project: project,
+ ref: 'master',
+ status: 'running',
+ sha: project.commit.id,
+ )
end
- it { expect(page).to have_link('Cancel') }
- it { expect(page).to have_selector('.ci-running') }
+ [:all, :running, :branches].each do |scope|
+ context "when displaying #{scope}" do
+ before do
+ visit_project_pipelines(scope: scope)
+ end
- context 'when canceling' do
- before { click_link('Cancel') }
-
- it { expect(page).not_to have_link('Cancel') }
- it { expect(page).to have_selector('.ci-canceled') }
+ it 'contains pipeline commit short SHA' do
+ expect(page).to have_content(pipeline.short_sha)
+ end
+ end
end
- end
- context 'retryable pipelines' do
- let!(:build) { create(:ci_build, pipeline: pipeline, stage: 'test', commands: 'test') }
+ context 'when pipeline is cancelable' do
+ let!(:build) do
+ create(:ci_build, pipeline: pipeline,
+ stage: 'test',
+ commands: 'test')
+ end
- before do
- build.drop
- visit namespace_project_pipelines_path(project.namespace, project)
- end
+ before do
+ build.run
+ visit_project_pipelines
+ end
- it { expect(page).to have_link('Retry') }
- it { expect(page).to have_selector('.ci-failed') }
+ it 'indicates that pipeline can be canceled' do
+ expect(page).to have_link('Cancel')
+ expect(page).to have_selector('.ci-running')
+ end
- context 'when retrying' do
- before { click_link('Retry') }
+ context 'when canceling' do
+ before { click_link('Cancel') }
- it { expect(page).not_to have_link('Retry') }
- it { expect(page).to have_selector('.ci-running') }
+ it 'indicated that pipelines was canceled' do
+ expect(page).not_to have_link('Cancel')
+ expect(page).to have_selector('.ci-canceled')
+ end
+ end
end
- end
- context 'with manual actions' do
- let!(:manual) do
- create(:ci_build, :manual, pipeline: pipeline,
- name: 'manual build',
- stage: 'test',
- commands: 'test')
- end
+ context 'when pipeline is retryable' do
+ let!(:build) do
+ create(:ci_build, pipeline: pipeline,
+ stage: 'test',
+ commands: 'test')
+ end
- before do
- visit namespace_project_pipelines_path(project.namespace, project)
- end
+ before do
+ build.drop
+ visit_project_pipelines
+ end
- it 'has link to the manual action' do
- find('.js-pipeline-dropdown-manual-actions').click
+ it 'indicates that pipeline can be retried' do
+ expect(page).to have_link('Retry')
+ expect(page).to have_selector('.ci-failed')
+ end
- expect(page).to have_link('Manual build')
- end
+ context 'when retrying' do
+ before { click_link('Retry') }
- context 'when manual action was played' do
- before do
- find('.js-pipeline-dropdown-manual-actions').click
- click_link('Manual build')
+ it 'shows running pipeline that is not retryable' do
+ expect(page).not_to have_link('Retry')
+ expect(page).to have_selector('.ci-running')
+ end
end
+ end
- it 'enqueues manual action job' do
- expect(manual.reload).to be_pending
+ context 'when pipeline has configuration errors' do
+ let(:pipeline) do
+ create(:ci_pipeline, :invalid, project: project)
end
- 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_project_pipelines }
- before do
- visit namespace_project_pipelines_path(project.namespace, project)
+ it 'contains badge that indicates errors' do
+ expect(page).to have_content 'yaml invalid'
end
- it 'is cancelable' do
- expect(page).to have_link('Cancel')
+ it 'contains badge with tooltip which contains error' do
+ expect(pipeline).to have_yaml_errors
+ expect(page).to have_selector(
+ %Q{span[data-original-title="#{pipeline.yaml_errors}"]})
end
+ end
- it 'has pipeline running' do
- expect(page).to have_selector('.ci-running')
+ context 'with manual actions' do
+ let!(:manual) do
+ create(:ci_build, :manual,
+ pipeline: pipeline,
+ name: 'manual build',
+ stage: 'test',
+ commands: 'test')
end
- context 'when canceling' do
- before { click_link('Cancel') }
+ before { visit_project_pipelines }
- it { expect(page).not_to have_link('Cancel') }
- it { expect(page).to have_selector('.ci-canceled') }
+ it 'has a dropdown with play button' do
+ expect(page).to have_selector('.dropdown-toggle.btn.btn-default .icon-play')
end
- end
- context 'when failed' do
- let!(:status) { create(:generic_commit_status, :pending, pipeline: pipeline, stage: 'test') }
+ it 'has link to the manual action' do
+ find('.js-pipeline-dropdown-manual-actions').click
- before do
- status.drop
- visit namespace_project_pipelines_path(project.namespace, project)
+ expect(page).to have_link('manual build')
end
- it 'is not retryable' do
- expect(page).not_to have_link('Retry')
- end
+ context 'when manual action was played' do
+ before do
+ find('.js-pipeline-dropdown-manual-actions').click
+ click_link('manual build')
+ end
- it 'has failed pipeline' do
- expect(page).to have_selector('.ci-failed')
+ it 'enqueues manual action job' do
+ expect(manual.reload).to be_pending
+ end
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) }
+ context 'for generic statuses' do
+ context 'when running' do
+ let!(:running) do
+ create(:generic_commit_status,
+ status: 'running',
+ pipeline: pipeline,
+ stage: 'test')
+ end
+
+ before { visit_project_pipelines }
+
+ it 'is cancelable' do
+ expect(page).to have_link('Cancel')
+ end
+
+ it 'has pipeline running' do
+ expect(page).to have_selector('.ci-running')
+ end
+
+ context 'when canceling' do
+ before { click_link('Cancel') }
+
+ it 'indicates that pipeline was canceled' do
+ expect(page).not_to have_link('Cancel')
+ expect(page).to have_selector('.ci-canceled')
+ end
+ end
+ end
- it { expect(page).to have_selector('.build-artifacts') }
- it do
- find('.js-pipeline-dropdown-download').click
- expect(page).to have_link(with_artifacts.name)
+ context 'when failed' do
+ let!(:status) do
+ create(:generic_commit_status, :pending,
+ pipeline: pipeline,
+ stage: 'test')
+ end
+
+ before do
+ status.drop
+ visit_project_pipelines
+ 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 'with artifacts expired' do
- let!(:with_artifacts_expired) { create(:ci_build, :artifacts_expired, :success, pipeline: pipeline, name: 'rspec', stage: 'test') }
+ context 'downloadable pipelines' do
+ context 'with artifacts' do
+ let!(:with_artifacts) do
+ create(:ci_build, :artifacts, :success,
+ pipeline: pipeline,
+ name: 'rspec tests',
+ stage: 'test')
+ end
- before { visit namespace_project_pipelines_path(project.namespace, project) }
+ before { visit_project_pipelines }
- it { expect(page).not_to have_selector('.build-artifacts') }
- end
+ it 'has artifats' do
+ expect(page).to have_selector('.build-artifacts')
+ end
- context 'without artifacts' do
- let!(:without_artifacts) { create(:ci_build, :success, pipeline: pipeline, name: 'rspec', stage: 'test') }
+ it 'has artifacts download dropdown' do
+ find('.js-pipeline-dropdown-download').click
- before { visit namespace_project_pipelines_path(project.namespace, project) }
+ expect(page).to have_link(with_artifacts.name)
+ end
+ end
- it { expect(page).not_to have_selector('.build-artifacts') }
- end
- end
+ context 'with artifacts expired' do
+ let!(:with_artifacts_expired) do
+ create(:ci_build, :artifacts_expired, :success,
+ pipeline: pipeline,
+ name: 'rspec',
+ stage: 'test')
+ end
- context 'mini pipleine graph' do
- let!(:build) do
- create(:ci_build, pipeline: pipeline, stage: 'build', name: 'build')
- end
+ before { visit_project_pipelines }
- before do
- visit namespace_project_pipelines_path(project.namespace, project)
- end
+ it { expect(page).not_to have_selector('.build-artifacts') }
+ end
+
+ context 'without artifacts' do
+ let!(:without_artifacts) do
+ create(:ci_build, :success,
+ pipeline: pipeline,
+ name: 'rspec',
+ stage: 'test')
+ end
- it 'should render a mini pipeline graph' do
- endpoint = stage_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline, stage: build.name)
+ before { visit_project_pipelines }
- expect(page).to have_selector('.mini-pipeline-graph')
- expect(page).to have_selector(".js-builds-dropdown-button[data-stage-endpoint='#{endpoint}']")
+ it { expect(page).not_to have_selector('.build-artifacts') }
+ end
end
- context 'when clicking a graph stage' do
- it 'should open a dropdown' do
- find('.js-builds-dropdown-button').trigger('click')
+ context 'mini pipeline graph' do
+ let!(:build) do
+ create(:ci_build, :pending, pipeline: pipeline,
+ stage: 'build',
+ name: 'build')
+ end
- wait_for_ajax
+ before { visit_project_pipelines }
- expect(page).to have_link build.name
+ it 'should render a mini pipeline graph' do
+ expect(page).to have_selector('.js-mini-pipeline-graph')
+ expect(page).to have_selector('.js-builds-dropdown-button')
end
- it 'should be possible to retry the failed build' do
- find('.js-builds-dropdown-button').trigger('click')
+ context 'when clicking a stage badge' do
+ it 'should open a dropdown' do
+ find('.js-builds-dropdown-button').trigger('click')
+
+ expect(page).to have_link build.name
+ end
- wait_for_ajax
+ it 'should be possible to cancel pending build' do
+ find('.js-builds-dropdown-button').trigger('click')
+ find('a.js-ci-action-icon').trigger('click')
- find('a.ci-action-icon-container').trigger('click')
- expect(page).not_to have_content('Cancel running')
+ expect(page).to have_content('canceled')
+ expect(build.reload).to be_canceled
+ end
end
end
end
- end
- describe 'POST /:project/pipelines' do
- let(:project) { create(:project) }
+ describe 'POST /:project/pipelines' do
+ let(:project) { create(:project) }
- before { visit new_namespace_project_pipeline_path(project.namespace, project) }
+ before do
+ visit new_namespace_project_pipeline_path(project.namespace, project)
+ end
+
+ 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 }
- context 'for valid commit' do
- before { fill_in('pipeline[ref]', with: 'master') }
+ it 'creates a new pipeline' do
+ expect { click_on 'Create pipeline' }
+ .to change { Ci::Pipeline.count }.by(1)
+ end
+ end
- context 'with gitlab-ci.yml' do
- before { stub_ci_pipeline_to_return_yaml_file }
+ context 'without gitlab-ci.yml' do
+ before { click_on 'Create pipeline' }
- it { expect{ click_on 'Create pipeline' }.to change{ Ci::Pipeline.count }.by(1) }
+ it { expect(page).to have_content('Missing .gitlab-ci.yml file') }
+ end
end
- context 'without gitlab-ci.yml' do
- before { click_on 'Create pipeline' }
+ 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('Missing .gitlab-ci.yml file') }
+ it { expect(page).to have_content('Reference not found') }
end
end
- context 'for invalid commit' do
+ describe 'Create pipelines' do
+ let(:project) { create(:project) }
+
before do
- fill_in('pipeline[ref]', with: 'invalid-reference')
- click_on 'Create pipeline'
+ 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
- it { expect(page).to have_content('Reference not found') }
+ 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
- describe 'Create pipelines', feature: true do
- let(:project) { create(:project) }
-
+ context 'when user is not logged in' do
before do
- visit new_namespace_project_pipeline_path(project.namespace, project)
+ visit namespace_project_pipelines_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
+ context 'when project is public' do
+ let(:project) { create(:project, :public) }
+
+ it { expect(page).to have_content 'No pipelines to show' }
+ it { expect(page).to have_http_status(:success) }
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)
+ context 'when project is private' do
+ let(:project) { create(:project, :private) }
- within('.ui-autocomplete') do
- expect(page).to have_selector('li', text: 'fix')
- end
- end
+ it { expect(page).to have_content 'You need to sign in' }
end
end
+
+ def visit_project_pipelines(**query)
+ visit namespace_project_pipelines_path(project.namespace, project, query)
+ wait_for_vue_resource
+ end
end
diff --git a/spec/features/projects/project_settings_spec.rb b/spec/features/projects/project_settings_spec.rb
index bf60cca4ea4..5d0314d5c09 100644
--- a/spec/features/projects/project_settings_spec.rb
+++ b/spec/features/projects/project_settings_spec.rb
@@ -21,13 +21,23 @@ describe 'Edit Project Settings', feature: true do
expect(page).to have_content "Name can contain only letters, digits, emojis, '_', '.', dash, space. It must start with letter, digit, emoji or '_'."
expect(page).to have_button 'Save changes'
end
+
+ scenario 'shows a successful notice when the project is updated' do
+ visit edit_namespace_project_path(project.namespace, project)
+
+ fill_in 'project_name_edit', with: 'hello world'
+
+ click_button 'Save changes'
+
+ expect(page).to have_content "Project 'hello world' was successfully updated."
+ end
end
describe 'Rename repository' do
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 'project_name', with: 'foo&bar'
fill_in 'Path', with: 'foo&bar'
click_button 'Rename project'
@@ -43,7 +53,7 @@ describe 'Edit Project Settings', feature: true do
it 'shows error for invalid project name' do
visit edit_namespace_project_path(project.namespace, project)
- fill_in 'Project name', with: '🚀 foo bar ☁️'
+ fill_in 'project_name', with: '🚀 foo bar ☁️'
click_button 'Rename project'
diff --git a/spec/features/projects/ref_switcher_spec.rb b/spec/features/projects/ref_switcher_spec.rb
index 472491188c9..38fe2d92885 100644
--- a/spec/features/projects/ref_switcher_spec.rb
+++ b/spec/features/projects/ref_switcher_spec.rb
@@ -17,14 +17,15 @@ feature 'Ref switcher', feature: true, js: true do
page.within '.project-refs-form' do
input = find('input[type="search"]')
- input.set 'expand'
+ input.set 'binary'
+ wait_for_ajax
input.native.send_keys :down
input.native.send_keys :down
input.native.send_keys :enter
end
- expect(page).to have_title 'expand-collapse-files'
+ expect(page).to have_title 'binary-encoding'
end
it "user selects ref with special characters" do
diff --git a/spec/features/projects/services/mattermost_slash_command_spec.rb b/spec/features/projects/services/mattermost_slash_command_spec.rb
index f474e7e891b..f5adb53a2dc 100644
--- a/spec/features/projects/services/mattermost_slash_command_spec.rb
+++ b/spec/features/projects/services/mattermost_slash_command_spec.rb
@@ -1,32 +1,31 @@
require 'spec_helper'
feature 'Setup Mattermost slash commands', feature: true do
- include WaitForAjax
-
let(:user) { create(:user) }
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project) }
let(:service) { project.create_mattermost_slash_commands_service }
+ let(:mattermost_enabled) { true }
before do
+ Settings.mattermost['enabled'] = mattermost_enabled
project.team << [user, :master]
login_as(user)
+ visit edit_namespace_project_service_path(project.namespace, project, service)
end
- describe 'user visites the mattermost slash command config page', js: true do
+ describe 'user visits the mattermost slash command config page' do
it 'shows a help message' do
- visit edit_namespace_project_service_path(project.namespace, project, service)
+ expect(page).to have_content("This service allows users to perform common")
+ end
- wait_for_ajax
+ it 'shows a token placeholder' do
+ token_placeholder = find_field('service_token')['placeholder']
- expect(page).to have_content("This service allows GitLab users to perform common")
+ expect(token_placeholder).to eq('XXxxXXxxXXxxXXxxXXxxXXxx')
end
- end
-
- describe 'saving a token' do
- let(:token) { ('a'..'z').to_a.join }
it 'shows the token after saving' do
- visit edit_namespace_project_service_path(project.namespace, project, service)
+ token = ('a'..'z').to_a.join
fill_in 'service_token', with: token
click_on 'Save'
@@ -35,14 +34,121 @@ feature 'Setup Mattermost slash commands', feature: true do
expect(value).to eq(token)
end
- end
- describe 'the trigger url' do
- it 'shows the correct url' do
- visit edit_namespace_project_service_path(project.namespace, project, service)
+ it 'shows the add to mattermost button' do
+ expect(page).to have_link('Add to Mattermost')
+ end
+
+ it 'shows an explanation if user is a member of no teams' do
+ stub_teams(count: 0)
+
+ click_link 'Add to Mattermost'
+
+ expect(page).to have_content('You aren’t a member of any team on the Mattermost instance')
+ expect(page).to have_link('join a team', href: "#{Gitlab.config.mattermost.host}/select_team")
+ end
+
+ it 'shows an explanation if user is a member of 1 team' do
+ stub_teams(count: 1)
+
+ click_link 'Add to Mattermost'
+
+ expect(page).to have_content('The team where the slash commands will be used in')
+ expect(page).to have_content('This is the only available team.')
+ end
+
+ it 'shows a disabled prefilled select if user is a member of 1 team' do
+ teams = stub_teams(count: 1)
+
+ click_link 'Add to Mattermost'
+
+ team_name = teams.first[1]['display_name']
+ select_element = find('select#mattermost_team_id')
+ selected_option = select_element.find('option[selected]')
+
+ expect(select_element['disabled']).to eq('disabled')
+ expect(selected_option).to have_content(team_name.to_s)
+ end
+
+ it 'has a hidden input for the prefilled value if user is a member of 1 team' do
+ teams = stub_teams(count: 1)
+
+ click_link 'Add to Mattermost'
+
+ expect(find('input#mattermost_team_id', visible: false).value).to eq(teams.first[0].to_s)
+ end
+
+ it 'shows an explanation user is a member of multiple teams' do
+ stub_teams(count: 2)
+
+ click_link 'Add to Mattermost'
+
+ expect(page).to have_content('Select the team where the slash commands will be used in')
+ expect(page).to have_content('The list shows all available teams.')
+ end
+
+ it 'shows a select with team options user is a member of multiple teams' do
+ stub_teams(count: 2)
+
+ click_link 'Add to Mattermost'
+
+ select_element = find('select#mattermost_team_id')
+ selected_option = select_element.find('option[selected]')
+
+ expect(select_element['disabled']).to be(nil)
+ expect(selected_option).to have_content('Select team...')
+ # The 'Select team...' placeholder is item `0`.
+ expect(select_element.all('option').count).to eq(3)
+ end
+
+ it 'shows an error alert with the error message if there is an error requesting teams' do
+ allow_any_instance_of(MattermostSlashCommandsService).to receive(:list_teams) { [[], 'test mattermost error message'] }
+
+ click_link 'Add to Mattermost'
+
+ expect(page).to have_selector('.alert')
+ expect(page).to have_content('test mattermost error message')
+ end
+
+ def stub_teams(count: 0)
+ teams = create_teams(count)
+
+ allow_any_instance_of(MattermostSlashCommandsService).to receive(:list_teams) { teams }
+
+ teams
+ end
+
+ def create_teams(count = 0)
+ teams = {}
+
+ count.times do |i|
+ i += 1
+ teams[i] = { id: i, display_name: i }
+ end
+
+ teams
+ end
+
+ describe 'mattermost service is not enabled' do
+ let(:mattermost_enabled) { false }
+
+ it 'shows the correct trigger url' do
+ value = find_field('request_url').value
+
+ expect(value).to match("api/v3/projects/#{project.id}/services/mattermost_slash_commands/trigger")
+ end
+
+ it 'shows a token placeholder' do
+ token_placeholder = find_field('service_token')['placeholder']
+
+ expect(token_placeholder).to eq('XXxxXXxxXXxxXXxxXXxxXXxx')
+ end
+ end
+ end
- value = find_field('request_url').value
- expect(value).to match("api/v3/projects/#{project.id}/services/mattermost_slash_commands/trigger")
+ describe 'stable logo url' do
+ it 'shows a publicly available logo' do
+ expect(File.exist?(Rails.root.join('public/slash-command-logo.png')))
end
end
end
diff --git a/spec/features/projects/services/slack_service_spec.rb b/spec/features/projects/services/slack_service_spec.rb
index 320ed13a01d..16541f51d98 100644
--- a/spec/features/projects/services/slack_service_spec.rb
+++ b/spec/features/projects/services/slack_service_spec.rb
@@ -2,8 +2,8 @@ require 'spec_helper'
feature 'Projects > Slack service > Setup events', feature: true do
let(:user) { create(:user) }
- let(:service) { SlackNotificationService.new }
- let(:project) { create(:project, slack_notification_service: service) }
+ let(:service) { SlackService.new }
+ let(:project) { create(:project, slack_service: service) }
background do
service.fields
diff --git a/spec/features/projects/services/slack_slash_command_spec.rb b/spec/features/projects/services/slack_slash_command_spec.rb
index 32b32f7ae8e..db903a0c8f0 100644
--- a/spec/features/projects/services/slack_slash_command_spec.rb
+++ b/spec/features/projects/services/slack_slash_command_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
feature 'Slack slash commands', feature: true do
- include WaitForAjax
-
given(:user) { create(:user) }
given(:project) { create(:project) }
given(:service) { project.create_slack_slash_commands_service }
@@ -10,19 +8,20 @@ feature 'Slack slash commands', feature: true do
background do
project.team << [user, :master]
login_as(user)
- end
-
- scenario 'user visits the slack slash command config page and shows a help message', js: true do
visit edit_namespace_project_service_path(project.namespace, project, service)
+ end
- wait_for_ajax
+ it 'shows a token placeholder' do
+ token_placeholder = find_field('service_token')['placeholder']
- expect(page).to have_content('This service allows GitLab users to perform common')
+ expect(token_placeholder).to eq('XXxxXXxxXXxxXXxxXXxxXXxx')
end
- scenario 'shows the token after saving' do
- visit edit_namespace_project_service_path(project.namespace, project, service)
+ it 'shows a help message' do
+ expect(page).to have_content('This service allows users to perform common')
+ end
+ it 'shows the token after saving' do
fill_in 'service_token', with: 'token'
click_on 'Save'
@@ -31,9 +30,7 @@ feature 'Slack slash commands', feature: true do
expect(value).to eq('token')
end
- scenario 'shows the correct trigger url' do
- visit edit_namespace_project_service_path(project.namespace, project, service)
-
+ it 'shows the correct trigger url' do
value = find_field('url').value
expect(value).to match("api/v3/projects/#{project.id}/services/slack_slash_commands/trigger")
end
diff --git a/spec/features/projects/settings/merge_requests_settings_spec.rb b/spec/features/projects/settings/merge_requests_settings_spec.rb
index 4bfaa499272..6815039d5ed 100644
--- a/spec/features/projects/settings/merge_requests_settings_spec.rb
+++ b/spec/features/projects/settings/merge_requests_settings_spec.rb
@@ -11,41 +11,36 @@ feature 'Project settings > Merge Requests', feature: true, js: true do
login_as(user)
end
- context 'when Merge Request and Builds are initially enabled' do
- before do
- project.project_feature.update_attribute('merge_requests_access_level', ProjectFeature::ENABLED)
- end
-
- context 'when Builds are initially enabled' do
+ context 'when Merge Request and Pipelines are initially enabled' do
+ context 'when Pipelines are initially enabled' do
before do
- project.project_feature.update_attribute('builds_access_level', ProjectFeature::ENABLED)
visit edit_project_path(project)
end
scenario 'shows the Merge Requests settings' do
- expect(page).to have_content('Only allow merge requests to be merged if the build succeeds')
+ expect(page).to have_content('Only allow merge requests to be merged if the pipeline succeeds')
expect(page).to have_content('Only allow merge requests to be merged if all discussions are resolved')
select 'Disabled', from: "project_project_feature_attributes_merge_requests_access_level"
- expect(page).not_to have_content('Only allow merge requests to be merged if the build succeeds')
+ expect(page).not_to have_content('Only allow merge requests to be merged if the pipeline succeeds')
expect(page).not_to have_content('Only allow merge requests to be merged if all discussions are resolved')
end
end
- context 'when Builds are initially disabled' do
+ context 'when Pipelines are initially disabled' do
before do
project.project_feature.update_attribute('builds_access_level', ProjectFeature::DISABLED)
visit edit_project_path(project)
end
scenario 'shows the Merge Requests settings that do not depend on Builds feature' do
- expect(page).not_to have_content('Only allow merge requests to be merged if the build succeeds')
+ expect(page).not_to have_content('Only allow merge requests to be merged if the pipeline succeeds')
expect(page).to have_content('Only allow merge requests to be merged if all discussions are resolved')
select 'Everyone with access', from: "project_project_feature_attributes_builds_access_level"
- expect(page).to have_content('Only allow merge requests to be merged if the build succeeds')
+ expect(page).to have_content('Only allow merge requests to be merged if the pipeline succeeds')
expect(page).to have_content('Only allow merge requests to be merged if all discussions are resolved')
end
end
@@ -58,12 +53,12 @@ feature 'Project settings > Merge Requests', feature: true, js: true do
end
scenario 'does not show the Merge Requests settings' do
- expect(page).not_to have_content('Only allow merge requests to be merged if the build succeeds')
+ expect(page).not_to have_content('Only allow merge requests to be merged if the pipeline succeeds')
expect(page).not_to have_content('Only allow merge requests to be merged if all discussions are resolved')
select 'Everyone with access', from: "project_project_feature_attributes_merge_requests_access_level"
- expect(page).to have_content('Only allow merge requests to be merged if the build succeeds')
+ expect(page).to have_content('Only allow merge requests to be merged if the pipeline succeeds')
expect(page).to have_content('Only allow merge requests to be merged if all discussions are resolved')
end
end
diff --git a/spec/features/projects/settings/visibility_settings_spec.rb b/spec/features/projects/settings/visibility_settings_spec.rb
new file mode 100644
index 00000000000..cef315ac9cd
--- /dev/null
+++ b/spec/features/projects/settings/visibility_settings_spec.rb
@@ -0,0 +1,47 @@
+require 'spec_helper'
+
+feature 'Visibility settings', feature: true, js: true do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, namespace: user.namespace, visibility_level: 20) }
+
+ context 'as owner' do
+ before do
+ login_as(user)
+ visit edit_namespace_project_path(project.namespace, project)
+ end
+
+ scenario 'project visibility select is available' do
+ visibility_select_container = find('.js-visibility-select')
+
+ expect(visibility_select_container.find('.visibility-select').value).to eq project.visibility_level.to_s
+ expect(visibility_select_container).to have_content 'The project can be cloned without any authentication.'
+ end
+
+ scenario 'project visibility description updates on change' do
+ visibility_select_container = find('.js-visibility-select')
+ visibility_select = visibility_select_container.find('.visibility-select')
+ visibility_select.select('Private')
+
+ expect(visibility_select.value).to eq '0'
+ expect(visibility_select_container).to have_content 'Project access must be granted explicitly to each user.'
+ end
+ end
+
+ context 'as master' do
+ let(:master_user) { create(:user) }
+
+ before do
+ project.team << [master_user, :master]
+ login_as(master_user)
+ visit edit_namespace_project_path(project.namespace, project)
+ end
+
+ scenario 'project visibility is locked' do
+ visibility_select_container = find('.js-visibility-select')
+
+ expect(visibility_select_container).not_to have_select '.visibility-select'
+ expect(visibility_select_container).to have_content 'Public'
+ expect(visibility_select_container).to have_content 'The project can be cloned without any authentication.'
+ end
+ end
+end
diff --git a/spec/features/projects/view_on_env_spec.rb b/spec/features/projects/view_on_env_spec.rb
new file mode 100644
index 00000000000..ce5c5f21167
--- /dev/null
+++ b/spec/features/projects/view_on_env_spec.rb
@@ -0,0 +1,140 @@
+require 'spec_helper'
+
+describe 'View on environment', js: true do
+ include WaitForAjax
+
+ let(:branch_name) { 'feature' }
+ let(:file_path) { 'files/ruby/feature.rb' }
+ let(:project) { create(:project, :repository) }
+ let(:user) { project.creator }
+
+ before do
+ project.add_master(user)
+ end
+
+ context 'when the branch has a route map' do
+ let(:route_map) do
+ <<-MAP.strip_heredoc
+ - source: /files/(.*)\\..*/
+ public: '\\1'
+ MAP
+ end
+
+ before do
+ Files::CreateService.new(
+ project,
+ user,
+ start_branch: branch_name,
+ target_branch: branch_name,
+ commit_message: "Add .gitlab/route-map.yml",
+ file_path: '.gitlab/route-map.yml',
+ file_content: route_map
+ ).execute
+
+ # Update the file so that we still have a commit that will have a file on the environment
+ Files::UpdateService.new(
+ project,
+ user,
+ start_branch: branch_name,
+ target_branch: branch_name,
+ commit_message: "Update feature",
+ file_path: file_path,
+ file_content: "# Noop"
+ ).execute
+ end
+
+ context 'and an active deployment' do
+ let(:sha) { project.commit(branch_name).sha }
+ let(:environment) { create(:environment, project: project, name: 'review/feature', external_url: 'http://feature.review.example.com') }
+ let!(:deployment) { create(:deployment, environment: environment, ref: branch_name, sha: sha) }
+
+ context 'when visiting the diff of a merge request for the branch' do
+ let(:merge_request) { create(:merge_request, :simple, source_project: project, source_branch: branch_name) }
+
+ before do
+ login_as(user)
+
+ visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request)
+
+ wait_for_ajax
+ end
+
+ it 'has a "View on env" button' do
+ within '.diffs' do
+ expect(page).to have_link('View on feature.review.example.com', href: 'http://feature.review.example.com/ruby/feature')
+ end
+ end
+ end
+
+ context 'when visiting a comparison for the branch' do
+ before do
+ login_as(user)
+
+ visit namespace_project_compare_path(project.namespace, project, from: 'master', to: branch_name)
+
+ wait_for_ajax
+ end
+
+ it 'has a "View on env" button' do
+ expect(page).to have_link('View on feature.review.example.com', href: 'http://feature.review.example.com/ruby/feature')
+ end
+ end
+
+ context 'when visiting a comparison for the commit' do
+ before do
+ login_as(user)
+
+ visit namespace_project_compare_path(project.namespace, project, from: 'master', to: sha)
+
+ wait_for_ajax
+ end
+
+ it 'has a "View on env" button' do
+ expect(page).to have_link('View on feature.review.example.com', href: 'http://feature.review.example.com/ruby/feature')
+ end
+ end
+
+ context 'when visiting a blob on the branch' do
+ before do
+ login_as(user)
+
+ visit namespace_project_blob_path(project.namespace, project, File.join(branch_name, file_path))
+
+ wait_for_ajax
+ end
+
+ it 'has a "View on env" button' do
+ expect(page).to have_link('View on feature.review.example.com', href: 'http://feature.review.example.com/ruby/feature')
+ end
+ end
+
+ context 'when visiting a blob on the commit' do
+ before do
+ login_as(user)
+
+ visit namespace_project_blob_path(project.namespace, project, File.join(sha, file_path))
+
+ wait_for_ajax
+ end
+
+ it 'has a "View on env" button' do
+ expect(page).to have_link('View on feature.review.example.com', href: 'http://feature.review.example.com/ruby/feature')
+ end
+ end
+
+ context 'when visiting the commit' do
+ before do
+ login_as(user)
+
+ visit namespace_project_commit_path(project.namespace, project, sha)
+
+ wait_for_ajax
+ end
+
+ it 'has a "View on env" button' do
+ expect(page).to have_link('View on feature.review.example.com', href: 'http://feature.review.example.com/ruby/feature')
+ end
+ end
+ end
+ end
+end
diff --git a/spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb b/spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb
index b4f5f6b3fc5..20219f3cc9a 100644
--- a/spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb
+++ b/spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb
@@ -2,7 +2,6 @@ require 'spec_helper'
describe 'Projects > Wiki > User views wiki in project page', feature: true do
let(:user) { create(:user) }
- let(:project) { create(:empty_project) }
before do
project.team << [user, :master]
@@ -10,12 +9,11 @@ describe 'Projects > Wiki > User views wiki in project page', feature: true do
end
context 'when repository is disabled for project' do
- before do
- project.project_feature.update!(
- repository_access_level: ProjectFeature::DISABLED,
- merge_requests_access_level: ProjectFeature::DISABLED,
- builds_access_level: ProjectFeature::DISABLED
- )
+ let(:project) do
+ create(:empty_project,
+ :repository_disabled,
+ :merge_requests_disabled,
+ :builds_disabled)
end
context 'when wiki homepage contains a link' do
diff --git a/spec/features/search_spec.rb b/spec/features/search_spec.rb
index caecd027aaa..0fe5a897565 100644
--- a/spec/features/search_spec.rb
+++ b/spec/features/search_spec.rb
@@ -169,16 +169,16 @@ describe "Search", feature: true do
find('.dropdown-menu').click_link 'Issues assigned to me'
sleep 2
- expect(page).to have_selector('.issues-holder')
- expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name)
+ expect(page).to have_selector('.filtered-search')
+ expect(find('.filtered-search').value).to eq("assignee:@#{user.username}")
end
it 'takes user to her issues page when issues authored is clicked' do
find('.dropdown-menu').click_link "Issues I've created"
sleep 2
- expect(page).to have_selector('.issues-holder')
- expect(find('.js-author-search .dropdown-toggle-text')).to have_content(user.name)
+ expect(page).to have_selector('.filtered-search')
+ expect(find('.filtered-search').value).to eq("author:@#{user.username}")
end
it 'takes user to her MR page when MR assigned is clicked' do
@@ -211,4 +211,44 @@ describe "Search", feature: true do
end
end
end
+
+ describe 'search for commits' do
+ before do
+ visit search_path(project_id: project.id)
+ end
+
+ it 'redirects to commit page when search by sha and only commit found' do
+ fill_in 'search', with: '6d394385cf567f80a8fd85055db1ab4c5295806f'
+
+ click_button 'Search'
+
+ expect(page).to have_current_path(namespace_project_commit_path(project.namespace, project, '6d394385cf567f80a8fd85055db1ab4c5295806f'))
+ end
+
+ it 'redirects to single commit regardless of query case' do
+ fill_in 'search', with: '6D394385cf'
+
+ click_button 'Search'
+
+ expect(page).to have_current_path(namespace_project_commit_path(project.namespace, project, '6d394385cf567f80a8fd85055db1ab4c5295806f'))
+ end
+
+ it 'holds on /search page when the only commit is found by message' do
+ create_commit('Message referencing another sha: "deadbeef" ', project, user, 'master')
+
+ fill_in 'search', with: 'deadbeef'
+ click_button 'Search'
+
+ expect(page).to have_current_path('/search', only_path: true)
+ end
+
+ it 'shows multiple matching commits' do
+ fill_in 'search', with: 'See merge request'
+
+ click_button 'Search'
+ click_link 'Commits'
+
+ expect(page).to have_selector('.commit-row-description', count: 9)
+ end
+ end
end
diff --git a/spec/features/security/project/internal_access_spec.rb b/spec/features/security/project/internal_access_spec.rb
index 1897c8119d2..24af062d763 100644
--- a/spec/features/security/project/internal_access_spec.rb
+++ b/spec/features/security/project/internal_access_spec.rb
@@ -82,8 +82,8 @@ describe "Internal Project Access", feature: true do
it { is_expected.to be_denied_for(:visitor) }
end
- describe "GET /:project_path/project_members" do
- subject { namespace_project_project_members_path(project.namespace, project) }
+ describe "GET /:project_path/settings/members" do
+ subject { namespace_project_settings_members_path(project.namespace, project) }
it { is_expected.to be_allowed_for(:admin) }
it { is_expected.to be_allowed_for(:owner).of(project) }
@@ -96,6 +96,20 @@ describe "Internal Project Access", feature: true do
it { is_expected.to be_denied_for(:external) }
end
+ describe "GET /:project_path/settings/ci_cd" do
+ subject { namespace_project_settings_ci_cd_path(project.namespace, project) }
+
+ it { is_expected.to be_allowed_for(:admin) }
+ it { is_expected.to be_allowed_for(:owner).of(project) }
+ it { is_expected.to be_allowed_for(:master).of(project) }
+ it { is_expected.to be_denied_for(:developer).of(project) }
+ it { is_expected.to be_denied_for(:reporter).of(project) }
+ it { is_expected.to be_denied_for(:guest).of(project) }
+ it { is_expected.to be_denied_for(:user) }
+ it { is_expected.to be_denied_for(:visitor) }
+ it { is_expected.to be_denied_for(:external) }
+ end
+
describe "GET /:project_path/blob" do
let(:commit) { project.repository.commit }
subject { namespace_project_blob_path(project.namespace, project, File.join(commit.id, '.gitignore')) }
@@ -262,8 +276,8 @@ describe "Internal Project Access", feature: true do
it { is_expected.to be_denied_for(:visitor) }
end
- describe "GET /:project_path/hooks" do
- subject { namespace_project_hooks_path(project.namespace, project) }
+ describe "GET /:project_path/settings/integrations" do
+ subject { namespace_project_settings_integrations_path(project.namespace, project) }
it { is_expected.to be_allowed_for(:admin) }
it { is_expected.to be_allowed_for(:owner).of(project) }
diff --git a/spec/features/security/project/private_access_spec.rb b/spec/features/security/project/private_access_spec.rb
index f52e23f9433..c511dcfa18e 100644
--- a/spec/features/security/project/private_access_spec.rb
+++ b/spec/features/security/project/private_access_spec.rb
@@ -82,8 +82,8 @@ describe "Private Project Access", feature: true do
it { is_expected.to be_denied_for(:visitor) }
end
- describe "GET /:project_path/project_members" do
- subject { namespace_project_project_members_path(project.namespace, project) }
+ describe "GET /:project_path/settings/members" do
+ subject { namespace_project_settings_members_path(project.namespace, project) }
it { is_expected.to be_allowed_for(:admin) }
it { is_expected.to be_allowed_for(:owner).of(project) }
@@ -92,8 +92,22 @@ describe "Private Project Access", feature: true do
it { is_expected.to be_allowed_for(:reporter).of(project) }
it { is_expected.to be_allowed_for(:guest).of(project) }
it { is_expected.to be_denied_for(:user) }
+ it { is_expected.to be_denied_for(:visitor) }
it { is_expected.to be_denied_for(:external) }
+ end
+
+ describe "GET /:project_path/settings/ci_cd" do
+ subject { namespace_project_settings_ci_cd_path(project.namespace, project) }
+
+ it { is_expected.to be_allowed_for(:admin) }
+ it { is_expected.to be_allowed_for(:owner).of(project) }
+ it { is_expected.to be_allowed_for(:master).of(project) }
+ it { is_expected.to be_denied_for(:developer).of(project) }
+ it { is_expected.to be_denied_for(:reporter).of(project) }
+ it { is_expected.to be_denied_for(:guest).of(project) }
+ it { is_expected.to be_denied_for(:user) }
it { is_expected.to be_denied_for(:visitor) }
+ it { is_expected.to be_denied_for(:external) }
end
describe "GET /:project_path/blob" do
@@ -234,8 +248,8 @@ describe "Private Project Access", feature: true do
it { is_expected.to be_denied_for(:visitor) }
end
- describe "GET /:project_path/hooks" do
- subject { namespace_project_hooks_path(project.namespace, project) }
+ describe "GET /:project_path/namespace/hooks" do
+ subject { namespace_project_settings_integrations_path(project.namespace, project) }
it { is_expected.to be_allowed_for(:admin) }
it { is_expected.to be_allowed_for(:owner).of(project) }
diff --git a/spec/features/security/project/public_access_spec.rb b/spec/features/security/project/public_access_spec.rb
index bed9e92fcb6..d8cc012c27e 100644
--- a/spec/features/security/project/public_access_spec.rb
+++ b/spec/features/security/project/public_access_spec.rb
@@ -82,8 +82,8 @@ describe "Public Project Access", feature: true do
it { is_expected.to be_allowed_for(:visitor) }
end
- describe "GET /:project_path/project_members" do
- subject { namespace_project_project_members_path(project.namespace, project) }
+ describe "GET /:project_path/settings/members" do
+ subject { namespace_project_settings_members_path(project.namespace, project) }
it { is_expected.to be_allowed_for(:admin) }
it { is_expected.to be_allowed_for(:owner).of(project) }
@@ -96,6 +96,20 @@ describe "Public Project Access", feature: true do
it { is_expected.to be_allowed_for(:external) }
end
+ describe "GET /:project_path/settings/ci_cd" do
+ subject { namespace_project_settings_ci_cd_path(project.namespace, project) }
+
+ it { is_expected.to be_allowed_for(:admin) }
+ it { is_expected.to be_allowed_for(:owner).of(project) }
+ it { is_expected.to be_allowed_for(:master).of(project) }
+ it { is_expected.to be_denied_for(:developer).of(project) }
+ it { is_expected.to be_denied_for(:reporter).of(project) }
+ it { is_expected.to be_denied_for(:guest).of(project) }
+ it { is_expected.to be_denied_for(:user) }
+ it { is_expected.to be_denied_for(:visitor) }
+ it { is_expected.to be_denied_for(:external) }
+ end
+
describe "GET /:project_path/pipelines" do
subject { namespace_project_pipelines_path(project.namespace, project) }
@@ -400,8 +414,8 @@ describe "Public Project Access", feature: true do
it { is_expected.to be_allowed_for(:visitor) }
end
- describe "GET /:project_path/hooks" do
- subject { namespace_project_hooks_path(project.namespace, project) }
+ describe "GET /:project_path/settings/integrations" do
+ subject { namespace_project_settings_integrations_path(project.namespace, project) }
it { is_expected.to be_allowed_for(:admin) }
it { is_expected.to be_allowed_for(:owner).of(project) }
diff --git a/spec/features/signup_spec.rb b/spec/features/signup_spec.rb
index 65544f79eba..9fde8d6e5cf 100644
--- a/spec/features/signup_spec.rb
+++ b/spec/features/signup_spec.rb
@@ -10,10 +10,11 @@ feature 'Signup', feature: true do
visit root_path
- fill_in 'new_user_name', with: user.name
- fill_in 'new_user_username', with: user.username
- fill_in 'new_user_email', with: user.email
- fill_in 'new_user_password', with: user.password
+ fill_in 'new_user_name', with: user.name
+ fill_in 'new_user_username', with: user.username
+ fill_in 'new_user_email', with: user.email
+ fill_in 'new_user_email_confirmation', with: user.email
+ fill_in 'new_user_password', with: user.password
click_button "Register"
expect(current_path).to eq users_almost_there_path
@@ -29,10 +30,11 @@ feature 'Signup', feature: true do
visit root_path
- fill_in 'new_user_name', with: user.name
- fill_in 'new_user_username', with: user.username
- fill_in 'new_user_email', with: user.email
- fill_in 'new_user_password', with: user.password
+ fill_in 'new_user_name', with: user.name
+ fill_in 'new_user_username', with: user.username
+ fill_in 'new_user_email', with: user.email
+ fill_in 'new_user_email_confirmation', with: user.email
+ fill_in 'new_user_password', with: user.password
click_button "Register"
expect(current_path).to eq dashboard_projects_path
@@ -55,8 +57,9 @@ feature 'Signup', feature: true do
click_button "Register"
expect(current_path).to eq user_registration_path
- expect(page).to have_content("error prohibited this user from being saved")
+ expect(page).to have_content("errors prohibited this user from being saved")
expect(page).to have_content("Email has already been taken")
+ expect(page).to have_content("Email confirmation doesn't match")
end
it 'does not redisplay the password' do
diff --git a/spec/features/snippets/create_snippet_spec.rb b/spec/features/snippets/create_snippet_spec.rb
index cb95e7828db..5470276bf06 100644
--- a/spec/features/snippets/create_snippet_spec.rb
+++ b/spec/features/snippets/create_snippet_spec.rb
@@ -17,4 +17,18 @@ feature 'Create Snippet', feature: true do
expect(page).to have_content('My Snippet Title')
expect(page).to have_content('Hello World!')
end
+
+ scenario 'Authenticated user creates a snippet with + in filename' do
+ fill_in 'personal_snippet_title', with: 'My Snippet Title'
+ page.within('.file-editor') do
+ find(:xpath, "//input[@id='personal_snippet_file_name']").set 'snippet+file+name'
+ find(:xpath, "//input[@id='personal_snippet_content']").set 'Hello World!'
+ end
+
+ click_button 'Create snippet'
+
+ expect(page).to have_content('My Snippet Title')
+ expect(page).to have_content('snippet+file+name')
+ expect(page).to have_content('Hello World!')
+ end
end
diff --git a/spec/features/tags/master_creates_tag_spec.rb b/spec/features/tags/master_creates_tag_spec.rb
index 08a97085a9c..ca25c696f75 100644
--- a/spec/features/tags/master_creates_tag_spec.rb
+++ b/spec/features/tags/master_creates_tag_spec.rb
@@ -34,7 +34,7 @@ feature 'Master creates tag', feature: true do
expect(current_path).to eq(
namespace_project_tag_path(project.namespace, project, 'v3.0'))
expect(page).to have_content 'v3.0'
- page.within 'pre.body' do
+ page.within 'pre.wrap' do
expect(page).to have_content "Awesome tag message\n\n- hello\n- world"
end
end
diff --git a/spec/features/task_lists_spec.rb b/spec/features/task_lists_spec.rb
index abb27c90e0a..a5d14aa19f1 100644
--- a/spec/features/task_lists_spec.rb
+++ b/spec/features/task_lists_spec.rb
@@ -36,6 +36,19 @@ feature 'Task Lists', feature: true do
MARKDOWN
end
+ let(:nested_tasks_markdown) do
+ <<-EOT.strip_heredoc
+ - [ ] Task a
+ - [x] Task a.1
+ - [ ] Task a.2
+ - [ ] Task b
+
+ 1. [ ] Task 1
+ 1. [ ] Task 1.1
+ 1. [x] Task 1.2
+ EOT
+ end
+
before do
Warden.test_mode!
@@ -123,6 +136,35 @@ feature 'Task Lists', feature: true do
expect(page).to have_content("1 of 1 task completed")
end
end
+
+ describe 'nested tasks', js: true do
+ let(:issue) { create(:issue, description: nested_tasks_markdown, author: user, project: project) }
+
+ before { visit_issue(project, issue) }
+
+ it 'renders' do
+ expect(page).to have_selector('ul.task-list', count: 2)
+ expect(page).to have_selector('li.task-list-item', count: 7)
+ expect(page).to have_selector('ul input[checked]', count: 1)
+ expect(page).to have_selector('ol input[checked]', count: 1)
+ end
+
+ it 'solves tasks' do
+ expect(page).to have_content("2 of 7 tasks completed")
+
+ page.find('li.task-list-item', text: 'Task b').find('input').click
+ page.find('li.task-list-item ul li.task-list-item', text: 'Task a.2').find('input').click
+ page.find('li.task-list-item ol li.task-list-item', text: 'Task 1.1').find('input').click
+
+ expect(page).to have_content("5 of 7 tasks completed")
+
+ visit_issue(project, issue) # reload to see new system notes
+
+ expect(page).to have_content('marked the task Task b as complete')
+ expect(page).to have_content('marked the task Task a.2 as complete')
+ expect(page).to have_content('marked the task Task 1.1 as complete')
+ end
+ end
end
describe 'for Notes' do
@@ -236,7 +278,7 @@ feature 'Task Lists', feature: true do
expect(page).to have_content("2 of 6 tasks completed")
end
end
-
+
describe 'single incomplete task' do
let!(:merge) { create(:merge_request, :simple, description: singleIncompleteMarkdown, author: user, source_project: project) }
diff --git a/spec/features/todos/todos_spec.rb b/spec/features/todos/todos_spec.rb
index 88eabea7e3a..1b352be9331 100644
--- a/spec/features/todos/todos_spec.rb
+++ b/spec/features/todos/todos_spec.rb
@@ -155,5 +155,24 @@ describe 'Dashboard Todos', feature: true do
expect(page).to have_selector('.todos-all-done', count: 1)
end
end
+
+ context 'User has a Build Failed todo' do
+ let!(:todo) { create(:todo, :build_failed, user: user, project: project, author: author) }
+
+ before do
+ login_as user
+ visit dashboard_todos_path
+ end
+
+ it 'shows the todo' do
+ expect(page).to have_content 'The build failed for merge request'
+ end
+
+ it 'links to the pipelines for the merge request' do
+ href = pipelines_namespace_project_merge_request_path(project.namespace, project, todo.target)
+
+ expect(page).to have_link "merge request #{todo.target.to_reference(full: true)}", href
+ end
+ end
end
end
diff --git a/spec/features/triggers_spec.rb b/spec/features/triggers_spec.rb
index 72354834c5a..4a7511589d6 100644
--- a/spec/features/triggers_spec.rb
+++ b/spec/features/triggers_spec.rb
@@ -7,7 +7,7 @@ describe 'Triggers' do
before do
@project = FactoryGirl.create :empty_project
@project.team << [user, :master]
- visit namespace_project_triggers_path(@project.namespace, @project)
+ visit namespace_project_settings_ci_cd_path(@project.namespace, @project)
end
context 'create a trigger' do
diff --git a/spec/features/u2f_spec.rb b/spec/features/u2f_spec.rb
index be21b403084..a8d00bb8e5a 100644
--- a/spec/features/u2f_spec.rb
+++ b/spec/features/u2f_spec.rb
@@ -45,12 +45,12 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
it 'allows registering a new device with a name' do
visit profile_account_path
manage_two_factor_authentication
- expect(page.body).to match("You've already enabled two-factor authentication using mobile")
+ expect(page).to have_content("You've already enabled two-factor authentication using mobile")
u2f_device = register_u2f_device
- expect(page.body).to match(u2f_device.name)
- expect(page.body).to match('Your U2F device was registered')
+ expect(page).to have_content(u2f_device.name)
+ expect(page).to have_content('Your U2F device was registered')
end
it 'allows registering more than one device' do
@@ -59,30 +59,30 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
# First device
manage_two_factor_authentication
first_device = register_u2f_device
- expect(page.body).to match('Your U2F device was registered')
+ expect(page).to have_content('Your U2F device was registered')
# Second device
second_device = register_u2f_device
- expect(page.body).to match('Your U2F device was registered')
+ expect(page).to have_content('Your U2F device was registered')
- expect(page.body).to match(first_device.name)
- expect(page.body).to match(second_device.name)
+ expect(page).to have_content(first_device.name)
+ expect(page).to have_content(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")
+ expect(page).to have_content("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).to have_content('Successfully deleted')
expect(page.body).not_to match(first_u2f_device.name)
- expect(page.body).to match(second_u2f_device.name)
+ expect(page).to have_content(second_u2f_device.name)
end
end
@@ -91,7 +91,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
visit profile_account_path
manage_two_factor_authentication
u2f_device = register_u2f_device
- expect(page.body).to match('Your U2F device was registered')
+ expect(page).to have_content('Your U2F device was registered')
logout
# Second user
@@ -100,7 +100,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
visit profile_account_path
manage_two_factor_authentication
register_u2f_device(u2f_device)
- expect(page.body).to match('Your U2F device was registered')
+ expect(page).to have_content('Your U2F device was registered')
expect(U2fRegistration.count).to eq(2)
end
@@ -117,8 +117,8 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
click_on 'Register U2F Device'
expect(U2fRegistration.count).to eq(0)
- expect(page.body).to match("The form contains the following error")
- expect(page.body).to match("did not send a valid JSON response")
+ expect(page).to have_content("The form contains the following error")
+ expect(page).to have_content("did not send a valid JSON response")
end
it "allows retrying registration" do
@@ -130,12 +130,12 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
click_on 'Setup New U2F Device'
expect(page).to have_content('Your device was successfully set up')
click_on 'Register U2F Device'
- expect(page.body).to match("The form contains the following error")
+ expect(page).to have_content("The form contains the following error")
# Successful registration
register_u2f_device
- expect(page.body).to match('Your U2F device was registered')
+ expect(page).to have_content('Your U2F device was registered')
expect(U2fRegistration.count).to eq(1)
end
end
@@ -160,10 +160,9 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
login_with(user)
@u2f_device.respond_to_u2f_authentication
- click_on "Sign in via U2F device"
- expect(page.body).to match('We heard back from your U2F device')
- click_on "Authenticate via U2F Device"
- expect(page.body).to match('href="/users/sign_out"')
+
+ expect(page).to have_content('We heard back from your U2F device')
+ expect(page).to have_css('.sign-out-link', visible: false)
end
end
@@ -173,11 +172,9 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
login_with(user)
@u2f_device.respond_to_u2f_authentication
- click_on "Sign in via U2F device"
- expect(page.body).to match('We heard back from your U2F device')
- click_on "Authenticate via U2F Device"
- expect(page.body).to match('href="/users/sign_out"')
+ expect(page).to have_content('We heard back from your U2F device')
+ expect(page).to have_css('.sign-out-link', visible: false)
end
end
@@ -185,8 +182,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
login_with(user, remember: true)
@u2f_device.respond_to_u2f_authentication
- click_on "Sign in via U2F device"
- expect(page.body).to match('We heard back from your U2F device')
+ expect(page).to have_content('We heard back from your U2F device')
within 'div#js-authenticate-u2f' do
field = first('input#user_remember_me', visible: false)
@@ -208,11 +204,8 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
# Try authenticating user with the old U2F device
login_as(current_user)
@u2f_device.respond_to_u2f_authentication
- click_on "Sign in via U2F device"
- expect(page.body).to match('We heard back from your U2F device')
- click_on "Authenticate via U2F Device"
-
- expect(page.body).to match('Authentication via U2F device failed')
+ expect(page).to have_content('We heard back from your U2F device')
+ expect(page).to have_content('Authentication via U2F device failed')
end
end
@@ -229,11 +222,9 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
# Try authenticating user with the same U2F device
login_as(current_user)
@u2f_device.respond_to_u2f_authentication
- click_on "Sign in via U2F device"
- expect(page.body).to match('We heard back from your U2F device')
- click_on "Authenticate via U2F Device"
+ expect(page).to have_content('We heard back from your U2F device')
- expect(page.body).to match('href="/users/sign_out"')
+ expect(page).to have_css('.sign-out-link', visible: false)
end
end
end
@@ -243,11 +234,9 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
unregistered_device = FakeU2fDevice.new(page, FFaker::Name.first_name)
login_as(user)
unregistered_device.respond_to_u2f_authentication
- click_on "Sign in via U2F device"
- expect(page.body).to match('We heard back from your U2F device')
- click_on "Authenticate via U2F Device"
+ expect(page).to have_content('We heard back from your U2F device')
- expect(page.body).to match('Authentication via U2F device failed')
+ expect(page).to have_content('Authentication via U2F device failed')
end
end
@@ -270,11 +259,9 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
[first_device, second_device].each do |device|
login_as(user)
device.respond_to_u2f_authentication
- click_on "Sign in via U2F device"
- expect(page.body).to match('We heard back from your U2F device')
- click_on "Authenticate via U2F Device"
+ expect(page).to have_content('We heard back from your U2F device')
- expect(page.body).to match('href="/users/sign_out"')
+ expect(page).to have_css('.sign-out-link', visible: false)
logout
end
@@ -299,4 +286,50 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
end
end
end
+
+ describe 'fallback code authentication' do
+ let(:user) { create(:user) }
+
+ def assert_fallback_ui(page)
+ expect(page).to have_button('Verify code')
+ expect(page).to have_css('#user_otp_attempt')
+ expect(page).not_to have_link('Sign in via 2FA code')
+ expect(page).not_to have_css('#js-authenticate-u2f')
+ end
+
+ before do
+ # Register and logout
+ login_as(user)
+ user.update_attribute(:otp_required_for_login, true)
+ visit profile_account_path
+ end
+
+ describe 'when no u2f device is registered' do
+ before do
+ logout
+ login_with(user)
+ end
+
+ it 'shows the fallback otp code UI' do
+ assert_fallback_ui(page)
+ end
+ end
+
+ describe 'when a u2f device is registered' do
+ before do
+ manage_two_factor_authentication
+ @u2f_device = register_u2f_device
+ logout
+ login_with(user)
+ end
+
+ it 'provides a button that shows the fallback otp code UI' do
+ expect(page).to have_link('Sign in via 2FA code')
+
+ click_link('Sign in via 2FA code')
+
+ assert_fallback_ui(page)
+ end
+ end
+ end
end
diff --git a/spec/features/users_spec.rb b/spec/features/users_spec.rb
index afa98f3f715..2de0fbe7ab2 100644
--- a/spec/features/users_spec.rb
+++ b/spec/features/users_spec.rb
@@ -6,10 +6,11 @@ feature 'Users', feature: true, js: true do
scenario 'GET /users/sign_in creates a new user account' do
visit new_user_session_path
click_link 'Register'
- fill_in 'new_user_name', with: 'Name Surname'
- fill_in 'new_user_username', with: 'Great'
- fill_in 'new_user_email', with: 'name@mail.com'
- fill_in 'new_user_password', with: 'password1234'
+ fill_in 'new_user_name', with: 'Name Surname'
+ fill_in 'new_user_username', with: 'Great'
+ fill_in 'new_user_email', with: 'name@mail.com'
+ fill_in 'new_user_email_confirmation', with: 'name@mail.com'
+ fill_in 'new_user_password', with: 'password1234'
expect { click_button 'Register' }.to change { User.count }.by(1)
end
@@ -33,10 +34,11 @@ feature 'Users', feature: true, js: true do
scenario 'Should show one error if email is already taken' do
visit new_user_session_path
click_link 'Register'
- fill_in 'new_user_name', with: 'Another user name'
- fill_in 'new_user_username', with: 'anotheruser'
- fill_in 'new_user_email', with: user.email
- fill_in 'new_user_password', with: '12341234'
+ fill_in 'new_user_name', with: 'Another user name'
+ fill_in 'new_user_username', with: 'anotheruser'
+ fill_in 'new_user_email', with: user.email
+ fill_in 'new_user_email_confirmation', with: user.email
+ fill_in 'new_user_password', with: '12341234'
expect { click_button 'Register' }.to change { User.count }.by(0)
expect(page).to have_text('Email has already been taken')
expect(number_of_errors_on_page(page)).to be(1), 'errors on page:\n #{errors_on_page page}'
diff --git a/spec/features/variables_spec.rb b/spec/features/variables_spec.rb
index ff30ffd7820..9a4bc027004 100644
--- a/spec/features/variables_spec.rb
+++ b/spec/features/variables_spec.rb
@@ -10,7 +10,7 @@ describe 'Project variables', js: true do
project.team << [user, :master]
project.variables << variable
- visit namespace_project_variables_path(project.namespace, project)
+ visit namespace_project_settings_ci_cd_path(project.namespace, project)
end
it 'shows list of variables' do
diff --git a/spec/finders/branches_finder_spec.rb b/spec/finders/branches_finder_spec.rb
index db60c01db0d..91f34973ba5 100644
--- a/spec/finders/branches_finder_spec.rb
+++ b/spec/finders/branches_finder_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe BranchesFinder do
let(:user) { create(:user) }
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:repository) { project.repository }
describe '#execute' do
diff --git a/spec/finders/contributed_projects_finder_spec.rb b/spec/finders/contributed_projects_finder_spec.rb
index 65d7f14c721..34f665826b6 100644
--- a/spec/finders/contributed_projects_finder_spec.rb
+++ b/spec/finders/contributed_projects_finder_spec.rb
@@ -6,19 +6,16 @@ describe ContributedProjectsFinder do
let(:finder) { described_class.new(source_user) }
- let!(:public_project) { create(:project, :public) }
- let!(:private_project) { create(:project, :private) }
+ let!(:public_project) { create(:empty_project, :public) }
+ let!(:private_project) { create(:empty_project, :private) }
before do
- private_project.team << [source_user, Gitlab::Access::MASTER]
- private_project.team << [current_user, Gitlab::Access::DEVELOPER]
- public_project.team << [source_user, Gitlab::Access::MASTER]
+ private_project.add_master(source_user)
+ private_project.add_developer(current_user)
+ public_project.add_master(source_user)
- create(:event, action: Event::PUSHED, project: public_project,
- target: public_project, author: source_user)
-
- create(:event, action: Event::PUSHED, project: private_project,
- target: private_project, author: source_user)
+ create(:event, :pushed, project: public_project, target: public_project, author: source_user)
+ create(:event, :pushed, project: private_project, target: private_project, author: source_user)
end
describe 'without a current user' do
diff --git a/spec/finders/environments_finder_spec.rb b/spec/finders/environments_finder_spec.rb
new file mode 100644
index 00000000000..0c063f6d5ee
--- /dev/null
+++ b/spec/finders/environments_finder_spec.rb
@@ -0,0 +1,110 @@
+require 'spec_helper'
+
+describe EnvironmentsFinder do
+ describe '#execute' do
+ let(:project) { create(:project, :repository) }
+ let(:user) { project.creator }
+ let(:environment) { create(:environment, project: project) }
+
+ before do
+ project.add_master(user)
+ end
+
+ context 'tagged deployment' do
+ before do
+ create(:deployment, environment: environment, ref: '1.0', tag: true, sha: project.commit.id)
+ end
+
+ it 'returns environment when with_tags is set' do
+ expect(described_class.new(project, user, ref: 'master', commit: project.commit, with_tags: true).execute)
+ .to contain_exactly(environment)
+ end
+
+ it 'does not return environment when no with_tags is set' do
+ expect(described_class.new(project, user, ref: 'master', commit: project.commit).execute)
+ .to be_empty
+ end
+
+ it 'does not return environment when commit is not part of deployment' do
+ expect(described_class.new(project, user, ref: 'master', commit: project.commit('feature')).execute)
+ .to be_empty
+ end
+ end
+
+ context 'branch deployment' do
+ before do
+ create(:deployment, environment: environment, ref: 'master', sha: project.commit.id)
+ end
+
+ it 'returns environment when ref is set' do
+ expect(described_class.new(project, user, ref: 'master', commit: project.commit).execute)
+ .to contain_exactly(environment)
+ end
+
+ it 'does not environment when ref is different' do
+ expect(described_class.new(project, user, ref: 'feature', commit: project.commit).execute)
+ .to be_empty
+ end
+
+ it 'does not return environment when commit is not part of deployment' do
+ expect(described_class.new(project, user, ref: 'master', commit: project.commit('feature')).execute)
+ .to be_empty
+ end
+
+ it 'returns environment when commit constraint is not set' do
+ expect(described_class.new(project, user, ref: 'master').execute)
+ .to contain_exactly(environment)
+ end
+ end
+
+ context 'commit deployment' do
+ before do
+ create(:deployment, environment: environment, ref: 'master', sha: project.commit.id)
+ end
+
+ it 'returns environment' do
+ expect(described_class.new(project, user, commit: project.commit).execute)
+ .to contain_exactly(environment)
+ end
+ end
+
+ context 'recently updated' do
+ context 'when last deployment to environment is the most recent one' do
+ before do
+ create(:deployment, environment: environment, ref: 'feature')
+ end
+
+ it 'finds recently updated environment' do
+ expect(described_class.new(project, user, ref: 'feature', recently_updated: true).execute)
+ .to contain_exactly(environment)
+ end
+ end
+
+ context 'when last deployment to environment is not the most recent' do
+ before do
+ create(:deployment, environment: environment, ref: 'feature')
+ create(:deployment, environment: environment, ref: 'master')
+ end
+
+ it 'does not find environment' do
+ expect(described_class.new(project, user, ref: 'feature', recently_updated: true).execute)
+ .to be_empty
+ end
+ end
+
+ context 'when there are two environments that deploy to the same branch' do
+ let(:second_environment) { create(:environment, project: project) }
+
+ before do
+ create(:deployment, environment: environment, ref: 'feature')
+ create(:deployment, environment: second_environment, ref: 'feature')
+ end
+
+ it 'finds both environments' do
+ expect(described_class.new(project, user, ref: 'feature', recently_updated: true).execute)
+ .to contain_exactly(environment, second_environment)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/finders/group_projects_finder_spec.rb b/spec/finders/group_projects_finder_spec.rb
index 00eec3f3f4c..ef97b061ca7 100644
--- a/spec/finders/group_projects_finder_spec.rb
+++ b/spec/finders/group_projects_finder_spec.rb
@@ -6,11 +6,11 @@ describe GroupProjectsFinder do
let(:finder) { described_class.new(source_user) }
- let!(:public_project) { create(:project, :public, group: group, path: '1') }
- let!(:private_project) { create(:project, :private, group: group, path: '2') }
- let!(:shared_project_1) { create(:project, :public, path: '3') }
- let!(:shared_project_2) { create(:project, :private, path: '4') }
- let!(:shared_project_3) { create(:project, :internal, path: '5') }
+ let!(:public_project) { create(:empty_project, :public, group: group, path: '1') }
+ let!(:private_project) { create(:empty_project, :private, group: group, path: '2') }
+ let!(:shared_project_1) { create(:empty_project, :public, path: '3') }
+ let!(:shared_project_2) { create(:empty_project, :private, path: '4') }
+ let!(:shared_project_3) { create(:empty_project, :internal, path: '5') }
before do
shared_project_1.project_group_links.create(group_access: Gitlab::Access::MASTER, group: group)
diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb
index 97737d7ddc7..12ab1d6dde8 100644
--- a/spec/finders/issues_finder_spec.rb
+++ b/spec/finders/issues_finder_spec.rb
@@ -224,7 +224,7 @@ describe IssuesFinder do
let(:scope) { nil }
it "doesn't return team-only issues to non team members" do
- project = create(:empty_project, :public, issues_access_level: ProjectFeature::PRIVATE)
+ project = create(:empty_project, :public, :issues_private)
issue = create(:issue, project: project)
expect(issues).not_to include(issue)
diff --git a/spec/finders/joined_groups_finder_spec.rb b/spec/finders/joined_groups_finder_spec.rb
index 29a47e005a6..4c389746252 100644
--- a/spec/finders/joined_groups_finder_spec.rb
+++ b/spec/finders/joined_groups_finder_spec.rb
@@ -42,7 +42,7 @@ describe JoinedGroupsFinder do
context 'if profile visitor is in one of the private group projects' do
before do
- project = create(:project, :private, group: private_group, name: 'B', path: 'B')
+ project = create(:empty_project, :private, group: private_group, name: 'B', path: 'B')
project.add_user(profile_visitor, Gitlab::Access::DEVELOPER)
end
diff --git a/spec/finders/merge_requests_finder_spec.rb b/spec/finders/merge_requests_finder_spec.rb
index 88361e27102..3dcd7781e5b 100644
--- a/spec/finders/merge_requests_finder_spec.rb
+++ b/spec/finders/merge_requests_finder_spec.rb
@@ -4,9 +4,9 @@ describe MergeRequestsFinder do
let(:user) { create :user }
let(:user2) { create :user }
- let(:project1) { create(:project) }
- let(:project2) { create(:project, forked_from_project: project1) }
- let(:project3) { create(:project, forked_from_project: project1, archived: true) }
+ let(:project1) { create(:empty_project) }
+ let(:project2) { create(:empty_project, forked_from_project: project1) }
+ let(:project3) { create(:empty_project, :archived, forked_from_project: project1) }
let!(:merge_request1) { create(:merge_request, :simple, author: user, source_project: project2, target_project: project1) }
let!(:merge_request2) { create(:merge_request, :simple, author: user, source_project: project2, target_project: project1, state: 'closed') }
diff --git a/spec/finders/move_to_project_finder_spec.rb b/spec/finders/move_to_project_finder_spec.rb
index fdce4e714ff..dea87980e25 100644
--- a/spec/finders/move_to_project_finder_spec.rb
+++ b/spec/finders/move_to_project_finder_spec.rb
@@ -2,13 +2,13 @@ require 'spec_helper'
describe MoveToProjectFinder do
let(:user) { create(:user) }
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project) }
- let(:no_access_project) { create(:project) }
- let(:guest_project) { create(:project) }
- let(:reporter_project) { create(:project) }
- let(:developer_project) { create(:project) }
- let(:master_project) { create(:project) }
+ let(:no_access_project) { create(:empty_project) }
+ let(:guest_project) { create(:empty_project) }
+ let(:reporter_project) { create(:empty_project) }
+ let(:developer_project) { create(:empty_project) }
+ let(:master_project) { create(:empty_project) }
subject { described_class.new(user) }
@@ -36,8 +36,8 @@ describe MoveToProjectFinder do
it 'does not return archived projects' do
reporter_project.team << [user, :reporter]
- reporter_project.update_attributes(archived: true)
- other_reporter_project = create(:project)
+ reporter_project.archive!
+ other_reporter_project = create(:empty_project)
other_reporter_project.team << [user, :reporter]
expect(subject.execute(project).to_a).to eq([other_reporter_project])
@@ -46,7 +46,7 @@ describe MoveToProjectFinder do
it 'does not return projects for which issues are disabled' do
reporter_project.team << [user, :reporter]
reporter_project.update_attributes(issues_enabled: false)
- other_reporter_project = create(:project)
+ other_reporter_project = create(:empty_project)
other_reporter_project.team << [user, :reporter]
expect(subject.execute(project).to_a).to eq([other_reporter_project])
@@ -83,10 +83,10 @@ describe MoveToProjectFinder do
end
it 'returns projects matching a search query' do
- foo_project = create(:project)
+ foo_project = create(:empty_project)
foo_project.team << [user, :master]
- wadus_project = create(:project, name: 'wadus')
+ wadus_project = create(:empty_project, name: 'wadus')
wadus_project.team << [user, :master]
expect(subject.execute(project).to_a).to eq([wadus_project, foo_project])
diff --git a/spec/finders/notes_finder_spec.rb b/spec/finders/notes_finder_spec.rb
index 4d21254323c..f8b05d4e9bc 100644
--- a/spec/finders/notes_finder_spec.rb
+++ b/spec/finders/notes_finder_spec.rb
@@ -9,8 +9,6 @@ describe NotesFinder do
end
describe '#execute' do
- it 'finds notes on snippets when project is public and user isnt a member'
-
it 'finds notes on merge requests' do
create(:note_on_merge_request, project: project)
@@ -28,7 +26,7 @@ describe NotesFinder do
end
it "excludes notes on commits the author can't download" do
- project = create(:project, :private)
+ project = create(:project, :private, :repository)
note = create(:note_on_commit, project: project)
params = { target_type: 'commit', target_id: note.noteable.id }
@@ -45,9 +43,11 @@ describe NotesFinder do
context 'on restricted projects' do
let(:project) do
- create(:empty_project, :public, issues_access_level: ProjectFeature::PRIVATE,
- snippets_access_level: ProjectFeature::PRIVATE,
- merge_requests_access_level: ProjectFeature::PRIVATE)
+ create(:empty_project,
+ :public,
+ :issues_private,
+ :snippets_private,
+ :merge_requests_private)
end
it 'publicly excludes notes on merge requests' do
@@ -76,7 +76,7 @@ describe NotesFinder do
end
context 'for target' do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:note1) { create :note_on_commit, project: project }
let(:note2) { create :note_on_commit, project: project }
let(:commit) { note1.noteable }
diff --git a/spec/finders/personal_projects_finder_spec.rb b/spec/finders/personal_projects_finder_spec.rb
index a4681fe59d8..e0e17af681a 100644
--- a/spec/finders/personal_projects_finder_spec.rb
+++ b/spec/finders/personal_projects_finder_spec.rb
@@ -4,14 +4,14 @@ describe PersonalProjectsFinder do
let(:source_user) { create(:user) }
let(:current_user) { create(:user) }
let(:finder) { described_class.new(source_user) }
- let!(:public_project) { create(:project, :public, namespace: source_user.namespace) }
+ let!(:public_project) { create(:empty_project, :public, namespace: source_user.namespace) }
let!(:private_project) do
- create(:project, :private, namespace: source_user.namespace, path: 'mepmep')
+ create(:empty_project, :private, namespace: source_user.namespace, path: 'mepmep')
end
let!(:internal_project) do
- create(:project, :internal, namespace: source_user.namespace, path: 'C')
+ create(:empty_project, :internal, namespace: source_user.namespace, path: 'C')
end
before do
diff --git a/spec/finders/pipelines_finder_spec.rb b/spec/finders/pipelines_finder_spec.rb
index b0811d134fa..fdc8215aa47 100644
--- a/spec/finders/pipelines_finder_spec.rb
+++ b/spec/finders/pipelines_finder_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe PipelinesFinder do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let!(:tag_pipeline) { create(:ci_pipeline, project: project, ref: 'v1.0.0') }
let!(:branch_pipeline) { create(:ci_pipeline, project: project) }
diff --git a/spec/finders/projects_finder_spec.rb b/spec/finders/projects_finder_spec.rb
index 13bda5f7c5a..e44e7434c80 100644
--- a/spec/finders/projects_finder_spec.rb
+++ b/spec/finders/projects_finder_spec.rb
@@ -6,19 +6,19 @@ describe ProjectsFinder do
let(:group) { create(:group, :public) }
let!(:private_project) do
- create(:project, :private, name: 'A', path: 'A')
+ create(:empty_project, :private, name: 'A', path: 'A')
end
let!(:internal_project) do
- create(:project, :internal, group: group, name: 'B', path: 'B')
+ create(:empty_project, :internal, group: group, name: 'B', path: 'B')
end
let!(:public_project) do
- create(:project, :public, group: group, name: 'C', path: 'C')
+ create(:empty_project, :public, group: group, name: 'C', path: 'C')
end
let!(:shared_project) do
- create(:project, :private, name: 'D', path: 'D')
+ create(:empty_project, :private, name: 'D', path: 'D')
end
let(:finder) { described_class.new }
diff --git a/spec/finders/tags_finder_spec.rb b/spec/finders/tags_finder_spec.rb
index 98b42e264dc..460e278e2d3 100644
--- a/spec/finders/tags_finder_spec.rb
+++ b/spec/finders/tags_finder_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe TagsFinder do
let(:user) { create(:user) }
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:repository) { project.repository }
describe '#execute' do
diff --git a/spec/fixtures/api/schemas/issue.json b/spec/fixtures/api/schemas/issue.json
index 77f2bcee1f3..8e19cee5440 100644
--- a/spec/fixtures/api/schemas/issue.json
+++ b/spec/fixtures/api/schemas/issue.json
@@ -6,6 +6,7 @@
"confidential"
],
"properties" : {
+ "id": { "type": "integer" },
"iid": { "type": "integer" },
"title": { "type": "string" },
"confidential": { "type": "boolean" },
diff --git a/spec/fixtures/api/schemas/list.json b/spec/fixtures/api/schemas/list.json
index 8d94cf26ecb..819287bf919 100644
--- a/spec/fixtures/api/schemas/list.json
+++ b/spec/fixtures/api/schemas/list.json
@@ -10,7 +10,7 @@
"id": { "type": "integer" },
"list_type": {
"type": "string",
- "enum": ["backlog", "label", "done"]
+ "enum": ["label", "done"]
},
"label": {
"type": ["object", "null"],
diff --git a/spec/fixtures/config/redis_config_with_env.yml b/spec/fixtures/config/redis_config_with_env.yml
new file mode 100644
index 00000000000..f5860f37e47
--- /dev/null
+++ b/spec/fixtures/config/redis_config_with_env.yml
@@ -0,0 +1,2 @@
+test:
+ url: <%= ENV['TEST_GITLAB_REDIS_URL'] %>
diff --git a/spec/fixtures/emails/reply_without_subaddressing_and_key_inside_references_with_a_comma.eml b/spec/fixtures/emails/reply_without_subaddressing_and_key_inside_references_with_a_comma.eml
new file mode 100644
index 00000000000..6823db0cfc8
--- /dev/null
+++ b/spec/fixtures/emails/reply_without_subaddressing_and_key_inside_references_with_a_comma.eml
@@ -0,0 +1,42 @@
+Return-Path: <jake@adventuretime.ooo>
+Received: from iceking.adventuretime.ooo ([unix socket]) by iceking (Cyrus v2.2.13-Debian-2.2.13-19+squeeze3) with LMTPA; Thu, 13 Jun 2013 17:03:50 -0400
+Received: from mail-ie0-x234.google.com (mail-ie0-x234.google.com [IPv6:2607:f8b0:4001:c03::234]) by iceking.adventuretime.ooo (8.14.3/8.14.3/Debian-9.4) with ESMTP id r5DL3nFJ016967 (version=TLSv1/SSLv3 cipher=RC4-SHA bits=128 verify=NOT) for <reply+59d8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 17:03:50 -0400
+Received: by mail-ie0-f180.google.com with SMTP id f4so21977375iea.25 for <reply@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 14:03:48 -0700
+Received: by 10.0.0.1 with HTTP; Thu, 13 Jun 2013 14:03:48 -0700
+Date: Thu, 13 Jun 2013 17:03:48 -0400
+From: Jake the Dog <jake@adventuretime.ooo>
+To: reply@appmail.adventuretime.ooo
+Message-ID: <CADkmRc+rNGAGGbV2iE5p918UVy4UyJqVcXRO2=otppgzduJSg@mail.gmail.com>
+In-Reply-To: <issue_1@localhost>
+References: <issue_1@localhost> <reply-59d8df8370b7e95c5a49fbf86aeb2c93@localhost>,<exchange@microsoft.com>
+Subject: re: [Discourse Meta] eviltrout posted in 'Adventure Time Sux'
+Mime-Version: 1.0
+Content-Type: text/plain;
+ charset=ISO-8859-1
+Content-Transfer-Encoding: 7bit
+X-Sieve: CMU Sieve 2.2
+X-Received: by 10.0.0.1 with SMTP id n7mr11234144ipb.85.1371157428600; Thu,
+ 13 Jun 2013 14:03:48 -0700 (PDT)
+X-Scanned-By: MIMEDefang 2.69 on IPv6:2001:470:1d:165::1
+
+I could not disagree more. I am obviously biased but adventure time is the
+greatest show ever created. Everyone should watch it.
+
+- Jake out
+
+
+On Sun, Jun 9, 2013 at 1:39 PM, eviltrout via Discourse Meta
+<reply+59d8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo> wrote:
+>
+>
+>
+> eviltrout posted in 'Adventure Time Sux' on Discourse Meta:
+>
+> ---
+> hey guys everyone knows adventure time sucks!
+>
+> ---
+> Please visit this link to respond: http://localhost:3000/t/adventure-time-sux/1234/3
+>
+> To unsubscribe from these emails, visit your [user preferences](http://localhost:3000/user_preferences).
+>
diff --git a/spec/fixtures/pages.tar.gz b/spec/fixtures/pages.tar.gz
new file mode 100644
index 00000000000..d0e89378b3e
--- /dev/null
+++ b/spec/fixtures/pages.tar.gz
Binary files differ
diff --git a/spec/fixtures/pages.zip b/spec/fixtures/pages.zip
new file mode 100644
index 00000000000..9558fcd4b94
--- /dev/null
+++ b/spec/fixtures/pages.zip
Binary files differ
diff --git a/spec/fixtures/pages.zip.meta b/spec/fixtures/pages.zip.meta
new file mode 100644
index 00000000000..1e6198a15f0
--- /dev/null
+++ b/spec/fixtures/pages.zip.meta
Binary files differ
diff --git a/spec/fixtures/pages_empty.tar.gz b/spec/fixtures/pages_empty.tar.gz
new file mode 100644
index 00000000000..5c2afa1a8f6
--- /dev/null
+++ b/spec/fixtures/pages_empty.tar.gz
Binary files differ
diff --git a/spec/fixtures/pages_empty.zip b/spec/fixtures/pages_empty.zip
new file mode 100644
index 00000000000..db3f0334c12
--- /dev/null
+++ b/spec/fixtures/pages_empty.zip
Binary files differ
diff --git a/spec/fixtures/pages_empty.zip.meta b/spec/fixtures/pages_empty.zip.meta
new file mode 100644
index 00000000000..d0b93b3b9c0
--- /dev/null
+++ b/spec/fixtures/pages_empty.zip.meta
Binary files differ
diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb
index 92053e5a7c6..8b201f348f1 100644
--- a/spec/helpers/application_helper_spec.rb
+++ b/spec/helpers/application_helper_spec.rb
@@ -55,7 +55,7 @@ describe ApplicationHelper do
describe 'project_icon' do
it 'returns an url for the avatar' do
- project = create(:project, avatar: File.open(uploaded_image_temp_path))
+ project = create(:empty_project, avatar: File.open(uploaded_image_temp_path))
avatar_url = "http://#{Gitlab.config.gitlab.host}/uploads/project/avatar/#{project.id}/banana_sample.gif"
expect(helper.project_icon("#{project.namespace.to_param}/#{project.to_param}").to_s).
@@ -63,7 +63,7 @@ describe ApplicationHelper do
end
it 'gives uploaded icon when present' do
- project = create(:project)
+ project = create(:empty_project)
allow_any_instance_of(Project).to receive(:avatar_in_git).and_return(true)
diff --git a/spec/helpers/blob_helper_spec.rb b/spec/helpers/blob_helper_spec.rb
index a43a7238c70..fa516f9903e 100644
--- a/spec/helpers/blob_helper_spec.rb
+++ b/spec/helpers/blob_helper_spec.rb
@@ -70,7 +70,7 @@ describe BlobHelper do
describe "#edit_blob_link" do
let(:namespace) { create(:namespace, name: 'gitlab' )}
- let(:project) { create(:project, namespace: namespace) }
+ let(:project) { create(:project, :repository, namespace: namespace) }
before do
allow(self).to receive(:current_user).and_return(double)
diff --git a/spec/helpers/commits_helper_spec.rb b/spec/helpers/commits_helper_spec.rb
index 727c25ff529..a2c008790f9 100644
--- a/spec/helpers/commits_helper_spec.rb
+++ b/spec/helpers/commits_helper_spec.rb
@@ -26,4 +26,23 @@ describe CommitsHelper do
not_to include('onmouseover="alert(1)"')
end
end
+
+ describe '#view_on_environment_button' do
+ let(:project) { create(:empty_project) }
+ let(:environment) { create(:environment, external_url: 'http://example.com') }
+ let(:path) { 'source/file.html' }
+ let(:sha) { RepoHelpers.sample_commit.id }
+
+ before do
+ allow(environment).to receive(:external_url_for).with(path, sha).and_return('http://example.com/file.html')
+ end
+
+ it 'returns a link tag linking to the file in the environment' do
+ html = helper.view_on_environment_button(sha, path, environment)
+ node = Nokogiri::HTML.parse(html).at_css('a')
+
+ expect(node[:title]).to eq('View on example.com')
+ expect(node[:href]).to eq('http://example.com/file.html')
+ end
+ end
end
diff --git a/spec/helpers/diff_helper_spec.rb b/spec/helpers/diff_helper_spec.rb
index 468bcc7badc..eae097126ce 100644
--- a/spec/helpers/diff_helper_spec.rb
+++ b/spec/helpers/diff_helper_spec.rb
@@ -134,7 +134,7 @@ describe DiffHelper do
let(:new_pos) { 50 }
let(:text) { 'some_text' }
- it "should generate foldable top match line for inline view with empty text by default" do
+ it "generates foldable top match line for inline view with empty text by default" do
output = diff_match_line old_pos, new_pos
expect(output).to be_html_safe
@@ -143,7 +143,7 @@ describe DiffHelper do
expect(output).to have_css 'td:nth-child(3):not(.parallel).line_content.match', text: ''
end
- it "should allow to define text and bottom option" do
+ it "allows to define text and bottom option" do
output = diff_match_line old_pos, new_pos, text: text, bottom: true
expect(output).to be_html_safe
@@ -152,7 +152,7 @@ describe DiffHelper do
expect(output).to have_css 'td:nth-child(3):not(.parallel).line_content.match', text: text
end
- it "should generate match line for parallel view" do
+ it "generates match line for parallel view" do
output = diff_match_line old_pos, new_pos, text: text, view: :parallel
expect(output).to be_html_safe
@@ -162,7 +162,7 @@ describe DiffHelper do
expect(output).to have_css 'td:nth-child(4).line_content.match.parallel', text: text
end
- it "should allow to generate only left match line for parallel view" do
+ it "allows to generate only left match line for parallel view" do
output = diff_match_line old_pos, nil, text: text, view: :parallel
expect(output).to be_html_safe
@@ -171,7 +171,7 @@ describe DiffHelper do
expect(output).not_to have_css 'td:nth-child(3)'
end
- it "should allow to generate only right match line for parallel view" do
+ it "allows to generate only right match line for parallel view" do
output = diff_match_line nil, new_pos, text: text, view: :parallel
expect(output).to be_html_safe
diff --git a/spec/helpers/gitlab_markdown_helper_spec.rb b/spec/helpers/gitlab_markdown_helper_spec.rb
index 1d494edcd3b..b8ec3521edb 100644
--- a/spec/helpers/gitlab_markdown_helper_spec.rb
+++ b/spec/helpers/gitlab_markdown_helper_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe GitlabMarkdownHelper do
include ApplicationHelper
- let!(:project) { create(:project) }
+ let!(:project) { create(:project, :repository) }
let(:user) { create(:user, username: 'gfm') }
let(:commit) { project.commit }
@@ -55,18 +55,18 @@ describe GitlabMarkdownHelper do
end
describe '#link_to_gfm' do
- let(:commit_path) { namespace_project_commit_path(project.namespace, project, commit) }
- let(:issues) { create_list(:issue, 2, project: project) }
+ let(:link) { '/commits/0a1b2c3d' }
+ let(:issues) { create_list(:issue, 2, project: project) }
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)
+ actual = helper.link_to_gfm("This should finally fix #{issues[0].to_reference} and #{issues[1].to_reference} for real", link)
doc = Nokogiri::HTML.parse(actual)
# Make sure we didn't create invalid markup
expect(doc.errors).to be_empty
# Leading commit link
- expect(doc.css('a')[0].attr('href')).to eq commit_path
+ expect(doc.css('a')[0].attr('href')).to eq link
expect(doc.css('a')[0].text).to eq 'This should finally fix '
# First issue link
@@ -75,7 +75,7 @@ describe GitlabMarkdownHelper do
expect(doc.css('a')[1].text).to eq issues[0].to_reference
# Internal commit link
- expect(doc.css('a')[2].attr('href')).to eq commit_path
+ expect(doc.css('a')[2].attr('href')).to eq link
expect(doc.css('a')[2].text).to eq ' and '
# Second issue link
@@ -84,12 +84,12 @@ describe GitlabMarkdownHelper do
expect(doc.css('a')[3].text).to eq issues[1].to_reference
# Trailing commit link
- expect(doc.css('a')[4].attr('href')).to eq commit_path
+ expect(doc.css('a')[4].attr('href')).to eq link
expect(doc.css('a')[4].text).to eq ' for real'
end
it 'forwards HTML options' do
- actual = helper.link_to_gfm("Fixed in #{commit.id}", commit_path, class: 'foo')
+ actual = helper.link_to_gfm("Fixed in #{commit.id}", link, class: 'foo')
doc = Nokogiri::HTML.parse(actual)
expect(doc.css('a')).to satisfy do |v|
@@ -100,7 +100,7 @@ describe GitlabMarkdownHelper do
it "escapes HTML passed in as the body" do
actual = "This is a <h1>test</h1> - see #{issues[0].to_reference}"
- expect(helper.link_to_gfm(actual, commit_path)).
+ expect(helper.link_to_gfm(actual, link)).
to match('&lt;h1&gt;test&lt;/h1&gt;')
end
@@ -170,4 +170,14 @@ describe GitlabMarkdownHelper do
expect(doc.content).to eq "@#{user.username}, can you look at this?..."
end
end
+
+ describe '#cross_project_reference' do
+ it 'shows the full MR reference' do
+ expect(helper.cross_project_reference(project, merge_request)).to include(project.path_with_namespace)
+ end
+
+ it 'shows the full issue reference' do
+ expect(helper.cross_project_reference(project, issue)).to include(project.path_with_namespace)
+ end
+ end
end
diff --git a/spec/helpers/graph_helper_spec.rb b/spec/helpers/graph_helper_spec.rb
index 51c49f0e587..400635abdde 100644
--- a/spec/helpers/graph_helper_spec.rb
+++ b/spec/helpers/graph_helper_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe GraphHelper do
describe '#get_refs' do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:commit) { project.commit("master") }
let(:graph) { Network::Graph.new(project, 'master', commit, '') }
diff --git a/spec/helpers/issuables_helper_spec.rb b/spec/helpers/issuables_helper_spec.rb
index a4f08dc4af0..df71680e44c 100644
--- a/spec/helpers/issuables_helper_spec.rb
+++ b/spec/helpers/issuables_helper_spec.rb
@@ -115,6 +115,46 @@ describe IssuablesHelper do
end
end
+ describe '#issuable_reference' do
+ context 'when show_full_reference truthy' do
+ it 'display issuable full reference' do
+ assign(:show_full_reference, true)
+ issue = build_stubbed(:issue)
+
+ expect(helper.issuable_reference(issue)).to eql(issue.to_reference(full: true))
+ end
+ end
+
+ context 'when show_full_reference falsey' do
+ context 'when @group present' do
+ it 'display issuable reference to @group' do
+ project = build_stubbed(:project)
+
+ assign(:show_full_reference, nil)
+ assign(:group, project.namespace)
+
+ issue = build_stubbed(:issue)
+
+ expect(helper.issuable_reference(issue)).to eql(issue.to_reference(project.namespace))
+ end
+ end
+
+ context 'when @project present' do
+ it 'display issuable reference to @project' do
+ project = build_stubbed(:project)
+
+ assign(:show_full_reference, nil)
+ assign(:group, nil)
+ assign(:project, project)
+
+ issue = build_stubbed(:issue)
+
+ expect(helper.issuable_reference(issue)).to eql(issue.to_reference(project))
+ end
+ end
+ end
+ end
+
describe '#issuable_filter_present?' do
it 'returns true when any key is present' do
allow(helper).to receive(:params).and_return(
diff --git a/spec/helpers/issues_helper_spec.rb b/spec/helpers/issues_helper_spec.rb
index abe08d95ece..13fb9c1f1a7 100644
--- a/spec/helpers/issues_helper_spec.rb
+++ b/spec/helpers/issues_helper_spec.rb
@@ -1,7 +1,7 @@
require "spec_helper"
describe IssuesHelper do
- let(:project) { create :project }
+ let(:project) { create(:empty_project) }
let(:issue) { create :issue, project: project }
let(:ext_project) { create :redmine_project }
@@ -98,15 +98,15 @@ describe IssuesHelper do
end
end
- describe '#award_active_class' do
+ describe '#award_state_class' do
let!(:upvote) { create(:award_emoji) }
- it "returns empty string for unauthenticated user" do
- expect(award_active_class(AwardEmoji.all, nil)).to eq("")
+ it "returns disabled string for unauthenticated user" do
+ expect(award_state_class(AwardEmoji.all, nil)).to eq("disabled")
end
it "returns active string for author" do
- expect(award_active_class(AwardEmoji.all, upvote.user)).to eq("active")
+ expect(award_state_class(AwardEmoji.all, upvote.user)).to eq("active")
end
end
diff --git a/spec/helpers/members_helper_spec.rb b/spec/helpers/members_helper_spec.rb
index 33934cdf8b1..2b455571d52 100644
--- a/spec/helpers/members_helper_spec.rb
+++ b/spec/helpers/members_helper_spec.rb
@@ -46,7 +46,7 @@ describe MembersHelper do
end
describe '#leave_confirmation_message' do
- let(:project) { build_stubbed(:project) }
+ let(:project) { build_stubbed(:empty_project) }
let(:group) { build_stubbed(:group) }
let(:user) { build_stubbed(:user) }
diff --git a/spec/helpers/merge_requests_helper_spec.rb b/spec/helpers/merge_requests_helper_spec.rb
index 903224589dd..25f23826648 100644
--- a/spec/helpers/merge_requests_helper_spec.rb
+++ b/spec/helpers/merge_requests_helper_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe MergeRequestsHelper do
describe 'ci_build_details_path' do
- let(:project) { create :project }
+ let(:project) { create(:empty_project) }
let(:merge_request) { MergeRequest.new }
let(:ci_service) { CiService.new }
let(:last_commit) { Ci::Pipeline.new({}) }
@@ -30,7 +30,7 @@ describe MergeRequestsHelper do
it { is_expected.to eq('#1, #2, and #3') }
context 'for JIRA issues' do
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project) }
let(:issues) do
[
ExternalIssue.new('JIRA-123', project),
@@ -52,8 +52,8 @@ describe MergeRequestsHelper do
end
describe 'within different projects' do
- let(:project) { create(:project) }
- let(:fork_project) { create(:project, forked_from_project: project) }
+ let(:project) { create(:empty_project) }
+ let(:fork_project) { create(:empty_project, forked_from_project: project) }
let(:merge_request) { create(:merge_request, source_project: fork_project, target_project: project) }
subject { format_mr_branch_names(merge_request) }
let(:source_title) { "#{fork_project.path_with_namespace}:#{merge_request.source_branch}" }
@@ -62,4 +62,103 @@ describe MergeRequestsHelper do
it { is_expected.to eq([source_title, target_title]) }
end
end
+
+ describe '#mr_widget_refresh_url' do
+ let(:guest) { create(:user) }
+ let(:project) { create(:project, :public) }
+ let(:project_fork) { Projects::ForkService.new(project, guest).execute }
+ let(:merge_request) { create(:merge_request, source_project: project_fork, target_project: project) }
+
+ it 'returns correct url for MR' do
+ expected_url = "#{project.path_with_namespace}/merge_requests/#{merge_request.iid}/merge_widget_refresh"
+
+ expect(mr_widget_refresh_url(merge_request)).to end_with(expected_url)
+ end
+
+ it 'returns empty string for nil' do
+ expect(mr_widget_refresh_url(nil)).to eq('')
+ end
+ end
+
+ describe '#mr_closes_issues' do
+ let(:user_1) { create(:user) }
+ let(:user_2) { create(:user) }
+
+ let(:project_1) { create(:project, :private, creator: user_1, namespace: user_1.namespace) }
+ let(:project_2) { create(:project, :private, creator: user_2, namespace: user_2.namespace) }
+
+ let(:issue_1) { create(:issue, project: project_1) }
+ let(:issue_2) { create(:issue, project: project_2) }
+
+ let(:merge_request) { create(:merge_request, source_project: project_1, target_project: project_1,) }
+
+ let(:merge_request) do
+ create(:merge_request,
+ source_project: project_1, target_project: project_1,
+ description: "Fixes #{issue_1.to_reference} Fixes #{issue_2.to_reference(project_1)}")
+ end
+
+ before do
+ project_1.team << [user_2, :developer]
+ project_2.team << [user_2, :developer]
+ allow(merge_request.project).to receive(:default_branch).and_return(merge_request.target_branch)
+ @merge_request = merge_request
+ end
+
+ context 'user without access to another private project' do
+ let(:current_user) { user_1 }
+
+ it 'cannot see that project\'s issue that will be closed on acceptance' do
+ expect(mr_closes_issues).to contain_exactly(issue_1)
+ end
+ end
+
+ context 'user with access to another private project' do
+ let(:current_user) { user_2 }
+
+ it 'can see that project\'s issue that will be closed on acceptance' do
+ expect(mr_closes_issues).to contain_exactly(issue_1, issue_2)
+ end
+ end
+ end
+
+ describe '#mr_issues_mentioned_but_not_closing' do
+ let(:user_1) { create(:user) }
+ let(:user_2) { create(:user) }
+
+ let(:project_1) { create(:project, :private, creator: user_1, namespace: user_1.namespace) }
+ let(:project_2) { create(:project, :private, creator: user_2, namespace: user_2.namespace) }
+
+ let(:issue_1) { create(:issue, project: project_1) }
+ let(:issue_2) { create(:issue, project: project_2) }
+
+ let(:merge_request) do
+ create(:merge_request,
+ source_project: project_1, target_project: project_1,
+ description: "#{issue_1.to_reference} #{issue_2.to_reference(project_1)}")
+ end
+
+ before do
+ project_1.team << [user_2, :developer]
+ project_2.team << [user_2, :developer]
+ allow(merge_request.project).to receive(:default_branch).and_return(merge_request.target_branch)
+ @merge_request = merge_request
+ end
+
+ context 'user without access to another private project' do
+ let(:current_user) { user_1 }
+
+ it 'cannot see that project\'s issue that will be closed on acceptance' do
+ expect(mr_issues_mentioned_but_not_closing).to contain_exactly(issue_1)
+ end
+ end
+
+ context 'user with access to another private project' do
+ let(:current_user) { user_2 }
+
+ it 'can see that project\'s issue that will be closed on acceptance' do
+ expect(mr_issues_mentioned_but_not_closing).to contain_exactly(issue_1, issue_2)
+ end
+ end
+ end
end
diff --git a/spec/helpers/milestones_helper_spec.rb b/spec/helpers/milestones_helper_spec.rb
index ea744dbb629..14a95479339 100644
--- a/spec/helpers/milestones_helper_spec.rb
+++ b/spec/helpers/milestones_helper_spec.rb
@@ -21,24 +21,22 @@ describe MilestonesHelper do
end
describe '#milestone_counts' do
- let(:project) { FactoryGirl.create(:project) }
+ let(:project) { create(:empty_project) }
let(:counts) { helper.milestone_counts(project.milestones) }
context 'when there are milestones' do
- let!(:milestone_1) { FactoryGirl.create(:active_milestone, project: project) }
- let!(:milestone_2) { FactoryGirl.create(:active_milestone, project: project) }
- let!(:milestone_3) { FactoryGirl.create(:closed_milestone, project: project) }
-
it 'returns the correct counts' do
+ create_list(:active_milestone, 2, project: project)
+ create(:closed_milestone, project: project)
+
expect(counts).to eq(opened: 2, closed: 1, all: 3)
end
end
context 'when there are only milestones of one type' do
- let!(:milestone_1) { FactoryGirl.create(:active_milestone, project: project) }
- let!(:milestone_2) { FactoryGirl.create(:active_milestone, project: project) }
-
it 'returns the correct counts' do
+ create_list(:active_milestone, 2, project: project)
+
expect(counts).to eq(opened: 2, closed: 0, all: 2)
end
end
diff --git a/spec/helpers/preferences_helper_spec.rb b/spec/helpers/preferences_helper_spec.rb
index 77841e85223..1f02e06e312 100644
--- a/spec/helpers/preferences_helper_spec.rb
+++ b/spec/helpers/preferences_helper_spec.rb
@@ -110,7 +110,7 @@ describe PreferencesHelper do
end
context 'when repository is not empty' do
- let(:project) { create(:project, :public) }
+ let(:project) { create(:project, :public, :repository) }
it 'returns readme if user has repository access' do
allow(helper).to receive(:can?).with(nil, :download_code, project).and_return(true)
diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb
index 8113742923b..aca0bb1d794 100644
--- a/spec/helpers/projects_helper_spec.rb
+++ b/spec/helpers/projects_helper_spec.rb
@@ -10,7 +10,7 @@ describe ProjectsHelper do
end
describe "can_change_visibility_level?" do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:user) { create(:project_member, :reporter, user: create(:user), project: project).user }
let(:fork_project) { Projects::ForkService.new(project, user).execute }
@@ -97,7 +97,7 @@ describe ProjectsHelper do
end
describe '#license_short_name' do
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project) }
context 'when project.repository has a license_key' do
it 'returns the nickname of the license if present' do
@@ -203,7 +203,6 @@ describe ProjectsHelper do
context "when project moves from public to private" do
before do
- project.project_feature.update_attributes(issues_access_level: ProjectFeature::ENABLED)
project.update_attributes(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
end
diff --git a/spec/helpers/search_helper_spec.rb b/spec/helpers/search_helper_spec.rb
index 4b2ca3514f8..b7e547dc1f5 100644
--- a/spec/helpers/search_helper_spec.rb
+++ b/spec/helpers/search_helper_spec.rb
@@ -41,8 +41,13 @@ describe SearchHelper do
expect(search_autocomplete_opts("gro").size).to eq(1)
end
+ it "includes nested group" do
+ create(:group, :nested, name: 'foo').add_owner(user)
+ expect(search_autocomplete_opts('foo').size).to eq(1)
+ end
+
it "includes the user's projects" do
- project = create(:project, namespace: create(:namespace, owner: user))
+ project = create(:empty_project, namespace: create(:namespace, owner: user))
expect(search_autocomplete_opts(project.name).size).to eq(1)
end
@@ -52,7 +57,9 @@ describe SearchHelper do
end
context "with a current project" do
- before { @project = create(:project) }
+ before do
+ @project = create(:project, :repository)
+ end
it "includes project-specific sections" do
expect(search_autocomplete_opts("Files").size).to eq(1)
diff --git a/spec/helpers/storage_helper_spec.rb b/spec/helpers/storage_helper_spec.rb
new file mode 100644
index 00000000000..4627a1e1872
--- /dev/null
+++ b/spec/helpers/storage_helper_spec.rb
@@ -0,0 +1,21 @@
+require 'spec_helper'
+
+describe StorageHelper do
+ describe '#storage_counter' do
+ it 'formats bytes to one decimal place' do
+ expect(helper.storage_counter(1.23.megabytes)).to eq '1.2 MB'
+ end
+
+ it 'does not add decimals for sizes < 1 MB' do
+ expect(helper.storage_counter(23.5.kilobytes)).to eq '24 KB'
+ end
+
+ it 'does not add decimals for zeroes' do
+ expect(helper.storage_counter(2.megabytes)).to eq '2 MB'
+ end
+
+ it 'uses commas as thousands separator' do
+ expect(helper.storage_counter(100_000_000_000_000_000)).to eq '90,949.5 TB'
+ end
+ end
+end
diff --git a/spec/helpers/submodule_helper_spec.rb b/spec/helpers/submodule_helper_spec.rb
index 37ac6a2699d..4da1569e59f 100644
--- a/spec/helpers/submodule_helper_spec.rb
+++ b/spec/helpers/submodule_helper_spec.rb
@@ -116,7 +116,7 @@ describe SubmoduleHelper do
context 'submodules with relative links' do
let(:group) { create(:group, name: "Master Project", path: "master-project") }
- let(:project) { create(:project, group: group) }
+ let(:project) { create(:empty_project, group: group) }
let(:commit_id) { sample_commit[:id] }
before do
@@ -145,7 +145,7 @@ describe SubmoduleHelper do
context 'personal project' do
let(:user) { create(:user) }
- let(:project) { create(:project, namespace: user.namespace) }
+ let(:project) { create(:empty_project, namespace: user.namespace) }
it 'one level down with personal project' do
result = relative_self_links('../test.git', commit_id)
diff --git a/spec/helpers/tree_helper_spec.rb b/spec/helpers/tree_helper_spec.rb
index 8d6537ba4b5..9523d0f4aa6 100644
--- a/spec/helpers/tree_helper_spec.rb
+++ b/spec/helpers/tree_helper_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe TreeHelper do
describe 'flatten_tree' do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
before do
@repository = project.repository
diff --git a/spec/helpers/visibility_level_helper_spec.rb b/spec/helpers/visibility_level_helper_spec.rb
index db3ad1b99e9..8942b00b128 100644
--- a/spec/helpers/visibility_level_helper_spec.rb
+++ b/spec/helpers/visibility_level_helper_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe VisibilityLevelHelper do
- let(:project) { build(:project) }
+ let(:project) { build(:empty_project) }
let(:group) { build(:group) }
let(:personal_snippet) { build(:personal_snippet) }
let(:project_snippet) { build(:project_snippet) }
@@ -60,8 +60,8 @@ describe VisibilityLevelHelper do
describe "skip_level?" do
describe "forks" do
- let(:project) { create(:project, :internal) }
- let(:fork_project) { create(:project, forked_from_project: project) }
+ let(:project) { create(:empty_project, :internal) }
+ let(:fork_project) { create(:empty_project, forked_from_project: project) }
it "skips levels" do
expect(skip_level?(fork_project, Gitlab::VisibilityLevel::PUBLIC)).to be_truthy
@@ -71,7 +71,7 @@ describe VisibilityLevelHelper do
end
describe "non-forked project" do
- let(:project) { create(:project, :internal) }
+ let(:project) { create(:empty_project, :internal) }
it "skips levels" do
expect(skip_level?(project, Gitlab::VisibilityLevel::PUBLIC)).to be_falsey
diff --git a/spec/initializers/metrics_spec.rb b/spec/initializers/metrics_spec.rb
new file mode 100644
index 00000000000..bb595162370
--- /dev/null
+++ b/spec/initializers/metrics_spec.rb
@@ -0,0 +1,16 @@
+require 'spec_helper'
+require_relative '../../config/initializers/metrics'
+
+describe 'instrument_classes', lib: true do
+ let(:config) { double(:config) }
+
+ before do
+ allow(config).to receive(:instrument_method)
+ allow(config).to receive(:instrument_methods)
+ allow(config).to receive(:instrument_instance_methods)
+ end
+
+ it 'can autoload and instrument all files' do
+ expect { instrument_classes(config) }.not_to raise_error
+ end
+end
diff --git a/spec/initializers/secret_token_spec.rb b/spec/initializers/secret_token_spec.rb
index 837b0de9a4c..ad7f032d1e5 100644
--- a/spec/initializers/secret_token_spec.rb
+++ b/spec/initializers/secret_token_spec.rb
@@ -2,10 +2,11 @@ require 'spec_helper'
require_relative '../../config/initializers/secret_token'
describe 'create_tokens', lib: true do
+ include StubENV
+
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)
@@ -17,7 +18,7 @@ describe 'create_tokens', lib: true do
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)
+ stub_env('SECRET_KEY_BASE', 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)
@@ -69,7 +70,7 @@ describe 'create_tokens', lib: true do
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')
+ stub_env('SECRET_KEY_BASE', 'env_key')
secrets.secret_key_base = 'secret_key_base'
secrets.otp_key_base = 'otp_key_base'
end
diff --git a/spec/javascripts/.eslintrc b/spec/javascripts/.eslintrc
index 7792acffac2..fbd9bb9f0ff 100644
--- a/spec/javascripts/.eslintrc
+++ b/spec/javascripts/.eslintrc
@@ -1,15 +1,31 @@
{
- "plugins": ["jasmine"],
"env": {
"jasmine": true
},
"extends": "plugin:jasmine/recommended",
- "rules": {
- "prefer-arrow-callback": 0,
- "func-names": 0
- },
"globals": {
- "fixture": false,
+ "appendLoadFixtures": false,
+ "appendLoadStyleFixtures": false,
+ "appendSetFixtures": false,
+ "appendSetStyleFixtures": false,
+ "getJSONFixture": false,
+ "loadFixtures": false,
+ "loadJSONFixtures": false,
+ "loadStyleFixtures": false,
+ "preloadFixtures": false,
+ "preloadStyleFixtures": false,
+ "readFixtures": false,
+ "sandbox": false,
+ "setFixtures": false,
+ "setStyleFixtures": false,
"spyOnEvent": false
+ },
+ "plugins": ["jasmine"],
+ "rules": {
+ "func-names": 0,
+ "jasmine/no-suite-dupes": [1, "branch"],
+ "jasmine/no-spec-dupes": [1, "branch"],
+ "no-console": 0,
+ "prefer-arrow-callback": 0
}
}
diff --git a/spec/javascripts/abuse_reports_spec.js.es6 b/spec/javascripts/abuse_reports_spec.js.es6
index 49e56249565..76b370b345b 100644
--- a/spec/javascripts/abuse_reports_spec.js.es6
+++ b/spec/javascripts/abuse_reports_spec.js.es6
@@ -1,5 +1,5 @@
-/*= require lib/utils/text_utility */
-/*= require abuse_reports */
+require('~/lib/utils/text_utility');
+require('~/abuse_reports');
((global) => {
describe('Abuse Reports', () => {
@@ -13,15 +13,14 @@
(index, element) => element.innerText.indexOf(searchText) > -1,
).first();
- fixture.preload(FIXTURE);
+ preloadFixtures(FIXTURE);
beforeEach(function () {
- fixture.load(FIXTURE);
+ loadFixtures(FIXTURE);
this.abuseReports = new global.AbuseReports();
messages = $('.abuse-reports .message');
});
-
it('should truncate long messages', () => {
const $longMessage = findMessage('LONG MESSAGE');
expect($longMessage.data('original-message')).toEqual(jasmine.anything());
diff --git a/spec/javascripts/activities_spec.js.es6 b/spec/javascripts/activities_spec.js.es6
index 192da4ee8d9..e6a6fc36ca1 100644
--- a/spec/javascripts/activities_spec.js.es6
+++ b/spec/javascripts/activities_spec.js.es6
@@ -1,13 +1,12 @@
-/* eslint-disable no-unused-expressions, comma-spacing, prefer-const, no-prototype-builtins, semi, no-new, keyword-spacing, no-plusplus, no-shadow, max-len */
+/* eslint-disable no-unused-expressions, no-prototype-builtins, no-new, no-shadow, max-len */
-/*= require js.cookie.js */
-/*= require jquery.endless-scroll.js */
-/*= require pager */
-/*= require activities */
+require('vendor/jquery.endless-scroll.js');
+require('~/pager');
+require('~/activities');
(() => {
window.gon || (window.gon = {});
- const fixtureTemplate = 'event_filter.html';
+ const fixtureTemplate = 'static/event_filter.html.raw';
const filters = [
{
id: 'all',
@@ -19,37 +18,37 @@
name: 'merge events',
}, {
id: 'comments',
- },{
+ }, {
id: 'team',
}];
function getEventName(index) {
- let filter = filters[index];
+ const filter = filters[index];
return filter.hasOwnProperty('name') ? filter.name : filter.id;
}
function getSelector(index) {
- let filter = filters[index];
- return `#${filter.id}_event_filter`
+ const filter = filters[index];
+ return `#${filter.id}_event_filter`;
}
describe('Activities', () => {
beforeEach(() => {
- fixture.load(fixtureTemplate);
+ loadFixtures(fixtureTemplate);
new gl.Activities();
});
- for(let i = 0; i < filters.length; i++) {
+ for (let i = 0; i < filters.length; i += 1) {
((i) => {
describe(`when selecting ${getEventName(i)}`, () => {
beforeEach(() => {
$(getSelector(i)).click();
});
- for(let x = 0; x < filters.length; x++) {
+ for (let x = 0; x < filters.length; x += 1) {
((x) => {
- let shouldHighlight = i === x;
- let testName = shouldHighlight ? 'should highlight' : 'should not highlight';
+ const shouldHighlight = i === x;
+ const testName = shouldHighlight ? 'should highlight' : 'should not highlight';
it(`${testName} ${getEventName(x)}`, () => {
expect($(getSelector(x)).parent().hasClass('active')).toEqual(shouldHighlight);
diff --git a/spec/javascripts/awards_handler_spec.js b/spec/javascripts/awards_handler_spec.js
index 89201c8cb8b..001cd8d6325 100644
--- a/spec/javascripts/awards_handler_spec.js
+++ b/spec/javascripts/awards_handler_spec.js
@@ -1,10 +1,8 @@
-/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, no-unused-expressions, comma-dangle, new-parens, no-unused-vars, quotes, jasmine/no-spec-dupes, prefer-template, padded-blocks, max-len */
+/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, no-unused-expressions, comma-dangle, new-parens, no-unused-vars, quotes, jasmine/no-spec-dupes, prefer-template, max-len */
/* global AwardsHandler */
-/*= require awards_handler */
-/*= require jquery */
-/*= require js.cookie */
-/*= require ./fixtures/emoji_menu */
+require('~/awards_handler');
+require('./fixtures/emoji_menu');
(function() {
var awardsHandler, lazyAssert, urlRoot;
@@ -34,9 +32,9 @@
};
describe('AwardsHandler', function() {
- fixture.preload('issues/open-issue.html.raw');
+ preloadFixtures('issues/open-issue.html.raw');
beforeEach(function() {
- fixture.load('issues/open-issue.html.raw');
+ loadFixtures('issues/open-issue.html.raw');
awardsHandler = new AwardsHandler;
spyOn(awardsHandler, 'postEmoji').and.callFake((function(_this) {
return function(url, emoji, cb) {
@@ -113,7 +111,7 @@
});
});
describe('::getAwardUrl', function() {
- return it('should return the url for request', function() {
+ return it('returns the url for request', function() {
return expect(awardsHandler.getAwardUrl()).toBe('http://test.host/frontend-fixtures/issues-project/issues/1/toggle_award_emoji');
});
});
@@ -231,5 +229,4 @@
});
});
});
-
}).call(this);
diff --git a/spec/javascripts/behaviors/autosize_spec.js b/spec/javascripts/behaviors/autosize_spec.js
index b4573e53a4e..4a3da9e318b 100644
--- a/spec/javascripts/behaviors/autosize_spec.js
+++ b/spec/javascripts/behaviors/autosize_spec.js
@@ -1,12 +1,12 @@
-/* eslint-disable space-before-function-paren, no-var, comma-dangle, no-return-assign, padded-blocks, max-len */
+/* eslint-disable space-before-function-paren, no-var, comma-dangle, no-return-assign, max-len */
-/*= require behaviors/autosize */
+require('~/behaviors/autosize');
(function() {
describe('Autosize behavior', function() {
var load;
beforeEach(function() {
- return fixture.set('<textarea class="js-autosize" style="resize: vertical"></textarea>');
+ return setFixtures('<textarea class="js-autosize" style="resize: vertical"></textarea>');
});
it('does not overwrite the resize property', function() {
load();
@@ -15,8 +15,7 @@
});
});
return load = function() {
- return $(document).trigger('page:load');
+ return $(document).trigger('load');
};
});
-
}).call(this);
diff --git a/spec/javascripts/behaviors/quick_submit_spec.js b/spec/javascripts/behaviors/quick_submit_spec.js
index 0f61000bc37..b84126c0e3d 100644
--- a/spec/javascripts/behaviors/quick_submit_spec.js
+++ b/spec/javascripts/behaviors/quick_submit_spec.js
@@ -1,13 +1,13 @@
-/* eslint-disable space-before-function-paren, no-var, no-return-assign, comma-dangle, jasmine/no-spec-dupes, new-cap, padded-blocks, max-len */
+/* eslint-disable space-before-function-paren, no-var, no-return-assign, comma-dangle, jasmine/no-spec-dupes, new-cap, max-len */
-/*= require behaviors/quick_submit */
+require('~/behaviors/quick_submit');
(function() {
describe('Quick Submit behavior', function() {
var keydownEvent;
- fixture.preload('behaviors/quick_submit.html');
+ preloadFixtures('static/behaviors/quick_submit.html.raw');
beforeEach(function() {
- fixture.load('behaviors/quick_submit.html');
+ loadFixtures('static/behaviors/quick_submit.html.raw');
$('form').submit(function(e) {
// Prevent a form submit from moving us off the testing page
return e.preventDefault();
@@ -93,5 +93,4 @@
return $.Event('keydown', $.extend({}, defaults, options));
};
});
-
}).call(this);
diff --git a/spec/javascripts/behaviors/requires_input_spec.js b/spec/javascripts/behaviors/requires_input_spec.js
index c3f4c867d6a..a958ac76e66 100644
--- a/spec/javascripts/behaviors/requires_input_spec.js
+++ b/spec/javascripts/behaviors/requires_input_spec.js
@@ -1,12 +1,12 @@
-/* eslint-disable space-before-function-paren, no-var, padded-blocks */
+/* eslint-disable space-before-function-paren, no-var */
-/*= require behaviors/requires_input */
+require('~/behaviors/requires_input');
(function() {
describe('requiresInput', function() {
- fixture.preload('behaviors/requires_input.html');
+ preloadFixtures('static/behaviors/requires_input.html.raw');
beforeEach(function() {
- return fixture.load('behaviors/requires_input.html');
+ return loadFixtures('static/behaviors/requires_input.html.raw');
});
it('disables submit when any field is required', function() {
$('.js-requires-input').requiresInput();
@@ -34,12 +34,5 @@
$('#required5').val('1').change();
return expect($('.submit')).not.toBeDisabled();
});
- return it('is called on page:load event', function() {
- var spy;
- spy = spyOn($.fn, 'requiresInput');
- $(document).trigger('page:load');
- return expect(spy).toHaveBeenCalled();
- });
});
-
}).call(this);
diff --git a/spec/javascripts/boards/boards_store_spec.js.es6 b/spec/javascripts/boards/boards_store_spec.js.es6
index b3a1afa28a5..9dd741a680b 100644
--- a/spec/javascripts/boards/boards_store_spec.js.es6
+++ b/spec/javascripts/boards/boards_store_spec.js.es6
@@ -1,4 +1,4 @@
-/* eslint-disable comma-dangle, one-var, no-unused-vars, indent */
+/* eslint-disable comma-dangle, one-var, no-unused-vars */
/* global Vue */
/* global BoardService */
/* global boardsMockInterceptor */
@@ -6,24 +6,19 @@
/* global listObj */
/* global listObjDuplicate */
-//= require jquery
-//= require jquery_ujs
-//= require js.cookie
-//= require vue
-//= require vue-resource
-//= require lib/utils/url_utility
-//= require boards/models/issue
-//= require boards/models/label
-//= require boards/models/list
-//= require boards/models/user
-//= require boards/services/board_service
-//= require boards/stores/boards_store
-//= require ./mock_data
+require('~/lib/utils/url_utility');
+require('~/boards/models/issue');
+require('~/boards/models/label');
+require('~/boards/models/list');
+require('~/boards/models/user');
+require('~/boards/services/board_service');
+require('~/boards/stores/boards_store');
+require('./mock_data');
describe('Store', () => {
beforeEach(() => {
Vue.http.interceptors.push(boardsMockInterceptor);
- gl.boardService = new BoardService('/test/issue-boards/board', '1');
+ gl.boardService = new BoardService('/test/issue-boards/board', '', '1');
gl.issueBoards.BoardsStore.create();
Cookies.set('issue_board_welcome_hidden', 'false', {
@@ -61,18 +56,6 @@ describe('Store', () => {
expect(list).toBeDefined();
});
- it('finds list limited by type', () => {
- gl.issueBoards.BoardsStore.addList({
- id: 1,
- position: 0,
- title: 'Test',
- list_type: 'backlog'
- });
- const list = gl.issueBoards.BoardsStore.findList('id', 1, 'backlog');
-
- expect(list).toBeDefined();
- });
-
it('gets issue when new list added', (done) => {
gl.issueBoards.BoardsStore.addList(listObj);
const list = gl.issueBoards.BoardsStore.findList('id', 1);
@@ -117,10 +100,7 @@ describe('Store', () => {
expect(gl.issueBoards.BoardsStore.shouldAddBlankState()).toBe(false);
});
- it('check for blank state adding when backlog & done list exist', () => {
- gl.issueBoards.BoardsStore.addList({
- list_type: 'backlog'
- });
+ it('check for blank state adding when done list exist', () => {
gl.issueBoards.BoardsStore.addList({
list_type: 'done'
});
@@ -146,8 +126,8 @@ describe('Store', () => {
});
it('moves the position of lists', () => {
- const listOne = gl.issueBoards.BoardsStore.addList(listObj),
- listTwo = gl.issueBoards.BoardsStore.addList(listObjDuplicate);
+ const listOne = gl.issueBoards.BoardsStore.addList(listObj);
+ const listTwo = gl.issueBoards.BoardsStore.addList(listObjDuplicate);
expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(2);
@@ -157,8 +137,8 @@ describe('Store', () => {
});
it('moves an issue from one list to another', (done) => {
- const listOne = gl.issueBoards.BoardsStore.addList(listObj),
- listTwo = gl.issueBoards.BoardsStore.addList(listObjDuplicate);
+ const listOne = gl.issueBoards.BoardsStore.addList(listObj);
+ const listTwo = gl.issueBoards.BoardsStore.addList(listObjDuplicate);
expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(2);
diff --git a/spec/javascripts/boards/issue_card_spec.js.es6 b/spec/javascripts/boards/issue_card_spec.js.es6
new file mode 100644
index 00000000000..4340a571017
--- /dev/null
+++ b/spec/javascripts/boards/issue_card_spec.js.es6
@@ -0,0 +1,191 @@
+/* global Vue */
+/* global ListUser */
+/* global ListLabel */
+/* global listObj */
+/* global ListIssue */
+
+require('~/boards/models/issue');
+require('~/boards/models/label');
+require('~/boards/models/list');
+require('~/boards/models/user');
+require('~/boards/stores/boards_store');
+require('~/boards/components/issue_card_inner');
+require('./mock_data');
+
+describe('Issue card component', () => {
+ const user = new ListUser({
+ id: 1,
+ name: 'testing 123',
+ username: 'test',
+ avatar: 'test_image',
+ });
+ const label1 = new ListLabel({
+ id: 3,
+ title: 'testing 123',
+ color: 'blue',
+ text_color: 'white',
+ description: 'test',
+ });
+ let component;
+ let issue;
+ let list;
+
+ beforeEach(() => {
+ setFixtures('<div class="test-container"></div>');
+
+ list = listObj;
+ issue = new ListIssue({
+ title: 'Testing',
+ iid: 1,
+ confidential: false,
+ labels: [list.label],
+ });
+
+ component = new Vue({
+ el: document.querySelector('.test-container'),
+ data() {
+ return {
+ list,
+ issue,
+ issueLinkBase: '/test',
+ rootPath: '/',
+ };
+ },
+ components: {
+ 'issue-card': gl.issueBoards.IssueCardInner,
+ },
+ template: `
+ <issue-card
+ :issue="issue"
+ :list="list"
+ :issue-link-base="issueLinkBase"
+ :root-path="rootPath"></issue-card>
+ `,
+ });
+ });
+
+ it('renders issue title', () => {
+ expect(
+ component.$el.querySelector('.card-title').textContent,
+ ).toContain(issue.title);
+ });
+
+ it('includes issue base in link', () => {
+ expect(
+ component.$el.querySelector('.card-title a').getAttribute('href'),
+ ).toContain('/test');
+ });
+
+ it('includes issue title on link', () => {
+ expect(
+ component.$el.querySelector('.card-title a').getAttribute('title'),
+ ).toBe(issue.title);
+ });
+
+ it('does not render confidential icon', () => {
+ expect(
+ component.$el.querySelector('.fa-eye-flash'),
+ ).toBeNull();
+ });
+
+ it('renders confidential icon', (done) => {
+ component.issue.confidential = true;
+
+ setTimeout(() => {
+ expect(
+ component.$el.querySelector('.confidential-icon'),
+ ).not.toBeNull();
+ done();
+ }, 0);
+ });
+
+ it('renders issue ID with #', () => {
+ expect(
+ component.$el.querySelector('.card-number').textContent,
+ ).toContain(`#${issue.id}`);
+ });
+
+ describe('assignee', () => {
+ it('does not render assignee', () => {
+ expect(
+ component.$el.querySelector('.card-assignee'),
+ ).toBeNull();
+ });
+
+ describe('exists', () => {
+ beforeEach((done) => {
+ component.issue.assignee = user;
+
+ setTimeout(() => {
+ done();
+ }, 0);
+ });
+
+ it('renders assignee', () => {
+ expect(
+ component.$el.querySelector('.card-assignee'),
+ ).not.toBeNull();
+ });
+
+ it('sets title', () => {
+ expect(
+ component.$el.querySelector('.card-assignee').getAttribute('title'),
+ ).toContain(`Assigned to ${user.name}`);
+ });
+
+ it('sets users path', () => {
+ expect(
+ component.$el.querySelector('.card-assignee').getAttribute('href'),
+ ).toBe('/test');
+ });
+
+ it('renders avatar', () => {
+ expect(
+ component.$el.querySelector('.card-assignee img'),
+ ).not.toBeNull();
+ });
+ });
+ });
+
+ describe('labels', () => {
+ it('does not render any', () => {
+ expect(
+ component.$el.querySelector('.label'),
+ ).toBeNull();
+ });
+
+ describe('exists', () => {
+ beforeEach((done) => {
+ component.issue.addLabel(label1);
+
+ setTimeout(() => {
+ done();
+ }, 0);
+ });
+
+ it('does not render list label', () => {
+ expect(
+ component.$el.querySelectorAll('.label').length,
+ ).toBe(1);
+ });
+
+ it('renders label', () => {
+ expect(
+ component.$el.querySelector('.label').textContent,
+ ).toContain(label1.title);
+ });
+
+ it('sets label description as title', () => {
+ expect(
+ component.$el.querySelector('.label').getAttribute('title'),
+ ).toContain(label1.description);
+ });
+
+ it('sets background color of button', () => {
+ expect(
+ component.$el.querySelector('.label').style.backgroundColor,
+ ).toContain(label1.color);
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/boards/issue_spec.js.es6 b/spec/javascripts/boards/issue_spec.js.es6
index c8a61a0a9b5..aab4d9c501e 100644
--- a/spec/javascripts/boards/issue_spec.js.es6
+++ b/spec/javascripts/boards/issue_spec.js.es6
@@ -2,25 +2,20 @@
/* global BoardService */
/* global ListIssue */
-//= require jquery
-//= require jquery_ujs
-//= require js.cookie
-//= require vue
-//= require vue-resource
-//= require lib/utils/url_utility
-//= require boards/models/issue
-//= require boards/models/label
-//= require boards/models/list
-//= require boards/models/user
-//= require boards/services/board_service
-//= require boards/stores/boards_store
-//= require ./mock_data
+require('~/lib/utils/url_utility');
+require('~/boards/models/issue');
+require('~/boards/models/label');
+require('~/boards/models/list');
+require('~/boards/models/user');
+require('~/boards/services/board_service');
+require('~/boards/stores/boards_store');
+require('./mock_data');
describe('Issue model', () => {
let issue;
beforeEach(() => {
- gl.boardService = new BoardService('/test/issue-boards/board', '1');
+ gl.boardService = new BoardService('/test/issue-boards/board', '', '1');
gl.issueBoards.BoardsStore.create();
issue = new ListIssue({
diff --git a/spec/javascripts/boards/list_spec.js.es6 b/spec/javascripts/boards/list_spec.js.es6
index 7d942ec3d65..4397a32fedc 100644
--- a/spec/javascripts/boards/list_spec.js.es6
+++ b/spec/javascripts/boards/list_spec.js.es6
@@ -5,26 +5,21 @@
/* global List */
/* global listObj */
-//= require jquery
-//= require jquery_ujs
-//= require js.cookie
-//= require vue
-//= require vue-resource
-//= require lib/utils/url_utility
-//= require boards/models/issue
-//= require boards/models/label
-//= require boards/models/list
-//= require boards/models/user
-//= require boards/services/board_service
-//= require boards/stores/boards_store
-//= require ./mock_data
+require('~/lib/utils/url_utility');
+require('~/boards/models/issue');
+require('~/boards/models/label');
+require('~/boards/models/list');
+require('~/boards/models/user');
+require('~/boards/services/board_service');
+require('~/boards/stores/boards_store');
+require('./mock_data');
describe('List model', () => {
let list;
beforeEach(() => {
Vue.http.interceptors.push(boardsMockInterceptor);
- gl.boardService = new BoardService('/test/issue-boards/board', '1');
+ gl.boardService = new BoardService('/test/issue-boards/board', '', '1');
gl.issueBoards.BoardsStore.create();
list = new List(listObj);
diff --git a/spec/javascripts/boards/mock_data.js.es6 b/spec/javascripts/boards/mock_data.js.es6
index 8d3e2237fda..7a399b307ad 100644
--- a/spec/javascripts/boards/mock_data.js.es6
+++ b/spec/javascripts/boards/mock_data.js.es6
@@ -56,3 +56,8 @@ const boardsMockInterceptor = (request, next) => {
status: 200
}));
};
+
+window.listObj = listObj;
+window.listObjDuplicate = listObjDuplicate;
+window.BoardsMockData = BoardsMockData;
+window.boardsMockInterceptor = boardsMockInterceptor;
diff --git a/spec/javascripts/boards/modal_store_spec.js.es6 b/spec/javascripts/boards/modal_store_spec.js.es6
new file mode 100644
index 00000000000..1815847f3fa
--- /dev/null
+++ b/spec/javascripts/boards/modal_store_spec.js.es6
@@ -0,0 +1,132 @@
+/* global Vue */
+/* global ListIssue */
+
+require('~/boards/models/issue');
+require('~/boards/models/label');
+require('~/boards/models/list');
+require('~/boards/models/user');
+require('~/boards/stores/modal_store');
+
+describe('Modal store', () => {
+ let issue;
+ let issue2;
+ const Store = gl.issueBoards.ModalStore;
+
+ beforeEach(() => {
+ // Setup default state
+ Store.store.issues = [];
+ Store.store.selectedIssues = [];
+
+ issue = new ListIssue({
+ title: 'Testing',
+ iid: 1,
+ confidential: false,
+ labels: [],
+ });
+ issue2 = new ListIssue({
+ title: 'Testing',
+ iid: 2,
+ confidential: false,
+ labels: [],
+ });
+ Store.store.issues.push(issue);
+ Store.store.issues.push(issue2);
+ });
+
+ it('returns selected count', () => {
+ expect(Store.selectedCount()).toBe(0);
+ });
+
+ it('toggles the issue as selected', () => {
+ Store.toggleIssue(issue);
+
+ expect(issue.selected).toBe(true);
+ expect(Store.selectedCount()).toBe(1);
+ });
+
+ it('toggles the issue as un-selected', () => {
+ Store.toggleIssue(issue);
+ Store.toggleIssue(issue);
+
+ expect(issue.selected).toBe(false);
+ expect(Store.selectedCount()).toBe(0);
+ });
+
+ it('toggles all issues as selected', () => {
+ Store.toggleAll();
+
+ expect(issue.selected).toBe(true);
+ expect(issue2.selected).toBe(true);
+ expect(Store.selectedCount()).toBe(2);
+ });
+
+ it('toggles all issues as un-selected', () => {
+ Store.toggleAll();
+ Store.toggleAll();
+
+ expect(issue.selected).toBe(false);
+ expect(issue2.selected).toBe(false);
+ expect(Store.selectedCount()).toBe(0);
+ });
+
+ it('toggles all if a single issue is selected', () => {
+ Store.toggleIssue(issue);
+ Store.toggleAll();
+
+ expect(issue.selected).toBe(true);
+ expect(issue2.selected).toBe(true);
+ expect(Store.selectedCount()).toBe(2);
+ });
+
+ it('adds issue to selected array', () => {
+ issue.selected = true;
+ Store.addSelectedIssue(issue);
+
+ expect(Store.selectedCount()).toBe(1);
+ });
+
+ it('removes issue from selected array', () => {
+ Store.addSelectedIssue(issue);
+ Store.removeSelectedIssue(issue);
+
+ expect(Store.selectedCount()).toBe(0);
+ });
+
+ it('returns selected issue index if present', () => {
+ Store.toggleIssue(issue);
+
+ expect(Store.selectedIssueIndex(issue)).toBe(0);
+ });
+
+ it('returns -1 if issue is not selected', () => {
+ expect(Store.selectedIssueIndex(issue)).toBe(-1);
+ });
+
+ it('finds the selected issue', () => {
+ Store.toggleIssue(issue);
+
+ expect(Store.findSelectedIssue(issue)).toBe(issue);
+ });
+
+ it('does not find a selected issue', () => {
+ expect(Store.findSelectedIssue(issue)).toBe(undefined);
+ });
+
+ it('does not remove from selected issue if tab is not all', () => {
+ Store.store.activeTab = 'selected';
+
+ Store.toggleIssue(issue);
+ Store.toggleIssue(issue);
+
+ expect(Store.store.selectedIssues.length).toBe(1);
+ expect(Store.selectedCount()).toBe(0);
+ });
+
+ it('gets selected issue array with only selected issues', () => {
+ Store.toggleIssue(issue);
+ Store.toggleIssue(issue2);
+ Store.toggleIssue(issue2);
+
+ expect(Store.getSelectedIssues().length).toBe(1);
+ });
+});
diff --git a/spec/javascripts/bootstrap_linked_tabs_spec.js.es6 b/spec/javascripts/bootstrap_linked_tabs_spec.js.es6
index 133712debab..fa9f95e16cd 100644
--- a/spec/javascripts/bootstrap_linked_tabs_spec.js.es6
+++ b/spec/javascripts/bootstrap_linked_tabs_spec.js.es6
@@ -1,16 +1,27 @@
-//= require lib/utils/bootstrap_linked_tabs
+require('~/lib/utils/bootstrap_linked_tabs');
(() => {
+ // TODO: remove this hack!
+ // PhantomJS causes spyOn to panic because replaceState isn't "writable"
+ let phantomjs;
+ try {
+ phantomjs = !Object.getOwnPropertyDescriptor(window.history, 'replaceState').writable;
+ } catch (err) {
+ phantomjs = false;
+ }
+
describe('Linked Tabs', () => {
- fixture.preload('linked_tabs');
+ preloadFixtures('static/linked_tabs.html.raw');
beforeEach(() => {
- fixture.load('linked_tabs');
+ loadFixtures('static/linked_tabs.html.raw');
});
describe('when is initialized', () => {
beforeEach(() => {
- spyOn(window.history, 'replaceState').and.callFake(function () {});
+ if (!phantomjs) {
+ spyOn(window.history, 'replaceState').and.callFake(function () {});
+ }
});
it('should activate the tab correspondent to the given action', () => {
@@ -36,7 +47,7 @@
describe('on click', () => {
it('should change the url according to the clicked tab', () => {
- const historySpy = spyOn(history, 'replaceState').and.callFake(() => {});
+ const historySpy = !phantomjs && spyOn(history, 'replaceState').and.callFake(() => {});
const linkedTabs = new window.gl.LinkedTabs({ // eslint-disable-line
action: 'show',
@@ -49,10 +60,11 @@
secondTab.click();
- expect(historySpy).toHaveBeenCalledWith({
- turbolinks: true,
- url: newState,
- }, document.title, newState);
+ if (historySpy) {
+ expect(historySpy).toHaveBeenCalledWith({
+ url: newState,
+ }, document.title, newState);
+ }
});
});
});
diff --git a/spec/javascripts/build_spec.js.es6 b/spec/javascripts/build_spec.js.es6
index 3983cad4c13..0bd50588f5a 100644
--- a/spec/javascripts/build_spec.js.es6
+++ b/spec/javascripts/build_spec.js.es6
@@ -1,12 +1,11 @@
/* eslint-disable no-new */
/* global Build */
-/* global Turbolinks */
-//= require lib/utils/datetime_utility
-//= require build
-//= require breakpoints
-//= require jquery.nicescroll
-//= require turbolinks
+require('~/lib/utils/datetime_utility');
+require('~/lib/utils/url_utility');
+require('~/build');
+require('~/breakpoints');
+require('vendor/jquery.nicescroll');
describe('Build', () => {
const BUILD_URL = `${gl.TEST_HOST}/frontend-fixtures/builds-project/builds/1`;
@@ -17,10 +16,10 @@ describe('Build', () => {
offset: BUILD_TRACE.length, n_open_tags: 0, fg_color: null, bg_color: null, style_mask: 0,
}));
- fixture.preload('builds/build-with-artifacts.html.raw');
+ preloadFixtures('builds/build-with-artifacts.html.raw');
beforeEach(() => {
- fixture.load('builds/build-with-artifacts.html.raw');
+ loadFixtures('builds/build-with-artifacts.html.raw');
spyOn($, 'ajax');
});
@@ -167,7 +166,7 @@ describe('Build', () => {
});
it('reloads the page when the build is done', () => {
- spyOn(Turbolinks, 'visit');
+ spyOn(gl.utils, 'visitUrl');
jasmine.clock().tick(4001);
const [{ success, context }] = $.ajax.calls.argsFor(1);
@@ -177,7 +176,7 @@ describe('Build', () => {
append: true,
});
- expect(Turbolinks.visit).toHaveBeenCalledWith(BUILD_URL);
+ expect(gl.utils.visitUrl).toHaveBeenCalledWith(BUILD_URL);
});
});
});
diff --git a/spec/javascripts/commit/pipelines/mock_data.js.es6 b/spec/javascripts/commit/pipelines/mock_data.js.es6
new file mode 100644
index 00000000000..188908d66bd
--- /dev/null
+++ b/spec/javascripts/commit/pipelines/mock_data.js.es6
@@ -0,0 +1,92 @@
+/* eslint-disable no-unused-vars */
+const pipeline = {
+ id: 73,
+ user: {
+ name: 'Administrator',
+ username: 'root',
+ id: 1,
+ state: 'active',
+ avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ web_url: 'http://localhost:3000/root',
+ },
+ path: '/root/review-app/pipelines/73',
+ details: {
+ status: {
+ icon: 'icon_status_failed',
+ text: 'failed',
+ label: 'failed',
+ group: 'failed',
+ has_details: true,
+ details_path: '/root/review-app/pipelines/73',
+ },
+ duration: null,
+ finished_at: '2017-01-25T00:00:17.130Z',
+ stages: [{
+ name: 'build',
+ title: 'build: failed',
+ status: {
+ icon: 'icon_status_failed',
+ text: 'failed',
+ label: 'failed',
+ group: 'failed',
+ has_details: true,
+ details_path: '/root/review-app/pipelines/73#build',
+ },
+ path: '/root/review-app/pipelines/73#build',
+ dropdown_path: '/root/review-app/pipelines/73/stage.json?stage=build',
+ }],
+ artifacts: [],
+ manual_actions: [
+ {
+ name: 'stop_review',
+ path: '/root/review-app/builds/1463/play',
+ },
+ {
+ name: 'name',
+ path: '/root/review-app/builds/1490/play',
+ },
+ ],
+ },
+ flags: {
+ latest: true,
+ triggered: false,
+ stuck: false,
+ yaml_errors: false,
+ retryable: true,
+ cancelable: false,
+ },
+ ref:
+ {
+ name: 'master',
+ path: '/root/review-app/tree/master',
+ tag: false,
+ branch: true,
+ },
+ commit: {
+ id: 'fbd79f04fa98717641deaaeb092a4d417237c2e4',
+ short_id: 'fbd79f04',
+ title: 'Update .gitlab-ci.yml',
+ author_name: 'Administrator',
+ author_email: 'admin@example.com',
+ created_at: '2017-01-16T12:13:57.000-05:00',
+ committer_name: 'Administrator',
+ committer_email: 'admin@example.com',
+ message: 'Update .gitlab-ci.yml',
+ author: {
+ name: 'Administrator',
+ username: 'root',
+ id: 1,
+ state: 'active',
+ avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ web_url: 'http://localhost:3000/root',
+ },
+ author_gravatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ commit_url: 'http://localhost:3000/root/review-app/commit/fbd79f04fa98717641deaaeb092a4d417237c2e4',
+ commit_path: '/root/review-app/commit/fbd79f04fa98717641deaaeb092a4d417237c2e4',
+ },
+ retry_path: '/root/review-app/pipelines/73/retry',
+ created_at: '2017-01-16T17:13:59.800Z',
+ updated_at: '2017-01-25T00:00:17.132Z',
+};
+
+module.exports = pipeline;
diff --git a/spec/javascripts/commit/pipelines/pipelines_spec.js.es6 b/spec/javascripts/commit/pipelines/pipelines_spec.js.es6
new file mode 100644
index 00000000000..f09c57978a1
--- /dev/null
+++ b/spec/javascripts/commit/pipelines/pipelines_spec.js.es6
@@ -0,0 +1,105 @@
+/* global pipeline, Vue */
+
+require('~/flash');
+require('~/commit/pipelines/pipelines_store');
+require('~/commit/pipelines/pipelines_service');
+require('~/commit/pipelines/pipelines_table');
+require('~/vue_shared/vue_resource_interceptor');
+const pipeline = require('./mock_data');
+
+describe('Pipelines table in Commits and Merge requests', () => {
+ preloadFixtures('static/pipelines_table.html.raw');
+
+ beforeEach(() => {
+ loadFixtures('static/pipelines_table.html.raw');
+ });
+
+ describe('successfull request', () => {
+ describe('without pipelines', () => {
+ const pipelinesEmptyResponse = (request, next) => {
+ next(request.respondWith(JSON.stringify([]), {
+ status: 200,
+ }));
+ };
+
+ beforeEach(() => {
+ Vue.http.interceptors.push(pipelinesEmptyResponse);
+ });
+
+ afterEach(() => {
+ Vue.http.interceptors = _.without(
+ Vue.http.interceptors, pipelinesEmptyResponse,
+ );
+ });
+
+ it('should render the empty state', (done) => {
+ const component = new gl.commits.pipelines.PipelinesTableView({
+ el: document.querySelector('#commit-pipeline-table-view'),
+ });
+
+ setTimeout(() => {
+ expect(component.$el.querySelector('.js-blank-state-title').textContent).toContain('No pipelines to show');
+ done();
+ }, 1);
+ });
+ });
+
+ describe('with pipelines', () => {
+ const pipelinesResponse = (request, next) => {
+ next(request.respondWith(JSON.stringify([pipeline]), {
+ status: 200,
+ }));
+ };
+
+ beforeEach(() => {
+ Vue.http.interceptors.push(pipelinesResponse);
+ });
+
+ afterEach(() => {
+ Vue.http.interceptors = _.without(
+ Vue.http.interceptors, pipelinesResponse,
+ );
+ });
+
+ it('should render a table with the received pipelines', (done) => {
+ const component = new gl.commits.pipelines.PipelinesTableView({
+ el: document.querySelector('#commit-pipeline-table-view'),
+ });
+
+ setTimeout(() => {
+ expect(component.$el.querySelectorAll('table > tbody > tr').length).toEqual(1);
+ done();
+ }, 0);
+ });
+ });
+ });
+
+ describe('unsuccessfull request', () => {
+ const pipelinesErrorResponse = (request, next) => {
+ next(request.respondWith(JSON.stringify([]), {
+ status: 500,
+ }));
+ };
+
+ beforeEach(() => {
+ Vue.http.interceptors.push(pipelinesErrorResponse);
+ });
+
+ afterEach(() => {
+ Vue.http.interceptors = _.without(
+ Vue.http.interceptors, pipelinesErrorResponse,
+ );
+ });
+
+ it('should render empty state', (done) => {
+ const component = new gl.commits.pipelines.PipelinesTableView({
+ el: document.querySelector('#commit-pipeline-table-view'),
+ });
+
+ setTimeout(() => {
+ expect(component.$el.querySelector('.js-blank-state-title').textContent).toContain('No pipelines to show');
+ done();
+ }, 0);
+ });
+ });
+});
diff --git a/spec/javascripts/commit/pipelines/pipelines_store_spec.js.es6 b/spec/javascripts/commit/pipelines/pipelines_store_spec.js.es6
new file mode 100644
index 00000000000..559723bafcc
--- /dev/null
+++ b/spec/javascripts/commit/pipelines/pipelines_store_spec.js.es6
@@ -0,0 +1,30 @@
+require('~/commit/pipelines/pipelines_store');
+
+describe('Store', () => {
+ let store;
+
+ beforeEach(() => {
+ store = new gl.commits.pipelines.PipelinesStore();
+ });
+
+ it('should start with a blank state', () => {
+ expect(store.state.pipelines.length).toBe(0);
+ });
+
+ it('should store an array of pipelines', () => {
+ const pipelines = [
+ {
+ id: '1',
+ name: 'pipeline',
+ },
+ {
+ id: '2',
+ name: 'pipeline_2',
+ },
+ ];
+
+ store.storePipelines(pipelines);
+
+ expect(store.state.pipelines.length).toBe(pipelines.length);
+ });
+});
diff --git a/spec/javascripts/commits_spec.js.es6 b/spec/javascripts/commits_spec.js.es6
new file mode 100644
index 00000000000..05260760c43
--- /dev/null
+++ b/spec/javascripts/commits_spec.js.es6
@@ -0,0 +1,62 @@
+/* global CommitsList */
+
+require('vendor/jquery.endless-scroll');
+require('~/pager');
+require('~/commits');
+
+(() => {
+ // TODO: remove this hack!
+ // PhantomJS causes spyOn to panic because replaceState isn't "writable"
+ let phantomjs;
+ try {
+ phantomjs = !Object.getOwnPropertyDescriptor(window.history, 'replaceState').writable;
+ } catch (err) {
+ phantomjs = false;
+ }
+
+ describe('Commits List', () => {
+ beforeEach(() => {
+ setFixtures(`
+ <form class="commits-search-form" action="/h5bp/html5-boilerplate/commits/master">
+ <input id="commits-search">
+ </form>
+ <ol id="commits-list"></ol>
+ `);
+ });
+
+ it('should be defined', () => {
+ expect(CommitsList).toBeDefined();
+ });
+
+ describe('on entering input', () => {
+ let ajaxSpy;
+
+ beforeEach(() => {
+ CommitsList.init(25);
+ CommitsList.searchField.val('');
+
+ if (!phantomjs) {
+ spyOn(history, 'replaceState').and.stub();
+ }
+ ajaxSpy = spyOn(jQuery, 'ajax').and.callFake((req) => {
+ req.success({
+ data: '<li>Result</li>',
+ });
+ });
+ });
+
+ it('should save the last search string', () => {
+ CommitsList.searchField.val('GitLab');
+ CommitsList.filterResults();
+ expect(ajaxSpy).toHaveBeenCalled();
+ expect(CommitsList.lastSearch).toEqual('GitLab');
+ });
+
+ it('should not make ajax call if the input does not change', () => {
+ CommitsList.filterResults();
+ expect(ajaxSpy).not.toHaveBeenCalled();
+ expect(CommitsList.lastSearch).toEqual('');
+ });
+ });
+ });
+})();
diff --git a/spec/javascripts/dashboard_spec.js.es6 b/spec/javascripts/dashboard_spec.js.es6
index aadf6f518a8..c0bdb89ed63 100644
--- a/spec/javascripts/dashboard_spec.js.es6
+++ b/spec/javascripts/dashboard_spec.js.es6
@@ -1,13 +1,11 @@
-/* eslint-disable no-new, padded-blocks */
+/* eslint-disable no-new */
-/*= require sidebar */
-/*= require jquery */
-/*= require js.cookie */
-/*= require lib/utils/text_utility */
+require('~/sidebar');
+require('~/lib/utils/text_utility');
((global) => {
describe('Dashboard', () => {
- const fixtureTemplate = 'dashboard.html';
+ const fixtureTemplate = 'static/dashboard.html.raw';
function todosCountText() {
return $('.js-todos-count').text();
@@ -17,9 +15,9 @@
$(document).trigger('todo:toggle', newCount);
}
- fixture.preload(fixtureTemplate);
+ preloadFixtures(fixtureTemplate);
beforeEach(() => {
- fixture.load(fixtureTemplate);
+ loadFixtures(fixtureTemplate);
new global.Sidebar();
});
@@ -36,5 +34,4 @@
expect(todosCountText()).toEqual('1,000,000');
});
});
-
})(window.gl);
diff --git a/spec/javascripts/datetime_utility_spec.js.es6 b/spec/javascripts/datetime_utility_spec.js.es6
index 8ece24555c5..d5eec10be42 100644
--- a/spec/javascripts/datetime_utility_spec.js.es6
+++ b/spec/javascripts/datetime_utility_spec.js.es6
@@ -1,4 +1,4 @@
-//= require lib/utils/datetime_utility
+require('~/lib/utils/datetime_utility');
(() => {
describe('Date time utils', () => {
diff --git a/spec/javascripts/diff_comments_store_spec.js.es6 b/spec/javascripts/diff_comments_store_spec.js.es6
index 18805d26ac0..f956394ef53 100644
--- a/spec/javascripts/diff_comments_store_spec.js.es6
+++ b/spec/javascripts/diff_comments_store_spec.js.es6
@@ -1,15 +1,14 @@
-/* eslint-disable no-extra-semi, jasmine/no-global-setup, dot-notation, jasmine/no-expect-in-setup-teardown, max-len */
+/* eslint-disable jasmine/no-global-setup, dot-notation, jasmine/no-expect-in-setup-teardown, max-len */
/* global CommentsStore */
-//= require vue
-//= require diff_notes/models/discussion
-//= require diff_notes/models/note
-//= require diff_notes/stores/comments
+require('~/diff_notes/models/discussion');
+require('~/diff_notes/models/note');
+require('~/diff_notes/stores/comments');
(() => {
function createDiscussion(noteId = 1, resolved = true) {
CommentsStore.create('a', noteId, true, resolved, 'test');
- };
+ }
beforeEach(() => {
CommentsStore.state = {};
diff --git a/spec/javascripts/environments/environment_actions_spec.js.es6 b/spec/javascripts/environments/environment_actions_spec.js.es6
index 4bae3f30bb5..b1838045a06 100644
--- a/spec/javascripts/environments/environment_actions_spec.js.es6
+++ b/spec/javascripts/environments/environment_actions_spec.js.es6
@@ -1,11 +1,10 @@
-//= require vue
-//= require environments/components/environment_actions
+require('~/environments/components/environment_actions');
describe('Actions Component', () => {
- fixture.preload('environments/element.html');
+ preloadFixtures('static/environments/element.html.raw');
beforeEach(() => {
- fixture.load('environments/element.html');
+ loadFixtures('static/environments/element.html.raw');
});
it('should render a dropdown with the provided actions', () => {
diff --git a/spec/javascripts/environments/environment_external_url_spec.js.es6 b/spec/javascripts/environments/environment_external_url_spec.js.es6
index 9f82567c35b..a6a587e69f5 100644
--- a/spec/javascripts/environments/environment_external_url_spec.js.es6
+++ b/spec/javascripts/environments/environment_external_url_spec.js.es6
@@ -1,10 +1,9 @@
-//= require vue
-//= require environments/components/environment_external_url
+require('~/environments/components/environment_external_url');
describe('External URL Component', () => {
- fixture.preload('environments/element.html');
+ preloadFixtures('static/environments/element.html.raw');
beforeEach(() => {
- fixture.load('environments/element.html');
+ loadFixtures('static/environments/element.html.raw');
});
it('should link to the provided externalUrl prop', () => {
diff --git a/spec/javascripts/environments/environment_item_spec.js.es6 b/spec/javascripts/environments/environment_item_spec.js.es6
index 5d7c6b2411d..d87cc0996c9 100644
--- a/spec/javascripts/environments/environment_item_spec.js.es6
+++ b/spec/javascripts/environments/environment_item_spec.js.es6
@@ -1,11 +1,10 @@
-//= require vue
-//= require timeago
-//= require environments/components/environment_item
+window.timeago = require('vendor/timeago');
+require('~/environments/components/environment_item');
describe('Environment item', () => {
- fixture.preload('environments/table.html');
+ preloadFixtures('static/environments/table.html.raw');
beforeEach(() => {
- fixture.load('environments/table.html');
+ loadFixtures('static/environments/table.html.raw');
});
describe('When item is folder', () => {
@@ -120,7 +119,7 @@ describe('Environment item', () => {
},
],
},
- 'stoppable?': true,
+ 'stop_action?': true,
environment_path: 'root/ci-folders/environments/31',
created_at: '2016-11-07T11:11:16.525Z',
updated_at: '2016-11-10T15:55:58.778Z',
diff --git a/spec/javascripts/environments/environment_rollback_spec.js.es6 b/spec/javascripts/environments/environment_rollback_spec.js.es6
index 77ba0ab38ec..043b8708a6e 100644
--- a/spec/javascripts/environments/environment_rollback_spec.js.es6
+++ b/spec/javascripts/environments/environment_rollback_spec.js.es6
@@ -1,12 +1,12 @@
-//= require vue
-//= require environments/components/environment_rollback
+require('~/environments/components/environment_rollback');
+
describe('Rollback Component', () => {
- fixture.preload('environments/element.html');
+ preloadFixtures('static/environments/element.html.raw');
const retryURL = 'https://gitlab.com/retry';
beforeEach(() => {
- fixture.load('environments/element.html');
+ loadFixtures('static/environments/element.html.raw');
});
it('Should link to the provided retryUrl', () => {
@@ -33,7 +33,6 @@ describe('Rollback Component', () => {
expect(component.$el.querySelector('span').textContent).toContain('Re-deploy');
});
-
it('Should render Rollback label when isLastDeployment is false', () => {
const component = new window.gl.environmentsList.RollbackComponent({
el: document.querySelector('.test-dom-element'),
diff --git a/spec/javascripts/environments/environment_spec.js.es6 b/spec/javascripts/environments/environment_spec.js.es6
new file mode 100644
index 00000000000..87eda136122
--- /dev/null
+++ b/spec/javascripts/environments/environment_spec.js.es6
@@ -0,0 +1,125 @@
+/* global Vue, environment */
+
+require('~/flash');
+require('~/environments/stores/environments_store');
+require('~/environments/components/environment');
+require('./mock_data');
+
+describe('Environment', () => {
+ preloadFixtures('static/environments/environments.html.raw');
+
+ let component;
+
+ beforeEach(() => {
+ loadFixtures('static/environments/environments.html.raw');
+ });
+
+ describe('successfull request', () => {
+ describe('without environments', () => {
+ const environmentsEmptyResponseInterceptor = (request, next) => {
+ next(request.respondWith(JSON.stringify([]), {
+ status: 200,
+ }));
+ };
+
+ beforeEach(() => {
+ Vue.http.interceptors.push(environmentsEmptyResponseInterceptor);
+ });
+
+ afterEach(() => {
+ Vue.http.interceptors = _.without(
+ Vue.http.interceptors, environmentsEmptyResponseInterceptor,
+ );
+ });
+
+ it('should render the empty state', (done) => {
+ component = new gl.environmentsList.EnvironmentsComponent({
+ el: document.querySelector('#environments-list-view'),
+ propsData: {
+ store: gl.environmentsList.EnvironmentsStore.create(),
+ },
+ });
+
+ setTimeout(() => {
+ expect(
+ component.$el.querySelector('.js-new-environment-button').textContent,
+ ).toContain('New Environment');
+
+ expect(
+ component.$el.querySelector('.js-blank-state-title').textContent,
+ ).toContain('You don\'t have any environments right now.');
+
+ done();
+ }, 0);
+ });
+ });
+
+ describe('with environments', () => {
+ const environmentsResponseInterceptor = (request, next) => {
+ next(request.respondWith(JSON.stringify([environment]), {
+ status: 200,
+ }));
+ };
+
+ beforeEach(() => {
+ Vue.http.interceptors.push(environmentsResponseInterceptor);
+ });
+
+ afterEach(() => {
+ Vue.http.interceptors = _.without(
+ Vue.http.interceptors, environmentsResponseInterceptor,
+ );
+ });
+
+ it('should render a table with environments', (done) => {
+ component = new gl.environmentsList.EnvironmentsComponent({
+ el: document.querySelector('#environments-list-view'),
+ propsData: {
+ store: gl.environmentsList.EnvironmentsStore.create(),
+ },
+ });
+
+ setTimeout(() => {
+ expect(
+ component.$el.querySelectorAll('table tbody tr').length,
+ ).toEqual(1);
+ done();
+ }, 0);
+ });
+ });
+ });
+
+ describe('unsuccessfull request', () => {
+ const environmentsErrorResponseInterceptor = (request, next) => {
+ next(request.respondWith(JSON.stringify([]), {
+ status: 500,
+ }));
+ };
+
+ beforeEach(() => {
+ Vue.http.interceptors.push(environmentsErrorResponseInterceptor);
+ });
+
+ afterEach(() => {
+ Vue.http.interceptors = _.without(
+ Vue.http.interceptors, environmentsErrorResponseInterceptor,
+ );
+ });
+
+ it('should render empty state', (done) => {
+ component = new gl.environmentsList.EnvironmentsComponent({
+ el: document.querySelector('#environments-list-view'),
+ propsData: {
+ store: gl.environmentsList.EnvironmentsStore.create(),
+ },
+ });
+
+ setTimeout(() => {
+ expect(
+ component.$el.querySelector('.js-blank-state-title').textContent,
+ ).toContain('You don\'t have any environments right now.');
+ done();
+ }, 0);
+ });
+ });
+});
diff --git a/spec/javascripts/environments/environment_stop_spec.js.es6 b/spec/javascripts/environments/environment_stop_spec.js.es6
index 84a41b2bf46..2dfce5ba824 100644
--- a/spec/javascripts/environments/environment_stop_spec.js.es6
+++ b/spec/javascripts/environments/environment_stop_spec.js.es6
@@ -1,13 +1,13 @@
-//= require vue
-//= require environments/components/environment_stop
+require('~/environments/components/environment_stop');
+
describe('Stop Component', () => {
- fixture.preload('environments/element.html');
+ preloadFixtures('static/environments/element.html.raw');
let stopURL;
let component;
beforeEach(() => {
- fixture.load('environments/element.html');
+ loadFixtures('static/environments/element.html.raw');
stopURL = '/stop';
component = new window.gl.environmentsList.StopComponent({
diff --git a/spec/javascripts/environments/environments_store_spec.js.es6 b/spec/javascripts/environments/environments_store_spec.js.es6
index 17c00acf63e..9a8300d3832 100644
--- a/spec/javascripts/environments/environments_store_spec.js.es6
+++ b/spec/javascripts/environments/environments_store_spec.js.es6
@@ -1,8 +1,7 @@
/* global environmentsList */
-//= require vue
-//= require environments/stores/environments_store
-//= require ./mock_data
+require('~/environments/stores/environments_store');
+require('./mock_data');
(() => {
describe('Store', () => {
diff --git a/spec/javascripts/environments/mock_data.js.es6 b/spec/javascripts/environments/mock_data.js.es6
index 9e16bc3e6a5..80e1cbc6f4d 100644
--- a/spec/javascripts/environments/mock_data.js.es6
+++ b/spec/javascripts/environments/mock_data.js.es6
@@ -1,4 +1,4 @@
-/* eslint-disable no-unused-vars */
+
const environmentsList = [
{
id: 31,
@@ -50,7 +50,7 @@ const environmentsList = [
},
manual_actions: [],
},
- 'stoppable?': true,
+ 'stop_action?': true,
environment_path: '/root/ci-folders/environments/31',
created_at: '2016-11-07T11:11:16.525Z',
updated_at: '2016-11-07T11:11:16.525Z',
@@ -105,7 +105,7 @@ const environmentsList = [
},
manual_actions: [],
},
- 'stoppable?': false,
+ 'stop_action?': false,
environment_path: '/root/ci-folders/environments/31',
created_at: '2016-11-07T11:11:16.525Z',
updated_at: '2016-11-07T11:11:16.525Z',
@@ -116,7 +116,7 @@ const environmentsList = [
state: 'available',
environment_type: 'review',
last_deployment: null,
- 'stoppable?': true,
+ 'stop_action?': true,
environment_path: '/root/ci-folders/environments/31',
created_at: '2016-11-07T11:11:16.525Z',
updated_at: '2016-11-07T11:11:16.525Z',
@@ -127,9 +127,27 @@ const environmentsList = [
state: 'available',
environment_type: 'review',
last_deployment: null,
- 'stoppable?': true,
+ 'stop_action?': true,
environment_path: '/root/ci-folders/environments/31',
created_at: '2016-11-07T11:11:16.525Z',
updated_at: '2016-11-07T11:11:16.525Z',
},
];
+
+window.environmentsList = environmentsList;
+
+const environment = {
+ id: 4,
+ name: 'production',
+ state: 'available',
+ external_url: 'http://production.',
+ environment_type: null,
+ last_deployment: {},
+ 'stop_action?': false,
+ environment_path: '/root/review-app/environments/4',
+ stop_path: '/root/review-app/environments/4/stop',
+ created_at: '2016-12-16T11:51:04.690Z',
+ updated_at: '2016-12-16T12:04:51.133Z',
+};
+
+window.environment = environment;
diff --git a/spec/javascripts/extensions/array_spec.js.es6 b/spec/javascripts/extensions/array_spec.js.es6
index 2ec759c8e80..ba5eb81defc 100644
--- a/spec/javascripts/extensions/array_spec.js.es6
+++ b/spec/javascripts/extensions/array_spec.js.es6
@@ -1,6 +1,6 @@
-/* eslint-disable space-before-function-paren, no-var, padded-blocks */
+/* eslint-disable space-before-function-paren, no-var */
-/*= require extensions/array */
+require('~/extensions/array');
(function() {
describe('Array extensions', function() {
@@ -42,5 +42,4 @@
});
});
});
-
}).call(this);
diff --git a/spec/javascripts/extensions/element_spec.js.es6 b/spec/javascripts/extensions/element_spec.js.es6
index c5b86d35204..2d8a128ed33 100644
--- a/spec/javascripts/extensions/element_spec.js.es6
+++ b/spec/javascripts/extensions/element_spec.js.es6
@@ -1,4 +1,4 @@
-/*= require extensions/element */
+require('~/extensions/element');
(() => {
describe('Element extensions', function () {
diff --git a/spec/javascripts/extensions/jquery_spec.js b/spec/javascripts/extensions/jquery_spec.js
index 76309930f27..c0bb0419814 100644
--- a/spec/javascripts/extensions/jquery_spec.js
+++ b/spec/javascripts/extensions/jquery_spec.js
@@ -1,12 +1,12 @@
-/* eslint-disable space-before-function-paren, no-var, padded-blocks */
+/* eslint-disable space-before-function-paren, no-var */
-/*= require extensions/jquery */
+require('~/extensions/jquery');
(function() {
describe('jQuery extensions', function() {
describe('disable', function() {
beforeEach(function() {
- return fixture.set('<input type="text" />');
+ return setFixtures('<input type="text" />');
});
it('adds the disabled attribute', function() {
var $input;
@@ -23,7 +23,7 @@
});
return describe('enable', function() {
beforeEach(function() {
- return fixture.set('<input type="text" disabled="disabled" class="disabled" />');
+ return setFixtures('<input type="text" disabled="disabled" class="disabled" />');
});
it('removes the disabled attribute', function() {
var $input;
@@ -39,5 +39,4 @@
});
});
});
-
}).call(this);
diff --git a/spec/javascripts/extensions/object_spec.js.es6 b/spec/javascripts/extensions/object_spec.js.es6
index 3b71c255b30..2467ed78459 100644
--- a/spec/javascripts/extensions/object_spec.js.es6
+++ b/spec/javascripts/extensions/object_spec.js.es6
@@ -1,4 +1,4 @@
-/*= require extensions/object */
+require('~/extensions/object');
describe('Object extensions', () => {
describe('assign', () => {
diff --git a/spec/javascripts/filtered_search/dropdown_user_spec.js.es6 b/spec/javascripts/filtered_search/dropdown_user_spec.js.es6
new file mode 100644
index 00000000000..f4b0d60db34
--- /dev/null
+++ b/spec/javascripts/filtered_search/dropdown_user_spec.js.es6
@@ -0,0 +1,75 @@
+require('~/filtered_search/dropdown_utils');
+require('~/filtered_search/filtered_search_tokenizer');
+require('~/filtered_search/filtered_search_dropdown');
+require('~/filtered_search/dropdown_user');
+
+(() => {
+ describe('Dropdown User', () => {
+ describe('getSearchInput', () => {
+ let dropdownUser;
+
+ beforeEach(() => {
+ spyOn(gl.FilteredSearchDropdown.prototype, 'constructor').and.callFake(() => {});
+ spyOn(gl.DropdownUser.prototype, 'getProjectId').and.callFake(() => {});
+ spyOn(gl.DropdownUtils, 'getSearchInput').and.callFake(() => {});
+
+ dropdownUser = new gl.DropdownUser();
+ });
+
+ it('should not return the double quote found in value', () => {
+ spyOn(gl.FilteredSearchTokenizer, 'processTokens').and.returnValue({
+ lastToken: {
+ value: '"johnny appleseed',
+ },
+ });
+
+ expect(dropdownUser.getSearchInput()).toBe('johnny appleseed');
+ });
+
+ it('should not return the single quote found in value', () => {
+ spyOn(gl.FilteredSearchTokenizer, 'processTokens').and.returnValue({
+ lastToken: {
+ value: '\'larry boy',
+ },
+ });
+
+ expect(dropdownUser.getSearchInput()).toBe('larry boy');
+ });
+ });
+
+ describe('config droplabAjaxFilter\'s endpoint', () => {
+ beforeEach(() => {
+ spyOn(gl.FilteredSearchDropdown.prototype, 'constructor').and.callFake(() => {});
+ spyOn(gl.DropdownUser.prototype, 'getProjectId').and.callFake(() => {});
+ });
+
+ it('should return endpoint', () => {
+ window.gon = {
+ relative_url_root: '',
+ };
+ const dropdown = new gl.DropdownUser();
+
+ expect(dropdown.config.droplabAjaxFilter.endpoint).toBe('/autocomplete/users.json');
+ });
+
+ it('should return endpoint when relative_url_root is undefined', () => {
+ const dropdown = new gl.DropdownUser();
+
+ expect(dropdown.config.droplabAjaxFilter.endpoint).toBe('/autocomplete/users.json');
+ });
+
+ it('should return endpoint with relative url when available', () => {
+ window.gon = {
+ relative_url_root: '/gitlab_directory',
+ };
+ const dropdown = new gl.DropdownUser();
+
+ expect(dropdown.config.droplabAjaxFilter.endpoint).toBe('/gitlab_directory/autocomplete/users.json');
+ });
+
+ afterEach(() => {
+ window.gon = {};
+ });
+ });
+ });
+})();
diff --git a/spec/javascripts/filtered_search/dropdown_utils_spec.js.es6 b/spec/javascripts/filtered_search/dropdown_utils_spec.js.es6
new file mode 100644
index 00000000000..1e2d7582d5b
--- /dev/null
+++ b/spec/javascripts/filtered_search/dropdown_utils_spec.js.es6
@@ -0,0 +1,290 @@
+require('~/extensions/array');
+require('~/filtered_search/dropdown_utils');
+require('~/filtered_search/filtered_search_tokenizer');
+require('~/filtered_search/filtered_search_dropdown_manager');
+
+(() => {
+ describe('Dropdown Utils', () => {
+ describe('getEscapedText', () => {
+ it('should return same word when it has no space', () => {
+ const escaped = gl.DropdownUtils.getEscapedText('textWithoutSpace');
+ expect(escaped).toBe('textWithoutSpace');
+ });
+
+ it('should escape with double quotes', () => {
+ let escaped = gl.DropdownUtils.getEscapedText('text with space');
+ expect(escaped).toBe('"text with space"');
+
+ escaped = gl.DropdownUtils.getEscapedText('won\'t fix');
+ expect(escaped).toBe('"won\'t fix"');
+ });
+
+ it('should escape with single quotes', () => {
+ const escaped = gl.DropdownUtils.getEscapedText('won"t fix');
+ expect(escaped).toBe('\'won"t fix\'');
+ });
+
+ it('should escape with single quotes by default', () => {
+ const escaped = gl.DropdownUtils.getEscapedText('won"t\' fix');
+ expect(escaped).toBe('\'won"t\' fix\'');
+ });
+ });
+
+ describe('filterWithSymbol', () => {
+ let input;
+ const item = {
+ title: '@root',
+ };
+
+ beforeEach(() => {
+ setFixtures(`
+ <input type="text" id="test" />
+ `);
+
+ input = document.getElementById('test');
+ });
+
+ it('should filter without symbol', () => {
+ input.value = ':roo';
+
+ const updatedItem = gl.DropdownUtils.filterWithSymbol('@', input, item);
+ expect(updatedItem.droplab_hidden).toBe(false);
+ });
+
+ it('should filter with symbol', () => {
+ input.value = '@roo';
+
+ const updatedItem = gl.DropdownUtils.filterWithSymbol('@', input, item);
+ expect(updatedItem.droplab_hidden).toBe(false);
+ });
+
+ it('should filter with colon', () => {
+ input.value = 'roo';
+
+ const updatedItem = gl.DropdownUtils.filterWithSymbol('@', input, item);
+ expect(updatedItem.droplab_hidden).toBe(false);
+ });
+
+ describe('filters multiple word title', () => {
+ const multipleWordItem = {
+ title: 'Community Contributions',
+ };
+
+ it('should filter with double quote', () => {
+ input.value = 'label:"';
+
+ const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
+ expect(updatedItem.droplab_hidden).toBe(false);
+ });
+
+ it('should filter with double quote and symbol', () => {
+ input.value = 'label:~"';
+
+ const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
+ expect(updatedItem.droplab_hidden).toBe(false);
+ });
+
+ it('should filter with double quote and multiple words', () => {
+ input.value = 'label:"community con';
+
+ const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
+ expect(updatedItem.droplab_hidden).toBe(false);
+ });
+
+ it('should filter with double quote, symbol and multiple words', () => {
+ input.value = 'label:~"community con';
+
+ const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
+ expect(updatedItem.droplab_hidden).toBe(false);
+ });
+
+ it('should filter with single quote', () => {
+ input.value = 'label:\'';
+
+ const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
+ expect(updatedItem.droplab_hidden).toBe(false);
+ });
+
+ it('should filter with single quote and symbol', () => {
+ input.value = 'label:~\'';
+
+ const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
+ expect(updatedItem.droplab_hidden).toBe(false);
+ });
+
+ it('should filter with single quote and multiple words', () => {
+ input.value = 'label:\'community con';
+
+ const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
+ expect(updatedItem.droplab_hidden).toBe(false);
+ });
+
+ it('should filter with single quote, symbol and multiple words', () => {
+ input.value = 'label:~\'community con';
+
+ const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
+ expect(updatedItem.droplab_hidden).toBe(false);
+ });
+ });
+ });
+
+ describe('filterHint', () => {
+ let input;
+
+ beforeEach(() => {
+ setFixtures(`
+ <input type="text" id="test" />
+ `);
+
+ input = document.getElementById('test');
+ });
+
+ it('should filter', () => {
+ input.value = 'l';
+ let updatedItem = gl.DropdownUtils.filterHint(input, {
+ hint: 'label',
+ });
+ expect(updatedItem.droplab_hidden).toBe(false);
+
+ input.value = 'o';
+ updatedItem = gl.DropdownUtils.filterHint(input, {
+ hint: 'label',
+ }, 'o');
+ expect(updatedItem.droplab_hidden).toBe(true);
+ });
+
+ it('should return droplab_hidden false when item has no hint', () => {
+ const updatedItem = gl.DropdownUtils.filterHint(input, {}, '');
+ expect(updatedItem.droplab_hidden).toBe(false);
+ });
+ });
+
+ describe('setDataValueIfSelected', () => {
+ beforeEach(() => {
+ spyOn(gl.FilteredSearchDropdownManager, 'addWordToInput')
+ .and.callFake(() => {});
+ });
+
+ it('calls addWordToInput when dataValue exists', () => {
+ const selected = {
+ getAttribute: () => 'value',
+ };
+
+ gl.DropdownUtils.setDataValueIfSelected(null, selected);
+ expect(gl.FilteredSearchDropdownManager.addWordToInput.calls.count()).toEqual(1);
+ });
+
+ it('returns true when dataValue exists', () => {
+ const selected = {
+ getAttribute: () => 'value',
+ };
+
+ const result = gl.DropdownUtils.setDataValueIfSelected(null, selected);
+ expect(result).toBe(true);
+ });
+
+ it('returns false when dataValue does not exist', () => {
+ const selected = {
+ getAttribute: () => null,
+ };
+
+ const result = gl.DropdownUtils.setDataValueIfSelected(null, selected);
+ expect(result).toBe(false);
+ });
+ });
+
+ describe('getInputSelectionPosition', () => {
+ describe('word with trailing spaces', () => {
+ const value = 'label:none ';
+
+ it('should return selectionStart when cursor is at the trailing space', () => {
+ const { left, right } = gl.DropdownUtils.getInputSelectionPosition({
+ selectionStart: 11,
+ value,
+ });
+
+ expect(left).toBe(11);
+ expect(right).toBe(11);
+ });
+
+ it('should return input when cursor is at the start of input', () => {
+ const { left, right } = gl.DropdownUtils.getInputSelectionPosition({
+ selectionStart: 0,
+ value,
+ });
+
+ expect(left).toBe(0);
+ expect(right).toBe(10);
+ });
+
+ it('should return input when cursor is at the middle of input', () => {
+ const { left, right } = gl.DropdownUtils.getInputSelectionPosition({
+ selectionStart: 7,
+ value,
+ });
+
+ expect(left).toBe(0);
+ expect(right).toBe(10);
+ });
+
+ it('should return input when cursor is at the end of input', () => {
+ const { left, right } = gl.DropdownUtils.getInputSelectionPosition({
+ selectionStart: 10,
+ value,
+ });
+
+ expect(left).toBe(0);
+ expect(right).toBe(10);
+ });
+ });
+
+ describe('multiple words', () => {
+ const value = 'label:~"Community Contribution"';
+
+ it('should return input when cursor is after the first word', () => {
+ const { left, right } = gl.DropdownUtils.getInputSelectionPosition({
+ selectionStart: 17,
+ value,
+ });
+
+ expect(left).toBe(0);
+ expect(right).toBe(31);
+ });
+
+ it('should return input when cursor is before the second word', () => {
+ const { left, right } = gl.DropdownUtils.getInputSelectionPosition({
+ selectionStart: 18,
+ value,
+ });
+
+ expect(left).toBe(0);
+ expect(right).toBe(31);
+ });
+ });
+
+ describe('incomplete multiple words', () => {
+ const value = 'label:~"Community Contribution';
+
+ it('should return entire input when cursor is at the start of input', () => {
+ const { left, right } = gl.DropdownUtils.getInputSelectionPosition({
+ selectionStart: 0,
+ value,
+ });
+
+ expect(left).toBe(0);
+ expect(right).toBe(30);
+ });
+
+ it('should return entire input when cursor is at the end of input', () => {
+ const { left, right } = gl.DropdownUtils.getInputSelectionPosition({
+ selectionStart: 30,
+ value,
+ });
+
+ expect(left).toBe(0);
+ expect(right).toBe(30);
+ });
+ });
+ });
+ });
+})();
diff --git a/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js.es6 b/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js.es6
new file mode 100644
index 00000000000..ed0b0196ec4
--- /dev/null
+++ b/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js.es6
@@ -0,0 +1,59 @@
+require('~/extensions/array');
+require('~/filtered_search/filtered_search_tokenizer');
+require('~/filtered_search/filtered_search_dropdown_manager');
+
+(() => {
+ describe('Filtered Search Dropdown Manager', () => {
+ describe('addWordToInput', () => {
+ function getInputValue() {
+ return document.querySelector('.filtered-search').value;
+ }
+
+ function setInputValue(value) {
+ document.querySelector('.filtered-search').value = value;
+ }
+
+ beforeEach(() => {
+ const input = document.createElement('input');
+ input.classList.add('filtered-search');
+ document.body.appendChild(input);
+ });
+
+ afterEach(() => {
+ document.querySelector('.filtered-search').outerHTML = '';
+ });
+
+ describe('input has no existing value', () => {
+ it('should add just tokenName', () => {
+ gl.FilteredSearchDropdownManager.addWordToInput('milestone');
+ expect(getInputValue()).toBe('milestone:');
+ });
+
+ it('should add tokenName and tokenValue', () => {
+ gl.FilteredSearchDropdownManager.addWordToInput('label', 'none');
+ expect(getInputValue()).toBe('label:none ');
+ });
+ });
+
+ describe('input has existing value', () => {
+ it('should be able to just add tokenName', () => {
+ setInputValue('a');
+ gl.FilteredSearchDropdownManager.addWordToInput('author');
+ expect(getInputValue()).toBe('author:');
+ });
+
+ it('should replace tokenValue', () => {
+ setInputValue('author:roo');
+ gl.FilteredSearchDropdownManager.addWordToInput('author', '@root');
+ expect(getInputValue()).toBe('author:@root ');
+ });
+
+ it('should add tokenValues containing spaces', () => {
+ setInputValue('label:~"test');
+ gl.FilteredSearchDropdownManager.addWordToInput('label', '~\'"test me"\'');
+ expect(getInputValue()).toBe('label:~\'"test me"\' ');
+ });
+ });
+ });
+ });
+})();
diff --git a/spec/javascripts/filtered_search/filtered_search_manager_spec.js.es6 b/spec/javascripts/filtered_search/filtered_search_manager_spec.js.es6
new file mode 100644
index 00000000000..98959dda242
--- /dev/null
+++ b/spec/javascripts/filtered_search/filtered_search_manager_spec.js.es6
@@ -0,0 +1,67 @@
+require('~/lib/utils/url_utility');
+require('~/lib/utils/common_utils');
+require('~/filtered_search/filtered_search_token_keys');
+require('~/filtered_search/filtered_search_tokenizer');
+require('~/filtered_search/filtered_search_dropdown_manager');
+require('~/filtered_search/filtered_search_manager');
+
+(() => {
+ describe('Filtered Search Manager', () => {
+ describe('search', () => {
+ let manager;
+ const defaultParams = '?scope=all&utf8=✓&state=opened';
+
+ function getInput() {
+ return document.querySelector('.filtered-search');
+ }
+
+ beforeEach(() => {
+ setFixtures(`
+ <input type='text' class='filtered-search' />
+ `);
+
+ spyOn(gl.FilteredSearchManager.prototype, 'bindEvents').and.callFake(() => {});
+ spyOn(gl.FilteredSearchManager.prototype, 'cleanup').and.callFake(() => {});
+ spyOn(gl.FilteredSearchManager.prototype, 'loadSearchParamsFromURL').and.callFake(() => {});
+ spyOn(gl.FilteredSearchDropdownManager.prototype, 'setDropdown').and.callFake(() => {});
+ spyOn(gl.utils, 'getParameterByName').and.returnValue(null);
+
+ manager = new gl.FilteredSearchManager();
+ });
+
+ afterEach(() => {
+ getInput().outerHTML = '';
+ });
+
+ it('should search with a single word', () => {
+ getInput().value = 'searchTerm';
+
+ spyOn(gl.utils, 'visitUrl').and.callFake((url) => {
+ expect(url).toEqual(`${defaultParams}&search=searchTerm`);
+ });
+
+ manager.search();
+ });
+
+ it('should search with multiple words', () => {
+ getInput().value = 'awesome search terms';
+
+ spyOn(gl.utils, 'visitUrl').and.callFake((url) => {
+ expect(url).toEqual(`${defaultParams}&search=awesome+search+terms`);
+ });
+
+ manager.search();
+ });
+
+ it('should search with special characters', () => {
+ getInput().value = '~!@#$%^&*()_+{}:<>,.?/';
+
+ spyOn(gl.utils, 'visitUrl').and.callFake((url) => {
+ expect(url).toEqual(`${defaultParams}&search=~!%40%23%24%25%5E%26*()_%2B%7B%7D%3A%3C%3E%2C.%3F%2F`);
+ });
+
+ manager.search();
+ });
+ });
+ });
+})();
diff --git a/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js.es6 b/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js.es6
new file mode 100644
index 00000000000..cf409a7e509
--- /dev/null
+++ b/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js.es6
@@ -0,0 +1,110 @@
+require('~/extensions/array');
+require('~/filtered_search/filtered_search_token_keys');
+
+(() => {
+ describe('Filtered Search Token Keys', () => {
+ describe('get', () => {
+ let tokenKeys;
+
+ beforeEach(() => {
+ tokenKeys = gl.FilteredSearchTokenKeys.get();
+ });
+
+ it('should return tokenKeys', () => {
+ expect(tokenKeys !== null).toBe(true);
+ });
+
+ it('should return tokenKeys as an array', () => {
+ expect(tokenKeys instanceof Array).toBe(true);
+ });
+ });
+
+ describe('getConditions', () => {
+ let conditions;
+
+ beforeEach(() => {
+ conditions = gl.FilteredSearchTokenKeys.getConditions();
+ });
+
+ it('should return conditions', () => {
+ expect(conditions !== null).toBe(true);
+ });
+
+ it('should return conditions as an array', () => {
+ expect(conditions instanceof Array).toBe(true);
+ });
+ });
+
+ describe('searchByKey', () => {
+ it('should return null when key not found', () => {
+ const tokenKey = gl.FilteredSearchTokenKeys.searchByKey('notakey');
+ expect(tokenKey === null).toBe(true);
+ });
+
+ it('should return tokenKey when found by key', () => {
+ const tokenKeys = gl.FilteredSearchTokenKeys.get();
+ const result = gl.FilteredSearchTokenKeys.searchByKey(tokenKeys[0].key);
+ expect(result).toEqual(tokenKeys[0]);
+ });
+ });
+
+ describe('searchBySymbol', () => {
+ it('should return null when symbol not found', () => {
+ const tokenKey = gl.FilteredSearchTokenKeys.searchBySymbol('notasymbol');
+ expect(tokenKey === null).toBe(true);
+ });
+
+ it('should return tokenKey when found by symbol', () => {
+ const tokenKeys = gl.FilteredSearchTokenKeys.get();
+ const result = gl.FilteredSearchTokenKeys.searchBySymbol(tokenKeys[0].symbol);
+ expect(result).toEqual(tokenKeys[0]);
+ });
+ });
+
+ describe('searchByKeyParam', () => {
+ it('should return null when key param not found', () => {
+ const tokenKey = gl.FilteredSearchTokenKeys.searchByKeyParam('notakeyparam');
+ expect(tokenKey === null).toBe(true);
+ });
+
+ it('should return tokenKey when found by key param', () => {
+ const tokenKeys = gl.FilteredSearchTokenKeys.get();
+ const result = gl.FilteredSearchTokenKeys.searchByKeyParam(`${tokenKeys[0].key}_${tokenKeys[0].param}`);
+ expect(result).toEqual(tokenKeys[0]);
+ });
+
+ it('should return alternative tokenKey when found by key param', () => {
+ const tokenKeys = gl.FilteredSearchTokenKeys.getAlternatives();
+ const result = gl.FilteredSearchTokenKeys.searchByKeyParam(`${tokenKeys[0].key}_${tokenKeys[0].param}`);
+ expect(result).toEqual(tokenKeys[0]);
+ });
+ });
+
+ describe('searchByConditionUrl', () => {
+ it('should return null when condition url not found', () => {
+ const condition = gl.FilteredSearchTokenKeys.searchByConditionUrl(null);
+ expect(condition === null).toBe(true);
+ });
+
+ it('should return condition when found by url', () => {
+ const conditions = gl.FilteredSearchTokenKeys.getConditions();
+ const result = gl.FilteredSearchTokenKeys.searchByConditionUrl(conditions[0].url);
+ expect(result).toBe(conditions[0]);
+ });
+ });
+
+ describe('searchByConditionKeyValue', () => {
+ it('should return null when condition tokenKey and value not found', () => {
+ const condition = gl.FilteredSearchTokenKeys.searchByConditionKeyValue(null, null);
+ expect(condition === null).toBe(true);
+ });
+
+ it('should return condition when found by tokenKey and value', () => {
+ const conditions = gl.FilteredSearchTokenKeys.getConditions();
+ const result = gl.FilteredSearchTokenKeys
+ .searchByConditionKeyValue(conditions[0].tokenKey, conditions[0].value);
+ expect(result).toEqual(conditions[0]);
+ });
+ });
+ });
+})();
diff --git a/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js.es6 b/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js.es6
new file mode 100644
index 00000000000..84c0e9cbfe2
--- /dev/null
+++ b/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js.es6
@@ -0,0 +1,104 @@
+require('~/extensions/array');
+require('~/filtered_search/filtered_search_token_keys');
+require('~/filtered_search/filtered_search_tokenizer');
+
+(() => {
+ describe('Filtered Search Tokenizer', () => {
+ describe('processTokens', () => {
+ it('returns for input containing only search value', () => {
+ const results = gl.FilteredSearchTokenizer.processTokens('searchTerm');
+ expect(results.searchToken).toBe('searchTerm');
+ expect(results.tokens.length).toBe(0);
+ expect(results.lastToken).toBe(results.searchToken);
+ });
+
+ it('returns for input containing only tokens', () => {
+ const results = gl.FilteredSearchTokenizer
+ .processTokens('author:@root label:~"Very Important" milestone:%v1.0 assignee:none');
+ expect(results.searchToken).toBe('');
+ expect(results.tokens.length).toBe(4);
+ expect(results.tokens[3]).toBe(results.lastToken);
+
+ expect(results.tokens[0].key).toBe('author');
+ expect(results.tokens[0].value).toBe('root');
+ expect(results.tokens[0].symbol).toBe('@');
+
+ expect(results.tokens[1].key).toBe('label');
+ expect(results.tokens[1].value).toBe('"Very Important"');
+ expect(results.tokens[1].symbol).toBe('~');
+
+ expect(results.tokens[2].key).toBe('milestone');
+ expect(results.tokens[2].value).toBe('v1.0');
+ expect(results.tokens[2].symbol).toBe('%');
+
+ expect(results.tokens[3].key).toBe('assignee');
+ expect(results.tokens[3].value).toBe('none');
+ expect(results.tokens[3].symbol).toBe('');
+ });
+
+ it('returns for input starting with search value and ending with tokens', () => {
+ const results = gl.FilteredSearchTokenizer
+ .processTokens('searchTerm anotherSearchTerm milestone:none');
+ expect(results.searchToken).toBe('searchTerm anotherSearchTerm');
+ expect(results.tokens.length).toBe(1);
+ expect(results.tokens[0]).toBe(results.lastToken);
+ expect(results.tokens[0].key).toBe('milestone');
+ expect(results.tokens[0].value).toBe('none');
+ expect(results.tokens[0].symbol).toBe('');
+ });
+
+ it('returns for input starting with tokens and ending with search value', () => {
+ const results = gl.FilteredSearchTokenizer
+ .processTokens('assignee:@user searchTerm');
+
+ expect(results.searchToken).toBe('searchTerm');
+ expect(results.tokens.length).toBe(1);
+ expect(results.tokens[0].key).toBe('assignee');
+ expect(results.tokens[0].value).toBe('user');
+ expect(results.tokens[0].symbol).toBe('@');
+ expect(results.lastToken).toBe(results.searchToken);
+ });
+
+ it('returns for input containing search value wrapped between tokens', () => {
+ const results = gl.FilteredSearchTokenizer
+ .processTokens('author:@root label:~"Won\'t fix" searchTerm anotherSearchTerm milestone:none');
+
+ expect(results.searchToken).toBe('searchTerm anotherSearchTerm');
+ expect(results.tokens.length).toBe(3);
+ expect(results.tokens[2]).toBe(results.lastToken);
+
+ expect(results.tokens[0].key).toBe('author');
+ expect(results.tokens[0].value).toBe('root');
+ expect(results.tokens[0].symbol).toBe('@');
+
+ expect(results.tokens[1].key).toBe('label');
+ expect(results.tokens[1].value).toBe('"Won\'t fix"');
+ expect(results.tokens[1].symbol).toBe('~');
+
+ expect(results.tokens[2].key).toBe('milestone');
+ expect(results.tokens[2].value).toBe('none');
+ expect(results.tokens[2].symbol).toBe('');
+ });
+
+ it('returns for input containing search value in between tokens', () => {
+ const results = gl.FilteredSearchTokenizer
+ .processTokens('author:@root searchTerm assignee:none anotherSearchTerm label:~Doing');
+ expect(results.searchToken).toBe('searchTerm anotherSearchTerm');
+ expect(results.tokens.length).toBe(3);
+ expect(results.tokens[2]).toBe(results.lastToken);
+
+ expect(results.tokens[0].key).toBe('author');
+ expect(results.tokens[0].value).toBe('root');
+ expect(results.tokens[0].symbol).toBe('@');
+
+ expect(results.tokens[1].key).toBe('assignee');
+ expect(results.tokens[1].value).toBe('none');
+ expect(results.tokens[1].symbol).toBe('');
+
+ expect(results.tokens[2].key).toBe('label');
+ expect(results.tokens[2].value).toBe('Doing');
+ expect(results.tokens[2].symbol).toBe('~');
+ });
+ });
+ });
+})();
diff --git a/spec/javascripts/fixtures/emoji_menu.js b/spec/javascripts/fixtures/emoji_menu.js
index 3d776bb9277..2ef242901e8 100644
--- a/spec/javascripts/fixtures/emoji_menu.js
+++ b/spec/javascripts/fixtures/emoji_menu.js
@@ -1,5 +1,4 @@
-/* eslint-disable space-before-function-paren, padded-blocks */
+/* eslint-disable space-before-function-paren */
(function() {
window.emojiMenu = "<div class='emoji-menu'>\n <input type=\"text\" name=\"emoji_search\" id=\"emoji_search\" value=\"\" class=\"emoji-search search-input form-control\" />\n <div class='emoji-menu-content'>\n <h5 class='emoji-menu-title'>\n Emoticons\n </h5>\n <ul class='clearfix emoji-menu-list'>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F47D\" title=\"alien\" data-aliases=\"\" data-emoji=\"alien\" data-unicode-name=\"1F47D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F47C\" title=\"angel\" data-aliases=\"\" data-emoji=\"angel\" data-unicode-name=\"1F47C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A2\" title=\"anger\" data-aliases=\"\" data-emoji=\"anger\" data-unicode-name=\"1F4A2\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F620\" title=\"angry\" data-aliases=\"\" data-emoji=\"angry\" data-unicode-name=\"1F620\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F627\" title=\"anguished\" data-aliases=\"\" data-emoji=\"anguished\" data-unicode-name=\"1F627\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F632\" title=\"astonished\" data-aliases=\"\" data-emoji=\"astonished\" data-unicode-name=\"1F632\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45F\" title=\"athletic_shoe\" data-aliases=\"\" data-emoji=\"athletic_shoe\" data-unicode-name=\"1F45F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F476\" title=\"baby\" data-aliases=\"\" data-emoji=\"baby\" data-unicode-name=\"1F476\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F459\" title=\"bikini\" data-aliases=\"\" data-emoji=\"bikini\" data-unicode-name=\"1F459\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F499\" title=\"blue_heart\" data-aliases=\"\" data-emoji=\"blue_heart\" data-unicode-name=\"1F499\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60A\" title=\"blush\" data-aliases=\"\" data-emoji=\"blush\" data-unicode-name=\"1F60A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A5\" title=\"boom\" data-aliases=\"\" data-emoji=\"boom\" data-unicode-name=\"1F4A5\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F462\" title=\"boot\" data-aliases=\"\" data-emoji=\"boot\" data-unicode-name=\"1F462\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F647\" title=\"bow\" data-aliases=\"\" data-emoji=\"bow\" data-unicode-name=\"1F647\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F466\" title=\"boy\" data-aliases=\"\" data-emoji=\"boy\" data-unicode-name=\"1F466\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F470\" title=\"bride_with_veil\" data-aliases=\"\" data-emoji=\"bride_with_veil\" data-unicode-name=\"1F470\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4BC\" title=\"briefcase\" data-aliases=\"\" data-emoji=\"briefcase\" data-unicode-name=\"1F4BC\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F494\" title=\"broken_heart\" data-aliases=\"\" data-emoji=\"broken_heart\" data-unicode-name=\"1F494\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F464\" title=\"bust_in_silhouette\" data-aliases=\"\" data-emoji=\"bust_in_silhouette\" data-unicode-name=\"1F464\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F465\" title=\"busts_in_silhouette\" data-aliases=\"\" data-emoji=\"busts_in_silhouette\" data-unicode-name=\"1F465\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44F\" title=\"clap\" data-aliases=\"\" data-emoji=\"clap\" data-unicode-name=\"1F44F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F302\" title=\"closed_umbrella\" data-aliases=\"\" data-emoji=\"closed_umbrella\" data-unicode-name=\"1F302\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F630\" title=\"cold_sweat\" data-aliases=\"\" data-emoji=\"cold_sweat\" data-unicode-name=\"1F630\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F616\" title=\"confounded\" data-aliases=\"\" data-emoji=\"confounded\" data-unicode-name=\"1F616\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F615\" title=\"confused\" data-aliases=\"\" data-emoji=\"confused\" data-unicode-name=\"1F615\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F477\" title=\"construction_worker\" data-aliases=\"\" data-emoji=\"construction_worker\" data-unicode-name=\"1F477\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F46E\" title=\"cop\" data-aliases=\"\" data-emoji=\"cop\" data-unicode-name=\"1F46E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F46B\" title=\"couple\" data-aliases=\"\" data-emoji=\"couple\" data-unicode-name=\"1F46B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F491\" title=\"couple_with_heart\" data-aliases=\"\" data-emoji=\"couple_with_heart\" data-unicode-name=\"1F491\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F48F\" title=\"couplekiss\" data-aliases=\"\" data-emoji=\"couplekiss\" data-unicode-name=\"1F48F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F451\" title=\"crown\" data-aliases=\"\" data-emoji=\"crown\" data-unicode-name=\"1F451\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F622\" title=\"cry\" data-aliases=\"\" data-emoji=\"cry\" data-unicode-name=\"1F622\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63F\" title=\"crying_cat_face\" data-aliases=\"\" data-emoji=\"crying_cat_face\" data-unicode-name=\"1F63F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F498\" title=\"cupid\" data-aliases=\"\" data-emoji=\"cupid\" data-unicode-name=\"1F498\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F483\" title=\"dancer\" data-aliases=\"\" data-emoji=\"dancer\" data-unicode-name=\"1F483\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F46F\" title=\"dancers\" data-aliases=\"\" data-emoji=\"dancers\" data-unicode-name=\"1F46F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A8\" title=\"dash\" data-aliases=\"\" data-emoji=\"dash\" data-unicode-name=\"1F4A8\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F61E\" title=\"disappointed\" data-aliases=\"\" data-emoji=\"disappointed\" data-unicode-name=\"1F61E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F625\" title=\"disappointed_relieved\" data-aliases=\"\" data-emoji=\"disappointed_relieved\" data-unicode-name=\"1F625\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4AB\" title=\"dizzy\" data-aliases=\"\" data-emoji=\"dizzy\" data-unicode-name=\"1F4AB\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F635\" title=\"dizzy_face\" data-aliases=\"\" data-emoji=\"dizzy_face\" data-unicode-name=\"1F635\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F457\" title=\"dress\" data-aliases=\"\" data-emoji=\"dress\" data-unicode-name=\"1F457\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A7\" title=\"droplet\" data-aliases=\"\" data-emoji=\"droplet\" data-unicode-name=\"1F4A7\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F442\" title=\"ear\" data-aliases=\"\" data-emoji=\"ear\" data-unicode-name=\"1F442\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F611\" title=\"expressionless\" data-aliases=\"\" data-emoji=\"expressionless\" data-unicode-name=\"1F611\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F453\" title=\"eyeglasses\" data-aliases=\"\" data-emoji=\"eyeglasses\" data-unicode-name=\"1F453\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F440\" title=\"eyes\" data-aliases=\"\" data-emoji=\"eyes\" data-unicode-name=\"1F440\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F46A\" title=\"family\" data-aliases=\"\" data-emoji=\"family\" data-unicode-name=\"1F46A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F628\" title=\"fearful\" data-aliases=\"\" data-emoji=\"fearful\" data-unicode-name=\"1F628\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F525\" title=\"fire\" data-aliases=\":flame:\" data-emoji=\"fire\" data-unicode-name=\"1F525\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-270A\" title=\"fist\" data-aliases=\"\" data-emoji=\"fist\" data-unicode-name=\"270A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F633\" title=\"flushed\" data-aliases=\"\" data-emoji=\"flushed\" data-unicode-name=\"1F633\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F463\" title=\"footprints\" data-aliases=\"\" data-emoji=\"footprints\" data-unicode-name=\"1F463\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F626\" title=\"frowning\" data-aliases=\":anguished:\" data-emoji=\"frowning\" data-unicode-name=\"1F626\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F48E\" title=\"gem\" data-aliases=\"\" data-emoji=\"gem\" data-unicode-name=\"1F48E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F467\" title=\"girl\" data-aliases=\"\" data-emoji=\"girl\" data-unicode-name=\"1F467\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F49A\" title=\"green_heart\" data-aliases=\"\" data-emoji=\"green_heart\" data-unicode-name=\"1F49A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62C\" title=\"grimacing\" data-aliases=\"\" data-emoji=\"grimacing\" data-unicode-name=\"1F62C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F601\" title=\"grin\" data-aliases=\"\" data-emoji=\"grin\" data-unicode-name=\"1F601\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F600\" title=\"grinning\" data-aliases=\"\" data-emoji=\"grinning\" data-unicode-name=\"1F600\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F482\" title=\"guardsman\" data-aliases=\"\" data-emoji=\"guardsman\" data-unicode-name=\"1F482\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F487\" title=\"haircut\" data-aliases=\"\" data-emoji=\"haircut\" data-unicode-name=\"1F487\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45C\" title=\"handbag\" data-aliases=\"\" data-emoji=\"handbag\" data-unicode-name=\"1F45C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F649\" title=\"hear_no_evil\" data-aliases=\"\" data-emoji=\"hear_no_evil\" data-unicode-name=\"1F649\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-2764\" title=\"heart\" data-aliases=\"\" data-emoji=\"heart\" data-unicode-name=\"2764\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60D\" title=\"heart_eyes\" data-aliases=\"\" data-emoji=\"heart_eyes\" data-unicode-name=\"1F60D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63B\" title=\"heart_eyes_cat\" data-aliases=\"\" data-emoji=\"heart_eyes_cat\" data-unicode-name=\"1F63B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F493\" title=\"heartbeat\" data-aliases=\"\" data-emoji=\"heartbeat\" data-unicode-name=\"1F493\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F497\" title=\"heartpulse\" data-aliases=\"\" data-emoji=\"heartpulse\" data-unicode-name=\"1F497\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F460\" title=\"high_heel\" data-aliases=\"\" data-emoji=\"high_heel\" data-unicode-name=\"1F460\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62F\" title=\"hushed\" data-aliases=\"\" data-emoji=\"hushed\" data-unicode-name=\"1F62F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F47F\" title=\"imp\" data-aliases=\"\" data-emoji=\"imp\" data-unicode-name=\"1F47F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F481\" title=\"information_desk_person\" data-aliases=\"\" data-emoji=\"information_desk_person\" data-unicode-name=\"1F481\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F607\" title=\"innocent\" data-aliases=\"\" data-emoji=\"innocent\" data-unicode-name=\"1F607\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F47A\" title=\"japanese_goblin\" data-aliases=\"\" data-emoji=\"japanese_goblin\" data-unicode-name=\"1F47A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F479\" title=\"japanese_ogre\" data-aliases=\"\" data-emoji=\"japanese_ogre\" data-unicode-name=\"1F479\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F456\" title=\"jeans\" data-aliases=\"\" data-emoji=\"jeans\" data-unicode-name=\"1F456\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F602\" title=\"joy\" data-aliases=\"\" data-emoji=\"joy\" data-unicode-name=\"1F602\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F639\" title=\"joy_cat\" data-aliases=\"\" data-emoji=\"joy_cat\" data-unicode-name=\"1F639\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F458\" title=\"kimono\" data-aliases=\"\" data-emoji=\"kimono\" data-unicode-name=\"1F458\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F48B\" title=\"kiss\" data-aliases=\"\" data-emoji=\"kiss\" data-unicode-name=\"1F48B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F617\" title=\"kissing\" data-aliases=\"\" data-emoji=\"kissing\" data-unicode-name=\"1F617\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63D\" title=\"kissing_cat\" data-aliases=\"\" data-emoji=\"kissing_cat\" data-unicode-name=\"1F63D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F61A\" title=\"kissing_closed_eyes\" data-aliases=\"\" data-emoji=\"kissing_closed_eyes\" data-unicode-name=\"1F61A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F618\" title=\"kissing_heart\" data-aliases=\"\" data-emoji=\"kissing_heart\" data-unicode-name=\"1F618\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F619\" title=\"kissing_smiling_eyes\" data-aliases=\"\" data-emoji=\"kissing_smiling_eyes\" data-unicode-name=\"1F619\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F606\" title=\"laughing\" data-aliases=\":satisfied:\" data-emoji=\"laughing\" data-unicode-name=\"1F606\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F444\" title=\"lips\" data-aliases=\"\" data-emoji=\"lips\" data-unicode-name=\"1F444\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F484\" title=\"lipstick\" data-aliases=\"\" data-emoji=\"lipstick\" data-unicode-name=\"1F484\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F48C\" title=\"love_letter\" data-aliases=\"\" data-emoji=\"love_letter\" data-unicode-name=\"1F48C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F468\" title=\"man\" data-aliases=\"\" data-emoji=\"man\" data-unicode-name=\"1F468\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F472\" title=\"man_with_gua_pi_mao\" data-aliases=\"\" data-emoji=\"man_with_gua_pi_mao\" data-unicode-name=\"1F472\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F473\" title=\"man_with_turban\" data-aliases=\"\" data-emoji=\"man_with_turban\" data-unicode-name=\"1F473\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45E\" title=\"mans_shoe\" data-aliases=\"\" data-emoji=\"mans_shoe\" data-unicode-name=\"1F45E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F637\" title=\"mask\" data-aliases=\"\" data-emoji=\"mask\" data-unicode-name=\"1F637\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F486\" title=\"massage\" data-aliases=\"\" data-emoji=\"massage\" data-unicode-name=\"1F486\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4AA\" title=\"muscle\" data-aliases=\"\" data-emoji=\"muscle\" data-unicode-name=\"1F4AA\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F485\" title=\"nail_care\" data-aliases=\"\" data-emoji=\"nail_care\" data-unicode-name=\"1F485\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F454\" title=\"necktie\" data-aliases=\"\" data-emoji=\"necktie\" data-unicode-name=\"1F454\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F610\" title=\"neutral_face\" data-aliases=\"\" data-emoji=\"neutral_face\" data-unicode-name=\"1F610\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F645\" title=\"no_good\" data-aliases=\"\" data-emoji=\"no_good\" data-unicode-name=\"1F645\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F636\" title=\"no_mouth\" data-aliases=\"\" data-emoji=\"no_mouth\" data-unicode-name=\"1F636\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F443\" title=\"nose\" data-aliases=\"\" data-emoji=\"nose\" data-unicode-name=\"1F443\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44C\" title=\"ok_hand\" data-aliases=\"\" data-emoji=\"ok_hand\" data-unicode-name=\"1F44C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F646\" title=\"ok_woman\" data-aliases=\"\" data-emoji=\"ok_woman\" data-unicode-name=\"1F646\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F474\" title=\"older_man\" data-aliases=\"\" data-emoji=\"older_man\" data-unicode-name=\"1F474\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F475\" title=\"older_woman\" data-aliases=\":grandma:\" data-emoji=\"older_woman\" data-unicode-name=\"1F475\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F450\" title=\"open_hands\" data-aliases=\"\" data-emoji=\"open_hands\" data-unicode-name=\"1F450\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62E\" title=\"open_mouth\" data-aliases=\"\" data-emoji=\"open_mouth\" data-unicode-name=\"1F62E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F614\" title=\"pensive\" data-aliases=\"\" data-emoji=\"pensive\" data-unicode-name=\"1F614\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F623\" title=\"persevere\" data-aliases=\"\" data-emoji=\"persevere\" data-unicode-name=\"1F623\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F64D\" title=\"person_frowning\" data-aliases=\"\" data-emoji=\"person_frowning\" data-unicode-name=\"1F64D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F471\" title=\"person_with_blond_hair\" data-aliases=\"\" data-emoji=\"person_with_blond_hair\" data-unicode-name=\"1F471\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F64E\" title=\"person_with_pouting_face\" data-aliases=\"\" data-emoji=\"person_with_pouting_face\" data-unicode-name=\"1F64E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F447\" title=\"point_down\" data-aliases=\"\" data-emoji=\"point_down\" data-unicode-name=\"1F447\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F448\" title=\"point_left\" data-aliases=\"\" data-emoji=\"point_left\" data-unicode-name=\"1F448\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F449\" title=\"point_right\" data-aliases=\"\" data-emoji=\"point_right\" data-unicode-name=\"1F449\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-261D\" title=\"point_up\" data-aliases=\"\" data-emoji=\"point_up\" data-unicode-name=\"261D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F446\" title=\"point_up_2\" data-aliases=\"\" data-emoji=\"point_up_2\" data-unicode-name=\"1F446\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A9\" title=\"poop\" data-aliases=\":shit: :hankey: :poo:\" data-emoji=\"poop\" data-unicode-name=\"1F4A9\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45D\" title=\"pouch\" data-aliases=\"\" data-emoji=\"pouch\" data-unicode-name=\"1F45D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63E\" title=\"pouting_cat\" data-aliases=\"\" data-emoji=\"pouting_cat\" data-unicode-name=\"1F63E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F64F\" title=\"pray\" data-aliases=\"\" data-emoji=\"pray\" data-unicode-name=\"1F64F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F478\" title=\"princess\" data-aliases=\"\" data-emoji=\"princess\" data-unicode-name=\"1F478\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44A\" title=\"punch\" data-aliases=\"\" data-emoji=\"punch\" data-unicode-name=\"1F44A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F49C\" title=\"purple_heart\" data-aliases=\"\" data-emoji=\"purple_heart\" data-unicode-name=\"1F49C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45B\" title=\"purse\" data-aliases=\"\" data-emoji=\"purse\" data-unicode-name=\"1F45B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F621\" title=\"rage\" data-aliases=\"\" data-emoji=\"rage\" data-unicode-name=\"1F621\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-270B\" title=\"raised_hand\" data-aliases=\"\" data-emoji=\"raised_hand\" data-unicode-name=\"270B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F64C\" title=\"raised_hands\" data-aliases=\"\" data-emoji=\"raised_hands\" data-unicode-name=\"1F64C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F64B\" title=\"raising_hand\" data-aliases=\"\" data-emoji=\"raising_hand\" data-unicode-name=\"1F64B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-263A\" title=\"relaxed\" data-aliases=\"\" data-emoji=\"relaxed\" data-unicode-name=\"263A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60C\" title=\"relieved\" data-aliases=\"\" data-emoji=\"relieved\" data-unicode-name=\"1F60C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F49E\" title=\"revolving_hearts\" data-aliases=\"\" data-emoji=\"revolving_hearts\" data-unicode-name=\"1F49E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F380\" title=\"ribbon\" data-aliases=\"\" data-emoji=\"ribbon\" data-unicode-name=\"1F380\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F48D\" title=\"ring\" data-aliases=\"\" data-emoji=\"ring\" data-unicode-name=\"1F48D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F3C3\" title=\"runner\" data-aliases=\"\" data-emoji=\"runner\" data-unicode-name=\"1F3C3\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F3BD\" title=\"running_shirt_with_sash\" data-aliases=\"\" data-emoji=\"running_shirt_with_sash\" data-unicode-name=\"1F3BD\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F461\" title=\"sandal\" data-aliases=\"\" data-emoji=\"sandal\" data-unicode-name=\"1F461\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F631\" title=\"scream\" data-aliases=\"\" data-emoji=\"scream\" data-unicode-name=\"1F631\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F640\" title=\"scream_cat\" data-aliases=\"\" data-emoji=\"scream_cat\" data-unicode-name=\"1F640\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F648\" title=\"see_no_evil\" data-aliases=\"\" data-emoji=\"see_no_evil\" data-unicode-name=\"1F648\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F455\" title=\"shirt\" data-aliases=\"\" data-emoji=\"shirt\" data-unicode-name=\"1F455\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F480\" title=\"skull\" data-aliases=\":skeleton:\" data-emoji=\"skull\" data-unicode-name=\"1F480\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F634\" title=\"sleeping\" data-aliases=\"\" data-emoji=\"sleeping\" data-unicode-name=\"1F634\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62A\" title=\"sleepy\" data-aliases=\"\" data-emoji=\"sleepy\" data-unicode-name=\"1F62A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F604\" title=\"smile\" data-aliases=\"\" data-emoji=\"smile\" data-unicode-name=\"1F604\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F638\" title=\"smile_cat\" data-aliases=\"\" data-emoji=\"smile_cat\" data-unicode-name=\"1F638\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F603\" title=\"smiley\" data-aliases=\"\" data-emoji=\"smiley\" data-unicode-name=\"1F603\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63A\" title=\"smiley_cat\" data-aliases=\"\" data-emoji=\"smiley_cat\" data-unicode-name=\"1F63A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F608\" title=\"smiling_imp\" data-aliases=\"\" data-emoji=\"smiling_imp\" data-unicode-name=\"1F608\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60F\" title=\"smirk\" data-aliases=\"\" data-emoji=\"smirk\" data-unicode-name=\"1F60F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63C\" title=\"smirk_cat\" data-aliases=\"\" data-emoji=\"smirk_cat\" data-unicode-name=\"1F63C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62D\" title=\"sob\" data-aliases=\"\" data-emoji=\"sob\" data-unicode-name=\"1F62D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-2728\" title=\"sparkles\" data-aliases=\"\" data-emoji=\"sparkles\" data-unicode-name=\"2728\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F496\" title=\"sparkling_heart\" data-aliases=\"\" data-emoji=\"sparkling_heart\" data-unicode-name=\"1F496\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F64A\" title=\"speak_no_evil\" data-aliases=\"\" data-emoji=\"speak_no_evil\" data-unicode-name=\"1F64A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4AC\" title=\"speech_balloon\" data-aliases=\"\" data-emoji=\"speech_balloon\" data-unicode-name=\"1F4AC\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F31F\" title=\"star2\" data-aliases=\"\" data-emoji=\"star2\" data-unicode-name=\"1F31F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F61B\" title=\"stuck_out_tongue\" data-aliases=\"\" data-emoji=\"stuck_out_tongue\" data-unicode-name=\"1F61B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F61D\" title=\"stuck_out_tongue_closed_eyes\" data-aliases=\"\" data-emoji=\"stuck_out_tongue_closed_eyes\" data-unicode-name=\"1F61D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F61C\" title=\"stuck_out_tongue_winking_eye\" data-aliases=\"\" data-emoji=\"stuck_out_tongue_winking_eye\" data-unicode-name=\"1F61C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60E\" title=\"sunglasses\" data-aliases=\"\" data-emoji=\"sunglasses\" data-unicode-name=\"1F60E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F613\" title=\"sweat\" data-aliases=\"\" data-emoji=\"sweat\" data-unicode-name=\"1F613\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A6\" title=\"sweat_drops\" data-aliases=\"\" data-emoji=\"sweat_drops\" data-unicode-name=\"1F4A6\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F605\" title=\"sweat_smile\" data-aliases=\"\" data-emoji=\"sweat_smile\" data-unicode-name=\"1F605\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4AD\" title=\"thought_balloon\" data-aliases=\"\" data-emoji=\"thought_balloon\" data-unicode-name=\"1F4AD\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44E\" title=\"thumbsdown\" data-aliases=\":-1:\" data-emoji=\"thumbsdown\" data-unicode-name=\"1F44E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44D\" title=\"thumbsup\" data-aliases=\":+1:\" data-emoji=\"thumbsup\" data-unicode-name=\"1F44D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62B\" title=\"tired_face\" data-aliases=\"\" data-emoji=\"tired_face\" data-unicode-name=\"1F62B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F445\" title=\"tongue\" data-aliases=\"\" data-emoji=\"tongue\" data-unicode-name=\"1F445\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F3A9\" title=\"tophat\" data-aliases=\"\" data-emoji=\"tophat\" data-unicode-name=\"1F3A9\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F624\" title=\"triumph\" data-aliases=\"\" data-emoji=\"triumph\" data-unicode-name=\"1F624\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F495\" title=\"two_hearts\" data-aliases=\"\" data-emoji=\"two_hearts\" data-unicode-name=\"1F495\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F46C\" title=\"two_men_holding_hands\" data-aliases=\"\" data-emoji=\"two_men_holding_hands\" data-unicode-name=\"1F46C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F46D\" title=\"two_women_holding_hands\" data-aliases=\"\" data-emoji=\"two_women_holding_hands\" data-unicode-name=\"1F46D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F612\" title=\"unamused\" data-aliases=\"\" data-emoji=\"unamused\" data-unicode-name=\"1F612\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-270C\" title=\"v\" data-aliases=\"\" data-emoji=\"v\" data-unicode-name=\"270C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F6B6\" title=\"walking\" data-aliases=\"\" data-emoji=\"walking\" data-unicode-name=\"1F6B6\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44B\" title=\"wave\" data-aliases=\"\" data-emoji=\"wave\" data-unicode-name=\"1F44B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F629\" title=\"weary\" data-aliases=\"\" data-emoji=\"weary\" data-unicode-name=\"1F629\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F609\" title=\"wink\" data-aliases=\"\" data-emoji=\"wink\" data-unicode-name=\"1F609\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F469\" title=\"woman\" data-aliases=\"\" data-emoji=\"woman\" data-unicode-name=\"1F469\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45A\" title=\"womans_clothes\" data-aliases=\"\" data-emoji=\"womans_clothes\" data-unicode-name=\"1F45A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F452\" title=\"womans_hat\" data-aliases=\"\" data-emoji=\"womans_hat\" data-unicode-name=\"1F452\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F61F\" title=\"worried\" data-aliases=\"\" data-emoji=\"worried\" data-unicode-name=\"1F61F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F49B\" title=\"yellow_heart\" data-aliases=\"\" data-emoji=\"yellow_heart\" data-unicode-name=\"1F49B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60B\" title=\"yum\" data-aliases=\"\" data-emoji=\"yum\" data-unicode-name=\"1F60B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A4\" title=\"zzz\" data-aliases=\"\" data-emoji=\"zzz\" data-unicode-name=\"1F4A4\"></div>\n </button>\n </li>\n </ul>\n </div>\n</div>";
-
}).call(this);
diff --git a/spec/javascripts/fixtures/environments/environments.html.haml b/spec/javascripts/fixtures/environments/environments.html.haml
index d89bc50c1f0..e6000fbb553 100644
--- a/spec/javascripts/fixtures/environments/environments.html.haml
+++ b/spec/javascripts/fixtures/environments/environments.html.haml
@@ -1,5 +1,5 @@
%div
- #environments-list-view{ data: { environments_data: "https://gitlab.com/foo/environments",
+ #environments-list-view{ data: { environments_data: "foo/environments",
"can-create-deployment" => "true",
"can-read-environment" => "true",
"can-create-environment" => "true",
diff --git a/spec/javascripts/fixtures/environments/table.html.haml b/spec/javascripts/fixtures/environments/table.html.haml
index 1ea1725c561..59edc0396d2 100644
--- a/spec/javascripts/fixtures/environments/table.html.haml
+++ b/spec/javascripts/fixtures/environments/table.html.haml
@@ -3,7 +3,7 @@
%tr
%th Environment
%th Last deployment
- %th Build
+ %th Job
%th Commit
%th
%th
diff --git a/spec/javascripts/fixtures/issuable_filter.html.haml b/spec/javascripts/fixtures/issuable_filter.html.haml
new file mode 100644
index 00000000000..ae745b292e6
--- /dev/null
+++ b/spec/javascripts/fixtures/issuable_filter.html.haml
@@ -0,0 +1,8 @@
+%form.js-filter-form{action: '/user/project/issues?scope=all&state=closed'}
+ %input{id: 'utf8', name: 'utf8', value: '✓'}
+ %input{id: 'check_all_issues', name: 'check_all_issues'}
+ %input{id: 'search', name: 'search'}
+ %input{id: 'author_id', name: 'author_id'}
+ %input{id: 'assignee_id', name: 'assignee_id'}
+ %input{id: 'milestone_title', name: 'milestone_title'}
+ %input{id: 'label_name', name: 'label_name'}
diff --git a/spec/javascripts/fixtures/mini_dropdown_graph.html.haml b/spec/javascripts/fixtures/mini_dropdown_graph.html.haml
index e9bf7568e95..29370b974af 100644
--- a/spec/javascripts/fixtures/mini_dropdown_graph.html.haml
+++ b/spec/javascripts/fixtures/mini_dropdown_graph.html.haml
@@ -1,8 +1,9 @@
-%div.js-builds-dropdown-tests
- %button.dropdown.js-builds-dropdown-button{'data-stage-endpoint' => 'foobar'}
+%div.js-builds-dropdown-tests.dropdown.dropdown.js-mini-pipeline-graph
+ %button.js-builds-dropdown-button{'data-stage-endpoint' => 'foobar', data: { toggle: 'dropdown'} }
Dropdown
- %div.js-builds-dropdown-container
- %div.js-builds-dropdown-list
- %div.js-builds-dropdown-loading.builds-dropdown-loading.hidden
+ %ul.dropdown-menu.mini-pipeline-graph-dropdown-menu.js-builds-dropdown-container
+ .js-builds-dropdown-list.scrollable-menu
+
+ .js-builds-dropdown-loading.builds-dropdown-loading.hidden
%span.fa.fa-spinner.fa-spin
diff --git a/spec/javascripts/fixtures/pipelines_table.html.haml b/spec/javascripts/fixtures/pipelines_table.html.haml
new file mode 100644
index 00000000000..fbe4a434f76
--- /dev/null
+++ b/spec/javascripts/fixtures/pipelines_table.html.haml
@@ -0,0 +1,2 @@
+#commit-pipeline-table-view{ data: { endpoint: "endpoint" } }
+.pipeline-svgs{ data: { "commit_icon_svg": "svg"} }
diff --git a/spec/javascripts/fixtures/project_title.html.haml b/spec/javascripts/fixtures/project_title.html.haml
index 4547feeb212..9d1f7877116 100644
--- a/spec/javascripts/fixtures/project_title.html.haml
+++ b/spec/javascripts/fixtures/project_title.html.haml
@@ -4,7 +4,7 @@
GitLab Org
%a.project-item-select-holder{href: "/gitlab-org/gitlab-test"}
GitLab Test
- %i.fa.chevron-down.dropdown-toggle-caret.js-projects-dropdown-toggle{ "data-toggle" => "dropdown", "data-target" => ".header-content" }
+ %i.fa.chevron-down.dropdown-toggle-caret.js-projects-dropdown-toggle{ "data-toggle" => "dropdown", "data-target" => ".header-content", "data-order-by" => "last_activity_at" }
.js-dropdown-menu-projects
.dropdown-menu.dropdown-select.dropdown-menu-projects
.dropdown-title
diff --git a/spec/javascripts/fixtures/projects.json b/spec/javascripts/fixtures/projects.json
index 4919d77e5a4..4ce7f5c601a 100644
--- a/spec/javascripts/fixtures/projects.json
+++ b/spec/javascripts/fixtures/projects.json
@@ -1 +1,445 @@
-[{"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}}}]
+[{
+ "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/fixtures/projects.rb b/spec/javascripts/fixtures/projects.rb
new file mode 100644
index 00000000000..56513219e1e
--- /dev/null
+++ b/spec/javascripts/fixtures/projects.rb
@@ -0,0 +1,28 @@
+require 'spec_helper'
+
+describe ProjectsController, '(JavaScript fixtures)', type: :controller do
+ include JavaScriptFixturesHelpers
+
+ let(:admin) { create(:admin) }
+ let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
+ let(:project) { create(:project, namespace: namespace, path: 'builds-project') }
+
+ render_views
+
+ before(:all) do
+ clean_frontend_fixtures('projects/')
+ end
+
+ before(:each) do
+ sign_in(admin)
+ end
+
+ it 'projects/dashboard.html.raw' do |example|
+ get :show,
+ namespace_id: project.namespace.to_param,
+ id: project.to_param
+
+ expect(response).to be_success
+ store_frontend_fixture(response, example.description)
+ end
+end
diff --git a/spec/javascripts/fixtures/static_fixtures.rb b/spec/javascripts/fixtures/static_fixtures.rb
new file mode 100644
index 00000000000..4569f16f0ca
--- /dev/null
+++ b/spec/javascripts/fixtures/static_fixtures.rb
@@ -0,0 +1,31 @@
+require 'spec_helper'
+
+describe ApplicationController, '(Static JavaScript fixtures)', type: :controller do
+ include JavaScriptFixturesHelpers
+
+ before(:all) do
+ clean_frontend_fixtures('static/')
+ end
+
+ fixtures_path = File.expand_path(JavaScriptFixturesHelpers::FIXTURE_PATH, Rails.root)
+ haml_fixtures = Dir.glob(File.expand_path('**/*.haml', fixtures_path)).map do |file_path|
+ file_path.sub(/\A#{fixtures_path}#{File::SEPARATOR}/, '')
+ end
+
+ haml_fixtures.each do |template_file_name|
+ it "static/#{template_file_name.sub(/\.haml\z/, '.raw')}" do |example|
+ fixture_file_name = example.description
+ rendered = render_template(template_file_name)
+ store_frontend_fixture(rendered, fixture_file_name)
+ end
+ end
+
+ private
+
+ def render_template(template_file_name)
+ fixture_path = JavaScriptFixturesHelpers::FIXTURE_PATH
+ controller = ApplicationController.new
+ controller.prepend_view_path(fixture_path)
+ controller.render_to_string(template: template_file_name, layout: false)
+ end
+end
diff --git a/spec/javascripts/fixtures/u2f.rb b/spec/javascripts/fixtures/u2f.rb
new file mode 100644
index 00000000000..c9c0b891237
--- /dev/null
+++ b/spec/javascripts/fixtures/u2f.rb
@@ -0,0 +1,43 @@
+require 'spec_helper'
+
+context 'U2F' do
+ include JavaScriptFixturesHelpers
+
+ let(:user) { create(:user, :two_factor_via_u2f) }
+
+ before(:all) do
+ clean_frontend_fixtures('u2f/')
+ end
+
+ describe SessionsController, '(JavaScript fixtures)', type: :controller do
+ render_views
+
+ before do
+ @request.env['devise.mapping'] = Devise.mappings[:user]
+ end
+
+ it 'u2f/authenticate.html.raw' do |example|
+ allow(controller).to receive(:find_user).and_return(user)
+
+ post :create, user: { login: user.username, password: user.password }
+
+ expect(response).to be_success
+ store_frontend_fixture(response, example.description)
+ end
+ end
+
+ describe Profiles::TwoFactorAuthsController, '(JavaScript fixtures)', type: :controller do
+ render_views
+
+ before do
+ sign_in(user)
+ end
+
+ it 'u2f/register.html.raw' do |example|
+ get :show
+
+ expect(response).to be_success
+ store_frontend_fixture(response, example.description)
+ end
+ end
+end
diff --git a/spec/javascripts/fixtures/u2f/authenticate.html.haml b/spec/javascripts/fixtures/u2f/authenticate.html.haml
deleted file mode 100644
index 779d6429a5f..00000000000
--- a/spec/javascripts/fixtures/u2f/authenticate.html.haml
+++ /dev/null
@@ -1 +0,0 @@
-= render partial: "u2f/authenticate", locals: { new_user_session_path: "/users/sign_in", params: {}, resource_name: "user" }
diff --git a/spec/javascripts/fixtures/u2f/register.html.haml b/spec/javascripts/fixtures/u2f/register.html.haml
deleted file mode 100644
index 5ed51be689c..00000000000
--- a/spec/javascripts/fixtures/u2f/register.html.haml
+++ /dev/null
@@ -1,2 +0,0 @@
-- user = FactoryGirl.build(:user, :two_factor_via_otp)
-= render partial: "u2f/register", locals: { create_u2f_profile_two_factor_auth_path: '/profile/two_factor_auth/create_u2f', current_user: user }
diff --git a/spec/javascripts/gfm_auto_complete_spec.js.es6 b/spec/javascripts/gfm_auto_complete_spec.js.es6
new file mode 100644
index 00000000000..c61c32f8a13
--- /dev/null
+++ b/spec/javascripts/gfm_auto_complete_spec.js.es6
@@ -0,0 +1,91 @@
+require('~/gfm_auto_complete');
+require('vendor/jquery.caret');
+require('vendor/jquery.atwho');
+
+const global = window.gl || (window.gl = {});
+const GfmAutoComplete = global.GfmAutoComplete;
+
+describe('GfmAutoComplete', function () {
+ describe('DefaultOptions.sorter', function () {
+ describe('assets loading', function () {
+ beforeEach(function () {
+ spyOn(GfmAutoComplete, 'isLoading').and.returnValue(true);
+
+ this.atwhoInstance = { setting: {} };
+ this.items = [];
+
+ this.sorterValue = GfmAutoComplete.DefaultOptions.sorter
+ .call(this.atwhoInstance, '', this.items);
+ });
+
+ it('should disable highlightFirst', function () {
+ expect(this.atwhoInstance.setting.highlightFirst).toBe(false);
+ });
+
+ it('should return the passed unfiltered items', function () {
+ expect(this.sorterValue).toEqual(this.items);
+ });
+ });
+
+ describe('assets finished loading', function () {
+ beforeEach(function () {
+ spyOn(GfmAutoComplete, 'isLoading').and.returnValue(false);
+ spyOn($.fn.atwho.default.callbacks, 'sorter');
+ });
+
+ it('should enable highlightFirst if alwaysHighlightFirst is set', function () {
+ const atwhoInstance = { setting: { alwaysHighlightFirst: true } };
+
+ GfmAutoComplete.DefaultOptions.sorter.call(atwhoInstance);
+
+ expect(atwhoInstance.setting.highlightFirst).toBe(true);
+ });
+
+ it('should enable highlightFirst if a query is present', function () {
+ const atwhoInstance = { setting: {} };
+
+ GfmAutoComplete.DefaultOptions.sorter.call(atwhoInstance, 'query');
+
+ expect(atwhoInstance.setting.highlightFirst).toBe(true);
+ });
+
+ it('should call the default atwho sorter', function () {
+ const atwhoInstance = { setting: {} };
+
+ const query = 'query';
+ const items = [];
+ const searchKey = 'searchKey';
+
+ GfmAutoComplete.DefaultOptions.sorter.call(atwhoInstance, query, items, searchKey);
+
+ expect($.fn.atwho.default.callbacks.sorter).toHaveBeenCalledWith(query, items, searchKey);
+ });
+ });
+ });
+
+ describe('isLoading', function () {
+ it('should be true with loading data object item', function () {
+ expect(GfmAutoComplete.isLoading({ name: 'loading' })).toBe(true);
+ });
+
+ it('should be true with loading data array', function () {
+ expect(GfmAutoComplete.isLoading(['loading'])).toBe(true);
+ });
+
+ it('should be true with loading data object array', function () {
+ expect(GfmAutoComplete.isLoading([{ name: 'loading' }])).toBe(true);
+ });
+
+ it('should be false with actual array data', function () {
+ expect(GfmAutoComplete.isLoading([
+ { title: 'Foo' },
+ { title: 'Bar' },
+ { title: 'Qux' },
+ ])).toBe(false);
+ });
+
+ it('should be false with actual data item', function () {
+ expect(GfmAutoComplete.isLoading({ title: 'Foo' })).toBe(false);
+ });
+ });
+});
diff --git a/spec/javascripts/gl_dropdown_spec.js.es6 b/spec/javascripts/gl_dropdown_spec.js.es6
index bfaf90e2aee..317f38c5888 100644
--- a/spec/javascripts/gl_dropdown_spec.js.es6
+++ b/spec/javascripts/gl_dropdown_spec.js.es6
@@ -1,11 +1,9 @@
-/* eslint-disable comma-dangle, prefer-const, no-param-reassign, no-plusplus, semi, no-unused-expressions, arrow-spacing, max-len */
-/* global Turbolinks */
+/* eslint-disable comma-dangle, no-param-reassign, no-unused-expressions, max-len */
-/*= require jquery */
-/*= require gl_dropdown */
-/*= require turbolinks */
-/*= require lib/utils/common_utils */
-/*= require lib/utils/type_utility */
+require('~/gl_dropdown');
+require('~/lib/utils/common_utils');
+require('~/lib/utils/type_utility');
+require('~/lib/utils/url_utility');
(() => {
const NON_SELECTABLE_CLASSES = '.divider, .separator, .dropdown-header, .dropdown-menu-empty-link';
@@ -22,7 +20,7 @@
let remoteCallback;
- let navigateWithKeys = function navigateWithKeys(direction, steps, cb, i) {
+ const navigateWithKeys = function navigateWithKeys(direction, steps, cb, i) {
i = i || 0;
if (!i) direction = direction.toUpperCase();
$('body').trigger({
@@ -30,7 +28,7 @@
which: ARROW_KEYS[direction],
keyCode: ARROW_KEYS[direction]
});
- i++;
+ i += 1;
if (i <= steps) {
navigateWithKeys(direction, steps, cb, i);
} else {
@@ -38,19 +36,22 @@
}
};
- let remoteMock = function remoteMock(data, term, callback) {
+ const remoteMock = function remoteMock(data, term, callback) {
remoteCallback = callback.bind({}, data);
- }
+ };
describe('Dropdown', function describeDropdown() {
- fixture.preload('gl_dropdown.html');
- fixture.preload('projects.json');
+ preloadFixtures('static/gl_dropdown.html.raw');
+ loadJSONFixtures('projects.json');
function initDropDown(hasRemote, isFilterable) {
this.dropdownButtonElement = $('#js-project-dropdown', this.dropdownContainerElement).glDropdown({
selectable: true,
filterable: isFilterable,
data: hasRemote ? remoteMock.bind({}, this.projectsData) : this.projectsData,
+ search: {
+ fields: ['name']
+ },
text: (project) => {
(project.name_with_namespace || project.name);
},
@@ -61,10 +62,10 @@
}
beforeEach(() => {
- fixture.load('gl_dropdown.html');
+ loadFixtures('static/gl_dropdown.html.raw');
this.dropdownContainerElement = $('.dropdown.inline');
this.$dropdownMenuElement = $('.dropdown-menu', this.dropdownContainerElement);
- this.projectsData = fixture.load('projects.json')[0];
+ this.projectsData = getJSONFixture('projects.json');
});
afterEach(() => {
@@ -87,7 +88,7 @@
it('should select a following item on DOWN keypress', () => {
expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(0);
- let randomIndex = (Math.floor(Math.random() * (this.projectsData.length - 1)) + 0);
+ const randomIndex = (Math.floor(Math.random() * (this.projectsData.length - 1)) + 0);
navigateWithKeys('down', randomIndex, () => {
expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(1);
expect($(`${ITEM_SELECTOR}:eq(${randomIndex}) a`, this.$dropdownMenuElement)).toHaveClass('is-focused');
@@ -98,7 +99,7 @@
expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(0);
navigateWithKeys('down', (this.projectsData.length - 1), () => {
expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(1);
- let randomIndex = (Math.floor(Math.random() * (this.projectsData.length - 2)) + 0);
+ const randomIndex = (Math.floor(Math.random() * (this.projectsData.length - 2)) + 0);
navigateWithKeys('up', randomIndex, () => {
expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(1);
expect($(`${ITEM_SELECTOR}:eq(${((this.projectsData.length - 2) - randomIndex)}) a`, this.$dropdownMenuElement)).toHaveClass('is-focused');
@@ -107,16 +108,16 @@
});
it('should click the selected item on ENTER keypress', () => {
- expect(this.dropdownContainerElement).toHaveClass('open')
- let randomIndex = Math.floor(Math.random() * (this.projectsData.length - 1)) + 0
+ expect(this.dropdownContainerElement).toHaveClass('open');
+ const randomIndex = Math.floor(Math.random() * (this.projectsData.length - 1)) + 0;
navigateWithKeys('down', randomIndex, () => {
- spyOn(Turbolinks, 'visit').and.stub();
+ spyOn(gl.utils, 'visitUrl').and.stub();
navigateWithKeys('enter', null, () => {
expect(this.dropdownContainerElement).not.toHaveClass('open');
- let link = $(`${ITEM_SELECTOR}:eq(${randomIndex}) a`, this.$dropdownMenuElement);
+ const link = $(`${ITEM_SELECTOR}:eq(${randomIndex}) a`, this.$dropdownMenuElement);
expect(link).toHaveClass('is-active');
- let linkedLocation = link.attr('href');
- if (linkedLocation && linkedLocation !== '#') expect(Turbolinks.visit).toHaveBeenCalledWith(linkedLocation);
+ const linkedLocation = link.attr('href');
+ if (linkedLocation && linkedLocation !== '#') expect(gl.utils.visitUrl).toHaveBeenCalledWith(linkedLocation);
});
});
});
@@ -138,18 +139,18 @@
this.dropdownButtonElement.click();
});
- it('should not focus search input while remote task is not complete', ()=> {
+ it('should not focus search input while remote task is not complete', () => {
expect($(document.activeElement)).not.toEqual($(SEARCH_INPUT_SELECTOR));
remoteCallback();
expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR));
});
- it('should focus search input after remote task is complete', ()=> {
+ it('should focus search input after remote task is complete', () => {
remoteCallback();
expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR));
});
- it('should focus on input when opening for the second time', ()=> {
+ it('should focus on input when opening for the second time', () => {
remoteCallback();
this.dropdownContainerElement.trigger({
type: 'keyup',
@@ -162,11 +163,26 @@
});
describe('input focus with array data', () => {
- it('should focus input when passing array data to drop down', ()=> {
+ it('should focus input when passing array data to drop down', () => {
initDropDown.call(this, false, true);
this.dropdownButtonElement.click();
expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR));
});
});
+
+ it('should still have input value on close and restore', () => {
+ const $searchInput = $(SEARCH_INPUT_SELECTOR);
+ initDropDown.call(this, false, true);
+ $searchInput
+ .trigger('focus')
+ .val('g')
+ .trigger('input');
+ expect($searchInput.val()).toEqual('g');
+ this.dropdownButtonElement.trigger('hidden.bs.dropdown');
+ $searchInput
+ .trigger('blur')
+ .trigger('focus');
+ expect($searchInput.val()).toEqual('g');
+ });
});
})();
diff --git a/spec/javascripts/gl_field_errors_spec.js.es6 b/spec/javascripts/gl_field_errors_spec.js.es6
index 5018e87ad6c..733023481f5 100644
--- a/spec/javascripts/gl_field_errors_spec.js.es6
+++ b/spec/javascripts/gl_field_errors_spec.js.es6
@@ -1,14 +1,13 @@
-/* eslint-disable space-before-function-paren, arrow-body-style, indent, padded-blocks */
+/* eslint-disable space-before-function-paren, arrow-body-style */
-//= require jquery
-//= require gl_field_errors
+require('~/gl_field_errors');
((global) => {
- fixture.preload('gl_field_errors.html');
+ preloadFixtures('static/gl_field_errors.html.raw');
describe('GL Style Field Errors', function() {
beforeEach(function() {
- fixture.load('gl_field_errors.html');
+ loadFixtures('static/gl_field_errors.html.raw');
const $form = this.$form = $('form.gl-show-field-errors');
this.fieldErrors = new global.GlFieldErrors($form);
});
@@ -28,7 +27,7 @@
expect(customErrorElem.length).toBe(1);
const customErrors = this.fieldErrors.state.inputs.filter((input) => {
- return input.inputElement.hasClass(customErrorFlag);
+ return input.inputElement.hasClass(customErrorFlag);
});
expect(customErrors.length).toBe(0);
});
@@ -107,7 +106,5 @@
expect(noTitleErrorElem.text()).toBe('This field is required.');
expect(hasTitleErrorElem.text()).toBe('Please provide a valid email address.');
});
-
});
-
})(window.gl || (window.gl = {}));
diff --git a/spec/javascripts/gl_form_spec.js.es6 b/spec/javascripts/gl_form_spec.js.es6
new file mode 100644
index 00000000000..71d6e2a7e22
--- /dev/null
+++ b/spec/javascripts/gl_form_spec.js.es6
@@ -0,0 +1,123 @@
+/* global autosize */
+
+window.autosize = require('vendor/autosize');
+require('~/gl_form');
+require('~/lib/utils/text_utility');
+require('~/lib/utils/common_utils');
+
+describe('GLForm', () => {
+ const global = window.gl || (window.gl = {});
+ const GLForm = global.GLForm;
+
+ it('should be defined in the global scope', () => {
+ expect(GLForm).toBeDefined();
+ });
+
+ describe('when instantiated', function () {
+ beforeEach((done) => {
+ this.form = $('<form class="gfm-form"><textarea class="js-gfm-input"></form>');
+ this.textarea = this.form.find('textarea');
+ spyOn($.prototype, 'off').and.returnValue(this.textarea);
+ spyOn($.prototype, 'on').and.returnValue(this.textarea);
+ spyOn($.prototype, 'css');
+ spyOn(window, 'autosize');
+
+ this.glForm = new GLForm(this.form);
+ setTimeout(() => {
+ $.prototype.off.calls.reset();
+ $.prototype.on.calls.reset();
+ $.prototype.css.calls.reset();
+ autosize.calls.reset();
+ done();
+ });
+ });
+
+ describe('.setupAutosize', () => {
+ beforeEach((done) => {
+ this.glForm.setupAutosize();
+ setTimeout(() => {
+ done();
+ });
+ });
+
+ it('should register an autosize event handler on the textarea', () => {
+ expect($.prototype.off).toHaveBeenCalledWith('autosize:resized');
+ expect($.prototype.on).toHaveBeenCalledWith('autosize:resized', jasmine.any(Function));
+ });
+
+ it('should register a mouseup event handler on the textarea', () => {
+ expect($.prototype.off).toHaveBeenCalledWith('mouseup.autosize');
+ expect($.prototype.on).toHaveBeenCalledWith('mouseup.autosize', jasmine.any(Function));
+ });
+
+ it('should autosize the textarea', () => {
+ expect(autosize).toHaveBeenCalledWith(jasmine.any(Object));
+ });
+
+ it('should set the resize css property to vertical', () => {
+ expect($.prototype.css).toHaveBeenCalledWith('resize', 'vertical');
+ });
+ });
+
+ describe('.setHeightData', () => {
+ beforeEach(() => {
+ spyOn($.prototype, 'data');
+ spyOn($.prototype, 'outerHeight').and.returnValue(200);
+ this.glForm.setHeightData();
+ });
+
+ it('should set the height data attribute', () => {
+ expect($.prototype.data).toHaveBeenCalledWith('height', 200);
+ });
+
+ it('should call outerHeight', () => {
+ expect($.prototype.outerHeight).toHaveBeenCalled();
+ });
+ });
+
+ describe('.destroyAutosize', () => {
+ describe('when called', () => {
+ beforeEach(() => {
+ spyOn($.prototype, 'data');
+ spyOn($.prototype, 'outerHeight').and.returnValue(200);
+ spyOn(window, 'outerHeight').and.returnValue(400);
+ spyOn(autosize, 'destroy');
+
+ this.glForm.destroyAutosize();
+ });
+
+ it('should call outerHeight', () => {
+ expect($.prototype.outerHeight).toHaveBeenCalled();
+ });
+
+ it('should get data-height attribute', () => {
+ expect($.prototype.data).toHaveBeenCalledWith('height');
+ });
+
+ it('should call autosize destroy', () => {
+ expect(autosize.destroy).toHaveBeenCalledWith(this.textarea);
+ });
+
+ it('should set the data-height attribute', () => {
+ expect($.prototype.data).toHaveBeenCalledWith('height', 200);
+ });
+
+ it('should set the outerHeight', () => {
+ expect($.prototype.outerHeight).toHaveBeenCalledWith(200);
+ });
+
+ it('should set the css', () => {
+ expect($.prototype.css).toHaveBeenCalledWith('max-height', window.outerHeight);
+ });
+ });
+
+ it('should return undefined if the data-height equals the outerHeight', () => {
+ spyOn($.prototype, 'outerHeight').and.returnValue(200);
+ spyOn($.prototype, 'data').and.returnValue(200);
+ spyOn(autosize, 'destroy');
+ expect(this.glForm.destroyAutosize()).toBeUndefined();
+ expect(autosize.destroy).not.toHaveBeenCalled();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js b/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js
index bc5cbeb6a40..a954bb60560 100644
--- a/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js
+++ b/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js
@@ -1,133 +1,130 @@
-/* eslint-disable quotes, indent, semi, object-curly-spacing, jasmine/no-suite-dupes, vars-on-top, no-var, padded-blocks, spaced-comment, max-len */
+/* eslint-disable quotes, jasmine/no-suite-dupes, vars-on-top, no-var, max-len */
/* global d3 */
/* global ContributorsGraph */
/* global ContributorsMasterGraph */
-//= require graphs/stat_graph_contributors_graph
+require('~/graphs/stat_graph_contributors_graph');
describe("ContributorsGraph", function () {
describe("#set_x_domain", function () {
it("set the x_domain", function () {
- ContributorsGraph.set_x_domain(20)
- expect(ContributorsGraph.prototype.x_domain).toEqual(20)
- })
- })
+ ContributorsGraph.set_x_domain(20);
+ expect(ContributorsGraph.prototype.x_domain).toEqual(20);
+ });
+ });
describe("#set_y_domain", function () {
it("sets the y_domain", function () {
- ContributorsGraph.set_y_domain([{commits: 30}])
- expect(ContributorsGraph.prototype.y_domain).toEqual([0, 30])
- })
- })
+ ContributorsGraph.set_y_domain([{ commits: 30 }]);
+ expect(ContributorsGraph.prototype.y_domain).toEqual([0, 30]);
+ });
+ });
describe("#init_x_domain", function () {
it("sets the initial x_domain", function () {
- ContributorsGraph.init_x_domain([{date: "2013-01-31"}, {date: "2012-01-31"}])
- expect(ContributorsGraph.prototype.x_domain).toEqual(["2012-01-31", "2013-01-31"])
- })
- })
+ ContributorsGraph.init_x_domain([{ date: "2013-01-31" }, { date: "2012-01-31" }]);
+ expect(ContributorsGraph.prototype.x_domain).toEqual(["2012-01-31", "2013-01-31"]);
+ });
+ });
describe("#init_y_domain", function () {
it("sets the initial y_domain", function () {
- ContributorsGraph.init_y_domain([{commits: 30}])
- expect(ContributorsGraph.prototype.y_domain).toEqual([0, 30])
- })
- })
+ ContributorsGraph.init_y_domain([{ commits: 30 }]);
+ expect(ContributorsGraph.prototype.y_domain).toEqual([0, 30]);
+ });
+ });
describe("#init_domain", function () {
it("calls init_x_domain and init_y_domain", function () {
- spyOn(ContributorsGraph, "init_x_domain")
- spyOn(ContributorsGraph, "init_y_domain")
- ContributorsGraph.init_domain()
- expect(ContributorsGraph.init_x_domain).toHaveBeenCalled()
- expect(ContributorsGraph.init_y_domain).toHaveBeenCalled()
- })
- })
+ spyOn(ContributorsGraph, "init_x_domain");
+ spyOn(ContributorsGraph, "init_y_domain");
+ ContributorsGraph.init_domain();
+ expect(ContributorsGraph.init_x_domain).toHaveBeenCalled();
+ expect(ContributorsGraph.init_y_domain).toHaveBeenCalled();
+ });
+ });
describe("#set_dates", function () {
it("sets the dates", function () {
- ContributorsGraph.set_dates("2013-12-01")
- expect(ContributorsGraph.prototype.dates).toEqual("2013-12-01")
- })
- })
+ ContributorsGraph.set_dates("2013-12-01");
+ expect(ContributorsGraph.prototype.dates).toEqual("2013-12-01");
+ });
+ });
describe("#set_x_domain", function () {
it("sets the instance's x domain using the prototype's x_domain", function () {
- ContributorsGraph.prototype.x_domain = 20
- var instance = new ContributorsGraph()
- instance.x = d3.time.scale().range([0, 100]).clamp(true)
- spyOn(instance.x, 'domain')
- instance.set_x_domain()
- expect(instance.x.domain).toHaveBeenCalledWith(20)
- })
- })
+ ContributorsGraph.prototype.x_domain = 20;
+ var instance = new ContributorsGraph();
+ instance.x = d3.time.scale().range([0, 100]).clamp(true);
+ spyOn(instance.x, 'domain');
+ instance.set_x_domain();
+ expect(instance.x.domain).toHaveBeenCalledWith(20);
+ });
+ });
describe("#set_y_domain", function () {
it("sets the instance's y domain using the prototype's y_domain", function () {
- ContributorsGraph.prototype.y_domain = 30
- var instance = new ContributorsGraph()
- instance.y = d3.scale.linear().range([100, 0]).nice()
- spyOn(instance.y, 'domain')
- instance.set_y_domain()
- expect(instance.y.domain).toHaveBeenCalledWith(30)
- })
- })
+ ContributorsGraph.prototype.y_domain = 30;
+ var instance = new ContributorsGraph();
+ instance.y = d3.scale.linear().range([100, 0]).nice();
+ spyOn(instance.y, 'domain');
+ instance.set_y_domain();
+ expect(instance.y.domain).toHaveBeenCalledWith(30);
+ });
+ });
describe("#set_domain", function () {
it("calls set_x_domain and set_y_domain", function () {
- var instance = new ContributorsGraph()
- spyOn(instance, 'set_x_domain')
- spyOn(instance, 'set_y_domain')
- instance.set_domain()
- expect(instance.set_x_domain).toHaveBeenCalled()
- expect(instance.set_y_domain).toHaveBeenCalled()
- })
- })
+ var instance = new ContributorsGraph();
+ spyOn(instance, 'set_x_domain');
+ spyOn(instance, 'set_y_domain');
+ instance.set_domain();
+ expect(instance.set_x_domain).toHaveBeenCalled();
+ expect(instance.set_y_domain).toHaveBeenCalled();
+ });
+ });
describe("#set_data", function () {
it("sets the data", function () {
- var instance = new ContributorsGraph()
- instance.set_data("20")
- expect(instance.data).toEqual("20")
- })
- })
-})
+ var instance = new ContributorsGraph();
+ instance.set_data("20");
+ expect(instance.data).toEqual("20");
+ });
+ });
+});
describe("ContributorsMasterGraph", function () {
-
// TODO: fix or remove
- //describe("#process_dates", function () {
- //it("gets and parses dates", function () {
- //var graph = new ContributorsMasterGraph()
- //var data = 'random data here'
- //spyOn(graph, 'parse_dates')
- //spyOn(graph, 'get_dates').andReturn("get")
- //spyOn(ContributorsGraph,'set_dates').andCallThrough()
- //graph.process_dates(data)
- //expect(graph.parse_dates).toHaveBeenCalledWith(data)
- //expect(graph.get_dates).toHaveBeenCalledWith(data)
- //expect(ContributorsGraph.set_dates).toHaveBeenCalledWith("get")
- //})
- //})
+ // describe("#process_dates", function () {
+ // it("gets and parses dates", function () {
+ // var graph = new ContributorsMasterGraph();
+ // var data = 'random data here';
+ // spyOn(graph, 'parse_dates');
+ // spyOn(graph, 'get_dates').andReturn("get");
+ // spyOn(ContributorsGraph,'set_dates').andCallThrough();
+ // graph.process_dates(data);
+ // expect(graph.parse_dates).toHaveBeenCalledWith(data);
+ // expect(graph.get_dates).toHaveBeenCalledWith(data);
+ // expect(ContributorsGraph.set_dates).toHaveBeenCalledWith("get");
+ // });
+ // });
describe("#get_dates", function () {
it("plucks the date field from data collection", function () {
- var graph = new ContributorsMasterGraph()
- var data = [{date: "2013-01-01"}, {date: "2012-12-15"}]
- expect(graph.get_dates(data)).toEqual(["2013-01-01", "2012-12-15"])
- })
- })
+ var graph = new ContributorsMasterGraph();
+ var data = [{ date: "2013-01-01" }, { date: "2012-12-15" }];
+ expect(graph.get_dates(data)).toEqual(["2013-01-01", "2012-12-15"]);
+ });
+ });
describe("#parse_dates", function () {
it("parses the dates", function () {
- var graph = new ContributorsMasterGraph()
- var parseDate = d3.time.format("%Y-%m-%d").parse
- var data = [{date: "2013-01-01"}, {date: "2012-12-15"}]
- var correct = [{date: parseDate(data[0].date)}, {date: parseDate(data[1].date)}]
- graph.parse_dates(data)
- expect(data).toEqual(correct)
- })
- })
-
-
-})
+ var graph = new ContributorsMasterGraph();
+ var parseDate = d3.time.format("%Y-%m-%d").parse;
+ var data = [{ date: "2013-01-01" }, { date: "2012-12-15" }];
+ var correct = [{ date: parseDate(data[0].date) }, { date: parseDate(data[1].date) }];
+ graph.parse_dates(data);
+ expect(data).toEqual(correct);
+ });
+ });
+});
diff --git a/spec/javascripts/graphs/stat_graph_contributors_util_spec.js b/spec/javascripts/graphs/stat_graph_contributors_util_spec.js
index 751f3d175e2..b15764abe8c 100644
--- a/spec/javascripts/graphs/stat_graph_contributors_util_spec.js
+++ b/spec/javascripts/graphs/stat_graph_contributors_util_spec.js
@@ -1,215 +1,218 @@
-/* eslint-disable quotes, padded-blocks, no-var, camelcase, object-curly-spacing, semi, indent, object-property-newline, comma-dangle, comma-spacing, spaced-comment, max-len, key-spacing, vars-on-top, quote-props, no-multi-spaces */
+/* eslint-disable quotes, no-var, camelcase, object-property-newline, comma-dangle, max-len, vars-on-top, quote-props */
/* global ContributorsStatGraphUtil */
-//= require graphs/stat_graph_contributors_util
+require('~/graphs/stat_graph_contributors_util');
describe("ContributorsStatGraphUtil", function () {
-
describe("#parse_log", function () {
it("returns a correctly parsed log", function () {
var fake_log = [
- {author_email: "karlo@email.com", author_name: "Karlo Soriano", date: "2013-05-09", additions: 471},
- {author_email: "dzaporozhets@email.com", author_name: "Dmitriy Zaporozhets", date: "2013-05-08", additions: 6, deletions: 1},
- {author_email: "dzaporozhets@email.com", author_name: "Dmitriy Zaporozhets", date: "2013-05-08", additions: 19, deletions: 3},
- {author_email: "dzaporozhets@email.com", author_name: "Dmitriy Zaporozhets", date: "2013-05-08", additions: 29, deletions: 3}]
+ { author_email: "karlo@email.com", author_name: "Karlo Soriano", date: "2013-05-09", additions: 471 },
+ { author_email: "dzaporozhets@email.com", author_name: "Dmitriy Zaporozhets", date: "2013-05-08", additions: 6, deletions: 1 },
+ { author_email: "dzaporozhets@email.com", author_name: "Dmitriy Zaporozhets", date: "2013-05-08", additions: 19, deletions: 3 },
+ { author_email: "dzaporozhets@email.com", author_name: "Dmitriy Zaporozhets", date: "2013-05-08", additions: 29, deletions: 3 }
+ ];
var correct_parsed_log = {
total: [
- {date: "2013-05-09", additions: 471, deletions: 0, commits: 1},
- {date: "2013-05-08", additions: 54, deletions: 7, commits: 3}],
- by_author:
- [
- {
- author_name: "Karlo Soriano", author_email: "karlo@email.com",
- "2013-05-09": {date: "2013-05-09", additions: 471, deletions: 0, commits: 1}
- },
- {
- author_name: "Dmitriy Zaporozhets",author_email: "dzaporozhets@email.com",
- "2013-05-08": {date: "2013-05-08", additions: 54, deletions: 7, commits: 3}
- }
+ { date: "2013-05-09", additions: 471, deletions: 0, commits: 1 },
+ { date: "2013-05-08", additions: 54, deletions: 7, commits: 3 }
+ ],
+ by_author: [
+ {
+ author_name: "Karlo Soriano", author_email: "karlo@email.com",
+ "2013-05-09": { date: "2013-05-09", additions: 471, deletions: 0, commits: 1 }
+ },
+ {
+ author_name: "Dmitriy Zaporozhets", author_email: "dzaporozhets@email.com",
+ "2013-05-08": { date: "2013-05-08", additions: 54, deletions: 7, commits: 3 }
+ }
]
- }
- expect(ContributorsStatGraphUtil.parse_log(fake_log)).toEqual(correct_parsed_log)
- })
- })
+ };
+ expect(ContributorsStatGraphUtil.parse_log(fake_log)).toEqual(correct_parsed_log);
+ });
+ });
describe("#store_data", function () {
-
- var fake_entry = {author: "Karlo Soriano", date: "2013-05-09", additions: 471}
- var fake_total = {}
- var fake_by_author = {}
+ var fake_entry = { author: "Karlo Soriano", date: "2013-05-09", additions: 471 };
+ var fake_total = {};
+ var fake_by_author = {};
it("calls #store_commits", function () {
- spyOn(ContributorsStatGraphUtil, 'store_commits')
- ContributorsStatGraphUtil.store_data(fake_entry, fake_total, fake_by_author)
- expect(ContributorsStatGraphUtil.store_commits).toHaveBeenCalled()
- })
+ spyOn(ContributorsStatGraphUtil, 'store_commits');
+ ContributorsStatGraphUtil.store_data(fake_entry, fake_total, fake_by_author);
+ expect(ContributorsStatGraphUtil.store_commits).toHaveBeenCalled();
+ });
it("calls #store_additions", function () {
- spyOn(ContributorsStatGraphUtil, 'store_additions')
- ContributorsStatGraphUtil.store_data(fake_entry, fake_total, fake_by_author)
- expect(ContributorsStatGraphUtil.store_additions).toHaveBeenCalled()
- })
+ spyOn(ContributorsStatGraphUtil, 'store_additions');
+ ContributorsStatGraphUtil.store_data(fake_entry, fake_total, fake_by_author);
+ expect(ContributorsStatGraphUtil.store_additions).toHaveBeenCalled();
+ });
it("calls #store_deletions", function () {
- spyOn(ContributorsStatGraphUtil, 'store_deletions')
- ContributorsStatGraphUtil.store_data(fake_entry, fake_total, fake_by_author)
- expect(ContributorsStatGraphUtil.store_deletions).toHaveBeenCalled()
- })
-
- })
+ spyOn(ContributorsStatGraphUtil, 'store_deletions');
+ ContributorsStatGraphUtil.store_data(fake_entry, fake_total, fake_by_author);
+ expect(ContributorsStatGraphUtil.store_deletions).toHaveBeenCalled();
+ });
+ });
// TODO: fix or remove
- //describe("#store_commits", function () {
- //var fake_total = "fake_total"
- //var fake_by_author = "fake_by_author"
-
- //it("calls #add twice with arguments fake_total and fake_by_author respectively", function () {
- //spyOn(ContributorsStatGraphUtil, 'add')
- //ContributorsStatGraphUtil.store_commits(fake_total, fake_by_author)
- //expect(ContributorsStatGraphUtil.add.argsForCall).toEqual([["fake_total", "commits", 1], ["fake_by_author", "commits", 1]])
- //})
- //})
+ // describe("#store_commits", function () {
+ // var fake_total = "fake_total";
+ // var fake_by_author = "fake_by_author";
+ //
+ // it("calls #add twice with arguments fake_total and fake_by_author respectively", function () {
+ // spyOn(ContributorsStatGraphUtil, 'add');
+ // ContributorsStatGraphUtil.store_commits(fake_total, fake_by_author);
+ // expect(ContributorsStatGraphUtil.add.argsForCall).toEqual([["fake_total", "commits", 1], ["fake_by_author", "commits", 1]]);
+ // });
+ // });
describe("#add", function () {
it("adds 1 to current test_field in collection", function () {
- var fake_collection = {test_field: 10}
- ContributorsStatGraphUtil.add(fake_collection, "test_field", 1)
- expect(fake_collection.test_field).toEqual(11)
- })
+ var fake_collection = { test_field: 10 };
+ ContributorsStatGraphUtil.add(fake_collection, "test_field", 1);
+ expect(fake_collection.test_field).toEqual(11);
+ });
it("inits and adds 1 if test_field in collection is not defined", function () {
- var fake_collection = {}
- ContributorsStatGraphUtil.add(fake_collection, "test_field", 1)
- expect(fake_collection.test_field).toEqual(1)
- })
- })
+ var fake_collection = {};
+ ContributorsStatGraphUtil.add(fake_collection, "test_field", 1);
+ expect(fake_collection.test_field).toEqual(1);
+ });
+ });
// TODO: fix or remove
- //describe("#store_additions", function () {
- //var fake_entry = {additions: 10}
- //var fake_total= "fake_total"
- //var fake_by_author = "fake_by_author"
- //it("calls #add twice with arguments fake_total and fake_by_author respectively", function () {
- //spyOn(ContributorsStatGraphUtil, 'add')
- //ContributorsStatGraphUtil.store_additions(fake_entry, fake_total, fake_by_author)
- //expect(ContributorsStatGraphUtil.add.argsForCall).toEqual([["fake_total", "additions", 10], ["fake_by_author", "additions", 10]])
- //})
- //})
+ // describe("#store_additions", function () {
+ // var fake_entry = {additions: 10};
+ // var fake_total= "fake_total";
+ // var fake_by_author = "fake_by_author";
+ // it("calls #add twice with arguments fake_total and fake_by_author respectively", function () {
+ // spyOn(ContributorsStatGraphUtil, 'add');
+ // ContributorsStatGraphUtil.store_additions(fake_entry, fake_total, fake_by_author);
+ // expect(ContributorsStatGraphUtil.add.argsForCall).toEqual([["fake_total", "additions", 10], ["fake_by_author", "additions", 10]]);
+ // });
+ // });
// TODO: fix or remove
- //describe("#store_deletions", function () {
- //var fake_entry = {deletions: 10}
- //var fake_total= "fake_total"
- //var fake_by_author = "fake_by_author"
- //it("calls #add twice with arguments fake_total and fake_by_author respectively", function () {
- //spyOn(ContributorsStatGraphUtil, 'add')
- //ContributorsStatGraphUtil.store_deletions(fake_entry, fake_total, fake_by_author)
- //expect(ContributorsStatGraphUtil.add.argsForCall).toEqual([["fake_total", "deletions", 10], ["fake_by_author", "deletions", 10]])
- //})
- //})
+ // describe("#store_deletions", function () {
+ // var fake_entry = {deletions: 10};
+ // var fake_total= "fake_total";
+ // var fake_by_author = "fake_by_author";
+ // it("calls #add twice with arguments fake_total and fake_by_author respectively", function () {
+ // spyOn(ContributorsStatGraphUtil, 'add');
+ // ContributorsStatGraphUtil.store_deletions(fake_entry, fake_total, fake_by_author);
+ // expect(ContributorsStatGraphUtil.add.argsForCall).toEqual([["fake_total", "deletions", 10], ["fake_by_author", "deletions", 10]]);
+ // });
+ // });
describe("#add_date", function () {
it("adds a date field to the collection", function () {
- var fake_date = "2013-10-02"
- var fake_collection = {}
- ContributorsStatGraphUtil.add_date(fake_date, fake_collection)
- expect(fake_collection[fake_date].date).toEqual("2013-10-02")
- })
- })
+ var fake_date = "2013-10-02";
+ var fake_collection = {};
+ ContributorsStatGraphUtil.add_date(fake_date, fake_collection);
+ expect(fake_collection[fake_date].date).toEqual("2013-10-02");
+ });
+ });
describe("#add_author", function () {
it("adds an author field to the collection", function () {
- var fake_author = { author_name: "Author", author_email: 'fake@email.com' }
- var fake_author_collection = {}
- var fake_email_collection = {}
- ContributorsStatGraphUtil.add_author(fake_author, fake_author_collection, fake_email_collection)
- expect(fake_author_collection[fake_author.author_name].author_name).toEqual("Author")
- expect(fake_email_collection[fake_author.author_email].author_name).toEqual("Author")
- })
- })
+ var fake_author = { author_name: "Author", author_email: 'fake@email.com' };
+ var fake_author_collection = {};
+ var fake_email_collection = {};
+ ContributorsStatGraphUtil.add_author(fake_author, fake_author_collection, fake_email_collection);
+ expect(fake_author_collection[fake_author.author_name].author_name).toEqual("Author");
+ expect(fake_email_collection[fake_author.author_email].author_name).toEqual("Author");
+ });
+ });
describe("#get_total_data", function () {
it("returns the collection sorted via specified field", function () {
var fake_parsed_log = {
- total: [{date: "2013-05-09", additions: 471, deletions: 0, commits: 1},
- {date: "2013-05-08", additions: 54, deletions: 7, commits: 3}],
- by_author:[
- {
- author: "Karlo Soriano",
- "2013-05-09": {date: "2013-05-09", additions: 471, deletions: 0, commits: 1}
- },
- {
- author: "Dmitriy Zaporozhets",
- "2013-05-08": {date: "2013-05-08", additions: 54, deletions: 7, commits: 3}
- }
- ]};
- var correct_total_data = [{date: "2013-05-08", commits: 3},
- {date: "2013-05-09", commits: 1}];
- expect(ContributorsStatGraphUtil.get_total_data(fake_parsed_log, "commits")).toEqual(correct_total_data)
- })
- })
+ total: [
+ { date: "2013-05-09", additions: 471, deletions: 0, commits: 1 },
+ { date: "2013-05-08", additions: 54, deletions: 7, commits: 3 }
+ ],
+ by_author: [
+ {
+ author: "Karlo Soriano",
+ "2013-05-09": { date: "2013-05-09", additions: 471, deletions: 0, commits: 1 }
+ },
+ {
+ author: "Dmitriy Zaporozhets",
+ "2013-05-08": { date: "2013-05-08", additions: 54, deletions: 7, commits: 3 }
+ }
+ ]
+ };
+ var correct_total_data = [
+ { date: "2013-05-08", commits: 3 },
+ { date: "2013-05-09", commits: 1 }
+ ];
+ expect(ContributorsStatGraphUtil.get_total_data(fake_parsed_log, "commits")).toEqual(correct_total_data);
+ });
+ });
describe("#pick_field", function () {
it("returns the collection with only the specified field and date", function () {
- var fake_parsed_log_total = [{date: "2013-05-09", additions: 471, deletions: 0, commits: 1},
- {date: "2013-05-08", additions: 54, deletions: 7, commits: 3}];
- ContributorsStatGraphUtil.pick_field(fake_parsed_log_total, "commits")
- var correct_pick_field_data = [{date: "2013-05-09", commits: 1},{date: "2013-05-08", commits: 3}];
- expect(ContributorsStatGraphUtil.pick_field(fake_parsed_log_total, "commits")).toEqual(correct_pick_field_data)
- })
- })
+ var fake_parsed_log_total = [
+ { date: "2013-05-09", additions: 471, deletions: 0, commits: 1 },
+ { date: "2013-05-08", additions: 54, deletions: 7, commits: 3 }
+ ];
+ ContributorsStatGraphUtil.pick_field(fake_parsed_log_total, "commits");
+ var correct_pick_field_data = [{ date: "2013-05-09", commits: 1 }, { date: "2013-05-08", commits: 3 }];
+ expect(ContributorsStatGraphUtil.pick_field(fake_parsed_log_total, "commits")).toEqual(correct_pick_field_data);
+ });
+ });
describe("#get_author_data", function () {
it("returns the log by author sorted by specified field", function () {
var fake_parsed_log = {
total: [
- {date: "2013-05-09", additions: 471, deletions: 0, commits: 1},
- {date: "2013-05-08", additions: 54, deletions: 7, commits: 3}
+ { date: "2013-05-09", additions: 471, deletions: 0, commits: 1 },
+ { date: "2013-05-08", additions: 54, deletions: 7, commits: 3 }
],
by_author: [
{
author_name: "Karlo Soriano", author_email: "karlo@email.com",
- "2013-05-09": {date: "2013-05-09", additions: 471, deletions: 0, commits: 1}
+ "2013-05-09": { date: "2013-05-09", additions: 471, deletions: 0, commits: 1 }
},
{
author_name: "Dmitriy Zaporozhets", author_email: "dzaporozhets@email.com",
- "2013-05-08": {date: "2013-05-08", additions: 54, deletions: 7, commits: 3}
+ "2013-05-08": { date: "2013-05-08", additions: 54, deletions: 7, commits: 3 }
}
]
- }
+ };
var correct_author_data = [
- {author_name:"Dmitriy Zaporozhets",author_email:"dzaporozhets@email.com",dates:{"2013-05-08":3},deletions:7,additions:54,"commits":3},
- {author_name:"Karlo Soriano",author_email:"karlo@email.com",dates:{"2013-05-09":1},deletions:0,additions:471,commits:1}
- ]
- expect(ContributorsStatGraphUtil.get_author_data(fake_parsed_log, "commits")).toEqual(correct_author_data)
- })
- })
+ { author_name: "Dmitriy Zaporozhets", author_email: "dzaporozhets@email.com", dates: { "2013-05-08": 3 }, deletions: 7, additions: 54, "commits": 3 },
+ { author_name: "Karlo Soriano", author_email: "karlo@email.com", dates: { "2013-05-09": 1 }, deletions: 0, additions: 471, commits: 1 }
+ ];
+ expect(ContributorsStatGraphUtil.get_author_data(fake_parsed_log, "commits")).toEqual(correct_author_data);
+ });
+ });
describe("#parse_log_entry", function () {
it("adds the corresponding info from the log entry to the author", function () {
- var fake_log_entry = { author_name: "Karlo Soriano", author_email: "karlo@email.com",
- "2013-05-09": {date: "2013-05-09", additions: 471, deletions: 0, commits: 1}
- }
- var correct_parsed_log = {author_name:"Karlo Soriano",author_email:"karlo@email.com",dates:{"2013-05-09":1},deletions:0,additions:471,commits:1}
- expect(ContributorsStatGraphUtil.parse_log_entry(fake_log_entry, 'commits', null)).toEqual(correct_parsed_log)
- })
- })
+ var fake_log_entry = { author_name: "Karlo Soriano", author_email: "karlo@email.com",
+ "2013-05-09": { date: "2013-05-09", additions: 471, deletions: 0, commits: 1 }
+ };
+ var correct_parsed_log = { author_name: "Karlo Soriano", author_email: "karlo@email.com", dates: { "2013-05-09": 1 }, deletions: 0, additions: 471, commits: 1 };
+ expect(ContributorsStatGraphUtil.parse_log_entry(fake_log_entry, 'commits', null)).toEqual(correct_parsed_log);
+ });
+ });
describe("#in_range", function () {
- var date = "2013-05-09"
+ var date = "2013-05-09";
it("returns true if date_range is null", function () {
- expect(ContributorsStatGraphUtil.in_range(date, null)).toEqual(true)
- })
+ expect(ContributorsStatGraphUtil.in_range(date, null)).toEqual(true);
+ });
it("returns true if date is in range", function () {
- var date_range = [new Date("2013-01-01"), new Date("2013-12-12")]
- expect(ContributorsStatGraphUtil.in_range(date, date_range)).toEqual(true)
- })
+ var date_range = [new Date("2013-01-01"), new Date("2013-12-12")];
+ expect(ContributorsStatGraphUtil.in_range(date, date_range)).toEqual(true);
+ });
it("returns false if date is not in range", function () {
- var date_range = [new Date("1999-12-01"), new Date("2000-12-01")]
- expect(ContributorsStatGraphUtil.in_range(date, date_range)).toEqual(false)
- })
- })
-
-
-})
+ var date_range = [new Date("1999-12-01"), new Date("2000-12-01")];
+ expect(ContributorsStatGraphUtil.in_range(date, date_range)).toEqual(false);
+ });
+ });
+});
diff --git a/spec/javascripts/graphs/stat_graph_spec.js b/spec/javascripts/graphs/stat_graph_spec.js
index 0da124632ae..876c23361bc 100644
--- a/spec/javascripts/graphs/stat_graph_spec.js
+++ b/spec/javascripts/graphs/stat_graph_spec.js
@@ -1,10 +1,9 @@
-/* eslint-disable quotes, padded-blocks, semi */
+/* eslint-disable quotes */
/* global StatGraph */
-//= require graphs/stat_graph
+require('~/graphs/stat_graph');
describe("StatGraph", function () {
-
describe("#get_log", function () {
it("returns log", function () {
StatGraph.log = "test";
@@ -16,7 +15,6 @@ describe("StatGraph", function () {
it("sets the log", function () {
StatGraph.set_log("test");
expect(StatGraph.log).toBe("test");
- })
- })
-
+ });
+ });
});
diff --git a/spec/javascripts/header_spec.js b/spec/javascripts/header_spec.js
index d2bcbc37b64..cecebb0b038 100644
--- a/spec/javascripts/header_spec.js
+++ b/spec/javascripts/header_spec.js
@@ -1,13 +1,12 @@
-/* eslint-disable space-before-function-paren, padded-blocks, no-var */
-/*= require header */
-/*= require lib/utils/text_utility */
-/*= require jquery */
+/* eslint-disable space-before-function-paren, no-var */
-(function() {
+require('~/header');
+require('~/lib/utils/text_utility');
+(function() {
describe('Header', function() {
var todosPendingCount = '.todos-pending-count';
- var fixtureTemplate = 'header.html';
+ var fixtureTemplate = 'static/header.html.raw';
function isTodosCountHidden() {
return $(todosPendingCount).hasClass('hidden');
@@ -17,9 +16,9 @@
$(document).trigger('todo:toggle', newCount);
}
- fixture.preload(fixtureTemplate);
+ preloadFixtures(fixtureTemplate);
beforeEach(function() {
- fixture.load(fixtureTemplate);
+ loadFixtures(fixtureTemplate);
});
it('should update todos-pending-count after receiving the todo:toggle event', function() {
@@ -51,5 +50,4 @@
});
});
});
-
}).call(this);
diff --git a/spec/javascripts/helpers/class_spec_helper.js.es6 b/spec/javascripts/helpers/class_spec_helper.js.es6
new file mode 100644
index 00000000000..d3c37d39431
--- /dev/null
+++ b/spec/javascripts/helpers/class_spec_helper.js.es6
@@ -0,0 +1,9 @@
+class ClassSpecHelper {
+ static itShouldBeAStaticMethod(base, method) {
+ return it('should be a static method', () => {
+ expect(Object.prototype.hasOwnProperty.call(base, method)).toBeTruthy();
+ });
+ }
+}
+
+window.ClassSpecHelper = ClassSpecHelper;
diff --git a/spec/javascripts/helpers/class_spec_helper_spec.js.es6 b/spec/javascripts/helpers/class_spec_helper_spec.js.es6
new file mode 100644
index 00000000000..0a61e561640
--- /dev/null
+++ b/spec/javascripts/helpers/class_spec_helper_spec.js.es6
@@ -0,0 +1,36 @@
+/* global ClassSpecHelper */
+
+require('./class_spec_helper');
+
+describe('ClassSpecHelper', () => {
+ describe('.itShouldBeAStaticMethod', function () {
+ beforeEach(() => {
+ class TestClass {
+ instanceMethod() { this.prop = 'val'; }
+ static staticMethod() {}
+ }
+
+ this.TestClass = TestClass;
+ });
+
+ ClassSpecHelper.itShouldBeAStaticMethod(ClassSpecHelper, 'itShouldBeAStaticMethod');
+
+ it('should have a defined spec', () => {
+ expect(ClassSpecHelper.itShouldBeAStaticMethod(this.TestClass, 'staticMethod').description).toBe('should be a static method');
+ });
+
+ it('should pass for a static method', () => {
+ const spec = ClassSpecHelper.itShouldBeAStaticMethod(this.TestClass, 'staticMethod');
+ expect(spec.status()).toBe('passed');
+ });
+
+ it('should fail for an instance method', (done) => {
+ const spec = ClassSpecHelper.itShouldBeAStaticMethod(this.TestClass, 'instanceMethod');
+ spec.resultCallback = (result) => {
+ expect(result.status).toBe('failed');
+ done();
+ };
+ spec.execute();
+ });
+ });
+});
diff --git a/spec/javascripts/issuable_spec.js.es6 b/spec/javascripts/issuable_spec.js.es6
new file mode 100644
index 00000000000..26d87cc5931
--- /dev/null
+++ b/spec/javascripts/issuable_spec.js.es6
@@ -0,0 +1,80 @@
+/* global Issuable */
+
+require('~/lib/utils/url_utility');
+require('~/issuable');
+
+(() => {
+ const BASE_URL = '/user/project/issues?scope=all&state=closed';
+ const DEFAULT_PARAMS = '&utf8=%E2%9C%93';
+
+ function updateForm(formValues, form) {
+ $.each(formValues, (id, value) => {
+ $(`#${id}`, form).val(value);
+ });
+ }
+
+ function resetForm(form) {
+ $('input[name!="utf8"]', form).each((index, input) => {
+ input.setAttribute('value', '');
+ });
+ }
+
+ describe('Issuable', () => {
+ preloadFixtures('static/issuable_filter.html.raw');
+
+ beforeEach(() => {
+ loadFixtures('static/issuable_filter.html.raw');
+ Issuable.init();
+ });
+
+ it('should be defined', () => {
+ expect(window.Issuable).toBeDefined();
+ });
+
+ describe('filtering', () => {
+ let $filtersForm;
+
+ beforeEach(() => {
+ $filtersForm = $('.js-filter-form');
+ loadFixtures('static/issuable_filter.html.raw');
+ resetForm($filtersForm);
+ });
+
+ it('should contain only the default parameters', () => {
+ spyOn(gl.utils, 'visitUrl');
+
+ Issuable.filterResults($filtersForm);
+
+ expect(gl.utils.visitUrl).toHaveBeenCalledWith(BASE_URL + DEFAULT_PARAMS);
+ });
+
+ it('should filter for the phrase "broken"', () => {
+ spyOn(gl.utils, 'visitUrl');
+
+ updateForm({ search: 'broken' }, $filtersForm);
+ Issuable.filterResults($filtersForm);
+ const params = `${DEFAULT_PARAMS}&search=broken`;
+
+ expect(gl.utils.visitUrl).toHaveBeenCalledWith(BASE_URL + params);
+ });
+
+ it('should keep query parameters after modifying filter', () => {
+ spyOn(gl.utils, 'visitUrl');
+
+ // initial filter
+ updateForm({ milestone_title: 'v1.0' }, $filtersForm);
+
+ Issuable.filterResults($filtersForm);
+ let params = `${DEFAULT_PARAMS}&milestone_title=v1.0`;
+ expect(gl.utils.visitUrl).toHaveBeenCalledWith(BASE_URL + params);
+
+ // update filter
+ updateForm({ label_name: 'Frontend' }, $filtersForm);
+
+ Issuable.filterResults($filtersForm);
+ params = `${DEFAULT_PARAMS}&milestone_title=v1.0&label_name=Frontend`;
+ expect(gl.utils.visitUrl).toHaveBeenCalledWith(BASE_URL + params);
+ });
+ });
+ });
+})();
diff --git a/spec/javascripts/issuable_time_tracker_spec.js.es6 b/spec/javascripts/issuable_time_tracker_spec.js.es6
new file mode 100644
index 00000000000..cb068a4f879
--- /dev/null
+++ b/spec/javascripts/issuable_time_tracker_spec.js.es6
@@ -0,0 +1,202 @@
+/* eslint-disable */
+
+require('jquery');
+require('vue');
+require('~/issuable/time_tracking/components/time_tracker');
+
+function initTimeTrackingComponent(opts) {
+ setFixtures(`
+ <div>
+ <div id="mock-container"></div>
+ </div>
+ `);
+
+ this.initialData = {
+ time_estimate: opts.timeEstimate,
+ time_spent: opts.timeSpent,
+ human_time_estimate: opts.timeEstimateHumanReadable,
+ human_time_spent: opts.timeSpentHumanReadable,
+ docsUrl: '/help/workflow/time_tracking.md',
+ };
+
+ const TimeTrackingComponent = Vue.component('issuable-time-tracker');
+ this.timeTracker = new TimeTrackingComponent({
+ el: '#mock-container',
+ propsData: this.initialData,
+ });
+}
+
+((gl) => {
+ describe('Issuable Time Tracker', function() {
+ describe('Initialization', function() {
+ beforeEach(function() {
+ initTimeTrackingComponent.call(this, { timeEstimate: 100000, timeSpent: 5000, timeEstimateHumanReadable: '2h 46m', timeSpentHumanReadable: '1h 23m' });
+ });
+
+ it('should return something defined', function() {
+ expect(this.timeTracker).toBeDefined();
+ });
+
+ it ('should correctly set timeEstimate', function(done) {
+ Vue.nextTick(() => {
+ expect(this.timeTracker.timeEstimate).toBe(this.initialData.time_estimate);
+ done();
+ });
+ });
+ it ('should correctly set time_spent', function(done) {
+ Vue.nextTick(() => {
+ expect(this.timeTracker.timeSpent).toBe(this.initialData.time_spent);
+ done();
+ });
+ });
+ });
+
+ describe('Content Display', function() {
+ describe('Panes', function() {
+ describe('Comparison pane', function() {
+ beforeEach(function() {
+ initTimeTrackingComponent.call(this, { timeEstimate: 100000, timeSpent: 5000, timeEstimateHumanReadable: '', timeSpentHumanReadable: '' });
+ });
+
+ it('should show the "Comparison" pane when timeEstimate and time_spent are truthy', function(done) {
+ Vue.nextTick(() => {
+ const $comparisonPane = this.timeTracker.$el.querySelector('.time-tracking-comparison-pane');
+ expect(this.timeTracker.showComparisonState).toBe(true);
+ done();
+ });
+ });
+
+ describe('Remaining meter', function() {
+ it('should display the remaining meter with the correct width', function(done) {
+ Vue.nextTick(() => {
+ const meterWidth = this.timeTracker.$el.querySelector('.time-tracking-comparison-pane .meter-fill').style.width;
+ const correctWidth = '5%';
+
+ expect(meterWidth).toBe(correctWidth);
+ done();
+ })
+ });
+
+ it('should display the remaining meter with the correct background color when within estimate', function(done) {
+ Vue.nextTick(() => {
+ const styledMeter = $(this.timeTracker.$el).find('.time-tracking-comparison-pane .within_estimate .meter-fill');
+ expect(styledMeter.length).toBe(1);
+ done()
+ });
+ });
+
+ it('should display the remaining meter with the correct background color when over estimate', function(done) {
+ this.timeTracker.time_estimate = 100000;
+ this.timeTracker.time_spent = 20000000;
+ Vue.nextTick(() => {
+ const styledMeter = $(this.timeTracker.$el).find('.time-tracking-comparison-pane .over_estimate .meter-fill');
+ expect(styledMeter.length).toBe(1);
+ done();
+ });
+ });
+ });
+ });
+
+ describe("Estimate only pane", function() {
+ beforeEach(function() {
+ initTimeTrackingComponent.call(this, { timeEstimate: 100000, timeSpent: 0, timeEstimateHumanReadable: '2h 46m', timeSpentHumanReadable: '' });
+ });
+
+ it('should display the human readable version of time estimated', function(done) {
+ Vue.nextTick(() => {
+ const estimateText = this.timeTracker.$el.querySelector('.time-tracking-estimate-only-pane').innerText;
+ const correctText = 'Estimated: 2h 46m';
+
+ expect(estimateText).toBe(correctText);
+ done();
+ });
+ });
+ });
+
+ describe('Spent only pane', function() {
+ beforeEach(function() {
+ initTimeTrackingComponent.call(this, { timeEstimate: 0, timeSpent: 5000, timeEstimateHumanReadable: '2h 46m', timeSpentHumanReadable: '1h 23m' });
+ });
+
+ it('should display the human readable version of time spent', function(done) {
+ Vue.nextTick(() => {
+ const spentText = this.timeTracker.$el.querySelector('.time-tracking-spend-only-pane').innerText;
+ const correctText = 'Spent: 1h 23m';
+
+ expect(spentText).toBe(correctText);
+ done();
+ });
+ });
+ });
+
+ describe('No time tracking pane', function() {
+ beforeEach(function() {
+ initTimeTrackingComponent.call(this, { timeEstimate: 0, timeSpent: 0, timeEstimateHumanReadable: 0, timeSpentHumanReadable: 0 });
+ });
+
+ it('should only show the "No time tracking" pane when both timeEstimate and time_spent are falsey', function(done) {
+ Vue.nextTick(() => {
+ const $noTrackingPane = this.timeTracker.$el.querySelector('.time-tracking-no-tracking-pane');
+ const noTrackingText =$noTrackingPane.innerText;
+ const correctText = 'No estimate or time spent';
+
+ expect(this.timeTracker.showNoTimeTrackingState).toBe(true);
+ expect($noTrackingPane).toBeVisible();
+ expect(noTrackingText).toBe(correctText);
+ done();
+ });
+ });
+ });
+
+ describe("Help pane", function() {
+ beforeEach(function() {
+ initTimeTrackingComponent.call(this, { timeEstimate: 0, timeSpent: 0 });
+ });
+
+ it('should not show the "Help" pane by default', function(done) {
+ Vue.nextTick(() => {
+ const $helpPane = this.timeTracker.$el.querySelector('.time-tracking-help-state');
+
+ expect(this.timeTracker.showHelpState).toBe(false);
+ expect($helpPane).toBeNull();
+ done();
+ });
+ });
+
+ it('should show the "Help" pane when help button is clicked', function(done) {
+ Vue.nextTick(() => {
+ $(this.timeTracker.$el).find('.help-button').click();
+
+ setTimeout(() => {
+ const $helpPane = this.timeTracker.$el.querySelector('.time-tracking-help-state');
+ expect(this.timeTracker.showHelpState).toBe(true);
+ expect($helpPane).toBeVisible();
+ done();
+ }, 10);
+ });
+ });
+
+ it('should not show the "Help" pane when help button is clicked and then closed', function(done) {
+ Vue.nextTick(() => {
+ $(this.timeTracker.$el).find('.help-button').click();
+
+ setTimeout(() => {
+
+ $(this.timeTracker.$el).find('.close-help-button').click();
+
+ setTimeout(() => {
+ const $helpPane = this.timeTracker.$el.querySelector('.time-tracking-help-state');
+
+ expect(this.timeTracker.showHelpState).toBe(false);
+ expect($helpPane).toBeNull();
+
+ done();
+ }, 1000);
+ }, 1000);
+ });
+ });
+ });
+ });
+ });
+ });
+})(window.gl || (window.gl = {}));
diff --git a/spec/javascripts/issue_spec.js b/spec/javascripts/issue_spec.js
index faab5ae00c2..5b0b7aa7903 100644
--- a/spec/javascripts/issue_spec.js
+++ b/spec/javascripts/issue_spec.js
@@ -1,16 +1,16 @@
-/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, no-use-before-define, indent, no-trailing-spaces, comma-dangle, padded-blocks, max-len */
+/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, no-use-before-define, comma-dangle, max-len */
/* global Issue */
-/*= require lib/utils/text_utility */
-/*= require issue */
+require('~/lib/utils/text_utility');
+require('~/issue');
(function() {
var INVALID_URL = 'http://goesnowhere.nothing/whereami';
var $boxClosed, $boxOpen, $btnClose, $btnReopen;
- fixture.preload('issues/closed-issue.html');
- fixture.preload('issues/issue-with-task-list.html');
- fixture.preload('issues/open-issue.html');
+ preloadFixtures('issues/closed-issue.html.raw');
+ preloadFixtures('issues/issue-with-task-list.html.raw');
+ preloadFixtures('issues/open-issue.html.raw');
function expectErrorMessage() {
var $flashMessage = $('div.flash-alert');
@@ -42,27 +42,27 @@
}
function findElements() {
- $boxClosed = $('div.status-box-closed');
- expect($boxClosed).toExist();
- expect($boxClosed).toHaveText('Closed');
+ $boxClosed = $('div.status-box-closed');
+ expect($boxClosed).toExist();
+ expect($boxClosed).toHaveText('Closed');
- $boxOpen = $('div.status-box-open');
- expect($boxOpen).toExist();
- expect($boxOpen).toHaveText('Open');
+ $boxOpen = $('div.status-box-open');
+ expect($boxOpen).toExist();
+ expect($boxOpen).toHaveText('Open');
- $btnClose = $('.btn-close.btn-grouped');
- expect($btnClose).toExist();
- expect($btnClose).toHaveText('Close issue');
+ $btnClose = $('.btn-close.btn-grouped');
+ expect($btnClose).toExist();
+ expect($btnClose).toHaveText('Close issue');
- $btnReopen = $('.btn-reopen.btn-grouped');
- expect($btnReopen).toExist();
- expect($btnReopen).toHaveText('Reopen issue');
+ $btnReopen = $('.btn-reopen.btn-grouped');
+ expect($btnReopen).toExist();
+ expect($btnReopen).toHaveText('Reopen issue');
}
describe('Issue', function() {
describe('task lists', function() {
- fixture.load('issues/issue-with-task-list.html');
beforeEach(function() {
+ loadFixtures('issues/issue-with-task-list.html.raw');
this.issue = new Issue();
});
@@ -86,7 +86,7 @@
describe('close issue', function() {
beforeEach(function() {
- fixture.load('issues/open-issue.html');
+ loadFixtures('issues/open-issue.html.raw');
findElements();
this.issue = new Issue();
@@ -140,7 +140,7 @@
describe('reopen issue', function() {
beforeEach(function() {
- fixture.load('issues/closed-issue.html');
+ loadFixtures('issues/closed-issue.html.raw');
findElements();
this.issue = new Issue();
@@ -161,5 +161,4 @@
expect($btnReopen).toHaveProp('disabled', false);
});
});
-
}).call(this);
diff --git a/spec/javascripts/labels_issue_sidebar_spec.js.es6 b/spec/javascripts/labels_issue_sidebar_spec.js.es6
index 0c48d04776f..37e038c16da 100644
--- a/spec/javascripts/labels_issue_sidebar_spec.js.es6
+++ b/spec/javascripts/labels_issue_sidebar_spec.js.es6
@@ -1,43 +1,41 @@
-/* eslint-disable no-new, no-plusplus, object-curly-spacing, prefer-const, semi */
+/* eslint-disable no-new */
/* global IssuableContext */
/* global LabelsSelect */
-//= require lib/utils/type_utility
-//= require jquery
-//= require bootstrap
-//= require gl_dropdown
-//= require select2
-//= require jquery.nicescroll
-//= require api
-//= require create_label
-//= require issuable_context
-//= require users_select
-//= require labels_select
+require('~/lib/utils/type_utility');
+require('~/gl_dropdown');
+require('select2');
+require('vendor/jquery.nicescroll');
+require('~/api');
+require('~/create_label');
+require('~/issuable_context');
+require('~/users_select');
+require('~/labels_select');
(() => {
let saveLabelCount = 0;
describe('Issue dropdown sidebar', () => {
- fixture.preload('issue_sidebar_label.html');
+ preloadFixtures('static/issue_sidebar_label.html.raw');
beforeEach(() => {
- fixture.load('issue_sidebar_label.html');
+ loadFixtures('static/issue_sidebar_label.html.raw');
new IssuableContext('{"id":1,"name":"Administrator","username":"root"}');
new LabelsSelect();
spyOn(jQuery, 'ajax').and.callFake((req) => {
const d = $.Deferred();
- let LABELS_DATA = []
+ let LABELS_DATA = [];
if (req.url === '/root/test/labels.json') {
- for (let i = 0; i < 10; i++) {
- LABELS_DATA.push({id: i, title: `test ${i}`, color: '#5CB85C'});
+ for (let i = 0; i < 10; i += 1) {
+ LABELS_DATA.push({ id: i, title: `test ${i}`, color: '#5CB85C' });
}
} else if (req.url === '/root/test/issues/2.json') {
- let tmp = []
- for (let i = 0; i < saveLabelCount; i++) {
- tmp.push({id: i, title: `test ${i}`, color: '#5CB85C'});
+ const tmp = [];
+ for (let i = 0; i < saveLabelCount; i += 1) {
+ tmp.push({ id: i, title: `test ${i}`, color: '#5CB85C' });
}
- LABELS_DATA = {labels: tmp};
+ LABELS_DATA = { labels: tmp };
}
d.resolve(LABELS_DATA);
diff --git a/spec/javascripts/lib/utils/common_utils_spec.js.es6 b/spec/javascripts/lib/utils/common_utils_spec.js.es6
index ef75f600898..006ede21093 100644
--- a/spec/javascripts/lib/utils/common_utils_spec.js.es6
+++ b/spec/javascripts/lib/utils/common_utils_spec.js.es6
@@ -1,4 +1,4 @@
-//= require lib/utils/common_utils
+require('~/lib/utils/common_utils');
(() => {
describe('common_utils', () => {
@@ -10,11 +10,12 @@
// IE11 will return a relative pathname while other browsers will return a full pathname.
// parseUrl uses an anchor element for parsing an url. With relative urls, the anchor
// element will create an absolute url relative to the current execution context.
- // The JavaScript test suite is executed at '/teaspoon' which will lead to an absolute
- // url starting with '/teaspoon'.
- expect(gl.utils.parseUrl('" test="asf"').pathname).toEqual('/teaspoon/%22%20test=%22asf%22');
+ // The JavaScript test suite is executed at '/' which will lead to an absolute url
+ // starting with '/'.
+ expect(gl.utils.parseUrl('" test="asf"').pathname).toContain('/%22%20test=%22asf%22');
});
});
+
describe('gl.utils.parseUrlPathname', () => {
beforeEach(() => {
spyOn(gl.utils, 'parseUrl').and.callFake(url => ({
@@ -28,5 +29,115 @@
expect(gl.utils.parseUrlPathname('some/relative/url')).toEqual('/some/relative/url');
});
});
+
+ describe('gl.utils.getUrlParamsArray', () => {
+ it('should return params array', () => {
+ expect(gl.utils.getUrlParamsArray() instanceof Array).toBe(true);
+ });
+
+ it('should remove the question mark from the search params', () => {
+ const paramsArray = gl.utils.getUrlParamsArray();
+ expect(paramsArray[0][0] !== '?').toBe(true);
+ });
+ });
+
+ describe('gl.utils.handleLocationHash', () => {
+ beforeEach(() => {
+ spyOn(window.document, 'getElementById').and.callThrough();
+ });
+
+ function expectGetElementIdToHaveBeenCalledWith(elementId) {
+ expect(window.document.getElementById).toHaveBeenCalledWith(elementId);
+ }
+
+ it('decodes hash parameter', () => {
+ window.history.pushState({}, null, '#random-hash');
+ gl.utils.handleLocationHash();
+
+ expectGetElementIdToHaveBeenCalledWith('random-hash');
+ expectGetElementIdToHaveBeenCalledWith('user-content-random-hash');
+ });
+
+ it('decodes cyrillic hash parameter', () => {
+ window.history.pushState({}, null, '#definição');
+ gl.utils.handleLocationHash();
+
+ expectGetElementIdToHaveBeenCalledWith('definição');
+ expectGetElementIdToHaveBeenCalledWith('user-content-definição');
+ });
+
+ it('decodes encoded cyrillic hash parameter', () => {
+ window.history.pushState({}, null, '#defini%C3%A7%C3%A3o');
+ gl.utils.handleLocationHash();
+
+ expectGetElementIdToHaveBeenCalledWith('definição');
+ expectGetElementIdToHaveBeenCalledWith('user-content-definição');
+ });
+ });
+
+ describe('gl.utils.getParameterByName', () => {
+ beforeEach(() => {
+ window.history.pushState({}, null, '?scope=all&p=2');
+ });
+
+ it('should return valid parameter', () => {
+ const value = gl.utils.getParameterByName('scope');
+ expect(value).toBe('all');
+ });
+
+ it('should return invalid parameter', () => {
+ const value = gl.utils.getParameterByName('fakeParameter');
+ expect(value).toBe(null);
+ });
+ });
+
+ describe('gl.utils.normalizedHeaders', () => {
+ it('should upperCase all the header keys to keep them consistent', () => {
+ const apiHeaders = {
+ 'X-Something-Workhorse': { workhorse: 'ok' },
+ 'x-something-nginx': { nginx: 'ok' },
+ };
+
+ const normalized = gl.utils.normalizeHeaders(apiHeaders);
+
+ const WORKHORSE = 'X-SOMETHING-WORKHORSE';
+ const NGINX = 'X-SOMETHING-NGINX';
+
+ expect(normalized[WORKHORSE].workhorse).toBe('ok');
+ expect(normalized[NGINX].nginx).toBe('ok');
+ });
+ });
+
+ describe('gl.utils.isMetaClick', () => {
+ it('should identify meta click on Windows/Linux', () => {
+ const e = {
+ metaKey: false,
+ ctrlKey: true,
+ which: 1,
+ };
+
+ expect(gl.utils.isMetaClick(e)).toBe(true);
+ });
+
+ it('should identify meta click on macOS', () => {
+ const e = {
+ metaKey: true,
+ ctrlKey: false,
+ which: 1,
+ };
+
+ expect(gl.utils.isMetaClick(e)).toBe(true);
+ });
+
+ it('should identify as meta click on middle-click or Mouse-wheel click', () => {
+ const e = {
+ metaKey: false,
+ ctrlKey: false,
+ which: 2,
+ };
+
+ expect(gl.utils.isMetaClick(e)).toBe(true);
+ });
+ });
});
})();
diff --git a/spec/javascripts/lib/utils/custom_event_polyfill_spec.js.es6 b/spec/javascripts/lib/utils/custom_event_polyfill_spec.js.es6
deleted file mode 100644
index 3645dd70c55..00000000000
--- a/spec/javascripts/lib/utils/custom_event_polyfill_spec.js.es6
+++ /dev/null
@@ -1,43 +0,0 @@
-//= require lib/utils/custom_event_polyfill
-
-describe('Custom Event Polyfill', () => {
- it('should be defined', () => {
- expect(window.CustomEvent).toBeDefined();
- });
-
- it('should create a `CustomEvent` instance', () => {
- const e = new window.CustomEvent('foo');
-
- expect(e.type).toEqual('foo');
- expect(e.bubbles).toBe(false);
- expect(e.cancelable).toBe(false);
- expect(e.detail).toBeFalsy();
- });
-
- it('should create a `CustomEvent` instance with a `details` object', () => {
- const e = new window.CustomEvent('bar', { detail: { foo: 'bar' } });
-
- expect(e.type).toEqual('bar');
- expect(e.bubbles).toBe(false);
- expect(e.cancelable).toBe(false);
- expect(e.detail.foo).toEqual('bar');
- });
-
- it('should create a `CustomEvent` instance with a `bubbles` boolean', () => {
- const e = new window.CustomEvent('bar', { bubbles: true });
-
- expect(e.type).toEqual('bar');
- expect(e.bubbles).toBe(true);
- expect(e.cancelable).toBe(false);
- expect(e.detail).toBeFalsy();
- });
-
- it('should create a `CustomEvent` instance with a `cancelable` boolean', () => {
- const e = new window.CustomEvent('bar', { cancelable: true });
-
- expect(e.type).toEqual('bar');
- expect(e.bubbles).toBe(false);
- expect(e.cancelable).toBe(true);
- expect(e.detail).toBeFalsy();
- });
-});
diff --git a/spec/javascripts/lib/utils/text_utility_spec.js.es6 b/spec/javascripts/lib/utils/text_utility_spec.js.es6
new file mode 100644
index 00000000000..86ade66ec29
--- /dev/null
+++ b/spec/javascripts/lib/utils/text_utility_spec.js.es6
@@ -0,0 +1,39 @@
+require('~/lib/utils/text_utility');
+
+(() => {
+ describe('text_utility', () => {
+ describe('gl.text.getTextWidth', () => {
+ it('returns zero width when no text is passed', () => {
+ expect(gl.text.getTextWidth('')).toBe(0);
+ });
+
+ it('returns zero width when no text is passed and font is passed', () => {
+ expect(gl.text.getTextWidth('', '100px sans-serif')).toBe(0);
+ });
+
+ it('returns width when text is passed', () => {
+ expect(gl.text.getTextWidth('foo') > 0).toBe(true);
+ });
+
+ it('returns bigger width when font is larger', () => {
+ const largeFont = gl.text.getTextWidth('foo', '100px sans-serif');
+ const regular = gl.text.getTextWidth('foo', '10px sans-serif');
+ expect(largeFont > regular).toBe(true);
+ });
+ });
+
+ describe('gl.text.pluralize', () => {
+ it('returns pluralized', () => {
+ expect(gl.text.pluralize('test', 2)).toBe('tests');
+ });
+
+ it('returns pluralized when count is 0', () => {
+ expect(gl.text.pluralize('test', 0)).toBe('tests');
+ });
+
+ it('does not return pluralized', () => {
+ expect(gl.text.pluralize('test', 1)).toBe('test');
+ });
+ });
+ });
+})();
diff --git a/spec/javascripts/line_highlighter_spec.js b/spec/javascripts/line_highlighter_spec.js
index decdf583410..8b196f7720f 100644
--- a/spec/javascripts/line_highlighter_spec.js
+++ b/spec/javascripts/line_highlighter_spec.js
@@ -1,12 +1,12 @@
-/* eslint-disable space-before-function-paren, no-var, no-param-reassign, quotes, prefer-template, no-else-return, new-cap, dot-notation, no-return-assign, comma-dangle, no-new, one-var, one-var-declaration-per-line, no-plusplus, jasmine/no-spec-dupes, no-underscore-dangle, padded-blocks, max-len */
+/* eslint-disable space-before-function-paren, no-var, no-param-reassign, quotes, prefer-template, no-else-return, new-cap, dot-notation, no-return-assign, comma-dangle, no-new, one-var, one-var-declaration-per-line, jasmine/no-spec-dupes, no-underscore-dangle, max-len */
/* global LineHighlighter */
-/*= require line_highlighter */
+require('~/line_highlighter');
(function() {
describe('LineHighlighter', function() {
var clickLine;
- fixture.preload('line_highlighter.html');
+ preloadFixtures('static/line_highlighter.html.raw');
clickLine = function(number, eventData) {
var e;
if (eventData == null) {
@@ -20,7 +20,7 @@
}
};
beforeEach(function() {
- fixture.load('line_highlighter.html');
+ loadFixtures('static/line_highlighter.html.raw');
this["class"] = new LineHighlighter();
this.css = this["class"].highlightClass;
return this.spies = {
@@ -33,11 +33,11 @@
return expect($('#LC13')).toHaveClass(this.css);
});
it('highlights a range of lines given in the URL hash', function() {
- var i, line, results;
+ var line, results;
new LineHighlighter('#L5-25');
expect($("." + this.css).length).toBe(21);
results = [];
- for (line = i = 5; i <= 25; line = ++i) {
+ for (line = 5; line <= 25; line += 1) {
results.push(expect($("#LC" + line)).toHaveClass(this.css));
}
return results;
@@ -124,27 +124,27 @@
});
describe('with existing single-line highlight', function() {
it('uses existing line as last line when target is lesser', function() {
- var i, line, results;
+ var line, results;
clickLine(20);
clickLine(15, {
shiftKey: true
});
expect($("." + this.css).length).toBe(6);
results = [];
- for (line = i = 15; i <= 20; line = ++i) {
+ for (line = 15; line <= 20; line += 1) {
results.push(expect($("#LC" + line)).toHaveClass(this.css));
}
return results;
});
return it('uses existing line as first line when target is greater', function() {
- var i, line, results;
+ var line, results;
clickLine(5);
clickLine(10, {
shiftKey: true
});
expect($("." + this.css).length).toBe(6);
results = [];
- for (line = i = 5; i <= 10; line = ++i) {
+ for (line = 5; line <= 10; line += 1) {
results.push(expect($("#LC" + line)).toHaveClass(this.css));
}
return results;
@@ -160,25 +160,25 @@
});
});
it('uses target as first line when it is less than existing first line', function() {
- var i, line, results;
+ var line, results;
clickLine(5, {
shiftKey: true
});
expect($("." + this.css).length).toBe(6);
results = [];
- for (line = i = 5; i <= 10; line = ++i) {
+ for (line = 5; line <= 10; line += 1) {
results.push(expect($("#LC" + line)).toHaveClass(this.css));
}
return results;
});
return it('uses target as last line when it is greater than existing first line', function() {
- var i, line, results;
+ var line, results;
clickLine(15, {
shiftKey: true
});
expect($("." + this.css).length).toBe(6);
results = [];
- for (line = i = 10; i <= 15; line = ++i) {
+ for (line = 10; line <= 15; line += 1) {
results.push(expect($("#LC" + line)).toHaveClass(this.css));
}
return results;
@@ -227,5 +227,4 @@
});
});
});
-
}).call(this);
diff --git a/spec/javascripts/merge_request_spec.js b/spec/javascripts/merge_request_spec.js
index 4cf1693af1b..25cfa9e9479 100644
--- a/spec/javascripts/merge_request_spec.js
+++ b/spec/javascripts/merge_request_spec.js
@@ -1,14 +1,14 @@
-/* eslint-disable space-before-function-paren, no-return-assign, padded-blocks */
+/* eslint-disable space-before-function-paren, no-return-assign */
/* global MergeRequest */
-/*= require merge_request */
+require('~/merge_request');
(function() {
describe('MergeRequest', function() {
return describe('task lists', function() {
- fixture.preload('merge_requests_show.html');
+ preloadFixtures('static/merge_requests_show.html.raw');
beforeEach(function() {
- fixture.load('merge_requests_show.html');
+ loadFixtures('static/merge_requests_show.html.raw');
return this.merge = new MergeRequest();
});
it('modifies the Markdown field', function() {
@@ -26,5 +26,4 @@
});
});
});
-
}).call(this);
diff --git a/spec/javascripts/merge_request_tabs_spec.js b/spec/javascripts/merge_request_tabs_spec.js
index 130d391bfab..92a0f1c05f7 100644
--- a/spec/javascripts/merge_request_tabs_spec.js
+++ b/spec/javascripts/merge_request_tabs_spec.js
@@ -1,11 +1,20 @@
/* eslint-disable no-var, comma-dangle, object-shorthand */
-/*= require merge_request_tabs */
-//= require breakpoints
-//= require lib/utils/common_utils
-//= require jquery.scrollTo
+require('~/merge_request_tabs');
+require('~/breakpoints');
+require('~/lib/utils/common_utils');
+require('vendor/jquery.scrollTo');
(function () {
+ // TODO: remove this hack!
+ // PhantomJS causes spyOn to panic because replaceState isn't "writable"
+ var phantomjs;
+ try {
+ phantomjs = !Object.getOwnPropertyDescriptor(window.history, 'replaceState').writable;
+ } catch (err) {
+ phantomjs = false;
+ }
+
describe('MergeRequestTabs', function () {
var stubLocation = {};
var setLocation = function (stubs) {
@@ -16,21 +25,23 @@
};
$.extend(stubLocation, defaults, stubs || {});
};
- fixture.preload('merge_request_tabs.html');
+ preloadFixtures('static/merge_request_tabs.html.raw');
beforeEach(function () {
this.class = new gl.MergeRequestTabs({ stubLocation: stubLocation });
setLocation();
- this.spies = {
- history: spyOn(window.history, 'replaceState').and.callFake(function () {})
- };
+ if (!phantomjs) {
+ this.spies = {
+ history: spyOn(window.history, 'replaceState').and.callFake(function () {})
+ };
+ }
});
describe('#activateTab', function () {
beforeEach(function () {
spyOn($, 'ajax').and.callFake(function () {});
- fixture.load('merge_request_tabs.html');
+ loadFixtures('static/merge_request_tabs.html.raw');
this.subject = this.class.activateTab;
});
it('shows the first tab when action is show', function () {
@@ -50,6 +61,56 @@
expect($('#diffs')).toHaveClass('active');
});
});
+ describe('#opensInNewTab', function () {
+ var commitsLink;
+ var tabUrl;
+
+ beforeEach(function () {
+ commitsLink = '.commits-tab li a';
+ tabUrl = $(commitsLink).attr('href');
+
+ spyOn($.fn, 'attr').and.returnValue(tabUrl);
+ });
+ it('opens page tab in a new browser tab with Ctrl+Click - Windows/Linux', function () {
+ spyOn(window, 'open').and.callFake(function (url, name) {
+ expect(url).toEqual(tabUrl);
+ expect(name).toEqual('_blank');
+ });
+
+ this.class.clickTab({
+ metaKey: false,
+ ctrlKey: true,
+ which: 1,
+ stopImmediatePropagation: function () {}
+ });
+ });
+ it('opens page tab in a new browser tab with Cmd+Click - Mac', function () {
+ spyOn(window, 'open').and.callFake(function (url, name) {
+ expect(url).toEqual(tabUrl);
+ expect(name).toEqual('_blank');
+ });
+
+ this.class.clickTab({
+ metaKey: true,
+ ctrlKey: false,
+ which: 1,
+ stopImmediatePropagation: function () {}
+ });
+ });
+ it('opens page tab in a new browser tab with Middle-click - Mac/PC', function () {
+ spyOn(window, 'open').and.callFake(function (url, name) {
+ expect(url).toEqual(tabUrl);
+ expect(name).toEqual('_blank');
+ });
+
+ this.class.clickTab({
+ metaKey: false,
+ ctrlKey: false,
+ which: 2,
+ stopImmediatePropagation: function () {}
+ });
+ });
+ });
describe('#setCurrentAction', function () {
beforeEach(function () {
@@ -98,10 +159,11 @@
pathname: '/foo/bar/merge_requests/1'
});
newState = this.subject('commits');
- expect(this.spies.history).toHaveBeenCalledWith({
- turbolinks: true,
- url: newState
- }, document.title, newState);
+ if (!phantomjs) {
+ expect(this.spies.history).toHaveBeenCalledWith({
+ url: newState
+ }, document.title, newState);
+ }
});
it('treats "show" like "notes"', function () {
setLocation({
diff --git a/spec/javascripts/merge_request_widget_spec.js b/spec/javascripts/merge_request_widget_spec.js
index 6f91529db00..8cefdd2409d 100644
--- a/spec/javascripts/merge_request_widget_spec.js
+++ b/spec/javascripts/merge_request_widget_spec.js
@@ -1,7 +1,8 @@
-/* eslint-disable space-before-function-paren, quotes, comma-dangle, dot-notation, indent, quote-props, no-var, padded-blocks, max-len */
+/* eslint-disable space-before-function-paren, quotes, comma-dangle, dot-notation, quote-props, no-var, max-len */
-/*= require merge_request_widget */
-/*= require lib/utils/datetime_utility */
+require('~/merge_request_widget');
+require('~/smart_interval');
+require('~/lib/utils/datetime_utility');
(function() {
describe('MergeRequestWidget', function() {
@@ -21,7 +22,11 @@
normal: "Build {{status}}"
},
gitlab_icon: "gitlab_logo.png",
- builds_path: "http://sampledomain.local/sampleBuildsPath"
+ ci_pipeline: 80,
+ ci_sha: "12a34bc5",
+ builds_path: "http://sampledomain.local/sampleBuildsPath",
+ commits_path: "http://sampledomain.local/commits",
+ pipeline_path: "http://sampledomain.local/pipelines"
};
this["class"] = new window.gl.MergeRequestWidget(this.opts);
});
@@ -42,17 +47,17 @@
});
it('should call renderEnvironments when the environments property is set', function() {
- const spy = spyOn(this.class, 'renderEnvironments').and.stub();
- this.class.getCIEnvironmentsStatus();
- expect(spy).toHaveBeenCalledWith(this.ciEnvironmentsStatusData);
- });
-
- it('should not call renderEnvironments when the environments property is not set', function() {
- this.ciEnvironmentsStatusData = null;
- const spy = spyOn(this.class, 'renderEnvironments').and.stub();
- this.class.getCIEnvironmentsStatus();
- expect(spy).not.toHaveBeenCalled();
- });
+ const spy = spyOn(this.class, 'renderEnvironments').and.stub();
+ this.class.getCIEnvironmentsStatus();
+ expect(spy).toHaveBeenCalledWith(this.ciEnvironmentsStatusData);
+ });
+
+ it('should not call renderEnvironments when the environments property is not set', function() {
+ this.ciEnvironmentsStatusData = null;
+ const spy = spyOn(this.class, 'renderEnvironments').and.stub();
+ this.class.getCIEnvironmentsStatus();
+ expect(spy).not.toHaveBeenCalled();
+ });
});
describe('renderEnvironments', function() {
@@ -107,21 +112,22 @@
});
describe('mergeInProgress', function() {
- it('should display error with h4 tag', function() {
- spyOn(this.class.$widgetBody, 'html').and.callFake(function(html) {
- expect(html).toBe('<h4>Sorry, something went wrong.</h4>');
- });
- spyOn($, 'ajax').and.callFake(function(e) {
- e.success({ merge_error: 'Sorry, something went wrong.' });
- });
- this.class.mergeInProgress(null);
+ it('should display error with h4 tag', function() {
+ spyOn(this.class.$widgetBody, 'html').and.callFake(function(html) {
+ expect(html).toBe('<h4>Sorry, something went wrong.</h4>');
+ });
+ spyOn($, 'ajax').and.callFake(function(e) {
+ e.success({ merge_error: 'Sorry, something went wrong.' });
});
+ this.class.mergeInProgress(null);
});
+ });
- return describe('getCIStatus', function() {
+ describe('getCIStatus', function() {
beforeEach(function() {
this.ciStatusData = {
"title": "Sample MR title",
+ "pipeline": 80,
"sha": "12a34bc5",
"status": "success",
"coverage": 98
@@ -165,7 +171,22 @@
this["class"].getCIStatus(true);
return expect(spy).not.toHaveBeenCalled();
});
+ it('should update the pipeline URL when the pipeline changes', function() {
+ var spy;
+ spy = spyOn(this["class"], 'updatePipelineUrls').and.stub();
+ this["class"].getCIStatus(false);
+ this.ciStatusData.pipeline += 1;
+ this["class"].getCIStatus(false);
+ return expect(spy).toHaveBeenCalled();
+ });
+ it('should update the commit URL when the sha changes', function() {
+ var spy;
+ spy = spyOn(this["class"], 'updateCommitUrls').and.stub();
+ this["class"].getCIStatus(false);
+ this.ciStatusData.sha = "9b50b99a";
+ this["class"].getCIStatus(false);
+ return expect(spy).toHaveBeenCalled();
+ });
});
});
-
}).call(this);
diff --git a/spec/javascripts/mini_pipeline_graph_dropdown_spec.js.es6 b/spec/javascripts/mini_pipeline_graph_dropdown_spec.js.es6
index d1793e9308e..7cdade01e00 100644
--- a/spec/javascripts/mini_pipeline_graph_dropdown_spec.js.es6
+++ b/spec/javascripts/mini_pipeline_graph_dropdown_spec.js.es6
@@ -1,14 +1,14 @@
/* eslint-disable no-new */
-//= require flash
-//= require mini_pipeline_graph_dropdown
+require('~/flash');
+require('~/mini_pipeline_graph_dropdown');
(() => {
describe('Mini Pipeline Graph Dropdown', () => {
- fixture.preload('mini_dropdown_graph');
+ preloadFixtures('static/mini_dropdown_graph.html.raw');
beforeEach(() => {
- fixture.load('mini_dropdown_graph');
+ loadFixtures('static/mini_dropdown_graph.html.raw');
});
describe('When is initialized', () => {
@@ -31,7 +31,7 @@
it('should call getBuildsList', () => {
const getBuildsListSpy = spyOn(gl.MiniPipelineGraph.prototype, 'getBuildsList').and.callFake(function () {});
- new gl.MiniPipelineGraph({ container: '.js-builds-dropdown-tests' });
+ new gl.MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }).bindEvents();
document.querySelector('.js-builds-dropdown-button').click();
@@ -41,7 +41,7 @@
it('should make a request to the endpoint provided in the html', () => {
const ajaxSpy = spyOn($, 'ajax').and.callFake(function () {});
- new gl.MiniPipelineGraph({ container: '.js-builds-dropdown-tests' });
+ new gl.MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }).bindEvents();
document.querySelector('.js-builds-dropdown-button').click();
expect(ajaxSpy.calls.allArgs()[0][0].url).toEqual('foobar');
diff --git a/spec/javascripts/new_branch_spec.js b/spec/javascripts/new_branch_spec.js
index a6cb9e47744..9b657868523 100644
--- a/spec/javascripts/new_branch_spec.js
+++ b/spec/javascripts/new_branch_spec.js
@@ -1,14 +1,14 @@
-/* eslint-disable space-before-function-paren, one-var, no-var, one-var-declaration-per-line, no-return-assign, quotes, padded-blocks, max-len */
+/* eslint-disable space-before-function-paren, one-var, no-var, one-var-declaration-per-line, no-return-assign, quotes, max-len */
/* global NewBranchForm */
-/*= require jquery-ui/autocomplete */
-/*= require new_branch_form */
+require('jquery-ui/ui/autocomplete');
+require('~/new_branch_form');
(function() {
describe('Branch', function() {
return describe('create a new branch', function() {
var expectToHaveError, fillNameWith;
- fixture.preload('new_branch.html');
+ preloadFixtures('static/new_branch.html.raw');
fillNameWith = function(value) {
return $('.js-branch-name').val(value).trigger('blur');
};
@@ -16,7 +16,7 @@
return expect($('.js-branch-name-error span').text()).toEqual(error);
};
beforeEach(function() {
- fixture.load('new_branch.html');
+ loadFixtures('static/new_branch.html.raw');
$('form').on('submit', function(e) {
return e.preventDefault();
});
@@ -166,5 +166,4 @@
});
});
});
-
}).call(this);
diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js
index d3bfa7730fa..af495787c54 100644
--- a/spec/javascripts/notes_spec.js
+++ b/spec/javascripts/notes_spec.js
@@ -1,10 +1,10 @@
-/* eslint-disable space-before-function-paren, no-unused-expressions, no-var, object-shorthand, comma-dangle, semi, padded-blocks, max-len */
+/* eslint-disable space-before-function-paren, no-unused-expressions, no-var, object-shorthand, comma-dangle, max-len */
/* global Notes */
-/*= require notes */
-/*= require autosize */
-/*= require gl_form */
-/*= require lib/utils/text_utility */
+require('~/notes');
+require('vendor/autosize');
+require('~/gl_form');
+require('~/lib/utils/text_utility');
(function() {
window.gon || (window.gon = {});
@@ -12,13 +12,14 @@
gl.utils = gl.utils || {};
describe('Notes', function() {
- var commentsTemplate = 'issues/issue_with_comment.raw';
- fixture.preload(commentsTemplate);
+ var commentsTemplate = 'issues/issue_with_comment.html.raw';
+ preloadFixtures(commentsTemplate);
beforeEach(function () {
- fixture.load(commentsTemplate);
+ loadFixtures(commentsTemplate);
gl.utils.disableButtonIfEmptyField = _.noop;
window.project_uploads_path = 'http://test.host/uploads';
+ $('body').data('page', 'projects:issues:show');
});
describe('task lists', function() {
@@ -71,8 +72,7 @@
$('.js-comment-button').click();
expect(this.autoSizeSpy).toHaveBeenTriggered();
- })
+ });
});
});
-
}).call(this);
diff --git a/spec/javascripts/pipelines_spec.js.es6 b/spec/javascripts/pipelines_spec.js.es6
index 85c9cf4b4f1..72770a702d3 100644
--- a/spec/javascripts/pipelines_spec.js.es6
+++ b/spec/javascripts/pipelines_spec.js.es6
@@ -1,11 +1,16 @@
-//= require pipelines
+require('~/pipelines');
+
+// Fix for phantomJS
+if (!Element.prototype.matches && Element.prototype.webkitMatchesSelector) {
+ Element.prototype.matches = Element.prototype.webkitMatchesSelector;
+}
(() => {
describe('Pipelines', () => {
- fixture.preload('pipeline_graph');
+ preloadFixtures('static/pipeline_graph.html.raw');
beforeEach(() => {
- fixture.load('pipeline_graph');
+ loadFixtures('static/pipeline_graph.html.raw');
});
it('should be defined', () => {
diff --git a/spec/javascripts/pretty_time_spec.js.es6 b/spec/javascripts/pretty_time_spec.js.es6
index 2e12d45f7a7..a4662cfb557 100644
--- a/spec/javascripts/pretty_time_spec.js.es6
+++ b/spec/javascripts/pretty_time_spec.js.es6
@@ -1,12 +1,12 @@
-//= require lib/utils/pretty_time
+require('~/lib/utils/pretty_time');
(() => {
- const PrettyTime = gl.PrettyTime;
+ const prettyTime = gl.utils.prettyTime;
- describe('PrettyTime methods', function () {
+ describe('prettyTime methods', function () {
describe('parseSeconds', function () {
it('should correctly parse a negative value', function () {
- const parser = PrettyTime.parseSeconds;
+ const parser = prettyTime.parseSeconds;
const zeroSeconds = parser(-1000);
@@ -17,7 +17,7 @@
});
it('should correctly parse a zero value', function () {
- const parser = PrettyTime.parseSeconds;
+ const parser = prettyTime.parseSeconds;
const zeroSeconds = parser(0);
@@ -28,7 +28,7 @@
});
it('should correctly parse a small non-zero second values', function () {
- const parser = PrettyTime.parseSeconds;
+ const parser = prettyTime.parseSeconds;
const subOneMinute = parser(10);
@@ -53,7 +53,7 @@
});
it('should correctly parse large second values', function () {
- const parser = PrettyTime.parseSeconds;
+ const parser = prettyTime.parseSeconds;
const aboveOneHour = parser(4800);
@@ -87,7 +87,7 @@
minutes: 20,
};
- const timeString = PrettyTime.stringifyTime(timeObject);
+ const timeString = prettyTime.stringifyTime(timeObject);
expect(timeString).toBe('1w 4d 7h 20m');
});
@@ -100,7 +100,7 @@
minutes: 20,
};
- const timeString = PrettyTime.stringifyTime(timeObject);
+ const timeString = prettyTime.stringifyTime(timeObject);
expect(timeString).toBe('4d 20m');
});
@@ -113,7 +113,7 @@
minutes: 0,
};
- const timeString = PrettyTime.stringifyTime(timeObject);
+ const timeString = prettyTime.stringifyTime(timeObject);
expect(timeString).toBe('0m');
});
@@ -122,12 +122,12 @@
describe('abbreviateTime', function () {
it('should abbreviate stringified times for weeks', function () {
const fullTimeString = '1w 3d 4h 5m';
- expect(PrettyTime.abbreviateTime(fullTimeString)).toBe('1w');
+ expect(prettyTime.abbreviateTime(fullTimeString)).toBe('1w');
});
it('should abbreviate stringified times for non-weeks', function () {
const fullTimeString = '0w 3d 4h 5m';
- expect(PrettyTime.abbreviateTime(fullTimeString)).toBe('3d');
+ expect(prettyTime.abbreviateTime(fullTimeString)).toBe('3d');
});
});
});
diff --git a/spec/javascripts/project_dashboard_spec.js.es6 b/spec/javascripts/project_dashboard_spec.js.es6
new file mode 100644
index 00000000000..24833b4eb57
--- /dev/null
+++ b/spec/javascripts/project_dashboard_spec.js.es6
@@ -0,0 +1,86 @@
+require('~/sidebar');
+
+(() => {
+ describe('Project dashboard page', () => {
+ let $pageWithSidebar = null;
+ let $sidebarToggle = null;
+ let sidebar = null;
+ const fixtureTemplate = 'projects/dashboard.html.raw';
+
+ const assertSidebarStateExpanded = (shouldBeExpanded) => {
+ expect(sidebar.isExpanded).toBe(shouldBeExpanded);
+ expect($pageWithSidebar.hasClass('page-sidebar-expanded')).toBe(shouldBeExpanded);
+ };
+
+ preloadFixtures(fixtureTemplate);
+ beforeEach(() => {
+ loadFixtures(fixtureTemplate);
+
+ $pageWithSidebar = $('.page-with-sidebar');
+ $sidebarToggle = $('.toggle-nav-collapse');
+
+ // otherwise instantiating the Sidebar for the second time
+ // won't do anything, as the Sidebar is a singleton class
+ gl.Sidebar.singleton = null;
+ sidebar = new gl.Sidebar();
+ });
+
+ it('can show the sidebar when the toggler is clicked', () => {
+ assertSidebarStateExpanded(false);
+ $sidebarToggle.click();
+ assertSidebarStateExpanded(true);
+ });
+
+ it('should dismiss the sidebar when clone button clicked', () => {
+ $sidebarToggle.click();
+ assertSidebarStateExpanded(true);
+
+ const cloneButton = $('.project-clone-holder a.clone-dropdown-btn');
+ cloneButton.click();
+ assertSidebarStateExpanded(false);
+ });
+
+ it('should dismiss the sidebar when download button clicked', () => {
+ $sidebarToggle.click();
+ assertSidebarStateExpanded(true);
+
+ const downloadButton = $('.project-action-button .btn:has(i.fa-download)');
+ downloadButton.click();
+ assertSidebarStateExpanded(false);
+ });
+
+ it('should dismiss the sidebar when add button clicked', () => {
+ $sidebarToggle.click();
+ assertSidebarStateExpanded(true);
+
+ const addButton = $('.project-action-button .btn:has(i.fa-plus)');
+ addButton.click();
+ assertSidebarStateExpanded(false);
+ });
+
+ it('should dismiss the sidebar when notification button clicked', () => {
+ $sidebarToggle.click();
+ assertSidebarStateExpanded(true);
+
+ const notifButton = $('.js-notification-toggle-btns .notifications-btn');
+ notifButton.click();
+ assertSidebarStateExpanded(false);
+ });
+
+ it('should dismiss the sidebar when clicking on the body', () => {
+ $sidebarToggle.click();
+ assertSidebarStateExpanded(true);
+
+ $('body').click();
+ assertSidebarStateExpanded(false);
+ });
+
+ it('should dismiss the sidebar when clicking on the project description header', () => {
+ $sidebarToggle.click();
+ assertSidebarStateExpanded(true);
+
+ $('.project-home-panel').click();
+ assertSidebarStateExpanded(false);
+ });
+ });
+})();
diff --git a/spec/javascripts/project_title_spec.js b/spec/javascripts/project_title_spec.js
index bb802a4b5e3..bfe3d2df79d 100644
--- a/spec/javascripts/project_title_spec.js
+++ b/spec/javascripts/project_title_spec.js
@@ -1,31 +1,32 @@
-/* eslint-disable space-before-function-paren, no-unused-expressions, no-return-assign, no-param-reassign, no-var, new-cap, wrap-iife, no-unused-vars, quotes, jasmine/no-expect-in-setup-teardown, padded-blocks, max-len */
-
+/* eslint-disable space-before-function-paren, no-unused-expressions, no-return-assign, no-param-reassign, no-var, new-cap, wrap-iife, no-unused-vars, quotes, jasmine/no-expect-in-setup-teardown, max-len */
/* global Project */
-/*= require bootstrap */
-/*= require select2 */
-/*= require lib/utils/type_utility */
-/*= require gl_dropdown */
-/*= require api */
-/*= require project_select */
-/*= require project */
+require('select2/select2.js');
+require('~/lib/utils/type_utility');
+require('~/gl_dropdown');
+require('~/api');
+require('~/project_select');
+require('~/project');
(function() {
- window.gon || (window.gon = {});
-
- window.gon.api_version = 'v3';
-
describe('Project Title', function() {
- fixture.preload('project_title.html');
- fixture.preload('projects.json');
+ preloadFixtures('static/project_title.html.raw');
+ loadJSONFixtures('projects.json');
+
beforeEach(function() {
- fixture.load('project_title.html');
+ loadFixtures('static/project_title.html.raw');
+
+ window.gon = {};
+ window.gon.api_version = 'v3';
+
return this.project = new Project();
});
- return describe('project list', function() {
+
+ describe('project list', function() {
var fakeAjaxResponse = function fakeAjaxResponse(req) {
var d;
expect(req.url).toBe('/api/v3/projects.json?simple=true');
+ expect(req.data).toEqual({ search: '', order_by: 'last_activity_at', per_page: 20 });
d = $.Deferred();
d.resolve(this.projects_data);
return d.promise();
@@ -33,7 +34,7 @@
beforeEach((function(_this) {
return function() {
- _this.projects_data = fixture.load('projects.json')[0];
+ _this.projects_data = getJSONFixture('projects.json');
return spyOn(jQuery, 'ajax').and.callFake(fakeAjaxResponse.bind(_this));
};
})(this));
@@ -48,6 +49,9 @@
return expect($('.header-content').hasClass('open')).toBe(false);
});
});
- });
+ afterEach(() => {
+ window.gon = {};
+ });
+ });
}).call(this);
diff --git a/spec/javascripts/right_sidebar_spec.js b/spec/javascripts/right_sidebar_spec.js
index a083dbf033a..f7636865aa1 100644
--- a/spec/javascripts/right_sidebar_spec.js
+++ b/spec/javascripts/right_sidebar_spec.js
@@ -1,11 +1,8 @@
-/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, new-parens, no-return-assign, new-cap, vars-on-top, semi, padded-blocks, max-len */
+/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, new-parens, no-return-assign, new-cap, vars-on-top, max-len */
/* global Sidebar */
-/*= require right_sidebar */
-/*= require jquery */
-/*= require js.cookie */
-
-/*= require extensions/jquery.js */
+require('~/right_sidebar');
+require('~/extensions/jquery.js');
(function() {
var $aside, $icon, $labelsIcon, $page, $toggle, assertSidebarState;
@@ -36,9 +33,11 @@
describe('RightSidebar', function() {
var fixtureName = 'issues/open-issue.html.raw';
- fixture.preload(fixtureName);
+ preloadFixtures(fixtureName);
+ loadJSONFixtures('todos.json');
+
beforeEach(function() {
- fixture.load(fixtureName);
+ loadFixtures(fixtureName);
this.sidebar = new Sidebar;
$aside = $('.right-sidebar');
$page = $('.page-with-sidebar');
@@ -65,9 +64,10 @@
});
it('should broadcast todo:toggle event when add todo clicked', function() {
+ var todos = getJSONFixture('todos.json');
spyOn(jQuery, 'ajax').and.callFake(function() {
var d = $.Deferred();
- var response = fixture.load('todos.json');
+ var response = todos;
d.resolve(response);
return d.promise();
});
@@ -77,7 +77,6 @@
$('.js-issuable-todo').click();
expect(todoToggleSpy.calls.count()).toEqual(1);
- })
+ });
});
-
}).call(this);
diff --git a/spec/javascripts/search_autocomplete_spec.js b/spec/javascripts/search_autocomplete_spec.js
index 1b7f642d59e..9572b52ec1e 100644
--- a/spec/javascripts/search_autocomplete_spec.js
+++ b/spec/javascripts/search_autocomplete_spec.js
@@ -1,25 +1,19 @@
-/* eslint-disable space-before-function-paren, max-len, no-var, one-var, one-var-declaration-per-line, no-unused-expressions, consistent-return, no-param-reassign, default-case, no-return-assign, comma-dangle, object-shorthand, prefer-template, quotes, new-parens, vars-on-top, new-cap, padded-blocks, max-len */
+/* eslint-disable space-before-function-paren, max-len, no-var, one-var, one-var-declaration-per-line, no-unused-expressions, consistent-return, no-param-reassign, default-case, no-return-assign, comma-dangle, object-shorthand, prefer-template, quotes, new-parens, vars-on-top, new-cap, max-len */
-/*= require gl_dropdown */
-/*= require search_autocomplete */
-/*= require jquery */
-/*= require lib/utils/common_utils */
-/*= require lib/utils/type_utility */
-/*= require fuzzaldrin-plus */
-/*= require turbolinks */
-/*= require jquery.turbolinks */
+require('~/gl_dropdown');
+require('~/search_autocomplete');
+require('~/lib/utils/common_utils');
+require('~/lib/utils/type_utility');
+require('vendor/fuzzaldrin-plus');
(function() {
var addBodyAttributes, assertLinks, dashboardIssuesPath, dashboardMRsPath, groupIssuesPath, groupMRsPath, groupName, mockDashboardOptions, mockGroupOptions, mockProjectOptions, projectIssuesPath, projectMRsPath, projectName, userId, widget;
+ var userName = 'root';
widget = null;
userId = 1;
- window.gon || (window.gon = {});
-
- window.gon.current_user_id = userId;
-
dashboardIssuesPath = '/dashboard/issues';
dashboardMRsPath = '/dashboard/merge_requests';
@@ -93,8 +87,8 @@
assertLinks = function(list, issuesPath, mrsPath) {
var a1, a2, a3, a4, issuesAssignedToMeLink, issuesIHaveCreatedLink, mrsAssignedToMeLink, mrsIHaveCreatedLink;
- issuesAssignedToMeLink = issuesPath + "/?assignee_id=" + userId;
- issuesIHaveCreatedLink = issuesPath + "/?author_id=" + userId;
+ issuesAssignedToMeLink = issuesPath + "/?assignee_username=" + userName;
+ issuesIHaveCreatedLink = issuesPath + "/?author_username=" + userName;
mrsAssignedToMeLink = mrsPath + "/?assignee_id=" + userId;
mrsIHaveCreatedLink = mrsPath + "/?author_id=" + userId;
a1 = "a[href='" + issuesAssignedToMeLink + "']";
@@ -112,16 +106,28 @@
};
describe('Search autocomplete dropdown', function() {
- fixture.preload('search_autocomplete.html');
+ preloadFixtures('static/search_autocomplete.html.raw');
beforeEach(function() {
- fixture.load('search_autocomplete.html');
+ loadFixtures('static/search_autocomplete.html.raw');
+ widget = new gl.SearchAutocomplete;
+ // Prevent turbolinks from triggering within gl_dropdown
+ spyOn(window.gl.utils, 'visitUrl').and.returnValue(true);
+
+ window.gon = {};
+ window.gon.current_user_id = userId;
+ window.gon.current_username = userName;
+
return widget = new gl.SearchAutocomplete;
});
+
+ afterEach(function() {
+ window.gon = {};
+ });
it('should show Dashboard specific dropdown menu', function() {
var list;
addBodyAttributes();
mockDashboardOptions();
- widget.searchInput.focus();
+ widget.searchInput.triggerHandler('focus');
list = widget.wrap.find('.dropdown-menu').find('ul');
return assertLinks(list, dashboardIssuesPath, dashboardMRsPath);
});
@@ -129,7 +135,7 @@
var list;
addBodyAttributes('group');
mockGroupOptions();
- widget.searchInput.focus();
+ widget.searchInput.triggerHandler('focus');
list = widget.wrap.find('.dropdown-menu').find('ul');
return assertLinks(list, groupIssuesPath, groupMRsPath);
});
@@ -137,7 +143,7 @@
var list;
addBodyAttributes('project');
mockProjectOptions();
- widget.searchInput.focus();
+ widget.searchInput.triggerHandler('focus');
list = widget.wrap.find('.dropdown-menu').find('ul');
return assertLinks(list, projectIssuesPath, projectMRsPath);
});
@@ -146,7 +152,7 @@
addBodyAttributes('project');
mockProjectOptions();
widget.searchInput.val('help');
- widget.searchInput.focus();
+ widget.searchInput.triggerHandler('focus');
list = widget.wrap.find('.dropdown-menu').find('ul');
link = "a[href='" + projectIssuesPath + "/?assignee_id=" + userId + "']";
return expect(list.find(link).length).toBe(0);
@@ -157,7 +163,7 @@
addBodyAttributes();
mockDashboardOptions(true);
var submitSpy = spyOnEvent('form', 'submit');
- widget.searchInput.focus();
+ widget.searchInput.triggerHandler('focus');
widget.wrap.trigger($.Event('keydown', { which: DOWN }));
var enterKeyEvent = $.Event('keydown', { which: ENTER });
widget.searchInput.trigger(enterKeyEvent);
@@ -169,5 +175,4 @@
expect(enterKeyEvent.isDefaultPrevented()).toBe(true);
});
});
-
}).call(this);
diff --git a/spec/javascripts/shortcuts_issuable_spec.js b/spec/javascripts/shortcuts_issuable_spec.js
index 7bc898aed5d..602ac01aec3 100644
--- a/spec/javascripts/shortcuts_issuable_spec.js
+++ b/spec/javascripts/shortcuts_issuable_spec.js
@@ -1,79 +1,82 @@
-/* eslint-disable space-before-function-paren, no-return-assign, no-var, quotes, padded-blocks */
+/* eslint-disable space-before-function-paren, no-return-assign, no-var, quotes */
/* global ShortcutsIssuable */
-/*= require shortcuts_issuable */
+require('~/copy_as_gfm');
+require('~/shortcuts_issuable');
(function() {
describe('ShortcutsIssuable', function() {
var fixtureName = 'issues/open-issue.html.raw';
- fixture.preload(fixtureName);
+ preloadFixtures(fixtureName);
beforeEach(function() {
- fixture.load(fixtureName);
+ loadFixtures(fixtureName);
document.querySelector('.js-new-note-form').classList.add('js-main-target-form');
- return this.shortcut = new ShortcutsIssuable();
+ this.shortcut = new ShortcutsIssuable();
});
- return describe('#replyWithSelectedText', function() {
+ describe('#replyWithSelectedText', function() {
var stubSelection;
- // Stub window.getSelection to return the provided String.
- stubSelection = function(text) {
- return window.getSelection = function() {
- return text;
+ // Stub window.gl.utils.getSelectedFragment to return a node with the provided HTML.
+ stubSelection = function(html) {
+ window.gl.utils.getSelectedFragment = function() {
+ var node = document.createElement('div');
+ node.innerHTML = html;
+ return node;
};
};
beforeEach(function() {
- return this.selector = 'form.js-main-target-form textarea#note_note';
+ this.selector = 'form.js-main-target-form textarea#note_note';
});
describe('with empty selection', function() {
- return it('does nothing', function() {
- stubSelection('');
+ it('does not return an error', function() {
this.shortcut.replyWithSelectedText();
- return expect($(this.selector).val()).toBe('');
+ expect($(this.selector).val()).toBe('');
+ });
+ it('triggers `input`', function() {
+ var focused = false;
+ $(this.selector).on('focus', function() {
+ focused = true;
+ });
+ this.shortcut.replyWithSelectedText();
+ expect(focused).toBe(true);
});
});
describe('with any selection', function() {
beforeEach(function() {
- return stubSelection('Selected text.');
+ stubSelection('<p>Selected text.</p>');
});
it('leaves existing input intact', function() {
$(this.selector).val('This text was already here.');
expect($(this.selector).val()).toBe('This text was already here.');
this.shortcut.replyWithSelectedText();
- return expect($(this.selector).val()).toBe("This text was already here.\n> Selected text.\n\n");
+ expect($(this.selector).val()).toBe("This text was already here.\n\n> Selected text.\n\n");
});
it('triggers `input`', function() {
- var triggered;
- triggered = false;
+ var triggered = false;
$(this.selector).on('input', function() {
- return triggered = true;
+ triggered = true;
});
this.shortcut.replyWithSelectedText();
- return expect(triggered).toBe(true);
+ expect(triggered).toBe(true);
});
- return it('triggers `focus`', function() {
- var focused;
- focused = false;
- $(this.selector).on('focus', function() {
- return focused = true;
- });
+ it('triggers `focus`', function() {
this.shortcut.replyWithSelectedText();
- return expect(focused).toBe(true);
+ expect(document.activeElement).toBe(document.querySelector(this.selector));
});
});
describe('with a one-line selection', function() {
- return it('quotes the selection', function() {
- stubSelection('This text has been selected.');
+ it('quotes the selection', function() {
+ stubSelection('<p>This text has been selected.</p>');
this.shortcut.replyWithSelectedText();
- return expect($(this.selector).val()).toBe("> This text has been selected.\n\n");
+ expect($(this.selector).val()).toBe("> This text has been selected.\n\n");
});
});
- return describe('with a multi-line selection', function() {
- return it('quotes the selected lines as a group', function() {
- stubSelection("Selected line one.\n\nSelected line two.\nSelected line three.\n");
+ describe('with a multi-line selection', function() {
+ it('quotes the selected lines as a group', function() {
+ stubSelection("<p>Selected line one.</p>\n\n<p>Selected line two.</p>\n\n<p>Selected line three.</p>");
this.shortcut.replyWithSelectedText();
- return expect($(this.selector).val()).toBe("> Selected line one.\n> Selected line two.\n> Selected line three.\n\n");
+ expect($(this.selector).val()).toBe("> Selected line one.\n>\n> Selected line two.\n>\n> Selected line three.\n\n");
});
});
});
});
-
}).call(this);
diff --git a/spec/javascripts/signin_tabs_memoizer_spec.js.es6 b/spec/javascripts/signin_tabs_memoizer_spec.js.es6
index 9a9fb22255b..d83d9a57b42 100644
--- a/spec/javascripts/signin_tabs_memoizer_spec.js.es6
+++ b/spec/javascripts/signin_tabs_memoizer_spec.js.es6
@@ -1,8 +1,8 @@
-/*= require signin_tabs_memoizer */
+require('~/signin_tabs_memoizer');
((global) => {
describe('SigninTabsMemoizer', () => {
- const fixtureTemplate = 'signin_tabs.html';
+ const fixtureTemplate = 'static/signin_tabs.html.raw';
const tabSelector = 'ul.nav-tabs';
const currentTabKey = 'current_signin_tab';
let memo;
@@ -15,10 +15,10 @@
return memo;
}
- fixture.preload(fixtureTemplate);
+ preloadFixtures(fixtureTemplate);
beforeEach(() => {
- fixture.load(fixtureTemplate);
+ loadFixtures(fixtureTemplate);
});
it('does nothing if no tab was previously selected', () => {
diff --git a/spec/javascripts/smart_interval_spec.js.es6 b/spec/javascripts/smart_interval_spec.js.es6
index 1b7ca97cde4..4366ec2a5b8 100644
--- a/spec/javascripts/smart_interval_spec.js.es6
+++ b/spec/javascripts/smart_interval_spec.js.es6
@@ -1,5 +1,4 @@
-//= require jquery
-//= require smart_interval
+require('~/smart_interval');
(() => {
const DEFAULT_MAX_INTERVAL = 100;
@@ -103,7 +102,7 @@
describe('DOM Events', function () {
beforeEach(function () {
// This ensures DOM and DOM events are initialized for these specs.
- fixture.set('<div></div>');
+ setFixtures('<div></div>');
this.smartInterval = createDefaultSmartInterval();
});
@@ -164,7 +163,7 @@
const interval = this.smartInterval;
setTimeout(() => {
- $(document).trigger('page:before-unload');
+ $(document).triggerHandler('beforeunload');
expect(interval.state.intervalId).toBeUndefined();
expect(interval.getCurrentInterval()).toBe(interval.cfg.startingInterval);
done();
diff --git a/spec/javascripts/spec_helper.js b/spec/javascripts/spec_helper.js
deleted file mode 100644
index 831dfada952..00000000000
--- a/spec/javascripts/spec_helper.js
+++ /dev/null
@@ -1,48 +0,0 @@
-/* eslint-disable space-before-function-paren */
-// PhantomJS (Teaspoons default driver) doesn't have support for
-// Function.prototype.bind, which has caused confusion. Use this polyfill to
-// avoid the confusion.
-/*= require support/bind-poly */
-
-// You can require your own javascript files here. By default this will include
-// everything in application, however you may get better load performance if you
-// require the specific files that are being used in the spec that tests them.
-/*= require jquery */
-/*= require jquery.turbolinks */
-/*= require bootstrap */
-/*= require underscore */
-
-// Teaspoon includes some support files, but you can use anything from your own
-// support path too.
-// require support/jasmine-jquery-1.7.0
-// require support/jasmine-jquery-2.0.0
-/*= require support/jasmine-jquery-2.1.0 */
-
-// require support/sinon
-// require support/your-support-file
-// Deferring execution
-// If you're using CommonJS, RequireJS or some other asynchronous library you can
-// defer execution. Call Teaspoon.execute() after everything has been loaded.
-// Simple example of a timeout:
-// Teaspoon.defer = true
-// setTimeout(Teaspoon.execute, 1000)
-// Matching files
-// By default Teaspoon will look for files that match
-// _spec.{js,js.es6}. Add a filename_spec.js file in your spec path
-// and it'll be included in the default suite automatically. If you want to
-// customize suites, check out the configuration in teaspoon_env.rb
-// Manifest
-// If you'd rather require your spec files manually (to control order for
-// instance) you can disable the suite matcher in the configuration and use this
-// file as a manifest.
-// For more information: http://github.com/modeset/teaspoon
-
-(function() {
-
-
-}).call(this);
-
-// defined in ActionDispatch::TestRequest
-// see https://github.com/rails/rails/blob/v4.2.7.1/actionpack/lib/action_dispatch/testing/test_request.rb#L7
-window.gl = window.gl || {};
-gl.TEST_HOST = 'http://test.host';
diff --git a/spec/javascripts/subbable_resource_spec.js.es6 b/spec/javascripts/subbable_resource_spec.js.es6
index 6a70dd856a7..454386697f5 100644
--- a/spec/javascripts/subbable_resource_spec.js.es6
+++ b/spec/javascripts/subbable_resource_spec.js.es6
@@ -1,9 +1,6 @@
-/* eslint-disable max-len, arrow-parens, comma-dangle, no-plusplus */
+/* eslint-disable max-len, arrow-parens, comma-dangle */
-//= vue
-//= vue-resource
-//= require jquery
-//= require subbable_resource
+require('~/subbable_resource');
/*
* Test that each rest verb calls the publish and subscribe function and passes the correct value back
@@ -53,7 +50,7 @@
this.MockResource.subscribe(callbacks.two);
this.MockResource.subscribe(callbacks.three);
- state.myprop++;
+ state.myprop += 1;
this.MockResource.publish(state);
diff --git a/spec/javascripts/syntax_highlight_spec.js b/spec/javascripts/syntax_highlight_spec.js
index ac411f6c306..c0c3837d1f4 100644
--- a/spec/javascripts/syntax_highlight_spec.js
+++ b/spec/javascripts/syntax_highlight_spec.js
@@ -1,6 +1,6 @@
-/* eslint-disable space-before-function-paren, no-var, no-return-assign, quotes, padded-blocks */
+/* eslint-disable space-before-function-paren, no-var, no-return-assign, quotes */
-/*= require syntax_highlight */
+require('~/syntax_highlight');
(function() {
describe('Syntax Highlighter', function() {
@@ -13,7 +13,7 @@
};
describe('on a js-syntax-highlight element', function() {
beforeEach(function() {
- return fixture.set('<div class="js-syntax-highlight"></div>');
+ return setFixtures('<div class="js-syntax-highlight"></div>');
});
return it('applies syntax highlighting', function() {
stubUserColorScheme('monokai');
@@ -23,7 +23,7 @@
});
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>");
+ return setFixtures("<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');
@@ -33,7 +33,7 @@
});
return it('prevents an infinite loop when no matches exist', function() {
var highlight;
- fixture.set('<div></div>');
+ setFixtures('<div></div>');
highlight = function() {
return $('div').syntaxHighlight();
};
@@ -41,5 +41,4 @@
});
});
});
-
}).call(this);
diff --git a/spec/javascripts/test_bundle.js b/spec/javascripts/test_bundle.js
new file mode 100644
index 00000000000..7df8d2fd8b4
--- /dev/null
+++ b/spec/javascripts/test_bundle.js
@@ -0,0 +1,44 @@
+// enable test fixtures
+require('jasmine-jquery');
+
+jasmine.getFixtures().fixturesPath = 'base/spec/javascripts/fixtures';
+jasmine.getJSONFixtures().fixturesPath = 'base/spec/javascripts/fixtures';
+
+// include common libraries
+window.$ = window.jQuery = require('jquery');
+window._ = require('underscore');
+window.Cookies = require('vendor/js.cookie');
+window.Vue = require('vue');
+window.Vue.use(require('vue-resource'));
+require('jquery-ujs');
+require('bootstrap/js/affix');
+require('bootstrap/js/alert');
+require('bootstrap/js/button');
+require('bootstrap/js/collapse');
+require('bootstrap/js/dropdown');
+require('bootstrap/js/modal');
+require('bootstrap/js/scrollspy');
+require('bootstrap/js/tab');
+require('bootstrap/js/transition');
+require('bootstrap/js/tooltip');
+require('bootstrap/js/popover');
+
+// stub expected globals
+window.gl = window.gl || {};
+window.gl.TEST_HOST = 'http://test.host';
+window.gon = window.gon || {};
+
+// render all of our tests
+const testsContext = require.context('.', true, /_spec$/);
+testsContext.keys().forEach(function (path) {
+ try {
+ testsContext(path);
+ } catch (err) {
+ console.error('[ERROR] Unable to load spec: ', path);
+ describe('Test bundle', function () {
+ it(`includes '${path}'`, function () {
+ expect(err).toBeNull();
+ });
+ });
+ }
+});
diff --git a/spec/javascripts/u2f/authenticate_spec.js b/spec/javascripts/u2f/authenticate_spec.js
index a8874ab12d3..cba1af4daa4 100644
--- a/spec/javascripts/u2f/authenticate_spec.js
+++ b/spec/javascripts/u2f/authenticate_spec.js
@@ -1,40 +1,44 @@
-/* eslint-disable space-before-function-paren, new-parens, quotes, comma-dangle, no-var, one-var, one-var-declaration-per-line, padded-blocks, max-len */
+/* eslint-disable space-before-function-paren, new-parens, quotes, comma-dangle, no-var, one-var, one-var-declaration-per-line, max-len */
/* global MockU2FDevice */
/* global U2FAuthenticate */
-/*= require u2f/authenticate */
-/*= require u2f/util */
-/*= require u2f/error */
-/*= require u2f */
-/*= require ./mock_u2f_device */
+require('~/u2f/authenticate');
+require('~/u2f/util');
+require('~/u2f/error');
+require('vendor/u2f');
+require('./mock_u2f_device');
(function() {
describe('U2FAuthenticate', function() {
- fixture.load('u2f/authenticate');
+ preloadFixtures('u2f/authenticate.html.raw');
+
beforeEach(function() {
+ loadFixtures('u2f/authenticate.html.raw');
this.u2fDevice = new MockU2FDevice;
this.container = $("#js-authenticate-u2f");
- this.component = new U2FAuthenticate(this.container, {
- sign_requests: []
- }, "token");
+ this.component = new window.gl.U2FAuthenticate(
+ this.container,
+ '#js-login-u2f-form',
+ {
+ sign_requests: []
+ },
+ document.querySelector('#js-login-2fa-device'),
+ document.querySelector('.js-2fa-form')
+ );
+
+ // bypass automatic form submission within renderAuthenticated
+ spyOn(this.component, 'renderAuthenticated').and.returnValue(true);
+
return this.component.start();
});
it('allows authenticating via a U2F device', function() {
- var authenticatedMessage, deviceResponse, inProgressMessage, 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('Sign in via U2F device');
- setupButton.trigger('click');
+ var inProgressMessage;
inProgressMessage = this.container.find("p");
expect(inProgressMessage.text()).toContain("Trying to communicate with your device");
this.u2fDevice.respondToAuthenticateRequest({
deviceData: "this is data from the device"
});
- authenticatedMessage = this.container.find("p");
- deviceResponse = this.container.find('#js-device-response');
- expect(authenticatedMessage.text()).toContain("Click this button to authenticate with the GitLab server");
- return expect(deviceResponse.val()).toBe('{"deviceData":"this is data from the device"}');
+ expect(this.component.renderAuthenticated).toHaveBeenCalledWith('{"deviceData":"this is data from the device"}');
});
return describe("errors", function() {
it("displays an error message", function() {
@@ -48,7 +52,7 @@
return expect(errorMessage.text()).toContain("There was a problem communicating with your device");
});
return it("allows retrying authentication after an error", function() {
- var authenticatedMessage, retryButton, setupButton;
+ var retryButton, setupButton;
setupButton = this.container.find("#js-login-u2f-device");
setupButton.trigger('click');
this.u2fDevice.respondToAuthenticateRequest({
@@ -61,10 +65,8 @@
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");
+ expect(this.component.renderAuthenticated).toHaveBeenCalledWith('{"deviceData":"this is data from the device"}');
});
});
});
-
}).call(this);
diff --git a/spec/javascripts/u2f/mock_u2f_device.js b/spec/javascripts/u2f/mock_u2f_device.js
index 1459f968c3d..287bfb4138b 100644
--- a/spec/javascripts/u2f/mock_u2f_device.js
+++ b/spec/javascripts/u2f/mock_u2f_device.js
@@ -1,6 +1,7 @@
-/* eslint-disable space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-unused-expressions, no-return-assign, no-param-reassign, padded-blocks, max-len */
+/* eslint-disable space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-unused-expressions, no-return-assign, no-param-reassign, max-len */
+
(function() {
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+ var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
this.MockU2FDevice = (function() {
function MockU2FDevice() {
@@ -28,7 +29,5 @@
};
return MockU2FDevice;
-
})();
-
}).call(this);
diff --git a/spec/javascripts/u2f/register_spec.js b/spec/javascripts/u2f/register_spec.js
index 189592ea87a..10578c2c4b5 100644
--- a/spec/javascripts/u2f/register_spec.js
+++ b/spec/javascripts/u2f/register_spec.js
@@ -1,17 +1,19 @@
-/* eslint-disable space-before-function-paren, new-parens, quotes, no-var, one-var, one-var-declaration-per-line, comma-dangle, padded-blocks, max-len */
+/* eslint-disable space-before-function-paren, new-parens, quotes, no-var, one-var, one-var-declaration-per-line, comma-dangle, max-len */
/* global MockU2FDevice */
/* global U2FRegister */
-/*= require u2f/register */
-/*= require u2f/util */
-/*= require u2f/error */
-/*= require u2f */
-/*= require ./mock_u2f_device */
+require('~/u2f/register');
+require('~/u2f/util');
+require('~/u2f/error');
+require('vendor/u2f');
+require('./mock_u2f_device');
(function() {
describe('U2FRegister', function() {
- fixture.load('u2f/register');
+ preloadFixtures('u2f/register.html.raw');
+
beforeEach(function() {
+ loadFixtures('u2f/register.html.raw');
this.u2fDevice = new MockU2FDevice;
this.container = $("#js-register-u2f");
this.component = new U2FRegister(this.container, $("#js-register-u2f-templates"), {}, "token");
@@ -72,5 +74,4 @@
});
});
});
-
}).call(this);
diff --git a/spec/javascripts/visibility_select_spec.js.es6 b/spec/javascripts/visibility_select_spec.js.es6
new file mode 100644
index 00000000000..9727c03c91e
--- /dev/null
+++ b/spec/javascripts/visibility_select_spec.js.es6
@@ -0,0 +1,100 @@
+require('~/visibility_select');
+
+(() => {
+ const VisibilitySelect = gl.VisibilitySelect;
+
+ describe('VisibilitySelect', function () {
+ const lockedElement = document.createElement('div');
+ lockedElement.dataset.helpBlock = 'lockedHelpBlock';
+
+ const checkedElement = document.createElement('div');
+ checkedElement.dataset.description = 'checkedDescription';
+
+ const mockElements = {
+ container: document.createElement('div'),
+ select: document.createElement('div'),
+ '.help-block': document.createElement('div'),
+ '.js-locked': lockedElement,
+ 'option:checked': checkedElement,
+ };
+
+ beforeEach(function () {
+ spyOn(Element.prototype, 'querySelector').and.callFake(selector => mockElements[selector]);
+ });
+
+ describe('#constructor', function () {
+ beforeEach(function () {
+ this.visibilitySelect = new VisibilitySelect(mockElements.container);
+ });
+
+ it('sets the container member', function () {
+ expect(this.visibilitySelect.container).toEqual(mockElements.container);
+ });
+
+ it('queries and sets the helpBlock member', function () {
+ expect(Element.prototype.querySelector).toHaveBeenCalledWith('.help-block');
+ expect(this.visibilitySelect.helpBlock).toEqual(mockElements['.help-block']);
+ });
+
+ it('queries and sets the select member', function () {
+ expect(Element.prototype.querySelector).toHaveBeenCalledWith('select');
+ expect(this.visibilitySelect.select).toEqual(mockElements.select);
+ });
+
+ describe('if there is no container element provided', function () {
+ it('throws an error', function () {
+ expect(() => new VisibilitySelect()).toThrowError('VisibilitySelect requires a container element as argument 1');
+ });
+ });
+ });
+
+ describe('#init', function () {
+ describe('if there is a select', function () {
+ beforeEach(function () {
+ this.visibilitySelect = new VisibilitySelect(mockElements.container);
+ });
+
+ it('calls updateHelpText', function () {
+ spyOn(VisibilitySelect.prototype, 'updateHelpText');
+ this.visibilitySelect.init();
+ expect(this.visibilitySelect.updateHelpText).toHaveBeenCalled();
+ });
+
+ it('adds a change event listener', function () {
+ spyOn(this.visibilitySelect.select, 'addEventListener');
+ this.visibilitySelect.init();
+ expect(this.visibilitySelect.select.addEventListener.calls.argsFor(0)).toContain('change');
+ });
+ });
+
+ describe('if there is no select', function () {
+ beforeEach(function () {
+ mockElements.select = undefined;
+ this.visibilitySelect = new VisibilitySelect(mockElements.container);
+ this.visibilitySelect.init();
+ });
+
+ it('updates the helpBlock text to the locked `data-help-block` messaged', function () {
+ expect(this.visibilitySelect.helpBlock.textContent)
+ .toEqual(lockedElement.dataset.helpBlock);
+ });
+
+ afterEach(function () {
+ mockElements.select = document.createElement('div');
+ });
+ });
+ });
+
+ describe('#updateHelpText', function () {
+ beforeEach(function () {
+ this.visibilitySelect = new VisibilitySelect(mockElements.container);
+ this.visibilitySelect.init();
+ });
+
+ it('updates the helpBlock text to the selected options `data-description`', function () {
+ expect(this.visibilitySelect.helpBlock.textContent)
+ .toEqual(checkedElement.dataset.description);
+ });
+ });
+ });
+})();
diff --git a/spec/javascripts/vue_common_components/commit_spec.js.es6 b/spec/javascripts/vue_common_components/commit_spec.js.es6
deleted file mode 100644
index 26dfdb94aae..00000000000
--- a/spec/javascripts/vue_common_components/commit_spec.js.es6
+++ /dev/null
@@ -1,131 +0,0 @@
-//= require vue_common_component/commit
-
-describe('Commit component', () => {
- let props;
- let component;
-
- it('should render a code-fork icon if it does not represent a tag', () => {
- fixture.set('<div class="test-commit-container"></div>');
- component = new window.gl.CommitComponent({
- el: document.querySelector('.test-commit-container'),
- propsData: {
- tag: false,
- commitRef: {
- name: 'master',
- ref_url: 'http://localhost/namespace2/gitlabhq/tree/master',
- },
- commitUrl: 'https://gitlab.com/gitlab-org/gitlab-ce/commit/b7836eddf62d663c665769e1b0960197fd215067',
- shortSha: 'b7836edd',
- title: 'Commit message',
- author: {
- avatar_url: 'https://gitlab.com/uploads/user/avatar/300478/avatar.png',
- web_url: 'https://gitlab.com/jschatz1',
- username: 'jschatz1',
- },
- },
- });
-
- expect(component.$el.querySelector('.icon-container i').classList).toContain('fa-code-fork');
- });
-
- describe('Given all the props', () => {
- beforeEach(() => {
- fixture.set('<div class="test-commit-container"></div>');
-
- props = {
- tag: true,
- commitRef: {
- name: 'master',
- ref_url: 'http://localhost/namespace2/gitlabhq/tree/master',
- },
- commitUrl: 'https://gitlab.com/gitlab-org/gitlab-ce/commit/b7836eddf62d663c665769e1b0960197fd215067',
- shortSha: 'b7836edd',
- title: 'Commit message',
- author: {
- avatar_url: 'https://gitlab.com/uploads/user/avatar/300478/avatar.png',
- web_url: 'https://gitlab.com/jschatz1',
- username: 'jschatz1',
- },
- commitIconSvg: '<svg></svg>',
- };
-
- component = new window.gl.CommitComponent({
- el: document.querySelector('.test-commit-container'),
- propsData: props,
- });
- });
-
- it('should render a tag icon if it represents a tag', () => {
- expect(component.$el.querySelector('.icon-container i').classList).toContain('fa-tag');
- });
-
- it('should render a link to the ref url', () => {
- expect(component.$el.querySelector('.branch-name').getAttribute('href')).toEqual(props.commitRef.ref_url);
- });
-
- it('should render the ref name', () => {
- expect(component.$el.querySelector('.branch-name').textContent).toContain(props.commitRef.name);
- });
-
- it('should render the commit short sha with a link to the commit url', () => {
- expect(component.$el.querySelector('.commit-id').getAttribute('href')).toEqual(props.commitUrl);
- expect(component.$el.querySelector('.commit-id').textContent).toContain(props.shortSha);
- });
-
- it('should render the given commitIconSvg', () => {
- expect(component.$el.querySelector('.js-commit-icon').children).toContain('svg');
- });
-
- describe('Given commit title and author props', () => {
- it('should render a link to the author profile', () => {
- expect(
- component.$el.querySelector('.commit-title .avatar-image-container').getAttribute('href'),
- ).toEqual(props.author.web_url);
- });
-
- it('Should render the author avatar with title and alt attributes', () => {
- expect(
- component.$el.querySelector('.commit-title .avatar-image-container img').getAttribute('title'),
- ).toContain(props.author.username);
- expect(
- component.$el.querySelector('.commit-title .avatar-image-container img').getAttribute('alt'),
- ).toContain(`${props.author.username}'s avatar`);
- });
- });
-
- it('should render the commit title', () => {
- expect(
- component.$el.querySelector('a.commit-row-message').getAttribute('href'),
- ).toEqual(props.commitUrl);
- expect(
- component.$el.querySelector('a.commit-row-message').textContent,
- ).toContain(props.title);
- });
- });
-
- describe('When commit title is not provided', () => {
- it('should render default message', () => {
- fixture.set('<div class="test-commit-container"></div>');
- props = {
- tag: false,
- commitRef: {
- name: 'master',
- ref_url: 'http://localhost/namespace2/gitlabhq/tree/master',
- },
- commitUrl: 'https://gitlab.com/gitlab-org/gitlab-ce/commit/b7836eddf62d663c665769e1b0960197fd215067',
- shortSha: 'b7836edd',
- title: null,
- author: {},
- };
-
- component = new window.gl.CommitComponent({
- el: document.querySelector('.test-commit-container'),
- propsData: props,
- });
-
- expect(
- component.$el.querySelector('.commit-title span').textContent,
- ).toContain('Cant find HEAD commit for this branch');
- });
- });
-});
diff --git a/spec/javascripts/vue_shared/components/commit_spec.js.es6 b/spec/javascripts/vue_shared/components/commit_spec.js.es6
new file mode 100644
index 00000000000..15ab10b9b69
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/commit_spec.js.es6
@@ -0,0 +1,131 @@
+require('~/vue_shared/components/commit');
+
+describe('Commit component', () => {
+ let props;
+ let component;
+
+ it('should render a code-fork icon if it does not represent a tag', () => {
+ setFixtures('<div class="test-commit-container"></div>');
+ component = new window.gl.CommitComponent({
+ el: document.querySelector('.test-commit-container'),
+ propsData: {
+ tag: false,
+ commitRef: {
+ name: 'master',
+ ref_url: 'http://localhost/namespace2/gitlabhq/tree/master',
+ },
+ commitUrl: 'https://gitlab.com/gitlab-org/gitlab-ce/commit/b7836eddf62d663c665769e1b0960197fd215067',
+ shortSha: 'b7836edd',
+ title: 'Commit message',
+ author: {
+ avatar_url: 'https://gitlab.com/uploads/user/avatar/300478/avatar.png',
+ web_url: 'https://gitlab.com/jschatz1',
+ username: 'jschatz1',
+ },
+ },
+ });
+
+ expect(component.$el.querySelector('.icon-container i').classList).toContain('fa-code-fork');
+ });
+
+ describe('Given all the props', () => {
+ beforeEach(() => {
+ setFixtures('<div class="test-commit-container"></div>');
+
+ props = {
+ tag: true,
+ commitRef: {
+ name: 'master',
+ ref_url: 'http://localhost/namespace2/gitlabhq/tree/master',
+ },
+ commitUrl: 'https://gitlab.com/gitlab-org/gitlab-ce/commit/b7836eddf62d663c665769e1b0960197fd215067',
+ shortSha: 'b7836edd',
+ title: 'Commit message',
+ author: {
+ avatar_url: 'https://gitlab.com/uploads/user/avatar/300478/avatar.png',
+ web_url: 'https://gitlab.com/jschatz1',
+ username: 'jschatz1',
+ },
+ commitIconSvg: '<svg></svg>',
+ };
+
+ component = new window.gl.CommitComponent({
+ el: document.querySelector('.test-commit-container'),
+ propsData: props,
+ });
+ });
+
+ it('should render a tag icon if it represents a tag', () => {
+ expect(component.$el.querySelector('.icon-container i').classList).toContain('fa-tag');
+ });
+
+ it('should render a link to the ref url', () => {
+ expect(component.$el.querySelector('.branch-name').getAttribute('href')).toEqual(props.commitRef.ref_url);
+ });
+
+ it('should render the ref name', () => {
+ expect(component.$el.querySelector('.branch-name').textContent).toContain(props.commitRef.name);
+ });
+
+ it('should render the commit short sha with a link to the commit url', () => {
+ expect(component.$el.querySelector('.commit-id').getAttribute('href')).toEqual(props.commitUrl);
+ expect(component.$el.querySelector('.commit-id').textContent).toContain(props.shortSha);
+ });
+
+ it('should render the given commitIconSvg', () => {
+ expect(component.$el.querySelector('.js-commit-icon').children).toContain('svg');
+ });
+
+ describe('Given commit title and author props', () => {
+ it('should render a link to the author profile', () => {
+ expect(
+ component.$el.querySelector('.commit-title .avatar-image-container').getAttribute('href'),
+ ).toEqual(props.author.web_url);
+ });
+
+ it('Should render the author avatar with title and alt attributes', () => {
+ expect(
+ component.$el.querySelector('.commit-title .avatar-image-container img').getAttribute('title'),
+ ).toContain(props.author.username);
+ expect(
+ component.$el.querySelector('.commit-title .avatar-image-container img').getAttribute('alt'),
+ ).toContain(`${props.author.username}'s avatar`);
+ });
+ });
+
+ it('should render the commit title', () => {
+ expect(
+ component.$el.querySelector('a.commit-row-message').getAttribute('href'),
+ ).toEqual(props.commitUrl);
+ expect(
+ component.$el.querySelector('a.commit-row-message').textContent,
+ ).toContain(props.title);
+ });
+ });
+
+ describe('When commit title is not provided', () => {
+ it('should render default message', () => {
+ setFixtures('<div class="test-commit-container"></div>');
+ props = {
+ tag: false,
+ commitRef: {
+ name: 'master',
+ ref_url: 'http://localhost/namespace2/gitlabhq/tree/master',
+ },
+ commitUrl: 'https://gitlab.com/gitlab-org/gitlab-ce/commit/b7836eddf62d663c665769e1b0960197fd215067',
+ shortSha: 'b7836edd',
+ title: null,
+ author: {},
+ };
+
+ component = new window.gl.CommitComponent({
+ el: document.querySelector('.test-commit-container'),
+ propsData: props,
+ });
+
+ expect(
+ component.$el.querySelector('.commit-title span').textContent,
+ ).toContain('Cant find HEAD commit for this branch');
+ });
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js.es6 b/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js.es6
new file mode 100644
index 00000000000..412abfd5e41
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js.es6
@@ -0,0 +1,87 @@
+require('~/vue_shared/components/pipelines_table_row');
+const pipeline = require('../../commit/pipelines/mock_data');
+
+describe('Pipelines Table Row', () => {
+ let component;
+ preloadFixtures('static/environments/element.html.raw');
+
+ beforeEach(() => {
+ loadFixtures('static/environments/element.html.raw');
+
+ component = new gl.pipelines.PipelinesTableRowComponent({
+ el: document.querySelector('.test-dom-element'),
+ propsData: {
+ pipeline,
+ svgs: {},
+ },
+ });
+ });
+
+ it('should render a table row', () => {
+ expect(component.$el).toEqual('TR');
+ });
+
+ describe('status column', () => {
+ it('should render a pipeline link', () => {
+ expect(
+ component.$el.querySelector('td.commit-link a').getAttribute('href'),
+ ).toEqual(pipeline.path);
+ });
+
+ it('should render status text', () => {
+ expect(
+ component.$el.querySelector('td.commit-link a').textContent,
+ ).toContain(pipeline.details.status.text);
+ });
+ });
+
+ describe('information column', () => {
+ it('should render a pipeline link', () => {
+ expect(
+ component.$el.querySelector('td:nth-child(2) a').getAttribute('href'),
+ ).toEqual(pipeline.path);
+ });
+
+ it('should render pipeline ID', () => {
+ expect(
+ component.$el.querySelector('td:nth-child(2) a > span').textContent,
+ ).toEqual(`#${pipeline.id}`);
+ });
+
+ describe('when a user is provided', () => {
+ it('should render user information', () => {
+ expect(
+ component.$el.querySelector('td:nth-child(2) a:nth-child(3)').getAttribute('href'),
+ ).toEqual(pipeline.user.web_url);
+
+ expect(
+ component.$el.querySelector('td:nth-child(2) img').getAttribute('title'),
+ ).toEqual(pipeline.user.name);
+ });
+ });
+ });
+
+ describe('commit column', () => {
+ it('should render link to commit', () => {
+ expect(
+ component.$el.querySelector('td:nth-child(3) .commit-id').getAttribute('href'),
+ ).toEqual(pipeline.commit.commit_path);
+ });
+ });
+
+ describe('stages column', () => {
+ it('should render an icon for each stage', () => {
+ expect(
+ component.$el.querySelectorAll('td:nth-child(4) .js-builds-dropdown-button').length,
+ ).toEqual(pipeline.details.stages.length);
+ });
+ });
+
+ describe('actions column', () => {
+ it('should render the provided actions', () => {
+ expect(
+ component.$el.querySelectorAll('td:nth-child(6) ul li').length,
+ ).toEqual(pipeline.details.manual_actions.length);
+ });
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/pipelines_table_spec.js.es6 b/spec/javascripts/vue_shared/components/pipelines_table_spec.js.es6
new file mode 100644
index 00000000000..54d81e2ea7d
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/pipelines_table_spec.js.es6
@@ -0,0 +1,64 @@
+require('~/vue_shared/components/pipelines_table');
+require('~/lib/utils/datetime_utility');
+const pipeline = require('../../commit/pipelines/mock_data');
+
+describe('Pipelines Table', () => {
+ preloadFixtures('static/environments/element.html.raw');
+
+ beforeEach(() => {
+ loadFixtures('static/environments/element.html.raw');
+ });
+
+ describe('table', () => {
+ let component;
+ beforeEach(() => {
+ component = new gl.pipelines.PipelinesTableComponent({
+ el: document.querySelector('.test-dom-element'),
+ propsData: {
+ pipelines: [],
+ svgs: {},
+ },
+ });
+ });
+
+ it('should render a table', () => {
+ expect(component.$el).toEqual('TABLE');
+ });
+
+ it('should render table head with correct columns', () => {
+ expect(component.$el.querySelector('th.js-pipeline-status').textContent).toEqual('Status');
+ expect(component.$el.querySelector('th.js-pipeline-info').textContent).toEqual('Pipeline');
+ expect(component.$el.querySelector('th.js-pipeline-commit').textContent).toEqual('Commit');
+ expect(component.$el.querySelector('th.js-pipeline-stages').textContent).toEqual('Stages');
+ expect(component.$el.querySelector('th.js-pipeline-date').textContent).toEqual('');
+ expect(component.$el.querySelector('th.js-pipeline-actions').textContent).toEqual('');
+ });
+ });
+
+ describe('without data', () => {
+ it('should render an empty table', () => {
+ const component = new gl.pipelines.PipelinesTableComponent({
+ el: document.querySelector('.test-dom-element'),
+ propsData: {
+ pipelines: [],
+ svgs: {},
+ },
+ });
+ expect(component.$el.querySelectorAll('tbody tr').length).toEqual(0);
+ });
+ });
+
+ describe('with data', () => {
+ it('should render rows', () => {
+ const component = new gl.pipelines.PipelinesTableComponent({
+ el: document.querySelector('.test-dom-element'),
+ propsData: {
+ pipelines: [pipeline],
+ svgs: {},
+ },
+ });
+
+ expect(component.$el.querySelectorAll('tbody tr').length).toEqual(1);
+ });
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/table_pagination_spec.js.es6 b/spec/javascripts/vue_shared/components/table_pagination_spec.js.es6
new file mode 100644
index 00000000000..e84f0dcfe67
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/table_pagination_spec.js.es6
@@ -0,0 +1,166 @@
+require('~/lib/utils/common_utils');
+require('~/vue_shared/components/table_pagination');
+
+describe('Pagination component', () => {
+ let component;
+
+ const changeChanges = {
+ one: '',
+ two: '',
+ };
+
+ const change = (one, two) => {
+ changeChanges.one = one;
+ changeChanges.two = two;
+ };
+
+ it('should render and start at page 1', () => {
+ setFixtures('<div class="test-pagination-container"></div>');
+
+ component = new window.gl.VueGlPagination({
+ el: document.querySelector('.test-pagination-container'),
+ propsData: {
+ pageInfo: {
+ totalPages: 10,
+ nextPage: 2,
+ previousPage: '',
+ },
+ change,
+ },
+ });
+
+ expect(component.$el.classList).toContain('gl-pagination');
+
+ component.changePage({ target: { innerText: '1' } });
+
+ expect(changeChanges.one).toEqual(1);
+ expect(changeChanges.two).toEqual('all');
+ });
+
+ it('should go to the previous page', () => {
+ setFixtures('<div class="test-pagination-container"></div>');
+
+ component = new window.gl.VueGlPagination({
+ el: document.querySelector('.test-pagination-container'),
+ propsData: {
+ pageInfo: {
+ totalPages: 10,
+ nextPage: 3,
+ previousPage: 1,
+ },
+ change,
+ },
+ });
+
+ component.changePage({ target: { innerText: 'Prev' } });
+
+ expect(changeChanges.one).toEqual(1);
+ expect(changeChanges.two).toEqual('all');
+ });
+
+ it('should go to the next page', () => {
+ setFixtures('<div class="test-pagination-container"></div>');
+
+ component = new window.gl.VueGlPagination({
+ el: document.querySelector('.test-pagination-container'),
+ propsData: {
+ pageInfo: {
+ totalPages: 10,
+ nextPage: 5,
+ previousPage: 3,
+ },
+ change,
+ },
+ });
+
+ component.changePage({ target: { innerText: 'Next' } });
+
+ expect(changeChanges.one).toEqual(5);
+ expect(changeChanges.two).toEqual('all');
+ });
+
+ it('should go to the last page', () => {
+ setFixtures('<div class="test-pagination-container"></div>');
+
+ component = new window.gl.VueGlPagination({
+ el: document.querySelector('.test-pagination-container'),
+ propsData: {
+ pageInfo: {
+ totalPages: 10,
+ nextPage: 5,
+ previousPage: 3,
+ },
+ change,
+ },
+ });
+
+ component.changePage({ target: { innerText: 'Last >>' } });
+
+ expect(changeChanges.one).toEqual(10);
+ expect(changeChanges.two).toEqual('all');
+ });
+
+ it('should go to the first page', () => {
+ setFixtures('<div class="test-pagination-container"></div>');
+
+ component = new window.gl.VueGlPagination({
+ el: document.querySelector('.test-pagination-container'),
+ propsData: {
+ pageInfo: {
+ totalPages: 10,
+ nextPage: 5,
+ previousPage: 3,
+ },
+ change,
+ },
+ });
+
+ component.changePage({ target: { innerText: '<< First' } });
+
+ expect(changeChanges.one).toEqual(1);
+ expect(changeChanges.two).toEqual('all');
+ });
+
+ it('should do nothing', () => {
+ setFixtures('<div class="test-pagination-container"></div>');
+
+ component = new window.gl.VueGlPagination({
+ el: document.querySelector('.test-pagination-container'),
+ propsData: {
+ pageInfo: {
+ totalPages: 10,
+ nextPage: 2,
+ previousPage: '',
+ },
+ change,
+ },
+ });
+
+ component.changePage({ target: { innerText: '...' } });
+
+ expect(changeChanges.one).toEqual(1);
+ expect(changeChanges.two).toEqual('all');
+ });
+});
+
+describe('paramHelper', () => {
+ it('can parse url parameters correctly', () => {
+ window.history.pushState({}, null, '?scope=all&p=2');
+
+ const scope = gl.utils.getParameterByName('scope');
+ const p = gl.utils.getParameterByName('p');
+
+ expect(scope).toEqual('all');
+ expect(p).toEqual('2');
+ });
+
+ it('returns null if param not in url', () => {
+ window.history.pushState({}, null, '?p=2');
+
+ const scope = gl.utils.getParameterByName('scope');
+ const p = gl.utils.getParameterByName('p');
+
+ expect(scope).toEqual(null);
+ expect(p).toEqual('2');
+ });
+});
diff --git a/spec/javascripts/zen_mode_spec.js b/spec/javascripts/zen_mode_spec.js
index d80ce5a7f7e..ce33a6814aa 100644
--- a/spec/javascripts/zen_mode_spec.js
+++ b/spec/javascripts/zen_mode_spec.js
@@ -1,18 +1,18 @@
-/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, object-shorthand, comma-dangle, no-return-assign, new-cap, padded-blocks, max-len */
+/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, object-shorthand, comma-dangle, no-return-assign, new-cap, max-len */
/* global Dropzone */
/* global Mousetrap */
/* global ZenMode */
-/*= require zen_mode */
+require('~/zen_mode');
(function() {
var enterZen, escapeKeydown, exitZen;
describe('ZenMode', function() {
var fixtureName = 'issues/open-issue.html.raw';
- fixture.preload(fixtureName);
+ preloadFixtures(fixtureName);
beforeEach(function() {
- fixture.load(fixtureName);
+ loadFixtures(fixtureName);
spyOn(Dropzone, 'forElement').and.callFake(function() {
return {
enable: function() {
@@ -32,9 +32,9 @@
return expect(Mousetrap.pause).toHaveBeenCalled();
});
return it('removes textarea styling', function() {
- $('textarea').attr('style', 'height: 400px');
+ $('.notes-form textarea').attr('style', 'height: 400px');
enterZen();
- return expect('textarea').not.toHaveAttr('style');
+ return expect($('.notes-form textarea')).not.toHaveAttr('style');
});
});
describe('in use', function() {
@@ -43,7 +43,7 @@
});
return it('exits on Escape', function() {
escapeKeydown();
- return expect($('.zen-backdrop')).not.toHaveClass('fullscreen');
+ return expect($('.notes-form .zen-backdrop')).not.toHaveClass('fullscreen');
});
});
return describe('on exit', function() {
@@ -64,17 +64,16 @@
});
enterZen = function() {
- return $('.js-zen-enter').click();
+ return $('.notes-form .js-zen-enter').click();
};
- exitZen = function() { // Ohmmmmmmm
- return $('.js-zen-leave').click();
+ exitZen = function() {
+ return $('.notes-form .js-zen-leave').click();
};
escapeKeydown = function() {
- return $('textarea').trigger($.Event('keydown', {
+ return $('.notes-form textarea').trigger($.Event('keydown', {
keyCode: 27
}));
};
-
}).call(this);
diff --git a/spec/lib/api/helpers/pagination_spec.rb b/spec/lib/api/helpers/pagination_spec.rb
new file mode 100644
index 00000000000..267318faed4
--- /dev/null
+++ b/spec/lib/api/helpers/pagination_spec.rb
@@ -0,0 +1,94 @@
+require 'spec_helper'
+
+describe API::Helpers::Pagination do
+ let(:resource) { Project.all }
+
+ subject do
+ Class.new.include(described_class).new
+ end
+
+ describe '#paginate' do
+ let(:value) { spy('return value') }
+
+ before do
+ allow(value).to receive(:to_query).and_return(value)
+
+ allow(subject).to receive(:header).and_return(value)
+ allow(subject).to receive(:params).and_return(value)
+ allow(subject).to receive(:request).and_return(value)
+ end
+
+ describe 'required instance methods' do
+ let(:return_spy) { spy }
+
+ it 'requires some instance methods' do
+ expect_message(:header)
+ expect_message(:params)
+ expect_message(:request)
+
+ subject.paginate(resource)
+ end
+ end
+
+ context 'when resource can be paginated' do
+ before do
+ create_list(:empty_project, 3)
+ end
+
+ describe 'first page' do
+ before do
+ allow(subject).to receive(:params)
+ .and_return({ page: 1, per_page: 2 })
+ end
+
+ it 'returns appropriate amount of resources' do
+ expect(subject.paginate(resource).count).to eq 2
+ end
+
+ it 'adds appropriate headers' do
+ expect_header('X-Total', '3')
+ expect_header('X-Total-Pages', '2')
+ expect_header('X-Per-Page', '2')
+ expect_header('X-Page', '1')
+ expect_header('X-Next-Page', '2')
+ expect_header('X-Prev-Page', '')
+ expect_header('Link', any_args)
+
+ subject.paginate(resource)
+ end
+ end
+
+ describe 'second page' do
+ before do
+ allow(subject).to receive(:params)
+ .and_return({ page: 2, per_page: 2 })
+ end
+
+ it 'returns appropriate amount of resources' do
+ expect(subject.paginate(resource).count).to eq 1
+ end
+
+ it 'adds appropriate headers' do
+ expect_header('X-Total', '3')
+ expect_header('X-Total-Pages', '2')
+ expect_header('X-Per-Page', '2')
+ expect_header('X-Page', '2')
+ expect_header('X-Next-Page', '')
+ expect_header('X-Prev-Page', '1')
+ expect_header('Link', any_args)
+
+ subject.paginate(resource)
+ end
+ end
+ end
+
+ def expect_header(name, value)
+ expect(subject).to receive(:header).with(name, value)
+ end
+
+ def expect_message(method)
+ expect(subject).to receive(method)
+ .at_least(:once).and_return(value)
+ end
+ end
+end
diff --git a/spec/lib/banzai/cross_project_reference_spec.rb b/spec/lib/banzai/cross_project_reference_spec.rb
index 81b9a513ce3..deaabceef1c 100644
--- a/spec/lib/banzai/cross_project_reference_spec.rb
+++ b/spec/lib/banzai/cross_project_reference_spec.rb
@@ -24,7 +24,7 @@ describe Banzai::CrossProjectReference, lib: true do
it 'returns the referenced project' do
project2 = double('referenced project')
- expect(Project).to receive(:find_with_namespace).
+ expect(Project).to receive(:find_by_full_path).
with('cross/reference').and_return(project2)
expect(project_from_ref('cross/reference')).to eq project2
diff --git a/spec/lib/banzai/filter/abstract_link_filter_spec.rb b/spec/lib/banzai/filter/abstract_link_filter_spec.rb
deleted file mode 100644
index 70a87fbc01e..00000000000
--- a/spec/lib/banzai/filter/abstract_link_filter_spec.rb
+++ /dev/null
@@ -1,52 +0,0 @@
-require 'spec_helper'
-
-describe Banzai::Filter::AbstractReferenceFilter do
- let(:project) { create(:empty_project) }
-
- describe '#references_per_project' do
- it 'returns a Hash containing references grouped per project paths' do
- doc = Nokogiri::HTML.fragment("#1 #{project.path_with_namespace}#2")
- filter = described_class.new(doc, project: project)
-
- expect(filter).to receive(:object_class).exactly(4).times.and_return(Issue)
- expect(filter).to receive(:object_sym).twice.and_return(:issue)
-
- refs = filter.references_per_project
-
- expect(refs).to be_an_instance_of(Hash)
- expect(refs[project.path_with_namespace]).to eq(Set.new(%w[1 2]))
- end
- end
-
- describe '#projects_per_reference' do
- it 'returns a Hash containing projects grouped per project paths' do
- doc = Nokogiri::HTML.fragment('')
- filter = described_class.new(doc, project: project)
-
- expect(filter).to receive(:references_per_project).
- and_return({ project.path_with_namespace => Set.new(%w[1]) })
-
- expect(filter.projects_per_reference).
- to eq({ project.path_with_namespace => project })
- end
- end
-
- describe '#find_projects_for_paths' do
- it 'returns a list of Projects for a list of paths' do
- doc = Nokogiri::HTML.fragment('')
- filter = described_class.new(doc, project: project)
-
- expect(filter.find_projects_for_paths([project.path_with_namespace])).
- to eq([project])
- end
- end
-
- describe '#current_project_path' do
- it 'returns the path of the current project' do
- doc = Nokogiri::HTML.fragment('')
- filter = described_class.new(doc, project: project)
-
- expect(filter.current_project_path).to eq(project.path_with_namespace)
- end
- end
-end
diff --git a/spec/lib/banzai/filter/abstract_reference_filter_spec.rb b/spec/lib/banzai/filter/abstract_reference_filter_spec.rb
new file mode 100644
index 00000000000..27684882435
--- /dev/null
+++ b/spec/lib/banzai/filter/abstract_reference_filter_spec.rb
@@ -0,0 +1,103 @@
+require 'spec_helper'
+
+describe Banzai::Filter::AbstractReferenceFilter do
+ let(:project) { create(:empty_project) }
+
+ describe '#references_per_project' do
+ it 'returns a Hash containing references grouped per project paths' do
+ doc = Nokogiri::HTML.fragment("#1 #{project.path_with_namespace}#2")
+ filter = described_class.new(doc, project: project)
+
+ expect(filter).to receive(:object_class).exactly(4).times.and_return(Issue)
+ expect(filter).to receive(:object_sym).twice.and_return(:issue)
+
+ refs = filter.references_per_project
+
+ expect(refs).to be_an_instance_of(Hash)
+ expect(refs[project.path_with_namespace]).to eq(Set.new(%w[1 2]))
+ end
+ end
+
+ describe '#projects_per_reference' do
+ it 'returns a Hash containing projects grouped per project paths' do
+ doc = Nokogiri::HTML.fragment('')
+ filter = described_class.new(doc, project: project)
+
+ expect(filter).to receive(:references_per_project).
+ and_return({ project.path_with_namespace => Set.new(%w[1]) })
+
+ expect(filter.projects_per_reference).
+ to eq({ project.path_with_namespace => project })
+ end
+ end
+
+ describe '#find_projects_for_paths' do
+ let(:doc) { Nokogiri::HTML.fragment('') }
+ let(:filter) { described_class.new(doc, project: project) }
+
+ context 'with RequestStore disabled' do
+ it 'returns a list of Projects for a list of paths' do
+ expect(filter.find_projects_for_paths([project.path_with_namespace])).
+ to eq([project])
+ end
+
+ it "return an empty array for paths that don't exist" do
+ expect(filter.find_projects_for_paths(['nonexistent/project'])).
+ to eq([])
+ end
+ end
+
+ context 'with RequestStore enabled' do
+ before do
+ RequestStore.begin!
+ end
+
+ after do
+ RequestStore.end!
+ RequestStore.clear!
+ end
+
+ it 'returns a list of Projects for a list of paths' do
+ expect(filter.find_projects_for_paths([project.path_with_namespace])).
+ to eq([project])
+ end
+
+ context "when no project with that path exists" do
+ it "returns no value" do
+ expect(filter.find_projects_for_paths(['nonexistent/project'])).
+ to eq([])
+ end
+
+ it "adds the ref to the project refs cache" do
+ project_refs_cache = {}
+ allow(filter).to receive(:project_refs_cache).and_return(project_refs_cache)
+
+ filter.find_projects_for_paths(['nonexistent/project'])
+
+ expect(project_refs_cache).to eq({ 'nonexistent/project' => nil })
+ end
+
+ context 'when the project refs cache includes nil values' do
+ before do
+ # adds { 'nonexistent/project' => nil } to cache
+ filter.project_from_ref_cached('nonexistent/project')
+ end
+
+ it "return an empty array for paths that don't exist" do
+ expect(filter.find_projects_for_paths(['nonexistent/project'])).
+ to eq([])
+ end
+ end
+ end
+ end
+ end
+
+ describe '#current_project_path' do
+ it 'returns the path of the current project' do
+ doc = Nokogiri::HTML.fragment('')
+ filter = described_class.new(doc, project: project)
+
+ expect(filter.current_project_path).to eq(project.path_with_namespace)
+ end
+ end
+end
diff --git a/spec/lib/banzai/filter/commit_range_reference_filter_spec.rb b/spec/lib/banzai/filter/commit_range_reference_filter_spec.rb
index 9703e2315b8..deadc36524c 100644
--- a/spec/lib/banzai/filter/commit_range_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/commit_range_reference_filter_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe Banzai::Filter::CommitRangeReferenceFilter, lib: true do
include FilterSpecHelper
- let(:project) { create(:project, :public) }
+ let(:project) { create(:project, :public, :repository) }
let(:commit1) { project.commit("HEAD~2") }
let(:commit2) { project.commit }
@@ -99,7 +99,7 @@ describe Banzai::Filter::CommitRangeReferenceFilter, lib: true do
end
context 'cross-project / cross-namespace complete reference' do
- let(:project2) { create(:project, :public) }
+ let(:project2) { create(:project, :public, :repository) }
let(:reference) { "#{project2.path_with_namespace}@#{commit1.id}...#{commit2.id}" }
it 'links to a valid reference' do
@@ -133,8 +133,8 @@ describe Banzai::Filter::CommitRangeReferenceFilter, lib: true do
context 'cross-project / same-namespace complete reference' do
let(:namespace) { create(:namespace) }
- let(:project) { create(:project, :public, namespace: namespace) }
- let(:project2) { create(:project, :public, path: "same-namespace", namespace: namespace) }
+ let(:project) { create(:project, :public, :repository, namespace: namespace) }
+ let(:project2) { create(:project, :public, :repository, path: "same-namespace", namespace: namespace) }
let(:reference) { "#{project2.path}@#{commit1.id}...#{commit2.id}" }
it 'links to a valid reference' do
@@ -168,8 +168,8 @@ describe Banzai::Filter::CommitRangeReferenceFilter, lib: true do
context 'cross-project shorthand reference' do
let(:namespace) { create(:namespace) }
- let(:project) { create(:project, :public, namespace: namespace) }
- let(:project2) { create(:project, :public, path: "same-namespace", namespace: namespace) }
+ let(:project) { create(:project, :public, :repository, namespace: namespace) }
+ let(:project2) { create(:project, :public, :repository, path: "same-namespace", namespace: namespace) }
let(:reference) { "#{project2.path}@#{commit1.id}...#{commit2.id}" }
it 'links to a valid reference' do
@@ -203,7 +203,7 @@ describe Banzai::Filter::CommitRangeReferenceFilter, lib: true do
context 'cross-project URL reference' do
let(:namespace) { create(:namespace) }
- let(:project2) { create(:project, :public, namespace: namespace) }
+ let(:project2) { create(:project, :public, :repository, namespace: namespace) }
let(:range) { CommitRange.new("#{commit1.id}...master", project) }
let(:reference) { urls.namespace_project_compare_url(project2.namespace, project2, from: commit1.id, to: 'master') }
diff --git a/spec/lib/banzai/filter/commit_reference_filter_spec.rb b/spec/lib/banzai/filter/commit_reference_filter_spec.rb
index 2e6dcc3a434..a19aac61229 100644
--- a/spec/lib/banzai/filter/commit_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/commit_reference_filter_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe Banzai::Filter::CommitReferenceFilter, lib: true do
include FilterSpecHelper
- let(:project) { create(:project, :public) }
+ let(:project) { create(:project, :public, :repository) }
let(:commit) { project.commit }
it 'requires project context' do
@@ -96,7 +96,7 @@ describe Banzai::Filter::CommitReferenceFilter, lib: true do
context 'cross-project / cross-namespace complete reference' do
let(:namespace) { create(:namespace) }
- let(:project2) { create(:project, :public, namespace: namespace) }
+ let(:project2) { create(:project, :public, :repository, namespace: namespace) }
let(:commit) { project2.commit }
let(:reference) { "#{project2.path_with_namespace}@#{commit.short_id}" }
@@ -122,7 +122,7 @@ describe Banzai::Filter::CommitReferenceFilter, lib: true do
context 'cross-project / same-namespace complete reference' do
let(:namespace) { create(:namespace) }
let(:project) { create(:empty_project, namespace: namespace) }
- let(:project2) { create(:project, :public, namespace: namespace) }
+ let(:project2) { create(:project, :public, :repository, namespace: namespace) }
let(:commit) { project2.commit }
let(:reference) { "#{project2.path_with_namespace}@#{commit.short_id}" }
@@ -148,7 +148,7 @@ describe Banzai::Filter::CommitReferenceFilter, lib: true do
context 'cross-project shorthand reference' do
let(:namespace) { create(:namespace) }
let(:project) { create(:empty_project, namespace: namespace) }
- let(:project2) { create(:project, :public, namespace: namespace) }
+ let(:project2) { create(:project, :public, :repository, namespace: namespace) }
let(:commit) { project2.commit }
let(:reference) { "#{project2.path_with_namespace}@#{commit.short_id}" }
@@ -173,7 +173,7 @@ describe Banzai::Filter::CommitReferenceFilter, lib: true do
context 'cross-project URL reference' do
let(:namespace) { create(:namespace) }
- let(:project2) { create(:project, :public, namespace: namespace) }
+ let(:project2) { create(:project, :public, :repository, namespace: namespace) }
let(:commit) { project2.commit }
let(:reference) { urls.namespace_project_commit_url(project2.namespace, project2, commit.id) }
diff --git a/spec/lib/banzai/filter/external_link_filter_spec.rb b/spec/lib/banzai/filter/external_link_filter_spec.rb
index 167397c736b..d9e4525cb28 100644
--- a/spec/lib/banzai/filter/external_link_filter_spec.rb
+++ b/spec/lib/banzai/filter/external_link_filter_spec.rb
@@ -80,4 +80,18 @@ describe Banzai::Filter::ExternalLinkFilter, lib: true do
expect(filter(act).to_html).to eq(exp)
end
end
+
+ context 'for protocol-relative links' do
+ let(:doc) { filter %q(<p><a href="//google.com/">Google</a></p>) }
+
+ it 'adds rel="nofollow" to external links' do
+ expect(doc.at_css('a')).to have_attribute('rel')
+ expect(doc.at_css('a')['rel']).to include 'nofollow'
+ end
+
+ it 'adds rel="noreferrer" to external links' do
+ expect(doc.at_css('a')).to have_attribute('rel')
+ expect(doc.at_css('a')['rel']).to include 'noreferrer'
+ end
+ end
end
diff --git a/spec/lib/banzai/filter/gollum_tags_filter_spec.rb b/spec/lib/banzai/filter/gollum_tags_filter_spec.rb
index fe2ce092e6b..082c0d4dd0d 100644
--- a/spec/lib/banzai/filter/gollum_tags_filter_spec.rb
+++ b/spec/lib/banzai/filter/gollum_tags_filter_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe Banzai::Filter::GollumTagsFilter, lib: true do
include FilterSpecHelper
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project) }
let(:user) { double }
let(:project_wiki) { ProjectWiki.new(project, user) }
diff --git a/spec/lib/banzai/filter/math_filter_spec.rb b/spec/lib/banzai/filter/math_filter_spec.rb
index 3fe2c7f5d5d..51883782e19 100644
--- a/spec/lib/banzai/filter/math_filter_spec.rb
+++ b/spec/lib/banzai/filter/math_filter_spec.rb
@@ -79,6 +79,13 @@ describe Banzai::Filter::MathFilter, lib: true do
expect(doc.to_s).to eq input
end
+ it 'ignores dollar signs if they are inside another element' do
+ input = '<p>We check strictly <em>$</em><code>2+2</code><em>$</em></p>'
+ doc = filter(input)
+
+ expect(doc.to_s).to eq input
+ end
+
# Display math
it 'adds data-math-style display attribute to display math' do
diff --git a/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb b/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb
index 275010c1a2c..3d3d36061f4 100644
--- a/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb
@@ -188,7 +188,7 @@ describe Banzai::Filter::MergeRequestReferenceFilter, lib: true do
context 'cross-project URL reference' do
let(:namespace) { create(:namespace, name: 'cross-reference') }
- let(:project2) { create(:project, :public, namespace: namespace) }
+ let(:project2) { create(:empty_project, :public, namespace: namespace) }
let(:merge) { create(:merge_request, source_project: project2, target_project: project2) }
let(:reference) { urls.namespace_project_merge_request_url(project2.namespace, project2, merge) + '/diffs#note_123' }
diff --git a/spec/lib/banzai/filter/milestone_reference_filter_spec.rb b/spec/lib/banzai/filter/milestone_reference_filter_spec.rb
index 73b5edb99b3..a317c751d32 100644
--- a/spec/lib/banzai/filter/milestone_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/milestone_reference_filter_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe Banzai::Filter::MilestoneReferenceFilter, lib: true do
include FilterSpecHelper
- let(:project) { create(:project, :public) }
+ let(:project) { create(:empty_project, :public) }
let(:milestone) { create(:milestone, project: project) }
let(:reference) { milestone.to_reference }
diff --git a/spec/lib/banzai/filter/plantuml_filter_spec.rb b/spec/lib/banzai/filter/plantuml_filter_spec.rb
new file mode 100644
index 00000000000..f85a5dcbd8b
--- /dev/null
+++ b/spec/lib/banzai/filter/plantuml_filter_spec.rb
@@ -0,0 +1,32 @@
+require 'spec_helper'
+
+describe Banzai::Filter::PlantumlFilter, lib: true do
+ include FilterSpecHelper
+
+ it 'should replace plantuml pre tag with img tag' do
+ stub_application_setting(plantuml_enabled: true, plantuml_url: "http://localhost:8080")
+ input = '<pre class="plantuml"><code>Bob -> Sara : Hello</code><pre>'
+ output = '<div class="imageblock"><div class="content"><img class="plantuml" src="http://localhost:8080/png/U9npoazIqBLJ24uiIbImKl18pSd91m0rkGMq"></div></div>'
+ doc = filter(input)
+
+ expect(doc.to_s).to eq output
+ end
+
+ it 'should not replace plantuml pre tag with img tag if disabled' do
+ stub_application_setting(plantuml_enabled: false)
+ input = '<pre class="plantuml"><code>Bob -> Sara : Hello</code><pre>'
+ output = '<pre class="plantuml"><code>Bob -&gt; Sara : Hello</code><pre></pre></pre>'
+ doc = filter(input)
+
+ expect(doc.to_s).to eq output
+ end
+
+ it 'should not replace plantuml pre tag with img tag if url is invalid' do
+ stub_application_setting(plantuml_enabled: true, plantuml_url: "invalid")
+ input = '<pre class="plantuml"><code>Bob -> Sara : Hello</code><pre>'
+ output = '<div class="listingblock"><div class="content"><pre class="plantuml plantuml-error"> PlantUML Error: cannot connect to PlantUML server at "invalid"</pre></div></div>'
+ doc = filter(input)
+
+ expect(doc.to_s).to eq output
+ end
+end
diff --git a/spec/lib/banzai/filter/relative_link_filter_spec.rb b/spec/lib/banzai/filter/relative_link_filter_spec.rb
index df2dd173b57..1957ba739e2 100644
--- a/spec/lib/banzai/filter/relative_link_filter_spec.rb
+++ b/spec/lib/banzai/filter/relative_link_filter_spec.rb
@@ -25,7 +25,7 @@ describe Banzai::Filter::RelativeLinkFilter, lib: true do
%(<a href="#{path}">#{path}</a>)
end
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:project_path) { project.path_with_namespace }
let(:ref) { 'markdown' }
let(:commit) { project.commit(ref) }
diff --git a/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb b/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb
index d265d29ee86..69e3c52b35a 100644
--- a/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb
+++ b/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb
@@ -6,21 +6,21 @@ describe Banzai::Filter::SyntaxHighlightFilter, lib: true do
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" v-pre="true"><code>def fun end</code></pre>')
+ expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight plaintext" lang="plaintext" v-pre="true"><code>def fun end</code></pre>')
end
end
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" v-pre="true"><code><span class="k">def</span> <span class="nf">fun</span> <span class="k">end</span></code></pre>')
+ expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight ruby" lang="ruby" v-pre="true"><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" v-pre="true"><code>This is a test</code></pre>')
+ expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight plaintext" lang="plaintext" v-pre="true"><code>This is a test</code></pre>')
end
end
@@ -31,7 +31,7 @@ describe Banzai::Filter::SyntaxHighlightFilter, lib: true do
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" v-pre="true"><code>This is a test</code></pre>')
+ expect(result.to_html).to eq('<pre class="code highlight" lang="" v-pre="true"><code>This is a test</code></pre>')
end
end
end
diff --git a/spec/lib/banzai/filter/upload_link_filter_spec.rb b/spec/lib/banzai/filter/upload_link_filter_spec.rb
index 8b76c1d73c9..639cac6406a 100644
--- a/spec/lib/banzai/filter/upload_link_filter_spec.rb
+++ b/spec/lib/banzai/filter/upload_link_filter_spec.rb
@@ -29,7 +29,7 @@ describe Banzai::Filter::UploadLinkFilter, lib: true do
%(<div><a href="#{path}">#{path}</a></div>)
end
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project) }
shared_examples :preserve_unchanged do
it 'does not modify any relative URL in anchor' do
diff --git a/spec/lib/banzai/filter/user_reference_filter_spec.rb b/spec/lib/banzai/filter/user_reference_filter_spec.rb
index 5bfeb82e738..3e1ac9fb2b2 100644
--- a/spec/lib/banzai/filter/user_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/user_reference_filter_spec.rb
@@ -152,6 +152,30 @@ describe Banzai::Filter::UserReferenceFilter, lib: true do
end
end
+ context 'when a project is not specified' do
+ let(:project) { nil }
+
+ it 'does not link a User' do
+ doc = reference_filter("Hey #{reference}")
+
+ expect(doc).not_to include('a')
+ end
+
+ context 'when skip_project_check set to true' do
+ it 'links to a User' do
+ doc = reference_filter("Hey #{reference}", skip_project_check: true)
+
+ expect(doc.css('a').first.attr('href')).to eq urls.user_url(user)
+ end
+
+ it 'does not link users using @all reference' do
+ doc = reference_filter("Hey #{User.reference_prefix}all", skip_project_check: true)
+
+ expect(doc).not_to include('a')
+ end
+ end
+ end
+
describe '#namespaces' do
it 'returns a Hash containing all Namespaces' do
document = Nokogiri::HTML.fragment("<p>#{user.to_reference}</p>")
diff --git a/spec/lib/banzai/filter/video_link_filter_spec.rb b/spec/lib/banzai/filter/video_link_filter_spec.rb
index 6ab1be9ccb7..00494f545a3 100644
--- a/spec/lib/banzai/filter/video_link_filter_spec.rb
+++ b/spec/lib/banzai/filter/video_link_filter_spec.rb
@@ -13,7 +13,7 @@ describe Banzai::Filter::VideoLinkFilter, lib: true do
%(<img src="#{path}" />)
end
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
context 'when the element src has a video extension' do
UploaderHelper::VIDEO_EXT.each do |ext|
diff --git a/spec/lib/banzai/reference_parser/external_issue_parser_spec.rb b/spec/lib/banzai/reference_parser/external_issue_parser_spec.rb
index 50a5d1a19ba..0af36776a54 100644
--- a/spec/lib/banzai/reference_parser/external_issue_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/external_issue_parser_spec.rb
@@ -12,7 +12,17 @@ describe Banzai::ReferenceParser::ExternalIssueParser, lib: true do
context 'when the link has a data-issue attribute' do
before { link['data-external-issue'] = 123 }
- it_behaves_like "referenced feature visibility", "issues"
+ levels = [ProjectFeature::DISABLED, ProjectFeature::PRIVATE, ProjectFeature::ENABLED]
+
+ levels.each do |level|
+ it "creates reference when the feature is #{level}" do
+ project.project_feature.update(issues_access_level: level)
+
+ visible_nodes = subject.nodes_visible_to_user(user, [link])
+
+ expect(visible_nodes).to include(link)
+ end
+ end
end
end
diff --git a/spec/lib/banzai/reference_parser/user_parser_spec.rb b/spec/lib/banzai/reference_parser/user_parser_spec.rb
index fafc2cec546..31ca9d27b0b 100644
--- a/spec/lib/banzai/reference_parser/user_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/user_parser_spec.rb
@@ -147,7 +147,7 @@ describe Banzai::ReferenceParser::UserParser, lib: true do
describe '#nodes_user_can_reference' do
context 'when the link has a data-author attribute' do
it 'returns the nodes when the user is a member of the project' do
- other_project = create(:project)
+ other_project = create(:empty_project)
other_project.team << [user, :developer]
link['data-project'] = other_project.id.to_s
@@ -164,7 +164,7 @@ describe Banzai::ReferenceParser::UserParser, lib: true do
end
it 'returns an empty Array when the user could not be found' do
- other_project = create(:project)
+ other_project = create(:empty_project)
link['data-project'] = other_project.id.to_s
link['data-author'] = ''
@@ -173,7 +173,7 @@ describe Banzai::ReferenceParser::UserParser, lib: true do
end
it 'returns an empty Array when the user is not a team member' do
- other_project = create(:project)
+ other_project = create(:empty_project)
link['data-project'] = other_project.id.to_s
link['data-author'] = user.id.to_s
diff --git a/spec/lib/ci/ansi2html_spec.rb b/spec/lib/ci/ansi2html_spec.rb
index 898f1e84ab0..0762fd7e56a 100644
--- a/spec/lib/ci/ansi2html_spec.rb
+++ b/spec/lib/ci/ansi2html_spec.rb
@@ -136,6 +136,14 @@ describe Ci::Ansi2html, lib: true do
expect(subject.convert("<")[:html]).to eq('&lt;')
end
+ it "replaces newlines with line break tags" do
+ expect(subject.convert("\n")[:html]).to eq('<br>')
+ end
+
+ it "groups carriage returns with newlines" do
+ expect(subject.convert("\r\n")[:html]).to eq('<br>')
+ end
+
describe "incremental update" do
shared_examples 'stateable converter' do
let(:pass1) { subject.convert(pre_text) }
diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
index 62d68721574..008c15c4de3 100644
--- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
+++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
@@ -4,6 +4,24 @@ module Ci
describe GitlabCiYamlProcessor, lib: true do
let(:path) { 'path' }
+ describe '#build_attributes' do
+ describe 'coverage entry' do
+ subject { described_class.new(config, path).build_attributes(:rspec) }
+
+ describe 'code coverage regexp' do
+ let(:config) do
+ YAML.dump(rspec: { script: 'rspec',
+ coverage: '/Code coverage: \d+\.\d+/' })
+ end
+
+ it 'includes coverage regexp in build attributes' do
+ expect(subject)
+ .to include(coverage_regex: 'Code coverage: \d+\.\d+')
+ end
+ end
+ end
+ end
+
describe "#builds_for_ref" do
let(:type) { 'test' }
@@ -21,6 +39,7 @@ module Ci
stage_idx: 1,
name: "rspec",
commands: "pwd\nrspec",
+ coverage_regex: nil,
tag_list: [],
options: {},
allow_failure: false,
@@ -435,6 +454,7 @@ module Ci
stage_idx: 1,
name: "rspec",
commands: "pwd\nrspec",
+ coverage_regex: nil,
tag_list: [],
options: {
image: "ruby:2.1",
@@ -463,6 +483,7 @@ module Ci
stage_idx: 1,
name: "rspec",
commands: "pwd\nrspec",
+ coverage_regex: nil,
tag_list: [],
options: {
image: "ruby:2.5",
@@ -702,6 +723,7 @@ module Ci
stage_idx: 1,
name: "rspec",
commands: "pwd\nrspec",
+ coverage_regex: nil,
tag_list: [],
options: {
image: "ruby:2.1",
@@ -769,6 +791,19 @@ module Ci
expect(builds.first[:environment]).to eq(environment[:name])
expect(builds.first[:options]).to include(environment: environment)
end
+
+ context 'the url has a port as variable' do
+ let(:environment) do
+ { name: 'production',
+ url: 'http://production.gitlab.com:$PORT' }
+ end
+
+ it 'allows a variable for the port' do
+ expect(builds.size).to eq(1)
+ expect(builds.first[:environment]).to eq(environment[:name])
+ expect(builds.first[:options]).to include(environment: environment)
+ end
+ end
end
context 'when no environment is specified' do
@@ -900,6 +935,7 @@ module Ci
stage_idx: 1,
name: "normal_job",
commands: "test",
+ coverage_regex: nil,
tag_list: [],
options: {},
when: "on_success",
@@ -945,6 +981,7 @@ module Ci
stage_idx: 0,
name: "job1",
commands: "execute-script-for-job",
+ coverage_regex: nil,
tag_list: [],
options: {},
when: "on_success",
@@ -957,6 +994,7 @@ module Ci
stage_idx: 0,
name: "job2",
commands: "execute-script-for-job",
+ coverage_regex: nil,
tag_list: [],
options: {},
when: "on_success",
diff --git a/spec/lib/constraints/project_url_constrainer_spec.rb b/spec/lib/constraints/project_url_constrainer_spec.rb
index 94266f6653b..a5251e9a8c2 100644
--- a/spec/lib/constraints/project_url_constrainer_spec.rb
+++ b/spec/lib/constraints/project_url_constrainer_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe ProjectUrlConstrainer, lib: true do
- let!(:project) { create(:project) }
+ let!(:project) { create(:empty_project) }
let!(:namespace) { project.namespace }
describe '#matches?' do
diff --git a/spec/lib/event_filter_spec.rb b/spec/lib/event_filter_spec.rb
index ec2f66b1136..d70690f589d 100644
--- a/spec/lib/event_filter_spec.rb
+++ b/spec/lib/event_filter_spec.rb
@@ -3,17 +3,17 @@ require 'spec_helper'
describe EventFilter, lib: true do
describe '#apply_filter' do
let(:source_user) { create(:user) }
- let!(:public_project) { create(:project, :public) }
-
- let!(:push_event) { create(:event, action: Event::PUSHED, project: public_project, target: public_project, author: source_user) }
- let!(:merged_event) { create(:event, action: Event::MERGED, project: public_project, target: public_project, author: source_user) }
- let!(:created_event) { create(:event, action: Event::CREATED, project: public_project, target: public_project, author: source_user) }
- let!(:updated_event) { create(:event, action: Event::UPDATED, project: public_project, target: public_project, author: source_user) }
- let!(:closed_event) { create(:event, action: Event::CLOSED, project: public_project, target: public_project, author: source_user) }
- let!(:reopened_event) { create(:event, action: Event::REOPENED, project: public_project, target: public_project, author: source_user) }
- let!(:comments_event) { create(:event, action: Event::COMMENTED, project: public_project, target: public_project, author: source_user) }
- let!(:joined_event) { create(:event, action: Event::JOINED, project: public_project, target: public_project, author: source_user) }
- let!(:left_event) { create(:event, action: Event::LEFT, project: public_project, target: public_project, author: source_user) }
+ let!(:public_project) { create(:empty_project, :public) }
+
+ let!(:push_event) { create(:event, :pushed, project: public_project, target: public_project, author: source_user) }
+ let!(:merged_event) { create(:event, :merged, project: public_project, target: public_project, author: source_user) }
+ let!(:created_event) { create(:event, :created, project: public_project, target: public_project, author: source_user) }
+ let!(:updated_event) { create(:event, :updated, project: public_project, target: public_project, author: source_user) }
+ let!(:closed_event) { create(:event, :closed, project: public_project, target: public_project, author: source_user) }
+ let!(:reopened_event) { create(:event, :reopened, project: public_project, target: public_project, author: source_user) }
+ let!(:comments_event) { create(:event, :commented, project: public_project, target: public_project, author: source_user) }
+ let!(:joined_event) { create(:event, :joined, project: public_project, target: public_project, author: source_user) }
+ let!(:left_event) { create(:event, :left, project: public_project, target: public_project, author: source_user) }
it 'applies push filter' do
events = EventFilter.new(EventFilter.push).apply_filter(Event.all)
diff --git a/spec/lib/extracts_path_spec.rb b/spec/lib/extracts_path_spec.rb
index 0e85e302f29..29c07655ae8 100644
--- a/spec/lib/extracts_path_spec.rb
+++ b/spec/lib/extracts_path_spec.rb
@@ -24,7 +24,7 @@ describe ExtractsPath, lib: true do
let(:params) { { path: sample_commit[:line_code_path], ref: ref } }
before do
- @project = create(:project)
+ @project = create(:project, :repository)
end
it "log tree path has no escape sequences" do
diff --git a/spec/lib/gitlab/asciidoc_spec.rb b/spec/lib/gitlab/asciidoc_spec.rb
index f3843ca64ff..ba199917f5c 100644
--- a/spec/lib/gitlab/asciidoc_spec.rb
+++ b/spec/lib/gitlab/asciidoc_spec.rb
@@ -8,6 +8,10 @@ module Gitlab
let(:html) { 'H<sub>2</sub>O' }
context "without project" do
+ before do
+ allow_any_instance_of(ApplicationSetting).to receive(:current).and_return(::ApplicationSetting.create_from_defaults)
+ end
+
it "converts the input using Asciidoctor and default options" do
expected_asciidoc_opts = {
safe: :secure,
diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb
index f251c0dd25a..b234de4c772 100644
--- a/spec/lib/gitlab/auth_spec.rb
+++ b/spec/lib/gitlab/auth_spec.rb
@@ -58,58 +58,102 @@ describe Gitlab::Auth, lib: true do
expect(gl_auth.find_for_git_client(user.username, 'password', project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, full_authentication_abilities))
end
- it 'recognizes user lfs tokens' do
- user = create(:user)
- token = Gitlab::LfsToken.new(user).token
+ context 'while using LFS authenticate' do
+ it 'recognizes user lfs tokens' do
+ user = create(:user)
+ token = Gitlab::LfsToken.new(user).token
- expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: user.username)
- expect(gl_auth.find_for_git_client(user.username, token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :lfs_token, full_authentication_abilities))
- end
+ expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: user.username)
+ expect(gl_auth.find_for_git_client(user.username, token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :lfs_token, full_authentication_abilities))
+ end
- it 'recognizes deploy key lfs tokens' do
- key = create(:deploy_key)
- token = Gitlab::LfsToken.new(key).token
+ it 'recognizes deploy key lfs tokens' do
+ key = create(:deploy_key)
+ token = Gitlab::LfsToken.new(key).token
- expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: "lfs+deploy-key-#{key.id}")
- expect(gl_auth.find_for_git_client("lfs+deploy-key-#{key.id}", token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(key, nil, :lfs_deploy_token, read_authentication_abilities))
- end
+ expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: "lfs+deploy-key-#{key.id}")
+ expect(gl_auth.find_for_git_client("lfs+deploy-key-#{key.id}", token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(key, nil, :lfs_deploy_token, read_authentication_abilities))
+ end
- context "while using OAuth tokens as passwords" do
- it 'succeeds for OAuth tokens with the `api` scope' do
+ it 'does not try password auth before oauth' do
user = create(:user)
- application = Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user)
- token = Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id, scopes: "api")
+ token = Gitlab::LfsToken.new(user).token
+
+ expect(gl_auth).not_to receive(:find_with_user_password)
+ gl_auth.find_for_git_client(user.username, token, project: nil, ip: 'ip')
+ end
+ end
+
+ context 'while using OAuth tokens as passwords' do
+ let(:user) { create(:user) }
+ let(:token_w_api_scope) { Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id, scopes: 'api') }
+ let(:application) { Doorkeeper::Application.create!(name: 'MyApp', redirect_uri: 'https://app.com', owner: user) }
+
+ it 'succeeds for OAuth tokens with the `api` scope' do
expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: 'oauth2')
- expect(gl_auth.find_for_git_client("oauth2", token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :oauth, read_authentication_abilities))
+ expect(gl_auth.find_for_git_client("oauth2", token_w_api_scope.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :oauth, read_authentication_abilities))
end
it 'fails for OAuth tokens with other scopes' do
- user = create(:user)
- application = Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user)
- token = Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id, scopes: "read_user")
+ token = Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id, scopes: 'read_user')
expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: 'oauth2')
expect(gl_auth.find_for_git_client("oauth2", token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(nil, nil))
end
+
+ it 'does not try password auth before oauth' do
+ expect(gl_auth).not_to receive(:find_with_user_password)
+
+ gl_auth.find_for_git_client("oauth2", token_w_api_scope.token, project: nil, ip: 'ip')
+ end
end
- context "while using personal access tokens as passwords" do
- it 'succeeds for personal access tokens with the `api` scope' do
- user = create(:user)
- personal_access_token = create(:personal_access_token, user: user, scopes: ['api'])
+ context 'while using personal access tokens as passwords' do
+ let(:user) { create(:user) }
+ let(:token_w_api_scope) { create(:personal_access_token, user: user, scopes: ['api']) }
+ it 'succeeds for personal access tokens with the `api` scope' do
expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: user.email)
- expect(gl_auth.find_for_git_client(user.email, personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :personal_token, full_authentication_abilities))
+ expect(gl_auth.find_for_git_client(user.email, token_w_api_scope.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :personal_token, full_authentication_abilities))
end
it 'fails for personal access tokens with other scopes' do
- user = create(:user)
personal_access_token = create(:personal_access_token, user: user, scopes: ['read_user'])
expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: user.email)
expect(gl_auth.find_for_git_client(user.email, personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(nil, nil))
end
+
+ it 'does not try password auth before personal access tokens' do
+ expect(gl_auth).not_to receive(:find_with_user_password)
+
+ gl_auth.find_for_git_client(user.email, token_w_api_scope.token, project: nil, ip: 'ip')
+ end
+ end
+
+ context 'while using regular user and password' do
+ it 'falls through lfs authentication' do
+ user = create(
+ :user,
+ username: 'normal_user',
+ password: 'my-secret',
+ )
+
+ expect(gl_auth.find_for_git_client(user.username, user.password, project: nil, ip: 'ip'))
+ .to eq(Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, full_authentication_abilities))
+ end
+
+ it 'falls through oauth authentication when the username is oauth2' do
+ user = create(
+ :user,
+ username: 'oauth2',
+ password: 'my-secret',
+ )
+
+ expect(gl_auth.find_for_git_client(user.username, user.password, project: nil, ip: 'ip'))
+ .to eq(Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, full_authentication_abilities))
+ end
end
it 'returns double nil for invalid credentials' do
diff --git a/spec/lib/gitlab/backup/manager_spec.rb b/spec/lib/gitlab/backup/manager_spec.rb
index 1b749d1bd39..f84782ab440 100644
--- a/spec/lib/gitlab/backup/manager_spec.rb
+++ b/spec/lib/gitlab/backup/manager_spec.rb
@@ -1,9 +1,27 @@
require 'spec_helper'
describe Backup::Manager, lib: true do
- describe '#remove_old' do
- let(:progress) { StringIO.new }
+ include StubENV
+
+ let(:progress) { StringIO.new }
+
+ before do
+ allow(progress).to receive(:puts)
+ allow(progress).to receive(:print)
+
+ allow_any_instance_of(String).to receive(:color) do |string, _color|
+ string
+ end
+
+ @old_progress = $progress # rubocop:disable Style/GlobalVars
+ $progress = progress # rubocop:disable Style/GlobalVars
+ end
+
+ after do
+ $progress = @old_progress # rubocop:disable Style/GlobalVars
+ end
+ describe '#remove_old' do
let(:files) do
[
'1451606400_2016_01_01_gitlab_backup.tar',
@@ -20,20 +38,6 @@ describe Backup::Manager, lib: true do
allow(Dir).to receive(:glob).and_return(files)
allow(FileUtils).to receive(:rm)
allow(Time).to receive(:now).and_return(Time.utc(2016))
-
- allow(progress).to receive(:puts)
- allow(progress).to receive(:print)
-
- allow_any_instance_of(String).to receive(:color) do |string, _color|
- string
- end
-
- @old_progress = $progress # rubocop:disable Style/GlobalVars
- $progress = progress # rubocop:disable Style/GlobalVars
- end
-
- after do
- $progress = @old_progress # rubocop:disable Style/GlobalVars
end
context 'when keep_time is zero' do
@@ -124,4 +128,82 @@ describe Backup::Manager, lib: true do
end
end
end
+
+ describe '#unpack' do
+ before do
+ allow(Dir).to receive(:chdir)
+ end
+
+ context 'when there are no backup files in the directory' do
+ before do
+ allow(Dir).to receive(:glob).and_return([])
+ end
+
+ it 'fails the operation and prints an error' do
+ expect { subject.unpack }.to raise_error SystemExit
+ expect(progress).to have_received(:puts)
+ .with(a_string_matching('No backups found'))
+ end
+ end
+
+ context 'when there are two backup files in the directory and BACKUP variable is not set' do
+ before do
+ allow(Dir).to receive(:glob).and_return(
+ [
+ '1451606400_2016_01_01_gitlab_backup.tar',
+ '1451520000_2015_12_31_gitlab_backup.tar',
+ ]
+ )
+ end
+
+ it 'fails the operation and prints an error' do
+ expect { subject.unpack }.to raise_error SystemExit
+ expect(progress).to have_received(:puts)
+ .with(a_string_matching('Found more than one backup'))
+ end
+ end
+
+ context 'when BACKUP variable is set to a non-existing file' do
+ before do
+ allow(Dir).to receive(:glob).and_return(
+ [
+ '1451606400_2016_01_01_gitlab_backup.tar'
+ ]
+ )
+ allow(File).to receive(:exist?).and_return(false)
+
+ stub_env('BACKUP', 'wrong')
+ end
+
+ it 'fails the operation and prints an error' do
+ expect { subject.unpack }.to raise_error SystemExit
+ expect(File).to have_received(:exist?).with('wrong_gitlab_backup.tar')
+ expect(progress).to have_received(:puts)
+ .with(a_string_matching('The backup file wrong_gitlab_backup.tar does not exist'))
+ end
+ end
+
+ context 'when BACKUP variable is set to a correct file' do
+ before do
+ allow(Dir).to receive(:glob).and_return(
+ [
+ '1451606400_2016_01_01_gitlab_backup.tar'
+ ]
+ )
+ allow(File).to receive(:exist?).and_return(true)
+ allow(Kernel).to receive(:system).and_return(true)
+ allow(YAML).to receive(:load_file).and_return(gitlab_version: Gitlab::VERSION)
+
+ stub_env('BACKUP', '1451606400_2016_01_01')
+ end
+
+ it 'unpacks the file' do
+ subject.unpack
+
+ expect(Kernel).to have_received(:system)
+ .with("tar", "-xf", "1451606400_2016_01_01_gitlab_backup.tar")
+ expect(progress).to have_received(:puts).with(a_string_matching('done'))
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/badge/build/metadata_spec.rb b/spec/lib/gitlab/badge/build/metadata_spec.rb
index d678e522721..9df96ea04eb 100644
--- a/spec/lib/gitlab/badge/build/metadata_spec.rb
+++ b/spec/lib/gitlab/badge/build/metadata_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
require 'lib/gitlab/badge/shared/metadata'
describe Gitlab::Badge::Build::Metadata do
- let(:badge) { double(project: create(:project), ref: 'feature') }
+ let(:badge) { double(project: create(:empty_project), ref: 'feature') }
let(:metadata) { described_class.new(badge) }
it_behaves_like 'badge metadata'
diff --git a/spec/lib/gitlab/badge/build/status_spec.rb b/spec/lib/gitlab/badge/build/status_spec.rb
index 70f03021d36..3c5414701a7 100644
--- a/spec/lib/gitlab/badge/build/status_spec.rb
+++ b/spec/lib/gitlab/badge/build/status_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Gitlab::Badge::Build::Status do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:sha) { project.commit.sha }
let(:branch) { 'master' }
let(:badge) { described_class.new(project, branch) }
diff --git a/spec/lib/gitlab/badge/coverage/metadata_spec.rb b/spec/lib/gitlab/badge/coverage/metadata_spec.rb
index 74eaf7eaf8b..5e93935ea37 100644
--- a/spec/lib/gitlab/badge/coverage/metadata_spec.rb
+++ b/spec/lib/gitlab/badge/coverage/metadata_spec.rb
@@ -3,7 +3,7 @@ require 'lib/gitlab/badge/shared/metadata'
describe Gitlab::Badge::Coverage::Metadata do
let(:badge) do
- double(project: create(:project), ref: 'feature', job: 'test')
+ double(project: create(:empty_project), ref: 'feature', job: 'test')
end
let(:metadata) { described_class.new(badge) }
diff --git a/spec/lib/gitlab/bitbucket_import/importer_spec.rb b/spec/lib/gitlab/bitbucket_import/importer_spec.rb
index 72b1ba36b58..0a2fe5af2c3 100644
--- a/spec/lib/gitlab/bitbucket_import/importer_spec.rb
+++ b/spec/lib/gitlab/bitbucket_import/importer_spec.rb
@@ -52,7 +52,7 @@ describe Gitlab::BitbucketImport::Importer, lib: true do
let(:project) do
create(
- :project,
+ :empty_project,
import_source: project_identifier,
import_data: ProjectImportData.new(credentials: data)
)
diff --git a/spec/lib/gitlab/blame_spec.rb b/spec/lib/gitlab/blame_spec.rb
index 89245761b6f..26b1baf75be 100644
--- a/spec/lib/gitlab/blame_spec.rb
+++ b/spec/lib/gitlab/blame_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Gitlab::Blame, lib: true do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:path) { 'files/ruby/popen.rb' }
let(:commit) { project.commit('master') }
let(:blob) { project.repository.blob_at(commit.id, path) }
diff --git a/spec/lib/gitlab/chat_commands/command_spec.rb b/spec/lib/gitlab/chat_commands/command_spec.rb
index a0ec8884635..b6e924d67be 100644
--- a/spec/lib/gitlab/chat_commands/command_spec.rb
+++ b/spec/lib/gitlab/chat_commands/command_spec.rb
@@ -11,7 +11,7 @@ describe Gitlab::ChatCommands::Command, service: true do
context 'when no command is available' do
let(:params) { { text: 'issue show 1' } }
- let(:project) { create(:project, has_external_issue_tracker: true) }
+ let(:project) { create(:empty_project, has_external_issue_tracker: true) }
it 'displays 404 messages' do
expect(subject[:response_type]).to be(:ephemeral)
@@ -24,7 +24,7 @@ describe Gitlab::ChatCommands::Command, service: true do
it 'displays the help message' do
expect(subject[:response_type]).to be(:ephemeral)
- expect(subject[:text]).to start_with('Available commands')
+ expect(subject[:text]).to start_with('Unknown command')
expect(subject[:text]).to match('/gitlab issue show')
end
end
@@ -34,23 +34,7 @@ describe Gitlab::ChatCommands::Command, service: true do
it 'rejects the actions' do
expect(subject[:response_type]).to be(:ephemeral)
- expect(subject[:text]).to start_with('Whoops! That action is not allowed')
- end
- end
-
- context 'issue is successfully created' do
- let(:params) { { text: "issue create my new issue" } }
-
- before do
- project.team << [user, :master]
- end
-
- it 'presents the issue' do
- expect(subject[:text]).to match("my new issue")
- end
-
- it 'shows a link to the new issue' do
- expect(subject[:text]).to match(/\/issues\/\d+/)
+ expect(subject[:text]).to start_with('Whoops! This action is not allowed')
end
end
@@ -66,7 +50,7 @@ describe Gitlab::ChatCommands::Command, service: true do
context 'and user can not create deployment' do
it 'returns action' do
expect(subject[:response_type]).to be(:ephemeral)
- expect(subject[:text]).to start_with('Whoops! That action is not allowed')
+ expect(subject[:text]).to start_with('Whoops! This action is not allowed')
end
end
@@ -76,7 +60,7 @@ describe Gitlab::ChatCommands::Command, service: true do
end
it 'returns action' do
- expect(subject[:text]).to include('Deployment from staging to production started.')
+ expect(subject[:text]).to include('Deployment started from staging to production')
expect(subject[:response_type]).to be(:in_channel)
end
@@ -106,7 +90,7 @@ describe Gitlab::ChatCommands::Command, service: true do
context 'IssueCreate is triggered' do
let(:params) { { text: 'issue create my title' } }
- it { is_expected.to eq(Gitlab::ChatCommands::IssueCreate) }
+ it { is_expected.to eq(Gitlab::ChatCommands::IssueNew) }
end
context 'IssueSearch is triggered' do
diff --git a/spec/lib/gitlab/chat_commands/deploy_spec.rb b/spec/lib/gitlab/chat_commands/deploy_spec.rb
index bd8099c92da..b3358a32161 100644
--- a/spec/lib/gitlab/chat_commands/deploy_spec.rb
+++ b/spec/lib/gitlab/chat_commands/deploy_spec.rb
@@ -15,8 +15,9 @@ describe Gitlab::ChatCommands::Deploy, service: true do
end
context 'if no environment is defined' do
- it 'returns nil' do
- expect(subject).to be_nil
+ it 'does not execute an action' do
+ expect(subject[:response_type]).to be(:ephemeral)
+ expect(subject[:text]).to eq("No action found to be executed")
end
end
@@ -26,8 +27,9 @@ describe Gitlab::ChatCommands::Deploy, service: true do
let!(:deployment) { create(:deployment, environment: staging, deployable: build) }
context 'without actions' do
- it 'returns nil' do
- expect(subject).to be_nil
+ it 'does not execute an action' do
+ expect(subject[:response_type]).to be(:ephemeral)
+ expect(subject[:text]).to eq("No action found to be executed")
end
end
@@ -37,8 +39,8 @@ describe Gitlab::ChatCommands::Deploy, service: true do
end
it 'returns success result' do
- expect(subject.type).to eq(:success)
- expect(subject.message).to include('Deployment from staging to production started')
+ expect(subject[:response_type]).to be(:in_channel)
+ expect(subject[:text]).to start_with('Deployment started from staging to production')
end
context 'when duplicate action exists' do
@@ -47,8 +49,8 @@ describe Gitlab::ChatCommands::Deploy, service: true do
end
it 'returns error' do
- expect(subject.type).to eq(:error)
- expect(subject.message).to include('Too many actions defined')
+ expect(subject[:response_type]).to be(:ephemeral)
+ expect(subject[:text]).to eq('Too many actions defined')
end
end
@@ -59,9 +61,9 @@ describe Gitlab::ChatCommands::Deploy, service: true do
name: 'teardown', environment: 'production')
end
- it 'returns success result' do
- expect(subject.type).to eq(:success)
- expect(subject.message).to include('Deployment from staging to production started')
+ it 'returns the success message' do
+ expect(subject[:response_type]).to be(:in_channel)
+ expect(subject[:text]).to start_with('Deployment started from staging to production')
end
end
end
diff --git a/spec/lib/gitlab/chat_commands/issue_create_spec.rb b/spec/lib/gitlab/chat_commands/issue_create_spec.rb
deleted file mode 100644
index 6c71e79ff6d..00000000000
--- a/spec/lib/gitlab/chat_commands/issue_create_spec.rb
+++ /dev/null
@@ -1,68 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::ChatCommands::IssueCreate, service: true do
- describe '#execute' do
- let(:project) { create(:empty_project) }
- let(:user) { create(:user) }
- let(:regex_match) { described_class.match("issue create bird is the word") }
-
- before do
- project.team << [user, :master]
- end
-
- subject do
- described_class.new(project, user).execute(regex_match)
- end
-
- context 'without description' do
- it 'creates the issue' do
- expect { subject }.to change { project.issues.count }.by(1)
-
- expect(subject.title).to eq('bird is the word')
- end
- end
-
- context 'with description' do
- let(:description) { "Surfin bird" }
- let(:regex_match) { described_class.match("issue create bird is the word\n#{description}") }
-
- it 'creates the issue with description' do
- subject
-
- expect(Issue.last.description).to eq(description)
- end
- end
-
- context "with more newlines between the title and the description" do
- let(:description) { "Surfin bird" }
- let(:regex_match) { described_class.match("issue create bird is the word\n\n#{description}\n") }
-
- it 'creates the issue' do
- expect { subject }.to change { project.issues.count }.by(1)
- end
- end
- end
-
- describe '.match' do
- it 'matches the title without description' do
- match = described_class.match("issue create my title")
-
- expect(match[:title]).to eq('my title')
- expect(match[:description]).to eq("")
- end
-
- it 'matches the title with description' do
- match = described_class.match("issue create my title\n\ndescription")
-
- expect(match[:title]).to eq('my title')
- expect(match[:description]).to eq('description')
- end
-
- it 'matches the alias new' do
- match = described_class.match("issue new my title")
-
- expect(match).not_to be_nil
- expect(match[:title]).to eq('my title')
- end
- end
-end
diff --git a/spec/lib/gitlab/chat_commands/issue_new_spec.rb b/spec/lib/gitlab/chat_commands/issue_new_spec.rb
new file mode 100644
index 00000000000..84c22328064
--- /dev/null
+++ b/spec/lib/gitlab/chat_commands/issue_new_spec.rb
@@ -0,0 +1,78 @@
+require 'spec_helper'
+
+describe Gitlab::ChatCommands::IssueNew, service: true do
+ describe '#execute' do
+ let(:project) { create(:empty_project) }
+ let(:user) { create(:user) }
+ let(:regex_match) { described_class.match("issue create bird is the word") }
+
+ before do
+ project.team << [user, :master]
+ end
+
+ subject do
+ described_class.new(project, user).execute(regex_match)
+ end
+
+ context 'without description' do
+ it 'creates the issue' do
+ expect { subject }.to change { project.issues.count }.by(1)
+
+ expect(subject[:response_type]).to be(:in_channel)
+ end
+ end
+
+ context 'with description' do
+ let(:description) { "Surfin bird" }
+ let(:regex_match) { described_class.match("issue create bird is the word\n#{description}") }
+
+ it 'creates the issue with description' do
+ subject
+
+ expect(Issue.last.description).to eq(description)
+ end
+ end
+
+ context "with more newlines between the title and the description" do
+ let(:description) { "Surfin bird" }
+ let(:regex_match) { described_class.match("issue create bird is the word\n\n#{description}\n") }
+
+ it 'creates the issue' do
+ expect { subject }.to change { project.issues.count }.by(1)
+ end
+ end
+
+ context 'issue cannot be created' do
+ let!(:issue) { create(:issue, project: project, title: 'bird is the word') }
+ let(:regex_match) { described_class.match("issue create #{'a' * 512}}") }
+
+ it 'displays the errors' do
+ expect(subject[:response_type]).to be(:ephemeral)
+ expect(subject[:text]).to match("- Title is too long")
+ end
+ end
+ end
+
+ describe '.match' do
+ it 'matches the title without description' do
+ match = described_class.match("issue create my title")
+
+ expect(match[:title]).to eq('my title')
+ expect(match[:description]).to eq("")
+ end
+
+ it 'matches the title with description' do
+ match = described_class.match("issue create my title\n\ndescription")
+
+ expect(match[:title]).to eq('my title')
+ expect(match[:description]).to eq('description')
+ end
+
+ it 'matches the alias new' do
+ match = described_class.match("issue new my title")
+
+ expect(match).not_to be_nil
+ expect(match[:title]).to eq('my title')
+ end
+ end
+end
diff --git a/spec/lib/gitlab/chat_commands/issue_search_spec.rb b/spec/lib/gitlab/chat_commands/issue_search_spec.rb
index 24c06a967fa..551ccb79a58 100644
--- a/spec/lib/gitlab/chat_commands/issue_search_spec.rb
+++ b/spec/lib/gitlab/chat_commands/issue_search_spec.rb
@@ -2,9 +2,9 @@ require 'spec_helper'
describe Gitlab::ChatCommands::IssueSearch, service: true do
describe '#execute' do
- let!(:issue) { create(:issue, title: 'find me') }
+ let!(:issue) { create(:issue, project: project, title: 'find me') }
let!(:confidential) { create(:issue, :confidential, project: project, title: 'mepmep find') }
- let(:project) { issue.project }
+ let(:project) { create(:empty_project) }
let(:user) { issue.author }
let(:regex_match) { described_class.match("issue search find") }
@@ -14,7 +14,8 @@ describe Gitlab::ChatCommands::IssueSearch, service: true do
context 'when the user has no access' do
it 'only returns the open issues' do
- expect(subject).not_to include(confidential)
+ expect(subject[:response_type]).to be(:ephemeral)
+ expect(subject[:text]).to match("not found")
end
end
@@ -24,13 +25,14 @@ describe Gitlab::ChatCommands::IssueSearch, service: true do
end
it 'returns all results' do
- expect(subject).to include(confidential, issue)
+ expect(subject).to have_key(:attachments)
+ expect(subject[:text]).to eq("Here are the 2 issues I found:")
end
end
context 'without hits on the query' do
it 'returns an empty collection' do
- expect(subject).to be_empty
+ expect(subject[:text]).to match("not found")
end
end
end
diff --git a/spec/lib/gitlab/chat_commands/issue_show_spec.rb b/spec/lib/gitlab/chat_commands/issue_show_spec.rb
index 2eab73e49e5..1f20d0a44ce 100644
--- a/spec/lib/gitlab/chat_commands/issue_show_spec.rb
+++ b/spec/lib/gitlab/chat_commands/issue_show_spec.rb
@@ -2,8 +2,8 @@ require 'spec_helper'
describe Gitlab::ChatCommands::IssueShow, service: true do
describe '#execute' do
- let(:issue) { create(:issue) }
- let(:project) { issue.project }
+ let(:issue) { create(:issue, project: project) }
+ let(:project) { create(:empty_project) }
let(:user) { issue.author }
let(:regex_match) { described_class.match("issue show #{issue.iid}") }
@@ -16,15 +16,19 @@ describe Gitlab::ChatCommands::IssueShow, service: true do
end
context 'the issue exists' do
+ let(:title) { subject[:attachments].first[:title] }
+
it 'returns the issue' do
- expect(subject.iid).to be issue.iid
+ expect(subject[:response_type]).to be(:in_channel)
+ expect(title).to start_with(issue.title)
end
context 'when its reference is given' do
let(:regex_match) { described_class.match("issue show #{issue.to_reference}") }
it 'shows the issue' do
- expect(subject.iid).to be issue.iid
+ expect(subject[:response_type]).to be(:in_channel)
+ expect(title).to start_with(issue.title)
end
end
end
@@ -32,17 +36,24 @@ describe Gitlab::ChatCommands::IssueShow, service: true do
context 'the issue does not exist' do
let(:regex_match) { described_class.match("issue show 2343242") }
- it "returns nil" do
- expect(subject).to be_nil
+ it "returns not found" do
+ expect(subject[:response_type]).to be(:ephemeral)
+ expect(subject[:text]).to match("not found")
end
end
end
- describe 'self.match' do
+ describe '.match' do
it 'matches the iid' do
match = described_class.match("issue show 123")
expect(match[:iid]).to eq("123")
end
+
+ it 'accepts a reference' do
+ match = described_class.match("issue show #{Issue.reference_prefix}123")
+
+ expect(match[:iid]).to eq("123")
+ end
end
end
diff --git a/spec/lib/gitlab/chat_commands/presenters/access_spec.rb b/spec/lib/gitlab/chat_commands/presenters/access_spec.rb
new file mode 100644
index 00000000000..ae41d75ab0c
--- /dev/null
+++ b/spec/lib/gitlab/chat_commands/presenters/access_spec.rb
@@ -0,0 +1,49 @@
+require 'spec_helper'
+
+describe Gitlab::ChatCommands::Presenters::Access do
+ describe '#access_denied' do
+ subject { described_class.new.access_denied }
+
+ it { is_expected.to be_a(Hash) }
+
+ it 'displays an error message' do
+ expect(subject[:text]).to match("is not allowed")
+ expect(subject[:response_type]).to be(:ephemeral)
+ end
+ end
+
+ describe '#not_found' do
+ subject { described_class.new.not_found }
+
+ it { is_expected.to be_a(Hash) }
+
+ it 'tells the user the resource was not found' do
+ expect(subject[:text]).to match("not found!")
+ expect(subject[:response_type]).to be(:ephemeral)
+ end
+ end
+
+ describe '#authorize' do
+ context 'with an authorization URL' do
+ subject { described_class.new('http://authorize.me').authorize }
+
+ it { is_expected.to be_a(Hash) }
+
+ it 'tells the user to authorize' do
+ expect(subject[:text]).to match("connect your GitLab account")
+ expect(subject[:response_type]).to be(:ephemeral)
+ end
+ end
+
+ context 'without authorization url' do
+ subject { described_class.new.authorize }
+
+ it { is_expected.to be_a(Hash) }
+
+ it 'tells the user to authorize' do
+ expect(subject[:text]).to match("Couldn't identify you")
+ expect(subject[:response_type]).to be(:ephemeral)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/chat_commands/presenters/deploy_spec.rb b/spec/lib/gitlab/chat_commands/presenters/deploy_spec.rb
new file mode 100644
index 00000000000..dc2dd300072
--- /dev/null
+++ b/spec/lib/gitlab/chat_commands/presenters/deploy_spec.rb
@@ -0,0 +1,47 @@
+require 'spec_helper'
+
+describe Gitlab::ChatCommands::Presenters::Deploy do
+ let(:build) { create(:ci_build) }
+
+ describe '#present' do
+ subject { described_class.new(build).present('staging', 'prod') }
+
+ it { is_expected.to have_key(:text) }
+ it { is_expected.to have_key(:response_type) }
+ it { is_expected.to have_key(:status) }
+ it { is_expected.not_to have_key(:attachments) }
+
+ it 'messages the channel of the deploy' do
+ expect(subject[:response_type]).to be(:in_channel)
+ expect(subject[:text]).to start_with("Deployment started from staging to prod")
+ end
+ end
+
+ describe '#no_actions' do
+ subject { described_class.new(nil).no_actions }
+
+ it { is_expected.to have_key(:text) }
+ it { is_expected.to have_key(:response_type) }
+ it { is_expected.to have_key(:status) }
+ it { is_expected.not_to have_key(:attachments) }
+
+ it 'tells the user there is no action' do
+ expect(subject[:response_type]).to be(:ephemeral)
+ expect(subject[:text]).to eq("No action found to be executed")
+ end
+ end
+
+ describe '#too_many_actions' do
+ subject { described_class.new([]).too_many_actions }
+
+ it { is_expected.to have_key(:text) }
+ it { is_expected.to have_key(:response_type) }
+ it { is_expected.to have_key(:status) }
+ it { is_expected.not_to have_key(:attachments) }
+
+ it 'tells the user there is no action' do
+ expect(subject[:response_type]).to be(:ephemeral)
+ expect(subject[:text]).to eq("Too many actions defined")
+ end
+ end
+end
diff --git a/spec/lib/gitlab/chat_commands/presenters/issue_new_spec.rb b/spec/lib/gitlab/chat_commands/presenters/issue_new_spec.rb
new file mode 100644
index 00000000000..17fcdbc2452
--- /dev/null
+++ b/spec/lib/gitlab/chat_commands/presenters/issue_new_spec.rb
@@ -0,0 +1,17 @@
+require 'spec_helper'
+
+describe Gitlab::ChatCommands::Presenters::IssueNew do
+ let(:project) { create(:empty_project) }
+ let(:issue) { create(:issue, project: project) }
+ let(:attachment) { subject[:attachments].first }
+
+ subject { described_class.new(issue).present }
+
+ it { is_expected.to be_a(Hash) }
+
+ it 'shows the issue' do
+ expect(subject[:response_type]).to be(:in_channel)
+ expect(subject).to have_key(:attachments)
+ expect(attachment[:title]).to start_with(issue.title)
+ end
+end
diff --git a/spec/lib/gitlab/chat_commands/presenters/issue_search_spec.rb b/spec/lib/gitlab/chat_commands/presenters/issue_search_spec.rb
new file mode 100644
index 00000000000..ec6d3e34a96
--- /dev/null
+++ b/spec/lib/gitlab/chat_commands/presenters/issue_search_spec.rb
@@ -0,0 +1,23 @@
+require 'spec_helper'
+
+describe Gitlab::ChatCommands::Presenters::IssueSearch do
+ let(:project) { create(:empty_project) }
+ let(:message) { subject[:text] }
+
+ before { create_list(:issue, 2, project: project) }
+
+ subject { described_class.new(project.issues).present }
+
+ it 'formats the message correct' do
+ is_expected.to have_key(:text)
+ is_expected.to have_key(:status)
+ is_expected.to have_key(:response_type)
+ is_expected.to have_key(:attachments)
+ end
+
+ it 'shows a list of results' do
+ expect(subject[:response_type]).to be(:ephemeral)
+
+ expect(message).to start_with("Here are the 2 issues I found")
+ end
+end
diff --git a/spec/lib/gitlab/chat_commands/presenters/issue_show_spec.rb b/spec/lib/gitlab/chat_commands/presenters/issue_show_spec.rb
new file mode 100644
index 00000000000..5b678d31fce
--- /dev/null
+++ b/spec/lib/gitlab/chat_commands/presenters/issue_show_spec.rb
@@ -0,0 +1,37 @@
+require 'spec_helper'
+
+describe Gitlab::ChatCommands::Presenters::IssueShow do
+ let(:project) { create(:empty_project) }
+ let(:issue) { create(:issue, project: project) }
+ let(:attachment) { subject[:attachments].first }
+
+ subject { described_class.new(issue).present }
+
+ it { is_expected.to be_a(Hash) }
+
+ it 'shows the issue' do
+ expect(subject[:response_type]).to be(:in_channel)
+ expect(subject).to have_key(:attachments)
+ expect(attachment[:title]).to start_with(issue.title)
+ end
+
+ context 'with upvotes' do
+ before do
+ create(:award_emoji, :upvote, awardable: issue)
+ end
+
+ it 'shows the upvote count' do
+ expect(subject[:response_type]).to be(:in_channel)
+ expect(attachment[:text]).to start_with("**Open** · :+1: 1")
+ end
+ end
+
+ context 'confidential issue' do
+ let(:issue) { create(:issue, project: project) }
+
+ it 'shows an ephemeral response' do
+ expect(subject[:response_type]).to be(:in_channel)
+ expect(attachment[:text]).to start_with("**Open**")
+ end
+ end
+end
diff --git a/spec/lib/gitlab/checks/change_access_spec.rb b/spec/lib/gitlab/checks/change_access_spec.rb
index 39069b49978..cadfbadca10 100644
--- a/spec/lib/gitlab/checks/change_access_spec.rb
+++ b/spec/lib/gitlab/checks/change_access_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe Gitlab::Checks::ChangeAccess, lib: true do
describe '#exec' do
let(:user) { create(:user) }
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:user_access) { Gitlab::UserAccess.new(user, project: project) }
let(:changes) do
{
@@ -56,7 +56,6 @@ describe Gitlab::Checks::ChangeAccess, lib: true do
it 'returns an error if the user is not allowed to do forced pushes to protected branches' do
expect(Gitlab::Checks::ForcePush).to receive(:force_push?).and_return(true)
- expect(user_access).to receive(:can_do_action?).with(:force_push_code_to_protected_branches).and_return(false)
expect(subject.status).to be(false)
expect(subject.message).to eq('You are not allowed to force push code to a protected branch on this project.')
@@ -88,8 +87,6 @@ describe Gitlab::Checks::ChangeAccess, lib: true do
end
it 'returns an error if the user is not allowed to delete protected branches' do
- expect(user_access).to receive(:can_do_action?).with(:remove_protected_branches).and_return(false)
-
expect(subject.status).to be(false)
expect(subject.message).to eq('You are not allowed to delete protected branches from this project.')
end
diff --git a/spec/lib/gitlab/checks/force_push_spec.rb b/spec/lib/gitlab/checks/force_push_spec.rb
index f6288011494..7a84bbebd02 100644
--- a/spec/lib/gitlab/checks/force_push_spec.rb
+++ b/spec/lib/gitlab/checks/force_push_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Gitlab::Checks::ChangeAccess, lib: true do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
context "exit code checking" do
it "does not raise a runtime error if the `popen` call to git returns a zero exit code" do
diff --git a/spec/lib/gitlab/ci/config/entry/coverage_spec.rb b/spec/lib/gitlab/ci/config/entry/coverage_spec.rb
new file mode 100644
index 00000000000..4c6bd859552
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/entry/coverage_spec.rb
@@ -0,0 +1,54 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Config::Entry::Coverage do
+ let(:entry) { described_class.new(config) }
+
+ describe 'validations' do
+ context "when entry config value doesn't have the surrounding '/'" do
+ let(:config) { 'Code coverage: \d+\.\d+' }
+
+ describe '#errors' do
+ subject { entry.errors }
+ it { is_expected.to include(/coverage config must be a regular expression/) }
+ end
+
+ describe '#valid?' do
+ subject { entry }
+ it { is_expected.not_to be_valid }
+ end
+ end
+
+ context "when entry config value has the surrounding '/'" do
+ let(:config) { '/Code coverage: \d+\.\d+/' }
+
+ describe '#value' do
+ subject { entry.value }
+ it { is_expected.to eq(config[1...-1]) }
+ end
+
+ describe '#errors' do
+ subject { entry.errors }
+ it { is_expected.to be_empty }
+ end
+
+ describe '#valid?' do
+ subject { entry }
+ it { is_expected.to be_valid }
+ end
+ end
+
+ context 'when entry value is not valid' do
+ let(:config) { '(malformed regexp' }
+
+ describe '#errors' do
+ subject { entry.errors }
+ it { is_expected.to include(/coverage config must be a regular expression/) }
+ end
+
+ describe '#valid?' do
+ subject { entry }
+ it { is_expected.not_to be_valid }
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/entry/environment_spec.rb b/spec/lib/gitlab/ci/config/entry/environment_spec.rb
index d97806295fb..2adbed2154f 100644
--- a/spec/lib/gitlab/ci/config/entry/environment_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/environment_spec.rb
@@ -196,22 +196,5 @@ describe Gitlab::Ci::Config::Entry::Environment do
end
end
end
-
- context 'when invalid URL is used' do
- let(:config) { { name: 'test', url: 'invalid-example.gitlab.com' } }
-
- describe '#valid?' do
- it 'is not valid' do
- expect(entry).not_to be_valid
- end
- end
-
- describe '#errors?' do
- it 'contains error about invalid URL' do
- expect(entry.errors)
- .to include "environment url must be a valid url"
- end
- end
- end
end
end
diff --git a/spec/lib/gitlab/ci/config/entry/global_spec.rb b/spec/lib/gitlab/ci/config/entry/global_spec.rb
index e64c8d46bd8..432a99dce33 100644
--- a/spec/lib/gitlab/ci/config/entry/global_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/global_spec.rb
@@ -4,12 +4,17 @@ describe Gitlab::Ci::Config::Entry::Global do
let(:global) { described_class.new(hash) }
describe '.nodes' do
- it 'can contain global config keys' do
- expect(described_class.nodes).to include :before_script
+ it 'returns a hash' do
+ expect(described_class.nodes).to be_a(Hash)
end
- it 'returns a hash' do
- expect(described_class.nodes).to be_a Hash
+ context 'when filtering all the entry/node names' do
+ it 'contains the expected node names' do
+ expect(described_class.nodes.keys)
+ .to match_array(%i[before_script image services
+ after_script variables stages
+ types cache])
+ end
end
end
diff --git a/spec/lib/gitlab/ci/config/entry/job_spec.rb b/spec/lib/gitlab/ci/config/entry/job_spec.rb
index fc9b8b86dc4..d20f4ec207d 100644
--- a/spec/lib/gitlab/ci/config/entry/job_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/job_spec.rb
@@ -3,6 +3,20 @@ require 'spec_helper'
describe Gitlab::Ci::Config::Entry::Job do
let(:entry) { described_class.new(config, name: :rspec) }
+ describe '.nodes' do
+ context 'when filtering all the entry/node names' do
+ subject { described_class.nodes.keys }
+
+ let(:result) do
+ %i[before_script script stage type after_script cache
+ image services only except variables artifacts
+ environment coverage]
+ end
+
+ it { is_expected.to match_array result }
+ end
+ end
+
describe 'validations' do
before { entry.compose! }
diff --git a/spec/lib/gitlab/ci/status/build/cancelable_spec.rb b/spec/lib/gitlab/ci/status/build/cancelable_spec.rb
index b3c07347de1..8ad9b7cdf07 100644
--- a/spec/lib/gitlab/ci/status/build/cancelable_spec.rb
+++ b/spec/lib/gitlab/ci/status/build/cancelable_spec.rb
@@ -62,7 +62,7 @@ describe Gitlab::Ci::Status::Build::Cancelable do
end
describe '#action_icon' do
- it { expect(subject.action_icon).to eq 'ban' }
+ it { expect(subject.action_icon).to eq 'icon_action_cancel' }
end
describe '#action_title' do
diff --git a/spec/lib/gitlab/ci/status/build/factory_spec.rb b/spec/lib/gitlab/ci/status/build/factory_spec.rb
index dccb29b5ef6..0c40fca0c1a 100644
--- a/spec/lib/gitlab/ci/status/build/factory_spec.rb
+++ b/spec/lib/gitlab/ci/status/build/factory_spec.rb
@@ -3,15 +3,23 @@ require 'spec_helper'
describe Gitlab::Ci::Status::Build::Factory do
let(:user) { create(:user) }
let(:project) { build.project }
-
- subject { described_class.new(build, user) }
- let(:status) { subject.fabricate! }
+ let(:status) { factory.fabricate! }
+ let(:factory) { described_class.new(build, user) }
before { project.team << [user, :developer] }
context 'when build is successful' do
let(:build) { create(:ci_build, :success) }
+ it 'matches correct core status' do
+ expect(factory.core_status).to be_a Gitlab::Ci::Status::Success
+ end
+
+ it 'matches correct extended statuses' do
+ expect(factory.extended_statuses)
+ .to eq [Gitlab::Ci::Status::Build::Retryable]
+ end
+
it 'fabricates a retryable build status' do
expect(status).to be_a Gitlab::Ci::Status::Build::Retryable
end
@@ -26,24 +34,72 @@ describe Gitlab::Ci::Status::Build::Factory do
end
context 'when build is failed' do
- let(:build) { create(:ci_build, :failed) }
+ context 'when build is not allowed to fail' do
+ let(:build) { create(:ci_build, :failed) }
- it 'fabricates a retryable build status' do
- expect(status).to be_a Gitlab::Ci::Status::Build::Retryable
+ it 'matches correct core status' do
+ expect(factory.core_status).to be_a Gitlab::Ci::Status::Failed
+ end
+
+ it 'matches correct extended statuses' do
+ expect(factory.extended_statuses)
+ .to eq [Gitlab::Ci::Status::Build::Retryable]
+ end
+
+ it 'fabricates a retryable build status' do
+ expect(status).to be_a Gitlab::Ci::Status::Build::Retryable
+ end
+
+ it 'fabricates status with correct details' do
+ expect(status.text).to eq 'failed'
+ expect(status.icon).to eq 'icon_status_failed'
+ expect(status.label).to eq 'failed'
+ expect(status).to have_details
+ expect(status).to have_action
+ end
end
- it 'fabricates status with correct details' do
- expect(status.text).to eq 'failed'
- expect(status.icon).to eq 'icon_status_failed'
- expect(status.label).to eq 'failed'
- expect(status).to have_details
- expect(status).to have_action
+ context 'when build is allowed to fail' do
+ let(:build) { create(:ci_build, :failed, :allowed_to_fail) }
+
+ it 'matches correct core status' do
+ expect(factory.core_status).to be_a Gitlab::Ci::Status::Failed
+ end
+
+ it 'matches correct extended statuses' do
+ expect(factory.extended_statuses)
+ .to eq [Gitlab::Ci::Status::Build::Retryable,
+ Gitlab::Ci::Status::Build::FailedAllowed]
+ end
+
+ it 'fabricates a failed but allowed build status' do
+ expect(status).to be_a Gitlab::Ci::Status::Build::FailedAllowed
+ end
+
+ it 'fabricates status with correct details' do
+ expect(status.text).to eq 'failed'
+ expect(status.icon).to eq 'icon_status_warning'
+ expect(status.label).to eq 'failed (allowed to fail)'
+ expect(status).to have_details
+ expect(status).to have_action
+ expect(status.action_title).to include 'Retry'
+ expect(status.action_path).to include 'retry'
+ end
end
end
context 'when build is a canceled' do
let(:build) { create(:ci_build, :canceled) }
+ it 'matches correct core status' do
+ expect(factory.core_status).to be_a Gitlab::Ci::Status::Canceled
+ end
+
+ it 'matches correct extended statuses' do
+ expect(factory.extended_statuses)
+ .to eq [Gitlab::Ci::Status::Build::Retryable]
+ end
+
it 'fabricates a retryable build status' do
expect(status).to be_a Gitlab::Ci::Status::Build::Retryable
end
@@ -60,6 +116,15 @@ describe Gitlab::Ci::Status::Build::Factory do
context 'when build is running' do
let(:build) { create(:ci_build, :running) }
+ it 'matches correct core status' do
+ expect(factory.core_status).to be_a Gitlab::Ci::Status::Running
+ end
+
+ it 'matches correct extended statuses' do
+ expect(factory.extended_statuses)
+ .to eq [Gitlab::Ci::Status::Build::Cancelable]
+ end
+
it 'fabricates a canceable build status' do
expect(status).to be_a Gitlab::Ci::Status::Build::Cancelable
end
@@ -76,6 +141,15 @@ describe Gitlab::Ci::Status::Build::Factory do
context 'when build is pending' do
let(:build) { create(:ci_build, :pending) }
+ it 'matches correct core status' do
+ expect(factory.core_status).to be_a Gitlab::Ci::Status::Pending
+ end
+
+ it 'matches correct extended statuses' do
+ expect(factory.extended_statuses)
+ .to eq [Gitlab::Ci::Status::Build::Cancelable]
+ end
+
it 'fabricates a cancelable build status' do
expect(status).to be_a Gitlab::Ci::Status::Build::Cancelable
end
@@ -92,6 +166,14 @@ describe Gitlab::Ci::Status::Build::Factory do
context 'when build is skipped' do
let(:build) { create(:ci_build, :skipped) }
+ it 'matches correct core status' do
+ expect(factory.core_status).to be_a Gitlab::Ci::Status::Skipped
+ end
+
+ it 'does not match extended statuses' do
+ expect(factory.extended_statuses).to be_empty
+ end
+
it 'fabricates a core skipped status' do
expect(status).to be_a Gitlab::Ci::Status::Skipped
end
@@ -109,6 +191,15 @@ describe Gitlab::Ci::Status::Build::Factory do
context 'when build is a play action' do
let(:build) { create(:ci_build, :playable) }
+ it 'matches correct core status' do
+ expect(factory.core_status).to be_a Gitlab::Ci::Status::Skipped
+ end
+
+ it 'matches correct extended statuses' do
+ expect(factory.extended_statuses)
+ .to eq [Gitlab::Ci::Status::Build::Play]
+ end
+
it 'fabricates a core skipped status' do
expect(status).to be_a Gitlab::Ci::Status::Build::Play
end
@@ -119,12 +210,22 @@ describe Gitlab::Ci::Status::Build::Factory do
expect(status.label).to eq 'manual play action'
expect(status).to have_details
expect(status).to have_action
+ expect(status.action_path).to include 'play'
end
end
context 'when build is an environment stop action' do
let(:build) { create(:ci_build, :playable, :teardown_environment) }
+ it 'matches correct core status' do
+ expect(factory.core_status).to be_a Gitlab::Ci::Status::Skipped
+ end
+
+ it 'matches correct extended statuses' do
+ expect(factory.extended_statuses)
+ .to eq [Gitlab::Ci::Status::Build::Stop]
+ end
+
it 'fabricates a core skipped status' do
expect(status).to be_a Gitlab::Ci::Status::Build::Stop
end
diff --git a/spec/lib/gitlab/ci/status/build/failed_allowed_spec.rb b/spec/lib/gitlab/ci/status/build/failed_allowed_spec.rb
new file mode 100644
index 00000000000..20f71459738
--- /dev/null
+++ b/spec/lib/gitlab/ci/status/build/failed_allowed_spec.rb
@@ -0,0 +1,110 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Status::Build::FailedAllowed do
+ let(:status) { double('core status') }
+ let(:user) { double('user') }
+
+ subject do
+ described_class.new(status)
+ end
+
+ describe '#text' do
+ it 'does not override status text' do
+ expect(status).to receive(:text)
+
+ subject.text
+ end
+ end
+
+ describe '#icon' do
+ it 'returns a warning icon' do
+ expect(subject.icon).to eq 'icon_status_warning'
+ end
+ end
+
+ describe '#label' do
+ it 'returns information about failed but allowed to fail status' do
+ expect(subject.label).to eq 'failed (allowed to fail)'
+ end
+ end
+
+ describe '#group' do
+ it 'returns status failed with warnings status group' do
+ expect(subject.group).to eq 'failed_with_warnings'
+ end
+ end
+
+ describe 'action details' do
+ describe '#has_action?' do
+ it 'does not decorate action details' do
+ expect(status).to receive(:has_action?)
+
+ subject.has_action?
+ end
+ end
+
+ describe '#action_path' do
+ it 'does not decorate action path' do
+ expect(status).to receive(:action_path)
+
+ subject.action_path
+ end
+ end
+
+ describe '#action_icon' do
+ it 'does not decorate action icon' do
+ expect(status).to receive(:action_icon)
+
+ subject.action_icon
+ end
+ end
+
+ describe '#action_title' do
+ it 'does not decorate action title' do
+ expect(status).to receive(:action_title)
+
+ subject.action_title
+ end
+ end
+ end
+
+ describe '.matches?' do
+ subject { described_class.matches?(build, user) }
+
+ context 'when build is failed' do
+ context 'when build is allowed to fail' do
+ let(:build) { create(:ci_build, :failed, :allowed_to_fail) }
+
+ it 'is a correct match' do
+ expect(subject).to be true
+ end
+ end
+
+ context 'when build is not allowed to fail' do
+ let(:build) { create(:ci_build, :failed) }
+
+ it 'is not a correct match' do
+ expect(subject).not_to be true
+ end
+ end
+ end
+
+ context 'when build did not fail' do
+ context 'when build is allowed to fail' do
+ let(:build) { create(:ci_build, :success, :allowed_to_fail) }
+
+ it 'is not a correct match' do
+ expect(subject).not_to be true
+ end
+ end
+
+ context 'when build is not allowed to fail' do
+ let(:build) { create(:ci_build, :success) }
+
+ it 'is not a correct match' do
+ expect(subject).not_to be true
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/status/build/play_spec.rb b/spec/lib/gitlab/ci/status/build/play_spec.rb
index f1b50a59ce6..f3e72ea1796 100644
--- a/spec/lib/gitlab/ci/status/build/play_spec.rb
+++ b/spec/lib/gitlab/ci/status/build/play_spec.rb
@@ -44,7 +44,7 @@ describe Gitlab::Ci::Status::Build::Play do
end
describe '#action_icon' do
- it { expect(subject.action_icon).to eq 'play' }
+ it { expect(subject.action_icon).to eq 'icon_action_play' }
end
describe '#action_title' do
diff --git a/spec/lib/gitlab/ci/status/build/retryable_spec.rb b/spec/lib/gitlab/ci/status/build/retryable_spec.rb
index 62036f8ec5d..2db0f8d29bd 100644
--- a/spec/lib/gitlab/ci/status/build/retryable_spec.rb
+++ b/spec/lib/gitlab/ci/status/build/retryable_spec.rb
@@ -62,7 +62,7 @@ describe Gitlab::Ci::Status::Build::Retryable do
end
describe '#action_icon' do
- it { expect(subject.action_icon).to eq 'refresh' }
+ it { expect(subject.action_icon).to eq 'icon_action_retry' }
end
describe '#action_title' do
diff --git a/spec/lib/gitlab/ci/status/build/stop_spec.rb b/spec/lib/gitlab/ci/status/build/stop_spec.rb
index 597e02e86e4..41c2b624774 100644
--- a/spec/lib/gitlab/ci/status/build/stop_spec.rb
+++ b/spec/lib/gitlab/ci/status/build/stop_spec.rb
@@ -46,7 +46,7 @@ describe Gitlab::Ci::Status::Build::Stop do
end
describe '#action_icon' do
- it { expect(subject.action_icon).to eq 'stop' }
+ it { expect(subject.action_icon).to eq 'icon_action_stop' }
end
describe '#action_title' do
diff --git a/spec/lib/gitlab/ci/status/external/common_spec.rb b/spec/lib/gitlab/ci/status/external/common_spec.rb
new file mode 100644
index 00000000000..5a97d98b55f
--- /dev/null
+++ b/spec/lib/gitlab/ci/status/external/common_spec.rb
@@ -0,0 +1,39 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Status::External::Common do
+ let(:user) { create(:user) }
+ let(:project) { external_status.project }
+ let(:external_target_url) { 'http://example.gitlab.com/status' }
+
+ let(:external_status) do
+ create(:generic_commit_status, target_url: external_target_url)
+ end
+
+ subject do
+ Gitlab::Ci::Status::Core
+ .new(external_status, user)
+ .extend(described_class)
+ end
+
+ describe '#has_action?' do
+ it { is_expected.not_to have_action }
+ end
+
+ describe '#has_details?' do
+ context 'when user has access to read commit status' do
+ before { project.team << [user, :developer] }
+
+ it { is_expected.to have_details }
+ end
+
+ context 'when user does not have access to read commit status' do
+ it { is_expected.not_to have_details }
+ end
+ end
+
+ describe '#details_path' do
+ it 'links to the external target URL' do
+ expect(subject.details_path).to eq external_target_url
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/status/external/factory_spec.rb b/spec/lib/gitlab/ci/status/external/factory_spec.rb
new file mode 100644
index 00000000000..c96fd53e730
--- /dev/null
+++ b/spec/lib/gitlab/ci/status/external/factory_spec.rb
@@ -0,0 +1,38 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Status::External::Factory do
+ let(:user) { create(:user) }
+ let(:project) { resource.project }
+ let(:status) { factory.fabricate! }
+ let(:factory) { described_class.new(resource, user) }
+ let(:external_url) { 'http://gitlab.com/status' }
+
+ before do
+ project.team << [user, :developer]
+ end
+
+ context 'when external status has a simple core status' do
+ HasStatus::AVAILABLE_STATUSES.each do |simple_status|
+ context "when core status is #{simple_status}" do
+ let(:resource) do
+ create(:generic_commit_status, status: simple_status,
+ target_url: external_url)
+ end
+
+ let(:expected_status) do
+ Gitlab::Ci::Status.const_get(simple_status.capitalize)
+ end
+
+ it "fabricates a core status #{simple_status}" do
+ expect(status).to be_a expected_status
+ end
+
+ it 'extends core status with common methods' do
+ expect(status).to have_details
+ expect(status).not_to have_action
+ expect(status.details_path).to eq external_url
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/status/factory_spec.rb b/spec/lib/gitlab/ci/status/factory_spec.rb
index f92a1c149bf..bbf9c7c83a3 100644
--- a/spec/lib/gitlab/ci/status/factory_spec.rb
+++ b/spec/lib/gitlab/ci/status/factory_spec.rb
@@ -1,24 +1,135 @@
require 'spec_helper'
describe Gitlab::Ci::Status::Factory do
- subject do
- described_class.new(resource, user)
+ let(:user) { create(:user) }
+ let(:fabricated_status) { factory.fabricate! }
+ let(:factory) { described_class.new(resource, user) }
+
+ context 'when object has a core status' do
+ HasStatus::AVAILABLE_STATUSES.each do |simple_status|
+ context "when simple core status is #{simple_status}" do
+ let(:resource) { double('resource', status: simple_status) }
+
+ let(:expected_status) do
+ Gitlab::Ci::Status.const_get(simple_status.capitalize)
+ end
+
+ it "fabricates a core status #{simple_status}" do
+ expect(fabricated_status).to be_a expected_status
+ end
+
+ it "matches a valid core status for #{simple_status}" do
+ expect(factory.core_status).to be_a expected_status
+ end
+
+ it "does not match any extended statuses for #{simple_status}" do
+ expect(factory.extended_statuses).to be_empty
+ end
+ end
+ end
end
- let(:user) { create(:user) }
+ context 'when resource supports multiple extended statuses' do
+ let(:resource) { double('resource', status: :success) }
- let(:status) { subject.fabricate! }
+ let(:first_extended_status) do
+ Class.new(SimpleDelegator) do
+ def first_method
+ 'first return value'
+ end
- context 'when object has a core status' do
- HasStatus::AVAILABLE_STATUSES.each do |core_status|
- context "when core status is #{core_status}" do
- let(:resource) { double(status: core_status) }
+ def second_method
+ 'second return value'
+ end
+
+ def self.matches?(*)
+ true
+ end
+ end
+ end
- it "fabricates a core status #{core_status}" do
- expect(status).to be_a(
- Gitlab::Ci::Status.const_get(core_status.capitalize))
+ let(:second_extended_status) do
+ Class.new(SimpleDelegator) do
+ def first_method
+ 'decorated return value'
end
+
+ def third_method
+ 'third return value'
+ end
+
+ def self.matches?(*)
+ true
+ end
+ end
+ end
+
+ shared_examples 'compound decorator factory' do
+ it 'fabricates compound decorator' do
+ expect(fabricated_status.first_method).to eq 'decorated return value'
+ expect(fabricated_status.second_method).to eq 'second return value'
+ expect(fabricated_status.third_method).to eq 'third return value'
end
+
+ it 'delegates to core status' do
+ expect(fabricated_status.text).to eq 'passed'
+ end
+
+ it 'latest matches status becomes a status name' do
+ expect(fabricated_status.class).to eq second_extended_status
+ end
+
+ it 'matches correct core status' do
+ expect(factory.core_status).to be_a Gitlab::Ci::Status::Success
+ end
+
+ it 'matches correct extended statuses' do
+ expect(factory.extended_statuses)
+ .to eq [first_extended_status, second_extended_status]
+ end
+ end
+
+ context 'when exclusive statuses are matches' do
+ before do
+ allow(described_class).to receive(:extended_statuses)
+ .and_return([[first_extended_status, second_extended_status]])
+ end
+
+ it 'does not fabricate compound decorator' do
+ expect(fabricated_status.first_method).to eq 'first return value'
+ expect(fabricated_status.second_method).to eq 'second return value'
+ expect(fabricated_status).not_to respond_to(:third_method)
+ end
+
+ it 'delegates to core status' do
+ expect(fabricated_status.text).to eq 'passed'
+ end
+
+ it 'matches correct core status' do
+ expect(factory.core_status).to be_a Gitlab::Ci::Status::Success
+ end
+
+ it 'matches correct extended statuses' do
+ expect(factory.extended_statuses).to eq [first_extended_status]
+ end
+ end
+
+ context 'when exclusive statuses are not matched' do
+ before do
+ allow(described_class).to receive(:extended_statuses)
+ .and_return([[first_extended_status], [second_extended_status]])
+ end
+
+ it_behaves_like 'compound decorator factory'
+ end
+
+ context 'when using simplified status grouping' do
+ before do
+ allow(described_class).to receive(:extended_statuses)
+ .and_return([first_extended_status, second_extended_status])
+ end
+
+ it_behaves_like 'compound decorator factory'
end
end
end
diff --git a/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb b/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb
index d4a2dc7fcc1..b10a447c27a 100644
--- a/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb
+++ b/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb
@@ -3,29 +3,32 @@ require 'spec_helper'
describe Gitlab::Ci::Status::Pipeline::Factory do
let(:user) { create(:user) }
let(:project) { pipeline.project }
-
- subject do
- described_class.new(pipeline, user)
- end
-
- let(:status) do
- subject.fabricate!
- end
+ let(:status) { factory.fabricate! }
+ let(:factory) { described_class.new(pipeline, user) }
before do
project.team << [user, :developer]
end
context 'when pipeline has a core status' do
- HasStatus::AVAILABLE_STATUSES.each do |core_status|
- context "when core status is #{core_status}" do
- let(:pipeline) do
- create(:ci_pipeline, status: core_status)
+ HasStatus::AVAILABLE_STATUSES.each do |simple_status|
+ context "when core status is #{simple_status}" do
+ let(:pipeline) { create(:ci_pipeline, status: simple_status) }
+
+ let(:expected_status) do
+ Gitlab::Ci::Status.const_get(simple_status.capitalize)
+ end
+
+ it "matches correct core status for #{simple_status}" do
+ expect(factory.core_status).to be_a expected_status
end
- it "fabricates a core status #{core_status}" do
- expect(status).to be_a(
- Gitlab::Ci::Status.const_get(core_status.capitalize))
+ it 'does not matche extended statuses' do
+ expect(factory.extended_statuses).to be_empty
+ end
+
+ it "fabricates a core status #{simple_status}" do
+ expect(status).to be_a expected_status
end
it 'extends core status with common pipeline methods' do
@@ -47,13 +50,22 @@ describe Gitlab::Ci::Status::Pipeline::Factory do
create(:ci_build, :allowed_to_fail, :failed, pipeline: pipeline)
end
+ it 'matches correct core status' do
+ expect(factory.core_status).to be_a Gitlab::Ci::Status::Success
+ end
+
+ it 'matches correct extended statuses' do
+ expect(factory.extended_statuses)
+ .to eq [Gitlab::Ci::Status::SuccessWarning]
+ end
+
it 'fabricates extended "success with warnings" status' do
- expect(status)
- .to be_a Gitlab::Ci::Status::Pipeline::SuccessWithWarnings
+ expect(status).to be_a Gitlab::Ci::Status::SuccessWarning
end
- it 'extends core status with common pipeline methods' do
+ it 'extends core status with common pipeline method' do
expect(status).to have_details
+ expect(status.details_path).to include "pipelines/#{pipeline.id}"
end
end
end
diff --git a/spec/lib/gitlab/ci/status/pipeline/success_with_warnings_spec.rb b/spec/lib/gitlab/ci/status/pipeline/success_with_warnings_spec.rb
deleted file mode 100644
index 979160eb9c4..00000000000
--- a/spec/lib/gitlab/ci/status/pipeline/success_with_warnings_spec.rb
+++ /dev/null
@@ -1,69 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::Ci::Status::Pipeline::SuccessWithWarnings do
- subject do
- described_class.new(double('status'))
- end
-
- describe '#test' do
- it { expect(subject.text).to eq 'passed' }
- end
-
- describe '#label' do
- it { expect(subject.label).to eq 'passed with warnings' }
- end
-
- describe '#icon' do
- it { expect(subject.icon).to eq 'icon_status_warning' }
- end
-
- describe '#group' do
- it { expect(subject.group).to eq 'success_with_warnings' }
- end
-
- describe '.matches?' do
- context 'when pipeline is successful' do
- let(:pipeline) do
- create(:ci_pipeline, status: :success)
- end
-
- context 'when pipeline has warnings' do
- before do
- allow(pipeline).to receive(:has_warnings?).and_return(true)
- end
-
- it 'is a correct match' do
- expect(described_class.matches?(pipeline, double)).to eq true
- end
- end
-
- context 'when pipeline does not have warnings' do
- it 'does not match' do
- expect(described_class.matches?(pipeline, double)).to eq false
- end
- end
- end
-
- context 'when pipeline is not successful' do
- let(:pipeline) do
- create(:ci_pipeline, status: :skipped)
- end
-
- context 'when pipeline has warnings' do
- before do
- allow(pipeline).to receive(:has_warnings?).and_return(true)
- end
-
- it 'does not match' do
- expect(described_class.matches?(pipeline, double)).to eq false
- end
- end
-
- context 'when pipeline does not have warnings' do
- it 'does not match' do
- expect(described_class.matches?(pipeline, double)).to eq false
- end
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/ci/status/stage/factory_spec.rb b/spec/lib/gitlab/ci/status/stage/factory_spec.rb
index 6f8721d30c2..bbb40e2c1ab 100644
--- a/spec/lib/gitlab/ci/status/stage/factory_spec.rb
+++ b/spec/lib/gitlab/ci/status/stage/factory_spec.rb
@@ -43,4 +43,25 @@ describe Gitlab::Ci::Status::Stage::Factory do
end
end
end
+
+ context 'when stage has warnings' do
+ let(:stage) do
+ build(:ci_stage, name: 'test', status: :success, pipeline: pipeline)
+ end
+
+ before do
+ create(:ci_build, :allowed_to_fail, :failed,
+ stage: 'test', pipeline: stage.pipeline)
+ end
+
+ it 'fabricates extended "success with warnings" status' do
+ expect(status)
+ .to be_a Gitlab::Ci::Status::SuccessWarning
+ end
+
+ it 'extends core status with common stage method' do
+ expect(status).to have_details
+ expect(status.details_path).to include "pipelines/#{pipeline.id}##{stage.name}"
+ end
+ end
end
diff --git a/spec/lib/gitlab/ci/status/success_warning_spec.rb b/spec/lib/gitlab/ci/status/success_warning_spec.rb
new file mode 100644
index 00000000000..7e2269397c6
--- /dev/null
+++ b/spec/lib/gitlab/ci/status/success_warning_spec.rb
@@ -0,0 +1,75 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Status::SuccessWarning do
+ subject do
+ described_class.new(double('status'))
+ end
+
+ describe '#test' do
+ it { expect(subject.text).to eq 'passed' }
+ end
+
+ describe '#label' do
+ it { expect(subject.label).to eq 'passed with warnings' }
+ end
+
+ describe '#icon' do
+ it { expect(subject.icon).to eq 'icon_status_warning' }
+ end
+
+ describe '#group' do
+ it { expect(subject.group).to eq 'success_with_warnings' }
+ end
+
+ describe '.matches?' do
+ let(:matchable) { double('matchable') }
+
+ context 'when matchable subject is successful' do
+ before do
+ allow(matchable).to receive(:success?).and_return(true)
+ end
+
+ context 'when matchable subject has warnings' do
+ before do
+ allow(matchable).to receive(:has_warnings?).and_return(true)
+ end
+
+ it 'is a correct match' do
+ expect(described_class.matches?(matchable, double)).to eq true
+ end
+ end
+
+ context 'when matchable subject does not have warnings' do
+ before do
+ allow(matchable).to receive(:has_warnings?).and_return(false)
+ end
+
+ it 'does not match' do
+ expect(described_class.matches?(matchable, double)).to eq false
+ end
+ end
+ end
+
+ context 'when matchable subject is not successful' do
+ before do
+ allow(matchable).to receive(:success?).and_return(false)
+ end
+
+ context 'when matchable subject has warnings' do
+ before do
+ allow(matchable).to receive(:has_warnings?).and_return(true)
+ end
+
+ it 'does not match' do
+ expect(described_class.matches?(matchable, double)).to eq false
+ end
+ end
+
+ context 'when matchable subject does not have warnings' do
+ it 'does not match' do
+ expect(described_class.matches?(matchable, double)).to eq false
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/trace_reader_spec.rb b/spec/lib/gitlab/ci/trace_reader_spec.rb
index f06d78694d6..ff5551bf703 100644
--- a/spec/lib/gitlab/ci/trace_reader_spec.rb
+++ b/spec/lib/gitlab/ci/trace_reader_spec.rb
@@ -11,13 +11,25 @@ describe Gitlab::Ci::TraceReader do
last_lines = random_lines
expected = lines.last(last_lines).join
+ result = subject.read(last_lines: last_lines)
- expect(subject.read(last_lines: last_lines)).to eq(expected)
+ expect(result).to eq(expected)
+ expect(result.encoding).to eq(Encoding.default_external)
end
end
it 'returns everything if trying to get too many lines' do
- expect(build_subject.read(last_lines: lines.size * 2)).to eq(lines.join)
+ result = build_subject.read(last_lines: lines.size * 2)
+
+ expect(result).to eq(lines.join)
+ expect(result.encoding).to eq(Encoding.default_external)
+ end
+
+ it 'returns all contents if last_lines is not specified' do
+ result = build_subject.read
+
+ expect(result).to eq(lines.join)
+ expect(result.encoding).to eq(Encoding.default_external)
end
it 'raises an error if not passing an integer for last_lines' do
diff --git a/spec/lib/gitlab/closing_issue_extractor_spec.rb b/spec/lib/gitlab/closing_issue_extractor_spec.rb
index 1bbaca0739a..97af1c2523d 100644
--- a/spec/lib/gitlab/closing_issue_extractor_spec.rb
+++ b/spec/lib/gitlab/closing_issue_extractor_spec.rb
@@ -1,11 +1,11 @@
require 'spec_helper'
describe Gitlab::ClosingIssueExtractor, lib: true do
- let(:project) { create(:project) }
- let(:project2) { create(:project) }
+ let(:project) { create(:empty_project) }
+ let(:project2) { create(:empty_project) }
let(:forked_project) { Projects::ForkService.new(project, project.creator).execute }
- let(:issue) { create(:issue, project: project) }
- let(:issue2) { create(:issue, project: project2) }
+ 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) }
diff --git a/spec/lib/gitlab/conflict/file_spec.rb b/spec/lib/gitlab/conflict/file_spec.rb
index 648d342ecf8..fbf679c5215 100644
--- a/spec/lib/gitlab/conflict/file_spec.rb
+++ b/spec/lib/gitlab/conflict/file_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Gitlab::Conflict::File, lib: true do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:repository) { project.repository }
let(:rugged) { repository.rugged }
let(:their_commit) { rugged.branches['conflict-start'].target }
diff --git a/spec/lib/gitlab/contributions_calendar_spec.rb b/spec/lib/gitlab/contributions_calendar_spec.rb
index 01b2a55b63c..e18a219ef36 100644
--- a/spec/lib/gitlab/contributions_calendar_spec.rb
+++ b/spec/lib/gitlab/contributions_calendar_spec.rb
@@ -17,7 +17,7 @@ describe Gitlab::ContributionsCalendar do
end
let(:feature_project) do
- create(:empty_project, :public, issues_access_level: ProjectFeature::PRIVATE) do |project|
+ create(:empty_project, :public, :issues_private) do |project|
create(:project_member, user: contributor, project: project).project
end
end
diff --git a/spec/lib/gitlab/current_settings_spec.rb b/spec/lib/gitlab/current_settings_spec.rb
index 004341ffd02..b01c4805a34 100644
--- a/spec/lib/gitlab/current_settings_spec.rb
+++ b/spec/lib/gitlab/current_settings_spec.rb
@@ -1,36 +1,64 @@
require 'spec_helper'
describe Gitlab::CurrentSettings do
+ include StubENV
+
+ before do
+ stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
+ end
+
describe '#current_application_settings' do
- it 'attempts to use cached values first' do
- allow_any_instance_of(Gitlab::CurrentSettings).to receive(:connect_to_db?).and_return(true)
- expect(ApplicationSetting).to receive(:current).and_return(::ApplicationSetting.create_from_defaults)
- expect(ApplicationSetting).not_to receive(:last)
+ context 'with DB available' do
+ before do
+ allow_any_instance_of(Gitlab::CurrentSettings).to receive(:connect_to_db?).and_return(true)
+ end
- expect(current_application_settings).to be_a(ApplicationSetting)
- end
+ it 'attempts to use cached values first' do
+ expect(ApplicationSetting).to receive(:current)
+ expect(ApplicationSetting).not_to receive(:last)
+
+ expect(current_application_settings).to be_a(ApplicationSetting)
+ end
- it 'does not attempt to connect to DB or Redis' do
- allow_any_instance_of(Gitlab::CurrentSettings).to receive(:connect_to_db?).and_return(false)
- expect(ApplicationSetting).not_to receive(:current)
- expect(ApplicationSetting).not_to receive(:last)
+ it 'falls back to DB if Redis returns an empty value' do
+ expect(ApplicationSetting).to receive(:last).and_call_original
- expect(current_application_settings).to eq fake_application_settings
+ expect(current_application_settings).to be_a(ApplicationSetting)
+ end
+
+ it 'falls back to DB if Redis fails' do
+ expect(ApplicationSetting).to receive(:current).and_raise(::Redis::BaseError)
+ expect(ApplicationSetting).to receive(:last).and_call_original
+
+ expect(current_application_settings).to be_a(ApplicationSetting)
+ end
end
- it 'falls back to DB if Redis returns an empty value' do
- allow_any_instance_of(Gitlab::CurrentSettings).to receive(:connect_to_db?).and_return(true)
- expect(ApplicationSetting).to receive(:last).and_call_original
+ context 'with DB unavailable' do
+ before do
+ allow_any_instance_of(Gitlab::CurrentSettings).to receive(:connect_to_db?).and_return(false)
+ end
- expect(current_application_settings).to be_a(ApplicationSetting)
+ it 'returns an in-memory ApplicationSetting object' do
+ expect(ApplicationSetting).not_to receive(:current)
+ expect(ApplicationSetting).not_to receive(:last)
+
+ expect(current_application_settings).to be_a(OpenStruct)
+ end
end
- it 'falls back to DB if Redis fails' do
- allow_any_instance_of(Gitlab::CurrentSettings).to receive(:connect_to_db?).and_return(true)
- expect(ApplicationSetting).to receive(:current).and_raise(::Redis::BaseError)
- expect(ApplicationSetting).to receive(:last).and_call_original
+ context 'when ENV["IN_MEMORY_APPLICATION_SETTINGS"] is true' do
+ before do
+ stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'true')
+ end
+
+ it 'returns an in-memory ApplicationSetting object' do
+ expect(ApplicationSetting).not_to receive(:current)
+ expect(ApplicationSetting).not_to receive(:last)
- expect(current_application_settings).to be_a(ApplicationSetting)
+ expect(current_application_settings).to be_a(ApplicationSetting)
+ expect(current_application_settings).not_to be_persisted
+ end
end
end
end
diff --git a/spec/lib/gitlab/cycle_analytics/code_event_fetcher_spec.rb b/spec/lib/gitlab/cycle_analytics/code_event_fetcher_spec.rb
new file mode 100644
index 00000000000..0267e8c2f69
--- /dev/null
+++ b/spec/lib/gitlab/cycle_analytics/code_event_fetcher_spec.rb
@@ -0,0 +1,12 @@
+require 'spec_helper'
+require 'lib/gitlab/cycle_analytics/shared_event_spec'
+
+describe Gitlab::CycleAnalytics::CodeEventFetcher do
+ let(:stage_name) { :code }
+
+ it_behaves_like 'default query config' do
+ it 'has a default order' do
+ expect(event.order).not_to be_nil
+ end
+ end
+end
diff --git a/spec/lib/gitlab/cycle_analytics/code_event_spec.rb b/spec/lib/gitlab/cycle_analytics/code_event_spec.rb
deleted file mode 100644
index 43f42d1bde8..00000000000
--- a/spec/lib/gitlab/cycle_analytics/code_event_spec.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-require 'spec_helper'
-require 'lib/gitlab/cycle_analytics/shared_event_spec'
-
-describe Gitlab::CycleAnalytics::CodeEvent do
- it_behaves_like 'default query config' do
- it 'does not have the default order' do
- expect(event.order).not_to eq(event.start_time_attrs)
- end
- end
-end
diff --git a/spec/lib/gitlab/cycle_analytics/code_stage_spec.rb b/spec/lib/gitlab/cycle_analytics/code_stage_spec.rb
new file mode 100644
index 00000000000..e8fc67acf05
--- /dev/null
+++ b/spec/lib/gitlab/cycle_analytics/code_stage_spec.rb
@@ -0,0 +1,8 @@
+require 'spec_helper'
+require 'lib/gitlab/cycle_analytics/shared_stage_spec'
+
+describe Gitlab::CycleAnalytics::CodeStage do
+ let(:stage_name) { :code }
+
+ it_behaves_like 'base stage'
+end
diff --git a/spec/lib/gitlab/cycle_analytics/events_spec.rb b/spec/lib/gitlab/cycle_analytics/events_spec.rb
index 6062e7af4f5..9d2ba481919 100644
--- a/spec/lib/gitlab/cycle_analytics/events_spec.rb
+++ b/spec/lib/gitlab/cycle_analytics/events_spec.rb
@@ -1,12 +1,14 @@
require 'spec_helper'
-describe Gitlab::CycleAnalytics::Events do
+describe 'cycle analytics events' do
let(:project) { create(:project) }
let(:from_date) { 10.days.ago }
let(:user) { create(:user, :admin) }
let!(:context) { create(:issue, project: project, created_at: 2.days.ago) }
- subject { described_class.new(project: project, options: { from: from_date, current_user: user }) }
+ let(:events) do
+ CycleAnalytics.new(project, { from: from_date, current_user: user })[stage].events
+ end
before do
allow_any_instance_of(Gitlab::ReferenceExtractor).to receive(:issues).and_return([context])
@@ -15,104 +17,112 @@ describe Gitlab::CycleAnalytics::Events do
end
describe '#issue_events' do
+ let(:stage) { :issue }
+
it 'has the total time' do
- expect(subject.issue_events.first[:total_time]).not_to be_empty
+ expect(events.first[:total_time]).not_to be_empty
end
it 'has a title' do
- expect(subject.issue_events.first[:title]).to eq(context.title)
+ expect(events.first[:title]).to eq(context.title)
end
it 'has the URL' do
- expect(subject.issue_events.first[:url]).not_to be_nil
+ expect(events.first[:url]).not_to be_nil
end
it 'has an iid' do
- expect(subject.issue_events.first[:iid]).to eq(context.iid.to_s)
+ expect(events.first[:iid]).to eq(context.iid.to_s)
end
it 'has a created_at timestamp' do
- expect(subject.issue_events.first[:created_at]).to end_with('ago')
+ expect(events.first[:created_at]).to end_with('ago')
end
it "has the author's URL" do
- expect(subject.issue_events.first[:author][:web_url]).not_to be_nil
+ expect(events.first[:author][:web_url]).not_to be_nil
end
it "has the author's avatar URL" do
- expect(subject.issue_events.first[:author][:avatar_url]).not_to be_nil
+ expect(events.first[:author][:avatar_url]).not_to be_nil
end
it "has the author's name" do
- expect(subject.issue_events.first[:author][:name]).to eq(context.author.name)
+ expect(events.first[:author][:name]).to eq(context.author.name)
end
end
describe '#plan_events' do
+ let(:stage) { :plan }
+
it 'has a title' do
- expect(subject.plan_events.first[:title]).not_to be_nil
+ expect(events.first[:title]).not_to be_nil
end
it 'has a sha short ID' do
- expect(subject.plan_events.first[:short_sha]).not_to be_nil
+ expect(events.first[:short_sha]).not_to be_nil
end
it 'has the URL' do
- expect(subject.plan_events.first[:commit_url]).not_to be_nil
+ expect(events.first[:commit_url]).not_to be_nil
end
it 'has the total time' do
- expect(subject.plan_events.first[:total_time]).not_to be_empty
+ expect(events.first[:total_time]).not_to be_empty
end
it "has the author's URL" do
- expect(subject.plan_events.first[:author][:web_url]).not_to be_nil
+ expect(events.first[:author][:web_url]).not_to be_nil
end
it "has the author's avatar URL" do
- expect(subject.plan_events.first[:author][:avatar_url]).not_to be_nil
+ expect(events.first[:author][:avatar_url]).not_to be_nil
end
it "has the author's name" do
- expect(subject.plan_events.first[:author][:name]).not_to be_nil
+ expect(events.first[:author][:name]).not_to be_nil
end
end
describe '#code_events' do
+ let(:stage) { :code }
+
before do
create_commit_referencing_issue(context)
end
it 'has the total time' do
- expect(subject.code_events.first[:total_time]).not_to be_empty
+ expect(events.first[:total_time]).not_to be_empty
end
it 'has a title' do
- expect(subject.code_events.first[:title]).to eq('Awesome merge_request')
+ expect(events.first[:title]).to eq('Awesome merge_request')
end
it 'has an iid' do
- expect(subject.code_events.first[:iid]).to eq(context.iid.to_s)
+ expect(events.first[:iid]).to eq(context.iid.to_s)
end
it 'has a created_at timestamp' do
- expect(subject.code_events.first[:created_at]).to end_with('ago')
+ expect(events.first[:created_at]).to end_with('ago')
end
it "has the author's URL" do
- expect(subject.code_events.first[:author][:web_url]).not_to be_nil
+ expect(events.first[:author][:web_url]).not_to be_nil
end
it "has the author's avatar URL" do
- expect(subject.code_events.first[:author][:avatar_url]).not_to be_nil
+ expect(events.first[:author][:avatar_url]).not_to be_nil
end
it "has the author's name" do
- expect(subject.code_events.first[:author][:name]).to eq(MergeRequest.first.author.name)
+ expect(events.first[:author][:name]).to eq(MergeRequest.first.author.name)
end
end
describe '#test_events' do
+ let(:stage) { :test }
+
let(:merge_request) { MergeRequest.first }
let!(:pipeline) do
create(:ci_pipeline,
@@ -130,83 +140,85 @@ describe Gitlab::CycleAnalytics::Events do
end
it 'has the name' do
- expect(subject.test_events.first[:name]).not_to be_nil
+ expect(events.first[:name]).not_to be_nil
end
it 'has the ID' do
- expect(subject.test_events.first[:id]).not_to be_nil
+ expect(events.first[:id]).not_to be_nil
end
it 'has the URL' do
- expect(subject.test_events.first[:url]).not_to be_nil
+ expect(events.first[:url]).not_to be_nil
end
it 'has the branch name' do
- expect(subject.test_events.first[:branch]).not_to be_nil
+ expect(events.first[:branch]).not_to be_nil
end
it 'has the branch URL' do
- expect(subject.test_events.first[:branch][:url]).not_to be_nil
+ expect(events.first[:branch][:url]).not_to be_nil
end
it 'has the short SHA' do
- expect(subject.test_events.first[:short_sha]).not_to be_nil
+ expect(events.first[:short_sha]).not_to be_nil
end
it 'has the commit URL' do
- expect(subject.test_events.first[:commit_url]).not_to be_nil
+ expect(events.first[:commit_url]).not_to be_nil
end
it 'has the date' do
- expect(subject.test_events.first[:date]).not_to be_nil
+ expect(events.first[:date]).not_to be_nil
end
it 'has the total time' do
- expect(subject.test_events.first[:total_time]).not_to be_empty
+ expect(events.first[:total_time]).not_to be_empty
end
end
describe '#review_events' do
+ let(:stage) { :review }
let!(:context) { create(:issue, project: project, created_at: 2.days.ago) }
it 'has the total time' do
- expect(subject.review_events.first[:total_time]).not_to be_empty
+ expect(events.first[:total_time]).not_to be_empty
end
it 'has a title' do
- expect(subject.review_events.first[:title]).to eq('Awesome merge_request')
+ expect(events.first[:title]).to eq('Awesome merge_request')
end
it 'has an iid' do
- expect(subject.review_events.first[:iid]).to eq(context.iid.to_s)
+ expect(events.first[:iid]).to eq(context.iid.to_s)
end
it 'has the URL' do
- expect(subject.review_events.first[:url]).not_to be_nil
+ expect(events.first[:url]).not_to be_nil
end
it 'has a state' do
- expect(subject.review_events.first[:state]).not_to be_nil
+ expect(events.first[:state]).not_to be_nil
end
it 'has a created_at timestamp' do
- expect(subject.review_events.first[:created_at]).not_to be_nil
+ expect(events.first[:created_at]).not_to be_nil
end
it "has the author's URL" do
- expect(subject.review_events.first[:author][:web_url]).not_to be_nil
+ expect(events.first[:author][:web_url]).not_to be_nil
end
it "has the author's avatar URL" do
- expect(subject.review_events.first[:author][:avatar_url]).not_to be_nil
+ expect(events.first[:author][:avatar_url]).not_to be_nil
end
it "has the author's name" do
- expect(subject.review_events.first[:author][:name]).to eq(MergeRequest.first.author.name)
+ expect(events.first[:author][:name]).to eq(MergeRequest.first.author.name)
end
end
describe '#staging_events' do
+ let(:stage) { :staging }
let(:merge_request) { MergeRequest.first }
let!(:pipeline) do
create(:ci_pipeline,
@@ -227,55 +239,56 @@ describe Gitlab::CycleAnalytics::Events do
end
it 'has the name' do
- expect(subject.staging_events.first[:name]).not_to be_nil
+ expect(events.first[:name]).not_to be_nil
end
it 'has the ID' do
- expect(subject.staging_events.first[:id]).not_to be_nil
+ expect(events.first[:id]).not_to be_nil
end
it 'has the URL' do
- expect(subject.staging_events.first[:url]).not_to be_nil
+ expect(events.first[:url]).not_to be_nil
end
it 'has the branch name' do
- expect(subject.staging_events.first[:branch]).not_to be_nil
+ expect(events.first[:branch]).not_to be_nil
end
it 'has the branch URL' do
- expect(subject.staging_events.first[:branch][:url]).not_to be_nil
+ expect(events.first[:branch][:url]).not_to be_nil
end
it 'has the short SHA' do
- expect(subject.staging_events.first[:short_sha]).not_to be_nil
+ expect(events.first[:short_sha]).not_to be_nil
end
it 'has the commit URL' do
- expect(subject.staging_events.first[:commit_url]).not_to be_nil
+ expect(events.first[:commit_url]).not_to be_nil
end
it 'has the date' do
- expect(subject.staging_events.first[:date]).not_to be_nil
+ expect(events.first[:date]).not_to be_nil
end
it 'has the total time' do
- expect(subject.staging_events.first[:total_time]).not_to be_empty
+ expect(events.first[:total_time]).not_to be_empty
end
it "has the author's URL" do
- expect(subject.staging_events.first[:author][:web_url]).not_to be_nil
+ expect(events.first[:author][:web_url]).not_to be_nil
end
it "has the author's avatar URL" do
- expect(subject.staging_events.first[:author][:avatar_url]).not_to be_nil
+ expect(events.first[:author][:avatar_url]).not_to be_nil
end
it "has the author's name" do
- expect(subject.staging_events.first[:author][:name]).to eq(MergeRequest.first.author.name)
+ expect(events.first[:author][:name]).to eq(MergeRequest.first.author.name)
end
end
describe '#production_events' do
+ let(:stage) { :production }
let!(:context) { create(:issue, project: project, created_at: 2.days.ago) }
before do
@@ -284,35 +297,35 @@ describe Gitlab::CycleAnalytics::Events do
end
it 'has the total time' do
- expect(subject.production_events.first[:total_time]).not_to be_empty
+ expect(events.first[:total_time]).not_to be_empty
end
it 'has a title' do
- expect(subject.production_events.first[:title]).to eq(context.title)
+ expect(events.first[:title]).to eq(context.title)
end
it 'has the URL' do
- expect(subject.production_events.first[:url]).not_to be_nil
+ expect(events.first[:url]).not_to be_nil
end
it 'has an iid' do
- expect(subject.production_events.first[:iid]).to eq(context.iid.to_s)
+ expect(events.first[:iid]).to eq(context.iid.to_s)
end
it 'has a created_at timestamp' do
- expect(subject.production_events.first[:created_at]).to end_with('ago')
+ expect(events.first[:created_at]).to end_with('ago')
end
it "has the author's URL" do
- expect(subject.production_events.first[:author][:web_url]).not_to be_nil
+ expect(events.first[:author][:web_url]).not_to be_nil
end
it "has the author's avatar URL" do
- expect(subject.production_events.first[:author][:avatar_url]).not_to be_nil
+ expect(events.first[:author][:avatar_url]).not_to be_nil
end
it "has the author's name" do
- expect(subject.production_events.first[:author][:name]).to eq(context.author.name)
+ expect(events.first[:author][:name]).to eq(context.author.name)
end
end
diff --git a/spec/lib/gitlab/cycle_analytics/issue_event_fetcher_spec.rb b/spec/lib/gitlab/cycle_analytics/issue_event_fetcher_spec.rb
new file mode 100644
index 00000000000..fd9fa2fee49
--- /dev/null
+++ b/spec/lib/gitlab/cycle_analytics/issue_event_fetcher_spec.rb
@@ -0,0 +1,8 @@
+require 'spec_helper'
+require 'lib/gitlab/cycle_analytics/shared_event_spec'
+
+describe Gitlab::CycleAnalytics::IssueEventFetcher do
+ let(:stage_name) { :issue }
+
+ it_behaves_like 'default query config'
+end
diff --git a/spec/lib/gitlab/cycle_analytics/issue_event_spec.rb b/spec/lib/gitlab/cycle_analytics/issue_event_spec.rb
deleted file mode 100644
index 1c5c308da7d..00000000000
--- a/spec/lib/gitlab/cycle_analytics/issue_event_spec.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-require 'spec_helper'
-require 'lib/gitlab/cycle_analytics/shared_event_spec'
-
-describe Gitlab::CycleAnalytics::IssueEvent do
- it_behaves_like 'default query config' do
- it 'has the default order' do
- expect(event.order).to eq(event.start_time_attrs)
- end
- end
-end
diff --git a/spec/lib/gitlab/cycle_analytics/issue_stage_spec.rb b/spec/lib/gitlab/cycle_analytics/issue_stage_spec.rb
new file mode 100644
index 00000000000..3127f01989d
--- /dev/null
+++ b/spec/lib/gitlab/cycle_analytics/issue_stage_spec.rb
@@ -0,0 +1,8 @@
+require 'spec_helper'
+require 'lib/gitlab/cycle_analytics/shared_stage_spec'
+
+describe Gitlab::CycleAnalytics::IssueStage do
+ let(:stage_name) { :issue }
+
+ it_behaves_like 'base stage'
+end
diff --git a/spec/lib/gitlab/cycle_analytics/plan_event_fetcher_spec.rb b/spec/lib/gitlab/cycle_analytics/plan_event_fetcher_spec.rb
new file mode 100644
index 00000000000..2e5dc5b5547
--- /dev/null
+++ b/spec/lib/gitlab/cycle_analytics/plan_event_fetcher_spec.rb
@@ -0,0 +1,16 @@
+require 'spec_helper'
+require 'lib/gitlab/cycle_analytics/shared_event_spec'
+
+describe Gitlab::CycleAnalytics::PlanEventFetcher do
+ let(:stage_name) { :plan }
+
+ it_behaves_like 'default query config' do
+ context 'no commits' do
+ it 'does not blow up if there are no commits' do
+ allow(event).to receive(:event_result).and_return([{}])
+
+ expect { event.fetch }.not_to raise_error
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/cycle_analytics/plan_event_spec.rb b/spec/lib/gitlab/cycle_analytics/plan_event_spec.rb
deleted file mode 100644
index 4a5604115ec..00000000000
--- a/spec/lib/gitlab/cycle_analytics/plan_event_spec.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-require 'spec_helper'
-require 'lib/gitlab/cycle_analytics/shared_event_spec'
-
-describe Gitlab::CycleAnalytics::PlanEvent do
- it_behaves_like 'default query config' do
- it 'has the default order' do
- expect(event.order).to eq(event.start_time_attrs)
- end
-
- context 'no commits' do
- it 'does not blow up if there are no commits' do
- allow_any_instance_of(Gitlab::CycleAnalytics::EventsQuery).to receive(:execute).and_return([{}])
-
- expect { event.fetch }.not_to raise_error
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/cycle_analytics/plan_stage_spec.rb b/spec/lib/gitlab/cycle_analytics/plan_stage_spec.rb
new file mode 100644
index 00000000000..4c715921ad6
--- /dev/null
+++ b/spec/lib/gitlab/cycle_analytics/plan_stage_spec.rb
@@ -0,0 +1,8 @@
+require 'spec_helper'
+require 'lib/gitlab/cycle_analytics/shared_stage_spec'
+
+describe Gitlab::CycleAnalytics::PlanStage do
+ let(:stage_name) { :plan }
+
+ it_behaves_like 'base stage'
+end
diff --git a/spec/lib/gitlab/cycle_analytics/production_event_fetcher_spec.rb b/spec/lib/gitlab/cycle_analytics/production_event_fetcher_spec.rb
new file mode 100644
index 00000000000..74001181305
--- /dev/null
+++ b/spec/lib/gitlab/cycle_analytics/production_event_fetcher_spec.rb
@@ -0,0 +1,8 @@
+require 'spec_helper'
+require 'lib/gitlab/cycle_analytics/shared_event_spec'
+
+describe Gitlab::CycleAnalytics::ProductionEventFetcher do
+ let(:stage_name) { :production }
+
+ it_behaves_like 'default query config'
+end
diff --git a/spec/lib/gitlab/cycle_analytics/production_event_spec.rb b/spec/lib/gitlab/cycle_analytics/production_event_spec.rb
deleted file mode 100644
index ac17e3b4287..00000000000
--- a/spec/lib/gitlab/cycle_analytics/production_event_spec.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-require 'spec_helper'
-require 'lib/gitlab/cycle_analytics/shared_event_spec'
-
-describe Gitlab::CycleAnalytics::ProductionEvent do
- it_behaves_like 'default query config' do
- it 'has the default order' do
- expect(event.order).to eq(event.start_time_attrs)
- end
- end
-end
diff --git a/spec/lib/gitlab/cycle_analytics/production_stage_spec.rb b/spec/lib/gitlab/cycle_analytics/production_stage_spec.rb
new file mode 100644
index 00000000000..916684b81eb
--- /dev/null
+++ b/spec/lib/gitlab/cycle_analytics/production_stage_spec.rb
@@ -0,0 +1,8 @@
+require 'spec_helper'
+require 'lib/gitlab/cycle_analytics/shared_stage_spec'
+
+describe Gitlab::CycleAnalytics::ProductionStage do
+ let(:stage_name) { :production }
+
+ it_behaves_like 'base stage'
+end
diff --git a/spec/lib/gitlab/cycle_analytics/review_event_fetcher_spec.rb b/spec/lib/gitlab/cycle_analytics/review_event_fetcher_spec.rb
new file mode 100644
index 00000000000..4f67c95ed4c
--- /dev/null
+++ b/spec/lib/gitlab/cycle_analytics/review_event_fetcher_spec.rb
@@ -0,0 +1,8 @@
+require 'spec_helper'
+require 'lib/gitlab/cycle_analytics/shared_event_spec'
+
+describe Gitlab::CycleAnalytics::ReviewEventFetcher do
+ let(:stage_name) { :review }
+
+ it_behaves_like 'default query config'
+end
diff --git a/spec/lib/gitlab/cycle_analytics/review_event_spec.rb b/spec/lib/gitlab/cycle_analytics/review_event_spec.rb
deleted file mode 100644
index 1ff53aa0227..00000000000
--- a/spec/lib/gitlab/cycle_analytics/review_event_spec.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-require 'spec_helper'
-require 'lib/gitlab/cycle_analytics/shared_event_spec'
-
-describe Gitlab::CycleAnalytics::ReviewEvent do
- it_behaves_like 'default query config' do
- it 'has the default order' do
- expect(event.order).to eq(event.start_time_attrs)
- end
- end
-end
diff --git a/spec/lib/gitlab/cycle_analytics/review_stage_spec.rb b/spec/lib/gitlab/cycle_analytics/review_stage_spec.rb
new file mode 100644
index 00000000000..1412c8dfa08
--- /dev/null
+++ b/spec/lib/gitlab/cycle_analytics/review_stage_spec.rb
@@ -0,0 +1,8 @@
+require 'spec_helper'
+require 'lib/gitlab/cycle_analytics/shared_stage_spec'
+
+describe Gitlab::CycleAnalytics::ReviewStage do
+ let(:stage_name) { :review }
+
+ it_behaves_like 'base stage'
+end
diff --git a/spec/lib/gitlab/cycle_analytics/shared_event_spec.rb b/spec/lib/gitlab/cycle_analytics/shared_event_spec.rb
index 7019e4c3351..9c5e57342e9 100644
--- a/spec/lib/gitlab/cycle_analytics/shared_event_spec.rb
+++ b/spec/lib/gitlab/cycle_analytics/shared_event_spec.rb
@@ -1,20 +1,13 @@
require 'spec_helper'
shared_examples 'default query config' do
- let(:event) { described_class.new(project: double, options: {}) }
-
- it 'has the start attributes' do
- expect(event.start_time_attrs).not_to be_nil
- end
+ let(:project) { create(:empty_project) }
+ let(:event) { described_class.new(project: project, stage: stage_name, options: { from: 1.day.ago }) }
it 'has the stage attribute' do
expect(event.stage).not_to be_nil
end
- it 'has the end attributes' do
- expect(event.end_time_attrs).not_to be_nil
- end
-
it 'has the projection attributes' do
expect(event.projections).not_to be_nil
end
diff --git a/spec/lib/gitlab/cycle_analytics/shared_stage_spec.rb b/spec/lib/gitlab/cycle_analytics/shared_stage_spec.rb
new file mode 100644
index 00000000000..08425acbfc8
--- /dev/null
+++ b/spec/lib/gitlab/cycle_analytics/shared_stage_spec.rb
@@ -0,0 +1,30 @@
+require 'spec_helper'
+
+shared_examples 'base stage' do
+ let(:stage) { described_class.new(project: double, options: {}) }
+
+ before do
+ allow(stage).to receive(:median).and_return(1.12)
+ allow_any_instance_of(Gitlab::CycleAnalytics::BaseEventFetcher).to receive(:event_result).and_return({})
+ end
+
+ it 'has the median data value' do
+ expect(stage.as_json[:value]).not_to be_nil
+ end
+
+ it 'has the median data stage' do
+ expect(stage.as_json[:title]).not_to be_nil
+ end
+
+ it 'has the median data description' do
+ expect(stage.as_json[:description]).not_to be_nil
+ end
+
+ it 'has the title' do
+ expect(stage.title).to eq(stage_name.to_s.capitalize)
+ end
+
+ it 'has the events' do
+ expect(stage.events).not_to be_nil
+ end
+end
diff --git a/spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb b/spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb
new file mode 100644
index 00000000000..3dd76ba5b8a
--- /dev/null
+++ b/spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb
@@ -0,0 +1,61 @@
+require 'spec_helper'
+
+describe Gitlab::CycleAnalytics::StageSummary, models: true do
+ let(:project) { create(:project, :repository) }
+ let(:from) { 1.day.ago }
+ let(:user) { create(:user, :admin) }
+ subject { described_class.new(project, from: Time.now, current_user: user).data }
+
+ describe "#new_issues" do
+ it "finds the number of issues created after the 'from date'" do
+ Timecop.freeze(5.days.ago) { create(:issue, project: project) }
+ Timecop.freeze(5.days.from_now) { create(:issue, project: project) }
+
+ expect(subject.first[:value]).to eq(1)
+ end
+
+ it "doesn't find issues from other projects" do
+ Timecop.freeze(5.days.from_now) { create(:issue, project: create(:empty_project)) }
+
+ expect(subject.first[:value]).to eq(0)
+ end
+ end
+
+ describe "#commits" do
+ it "finds the number of commits created after the 'from date'" do
+ Timecop.freeze(5.days.ago) { create_commit("Test message", project, user, 'master') }
+ Timecop.freeze(5.days.from_now) { create_commit("Test message", project, user, 'master') }
+
+ expect(subject.second[:value]).to eq(1)
+ end
+
+ it "doesn't find commits from other projects" do
+ Timecop.freeze(5.days.from_now) { create_commit("Test message", create(:project, :repository), user, 'master') }
+
+ expect(subject.second[:value]).to eq(0)
+ end
+
+ it "finds a large (> 100) snumber of commits if present" do
+ Timecop.freeze(5.days.from_now) { create_commit("Test message", project, user, 'master', count: 100) }
+
+ expect(subject.second[:value]).to eq(100)
+ end
+ end
+
+ describe "#deploys" do
+ it "finds the number of deploys made created after the 'from date'" do
+ Timecop.freeze(5.days.ago) { create(:deployment, project: project) }
+ Timecop.freeze(5.days.from_now) { create(:deployment, project: project) }
+
+ expect(subject.third[:value]).to eq(1)
+ end
+
+ it "doesn't find commits from other projects" do
+ Timecop.freeze(5.days.from_now) do
+ create(:deployment, project: create(:project, :repository))
+ end
+
+ expect(subject.third[:value]).to eq(0)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/cycle_analytics/staging_event_fetcher_spec.rb b/spec/lib/gitlab/cycle_analytics/staging_event_fetcher_spec.rb
new file mode 100644
index 00000000000..bbc82496340
--- /dev/null
+++ b/spec/lib/gitlab/cycle_analytics/staging_event_fetcher_spec.rb
@@ -0,0 +1,12 @@
+require 'spec_helper'
+require 'lib/gitlab/cycle_analytics/shared_event_spec'
+
+describe Gitlab::CycleAnalytics::StagingEventFetcher do
+ let(:stage_name) { :staging }
+
+ it_behaves_like 'default query config' do
+ it 'has a default order' do
+ expect(event.order).not_to be_nil
+ end
+ end
+end
diff --git a/spec/lib/gitlab/cycle_analytics/staging_event_spec.rb b/spec/lib/gitlab/cycle_analytics/staging_event_spec.rb
deleted file mode 100644
index 4862d4765f2..00000000000
--- a/spec/lib/gitlab/cycle_analytics/staging_event_spec.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-require 'spec_helper'
-require 'lib/gitlab/cycle_analytics/shared_event_spec'
-
-describe Gitlab::CycleAnalytics::StagingEvent do
- it_behaves_like 'default query config' do
- it 'does not have the default order' do
- expect(event.order).not_to eq(event.start_time_attrs)
- end
- end
-end
diff --git a/spec/lib/gitlab/cycle_analytics/staging_stage_spec.rb b/spec/lib/gitlab/cycle_analytics/staging_stage_spec.rb
new file mode 100644
index 00000000000..8154b3ac701
--- /dev/null
+++ b/spec/lib/gitlab/cycle_analytics/staging_stage_spec.rb
@@ -0,0 +1,8 @@
+require 'spec_helper'
+require 'lib/gitlab/cycle_analytics/shared_stage_spec'
+
+describe Gitlab::CycleAnalytics::StagingStage do
+ let(:stage_name) { :staging }
+
+ it_behaves_like 'base stage'
+end
diff --git a/spec/lib/gitlab/cycle_analytics/test_event_fetcher_spec.rb b/spec/lib/gitlab/cycle_analytics/test_event_fetcher_spec.rb
new file mode 100644
index 00000000000..6639fa54e0e
--- /dev/null
+++ b/spec/lib/gitlab/cycle_analytics/test_event_fetcher_spec.rb
@@ -0,0 +1,12 @@
+require 'spec_helper'
+require 'lib/gitlab/cycle_analytics/shared_event_spec'
+
+describe Gitlab::CycleAnalytics::TestEventFetcher do
+ let(:stage_name) { :test }
+
+ it_behaves_like 'default query config' do
+ it 'has a default order' do
+ expect(event.order).not_to be_nil
+ end
+ end
+end
diff --git a/spec/lib/gitlab/cycle_analytics/test_event_spec.rb b/spec/lib/gitlab/cycle_analytics/test_event_spec.rb
deleted file mode 100644
index e249db69fc6..00000000000
--- a/spec/lib/gitlab/cycle_analytics/test_event_spec.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-require 'spec_helper'
-require 'lib/gitlab/cycle_analytics/shared_event_spec'
-
-describe Gitlab::CycleAnalytics::TestEvent do
- it_behaves_like 'default query config' do
- it 'does not have the default order' do
- expect(event.order).not_to eq(event.start_time_attrs)
- end
- end
-end
diff --git a/spec/lib/gitlab/cycle_analytics/test_stage_spec.rb b/spec/lib/gitlab/cycle_analytics/test_stage_spec.rb
new file mode 100644
index 00000000000..eacde22cd56
--- /dev/null
+++ b/spec/lib/gitlab/cycle_analytics/test_stage_spec.rb
@@ -0,0 +1,8 @@
+require 'spec_helper'
+require 'lib/gitlab/cycle_analytics/shared_stage_spec'
+
+describe Gitlab::CycleAnalytics::TestStage do
+ let(:stage_name) { :test }
+
+ it_behaves_like 'base stage'
+end
diff --git a/spec/lib/gitlab/data_builder/note_spec.rb b/spec/lib/gitlab/data_builder/note_spec.rb
index 9a4dec91e56..04ec34492e1 100644
--- a/spec/lib/gitlab/data_builder/note_spec.rb
+++ b/spec/lib/gitlab/data_builder/note_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Gitlab::DataBuilder::Note, lib: true do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
let(:data) { described_class.build(note, user) }
let(:fixed_time) { Time.at(1425600000) } # Avoid time precision errors
diff --git a/spec/lib/gitlab/data_builder/pipeline_spec.rb b/spec/lib/gitlab/data_builder/pipeline_spec.rb
index a68f5943a6a..f13041e498c 100644
--- a/spec/lib/gitlab/data_builder/pipeline_spec.rb
+++ b/spec/lib/gitlab/data_builder/pipeline_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe Gitlab::DataBuilder::Pipeline do
let(:user) { create(:user) }
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:pipeline) do
create(:ci_pipeline,
diff --git a/spec/lib/gitlab/data_builder/push_spec.rb b/spec/lib/gitlab/data_builder/push_spec.rb
index a379f798a16..dbcfb9b7400 100644
--- a/spec/lib/gitlab/data_builder/push_spec.rb
+++ b/spec/lib/gitlab/data_builder/push_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Gitlab::DataBuilder::Push, lib: true do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
describe '.build_sample' do
diff --git a/spec/lib/gitlab/database_spec.rb b/spec/lib/gitlab/database_spec.rb
index 3031559c613..b142b3a2781 100644
--- a/spec/lib/gitlab/database_spec.rb
+++ b/spec/lib/gitlab/database_spec.rb
@@ -55,6 +55,22 @@ describe Gitlab::Database, lib: true do
end
end
+ describe '.nulls_first_order' do
+ context 'when using PostgreSQL' do
+ before { expect(described_class).to receive(:postgresql?).and_return(true) }
+
+ it { expect(described_class.nulls_first_order('column', 'ASC')).to eq 'column ASC NULLS FIRST'}
+ it { expect(described_class.nulls_first_order('column', 'DESC')).to eq 'column DESC NULLS FIRST'}
+ end
+
+ context 'when using MySQL' do
+ before { expect(described_class).to receive(:postgresql?).and_return(false) }
+
+ it { expect(described_class.nulls_first_order('column', 'ASC')).to eq 'column ASC'}
+ it { expect(described_class.nulls_first_order('column', 'DESC')).to eq 'column IS NULL, column DESC'}
+ end
+ end
+
describe '#true_value' do
it 'returns correct value for PostgreSQL' do
expect(described_class).to receive(:postgresql?).and_return(true)
diff --git a/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb b/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb
index 2a680f03476..f2bc15d39d7 100644
--- a/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb
+++ b/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb
@@ -1,21 +1,30 @@
require 'spec_helper'
describe Gitlab::Diff::FileCollection::MergeRequestDiff do
- let(:merge_request) { create :merge_request }
+ let(:merge_request) { create(:merge_request) }
+ let(:diff_files) { described_class.new(merge_request.merge_request_diff, diff_options: nil).diff_files }
- it 'does not hightlight binary files' do
+ it 'does not highlight binary files' do
allow_any_instance_of(Gitlab::Diff::File).to receive(:blob).and_return(double("text?" => false))
expect_any_instance_of(Gitlab::Diff::File).not_to receive(:highlighted_diff_lines)
- described_class.new(merge_request.merge_request_diff, diff_options: nil).diff_files
+ diff_files
end
- it 'does not hightlight file if blob is not accessable' do
+ it 'does not highlight file if blob is not accessable' do
allow_any_instance_of(Gitlab::Diff::File).to receive(:blob).and_return(nil)
expect_any_instance_of(Gitlab::Diff::File).not_to receive(:highlighted_diff_lines)
- described_class.new(merge_request.merge_request_diff, diff_options: nil).diff_files
+ diff_files
+ end
+
+ it 'does not files marked as undiffable in .gitattributes' do
+ allow_any_instance_of(Repository).to receive(:diffable?).and_return(false)
+
+ expect_any_instance_of(Gitlab::Diff::File).not_to receive(:highlighted_diff_lines)
+
+ diff_files
end
end
diff --git a/spec/lib/gitlab/diff/file_spec.rb b/spec/lib/gitlab/diff/file_spec.rb
index 38475792d93..050689b7c9a 100644
--- a/spec/lib/gitlab/diff/file_spec.rb
+++ b/spec/lib/gitlab/diff/file_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe Gitlab::Diff::File, lib: true do
include RepoHelpers
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:commit) { project.commit(sample_commit.id) }
let(:diff) { commit.raw_diffs.first }
let(:diff_file) { Gitlab::Diff::File.new(diff, diff_refs: commit.diff_refs, repository: project.repository) }
diff --git a/spec/lib/gitlab/diff/highlight_spec.rb b/spec/lib/gitlab/diff/highlight_spec.rb
index 1c2ddeed692..5893485634d 100644
--- a/spec/lib/gitlab/diff/highlight_spec.rb
+++ b/spec/lib/gitlab/diff/highlight_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe Gitlab::Diff::Highlight, lib: true do
include RepoHelpers
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:commit) { project.commit(sample_commit.id) }
let(:diff) { commit.raw_diffs.first }
let(:diff_file) { Gitlab::Diff::File.new(diff, diff_refs: commit.diff_refs, repository: project.repository) }
@@ -12,11 +12,11 @@ describe Gitlab::Diff::Highlight, lib: true do
context "with a diff file" do
let(:subject) { Gitlab::Diff::Highlight.new(diff_file, repository: project.repository).highlight }
- it 'should return Gitlab::Diff::Line elements' do
+ it 'returns Gitlab::Diff::Line elements' do
expect(subject.first).to be_an_instance_of(Gitlab::Diff::Line)
end
- it 'should not modify "match" lines' do
+ it 'does not modify "match" lines' do
expect(subject[0].text).to eq('@@ -6,12 +6,18 @@ module Popen')
expect(subject[22].text).to eq('@@ -19,6 +25,7 @@ module Popen')
end
@@ -43,11 +43,11 @@ describe Gitlab::Diff::Highlight, lib: true do
context "with diff lines" do
let(:subject) { Gitlab::Diff::Highlight.new(diff_file.diff_lines, repository: project.repository).highlight }
- it 'should return Gitlab::Diff::Line elements' do
+ it 'returns Gitlab::Diff::Line elements' do
expect(subject.first).to be_an_instance_of(Gitlab::Diff::Line)
end
- it 'should not modify "match" lines' do
+ it 'does not modify "match" lines' do
expect(subject[0].text).to eq('@@ -6,12 +6,18 @@ module Popen')
expect(subject[22].text).to eq('@@ -19,6 +25,7 @@ module Popen')
end
diff --git a/spec/lib/gitlab/diff/line_mapper_spec.rb b/spec/lib/gitlab/diff/line_mapper_spec.rb
index 4b943fa382d..2c7ecd1907e 100644
--- a/spec/lib/gitlab/diff/line_mapper_spec.rb
+++ b/spec/lib/gitlab/diff/line_mapper_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe Gitlab::Diff::LineMapper, lib: true do
include RepoHelpers
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:repository) { project.repository }
let(:commit) { project.commit(sample_commit.id) }
let(:diffs) { commit.raw_diffs }
diff --git a/spec/lib/gitlab/diff/parallel_diff_spec.rb b/spec/lib/gitlab/diff/parallel_diff_spec.rb
index af18d3c25a6..0f779339c54 100644
--- a/spec/lib/gitlab/diff/parallel_diff_spec.rb
+++ b/spec/lib/gitlab/diff/parallel_diff_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe Gitlab::Diff::ParallelDiff, lib: true do
include RepoHelpers
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:repository) { project.repository }
let(:commit) { project.commit(sample_commit.id) }
let(:diffs) { commit.raw_diffs }
@@ -12,7 +12,7 @@ describe Gitlab::Diff::ParallelDiff, lib: true do
subject { described_class.new(diff_file) }
describe '#parallelize' do
- it 'should return an array of arrays containing the parsed diff' do
+ it 'returns an array of arrays containing the parsed diff' do
diff_lines = diff_file.highlighted_diff_lines
expected = [
# Unchanged lines
diff --git a/spec/lib/gitlab/diff/position_spec.rb b/spec/lib/gitlab/diff/position_spec.rb
index 6e8fff6f516..cdf0af6d7ef 100644
--- a/spec/lib/gitlab/diff/position_spec.rb
+++ b/spec/lib/gitlab/diff/position_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe Gitlab::Diff::Position, lib: true do
include RepoHelpers
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
describe "position for an added file" do
let(:commit) { project.commit("2ea1f3dec713d940208fb5ce4a38765ecb5d3f73") }
diff --git a/spec/lib/gitlab/diff/position_tracer_spec.rb b/spec/lib/gitlab/diff/position_tracer_spec.rb
index c268f84c759..8e3e4034c8f 100644
--- a/spec/lib/gitlab/diff/position_tracer_spec.rb
+++ b/spec/lib/gitlab/diff/position_tracer_spec.rb
@@ -51,7 +51,7 @@ describe Gitlab::Diff::PositionTracer, lib: true do
include RepoHelpers
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:current_user) { project.owner }
let(:repository) { project.repository }
let(:file_name) { "test-file" }
@@ -99,7 +99,7 @@ describe Gitlab::Diff::PositionTracer, lib: true do
Files::CreateService.new(
project,
current_user,
- source_branch: branch_name,
+ start_branch: branch_name,
target_branch: branch_name,
commit_message: "Create file",
file_path: file_name,
@@ -112,7 +112,7 @@ describe Gitlab::Diff::PositionTracer, lib: true do
Files::UpdateService.new(
project,
current_user,
- source_branch: branch_name,
+ start_branch: branch_name,
target_branch: branch_name,
commit_message: "Update file",
file_path: file_name,
@@ -125,7 +125,7 @@ describe Gitlab::Diff::PositionTracer, lib: true do
Files::DeleteService.new(
project,
current_user,
- source_branch: branch_name,
+ start_branch: branch_name,
target_branch: branch_name,
commit_message: "Delete file",
file_path: file_name
@@ -1640,7 +1640,9 @@ describe Gitlab::Diff::PositionTracer, lib: true do
}
merge_request = create(:merge_request, source_branch: second_create_file_commit.sha, target_branch: branch_name, source_project: project)
- repository.merge(current_user, merge_request, options)
+
+ repository.merge(current_user, merge_request.diff_head_sha, merge_request, options)
+
project.commit(branch_name)
end
diff --git a/spec/lib/gitlab/email/email_shared_blocks.rb b/spec/lib/gitlab/email/email_shared_blocks.rb
index 19298e261e3..9d806fc524d 100644
--- a/spec/lib/gitlab/email/email_shared_blocks.rb
+++ b/spec/lib/gitlab/email/email_shared_blocks.rb
@@ -18,7 +18,7 @@ shared_context :email_shared_context do
end
end
-shared_examples :email_shared_examples do
+shared_examples :reply_processing_shared_examples do
context "when the user could not be found" do
before do
user.destroy
diff --git a/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb b/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb
index cb3651e3845..4a9c9a7fe34 100644
--- a/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb
+++ b/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb
@@ -3,7 +3,7 @@ require_relative '../email_shared_blocks'
describe Gitlab::Email::Handler::CreateIssueHandler, lib: true do
include_context :email_shared_context
- it_behaves_like :email_shared_examples
+ it_behaves_like :reply_processing_shared_examples
before do
stub_incoming_email_setting(enabled: true, address: "incoming+%{key}@appmail.adventuretime.ooo")
@@ -13,7 +13,7 @@ describe Gitlab::Email::Handler::CreateIssueHandler, lib: true do
let(:email_raw) { fixture_file('emails/valid_new_issue.eml') }
let(:namespace) { create(:namespace, path: 'gitlabhq') }
- let!(:project) { create(:project, :public, namespace: namespace) }
+ let!(:project) { create(:project, :public, :repository, namespace: namespace) }
let!(:user) do
create(
:user,
diff --git a/spec/lib/gitlab/email/handler/create_note_handler_spec.rb b/spec/lib/gitlab/email/handler/create_note_handler_spec.rb
index 48660d1dd1b..b300feaabe1 100644
--- a/spec/lib/gitlab/email/handler/create_note_handler_spec.rb
+++ b/spec/lib/gitlab/email/handler/create_note_handler_spec.rb
@@ -3,7 +3,7 @@ require_relative '../email_shared_blocks'
describe Gitlab::Email::Handler::CreateNoteHandler, lib: true do
include_context :email_shared_context
- it_behaves_like :email_shared_examples
+ it_behaves_like :reply_processing_shared_examples
before do
stub_incoming_email_setting(enabled: true, address: "reply+%{key}@appmail.adventuretime.ooo")
@@ -11,7 +11,7 @@ describe Gitlab::Email::Handler::CreateNoteHandler, lib: true do
end
let(:email_raw) { fixture_file('emails/valid_reply.eml') }
- let(:project) { create(:project, :public) }
+ let(:project) { create(:project, :public, :repository) }
let(:user) { create(:user) }
let(:note) { create(:diff_note_on_merge_request, project: project) }
let(:noteable) { note.noteable }
@@ -174,6 +174,12 @@ describe Gitlab::Email::Handler::CreateNoteHandler, lib: true do
it_behaves_like 'an email that contains a mail key', 'References'
end
+
+ context 'mail key is in the References header with a comma' do
+ let(:email_raw) { fixture_file('emails/reply_without_subaddressing_and_key_inside_references_with_a_comma.eml') }
+
+ it_behaves_like 'an email that contains a mail key', 'References'
+ end
end
end
end
diff --git a/spec/lib/gitlab/email/handler/unsubscribe_handler_spec.rb b/spec/lib/gitlab/email/handler/unsubscribe_handler_spec.rb
new file mode 100644
index 00000000000..0939e6c4514
--- /dev/null
+++ b/spec/lib/gitlab/email/handler/unsubscribe_handler_spec.rb
@@ -0,0 +1,61 @@
+require 'spec_helper'
+require_relative '../email_shared_blocks'
+
+describe Gitlab::Email::Handler::UnsubscribeHandler, lib: true do
+ include_context :email_shared_context
+
+ 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').gsub(mail_key, "#{mail_key}+unsubscribe") }
+ let(:project) { create(:empty_project, :public) }
+ let(:user) { create(:user) }
+ let(:noteable) { create(:issue, project: project) }
+
+ let!(:sent_notification) { SentNotification.record(noteable, user.id, mail_key) }
+
+ context 'when notification concerns a commit' do
+ let(:commit) { create(:commit, project: project) }
+ let!(:sent_notification) { SentNotification.record(commit, user.id, mail_key) }
+
+ it 'handler does not raise an error' do
+ expect { receiver.execute }.not_to raise_error
+ end
+ end
+
+ context 'user is unsubscribed' do
+ it 'leaves user unsubscribed' do
+ expect { receiver.execute }.not_to change { noteable.subscribed?(user) }.from(false)
+ end
+ end
+
+ context 'user is subscribed' do
+ before do
+ noteable.subscribe(user)
+ end
+
+ it 'unsubscribes user from notable' do
+ expect { receiver.execute }.to change { noteable.subscribed?(user) }.from(true).to(false)
+ 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 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
+end
diff --git a/spec/lib/gitlab/email/message/repository_push_spec.rb b/spec/lib/gitlab/email/message/repository_push_spec.rb
index 5b966bddb6a..7b3291b8315 100644
--- a/spec/lib/gitlab/email/message/repository_push_spec.rb
+++ b/spec/lib/gitlab/email/message/repository_push_spec.rb
@@ -4,7 +4,7 @@ describe Gitlab::Email::Message::RepositoryPush do
include RepoHelpers
let!(:group) { create(:group, name: 'my_group') }
- let!(:project) { create(:project, name: 'my_project', namespace: group) }
+ let!(:project) { create(:project, :repository, name: 'my_project', namespace: group) }
let!(:author) { create(:author, name: 'Author') }
let(:message) do
diff --git a/spec/lib/gitlab/email/reply_parser_spec.rb b/spec/lib/gitlab/email/reply_parser_spec.rb
index c7a0139d32a..28698e89c33 100644
--- a/spec/lib/gitlab/email/reply_parser_spec.rb
+++ b/spec/lib/gitlab/email/reply_parser_spec.rb
@@ -88,8 +88,6 @@ describe Gitlab::Email::ReplyParser, lib: true do
expect(test_parse_body(fixture_file("emails/inline_reply.eml"))).
to eq(
<<-BODY.strip_heredoc.chomp
- On Wed, Oct 8, 2014 at 11:12 AM, techAPJ <info@unconfigured.discourse.org> wrote:
-
> techAPJ <https://meta.discourse.org/users/techapj>
> November 28
>
diff --git a/spec/lib/gitlab/gfm/reference_rewriter_spec.rb b/spec/lib/gitlab/gfm/reference_rewriter_spec.rb
index f4703dc704f..5d416c9eec3 100644
--- a/spec/lib/gitlab/gfm/reference_rewriter_spec.rb
+++ b/spec/lib/gitlab/gfm/reference_rewriter_spec.rb
@@ -2,8 +2,8 @@ require 'spec_helper'
describe Gitlab::Gfm::ReferenceRewriter do
let(:text) { 'some text' }
- let(:old_project) { create(:project, name: 'old') }
- let(:new_project) { create(:project, name: 'new') }
+ let(:old_project) { create(:empty_project, name: 'old-project') }
+ let(:new_project) { create(:empty_project, name: 'new-project') }
let(:user) { create(:user) }
before { old_project.team << [user, :reporter] }
diff --git a/spec/lib/gitlab/gfm/uploads_rewriter_spec.rb b/spec/lib/gitlab/gfm/uploads_rewriter_spec.rb
index 6eca33f9fee..c3016f63ebf 100644
--- a/spec/lib/gitlab/gfm/uploads_rewriter_spec.rb
+++ b/spec/lib/gitlab/gfm/uploads_rewriter_spec.rb
@@ -2,8 +2,8 @@ require 'spec_helper'
describe Gitlab::Gfm::UploadsRewriter do
let(:user) { create(:user) }
- let(:old_project) { create(:project) }
- let(:new_project) { create(:project) }
+ let(:old_project) { create(:empty_project) }
+ let(:new_project) { create(:empty_project) }
let(:rewriter) { described_class.new(text, old_project, user) }
context 'text contains links to uploads' do
diff --git a/spec/lib/gitlab/git/attributes_spec.rb b/spec/lib/gitlab/git/attributes_spec.rb
new file mode 100644
index 00000000000..9c011e34c11
--- /dev/null
+++ b/spec/lib/gitlab/git/attributes_spec.rb
@@ -0,0 +1,150 @@
+require 'spec_helper'
+
+describe Gitlab::Git::Attributes, seed_helper: true do
+ let(:path) do
+ File.join(SEED_REPOSITORY_PATH, 'with-git-attributes.git')
+ end
+
+ subject { described_class.new(path) }
+
+ describe '#attributes' do
+ context 'using a path with attributes' do
+ it 'returns the attributes as a Hash' do
+ expect(subject.attributes('test.txt')).to eq({ 'text' => true })
+ end
+
+ it 'returns a Hash containing multiple attributes' do
+ expect(subject.attributes('test.sh')).
+ to eq({ 'eol' => 'lf', 'gitlab-language' => 'shell' })
+ end
+
+ it 'returns a Hash containing attributes for a file with multiple extensions' do
+ expect(subject.attributes('test.haml.html')).
+ to eq({ 'gitlab-language' => 'haml' })
+ end
+
+ it 'returns a Hash containing attributes for a file in a directory' do
+ expect(subject.attributes('foo/bar.txt')).to eq({ 'foo' => true })
+ end
+
+ it 'returns a Hash containing attributes with query string parameters' do
+ expect(subject.attributes('foo.cgi')).
+ to eq({ 'key' => 'value?p1=v1&p2=v2' })
+ end
+
+ it 'returns a Hash containing the attributes for an absolute path' do
+ expect(subject.attributes('/test.txt')).to eq({ 'text' => true })
+ end
+
+ it 'returns a Hash containing the attributes when a pattern is defined using an absolute path' do
+ # When a path is given without a leading slash it should still match
+ # patterns defined with a leading slash.
+ expect(subject.attributes('foo.png')).
+ to eq({ 'gitlab-language' => 'png' })
+
+ expect(subject.attributes('/foo.png')).
+ to eq({ 'gitlab-language' => 'png' })
+ end
+
+ it 'returns an empty Hash for a defined path without attributes' do
+ expect(subject.attributes('bla/bla.txt')).to eq({})
+ end
+
+ context 'when the "binary" option is set for a path' do
+ it 'returns true for the "binary" option' do
+ expect(subject.attributes('test.binary')['binary']).to eq(true)
+ end
+
+ it 'returns false for the "diff" option' do
+ expect(subject.attributes('test.binary')['diff']).to eq(false)
+ end
+ end
+ end
+
+ context 'using a path without any attributes' do
+ it 'returns an empty Hash' do
+ expect(subject.attributes('test.foo')).to eq({})
+ end
+ end
+ end
+
+ describe '#patterns' do
+ it 'parses a file with entries' do
+ expect(subject.patterns).to be_an_instance_of(Hash)
+ end
+
+ it 'parses an entry that uses a tab to separate the pattern and attributes' do
+ expect(subject.patterns[File.join(path, '*.md')]).
+ to eq({ 'gitlab-language' => 'markdown' })
+ end
+
+ it 'stores patterns in reverse order' do
+ first = subject.patterns.to_a[0]
+
+ expect(first[0]).to eq(File.join(path, 'bla/bla.txt'))
+ end
+
+ # It's a bit hard to test for something _not_ being processed. As such we'll
+ # just test the number of entries.
+ it 'ignores any comments and empty lines' do
+ expect(subject.patterns.length).to eq(10)
+ end
+
+ it 'does not parse anything when the attributes file does not exist' do
+ expect(File).to receive(:exist?).
+ with(File.join(path, 'info/attributes')).
+ and_return(false)
+
+ expect(subject.patterns).to eq({})
+ end
+ end
+
+ describe '#parse_attributes' do
+ it 'parses a boolean attribute' do
+ expect(subject.parse_attributes('text')).to eq({ 'text' => true })
+ end
+
+ it 'parses a negated boolean attribute' do
+ expect(subject.parse_attributes('-text')).to eq({ 'text' => false })
+ end
+
+ it 'parses a key-value pair' do
+ expect(subject.parse_attributes('foo=bar')).to eq({ 'foo' => 'bar' })
+ end
+
+ it 'parses multiple attributes' do
+ input = 'boolean key=value -negated'
+
+ expect(subject.parse_attributes(input)).
+ to eq({ 'boolean' => true, 'key' => 'value', 'negated' => false })
+ end
+
+ it 'parses attributes with query string parameters' do
+ expect(subject.parse_attributes('foo=bar?baz=1')).
+ to eq({ 'foo' => 'bar?baz=1' })
+ end
+ end
+
+ describe '#each_line' do
+ it 'iterates over every line in the attributes file' do
+ args = [String] * 14 # the number of lines in the file
+
+ expect { |b| subject.each_line(&b) }.to yield_successive_args(*args)
+ end
+
+ it 'does not yield when the attributes file does not exist' do
+ expect(File).to receive(:exist?).
+ with(File.join(path, 'info/attributes')).
+ and_return(false)
+
+ expect { |b| subject.each_line(&b) }.not_to yield_control
+ end
+
+ it 'does not yield when the attributes file has an unsupported encoding' do
+ path = File.join(SEED_REPOSITORY_PATH, 'with-invalid-git-attributes.git')
+ attrs = described_class.new(path)
+
+ expect { |b| attrs.each_line(&b) }.not_to yield_control
+ end
+ end
+end
diff --git a/spec/lib/gitlab/git/blame_spec.rb b/spec/lib/gitlab/git/blame_spec.rb
new file mode 100644
index 00000000000..e169f5af6b6
--- /dev/null
+++ b/spec/lib/gitlab/git/blame_spec.rb
@@ -0,0 +1,66 @@
+# coding: utf-8
+require "spec_helper"
+
+describe Gitlab::Git::Blame, seed_helper: true do
+ let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) }
+ let(:blame) do
+ Gitlab::Git::Blame.new(repository, SeedRepo::Commit::ID, "CONTRIBUTING.md")
+ end
+
+ context "each count" do
+ it do
+ data = []
+ blame.each do |commit, line|
+ data << {
+ commit: commit,
+ line: line
+ }
+ end
+
+ expect(data.size).to eq(95)
+ expect(data.first[:commit]).to be_kind_of(Gitlab::Git::Commit)
+ expect(data.first[:line]).to eq("# Contribute to GitLab")
+ end
+ end
+
+ context "ISO-8859 encoding" do
+ let(:blame) do
+ Gitlab::Git::Blame.new(repository, SeedRepo::EncodingCommit::ID, "encoding/iso8859.txt")
+ end
+
+ it 'converts to UTF-8' do
+ data = []
+ blame.each do |commit, line|
+ data << {
+ commit: commit,
+ line: line
+ }
+ end
+
+ expect(data.size).to eq(1)
+ expect(data.first[:commit]).to be_kind_of(Gitlab::Git::Commit)
+ expect(data.first[:line]).to eq("Ä ü")
+ end
+ end
+
+ context "unknown encoding" do
+ let(:blame) do
+ Gitlab::Git::Blame.new(repository, SeedRepo::EncodingCommit::ID, "encoding/iso8859.txt")
+ end
+
+ it 'converts to UTF-8' do
+ expect(CharlockHolmes::EncodingDetector).to receive(:detect).and_return(nil)
+ data = []
+ blame.each do |commit, line|
+ data << {
+ commit: commit,
+ line: line
+ }
+ end
+
+ expect(data.size).to eq(1)
+ expect(data.first[:commit]).to be_kind_of(Gitlab::Git::Commit)
+ expect(data.first[:line]).to eq(" ")
+ end
+ end
+end
diff --git a/spec/lib/gitlab/git/blob_snippet_spec.rb b/spec/lib/gitlab/git/blob_snippet_spec.rb
new file mode 100644
index 00000000000..79b1311ac91
--- /dev/null
+++ b/spec/lib/gitlab/git/blob_snippet_spec.rb
@@ -0,0 +1,19 @@
+# encoding: UTF-8
+
+require "spec_helper"
+
+describe Gitlab::Git::BlobSnippet, seed_helper: true do
+ describe :data do
+ context 'empty lines' do
+ let(:snippet) { Gitlab::Git::BlobSnippet.new('master', nil, nil, nil) }
+
+ it { expect(snippet.data).to be_nil }
+ end
+
+ context 'present lines' do
+ let(:snippet) { Gitlab::Git::BlobSnippet.new('master', ['wow', 'much'], 1, 'wow.rb') }
+
+ it { expect(snippet.data).to eq("wow\nmuch") }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/git/blob_spec.rb b/spec/lib/gitlab/git/blob_spec.rb
new file mode 100644
index 00000000000..84f79ec2391
--- /dev/null
+++ b/spec/lib/gitlab/git/blob_spec.rb
@@ -0,0 +1,489 @@
+# encoding: utf-8
+
+require "spec_helper"
+
+describe Gitlab::Git::Blob, seed_helper: true do
+ let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) }
+
+ describe :initialize do
+ let(:blob) { Gitlab::Git::Blob.new(name: 'test') }
+
+ it 'handles nil data' do
+ expect(blob.name).to eq('test')
+ expect(blob.size).to eq(nil)
+ expect(blob.loaded_size).to eq(nil)
+ end
+ end
+
+ describe :find do
+ context 'file in subdir' do
+ let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, "files/ruby/popen.rb") }
+
+ it { expect(blob.id).to eq(SeedRepo::RubyBlob::ID) }
+ it { expect(blob.name).to eq(SeedRepo::RubyBlob::NAME) }
+ it { expect(blob.path).to eq("files/ruby/popen.rb") }
+ it { expect(blob.commit_id).to eq(SeedRepo::Commit::ID) }
+ it { expect(blob.data[0..10]).to eq(SeedRepo::RubyBlob::CONTENT[0..10]) }
+ it { expect(blob.size).to eq(669) }
+ it { expect(blob.mode).to eq("100644") }
+ end
+
+ context 'file in root' do
+ let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, ".gitignore") }
+
+ it { expect(blob.id).to eq("dfaa3f97ca337e20154a98ac9d0be76ddd1fcc82") }
+ it { expect(blob.name).to eq(".gitignore") }
+ it { expect(blob.path).to eq(".gitignore") }
+ it { expect(blob.commit_id).to eq(SeedRepo::Commit::ID) }
+ it { expect(blob.data[0..10]).to eq("*.rbc\n*.sas") }
+ it { expect(blob.size).to eq(241) }
+ it { expect(blob.mode).to eq("100644") }
+ it { expect(blob).not_to be_binary }
+ end
+
+ context 'file in root with leading slash' do
+ let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, "/.gitignore") }
+
+ it { expect(blob.id).to eq("dfaa3f97ca337e20154a98ac9d0be76ddd1fcc82") }
+ it { expect(blob.name).to eq(".gitignore") }
+ it { expect(blob.path).to eq(".gitignore") }
+ it { expect(blob.commit_id).to eq(SeedRepo::Commit::ID) }
+ it { expect(blob.data[0..10]).to eq("*.rbc\n*.sas") }
+ it { expect(blob.size).to eq(241) }
+ it { expect(blob.mode).to eq("100644") }
+ end
+
+ context 'non-exist file' do
+ let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, "missing.rb") }
+
+ it { expect(blob).to be_nil }
+ end
+
+ context 'six submodule' do
+ let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, 'six') }
+
+ it { expect(blob.id).to eq('409f37c4f05865e4fb208c771485f211a22c4c2d') }
+ it { expect(blob.data).to eq('') }
+
+ it 'does not get messed up by load_all_data!' do
+ blob.load_all_data!(repository)
+ expect(blob.data).to eq('')
+ end
+
+ it 'does not mark the blob as binary' do
+ expect(blob).not_to be_binary
+ end
+ end
+
+ context 'large file' do
+ let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, 'files/images/6049019_460s.jpg') }
+ let(:blob_size) { 111803 }
+
+ it { expect(blob.size).to eq(blob_size) }
+ it { expect(blob.data.length).to eq(blob_size) }
+
+ it 'check that this test is sane' do
+ expect(blob.size).to be <= Gitlab::Git::Blob::MAX_DATA_DISPLAY_SIZE
+ end
+
+ it 'can load all data' do
+ blob.load_all_data!(repository)
+ expect(blob.data.length).to eq(blob_size)
+ end
+
+ it 'marks the blob as binary' do
+ expect(Gitlab::Git::Blob).to receive(:new).
+ with(hash_including(binary: true)).
+ and_call_original
+
+ expect(blob).to be_binary
+ end
+ end
+ end
+
+ describe :raw do
+ let(:raw_blob) { Gitlab::Git::Blob.raw(repository, SeedRepo::RubyBlob::ID) }
+ it { expect(raw_blob.id).to eq(SeedRepo::RubyBlob::ID) }
+ it { expect(raw_blob.data[0..10]).to eq("require \'fi") }
+ it { expect(raw_blob.size).to eq(669) }
+ it { expect(raw_blob.truncated?).to be_falsey }
+
+ context 'large file' do
+ it 'limits the size of a large file' do
+ blob_size = Gitlab::Git::Blob::MAX_DATA_DISPLAY_SIZE + 1
+ buffer = Array.new(blob_size, 0)
+ rugged_blob = Rugged::Blob.from_buffer(repository.rugged, buffer.join(''))
+ blob = Gitlab::Git::Blob.raw(repository, rugged_blob)
+
+ expect(blob.size).to eq(blob_size)
+ expect(blob.loaded_size).to eq(Gitlab::Git::Blob::MAX_DATA_DISPLAY_SIZE)
+ expect(blob.data.length).to eq(Gitlab::Git::Blob::MAX_DATA_DISPLAY_SIZE)
+ expect(blob.truncated?).to be_truthy
+
+ blob.load_all_data!(repository)
+ expect(blob.loaded_size).to eq(blob_size)
+ end
+ end
+ end
+
+ describe 'encoding' do
+ context 'file with russian text' do
+ let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, "encoding/russian.rb") }
+
+ it { expect(blob.name).to eq("russian.rb") }
+ it { expect(blob.data.lines.first).to eq("Хороший файл") }
+ it { expect(blob.size).to eq(23) }
+ it { expect(blob.truncated?).to be_falsey }
+ # Run it twice since data is encoded after the first run
+ it { expect(blob.truncated?).to be_falsey }
+ it { expect(blob.mode).to eq("100755") }
+ end
+
+ context 'file with Chinese text' do
+ let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, "encoding/テスト.txt") }
+
+ it { expect(blob.name).to eq("テスト.txt") }
+ it { expect(blob.data).to include("これはテスト") }
+ it { expect(blob.size).to eq(340) }
+ it { expect(blob.mode).to eq("100755") }
+ it { expect(blob.truncated?).to be_falsey }
+ end
+
+ context 'file with ISO-8859 text' do
+ let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::LastCommit::ID, "encoding/iso8859.txt") }
+
+ it { expect(blob.name).to eq("iso8859.txt") }
+ it { expect(blob.loaded_size).to eq(4) }
+ it { expect(blob.size).to eq(4) }
+ it { expect(blob.mode).to eq("100644") }
+ it { expect(blob.truncated?).to be_falsey }
+ end
+ end
+
+ describe 'mode' do
+ context 'file regular' do
+ let(:blob) do
+ Gitlab::Git::Blob.find(
+ repository,
+ 'fa1b1e6c004a68b7d8763b86455da9e6b23e36d6',
+ 'files/ruby/regex.rb'
+ )
+ end
+
+ it { expect(blob.name).to eq('regex.rb') }
+ it { expect(blob.path).to eq('files/ruby/regex.rb') }
+ it { expect(blob.size).to eq(1200) }
+ it { expect(blob.mode).to eq("100644") }
+ end
+
+ context 'file binary' do
+ let(:blob) do
+ Gitlab::Git::Blob.find(
+ repository,
+ 'fa1b1e6c004a68b7d8763b86455da9e6b23e36d6',
+ 'files/executables/ls'
+ )
+ end
+
+ it { expect(blob.name).to eq('ls') }
+ it { expect(blob.path).to eq('files/executables/ls') }
+ it { expect(blob.size).to eq(110080) }
+ it { expect(blob.mode).to eq("100755") }
+ end
+
+ context 'file symlink to regular' do
+ let(:blob) do
+ Gitlab::Git::Blob.find(
+ repository,
+ 'fa1b1e6c004a68b7d8763b86455da9e6b23e36d6',
+ 'files/links/ruby-style-guide.md'
+ )
+ end
+
+ it { expect(blob.name).to eq('ruby-style-guide.md') }
+ it { expect(blob.path).to eq('files/links/ruby-style-guide.md') }
+ it { expect(blob.size).to eq(31) }
+ it { expect(blob.mode).to eq("120000") }
+ end
+
+ context 'file symlink to binary' do
+ let(:blob) do
+ Gitlab::Git::Blob.find(
+ repository,
+ 'fa1b1e6c004a68b7d8763b86455da9e6b23e36d6',
+ 'files/links/touch'
+ )
+ end
+
+ it { expect(blob.name).to eq('touch') }
+ it { expect(blob.path).to eq('files/links/touch') }
+ it { expect(blob.size).to eq(20) }
+ it { expect(blob.mode).to eq("120000") }
+ end
+ end
+
+ describe :commit do
+ let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) }
+
+ let(:commit_options) do
+ {
+ file: {
+ content: 'Lorem ipsum...',
+ path: 'documents/story.txt'
+ },
+ author: {
+ email: 'user@example.com',
+ name: 'Test User',
+ time: Time.now
+ },
+ committer: {
+ email: 'user@example.com',
+ name: 'Test User',
+ time: Time.now
+ },
+ commit: {
+ message: 'Wow such commit',
+ branch: 'fix-mode'
+ }
+ }
+ end
+
+ let(:commit_sha) { Gitlab::Git::Blob.commit(repository, commit_options) }
+ let(:commit) { repository.lookup(commit_sha) }
+
+ it 'should add file with commit' do
+ # Commit message valid
+ expect(commit.message).to eq('Wow such commit')
+
+ tree = commit.tree.to_a.find { |tree| tree[:name] == 'documents' }
+
+ # Directory was created
+ expect(tree[:type]).to eq(:tree)
+
+ # File was created
+ expect(repository.lookup(tree[:oid]).first[:name]).to eq('story.txt')
+ end
+
+ describe "ref updating" do
+ it 'creates a commit but does not udate a ref' do
+ commit_opts = commit_options.tap{ |opts| opts[:commit][:update_ref] = false}
+ commit_sha = Gitlab::Git::Blob.commit(repository, commit_opts)
+ commit = repository.lookup(commit_sha)
+
+ # Commit message valid
+ expect(commit.message).to eq('Wow such commit')
+
+ # Does not update any related ref
+ expect(repository.lookup("fix-mode").oid).not_to eq(commit.oid)
+ expect(repository.lookup("HEAD").oid).not_to eq(commit.oid)
+ end
+ end
+
+ describe 'reject updates' do
+ it 'should reject updates' do
+ commit_options[:file][:update] = false
+ commit_options[:file][:path] = 'files/executables/ls'
+
+ expect{ commit_sha }.to raise_error('Filename already exists; update not allowed')
+ end
+ end
+
+ describe 'file modes' do
+ it 'should preserve file modes with commit' do
+ commit_options[:file][:path] = 'files/executables/ls'
+
+ entry = Gitlab::Git::Blob::find_entry_by_path(repository, commit.tree.oid, commit_options[:file][:path])
+ expect(entry[:filemode]).to eq(0100755)
+ end
+ end
+ end
+
+ describe :rename do
+ let(:repository) { Gitlab::Git::Repository.new(TEST_NORMAL_REPO_PATH) }
+ let(:rename_options) do
+ {
+ file: {
+ path: 'NEWCONTRIBUTING.md',
+ previous_path: 'CONTRIBUTING.md',
+ content: 'Lorem ipsum...',
+ update: true
+ },
+ author: {
+ email: 'user@example.com',
+ name: 'Test User',
+ time: Time.now
+ },
+ committer: {
+ email: 'user@example.com',
+ name: 'Test User',
+ time: Time.now
+ },
+ commit: {
+ message: 'Rename readme',
+ branch: 'master'
+ }
+ }
+ end
+
+ let(:rename_options2) do
+ {
+ file: {
+ content: 'Lorem ipsum...',
+ path: 'bin/new_executable',
+ previous_path: 'bin/executable',
+ },
+ author: {
+ email: 'user@example.com',
+ name: 'Test User',
+ time: Time.now
+ },
+ committer: {
+ email: 'user@example.com',
+ name: 'Test User',
+ time: Time.now
+ },
+ commit: {
+ message: 'Updates toberenamed.txt',
+ branch: 'master',
+ update_ref: false
+ }
+ }
+ end
+
+ it 'maintains file permissions when renaming' do
+ mode = 0o100755
+
+ Gitlab::Git::Blob.rename(repository, rename_options2)
+
+ expect(repository.rugged.index.get(rename_options2[:file][:path])[:mode]).to eq(mode)
+ end
+
+ it 'renames the file with commit and not change file permissions' do
+ ref = rename_options[:commit][:branch]
+
+ expect(repository.rugged.index.get('CONTRIBUTING.md')).not_to be_nil
+ expect { Gitlab::Git::Blob.rename(repository, rename_options) }.to change { repository.commit_count(ref) }.by(1)
+
+ expect(repository.rugged.index.get('CONTRIBUTING.md')).to be_nil
+ expect(repository.rugged.index.get('NEWCONTRIBUTING.md')).not_to be_nil
+ end
+ end
+
+ describe :remove do
+ let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) }
+
+ let(:commit_options) do
+ {
+ file: {
+ path: 'README.md'
+ },
+ author: {
+ email: 'user@example.com',
+ name: 'Test User',
+ time: Time.now
+ },
+ committer: {
+ email: 'user@example.com',
+ name: 'Test User',
+ time: Time.now
+ },
+ commit: {
+ message: 'Remove readme',
+ branch: 'feature'
+ }
+ }
+ end
+
+ let(:commit_sha) { Gitlab::Git::Blob.remove(repository, commit_options) }
+ let(:commit) { repository.lookup(commit_sha) }
+ let(:blob) { Gitlab::Git::Blob.find(repository, commit_sha, "README.md") }
+
+ it 'should remove file with commit' do
+ # Commit message valid
+ expect(commit.message).to eq('Remove readme')
+
+ # File was removed
+ expect(blob).to be_nil
+ end
+ end
+
+ describe :lfs_pointers do
+ context 'file a valid lfs pointer' do
+ let(:blob) do
+ Gitlab::Git::Blob.find(
+ repository,
+ '33bcff41c232a11727ac6d660bd4b0c2ba86d63d',
+ 'files/lfs/image.jpg'
+ )
+ end
+
+ it { expect(blob.lfs_pointer?).to eq(true) }
+ it { expect(blob.lfs_oid).to eq("4206f951d2691c78aac4c0ce9f2b23580b2c92cdcc4336e1028742c0274938e0") }
+ it { expect(blob.lfs_size).to eq("19548") }
+ it { expect(blob.id).to eq("f4d76af13003d1106be7ac8c5a2a3d37ddf32c2a") }
+ it { expect(blob.name).to eq("image.jpg") }
+ it { expect(blob.path).to eq("files/lfs/image.jpg") }
+ it { expect(blob.size).to eq(130) }
+ it { expect(blob.mode).to eq("100644") }
+ end
+
+ describe 'file an invalid lfs pointer' do
+ context 'with correct version header but incorrect size and oid' do
+ let(:blob) do
+ Gitlab::Git::Blob.find(
+ repository,
+ '33bcff41c232a11727ac6d660bd4b0c2ba86d63d',
+ 'files/lfs/archive-invalid.tar'
+ )
+ end
+
+ it { expect(blob.lfs_pointer?).to eq(false) }
+ it { expect(blob.lfs_oid).to eq(nil) }
+ it { expect(blob.lfs_size).to eq(nil) }
+ it { expect(blob.id).to eq("f8a898db217a5a85ed8b3d25b34c1df1d1094c46") }
+ it { expect(blob.name).to eq("archive-invalid.tar") }
+ it { expect(blob.path).to eq("files/lfs/archive-invalid.tar") }
+ it { expect(blob.size).to eq(43) }
+ it { expect(blob.mode).to eq("100644") }
+ end
+
+ context 'with correct version header and size but incorrect size and oid' do
+ let(:blob) do
+ Gitlab::Git::Blob.find(
+ repository,
+ '33bcff41c232a11727ac6d660bd4b0c2ba86d63d',
+ 'files/lfs/picture-invalid.png'
+ )
+ end
+
+ it { expect(blob.lfs_pointer?).to eq(false) }
+ it { expect(blob.lfs_oid).to eq(nil) }
+ it { expect(blob.lfs_size).to eq("1575078") }
+ it { expect(blob.id).to eq("5ae35296e1f95c1ef9feda1241477ed29a448572") }
+ it { expect(blob.name).to eq("picture-invalid.png") }
+ it { expect(blob.path).to eq("files/lfs/picture-invalid.png") }
+ it { expect(blob.size).to eq(57) }
+ it { expect(blob.mode).to eq("100644") }
+ end
+
+ context 'with correct version header and size but invalid size and oid' do
+ let(:blob) do
+ Gitlab::Git::Blob.find(
+ repository,
+ '33bcff41c232a11727ac6d660bd4b0c2ba86d63d',
+ 'files/lfs/file-invalid.zip'
+ )
+ end
+
+ it { expect(blob.lfs_pointer?).to eq(false) }
+ it { expect(blob.lfs_oid).to eq(nil) }
+ it { expect(blob.lfs_size).to eq(nil) }
+ it { expect(blob.id).to eq("d831981bd876732b85a1bcc6cc01210c9f36248f") }
+ it { expect(blob.name).to eq("file-invalid.zip") }
+ it { expect(blob.path).to eq("files/lfs/file-invalid.zip") }
+ it { expect(blob.size).to eq(60) }
+ it { expect(blob.mode).to eq("100644") }
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/git/branch_spec.rb b/spec/lib/gitlab/git/branch_spec.rb
new file mode 100644
index 00000000000..78234b396c5
--- /dev/null
+++ b/spec/lib/gitlab/git/branch_spec.rb
@@ -0,0 +1,31 @@
+require "spec_helper"
+
+describe Gitlab::Git::Branch, seed_helper: true do
+ let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) }
+
+ subject { repository.branches }
+
+ it { is_expected.to be_kind_of Array }
+
+ describe '#size' do
+ subject { super().size }
+ it { is_expected.to eq(SeedRepo::Repo::BRANCHES.size) }
+ end
+
+ describe 'first branch' do
+ let(:branch) { repository.branches.first }
+
+ it { expect(branch.name).to eq(SeedRepo::Repo::BRANCHES.first) }
+ it { expect(branch.dereferenced_target.sha).to eq("0b4bc9a49b562e85de7cc9e834518ea6828729b9") }
+ end
+
+ describe 'master branch' do
+ let(:branch) do
+ repository.branches.find { |branch| branch.name == 'master' }
+ end
+
+ it { expect(branch.dereferenced_target.sha).to eq(SeedRepo::LastCommit::ID) }
+ end
+
+ it { expect(repository.branches.size).to eq(SeedRepo::Repo::BRANCHES.size) }
+end
diff --git a/spec/lib/gitlab/git/commit_spec.rb b/spec/lib/gitlab/git/commit_spec.rb
new file mode 100644
index 00000000000..e1be6784c20
--- /dev/null
+++ b/spec/lib/gitlab/git/commit_spec.rb
@@ -0,0 +1,408 @@
+require "spec_helper"
+
+describe Gitlab::Git::Commit, seed_helper: true do
+ let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) }
+ let(:commit) { Gitlab::Git::Commit.find(repository, SeedRepo::Commit::ID) }
+ let(:rugged_commit) do
+ repository.rugged.lookup(SeedRepo::Commit::ID)
+ end
+
+ describe "Commit info" do
+ before do
+ repo = Gitlab::Git::Repository.new(TEST_REPO_PATH).rugged
+
+ @committer = {
+ email: 'mike@smith.com',
+ name: "Mike Smith",
+ time: Time.now
+ }
+
+ @author = {
+ email: 'john@smith.com',
+ name: "John Smith",
+ time: Time.now
+ }
+
+ @parents = [repo.head.target]
+ @gitlab_parents = @parents.map { |c| Gitlab::Git::Commit.decorate(c) }
+ @tree = @parents.first.tree
+
+ sha = Rugged::Commit.create(
+ repo,
+ author: @author,
+ committer: @committer,
+ tree: @tree,
+ parents: @parents,
+ message: "Refactoring specs",
+ update_ref: "HEAD"
+ )
+
+ @raw_commit = repo.lookup(sha)
+ @commit = Gitlab::Git::Commit.new(@raw_commit)
+ end
+
+ it { expect(@commit.short_id).to eq(@raw_commit.oid[0..10]) }
+ it { expect(@commit.id).to eq(@raw_commit.oid) }
+ it { expect(@commit.sha).to eq(@raw_commit.oid) }
+ it { expect(@commit.safe_message).to eq(@raw_commit.message) }
+ it { expect(@commit.created_at).to eq(@raw_commit.author[:time]) }
+ it { expect(@commit.date).to eq(@raw_commit.committer[:time]) }
+ it { expect(@commit.author_email).to eq(@author[:email]) }
+ it { expect(@commit.author_name).to eq(@author[:name]) }
+ it { expect(@commit.committer_name).to eq(@committer[:name]) }
+ it { expect(@commit.committer_email).to eq(@committer[:email]) }
+ it { expect(@commit.different_committer?).to be_truthy }
+ it { expect(@commit.parents).to eq(@gitlab_parents) }
+ it { expect(@commit.parent_id).to eq(@parents.first.oid) }
+ it { expect(@commit.no_commit_message).to eq("--no commit message") }
+ it { expect(@commit.tree).to eq(@tree) }
+
+ after do
+ # Erase the new commit so other tests get the original repo
+ repo = Gitlab::Git::Repository.new(TEST_REPO_PATH).rugged
+ repo.references.update("refs/heads/master", SeedRepo::LastCommit::ID)
+ end
+ end
+
+ context 'Class methods' do
+ describe :find do
+ it "should return first head commit if without params" do
+ expect(Gitlab::Git::Commit.last(repository).id).to eq(
+ repository.raw.head.target.oid
+ )
+ end
+
+ it "should return valid commit" do
+ expect(Gitlab::Git::Commit.find(repository, SeedRepo::Commit::ID)).to be_valid_commit
+ end
+
+ it "should return valid commit for tag" do
+ expect(Gitlab::Git::Commit.find(repository, 'v1.0.0').id).to eq('6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9')
+ end
+
+ it "should return nil for non-commit ids" do
+ blob = Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, "files/ruby/popen.rb")
+ expect(Gitlab::Git::Commit.find(repository, blob.id)).to be_nil
+ end
+
+ it "should return nil for parent of non-commit object" do
+ blob = Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, "files/ruby/popen.rb")
+ expect(Gitlab::Git::Commit.find(repository, "#{blob.id}^")).to be_nil
+ end
+
+ it "should return nil for nonexisting ids" do
+ expect(Gitlab::Git::Commit.find(repository, "+123_4532530XYZ")).to be_nil
+ end
+
+ context 'with broken repo' do
+ let(:repository) { Gitlab::Git::Repository.new(TEST_BROKEN_REPO_PATH) }
+
+ it 'returns nil' do
+ expect(Gitlab::Git::Commit.find(repository, SeedRepo::Commit::ID)).to be_nil
+ end
+ end
+ end
+
+ describe :last_for_path do
+ context 'no path' do
+ subject { Gitlab::Git::Commit.last_for_path(repository, 'master') }
+
+ describe '#id' do
+ subject { super().id }
+ it { is_expected.to eq(SeedRepo::LastCommit::ID) }
+ end
+ end
+
+ context 'path' do
+ subject { Gitlab::Git::Commit.last_for_path(repository, 'master', 'files/ruby') }
+
+ describe '#id' do
+ subject { super().id }
+ it { is_expected.to eq(SeedRepo::Commit::ID) }
+ end
+ end
+
+ context 'ref + path' do
+ subject { Gitlab::Git::Commit.last_for_path(repository, SeedRepo::Commit::ID, 'encoding') }
+
+ describe '#id' do
+ subject { super().id }
+ it { is_expected.to eq(SeedRepo::BigCommit::ID) }
+ end
+ end
+ end
+
+ describe "where" do
+ context 'path is empty string' do
+ subject do
+ commits = Gitlab::Git::Commit.where(
+ repo: repository,
+ ref: 'master',
+ path: '',
+ limit: 10
+ )
+
+ commits.map { |c| c.id }
+ end
+
+ it 'has 10 elements' do
+ expect(subject.size).to eq(10)
+ end
+ it { is_expected.to include(SeedRepo::EmptyCommit::ID) }
+ end
+
+ context 'path is nil' do
+ subject do
+ commits = Gitlab::Git::Commit.where(
+ repo: repository,
+ ref: 'master',
+ path: nil,
+ limit: 10
+ )
+
+ commits.map { |c| c.id }
+ end
+
+ it 'has 10 elements' do
+ expect(subject.size).to eq(10)
+ end
+ it { is_expected.to include(SeedRepo::EmptyCommit::ID) }
+ end
+
+ context 'ref is branch name' do
+ subject do
+ commits = Gitlab::Git::Commit.where(
+ repo: repository,
+ ref: 'master',
+ path: 'files',
+ limit: 3,
+ offset: 1
+ )
+
+ commits.map { |c| c.id }
+ end
+
+ it 'has 3 elements' do
+ expect(subject.size).to eq(3)
+ end
+ it { is_expected.to include("d14d6c0abdd253381df51a723d58691b2ee1ab08") }
+ it { is_expected.not_to include("eb49186cfa5c4338011f5f590fac11bd66c5c631") }
+ end
+
+ context 'ref is commit id' do
+ subject do
+ commits = Gitlab::Git::Commit.where(
+ repo: repository,
+ ref: "874797c3a73b60d2187ed6e2fcabd289ff75171e",
+ path: 'files',
+ limit: 3,
+ offset: 1
+ )
+
+ commits.map { |c| c.id }
+ end
+
+ it 'has 3 elements' do
+ expect(subject.size).to eq(3)
+ end
+ it { is_expected.to include("2f63565e7aac07bcdadb654e253078b727143ec4") }
+ it { is_expected.not_to include(SeedRepo::Commit::ID) }
+ end
+
+ context 'ref is tag' do
+ subject do
+ commits = Gitlab::Git::Commit.where(
+ repo: repository,
+ ref: 'v1.0.0',
+ path: 'files',
+ limit: 3,
+ offset: 1
+ )
+
+ commits.map { |c| c.id }
+ end
+
+ it 'has 3 elements' do
+ expect(subject.size).to eq(3)
+ end
+ it { is_expected.to include("874797c3a73b60d2187ed6e2fcabd289ff75171e") }
+ it { is_expected.not_to include(SeedRepo::Commit::ID) }
+ end
+ end
+
+ describe :between do
+ subject do
+ commits = Gitlab::Git::Commit.between(repository, SeedRepo::Commit::PARENT_ID, SeedRepo::Commit::ID)
+ commits.map { |c| c.id }
+ end
+
+ it 'has 1 element' do
+ expect(subject.size).to eq(1)
+ end
+ it { is_expected.to include(SeedRepo::Commit::ID) }
+ it { is_expected.not_to include(SeedRepo::FirstCommit::ID) }
+ end
+
+ describe :find_all do
+ context 'max_count' do
+ subject do
+ commits = Gitlab::Git::Commit.find_all(
+ repository,
+ max_count: 50
+ )
+
+ commits.map { |c| c.id }
+ end
+
+ it 'has 31 elements' do
+ expect(subject.size).to eq(33)
+ end
+ it { is_expected.to include(SeedRepo::Commit::ID) }
+ it { is_expected.to include(SeedRepo::Commit::PARENT_ID) }
+ it { is_expected.to include(SeedRepo::FirstCommit::ID) }
+ end
+
+ context 'ref + max_count + skip' do
+ subject do
+ commits = Gitlab::Git::Commit.find_all(
+ repository,
+ ref: 'master',
+ max_count: 50,
+ skip: 1
+ )
+
+ commits.map { |c| c.id }
+ end
+
+ it 'has 23 elements' do
+ expect(subject.size).to eq(24)
+ end
+ it { is_expected.to include(SeedRepo::Commit::ID) }
+ it { is_expected.to include(SeedRepo::FirstCommit::ID) }
+ it { is_expected.not_to include(SeedRepo::LastCommit::ID) }
+ end
+
+ context 'contains feature + max_count' do
+ subject do
+ commits = Gitlab::Git::Commit.find_all(
+ repository,
+ contains: 'feature',
+ max_count: 7
+ )
+
+ commits.map { |c| c.id }
+ end
+
+ it 'has 7 elements' do
+ expect(subject.size).to eq(7)
+ end
+
+ it { is_expected.not_to include(SeedRepo::Commit::PARENT_ID) }
+ it { is_expected.not_to include(SeedRepo::Commit::ID) }
+ it { is_expected.to include(SeedRepo::BigCommit::ID) }
+ end
+ end
+ end
+
+ describe :init_from_rugged do
+ let(:gitlab_commit) { Gitlab::Git::Commit.new(rugged_commit) }
+ subject { gitlab_commit }
+
+ describe '#id' do
+ subject { super().id }
+ it { is_expected.to eq(SeedRepo::Commit::ID) }
+ end
+ end
+
+ describe :init_from_hash do
+ let(:commit) { Gitlab::Git::Commit.new(sample_commit_hash) }
+ subject { commit }
+
+ describe '#id' do
+ subject { super().id }
+ it { is_expected.to eq(sample_commit_hash[:id])}
+ end
+
+ describe '#message' do
+ subject { super().message }
+ it { is_expected.to eq(sample_commit_hash[:message])}
+ end
+ end
+
+ describe :stats do
+ subject { commit.stats }
+
+ describe '#additions' do
+ subject { super().additions }
+ it { is_expected.to eq(11) }
+ end
+
+ describe '#deletions' do
+ subject { super().deletions }
+ it { is_expected.to eq(6) }
+ end
+ end
+
+ describe :to_diff do
+ subject { commit.to_diff }
+
+ it { is_expected.not_to include "From #{SeedRepo::Commit::ID}" }
+ it { is_expected.to include 'diff --git a/files/ruby/popen.rb b/files/ruby/popen.rb'}
+ end
+
+ describe :has_zero_stats? do
+ it { expect(commit.has_zero_stats?).to eq(false) }
+ end
+
+ describe :to_patch do
+ subject { commit.to_patch }
+
+ it { is_expected.to include "From #{SeedRepo::Commit::ID}" }
+ it { is_expected.to include 'diff --git a/files/ruby/popen.rb b/files/ruby/popen.rb'}
+ end
+
+ describe :to_hash do
+ let(:hash) { commit.to_hash }
+ subject { hash }
+
+ it { is_expected.to be_kind_of Hash }
+
+ describe '#keys' do
+ subject { super().keys.sort }
+ it { is_expected.to match(sample_commit_hash.keys.sort) }
+ end
+ end
+
+ describe :diffs do
+ subject { commit.diffs }
+
+ it { is_expected.to be_kind_of Gitlab::Git::DiffCollection }
+ it { expect(subject.count).to eq(2) }
+ it { expect(subject.first).to be_kind_of Gitlab::Git::Diff }
+ end
+
+ describe :ref_names do
+ let(:commit) { Gitlab::Git::Commit.find(repository, 'master') }
+ subject { commit.ref_names(repository) }
+
+ it 'has 1 element' do
+ expect(subject.size).to eq(1)
+ end
+ it { is_expected.to include("master") }
+ it { is_expected.not_to include("feature") }
+ end
+
+ def sample_commit_hash
+ {
+ author_email: "dmitriy.zaporozhets@gmail.com",
+ author_name: "Dmitriy Zaporozhets",
+ authored_date: "2012-02-27 20:51:12 +0200",
+ committed_date: "2012-02-27 20:51:12 +0200",
+ committer_email: "dmitriy.zaporozhets@gmail.com",
+ committer_name: "Dmitriy Zaporozhets",
+ id: SeedRepo::Commit::ID,
+ message: "tree css fixes",
+ parent_ids: ["874797c3a73b60d2187ed6e2fcabd289ff75171e"]
+ }
+ end
+end
diff --git a/spec/lib/gitlab/git/compare_spec.rb b/spec/lib/gitlab/git/compare_spec.rb
new file mode 100644
index 00000000000..f66b68e4218
--- /dev/null
+++ b/spec/lib/gitlab/git/compare_spec.rb
@@ -0,0 +1,109 @@
+require "spec_helper"
+
+describe Gitlab::Git::Compare, seed_helper: true do
+ let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) }
+ let(:compare) { Gitlab::Git::Compare.new(repository, SeedRepo::BigCommit::ID, SeedRepo::Commit::ID, false) }
+ let(:compare_straight) { Gitlab::Git::Compare.new(repository, SeedRepo::BigCommit::ID, SeedRepo::Commit::ID, true) }
+
+ describe :commits do
+ subject do
+ compare.commits.map(&:id)
+ end
+
+ it 'has 8 elements' do
+ expect(subject.size).to eq(8)
+ end
+
+ it { is_expected.to include(SeedRepo::Commit::PARENT_ID) }
+ it { is_expected.not_to include(SeedRepo::BigCommit::PARENT_ID) }
+
+ context 'non-existing base ref' do
+ let(:compare) { Gitlab::Git::Compare.new(repository, 'no-such-branch', SeedRepo::Commit::ID) }
+
+ it { is_expected.to be_empty }
+ end
+
+ context 'non-existing head ref' do
+ let(:compare) { Gitlab::Git::Compare.new(repository, SeedRepo::BigCommit::ID, '1234567890') }
+
+ it { is_expected.to be_empty }
+ end
+
+ context 'base ref is equal to head ref' do
+ let(:compare) { Gitlab::Git::Compare.new(repository, SeedRepo::BigCommit::ID, SeedRepo::BigCommit::ID) }
+
+ it { is_expected.to be_empty }
+ end
+
+ context 'providing nil as base ref or head ref' do
+ let(:compare) { Gitlab::Git::Compare.new(repository, nil, nil) }
+
+ it { is_expected.to be_empty }
+ end
+ end
+
+ describe :diffs do
+ subject do
+ compare.diffs.map(&:new_path)
+ end
+
+ it 'has 10 elements' do
+ expect(subject.size).to eq(10)
+ end
+
+ it { is_expected.to include('files/ruby/popen.rb') }
+ it { is_expected.not_to include('LICENSE') }
+
+ context 'non-existing base ref' do
+ let(:compare) { Gitlab::Git::Compare.new(repository, 'no-such-branch', SeedRepo::Commit::ID) }
+
+ it { is_expected.to be_empty }
+ end
+
+ context 'non-existing head ref' do
+ let(:compare) { Gitlab::Git::Compare.new(repository, SeedRepo::BigCommit::ID, '1234567890') }
+
+ it { is_expected.to be_empty }
+ end
+ end
+
+ describe :same do
+ subject do
+ compare.same
+ end
+
+ it { is_expected.to eq(false) }
+
+ context 'base ref is equal to head ref' do
+ let(:compare) { Gitlab::Git::Compare.new(repository, SeedRepo::BigCommit::ID, SeedRepo::BigCommit::ID) }
+
+ it { is_expected.to eq(true) }
+ end
+ end
+
+ describe :commits_straight do
+ subject do
+ compare_straight.commits.map(&:id)
+ end
+
+ it 'has 8 elements' do
+ expect(subject.size).to eq(8)
+ end
+
+ it { is_expected.to include(SeedRepo::Commit::PARENT_ID) }
+ it { is_expected.not_to include(SeedRepo::BigCommit::PARENT_ID) }
+ end
+
+ describe :diffs_straight do
+ subject do
+ compare_straight.diffs.map(&:new_path)
+ end
+
+ it 'has 10 elements' do
+ expect(subject.size).to eq(10)
+ end
+
+ it { is_expected.to include('files/ruby/popen.rb') }
+ it { is_expected.not_to include('LICENSE') }
+ end
+end
diff --git a/spec/lib/gitlab/git/diff_collection_spec.rb b/spec/lib/gitlab/git/diff_collection_spec.rb
new file mode 100644
index 00000000000..4fa72c565ae
--- /dev/null
+++ b/spec/lib/gitlab/git/diff_collection_spec.rb
@@ -0,0 +1,460 @@
+require 'spec_helper'
+
+describe Gitlab::Git::DiffCollection, seed_helper: true do
+ subject do
+ Gitlab::Git::DiffCollection.new(
+ iterator,
+ max_files: max_files,
+ max_lines: max_lines,
+ all_diffs: all_diffs,
+ no_collapse: no_collapse
+ )
+ end
+ let(:iterator) { Array.new(file_count, fake_diff(line_length, line_count)) }
+ let(:file_count) { 0 }
+ let(:line_length) { 1 }
+ let(:line_count) { 1 }
+ let(:max_files) { 10 }
+ let(:max_lines) { 100 }
+ let(:all_diffs) { false }
+ let(:no_collapse) { true }
+
+ describe '#to_a' do
+ subject { super().to_a }
+ it { is_expected.to be_kind_of ::Array }
+ end
+
+ describe :decorate! do
+ let(:file_count) { 3 }
+
+ it 'modifies the array in place' do
+ count = 0
+ subject.decorate! { |d| !d.nil? && count += 1 }
+ expect(subject.to_a).to eq([1, 2, 3])
+ expect(count).to eq(3)
+ end
+
+ it 'avoids future iterator iterations' do
+ subject.decorate! { |d| d unless d.nil? }
+
+ expect(iterator).not_to receive(:each)
+
+ subject.overflow?
+ end
+ end
+
+ context 'overflow handling' do
+ context 'adding few enough files' do
+ let(:file_count) { 3 }
+
+ context 'and few enough lines' do
+ let(:line_count) { 10 }
+
+ describe '#overflow?' do
+ subject { super().overflow? }
+ it { is_expected.to be_falsey }
+ end
+
+ describe '#empty?' do
+ subject { super().empty? }
+ it { is_expected.to be_falsey }
+ end
+
+ describe '#real_size' do
+ subject { super().real_size }
+ it { is_expected.to eq('3') }
+ end
+ it { expect(subject.size).to eq(3) }
+
+ context 'when limiting is disabled' do
+ let(:all_diffs) { true }
+
+ describe '#overflow?' do
+ subject { super().overflow? }
+ it { is_expected.to be_falsey }
+ end
+
+ describe '#empty?' do
+ subject { super().empty? }
+ it { is_expected.to be_falsey }
+ end
+
+ describe '#real_size' do
+ subject { super().real_size }
+ it { is_expected.to eq('3') }
+ end
+ it { expect(subject.size).to eq(3) }
+ end
+ end
+
+ context 'and too many lines' do
+ let(:line_count) { 1000 }
+
+ describe '#overflow?' do
+ subject { super().overflow? }
+ it { is_expected.to be_truthy }
+ end
+
+ describe '#empty?' do
+ subject { super().empty? }
+ it { is_expected.to be_falsey }
+ end
+
+ describe '#real_size' do
+ subject { super().real_size }
+ it { is_expected.to eq('0+') }
+ end
+ it { expect(subject.size).to eq(0) }
+
+ context 'when limiting is disabled' do
+ let(:all_diffs) { true }
+
+ describe '#overflow?' do
+ subject { super().overflow? }
+ it { is_expected.to be_falsey }
+ end
+
+ describe '#empty?' do
+ subject { super().empty? }
+ it { is_expected.to be_falsey }
+ end
+
+ describe '#real_size' do
+ subject { super().real_size }
+ it { is_expected.to eq('3') }
+ end
+ it { expect(subject.size).to eq(3) }
+ end
+ end
+ end
+
+ context 'adding too many files' do
+ let(:file_count) { 11 }
+
+ context 'and few enough lines' do
+ let(:line_count) { 1 }
+
+ describe '#overflow?' do
+ subject { super().overflow? }
+ it { is_expected.to be_truthy }
+ end
+
+ describe '#empty?' do
+ subject { super().empty? }
+ it { is_expected.to be_falsey }
+ end
+
+ describe '#real_size' do
+ subject { super().real_size }
+ it { is_expected.to eq('10+') }
+ end
+ it { expect(subject.size).to eq(10) }
+
+ context 'when limiting is disabled' do
+ let(:all_diffs) { true }
+
+ describe '#overflow?' do
+ subject { super().overflow? }
+ it { is_expected.to be_falsey }
+ end
+
+ describe '#empty?' do
+ subject { super().empty? }
+ it { is_expected.to be_falsey }
+ end
+
+ describe '#real_size' do
+ subject { super().real_size }
+ it { is_expected.to eq('11') }
+ end
+ it { expect(subject.size).to eq(11) }
+ end
+ end
+
+ context 'and too many lines' do
+ let(:line_count) { 30 }
+
+ describe '#overflow?' do
+ subject { super().overflow? }
+ it { is_expected.to be_truthy }
+ end
+
+ describe '#empty?' do
+ subject { super().empty? }
+ it { is_expected.to be_falsey }
+ end
+
+ describe '#real_size' do
+ subject { super().real_size }
+ it { is_expected.to eq('3+') }
+ end
+ it { expect(subject.size).to eq(3) }
+
+ context 'when limiting is disabled' do
+ let(:all_diffs) { true }
+
+ describe '#overflow?' do
+ subject { super().overflow? }
+ it { is_expected.to be_falsey }
+ end
+
+ describe '#empty?' do
+ subject { super().empty? }
+ it { is_expected.to be_falsey }
+ end
+
+ describe '#real_size' do
+ subject { super().real_size }
+ it { is_expected.to eq('11') }
+ end
+ it { expect(subject.size).to eq(11) }
+ end
+ end
+ end
+
+ context 'adding exactly the maximum number of files' do
+ let(:file_count) { 10 }
+
+ context 'and few enough lines' do
+ let(:line_count) { 1 }
+
+ describe '#overflow?' do
+ subject { super().overflow? }
+ it { is_expected.to be_falsey }
+ end
+
+ describe '#empty?' do
+ subject { super().empty? }
+ it { is_expected.to be_falsey }
+ end
+
+ describe '#real_size' do
+ subject { super().real_size }
+ it { is_expected.to eq('10') }
+ end
+ it { expect(subject.size).to eq(10) }
+ end
+ end
+
+ context 'adding too many bytes' do
+ let(:file_count) { 10 }
+ let(:line_length) { 5200 }
+
+ describe '#overflow?' do
+ subject { super().overflow? }
+ it { is_expected.to be_truthy }
+ end
+
+ describe '#empty?' do
+ subject { super().empty? }
+ it { is_expected.to be_falsey }
+ end
+
+ describe '#real_size' do
+ subject { super().real_size }
+ it { is_expected.to eq('9+') }
+ end
+ it { expect(subject.size).to eq(9) }
+
+ context 'when limiting is disabled' do
+ let(:all_diffs) { true }
+
+ describe '#overflow?' do
+ subject { super().overflow? }
+ it { is_expected.to be_falsey }
+ end
+
+ describe '#empty?' do
+ subject { super().empty? }
+ it { is_expected.to be_falsey }
+ end
+
+ describe '#real_size' do
+ subject { super().real_size }
+ it { is_expected.to eq('10') }
+ end
+ it { expect(subject.size).to eq(10) }
+ end
+ end
+ end
+
+ describe 'empty collection' do
+ subject { Gitlab::Git::DiffCollection.new([]) }
+
+ describe '#overflow?' do
+ subject { super().overflow? }
+ it { is_expected.to be_falsey }
+ end
+
+ describe '#empty?' do
+ subject { super().empty? }
+ it { is_expected.to be_truthy }
+ end
+
+ describe '#size' do
+ subject { super().size }
+ it { is_expected.to eq(0) }
+ end
+
+ describe '#real_size' do
+ subject { super().real_size }
+ it { is_expected.to eq('0')}
+ end
+ end
+
+ describe :each do
+ context 'when diff are too large' do
+ let(:collection) do
+ Gitlab::Git::DiffCollection.new([{ diff: 'a' * 204800 }])
+ end
+
+ it 'yields Diff instances even when they are too large' do
+ expect { |b| collection.each(&b) }.
+ to yield_with_args(an_instance_of(Gitlab::Git::Diff))
+ end
+
+ it 'prunes diffs that are too large' do
+ diff = nil
+
+ collection.each do |d|
+ diff = d
+ end
+
+ expect(diff.diff).to eq('')
+ end
+ end
+
+ context 'when diff is quite large will collapse by default' do
+ let(:iterator) { [{ diff: 'a' * 20480 }] }
+
+ context 'when no collapse is set' do
+ let(:no_collapse) { true }
+
+ it 'yields Diff instances even when they are quite big' do
+ expect { |b| subject.each(&b) }.
+ to yield_with_args(an_instance_of(Gitlab::Git::Diff))
+ end
+
+ it 'does not prune diffs' do
+ diff = nil
+
+ subject.each do |d|
+ diff = d
+ end
+
+ expect(diff.diff).not_to eq('')
+ end
+ end
+
+ context 'when no collapse is unset' do
+ let(:no_collapse) { false }
+
+ it 'yields Diff instances even when they are quite big' do
+ expect { |b| subject.each(&b) }.
+ to yield_with_args(an_instance_of(Gitlab::Git::Diff))
+ end
+
+ it 'prunes diffs that are quite big' do
+ diff = nil
+
+ subject.each do |d|
+ diff = d
+ end
+
+ expect(diff.diff).to eq('')
+ end
+
+ context 'when go over safe limits on files' do
+ let(:iterator) { [ fake_diff(1, 1) ] * 4 }
+
+ before(:each) do
+ stub_const('Gitlab::Git::DiffCollection::DEFAULT_LIMITS', { max_files: 2, max_lines: max_lines })
+ end
+
+ it 'prunes diffs by default even little ones' do
+ subject.each_with_index do |d, i|
+ if i < 2
+ expect(d.diff).not_to eq('')
+ else # 90 lines
+ expect(d.diff).to eq('')
+ end
+ end
+ end
+ end
+
+ context 'when go over safe limits on lines' do
+ let(:iterator) do
+ [
+ fake_diff(1, 45),
+ fake_diff(1, 45),
+ fake_diff(1, 20480),
+ fake_diff(1, 1)
+ ]
+ end
+
+ before(:each) do
+ stub_const('Gitlab::Git::DiffCollection::DEFAULT_LIMITS', { max_files: max_files, max_lines: 80 })
+ end
+
+ it 'prunes diffs by default even little ones' do
+ subject.each_with_index do |d, i|
+ if i < 2
+ expect(d.diff).not_to eq('')
+ else # 90 lines
+ expect(d.diff).to eq('')
+ end
+ end
+ end
+ end
+
+ context 'when go over safe limits on bytes' do
+ let(:iterator) do
+ [
+ fake_diff(1, 45),
+ fake_diff(1, 45),
+ fake_diff(1, 20480),
+ fake_diff(1, 1)
+ ]
+ end
+
+ before(:each) do
+ stub_const('Gitlab::Git::DiffCollection::DEFAULT_LIMITS', { max_files: max_files, max_lines: 80 })
+ end
+
+ it 'prunes diffs by default even little ones' do
+ subject.each_with_index do |d, i|
+ if i < 2
+ expect(d.diff).not_to eq('')
+ else # > 80 bytes
+ expect(d.diff).to eq('')
+ end
+ end
+ end
+ end
+ end
+
+ context 'when limiting is disabled' do
+ let(:all_diffs) { true }
+
+ it 'yields Diff instances even when they are quite big' do
+ expect { |b| subject.each(&b) }.
+ to yield_with_args(an_instance_of(Gitlab::Git::Diff))
+ end
+
+ it 'does not prune diffs' do
+ diff = nil
+
+ subject.each do |d|
+ diff = d
+ end
+
+ expect(diff.diff).not_to eq('')
+ end
+ end
+ end
+ end
+
+ def fake_diff(line_length, line_count)
+ { 'diff' => "#{'a' * line_length}\n" * line_count }
+ end
+end
diff --git a/spec/lib/gitlab/git/diff_spec.rb b/spec/lib/gitlab/git/diff_spec.rb
new file mode 100644
index 00000000000..4c55532d165
--- /dev/null
+++ b/spec/lib/gitlab/git/diff_spec.rb
@@ -0,0 +1,287 @@
+require "spec_helper"
+
+describe Gitlab::Git::Diff, seed_helper: true do
+ let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) }
+
+ before do
+ @raw_diff_hash = {
+ diff: <<EOT.gsub(/^ {8}/, "").sub(/\n$/, ""),
+ --- a/.gitmodules
+ +++ b/.gitmodules
+ @@ -4,3 +4,6 @@
+ [submodule "gitlab-shell"]
+ \tpath = gitlab-shell
+ \turl = https://github.com/gitlabhq/gitlab-shell.git
+ +[submodule "gitlab-grack"]
+ + path = gitlab-grack
+ + url = https://gitlab.com/gitlab-org/gitlab-grack.git
+
+EOT
+ new_path: ".gitmodules",
+ old_path: ".gitmodules",
+ a_mode: '100644',
+ b_mode: '100644',
+ new_file: false,
+ renamed_file: false,
+ deleted_file: false,
+ too_large: false
+ }
+
+ @rugged_diff = repository.rugged.diff("5937ac0a7beb003549fc5fd26fc247adbce4a52e^", "5937ac0a7beb003549fc5fd26fc247adbce4a52e", paths:
+ [".gitmodules"]).patches.first
+ end
+
+ describe '.new' do
+ context 'using a Hash' do
+ context 'with a small diff' do
+ let(:diff) { described_class.new(@raw_diff_hash) }
+
+ it 'initializes the diff' do
+ expect(diff.to_hash).to eq(@raw_diff_hash)
+ end
+
+ it 'does not prune the diff' do
+ expect(diff).not_to be_too_large
+ end
+ end
+
+ context 'using a diff that is too large' do
+ it 'prunes the diff' do
+ diff = described_class.new(diff: 'a' * 204800)
+
+ expect(diff.diff).to be_empty
+ expect(diff).to be_too_large
+ end
+ end
+ end
+
+ context 'using a Rugged::Patch' do
+ context 'with a small diff' do
+ let(:diff) { described_class.new(@rugged_diff) }
+
+ it 'initializes the diff' do
+ expect(diff.to_hash).to eq(@raw_diff_hash.merge(too_large: nil))
+ end
+
+ it 'does not prune the diff' do
+ expect(diff).not_to be_too_large
+ end
+ end
+
+ context 'using a diff that is too large' do
+ it 'prunes the diff' do
+ expect_any_instance_of(String).to receive(:bytesize).
+ and_return(1024 * 1024 * 1024)
+
+ diff = described_class.new(@rugged_diff)
+
+ expect(diff.diff).to be_empty
+ expect(diff).to be_too_large
+ end
+ end
+
+ context 'using a collapsable diff that is too large' do
+ before do
+ # The patch total size is 200, with lines between 21 and 54.
+ # This is a quick-and-dirty way to test this. Ideally, a new patch is
+ # added to the test repo with a size that falls between the real limits.
+ stub_const("#{described_class}::DIFF_SIZE_LIMIT", 150)
+ stub_const("#{described_class}::DIFF_COLLAPSE_LIMIT", 100)
+ end
+
+ it 'prunes the diff as a large diff instead of as a collapsed diff' do
+ diff = described_class.new(@rugged_diff, collapse: true)
+
+ expect(diff.diff).to be_empty
+ expect(diff).to be_too_large
+ expect(diff).not_to be_collapsed
+ end
+ end
+
+ context 'using a large binary diff' do
+ it 'does not prune the diff' do
+ expect_any_instance_of(Rugged::Diff::Delta).to receive(:binary?).
+ and_return(true)
+
+ diff = described_class.new(@rugged_diff)
+
+ expect(diff.diff).not_to be_empty
+ end
+ end
+ end
+ end
+
+ describe 'straight diffs' do
+ let(:options) { { straight: true } }
+ let(:diffs) { described_class.between(repository, 'feature', 'master', options) }
+
+ it 'has the correct size' do
+ expect(diffs.size).to eq(24)
+ end
+
+ context 'diff' do
+ it 'is an instance of Diff' do
+ expect(diffs.first).to be_kind_of(described_class)
+ end
+
+ it 'has the correct new_path' do
+ expect(diffs.first.new_path).to eq('.DS_Store')
+ end
+
+ it 'has the correct diff' do
+ expect(diffs.first.diff).to include('Binary files /dev/null and b/.DS_Store differ')
+ end
+ end
+ end
+
+ describe '.between' do
+ let(:diffs) { described_class.between(repository, 'feature', 'master') }
+ subject { diffs }
+
+ it { is_expected.to be_kind_of Gitlab::Git::DiffCollection }
+
+ describe '#size' do
+ subject { super().size }
+
+ it { is_expected.to eq(1) }
+ end
+
+ context 'diff' do
+ subject { diffs.first }
+
+ it { is_expected.to be_kind_of described_class }
+
+ describe '#new_path' do
+ subject { super().new_path }
+
+ it { is_expected.to eq('files/ruby/feature.rb') }
+ end
+
+ describe '#diff' do
+ subject { super().diff }
+
+ it { is_expected.to include '+class Feature' }
+ end
+ end
+ end
+
+ describe '.filter_diff_options' do
+ let(:options) { { max_size: 100, invalid_opt: true } }
+
+ context "without default options" do
+ let(:filtered_options) { described_class.filter_diff_options(options) }
+
+ it "should filter invalid options" do
+ expect(filtered_options).not_to have_key(:invalid_opt)
+ end
+ end
+
+ context "with default options" do
+ let(:filtered_options) do
+ default_options = { max_size: 5, bad_opt: 1, ignore_whitespace: true }
+ described_class.filter_diff_options(options, default_options)
+ end
+
+ it "should filter invalid options" do
+ expect(filtered_options).not_to have_key(:invalid_opt)
+ expect(filtered_options).not_to have_key(:bad_opt)
+ end
+
+ it "should merge with default options" do
+ expect(filtered_options).to have_key(:ignore_whitespace)
+ end
+
+ it "should override default options" do
+ expect(filtered_options).to have_key(:max_size)
+ expect(filtered_options[:max_size]).to eq(100)
+ end
+ end
+ end
+
+ describe '#submodule?' do
+ before do
+ commit = repository.lookup('5937ac0a7beb003549fc5fd26fc247adbce4a52e')
+ @diffs = commit.parents[0].diff(commit).patches
+ end
+
+ it { expect(described_class.new(@diffs[0]).submodule?).to eq(false) }
+ it { expect(described_class.new(@diffs[1]).submodule?).to eq(true) }
+ end
+
+ describe '#line_count' do
+ it 'returns the correct number of lines' do
+ diff = described_class.new(@rugged_diff)
+
+ expect(diff.line_count).to eq(9)
+ end
+ end
+
+ describe '#too_large?' do
+ it 'returns true for a diff that is too large' do
+ diff = described_class.new(diff: 'a' * 204800)
+
+ expect(diff.too_large?).to eq(true)
+ end
+
+ it 'returns false for a diff that is small enough' do
+ diff = described_class.new(diff: 'a')
+
+ expect(diff.too_large?).to eq(false)
+ end
+
+ it 'returns true for a diff that was explicitly marked as being too large' do
+ diff = described_class.new(diff: 'a')
+
+ diff.prune_large_diff!
+
+ expect(diff.too_large?).to eq(true)
+ end
+ end
+
+ describe '#collapsed?' do
+ it 'returns false by default even on quite big diff' do
+ diff = described_class.new(diff: 'a' * 20480)
+
+ expect(diff).not_to be_collapsed
+ end
+
+ it 'returns false by default for a diff that is small enough' do
+ diff = described_class.new(diff: 'a')
+
+ expect(diff).not_to be_collapsed
+ end
+
+ it 'returns true for a diff that was explicitly marked as being collapsed' do
+ diff = described_class.new(diff: 'a')
+
+ diff.prune_collapsed_diff!
+
+ expect(diff).to be_collapsed
+ end
+ end
+
+ describe '#collapsible?' do
+ it 'returns true for a diff that is quite large' do
+ diff = described_class.new(diff: 'a' * 20480)
+
+ expect(diff).to be_collapsible
+ end
+
+ it 'returns false for a diff that is small enough' do
+ diff = described_class.new(diff: 'a')
+
+ expect(diff).not_to be_collapsible
+ end
+ end
+
+ describe '#prune_collapsed_diff!' do
+ it 'prunes the diff' do
+ diff = described_class.new(diff: "foo\nbar")
+
+ diff.prune_collapsed_diff!
+
+ expect(diff.diff).to eq('')
+ expect(diff.line_count).to eq(0)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/git/encoding_helper_spec.rb b/spec/lib/gitlab/git/encoding_helper_spec.rb
new file mode 100644
index 00000000000..83311536893
--- /dev/null
+++ b/spec/lib/gitlab/git/encoding_helper_spec.rb
@@ -0,0 +1,84 @@
+require "spec_helper"
+
+describe Gitlab::Git::EncodingHelper do
+ let(:ext_class) { Class.new { extend Gitlab::Git::EncodingHelper } }
+ let(:binary_string) { File.join(SEED_REPOSITORY_PATH, 'gitlab_logo.png') }
+
+ describe '#encode!' do
+ [
+ [
+ 'leaves ascii only string as is',
+ 'ascii only string',
+ 'ascii only string'
+ ],
+ [
+ 'leaves valid utf8 string as is',
+ 'multibyte string №∑∉',
+ 'multibyte string №∑∉'
+ ],
+ [
+ 'removes invalid bytes from ASCII-8bit encoded multibyte string. This can occur when a git diff match line truncates in the middle of a multibyte character. This occurs after the second word in this example. The test string is as short as we can get while still triggering the error condition when not looking at `detect[:confidence]`.',
+ "mu ns\xC3\n Lorem ipsum dolor sit amet, consectetur adipisicing ut\xC3\xA0y\xC3\xB9abcd\xC3\xB9efg kia elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non p\n {: .normal_pn}\n \n-Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in\n# *Lorem ipsum\xC3\xB9l\xC3\xB9l\xC3\xA0 dolor\xC3\xB9k\xC3\xB9 sit\xC3\xA8b\xC3\xA8 N\xC3\xA8 amet b\xC3\xA0d\xC3\xAC*\n+# *consectetur\xC3\xB9l\xC3\xB9l\xC3\xA0 adipisicing\xC3\xB9k\xC3\xB9 elit\xC3\xA8b\xC3\xA8 N\xC3\xA8 sed do\xC3\xA0d\xC3\xAC*{: .italic .smcaps}\n \n \xEF\x9B\xA1 eiusmod tempor incididunt, ut\xC3\xAAn\xC3\xB9 labore et dolore. Tw\xC4\x83nj\xC3\xAC magna aliqua. Ut enim ad minim veniam\n {: .normal}\n@@ -9,5 +9,5 @@ quis nostrud\xC3\xAAt\xC3\xB9 exercitiation ullamco laboris m\xC3\xB9s\xC3\xB9k\xC3\xB9abc\xC3\xB9 nisi ".force_encoding('ASCII-8BIT'),
+ "mu ns\n Lorem ipsum dolor sit amet, consectetur adipisicing ut\xC3\xA0y\xC3\xB9abcd\xC3\xB9efg kia elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non p\n {: .normal_pn}\n \n-Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in\n# *Lorem ipsum\xC3\xB9l\xC3\xB9l\xC3\xA0 dolor\xC3\xB9k\xC3\xB9 sit\xC3\xA8b\xC3\xA8 N\xC3\xA8 amet b\xC3\xA0d\xC3\xAC*\n+# *consectetur\xC3\xB9l\xC3\xB9l\xC3\xA0 adipisicing\xC3\xB9k\xC3\xB9 elit\xC3\xA8b\xC3\xA8 N\xC3\xA8 sed do\xC3\xA0d\xC3\xAC*{: .italic .smcaps}\n \n \xEF\x9B\xA1 eiusmod tempor incididunt, ut\xC3\xAAn\xC3\xB9 labore et dolore. Tw\xC4\x83nj\xC3\xAC magna aliqua. Ut enim ad minim veniam\n {: .normal}\n@@ -9,5 +9,5 @@ quis nostrud\xC3\xAAt\xC3\xB9 exercitiation ullamco laboris m\xC3\xB9s\xC3\xB9k\xC3\xB9abc\xC3\xB9 nisi ",
+ ],
+ ].each do |description, test_string, xpect|
+ it description do
+ expect(ext_class.encode!(test_string)).to eq(xpect)
+ end
+ end
+
+ it 'leaves binary string as is' do
+ expect(ext_class.encode!(binary_string)).to eq(binary_string)
+ end
+ end
+
+ describe '#encode_utf8' do
+ [
+ [
+ "encodes valid utf8 encoded string to utf8",
+ "λ, λ, λ".encode("UTF-8"),
+ "λ, λ, λ".encode("UTF-8"),
+ ],
+ [
+ "encodes valid ASCII-8BIT encoded string to utf8",
+ "ascii only".encode("ASCII-8BIT"),
+ "ascii only".encode("UTF-8"),
+ ],
+ [
+ "encodes valid ISO-8859-1 encoded string to utf8",
+ "Rüby ist eine Programmiersprache. Wir verlängern den text damit ICU die Sprache erkennen kann.".encode("ISO-8859-1", "UTF-8"),
+ "Rüby ist eine Programmiersprache. Wir verlängern den text damit ICU die Sprache erkennen kann.".encode("UTF-8"),
+ ],
+ ].each do |description, test_string, xpect|
+ it description do
+ r = ext_class.encode_utf8(test_string.force_encoding('UTF-8'))
+ expect(r).to eq(xpect)
+ expect(r.encoding.name).to eq('UTF-8')
+ end
+ end
+ end
+
+ describe '#clean' do
+ [
+ [
+ 'leaves ascii only string as is',
+ 'ascii only string',
+ 'ascii only string'
+ ],
+ [
+ 'leaves valid utf8 string as is',
+ 'multibyte string №∑∉',
+ 'multibyte string №∑∉'
+ ],
+ [
+ 'removes invalid bytes from ASCII-8bit encoded multibyte string.',
+ "Lorem ipsum\xC3\n dolor sit amet, xy\xC3\xA0y\xC3\xB9abcd\xC3\xB9efg".force_encoding('ASCII-8BIT'),
+ "Lorem ipsum\n dolor sit amet, xyàyùabcdùefg",
+ ],
+ ].each do |description, test_string, xpect|
+ it description do
+ expect(ext_class.encode!(test_string)).to eq(xpect)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/git/hook_spec.rb b/spec/lib/gitlab/git/hook_spec.rb
index d1f947b6850..3f279c21865 100644
--- a/spec/lib/gitlab/git/hook_spec.rb
+++ b/spec/lib/gitlab/git/hook_spec.rb
@@ -3,7 +3,7 @@ require 'fileutils'
describe Gitlab::Git::Hook, lib: true do
describe "#trigger" do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
def create_hook(name)
diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
new file mode 100644
index 00000000000..2a915bf426f
--- /dev/null
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -0,0 +1,1184 @@
+require "spec_helper"
+
+describe Gitlab::Git::Repository, seed_helper: true do
+ include Gitlab::Git::EncodingHelper
+
+ let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) }
+
+ describe "Respond to" do
+ subject { repository }
+
+ it { is_expected.to respond_to(:raw) }
+ it { is_expected.to respond_to(:rugged) }
+ it { is_expected.to respond_to(:root_ref) }
+ it { is_expected.to respond_to(:tags) }
+ end
+
+ describe "#discover_default_branch" do
+ let(:master) { 'master' }
+ let(:feature) { 'feature' }
+ let(:feature2) { 'feature2' }
+
+ it "returns 'master' when master exists" do
+ expect(repository).to receive(:branch_names).at_least(:once).and_return([feature, master])
+ expect(repository.discover_default_branch).to eq('master')
+ end
+
+ it "returns non-master when master exists but default branch is set to something else" do
+ File.write(File.join(repository.path, 'HEAD'), 'ref: refs/heads/feature')
+ expect(repository).to receive(:branch_names).at_least(:once).and_return([feature, master])
+ expect(repository.discover_default_branch).to eq('feature')
+ File.write(File.join(repository.path, 'HEAD'), 'ref: refs/heads/master')
+ end
+
+ it "returns a non-master branch when only one exists" do
+ expect(repository).to receive(:branch_names).at_least(:once).and_return([feature])
+ expect(repository.discover_default_branch).to eq('feature')
+ end
+
+ it "returns a non-master branch when more than one exists and master does not" do
+ expect(repository).to receive(:branch_names).at_least(:once).and_return([feature, feature2])
+ expect(repository.discover_default_branch).to eq('feature')
+ end
+
+ it "returns nil when no branch exists" do
+ expect(repository).to receive(:branch_names).at_least(:once).and_return([])
+ expect(repository.discover_default_branch).to be_nil
+ end
+ end
+
+ describe :branch_names do
+ subject { repository.branch_names }
+
+ it 'has SeedRepo::Repo::BRANCHES.size elements' do
+ expect(subject.size).to eq(SeedRepo::Repo::BRANCHES.size)
+ end
+ it { is_expected.to include("master") }
+ it { is_expected.not_to include("branch-from-space") }
+ end
+
+ describe :tag_names do
+ subject { repository.tag_names }
+
+ it { is_expected.to be_kind_of Array }
+ it 'has SeedRepo::Repo::TAGS.size elements' do
+ expect(subject.size).to eq(SeedRepo::Repo::TAGS.size)
+ end
+
+ describe '#last' do
+ subject { super().last }
+ it { is_expected.to eq("v1.2.1") }
+ end
+ it { is_expected.to include("v1.0.0") }
+ it { is_expected.not_to include("v5.0.0") }
+ end
+
+ shared_examples 'archive check' do |extenstion|
+ it { expect(metadata['ArchivePath']).to match(/tmp\/gitlab-git-test.git\/gitlab-git-test-master-#{SeedRepo::LastCommit::ID}/) }
+ it { expect(metadata['ArchivePath']).to end_with extenstion }
+ end
+
+ describe :archive do
+ let(:metadata) { repository.archive_metadata('master', '/tmp') }
+
+ it_should_behave_like 'archive check', '.tar.gz'
+ end
+
+ describe :archive_zip do
+ let(:metadata) { repository.archive_metadata('master', '/tmp', 'zip') }
+
+ it_should_behave_like 'archive check', '.zip'
+ end
+
+ describe :archive_bz2 do
+ let(:metadata) { repository.archive_metadata('master', '/tmp', 'tbz2') }
+
+ it_should_behave_like 'archive check', '.tar.bz2'
+ end
+
+ describe :archive_fallback do
+ let(:metadata) { repository.archive_metadata('master', '/tmp', 'madeup') }
+
+ it_should_behave_like 'archive check', '.tar.gz'
+ end
+
+ describe :size do
+ subject { repository.size }
+
+ it { is_expected.to be < 2 }
+ end
+
+ describe :has_commits? do
+ it { expect(repository.has_commits?).to be_truthy }
+ end
+
+ describe :empty? do
+ it { expect(repository.empty?).to be_falsey }
+ end
+
+ describe :bare? do
+ it { expect(repository.bare?).to be_truthy }
+ end
+
+ describe :heads do
+ let(:heads) { repository.heads }
+ subject { heads }
+
+ it { is_expected.to be_kind_of Array }
+
+ describe '#size' do
+ subject { super().size }
+ it { is_expected.to eq(SeedRepo::Repo::BRANCHES.size) }
+ end
+
+ context :head do
+ subject { heads.first }
+
+ describe '#name' do
+ subject { super().name }
+ it { is_expected.to eq("feature") }
+ end
+
+ context :commit do
+ subject { heads.first.dereferenced_target.sha }
+
+ it { is_expected.to eq("0b4bc9a49b562e85de7cc9e834518ea6828729b9") }
+ end
+ end
+ end
+
+ describe :ref_names do
+ let(:ref_names) { repository.ref_names }
+ subject { ref_names }
+
+ it { is_expected.to be_kind_of Array }
+
+ describe '#first' do
+ subject { super().first }
+ it { is_expected.to eq('feature') }
+ end
+
+ describe '#last' do
+ subject { super().last }
+ it { is_expected.to eq('v1.2.1') }
+ end
+ end
+
+ describe :search_files do
+ let(:results) { repository.search_files('rails', 'master') }
+ subject { results }
+
+ it { is_expected.to be_kind_of Array }
+
+ describe '#first' do
+ subject { super().first }
+ it { is_expected.to be_kind_of Gitlab::Git::BlobSnippet }
+ end
+
+ context 'blob result' do
+ subject { results.first }
+
+ describe '#ref' do
+ subject { super().ref }
+ it { is_expected.to eq('master') }
+ end
+
+ describe '#filename' do
+ subject { super().filename }
+ it { is_expected.to eq('CHANGELOG') }
+ end
+
+ describe '#startline' do
+ subject { super().startline }
+ it { is_expected.to eq(35) }
+ end
+
+ describe '#data' do
+ subject { super().data }
+ it { is_expected.to include "Ability to filter by multiple labels" }
+ end
+ end
+ end
+
+ context :submodules do
+ let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) }
+
+ context 'where repo has submodules' do
+ let(:submodules) { repository.submodules('master') }
+ let(:submodule) { submodules.first }
+
+ it { expect(submodules).to be_kind_of Hash }
+ it { expect(submodules.empty?).to be_falsey }
+
+ it 'should have valid data' do
+ expect(submodule).to eq([
+ "six", {
+ "id" => "409f37c4f05865e4fb208c771485f211a22c4c2d",
+ "path" => "six",
+ "url" => "git://github.com/randx/six.git"
+ }
+ ])
+ end
+
+ it 'should handle nested submodules correctly' do
+ nested = submodules['nested/six']
+ expect(nested['path']).to eq('nested/six')
+ expect(nested['url']).to eq('git://github.com/randx/six.git')
+ expect(nested['id']).to eq('24fb71c79fcabc63dfd8832b12ee3bf2bf06b196')
+ end
+
+ it 'should handle deeply nested submodules correctly' do
+ nested = submodules['deeper/nested/six']
+ expect(nested['path']).to eq('deeper/nested/six')
+ expect(nested['url']).to eq('git://github.com/randx/six.git')
+ expect(nested['id']).to eq('24fb71c79fcabc63dfd8832b12ee3bf2bf06b196')
+ end
+
+ it 'should not have an entry for an invalid submodule' do
+ expect(submodules).not_to have_key('invalid/path')
+ end
+
+ it 'should not have an entry for an uncommited submodule dir' do
+ submodules = repository.submodules('fix-existing-submodule-dir')
+ expect(submodules).not_to have_key('submodule-existing-dir')
+ end
+
+ it 'should handle tags correctly' do
+ submodules = repository.submodules('v1.2.1')
+
+ expect(submodules.first).to eq([
+ "six", {
+ "id" => "409f37c4f05865e4fb208c771485f211a22c4c2d",
+ "path" => "six",
+ "url" => "git://github.com/randx/six.git"
+ }
+ ])
+ end
+ end
+
+ context 'where repo doesn\'t have submodules' do
+ let(:submodules) { repository.submodules('6d39438') }
+ it 'should return an empty hash' do
+ expect(submodules).to be_empty
+ end
+ end
+ end
+
+ describe :commit_count do
+ it { expect(repository.commit_count("master")).to eq(25) }
+ it { expect(repository.commit_count("feature")).to eq(9) }
+ end
+
+ describe "#reset" do
+ change_path = File.join(TEST_NORMAL_REPO_PATH, "CHANGELOG")
+ untracked_path = File.join(TEST_NORMAL_REPO_PATH, "UNTRACKED")
+ tracked_path = File.join(TEST_NORMAL_REPO_PATH, "files", "ruby", "popen.rb")
+
+ change_text = "New changelog text"
+ untracked_text = "This file is untracked"
+
+ reset_commit = SeedRepo::LastCommit::ID
+
+ context "--hard" do
+ before(:all) do
+ # Modify a tracked file
+ File.open(change_path, "w") do |f|
+ f.write(change_text)
+ end
+
+ # Add an untracked file to the working directory
+ File.open(untracked_path, "w") do |f|
+ f.write(untracked_text)
+ end
+
+ @normal_repo = Gitlab::Git::Repository.new(TEST_NORMAL_REPO_PATH)
+ @normal_repo.reset("HEAD", :hard)
+ end
+
+ it "should replace the working directory with the content of the index" do
+ File.open(change_path, "r") do |f|
+ expect(f.each_line.first).not_to eq(change_text)
+ end
+
+ File.open(tracked_path, "r") do |f|
+ expect(f.each_line.to_a[8]).to include('raise RuntimeError, "System commands')
+ end
+ end
+
+ it "should not touch untracked files" do
+ expect(File.exist?(untracked_path)).to be_truthy
+ end
+
+ it "should move the HEAD to the correct commit" do
+ new_head = @normal_repo.rugged.head.target.oid
+ expect(new_head).to eq(reset_commit)
+ end
+
+ it "should move the tip of the master branch to the correct commit" do
+ new_tip = @normal_repo.rugged.references["refs/heads/master"].
+ target.oid
+
+ expect(new_tip).to eq(reset_commit)
+ end
+
+ after(:all) do
+ # Fast-forward to the original HEAD
+ FileUtils.rm_rf(TEST_NORMAL_REPO_PATH)
+ ensure_seeds
+ end
+ end
+ end
+
+ describe "#checkout" do
+ new_branch = "foo_branch"
+
+ context "-b" do
+ before(:all) do
+ @normal_repo = Gitlab::Git::Repository.new(TEST_NORMAL_REPO_PATH)
+ @normal_repo.checkout(new_branch, { b: true }, "origin/feature")
+ end
+
+ it "should create a new branch" do
+ expect(@normal_repo.rugged.branches[new_branch]).not_to be_nil
+ end
+
+ it "should move the HEAD to the correct commit" do
+ expect(@normal_repo.rugged.head.target.oid).to(
+ eq(@normal_repo.rugged.branches["origin/feature"].target.oid)
+ )
+ end
+
+ it "should refresh the repo's #heads collection" do
+ head_names = @normal_repo.heads.map { |h| h.name }
+ expect(head_names).to include(new_branch)
+ end
+
+ after(:all) do
+ FileUtils.rm_rf(TEST_NORMAL_REPO_PATH)
+ ensure_seeds
+ end
+ end
+
+ context "without -b" do
+ context "and specifying a nonexistent branch" do
+ it "should not do anything" do
+ normal_repo = Gitlab::Git::Repository.new(TEST_NORMAL_REPO_PATH)
+
+ expect { normal_repo.checkout(new_branch) }.to raise_error(Rugged::ReferenceError)
+ expect(normal_repo.rugged.branches[new_branch]).to be_nil
+ expect(normal_repo.rugged.head.target.oid).to(
+ eq(normal_repo.rugged.branches["master"].target.oid)
+ )
+
+ head_names = normal_repo.heads.map { |h| h.name }
+ expect(head_names).not_to include(new_branch)
+ end
+
+ after(:all) do
+ FileUtils.rm_rf(TEST_NORMAL_REPO_PATH)
+ ensure_seeds
+ end
+ end
+
+ context "and with a valid branch" do
+ before(:all) do
+ @normal_repo = Gitlab::Git::Repository.new(TEST_NORMAL_REPO_PATH)
+ @normal_repo.rugged.branches.create("feature", "origin/feature")
+ @normal_repo.checkout("feature")
+ end
+
+ it "should move the HEAD to the correct commit" do
+ expect(@normal_repo.rugged.head.target.oid).to(
+ eq(@normal_repo.rugged.branches["feature"].target.oid)
+ )
+ end
+
+ it "should update the working directory" do
+ File.open(File.join(TEST_NORMAL_REPO_PATH, ".gitignore"), "r") do |f|
+ expect(f.read.each_line.to_a).not_to include(".DS_Store\n")
+ end
+ end
+
+ after(:all) do
+ FileUtils.rm_rf(TEST_NORMAL_REPO_PATH)
+ ensure_seeds
+ end
+ end
+ end
+ end
+
+ describe "#delete_branch" do
+ before(:all) do
+ @repo = Gitlab::Git::Repository.new(TEST_MUTABLE_REPO_PATH)
+ @repo.delete_branch("feature")
+ end
+
+ it "should remove the branch from the repo" do
+ expect(@repo.rugged.branches["feature"]).to be_nil
+ end
+
+ it "should update the repo's #heads collection" do
+ expect(@repo.heads).not_to include("feature")
+ end
+
+ after(:all) do
+ FileUtils.rm_rf(TEST_MUTABLE_REPO_PATH)
+ ensure_seeds
+ end
+ end
+
+ describe "#create_branch" do
+ before(:all) do
+ @repo = Gitlab::Git::Repository.new(TEST_MUTABLE_REPO_PATH)
+ end
+
+ it "should create a new branch" do
+ expect(@repo.create_branch('new_branch', 'master')).not_to be_nil
+ end
+
+ it "should create a new branch with the right name" do
+ expect(@repo.create_branch('another_branch', 'master').name).to eq('another_branch')
+ end
+
+ it "should fail if we create an existing branch" do
+ @repo.create_branch('duplicated_branch', 'master')
+ expect{@repo.create_branch('duplicated_branch', 'master')}.to raise_error("Branch duplicated_branch already exists")
+ end
+
+ it "should fail if we create a branch from a non existing ref" do
+ expect{@repo.create_branch('branch_based_in_wrong_ref', 'master_2_the_revenge')}.to raise_error("Invalid reference master_2_the_revenge")
+ end
+
+ after(:all) do
+ FileUtils.rm_rf(TEST_MUTABLE_REPO_PATH)
+ ensure_seeds
+ end
+ end
+
+ describe "#remote_names" do
+ let(:remotes) { repository.remote_names }
+
+ it "should have one entry: 'origin'" do
+ expect(remotes.size).to eq(1)
+ expect(remotes.first).to eq("origin")
+ end
+ end
+
+ describe "#refs_hash" do
+ let(:refs) { repository.refs_hash }
+
+ it "should have as many entries as branches and tags" do
+ expected_refs = SeedRepo::Repo::BRANCHES + SeedRepo::Repo::TAGS
+ # We flatten in case a commit is pointed at by more than one branch and/or tag
+ expect(refs.values.flatten.size).to eq(expected_refs.size)
+ end
+ end
+
+ describe "#remote_delete" do
+ before(:all) do
+ @repo = Gitlab::Git::Repository.new(TEST_MUTABLE_REPO_PATH)
+ @repo.remote_delete("expendable")
+ end
+
+ it "should remove the remote" do
+ expect(@repo.rugged.remotes).not_to include("expendable")
+ end
+
+ after(:all) do
+ FileUtils.rm_rf(TEST_MUTABLE_REPO_PATH)
+ ensure_seeds
+ end
+ end
+
+ describe "#remote_add" do
+ before(:all) do
+ @repo = Gitlab::Git::Repository.new(TEST_MUTABLE_REPO_PATH)
+ @repo.remote_add("new_remote", SeedHelper::GITLAB_URL)
+ end
+
+ it "should add the remote" do
+ expect(@repo.rugged.remotes.each_name.to_a).to include("new_remote")
+ end
+
+ after(:all) do
+ FileUtils.rm_rf(TEST_MUTABLE_REPO_PATH)
+ ensure_seeds
+ end
+ end
+
+ describe "#remote_update" do
+ before(:all) do
+ @repo = Gitlab::Git::Repository.new(TEST_MUTABLE_REPO_PATH)
+ @repo.remote_update("expendable", url: TEST_NORMAL_REPO_PATH)
+ end
+
+ it "should add the remote" do
+ expect(@repo.rugged.remotes["expendable"].url).to(
+ eq(TEST_NORMAL_REPO_PATH)
+ )
+ end
+
+ after(:all) do
+ FileUtils.rm_rf(TEST_MUTABLE_REPO_PATH)
+ ensure_seeds
+ end
+ end
+
+ describe "#log" do
+ commit_with_old_name = nil
+ commit_with_new_name = nil
+ rename_commit = nil
+
+ before(:all) do
+ # Add new commits so that there's a renamed file in the commit history
+ repo = Gitlab::Git::Repository.new(TEST_REPO_PATH).rugged
+
+ commit_with_old_name = new_commit_edit_old_file(repo)
+ rename_commit = new_commit_move_file(repo)
+ commit_with_new_name = new_commit_edit_new_file(repo)
+ end
+
+ context "where 'follow' == true" do
+ options = { ref: "master", follow: true }
+
+ context "and 'path' is a directory" do
+ let(:log_commits) do
+ repository.log(options.merge(path: "encoding"))
+ end
+
+ it "should not follow renames" do
+ expect(log_commits).to include(commit_with_new_name)
+ expect(log_commits).to include(rename_commit)
+ expect(log_commits).not_to include(commit_with_old_name)
+ end
+ end
+
+ context "and 'path' is a file that matches the new filename" do
+ let(:log_commits) do
+ repository.log(options.merge(path: "encoding/CHANGELOG"))
+ end
+
+ it "should follow renames" do
+ expect(log_commits).to include(commit_with_new_name)
+ expect(log_commits).to include(rename_commit)
+ expect(log_commits).to include(commit_with_old_name)
+ end
+ end
+
+ context "and 'path' is a file that matches the old filename" do
+ let(:log_commits) do
+ repository.log(options.merge(path: "CHANGELOG"))
+ end
+
+ it "should not follow renames" do
+ expect(log_commits).to include(commit_with_old_name)
+ expect(log_commits).to include(rename_commit)
+ expect(log_commits).not_to include(commit_with_new_name)
+ end
+ end
+
+ context "unknown ref" do
+ let(:log_commits) { repository.log(options.merge(ref: 'unknown')) }
+
+ it "should return empty" do
+ expect(log_commits).to eq([])
+ end
+ end
+ end
+
+ context "where 'follow' == false" do
+ options = { follow: false }
+
+ context "and 'path' is a directory" do
+ let(:log_commits) do
+ repository.log(options.merge(path: "encoding"))
+ end
+
+ it "should not follow renames" do
+ expect(log_commits).to include(commit_with_new_name)
+ expect(log_commits).to include(rename_commit)
+ expect(log_commits).not_to include(commit_with_old_name)
+ end
+ end
+
+ context "and 'path' is a file that matches the new filename" do
+ let(:log_commits) do
+ repository.log(options.merge(path: "encoding/CHANGELOG"))
+ end
+
+ it "should not follow renames" do
+ expect(log_commits).to include(commit_with_new_name)
+ expect(log_commits).to include(rename_commit)
+ expect(log_commits).not_to include(commit_with_old_name)
+ end
+ end
+
+ context "and 'path' is a file that matches the old filename" do
+ let(:log_commits) do
+ repository.log(options.merge(path: "CHANGELOG"))
+ end
+
+ it "should not follow renames" do
+ expect(log_commits).to include(commit_with_old_name)
+ expect(log_commits).to include(rename_commit)
+ expect(log_commits).not_to include(commit_with_new_name)
+ end
+ end
+
+ context "and 'path' includes a directory that used to be a file" do
+ let(:log_commits) do
+ repository.log(options.merge(ref: "refs/heads/fix-blob-path", path: "files/testdir/file.txt"))
+ end
+
+ it "should return a list of commits" do
+ expect(log_commits.size).to eq(1)
+ end
+ end
+ end
+
+ context "compare results between log_by_walk and log_by_shell" do
+ let(:options) { { ref: "master" } }
+ let(:commits_by_walk) { repository.log(options).map(&:oid) }
+ let(:commits_by_shell) { repository.log(options.merge({ disable_walk: true })).map(&:oid) }
+
+ it { expect(commits_by_walk).to eq(commits_by_shell) }
+
+ context "with limit" do
+ let(:options) { { ref: "master", limit: 1 } }
+
+ it { expect(commits_by_walk).to eq(commits_by_shell) }
+ end
+
+ context "with offset" do
+ let(:options) { { ref: "master", offset: 1 } }
+
+ it { expect(commits_by_walk).to eq(commits_by_shell) }
+ end
+
+ context "with skip_merges" do
+ let(:options) { { ref: "master", skip_merges: true } }
+
+ it { expect(commits_by_walk).to eq(commits_by_shell) }
+ end
+
+ context "with path" do
+ let(:options) { { ref: "master", path: "encoding" } }
+
+ it { expect(commits_by_walk).to eq(commits_by_shell) }
+
+ context "with follow" do
+ let(:options) { { ref: "master", path: "encoding", follow: true } }
+
+ it { expect(commits_by_walk).to eq(commits_by_shell) }
+ end
+ end
+ end
+
+ context "where provides 'after' timestamp" do
+ options = { after: Time.iso8601('2014-03-03T20:15:01+00:00') }
+
+ it "should returns commits on or after that timestamp" do
+ commits = repository.log(options)
+
+ expect(commits.size).to be > 0
+ satisfy do
+ commits.all? { |commit| commit.created_at >= options[:after] }
+ end
+ end
+ end
+
+ context "where provides 'before' timestamp" do
+ options = { before: Time.iso8601('2014-03-03T20:15:01+00:00') }
+
+ it "should returns commits on or before that timestamp" do
+ commits = repository.log(options)
+
+ expect(commits.size).to be > 0
+ satisfy do
+ commits.all? { |commit| commit.created_at <= options[:before] }
+ end
+ end
+ end
+
+ after(:all) do
+ # Erase our commits so other tests get the original repo
+ repo = Gitlab::Git::Repository.new(TEST_REPO_PATH).rugged
+ repo.references.update("refs/heads/master", SeedRepo::LastCommit::ID)
+ end
+ end
+
+ describe "#commits_between" do
+ context 'two SHAs' do
+ let(:first_sha) { 'b0e52af38d7ea43cf41d8a6f2471351ac036d6c9' }
+ let(:second_sha) { '0e50ec4d3c7ce42ab74dda1d422cb2cbffe1e326' }
+
+ it 'returns the number of commits between' do
+ expect(repository.commits_between(first_sha, second_sha).count).to eq(3)
+ end
+ end
+
+ context 'SHA and master branch' do
+ let(:sha) { 'b0e52af38d7ea43cf41d8a6f2471351ac036d6c9' }
+ let(:branch) { 'master' }
+
+ it 'returns the number of commits between a sha and a branch' do
+ expect(repository.commits_between(sha, branch).count).to eq(5)
+ end
+
+ it 'returns the number of commits between a branch and a sha' do
+ expect(repository.commits_between(branch, sha).count).to eq(0) # sha is before branch
+ end
+ end
+
+ context 'two branches' do
+ let(:first_branch) { 'feature' }
+ let(:second_branch) { 'master' }
+
+ it 'returns the number of commits between' do
+ expect(repository.commits_between(first_branch, second_branch).count).to eq(17)
+ end
+ end
+ end
+
+ describe '#count_commits_between' do
+ subject { repository.count_commits_between('feature', 'master') }
+
+ it { is_expected.to eq(17) }
+ end
+
+ describe "branch_names_contains" do
+ subject { repository.branch_names_contains(SeedRepo::LastCommit::ID) }
+
+ it { is_expected.to include('master') }
+ it { is_expected.not_to include('feature') }
+ it { is_expected.not_to include('fix') }
+ end
+
+ describe '#autocrlf' do
+ before(:all) do
+ @repo = Gitlab::Git::Repository.new(TEST_MUTABLE_REPO_PATH)
+ @repo.rugged.config['core.autocrlf'] = true
+ end
+
+ it 'return the value of the autocrlf option' do
+ expect(@repo.autocrlf).to be(true)
+ end
+
+ after(:all) do
+ @repo.rugged.config.delete('core.autocrlf')
+ end
+ end
+
+ describe '#autocrlf=' do
+ before(:all) do
+ @repo = Gitlab::Git::Repository.new(TEST_MUTABLE_REPO_PATH)
+ @repo.rugged.config['core.autocrlf'] = false
+ end
+
+ it 'should set the autocrlf option to the provided option' do
+ @repo.autocrlf = :input
+
+ File.open(File.join(TEST_MUTABLE_REPO_PATH, '.git', 'config')) do |config_file|
+ expect(config_file.read).to match('autocrlf = input')
+ end
+ end
+
+ after(:all) do
+ @repo.rugged.config.delete('core.autocrlf')
+ end
+ end
+
+ describe '#find_branch' do
+ it 'should return a Branch for master' do
+ branch = repository.find_branch('master')
+
+ expect(branch).to be_a_kind_of(Gitlab::Git::Branch)
+ expect(branch.name).to eq('master')
+ end
+
+ it 'should handle non-existent branch' do
+ branch = repository.find_branch('this-is-garbage')
+
+ expect(branch).to eq(nil)
+ end
+
+ it 'should reload Rugged::Repository and return master' do
+ expect(Rugged::Repository).to receive(:new).twice.and_call_original
+
+ repository.find_branch('master')
+ branch = repository.find_branch('master', force_reload: true)
+
+ expect(branch).to be_a_kind_of(Gitlab::Git::Branch)
+ expect(branch.name).to eq('master')
+ end
+ end
+
+ describe '#branches with deleted branch' do
+ before(:each) do
+ ref = double()
+ allow(ref).to receive(:name) { 'bad-branch' }
+ allow(ref).to receive(:target) { raise Rugged::ReferenceError }
+ allow(repository.rugged).to receive(:branches) { [ref] }
+ end
+
+ it 'should return empty branches' do
+ expect(repository.branches).to eq([])
+ end
+ end
+
+ describe '#branch_count' do
+ before(:each) do
+ valid_ref = double(:ref)
+ invalid_ref = double(:ref)
+
+ allow(valid_ref).to receive_messages(name: 'master', target: double(:target))
+
+ allow(invalid_ref).to receive_messages(name: 'bad-branch')
+ allow(invalid_ref).to receive(:target) { raise Rugged::ReferenceError }
+
+ allow(repository.rugged).to receive_messages(branches: [valid_ref, invalid_ref])
+ end
+
+ it 'returns the number of branches' do
+ expect(repository.branch_count).to eq(1)
+ end
+ end
+
+ describe '#mkdir' do
+ let(:commit_options) do
+ {
+ author: {
+ email: 'user@example.com',
+ name: 'Test User',
+ time: Time.now
+ },
+ committer: {
+ email: 'user@example.com',
+ name: 'Test User',
+ time: Time.now
+ },
+ commit: {
+ message: 'Test message',
+ branch: 'refs/heads/fix',
+ }
+ }
+ end
+
+ def generate_diff_for_path(path)
+ "diff --git a/#{path}/.gitkeep b/#{path}/.gitkeep
+new file mode 100644
+index 0000000..e69de29
+--- /dev/null
++++ b/#{path}/.gitkeep\n"
+ end
+
+ shared_examples 'mkdir diff check' do |path, expected_path|
+ it 'creates a directory' do
+ result = repository.mkdir(path, commit_options)
+ expect(result).not_to eq(nil)
+
+ # Verify another mkdir doesn't create a directory that already exists
+ expect{ repository.mkdir(path, commit_options) }.to raise_error('Directory already exists')
+ end
+ end
+
+ describe 'creates a directory in root directory' do
+ it_should_behave_like 'mkdir diff check', 'new_dir', 'new_dir'
+ end
+
+ describe 'creates a directory in subdirectory' do
+ it_should_behave_like 'mkdir diff check', 'files/ruby/test', 'files/ruby/test'
+ end
+
+ describe 'creates a directory in subdirectory with a slash' do
+ it_should_behave_like 'mkdir diff check', '/files/ruby/test2', 'files/ruby/test2'
+ end
+
+ describe 'creates a directory in subdirectory with multiple slashes' do
+ it_should_behave_like 'mkdir diff check', '//files/ruby/test3', 'files/ruby/test3'
+ end
+
+ describe 'handles relative paths' do
+ it_should_behave_like 'mkdir diff check', 'files/ruby/../test_relative', 'files/test_relative'
+ end
+
+ describe 'creates nested directories' do
+ it_should_behave_like 'mkdir diff check', 'files/missing/test', 'files/missing/test'
+ end
+
+ it 'does not attempt to create a directory with invalid relative path' do
+ expect{ repository.mkdir('../files/missing/test', commit_options) }.to raise_error('Invalid path')
+ end
+
+ it 'does not attempt to overwrite a file' do
+ expect{ repository.mkdir('README.md', commit_options) }.to raise_error('Directory already exists as a file')
+ end
+
+ it 'does not attempt to overwrite a directory' do
+ expect{ repository.mkdir('files', commit_options) }.to raise_error('Directory already exists')
+ end
+ end
+
+ describe "#ls_files" do
+ let(:master_file_paths) { repository.ls_files("master") }
+ let(:not_existed_branch) { repository.ls_files("not_existed_branch") }
+
+ it "read every file paths of master branch" do
+ expect(master_file_paths.length).to equal(40)
+ end
+
+ it "reads full file paths of master branch" do
+ expect(master_file_paths).to include("files/html/500.html")
+ end
+
+ it "dose not read submodule directory and empty directory of master branch" do
+ expect(master_file_paths).not_to include("six")
+ end
+
+ it "does not include 'nil'" do
+ expect(master_file_paths).not_to include(nil)
+ end
+
+ it "returns empty array when not existed branch" do
+ expect(not_existed_branch.length).to equal(0)
+ end
+ end
+
+ describe "#copy_gitattributes" do
+ let(:attributes_path) { File.join(TEST_REPO_PATH, 'info/attributes') }
+
+ it "raises an error with invalid ref" do
+ expect { repository.copy_gitattributes("invalid") }.to raise_error(Gitlab::Git::Repository::InvalidRef)
+ end
+
+ context "with no .gitattrbutes" do
+ before(:each) do
+ repository.copy_gitattributes("master")
+ end
+
+ it "does not have an info/attributes" do
+ expect(File.exist?(attributes_path)).to be_falsey
+ end
+
+ after(:each) do
+ FileUtils.rm_rf(attributes_path)
+ end
+ end
+
+ context "with .gitattrbutes" do
+ before(:each) do
+ repository.copy_gitattributes("gitattributes")
+ end
+
+ it "has an info/attributes" do
+ expect(File.exist?(attributes_path)).to be_truthy
+ end
+
+ it "has the same content in info/attributes as .gitattributes" do
+ contents = File.open(attributes_path, "rb") { |f| f.read }
+ expect(contents).to eq("*.md binary\n")
+ end
+
+ after(:each) do
+ FileUtils.rm_rf(attributes_path)
+ end
+ end
+
+ context "with updated .gitattrbutes" do
+ before(:each) do
+ repository.copy_gitattributes("gitattributes")
+ repository.copy_gitattributes("gitattributes-updated")
+ end
+
+ it "has an info/attributes" do
+ expect(File.exist?(attributes_path)).to be_truthy
+ end
+
+ it "has the updated content in info/attributes" do
+ contents = File.read(attributes_path)
+ expect(contents).to eq("*.txt binary\n")
+ end
+
+ after(:each) do
+ FileUtils.rm_rf(attributes_path)
+ end
+ end
+
+ context "with no .gitattrbutes in HEAD but with previous info/attributes" do
+ before(:each) do
+ repository.copy_gitattributes("gitattributes")
+ repository.copy_gitattributes("master")
+ end
+
+ it "does not have an info/attributes" do
+ expect(File.exist?(attributes_path)).to be_falsey
+ end
+
+ after(:each) do
+ FileUtils.rm_rf(attributes_path)
+ end
+ end
+ end
+
+ describe '#diffable' do
+ info_dir_path = attributes_path = File.join(TEST_REPO_PATH, 'info')
+ attributes_path = File.join(info_dir_path, 'attributes')
+
+ before(:all) do
+ FileUtils.mkdir(info_dir_path) unless File.exist?(info_dir_path)
+ File.write(attributes_path, "*.md -diff\n")
+ end
+
+ it "should return true for files which are text and do not have attributes" do
+ blob = Gitlab::Git::Blob.find(
+ repository,
+ '33bcff41c232a11727ac6d660bd4b0c2ba86d63d',
+ 'LICENSE'
+ )
+ expect(repository.diffable?(blob)).to be_truthy
+ end
+
+ it "should return false for binary files which do not have attributes" do
+ blob = Gitlab::Git::Blob.find(
+ repository,
+ '33bcff41c232a11727ac6d660bd4b0c2ba86d63d',
+ 'files/images/logo-white.png'
+ )
+ expect(repository.diffable?(blob)).to be_falsey
+ end
+
+ it "should return false for text files which have been marked as not being diffable in attributes" do
+ blob = Gitlab::Git::Blob.find(
+ repository,
+ '33bcff41c232a11727ac6d660bd4b0c2ba86d63d',
+ 'README.md'
+ )
+ expect(repository.diffable?(blob)).to be_falsey
+ end
+
+ after(:all) do
+ FileUtils.rm_rf(info_dir_path)
+ end
+ end
+
+ describe '#tag_exists?' do
+ it 'returns true for an existing tag' do
+ tag = repository.tag_names.first
+
+ expect(repository.tag_exists?(tag)).to eq(true)
+ end
+
+ it 'returns false for a non-existing tag' do
+ expect(repository.tag_exists?('v9000')).to eq(false)
+ end
+ end
+
+ describe '#branch_exists?' do
+ it 'returns true for an existing branch' do
+ expect(repository.branch_exists?('master')).to eq(true)
+ end
+
+ it 'returns false for a non-existing branch' do
+ expect(repository.branch_exists?('kittens')).to eq(false)
+ end
+
+ it 'returns false when using an invalid branch name' do
+ expect(repository.branch_exists?('.bla')).to eq(false)
+ end
+ end
+
+ describe '#local_branches' do
+ before(:all) do
+ @repo = Gitlab::Git::Repository.new(TEST_MUTABLE_REPO_PATH)
+ end
+
+ after(:all) do
+ FileUtils.rm_rf(TEST_MUTABLE_REPO_PATH)
+ ensure_seeds
+ end
+
+ it 'returns the local branches' do
+ create_remote_branch('joe', 'remote_branch', 'master')
+ @repo.create_branch('local_branch', 'master')
+
+ expect(@repo.local_branches.any? { |branch| branch.name == 'remote_branch' }).to eq(false)
+ expect(@repo.local_branches.any? { |branch| branch.name == 'local_branch' }).to eq(true)
+ end
+ end
+
+ def create_remote_branch(remote_name, branch_name, source_branch_name)
+ source_branch = @repo.branches.find { |branch| branch.name == source_branch_name }
+ rugged = @repo.rugged
+ rugged.references.create("refs/remotes/#{remote_name}/#{branch_name}", source_branch.dereferenced_target.sha)
+ end
+
+ # Build the options hash that's passed to Rugged::Commit#create
+ def commit_options(repo, index, message)
+ options = {}
+ options[:tree] = index.write_tree(repo)
+ options[:author] = {
+ email: "test@example.com",
+ name: "Test Author",
+ time: Time.gm(2014, "mar", 3, 20, 15, 1)
+ }
+ options[:committer] = {
+ email: "test@example.com",
+ name: "Test Author",
+ time: Time.gm(2014, "mar", 3, 20, 15, 1)
+ }
+ options[:message] ||= message
+ options[:parents] = repo.empty? ? [] : [repo.head.target].compact
+ options[:update_ref] = "HEAD"
+
+ options
+ end
+
+ # Writes a new commit to the repo and returns a Rugged::Commit. Replaces the
+ # contents of CHANGELOG with a single new line of text.
+ def new_commit_edit_old_file(repo)
+ oid = repo.write("I replaced the changelog with this text", :blob)
+ index = repo.index
+ index.read_tree(repo.head.target.tree)
+ index.add(path: "CHANGELOG", oid: oid, mode: 0100644)
+
+ options = commit_options(
+ repo,
+ index,
+ "Edit CHANGELOG in its original location"
+ )
+
+ sha = Rugged::Commit.create(repo, options)
+ repo.lookup(sha)
+ end
+
+ # Writes a new commit to the repo and returns a Rugged::Commit. Replaces the
+ # contents of encoding/CHANGELOG with new text.
+ def new_commit_edit_new_file(repo)
+ oid = repo.write("I'm a new changelog with different text", :blob)
+ index = repo.index
+ index.read_tree(repo.head.target.tree)
+ index.add(path: "encoding/CHANGELOG", oid: oid, mode: 0100644)
+
+ options = commit_options(repo, index, "Edit encoding/CHANGELOG")
+
+ sha = Rugged::Commit.create(repo, options)
+ repo.lookup(sha)
+ end
+
+ # Writes a new commit to the repo and returns a Rugged::Commit. Moves the
+ # CHANGELOG file to the encoding/ directory.
+ def new_commit_move_file(repo)
+ blob_oid = repo.head.target.tree.detect { |i| i[:name] == "CHANGELOG" }[:oid]
+ file_content = repo.lookup(blob_oid).content
+ oid = repo.write(file_content, :blob)
+ index = repo.index
+ index.read_tree(repo.head.target.tree)
+ index.add(path: "encoding/CHANGELOG", oid: oid, mode: 0100644)
+ index.remove("CHANGELOG")
+
+ options = commit_options(repo, index, "Move CHANGELOG to encoding/")
+
+ sha = Rugged::Commit.create(repo, options)
+ repo.lookup(sha)
+ end
+end
diff --git a/spec/lib/gitlab/git/rev_list_spec.rb b/spec/lib/gitlab/git/rev_list_spec.rb
index 1f9c987be0b..d48629a296d 100644
--- a/spec/lib/gitlab/git/rev_list_spec.rb
+++ b/spec/lib/gitlab/git/rev_list_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Gitlab::Git::RevList, lib: true do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
context "validations" do
described_class::ALLOWED_VARIABLES.each do |var|
diff --git a/spec/lib/gitlab/git/tag_spec.rb b/spec/lib/gitlab/git/tag_spec.rb
new file mode 100644
index 00000000000..ad469e94735
--- /dev/null
+++ b/spec/lib/gitlab/git/tag_spec.rb
@@ -0,0 +1,25 @@
+require "spec_helper"
+
+describe Gitlab::Git::Tag, seed_helper: true do
+ let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) }
+
+ describe 'first tag' do
+ let(:tag) { repository.tags.first }
+
+ it { expect(tag.name).to eq("v1.0.0") }
+ it { expect(tag.target).to eq("f4e6814c3e4e7a0de82a9e7cd20c626cc963a2f8") }
+ it { expect(tag.dereferenced_target.sha).to eq("6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9") }
+ it { expect(tag.message).to eq("Release") }
+ end
+
+ describe 'last tag' do
+ let(:tag) { repository.tags.last }
+
+ it { expect(tag.name).to eq("v1.2.1") }
+ it { expect(tag.target).to eq("2ac1f24e253e08135507d0830508febaaccf02ee") }
+ it { expect(tag.dereferenced_target.sha).to eq("fa1b1e6c004a68b7d8763b86455da9e6b23e36d6") }
+ it { expect(tag.message).to eq("Version 1.2.1") }
+ end
+
+ it { expect(repository.tags.size).to eq(SeedRepo::Repo::TAGS.size) }
+end
diff --git a/spec/lib/gitlab/git/tree_spec.rb b/spec/lib/gitlab/git/tree_spec.rb
new file mode 100644
index 00000000000..688e2a75373
--- /dev/null
+++ b/spec/lib/gitlab/git/tree_spec.rb
@@ -0,0 +1,76 @@
+require "spec_helper"
+
+describe Gitlab::Git::Tree, seed_helper: true do
+ context :repo do
+ let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) }
+ let(:tree) { Gitlab::Git::Tree.where(repository, SeedRepo::Commit::ID) }
+
+ it { expect(tree).to be_kind_of Array }
+ it { expect(tree.empty?).to be_falsey }
+ it { expect(tree.select(&:dir?).size).to eq(2) }
+ it { expect(tree.select(&:file?).size).to eq(10) }
+ it { expect(tree.select(&:submodule?).size).to eq(2) }
+
+ describe :dir do
+ let(:dir) { tree.select(&:dir?).first }
+
+ it { expect(dir).to be_kind_of Gitlab::Git::Tree }
+ it { expect(dir.id).to eq('3c122d2b7830eca25235131070602575cf8b41a1') }
+ it { expect(dir.commit_id).to eq(SeedRepo::Commit::ID) }
+ it { expect(dir.name).to eq('encoding') }
+ it { expect(dir.path).to eq('encoding') }
+
+ context :subdir do
+ let(:subdir) { Gitlab::Git::Tree.where(repository, SeedRepo::Commit::ID, 'files').first }
+
+ it { expect(subdir).to be_kind_of Gitlab::Git::Tree }
+ it { expect(subdir.id).to eq('a1e8f8d745cc87e3a9248358d9352bb7f9a0aeba') }
+ it { expect(subdir.commit_id).to eq(SeedRepo::Commit::ID) }
+ it { expect(subdir.name).to eq('html') }
+ it { expect(subdir.path).to eq('files/html') }
+ end
+
+ context :subdir_file do
+ let(:subdir_file) { Gitlab::Git::Tree.where(repository, SeedRepo::Commit::ID, 'files/ruby').first }
+
+ it { expect(subdir_file).to be_kind_of Gitlab::Git::Tree }
+ it { expect(subdir_file.id).to eq('7e3e39ebb9b2bf433b4ad17313770fbe4051649c') }
+ it { expect(subdir_file.commit_id).to eq(SeedRepo::Commit::ID) }
+ it { expect(subdir_file.name).to eq('popen.rb') }
+ it { expect(subdir_file.path).to eq('files/ruby/popen.rb') }
+ end
+ end
+
+ describe :file do
+ let(:file) { tree.select(&:file?).first }
+
+ it { expect(file).to be_kind_of Gitlab::Git::Tree }
+ it { expect(file.id).to eq('dfaa3f97ca337e20154a98ac9d0be76ddd1fcc82') }
+ it { expect(file.commit_id).to eq(SeedRepo::Commit::ID) }
+ it { expect(file.name).to eq('.gitignore') }
+ end
+
+ describe :readme do
+ let(:file) { tree.select(&:readme?).first }
+
+ it { expect(file).to be_kind_of Gitlab::Git::Tree }
+ it { expect(file.name).to eq('README.md') }
+ end
+
+ describe :contributing do
+ let(:file) { tree.select(&:contributing?).first }
+
+ it { expect(file).to be_kind_of Gitlab::Git::Tree }
+ it { expect(file.name).to eq('CONTRIBUTING.md') }
+ end
+
+ describe :submodule do
+ let(:submodule) { tree.select(&:submodule?).first }
+
+ it { expect(submodule).to be_kind_of Gitlab::Git::Tree }
+ it { expect(submodule.id).to eq('79bceae69cb5750d6567b223597999bfa91cb3b9') }
+ it { expect(submodule.commit_id).to eq('570e7b2abdd848b95f2f578043fc23bd6f6fd24d') }
+ it { expect(submodule.name).to eq('gitlab-shell') }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/git/util_spec.rb b/spec/lib/gitlab/git/util_spec.rb
new file mode 100644
index 00000000000..8d43b570e98
--- /dev/null
+++ b/spec/lib/gitlab/git/util_spec.rb
@@ -0,0 +1,16 @@
+require 'spec_helper'
+
+describe Gitlab::Git::Util do
+ describe :count_lines do
+ [
+ ["", 0],
+ ["foo", 1],
+ ["foo\n", 1],
+ ["foo\n\n", 2],
+ ].each do |string, line_count|
+ it "counts #{line_count} lines in #{string.inspect}" do
+ expect(Gitlab::Git::Util.count_lines(string)).to eq(line_count)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb
index f1d0a190002..a55bd4387e0 100644
--- a/spec/lib/gitlab/git_access_spec.rb
+++ b/spec/lib/gitlab/git_access_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe Gitlab::GitAccess, lib: true do
let(:access) { Gitlab::GitAccess.new(actor, project, 'web', authentication_abilities: authentication_abilities) }
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
let(:actor) { user }
let(:authentication_abilities) do
@@ -50,7 +50,7 @@ describe Gitlab::GitAccess, lib: true do
end
end
- describe 'download_access_check' do
+ describe '#check_download_access!' do
subject { access.check('git-upload-pack', '_any') }
describe 'master permissions' do
@@ -82,20 +82,18 @@ describe Gitlab::GitAccess, lib: true do
end
end
- describe 'without acccess to project' do
+ describe 'without access to project' do
context 'pull code' do
it { expect(subject.allowed?).to be_falsey }
end
context 'when project is public' do
- let(:public_project) { create(:project, :public) }
+ let(:public_project) { create(:project, :public, :repository) }
let(:guest_access) { Gitlab::GitAccess.new(nil, public_project, 'web', authentication_abilities: []) }
subject { guest_access.check('git-upload-pack', '_any') }
context 'when repository is enabled' do
it 'give access to download code' do
- public_project.project_feature.update_attribute(:repository_access_level, ProjectFeature::ENABLED)
-
expect(subject.allowed?).to be_truthy
end
end
@@ -112,7 +110,7 @@ describe Gitlab::GitAccess, lib: true do
end
describe 'deploy key permissions' do
- let(:key) { create(:deploy_key) }
+ let(:key) { create(:deploy_key, user: user) }
let(:actor) { key }
context 'pull code' do
@@ -124,19 +122,19 @@ describe Gitlab::GitAccess, lib: true do
context 'when unauthorized' do
context 'from public project' do
- let(:project) { create(:project, :public) }
+ let(:project) { create(:project, :public, :repository) }
it { expect(subject).to be_allowed }
end
context 'from internal project' do
- let(:project) { create(:project, :internal) }
+ let(:project) { create(:project, :internal, :repository) }
it { expect(subject).not_to be_allowed }
end
context 'from private project' do
- let(:project) { create(:project, :internal) }
+ let(:project) { create(:project, :private, :repository) }
it { expect(subject).not_to be_allowed }
end
@@ -148,7 +146,7 @@ describe Gitlab::GitAccess, lib: true do
let(:authentication_abilities) { build_authentication_abilities }
describe 'owner' do
- let(:project) { create(:project, namespace: user.namespace) }
+ let(:project) { create(:project, :repository, namespace: user.namespace) }
context 'pull code' do
it { expect(subject).to be_allowed }
@@ -183,7 +181,7 @@ describe Gitlab::GitAccess, lib: true do
end
end
- describe 'push_access_check' do
+ describe '#check_push_access!' do
before { merge_into_protected_branch }
let(:unprotected_branch) { FFaker::Internet.user_name }
@@ -209,7 +207,13 @@ describe Gitlab::GitAccess, lib: true do
stub_git_hooks
project.repository.add_branch(user, unprotected_branch, 'feature')
target_branch = project.repository.lookup('feature')
- source_branch = project.repository.commit_file(user, FFaker::InternetSE.login_user_name, FFaker::HipsterIpsum.paragraph, FFaker::HipsterIpsum.sentence, unprotected_branch, false)
+ source_branch = project.repository.commit_file(
+ user,
+ FFaker::InternetSE.login_user_name,
+ FFaker::HipsterIpsum.paragraph,
+ message: FFaker::HipsterIpsum.sentence,
+ branch_name: unprotected_branch,
+ update: false)
rugged = project.repository.rugged
author = { email: "email@example.com", time: Time.now, name: "Example Git User" }
@@ -231,7 +235,7 @@ describe Gitlab::GitAccess, lib: true do
permissions_matrix[role].each do |action, allowed|
context action do
- subject { access.push_access_check(changes[action]) }
+ subject { access.send(:check_push_access!, changes[action]) }
it { expect(subject.allowed?).to allowed ? be_truthy : be_falsey }
end
end
@@ -353,30 +357,30 @@ describe Gitlab::GitAccess, lib: true do
end
end
- shared_examples 'can not push code' do
+ shared_examples 'pushing code' do |can|
subject { access.check('git-receive-pack', '_any') }
context 'when project is authorized' do
before { authorize }
- it { expect(subject).not_to be_allowed }
+ it { expect(subject).public_send(can, be_allowed) }
end
context 'when unauthorized' do
context 'to public project' do
- let(:project) { create(:project, :public) }
+ let(:project) { create(:project, :public, :repository) }
it { expect(subject).not_to be_allowed }
end
context 'to internal project' do
- let(:project) { create(:project, :internal) }
+ let(:project) { create(:project, :internal, :repository) }
it { expect(subject).not_to be_allowed }
end
context 'to private project' do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :private, :repository) }
it { expect(subject).not_to be_allowed }
end
@@ -386,7 +390,7 @@ describe Gitlab::GitAccess, lib: true do
describe 'build authentication abilities' do
let(:authentication_abilities) { build_authentication_abilities }
- it_behaves_like 'can not push code' do
+ it_behaves_like 'pushing code', :not_to do
def authorize
project.team << [user, :reporter]
end
@@ -394,12 +398,26 @@ describe Gitlab::GitAccess, lib: true do
end
describe 'deploy key permissions' do
- let(:key) { create(:deploy_key) }
+ let(:key) { create(:deploy_key, user: user, can_push: can_push) }
let(:actor) { key }
- it_behaves_like 'can not push code' do
- def authorize
- key.projects << project
+ context 'when deploy_key can push' do
+ let(:can_push) { true }
+
+ it_behaves_like 'pushing code', :to do
+ def authorize
+ key.projects << project
+ end
+ end
+ end
+
+ context 'when deploy_key cannot push' do
+ let(:can_push) { false }
+
+ it_behaves_like 'pushing code', :not_to do
+ def authorize
+ key.projects << project
+ end
end
end
end
diff --git a/spec/lib/gitlab/git_access_wiki_spec.rb b/spec/lib/gitlab/git_access_wiki_spec.rb
index 578db51631e..1ae293416e4 100644
--- a/spec/lib/gitlab/git_access_wiki_spec.rb
+++ b/spec/lib/gitlab/git_access_wiki_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe Gitlab::GitAccessWiki, lib: true do
let(:access) { Gitlab::GitAccessWiki.new(user, project, 'web', authentication_abilities: authentication_abilities) }
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
let(:authentication_abilities) do
[
@@ -27,7 +27,7 @@ describe Gitlab::GitAccessWiki, lib: true do
['6f6d7e7ed 570e7b2ab refs/heads/master']
end
- describe '#download_access_check' do
+ describe '#access_check_download!' do
subject { access.check('git-upload-pack', '_any') }
before do
@@ -36,8 +36,6 @@ describe Gitlab::GitAccessWiki, lib: true do
context 'when wiki feature is enabled' do
it 'give access to download wiki code' do
- project.project_feature.update_attribute(:wiki_access_level, ProjectFeature::ENABLED)
-
expect(subject.allowed?).to be_truthy
end
end
diff --git a/spec/lib/gitlab/github_import/branch_formatter_spec.rb b/spec/lib/gitlab/github_import/branch_formatter_spec.rb
index 462caa5b5fe..36e7d739f7e 100644
--- a/spec/lib/gitlab/github_import/branch_formatter_spec.rb
+++ b/spec/lib/gitlab/github_import/branch_formatter_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Gitlab::GithubImport::BranchFormatter, lib: true do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:commit) { create(:commit, project: project) }
let(:repo) { double }
let(:raw) do
diff --git a/spec/lib/gitlab/github_import/comment_formatter_spec.rb b/spec/lib/gitlab/github_import/comment_formatter_spec.rb
index c520a9c53ad..e6e33d3686a 100644
--- a/spec/lib/gitlab/github_import/comment_formatter_spec.rb
+++ b/spec/lib/gitlab/github_import/comment_formatter_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Gitlab::GithubImport::CommentFormatter, lib: true do
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project) }
let(:octocat) { double(id: 123456, login: 'octocat') }
let(:created_at) { DateTime.strptime('2013-04-10T20:09:31Z') }
let(:updated_at) { DateTime.strptime('2014-03-03T18:58:10Z') }
diff --git a/spec/lib/gitlab/github_import/importer_spec.rb b/spec/lib/gitlab/github_import/importer_spec.rb
index 72421832ffc..afd78abdc9b 100644
--- a/spec/lib/gitlab/github_import/importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer_spec.rb
@@ -202,7 +202,7 @@ describe Gitlab::GithubImport::Importer, lib: true do
end
end
- let(:project) { create(:project, import_url: "#{repo_root}/octocat/Hello-World.git", wiki_access_level: ProjectFeature::DISABLED) }
+ let(:project) { create(:project, :wiki_disabled, import_url: "#{repo_root}/octocat/Hello-World.git") }
let(:credentials) { { user: 'joe' } }
context 'when importing a GitHub project' do
diff --git a/spec/lib/gitlab/github_import/issue_formatter_spec.rb b/spec/lib/gitlab/github_import/issue_formatter_spec.rb
index e31ed9c1fa0..eec1fabab54 100644
--- a/spec/lib/gitlab/github_import/issue_formatter_spec.rb
+++ b/spec/lib/gitlab/github_import/issue_formatter_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Gitlab::GithubImport::IssueFormatter, lib: true do
- let!(:project) { create(:project, namespace: create(:namespace, path: 'octocat')) }
+ let!(:project) { create(:empty_project, namespace: create(:namespace, path: 'octocat')) }
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') }
diff --git a/spec/lib/gitlab/github_import/label_formatter_spec.rb b/spec/lib/gitlab/github_import/label_formatter_spec.rb
index 8098754d735..10449ef5fcb 100644
--- a/spec/lib/gitlab/github_import/label_formatter_spec.rb
+++ b/spec/lib/gitlab/github_import/label_formatter_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Gitlab::GithubImport::LabelFormatter, lib: true do
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project) }
let(:raw) { double(name: 'improvements', color: 'e6e6e6') }
subject { described_class.new(project, raw) }
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 2b3256edcb2..90947ff4707 100644
--- a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb
+++ b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:source_sha) { create(:commit, project: project).id }
let(:target_sha) { create(:commit, project: project, git_commit: RepoHelpers.another_sample_commit).id }
let(:repository) { double(id: 1, fork: false) }
diff --git a/spec/lib/gitlab/github_import/release_formatter_spec.rb b/spec/lib/gitlab/github_import/release_formatter_spec.rb
index 793128c6ab9..13b15e669ab 100644
--- a/spec/lib/gitlab/github_import/release_formatter_spec.rb
+++ b/spec/lib/gitlab/github_import/release_formatter_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Gitlab::GithubImport::ReleaseFormatter, lib: true do
- let!(:project) { create(:project, namespace: create(:namespace, path: 'octocat')) }
+ let!(:project) { create(:empty_project, namespace: create(:namespace, path: 'octocat')) }
let(:octocat) { double(id: 123456, login: 'octocat') }
let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') }
diff --git a/spec/lib/gitlab/google_code_import/importer_spec.rb b/spec/lib/gitlab/google_code_import/importer_spec.rb
index 097861fd34d..ccaa88a5c79 100644
--- a/spec/lib/gitlab/google_code_import/importer_spec.rb
+++ b/spec/lib/gitlab/google_code_import/importer_spec.rb
@@ -10,7 +10,7 @@ describe Gitlab::GoogleCodeImport::Importer, lib: true do
'user_map' => { 'thilo...' => "@#{mapped_user.username}" }
}
end
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project) }
subject { described_class.new(project) }
diff --git a/spec/lib/gitlab/graphs/commits_spec.rb b/spec/lib/gitlab/graphs/commits_spec.rb
index f5c064303ad..abb5a26060f 100644
--- a/spec/lib/gitlab/graphs/commits_spec.rb
+++ b/spec/lib/gitlab/graphs/commits_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Gitlab::Graphs::Commits, lib: true do
- let!(:project) { create(:project, :public, :empty_repo) }
+ let!(:project) { create(:empty_project, :public) }
let!(:commit1) { create(:commit, git_commit: RepoHelpers.sample_commit, project: project, committed_date: Time.now) }
let!(:commit1_yesterday) { create(:commit, git_commit: RepoHelpers.sample_commit, project: project, committed_date: 1.day.ago)}
diff --git a/spec/lib/gitlab/highlight_spec.rb b/spec/lib/gitlab/highlight_spec.rb
index fc021416d92..e177d883158 100644
--- a/spec/lib/gitlab/highlight_spec.rb
+++ b/spec/lib/gitlab/highlight_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe Gitlab::Highlight, lib: true do
include RepoHelpers
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:repository) { project.repository }
let(:commit) { project.commit(sample_commit.id) }
@@ -12,7 +12,7 @@ describe Gitlab::Highlight, lib: true do
Gitlab::Highlight.highlight_lines(project.repository, commit.id, 'files/ruby/popen.rb')
end
- it 'should properly highlight all the lines' do
+ it 'highlights all the lines properly' do
expect(lines[4]).to eq(%Q{<span id="LC5" class="line"> <span class="kp">extend</span> <span class="nb">self</span></span>\n})
expect(lines[21]).to eq(%Q{<span id="LC22" class="line"> <span class="k">unless</span> <span class="no">File</span><span class="p">.</span><span class="nf">directory?</span><span class="p">(</span><span class="n">path</span><span class="p">)</span></span>\n})
expect(lines[26]).to eq(%Q{<span id="LC27" class="line"> <span class="vi">@cmd_status</span> <span class="o">=</span> <span class="mi">0</span></span>\n})
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index 7e618e2fcf5..06617f3b007 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -15,6 +15,7 @@ issues:
- events
- merge_requests_closing_issues
- metrics
+- timelogs
events:
- author
- project
@@ -51,6 +52,7 @@ snippets:
- project
- notes
- award_emoji
+- user_agent_detail
releases:
- project
project_members:
@@ -77,6 +79,7 @@ merge_requests:
- events
- merge_requests_closing_issues
- metrics
+- timelogs
merge_request_diff:
- merge_request
pipelines:
@@ -137,8 +140,8 @@ project:
- assembla_service
- asana_service
- gemnasium_service
-- slack_notification_service
-- mattermost_notification_service
+- slack_service
+- mattermost_service
- buildkite_service
- bamboo_service
- teamcity_service
@@ -189,11 +192,17 @@ project:
- environments
- deployments
- project_feature
+- pages_domains
- authorized_users
- project_authorizations
- route
+- statistics
award_emoji:
- awardable
- user
priorities:
- label
+timelogs:
+- issue
+- merge_request
+- user
diff --git a/spec/lib/gitlab/import_export/import_export_spec.rb b/spec/lib/gitlab/import_export/import_export_spec.rb
index d6409a29550..53f7d244d88 100644
--- a/spec/lib/gitlab/import_export/import_export_spec.rb
+++ b/spec/lib/gitlab/import_export/import_export_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe Gitlab::ImportExport, services: true do
describe 'export filename' do
- let(:project) { create(:project, :public, path: 'project-path') }
+ let(:project) { create(:empty_project, :public, path: 'project-path') }
it 'contains the project path' do
expect(described_class.export_filename(project: project)).to include(project.path)
diff --git a/spec/lib/gitlab/import_export/members_mapper_spec.rb b/spec/lib/gitlab/import_export/members_mapper_spec.rb
index 1cb02f8e318..b9d4e59e770 100644
--- a/spec/lib/gitlab/import_export/members_mapper_spec.rb
+++ b/spec/lib/gitlab/import_export/members_mapper_spec.rb
@@ -2,8 +2,8 @@ require 'spec_helper'
describe Gitlab::ImportExport::MembersMapper, services: true do
describe 'map members' do
- let(:user) { create(:user, authorized_projects_populated: true) }
- let(:project) { create(:project, :public, name: 'searchable_project') }
+ let(:user) { create(:admin, authorized_projects_populated: true) }
+ let(:project) { create(:empty_project, :public, name: 'searchable_project') }
let(:user2) { create(:user, authorized_projects_populated: true) }
let(:exported_user_id) { 99 }
let(:exported_members) do
@@ -24,7 +24,7 @@ describe Gitlab::ImportExport::MembersMapper, services: true do
{
"id" => exported_user_id,
"email" => user2.email,
- "username" => user2.username
+ "username" => 'test'
}
},
{
@@ -48,6 +48,10 @@ describe Gitlab::ImportExport::MembersMapper, services: true do
exported_members: exported_members, user: user, project: project)
end
+ it 'includes the exported user ID in the map' do
+ expect(members_mapper.map.keys).to include(exported_user_id)
+ end
+
it 'maps a project member' do
expect(members_mapper.map[exported_user_id]).to eq(user2.id)
end
@@ -56,12 +60,6 @@ describe Gitlab::ImportExport::MembersMapper, services: true do
expect(members_mapper.map[-1]).to eq(user.id)
end
- it 'updates missing author IDs on missing project member' do
- members_mapper.map[-1]
-
- expect(members_mapper.missing_author_ids.first).to eq(-1)
- end
-
it 'has invited members with no user' do
members_mapper.map
@@ -74,5 +72,71 @@ describe Gitlab::ImportExport::MembersMapper, services: true do
expect(user.authorized_project?(project)).to be true
expect(user2.authorized_project?(project)).to be true
end
+
+ context 'user is not an admin' do
+ let(:user) { create(:user, authorized_projects_populated: true) }
+
+ it 'does not map a project member' do
+ expect(members_mapper.map[exported_user_id]).to eq(user.id)
+ end
+
+ it 'defaults to importer project member if it does not exist' do
+ expect(members_mapper.map[-1]).to eq(user.id)
+ end
+ end
+
+ context 'chooses the one with an email first' do
+ let(:user3) { create(:user, username: 'test') }
+
+ it 'maps the project member that has a matching email first' do
+ expect(members_mapper.map[exported_user_id]).to eq(user2.id)
+ end
+ end
+
+ context 'importer same as group member' do
+ let(:user2) { create(:admin, authorized_projects_populated: true) }
+ let(:group) { create(:group) }
+ let(:project) { create(:empty_project, :public, name: 'searchable_project', namespace: group) }
+ let(:members_mapper) do
+ described_class.new(
+ exported_members: exported_members, user: user2, project: project)
+ end
+
+ before do
+ group.add_users([user, user2], GroupMember::DEVELOPER)
+ end
+
+ it 'maps the project member' do
+ expect(members_mapper.map[exported_user_id]).to eq(user2.id)
+ end
+
+ it 'maps the project member if it already exists' do
+ project.add_master(user2)
+
+ expect(members_mapper.map[exported_user_id]).to eq(user2.id)
+ end
+ end
+
+ context 'importing group members' do
+ let(:group) { create(:group) }
+ let(:project) { create(:empty_project, namespace: group) }
+ let(:members_mapper) do
+ described_class.new(
+ exported_members: exported_members, user: user, project: project)
+ end
+
+ before do
+ group.add_users([user, user2], GroupMember::DEVELOPER)
+ user.update(email: 'invite@test.com')
+ end
+
+ it 'maps the importer' do
+ expect(members_mapper.map[-1]).to eq(user.id)
+ end
+
+ it 'maps the group member' do
+ expect(members_mapper.map[exported_user_id]).to eq(user2.id)
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json
index 931d426c87f..2e9f60432b4 100644
--- a/spec/lib/gitlab/import_export/project.json
+++ b/spec/lib/gitlab/import_export/project.json
@@ -15,6 +15,28 @@
"type": "ProjectLabel",
"priorities": [
]
+ },
+ {
+ "id": 3,
+ "title": "test3",
+ "color": "#428bca",
+ "group_id": 8,
+ "created_at": "2016-07-22T08:55:44.161Z",
+ "updated_at": "2016-07-22T08:55:44.161Z",
+ "template": false,
+ "description": "",
+ "project_id": null,
+ "type": "GroupLabel",
+ "priorities": [
+ {
+ "id": 1,
+ "project_id": 5,
+ "label_id": 1,
+ "priority": 1,
+ "created_at": "2016-10-18T09:35:43.338Z",
+ "updated_at": "2016-10-18T09:35:43.338Z"
+ }
+ ]
}
],
"issues": [
@@ -6959,11 +6981,16 @@
]
}
],
- "variables": [
-
- ],
"triggers": [
-
+ {
+ "id": 123,
+ "token": "cdbfasdf44a5958c83654733449e585",
+ "project_id": null,
+ "deleted_at": null,
+ "created_at": "2017-01-16T15:25:28.637Z",
+ "updated_at": "2017-01-16T15:25:28.637Z",
+ "gl_project_id": 123
+ }
],
"deploy_keys": [
diff --git a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
index 4b07fa53bf5..0af13ba8e47 100644
--- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
@@ -6,7 +6,7 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do
let(:user) { create(:user) }
let(:namespace) { create(:namespace, owner: user) }
let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: "", project_path: 'path') }
- let!(:project) { create(:empty_project, name: 'project', path: 'project', builds_access_level: ProjectFeature::DISABLED, issues_access_level: ProjectFeature::DISABLED) }
+ let!(:project) { create(:empty_project, :builds_disabled, :issues_disabled, name: 'project', path: 'project') }
let(:project_tree_restorer) { described_class.new(user: user, shared: shared, project: project) }
let(:restored_project_json) { project_tree_restorer.restore }
@@ -121,13 +121,13 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do
end
context 'with group' do
- let!(:project) do
+ let!(:project) do
create(:empty_project,
- name: 'project',
- path: 'project',
- builds_access_level: ProjectFeature::DISABLED,
- issues_access_level: ProjectFeature::DISABLED,
- group: create(:group))
+ :builds_disabled,
+ :issues_disabled,
+ name: 'project',
+ path: 'project',
+ group: create(:group))
end
it 'has group labels' do
@@ -197,6 +197,20 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do
expect(restored_project_json).to be true
end
end
+
+ context 'tokens are regenerated' do
+ before do
+ restored_project_json
+ end
+
+ it 'has a new CI trigger token' do
+ expect(Ci::Trigger.where(token: 'cdbfasdf44a5958c83654733449e585')).to be_empty
+ end
+
+ it 'has a new CI build token' do
+ expect(Ci::Build.where(token: 'abcd')).to be_empty
+ end
+ end
end
end
end
diff --git a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
index c8bba553558..3628adefc0c 100644
--- a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe Gitlab::ImportExport::ProjectTreeSaver, services: true do
describe 'saves the project tree into a json object' do
let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: project.path_with_namespace) }
- let(:project_tree_saver) { described_class.new(project: project, shared: shared) }
+ let(:project_tree_saver) { described_class.new(project: project, current_user: user, shared: shared) }
let(:export_path) { "#{Dir::tmpdir}/project_tree_saver_spec" }
let(:user) { create(:user) }
let(:project) { setup_project }
@@ -92,7 +92,7 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do
end
it 'has pipeline builds' do
- expect(saved_project_json['pipelines'].first['statuses'].count { |hash| hash['type'] == 'Ci::Build'}).to eq(1)
+ expect(saved_project_json['pipelines'].first['statuses'].count { |hash| hash['type'] == 'Ci::Build' }).to eq(1)
end
it 'has pipeline commits' do
@@ -112,13 +112,13 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do
end
it 'has project and group labels' do
- label_types = saved_project_json['issues'].first['label_links'].map { |link| link['label']['type']}
+ label_types = saved_project_json['issues'].first['label_links'].map { |link| link['label']['type'] }
expect(label_types).to match_array(['ProjectLabel', 'GroupLabel'])
end
it 'has priorities associated to labels' do
- priorities = saved_project_json['issues'].first['label_links'].map { |link| link['label']['priorities']}
+ priorities = saved_project_json['issues'].first['label_links'].map { |link| link['label']['priorities'] }
expect(priorities.flatten).not_to be_empty
end
@@ -140,6 +140,51 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do
expect(project_tree_saver.save).to be true
end
+
+ context 'group members' do
+ let(:user2) { create(:user, email: 'group@member.com') }
+ let(:member_emails) do
+ saved_project_json['project_members'].map do |pm|
+ pm['user']['email']
+ end
+ end
+
+ before do
+ Group.first.add_developer(user2)
+ end
+
+ it 'does not export group members if it has no permission' do
+ Group.first.add_developer(user)
+
+ expect(member_emails).not_to include('group@member.com')
+ end
+
+ it 'does not export group members as master' do
+ Group.first.add_master(user)
+
+ expect(member_emails).not_to include('group@member.com')
+ end
+
+ it 'exports group members as group owner' do
+ Group.first.add_owner(user)
+
+ expect(member_emails).to include('group@member.com')
+ end
+
+ context 'as admin' do
+ let(:user) { create(:admin) }
+
+ it 'exports group members as admin' do
+ expect(member_emails).to include('group@member.com')
+ end
+
+ it 'exports group members as project members' do
+ member_types = saved_project_json['project_members'].map { |pm| pm['source_type'] }
+
+ expect(member_types).to all(eq('Project'))
+ end
+ end
+ end
end
end
@@ -151,6 +196,10 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do
project = create(:project,
:public,
+ :repository,
+ :issues_disabled,
+ :wiki_enabled,
+ :builds_private,
issues: [issue],
snippets: [snippet],
releases: [release],
@@ -166,10 +215,10 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do
commit_status = create(:commit_status, project: project)
ci_pipeline = create(:ci_pipeline,
- project: project,
- sha: merge_request.diff_head_sha,
- ref: merge_request.source_branch,
- statuses: [commit_status])
+ project: project,
+ sha: merge_request.diff_head_sha,
+ ref: merge_request.source_branch,
+ statuses: [commit_status])
create(:ci_build, pipeline: ci_pipeline, project: project)
create(:milestone, project: project)
@@ -181,13 +230,9 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do
project: project,
commit_id: ci_pipeline.sha)
- create(:event, target: milestone, project: project, action: Event::CREATED, author: user)
+ create(:event, :created, target: milestone, project: project, author: user)
create(:service, project: project, type: 'CustomIssueTrackerService', category: 'issue_tracker')
- project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED)
- project.project_feature.update_attribute(:wiki_access_level, ProjectFeature::ENABLED)
- project.project_feature.update_attribute(:builds_access_level, ProjectFeature::PRIVATE)
-
project
end
diff --git a/spec/lib/gitlab/import_export/reader_spec.rb b/spec/lib/gitlab/import_export/reader_spec.rb
index 3ceb1e7e803..48d74b07e27 100644
--- a/spec/lib/gitlab/import_export/reader_spec.rb
+++ b/spec/lib/gitlab/import_export/reader_spec.rb
@@ -86,6 +86,10 @@ describe Gitlab::ImportExport::Reader, lib: true do
expect(described_class.new(shared: shared).project_tree).to match(include: [{ issues: { methods: [:name] } }])
end
+ it 'generates the correct hash for group members' do
+ expect(described_class.new(shared: shared).group_members_tree).to match({ include: { user: { only: [:email] } } })
+ end
+
def setup_yaml(hash)
allow(YAML).to receive(:load_file).with(test_config).and_return(hash)
end
diff --git a/spec/lib/gitlab/import_export/relation_factory_spec.rb b/spec/lib/gitlab/import_export/relation_factory_spec.rb
index 3aa492a8ab1..57e412b0cef 100644
--- a/spec/lib/gitlab/import_export/relation_factory_spec.rb
+++ b/spec/lib/gitlab/import_export/relation_factory_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe Gitlab::ImportExport::RelationFactory, lib: true do
let(:project) { create(:empty_project) }
let(:members_mapper) { double('members_mapper').as_null_object }
- let(:user) { create(:user) }
+ let(:user) { create(:admin) }
let(:created_object) do
described_class.create(relation_sym: relation_sym,
relation_hash: relation_hash,
@@ -55,8 +55,8 @@ describe Gitlab::ImportExport::RelationFactory, lib: true do
expect(created_object.project_id).to eq(project.id)
end
- it 'has a token' do
- expect(created_object.token).to eq(token)
+ it 'has a nil token' do
+ expect(created_object.token).to eq(nil)
end
context 'original service exists' do
@@ -122,4 +122,71 @@ describe Gitlab::ImportExport::RelationFactory, lib: true do
expect(created_object.values).not_to include(99)
end
end
+
+ context 'Notes user references' do
+ let(:relation_sym) { :notes }
+ let(:new_user) { create(:user) }
+ let(:exported_member) do
+ {
+ "id" => 111,
+ "access_level" => 30,
+ "source_id" => 1,
+ "source_type" => "Project",
+ "user_id" => 3,
+ "notification_level" => 3,
+ "created_at" => "2016-11-18T09:29:42.634Z",
+ "updated_at" => "2016-11-18T09:29:42.634Z",
+ "user" => {
+ "id" => 999,
+ "email" => new_user.email,
+ "username" => new_user.username
+ }
+ }
+ end
+
+ let(:relation_hash) do
+ {
+ "id" => 4947,
+ "note" => "merged",
+ "noteable_type" => "MergeRequest",
+ "author_id" => 999,
+ "created_at" => "2016-11-18T09:29:42.634Z",
+ "updated_at" => "2016-11-18T09:29:42.634Z",
+ "project_id" => 1,
+ "attachment" => {
+ "url" => nil
+ },
+ "noteable_id" => 377,
+ "system" => true,
+ "author" => {
+ "name" => "Administrator"
+ },
+ "events" => [
+
+ ]
+ }
+ end
+
+ let(:members_mapper) do
+ Gitlab::ImportExport::MembersMapper.new(
+ exported_members: [exported_member],
+ user: user,
+ project: project)
+ end
+
+ it 'maps the right author to the imported note' do
+ expect(created_object.author).to eq(new_user)
+ end
+ end
+
+ context 'encrypted attributes' do
+ let(:relation_sym) { 'Ci::Variable' }
+ let(:relation_hash) do
+ create(:ci_variable).as_json
+ end
+
+ it 'has no value for the encrypted attribute' do
+ expect(created_object.value).to be_nil
+ end
+ end
end
diff --git a/spec/lib/gitlab/import_export/repo_bundler_spec.rb b/spec/lib/gitlab/import_export/repo_bundler_spec.rb
index 135e99bc953..d39ea60ff7f 100644
--- a/spec/lib/gitlab/import_export/repo_bundler_spec.rb
+++ b/spec/lib/gitlab/import_export/repo_bundler_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe Gitlab::ImportExport::RepoSaver, services: true do
describe 'bundle a project Git repo' do
let(:user) { create(:user) }
- let!(:project) { create(:project, :public, name: 'searchable_project') }
+ let!(:project) { create(:empty_project, :public, name: 'searchable_project') }
let(:export_path) { "#{Dir::tmpdir}/project_tree_saver_spec" }
let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: project.path_with_namespace) }
let(:bundler) { described_class.new(project: project, shared: shared) }
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index 78d6b2c5032..c5ac702d831 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -20,6 +20,7 @@ Issue:
- lock_version
- milestone_id
- weight
+- time_estimate
Event:
- id
- target_type
@@ -150,6 +151,7 @@ MergeRequest:
- milestone_id
- approvals_before_merge
- rebase_commit_sha
+- time_estimate
MergeRequestDiff:
- id
- state
@@ -220,6 +222,7 @@ CommitStatus:
- queued_at
- token
- lock_version
+- coverage_regex
Ci::Variable:
- id
- project_id
@@ -247,6 +250,8 @@ DeployKey:
- type
- fingerprint
- public
+- can_push
+- last_used_at
Service:
- id
- type
@@ -342,3 +347,11 @@ LabelPriority:
- priority
- created_at
- updated_at
+Timelog:
+- id
+- time_spent
+- merge_request_id
+- issue_id
+- user_id
+- created_at
+- updated_at
diff --git a/spec/lib/gitlab/import_export/wiki_repo_bundler_spec.rb b/spec/lib/gitlab/import_export/wiki_repo_bundler_spec.rb
index b628da0f3e8..47d5d2fc150 100644
--- a/spec/lib/gitlab/import_export/wiki_repo_bundler_spec.rb
+++ b/spec/lib/gitlab/import_export/wiki_repo_bundler_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe Gitlab::ImportExport::WikiRepoSaver, services: true do
describe 'bundle a wiki Git repo' do
let(:user) { create(:user) }
- let!(:project) { create(:project, :public, name: 'searchable_project') }
+ let!(:project) { create(:empty_project, :public, name: 'searchable_project') }
let(:export_path) { "#{Dir::tmpdir}/project_tree_saver_spec" }
let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: project.path_with_namespace) }
let(:wiki_bundler) { described_class.new(project: project, shared: shared) }
diff --git a/spec/lib/gitlab/incoming_email_spec.rb b/spec/lib/gitlab/incoming_email_spec.rb
index 1dcf2c0668b..698bd72d0f8 100644
--- a/spec/lib/gitlab/incoming_email_spec.rb
+++ b/spec/lib/gitlab/incoming_email_spec.rb
@@ -23,6 +23,48 @@ describe Gitlab::IncomingEmail, lib: true do
end
end
+ describe 'self.supports_wildcard?' do
+ context 'address contains the wildard placeholder' do
+ before do
+ stub_incoming_email_setting(address: 'replies+%{key}@example.com')
+ end
+
+ it 'confirms that wildcard is supported' do
+ expect(described_class.supports_wildcard?).to be_truthy
+ end
+ end
+
+ context "address doesn't contain the wildcard placeholder" do
+ before do
+ stub_incoming_email_setting(address: 'replies@example.com')
+ end
+
+ it 'returns that wildcard is not supported' do
+ expect(described_class.supports_wildcard?).to be_falsey
+ end
+ end
+
+ context 'address is not set' do
+ before do
+ stub_incoming_email_setting(address: nil)
+ end
+
+ it 'returns that wildard is not supported' do
+ expect(described_class.supports_wildcard?).to be_falsey
+ end
+ end
+ end
+
+ context 'self.unsubscribe_address' do
+ before do
+ stub_incoming_email_setting(address: 'replies+%{key}@example.com')
+ end
+
+ it 'returns the address with interpolated reply key and unsubscribe suffix' do
+ expect(described_class.unsubscribe_address('key')).to eq('replies+key+unsubscribe@example.com')
+ end
+ end
+
context "self.reply_address" do
before do
stub_incoming_email_setting(address: "replies+%{key}@example.com")
@@ -48,4 +90,19 @@ describe Gitlab::IncomingEmail, lib: true do
expect(described_class.key_from_fallback_message_id('reply-key@localhost')).to eq('key')
end
end
+
+ context 'self.scan_fallback_references' do
+ let(:references) do
+ '<issue_1@localhost>' +
+ ' <reply-59d8df8370b7e95c5a49fbf86aeb2c93@localhost>' +
+ ',<exchange@microsoft.com>'
+ end
+
+ it 'returns reply key' do
+ expect(described_class.scan_fallback_references(references))
+ .to eq(%w[issue_1@localhost
+ reply-59d8df8370b7e95c5a49fbf86aeb2c93@localhost
+ exchange@microsoft.com])
+ end
+ end
end
diff --git a/spec/lib/gitlab/job_waiter_spec.rb b/spec/lib/gitlab/job_waiter_spec.rb
new file mode 100644
index 00000000000..780f5b1f8d7
--- /dev/null
+++ b/spec/lib/gitlab/job_waiter_spec.rb
@@ -0,0 +1,30 @@
+require 'spec_helper'
+
+describe Gitlab::JobWaiter do
+ describe '#wait' do
+ let(:waiter) { described_class.new(%w(a)) }
+ it 'returns when all jobs have been completed' do
+ expect(Gitlab::SidekiqStatus).to receive(:all_completed?).with(%w(a)).
+ and_return(true)
+
+ expect(waiter).not_to receive(:sleep)
+
+ waiter.wait
+ end
+
+ it 'sleeps between checking the job statuses' do
+ expect(Gitlab::SidekiqStatus).to receive(:all_completed?).
+ with(%w(a)).
+ and_return(false, true)
+
+ expect(waiter).to receive(:sleep).with(described_class::INTERVAL)
+
+ waiter.wait
+ end
+
+ it 'returns when timing out' do
+ expect(waiter).not_to receive(:sleep)
+ waiter.wait(0)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ldap/access_spec.rb b/spec/lib/gitlab/ldap/access_spec.rb
index 534bcbf39fe..9dd997aa7dc 100644
--- a/spec/lib/gitlab/ldap/access_spec.rb
+++ b/spec/lib/gitlab/ldap/access_spec.rb
@@ -14,10 +14,10 @@ describe Gitlab::LDAP::Access, lib: true do
it { is_expected.to be_falsey }
- it 'should block user in GitLab' do
+ it 'blocks user in GitLab' do
+ expect(access).to receive(:block_user).with(user, 'does not exist anymore')
+
access.allowed?
- expect(user).to be_blocked
- expect(user).to be_ldap_blocked
end
end
@@ -34,9 +34,9 @@ describe Gitlab::LDAP::Access, lib: true do
it { is_expected.to be_falsey }
it 'blocks user in GitLab' do
+ expect(access).to receive(:block_user).with(user, 'is disabled in Active Directory')
+
access.allowed?
- expect(user).to be_blocked
- expect(user).to be_ldap_blocked
end
end
@@ -53,7 +53,10 @@ describe Gitlab::LDAP::Access, lib: true do
end
it 'does not unblock user in GitLab' do
+ expect(access).not_to receive(:unblock_user)
+
access.allowed?
+
expect(user).to be_blocked
expect(user).not_to be_ldap_blocked # this block is handled by omniauth not by our internal logic
end
@@ -65,8 +68,9 @@ describe Gitlab::LDAP::Access, lib: true do
end
it 'unblocks user in GitLab' do
+ expect(access).to receive(:unblock_user).with(user, 'is not disabled anymore')
+
access.allowed?
- expect(user).not_to be_blocked
end
end
end
@@ -87,9 +91,9 @@ describe Gitlab::LDAP::Access, lib: true do
it { is_expected.to be_falsey }
it 'blocks user in GitLab' do
+ expect(access).to receive(:block_user).with(user, 'does not exist anymore')
+
access.allowed?
- expect(user).to be_blocked
- expect(user).to be_ldap_blocked
end
end
@@ -99,11 +103,54 @@ describe Gitlab::LDAP::Access, lib: true do
end
it 'unblocks the user if it exists' do
+ expect(access).to receive(:unblock_user).with(user, 'is available again')
+
access.allowed?
- expect(user).not_to be_blocked
end
end
end
end
end
+
+ describe '#block_user' do
+ before do
+ user.activate
+ allow(Gitlab::AppLogger).to receive(:info)
+
+ access.block_user user, 'reason'
+ end
+
+ it 'blocks the user' do
+ expect(user).to be_blocked
+ expect(user).to be_ldap_blocked
+ end
+
+ it 'logs the reason' do
+ expect(Gitlab::AppLogger).to have_received(:info).with(
+ "LDAP account \"123456\" reason, " \
+ "blocking Gitlab user \"#{user.name}\" (#{user.email})"
+ )
+ end
+ end
+
+ describe '#unblock_user' do
+ before do
+ user.ldap_block
+ allow(Gitlab::AppLogger).to receive(:info)
+
+ access.unblock_user user, 'reason'
+ end
+
+ it 'activates the user' do
+ expect(user).not_to be_blocked
+ expect(user).not_to be_ldap_blocked
+ end
+
+ it 'logs the reason' do
+ Gitlab::AppLogger.info(
+ "LDAP account \"123456\" reason, " \
+ "unblocking Gitlab user \"#{user.name}\" (#{user.email})"
+ )
+ end
+ end
end
diff --git a/spec/lib/gitlab/ldap/config_spec.rb b/spec/lib/gitlab/ldap/config_spec.rb
index 1a6803e01c3..cab2e9908ff 100644
--- a/spec/lib/gitlab/ldap/config_spec.rb
+++ b/spec/lib/gitlab/ldap/config_spec.rb
@@ -129,4 +129,27 @@ describe Gitlab::LDAP::Config, lib: true do
expect(config.has_auth?).to be_falsey
end
end
+
+ describe '#attributes' do
+ it 'uses default attributes when no custom attributes are configured' do
+ expect(config.attributes).to eq(config.default_attributes)
+ end
+
+ it 'merges the configuration attributes with default attributes' do
+ stub_ldap_config(
+ options: {
+ 'attributes' => {
+ 'username' => %w(sAMAccountName),
+ 'email' => %w(userPrincipalName)
+ }
+ }
+ )
+
+ expect(config.attributes).to include({
+ 'username' => %w(sAMAccountName),
+ 'email' => %w(userPrincipalName),
+ 'name' => 'cn'
+ })
+ end
+ end
end
diff --git a/spec/lib/gitlab/ldap/person_spec.rb b/spec/lib/gitlab/ldap/person_spec.rb
new file mode 100644
index 00000000000..9a556cde5d5
--- /dev/null
+++ b/spec/lib/gitlab/ldap/person_spec.rb
@@ -0,0 +1,46 @@
+require 'spec_helper'
+
+describe Gitlab::LDAP::Person do
+ include LdapHelpers
+
+ let(:entry) { ldap_user_entry('john.doe') }
+
+ before do
+ stub_ldap_config(
+ options: {
+ 'attributes' => {
+ 'name' => 'cn',
+ 'email' => %w(mail email userPrincipalName)
+ }
+ }
+ )
+ end
+
+ describe '#name' do
+ it 'uses the configured name attribute and handles values as an array' do
+ name = 'John Doe'
+ entry['cn'] = [name]
+ person = Gitlab::LDAP::Person.new(entry, 'ldapmain')
+
+ expect(person.name).to eq(name)
+ end
+ end
+
+ describe '#email' do
+ it 'returns the value of mail, if present' do
+ mail = 'john@example.com'
+ entry['mail'] = mail
+ person = Gitlab::LDAP::Person.new(entry, 'ldapmain')
+
+ expect(person.email).to eq([mail])
+ end
+
+ it 'returns the value of userPrincipalName, if mail and email are not present' do
+ user_principal_name = 'john.doe@example.com'
+ entry['userPrincipalName'] = user_principal_name
+ person = Gitlab::LDAP::Person.new(entry, 'ldapmain')
+
+ expect(person.email).to eq([user_principal_name])
+ end
+ end
+end
diff --git a/spec/lib/gitlab/metrics/rack_middleware_spec.rb b/spec/lib/gitlab/metrics/rack_middleware_spec.rb
index bcaffd27909..fb470ea7568 100644
--- a/spec/lib/gitlab/metrics/rack_middleware_spec.rb
+++ b/spec/lib/gitlab/metrics/rack_middleware_spec.rb
@@ -33,7 +33,7 @@ describe Gitlab::Metrics::RackMiddleware do
end
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)")
+ route = double(:route, request_method: "GET", path: "/:version/projects/:id/archive(.:format)")
endpoint = double(:endpoint, route: route)
env['api.endpoint'] = endpoint
@@ -117,7 +117,7 @@ describe Gitlab::Metrics::RackMiddleware do
let(:transaction) { middleware.transaction_from_env(env) }
it 'tags a transaction with the method and path of the route in the grape endpount' do
- route = double(:route, route_method: "GET", route_path: "/:version/projects/:id/archive(.:format)")
+ route = double(:route, request_method: "GET", path: "/:version/projects/:id/archive(.:format)")
endpoint = double(:endpoint, route: route)
env['api.endpoint'] = endpoint
@@ -126,5 +126,16 @@ describe Gitlab::Metrics::RackMiddleware do
expect(transaction.action).to eq('Grape#GET /projects/:id/archive')
end
+
+ it 'does not tag a transaction if route infos are missing' do
+ endpoint = double(:endpoint)
+ allow(endpoint).to receive(:route).and_raise
+
+ env['api.endpoint'] = endpoint
+
+ middleware.tag_endpoint(transaction, env)
+
+ expect(transaction.action).to be_nil
+ end
end
end
diff --git a/spec/lib/gitlab/middleware/multipart_spec.rb b/spec/lib/gitlab/middleware/multipart_spec.rb
index ab1ab22795c..8d925460f01 100644
--- a/spec/lib/gitlab/middleware/multipart_spec.rb
+++ b/spec/lib/gitlab/middleware/multipart_spec.rb
@@ -12,7 +12,7 @@ describe Gitlab::Middleware::Multipart do
expect(app).to receive(:call) do |env|
file = Rack::Request.new(env).params['file']
- expect(file).to be_a(File)
+ expect(file).to be_a(::UploadedFile)
expect(file.path).to eq(tempfile.path)
end
@@ -39,7 +39,7 @@ describe Gitlab::Middleware::Multipart do
expect(app).to receive(:call) do |env|
file = Rack::Request.new(env).params['user']['avatar']
- expect(file).to be_a(File)
+ expect(file).to be_a(::UploadedFile)
expect(file.path).to eq(tempfile.path)
end
@@ -54,7 +54,7 @@ describe Gitlab::Middleware::Multipart do
expect(app).to receive(:call) do |env|
file = Rack::Request.new(env).params['project']['milestone']['themesong']
- expect(file).to be_a(File)
+ expect(file).to be_a(::UploadedFile)
expect(file.path).to eq(tempfile.path)
end
diff --git a/spec/lib/gitlab/project_search_results_spec.rb b/spec/lib/gitlab/project_search_results_spec.rb
index 14ee386dba6..9a8096208db 100644
--- a/spec/lib/gitlab/project_search_results_spec.rb
+++ b/spec/lib/gitlab/project_search_results_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe Gitlab::ProjectSearchResults, lib: true do
let(:user) { create(:user) }
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project) }
let(:query) { 'hello world' }
describe 'initialize with empty ref' do
@@ -22,6 +22,7 @@ describe Gitlab::ProjectSearchResults, lib: true do
end
describe 'blob search' do
+ let(:project) { create(:project, :repository) }
let(:results) { described_class.new(user, project, 'files').objects('blobs') }
it 'finds by name' do
@@ -74,6 +75,7 @@ describe Gitlab::ProjectSearchResults, lib: true do
end
describe 'confidential issues' do
+ let(:project) { create(:empty_project) }
let(:query) { 'issue' }
let(:author) { create(:user) }
let(:assignee) { create(:user) }
@@ -161,7 +163,7 @@ describe Gitlab::ProjectSearchResults, lib: true do
end
it "doesn't list issue notes when access is restricted" do
- project = create(:empty_project, :public, issues_access_level: ProjectFeature::PRIVATE)
+ project = create(:empty_project, :public, :issues_private)
note = create(:note_on_issue, project: project)
results = described_class.new(user, project, note.note)
@@ -170,7 +172,7 @@ describe Gitlab::ProjectSearchResults, lib: true do
end
it "doesn't list merge_request notes when access is restricted" do
- project = create(:empty_project, :public, merge_requests_access_level: ProjectFeature::PRIVATE)
+ project = create(:empty_project, :public, :merge_requests_private)
note = create(:note_on_merge_request, project: project)
results = described_class.new(user, project, note.note)
@@ -178,4 +180,119 @@ describe Gitlab::ProjectSearchResults, lib: true do
expect(results.objects('notes')).not_to include note
end
end
+
+ # Examples for commit access level test
+ #
+ # params:
+ # * search_phrase
+ # * commit
+ #
+ shared_examples 'access restricted commits' do
+ context 'when project is internal' do
+ let(:project) { create(:project, :internal, :repository) }
+
+ it 'does not search if user is not authenticated' do
+ commits = described_class.new(nil, project, search_phrase).objects('commits')
+
+ expect(commits).to be_empty
+ end
+
+ it 'searches if user is authenticated' do
+ commits = described_class.new(user, project, search_phrase).objects('commits')
+
+ expect(commits).to contain_exactly commit
+ end
+ end
+
+ context 'when project is private' do
+ let!(:creator) { create(:user, username: 'private-project-author') }
+ let!(:private_project) { create(:project, :private, :repository, creator: creator, namespace: creator.namespace) }
+ let(:team_master) do
+ user = create(:user, username: 'private-project-master')
+ private_project.team << [user, :master]
+ user
+ end
+ let(:team_reporter) do
+ user = create(:user, username: 'private-project-reporter')
+ private_project.team << [user, :reporter]
+ user
+ end
+
+ it 'does not show commit to stranger' do
+ commits = described_class.new(nil, private_project, search_phrase).objects('commits')
+
+ expect(commits).to be_empty
+ end
+
+ context 'team access' do
+ it 'shows commit to creator' do
+ commits = described_class.new(creator, private_project, search_phrase).objects('commits')
+
+ expect(commits).to contain_exactly commit
+ end
+
+ it 'shows commit to master' do
+ commits = described_class.new(team_master, private_project, search_phrase).objects('commits')
+
+ expect(commits).to contain_exactly commit
+ end
+
+ it 'shows commit to reporter' do
+ commits = described_class.new(team_reporter, private_project, search_phrase).objects('commits')
+
+ expect(commits).to contain_exactly commit
+ end
+ end
+ end
+ end
+
+ describe 'commit search' do
+ context 'by commit message' do
+ let(:project) { create(:project, :public, :repository) }
+ let(:commit) { project.repository.commit('59e29889be61e6e0e5e223bfa9ac2721d31605b8') }
+ let(:message) { 'Sorry, I did a mistake' }
+
+ it 'finds commit by message' do
+ commits = described_class.new(user, project, message).objects('commits')
+
+ expect(commits).to contain_exactly commit
+ end
+
+ it 'handles when no commit match' do
+ commits = described_class.new(user, project, 'not really an existing description').objects('commits')
+
+ expect(commits).to be_empty
+ end
+
+ it_behaves_like 'access restricted commits' do
+ let(:search_phrase) { message }
+ let(:commit) { project.repository.commit('59e29889be61e6e0e5e223bfa9ac2721d31605b8') }
+ end
+ end
+
+ context 'by commit hash' do
+ let(:project) { create(:project, :public, :repository) }
+ let(:commit) { project.repository.commit('0b4bc9a') }
+ commit_hashes = { short: '0b4bc9a', full: '0b4bc9a49b562e85de7cc9e834518ea6828729b9' }
+
+ commit_hashes.each do |type, commit_hash|
+ it "shows commit by #{type} hash id" do
+ commits = described_class.new(user, project, commit_hash).objects('commits')
+
+ expect(commits).to contain_exactly commit
+ end
+ end
+
+ it 'handles not existing commit hash correctly' do
+ commits = described_class.new(user, project, 'deadbeef').objects('commits')
+
+ expect(commits).to be_empty
+ end
+
+ it_behaves_like 'access restricted commits' do
+ let(:search_phrase) { '0b4bc9a49' }
+ let(:commit) { project.repository.commit('0b4bc9a') }
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/project_transfer_spec.rb b/spec/lib/gitlab/project_transfer_spec.rb
new file mode 100644
index 00000000000..e2d6b1b9ab7
--- /dev/null
+++ b/spec/lib/gitlab/project_transfer_spec.rb
@@ -0,0 +1,51 @@
+require 'spec_helper'
+
+describe Gitlab::ProjectTransfer, lib: true do
+ before do
+ @root_dir = File.join(Rails.root, "public", "uploads")
+ @project_transfer = Gitlab::ProjectTransfer.new
+ allow(@project_transfer).to receive(:root_dir).and_return(@root_dir)
+
+ @project_path_was = "test_project_was"
+ @project_path = "test_project"
+ @namespace_path_was = "test_namespace_was"
+ @namespace_path = "test_namespace"
+ end
+
+ after do
+ FileUtils.rm_rf([
+ File.join(@root_dir, @namespace_path),
+ File.join(@root_dir, @namespace_path_was)
+ ])
+ end
+
+ describe '#move_project' do
+ it "moves project upload to another namespace" do
+ FileUtils.mkdir_p(File.join(@root_dir, @namespace_path_was, @project_path))
+ @project_transfer.move_project(@project_path, @namespace_path_was, @namespace_path)
+
+ expected_path = File.join(@root_dir, @namespace_path, @project_path)
+ expect(Dir.exist?(expected_path)).to be_truthy
+ end
+ end
+
+ describe '#rename_project' do
+ it "renames project" do
+ FileUtils.mkdir_p(File.join(@root_dir, @namespace_path, @project_path_was))
+ @project_transfer.rename_project(@project_path_was, @project_path, @namespace_path)
+
+ expected_path = File.join(@root_dir, @namespace_path, @project_path)
+ expect(Dir.exist?(expected_path)).to be_truthy
+ end
+ end
+
+ describe '#rename_namespace' do
+ it "renames namespace" do
+ FileUtils.mkdir_p(File.join(@root_dir, @namespace_path_was, @project_path))
+ @project_transfer.rename_namespace(@namespace_path_was, @namespace_path)
+
+ expected_path = File.join(@root_dir, @namespace_path, @project_path)
+ expect(Dir.exist?(expected_path)).to be_truthy
+ end
+ end
+end
diff --git a/spec/lib/gitlab/redis_spec.rb b/spec/lib/gitlab/redis_spec.rb
index e5406fb2d33..917c5c46db1 100644
--- a/spec/lib/gitlab/redis_spec.rb
+++ b/spec/lib/gitlab/redis_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Gitlab::Redis do
- let(:redis_config) { Rails.root.join('config', 'resque.yml').to_s }
+ include StubENV
before(:each) { clear_raw_config }
after(:each) { clear_raw_config }
@@ -72,6 +72,20 @@ describe Gitlab::Redis do
expect(url2).not_to end_with('foobar')
end
+
+ context 'when yml file with env variable' do
+ let(:redis_config) { Rails.root.join('spec/fixtures/config/redis_config_with_env.yml') }
+
+ before do
+ stub_env('TEST_GITLAB_REDIS_URL', 'redis://redishost:6379')
+ end
+
+ it 'reads redis url from env variable' do
+ stub_const("#{described_class}::CONFIG_FILE", redis_config)
+
+ expect(described_class.url).to eq 'redis://redishost:6379'
+ end
+ end
end
describe '._raw_config' do
diff --git a/spec/lib/gitlab/reference_extractor_spec.rb b/spec/lib/gitlab/reference_extractor_spec.rb
index bf0ab9635fd..6b689c41ef6 100644
--- a/spec/lib/gitlab/reference_extractor_spec.rb
+++ b/spec/lib/gitlab/reference_extractor_spec.rb
@@ -1,9 +1,11 @@
require 'spec_helper'
describe Gitlab::ReferenceExtractor, lib: true do
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project) }
- before { project.team << [project.creator, :developer] }
+ before do
+ project.team << [project.creator, :developer]
+ end
subject { Gitlab::ReferenceExtractor.new(project, project.creator) }
@@ -78,22 +80,27 @@ describe Gitlab::ReferenceExtractor, lib: true do
end
it 'accesses valid commits' do
+ project = create(:project, :repository) { |p| p.add_developer(p.creator) }
commit = project.commit('master')
- subject.analyze("this references commits #{commit.sha[0..6]} and 012345")
- extracted = subject.commits
+ extractor = described_class.new(project, project.creator)
+ extractor.analyze("this references commits #{commit.sha[0..6]} and 012345")
+ extracted = extractor.commits
+
expect(extracted.size).to eq(1)
expect(extracted[0].sha).to eq(commit.sha)
expect(extracted[0].message).to eq(commit.message)
end
it 'accesses valid commit ranges' do
+ project = create(:project, :repository) { |p| p.add_developer(p.creator) }
commit = project.commit('master')
earlier_commit = project.commit('master~2')
- subject.analyze("this references commits #{earlier_commit.sha[0..6]}...#{commit.sha[0..6]}")
+ extractor = described_class.new(project, project.creator)
+ extractor.analyze("this references commits #{earlier_commit.sha[0..6]}...#{commit.sha[0..6]}")
+ extracted = extractor.commit_ranges
- extracted = subject.commit_ranges
expect(extracted.size).to eq(1)
expect(extracted.first).to be_kind_of(CommitRange)
expect(extracted.first.commit_from).to eq earlier_commit
@@ -102,7 +109,6 @@ describe Gitlab::ReferenceExtractor, lib: true do
context 'with an external issue tracker' do
let(:project) { create(:jira_project) }
- subject { described_class.new(project, project.creator) }
it 'returns JIRA issues for a JIRA-integrated project' do
subject.analyze('JIRA-123 and FOOBAR-4567')
@@ -112,7 +118,7 @@ describe Gitlab::ReferenceExtractor, lib: true do
end
context 'with a project with an underscore' do
- let(:other_project) { create(:project, path: 'test_project') }
+ let(:other_project) { create(:empty_project, path: 'test_project') }
let(:issue) { create(:issue, project: other_project) }
before do
diff --git a/spec/lib/gitlab/route_map_spec.rb b/spec/lib/gitlab/route_map_spec.rb
new file mode 100644
index 00000000000..2370f56a613
--- /dev/null
+++ b/spec/lib/gitlab/route_map_spec.rb
@@ -0,0 +1,90 @@
+require 'spec_helper'
+
+describe Gitlab::RouteMap, lib: true do
+ describe '#initialize' do
+ context 'when the data is not YAML' do
+ it 'raises an error' do
+ expect { described_class.new('"') }.
+ to raise_error(Gitlab::RouteMap::FormatError, /valid YAML/)
+ end
+ end
+
+ context 'when the data is not a YAML array' do
+ it 'raises an error' do
+ expect { described_class.new(YAML.dump('foo')) }.
+ to raise_error(Gitlab::RouteMap::FormatError, /an array/)
+ end
+ end
+
+ context 'when an entry is not a hash' do
+ it 'raises an error' do
+ expect { described_class.new(YAML.dump(['foo'])) }.
+ to raise_error(Gitlab::RouteMap::FormatError, /a hash/)
+ end
+ end
+
+ context 'when an entry does not have a source key' do
+ it 'raises an error' do
+ expect { described_class.new(YAML.dump([{ 'public' => 'index.html' }])) }.
+ to raise_error(Gitlab::RouteMap::FormatError, /source key/)
+ end
+ end
+
+ context 'when an entry does not have a public key' do
+ it 'raises an error' do
+ expect { described_class.new(YAML.dump([{ 'source' => '/index\.html/' }])) }.
+ to raise_error(Gitlab::RouteMap::FormatError, /public key/)
+ end
+ end
+
+ context 'when an entry source is not a valid regex' do
+ it 'raises an error' do
+ expect { described_class.new(YAML.dump([{ 'source' => '/[/', 'public' => 'index.html' }])) }.
+ to raise_error(Gitlab::RouteMap::FormatError, /regular expression/)
+ end
+ end
+
+ context 'when all is good' do
+ it 'returns a route map' do
+ route_map = described_class.new(YAML.dump([{ 'source' => 'index.haml', 'public' => 'index.html' }, { 'source' => '/(.*)\.md/', 'public' => '\1.html' }]))
+
+ expect(route_map.public_path_for_source_path('index.haml')).to eq('index.html')
+ expect(route_map.public_path_for_source_path('foo.md')).to eq('foo.html')
+ end
+ end
+ end
+
+ describe '#public_path_for_source_path' do
+ subject do
+ described_class.new(<<-'MAP'.strip_heredoc)
+ # Team data
+ - source: 'data/team.yml'
+ public: 'team/'
+
+ # Blogposts
+ - source: /source/posts/([0-9]{4})-([0-9]{2})-([0-9]{2})-(.+?)\..*/ # source/posts/2017-01-30-around-the-world-in-6-releases.html.md.erb
+ public: '\1/\2/\3/\4/' # 2017/01/30/around-the-world-in-6-releases/
+
+ # HTML files
+ - source: /source/(.+?\.html).*/ # source/index.html.haml
+ public: '\1' # index.html
+
+ # Other files
+ - source: /source/(.*)/ # source/images/blogimages/around-the-world-in-6-releases-cover.png
+ public: '\1' # images/blogimages/around-the-world-in-6-releases-cover.png
+ MAP
+ end
+
+ it 'returns the public path for a provided source path' do
+ expect(subject.public_path_for_source_path('data/team.yml')).to eq('team/')
+
+ expect(subject.public_path_for_source_path('source/posts/2017-01-30-around-the-world-in-6-releases.html.md.erb')).to eq('2017/01/30/around-the-world-in-6-releases/')
+
+ expect(subject.public_path_for_source_path('source/index.html.haml')).to eq('index.html')
+
+ expect(subject.public_path_for_source_path('source/images/blogimages/around-the-world-in-6-releases-cover.png')).to eq('images/blogimages/around-the-world-in-6-releases-cover.png')
+
+ expect(subject.public_path_for_source_path('.gitlab/route-map.yml')).to be_nil
+ end
+ end
+end
diff --git a/spec/lib/gitlab/search_results_spec.rb b/spec/lib/gitlab/search_results_spec.rb
index 9614aad3e73..847fb977400 100644
--- a/spec/lib/gitlab/search_results_spec.rb
+++ b/spec/lib/gitlab/search_results_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe Gitlab::SearchResults do
let(:user) { create(:user) }
- let!(:project) { create(:project, name: 'foo') }
+ let!(:project) { create(:empty_project, name: 'foo') }
let!(:issue) { create(:issue, project: project, title: 'foo') }
let!(:merge_request) do
diff --git a/spec/lib/gitlab/serialize/ci/variables_spec.rb b/spec/lib/gitlab/serialize/ci/variables_spec.rb
deleted file mode 100644
index 7ea74da5252..00000000000
--- a/spec/lib/gitlab/serialize/ci/variables_spec.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::Serialize::Ci::Variables do
- subject do
- described_class.load(described_class.dump(object))
- end
-
- let(:object) do
- [{ key: :key, value: 'value', public: true },
- { key: 'wee', value: 1, public: false }]
- end
-
- it 'converts keys into strings' do
- is_expected.to eq([
- { key: 'key', value: 'value', public: true },
- { key: 'wee', value: 1, public: false }])
- end
-end
diff --git a/spec/lib/gitlab/serializer/ci/variables_spec.rb b/spec/lib/gitlab/serializer/ci/variables_spec.rb
new file mode 100644
index 00000000000..b810c68ea03
--- /dev/null
+++ b/spec/lib/gitlab/serializer/ci/variables_spec.rb
@@ -0,0 +1,18 @@
+require 'spec_helper'
+
+describe Gitlab::Serializer::Ci::Variables do
+ subject do
+ described_class.load(described_class.dump(object))
+ end
+
+ let(:object) do
+ [{ key: :key, value: 'value', public: true },
+ { key: 'wee', value: 1, public: false }]
+ end
+
+ it 'converts keys into strings' do
+ is_expected.to eq([
+ { key: 'key', value: 'value', public: true },
+ { key: 'wee', value: 1, public: false }])
+ end
+end
diff --git a/spec/lib/gitlab/serializer/pagination_spec.rb b/spec/lib/gitlab/serializer/pagination_spec.rb
new file mode 100644
index 00000000000..519eb1b274f
--- /dev/null
+++ b/spec/lib/gitlab/serializer/pagination_spec.rb
@@ -0,0 +1,49 @@
+require 'spec_helper'
+
+describe Gitlab::Serializer::Pagination do
+ let(:request) { spy('request') }
+ let(:response) { spy('response') }
+ let(:headers) { spy('headers') }
+
+ before do
+ allow(request).to receive(:query_parameters)
+ .and_return(params)
+
+ allow(response).to receive(:headers)
+ .and_return(headers)
+ end
+
+ let(:pagination) { described_class.new(request, response) }
+
+ describe '#paginate' do
+ subject { pagination.paginate(resource) }
+
+ let(:resource) { User.all }
+ let(:params) { { page: 1, per_page: 2 } }
+
+ context 'when a multiple resources are present in relation' do
+ before { create_list(:user, 3) }
+
+ it 'correctly paginates the resource' do
+ expect(subject.count).to be 2
+ end
+
+ it 'appends relevant headers' do
+ expect(headers).to receive(:[]=).with('X-Total', '3')
+ expect(headers).to receive(:[]=).with('X-Total-Pages', '2')
+ expect(headers).to receive(:[]=).with('X-Per-Page', '2')
+
+ subject
+ end
+ end
+
+ context 'when an invalid resource is about to be paginated' do
+ let(:resource) { create(:user) }
+
+ it 'raises error' do
+ expect { subject }.to raise_error(
+ described_class::InvalidResourceError)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/sidekiq_status/client_middleware_spec.rb b/spec/lib/gitlab/sidekiq_status/client_middleware_spec.rb
new file mode 100644
index 00000000000..287bf62d9bd
--- /dev/null
+++ b/spec/lib/gitlab/sidekiq_status/client_middleware_spec.rb
@@ -0,0 +1,12 @@
+require 'spec_helper'
+
+describe Gitlab::SidekiqStatus::ClientMiddleware do
+ describe '#call' do
+ it 'tracks the job in Redis' do
+ expect(Gitlab::SidekiqStatus).to receive(:set).with('123')
+
+ described_class.new.
+ call('Foo', { 'jid' => '123' }, double(:queue), double(:pool)) { nil }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/sidekiq_status/server_middleware_spec.rb b/spec/lib/gitlab/sidekiq_status/server_middleware_spec.rb
new file mode 100644
index 00000000000..80728197b8c
--- /dev/null
+++ b/spec/lib/gitlab/sidekiq_status/server_middleware_spec.rb
@@ -0,0 +1,14 @@
+require 'spec_helper'
+
+describe Gitlab::SidekiqStatus::ServerMiddleware do
+ describe '#call' do
+ it 'stops tracking of a job upon completion' do
+ expect(Gitlab::SidekiqStatus).to receive(:unset).with('123')
+
+ ret = described_class.new.
+ call(double(:worker), { 'jid' => '123' }, double(:queue)) { 10 }
+
+ expect(ret).to eq(10)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/sidekiq_status_spec.rb b/spec/lib/gitlab/sidekiq_status_spec.rb
new file mode 100644
index 00000000000..0aa36a3416b
--- /dev/null
+++ b/spec/lib/gitlab/sidekiq_status_spec.rb
@@ -0,0 +1,50 @@
+require 'spec_helper'
+
+describe Gitlab::SidekiqStatus do
+ describe '.set', :redis do
+ it 'stores the job ID' do
+ described_class.set('123')
+
+ key = described_class.key_for('123')
+
+ Sidekiq.redis do |redis|
+ expect(redis.exists(key)).to eq(true)
+ expect(redis.ttl(key) > 0).to eq(true)
+ end
+ end
+ end
+
+ describe '.unset', :redis do
+ it 'removes the job ID' do
+ described_class.set('123')
+ described_class.unset('123')
+
+ key = described_class.key_for('123')
+
+ Sidekiq.redis do |redis|
+ expect(redis.exists(key)).to eq(false)
+ end
+ end
+ end
+
+ describe '.all_completed?', :redis do
+ it 'returns true if all jobs have been completed' do
+ expect(described_class.all_completed?(%w(123))).to eq(true)
+ end
+
+ it 'returns false if a job has not yet been completed' do
+ described_class.set('123')
+
+ expect(described_class.all_completed?(%w(123 456))).to eq(false)
+ end
+ end
+
+ describe '.key_for' do
+ it 'returns the key for a job ID' do
+ key = described_class.key_for('123')
+
+ expect(key).to be_an_instance_of(String)
+ expect(key).to include('123')
+ end
+ end
+end
diff --git a/spec/lib/gitlab/template/issue_template_spec.rb b/spec/lib/gitlab/template/issue_template_spec.rb
index d2d334e6413..1335a2b8f35 100644
--- a/spec/lib/gitlab/template/issue_template_spec.rb
+++ b/spec/lib/gitlab/template/issue_template_spec.rb
@@ -4,16 +4,14 @@ describe Gitlab::Template::IssueTemplate do
subject { described_class }
let(:user) { create(:user) }
- let(:project) { create(:project) }
- let(:file_path_1) { '.gitlab/issue_templates/bug.md' }
- let(:file_path_2) { '.gitlab/issue_templates/template_test.md' }
- let(:file_path_3) { '.gitlab/issue_templates/feature_proposal.md' }
-
- before do
- project.add_user(user, Gitlab::Access::MASTER)
- project.repository.commit_file(user, file_path_1, "something valid", "test 3", "master", false)
- project.repository.commit_file(user, file_path_2, "template_test", "test 1", "master", false)
- project.repository.commit_file(user, file_path_3, "feature_proposal", "test 2", "master", false)
+
+ let(:project) do
+ create(:project,
+ :repository,
+ create_template: {
+ user: user,
+ access: Gitlab::Access::MASTER,
+ path: 'issue_templates' })
end
describe '.all' do
diff --git a/spec/lib/gitlab/template/merge_request_template_spec.rb b/spec/lib/gitlab/template/merge_request_template_spec.rb
index ddf68c4cf78..320b870309a 100644
--- a/spec/lib/gitlab/template/merge_request_template_spec.rb
+++ b/spec/lib/gitlab/template/merge_request_template_spec.rb
@@ -4,16 +4,14 @@ describe Gitlab::Template::MergeRequestTemplate do
subject { described_class }
let(:user) { create(:user) }
- let(:project) { create(:project) }
- let(:file_path_1) { '.gitlab/merge_request_templates/bug.md' }
- let(:file_path_2) { '.gitlab/merge_request_templates/template_test.md' }
- let(:file_path_3) { '.gitlab/merge_request_templates/feature_proposal.md' }
-
- before do
- project.add_user(user, Gitlab::Access::MASTER)
- project.repository.commit_file(user, file_path_1, "something valid", "test 3", "master", false)
- project.repository.commit_file(user, file_path_2, "template_test", "test 1", "master", false)
- project.repository.commit_file(user, file_path_3, "feature_proposal", "test 2", "master", false)
+
+ let(:project) do
+ create(:project,
+ :repository,
+ create_template: {
+ user: user,
+ access: Gitlab::Access::MASTER,
+ path: 'merge_request_templates' })
end
describe '.all' do
diff --git a/spec/lib/gitlab/uploads_transfer_spec.rb b/spec/lib/gitlab/uploads_transfer_spec.rb
deleted file mode 100644
index 4092f7fb638..00000000000
--- a/spec/lib/gitlab/uploads_transfer_spec.rb
+++ /dev/null
@@ -1,50 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::UploadsTransfer, lib: true do
- before do
- @root_dir = File.join(Rails.root, "public", "uploads")
- @upload_transfer = Gitlab::UploadsTransfer.new
-
- @project_path_was = "test_project_was"
- @project_path = "test_project"
- @namespace_path_was = "test_namespace_was"
- @namespace_path = "test_namespace"
- end
-
- after do
- FileUtils.rm_rf([
- File.join(@root_dir, @namespace_path),
- File.join(@root_dir, @namespace_path_was)
- ])
- end
-
- describe '#move_project' do
- it "moves project upload to another namespace" do
- FileUtils.mkdir_p(File.join(@root_dir, @namespace_path_was, @project_path))
- @upload_transfer.move_project(@project_path, @namespace_path_was, @namespace_path)
-
- expected_path = File.join(@root_dir, @namespace_path, @project_path)
- expect(Dir.exist?(expected_path)).to be_truthy
- end
- end
-
- describe '#rename_project' do
- it "renames project" do
- FileUtils.mkdir_p(File.join(@root_dir, @namespace_path, @project_path_was))
- @upload_transfer.rename_project(@project_path_was, @project_path, @namespace_path)
-
- expected_path = File.join(@root_dir, @namespace_path, @project_path)
- expect(Dir.exist?(expected_path)).to be_truthy
- end
- end
-
- describe '#rename_namespace' do
- it "renames namespace" do
- FileUtils.mkdir_p(File.join(@root_dir, @namespace_path_was, @project_path))
- @upload_transfer.rename_namespace(@namespace_path_was, @namespace_path)
-
- expected_path = File.join(@root_dir, @namespace_path, @project_path)
- expect(Dir.exist?(expected_path)).to be_truthy
- end
- end
-end
diff --git a/spec/lib/gitlab/url_builder_spec.rb b/spec/lib/gitlab/url_builder_spec.rb
index a826b24419a..3fe8cf43934 100644
--- a/spec/lib/gitlab/url_builder_spec.rb
+++ b/spec/lib/gitlab/url_builder_spec.rb
@@ -99,7 +99,7 @@ describe Gitlab::UrlBuilder, lib: true do
context 'on another object' do
it 'returns a proper URL' do
- project = build_stubbed(:project)
+ project = build_stubbed(:empty_project)
expect { described_class.build(project) }.
to raise_error(NotImplementedError, 'No URL builder defined for Project')
diff --git a/spec/lib/gitlab/user_access_spec.rb b/spec/lib/gitlab/user_access_spec.rb
index d3c3b800b94..369e55f61f1 100644
--- a/spec/lib/gitlab/user_access_spec.rb
+++ b/spec/lib/gitlab/user_access_spec.rb
@@ -66,7 +66,8 @@ describe Gitlab::UserAccess, lib: true do
end
describe 'push to protected branch' do
- let(:branch) { create :protected_branch, project: project }
+ let(:branch) { create :protected_branch, project: project, name: "test" }
+ let(:not_existing_branch) { create :protected_branch, :developers_can_merge, project: project }
it 'returns true if user is a master' do
project.team << [user, :master]
@@ -85,6 +86,12 @@ describe Gitlab::UserAccess, lib: true do
expect(access.can_push_to_branch?(branch.name)).to be_falsey
end
+
+ it 'returns true if branch does not exist and user has permission to merge' do
+ project.team << [user, :developer]
+
+ expect(access.can_push_to_branch?(not_existing_branch.name)).to be_truthy
+ end
end
describe 'push to protected branch if allowed for developers' do
diff --git a/spec/lib/gitlab/view/presenter/base_spec.rb b/spec/lib/gitlab/view/presenter/base_spec.rb
new file mode 100644
index 00000000000..f2c152cdcd4
--- /dev/null
+++ b/spec/lib/gitlab/view/presenter/base_spec.rb
@@ -0,0 +1,51 @@
+require 'spec_helper'
+
+describe Gitlab::View::Presenter::Base do
+ let(:project) { double(:project) }
+ let(:presenter_class) do
+ Struct.new(:subject).include(described_class)
+ end
+
+ describe '.presenter?' do
+ it 'returns true' do
+ presenter = presenter_class.new(project)
+
+ expect(presenter.class).to be_presenter
+ end
+ end
+
+ describe '.presents' do
+ it 'exposes #subject with the given keyword' do
+ presenter_class.presents(:foo)
+ presenter = presenter_class.new(project)
+
+ expect(presenter.foo).to eq(project)
+ end
+ end
+
+ describe '#can?' do
+ context 'user is not allowed' do
+ it 'returns false' do
+ presenter = presenter_class.new(build_stubbed(:empty_project))
+
+ expect(presenter.can?(nil, :read_project)).to be_falsy
+ end
+ end
+
+ context 'user is allowed' do
+ it 'returns true' do
+ presenter = presenter_class.new(build_stubbed(:empty_project, :public))
+
+ expect(presenter.can?(nil, :read_project)).to be_truthy
+ end
+ end
+
+ context 'subject is overriden' do
+ it 'returns true' do
+ presenter = presenter_class.new(build_stubbed(:empty_project, :public))
+
+ expect(presenter.can?(nil, :read_project, build_stubbed(:empty_project))).to be_falsy
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/view/presenter/delegated_spec.rb b/spec/lib/gitlab/view/presenter/delegated_spec.rb
new file mode 100644
index 00000000000..e9d4af54389
--- /dev/null
+++ b/spec/lib/gitlab/view/presenter/delegated_spec.rb
@@ -0,0 +1,33 @@
+require 'spec_helper'
+
+describe Gitlab::View::Presenter::Delegated do
+ let(:project) { double(:project, user: 'John Doe') }
+ let(:presenter_class) do
+ Class.new(described_class)
+ end
+
+ it 'includes Gitlab::View::Presenter::Base' do
+ expect(described_class).to include(Gitlab::View::Presenter::Base)
+ end
+
+ describe '#initialize' do
+ it 'takes arbitrary key/values and exposes them' do
+ presenter = presenter_class.new(project, current_user: 'Jane Doe')
+
+ expect(presenter.current_user).to eq('Jane Doe')
+ end
+
+ it 'raise an error if the presentee already respond to method' do
+ expect { presenter_class.new(project, user: 'Jane Doe') }.
+ to raise_error Gitlab::View::Presenter::CannotOverrideMethodError
+ end
+ end
+
+ describe 'delegation' do
+ it 'forwards missing methods to subject' do
+ presenter = presenter_class.new(project)
+
+ expect(presenter.user).to eq('John Doe')
+ end
+ end
+end
diff --git a/spec/lib/gitlab/view/presenter/factory_spec.rb b/spec/lib/gitlab/view/presenter/factory_spec.rb
new file mode 100644
index 00000000000..70d2e22b48f
--- /dev/null
+++ b/spec/lib/gitlab/view/presenter/factory_spec.rb
@@ -0,0 +1,31 @@
+require 'spec_helper'
+
+describe Gitlab::View::Presenter::Factory do
+ let(:build) { Ci::Build.new }
+
+ describe '#initialize' do
+ context 'without optional parameters' do
+ it 'takes a subject and optional params' do
+ presenter = described_class.new(build)
+
+ expect { presenter }.not_to raise_error
+ end
+ end
+
+ context 'with optional parameters' do
+ it 'takes a subject and optional params' do
+ presenter = described_class.new(build, user: 'user')
+
+ expect { presenter }.not_to raise_error
+ end
+ end
+ end
+
+ describe '#fabricate!' do
+ it 'detects the presenter based on the given subject' do
+ presenter = described_class.new(build).fabricate!
+
+ expect(presenter).to be_a(Ci::BuildPresenter)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/view/presenter/simple_spec.rb b/spec/lib/gitlab/view/presenter/simple_spec.rb
new file mode 100644
index 00000000000..1795ed2405b
--- /dev/null
+++ b/spec/lib/gitlab/view/presenter/simple_spec.rb
@@ -0,0 +1,34 @@
+require 'spec_helper'
+
+describe Gitlab::View::Presenter::Simple do
+ let(:project) { double(:project, user: 'John Doe') }
+ let(:presenter_class) do
+ Class.new(described_class)
+ end
+
+ it 'includes Gitlab::View::Presenter::Base' do
+ expect(described_class).to include(Gitlab::View::Presenter::Base)
+ end
+
+ describe '#initialize' do
+ it 'takes arbitrary key/values and exposes them' do
+ presenter = presenter_class.new(project, current_user: 'Jane Doe')
+
+ expect(presenter.current_user).to eq('Jane Doe')
+ end
+
+ it 'override the presentee attributes' do
+ presenter = presenter_class.new(project, user: 'Jane Doe')
+
+ expect(presenter.user).to eq('Jane Doe')
+ end
+ end
+
+ describe 'delegation' do
+ it 'does not forward missing methods to subject' do
+ presenter = presenter_class.new(project)
+
+ expect { presenter.user }.to raise_error(NoMethodError)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb
index 61da91dcbd3..a32c6131030 100644
--- a/spec/lib/gitlab/workhorse_spec.rb
+++ b/spec/lib/gitlab/workhorse_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Gitlab::Workhorse, lib: true do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:repository) { project.repository }
def decode_workhorse_header(array)
@@ -42,7 +42,8 @@ describe Gitlab::Workhorse, lib: true do
out = {
subprotocols: ['foo'],
url: 'wss://example.com/terminal.ws',
- headers: { 'Authorization' => ['Token x'] }
+ headers: { 'Authorization' => ['Token x'] },
+ max_session_time: 600
}
out[:ca_pem] = ca_pem if ca_pem
out
@@ -53,7 +54,8 @@ describe Gitlab::Workhorse, lib: true do
'Terminal' => {
'Subprotocols' => ['foo'],
'Url' => 'wss://example.com/terminal.ws',
- 'Header' => { 'Authorization' => ['Token x'] }
+ 'Header' => { 'Authorization' => ['Token x'] },
+ 'MaxSessionTime' => 600
}
}
out['Terminal']['CAPem'] = ca_pem if ca_pem
@@ -174,4 +176,27 @@ describe Gitlab::Workhorse, lib: true do
described_class.verify_api_request!(headers)
end
end
+
+ describe '.git_http_ok' do
+ let(:user) { create(:user) }
+
+ subject { described_class.git_http_ok(repository, user) }
+
+ it { expect(subject).to eq({ GL_ID: "user-#{user.id}", RepoPath: repository.path_to_repo }) }
+
+ context 'when Gitaly socket path is present' do
+ let(:gitaly_socket_path) { '/tmp/gitaly.sock' }
+
+ before do
+ allow(Gitlab.config.gitaly).to receive(:socket_path).and_return(gitaly_socket_path)
+ end
+
+ it 'includes Gitaly params in the returned value' do
+ expect(subject).to include({
+ GitalyResourcePath: "/projects/#{repository.project.id}/git-http/info-refs",
+ GitalySocketPath: gitaly_socket_path,
+ })
+ end
+ end
+ end
end
diff --git a/spec/lib/light_url_builder_spec.rb b/spec/lib/light_url_builder_spec.rb
index a826b24419a..3fe8cf43934 100644
--- a/spec/lib/light_url_builder_spec.rb
+++ b/spec/lib/light_url_builder_spec.rb
@@ -99,7 +99,7 @@ describe Gitlab::UrlBuilder, lib: true do
context 'on another object' do
it 'returns a proper URL' do
- project = build_stubbed(:project)
+ project = build_stubbed(:empty_project)
expect { described_class.build(project) }.
to raise_error(NotImplementedError, 'No URL builder defined for Project')
diff --git a/spec/lib/mattermost/client_spec.rb b/spec/lib/mattermost/client_spec.rb
new file mode 100644
index 00000000000..dc11a414717
--- /dev/null
+++ b/spec/lib/mattermost/client_spec.rb
@@ -0,0 +1,24 @@
+require 'spec_helper'
+
+describe Mattermost::Client do
+ let(:user) { build(:user) }
+
+ subject { described_class.new(user) }
+
+ context 'JSON parse error' do
+ before do
+ Struct.new("Request", :body, :success?)
+ end
+
+ it 'yields an error on malformed JSON' do
+ bad_json = Struct::Request.new("I'm not json", true)
+ expect { subject.send(:json_response, bad_json) }.to raise_error(Mattermost::ClientError)
+ end
+
+ it 'shows a client error if the request was unsuccessful' do
+ bad_request = Struct::Request.new("true", false)
+
+ expect { subject.send(:json_response, bad_request) }.to raise_error(Mattermost::ClientError)
+ end
+ end
+end
diff --git a/spec/lib/mattermost/command_spec.rb b/spec/lib/mattermost/command_spec.rb
new file mode 100644
index 00000000000..5ccf1100898
--- /dev/null
+++ b/spec/lib/mattermost/command_spec.rb
@@ -0,0 +1,61 @@
+require 'spec_helper'
+
+describe Mattermost::Command do
+ let(:params) { { 'token' => 'token', team_id: 'abc' } }
+
+ before do
+ Mattermost::Session.base_uri('http://mattermost.example.com')
+
+ allow_any_instance_of(Mattermost::Client).to receive(:with_session).
+ and_yield(Mattermost::Session.new(nil))
+ end
+
+ describe '#create' do
+ let(:params) do
+ { team_id: 'abc',
+ trigger: 'gitlab'
+ }
+ end
+
+ subject { described_class.new(nil).create(params) }
+
+ context 'for valid trigger word' do
+ before do
+ stub_request(:post, 'http://mattermost.example.com/api/v3/teams/abc/commands/create').
+ with(body: {
+ team_id: 'abc',
+ trigger: 'gitlab' }.to_json).
+ to_return(
+ status: 200,
+ headers: { 'Content-Type' => 'application/json' },
+ body: { token: 'token' }.to_json
+ )
+ end
+
+ it 'returns a token' do
+ is_expected.to eq('token')
+ end
+ end
+
+ context 'for error message' do
+ before do
+ stub_request(:post, 'http://mattermost.example.com/api/v3/teams/abc/commands/create').
+ to_return(
+ status: 500,
+ headers: { 'Content-Type' => 'application/json' },
+ body: {
+ id: 'api.command.duplicate_trigger.app_error',
+ message: 'This trigger word is already in use. Please choose another word.',
+ detailed_error: '',
+ request_id: 'obc374man7bx5r3dbc1q5qhf3r',
+ status_code: 500
+ }.to_json
+ )
+ end
+
+ it 'raises an error with message' do
+ expect { subject }.to raise_error(Mattermost::Error, 'This trigger word is already in use. Please choose another word.')
+ end
+ end
+ end
+end
diff --git a/spec/lib/mattermost/session_spec.rb b/spec/lib/mattermost/session_spec.rb
index 3c2eddbd221..74d12e37181 100644
--- a/spec/lib/mattermost/session_spec.rb
+++ b/spec/lib/mattermost/session_spec.rb
@@ -95,5 +95,29 @@ describe Mattermost::Session, type: :request do
end
end
end
+
+ context 'with lease' do
+ before do
+ allow(subject).to receive(:lease_try_obtain).and_return('aldkfjsldfk')
+ end
+
+ it 'tries to obtain a lease' do
+ expect(subject).to receive(:lease_try_obtain)
+ expect(Gitlab::ExclusiveLease).to receive(:cancel)
+
+ # Cannot setup a session, but we should still cancel the lease
+ expect { subject.with_session }.to raise_error(Mattermost::NoSessionError)
+ end
+ end
+
+ context 'without lease' do
+ before do
+ allow(subject).to receive(:lease_try_obtain).and_return(nil)
+ end
+
+ it 'returns a NoSessionError error' do
+ expect { subject.with_session }.to raise_error(Mattermost::NoSessionError)
+ end
+ end
end
end
diff --git a/spec/lib/mattermost/team_spec.rb b/spec/lib/mattermost/team_spec.rb
new file mode 100644
index 00000000000..2d14be6bcc2
--- /dev/null
+++ b/spec/lib/mattermost/team_spec.rb
@@ -0,0 +1,66 @@
+require 'spec_helper'
+
+describe Mattermost::Team do
+ before do
+ Mattermost::Session.base_uri('http://mattermost.example.com')
+
+ allow_any_instance_of(Mattermost::Client).to receive(:with_session).
+ and_yield(Mattermost::Session.new(nil))
+ end
+
+ describe '#all' do
+ subject { described_class.new(nil).all }
+
+ context 'for valid request' do
+ let(:response) do
+ [{
+ "id" => "xiyro8huptfhdndadpz8r3wnbo",
+ "create_at" => 1482174222155,
+ "update_at" => 1482174222155,
+ "delete_at" => 0,
+ "display_name" => "chatops",
+ "name" => "chatops",
+ "email" => "admin@example.com",
+ "type" => "O",
+ "company_name" => "",
+ "allowed_domains" => "",
+ "invite_id" => "o4utakb9jtb7imctdfzbf9r5ro",
+ "allow_open_invite" => false }]
+ end
+
+ before do
+ stub_request(:get, 'http://mattermost.example.com/api/v3/teams/all').
+ to_return(
+ status: 200,
+ headers: { 'Content-Type' => 'application/json' },
+ body: response.to_json
+ )
+ end
+
+ it 'returns a token' do
+ is_expected.to eq(response)
+ end
+ end
+
+ context 'for error message' do
+ before do
+ stub_request(:get, 'http://mattermost.example.com/api/v3/teams/all').
+ to_return(
+ status: 500,
+ headers: { 'Content-Type' => 'application/json' },
+ body: {
+ id: 'api.team.list.app_error',
+ message: 'Cannot list teams.',
+ detailed_error: '',
+ request_id: 'obc374man7bx5r3dbc1q5qhf3r',
+ status_code: 500
+ }.to_json
+ )
+ end
+
+ it 'raises an error with message' do
+ expect { subject }.to raise_error(Mattermost::Error, 'Cannot list teams.')
+ end
+ end
+ end
+end
diff --git a/spec/lib/repository_cache_spec.rb b/spec/lib/repository_cache_spec.rb
index f227926f39c..5892f3481a4 100644
--- a/spec/lib/repository_cache_spec.rb
+++ b/spec/lib/repository_cache_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe RepositoryCache, lib: true do
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project) }
let(:backend) { double('backend').as_null_object }
let(:cache) { RepositoryCache.new('example', project.id, backend) }
diff --git a/spec/migrations/fill_authorized_projects_spec.rb b/spec/migrations/fill_authorized_projects_spec.rb
new file mode 100644
index 00000000000..99dc4195818
--- /dev/null
+++ b/spec/migrations/fill_authorized_projects_spec.rb
@@ -0,0 +1,18 @@
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20170106142508_fill_authorized_projects.rb')
+
+describe FillAuthorizedProjects do
+ describe '#up' do
+ it 'schedules the jobs in batches' do
+ user1 = create(:user)
+ user2 = create(:user)
+
+ expect(Sidekiq::Client).to receive(:push_bulk).with(
+ 'class' => 'AuthorizedProjectsWorker',
+ 'args' => [[user1.id], [user2.id]]
+ )
+
+ described_class.new.up
+ end
+ end
+end
diff --git a/spec/migrations/remove_dot_git_from_usernames_spec.rb b/spec/migrations/remove_dot_git_from_usernames_spec.rb
new file mode 100644
index 00000000000..8737e00eaeb
--- /dev/null
+++ b/spec/migrations/remove_dot_git_from_usernames_spec.rb
@@ -0,0 +1,57 @@
+# encoding: utf-8
+
+require 'spec_helper'
+require Rails.root.join('db', 'migrate', '20161226122833_remove_dot_git_from_usernames.rb')
+
+describe RemoveDotGitFromUsernames do
+ let(:user) { create(:user) }
+ let(:migration) { described_class.new }
+
+ describe '#up' do
+ before do
+ update_namespace(user, 'test.git')
+ end
+
+ it 'renames user with .git in username' do
+ migration.up
+
+ expect(user.reload.username).to eq('test_git')
+ expect(user.namespace.reload.path).to eq('test_git')
+ expect(user.namespace.route.path).to eq('test_git')
+ end
+ end
+
+ context 'when new path exists already' do
+ describe '#up' do
+ let(:user2) { create(:user) }
+
+ before do
+ update_namespace(user, 'test.git')
+ update_namespace(user2, 'test_git')
+
+ storages = { 'default' => 'tmp/tests/custom_repositories' }
+
+ allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
+ allow(migration).to receive(:route_exists?).with('test_git').and_return(true)
+ allow(migration).to receive(:route_exists?).with('test_git1').and_return(false)
+ end
+
+ it 'renames user with .git in username' do
+ migration.up
+
+ expect(user.reload.username).to eq('test_git1')
+ expect(user.namespace.reload.path).to eq('test_git1')
+ expect(user.namespace.route.path).to eq('test_git1')
+ end
+ end
+ end
+
+ def update_namespace(user, path)
+ namespace = user.namespace
+ namespace.path = path
+ namespace.save!(validate: false)
+
+ user.username = path
+ user.save!(validate: false)
+ end
+end
diff --git a/spec/migrations/rename_reserved_project_names_spec.rb b/spec/migrations/rename_reserved_project_names_spec.rb
new file mode 100644
index 00000000000..4fb7ed36884
--- /dev/null
+++ b/spec/migrations/rename_reserved_project_names_spec.rb
@@ -0,0 +1,47 @@
+# encoding: utf-8
+
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20161221153951_rename_reserved_project_names.rb')
+
+# This migration uses multiple threads, and thus different transactions. This
+# means data created in this spec may not be visible to some threads. To work
+# around this we use the TRUNCATE cleaning strategy.
+describe RenameReservedProjectNames, truncate: true do
+ let(:migration) { described_class.new }
+ let!(:project) { create(:empty_project) }
+
+ before do
+ project.path = 'projects'
+ project.save!(validate: false)
+ end
+
+ describe '#up' do
+ context 'when project repository exists' do
+ before { project.create_repository }
+
+ context 'when no exception is raised' do
+ it 'renames project with reserved names' do
+ migration.up
+
+ expect(project.reload.path).to eq('projects0')
+ end
+ end
+
+ context 'when exception is raised during rename' do
+ before do
+ allow(project).to receive(:rename_repo).and_raise(StandardError)
+ end
+
+ it 'captures exception from project rename' do
+ expect { migration.up }.not_to raise_error
+ end
+ end
+ end
+
+ context 'when project repository does not exist' do
+ it 'does not raise error' do
+ expect { migration.up }.not_to raise_error
+ end
+ end
+ end
+end
diff --git a/spec/models/ability_spec.rb b/spec/models/ability_spec.rb
index 1bdf005c823..30f8fdf91b2 100644
--- a/spec/models/ability_spec.rb
+++ b/spec/models/ability_spec.rb
@@ -3,7 +3,7 @@ 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) }
+ let(:note) { create(:note_on_issue, project: project) }
context 'using an anonymous user' do
it 'returns false' do
@@ -60,7 +60,7 @@ describe Ability, lib: true do
describe '.users_that_can_read_project' do
context 'using a public project' do
it 'returns all the users' do
- project = create(:project, :public)
+ project = create(:empty_project, :public)
user = build(:user)
expect(described_class.users_that_can_read_project([user], project)).
@@ -69,7 +69,7 @@ describe Ability, lib: true do
end
context 'using an internal project' do
- let(:project) { create(:project, :internal) }
+ let(:project) { create(:empty_project, :internal) }
it 'returns users that are administrators' do
user = build(:user, admin: true)
@@ -120,7 +120,7 @@ describe Ability, lib: true do
end
context 'using a private project' do
- let(:project) { create(:project, :private) }
+ let(:project) { create(:empty_project, :private) }
it 'returns users that are administrators' do
user = build(:user, admin: true)
@@ -171,6 +171,33 @@ describe Ability, lib: true do
end
end
+ describe '.users_that_can_read_personal_snippet' do
+ def users_for_snippet(snippet)
+ described_class.users_that_can_read_personal_snippet(users, snippet)
+ end
+
+ let(:users) { create_list(:user, 3) }
+ let(:author) { users[0] }
+
+ it 'private snippet is readable only by its author' do
+ snippet = create(:personal_snippet, :private, author: author)
+
+ expect(users_for_snippet(snippet)).to match_array([author])
+ end
+
+ it 'internal snippet is readable by all registered users' do
+ snippet = create(:personal_snippet, :public, author: author)
+
+ expect(users_for_snippet(snippet)).to match_array(users)
+ end
+
+ it 'public snippet is readable by all users' do
+ snippet = create(:personal_snippet, :public, author: author)
+
+ expect(users_for_snippet(snippet)).to match_array(users)
+ end
+ end
+
describe '.issues_readable_by_user' do
context 'with an admin user' do
it 'returns all given issues' do
@@ -220,7 +247,7 @@ describe Ability, lib: true do
end
describe '.project_disabled_features_rules' do
- let(:project) { create(:project, wiki_access_level: ProjectFeature::DISABLED) }
+ let(:project) { create(:empty_project, :wiki_disabled) }
subject { described_class.allowed(project.owner, project) }
diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb
deleted file mode 100644
index cd3b6d51545..00000000000
--- a/spec/models/build_spec.rb
+++ /dev/null
@@ -1,1338 +0,0 @@
-require 'spec_helper'
-
-describe Ci::Build, models: true do
- let(:project) { create(:project) }
-
- let(:pipeline) do
- create(:ci_pipeline, project: project,
- sha: project.commit.id,
- ref: project.default_branch,
- status: 'success')
- end
-
- let(:build) { create(:ci_build, pipeline: pipeline) }
-
- it { is_expected.to validate_presence_of :ref }
-
- it { is_expected.to respond_to :trace_html }
-
- describe '#first_pending' do
- let!(:first) { create(:ci_build, pipeline: pipeline, status: 'pending', created_at: Date.yesterday) }
- let!(:second) { create(:ci_build, pipeline: pipeline, status: 'pending') }
- subject { Ci::Build.first_pending }
-
- it { is_expected.to be_a(Ci::Build) }
- it('returns with the first pending build') { is_expected.to eq(first) }
- end
-
- describe '#create_from' do
- before do
- build.status = 'success'
- build.save
- end
- let(:create_from_build) { Ci::Build.create_from build }
-
- it 'exists a pending task' do
- expect(Ci::Build.pending.count(:all)).to eq 0
- create_from_build
- expect(Ci::Build.pending.count(:all)).to be > 0
- end
- end
-
- describe '#failed_but_allowed?' do
- subject { build.failed_but_allowed? }
-
- context 'when build is not allowed to fail' do
- before do
- build.allow_failure = false
- end
-
- context 'and build.status is success' do
- before do
- build.status = 'success'
- end
-
- it { is_expected.to be_falsey }
- end
-
- context 'and build.status is failed' do
- before do
- build.status = 'failed'
- end
-
- it { is_expected.to be_falsey }
- end
- end
-
- context 'when build is allowed to fail' do
- before do
- build.allow_failure = true
- end
-
- context 'and build.status is success' do
- before do
- build.status = 'success'
- end
-
- it { is_expected.to be_falsey }
- end
-
- context 'and build.status is failed' do
- before do
- build.status = 'failed'
- end
-
- it { is_expected.to be_truthy }
- end
- end
- end
-
- describe '#persisted_environment' do
- before do
- @environment = create(:environment, project: project, name: "foo-#{project.default_branch}")
- end
-
- subject { build.persisted_environment }
-
- context 'referenced literally' do
- let(:build) { create(:ci_build, pipeline: pipeline, environment: "foo-#{project.default_branch}") }
-
- it { is_expected.to eq(@environment) }
- end
-
- context 'referenced with a variable' do
- let(:build) { create(:ci_build, pipeline: pipeline, environment: "foo-$CI_BUILD_REF_NAME") }
-
- it { is_expected.to eq(@environment) }
- end
- end
-
- describe '#trace' do
- it { expect(build.trace).to be_nil }
-
- context 'when build.trace contains text' do
- let(:text) { 'example output' }
- before do
- build.trace = text
- end
-
- it { expect(build.trace).to eq(text) }
- end
-
- context 'when build.trace hides runners token' do
- let(:token) { 'my_secret_token' }
-
- before do
- build.update(trace: token)
- build.project.update(runners_token: token)
- end
-
- it { expect(build.trace).not_to include(token) }
- it { expect(build.raw_trace).to include(token) }
- end
-
- context 'when build.trace hides build token' do
- let(:token) { 'my_secret_token' }
-
- before do
- build.update(trace: token)
- build.update(token: token)
- end
-
- it { expect(build.trace).not_to include(token) }
- it { expect(build.raw_trace).to include(token) }
- end
- end
-
- describe '#raw_trace' do
- subject { build.raw_trace }
-
- context 'when build.trace hides runners token' do
- let(:token) { 'my_secret_token' }
-
- before do
- build.project.update(runners_token: token)
- build.update(trace: token)
- end
-
- it { is_expected.not_to include(token) }
- end
-
- context 'when build.trace hides build token' do
- let(:token) { 'my_secret_token' }
-
- before do
- build.update(token: token)
- build.update(trace: token)
- end
-
- it { is_expected.not_to include(token) }
- end
- end
-
- context '#append_trace' do
- subject { build.trace_html }
-
- context 'when build.trace hides runners token' do
- let(:token) { 'my_secret_token' }
-
- before do
- build.project.update(runners_token: token)
- build.append_trace(token, 0)
- end
-
- it { is_expected.not_to include(token) }
- end
-
- context 'when build.trace hides build token' do
- let(:token) { 'my_secret_token' }
-
- before do
- build.update(token: token)
- build.append_trace(token, 0)
- end
-
- it { is_expected.not_to include(token) }
- end
- end
-
- # TODO: build timeout
- # describe :timeout do
- # subject { build.timeout }
- #
- # it { is_expected.to eq(pipeline.project.timeout) }
- # end
-
- describe '#options' do
- let(:options) do
- {
- image: "ruby:2.1",
- services: [
- "postgres"
- ]
- }
- end
-
- subject { build.options }
- it { is_expected.to eq(options) }
- end
-
- # TODO: allow_git_fetch
- # describe :allow_git_fetch do
- # subject { build.allow_git_fetch }
- #
- # it { is_expected.to eq(project.allow_git_fetch) }
- # end
-
- describe '#project' do
- subject { build.project }
-
- it { is_expected.to eq(pipeline.project) }
- end
-
- describe '#project_id' do
- subject { build.project_id }
-
- it { is_expected.to eq(pipeline.project_id) }
- end
-
- describe '#project_name' do
- subject { build.project_name }
-
- it { is_expected.to eq(project.name) }
- end
-
- describe '#extract_coverage' do
- context 'valid content & regex' do
- subject { build.extract_coverage('Coverage 1033 / 1051 LOC (98.29%) covered', '\(\d+.\d+\%\) covered') }
-
- it { is_expected.to eq(98.29) }
- end
-
- context 'valid content & bad regex' do
- subject { build.extract_coverage('Coverage 1033 / 1051 LOC (98.29%) covered', 'very covered') }
-
- it { is_expected.to be_nil }
- end
-
- context 'no coverage content & regex' do
- subject { build.extract_coverage('No coverage for today :sad:', '\(\d+.\d+\%\) covered') }
-
- it { is_expected.to be_nil }
- end
-
- context 'multiple results in content & regex' do
- subject { build.extract_coverage(' (98.39%) covered. (98.29%) covered', '\(\d+.\d+\%\) covered') }
-
- it { is_expected.to eq(98.29) }
- end
-
- context 'using a regex capture' do
- subject { build.extract_coverage('TOTAL 9926 3489 65%', 'TOTAL\s+\d+\s+\d+\s+(\d{1,3}\%)') }
-
- it { is_expected.to eq(65) }
- end
- end
-
- describe '#ref_slug' do
- {
- 'master' => 'master',
- '1-foo' => '1-foo',
- 'fix/1-foo' => 'fix-1-foo',
- 'fix-1-foo' => 'fix-1-foo',
- 'a' * 63 => 'a' * 63,
- 'a' * 64 => 'a' * 63,
- 'FOO' => 'foo',
- }.each do |ref, slug|
- it "transforms #{ref} to #{slug}" do
- build.ref = ref
-
- expect(build.ref_slug).to eq(slug)
- end
- end
- end
-
- describe '#variables' do
- let(:container_registry_enabled) { false }
- let(:predefined_variables) do
- [
- { key: 'CI', value: 'true', public: true },
- { key: 'GITLAB_CI', value: 'true', public: true },
- { key: 'CI_BUILD_ID', value: build.id.to_s, public: true },
- { key: 'CI_BUILD_TOKEN', value: build.token, public: false },
- { key: 'CI_BUILD_REF', value: build.sha, public: true },
- { key: 'CI_BUILD_BEFORE_SHA', value: build.before_sha, public: true },
- { key: 'CI_BUILD_REF_NAME', value: 'master', public: true },
- { key: 'CI_BUILD_REF_SLUG', value: 'master', public: true },
- { key: 'CI_BUILD_NAME', value: 'test', public: true },
- { key: 'CI_BUILD_STAGE', value: 'test', public: true },
- { key: 'CI_SERVER_NAME', value: 'GitLab', public: true },
- { key: 'CI_SERVER_VERSION', value: Gitlab::VERSION, public: true },
- { key: 'CI_SERVER_REVISION', value: Gitlab::REVISION, public: true },
- { key: 'CI_PROJECT_ID', value: project.id.to_s, public: true },
- { key: 'CI_PROJECT_NAME', value: project.path, public: true },
- { key: 'CI_PROJECT_PATH', value: project.path_with_namespace, public: true },
- { key: 'CI_PROJECT_NAMESPACE', value: project.namespace.path, public: true },
- { key: 'CI_PROJECT_URL', value: project.web_url, public: true },
- { key: 'CI_PIPELINE_ID', value: pipeline.id.to_s, public: true }
- ]
- end
-
- before do
- stub_container_registry_config(enabled: container_registry_enabled, host_port: 'registry.example.com')
- end
-
- subject { build.variables }
-
- context 'returns variables' do
- before do
- build.yaml_variables = []
- end
-
- it { is_expected.to eq(predefined_variables) }
- end
-
- context 'when build has user' do
- let(:user) { create(:user, username: 'starter') }
- let(:user_variables) do
- [
- { key: 'GITLAB_USER_ID', value: user.id.to_s, public: true },
- { key: 'GITLAB_USER_EMAIL', value: user.email, public: true }
- ]
- end
-
- before do
- build.update_attributes(user: user)
- end
-
- it { user_variables.each { |v| is_expected.to include(v) } }
- end
-
- context 'when build has an environment' do
- before do
- build.update(environment: 'production')
- create(:environment, project: build.project, name: 'production', slug: 'prod-slug')
- end
-
- let(:environment_variables) do
- [
- { key: 'CI_ENVIRONMENT_NAME', value: 'production', public: true },
- { key: 'CI_ENVIRONMENT_SLUG', value: 'prod-slug', public: true }
- ]
- end
-
- it { environment_variables.each { |v| is_expected.to include(v) } }
- end
-
- context 'when build started manually' do
- before do
- build.update_attributes(when: :manual)
- end
-
- let(:manual_variable) do
- { key: 'CI_BUILD_MANUAL', value: 'true', public: true }
- end
-
- it { is_expected.to include(manual_variable) }
- end
-
- context 'when build is for tag' do
- let(:tag_variable) do
- { key: 'CI_BUILD_TAG', value: 'master', public: true }
- end
-
- before do
- build.update_attributes(tag: true)
- end
-
- it { is_expected.to include(tag_variable) }
- end
-
- context 'when secure variable is defined' do
- let(:secure_variable) do
- { key: 'SECRET_KEY', value: 'secret_value', public: false }
- end
-
- before do
- build.project.variables << Ci::Variable.new(key: 'SECRET_KEY', value: 'secret_value')
- end
-
- it { is_expected.to include(secure_variable) }
- end
-
- context 'when build is for triggers' do
- let(:trigger) { create(:ci_trigger, project: project) }
- let(:trigger_request) { create(:ci_trigger_request_with_variables, pipeline: pipeline, trigger: trigger) }
- let(:user_trigger_variable) do
- { key: :TRIGGER_KEY_1, value: 'TRIGGER_VALUE_1', public: false }
- end
- let(:predefined_trigger_variable) do
- { key: 'CI_BUILD_TRIGGERED', value: 'true', public: true }
- end
-
- before do
- build.trigger_request = trigger_request
- end
-
- it { is_expected.to include(user_trigger_variable) }
- it { is_expected.to include(predefined_trigger_variable) }
- end
-
- context 'when yaml_variables are undefined' do
- before do
- build.yaml_variables = nil
- end
-
- context 'use from gitlab-ci.yml' do
- before do
- stub_ci_pipeline_yaml_file(config)
- end
-
- context 'when config is not found' do
- let(:config) { nil }
-
- it { is_expected.to eq(predefined_variables) }
- end
-
- context 'when config does not have a questioned job' do
- let(:config) do
- YAML.dump({
- test_other: {
- script: 'Hello World'
- }
- })
- end
-
- it { is_expected.to eq(predefined_variables) }
- end
-
- context 'when config has variables' do
- let(:config) do
- YAML.dump({
- test: {
- script: 'Hello World',
- variables: {
- KEY: 'value'
- }
- }
- })
- end
- let(:variables) do
- [{ key: 'KEY', value: 'value', public: true }]
- end
-
- it { is_expected.to eq(predefined_variables + variables) }
- end
- end
- end
-
- context 'when container registry is enabled' do
- let(:container_registry_enabled) { true }
- let(:ci_registry) do
- { key: 'CI_REGISTRY', value: 'registry.example.com', public: true }
- end
- let(:ci_registry_image) do
- { key: 'CI_REGISTRY_IMAGE', value: project.container_registry_repository_url, public: true }
- end
-
- context 'and is disabled for project' do
- before do
- project.update(container_registry_enabled: false)
- end
-
- it { is_expected.to include(ci_registry) }
- it { is_expected.not_to include(ci_registry_image) }
- end
-
- context 'and is enabled for project' do
- before do
- project.update(container_registry_enabled: true)
- end
-
- it { is_expected.to include(ci_registry) }
- it { is_expected.to include(ci_registry_image) }
- end
- end
-
- context 'when runner is assigned to build' do
- let(:runner) { create(:ci_runner, description: 'description', tag_list: ['docker', 'linux']) }
-
- before do
- build.update(runner: runner)
- end
-
- it { is_expected.to include({ key: 'CI_RUNNER_ID', value: runner.id.to_s, public: true }) }
- it { is_expected.to include({ key: 'CI_RUNNER_DESCRIPTION', value: 'description', public: true }) }
- it { is_expected.to include({ key: 'CI_RUNNER_TAGS', value: 'docker, linux', public: true }) }
- end
-
- context 'when build is for a deployment' do
- let(:deployment_variable) { { key: 'KUBERNETES_TOKEN', value: 'TOKEN', public: false } }
-
- before do
- build.environment = 'production'
- allow(project).to receive(:deployment_variables).and_return([deployment_variable])
- end
-
- it { is_expected.to include(deployment_variable) }
- end
-
- context 'returns variables in valid order' do
- before do
- allow(build).to receive(:predefined_variables) { ['predefined'] }
- allow(project).to receive(:predefined_variables) { ['project'] }
- allow(pipeline).to receive(:predefined_variables) { ['pipeline'] }
- allow(build).to receive(:yaml_variables) { ['yaml'] }
- allow(project).to receive(:secret_variables) { ['secret'] }
- end
-
- it { is_expected.to eq(%w[predefined project pipeline yaml secret]) }
- end
- end
-
- describe '#has_tags?' do
- context 'when build has tags' do
- subject { create(:ci_build, tag_list: ['tag']) }
- it { is_expected.to have_tags }
- end
-
- context 'when build does not have tags' do
- subject { create(:ci_build, tag_list: []) }
- it { is_expected.not_to have_tags }
- end
- end
-
- describe '#any_runners_online?' do
- subject { build.any_runners_online? }
-
- context 'when no runners' do
- it { is_expected.to be_falsey }
- end
-
- context 'when there are runners' do
- let(:runner) { create(:ci_runner) }
-
- before do
- build.project.runners << runner
- runner.update_attributes(contacted_at: 1.second.ago)
- end
-
- it { is_expected.to be_truthy }
-
- it 'that is inactive' do
- runner.update_attributes(active: false)
- is_expected.to be_falsey
- end
-
- it 'that is not online' do
- runner.update_attributes(contacted_at: nil)
- is_expected.to be_falsey
- end
-
- it 'that cannot handle build' do
- expect_any_instance_of(Ci::Runner).to receive(:can_pick?).and_return(false)
- is_expected.to be_falsey
- end
- end
- end
-
- describe '#stuck?' do
- subject { build.stuck? }
-
- context "when commit_status.status is pending" do
- before do
- build.status = 'pending'
- end
-
- it { is_expected.to be_truthy }
-
- context "and there are specific runner" do
- let(:runner) { create(:ci_runner, contacted_at: 1.second.ago) }
-
- before do
- build.project.runners << runner
- runner.save
- end
-
- it { is_expected.to be_falsey }
- end
- end
-
- %w[success failed canceled running].each do |state|
- context "when commit_status.status is #{state}" do
- before do
- build.status = state
- end
-
- it { is_expected.to be_falsey }
- end
- end
- end
-
- describe '#artifacts?' do
- subject { build.artifacts? }
-
- context 'artifacts archive does not exist' do
- before do
- build.update_attributes(artifacts_file: nil)
- end
-
- it { is_expected.to be_falsy }
- end
-
- context 'artifacts archive exists' do
- let(:build) { create(:ci_build, :artifacts) }
- it { is_expected.to be_truthy }
-
- context 'is expired' do
- before { build.update(artifacts_expire_at: Time.now - 7.days) }
- it { is_expected.to be_falsy }
- end
-
- context 'is not expired' do
- before { build.update(artifacts_expire_at: Time.now + 7.days) }
- it { is_expected.to be_truthy }
- end
- end
- end
-
- describe '#artifacts_expired?' do
- subject { build.artifacts_expired? }
-
- context 'is expired' do
- before { build.update(artifacts_expire_at: Time.now - 7.days) }
-
- it { is_expected.to be_truthy }
- end
-
- context 'is not expired' do
- before { build.update(artifacts_expire_at: Time.now + 7.days) }
-
- it { is_expected.to be_falsey }
- end
- end
-
- describe '#artifacts_metadata?' do
- subject { build.artifacts_metadata? }
- context 'artifacts metadata does not exist' do
- it { is_expected.to be_falsy }
- end
-
- context 'artifacts archive is a zip file and metadata exists' do
- let(:build) { create(:ci_build, :artifacts) }
- it { is_expected.to be_truthy }
- end
- end
- describe '#repo_url' do
- let(:build) { create(:ci_build) }
- let(:project) { build.project }
-
- subject { build.repo_url }
-
- it { is_expected.to be_a(String) }
- it { is_expected.to end_with(".git") }
- it { is_expected.to start_with(project.web_url[0..6]) }
- it { is_expected.to include(build.token) }
- it { is_expected.to include('gitlab-ci-token') }
- it { is_expected.to include(project.web_url[7..-1]) }
- end
-
- describe '#artifacts_expire_in' do
- subject { build.artifacts_expire_in }
- it { is_expected.to be_nil }
-
- context 'when artifacts_expire_at is specified' do
- let(:expire_at) { Time.now + 7.days }
-
- before { build.artifacts_expire_at = expire_at }
-
- it { is_expected.to be_within(5).of(expire_at - Time.now) }
- end
- end
-
- describe '#artifacts_expire_in=' do
- subject { build.artifacts_expire_in }
-
- it 'when assigning valid duration' do
- build.artifacts_expire_in = '7 days'
-
- is_expected.to be_within(10).of(7.days.to_i)
- end
-
- it 'when assigning invalid duration' do
- expect { build.artifacts_expire_in = '7 elephants' }.to raise_error(ChronicDuration::DurationParseError)
- is_expected.to be_nil
- end
-
- it 'when resseting value' do
- build.artifacts_expire_in = nil
-
- is_expected.to be_nil
- end
- end
-
- describe '#keep_artifacts!' do
- let(:build) { create(:ci_build, artifacts_expire_at: Time.now + 7.days) }
-
- it 'to reset expire_at' do
- build.keep_artifacts!
-
- expect(build.artifacts_expire_at).to be_nil
- end
- end
-
- describe '#depends_on_builds' do
- let!(:build) { create(:ci_build, pipeline: pipeline, name: 'build', stage_idx: 0, stage: 'build') }
- let!(:rspec_test) { create(:ci_build, pipeline: pipeline, name: 'rspec', stage_idx: 1, stage: 'test') }
- let!(:rubocop_test) { create(:ci_build, pipeline: pipeline, name: 'rubocop', stage_idx: 1, stage: 'test') }
- let!(:staging) { create(:ci_build, pipeline: pipeline, name: 'staging', stage_idx: 2, stage: 'deploy') }
-
- it 'expects to have no dependents if this is first build' do
- expect(build.depends_on_builds).to be_empty
- end
-
- it 'expects to have one dependent if this is test' do
- expect(rspec_test.depends_on_builds.map(&:id)).to contain_exactly(build.id)
- end
-
- it 'expects to have all builds from build and test stage if this is last' do
- expect(staging.depends_on_builds.map(&:id)).to contain_exactly(build.id, rspec_test.id, rubocop_test.id)
- end
-
- it 'expects to have retried builds instead the original ones' do
- retried_rspec = Ci::Build.retry(rspec_test)
- expect(staging.depends_on_builds.map(&:id)).to contain_exactly(build.id, retried_rspec.id, rubocop_test.id)
- end
- end
-
- def create_mr(build, pipeline, factory: :merge_request, created_at: Time.now)
- create(factory, source_project_id: pipeline.gl_project_id,
- target_project_id: pipeline.gl_project_id,
- source_branch: build.ref,
- created_at: created_at)
- end
-
- describe '#merge_request' do
- context 'when a MR has a reference to the pipeline' do
- before do
- @merge_request = create_mr(build, pipeline, factory: :merge_request)
-
- commits = [double(id: pipeline.sha)]
- allow(@merge_request).to receive(:commits).and_return(commits)
- allow(MergeRequest).to receive_message_chain(:includes, :where, :reorder).and_return([@merge_request])
- end
-
- it 'returns the single associated MR' do
- expect(build.merge_request.id).to eq(@merge_request.id)
- end
- end
-
- context 'when there is not a MR referencing the pipeline' do
- it 'returns nil' do
- expect(build.merge_request).to be_nil
- end
- end
-
- context 'when more than one MR have a reference to the pipeline' do
- before do
- @merge_request = create_mr(build, pipeline, factory: :merge_request)
- @merge_request.close!
- @merge_request2 = create_mr(build, pipeline, factory: :merge_request)
-
- commits = [double(id: pipeline.sha)]
- allow(@merge_request).to receive(:commits).and_return(commits)
- allow(@merge_request2).to receive(:commits).and_return(commits)
- allow(MergeRequest).to receive_message_chain(:includes, :where, :reorder).and_return([@merge_request, @merge_request2])
- end
-
- it 'returns the first MR' do
- expect(build.merge_request.id).to eq(@merge_request.id)
- end
- end
-
- context 'when a Build is created after the MR' do
- before do
- @merge_request = create_mr(build, pipeline, factory: :merge_request_with_diffs)
- pipeline2 = create(:ci_pipeline, project: project)
- @build2 = create(:ci_build, pipeline: pipeline2)
-
- allow(@merge_request).to receive(:commits_sha).
- and_return([pipeline.sha, pipeline2.sha])
- allow(MergeRequest).to receive_message_chain(:includes, :where, :reorder).and_return([@merge_request])
- end
-
- it 'returns the current MR' do
- expect(@build2.merge_request.id).to eq(@merge_request.id)
- end
- end
- end
-
- describe 'build erasable' do
- shared_examples 'erasable' do
- it 'removes artifact file' do
- expect(build.artifacts_file.exists?).to be_falsy
- end
-
- it 'removes artifact metadata file' do
- expect(build.artifacts_metadata.exists?).to be_falsy
- end
-
- it 'erases build trace in trace file' do
- expect(build.trace).to be_empty
- end
-
- it 'sets erased to true' do
- expect(build.erased?).to be true
- end
-
- it 'sets erase date' do
- expect(build.erased_at).not_to be_falsy
- end
- end
-
- context 'build is not erasable' do
- let!(:build) { create(:ci_build) }
-
- describe '#erase' do
- subject { build.erase }
-
- it { is_expected.to be false }
- end
-
- describe '#erasable?' do
- subject { build.erasable? }
- it { is_expected.to eq false }
- end
- end
-
- context 'build is erasable' do
- let!(:build) { create(:ci_build, :trace, :success, :artifacts) }
-
- describe '#erase' do
- before do
- build.erase(erased_by: user)
- end
-
- context 'erased by user' do
- let!(:user) { create(:user, username: 'eraser') }
-
- include_examples 'erasable'
-
- it 'records user who erased a build' do
- expect(build.erased_by).to eq user
- end
- end
-
- context 'erased by system' do
- let(:user) { nil }
-
- include_examples 'erasable'
-
- it 'does not set user who erased a build' do
- expect(build.erased_by).to be_nil
- end
- end
- end
-
- describe '#erasable?' do
- subject { build.erasable? }
- it { is_expected.to be_truthy }
- end
-
- describe '#erased?' do
- let!(:build) { create(:ci_build, :trace, :success, :artifacts) }
- subject { build.erased? }
-
- context 'build has not been erased' do
- it { is_expected.to be_falsey }
- end
-
- context 'build has been erased' do
- before do
- build.erase
- end
-
- it { is_expected.to be_truthy }
- end
- end
-
- context 'metadata and build trace are not available' do
- let!(:build) { create(:ci_build, :success, :artifacts) }
-
- before do
- build.remove_artifacts_metadata!
- end
-
- describe '#erase' do
- it 'does not raise error' do
- expect { build.erase }.not_to raise_error
- end
- end
- end
- end
- end
-
- describe '#commit' do
- it 'returns commit pipeline has been created for' do
- expect(build.commit).to eq project.commit
- end
- end
-
- describe '#when' do
- subject { build.when }
-
- context 'when `when` is undefined' do
- before do
- build.when = nil
- end
-
- context 'use from gitlab-ci.yml' do
- before do
- stub_ci_pipeline_yaml_file(config)
- end
-
- context 'when config is not found' do
- let(:config) { nil }
-
- it { is_expected.to eq('on_success') }
- end
-
- context 'when config does not have a questioned job' do
- let(:config) do
- YAML.dump({
- test_other: {
- script: 'Hello World'
- }
- })
- end
-
- it { is_expected.to eq('on_success') }
- end
-
- context 'when config has `when`' do
- let(:config) do
- YAML.dump({
- test: {
- script: 'Hello World',
- when: 'always'
- }
- })
- end
-
- it { is_expected.to eq('always') }
- end
- end
- end
- end
-
- describe '#cancelable?' do
- subject { build }
-
- context 'when build is cancelable' do
- context 'when build is pending' do
- it { is_expected.to be_cancelable }
- end
-
- context 'when build is running' do
- before do
- build.run!
- end
-
- it { is_expected.to be_cancelable }
- end
- end
-
- context 'when build is not cancelable' do
- context 'when build is successful' do
- before do
- build.success!
- end
-
- it { is_expected.not_to be_cancelable }
- end
-
- context 'when build is failed' do
- before do
- build.drop!
- end
-
- it { is_expected.not_to be_cancelable }
- end
- end
- end
-
- describe '#retryable?' do
- subject { build }
-
- context 'when build is retryable' do
- context 'when build is successful' do
- before do
- build.success!
- end
-
- it { is_expected.to be_retryable }
- end
-
- context 'when build is failed' do
- before do
- build.drop!
- end
-
- it { is_expected.to be_retryable }
- end
-
- context 'when build is canceled' do
- before do
- build.cancel!
- end
-
- it { is_expected.to be_retryable }
- end
- end
-
- context 'when build is not retryable' do
- context 'when build is running' do
- before do
- build.run!
- end
-
- it { is_expected.not_to be_retryable }
- end
-
- context 'when build is skipped' do
- before do
- build.skip!
- end
-
- it { is_expected.not_to be_retryable }
- end
- end
- end
-
- describe '#manual?' do
- before do
- build.update(when: value)
- end
-
- subject { build.manual? }
-
- context 'when is set to manual' do
- let(:value) { 'manual' }
-
- it { is_expected.to be_truthy }
- end
-
- context 'when set to something else' do
- let(:value) { 'something else' }
-
- it { is_expected.to be_falsey }
- end
- end
-
- describe '#other_actions' do
- let(:build) { create(:ci_build, :manual, pipeline: pipeline) }
- let!(:other_build) { create(:ci_build, :manual, pipeline: pipeline, name: 'other action') }
-
- subject { build.other_actions }
-
- it 'returns other actions' do
- is_expected.to contain_exactly(other_build)
- end
-
- context 'when build is retried' do
- let!(:new_build) { Ci::Build.retry(build) }
-
- it 'does not return any of them' do
- is_expected.not_to include(build, new_build)
- end
- end
-
- context 'when other build is retried' do
- let!(:retried_build) { Ci::Build.retry(other_build) }
-
- it 'returns a retried build' do
- is_expected.to contain_exactly(retried_build)
- end
- end
- end
-
- describe '#play' do
- let(:build) { create(:ci_build, :manual, pipeline: pipeline) }
-
- subject { build.play }
-
- it 'enqueues a build' do
- is_expected.to be_pending
- is_expected.to eq(build)
- end
-
- context 'for successful build' do
- before do
- build.update(status: 'success')
- end
-
- it 'creates a new build' do
- is_expected.to be_pending
- is_expected.not_to eq(build)
- end
- end
- end
-
- describe '#when' do
- subject { build.when }
-
- context 'when `when` is undefined' do
- before do
- build.when = nil
- end
-
- context 'use from gitlab-ci.yml' do
- before do
- stub_ci_pipeline_yaml_file(config)
- end
-
- context 'when config is not found' do
- let(:config) { nil }
-
- it { is_expected.to eq('on_success') }
- end
-
- context 'when config does not have a questioned job' do
- let(:config) do
- YAML.dump({
- test_other: {
- script: 'Hello World'
- }
- })
- end
-
- it { is_expected.to eq('on_success') }
- end
-
- context 'when config has when' do
- let(:config) do
- YAML.dump({
- test: {
- script: 'Hello World',
- when: 'always'
- }
- })
- end
-
- it { is_expected.to eq('always') }
- end
- end
- end
- end
-
- describe '#retryable?' do
- context 'when build is running' do
- before { build.run! }
-
- it 'returns false' do
- expect(build).not_to be_retryable
- end
- end
-
- context 'when build is finished' do
- before do
- build.success!
- end
-
- it 'returns true' do
- expect(build).to be_retryable
- end
- end
- end
-
- describe '#has_environment?' do
- subject { build.has_environment? }
-
- context 'when environment is defined' do
- before do
- build.update(environment: 'review')
- end
-
- it { is_expected.to be_truthy }
- end
-
- context 'when environment is not defined' do
- before do
- build.update(environment: nil)
- end
-
- it { is_expected.to be_falsey }
- end
- end
-
- describe '#starts_environment?' do
- subject { build.starts_environment? }
-
- context 'when environment is defined' do
- before do
- build.update(environment: 'review')
- end
-
- context 'no action is defined' do
- it { is_expected.to be_truthy }
- end
-
- context 'and start action is defined' do
- before do
- build.update(options: { environment: { action: 'start' } } )
- end
-
- it { is_expected.to be_truthy }
- end
- end
-
- context 'when environment is not defined' do
- before do
- build.update(environment: nil)
- end
-
- it { is_expected.to be_falsey }
- end
- end
-
- describe '#stops_environment?' do
- subject { build.stops_environment? }
-
- context 'when environment is defined' do
- before do
- build.update(environment: 'review')
- end
-
- context 'no action is defined' do
- it { is_expected.to be_falsey }
- end
-
- context 'and stop action is defined' do
- before do
- build.update(options: { environment: { action: 'stop' } } )
- end
-
- it { is_expected.to be_truthy }
- end
- end
-
- context 'when environment is not defined' do
- before do
- build.update(environment: nil)
- end
-
- it { is_expected.to be_falsey }
- end
- end
-
- describe '#last_deployment' do
- subject { build.last_deployment }
-
- context 'when multiple deployments are created' do
- let!(:deployment1) { create(:deployment, deployable: build) }
- let!(:deployment2) { create(:deployment, deployable: build) }
-
- it 'returns the latest one' do
- is_expected.to eq(deployment2)
- end
- end
- end
-
- describe '#outdated_deployment?' do
- subject { build.outdated_deployment? }
-
- context 'when build succeeded' do
- let(:build) { create(:ci_build, :success) }
- let!(:deployment) { create(:deployment, deployable: build) }
-
- context 'current deployment is latest' do
- it { is_expected.to be_falsey }
- end
-
- context 'current deployment is not latest on environment' do
- let!(:deployment2) { create(:deployment, environment: deployment.environment) }
-
- it { is_expected.to be_truthy }
- end
- end
-
- context 'when build failed' do
- let(:build) { create(:ci_build, :failed) }
-
- it { is_expected.to be_falsey }
- end
- end
-
- describe '#expanded_environment_name' do
- subject { build.expanded_environment_name }
-
- context 'when environment uses $CI_BUILD_REF_NAME' do
- let(:build) do
- create(:ci_build,
- ref: 'master',
- environment: 'review/$CI_BUILD_REF_NAME')
- end
-
- it { is_expected.to eq('review/master') }
- end
-
- context 'when environment uses yaml_variables containing symbol keys' do
- let(:build) do
- create(:ci_build,
- yaml_variables: [{ key: :APP_HOST, value: 'host' }],
- environment: 'review/$APP_HOST')
- end
-
- it { is_expected.to eq('review/host') }
- end
- end
-
- describe '#detailed_status' do
- let(:user) { create(:user) }
-
- it 'returns a detailed status' do
- expect(build.detailed_status(user))
- .to be_a Gitlab::Ci::Status::Build::Cancelable
- end
- end
-end
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index a7e90c8a381..4080092405d 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -1,14 +1,1003 @@
require 'spec_helper'
-describe Ci::Build, models: true do
- let(:build) { create(:ci_build) }
+describe Ci::Build, :models do
+ let(:project) { create(:project, :repository) }
+ let(:build) { create(:ci_build, pipeline: pipeline) }
let(:test_trace) { 'This is a test' }
+ let(:pipeline) do
+ create(:ci_pipeline, project: project,
+ sha: project.commit.id,
+ ref: project.default_branch,
+ status: 'success')
+ end
+
it { is_expected.to belong_to(:runner) }
it { is_expected.to belong_to(:trigger_request) }
it { is_expected.to belong_to(:erased_by) }
-
it { is_expected.to have_many(:deployments) }
+ it { is_expected.to validate_presence_of :ref }
+ it { is_expected.to respond_to :trace_html }
+
+ describe '#any_runners_online?' do
+ subject { build.any_runners_online? }
+
+ context 'when no runners' do
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when there are runners' do
+ let(:runner) { create(:ci_runner) }
+
+ before do
+ build.project.runners << runner
+ runner.update_attributes(contacted_at: 1.second.ago)
+ end
+
+ it { is_expected.to be_truthy }
+
+ it 'that is inactive' do
+ runner.update_attributes(active: false)
+ is_expected.to be_falsey
+ end
+
+ it 'that is not online' do
+ runner.update_attributes(contacted_at: nil)
+ is_expected.to be_falsey
+ end
+
+ it 'that cannot handle build' do
+ expect_any_instance_of(Ci::Runner).to receive(:can_pick?).and_return(false)
+ is_expected.to be_falsey
+ end
+ end
+ end
+
+ describe '#append_trace' do
+ subject { build.trace_html }
+
+ context 'when build.trace hides runners token' do
+ let(:token) { 'my_secret_token' }
+
+ before do
+ build.project.update(runners_token: token)
+ build.append_trace(token, 0)
+ end
+
+ it { is_expected.not_to include(token) }
+ end
+
+ context 'when build.trace hides build token' do
+ let(:token) { 'my_secret_token' }
+
+ before do
+ build.update(token: token)
+ build.append_trace(token, 0)
+ end
+
+ it { is_expected.not_to include(token) }
+ end
+ end
+
+ describe '#artifacts?' do
+ subject { build.artifacts? }
+
+ context 'artifacts archive does not exist' do
+ before do
+ build.update_attributes(artifacts_file: nil)
+ end
+
+ it { is_expected.to be_falsy }
+ end
+
+ context 'artifacts archive exists' do
+ let(:build) { create(:ci_build, :artifacts) }
+ it { is_expected.to be_truthy }
+
+ context 'is expired' do
+ before { build.update(artifacts_expire_at: Time.now - 7.days) }
+ it { is_expected.to be_falsy }
+ end
+
+ context 'is not expired' do
+ before { build.update(artifacts_expire_at: Time.now + 7.days) }
+ it { is_expected.to be_truthy }
+ end
+ end
+ end
+
+ describe '#artifacts_expired?' do
+ subject { build.artifacts_expired? }
+
+ context 'is expired' do
+ before { build.update(artifacts_expire_at: Time.now - 7.days) }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'is not expired' do
+ before { build.update(artifacts_expire_at: Time.now + 7.days) }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ describe '#artifacts_metadata?' do
+ subject { build.artifacts_metadata? }
+ context 'artifacts metadata does not exist' do
+ it { is_expected.to be_falsy }
+ end
+
+ context 'artifacts archive is a zip file and metadata exists' do
+ let(:build) { create(:ci_build, :artifacts) }
+ it { is_expected.to be_truthy }
+ end
+ end
+
+ describe '#artifacts_expire_in' do
+ subject { build.artifacts_expire_in }
+ it { is_expected.to be_nil }
+
+ context 'when artifacts_expire_at is specified' do
+ let(:expire_at) { Time.now + 7.days }
+
+ before { build.artifacts_expire_at = expire_at }
+
+ it { is_expected.to be_within(5).of(expire_at - Time.now) }
+ end
+ end
+
+ describe '#artifacts_expire_in=' do
+ subject { build.artifacts_expire_in }
+
+ it 'when assigning valid duration' do
+ build.artifacts_expire_in = '7 days'
+
+ is_expected.to be_within(10).of(7.days.to_i)
+ end
+
+ it 'when assigning invalid duration' do
+ expect { build.artifacts_expire_in = '7 elephants' }.to raise_error(ChronicDuration::DurationParseError)
+ is_expected.to be_nil
+ end
+
+ it 'when resseting value' do
+ build.artifacts_expire_in = nil
+
+ is_expected.to be_nil
+ end
+ end
+
+ describe '#commit' do
+ it 'returns commit pipeline has been created for' do
+ expect(build.commit).to eq project.commit
+ end
+ end
+
+ describe '#create_from' do
+ before do
+ build.status = 'success'
+ build.save
+ end
+ let(:create_from_build) { Ci::Build.create_from build }
+
+ it 'exists a pending task' do
+ expect(Ci::Build.pending.count(:all)).to eq 0
+ create_from_build
+ expect(Ci::Build.pending.count(:all)).to be > 0
+ end
+ end
+
+ describe '#depends_on_builds' do
+ let!(:build) { create(:ci_build, pipeline: pipeline, name: 'build', stage_idx: 0, stage: 'build') }
+ let!(:rspec_test) { create(:ci_build, pipeline: pipeline, name: 'rspec', stage_idx: 1, stage: 'test') }
+ let!(:rubocop_test) { create(:ci_build, pipeline: pipeline, name: 'rubocop', stage_idx: 1, stage: 'test') }
+ let!(:staging) { create(:ci_build, pipeline: pipeline, name: 'staging', stage_idx: 2, stage: 'deploy') }
+
+ it 'expects to have no dependents if this is first build' do
+ expect(build.depends_on_builds).to be_empty
+ end
+
+ it 'expects to have one dependent if this is test' do
+ expect(rspec_test.depends_on_builds.map(&:id)).to contain_exactly(build.id)
+ end
+
+ it 'expects to have all builds from build and test stage if this is last' do
+ expect(staging.depends_on_builds.map(&:id)).to contain_exactly(build.id, rspec_test.id, rubocop_test.id)
+ end
+
+ it 'expects to have retried builds instead the original ones' do
+ retried_rspec = Ci::Build.retry(rspec_test)
+ expect(staging.depends_on_builds.map(&:id)).to contain_exactly(build.id, retried_rspec.id, rubocop_test.id)
+ end
+ end
+
+ describe '#detailed_status' do
+ let(:user) { create(:user) }
+
+ it 'returns a detailed status' do
+ expect(build.detailed_status(user))
+ .to be_a Gitlab::Ci::Status::Build::Cancelable
+ end
+ end
+
+ describe '#coverage_regex' do
+ subject { build.coverage_regex }
+
+ context 'when project has build_coverage_regex set' do
+ let(:project_regex) { '\(\d+\.\d+\) covered' }
+
+ before do
+ project.build_coverage_regex = project_regex
+ end
+
+ context 'and coverage_regex attribute is not set' do
+ it { is_expected.to eq(project_regex) }
+ end
+
+ context 'but coverage_regex attribute is also set' do
+ let(:build_regex) { 'Code coverage: \d+\.\d+' }
+
+ before do
+ build.coverage_regex = build_regex
+ end
+
+ it { is_expected.to eq(build_regex) }
+ end
+ end
+
+ context 'when neither project nor build has coverage regex set' do
+ it { is_expected.to be_nil }
+ end
+ end
+
+ describe '#update_coverage' do
+ context "regarding coverage_regex's value," do
+ it "saves the correct extracted coverage value" do
+ build.coverage_regex = '\(\d+.\d+\%\) covered'
+ allow(build).to receive(:trace) { 'Coverage 1033 / 1051 LOC (98.29%) covered' }
+ expect(build).to receive(:update_attributes).with(coverage: 98.29) { true }
+ expect(build.update_coverage).to be true
+ end
+ end
+ end
+
+ describe 'deployment' do
+ describe '#last_deployment' do
+ subject { build.last_deployment }
+
+ context 'when multiple deployments are created' do
+ let!(:deployment1) { create(:deployment, deployable: build) }
+ let!(:deployment2) { create(:deployment, deployable: build) }
+
+ it 'returns the latest one' do
+ is_expected.to eq(deployment2)
+ end
+ end
+ end
+
+ describe '#outdated_deployment?' do
+ subject { build.outdated_deployment? }
+
+ context 'when build succeeded' do
+ let(:build) { create(:ci_build, :success) }
+ let!(:deployment) { create(:deployment, deployable: build) }
+
+ context 'current deployment is latest' do
+ it { is_expected.to be_falsey }
+ end
+
+ context 'current deployment is not latest on environment' do
+ let!(:deployment2) { create(:deployment, environment: deployment.environment) }
+
+ it { is_expected.to be_truthy }
+ end
+ end
+
+ context 'when build failed' do
+ let(:build) { create(:ci_build, :failed) }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+ end
+
+ describe 'environment' do
+ describe '#has_environment?' do
+ subject { build.has_environment? }
+
+ context 'when environment is defined' do
+ before do
+ build.update(environment: 'review')
+ end
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when environment is not defined' do
+ before do
+ build.update(environment: nil)
+ end
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ describe '#expanded_environment_name' do
+ subject { build.expanded_environment_name }
+
+ context 'when environment uses $CI_BUILD_REF_NAME' do
+ let(:build) do
+ create(:ci_build,
+ ref: 'master',
+ environment: 'review/$CI_BUILD_REF_NAME')
+ end
+
+ it { is_expected.to eq('review/master') }
+ end
+
+ context 'when environment uses yaml_variables containing symbol keys' do
+ let(:build) do
+ create(:ci_build,
+ yaml_variables: [{ key: :APP_HOST, value: 'host' }],
+ environment: 'review/$APP_HOST')
+ end
+
+ it { is_expected.to eq('review/host') }
+ end
+ end
+
+ describe '#starts_environment?' do
+ subject { build.starts_environment? }
+
+ context 'when environment is defined' do
+ before do
+ build.update(environment: 'review')
+ end
+
+ context 'no action is defined' do
+ it { is_expected.to be_truthy }
+ end
+
+ context 'and start action is defined' do
+ before do
+ build.update(options: { environment: { action: 'start' } } )
+ end
+
+ it { is_expected.to be_truthy }
+ end
+ end
+
+ context 'when environment is not defined' do
+ before do
+ build.update(environment: nil)
+ end
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ describe '#stops_environment?' do
+ subject { build.stops_environment? }
+
+ context 'when environment is defined' do
+ before do
+ build.update(environment: 'review')
+ end
+
+ context 'no action is defined' do
+ it { is_expected.to be_falsey }
+ end
+
+ context 'and stop action is defined' do
+ before do
+ build.update(options: { environment: { action: 'stop' } } )
+ end
+
+ it { is_expected.to be_truthy }
+ end
+ end
+
+ context 'when environment is not defined' do
+ before do
+ build.update(environment: nil)
+ end
+
+ it { is_expected.to be_falsey }
+ end
+ end
+ end
+
+ describe 'erasable build' do
+ shared_examples 'erasable' do
+ it 'removes artifact file' do
+ expect(build.artifacts_file.exists?).to be_falsy
+ end
+
+ it 'removes artifact metadata file' do
+ expect(build.artifacts_metadata.exists?).to be_falsy
+ end
+
+ it 'erases build trace in trace file' do
+ expect(build.trace).to be_empty
+ end
+
+ it 'sets erased to true' do
+ expect(build.erased?).to be true
+ end
+
+ it 'sets erase date' do
+ expect(build.erased_at).not_to be_falsy
+ end
+ end
+
+ context 'build is not erasable' do
+ let!(:build) { create(:ci_build) }
+
+ describe '#erase' do
+ subject { build.erase }
+
+ it { is_expected.to be false }
+ end
+
+ describe '#erasable?' do
+ subject { build.erasable? }
+ it { is_expected.to eq false }
+ end
+ end
+
+ context 'build is erasable' do
+ let!(:build) { create(:ci_build, :trace, :success, :artifacts) }
+
+ describe '#erase' do
+ before do
+ build.erase(erased_by: user)
+ end
+
+ context 'erased by user' do
+ let!(:user) { create(:user, username: 'eraser') }
+
+ include_examples 'erasable'
+
+ it 'records user who erased a build' do
+ expect(build.erased_by).to eq user
+ end
+ end
+
+ context 'erased by system' do
+ let(:user) { nil }
+
+ include_examples 'erasable'
+
+ it 'does not set user who erased a build' do
+ expect(build.erased_by).to be_nil
+ end
+ end
+ end
+
+ describe '#erasable?' do
+ subject { build.erasable? }
+ it { is_expected.to be_truthy }
+ end
+
+ describe '#erased?' do
+ let!(:build) { create(:ci_build, :trace, :success, :artifacts) }
+ subject { build.erased? }
+
+ context 'job has not been erased' do
+ it { is_expected.to be_falsey }
+ end
+
+ context 'job has been erased' do
+ before do
+ build.erase
+ end
+
+ it { is_expected.to be_truthy }
+ end
+ end
+
+ context 'metadata and build trace are not available' do
+ let!(:build) { create(:ci_build, :success, :artifacts) }
+
+ before do
+ build.remove_artifacts_metadata!
+ end
+
+ describe '#erase' do
+ it 'does not raise error' do
+ expect { build.erase }.not_to raise_error
+ end
+ end
+ end
+ end
+ end
+
+ describe '#extract_coverage' do
+ context 'valid content & regex' do
+ subject { build.extract_coverage('Coverage 1033 / 1051 LOC (98.29%) covered', '\(\d+.\d+\%\) covered') }
+
+ it { is_expected.to eq(98.29) }
+ end
+
+ context 'valid content & bad regex' do
+ subject { build.extract_coverage('Coverage 1033 / 1051 LOC (98.29%) covered', 'very covered') }
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'no coverage content & regex' do
+ subject { build.extract_coverage('No coverage for today :sad:', '\(\d+.\d+\%\) covered') }
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'multiple results in content & regex' do
+ subject { build.extract_coverage(' (98.39%) covered. (98.29%) covered', '\(\d+.\d+\%\) covered') }
+
+ it { is_expected.to eq(98.29) }
+ end
+
+ context 'using a regex capture' do
+ subject { build.extract_coverage('TOTAL 9926 3489 65%', 'TOTAL\s+\d+\s+\d+\s+(\d{1,3}\%)') }
+
+ it { is_expected.to eq(65) }
+ end
+ end
+
+ describe '#first_pending' do
+ let!(:first) { create(:ci_build, pipeline: pipeline, status: 'pending', created_at: Date.yesterday) }
+ let!(:second) { create(:ci_build, pipeline: pipeline, status: 'pending') }
+ subject { Ci::Build.first_pending }
+
+ it { is_expected.to be_a(Ci::Build) }
+ it('returns with the first pending build') { is_expected.to eq(first) }
+ end
+
+ describe '#failed_but_allowed?' do
+ subject { build.failed_but_allowed? }
+
+ context 'when build is not allowed to fail' do
+ before do
+ build.allow_failure = false
+ end
+
+ context 'and build.status is success' do
+ before do
+ build.status = 'success'
+ end
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'and build.status is failed' do
+ before do
+ build.status = 'failed'
+ end
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ context 'when build is allowed to fail' do
+ before do
+ build.allow_failure = true
+ end
+
+ context 'and build.status is success' do
+ before do
+ build.status = 'success'
+ end
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'and build.status is failed' do
+ before do
+ build.status = 'failed'
+ end
+
+ it { is_expected.to be_truthy }
+ end
+ end
+ end
+
+ describe 'flags' do
+ describe '#cancelable?' do
+ subject { build }
+
+ context 'when build is cancelable' do
+ context 'when build is pending' do
+ it { is_expected.to be_cancelable }
+ end
+
+ context 'when build is running' do
+ before do
+ build.run!
+ end
+
+ it { is_expected.to be_cancelable }
+ end
+ end
+
+ context 'when build is not cancelable' do
+ context 'when build is successful' do
+ before do
+ build.success!
+ end
+
+ it { is_expected.not_to be_cancelable }
+ end
+
+ context 'when build is failed' do
+ before do
+ build.drop!
+ end
+
+ it { is_expected.not_to be_cancelable }
+ end
+ end
+ end
+
+ describe '#retryable?' do
+ subject { build }
+
+ context 'when build is retryable' do
+ context 'when build is successful' do
+ before do
+ build.success!
+ end
+
+ it { is_expected.to be_retryable }
+ end
+
+ context 'when build is failed' do
+ before do
+ build.drop!
+ end
+
+ it { is_expected.to be_retryable }
+ end
+
+ context 'when build is canceled' do
+ before do
+ build.cancel!
+ end
+
+ it { is_expected.to be_retryable }
+ end
+ end
+
+ context 'when build is not retryable' do
+ context 'when build is running' do
+ before do
+ build.run!
+ end
+
+ it { is_expected.not_to be_retryable }
+ end
+
+ context 'when build is skipped' do
+ before do
+ build.skip!
+ end
+
+ it { is_expected.not_to be_retryable }
+ end
+ end
+ end
+
+ describe '#manual?' do
+ before do
+ build.update(when: value)
+ end
+
+ subject { build.manual? }
+
+ context 'when is set to manual' do
+ let(:value) { 'manual' }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when set to something else' do
+ let(:value) { 'something else' }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+ end
+
+ describe '#has_tags?' do
+ context 'when build has tags' do
+ subject { create(:ci_build, tag_list: ['tag']) }
+ it { is_expected.to have_tags }
+ end
+
+ context 'when build does not have tags' do
+ subject { create(:ci_build, tag_list: []) }
+ it { is_expected.not_to have_tags }
+ end
+ end
+
+ describe '#keep_artifacts!' do
+ let(:build) { create(:ci_build, artifacts_expire_at: Time.now + 7.days) }
+
+ it 'to reset expire_at' do
+ build.keep_artifacts!
+
+ expect(build.artifacts_expire_at).to be_nil
+ end
+ end
+
+ describe '#merge_request' do
+ def create_mr(build, pipeline, factory: :merge_request, created_at: Time.now)
+ create(factory, source_project_id: pipeline.gl_project_id,
+ target_project_id: pipeline.gl_project_id,
+ source_branch: build.ref,
+ created_at: created_at)
+ end
+
+ context 'when a MR has a reference to the pipeline' do
+ before do
+ @merge_request = create_mr(build, pipeline, factory: :merge_request)
+
+ commits = [double(id: pipeline.sha)]
+ allow(@merge_request).to receive(:commits).and_return(commits)
+ allow(MergeRequest).to receive_message_chain(:includes, :where, :reorder).and_return([@merge_request])
+ end
+
+ it 'returns the single associated MR' do
+ expect(build.merge_request.id).to eq(@merge_request.id)
+ end
+ end
+
+ context 'when there is not a MR referencing the pipeline' do
+ it 'returns nil' do
+ expect(build.merge_request).to be_nil
+ end
+ end
+
+ context 'when more than one MR have a reference to the pipeline' do
+ before do
+ @merge_request = create_mr(build, pipeline, factory: :merge_request)
+ @merge_request.close!
+ @merge_request2 = create_mr(build, pipeline, factory: :merge_request)
+
+ commits = [double(id: pipeline.sha)]
+ allow(@merge_request).to receive(:commits).and_return(commits)
+ allow(@merge_request2).to receive(:commits).and_return(commits)
+ allow(MergeRequest).to receive_message_chain(:includes, :where, :reorder).and_return([@merge_request, @merge_request2])
+ end
+
+ it 'returns the first MR' do
+ expect(build.merge_request.id).to eq(@merge_request.id)
+ end
+ end
+
+ context 'when a Build is created after the MR' do
+ before do
+ @merge_request = create_mr(build, pipeline, factory: :merge_request_with_diffs)
+ pipeline2 = create(:ci_pipeline, project: project)
+ @build2 = create(:ci_build, pipeline: pipeline2)
+
+ allow(@merge_request).to receive(:commits_sha).
+ and_return([pipeline.sha, pipeline2.sha])
+ allow(MergeRequest).to receive_message_chain(:includes, :where, :reorder).and_return([@merge_request])
+ end
+
+ it 'returns the current MR' do
+ expect(@build2.merge_request.id).to eq(@merge_request.id)
+ end
+ end
+ end
+
+ describe '#options' do
+ let(:options) do
+ {
+ image: "ruby:2.1",
+ services: [
+ "postgres"
+ ]
+ }
+ end
+
+ it 'contains options' do
+ expect(build.options).to eq(options)
+ end
+ end
+
+ describe '#other_actions' do
+ let(:build) { create(:ci_build, :manual, pipeline: pipeline) }
+ let!(:other_build) { create(:ci_build, :manual, pipeline: pipeline, name: 'other action') }
+
+ subject { build.other_actions }
+
+ it 'returns other actions' do
+ is_expected.to contain_exactly(other_build)
+ end
+
+ context 'when build is retried' do
+ let!(:new_build) { Ci::Build.retry(build) }
+
+ it 'does not return any of them' do
+ is_expected.not_to include(build, new_build)
+ end
+ end
+
+ context 'when other build is retried' do
+ let!(:retried_build) { Ci::Build.retry(other_build) }
+
+ it 'returns a retried build' do
+ is_expected.to contain_exactly(retried_build)
+ end
+ end
+ end
+
+ describe '#persisted_environment' do
+ before do
+ @environment = create(:environment, project: project, name: "foo-#{project.default_branch}")
+ end
+
+ subject { build.persisted_environment }
+
+ context 'referenced literally' do
+ let(:build) { create(:ci_build, pipeline: pipeline, environment: "foo-#{project.default_branch}") }
+
+ it { is_expected.to eq(@environment) }
+ end
+
+ context 'referenced with a variable' do
+ let(:build) { create(:ci_build, pipeline: pipeline, environment: "foo-$CI_BUILD_REF_NAME") }
+
+ it { is_expected.to eq(@environment) }
+ end
+ end
+
+ describe '#play' do
+ let(:build) { create(:ci_build, :manual, pipeline: pipeline) }
+
+ subject { build.play }
+
+ it 'enqueues a build' do
+ is_expected.to be_pending
+ is_expected.to eq(build)
+ end
+
+ context 'for successful build' do
+ before do
+ build.update(status: 'success')
+ end
+
+ it 'creates a new build' do
+ is_expected.to be_pending
+ is_expected.not_to eq(build)
+ end
+ end
+ end
+
+ describe 'project settings' do
+ describe '#timeout' do
+ it 'returns project timeout configuration' do
+ expect(build.timeout).to eq(project.build_timeout)
+ end
+ end
+
+ describe '#allow_git_fetch' do
+ it 'return project allow_git_fetch configuration' do
+ expect(build.allow_git_fetch).to eq(project.build_allow_git_fetch)
+ end
+ end
+ end
+
+ describe '#project' do
+ subject { build.project }
+
+ it { is_expected.to eq(pipeline.project) }
+ end
+
+ describe '#project_id' do
+ subject { build.project_id }
+
+ it { is_expected.to eq(pipeline.project_id) }
+ end
+
+ describe '#project_name' do
+ subject { build.project_name }
+
+ it { is_expected.to eq(project.name) }
+ end
+
+ describe '#raw_trace' do
+ subject { build.raw_trace }
+
+ context 'when build.trace hides runners token' do
+ let(:token) { 'my_secret_token' }
+
+ before do
+ build.project.update(runners_token: token)
+ build.update(trace: token)
+ end
+
+ it { is_expected.not_to include(token) }
+ end
+
+ context 'when build.trace hides build token' do
+ let(:token) { 'my_secret_token' }
+
+ before do
+ build.update(token: token)
+ build.update(trace: token)
+ end
+
+ it { is_expected.not_to include(token) }
+ end
+ end
+
+ describe '#ref_slug' do
+ {
+ 'master' => 'master',
+ '1-foo' => '1-foo',
+ 'fix/1-foo' => 'fix-1-foo',
+ 'fix-1-foo' => 'fix-1-foo',
+ 'a' * 63 => 'a' * 63,
+ 'a' * 64 => 'a' * 63,
+ 'FOO' => 'foo',
+ }.each do |ref, slug|
+ it "transforms #{ref} to #{slug}" do
+ build.ref = ref
+
+ expect(build.ref_slug).to eq(slug)
+ end
+ end
+ end
+
+ describe '#repo_url' do
+ let(:build) { create(:ci_build) }
+ let(:project) { build.project }
+
+ subject { build.repo_url }
+
+ it { is_expected.to be_a(String) }
+ it { is_expected.to end_with(".git") }
+ it { is_expected.to start_with(project.web_url[0..6]) }
+ it { is_expected.to include(build.token) }
+ it { is_expected.to include('gitlab-ci-token') }
+ it { is_expected.to include(project.web_url[7..-1]) }
+ end
+
+ describe '#stuck?' do
+ subject { build.stuck? }
+
+ context "when commit_status.status is pending" do
+ before do
+ build.status = 'pending'
+ end
+
+ it { is_expected.to be_truthy }
+
+ context "and there are specific runner" do
+ let(:runner) { create(:ci_runner, contacted_at: 1.second.ago) }
+
+ before do
+ build.project.runners << runner
+ runner.save
+ end
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ %w[success failed canceled running].each do |state|
+ context "when commit_status.status is #{state}" do
+ before do
+ build.status = state
+ end
+
+ it { is_expected.to be_falsey }
+ end
+ end
+ end
describe '#trace' do
it 'obfuscates project runners token' do
@@ -24,6 +1013,63 @@ describe Ci::Build, models: true do
expect(build.trace).to eq(test_trace)
end
+
+ context 'when build does not have trace' do
+ it 'is is empty' do
+ expect(build.trace).to be_nil
+ end
+ end
+
+ context 'when trace contains text' do
+ let(:text) { 'example output' }
+ before do
+ build.trace = text
+ end
+
+ it { expect(build.trace).to eq(text) }
+ end
+
+ context 'when trace hides runners token' do
+ let(:token) { 'my_secret_token' }
+
+ before do
+ build.update(trace: token)
+ build.project.update(runners_token: token)
+ end
+
+ it { expect(build.trace).not_to include(token) }
+ it { expect(build.raw_trace).to include(token) }
+ end
+
+ context 'when build.trace hides build token' do
+ let(:token) { 'my_secret_token' }
+
+ before do
+ build.update(trace: token)
+ build.update(token: token)
+ end
+
+ it { expect(build.trace).not_to include(token) }
+ it { expect(build.raw_trace).to include(token) }
+ end
+ end
+
+ describe '#has_expiring_artifacts?' do
+ context 'when artifacts have expiration date set' do
+ before { build.update(artifacts_expire_at: 1.day.from_now) }
+
+ it 'has expiring artifacts' do
+ expect(build).to have_expiring_artifacts
+ end
+ end
+
+ context 'when artifacts do not have expiration date set' do
+ before { build.update(artifacts_expire_at: nil) }
+
+ it 'does not have expiring artifacts' do
+ expect(build).not_to have_expiring_artifacts
+ end
+ end
end
describe '#has_trace_file?' do
@@ -85,4 +1131,325 @@ describe Ci::Build, models: true do
it { expect(build.trace_file_path).to eq(build.old_path_to_trace) }
end
end
+
+ describe '#update_project_statistics' do
+ let!(:build) { create(:ci_build, artifacts_size: 23) }
+
+ it 'updates project statistics when the artifact size changes' do
+ expect(ProjectCacheWorker).to receive(:perform_async)
+ .with(build.project_id, [], [:build_artifacts_size])
+
+ build.artifacts_size = 42
+ build.save!
+ end
+
+ it 'does not update project statistics when the artifact size stays the same' do
+ expect(ProjectCacheWorker).not_to receive(:perform_async)
+
+ build.name = 'changed'
+ build.save!
+ end
+
+ it 'updates project statistics when the build is destroyed' do
+ expect(ProjectCacheWorker).to receive(:perform_async)
+ .with(build.project_id, [], [:build_artifacts_size])
+
+ build.destroy
+ end
+ end
+
+ describe '#when' do
+ subject { build.when }
+
+ context 'when `when` is undefined' do
+ before do
+ build.when = nil
+ end
+
+ context 'use from gitlab-ci.yml' do
+ before do
+ stub_ci_pipeline_yaml_file(config)
+ end
+
+ context 'when config is not found' do
+ let(:config) { nil }
+
+ it { is_expected.to eq('on_success') }
+ end
+
+ context 'when config does not have a questioned job' do
+ let(:config) do
+ YAML.dump({
+ test_other: {
+ script: 'Hello World'
+ }
+ })
+ end
+
+ it { is_expected.to eq('on_success') }
+ end
+
+ context 'when config has `when`' do
+ let(:config) do
+ YAML.dump({
+ test: {
+ script: 'Hello World',
+ when: 'always'
+ }
+ })
+ end
+
+ it { is_expected.to eq('always') }
+ end
+ end
+ end
+ end
+
+ describe '#variables' do
+ let(:container_registry_enabled) { false }
+ let(:predefined_variables) do
+ [
+ { key: 'CI', value: 'true', public: true },
+ { key: 'GITLAB_CI', value: 'true', public: true },
+ { key: 'CI_BUILD_ID', value: build.id.to_s, public: true },
+ { key: 'CI_BUILD_TOKEN', value: build.token, public: false },
+ { key: 'CI_BUILD_REF', value: build.sha, public: true },
+ { key: 'CI_BUILD_BEFORE_SHA', value: build.before_sha, public: true },
+ { key: 'CI_BUILD_REF_NAME', value: 'master', public: true },
+ { key: 'CI_BUILD_REF_SLUG', value: 'master', public: true },
+ { key: 'CI_BUILD_NAME', value: 'test', public: true },
+ { key: 'CI_BUILD_STAGE', value: 'test', public: true },
+ { key: 'CI_SERVER_NAME', value: 'GitLab', public: true },
+ { key: 'CI_SERVER_VERSION', value: Gitlab::VERSION, public: true },
+ { key: 'CI_SERVER_REVISION', value: Gitlab::REVISION, public: true },
+ { key: 'CI_PROJECT_ID', value: project.id.to_s, public: true },
+ { key: 'CI_PROJECT_NAME', value: project.path, public: true },
+ { key: 'CI_PROJECT_PATH', value: project.path_with_namespace, public: true },
+ { key: 'CI_PROJECT_NAMESPACE', value: project.namespace.path, public: true },
+ { key: 'CI_PROJECT_URL', value: project.web_url, public: true },
+ { key: 'CI_PIPELINE_ID', value: pipeline.id.to_s, public: true }
+ ]
+ end
+
+ before do
+ stub_container_registry_config(enabled: container_registry_enabled, host_port: 'registry.example.com')
+ end
+
+ subject { build.variables }
+
+ context 'returns variables' do
+ before do
+ build.yaml_variables = []
+ end
+
+ it { is_expected.to eq(predefined_variables) }
+ end
+
+ context 'when build has user' do
+ let(:user) { create(:user, username: 'starter') }
+ let(:user_variables) do
+ [
+ { key: 'GITLAB_USER_ID', value: user.id.to_s, public: true },
+ { key: 'GITLAB_USER_EMAIL', value: user.email, public: true }
+ ]
+ end
+
+ before do
+ build.update_attributes(user: user)
+ end
+
+ it { user_variables.each { |v| is_expected.to include(v) } }
+ end
+
+ context 'when build has an environment' do
+ before do
+ build.update(environment: 'production')
+ create(:environment, project: build.project, name: 'production', slug: 'prod-slug')
+ end
+
+ let(:environment_variables) do
+ [
+ { key: 'CI_ENVIRONMENT_NAME', value: 'production', public: true },
+ { key: 'CI_ENVIRONMENT_SLUG', value: 'prod-slug', public: true }
+ ]
+ end
+
+ it { environment_variables.each { |v| is_expected.to include(v) } }
+ end
+
+ context 'when build started manually' do
+ before do
+ build.update_attributes(when: :manual)
+ end
+
+ let(:manual_variable) do
+ { key: 'CI_BUILD_MANUAL', value: 'true', public: true }
+ end
+
+ it { is_expected.to include(manual_variable) }
+ end
+
+ context 'when build is for tag' do
+ let(:tag_variable) do
+ { key: 'CI_BUILD_TAG', value: 'master', public: true }
+ end
+
+ before do
+ build.update_attributes(tag: true)
+ end
+
+ it { is_expected.to include(tag_variable) }
+ end
+
+ context 'when secure variable is defined' do
+ let(:secure_variable) do
+ { key: 'SECRET_KEY', value: 'secret_value', public: false }
+ end
+
+ before do
+ build.project.variables << Ci::Variable.new(key: 'SECRET_KEY', value: 'secret_value')
+ end
+
+ it { is_expected.to include(secure_variable) }
+ end
+
+ context 'when build is for triggers' do
+ let(:trigger) { create(:ci_trigger, project: project) }
+ let(:trigger_request) { create(:ci_trigger_request_with_variables, pipeline: pipeline, trigger: trigger) }
+ let(:user_trigger_variable) do
+ { key: :TRIGGER_KEY_1, value: 'TRIGGER_VALUE_1', public: false }
+ end
+ let(:predefined_trigger_variable) do
+ { key: 'CI_BUILD_TRIGGERED', value: 'true', public: true }
+ end
+
+ before do
+ build.trigger_request = trigger_request
+ end
+
+ it { is_expected.to include(user_trigger_variable) }
+ it { is_expected.to include(predefined_trigger_variable) }
+ end
+
+ context 'when yaml_variables are undefined' do
+ before do
+ build.yaml_variables = nil
+ end
+
+ context 'use from gitlab-ci.yml' do
+ before do
+ stub_ci_pipeline_yaml_file(config)
+ end
+
+ context 'when config is not found' do
+ let(:config) { nil }
+
+ it { is_expected.to eq(predefined_variables) }
+ end
+
+ context 'when config does not have a questioned job' do
+ let(:config) do
+ YAML.dump({
+ test_other: {
+ script: 'Hello World'
+ }
+ })
+ end
+
+ it { is_expected.to eq(predefined_variables) }
+ end
+
+ context 'when config has variables' do
+ let(:config) do
+ YAML.dump({
+ test: {
+ script: 'Hello World',
+ variables: {
+ KEY: 'value'
+ }
+ }
+ })
+ end
+ let(:variables) do
+ [{ key: 'KEY', value: 'value', public: true }]
+ end
+
+ it { is_expected.to eq(predefined_variables + variables) }
+ end
+ end
+ end
+
+ context 'when container registry is enabled' do
+ let(:container_registry_enabled) { true }
+ let(:ci_registry) do
+ { key: 'CI_REGISTRY', value: 'registry.example.com', public: true }
+ end
+ let(:ci_registry_image) do
+ { key: 'CI_REGISTRY_IMAGE', value: project.container_registry_repository_url, public: true }
+ end
+
+ context 'and is disabled for project' do
+ before do
+ project.update(container_registry_enabled: false)
+ end
+
+ it { is_expected.to include(ci_registry) }
+ it { is_expected.not_to include(ci_registry_image) }
+ end
+
+ context 'and is enabled for project' do
+ before do
+ project.update(container_registry_enabled: true)
+ end
+
+ it { is_expected.to include(ci_registry) }
+ it { is_expected.to include(ci_registry_image) }
+ end
+ end
+
+ context 'when runner is assigned to build' do
+ let(:runner) { create(:ci_runner, description: 'description', tag_list: ['docker', 'linux']) }
+
+ before do
+ build.update(runner: runner)
+ end
+
+ it { is_expected.to include({ key: 'CI_RUNNER_ID', value: runner.id.to_s, public: true }) }
+ it { is_expected.to include({ key: 'CI_RUNNER_DESCRIPTION', value: 'description', public: true }) }
+ it { is_expected.to include({ key: 'CI_RUNNER_TAGS', value: 'docker, linux', public: true }) }
+ end
+
+ context 'when build is for a deployment' do
+ let(:deployment_variable) { { key: 'KUBERNETES_TOKEN', value: 'TOKEN', public: false } }
+
+ before do
+ build.environment = 'production'
+ allow(project).to receive(:deployment_variables).and_return([deployment_variable])
+ end
+
+ it { is_expected.to include(deployment_variable) }
+ end
+
+ context 'returns variables in valid order' do
+ before do
+ allow(build).to receive(:predefined_variables) { ['predefined'] }
+ allow(project).to receive(:predefined_variables) { ['project'] }
+ allow(pipeline).to receive(:predefined_variables) { ['pipeline'] }
+ allow(build).to receive(:yaml_variables) { ['yaml'] }
+ allow(project).to receive(:secret_variables) { ['secret'] }
+ end
+
+ it { is_expected.to eq(%w[predefined project pipeline yaml secret]) }
+ end
+ end
+
+ describe 'State transition: any => [:pending]' do
+ let(:build) { create(:ci_build, :created) }
+
+ it 'queues BuildQueueWorker' do
+ expect(BuildQueueWorker).to receive(:perform_async).with(build.id)
+
+ build.enqueue
+ end
+ end
end
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index dc377d15f15..426be74cd02 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -122,55 +122,80 @@ describe Ci::Pipeline, models: true do
end
end
- describe '#stages' do
+ describe 'pipeline stages' do
before do
- create(:commit_status, pipeline: pipeline, stage: 'build', name: 'linux', stage_idx: 0, status: 'success')
- create(:commit_status, pipeline: pipeline, stage: 'build', name: 'mac', stage_idx: 0, status: 'failed')
- create(:commit_status, pipeline: pipeline, stage: 'deploy', name: 'staging', stage_idx: 2, status: 'running')
- create(:commit_status, pipeline: pipeline, stage: 'test', name: 'rspec', stage_idx: 1, status: 'success')
- end
-
- subject { pipeline.stages }
-
- context 'stages list' do
- it 'returns ordered list of stages' do
- expect(subject.map(&:name)).to eq(%w[build test deploy])
+ 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
+
+ describe '#stages' do
+ subject { pipeline.stages }
+
+ context 'stages list' do
+ it 'returns ordered list of stages' do
+ expect(subject.map(&:name)).to eq(%w[build test deploy])
+ end
end
- end
- it 'returns a valid number of stages' do
- expect(pipeline.stages_count).to eq(3)
- end
+ context 'stages with statuses' do
+ let(:statuses) do
+ subject.map { |stage| [stage.name, stage.status] }
+ end
- it 'returns a valid names of stages' do
- expect(pipeline.stages_name).to eq(['build', 'test', 'deploy'])
- end
+ it 'returns list of stages with correct statuses' do
+ expect(statuses).to eq([['build', 'failed'],
+ ['test', 'success'],
+ ['deploy', 'running']])
+ end
+
+ context 'when commit status is retried' do
+ before do
+ create(:commit_status, pipeline: pipeline,
+ stage: 'build',
+ name: 'mac',
+ stage_idx: 0,
+ status: 'success')
+ end
- context 'stages with statuses' do
- let(:statuses) do
- subject.map do |stage|
- [stage.name, stage.status]
+ it 'ignores the previous state' do
+ expect(statuses).to eq([['build', 'success'],
+ ['test', 'success'],
+ ['deploy', 'running']])
+ end
end
end
+ end
- it 'returns list of stages with statuses' do
- expect(statuses).to eq([['build', 'failed'],
- ['test', 'success'],
- ['deploy', 'running']
- ])
+ describe '#stages_count' do
+ it 'returns a valid number of stages' do
+ expect(pipeline.stages_count).to eq(3)
end
+ end
- context 'when build is retried' do
- before do
- create(:commit_status, pipeline: pipeline, stage: 'build', name: 'mac', stage_idx: 0, status: 'success')
- end
-
- it 'ignores the previous state' do
- expect(statuses).to eq([['build', 'success'],
- ['test', 'success'],
- ['deploy', 'running']
- ])
- end
+ describe '#stages_name' do
+ it 'returns a valid names of stages' do
+ expect(pipeline.stages_name).to eq(['build', 'test', 'deploy'])
end
end
end
@@ -259,7 +284,7 @@ describe Ci::Pipeline, models: true do
end
describe 'merge request metrics' do
- let(:project) { FactoryGirl.create :project }
+ let(:project) { create(:project, :repository) }
let(:pipeline) { FactoryGirl.create(:ci_empty_pipeline, status: 'created', project: project, ref: 'master', sha: project.repository.commit('master').id) }
let!(:merge_request) { create(:merge_request, source_project: project, source_branch: pipeline.ref) }
@@ -314,7 +339,7 @@ describe Ci::Pipeline, models: true do
end
context 'with non-empty project' do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:pipeline) do
create(:ci_pipeline,
@@ -464,6 +489,19 @@ describe Ci::Pipeline, models: true do
end
end
+ describe '.latest_successful_for' do
+ include_context 'with some outdated pipelines'
+
+ let!(:latest_successful_pipeline) do
+ create_pipeline(:success, 'ref', 'D')
+ end
+
+ it 'returns the latest successful pipeline' do
+ expect(described_class.latest_successful_for('ref')).
+ to eq(latest_successful_pipeline)
+ end
+ end
+
describe '#status' do
let!(:build) { create(:ci_build, :created, pipeline: pipeline, name: 'test') }
@@ -852,7 +890,7 @@ describe Ci::Pipeline, models: true do
end
describe "#merge_requests" do
- let(:project) { FactoryGirl.create :project }
+ let(:project) { create(:project, :repository) }
let(:pipeline) { FactoryGirl.create(:ci_empty_pipeline, status: 'created', project: project, ref: 'master', sha: project.repository.commit('master').id) }
it "returns merge requests whose `diff_head_sha` matches the pipeline's SHA" do
@@ -875,8 +913,50 @@ describe Ci::Pipeline, models: true do
end
end
+ describe '#stuck?' do
+ before do
+ create(:ci_build, :pending, pipeline: pipeline)
+ end
+
+ context 'when pipeline is stuck' do
+ it 'is stuck' do
+ expect(pipeline).to be_stuck
+ end
+ end
+
+ context 'when pipeline is not stuck' do
+ before { create(:ci_runner, :shared, :online) }
+
+ it 'is not stuck' do
+ expect(pipeline).not_to be_stuck
+ end
+ end
+ end
+
+ describe '#has_yaml_errors?' do
+ context 'when pipeline has errors' do
+ let(:pipeline) do
+ create(:ci_pipeline, config: { rspec: nil })
+ end
+
+ it 'contains yaml errors' do
+ expect(pipeline).to have_yaml_errors
+ end
+ end
+
+ context 'when pipeline does not have errors' do
+ let(:pipeline) do
+ create(:ci_pipeline, config: { rspec: { script: 'rake test' } })
+ end
+
+ it 'does not containyaml errors' do
+ expect(pipeline).not_to have_yaml_errors
+ end
+ end
+ end
+
describe 'notifications when pipeline success or failed' do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:pipeline) do
create(:ci_pipeline,
diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb
index ef65eb99328..3f32248e52b 100644
--- a/spec/models/ci/runner_spec.rb
+++ b/spec/models/ci/runner_spec.rb
@@ -91,8 +91,7 @@ describe Ci::Runner, models: true do
end
describe '#can_pick?' do
- let(:project) { create(:project) }
- let(:pipeline) { create(:ci_pipeline, project: project) }
+ let(:pipeline) { create(:ci_pipeline) }
let(:build) { create(:ci_build, pipeline: pipeline) }
let(:runner) { create(:ci_runner) }
@@ -263,10 +262,66 @@ describe Ci::Runner, models: true do
end
end
+ describe '#tick_runner_queue' do
+ let(:runner) { create(:ci_runner) }
+
+ it 'returns a new last_update value' do
+ expect(runner.tick_runner_queue).not_to be_empty
+ end
+ end
+
+ describe '#ensure_runner_queue_value' do
+ let(:runner) { create(:ci_runner) }
+
+ it 'sets a new last_update value when it is called the first time' do
+ last_update = runner.ensure_runner_queue_value
+
+ expect_value_in_redis.to eq(last_update)
+ end
+
+ it 'does not change if it is not expired and called again' do
+ last_update = runner.ensure_runner_queue_value
+
+ expect(runner.ensure_runner_queue_value).to eq(last_update)
+ expect_value_in_redis.to eq(last_update)
+ end
+
+ context 'updates runner queue after changing editable value' do
+ let!(:last_update) { runner.ensure_runner_queue_value }
+
+ before do
+ runner.update(description: 'new runner')
+ end
+
+ it 'sets a new last_update value' do
+ expect_value_in_redis.not_to eq(last_update)
+ end
+ end
+
+ context 'does not update runner value after save' do
+ let!(:last_update) { runner.ensure_runner_queue_value }
+
+ before do
+ runner.touch
+ end
+
+ it 'has an old last_update value' do
+ expect_value_in_redis.to eq(last_update)
+ end
+ end
+
+ def expect_value_in_redis
+ Gitlab::Redis.with do |redis|
+ runner_queue_key = runner.send(:runner_queue_key)
+ expect(redis.get(runner_queue_key))
+ end
+ end
+ end
+
describe '.assignable_for' do
let(:runner) { create(:ci_runner) }
- let(:project) { create(:project) }
- let(:another_project) { create(:project) }
+ let(:project) { create(:empty_project) }
+ let(:another_project) { create(:empty_project) }
before do
project.runners << runner
diff --git a/spec/models/ci/stage_spec.rb b/spec/models/ci/stage_spec.rb
index 742bedb37e4..c4a9743a4e2 100644
--- a/spec/models/ci/stage_spec.rb
+++ b/spec/models/ci/stage_spec.rb
@@ -142,6 +142,78 @@ describe Ci::Stage, models: true do
end
end
+ describe '#success?' do
+ context 'when stage is successful' do
+ before do
+ create_job(:ci_build, status: :success)
+ create_job(:generic_commit_status, status: :success)
+ end
+
+ it 'is successful' do
+ expect(stage).to be_success
+ end
+ end
+
+ context 'when stage is not successful' do
+ before do
+ create_job(:ci_build, status: :failed)
+ create_job(:generic_commit_status, status: :success)
+ end
+
+ it 'is not successful' do
+ expect(stage).not_to be_success
+ end
+ end
+ end
+
+ describe '#has_warnings?' do
+ context 'when stage has warnings' do
+ context 'when using memoized warnings flag' do
+ context 'when there are warnings' do
+ let(:stage) { build(:ci_stage, warnings: true) }
+
+ it 'has memoized warnings' do
+ expect(stage).not_to receive(:statuses)
+ expect(stage).to have_warnings
+ end
+ end
+
+ context 'when there are no warnings' do
+ let(:stage) { build(:ci_stage, warnings: false) }
+
+ it 'has memoized warnings' do
+ expect(stage).not_to receive(:statuses)
+ expect(stage).not_to have_warnings
+ end
+ end
+ end
+
+ context 'when calculating warnings from statuses' do
+ before do
+ create(:ci_build, :failed, :allowed_to_fail,
+ stage: stage_name, pipeline: pipeline)
+ end
+
+ it 'has warnings calculated from statuses' do
+ expect(stage).to receive(:statuses).and_call_original
+ expect(stage).to have_warnings
+ end
+ end
+ end
+
+ context 'when stage does not have warnings' do
+ before do
+ create(:ci_build, :success, stage: stage_name,
+ pipeline: pipeline)
+ end
+
+ it 'does not have warnings calculated from statuses' do
+ expect(stage).to receive(:statuses).and_call_original
+ expect(stage).not_to have_warnings
+ end
+ end
+ end
+
def create_job(type, status: 'success', stage: stage_name)
create(type, pipeline: pipeline, stage: stage, status: status)
end
diff --git a/spec/models/commit_range_spec.rb b/spec/models/commit_range_spec.rb
index 30782ca75a0..e4bddf67096 100644
--- a/spec/models/commit_range_spec.rb
+++ b/spec/models/commit_range_spec.rb
@@ -7,7 +7,7 @@ describe CommitRange, models: true do
it { is_expected.to include_module(Referable) }
end
- let!(:project) { create(:project, :public) }
+ let!(:project) { create(:project, :public, :repository) }
let!(:commit1) { project.commit("HEAD~2") }
let!(:commit2) { project.commit }
diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb
index 74b50d2908d..32f9366a14c 100644
--- a/spec/models/commit_spec.rb
+++ b/spec/models/commit_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Commit, models: true do
- let(:project) { create(:project, :public) }
+ let(:project) { create(:project, :public, :repository) }
let(:commit) { project.commit }
describe 'modules' do
@@ -34,7 +34,7 @@ describe Commit, models: true do
end
describe '#to_reference' do
- let(:project) { create(:project, path: 'sample-project') }
+ let(:project) { create(:project, :repository, path: 'sample-project') }
let(:commit) { project.commit }
it 'returns a String reference to the object' do
@@ -42,13 +42,13 @@ describe Commit, models: true do
end
it 'supports a cross-project reference' do
- another_project = build(:project, name: 'another-project', namespace: project.namespace)
+ another_project = build(:project, :repository, name: 'another-project', namespace: project.namespace)
expect(commit.to_reference(another_project)).to eq "sample-project@#{commit.id}"
end
end
describe '#reference_link_text' do
- let(:project) { create(:project, path: 'sample-project') }
+ let(:project) { create(:project, :repository, path: 'sample-project') }
let(:commit) { project.commit }
it 'returns a String reference to the object' do
@@ -56,7 +56,7 @@ describe Commit, models: true do
end
it 'supports a cross-project reference' do
- another_project = build(:project, name: 'another-project', namespace: project.namespace)
+ another_project = build(:project, :repository, name: 'another-project', namespace: project.namespace)
expect(commit.reference_link_text(another_project)).to eq "sample-project@#{commit.short_id}"
end
end
@@ -131,7 +131,7 @@ eos
describe '#closes_issues' do
let(:issue) { create :issue, project: project }
- let(:other_project) { create :project, :public }
+ let(:other_project) { create(:empty_project, :public) }
let(:other_issue) { create :issue, project: other_project }
let(:commiter) { create :user }
@@ -154,7 +154,7 @@ eos
end
it_behaves_like 'a mentionable' do
- subject { create(:project).commit }
+ subject { create(:project, :repository).commit }
let(:author) { create(:user, email: subject.author_email) }
let(:backref_text) { "commit #{subject.id}" }
@@ -323,4 +323,50 @@ eos
expect(new_commit.message).to eq(commit.message)
end
end
+
+ describe '#work_in_progress?' do
+ ['squash! ', 'fixup! ', 'wip: ', 'WIP: ', '[WIP] '].each do |wip_prefix|
+ it "detects the '#{wip_prefix}' prefix" do
+ commit.message = "#{wip_prefix}#{commit.message}"
+
+ expect(commit).to be_work_in_progress
+ end
+ end
+
+ it "detects WIP for a commit just saying 'wip'" do
+ commit.message = "wip"
+
+ expect(commit).to be_work_in_progress
+ end
+
+ it "doesn't detect WIP for a commit that begins with 'FIXUP! '" do
+ commit.message = "FIXUP! #{commit.message}"
+
+ expect(commit).not_to be_work_in_progress
+ end
+
+ it "doesn't detect WIP for words starting with WIP" do
+ commit.message = "Wipout #{commit.message}"
+
+ expect(commit).not_to be_work_in_progress
+ end
+ end
+
+ describe '.valid_hash?' do
+ it 'checks hash contents' do
+ expect(described_class.valid_hash?('abcdef01239ABCDEF')).to be true
+ expect(described_class.valid_hash?("abcdef01239ABCD\nEF")).to be false
+ expect(described_class.valid_hash?(' abcdef01239ABCDEF ')).to be false
+ expect(described_class.valid_hash?('Gabcdef01239ABCDEF')).to be false
+ expect(described_class.valid_hash?('gabcdef01239ABCDEF')).to be false
+ expect(described_class.valid_hash?('-abcdef01239ABCDEF')).to be false
+ end
+
+ it 'checks hash length' do
+ expect(described_class.valid_hash?('a' * 6)).to be false
+ expect(described_class.valid_hash?('a' * 7)).to be true
+ expect(described_class.valid_hash?('a' * 40)).to be true
+ expect(described_class.valid_hash?('a' * 41)).to be false
+ end
+ end
end
diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb
index 701f3323c0f..bf4394f7d5b 100644
--- a/spec/models/commit_status_spec.rb
+++ b/spec/models/commit_status_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe CommitStatus, models: true do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:pipeline) do
create(:ci_pipeline, project: project, sha: project.commit.id)
@@ -243,4 +243,23 @@ describe CommitStatus, models: true do
.to be_a Gitlab::Ci::Status::Success
end
end
+
+ describe '#sortable_name' do
+ tests = {
+ 'karma' => ['karma'],
+ 'karma 0 20' => ['karma ', 0, ' ', 20],
+ 'karma 10 20' => ['karma ', 10, ' ', 20],
+ 'karma 50:100' => ['karma ', 50, ':', 100],
+ 'karma 1.10' => ['karma ', 1, '.', 10],
+ 'karma 1.5.1' => ['karma ', 1, '.', 5, '.', 1],
+ 'karma 1 a' => ['karma ', 1, ' a']
+ }
+
+ tests.each do |name, sortable_name|
+ it "'#{name}' sorts as '#{sortable_name}'" do
+ commit_status.name = name
+ expect(commit_status.sortable_name).to eq(sortable_name)
+ end
+ end
+ end
end
diff --git a/spec/models/compare_spec.rb b/spec/models/compare_spec.rb
index 49ab3c4b6e9..da003dbf794 100644
--- a/spec/models/compare_spec.rb
+++ b/spec/models/compare_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe Compare, models: true do
include RepoHelpers
- let(:project) { create(:project, :public) }
+ let(:project) { create(:project, :public, :repository) }
let(:commit) { project.commit }
let(:start_commit) { sample_image_commit }
diff --git a/spec/models/concerns/has_status_spec.rb b/spec/models/concerns/has_status_spec.rb
index 4d0f51fe82a..dbfe3cd2d36 100644
--- a/spec/models/concerns/has_status_spec.rb
+++ b/spec/models/concerns/has_status_spec.rb
@@ -219,4 +219,10 @@ describe HasStatus do
end
end
end
+
+ describe '::DEFAULT_STATUS' do
+ it 'is a status created' do
+ expect(described_class::DEFAULT_STATUS).to eq 'created'
+ end
+ end
end
diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb
index 4fa06a8c60a..545a11912e3 100644
--- a/spec/models/concerns/issuable_spec.rb
+++ b/spec/models/concerns/issuable_spec.rb
@@ -44,6 +44,45 @@ describe Issue, "Issuable" do
it { expect(described_class).to respond_to(:assigned) }
end
+ describe "before_save" do
+ describe "#update_cache_counts" do
+ context "when previous assignee exists" do
+ before do
+ assignee = create(:user)
+ issue.project.team << [assignee, :developer]
+ issue.update(assignee: assignee)
+ end
+
+ it "updates cache counts for new assignee" do
+ user = create(:user)
+
+ expect(user).to receive(:update_cache_counts)
+
+ issue.update(assignee: user)
+ end
+
+ it "updates cache counts for previous assignee" do
+ old_assignee = issue.assignee
+ allow(User).to receive(:find_by_id).with(old_assignee.id).and_return(old_assignee)
+
+ expect(old_assignee).to receive(:update_cache_counts)
+
+ issue.update(assignee: nil)
+ end
+ end
+
+ context "when previous assignee does not exist" do
+ before{ issue.update(assignee: nil) }
+
+ it "updates cache count for the new assignee" do
+ expect_any_instance_of(User).to receive(:update_cache_counts)
+
+ issue.update(assignee: user)
+ end
+ end
+ end
+ end
+
describe ".search" do
let!(:searchable_issue) { create(:issue, title: "Searchable issue") }
@@ -262,7 +301,7 @@ describe Issue, "Issuable" do
end
describe '#labels_array' do
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project) }
let(:bug) { create(:label, project: project, title: 'bug') }
let(:issue) { create(:issue, project: project) }
@@ -276,7 +315,7 @@ describe Issue, "Issuable" do
end
describe '#user_notes_count' do
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project) }
let(:issue1) { create(:issue, project: project) }
let(:issue2) { create(:issue, project: project) }
@@ -320,7 +359,7 @@ describe Issue, "Issuable" do
end
describe ".with_label" do
- let(:project) { create(:project, :public) }
+ let(:project) { create(:empty_project, :public) }
let(:bug) { create(:label, project: project, title: 'bug') }
let(:feature) { create(:label, project: project, title: 'feature') }
let(:enhancement) { create(:label, project: project, title: 'enhancement') }
@@ -369,4 +408,42 @@ describe Issue, "Issuable" do
expect(issue.assignee_or_author?(user)).to eq(false)
end
end
+
+ describe '#spend_time' do
+ let(:user) { create(:user) }
+ let(:issue) { create(:issue) }
+
+ def spend_time(seconds)
+ issue.spend_time(duration: seconds, user: user)
+ issue.save!
+ end
+
+ context 'adding time' do
+ it 'should update the total time spent' do
+ spend_time(1800)
+
+ expect(issue.total_time_spent).to eq(1800)
+ end
+ end
+
+ context 'substracting time' do
+ before do
+ spend_time(1800)
+ end
+
+ it 'should update the total time spent' do
+ spend_time(-900)
+
+ expect(issue.total_time_spent).to eq(900)
+ end
+
+ context 'when time to substract exceeds the total time spent' do
+ it 'raise a validation error' do
+ expect do
+ spend_time(-3600)
+ end.to raise_error(ActiveRecord::RecordInvalid)
+ end
+ end
+ end
+ end
end
diff --git a/spec/models/concerns/mentionable_spec.rb b/spec/models/concerns/mentionable_spec.rb
index 132858950d5..2092576e981 100644
--- a/spec/models/concerns/mentionable_spec.rb
+++ b/spec/models/concerns/mentionable_spec.rb
@@ -13,7 +13,7 @@ describe Mentionable do
end
describe 'references' do
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project) }
let(:mentionable) { Example.new }
it 'excludes JIRA references' do
@@ -30,12 +30,20 @@ describe Issue, "Mentionable" do
describe '#mentioned_users' do
let!(:user) { create(:user, username: 'stranger') }
let!(:user2) { create(:user, username: 'john') }
- let!(:issue) { create(:issue, description: "#{user.to_reference} mentioned") }
+ let!(:user3) { create(:user, username: 'jim') }
+ let(:issue) { create(:issue, description: "#{user.to_reference} mentioned") }
subject { issue.mentioned_users }
- it { is_expected.to include(user) }
- it { is_expected.not_to include(user2) }
+ it { expect(subject).to contain_exactly(user) }
+
+ context 'when a note on personal snippet' do
+ let!(:note) { create(:note_on_personal_snippet, note: "#{user.to_reference} mentioned #{user3.to_reference}") }
+
+ subject { note.mentioned_users }
+
+ it { expect(subject).to contain_exactly(user, user3) }
+ end
end
describe '#referenced_mentionables' do
@@ -75,13 +83,13 @@ describe Issue, "Mentionable" do
end
describe '#create_cross_references!' do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:author) { build(:user) }
let(:commit) { project.commit }
let(:commit2) { project.commit }
let!(:issue) do
- create(:issue, project: project, description: commit.to_reference)
+ create(:issue, project: project, description: "See #{commit.to_reference}")
end
it 'correctly removes already-mentioned Commits' do
@@ -92,7 +100,7 @@ describe Issue, "Mentionable" do
end
describe '#create_new_cross_references!' do
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project) }
let(:author) { create(:author) }
let(:issues) { create_list(:issue, 2, project: project, author: author) }
@@ -138,6 +146,16 @@ describe Issue, "Mentionable" do
issue.update_attributes(description: issues[1].to_reference)
issue.create_new_cross_references!
end
+
+ it 'notifies new references from project snippet note' do
+ snippet = create(:snippet, project: project)
+ note = create(:note, note: issues[0].to_reference, noteable: snippet, project: project, author: author)
+
+ expect(SystemNoteService).to receive(:cross_reference).with(issues[1], any_args)
+
+ note.update_attributes(note: issues[1].to_reference)
+ note.create_new_cross_references!
+ end
end
def create_issue(description:)
diff --git a/spec/models/concerns/milestoneish_spec.rb b/spec/models/concerns/milestoneish_spec.rb
index 0e097559b59..ad703a6c8bb 100644
--- a/spec/models/concerns/milestoneish_spec.rb
+++ b/spec/models/concerns/milestoneish_spec.rb
@@ -7,7 +7,7 @@ describe Milestone, 'Milestoneish' do
let(:member) { create(:user) }
let(:guest) { create(:user) }
let(:admin) { create(:admin) }
- let(:project) { create(:project, :public) }
+ let(:project) { create(:empty_project, :public) }
let(:milestone) { create(:milestone, project: project) }
let!(:issue) { create(:issue, project: project, milestone: milestone) }
let!(:security_issue_1) { create(:issue, :confidential, project: project, author: author, milestone: milestone) }
diff --git a/spec/models/concerns/presentable_spec.rb b/spec/models/concerns/presentable_spec.rb
new file mode 100644
index 00000000000..941647a79fb
--- /dev/null
+++ b/spec/models/concerns/presentable_spec.rb
@@ -0,0 +1,15 @@
+require 'spec_helper'
+
+describe Presentable do
+ let(:build) { Ci::Build.new }
+
+ describe '#present' do
+ it 'returns a presenter' do
+ expect(build.present).to be_a(Ci::BuildPresenter)
+ end
+
+ it 'takes optional attributes' do
+ expect(build.present(foo: 'bar').foo).to eq('bar')
+ end
+ end
+end
diff --git a/spec/models/concerns/project_features_compatibility_spec.rb b/spec/models/concerns/project_features_compatibility_spec.rb
index 9041690023f..6cf5877424d 100644
--- a/spec/models/concerns/project_features_compatibility_spec.rb
+++ b/spec/models/concerns/project_features_compatibility_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe ProjectFeaturesCompatibility do
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project) }
let(:features) { %w(issues wiki builds merge_requests snippets) }
# We had issues_enabled, snippets_enabled, builds_enabled, merge_requests_enabled and issues_enabled fields on projects table
diff --git a/spec/models/concerns/routable_spec.rb b/spec/models/concerns/routable_spec.rb
index b556135532f..30443534cca 100644
--- a/spec/models/concerns/routable_spec.rb
+++ b/spec/models/concerns/routable_spec.rb
@@ -68,4 +68,14 @@ describe Group, 'Routable' do
end
end
end
+
+ describe '.member_descendants' do
+ let!(:user) { create(:user) }
+ let!(:nested_group) { create(:group, parent: group) }
+
+ before { group.add_owner(user) }
+ subject { described_class.member_descendants(user.id) }
+
+ it { is_expected.to eq([nested_group]) }
+ end
end
diff --git a/spec/models/cycle_analytics/code_spec.rb b/spec/models/cycle_analytics/code_spec.rb
index 7771785ead3..9053485939e 100644
--- a/spec/models/cycle_analytics/code_spec.rb
+++ b/spec/models/cycle_analytics/code_spec.rb
@@ -3,10 +3,10 @@ require 'spec_helper'
describe 'CycleAnalytics#code', feature: true do
extend CycleAnalyticsHelpers::TestGeneration
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:from_date) { 10.days.ago }
let(:user) { create(:user, :admin) }
- subject { CycleAnalytics.new(project, user, from: from_date) }
+ subject { CycleAnalytics.new(project, from: from_date) }
context 'with deployment' do
generate_cycle_analytics_spec(
@@ -16,10 +16,10 @@ describe 'CycleAnalytics#code', feature: true do
-> (context, data) do
context.create_commit_referencing_issue(data[:issue])
end]],
- end_time_conditions: [["merge request that closes issue is created",
- -> (context, data) do
- context.create_merge_request_closing_issue(data[:issue])
- end]],
+ end_time_conditions: [["merge request that closes issue is created",
+ -> (context, data) do
+ context.create_merge_request_closing_issue(data[:issue])
+ end]],
post_fn: -> (context, data) do
context.merge_merge_requests_closing_issue(data[:issue])
context.deploy_master
@@ -27,17 +27,15 @@ describe 'CycleAnalytics#code', feature: true do
context "when a regular merge request (that doesn't close the issue) is created" do
it "returns nil" do
- 5.times do
- issue = create(:issue, project: project)
+ issue = create(:issue, project: project)
- create_commit_referencing_issue(issue)
- create_merge_request_closing_issue(issue, message: "Closes nothing")
+ create_commit_referencing_issue(issue)
+ create_merge_request_closing_issue(issue, message: "Closes nothing")
- merge_merge_requests_closing_issue(issue)
- deploy_master
- end
+ merge_merge_requests_closing_issue(issue)
+ deploy_master
- expect(subject.code).to be_nil
+ expect(subject[:code].median).to be_nil
end
end
end
@@ -50,26 +48,24 @@ describe 'CycleAnalytics#code', feature: true do
-> (context, data) do
context.create_commit_referencing_issue(data[:issue])
end]],
- end_time_conditions: [["merge request that closes issue is created",
- -> (context, data) do
- context.create_merge_request_closing_issue(data[:issue])
- end]],
+ end_time_conditions: [["merge request that closes issue is created",
+ -> (context, data) do
+ context.create_merge_request_closing_issue(data[:issue])
+ end]],
post_fn: -> (context, data) do
context.merge_merge_requests_closing_issue(data[:issue])
end)
context "when a regular merge request (that doesn't close the issue) is created" do
it "returns nil" do
- 5.times do
- issue = create(:issue, project: project)
+ issue = create(:issue, project: project)
- create_commit_referencing_issue(issue)
- create_merge_request_closing_issue(issue, message: "Closes nothing")
+ create_commit_referencing_issue(issue)
+ create_merge_request_closing_issue(issue, message: "Closes nothing")
- merge_merge_requests_closing_issue(issue)
- end
+ merge_merge_requests_closing_issue(issue)
- expect(subject.code).to be_nil
+ expect(subject[:code].median).to be_nil
end
end
end
diff --git a/spec/models/cycle_analytics/issue_spec.rb b/spec/models/cycle_analytics/issue_spec.rb
index 5ed3d37f2fb..fc7d18bd40e 100644
--- a/spec/models/cycle_analytics/issue_spec.rb
+++ b/spec/models/cycle_analytics/issue_spec.rb
@@ -3,10 +3,10 @@ require 'spec_helper'
describe 'CycleAnalytics#issue', models: true do
extend CycleAnalyticsHelpers::TestGeneration
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:from_date) { 10.days.ago }
let(:user) { create(:user, :admin) }
- subject { CycleAnalytics.new(project, user, from: from_date) }
+ subject { CycleAnalytics.new(project, from: from_date) }
generate_cycle_analytics_spec(
phase: :issue,
@@ -33,16 +33,14 @@ describe 'CycleAnalytics#issue', models: true do
context "when a regular label (instead of a list label) is added to the issue" do
it "returns nil" do
- 5.times do
- regular_label = create(:label)
- issue = create(:issue, project: project)
- issue.update(label_ids: [regular_label.id])
+ regular_label = create(:label)
+ issue = create(:issue, project: project)
+ issue.update(label_ids: [regular_label.id])
- create_merge_request_closing_issue(issue)
- merge_merge_requests_closing_issue(issue)
- end
+ create_merge_request_closing_issue(issue)
+ merge_merge_requests_closing_issue(issue)
- expect(subject.issue).to be_nil
+ expect(subject[:issue].median).to be_nil
end
end
end
diff --git a/spec/models/cycle_analytics/plan_spec.rb b/spec/models/cycle_analytics/plan_spec.rb
index baf3e3241a1..55483fc876a 100644
--- a/spec/models/cycle_analytics/plan_spec.rb
+++ b/spec/models/cycle_analytics/plan_spec.rb
@@ -3,10 +3,10 @@ require 'spec_helper'
describe 'CycleAnalytics#plan', feature: true do
extend CycleAnalyticsHelpers::TestGeneration
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:from_date) { 10.days.ago }
let(:user) { create(:user, :admin) }
- subject { CycleAnalytics.new(project, user, from: from_date) }
+ subject { CycleAnalytics.new(project, from: from_date) }
generate_cycle_analytics_spec(
phase: :plan,
@@ -44,7 +44,7 @@ describe 'CycleAnalytics#plan', feature: true do
create_merge_request_closing_issue(issue, source_branch: branch_name)
merge_merge_requests_closing_issue(issue)
- expect(subject.issue).to be_nil
+ expect(subject[:issue].median).to be_nil
end
end
end
diff --git a/spec/models/cycle_analytics/production_spec.rb b/spec/models/cycle_analytics/production_spec.rb
index 21b9c6e7150..b9fe492fe2c 100644
--- a/spec/models/cycle_analytics/production_spec.rb
+++ b/spec/models/cycle_analytics/production_spec.rb
@@ -3,10 +3,10 @@ require 'spec_helper'
describe 'CycleAnalytics#production', feature: true do
extend CycleAnalyticsHelpers::TestGeneration
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:from_date) { 10.days.ago }
let(:user) { create(:user, :admin) }
- subject { CycleAnalytics.new(project, user, from: from_date) }
+ subject { CycleAnalytics.new(project, from: from_date) }
generate_cycle_analytics_spec(
phase: :production,
@@ -21,7 +21,13 @@ describe 'CycleAnalytics#production', feature: true do
["production deploy happens after merge request is merged (along with other changes)",
lambda do |context, data|
# Make other changes on master
- sha = context.project.repository.commit_file(context.user, context.random_git_name, "content", "commit message", 'master', false)
+ sha = context.project.repository.commit_file(
+ context.user,
+ context.random_git_name,
+ 'content',
+ message: 'commit message',
+ branch_name: 'master',
+ update: false)
context.project.repository.commit(sha)
context.deploy_master
@@ -29,26 +35,22 @@ describe 'CycleAnalytics#production', feature: true do
context "when a regular merge request (that doesn't close the issue) is merged and deployed" do
it "returns nil" do
- 5.times do
- merge_request = create(:merge_request)
- MergeRequests::MergeService.new(project, user).execute(merge_request)
- deploy_master
- end
+ merge_request = create(:merge_request)
+ MergeRequests::MergeService.new(project, user).execute(merge_request)
+ deploy_master
- expect(subject.production).to be_nil
+ expect(subject[:production].median).to be_nil
end
end
context "when the deployment happens to a non-production environment" do
it "returns nil" do
- 5.times do
- issue = create(:issue, project: project)
- merge_request = create_merge_request_closing_issue(issue)
- MergeRequests::MergeService.new(project, user).execute(merge_request)
- deploy_master(environment: 'staging')
- end
-
- expect(subject.production).to be_nil
+ issue = create(:issue, project: project)
+ merge_request = create_merge_request_closing_issue(issue)
+ MergeRequests::MergeService.new(project, user).execute(merge_request)
+ deploy_master(environment: 'staging')
+
+ expect(subject[:production].median).to be_nil
end
end
end
diff --git a/spec/models/cycle_analytics/review_spec.rb b/spec/models/cycle_analytics/review_spec.rb
index 158621d59a4..febb18c9884 100644
--- a/spec/models/cycle_analytics/review_spec.rb
+++ b/spec/models/cycle_analytics/review_spec.rb
@@ -3,10 +3,10 @@ require 'spec_helper'
describe 'CycleAnalytics#review', feature: true do
extend CycleAnalyticsHelpers::TestGeneration
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:from_date) { 10.days.ago }
let(:user) { create(:user, :admin) }
- subject { CycleAnalytics.new(project, user, from: from_date) }
+ subject { CycleAnalytics.new(project, from: from_date) }
generate_cycle_analytics_spec(
phase: :review,
@@ -23,11 +23,9 @@ describe 'CycleAnalytics#review', feature: true do
context "when a regular merge request (that doesn't close the issue) is created and merged" do
it "returns nil" do
- 5.times do
- MergeRequests::MergeService.new(project, user).execute(create(:merge_request))
- end
+ MergeRequests::MergeService.new(project, user).execute(create(:merge_request))
- expect(subject.review).to be_nil
+ expect(subject[:review].median).to be_nil
end
end
end
diff --git a/spec/models/cycle_analytics/staging_spec.rb b/spec/models/cycle_analytics/staging_spec.rb
index dad653964b7..9a024d533a1 100644
--- a/spec/models/cycle_analytics/staging_spec.rb
+++ b/spec/models/cycle_analytics/staging_spec.rb
@@ -3,10 +3,11 @@ require 'spec_helper'
describe 'CycleAnalytics#staging', feature: true do
extend CycleAnalyticsHelpers::TestGeneration
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:from_date) { 10.days.ago }
let(:user) { create(:user, :admin) }
- subject { CycleAnalytics.new(project, user, from: from_date) }
+
+ subject { CycleAnalytics.new(project, from: from_date) }
generate_cycle_analytics_spec(
phase: :staging,
@@ -28,10 +29,10 @@ describe 'CycleAnalytics#staging', feature: true do
sha = context.project.repository.commit_file(
context.user,
context.random_git_name,
- "content",
- "commit message",
- 'master',
- false)
+ 'content',
+ message: 'commit message',
+ branch_name: 'master',
+ update: false)
context.project.repository.commit(sha)
context.deploy_master
@@ -39,26 +40,22 @@ describe 'CycleAnalytics#staging', feature: true do
context "when a regular merge request (that doesn't close the issue) is merged and deployed" do
it "returns nil" do
- 5.times do
- merge_request = create(:merge_request)
- MergeRequests::MergeService.new(project, user).execute(merge_request)
- deploy_master
- end
+ merge_request = create(:merge_request)
+ MergeRequests::MergeService.new(project, user).execute(merge_request)
+ deploy_master
- expect(subject.staging).to be_nil
+ expect(subject[:staging].median).to be_nil
end
end
context "when the deployment happens to a non-production environment" do
it "returns nil" do
- 5.times do
- issue = create(:issue, project: project)
- merge_request = create_merge_request_closing_issue(issue)
- MergeRequests::MergeService.new(project, user).execute(merge_request)
- deploy_master(environment: 'staging')
- end
+ issue = create(:issue, project: project)
+ merge_request = create_merge_request_closing_issue(issue)
+ MergeRequests::MergeService.new(project, user).execute(merge_request)
+ deploy_master(environment: 'staging')
- expect(subject.staging).to be_nil
+ expect(subject[:staging].median).to be_nil
end
end
end
diff --git a/spec/models/cycle_analytics/summary_spec.rb b/spec/models/cycle_analytics/summary_spec.rb
deleted file mode 100644
index 725bc68b25f..00000000000
--- a/spec/models/cycle_analytics/summary_spec.rb
+++ /dev/null
@@ -1,59 +0,0 @@
-require 'spec_helper'
-
-describe CycleAnalytics::Summary, models: true do
- let(:project) { create(:project) }
- let(:from) { Time.now }
- let(:user) { create(:user, :admin) }
- subject { described_class.new(project, user, from: from) }
-
- describe "#new_issues" do
- it "finds the number of issues created after the 'from date'" do
- Timecop.freeze(5.days.ago) { create(:issue, project: project) }
- Timecop.freeze(5.days.from_now) { create(:issue, project: project) }
-
- expect(subject.new_issues).to eq(1)
- end
-
- it "doesn't find issues from other projects" do
- Timecop.freeze(5.days.from_now) { create(:issue, project: create(:project)) }
-
- expect(subject.new_issues).to eq(0)
- end
- end
-
- describe "#commits" do
- it "finds the number of commits created after the 'from date'" do
- Timecop.freeze(5.days.ago) { create_commit("Test message", project, user, 'master') }
- Timecop.freeze(5.days.from_now) { create_commit("Test message", project, user, 'master') }
-
- expect(subject.commits).to eq(1)
- end
-
- it "doesn't find commits from other projects" do
- Timecop.freeze(5.days.from_now) { create_commit("Test message", create(:project), user, 'master') }
-
- expect(subject.commits).to eq(0)
- end
-
- it "finds a large (> 100) snumber of commits if present" do
- Timecop.freeze(5.days.from_now) { create_commit("Test message", project, user, 'master', count: 100) }
-
- expect(subject.commits).to eq(100)
- end
- end
-
- describe "#deploys" do
- it "finds the number of deploys made created after the 'from date'" do
- Timecop.freeze(5.days.ago) { create(:deployment, project: project) }
- Timecop.freeze(5.days.from_now) { create(:deployment, project: project) }
-
- expect(subject.deploys).to eq(1)
- end
-
- it "doesn't find commits from other projects" do
- Timecop.freeze(5.days.from_now) { create(:deployment, project: create(:project)) }
-
- expect(subject.deploys).to eq(0)
- end
- end
-end
diff --git a/spec/models/cycle_analytics/test_spec.rb b/spec/models/cycle_analytics/test_spec.rb
index 2313724e8f3..c2ba012a0e6 100644
--- a/spec/models/cycle_analytics/test_spec.rb
+++ b/spec/models/cycle_analytics/test_spec.rb
@@ -3,10 +3,10 @@ require 'spec_helper'
describe 'CycleAnalytics#test', feature: true do
extend CycleAnalyticsHelpers::TestGeneration
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:from_date) { 10.days.ago }
let(:user) { create(:user, :admin) }
- subject { CycleAnalytics.new(project, user, from: from_date) }
+ subject { CycleAnalytics.new(project, from: from_date) }
generate_cycle_analytics_spec(
phase: :test,
@@ -24,65 +24,57 @@ describe 'CycleAnalytics#test', feature: true do
context "when the pipeline is for a regular merge request (that doesn't close an issue)" do
it "returns nil" do
- 5.times do
- issue = create(:issue, project: project)
- merge_request = create_merge_request_closing_issue(issue)
- pipeline = create(:ci_pipeline, ref: "refs/heads/#{merge_request.source_branch}", sha: merge_request.diff_head_sha)
+ issue = create(:issue, project: project)
+ merge_request = create_merge_request_closing_issue(issue)
+ pipeline = create(:ci_pipeline, ref: "refs/heads/#{merge_request.source_branch}", sha: merge_request.diff_head_sha)
- pipeline.run!
- pipeline.succeed!
+ pipeline.run!
+ pipeline.succeed!
- merge_merge_requests_closing_issue(issue)
- end
+ merge_merge_requests_closing_issue(issue)
- expect(subject.test).to be_nil
+ expect(subject[:test].median).to be_nil
end
end
context "when the pipeline is not for a merge request" do
it "returns nil" do
- 5.times do
- pipeline = create(:ci_pipeline, ref: "refs/heads/master", sha: project.repository.commit('master').sha)
+ pipeline = create(:ci_pipeline, ref: "refs/heads/master", sha: project.repository.commit('master').sha)
- pipeline.run!
- pipeline.succeed!
- end
+ pipeline.run!
+ pipeline.succeed!
- expect(subject.test).to be_nil
+ expect(subject[:test].median).to be_nil
end
end
context "when the pipeline is dropped (failed)" do
it "returns nil" do
- 5.times do
- issue = create(:issue, project: project)
- merge_request = create_merge_request_closing_issue(issue)
- pipeline = create(:ci_pipeline, ref: "refs/heads/#{merge_request.source_branch}", sha: merge_request.diff_head_sha)
+ issue = create(:issue, project: project)
+ merge_request = create_merge_request_closing_issue(issue)
+ pipeline = create(:ci_pipeline, ref: "refs/heads/#{merge_request.source_branch}", sha: merge_request.diff_head_sha)
- pipeline.run!
- pipeline.drop!
+ pipeline.run!
+ pipeline.drop!
- merge_merge_requests_closing_issue(issue)
- end
+ merge_merge_requests_closing_issue(issue)
- expect(subject.test).to be_nil
+ expect(subject[:test].median).to be_nil
end
end
context "when the pipeline is cancelled" do
it "returns nil" do
- 5.times do
- issue = create(:issue, project: project)
- merge_request = create_merge_request_closing_issue(issue)
- pipeline = create(:ci_pipeline, ref: "refs/heads/#{merge_request.source_branch}", sha: merge_request.diff_head_sha)
+ issue = create(:issue, project: project)
+ merge_request = create_merge_request_closing_issue(issue)
+ pipeline = create(:ci_pipeline, ref: "refs/heads/#{merge_request.source_branch}", sha: merge_request.diff_head_sha)
- pipeline.run!
- pipeline.cancel!
+ pipeline.run!
+ pipeline.cancel!
- merge_merge_requests_closing_issue(issue)
- end
+ merge_merge_requests_closing_issue(issue)
- expect(subject.test).to be_nil
+ expect(subject[:test].median).to be_nil
end
end
end
diff --git a/spec/models/deploy_key_spec.rb b/spec/models/deploy_key_spec.rb
index 93623e8e99b..8ef8218cf74 100644
--- a/spec/models/deploy_key_spec.rb
+++ b/spec/models/deploy_key_spec.rb
@@ -1,8 +1,22 @@
require 'spec_helper'
describe DeployKey, models: true do
+ include EmailHelpers
+
describe "Associations" do
it { is_expected.to have_many(:deploy_keys_projects) }
it { is_expected.to have_many(:projects) }
end
+
+ describe 'notification' do
+ let(:user) { create(:user) }
+
+ it 'does not send a notification' do
+ perform_enqueued_jobs do
+ create(:deploy_key, user: user)
+ end
+
+ should_not_email(user)
+ end
+ end
end
diff --git a/spec/models/deploy_keys_project_spec.rb b/spec/models/deploy_keys_project_spec.rb
index 8a1e337c1a3..aacc178a19e 100644
--- a/spec/models/deploy_keys_project_spec.rb
+++ b/spec/models/deploy_keys_project_spec.rb
@@ -12,7 +12,7 @@ describe DeployKeysProject, models: true do
end
describe "Destroying" do
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project) }
subject { create(:deploy_keys_project, project: project) }
let(:deploy_key) { subject.deploy_key }
@@ -39,7 +39,7 @@ describe DeployKeysProject, models: true do
end
context "when the deploy key is used by more than one project" do
- let!(:other_project) { create(:project) }
+ let!(:other_project) { create(:empty_project) }
before do
other_project.deploy_keys << deploy_key
diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb
index ca594a320c0..080ff2f3f43 100644
--- a/spec/models/deployment_spec.rb
+++ b/spec/models/deployment_spec.rb
@@ -17,7 +17,7 @@ describe Deployment, models: true do
it { is_expected.to validate_presence_of(:sha) }
describe '#includes_commit?' do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:environment) { create(:environment, project: project) }
let(:deployment) do
create(:deployment, environment: environment, sha: project.commit.id)
@@ -77,8 +77,8 @@ describe Deployment, models: true do
end
end
- describe '#stoppable?' do
- subject { deployment.stoppable? }
+ describe '#stop_action?' do
+ subject { deployment.stop_action? }
context 'when no other actions' do
let(:deployment) { build(:deployment) }
diff --git a/spec/models/diff_note_spec.rb b/spec/models/diff_note_spec.rb
index 3db5937a4f3..9ea3a4b7020 100644
--- a/spec/models/diff_note_spec.rb
+++ b/spec/models/diff_note_spec.rb
@@ -3,8 +3,8 @@ require 'spec_helper'
describe DiffNote, models: true do
include RepoHelpers
- let(:project) { create(:project) }
- let(:merge_request) { create(:merge_request, source_project: project) }
+ let(:merge_request) { create(:merge_request) }
+ let(:project) { merge_request.project }
let(:commit) { project.commit(sample_commit.id) }
let(:path) { "files/ruby/popen.rb" }
diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb
index 93eb402e060..960f29f3805 100644
--- a/spec/models/environment_spec.rb
+++ b/spec/models/environment_spec.rb
@@ -7,8 +7,6 @@ describe Environment, models: true do
it { is_expected.to belong_to(:project) }
it { is_expected.to have_many(:deployments) }
- it { is_expected.to delegate_method(:last_deployment).to(:deployments).as(:last) }
-
it { is_expected.to delegate_method(:stop_action).to(:last_deployment) }
it { is_expected.to delegate_method(:manual_actions).to(:last_deployment) }
@@ -22,6 +20,20 @@ describe Environment, models: true do
it { is_expected.to validate_length_of(:external_url).is_at_most(255) }
it { is_expected.to validate_uniqueness_of(:external_url).scoped_to(:project_id) }
+ describe '.order_by_last_deployed_at' do
+ let(:project) { create(:project) }
+ let!(:environment1) { create(:environment, project: project) }
+ let!(:environment2) { create(:environment, project: project) }
+ let!(:environment3) { create(:environment, project: project) }
+ let!(:deployment1) { create(:deployment, environment: environment1) }
+ let!(:deployment2) { create(:deployment, environment: environment2) }
+ let!(:deployment3) { create(:deployment, environment: environment1) }
+
+ it 'returns the environments in order of having been last deployed' do
+ expect(project.environments.order_by_last_deployed_at.to_a).to eq([environment3, environment2, environment1])
+ end
+ end
+
describe '#nullify_external_url' do
it 'replaces a blank url with nil' do
env = build(:environment, external_url: "")
@@ -32,7 +44,7 @@ describe Environment, models: true do
end
describe '#includes_commit?' do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
context 'without a last deployment' do
it "returns false" do
@@ -63,8 +75,25 @@ describe Environment, models: true do
end
end
+ describe '#update_merge_request_metrics?' do
+ { 'production' => true,
+ 'production/eu' => true,
+ 'production/www.gitlab.com' => true,
+ 'productioneu' => false,
+ 'Production' => false,
+ 'Production/eu' => false,
+ 'test-production' => false
+ }.each do |name, expected_value|
+ it "returns #{expected_value} for #{name}" do
+ env = create(:environment, name: name)
+
+ expect(env.update_merge_request_metrics?).to eq(expected_value)
+ end
+ end
+ end
+
describe '#first_deployment_for' do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let!(:deployment) { create(:deployment, environment: environment, ref: commit.parent.id) }
let!(:deployment1) { create(:deployment, environment: environment, ref: commit.id) }
let(:head_commit) { project.commit }
@@ -95,8 +124,8 @@ describe Environment, models: true do
end
end
- describe '#stoppable?' do
- subject { environment.stoppable? }
+ describe '#stop_action?' do
+ subject { environment.stop_action? }
context 'when no other actions' do
it { is_expected.to be_falsey }
@@ -125,17 +154,39 @@ describe Environment, models: true do
end
end
- describe '#stop!' do
+ describe '#stop_with_action!' do
let(:user) { create(:user) }
- subject { environment.stop!(user) }
+ subject { environment.stop_with_action!(user) }
before do
- expect(environment).to receive(:stoppable?).and_call_original
+ expect(environment).to receive(:available?).and_call_original
end
context 'when no other actions' do
- it { is_expected.to be_nil }
+ context 'environment is available' do
+ before do
+ environment.update(state: :available)
+ end
+
+ it do
+ subject
+
+ expect(environment).to be_stopped
+ end
+ end
+
+ context 'environment is already stopped' do
+ before do
+ environment.update(state: :stopped)
+ end
+
+ it do
+ subject
+
+ expect(environment).to be_stopped
+ end
+ end
end
context 'when matching action is defined' do
@@ -271,6 +322,11 @@ describe Environment, models: true do
"1-foo" => "env-1-foo" + SUFFIX,
"1/foo" => "env-1-foo" + SUFFIX,
"foo-" => "foo" + SUFFIX,
+ "foo--bar" => "foo-bar" + SUFFIX,
+ "foo**bar" => "foo-bar" + SUFFIX,
+ "*-foo" => "env-foo" + SUFFIX,
+ "staging-12345678-" => "staging-12345678" + SUFFIX,
+ "staging-12345678-01234567" => "staging-12345678" + SUFFIX,
}.each do |name, matcher|
it "returns a slug matching #{matcher}, given #{name}" do
slug = described_class.new(name: name).generate_slug
@@ -279,4 +335,33 @@ describe Environment, models: true do
end
end
end
+
+ describe '#external_url_for' do
+ let(:source_path) { 'source/file.html' }
+ let(:sha) { RepoHelpers.sample_commit.id }
+
+ before do
+ environment.external_url = 'http://example.com'
+ end
+
+ context 'when the public path is not known' do
+ before do
+ allow(project).to receive(:public_path_for_source_path).with(source_path, sha).and_return(nil)
+ end
+
+ it 'returns nil' do
+ expect(environment.external_url_for(source_path, sha)).to be_nil
+ end
+ end
+
+ context 'when the public path is known' do
+ before do
+ allow(project).to receive(:public_path_for_source_path).with(source_path, sha).and_return('file.html')
+ end
+
+ it 'returns the full external URL' do
+ expect(environment.external_url_for(source_path, sha)).to eq('http://example.com/file.html')
+ end
+ end
+ end
end
diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb
index f8660da031d..8c90a538f57 100644
--- a/spec/models/event_spec.rb
+++ b/spec/models/event_spec.rb
@@ -19,7 +19,7 @@ describe Event, models: true do
let(:project) { create(:empty_project) }
it 'calls the reset_project_activity method' do
- expect_any_instance_of(Event).to receive(:reset_project_activity)
+ expect_any_instance_of(described_class).to receive(:reset_project_activity)
create_event(project, project.owner)
end
@@ -27,7 +27,7 @@ describe Event, models: true do
end
describe "Push event" do
- let(:project) { create(:project, :private) }
+ let(:project) { create(:empty_project, :private) }
let(:user) { project.owner }
let(:event) { create_event(project, user) }
@@ -43,33 +43,33 @@ describe Event, models: true do
describe '#membership_changed?' do
context "created" do
- subject { build(:event, action: Event::CREATED).membership_changed? }
+ subject { build(:event, :created).membership_changed? }
it { is_expected.to be_falsey }
end
context "updated" do
- subject { build(:event, action: Event::UPDATED).membership_changed? }
+ subject { build(:event, :updated).membership_changed? }
it { is_expected.to be_falsey }
end
context "expired" do
- subject { build(:event, action: Event::EXPIRED).membership_changed? }
+ subject { build(:event, :expired).membership_changed? }
it { is_expected.to be_truthy }
end
context "left" do
- subject { build(:event, action: Event::LEFT).membership_changed? }
+ subject { build(:event, :left).membership_changed? }
it { is_expected.to be_truthy }
end
context "joined" do
- subject { build(:event, action: Event::JOINED).membership_changed? }
+ subject { build(:event, :joined).membership_changed? }
it { is_expected.to be_truthy }
end
end
describe '#note?' do
- subject { Event.new(project: target.project, target: target) }
+ subject { described_class.new(project: target.project, target: target) }
context 'issue note event' do
let(:target) { create(:note_on_issue) }
@@ -97,7 +97,7 @@ describe Event, models: true do
let(:note_on_commit) { create(:note_on_commit, project: project) }
let(:note_on_issue) { create(:note_on_issue, noteable: issue, project: project) }
let(:note_on_confidential_issue) { create(:note_on_issue, noteable: confidential_issue, project: project) }
- let(:event) { Event.new(project: project, target: target, author_id: author.id) }
+ let(:event) { described_class.new(project: project, target: target, author_id: author.id) }
before do
project.team << [member, :developer]
@@ -187,7 +187,7 @@ describe Event, models: true do
end
context 'merge request diff note event' do
- let(:project) { create(:project, :public) }
+ let(:project) { create(:empty_project, :public) }
let(:merge_request) { create(:merge_request, source_project: project, author: author, assignee: assignee) }
let(:note_on_merge_request) { create(:legacy_diff_note_on_merge_request, noteable: merge_request, project: project) }
let(:target) { note_on_merge_request }
@@ -202,7 +202,7 @@ describe Event, models: true do
end
context 'private project' do
- let(:project) { create(:project, :private) }
+ let(:project) { create(:empty_project, :private) }
it do
expect(event.visible_to_user?(non_member)).to eq false
@@ -221,13 +221,13 @@ describe Event, models: true do
let!(:event2) { create(:closed_issue_event) }
describe 'without an explicit limit' do
- subject { Event.limit_recent }
+ subject { described_class.limit_recent }
it { is_expected.to eq([event2, event1]) }
end
describe 'with an explicit limit' do
- subject { Event.limit_recent(1) }
+ subject { described_class.limit_recent(1) }
it { is_expected.to eq([event2]) }
end
@@ -294,9 +294,9 @@ describe Event, models: true do
}
}
- Event.create({
+ described_class.create({
project: project,
- action: Event::PUSHED,
+ action: described_class::PUSHED,
data: data,
author_id: user.id
}.merge!(attrs))
diff --git a/spec/models/forked_project_link_spec.rb b/spec/models/forked_project_link_spec.rb
index 1863581f57b..454550c9710 100644
--- a/spec/models/forked_project_link_spec.rb
+++ b/spec/models/forked_project_link_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe ForkedProjectLink, "add link on fork" do
- let(:project_from) { create(:project) }
+ let(:project_from) { create(:project, :repository) }
let(:namespace) { create(:namespace) }
let(:user) { create(:user, namespace: namespace) }
@@ -21,7 +21,7 @@ end
describe '#forked?' do
let(:forked_project_link) { build(:forked_project_link) }
- let(:project_from) { create(:project) }
+ let(:project_from) { create(:project, :repository) }
let(:project_to) { create(:project, forked_project_link: forked_project_link) }
before :each do
diff --git a/spec/models/generic_commit_status_spec.rb b/spec/models/generic_commit_status_spec.rb
index 6004bfdb7b7..f4c3e6d503f 100644
--- a/spec/models/generic_commit_status_spec.rb
+++ b/spec/models/generic_commit_status_spec.rb
@@ -1,10 +1,20 @@
require 'spec_helper'
describe GenericCommitStatus, models: true do
- let(:pipeline) { create(:ci_pipeline) }
+ let(:project) { create(:empty_project) }
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+ let(:external_url) { 'http://example.gitlab.com/status' }
let(:generic_commit_status) do
- create(:generic_commit_status, pipeline: pipeline)
+ create(:generic_commit_status, pipeline: pipeline,
+ target_url: external_url)
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_length_of(:target_url).is_at_most(255) }
+ it { is_expected.to allow_value(nil).for(:target_url) }
+ it { is_expected.to allow_value('http://gitlab.com/s').for(:target_url) }
+ it { is_expected.not_to allow_value('javascript:alert(1)').for(:target_url) }
end
describe '#context' do
@@ -22,10 +32,25 @@ describe GenericCommitStatus, models: true do
describe '#detailed_status' do
let(:user) { create(:user) }
+ let(:status) { generic_commit_status.detailed_status(user) }
it 'returns detailed status object' do
- expect(generic_commit_status.detailed_status(user))
- .to be_a Gitlab::Ci::Status::Success
+ expect(status).to be_a Gitlab::Ci::Status::Success
+ end
+
+ context 'when user has ability to see datails' do
+ before { project.team << [user, :developer] }
+
+ it 'details path points to an external URL' do
+ expect(status).to have_details
+ expect(status.details_path).to eq external_url
+ end
+ end
+
+ context 'when user should not see details' do
+ it 'does not have details' do
+ expect(status).not_to have_details
+ end
end
end
diff --git a/spec/models/global_milestone_spec.rb b/spec/models/global_milestone_spec.rb
index dd033480527..cacbab8bcb1 100644
--- a/spec/models/global_milestone_spec.rb
+++ b/spec/models/global_milestone_spec.rb
@@ -4,29 +4,75 @@ describe GlobalMilestone, models: true do
let(:user) { create(:user) }
let(:user2) { create(:user) }
let(:group) { create(:group) }
- let(:project1) { create(:project, group: group) }
- let(:project2) { create(:project, path: 'gitlab-ci', group: group) }
- let(:project3) { create(:project, path: 'cookbook-gitlab', group: group) }
- let(:milestone1_project1) { create(:milestone, title: "Milestone v1.2", project: project1) }
- let(:milestone1_project2) { create(:milestone, title: "Milestone v1.2", project: project2) }
- let(:milestone1_project3) { create(:milestone, title: "Milestone v1.2", project: project3) }
- let(:milestone2_project1) { create(:milestone, title: "VD-123", project: project1) }
- let(:milestone2_project2) { create(:milestone, title: "VD-123", project: project2) }
- let(:milestone2_project3) { create(:milestone, title: "VD-123", project: project3) }
+ let(:project1) { create(:empty_project, group: group) }
+ let(:project2) { create(:empty_project, path: 'gitlab-ci', group: group) }
+ let(:project3) { create(:empty_project, path: 'cookbook-gitlab', group: group) }
describe '.build_collection' do
+ let(:milestone1_due_date) { 2.weeks.from_now.to_date }
+
+ let!(:milestone1_project1) do
+ create(
+ :milestone,
+ title: "Milestone v1.2",
+ project: project1,
+ due_date: milestone1_due_date
+ )
+ end
+
+ let!(:milestone1_project2) do
+ create(
+ :milestone,
+ title: "Milestone v1.2",
+ project: project2,
+ due_date: milestone1_due_date
+ )
+ end
+
+ let!(:milestone1_project3) do
+ create(
+ :milestone,
+ title: "Milestone v1.2",
+ project: project3,
+ due_date: milestone1_due_date
+ )
+ end
+
+ let!(:milestone2_project1) do
+ create(
+ :milestone,
+ title: "VD-123",
+ project: project1,
+ due_date: nil
+ )
+ end
+
+ let!(:milestone2_project2) do
+ create(
+ :milestone,
+ title: "VD-123",
+ project: project2,
+ due_date: nil
+ )
+ end
+
+ let!(:milestone2_project3) do
+ create(
+ :milestone,
+ title: "VD-123",
+ project: project3,
+ due_date: nil
+ )
+ end
+
before do
- milestones =
- [
- milestone1_project1,
- milestone1_project2,
- milestone1_project3,
- milestone2_project1,
- milestone2_project2,
- milestone2_project3
- ]
+ projects = [
+ project1,
+ project2,
+ project3
+ ]
- @global_milestones = GlobalMilestone.build_collection(milestones)
+ @global_milestones = GlobalMilestone.build_collection(projects, {})
end
it 'has all project milestones' do
@@ -40,9 +86,17 @@ describe GlobalMilestone, models: true do
it 'has all project milestones' do
expect(@global_milestones.map { |group_milestone| group_milestone.milestones.count }.sum).to eq(6)
end
+
+ it 'sorts collection by due date' do
+ expect(@global_milestones.map(&:due_date)).to eq [nil, milestone1_due_date]
+ end
end
describe '#initialize' do
+ let(:milestone1_project1) { create(:milestone, title: "Milestone v1.2", project: project1) }
+ let(:milestone1_project2) { create(:milestone, title: "Milestone v1.2", project: project2) }
+ let(:milestone1_project3) { create(:milestone, title: "Milestone v1.2", project: project3) }
+
before do
milestones =
[
diff --git a/spec/models/group_label_spec.rb b/spec/models/group_label_spec.rb
index 668aa6fb357..555a876daeb 100644
--- a/spec/models/group_label_spec.rb
+++ b/spec/models/group_label_spec.rb
@@ -43,7 +43,7 @@ describe GroupLabel, models: true do
let(:target_project) { build_stubbed(:empty_project, name: 'project-2', namespace: namespace) }
it 'returns a String reference to the object' do
- expect(label.to_reference(source_project, target_project)).to eq %(project-1~#{label.id})
+ expect(label.to_reference(source_project, target_project: target_project)).to eq %(project-1~#{label.id})
end
end
diff --git a/spec/models/group_milestone_spec.rb b/spec/models/group_milestone_spec.rb
new file mode 100644
index 00000000000..916afb7aaf5
--- /dev/null
+++ b/spec/models/group_milestone_spec.rb
@@ -0,0 +1,32 @@
+require 'spec_helper'
+
+describe GroupMilestone, models: true do
+ let(:group) { create(:group) }
+ let(:project) { create(:empty_project, group: group) }
+ let(:project_milestone) do
+ create(:milestone, title: "Milestone v1.2", project: project)
+ end
+
+ describe '.build' do
+ it 'returns milestone with group assigned' do
+ milestone = GroupMilestone.build(
+ group,
+ [project],
+ project_milestone.title
+ )
+
+ expect(milestone.group).to eq group
+ end
+ end
+
+ describe '.build_collection' do
+ before do
+ project_milestone
+ end
+
+ it 'returns array of milestones, each with group assigned' do
+ milestones = GroupMilestone.build_collection(group, [project], {})
+ expect(milestones).to all(have_attributes(group: group))
+ end
+ end
+end
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index 7d5ecfbaa64..a4e6eb4e3a6 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -81,13 +81,19 @@ describe Group, models: true do
describe 'public_only' do
subject { described_class.public_only.to_a }
- it{ is_expected.to eq([group]) }
+ it { is_expected.to eq([group]) }
end
describe 'public_and_internal_only' do
subject { described_class.public_and_internal_only.to_a }
- it{ is_expected.to match_array([group, internal_group]) }
+ it { is_expected.to match_array([group, internal_group]) }
+ end
+
+ describe 'non_public_only' do
+ subject { described_class.non_public_only.to_a }
+
+ it { is_expected.to match_array([private_group, internal_group]) }
end
end
@@ -269,6 +275,12 @@ describe Group, models: true do
it 'returns the canonical URL' do
expect(group.web_url).to include("groups/#{group.name}")
end
+
+ context 'nested group' do
+ let(:nested_group) { create(:group, :nested) }
+
+ it { expect(nested_group.web_url).to include("groups/#{nested_group.full_path}") }
+ end
end
describe 'nested group' do
@@ -277,4 +289,28 @@ describe Group, models: true do
it { is_expected.to be_valid }
it { expect(subject.parent).to be_kind_of(Group) }
end
+
+ describe '#members_with_parents' do
+ let!(:group) { create(:group, :nested) }
+ let!(:master) { group.parent.add_user(create(:user), GroupMember::MASTER) }
+ let!(:developer) { group.add_user(create(:user), GroupMember::DEVELOPER) }
+
+ it 'returns parents members' do
+ expect(group.members_with_parents).to include(developer)
+ expect(group.members_with_parents).to include(master)
+ end
+ end
+
+ describe '#user_ids_for_project_authorizations' do
+ it 'returns the user IDs for which to refresh authorizations' do
+ master = create(:user)
+ developer = create(:user)
+
+ group.add_user(master, GroupMember::MASTER)
+ group.add_user(developer, GroupMember::DEVELOPER)
+
+ expect(group.user_ids_for_project_authorizations).
+ to include(master.id, developer.id)
+ end
+ end
end
diff --git a/spec/models/guest_spec.rb b/spec/models/guest_spec.rb
index d79f929f7a1..c60bd7af958 100644
--- a/spec/models/guest_spec.rb
+++ b/spec/models/guest_spec.rb
@@ -1,9 +1,9 @@
require 'spec_helper'
describe Guest, lib: true do
- let(:public_project) { create(:project, :public) }
- let(:private_project) { create(:project, :private) }
- let(:internal_project) { create(:project, :internal) }
+ let(:public_project) { build_stubbed(:empty_project, :public) }
+ let(:private_project) { build_stubbed(:empty_project, :private) }
+ let(:internal_project) { build_stubbed(:empty_project, :internal) }
describe '.can_pull?' do
context 'when project is private' do
@@ -37,8 +37,6 @@ describe Guest, lib: true do
context 'when repository is enabled' do
it 'allows to pull the repo' do
- public_project.project_feature.update_attribute(:repository_access_level, ProjectFeature::ENABLED)
-
expect(Guest.can?(:download_code, public_project)).to eq(true)
end
end
diff --git a/spec/models/hooks/system_hook_spec.rb b/spec/models/hooks/system_hook_spec.rb
index ad2b710041a..e8caad00c44 100644
--- a/spec/models/hooks/system_hook_spec.rb
+++ b/spec/models/hooks/system_hook_spec.rb
@@ -4,7 +4,7 @@ describe SystemHook, models: true do
describe "execute" do
let(:system_hook) { create(:system_hook) }
let(:user) { create(:user) }
- let(:project) { create(:project, namespace: user.namespace) }
+ let(:project) { create(:empty_project, namespace: user.namespace) }
let(:group) { create(:group) }
before do
diff --git a/spec/models/hooks/web_hook_spec.rb b/spec/models/hooks/web_hook_spec.rb
index e52b9d75cef..9d4db1bfb52 100644
--- a/spec/models/hooks/web_hook_spec.rb
+++ b/spec/models/hooks/web_hook_spec.rb
@@ -25,7 +25,7 @@ describe WebHook, models: true do
end
describe "execute" do
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project) }
let(:project_hook) { create(:project_hook) }
before(:each) do
diff --git a/spec/models/issue/metrics_spec.rb b/spec/models/issue/metrics_spec.rb
index 2459a49f095..08712f2a768 100644
--- a/spec/models/issue/metrics_spec.rb
+++ b/spec/models/issue/metrics_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Issue::Metrics, models: true do
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project) }
subject { create(:issue, project: project) }
diff --git a/spec/models/issue_collection_spec.rb b/spec/models/issue_collection_spec.rb
index d742c814680..d8aed25c041 100644
--- a/spec/models/issue_collection_spec.rb
+++ b/spec/models/issue_collection_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe IssueCollection do
let(:user) { create(:user) }
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project) }
let(:issue1) { create(:issue, project: project) }
let(:issue2) { create(:issue, project: project) }
let(:collection) { described_class.new([issue1, issue2]) }
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index bb56e44db29..bba9058f394 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -23,17 +23,74 @@ describe Issue, models: true do
end
describe '#to_reference' do
- let(:project) { build(:empty_project, name: 'sample-project') }
- let(:issue) { build(:issue, iid: 1, project: project) }
+ let(:namespace) { build(:namespace, path: 'sample-namespace') }
+ let(:project) { build(:empty_project, name: 'sample-project', namespace: namespace) }
+ let(:issue) { build(:issue, iid: 1, project: project) }
+ let(:group) { create(:group, name: 'Group', path: 'sample-group') }
+
+ context 'when nil argument' do
+ it 'returns issue id' do
+ expect(issue.to_reference).to eq "#1"
+ end
+ end
+
+ context 'when full is true' do
+ it 'returns complete path to the issue' do
+ expect(issue.to_reference(full: true)).to eq 'sample-namespace/sample-project#1'
+ expect(issue.to_reference(project, full: true)).to eq 'sample-namespace/sample-project#1'
+ expect(issue.to_reference(group, full: true)).to eq 'sample-namespace/sample-project#1'
+ end
+ end
+
+ context 'when same project argument' do
+ it 'returns issue id' do
+ expect(issue.to_reference(project)).to eq("#1")
+ end
+ end
+
+ context 'when cross namespace project argument' do
+ let(:another_namespace_project) { create(:empty_project, name: 'another-project') }
- it 'returns a String reference to the object' do
- expect(issue.to_reference).to eq "#1"
+ it 'returns complete path to the issue' do
+ expect(issue.to_reference(another_namespace_project)).to eq 'sample-namespace/sample-project#1'
+ end
end
it 'supports a cross-project reference' do
- another_project = build(:project, name: 'another-project', namespace: project.namespace)
+ another_project = build(:empty_project, name: 'another-project', namespace: project.namespace)
expect(issue.to_reference(another_project)).to eq "sample-project#1"
end
+
+ context 'when same namespace / cross-project argument' do
+ let(:another_project) { create(:empty_project, namespace: namespace) }
+
+ it 'returns path to the issue with the project name' do
+ expect(issue.to_reference(another_project)).to eq 'sample-project#1'
+ end
+ end
+
+ context 'when different namespace / cross-project argument' do
+ let(:another_namespace) { create(:namespace, path: 'another-namespace') }
+ let(:another_project) { create(:empty_project, path: 'another-project', namespace: another_namespace) }
+
+ it 'returns full path to the issue' do
+ expect(issue.to_reference(another_project)).to eq 'sample-namespace/sample-project#1'
+ end
+ end
+
+ context 'when argument is a namespace' do
+ context 'with same project path' do
+ it 'returns path to the issue with the project name' do
+ expect(issue.to_reference(namespace)).to eq 'sample-project#1'
+ end
+ end
+
+ context 'with different project path' do
+ it 'returns full path to the issue' do
+ expect(issue.to_reference(group)).to eq 'sample-namespace/sample-project#1'
+ end
+ end
+ end
end
describe '#is_being_reassigned?' do
@@ -56,9 +113,9 @@ describe Issue, models: true do
end
describe '#closed_by_merge_requests' do
- let(:project) { create(:project) }
- let(:issue) { create(:issue, project: project, state: "opened")}
- let(:closed_issue) { build(:issue, project: project, state: "closed")}
+ let(:project) { create(:project, :repository) }
+ let(:issue) { create(:issue, project: project)}
+ let(:closed_issue) { build(:issue, :closed, project: project)}
let(:mr) do
opts = {
@@ -100,7 +157,7 @@ describe Issue, models: true do
describe '#referenced_merge_requests' do
it 'returns the referenced merge requests' do
- project = create(:project, :public)
+ project = create(:empty_project, :public)
mr1 = create(:merge_request,
source_project: project,
@@ -133,7 +190,7 @@ describe Issue, models: true do
end
context 'user is reporter in project issue belongs to' do
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project) }
let(:issue) { create(:issue, project: project) }
before { project.team << [user, :reporter] }
@@ -147,7 +204,7 @@ describe Issue, models: true do
context 'checking destination project also' do
subject { issue.can_move?(user, to_project) }
- let(:to_project) { create(:project) }
+ let(:to_project) { create(:empty_project) }
context 'destination project allowed' do
before { to_project.team << [user, :reporter] }
@@ -213,7 +270,7 @@ describe Issue, models: true do
end
it_behaves_like 'an editable mentionable' do
- subject { create(:issue) }
+ subject { create(:issue, project: create(:project, :repository)) }
let(:backref_text) { "issue #{subject.to_reference}" }
let(:set_mentionable_text) { ->(txt){ subject.description = txt } }
@@ -242,7 +299,7 @@ describe Issue, models: true do
describe '#participants' do
context 'using a public project' do
- let(:project) { create(:project, :public) }
+ let(:project) { create(:empty_project, :public) }
let(:issue) { create(:issue, project: project) }
let!(:note1) do
@@ -264,7 +321,7 @@ describe Issue, models: true do
context 'using a private project' do
it 'does not include mentioned users that do not have access to the project' do
- project = create(:project)
+ project = create(:empty_project)
user = create(:user)
issue = create(:issue, project: project)
diff --git a/spec/models/key_spec.rb b/spec/models/key_spec.rb
index 2a33d819138..7c40cfd8253 100644
--- a/spec/models/key_spec.rb
+++ b/spec/models/key_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe Key, models: true do
+ include EmailHelpers
+
describe "Associations" do
it { is_expected.to belong_to(:user) }
end
@@ -26,6 +28,34 @@ describe Key, models: true do
expect(build(:key, user: user).publishable_key).to include("#{user.name} (#{Gitlab.config.gitlab.host})")
end
end
+
+ describe "#update_last_used_at" do
+ let(:key) { create(:key) }
+
+ context 'when key was not updated during the last day' do
+ before do
+ allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain).
+ and_return('000000')
+ end
+
+ it 'enqueues a UseKeyWorker job' do
+ expect(UseKeyWorker).to receive(:perform_async).with(key.id)
+ key.update_last_used_at
+ end
+ end
+
+ context 'when key was updated during the last day' do
+ before do
+ allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain).
+ and_return(false)
+ end
+
+ it 'does not enqueue a UseKeyWorker job' do
+ expect(UseKeyWorker).not_to receive(:perform_async)
+ key.update_last_used_at
+ end
+ end
+ end
end
context "validation of uniqueness (based on fingerprint uniqueness)" do
@@ -96,4 +126,16 @@ describe Key, models: true do
expect(described_class.new(key: " #{valid_key} ").key).to eq(valid_key)
end
end
+
+ describe 'notification' do
+ let(:user) { create(:user) }
+
+ it 'sends a notification' do
+ perform_enqueued_jobs do
+ create(:key, user: user)
+ end
+
+ should_email(user)
+ end
+ end
end
diff --git a/spec/models/label_spec.rb b/spec/models/label_spec.rb
index 0c163659a71..a9139f7d4ab 100644
--- a/spec/models/label_spec.rb
+++ b/spec/models/label_spec.rb
@@ -31,12 +31,14 @@ describe Label, models: true do
it 'validates title' do
is_expected.not_to allow_value('G,ITLAB').for(:title)
is_expected.not_to allow_value('').for(:title)
+ is_expected.not_to allow_value('s' * 256).for(:title)
is_expected.to allow_value('GITLAB').for(:title)
is_expected.to allow_value('gitlab').for(:title)
is_expected.to allow_value('G?ITLAB').for(:title)
is_expected.to allow_value('G&ITLAB').for(:title)
is_expected.to allow_value("customer's request").for(:title)
+ is_expected.to allow_value('s' * 255).for(:title)
end
end
diff --git a/spec/models/lfs_objects_project_spec.rb b/spec/models/lfs_objects_project_spec.rb
new file mode 100644
index 00000000000..7bc278e350f
--- /dev/null
+++ b/spec/models/lfs_objects_project_spec.rb
@@ -0,0 +1,36 @@
+require 'spec_helper'
+
+describe LfsObjectsProject, models: true do
+ subject { create(:lfs_objects_project, project: project) }
+ let(:project) { create(:empty_project) }
+
+ describe 'associations' do
+ it { is_expected.to belong_to(:project) }
+ it { is_expected.to belong_to(:lfs_object) }
+ end
+
+ describe 'validation' do
+ it { is_expected.to validate_presence_of(:lfs_object_id) }
+ it { is_expected.to validate_uniqueness_of(:lfs_object_id).scoped_to(:project_id).with_message("already exists in project") }
+
+ it { is_expected.to validate_presence_of(:project_id) }
+ end
+
+ describe '#update_project_statistics' do
+ it 'updates project statistics when the object is added' do
+ expect(ProjectCacheWorker).to receive(:perform_async)
+ .with(project.id, [], [:lfs_objects_size])
+
+ subject.save!
+ end
+
+ it 'updates project statistics when the object is removed' do
+ subject.save!
+
+ expect(ProjectCacheWorker).to receive(:perform_async)
+ .with(project.id, [], [:lfs_objects_size])
+
+ subject.destroy
+ end
+ end
+end
diff --git a/spec/models/list_spec.rb b/spec/models/list_spec.rb
index 9e1a52011c3..e6ca4853873 100644
--- a/spec/models/list_spec.rb
+++ b/spec/models/list_spec.rb
@@ -19,13 +19,6 @@ describe List do
expect(subject).to validate_uniqueness_of(:label_id).scoped_to(:board_id)
end
- context 'when list_type is set to backlog' do
- subject { described_class.new(list_type: :backlog) }
-
- it { is_expected.not_to validate_presence_of(:label) }
- it { is_expected.not_to validate_presence_of(:position) }
- end
-
context 'when list_type is set to done' do
subject { described_class.new(list_type: :done) }
@@ -41,12 +34,6 @@ describe List do
expect(subject.destroy).to be_truthy
end
- it 'can not be destroyed when list_type is set to backlog' do
- subject = create(:backlog_list)
-
- expect(subject.destroy).to be_falsey
- end
-
it 'can not be destroyed when when list_type is set to done' do
subject = create(:done_list)
@@ -55,19 +42,13 @@ describe List do
end
describe '#destroyable?' do
- it 'retruns true when list_type is set to label' do
+ it 'returns true when list_type is set to label' do
subject.list_type = :label
expect(subject).to be_destroyable
end
- it 'retruns false when list_type is set to backlog' do
- subject.list_type = :backlog
-
- expect(subject).not_to be_destroyable
- end
-
- it 'retruns false when list_type is set to done' do
+ it 'returns false when list_type is set to done' do
subject.list_type = :done
expect(subject).not_to be_destroyable
@@ -75,19 +56,13 @@ describe List do
end
describe '#movable?' do
- it 'retruns true when list_type is set to label' do
+ it 'returns true when list_type is set to label' do
subject.list_type = :label
expect(subject).to be_movable
end
- it 'retruns false when list_type is set to backlog' do
- subject.list_type = :backlog
-
- expect(subject).not_to be_movable
- end
-
- it 'retruns false when list_type is set to done' do
+ it 'returns false when list_type is set to done' do
subject.list_type = :done
expect(subject).not_to be_movable
@@ -102,12 +77,6 @@ describe List do
expect(subject.title).to eq 'Development'
end
- it 'returns Backlog when list_type is set to backlog' do
- subject.list_type = :backlog
-
- expect(subject.title).to eq 'Backlog'
- end
-
it 'returns Done when list_type is set to done' do
subject.list_type = :done
diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb
index 4f7c8a36cb5..16e2144d6a1 100644
--- a/spec/models/member_spec.rb
+++ b/spec/models/member_spec.rb
@@ -481,7 +481,7 @@ describe Member, models: true do
describe "destroying a record", truncate: true do
it "refreshes user's authorized projects" do
- project = create(:project, :private)
+ project = create(:empty_project, :private)
user = create(:user)
member = project.team << [user, :reporter]
diff --git a/spec/models/members/project_member_spec.rb b/spec/models/members/project_member_spec.rb
index 68f72f5c86e..e4be0aba7a6 100644
--- a/spec/models/members/project_member_spec.rb
+++ b/spec/models/members/project_member_spec.rb
@@ -83,8 +83,8 @@ describe ProjectMember, models: true do
describe '.import_team' do
before do
- @project_1 = create :project
- @project_2 = create :project
+ @project_1 = create(:empty_project)
+ @project_2 = create(:empty_project)
@user_1 = create :user
@user_2 = create :user
@@ -117,7 +117,7 @@ describe ProjectMember, models: true do
users = create_list(:user, 2)
described_class.add_users_to_projects(
- [projects.first.id, projects.second],
+ [projects.first.id, projects.second.id],
[users.first.id, users.second],
described_class::MASTER)
@@ -131,8 +131,8 @@ describe ProjectMember, models: true do
describe '.truncate_teams' do
before do
- @project_1 = create :project
- @project_2 = create :project
+ @project_1 = create(:empty_project)
+ @project_2 = create(:empty_project)
@user_1 = create :user
@user_2 = create :user
diff --git a/spec/models/merge_request/metrics_spec.rb b/spec/models/merge_request/metrics_spec.rb
index 255db41cb19..9afed311e27 100644
--- a/spec/models/merge_request/metrics_spec.rb
+++ b/spec/models/merge_request/metrics_spec.rb
@@ -1,9 +1,7 @@
require 'spec_helper'
describe MergeRequest::Metrics, models: true do
- let(:project) { create(:project) }
-
- subject { create(:merge_request, source_project: project) }
+ subject { create(:merge_request) }
describe "when recording the default set of metrics on merge request save" do
it "records the merge time" do
diff --git a/spec/models/merge_request_diff_spec.rb b/spec/models/merge_request_diff_spec.rb
index eb876d105da..6d599e148a2 100644
--- a/spec/models/merge_request_diff_spec.rb
+++ b/spec/models/merge_request_diff_spec.rb
@@ -76,6 +76,32 @@ describe MergeRequestDiff, models: true do
end
end
+ describe '#save_diffs' do
+ it 'saves collected state' do
+ mr_diff = create(:merge_request).merge_request_diff
+
+ expect(mr_diff.collected?).to be_truthy
+ end
+
+ it 'saves overflow state' do
+ allow(Commit).to receive(:max_diff_options)
+ .and_return(max_lines: 0, max_files: 0)
+
+ mr_diff = create(:merge_request).merge_request_diff
+
+ expect(mr_diff.overflow?).to be_truthy
+ end
+
+ it 'saves empty state' do
+ allow_any_instance_of(MergeRequestDiff).to receive(:commits)
+ .and_return([])
+
+ mr_diff = create(:merge_request).merge_request_diff
+
+ expect(mr_diff.empty?).to be_truthy
+ end
+ end
+
describe '#commits_sha' do
it 'returns all commits SHA using serialized commits' do
subject.st_commits = [
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 5da00a8636a..a01741a9971 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -65,7 +65,7 @@ describe MergeRequest, models: true do
end
describe '#target_branch_sha' do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
subject { create(:merge_request, source_project: project, target_project: project) }
@@ -97,7 +97,7 @@ describe MergeRequest, models: true do
commit = double('commit1', safe_message: "Fixes #{issue.to_reference}")
allow(subject).to receive(:commits).and_return([commit])
- expect { subject.cache_merge_request_closes_issues! }.to change(subject.merge_requests_closing_issues, :count).by(1)
+ expect { subject.cache_merge_request_closes_issues!(subject.author) }.to change(subject.merge_requests_closing_issues, :count).by(1)
end
it 'does not cache issues from external trackers' do
@@ -106,7 +106,7 @@ describe MergeRequest, models: true do
commit = double('commit1', safe_message: "Fixes #{issue.to_reference}")
allow(subject).to receive(:commits).and_return([commit])
- expect { subject.cache_merge_request_closes_issues! }.not_to change(subject.merge_requests_closing_issues, :count)
+ expect { subject.cache_merge_request_closes_issues!(subject.author) }.not_to change(subject.merge_requests_closing_issues, :count)
end
end
@@ -150,9 +150,13 @@ describe MergeRequest, models: true do
end
it 'supports a cross-project reference' do
- another_project = build(:project, name: 'another-project', namespace: project.namespace)
+ another_project = build(:empty_project, name: 'another-project', namespace: project.namespace)
expect(merge_request.to_reference(another_project)).to eq "sample-project!1"
end
+
+ it 'returns a String reference with the full path' do
+ expect(merge_request.to_reference(full: true)).to eq(project.path_with_namespace + '!1')
+ end
end
describe '#raw_diffs' do
@@ -241,8 +245,8 @@ describe MergeRequest, models: true do
describe '#for_fork?' do
it 'returns true if the merge request is for a fork' do
- subject.source_project = create(:project, namespace: create(:group))
- subject.target_project = create(:project, namespace: create(:group))
+ subject.source_project = build_stubbed(:empty_project, namespace: create(:group))
+ subject.target_project = build_stubbed(:empty_project, namespace: create(:group))
expect(subject.for_fork?).to be_truthy
end
@@ -283,16 +287,20 @@ describe MergeRequest, models: true do
end
describe '#issues_mentioned_but_not_closing' do
- it 'detects issues mentioned in description but not closed' do
- mentioned_issue = create(:issue, project: subject.project)
+ let(:closing_issue) { create :issue, project: subject.project }
+ let(:mentioned_issue) { create :issue, project: subject.project }
+ let(:commit) { double('commit', safe_message: "Fixes #{closing_issue.to_reference}") }
+
+ it 'detects issues mentioned in description but not closed' do
subject.project.team << [subject.author, :developer]
- subject.description = "Is related to #{mentioned_issue.to_reference}"
+ subject.description = "Is related to #{mentioned_issue.to_reference} and #{closing_issue.to_reference}"
+ allow(subject).to receive(:commits).and_return([commit])
allow(subject.project).to receive(:default_branch).
and_return(subject.target_branch)
- expect(subject.issues_mentioned_but_not_closing).to match_array([mentioned_issue])
+ expect(subject.issues_mentioned_but_not_closing(subject.author)).to match_array([mentioned_issue])
end
end
@@ -493,8 +501,8 @@ describe MergeRequest, models: true do
end
describe '#diverged_commits_count' do
- let(:project) { create(:project) }
- let(:fork_project) { create(:project, forked_from_project: project) }
+ let(:project) { create(:project, :repository) }
+ let(:fork_project) { create(:project, :repository, forked_from_project: project) }
context 'when the target branch does not exist anymore' do
subject { create(:merge_request, source_project: project, target_project: project) }
@@ -719,7 +727,7 @@ describe MergeRequest, models: true do
end
describe '#participants' do
- let(:project) { create(:project, :public) }
+ let(:project) { create(:empty_project, :public) }
let(:mr) do
create(:merge_request, source_project: project, target_project: project)
@@ -760,15 +768,17 @@ describe MergeRequest, models: true do
end
describe '#check_if_can_be_merged' do
- let(:project) { create(:project, only_allow_merge_if_build_succeeds: true) }
+ let(:project) { create(:empty_project, only_allow_merge_if_build_succeeds: true) }
subject { create(:merge_request, source_project: project, merge_status: :unchecked) }
context 'when it is not broken and has no conflicts' do
- it 'is marked as mergeable' do
+ before do
allow(subject).to receive(:broken?) { false }
allow(project.repository).to receive(:can_be_merged?).and_return(true)
+ end
+ it 'is marked as mergeable' do
expect { subject.check_if_can_be_merged }.to change { subject.merge_status }.to('can_be_merged')
end
end
@@ -779,6 +789,12 @@ describe MergeRequest, models: true do
it 'becomes unmergeable' do
expect { subject.check_if_can_be_merged }.to change { subject.merge_status }.to('cannot_be_merged')
end
+
+ it 'creates Todo on unmergeability' do
+ expect_any_instance_of(TodoService).to receive(:merge_request_became_unmergeable).with(subject)
+
+ subject.check_if_can_be_merged
+ end
end
context 'when it has conflicts' do
@@ -794,7 +810,7 @@ describe MergeRequest, models: true do
end
describe '#mergeable?' do
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project) }
subject { create(:merge_request, source_project: project) }
@@ -814,7 +830,7 @@ describe MergeRequest, models: true do
end
describe '#mergeable_state?' do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
subject { create(:merge_request, source_project: project) }
@@ -941,7 +957,7 @@ describe MergeRequest, models: true do
let(:merge_request) { create(:merge_request_with_diff_notes, source_project: project) }
context 'when project.only_allow_merge_if_all_discussions_are_resolved == true' do
- let(:project) { create(:project, only_allow_merge_if_all_discussions_are_resolved: true) }
+ let(:project) { create(:project, :repository, only_allow_merge_if_all_discussions_are_resolved: true) }
context 'with all discussions resolved' do
before do
@@ -975,7 +991,7 @@ describe MergeRequest, models: true do
end
context 'when project.only_allow_merge_if_all_discussions_are_resolved == false' do
- let(:project) { create(:project, only_allow_merge_if_all_discussions_are_resolved: false) }
+ let(:project) { create(:project, :repository, only_allow_merge_if_all_discussions_are_resolved: false) }
context 'with unresolved discussions' do
before do
@@ -989,10 +1005,16 @@ describe MergeRequest, models: true do
end
end
- describe "#environments" do
- let(:project) { create(:project) }
+ describe "#environments_for" do
+ let(:project) { create(:project, :repository) }
+ let(:user) { project.creator }
let(:merge_request) { create(:merge_request, source_project: project) }
+ before do
+ merge_request.source_project.add_master(user)
+ merge_request.target_project.add_master(user)
+ end
+
context 'with multiple environments' do
let(:environments) { create_list(:environment, 3, project: project) }
@@ -1002,13 +1024,13 @@ describe MergeRequest, models: true do
end
it 'selects deployed environments' do
- expect(merge_request.environments).to contain_exactly(environments.first)
+ expect(merge_request.environments_for(user)).to contain_exactly(environments.first)
end
end
context 'with environments on source project' do
let(:source_project) do
- create(:project) do |fork_project|
+ create(:project, :repository) do |fork_project|
fork_project.create_forked_project_link(forked_to_project_id: fork_project.id, forked_from_project_id: project.id)
end
end
@@ -1026,7 +1048,7 @@ describe MergeRequest, models: true do
end
it 'selects deployed environments' do
- expect(merge_request.environments).to contain_exactly(source_environment)
+ expect(merge_request.environments_for(user)).to contain_exactly(source_environment)
end
context 'with environments on target project' do
@@ -1037,7 +1059,7 @@ describe MergeRequest, models: true do
end
it 'selects deployed environments' do
- expect(merge_request.environments).to contain_exactly(source_environment, target_environment)
+ expect(merge_request.environments_for(user)).to contain_exactly(source_environment, target_environment)
end
end
end
@@ -1048,7 +1070,7 @@ describe MergeRequest, models: true do
end
it 'returns an empty array' do
- expect(merge_request.environments).to be_empty
+ expect(merge_request.environments_for(user)).to be_empty
end
end
end
@@ -1385,8 +1407,8 @@ describe MergeRequest, models: true do
end
describe "#source_project_missing?" do
- let(:project) { create(:project) }
- let(:fork_project) { create(:project, forked_from_project: project) }
+ let(:project) { create(:empty_project) }
+ let(:fork_project) { create(:empty_project, forked_from_project: project) }
let(:user) { create(:user) }
let(:unlink_project) { Projects::UnlinkForkService.new(fork_project, user) }
@@ -1423,8 +1445,8 @@ describe MergeRequest, models: true do
end
describe "#closed_without_fork?" do
- let(:project) { create(:project) }
- let(:fork_project) { create(:project, forked_from_project: project) }
+ let(:project) { create(:empty_project) }
+ let(:fork_project) { create(:empty_project, forked_from_project: project) }
let(:user) { create(:user) }
let(:unlink_project) { Projects::UnlinkForkService.new(fork_project, user) }
@@ -1469,9 +1491,9 @@ describe MergeRequest, models: true do
end
context 'forked project' do
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project) }
let(:user) { create(:user) }
- let(:fork_project) { create(:project, forked_from_project: project, namespace: user.namespace) }
+ let(:fork_project) { create(:empty_project, forked_from_project: project, namespace: user.namespace) }
let!(:merge_request) do
create(:closed_merge_request,
@@ -1506,6 +1528,108 @@ describe MergeRequest, models: true do
end
end
+ describe '#mergeable_with_slash_command?' do
+ def create_pipeline(status)
+ create(:ci_pipeline_with_one_job,
+ project: project,
+ ref: merge_request.source_branch,
+ sha: merge_request.diff_head_sha,
+ status: status)
+ end
+
+ let(:project) { create(:project, :public, :repository, only_allow_merge_if_build_succeeds: true) }
+ let(:developer) { create(:user) }
+ let(:user) { create(:user) }
+ let(:merge_request) { create(:merge_request, source_project: project) }
+ let(:mr_sha) { merge_request.diff_head_sha }
+
+ before do
+ project.team << [developer, :developer]
+ end
+
+ context 'when autocomplete_precheck is set to true' do
+ it 'is mergeable by developer' do
+ expect(merge_request.mergeable_with_slash_command?(developer, autocomplete_precheck: true)).to be_truthy
+ end
+
+ it 'is not mergeable by normal user' do
+ expect(merge_request.mergeable_with_slash_command?(user, autocomplete_precheck: true)).to be_falsey
+ end
+ end
+
+ context 'when autocomplete_precheck is set to false' do
+ it 'is mergeable by developer' do
+ expect(merge_request.mergeable_with_slash_command?(developer, last_diff_sha: mr_sha)).to be_truthy
+ end
+
+ it 'is not mergeable by normal user' do
+ expect(merge_request.mergeable_with_slash_command?(user, last_diff_sha: mr_sha)).to be_falsey
+ end
+
+ context 'closed MR' do
+ before do
+ merge_request.update_attribute(:state, :closed)
+ end
+
+ it 'is not mergeable' do
+ expect(merge_request.mergeable_with_slash_command?(developer, last_diff_sha: mr_sha)).to be_falsey
+ end
+ end
+
+ context 'MR with WIP' do
+ before do
+ merge_request.update_attribute(:title, 'WIP: some MR')
+ end
+
+ it 'is not mergeable' do
+ expect(merge_request.mergeable_with_slash_command?(developer, last_diff_sha: mr_sha)).to be_falsey
+ end
+ end
+
+ context 'sha differs from the MR diff_head_sha' do
+ it 'is not mergeable' do
+ expect(merge_request.mergeable_with_slash_command?(developer, last_diff_sha: 'some other sha')).to be_falsey
+ end
+ end
+
+ context 'sha is not provided' do
+ it 'is not mergeable' do
+ expect(merge_request.mergeable_with_slash_command?(developer)).to be_falsey
+ end
+ end
+
+ context 'with pipeline ok' do
+ before do
+ create_pipeline(:success)
+ end
+
+ it 'is mergeable' do
+ expect(merge_request.mergeable_with_slash_command?(developer, last_diff_sha: mr_sha)).to be_truthy
+ end
+ end
+
+ context 'with failing pipeline' do
+ before do
+ create_pipeline(:failed)
+ end
+
+ it 'is not mergeable' do
+ expect(merge_request.mergeable_with_slash_command?(developer, last_diff_sha: mr_sha)).to be_falsey
+ end
+ end
+
+ context 'with running pipeline' do
+ before do
+ create_pipeline(:running)
+ end
+
+ it 'is mergeable' do
+ expect(merge_request.mergeable_with_slash_command?(developer, last_diff_sha: mr_sha)).to be_truthy
+ end
+ end
+ end
+ end
+
describe '#has_commits?' do
before do
allow(subject.merge_request_diff).to receive(:commits_count).
diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb
index 064f29d2d66..3cee2b7714f 100644
--- a/spec/models/milestone_spec.rb
+++ b/spec/models/milestone_spec.rb
@@ -24,7 +24,7 @@ describe Milestone, models: true do
it { is_expected.to have_many(:issues) }
end
- let(:project) { create(:project, :public) }
+ let(:project) { create(:empty_project, :public) }
let(:milestone) { create(:milestone, project: project) }
let(:issue) { create(:issue, project: project) }
let(:user) { create(:user) }
@@ -44,7 +44,7 @@ describe Milestone, models: true do
end
it "accepts the same title in another project" do
- project = build(:project)
+ project = build(:empty_project)
new_milestone = Milestone.new(project: project, title: milestone.title)
expect(new_milestone).to be_valid
@@ -257,7 +257,7 @@ describe Milestone, models: true do
end
it 'supports a cross-project reference' do
- another_project = build(:project, name: 'another-project', namespace: project.namespace)
+ another_project = build(:empty_project, name: 'another-project', namespace: project.namespace)
expect(milestone.to_reference(another_project)).to eq "sample-project%1"
end
end
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index 9fd06bb6b23..7bb1657bc3a 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -4,6 +4,9 @@ describe Namespace, models: true do
let!(:namespace) { create(:namespace) }
it { is_expected.to have_many :projects }
+ it { is_expected.to have_many :project_statistics }
+ it { is_expected.to belong_to :parent }
+ it { is_expected.to have_many :children }
it { is_expected.to validate_presence_of(:name) }
it { is_expected.to validate_uniqueness_of(:name).scoped_to(:parent_id) }
@@ -57,10 +60,54 @@ describe Namespace, models: true do
end
end
+ describe '.with_statistics' do
+ let(:namespace) { create :namespace }
+
+ let(:project1) do
+ create(:empty_project,
+ namespace: namespace,
+ statistics: build(:project_statistics,
+ storage_size: 606,
+ repository_size: 101,
+ lfs_objects_size: 202,
+ build_artifacts_size: 303))
+ end
+
+ let(:project2) do
+ create(:empty_project,
+ namespace: namespace,
+ statistics: build(:project_statistics,
+ storage_size: 60,
+ repository_size: 10,
+ lfs_objects_size: 20,
+ build_artifacts_size: 30))
+ end
+
+ it "sums all project storage counters in the namespace" do
+ project1
+ project2
+ statistics = Namespace.with_statistics.find(namespace.id)
+
+ expect(statistics.storage_size).to eq 666
+ expect(statistics.repository_size).to eq 111
+ expect(statistics.lfs_objects_size).to eq 222
+ expect(statistics.build_artifacts_size).to eq 333
+ end
+
+ it "correctly handles namespaces without projects" do
+ statistics = Namespace.with_statistics.find(namespace.id)
+
+ expect(statistics.storage_size).to eq 0
+ expect(statistics.repository_size).to eq 0
+ expect(statistics.lfs_objects_size).to eq 0
+ expect(statistics.build_artifacts_size).to eq 0
+ end
+ end
+
describe '#move_dir' do
before do
@namespace = create :namespace
- @project = create :project, namespace: @namespace
+ @project = create(:empty_project, namespace: @namespace)
allow(@namespace).to receive(:path_changed?).and_return(true)
end
@@ -72,6 +119,7 @@ describe Namespace, models: true 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)
+ expect(@namespace).to receive(:remove_exports!)
expect(@namespace.move_dir).to be_truthy
end
@@ -91,14 +139,20 @@ describe Namespace, models: true do
end
describe :rm_dir do
- let!(:project) { create(:project, namespace: namespace) }
+ let!(:project) { create(:empty_project, namespace: namespace) }
let!(:path) { File.join(Gitlab.config.repositories.storages.default, namespace.path) }
- before { namespace.destroy }
-
it "removes its dirs when deleted" do
+ namespace.destroy
+
expect(File.exist?(path)).to be(false)
end
+
+ it 'removes the exports folder' do
+ expect(namespace).to receive(:remove_exports!)
+
+ namespace.destroy
+ end
end
describe '.find_by_path_or_name' do
@@ -137,17 +191,38 @@ describe Namespace, models: true do
it { expect(nested_group.full_name).to eq("#{group.name} / #{nested_group.name}") }
end
- describe '#parents' do
+ describe '#ancestors' do
let(:group) { create(:group) }
let(:nested_group) { create(:group, parent: group) }
let(:deep_nested_group) { create(:group, parent: nested_group) }
let(:very_deep_nested_group) { create(:group, parent: deep_nested_group) }
- it 'returns the correct parents' do
- expect(very_deep_nested_group.parents).to eq([group, nested_group, deep_nested_group])
- expect(deep_nested_group.parents).to eq([group, nested_group])
- expect(nested_group.parents).to eq([group])
- expect(group.parents).to eq([])
+ it 'returns the correct ancestors' do
+ expect(very_deep_nested_group.ancestors).to eq([group, nested_group, deep_nested_group])
+ expect(deep_nested_group.ancestors).to eq([group, nested_group])
+ expect(nested_group.ancestors).to eq([group])
+ expect(group.ancestors).to eq([])
+ end
+ end
+
+ describe '#descendants' do
+ let!(:group) { create(:group) }
+ let!(:nested_group) { create(:group, parent: group) }
+ let!(:deep_nested_group) { create(:group, parent: nested_group) }
+ let!(:very_deep_nested_group) { create(:group, parent: deep_nested_group) }
+
+ it 'returns the correct descendants' do
+ expect(very_deep_nested_group.descendants.to_a).to eq([])
+ expect(deep_nested_group.descendants.to_a).to eq([very_deep_nested_group])
+ expect(nested_group.descendants.to_a).to eq([deep_nested_group, very_deep_nested_group])
+ expect(group.descendants.to_a).to eq([nested_group, deep_nested_group, very_deep_nested_group])
+ end
+ end
+
+ describe '#user_ids_for_project_authorizations' do
+ it 'returns the user IDs for which to refresh authorizations' do
+ expect(namespace.user_ids_for_project_authorizations).
+ to eq([namespace.owner_id])
end
end
end
diff --git a/spec/models/network/graph_spec.rb b/spec/models/network/graph_spec.rb
index b76513d2a3c..492c4e01bd8 100644
--- a/spec/models/network/graph_spec.rb
+++ b/spec/models/network/graph_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Network::Graph, models: true do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let!(:note_on_commit) { create(:note_on_commit, project: project) }
it '#initialize' do
diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb
index 310fecd8a5c..1cde9e04951 100644
--- a/spec/models/note_spec.rb
+++ b/spec/models/note_spec.rb
@@ -42,7 +42,7 @@ describe Note, models: true do
context 'when noteable and note project differ' do
subject do
build(:note, noteable: build_stubbed(:issue),
- project: build_stubbed(:project))
+ project: build_stubbed(:empty_project))
end
it { is_expected.to be_invalid }
@@ -52,6 +52,19 @@ describe Note, models: true do
subject { create(:note) }
it { is_expected.to be_valid }
end
+
+ context 'when project is missing for a project related note' do
+ subject { build(:note, project: nil, noteable: build_stubbed(:issue)) }
+ it { is_expected.to be_invalid }
+ end
+
+ context 'when noteable is a personal snippet' do
+ subject { build(:note_on_personal_snippet) }
+
+ it 'is valid without project' do
+ is_expected.to be_valid
+ end
+ end
end
describe "Commit notes" do
@@ -80,8 +93,8 @@ describe Note, models: true do
describe 'authorization' do
before do
- @p1 = create(:project)
- @p2 = create(:project)
+ @p1 = create(:empty_project)
+ @p2 = create(:empty_project)
@u1 = create(:user)
@u2 = create(:user)
@u3 = create(:user)
@@ -125,7 +138,7 @@ describe Note, models: true do
it_behaves_like 'an editable mentionable' do
subject { create :note, noteable: issue, project: issue.project }
- let(:issue) { create :issue }
+ let(:issue) { create(:issue, project: create(:project, :repository)) }
let(:backref_text) { issue.gfm_reference }
let(:set_mentionable_text) { ->(txt) { subject.note = txt } }
end
@@ -139,6 +152,7 @@ describe Note, models: true do
with([{
text: note1.note,
context: {
+ skip_project_check: false,
pipeline: :note,
cache_key: [note1, "note"],
project: note1.project,
@@ -150,6 +164,7 @@ describe Note, models: true do
with([{
text: note2.note,
context: {
+ skip_project_check: false,
pipeline: :note,
cache_key: [note2, "note"],
project: note2.project,
@@ -176,10 +191,10 @@ describe Note, models: true do
describe "cross_reference_not_visible_for?" do
let(:private_user) { create(:user) }
- let(:private_project) { create(:project, namespace: private_user.namespace).tap { |p| p.team << [private_user, :master] } }
+ let(:private_project) { create(:empty_project, namespace: private_user.namespace) { |p| p.team << [private_user, :master] } }
let(:private_issue) { create(:issue, project: private_project) }
- let(:ext_proj) { create(:project, :public) }
+ let(:ext_proj) { create(:empty_project, :public) }
let(:ext_issue) { create(:issue, project: ext_proj) }
let(:note) do
@@ -222,7 +237,7 @@ describe Note, models: true do
describe '#participants' do
it 'includes the note author' do
- project = create(:project, :public)
+ project = create(:empty_project, :public)
issue = create(:issue, project: project)
note = create(:note_on_issue, noteable: issue, project: project)
@@ -306,4 +321,70 @@ describe Note, models: true do
end
end
end
+
+ describe '#for_personal_snippet?' do
+ it 'returns false for a project snippet note' do
+ expect(build(:note_on_project_snippet).for_personal_snippet?).to be_falsy
+ end
+
+ it 'returns true for a personal snippet note' do
+ expect(build(:note_on_personal_snippet).for_personal_snippet?).to be_truthy
+ end
+ end
+
+ describe '#to_ability_name' do
+ it 'returns snippet for a project snippet note' do
+ expect(build(:note_on_project_snippet).to_ability_name).to eq('snippet')
+ end
+
+ it 'returns personal_snippet for a personal snippet note' do
+ expect(build(:note_on_personal_snippet).to_ability_name).to eq('personal_snippet')
+ end
+
+ it 'returns merge_request for an MR note' do
+ expect(build(:note_on_merge_request).to_ability_name).to eq('merge_request')
+ end
+
+ it 'returns issue for an issue note' do
+ expect(build(:note_on_issue).to_ability_name).to eq('issue')
+ end
+
+ it 'returns issue for a commit note' do
+ expect(build(:note_on_commit).to_ability_name).to eq('commit')
+ end
+ end
+
+ describe '#cache_markdown_field' do
+ let(:html) { '<p>some html</p>'}
+
+ context 'note for a project snippet' do
+ let(:note) { build(:note_on_project_snippet) }
+
+ before do
+ expect(Banzai::Renderer).to receive(:cacheless_render_field).
+ with(note, :note, { skip_project_check: false }).and_return(html)
+
+ note.save
+ end
+
+ it 'creates a note' do
+ expect(note.note_html).to eq(html)
+ end
+ end
+
+ context 'note for a personal snippet' do
+ let(:note) { build(:note_on_personal_snippet) }
+
+ before do
+ expect(Banzai::Renderer).to receive(:cacheless_render_field).
+ with(note, :note, { skip_project_check: true }).and_return(html)
+
+ note.save
+ end
+
+ it 'creates a note' do
+ expect(note.note_html).to eq(html)
+ end
+ end
+ end
end
diff --git a/spec/models/pages_domain_spec.rb b/spec/models/pages_domain_spec.rb
new file mode 100644
index 00000000000..e6a4583a8fb
--- /dev/null
+++ b/spec/models/pages_domain_spec.rb
@@ -0,0 +1,168 @@
+require 'spec_helper'
+
+describe PagesDomain, models: true do
+ describe 'associations' do
+ it { is_expected.to belong_to(:project) }
+ end
+
+ describe :validate_domain do
+ subject { build(:pages_domain, domain: domain) }
+
+ context 'is unique' do
+ let(:domain) { 'my.domain.com' }
+
+ it { is_expected.to validate_uniqueness_of(:domain) }
+ end
+
+ context 'valid domain' do
+ let(:domain) { 'my.domain.com' }
+
+ it { is_expected.to be_valid }
+ end
+
+ context 'valid hexadecimal-looking domain' do
+ let(:domain) { '0x12345.com'}
+
+ it { is_expected.to be_valid }
+ end
+
+ context 'no domain' do
+ let(:domain) { nil }
+
+ it { is_expected.not_to be_valid }
+ end
+
+ context 'invalid domain' do
+ let(:domain) { '0123123' }
+
+ it { is_expected.not_to be_valid }
+ end
+
+ context 'domain from .example.com' do
+ let(:domain) { 'my.domain.com' }
+
+ before { allow(Settings.pages).to receive(:host).and_return('domain.com') }
+
+ it { is_expected.not_to be_valid }
+ end
+ end
+
+ describe 'validate certificate' do
+ subject { domain }
+
+ context 'when only certificate is specified' do
+ let(:domain) { build(:pages_domain, :with_certificate) }
+
+ it { is_expected.not_to be_valid }
+ end
+
+ context 'when only key is specified' do
+ let(:domain) { build(:pages_domain, :with_key) }
+
+ it { is_expected.not_to be_valid }
+ end
+
+ context 'with matching key' do
+ let(:domain) { build(:pages_domain, :with_certificate, :with_key) }
+
+ it { is_expected.to be_valid }
+ end
+
+ context 'for not matching key' do
+ let(:domain) { build(:pages_domain, :with_missing_chain, :with_key) }
+
+ it { is_expected.not_to be_valid }
+ end
+ end
+
+ describe :url do
+ subject { domain.url }
+
+ context 'without the certificate' do
+ let(:domain) { build(:pages_domain) }
+
+ it { is_expected.to eq('http://my.domain.com') }
+ end
+
+ context 'with a certificate' do
+ let(:domain) { build(:pages_domain, :with_certificate) }
+
+ it { is_expected.to eq('https://my.domain.com') }
+ end
+ end
+
+ describe :has_matching_key? do
+ subject { domain.has_matching_key? }
+
+ context 'for matching key' do
+ let(:domain) { build(:pages_domain, :with_certificate, :with_key) }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'for invalid key' do
+ let(:domain) { build(:pages_domain, :with_missing_chain, :with_key) }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ describe :has_intermediates? do
+ subject { domain.has_intermediates? }
+
+ context 'for self signed' do
+ let(:domain) { build(:pages_domain, :with_certificate) }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'for missing certificate chain' do
+ let(:domain) { build(:pages_domain, :with_missing_chain) }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'for trusted certificate chain' do
+ # We only validate that we can to rebuild the trust chain, for certificates
+ # We assume that 'AddTrustExternalCARoot' needed to validate the chain is in trusted store.
+ # It will be if ca-certificates is installed on Debian/Ubuntu/Alpine
+
+ let(:domain) { build(:pages_domain, :with_trusted_chain) }
+
+ it { is_expected.to be_truthy }
+ end
+ end
+
+ describe :expired? do
+ subject { domain.expired? }
+
+ context 'for valid' do
+ let(:domain) { build(:pages_domain, :with_certificate) }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'for expired' do
+ let(:domain) { build(:pages_domain, :with_expired_certificate) }
+
+ it { is_expected.to be_truthy }
+ end
+ end
+
+ describe :subject do
+ let(:domain) { build(:pages_domain, :with_certificate) }
+
+ subject { domain.subject }
+
+ it { is_expected.to eq('/CN=test-certificate') }
+ end
+
+ describe :certificate_text do
+ let(:domain) { build(:pages_domain, :with_certificate) }
+
+ subject { domain.certificate_text }
+
+ # We test only existence of output, since the output is long
+ it { is_expected.not_to be_empty }
+ end
+end
diff --git a/spec/models/project_feature_spec.rb b/spec/models/project_feature_spec.rb
index a55d43ab2f9..09a4448d387 100644
--- a/spec/models/project_feature_spec.rb
+++ b/spec/models/project_feature_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe ProjectFeature do
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project) }
let(:user) { create(:user) }
describe '#feature_available?' do
@@ -35,7 +35,7 @@ describe ProjectFeature do
it "returns true when user is a member of project group" do
group = create(:group)
- project = create(:project, namespace: group)
+ project = create(:empty_project, namespace: group)
group.add_developer(user)
features.each do |feature|
@@ -57,7 +57,6 @@ describe ProjectFeature do
context 'when feature is enabled for everyone' do
it "returns true" do
features.each do |feature|
- project.project_feature.update_attribute("#{feature}_access_level".to_sym, ProjectFeature::ENABLED)
expect(project.feature_available?(:issues, user)).to eq(true)
end
end
@@ -104,7 +103,6 @@ describe ProjectFeature do
it "returns true when feature is enabled for everyone" do
features.each do |feature|
- project.project_feature.update_attribute("#{feature}_access_level".to_sym, ProjectFeature::ENABLED)
expect(project.public_send("#{feature}_enabled?")).to eq(true)
end
end
diff --git a/spec/models/project_group_link_spec.rb b/spec/models/project_group_link_spec.rb
index 47397a822c1..59a4ae1b799 100644
--- a/spec/models/project_group_link_spec.rb
+++ b/spec/models/project_group_link_spec.rb
@@ -17,7 +17,7 @@ describe ProjectGroupLink do
describe "destroying a record", truncate: true do
it "refreshes group users' authorized projects" do
- project = create(:project, :private)
+ project = create(:empty_project, :private)
group = create(:group)
reporter = create(:user)
group_users = group.users
diff --git a/spec/models/project_label_spec.rb b/spec/models/project_label_spec.rb
index 4d538cac007..9cdbfa44e5b 100644
--- a/spec/models/project_label_spec.rb
+++ b/spec/models/project_label_spec.rb
@@ -100,7 +100,7 @@ describe ProjectLabel, models: true do
end
context 'cross project reference' do
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project) }
context 'using name' do
it 'returns cross reference with label name' do
diff --git a/spec/models/project_services/asana_service_spec.rb b/spec/models/project_services/asana_service_spec.rb
index 8e5145e824b..48aef3a93f2 100644
--- a/spec/models/project_services/asana_service_spec.rb
+++ b/spec/models/project_services/asana_service_spec.rb
@@ -18,7 +18,7 @@ describe AsanaService, models: true do
describe 'Execute' do
let(:user) { create(:user) }
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project) }
def create_data_for_commits(*messages)
{
diff --git a/spec/models/project_services/assembla_service_spec.rb b/spec/models/project_services/assembla_service_spec.rb
index 4c5acb7990b..96f00af898e 100644
--- a/spec/models/project_services/assembla_service_spec.rb
+++ b/spec/models/project_services/assembla_service_spec.rb
@@ -8,7 +8,7 @@ describe AssemblaService, models: true do
describe "Execute" do
let(:user) { create(:user) }
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
before do
@assembla_service = AssemblaService.new
diff --git a/spec/models/project_services/bamboo_service_spec.rb b/spec/models/project_services/bamboo_service_spec.rb
index d7e1a4e3b6c..497a626a418 100644
--- a/spec/models/project_services/bamboo_service_spec.rb
+++ b/spec/models/project_services/bamboo_service_spec.rb
@@ -1,14 +1,28 @@
require 'spec_helper'
-describe BambooService, models: true do
+describe BambooService, models: true, caching: true do
+ include ReactiveCachingHelpers
+
+ let(:bamboo_url) { 'http://gitlab.com/bamboo' }
+
+ subject(:service) do
+ described_class.create(
+ project: create(:empty_project),
+ properties: {
+ bamboo_url: bamboo_url,
+ username: 'mic',
+ password: 'password',
+ build_key: 'foo'
+ }
+ )
+ end
+
describe 'Associations' do
it { is_expected.to belong_to :project }
it { is_expected.to have_one :service_hook }
end
describe 'Validations' do
- subject { service }
-
context 'when service is active' do
before { subject.active = true }
@@ -103,90 +117,103 @@ describe BambooService, models: true do
end
describe '#build_page' do
- it 'returns a specific URL when status is 500' do
- stub_request(status: 500)
+ it 'returns the contents of the reactive cache' do
+ stub_reactive_cache(service, { build_page: 'foo' }, 'sha', 'ref')
- expect(service.build_page('123', 'unused')).to eq('http://gitlab.com/bamboo/browse/foo')
+ expect(service.build_page('sha', 'ref')).to eq('foo')
end
+ end
- it 'returns a specific URL when response has no results' do
- stub_request(body: %Q({"results":{"results":{"size":"0"}}}))
+ describe '#commit_status' do
+ it 'returns the contents of the reactive cache' do
+ stub_reactive_cache(service, { commit_status: 'foo' }, 'sha', 'ref')
- expect(service.build_page('123', 'unused')).to eq('http://gitlab.com/bamboo/browse/foo')
+ expect(service.commit_status('sha', 'ref')).to eq('foo')
end
+ end
- it 'returns a build URL when bamboo_url has no trailing slash' do
- stub_request(body: %Q({"results":{"results":{"result":{"planResultKey":{"key":"42"}}}}}))
+ describe '#calculate_reactive_cache' do
+ context '#build_page' do
+ subject { service.calculate_reactive_cache('123', 'unused')[:build_page] }
- expect(service(bamboo_url: 'http://gitlab.com/bamboo').build_page('123', 'unused')).to eq('http://gitlab.com/bamboo/browse/42')
- end
+ it 'returns a specific URL when status is 500' do
+ stub_request(status: 500)
- it 'returns a build URL when bamboo_url has a trailing slash' do
- stub_request(body: %Q({"results":{"results":{"result":{"planResultKey":{"key":"42"}}}}}))
+ is_expected.to eq('http://gitlab.com/bamboo/browse/foo')
+ end
- expect(service(bamboo_url: 'http://gitlab.com/bamboo/').build_page('123', 'unused')).to eq('http://gitlab.com/bamboo/browse/42')
- end
- end
+ it 'returns a specific URL when response has no results' do
+ stub_request(body: bamboo_response(size: 0))
- describe '#commit_status' do
- it 'sets commit status to :error when status is 500' do
- stub_request(status: 500)
+ is_expected.to eq('http://gitlab.com/bamboo/browse/foo')
+ end
- expect(service.commit_status('123', 'unused')).to eq(:error)
- end
+ it 'returns a build URL when bamboo_url has no trailing slash' do
+ stub_request(body: bamboo_response)
- it 'sets commit status to "pending" when status is 404' do
- stub_request(status: 404)
+ is_expected.to eq('http://gitlab.com/bamboo/browse/42')
+ end
- expect(service.commit_status('123', 'unused')).to eq('pending')
- end
+ context 'bamboo_url has trailing slash' do
+ let(:bamboo_url) { 'http://gitlab.com/bamboo/' }
- it 'sets commit status to "pending" when response has no results' do
- stub_request(body: %Q({"results":{"results":{"size":"0"}}}))
+ it 'returns a build URL' do
+ stub_request(body: bamboo_response)
- expect(service.commit_status('123', 'unused')).to eq('pending')
+ is_expected.to eq('http://gitlab.com/bamboo/browse/42')
+ end
+ end
end
- it 'sets commit status to "success" when build state contains Success' do
- stub_request(build_state: 'YAY Success!')
+ context '#commit_status' do
+ subject { service.calculate_reactive_cache('123', 'unused')[:commit_status] }
- expect(service.commit_status('123', 'unused')).to eq('success')
- end
+ it 'sets commit status to :error when status is 500' do
+ stub_request(status: 500)
- it 'sets commit status to "failed" when build state contains Failed' do
- stub_request(build_state: 'NO Failed!')
+ is_expected.to eq(:error)
+ end
- expect(service.commit_status('123', 'unused')).to eq('failed')
- end
+ it 'sets commit status to "pending" when status is 404' do
+ stub_request(status: 404)
- it 'sets commit status to "pending" when build state contains Pending' do
- stub_request(build_state: 'NO Pending!')
+ is_expected.to eq('pending')
+ end
- expect(service.commit_status('123', 'unused')).to eq('pending')
- end
+ it 'sets commit status to "pending" when response has no results' do
+ stub_request(body: %Q({"results":{"results":{"size":"0"}}}))
- it 'sets commit status to :error when build state is unknown' do
- stub_request(build_state: 'FOO BAR!')
+ is_expected.to eq('pending')
+ end
- expect(service.commit_status('123', 'unused')).to eq(:error)
- end
- end
+ it 'sets commit status to "success" when build state contains Success' do
+ stub_request(body: bamboo_response(build_state: 'YAY Success!'))
- def service(bamboo_url: 'http://gitlab.com/bamboo')
- described_class.create(
- project: create(:empty_project),
- properties: {
- bamboo_url: bamboo_url,
- username: 'mic',
- password: 'password',
- build_key: 'foo'
- }
- )
+ is_expected.to eq('success')
+ end
+
+ it 'sets commit status to "failed" when build state contains Failed' do
+ stub_request(body: bamboo_response(build_state: 'NO Failed!'))
+
+ is_expected.to eq('failed')
+ end
+
+ it 'sets commit status to "pending" when build state contains Pending' do
+ stub_request(body: bamboo_response(build_state: 'NO Pending!'))
+
+ is_expected.to eq('pending')
+ end
+
+ it 'sets commit status to :error when build state is unknown' do
+ stub_request(body: bamboo_response(build_state: 'FOO BAR!'))
+
+ is_expected.to eq(:error)
+ end
+ end
end
- def stub_request(status: 200, body: nil, build_state: 'success')
+ def stub_request(status: 200, body: nil)
bamboo_full_url = 'http://mic:password@gitlab.com/bamboo/rest/api/latest/result?label=123&os_authType=basic'
- body ||= %Q({"results":{"results":{"result":{"buildState":"#{build_state}"}}}})
WebMock.stub_request(:get, bamboo_full_url).to_return(
status: status,
@@ -194,4 +221,8 @@ describe BambooService, models: true do
body: body
)
end
+
+ def bamboo_response(result_key: 42, build_state: 'success', size: 1)
+ %Q({"results":{"results":{"size":"#{size}","result":{"buildState":"#{build_state}","planResultKey":{"key":"#{result_key}"}}}}})
+ end
end
diff --git a/spec/models/project_services/buildkite_service_spec.rb b/spec/models/project_services/buildkite_service_spec.rb
index 6f65beb79d0..dbd23ff5491 100644
--- a/spec/models/project_services/buildkite_service_spec.rb
+++ b/spec/models/project_services/buildkite_service_spec.rb
@@ -1,6 +1,21 @@
require 'spec_helper'
-describe BuildkiteService, models: true do
+describe BuildkiteService, models: true, caching: true do
+ include ReactiveCachingHelpers
+
+ let(:project) { create(:empty_project) }
+
+ subject(:service) do
+ described_class.create(
+ project: project,
+ properties: {
+ service_hook: true,
+ project_url: 'https://buildkite.com/account-name/example-project',
+ token: 'secret-sauce-webhook-token:secret-sauce-status-token'
+ }
+ )
+ end
+
describe 'Associations' do
it { is_expected.to belong_to :project }
it { is_expected.to have_one :service_hook }
@@ -25,21 +40,12 @@ describe BuildkiteService, models: true do
describe 'commits methods' do
before do
- @project = Project.new
- allow(@project).to receive(:default_branch).and_return('default-brancho')
-
- @service = BuildkiteService.new
- allow(@service).to receive_messages(
- project: @project,
- service_hook: true,
- project_url: 'https://buildkite.com/account-name/example-project',
- token: 'secret-sauce-webhook-token:secret-sauce-status-token'
- )
+ allow(project).to receive(:default_branch).and_return('default-brancho')
end
describe '#webhook_url' do
it 'returns the webhook url' do
- expect(@service.webhook_url).to eq(
+ expect(service.webhook_url).to eq(
'https://webhook.buildkite.com/deliver/secret-sauce-webhook-token'
)
end
@@ -47,7 +53,7 @@ describe BuildkiteService, models: true do
describe '#commit_status_path' do
it 'returns the correct status page' do
- expect(@service.commit_status_path('2ab7834c')).to eq(
+ expect(service.commit_status_path('2ab7834c')).to eq(
'https://gitlab.buildkite.com/status/secret-sauce-status-token.json?commit=2ab7834c'
)
end
@@ -55,10 +61,53 @@ describe BuildkiteService, models: true do
describe '#build_page' do
it 'returns the correct build page' do
- expect(@service.build_page('2ab7834c', nil)).to eq(
+ expect(service.build_page('2ab7834c', nil)).to eq(
'https://buildkite.com/account-name/example-project/builds?commit=2ab7834c'
)
end
end
+
+ describe '#commit_status' do
+ it 'returns the contents of the reactive cache' do
+ stub_reactive_cache(service, { commit_status: 'foo' }, 'sha', 'ref')
+
+ expect(service.commit_status('sha', 'ref')).to eq('foo')
+ end
+ end
+
+ describe '#calculate_reactive_cache' do
+ context '#commit_status' do
+ subject { service.calculate_reactive_cache('123', 'unused')[:commit_status] }
+
+ it 'sets commit status to :error when status is 500' do
+ stub_request(status: 500)
+
+ is_expected.to eq(:error)
+ end
+
+ it 'sets commit status to :error when status is 404' do
+ stub_request(status: 404)
+
+ is_expected.to eq(:error)
+ end
+
+ it 'passes through build status untouched when status is 200' do
+ stub_request(body: %Q({"status":"Great Success"}))
+
+ is_expected.to eq('Great Success')
+ end
+ end
+ end
+ end
+
+ def stub_request(status: 200, body: nil)
+ body ||= %Q({"status":"success"})
+ buildkite_full_url = 'https://gitlab.buildkite.com/status/secret-sauce-status-token.json?commit=123'
+
+ WebMock.stub_request(:get, buildkite_full_url).to_return(
+ status: status,
+ headers: { 'Content-Type' => 'application/json' },
+ body: body
+ )
end
end
diff --git a/spec/models/project_services/campfire_service_spec.rb b/spec/models/project_services/campfire_service_spec.rb
index a3b9d084a75..953e664fb66 100644
--- a/spec/models/project_services/campfire_service_spec.rb
+++ b/spec/models/project_services/campfire_service_spec.rb
@@ -22,7 +22,7 @@ describe CampfireService, models: true do
describe "#execute" do
let(:user) { create(:user) }
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
before do
@campfire_service = CampfireService.new
diff --git a/spec/models/project_services/drone_ci_service_spec.rb b/spec/models/project_services/drone_ci_service_spec.rb
index f13bb1e8adf..f9307d6de7b 100644
--- a/spec/models/project_services/drone_ci_service_spec.rb
+++ b/spec/models/project_services/drone_ci_service_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
-describe DroneCiService, models: true do
+describe DroneCiService, models: true, caching: true do
+ include ReactiveCachingHelpers
+
describe 'associations' do
it { is_expected.to belong_to(:project) }
it { is_expected.to have_one(:service_hook) }
@@ -25,7 +27,7 @@ describe DroneCiService, models: true do
shared_context :drone_ci_service do
let(:drone) { DroneCiService.new }
- let(:project) { create(:project, name: 'project') }
+ let(:project) { create(:project, :repository, name: 'project') }
let(:path) { "#{project.namespace.path}/#{project.path}" }
let(:drone_url) { 'http://drone.example.com' }
let(:sha) { '2ab7834c' }
@@ -33,6 +35,10 @@ describe DroneCiService, models: true do
let(:token) { 'secret' }
let(:iid) { rand(1..9999) }
+ # URL's
+ let(:build_page) { "#{drone_url}/gitlab/#{path}/redirect/commits/#{sha}?branch=#{branch}" }
+ let(:commit_status_path) { "#{drone_url}/gitlab/#{path}/commits/#{sha}?branch=#{branch}&access_token=#{token}" }
+
before(:each) do
allow(drone).to receive_messages(
project_id: project.id,
@@ -42,22 +48,66 @@ describe DroneCiService, models: true do
token: token
)
end
+
+ def stub_request(status: 200, body: nil)
+ body ||= %Q({"status":"success"})
+
+ WebMock.stub_request(:get, commit_status_path).to_return(
+ status: status,
+ headers: { 'Content-Type' => 'application/json' },
+ body: body
+ )
+ end
end
describe "service page/path methods" do
include_context :drone_ci_service
- # URL's
- let(:commit_page) { "#{drone_url}/gitlab/#{path}/redirect/commits/#{sha}?branch=#{branch}" }
- let(:merge_request_page) { "#{drone_url}/gitlab/#{path}/redirect/pulls/#{iid}" }
- let(:commit_status_path) { "#{drone_url}/gitlab/#{path}/commits/#{sha}?branch=#{branch}&access_token=#{token}" }
- let(:merge_request_status_path) { "#{drone_url}/gitlab/#{path}/pulls/#{iid}?access_token=#{token}" }
-
- it { expect(drone.build_page(sha, branch)).to eq(commit_page) }
- it { expect(drone.commit_page(sha, branch)).to eq(commit_page) }
- it { expect(drone.merge_request_page(iid, sha, branch)).to eq(merge_request_page) }
+ it { expect(drone.build_page(sha, branch)).to eq(build_page) }
it { expect(drone.commit_status_path(sha, branch)).to eq(commit_status_path) }
- it { expect(drone.merge_request_status_path(iid, sha, branch)).to eq(merge_request_status_path) }
+ end
+
+ describe '#commit_status' do
+ include_context :drone_ci_service
+
+ it 'returns the contents of the reactive cache' do
+ stub_reactive_cache(drone, { commit_status: 'foo' }, 'sha', 'ref')
+
+ expect(drone.commit_status('sha', 'ref')).to eq('foo')
+ end
+ end
+
+ describe '#calculate_reactive_cache' do
+ include_context :drone_ci_service
+
+ context '#commit_status' do
+ subject { drone.calculate_reactive_cache(sha, branch)[:commit_status] }
+
+ it 'sets commit status to :error when status is 500' do
+ stub_request(status: 500)
+
+ is_expected.to eq(:error)
+ end
+
+ it 'sets commit status to :error when status is 404' do
+ stub_request(status: 404)
+
+ is_expected.to eq(:error)
+ end
+
+ { "killed" => :canceled,
+ "failure" => :failed,
+ "error" => :failed,
+ "success" => "success",
+ }.each do |drone_status, our_status|
+
+ it "sets commit status to #{our_status.inspect} when returned status is #{drone_status.inspect}" do
+ stub_request(body: %Q({"status":"#{drone_status}"}))
+
+ is_expected.to eq(our_status)
+ end
+ end
+ end
end
describe "execute" do
diff --git a/spec/models/project_services/external_wiki_service_spec.rb b/spec/models/project_services/external_wiki_service_spec.rb
index 342d86aeca9..bdeea1db1e3 100644
--- a/spec/models/project_services/external_wiki_service_spec.rb
+++ b/spec/models/project_services/external_wiki_service_spec.rb
@@ -23,7 +23,7 @@ describe ExternalWikiService, models: true do
end
describe 'External wiki' do
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project) }
context 'when it is active' do
before do
diff --git a/spec/models/project_services/flowdock_service_spec.rb b/spec/models/project_services/flowdock_service_spec.rb
index d6db02d6e76..a97e8c6e4ce 100644
--- a/spec/models/project_services/flowdock_service_spec.rb
+++ b/spec/models/project_services/flowdock_service_spec.rb
@@ -22,7 +22,7 @@ describe FlowdockService, models: true do
describe "Execute" do
let(:user) { create(:user) }
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
before do
@flowdock_service = FlowdockService.new
diff --git a/spec/models/project_services/gemnasium_service_spec.rb b/spec/models/project_services/gemnasium_service_spec.rb
index 529044d1d8b..a13fbae03eb 100644
--- a/spec/models/project_services/gemnasium_service_spec.rb
+++ b/spec/models/project_services/gemnasium_service_spec.rb
@@ -24,7 +24,7 @@ describe GemnasiumService, models: true do
describe "Execute" do
let(:user) { create(:user) }
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
before do
@gemnasium_service = GemnasiumService.new
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 9b80f0e7296..dcb70ee28a8 100644
--- a/spec/models/project_services/gitlab_issue_tracker_service_spec.rb
+++ b/spec/models/project_services/gitlab_issue_tracker_service_spec.rb
@@ -8,21 +8,21 @@ describe GitlabIssueTrackerService, models: true do
describe 'Validations' do
context 'when service is active' do
- subject { described_class.new(project: create(:project), active: true) }
+ subject { described_class.new(project: create(:empty_project), active: true) }
it { is_expected.to validate_presence_of(:issues_url) }
it_behaves_like 'issue tracker service URL attribute', :issues_url
end
context 'when service is inactive' do
- subject { described_class.new(project: create(:project), active: false) }
+ subject { described_class.new(project: create(:empty_project), active: false) }
it { is_expected.not_to validate_presence_of(:issues_url) }
end
end
describe 'project and issue urls' do
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project) }
context 'with absolute urls' do
before do
diff --git a/spec/models/project_services/hipchat_service_spec.rb b/spec/models/project_services/hipchat_service_spec.rb
index 2da3a9cb09f..bf422ac7ce1 100644
--- a/spec/models/project_services/hipchat_service_spec.rb
+++ b/spec/models/project_services/hipchat_service_spec.rb
@@ -22,8 +22,8 @@ describe HipchatService, models: true do
describe "Execute" do
let(:hipchat) { HipchatService.new }
- let(:user) { create(:user, username: 'username') }
- let(:project) { create(:project, name: 'project') }
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :repository) }
let(:api_url) { 'https://hipchat.example.com/v2/room/123456/notification?auth_token=verySecret' }
let(:project_name) { project.name_with_namespace.gsub(/\s/, '') }
let(:token) { 'verySecret' }
@@ -165,7 +165,7 @@ describe HipchatService, models: true do
context "Note events" do
let(:user) { create(:user) }
- let(:project) { create(:project, creator_id: user.id) }
+ let(:project) { create(:project, :repository, creator: user) }
context 'when commit comment event triggered' do
let(:commit_note) do
diff --git a/spec/models/project_services/irker_service_spec.rb b/spec/models/project_services/irker_service_spec.rb
index f8c45b37561..b9fb6f3f6f4 100644
--- a/spec/models/project_services/irker_service_spec.rb
+++ b/spec/models/project_services/irker_service_spec.rb
@@ -25,7 +25,7 @@ describe IrkerService, models: true do
describe 'Execute' do
let(:irker) { IrkerService.new }
let(:user) { create(:user) }
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:sample_data) do
Gitlab::DataBuilder::Push.build_sample(project, user)
end
diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb
index 862e3a72a73..2f6b159d76e 100644
--- a/spec/models/project_services/jira_service_spec.rb
+++ b/spec/models/project_services/jira_service_spec.rb
@@ -71,7 +71,7 @@ describe JiraService, models: true do
describe '#close_issue' do
let(:custom_base_url) { 'http://custom_url' }
let(:user) { create(:user) }
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project) }
let(:merge_request) { create(:merge_request) }
before do
@@ -207,12 +207,12 @@ describe JiraService, models: true do
end
describe "Stored password invalidation" do
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project) }
context "when a password was previously set" do
before do
@jira_service = JiraService.create!(
- project: create(:project),
+ project: project,
properties: {
url: 'http://jira.example.com/rest/api/2',
username: 'mic',
@@ -252,7 +252,7 @@ describe JiraService, models: true do
context "when no password was previously set" do
before do
@jira_service = JiraService.create(
- project: create(:project),
+ project: project,
properties: {
url: 'http://jira.example.com/rest/api/2',
username: 'mic'
@@ -281,7 +281,7 @@ describe JiraService, models: true do
end
describe 'description and title' do
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project) }
context 'when it is not set' do
before do
@@ -316,7 +316,7 @@ describe JiraService, models: true do
end
describe 'project and issue urls' do
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project) }
context 'when gitlab.yml was initialized' do
before do
diff --git a/spec/models/project_services/kubernetes_service_spec.rb b/spec/models/project_services/kubernetes_service_spec.rb
index 4f3cd14e941..9052479d35e 100644
--- a/spec/models/project_services/kubernetes_service_spec.rb
+++ b/spec/models/project_services/kubernetes_service_spec.rb
@@ -181,11 +181,23 @@ describe KubernetesService, models: true, caching: true do
let(:pod) { kube_pod(app: environment.slug) }
let(:terminals) { kube_terminals(service, pod) }
- it 'returns terminals' do
- stub_reactive_cache(service, pods: [ pod, pod, kube_pod(app: "should-be-filtered-out") ])
+ before do
+ stub_reactive_cache(
+ service,
+ pods: [ pod, pod, kube_pod(app: "should-be-filtered-out") ]
+ )
+ end
+ it 'returns terminals' do
is_expected.to eq(terminals + terminals)
end
+
+ it 'uses max session time from settings' do
+ stub_application_setting(terminal_max_session_time: 600)
+
+ times = subject.map { |terminal| terminal[:max_session_time] }
+ expect(times).to eq [600, 600, 600, 600]
+ end
end
end
diff --git a/spec/models/project_services/mattermost_notification_service_spec.rb b/spec/models/project_services/mattermost_notification_service_spec.rb
deleted file mode 100644
index 7832d6f50cf..00000000000
--- a/spec/models/project_services/mattermost_notification_service_spec.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-require 'spec_helper'
-
-describe MattermostNotificationService, models: true do
- it_behaves_like "slack or mattermost notifications"
-end
diff --git a/spec/models/project_services/mattermost_service_spec.rb b/spec/models/project_services/mattermost_service_spec.rb
new file mode 100644
index 00000000000..490d6aedffc
--- /dev/null
+++ b/spec/models/project_services/mattermost_service_spec.rb
@@ -0,0 +1,5 @@
+require 'spec_helper'
+
+describe MattermostService, models: true do
+ it_behaves_like "slack or mattermost notifications"
+end
diff --git a/spec/models/project_services/mattermost_slash_commands_service_spec.rb b/spec/models/project_services/mattermost_slash_commands_service_spec.rb
index 1ae1483e2a4..98f3d420c8a 100644
--- a/spec/models/project_services/mattermost_slash_commands_service_spec.rb
+++ b/spec/models/project_services/mattermost_slash_commands_service_spec.rb
@@ -2,4 +2,120 @@ require 'spec_helper'
describe MattermostSlashCommandsService, :models do
it_behaves_like "chat slash commands service"
+
+ context 'Mattermost API' do
+ let(:project) { create(:empty_project) }
+ let(:service) { project.build_mattermost_slash_commands_service }
+ let(:user) { create(:user) }
+
+ before do
+ Mattermost::Session.base_uri("http://mattermost.example.com")
+
+ allow_any_instance_of(Mattermost::Client).to receive(:with_session).
+ and_yield(Mattermost::Session.new(nil))
+ end
+
+ describe '#configure' do
+ subject do
+ service.configure(user, team_id: 'abc',
+ trigger: 'gitlab', url: 'http://trigger.url',
+ icon_url: 'http://icon.url/icon.png')
+ end
+
+ context 'the requests succeeds' do
+ before do
+ stub_request(:post, 'http://mattermost.example.com/api/v3/teams/abc/commands/create').
+ with(body: {
+ team_id: 'abc',
+ trigger: 'gitlab',
+ url: 'http://trigger.url',
+ icon_url: 'http://icon.url/icon.png',
+ auto_complete: true,
+ auto_complete_desc: "Perform common operations on: #{project.name_with_namespace}",
+ auto_complete_hint: '[help]',
+ description: "Perform common operations on: #{project.name_with_namespace}",
+ display_name: "GitLab / #{project.name_with_namespace}",
+ method: 'P',
+ username: 'GitLab' }.to_json).
+ to_return(
+ status: 200,
+ headers: { 'Content-Type' => 'application/json' },
+ body: { token: 'token' }.to_json
+ )
+ end
+
+ it 'saves the service' do
+ expect { subject }.to change { project.services.count }.by(1)
+ end
+
+ it 'saves the token' do
+ subject
+
+ expect(service.reload.token).to eq('token')
+ end
+ end
+
+ context 'an error is received' do
+ before do
+ stub_request(:post, 'http://mattermost.example.com/api/v3/teams/abc/commands/create').
+ to_return(
+ status: 500,
+ headers: { 'Content-Type' => 'application/json' },
+ body: {
+ id: 'api.command.duplicate_trigger.app_error',
+ message: 'This trigger word is already in use. Please choose another word.',
+ detailed_error: '',
+ request_id: 'obc374man7bx5r3dbc1q5qhf3r',
+ status_code: 500
+ }.to_json
+ )
+ end
+
+ it 'shows error messages' do
+ succeeded, message = subject
+
+ expect(succeeded).to be(false)
+ expect(message).to eq('This trigger word is already in use. Please choose another word.')
+ end
+ end
+ end
+
+ describe '#list_teams' do
+ subject do
+ service.list_teams(user)
+ end
+
+ context 'the requests succeeds' do
+ before do
+ stub_request(:get, 'http://mattermost.example.com/api/v3/teams/all').
+ to_return(
+ status: 200,
+ headers: { 'Content-Type' => 'application/json' },
+ body: ['list'].to_json
+ )
+ end
+
+ it 'returns a list of teams' do
+ expect(subject).not_to be_empty
+ end
+ end
+
+ context 'an error is received' do
+ before do
+ stub_request(:get, 'http://mattermost.example.com/api/v3/teams/all').
+ to_return(
+ status: 500,
+ headers: { 'Content-Type' => 'application/json' },
+ body: {
+ message: 'Failed to get team list.'
+ }.to_json
+ )
+ end
+
+ it 'shows error messages' do
+ expect(subject).to eq([[], "Failed to get team list."])
+ end
+ end
+ end
+ end
end
diff --git a/spec/models/project_services/pipeline_email_service_spec.rb b/spec/models/project_services/pipeline_email_service_spec.rb
index 7c8824485f5..03932895b0e 100644
--- a/spec/models/project_services/pipeline_email_service_spec.rb
+++ b/spec/models/project_services/pipeline_email_service_spec.rb
@@ -7,7 +7,7 @@ describe PipelinesEmailService do
create(:ci_pipeline, project: project, sha: project.commit('master').sha)
end
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:recipient) { 'test@gitlab.com' }
let(:data) do
diff --git a/spec/models/project_services/pushover_service_spec.rb b/spec/models/project_services/pushover_service_spec.rb
index 8fc92a9ab51..a7e7594a7d5 100644
--- a/spec/models/project_services/pushover_service_spec.rb
+++ b/spec/models/project_services/pushover_service_spec.rb
@@ -27,7 +27,7 @@ describe PushoverService, models: true do
describe 'Execute' do
let(:pushover) { PushoverService.new }
let(:user) { create(:user) }
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:sample_data) do
Gitlab::DataBuilder::Push.build_sample(project, user)
end
diff --git a/spec/models/project_services/slack_notification_service_spec.rb b/spec/models/project_services/slack_notification_service_spec.rb
deleted file mode 100644
index 110b5bf2115..00000000000
--- a/spec/models/project_services/slack_notification_service_spec.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-require 'spec_helper'
-
-describe SlackNotificationService, models: true do
- it_behaves_like "slack or mattermost notifications"
-end
diff --git a/spec/models/project_services/slack_service_spec.rb b/spec/models/project_services/slack_service_spec.rb
new file mode 100644
index 00000000000..9a3ecc66d83
--- /dev/null
+++ b/spec/models/project_services/slack_service_spec.rb
@@ -0,0 +1,5 @@
+require 'spec_helper'
+
+describe SlackService, models: true do
+ it_behaves_like "slack or mattermost notifications"
+end
diff --git a/spec/models/project_services/slack_slash_commands_service.rb b/spec/models/project_services/slack_slash_commands_service.rb
deleted file mode 100644
index 5775e439906..00000000000
--- a/spec/models/project_services/slack_slash_commands_service.rb
+++ /dev/null
@@ -1,40 +0,0 @@
-require 'spec_helper'
-
-describe SlackSlashCommandsService, :models do
- it_behaves_like "chat slash commands service"
-
- describe '#trigger' do
- context 'when an auth url is generated' do
- let(:project) { create(:empty_project) }
- let(:params) do
- {
- team_domain: 'http://domain.tld',
- team_id: 'T3423423',
- user_id: 'U234234',
- user_name: 'mepmep',
- token: 'token'
- }
- end
-
- let(:service) do
- project.create_slack_slash_commands_service(
- properties: { token: 'token' }
- )
- end
-
- let(:authorize_url) do
- 'http://authorize.example.com/'
- end
-
- before do
- allow(service).to receive(:authorize_chat_name_url).and_return(authorize_url)
- end
-
- it 'uses slack compatible links' do
- response = service.trigger(params)
-
- expect(response[:text]).to include("<#{authorize_url}|connect your GitLab account>")
- end
- end
- end
-end
diff --git a/spec/models/project_services/slack_slash_commands_service_spec.rb b/spec/models/project_services/slack_slash_commands_service_spec.rb
new file mode 100644
index 00000000000..5766aa340e2
--- /dev/null
+++ b/spec/models/project_services/slack_slash_commands_service_spec.rb
@@ -0,0 +1,41 @@
+require 'spec_helper'
+
+describe SlackSlashCommandsService, :models do
+ it_behaves_like "chat slash commands service"
+
+ describe '#trigger' do
+ context 'when an auth url is generated' do
+ let(:project) { create(:empty_project) }
+ let(:params) do
+ {
+ team_domain: 'http://domain.tld',
+ team_id: 'T3423423',
+ user_id: 'U234234',
+ user_name: 'mepmep',
+ token: 'token'
+ }
+ end
+
+ let(:service) do
+ project.create_slack_slash_commands_service(
+ properties: { token: 'token' },
+ active: true
+ )
+ end
+
+ let(:authorize_url) do
+ 'http://authorize.example.com/'
+ end
+
+ before do
+ allow(service).to receive(:authorize_chat_name_url).and_return(authorize_url)
+ end
+
+ it 'uses slack compatible links' do
+ response = service.trigger(params)
+
+ expect(response[:text]).to include("<#{authorize_url}|connect your GitLab account>")
+ end
+ end
+ end
+end
diff --git a/spec/models/project_services/teamcity_service_spec.rb b/spec/models/project_services/teamcity_service_spec.rb
index f7e878844dc..a1edd083aa1 100644
--- a/spec/models/project_services/teamcity_service_spec.rb
+++ b/spec/models/project_services/teamcity_service_spec.rb
@@ -1,14 +1,28 @@
require 'spec_helper'
-describe TeamcityService, models: true do
+describe TeamcityService, models: true, caching: true do
+ include ReactiveCachingHelpers
+
+ let(:teamcity_url) { 'http://gitlab.com/teamcity' }
+
+ subject(:service) do
+ described_class.create(
+ project: create(:empty_project),
+ properties: {
+ teamcity_url: teamcity_url,
+ username: 'mic',
+ password: 'password',
+ build_type: 'foo'
+ }
+ )
+ end
+
describe 'Associations' do
it { is_expected.to belong_to :project }
it { is_expected.to have_one :service_hook }
end
describe 'Validations' do
- subject { service }
-
context 'when service is active' do
before { subject.active = true }
@@ -103,73 +117,87 @@ describe TeamcityService, models: true do
end
describe '#build_page' do
- it 'returns a specific URL when status is 500' do
- stub_request(status: 500)
+ it 'returns the contents of the reactive cache' do
+ stub_reactive_cache(service, { build_page: 'foo' }, 'sha', 'ref')
- expect(service.build_page('123', 'unused')).to eq('http://gitlab.com/teamcity/viewLog.html?buildTypeId=foo')
+ expect(service.build_page('sha', 'ref')).to eq('foo')
end
+ end
- it 'returns a build URL when teamcity_url has no trailing slash' do
- stub_request(body: %Q({"build":{"id":"666"}}))
+ describe '#commit_status' do
+ it 'returns the contents of the reactive cache' do
+ stub_reactive_cache(service, { commit_status: 'foo' }, 'sha', 'ref')
- expect(service(teamcity_url: 'http://gitlab.com/teamcity').build_page('123', 'unused')).to eq('http://gitlab.com/teamcity/viewLog.html?buildId=666&buildTypeId=foo')
+ expect(service.commit_status('sha', 'ref')).to eq('foo')
end
+ end
- it 'returns a build URL when teamcity_url has a trailing slash' do
- stub_request(body: %Q({"build":{"id":"666"}}))
+ describe '#calculate_reactive_cache' do
+ context 'build_page' do
+ subject { service.calculate_reactive_cache('123', 'unused')[:build_page] }
- expect(service(teamcity_url: 'http://gitlab.com/teamcity/').build_page('123', 'unused')).to eq('http://gitlab.com/teamcity/viewLog.html?buildId=666&buildTypeId=foo')
- end
- end
+ it 'returns a specific URL when status is 500' do
+ stub_request(status: 500)
- describe '#commit_status' do
- it 'sets commit status to :error when status is 500' do
- stub_request(status: 500)
+ is_expected.to eq('http://gitlab.com/teamcity/viewLog.html?buildTypeId=foo')
+ end
- expect(service.commit_status('123', 'unused')).to eq(:error)
- end
+ it 'returns a build URL when teamcity_url has no trailing slash' do
+ stub_request(body: %Q({"build":{"id":"666"}}))
- it 'sets commit status to "pending" when status is 404' do
- stub_request(status: 404)
+ is_expected.to eq('http://gitlab.com/teamcity/viewLog.html?buildId=666&buildTypeId=foo')
+ end
- expect(service.commit_status('123', 'unused')).to eq('pending')
- end
+ context 'teamcity_url has trailing slash' do
+ let(:teamcity_url) { 'http://gitlab.com/teamcity/' }
- it 'sets commit status to "success" when build status contains SUCCESS' do
- stub_request(build_status: 'YAY SUCCESS!')
+ it 'returns a build URL' do
+ stub_request(body: %Q({"build":{"id":"666"}}))
- expect(service.commit_status('123', 'unused')).to eq('success')
+ is_expected.to eq('http://gitlab.com/teamcity/viewLog.html?buildId=666&buildTypeId=foo')
+ end
+ end
end
- it 'sets commit status to "failed" when build status contains FAILURE' do
- stub_request(build_status: 'NO FAILURE!')
+ context 'commit_status' do
+ subject { service.calculate_reactive_cache('123', 'unused')[:commit_status] }
- expect(service.commit_status('123', 'unused')).to eq('failed')
- end
+ it 'sets commit status to :error when status is 500' do
+ stub_request(status: 500)
- it 'sets commit status to "pending" when build status contains Pending' do
- stub_request(build_status: 'NO Pending!')
+ is_expected.to eq(:error)
+ end
- expect(service.commit_status('123', 'unused')).to eq('pending')
- end
+ it 'sets commit status to "pending" when status is 404' do
+ stub_request(status: 404)
- it 'sets commit status to :error when build status is unknown' do
- stub_request(build_status: 'FOO BAR!')
+ is_expected.to eq('pending')
+ end
- expect(service.commit_status('123', 'unused')).to eq(:error)
- end
- end
+ it 'sets commit status to "success" when build status contains SUCCESS' do
+ stub_request(build_status: 'YAY SUCCESS!')
- def service(teamcity_url: 'http://gitlab.com/teamcity')
- described_class.create(
- project: create(:empty_project),
- properties: {
- teamcity_url: teamcity_url,
- username: 'mic',
- password: 'password',
- build_type: 'foo'
- }
- )
+ is_expected.to eq('success')
+ end
+
+ it 'sets commit status to "failed" when build status contains FAILURE' do
+ stub_request(build_status: 'NO FAILURE!')
+
+ is_expected.to eq('failed')
+ end
+
+ it 'sets commit status to "pending" when build status contains Pending' do
+ stub_request(build_status: 'NO Pending!')
+
+ is_expected.to eq('pending')
+ end
+
+ it 'sets commit status to :error when build status is unknown' do
+ stub_request(build_status: 'FOO BAR!')
+
+ is_expected.to eq(:error)
+ end
+ end
end
def stub_request(status: 200, body: nil, build_status: 'success')
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 0455cd2fe49..2129bcbd74d 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -21,8 +21,8 @@ describe Project, models: true do
it { is_expected.to have_many(:hooks).dependent(:destroy) }
it { is_expected.to have_many(:protected_branches).dependent(:destroy) }
it { is_expected.to have_one(:forked_project_link).dependent(:destroy) }
- it { is_expected.to have_one(:slack_notification_service).dependent(:destroy) }
- it { is_expected.to have_one(:mattermost_notification_service).dependent(:destroy) }
+ it { is_expected.to have_one(:slack_service).dependent(:destroy) }
+ it { is_expected.to have_one(:mattermost_service).dependent(:destroy) }
it { is_expected.to have_one(:pushover_service).dependent(:destroy) }
it { is_expected.to have_one(:asana_service).dependent(:destroy) }
it { is_expected.to have_many(:boards).dependent(:destroy) }
@@ -49,6 +49,7 @@ describe Project, models: true do
it { is_expected.to have_one(:gitlab_issue_tracker_service).dependent(:destroy) }
it { is_expected.to have_one(:external_wiki_service).dependent(:destroy) }
it { is_expected.to have_one(:project_feature).dependent(:destroy) }
+ it { is_expected.to have_one(:statistics).class_name('ProjectStatistics').dependent(:delete) }
it { is_expected.to have_one(:import_data).class_name('ProjectImportData').dependent(:destroy) }
it { is_expected.to have_one(:last_event).class_name('Event') }
it { is_expected.to have_one(:forked_from_project).through(:forked_project_link) }
@@ -59,6 +60,7 @@ describe Project, models: true do
it { is_expected.to have_many(:runners) }
it { is_expected.to have_many(:variables) }
it { is_expected.to have_many(:triggers) }
+ it { is_expected.to have_many(:pages_domains) }
it { is_expected.to have_many(:labels).class_name('ProjectLabel').dependent(:destroy) }
it { is_expected.to have_many(:users_star_projects).dependent(:destroy) }
it { is_expected.to have_many(:environments).dependent(:destroy) }
@@ -72,9 +74,7 @@ describe Project, models: true do
context 'after initialized' do
it "has a project_feature" do
- project = FactoryGirl.build(:project)
-
- expect(project.project_feature.present?).to be_present
+ expect(Project.new.project_feature).to be_present
end
end
@@ -128,7 +128,7 @@ describe Project, models: true do
end
describe 'validation' do
- let!(:project) { create(:project) }
+ let!(:project) { create(:empty_project) }
it { is_expected.to validate_presence_of(:name) }
it { is_expected.to validate_uniqueness_of(:name).scoped_to(:namespace_id) }
@@ -147,7 +147,7 @@ describe Project, models: true do
it { is_expected.to validate_presence_of(:repository_storage) }
it 'does not allow new projects beyond user limits' do
- project2 = build(:project)
+ project2 = build(:empty_project)
allow(project2).to receive(:creator).and_return(double(can_create_project?: false, projects_limit: 0).as_null_object)
expect(project2).not_to be_valid
expect(project2.errors[:limit_reached].first).to match(/Personal project creation is not allowed/)
@@ -156,7 +156,7 @@ describe Project, models: true do
describe 'wiki path conflict' do
context "when the new path has been used by the wiki of other Project" do
it 'has an error on the name attribute' do
- new_project = build_stubbed(:project, namespace_id: project.namespace_id, path: "#{project.path}.wiki")
+ new_project = build_stubbed(:empty_project, namespace_id: project.namespace_id, path: "#{project.path}.wiki")
expect(new_project).not_to be_valid
expect(new_project.errors[:name].first).to eq('has already been taken')
@@ -165,8 +165,8 @@ describe Project, models: true do
context "when the new wiki path has been used by the path of other Project" do
it 'has an error on the name attribute' do
- project_with_wiki_suffix = create(:project, path: 'foo.wiki')
- new_project = build_stubbed(:project, namespace_id: project_with_wiki_suffix.namespace_id, path: 'foo')
+ project_with_wiki_suffix = create(:empty_project, path: 'foo.wiki')
+ new_project = build_stubbed(:empty_project, namespace_id: project_with_wiki_suffix.namespace_id, path: 'foo')
expect(new_project).not_to be_valid
expect(new_project.errors[:name].first).to eq('has already been taken')
@@ -175,7 +175,7 @@ describe Project, models: true do
end
context 'repository storages inclussion' do
- let(:project2) { build(:project, repository_storage: 'missing') }
+ let(:project2) { build(:empty_project, repository_storage: 'missing') }
before do
storages = { 'custom' => 'tmp/tests/custom_repositories' }
@@ -189,34 +189,54 @@ describe Project, models: true do
end
it 'does not allow an invalid URI as import_url' do
- project2 = build(:project, import_url: 'invalid://')
+ project2 = build(:empty_project, import_url: 'invalid://')
expect(project2).not_to be_valid
end
it 'does allow a valid URI as import_url' do
- project2 = build(:project, import_url: 'ssh://test@gitlab.com/project.git')
+ project2 = build(:empty_project, import_url: 'ssh://test@gitlab.com/project.git')
expect(project2).to be_valid
end
it 'allows an empty URI' do
- project2 = build(:project, import_url: '')
+ project2 = build(:empty_project, import_url: '')
expect(project2).to be_valid
end
it 'does not produce import data on an empty URI' do
- project2 = build(:project, import_url: '')
+ project2 = build(:empty_project, import_url: '')
expect(project2.import_data).to be_nil
end
it 'does not produce import data on an invalid URI' do
- project2 = build(:project, import_url: 'test://')
+ project2 = build(:empty_project, import_url: 'test://')
expect(project2.import_data).to be_nil
end
+
+ describe 'project pending deletion' do
+ let!(:project_pending_deletion) do
+ create(:empty_project,
+ pending_delete: true)
+ end
+ let(:new_project) do
+ build(:empty_project,
+ name: project_pending_deletion.name,
+ namespace: project_pending_deletion.namespace)
+ end
+
+ before do
+ new_project.validate
+ end
+
+ it 'contains errors related to the project being deleted' do
+ expect(new_project.errors.full_messages.first).to eq('The project is still being deleted. Please try again later.')
+ end
+ end
end
describe 'default_scope' do
@@ -263,9 +283,10 @@ describe Project, models: true do
end
describe '#to_reference' do
- let(:owner) { create(:user, name: 'Gitlab') }
+ let(:owner) { create(:user, name: 'Gitlab') }
let(:namespace) { create(:namespace, path: 'sample-namespace', owner: owner) }
- let(:project) { create(:empty_project, path: 'sample-project', namespace: namespace) }
+ let(:project) { create(:empty_project, path: 'sample-project', namespace: namespace) }
+ let(:group) { create(:group, name: 'Group', path: 'sample-group', owner: owner) }
context 'when nil argument' do
it 'returns nil' do
@@ -273,6 +294,14 @@ describe Project, models: true do
end
end
+ context 'when full is true' do
+ it 'returns complete path to the project' do
+ expect(project.to_reference(full: true)).to eq 'sample-namespace/sample-project'
+ expect(project.to_reference(project, full: true)).to eq 'sample-namespace/sample-project'
+ expect(project.to_reference(group, full: true)).to eq 'sample-namespace/sample-project'
+ end
+ end
+
context 'when same project argument' do
it 'returns nil' do
expect(project.to_reference(project)).to be_nil
@@ -290,10 +319,33 @@ describe Project, models: true do
context 'when same namespace / cross-project argument' do
let(:another_project) { create(:empty_project, namespace: namespace) }
- it 'returns complete path to the project' do
+ it 'returns path to the project' do
expect(project.to_reference(another_project)).to eq 'sample-project'
end
end
+
+ context 'when different namespace / cross-project argument' do
+ let(:another_namespace) { create(:namespace, path: 'another-namespace', owner: owner) }
+ let(:another_project) { create(:empty_project, path: 'another-project', namespace: another_namespace) }
+
+ it 'returns full path to the project' do
+ expect(project.to_reference(another_project)).to eq 'sample-namespace/sample-project'
+ end
+ end
+
+ context 'when argument is a namespace' do
+ context 'with same project path' do
+ it 'returns path to the project' do
+ expect(project.to_reference(namespace)).to eq 'sample-project'
+ end
+ end
+
+ context 'with different project path' do
+ it 'returns full path to the project' do
+ expect(project.to_reference(group)).to eq 'sample-namespace/sample-project'
+ end
+ end
+ end
end
describe '#to_human_reference' do
@@ -331,7 +383,7 @@ describe Project, models: true do
end
describe '#repository_storage_path' do
- let(:project) { create(:project, repository_storage: 'custom') }
+ let(:project) { create(:empty_project, repository_storage: 'custom') }
before do
FileUtils.mkdir('tmp/tests/custom_repositories')
@@ -361,14 +413,6 @@ describe Project, models: true do
end
end
- describe "#web_url_without_protocol" do
- let(:project) { create(:empty_project, path: "somewhere") }
-
- it 'returns the web URL without the protocol for this repo' do
- expect(project.web_url_without_protocol).to eq("#{Gitlab.config.gitlab.url.split('://')[1]}/#{project.namespace.path}/somewhere")
- end
- end
-
describe "#new_issue_address" do
let(:project) { create(:empty_project, path: "somewhere") }
let(:user) { create(:user) }
@@ -399,7 +443,7 @@ describe Project, models: true do
describe 'last_activity methods' do
let(:timestamp) { 2.hours.ago }
# last_activity_at gets set to created_at upon creation
- let(:project) { create(:project, created_at: timestamp, updated_at: timestamp) }
+ let(:project) { create(:empty_project, created_at: timestamp, updated_at: timestamp) }
describe 'last_activity' do
it 'alias last_activity to last_event' do
@@ -483,7 +527,7 @@ describe Project, models: true do
context 'with namespace' do
before do
@group = create :group, name: 'gitlab'
- @project = create(:project, name: 'gitlabhq', namespace: @group)
+ @project = create(:empty_project, name: 'gitlabhq', namespace: @group)
end
it { expect(@project.to_param).to eq('gitlabhq') }
@@ -509,7 +553,7 @@ describe Project, models: true do
end
describe '#repository' do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
it 'returns valid repo' do
expect(project.repository).to be_kind_of(Repository)
@@ -517,20 +561,22 @@ describe Project, models: true do
end
describe '#default_issues_tracker?' do
- let(:project) { create(:project) }
- let(:ext_project) { create(:redmine_project) }
-
it "is true if used internal tracker" do
+ project = build(:empty_project)
+
expect(project.default_issues_tracker?).to be_truthy
end
it "is false if used other tracker" do
- expect(ext_project.default_issues_tracker?).to be_falsey
+ # NOTE: The current nature of this factory requires persistence
+ project = create(:redmine_project)
+
+ expect(project.default_issues_tracker?).to be_falsey
end
end
describe '#external_issue_tracker' do
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project) }
let(:ext_project) { create(:redmine_project) }
context 'on existing projects with no value for has_external_issue_tracker' do
@@ -565,7 +611,7 @@ describe Project, models: true do
end
describe '#cache_has_external_issue_tracker' do
- let(:project) { create(:project, has_external_issue_tracker: nil) }
+ let(:project) { create(:empty_project, has_external_issue_tracker: nil) }
it 'stores true if there is any external_issue_tracker' do
services = double(:service, external_issue_trackers: [RedmineService.new])
@@ -587,9 +633,9 @@ describe Project, models: true do
end
describe '#has_wiki?' do
- let(:no_wiki_project) { create(:project, wiki_access_level: ProjectFeature::DISABLED, has_external_wiki: false) }
- let(:wiki_enabled_project) { create(:project) }
- let(:external_wiki_project) { create(:project, has_external_wiki: true) }
+ let(:no_wiki_project) { create(:empty_project, :wiki_disabled, has_external_wiki: false) }
+ let(:wiki_enabled_project) { create(:empty_project) }
+ let(:external_wiki_project) { create(:empty_project, has_external_wiki: true) }
it 'returns true if project is wiki enabled or has external wiki' do
expect(wiki_enabled_project).to have_wiki
@@ -599,7 +645,7 @@ describe Project, models: true do
end
describe '#external_wiki' do
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project) }
context 'with an active external wiki' do
before do
@@ -650,7 +696,7 @@ describe Project, models: true do
end
describe '#open_branches' do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
before do
project.protected_branches.create(name: 'master')
@@ -672,7 +718,7 @@ describe Project, models: true do
it 'counts stars from multiple users' do
user1 = create :user
user2 = create :user
- project = create :project, :public
+ project = create(:empty_project, :public)
expect(project.star_count).to eq(0)
@@ -694,8 +740,8 @@ describe Project, models: true do
it 'counts stars on the right project' do
user = create :user
- project1 = create :project, :public
- project2 = create :project, :public
+ project1 = create(:empty_project, :public)
+ project2 = create(:empty_project, :public)
expect(project1.star_count).to eq(0)
expect(project2.star_count).to eq(0)
@@ -727,7 +773,7 @@ describe Project, models: true do
end
describe '#avatar_type' do
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project) }
it 'is true if avatar is image' do
project.update_attribute(:avatar, 'uploads/avatar.png')
@@ -743,7 +789,7 @@ describe Project, models: true do
describe '#avatar_url' do
subject { project.avatar_url }
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project) }
context 'When avatar file is uploaded' do
before do
@@ -778,7 +824,7 @@ describe Project, models: true do
end
describe '#pipeline_for' do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let!(:pipeline) { create_pipeline }
shared_examples 'giving the correct pipeline' do
@@ -812,13 +858,33 @@ describe Project, models: true do
end
describe '#builds_enabled' do
- let(:project) { create :project }
+ let(:project) { create(:empty_project) }
subject { project.builds_enabled }
it { expect(project.builds_enabled?).to be_truthy }
end
+ describe '.with_shared_runners' do
+ subject { Project.with_shared_runners }
+
+ context 'when shared runners are enabled for project' do
+ let!(:project) { create(:empty_project, shared_runners_enabled: true) }
+
+ it "returns a project" do
+ is_expected.to eq([project])
+ end
+ end
+
+ context 'when shared runners are disabled for project' do
+ let!(:project) { create(:empty_project, shared_runners_enabled: false) }
+
+ it "returns an empty array" do
+ is_expected.to be_empty
+ end
+ end
+ end
+
describe '.cached_count', caching: true do
let(:group) { create(:group, :public) }
let!(:project1) { create(:empty_project, :public, group: group) }
@@ -864,7 +930,7 @@ describe Project, models: true do
end
describe '.visible_to_user' do
- let!(:project) { create(:project, :private) }
+ let!(:project) { create(:empty_project, :private) }
let!(:user) { create(:user) }
subject { described_class.visible_to_user(user) }
@@ -961,8 +1027,30 @@ describe Project, models: true do
end
end
+ describe '#shared_runners' do
+ let!(:runner) { create(:ci_runner, :shared) }
+
+ subject { project.shared_runners }
+
+ context 'when shared runners are enabled for project' do
+ let!(:project) { create(:empty_project, shared_runners_enabled: true) }
+
+ it "returns a list of shared runners" do
+ is_expected.to eq([runner])
+ end
+ end
+
+ context 'when shared runners are disabled for project' do
+ let!(:project) { create(:empty_project, shared_runners_enabled: false) }
+
+ it "returns a empty list" do
+ is_expected.to be_empty
+ end
+ end
+ end
+
describe '#visibility_level_allowed?' do
- let(:project) { create(:project, :internal) }
+ let(:project) { create(:empty_project, :internal) }
context 'when checking on non-forked project' do
it { expect(project.visibility_level_allowed?(Gitlab::VisibilityLevel::PRIVATE)).to be_truthy }
@@ -971,8 +1059,8 @@ describe Project, models: true do
end
context 'when checking on forked project' do
- let(:project) { create(:project, :internal) }
- let(:forked_project) { create(:project, forked_from_project: project) }
+ let(:project) { create(:empty_project, :internal) }
+ let(:forked_project) { create(:empty_project, forked_from_project: project) }
it { expect(forked_project.visibility_level_allowed?(Gitlab::VisibilityLevel::PRIVATE)).to be_truthy }
it { expect(forked_project.visibility_level_allowed?(Gitlab::VisibilityLevel::INTERNAL)).to be_truthy }
@@ -980,8 +1068,24 @@ describe Project, models: true do
end
end
+ describe '#pages_deployed?' do
+ let(:project) { create :empty_project }
+
+ subject { project.pages_deployed? }
+
+ context 'if public folder does exist' do
+ before { allow(Dir).to receive(:exist?).with(project.public_pages_path).and_return(true) }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context "if public folder doesn't exist" do
+ it { is_expected.to be_falsey }
+ end
+ end
+
describe '.search' do
- let(:project) { create(:project, description: 'kitten mittens') }
+ let(:project) { create(:empty_project, description: 'kitten mittens') }
it 'returns projects with a matching name' do
expect(described_class.search(project.name)).to eq([project])
@@ -1039,7 +1143,7 @@ describe Project, models: true do
end
describe '#rename_repo' do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:gitlab_shell) { Gitlab::Shell.new }
before do
@@ -1089,7 +1193,7 @@ describe Project, models: true do
end
describe '#expire_caches_before_rename' do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:repo) { double(:repo, exists?: true) }
let(:wiki) { double(:wiki, exists?: true) }
@@ -1110,7 +1214,7 @@ describe Project, models: true do
end
describe '.search_by_title' do
- let(:project) { create(:project, name: 'kittens') }
+ let(:project) { create(:empty_project, name: 'kittens') }
it 'returns projects with a matching name' do
expect(described_class.search_by_title(project.name)).to eq([project])
@@ -1129,8 +1233,8 @@ describe Project, models: true do
let(:private_group) { create(:group, visibility_level: 0) }
let(:internal_group) { create(:group, visibility_level: 10) }
- let(:private_project) { create :project, :private, group: private_group }
- let(:internal_project) { create :project, :internal, group: internal_group }
+ let(:private_project) { create :empty_project, :private, group: private_group }
+ let(:internal_project) { create :empty_project, :internal, group: internal_group }
context 'when group is private project can not be internal' do
it { expect(private_project.visibility_level_allowed?(Gitlab::VisibilityLevel::INTERNAL)).to be_falsey }
@@ -1142,7 +1246,7 @@ describe Project, models: true do
end
describe '#create_repository' do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:shell) { Gitlab::Shell.new }
before do
@@ -1184,7 +1288,7 @@ describe Project, models: true do
describe '#protected_branch?' do
context 'existing project' do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
it 'returns true when the branch matches a protected branch via direct match' do
create(:protected_branch, project: project, name: "foo")
@@ -1368,7 +1472,7 @@ describe Project, models: true do
name: name)
end
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:pipeline) { create_pipeline }
context 'with many builds' do
@@ -1448,7 +1552,7 @@ describe Project, models: true do
end
context 'not forked' do
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project) }
it 'schedules a RepositoryImportWorker job' do
expect(RepositoryImportWorker).to receive(:perform_async).with(project.id)
@@ -1459,19 +1563,19 @@ describe Project, models: true do
end
describe '#gitlab_project_import?' do
- subject(:project) { build(:project, import_type: 'gitlab_project') }
+ subject(:project) { build(:empty_project, import_type: 'gitlab_project') }
it { expect(project.gitlab_project_import?).to be true }
end
describe '#gitea_import?' do
- subject(:project) { build(:project, import_type: 'gitea') }
+ subject(:project) { build(:empty_project, import_type: 'gitea') }
it { expect(project.gitea_import?).to be true }
end
describe '#lfs_enabled?' do
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project) }
shared_examples 'project overrides group' do
it 'returns true when enabled in project' do
@@ -1532,11 +1636,13 @@ describe Project, models: true do
end
end
- describe 'change_head' do
- let(:project) { create(:project) }
+ describe '#change_head' do
+ let(:project) { create(:project, :repository) }
- it 'calls the before_change_head method' do
+ it 'calls the before_change_head and after_change_head methods' do
expect(project.repository).to receive(:before_change_head)
+ expect(project.repository).to receive(:after_change_head)
+
project.change_head(project.default_branch)
end
@@ -1552,11 +1658,6 @@ describe Project, models: true do
project.change_head(project.default_branch)
end
- it 'expires the avatar cache' do
- expect(project.repository).to receive(:expire_avatar_cache)
- project.change_head(project.default_branch)
- end
-
it 'reloads the default branch' do
expect(project).to receive(:reload_default_branch)
project.change_head(project.default_branch)
@@ -1564,7 +1665,7 @@ describe Project, models: true do
end
describe '#pushes_since_gc' do
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project) }
after do
project.reset_pushes_since_gc
@@ -1586,7 +1687,7 @@ describe Project, models: true do
end
describe '#increment_pushes_since_gc' do
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project) }
after do
project.reset_pushes_since_gc
@@ -1600,7 +1701,7 @@ describe Project, models: true do
end
describe '#reset_pushes_since_gc' do
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project) }
after do
project.reset_pushes_since_gc
@@ -1615,121 +1716,158 @@ describe Project, models: true do
end
end
- describe '#environments_for' do
- let(:project) { create(:project) }
- let(:environment) { create(:environment, project: project) }
+ describe '#deployment_variables' do
+ context 'when project has no deployment service' do
+ let(:project) { create(:empty_project) }
- context 'tagged deployment' do
- before do
- create(:deployment, environment: environment, ref: '1.0', tag: true, sha: project.commit.id)
+ it 'returns an empty array' do
+ expect(project.deployment_variables).to eq []
end
+ end
- it 'returns environment when with_tags is set' do
- expect(project.environments_for('master', commit: project.commit, with_tags: true))
- .to contain_exactly(environment)
- end
+ context 'when project has a deployment service' do
+ let(:project) { create(:kubernetes_project) }
- it 'does not return environment when no with_tags is set' do
- expect(project.environments_for('master', commit: project.commit))
- .to be_empty
+ it 'returns variables from this service' do
+ expect(project.deployment_variables).to include(
+ { key: 'KUBE_TOKEN', value: project.kubernetes_service.token, public: false }
+ )
end
+ end
+ end
- it 'does not return environment when commit is not part of deployment' do
- expect(project.environments_for('master', commit: project.commit('feature')))
- .to be_empty
- end
+ describe '#update_project_statistics' do
+ let(:project) { create(:empty_project) }
+
+ it "is called after creation" do
+ expect(project.statistics).to be_a ProjectStatistics
+ expect(project.statistics).to be_persisted
end
- context 'branch deployment' do
- before do
- create(:deployment, environment: environment, ref: 'master', sha: project.commit.id)
- end
+ it "copies the namespace_id" do
+ expect(project.statistics.namespace_id).to eq project.namespace_id
+ end
- it 'returns environment when ref is set' do
- expect(project.environments_for('master', commit: project.commit))
- .to contain_exactly(environment)
- end
+ it "updates the namespace_id when changed" do
+ namespace = create(:namespace)
+ project.update(namespace: namespace)
+
+ expect(project.statistics.namespace_id).to eq namespace.id
+ end
+ end
+
+ describe 'inside_path' do
+ let!(:project1) { create(:empty_project) }
+ let!(:project2) { create(:empty_project) }
+ let!(:path) { project1.namespace.path }
+
+ it { expect(Project.inside_path(path)).to eq([project1]) }
+ end
+
+ describe '#route_map_for' do
+ let(:project) { create(:project) }
+ let(:route_map) do
+ <<-MAP.strip_heredoc
+ - source: /source/(.*)/
+ public: '\\1'
+ MAP
+ end
+
+ before do
+ project.repository.commit_file(User.last, '.gitlab/route-map.yml', route_map, message: 'Add .gitlab/route-map.yml', branch_name: 'master', update: false)
+ end
- it 'does not environment when ref is different' do
- expect(project.environments_for('feature', commit: project.commit))
- .to be_empty
+ context 'when there is a .gitlab/route-map.yml at the commit' do
+ context 'when the route map is valid' do
+ it 'returns a route map' do
+ map = project.route_map_for(project.commit.sha)
+ expect(map).to be_a_kind_of(Gitlab::RouteMap)
+ end
end
- it 'does not return environment when commit is not part of deployment' do
- expect(project.environments_for('master', commit: project.commit('feature')))
- .to be_empty
+ context 'when the route map is invalid' do
+ let(:route_map) { 'INVALID' }
+
+ it 'returns nil' do
+ expect(project.route_map_for(project.commit.sha)).to be_nil
+ end
end
+ end
- it 'returns environment when commit constraint is not set' do
- expect(project.environments_for('master'))
- .to contain_exactly(environment)
+ context 'when there is no .gitlab/route-map.yml at the commit' do
+ it 'returns nil' do
+ expect(project.route_map_for(project.commit.parent.sha)).to be_nil
end
end
end
- describe '#environments_recently_updated_on_branch' do
+ describe '#public_path_for_source_path' do
let(:project) { create(:project) }
- let(:environment) { create(:environment, project: project) }
+ let(:route_map) do
+ Gitlab::RouteMap.new(<<-MAP.strip_heredoc)
+ - source: /source/(.*)/
+ public: '\\1'
+ MAP
+ end
+ let(:sha) { project.commit.id }
- context 'when last deployment to environment is the most recent one' do
+ context 'when there is a route map' do
before do
- create(:deployment, environment: environment, ref: 'feature')
+ allow(project).to receive(:route_map_for).with(sha).and_return(route_map)
end
- it 'finds recently updated environment' do
- expect(project.environments_recently_updated_on_branch('feature'))
- .to contain_exactly(environment)
- end
- end
-
- context 'when last deployment to environment is not the most recent' do
- before do
- create(:deployment, environment: environment, ref: 'feature')
- create(:deployment, environment: environment, ref: 'master')
+ context 'when the source path is mapped' do
+ it 'returns the public path' do
+ expect(project.public_path_for_source_path('source/file.html', sha)).to eq('file.html')
+ end
end
- it 'does not find environment' do
- expect(project.environments_recently_updated_on_branch('feature'))
- .to be_empty
+ context 'when the source path is not mapped' do
+ it 'returns nil' do
+ expect(project.public_path_for_source_path('file.html', sha)).to be_nil
+ end
end
end
- context 'when there are two environments that deploy to the same branch' do
- let(:second_environment) { create(:environment, project: project) }
-
+ context 'when there is no route map' do
before do
- create(:deployment, environment: environment, ref: 'feature')
- create(:deployment, environment: second_environment, ref: 'feature')
+ allow(project).to receive(:route_map_for).with(sha).and_return(nil)
end
- it 'finds both environments' do
- expect(project.environments_recently_updated_on_branch('feature'))
- .to contain_exactly(environment, second_environment)
+ it 'returns nil' do
+ expect(project.public_path_for_source_path('source/file.html', sha)).to be_nil
end
end
end
- describe '#deployment_variables' do
- context 'when project has no deployment service' do
- let(:project) { create(:empty_project) }
+ def enable_lfs
+ allow(Gitlab.config.lfs).to receive(:enabled).and_return(true)
+ end
- it 'returns an empty array' do
- expect(project.deployment_variables).to eq []
- end
+ describe '#pages_url' do
+ let(:group) { create :group, name: group_name }
+ let(:project) { create :empty_project, namespace: group, name: project_name }
+ let(:domain) { 'Example.com' }
+
+ subject { project.pages_url }
+
+ before do
+ allow(Settings.pages).to receive(:host).and_return(domain)
+ allow(Gitlab.config.pages).to receive(:url).and_return('http://example.com')
end
- context 'when project has a deployment service' do
- let(:project) { create(:kubernetes_project) }
+ context 'group page' do
+ let(:group_name) { 'Group' }
+ let(:project_name) { 'group.example.com' }
- it 'returns variables from this service' do
- expect(project.deployment_variables).to include(
- { key: 'KUBE_TOKEN', value: project.kubernetes_service.token, public: false }
- )
- end
+ it { is_expected.to eq("http://group.example.com") }
end
- end
- def enable_lfs
- allow(Gitlab.config.lfs).to receive(:enabled).and_return(true)
+ context 'project page' do
+ let(:group_name) { 'Group' }
+ let(:project_name) { 'Project' }
+
+ it { is_expected.to eq("http://group.example.com/project") }
+ end
end
end
diff --git a/spec/models/project_statistics_spec.rb b/spec/models/project_statistics_spec.rb
new file mode 100644
index 00000000000..ff29f6f66ba
--- /dev/null
+++ b/spec/models/project_statistics_spec.rb
@@ -0,0 +1,160 @@
+require 'rails_helper'
+
+describe ProjectStatistics, models: true do
+ let(:project) { create :empty_project }
+ let(:statistics) { project.statistics }
+
+ describe 'constants' do
+ describe 'STORAGE_COLUMNS' do
+ it 'is an array of symbols' do
+ expect(described_class::STORAGE_COLUMNS).to be_kind_of Array
+ expect(described_class::STORAGE_COLUMNS.map(&:class).uniq).to eq [Symbol]
+ end
+ end
+
+ describe 'STATISTICS_COLUMNS' do
+ it 'is an array of symbols' do
+ expect(described_class::STATISTICS_COLUMNS).to be_kind_of Array
+ expect(described_class::STATISTICS_COLUMNS.map(&:class).uniq).to eq [Symbol]
+ end
+
+ it 'includes all storage columns' do
+ expect(described_class::STATISTICS_COLUMNS & described_class::STORAGE_COLUMNS).to eq described_class::STORAGE_COLUMNS
+ end
+ end
+ end
+
+ describe 'associations' do
+ it { is_expected.to belong_to(:project) }
+ it { is_expected.to belong_to(:namespace) }
+ end
+
+ describe 'statistics columns' do
+ it "support values up to 8 exabytes" do
+ statistics.update!(
+ commit_count: 8.exabytes - 1,
+ repository_size: 2.exabytes,
+ lfs_objects_size: 2.exabytes,
+ build_artifacts_size: 4.exabytes - 1,
+ )
+
+ statistics.reload
+
+ expect(statistics.commit_count).to eq(8.exabytes - 1)
+ expect(statistics.repository_size).to eq(2.exabytes)
+ expect(statistics.lfs_objects_size).to eq(2.exabytes)
+ expect(statistics.build_artifacts_size).to eq(4.exabytes - 1)
+ expect(statistics.storage_size).to eq(8.exabytes - 1)
+ end
+ end
+
+ describe '#total_repository_size' do
+ it "sums repository and LFS object size" do
+ statistics.repository_size = 2
+ statistics.lfs_objects_size = 3
+ statistics.build_artifacts_size = 4
+
+ expect(statistics.total_repository_size).to eq 5
+ end
+ end
+
+ describe '#refresh!' do
+ before do
+ allow(statistics).to receive(:update_commit_count)
+ allow(statistics).to receive(:update_repository_size)
+ allow(statistics).to receive(:update_lfs_objects_size)
+ allow(statistics).to receive(:update_build_artifacts_size)
+ allow(statistics).to receive(:update_storage_size)
+ end
+
+ context "without arguments" do
+ before do
+ statistics.refresh!
+ end
+
+ it "sums all counters" do
+ expect(statistics).to have_received(:update_commit_count)
+ expect(statistics).to have_received(:update_repository_size)
+ expect(statistics).to have_received(:update_lfs_objects_size)
+ expect(statistics).to have_received(:update_build_artifacts_size)
+ end
+ end
+
+ context "when passing an only: argument" do
+ before do
+ statistics.refresh! only: [:lfs_objects_size]
+ end
+
+ it "only updates the given columns" do
+ expect(statistics).to have_received(:update_lfs_objects_size)
+ expect(statistics).not_to have_received(:update_commit_count)
+ expect(statistics).not_to have_received(:update_repository_size)
+ expect(statistics).not_to have_received(:update_build_artifacts_size)
+ end
+ end
+ end
+
+ describe '#update_commit_count' do
+ before do
+ allow(project.repository).to receive(:commit_count).and_return(23)
+ statistics.update_commit_count
+ end
+
+ it "stores the number of commits in the repository" do
+ expect(statistics.commit_count).to eq 23
+ end
+ end
+
+ describe '#update_repository_size' do
+ before do
+ allow(project.repository).to receive(:size).and_return(12)
+ statistics.update_repository_size
+ end
+
+ it "stores the size of the repository" do
+ expect(statistics.repository_size).to eq 12.megabytes
+ end
+ end
+
+ describe '#update_lfs_objects_size' do
+ let!(:lfs_object1) { create(:lfs_object, size: 23.megabytes) }
+ let!(:lfs_object2) { create(:lfs_object, size: 34.megabytes) }
+ let!(:lfs_objects_project1) { create(:lfs_objects_project, project: project, lfs_object: lfs_object1) }
+ let!(:lfs_objects_project2) { create(:lfs_objects_project, project: project, lfs_object: lfs_object2) }
+
+ before do
+ statistics.update_lfs_objects_size
+ end
+
+ it "stores the size of related LFS objects" do
+ expect(statistics.lfs_objects_size).to eq 57.megabytes
+ end
+ end
+
+ describe '#update_build_artifacts_size' do
+ let!(:pipeline) { create(:ci_pipeline, project: project) }
+ let!(:build1) { create(:ci_build, pipeline: pipeline, artifacts_size: 45.megabytes) }
+ let!(:build2) { create(:ci_build, pipeline: pipeline, artifacts_size: 56.megabytes) }
+
+ before do
+ statistics.update_build_artifacts_size
+ end
+
+ it "stores the size of related build artifacts" do
+ expect(statistics.build_artifacts_size).to eq 101.megabytes
+ end
+ end
+
+ describe '#update_storage_size' do
+ it "sums all storage counters" do
+ statistics.update!(
+ repository_size: 2,
+ lfs_objects_size: 3,
+ )
+
+ statistics.reload
+
+ expect(statistics.storage_size).to eq 5
+ end
+ end
+end
diff --git a/spec/models/project_team_spec.rb b/spec/models/project_team_spec.rb
index 0475cecaa2d..942eeab251d 100644
--- a/spec/models/project_team_spec.rb
+++ b/spec/models/project_team_spec.rb
@@ -265,10 +265,10 @@ describe ProjectTeam, models: true 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) }
+ let(:personal_project) { create(:empty_project, namespace: developer.namespace) }
+ let(:group_project) { create(:empty_project, namespace: group) }
+ let(:members_project) { create(:empty_project) }
+ let(:shared_project) { create(:empty_project) }
before do
group.add_master(master)
@@ -330,7 +330,7 @@ describe ProjectTeam, models: true do
reporter = create(:user)
promoted_guest = create(:user)
guest = create(:user)
- project = create(:project)
+ project = create(:empty_project)
project.add_master(master)
project.add_reporter(reporter)
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index b5a42edd192..9bfa6409607 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -4,7 +4,7 @@ describe Repository, models: true do
include RepoHelpers
TestBlob = Struct.new(:name)
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:repository) { project.repository }
let(:user) { create(:user) }
@@ -15,7 +15,12 @@ describe Repository, models: true do
let(:merge_commit) do
merge_request = create(:merge_request, source_branch: 'feature', target_branch: 'master', source_project: project)
- merge_commit_id = repository.merge(user, merge_request, commit_options)
+
+ merge_commit_id = repository.merge(user,
+ merge_request.diff_head_sha,
+ merge_request,
+ commit_options)
+
repository.commit(merge_commit_id)
end
@@ -90,6 +95,30 @@ describe Repository, models: true do
it { is_expected.to eq(['v1.1.0', 'v1.0.0']) }
end
+
+ context 'annotated tag pointing to a blob' do
+ let(:annotated_tag_name) { 'annotated-tag' }
+
+ subject { repository.tags_sorted_by('updated_asc').map(&:name) }
+
+ before do
+ options = { message: 'test tag message\n',
+ tagger: { name: 'John Smith', email: 'john@gmail.com' } }
+ repository.rugged.tags.create(annotated_tag_name, 'a48e4fc218069f68ef2e769dd8dfea3991362175', options)
+
+ double_first = double(committed_date: Time.now - 1.second)
+ double_last = double(committed_date: Time.now)
+
+ allow(tag_a).to receive(:dereferenced_target).and_return(double_last)
+ allow(tag_b).to receive(:dereferenced_target).and_return(double_first)
+ end
+
+ it { is_expected.to eq(['v1.1.0', 'v1.0.0', annotated_tag_name]) }
+
+ after do
+ repository.rugged.tags.delete(annotated_tag_name)
+ end
+ end
end
end
@@ -139,6 +168,22 @@ describe Repository, models: true do
it { is_expected.to eq('c1acaa58bbcbc3eafe538cb8274ba387047b69f8') }
end
+ describe '#last_commit_id_for_path' do
+ subject { repository.last_commit_id_for_path(sample_commit.id, '.gitignore') }
+
+ it "returns last commit id for a given path" do
+ is_expected.to eq('c1acaa58bbcbc3eafe538cb8274ba387047b69f8')
+ end
+
+ it "caches last commit id for a given path" do
+ cache = repository.send(:cache)
+ key = "last_commit_id_for_path:#{sample_commit.id}:#{Digest::SHA1.hexdigest('.gitignore')}"
+
+ expect(cache).to receive(:fetch).with(key).and_return('c1acaa5')
+ is_expected.to eq('c1acaa5')
+ end
+ end
+
describe '#find_commits_by_message' do
it 'returns commits with messages containing a given string' do
commit_ids = repository.find_commits_by_message('submodule').map(&:id)
@@ -249,17 +294,39 @@ describe Repository, models: true do
describe "#commit_dir" do
it "commits a change that creates a new directory" do
expect do
- repository.commit_dir(user, 'newdir', 'Create newdir', 'master')
+ repository.commit_dir(user, 'newdir',
+ message: 'Create newdir', branch_name: 'master')
end.to change { repository.commits('master').count }.by(1)
newdir = repository.tree('master', 'newdir')
expect(newdir.path).to eq('newdir')
end
+ context "when committing to another project" do
+ let(:forked_project) { create(:project) }
+
+ it "creates a fork and commit to the forked project" do
+ expect do
+ repository.commit_dir(user, 'newdir',
+ message: 'Create newdir', branch_name: 'patch',
+ start_branch_name: 'master', start_project: forked_project)
+ end.to change { repository.commits('master').count }.by(0)
+
+ expect(repository.branch_exists?('patch')).to be_truthy
+ expect(forked_project.repository.branch_exists?('patch')).to be_falsy
+
+ newdir = repository.tree('patch', 'newdir')
+ expect(newdir.path).to eq('newdir')
+ end
+ end
+
context "when an author is specified" do
it "uses the given email/name to set the commit's author" do
expect do
- repository.commit_dir(user, "newdir", "Add newdir", 'master', author_email: author_email, author_name: author_name)
+ repository.commit_dir(user, 'newdir',
+ message: 'Add newdir',
+ branch_name: 'master',
+ author_email: author_email, author_name: author_name)
end.to change { repository.commits('master').count }.by(1)
last_commit = repository.commit
@@ -274,8 +341,9 @@ describe Repository, models: true do
it 'commits change to a file successfully' do
expect do
repository.commit_file(user, 'CHANGELOG', 'Changelog!',
- 'Updates file content',
- 'master', true)
+ message: 'Updates file content',
+ branch_name: 'master',
+ update: true)
end.to change { repository.commits('master').count }.by(1)
blob = repository.blob_at('master', 'CHANGELOG')
@@ -286,8 +354,12 @@ describe Repository, models: true do
context "when an author is specified" do
it "uses the given email/name to set the commit's author" do
expect do
- repository.commit_file(user, "README", 'README!', 'Add README',
- 'master', true, author_email: author_email, author_name: author_name)
+ repository.commit_file(user, 'README', 'README!',
+ message: 'Add README',
+ branch_name: 'master',
+ update: true,
+ author_email: author_email,
+ author_name: author_name)
end.to change { repository.commits('master').count }.by(1)
last_commit = repository.commit
@@ -302,7 +374,7 @@ describe Repository, models: true do
it 'updates filename successfully' do
expect do
repository.update_file(user, 'NEWLICENSE', 'Copyright!',
- branch: 'master',
+ branch_name: 'master',
previous_path: 'LICENSE',
message: 'Changes filename')
end.to change { repository.commits('master').count }.by(1)
@@ -315,15 +387,16 @@ describe Repository, models: true do
context "when an author is specified" do
it "uses the given email/name to set the commit's author" do
- repository.commit_file(user, "README", 'README!', 'Add README', 'master', true)
+ repository.commit_file(user, 'README', 'README!',
+ message: 'Add README', branch_name: 'master', update: true)
expect do
- repository.update_file(user, 'README', "Updated README!",
- branch: 'master',
- previous_path: 'README',
- message: 'Update README',
- author_email: author_email,
- author_name: author_name)
+ repository.update_file(user, 'README', 'Updated README!',
+ branch_name: 'master',
+ previous_path: 'README',
+ message: 'Update README',
+ author_email: author_email,
+ author_name: author_name)
end.to change { repository.commits('master').count }.by(1)
last_commit = repository.commit
@@ -336,10 +409,12 @@ describe Repository, models: true do
describe "#remove_file" do
it 'removes file successfully' do
- repository.commit_file(user, "README", 'README!', 'Add README', 'master', true)
+ repository.commit_file(user, 'README', 'README!',
+ message: 'Add README', branch_name: 'master', update: true)
expect do
- repository.remove_file(user, "README", "Remove README", 'master')
+ repository.remove_file(user, 'README',
+ message: 'Remove README', branch_name: 'master')
end.to change { repository.commits('master').count }.by(1)
expect(repository.blob_at('master', 'README')).to be_nil
@@ -347,10 +422,13 @@ describe Repository, models: true do
context "when an author is specified" do
it "uses the given email/name to set the commit's author" do
- repository.commit_file(user, "README", 'README!', 'Add README', 'master', true)
+ repository.commit_file(user, 'README', 'README!',
+ message: 'Add README', branch_name: 'master', update: true)
expect do
- repository.remove_file(user, "README", "Remove README", 'master', author_email: author_email, author_name: author_name)
+ repository.remove_file(user, 'README',
+ message: 'Remove README', branch_name: 'master',
+ author_email: author_email, author_name: author_name)
end.to change { repository.commits('master').count }.by(1)
last_commit = repository.commit
@@ -498,11 +576,14 @@ describe Repository, models: true do
describe "#license_blob", caching: true do
before do
- repository.remove_file(user, 'LICENSE', 'Remove LICENSE', 'master')
+ repository.remove_file(
+ user, 'LICENSE', message: 'Remove LICENSE', branch_name: 'master')
end
it 'handles when HEAD points to non-existent ref' do
- repository.commit_file(user, 'LICENSE', 'Copyright!', 'Add LICENSE', 'master', false)
+ repository.commit_file(
+ user, 'LICENSE', 'Copyright!',
+ message: 'Add LICENSE', branch_name: 'master', update: false)
allow(repository).to receive(:file_on_head).
and_raise(Rugged::ReferenceError)
@@ -511,21 +592,27 @@ describe Repository, models: true do
end
it 'looks in the root_ref only' do
- repository.remove_file(user, 'LICENSE', 'Remove LICENSE', 'markdown')
- repository.commit_file(user, 'LICENSE', Licensee::License.new('mit').content, 'Add LICENSE', 'markdown', false)
+ repository.remove_file(user, 'LICENSE',
+ message: 'Remove LICENSE', branch_name: 'markdown')
+ repository.commit_file(user, 'LICENSE',
+ Licensee::License.new('mit').content,
+ message: 'Add LICENSE', branch_name: 'markdown', update: false)
expect(repository.license_blob).to be_nil
end
it 'detects license file with no recognizable open-source license content' do
- repository.commit_file(user, 'LICENSE', 'Copyright!', 'Add LICENSE', 'master', false)
+ repository.commit_file(user, 'LICENSE', 'Copyright!',
+ message: 'Add LICENSE', branch_name: 'master', update: false)
expect(repository.license_blob.name).to eq('LICENSE')
end
%w[LICENSE LICENCE LiCensE LICENSE.md LICENSE.foo COPYING COPYING.md].each do |filename|
it "detects '#{filename}'" do
- repository.commit_file(user, filename, Licensee::License.new('mit').content, "Add #{filename}", 'master', false)
+ repository.commit_file(user, filename,
+ Licensee::License.new('mit').content,
+ message: "Add #{filename}", branch_name: 'master', update: false)
expect(repository.license_blob.name).to eq(filename)
end
@@ -534,7 +621,8 @@ describe Repository, models: true do
describe '#license_key', caching: true do
before do
- repository.remove_file(user, 'LICENSE', 'Remove LICENSE', 'master')
+ repository.remove_file(user, 'LICENSE',
+ message: 'Remove LICENSE', branch_name: 'master')
end
it 'returns nil when no license is detected' do
@@ -548,13 +636,16 @@ describe Repository, models: true do
end
it 'detects license file with no recognizable open-source license content' do
- repository.commit_file(user, 'LICENSE', 'Copyright!', 'Add LICENSE', 'master', false)
+ repository.commit_file(user, 'LICENSE', 'Copyright!',
+ message: 'Add LICENSE', branch_name: 'master', update: false)
expect(repository.license_key).to be_nil
end
it 'returns the license key' do
- repository.commit_file(user, 'LICENSE', Licensee::License.new('mit').content, 'Add LICENSE', 'master', false)
+ repository.commit_file(user, 'LICENSE',
+ Licensee::License.new('mit').content,
+ message: 'Add LICENSE', branch_name: 'master', update: false)
expect(repository.license_key).to eq('mit')
end
@@ -667,7 +758,7 @@ describe Repository, models: true do
allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, ''])
expect do
- repository.rm_branch(user, 'new_feature')
+ repository.rm_branch(user, 'feature')
end.to raise_error(GitHooksService::PreReceiveError)
end
@@ -688,36 +779,51 @@ describe Repository, models: true do
context 'when pre hooks were successful' do
before do
- expect_any_instance_of(GitHooksService).to receive(:execute).
- with(user, repository.path_to_repo, old_rev, new_rev, 'refs/heads/feature').
- and_yield.and_return(true)
+ service = GitHooksService.new
+ expect(GitHooksService).to receive(:new).and_return(service)
+ expect(service).to receive(:execute).
+ with(
+ user,
+ repository.path_to_repo,
+ old_rev,
+ new_rev,
+ 'refs/heads/feature').
+ and_yield(service).and_return(true)
end
it 'runs without errors' do
expect do
- repository.update_branch_with_hooks(user, 'feature') { new_rev }
+ GitOperationService.new(user, repository).with_branch('feature') do
+ new_rev
+ end
end.not_to raise_error
end
it 'ensures the autocrlf Git option is set to :input' do
- expect(repository).to receive(:update_autocrlf_option)
+ service = GitOperationService.new(user, repository)
- repository.update_branch_with_hooks(user, 'feature') { new_rev }
+ expect(service).to receive(:update_autocrlf_option)
+
+ service.with_branch('feature') { new_rev }
end
context "when the branch wasn't empty" do
it 'updates the head' do
expect(repository.find_branch('feature').dereferenced_target.id).to eq(old_rev)
- repository.update_branch_with_hooks(user, 'feature') { new_rev }
+
+ GitOperationService.new(user, repository).with_branch('feature') do
+ new_rev
+ end
+
expect(repository.find_branch('feature').dereferenced_target.id).to eq(new_rev)
end
end
end
context 'when the update adds more than one commit' do
- it 'runs without errors' do
- old_rev = '33f3729a45c02fc67d00adb1b8bca394b0e761d9'
+ let(:old_rev) { '33f3729a45c02fc67d00adb1b8bca394b0e761d9' }
+ it 'runs without errors' do
# old_rev is an ancestor of new_rev
expect(repository.rugged.merge_base(old_rev, new_rev)).to eq(old_rev)
@@ -727,22 +833,28 @@ describe Repository, models: true do
branch = 'feature-ff-target'
repository.add_branch(user, branch, old_rev)
- expect { repository.update_branch_with_hooks(user, branch) { new_rev } }.not_to raise_error
+ expect do
+ GitOperationService.new(user, repository).with_branch(branch) do
+ new_rev
+ end
+ end.not_to raise_error
end
end
context 'when the update would remove commits from the target branch' do
- it 'raises an exception' do
- branch = 'master'
- old_rev = repository.find_branch(branch).dereferenced_target.sha
+ let(:branch) { 'master' }
+ let(:old_rev) { repository.find_branch(branch).dereferenced_target.sha }
+ it 'raises an exception' do
# The 'master' branch is NOT an ancestor of new_rev.
expect(repository.rugged.merge_base(old_rev, new_rev)).not_to eq(old_rev)
# Updating 'master' to new_rev would lose the commits on 'master' that
# are not contained in new_rev. This should not be allowed.
expect do
- repository.update_branch_with_hooks(user, branch) { new_rev }
+ GitOperationService.new(user, repository).with_branch(branch) do
+ new_rev
+ end
end.to raise_error(Repository::CommitError)
end
end
@@ -752,7 +864,9 @@ describe Repository, models: true do
allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, ''])
expect do
- repository.update_branch_with_hooks(user, 'feature') { new_rev }
+ GitOperationService.new(user, repository).with_branch('feature') do
+ new_rev
+ end
end.to raise_error(GitHooksService::PreReceiveError)
end
end
@@ -760,7 +874,6 @@ describe Repository, models: true do
context 'when target branch is different from source branch' do
before do
allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([true, ''])
- allow(repository).to receive(:update_ref!)
end
it 'expires branch cache' do
@@ -769,7 +882,10 @@ describe Repository, models: true do
expect(repository).not_to receive(:expire_emptiness_caches)
expect(repository).to receive(:expire_branches_cache)
- repository.update_branch_with_hooks(user, 'new-feature') { new_rev }
+ GitOperationService.new(user, repository).
+ with_branch('new-feature') do
+ new_rev
+ end
end
end
@@ -787,7 +903,9 @@ describe Repository, models: true do
expect(empty_repository).to receive(:expire_branches_cache)
empty_repository.commit_file(user, 'CHANGELOG', 'Changelog!',
- 'Updates file content', 'master', false)
+ message: 'Updates file content',
+ branch_name: 'master',
+ update: false)
end
end
end
@@ -837,7 +955,7 @@ describe Repository, models: true do
end
it 'sets autocrlf to :input' do
- repository.update_autocrlf_option
+ GitOperationService.new(nil, repository).send(:update_autocrlf_option)
expect(repository.raw_repository.autocrlf).to eq(:input)
end
@@ -852,7 +970,7 @@ describe Repository, models: true do
expect(repository.raw_repository).not_to receive(:autocrlf=).
with(:input)
- repository.update_autocrlf_option
+ GitOperationService.new(nil, repository).send(:update_autocrlf_option)
end
end
end
@@ -969,8 +1087,11 @@ describe Repository, models: true do
it 'sets the `in_progress_merge_commit_sha` flag for the given merge request' do
merge_request = create(:merge_request, source_branch: 'feature', target_branch: 'master', source_project: project)
- merge_commit_id = repository.merge(user, merge_request, commit_options)
- repository.commit(merge_commit_id)
+
+ merge_commit_id = repository.merge(user,
+ merge_request.diff_head_sha,
+ merge_request,
+ commit_options)
expect(merge_request.in_progress_merge_commit_sha).to eq(merge_commit_id)
end
@@ -1134,6 +1255,24 @@ describe Repository, models: true do
end
end
+ describe '#after_change_head' do
+ it 'flushes the readme cache' do
+ expect(repository).to receive(:expire_method_caches).with([
+ :readme,
+ :changelog,
+ :license,
+ :contributing,
+ :version,
+ :gitignore,
+ :koding,
+ :gitlab_ci,
+ :avatar
+ ])
+
+ repository.after_change_head
+ end
+ end
+
describe '#before_push_tag' do
it 'flushes the cache' do
expect(repository).to receive(:expire_statistics_caches)
@@ -1330,9 +1469,10 @@ describe Repository, models: true do
describe '#rm_tag' do
it 'removes a tag' do
expect(repository).to receive(:before_remove_tag)
- expect(repository.rugged.tags).to receive(:delete).with('v1.1.0')
- repository.rm_tag('v1.1.0')
+ repository.rm_tag(create(:user), 'v1.1.0')
+
+ expect(repository.find_tag('v1.1.0')).to be_nil
end
end
@@ -1400,16 +1540,16 @@ describe Repository, models: true do
end
end
- describe '#update_ref!' do
+ describe '#update_ref' do
it 'can create a ref' do
- repository.update_ref!('refs/heads/foobar', 'refs/heads/master', Gitlab::Git::BLANK_SHA)
+ GitOperationService.new(nil, repository).send(:update_ref, 'refs/heads/foobar', 'refs/heads/master', Gitlab::Git::BLANK_SHA)
expect(repository.find_branch('foobar')).not_to be_nil
end
it 'raises CommitError when the ref update fails' do
expect do
- repository.update_ref!('refs/heads/master', 'refs/heads/master', Gitlab::Git::BLANK_SHA)
+ GitOperationService.new(nil, repository).send(:update_ref, 'refs/heads/master', 'refs/heads/master', Gitlab::Git::BLANK_SHA)
end.to raise_error(Repository::CommitError)
end
end
@@ -1497,14 +1637,6 @@ describe Repository, models: true do
end
end
- describe '#expire_avatar_cache' do
- it 'expires the cache' do
- expect(repository).to receive(:expire_method_caches).with(%i(avatar))
-
- repository.expire_avatar_cache
- end
- end
-
describe '#file_on_head' do
context 'with a non-existing repository' do
it 'returns nil' do
@@ -1650,4 +1782,40 @@ describe Repository, models: true do
repository.refresh_method_caches(%i(readme license))
end
end
+
+ describe '#gitlab_ci_yml_for' do
+ before do
+ repository.commit_file(User.last, '.gitlab-ci.yml', 'CONTENT', message: 'Add .gitlab-ci.yml', branch_name: 'master', update: false)
+ end
+
+ context 'when there is a .gitlab-ci.yml at the commit' do
+ it 'returns the content' do
+ expect(repository.gitlab_ci_yml_for(repository.commit.sha)).to eq('CONTENT')
+ end
+ end
+
+ context 'when there is no .gitlab-ci.yml at the commit' do
+ it 'returns nil' do
+ expect(repository.gitlab_ci_yml_for(repository.commit.parent.sha)).to be_nil
+ end
+ end
+ end
+
+ describe '#route_map_for' do
+ before do
+ repository.commit_file(User.last, '.gitlab/route-map.yml', 'CONTENT', message: 'Add .gitlab/route-map.yml', branch_name: 'master', update: false)
+ end
+
+ context 'when there is a .gitlab/route-map.yml at the commit' do
+ it 'returns the content' do
+ expect(repository.route_map_for(repository.commit.sha)).to eq('CONTENT')
+ end
+ end
+
+ context 'when there is no .gitlab/route-map.yml at the commit' do
+ it 'returns nil' do
+ expect(repository.route_map_for(repository.commit.parent.sha)).to be_nil
+ end
+ end
+ end
end
diff --git a/spec/models/route_spec.rb b/spec/models/route_spec.rb
index 8481a9bef16..dd2a5109abc 100644
--- a/spec/models/route_spec.rb
+++ b/spec/models/route_spec.rb
@@ -14,7 +14,7 @@ describe Route, models: true do
it { is_expected.to validate_uniqueness_of(:path) }
end
- describe '#rename_children' do
+ describe '#rename_descendants' do
let!(:nested_group) { create(:group, path: "test", parent: group) }
let!(:deep_nested_group) { create(:group, path: "foo", parent: nested_group) }
let!(:similar_group) { create(:group, path: 'gitlab-org') }
diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb
index 691511cd93f..0e2f07e945f 100644
--- a/spec/models/service_spec.rb
+++ b/spec/models/service_spec.rb
@@ -12,7 +12,7 @@ describe Service, models: true do
end
describe "Testable" do
- let(:project) { create :project }
+ let(:project) { create(:project, :repository) }
before do
allow(@service).to receive(:project).and_return(project)
@@ -35,7 +35,7 @@ describe Service, models: true do
end
describe "With commits" do
- let(:project) { create :project }
+ let(:project) { create(:project, :repository) }
before do
allow(@service).to receive(:project).and_return(project)
@@ -60,7 +60,7 @@ describe Service, models: true do
api_key: '123456789'
})
end
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project) }
describe 'is prefilled for projects pushover service' do
it "has all fields prefilled" do
@@ -79,7 +79,7 @@ describe Service, models: true do
describe "{property}_changed?" do
let(:service) do
BambooService.create(
- project: create(:project),
+ project: create(:empty_project),
properties: {
bamboo_url: 'http://gitlab.com',
username: 'mic',
@@ -119,7 +119,7 @@ describe Service, models: true do
describe "{property}_touched?" do
let(:service) do
BambooService.create(
- project: create(:project),
+ project: create(:empty_project),
properties: {
bamboo_url: 'http://gitlab.com',
username: 'mic',
@@ -159,7 +159,7 @@ describe Service, models: true do
describe "{property}_was" do
let(:service) do
BambooService.create(
- project: create(:project),
+ project: create(:empty_project),
properties: {
bamboo_url: 'http://gitlab.com',
username: 'mic',
@@ -199,7 +199,7 @@ describe Service, models: true do
describe 'initialize service with no properties' do
let(:service) do
GitlabIssueTrackerService.create(
- project: create(:project),
+ project: create(:empty_project),
title: 'random title'
)
end
@@ -214,7 +214,7 @@ describe Service, models: true do
end
describe "callbacks" do
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project) }
let!(:service) do
RedmineService.new(
project: project,
diff --git a/spec/models/snippet_spec.rb b/spec/models/snippet_spec.rb
index 7425a897769..219ab1989ea 100644
--- a/spec/models/snippet_spec.rb
+++ b/spec/models/snippet_spec.rb
@@ -42,7 +42,7 @@ describe Snippet, models: true do
end
it 'supports a cross-project reference' do
- another_project = build(:project, name: 'another-project', namespace: project.namespace)
+ another_project = build(:empty_project, name: 'another-project', namespace: project.namespace)
expect(snippet.to_reference(another_project)).to eq "sample-project$1"
end
end
@@ -55,7 +55,7 @@ describe Snippet, models: true do
end
it 'still returns shortest reference when project arg present' do
- another_project = build(:project, name: 'another-project')
+ another_project = build(:empty_project, name: 'another-project')
expect(snippet.to_reference(another_project)).to eq "$1"
end
end
@@ -173,7 +173,7 @@ describe Snippet, models: true do
end
describe '#participants' do
- let(:project) { create(:project, :public) }
+ let(:project) { create(:empty_project, :public) }
let(:snippet) { create(:snippet, content: 'foo', project: project) }
let!(:note1) do
diff --git a/spec/models/timelog_spec.rb b/spec/models/timelog_spec.rb
new file mode 100644
index 00000000000..ebc694213b6
--- /dev/null
+++ b/spec/models/timelog_spec.rb
@@ -0,0 +1,38 @@
+require 'rails_helper'
+
+RSpec.describe Timelog, type: :model do
+ subject { build(:timelog) }
+ let(:issue) { create(:issue) }
+ let(:merge_request) { create(:merge_request) }
+
+ it { is_expected.to be_valid }
+
+ it { is_expected.to validate_presence_of(:time_spent) }
+ it { is_expected.to validate_presence_of(:user) }
+
+ describe 'Issuable validation' do
+ it 'is invalid if issue_id and merge_request_id are missing' do
+ subject.attributes = { issue: nil, merge_request: nil }
+
+ expect(subject).to be_invalid
+ end
+
+ it 'is invalid if issue_id and merge_request_id are set' do
+ subject.attributes = { issue: issue, merge_request: merge_request }
+
+ expect(subject).to be_invalid
+ end
+
+ it 'is valid if only issue_id is set' do
+ subject.attributes = { issue: issue, merge_request: nil }
+
+ expect(subject).to be_valid
+ end
+
+ it 'is valid if only merge_request_id is set' do
+ subject.attributes = { merge_request: merge_request, issue: nil }
+
+ expect(subject).to be_valid
+ end
+ end
+end
diff --git a/spec/models/todo_spec.rb b/spec/models/todo_spec.rb
index 623b82c01d8..581305ad39f 100644
--- a/spec/models/todo_spec.rb
+++ b/spec/models/todo_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
describe Todo, models: true do
- let(:project) { create(:project) }
- let(:commit) { project.commit }
let(:issue) { create(:issue) }
describe 'relationships' do
@@ -82,6 +80,9 @@ describe Todo, models: true do
describe '#target' do
context 'for commits' do
+ let(:project) { create(:project, :repository) }
+ let(:commit) { project.commit }
+
it 'returns an instance of Commit when exists' do
subject.project = project
subject.target_type = 'Commit'
@@ -108,17 +109,20 @@ describe Todo, models: true do
end
describe '#target_reference' do
- it 'returns the short commit id for commits' do
+ it 'returns commit full reference with short id' do
+ project = create(:project, :repository)
+ commit = project.commit
+
subject.project = project
subject.target_type = 'Commit'
subject.commit_id = commit.id
- expect(subject.target_reference).to eq commit.short_id
+ expect(subject.target_reference).to eq commit.reference_link_text(full: true)
end
- it 'returns reference for issuables' do
+ it 'returns full reference for issuables' do
subject.target = issue
- expect(subject.target_reference).to eq issue.to_reference
+ expect(subject.target_reference).to eq issue.to_reference(full: true)
end
end
end
diff --git a/spec/models/tree_spec.rb b/spec/models/tree_spec.rb
index 0737999e125..a87983b7492 100644
--- a/spec/models/tree_spec.rb
+++ b/spec/models/tree_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Tree, models: true do
- let(:repository) { create(:project).repository }
+ let(:repository) { create(:project, :repository).repository }
let(:sha) { repository.root_ref }
subject { described_class.new(repository, '54fcc214') }
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 8b20ee81614..fe88ec63af6 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -23,9 +23,9 @@ describe User, models: true do
it { is_expected.to have_many(:recent_events).class_name('Event') }
it { is_expected.to have_many(:issues).dependent(:destroy) }
it { is_expected.to have_many(:notes).dependent(:destroy) }
- it { is_expected.to have_many(:assigned_issues).dependent(:destroy) }
+ it { is_expected.to have_many(:assigned_issues).dependent(:nullify) }
it { is_expected.to have_many(:merge_requests).dependent(:destroy) }
- it { is_expected.to have_many(:assigned_merge_requests).dependent(:destroy) }
+ it { is_expected.to have_many(:assigned_merge_requests).dependent(:nullify) }
it { is_expected.to have_many(:identities).dependent(:destroy) }
it { is_expected.to have_one(:abuse_report) }
it { is_expected.to have_many(:spam_logs).dependent(:destroy) }
@@ -48,7 +48,7 @@ describe User, models: true do
describe '#project_members' do
it 'does not include project memberships for which user is a requester' do
user = create(:user)
- project = create(:project, :public, :access_requestable)
+ project = create(:empty_project, :public, :access_requestable)
project.request_access(user)
expect(user.project_members).to be_empty
@@ -386,13 +386,15 @@ describe User, models: true do
describe 'projects' do
before do
- @user = create :user
- @project = create :project, namespace: @user.namespace
- @project_2 = create :project, group: create(:group) # Grant MASTER access to the user
- @project_3 = create :project, group: create(:group) # Grant DEVELOPER access to the user
+ @user = create(:user)
- @project_2.team << [@user, :master]
- @project_3.team << [@user, :developer]
+ @project = create(:empty_project, namespace: @user.namespace)
+ @project_2 = create(:empty_project, group: create(:group)) do |project|
+ project.add_master(@user)
+ end
+ @project_3 = create(:empty_project, group: create(:group)) do |project|
+ project.add_developer(@user)
+ end
end
it { expect(@user.authorized_projects).to include(@project) }
@@ -435,7 +437,7 @@ describe User, models: true do
describe 'namespaced' do
before do
@user = create :user
- @project = create :project, namespace: @user.namespace
+ @project = create(:empty_project, namespace: @user.namespace)
end
it { expect(@user.several_namespaces?).to be_falsey }
@@ -517,7 +519,7 @@ describe User, models: true do
before do
User.delete_all
@user = create :user
- @project = create :project
+ @project = create(:empty_project)
end
it { expect(User.not_in_project(@project)).to include(@user, @project.owner) }
@@ -795,14 +797,14 @@ describe User, models: true do
describe '#avatar_type' do
let(:user) { create(:user) }
- it "is 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 "is 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"])
+ expect(user.avatar_type).to eq(['only images allowed'])
end
end
@@ -924,11 +926,11 @@ describe User, models: true do
end
end
- describe "#starred?" do
- it "determines if user starred a project" do
+ describe '#starred?' do
+ it 'determines if user starred a project' do
user = create :user
- project1 = create :project, :public
- project2 = create :project, :public
+ project1 = create(:empty_project, :public)
+ project2 = create(:empty_project, :public)
expect(user.starred?(project1)).to be_falsey
expect(user.starred?(project2)).to be_falsey
@@ -951,10 +953,10 @@ describe User, models: true do
end
end
- describe "#toggle_star" do
- it "toggles stars" do
+ describe '#toggle_star' do
+ it 'toggles stars' do
user = create :user
- project = create :project, :public
+ project = create(:empty_project, :public)
expect(user.starred?(project)).to be_falsey
user.toggle_star(project)
@@ -964,42 +966,55 @@ describe User, models: true do
end
end
- describe "#sort" do
+ describe '#sort' do
before do
User.delete_all
@user = create :user, created_at: Date.today, last_sign_in_at: Date.today, name: 'Alpha'
@user1 = create :user, created_at: Date.today - 1, last_sign_in_at: Date.today - 1, name: 'Omega'
+ @user2 = create :user, created_at: Date.today - 2, last_sign_in_at: nil, name: 'Beta'
end
- it "sorts users by the recent sign-in time" do
- expect(User.sort('recent_sign_in').first).to eq(@user)
+ context 'when sort by recent_sign_in' do
+ it 'sorts users by the recent sign-in time' do
+ expect(User.sort('recent_sign_in').first).to eq(@user)
+ end
+
+ it 'pushes users who never signed in to the end' do
+ expect(User.sort('recent_sign_in').third).to eq(@user2)
+ end
end
- it "sorts users by the oldest sign-in time" do
- expect(User.sort('oldest_sign_in').first).to eq(@user1)
+ context 'when sort by oldest_sign_in' do
+ it 'sorts users by the oldest sign-in time' do
+ expect(User.sort('oldest_sign_in').first).to eq(@user1)
+ end
+
+ it 'pushes users who never signed in to the end' do
+ expect(User.sort('oldest_sign_in').third).to eq(@user2)
+ end
end
- it "sorts users in descending order by their creation time" do
+ it 'sorts users in descending order by their creation time' do
expect(User.sort('created_desc').first).to eq(@user)
end
- it "sorts users in ascending order by their creation time" do
- expect(User.sort('created_asc').first).to eq(@user1)
+ it 'sorts users in ascending order by their creation time' do
+ expect(User.sort('created_asc').first).to eq(@user2)
end
- it "sorts users by id in descending order when nil is passed" do
- expect(User.sort(nil).first).to eq(@user1)
+ it 'sorts users by id in descending order when nil is passed' do
+ expect(User.sort(nil).first).to eq(@user2)
end
end
describe "#contributed_projects" do
subject { create(:user) }
- let!(:project1) { create(:project) }
- let!(:project2) { create(:project, forked_from_project: project3) }
- let!(:project3) { create(:project) }
+ let!(:project1) { create(:empty_project) }
+ let!(:project2) { create(:empty_project, forked_from_project: project3) }
+ let!(:project3) { create(:empty_project) }
let!(:merge_request) { create(:merge_request, source_project: project2, target_project: project3, author: subject) }
- let!(:push_event) { create(:event, action: Event::PUSHED, project: project1, target: project1, author: subject) }
- let!(:merge_event) { create(:event, action: Event::CREATED, project: project3, target: merge_request, author: subject) }
+ let!(:push_event) { create(:event, :pushed, project: project1, target: project1, author: subject) }
+ let!(:merge_event) { create(:event, :created, project: project3, target: merge_request, author: subject) }
before do
project1.team << [subject, :master]
@@ -1038,12 +1053,12 @@ describe User, models: true do
describe "#recent_push" do
subject { create(:user) }
- let!(:project1) { create(:project) }
- let!(:project2) { create(:project, forked_from_project: project1) }
+ let!(:project1) { create(:project, :repository) }
+ let!(:project2) { create(:project, :repository, forked_from_project: project1) }
let!(:push_data) do
Gitlab::DataBuilder::Push.build_sample(project2, subject)
end
- let!(:push_event) { create(:event, action: Event::PUSHED, project: project2, target: project1, author: subject, data: push_data) }
+ let!(:push_event) { create(:event, :pushed, project: project2, target: project1, author: subject, data: push_data) }
before do
project1.team << [subject, :master]
@@ -1071,7 +1086,7 @@ describe User, models: true do
expect(subject.recent_push(project2)).to eq(push_event)
push_data1 = Gitlab::DataBuilder::Push.build_sample(project1, subject)
- push_event1 = create(:event, action: Event::PUSHED, project: project1, target: project1, author: subject, data: push_data1)
+ push_event1 = create(:event, :pushed, project: project1, target: project1, author: subject, data: push_data1)
expect(subject.recent_push([project1, project2])).to eq(push_event1) # Newest
end
@@ -1113,7 +1128,7 @@ describe User, models: true do
it "includes user's personal projects" do
user = create(:user)
- project = create(:project, :private, namespace: user.namespace)
+ project = create(:empty_project, :private, namespace: user.namespace)
expect(user.authorized_projects).to include(project)
end
@@ -1121,7 +1136,7 @@ describe User, models: true do
it "includes personal projects user has been given access to" do
user1 = create(:user)
user2 = create(:user)
- project = create(:project, :private, namespace: user1.namespace)
+ project = create(:empty_project, :private, namespace: user1.namespace)
project.team << [user2, Gitlab::Access::DEVELOPER]
@@ -1130,7 +1145,7 @@ describe User, models: true do
it "includes projects of groups user has been added to" do
group = create(:group)
- project = create(:project, group: group)
+ project = create(:empty_project, group: group)
user = create(:user)
group.add_developer(user)
@@ -1140,7 +1155,7 @@ describe User, models: true do
it "does not include projects of groups user has been removed from" do
group = create(:group)
- project = create(:project, group: group)
+ project = create(:empty_project, group: group)
user = create(:user)
member = group.add_developer(user)
@@ -1152,7 +1167,7 @@ describe User, models: true do
it "includes projects shared with user's group" do
user = create(:user)
- project = create(:project, :private)
+ project = create(:empty_project, :private)
group = create(:group)
group.add_reporter(user)
@@ -1164,7 +1179,7 @@ describe User, models: true do
it "does not include destroyed projects user had access to" do
user1 = create(:user)
user2 = create(:user)
- project = create(:project, :private, namespace: user1.namespace)
+ project = create(:empty_project, :private, namespace: user1.namespace)
project.team << [user2, Gitlab::Access::DEVELOPER]
expect(user2.authorized_projects).to include(project)
@@ -1175,7 +1190,7 @@ describe User, models: true do
it "does not include projects of destroyed groups user had access to" do
group = create(:group)
- project = create(:project, namespace: group)
+ project = create(:empty_project, namespace: group)
user = create(:user)
group.add_developer(user)
@@ -1190,14 +1205,9 @@ describe User, models: true do
let(:user) { create(:user) }
it 'includes projects for which the user access level is above or equal to reporter' do
- create(:project)
- reporter_project = create(:project)
- developer_project = create(:project)
- master_project = create(:project)
-
- reporter_project.team << [user, :reporter]
- developer_project.team << [user, :developer]
- master_project.team << [user, :master]
+ reporter_project = create(:empty_project) { |p| p.add_reporter(user) }
+ developer_project = create(:empty_project) { |p| p.add_developer(user) }
+ master_project = create(:empty_project) { |p| p.add_master(user) }
expect(user.projects_where_can_admin_issues.to_a).to eq([master_project, developer_project, reporter_project])
expect(user.can?(:admin_issue, master_project)).to eq(true)
@@ -1206,10 +1216,8 @@ describe User, models: true do
end
it 'does not include for which the user access level is below reporter' do
- project = create(:project)
- guest_project = create(:project)
-
- guest_project.team << [user, :guest]
+ project = create(:empty_project)
+ guest_project = create(:empty_project) { |p| p.add_guest(user) }
expect(user.projects_where_can_admin_issues.to_a).to be_empty
expect(user.can?(:admin_issue, guest_project)).to eq(false)
@@ -1217,15 +1225,14 @@ describe User, models: true do
end
it 'does not include archived projects' do
- project = create(:project)
- project.update_attributes(archived: true)
+ project = create(:empty_project, :archived)
expect(user.projects_where_can_admin_issues.to_a).to be_empty
expect(user.can?(:admin_issue, project)).to eq(false)
end
it 'does not include projects for which issues are disabled' do
- project = create(:project, issues_access_level: ProjectFeature::DISABLED)
+ project = create(:empty_project, :issues_disabled)
expect(user.projects_where_can_admin_issues.to_a).to be_empty
expect(user.can?(:admin_issue, project)).to eq(false)
@@ -1241,7 +1248,7 @@ describe User, models: true do
end
context 'without any projects' do
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project) }
it 'does not load' do
expect(user.ci_authorized_runners).to be_empty
@@ -1250,7 +1257,7 @@ describe User, models: true do
context 'with personal projects runners' do
let(:namespace) { create(:namespace, owner: user) }
- let(:project) { create(:project, namespace: namespace) }
+ let(:project) { create(:empty_project, namespace: namespace) }
it 'loads' do
expect(user.ci_authorized_runners).to contain_exactly(runner)
@@ -1281,7 +1288,7 @@ describe User, models: true do
context 'with groups projects runners' do
let(:group) { create(:group) }
- let(:project) { create(:project, group: group) }
+ let(:project) { create(:empty_project, group: group) }
def add_user(access)
group.add_user(user, access)
@@ -1291,7 +1298,7 @@ describe User, models: true do
end
context 'with other projects runners' do
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project) }
def add_user(access)
project.team << [user, access]
@@ -1321,8 +1328,8 @@ describe User, models: true do
end
describe '#projects_with_reporter_access_limited_to' do
- let(:project1) { create(:project) }
- let(:project2) { create(:project) }
+ let(:project1) { create(:empty_project) }
+ let(:project2) { create(:empty_project) }
let(:user) { create(:user) }
before do
@@ -1356,6 +1363,39 @@ describe User, models: true do
end
end
+ describe '#nested_groups' do
+ let!(:user) { create(:user) }
+ let!(:group) { create(:group) }
+ let!(:nested_group) { create(:group, parent: group) }
+
+ before do
+ group.add_owner(user)
+
+ # Add more data to ensure method does not include wrong groups
+ create(:group).add_owner(create(:user))
+ end
+
+ it { expect(user.nested_groups).to eq([nested_group]) }
+ end
+
+ describe '#nested_projects' do
+ let!(:user) { create(:user) }
+ let!(:group) { create(:group) }
+ let!(:nested_group) { create(:group, parent: group) }
+ let!(:project) { create(:empty_project, namespace: group) }
+ let!(:nested_project) { create(:empty_project, namespace: nested_group) }
+
+ before do
+ group.add_owner(user)
+
+ # Add more data to ensure method does not include wrong projects
+ other_project = create(:empty_project, namespace: create(:group, :nested))
+ other_project.add_developer(create(:user))
+ end
+
+ it { expect(user.nested_projects).to eq([nested_project]) }
+ end
+
describe '#refresh_authorized_projects', redis: true do
let(:project1) { create(:empty_project) }
let(:project2) { create(:empty_project) }
@@ -1382,4 +1422,37 @@ describe User, models: true do
expect(user.project_authorizations.where(access_level: Gitlab::Access::REPORTER).exists?).to eq(true)
end
end
+
+ describe '#access_level=' do
+ let(:user) { build(:user) }
+
+ it 'does nothing for an invalid access level' do
+ user.access_level = :invalid_access_level
+
+ expect(user.access_level).to eq(:regular)
+ expect(user.admin).to be false
+ end
+
+ it "assigns the 'admin' access level" do
+ user.access_level = :admin
+
+ expect(user.access_level).to eq(:admin)
+ expect(user.admin).to be true
+ end
+
+ it "doesn't clear existing access levels when an invalid access level is passed in" do
+ user.access_level = :admin
+ user.access_level = :invalid_access_level
+
+ expect(user.access_level).to eq(:admin)
+ expect(user.admin).to be true
+ end
+
+ it "accepts string values in addition to symbols" do
+ user.access_level = 'admin'
+
+ expect(user.access_level).to eq(:admin)
+ expect(user.admin).to be true
+ end
+ end
end
diff --git a/spec/policies/base_policy_spec.rb b/spec/policies/base_policy_spec.rb
new file mode 100644
index 00000000000..63acc0b68cd
--- /dev/null
+++ b/spec/policies/base_policy_spec.rb
@@ -0,0 +1,17 @@
+require 'spec_helper'
+
+describe BasePolicy, models: true do
+ let(:build) { Ci::Build.new }
+
+ describe '.class_for' do
+ it 'detects policy class based on the subject ancestors' do
+ expect(described_class.class_for(build)).to eq(Ci::BuildPolicy)
+ end
+
+ it 'detects policy class for a presented subject' do
+ presentee = Ci::BuildPresenter.new(build)
+
+ expect(described_class.class_for(presentee)).to eq(Ci::BuildPolicy)
+ end
+ end
+end
diff --git a/spec/policies/ci/build_policy_spec.rb b/spec/policies/ci/build_policy_spec.rb
new file mode 100644
index 00000000000..0f280f32eac
--- /dev/null
+++ b/spec/policies/ci/build_policy_spec.rb
@@ -0,0 +1,93 @@
+require 'spec_helper'
+
+describe Ci::BuildPolicy, :models do
+ let(:user) { create(:user) }
+ let(:build) { create(:ci_build, pipeline: pipeline) }
+ let(:pipeline) { create(:ci_empty_pipeline, project: project) }
+
+ let(:policies) do
+ described_class.abilities(user, build).to_set
+ end
+
+ shared_context 'public pipelines disabled' do
+ before { project.update_attribute(:public_builds, false) }
+ end
+
+ describe '#rules' do
+ context 'when user does not have access to the project' do
+ let(:project) { create(:empty_project, :private) }
+
+ context 'when public builds are enabled' do
+ it 'does not include ability to read build' do
+ expect(policies).not_to include :read_build
+ end
+ end
+
+ context 'when public builds are disabled' do
+ include_context 'public pipelines disabled'
+
+ it 'does not include ability to read build' do
+ expect(policies).not_to include :read_build
+ end
+ end
+ end
+
+ context 'when anonymous user has access to the project' do
+ let(:project) { create(:empty_project, :public) }
+
+ context 'when public builds are enabled' do
+ it 'includes ability to read build' do
+ expect(policies).to include :read_build
+ end
+ end
+
+ context 'when public builds are disabled' do
+ include_context 'public pipelines disabled'
+
+ it 'does not include ability to read build' do
+ expect(policies).not_to include :read_build
+ end
+ end
+ end
+
+ context 'when team member has access to the project' do
+ let(:project) { create(:empty_project, :public) }
+
+ context 'team member is a guest' do
+ before { project.team << [user, :guest] }
+
+ context 'when public builds are enabled' do
+ it 'includes ability to read build' do
+ expect(policies).to include :read_build
+ end
+ end
+
+ context 'when public builds are disabled' do
+ include_context 'public pipelines disabled'
+
+ it 'does not include ability to read build' do
+ expect(policies).not_to include :read_build
+ end
+ end
+ end
+
+ context 'team member is a reporter' do
+ before { project.team << [user, :reporter] }
+
+ context 'when public builds are enabled' do
+ it 'includes ability to read build' do
+ expect(policies).to include :read_build
+ end
+ end
+
+ context 'when public builds are disabled' do
+ include_context 'public pipelines disabled'
+
+ it 'does not include ability to read build' do
+ expect(policies).to include :read_build
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/policies/group_policy_spec.rb b/spec/policies/group_policy_spec.rb
index a20ac303a53..5c34ff04152 100644
--- a/spec/policies/group_policy_spec.rb
+++ b/spec/policies/group_policy_spec.rb
@@ -105,4 +105,70 @@ describe GroupPolicy, models: true do
is_expected.to include(*owner_permissions)
end
end
+
+ describe 'private nested group inherit permissions' do
+ let(:nested_group) { create(:group, :private, parent: group) }
+
+ subject { described_class.abilities(current_user, nested_group).to_set }
+
+ context 'with no user' do
+ let(:current_user) { nil }
+
+ it do
+ is_expected.not_to include(:read_group)
+ is_expected.not_to include(*master_permissions)
+ is_expected.not_to include(*owner_permissions)
+ end
+ end
+
+ context 'guests' do
+ let(:current_user) { guest }
+
+ it do
+ is_expected.to include(:read_group)
+ is_expected.not_to include(*master_permissions)
+ is_expected.not_to include(*owner_permissions)
+ end
+ end
+
+ context 'reporter' do
+ let(:current_user) { reporter }
+
+ it do
+ is_expected.to include(:read_group)
+ is_expected.not_to include(*master_permissions)
+ is_expected.not_to include(*owner_permissions)
+ end
+ end
+
+ context 'developer' do
+ let(:current_user) { developer }
+
+ it do
+ is_expected.to include(:read_group)
+ is_expected.not_to include(*master_permissions)
+ is_expected.not_to include(*owner_permissions)
+ end
+ end
+
+ context 'master' do
+ let(:current_user) { master }
+
+ it do
+ is_expected.to include(:read_group)
+ is_expected.to include(*master_permissions)
+ is_expected.not_to include(*owner_permissions)
+ end
+ end
+
+ context 'owner' do
+ let(:current_user) { owner }
+
+ it do
+ is_expected.to include(:read_group)
+ is_expected.to include(*master_permissions)
+ is_expected.to include(*owner_permissions)
+ end
+ end
+ end
end
diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb
index eeab9827d99..0a5edf35f59 100644
--- a/spec/policies/project_policy_spec.rb
+++ b/spec/policies/project_policy_spec.rb
@@ -10,61 +10,59 @@ describe ProjectPolicy, models: true do
let(:project) { create(:empty_project, :public, namespace: owner.namespace) }
let(:guest_permissions) do
- [
- :read_project, :read_board, :read_list, :read_wiki, :read_issue, :read_label,
- :read_milestone, :read_project_snippet, :read_project_member,
- :read_note, :create_project, :create_issue, :create_note,
- :upload_file
+ %i[
+ read_project read_board read_list read_wiki read_issue read_label
+ read_milestone read_project_snippet read_project_member
+ read_note create_project create_issue create_note
+ upload_file
]
end
let(:reporter_permissions) do
- [
- :download_code, :fork_project, :create_project_snippet, :update_issue,
- :admin_issue, :admin_label, :admin_list, :read_commit_status, :read_build,
- :read_container_image, :read_pipeline, :read_environment, :read_deployment,
- :read_merge_request, :download_wiki_code
+ %i[
+ download_code fork_project create_project_snippet update_issue
+ admin_issue admin_label admin_list read_commit_status read_build
+ read_container_image read_pipeline read_environment read_deployment
+ read_merge_request download_wiki_code
]
end
let(:team_member_reporter_permissions) do
- [
- :build_download_code, :build_read_container_image
- ]
+ %i[build_download_code build_read_container_image]
end
let(:developer_permissions) do
- [
- :admin_merge_request, :update_merge_request, :create_commit_status,
- :update_commit_status, :create_build, :update_build, :create_pipeline,
- :update_pipeline, :create_merge_request, :create_wiki, :push_code,
- :resolve_note, :create_container_image, :update_container_image,
- :create_environment, :create_deployment
+ %i[
+ admin_merge_request update_merge_request create_commit_status
+ update_commit_status create_build update_build create_pipeline
+ update_pipeline create_merge_request create_wiki push_code
+ resolve_note create_container_image update_container_image
+ create_environment create_deployment
]
end
let(:master_permissions) do
- [
- :push_code_to_protected_branches, :update_project_snippet, :update_environment,
- :update_deployment, :admin_milestone, :admin_project_snippet,
- :admin_project_member, :admin_note, :admin_wiki, :admin_project,
- :admin_commit_status, :admin_build, :admin_container_image,
- :admin_pipeline, :admin_environment, :admin_deployment
+ %i[
+ push_code_to_protected_branches update_project_snippet update_environment
+ update_deployment admin_milestone admin_project_snippet
+ admin_project_member admin_note admin_wiki admin_project
+ admin_commit_status admin_build admin_container_image
+ admin_pipeline admin_environment admin_deployment
]
end
let(:public_permissions) do
- [
- :download_code, :fork_project, :read_commit_status, :read_pipeline,
- :read_container_image, :build_download_code, :build_read_container_image,
- :download_wiki_code
+ %i[
+ download_code fork_project read_commit_status read_pipeline
+ read_container_image build_download_code build_read_container_image
+ download_wiki_code
]
end
let(:owner_permissions) do
- [
- :change_namespace, :change_visibility_level, :rename_project, :remove_project,
- :archive_project, :remove_fork_project, :destroy_merge_request, :destroy_issue
+ %i[
+ change_namespace change_visibility_level rename_project remove_project
+ archive_project remove_fork_project destroy_merge_request destroy_issue
]
end
diff --git a/spec/policies/project_snippet_policy_spec.rb b/spec/policies/project_snippet_policy_spec.rb
new file mode 100644
index 00000000000..d0758af57dd
--- /dev/null
+++ b/spec/policies/project_snippet_policy_spec.rb
@@ -0,0 +1,101 @@
+require 'spec_helper'
+
+describe ProjectSnippetPolicy, models: true do
+ let(:current_user) { create(:user) }
+
+ let(:author_permissions) do
+ [
+ :update_project_snippet,
+ :admin_project_snippet
+ ]
+ end
+
+ subject { described_class.abilities(current_user, project_snippet).to_set }
+
+ context 'public snippet' do
+ let(:project_snippet) { create(:project_snippet, :public) }
+
+ context 'no user' do
+ let(:current_user) { nil }
+
+ it do
+ is_expected.to include(:read_project_snippet)
+ is_expected.not_to include(*author_permissions)
+ end
+ end
+
+ context 'regular user' do
+ it do
+ is_expected.to include(:read_project_snippet)
+ is_expected.not_to include(*author_permissions)
+ end
+ end
+ end
+
+ context 'internal snippet' do
+ let(:project_snippet) { create(:project_snippet, :internal) }
+
+ context 'no user' do
+ let(:current_user) { nil }
+
+ it do
+ is_expected.not_to include(:read_project_snippet)
+ is_expected.not_to include(*author_permissions)
+ end
+ end
+
+ context 'regular user' do
+ it do
+ is_expected.to include(:read_project_snippet)
+ is_expected.not_to include(*author_permissions)
+ end
+ end
+ end
+
+ context 'private snippet' do
+ let(:project_snippet) { create(:project_snippet, :private) }
+
+ context 'no user' do
+ let(:current_user) { nil }
+
+ it do
+ is_expected.not_to include(:read_project_snippet)
+ is_expected.not_to include(*author_permissions)
+ end
+ end
+
+ context 'regular user' do
+ it do
+ is_expected.not_to include(:read_project_snippet)
+ is_expected.not_to include(*author_permissions)
+ end
+ end
+
+ context 'snippet author' do
+ let(:project_snippet) { create(:project_snippet, :private, author: current_user) }
+
+ it do
+ is_expected.to include(:read_project_snippet)
+ is_expected.to include(*author_permissions)
+ end
+ end
+
+ context 'project team member' do
+ before { project_snippet.project.team << [current_user, :developer] }
+
+ it do
+ is_expected.to include(:read_project_snippet)
+ is_expected.not_to include(*author_permissions)
+ end
+ end
+
+ context 'admin user' do
+ let(:current_user) { create(:admin) }
+
+ it do
+ is_expected.to include(:read_project_snippet)
+ is_expected.to include(*author_permissions)
+ end
+ end
+ end
+end
diff --git a/spec/presenters/ci/build_presenter_spec.rb b/spec/presenters/ci/build_presenter_spec.rb
new file mode 100644
index 00000000000..7a35da38b2b
--- /dev/null
+++ b/spec/presenters/ci/build_presenter_spec.rb
@@ -0,0 +1,77 @@
+require 'spec_helper'
+
+describe Ci::BuildPresenter do
+ let(:project) { create(:empty_project) }
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+ let(:build) { create(:ci_build, pipeline: pipeline) }
+
+ subject(:presenter) do
+ described_class.new(build)
+ end
+
+ it 'inherits from Gitlab::View::Presenter::Delegated' do
+ expect(described_class.superclass).to eq(Gitlab::View::Presenter::Delegated)
+ end
+
+ describe '#initialize' do
+ it 'takes a build and optional params' do
+ expect { presenter }.not_to raise_error
+ end
+
+ it 'exposes build' do
+ expect(presenter.build).to eq(build)
+ end
+
+ it 'forwards missing methods to build' do
+ expect(presenter.ref).to eq('master')
+ end
+ end
+
+ describe '#erased_by_user?' do
+ it 'takes a build and optional params' do
+ expect(presenter).not_to be_erased_by_user
+ end
+ end
+
+ describe '#erased_by_name' do
+ context 'when build is not erased' do
+ before do
+ expect(presenter).to receive(:erased_by_user?).and_return(false)
+ end
+
+ it 'returns nil' do
+ expect(presenter.erased_by_name).to be_nil
+ end
+ end
+
+ context 'when build is erased' do
+ before do
+ expect(presenter).to receive(:erased_by_user?).and_return(true)
+ expect(build).to receive(:erased_by).
+ and_return(double(:user, name: 'John Doe'))
+ end
+
+ it 'returns the name of the eraser' do
+ expect(presenter.erased_by_name).to eq('John Doe')
+ end
+ end
+ end
+
+ describe 'quack like a Ci::Build permission-wise' do
+ context 'user is not allowed' do
+ let(:project) { build_stubbed(:empty_project, public_builds: false) }
+
+ it 'returns false' do
+ expect(presenter.can?(nil, :read_build)).to be_falsy
+ end
+ end
+
+ context 'user is allowed' do
+ let(:project) { build_stubbed(:empty_project, :public) }
+
+ it 'returns true' do
+ expect(presenter.can?(nil, :read_build)).to be_truthy
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/access_requests_spec.rb b/spec/requests/api/access_requests_spec.rb
index 1a771b3c87a..e487297748b 100644
--- a/spec/requests/api/access_requests_spec.rb
+++ b/spec/requests/api/access_requests_spec.rb
@@ -9,7 +9,7 @@ describe API::AccessRequests, api: true do
let(:stranger) { create(:user) }
let(:project) do
- create(:project, :public, :access_requestable, creator_id: master.id, namespace: master.namespace) do |project|
+ create(:empty_project, :public, :access_requestable, creator_id: master.id, namespace: master.namespace) do |project|
project.team << [developer, :developer]
project.team << [master, :master]
project.request_access(access_requester)
diff --git a/spec/requests/api/boards_spec.rb b/spec/requests/api/boards_spec.rb
index 3019724f52e..c14c3cb1ce7 100644
--- a/spec/requests/api/boards_spec.rb
+++ b/spec/requests/api/boards_spec.rb
@@ -8,7 +8,7 @@ describe API::Boards, api: true do
let(:non_member) { create(:user) }
let(:guest) { create(:user) }
let(:admin) { create(:user, :admin) }
- let!(:project) { create(:project, :public, creator_id: user.id, namespace: user.namespace ) }
+ let!(:project) { create(:empty_project, :public, creator_id: user.id, namespace: user.namespace ) }
let!(:dev_label) do
create(:label, title: 'Development', color: '#FFAABB', project: project)
@@ -188,7 +188,7 @@ describe API::Boards, api: true do
context "when the user is project owner" do
let(:owner) { create(:user) }
- let(:project) { create(:project, namespace: owner.namespace) }
+ let(:project) { create(:empty_project, namespace: owner.namespace) }
it "deletes the list if an admin requests it" do
delete api("#{base_url}/#{dev_list.id}", owner)
diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb
index 2878e0cb59b..5a3ffc284f2 100644
--- a/spec/requests/api/branches_spec.rb
+++ b/spec/requests/api/branches_spec.rb
@@ -6,7 +6,7 @@ describe API::Branches, api: true do
let(:user) { create(:user) }
let(:user2) { create(:user) }
- let!(:project) { create(:project, creator_id: user.id) }
+ let!(:project) { create(:project, :repository, creator: user) }
let!(:master) { create(:project_member, :master, user: user, project: project) }
let!(:guest) { create(:project_member, :guest, user: user2, project: project) }
let!(:branch_name) { 'feature' }
diff --git a/spec/requests/api/builds_spec.rb b/spec/requests/api/builds_spec.rb
index 7be7acebb19..834c4e52693 100644
--- a/spec/requests/api/builds_spec.rb
+++ b/spec/requests/api/builds_spec.rb
@@ -5,7 +5,7 @@ describe API::Builds, api: true do
let(:user) { create(:user) }
let(:api_user) { user }
- let!(:project) { create(:project, creator_id: user.id, public_builds: false) }
+ let!(:project) { create(:project, :repository, creator: user, public_builds: false) }
let!(:developer) { create(:project_member, :developer, user: user, project: project) }
let(:reporter) { create(:project_member, :reporter, project: project) }
let(:guest) { create(:project_member, :guest, project: project) }
@@ -67,7 +67,7 @@ describe API::Builds, api: true do
context 'unauthorized user' do
let(:api_user) { nil }
- it 'should not return project builds' do
+ it 'does not return project builds' do
expect(response).to have_http_status(401)
end
end
@@ -86,7 +86,7 @@ describe API::Builds, api: true do
context 'when commit exists in repository' do
context 'when user is authorized' do
- context 'when pipeline has builds' do
+ context 'when pipeline has jobs' do
before do
create(:ci_pipeline, project: project, sha: project.commit.id)
create(:ci_build, pipeline: pipeline)
@@ -95,7 +95,7 @@ describe API::Builds, api: true do
get api("/projects/#{project.id}/repository/commits/#{project.commit.id}/builds", api_user)
end
- it 'returns project builds for specific commit' do
+ it 'returns project jobs for specific commit' do
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.size).to eq 2
@@ -111,7 +111,7 @@ describe API::Builds, api: true do
end
end
- context 'when pipeline has no builds' do
+ context 'when pipeline has no jobs' do
before do
branch_head = project.commit('feature').id
get api("/projects/#{project.id}/repository/commits/#{branch_head}/builds", api_user)
@@ -133,7 +133,7 @@ describe API::Builds, api: true do
get api("/projects/#{project.id}/repository/commits/#{project.commit.id}/builds", nil)
end
- it 'does not return project builds' do
+ it 'does not return project jobs' do
expect(response).to have_http_status(401)
expect(json_response.except('message')).to be_empty
end
@@ -147,7 +147,7 @@ describe API::Builds, api: true do
end
context 'authorized user' do
- it 'returns specific build data' do
+ it 'returns specific job data' do
expect(response).to have_http_status(200)
expect(json_response['name']).to eq('test')
end
@@ -165,7 +165,7 @@ describe API::Builds, api: true do
context 'unauthorized user' do
let(:api_user) { nil }
- it 'does not return specific build data' do
+ it 'does not return specific job data' do
expect(response).to have_http_status(401)
end
end
@@ -176,7 +176,7 @@ describe API::Builds, api: true do
get api("/projects/#{project.id}/builds/#{build.id}/artifacts", api_user)
end
- context 'build with artifacts' do
+ context 'job with artifacts' do
let(:build) { create(:ci_build, :artifacts, pipeline: pipeline) }
context 'authorized user' do
@@ -185,22 +185,23 @@ describe API::Builds, api: true do
'Content-Disposition' => 'attachment; filename=ci_build_artifacts.zip' }
end
- it 'returns specific build artifacts' do
+ it 'returns specific job artifacts' do
expect(response).to have_http_status(200)
expect(response.headers).to include(download_headers)
+ expect(response.body).to match_file(build.artifacts_file.file.file)
end
end
context 'unauthorized user' do
let(:api_user) { nil }
- it 'does not return specific build artifacts' do
+ it 'does not return specific job artifacts' do
expect(response).to have_http_status(401)
end
end
end
- it 'does not return build artifacts if not uploaded' do
+ it 'does not return job artifacts if not uploaded' do
expect(response).to have_http_status(404)
end
end
@@ -241,7 +242,7 @@ describe API::Builds, api: true do
end
end
- context 'non-existing build' do
+ context 'non-existing job' do
shared_examples 'not found' do
it { expect(response).to have_http_status(:not_found) }
end
@@ -254,7 +255,7 @@ describe API::Builds, api: true do
it_behaves_like 'not found'
end
- context 'has no such build' do
+ context 'has no such job' do
before do
get path_for_ref(pipeline.ref, 'NOBUILD')
end
@@ -263,7 +264,7 @@ describe API::Builds, api: true do
end
end
- context 'find proper build' do
+ context 'find proper job' do
shared_examples 'a valid file' do
let(:download_headers) do
{ 'Content-Transfer-Encoding' => 'binary',
@@ -311,7 +312,7 @@ describe API::Builds, api: true do
end
context 'authorized user' do
- it 'returns specific build trace' do
+ it 'returns specific job trace' do
expect(response).to have_http_status(200)
expect(response.body).to eq(build.trace)
end
@@ -320,7 +321,7 @@ describe API::Builds, api: true do
context 'unauthorized user' do
let(:api_user) { nil }
- it 'does not return specific build trace' do
+ it 'does not return specific job trace' do
expect(response).to have_http_status(401)
end
end
@@ -333,7 +334,7 @@ describe API::Builds, api: true do
context 'authorized user' do
context 'user with :update_build persmission' do
- it 'cancels running or pending build' do
+ it 'cancels running or pending job' do
expect(response).to have_http_status(201)
expect(project.builds.first.status).to eq('canceled')
end
@@ -342,7 +343,7 @@ describe API::Builds, api: true do
context 'user without :update_build permission' do
let(:api_user) { reporter.user }
- it 'does not cancel build' do
+ it 'does not cancel job' do
expect(response).to have_http_status(403)
end
end
@@ -351,7 +352,7 @@ describe API::Builds, api: true do
context 'unauthorized user' do
let(:api_user) { nil }
- it 'does not cancel build' do
+ it 'does not cancel job' do
expect(response).to have_http_status(401)
end
end
@@ -366,7 +367,7 @@ describe API::Builds, api: true do
context 'authorized user' do
context 'user with :update_build permission' do
- it 'retries non-running build' do
+ it 'retries non-running job' do
expect(response).to have_http_status(201)
expect(project.builds.first.status).to eq('canceled')
expect(json_response['status']).to eq('pending')
@@ -376,7 +377,7 @@ describe API::Builds, api: true do
context 'user without :update_build permission' do
let(:api_user) { reporter.user }
- it 'does not retry build' do
+ it 'does not retry job' do
expect(response).to have_http_status(403)
end
end
@@ -385,7 +386,7 @@ describe API::Builds, api: true do
context 'unauthorized user' do
let(:api_user) { nil }
- it 'does not retry build' do
+ it 'does not retry job' do
expect(response).to have_http_status(401)
end
end
@@ -396,23 +397,23 @@ describe API::Builds, api: true do
post api("/projects/#{project.id}/builds/#{build.id}/erase", user)
end
- context 'build is erasable' do
+ context 'job is erasable' do
let(:build) { create(:ci_build, :trace, :artifacts, :success, project: project, pipeline: pipeline) }
- it 'erases build content' do
+ it 'erases job content' do
expect(response.status).to eq 201
expect(build.trace).to be_empty
expect(build.artifacts_file.exists?).to be_falsy
expect(build.artifacts_metadata.exists?).to be_falsy
end
- it 'updates build' do
+ it 'updates job' do
expect(build.reload.erased_at).to be_truthy
expect(build.reload.erased_by).to eq user
end
end
- context 'build is not erasable' do
+ context 'job is not erasable' do
let(:build) { create(:ci_build, :trace, project: project, pipeline: pipeline) }
it 'responds with forbidden' do
@@ -452,20 +453,20 @@ describe API::Builds, api: true do
post api("/projects/#{project.id}/builds/#{build.id}/play", user)
end
- context 'on an playable build' do
+ context 'on an playable job' do
let(:build) { create(:ci_build, :manual, project: project, pipeline: pipeline) }
- it 'plays the build' do
+ it 'plays the job' do
expect(response).to have_http_status 200
expect(json_response['user']['id']).to eq(user.id)
expect(json_response['id']).to eq(build.id)
end
end
- context 'on a non-playable build' do
+ context 'on a non-playable job' do
it 'returns a status code 400, Bad Request' do
expect(response).to have_http_status 400
- expect(response.body).to match("Unplayable Build")
+ expect(response.body).to match("Unplayable Job")
end
end
end
diff --git a/spec/requests/api/commit_statuses_spec.rb b/spec/requests/api/commit_statuses_spec.rb
index 335efc4db6c..88361def3cf 100644
--- a/spec/requests/api/commit_statuses_spec.rb
+++ b/spec/requests/api/commit_statuses_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe API::CommitStatuses, api: true do
include ApiHelpers
- let!(:project) { create(:project) }
+ let!(:project) { create(:project, :repository) }
let(:commit) { project.repository.commit }
let(:commit_status) { create(:commit_status, pipeline: pipeline) }
let(:guest) { create_user(:guest) }
@@ -152,8 +152,11 @@ describe API::CommitStatuses, api: true do
context 'with all optional parameters' do
before do
- optional_params = { state: 'success', context: 'coverage',
- ref: 'develop', target_url: 'url', description: 'test' }
+ optional_params = { state: 'success',
+ context: 'coverage',
+ ref: 'develop',
+ description: 'test',
+ target_url: 'http://gitlab.com/status' }
post api(post_url, developer), optional_params
end
@@ -164,12 +167,12 @@ describe API::CommitStatuses, api: true do
expect(json_response['status']).to eq('success')
expect(json_response['name']).to eq('coverage')
expect(json_response['ref']).to eq('develop')
- expect(json_response['target_url']).to eq('url')
expect(json_response['description']).to eq('test')
+ expect(json_response['target_url']).to eq('http://gitlab.com/status')
end
end
- context 'invalid status' do
+ context 'when status is invalid' do
before { post api(post_url, developer), state: 'invalid' }
it 'does not create commit status' do
@@ -177,7 +180,7 @@ describe API::CommitStatuses, api: true do
end
end
- context 'request without state' do
+ context 'when request without a state made' do
before { post api(post_url, developer) }
it 'does not create commit status' do
@@ -185,7 +188,7 @@ describe API::CommitStatuses, api: true do
end
end
- context 'invalid commit' do
+ context 'when commit SHA is invalid' do
let(:sha) { 'invalid_sha' }
before { post api(post_url, developer), state: 'running' }
@@ -193,6 +196,19 @@ describe API::CommitStatuses, api: true do
expect(response).to have_http_status(404)
end
end
+
+ context 'when target URL is an invalid address' do
+ before do
+ post api(post_url, developer), state: 'pending',
+ target_url: 'invalid url'
+ end
+
+ it 'responds with bad request status and validation errors' do
+ expect(response).to have_http_status(400)
+ expect(json_response['message']['target_url'])
+ .to include 'must be a valid URL'
+ end
+ end
end
context 'reporter user' do
diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb
index 964cded917c..af9028a8978 100644
--- a/spec/requests/api/commits_spec.rb
+++ b/spec/requests/api/commits_spec.rb
@@ -5,7 +5,7 @@ describe API::Commits, api: true do
include ApiHelpers
let(:user) { create(:user) }
let(:user2) { create(:user) }
- let!(:project) { create(:project, creator_id: user.id, namespace: user.namespace) }
+ let!(:project) { create(:project, :repository, creator: user, namespace: user.namespace) }
let!(:master) { create(:project_member, :master, user: user, project: project) }
let!(:guest) { create(:project_member, :guest, user: user2, project: project) }
let!(:note) { create(:note_on_commit, author: user, project: project, commit_id: project.repository.commit.id, note: 'a comment on a commit') }
@@ -146,6 +146,16 @@ describe API::Commits, api: true do
expect(response).to have_http_status(400)
end
+
+ context 'with project path in URL' do
+ let(:url) { "/projects/#{project.namespace.path}%2F#{project.path}/repository/commits" }
+
+ it 'a new file in project repo' do
+ post api(url, user), valid_c_params
+
+ expect(response).to have_http_status(201)
+ end
+ end
end
context :delete do
diff --git a/spec/requests/api/deploy_keys_spec.rb b/spec/requests/api/deploy_keys_spec.rb
index aabab8e6ae6..766234d7104 100644
--- a/spec/requests/api/deploy_keys_spec.rb
+++ b/spec/requests/api/deploy_keys_spec.rb
@@ -5,8 +5,8 @@ describe API::DeployKeys, api: true do
let(:user) { create(:user) }
let(:admin) { create(:admin) }
- let(:project) { create(:project, creator_id: user.id) }
- let(:project2) { create(:project, creator_id: user.id) }
+ let(:project) { create(:empty_project, creator_id: user.id) }
+ let(:project2) { create(:empty_project, creator_id: user.id) }
let(:deploy_key) { create(:deploy_key, public: true) }
let!(:deploy_keys_project) do
@@ -73,19 +73,14 @@ describe API::DeployKeys, api: true 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 invalid'
- ])
+ expect(json_response['error']).to eq('key is missing')
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'
- ])
+ expect(json_response['error']).to eq('title is missing')
end
it 'should create new ssh key' do
diff --git a/spec/requests/api/environments_spec.rb b/spec/requests/api/environments_spec.rb
index b9d535bc314..8168b613766 100644
--- a/spec/requests/api/environments_spec.rb
+++ b/spec/requests/api/environments_spec.rb
@@ -5,7 +5,7 @@ describe API::Environments, api: true do
let(:user) { create(:user) }
let(:non_member) { create(:user) }
- let(:project) { create(:project, :private, namespace: user.namespace) }
+ let(:project) { create(:empty_project, :private, namespace: user.namespace) }
let!(:environment) { create(:environment, project: project) }
before do
diff --git a/spec/requests/api/files_spec.rb b/spec/requests/api/files_spec.rb
index 685da28c673..5e26e779366 100644
--- a/spec/requests/api/files_spec.rb
+++ b/spec/requests/api/files_spec.rb
@@ -3,8 +3,8 @@ require 'spec_helper'
describe API::Files, api: true do
include ApiHelpers
let(:user) { create(:user) }
- let!(:project) { create(:project, namespace: user.namespace ) }
- let(:guest) { create(:user).tap { |u| create(:project_member, :guest, user: u, project: project) } }
+ let!(:project) { create(:project, :repository, namespace: user.namespace ) }
+ let(:guest) { create(:user) { |u| project.add_guest(u) } }
let(:file_path) { 'files/ruby/popen.rb' }
let(:params) do
{
diff --git a/spec/requests/api/fork_spec.rb b/spec/requests/api/fork_spec.rb
index e38d5745d44..92ac4fd334d 100644
--- a/spec/requests/api/fork_spec.rb
+++ b/spec/requests/api/fork_spec.rb
@@ -1,10 +1,9 @@
require 'spec_helper'
-describe API::API, api: true do
+describe API::Projects, api: true do
include ApiHelpers
let(:user) { create(:user) }
let(:user2) { create(:user) }
- let(:user3) { create(:user) }
let(:admin) { create(:admin) }
let(:group) { create(:group) }
let(:group2) do
@@ -13,17 +12,14 @@ describe API::API, api: true do
group
end
- let(:project) do
- create(:project, creator_id: user.id, namespace: user.namespace)
- end
-
- let(:project_user2) do
- create(:project_member, :reporter, user: user2, project: project)
- end
-
describe 'POST /projects/fork/:id' do
- before { project_user2 }
- before { user3 }
+ let(:project) do
+ create(:project, :repository, creator: user, namespace: user.namespace)
+ end
+
+ before do
+ project.add_reporter(user2)
+ end
context 'when authenticated' do
it 'forks if user has sufficient access to project' do
@@ -49,7 +45,8 @@ describe API::API, api: true do
end
it 'fails on missing project access for the project to fork' do
- post api("/projects/fork/#{project.id}", user3)
+ new_user = create(:user)
+ post api("/projects/fork/#{project.id}", new_user)
expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 Project Not Found')
diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb
index cdeb965b413..a027c23bb88 100644
--- a/spec/requests/api/groups_spec.rb
+++ b/spec/requests/api/groups_spec.rb
@@ -10,9 +10,9 @@ describe API::Groups, api: true do
let(:admin) { create(:admin) }
let!(:group1) { create(:group, avatar: File.open(uploaded_image_temp_path)) }
let!(:group2) { create(:group, :private) }
- let!(:project1) { create(:project, namespace: group1) }
- let!(:project2) { create(:project, namespace: group2) }
- let!(:project3) { create(:project, namespace: group1, path: 'test', visibility_level: Gitlab::VisibilityLevel::PRIVATE) }
+ let!(:project1) { create(:empty_project, namespace: group1) }
+ let!(:project2) { create(:empty_project, namespace: group2) }
+ let!(:project3) { create(:empty_project, namespace: group1, path: 'test', visibility_level: Gitlab::VisibilityLevel::PRIVATE) }
before do
group1.add_owner(user1)
@@ -23,6 +23,7 @@ describe API::Groups, api: true do
context "when unauthenticated" do
it "returns authentication error" do
get api("/groups")
+
expect(response).to have_http_status(401)
end
end
@@ -30,20 +31,55 @@ describe API::Groups, api: true do
context "when authenticated as user" 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
expect(json_response.length).to eq(1)
expect(json_response.first['name']).to eq(group1.name)
end
+
+ it "does not include statistics" do
+ get api("/groups", user1), statistics: true
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first).not_to include 'statistics'
+ end
end
context "when authenticated as admin" 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
expect(json_response.length).to eq(2)
end
+
+ it "does not include statistics by default" do
+ get api("/groups", admin)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first).not_to include('statistics')
+ end
+
+ it "includes statistics if requested" do
+ attributes = {
+ storage_size: 702,
+ repository_size: 123,
+ lfs_objects_size: 234,
+ build_artifacts_size: 345,
+ }
+
+ project1.statistics.update!(attributes)
+
+ get api("/groups", admin), statistics: true
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first['statistics']).to eq attributes.stringify_keys
+ end
end
context "when using skip_groups in request" do
@@ -61,6 +97,7 @@ describe API::Groups, api: true do
it "returns all groups you have access to" do
public_group = create :group, :public
+
get api("/groups", user1), all_available: true
expect(response).to have_http_status(200)
@@ -107,6 +144,7 @@ describe API::Groups, api: true do
context 'when unauthenticated' do
it 'returns authentication error' do
get api('/groups/owned')
+
expect(response).to have_http_status(401)
end
end
@@ -114,6 +152,7 @@ describe API::Groups, api: true do
context 'when authenticated as group owner' do
it 'returns an array of groups the user owns' do
get api('/groups/owned', user2)
+
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first['name']).to eq(group2.name)
@@ -124,7 +163,7 @@ describe API::Groups, api: true do
describe "GET /groups/:id" do
context "when authenticated as user" do
it "returns one of user1's groups" do
- project = create(:project, namespace: group2, path: 'Foo')
+ project = create(:empty_project, namespace: group2, path: 'Foo')
create(:project_group_link, project: project, group: group1)
get api("/groups/#{group1.id}", user1)
@@ -137,6 +176,9 @@ describe API::Groups, api: true do
expect(json_response['visibility_level']).to eq(group1.visibility_level)
expect(json_response['avatar_url']).to eq(group1.avatar_url)
expect(json_response['web_url']).to eq(group1.web_url)
+ expect(json_response['request_access_enabled']).to eq(group1.request_access_enabled)
+ expect(json_response['full_name']).to eq(group1.full_name)
+ expect(json_response['full_path']).to eq(group1.full_path)
expect(json_response['projects']).to be_an Array
expect(json_response['projects'].length).to eq(2)
expect(json_response['shared_projects']).to be_an Array
@@ -146,6 +188,7 @@ describe API::Groups, api: true do
it "does not return a non existing group" do
get api("/groups/1328", user1)
+
expect(response).to have_http_status(404)
end
@@ -159,12 +202,14 @@ describe API::Groups, api: true do
context "when authenticated as admin" 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 "does not return a non existing group" do
get api("/groups/1328", admin)
+
expect(response).to have_http_status(404)
end
end
@@ -172,12 +217,14 @@ describe API::Groups, api: true do
context 'when using group path in URL' 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 'does not return a non existing group' do
get api('/groups/unknown', admin)
+
expect(response).to have_http_status(404)
end
@@ -243,7 +290,7 @@ describe API::Groups, api: true do
expect(json_response.length).to eq(2)
project_names = json_response.map { |proj| proj['name' ] }
expect(project_names).to match_array([project1.name, project3.name])
- expect(json_response.first['default_branch']).to be_present
+ expect(json_response.first['visibility_level']).to be_present
end
it "returns the group's projects with simple representation" do
@@ -253,11 +300,11 @@ describe API::Groups, api: true do
expect(json_response.length).to eq(2)
project_names = json_response.map { |proj| proj['name' ] }
expect(project_names).to match_array([project1.name, project3.name])
- expect(json_response.first['default_branch']).not_to be_present
+ expect(json_response.first['visibility_level']).not_to be_present
end
it 'filters the groups projects' do
- public_project = create(:project, :public, path: 'test1', group: group1)
+ public_project = create(:empty_project, :public, path: 'test1', group: group1)
get api("/groups/#{group1.id}/projects", user1), visibility: 'public'
@@ -269,6 +316,7 @@ describe API::Groups, api: true do
it "does not return a non existing group" do
get api("/groups/1328/projects", user1)
+
expect(response).to have_http_status(404)
end
@@ -278,7 +326,7 @@ describe API::Groups, api: true do
expect(response).to have_http_status(404)
end
- it "should only return projects to which user has access" do
+ it "only returns projects to which user has access" do
project3.team << [user3, :developer]
get api("/groups/#{group1.id}/projects", user3)
@@ -290,21 +338,23 @@ describe API::Groups, api: true do
end
context "when authenticated as admin" do
- it "should return any existing group" do
+ it "returns any existing group" do
get api("/groups/#{group2.id}/projects", admin)
+
expect(response).to have_http_status(200)
expect(json_response.length).to eq(1)
expect(json_response.first['name']).to eq(project2.name)
end
- it "should not return a non existing group" do
+ it "does not return a non existing group" do
get api("/groups/1328/projects", admin)
+
expect(response).to have_http_status(404)
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}/projects", admin)
expect(response).to have_http_status(200)
@@ -314,6 +364,7 @@ describe API::Groups, api: true do
it 'does not return a non existing group' do
get api('/groups/unknown/projects', admin)
+
expect(response).to have_http_status(404)
end
@@ -329,6 +380,7 @@ describe API::Groups, api: true do
context "when authenticated as user without group permissions" do
it "does not create group" do
post api("/groups", user1), attributes_for(:group)
+
expect(response).to have_http_status(403)
end
end
@@ -338,6 +390,7 @@ describe API::Groups, api: true do
group = attributes_for(:group, { request_access_enabled: false })
post api("/groups", user3), group
+
expect(response).to have_http_status(201)
expect(json_response["name"]).to eq(group[:name])
@@ -347,17 +400,20 @@ describe API::Groups, api: true 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 "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 "returns 400 bad request error if path not given" do
post api("/groups", user3), { name: 'test' }
+
expect(response).to have_http_status(400)
end
end
@@ -367,18 +423,22 @@ describe API::Groups, api: true do
context "when authenticated as user" do
it "removes group" do
delete api("/groups/#{group1.id}", user1)
+
expect(response).to have_http_status(200)
end
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 "does not remove a non existing group" do
delete api("/groups/1328", user1)
+
expect(response).to have_http_status(404)
end
@@ -392,27 +452,31 @@ describe API::Groups, api: true do
context "when authenticated as admin" do
it "removes any existing group" do
delete api("/groups/#{group2.id}", admin)
+
expect(response).to have_http_status(200)
end
it "does not remove a non existing group" do
delete api("/groups/1328", admin)
+
expect(response).to have_http_status(404)
end
end
end
describe "POST /groups/:id/projects/:project_id" do
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project) }
+ let(:project_path) { "#{project.namespace.path}%2F#{project.path}" }
+
before(:each) do
allow_any_instance_of(Projects::TransferService).
to receive(:execute).and_return(true)
- allow(Project).to receive(:find).and_return(project)
end
context "when authenticated as user" 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
@@ -420,8 +484,45 @@ describe API::Groups, api: true do
context "when authenticated as admin" do
it "transfers project to group" do
post api("/groups/#{group1.id}/projects/#{project.id}", admin)
+
expect(response).to have_http_status(201)
end
+
+ context 'when using project path in URL' do
+ context 'with a valid project path' do
+ it "transfers project to group" do
+ post api("/groups/#{group1.id}/projects/#{project_path}", admin)
+
+ expect(response).to have_http_status(201)
+ end
+ end
+
+ context 'with a non-existent project path' do
+ it "does not transfer project to group" do
+ post api("/groups/#{group1.id}/projects/nogroup%2Fnoproject", admin)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ context 'when using a group path in URL' do
+ context 'with a valid group path' do
+ it "transfers project to group" do
+ post api("/groups/#{group1.path}/projects/#{project_path}", admin)
+
+ expect(response).to have_http_status(201)
+ end
+ end
+
+ context 'with a non-existent group path' do
+ it "does not transfer project to group" do
+ post api("/groups/noexist/projects/#{project_path}", admin)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
end
end
end
diff --git a/spec/requests/api/helpers_spec.rb b/spec/requests/api/helpers_spec.rb
index c3d7ac3eef8..a89676fec93 100644
--- a/spec/requests/api/helpers_spec.rb
+++ b/spec/requests/api/helpers_spec.rb
@@ -12,6 +12,7 @@ describe API::Helpers, api: true do
let(:params) { {} }
let(:env) { { 'REQUEST_METHOD' => 'GET' } }
let(:request) { Rack::Request.new(env) }
+ let(:header) { }
def set_env(user_or_token, identifier)
clear_env
@@ -46,7 +47,7 @@ describe API::Helpers, api: true do
allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ value }
end
- def error!(message, status)
+ def error!(message, status, header)
raise Exception.new("#{status} - #{message}")
end
@@ -396,7 +397,7 @@ describe API::Helpers, api: true do
%w[HEAD GET].each do |method_name|
context "method is #{method_name}" do
before do
- expect_any_instance_of(self.class).to receive(:route).and_return(double(route_method: method_name))
+ expect_any_instance_of(self.class).to receive(:route).and_return(double(request_method: method_name))
end
it 'does not raise an error' do
@@ -410,7 +411,7 @@ describe API::Helpers, api: true do
%w[POST PUT PATCH DELETE].each do |method_name|
context "method is #{method_name}" do
before do
- expect_any_instance_of(self.class).to receive(:route).and_return(double(route_method: method_name))
+ expect_any_instance_of(self.class).to receive(:route).and_return(double(request_method: method_name))
end
it 'calls authenticate!' do
diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb
index 35644bd8cc9..ffeacb15f17 100644
--- a/spec/requests/api/internal_spec.rb
+++ b/spec/requests/api/internal_spec.rb
@@ -4,7 +4,7 @@ describe API::Internal, api: true do
include ApiHelpers
let(:user) { create(:user) }
let(:key) { create(:key, user: user) }
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:secret_token) { Gitlab::Shell.secret_token }
describe "GET /internal/check", no_db: true do
@@ -239,7 +239,7 @@ describe API::Internal, api: true do
end
context "blocked user" do
- let(:personal_project) { create(:project, namespace: user.namespace) }
+ let(:personal_project) { create(:empty_project, namespace: user.namespace) }
before do
user.block
@@ -265,7 +265,7 @@ describe API::Internal, api: true do
end
context "archived project" do
- let(:personal_project) { create(:project, namespace: user.namespace) }
+ let(:personal_project) { create(:empty_project, namespace: user.namespace) }
before do
project.team << [user, :developer]
@@ -337,8 +337,7 @@ describe API::Internal, api: true do
context 'ssh access has been disabled' do
before do
- settings = ::ApplicationSetting.create_from_defaults
- settings.update_attribute(:enabled_git_access_protocol, 'http')
+ stub_application_setting(enabled_git_access_protocol: 'http')
end
it 'rejects the SSH push' do
@@ -360,8 +359,7 @@ describe API::Internal, api: true do
context 'http access has been disabled' do
before do
- settings = ::ApplicationSetting.create_from_defaults
- settings.update_attribute(:enabled_git_access_protocol, 'ssh')
+ stub_application_setting(enabled_git_access_protocol: 'ssh')
end
it 'rejects the HTTP push' do
@@ -383,8 +381,7 @@ describe API::Internal, api: true do
context 'web actions are always allowed' do
it 'allows WEB push' do
- settings = ::ApplicationSetting.create_from_defaults
- settings.update_attribute(:enabled_git_access_protocol, 'ssh')
+ stub_application_setting(enabled_git_access_protocol: 'ssh')
project.team << [user, :developer]
push(key, project, 'web')
diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb
index 5c80dd98dc7..cca00df9591 100644
--- a/spec/requests/api/issues_spec.rb
+++ b/spec/requests/api/issues_spec.rb
@@ -11,7 +11,7 @@ describe API::Issues, api: true do
let(:author) { create(:author) }
let(:assignee) { create(:assignee) }
let(:admin) { create(:user, :admin) }
- let!(:project) { create(:project, :public, creator_id: user.id, namespace: user.namespace ) }
+ let!(:project) { create(:empty_project, :public, creator_id: user.id, namespace: user.namespace ) }
let!(:closed_issue) do
create :closed_issue,
author: user,
@@ -50,6 +50,8 @@ describe API::Issues, api: true do
end
let!(:note) { create(:note_on_issue, author: user, project: project, noteable: issue) }
+ let(:no_milestone_title) { URI.escape(Milestone::None.title) }
+
before do
project.team << [user, :reporter]
project.team << [guest, :guest]
@@ -107,6 +109,7 @@ describe API::Issues, api: true do
it 'returns an array of labeled issues when at least one label matches' do
get api("/issues?labels=#{label.title},foo,bar", user)
+
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
@@ -136,6 +139,51 @@ describe API::Issues, api: true do
expect(json_response.length).to eq(0)
end
+ it 'returns an empty array if no issue matches milestone' do
+ get api("/issues?milestone=#{empty_milestone.title}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(0)
+ end
+
+ it 'returns an empty array if milestone does not exist' do
+ get api("/issues?milestone=foo", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(0)
+ end
+
+ it 'returns an array of issues in given milestone' do
+ get api("/issues?milestone=#{milestone.title}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(2)
+ expect(json_response.first['id']).to eq(issue.id)
+ expect(json_response.second['id']).to eq(closed_issue.id)
+ end
+
+ it 'returns an array of issues matching state in milestone' do
+ get api("/issues?milestone=#{milestone.title}"\
+ '&state=closed', user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['id']).to eq(closed_issue.id)
+ end
+
+ it 'returns an array of issues with no milestone' do
+ get api("/issues?milestone=#{no_milestone_title}", author)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['id']).to eq(confidential_issue.id)
+ end
+
it 'sorts by created_at descending by default' do
get api('/issues', user)
response_dates = json_response.map { |issue| issue['created_at'] }
@@ -176,7 +224,7 @@ describe API::Issues, api: true do
describe "GET /groups/:id/issues" do
let!(:group) { create(:group) }
- let!(:group_project) { create(:project, :public, creator_id: user.id, namespace: group) }
+ let!(:group_project) { create(:empty_project, :public, creator_id: user.id, namespace: group) }
let!(:group_closed_issue) do
create :closed_issue,
author: user,
@@ -318,6 +366,15 @@ describe API::Issues, api: true do
expect(json_response.first['id']).to eq(group_closed_issue.id)
end
+ it 'returns an array of issues with no milestone' do
+ get api("#{base_url}?milestone=#{no_milestone_title}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['id']).to eq(group_confidential_issue.id)
+ end
+
it 'sorts by created_at descending by default' do
get api(base_url, user)
response_dates = json_response.map { |issue| issue['created_at'] }
@@ -357,7 +414,6 @@ describe API::Issues, api: true do
describe "GET /projects/:id/issues" do
let(:base_url) { "/projects/#{project.id}" }
- let(:title) { milestone.title }
it "returns 404 on private projects for other users" do
private_project = create(:empty_project, :private)
@@ -369,7 +425,7 @@ describe API::Issues, api: true do
end
it 'returns no issues when user has access to project but not issues' do
- restricted_project = create(:empty_project, :public, issues_access_level: ProjectFeature::PRIVATE)
+ restricted_project = create(:empty_project, :public, :issues_private)
create(:issue, project: restricted_project)
get api("/projects/#{restricted_project.id}/issues", non_member)
@@ -433,8 +489,9 @@ describe API::Issues, api: true do
expect(json_response.first['labels']).to eq([label.title])
end
- it 'returns an array of labeled project issues when at least one label matches' do
+ it 'returns an array of labeled project issues where all labels match' do
get api("#{base_url}/issues?labels=#{label.title},foo,bar", user)
+
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
@@ -463,7 +520,8 @@ describe API::Issues, api: true do
end
it 'returns an array of issues in given milestone' do
- get api("#{base_url}/issues?milestone=#{title}", user)
+ get api("#{base_url}/issues?milestone=#{milestone.title}", user)
+
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(2)
@@ -480,6 +538,15 @@ describe API::Issues, api: true do
expect(json_response.first['id']).to eq(closed_issue.id)
end
+ it 'returns an array of issues with no milestone' do
+ get api("#{base_url}/issues?milestone=#{no_milestone_title}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['id']).to eq(confidential_issue.id)
+ end
+
it 'sorts by created_at descending by default' do
get api("#{base_url}/issues", user)
response_dates = json_response.map { |issue| issue['created_at'] }
@@ -545,14 +612,6 @@ describe API::Issues, api: true do
expect(json_response['iid']).to eq(issue.iid)
end
- it 'returns a project issue by iid' do
- get api("/projects/#{project.id}/issues?iid=#{issue.iid}", user)
- expect(response.status).to eq 200
- expect(json_response.first['title']).to eq issue.title
- expect(json_response.first['id']).to eq issue.id
- expect(json_response.first['iid']).to eq issue.iid
- end
-
it "returns 404 if issue id not found" do
get api("/projects/#{project.id}/issues/54321", user)
expect(response).to have_http_status(404)
@@ -932,6 +991,13 @@ describe API::Issues, api: true do
expect(json_response['state']).to eq "closed"
end
+ it 'reopens a project isssue' do
+ put api("/projects/#{project.id}/issues/#{closed_issue.id}", user), state_event: 'reopen'
+
+ expect(response).to have_http_status(200)
+ expect(json_response['state']).to eq 'reopened'
+ end
+
context 'when an admin or owner makes the request' do
it 'accepts the update date to be set' do
update_time = 2.weeks.ago
@@ -969,7 +1035,7 @@ describe API::Issues, api: true do
context "when the user is project owner" do
let(:owner) { create(:user) }
- let(:project) { create(:project, namespace: owner.namespace) }
+ let(:project) { create(:empty_project, namespace: owner.namespace) }
it "deletes the issue if an admin requests it" do
delete api("/projects/#{project.id}/issues/#{issue.id}", owner)
@@ -988,8 +1054,8 @@ describe API::Issues, api: true do
end
describe '/projects/:id/issues/:issue_id/move' do
- let!(:target_project) { create(:project, path: 'project2', creator_id: user.id, namespace: user.namespace ) }
- let!(:target_project2) { create(:project, creator_id: non_member.id, namespace: non_member.namespace ) }
+ let!(:target_project) { create(:empty_project, path: 'project2', creator_id: user.id, namespace: user.namespace ) }
+ let!(:target_project2) { create(:empty_project, creator_id: non_member.id, namespace: non_member.namespace ) }
it 'moves an issue' do
post api("/projects/#{project.id}/issues/#{issue.id}/move", user),
@@ -1110,4 +1176,10 @@ describe API::Issues, api: true do
expect(response).to have_http_status(404)
end
end
+
+ describe 'time tracking endpoints' do
+ let(:issuable) { issue }
+
+ include_examples 'time tracking endpoints', 'issue'
+ end
end
diff --git a/spec/requests/api/labels_spec.rb b/spec/requests/api/labels_spec.rb
index b29ce1ea25e..a8cd787f398 100644
--- a/spec/requests/api/labels_spec.rb
+++ b/spec/requests/api/labels_spec.rb
@@ -4,7 +4,7 @@ describe API::Labels, api: true do
include ApiHelpers
let(:user) { create(:user) }
- let(:project) { create(:project, creator_id: user.id, namespace: user.namespace) }
+ let(:project) { create(:empty_project, creator_id: user.id, namespace: user.namespace) }
let!(:label1) { create(:label, title: 'label1', project: project) }
let!(:priority_label) { create(:label, title: 'bug', project: project, priority: 3) }
diff --git a/spec/requests/api/members_spec.rb b/spec/requests/api/members_spec.rb
index 2c94c86ccfa..9892e014cb9 100644
--- a/spec/requests/api/members_spec.rb
+++ b/spec/requests/api/members_spec.rb
@@ -9,7 +9,7 @@ describe API::Members, api: true do
let(:stranger) { create(:user) }
let(:project) do
- create(:project, :public, :access_requestable, creator_id: master.id, namespace: master.namespace) do |project|
+ create(:empty_project, :public, :access_requestable, creator_id: master.id, namespace: master.namespace) do |project|
project.team << [developer, :developer]
project.team << [master, :master]
project.request_access(access_requester)
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index f032d1b683d..ff10e79e417 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -6,12 +6,10 @@ describe API::MergeRequests, api: true do
let(:user) { create(:user) }
let(:admin) { create(:user, :admin) }
let(:non_member) { create(:user) }
- let!(:project) { create(:project, creator_id: user.id, namespace: user.namespace) }
- let!(:merge_request) { create(:merge_request, :simple, author: user, assignee: user, source_project: project, target_project: project, title: "Test", created_at: base_time) }
- let!(:merge_request_closed) { create(:merge_request, state: "closed", author: user, assignee: user, source_project: project, target_project: project, title: "Closed test", created_at: base_time + 1.second) }
- let!(:merge_request_merged) { create(:merge_request, state: "merged", author: user, assignee: user, source_project: project, target_project: project, title: "Merged test", created_at: base_time + 2.seconds, merge_commit_sha: '9999999999999999999999999999999999999999') }
- let!(:note) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "a comment on a MR") }
- let!(:note2) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "another comment on a MR") }
+ let!(:project) { create(:project, :public, :repository, creator: user, namespace: user.namespace) }
+ let!(:merge_request) { create(:merge_request, :simple, author: user, assignee: user, source_project: project, title: "Test", created_at: base_time) }
+ let!(:merge_request_closed) { create(:merge_request, state: "closed", author: user, assignee: user, source_project: project, title: "Closed test", created_at: base_time + 1.second) }
+ let!(:merge_request_merged) { create(:merge_request, state: "merged", author: user, assignee: user, source_project: project, title: "Merged test", created_at: base_time + 2.seconds, merge_commit_sha: '9999999999999999999999999999999999999999') }
let(:milestone) { create(:milestone, title: '1.0.0', project: project) }
before do
@@ -75,6 +73,16 @@ describe API::MergeRequests, api: true do
expect(json_response.first['title']).to eq(merge_request_merged.title)
end
+ it 'returns merge_request by "iids" array' do
+ get api("/projects/#{project.id}/merge_requests", user), iids: [merge_request.iid, merge_request_closed.iid]
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(2)
+ expect(json_response.first['title']).to eq merge_request_closed.title
+ expect(json_response.first['id']).to eq merge_request_closed.id
+ end
+
context "with ordering" do
before do
@mr_later = mr_with_later_created_and_updated_at_time
@@ -161,24 +169,6 @@ describe API::MergeRequests, api: true do
expect(json_response['force_close_merge_request']).to be_falsy
end
- it 'returns merge_request by iid' do
- url = "/projects/#{project.id}/merge_requests?iid=#{merge_request.iid}"
- get api(url, user)
- expect(response.status).to eq 200
- expect(json_response.first['title']).to eq merge_request.title
- expect(json_response.first['id']).to eq merge_request.id
- end
-
- it 'returns merge_request by iid array' do
- get api("/projects/#{project.id}/merge_requests", user), iid: [merge_request.iid, merge_request_closed.iid]
-
- expect(response).to have_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(2)
- expect(json_response.first['title']).to eq merge_request_closed.title
- expect(json_response.first['id']).to eq merge_request_closed.id
- end
-
it "returns a 404 error if merge_request_id not found" do
get api("/projects/#{project.id}/merge_requests/999", user)
expect(response).to have_http_status(404)
@@ -308,8 +298,8 @@ describe API::MergeRequests, api: true do
context 'forked projects' do
let!(:user2) { create(:user) }
- let!(:fork_project) { create(:project, forked_from_project: project, namespace: user2.namespace, creator_id: user2.id) }
- let!(:unrelated_project) { create(:project, namespace: create(:user).namespace, creator_id: user2.id) }
+ let!(:fork_project) { create(:empty_project, forked_from_project: project, namespace: user2.namespace, creator_id: user2.id) }
+ let!(:unrelated_project) { create(:empty_project, namespace: create(:user).namespace, creator_id: user2.id) }
before :each do |each|
fork_project.team << [user2, :reporter]
@@ -556,11 +546,12 @@ describe API::MergeRequests, api: true do
original_count = merge_request.notes.size
post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/comments", user), note: "My comment"
+
expect(response).to have_http_status(201)
expect(json_response['note']).to eq('My comment')
expect(json_response['author']['name']).to eq(user.name)
expect(json_response['author']['username']).to eq(user.username)
- expect(merge_request.notes.size).to eq(original_count + 1)
+ expect(merge_request.reload.notes.size).to eq(original_count + 1)
end
it "returns 400 if note is missing" do
@@ -576,6 +567,9 @@ describe API::MergeRequests, api: true do
end
describe "GET :id/merge_requests/:merge_request_id/comments" do
+ let!(:note) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "a comment on a MR") }
+ let!(:note2) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "another comment on a MR") }
+
it "returns merge_request comments ordered by created_at" do
get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/comments", user)
expect(response).to have_http_status(200)
@@ -627,6 +621,17 @@ describe API::MergeRequests, api: true do
expect(json_response.first['title']).to eq(issue.title)
expect(json_response.first['id']).to eq(issue.id)
end
+
+ it 'returns 403 if the user has no access to the merge request' do
+ project = create(:empty_project, :private)
+ merge_request = create(:merge_request, :simple, source_project: project)
+ guest = create(:user)
+ project.team << [guest, :guest]
+
+ get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/closes_issues", guest)
+
+ expect(response).to have_http_status(403)
+ end
end
describe 'POST :id/merge_requests/:merge_request_id/subscription' do
@@ -648,6 +653,15 @@ describe API::MergeRequests, api: true do
expect(response).to have_http_status(404)
end
+
+ it 'returns 403 if user has no access to read code' do
+ guest = create(:user)
+ project.team << [guest, :guest]
+
+ post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", guest)
+
+ expect(response).to have_http_status(403)
+ end
end
describe 'DELETE :id/merge_requests/:merge_request_id/subscription' do
@@ -669,6 +683,21 @@ describe API::MergeRequests, api: true do
expect(response).to have_http_status(404)
end
+
+ it 'returns 403 if user has no access to read code' do
+ guest = create(:user)
+ project.team << [guest, :guest]
+
+ delete api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", guest)
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ describe 'Time tracking' do
+ let(:issuable) { merge_request }
+
+ include_examples 'time tracking endpoints', 'merge_request'
end
def mr_with_later_created_and_updated_at_time
diff --git a/spec/requests/api/notes_spec.rb b/spec/requests/api/notes_spec.rb
index 028f93c8561..0353ebea9e5 100644
--- a/spec/requests/api/notes_spec.rb
+++ b/spec/requests/api/notes_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe API::Notes, api: true do
include ApiHelpers
let(:user) { create(:user) }
- let!(:project) { create(:project, :public, namespace: user.namespace) }
+ let!(:project) { create(:empty_project, :public, namespace: user.namespace) }
let!(:issue) { create(:issue, project: project, author: user) }
let!(:merge_request) { create(:merge_request, source_project: project, target_project: project, author: user) }
let!(:snippet) { create(:project_snippet, project: project, author: user) }
@@ -14,12 +14,12 @@ describe API::Notes, api: true do
# For testing the cross-reference of a private issue in a public issue
let(:private_user) { create(:user) }
let(:private_project) do
- create(:project, namespace: private_user.namespace).
+ create(:empty_project, namespace: private_user.namespace).
tap { |p| p.team << [private_user, :master] }
end
let(:private_issue) { create(:issue, project: private_project) }
- let(:ext_proj) { create(:project, :public) }
+ let(:ext_proj) { create(:empty_project, :public) }
let(:ext_issue) { create(:issue, project: ext_proj) }
let!(:cross_reference_note) do
@@ -264,8 +264,20 @@ describe API::Notes, api: true do
end
end
+ context 'when user does not have access to read the noteable' do
+ it 'responds with 404' do
+ project = create(:empty_project, :private) { |p| p.add_guest(user) }
+ issue = create(:issue, :confidential, project: project)
+
+ post api("/projects/#{project.id}/issues/#{issue.id}/notes", user),
+ body: 'Foo'
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
context 'when user does not have access to create noteable' do
- let(:private_issue) { create(:issue, project: create(:project, :private)) }
+ let(:private_issue) { create(:issue, project: create(:empty_project, :private)) }
##
# We are posting to project user has access to, but we use issue id
diff --git a/spec/requests/api/notification_settings_spec.rb b/spec/requests/api/notification_settings_spec.rb
index 8691a81420f..39d3afcb78f 100644
--- a/spec/requests/api/notification_settings_spec.rb
+++ b/spec/requests/api/notification_settings_spec.rb
@@ -5,7 +5,7 @@ describe API::NotificationSettings, api: true do
let(:user) { create(:user) }
let!(:group) { create(:group) }
- let!(:project) { create(:project, :public, creator_id: user.id, namespace: group) }
+ let!(:project) { create(:empty_project, :public, creator_id: user.id, namespace: group) }
describe "GET /notification_settings" do
it "returns global notification settings for the current user" do
diff --git a/spec/requests/api/pipelines_spec.rb b/spec/requests/api/pipelines_spec.rb
index 9a01f7fa1c4..b7a0b5a9e13 100644
--- a/spec/requests/api/pipelines_spec.rb
+++ b/spec/requests/api/pipelines_spec.rb
@@ -5,7 +5,7 @@ describe API::Pipelines, api: true do
let(:user) { create(:user) }
let(:non_member) { create(:user) }
- let(:project) { create(:project, creator_id: user.id) }
+ let(:project) { create(:project, :repository, creator: user) }
let!(:pipeline) do
create(:ci_empty_pipeline, project: project, sha: project.commit.id,
diff --git a/spec/requests/api/project_hooks_spec.rb b/spec/requests/api/project_hooks_spec.rb
index a42cedae614..f4973d71088 100644
--- a/spec/requests/api/project_hooks_spec.rb
+++ b/spec/requests/api/project_hooks_spec.rb
@@ -4,7 +4,7 @@ describe API::ProjectHooks, 'ProjectHooks', api: true do
include ApiHelpers
let(:user) { create(:user) }
let(:user3) { create(:user) }
- let!(:project) { create(:project, creator_id: user.id, namespace: user.namespace) }
+ let!(:project) { create(:empty_project, creator_id: user.id, namespace: user.namespace) }
let!(:hook) do
create(:project_hook,
:all_events_enabled,
@@ -86,7 +86,8 @@ describe API::ProjectHooks, 'ProjectHooks', api: true do
describe "POST /projects/:id/hooks" do
it "adds hook to project" do
expect do
- post api("/projects/#{project.id}/hooks", user), url: "http://example.com", issues_events: true
+ post api("/projects/#{project.id}/hooks", user),
+ url: "http://example.com", issues_events: true, wiki_page_events: true
end.to change {project.hooks.count}.by(1)
expect(response).to have_http_status(201)
@@ -98,7 +99,7 @@ describe API::ProjectHooks, 'ProjectHooks', api: true do
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['wiki_page_events']).to eq(true)
expect(json_response['enable_ssl_verification']).to eq(true)
expect(json_response).not_to include('token')
end
@@ -203,7 +204,7 @@ describe API::ProjectHooks, 'ProjectHooks', api: true do
it "returns a 404 if a user attempts to delete project hooks he/she does not own" do
test_user = create(:user)
- other_project = create(:project)
+ other_project = create(:empty_project)
other_project.team << [test_user, :master]
delete api("/projects/#{other_project.id}/hooks/#{hook.id}", test_user)
diff --git a/spec/requests/api/project_snippets_spec.rb b/spec/requests/api/project_snippets_spec.rb
index 01032c0929b..eea76c7bb94 100644
--- a/spec/requests/api/project_snippets_spec.rb
+++ b/spec/requests/api/project_snippets_spec.rb
@@ -4,25 +4,14 @@ describe API::ProjectSnippets, api: true do
include ApiHelpers
let(:project) { create(:empty_project, :public) }
+ let(:user) { create(:user) }
let(:admin) { create(:admin) }
- describe 'GET /projects/:project_id/snippets/:id' do
- # TODO (rspeicher): Deprecated; remove in 9.0
- it 'always exposes expires_at as nil' do
- snippet = create(:project_snippet, author: admin)
-
- get api("/projects/#{snippet.project.id}/snippets/#{snippet.id}", admin)
-
- expect(json_response).to have_key('expires_at')
- expect(json_response['expires_at']).to be_nil
- end
- end
-
describe 'GET /projects/:project_id/snippets/' do
let(:user) { create(:user) }
it 'returns all snippets available to team member' do
- project.team << [user, :developer]
+ project.add_developer(user)
public_snippet = create(:project_snippet, :public, project: project)
internal_snippet = create(:project_snippet, :internal, project: project)
private_snippet = create(:project_snippet, :private, project: project)
@@ -50,7 +39,7 @@ describe API::ProjectSnippets, api: true do
title: 'Test Title',
file_name: 'test.rb',
code: 'puts "hello world"',
- visibility_level: Gitlab::VisibilityLevel::PUBLIC
+ visibility_level: Snippet::PUBLIC
}
end
@@ -72,6 +61,51 @@ describe API::ProjectSnippets, api: true do
expect(response).to have_http_status(400)
end
+
+ context 'when the snippet is spam' do
+ def create_snippet(project, snippet_params = {})
+ project.add_developer(user)
+
+ post api("/projects/#{project.id}/snippets", user), params.merge(snippet_params)
+ end
+
+ before do
+ allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true)
+ end
+
+ context 'when the project is private' do
+ let(:private_project) { create(:project_empty_repo, :private) }
+
+ context 'when the snippet is public' do
+ it 'creates the snippet' do
+ expect { create_snippet(private_project, visibility_level: Snippet::PUBLIC) }.
+ to change { Snippet.count }.by(1)
+ end
+ end
+ end
+
+ context 'when the project is public' do
+ context 'when the snippet is private' do
+ it 'creates the snippet' do
+ expect { create_snippet(project, visibility_level: Snippet::PRIVATE) }.
+ to change { Snippet.count }.by(1)
+ end
+ end
+
+ context 'when the snippet is public' do
+ it 'rejects the shippet' do
+ expect { create_snippet(project, visibility_level: Snippet::PUBLIC) }.
+ not_to change { Snippet.count }
+ expect(response).to have_http_status(400)
+ end
+
+ it 'creates a spam log' do
+ expect { create_snippet(project, visibility_level: Snippet::PUBLIC) }.
+ to change { SpamLog.count }.by(1)
+ end
+ end
+ end
+ end
end
describe 'PUT /projects/:project_id/snippets/:id/' do
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 8304c408064..225e2e005df 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -8,8 +8,8 @@ describe API::Projects, api: true do
let(:user2) { create(:user) }
let(:user3) { create(:user) }
let(:admin) { create(:admin) }
- let(:project) { create(:project, creator_id: user.id, namespace: user.namespace) }
- let(:project2) { create(:project, path: 'project2', creator_id: user.id, namespace: user.namespace) }
+ let(:project) { create(:empty_project, creator_id: user.id, namespace: user.namespace) }
+ let(:project2) { create(:empty_project, path: 'project2', creator_id: user.id, namespace: user.namespace) }
let(:snippet) { create(:project_snippet, :public, author: user, project: project, title: 'example') }
let(:project_member) { create(:project_member, :master, user: user, project: project) }
let(:project_member2) { create(:project_member, :developer, user: user3, project: project) }
@@ -17,6 +17,7 @@ describe API::Projects, api: true do
let(:project3) do
create(:project,
:private,
+ :repository,
name: 'second_project',
path: 'second_project',
creator_id: user.id,
@@ -32,7 +33,7 @@ describe API::Projects, api: true do
access_level: ProjectMember::MASTER)
end
let(:project4) do
- create(:project,
+ create(:empty_project,
name: 'third_project',
path: 'third_project',
creator_id: user4.id,
@@ -49,7 +50,7 @@ describe API::Projects, api: true do
end
end
- context 'when authenticated' do
+ context 'when authenticated as regular user' do
it 'returns an array of projects' do
get api('/projects', user)
expect(response).to have_http_status(200)
@@ -172,6 +173,22 @@ describe API::Projects, api: true do
end
end
end
+
+ it "does not include statistics by default" do
+ get api('/projects/all', admin)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first).not_to include('statistics')
+ end
+
+ it "includes statistics if requested" do
+ get api('/projects/all', admin), statistics: true
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first).to include 'statistics'
+ end
end
end
@@ -196,6 +213,32 @@ describe API::Projects, api: true do
expect(json_response.first['name']).to eq(project4.name)
expect(json_response.first['owner']['username']).to eq(user4.username)
end
+
+ it "does not include statistics by default" do
+ get api('/projects/owned', user4)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first).not_to include('statistics')
+ end
+
+ it "includes statistics if requested" do
+ attributes = {
+ commit_count: 23,
+ storage_size: 702,
+ repository_size: 123,
+ lfs_objects_size: 234,
+ build_artifacts_size: 345,
+ }
+
+ project4.statistics.update!(attributes)
+
+ get api('/projects/owned', user4), statistics: true
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first['statistics']).to eq attributes.stringify_keys
+ end
end
end
@@ -210,7 +253,7 @@ describe API::Projects, api: true do
end
end
- let!(:public_project) { create(:project, :public) }
+ let!(:public_project) { create(:empty_project, :public) }
before do
project
project2
@@ -241,7 +284,7 @@ describe API::Projects, api: true do
end
describe 'GET /projects/starred' do
- let(:public_project) { create(:project, :public) }
+ let(:public_project) { create(:empty_project, :public) }
before do
project_member2
@@ -416,7 +459,7 @@ describe API::Projects, api: true do
before { project }
before { admin }
- it 'should create new project without path and return 201' do
+ it 'creates new project without path and return 201' do
expect { post api("/projects/user/#{user.id}", admin), name: 'foo' }.to change {Project.count}.by(1)
expect(response).to have_http_status(201)
end
@@ -541,7 +584,7 @@ describe API::Projects, api: true do
describe 'GET /projects/:id' do
context 'when unauthenticated' do
it 'returns the public projects' do
- public_project = create(:project, :public)
+ public_project = create(:empty_project, :public)
get api("/projects/#{public_project.id}")
@@ -623,13 +666,25 @@ describe API::Projects, api: true do
it 'handles users with dots' do
dot_user = create(:user, username: 'dot.user')
- project = create(:project, creator_id: dot_user.id, namespace: dot_user.namespace)
+ project = create(:empty_project, creator_id: dot_user.id, namespace: dot_user.namespace)
get api("/projects/#{dot_user.namespace.name}%2F#{project.path}", dot_user)
expect(response).to have_http_status(200)
expect(json_response['name']).to eq(project.name)
end
+ it 'exposes namespace fields' do
+ get api("/projects/#{project.id}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['namespace']).to eq({
+ 'id' => user.namespace.id,
+ 'name' => user.namespace.name,
+ 'path' => user.namespace.path,
+ 'kind' => user.namespace.kind,
+ })
+ end
+
describe 'permissions' do
context 'all projects' do
before { project.team << [user, :master] }
@@ -657,7 +712,7 @@ describe API::Projects, api: true do
end
context 'group project' do
- let(:project2) { create(:project, group: create(:group)) }
+ let(:project2) { create(:empty_project, group: create(:group)) }
before { project2.group.add_owner(user) }
@@ -702,7 +757,7 @@ describe API::Projects, api: true do
context 'when unauthenticated' do
it_behaves_like 'project events response' do
- let(:project) { create(:project, :public) }
+ let(:project) { create(:empty_project, :public) }
let(:current_user) { nil }
end
end
@@ -753,7 +808,7 @@ describe API::Projects, api: true do
context 'when unauthenticated' do
it_behaves_like 'project users response' do
- let(:project) { create(:project, :public) }
+ let(:project) { create(:empty_project, :public) }
let(:current_user) { nil }
end
end
@@ -867,11 +922,11 @@ describe API::Projects, api: true do
end
describe :fork_admin do
- let(:project_fork_target) { create(:project) }
- let(:project_fork_source) { create(:project, :public) }
+ let(:project_fork_target) { create(:empty_project) }
+ let(:project_fork_source) { create(:empty_project, :public) }
describe 'POST /projects/:id/fork/:forked_from_id' do
- let(:new_project_fork_source) { create(:project, :public) }
+ let(:new_project_fork_source) { create(:empty_project, :public) }
it "is not available for non admin users" do
post api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", user)
@@ -912,7 +967,7 @@ describe API::Projects, api: true do
end
context 'when users belong to project group' do
- let(:project_fork_target) { create(:project, group: create(:group)) }
+ let(:project_fork_target) { create(:empty_project, group: create(:group)) }
before do
project_fork_target.group.add_owner user
@@ -1030,48 +1085,6 @@ describe API::Projects, api: true do
end
end
- describe 'GET /projects/search/:query' do
- let!(:query) { 'query'}
- let!(:search) { create(:empty_project, name: query, creator_id: user.id, namespace: user.namespace) }
- let!(:pre) { create(:empty_project, name: "pre_#{query}", creator_id: user.id, namespace: user.namespace) }
- let!(:post) { create(:empty_project, name: "#{query}_post", creator_id: user.id, namespace: user.namespace) }
- let!(:pre_post) { create(:empty_project, name: "pre_#{query}_post", creator_id: user.id, namespace: user.namespace) }
- let!(:unfound) { create(:empty_project, name: 'unfound', creator_id: user.id, namespace: user.namespace) }
- let!(:internal) { create(:empty_project, :internal, name: "internal #{query}") }
- let!(:unfound_internal) { create(:empty_project, :internal, name: 'unfound internal') }
- let!(:public) { create(:empty_project, :public, name: "public #{query}") }
- let!(:unfound_public) { create(:empty_project, :public, name: 'unfound public') }
-
- shared_examples_for 'project search response' do |args = {}|
- it 'returns project search responses' do
- get api("/projects/search/#{query}", current_user)
-
- expect(response).to have_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response.size).to eq(args[:results])
- json_response.each { |project| expect(project['name']).to match(args[:match_regex] || /.*query.*/) }
- end
- end
-
- context 'when unauthenticated' do
- it_behaves_like 'project search response', results: 1 do
- let(:current_user) { nil }
- end
- end
-
- context 'when authenticated' do
- it_behaves_like 'project search response', results: 6 do
- let(:current_user) { user }
- end
- end
-
- context 'when authenticated as a different user' do
- it_behaves_like 'project search response', results: 2, match_regex: /(internal|public) query/ do
- let(:current_user) { user2 }
- end
- end
- end
-
describe 'PUT /projects/:id' do
before { project }
before { user }
diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb
index 0b19fa38c55..c61208e395c 100644
--- a/spec/requests/api/repositories_spec.rb
+++ b/spec/requests/api/repositories_spec.rb
@@ -8,7 +8,7 @@ describe API::Repositories, api: true do
let(:user) { create(:user) }
let(:guest) { create(:user).tap { |u| create(:project_member, :guest, user: u, project: project) } }
- let!(:project) { create(:project, creator_id: user.id) }
+ let!(:project) { create(:project, :repository, creator: user) }
let!(:master) { create(:project_member, :master, user: user, project: project) }
describe "GET /projects/:id/repository/tree" do
@@ -74,7 +74,7 @@ describe API::Repositories, api: true do
context 'when unauthenticated', 'and project is public' do
it_behaves_like 'repository tree' do
- let(:project) { create(:project, :public) }
+ let(:project) { create(:project, :public, :repository) }
let(:current_user) { nil }
end
end
@@ -144,7 +144,7 @@ describe API::Repositories, api: true do
context 'when unauthenticated', 'and project is public' do
it_behaves_like 'repository blob' do
- let(:project) { create(:project, :public) }
+ let(:project) { create(:project, :public, :repository) }
let(:current_user) { nil }
end
end
@@ -198,7 +198,7 @@ describe API::Repositories, api: true do
context 'when unauthenticated', 'and project is public' do
it_behaves_like 'repository raw blob' do
- let(:project) { create(:project, :public) }
+ let(:project) { create(:project, :public, :repository) }
let(:current_user) { nil }
end
end
@@ -273,7 +273,7 @@ describe API::Repositories, api: true do
context 'when unauthenticated', 'and project is public' do
it_behaves_like 'repository archive' do
- let(:project) { create(:project, :public) }
+ let(:project) { create(:project, :public, :repository) }
let(:current_user) { nil }
end
end
@@ -347,7 +347,7 @@ describe API::Repositories, api: true do
context 'when unauthenticated', 'and project is public' do
it_behaves_like 'repository compare' do
- let(:project) { create(:project, :public) }
+ let(:project) { create(:project, :public, :repository) }
let(:current_user) { nil }
end
end
@@ -394,7 +394,7 @@ describe API::Repositories, api: true do
context 'when unauthenticated', 'and project is public' do
it_behaves_like 'repository contributors' do
- let(:project) { create(:project, :public) }
+ let(:project) { create(:project, :public, :repository) }
let(:current_user) { nil }
end
end
diff --git a/spec/requests/api/runners_spec.rb b/spec/requests/api/runners_spec.rb
index 99414270be6..f2d81a28cb8 100644
--- a/spec/requests/api/runners_spec.rb
+++ b/spec/requests/api/runners_spec.rb
@@ -7,8 +7,8 @@ describe API::Runners, api: true do
let(:user) { create(:user) }
let(:user2) { create(:user) }
- let(:project) { create(:project, creator_id: user.id) }
- let(:project2) { create(:project, creator_id: user.id) }
+ let(:project) { create(:empty_project, creator_id: user.id) }
+ let(:project2) { create(:empty_project, creator_id: user.id) }
let!(:shared_runner) { create(:ci_runner, :shared) }
let!(:unused_specific_runner) { create(:ci_runner) }
diff --git a/spec/requests/api/services_spec.rb b/spec/requests/api/services_spec.rb
index 668e39f9dba..776dc655650 100644
--- a/spec/requests/api/services_spec.rb
+++ b/spec/requests/api/services_spec.rb
@@ -6,7 +6,7 @@ describe API::Services, api: true do
let(:user) { create(:user) }
let(:admin) { create(:admin) }
let(:user2) { create(:user) }
- let(:project) {create(:project, creator_id: user.id, namespace: user.namespace) }
+ let(:project) { create(:empty_project, creator_id: user.id, namespace: user.namespace) }
Service.available_services_names.each do |service|
describe "PUT /projects/:id/services/#{service.dasherize}" do
@@ -16,6 +16,15 @@ describe API::Services, api: true do
put api("/projects/#{project.id}/services/#{dashed_service}", user), service_attrs
expect(response).to have_http_status(200)
+
+ current_service = project.services.first
+ event = current_service.event_names.empty? ? "foo" : current_service.event_names.first
+ state = current_service[event] || false
+
+ put api("/projects/#{project.id}/services/#{dashed_service}?#{event}=#{!state}", user), service_attrs
+
+ expect(response).to have_http_status(200)
+ expect(project.services.first[event]).not_to eq(state) unless event == "foo"
end
it "returns if required fields missing" do
@@ -92,57 +101,78 @@ describe API::Services, api: true do
describe 'POST /projects/:id/services/:slug/trigger' do
let!(:project) { create(:empty_project) }
- let(:service_name) { 'mattermost_slash_commands' }
- context 'no service is available' do
- it 'returns a not found message' do
- post api("/projects/#{project.id}/services/idonotexist/trigger")
+ describe 'Mattermost Service' do
+ let(:service_name) { 'mattermost_slash_commands' }
- expect(response).to have_http_status(404)
- expect(json_response["error"]).to eq("404 Not Found")
+ context 'no service is available' do
+ it 'returns a not found message' do
+ post api("/projects/#{project.id}/services/idonotexist/trigger")
+
+ expect(response).to have_http_status(404)
+ expect(json_response["error"]).to eq("404 Not Found")
+ end
end
- end
- context 'the service exists' do
- let(:params) { { token: 'token' } }
+ context 'the service exists' do
+ let(:params) { { token: 'token' } }
- context 'the service is not active' do
- let!(:inactive_service) do
- project.create_mattermost_slash_commands_service(
- active: false,
- properties: { token: 'token' }
- )
- end
+ context 'the service is not active' do
+ before do
+ project.create_mattermost_slash_commands_service(
+ active: false,
+ properties: params
+ )
+ end
- it 'when the service is inactive' do
- post api("/projects/#{project.id}/services/mattermost_slash_commands/trigger"), params
+ it 'when the service is inactive' do
+ post api("/projects/#{project.id}/services/#{service_name}/trigger"), params
- expect(response).to have_http_status(404)
+ expect(response).to have_http_status(404)
+ end
end
- end
- context 'the service is active' do
- let!(:active_service) do
- project.create_mattermost_slash_commands_service(
- active: true,
- properties: { token: 'token' }
- )
+ context 'the service is active' do
+ before do
+ project.create_mattermost_slash_commands_service(
+ active: true,
+ properties: params
+ )
+ end
+
+ it 'returns status 200' do
+ post api("/projects/#{project.id}/services/#{service_name}/trigger"), params
+
+ expect(response).to have_http_status(200)
+ end
end
- it 'returns status 200' do
- post api("/projects/#{project.id}/services/mattermost_slash_commands/trigger"), params
+ context 'when the project can not be found' do
+ it 'returns a generic 404' do
+ post api("/projects/404/services/#{service_name}/trigger"), params
- expect(response).to have_http_status(200)
+ expect(response).to have_http_status(404)
+ expect(json_response["message"]).to eq("404 Service Not Found")
+ end
end
end
+ end
- context 'when the project can not be found' do
- it 'returns a generic 404' do
- post api("/projects/404/services/mattermost_slash_commands/trigger"), params
+ describe 'Slack Service' do
+ let(:service_name) { 'slack_slash_commands' }
- expect(response).to have_http_status(404)
- expect(json_response["message"]).to eq("404 Service Not Found")
- end
+ before do
+ project.create_slack_slash_commands_service(
+ active: true,
+ properties: { token: 'token' }
+ )
+ end
+
+ it 'returns status 200' do
+ post api("/projects/#{project.id}/services/#{service_name}/trigger"), token: 'token', text: 'help'
+
+ expect(response).to have_http_status(200)
+ expect(json_response['response_type']).to eq("ephemeral")
end
end
end
diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb
index 9a8d633d657..91e3c333a02 100644
--- a/spec/requests/api/settings_spec.rb
+++ b/spec/requests/api/settings_spec.rb
@@ -16,6 +16,8 @@ describe API::Settings, 'Settings', api: true do
expect(json_response['repository_storage']).to eq('default')
expect(json_response['koding_enabled']).to be_falsey
expect(json_response['koding_url']).to be_nil
+ expect(json_response['plantuml_enabled']).to be_falsey
+ expect(json_response['plantuml_url']).to be_nil
end
end
@@ -28,7 +30,8 @@ describe API::Settings, 'Settings', api: true do
it "updates application settings" do
put api("/application/settings", admin),
- default_projects_limit: 3, signin_enabled: false, repository_storage: 'custom', koding_enabled: true, koding_url: 'http://koding.example.com'
+ default_projects_limit: 3, signin_enabled: false, repository_storage: 'custom', koding_enabled: true, koding_url: 'http://koding.example.com',
+ plantuml_enabled: true, plantuml_url: 'http://plantuml.example.com'
expect(response).to have_http_status(200)
expect(json_response['default_projects_limit']).to eq(3)
expect(json_response['signin_enabled']).to be_falsey
@@ -36,6 +39,8 @@ describe API::Settings, 'Settings', api: true do
expect(json_response['repository_storages']).to eq(['custom'])
expect(json_response['koding_enabled']).to be_truthy
expect(json_response['koding_url']).to eq('http://koding.example.com')
+ expect(json_response['plantuml_enabled']).to be_truthy
+ expect(json_response['plantuml_url']).to eq('http://plantuml.example.com')
end
end
@@ -44,8 +49,16 @@ describe API::Settings, 'Settings', api: true do
put api("/application/settings", admin), koding_enabled: true
expect(response).to have_http_status(400)
- expect(json_response['message']).to have_key('koding_url')
- expect(json_response['message']['koding_url']).to include "can't be blank"
+ expect(json_response['error']).to eq('koding_url is missing')
+ end
+ end
+
+ context "missing plantuml_url value when plantuml_enabled is true" do
+ it "returns a blank parameter error message" do
+ put api("/application/settings", admin), plantuml_enabled: true
+
+ expect(response).to have_http_status(400)
+ expect(json_response['error']).to eq('plantuml_url is missing')
end
end
end
diff --git a/spec/requests/api/snippets_spec.rb b/spec/requests/api/snippets_spec.rb
index f6fb6ea5506..6b9a739b439 100644
--- a/spec/requests/api/snippets_spec.rb
+++ b/spec/requests/api/snippets_spec.rb
@@ -80,7 +80,7 @@ describe API::Snippets, api: true do
title: 'Test Title',
file_name: 'test.rb',
content: 'puts "hello world"',
- visibility_level: Gitlab::VisibilityLevel::PUBLIC
+ visibility_level: Snippet::PUBLIC
}
end
@@ -101,6 +101,36 @@ describe API::Snippets, api: true do
expect(response).to have_http_status(400)
end
+
+ context 'when the snippet is spam' do
+ def create_snippet(snippet_params = {})
+ post api('/snippets', user), params.merge(snippet_params)
+ end
+
+ before do
+ allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true)
+ end
+
+ context 'when the snippet is private' do
+ it 'creates the snippet' do
+ expect { create_snippet(visibility_level: Snippet::PRIVATE) }.
+ to change { Snippet.count }.by(1)
+ end
+ end
+
+ context 'when the snippet is public' do
+ it 'rejects the shippet' do
+ expect { create_snippet(visibility_level: Snippet::PUBLIC) }.
+ not_to change { Snippet.count }
+ expect(response).to have_http_status(400)
+ end
+
+ it 'creates a spam log' do
+ expect { create_snippet(visibility_level: Snippet::PUBLIC) }.
+ to change { SpamLog.count }.by(1)
+ end
+ end
+ end
end
describe 'PUT /snippets/:id' do
diff --git a/spec/requests/api/tags_spec.rb b/spec/requests/api/tags_spec.rb
index a1c32ae65ba..898d2b27e5c 100644
--- a/spec/requests/api/tags_spec.rb
+++ b/spec/requests/api/tags_spec.rb
@@ -7,7 +7,7 @@ describe API::Tags, api: true do
let(:user) { create(:user) }
let(:user2) { create(:user) }
- let!(:project) { create(:project, creator_id: user.id) }
+ let!(:project) { create(:project, :repository, creator: user) }
let!(:master) { create(:project_member, :master, user: user, project: project) }
let!(:guest) { create(:project_member, :guest, user: user2, project: project) }
@@ -29,7 +29,7 @@ describe API::Tags, api: true do
context 'when unauthenticated' do
it_behaves_like 'repository tags' do
- let(:project) { create(:project, :public) }
+ let(:project) { create(:project, :public, :repository) }
let(:current_user) { nil }
end
end
@@ -88,7 +88,7 @@ describe API::Tags, api: true do
context 'when unauthenticated' do
it_behaves_like 'repository tag' do
- let(:project) { create(:project, :public) }
+ let(:project) { create(:project, :public, :repository) }
let(:current_user) { nil }
end
end
diff --git a/spec/requests/api/todos_spec.rb b/spec/requests/api/todos_spec.rb
index 887a2ba5b84..56dc017ce54 100644
--- a/spec/requests/api/todos_spec.rb
+++ b/spec/requests/api/todos_spec.rb
@@ -3,8 +3,8 @@ require 'spec_helper'
describe API::Todos, api: true do
include ApiHelpers
- let(:project_1) { create(:project) }
- let(:project_2) { create(:project) }
+ let(:project_1) { create(:empty_project) }
+ let(:project_2) { create(:empty_project) }
let(:author_1) { create(:user) }
let(:author_2) { create(:user) }
let(:john_doe) { create(:user, username: 'john_doe') }
@@ -183,12 +183,25 @@ describe API::Todos, api: true do
expect(response.status).to eq(404)
end
+
+ it 'returns an error if the issuable is not accessible' do
+ guest = create(:user)
+ project_1.team << [guest, :guest]
+
+ post api("/projects/#{project_1.id}/#{issuable_type}/#{issuable.id}/todo", guest)
+
+ if issuable_type == 'merge_requests'
+ expect(response).to have_http_status(403)
+ else
+ expect(response).to have_http_status(404)
+ end
+ end
end
describe 'POST :id/issuable_type/:issueable_id/todo' do
context 'for an issue' do
it_behaves_like 'an issuable', 'issues' do
- let(:issuable) { create(:issue, author: author_1, project: project_1) }
+ let(:issuable) { create(:issue, :confidential, author: author_1, project: project_1) }
end
end
diff --git a/spec/requests/api/triggers_spec.rb b/spec/requests/api/triggers_spec.rb
index 67ec3168679..84104aa66ee 100644
--- a/spec/requests/api/triggers_spec.rb
+++ b/spec/requests/api/triggers_spec.rb
@@ -7,7 +7,7 @@ describe API::Triggers do
let(:user2) { create(:user) }
let!(:trigger_token) { 'secure_token' }
let!(:trigger_token_2) { 'secure_token_2' }
- let!(:project) { create(:project, creator_id: user.id) }
+ let!(:project) { create(:project, :repository, creator: user) }
let!(:master) { create(:project_member, :master, user: user, project: project) }
let!(:developer) { create(:project_member, :developer, user: user2, project: project) }
let!(:trigger) { create(:ci_trigger, project: project, token: trigger_token) }
@@ -15,7 +15,7 @@ describe API::Triggers do
let!(:trigger_request) { create(:ci_trigger_request, trigger: trigger, created_at: '2015-01-01 12:13:14') }
describe 'POST /projects/:project_id/trigger' do
- let!(:project2) { create(:empty_project) }
+ let!(:project2) { create(:project) }
let(:options) do
{
token: trigger_token
diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb
index 9e317f3a7e9..8692f9da976 100644
--- a/spec/requests/api/users_spec.rb
+++ b/spec/requests/api/users_spec.rb
@@ -137,6 +137,15 @@ describe API::Users, api: true do
expect(new_user.can_create_group).to eq(true)
end
+ it "creates user with optional attributes" do
+ optional_attributes = { confirm: true }
+ attributes = attributes_for(:user).merge(optional_attributes)
+
+ post api('/users', admin), attributes
+
+ expect(response).to have_http_status(201)
+ end
+
it "creates non-admin user" do
post api('/users', admin), attributes_for(:user, admin: false, can_create_group: false)
expect(response).to have_http_status(201)
@@ -265,6 +274,14 @@ describe API::Users, api: true do
expect(response).to have_http_status(409)
expect(json_response['message']).to eq('Username has already been taken')
end
+
+ it 'creates user with new identity' do
+ post api("/users", admin), attributes_for(:user, provider: 'github', extern_uid: '67890')
+
+ expect(response).to have_http_status(201)
+ expect(json_response['identities'].first['extern_uid']).to eq('67890')
+ expect(json_response['identities'].first['provider']).to eq('github')
+ end
end
end
@@ -288,6 +305,13 @@ describe API::Users, api: true do
expect(user.reload.bio).to eq('new test bio')
end
+ it "updates user with new password and forces reset on next login" do
+ put api("/users/#{user.id}", admin), password: '12345678'
+
+ expect(response).to have_http_status(200)
+ expect(user.reload.password_expires_at).to be <= Time.now
+ end
+
it "updates user with organization" do
put api("/users/#{user.id}", admin), { organization: 'GitLab' }
@@ -317,9 +341,9 @@ describe API::Users, api: true do
end
it 'updates user with new identity' do
- put api("/users/#{user.id}", admin), provider: 'github', extern_uid: '67890'
+ put api("/users/#{user.id}", admin), provider: 'github', extern_uid: 'john'
expect(response).to have_http_status(200)
- expect(user.reload.identities.first.extern_uid).to eq('67890')
+ expect(user.reload.identities.first.extern_uid).to eq('john')
expect(user.reload.identities.first.provider).to eq('github')
end
diff --git a/spec/requests/api/v3/deploy_keys_spec.rb b/spec/requests/api/v3/deploy_keys_spec.rb
new file mode 100644
index 00000000000..f5bdf408c5e
--- /dev/null
+++ b/spec/requests/api/v3/deploy_keys_spec.rb
@@ -0,0 +1,172 @@
+require 'spec_helper'
+
+describe API::V3::DeployKeys, api: true do
+ include ApiHelpers
+
+ let(:user) { create(:user) }
+ let(:admin) { create(:admin) }
+ let(:project) { create(:empty_project, creator_id: user.id) }
+ let(:project2) { create(:empty_project, creator_id: user.id) }
+ let(:deploy_key) { create(:deploy_key, public: true) }
+
+ let!(:deploy_keys_project) do
+ create(:deploy_keys_project, project: project, deploy_key: deploy_key)
+ end
+
+ describe 'GET /deploy_keys' do
+ context 'when unauthenticated' do
+ it 'should return authentication error' do
+ get v3_api('/deploy_keys')
+
+ expect(response.status).to eq(401)
+ end
+ end
+
+ context 'when authenticated as non-admin user' do
+ it 'should return a 403 error' do
+ get v3_api('/deploy_keys', user)
+
+ expect(response.status).to eq(403)
+ end
+ end
+
+ context 'when authenticated as admin' do
+ it 'should return all deploy keys' do
+ get v3_api('/deploy_keys', admin)
+
+ expect(response.status).to eq(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first['id']).to eq(deploy_keys_project.deploy_key.id)
+ end
+ end
+ end
+
+ %w(deploy_keys keys).each do |path|
+ describe "GET /projects/:id/#{path}" do
+ before { deploy_key }
+
+ it 'should return array of ssh keys' do
+ get v3_api("/projects/#{project.id}/#{path}", admin)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first['title']).to eq(deploy_key.title)
+ end
+ end
+
+ describe "GET /projects/:id/#{path}/:key_id" do
+ it 'should return a single key' do
+ get v3_api("/projects/#{project.id}/#{path}/#{deploy_key.id}", admin)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['title']).to eq(deploy_key.title)
+ end
+
+ it 'should return 404 Not Found with invalid ID' do
+ get v3_api("/projects/#{project.id}/#{path}/404", admin)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ describe "POST /projects/:id/deploy_keys" do
+ it 'should not create an invalid ssh key' do
+ post v3_api("/projects/#{project.id}/#{path}", admin), { title: 'invalid key' }
+
+ expect(response).to have_http_status(400)
+ expect(json_response['error']).to eq('key is missing')
+ end
+
+ it 'should not create a key without title' do
+ post v3_api("/projects/#{project.id}/#{path}", admin), key: 'some key'
+
+ expect(response).to have_http_status(400)
+ expect(json_response['error']).to eq('title is missing')
+ end
+
+ it 'should create new ssh key' do
+ key_attrs = attributes_for :another_key
+
+ expect do
+ post v3_api("/projects/#{project.id}/#{path}", admin), key_attrs
+ end.to change{ project.deploy_keys.count }.by(1)
+ end
+
+ it 'returns an existing ssh key when attempting to add a duplicate' do
+ expect do
+ post v3_api("/projects/#{project.id}/#{path}", admin), { key: deploy_key.key, title: deploy_key.title }
+ end.not_to change { project.deploy_keys.count }
+
+ expect(response).to have_http_status(201)
+ end
+
+ it 'joins an existing ssh key to a new project' do
+ expect do
+ post v3_api("/projects/#{project2.id}/#{path}", admin), { key: deploy_key.key, title: deploy_key.title }
+ end.to change { project2.deploy_keys.count }.by(1)
+
+ expect(response).to have_http_status(201)
+ end
+ end
+
+ describe "DELETE /projects/:id/#{path}/:key_id" do
+ before { deploy_key }
+
+ it 'should delete existing key' do
+ expect do
+ delete v3_api("/projects/#{project.id}/#{path}/#{deploy_key.id}", admin)
+ end.to change{ project.deploy_keys.count }.by(-1)
+ end
+
+ it 'should return 404 Not Found with invalid ID' do
+ delete v3_api("/projects/#{project.id}/#{path}/404", admin)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ describe "POST /projects/:id/#{path}/:key_id/enable" do
+ let(:project2) { create(:empty_project) }
+
+ context 'when the user can admin the project' do
+ it 'enables the key' do
+ expect do
+ post v3_api("/projects/#{project2.id}/#{path}/#{deploy_key.id}/enable", admin)
+ end.to change { project2.deploy_keys.count }.from(0).to(1)
+
+ expect(response).to have_http_status(201)
+ expect(json_response['id']).to eq(deploy_key.id)
+ end
+ end
+
+ context 'when authenticated as non-admin user' do
+ it 'should return a 404 error' do
+ post v3_api("/projects/#{project2.id}/#{path}/#{deploy_key.id}/enable", user)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ describe "DELETE /projects/:id/deploy_keys/:key_id/disable" do
+ context 'when the user can admin the project' do
+ it 'disables the key' do
+ expect do
+ delete v3_api("/projects/#{project.id}/#{path}/#{deploy_key.id}/disable", admin)
+ end.to change { project.deploy_keys.count }.from(1).to(0)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['id']).to eq(deploy_key.id)
+ end
+ end
+
+ context 'when authenticated as non-admin user' do
+ it 'should return a 404 error' do
+ delete v3_api("/projects/#{project.id}/#{path}/#{deploy_key.id}/disable", user)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/v3/issues_spec.rb b/spec/requests/api/v3/issues_spec.rb
new file mode 100644
index 00000000000..33a127de98a
--- /dev/null
+++ b/spec/requests/api/v3/issues_spec.rb
@@ -0,0 +1,1259 @@
+require 'spec_helper'
+
+describe API::V3::Issues, api: true do
+ include ApiHelpers
+ include EmailHelpers
+
+ let(:user) { create(:user) }
+ let(:user2) { create(:user) }
+ let(:non_member) { create(:user) }
+ let(:guest) { create(:user) }
+ let(:author) { create(:author) }
+ let(:assignee) { create(:assignee) }
+ let(:admin) { create(:user, :admin) }
+ let!(:project) { create(:empty_project, :public, creator_id: user.id, namespace: user.namespace ) }
+ let!(:closed_issue) do
+ create :closed_issue,
+ author: user,
+ assignee: user,
+ project: project,
+ state: :closed,
+ milestone: milestone,
+ created_at: generate(:issue_created_at),
+ updated_at: 3.hours.ago
+ end
+ let!(:confidential_issue) do
+ create :issue,
+ :confidential,
+ project: project,
+ author: author,
+ assignee: assignee,
+ created_at: generate(:issue_created_at),
+ updated_at: 2.hours.ago
+ end
+ let!(:issue) do
+ create :issue,
+ author: user,
+ assignee: user,
+ project: project,
+ milestone: milestone,
+ created_at: generate(:issue_created_at),
+ updated_at: 1.hour.ago
+ end
+ let!(:label) do
+ create(:label, title: 'label', color: '#FFAABB', project: project)
+ end
+ let!(:label_link) { create(:label_link, label: label, target: issue) }
+ let!(:milestone) { create(:milestone, title: '1.0.0', project: project) }
+ let!(:empty_milestone) do
+ create(:milestone, title: '2.0.0', project: project)
+ end
+ let!(:note) { create(:note_on_issue, author: user, project: project, noteable: issue) }
+
+ let(:no_milestone_title) { URI.escape(Milestone::None.title) }
+
+ before do
+ project.team << [user, :reporter]
+ project.team << [guest, :guest]
+ end
+
+ describe "GET /issues" do
+ context "when unauthenticated" do
+ it "returns authentication error" do
+ get v3_api("/issues")
+
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ context "when authenticated" do
+ it "returns an array of issues" do
+ get v3_api("/issues", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first['title']).to eq(issue.title)
+ expect(json_response.last).to have_key('web_url')
+ end
+
+ it 'returns an array of closed issues' do
+ get v3_api('/issues?state=closed', user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['id']).to eq(closed_issue.id)
+ end
+
+ it 'returns an array of opened issues' do
+ get v3_api('/issues?state=opened', user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['id']).to eq(issue.id)
+ end
+
+ it 'returns an array of all issues' do
+ get v3_api('/issues?state=all', user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(2)
+ expect(json_response.first['id']).to eq(issue.id)
+ expect(json_response.second['id']).to eq(closed_issue.id)
+ end
+
+ it 'returns an array of labeled issues' do
+ get v3_api("/issues?labels=#{label.title}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['labels']).to eq([label.title])
+ end
+
+ it 'returns an array of labeled issues when at least one label matches' do
+ get v3_api("/issues?labels=#{label.title},foo,bar", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['labels']).to eq([label.title])
+ end
+
+ it 'returns an empty array if no issue matches labels' do
+ get v3_api('/issues?labels=foo,bar', user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(0)
+ end
+
+ it 'returns an array of labeled issues matching given state' do
+ get v3_api("/issues?labels=#{label.title}&state=opened", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['labels']).to eq([label.title])
+ expect(json_response.first['state']).to eq('opened')
+ end
+
+ it 'returns an empty array if no issue matches labels and state filters' do
+ get v3_api("/issues?labels=#{label.title}&state=closed", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(0)
+ end
+
+ it 'returns an empty array if no issue matches milestone' do
+ get v3_api("/issues?milestone=#{empty_milestone.title}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(0)
+ end
+
+ it 'returns an empty array if milestone does not exist' do
+ get v3_api("/issues?milestone=foo", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(0)
+ end
+
+ it 'returns an array of issues in given milestone' do
+ get v3_api("/issues?milestone=#{milestone.title}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(2)
+ expect(json_response.first['id']).to eq(issue.id)
+ expect(json_response.second['id']).to eq(closed_issue.id)
+ end
+
+ it 'returns an array of issues matching state in milestone' do
+ get v3_api("/issues?milestone=#{milestone.title}", user),
+ '&state=closed'
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['id']).to eq(closed_issue.id)
+ end
+
+ it 'returns an array of issues with no milestone' do
+ get v3_api("/issues?milestone=#{no_milestone_title}", author)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['id']).to eq(confidential_issue.id)
+ end
+
+ it 'sorts by created_at descending by default' do
+ get v3_api('/issues', user)
+
+ response_dates = json_response.map { |issue| issue['created_at'] }
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(response_dates).to eq(response_dates.sort.reverse)
+ end
+
+ it 'sorts ascending when requested' do
+ get v3_api('/issues?sort=asc', user)
+
+ response_dates = json_response.map { |issue| issue['created_at'] }
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(response_dates).to eq(response_dates.sort)
+ end
+
+ it 'sorts by updated_at descending when requested' do
+ get v3_api('/issues?order_by=updated_at', user)
+
+ response_dates = json_response.map { |issue| issue['updated_at'] }
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(response_dates).to eq(response_dates.sort.reverse)
+ end
+
+ it 'sorts by updated_at ascending when requested' do
+ get v3_api('/issues?order_by=updated_at&sort=asc', user)
+
+ response_dates = json_response.map { |issue| issue['updated_at'] }
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(response_dates).to eq(response_dates.sort)
+ end
+ end
+ end
+
+ describe "GET /groups/:id/issues" do
+ let!(:group) { create(:group) }
+ let!(:group_project) { create(:empty_project, :public, creator_id: user.id, namespace: group) }
+ let!(:group_closed_issue) do
+ create :closed_issue,
+ author: user,
+ assignee: user,
+ project: group_project,
+ state: :closed,
+ milestone: group_milestone,
+ updated_at: 3.hours.ago
+ end
+ let!(:group_confidential_issue) do
+ create :issue,
+ :confidential,
+ project: group_project,
+ author: author,
+ assignee: assignee,
+ updated_at: 2.hours.ago
+ end
+ let!(:group_issue) do
+ create :issue,
+ author: user,
+ assignee: user,
+ project: group_project,
+ milestone: group_milestone,
+ updated_at: 1.hour.ago
+ end
+ let!(:group_label) do
+ create(:label, title: 'group_lbl', color: '#FFAABB', project: group_project)
+ end
+ let!(:group_label_link) { create(:label_link, label: group_label, target: group_issue) }
+ let!(:group_milestone) { create(:milestone, title: '3.0.0', project: group_project) }
+ let!(:group_empty_milestone) do
+ create(:milestone, title: '4.0.0', project: group_project)
+ end
+ let!(:group_note) { create(:note_on_issue, author: user, project: group_project, noteable: group_issue) }
+
+ before do
+ group_project.team << [user, :reporter]
+ end
+ let(:base_url) { "/groups/#{group.id}/issues" }
+
+ it 'returns group issues without confidential issues for non project members' do
+ get v3_api(base_url, non_member)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['title']).to eq(group_issue.title)
+ end
+
+ it 'returns group confidential issues for author' do
+ get v3_api(base_url, author)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(2)
+ end
+
+ it 'returns group confidential issues for assignee' do
+ get v3_api(base_url, assignee)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(2)
+ end
+
+ it 'returns group issues with confidential issues for project members' do
+ get v3_api(base_url, user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(2)
+ end
+
+ it 'returns group confidential issues for admin' do
+ get v3_api(base_url, admin)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(2)
+ end
+
+ it 'returns an array of labeled group issues' do
+ get v3_api("#{base_url}?labels=#{group_label.title}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['labels']).to eq([group_label.title])
+ end
+
+ it 'returns an array of labeled group issues where all labels match' do
+ get v3_api("#{base_url}?labels=#{group_label.title},foo,bar", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(0)
+ end
+
+ it 'returns an empty array if no group issue matches labels' do
+ get v3_api("#{base_url}?labels=foo,bar", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(0)
+ end
+
+ it 'returns an empty array if no issue matches milestone' do
+ get v3_api("#{base_url}?milestone=#{group_empty_milestone.title}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(0)
+ end
+
+ it 'returns an empty array if milestone does not exist' do
+ get v3_api("#{base_url}?milestone=foo", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(0)
+ end
+
+ it 'returns an array of issues in given milestone' do
+ get v3_api("#{base_url}?milestone=#{group_milestone.title}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['id']).to eq(group_issue.id)
+ end
+
+ it 'returns an array of issues matching state in milestone' do
+ get v3_api("#{base_url}?milestone=#{group_milestone.title}", user),
+ '&state=closed'
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['id']).to eq(group_closed_issue.id)
+ end
+
+ it 'returns an array of issues with no milestone' do
+ get v3_api("#{base_url}?milestone=#{no_milestone_title}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['id']).to eq(group_confidential_issue.id)
+ end
+
+ it 'sorts by created_at descending by default' do
+ get v3_api(base_url, user)
+
+ response_dates = json_response.map { |issue| issue['created_at'] }
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(response_dates).to eq(response_dates.sort.reverse)
+ end
+
+ it 'sorts ascending when requested' do
+ get v3_api("#{base_url}?sort=asc", user)
+
+ response_dates = json_response.map { |issue| issue['created_at'] }
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(response_dates).to eq(response_dates.sort)
+ end
+
+ it 'sorts by updated_at descending when requested' do
+ get v3_api("#{base_url}?order_by=updated_at", user)
+
+ response_dates = json_response.map { |issue| issue['updated_at'] }
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(response_dates).to eq(response_dates.sort.reverse)
+ end
+
+ it 'sorts by updated_at ascending when requested' do
+ get v3_api("#{base_url}?order_by=updated_at&sort=asc", user)
+
+ response_dates = json_response.map { |issue| issue['updated_at'] }
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(response_dates).to eq(response_dates.sort)
+ end
+ end
+
+ describe "GET /projects/:id/issues" do
+ let(:base_url) { "/projects/#{project.id}" }
+
+ it "returns 404 on private projects for other users" do
+ private_project = create(:empty_project, :private)
+ create(:issue, project: private_project)
+
+ get v3_api("/projects/#{private_project.id}/issues", non_member)
+
+ expect(response).to have_http_status(404)
+ end
+
+ it 'returns no issues when user has access to project but not issues' do
+ restricted_project = create(:empty_project, :public, issues_access_level: ProjectFeature::PRIVATE)
+ create(:issue, project: restricted_project)
+
+ get v3_api("/projects/#{restricted_project.id}/issues", non_member)
+
+ expect(json_response).to eq([])
+ end
+
+ it 'returns project issues without confidential issues for non project members' do
+ get v3_api("#{base_url}/issues", non_member)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(2)
+ expect(json_response.first['title']).to eq(issue.title)
+ end
+
+ it 'returns project issues without confidential issues for project members with guest role' do
+ get v3_api("#{base_url}/issues", guest)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(2)
+ expect(json_response.first['title']).to eq(issue.title)
+ end
+
+ it 'returns project confidential issues for author' do
+ get v3_api("#{base_url}/issues", author)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(3)
+ expect(json_response.first['title']).to eq(issue.title)
+ end
+
+ it 'returns project confidential issues for assignee' do
+ get v3_api("#{base_url}/issues", assignee)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(3)
+ expect(json_response.first['title']).to eq(issue.title)
+ end
+
+ it 'returns project issues with confidential issues for project members' do
+ get v3_api("#{base_url}/issues", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(3)
+ expect(json_response.first['title']).to eq(issue.title)
+ end
+
+ it 'returns project confidential issues for admin' do
+ get v3_api("#{base_url}/issues", admin)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(3)
+ expect(json_response.first['title']).to eq(issue.title)
+ end
+
+ it 'returns an array of labeled project issues' do
+ get v3_api("#{base_url}/issues?labels=#{label.title}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['labels']).to eq([label.title])
+ end
+
+ it 'returns an array of labeled project issues where all labels match' do
+ get v3_api("#{base_url}/issues?labels=#{label.title},foo,bar", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['labels']).to eq([label.title])
+ end
+
+ it 'returns an empty array if no project issue matches labels' do
+ get v3_api("#{base_url}/issues?labels=foo,bar", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(0)
+ end
+
+ it 'returns an empty array if no issue matches milestone' do
+ get v3_api("#{base_url}/issues?milestone=#{empty_milestone.title}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(0)
+ end
+
+ it 'returns an empty array if milestone does not exist' do
+ get v3_api("#{base_url}/issues?milestone=foo", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(0)
+ end
+
+ it 'returns an array of issues in given milestone' do
+ get v3_api("#{base_url}/issues?milestone=#{milestone.title}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(2)
+ expect(json_response.first['id']).to eq(issue.id)
+ expect(json_response.second['id']).to eq(closed_issue.id)
+ end
+
+ it 'returns an array of issues matching state in milestone' do
+ get v3_api("#{base_url}/issues?milestone=#{milestone.title}", user),
+ '&state=closed'
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['id']).to eq(closed_issue.id)
+ end
+
+ it 'returns an array of issues with no milestone' do
+ get v3_api("#{base_url}/issues?milestone=#{no_milestone_title}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['id']).to eq(confidential_issue.id)
+ end
+
+ it 'sorts by created_at descending by default' do
+ get v3_api("#{base_url}/issues", user)
+
+ response_dates = json_response.map { |issue| issue['created_at'] }
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(response_dates).to eq(response_dates.sort.reverse)
+ end
+
+ it 'sorts ascending when requested' do
+ get v3_api("#{base_url}/issues?sort=asc", user)
+
+ response_dates = json_response.map { |issue| issue['created_at'] }
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(response_dates).to eq(response_dates.sort)
+ end
+
+ it 'sorts by updated_at descending when requested' do
+ get v3_api("#{base_url}/issues?order_by=updated_at", user)
+
+ response_dates = json_response.map { |issue| issue['updated_at'] }
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(response_dates).to eq(response_dates.sort.reverse)
+ end
+
+ it 'sorts by updated_at ascending when requested' do
+ get v3_api("#{base_url}/issues?order_by=updated_at&sort=asc", user)
+
+ response_dates = json_response.map { |issue| issue['updated_at'] }
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(response_dates).to eq(response_dates.sort)
+ end
+ end
+
+ describe "GET /projects/:id/issues/:issue_id" do
+ it 'exposes known attributes' do
+ get v3_api("/projects/#{project.id}/issues/#{issue.id}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['id']).to eq(issue.id)
+ expect(json_response['iid']).to eq(issue.iid)
+ expect(json_response['project_id']).to eq(issue.project.id)
+ expect(json_response['title']).to eq(issue.title)
+ expect(json_response['description']).to eq(issue.description)
+ expect(json_response['state']).to eq(issue.state)
+ expect(json_response['created_at']).to be_present
+ expect(json_response['updated_at']).to be_present
+ expect(json_response['labels']).to eq(issue.label_names)
+ expect(json_response['milestone']).to be_a Hash
+ expect(json_response['assignee']).to be_a Hash
+ expect(json_response['author']).to be_a Hash
+ expect(json_response['confidential']).to be_falsy
+ end
+
+ it "returns a project issue by id" do
+ get v3_api("/projects/#{project.id}/issues/#{issue.id}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['title']).to eq(issue.title)
+ expect(json_response['iid']).to eq(issue.iid)
+ end
+
+ it 'returns a project issue by iid' do
+ get v3_api("/projects/#{project.id}/issues?iid=#{issue.iid}", user)
+
+ expect(response.status).to eq 200
+ expect(json_response.length).to eq 1
+ expect(json_response.first['title']).to eq issue.title
+ expect(json_response.first['id']).to eq issue.id
+ expect(json_response.first['iid']).to eq issue.iid
+ end
+
+ it 'returns an empty array for an unknown project issue iid' do
+ get v3_api("/projects/#{project.id}/issues?iid=#{issue.iid + 10}", user)
+
+ expect(response.status).to eq 200
+ expect(json_response.length).to eq 0
+ end
+
+ it "returns 404 if issue id not found" do
+ get v3_api("/projects/#{project.id}/issues/54321", user)
+
+ expect(response).to have_http_status(404)
+ end
+
+ context 'confidential issues' do
+ it "returns 404 for non project members" do
+ get v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", non_member)
+
+ expect(response).to have_http_status(404)
+ end
+
+ it "returns 404 for project members with guest role" do
+ get v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", guest)
+
+ expect(response).to have_http_status(404)
+ end
+
+ it "returns confidential issue for project members" do
+ get v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['title']).to eq(confidential_issue.title)
+ expect(json_response['iid']).to eq(confidential_issue.iid)
+ end
+
+ it "returns confidential issue for author" do
+ get v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", author)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['title']).to eq(confidential_issue.title)
+ expect(json_response['iid']).to eq(confidential_issue.iid)
+ end
+
+ it "returns confidential issue for assignee" do
+ get v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", assignee)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['title']).to eq(confidential_issue.title)
+ expect(json_response['iid']).to eq(confidential_issue.iid)
+ end
+
+ it "returns confidential issue for admin" do
+ get v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", admin)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['title']).to eq(confidential_issue.title)
+ expect(json_response['iid']).to eq(confidential_issue.iid)
+ end
+ end
+ end
+
+ describe "POST /projects/:id/issues" do
+ it 'creates a new project issue' do
+ post v3_api("/projects/#{project.id}/issues", user),
+ title: 'new issue', labels: 'label, label2'
+
+ expect(response).to have_http_status(201)
+ expect(json_response['title']).to eq('new issue')
+ expect(json_response['description']).to be_nil
+ expect(json_response['labels']).to eq(['label', 'label2'])
+ expect(json_response['confidential']).to be_falsy
+ end
+
+ it 'creates a new confidential project issue' do
+ post v3_api("/projects/#{project.id}/issues", user),
+ title: 'new issue', confidential: true
+
+ expect(response).to have_http_status(201)
+ expect(json_response['title']).to eq('new issue')
+ expect(json_response['confidential']).to be_truthy
+ end
+
+ it 'creates a new confidential project issue with a different param' do
+ post v3_api("/projects/#{project.id}/issues", user),
+ title: 'new issue', confidential: 'y'
+
+ expect(response).to have_http_status(201)
+ expect(json_response['title']).to eq('new issue')
+ expect(json_response['confidential']).to be_truthy
+ end
+
+ it 'creates a public issue when confidential param is false' do
+ post v3_api("/projects/#{project.id}/issues", user),
+ title: 'new issue', confidential: false
+
+ expect(response).to have_http_status(201)
+ expect(json_response['title']).to eq('new issue')
+ expect(json_response['confidential']).to be_falsy
+ end
+
+ it 'creates a public issue when confidential param is invalid' do
+ post v3_api("/projects/#{project.id}/issues", user),
+ title: 'new issue', confidential: 'foo'
+
+ expect(response).to have_http_status(400)
+ expect(json_response['error']).to eq('confidential is invalid')
+ end
+
+ it "sends notifications for subscribers of newly added labels" do
+ label = project.labels.first
+ label.toggle_subscription(user2, project)
+
+ perform_enqueued_jobs do
+ post v3_api("/projects/#{project.id}/issues", user),
+ title: 'new issue', labels: label.title
+ end
+
+ should_email(user2)
+ end
+
+ it "returns a 400 bad request if title not given" do
+ post v3_api("/projects/#{project.id}/issues", user), labels: 'label, label2'
+
+ expect(response).to have_http_status(400)
+ end
+
+ it 'allows special label names' do
+ post v3_api("/projects/#{project.id}/issues", user),
+ title: 'new issue',
+ labels: 'label, label?, label&foo, ?, &'
+
+ expect(response.status).to eq(201)
+ expect(json_response['labels']).to include 'label'
+ expect(json_response['labels']).to include 'label?'
+ expect(json_response['labels']).to include 'label&foo'
+ expect(json_response['labels']).to include '?'
+ expect(json_response['labels']).to include '&'
+ end
+
+ it 'returns 400 if title is too long' do
+ post v3_api("/projects/#{project.id}/issues", user),
+ title: 'g' * 256
+
+ expect(response).to have_http_status(400)
+ expect(json_response['message']['title']).to eq([
+ 'is too long (maximum is 255 characters)'
+ ])
+ end
+
+ context 'resolving issues in a merge request' do
+ let(:discussion) { Discussion.for_diff_notes([create(:diff_note_on_merge_request)]).first }
+ let(:merge_request) { discussion.noteable }
+ let(:project) { merge_request.source_project }
+ before do
+ project.team << [user, :master]
+ post v3_api("/projects/#{project.id}/issues", user),
+ title: 'New Issue',
+ merge_request_for_resolving_discussions: merge_request.iid
+ end
+
+ it 'creates a new project issue' do
+ expect(response).to have_http_status(:created)
+ end
+
+ it 'resolves the discussions in a merge request' do
+ discussion.first_note.reload
+
+ expect(discussion.resolved?).to be(true)
+ end
+
+ it 'assigns a description to the issue mentioning the merge request' do
+ expect(json_response['description']).to include(merge_request.to_reference)
+ end
+ end
+
+ context 'with due date' do
+ it 'creates a new project issue' do
+ due_date = 2.weeks.from_now.strftime('%Y-%m-%d')
+
+ post v3_api("/projects/#{project.id}/issues", user),
+ title: 'new issue', due_date: due_date
+
+ expect(response).to have_http_status(201)
+ expect(json_response['title']).to eq('new issue')
+ expect(json_response['description']).to be_nil
+ expect(json_response['due_date']).to eq(due_date)
+ end
+ end
+
+ context 'when an admin or owner makes the request' do
+ it 'accepts the creation date to be set' do
+ creation_time = 2.weeks.ago
+ post v3_api("/projects/#{project.id}/issues", user),
+ title: 'new issue', labels: 'label, label2', created_at: creation_time
+
+ expect(response).to have_http_status(201)
+ expect(Time.parse(json_response['created_at'])).to be_like_time(creation_time)
+ end
+ end
+
+ context 'the user can only read the issue' do
+ it 'cannot create new labels' do
+ expect do
+ post v3_api("/projects/#{project.id}/issues", non_member), title: 'new issue', labels: 'label, label2'
+ end.not_to change { project.labels.count }
+ end
+ end
+ end
+
+ describe 'POST /projects/:id/issues with spam filtering' do
+ before do
+ allow_any_instance_of(SpamService).to receive(:check_for_spam?).and_return(true)
+ allow_any_instance_of(AkismetService).to receive_messages(is_spam?: true)
+ end
+
+ let(:params) do
+ {
+ title: 'new issue',
+ description: 'content here',
+ labels: 'label, label2'
+ }
+ end
+
+ it "does not create a new project issue" do
+ expect { post v3_api("/projects/#{project.id}/issues", user), params }.not_to change(Issue, :count)
+
+ expect(response).to have_http_status(400)
+ expect(json_response['message']).to eq({ "error" => "Spam detected" })
+
+ spam_logs = SpamLog.all
+
+ expect(spam_logs.count).to eq(1)
+ expect(spam_logs[0].title).to eq('new issue')
+ expect(spam_logs[0].description).to eq('content here')
+ expect(spam_logs[0].user).to eq(user)
+ expect(spam_logs[0].noteable_type).to eq('Issue')
+ end
+ end
+
+ describe "PUT /projects/:id/issues/:issue_id to update only title" do
+ it "updates a project issue" do
+ put v3_api("/projects/#{project.id}/issues/#{issue.id}", user),
+ title: 'updated title'
+
+ expect(response).to have_http_status(200)
+ expect(json_response['title']).to eq('updated title')
+ end
+
+ it "returns 404 error if issue id not found" do
+ put v3_api("/projects/#{project.id}/issues/44444", user),
+ title: 'updated title'
+
+ expect(response).to have_http_status(404)
+ end
+
+ it 'allows special label names' do
+ put v3_api("/projects/#{project.id}/issues/#{issue.id}", user),
+ title: 'updated title',
+ labels: 'label, label?, label&foo, ?, &'
+
+ expect(response.status).to eq(200)
+ expect(json_response['labels']).to include 'label'
+ expect(json_response['labels']).to include 'label?'
+ expect(json_response['labels']).to include 'label&foo'
+ expect(json_response['labels']).to include '?'
+ expect(json_response['labels']).to include '&'
+ end
+
+ context 'confidential issues' do
+ it "returns 403 for non project members" do
+ put v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", non_member),
+ title: 'updated title'
+
+ expect(response).to have_http_status(403)
+ end
+
+ it "returns 403 for project members with guest role" do
+ put v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", guest),
+ title: 'updated title'
+
+ expect(response).to have_http_status(403)
+ end
+
+ it "updates a confidential issue for project members" do
+ put v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", user),
+ title: 'updated title'
+
+ expect(response).to have_http_status(200)
+ expect(json_response['title']).to eq('updated title')
+ end
+
+ it "updates a confidential issue for author" do
+ put v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", author),
+ title: 'updated title'
+
+ expect(response).to have_http_status(200)
+ expect(json_response['title']).to eq('updated title')
+ end
+
+ it "updates a confidential issue for admin" do
+ put v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", admin),
+ title: 'updated title'
+
+ expect(response).to have_http_status(200)
+ expect(json_response['title']).to eq('updated title')
+ end
+
+ it 'sets an issue to confidential' do
+ put v3_api("/projects/#{project.id}/issues/#{issue.id}", user),
+ confidential: true
+
+ expect(response).to have_http_status(200)
+ expect(json_response['confidential']).to be_truthy
+ end
+
+ it 'makes a confidential issue public' do
+ put v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", user),
+ confidential: false
+
+ expect(response).to have_http_status(200)
+ expect(json_response['confidential']).to be_falsy
+ end
+
+ it 'does not update a confidential issue with wrong confidential flag' do
+ put v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", user),
+ confidential: 'foo'
+
+ expect(response).to have_http_status(400)
+ expect(json_response['error']).to eq('confidential is invalid')
+ end
+ end
+ end
+
+ describe 'PUT /projects/:id/issues/:issue_id to update labels' do
+ let!(:label) { create(:label, title: 'dummy', project: project) }
+ let!(:label_link) { create(:label_link, label: label, target: issue) }
+
+ it 'does not update labels if not present' do
+ put v3_api("/projects/#{project.id}/issues/#{issue.id}", user),
+ title: 'updated title'
+
+ expect(response).to have_http_status(200)
+ expect(json_response['labels']).to eq([label.title])
+ end
+
+ it "sends notifications for subscribers of newly added labels when issue is updated" do
+ label = create(:label, title: 'foo', color: '#FFAABB', project: project)
+ label.toggle_subscription(user2, project)
+
+ perform_enqueued_jobs do
+ put v3_api("/projects/#{project.id}/issues/#{issue.id}", user),
+ title: 'updated title', labels: label.title
+ end
+
+ should_email(user2)
+ end
+
+ it 'removes all labels' do
+ put v3_api("/projects/#{project.id}/issues/#{issue.id}", user), labels: ''
+
+ expect(response).to have_http_status(200)
+ expect(json_response['labels']).to eq([])
+ end
+
+ it 'updates labels' do
+ put v3_api("/projects/#{project.id}/issues/#{issue.id}", user),
+ labels: 'foo,bar'
+
+ expect(response).to have_http_status(200)
+ expect(json_response['labels']).to include 'foo'
+ expect(json_response['labels']).to include 'bar'
+ end
+
+ it 'allows special label names' do
+ put v3_api("/projects/#{project.id}/issues/#{issue.id}", user),
+ labels: 'label:foo, label-bar,label_bar,label/bar,label?bar,label&bar,?,&'
+
+ expect(response.status).to eq(200)
+ expect(json_response['labels']).to include 'label:foo'
+ expect(json_response['labels']).to include 'label-bar'
+ expect(json_response['labels']).to include 'label_bar'
+ expect(json_response['labels']).to include 'label/bar'
+ expect(json_response['labels']).to include 'label?bar'
+ expect(json_response['labels']).to include 'label&bar'
+ expect(json_response['labels']).to include '?'
+ expect(json_response['labels']).to include '&'
+ end
+
+ it 'returns 400 if title is too long' do
+ put v3_api("/projects/#{project.id}/issues/#{issue.id}", user),
+ title: 'g' * 256
+
+ expect(response).to have_http_status(400)
+ expect(json_response['message']['title']).to eq([
+ 'is too long (maximum is 255 characters)'
+ ])
+ end
+ end
+
+ describe "PUT /projects/:id/issues/:issue_id to update state and label" do
+ it "updates a project issue" do
+ put v3_api("/projects/#{project.id}/issues/#{issue.id}", user),
+ labels: 'label2', state_event: "close"
+
+ expect(response).to have_http_status(200)
+ expect(json_response['labels']).to include 'label2'
+ expect(json_response['state']).to eq "closed"
+ end
+
+ it 'reopens a project isssue' do
+ put v3_api("/projects/#{project.id}/issues/#{closed_issue.id}", user), state_event: 'reopen'
+
+ expect(response).to have_http_status(200)
+ expect(json_response['state']).to eq 'reopened'
+ end
+
+ context 'when an admin or owner makes the request' do
+ it 'accepts the update date to be set' do
+ update_time = 2.weeks.ago
+ put v3_api("/projects/#{project.id}/issues/#{issue.id}", user),
+ labels: 'label3', state_event: 'close', updated_at: update_time
+
+ expect(response).to have_http_status(200)
+ expect(json_response['labels']).to include 'label3'
+ expect(Time.parse(json_response['updated_at'])).to be_like_time(update_time)
+ end
+ end
+ end
+
+ describe 'PUT /projects/:id/issues/:issue_id to update due date' do
+ it 'creates a new project issue' do
+ due_date = 2.weeks.from_now.strftime('%Y-%m-%d')
+
+ put v3_api("/projects/#{project.id}/issues/#{issue.id}", user), due_date: due_date
+
+ expect(response).to have_http_status(200)
+ expect(json_response['due_date']).to eq(due_date)
+ end
+ end
+
+ describe "DELETE /projects/:id/issues/:issue_id" do
+ it "rejects a non member from deleting an issue" do
+ delete v3_api("/projects/#{project.id}/issues/#{issue.id}", non_member)
+
+ expect(response).to have_http_status(403)
+ end
+
+ it "rejects a developer from deleting an issue" do
+ delete v3_api("/projects/#{project.id}/issues/#{issue.id}", author)
+
+ expect(response).to have_http_status(403)
+ end
+
+ context "when the user is project owner" do
+ let(:owner) { create(:user) }
+ let(:project) { create(:empty_project, namespace: owner.namespace) }
+
+ it "deletes the issue if an admin requests it" do
+ delete v3_api("/projects/#{project.id}/issues/#{issue.id}", owner)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['state']).to eq 'opened'
+ end
+ end
+
+ context 'when issue does not exist' do
+ it 'returns 404 when trying to move an issue' do
+ delete v3_api("/projects/#{project.id}/issues/123", user)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ describe '/projects/:id/issues/:issue_id/move' do
+ let!(:target_project) { create(:empty_project, path: 'project2', creator_id: user.id, namespace: user.namespace ) }
+ let!(:target_project2) { create(:empty_project, creator_id: non_member.id, namespace: non_member.namespace ) }
+
+ it 'moves an issue' do
+ post v3_api("/projects/#{project.id}/issues/#{issue.id}/move", user),
+ to_project_id: target_project.id
+
+ expect(response).to have_http_status(201)
+ expect(json_response['project_id']).to eq(target_project.id)
+ end
+
+ context 'when source and target projects are the same' do
+ it 'returns 400 when trying to move an issue' do
+ post v3_api("/projects/#{project.id}/issues/#{issue.id}/move", user),
+ to_project_id: project.id
+
+ expect(response).to have_http_status(400)
+ expect(json_response['message']).to eq('Cannot move issue to project it originates from!')
+ end
+ end
+
+ context 'when the user does not have the permission to move issues' do
+ it 'returns 400 when trying to move an issue' do
+ post v3_api("/projects/#{project.id}/issues/#{issue.id}/move", user),
+ to_project_id: target_project2.id
+
+ expect(response).to have_http_status(400)
+ expect(json_response['message']).to eq('Cannot move issue due to insufficient permissions!')
+ end
+ end
+
+ it 'moves the issue to another namespace if I am admin' do
+ post v3_api("/projects/#{project.id}/issues/#{issue.id}/move", admin),
+ to_project_id: target_project2.id
+
+ expect(response).to have_http_status(201)
+ expect(json_response['project_id']).to eq(target_project2.id)
+ end
+
+ context 'when issue does not exist' do
+ it 'returns 404 when trying to move an issue' do
+ post v3_api("/projects/#{project.id}/issues/123/move", user),
+ to_project_id: target_project.id
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 Issue Not Found')
+ end
+ end
+
+ context 'when source project does not exist' do
+ it 'returns 404 when trying to move an issue' do
+ post v3_api("/projects/123/issues/#{issue.id}/move", user),
+ to_project_id: target_project.id
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 Project Not Found')
+ end
+ end
+
+ context 'when target project does not exist' do
+ it 'returns 404 when trying to move an issue' do
+ post v3_api("/projects/#{project.id}/issues/#{issue.id}/move", user),
+ to_project_id: 123
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ describe 'POST :id/issues/:issue_id/subscription' do
+ it 'subscribes to an issue' do
+ post v3_api("/projects/#{project.id}/issues/#{issue.id}/subscription", user2)
+
+ expect(response).to have_http_status(201)
+ expect(json_response['subscribed']).to eq(true)
+ end
+
+ it 'returns 304 if already subscribed' do
+ post v3_api("/projects/#{project.id}/issues/#{issue.id}/subscription", user)
+
+ expect(response).to have_http_status(304)
+ end
+
+ it 'returns 404 if the issue is not found' do
+ post v3_api("/projects/#{project.id}/issues/123/subscription", user)
+
+ expect(response).to have_http_status(404)
+ end
+
+ it 'returns 404 if the issue is confidential' do
+ post v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}/subscription", non_member)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ describe 'DELETE :id/issues/:issue_id/subscription' do
+ it 'unsubscribes from an issue' do
+ delete v3_api("/projects/#{project.id}/issues/#{issue.id}/subscription", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['subscribed']).to eq(false)
+ end
+
+ it 'returns 304 if not subscribed' do
+ delete v3_api("/projects/#{project.id}/issues/#{issue.id}/subscription", user2)
+
+ expect(response).to have_http_status(304)
+ end
+
+ it 'returns 404 if the issue is not found' do
+ delete v3_api("/projects/#{project.id}/issues/123/subscription", user)
+
+ expect(response).to have_http_status(404)
+ end
+
+ it 'returns 404 if the issue is confidential' do
+ delete v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}/subscription", non_member)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ describe 'time tracking endpoints' do
+ let(:issuable) { issue }
+
+ include_examples 'time tracking endpoints', 'issue'
+ end
+end
diff --git a/spec/requests/api/v3/merge_requests_spec.rb b/spec/requests/api/v3/merge_requests_spec.rb
new file mode 100644
index 00000000000..b94e1ef4ced
--- /dev/null
+++ b/spec/requests/api/v3/merge_requests_spec.rb
@@ -0,0 +1,726 @@
+require "spec_helper"
+
+describe API::MergeRequests, api: true do
+ include ApiHelpers
+ let(:base_time) { Time.now }
+ let(:user) { create(:user) }
+ let(:admin) { create(:user, :admin) }
+ let(:non_member) { create(:user) }
+ let!(:project) { create(:project, :public, :repository, creator: user, namespace: user.namespace) }
+ let!(:merge_request) { create(:merge_request, :simple, author: user, assignee: user, source_project: project, title: "Test", created_at: base_time) }
+ let!(:merge_request_closed) { create(:merge_request, state: "closed", author: user, assignee: user, source_project: project, title: "Closed test", created_at: base_time + 1.second) }
+ let!(:merge_request_merged) { create(:merge_request, state: "merged", author: user, assignee: user, source_project: project, title: "Merged test", created_at: base_time + 2.seconds, merge_commit_sha: '9999999999999999999999999999999999999999') }
+ let(:milestone) { create(:milestone, title: '1.0.0', project: project) }
+
+ before do
+ project.team << [user, :reporter]
+ end
+
+ describe "GET /projects/:id/merge_requests" do
+ context "when unauthenticated" do
+ it "returns authentication error" do
+ get v3_api("/projects/#{project.id}/merge_requests")
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ context "when authenticated" do
+ it "returns an array of all merge_requests" do
+ get v3_api("/projects/#{project.id}/merge_requests", user)
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(3)
+ expect(json_response.last['title']).to eq(merge_request.title)
+ expect(json_response.last).to have_key('web_url')
+ expect(json_response.last['sha']).to eq(merge_request.diff_head_sha)
+ expect(json_response.last['merge_commit_sha']).to be_nil
+ expect(json_response.last['merge_commit_sha']).to eq(merge_request.merge_commit_sha)
+ expect(json_response.first['title']).to eq(merge_request_merged.title)
+ expect(json_response.first['sha']).to eq(merge_request_merged.diff_head_sha)
+ expect(json_response.first['merge_commit_sha']).not_to be_nil
+ expect(json_response.first['merge_commit_sha']).to eq(merge_request_merged.merge_commit_sha)
+ end
+
+ it "returns an array of all merge_requests" do
+ get v3_api("/projects/#{project.id}/merge_requests?state", user)
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(3)
+ expect(json_response.last['title']).to eq(merge_request.title)
+ end
+
+ it "returns an array of open merge_requests" do
+ get v3_api("/projects/#{project.id}/merge_requests?state=opened", user)
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.last['title']).to eq(merge_request.title)
+ end
+
+ it "returns an array of closed merge_requests" do
+ get v3_api("/projects/#{project.id}/merge_requests?state=closed", user)
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['title']).to eq(merge_request_closed.title)
+ end
+
+ it "returns an array of merged merge_requests" do
+ get v3_api("/projects/#{project.id}/merge_requests?state=merged", user)
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['title']).to eq(merge_request_merged.title)
+ end
+
+ context "with ordering" do
+ before do
+ @mr_later = mr_with_later_created_and_updated_at_time
+ @mr_earlier = mr_with_earlier_created_and_updated_at_time
+ end
+
+ it "returns an array of merge_requests in ascending order" do
+ get v3_api("/projects/#{project.id}/merge_requests?sort=asc", user)
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(3)
+ response_dates = json_response.map{ |merge_request| merge_request['created_at'] }
+ expect(response_dates).to eq(response_dates.sort)
+ end
+
+ it "returns an array of merge_requests in descending order" do
+ get v3_api("/projects/#{project.id}/merge_requests?sort=desc", user)
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(3)
+ response_dates = json_response.map{ |merge_request| merge_request['created_at'] }
+ expect(response_dates).to eq(response_dates.sort.reverse)
+ end
+
+ it "returns an array of merge_requests ordered by updated_at" do
+ get v3_api("/projects/#{project.id}/merge_requests?order_by=updated_at", user)
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(3)
+ response_dates = json_response.map{ |merge_request| merge_request['updated_at'] }
+ expect(response_dates).to eq(response_dates.sort.reverse)
+ end
+
+ it "returns an array of merge_requests ordered by created_at" do
+ get v3_api("/projects/#{project.id}/merge_requests?order_by=created_at&sort=asc", user)
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(3)
+ response_dates = json_response.map{ |merge_request| merge_request['created_at'] }
+ expect(response_dates).to eq(response_dates.sort)
+ end
+ end
+ end
+ end
+
+ describe "GET /projects/:id/merge_requests/:merge_request_id" do
+ it 'exposes known attributes' do
+ get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['id']).to eq(merge_request.id)
+ expect(json_response['iid']).to eq(merge_request.iid)
+ expect(json_response['project_id']).to eq(merge_request.project.id)
+ expect(json_response['title']).to eq(merge_request.title)
+ expect(json_response['description']).to eq(merge_request.description)
+ expect(json_response['state']).to eq(merge_request.state)
+ expect(json_response['created_at']).to be_present
+ expect(json_response['updated_at']).to be_present
+ expect(json_response['labels']).to eq(merge_request.label_names)
+ expect(json_response['milestone']).to be_nil
+ expect(json_response['assignee']).to be_a Hash
+ expect(json_response['author']).to be_a Hash
+ expect(json_response['target_branch']).to eq(merge_request.target_branch)
+ expect(json_response['source_branch']).to eq(merge_request.source_branch)
+ expect(json_response['upvotes']).to eq(0)
+ expect(json_response['downvotes']).to eq(0)
+ expect(json_response['source_project_id']).to eq(merge_request.source_project.id)
+ expect(json_response['target_project_id']).to eq(merge_request.target_project.id)
+ expect(json_response['work_in_progress']).to be_falsy
+ expect(json_response['merge_when_build_succeeds']).to be_falsy
+ expect(json_response['merge_status']).to eq('can_be_merged')
+ expect(json_response['should_close_merge_request']).to be_falsy
+ expect(json_response['force_close_merge_request']).to be_falsy
+ end
+
+ it "returns merge_request" do
+ get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user)
+ expect(response).to have_http_status(200)
+ expect(json_response['title']).to eq(merge_request.title)
+ expect(json_response['iid']).to eq(merge_request.iid)
+ expect(json_response['work_in_progress']).to eq(false)
+ expect(json_response['merge_status']).to eq('can_be_merged')
+ expect(json_response['should_close_merge_request']).to be_falsy
+ expect(json_response['force_close_merge_request']).to be_falsy
+ end
+
+ it 'returns merge_request by iid' do
+ url = "/projects/#{project.id}/merge_requests?iid=#{merge_request.iid}"
+ get v3_api(url, user)
+ expect(response.status).to eq 200
+ expect(json_response.first['title']).to eq merge_request.title
+ expect(json_response.first['id']).to eq merge_request.id
+ end
+
+ it 'returns merge_request by iid array' do
+ get v3_api("/projects/#{project.id}/merge_requests", user), iid: [merge_request.iid, merge_request_closed.iid]
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(2)
+ expect(json_response.first['title']).to eq merge_request_closed.title
+ expect(json_response.first['id']).to eq merge_request_closed.id
+ end
+
+ it "returns a 404 error if merge_request_id not found" do
+ get v3_api("/projects/#{project.id}/merge_requests/999", user)
+ expect(response).to have_http_status(404)
+ end
+
+ context 'Work in Progress' do
+ let!(:merge_request_wip) { create(:merge_request, author: user, assignee: user, source_project: project, target_project: project, title: "WIP: Test", created_at: base_time + 1.second) }
+
+ it "returns merge_request" do
+ get v3_api("/projects/#{project.id}/merge_requests/#{merge_request_wip.id}", user)
+ expect(response).to have_http_status(200)
+ expect(json_response['work_in_progress']).to eq(true)
+ end
+ end
+ end
+
+ describe 'GET /projects/:id/merge_requests/:merge_request_id/commits' do
+ it 'returns a 200 when merge request is valid' do
+ get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/commits", user)
+ commit = merge_request.commits.first
+
+ expect(response.status).to eq 200
+ expect(json_response.size).to eq(merge_request.commits.size)
+ expect(json_response.first['id']).to eq(commit.id)
+ expect(json_response.first['title']).to eq(commit.title)
+ end
+
+ it 'returns a 404 when merge_request_id not found' do
+ get v3_api("/projects/#{project.id}/merge_requests/999/commits", user)
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ describe 'GET /projects/:id/merge_requests/:merge_request_id/changes' do
+ it 'returns the change information of the merge_request' do
+ get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/changes", user)
+ expect(response.status).to eq 200
+ expect(json_response['changes'].size).to eq(merge_request.diffs.size)
+ end
+
+ it 'returns a 404 when merge_request_id not found' do
+ get v3_api("/projects/#{project.id}/merge_requests/999/changes", user)
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ describe "POST /projects/:id/merge_requests" do
+ context 'between branches projects' do
+ it "returns merge_request" do
+ post v3_api("/projects/#{project.id}/merge_requests", user),
+ title: 'Test merge_request',
+ source_branch: 'feature_conflict',
+ target_branch: 'master',
+ author: user,
+ labels: 'label, label2',
+ milestone_id: milestone.id,
+ remove_source_branch: true
+
+ expect(response).to have_http_status(201)
+ expect(json_response['title']).to eq('Test merge_request')
+ expect(json_response['labels']).to eq(['label', 'label2'])
+ expect(json_response['milestone']['id']).to eq(milestone.id)
+ expect(json_response['force_remove_source_branch']).to be_truthy
+ end
+
+ it "returns 422 when source_branch equals target_branch" do
+ post v3_api("/projects/#{project.id}/merge_requests", user),
+ title: "Test merge_request", source_branch: "master", target_branch: "master", author: user
+ expect(response).to have_http_status(422)
+ end
+
+ it "returns 400 when source_branch is missing" do
+ post v3_api("/projects/#{project.id}/merge_requests", user),
+ title: "Test merge_request", target_branch: "master", author: user
+ expect(response).to have_http_status(400)
+ end
+
+ it "returns 400 when target_branch is missing" do
+ post v3_api("/projects/#{project.id}/merge_requests", user),
+ title: "Test merge_request", source_branch: "markdown", author: user
+ expect(response).to have_http_status(400)
+ end
+
+ it "returns 400 when title is missing" do
+ post v3_api("/projects/#{project.id}/merge_requests", user),
+ target_branch: 'master', source_branch: 'markdown'
+ expect(response).to have_http_status(400)
+ end
+
+ it 'allows special label names' do
+ post v3_api("/projects/#{project.id}/merge_requests", user),
+ title: 'Test merge_request',
+ source_branch: 'markdown',
+ target_branch: 'master',
+ author: user,
+ labels: 'label, label?, label&foo, ?, &'
+ expect(response.status).to eq(201)
+ expect(json_response['labels']).to include 'label'
+ expect(json_response['labels']).to include 'label?'
+ expect(json_response['labels']).to include 'label&foo'
+ expect(json_response['labels']).to include '?'
+ expect(json_response['labels']).to include '&'
+ end
+
+ context 'with existing MR' do
+ before do
+ post v3_api("/projects/#{project.id}/merge_requests", user),
+ title: 'Test merge_request',
+ source_branch: 'feature_conflict',
+ target_branch: 'master',
+ author: user
+ @mr = MergeRequest.all.last
+ end
+
+ it 'returns 409 when MR already exists for source/target' do
+ expect do
+ post v3_api("/projects/#{project.id}/merge_requests", user),
+ title: 'New test merge_request',
+ source_branch: 'feature_conflict',
+ target_branch: 'master',
+ author: user
+ end.to change { MergeRequest.count }.by(0)
+ expect(response).to have_http_status(409)
+ end
+ end
+ end
+
+ context 'forked projects' do
+ let!(:user2) { create(:user) }
+ let!(:fork_project) { create(:empty_project, forked_from_project: project, namespace: user2.namespace, creator_id: user2.id) }
+ let!(:unrelated_project) { create(:empty_project, namespace: create(:user).namespace, creator_id: user2.id) }
+
+ before :each do |each|
+ fork_project.team << [user2, :reporter]
+ end
+
+ it "returns merge_request" do
+ post v3_api("/projects/#{fork_project.id}/merge_requests", user2),
+ title: 'Test merge_request', source_branch: "feature_conflict", target_branch: "master",
+ author: user2, target_project_id: project.id, description: 'Test description for Test merge_request'
+ expect(response).to have_http_status(201)
+ expect(json_response['title']).to eq('Test merge_request')
+ expect(json_response['description']).to eq('Test description for Test merge_request')
+ end
+
+ it "does not return 422 when source_branch equals target_branch" do
+ expect(project.id).not_to eq(fork_project.id)
+ expect(fork_project.forked?).to be_truthy
+ expect(fork_project.forked_from_project).to eq(project)
+ post v3_api("/projects/#{fork_project.id}/merge_requests", user2),
+ title: 'Test merge_request', source_branch: "master", target_branch: "master", author: user2, target_project_id: project.id
+ expect(response).to have_http_status(201)
+ expect(json_response['title']).to eq('Test merge_request')
+ end
+
+ it "returns 400 when source_branch is missing" do
+ post v3_api("/projects/#{fork_project.id}/merge_requests", user2),
+ title: 'Test merge_request', target_branch: "master", author: user2, target_project_id: project.id
+ expect(response).to have_http_status(400)
+ end
+
+ it "returns 400 when target_branch is missing" do
+ post v3_api("/projects/#{fork_project.id}/merge_requests", user2),
+ title: 'Test merge_request', target_branch: "master", author: user2, target_project_id: project.id
+ expect(response).to have_http_status(400)
+ end
+
+ it "returns 400 when title is missing" do
+ post v3_api("/projects/#{fork_project.id}/merge_requests", user2),
+ target_branch: 'master', source_branch: 'markdown', author: user2, target_project_id: project.id
+ expect(response).to have_http_status(400)
+ end
+
+ context 'when target_branch is specified' do
+ it 'returns 422 if not a forked project' do
+ post v3_api("/projects/#{project.id}/merge_requests", user),
+ title: 'Test merge_request',
+ target_branch: 'master',
+ source_branch: 'markdown',
+ author: user,
+ target_project_id: fork_project.id
+ expect(response).to have_http_status(422)
+ end
+
+ it 'returns 422 if targeting a different fork' do
+ post v3_api("/projects/#{fork_project.id}/merge_requests", user2),
+ title: 'Test merge_request',
+ target_branch: 'master',
+ source_branch: 'markdown',
+ author: user2,
+ target_project_id: unrelated_project.id
+ expect(response).to have_http_status(422)
+ end
+ end
+
+ it "returns 201 when target_branch is specified and for the same project" do
+ post v3_api("/projects/#{fork_project.id}/merge_requests", user2),
+ title: 'Test merge_request', target_branch: 'master', source_branch: 'markdown', author: user2, target_project_id: fork_project.id
+ expect(response).to have_http_status(201)
+ end
+ end
+ end
+
+ describe "DELETE /projects/:id/merge_requests/:merge_request_id" do
+ context "when the user is developer" do
+ let(:developer) { create(:user) }
+
+ before do
+ project.team << [developer, :developer]
+ end
+
+ it "denies the deletion of the merge request" do
+ delete v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", developer)
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ context "when the user is project owner" do
+ it "destroys the merge request owners can destroy" do
+ delete v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user)
+
+ expect(response).to have_http_status(200)
+ end
+ end
+ end
+
+ describe "PUT /projects/:id/merge_requests/:merge_request_id/merge" do
+ let(:pipeline) { create(:ci_pipeline_without_jobs) }
+
+ it "returns merge_request in case of success" do
+ put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user)
+
+ expect(response).to have_http_status(200)
+ end
+
+ it "returns 406 if branch can't be merged" do
+ allow_any_instance_of(MergeRequest).
+ to receive(:can_be_merged?).and_return(false)
+
+ put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user)
+
+ expect(response).to have_http_status(406)
+ expect(json_response['message']).to eq('Branch cannot be merged')
+ end
+
+ it "returns 405 if merge_request is not open" do
+ merge_request.close
+ put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user)
+ expect(response).to have_http_status(405)
+ expect(json_response['message']).to eq('405 Method Not Allowed')
+ end
+
+ it "returns 405 if merge_request is a work in progress" do
+ merge_request.update_attribute(:title, "WIP: #{merge_request.title}")
+ put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user)
+ expect(response).to have_http_status(405)
+ expect(json_response['message']).to eq('405 Method Not Allowed')
+ end
+
+ it 'returns 405 if the build failed for a merge request that requires success' do
+ allow_any_instance_of(MergeRequest).to receive(:mergeable_ci_state?).and_return(false)
+
+ put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user)
+
+ expect(response).to have_http_status(405)
+ expect(json_response['message']).to eq('405 Method Not Allowed')
+ end
+
+ it "returns 401 if user has no permissions to merge" do
+ user2 = create(:user)
+ project.team << [user2, :reporter]
+ put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user2)
+ expect(response).to have_http_status(401)
+ expect(json_response['message']).to eq('401 Unauthorized')
+ end
+
+ it "returns 409 if the SHA parameter doesn't match" do
+ put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user), sha: merge_request.diff_head_sha.reverse
+
+ expect(response).to have_http_status(409)
+ expect(json_response['message']).to start_with('SHA does not match HEAD of source branch')
+ end
+
+ it "succeeds if the SHA parameter matches" do
+ put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user), sha: merge_request.diff_head_sha
+
+ expect(response).to have_http_status(200)
+ end
+
+ it "enables merge when pipeline succeeds if the pipeline is active" do
+ allow_any_instance_of(MergeRequest).to receive(:head_pipeline).and_return(pipeline)
+ allow(pipeline).to receive(:active?).and_return(true)
+
+ put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user), merge_when_build_succeeds: true
+
+ expect(response).to have_http_status(200)
+ expect(json_response['title']).to eq('Test')
+ expect(json_response['merge_when_build_succeeds']).to eq(true)
+ end
+ end
+
+ describe "PUT /projects/:id/merge_requests/:merge_request_id" do
+ context "to close a MR" do
+ it "returns merge_request" do
+ put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), state_event: "close"
+
+ expect(response).to have_http_status(200)
+ expect(json_response['state']).to eq('closed')
+ end
+ end
+
+ it "updates title and returns merge_request" do
+ put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), title: "New title"
+ expect(response).to have_http_status(200)
+ expect(json_response['title']).to eq('New title')
+ end
+
+ it "updates description and returns merge_request" do
+ put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), description: "New description"
+ expect(response).to have_http_status(200)
+ expect(json_response['description']).to eq('New description')
+ end
+
+ it "updates milestone_id and returns merge_request" do
+ put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), milestone_id: milestone.id
+ expect(response).to have_http_status(200)
+ expect(json_response['milestone']['id']).to eq(milestone.id)
+ end
+
+ it "returns merge_request with renamed target_branch" do
+ put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), target_branch: "wiki"
+ expect(response).to have_http_status(200)
+ expect(json_response['target_branch']).to eq('wiki')
+ end
+
+ it "returns merge_request that removes the source branch" do
+ put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), remove_source_branch: true
+
+ expect(response).to have_http_status(200)
+ expect(json_response['force_remove_source_branch']).to be_truthy
+ end
+
+ it 'allows special label names' do
+ put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user),
+ title: 'new issue',
+ labels: 'label, label?, label&foo, ?, &'
+
+ expect(response.status).to eq(200)
+ expect(json_response['labels']).to include 'label'
+ expect(json_response['labels']).to include 'label?'
+ expect(json_response['labels']).to include 'label&foo'
+ expect(json_response['labels']).to include '?'
+ expect(json_response['labels']).to include '&'
+ end
+
+ it 'does not update state when title is empty' do
+ put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), state_event: 'close', title: nil
+
+ merge_request.reload
+ expect(response).to have_http_status(400)
+ expect(merge_request.state).to eq('opened')
+ end
+
+ it 'does not update state when target_branch is empty' do
+ put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), state_event: 'close', target_branch: nil
+
+ merge_request.reload
+ expect(response).to have_http_status(400)
+ expect(merge_request.state).to eq('opened')
+ end
+ end
+
+ describe "POST /projects/:id/merge_requests/:merge_request_id/comments" do
+ it "returns comment" do
+ original_count = merge_request.notes.size
+
+ post v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/comments", user), note: "My comment"
+
+ expect(response).to have_http_status(201)
+ expect(json_response['note']).to eq('My comment')
+ expect(json_response['author']['name']).to eq(user.name)
+ expect(json_response['author']['username']).to eq(user.username)
+ expect(merge_request.reload.notes.size).to eq(original_count + 1)
+ end
+
+ it "returns 400 if note is missing" do
+ post v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/comments", user)
+ expect(response).to have_http_status(400)
+ end
+
+ it "returns 404 if note is attached to non existent merge request" do
+ post v3_api("/projects/#{project.id}/merge_requests/404/comments", user),
+ note: 'My comment'
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ describe "GET :id/merge_requests/:merge_request_id/comments" do
+ let!(:note) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "a comment on a MR") }
+ let!(:note2) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "another comment on a MR") }
+
+ it "returns merge_request comments ordered by created_at" do
+ get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/comments", user)
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(2)
+ expect(json_response.first['note']).to eq("a comment on a MR")
+ expect(json_response.first['author']['id']).to eq(user.id)
+ expect(json_response.last['note']).to eq("another comment on a MR")
+ end
+
+ it "returns a 404 error if merge_request_id not found" do
+ get v3_api("/projects/#{project.id}/merge_requests/999/comments", user)
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ describe 'GET :id/merge_requests/:merge_request_id/closes_issues' do
+ it 'returns the issue that will be closed on merge' do
+ issue = create(:issue, project: project)
+ mr = merge_request.tap do |mr|
+ mr.update_attribute(:description, "Closes #{issue.to_reference(mr.project)}")
+ end
+
+ get v3_api("/projects/#{project.id}/merge_requests/#{mr.id}/closes_issues", user)
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['id']).to eq(issue.id)
+ end
+
+ it 'returns an empty array when there are no issues to be closed' do
+ get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/closes_issues", user)
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(0)
+ end
+
+ it 'handles external issues' do
+ jira_project = create(:jira_project, :public, name: 'JIR_EXT1')
+ issue = ExternalIssue.new("#{jira_project.name}-123", jira_project)
+ merge_request = create(:merge_request, :simple, author: user, assignee: user, source_project: jira_project)
+ merge_request.update_attribute(:description, "Closes #{issue.to_reference(jira_project)}")
+
+ get v3_api("/projects/#{jira_project.id}/merge_requests/#{merge_request.id}/closes_issues", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['title']).to eq(issue.title)
+ expect(json_response.first['id']).to eq(issue.id)
+ end
+
+ it 'returns 403 if the user has no access to the merge request' do
+ project = create(:empty_project, :private)
+ merge_request = create(:merge_request, :simple, source_project: project)
+ guest = create(:user)
+ project.team << [guest, :guest]
+
+ get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/closes_issues", guest)
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ describe 'POST :id/merge_requests/:merge_request_id/subscription' do
+ it 'subscribes to a merge request' do
+ post v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", admin)
+
+ expect(response).to have_http_status(201)
+ expect(json_response['subscribed']).to eq(true)
+ end
+
+ it 'returns 304 if already subscribed' do
+ post v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", user)
+
+ expect(response).to have_http_status(304)
+ end
+
+ it 'returns 404 if the merge request is not found' do
+ post v3_api("/projects/#{project.id}/merge_requests/123/subscription", user)
+
+ expect(response).to have_http_status(404)
+ end
+
+ it 'returns 403 if user has no access to read code' do
+ guest = create(:user)
+ project.team << [guest, :guest]
+
+ post v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", guest)
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ describe 'DELETE :id/merge_requests/:merge_request_id/subscription' do
+ it 'unsubscribes from a merge request' do
+ delete v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['subscribed']).to eq(false)
+ end
+
+ it 'returns 304 if not subscribed' do
+ delete v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", admin)
+
+ expect(response).to have_http_status(304)
+ end
+
+ it 'returns 404 if the merge request is not found' do
+ post v3_api("/projects/#{project.id}/merge_requests/123/subscription", user)
+
+ expect(response).to have_http_status(404)
+ end
+
+ it 'returns 403 if user has no access to read code' do
+ guest = create(:user)
+ project.team << [guest, :guest]
+
+ delete v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", guest)
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ describe 'Time tracking' do
+ let(:issuable) { merge_request }
+
+ include_examples 'time tracking endpoints', 'merge_request'
+ end
+
+ def mr_with_later_created_and_updated_at_time
+ merge_request
+ merge_request.created_at += 1.hour
+ merge_request.updated_at += 30.minutes
+ merge_request.save
+ merge_request
+ end
+
+ def mr_with_earlier_created_and_updated_at_time
+ merge_request_closed
+ merge_request_closed.created_at -= 1.hour
+ merge_request_closed.updated_at -= 30.minutes
+ merge_request_closed.save
+ merge_request_closed
+ end
+end
diff --git a/spec/requests/api/v3/project_snippets_spec.rb b/spec/requests/api/v3/project_snippets_spec.rb
new file mode 100644
index 00000000000..3700477f0db
--- /dev/null
+++ b/spec/requests/api/v3/project_snippets_spec.rb
@@ -0,0 +1,188 @@
+require 'rails_helper'
+
+describe API::ProjectSnippets, api: true do
+ include ApiHelpers
+
+ let(:project) { create(:empty_project, :public) }
+ let(:user) { create(:user) }
+ let(:admin) { create(:admin) }
+
+ describe 'GET /projects/:project_id/snippets/:id' do
+ # TODO (rspeicher): Deprecated; remove in 9.0
+ it 'always exposes expires_at as nil' do
+ snippet = create(:project_snippet, author: admin)
+
+ get v3_api("/projects/#{snippet.project.id}/snippets/#{snippet.id}", admin)
+
+ expect(json_response).to have_key('expires_at')
+ expect(json_response['expires_at']).to be_nil
+ end
+ end
+
+ describe 'GET /projects/:project_id/snippets/' do
+ let(:user) { create(:user) }
+
+ it 'returns all snippets available to team member' do
+ project.add_developer(user)
+ public_snippet = create(:project_snippet, :public, project: project)
+ internal_snippet = create(:project_snippet, :internal, project: project)
+ private_snippet = create(:project_snippet, :private, project: project)
+
+ get v3_api("/projects/#{project.id}/snippets/", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response.size).to eq(3)
+ expect(json_response.map{ |snippet| snippet['id']} ).to include(public_snippet.id, internal_snippet.id, private_snippet.id)
+ expect(json_response.last).to have_key('web_url')
+ end
+
+ it 'hides private snippets from regular user' do
+ create(:project_snippet, :private, project: project)
+
+ get v3_api("/projects/#{project.id}/snippets/", user)
+ expect(response).to have_http_status(200)
+ expect(json_response.size).to eq(0)
+ end
+ end
+
+ describe 'POST /projects/:project_id/snippets/' do
+ let(:params) do
+ {
+ title: 'Test Title',
+ file_name: 'test.rb',
+ code: 'puts "hello world"',
+ visibility_level: Snippet::PUBLIC
+ }
+ end
+
+ it 'creates a new snippet' do
+ post v3_api("/projects/#{project.id}/snippets/", admin), params
+
+ expect(response).to have_http_status(201)
+ snippet = ProjectSnippet.find(json_response['id'])
+ expect(snippet.content).to eq(params[:code])
+ expect(snippet.title).to eq(params[:title])
+ expect(snippet.file_name).to eq(params[:file_name])
+ expect(snippet.visibility_level).to eq(params[:visibility_level])
+ end
+
+ it 'returns 400 for missing parameters' do
+ params.delete(:title)
+
+ post v3_api("/projects/#{project.id}/snippets/", admin), params
+
+ expect(response).to have_http_status(400)
+ end
+
+ context 'when the snippet is spam' do
+ def create_snippet(project, snippet_params = {})
+ project.add_developer(user)
+
+ post v3_api("/projects/#{project.id}/snippets", user), params.merge(snippet_params)
+ end
+
+ before do
+ allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true)
+ end
+
+ context 'when the project is private' do
+ let(:private_project) { create(:project_empty_repo, :private) }
+
+ context 'when the snippet is public' do
+ it 'creates the snippet' do
+ expect { create_snippet(private_project, visibility_level: Snippet::PUBLIC) }.
+ to change { Snippet.count }.by(1)
+ end
+ end
+ end
+
+ context 'when the project is public' do
+ context 'when the snippet is private' do
+ it 'creates the snippet' do
+ expect { create_snippet(project, visibility_level: Snippet::PRIVATE) }.
+ to change { Snippet.count }.by(1)
+ end
+ end
+
+ context 'when the snippet is public' do
+ it 'rejects the shippet' do
+ expect { create_snippet(project, visibility_level: Snippet::PUBLIC) }.
+ not_to change { Snippet.count }
+ expect(response).to have_http_status(400)
+ end
+
+ it 'creates a spam log' do
+ expect { create_snippet(project, visibility_level: Snippet::PUBLIC) }.
+ to change { SpamLog.count }.by(1)
+ end
+ end
+ end
+ end
+ end
+
+ describe 'PUT /projects/:project_id/snippets/:id/' do
+ let(:snippet) { create(:project_snippet, author: admin) }
+
+ it 'updates snippet' do
+ new_content = 'New content'
+
+ put v3_api("/projects/#{snippet.project.id}/snippets/#{snippet.id}/", admin), code: new_content
+
+ expect(response).to have_http_status(200)
+ snippet.reload
+ expect(snippet.content).to eq(new_content)
+ end
+
+ it 'returns 404 for invalid snippet id' do
+ put v3_api("/projects/#{snippet.project.id}/snippets/1234", admin), title: 'foo'
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 Snippet Not Found')
+ end
+
+ it 'returns 400 for missing parameters' do
+ put v3_api("/projects/#{project.id}/snippets/1234", admin)
+
+ expect(response).to have_http_status(400)
+ end
+ end
+
+ describe 'DELETE /projects/:project_id/snippets/:id/' do
+ let(:snippet) { create(:project_snippet, author: admin) }
+
+ it 'deletes snippet' do
+ admin = create(:admin)
+ snippet = create(:project_snippet, author: admin)
+
+ delete v3_api("/projects/#{snippet.project.id}/snippets/#{snippet.id}/", admin)
+
+ expect(response).to have_http_status(200)
+ end
+
+ it 'returns 404 for invalid snippet id' do
+ delete v3_api("/projects/#{snippet.project.id}/snippets/1234", admin)
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 Snippet Not Found')
+ end
+ end
+
+ describe 'GET /projects/:project_id/snippets/:id/raw' do
+ let(:snippet) { create(:project_snippet, author: admin) }
+
+ it 'returns raw text' do
+ get v3_api("/projects/#{snippet.project.id}/snippets/#{snippet.id}/raw", admin)
+
+ expect(response).to have_http_status(200)
+ expect(response.content_type).to eq 'text/plain'
+ expect(response.body).to eq(snippet.content)
+ end
+
+ it 'returns 404 for invalid snippet id' do
+ delete v3_api("/projects/#{snippet.project.id}/snippets/1234", admin)
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 Snippet Not Found')
+ end
+ end
+end
diff --git a/spec/requests/api/v3/projects_spec.rb b/spec/requests/api/v3/projects_spec.rb
new file mode 100644
index 00000000000..a495122bba7
--- /dev/null
+++ b/spec/requests/api/v3/projects_spec.rb
@@ -0,0 +1,1424 @@
+require 'spec_helper'
+
+describe API::V3::Projects, api: true do
+ include ApiHelpers
+ include Gitlab::CurrentSettings
+
+ let(:user) { create(:user) }
+ let(:user2) { create(:user) }
+ let(:user3) { create(:user) }
+ let(:admin) { create(:admin) }
+ let(:project) { create(:empty_project, creator_id: user.id, namespace: user.namespace) }
+ let(:project2) { create(:empty_project, path: 'project2', creator_id: user.id, namespace: user.namespace) }
+ let(:snippet) { create(:project_snippet, :public, author: user, project: project, title: 'example') }
+ let(:project_member) { create(:project_member, :master, user: user, project: project) }
+ let(:project_member2) { create(:project_member, :developer, user: user3, project: project) }
+ let(:user4) { create(:user) }
+ let(:project3) do
+ create(:project,
+ :private,
+ :repository,
+ name: 'second_project',
+ path: 'second_project',
+ creator_id: user.id,
+ namespace: user.namespace,
+ merge_requests_enabled: false,
+ issues_enabled: false, wiki_enabled: false,
+ snippets_enabled: false)
+ end
+ let(:project_member3) do
+ create(:project_member,
+ user: user4,
+ project: project3,
+ access_level: ProjectMember::MASTER)
+ end
+ let(:project4) do
+ create(:empty_project,
+ name: 'third_project',
+ path: 'third_project',
+ creator_id: user4.id,
+ namespace: user4.namespace)
+ end
+
+ describe 'GET /projects' do
+ before { project }
+
+ context 'when unauthenticated' do
+ it 'returns authentication error' do
+ get v3_api('/projects')
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ context 'when authenticated as regular user' do
+ it 'returns an array of projects' do
+ get v3_api('/projects', user)
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first['name']).to eq(project.name)
+ expect(json_response.first['owner']['username']).to eq(user.username)
+ end
+
+ it 'includes the project labels as the tag_list' do
+ get v3_api('/projects', user)
+ expect(response.status).to eq 200
+ expect(json_response).to be_an Array
+ expect(json_response.first.keys).to include('tag_list')
+ end
+
+ it 'includes open_issues_count' do
+ get v3_api('/projects', user)
+ expect(response.status).to eq 200
+ expect(json_response).to be_an Array
+ expect(json_response.first.keys).to include('open_issues_count')
+ end
+
+ it 'does not include open_issues_count' do
+ project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED)
+
+ get v3_api('/projects', user)
+ expect(response.status).to eq 200
+ expect(json_response).to be_an Array
+ expect(json_response.first.keys).not_to include('open_issues_count')
+ end
+
+ context 'GET /projects?simple=true' do
+ it 'returns a simplified version of all the projects' do
+ expected_keys = ["id", "http_url_to_repo", "web_url", "name", "name_with_namespace", "path", "path_with_namespace"]
+
+ get v3_api('/projects?simple=true', user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first.keys).to match_array expected_keys
+ end
+ end
+
+ context 'and using search' do
+ it 'returns searched project' do
+ get v3_api('/projects', user), { search: project.name }
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ end
+ end
+
+ context 'and using the visibility filter' do
+ it 'filters based on private visibility param' do
+ get v3_api('/projects', user), { visibility: 'private' }
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(user.namespace.projects.where(visibility_level: Gitlab::VisibilityLevel::PRIVATE).count)
+ end
+
+ it 'filters based on internal visibility param' do
+ get v3_api('/projects', user), { visibility: 'internal' }
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(user.namespace.projects.where(visibility_level: Gitlab::VisibilityLevel::INTERNAL).count)
+ end
+
+ it 'filters based on public visibility param' do
+ get v3_api('/projects', user), { visibility: 'public' }
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(user.namespace.projects.where(visibility_level: Gitlab::VisibilityLevel::PUBLIC).count)
+ end
+ end
+
+ context 'and using sorting' do
+ before do
+ project2
+ project3
+ end
+
+ it 'returns the correct order when sorted by id' do
+ get v3_api('/projects', user), { order_by: 'id', sort: 'desc' }
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first['id']).to eq(project3.id)
+ end
+ end
+ end
+ end
+
+ describe 'GET /projects/all' do
+ before { project }
+
+ context 'when unauthenticated' do
+ it 'returns authentication error' do
+ get v3_api('/projects/all')
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ context 'when authenticated as regular user' do
+ it 'returns authentication error' do
+ get v3_api('/projects/all', user)
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ context 'when authenticated as admin' do
+ it 'returns an array of all projects' do
+ get v3_api('/projects/all', admin)
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+
+ expect(json_response).to satisfy do |response|
+ response.one? do |entry|
+ entry.has_key?('permissions') &&
+ entry['name'] == project.name &&
+ entry['owner']['username'] == user.username
+ end
+ end
+ end
+
+ it "does not include statistics by default" do
+ get v3_api('/projects/all', admin)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first).not_to include('statistics')
+ end
+
+ it "includes statistics if requested" do
+ get v3_api('/projects/all', admin), statistics: true
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first).to include 'statistics'
+ end
+ end
+ end
+
+ describe 'GET /projects/owned' do
+ before do
+ project3
+ project4
+ end
+
+ context 'when unauthenticated' do
+ it 'returns authentication error' do
+ get v3_api('/projects/owned')
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ context 'when authenticated as project owner' do
+ it 'returns an array of projects the user owns' do
+ get v3_api('/projects/owned', user4)
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first['name']).to eq(project4.name)
+ expect(json_response.first['owner']['username']).to eq(user4.username)
+ end
+
+ it "does not include statistics by default" do
+ get v3_api('/projects/owned', user4)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first).not_to include('statistics')
+ end
+
+ it "includes statistics if requested" do
+ attributes = {
+ commit_count: 23,
+ storage_size: 702,
+ repository_size: 123,
+ lfs_objects_size: 234,
+ build_artifacts_size: 345,
+ }
+
+ project4.statistics.update!(attributes)
+
+ get v3_api('/projects/owned', user4), statistics: true
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first['statistics']).to eq attributes.stringify_keys
+ end
+ end
+ end
+
+ describe 'GET /projects/visible' do
+ shared_examples_for 'visible projects response' do
+ it 'returns the visible projects' do
+ get v3_api('/projects/visible', current_user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.map { |p| p['id'] }).to contain_exactly(*projects.map(&:id))
+ end
+ end
+
+ let!(:public_project) { create(:empty_project, :public) }
+ before do
+ project
+ project2
+ project3
+ project4
+ end
+
+ context 'when unauthenticated' do
+ it_behaves_like 'visible projects response' do
+ let(:current_user) { nil }
+ let(:projects) { [public_project] }
+ end
+ end
+
+ context 'when authenticated' do
+ it_behaves_like 'visible projects response' do
+ let(:current_user) { user }
+ let(:projects) { [public_project, project, project2, project3] }
+ end
+ end
+
+ context 'when authenticated as a different user' do
+ it_behaves_like 'visible projects response' do
+ let(:current_user) { user2 }
+ let(:projects) { [public_project] }
+ end
+ end
+ end
+
+ describe 'GET /projects/starred' do
+ let(:public_project) { create(:empty_project, :public) }
+
+ before do
+ project_member2
+ user3.update_attributes(starred_projects: [project, project2, project3, public_project])
+ end
+
+ it 'returns the starred projects viewable by the user' do
+ get v3_api('/projects/starred', user3)
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.map { |project| project['id'] }).to contain_exactly(project.id, public_project.id)
+ end
+ end
+
+ describe 'POST /projects' do
+ context 'maximum number of projects reached' do
+ it 'does not create new project and respond with 403' do
+ allow_any_instance_of(User).to receive(:projects_limit_left).and_return(0)
+ expect { post v3_api('/projects', user2), name: 'foo' }.
+ to change {Project.count}.by(0)
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ it 'creates new project without path and return 201' do
+ expect { post v3_api('/projects', user), name: 'foo' }.
+ to change { Project.count }.by(1)
+ expect(response).to have_http_status(201)
+ end
+
+ it 'creates last project before reaching project limit' do
+ allow_any_instance_of(User).to receive(:projects_limit_left).and_return(1)
+ post v3_api('/projects', user2), name: 'foo'
+ expect(response).to have_http_status(201)
+ end
+
+ it 'does not create new project without name and return 400' do
+ expect { post v3_api('/projects', user) }.not_to change { Project.count }
+ expect(response).to have_http_status(400)
+ end
+
+ it "assigns attributes to project" do
+ project = attributes_for(:project, {
+ path: 'camelCasePath',
+ description: FFaker::Lorem.sentence,
+ issues_enabled: false,
+ merge_requests_enabled: false,
+ wiki_enabled: false,
+ only_allow_merge_if_build_succeeds: false,
+ request_access_enabled: true,
+ only_allow_merge_if_all_discussions_are_resolved: false
+ })
+
+ post v3_api('/projects', user), project
+
+ project.each_pair do |k, v|
+ next if %i[has_external_issue_tracker issues_enabled merge_requests_enabled wiki_enabled].include?(k)
+ expect(json_response[k.to_s]).to eq(v)
+ end
+
+ # Check feature permissions attributes
+ project = Project.find_by_path(project[:path])
+ expect(project.project_feature.issues_access_level).to eq(ProjectFeature::DISABLED)
+ expect(project.project_feature.merge_requests_access_level).to eq(ProjectFeature::DISABLED)
+ expect(project.project_feature.wiki_access_level).to eq(ProjectFeature::DISABLED)
+ end
+
+ it 'sets a project as public' do
+ project = attributes_for(:project, :public)
+ post v3_api('/projects', user), project
+ expect(json_response['public']).to be_truthy
+ expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PUBLIC)
+ end
+
+ it 'sets a project as public using :public' do
+ project = attributes_for(:project, { public: true })
+ post v3_api('/projects', user), project
+ expect(json_response['public']).to be_truthy
+ expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PUBLIC)
+ end
+
+ it 'sets a project as internal' do
+ project = attributes_for(:project, :internal)
+ post v3_api('/projects', user), project
+ expect(json_response['public']).to be_falsey
+ expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::INTERNAL)
+ end
+
+ it 'sets a project as internal overriding :public' do
+ project = attributes_for(:project, :internal, { public: true })
+ post v3_api('/projects', user), project
+ expect(json_response['public']).to be_falsey
+ expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::INTERNAL)
+ end
+
+ it 'sets a project as private' do
+ project = attributes_for(:project, :private)
+ post v3_api('/projects', user), project
+ expect(json_response['public']).to be_falsey
+ expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE)
+ end
+
+ it 'sets a project as private using :public' do
+ project = attributes_for(:project, { public: false })
+ post v3_api('/projects', user), project
+ expect(json_response['public']).to be_falsey
+ expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE)
+ end
+
+ it 'sets a project as allowing merge even if build fails' do
+ project = attributes_for(:project, { only_allow_merge_if_build_succeeds: false })
+ post v3_api('/projects', user), project
+ expect(json_response['only_allow_merge_if_build_succeeds']).to be_falsey
+ end
+
+ it 'sets a project as allowing merge only if build succeeds' do
+ project = attributes_for(:project, { only_allow_merge_if_build_succeeds: true })
+ post v3_api('/projects', user), project
+ expect(json_response['only_allow_merge_if_build_succeeds']).to be_truthy
+ end
+
+ it 'sets a project as allowing merge even if discussions are unresolved' do
+ project = attributes_for(:project, { only_allow_merge_if_all_discussions_are_resolved: false })
+
+ post v3_api('/projects', user), project
+
+ expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to be_falsey
+ end
+
+ it 'sets a project as allowing merge if only_allow_merge_if_all_discussions_are_resolved is nil' do
+ project = attributes_for(:project, only_allow_merge_if_all_discussions_are_resolved: nil)
+
+ post v3_api('/projects', user), project
+
+ expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to be_falsey
+ end
+
+ it 'sets a project as allowing merge only if all discussions are resolved' do
+ project = attributes_for(:project, { only_allow_merge_if_all_discussions_are_resolved: true })
+
+ post v3_api('/projects', user), project
+
+ expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to be_truthy
+ end
+
+ context 'when a visibility level is restricted' do
+ before do
+ @project = attributes_for(:project, { public: true })
+ stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC])
+ end
+
+ it 'does not allow a non-admin to use a restricted visibility level' do
+ post v3_api('/projects', user), @project
+
+ expect(response).to have_http_status(400)
+ expect(json_response['message']['visibility_level'].first).to(
+ match('restricted by your GitLab administrator')
+ )
+ end
+
+ it 'allows an admin to override restricted visibility settings' do
+ post v3_api('/projects', admin), @project
+ expect(json_response['public']).to be_truthy
+ expect(json_response['visibility_level']).to(
+ eq(Gitlab::VisibilityLevel::PUBLIC)
+ )
+ end
+ end
+ end
+
+ describe 'POST /projects/user/:id' do
+ before { project }
+ before { admin }
+
+ it 'should create new project without path and return 201' do
+ expect { post v3_api("/projects/user/#{user.id}", admin), name: 'foo' }.to change {Project.count}.by(1)
+ expect(response).to have_http_status(201)
+ end
+
+ it 'responds with 400 on failure and not project' do
+ expect { post v3_api("/projects/user/#{user.id}", admin) }.
+ not_to change { Project.count }
+
+ expect(response).to have_http_status(400)
+ expect(json_response['error']).to eq('name is missing')
+ end
+
+ it 'assigns attributes to project' do
+ project = attributes_for(:project, {
+ description: FFaker::Lorem.sentence,
+ issues_enabled: false,
+ merge_requests_enabled: false,
+ wiki_enabled: false,
+ request_access_enabled: true
+ })
+
+ post v3_api("/projects/user/#{user.id}", admin), project
+
+ expect(response).to have_http_status(201)
+ project.each_pair do |k, v|
+ next if %i[has_external_issue_tracker path].include?(k)
+ expect(json_response[k.to_s]).to eq(v)
+ end
+ end
+
+ it 'sets a project as public' do
+ project = attributes_for(:project, :public)
+ post v3_api("/projects/user/#{user.id}", admin), project
+
+ expect(response).to have_http_status(201)
+ expect(json_response['public']).to be_truthy
+ expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PUBLIC)
+ end
+
+ it 'sets a project as public using :public' do
+ project = attributes_for(:project, { public: true })
+ post v3_api("/projects/user/#{user.id}", admin), project
+
+ expect(response).to have_http_status(201)
+ expect(json_response['public']).to be_truthy
+ expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PUBLIC)
+ end
+
+ it 'sets a project as internal' do
+ project = attributes_for(:project, :internal)
+ post v3_api("/projects/user/#{user.id}", admin), project
+
+ expect(response).to have_http_status(201)
+ expect(json_response['public']).to be_falsey
+ expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::INTERNAL)
+ end
+
+ it 'sets a project as internal overriding :public' do
+ project = attributes_for(:project, :internal, { public: true })
+ post v3_api("/projects/user/#{user.id}", admin), project
+ expect(response).to have_http_status(201)
+ expect(json_response['public']).to be_falsey
+ expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::INTERNAL)
+ end
+
+ it 'sets a project as private' do
+ project = attributes_for(:project, :private)
+ post v3_api("/projects/user/#{user.id}", admin), project
+ expect(json_response['public']).to be_falsey
+ expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE)
+ end
+
+ it 'sets a project as private using :public' do
+ project = attributes_for(:project, { public: false })
+ post v3_api("/projects/user/#{user.id}", admin), project
+ expect(json_response['public']).to be_falsey
+ expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE)
+ end
+
+ it 'sets a project as allowing merge even if build fails' do
+ project = attributes_for(:project, { only_allow_merge_if_build_succeeds: false })
+ post v3_api("/projects/user/#{user.id}", admin), project
+ expect(json_response['only_allow_merge_if_build_succeeds']).to be_falsey
+ end
+
+ it 'sets a project as allowing merge only if build succeeds' do
+ project = attributes_for(:project, { only_allow_merge_if_build_succeeds: true })
+ post v3_api("/projects/user/#{user.id}", admin), project
+ expect(json_response['only_allow_merge_if_build_succeeds']).to be_truthy
+ end
+
+ it 'sets a project as allowing merge even if discussions are unresolved' do
+ project = attributes_for(:project, { only_allow_merge_if_all_discussions_are_resolved: false })
+
+ post v3_api("/projects/user/#{user.id}", admin), project
+
+ expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to be_falsey
+ end
+
+ it 'sets a project as allowing merge only if all discussions are resolved' do
+ project = attributes_for(:project, { only_allow_merge_if_all_discussions_are_resolved: true })
+
+ post v3_api("/projects/user/#{user.id}", admin), project
+
+ expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to be_truthy
+ end
+ end
+
+ describe "POST /projects/:id/uploads" do
+ before { project }
+
+ it "uploads the file and returns its info" do
+ post v3_api("/projects/#{project.id}/uploads", user), file: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png")
+
+ expect(response).to have_http_status(201)
+ expect(json_response['alt']).to eq("dk")
+ expect(json_response['url']).to start_with("/uploads/")
+ expect(json_response['url']).to end_with("/dk.png")
+ end
+ end
+
+ describe 'GET /projects/:id' do
+ context 'when unauthenticated' do
+ it 'returns the public projects' do
+ public_project = create(:empty_project, :public)
+
+ get v3_api("/projects/#{public_project.id}")
+
+ expect(response).to have_http_status(200)
+ expect(json_response['id']).to eq(public_project.id)
+ expect(json_response['description']).to eq(public_project.description)
+ expect(json_response.keys).not_to include('permissions')
+ end
+ end
+
+ context 'when authenticated' do
+ before do
+ project
+ project_member
+ end
+
+ it 'returns a project by id' do
+ group = create(:group)
+ link = create(:project_group_link, project: project, group: group)
+
+ get v3_api("/projects/#{project.id}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['id']).to eq(project.id)
+ expect(json_response['description']).to eq(project.description)
+ expect(json_response['default_branch']).to eq(project.default_branch)
+ expect(json_response['tag_list']).to be_an Array
+ expect(json_response['public']).to be_falsey
+ expect(json_response['archived']).to be_falsey
+ expect(json_response['visibility_level']).to be_present
+ expect(json_response['ssh_url_to_repo']).to be_present
+ expect(json_response['http_url_to_repo']).to be_present
+ expect(json_response['web_url']).to be_present
+ expect(json_response['owner']).to be_a Hash
+ expect(json_response['owner']).to be_a Hash
+ expect(json_response['name']).to eq(project.name)
+ expect(json_response['path']).to be_present
+ expect(json_response['issues_enabled']).to be_present
+ expect(json_response['merge_requests_enabled']).to be_present
+ expect(json_response['wiki_enabled']).to be_present
+ expect(json_response['builds_enabled']).to be_present
+ expect(json_response['snippets_enabled']).to be_present
+ expect(json_response['container_registry_enabled']).to be_present
+ expect(json_response['created_at']).to be_present
+ expect(json_response['last_activity_at']).to be_present
+ expect(json_response['shared_runners_enabled']).to be_present
+ expect(json_response['creator_id']).to be_present
+ expect(json_response['namespace']).to be_present
+ expect(json_response['avatar_url']).to be_nil
+ expect(json_response['star_count']).to be_present
+ expect(json_response['forks_count']).to be_present
+ expect(json_response['public_builds']).to be_present
+ expect(json_response['shared_with_groups']).to be_an Array
+ expect(json_response['shared_with_groups'].length).to eq(1)
+ expect(json_response['shared_with_groups'][0]['group_id']).to eq(group.id)
+ expect(json_response['shared_with_groups'][0]['group_name']).to eq(group.name)
+ expect(json_response['shared_with_groups'][0]['group_access_level']).to eq(link.group_access)
+ expect(json_response['only_allow_merge_if_build_succeeds']).to eq(project.only_allow_merge_if_build_succeeds)
+ expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to eq(project.only_allow_merge_if_all_discussions_are_resolved)
+ end
+
+ it 'returns a project by path name' do
+ get v3_api("/projects/#{project.id}", user)
+ expect(response).to have_http_status(200)
+ expect(json_response['name']).to eq(project.name)
+ end
+
+ it 'returns a 404 error if not found' do
+ get v3_api('/projects/42', user)
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 Project Not Found')
+ end
+
+ it 'returns a 404 error if user is not a member' do
+ other_user = create(:user)
+ get v3_api("/projects/#{project.id}", other_user)
+ expect(response).to have_http_status(404)
+ end
+
+ it 'handles users with dots' do
+ dot_user = create(:user, username: 'dot.user')
+ project = create(:empty_project, creator_id: dot_user.id, namespace: dot_user.namespace)
+
+ get v3_api("/projects/#{dot_user.namespace.name}%2F#{project.path}", dot_user)
+ expect(response).to have_http_status(200)
+ expect(json_response['name']).to eq(project.name)
+ end
+
+ it 'exposes namespace fields' do
+ get v3_api("/projects/#{project.id}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['namespace']).to eq({
+ 'id' => user.namespace.id,
+ 'name' => user.namespace.name,
+ 'path' => user.namespace.path,
+ 'kind' => user.namespace.kind,
+ })
+ end
+
+ describe 'permissions' do
+ context 'all projects' do
+ before { project.team << [user, :master] }
+
+ it 'contains permission information' do
+ get v3_api("/projects", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response.first['permissions']['project_access']['access_level']).
+ to eq(Gitlab::Access::MASTER)
+ expect(json_response.first['permissions']['group_access']).to be_nil
+ end
+ end
+
+ context 'personal project' do
+ it 'sets project access and returns 200' do
+ project.team << [user, :master]
+ get v3_api("/projects/#{project.id}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['permissions']['project_access']['access_level']).
+ to eq(Gitlab::Access::MASTER)
+ expect(json_response['permissions']['group_access']).to be_nil
+ end
+ end
+
+ context 'group project' do
+ let(:project2) { create(:empty_project, group: create(:group)) }
+
+ before { project2.group.add_owner(user) }
+
+ it 'sets the owner and return 200' do
+ get v3_api("/projects/#{project2.id}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['permissions']['project_access']).to be_nil
+ expect(json_response['permissions']['group_access']['access_level']).
+ to eq(Gitlab::Access::OWNER)
+ end
+ end
+ end
+ end
+ end
+
+ describe 'GET /projects/:id/events' do
+ shared_examples_for 'project events response' do
+ it 'returns the project events' do
+ member = create(:user)
+ create(:project_member, :developer, user: member, project: project)
+ note = create(:note_on_issue, note: 'What an awesome day!', project: project)
+ EventCreateService.new.leave_note(note, note.author)
+
+ get v3_api("/projects/#{project.id}/events", current_user)
+
+ expect(response).to have_http_status(200)
+
+ first_event = json_response.first
+
+ expect(first_event['action_name']).to eq('commented on')
+ expect(first_event['note']['body']).to eq('What an awesome day!')
+
+ last_event = json_response.last
+
+ expect(last_event['action_name']).to eq('joined')
+ expect(last_event['project_id'].to_i).to eq(project.id)
+ expect(last_event['author_username']).to eq(member.username)
+ expect(last_event['author']['name']).to eq(member.name)
+ end
+ end
+
+ context 'when unauthenticated' do
+ it_behaves_like 'project events response' do
+ let(:project) { create(:empty_project, :public) }
+ let(:current_user) { nil }
+ end
+ end
+
+ context 'when authenticated' do
+ context 'valid request' do
+ it_behaves_like 'project events response' do
+ let(:current_user) { user }
+ end
+ end
+
+ it 'returns a 404 error if not found' do
+ get v3_api('/projects/42/events', user)
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 Project Not Found')
+ end
+
+ it 'returns a 404 error if user is not a member' do
+ other_user = create(:user)
+
+ get v3_api("/projects/#{project.id}/events", other_user)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ describe 'GET /projects/:id/users' do
+ shared_examples_for 'project users response' do
+ it 'returns the project users' do
+ member = create(:user)
+ create(:project_member, :developer, user: member, project: project)
+
+ get v3_api("/projects/#{project.id}/users", current_user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.size).to eq(1)
+
+ first_user = json_response.first
+
+ expect(first_user['username']).to eq(member.username)
+ expect(first_user['name']).to eq(member.name)
+ expect(first_user.keys).to contain_exactly(*%w[name username id state avatar_url web_url])
+ end
+ end
+
+ context 'when unauthenticated' do
+ it_behaves_like 'project users response' do
+ let(:project) { create(:empty_project, :public) }
+ let(:current_user) { nil }
+ end
+ end
+
+ context 'when authenticated' do
+ context 'valid request' do
+ it_behaves_like 'project users response' do
+ let(:current_user) { user }
+ end
+ end
+
+ it 'returns a 404 error if not found' do
+ get v3_api('/projects/42/users', user)
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 Project Not Found')
+ end
+
+ it 'returns a 404 error if user is not a member' do
+ other_user = create(:user)
+
+ get v3_api("/projects/#{project.id}/users", other_user)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ describe 'GET /projects/:id/snippets' do
+ before { snippet }
+
+ it 'returns an array of project snippets' do
+ get v3_api("/projects/#{project.id}/snippets", user)
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first['title']).to eq(snippet.title)
+ end
+ end
+
+ describe 'GET /projects/:id/snippets/:snippet_id' do
+ it 'returns a project snippet' do
+ get v3_api("/projects/#{project.id}/snippets/#{snippet.id}", user)
+ expect(response).to have_http_status(200)
+ expect(json_response['title']).to eq(snippet.title)
+ end
+
+ it 'returns a 404 error if snippet id not found' do
+ get v3_api("/projects/#{project.id}/snippets/1234", user)
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ describe 'POST /projects/:id/snippets' do
+ it 'creates a new project snippet' do
+ post v3_api("/projects/#{project.id}/snippets", user),
+ title: 'v3_api test', file_name: 'sample.rb', code: 'test',
+ visibility_level: '0'
+ expect(response).to have_http_status(201)
+ expect(json_response['title']).to eq('v3_api test')
+ end
+
+ it 'returns a 400 error if invalid snippet is given' do
+ post v3_api("/projects/#{project.id}/snippets", user)
+ expect(status).to eq(400)
+ end
+ end
+
+ describe 'PUT /projects/:id/snippets/:snippet_id' do
+ it 'updates an existing project snippet' do
+ put v3_api("/projects/#{project.id}/snippets/#{snippet.id}", user),
+ code: 'updated code'
+ expect(response).to have_http_status(200)
+ expect(json_response['title']).to eq('example')
+ expect(snippet.reload.content).to eq('updated code')
+ end
+
+ it 'updates an existing project snippet with new title' do
+ put v3_api("/projects/#{project.id}/snippets/#{snippet.id}", user),
+ title: 'other v3_api test'
+ expect(response).to have_http_status(200)
+ expect(json_response['title']).to eq('other v3_api test')
+ end
+ end
+
+ describe 'DELETE /projects/:id/snippets/:snippet_id' do
+ before { snippet }
+
+ it 'deletes existing project snippet' do
+ expect do
+ delete v3_api("/projects/#{project.id}/snippets/#{snippet.id}", user)
+ end.to change { Snippet.count }.by(-1)
+ expect(response).to have_http_status(200)
+ end
+
+ it 'returns 404 when deleting unknown snippet id' do
+ delete v3_api("/projects/#{project.id}/snippets/1234", user)
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ describe 'GET /projects/:id/snippets/:snippet_id/raw' do
+ it 'gets a raw project snippet' do
+ get v3_api("/projects/#{project.id}/snippets/#{snippet.id}/raw", user)
+ expect(response).to have_http_status(200)
+ end
+
+ it 'returns a 404 error if raw project snippet not found' do
+ get v3_api("/projects/#{project.id}/snippets/5555/raw", user)
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ describe :fork_admin do
+ let(:project_fork_target) { create(:empty_project) }
+ let(:project_fork_source) { create(:empty_project, :public) }
+
+ describe 'POST /projects/:id/fork/:forked_from_id' do
+ let(:new_project_fork_source) { create(:empty_project, :public) }
+
+ it "is not available for non admin users" do
+ post v3_api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", user)
+ expect(response).to have_http_status(403)
+ end
+
+ it 'allows project to be forked from an existing project' do
+ expect(project_fork_target.forked?).not_to be_truthy
+ post v3_api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", admin)
+ expect(response).to have_http_status(201)
+ project_fork_target.reload
+ expect(project_fork_target.forked_from_project.id).to eq(project_fork_source.id)
+ expect(project_fork_target.forked_project_link).not_to be_nil
+ expect(project_fork_target.forked?).to be_truthy
+ end
+
+ it 'fails if forked_from project which does not exist' do
+ post v3_api("/projects/#{project_fork_target.id}/fork/9999", admin)
+ expect(response).to have_http_status(404)
+ end
+
+ it 'fails with 409 if already forked' do
+ post v3_api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", admin)
+ project_fork_target.reload
+ expect(project_fork_target.forked_from_project.id).to eq(project_fork_source.id)
+ post v3_api("/projects/#{project_fork_target.id}/fork/#{new_project_fork_source.id}", admin)
+ expect(response).to have_http_status(409)
+ project_fork_target.reload
+ expect(project_fork_target.forked_from_project.id).to eq(project_fork_source.id)
+ expect(project_fork_target.forked?).to be_truthy
+ end
+ end
+
+ describe 'DELETE /projects/:id/fork' do
+ it "is not visible to users outside group" do
+ delete v3_api("/projects/#{project_fork_target.id}/fork", user)
+ expect(response).to have_http_status(404)
+ end
+
+ context 'when users belong to project group' do
+ let(:project_fork_target) { create(:empty_project, group: create(:group)) }
+
+ before do
+ project_fork_target.group.add_owner user
+ project_fork_target.group.add_developer user2
+ end
+
+ it 'is forbidden to non-owner users' do
+ delete v3_api("/projects/#{project_fork_target.id}/fork", user2)
+ expect(response).to have_http_status(403)
+ end
+
+ it 'makes forked project unforked' do
+ post v3_api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", admin)
+ project_fork_target.reload
+ expect(project_fork_target.forked_from_project).not_to be_nil
+ expect(project_fork_target.forked?).to be_truthy
+ delete v3_api("/projects/#{project_fork_target.id}/fork", admin)
+ expect(response).to have_http_status(200)
+ project_fork_target.reload
+ expect(project_fork_target.forked_from_project).to be_nil
+ expect(project_fork_target.forked?).not_to be_truthy
+ end
+
+ it 'is idempotent if not forked' do
+ expect(project_fork_target.forked_from_project).to be_nil
+ delete v3_api("/projects/#{project_fork_target.id}/fork", admin)
+ expect(response).to have_http_status(304)
+ expect(project_fork_target.reload.forked_from_project).to be_nil
+ end
+ end
+ end
+ end
+
+ describe "POST /projects/:id/share" do
+ let(:group) { create(:group) }
+
+ it "shares project with group" do
+ expires_at = 10.days.from_now.to_date
+
+ expect do
+ post v3_api("/projects/#{project.id}/share", user), group_id: group.id, group_access: Gitlab::Access::DEVELOPER, expires_at: expires_at
+ end.to change { ProjectGroupLink.count }.by(1)
+
+ expect(response).to have_http_status(201)
+ expect(json_response['group_id']).to eq(group.id)
+ expect(json_response['group_access']).to eq(Gitlab::Access::DEVELOPER)
+ expect(json_response['expires_at']).to eq(expires_at.to_s)
+ end
+
+ it "returns a 400 error when group id is not given" do
+ post v3_api("/projects/#{project.id}/share", user), group_access: Gitlab::Access::DEVELOPER
+ expect(response).to have_http_status(400)
+ end
+
+ it "returns a 400 error when access level is not given" do
+ post v3_api("/projects/#{project.id}/share", user), group_id: group.id
+ expect(response).to have_http_status(400)
+ end
+
+ it "returns a 400 error when sharing is disabled" do
+ project.namespace.update(share_with_group_lock: true)
+ post v3_api("/projects/#{project.id}/share", user), group_id: group.id, group_access: Gitlab::Access::DEVELOPER
+ expect(response).to have_http_status(400)
+ end
+
+ it 'returns a 404 error when user cannot read group' do
+ private_group = create(:group, :private)
+
+ post v3_api("/projects/#{project.id}/share", user), group_id: private_group.id, group_access: Gitlab::Access::DEVELOPER
+
+ expect(response).to have_http_status(404)
+ end
+
+ it 'returns a 404 error when group does not exist' do
+ post v3_api("/projects/#{project.id}/share", user), group_id: 1234, group_access: Gitlab::Access::DEVELOPER
+
+ expect(response).to have_http_status(404)
+ end
+
+ it "returns a 400 error when wrong params passed" do
+ post v3_api("/projects/#{project.id}/share", user), group_id: group.id, group_access: 1234
+
+ expect(response).to have_http_status(400)
+ expect(json_response['error']).to eq 'group_access does not have a valid value'
+ end
+ end
+
+ describe 'DELETE /projects/:id/share/:group_id' do
+ it 'returns 204 when deleting a group share' do
+ group = create(:group, :public)
+ create(:project_group_link, group: group, project: project)
+
+ delete v3_api("/projects/#{project.id}/share/#{group.id}", user)
+
+ expect(response).to have_http_status(204)
+ expect(project.project_group_links).to be_empty
+ end
+
+ it 'returns a 400 when group id is not an integer' do
+ delete v3_api("/projects/#{project.id}/share/foo", user)
+
+ expect(response).to have_http_status(400)
+ end
+
+ it 'returns a 404 error when group link does not exist' do
+ delete v3_api("/projects/#{project.id}/share/1234", user)
+
+ expect(response).to have_http_status(404)
+ end
+
+ it 'returns a 404 error when project does not exist' do
+ delete v3_api("/projects/123/share/1234", user)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ describe 'GET /projects/search/:query' do
+ let!(:query) { 'query'}
+ let!(:search) { create(:empty_project, name: query, creator_id: user.id, namespace: user.namespace) }
+ let!(:pre) { create(:empty_project, name: "pre_#{query}", creator_id: user.id, namespace: user.namespace) }
+ let!(:post) { create(:empty_project, name: "#{query}_post", creator_id: user.id, namespace: user.namespace) }
+ let!(:pre_post) { create(:empty_project, name: "pre_#{query}_post", creator_id: user.id, namespace: user.namespace) }
+ let!(:unfound) { create(:empty_project, name: 'unfound', creator_id: user.id, namespace: user.namespace) }
+ let!(:internal) { create(:empty_project, :internal, name: "internal #{query}") }
+ let!(:unfound_internal) { create(:empty_project, :internal, name: 'unfound internal') }
+ let!(:public) { create(:empty_project, :public, name: "public #{query}") }
+ let!(:unfound_public) { create(:empty_project, :public, name: 'unfound public') }
+ let!(:one_dot_two) { create(:empty_project, :public, name: "one.dot.two") }
+
+ shared_examples_for 'project search response' do |args = {}|
+ it 'returns project search responses' do
+ get v3_api("/projects/search/#{args[:query]}", current_user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.size).to eq(args[:results])
+ json_response.each { |project| expect(project['name']).to match(args[:match_regex] || /.*#{args[:query]}.*/) }
+ end
+ end
+
+ context 'when unauthenticated' do
+ it_behaves_like 'project search response', query: 'query', results: 1 do
+ let(:current_user) { nil }
+ end
+ end
+
+ context 'when authenticated' do
+ it_behaves_like 'project search response', query: 'query', results: 6 do
+ let(:current_user) { user }
+ end
+ it_behaves_like 'project search response', query: 'one.dot.two', results: 1 do
+ let(:current_user) { user }
+ end
+ end
+
+ context 'when authenticated as a different user' do
+ it_behaves_like 'project search response', query: 'query', results: 2, match_regex: /(internal|public) query/ do
+ let(:current_user) { user2 }
+ end
+ end
+ end
+
+ describe 'PUT /projects/:id' do
+ before { project }
+ before { user }
+ before { user3 }
+ before { user4 }
+ before { project3 }
+ before { project4 }
+ before { project_member3 }
+ before { project_member2 }
+
+ context 'when unauthenticated' do
+ it 'returns authentication error' do
+ project_param = { name: 'bar' }
+ put v3_api("/projects/#{project.id}"), project_param
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ context 'when authenticated as project owner' do
+ it 'updates name' do
+ project_param = { name: 'bar' }
+ put v3_api("/projects/#{project.id}", user), project_param
+ expect(response).to have_http_status(200)
+ project_param.each_pair do |k, v|
+ expect(json_response[k.to_s]).to eq(v)
+ end
+ end
+
+ it 'updates visibility_level' do
+ project_param = { visibility_level: 20 }
+ put v3_api("/projects/#{project3.id}", user), project_param
+ expect(response).to have_http_status(200)
+ project_param.each_pair do |k, v|
+ expect(json_response[k.to_s]).to eq(v)
+ end
+ end
+
+ it 'updates visibility_level from public to private' do
+ project3.update_attributes({ visibility_level: Gitlab::VisibilityLevel::PUBLIC })
+ project_param = { public: false }
+ put v3_api("/projects/#{project3.id}", user), project_param
+ expect(response).to have_http_status(200)
+ project_param.each_pair do |k, v|
+ expect(json_response[k.to_s]).to eq(v)
+ end
+ expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE)
+ end
+
+ it 'does not update name to existing name' do
+ project_param = { name: project3.name }
+ put v3_api("/projects/#{project.id}", user), project_param
+ expect(response).to have_http_status(400)
+ expect(json_response['message']['name']).to eq(['has already been taken'])
+ end
+
+ it 'updates request_access_enabled' do
+ project_param = { request_access_enabled: false }
+
+ put v3_api("/projects/#{project.id}", user), project_param
+
+ expect(response).to have_http_status(200)
+ expect(json_response['request_access_enabled']).to eq(false)
+ end
+
+ it 'updates path & name to existing path & name in different namespace' do
+ project_param = { path: project4.path, name: project4.name }
+ put v3_api("/projects/#{project3.id}", user), project_param
+ expect(response).to have_http_status(200)
+ project_param.each_pair do |k, v|
+ expect(json_response[k.to_s]).to eq(v)
+ end
+ end
+ end
+
+ context 'when authenticated as project master' do
+ it 'updates path' do
+ project_param = { path: 'bar' }
+ put v3_api("/projects/#{project3.id}", user4), project_param
+ expect(response).to have_http_status(200)
+ project_param.each_pair do |k, v|
+ expect(json_response[k.to_s]).to eq(v)
+ end
+ end
+
+ it 'updates other attributes' do
+ project_param = { issues_enabled: true,
+ wiki_enabled: true,
+ snippets_enabled: true,
+ merge_requests_enabled: true,
+ description: 'new description' }
+
+ put v3_api("/projects/#{project3.id}", user4), project_param
+ expect(response).to have_http_status(200)
+ project_param.each_pair do |k, v|
+ expect(json_response[k.to_s]).to eq(v)
+ end
+ end
+
+ it 'does not update path to existing path' do
+ project_param = { path: project.path }
+ put v3_api("/projects/#{project3.id}", user4), project_param
+ expect(response).to have_http_status(400)
+ expect(json_response['message']['path']).to eq(['has already been taken'])
+ end
+
+ it 'does not update name' do
+ project_param = { name: 'bar' }
+ put v3_api("/projects/#{project3.id}", user4), project_param
+ expect(response).to have_http_status(403)
+ end
+
+ it 'does not update visibility_level' do
+ project_param = { visibility_level: 20 }
+ put v3_api("/projects/#{project3.id}", user4), project_param
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ context 'when authenticated as project developer' do
+ it 'does not update other attributes' do
+ project_param = { path: 'bar',
+ issues_enabled: true,
+ wiki_enabled: true,
+ snippets_enabled: true,
+ merge_requests_enabled: true,
+ description: 'new description',
+ request_access_enabled: true }
+ put v3_api("/projects/#{project.id}", user3), project_param
+ expect(response).to have_http_status(403)
+ end
+ end
+ end
+
+ describe 'POST /projects/:id/archive' do
+ context 'on an unarchived project' do
+ it 'archives the project' do
+ post v3_api("/projects/#{project.id}/archive", user)
+
+ expect(response).to have_http_status(201)
+ expect(json_response['archived']).to be_truthy
+ end
+ end
+
+ context 'on an archived project' do
+ before do
+ project.archive!
+ end
+
+ it 'remains archived' do
+ post v3_api("/projects/#{project.id}/archive", user)
+
+ expect(response).to have_http_status(201)
+ expect(json_response['archived']).to be_truthy
+ end
+ end
+
+ context 'user without archiving rights to the project' do
+ before do
+ project.team << [user3, :developer]
+ end
+
+ it 'rejects the action' do
+ post v3_api("/projects/#{project.id}/archive", user3)
+
+ expect(response).to have_http_status(403)
+ end
+ end
+ end
+
+ describe 'POST /projects/:id/unarchive' do
+ context 'on an unarchived project' do
+ it 'remains unarchived' do
+ post v3_api("/projects/#{project.id}/unarchive", user)
+
+ expect(response).to have_http_status(201)
+ expect(json_response['archived']).to be_falsey
+ end
+ end
+
+ context 'on an archived project' do
+ before do
+ project.archive!
+ end
+
+ it 'unarchives the project' do
+ post v3_api("/projects/#{project.id}/unarchive", user)
+
+ expect(response).to have_http_status(201)
+ expect(json_response['archived']).to be_falsey
+ end
+ end
+
+ context 'user without archiving rights to the project' do
+ before do
+ project.team << [user3, :developer]
+ end
+
+ it 'rejects the action' do
+ post v3_api("/projects/#{project.id}/unarchive", user3)
+
+ expect(response).to have_http_status(403)
+ end
+ end
+ end
+
+ describe 'POST /projects/:id/star' do
+ context 'on an unstarred project' do
+ it 'stars the project' do
+ expect { post v3_api("/projects/#{project.id}/star", user) }.to change { project.reload.star_count }.by(1)
+
+ expect(response).to have_http_status(201)
+ expect(json_response['star_count']).to eq(1)
+ end
+ end
+
+ context 'on a starred project' do
+ before do
+ user.toggle_star(project)
+ project.reload
+ end
+
+ it 'does not modify the star count' do
+ expect { post v3_api("/projects/#{project.id}/star", user) }.not_to change { project.reload.star_count }
+
+ expect(response).to have_http_status(304)
+ end
+ end
+ end
+
+ describe 'DELETE /projects/:id/star' do
+ context 'on a starred project' do
+ before do
+ user.toggle_star(project)
+ project.reload
+ end
+
+ it 'unstars the project' do
+ expect { delete v3_api("/projects/#{project.id}/star", user) }.to change { project.reload.star_count }.by(-1)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['star_count']).to eq(0)
+ end
+ end
+
+ context 'on an unstarred project' do
+ it 'does not modify the star count' do
+ expect { delete v3_api("/projects/#{project.id}/star", user) }.not_to change { project.reload.star_count }
+
+ expect(response).to have_http_status(304)
+ end
+ end
+ end
+
+ describe 'DELETE /projects/:id' do
+ context 'when authenticated as user' do
+ it 'removes project' do
+ delete v3_api("/projects/#{project.id}", user)
+ expect(response).to have_http_status(200)
+ end
+
+ it 'does not remove a project if not an owner' do
+ user3 = create(:user)
+ project.team << [user3, :developer]
+ delete v3_api("/projects/#{project.id}", user3)
+ expect(response).to have_http_status(403)
+ end
+
+ it 'does not remove a non existing project' do
+ delete v3_api('/projects/1328', user)
+ expect(response).to have_http_status(404)
+ end
+
+ it 'does not remove a project not attached to user' do
+ delete v3_api("/projects/#{project.id}", user2)
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'when authenticated as admin' do
+ it 'removes any existing project' do
+ delete v3_api("/projects/#{project.id}", admin)
+ expect(response).to have_http_status(200)
+ end
+
+ it 'does not remove a non existing project' do
+ delete v3_api('/projects/1328', admin)
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/variables_spec.rb b/spec/requests/api/variables_spec.rb
index 7435f320607..769f04c5057 100644
--- a/spec/requests/api/variables_spec.rb
+++ b/spec/requests/api/variables_spec.rb
@@ -5,7 +5,7 @@ describe API::Variables, api: true do
let(:user) { create(:user) }
let(:user2) { create(:user) }
- let!(:project) { create(:project, creator_id: user.id) }
+ let!(:project) { create(:empty_project, creator_id: user.id) }
let!(:master) { create(:project_member, :master, user: user, project: project) }
let!(:developer) { create(:project_member, :developer, user: user2, project: project) }
let!(:variable) { create(:ci_variable, project: project) }
diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb
index 79f12ace999..d85afdeab42 100644
--- a/spec/requests/ci/api/builds_spec.rb
+++ b/spec/requests/ci/api/builds_spec.rb
@@ -4,7 +4,8 @@ describe Ci::API::Builds do
include ApiHelpers
let(:runner) { FactoryGirl.create(:ci_runner, tag_list: ["mysql", "ruby"]) }
- let(:project) { FactoryGirl.create(:empty_project) }
+ let(:project) { FactoryGirl.create(:empty_project, shared_runners_enabled: false) }
+ let(:last_update) { nil }
describe "Builds API for runners" do
let(:pipeline) { create(:ci_pipeline_without_jobs, project: project, ref: 'master') }
@@ -16,6 +17,8 @@ describe Ci::API::Builds do
describe "POST /builds/register" do
let!(:build) { create(:ci_build, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
let(:user_agent) { 'gitlab-ci-multi-runner 1.5.2 (1-5-stable; go1.6.3; linux/amd64)' }
+ let!(:last_update) { }
+ let!(:new_update) { }
before do
stub_container_registry_config(enabled: false)
@@ -24,7 +27,31 @@ describe Ci::API::Builds do
shared_examples 'no builds available' do
context 'when runner sends version in User-Agent' do
context 'for stable version' do
- it { expect(response).to have_http_status(204) }
+ it 'gives 204 and set X-GitLab-Last-Update' do
+ expect(response).to have_http_status(204)
+ expect(response.header).to have_key('X-GitLab-Last-Update')
+ end
+ end
+
+ context 'when last_update is up-to-date' do
+ let(:last_update) { runner.ensure_runner_queue_value }
+
+ it 'gives 204 and set the same X-GitLab-Last-Update' do
+ expect(response).to have_http_status(204)
+ expect(response.header['X-GitLab-Last-Update'])
+ .to eq(last_update)
+ end
+ end
+
+ context 'when last_update is outdated' do
+ let(:last_update) { runner.ensure_runner_queue_value }
+ let(:new_update) { runner.tick_runner_queue }
+
+ it 'gives 204 and set a new X-GitLab-Last-Update' do
+ expect(response).to have_http_status(204)
+ expect(response.header['X-GitLab-Last-Update'])
+ .to eq(new_update)
+ end
end
context 'for beta version' do
@@ -37,6 +64,11 @@ describe Ci::API::Builds do
let(:user_agent) { 'Go-http-client/1.1' }
it { expect(response).to have_http_status(404) }
end
+
+ context "when runner doesn't have a User-Agent" do
+ let(:user_agent) { nil }
+ it { expect(response).to have_http_status(404) }
+ end
end
context 'when there is a pending build' do
@@ -44,6 +76,7 @@ describe Ci::API::Builds do
register_builds info: { platform: :darwin }
expect(response).to have_http_status(201)
+ expect(response.headers).not_to have_key('X-GitLab-Last-Update')
expect(json_response['sha']).to eq(build.sha)
expect(runner.reload.platform).to eq("darwin")
expect(json_response["options"]).to eq({ "image" => "ruby:2.1", "services" => ["postgres"] })
@@ -58,6 +91,20 @@ describe Ci::API::Builds do
expect { register_builds }.to change { runner.reload.contacted_at }
end
+ context 'when concurrently updating build' do
+ before do
+ expect_any_instance_of(Ci::Build).to receive(:run!).
+ and_raise(ActiveRecord::StaleObjectError.new(nil, nil))
+ end
+
+ it 'returns a conflict' do
+ register_builds info: { platform: :darwin }
+
+ expect(response).to have_http_status(409)
+ expect(response.headers).not_to have_key('X-GitLab-Last-Update')
+ end
+ end
+
context 'registry credentials' do
let(:registry_credentials) do
{ 'type' => 'registry',
@@ -114,10 +161,10 @@ describe Ci::API::Builds do
end
context 'for shared runner' do
- let(:shared_runner) { create(:ci_runner, token: "SharedRunner") }
+ let!(:runner) { create(:ci_runner, :shared, token: "SharedRunner") }
before do
- register_builds shared_runner.token
+ register_builds(runner.token)
end
it_behaves_like 'no builds available'
@@ -219,7 +266,9 @@ describe Ci::API::Builds do
end
def register_builds(token = runner.token, **params)
- post ci_api("/builds/register"), params.merge(token: token), { 'User-Agent' => user_agent }
+ new_params = params.merge(token: token, last_update: last_update)
+
+ post ci_api("/builds/register"), new_params, { 'User-Agent' => user_agent }
end
end
@@ -239,7 +288,7 @@ describe Ci::API::Builds do
expect(build.reload.trace).to eq 'BUILD TRACE'
end
- context 'build has been erased' do
+ context 'job has been erased' do
let(:build) { create(:ci_build, runner_id: runner.id, erased_at: Time.now) }
it 'responds with forbidden' do
@@ -409,7 +458,7 @@ describe Ci::API::Builds do
before { build.run! }
describe "POST /builds/:id/artifacts/authorize" do
- context "should authorize posting artifact to running build" do
+ context "authorizes posting artifact to running build" do
it "using token as parameter" do
post authorize_url, { token: build.token }, headers
@@ -443,7 +492,7 @@ describe Ci::API::Builds do
end
end
- context "should fail to post too large artifact" do
+ context "fails to post too large artifact" do
it "using token as parameter" do
stub_application_setting(max_artifacts_size: 0)
diff --git a/spec/requests/ci/api/triggers_spec.rb b/spec/requests/ci/api/triggers_spec.rb
index 2d434ab5dd8..a30be767119 100644
--- a/spec/requests/ci/api/triggers_spec.rb
+++ b/spec/requests/ci/api/triggers_spec.rb
@@ -5,9 +5,9 @@ describe Ci::API::Triggers do
describe 'POST /projects/:project_id/refs/:ref/trigger' do
let!(:trigger_token) { 'secure token' }
- let!(:project) { FactoryGirl.create(:project, ci_id: 10) }
- let!(:project2) { FactoryGirl.create(:empty_project, ci_id: 11) }
- let!(:trigger) { FactoryGirl.create(:ci_trigger, project: project, token: trigger_token) }
+ let!(:project) { create(:project, :repository, ci_id: 10) }
+ let!(:project2) { create(:empty_project, ci_id: 11) }
+ let!(:trigger) { create(:ci_trigger, project: project, token: trigger_token) }
let(:options) do
{
token: trigger_token
diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb
index d71bb08c218..87786e85621 100644
--- a/spec/requests/git_http_spec.rb
+++ b/spec/requests/git_http_spec.rb
@@ -12,7 +12,7 @@ describe 'Git HTTP requests', lib: true do
describe "User with no identities" do
let(:user) { create(:user) }
- let(:project) { create(:project, path: 'project.git-project') }
+ let(:project) { create(:project, :repository, path: 'project.git-project') }
context "when the project doesn't exist" do
context "when no authentication is provided" do
@@ -55,6 +55,28 @@ describe 'Git HTTP requests', lib: true do
expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
end
end
+
+ context 'but the repo is disabled' do
+ let(:project) { create(:project, :repository_disabled, :wiki_enabled) }
+ let(:wiki) { ProjectWiki.new(project) }
+ let(:path) { "/#{wiki.repository.path_with_namespace}.git" }
+
+ before do
+ project.team << [user, :developer]
+ end
+
+ it 'allows clones' do
+ download(path, user: user.username, password: user.password) do |response|
+ expect(response).to have_http_status(200)
+ end
+ end
+
+ it 'allows pushes' do
+ upload(path, user: user.username, password: user.password) do |response|
+ expect(response).to have_http_status(200)
+ end
+ end
+ end
end
context "when the project exists" do
@@ -119,7 +141,7 @@ describe 'Git HTTP requests', lib: true do
context 'when the repo is public' do
context 'but the repo is disabled' do
it 'does not allow to clone the repo' do
- project = create(:project, :public, repository_access_level: ProjectFeature::DISABLED)
+ project = create(:project, :public, :repository_disabled)
download("#{project.path_with_namespace}.git", {}) do |response|
expect(response).to have_http_status(:unauthorized)
@@ -129,7 +151,7 @@ describe 'Git HTTP requests', lib: true do
context 'but the repo is enabled' do
it 'allows to clone the repo' do
- project = create(:project, :public, repository_access_level: ProjectFeature::ENABLED)
+ project = create(:project, :public, :repository_enabled)
download("#{project.path_with_namespace}.git", {}) do |response|
expect(response).to have_http_status(:ok)
@@ -139,7 +161,7 @@ describe 'Git HTTP requests', lib: true do
context 'but only project members are allowed' do
it 'does not allow to clone the repo' do
- project = create(:project, :public, repository_access_level: ProjectFeature::PRIVATE)
+ project = create(:project, :public, :repository_private)
download("#{project.path_with_namespace}.git", {}) do |response|
expect(response).to have_http_status(:unauthorized)
@@ -338,10 +360,6 @@ describe 'Git HTTP requests', lib: true do
let(:project) { build.project }
let(:other_project) { create(:empty_project) }
- before do
- project.project_feature.update_attributes(builds_access_level: ProjectFeature::ENABLED)
- end
-
context 'when build created by system is authenticated' do
it "downloads get status 200" do
clone_get "#{project.path_with_namespace}.git", user: 'gitlab-ci-token', password: build.token
@@ -371,12 +389,26 @@ describe 'Git HTTP requests', lib: true do
shared_examples 'can download code only' do
it 'downloads get status 200' do
- clone_get "#{project.path_with_namespace}.git", user: 'gitlab-ci-token', password: build.token
+ allow_any_instance_of(Repository).
+ to receive(:exists?).and_return(true)
+
+ clone_get "#{project.path_with_namespace}.git",
+ user: 'gitlab-ci-token', password: build.token
expect(response).to have_http_status(200)
expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
end
+ it 'downloads from non-existing repository and gets 403' do
+ allow_any_instance_of(Repository).
+ to receive(:exists?).and_return(false)
+
+ clone_get "#{project.path_with_namespace}.git",
+ user: 'gitlab-ci-token', password: build.token
+
+ expect(response).to have_http_status(403)
+ end
+
it 'uploads get status 403' do
push_get "#{project.path_with_namespace}.git", user: 'gitlab-ci-token', password: build.token
diff --git a/spec/requests/lfs_http_spec.rb b/spec/requests/lfs_http_spec.rb
index 9bfc84c7425..c0e7bab8199 100644
--- a/spec/requests/lfs_http_spec.rb
+++ b/spec/requests/lfs_http_spec.rb
@@ -600,6 +600,7 @@ describe 'Git LFS API and storage' do
expect(json_response).to eq('objects' => [
{ 'oid' => sample_oid,
'size' => sample_size,
+ 'authenticated' => true,
'actions' => {
'download' => {
'href' => "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}",
diff --git a/spec/requests/projects/artifacts_controller_spec.rb b/spec/requests/projects/artifacts_controller_spec.rb
index e02f0eacc93..d20866c0d44 100644
--- a/spec/requests/projects/artifacts_controller_spec.rb
+++ b/spec/requests/projects/artifacts_controller_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe Projects::ArtifactsController do
let(:user) { create(:user) }
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:pipeline) do
create(:ci_pipeline,
diff --git a/spec/requests/projects/cycle_analytics_events_spec.rb b/spec/requests/projects/cycle_analytics_events_spec.rb
index e0368e6001f..0edbffbcd3b 100644
--- a/spec/requests/projects/cycle_analytics_events_spec.rb
+++ b/spec/requests/projects/cycle_analytics_events_spec.rb
@@ -1,8 +1,10 @@
require 'spec_helper'
describe 'cycle analytics events' do
+ include ApiHelpers
+
let(:user) { create(:user) }
- let(:project) { create(:project, public_builds: false) }
+ let(:project) { create(:project, :repository, public_builds: false) }
let(:issue) { create(:issue, project: project, created_at: 2.days.ago) }
describe 'GET /:namespace/:project/cycle_analytics/events/issues' do
@@ -11,7 +13,12 @@ describe 'cycle analytics events' do
allow_any_instance_of(Gitlab::ReferenceExtractor).to receive(:issues).and_return([issue])
- 3.times { create_cycle }
+ 3.times do |count|
+ Timecop.freeze(Time.now + count.days) do
+ create_cycle
+ end
+ end
+
deploy_master
login_as(user)
@@ -20,19 +27,19 @@ describe 'cycle analytics events' do
it 'lists the issue events' do
get namespace_project_cycle_analytics_issue_path(project.namespace, project, format: :json)
- expect(json_response['events']).not_to be_empty
-
- first_issue_iid = Issue.order(created_at: :desc).pluck(:iid).first.to_s
+ first_issue_iid = project.issues.sort(:created_desc).pluck(:iid).first.to_s
+ expect(json_response['events']).not_to be_empty
expect(json_response['events'].first['iid']).to eq(first_issue_iid)
end
it 'lists the plan events' do
get namespace_project_cycle_analytics_plan_path(project.namespace, project, format: :json)
- expect(json_response['events']).not_to be_empty
+ first_mr_short_sha = project.merge_requests.sort(:created_asc).first.commits.first.short_id
- expect(json_response['events'].first['short_sha']).to eq(MergeRequest.last.commits.first.short_id)
+ expect(json_response['events']).not_to be_empty
+ expect(json_response['events'].first['short_sha']).to eq(first_mr_short_sha)
end
it 'lists the code events' do
@@ -40,7 +47,7 @@ describe 'cycle analytics events' do
expect(json_response['events']).not_to be_empty
- first_mr_iid = project.merge_requests.order(id: :desc).pluck(:iid).first.to_s
+ first_mr_iid = project.merge_requests.sort(:created_desc).pluck(:iid).first.to_s
expect(json_response['events'].first['iid']).to eq(first_mr_iid)
end
@@ -49,17 +56,15 @@ describe 'cycle analytics events' do
get namespace_project_cycle_analytics_test_path(project.namespace, project, format: :json)
expect(json_response['events']).not_to be_empty
-
expect(json_response['events'].first['date']).not_to be_empty
end
it 'lists the review events' do
get namespace_project_cycle_analytics_review_path(project.namespace, project, format: :json)
- expect(json_response['events']).not_to be_empty
-
- first_mr_iid = MergeRequest.order(created_at: :desc).pluck(:iid).first.to_s
+ first_mr_iid = project.merge_requests.sort(:created_desc).pluck(:iid).first.to_s
+ expect(json_response['events']).not_to be_empty
expect(json_response['events'].first['iid']).to eq(first_mr_iid)
end
@@ -67,35 +72,32 @@ describe 'cycle analytics events' do
get namespace_project_cycle_analytics_staging_path(project.namespace, project, format: :json)
expect(json_response['events']).not_to be_empty
-
expect(json_response['events'].first['date']).not_to be_empty
end
it 'lists the production events' do
get namespace_project_cycle_analytics_production_path(project.namespace, project, format: :json)
- expect(json_response['events']).not_to be_empty
-
- first_issue_iid = Issue.order(created_at: :desc).pluck(:iid).first.to_s
+ first_issue_iid = project.issues.sort(:created_desc).pluck(:iid).first.to_s
+ expect(json_response['events']).not_to be_empty
expect(json_response['events'].first['iid']).to eq(first_issue_iid)
end
context 'specific branch' do
it 'lists the test events' do
- branch = MergeRequest.first.source_branch
+ branch = project.merge_requests.first.source_branch
get namespace_project_cycle_analytics_test_path(project.namespace, project, format: :json, branch: branch)
expect(json_response['events']).not_to be_empty
-
expect(json_response['events'].first['date']).not_to be_empty
end
end
context 'with private project and builds' do
before do
- ProjectMember.first.update(access_level: Gitlab::Access::GUEST)
+ project.members.first.update(access_level: Gitlab::Access::GUEST)
end
it 'does not list the test events' do
@@ -118,10 +120,6 @@ describe 'cycle analytics events' do
end
end
- def json_response
- JSON.parse(response.body)
- end
-
def create_cycle
milestone = create(:milestone, project: project)
issue.update(milestone: milestone)
diff --git a/spec/routing/admin_routing_spec.rb b/spec/routing/admin_routing_spec.rb
index 661b671301e..99c44bde151 100644
--- a/spec/routing/admin_routing_spec.rb
+++ b/spec/routing/admin_routing_spec.rb
@@ -122,12 +122,18 @@ describe Admin::HealthCheckController, "routing" do
end
describe Admin::GroupsController, "routing" do
+ let(:name) { 'complex.group-namegit' }
+
it "to #index" do
expect(get("/admin/groups")).to route_to('admin/groups#index')
end
it "to #show" do
- expect(get("/admin/groups/gitlab")).to route_to('admin/groups#show', id: 'gitlab')
- expect(get("/admin/groups/gitlab/subgroup")).to route_to('admin/groups#show', id: 'gitlab/subgroup')
+ expect(get("/admin/groups/#{name}")).to route_to('admin/groups#show', id: name)
+ expect(get("/admin/groups/#{name}/subgroup")).to route_to('admin/groups#show', id: "#{name}/subgroup")
+ end
+
+ it "to #edit" do
+ expect(get("/admin/groups/#{name}/edit")).to route_to('admin/groups#edit', id: name)
end
end
diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb
index 77549db2927..a5bc62ef6c2 100644
--- a/spec/routing/project_routing_spec.rb
+++ b/spec/routing/project_routing_spec.rb
@@ -2,8 +2,8 @@ require 'spec_helper'
describe 'project routing' do
before do
- allow(Project).to receive(:find_with_namespace).and_return(false)
- allow(Project).to receive(:find_with_namespace).with('gitlab/gitlabhq').and_return(true)
+ allow(Project).to receive(:find_by_full_path).and_return(false)
+ allow(Project).to receive(:find_by_full_path).with('gitlab/gitlabhq').and_return(true)
end
# Shared examples for a resource inside a Project
@@ -27,35 +27,42 @@ describe 'project routing' do
# let(:actions) { [:index] }
# let(:controller) { 'issues' }
# end
+ #
+ # # Different controller name and path
+ # it_behaves_like 'RESTful project resources' do
+ # let(:controller) { 'pages_domains' }
+ # let(:controller_path) { 'pages/domains' }
+ # end
shared_examples 'RESTful project resources' do
let(:actions) { [:index, :create, :new, :edit, :show, :update, :destroy] }
+ let(:controller_path) { controller }
it 'to #index' do
- expect(get("/gitlab/gitlabhq/#{controller}")).to route_to("projects/#{controller}#index", namespace_id: 'gitlab', project_id: 'gitlabhq') if actions.include?(:index)
+ expect(get("/gitlab/gitlabhq/#{controller_path}")).to route_to("projects/#{controller}#index", namespace_id: 'gitlab', project_id: 'gitlabhq') if actions.include?(:index)
end
it 'to #create' do
- expect(post("/gitlab/gitlabhq/#{controller}")).to route_to("projects/#{controller}#create", namespace_id: 'gitlab', project_id: 'gitlabhq') if actions.include?(:create)
+ expect(post("/gitlab/gitlabhq/#{controller_path}")).to route_to("projects/#{controller}#create", namespace_id: 'gitlab', project_id: 'gitlabhq') if actions.include?(:create)
end
it 'to #new' do
- expect(get("/gitlab/gitlabhq/#{controller}/new")).to route_to("projects/#{controller}#new", namespace_id: 'gitlab', project_id: 'gitlabhq') if actions.include?(:new)
+ expect(get("/gitlab/gitlabhq/#{controller_path}/new")).to route_to("projects/#{controller}#new", namespace_id: 'gitlab', project_id: 'gitlabhq') if actions.include?(:new)
end
it 'to #edit' do
- expect(get("/gitlab/gitlabhq/#{controller}/1/edit")).to route_to("projects/#{controller}#edit", namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') if actions.include?(:edit)
+ expect(get("/gitlab/gitlabhq/#{controller_path}/1/edit")).to route_to("projects/#{controller}#edit", namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') if actions.include?(:edit)
end
it 'to #show' do
- expect(get("/gitlab/gitlabhq/#{controller}/1")).to route_to("projects/#{controller}#show", namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') if actions.include?(:show)
+ expect(get("/gitlab/gitlabhq/#{controller_path}/1")).to route_to("projects/#{controller}#show", namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') if actions.include?(:show)
end
it 'to #update' do
- expect(put("/gitlab/gitlabhq/#{controller}/1")).to route_to("projects/#{controller}#update", namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') if actions.include?(:update)
+ expect(put("/gitlab/gitlabhq/#{controller_path}/1")).to route_to("projects/#{controller}#update", namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') if actions.include?(:update)
end
it 'to #destroy' do
- expect(delete("/gitlab/gitlabhq/#{controller}/1")).to route_to("projects/#{controller}#destroy", namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') if actions.include?(:destroy)
+ expect(delete("/gitlab/gitlabhq/#{controller_path}/1")).to route_to("projects/#{controller}#destroy", namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') if actions.include?(:destroy)
end
end
@@ -86,13 +93,13 @@ describe 'project routing' do
end
context 'name with dot' do
- before { allow(Project).to receive(:find_with_namespace).with('gitlab/gitlabhq.keys').and_return(true) }
+ before { allow(Project).to receive(:find_by_full_path).with('gitlab/gitlabhq.keys').and_return(true) }
it { expect(get('/gitlab/gitlabhq.keys')).to route_to('projects#show', namespace_id: 'gitlab', id: 'gitlabhq.keys') }
end
context 'with nested group' do
- before { allow(Project).to receive(:find_with_namespace).with('gitlab/subgroup/gitlabhq').and_return(true) }
+ before { allow(Project).to receive(:find_by_full_path).with('gitlab/subgroup/gitlabhq').and_return(true) }
it { expect(get('/gitlab/subgroup/gitlabhq')).to route_to('projects#show', namespace_id: 'gitlab/subgroup', id: 'gitlabhq') }
end
@@ -539,4 +546,20 @@ describe 'project routing' do
'projects/avatars#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq')
end
end
+
+ describe Projects::PagesDomainsController, 'routing' do
+ it_behaves_like 'RESTful project resources' do
+ let(:actions) { [:show, :new, :create, :destroy] }
+ let(:controller) { 'pages_domains' }
+ let(:controller_path) { 'pages/domains' }
+ end
+
+ it 'to #destroy with a valid domain name' do
+ expect(delete('/gitlab/gitlabhq/pages/domains/my.domain.com')).to route_to('projects/pages_domains#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'my.domain.com')
+ end
+
+ it 'to #show with a valid domain' do
+ expect(get('/gitlab/gitlabhq/pages/domains/my.domain.com')).to route_to('projects/pages_domains#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'my.domain.com')
+ end
+ end
end
diff --git a/spec/serializers/analytics_build_serializer_spec.rb b/spec/serializers/analytics_build_serializer_spec.rb
index f0551c78671..e3b1dd93dc2 100644
--- a/spec/serializers/analytics_build_serializer_spec.rb
+++ b/spec/serializers/analytics_build_serializer_spec.rb
@@ -1,17 +1,13 @@
require 'spec_helper'
describe AnalyticsBuildSerializer do
- let(:serializer) do
- described_class
- .new.represent(resource)
- end
-
- let(:json) { serializer.as_json }
let(:resource) { create(:ci_build) }
+ subject { described_class.new.represent(resource) }
+
context 'when there is a single object provided' do
it 'contains important elements of analyticsBuild' do
- expect(json)
+ expect(subject)
.to include(:name, :branch, :short_sha, :date, :total_time, :url, :author)
end
end
diff --git a/spec/serializers/analytics_issue_serializer_spec.rb b/spec/serializers/analytics_issue_serializer_spec.rb
index 6afbb2df35c..2f08958a783 100644
--- a/spec/serializers/analytics_issue_serializer_spec.rb
+++ b/spec/serializers/analytics_issue_serializer_spec.rb
@@ -1,14 +1,13 @@
require 'spec_helper'
describe AnalyticsIssueSerializer do
- let(:serializer) do
+ subject do
described_class
.new(project: project, entity: :merge_request)
.represent(resource)
end
let(:user) { create(:user) }
- let(:json) { serializer.as_json }
let(:project) { create(:project) }
let(:resource) do
{
@@ -23,7 +22,7 @@ describe AnalyticsIssueSerializer do
context 'when there is a single object provided' do
it 'contains important elements of the issue' do
- expect(json).to include(:title, :iid, :created_at, :total_time, :url, :author)
+ expect(subject).to include(:title, :iid, :created_at, :total_time, :url, :author)
end
end
end
diff --git a/spec/serializers/analytics_merge_request_serializer_spec.rb b/spec/serializers/analytics_merge_request_serializer_spec.rb
index cdfae27193f..62067cc0ef2 100644
--- a/spec/serializers/analytics_merge_request_serializer_spec.rb
+++ b/spec/serializers/analytics_merge_request_serializer_spec.rb
@@ -1,14 +1,13 @@
require 'spec_helper'
describe AnalyticsMergeRequestSerializer do
- let(:serializer) do
+ subject do
described_class
.new(project: project, entity: :merge_request)
.represent(resource)
end
let(:user) { create(:user) }
- let(:json) { serializer.as_json }
let(:project) { create(:project) }
let(:resource) do
{
@@ -24,7 +23,7 @@ describe AnalyticsMergeRequestSerializer do
context 'when there is a single object provided' do
it 'contains important elements of the merge request' do
- expect(json).to include(:title, :iid, :created_at, :total_time, :url, :author, :state)
+ expect(subject).to include(:title, :iid, :created_at, :total_time, :url, :author, :state)
end
end
end
diff --git a/spec/serializers/analytics_stage_serializer_spec.rb b/spec/serializers/analytics_stage_serializer_spec.rb
new file mode 100644
index 00000000000..be6aa7c65c3
--- /dev/null
+++ b/spec/serializers/analytics_stage_serializer_spec.rb
@@ -0,0 +1,24 @@
+require 'spec_helper'
+
+describe AnalyticsStageSerializer do
+ subject do
+ described_class.new.represent(resource)
+ end
+
+ let(:resource) do
+ Gitlab::CycleAnalytics::CodeStage.new(project: double, options: {})
+ end
+
+ before do
+ allow_any_instance_of(Gitlab::CycleAnalytics::BaseStage).to receive(:median).and_return(1.12)
+ allow_any_instance_of(Gitlab::CycleAnalytics::BaseEventFetcher).to receive(:event_result).and_return({})
+ end
+
+ it 'it generates payload for single object' do
+ expect(subject).to be_kind_of Hash
+ end
+
+ it 'contains important elements of AnalyticsStage' do
+ expect(subject).to include(:title, :description, :value)
+ end
+end
diff --git a/spec/serializers/analytics_summary_serializer_spec.rb b/spec/serializers/analytics_summary_serializer_spec.rb
new file mode 100644
index 00000000000..5d7a94c2d02
--- /dev/null
+++ b/spec/serializers/analytics_summary_serializer_spec.rb
@@ -0,0 +1,28 @@
+require 'spec_helper'
+
+describe AnalyticsSummarySerializer do
+ subject do
+ described_class.new.represent(resource)
+ end
+
+ let(:project) { create(:empty_project) }
+ let(:user) { create(:user) }
+
+ let(:resource) do
+ Gitlab::CycleAnalytics::Summary::Issue
+ .new(project: double, from: 1.day.ago, current_user: user)
+ end
+
+ before do
+ allow_any_instance_of(Gitlab::CycleAnalytics::Summary::Issue)
+ .to receive(:value).and_return(1.12)
+ end
+
+ it 'it generates payload for single object' do
+ expect(subject).to be_kind_of Hash
+ end
+
+ it 'contains important elements of AnalyticsStage' do
+ expect(subject).to include(:title, :value)
+ end
+end
diff --git a/spec/serializers/build_action_entity_spec.rb b/spec/serializers/build_action_entity_spec.rb
new file mode 100644
index 00000000000..0f7be8b2c39
--- /dev/null
+++ b/spec/serializers/build_action_entity_spec.rb
@@ -0,0 +1,21 @@
+require 'spec_helper'
+
+describe BuildActionEntity do
+ let(:build) { create(:ci_build, name: 'test_build') }
+
+ let(:entity) do
+ described_class.new(build, request: double)
+ end
+
+ describe '#as_json' do
+ subject { entity.as_json }
+
+ it 'contains original build name' do
+ expect(subject[:name]).to eq 'test_build'
+ end
+
+ it 'contains path to the action play' do
+ expect(subject[:path]).to include "builds/#{build.id}/play"
+ end
+ end
+end
diff --git a/spec/serializers/build_artifact_entity_spec.rb b/spec/serializers/build_artifact_entity_spec.rb
new file mode 100644
index 00000000000..2fc60aa9de6
--- /dev/null
+++ b/spec/serializers/build_artifact_entity_spec.rb
@@ -0,0 +1,22 @@
+require 'spec_helper'
+
+describe BuildArtifactEntity do
+ let(:build) { create(:ci_build, name: 'test:build') }
+
+ let(:entity) do
+ described_class.new(build, request: double)
+ end
+
+ describe '#as_json' do
+ subject { entity.as_json }
+
+ it 'contains build name' do
+ expect(subject[:name]).to eq 'test:build'
+ end
+
+ it 'contains path to the artifacts' do
+ expect(subject[:path])
+ .to include "builds/#{build.id}/artifacts/download"
+ end
+ end
+end
diff --git a/spec/serializers/commit_entity_spec.rb b/spec/serializers/commit_entity_spec.rb
index 15f11ac3df9..0333d73b5b5 100644
--- a/spec/serializers/commit_entity_spec.rb
+++ b/spec/serializers/commit_entity_spec.rb
@@ -33,10 +33,12 @@ describe CommitEntity do
it 'contains path to commit' do
expect(subject).to include(:commit_path)
+ expect(subject[:commit_path]).to include "commit/#{commit.id}"
end
it 'contains URL to commit' do
expect(subject).to include(:commit_url)
+ expect(subject[:commit_path]).to include "commit/#{commit.id}"
end
it 'needs to receive project in the request' do
@@ -45,4 +47,8 @@ describe CommitEntity do
subject
end
+
+ it 'exposes gravatar url that belongs to author' do
+ expect(subject.fetch(:author_gravatar_url)).to match /gravatar/
+ end
end
diff --git a/spec/serializers/environment_serializer_spec.rb b/spec/serializers/environment_serializer_spec.rb
index b7ed4eb0239..1b95f1ff198 100644
--- a/spec/serializers/environment_serializer_spec.rb
+++ b/spec/serializers/environment_serializer_spec.rb
@@ -1,16 +1,15 @@
require 'spec_helper'
describe EnvironmentSerializer do
- let(:serializer) do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+
+ let(:json) do
described_class
.new(user: user, project: project)
.represent(resource)
end
- let(:json) { serializer.as_json }
- let(:user) { create(:user) }
- let(:project) { create(:project) }
-
context 'when there is a single object provided' do
before do
create(:ci_build, :manual, name: 'manual1',
@@ -53,4 +52,136 @@ describe EnvironmentSerializer do
expect(json).to be_an_instance_of Array
end
end
+
+ context 'when representing environments within folders' do
+ let(:serializer) do
+ described_class.new(project: project).within_folders
+ end
+
+ let(:resource) { Environment.all }
+
+ subject { serializer.represent(resource) }
+
+ context 'when there is a single environment' do
+ before { create(:environment, name: 'staging') }
+
+ it 'represents one standalone environment' do
+ expect(subject.count).to eq 1
+ expect(subject.first[:name]).to eq 'staging'
+ expect(subject.first[:size]).to eq 1
+ expect(subject.first[:latest][:name]).to eq 'staging'
+ end
+ end
+
+ context 'when there are multiple environments in folder' do
+ before do
+ create(:environment, name: 'staging/my-review-1')
+ create(:environment, name: 'staging/my-review-2')
+ end
+
+ it 'represents one item that is a folder' do
+ expect(subject.count).to eq 1
+ expect(subject.first[:name]).to eq 'staging'
+ expect(subject.first[:size]).to eq 2
+ expect(subject.first[:latest][:name]).to eq 'staging/my-review-2'
+ expect(subject.first[:latest][:environment_type]).to eq 'staging'
+ end
+ end
+
+ context 'when there are multiple folders and standalone environments' do
+ before do
+ create(:environment, name: 'staging/my-review-1')
+ create(:environment, name: 'staging/my-review-2')
+ create(:environment, name: 'production/my-review-3')
+ create(:environment, name: 'testing')
+ end
+
+ it 'represents multiple items grouped within folders' do
+ expect(subject.count).to eq 3
+
+ expect(subject.first[:name]).to eq 'production'
+ expect(subject.first[:size]).to eq 1
+ expect(subject.first[:latest][:name]).to eq 'production/my-review-3'
+ expect(subject.first[:latest][:environment_type]).to eq 'production'
+ expect(subject.second[:name]).to eq 'staging'
+ expect(subject.second[:size]).to eq 2
+ expect(subject.second[:latest][:name]).to eq 'staging/my-review-2'
+ expect(subject.second[:latest][:environment_type]).to eq 'staging'
+ expect(subject.third[:name]).to eq 'testing'
+ expect(subject.third[:size]).to eq 1
+ expect(subject.third[:latest][:name]).to eq 'testing'
+ expect(subject.third[:latest][:environment_type]).to be_nil
+ end
+ end
+ end
+
+ context 'when used with pagination' do
+ let(:request) { spy('request') }
+ let(:response) { spy('response') }
+ let(:resource) { Environment.all }
+ let(:pagination) { { page: 1, per_page: 2 } }
+
+ let(:serializer) do
+ described_class.new(project: project)
+ .with_pagination(request, response)
+ end
+
+ before do
+ allow(request).to receive(:query_parameters)
+ .and_return(pagination)
+ end
+
+ subject { serializer.represent(resource) }
+
+ it 'creates a paginated serializer' do
+ expect(serializer).to be_paginated
+ end
+
+ context 'when resource is paginatable relation' do
+ context 'when there is a single environment object in relation' do
+ before { create(:environment) }
+
+ it 'serializes environments' do
+ expect(subject.first).to have_key :id
+ end
+ end
+
+ context 'when multiple environment objects are serialized' do
+ before { create_list(:environment, 3) }
+
+ it 'serializes appropriate number of objects' do
+ expect(subject.count).to be 2
+ end
+
+ it 'appends relevant headers' do
+ expect(response).to receive(:[]=).with('X-Total', '3')
+ expect(response).to receive(:[]=).with('X-Total-Pages', '2')
+ expect(response).to receive(:[]=).with('X-Per-Page', '2')
+
+ subject
+ end
+ end
+
+ context 'when grouping environments within folders' do
+ let(:serializer) do
+ described_class.new(project: project)
+ .with_pagination(request, response)
+ .within_folders
+ end
+
+ before do
+ create(:environment, name: 'staging/review-1')
+ create(:environment, name: 'staging/review-2')
+ create(:environment, name: 'production/deploy-3')
+ create(:environment, name: 'testing')
+ end
+
+ it 'paginates grouped items including ordering' do
+ expect(subject.count).to eq 2
+ expect(subject.first[:name]).to eq 'production'
+ expect(subject.second[:name]).to eq 'staging'
+ end
+ end
+ end
+ end
end
diff --git a/spec/serializers/pipeline_entity_spec.rb b/spec/serializers/pipeline_entity_spec.rb
new file mode 100644
index 00000000000..ccb72973f9c
--- /dev/null
+++ b/spec/serializers/pipeline_entity_spec.rb
@@ -0,0 +1,150 @@
+require 'spec_helper'
+
+describe PipelineEntity do
+ let(:user) { create(:user) }
+ let(:request) { double('request') }
+
+ before do
+ allow(request).to receive(:user).and_return(user)
+ end
+
+ let(:entity) do
+ described_class.represent(pipeline, request: request)
+ end
+
+ describe '#as_json' do
+ subject { entity.as_json }
+
+ context 'when pipeline is empty' do
+ let(:pipeline) { create(:ci_empty_pipeline) }
+
+ it 'contains required fields' do
+ expect(subject).to include :id, :user, :path
+ expect(subject).to include :ref, :commit
+ expect(subject).to include :updated_at, :created_at
+ end
+
+ it 'contains details' do
+ expect(subject).to include :details
+ expect(subject[:details])
+ .to include :duration, :finished_at
+ expect(subject[:details])
+ .to include :stages, :artifacts, :manual_actions
+ expect(subject[:details][:status]).to include :icon, :text, :label
+ end
+
+ it 'contains flags' do
+ expect(subject).to include :flags
+ expect(subject[:flags])
+ .to include :latest, :triggered, :stuck,
+ :yaml_errors, :retryable, :cancelable
+ end
+ end
+
+ context 'when pipeline is retryable' do
+ let(:project) { create(:empty_project) }
+
+ let(:pipeline) do
+ create(:ci_pipeline, status: :success, project: project)
+ end
+
+ before do
+ create(:ci_build, :failed, pipeline: pipeline)
+ end
+
+ context 'user has ability to retry pipeline' do
+ before { project.team << [user, :developer] }
+
+ it 'retryable flag is true' do
+ expect(subject[:flags][:retryable]).to eq true
+ end
+
+ it 'contains retry path' do
+ expect(subject[:retry_path]).to be_present
+ end
+ end
+
+ context 'user does not have ability to retry pipeline' do
+ it 'retryable flag is false' do
+ expect(subject[:flags][:retryable]).to eq false
+ end
+
+ it 'does not contain retry path' do
+ expect(subject).not_to have_key(:retry_path)
+ end
+ end
+ end
+
+ context 'when pipeline is cancelable' do
+ let(:project) { create(:empty_project) }
+
+ let(:pipeline) do
+ create(:ci_pipeline, status: :running, project: project)
+ end
+
+ before do
+ create(:ci_build, :pending, pipeline: pipeline)
+ end
+
+ context 'user has ability to cancel pipeline' do
+ before { project.team << [user, :developer] }
+
+ it 'cancelable flag is true' do
+ expect(subject[:flags][:cancelable]).to eq true
+ end
+
+ it 'contains cancel path' do
+ expect(subject[:cancel_path]).to be_present
+ end
+ end
+
+ context 'user does not have ability to cancel pipeline' do
+ it 'cancelable flag is false' do
+ expect(subject[:flags][:cancelable]).to eq false
+ end
+
+ it 'does not contain cancel path' do
+ expect(subject).not_to have_key(:cancel_path)
+ end
+ end
+ end
+
+ context 'when pipeline has YAML errors' do
+ let(:pipeline) do
+ create(:ci_pipeline, config: { rspec: { invalid: :value } })
+ end
+
+ it 'contains flag that indicates there are errors' do
+ expect(subject[:flags][:yaml_errors]).to be true
+ end
+
+ it 'contains information about error' do
+ expect(subject[:yaml_errors]).to be_present
+ end
+ end
+
+ context 'when pipeline does not have YAML errors' do
+ let(:pipeline) { create(:ci_empty_pipeline) }
+
+ it 'contains flag that indicates there are no errors' do
+ expect(subject[:flags][:yaml_errors]).to be false
+ end
+
+ it 'does not contain field that normally holds an error' do
+ expect(subject).not_to have_key(:yaml_errors)
+ end
+ end
+
+ context 'when pipeline ref is empty' do
+ let(:pipeline) { create(:ci_empty_pipeline) }
+
+ before do
+ allow(pipeline).to receive(:ref).and_return(nil)
+ end
+
+ it 'does not generate branch path' do
+ expect(subject[:ref][:path]).to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/serializers/pipeline_serializer_spec.rb b/spec/serializers/pipeline_serializer_spec.rb
new file mode 100644
index 00000000000..2aaef03cb93
--- /dev/null
+++ b/spec/serializers/pipeline_serializer_spec.rb
@@ -0,0 +1,97 @@
+require 'spec_helper'
+
+describe PipelineSerializer do
+ let(:user) { create(:user) }
+
+ let(:serializer) do
+ described_class.new(user: user)
+ end
+
+ subject { serializer.represent(resource) }
+
+ describe '#represent' do
+ context 'when used without pagination' do
+ it 'created a not paginated serializer' do
+ expect(serializer).not_to be_paginated
+ end
+
+ context 'when a single object is being serialized' do
+ let(:resource) { create(:ci_empty_pipeline) }
+
+ it 'serializers the pipeline object' do
+ expect(subject[:id]).to eq resource.id
+ end
+ end
+
+ context 'when multiple objects are being serialized' do
+ let(:resource) { create_list(:ci_pipeline, 2) }
+
+ it 'serializers the array of pipelines' do
+ expect(subject).not_to be_empty
+ end
+ end
+ end
+
+ context 'when used with pagination' do
+ let(:request) { spy('request') }
+ let(:response) { spy('response') }
+ let(:pagination) { {} }
+
+ before do
+ allow(request)
+ .to receive(:query_parameters)
+ .and_return(pagination)
+ end
+
+ let(:serializer) do
+ described_class.new(user: user)
+ .with_pagination(request, response)
+ end
+
+ it 'created a paginated serializer' do
+ expect(serializer).to be_paginated
+ end
+
+ context 'when resource is not paginatable' do
+ context 'when a single pipeline object is being serialized' do
+ let(:resource) { create(:ci_empty_pipeline) }
+ let(:pagination) { { page: 1, per_page: 1 } }
+
+ it 'raises error' do
+ expect { subject }.to raise_error(
+ Gitlab::Serializer::Pagination::InvalidResourceError)
+ end
+ end
+ end
+
+ context 'when resource is paginatable relation' do
+ let(:resource) { Ci::Pipeline.all }
+ let(:pagination) { { page: 1, per_page: 2 } }
+
+ context 'when a single pipeline object is present in relation' do
+ before { create(:ci_empty_pipeline) }
+
+ it 'serializes pipeline relation' do
+ expect(subject.first).to have_key :id
+ end
+ end
+
+ context 'when a multiple pipeline objects are being serialized' do
+ before { create_list(:ci_empty_pipeline, 3) }
+
+ it 'serializes appropriate number of objects' do
+ expect(subject.count).to be 2
+ end
+
+ it 'appends relevant headers' do
+ expect(response).to receive(:[]=).with('X-Total', '3')
+ expect(response).to receive(:[]=).with('X-Total-Pages', '2')
+ expect(response).to receive(:[]=).with('X-Per-Page', '2')
+
+ subject
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/serializers/request_aware_entity_spec.rb b/spec/serializers/request_aware_entity_spec.rb
new file mode 100644
index 00000000000..aa666b961dc
--- /dev/null
+++ b/spec/serializers/request_aware_entity_spec.rb
@@ -0,0 +1,22 @@
+require 'spec_helper'
+
+describe RequestAwareEntity do
+ subject do
+ Class.new.include(described_class).new
+ end
+
+ it 'includes URL helpers' do
+ expect(subject).to respond_to(:namespace_project_path)
+ end
+
+ it 'includes method for checking abilities' do
+ expect(subject).to respond_to(:can?)
+ end
+
+ it 'fetches request from options' do
+ expect(subject).to receive(:options)
+ .and_return({ request: 'some value' })
+
+ expect(subject.request).to eq 'some value'
+ end
+end
diff --git a/spec/serializers/stage_entity_spec.rb b/spec/serializers/stage_entity_spec.rb
new file mode 100644
index 00000000000..4ab40d08432
--- /dev/null
+++ b/spec/serializers/stage_entity_spec.rb
@@ -0,0 +1,51 @@
+require 'spec_helper'
+
+describe StageEntity do
+ let(:pipeline) { create(:ci_pipeline) }
+ let(:request) { double('request') }
+ let(:user) { create(:user) }
+
+ let(:entity) do
+ described_class.new(stage, request: request)
+ end
+
+ let(:stage) do
+ build(:ci_stage, pipeline: pipeline, name: 'test')
+ end
+
+ before do
+ allow(request).to receive(:user).and_return(user)
+ create(:ci_build, :success, pipeline: pipeline)
+ end
+
+ describe '#as_json' do
+ subject { entity.as_json }
+
+ it 'contains relevant fields' do
+ expect(subject).to include :name, :status, :path
+ end
+
+ it 'contains detailed status' do
+ expect(subject[:status]).to include :text, :label, :group, :icon
+ expect(subject[:status][:label]).to eq 'passed'
+ end
+
+ it 'contains valid name' do
+ expect(subject[:name]).to eq 'test'
+ end
+
+ it 'contains path to the stage' do
+ expect(subject[:path])
+ .to include "pipelines/#{pipeline.id}##{stage.name}"
+ end
+
+ it 'contains path to the stage dropdown' do
+ expect(subject[:dropdown_path])
+ .to include "pipelines/#{pipeline.id}/stage.json?stage=test"
+ end
+
+ it 'contains stage title' do
+ expect(subject[:title]).to eq 'test: passed'
+ end
+ end
+end
diff --git a/spec/serializers/status_entity_spec.rb b/spec/serializers/status_entity_spec.rb
new file mode 100644
index 00000000000..89428b4216e
--- /dev/null
+++ b/spec/serializers/status_entity_spec.rb
@@ -0,0 +1,23 @@
+require 'spec_helper'
+
+describe StatusEntity do
+ let(:entity) { described_class.new(status) }
+
+ let(:status) do
+ Gitlab::Ci::Status::Success.new(double('object'), double('user'))
+ end
+
+ before do
+ allow(status).to receive(:has_details?).and_return(true)
+ allow(status).to receive(:details_path).and_return('some/path')
+ end
+
+ describe '#as_json' do
+ subject { entity.as_json }
+
+ it 'contains status details' do
+ expect(subject).to include :text, :icon, :label, :group
+ expect(subject).to include :has_details, :details_path
+ end
+ end
+end
diff --git a/spec/services/boards/create_service_spec.rb b/spec/services/boards/create_service_spec.rb
index fde807cc410..7b29b043296 100644
--- a/spec/services/boards/create_service_spec.rb
+++ b/spec/services/boards/create_service_spec.rb
@@ -11,12 +11,11 @@ describe Boards::CreateService, services: true do
expect { service.execute }.to change(Board, :count).by(1)
end
- it 'creates default lists' do
+ it 'creates the default lists' do
board = service.execute
- expect(board.lists.size).to eq 2
- expect(board.lists.first).to be_backlog
- expect(board.lists.last).to be_done
+ expect(board.lists.size).to eq 1
+ expect(board.lists.first).to be_done
end
end
diff --git a/spec/services/boards/issues/list_service_spec.rb b/spec/services/boards/issues/list_service_spec.rb
index 7c206cf3ce7..305278843f5 100644
--- a/spec/services/boards/issues/list_service_spec.rb
+++ b/spec/services/boards/issues/list_service_spec.rb
@@ -13,7 +13,6 @@ describe Boards::Issues::ListService, services: true do
let(:p2) { create(:label, title: 'P2', project: project, priority: 2) }
let(:p3) { create(:label, title: 'P3', project: project, priority: 3) }
- let!(:backlog) { create(:backlog_list, board: board) }
let!(:list1) { create(:list, board: board, label: development, position: 0) }
let!(:list2) { create(:list, board: board, label: testing, position: 1) }
let!(:done) { create(:done_list, board: board) }
@@ -45,8 +44,8 @@ describe Boards::Issues::ListService, services: true do
end
context 'sets default order to priority' do
- it 'returns opened issues when listing issues from Backlog' do
- params = { board_id: board.id, id: backlog.id }
+ it 'returns opened issues when list id is missing' do
+ params = { board_id: board.id }
issues = described_class.new(project, user, params).execute
diff --git a/spec/services/boards/issues/move_service_spec.rb b/spec/services/boards/issues/move_service_spec.rb
index c43b2aec490..77f75167b3d 100644
--- a/spec/services/boards/issues/move_service_spec.rb
+++ b/spec/services/boards/issues/move_service_spec.rb
@@ -10,7 +10,6 @@ describe Boards::Issues::MoveService, services: true do
let(:development) { create(:label, project: project, name: 'Development') }
let(:testing) { create(:label, project: project, name: 'Testing') }
- let!(:backlog) { create(:backlog_list, board: board1) }
let!(:list1) { create(:list, board: board1, label: development, position: 0) }
let!(:list2) { create(:list, board: board1, label: testing, position: 1) }
let!(:done) { create(:done_list, board: board1) }
@@ -19,41 +18,6 @@ describe Boards::Issues::MoveService, services: true do
project.team << [user, :developer]
end
- context 'when moving from backlog' do
- it 'adds the label of the list it goes to' do
- issue = create(:labeled_issue, project: project, labels: [bug])
- params = { board_id: board1.id, from_list_id: backlog.id, to_list_id: list1.id }
-
- described_class.new(project, user, params).execute(issue)
-
- expect(issue.reload.labels).to contain_exactly(bug, development)
- end
- end
-
- context 'when moving to backlog' do
- it 'removes all list-labels' do
- issue = create(:labeled_issue, project: project, labels: [bug, development, testing])
- params = { board_id: board1.id, from_list_id: list1.id, to_list_id: backlog.id }
-
- described_class.new(project, user, params).execute(issue)
-
- expect(issue.reload.labels).to contain_exactly(bug)
- end
- end
-
- context 'when moving from backlog to done' do
- it 'closes the issue' do
- issue = create(:labeled_issue, project: project, labels: [bug])
- params = { board_id: board1.id, from_list_id: backlog.id, to_list_id: done.id }
-
- described_class.new(project, user, params).execute(issue)
- issue.reload
-
- expect(issue.labels).to contain_exactly(bug)
- expect(issue).to be_closed
- end
- end
-
context 'when moving an issue between lists' do
let(:issue) { create(:labeled_issue, project: project, labels: [bug, development]) }
let(:params) { { board_id: board1.id, from_list_id: list1.id, to_list_id: list2.id } }
@@ -113,19 +77,6 @@ describe Boards::Issues::MoveService, services: true do
end
end
- context 'when moving from done to backlog' do
- it 'reopens the issue' do
- issue = create(:labeled_issue, :closed, project: project, labels: [bug])
- params = { board_id: board1.id, from_list_id: done.id, to_list_id: backlog.id }
-
- described_class.new(project, user, params).execute(issue)
- issue.reload
-
- expect(issue.labels).to contain_exactly(bug)
- expect(issue).to be_reopened
- end
- end
-
context 'when moving to same list' do
let(:issue) { create(:labeled_issue, project: project, labels: [bug, development]) }
let(:params) { { board_id: board1.id, from_list_id: list1.id, to_list_id: list1.id } }
diff --git a/spec/services/boards/lists/create_service_spec.rb b/spec/services/boards/lists/create_service_spec.rb
index a7e9efcf93f..ebac38e68f1 100644
--- a/spec/services/boards/lists/create_service_spec.rb
+++ b/spec/services/boards/lists/create_service_spec.rb
@@ -21,7 +21,7 @@ describe Boards::Lists::CreateService, services: true do
end
end
- context 'when board lists has backlog, and done lists' do
+ context 'when board lists has the done list' do
it 'creates a new list at beginning of the list' do
list = service.execute(board)
@@ -40,7 +40,7 @@ describe Boards::Lists::CreateService, services: true do
end
end
- context 'when board lists has backlog, label and done lists' do
+ context 'when board lists has label and done lists' do
it 'creates a new list at end of the label lists' do
list1 = create(:list, board: board, position: 0)
diff --git a/spec/services/boards/lists/destroy_service_spec.rb b/spec/services/boards/lists/destroy_service_spec.rb
index 628caf03476..a30860f828a 100644
--- a/spec/services/boards/lists/destroy_service_spec.rb
+++ b/spec/services/boards/lists/destroy_service_spec.rb
@@ -15,7 +15,6 @@ describe Boards::Lists::DestroyService, services: true do
end
it 'decrements position of higher lists' do
- backlog = board.backlog_list
development = create(:list, board: board, position: 0)
review = create(:list, board: board, position: 1)
staging = create(:list, board: board, position: 2)
@@ -23,20 +22,12 @@ describe Boards::Lists::DestroyService, services: true do
described_class.new(project, user).execute(development)
- expect(backlog.reload.position).to be_nil
expect(review.reload.position).to eq 0
expect(staging.reload.position).to eq 1
expect(done.reload.position).to be_nil
end
end
- it 'does not remove list from board when list type is backlog' do
- list = board.backlog_list
- service = described_class.new(project, user)
-
- expect { service.execute(list) }.not_to change(board.lists, :count)
- end
-
it 'does not remove list from board when list type is done' do
list = board.done_list
service = described_class.new(project, user)
diff --git a/spec/services/boards/lists/list_service_spec.rb b/spec/services/boards/lists/list_service_spec.rb
index 334cee3f06d..2dffc62b215 100644
--- a/spec/services/boards/lists/list_service_spec.rb
+++ b/spec/services/boards/lists/list_service_spec.rb
@@ -10,7 +10,7 @@ describe Boards::Lists::ListService, services: true do
service = described_class.new(project, double)
- expect(service.execute(board)).to eq [board.backlog_list, list, board.done_list]
+ expect(service.execute(board)).to eq [list, board.done_list]
end
end
end
diff --git a/spec/services/boards/lists/move_service_spec.rb b/spec/services/boards/lists/move_service_spec.rb
index 63fa0bb8c5f..3786dc82bf0 100644
--- a/spec/services/boards/lists/move_service_spec.rb
+++ b/spec/services/boards/lists/move_service_spec.rb
@@ -6,7 +6,6 @@ describe Boards::Lists::MoveService, services: true do
let(:board) { create(:board, project: project) }
let(:user) { create(:user) }
- let!(:backlog) { create(:backlog_list, board: board) }
let!(:planning) { create(:list, board: board, position: 0) }
let!(:development) { create(:list, board: board, position: 1) }
let!(:review) { create(:list, board: board, position: 2) }
@@ -87,14 +86,6 @@ describe Boards::Lists::MoveService, services: true do
end
end
- it 'keeps position of lists when list type is backlog' do
- service = described_class.new(project, user, position: 2)
-
- service.execute(backlog)
-
- expect(current_list_positions).to eq [0, 1, 2, 3]
- end
-
it 'keeps position of lists when list type is done' do
service = described_class.new(project, user, position: 2)
diff --git a/spec/services/ci/register_build_service_spec.rb b/spec/services/ci/register_build_service_spec.rb
index a3fc23ba177..d9f774a1095 100644
--- a/spec/services/ci/register_build_service_spec.rb
+++ b/spec/services/ci/register_build_service_spec.rb
@@ -2,7 +2,6 @@ require 'spec_helper'
module Ci
describe RegisterBuildService, services: true do
- let!(:service) { RegisterBuildService.new }
let!(:project) { FactoryGirl.create :empty_project, shared_runners_enabled: false }
let!(:pipeline) { FactoryGirl.create :ci_pipeline, project: project }
let!(:pending_build) { FactoryGirl.create :ci_build, pipeline: pipeline }
@@ -19,29 +18,29 @@ module Ci
pending_build.tag_list = ["linux"]
pending_build.save
specific_runner.tag_list = ["linux"]
- expect(service.execute(specific_runner)).to eq(pending_build)
+ expect(execute(specific_runner)).to eq(pending_build)
end
it "does not pick build with different tag" do
pending_build.tag_list = ["linux"]
pending_build.save
specific_runner.tag_list = ["win32"]
- expect(service.execute(specific_runner)).to be_falsey
+ expect(execute(specific_runner)).to be_falsey
end
it "picks build without tag" do
- expect(service.execute(specific_runner)).to eq(pending_build)
+ expect(execute(specific_runner)).to eq(pending_build)
end
it "does not pick build with tag" do
pending_build.tag_list = ["linux"]
pending_build.save
- expect(service.execute(specific_runner)).to be_falsey
+ expect(execute(specific_runner)).to be_falsey
end
it "pick build without tag" do
specific_runner.tag_list = ["win32"]
- expect(service.execute(specific_runner)).to eq(pending_build)
+ expect(execute(specific_runner)).to eq(pending_build)
end
end
@@ -56,13 +55,13 @@ module Ci
end
it 'does not pick a build' do
- expect(service.execute(shared_runner)).to be_nil
+ expect(execute(shared_runner)).to be_nil
end
end
context 'for specific runner' do
it 'does not pick a build' do
- expect(service.execute(specific_runner)).to be_nil
+ expect(execute(specific_runner)).to be_nil
end
end
end
@@ -86,34 +85,34 @@ module Ci
it 'prefers projects without builds first' do
# it gets for one build from each of the projects
- expect(service.execute(shared_runner)).to eq(build1_project1)
- expect(service.execute(shared_runner)).to eq(build1_project2)
- expect(service.execute(shared_runner)).to eq(build1_project3)
+ expect(execute(shared_runner)).to eq(build1_project1)
+ expect(execute(shared_runner)).to eq(build1_project2)
+ expect(execute(shared_runner)).to eq(build1_project3)
# then it gets a second build from each of the projects
- expect(service.execute(shared_runner)).to eq(build2_project1)
- expect(service.execute(shared_runner)).to eq(build2_project2)
+ expect(execute(shared_runner)).to eq(build2_project1)
+ expect(execute(shared_runner)).to eq(build2_project2)
# in the end the third build
- expect(service.execute(shared_runner)).to eq(build3_project1)
+ expect(execute(shared_runner)).to eq(build3_project1)
end
it 'equalises number of running builds' do
# after finishing the first build for project 1, get a second build from the same project
- expect(service.execute(shared_runner)).to eq(build1_project1)
+ expect(execute(shared_runner)).to eq(build1_project1)
build1_project1.reload.success
- expect(service.execute(shared_runner)).to eq(build2_project1)
+ expect(execute(shared_runner)).to eq(build2_project1)
- expect(service.execute(shared_runner)).to eq(build1_project2)
+ expect(execute(shared_runner)).to eq(build1_project2)
build1_project2.reload.success
- expect(service.execute(shared_runner)).to eq(build2_project2)
- expect(service.execute(shared_runner)).to eq(build1_project3)
- expect(service.execute(shared_runner)).to eq(build3_project1)
+ expect(execute(shared_runner)).to eq(build2_project2)
+ expect(execute(shared_runner)).to eq(build1_project3)
+ expect(execute(shared_runner)).to eq(build3_project1)
end
end
context 'shared runner' do
- let(:build) { service.execute(shared_runner) }
+ let(:build) { execute(shared_runner) }
it { expect(build).to be_kind_of(Build) }
it { expect(build).to be_valid }
@@ -122,7 +121,7 @@ module Ci
end
context 'specific runner' do
- let(:build) { service.execute(specific_runner) }
+ let(:build) { execute(specific_runner) }
it { expect(build).to be_kind_of(Build) }
it { expect(build).to be_valid }
@@ -137,13 +136,13 @@ module Ci
end
context 'shared runner' do
- let(:build) { service.execute(shared_runner) }
+ let(:build) { execute(shared_runner) }
it { expect(build).to be_nil }
end
context 'specific runner' do
- let(:build) { service.execute(specific_runner) }
+ let(:build) { execute(specific_runner) }
it { expect(build).to be_kind_of(Build) }
it { expect(build).to be_valid }
@@ -159,17 +158,21 @@ module Ci
end
context 'and uses shared runner' do
- let(:build) { service.execute(shared_runner) }
+ let(:build) { execute(shared_runner) }
it { expect(build).to be_nil }
end
context 'and uses specific runner' do
- let(:build) { service.execute(specific_runner) }
+ let(:build) { execute(specific_runner) }
it { expect(build).to be_nil }
end
end
+
+ def execute(runner)
+ described_class.new(runner).execute.build
+ end
end
end
end
diff --git a/spec/services/ci/stop_environments_service_spec.rb b/spec/services/ci/stop_environments_service_spec.rb
index 6f7d1a5d28d..560f83d94f7 100644
--- a/spec/services/ci/stop_environments_service_spec.rb
+++ b/spec/services/ci/stop_environments_service_spec.rb
@@ -42,10 +42,10 @@ describe Ci::StopEnvironmentsService, services: true do
end
end
- context 'when environment is not stoppable' do
+ context 'when environment is not stopped' do
before do
allow_any_instance_of(Environment)
- .to receive(:stoppable?).and_return(false)
+ .to receive(:state).and_return(:stopped)
end
it 'does not stop environment' do
diff --git a/spec/services/ci/update_build_queue_service_spec.rb b/spec/services/ci/update_build_queue_service_spec.rb
new file mode 100644
index 00000000000..f01a388b895
--- /dev/null
+++ b/spec/services/ci/update_build_queue_service_spec.rb
@@ -0,0 +1,47 @@
+require 'spec_helper'
+
+describe Ci::UpdateBuildQueueService, :services do
+ let(:project) { create(:project) }
+ let(:build) { create(:ci_build, pipeline: pipeline) }
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+
+ context 'when updating specific runners' do
+ let(:runner) { create(:ci_runner) }
+
+ context 'when there are runner that can pick build' do
+ before { build.project.runners << runner }
+
+ it 'ticks runner queue value' do
+ expect { subject.execute(build) }
+ .to change { runner.ensure_runner_queue_value }
+ end
+ end
+
+ context 'when there are no runners that can pick build' do
+ it 'does not tick runner queue value' do
+ expect { subject.execute(build) }
+ .not_to change { runner.ensure_runner_queue_value }
+ end
+ end
+ end
+
+ context 'when updating shared runners' do
+ let(:runner) { create(:ci_runner, :shared) }
+
+ context 'when there are runner that can pick build' do
+ it 'ticks runner queue value' do
+ expect { subject.execute(build) }
+ .to change { runner.ensure_runner_queue_value }
+ end
+ end
+
+ context 'when there are no runners that can pick build' do
+ before { build.tag_list = [:docker] }
+
+ it 'does not tick runner queue value' do
+ expect { subject.execute(build) }
+ .not_to change { runner.ensure_runner_queue_value }
+ end
+ end
+ end
+end
diff --git a/spec/services/compare_service_spec.rb b/spec/services/compare_service_spec.rb
index 3760f19aaa2..0a7fc58523f 100644
--- a/spec/services/compare_service_spec.rb
+++ b/spec/services/compare_service_spec.rb
@@ -3,17 +3,17 @@ require 'spec_helper'
describe CompareService, services: true do
let(:project) { create(:project) }
let(:user) { create(:user) }
- let(:service) { described_class.new }
+ let(:service) { described_class.new(project, 'feature') }
describe '#execute' do
context 'compare with base, like feature...fix' do
- subject { service.execute(project, 'feature', project, 'fix', straight: false) }
+ subject { service.execute(project, 'fix', straight: false) }
it { expect(subject.diffs.size).to eq(1) }
end
context 'straight compare, like feature..fix' do
- subject { service.execute(project, 'feature', project, 'fix', straight: true) }
+ subject { service.execute(project, 'fix', straight: true) }
it { expect(subject.diffs.size).to eq(3) }
end
diff --git a/spec/services/event_create_service_spec.rb b/spec/services/event_create_service_spec.rb
index b7dc99ed887..f2c2009bcbf 100644
--- a/spec/services/event_create_service_spec.rb
+++ b/spec/services/event_create_service_spec.rb
@@ -9,7 +9,7 @@ describe EventCreateService, services: true do
it { expect(service.open_issue(issue, issue.author)).to be_truthy }
- it "should create new event" do
+ it "creates new event" do
expect { service.open_issue(issue, issue.author) }.to change { Event.count }
end
end
@@ -19,7 +19,7 @@ describe EventCreateService, services: true do
it { expect(service.close_issue(issue, issue.author)).to be_truthy }
- it "should create new event" do
+ it "creates new event" do
expect { service.close_issue(issue, issue.author) }.to change { Event.count }
end
end
@@ -29,7 +29,7 @@ describe EventCreateService, services: true do
it { expect(service.reopen_issue(issue, issue.author)).to be_truthy }
- it "should create new event" do
+ it "creates new event" do
expect { service.reopen_issue(issue, issue.author) }.to change { Event.count }
end
end
diff --git a/spec/services/files/update_service_spec.rb b/spec/services/files/update_service_spec.rb
index d3c37c7820f..35e6e139238 100644
--- a/spec/services/files/update_service_spec.rb
+++ b/spec/services/files/update_service_spec.rb
@@ -6,7 +6,10 @@ describe Files::UpdateService do
let(:project) { create(:project) }
let(:user) { create(:user) }
let(:file_path) { 'files/ruby/popen.rb' }
- let(:new_contents) { "New Content" }
+ let(:new_contents) { 'New Content' }
+ let(:target_branch) { project.default_branch }
+ let(:last_commit_sha) { nil }
+
let(:commit_params) do
{
file_path: file_path,
@@ -14,9 +17,9 @@ describe Files::UpdateService do
file_content: new_contents,
file_content_encoding: "text",
last_commit_sha: last_commit_sha,
- source_project: project,
- source_branch: project.default_branch,
- target_branch: project.default_branch,
+ start_project: project,
+ start_branch: project.default_branch,
+ target_branch: target_branch
}
end
@@ -54,18 +57,6 @@ describe Files::UpdateService do
end
context "when the last_commit_sha is not supplied" do
- let(:commit_params) do
- {
- file_path: file_path,
- commit_message: "Update File",
- file_content: new_contents,
- file_content_encoding: "text",
- source_project: project,
- source_branch: project.default_branch,
- target_branch: project.default_branch,
- }
- end
-
it "returns a hash with the :success status " do
results = subject.execute
@@ -80,5 +71,15 @@ describe Files::UpdateService do
expect(results.data).to eq(new_contents)
end
end
+
+ context 'when target branch is different than source branch' do
+ let(:target_branch) { "#{project.default_branch}-new" }
+
+ it 'fires hooks only once' do
+ expect(GitHooksService).to receive(:new).once.and_call_original
+
+ subject.execute
+ end
+ end
end
end
diff --git a/spec/services/git_hooks_service_spec.rb b/spec/services/git_hooks_service_spec.rb
index 41b0968b8b4..3318dfb22b6 100644
--- a/spec/services/git_hooks_service_spec.rb
+++ b/spec/services/git_hooks_service_spec.rb
@@ -21,7 +21,7 @@ describe GitHooksService, services: true do
hook = double(trigger: [true, nil])
expect(Gitlab::Git::Hook).to receive(:new).exactly(3).times.and_return(hook)
- expect(service.execute(user, @repo_path, @blankrev, @newrev, @ref) { }).to eq([true, nil])
+ service.execute(user, @repo_path, @blankrev, @newrev, @ref) { }
end
end
diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb
index e7624e70725..2a0f00ce937 100644
--- a/spec/services/git_push_service_spec.rb
+++ b/spec/services/git_push_service_spec.rb
@@ -583,7 +583,7 @@ describe GitPushService, services: true do
service.push_commits = [commit]
expect(ProjectCacheWorker).to receive(:perform_async).
- with(project.id, %i(readme))
+ with(project.id, %i(readme), %i(commit_count repository_size))
service.update_caches
end
@@ -596,7 +596,7 @@ describe GitPushService, services: true do
it 'does not flush any conditional caches' do
expect(ProjectCacheWorker).to receive(:perform_async).
- with(project.id, []).
+ with(project.id, [], %i(commit_count repository_size)).
and_call_original
service.update_caches
@@ -604,6 +604,25 @@ describe GitPushService, services: true do
end
end
+ describe '#process_commit_messages' do
+ let(:service) do
+ described_class.new(project,
+ user,
+ oldrev: sample_commit.parent_id,
+ newrev: sample_commit.id,
+ ref: 'refs/heads/master')
+ end
+
+ it 'only schedules a limited number of commits' do
+ allow(service).to receive(:push_commits).
+ and_return(Array.new(1000, double(:commit, to_hash: {})))
+
+ expect(ProcessCommitWorker).to receive(:perform_async).exactly(100).times
+
+ service.process_commit_messages
+ end
+ end
+
def execute_service(project, user, oldrev, newrev, ref)
service = described_class.new(project, user, oldrev: oldrev, newrev: newrev, ref: ref )
service.execute
diff --git a/spec/services/groups/create_service_spec.rb b/spec/services/groups/create_service_spec.rb
index 71a0b8e2a12..14717a7455d 100644
--- a/spec/services/groups/create_service_spec.rb
+++ b/spec/services/groups/create_service_spec.rb
@@ -1,11 +1,12 @@
require 'spec_helper'
-describe Groups::CreateService, services: true do
- let!(:user) { create(:user) }
+describe Groups::CreateService, '#execute', services: true do
+ let!(:user) { create(:user) }
let!(:group_params) { { path: "group_path", visibility_level: Gitlab::VisibilityLevel::PUBLIC } }
- describe "execute" do
- let!(:service) { described_class.new(user, group_params ) }
+ describe 'visibility level restrictions' do
+ let!(:service) { described_class.new(user, group_params) }
+
subject { service.execute }
context "create groups without restricted visibility level" do
@@ -14,7 +15,29 @@ describe Groups::CreateService, services: true do
context "cannot create group with restricted visibility level" do
before { allow_any_instance_of(ApplicationSetting).to receive(:restricted_visibility_levels).and_return([Gitlab::VisibilityLevel::PUBLIC]) }
+
it { is_expected.not_to be_persisted }
end
end
+
+ describe 'creating subgroup' do
+ let!(:group) { create(:group) }
+ let!(:service) { described_class.new(user, group_params.merge(parent_id: group.id)) }
+
+ subject { service.execute }
+
+ context 'as group owner' do
+ before { group.add_owner(user) }
+
+ it { is_expected.to be_persisted }
+ end
+
+ context 'as guest' do
+ it 'does not save group and returns an error' do
+ is_expected.not_to be_persisted
+ expect(subject.errors[:parent_id].first).to eq('manage access required to create subgroup')
+ expect(subject.parent_id).to be_nil
+ end
+ end
+ end
end
diff --git a/spec/services/groups/update_service_spec.rb b/spec/services/groups/update_service_spec.rb
index 9c2331144a0..7c0fccb9d41 100644
--- a/spec/services/groups/update_service_spec.rb
+++ b/spec/services/groups/update_service_spec.rb
@@ -1,15 +1,15 @@
require 'spec_helper'
describe Groups::UpdateService, services: true do
- let!(:user) { create(:user) }
- let!(:private_group) { create(:group, :private) }
- let!(:internal_group) { create(:group, :internal) }
- let!(:public_group) { create(:group, :public) }
+ let!(:user) { create(:user) }
+ let!(:private_group) { create(:group, :private) }
+ let!(:internal_group) { create(:group, :internal) }
+ let!(:public_group) { create(:group, :public) }
describe "#execute" do
context "project visibility_level validation" do
context "public group with public projects" do
- let!(:service) { described_class.new(public_group, user, visibility_level: Gitlab::VisibilityLevel::INTERNAL ) }
+ let!(:service) { described_class.new(public_group, user, visibility_level: Gitlab::VisibilityLevel::INTERNAL) }
before do
public_group.add_user(user, Gitlab::Access::MASTER)
@@ -23,7 +23,7 @@ describe Groups::UpdateService, services: true do
end
context "internal group with internal project" do
- let!(:service) { described_class.new(internal_group, user, visibility_level: Gitlab::VisibilityLevel::PRIVATE ) }
+ let!(:service) { described_class.new(internal_group, user, visibility_level: Gitlab::VisibilityLevel::PRIVATE) }
before do
internal_group.add_user(user, Gitlab::Access::MASTER)
@@ -39,7 +39,7 @@ describe Groups::UpdateService, services: true do
end
context "unauthorized visibility_level validation" do
- let!(:service) { described_class.new(internal_group, user, visibility_level: 99 ) }
+ let!(:service) { described_class.new(internal_group, user, visibility_level: 99) }
before do
internal_group.add_user(user, Gitlab::Access::MASTER)
end
@@ -49,4 +49,41 @@ describe Groups::UpdateService, services: true do
expect(internal_group.errors.count).to eq(1)
end
end
+
+ context 'rename group' do
+ let!(:service) { described_class.new(internal_group, user, path: SecureRandom.hex) }
+
+ before do
+ internal_group.add_user(user, Gitlab::Access::MASTER)
+ create(:project, :internal, group: internal_group)
+ end
+
+ it 'returns true' do
+ expect(service.execute).to eq(true)
+ end
+
+ context 'error moving group' do
+ before do
+ allow(internal_group).to receive(:move_dir).and_raise(Gitlab::UpdatePathError)
+ end
+
+ it 'does not raise an error' do
+ expect { service.execute }.not_to raise_error
+ end
+
+ it 'returns false' do
+ expect(service.execute).to eq(false)
+ end
+
+ it 'has the right error' do
+ service.execute
+
+ expect(internal_group.errors.full_messages.first).to eq('Gitlab::UpdatePathError')
+ end
+
+ it "hasn't changed the path" do
+ expect { service.execute}.not_to change { internal_group.reload.path}
+ end
+ end
+ end
end
diff --git a/spec/services/issuable/bulk_update_service_spec.rb b/spec/services/issuable/bulk_update_service_spec.rb
index 5f3020b6525..0475f38fe5e 100644
--- a/spec/services/issuable/bulk_update_service_spec.rb
+++ b/spec/services/issuable/bulk_update_service_spec.rb
@@ -52,7 +52,10 @@ describe Issuable::BulkUpdateService, services: true do
context 'when the new assignee ID is a valid user' do
it 'succeeds' do
- result = bulk_update(issue, assignee_id: create(:user).id)
+ new_assignee = create(:user)
+ project.team << [new_assignee, :developer]
+
+ result = bulk_update(issue, assignee_id: new_assignee.id)
expect(result[:success]).to be_truthy
expect(result[:count]).to eq(1)
@@ -60,15 +63,16 @@ describe Issuable::BulkUpdateService, services: true do
it 'updates the assignee to the use ID passed' do
assignee = create(:user)
+ project.team << [assignee, :developer]
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
- it 'unassigns the issues' do
- expect { bulk_update(issue, assignee_id: -1) }
+ context "when the new assignee ID is #{IssuableFinder::NONE}" do
+ it "unassigns the issues" do
+ expect { bulk_update(issue, assignee_id: IssuableFinder::NONE) }
.to change { issue.reload.assignee }.to(nil)
end
end
diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb
index 8bde61ee336..30578ee4c7d 100644
--- a/spec/services/issues/create_service_spec.rb
+++ b/spec/services/issues/create_service_spec.rb
@@ -135,6 +135,8 @@ describe Issues::CreateService, services: true do
end
end
+ it_behaves_like 'issuable create service'
+
it_behaves_like 'new issuable record that supports slash commands'
context 'for a merge request' do
@@ -179,5 +181,107 @@ describe Issues::CreateService, services: true do
expect(issue.title).to be_nil
end
end
+
+ context 'checking spam' do
+ let(:opts) do
+ {
+ title: 'Awesome issue',
+ description: 'please fix',
+ request: double(:request, env: {})
+ }
+ end
+
+ before do
+ allow_any_instance_of(SpamService).to receive(:check_for_spam?).and_return(true)
+ end
+
+ context 'when recaptcha was verified' do
+ let(:log_user) { user }
+ let(:spam_logs) { create_list(:spam_log, 2, user: log_user, title: 'Awesome issue') }
+
+ before do
+ opts[:recaptcha_verified] = true
+ opts[:spam_log_id] = spam_logs.last.id
+
+ expect(AkismetService).not_to receive(:new)
+ end
+
+ it 'does no mark an issue as a spam ' do
+ expect(issue).not_to be_spam
+ end
+
+ it 'an issue is valid ' do
+ expect(issue.valid?).to be_truthy
+ end
+
+ it 'does not assign a spam_log to an issue' do
+ expect(issue.spam_log).to be_nil
+ end
+
+ it 'marks related spam_log as recaptcha_verified' do
+ expect { issue }.to change{SpamLog.last.recaptcha_verified}.from(false).to(true)
+ end
+
+ context 'when spam log does not belong to a user' do
+ let(:log_user) { create(:user) }
+
+ it 'does not mark spam_log as recaptcha_verified' do
+ expect { issue }.not_to change{SpamLog.last.recaptcha_verified}
+ end
+ end
+
+ context 'when spam log title does not match the issue title' do
+ before do
+ opts[:title] = 'Another issue'
+ end
+
+ it 'does not mark spam_log as recaptcha_verified' do
+ expect { issue }.not_to change{SpamLog.last.recaptcha_verified}
+ end
+ end
+ end
+
+ context 'when recaptcha was not verified' do
+ context 'when akismet detects spam' do
+ before do
+ allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true)
+ end
+
+ it 'marks an issue as a spam ' do
+ expect(issue).to be_spam
+ end
+
+ it 'an issue is not valid ' do
+ expect(issue.valid?).to be_falsey
+ end
+
+ it 'creates a new spam_log' do
+ expect{issue}.to change{SpamLog.count}.from(0).to(1)
+ end
+
+ it 'assigns a spam_log to an issue' do
+ expect(issue.spam_log).to eq(SpamLog.last)
+ end
+ end
+
+ context 'when akismet does not detect spam' do
+ before do
+ allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(false)
+ end
+
+ it 'does not mark an issue as a spam ' do
+ expect(issue).not_to be_spam
+ end
+
+ it 'an issue is valid ' do
+ expect(issue.valid?).to be_truthy
+ end
+
+ it 'does not assign a spam_log to an issue' do
+ expect(issue.spam_log).to be_nil
+ end
+ end
+ end
+ end
end
end
diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb
index eafbea46905..d83b09fd32c 100644
--- a/spec/services/issues/update_service_spec.rb
+++ b/spec/services/issues/update_service_spec.rb
@@ -142,6 +142,17 @@ describe Issues::UpdateService, services: true do
update_issue(confidential: true)
end
+
+ it 'does not update assignee_id with unauthorized users' do
+ project.update(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
+ update_issue(confidential: true)
+ non_member = create(:user)
+ original_assignee = issue.assignee
+
+ update_issue(assignee_id: non_member.id)
+
+ expect(issue.reload.assignee_id).to eq(original_assignee.id)
+ end
end
context 'todos' do
diff --git a/spec/services/labels/promote_service_spec.rb b/spec/services/labels/promote_service_spec.rb
new file mode 100644
index 00000000000..4b90ad19640
--- /dev/null
+++ b/spec/services/labels/promote_service_spec.rb
@@ -0,0 +1,187 @@
+require 'spec_helper'
+
+describe Labels::PromoteService, services: true do
+ describe '#execute' do
+ let!(:user) { create(:user) }
+
+ context 'project without group' do
+ let!(:project_1) { create(:empty_project) }
+
+ let!(:project_label_1_1) { create(:label, project: project_1) }
+
+ subject(:service) { described_class.new(project_1, user) }
+
+ it 'fails on project without group' do
+ expect(service.execute(project_label_1_1)).to be_falsey
+ end
+ end
+
+ context 'project with group' do
+ let!(:promoted_label_name) { "Promoted Label" }
+ let!(:untouched_label_name) { "Untouched Label" }
+ let!(:promoted_description) { "Promoted Description" }
+ let!(:promoted_color) { "#0000FF" }
+ let!(:label_2_1_priority) { 1 }
+ let!(:label_3_1_priority) { 2 }
+
+ let!(:group_1) { create(:group) }
+ let!(:group_2) { create(:group) }
+
+ let!(:project_1) { create(:empty_project, namespace: group_1) }
+ let!(:project_2) { create(:empty_project, namespace: group_1) }
+ let!(:project_3) { create(:empty_project, namespace: group_1) }
+ let!(:project_4) { create(:empty_project, namespace: group_2) }
+
+ # Labels/issues can't be lazily created so we might as well eager initialize
+ # all other objects too since we use them inside
+ let!(:project_label_1_1) { create(:label, project: project_1, name: promoted_label_name, color: promoted_color, description: promoted_description) }
+ let!(:project_label_1_2) { create(:label, project: project_1, name: untouched_label_name) }
+ let!(:project_label_2_1) { create(:label, project: project_2, priority: label_2_1_priority, name: promoted_label_name, color: "#FF0000") }
+ let!(:project_label_3_1) { create(:label, project: project_3, priority: label_3_1_priority, name: promoted_label_name) }
+ let!(:project_label_3_2) { create(:label, project: project_3, priority: 1, name: untouched_label_name) }
+ let!(:project_label_4_1) { create(:label, project: project_4, name: promoted_label_name) }
+
+ let!(:issue_1_1) { create(:labeled_issue, project: project_1, labels: [project_label_1_1, project_label_1_2]) }
+ let!(:issue_1_2) { create(:labeled_issue, project: project_1, labels: [project_label_1_2]) }
+ let!(:issue_2_1) { create(:labeled_issue, project: project_2, labels: [project_label_2_1]) }
+ let!(:issue_4_1) { create(:labeled_issue, project: project_4, labels: [project_label_4_1]) }
+
+ let!(:merge_3_1) { create(:labeled_merge_request, source_project: project_3, target_project: project_3, labels: [project_label_3_1, project_label_3_2]) }
+
+ let!(:issue_board_2_1) { create(:board, project: project_2) }
+ let!(:issue_board_list_2_1) { create(:list, board: issue_board_2_1, label: project_label_2_1) }
+
+ let(:new_label) { group_1.labels.find_by(title: promoted_label_name) }
+
+ subject(:service) { described_class.new(project_1, user) }
+
+ it 'fails on group label' do
+ group_label = create(:group_label, group: group_1)
+
+ expect(service.execute(group_label)).to be_falsey
+ end
+
+ it 'is truthy on success' do
+ expect(service.execute(project_label_1_1)).to be_truthy
+ end
+
+ it 'recreates the label as a group label' do
+ expect { service.execute(project_label_1_1) }.
+ to change(project_1.labels, :count).by(-1).
+ and change(group_1.labels, :count).by(1)
+ expect(new_label).not_to be_nil
+ end
+
+ it 'copies title, description and color' do
+ service.execute(project_label_1_1)
+
+ expect(new_label.title).to eq(promoted_label_name)
+ expect(new_label.description).to eq(promoted_description)
+ expect(new_label.color).to eq(promoted_color)
+ end
+
+ it 'merges labels with the same name in group' do
+ expect { service.execute(project_label_1_1) }.to change(project_2.labels, :count).by(-1).and \
+ change(project_3.labels, :count).by(-1)
+ end
+
+ it 'recreates priorities' do
+ service.execute(project_label_1_1)
+
+ expect(new_label.priority(project_1)).to be_nil
+ expect(new_label.priority(project_2)).to eq(label_2_1_priority)
+ expect(new_label.priority(project_3)).to eq(label_3_1_priority)
+ end
+
+ it 'does not touch project out of promoted group' do
+ service.execute(project_label_1_1)
+ project_4_new_label = project_4.labels.find_by(title: promoted_label_name)
+
+ expect(project_4_new_label).not_to be_nil
+ expect(project_4_new_label.id).to eq(project_label_4_1.id)
+ end
+
+ it 'does not touch out of group priority' do
+ service.execute(project_label_1_1)
+
+ expect(new_label.priority(project_4)).to be_nil
+ end
+
+ it 'relinks issue with the promoted label' do
+ service.execute(project_label_1_1)
+ issue_label = issue_1_1.labels.find_by(title: promoted_label_name)
+
+ expect(issue_label).not_to be_nil
+ expect(issue_label.id).to eq(new_label.id)
+ end
+
+ it 'does not remove untouched labels from issue' do
+ expect { service.execute(project_label_1_1) }.not_to change(issue_1_1.labels, :count)
+ end
+
+ it 'does not relink untouched label in issue' do
+ service.execute(project_label_1_1)
+ issue_label = issue_1_2.labels.find_by(title: untouched_label_name)
+
+ expect(issue_label).not_to be_nil
+ expect(issue_label.id).to eq(project_label_1_2.id)
+ end
+
+ it 'relinks issues with merged labels' do
+ service.execute(project_label_1_1)
+ issue_label = issue_2_1.labels.find_by(title: promoted_label_name)
+
+ expect(issue_label).not_to be_nil
+ expect(issue_label.id).to eq(new_label.id)
+ end
+
+ it 'does not relink issues from other group' do
+ service.execute(project_label_1_1)
+ issue_label = issue_4_1.labels.find_by(title: promoted_label_name)
+
+ expect(issue_label).not_to be_nil
+ expect(issue_label.id).to eq(project_label_4_1.id)
+ end
+
+ it 'updates merge request' do
+ service.execute(project_label_1_1)
+ merge_label = merge_3_1.labels.find_by(title: promoted_label_name)
+
+ expect(merge_label).not_to be_nil
+ expect(merge_label.id).to eq(new_label.id)
+ end
+
+ it 'updates board lists' do
+ service.execute(project_label_1_1)
+ list = issue_board_2_1.lists.find_by(label: new_label)
+
+ expect(list).not_to be_nil
+ end
+
+ # In case someone adds a new relation to Label.rb and forgets to relink it
+ # and the database doesn't have foreign key constraints
+ it 'relinks all relations' do
+ service.execute(project_label_1_1)
+
+ Label.reflect_on_all_associations.each do |association|
+ expect(project_label_1_1.send(association.name).any?).to be_falsey
+ end
+ end
+
+ context 'with invalid group label' do
+ before do
+ allow(service).to receive(:clone_label_to_group_label).and_wrap_original do |m, *args|
+ label = m.call(*args)
+ allow(label).to receive(:valid?).and_return(false)
+
+ label
+ end
+ end
+
+ it 'raises an exception' do
+ expect { service.execute(project_label_1_1) }.to raise_error(ActiveRecord::RecordInvalid)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/merge_requests/close_service_spec.rb b/spec/services/merge_requests/close_service_spec.rb
index 5f6a7716beb..d55a7657c0e 100644
--- a/spec/services/merge_requests/close_service_spec.rb
+++ b/spec/services/merge_requests/close_service_spec.rb
@@ -29,7 +29,7 @@ describe MergeRequests::CloseService, services: true do
it { expect(@merge_request).to be_valid }
it { expect(@merge_request).to be_closed }
- it 'should execute hooks with close action' do
+ it 'executes hooks with close action' do
expect(service).to have_received(:execute_hooks).
with(@merge_request, 'close')
end
diff --git a/spec/services/merge_requests/create_service_spec.rb b/spec/services/merge_requests/create_service_spec.rb
index b8142889075..673c0bd6c9c 100644
--- a/spec/services/merge_requests/create_service_spec.rb
+++ b/spec/services/merge_requests/create_service_spec.rb
@@ -84,6 +84,8 @@ describe MergeRequests::CreateService, services: true do
end
end
+ it_behaves_like 'issuable create service'
+
context 'while saving references to issues that the created merge request closes' do
let(:first_issue) { create(:issue, project: project) }
let(:second_issue) { create(:issue, project: project) }
diff --git a/spec/services/merge_requests/merge_request_diff_cache_service_spec.rb b/spec/services/merge_requests/merge_request_diff_cache_service_spec.rb
index 05cdbe5287a..35804d41b46 100644
--- a/spec/services/merge_requests/merge_request_diff_cache_service_spec.rb
+++ b/spec/services/merge_requests/merge_request_diff_cache_service_spec.rb
@@ -11,6 +11,7 @@ describe MergeRequests::MergeRequestDiffCacheService do
expect(Rails.cache).to receive(:read).with(cache_key).and_return({})
expect(Rails.cache).to receive(:write).with(cache_key, anything)
allow_any_instance_of(Gitlab::Diff::File).to receive(:blob).and_return(double("text?" => true))
+ allow_any_instance_of(Repository).to receive(:diffable?).and_return(true)
subject.execute(merge_request)
end
diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb
index 7e3705983fb..2cc21acab7b 100644
--- a/spec/services/merge_requests/refresh_service_spec.rb
+++ b/spec/services/merge_requests/refresh_service_spec.rb
@@ -89,7 +89,7 @@ describe MergeRequests::RefreshService, services: true do
# Merge master -> feature branch
author = { email: 'test@gitlab.com', time: Time.now, name: "Me" }
commit_options = { message: 'Test message', committer: author, author: author }
- @project.repository.merge(@user, @merge_request, commit_options)
+ @project.repository.merge(@user, @merge_request.diff_head_sha, @merge_request, commit_options)
commit = @project.repository.commit('feature')
service.new(@project, @user).execute(@oldrev, commit.id, 'refs/heads/feature')
reload_mrs
@@ -106,23 +106,46 @@ describe MergeRequests::RefreshService, services: true do
context 'push to fork repo source branch' do
let(:refresh_service) { service.new(@fork_project, @user) }
- before do
- allow(refresh_service).to receive(:execute_hooks)
- refresh_service.execute(@oldrev, @newrev, 'refs/heads/master')
- reload_mrs
- end
- it 'executes hooks with update action' do
- expect(refresh_service).to have_received(:execute_hooks).
- with(@fork_merge_request, 'update', @oldrev)
+ context 'open fork merge request' do
+ before do
+ allow(refresh_service).to receive(:execute_hooks)
+ refresh_service.execute(@oldrev, @newrev, 'refs/heads/master')
+ reload_mrs
+ end
+
+ it 'executes hooks with update action' do
+ expect(refresh_service).to have_received(:execute_hooks).
+ with(@fork_merge_request, 'update', @oldrev)
+ end
+
+ it { expect(@merge_request.notes).to be_empty }
+ it { expect(@merge_request).to be_open }
+ it { expect(@fork_merge_request.notes.last.note).to include('added 28 commits') }
+ it { expect(@fork_merge_request).to be_open }
+ it { expect(@build_failed_todo).to be_pending }
+ it { expect(@fork_build_failed_todo).to be_pending }
end
- it { expect(@merge_request.notes).to be_empty }
- it { expect(@merge_request).to be_open }
- it { expect(@fork_merge_request.notes.last.note).to include('added 28 commits') }
- it { expect(@fork_merge_request).to be_open }
- it { expect(@build_failed_todo).to be_pending }
- it { expect(@fork_build_failed_todo).to be_pending }
+ context 'closed fork merge request' do
+ before do
+ @fork_merge_request.close!
+ allow(refresh_service).to receive(:execute_hooks)
+ refresh_service.execute(@oldrev, @newrev, 'refs/heads/master')
+ reload_mrs
+ end
+
+ it 'do not execute hooks with update action' do
+ expect(refresh_service).not_to have_received(:execute_hooks)
+ end
+
+ it { expect(@merge_request.notes).to be_empty }
+ it { expect(@merge_request).to be_open }
+ it { expect(@fork_merge_request.notes).to be_empty }
+ it { expect(@fork_merge_request).to be_closed }
+ it { expect(@build_failed_todo).to be_pending }
+ it { expect(@fork_build_failed_todo).to be_pending }
+ end
end
context 'push to fork repo target branch' do
@@ -237,6 +260,70 @@ describe MergeRequests::RefreshService, services: true do
end
end
+ context 'marking the merge request as work in progress' do
+ let(:refresh_service) { service.new(@project, @user) }
+ before do
+ allow(refresh_service).to receive(:execute_hooks)
+ end
+
+ it 'marks the merge request as work in progress from fixup commits' do
+ fixup_merge_request = create(:merge_request,
+ source_project: @project,
+ source_branch: 'wip',
+ target_branch: 'master',
+ target_project: @project)
+ commits = fixup_merge_request.commits
+ oldrev = commits.last.id
+ newrev = commits.first.id
+
+ refresh_service.execute(oldrev, newrev, 'refs/heads/wip')
+ fixup_merge_request.reload
+
+ expect(fixup_merge_request.work_in_progress?).to eq(true)
+ expect(fixup_merge_request.notes.last.note).to match(
+ /marked as a \*\*Work In Progress\*\* from #{Commit.reference_pattern}/
+ )
+ end
+
+ it 'references the commit that caused the Work in Progress status' do
+ refresh_service.execute(@oldrev, @newrev, 'refs/heads/master')
+
+ allow(refresh_service).to receive(:find_new_commits)
+ refresh_service.instance_variable_set("@commits", [
+ instance_double(
+ Commit,
+ id: 'aaaaaaa',
+ short_id: 'aaaaaaa',
+ title: 'Fix issue',
+ work_in_progress?: false
+ ),
+ instance_double(
+ Commit,
+ id: 'bbbbbbb',
+ short_id: 'bbbbbbb',
+ title: 'fixup! Fix issue',
+ work_in_progress?: true,
+ to_reference: 'bbbbbbb'
+ ),
+ instance_double(
+ Commit,
+ id: 'ccccccc',
+ short_id: 'ccccccc',
+ title: 'fixup! Fix issue',
+ work_in_progress?: true,
+ to_reference: 'ccccccc'
+ ),
+ ])
+
+ refresh_service.execute(@oldrev, @newrev, 'refs/heads/wip')
+ reload_mrs
+
+ expect(@merge_request.notes.last.note).to eq(
+ "marked as a **Work In Progress** from bbbbbbb"
+ )
+ end
+ end
+
def reload_mrs
@merge_request.reload
@fork_merge_request.reload
diff --git a/spec/services/merge_requests/resolve_service_spec.rb b/spec/services/merge_requests/resolve_service_spec.rb
index 388abb6a0df..a0e51681725 100644
--- a/spec/services/merge_requests/resolve_service_spec.rb
+++ b/spec/services/merge_requests/resolve_service_spec.rb
@@ -66,7 +66,13 @@ describe MergeRequests::ResolveService do
context 'when the source project is a fork and does not contain the HEAD of the target branch' do
let!(:target_head) do
- project.repository.commit_file(user, 'new-file-in-target', '', 'Add new file in target', 'conflict-start', false)
+ project.repository.commit_file(
+ user,
+ 'new-file-in-target',
+ '',
+ message: 'Add new file in target',
+ branch_name: 'conflict-start',
+ update: false)
end
before do
diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb
index 88c786947d3..7d73c0ea5d0 100644
--- a/spec/services/merge_requests/update_service_spec.rb
+++ b/spec/services/merge_requests/update_service_spec.rb
@@ -121,6 +121,99 @@ describe MergeRequests::UpdateService, services: true do
end
end
+ context 'merge' do
+ let(:opts) do
+ {
+ merge: merge_request.diff_head_sha
+ }
+ end
+
+ let(:service) { MergeRequests::UpdateService.new(project, user, opts) }
+
+ context 'without pipeline' do
+ before do
+ merge_request.merge_error = 'Error'
+
+ perform_enqueued_jobs do
+ service.execute(merge_request)
+ @merge_request = MergeRequest.find(merge_request.id)
+ end
+ end
+
+ it { expect(@merge_request).to be_valid }
+ it { expect(@merge_request.state).to eq('merged') }
+ it { expect(@merge_request.merge_error).to be_nil }
+ end
+
+ context 'with finished pipeline' do
+ before do
+ create(:ci_pipeline_with_one_job,
+ project: project,
+ ref: merge_request.source_branch,
+ sha: merge_request.diff_head_sha,
+ status: :success)
+
+ perform_enqueued_jobs do
+ @merge_request = service.execute(merge_request)
+ @merge_request = MergeRequest.find(merge_request.id)
+ end
+ end
+
+ it { expect(@merge_request).to be_valid }
+ it { expect(@merge_request.state).to eq('merged') }
+ end
+
+ context 'with active pipeline' do
+ before do
+ service_mock = double
+ create(:ci_pipeline_with_one_job,
+ project: project,
+ ref: merge_request.source_branch,
+ sha: merge_request.diff_head_sha)
+
+ expect(MergeRequests::MergeWhenPipelineSucceedsService).to receive(:new).with(project, user).
+ and_return(service_mock)
+ expect(service_mock).to receive(:execute).with(merge_request)
+ end
+
+ it { service.execute(merge_request) }
+ end
+
+ context 'with a non-authorised user' do
+ let(:visitor) { create(:user) }
+ let(:service) { MergeRequests::UpdateService.new(project, visitor, opts) }
+
+ before do
+ merge_request.update_attribute(:merge_error, 'Error')
+
+ perform_enqueued_jobs do
+ @merge_request = service.execute(merge_request)
+ @merge_request = MergeRequest.find(merge_request.id)
+ end
+ end
+
+ it { expect(@merge_request.state).to eq('opened') }
+ it { expect(@merge_request.merge_error).not_to be_nil }
+ end
+
+ context 'MR can not be merged when note sha != MR sha' do
+ let(:opts) do
+ {
+ merge: 'other_commit'
+ }
+ end
+
+ before do
+ perform_enqueued_jobs do
+ @merge_request = service.execute(merge_request)
+ @merge_request = MergeRequest.find(merge_request.id)
+ end
+ end
+
+ it { expect(@merge_request.state).to eq('opened') }
+ end
+ end
+
context 'todos' do
let!(:pending_todo) { create(:todo, :assigned, user: user, project: project, target: merge_request, author: user2) }
diff --git a/spec/services/notes/create_service_spec.rb b/spec/services/notes/create_service_spec.rb
index 25804696d2e..9c92a5080c6 100644
--- a/spec/services/notes/create_service_spec.rb
+++ b/spec/services/notes/create_service_spec.rb
@@ -15,39 +15,45 @@ describe Notes::CreateService, services: true do
context "valid params" do
it 'returns a valid note' do
- note = Notes::CreateService.new(project, user, opts).execute
+ note = described_class.new(project, user, opts).execute
expect(note).to be_valid
end
it 'returns a persisted note' do
- note = Notes::CreateService.new(project, user, opts).execute
+ note = described_class.new(project, user, opts).execute
expect(note).to be_persisted
end
it 'note has valid content' do
- note = Notes::CreateService.new(project, user, opts).execute
+ note = described_class.new(project, user, opts).execute
expect(note.note).to eq(opts[:note])
end
+ it 'note belongs to the correct project' do
+ note = described_class.new(project, user, opts).execute
+
+ expect(note.project).to eq(project)
+ end
+
it 'TodoService#new_note is called' do
- note = build(:note)
- allow(project).to receive_message_chain(:notes, :new).with(opts) { note }
+ note = build(:note, project: project)
+ allow(Note).to receive(:new).with(opts) { note }
expect_any_instance_of(TodoService).to receive(:new_note).with(note, user)
- Notes::CreateService.new(project, user, opts).execute
+ described_class.new(project, user, opts).execute
end
it 'enqueues NewNoteWorker' do
- note = build(:note, id: 999)
- allow(project).to receive_message_chain(:notes, :new).with(opts) { note }
+ note = build(:note, id: 999, project: project)
+ allow(Note).to receive(:new).with(opts) { note }
expect(NewNoteWorker).to receive(:perform_async).with(note.id)
- Notes::CreateService.new(project, user, opts).execute
+ described_class.new(project, user, opts).execute
end
end
@@ -63,6 +69,38 @@ describe Notes::CreateService, services: true do
expect(note.note).to eq "HELLO\nWORLD"
end
end
+
+ describe '/merge with sha option' do
+ let(:note_text) { %(HELLO\n/merge\nWORLD) }
+ let(:params) { opts.merge(note: note_text, merge_request_diff_head_sha: 'sha') }
+
+ it 'saves the note and exectues merge command' do
+ note = described_class.new(project, user, params).execute
+
+ expect(note.note).to eq "HELLO\nWORLD"
+ end
+ end
+ end
+
+ describe 'personal snippet note' do
+ subject { described_class.new(nil, user, params).execute }
+
+ let(:snippet) { create(:personal_snippet) }
+ let(:params) do
+ { note: 'comment', noteable_type: 'Snippet', noteable_id: snippet.id }
+ end
+
+ it 'returns a valid note' do
+ expect(subject).to be_valid
+ end
+
+ it 'returns a persisted note' do
+ expect(subject).to be_persisted
+ end
+
+ it 'note has valid content' do
+ expect(subject.note).to eq(params[:note])
+ end
end
end
@@ -77,7 +115,7 @@ describe Notes::CreateService, services: true do
noteable_type: 'Issue',
noteable_id: issue.id
}
- note = Notes::CreateService.new(project, user, opts).execute
+ note = described_class.new(project, user, opts).execute
expect(note).to be_valid
expect(note.name).to eq('smile')
@@ -89,7 +127,7 @@ describe Notes::CreateService, services: true do
noteable_type: 'Issue',
noteable_id: issue.id
}
- note = Notes::CreateService.new(project, user, opts).execute
+ note = described_class.new(project, user, opts).execute
expect(note).to be_valid
expect(note.note).to eq(opts[:note])
@@ -104,7 +142,7 @@ describe Notes::CreateService, services: true do
expect_any_instance_of(TodoService).to receive(:new_award_emoji).with(issue, user)
- Notes::CreateService.new(project, user, opts).execute
+ described_class.new(project, user, opts).execute
end
end
end
diff --git a/spec/services/notes/slash_commands_service_spec.rb b/spec/services/notes/slash_commands_service_spec.rb
index d1099884a02..1a64c8bbf00 100644
--- a/spec/services/notes/slash_commands_service_spec.rb
+++ b/spec/services/notes/slash_commands_service_spec.rb
@@ -5,6 +5,8 @@ describe Notes::SlashCommandsService, services: true do
let(:project) { create(:empty_project) }
let(:master) { create(:user).tap { |u| project.team << [u, :master] } }
let(:assignee) { create(:user) }
+
+ before { project.team << [assignee, :master] }
end
shared_examples 'note on noteable that does not support slash commands' do
@@ -84,6 +86,18 @@ describe Notes::SlashCommandsService, services: true do
expect(note.noteable).to be_open
end
end
+
+ describe '/spend' do
+ let(:note_text) { '/spend 1h' }
+
+ it 'updates the spent time on the noteable' do
+ content, command_params = service.extract_commands(note)
+ service.execute(command_params, note)
+
+ expect(content).to eq ''
+ expect(note.noteable.time_spent).to eq(3600)
+ end
+ end
end
describe 'note with command & text' do
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index f3e80ac22a0..7cf2cd9968f 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -33,6 +33,49 @@ describe NotificationService, services: true do
end
end
+ # Next shared examples are intended to test notifications of "participants"
+ #
+ # they take the following parameters:
+ # * issuable
+ # * notification trigger
+ # * participant
+ #
+ shared_examples 'participating by note notification' do
+ it 'emails the participant' do
+ create(:note_on_issue, noteable: issuable, project_id: project.id, note: 'anything', author: participant)
+
+ notification_trigger
+
+ should_email(participant)
+ end
+ end
+
+ shared_examples 'participating by assignee notification' do
+ it 'emails the participant' do
+ issuable.update_attribute(:assignee, participant)
+
+ notification_trigger
+
+ should_email(participant)
+ end
+ end
+
+ shared_examples 'participating by author notification' do
+ it 'emails the participant' do
+ issuable.author = participant
+
+ notification_trigger
+
+ should_email(participant)
+ end
+ end
+
+ shared_examples_for 'participating notifications' do
+ it_should_behave_like 'participating by note notification'
+ it_should_behave_like 'participating by author notification'
+ it_should_behave_like 'participating by assignee notification'
+ end
+
describe 'Keys' do
describe '#new_key' do
let!(:key) { create(:personal_key) }
@@ -269,6 +312,55 @@ describe NotificationService, services: true do
end
end
+ context 'personal snippet note' do
+ let(:snippet) { create(:personal_snippet, :public, author: @u_snippet_author) }
+ let(:note) { create(:note_on_personal_snippet, noteable: snippet, note: '@mentioned note', author: @u_note_author) }
+
+ before do
+ @u_watcher = create_global_setting_for(create(:user), :watch)
+ @u_participant = create_global_setting_for(create(:user), :participating)
+ @u_disabled = create_global_setting_for(create(:user), :disabled)
+ @u_mentioned = create_global_setting_for(create(:user, username: 'mentioned'), :mention)
+ @u_mentioned_level = create_global_setting_for(create(:user, username: 'participator'), :mention)
+ @u_note_author = create(:user, username: 'note_author')
+ @u_snippet_author = create(:user, username: 'snippet_author')
+ @u_not_mentioned = create_global_setting_for(create(:user, username: 'regular'), :participating)
+
+ reset_delivered_emails!
+ end
+
+ let!(:notes) do
+ [
+ create(:note_on_personal_snippet, noteable: snippet, note: 'note', author: @u_watcher),
+ create(:note_on_personal_snippet, noteable: snippet, note: 'note', author: @u_participant),
+ create(:note_on_personal_snippet, noteable: snippet, note: 'note', author: @u_mentioned),
+ create(:note_on_personal_snippet, noteable: snippet, note: 'note', author: @u_disabled),
+ create(:note_on_personal_snippet, noteable: snippet, note: 'note', author: @u_note_author),
+ ]
+ end
+
+ describe '#new_note' do
+ it 'notifies the participants' do
+ notification.new_note(note)
+
+ # it emails participants
+ should_email(@u_watcher)
+ should_email(@u_participant)
+ should_email(@u_watcher)
+ should_email(@u_snippet_author)
+
+ # it emails mentioned users
+ should_email(@u_mentioned)
+
+ # it does not email participants with mention notification level
+ should_not_email(@u_mentioned_level)
+
+ # it does not email note author
+ should_not_email(@u_note_author)
+ end
+ end
+ end
+
context 'commit note' do
let(:project) { create(:project, :public) }
let(:note) { create(:note_on_commit, project: project) }
@@ -351,6 +443,8 @@ describe NotificationService, services: true do
before do
build_team(issue.project)
+ build_group(issue.project)
+
add_users_with_subscription(issue.project, issue)
reset_delivered_emails!
update_custom_notification(:new_issue, @u_guest_custom, project)
@@ -367,6 +461,8 @@ describe NotificationService, services: true do
should_email(@u_guest_custom)
should_email(@u_custom_global)
should_email(@u_participant_mentioned)
+ should_email(@g_global_watcher)
+ should_email(@g_watcher)
should_not_email(@u_mentioned)
should_not_email(@u_participating)
should_not_email(@u_disabled)
@@ -539,32 +635,10 @@ describe NotificationService, services: true do
should_not_email(@u_lazy_participant)
end
- context 'participating' do
- context 'by assignee' do
- before do
- issue.update_attribute(:assignee, @u_lazy_participant)
- notification.reassigned_issue(issue, @u_disabled)
- end
-
- it { should_email(@u_lazy_participant) }
- end
-
- context 'by note' do
- let!(:note) { create(:note_on_issue, noteable: issue, project_id: issue.project_id, note: 'anything', author: @u_lazy_participant) }
-
- before { notification.reassigned_issue(issue, @u_disabled) }
-
- it { should_email(@u_lazy_participant) }
- end
-
- context 'by author' do
- before do
- issue.author = @u_lazy_participant
- notification.reassigned_issue(issue, @u_disabled)
- end
-
- it { should_email(@u_lazy_participant) }
- end
+ it_behaves_like 'participating notifications' do
+ let(:participant) { create(:user, username: 'user-participant') }
+ let(:issuable) { issue }
+ let(:notification_trigger) { notification.reassigned_issue(issue, @u_disabled) }
end
end
@@ -671,32 +745,10 @@ describe NotificationService, services: true do
should_not_email(@u_lazy_participant)
end
- context 'participating' do
- context 'by assignee' do
- before do
- issue.update_attribute(:assignee, @u_lazy_participant)
- notification.close_issue(issue, @u_disabled)
- end
-
- it { should_email(@u_lazy_participant) }
- end
-
- context 'by note' do
- let!(:note) { create(:note_on_issue, noteable: issue, project_id: issue.project_id, note: 'anything', author: @u_lazy_participant) }
-
- before { notification.close_issue(issue, @u_disabled) }
-
- it { should_email(@u_lazy_participant) }
- end
-
- context 'by author' do
- before do
- issue.author = @u_lazy_participant
- notification.close_issue(issue, @u_disabled)
- end
-
- it { should_email(@u_lazy_participant) }
- end
+ it_behaves_like 'participating notifications' do
+ let(:participant) { create(:user, username: 'user-participant') }
+ let(:issuable) { issue }
+ let(:notification_trigger) { notification.close_issue(issue, @u_disabled) }
end
end
@@ -723,32 +775,10 @@ describe NotificationService, services: true do
should_not_email(@u_lazy_participant)
end
- context 'participating' do
- context 'by assignee' do
- before do
- issue.update_attribute(:assignee, @u_lazy_participant)
- notification.reopen_issue(issue, @u_disabled)
- end
-
- it { should_email(@u_lazy_participant) }
- end
-
- context 'by note' do
- let!(:note) { create(:note_on_issue, noteable: issue, project_id: issue.project_id, note: 'anything', author: @u_lazy_participant) }
-
- before { notification.reopen_issue(issue, @u_disabled) }
-
- it { should_email(@u_lazy_participant) }
- end
-
- context 'by author' do
- before do
- issue.author = @u_lazy_participant
- notification.reopen_issue(issue, @u_disabled)
- end
-
- it { should_email(@u_lazy_participant) }
- end
+ it_behaves_like 'participating notifications' do
+ let(:participant) { create(:user, username: 'user-participant') }
+ let(:issuable) { issue }
+ let(:notification_trigger) { notification.reopen_issue(issue, @u_disabled) }
end
end
end
@@ -809,31 +839,28 @@ describe NotificationService, services: true do
end
context 'participating' do
- context 'by assignee' do
- before do
- merge_request.update_attribute(:assignee, @u_lazy_participant)
- notification.new_merge_request(merge_request, @u_disabled)
- end
-
- it { should_email(@u_lazy_participant) }
+ it_should_behave_like 'participating by assignee notification' do
+ let(:participant) { create(:user, username: 'user-participant')}
+ let(:issuable) { merge_request }
+ let(:notification_trigger) { notification.new_merge_request(merge_request, @u_disabled) }
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.new_merge_request(merge_request, @u_disabled) }
-
- it { should_email(@u_lazy_participant) }
+ it_should_behave_like 'participating by note notification' do
+ let(:participant) { create(:user, username: 'user-participant')}
+ let(:issuable) { merge_request }
+ let(:notification_trigger) { notification.new_merge_request(merge_request, @u_disabled) }
end
context 'by author' do
+ let(:participant) { create(:user, username: 'user-participant')}
+
before do
- merge_request.author = @u_lazy_participant
+ merge_request.author = participant
merge_request.save
notification.new_merge_request(merge_request, @u_disabled)
end
- it { should_not_email(@u_lazy_participant) }
+ it { should_not_email(participant) }
end
end
end
@@ -868,33 +895,10 @@ describe NotificationService, services: true do
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.reassigned_merge_request(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.reassigned_merge_request(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.reassigned_merge_request(merge_request, @u_disabled)
- end
-
- it { should_email(@u_lazy_participant) }
- end
+ it_behaves_like 'participating notifications' do
+ let(:participant) { create(:user, username: 'user-participant') }
+ let(:issuable) { merge_request }
+ let(:notification_trigger) { notification.reassigned_merge_request(merge_request, @u_disabled) }
end
end
@@ -965,33 +969,10 @@ describe NotificationService, services: true do
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.close_mr(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.close_mr(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.close_mr(merge_request, @u_disabled)
- end
-
- it { should_email(@u_lazy_participant) }
- end
+ it_behaves_like 'participating notifications' do
+ let(:participant) { create(:user, username: 'user-participant') }
+ let(:issuable) { merge_request }
+ let(:notification_trigger) { notification.close_mr(merge_request, @u_disabled) }
end
end
@@ -1032,33 +1013,10 @@ describe NotificationService, services: true do
should_not_email(@u_watcher)
end
- context 'participating' do
- context 'by assignee' do
- before do
- merge_request.update_attribute(:assignee, @u_lazy_participant)
- notification.merge_mr(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.merge_mr(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.merge_mr(merge_request, @u_disabled)
- end
-
- it { should_email(@u_lazy_participant) }
- end
+ it_behaves_like 'participating notifications' do
+ let(:participant) { create(:user, username: 'user-participant') }
+ let(:issuable) { merge_request }
+ let(:notification_trigger) { notification.merge_mr(merge_request, @u_disabled) }
end
end
@@ -1085,33 +1043,10 @@ describe NotificationService, services: true do
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.reopen_mr(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.reopen_mr(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.reopen_mr(merge_request, @u_disabled)
- end
-
- it { should_email(@u_lazy_participant) }
- end
+ it_behaves_like 'participating notifications' do
+ let(:participant) { create(:user, username: 'user-participant') }
+ let(:issuable) { merge_request }
+ let(:notification_trigger) { notification.reopen_mr(merge_request, @u_disabled) }
end
end
@@ -1131,33 +1066,10 @@ describe NotificationService, services: true do
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
+ it_behaves_like 'participating notifications' do
+ let(:participant) { create(:user, username: 'user-participant') }
+ let(:issuable) { merge_request }
+ let(:notification_trigger) { notification.resolve_all_discussions(merge_request, @u_disabled) }
end
end
end
@@ -1310,6 +1222,22 @@ describe NotificationService, services: true do
project.add_master(@u_custom_global)
end
+ # Users in the project's group but not part of project's team
+ # with different notification settings
+ def build_group(project)
+ group = create(:group, :public)
+ project.group = group
+
+ # Group member: global=disabled, group=watch
+ @g_watcher = create_user_with_notification(:watch, 'group_watcher', project.group)
+ @g_watcher.notification_settings_for(nil).disabled!
+
+ # Group member: global=watch, group=global
+ @g_global_watcher = create_global_setting_for(create(:user), :watch)
+ group.add_users([@g_watcher, @g_global_watcher], :master)
+ group
+ end
+
def create_global_setting_for(user, level)
setting = user.global_notification_setting
setting.level = level
@@ -1318,9 +1246,9 @@ describe NotificationService, services: true do
user
end
- def create_user_with_notification(level, username)
+ def create_user_with_notification(level, username, resource = project)
user = create(:user, username: username)
- setting = user.notification_settings_for(project)
+ setting = user.notification_settings_for(resource)
setting.level = level
setting.save
diff --git a/spec/services/pages_service_spec.rb b/spec/services/pages_service_spec.rb
new file mode 100644
index 00000000000..aa63fe3a5c1
--- /dev/null
+++ b/spec/services/pages_service_spec.rb
@@ -0,0 +1,47 @@
+require 'spec_helper'
+
+describe PagesService, services: true do
+ let(:build) { create(:ci_build) }
+ let(:data) { Gitlab::DataBuilder::Build.build(build) }
+ let(:service) { PagesService.new(data) }
+
+ before do
+ allow(Gitlab.config.pages).to receive(:enabled).and_return(true)
+ end
+
+ context 'execute asynchronously for pages job' do
+ before { build.name = 'pages' }
+
+ context 'on success' do
+ before { build.success }
+
+ it 'executes worker' do
+ expect(PagesWorker).to receive(:perform_async)
+ service.execute
+ end
+ end
+
+ %w(pending running failed canceled).each do |status|
+ context "on #{status}" do
+ before { build.status = status }
+
+ it 'does not execute worker' do
+ expect(PagesWorker).not_to receive(:perform_async)
+ service.execute
+ end
+ end
+ end
+ end
+
+ context 'for other jobs' do
+ before do
+ build.name = 'other job'
+ build.success
+ end
+
+ it 'does not execute worker' do
+ expect(PagesWorker).not_to receive(:perform_async)
+ service.execute
+ end
+ end
+end
diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb
index a1539b69401..af515ad2e0e 100644
--- a/spec/services/projects/create_service_spec.rb
+++ b/spec/services/projects/create_service_spec.rb
@@ -90,10 +90,6 @@ describe Projects::CreateService, '#execute', services: true do
end
context 'global builds_enabled true does enable CI by default' do
- before do
- project.project_feature.update_attribute(:builds_access_level, ProjectFeature::ENABLED)
- end
-
it { is_expected.to be_truthy }
end
end
diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb
index 90771825f5c..3faa88c00a1 100644
--- a/spec/services/projects/destroy_service_spec.rb
+++ b/spec/services/projects/destroy_service_spec.rb
@@ -9,12 +9,27 @@ describe Projects::DestroyService, services: true do
shared_examples 'deleting the project' do
it 'deletes the project' do
- expect(Project.all).not_to include(project)
+ expect(Project.unscoped.all).not_to include(project)
expect(Dir.exist?(path)).to be_falsey
expect(Dir.exist?(remove_path)).to be_falsey
end
end
+ shared_examples 'deleting the project with pipeline and build' do
+ context 'with pipeline and build' do # which has optimistic locking
+ let!(:pipeline) { create(:ci_pipeline, project: project) }
+ let!(:build) { create(:ci_build, :artifacts, pipeline: pipeline) }
+
+ before do
+ perform_enqueued_jobs do
+ destroy_project(project, user, {})
+ end
+ end
+
+ it_behaves_like 'deleting the project'
+ end
+ end
+
context 'Sidekiq inline' do
before do
# Run sidekiq immediatly to check that renamed repository will be removed
@@ -35,30 +50,24 @@ describe Projects::DestroyService, services: true do
it { expect(Dir.exist?(remove_path)).to be_truthy }
end
- context 'async delete of project with private issue visibility' do
- let!(:async) { true }
+ context 'with async_execute' do
+ let(:async) { true }
- before do
- project.project_feature.update_attribute("issues_access_level", ProjectFeature::PRIVATE)
- # Run sidekiq immediately to check that renamed repository will be removed
- Sidekiq::Testing.inline! { destroy_project(project, user, {}) }
+ context 'async delete of project with private issue visibility' do
+ before do
+ project.project_feature.update_attribute("issues_access_level", ProjectFeature::PRIVATE)
+ # Run sidekiq immediately to check that renamed repository will be removed
+ Sidekiq::Testing.inline! { destroy_project(project, user, {}) }
+ end
+
+ it_behaves_like 'deleting the project'
end
- it_behaves_like 'deleting the project'
+ it_behaves_like 'deleting the project with pipeline and build'
end
- context 'delete with pipeline' do # which has optimistic locking
- let!(:pipeline) { create(:ci_pipeline, project: project) }
-
- before do
- expect(project).to receive(:destroy!).and_call_original
-
- perform_enqueued_jobs do
- destroy_project(project, user, {})
- end
- end
-
- it_behaves_like 'deleting the project'
+ context 'with execute' do
+ it_behaves_like 'deleting the project with pipeline and build'
end
context 'container registry' do
diff --git a/spec/services/projects/fork_service_spec.rb b/spec/services/projects/fork_service_spec.rb
index 64d15c0523c..8e614211116 100644
--- a/spec/services/projects/fork_service_spec.rb
+++ b/spec/services/projects/fork_service_spec.rb
@@ -5,10 +5,12 @@ describe Projects::ForkService, services: true do
before do
@from_namespace = create(:namespace)
@from_user = create(:user, namespace: @from_namespace )
+ avatar = fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png")
@from_project = create(:project,
creator_id: @from_user.id,
namespace: @from_namespace,
star_count: 107,
+ avatar: avatar,
description: 'wow such project')
@to_namespace = create(:namespace)
@to_user = create(:user, namespace: @to_namespace)
@@ -36,6 +38,17 @@ describe Projects::ForkService, services: true do
it { expect(to_project.namespace).to eq(@to_user.namespace) }
it { expect(to_project.star_count).to be_zero }
it { expect(to_project.description).to eq(@from_project.description) }
+ it { expect(to_project.avatar.file).to be_exists }
+
+ # This test is here because we had a bug where the from-project lost its
+ # avatar after being forked.
+ # https://gitlab.com/gitlab-org/gitlab-ce/issues/26158
+ it "after forking the from-project still has its avatar" do
+ # If we do not fork the project first we cannot detect the bug.
+ expect(to_project).to be_persisted
+
+ expect(@from_project.avatar.file).to be_exists
+ end
end
end
diff --git a/spec/services/projects/participants_service_spec.rb b/spec/services/projects/participants_service_spec.rb
new file mode 100644
index 00000000000..063b3bd76eb
--- /dev/null
+++ b/spec/services/projects/participants_service_spec.rb
@@ -0,0 +1,32 @@
+require 'spec_helper'
+
+describe Projects::ParticipantsService, services: true do
+ describe '#groups' do
+ describe 'avatar_url' do
+ let(:project) { create(:empty_project, :public) }
+ let(:group) { create(:group, avatar: fixture_file_upload(Rails.root + 'spec/fixtures/dk.png')) }
+ let(:user) { create(:user) }
+ let(:base_url) { Settings.send(:build_base_gitlab_url) }
+ let!(:group_member) { create(:group_member, group: group, user: user) }
+
+ it 'should return an url for the avatar' do
+ participants = described_class.new(project, user)
+ groups = participants.groups
+
+ expect(groups.size).to eq 1
+ expect(groups.first[:avatar_url]).to eq "#{base_url}/uploads/group/avatar/#{group.id}/dk.png"
+ end
+
+ it 'should return an url for the avatar with relative url' do
+ stub_config_setting(relative_url_root: '/gitlab')
+ stub_config_setting(url: Settings.send(:build_gitlab_url))
+
+ participants = described_class.new(project, user)
+ groups = participants.groups
+
+ expect(groups.size).to eq 1
+ expect(groups.first[:avatar_url]).to eq "#{base_url}/gitlab/uploads/group/avatar/#{group.id}/dk.png"
+ end
+ end
+ end
+end
diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb
index 1540b90163a..5c6fbea8d0e 100644
--- a/spec/services/projects/transfer_service_spec.rb
+++ b/spec/services/projects/transfer_service_spec.rb
@@ -9,6 +9,8 @@ describe Projects::TransferService, services: true do
before do
allow_any_instance_of(Gitlab::UploadsTransfer).
to receive(:move_project).and_return(true)
+ allow_any_instance_of(Gitlab::PagesTransfer).
+ to receive(:move_project).and_return(true)
group.add_owner(user)
@result = transfer_project(project, user, group)
end
@@ -81,4 +83,30 @@ describe Projects::TransferService, services: true do
transfer_project(project, user, group)
end
end
+
+ describe 'refreshing project authorizations' do
+ let(:group) { create(:group) }
+ let(:owner) { project.namespace.owner }
+ let(:group_member) { create(:user) }
+
+ before do
+ group.add_user(owner, GroupMember::MASTER)
+ group.add_user(group_member, GroupMember::DEVELOPER)
+ end
+
+ it 'refreshes the permissions of the old and new namespace' do
+ transfer_project(project, owner, group)
+
+ expect(group_member.authorized_projects).to include(project)
+ expect(owner.authorized_projects).to include(project)
+ end
+
+ it 'only schedules a single job for every user' do
+ expect(UserProjectAccessChangedService).to receive(:new).
+ with([owner.id, group_member.id]).
+ and_call_original
+
+ transfer_project(project, owner, group)
+ end
+ end
end
diff --git a/spec/services/projects/update_pages_configuration_service_spec.rb b/spec/services/projects/update_pages_configuration_service_spec.rb
new file mode 100644
index 00000000000..8b329bc21c3
--- /dev/null
+++ b/spec/services/projects/update_pages_configuration_service_spec.rb
@@ -0,0 +1,24 @@
+require 'spec_helper'
+
+describe Projects::UpdatePagesConfigurationService, services: true do
+ let(:project) { create(:empty_project) }
+ subject { described_class.new(project) }
+
+ describe "#update" do
+ let(:file) { Tempfile.new('pages-test') }
+
+ after do
+ file.close
+ file.unlink
+ end
+
+ it 'updates the .update file' do
+ # Access this reference to ensure scoping works
+ Projects::Settings # rubocop:disable Lint/Void
+ expect(subject).to receive(:pages_config_file).and_return(file.path)
+ expect(subject).to receive(:reload_daemon).and_call_original
+
+ expect(subject.execute).to eq({ status: :success })
+ end
+ end
+end
diff --git a/spec/services/projects/update_pages_service_spec.rb b/spec/services/projects/update_pages_service_spec.rb
new file mode 100644
index 00000000000..411b22a0fb8
--- /dev/null
+++ b/spec/services/projects/update_pages_service_spec.rb
@@ -0,0 +1,80 @@
+require "spec_helper"
+
+describe Projects::UpdatePagesService do
+ let(:project) { create :project }
+ let(:pipeline) { create :ci_pipeline, project: project, sha: project.commit('HEAD').sha }
+ let(:build) { create :ci_build, pipeline: pipeline, ref: 'HEAD' }
+ let(:invalid_file) { fixture_file_upload(Rails.root + 'spec/fixtures/dk.png') }
+
+ subject { described_class.new(project, build) }
+
+ before do
+ project.remove_pages
+ end
+
+ %w(tar.gz zip).each do |format|
+ context "for valid #{format}" do
+ let(:file) { fixture_file_upload(Rails.root + "spec/fixtures/pages.#{format}") }
+ let(:empty_file) { fixture_file_upload(Rails.root + "spec/fixtures/pages_empty.#{format}") }
+ let(:metadata) do
+ filename = Rails.root + "spec/fixtures/pages.#{format}.meta"
+ fixture_file_upload(filename) if File.exist?(filename)
+ end
+
+ before do
+ build.update_attributes(artifacts_file: file)
+ build.update_attributes(artifacts_metadata: metadata)
+ end
+
+ it 'succeeds' do
+ expect(project.pages_deployed?).to be_falsey
+ expect(execute).to eq(:success)
+ expect(project.pages_deployed?).to be_truthy
+ end
+
+ it 'limits pages size' do
+ stub_application_setting(max_pages_size: 1)
+ expect(execute).not_to eq(:success)
+ end
+
+ it 'removes pages after destroy' do
+ expect(PagesWorker).to receive(:perform_in)
+ expect(project.pages_deployed?).to be_falsey
+ expect(execute).to eq(:success)
+ expect(project.pages_deployed?).to be_truthy
+ project.destroy
+ expect(project.pages_deployed?).to be_falsey
+ end
+
+ it 'fails if sha on branch is not latest' do
+ pipeline.update_attributes(sha: 'old_sha')
+ build.update_attributes(artifacts_file: file)
+ expect(execute).not_to eq(:success)
+ end
+
+ it 'fails for empty file fails' do
+ build.update_attributes(artifacts_file: empty_file)
+ expect(execute).not_to eq(:success)
+ end
+ end
+ end
+
+ it 'fails to remove project pages when no pages is deployed' do
+ expect(PagesWorker).not_to receive(:perform_in)
+ expect(project.pages_deployed?).to be_falsey
+ project.destroy
+ end
+
+ it 'fails if no artifacts' do
+ expect(execute).not_to eq(:success)
+ end
+
+ it 'fails for invalid archive' do
+ build.update_attributes(artifacts_file: invalid_file)
+ expect(execute).not_to eq(:success)
+ end
+
+ def execute
+ subject.execute[:status]
+ end
+end
diff --git a/spec/services/projects/update_service_spec.rb b/spec/services/projects/update_service_spec.rb
index e139be19140..caa23962519 100644
--- a/spec/services/projects/update_service_spec.rb
+++ b/spec/services/projects/update_service_spec.rb
@@ -1,145 +1,101 @@
require 'spec_helper'
describe Projects::UpdateService, services: true do
- describe :update_by_user do
- before do
- @user = create :user
- @admin = create :user, admin: true
- @project = create :project, creator_id: @user.id, namespace: @user.namespace
- @opts = {}
- end
+ let(:user) { create(:user) }
+ let(:admin) { create(:admin) }
+ let(:project) { create(:project, creator_id: user.id, namespace: user.namespace) }
- context 'is private when updated to private' do
- before do
- @created_private = @project.private?
+ describe 'update_by_user' do
+ context 'when visibility_level is INTERNAL' do
+ it 'updates the project to internal' do
+ result = update_project(project, user, visibility_level: Gitlab::VisibilityLevel::INTERNAL)
- @opts.merge!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
- update_project(@project, @user, @opts)
+ expect(result).to eq({ status: :success })
+ expect(project).to be_internal
end
-
- it { expect(@created_private).to be_truthy }
- it { expect(@project.private?).to be_truthy }
end
- context 'is internal when updated to internal' do
- before do
- @created_private = @project.private?
-
- @opts.merge!(visibility_level: Gitlab::VisibilityLevel::INTERNAL)
- update_project(@project, @user, @opts)
+ context 'when visibility_level is PUBLIC' do
+ it 'updates the project to public' do
+ result = update_project(project, user, visibility_level: Gitlab::VisibilityLevel::PUBLIC)
+ expect(result).to eq({ status: :success })
+ expect(project).to be_public
end
-
- it { expect(@created_private).to be_truthy }
- it { expect(@project.internal?).to be_truthy }
end
- context 'is public when updated to public' do
+ context 'when visibility levels are restricted to PUBLIC only' do
before do
- @created_private = @project.private?
-
- @opts.merge!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
- update_project(@project, @user, @opts)
- end
-
- it { expect(@created_private).to be_truthy }
- it { expect(@project.public?).to be_truthy }
- end
-
- context 'respect configured visibility restrictions setting' do
- before(:each) do
stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC])
end
- context 'is private when updated to private' do
- before do
- @created_private = @project.private?
-
- @opts.merge!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
- update_project(@project, @user, @opts)
+ context 'when visibility_level is INTERNAL' do
+ it 'updates the project to internal' do
+ result = update_project(project, user, visibility_level: Gitlab::VisibilityLevel::INTERNAL)
+ expect(result).to eq({ status: :success })
+ expect(project).to be_internal
end
-
- it { expect(@created_private).to be_truthy }
- it { expect(@project.private?).to be_truthy }
end
- context 'is internal when updated to internal' do
- before do
- @created_private = @project.private?
+ context 'when visibility_level is PUBLIC' do
+ it 'does not update the project to public' do
+ result = update_project(project, user, visibility_level: Gitlab::VisibilityLevel::PUBLIC)
- @opts.merge!(visibility_level: Gitlab::VisibilityLevel::INTERNAL)
- update_project(@project, @user, @opts)
+ expect(result).to eq({ status: :error, message: 'Visibility level unallowed' })
+ expect(project).to be_private
end
- it { expect(@created_private).to be_truthy }
- it { expect(@project.internal?).to be_truthy }
- end
-
- context 'is private when updated to public' do
- before do
- @created_private = @project.private?
-
- @opts.merge!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
- update_project(@project, @user, @opts)
+ context 'when updated by an admin' do
+ it 'updates the project to public' do
+ result = update_project(project, admin, visibility_level: Gitlab::VisibilityLevel::PUBLIC)
+ expect(result).to eq({ status: :success })
+ expect(project).to be_public
+ end
end
-
- it { expect(@created_private).to be_truthy }
- it { expect(@project.private?).to be_truthy }
- end
-
- context 'is public when updated to public by admin' do
- before do
- @created_private = @project.private?
-
- @opts.merge!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
- update_project(@project, @admin, @opts)
- end
-
- it { expect(@created_private).to be_truthy }
- it { expect(@project.public?).to be_truthy }
end
end
end
- describe :visibility_level do
- let(:user) { create :user, admin: true }
+ describe 'visibility_level' do
let(:project) { create(:project, :internal) }
let(:forked_project) { create(:forked_project_with_submodules, :internal) }
- let(:opts) { {} }
before do
forked_project.build_forked_project_link(forked_to_project_id: forked_project.id, forked_from_project_id: project.id)
forked_project.save
-
- @created_internal = project.internal?
- @fork_created_internal = forked_project.internal?
end
- context 'updates forks visibility level when parent set to more restrictive' do
- before do
- opts.merge!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
- update_project(project, user, opts).inspect
- end
+ it 'updates forks visibility level when parent set to more restrictive' do
+ opts = { visibility_level: Gitlab::VisibilityLevel::PRIVATE }
+
+ expect(project).to be_internal
+ expect(forked_project).to be_internal
- it { expect(@created_internal).to be_truthy }
- it { expect(@fork_created_internal).to be_truthy }
- it { expect(project.private?).to be_truthy }
- it { expect(project.forks.first.private?).to be_truthy }
+ expect(update_project(project, admin, opts)).to eq({ status: :success })
+
+ expect(project).to be_private
+ expect(forked_project.reload).to be_private
end
- context 'does not update forks visibility level when parent set to less restrictive' do
- before do
- opts.merge!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
- update_project(project, user, opts).inspect
- end
+ it 'does not update forks visibility level when parent set to less restrictive' do
+ opts = { visibility_level: Gitlab::VisibilityLevel::PUBLIC }
+
+ expect(project).to be_internal
+ expect(forked_project).to be_internal
- it { expect(@created_internal).to be_truthy }
- it { expect(@fork_created_internal).to be_truthy }
- it { expect(project.public?).to be_truthy }
- it { expect(project.forks.first.internal?).to be_truthy }
+ expect(update_project(project, admin, opts)).to eq({ status: :success })
+
+ expect(project).to be_public
+ expect(forked_project.reload).to be_internal
end
end
+ it 'returns an error result when record cannot be updated' do
+ result = update_project(project, admin, { name: 'foo&bar' })
+
+ expect(result).to eq({ status: :error, message: 'Project could not be updated' })
+ end
+
def update_project(project, user, opts)
- Projects::UpdateService.new(project, user, opts).execute
+ described_class.new(project, user, opts).execute
end
end
diff --git a/spec/services/search_service_spec.rb b/spec/services/search_service_spec.rb
index bd89c4a7c11..bed1031e40a 100644
--- a/spec/services/search_service_spec.rb
+++ b/spec/services/search_service_spec.rb
@@ -41,6 +41,25 @@ describe 'Search::GlobalService', services: true do
results = context.execute
expect(results.objects('projects')).to match_array [found_project]
end
+
+ context 'nested group' do
+ let!(:nested_group) { create(:group, :nested) }
+ let!(:project) { create(:project, namespace: nested_group) }
+
+ before { project.add_master(user) }
+
+ it 'returns result from nested group' do
+ context = Search::GlobalService.new(user, search: project.path)
+ results = context.execute
+ expect(results.objects('projects')).to match_array [project]
+ end
+
+ it 'returns result from descendants when search inside group' do
+ context = Search::GlobalService.new(user, search: project.path, group_id: nested_group.parent)
+ results = context.execute
+ expect(results.objects('projects')).to match_array [project]
+ end
+ end
end
end
end
diff --git a/spec/services/slash_commands/interpret_service_spec.rb b/spec/services/slash_commands/interpret_service_spec.rb
index becf627a4f5..0b0925983eb 100644
--- a/spec/services/slash_commands/interpret_service_spec.rb
+++ b/spec/services/slash_commands/interpret_service_spec.rb
@@ -1,12 +1,13 @@
require 'spec_helper'
describe SlashCommands::InterpretService, services: true do
- let(:project) { create(:empty_project, :public) }
+ let(:project) { create(:project, :public) }
let(:developer) { create(:user) }
let(:issue) { create(:issue, project: project) }
let(:milestone) { create(:milestone, project: project, title: '9.10') }
let(:inprogress) { create(:label, project: project, title: 'In Progress') }
let(:bug) { create(:label, project: project, title: 'Bug') }
+ let(:note) { build(:note, commit_id: merge_request.diff_head_sha) }
before do
project.team << [developer, :developer]
@@ -210,6 +211,46 @@ describe SlashCommands::InterpretService, services: true do
end
end
+ shared_examples 'estimate command' do
+ it 'populates time_estimate: 3600 if content contains /estimate 1h' do
+ _, updates = service.execute(content, issuable)
+
+ expect(updates).to eq(time_estimate: 3600)
+ end
+ end
+
+ shared_examples 'spend command' do
+ it 'populates spend_time: 3600 if content contains /spend 1h' do
+ _, updates = service.execute(content, issuable)
+
+ expect(updates).to eq(spend_time: { duration: 3600, user: developer })
+ end
+ end
+
+ shared_examples 'spend command with negative time' do
+ it 'populates spend_time: -1800 if content contains /spend -30m' do
+ _, updates = service.execute(content, issuable)
+
+ expect(updates).to eq(spend_time: { duration: -1800, user: developer })
+ end
+ end
+
+ shared_examples 'remove_estimate command' do
+ it 'populates time_estimate: 0 if content contains /remove_estimate' do
+ _, updates = service.execute(content, issuable)
+
+ expect(updates).to eq(time_estimate: 0)
+ end
+ end
+
+ shared_examples 'remove_time_spent command' do
+ it 'populates spend_time: :reset if content contains /remove_time_spent' do
+ _, updates = service.execute(content, issuable)
+
+ expect(updates).to eq(spend_time: { duration: :reset, user: developer })
+ end
+ end
+
shared_examples 'empty command' do
it 'populates {} if content contains an unsupported command' do
_, updates = service.execute(content, issuable)
@@ -218,6 +259,14 @@ describe SlashCommands::InterpretService, services: true do
end
end
+ shared_examples 'merge command' do
+ it 'runs merge command if content contains /merge' do
+ _, updates = service.execute(content, issuable)
+
+ expect(updates).to eq(merge: merge_request.diff_head_sha)
+ end
+ end
+
it_behaves_like 'reopen command' do
let(:content) { '/reopen' }
let(:issuable) { issue }
@@ -238,6 +287,64 @@ describe SlashCommands::InterpretService, services: true do
let(:issuable) { merge_request }
end
+ context 'merge command' do
+ let(:service) { described_class.new(project, developer, { merge_request_diff_head_sha: merge_request.diff_head_sha }) }
+
+ it_behaves_like 'merge command' do
+ let(:content) { '/merge' }
+ let(:issuable) { merge_request }
+ end
+
+ context 'can not be merged when logged user does not have permissions' do
+ let(:service) { described_class.new(project, create(:user)) }
+
+ it_behaves_like 'empty command' do
+ let(:content) { "/merge" }
+ let(:issuable) { merge_request }
+ end
+ end
+
+ context 'can not be merged when sha does not match' do
+ let(:service) { described_class.new(project, developer, { merge_request_diff_head_sha: 'othersha' }) }
+
+ it_behaves_like 'empty command' do
+ let(:content) { "/merge" }
+ let(:issuable) { merge_request }
+ end
+ end
+
+ context 'when sha is missing' do
+ let(:service) { described_class.new(project, developer, {}) }
+
+ it 'precheck passes and returns merge command' do
+ _, updates = service.execute('/merge', merge_request)
+
+ expect(updates).to eq(merge: nil)
+ end
+ end
+
+ context 'issue can not be merged' do
+ it_behaves_like 'empty command' do
+ let(:content) { "/merge" }
+ let(:issuable) { issue }
+ end
+ end
+
+ context 'non persisted merge request cant be merged' do
+ it_behaves_like 'empty command' do
+ let(:content) { "/merge" }
+ let(:issuable) { build(:merge_request) }
+ end
+ end
+
+ context 'not persisted merge request can not be merged' do
+ it_behaves_like 'empty command' do
+ let(:content) { "/merge" }
+ let(:issuable) { build(:merge_request, source_project: project) }
+ end
+ end
+ end
+
it_behaves_like 'title command' do
let(:content) { '/title A brand new title' }
let(:issuable) { issue }
@@ -451,6 +558,51 @@ describe SlashCommands::InterpretService, services: true do
let(:issuable) { merge_request }
end
+ it_behaves_like 'estimate command' do
+ let(:content) { '/estimate 1h' }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'empty command' do
+ let(:content) { '/estimate' }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'empty command' do
+ let(:content) { '/estimate abc' }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'spend command' do
+ let(:content) { '/spend 1h' }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'spend command with negative time' do
+ let(:content) { '/spend -30m' }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'empty command' do
+ let(:content) { '/spend' }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'empty command' do
+ let(:content) { '/spend abc' }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'remove_estimate command' do
+ let(:content) { '/remove_estimate' }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'remove_time_spent command' do
+ let(:content) { '/remove_time_spent' }
+ let(:issuable) { issue }
+ end
+
context 'when current_user cannot :admin_issue' do
let(:visitor) { create(:user) }
let(:issue) { create(:issue, project: project, author: visitor) }
@@ -501,5 +653,37 @@ describe SlashCommands::InterpretService, services: true do
let(:issuable) { issue }
end
end
+
+ context '/target_branch command' do
+ let(:non_empty_project) { create(:project) }
+ let(:another_merge_request) { create(:merge_request, author: developer, source_project: non_empty_project) }
+ let(:service) { described_class.new(non_empty_project, developer)}
+
+ it 'updates target_branch if /target_branch command is executed' do
+ _, updates = service.execute('/target_branch merge-test', merge_request)
+
+ expect(updates).to eq(target_branch: 'merge-test')
+ end
+
+ it 'handles blanks around param' do
+ _, updates = service.execute('/target_branch merge-test ', merge_request)
+
+ expect(updates).to eq(target_branch: 'merge-test')
+ end
+
+ context 'ignores command with no argument' do
+ it_behaves_like 'empty command' do
+ let(:content) { '/target_branch' }
+ let(:issuable) { another_merge_request }
+ end
+ end
+
+ context 'ignores non-existing target branch' do
+ it_behaves_like 'empty command' do
+ let(:content) { '/target_branch totally_non_existing_branch' }
+ let(:issuable) { another_merge_request }
+ end
+ end
+ end
end
end
diff --git a/spec/services/spam_service_spec.rb b/spec/services/spam_service_spec.rb
new file mode 100644
index 00000000000..271c17dd8c0
--- /dev/null
+++ b/spec/services/spam_service_spec.rb
@@ -0,0 +1,47 @@
+require 'spec_helper'
+
+describe SpamService, services: true do
+ describe '#check' do
+ let(:project) { create(:project, :public) }
+ let(:issue) { create(:issue, project: project) }
+ let(:request) { double(:request, env: {}) }
+
+ def check_spam(issue, request)
+ described_class.new(issue, request).check
+ end
+
+ context 'when indicated as spam by akismet' do
+ before { allow(AkismetService).to receive(:new).and_return(double(is_spam?: true)) }
+
+ it 'returns false when request is missing' do
+ expect(check_spam(issue, nil)).to be_falsey
+ end
+
+ it 'returns false when issue is not public' do
+ issue = create(:issue, project: create(:project, :private))
+
+ expect(check_spam(issue, request)).to be_falsey
+ end
+
+ it 'returns true' do
+ expect(check_spam(issue, request)).to be_truthy
+ end
+
+ it 'creates a spam log' do
+ expect { check_spam(issue, request) }.to change { SpamLog.count }.from(0).to(1)
+ end
+ end
+
+ context 'when not indicated as spam by akismet' do
+ before { allow(AkismetService).to receive(:new).and_return(double(is_spam?: false)) }
+
+ it 'returns false' do
+ expect(check_spam(issue, request)).to be_falsey
+ end
+
+ it 'does not create a spam log' do
+ expect { check_spam(issue, request) }.not_to change { SpamLog.count }
+ end
+ end
+ end
+end
diff --git a/spec/services/system_hooks_service_spec.rb b/spec/services/system_hooks_service_spec.rb
index fef211ded50..db9f1231682 100644
--- a/spec/services/system_hooks_service_spec.rb
+++ b/spec/services/system_hooks_service_spec.rb
@@ -12,6 +12,7 @@ describe SystemHooksService, services: true do
it { expect(event_data(user, :create)).to include(:event_name, :name, :created_at, :updated_at, :email, :user_id, :username) }
it { expect(event_data(user, :destroy)).to include(:event_name, :name, :created_at, :updated_at, :email, :user_id, :username) }
it { expect(event_data(project, :create)).to include(:event_name, :name, :created_at, :updated_at, :path, :project_id, :owner_name, :owner_email, :project_visibility) }
+ it { expect(event_data(project, :update)).to include(:event_name, :name, :created_at, :updated_at, :path, :project_id, :owner_name, :owner_email, :project_visibility) }
it { expect(event_data(project, :destroy)).to include(:event_name, :name, :created_at, :updated_at, :path, :project_id, :owner_name, :owner_email, :project_visibility) }
it { expect(event_data(project_member, :create)).to include(:event_name, :created_at, :updated_at, :project_name, :project_path, :project_path_with_namespace, :project_id, :user_name, :user_username, :user_email, :user_id, :access_level, :project_visibility) }
it { expect(event_data(project_member, :destroy)).to include(:event_name, :created_at, :updated_at, :project_name, :project_path, :project_path_with_namespace, :project_id, :user_name, :user_username, :user_email, :user_id, :access_level, :project_visibility) }
@@ -68,6 +69,7 @@ describe SystemHooksService, services: true do
it { expect(event_name(project, :destroy)).to eq "project_destroy" }
it { expect(event_name(project, :rename)).to eq "project_rename" }
it { expect(event_name(project, :transfer)).to eq "project_transfer" }
+ it { expect(event_name(project, :update)).to eq "project_update" }
it { expect(event_name(project_member, :create)).to eq "user_add_to_team" }
it { expect(event_name(project_member, :destroy)).to eq "user_remove_from_team" }
it { expect(event_name(key, :create)).to eq 'key_create' }
diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb
index 0e8adb68721..7f027ae02a2 100644
--- a/spec/services/system_note_service_spec.rb
+++ b/spec/services/system_note_service_spec.rb
@@ -245,6 +245,8 @@ describe SystemNoteService, services: true do
end
describe '.change_title' do
+ let(:noteable) { create(:issue, project: project, title: 'Lorem ipsum') }
+
subject { described_class.change_title(noteable, project, author, 'Old title') }
context 'when noteable responds to `title`' do
@@ -252,7 +254,7 @@ describe SystemNoteService, services: true do
it 'sets the note text' do
expect(subject.note).
- to eq "changed title from **{-Old title-}** to **{+#{noteable.title}+}**"
+ to eq "changed title from **{-Old title-}** to **{+Lorem ipsum+}**"
end
end
end
@@ -416,6 +418,45 @@ describe SystemNoteService, services: true do
to be_truthy
end
end
+
+ context 'when noteable is an Issue' do
+ let(:issue) { create(:issue, project: project) }
+
+ it 'is truthy when issue is closed' do
+ issue.close
+
+ expect(described_class.cross_reference_disallowed?(issue, project.commit)).
+ to be_truthy
+ end
+
+ it 'is falsey when issue is open' do
+ expect(described_class.cross_reference_disallowed?(issue, project.commit)).
+ to be_falsy
+ end
+ end
+
+ context 'when noteable is a Merge Request' do
+ let(:merge_request) { create(:merge_request, :simple, source_project: project) }
+
+ it 'is truthy when merge request is closed' do
+ allow(merge_request).to receive(:closed?).and_return(:true)
+
+ expect(described_class.cross_reference_disallowed?(merge_request, project.commit)).
+ to be_truthy
+ end
+
+ it 'is truthy when merge request is merged' do
+ allow(merge_request).to receive(:closed?).and_return(:true)
+
+ expect(described_class.cross_reference_disallowed?(merge_request, project.commit)).
+ to be_truthy
+ end
+
+ it 'is falsey when merge request is open' do
+ expect(described_class.cross_reference_disallowed?(merge_request, project.commit)).
+ to be_falsy
+ end
+ end
end
describe '.cross_reference_exists?' do
@@ -740,4 +781,92 @@ describe SystemNoteService, services: true do
expect(note.note).to include(issue.to_reference)
end
end
+
+ describe '.change_time_estimate' do
+ subject { described_class.change_time_estimate(noteable, project, author) }
+
+ it_behaves_like 'a system note'
+
+ context 'with a time estimate' do
+ it 'sets the note text' do
+ noteable.update_attribute(:time_estimate, 277200)
+
+ expect(subject.note).to eq "changed time estimate to 1w 4d 5h"
+ end
+ end
+
+ context 'without a time estimate' do
+ it 'sets the note text' do
+ expect(subject.note).to eq "removed time estimate"
+ end
+ end
+ end
+
+ describe '.change_time_spent' do
+ # We need a custom noteable in order to the shared examples to be green.
+ let(:noteable) do
+ mr = create(:merge_request, source_project: project)
+ mr.spend_time(duration: 360000, user: author)
+ mr.save!
+ mr
+ end
+
+ subject do
+ described_class.change_time_spent(noteable, project, author)
+ end
+
+ it_behaves_like 'a system note'
+
+ context 'when time was added' do
+ it 'sets the note text' do
+ spend_time!(277200)
+
+ expect(subject.note).to eq "added 1w 4d 5h of time spent"
+ end
+ end
+
+ context 'when time was subtracted' do
+ it 'sets the note text' do
+ spend_time!(-277200)
+
+ expect(subject.note).to eq "subtracted 1w 4d 5h of time spent"
+ end
+ end
+
+ context 'when time was removed' do
+ it 'sets the note text' do
+ spend_time!(:reset)
+
+ expect(subject.note).to eq "removed time spent"
+ end
+ end
+
+ def spend_time!(seconds)
+ noteable.spend_time(duration: seconds, user: author)
+ noteable.save!
+ end
+ end
+
+ describe '.add_merge_request_wip_from_commit' do
+ let(:noteable) do
+ create(:merge_request, source_project: project, target_project: project)
+ end
+
+ subject do
+ described_class.add_merge_request_wip_from_commit(
+ noteable,
+ project,
+ author,
+ noteable.diff_head_commit
+ )
+ end
+
+ it_behaves_like 'a system note'
+
+ it "posts the 'marked as a Work In Progress from commit' system note" do
+ expect(subject.note).to match(
+ /marked as a \*\*Work In Progress\*\* from #{Commit.reference_pattern}/
+ )
+ end
+ end
end
diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb
index ed55791d24e..13d584a8975 100644
--- a/spec/services/todo_service_spec.rb
+++ b/spec/services/todo_service_spec.rb
@@ -469,6 +469,13 @@ describe TodoService, services: true do
should_create_todo(user: author, target: mr_unassigned, action: Todo::BUILD_FAILED)
end
+
+ it 'creates a pending todo for merge_user' do
+ mr_unassigned.update(merge_when_build_succeeds: true, merge_user: admin)
+ service.merge_request_build_failed(mr_unassigned)
+
+ should_create_todo(user: admin, author: admin, target: mr_unassigned, action: Todo::BUILD_FAILED)
+ end
end
describe '#merge_request_push' do
@@ -482,6 +489,15 @@ describe TodoService, services: true do
end
end
+ describe '#merge_request_became_unmergeable' do
+ it 'creates a pending todo for a merge_user' do
+ mr_unassigned.update(merge_when_build_succeeds: true, merge_user: admin)
+ service.merge_request_became_unmergeable(mr_unassigned)
+
+ should_create_todo(user: admin, author: admin, target: mr_unassigned, action: Todo::UNMERGEABLE)
+ end
+ end
+
describe '#mark_todo' do
it 'creates a todo from a merge request' do
service.mark_todo(mr_unassigned, author)
diff --git a/spec/services/user_project_access_changed_service_spec.rb b/spec/services/user_project_access_changed_service_spec.rb
new file mode 100644
index 00000000000..b4efe7de431
--- /dev/null
+++ b/spec/services/user_project_access_changed_service_spec.rb
@@ -0,0 +1,12 @@
+require 'spec_helper'
+
+describe UserProjectAccessChangedService do
+ describe '#execute' do
+ it 'schedules the user IDs' do
+ expect(AuthorizedProjectsWorker).to receive(:bulk_perform_and_wait).
+ with([[1], [2]])
+
+ described_class.new([1, 2]).execute
+ end
+ end
+end
diff --git a/spec/services/users/refresh_authorized_projects_service_spec.rb b/spec/services/users/refresh_authorized_projects_service_spec.rb
index 72c8f7cd8ec..690fe979492 100644
--- a/spec/services/users/refresh_authorized_projects_service_spec.rb
+++ b/spec/services/users/refresh_authorized_projects_service_spec.rb
@@ -10,7 +10,21 @@ describe Users::RefreshAuthorizedProjectsService do
create!(project: project, user: user, access_level: access_level)
end
- describe '#execute' do
+ describe '#execute', :redis do
+ it 'refreshes the authorizations using a lease' do
+ expect_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain).
+ and_return('foo')
+
+ expect(Gitlab::ExclusiveLease).to receive(:cancel).
+ with(an_instance_of(String), 'foo')
+
+ expect(service).to receive(:execute_without_lease)
+
+ service.execute
+ end
+ end
+
+ describe '#execute_without_lease' do
before do
user.project_authorizations.delete_all
end
@@ -19,53 +33,64 @@ describe Users::RefreshAuthorizedProjectsService do
project2 = create(:empty_project)
to_remove = create_authorization(project2, user)
- expect(service).to receive(:update_with_lease).
- with([to_remove.id], [[user.id, project.id, Gitlab::Access::MASTER]])
+ expect(service).to receive(:update_authorizations).
+ with([to_remove.project_id], [[user.id, project.id, Gitlab::Access::MASTER]])
- service.execute
+ service.execute_without_lease
end
it 'sets the access level of a project to the highest available level' do
to_remove = create_authorization(project, user, Gitlab::Access::DEVELOPER)
- expect(service).to receive(:update_with_lease).
- with([to_remove.id], [[user.id, project.id, Gitlab::Access::MASTER]])
+ expect(service).to receive(:update_authorizations).
+ with([to_remove.project_id], [[user.id, project.id, Gitlab::Access::MASTER]])
- service.execute
+ service.execute_without_lease
end
it 'returns a User' do
- expect(service.execute).to be_an_instance_of(User)
+ expect(service.execute_without_lease).to be_an_instance_of(User)
end
end
- describe '#update_with_lease', :redis do
- it 'refreshes the authorizations using a lease' do
- expect_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain).
- and_return('foo')
+ describe '#update_authorizations' do
+ context 'when there are no rows to add and remove' do
+ it 'does not change authorizations' do
+ expect(user).not_to receive(:remove_project_authorizations)
+ expect(ProjectAuthorization).not_to receive(:insert_authorizations)
- expect(Gitlab::ExclusiveLease).to receive(:cancel).
- with(an_instance_of(String), 'foo')
+ service.update_authorizations([], [])
+ end
- expect(service).to receive(:update_authorizations).with([1], [])
+ context 'when the authorized projects column is not set' do
+ before do
+ user.update!(authorized_projects_populated: nil)
+ end
- service.update_with_lease([1])
- end
- end
+ it 'populates the authorized projects column' do
+ service.update_authorizations([], [])
- describe '#update_authorizations' do
- it 'does nothing when there are no rows to add and remove' do
- expect(user).not_to receive(:remove_project_authorizations)
- expect(ProjectAuthorization).not_to receive(:insert_authorizations)
- expect(user).not_to receive(:set_authorized_projects_column)
+ expect(user.authorized_projects_populated).to eq true
+ end
+ end
+
+ context 'when the authorized projects column is set' do
+ before do
+ user.update!(authorized_projects_populated: true)
+ end
- service.update_authorizations([], [])
+ it 'does nothing' do
+ expect(user).not_to receive(:set_authorized_projects_column)
+
+ service.update_authorizations([], [])
+ end
+ end
end
it 'removes authorizations that should be removed' do
authorization = create_authorization(project, user)
- service.update_authorizations([authorization.id])
+ service.update_authorizations([authorization.project_id])
expect(user.project_authorizations).to be_empty
end
@@ -84,7 +109,7 @@ describe Users::RefreshAuthorizedProjectsService do
it 'populates the authorized projects column' do
# make sure we start with a nil value no matter what the default in the
# factory may be.
- user.update(authorized_projects_populated: nil)
+ user.update!(authorized_projects_populated: nil)
service.update_authorizations([], [[user.id, project.id, Gitlab::Access::MASTER]])
@@ -122,7 +147,12 @@ describe Users::RefreshAuthorizedProjectsService do
end
it 'sets the values to the project authorization rows' do
- expect(hash.values).to eq([ProjectAuthorization.first])
+ expect(hash.values.length).to eq(1)
+
+ value = hash.values[0]
+
+ expect(value.project_id).to eq(project.id)
+ expect(value.access_level).to eq(Gitlab::Access::MASTER)
end
end
@@ -142,10 +172,6 @@ describe Users::RefreshAuthorizedProjectsService do
expect(service.current_authorizations.length).to eq(1)
end
- it 'includes the row ID for every row' do
- expect(row.id).to be_a_kind_of(Numeric)
- end
-
it 'includes the project ID for every row' do
expect(row.project_id).to eq(project.id)
end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 6ee3307512d..5fda7c63cdb 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -2,13 +2,17 @@ require './spec/simplecov_env'
SimpleCovEnv.start!
ENV["RAILS_ENV"] ||= 'test'
+ENV["IN_MEMORY_APPLICATION_SETTINGS"] = 'true'
require File.expand_path("../../config/environment", __FILE__)
require 'rspec/rails'
require 'shoulda/matchers'
-require 'sidekiq/testing/inline'
require 'rspec/retry'
+if ENV['RSPEC_PROFILING_POSTGRES_URL'] || ENV['RSPEC_PROFILING']
+ require 'rspec_profiling/rspec'
+end
+
if ENV['CI'] && !ENV['NO_KNAPSACK']
require 'knapsack'
Knapsack::Adapters::RSpecAdapter.bind
@@ -31,6 +35,7 @@ RSpec.configure do |config|
config.include Warden::Test::Helpers, type: :request
config.include LoginHelpers, type: :feature
config.include SearchHelpers, type: :feature
+ config.include WaitForAjax, type: :feature
config.include StubConfiguration
config.include EmailHelpers, type: :mailer
config.include TestEnv
diff --git a/spec/support/api/time_tracking_shared_examples.rb b/spec/support/api/time_tracking_shared_examples.rb
new file mode 100644
index 00000000000..210cd5817e0
--- /dev/null
+++ b/spec/support/api/time_tracking_shared_examples.rb
@@ -0,0 +1,132 @@
+shared_examples 'an unauthorized API user' do
+ it { is_expected.to eq(403) }
+end
+
+shared_examples 'time tracking endpoints' do |issuable_name|
+ issuable_collection_name = issuable_name.pluralize
+
+ describe "POST /projects/:id/#{issuable_collection_name}/:#{issuable_name}_id/time_estimate" do
+ context 'with an unauthorized user' do
+ subject { post(api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_estimate", non_member), duration: '1w') }
+
+ it_behaves_like 'an unauthorized API user'
+ end
+
+ it "sets the time estimate for #{issuable_name}" do
+ post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_estimate", user), duration: '1w'
+
+ expect(response).to have_http_status(200)
+ expect(json_response['human_time_estimate']).to eq('1w')
+ end
+
+ describe 'updating the current estimate' do
+ before do
+ post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_estimate", user), duration: '1w'
+ end
+
+ context 'when duration has a bad format' do
+ it 'does not modify the original estimate' do
+ post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_estimate", user), duration: 'foo'
+
+ expect(response).to have_http_status(400)
+ expect(issuable.reload.human_time_estimate).to eq('1w')
+ end
+ end
+
+ context 'with a valid duration' do
+ it 'updates the estimate' do
+ post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_estimate", user), duration: '3w1h'
+
+ expect(response).to have_http_status(200)
+ expect(issuable.reload.human_time_estimate).to eq('3w 1h')
+ end
+ end
+ end
+ end
+
+ describe "POST /projects/:id/#{issuable_collection_name}/:#{issuable_name}_id/reset_time_estimate" do
+ context 'with an unauthorized user' do
+ subject { post(api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/reset_time_estimate", non_member)) }
+
+ it_behaves_like 'an unauthorized API user'
+ end
+
+ it "resets the time estimate for #{issuable_name}" do
+ post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/reset_time_estimate", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['time_estimate']).to eq(0)
+ end
+ end
+
+ describe "POST /projects/:id/#{issuable_collection_name}/:#{issuable_name}_id/add_spent_time" do
+ context 'with an unauthorized user' do
+ subject do
+ post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/add_spent_time", non_member),
+ duration: '2h'
+ end
+
+ it_behaves_like 'an unauthorized API user'
+ end
+
+ it "add spent time for #{issuable_name}" do
+ post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/add_spent_time", user),
+ duration: '2h'
+
+ expect(response).to have_http_status(201)
+ expect(json_response['human_total_time_spent']).to eq('2h')
+ end
+
+ context 'when subtracting time' do
+ it 'subtracts time of the total spent time' do
+ issuable.update_attributes!(spend_time: { duration: 7200, user: user })
+
+ post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/add_spent_time", user),
+ duration: '-1h'
+
+ expect(response).to have_http_status(201)
+ expect(json_response['total_time_spent']).to eq(3600)
+ end
+ end
+
+ context 'when time to subtract is greater than the total spent time' do
+ it 'does not modify the total time spent' do
+ issuable.update_attributes!(spend_time: { duration: 7200, user: user })
+
+ post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/add_spent_time", user),
+ duration: '-1w'
+
+ expect(response).to have_http_status(400)
+ expect(json_response['message']['time_spent'].first).to match(/exceeds the total time spent/)
+ end
+ end
+ end
+
+ describe "POST /projects/:id/#{issuable_collection_name}/:#{issuable_name}_id/reset_spent_time" do
+ context 'with an unauthorized user' do
+ subject { post(api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/reset_spent_time", non_member)) }
+
+ it_behaves_like 'an unauthorized API user'
+ end
+
+ it "resets spent time for #{issuable_name}" do
+ post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/reset_spent_time", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['total_time_spent']).to eq(0)
+ end
+ end
+
+ describe "GET /projects/:id/#{issuable_collection_name}/:#{issuable_name}_id/time_stats" do
+ it "returns the time stats for #{issuable_name}" do
+ issuable.update_attributes!(spend_time: { duration: 1800, user: user },
+ time_estimate: 3600)
+
+ get api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_stats", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['total_time_spent']).to eq(1800)
+ expect(json_response['time_estimate']).to eq(3600)
+ end
+ end
+end
diff --git a/spec/support/api_helpers.rb b/spec/support/api_helpers.rb
index 68b196d9033..ae6e708cf87 100644
--- a/spec/support/api_helpers.rb
+++ b/spec/support/api_helpers.rb
@@ -17,8 +17,8 @@ module ApiHelpers
# => "/api/v2/issues?foo=bar&private_token=..."
#
# Returns the relative path to the requested API resource
- def api(path, user = nil)
- "/api/#{API::API.version}#{path}" +
+ def api(path, user = nil, version: API::API.version)
+ "/api/#{version}#{path}" +
# Normalize query string
(path.index('?') ? '' : '?') +
@@ -31,6 +31,11 @@ module ApiHelpers
end
end
+ # Temporary helper method for simplifying V3 exclusive API specs
+ def v3_api(path, user = nil)
+ api(path, user, version: 'v3')
+ end
+
def ci_api(path, user = nil)
"/ci/api/v1/#{path}" +
diff --git a/spec/support/cycle_analytics_helpers.rb b/spec/support/cycle_analytics_helpers.rb
index 75c95d70951..6ed55289ed9 100644
--- a/spec/support/cycle_analytics_helpers.rb
+++ b/spec/support/cycle_analytics_helpers.rb
@@ -35,7 +35,13 @@ module CycleAnalyticsHelpers
project.repository.add_branch(user, source_branch, 'master')
end
- sha = project.repository.commit_file(user, random_git_name, "content", "commit message", source_branch, false)
+ sha = project.repository.commit_file(
+ user,
+ random_git_name,
+ 'content',
+ message: 'commit message',
+ branch_name: source_branch,
+ update: false)
project.repository.commit(sha)
opts = {
diff --git a/spec/support/cycle_analytics_helpers/test_generation.rb b/spec/support/cycle_analytics_helpers/test_generation.rb
index 8e19a6c92e2..19b32c84d81 100644
--- a/spec/support/cycle_analytics_helpers/test_generation.rb
+++ b/spec/support/cycle_analytics_helpers/test_generation.rb
@@ -2,7 +2,6 @@
# Note: The ABC size is large here because we have a method generating test cases with
# multiple nested contexts. This shouldn't count as a violation.
-
module CycleAnalyticsHelpers
module TestGeneration
# Generate the most common set of specs that all cycle analytics phases need to have.
@@ -51,11 +50,11 @@ module CycleAnalyticsHelpers
end
median_time_difference = time_differences.sort[2]
- expect(subject.send(phase)).to be_within(5).of(median_time_difference)
+ expect(subject[phase].median).to be_within(5).of(median_time_difference)
end
context "when the data belongs to another project" do
- let(:other_project) { create(:project) }
+ let(:other_project) { create(:project, :repository) }
it "returns nil" do
# Use a stub to "trick" the data/condition functions
@@ -64,26 +63,24 @@ module CycleAnalyticsHelpers
# test case.
allow(self).to receive(:project) { other_project }
- 5.times do
- data = data_fn[self]
- start_time = Time.now
- end_time = rand(1..10).days.from_now
-
- start_time_conditions.each do |condition_name, condition_fn|
- Timecop.freeze(start_time) { condition_fn[self, data] }
- end
+ data = data_fn[self]
+ start_time = Time.now
+ end_time = rand(1..10).days.from_now
- end_time_conditions.each do |condition_name, condition_fn|
- Timecop.freeze(end_time) { condition_fn[self, data] }
- end
+ start_time_conditions.each do |condition_name, condition_fn|
+ Timecop.freeze(start_time) { condition_fn[self, data] }
+ end
- Timecop.freeze(end_time + 1.day) { post_fn[self, data] } if post_fn
+ end_time_conditions.each do |condition_name, condition_fn|
+ Timecop.freeze(end_time) { condition_fn[self, data] }
end
+ Timecop.freeze(end_time + 1.day) { post_fn[self, data] } if post_fn
+
# Turn off the stub before checking assertions
allow(self).to receive(:project).and_call_original
- expect(subject.send(phase)).to be_nil
+ expect(subject[phase].median).to be_nil
end
end
@@ -106,7 +103,7 @@ module CycleAnalyticsHelpers
Timecop.freeze(end_time + 1.day) { post_fn[self, data] } if post_fn
- expect(subject.send(phase)).to be_nil
+ expect(subject[phase].median).to be_nil
end
end
end
@@ -115,18 +112,16 @@ module CycleAnalyticsHelpers
context "start condition NOT PRESENT: #{start_time_conditions.map(&:first).to_sentence}" do
context "end condition: #{end_time_conditions.map(&:first).to_sentence}" do
it "returns nil" do
- 5.times do
- data = data_fn[self]
- end_time = rand(1..10).days.from_now
-
- end_time_conditions.each_with_index do |(condition_name, condition_fn), index|
- Timecop.freeze(end_time + index.days) { condition_fn[self, data] }
- end
+ data = data_fn[self]
+ end_time = rand(1..10).days.from_now
- Timecop.freeze(end_time + 1.day) { post_fn[self, data] } if post_fn
+ end_time_conditions.each_with_index do |(condition_name, condition_fn), index|
+ Timecop.freeze(end_time + index.days) { condition_fn[self, data] }
end
- expect(subject.send(phase)).to be_nil
+ Timecop.freeze(end_time + 1.day) { post_fn[self, data] } if post_fn
+
+ expect(subject[phase].median).to be_nil
end
end
end
@@ -134,18 +129,16 @@ module CycleAnalyticsHelpers
context "start condition: #{start_time_conditions.map(&:first).to_sentence}" do
context "end condition NOT PRESENT: #{end_time_conditions.map(&:first).to_sentence}" do
it "returns nil" do
- 5.times do
- data = data_fn[self]
- start_time = Time.now
-
- start_time_conditions.each do |condition_name, condition_fn|
- Timecop.freeze(start_time) { condition_fn[self, data] }
- end
+ data = data_fn[self]
+ start_time = Time.now
- post_fn[self, data] if post_fn
+ start_time_conditions.each do |condition_name, condition_fn|
+ Timecop.freeze(start_time) { condition_fn[self, data] }
end
- expect(subject.send(phase)).to be_nil
+ post_fn[self, data] if post_fn
+
+ expect(subject[phase].median).to be_nil
end
end
end
@@ -153,7 +146,7 @@ module CycleAnalyticsHelpers
context "when none of the start / end conditions are matched" do
it "returns nil" do
- expect(subject.send(phase)).to be_nil
+ expect(subject[phase].median).to be_nil
end
end
end
diff --git a/spec/support/fake_u2f_device.rb b/spec/support/fake_u2f_device.rb
index 8c407b867fe..a7605cd483a 100644
--- a/spec/support/fake_u2f_device.rb
+++ b/spec/support/fake_u2f_device.rb
@@ -5,7 +5,7 @@ class FakeU2fDevice
@page = page
@name = name
end
-
+
def respond_to_u2f_registration
app_id = @page.evaluate_script('gon.u2f.app_id')
challenges = @page.evaluate_script('gon.u2f.challenges')
@@ -28,6 +28,7 @@ class FakeU2fDevice
u2f.sign = function(appId, challenges, signRequests, callback) {
callback(#{json_response});
};
+ window.gl.u2fAuthenticate.start();
")
end
diff --git a/spec/support/features/issuable_slash_commands_shared_examples.rb b/spec/support/features/issuable_slash_commands_shared_examples.rb
index 194620d0a68..a4713e53f63 100644
--- a/spec/support/features/issuable_slash_commands_shared_examples.rb
+++ b/spec/support/features/issuable_slash_commands_shared_examples.rb
@@ -76,7 +76,7 @@ shared_examples 'issuable record that supports slash commands in its description
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!'
+ expect(page).to have_content 'Commands applied'
issuable.reload
@@ -97,7 +97,7 @@ shared_examples 'issuable record that supports slash commands in its description
write_note("/close")
expect(page).not_to have_content '/close'
- expect(page).to have_content 'Your commands have been executed!'
+ expect(page).to have_content 'Commands applied'
expect(issuable.reload).to be_closed
end
@@ -114,7 +114,7 @@ shared_examples 'issuable record that supports slash commands in its description
write_note("/close")
expect(page).not_to have_content '/close'
- expect(page).not_to have_content 'Your commands have been executed!'
+ expect(page).not_to have_content 'Commands applied'
expect(issuable).to be_open
end
@@ -132,7 +132,7 @@ shared_examples 'issuable record that supports slash commands in its description
write_note("/reopen")
expect(page).not_to have_content '/reopen'
- expect(page).to have_content 'Your commands have been executed!'
+ expect(page).to have_content 'Commands applied'
expect(issuable.reload).to be_open
end
@@ -149,7 +149,7 @@ shared_examples 'issuable record that supports slash commands in its description
write_note("/reopen")
expect(page).not_to have_content '/reopen'
- expect(page).not_to have_content 'Your commands have been executed!'
+ expect(page).not_to have_content 'Commands applied'
expect(issuable).to be_closed
end
@@ -162,7 +162,7 @@ shared_examples 'issuable record that supports slash commands in its description
write_note("/title Awesome new title")
expect(page).not_to have_content '/title'
- expect(page).to have_content 'Your commands have been executed!'
+ expect(page).to have_content 'Commands applied'
expect(issuable.reload.title).to eq 'Awesome new title'
end
@@ -179,7 +179,7 @@ shared_examples 'issuable record that supports slash commands in its description
write_note("/title Awesome new title")
expect(page).not_to have_content '/title'
- expect(page).not_to have_content 'Your commands have been executed!'
+ expect(page).not_to have_content 'Commands applied'
expect(issuable.reload.title).not_to eq 'Awesome new title'
end
@@ -191,7 +191,7 @@ shared_examples 'issuable record that supports slash commands in its description
write_note("/todo")
expect(page).not_to have_content '/todo'
- expect(page).to have_content 'Your commands have been executed!'
+ expect(page).to have_content 'Commands applied'
todos = TodosFinder.new(master).execute
todo = todos.first
@@ -222,7 +222,7 @@ shared_examples 'issuable record that supports slash commands in its description
write_note("/done")
expect(page).not_to have_content '/done'
- expect(page).to have_content 'Your commands have been executed!'
+ expect(page).to have_content 'Commands applied'
expect(todo.reload).to be_done
end
@@ -235,7 +235,7 @@ shared_examples 'issuable record that supports slash commands in its description
write_note("/subscribe")
expect(page).not_to have_content '/subscribe'
- expect(page).to have_content 'Your commands have been executed!'
+ expect(page).to have_content 'Commands applied'
expect(issuable.subscribed?(master, project)).to be_truthy
end
@@ -252,7 +252,7 @@ shared_examples 'issuable record that supports slash commands in its description
write_note("/unsubscribe")
expect(page).not_to have_content '/unsubscribe'
- expect(page).to have_content 'Your commands have been executed!'
+ expect(page).to have_content 'Commands applied'
expect(issuable.subscribed?(master, project)).to be_falsy
end
diff --git a/spec/support/import_export/export_file_helper.rb b/spec/support/import_export/export_file_helper.rb
index 1b0a4583f5c..944ea30656f 100644
--- a/spec/support/import_export/export_file_helper.rb
+++ b/spec/support/import_export/export_file_helper.rb
@@ -35,7 +35,7 @@ module ExportFileHelper
project: project,
commit_id: ci_pipeline.sha)
- create(:event, target: milestone, project: project, action: Event::CREATED, author: user)
+ create(:event, :created, target: milestone, project: project, author: user)
create(:project_member, :master, user: user, project: project)
create(:ci_variable, project: project)
create(:ci_trigger, project: project)
diff --git a/spec/support/javascript_fixtures_helpers.rb b/spec/support/javascript_fixtures_helpers.rb
index 99e98eebdb4..0b8729db0f9 100644
--- a/spec/support/javascript_fixtures_helpers.rb
+++ b/spec/support/javascript_fixtures_helpers.rb
@@ -20,12 +20,26 @@ module JavaScriptFixturesHelpers
# Public: Store a response object as fixture file
#
- # response - response object to store
+ # response - string or response object to store
# fixture_file_name - file name to store the fixture in (relative to FIXTURE_PATH)
#
def store_frontend_fixture(response, fixture_file_name)
fixture_file_name = File.expand_path(fixture_file_name, FIXTURE_PATH)
+ fixture = response.respond_to?(:body) ? parse_response(response) : response
+
+ FileUtils.mkdir_p(File.dirname(fixture_file_name))
+ File.write(fixture_file_name, fixture)
+ end
+
+ private
+
+ # Private: Prepare a response object for use as a frontend fixture
+ #
+ # response - response object to prepare
+ #
+ def parse_response(response)
fixture = response.body
+ fixture.force_encoding("utf-8")
response_mime_type = Mime::Type.lookup(response.content_type)
if response_mime_type.html?
@@ -34,7 +48,7 @@ module JavaScriptFixturesHelpers
link_tags = doc.css('link')
link_tags.remove
- scripts = doc.css('script')
+ scripts = doc.css("script:not([type='text/template'])")
scripts.remove
fixture = doc.to_html
@@ -44,7 +58,6 @@ module JavaScriptFixturesHelpers
fixture.gsub!(%r{="/}, "=\"http://#{test_host}/")
end
- FileUtils.mkdir_p(File.dirname(fixture_file_name))
- File.write(fixture_file_name, fixture)
+ fixture
end
end
diff --git a/spec/support/kubernetes_helpers.rb b/spec/support/kubernetes_helpers.rb
index 6c4c246a68b..444612cf871 100644
--- a/spec/support/kubernetes_helpers.rb
+++ b/spec/support/kubernetes_helpers.rb
@@ -43,7 +43,8 @@ module KubernetesHelpers
url: container_exec_url(service.api_url, service.namespace, pod_name, container['name']),
subprotocols: ['channel.k8s.io'],
headers: { 'Authorization' => ["Bearer #{service.token}"] },
- created_at: DateTime.parse(pod['metadata']['creationTimestamp'])
+ created_at: DateTime.parse(pod['metadata']['creationTimestamp']),
+ max_session_time: 0
}
terminal[:ca_pem] = service.ca_pem if service.ca_pem.present?
terminal
diff --git a/spec/support/matchers/be_valid_commit.rb b/spec/support/matchers/be_valid_commit.rb
new file mode 100644
index 00000000000..3696e4d5f03
--- /dev/null
+++ b/spec/support/matchers/be_valid_commit.rb
@@ -0,0 +1,8 @@
+RSpec::Matchers.define :be_valid_commit do
+ match do |actual|
+ actual &&
+ actual.id == SeedRepo::Commit::ID &&
+ actual.message == SeedRepo::Commit::MESSAGE &&
+ actual.author_name == SeedRepo::Commit::AUTHOR_FULL_NAME
+ end
+end
diff --git a/spec/support/matchers/match_file.rb b/spec/support/matchers/match_file.rb
new file mode 100644
index 00000000000..d1888b3376a
--- /dev/null
+++ b/spec/support/matchers/match_file.rb
@@ -0,0 +1,5 @@
+RSpec::Matchers.define :match_file do |expected|
+ match do |actual|
+ expect(Digest::MD5.hexdigest(actual)).to eq(Digest::MD5.hexdigest(File.read(expected)))
+ end
+end
diff --git a/spec/support/mentionable_shared_examples.rb b/spec/support/mentionable_shared_examples.rb
index f57c82809a6..87936bb4859 100644
--- a/spec/support/mentionable_shared_examples.rb
+++ b/spec/support/mentionable_shared_examples.rb
@@ -12,7 +12,7 @@ shared_context 'mentionable context' do
let!(:mentioned_mr) { create(:merge_request, source_project: project) }
let(:mentioned_commit) { project.commit("HEAD~1") }
- let(:ext_proj) { create(:project, :public) }
+ let(:ext_proj) { create(:project, :public, :repository) }
let(:ext_issue) { create(:issue, project: ext_proj) }
let(:ext_mr) { create(:merge_request, :simple, source_project: ext_proj) }
let(:ext_commit) { ext_proj.commit("HEAD~2") }
diff --git a/spec/support/mobile_helpers.rb b/spec/support/mobile_helpers.rb
new file mode 100644
index 00000000000..20d5849bcab
--- /dev/null
+++ b/spec/support/mobile_helpers.rb
@@ -0,0 +1,13 @@
+module MobileHelpers
+ def resize_screen_sm
+ resize_window(900, 768)
+ end
+
+ def restore_window_size
+ resize_window(1366, 768)
+ end
+
+ def resize_window(width, height)
+ page.driver.resize_window width, height
+ end
+end
diff --git a/spec/support/notify_shared_examples.rb b/spec/support/notify_shared_examples.rb
index 49867aa5cc4..a3724b801b3 100644
--- a/spec/support/notify_shared_examples.rb
+++ b/spec/support/notify_shared_examples.rb
@@ -179,9 +179,24 @@ shared_examples 'it should show Gmail Actions View Commit link' do
end
shared_examples 'an unsubscribeable thread' do
+ it_behaves_like 'an unsubscribeable thread with incoming address without %{key}'
+
+ it 'has a List-Unsubscribe header in the correct format' do
+ is_expected.to have_header 'List-Unsubscribe', /unsubscribe/
+ is_expected.to have_header 'List-Unsubscribe', /mailto/
+ is_expected.to have_header 'List-Unsubscribe', /^<.+,.+>$/
+ end
+
+ it { is_expected.to have_body_text /unsubscribe/ }
+end
+
+shared_examples 'an unsubscribeable thread with incoming address without %{key}' do
+ include_context 'reply-by-email is enabled with incoming address without %{key}'
+
it 'has a List-Unsubscribe header in the correct format' do
is_expected.to have_header 'List-Unsubscribe', /unsubscribe/
- is_expected.to have_header 'List-Unsubscribe', /^<.+>$/
+ is_expected.not_to have_header 'List-Unsubscribe', /mailto/
+ is_expected.to have_header 'List-Unsubscribe', /^<[^,]+>$/
end
it { is_expected.to have_body_text /unsubscribe/ }
diff --git a/spec/support/reactive_caching_helpers.rb b/spec/support/reactive_caching_helpers.rb
index 279db3c5748..98eb57f8b54 100644
--- a/spec/support/reactive_caching_helpers.rb
+++ b/spec/support/reactive_caching_helpers.rb
@@ -3,31 +3,35 @@ module ReactiveCachingHelpers
([subject.class.reactive_cache_key.call(subject)].flatten + qualifiers).join(':')
end
- def stub_reactive_cache(subject = nil, data = nil)
+ def alive_reactive_cache_key(subject, *qualifiers)
+ reactive_cache_key(subject, *(qualifiers + ['alive']))
+ end
+
+ def stub_reactive_cache(subject = nil, data = nil, *qualifiers)
allow(ReactiveCachingWorker).to receive(:perform_async)
allow(ReactiveCachingWorker).to receive(:perform_in)
- write_reactive_cache(subject, data) if data
+ write_reactive_cache(subject, data, *qualifiers) if data
end
- def read_reactive_cache(subject)
- Rails.cache.read(reactive_cache_key(subject))
+ def read_reactive_cache(subject, *qualifiers)
+ Rails.cache.read(reactive_cache_key(subject, *qualifiers))
end
- def write_reactive_cache(subject, data)
- start_reactive_cache_lifetime(subject)
- Rails.cache.write(reactive_cache_key(subject), data)
+ def write_reactive_cache(subject, data, *qualifiers)
+ start_reactive_cache_lifetime(subject, *qualifiers)
+ Rails.cache.write(reactive_cache_key(subject, *qualifiers), data)
end
- def reactive_cache_alive?(subject)
- Rails.cache.read(reactive_cache_key(subject, 'alive'))
+ def reactive_cache_alive?(subject, *qualifiers)
+ Rails.cache.read(alive_reactive_cache_key(subject, *qualifiers))
end
- def invalidate_reactive_cache(subject)
- Rails.cache.delete(reactive_cache_key(subject, 'alive'))
+ def invalidate_reactive_cache(subject, *qualifiers)
+ Rails.cache.delete(alive_reactive_cache_key(subject, *qualifiers))
end
- def start_reactive_cache_lifetime(subject)
- Rails.cache.write(reactive_cache_key(subject, 'alive'), true)
+ def start_reactive_cache_lifetime(subject, *qualifiers)
+ Rails.cache.write(alive_reactive_cache_key(subject, *qualifiers), true)
end
def expect_reactive_cache_update_queued(subject)
diff --git a/spec/support/seed_helper.rb b/spec/support/seed_helper.rb
new file mode 100644
index 00000000000..03fa0a66b9a
--- /dev/null
+++ b/spec/support/seed_helper.rb
@@ -0,0 +1,112 @@
+# This file is specific to specs in spec/lib/gitlab/git/
+
+SEED_REPOSITORY_PATH = File.expand_path('../../tmp/repositories', __dir__)
+TEST_REPO_PATH = File.join(SEED_REPOSITORY_PATH, 'gitlab-git-test.git')
+TEST_NORMAL_REPO_PATH = File.join(SEED_REPOSITORY_PATH, "not-bare-repo.git")
+TEST_MUTABLE_REPO_PATH = File.join(SEED_REPOSITORY_PATH, "mutable-repo.git")
+TEST_BROKEN_REPO_PATH = File.join(SEED_REPOSITORY_PATH, "broken-repo.git")
+
+module SeedHelper
+ GITLAB_URL = "https://gitlab.com/gitlab-org/gitlab-git-test.git"
+
+ def ensure_seeds
+ if File.exist?(SEED_REPOSITORY_PATH)
+ FileUtils.rm_r(SEED_REPOSITORY_PATH)
+ end
+
+ FileUtils.mkdir_p(SEED_REPOSITORY_PATH)
+
+ create_bare_seeds
+ create_normal_seeds
+ create_mutable_seeds
+ create_broken_seeds
+ create_git_attributes
+ create_invalid_git_attributes
+ end
+
+ def create_bare_seeds
+ system(git_env, *%W(#{Gitlab.config.git.bin_path} clone --bare #{GITLAB_URL}),
+ chdir: SEED_REPOSITORY_PATH,
+ out: '/dev/null',
+ err: '/dev/null')
+ end
+
+ def create_normal_seeds
+ system(git_env, *%W(#{Gitlab.config.git.bin_path} clone #{TEST_REPO_PATH} #{TEST_NORMAL_REPO_PATH}),
+ out: '/dev/null',
+ err: '/dev/null')
+ end
+
+ def create_mutable_seeds
+ system(git_env, *%W(#{Gitlab.config.git.bin_path} clone #{TEST_REPO_PATH} #{TEST_MUTABLE_REPO_PATH}),
+ out: '/dev/null',
+ err: '/dev/null')
+
+ system(git_env, *%w(git branch -t feature origin/feature),
+ chdir: TEST_MUTABLE_REPO_PATH, out: '/dev/null', err: '/dev/null')
+
+ system(git_env, *%W(#{Gitlab.config.git.bin_path} remote add expendable #{GITLAB_URL}),
+ chdir: TEST_MUTABLE_REPO_PATH, out: '/dev/null', err: '/dev/null')
+ end
+
+ def create_broken_seeds
+ system(git_env, *%W(#{Gitlab.config.git.bin_path} clone --bare #{TEST_REPO_PATH} #{TEST_BROKEN_REPO_PATH}),
+ out: '/dev/null',
+ err: '/dev/null')
+
+ refs_path = File.join(TEST_BROKEN_REPO_PATH, 'refs')
+
+ FileUtils.rm_r(refs_path)
+ end
+
+ def create_git_attributes
+ dir = File.join(SEED_REPOSITORY_PATH, 'with-git-attributes.git', 'info')
+
+ FileUtils.mkdir_p(dir)
+
+ File.open(File.join(dir, 'attributes'), 'w') do |handle|
+ handle.write <<-EOF.strip
+# This is a comment, it should be ignored.
+
+*.txt text
+*.jpg -text
+*.sh eol=lf gitlab-language=shell
+*.haml.* gitlab-language=haml
+foo/bar.* foo
+*.cgi key=value?p1=v1&p2=v2
+/*.png gitlab-language=png
+*.binary binary
+
+# This uses a tab instead of spaces to ensure the parser also supports this.
+*.md\tgitlab-language=markdown
+bla/bla.txt
+ EOF
+ end
+ end
+
+ def create_invalid_git_attributes
+ dir = File.join(SEED_REPOSITORY_PATH, 'with-invalid-git-attributes.git', 'info')
+
+ FileUtils.mkdir_p(dir)
+
+ enc = Encoding::UTF_16
+
+ File.open(File.join(dir, 'attributes'), 'w', encoding: enc) do |handle|
+ handle.write('# hello'.encode(enc))
+ end
+ end
+
+ # Prevent developer git configurations from being persisted to test
+ # repositories
+ def git_env
+ { 'GIT_TEMPLATE_DIR' => '' }
+ end
+end
+
+RSpec.configure do |config|
+ config.include SeedHelper, :seed_helper
+
+ config.before(:all, :seed_helper) do
+ ensure_seeds
+ end
+end
diff --git a/spec/support/seed_repo.rb b/spec/support/seed_repo.rb
new file mode 100644
index 00000000000..9f2cd7c67c5
--- /dev/null
+++ b/spec/support/seed_repo.rb
@@ -0,0 +1,143 @@
+# Seed repo:
+# 0e50ec4d3c7ce42ab74dda1d422cb2cbffe1e326 Merge branch 'lfs_pointers' into 'master'
+# 33bcff41c232a11727ac6d660bd4b0c2ba86d63d Add valid and invalid lfs pointers
+# 732401c65e924df81435deb12891ef570167d2e2 Update year in license file
+# b0e52af38d7ea43cf41d8a6f2471351ac036d6c9 Empty commit
+# 40f4a7a617393735a95a0bb67b08385bc1e7c66d Add ISO-8859-encoded file
+# 66028349a123e695b589e09a36634d976edcc5e8 Merge branch 'add-comments-to-gitmodules' into 'master'
+# de5714f34c4e34f1d50b9a61a2e6c9132fe2b5fd Add comments to the end of .gitmodules to test parsing
+# fa1b1e6c004a68b7d8763b86455da9e6b23e36d6 Merge branch 'add-files' into 'master'
+# eb49186cfa5c4338011f5f590fac11bd66c5c631 Add submodules nested deeper than the root
+# 18d9c205d0d22fdf62bc2f899443b83aafbf941f Add executables and links files
+# 5937ac0a7beb003549fc5fd26fc247adbce4a52e Add submodule from gitlab.com
+# 570e7b2abdd848b95f2f578043fc23bd6f6fd24d Change some files
+# 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 More submodules
+# d14d6c0abdd253381df51a723d58691b2ee1ab08 Remove ds_store files
+# c1acaa58bbcbc3eafe538cb8274ba387047b69f8 Ignore DS files
+# ae73cb07c9eeaf35924a10f713b364d32b2dd34f Binary file added
+# 874797c3a73b60d2187ed6e2fcabd289ff75171e Ruby files modified
+# 2f63565e7aac07bcdadb654e253078b727143ec4 Modified image
+# 33f3729a45c02fc67d00adb1b8bca394b0e761d9 Image added
+# 913c66a37b4a45b9769037c55c2d238bd0942d2e Files, encoding and much more
+# cfe32cf61b73a0d5e9f13e774abde7ff789b1660 Add submodule
+# 6d394385cf567f80a8fd85055db1ab4c5295806f Added contributing guide
+# 1a0b36b3cdad1d2ee32457c102a8c0b7056fa863 Initial commit
+
+module SeedRepo
+ module BigCommit
+ ID = "913c66a37b4a45b9769037c55c2d238bd0942d2e"
+ PARENT_ID = "cfe32cf61b73a0d5e9f13e774abde7ff789b1660"
+ MESSAGE = "Files, encoding and much more"
+ AUTHOR_FULL_NAME = "Dmitriy Zaporozhets"
+ FILES_COUNT = 2
+ end
+
+ module Commit
+ ID = "570e7b2abdd848b95f2f578043fc23bd6f6fd24d"
+ PARENT_ID = "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9"
+ MESSAGE = "Change some files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n"
+ AUTHOR_FULL_NAME = "Dmitriy Zaporozhets"
+ FILES = ["files/ruby/popen.rb", "files/ruby/regex.rb"]
+ FILES_COUNT = 2
+ C_FILE_PATH = "files/ruby"
+ C_FILES = ["popen.rb", "regex.rb", "version_info.rb"]
+ BLOB_FILE = %{%h3= @key.title\n%hr\n%pre= @key.key\n.actions\n = link_to 'Remove', @key, :confirm => 'Are you sure?', :method => :delete, :class => \"btn danger delete-key\"\n\n\n}
+ BLOB_FILE_PATH = "app/views/keys/show.html.haml"
+ end
+
+ module EmptyCommit
+ ID = "b0e52af38d7ea43cf41d8a6f2471351ac036d6c9"
+ PARENT_ID = "40f4a7a617393735a95a0bb67b08385bc1e7c66d"
+ MESSAGE = "Empty commit"
+ AUTHOR_FULL_NAME = "Rémy Coutable"
+ FILES = []
+ FILES_COUNT = FILES.count
+ end
+
+ module EncodingCommit
+ ID = "40f4a7a617393735a95a0bb67b08385bc1e7c66d"
+ PARENT_ID = "66028349a123e695b589e09a36634d976edcc5e8"
+ MESSAGE = "Add ISO-8859-encoded file"
+ AUTHOR_FULL_NAME = "Stan Hu"
+ FILES = ["encoding/iso8859.txt"]
+ FILES_COUNT = FILES.count
+ end
+
+ module FirstCommit
+ ID = "1a0b36b3cdad1d2ee32457c102a8c0b7056fa863"
+ PARENT_ID = nil
+ MESSAGE = "Initial commit"
+ AUTHOR_FULL_NAME = "Dmitriy Zaporozhets"
+ FILES = ["LICENSE", ".gitignore", "README.md"]
+ FILES_COUNT = 3
+ end
+
+ module LastCommit
+ ID = "4b4918a572fa86f9771e5ba40fbd48e1eb03e2c6"
+ PARENT_ID = "0e1b353b348f8477bdbec1ef47087171c5032cd9"
+ MESSAGE = "Merge branch 'master' into 'master'"
+ AUTHOR_FULL_NAME = "Stan Hu"
+ FILES = ["bin/executable"]
+ FILES_COUNT = FILES.count
+ end
+
+ module Repo
+ HEAD = "master"
+ BRANCHES = %w[
+ feature
+ fix
+ fix-blob-path
+ fix-existing-submodule-dir
+ fix-mode
+ gitattributes
+ gitattributes-updated
+ master
+ merge-test
+ ]
+ TAGS = %w[v1.0.0 v1.1.0 v1.2.0 v1.2.1]
+ end
+
+ module RubyBlob
+ ID = "7e3e39ebb9b2bf433b4ad17313770fbe4051649c"
+ NAME = "popen.rb"
+ CONTENT = <<-eos
+require 'fileutils'
+require 'open3'
+
+module Popen
+ extend self
+
+ def popen(cmd, path=nil)
+ unless cmd.is_a?(Array)
+ raise RuntimeError, "System commands must be given as an array of strings"
+ end
+
+ path ||= Dir.pwd
+
+ vars = {
+ "PWD" => path
+ }
+
+ options = {
+ chdir: path
+ }
+
+ unless File.directory?(path)
+ FileUtils.mkdir_p(path)
+ end
+
+ @cmd_output = ""
+ @cmd_status = 0
+
+ Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|
+ @cmd_output << stdout.read
+ @cmd_output << stderr.read
+ @cmd_status = wait_thr.value.exitstatus
+ end
+
+ return @cmd_output, @cmd_status
+ end
+end
+ eos
+ end
+end
diff --git a/spec/support/services/issuable_create_service_shared_examples.rb b/spec/support/services/issuable_create_service_shared_examples.rb
new file mode 100644
index 00000000000..4f0c745b7ee
--- /dev/null
+++ b/spec/support/services/issuable_create_service_shared_examples.rb
@@ -0,0 +1,52 @@
+shared_examples 'issuable create service' do
+ context 'asssignee_id' do
+ let(:assignee) { create(:user) }
+
+ before { project.team << [user, :master] }
+
+ it 'removes assignee_id when user id is invalid' do
+ opts = { title: 'Title', description: 'Description', assignee_id: -1 }
+
+ issuable = described_class.new(project, user, opts).execute
+
+ expect(issuable.assignee_id).to be_nil
+ end
+
+ it 'removes assignee_id when user id is 0' do
+ opts = { title: 'Title', description: 'Description', assignee_id: 0 }
+
+ issuable = described_class.new(project, user, opts).execute
+
+ expect(issuable.assignee_id).to be_nil
+ end
+
+ it 'saves assignee when user id is valid' do
+ project.team << [assignee, :master]
+ opts = { title: 'Title', description: 'Description', assignee_id: assignee.id }
+
+ issuable = described_class.new(project, user, opts).execute
+
+ expect(issuable.assignee_id).to eq(assignee.id)
+ end
+
+ context "when issuable feature is private" do
+ before do
+ project.project_feature.update(issues_access_level: ProjectFeature::PRIVATE,
+ merge_requests_access_level: ProjectFeature::PRIVATE)
+ end
+
+ levels = [Gitlab::VisibilityLevel::INTERNAL, Gitlab::VisibilityLevel::PUBLIC]
+
+ levels.each do |level|
+ it "removes not authorized assignee when project is #{Gitlab::VisibilityLevel.level_name(level)}" do
+ project.update(visibility_level: level)
+ opts = { title: 'Title', description: 'Description', assignee_id: assignee.id }
+
+ issuable = described_class.new(project, user, opts).execute
+
+ expect(issuable.assignee_id).to be_nil
+ end
+ end
+ end
+ end
+end
diff --git a/spec/support/services/issuable_create_service_slash_commands_shared_examples.rb b/spec/support/services/issuable_create_service_slash_commands_shared_examples.rb
index 5f9645ed44f..dd54b0addda 100644
--- a/spec/support/services/issuable_create_service_slash_commands_shared_examples.rb
+++ b/spec/support/services/issuable_create_service_slash_commands_shared_examples.rb
@@ -11,6 +11,8 @@ shared_examples 'new issuable record that supports slash commands' do
let(:params) { base_params.merge(defined?(default_params) ? default_params : {}).merge(example_params) }
let(:issuable) { described_class.new(project, user, params).execute }
+ before { project.team << [assignee, :master ] }
+
context 'with labels in command only' do
let(:example_params) do
{
@@ -55,7 +57,7 @@ shared_examples 'new issuable record that supports slash commands' do
context 'with assignee and milestone in params and command' do
let(:example_params) do
{
- assignee: build_stubbed(:user),
+ assignee: create(:user),
milestone_id: double(:milestone),
description: %(/assign @#{assignee.username}\n/milestone %"#{milestone.name}")
}
diff --git a/spec/support/services/issuable_update_service_shared_examples.rb b/spec/support/services/issuable_update_service_shared_examples.rb
index a3336755773..49cea1e608c 100644
--- a/spec/support/services/issuable_update_service_shared_examples.rb
+++ b/spec/support/services/issuable_update_service_shared_examples.rb
@@ -1,4 +1,8 @@
shared_examples 'issuable update service' do
+ def update_issuable(opts)
+ described_class.new(project, user, opts).execute(open_issuable)
+ end
+
context 'changing state' do
before { expect(project).to receive(:execute_hooks).once }
@@ -14,4 +18,52 @@ shared_examples 'issuable update service' do
end
end
end
+
+ context 'asssignee_id' do
+ it 'does not update assignee when assignee_id is invalid' do
+ open_issuable.update(assignee_id: user.id)
+
+ update_issuable(assignee_id: -1)
+
+ expect(open_issuable.reload.assignee).to eq(user)
+ end
+
+ it 'unassigns assignee when user id is 0' do
+ open_issuable.update(assignee_id: user.id)
+
+ update_issuable(assignee_id: 0)
+
+ expect(open_issuable.assignee_id).to be_nil
+ end
+
+ it 'saves assignee when user id is valid' do
+ update_issuable(assignee_id: user.id)
+
+ expect(open_issuable.assignee_id).to eq(user.id)
+ end
+
+ it 'does not update assignee_id when user cannot read issue' do
+ non_member = create(:user)
+ original_assignee = open_issuable.assignee
+
+ update_issuable(assignee_id: non_member.id)
+
+ expect(open_issuable.assignee_id).to eq(original_assignee.id)
+ end
+
+ context "when issuable feature is private" do
+ levels = [Gitlab::VisibilityLevel::INTERNAL, Gitlab::VisibilityLevel::PUBLIC]
+
+ levels.each do |level|
+ it "does not update with unauthorized assignee when project is #{Gitlab::VisibilityLevel.level_name(level)}" do
+ assignee = create(:user)
+ project.update(visibility_level: level)
+ feature_visibility_attr = :"#{open_issuable.model_name.plural}_access_level"
+ project.project_feature.update_attribute(feature_visibility_attr, ProjectFeature::PRIVATE)
+
+ expect{ update_issuable(assignee_id: assignee) }.not_to change{ open_issuable.assignee }
+ end
+ end
+ end
+ end
end
diff --git a/spec/support/sidekiq.rb b/spec/support/sidekiq.rb
new file mode 100644
index 00000000000..575d3451150
--- /dev/null
+++ b/spec/support/sidekiq.rb
@@ -0,0 +1,5 @@
+require 'sidekiq/testing/inline'
+
+Sidekiq::Testing.server_middleware do |chain|
+ chain.add Gitlab::SidekiqStatus::ServerMiddleware
+end
diff --git a/spec/support/slack_mattermost_notifications_shared_examples.rb b/spec/support/slack_mattermost_notifications_shared_examples.rb
index 8582aea5fe5..704922b6cf4 100644
--- a/spec/support/slack_mattermost_notifications_shared_examples.rb
+++ b/spec/support/slack_mattermost_notifications_shared_examples.rb
@@ -26,7 +26,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do
describe "#execute" do
let(:user) { create(:user) }
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:username) { 'slack_username' }
let(:channel) { 'slack_channel' }
@@ -105,7 +105,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do
allow(chat_service).to receive(:username).and_return(username)
expect(Slack::Notifier).to receive(:new).
- with(webhook_url, username: username, channel: chat_service.default_channel).
+ with(webhook_url, username: username).
and_return(
double(:slack_service).as_null_object
)
@@ -196,7 +196,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do
describe "Note events" do
let(:user) { create(:user) }
- let(:project) { create(:project, creator_id: user.id) }
+ let(:project) { create(:project, :repository, creator: user) }
before do
allow(chat_service).to receive_messages(
@@ -269,7 +269,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do
describe 'Pipeline events' do
let(:user) { create(:user) }
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:pipeline) do
create(:ci_pipeline,
diff --git a/spec/support/stub_env.rb b/spec/support/stub_env.rb
new file mode 100644
index 00000000000..18597b5c71f
--- /dev/null
+++ b/spec/support/stub_env.rb
@@ -0,0 +1,7 @@
+module StubENV
+ def stub_env(key, value)
+ allow(ENV).to receive(:[]).and_call_original unless @env_already_stubbed
+ @env_already_stubbed ||= true
+ allow(ENV).to receive(:[]).with(key).and_return(value)
+ end
+end
diff --git a/spec/support/taskable_shared_examples.rb b/spec/support/taskable_shared_examples.rb
index ad1c783df4d..4056ff06b84 100644
--- a/spec/support/taskable_shared_examples.rb
+++ b/spec/support/taskable_shared_examples.rb
@@ -33,6 +33,30 @@ shared_examples 'a Taskable' do
end
end
+ describe 'with nested tasks' do
+ before do
+ subject.description = <<-EOT.strip_heredoc
+ - [ ] Task a
+ - [x] Task a.1
+ - [ ] Task a.2
+ - [ ] Task b
+
+ 1. [ ] Task 1
+ 1. [ ] Task 1.1
+ 1. [ ] Task 1.2
+ 1. [x] Task 2
+ 1. [x] Task 2.1
+ EOT
+ end
+
+ it 'returns the correct task status' do
+ expect(subject.task_status).to match('3 of')
+ expect(subject.task_status).to match('9 tasks completed')
+ expect(subject.task_status_short).to match('3/')
+ expect(subject.task_status_short).to match('9 tasks')
+ end
+ end
+
describe 'with an incomplete task' do
before do
subject.description = <<-EOT.strip_heredoc
@@ -48,6 +72,25 @@ shared_examples 'a Taskable' do
end
end
+ describe 'with tasks that are not formatted correctly' do
+ before do
+ subject.description = <<-EOT.strip_heredoc
+ [ ] task 1
+ [ ] task 2
+
+ - [ ]task 1
+ -[ ] task 2
+ EOT
+ end
+
+ it 'returns the correct task status' do
+ expect(subject.task_status).to match('0 of')
+ expect(subject.task_status).to match('0 tasks completed')
+ expect(subject.task_status_short).to match('0/')
+ expect(subject.task_status_short).to match('0 task')
+ end
+ end
+
describe 'with a complete task' do
before do
subject.description = <<-EOT.strip_heredoc
diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb
index 4cf81be3adc..b87232a350b 100644
--- a/spec/support/test_env.rb
+++ b/spec/support/test_env.rb
@@ -35,7 +35,9 @@ module TestEnv
'conflict-missing-side' => 'eb227b3',
'conflict-non-utf8' => 'd0a293c',
'conflict-too-large' => '39fa04f',
- 'deleted-image-test' => '6c17798'
+ 'deleted-image-test' => '6c17798',
+ 'wip' => 'b9238ee',
+ 'csv' => '3dd0896'
}
# gitlab-test-fork is a fork of gitlab-fork, but we don't necessarily
diff --git a/spec/support/time_tracking_shared_examples.rb b/spec/support/time_tracking_shared_examples.rb
new file mode 100644
index 00000000000..02657684b57
--- /dev/null
+++ b/spec/support/time_tracking_shared_examples.rb
@@ -0,0 +1,82 @@
+shared_examples 'issuable time tracker' do
+ it 'renders the sidebar component empty state' do
+ page.within '.time-tracking-no-tracking-pane' do
+ expect(page).to have_content 'No estimate or time spent'
+ end
+ end
+
+ it 'updates the sidebar component when estimate is added' do
+ submit_time('/estimate 3w 1d 1h')
+
+ page.within '.time-tracking-estimate-only-pane' do
+ expect(page).to have_content '3w 1d 1h'
+ end
+ end
+
+ it 'updates the sidebar component when spent is added' do
+ submit_time('/spend 3w 1d 1h')
+
+ page.within '.time-tracking-spend-only-pane' do
+ expect(page).to have_content '3w 1d 1h'
+ end
+ end
+
+ it 'shows the comparison when estimate and spent are added' do
+ submit_time('/estimate 3w 1d 1h')
+ submit_time('/spend 3w 1d 1h')
+
+ page.within '.time-tracking-comparison-pane' do
+ expect(page).to have_content '3w 1d 1h'
+ end
+ end
+
+ it 'updates the sidebar component when estimate is removed' do
+ submit_time('/estimate 3w 1d 1h')
+ submit_time('/remove_estimate')
+
+ page.within '#issuable-time-tracker' do
+ expect(page).to have_content 'No estimate or time spent'
+ end
+ end
+
+ it 'updates the sidebar component when spent is removed' do
+ submit_time('/spend 3w 1d 1h')
+ submit_time('/remove_time_spent')
+
+ page.within '#issuable-time-tracker' do
+ expect(page).to have_content 'No estimate or time spent'
+ end
+ end
+
+ it 'shows the help state when icon is clicked' do
+ page.within '#issuable-time-tracker' do
+ find('.help-button').click
+ expect(page).to have_content 'Track time with slash commands'
+ expect(page).to have_content 'Learn more'
+ end
+ end
+
+ it 'hides the help state when close icon is clicked' do
+ page.within '#issuable-time-tracker' do
+ find('.help-button').click
+ find('.close-help-button').click
+
+ expect(page).not_to have_content 'Track time with slash commands'
+ expect(page).not_to have_content 'Learn more'
+ end
+ end
+
+ it 'displays the correct help url' do
+ page.within '#issuable-time-tracker' do
+ find('.help-button').click
+
+ expect(find_link('Learn more')[:href]).to have_content('/help/workflow/time_tracking.md')
+ end
+ end
+end
+
+def submit_time(slash_command)
+ fill_in 'note[note]', with: slash_command
+ click_button 'Comment'
+ wait_for_ajax
+end
diff --git a/spec/tasks/config_lint_spec.rb b/spec/tasks/config_lint_spec.rb
new file mode 100644
index 00000000000..c32f9a740b7
--- /dev/null
+++ b/spec/tasks/config_lint_spec.rb
@@ -0,0 +1,27 @@
+require 'rake_helper'
+Rake.application.rake_require 'tasks/config_lint'
+
+describe ConfigLint do
+ let(:files){ ['lib/support/fake.sh'] }
+
+ it 'errors out if any bash scripts have errors' do
+ expect { ConfigLint.run(files){ system('exit 1') } }.to raise_error(SystemExit)
+ end
+
+ it 'passes if all scripts are fine' do
+ expect { ConfigLint.run(files){ system('exit 0') } }.not_to raise_error
+ end
+end
+
+describe 'config_lint rake task' do
+ before(:each) do
+ # Prevent `system` from actually being called
+ allow(Kernel).to receive(:system).and_return(true)
+ end
+
+ it 'runs lint on shell scripts' do
+ expect(Kernel).to receive(:system).with('bash', '-n', 'lib/support/init.d/gitlab')
+
+ run_rake_task('config_lint')
+ end
+end
diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb
index a9fea5f1e81..df8a47893f9 100644
--- a/spec/tasks/gitlab/backup_rake_spec.rb
+++ b/spec/tasks/gitlab/backup_rake_spec.rb
@@ -28,7 +28,7 @@ describe 'gitlab:app namespace rake task' do
end
def reenable_backup_sub_tasks
- %w{db repo uploads builds artifacts lfs registry}.each do |subtask|
+ %w{db repo uploads builds artifacts pages lfs registry}.each do |subtask|
Rake::Task["gitlab:backup:#{subtask}:create"].reenable
end
end
@@ -41,7 +41,7 @@ describe 'gitlab:app namespace rake task' do
context 'gitlab version' do
before do
- allow(Dir).to receive(:glob).and_return([])
+ allow(Dir).to receive(:glob).and_return(['1_gitlab_backup.tar'])
allow(Dir).to receive(:chdir)
allow(File).to receive(:exist?).and_return(true)
allow(Kernel).to receive(:system).and_return(true)
@@ -71,6 +71,7 @@ describe 'gitlab:app namespace rake task' do
expect(Rake::Task['gitlab:backup:builds:restore']).to receive(:invoke)
expect(Rake::Task['gitlab:backup:uploads:restore']).to receive(:invoke)
expect(Rake::Task['gitlab:backup:artifacts:restore']).to receive(:invoke)
+ expect(Rake::Task['gitlab:backup:pages:restore']).to receive(:invoke)
expect(Rake::Task['gitlab:backup:lfs:restore']).to receive(:invoke)
expect(Rake::Task['gitlab:backup:registry:restore']).to receive(:invoke)
expect(Rake::Task['gitlab:shell:setup']).to receive(:invoke)
@@ -202,7 +203,7 @@ describe 'gitlab:app namespace rake task' do
it 'sets correct permissions on the tar contents' do
tar_contents, exit_status = Gitlab::Popen.popen(
- %W{tar -tvf #{@backup_tar} db uploads.tar.gz repositories builds.tar.gz artifacts.tar.gz lfs.tar.gz registry.tar.gz}
+ %W{tar -tvf #{@backup_tar} db uploads.tar.gz repositories builds.tar.gz artifacts.tar.gz pages.tar.gz lfs.tar.gz registry.tar.gz}
)
expect(exit_status).to eq(0)
expect(tar_contents).to match('db/')
@@ -210,14 +211,15 @@ describe 'gitlab:app namespace rake task' do
expect(tar_contents).to match('repositories/')
expect(tar_contents).to match('builds.tar.gz')
expect(tar_contents).to match('artifacts.tar.gz')
+ expect(tar_contents).to match('pages.tar.gz')
expect(tar_contents).to match('lfs.tar.gz')
expect(tar_contents).to match('registry.tar.gz')
- expect(tar_contents).not_to match(/^.{4,9}[rwx].* (database.sql.gz|uploads.tar.gz|repositories|builds.tar.gz|artifacts.tar.gz|registry.tar.gz)\/$/)
+ expect(tar_contents).not_to match(/^.{4,9}[rwx].* (database.sql.gz|uploads.tar.gz|repositories|builds.tar.gz|pages.tar.gz|artifacts.tar.gz|registry.tar.gz)\/$/)
end
it 'deletes temp directories' do
temp_dirs = Dir.glob(
- File.join(Gitlab.config.backup.path, '{db,repositories,uploads,builds,artifacts,lfs,registry}')
+ File.join(Gitlab.config.backup.path, '{db,repositories,uploads,builds,artifacts,pages,lfs,registry}')
)
expect(temp_dirs).to be_empty
@@ -304,7 +306,7 @@ describe 'gitlab:app namespace rake task' do
it "does not contain skipped item" do
tar_contents, _exit_status = Gitlab::Popen.popen(
- %W{tar -tvf #{@backup_tar} db uploads.tar.gz repositories builds.tar.gz artifacts.tar.gz lfs.tar.gz registry.tar.gz}
+ %W{tar -tvf #{@backup_tar} db uploads.tar.gz repositories builds.tar.gz artifacts.tar.gz pages.tar.gz lfs.tar.gz registry.tar.gz}
)
expect(tar_contents).to match('db/')
@@ -312,6 +314,7 @@ describe 'gitlab:app namespace rake task' do
expect(tar_contents).to match('builds.tar.gz')
expect(tar_contents).to match('artifacts.tar.gz')
expect(tar_contents).to match('lfs.tar.gz')
+ expect(tar_contents).to match('pages.tar.gz')
expect(tar_contents).to match('registry.tar.gz')
expect(tar_contents).not_to match('repositories/')
end
@@ -327,6 +330,7 @@ describe 'gitlab:app namespace rake task' do
expect(Rake::Task['gitlab:backup:uploads:restore']).not_to receive :invoke
expect(Rake::Task['gitlab:backup:builds:restore']).to receive :invoke
expect(Rake::Task['gitlab:backup:artifacts:restore']).to receive :invoke
+ expect(Rake::Task['gitlab:backup:pages:restore']).to receive :invoke
expect(Rake::Task['gitlab:backup:lfs:restore']).to receive :invoke
expect(Rake::Task['gitlab:backup:registry:restore']).to receive :invoke
expect(Rake::Task['gitlab:shell:setup']).to receive :invoke
diff --git a/spec/tasks/gitlab/mail_google_schema_whitelisting.rb b/spec/tasks/gitlab/mail_google_schema_whitelisting.rb
index 80fc8c48fed..8d1cff7a261 100644
--- a/spec/tasks/gitlab/mail_google_schema_whitelisting.rb
+++ b/spec/tasks/gitlab/mail_google_schema_whitelisting.rb
@@ -20,7 +20,7 @@ describe 'gitlab:mail_google_schema_whitelisting rake task' do
Rake.application.invoke_task "gitlab:mail_google_schema_whitelisting"
end
- it 'should run the task without errors' do
+ it 'runs the task without errors' do
expect { run_rake_task }.not_to raise_error
end
end
diff --git a/spec/teaspoon_env.rb b/spec/teaspoon_env.rb
deleted file mode 100644
index 5ea020f313c..00000000000
--- a/spec/teaspoon_env.rb
+++ /dev/null
@@ -1,178 +0,0 @@
-Teaspoon.configure do |config|
- # Determines where the Teaspoon routes will be mounted. Changing this to "/jasmine" would allow you to browse to
- # `http://localhost:3000/jasmine` to run your tests.
- config.mount_at = "/teaspoon"
-
- # Specifies the root where Teaspoon will look for files. If you're testing an engine using a dummy application it can
- # be useful to set this to your engines root (e.g. `Teaspoon::Engine.root`).
- # Note: Defaults to `Rails.root` if nil.
- config.root = nil
-
- # Paths that will be appended to the Rails assets paths
- # Note: Relative to `config.root`.
- config.asset_paths = ["spec/javascripts", "spec/javascripts/stylesheets"]
-
- # Fixtures are rendered through a controller, which allows using HAML, RABL/JBuilder, etc. Files in these paths will
- # be rendered as fixtures.
- config.fixture_paths = ["spec/javascripts/fixtures"]
-
- # SUITES
- #
- # You can modify the default suite configuration and create new suites here. Suites are isolated from one another.
- #
- # When defining a suite you can provide a name and a block. If the name is left blank, :default is assumed. You can
- # omit various directives and the ones defined in the default suite will be used.
- #
- # To run a specific suite
- # - in the browser: http://localhost/teaspoon/[suite_name]
- # - with the rake task: rake teaspoon suite=[suite_name]
- # - with the cli: teaspoon --suite=[suite_name]
- config.suite do |suite|
- # Specify the framework you would like to use. This allows you to select versions, and will do some basic setup for
- # you -- which you can override with the directives below. This should be specified first, as it can override other
- # directives.
- # Note: If no version is specified, the latest is assumed.
- #
- # Versions: 1.3.1, 2.0.3, 2.1.3, 2.2.0
- suite.use_framework :jasmine, "2.2.0"
-
- # Specify a file matcher as a regular expression and all matching files will be loaded when the suite is run. These
- # files need to be within an asset path. You can add asset paths using the `config.asset_paths`.
- suite.matcher = "{spec/javascripts,app/assets}/**/*_spec.{js,js.es6,es6}"
-
- # Load additional JS files, but requiring them in your spec helper is the preferred way to do this.
- # suite.javascripts = []
-
- # You can include your own stylesheets if you want to change how Teaspoon looks.
- # Note: Spec related CSS can and should be loaded using fixtures.
- # suite.stylesheets = ["teaspoon"]
-
- # This suites spec helper, which can require additional support files. This file is loaded before any of your test
- # files are loaded.
- suite.helper = "spec_helper"
-
- # Partial to be rendered in the head tag of the runner. You can use the provided ones or define your own by creating
- # a `_boot.html.erb` in your fixtures path, and adjust the config to `"/boot"` for instance.
- #
- # Available: boot, boot_require_js
- suite.boot_partial = "boot"
-
- # Partial to be rendered in the body tag of the runner. You can define your own to create a custom body structure.
- suite.body_partial = "body"
-
- # Hooks allow you to use `Teaspoon.hook("fixtures")` before, after, or during your spec run. This will make a
- # synchronous Ajax request to the server that will call all of the blocks you've defined for that hook name.
- # suite.hook :fixtures, &proc{}
-
- # Determine whether specs loaded into the test harness should be embedded as individual script tags or concatenated
- # into a single file. Similar to Rails' asset `debug: true` and `config.assets.debug = true` options. By default,
- # Teaspoon expands all assets to provide more valuable stack traces that reference individual source files.
- # suite.expand_assets = true
- end
-
- # Example suite. Since we're just filtering to files already within the root test/javascripts, these files will also
- # be run in the default suite -- but can be focused into a more specific suite.
- # config.suite :targeted do |suite|
- # suite.matcher = "spec/javascripts/targeted/*_spec.{js,js.coffee,coffee}"
- # end
-
- # CONSOLE RUNNER SPECIFIC
- #
- # These configuration directives are applicable only when running via the rake task or command line interface. These
- # directives can be overridden using the command line interface arguments or with ENV variables when using the rake
- # task.
- #
- # Command Line Interface:
- # teaspoon --driver=phantomjs --server-port=31337 --fail-fast=true --format=junit --suite=my_suite /spec/file_spec.js
- #
- # Rake:
- # teaspoon DRIVER=phantomjs SERVER_PORT=31337 FAIL_FAST=true FORMATTERS=junit suite=my_suite
-
- # Specify which headless driver to use. Supports PhantomJS and Selenium Webdriver.
- #
- # Available: :phantomjs, :selenium, :capybara_webkit
- # PhantomJS: https://github.com/modeset/teaspoon/wiki/Using-PhantomJS
- # Selenium Webdriver: https://github.com/modeset/teaspoon/wiki/Using-Selenium-WebDriver
- # Capybara Webkit: https://github.com/modeset/teaspoon/wiki/Using-Capybara-Webkit
- # config.driver = :phantomjs
-
- # Specify additional options for the driver.
- #
- # PhantomJS: https://github.com/modeset/teaspoon/wiki/Using-PhantomJS
- # Selenium Webdriver: https://github.com/modeset/teaspoon/wiki/Using-Selenium-WebDriver
- # Capybara Webkit: https://github.com/modeset/teaspoon/wiki/Using-Capybara-Webkit
- # config.driver_options = nil
-
- # Specify the timeout for the driver. Specs are expected to complete within this time frame or the run will be
- # considered a failure. This is to avoid issues that can arise where tests stall.
- # config.driver_timeout = 180
-
- # Specify a server to use with Rack (e.g. thin, mongrel). If nil is provided Rack::Server is used.
- # config.server = nil
-
- # Specify a port to run on a specific port, otherwise Teaspoon will use a random available port.
- # config.server_port = nil
-
- # Timeout for starting the server in seconds. If your server is slow to start you may have to bump this, or you may
- # want to lower this if you know it shouldn't take long to start.
- # config.server_timeout = 20
-
- # Force Teaspoon to fail immediately after a failing suite. Can be useful to make Teaspoon fail early if you have
- # several suites, but in environments like CI this may not be desirable.
- # config.fail_fast = true
-
- # Specify the formatters to use when outputting the results.
- # Note: Output files can be specified by using `"junit>/path/to/output.xml"`.
- #
- # Available: :dot, :clean, :documentation, :json, :junit, :pride, :rspec_html, :snowday, :swayze_or_oprah, :tap, :tap_y, :teamcity
- # config.formatters = [:dot]
-
- # Specify if you want color output from the formatters.
- # config.color = true
-
- # Teaspoon pipes all console[log/debug/error] to $stdout. This is useful to catch places where you've forgotten to
- # remove them, but in verbose applications this may not be desirable.
- # config.suppress_log = false
-
- # COVERAGE REPORTS / THRESHOLD ASSERTIONS
- #
- # Coverage reports requires Istanbul (https://github.com/gotwarlost/istanbul) to add instrumentation to your code and
- # display coverage statistics.
- #
- # Coverage configurations are similar to suites. You can define several, and use different ones under different
- # conditions.
- #
- # To run with a specific coverage configuration
- # - with the rake task: rake teaspoon USE_COVERAGE=[coverage_name]
- # - with the cli: teaspoon --coverage=[coverage_name]
-
- # Specify that you always want a coverage configuration to be used. Otherwise, specify that you want coverage
- # on the CLI.
- # Set this to "true" or the name of your coverage config.
- config.use_coverage = true
-
- # You can have multiple coverage configs by passing a name to config.coverage.
- # e.g. config.coverage :ci do |coverage|
- # The default coverage config name is :default.
- config.coverage do |coverage|
- # Which coverage reports Istanbul should generate. Correlates directly to what Istanbul supports.
- #
- # Available: text-summary, text, html, lcov, lcovonly, cobertura, teamcity
- coverage.reports = ["text-summary", "html"]
-
- # The path that the coverage should be written to - when there's an artifact to write to disk.
- # Note: Relative to `config.root`.
- coverage.output_path = "coverage-javascript"
-
- # Assets to be ignored when generating coverage reports. Accepts an array of filenames or regular expressions. The
- # default excludes assets from vendor, gems and support libraries.
- coverage.ignore = [%r{vendor/}, %r{spec/}]
-
- # Various thresholds requirements can be defined, and those thresholds will be checked at the end of a run. If any
- # aren't met the run will fail with a message. Thresholds can be defined as a percentage (0-100), or nil.
- # coverage.statements = nil
- # coverage.functions = nil
- # coverage.branches = nil
- # coverage.lines = nil
- end
-end
diff --git a/spec/uploaders/avatar_uploader_spec.rb b/spec/uploaders/avatar_uploader_spec.rb
index 1f0e8732587..76f5a4b42ed 100644
--- a/spec/uploaders/avatar_uploader_spec.rb
+++ b/spec/uploaders/avatar_uploader_spec.rb
@@ -5,14 +5,14 @@ describe AvatarUploader do
subject { described_class.new(user) }
describe '#move_to_cache' do
- it 'is true' do
- expect(subject.move_to_cache).to eq(true)
+ it 'is false' do
+ expect(subject.move_to_cache).to eq(false)
end
end
describe '#move_to_store' do
- it 'is true' do
- expect(subject.move_to_store).to eq(true)
+ it 'is false' do
+ expect(subject.move_to_store).to eq(false)
end
end
end
diff --git a/spec/uploaders/file_uploader_spec.rb b/spec/uploaders/file_uploader_spec.rb
index bc86adfef68..6a712e33c96 100644
--- a/spec/uploaders/file_uploader_spec.rb
+++ b/spec/uploaders/file_uploader_spec.rb
@@ -17,7 +17,7 @@ describe FileUploader do
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')))
+ @uploader.store!(fixture_file_upload(Rails.root.join('spec', 'fixtures', 'rails_sample.jpg')))
end
it 'detects an image based on file extension' do
@@ -27,7 +27,7 @@ describe FileUploader do
context 'given an video file' do
before do
- video_file = File.new(Rails.root.join('spec', 'fixtures', 'video_sample.mp4'))
+ video_file = fixture_file_upload(Rails.root.join('spec', 'fixtures', 'video_sample.mp4'))
@uploader.store!(video_file)
end
@@ -37,7 +37,7 @@ describe FileUploader do
end
it 'does not return image_or_video? for other types' do
- @uploader.store!(File.new(Rails.root.join('spec', 'fixtures', 'doc_sample.txt')))
+ @uploader.store!(fixture_file_upload(Rails.root.join('spec', 'fixtures', 'doc_sample.txt')))
expect(@uploader.image_or_video?).to be false
end
diff --git a/spec/views/ci/lints/show.html.haml_spec.rb b/spec/views/ci/lints/show.html.haml_spec.rb
index 2dac5ee23c8..3390ae247ff 100644
--- a/spec/views/ci/lints/show.html.haml_spec.rb
+++ b/spec/views/ci/lints/show.html.haml_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe 'ci/lints/show' do
- include Devise::TestHelpers
+ include Devise::Test::ControllerHelpers
describe 'XSS protection' do
let(:config_processor) { Ci::GitlabCiYamlProcessor.new(YAML.dump(content)) }
diff --git a/spec/views/projects/builds/show.html.haml_spec.rb b/spec/views/projects/builds/show.html.haml_spec.rb
index 745d0c745bd..b6f6e7b7a2b 100644
--- a/spec/views/projects/builds/show.html.haml_spec.rb
+++ b/spec/views/projects/builds/show.html.haml_spec.rb
@@ -15,8 +15,38 @@ describe 'projects/builds/show', :view do
allow(view).to receive(:can?).and_return(true)
end
- describe 'environment info in build view' do
- context 'build with latest deployment' do
+ describe 'job information in header' do
+ let(:build) do
+ create(:ci_build, :success, environment: 'staging')
+ end
+
+ before do
+ render
+ end
+
+ it 'shows status name' do
+ expect(rendered).to have_css('.ci-status.ci-success', text: 'passed')
+ end
+
+ it 'does not render a link to the job' do
+ expect(rendered).not_to have_link('passed')
+ end
+
+ it 'shows job id' do
+ expect(rendered).to have_css('.js-build-id', text: build.id)
+ end
+
+ it 'shows a link to the pipeline' do
+ expect(rendered).to have_link(build.pipeline.id)
+ end
+
+ it 'shows a link to the commit' do
+ expect(rendered).to have_link(build.pipeline.short_sha)
+ end
+ end
+
+ describe 'environment info in job view' do
+ context 'job with latest deployment' do
let(:build) do
create(:ci_build, :success, environment: 'staging')
end
@@ -27,7 +57,7 @@ describe 'projects/builds/show', :view do
end
it 'shows deployment message' do
- expected_text = 'This build is the most recent deployment'
+ expected_text = 'This job is the most recent deployment'
render
expect(rendered).to have_css(
@@ -35,7 +65,7 @@ describe 'projects/builds/show', :view do
end
end
- context 'build with outdated deployment' do
+ context 'job with outdated deployment' do
let(:build) do
create(:ci_build, :success, environment: 'staging', pipeline: pipeline)
end
@@ -57,7 +87,7 @@ describe 'projects/builds/show', :view do
end
it 'shows deployment message' do
- expected_text = 'This build is an out-of-date deployment ' \
+ expected_text = 'This job is an out-of-date deployment ' \
"to staging.\nView the most recent deployment ##{second_deployment.iid}."
render
@@ -65,7 +95,7 @@ describe 'projects/builds/show', :view do
end
end
- context 'build failed to deploy' do
+ context 'job failed to deploy' do
let(:build) do
create(:ci_build, :failed, environment: 'staging', pipeline: pipeline)
end
@@ -75,7 +105,7 @@ describe 'projects/builds/show', :view do
end
it 'shows deployment message' do
- expected_text = 'The deployment of this build to staging did not succeed.'
+ expected_text = 'The deployment of this job to staging did not succeed.'
render
expect(rendered).to have_css(
@@ -83,7 +113,7 @@ describe 'projects/builds/show', :view do
end
end
- context 'build will deploy' do
+ context 'job will deploy' do
let(:build) do
create(:ci_build, :running, environment: 'staging', pipeline: pipeline)
end
@@ -94,7 +124,7 @@ describe 'projects/builds/show', :view do
end
it 'shows deployment message' do
- expected_text = 'This build is creating a deployment to staging'
+ expected_text = 'This job is creating a deployment to staging'
render
expect(rendered).to have_css(
@@ -107,7 +137,7 @@ describe 'projects/builds/show', :view do
end
it 'shows that deployment will be overwritten' do
- expected_text = 'This build is creating a deployment to staging'
+ expected_text = 'This job is creating a deployment to staging'
render
expect(rendered).to have_css(
@@ -120,7 +150,7 @@ describe 'projects/builds/show', :view do
context 'when environment does not exist' do
it 'shows deployment message' do
- expected_text = 'This build is creating a deployment to staging'
+ expected_text = 'This job is creating a deployment to staging'
render
expect(rendered).to have_css(
@@ -131,7 +161,7 @@ describe 'projects/builds/show', :view do
end
end
- context 'build that failed to deploy and environment has not been created' do
+ context 'job that failed to deploy and environment has not been created' do
let(:build) do
create(:ci_build, :failed, environment: 'staging', pipeline: pipeline)
end
@@ -141,7 +171,7 @@ describe 'projects/builds/show', :view do
end
it 'shows deployment message' do
- expected_text = 'The deployment of this build to staging did not succeed'
+ expected_text = 'The deployment of this job to staging did not succeed'
render
expect(rendered).to have_css(
@@ -149,7 +179,7 @@ describe 'projects/builds/show', :view do
end
end
- context 'build that will deploy and environment has not been created' do
+ context 'job that will deploy and environment has not been created' do
let(:build) do
create(:ci_build, :running, environment: 'staging', pipeline: pipeline)
end
@@ -159,7 +189,7 @@ describe 'projects/builds/show', :view do
end
it 'shows deployment message' do
- expected_text = 'This build is creating a deployment to staging'
+ expected_text = 'This job is creating a deployment to staging'
render
expect(rendered).to have_css(
@@ -170,7 +200,7 @@ describe 'projects/builds/show', :view do
end
end
- context 'when build is running' do
+ context 'when job is running' do
before do
build.run!
render
@@ -181,7 +211,7 @@ describe 'projects/builds/show', :view do
end
end
- context 'when build is not running' do
+ context 'when job is not running' do
before do
build.success!
render
diff --git a/spec/views/projects/merge_requests/show.html.haml_spec.rb b/spec/views/projects/merge_requests/show.html.haml_spec.rb
index 33cabd14913..7f123b15194 100644
--- a/spec/views/projects/merge_requests/show.html.haml_spec.rb
+++ b/spec/views/projects/merge_requests/show.html.haml_spec.rb
@@ -7,6 +7,7 @@ describe 'projects/merge_requests/show.html.haml' do
let(:project) { create(:project) }
let(:fork_project) { create(:project, forked_from_project: project) }
let(:unlink_project) { Projects::UnlinkForkService.new(fork_project, user) }
+ let(:note) { create(:note_on_merge_request, project: project, noteable: closed_merge_request) }
let(:closed_merge_request) do
create(:closed_merge_request,
@@ -19,8 +20,12 @@ describe 'projects/merge_requests/show.html.haml' do
assign(:project, project)
assign(:merge_request, closed_merge_request)
assign(:commits_count, 0)
+ assign(:note, note)
+ assign(:noteable, closed_merge_request)
+ assign(:notes, [])
+ assign(:pipelines, Ci::Pipeline.none)
- allow(view).to receive(:can?).and_return(true)
+ allow(view).to receive_messages(current_user: user, can?: true)
end
context 'when the merge request is closed' do
diff --git a/spec/views/projects/pipelines/_stage.html.haml_spec.rb b/spec/views/projects/pipelines/_stage.html.haml_spec.rb
index eb7f7ca4a1a..d25de8af5d2 100644
--- a/spec/views/projects/pipelines/_stage.html.haml_spec.rb
+++ b/spec/views/projects/pipelines/_stage.html.haml_spec.rb
@@ -7,15 +7,47 @@ describe 'projects/pipelines/_stage', :view do
before do
assign :stage, stage
+ end
+
+ context 'when there are only latest builds present' do
+ before do
+ create(:ci_build, name: 'test:build',
+ stage: stage.name,
+ pipeline: pipeline)
+ end
+
+ it 'shows the builds in the stage' do
+ render
+
+ expect(rendered).to have_text 'test:build'
+ end
+ end
+
+ context 'when build belongs to different stage' do
+ before do
+ create(:ci_build, name: 'test:build',
+ stage: 'other:stage',
+ pipeline: pipeline)
+ end
- create(:ci_build, name: 'test:build',
- stage: stage.name,
- pipeline: pipeline)
+ it 'does not render build' do
+ render
+
+ expect(rendered).not_to have_text 'test:build'
+ end
end
- it 'shows the builds in the stage' do
- render
+ context 'when there are retried builds present' do
+ before do
+ create_list(:ci_build, 2, name: 'test:build',
+ stage: stage.name,
+ pipeline: pipeline)
+ end
+
+ it 'shows only latest builds' do
+ render
- expect(rendered).to have_text 'test:build'
+ expect(rendered).to have_text 'test:build', count: 1
+ end
end
end
diff --git a/spec/views/projects/pipelines/show.html.haml_spec.rb b/spec/views/projects/pipelines/show.html.haml_spec.rb
index a066ea078e6..c101f6f164d 100644
--- a/spec/views/projects/pipelines/show.html.haml_spec.rb
+++ b/spec/views/projects/pipelines/show.html.haml_spec.rb
@@ -29,7 +29,7 @@ describe 'projects/pipelines/show' do
render
expect(rendered).to have_css('.js-pipeline-graph')
- expect(rendered).to have_css('.grouped-pipeline-dropdown')
+ expect(rendered).to have_css('.js-grouped-pipeline-dropdown')
# stages
expect(rendered).to have_text('Build')
diff --git a/spec/views/shared/milestones/_issuables.html.haml.rb b/spec/views/shared/milestones/_issuables.html.haml.rb
new file mode 100644
index 00000000000..4769d569548
--- /dev/null
+++ b/spec/views/shared/milestones/_issuables.html.haml.rb
@@ -0,0 +1,32 @@
+require 'spec_helper'
+
+describe 'shared/milestones/_issuables.html.haml' do
+ let(:issuables_size) { 100 }
+
+ before do
+ allow(view).to receive_messages(title: nil, id: nil, show_project_name: nil,
+ show_full_project_name: nil, dom_class: '',
+ issuables: double(size: issuables_size).as_null_object)
+
+ stub_template 'shared/milestones/_issuable.html.haml' => ''
+ end
+
+ it 'should show the issuables count if show_counter is true' do
+ render 'shared/milestones/issuables', show_counter: true
+ expect(rendered).to have_content('100')
+ end
+
+ it 'should not show the issuables count if show_counter is false' do
+ render 'shared/milestones/issuables', show_counter: false
+ expect(rendered).not_to have_content('100')
+ end
+
+ describe 'a high issuables count' do
+ let(:issuables_size) { 1000 }
+
+ it 'should show a delimited number if show_counter is true' do
+ render 'shared/milestones/issuables', show_counter: true
+ expect(rendered).to have_content('1,000')
+ end
+ end
+end
diff --git a/spec/workers/authorized_projects_worker_spec.rb b/spec/workers/authorized_projects_worker_spec.rb
index b6591f272f6..97c4bfcd248 100644
--- a/spec/workers/authorized_projects_worker_spec.rb
+++ b/spec/workers/authorized_projects_worker_spec.rb
@@ -3,6 +3,18 @@ require 'spec_helper'
describe AuthorizedProjectsWorker do
let(:worker) { described_class.new }
+ describe '.bulk_perform_and_wait' do
+ it 'schedules the ids and waits for the jobs to complete' do
+ project = create(:project)
+
+ project.owner.project_authorizations.delete_all
+
+ described_class.bulk_perform_and_wait([[project.owner.id]])
+
+ expect(project.owner.project_authorizations.count).to eq(1)
+ end
+ end
+
describe '#perform' do
it "refreshes user's authorized projects" do
user = create(:user)
diff --git a/spec/workers/git_garbage_collect_worker_spec.rb b/spec/workers/git_garbage_collect_worker_spec.rb
index e471a68a49a..5ef8cf1105b 100644
--- a/spec/workers/git_garbage_collect_worker_spec.rb
+++ b/spec/workers/git_garbage_collect_worker_spec.rb
@@ -107,7 +107,8 @@ describe GitGarbageCollectWorker do
tree: old_commit.tree,
parents: [old_commit],
)
- project.repository.update_ref!(
+ GitOperationService.new(nil, project.repository).send(
+ :update_ref,
"refs/heads/#{SecureRandom.hex(6)}",
new_commit_sha,
Gitlab::Git::BLANK_SHA
diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb
index 984acdade36..5919b99a6ed 100644
--- a/spec/workers/post_receive_spec.rb
+++ b/spec/workers/post_receive_spec.rb
@@ -74,7 +74,7 @@ describe PostReceive do
context "webhook" do
it "fetches the correct project" do
- expect(Project).to receive(:find_with_namespace).with(project.path_with_namespace).and_return(project)
+ expect(Project).to receive(:find_by_full_path).with(project.path_with_namespace).and_return(project)
PostReceive.new.perform(pwd(project), key_id, base64_changes)
end
@@ -89,7 +89,7 @@ describe PostReceive do
end
it "asks the project to trigger all hooks" do
- allow(Project).to receive(:find_with_namespace).and_return(project)
+ allow(Project).to receive(:find_by_full_path).and_return(project)
expect(project).to receive(:execute_hooks).twice
expect(project).to receive(:execute_services).twice
@@ -97,7 +97,7 @@ describe PostReceive do
end
it "enqueues a UpdateMergeRequestsWorker job" do
- allow(Project).to receive(:find_with_namespace).and_return(project)
+ allow(Project).to receive(:find_by_full_path).and_return(project)
expect(UpdateMergeRequestsWorker).to receive(:perform_async).with(project.id, project.owner.id, any_args)
PostReceive.new.perform(pwd(project), key_id, base64_changes)
diff --git a/spec/workers/project_cache_worker_spec.rb b/spec/workers/project_cache_worker_spec.rb
index 855c28b584e..f4f63b57a5f 100644
--- a/spec/workers/project_cache_worker_spec.rb
+++ b/spec/workers/project_cache_worker_spec.rb
@@ -1,8 +1,9 @@
require 'spec_helper'
describe ProjectCacheWorker do
- let(:project) { create(:project) }
let(:worker) { described_class.new }
+ let(:project) { create(:project) }
+ let(:statistics) { project.statistics }
describe '#perform' do
before do
@@ -12,7 +13,7 @@ describe ProjectCacheWorker do
context 'with a non-existing project' do
it 'does nothing' do
- expect(worker).not_to receive(:update_repository_size)
+ expect(worker).not_to receive(:update_statistics)
worker.perform(-1)
end
@@ -22,24 +23,19 @@ describe ProjectCacheWorker do
it 'does nothing' do
allow_any_instance_of(Repository).to receive(:exists?).and_return(false)
- expect(worker).not_to receive(:update_repository_size)
+ expect(worker).not_to receive(:update_statistics)
worker.perform(project.id)
end
end
context 'with an existing project' do
- it 'updates the repository size' do
- expect(worker).to receive(:update_repository_size).and_call_original
-
- worker.perform(project.id)
- end
-
- it 'updates the commit count' do
- expect_any_instance_of(Project).to receive(:update_commit_count).
- and_call_original
+ it 'updates the project statistics' do
+ expect(worker).to receive(:update_statistics)
+ .with(kind_of(Project), %i(repository_size))
+ .and_call_original
- worker.perform(project.id)
+ worker.perform(project.id, [], %w(repository_size))
end
it 'refreshes the method caches' do
@@ -47,33 +43,35 @@ describe ProjectCacheWorker do
with(%i(readme)).
and_call_original
- worker.perform(project.id, %i(readme))
+ worker.perform(project.id, %w(readme))
end
end
end
- describe '#update_repository_size' do
+ describe '#update_statistics' do
context 'when a lease could not be obtained' do
it 'does not update the repository size' do
allow(worker).to receive(:try_obtain_lease_for).
- with(project.id, :update_repository_size).
+ with(project.id, :update_statistics).
and_return(false)
- expect(project).not_to receive(:update_repository_size)
+ expect(statistics).not_to receive(:refresh!)
- worker.update_repository_size(project)
+ worker.update_statistics(project)
end
end
context 'when a lease could be obtained' do
- it 'updates the repository size' do
+ it 'updates the project statistics' do
allow(worker).to receive(:try_obtain_lease_for).
- with(project.id, :update_repository_size).
+ with(project.id, :update_statistics).
and_return(true)
- expect(project).to receive(:update_repository_size).and_call_original
+ expect(statistics).to receive(:refresh!)
+ .with(only: %i(repository_size))
+ .and_call_original
- worker.update_repository_size(project)
+ worker.update_statistics(project, %i(repository_size))
end
end
end
diff --git a/spec/workers/project_destroy_worker_spec.rb b/spec/workers/project_destroy_worker_spec.rb
index 1b910d9b91e..1f4c39eb64a 100644
--- a/spec/workers/project_destroy_worker_spec.rb
+++ b/spec/workers/project_destroy_worker_spec.rb
@@ -8,14 +8,14 @@ describe ProjectDestroyWorker do
describe "#perform" do
it "deletes the project" do
- subject.perform(project.id, project.owner, {})
+ subject.perform(project.id, project.owner.id, {})
expect(Project.all).not_to include(project)
expect(Dir.exist?(path)).to be_falsey
end
it "deletes the project but skips repo deletion" do
- subject.perform(project.id, project.owner, { "skip_repo" => true })
+ subject.perform(project.id, project.owner.id, { "skip_repo" => true })
expect(Project.all).not_to include(project)
expect(Dir.exist?(path)).to be_truthy
diff --git a/spec/workers/repository_check/single_repository_worker_spec.rb b/spec/workers/repository_check/single_repository_worker_spec.rb
index 59cfb2c8e3a..d2609d21546 100644
--- a/spec/workers/repository_check/single_repository_worker_spec.rb
+++ b/spec/workers/repository_check/single_repository_worker_spec.rb
@@ -5,7 +5,7 @@ describe RepositoryCheck::SingleRepositoryWorker do
subject { described_class.new }
it 'passes when the project has no push events' do
- project = create(:project_empty_repo, wiki_access_level: ProjectFeature::DISABLED)
+ project = create(:project_empty_repo, :wiki_disabled)
project.events.destroy_all
break_repo(project)
@@ -25,7 +25,7 @@ describe RepositoryCheck::SingleRepositoryWorker do
end
it 'fails if the wiki repository is broken' do
- project = create(:project_empty_repo, wiki_access_level: ProjectFeature::ENABLED)
+ project = create(:project_empty_repo, :wiki_enabled)
project.create_wiki
# Test sanity: everything should be fine before the wiki repo is broken
@@ -39,7 +39,7 @@ describe RepositoryCheck::SingleRepositoryWorker do
end
it 'skips wikis when disabled' do
- project = create(:project_empty_repo, wiki_access_level: ProjectFeature::DISABLED)
+ project = create(:project_empty_repo, :wiki_disabled)
# Make sure the test would fail if the wiki repo was checked
break_wiki(project)
@@ -49,7 +49,7 @@ describe RepositoryCheck::SingleRepositoryWorker do
end
it 'creates missing wikis' do
- project = create(:project_empty_repo, wiki_access_level: ProjectFeature::ENABLED)
+ project = create(:project_empty_repo, :wiki_enabled)
FileUtils.rm_rf(wiki_path(project))
subject.perform(project.id)
diff --git a/spec/workers/use_key_worker_spec.rb b/spec/workers/use_key_worker_spec.rb
new file mode 100644
index 00000000000..e50c788b82a
--- /dev/null
+++ b/spec/workers/use_key_worker_spec.rb
@@ -0,0 +1,23 @@
+require 'spec_helper'
+
+describe UseKeyWorker do
+ describe "#perform" do
+ it "updates the key's last_used_at attribute to the current time when it exists" do
+ worker = described_class.new
+ key = create(:key)
+ current_time = Time.zone.now
+
+ Timecop.freeze(current_time) do
+ expect { worker.perform(key.id) }
+ .to change { key.reload.last_used_at }.from(nil).to be_like_time(current_time)
+ end
+ end
+
+ it "returns false and skips the job when the key doesn't exist" do
+ worker = described_class.new
+ key = create(:key)
+
+ expect(worker.perform(key.id + 1)).to eq false
+ end
+ end
+end
diff --git a/vendor/assets/javascripts/date.format.js b/vendor/assets/javascripts/date.format.js
index f5dc4abcd80..2c9b4825443 100644
--- a/vendor/assets/javascripts/date.format.js
+++ b/vendor/assets/javascripts/date.format.js
@@ -11,115 +11,122 @@
* The date defaults to the current date/time.
* The mask defaults to dateFormat.masks.default.
*/
+ (function (global, factory) {
+ typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
+ typeof define === 'function' && define.amd ? define(factory) :
+ (global.dateFormat = factory());
+ }(this, (function () { 'use strict';
+ var dateFormat = function () {
+ var token = /d{1,4}|m{1,4}|yy(?:yy)?|([HhMsTt])\1?|[LloSZ]|"[^"]*"|'[^']*'/g,
+ timezone = /\b(?:[PMCEA][SDP]T|(?:Pacific|Mountain|Central|Eastern|Atlantic) (?:Standard|Daylight|Prevailing) Time|(?:GMT|UTC)(?:[-+]\d{4})?)\b/g,
+ timezoneClip = /[^-+\dA-Z]/g,
+ pad = function (val, len) {
+ val = String(val);
+ len = len || 2;
+ while (val.length < len) val = "0" + val;
+ return val;
+ };
-var dateFormat = function () {
- var token = /d{1,4}|m{1,4}|yy(?:yy)?|([HhMsTt])\1?|[LloSZ]|"[^"]*"|'[^']*'/g,
- timezone = /\b(?:[PMCEA][SDP]T|(?:Pacific|Mountain|Central|Eastern|Atlantic) (?:Standard|Daylight|Prevailing) Time|(?:GMT|UTC)(?:[-+]\d{4})?)\b/g,
- timezoneClip = /[^-+\dA-Z]/g,
- pad = function (val, len) {
- val = String(val);
- len = len || 2;
- while (val.length < len) val = "0" + val;
- return val;
- };
+ // Regexes and supporting functions are cached through closure
+ return function (date, mask, utc) {
+ var dF = dateFormat;
- // Regexes and supporting functions are cached through closure
- return function (date, mask, utc) {
- var dF = dateFormat;
+ // You can't provide utc if you skip other args (use the "UTC:" mask prefix)
+ if (arguments.length == 1 && Object.prototype.toString.call(date) == "[object String]" && !/\d/.test(date)) {
+ mask = date;
+ date = undefined;
+ }
- // You can't provide utc if you skip other args (use the "UTC:" mask prefix)
- if (arguments.length == 1 && Object.prototype.toString.call(date) == "[object String]" && !/\d/.test(date)) {
- mask = date;
- date = undefined;
- }
+ // Passing date through Date applies Date.parse, if necessary
+ date = date ? new Date(date) : new Date;
+ if (isNaN(date)) throw SyntaxError("invalid date");
- // Passing date through Date applies Date.parse, if necessary
- date = date ? new Date(date) : new Date;
- if (isNaN(date)) throw SyntaxError("invalid date");
+ mask = String(dF.masks[mask] || mask || dF.masks["default"]);
- mask = String(dF.masks[mask] || mask || dF.masks["default"]);
+ // Allow setting the utc argument via the mask
+ if (mask.slice(0, 4) == "UTC:") {
+ mask = mask.slice(4);
+ utc = true;
+ }
- // Allow setting the utc argument via the mask
- if (mask.slice(0, 4) == "UTC:") {
- mask = mask.slice(4);
- utc = true;
- }
+ var _ = utc ? "getUTC" : "get",
+ d = date[_ + "Date"](),
+ D = date[_ + "Day"](),
+ m = date[_ + "Month"](),
+ y = date[_ + "FullYear"](),
+ H = date[_ + "Hours"](),
+ M = date[_ + "Minutes"](),
+ s = date[_ + "Seconds"](),
+ L = date[_ + "Milliseconds"](),
+ o = utc ? 0 : date.getTimezoneOffset(),
+ flags = {
+ d: d,
+ dd: pad(d),
+ ddd: dF.i18n.dayNames[D],
+ dddd: dF.i18n.dayNames[D + 7],
+ m: m + 1,
+ mm: pad(m + 1),
+ mmm: dF.i18n.monthNames[m],
+ mmmm: dF.i18n.monthNames[m + 12],
+ yy: String(y).slice(2),
+ yyyy: y,
+ h: H % 12 || 12,
+ hh: pad(H % 12 || 12),
+ H: H,
+ HH: pad(H),
+ M: M,
+ MM: pad(M),
+ s: s,
+ ss: pad(s),
+ l: pad(L, 3),
+ L: pad(L > 99 ? Math.round(L / 10) : L),
+ t: H < 12 ? "a" : "p",
+ tt: H < 12 ? "am" : "pm",
+ T: H < 12 ? "A" : "P",
+ TT: H < 12 ? "AM" : "PM",
+ Z: utc ? "UTC" : (String(date).match(timezone) || [""]).pop().replace(timezoneClip, ""),
+ o: (o > 0 ? "-" : "+") + pad(Math.floor(Math.abs(o) / 60) * 100 + Math.abs(o) % 60, 4),
+ S: ["th", "st", "nd", "rd"][d % 10 > 3 ? 0 : (d % 100 - d % 10 != 10) * d % 10]
+ };
- var _ = utc ? "getUTC" : "get",
- d = date[_ + "Date"](),
- D = date[_ + "Day"](),
- m = date[_ + "Month"](),
- y = date[_ + "FullYear"](),
- H = date[_ + "Hours"](),
- M = date[_ + "Minutes"](),
- s = date[_ + "Seconds"](),
- L = date[_ + "Milliseconds"](),
- o = utc ? 0 : date.getTimezoneOffset(),
- flags = {
- d: d,
- dd: pad(d),
- ddd: dF.i18n.dayNames[D],
- dddd: dF.i18n.dayNames[D + 7],
- m: m + 1,
- mm: pad(m + 1),
- mmm: dF.i18n.monthNames[m],
- mmmm: dF.i18n.monthNames[m + 12],
- yy: String(y).slice(2),
- yyyy: y,
- h: H % 12 || 12,
- hh: pad(H % 12 || 12),
- H: H,
- HH: pad(H),
- M: M,
- MM: pad(M),
- s: s,
- ss: pad(s),
- l: pad(L, 3),
- L: pad(L > 99 ? Math.round(L / 10) : L),
- t: H < 12 ? "a" : "p",
- tt: H < 12 ? "am" : "pm",
- T: H < 12 ? "A" : "P",
- TT: H < 12 ? "AM" : "PM",
- Z: utc ? "UTC" : (String(date).match(timezone) || [""]).pop().replace(timezoneClip, ""),
- o: (o > 0 ? "-" : "+") + pad(Math.floor(Math.abs(o) / 60) * 100 + Math.abs(o) % 60, 4),
- S: ["th", "st", "nd", "rd"][d % 10 > 3 ? 0 : (d % 100 - d % 10 != 10) * d % 10]
- };
+ return mask.replace(token, function ($0) {
+ return $0 in flags ? flags[$0] : $0.slice(1, $0.length - 1);
+ });
+ };
+ }();
- return mask.replace(token, function ($0) {
- return $0 in flags ? flags[$0] : $0.slice(1, $0.length - 1);
- });
+ // Some common format strings
+ dateFormat.masks = {
+ "default": "ddd mmm dd yyyy HH:MM:ss",
+ shortDate: "m/d/yy",
+ mediumDate: "mmm d, yyyy",
+ longDate: "mmmm d, yyyy",
+ fullDate: "dddd, mmmm d, yyyy",
+ shortTime: "h:MM TT",
+ mediumTime: "h:MM:ss TT",
+ longTime: "h:MM:ss TT Z",
+ isoDate: "yyyy-mm-dd",
+ isoTime: "HH:MM:ss",
+ isoDateTime: "yyyy-mm-dd'T'HH:MM:ss",
+ isoUtcDateTime: "UTC:yyyy-mm-dd'T'HH:MM:ss'Z'"
};
-}();
-// Some common format strings
-dateFormat.masks = {
- "default": "ddd mmm dd yyyy HH:MM:ss",
- shortDate: "m/d/yy",
- mediumDate: "mmm d, yyyy",
- longDate: "mmmm d, yyyy",
- fullDate: "dddd, mmmm d, yyyy",
- shortTime: "h:MM TT",
- mediumTime: "h:MM:ss TT",
- longTime: "h:MM:ss TT Z",
- isoDate: "yyyy-mm-dd",
- isoTime: "HH:MM:ss",
- isoDateTime: "yyyy-mm-dd'T'HH:MM:ss",
- isoUtcDateTime: "UTC:yyyy-mm-dd'T'HH:MM:ss'Z'"
-};
+ // Internationalization strings
+ dateFormat.i18n = {
+ dayNames: [
+ "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat",
+ "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"
+ ],
+ monthNames: [
+ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
+ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"
+ ]
+ };
-// Internationalization strings
-dateFormat.i18n = {
- dayNames: [
- "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat",
- "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"
- ],
- monthNames: [
- "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
- "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"
- ]
-};
+ // For convenience...
+ Date.prototype.format = function (mask, utc) {
+ return dateFormat(this, mask, utc);
+ };
-// For convenience...
-Date.prototype.format = function (mask, utc) {
- return dateFormat(this, mask, utc);
-};
+ return dateFormat;
+})));
diff --git a/vendor/assets/javascripts/es6-promise.auto.js b/vendor/assets/javascripts/es6-promise.auto.js
index 19e6c13a655..b8887115a37 100644
--- a/vendor/assets/javascripts/es6-promise.auto.js
+++ b/vendor/assets/javascripts/es6-promise.auto.js
@@ -1154,6 +1154,3 @@ Promise.Promise = Promise;
return Promise;
})));
-
-ES6Promise.polyfill();
-//# sourceMappingURL=es6-promise.auto.map
diff --git a/vendor/assets/javascripts/jquery.atwho.js b/vendor/assets/javascripts/jquery.atwho.js
new file mode 100644
index 00000000000..0d295ebe5af
--- /dev/null
+++ b/vendor/assets/javascripts/jquery.atwho.js
@@ -0,0 +1,1202 @@
+/**
+ * at.js - 1.5.1
+ * Copyright (c) 2016 chord.luo <chord.luo@gmail.com>;
+ * Homepage: http://ichord.github.com/At.js
+ * License: MIT
+ */
+(function (root, factory) {
+ if (typeof define === 'function' && define.amd) {
+ // AMD. Register as an anonymous module unless amdModuleId is set
+ define(["jquery"], function (a0) {
+ return (factory(a0));
+ });
+ } else if (typeof exports === 'object') {
+ // Node. Does not work with strict CommonJS, but
+ // only CommonJS-like environments that support module.exports,
+ // like Node.
+ module.exports = factory(require("jquery"));
+ } else {
+ factory(jQuery);
+ }
+}(this, function ($) {
+var DEFAULT_CALLBACKS, KEY_CODE;
+
+KEY_CODE = {
+ DOWN: 40,
+ UP: 38,
+ ESC: 27,
+ TAB: 9,
+ ENTER: 13,
+ CTRL: 17,
+ A: 65,
+ P: 80,
+ N: 78,
+ LEFT: 37,
+ UP: 38,
+ RIGHT: 39,
+ DOWN: 40,
+ BACKSPACE: 8,
+ SPACE: 32
+};
+
+DEFAULT_CALLBACKS = {
+ beforeSave: function(data) {
+ return Controller.arrayToDefaultHash(data);
+ },
+ matcher: function(flag, subtext, should_startWithSpace, acceptSpaceBar) {
+ var _a, _y, match, regexp, space;
+ flag = flag.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
+ if (should_startWithSpace) {
+ flag = '(?:^|\\s)' + flag;
+ }
+ _a = decodeURI("%C3%80");
+ _y = decodeURI("%C3%BF");
+ space = acceptSpaceBar ? "\ " : "";
+ regexp = new RegExp(flag + "([A-Za-z" + _a + "-" + _y + "0-9_" + space + "\'\.\+\-]*)$|" + flag + "([^\\x00-\\xff]*)$", 'gi');
+ match = regexp.exec(subtext);
+ if (match) {
+ return match[2] || match[1];
+ } else {
+ return null;
+ }
+ },
+ filter: function(query, data, searchKey) {
+ var _results, i, item, len;
+ _results = [];
+ for (i = 0, len = data.length; i < len; i++) {
+ item = data[i];
+ if (~new String(item[searchKey]).toLowerCase().indexOf(query.toLowerCase())) {
+ _results.push(item);
+ }
+ }
+ return _results;
+ },
+ remoteFilter: null,
+ sorter: function(query, items, searchKey) {
+ var _results, i, item, len;
+ if (!query) {
+ return items;
+ }
+ _results = [];
+ for (i = 0, len = items.length; i < len; i++) {
+ item = items[i];
+ item.atwho_order = new String(item[searchKey]).toLowerCase().indexOf(query.toLowerCase());
+ if (item.atwho_order > -1) {
+ _results.push(item);
+ }
+ }
+ return _results.sort(function(a, b) {
+ return a.atwho_order - b.atwho_order;
+ });
+ },
+ tplEval: function(tpl, map) {
+ var error, error1, template;
+ template = tpl;
+ try {
+ if (typeof tpl !== 'string') {
+ template = tpl(map);
+ }
+ return template.replace(/\$\{([^\}]*)\}/g, function(tag, key, pos) {
+ return map[key];
+ });
+ } catch (error1) {
+ error = error1;
+ return "";
+ }
+ },
+ highlighter: function(li, query) {
+ var regexp;
+ if (!query) {
+ return li;
+ }
+ regexp = new RegExp(">\\s*(\\w*?)(" + query.replace("+", "\\+") + ")(\\w*)\\s*<", 'ig');
+ return li.replace(regexp, function(str, $1, $2, $3) {
+ return '> ' + $1 + '<strong>' + $2 + '</strong>' + $3 + ' <';
+ });
+ },
+ beforeInsert: function(value, $li, e) {
+ return value;
+ },
+ beforeReposition: function(offset) {
+ return offset;
+ },
+ afterMatchFailed: function(at, el) {}
+};
+
+var App;
+
+App = (function() {
+ function App(inputor) {
+ this.currentFlag = null;
+ this.controllers = {};
+ this.aliasMaps = {};
+ this.$inputor = $(inputor);
+ this.setupRootElement();
+ this.listen();
+ }
+
+ App.prototype.createContainer = function(doc) {
+ var ref;
+ if ((ref = this.$el) != null) {
+ ref.remove();
+ }
+ return $(doc.body).append(this.$el = $("<div class='atwho-container'></div>"));
+ };
+
+ App.prototype.setupRootElement = function(iframe, asRoot) {
+ var error, error1;
+ if (asRoot == null) {
+ asRoot = false;
+ }
+ if (iframe) {
+ this.window = iframe.contentWindow;
+ this.document = iframe.contentDocument || this.window.document;
+ this.iframe = iframe;
+ } else {
+ this.document = this.$inputor[0].ownerDocument;
+ this.window = this.document.defaultView || this.document.parentWindow;
+ try {
+ this.iframe = this.window.frameElement;
+ } catch (error1) {
+ error = error1;
+ this.iframe = null;
+ if ($.fn.atwho.debug) {
+ throw new Error("iframe auto-discovery is failed.\nPlease use `setIframe` to set the target iframe manually.\n" + error);
+ }
+ }
+ }
+ return this.createContainer((this.iframeAsRoot = asRoot) ? this.document : document);
+ };
+
+ App.prototype.controller = function(at) {
+ var c, current, currentFlag, ref;
+ if (this.aliasMaps[at]) {
+ current = this.controllers[this.aliasMaps[at]];
+ } else {
+ ref = this.controllers;
+ for (currentFlag in ref) {
+ c = ref[currentFlag];
+ if (currentFlag === at) {
+ current = c;
+ break;
+ }
+ }
+ }
+ if (current) {
+ return current;
+ } else {
+ return this.controllers[this.currentFlag];
+ }
+ };
+
+ App.prototype.setContextFor = function(at) {
+ this.currentFlag = at;
+ return this;
+ };
+
+ App.prototype.reg = function(flag, setting) {
+ var base, controller;
+ controller = (base = this.controllers)[flag] || (base[flag] = this.$inputor.is('[contentEditable]') ? new EditableController(this, flag) : new TextareaController(this, flag));
+ if (setting.alias) {
+ this.aliasMaps[setting.alias] = flag;
+ }
+ controller.init(setting);
+ return this;
+ };
+
+ App.prototype.listen = function() {
+ return this.$inputor.on('compositionstart', (function(_this) {
+ return function(e) {
+ var ref;
+ if ((ref = _this.controller()) != null) {
+ ref.view.hide();
+ }
+ _this.isComposing = true;
+ return null;
+ };
+ })(this)).on('compositionend', (function(_this) {
+ return function(e) {
+ _this.isComposing = false;
+ setTimeout(function(e) {
+ return _this.dispatch(e);
+ });
+ return null;
+ };
+ })(this)).on('keyup.atwhoInner', (function(_this) {
+ return function(e) {
+ return _this.onKeyup(e);
+ };
+ })(this)).on('keydown.atwhoInner', (function(_this) {
+ return function(e) {
+ return _this.onKeydown(e);
+ };
+ })(this)).on('blur.atwhoInner', (function(_this) {
+ return function(e) {
+ var c;
+ if (c = _this.controller()) {
+ c.expectedQueryCBId = null;
+ return c.view.hide(e, c.getOpt("displayTimeout"));
+ }
+ };
+ })(this)).on('click.atwhoInner', (function(_this) {
+ return function(e) {
+ return _this.dispatch(e);
+ };
+ })(this)).on('scroll.atwhoInner', (function(_this) {
+ return function() {
+ var lastScrollTop;
+ lastScrollTop = _this.$inputor.scrollTop();
+ return function(e) {
+ var currentScrollTop, ref;
+ currentScrollTop = e.target.scrollTop;
+ if (lastScrollTop !== currentScrollTop) {
+ if ((ref = _this.controller()) != null) {
+ ref.view.hide(e);
+ }
+ }
+ lastScrollTop = currentScrollTop;
+ return true;
+ };
+ };
+ })(this)());
+ };
+
+ App.prototype.shutdown = function() {
+ var _, c, ref;
+ ref = this.controllers;
+ for (_ in ref) {
+ c = ref[_];
+ c.destroy();
+ delete this.controllers[_];
+ }
+ this.$inputor.off('.atwhoInner');
+ return this.$el.remove();
+ };
+
+ App.prototype.dispatch = function(e) {
+ var _, c, ref, results;
+ ref = this.controllers;
+ results = [];
+ for (_ in ref) {
+ c = ref[_];
+ results.push(c.lookUp(e));
+ }
+ return results;
+ };
+
+ App.prototype.onKeyup = function(e) {
+ var ref;
+ switch (e.keyCode) {
+ case KEY_CODE.ESC:
+ e.preventDefault();
+ if ((ref = this.controller()) != null) {
+ ref.view.hide();
+ }
+ break;
+ case KEY_CODE.DOWN:
+ case KEY_CODE.UP:
+ case KEY_CODE.CTRL:
+ case KEY_CODE.ENTER:
+ $.noop();
+ break;
+ case KEY_CODE.P:
+ case KEY_CODE.N:
+ if (!e.ctrlKey) {
+ this.dispatch(e);
+ }
+ break;
+ default:
+ this.dispatch(e);
+ }
+ };
+
+ App.prototype.onKeydown = function(e) {
+ var ref, view;
+ view = (ref = this.controller()) != null ? ref.view : void 0;
+ if (!(view && view.visible())) {
+ return;
+ }
+ switch (e.keyCode) {
+ case KEY_CODE.ESC:
+ e.preventDefault();
+ view.hide(e);
+ break;
+ case KEY_CODE.UP:
+ e.preventDefault();
+ view.prev();
+ break;
+ case KEY_CODE.DOWN:
+ e.preventDefault();
+ view.next();
+ break;
+ case KEY_CODE.P:
+ if (!e.ctrlKey) {
+ return;
+ }
+ e.preventDefault();
+ view.prev();
+ break;
+ case KEY_CODE.N:
+ if (!e.ctrlKey) {
+ return;
+ }
+ e.preventDefault();
+ view.next();
+ break;
+ case KEY_CODE.TAB:
+ case KEY_CODE.ENTER:
+ case KEY_CODE.SPACE:
+ if (!view.visible()) {
+ return;
+ }
+ if (!this.controller().getOpt('spaceSelectsMatch') && e.keyCode === KEY_CODE.SPACE) {
+ return;
+ }
+ if (!this.controller().getOpt('tabSelectsMatch') && e.keyCode === KEY_CODE.TAB) {
+ return;
+ }
+ if (view.highlighted()) {
+ e.preventDefault();
+ view.choose(e);
+ } else {
+ view.hide(e);
+ }
+ break;
+ default:
+ $.noop();
+ }
+ };
+
+ return App;
+
+})();
+
+var Controller,
+ slice = [].slice;
+
+Controller = (function() {
+ Controller.prototype.uid = function() {
+ return (Math.random().toString(16) + "000000000").substr(2, 8) + (new Date().getTime());
+ };
+
+ function Controller(app, at1) {
+ this.app = app;
+ this.at = at1;
+ this.$inputor = this.app.$inputor;
+ this.id = this.$inputor[0].id || this.uid();
+ this.expectedQueryCBId = null;
+ this.setting = null;
+ this.query = null;
+ this.pos = 0;
+ this.range = null;
+ if ((this.$el = $("#atwho-ground-" + this.id, this.app.$el)).length === 0) {
+ this.app.$el.append(this.$el = $("<div id='atwho-ground-" + this.id + "'></div>"));
+ }
+ this.model = new Model(this);
+ this.view = new View(this);
+ }
+
+ Controller.prototype.init = function(setting) {
+ this.setting = $.extend({}, this.setting || $.fn.atwho["default"], setting);
+ this.view.init();
+ return this.model.reload(this.setting.data);
+ };
+
+ Controller.prototype.destroy = function() {
+ this.trigger('beforeDestroy');
+ this.model.destroy();
+ this.view.destroy();
+ return this.$el.remove();
+ };
+
+ Controller.prototype.callDefault = function() {
+ var args, error, error1, funcName;
+ funcName = arguments[0], args = 2 <= arguments.length ? slice.call(arguments, 1) : [];
+ try {
+ return DEFAULT_CALLBACKS[funcName].apply(this, args);
+ } catch (error1) {
+ error = error1;
+ return $.error(error + " Or maybe At.js doesn't have function " + funcName);
+ }
+ };
+
+ Controller.prototype.trigger = function(name, data) {
+ var alias, eventName;
+ if (data == null) {
+ data = [];
+ }
+ data.push(this);
+ alias = this.getOpt('alias');
+ eventName = alias ? name + "-" + alias + ".atwho" : name + ".atwho";
+ return this.$inputor.trigger(eventName, data);
+ };
+
+ Controller.prototype.callbacks = function(funcName) {
+ return this.getOpt("callbacks")[funcName] || DEFAULT_CALLBACKS[funcName];
+ };
+
+ Controller.prototype.getOpt = function(at, default_value) {
+ var e, error1;
+ try {
+ return this.setting[at];
+ } catch (error1) {
+ e = error1;
+ return null;
+ }
+ };
+
+ Controller.prototype.insertContentFor = function($li) {
+ var data, tpl;
+ tpl = this.getOpt('insertTpl');
+ data = $.extend({}, $li.data('item-data'), {
+ 'atwho-at': this.at
+ });
+ return this.callbacks("tplEval").call(this, tpl, data, "onInsert");
+ };
+
+ Controller.prototype.renderView = function(data) {
+ var searchKey;
+ searchKey = this.getOpt("searchKey");
+ data = this.callbacks("sorter").call(this, this.query.text, data.slice(0, 1001), searchKey);
+ return this.view.render(data.slice(0, this.getOpt('limit')));
+ };
+
+ Controller.arrayToDefaultHash = function(data) {
+ var i, item, len, results;
+ if (!$.isArray(data)) {
+ return data;
+ }
+ results = [];
+ for (i = 0, len = data.length; i < len; i++) {
+ item = data[i];
+ if ($.isPlainObject(item)) {
+ results.push(item);
+ } else {
+ results.push({
+ name: item
+ });
+ }
+ }
+ return results;
+ };
+
+ Controller.prototype.lookUp = function(e) {
+ var query, wait;
+ if (e && e.type === 'click' && !this.getOpt('lookUpOnClick')) {
+ return;
+ }
+ if (this.getOpt('suspendOnComposing') && this.app.isComposing) {
+ return;
+ }
+ query = this.catchQuery(e);
+ if (!query) {
+ this.expectedQueryCBId = null;
+ return query;
+ }
+ this.app.setContextFor(this.at);
+ if (wait = this.getOpt('delay')) {
+ this._delayLookUp(query, wait);
+ } else {
+ this._lookUp(query);
+ }
+ return query;
+ };
+
+ Controller.prototype._delayLookUp = function(query, wait) {
+ var now, remaining;
+ now = Date.now ? Date.now() : new Date().getTime();
+ this.previousCallTime || (this.previousCallTime = now);
+ remaining = wait - (now - this.previousCallTime);
+ if ((0 < remaining && remaining < wait)) {
+ this.previousCallTime = now;
+ this._stopDelayedCall();
+ return this.delayedCallTimeout = setTimeout((function(_this) {
+ return function() {
+ _this.previousCallTime = 0;
+ _this.delayedCallTimeout = null;
+ return _this._lookUp(query);
+ };
+ })(this), wait);
+ } else {
+ this._stopDelayedCall();
+ if (this.previousCallTime !== now) {
+ this.previousCallTime = 0;
+ }
+ return this._lookUp(query);
+ }
+ };
+
+ Controller.prototype._stopDelayedCall = function() {
+ if (this.delayedCallTimeout) {
+ clearTimeout(this.delayedCallTimeout);
+ return this.delayedCallTimeout = null;
+ }
+ };
+
+ Controller.prototype._generateQueryCBId = function() {
+ return {};
+ };
+
+ Controller.prototype._lookUp = function(query) {
+ var _callback;
+ _callback = function(queryCBId, data) {
+ if (queryCBId !== this.expectedQueryCBId) {
+ return;
+ }
+ if (data && data.length > 0) {
+ return this.renderView(this.constructor.arrayToDefaultHash(data));
+ } else {
+ return this.view.hide();
+ }
+ };
+ this.expectedQueryCBId = this._generateQueryCBId();
+ return this.model.query(query.text, $.proxy(_callback, this, this.expectedQueryCBId));
+ };
+
+ return Controller;
+
+})();
+
+var TextareaController,
+ extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
+ hasProp = {}.hasOwnProperty;
+
+TextareaController = (function(superClass) {
+ extend(TextareaController, superClass);
+
+ function TextareaController() {
+ return TextareaController.__super__.constructor.apply(this, arguments);
+ }
+
+ TextareaController.prototype.catchQuery = function() {
+ var caretPos, content, end, isString, query, start, subtext;
+ content = this.$inputor.val();
+ caretPos = this.$inputor.caret('pos', {
+ iframe: this.app.iframe
+ });
+ subtext = content.slice(0, caretPos);
+ query = this.callbacks("matcher").call(this, this.at, subtext, this.getOpt('startWithSpace'), this.getOpt("acceptSpaceBar"));
+ isString = typeof query === 'string';
+ if (isString && query.length < this.getOpt('minLen', 0)) {
+ return;
+ }
+ if (isString && query.length <= this.getOpt('maxLen', 20)) {
+ start = caretPos - query.length;
+ end = start + query.length;
+ this.pos = start;
+ query = {
+ 'text': query,
+ 'headPos': start,
+ 'endPos': end
+ };
+ this.trigger("matched", [this.at, query.text]);
+ } else {
+ query = null;
+ this.view.hide();
+ }
+ return this.query = query;
+ };
+
+ TextareaController.prototype.rect = function() {
+ var c, iframeOffset, scaleBottom;
+ if (!(c = this.$inputor.caret('offset', this.pos - 1, {
+ iframe: this.app.iframe
+ }))) {
+ return;
+ }
+ if (this.app.iframe && !this.app.iframeAsRoot) {
+ iframeOffset = $(this.app.iframe).offset();
+ c.left += iframeOffset.left;
+ c.top += iframeOffset.top;
+ }
+ scaleBottom = this.app.document.selection ? 0 : 2;
+ return {
+ left: c.left,
+ top: c.top,
+ bottom: c.top + c.height + scaleBottom
+ };
+ };
+
+ TextareaController.prototype.insert = function(content, $li) {
+ var $inputor, source, startStr, suffix, text;
+ $inputor = this.$inputor;
+ source = $inputor.val();
+ startStr = source.slice(0, Math.max(this.query.headPos - this.at.length, 0));
+ suffix = (suffix = this.getOpt('suffix')) === "" ? suffix : suffix || " ";
+ content += suffix;
+ text = "" + startStr + content + (source.slice(this.query['endPos'] || 0));
+ $inputor.val(text);
+ $inputor.caret('pos', startStr.length + content.length, {
+ iframe: this.app.iframe
+ });
+ if (!$inputor.is(':focus')) {
+ $inputor.focus();
+ }
+ return $inputor.change();
+ };
+
+ return TextareaController;
+
+})(Controller);
+
+var EditableController,
+ extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
+ hasProp = {}.hasOwnProperty;
+
+EditableController = (function(superClass) {
+ extend(EditableController, superClass);
+
+ function EditableController() {
+ return EditableController.__super__.constructor.apply(this, arguments);
+ }
+
+ EditableController.prototype._getRange = function() {
+ var sel;
+ sel = this.app.window.getSelection();
+ if (sel.rangeCount > 0) {
+ return sel.getRangeAt(0);
+ }
+ };
+
+ EditableController.prototype._setRange = function(position, node, range) {
+ if (range == null) {
+ range = this._getRange();
+ }
+ if (!range) {
+ return;
+ }
+ node = $(node)[0];
+ if (position === 'after') {
+ range.setEndAfter(node);
+ range.setStartAfter(node);
+ } else {
+ range.setEndBefore(node);
+ range.setStartBefore(node);
+ }
+ range.collapse(false);
+ return this._clearRange(range);
+ };
+
+ EditableController.prototype._clearRange = function(range) {
+ var sel;
+ if (range == null) {
+ range = this._getRange();
+ }
+ sel = this.app.window.getSelection();
+ if (this.ctrl_a_pressed == null) {
+ sel.removeAllRanges();
+ return sel.addRange(range);
+ }
+ };
+
+ EditableController.prototype._movingEvent = function(e) {
+ var ref;
+ return e.type === 'click' || ((ref = e.which) === KEY_CODE.RIGHT || ref === KEY_CODE.LEFT || ref === KEY_CODE.UP || ref === KEY_CODE.DOWN);
+ };
+
+ EditableController.prototype._unwrap = function(node) {
+ var next;
+ node = $(node).unwrap().get(0);
+ if ((next = node.nextSibling) && next.nodeValue) {
+ node.nodeValue += next.nodeValue;
+ $(next).remove();
+ }
+ return node;
+ };
+
+ EditableController.prototype.catchQuery = function(e) {
+ var $inserted, $query, _range, index, inserted, isString, lastNode, matched, offset, query, query_content, range;
+ if (!(range = this._getRange())) {
+ return;
+ }
+ if (!range.collapsed) {
+ return;
+ }
+ if (e.which === KEY_CODE.ENTER) {
+ ($query = $(range.startContainer).closest('.atwho-query')).contents().unwrap();
+ if ($query.is(':empty')) {
+ $query.remove();
+ }
+ ($query = $(".atwho-query", this.app.document)).text($query.text()).contents().last().unwrap();
+ this._clearRange();
+ return;
+ }
+ if (/firefox/i.test(navigator.userAgent)) {
+ if ($(range.startContainer).is(this.$inputor)) {
+ this._clearRange();
+ return;
+ }
+ if (e.which === KEY_CODE.BACKSPACE && range.startContainer.nodeType === document.ELEMENT_NODE && (offset = range.startOffset - 1) >= 0) {
+ _range = range.cloneRange();
+ _range.setStart(range.startContainer, offset);
+ if ($(_range.cloneContents()).contents().last().is('.atwho-inserted')) {
+ inserted = $(range.startContainer).contents().get(offset);
+ this._setRange('after', $(inserted).contents().last());
+ }
+ } else if (e.which === KEY_CODE.LEFT && range.startContainer.nodeType === document.TEXT_NODE) {
+ $inserted = $(range.startContainer.previousSibling);
+ if ($inserted.is('.atwho-inserted') && range.startOffset === 0) {
+ this._setRange('after', $inserted.contents().last());
+ }
+ }
+ }
+ $(range.startContainer).closest('.atwho-inserted').addClass('atwho-query').siblings().removeClass('atwho-query');
+ if (($query = $(".atwho-query", this.app.document)).length > 0 && $query.is(':empty') && $query.text().length === 0) {
+ $query.remove();
+ }
+ if (!this._movingEvent(e)) {
+ $query.removeClass('atwho-inserted');
+ }
+ if ($query.length > 0) {
+ switch (e.which) {
+ case KEY_CODE.LEFT:
+ this._setRange('before', $query.get(0), range);
+ $query.removeClass('atwho-query');
+ return;
+ case KEY_CODE.RIGHT:
+ this._setRange('after', $query.get(0).nextSibling, range);
+ $query.removeClass('atwho-query');
+ return;
+ }
+ }
+ if ($query.length > 0 && (query_content = $query.attr('data-atwho-at-query'))) {
+ $query.empty().html(query_content).attr('data-atwho-at-query', null);
+ this._setRange('after', $query.get(0), range);
+ }
+ _range = range.cloneRange();
+ _range.setStart(range.startContainer, 0);
+ matched = this.callbacks("matcher").call(this, this.at, _range.toString(), this.getOpt('startWithSpace'), this.getOpt("acceptSpaceBar"));
+ isString = typeof matched === 'string';
+ if ($query.length === 0 && isString && (index = range.startOffset - this.at.length - matched.length) >= 0) {
+ range.setStart(range.startContainer, index);
+ $query = $('<span/>', this.app.document).attr(this.getOpt("editableAtwhoQueryAttrs")).addClass('atwho-query');
+ range.surroundContents($query.get(0));
+ lastNode = $query.contents().last().get(0);
+ if (/firefox/i.test(navigator.userAgent)) {
+ range.setStart(lastNode, lastNode.length);
+ range.setEnd(lastNode, lastNode.length);
+ this._clearRange(range);
+ } else {
+ this._setRange('after', lastNode, range);
+ }
+ }
+ if (isString && matched.length < this.getOpt('minLen', 0)) {
+ return;
+ }
+ if (isString && matched.length <= this.getOpt('maxLen', 20)) {
+ query = {
+ text: matched,
+ el: $query
+ };
+ this.trigger("matched", [this.at, query.text]);
+ return this.query = query;
+ } else {
+ this.view.hide();
+ this.query = {
+ el: $query
+ };
+ if ($query.text().indexOf(this.at) >= 0) {
+ if (this._movingEvent(e) && $query.hasClass('atwho-inserted')) {
+ $query.removeClass('atwho-query');
+ } else if (false !== this.callbacks('afterMatchFailed').call(this, this.at, $query)) {
+ this._setRange("after", this._unwrap($query.text($query.text()).contents().first()));
+ }
+ }
+ return null;
+ }
+ };
+
+ EditableController.prototype.rect = function() {
+ var $iframe, iframeOffset, rect;
+ rect = this.query.el.offset();
+ if (this.app.iframe && !this.app.iframeAsRoot) {
+ iframeOffset = ($iframe = $(this.app.iframe)).offset();
+ rect.left += iframeOffset.left - this.$inputor.scrollLeft();
+ rect.top += iframeOffset.top - this.$inputor.scrollTop();
+ }
+ rect.bottom = rect.top + this.query.el.height();
+ return rect;
+ };
+
+ EditableController.prototype.insert = function(content, $li) {
+ var data, range, suffix, suffixNode;
+ if (!this.$inputor.is(':focus')) {
+ this.$inputor.focus();
+ }
+ suffix = (suffix = this.getOpt('suffix')) === "" ? suffix : suffix || "\u00A0";
+ data = $li.data('item-data');
+ this.query.el.removeClass('atwho-query').addClass('atwho-inserted').html(content).attr('data-atwho-at-query', "" + data['atwho-at'] + this.query.text);
+ if (range = this._getRange()) {
+ range.setEndAfter(this.query.el[0]);
+ range.collapse(false);
+ range.insertNode(suffixNode = this.app.document.createTextNode("\u200D" + suffix));
+ this._setRange('after', suffixNode, range);
+ }
+ if (!this.$inputor.is(':focus')) {
+ this.$inputor.focus();
+ }
+ return this.$inputor.change();
+ };
+
+ return EditableController;
+
+})(Controller);
+
+var Model;
+
+Model = (function() {
+ function Model(context) {
+ this.context = context;
+ this.at = this.context.at;
+ this.storage = this.context.$inputor;
+ }
+
+ Model.prototype.destroy = function() {
+ return this.storage.data(this.at, null);
+ };
+
+ Model.prototype.saved = function() {
+ return this.fetch() > 0;
+ };
+
+ Model.prototype.query = function(query, callback) {
+ var _remoteFilter, data, searchKey;
+ data = this.fetch();
+ searchKey = this.context.getOpt("searchKey");
+ data = this.context.callbacks('filter').call(this.context, query, data, searchKey) || [];
+ _remoteFilter = this.context.callbacks('remoteFilter');
+ if (data.length > 0 || (!_remoteFilter && data.length === 0)) {
+ return callback(data);
+ } else {
+ return _remoteFilter.call(this.context, query, callback);
+ }
+ };
+
+ Model.prototype.fetch = function() {
+ return this.storage.data(this.at) || [];
+ };
+
+ Model.prototype.save = function(data) {
+ return this.storage.data(this.at, this.context.callbacks("beforeSave").call(this.context, data || []));
+ };
+
+ Model.prototype.load = function(data) {
+ if (!(this.saved() || !data)) {
+ return this._load(data);
+ }
+ };
+
+ Model.prototype.reload = function(data) {
+ return this._load(data);
+ };
+
+ Model.prototype._load = function(data) {
+ if (typeof data === "string") {
+ return $.ajax(data, {
+ dataType: "json"
+ }).done((function(_this) {
+ return function(data) {
+ return _this.save(data);
+ };
+ })(this));
+ } else {
+ return this.save(data);
+ }
+ };
+
+ return Model;
+
+})();
+
+var View;
+
+View = (function() {
+ function View(context) {
+ this.context = context;
+ this.$el = $("<div class='atwho-view'><ul class='atwho-view-ul'></ul></div>");
+ this.$elUl = this.$el.children();
+ this.timeoutID = null;
+ this.context.$el.append(this.$el);
+ this.bindEvent();
+ }
+
+ View.prototype.init = function() {
+ var header_tpl, id;
+ id = this.context.getOpt("alias") || this.context.at.charCodeAt(0);
+ header_tpl = this.context.getOpt("headerTpl");
+ if (header_tpl && this.$el.children().length === 1) {
+ this.$el.prepend(header_tpl);
+ }
+ return this.$el.attr({
+ 'id': "at-view-" + id
+ });
+ };
+
+ View.prototype.destroy = function() {
+ return this.$el.remove();
+ };
+
+ View.prototype.bindEvent = function() {
+ var $menu, lastCoordX, lastCoordY;
+ $menu = this.$el.find('ul');
+ lastCoordX = 0;
+ lastCoordY = 0;
+ return $menu.on('mousemove.atwho-view', 'li', (function(_this) {
+ return function(e) {
+ var $cur;
+ if (lastCoordX === e.clientX && lastCoordY === e.clientY) {
+ return;
+ }
+ lastCoordX = e.clientX;
+ lastCoordY = e.clientY;
+ $cur = $(e.currentTarget);
+ if ($cur.hasClass('cur')) {
+ return;
+ }
+ $menu.find('.cur').removeClass('cur');
+ return $cur.addClass('cur');
+ };
+ })(this)).on('click.atwho-view', 'li', (function(_this) {
+ return function(e) {
+ $menu.find('.cur').removeClass('cur');
+ $(e.currentTarget).addClass('cur');
+ _this.choose(e);
+ return e.preventDefault();
+ };
+ })(this));
+ };
+
+ View.prototype.visible = function() {
+ return this.$el.is(":visible");
+ };
+
+ View.prototype.highlighted = function() {
+ return this.$el.find(".cur").length > 0;
+ };
+
+ View.prototype.choose = function(e) {
+ var $li, content;
+ if (($li = this.$el.find(".cur")).length) {
+ content = this.context.insertContentFor($li);
+ this.context._stopDelayedCall();
+ this.context.insert(this.context.callbacks("beforeInsert").call(this.context, content, $li, e), $li);
+ this.context.trigger("inserted", [$li, e]);
+ this.hide(e);
+ }
+ if (this.context.getOpt("hideWithoutSuffix")) {
+ return this.stopShowing = true;
+ }
+ };
+
+ View.prototype.reposition = function(rect) {
+ var _window, offset, overflowOffset, ref;
+ _window = this.context.app.iframeAsRoot ? this.context.app.window : window;
+ if (rect.bottom + this.$el.height() - $(_window).scrollTop() > $(_window).height()) {
+ rect.bottom = rect.top - this.$el.height();
+ }
+ if (rect.left > (overflowOffset = $(_window).width() - this.$el.width() - 5)) {
+ rect.left = overflowOffset;
+ }
+ offset = {
+ left: rect.left,
+ top: rect.bottom
+ };
+ if ((ref = this.context.callbacks("beforeReposition")) != null) {
+ ref.call(this.context, offset);
+ }
+ this.$el.offset(offset);
+ return this.context.trigger("reposition", [offset]);
+ };
+
+ View.prototype.next = function() {
+ var cur, next, nextEl, offset;
+ cur = this.$el.find('.cur').removeClass('cur');
+ next = cur.next();
+ if (!next.length) {
+ next = this.$el.find('li:first');
+ }
+ next.addClass('cur');
+ nextEl = next[0];
+ offset = nextEl.offsetTop + nextEl.offsetHeight + (nextEl.nextSibling ? nextEl.nextSibling.offsetHeight : 0);
+ return this.scrollTop(Math.max(0, offset - this.$el.height()));
+ };
+
+ View.prototype.prev = function() {
+ var cur, offset, prev, prevEl;
+ cur = this.$el.find('.cur').removeClass('cur');
+ prev = cur.prev();
+ if (!prev.length) {
+ prev = this.$el.find('li:last');
+ }
+ prev.addClass('cur');
+ prevEl = prev[0];
+ offset = prevEl.offsetTop + prevEl.offsetHeight + (prevEl.nextSibling ? prevEl.nextSibling.offsetHeight : 0);
+ return this.scrollTop(Math.max(0, offset - this.$el.height()));
+ };
+
+ View.prototype.scrollTop = function(scrollTop) {
+ var scrollDuration;
+ scrollDuration = this.context.getOpt('scrollDuration');
+ if (scrollDuration) {
+ return this.$elUl.animate({
+ scrollTop: scrollTop
+ }, scrollDuration);
+ } else {
+ return this.$elUl.scrollTop(scrollTop);
+ }
+ };
+
+ View.prototype.show = function() {
+ var rect;
+ if (this.stopShowing) {
+ this.stopShowing = false;
+ return;
+ }
+ if (!this.visible()) {
+ this.$el.show();
+ this.$el.scrollTop(0);
+ this.context.trigger('shown');
+ }
+ if (rect = this.context.rect()) {
+ return this.reposition(rect);
+ }
+ };
+
+ View.prototype.hide = function(e, time) {
+ var callback;
+ if (!this.visible()) {
+ return;
+ }
+ if (isNaN(time)) {
+ this.$el.hide();
+ return this.context.trigger('hidden', [e]);
+ } else {
+ callback = (function(_this) {
+ return function() {
+ return _this.hide();
+ };
+ })(this);
+ clearTimeout(this.timeoutID);
+ return this.timeoutID = setTimeout(callback, time);
+ }
+ };
+
+ View.prototype.render = function(list) {
+ var $li, $ul, i, item, len, li, tpl;
+ if (!($.isArray(list) && list.length > 0)) {
+ this.hide();
+ return;
+ }
+ this.$el.find('ul').empty();
+ $ul = this.$el.find('ul');
+ tpl = this.context.getOpt('displayTpl');
+ for (i = 0, len = list.length; i < len; i++) {
+ item = list[i];
+ item = $.extend({}, item, {
+ 'atwho-at': this.context.at
+ });
+ li = this.context.callbacks("tplEval").call(this.context, tpl, item, "onDisplay");
+ $li = $(this.context.callbacks("highlighter").call(this.context, li, this.context.query.text));
+ $li.data("item-data", item);
+ $ul.append($li);
+ }
+ this.show();
+ if (this.context.getOpt('highlightFirst')) {
+ return $ul.find("li:first").addClass("cur");
+ }
+ };
+
+ return View;
+
+})();
+
+var Api;
+
+Api = {
+ load: function(at, data) {
+ var c;
+ if (c = this.controller(at)) {
+ return c.model.load(data);
+ }
+ },
+ isSelecting: function() {
+ var ref;
+ return !!((ref = this.controller()) != null ? ref.view.visible() : void 0);
+ },
+ hide: function() {
+ var ref;
+ return (ref = this.controller()) != null ? ref.view.hide() : void 0;
+ },
+ reposition: function() {
+ var c;
+ if (c = this.controller()) {
+ return c.view.reposition(c.rect());
+ }
+ },
+ setIframe: function(iframe, asRoot) {
+ this.setupRootElement(iframe, asRoot);
+ return null;
+ },
+ run: function() {
+ return this.dispatch();
+ },
+ destroy: function() {
+ this.shutdown();
+ return this.$inputor.data('atwho', null);
+ }
+};
+
+$.fn.atwho = function(method) {
+ var _args, result;
+ _args = arguments;
+ result = null;
+ this.filter('textarea, input, [contenteditable=""], [contenteditable=true]').each(function() {
+ var $this, app;
+ if (!(app = ($this = $(this)).data("atwho"))) {
+ $this.data('atwho', (app = new App(this)));
+ }
+ if (typeof method === 'object' || !method) {
+ return app.reg(method.at, method);
+ } else if (Api[method] && app) {
+ return result = Api[method].apply(app, Array.prototype.slice.call(_args, 1));
+ } else {
+ return $.error("Method " + method + " does not exist on jQuery.atwho");
+ }
+ });
+ if (result != null) {
+ return result;
+ } else {
+ return this;
+ }
+};
+
+$.fn.atwho["default"] = {
+ at: void 0,
+ alias: void 0,
+ data: null,
+ displayTpl: "<li>${name}</li>",
+ insertTpl: "${atwho-at}${name}",
+ headerTpl: null,
+ callbacks: DEFAULT_CALLBACKS,
+ searchKey: "name",
+ suffix: void 0,
+ hideWithoutSuffix: false,
+ startWithSpace: true,
+ acceptSpaceBar: false,
+ highlightFirst: true,
+ limit: 5,
+ maxLen: 20,
+ minLen: 0,
+ displayTimeout: 300,
+ delay: null,
+ spaceSelectsMatch: false,
+ tabSelectsMatch: true,
+ editableAtwhoQueryAttrs: {},
+ scrollDuration: 150,
+ suspendOnComposing: true,
+ lookUpOnClick: true
+};
+
+$.fn.atwho.debug = false;
+
+}));
diff --git a/vendor/assets/javascripts/jquery.ba-resize.js b/vendor/assets/javascripts/jquery.ba-resize.js
deleted file mode 100644
index 1f41d379153..00000000000
--- a/vendor/assets/javascripts/jquery.ba-resize.js
+++ /dev/null
@@ -1,246 +0,0 @@
-/*!
- * jQuery resize event - v1.1 - 3/14/2010
- * http://benalman.com/projects/jquery-resize-plugin/
- *
- * Copyright (c) 2010 "Cowboy" Ben Alman
- * Dual licensed under the MIT and GPL licenses.
- * http://benalman.com/about/license/
- */
-
-// Script: jQuery resize event
-//
-// *Version: 1.1, Last updated: 3/14/2010*
-//
-// Project Home - http://benalman.com/projects/jquery-resize-plugin/
-// GitHub - http://github.com/cowboy/jquery-resize/
-// Source - http://github.com/cowboy/jquery-resize/raw/master/jquery.ba-resize.js
-// (Minified) - http://github.com/cowboy/jquery-resize/raw/master/jquery.ba-resize.min.js (1.0kb)
-//
-// About: License
-//
-// Copyright (c) 2010 "Cowboy" Ben Alman,
-// Dual licensed under the MIT and GPL licenses.
-// http://benalman.com/about/license/
-//
-// About: Examples
-//
-// This working example, complete with fully commented code, illustrates a few
-// ways in which this plugin can be used.
-//
-// resize event - http://benalman.com/code/projects/jquery-resize/examples/resize/
-//
-// About: Support and Testing
-//
-// Information about what version or versions of jQuery this plugin has been
-// tested with, what browsers it has been tested in, and where the unit tests
-// reside (so you can test it yourself).
-//
-// jQuery Versions - 1.3.2, 1.4.1, 1.4.2
-// Browsers Tested - Internet Explorer 6-8, Firefox 2-3.6, Safari 3-4, Chrome, Opera 9.6-10.1.
-// Unit Tests - http://benalman.com/code/projects/jquery-resize/unit/
-//
-// About: Release History
-//
-// 1.1 - (3/14/2010) Fixed a minor bug that was causing the event to trigger
-// immediately after bind in some circumstances. Also changed $.fn.data
-// to $.data to improve performance.
-// 1.0 - (2/10/2010) Initial release
-
-(function($,window,undefined){
- '$:nomunge'; // Used by YUI compressor.
-
- // A jQuery object containing all non-window elements to which the resize
- // event is bound.
- var elems = $([]),
-
- // Extend $.resize if it already exists, otherwise create it.
- jq_resize = $.resize = $.extend( $.resize, {} ),
-
- timeout_id,
-
- // Reused strings.
- str_setTimeout = 'setTimeout',
- str_resize = 'resize',
- str_data = str_resize + '-special-event',
- str_delay = 'delay',
- str_throttle = 'throttleWindow';
-
- // Property: jQuery.resize.delay
- //
- // The numeric interval (in milliseconds) at which the resize event polling
- // loop executes. Defaults to 250.
-
- jq_resize[ str_delay ] = 250;
-
- // Property: jQuery.resize.throttleWindow
- //
- // Throttle the native window object resize event to fire no more than once
- // every <jQuery.resize.delay> milliseconds. Defaults to true.
- //
- // Because the window object has its own resize event, it doesn't need to be
- // provided by this plugin, and its execution can be left entirely up to the
- // browser. However, since certain browsers fire the resize event continuously
- // while others do not, enabling this will throttle the window resize event,
- // making event behavior consistent across all elements in all browsers.
- //
- // While setting this property to false will disable window object resize
- // event throttling, please note that this property must be changed before any
- // window object resize event callbacks are bound.
-
- jq_resize[ str_throttle ] = true;
-
- // Event: resize event
- //
- // Fired when an element's width or height changes. Because browsers only
- // provide this event for the window element, for other elements a polling
- // loop is initialized, running every <jQuery.resize.delay> milliseconds
- // to see if elements' dimensions have changed. You may bind with either
- // .resize( fn ) or .bind( "resize", fn ), and unbind with .unbind( "resize" ).
- //
- // Usage:
- //
- // > jQuery('selector').bind( 'resize', function(e) {
- // > // element's width or height has changed!
- // > ...
- // > });
- //
- // Additional Notes:
- //
- // * The polling loop is not created until at least one callback is actually
- // bound to the 'resize' event, and this single polling loop is shared
- // across all elements.
- //
- // Double firing issue in jQuery 1.3.2:
- //
- // While this plugin works in jQuery 1.3.2, if an element's event callbacks
- // are manually triggered via .trigger( 'resize' ) or .resize() those
- // callbacks may double-fire, due to limitations in the jQuery 1.3.2 special
- // events system. This is not an issue when using jQuery 1.4+.
- //
- // > // While this works in jQuery 1.4+
- // > $(elem).css({ width: new_w, height: new_h }).resize();
- // >
- // > // In jQuery 1.3.2, you need to do this:
- // > var elem = $(elem);
- // > elem.css({ width: new_w, height: new_h });
- // > elem.data( 'resize-special-event', { width: elem.width(), height: elem.height() } );
- // > elem.resize();
-
- $.event.special[ str_resize ] = {
-
- // Called only when the first 'resize' event callback is bound per element.
- setup: function() {
- // Since window has its own native 'resize' event, return false so that
- // jQuery will bind the event using DOM methods. Since only 'window'
- // objects have a .setTimeout method, this should be a sufficient test.
- // Unless, of course, we're throttling the 'resize' event for window.
- if ( !jq_resize[ str_throttle ] && this[ str_setTimeout ] ) { return false; }
-
- var elem = $(this);
-
- // Add this element to the list of internal elements to monitor.
- elems = elems.add( elem );
-
- // Initialize data store on the element.
- $.data( this, str_data, { w: elem.width(), h: elem.height() } );
-
- // If this is the first element added, start the polling loop.
- if ( elems.length === 1 ) {
- loopy();
- }
- },
-
- // Called only when the last 'resize' event callback is unbound per element.
- teardown: function() {
- // Since window has its own native 'resize' event, return false so that
- // jQuery will unbind the event using DOM methods. Since only 'window'
- // objects have a .setTimeout method, this should be a sufficient test.
- // Unless, of course, we're throttling the 'resize' event for window.
- if ( !jq_resize[ str_throttle ] && this[ str_setTimeout ] ) { return false; }
-
- var elem = $(this);
-
- // Remove this element from the list of internal elements to monitor.
- elems = elems.not( elem );
-
- // Remove any data stored on the element.
- elem.removeData( str_data );
-
- // If this is the last element removed, stop the polling loop.
- if ( !elems.length ) {
- clearTimeout( timeout_id );
- }
- },
-
- // Called every time a 'resize' event callback is bound per element (new in
- // jQuery 1.4).
- add: function( handleObj ) {
- // Since window has its own native 'resize' event, return false so that
- // jQuery doesn't modify the event object. Unless, of course, we're
- // throttling the 'resize' event for window.
- if ( !jq_resize[ str_throttle ] && this[ str_setTimeout ] ) { return false; }
-
- var old_handler;
-
- // The new_handler function is executed every time the event is triggered.
- // This is used to update the internal element data store with the width
- // and height when the event is triggered manually, to avoid double-firing
- // of the event callback. See the "Double firing issue in jQuery 1.3.2"
- // comments above for more information.
-
- function new_handler( e, w, h ) {
- var elem = $(this),
- data = $.data( this, str_data );
-
- // If called from the polling loop, w and h will be passed in as
- // arguments. If called manually, via .trigger( 'resize' ) or .resize(),
- // those values will need to be computed.
- data.w = w !== undefined ? w : elem.width();
- data.h = h !== undefined ? h : elem.height();
-
- old_handler.apply( this, arguments );
- };
-
- // This may seem a little complicated, but it normalizes the special event
- // .add method between jQuery 1.4/1.4.1 and 1.4.2+
- if ( $.isFunction( handleObj ) ) {
- // 1.4, 1.4.1
- old_handler = handleObj;
- return new_handler;
- } else {
- // 1.4.2+
- old_handler = handleObj.handler;
- handleObj.handler = new_handler;
- }
- }
-
- };
-
- function loopy() {
-
- // Start the polling loop, asynchronously.
- timeout_id = window[ str_setTimeout ](function(){
-
- // Iterate over all elements to which the 'resize' event is bound.
- elems.each(function(){
- var elem = $(this),
- width = elem.width(),
- height = elem.height(),
- data = $.data( this, str_data );
-
- // If element size has changed since the last time, update the element
- // data store and trigger the 'resize' event.
- if ( width !== data.w || height !== data.h ) {
- elem.trigger( str_resize, [ data.w = width, data.h = height ] );
- }
-
- });
-
- // Loop.
- loopy();
-
- }, jq_resize[ str_delay ] );
-
- };
-
-})(jQuery,this);
diff --git a/vendor/assets/javascripts/jquery.caret.js b/vendor/assets/javascripts/jquery.caret.js
new file mode 100644
index 00000000000..811ec63ee47
--- /dev/null
+++ b/vendor/assets/javascripts/jquery.caret.js
@@ -0,0 +1,436 @@
+(function (root, factory) {
+ if (typeof define === 'function' && define.amd) {
+ // AMD. Register as an anonymous module.
+ define(["jquery"], function ($) {
+ return (root.returnExportsGlobal = factory($));
+ });
+ } else if (typeof exports === 'object') {
+ // Node. Does not work with strict CommonJS, but
+ // only CommonJS-like enviroments that support module.exports,
+ // like Node.
+ module.exports = factory(require("jquery"));
+ } else {
+ factory(jQuery);
+ }
+}(this, function ($) {
+
+/*
+ Implement Github like autocomplete mentions
+ http://ichord.github.com/At.js
+
+ Copyright (c) 2013 chord.luo@gmail.com
+ Licensed under the MIT license.
+*/
+
+/*
+本插件操作 textarea 或者 input 内的插入符
+只实现了获得插入符在文本框中的位置,我设置
+插入符的位置.
+*/
+
+"use strict";
+var EditableCaret, InputCaret, Mirror, Utils, discoveryIframeOf, methods, oDocument, oFrame, oWindow, pluginName, setContextBy;
+
+pluginName = 'caret';
+
+EditableCaret = (function() {
+ function EditableCaret($inputor) {
+ this.$inputor = $inputor;
+ this.domInputor = this.$inputor[0];
+ }
+
+ EditableCaret.prototype.setPos = function(pos) {
+ var fn, found, offset, sel;
+ if (sel = oWindow.getSelection()) {
+ offset = 0;
+ found = false;
+ (fn = function(pos, parent) {
+ var node, range, _i, _len, _ref, _results;
+ _ref = parent.childNodes;
+ _results = [];
+ for (_i = 0, _len = _ref.length; _i < _len; _i++) {
+ node = _ref[_i];
+ if (found) {
+ break;
+ }
+ if (node.nodeType === 3) {
+ if (offset + node.length >= pos) {
+ found = true;
+ range = oDocument.createRange();
+ range.setStart(node, pos - offset);
+ sel.removeAllRanges();
+ sel.addRange(range);
+ break;
+ } else {
+ _results.push(offset += node.length);
+ }
+ } else {
+ _results.push(fn(pos, node));
+ }
+ }
+ return _results;
+ })(pos, this.domInputor);
+ }
+ return this.domInputor;
+ };
+
+ EditableCaret.prototype.getIEPosition = function() {
+ return this.getPosition();
+ };
+
+ EditableCaret.prototype.getPosition = function() {
+ var inputor_offset, offset;
+ offset = this.getOffset();
+ inputor_offset = this.$inputor.offset();
+ offset.left -= inputor_offset.left;
+ offset.top -= inputor_offset.top;
+ return offset;
+ };
+
+ EditableCaret.prototype.getOldIEPos = function() {
+ var preCaretTextRange, textRange;
+ textRange = oDocument.selection.createRange();
+ preCaretTextRange = oDocument.body.createTextRange();
+ preCaretTextRange.moveToElementText(this.domInputor);
+ preCaretTextRange.setEndPoint("EndToEnd", textRange);
+ return preCaretTextRange.text.length;
+ };
+
+ EditableCaret.prototype.getPos = function() {
+ var clonedRange, pos, range;
+ if (range = this.range()) {
+ clonedRange = range.cloneRange();
+ clonedRange.selectNodeContents(this.domInputor);
+ clonedRange.setEnd(range.endContainer, range.endOffset);
+ pos = clonedRange.toString().length;
+ clonedRange.detach();
+ return pos;
+ } else if (oDocument.selection) {
+ return this.getOldIEPos();
+ }
+ };
+
+ EditableCaret.prototype.getOldIEOffset = function() {
+ var range, rect;
+ range = oDocument.selection.createRange().duplicate();
+ range.moveStart("character", -1);
+ rect = range.getBoundingClientRect();
+ return {
+ height: rect.bottom - rect.top,
+ left: rect.left,
+ top: rect.top
+ };
+ };
+
+ EditableCaret.prototype.getOffset = function(pos) {
+ var clonedRange, offset, range, rect, shadowCaret;
+ if (oWindow.getSelection && (range = this.range())) {
+ if (range.endOffset - 1 > 0 && range.endContainer !== this.domInputor) {
+ clonedRange = range.cloneRange();
+ clonedRange.setStart(range.endContainer, range.endOffset - 1);
+ clonedRange.setEnd(range.endContainer, range.endOffset);
+ rect = clonedRange.getBoundingClientRect();
+ offset = {
+ height: rect.height,
+ left: rect.left + rect.width,
+ top: rect.top
+ };
+ clonedRange.detach();
+ }
+ if (!offset || (offset != null ? offset.height : void 0) === 0) {
+ clonedRange = range.cloneRange();
+ shadowCaret = $(oDocument.createTextNode("|"));
+ clonedRange.insertNode(shadowCaret[0]);
+ clonedRange.selectNode(shadowCaret[0]);
+ rect = clonedRange.getBoundingClientRect();
+ offset = {
+ height: rect.height,
+ left: rect.left,
+ top: rect.top
+ };
+ shadowCaret.remove();
+ clonedRange.detach();
+ }
+ } else if (oDocument.selection) {
+ offset = this.getOldIEOffset();
+ }
+ if (offset) {
+ offset.top += $(oWindow).scrollTop();
+ offset.left += $(oWindow).scrollLeft();
+ }
+ return offset;
+ };
+
+ EditableCaret.prototype.range = function() {
+ var sel;
+ if (!oWindow.getSelection) {
+ return;
+ }
+ sel = oWindow.getSelection();
+ if (sel.rangeCount > 0) {
+ return sel.getRangeAt(0);
+ } else {
+ return null;
+ }
+ };
+
+ return EditableCaret;
+
+})();
+
+InputCaret = (function() {
+ function InputCaret($inputor) {
+ this.$inputor = $inputor;
+ this.domInputor = this.$inputor[0];
+ }
+
+ InputCaret.prototype.getIEPos = function() {
+ var endRange, inputor, len, normalizedValue, pos, range, textInputRange;
+ inputor = this.domInputor;
+ range = oDocument.selection.createRange();
+ pos = 0;
+ if (range && range.parentElement() === inputor) {
+ normalizedValue = inputor.value.replace(/\r\n/g, "\n");
+ len = normalizedValue.length;
+ textInputRange = inputor.createTextRange();
+ textInputRange.moveToBookmark(range.getBookmark());
+ endRange = inputor.createTextRange();
+ endRange.collapse(false);
+ if (textInputRange.compareEndPoints("StartToEnd", endRange) > -1) {
+ pos = len;
+ } else {
+ pos = -textInputRange.moveStart("character", -len);
+ }
+ }
+ return pos;
+ };
+
+ InputCaret.prototype.getPos = function() {
+ if (oDocument.selection) {
+ return this.getIEPos();
+ } else {
+ return this.domInputor.selectionStart;
+ }
+ };
+
+ InputCaret.prototype.setPos = function(pos) {
+ var inputor, range;
+ inputor = this.domInputor;
+ if (oDocument.selection) {
+ range = inputor.createTextRange();
+ range.move("character", pos);
+ range.select();
+ } else if (inputor.setSelectionRange) {
+ inputor.setSelectionRange(pos, pos);
+ }
+ return inputor;
+ };
+
+ InputCaret.prototype.getIEOffset = function(pos) {
+ var h, textRange, x, y;
+ textRange = this.domInputor.createTextRange();
+ pos || (pos = this.getPos());
+ textRange.move('character', pos);
+ x = textRange.boundingLeft;
+ y = textRange.boundingTop;
+ h = textRange.boundingHeight;
+ return {
+ left: x,
+ top: y,
+ height: h
+ };
+ };
+
+ InputCaret.prototype.getOffset = function(pos) {
+ var $inputor, offset, position;
+ $inputor = this.$inputor;
+ if (oDocument.selection) {
+ offset = this.getIEOffset(pos);
+ offset.top += $(oWindow).scrollTop() + $inputor.scrollTop();
+ offset.left += $(oWindow).scrollLeft() + $inputor.scrollLeft();
+ return offset;
+ } else {
+ offset = $inputor.offset();
+ position = this.getPosition(pos);
+ return offset = {
+ left: offset.left + position.left - $inputor.scrollLeft(),
+ top: offset.top + position.top - $inputor.scrollTop(),
+ height: position.height
+ };
+ }
+ };
+
+ InputCaret.prototype.getPosition = function(pos) {
+ var $inputor, at_rect, end_range, format, html, mirror, start_range;
+ $inputor = this.$inputor;
+ format = function(value) {
+ value = value.replace(/<|>|`|"|&/g, '?').replace(/\r\n|\r|\n/g, "<br/>");
+ if (/firefox/i.test(navigator.userAgent)) {
+ value = value.replace(/\s/g, '&nbsp;');
+ }
+ return value;
+ };
+ if (pos === void 0) {
+ pos = this.getPos();
+ }
+ start_range = $inputor.val().slice(0, pos);
+ end_range = $inputor.val().slice(pos);
+ html = "<span style='position: relative; display: inline;'>" + format(start_range) + "</span>";
+ html += "<span id='caret' style='position: relative; display: inline;'>|</span>";
+ html += "<span style='position: relative; display: inline;'>" + format(end_range) + "</span>";
+ mirror = new Mirror($inputor);
+ return at_rect = mirror.create(html).rect();
+ };
+
+ InputCaret.prototype.getIEPosition = function(pos) {
+ var h, inputorOffset, offset, x, y;
+ offset = this.getIEOffset(pos);
+ inputorOffset = this.$inputor.offset();
+ x = offset.left - inputorOffset.left;
+ y = offset.top - inputorOffset.top;
+ h = offset.height;
+ return {
+ left: x,
+ top: y,
+ height: h
+ };
+ };
+
+ return InputCaret;
+
+})();
+
+Mirror = (function() {
+ Mirror.prototype.css_attr = ["borderBottomWidth", "borderLeftWidth", "borderRightWidth", "borderTopStyle", "borderRightStyle", "borderBottomStyle", "borderLeftStyle", "borderTopWidth", "boxSizing", "fontFamily", "fontSize", "fontWeight", "height", "letterSpacing", "lineHeight", "marginBottom", "marginLeft", "marginRight", "marginTop", "outlineWidth", "overflow", "overflowX", "overflowY", "paddingBottom", "paddingLeft", "paddingRight", "paddingTop", "textAlign", "textOverflow", "textTransform", "whiteSpace", "wordBreak", "wordWrap"];
+
+ function Mirror($inputor) {
+ this.$inputor = $inputor;
+ }
+
+ Mirror.prototype.mirrorCss = function() {
+ var css,
+ _this = this;
+ css = {
+ position: 'absolute',
+ left: -9999,
+ top: 0,
+ zIndex: -20000
+ };
+ if (this.$inputor.prop('tagName') === 'TEXTAREA') {
+ this.css_attr.push('width');
+ }
+ $.each(this.css_attr, function(i, p) {
+ return css[p] = _this.$inputor.css(p);
+ });
+ return css;
+ };
+
+ Mirror.prototype.create = function(html) {
+ this.$mirror = $('<div></div>');
+ this.$mirror.css(this.mirrorCss());
+ this.$mirror.html(html);
+ this.$inputor.after(this.$mirror);
+ return this;
+ };
+
+ Mirror.prototype.rect = function() {
+ var $flag, pos, rect;
+ $flag = this.$mirror.find("#caret");
+ pos = $flag.position();
+ rect = {
+ left: pos.left,
+ top: pos.top,
+ height: $flag.height()
+ };
+ this.$mirror.remove();
+ return rect;
+ };
+
+ return Mirror;
+
+})();
+
+Utils = {
+ contentEditable: function($inputor) {
+ return !!($inputor[0].contentEditable && $inputor[0].contentEditable === 'true');
+ }
+};
+
+methods = {
+ pos: function(pos) {
+ if (pos || pos === 0) {
+ return this.setPos(pos);
+ } else {
+ return this.getPos();
+ }
+ },
+ position: function(pos) {
+ if (oDocument.selection) {
+ return this.getIEPosition(pos);
+ } else {
+ return this.getPosition(pos);
+ }
+ },
+ offset: function(pos) {
+ var offset;
+ offset = this.getOffset(pos);
+ return offset;
+ }
+};
+
+oDocument = null;
+
+oWindow = null;
+
+oFrame = null;
+
+setContextBy = function(settings) {
+ var iframe;
+ if (iframe = settings != null ? settings.iframe : void 0) {
+ oFrame = iframe;
+ oWindow = iframe.contentWindow;
+ return oDocument = iframe.contentDocument || oWindow.document;
+ } else {
+ oFrame = void 0;
+ oWindow = window;
+ return oDocument = document;
+ }
+};
+
+discoveryIframeOf = function($dom) {
+ var error;
+ oDocument = $dom[0].ownerDocument;
+ oWindow = oDocument.defaultView || oDocument.parentWindow;
+ try {
+ return oFrame = oWindow.frameElement;
+ } catch (_error) {
+ error = _error;
+ }
+};
+
+$.fn.caret = function(method, value, settings) {
+ var caret;
+ if (methods[method]) {
+ if ($.isPlainObject(value)) {
+ setContextBy(value);
+ value = void 0;
+ } else {
+ setContextBy(settings);
+ }
+ caret = Utils.contentEditable(this) ? new EditableCaret(this) : new InputCaret(this);
+ return methods[method].apply(caret, [value]);
+ } else {
+ return $.error("Method " + method + " does not exist on jQuery.caret");
+ }
+};
+
+$.fn.caret.EditableCaret = EditableCaret;
+
+$.fn.caret.InputCaret = InputCaret;
+
+$.fn.caret.Utils = Utils;
+
+$.fn.caret.apis = methods;
+
+
+}));
diff --git a/vendor/assets/javascripts/katex.js b/vendor/assets/javascripts/katex.js
index beb31ca6a7e..6b59a3477a7 100644
--- a/vendor/assets/javascripts/katex.js
+++ b/vendor/assets/javascripts/katex.js
@@ -35,8 +35,10 @@
1. Clone KaTeX. Anything later than 4fb9445a9 (is merged into master) will do.
2. make (requires node)
- 3. sed -i 's,fonts/,,' build/katex.css
- 4. Copy build/katex.js, build/katex.css and fonts/* to gitlab.
+ 3. sed -e 's,fonts/,,' -e 's/url\(([^)]*)\)/url(font-path\1)/g' build/katex.css > build/katex.scss
+ 4. Copy build/katex.js to gitlab/vendor/assets/javascripts/katex.js,
+ build/katex.scss to gitlab/vendor/assets/stylesheets/katex.scss and
+ fonts/* to gitlab/vendor/assets/fonts/.
*/
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.katex = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
diff --git a/vendor/assets/javascripts/u2f.js b/vendor/assets/javascripts/u2f.js
index e666b136051..a33e5e0ade9 100644
--- a/vendor/assets/javascripts/u2f.js
+++ b/vendor/assets/javascripts/u2f.js
@@ -745,4 +745,6 @@ u2f.getApiVersion = function(callback, opt_timeoutSeconds) {
};
port.postMessage(req);
});
-}; \ No newline at end of file
+};
+
+window.u2f || (window.u2f = u2f);
diff --git a/vendor/assets/javascripts/xterm/encoding-indexes.js b/vendor/assets/javascripts/xterm/encoding-indexes.js
new file mode 100644
index 00000000000..5fd98f5577e
--- /dev/null
+++ b/vendor/assets/javascripts/xterm/encoding-indexes.js
@@ -0,0 +1,39 @@
+(function(global) {
+ 'use strict';
+ global["encoding-indexes"] =
+{
+ "big5":[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,17392,19506,17923,17830,17784,160359,19831,17843,162993,19682,163013,15253,18230,18244,19527,19520,148159,144919,160594,159371,159954,19543,172881,18255,17882,19589,162924,19719,19108,18081,158499,29221,154196,137827,146950,147297,26189,22267,null,32149,22813,166841,15860,38708,162799,23515,138590,23204,13861,171696,23249,23479,23804,26478,34195,170309,29793,29853,14453,138579,145054,155681,16108,153822,15093,31484,40855,147809,166157,143850,133770,143966,17162,33924,40854,37935,18736,34323,22678,38730,37400,31184,31282,26208,27177,34973,29772,31685,26498,31276,21071,36934,13542,29636,155065,29894,40903,22451,18735,21580,16689,145038,22552,31346,162661,35727,18094,159368,16769,155033,31662,140476,40904,140481,140489,140492,40905,34052,144827,16564,40906,17633,175615,25281,28782,40907,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,12736,12737,12738,12739,12740,131340,12741,131281,131277,12742,12743,131275,139240,12744,131274,12745,12746,12747,12748,131342,12749,12750,256,193,461,192,274,201,282,200,332,211,465,210,null,7870,null,7872,202,257,225,462,224,593,275,233,283,232,299,237,464,236,333,243,466,242,363,250,468,249,470,472,474,476,252,null,7871,null,7873,234,609,9178,9179,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,172969,135493,null,25866,null,null,20029,28381,40270,37343,null,null,161589,25745,20250,20264,20392,20822,20852,20892,20964,21153,21160,21307,21326,21457,21464,22242,22768,22788,22791,22834,22836,23398,23454,23455,23706,24198,24635,25993,26622,26628,26725,27982,28860,30005,32420,32428,32442,32455,32463,32479,32518,32567,33402,33487,33647,35270,35774,35810,36710,36711,36718,29713,31996,32205,26950,31433,21031,null,null,null,null,37260,30904,37214,32956,null,36107,33014,133607,null,null,32927,40647,19661,40393,40460,19518,171510,159758,40458,172339,13761,null,28314,33342,29977,null,18705,39532,39567,40857,31111,164972,138698,132560,142054,20004,20097,20096,20103,20159,20203,20279,13388,20413,15944,20483,20616,13437,13459,13477,20870,22789,20955,20988,20997,20105,21113,21136,21287,13767,21417,13649,21424,13651,21442,21539,13677,13682,13953,21651,21667,21684,21689,21712,21743,21784,21795,21800,13720,21823,13733,13759,21975,13765,163204,21797,null,134210,134421,151851,21904,142534,14828,131905,36422,150968,169189,16467,164030,30586,142392,14900,18389,164189,158194,151018,25821,134524,135092,134357,135412,25741,36478,134806,134155,135012,142505,164438,148691,null,134470,170573,164073,18420,151207,142530,39602,14951,169460,16365,13574,152263,169940,161992,142660,40302,38933,null,17369,155813,25780,21731,142668,142282,135287,14843,135279,157402,157462,162208,25834,151634,134211,36456,139681,166732,132913,null,18443,131497,16378,22643,142733,null,148936,132348,155799,134988,134550,21881,16571,17338,null,19124,141926,135325,33194,39157,134556,25465,14846,141173,36288,22177,25724,15939,null,173569,134665,142031,142537,null,135368,145858,14738,14854,164507,13688,155209,139463,22098,134961,142514,169760,13500,27709,151099,null,null,161140,142987,139784,173659,167117,134778,134196,157724,32659,135375,141315,141625,13819,152035,134796,135053,134826,16275,134960,134471,135503,134732,null,134827,134057,134472,135360,135485,16377,140950,25650,135085,144372,161337,142286,134526,134527,142417,142421,14872,134808,135367,134958,173618,158544,167122,167321,167114,38314,21708,33476,21945,null,171715,39974,39606,161630,142830,28992,33133,33004,23580,157042,33076,14231,21343,164029,37302,134906,134671,134775,134907,13789,151019,13833,134358,22191,141237,135369,134672,134776,135288,135496,164359,136277,134777,151120,142756,23124,135197,135198,135413,135414,22428,134673,161428,164557,135093,134779,151934,14083,135094,135552,152280,172733,149978,137274,147831,164476,22681,21096,13850,153405,31666,23400,18432,19244,40743,18919,39967,39821,154484,143677,22011,13810,22153,20008,22786,138177,194680,38737,131206,20059,20155,13630,23587,24401,24516,14586,25164,25909,27514,27701,27706,28780,29227,20012,29357,149737,32594,31035,31993,32595,156266,13505,null,156491,32770,32896,157202,158033,21341,34916,35265,161970,35744,36125,38021,38264,38271,38376,167439,38886,39029,39118,39134,39267,170000,40060,40479,40644,27503,63751,20023,131207,38429,25143,38050,null,20539,28158,171123,40870,15817,34959,147790,28791,23797,19232,152013,13657,154928,24866,166450,36775,37366,29073,26393,29626,144001,172295,15499,137600,19216,30948,29698,20910,165647,16393,27235,172730,16931,34319,133743,31274,170311,166634,38741,28749,21284,139390,37876,30425,166371,40871,30685,20131,20464,20668,20015,20247,40872,21556,32139,22674,22736,138678,24210,24217,24514,141074,25995,144377,26905,27203,146531,27903,null,29184,148741,29580,16091,150035,23317,29881,35715,154788,153237,31379,31724,31939,32364,33528,34199,40873,34960,40874,36537,40875,36815,34143,39392,37409,40876,167353,136255,16497,17058,23066,null,null,null,39016,26475,17014,22333,null,34262,149883,33471,160013,19585,159092,23931,158485,159678,40877,40878,23446,40879,26343,32347,28247,31178,15752,17603,143958,141206,17306,17718,null,23765,146202,35577,23672,15634,144721,23928,40882,29015,17752,147692,138787,19575,14712,13386,131492,158785,35532,20404,131641,22975,33132,38998,170234,24379,134047,null,139713,166253,16642,18107,168057,16135,40883,172469,16632,14294,18167,158790,16764,165554,160767,17773,14548,152730,17761,17691,19849,19579,19830,17898,16328,150287,13921,17630,17597,16877,23870,23880,23894,15868,14351,23972,23993,14368,14392,24130,24253,24357,24451,14600,14612,14655,14669,24791,24893,23781,14729,25015,25017,25039,14776,25132,25232,25317,25368,14840,22193,14851,25570,25595,25607,25690,14923,25792,23829,22049,40863,14999,25990,15037,26111,26195,15090,26258,15138,26390,15170,26532,26624,15192,26698,26756,15218,15217,15227,26889,26947,29276,26980,27039,27013,15292,27094,15325,27237,27252,27249,27266,15340,27289,15346,27307,27317,27348,27382,27521,27585,27626,27765,27818,15563,27906,27910,27942,28033,15599,28068,28081,28181,28184,28201,28294,166336,28347,28386,28378,40831,28392,28393,28452,28468,15686,147265,28545,28606,15722,15733,29111,23705,15754,28716,15761,28752,28756,28783,28799,28809,131877,17345,13809,134872,147159,22462,159443,28990,153568,13902,27042,166889,23412,31305,153825,169177,31333,31357,154028,31419,31408,31426,31427,29137,156813,16842,31450,31453,31466,16879,21682,154625,31499,31573,31529,152334,154878,31650,31599,33692,154548,158847,31696,33825,31634,31672,154912,15789,154725,33938,31738,31750,31797,154817,31812,31875,149634,31910,26237,148856,31945,31943,31974,31860,31987,31989,31950,32359,17693,159300,32093,159446,29837,32137,32171,28981,32179,32210,147543,155689,32228,15635,32245,137209,32229,164717,32285,155937,155994,32366,32402,17195,37996,32295,32576,32577,32583,31030,156368,39393,32663,156497,32675,136801,131176,17756,145254,17667,164666,32762,156809,32773,32776,32797,32808,32815,172167,158915,32827,32828,32865,141076,18825,157222,146915,157416,26405,32935,166472,33031,33050,22704,141046,27775,156824,151480,25831,136330,33304,137310,27219,150117,150165,17530,33321,133901,158290,146814,20473,136445,34018,33634,158474,149927,144688,137075,146936,33450,26907,194964,16859,34123,33488,33562,134678,137140,14017,143741,144730,33403,33506,33560,147083,159139,158469,158615,144846,15807,33565,21996,33669,17675,159141,33708,33729,33747,13438,159444,27223,34138,13462,159298,143087,33880,154596,33905,15827,17636,27303,33866,146613,31064,33960,158614,159351,159299,34014,33807,33681,17568,33939,34020,154769,16960,154816,17731,34100,23282,159385,17703,34163,17686,26559,34326,165413,165435,34241,159880,34306,136578,159949,194994,17770,34344,13896,137378,21495,160666,34430,34673,172280,34798,142375,34737,34778,34831,22113,34412,26710,17935,34885,34886,161248,146873,161252,34910,34972,18011,34996,34997,25537,35013,30583,161551,35207,35210,35238,35241,35239,35260,166437,35303,162084,162493,35484,30611,37374,35472,162393,31465,162618,147343,18195,162616,29052,35596,35615,152624,152933,35647,35660,35661,35497,150138,35728,35739,35503,136927,17941,34895,35995,163156,163215,195028,14117,163155,36054,163224,163261,36114,36099,137488,36059,28764,36113,150729,16080,36215,36265,163842,135188,149898,15228,164284,160012,31463,36525,36534,36547,37588,36633,36653,164709,164882,36773,37635,172703,133712,36787,18730,166366,165181,146875,24312,143970,36857,172052,165564,165121,140069,14720,159447,36919,165180,162494,36961,165228,165387,37032,165651,37060,165606,37038,37117,37223,15088,37289,37316,31916,166195,138889,37390,27807,37441,37474,153017,37561,166598,146587,166668,153051,134449,37676,37739,166625,166891,28815,23235,166626,166629,18789,37444,166892,166969,166911,37747,37979,36540,38277,38310,37926,38304,28662,17081,140922,165592,135804,146990,18911,27676,38523,38550,16748,38563,159445,25050,38582,30965,166624,38589,21452,18849,158904,131700,156688,168111,168165,150225,137493,144138,38705,34370,38710,18959,17725,17797,150249,28789,23361,38683,38748,168405,38743,23370,168427,38751,37925,20688,143543,143548,38793,38815,38833,38846,38848,38866,38880,152684,38894,29724,169011,38911,38901,168989,162170,19153,38964,38963,38987,39014,15118,160117,15697,132656,147804,153350,39114,39095,39112,39111,19199,159015,136915,21936,39137,39142,39148,37752,39225,150057,19314,170071,170245,39413,39436,39483,39440,39512,153381,14020,168113,170965,39648,39650,170757,39668,19470,39700,39725,165376,20532,39732,158120,14531,143485,39760,39744,171326,23109,137315,39822,148043,39938,39935,39948,171624,40404,171959,172434,172459,172257,172323,172511,40318,40323,172340,40462,26760,40388,139611,172435,172576,137531,172595,40249,172217,172724,40592,40597,40606,40610,19764,40618,40623,148324,40641,15200,14821,15645,20274,14270,166955,40706,40712,19350,37924,159138,40727,40726,40761,22175,22154,40773,39352,168075,38898,33919,40802,40809,31452,40846,29206,19390,149877,149947,29047,150008,148296,150097,29598,166874,137466,31135,166270,167478,37737,37875,166468,37612,37761,37835,166252,148665,29207,16107,30578,31299,28880,148595,148472,29054,137199,28835,137406,144793,16071,137349,152623,137208,14114,136955,137273,14049,137076,137425,155467,14115,136896,22363,150053,136190,135848,136134,136374,34051,145062,34051,33877,149908,160101,146993,152924,147195,159826,17652,145134,170397,159526,26617,14131,15381,15847,22636,137506,26640,16471,145215,147681,147595,147727,158753,21707,22174,157361,22162,135135,134056,134669,37830,166675,37788,20216,20779,14361,148534,20156,132197,131967,20299,20362,153169,23144,131499,132043,14745,131850,132116,13365,20265,131776,167603,131701,35546,131596,20120,20685,20749,20386,20227,150030,147082,20290,20526,20588,20609,20428,20453,20568,20732,20825,20827,20829,20830,28278,144789,147001,147135,28018,137348,147081,20904,20931,132576,17629,132259,132242,132241,36218,166556,132878,21081,21156,133235,21217,37742,18042,29068,148364,134176,149932,135396,27089,134685,29817,16094,29849,29716,29782,29592,19342,150204,147597,21456,13700,29199,147657,21940,131909,21709,134086,22301,37469,38644,37734,22493,22413,22399,13886,22731,23193,166470,136954,137071,136976,23084,22968,37519,23166,23247,23058,153926,137715,137313,148117,14069,27909,29763,23073,155267,23169,166871,132115,37856,29836,135939,28933,18802,37896,166395,37821,14240,23582,23710,24158,24136,137622,137596,146158,24269,23375,137475,137476,14081,137376,14045,136958,14035,33066,166471,138682,144498,166312,24332,24334,137511,137131,23147,137019,23364,34324,161277,34912,24702,141408,140843,24539,16056,140719,140734,168072,159603,25024,131134,131142,140827,24985,24984,24693,142491,142599,149204,168269,25713,149093,142186,14889,142114,144464,170218,142968,25399,173147,25782,25393,25553,149987,142695,25252,142497,25659,25963,26994,15348,143502,144045,149897,144043,21773,144096,137433,169023,26318,144009,143795,15072,16784,152964,166690,152975,136956,152923,152613,30958,143619,137258,143924,13412,143887,143746,148169,26254,159012,26219,19347,26160,161904,138731,26211,144082,144097,26142,153714,14545,145466,145340,15257,145314,144382,29904,15254,26511,149034,26806,26654,15300,27326,14435,145365,148615,27187,27218,27337,27397,137490,25873,26776,27212,15319,27258,27479,147392,146586,37792,37618,166890,166603,37513,163870,166364,37991,28069,28427,149996,28007,147327,15759,28164,147516,23101,28170,22599,27940,30786,28987,148250,148086,28913,29264,29319,29332,149391,149285,20857,150180,132587,29818,147192,144991,150090,149783,155617,16134,16049,150239,166947,147253,24743,16115,29900,29756,37767,29751,17567,159210,17745,30083,16227,150745,150790,16216,30037,30323,173510,15129,29800,166604,149931,149902,15099,15821,150094,16127,149957,149747,37370,22322,37698,166627,137316,20703,152097,152039,30584,143922,30478,30479,30587,149143,145281,14942,149744,29752,29851,16063,150202,150215,16584,150166,156078,37639,152961,30750,30861,30856,30930,29648,31065,161601,153315,16654,31131,33942,31141,27181,147194,31290,31220,16750,136934,16690,37429,31217,134476,149900,131737,146874,137070,13719,21867,13680,13994,131540,134157,31458,23129,141045,154287,154268,23053,131675,30960,23082,154566,31486,16889,31837,31853,16913,154547,155324,155302,31949,150009,137136,31886,31868,31918,27314,32220,32263,32211,32590,156257,155996,162632,32151,155266,17002,158581,133398,26582,131150,144847,22468,156690,156664,149858,32733,31527,133164,154345,154947,31500,155150,39398,34373,39523,27164,144447,14818,150007,157101,39455,157088,33920,160039,158929,17642,33079,17410,32966,33033,33090,157620,39107,158274,33378,33381,158289,33875,159143,34320,160283,23174,16767,137280,23339,137377,23268,137432,34464,195004,146831,34861,160802,23042,34926,20293,34951,35007,35046,35173,35149,153219,35156,161669,161668,166901,166873,166812,166393,16045,33955,18165,18127,14322,35389,35356,169032,24397,37419,148100,26068,28969,28868,137285,40301,35999,36073,163292,22938,30659,23024,17262,14036,36394,36519,150537,36656,36682,17140,27736,28603,140065,18587,28537,28299,137178,39913,14005,149807,37051,37015,21873,18694,37307,37892,166475,16482,166652,37927,166941,166971,34021,35371,38297,38311,38295,38294,167220,29765,16066,149759,150082,148458,16103,143909,38543,167655,167526,167525,16076,149997,150136,147438,29714,29803,16124,38721,168112,26695,18973,168083,153567,38749,37736,166281,166950,166703,156606,37562,23313,35689,18748,29689,147995,38811,38769,39224,134950,24001,166853,150194,38943,169178,37622,169431,37349,17600,166736,150119,166756,39132,166469,16128,37418,18725,33812,39227,39245,162566,15869,39323,19311,39338,39516,166757,153800,27279,39457,23294,39471,170225,19344,170312,39356,19389,19351,37757,22642,135938,22562,149944,136424,30788,141087,146872,26821,15741,37976,14631,24912,141185,141675,24839,40015,40019,40059,39989,39952,39807,39887,171565,39839,172533,172286,40225,19630,147716,40472,19632,40204,172468,172269,172275,170287,40357,33981,159250,159711,158594,34300,17715,159140,159364,159216,33824,34286,159232,145367,155748,31202,144796,144960,18733,149982,15714,37851,37566,37704,131775,30905,37495,37965,20452,13376,36964,152925,30781,30804,30902,30795,137047,143817,149825,13978,20338,28634,28633,28702,28702,21524,147893,22459,22771,22410,40214,22487,28980,13487,147884,29163,158784,151447,23336,137141,166473,24844,23246,23051,17084,148616,14124,19323,166396,37819,37816,137430,134941,33906,158912,136211,148218,142374,148417,22932,146871,157505,32168,155995,155812,149945,149899,166394,37605,29666,16105,29876,166755,137375,16097,150195,27352,29683,29691,16086,150078,150164,137177,150118,132007,136228,149989,29768,149782,28837,149878,37508,29670,37727,132350,37681,166606,166422,37766,166887,153045,18741,166530,29035,149827,134399,22180,132634,134123,134328,21762,31172,137210,32254,136898,150096,137298,17710,37889,14090,166592,149933,22960,137407,137347,160900,23201,14050,146779,14000,37471,23161,166529,137314,37748,15565,133812,19094,14730,20724,15721,15692,136092,29045,17147,164376,28175,168164,17643,27991,163407,28775,27823,15574,147437,146989,28162,28428,15727,132085,30033,14012,13512,18048,16090,18545,22980,37486,18750,36673,166940,158656,22546,22472,14038,136274,28926,148322,150129,143331,135856,140221,26809,26983,136088,144613,162804,145119,166531,145366,144378,150687,27162,145069,158903,33854,17631,17614,159014,159057,158850,159710,28439,160009,33597,137018,33773,158848,159827,137179,22921,23170,137139,23137,23153,137477,147964,14125,23023,137020,14023,29070,37776,26266,148133,23150,23083,148115,27179,147193,161590,148571,148170,28957,148057,166369,20400,159016,23746,148686,163405,148413,27148,148054,135940,28838,28979,148457,15781,27871,194597,150095,32357,23019,23855,15859,24412,150109,137183,32164,33830,21637,146170,144128,131604,22398,133333,132633,16357,139166,172726,28675,168283,23920,29583,31955,166489,168992,20424,32743,29389,29456,162548,29496,29497,153334,29505,29512,16041,162584,36972,29173,149746,29665,33270,16074,30476,16081,27810,22269,29721,29726,29727,16098,16112,16116,16122,29907,16142,16211,30018,30061,30066,30093,16252,30152,30172,16320,30285,16343,30324,16348,30330,151388,29064,22051,35200,22633,16413,30531,16441,26465,16453,13787,30616,16490,16495,23646,30654,30667,22770,30744,28857,30748,16552,30777,30791,30801,30822,33864,152885,31027,26627,31026,16643,16649,31121,31129,36795,31238,36796,16743,31377,16818,31420,33401,16836,31439,31451,16847,20001,31586,31596,31611,31762,31771,16992,17018,31867,31900,17036,31928,17044,31981,36755,28864,134351,32207,32212,32208,32253,32686,32692,29343,17303,32800,32805,31545,32814,32817,32852,15820,22452,28832,32951,33001,17389,33036,29482,33038,33042,30048,33044,17409,15161,33110,33113,33114,17427,22586,33148,33156,17445,33171,17453,33189,22511,33217,33252,33364,17551,33446,33398,33482,33496,33535,17584,33623,38505,27018,33797,28917,33892,24803,33928,17668,33982,34017,34040,34064,34104,34130,17723,34159,34160,34272,17783,34418,34450,34482,34543,38469,34699,17926,17943,34990,35071,35108,35143,35217,162151,35369,35384,35476,35508,35921,36052,36082,36124,18328,22623,36291,18413,20206,36410,21976,22356,36465,22005,36528,18487,36558,36578,36580,36589,36594,36791,36801,36810,36812,36915,39364,18605,39136,37395,18718,37416,37464,37483,37553,37550,37567,37603,37611,37619,37620,37629,37699,37764,37805,18757,18769,40639,37911,21249,37917,37933,37950,18794,37972,38009,38189,38306,18855,38388,38451,18917,26528,18980,38720,18997,38834,38850,22100,19172,24808,39097,19225,39153,22596,39182,39193,20916,39196,39223,39234,39261,39266,19312,39365,19357,39484,39695,31363,39785,39809,39901,39921,39924,19565,39968,14191,138178,40265,39994,40702,22096,40339,40381,40384,40444,38134,36790,40571,40620,40625,40637,40646,38108,40674,40689,40696,31432,40772,131220,131767,132000,26906,38083,22956,132311,22592,38081,14265,132565,132629,132726,136890,22359,29043,133826,133837,134079,21610,194619,134091,21662,134139,134203,134227,134245,134268,24807,134285,22138,134325,134365,134381,134511,134578,134600,26965,39983,34725,134660,134670,134871,135056,134957,134771,23584,135100,24075,135260,135247,135286,26398,135291,135304,135318,13895,135359,135379,135471,135483,21348,33965,135907,136053,135990,35713,136567,136729,137155,137159,20088,28859,137261,137578,137773,137797,138282,138352,138412,138952,25283,138965,139029,29080,26709,139333,27113,14024,139900,140247,140282,141098,141425,141647,33533,141671,141715,142037,35237,142056,36768,142094,38840,142143,38983,39613,142412,null,142472,142519,154600,142600,142610,142775,142741,142914,143220,143308,143411,143462,144159,144350,24497,26184,26303,162425,144743,144883,29185,149946,30679,144922,145174,32391,131910,22709,26382,26904,146087,161367,155618,146961,147129,161278,139418,18640,19128,147737,166554,148206,148237,147515,148276,148374,150085,132554,20946,132625,22943,138920,15294,146687,148484,148694,22408,149108,14747,149295,165352,170441,14178,139715,35678,166734,39382,149522,149755,150037,29193,150208,134264,22885,151205,151430,132985,36570,151596,21135,22335,29041,152217,152601,147274,150183,21948,152646,152686,158546,37332,13427,152895,161330,152926,18200,152930,152934,153543,149823,153693,20582,13563,144332,24798,153859,18300,166216,154286,154505,154630,138640,22433,29009,28598,155906,162834,36950,156082,151450,35682,156674,156746,23899,158711,36662,156804,137500,35562,150006,156808,147439,156946,19392,157119,157365,141083,37989,153569,24981,23079,194765,20411,22201,148769,157436,20074,149812,38486,28047,158909,13848,35191,157593,157806,156689,157790,29151,157895,31554,168128,133649,157990,37124,158009,31301,40432,158202,39462,158253,13919,156777,131105,31107,158260,158555,23852,144665,33743,158621,18128,158884,30011,34917,159150,22710,14108,140685,159819,160205,15444,160384,160389,37505,139642,160395,37680,160486,149968,27705,38047,160848,134904,34855,35061,141606,164979,137137,28344,150058,137248,14756,14009,23568,31203,17727,26294,171181,170148,35139,161740,161880,22230,16607,136714,14753,145199,164072,136133,29101,33638,162269,168360,23143,19639,159919,166315,162301,162314,162571,163174,147834,31555,31102,163849,28597,172767,27139,164632,21410,159239,37823,26678,38749,164207,163875,158133,136173,143919,163912,23941,166960,163971,22293,38947,166217,23979,149896,26046,27093,21458,150181,147329,15377,26422,163984,164084,164142,139169,164175,164233,164271,164378,164614,164655,164746,13770,164968,165546,18682,25574,166230,30728,37461,166328,17394,166375,17375,166376,166726,166868,23032,166921,36619,167877,168172,31569,168208,168252,15863,168286,150218,36816,29327,22155,169191,169449,169392,169400,169778,170193,170313,170346,170435,170536,170766,171354,171419,32415,171768,171811,19620,38215,172691,29090,172799,19857,36882,173515,19868,134300,36798,21953,36794,140464,36793,150163,17673,32383,28502,27313,20202,13540,166700,161949,14138,36480,137205,163876,166764,166809,162366,157359,15851,161365,146615,153141,153942,20122,155265,156248,22207,134765,36366,23405,147080,150686,25566,25296,137206,137339,25904,22061,154698,21530,152337,15814,171416,19581,22050,22046,32585,155352,22901,146752,34672,19996,135146,134473,145082,33047,40286,36120,30267,40005,30286,30649,37701,21554,33096,33527,22053,33074,33816,32957,21994,31074,22083,21526,134813,13774,22021,22001,26353,164578,13869,30004,22000,21946,21655,21874,134209,134294,24272,151880,134774,142434,134818,40619,32090,21982,135285,25245,38765,21652,36045,29174,37238,25596,25529,25598,21865,142147,40050,143027,20890,13535,134567,20903,21581,21790,21779,30310,36397,157834,30129,32950,34820,34694,35015,33206,33820,135361,17644,29444,149254,23440,33547,157843,22139,141044,163119,147875,163187,159440,160438,37232,135641,37384,146684,173737,134828,134905,29286,138402,18254,151490,163833,135147,16634,40029,25887,142752,18675,149472,171388,135148,134666,24674,161187,135149,null,155720,135559,29091,32398,40272,19994,19972,13687,23309,27826,21351,13996,14812,21373,13989,149016,22682,150382,33325,21579,22442,154261,133497,null,14930,140389,29556,171692,19721,39917,146686,171824,19547,151465,169374,171998,33884,146870,160434,157619,145184,25390,32037,147191,146988,14890,36872,21196,15988,13946,17897,132238,30272,23280,134838,30842,163630,22695,16575,22140,39819,23924,30292,173108,40581,19681,30201,14331,24857,143578,148466,null,22109,135849,22439,149859,171526,21044,159918,13741,27722,40316,31830,39737,22494,137068,23635,25811,169168,156469,160100,34477,134440,159010,150242,134513,null,20990,139023,23950,38659,138705,40577,36940,31519,39682,23761,31651,25192,25397,39679,31695,39722,31870,39726,31810,31878,39957,31740,39689,40727,39963,149822,40794,21875,23491,20477,40600,20466,21088,15878,21201,22375,20566,22967,24082,38856,40363,36700,21609,38836,39232,38842,21292,24880,26924,21466,39946,40194,19515,38465,27008,20646,30022,137069,39386,21107,null,37209,38529,37212,null,37201,167575,25471,159011,27338,22033,37262,30074,25221,132092,29519,31856,154657,146685,null,149785,30422,39837,20010,134356,33726,34882,null,23626,27072,20717,22394,21023,24053,20174,27697,131570,20281,21660,21722,21146,36226,13822,24332,13811,null,27474,37244,40869,39831,38958,39092,39610,40616,40580,29050,31508,null,27642,34840,32632,null,22048,173642,36471,40787,null,36308,36431,40476,36353,25218,164733,36392,36469,31443,150135,31294,30936,27882,35431,30215,166490,40742,27854,34774,30147,172722,30803,194624,36108,29410,29553,35629,29442,29937,36075,150203,34351,24506,34976,17591,null,137275,159237,null,35454,140571,null,24829,30311,39639,40260,37742,39823,34805,null,34831,36087,29484,38689,39856,13782,29362,19463,31825,39242,155993,24921,19460,40598,24957,null,22367,24943,25254,25145,25294,14940,25058,21418,144373,25444,26626,13778,23895,166850,36826,167481,null,20697,138566,30982,21298,38456,134971,16485,null,30718,null,31938,155418,31962,31277,32870,32867,32077,29957,29938,35220,33306,26380,32866,160902,32859,29936,33027,30500,35209,157644,30035,159441,34729,34766,33224,34700,35401,36013,35651,30507,29944,34010,13877,27058,36262,null,35241,29800,28089,34753,147473,29927,15835,29046,24740,24988,15569,29026,24695,null,32625,166701,29264,24809,19326,21024,15384,146631,155351,161366,152881,137540,135934,170243,159196,159917,23745,156077,166415,145015,131310,157766,151310,17762,23327,156492,40784,40614,156267,12288,65292,12289,12290,65294,8231,65307,65306,65311,65281,65072,8230,8229,65104,65105,65106,183,65108,65109,65110,65111,65372,8211,65073,8212,65075,9588,65076,65103,65288,65289,65077,65078,65371,65373,65079,65080,12308,12309,65081,65082,12304,12305,65083,65084,12298,12299,65085,65086,12296,12297,65087,65088,12300,12301,65089,65090,12302,12303,65091,65092,65113,65114,65115,65116,65117,65118,8216,8217,8220,8221,12317,12318,8245,8242,65283,65286,65290,8251,167,12291,9675,9679,9651,9650,9678,9734,9733,9671,9670,9633,9632,9661,9660,12963,8453,175,65507,65343,717,65097,65098,65101,65102,65099,65100,65119,65120,65121,65291,65293,215,247,177,8730,65308,65310,65309,8806,8807,8800,8734,8786,8801,65122,65123,65124,65125,65126,65374,8745,8746,8869,8736,8735,8895,13266,13265,8747,8750,8757,8756,9792,9794,8853,8857,8593,8595,8592,8594,8598,8599,8601,8600,8741,8739,65295,65340,8725,65128,65284,65509,12306,65504,65505,65285,65312,8451,8457,65129,65130,65131,13269,13212,13213,13214,13262,13217,13198,13199,13252,176,20825,20827,20830,20829,20833,20835,21991,29929,31950,9601,9602,9603,9604,9605,9606,9607,9608,9615,9614,9613,9612,9611,9610,9609,9532,9524,9516,9508,9500,9620,9472,9474,9621,9484,9488,9492,9496,9581,9582,9584,9583,9552,9566,9578,9569,9698,9699,9701,9700,9585,9586,9587,65296,65297,65298,65299,65300,65301,65302,65303,65304,65305,8544,8545,8546,8547,8548,8549,8550,8551,8552,8553,12321,12322,12323,12324,12325,12326,12327,12328,12329,21313,21316,21317,65313,65314,65315,65316,65317,65318,65319,65320,65321,65322,65323,65324,65325,65326,65327,65328,65329,65330,65331,65332,65333,65334,65335,65336,65337,65338,65345,65346,65347,65348,65349,65350,65351,65352,65353,65354,65355,65356,65357,65358,65359,65360,65361,65362,65363,65364,65365,65366,65367,65368,65369,65370,913,914,915,916,917,918,919,920,921,922,923,924,925,926,927,928,929,931,932,933,934,935,936,937,945,946,947,948,949,950,951,952,953,954,955,956,957,958,959,960,961,963,964,965,966,967,968,969,12549,12550,12551,12552,12553,12554,12555,12556,12557,12558,12559,12560,12561,12562,12563,12564,12565,12566,12567,12568,12569,12570,12571,12572,12573,12574,12575,12576,12577,12578,12579,12580,12581,12582,12583,12584,12585,729,713,714,711,715,9216,9217,9218,9219,9220,9221,9222,9223,9224,9225,9226,9227,9228,9229,9230,9231,9232,9233,9234,9235,9236,9237,9238,9239,9240,9241,9242,9243,9244,9245,9246,9247,9249,8364,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,19968,20057,19969,19971,20035,20061,20102,20108,20154,20799,20837,20843,20960,20992,20993,21147,21269,21313,21340,21448,19977,19979,19976,19978,20011,20024,20961,20037,20040,20063,20062,20110,20129,20800,20995,21242,21315,21449,21475,22303,22763,22805,22823,22899,23376,23377,23379,23544,23567,23586,23608,23665,24029,24037,24049,24050,24051,24062,24178,24318,24331,24339,25165,19985,19984,19981,20013,20016,20025,20043,23609,20104,20113,20117,20114,20116,20130,20161,20160,20163,20166,20167,20173,20170,20171,20164,20803,20801,20839,20845,20846,20844,20887,20982,20998,20999,21000,21243,21246,21247,21270,21305,21320,21319,21317,21342,21380,21451,21450,21453,22764,22825,22827,22826,22829,23380,23569,23588,23610,23663,24052,24187,24319,24340,24341,24515,25096,25142,25163,25166,25903,25991,26007,26020,26041,26085,26352,26376,26408,27424,27490,27513,27595,27604,27611,27663,27700,28779,29226,29238,29243,29255,29273,29275,29356,29579,19993,19990,19989,19988,19992,20027,20045,20047,20046,20197,20184,20180,20181,20182,20183,20195,20196,20185,20190,20805,20804,20873,20874,20908,20985,20986,20984,21002,21152,21151,21253,21254,21271,21277,20191,21322,21321,21345,21344,21359,21358,21435,21487,21476,21491,21484,21486,21481,21480,21500,21496,21493,21483,21478,21482,21490,21489,21488,21477,21485,21499,22235,22234,22806,22830,22833,22900,22902,23381,23427,23612,24040,24039,24038,24066,24067,24179,24188,24321,24344,24343,24517,25098,25171,25172,25170,25169,26021,26086,26414,26412,26410,26411,26413,27491,27597,27665,27664,27704,27713,27712,27710,29359,29572,29577,29916,29926,29976,29983,29992,29993,30000,30001,30002,30003,30091,30333,30382,30399,30446,30683,30690,30707,31034,31166,31348,31435,19998,19999,20050,20051,20073,20121,20132,20134,20133,20223,20233,20249,20234,20245,20237,20240,20241,20239,20210,20214,20219,20208,20211,20221,20225,20235,20809,20807,20806,20808,20840,20849,20877,20912,21015,21009,21010,21006,21014,21155,21256,21281,21280,21360,21361,21513,21519,21516,21514,21520,21505,21515,21508,21521,21517,21512,21507,21518,21510,21522,22240,22238,22237,22323,22320,22312,22317,22316,22319,22313,22809,22810,22839,22840,22916,22904,22915,22909,22905,22914,22913,23383,23384,23431,23432,23429,23433,23546,23574,23673,24030,24070,24182,24180,24335,24347,24537,24534,25102,25100,25101,25104,25187,25179,25176,25910,26089,26088,26092,26093,26354,26355,26377,26429,26420,26417,26421,27425,27492,27515,27670,27741,27735,27737,27743,27744,27728,27733,27745,27739,27725,27726,28784,29279,29277,30334,31481,31859,31992,32566,32650,32701,32769,32771,32780,32786,32819,32895,32905,32907,32908,33251,33258,33267,33276,33292,33307,33311,33390,33394,33406,34411,34880,34892,34915,35199,38433,20018,20136,20301,20303,20295,20311,20318,20276,20315,20309,20272,20304,20305,20285,20282,20280,20291,20308,20284,20294,20323,20316,20320,20271,20302,20278,20313,20317,20296,20314,20812,20811,20813,20853,20918,20919,21029,21028,21033,21034,21032,21163,21161,21162,21164,21283,21363,21365,21533,21549,21534,21566,21542,21582,21543,21574,21571,21555,21576,21570,21531,21545,21578,21561,21563,21560,21550,21557,21558,21536,21564,21568,21553,21547,21535,21548,22250,22256,22244,22251,22346,22353,22336,22349,22343,22350,22334,22352,22351,22331,22767,22846,22941,22930,22952,22942,22947,22937,22934,22925,22948,22931,22922,22949,23389,23388,23386,23387,23436,23435,23439,23596,23616,23617,23615,23614,23696,23697,23700,23692,24043,24076,24207,24199,24202,24311,24324,24351,24420,24418,24439,24441,24536,24524,24535,24525,24561,24555,24568,24554,25106,25105,25220,25239,25238,25216,25206,25225,25197,25226,25212,25214,25209,25203,25234,25199,25240,25198,25237,25235,25233,25222,25913,25915,25912,26097,26356,26463,26446,26447,26448,26449,26460,26454,26462,26441,26438,26464,26451,26455,27493,27599,27714,27742,27801,27777,27784,27785,27781,27803,27754,27770,27792,27760,27788,27752,27798,27794,27773,27779,27762,27774,27764,27782,27766,27789,27796,27800,27778,28790,28796,28797,28792,29282,29281,29280,29380,29378,29590,29996,29995,30007,30008,30338,30447,30691,31169,31168,31167,31350,31995,32597,32918,32915,32925,32920,32923,32922,32946,33391,33426,33419,33421,35211,35282,35328,35895,35910,35925,35997,36196,36208,36275,36523,36554,36763,36784,36802,36806,36805,36804,24033,37009,37026,37034,37030,37027,37193,37318,37324,38450,38446,38449,38442,38444,20006,20054,20083,20107,20123,20126,20139,20140,20335,20381,20365,20339,20351,20332,20379,20363,20358,20355,20336,20341,20360,20329,20347,20374,20350,20367,20369,20346,20820,20818,20821,20841,20855,20854,20856,20925,20989,21051,21048,21047,21050,21040,21038,21046,21057,21182,21179,21330,21332,21331,21329,21350,21367,21368,21369,21462,21460,21463,21619,21621,21654,21624,21653,21632,21627,21623,21636,21650,21638,21628,21648,21617,21622,21644,21658,21602,21608,21643,21629,21646,22266,22403,22391,22378,22377,22369,22374,22372,22396,22812,22857,22855,22856,22852,22868,22974,22971,22996,22969,22958,22993,22982,22992,22989,22987,22995,22986,22959,22963,22994,22981,23391,23396,23395,23447,23450,23448,23452,23449,23451,23578,23624,23621,23622,23735,23713,23736,23721,23723,23729,23731,24088,24090,24086,24085,24091,24081,24184,24218,24215,24220,24213,24214,24310,24358,24359,24361,24448,24449,24447,24444,24541,24544,24573,24565,24575,24591,24596,24623,24629,24598,24618,24597,24609,24615,24617,24619,24603,25110,25109,25151,25150,25152,25215,25289,25292,25284,25279,25282,25273,25298,25307,25259,25299,25300,25291,25288,25256,25277,25276,25296,25305,25287,25293,25269,25306,25265,25304,25302,25303,25286,25260,25294,25918,26023,26044,26106,26132,26131,26124,26118,26114,26126,26112,26127,26133,26122,26119,26381,26379,26477,26507,26517,26481,26524,26483,26487,26503,26525,26519,26479,26480,26495,26505,26494,26512,26485,26522,26515,26492,26474,26482,27427,27494,27495,27519,27667,27675,27875,27880,27891,27825,27852,27877,27827,27837,27838,27836,27874,27819,27861,27859,27832,27844,27833,27841,27822,27863,27845,27889,27839,27835,27873,27867,27850,27820,27887,27868,27862,27872,28821,28814,28818,28810,28825,29228,29229,29240,29256,29287,29289,29376,29390,29401,29399,29392,29609,29608,29599,29611,29605,30013,30109,30105,30106,30340,30402,30450,30452,30693,30717,31038,31040,31041,31177,31176,31354,31353,31482,31998,32596,32652,32651,32773,32954,32933,32930,32945,32929,32939,32937,32948,32938,32943,33253,33278,33293,33459,33437,33433,33453,33469,33439,33465,33457,33452,33445,33455,33464,33443,33456,33470,33463,34382,34417,21021,34920,36555,36814,36820,36817,37045,37048,37041,37046,37319,37329,38263,38272,38428,38464,38463,38459,38468,38466,38585,38632,38738,38750,20127,20141,20142,20449,20405,20399,20415,20448,20433,20431,20445,20419,20406,20440,20447,20426,20439,20398,20432,20420,20418,20442,20430,20446,20407,20823,20882,20881,20896,21070,21059,21066,21069,21068,21067,21063,21191,21193,21187,21185,21261,21335,21371,21402,21467,21676,21696,21672,21710,21705,21688,21670,21683,21703,21698,21693,21674,21697,21700,21704,21679,21675,21681,21691,21673,21671,21695,22271,22402,22411,22432,22435,22434,22478,22446,22419,22869,22865,22863,22862,22864,23004,23000,23039,23011,23016,23043,23013,23018,23002,23014,23041,23035,23401,23459,23462,23460,23458,23461,23553,23630,23631,23629,23627,23769,23762,24055,24093,24101,24095,24189,24224,24230,24314,24328,24365,24421,24456,24453,24458,24459,24455,24460,24457,24594,24605,24608,24613,24590,24616,24653,24688,24680,24674,24646,24643,24684,24683,24682,24676,25153,25308,25366,25353,25340,25325,25345,25326,25341,25351,25329,25335,25327,25324,25342,25332,25361,25346,25919,25925,26027,26045,26082,26149,26157,26144,26151,26159,26143,26152,26161,26148,26359,26623,26579,26609,26580,26576,26604,26550,26543,26613,26601,26607,26564,26577,26548,26586,26597,26552,26575,26590,26611,26544,26585,26594,26589,26578,27498,27523,27526,27573,27602,27607,27679,27849,27915,27954,27946,27969,27941,27916,27953,27934,27927,27963,27965,27966,27958,27931,27893,27961,27943,27960,27945,27950,27957,27918,27947,28843,28858,28851,28844,28847,28845,28856,28846,28836,29232,29298,29295,29300,29417,29408,29409,29623,29642,29627,29618,29645,29632,29619,29978,29997,30031,30028,30030,30027,30123,30116,30117,30114,30115,30328,30342,30343,30344,30408,30406,30403,30405,30465,30457,30456,30473,30475,30462,30460,30471,30684,30722,30740,30732,30733,31046,31049,31048,31047,31161,31162,31185,31186,31179,31359,31361,31487,31485,31869,32002,32005,32000,32009,32007,32004,32006,32568,32654,32703,32772,32784,32781,32785,32822,32982,32997,32986,32963,32964,32972,32993,32987,32974,32990,32996,32989,33268,33314,33511,33539,33541,33507,33499,33510,33540,33509,33538,33545,33490,33495,33521,33537,33500,33492,33489,33502,33491,33503,33519,33542,34384,34425,34427,34426,34893,34923,35201,35284,35336,35330,35331,35998,36000,36212,36211,36276,36557,36556,36848,36838,36834,36842,36837,36845,36843,36836,36840,37066,37070,37057,37059,37195,37194,37325,38274,38480,38475,38476,38477,38754,38761,38859,38893,38899,38913,39080,39131,39135,39318,39321,20056,20147,20492,20493,20515,20463,20518,20517,20472,20521,20502,20486,20540,20511,20506,20498,20497,20474,20480,20500,20520,20465,20513,20491,20505,20504,20467,20462,20525,20522,20478,20523,20489,20860,20900,20901,20898,20941,20940,20934,20939,21078,21084,21076,21083,21085,21290,21375,21407,21405,21471,21736,21776,21761,21815,21756,21733,21746,21766,21754,21780,21737,21741,21729,21769,21742,21738,21734,21799,21767,21757,21775,22275,22276,22466,22484,22475,22467,22537,22799,22871,22872,22874,23057,23064,23068,23071,23067,23059,23020,23072,23075,23081,23077,23052,23049,23403,23640,23472,23475,23478,23476,23470,23477,23481,23480,23556,23633,23637,23632,23789,23805,23803,23786,23784,23792,23798,23809,23796,24046,24109,24107,24235,24237,24231,24369,24466,24465,24464,24665,24675,24677,24656,24661,24685,24681,24687,24708,24735,24730,24717,24724,24716,24709,24726,25159,25331,25352,25343,25422,25406,25391,25429,25410,25414,25423,25417,25402,25424,25405,25386,25387,25384,25421,25420,25928,25929,26009,26049,26053,26178,26185,26191,26179,26194,26188,26181,26177,26360,26388,26389,26391,26657,26680,26696,26694,26707,26681,26690,26708,26665,26803,26647,26700,26705,26685,26612,26704,26688,26684,26691,26666,26693,26643,26648,26689,27530,27529,27575,27683,27687,27688,27686,27684,27888,28010,28053,28040,28039,28006,28024,28023,27993,28051,28012,28041,28014,27994,28020,28009,28044,28042,28025,28037,28005,28052,28874,28888,28900,28889,28872,28879,29241,29305,29436,29433,29437,29432,29431,29574,29677,29705,29678,29664,29674,29662,30036,30045,30044,30042,30041,30142,30149,30151,30130,30131,30141,30140,30137,30146,30136,30347,30384,30410,30413,30414,30505,30495,30496,30504,30697,30768,30759,30776,30749,30772,30775,30757,30765,30752,30751,30770,31061,31056,31072,31071,31062,31070,31069,31063,31066,31204,31203,31207,31199,31206,31209,31192,31364,31368,31449,31494,31505,31881,32033,32023,32011,32010,32032,32034,32020,32016,32021,32026,32028,32013,32025,32027,32570,32607,32660,32709,32705,32774,32792,32789,32793,32791,32829,32831,33009,33026,33008,33029,33005,33012,33030,33016,33011,33032,33021,33034,33020,33007,33261,33260,33280,33296,33322,33323,33320,33324,33467,33579,33618,33620,33610,33592,33616,33609,33589,33588,33615,33586,33593,33590,33559,33600,33585,33576,33603,34388,34442,34474,34451,34468,34473,34444,34467,34460,34928,34935,34945,34946,34941,34937,35352,35344,35342,35340,35349,35338,35351,35347,35350,35343,35345,35912,35962,35961,36001,36002,36215,36524,36562,36564,36559,36785,36865,36870,36855,36864,36858,36852,36867,36861,36869,36856,37013,37089,37085,37090,37202,37197,37196,37336,37341,37335,37340,37337,38275,38498,38499,38497,38491,38493,38500,38488,38494,38587,39138,39340,39592,39640,39717,39730,39740,20094,20602,20605,20572,20551,20547,20556,20570,20553,20581,20598,20558,20565,20597,20596,20599,20559,20495,20591,20589,20828,20885,20976,21098,21103,21202,21209,21208,21205,21264,21263,21273,21311,21312,21310,21443,26364,21830,21866,21862,21828,21854,21857,21827,21834,21809,21846,21839,21845,21807,21860,21816,21806,21852,21804,21859,21811,21825,21847,22280,22283,22281,22495,22533,22538,22534,22496,22500,22522,22530,22581,22519,22521,22816,22882,23094,23105,23113,23142,23146,23104,23100,23138,23130,23110,23114,23408,23495,23493,23492,23490,23487,23494,23561,23560,23559,23648,23644,23645,23815,23814,23822,23835,23830,23842,23825,23849,23828,23833,23844,23847,23831,24034,24120,24118,24115,24119,24247,24248,24246,24245,24254,24373,24375,24407,24428,24425,24427,24471,24473,24478,24472,24481,24480,24476,24703,24739,24713,24736,24744,24779,24756,24806,24765,24773,24763,24757,24796,24764,24792,24789,24774,24799,24760,24794,24775,25114,25115,25160,25504,25511,25458,25494,25506,25509,25463,25447,25496,25514,25457,25513,25481,25475,25499,25451,25512,25476,25480,25497,25505,25516,25490,25487,25472,25467,25449,25448,25466,25949,25942,25937,25945,25943,21855,25935,25944,25941,25940,26012,26011,26028,26063,26059,26060,26062,26205,26202,26212,26216,26214,26206,26361,21207,26395,26753,26799,26786,26771,26805,26751,26742,26801,26791,26775,26800,26755,26820,26797,26758,26757,26772,26781,26792,26783,26785,26754,27442,27578,27627,27628,27691,28046,28092,28147,28121,28082,28129,28108,28132,28155,28154,28165,28103,28107,28079,28113,28078,28126,28153,28088,28151,28149,28101,28114,28186,28085,28122,28139,28120,28138,28145,28142,28136,28102,28100,28074,28140,28095,28134,28921,28937,28938,28925,28911,29245,29309,29313,29468,29467,29462,29459,29465,29575,29701,29706,29699,29702,29694,29709,29920,29942,29943,29980,29986,30053,30054,30050,30064,30095,30164,30165,30133,30154,30157,30350,30420,30418,30427,30519,30526,30524,30518,30520,30522,30827,30787,30798,31077,31080,31085,31227,31378,31381,31520,31528,31515,31532,31526,31513,31518,31534,31890,31895,31893,32070,32067,32113,32046,32057,32060,32064,32048,32051,32068,32047,32066,32050,32049,32573,32670,32666,32716,32718,32722,32796,32842,32838,33071,33046,33059,33067,33065,33072,33060,33282,33333,33335,33334,33337,33678,33694,33688,33656,33698,33686,33725,33707,33682,33674,33683,33673,33696,33655,33659,33660,33670,33703,34389,24426,34503,34496,34486,34500,34485,34502,34507,34481,34479,34505,34899,34974,34952,34987,34962,34966,34957,34955,35219,35215,35370,35357,35363,35365,35377,35373,35359,35355,35362,35913,35930,36009,36012,36011,36008,36010,36007,36199,36198,36286,36282,36571,36575,36889,36877,36890,36887,36899,36895,36893,36880,36885,36894,36896,36879,36898,36886,36891,36884,37096,37101,37117,37207,37326,37365,37350,37347,37351,37357,37353,38281,38506,38517,38515,38520,38512,38516,38518,38519,38508,38592,38634,38633,31456,31455,38914,38915,39770,40165,40565,40575,40613,40635,20642,20621,20613,20633,20625,20608,20630,20632,20634,26368,20977,21106,21108,21109,21097,21214,21213,21211,21338,21413,21883,21888,21927,21884,21898,21917,21912,21890,21916,21930,21908,21895,21899,21891,21939,21934,21919,21822,21938,21914,21947,21932,21937,21886,21897,21931,21913,22285,22575,22570,22580,22564,22576,22577,22561,22557,22560,22777,22778,22880,23159,23194,23167,23186,23195,23207,23411,23409,23506,23500,23507,23504,23562,23563,23601,23884,23888,23860,23879,24061,24133,24125,24128,24131,24190,24266,24257,24258,24260,24380,24429,24489,24490,24488,24785,24801,24754,24758,24800,24860,24867,24826,24853,24816,24827,24820,24936,24817,24846,24822,24841,24832,24850,25119,25161,25507,25484,25551,25536,25577,25545,25542,25549,25554,25571,25552,25569,25558,25581,25582,25462,25588,25578,25563,25682,25562,25593,25950,25958,25954,25955,26001,26000,26031,26222,26224,26228,26230,26223,26257,26234,26238,26231,26366,26367,26399,26397,26874,26837,26848,26840,26839,26885,26847,26869,26862,26855,26873,26834,26866,26851,26827,26829,26893,26898,26894,26825,26842,26990,26875,27454,27450,27453,27544,27542,27580,27631,27694,27695,27692,28207,28216,28244,28193,28210,28263,28234,28192,28197,28195,28187,28251,28248,28196,28246,28270,28205,28198,28271,28212,28237,28218,28204,28227,28189,28222,28363,28297,28185,28238,28259,28228,28274,28265,28255,28953,28954,28966,28976,28961,28982,29038,28956,29260,29316,29312,29494,29477,29492,29481,29754,29738,29747,29730,29733,29749,29750,29748,29743,29723,29734,29736,29989,29990,30059,30058,30178,30171,30179,30169,30168,30174,30176,30331,30332,30358,30355,30388,30428,30543,30701,30813,30828,30831,31245,31240,31243,31237,31232,31384,31383,31382,31461,31459,31561,31574,31558,31568,31570,31572,31565,31563,31567,31569,31903,31909,32094,32080,32104,32085,32043,32110,32114,32097,32102,32098,32112,32115,21892,32724,32725,32779,32850,32901,33109,33108,33099,33105,33102,33081,33094,33086,33100,33107,33140,33298,33308,33769,33795,33784,33805,33760,33733,33803,33729,33775,33777,33780,33879,33802,33776,33804,33740,33789,33778,33738,33848,33806,33796,33756,33799,33748,33759,34395,34527,34521,34541,34516,34523,34532,34512,34526,34903,35009,35010,34993,35203,35222,35387,35424,35413,35422,35388,35393,35412,35419,35408,35398,35380,35386,35382,35414,35937,35970,36015,36028,36019,36029,36033,36027,36032,36020,36023,36022,36031,36024,36234,36229,36225,36302,36317,36299,36314,36305,36300,36315,36294,36603,36600,36604,36764,36910,36917,36913,36920,36914,36918,37122,37109,37129,37118,37219,37221,37327,37396,37397,37411,37385,37406,37389,37392,37383,37393,38292,38287,38283,38289,38291,38290,38286,38538,38542,38539,38525,38533,38534,38541,38514,38532,38593,38597,38596,38598,38599,38639,38642,38860,38917,38918,38920,39143,39146,39151,39145,39154,39149,39342,39341,40643,40653,40657,20098,20653,20661,20658,20659,20677,20670,20652,20663,20667,20655,20679,21119,21111,21117,21215,21222,21220,21218,21219,21295,21983,21992,21971,21990,21966,21980,21959,21969,21987,21988,21999,21978,21985,21957,21958,21989,21961,22290,22291,22622,22609,22616,22615,22618,22612,22635,22604,22637,22602,22626,22610,22603,22887,23233,23241,23244,23230,23229,23228,23219,23234,23218,23913,23919,24140,24185,24265,24264,24338,24409,24492,24494,24858,24847,24904,24863,24819,24859,24825,24833,24840,24910,24908,24900,24909,24894,24884,24871,24845,24838,24887,25121,25122,25619,25662,25630,25642,25645,25661,25644,25615,25628,25620,25613,25654,25622,25623,25606,25964,26015,26032,26263,26249,26247,26248,26262,26244,26264,26253,26371,27028,26989,26970,26999,26976,26964,26997,26928,27010,26954,26984,26987,26974,26963,27001,27014,26973,26979,26971,27463,27506,27584,27583,27603,27645,28322,28335,28371,28342,28354,28304,28317,28359,28357,28325,28312,28348,28346,28331,28369,28310,28316,28356,28372,28330,28327,28340,29006,29017,29033,29028,29001,29031,29020,29036,29030,29004,29029,29022,28998,29032,29014,29242,29266,29495,29509,29503,29502,29807,29786,29781,29791,29790,29761,29759,29785,29787,29788,30070,30072,30208,30192,30209,30194,30193,30202,30207,30196,30195,30430,30431,30555,30571,30566,30558,30563,30585,30570,30572,30556,30565,30568,30562,30702,30862,30896,30871,30872,30860,30857,30844,30865,30867,30847,31098,31103,31105,33836,31165,31260,31258,31264,31252,31263,31262,31391,31392,31607,31680,31584,31598,31591,31921,31923,31925,32147,32121,32145,32129,32143,32091,32622,32617,32618,32626,32681,32680,32676,32854,32856,32902,32900,33137,33136,33144,33125,33134,33139,33131,33145,33146,33126,33285,33351,33922,33911,33853,33841,33909,33894,33899,33865,33900,33883,33852,33845,33889,33891,33897,33901,33862,34398,34396,34399,34553,34579,34568,34567,34560,34558,34555,34562,34563,34566,34570,34905,35039,35028,35033,35036,35032,35037,35041,35018,35029,35026,35228,35299,35435,35442,35443,35430,35433,35440,35463,35452,35427,35488,35441,35461,35437,35426,35438,35436,35449,35451,35390,35432,35938,35978,35977,36042,36039,36040,36036,36018,36035,36034,36037,36321,36319,36328,36335,36339,36346,36330,36324,36326,36530,36611,36617,36606,36618,36767,36786,36939,36938,36947,36930,36948,36924,36949,36944,36935,36943,36942,36941,36945,36926,36929,37138,37143,37228,37226,37225,37321,37431,37463,37432,37437,37440,37438,37467,37451,37476,37457,37428,37449,37453,37445,37433,37439,37466,38296,38552,38548,38549,38605,38603,38601,38602,38647,38651,38649,38646,38742,38772,38774,38928,38929,38931,38922,38930,38924,39164,39156,39165,39166,39347,39345,39348,39649,40169,40578,40718,40723,40736,20711,20718,20709,20694,20717,20698,20693,20687,20689,20721,20686,20713,20834,20979,21123,21122,21297,21421,22014,22016,22043,22039,22013,22036,22022,22025,22029,22030,22007,22038,22047,22024,22032,22006,22296,22294,22645,22654,22659,22675,22666,22649,22661,22653,22781,22821,22818,22820,22890,22889,23265,23270,23273,23255,23254,23256,23267,23413,23518,23527,23521,23525,23526,23528,23522,23524,23519,23565,23650,23940,23943,24155,24163,24149,24151,24148,24275,24278,24330,24390,24432,24505,24903,24895,24907,24951,24930,24931,24927,24922,24920,24949,25130,25735,25688,25684,25764,25720,25695,25722,25681,25703,25652,25709,25723,25970,26017,26071,26070,26274,26280,26269,27036,27048,27029,27073,27054,27091,27083,27035,27063,27067,27051,27060,27088,27085,27053,27084,27046,27075,27043,27465,27468,27699,28467,28436,28414,28435,28404,28457,28478,28448,28460,28431,28418,28450,28415,28399,28422,28465,28472,28466,28451,28437,28459,28463,28552,28458,28396,28417,28402,28364,28407,29076,29081,29053,29066,29060,29074,29246,29330,29334,29508,29520,29796,29795,29802,29808,29805,29956,30097,30247,30221,30219,30217,30227,30433,30435,30596,30589,30591,30561,30913,30879,30887,30899,30889,30883,31118,31119,31117,31278,31281,31402,31401,31469,31471,31649,31637,31627,31605,31639,31645,31636,31631,31672,31623,31620,31929,31933,31934,32187,32176,32156,32189,32190,32160,32202,32180,32178,32177,32186,32162,32191,32181,32184,32173,32210,32199,32172,32624,32736,32737,32735,32862,32858,32903,33104,33152,33167,33160,33162,33151,33154,33255,33274,33287,33300,33310,33355,33993,33983,33990,33988,33945,33950,33970,33948,33995,33976,33984,34003,33936,33980,34001,33994,34623,34588,34619,34594,34597,34612,34584,34645,34615,34601,35059,35074,35060,35065,35064,35069,35048,35098,35055,35494,35468,35486,35491,35469,35489,35475,35492,35498,35493,35496,35480,35473,35482,35495,35946,35981,35980,36051,36049,36050,36203,36249,36245,36348,36628,36626,36629,36627,36771,36960,36952,36956,36963,36953,36958,36962,36957,36955,37145,37144,37150,37237,37240,37239,37236,37496,37504,37509,37528,37526,37499,37523,37532,37544,37500,37521,38305,38312,38313,38307,38309,38308,38553,38556,38555,38604,38610,38656,38780,38789,38902,38935,38936,39087,39089,39171,39173,39180,39177,39361,39599,39600,39654,39745,39746,40180,40182,40179,40636,40763,40778,20740,20736,20731,20725,20729,20738,20744,20745,20741,20956,21127,21128,21129,21133,21130,21232,21426,22062,22075,22073,22066,22079,22068,22057,22099,22094,22103,22132,22070,22063,22064,22656,22687,22686,22707,22684,22702,22697,22694,22893,23305,23291,23307,23285,23308,23304,23534,23532,23529,23531,23652,23653,23965,23956,24162,24159,24161,24290,24282,24287,24285,24291,24288,24392,24433,24503,24501,24950,24935,24942,24925,24917,24962,24956,24944,24939,24958,24999,24976,25003,24974,25004,24986,24996,24980,25006,25134,25705,25711,25721,25758,25778,25736,25744,25776,25765,25747,25749,25769,25746,25774,25773,25771,25754,25772,25753,25762,25779,25973,25975,25976,26286,26283,26292,26289,27171,27167,27112,27137,27166,27161,27133,27169,27155,27146,27123,27138,27141,27117,27153,27472,27470,27556,27589,27590,28479,28540,28548,28497,28518,28500,28550,28525,28507,28536,28526,28558,28538,28528,28516,28567,28504,28373,28527,28512,28511,29087,29100,29105,29096,29270,29339,29518,29527,29801,29835,29827,29822,29824,30079,30240,30249,30239,30244,30246,30241,30242,30362,30394,30436,30606,30599,30604,30609,30603,30923,30917,30906,30922,30910,30933,30908,30928,31295,31292,31296,31293,31287,31291,31407,31406,31661,31665,31684,31668,31686,31687,31681,31648,31692,31946,32224,32244,32239,32251,32216,32236,32221,32232,32227,32218,32222,32233,32158,32217,32242,32249,32629,32631,32687,32745,32806,33179,33180,33181,33184,33178,33176,34071,34109,34074,34030,34092,34093,34067,34065,34083,34081,34068,34028,34085,34047,34054,34690,34676,34678,34656,34662,34680,34664,34649,34647,34636,34643,34907,34909,35088,35079,35090,35091,35093,35082,35516,35538,35527,35524,35477,35531,35576,35506,35529,35522,35519,35504,35542,35533,35510,35513,35547,35916,35918,35948,36064,36062,36070,36068,36076,36077,36066,36067,36060,36074,36065,36205,36255,36259,36395,36368,36381,36386,36367,36393,36383,36385,36382,36538,36637,36635,36639,36649,36646,36650,36636,36638,36645,36969,36974,36968,36973,36983,37168,37165,37159,37169,37255,37257,37259,37251,37573,37563,37559,37610,37548,37604,37569,37555,37564,37586,37575,37616,37554,38317,38321,38660,38662,38663,38665,38752,38797,38795,38799,38945,38955,38940,39091,39178,39187,39186,39192,39389,39376,39391,39387,39377,39381,39378,39385,39607,39662,39663,39719,39749,39748,39799,39791,40198,40201,40195,40617,40638,40654,22696,40786,20754,20760,20756,20752,20757,20864,20906,20957,21137,21139,21235,22105,22123,22137,22121,22116,22136,22122,22120,22117,22129,22127,22124,22114,22134,22721,22718,22727,22725,22894,23325,23348,23416,23536,23566,24394,25010,24977,25001,24970,25037,25014,25022,25034,25032,25136,25797,25793,25803,25787,25788,25818,25796,25799,25794,25805,25791,25810,25812,25790,25972,26310,26313,26297,26308,26311,26296,27197,27192,27194,27225,27243,27224,27193,27204,27234,27233,27211,27207,27189,27231,27208,27481,27511,27653,28610,28593,28577,28611,28580,28609,28583,28595,28608,28601,28598,28582,28576,28596,29118,29129,29136,29138,29128,29141,29113,29134,29145,29148,29123,29124,29544,29852,29859,29848,29855,29854,29922,29964,29965,30260,30264,30266,30439,30437,30624,30622,30623,30629,30952,30938,30956,30951,31142,31309,31310,31302,31308,31307,31418,31705,31761,31689,31716,31707,31713,31721,31718,31957,31958,32266,32273,32264,32283,32291,32286,32285,32265,32272,32633,32690,32752,32753,32750,32808,33203,33193,33192,33275,33288,33368,33369,34122,34137,34120,34152,34153,34115,34121,34157,34154,34142,34691,34719,34718,34722,34701,34913,35114,35122,35109,35115,35105,35242,35238,35558,35578,35563,35569,35584,35548,35559,35566,35582,35585,35586,35575,35565,35571,35574,35580,35947,35949,35987,36084,36420,36401,36404,36418,36409,36405,36667,36655,36664,36659,36776,36774,36981,36980,36984,36978,36988,36986,37172,37266,37664,37686,37624,37683,37679,37666,37628,37675,37636,37658,37648,37670,37665,37653,37678,37657,38331,38567,38568,38570,38613,38670,38673,38678,38669,38675,38671,38747,38748,38758,38808,38960,38968,38971,38967,38957,38969,38948,39184,39208,39198,39195,39201,39194,39405,39394,39409,39608,39612,39675,39661,39720,39825,40213,40227,40230,40232,40210,40219,40664,40660,40845,40860,20778,20767,20769,20786,21237,22158,22144,22160,22149,22151,22159,22741,22739,22737,22734,23344,23338,23332,23418,23607,23656,23996,23994,23997,23992,24171,24396,24509,25033,25026,25031,25062,25035,25138,25140,25806,25802,25816,25824,25840,25830,25836,25841,25826,25837,25986,25987,26329,26326,27264,27284,27268,27298,27292,27355,27299,27262,27287,27280,27296,27484,27566,27610,27656,28632,28657,28639,28640,28635,28644,28651,28655,28544,28652,28641,28649,28629,28654,28656,29159,29151,29166,29158,29157,29165,29164,29172,29152,29237,29254,29552,29554,29865,29872,29862,29864,30278,30274,30284,30442,30643,30634,30640,30636,30631,30637,30703,30967,30970,30964,30959,30977,31143,31146,31319,31423,31751,31757,31742,31735,31756,31712,31968,31964,31966,31970,31967,31961,31965,32302,32318,32326,32311,32306,32323,32299,32317,32305,32325,32321,32308,32313,32328,32309,32319,32303,32580,32755,32764,32881,32882,32880,32879,32883,33222,33219,33210,33218,33216,33215,33213,33225,33214,33256,33289,33393,34218,34180,34174,34204,34193,34196,34223,34203,34183,34216,34186,34407,34752,34769,34739,34770,34758,34731,34747,34746,34760,34763,35131,35126,35140,35128,35133,35244,35598,35607,35609,35611,35594,35616,35613,35588,35600,35905,35903,35955,36090,36093,36092,36088,36091,36264,36425,36427,36424,36426,36676,36670,36674,36677,36671,36991,36989,36996,36993,36994,36992,37177,37283,37278,37276,37709,37762,37672,37749,37706,37733,37707,37656,37758,37740,37723,37744,37722,37716,38346,38347,38348,38344,38342,38577,38584,38614,38684,38686,38816,38867,38982,39094,39221,39425,39423,39854,39851,39850,39853,40251,40255,40587,40655,40670,40668,40669,40667,40766,40779,21474,22165,22190,22745,22744,23352,24413,25059,25139,25844,25842,25854,25862,25850,25851,25847,26039,26332,26406,27315,27308,27331,27323,27320,27330,27310,27311,27487,27512,27567,28681,28683,28670,28678,28666,28689,28687,29179,29180,29182,29176,29559,29557,29863,29887,29973,30294,30296,30290,30653,30655,30651,30652,30990,31150,31329,31330,31328,31428,31429,31787,31783,31786,31774,31779,31777,31975,32340,32341,32350,32346,32353,32338,32345,32584,32761,32763,32887,32886,33229,33231,33290,34255,34217,34253,34256,34249,34224,34234,34233,34214,34799,34796,34802,34784,35206,35250,35316,35624,35641,35628,35627,35920,36101,36441,36451,36454,36452,36447,36437,36544,36681,36685,36999,36995,37000,37291,37292,37328,37780,37770,37782,37794,37811,37806,37804,37808,37784,37786,37783,38356,38358,38352,38357,38626,38620,38617,38619,38622,38692,38819,38822,38829,38905,38989,38991,38988,38990,38995,39098,39230,39231,39229,39214,39333,39438,39617,39683,39686,39759,39758,39757,39882,39881,39933,39880,39872,40273,40285,40288,40672,40725,40748,20787,22181,22750,22751,22754,23541,40848,24300,25074,25079,25078,25077,25856,25871,26336,26333,27365,27357,27354,27347,28699,28703,28712,28698,28701,28693,28696,29190,29197,29272,29346,29560,29562,29885,29898,29923,30087,30086,30303,30305,30663,31001,31153,31339,31337,31806,31807,31800,31805,31799,31808,32363,32365,32377,32361,32362,32645,32371,32694,32697,32696,33240,34281,34269,34282,34261,34276,34277,34295,34811,34821,34829,34809,34814,35168,35167,35158,35166,35649,35676,35672,35657,35674,35662,35663,35654,35673,36104,36106,36476,36466,36487,36470,36460,36474,36468,36692,36686,36781,37002,37003,37297,37294,37857,37841,37855,37827,37832,37852,37853,37846,37858,37837,37848,37860,37847,37864,38364,38580,38627,38698,38695,38753,38876,38907,39006,39000,39003,39100,39237,39241,39446,39449,39693,39912,39911,39894,39899,40329,40289,40306,40298,40300,40594,40599,40595,40628,21240,22184,22199,22198,22196,22204,22756,23360,23363,23421,23542,24009,25080,25082,25880,25876,25881,26342,26407,27372,28734,28720,28722,29200,29563,29903,30306,30309,31014,31018,31020,31019,31431,31478,31820,31811,31821,31983,31984,36782,32381,32380,32386,32588,32768,33242,33382,34299,34297,34321,34298,34310,34315,34311,34314,34836,34837,35172,35258,35320,35696,35692,35686,35695,35679,35691,36111,36109,36489,36481,36485,36482,37300,37323,37912,37891,37885,38369,38704,39108,39250,39249,39336,39467,39472,39479,39477,39955,39949,40569,40629,40680,40751,40799,40803,40801,20791,20792,22209,22208,22210,22804,23660,24013,25084,25086,25885,25884,26005,26345,27387,27396,27386,27570,28748,29211,29351,29910,29908,30313,30675,31824,32399,32396,32700,34327,34349,34330,34851,34850,34849,34847,35178,35180,35261,35700,35703,35709,36115,36490,36493,36491,36703,36783,37306,37934,37939,37941,37946,37944,37938,37931,38370,38712,38713,38706,38911,39015,39013,39255,39493,39491,39488,39486,39631,39764,39761,39981,39973,40367,40372,40386,40376,40605,40687,40729,40796,40806,40807,20796,20795,22216,22218,22217,23423,24020,24018,24398,25087,25892,27402,27489,28753,28760,29568,29924,30090,30318,30316,31155,31840,31839,32894,32893,33247,35186,35183,35324,35712,36118,36119,36497,36499,36705,37192,37956,37969,37970,38717,38718,38851,38849,39019,39253,39509,39501,39634,39706,40009,39985,39998,39995,40403,40407,40756,40812,40810,40852,22220,24022,25088,25891,25899,25898,26348,27408,29914,31434,31844,31843,31845,32403,32406,32404,33250,34360,34367,34865,35722,37008,37007,37987,37984,37988,38760,39023,39260,39514,39515,39511,39635,39636,39633,40020,40023,40022,40421,40607,40692,22225,22761,25900,28766,30321,30322,30679,32592,32648,34870,34873,34914,35731,35730,35734,33399,36123,37312,37994,38722,38728,38724,38854,39024,39519,39714,39768,40031,40441,40442,40572,40573,40711,40823,40818,24307,27414,28771,31852,31854,34875,35264,36513,37313,38002,38000,39025,39262,39638,39715,40652,28772,30682,35738,38007,38857,39522,39525,32412,35740,36522,37317,38013,38014,38012,40055,40056,40695,35924,38015,40474,29224,39530,39729,40475,40478,31858,9312,9313,9314,9315,9316,9317,9318,9319,9320,9321,9332,9333,9334,9335,9336,9337,9338,9339,9340,9341,8560,8561,8562,8563,8564,8565,8566,8567,8568,8569,20022,20031,20101,20128,20866,20886,20907,21241,21304,21353,21430,22794,23424,24027,12083,24191,24308,24400,24417,25908,26080,30098,30326,36789,38582,168,710,12541,12542,12445,12446,12291,20189,12293,12294,12295,12540,65339,65341,10045,12353,12354,12355,12356,12357,12358,12359,12360,12361,12362,12363,12364,12365,12366,12367,12368,12369,12370,12371,12372,12373,12374,12375,12376,12377,12378,12379,12380,12381,12382,12383,12384,12385,12386,12387,12388,12389,12390,12391,12392,12393,12394,12395,12396,12397,12398,12399,12400,12401,12402,12403,12404,12405,12406,12407,12408,12409,12410,12411,12412,12413,12414,12415,12416,12417,12418,12419,12420,12421,12422,12423,12424,12425,12426,12427,12428,12429,12430,12431,12432,12433,12434,12435,12449,12450,12451,12452,12453,12454,12455,12456,12457,12458,12459,12460,12461,12462,12463,12464,12465,12466,12467,12468,12469,12470,12471,12472,12473,12474,12475,12476,12477,12478,12479,12480,12481,12482,12483,12484,12485,12486,12487,12488,12489,12490,12491,12492,12493,12494,12495,12496,12497,12498,12499,12500,12501,12502,12503,12504,12505,12506,12507,12508,12509,12510,12511,12512,12513,12514,12515,12516,12517,12518,12519,12520,12521,12522,12523,12524,12525,12526,12527,12528,12529,12530,12531,12532,12533,12534,1040,1041,1042,1043,1044,1045,1025,1046,1047,1048,1049,1050,1051,1052,1053,1054,1055,1056,1057,1058,1059,1060,1061,1062,1063,1064,1065,1066,1067,1068,1069,1070,1071,1072,1073,1074,1075,1076,1077,1105,1078,1079,1080,1081,1082,1083,1084,1085,1086,1087,1088,1089,1090,1091,1092,1093,1094,1095,1096,1097,1098,1099,1100,1101,1102,1103,8679,8632,8633,12751,131276,20058,131210,20994,17553,40880,20872,40881,161287,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,65506,65508,65287,65282,12849,8470,8481,12443,12444,11904,11908,11910,11911,11912,11914,11916,11917,11925,11932,11933,11941,11943,11946,11948,11950,11958,11964,11966,11974,11978,11980,11981,11983,11990,11991,11998,12003,null,null,null,643,592,603,596,629,339,248,331,650,618,20034,20060,20981,21274,21378,19975,19980,20039,20109,22231,64012,23662,24435,19983,20871,19982,20014,20115,20162,20169,20168,20888,21244,21356,21433,22304,22787,22828,23568,24063,26081,27571,27596,27668,29247,20017,20028,20200,20188,20201,20193,20189,20186,21004,21276,21324,22306,22307,22807,22831,23425,23428,23570,23611,23668,23667,24068,24192,24194,24521,25097,25168,27669,27702,27715,27711,27707,29358,29360,29578,31160,32906,38430,20238,20248,20268,20213,20244,20209,20224,20215,20232,20253,20226,20229,20258,20243,20228,20212,20242,20913,21011,21001,21008,21158,21282,21279,21325,21386,21511,22241,22239,22318,22314,22324,22844,22912,22908,22917,22907,22910,22903,22911,23382,23573,23589,23676,23674,23675,23678,24031,24181,24196,24322,24346,24436,24533,24532,24527,25180,25182,25188,25185,25190,25186,25177,25184,25178,25189,26095,26094,26430,26425,26424,26427,26426,26431,26428,26419,27672,27718,27730,27740,27727,27722,27732,27723,27724,28785,29278,29364,29365,29582,29994,30335,31349,32593,33400,33404,33408,33405,33407,34381,35198,37017,37015,37016,37019,37012,38434,38436,38432,38435,20310,20283,20322,20297,20307,20324,20286,20327,20306,20319,20289,20312,20269,20275,20287,20321,20879,20921,21020,21022,21025,21165,21166,21257,21347,21362,21390,21391,21552,21559,21546,21588,21573,21529,21532,21541,21528,21565,21583,21569,21544,21540,21575,22254,22247,22245,22337,22341,22348,22345,22347,22354,22790,22848,22950,22936,22944,22935,22926,22946,22928,22927,22951,22945,23438,23442,23592,23594,23693,23695,23688,23691,23689,23698,23690,23686,23699,23701,24032,24074,24078,24203,24201,24204,24200,24205,24325,24349,24440,24438,24530,24529,24528,24557,24552,24558,24563,24545,24548,24547,24570,24559,24567,24571,24576,24564,25146,25219,25228,25230,25231,25236,25223,25201,25211,25210,25200,25217,25224,25207,25213,25202,25204,25911,26096,26100,26099,26098,26101,26437,26439,26457,26453,26444,26440,26461,26445,26458,26443,27600,27673,27674,27768,27751,27755,27780,27787,27791,27761,27759,27753,27802,27757,27783,27797,27804,27750,27763,27749,27771,27790,28788,28794,29283,29375,29373,29379,29382,29377,29370,29381,29589,29591,29587,29588,29586,30010,30009,30100,30101,30337,31037,32820,32917,32921,32912,32914,32924,33424,33423,33413,33422,33425,33427,33418,33411,33412,35960,36809,36799,37023,37025,37029,37022,37031,37024,38448,38440,38447,38445,20019,20376,20348,20357,20349,20352,20359,20342,20340,20361,20356,20343,20300,20375,20330,20378,20345,20353,20344,20368,20380,20372,20382,20370,20354,20373,20331,20334,20894,20924,20926,21045,21042,21043,21062,21041,21180,21258,21259,21308,21394,21396,21639,21631,21633,21649,21634,21640,21611,21626,21630,21605,21612,21620,21606,21645,21615,21601,21600,21656,21603,21607,21604,22263,22265,22383,22386,22381,22379,22385,22384,22390,22400,22389,22395,22387,22388,22370,22376,22397,22796,22853,22965,22970,22991,22990,22962,22988,22977,22966,22972,22979,22998,22961,22973,22976,22984,22964,22983,23394,23397,23443,23445,23620,23623,23726,23716,23712,23733,23727,23720,23724,23711,23715,23725,23714,23722,23719,23709,23717,23734,23728,23718,24087,24084,24089,24360,24354,24355,24356,24404,24450,24446,24445,24542,24549,24621,24614,24601,24626,24587,24628,24586,24599,24627,24602,24606,24620,24610,24589,24592,24622,24595,24593,24588,24585,24604,25108,25149,25261,25268,25297,25278,25258,25270,25290,25262,25267,25263,25275,25257,25264,25272,25917,26024,26043,26121,26108,26116,26130,26120,26107,26115,26123,26125,26117,26109,26129,26128,26358,26378,26501,26476,26510,26514,26486,26491,26520,26502,26500,26484,26509,26508,26490,26527,26513,26521,26499,26493,26497,26488,26489,26516,27429,27520,27518,27614,27677,27795,27884,27883,27886,27865,27830,27860,27821,27879,27831,27856,27842,27834,27843,27846,27885,27890,27858,27869,27828,27786,27805,27776,27870,27840,27952,27853,27847,27824,27897,27855,27881,27857,28820,28824,28805,28819,28806,28804,28817,28822,28802,28826,28803,29290,29398,29387,29400,29385,29404,29394,29396,29402,29388,29393,29604,29601,29613,29606,29602,29600,29612,29597,29917,29928,30015,30016,30014,30092,30104,30383,30451,30449,30448,30453,30712,30716,30713,30715,30714,30711,31042,31039,31173,31352,31355,31483,31861,31997,32821,32911,32942,32931,32952,32949,32941,33312,33440,33472,33451,33434,33432,33435,33461,33447,33454,33468,33438,33466,33460,33448,33441,33449,33474,33444,33475,33462,33442,34416,34415,34413,34414,35926,36818,36811,36819,36813,36822,36821,36823,37042,37044,37039,37043,37040,38457,38461,38460,38458,38467,20429,20421,20435,20402,20425,20427,20417,20436,20444,20441,20411,20403,20443,20423,20438,20410,20416,20409,20460,21060,21065,21184,21186,21309,21372,21399,21398,21401,21400,21690,21665,21677,21669,21711,21699,33549,21687,21678,21718,21686,21701,21702,21664,21616,21692,21666,21694,21618,21726,21680,22453,22430,22431,22436,22412,22423,22429,22427,22420,22424,22415,22425,22437,22426,22421,22772,22797,22867,23009,23006,23022,23040,23025,23005,23034,23037,23036,23030,23012,23026,23031,23003,23017,23027,23029,23008,23038,23028,23021,23464,23628,23760,23768,23756,23767,23755,23771,23774,23770,23753,23751,23754,23766,23763,23764,23759,23752,23750,23758,23775,23800,24057,24097,24098,24099,24096,24100,24240,24228,24226,24219,24227,24229,24327,24366,24406,24454,24631,24633,24660,24690,24670,24645,24659,24647,24649,24667,24652,24640,24642,24671,24612,24644,24664,24678,24686,25154,25155,25295,25357,25355,25333,25358,25347,25323,25337,25359,25356,25336,25334,25344,25363,25364,25338,25365,25339,25328,25921,25923,26026,26047,26166,26145,26162,26165,26140,26150,26146,26163,26155,26170,26141,26164,26169,26158,26383,26384,26561,26610,26568,26554,26588,26555,26616,26584,26560,26551,26565,26603,26596,26591,26549,26573,26547,26615,26614,26606,26595,26562,26553,26574,26599,26608,26546,26620,26566,26605,26572,26542,26598,26587,26618,26569,26570,26563,26602,26571,27432,27522,27524,27574,27606,27608,27616,27680,27681,27944,27956,27949,27935,27964,27967,27922,27914,27866,27955,27908,27929,27962,27930,27921,27904,27933,27970,27905,27928,27959,27907,27919,27968,27911,27936,27948,27912,27938,27913,27920,28855,28831,28862,28849,28848,28833,28852,28853,28841,29249,29257,29258,29292,29296,29299,29294,29386,29412,29416,29419,29407,29418,29414,29411,29573,29644,29634,29640,29637,29625,29622,29621,29620,29675,29631,29639,29630,29635,29638,29624,29643,29932,29934,29998,30023,30024,30119,30122,30329,30404,30472,30467,30468,30469,30474,30455,30459,30458,30695,30696,30726,30737,30738,30725,30736,30735,30734,30729,30723,30739,31050,31052,31051,31045,31044,31189,31181,31183,31190,31182,31360,31358,31441,31488,31489,31866,31864,31865,31871,31872,31873,32003,32008,32001,32600,32657,32653,32702,32775,32782,32783,32788,32823,32984,32967,32992,32977,32968,32962,32976,32965,32995,32985,32988,32970,32981,32969,32975,32983,32998,32973,33279,33313,33428,33497,33534,33529,33543,33512,33536,33493,33594,33515,33494,33524,33516,33505,33522,33525,33548,33531,33526,33520,33514,33508,33504,33530,33523,33517,34423,34420,34428,34419,34881,34894,34919,34922,34921,35283,35332,35335,36210,36835,36833,36846,36832,37105,37053,37055,37077,37061,37054,37063,37067,37064,37332,37331,38484,38479,38481,38483,38474,38478,20510,20485,20487,20499,20514,20528,20507,20469,20468,20531,20535,20524,20470,20471,20503,20508,20512,20519,20533,20527,20529,20494,20826,20884,20883,20938,20932,20933,20936,20942,21089,21082,21074,21086,21087,21077,21090,21197,21262,21406,21798,21730,21783,21778,21735,21747,21732,21786,21759,21764,21768,21739,21777,21765,21745,21770,21755,21751,21752,21728,21774,21763,21771,22273,22274,22476,22578,22485,22482,22458,22470,22461,22460,22456,22454,22463,22471,22480,22457,22465,22798,22858,23065,23062,23085,23086,23061,23055,23063,23050,23070,23091,23404,23463,23469,23468,23555,23638,23636,23788,23807,23790,23793,23799,23808,23801,24105,24104,24232,24238,24234,24236,24371,24368,24423,24669,24666,24679,24641,24738,24712,24704,24722,24705,24733,24707,24725,24731,24727,24711,24732,24718,25113,25158,25330,25360,25430,25388,25412,25413,25398,25411,25572,25401,25419,25418,25404,25385,25409,25396,25432,25428,25433,25389,25415,25395,25434,25425,25400,25431,25408,25416,25930,25926,26054,26051,26052,26050,26186,26207,26183,26193,26386,26387,26655,26650,26697,26674,26675,26683,26699,26703,26646,26673,26652,26677,26667,26669,26671,26702,26692,26676,26653,26642,26644,26662,26664,26670,26701,26682,26661,26656,27436,27439,27437,27441,27444,27501,32898,27528,27622,27620,27624,27619,27618,27623,27685,28026,28003,28004,28022,27917,28001,28050,27992,28002,28013,28015,28049,28045,28143,28031,28038,27998,28007,28000,28055,28016,28028,27999,28034,28056,27951,28008,28043,28030,28032,28036,27926,28035,28027,28029,28021,28048,28892,28883,28881,28893,28875,32569,28898,28887,28882,28894,28896,28884,28877,28869,28870,28871,28890,28878,28897,29250,29304,29303,29302,29440,29434,29428,29438,29430,29427,29435,29441,29651,29657,29669,29654,29628,29671,29667,29673,29660,29650,29659,29652,29661,29658,29655,29656,29672,29918,29919,29940,29941,29985,30043,30047,30128,30145,30139,30148,30144,30143,30134,30138,30346,30409,30493,30491,30480,30483,30482,30499,30481,30485,30489,30490,30498,30503,30755,30764,30754,30773,30767,30760,30766,30763,30753,30761,30771,30762,30769,31060,31067,31055,31068,31059,31058,31057,31211,31212,31200,31214,31213,31210,31196,31198,31197,31366,31369,31365,31371,31372,31370,31367,31448,31504,31492,31507,31493,31503,31496,31498,31502,31497,31506,31876,31889,31882,31884,31880,31885,31877,32030,32029,32017,32014,32024,32022,32019,32031,32018,32015,32012,32604,32609,32606,32608,32605,32603,32662,32658,32707,32706,32704,32790,32830,32825,33018,33010,33017,33013,33025,33019,33024,33281,33327,33317,33587,33581,33604,33561,33617,33573,33622,33599,33601,33574,33564,33570,33602,33614,33563,33578,33544,33596,33613,33558,33572,33568,33591,33583,33577,33607,33605,33612,33619,33566,33580,33611,33575,33608,34387,34386,34466,34472,34454,34445,34449,34462,34439,34455,34438,34443,34458,34437,34469,34457,34465,34471,34453,34456,34446,34461,34448,34452,34883,34884,34925,34933,34934,34930,34944,34929,34943,34927,34947,34942,34932,34940,35346,35911,35927,35963,36004,36003,36214,36216,36277,36279,36278,36561,36563,36862,36853,36866,36863,36859,36868,36860,36854,37078,37088,37081,37082,37091,37087,37093,37080,37083,37079,37084,37092,37200,37198,37199,37333,37346,37338,38492,38495,38588,39139,39647,39727,20095,20592,20586,20577,20574,20576,20563,20555,20573,20594,20552,20557,20545,20571,20554,20578,20501,20549,20575,20585,20587,20579,20580,20550,20544,20590,20595,20567,20561,20944,21099,21101,21100,21102,21206,21203,21293,21404,21877,21878,21820,21837,21840,21812,21802,21841,21858,21814,21813,21808,21842,21829,21772,21810,21861,21838,21817,21832,21805,21819,21824,21835,22282,22279,22523,22548,22498,22518,22492,22516,22528,22509,22525,22536,22520,22539,22515,22479,22535,22510,22499,22514,22501,22508,22497,22542,22524,22544,22503,22529,22540,22513,22505,22512,22541,22532,22876,23136,23128,23125,23143,23134,23096,23093,23149,23120,23135,23141,23148,23123,23140,23127,23107,23133,23122,23108,23131,23112,23182,23102,23117,23097,23116,23152,23145,23111,23121,23126,23106,23132,23410,23406,23489,23488,23641,23838,23819,23837,23834,23840,23820,23848,23821,23846,23845,23823,23856,23826,23843,23839,23854,24126,24116,24241,24244,24249,24242,24243,24374,24376,24475,24470,24479,24714,24720,24710,24766,24752,24762,24787,24788,24783,24804,24793,24797,24776,24753,24795,24759,24778,24767,24771,24781,24768,25394,25445,25482,25474,25469,25533,25502,25517,25501,25495,25515,25486,25455,25479,25488,25454,25519,25461,25500,25453,25518,25468,25508,25403,25503,25464,25477,25473,25489,25485,25456,25939,26061,26213,26209,26203,26201,26204,26210,26392,26745,26759,26768,26780,26733,26734,26798,26795,26966,26735,26787,26796,26793,26741,26740,26802,26767,26743,26770,26748,26731,26738,26794,26752,26737,26750,26779,26774,26763,26784,26761,26788,26744,26747,26769,26764,26762,26749,27446,27443,27447,27448,27537,27535,27533,27534,27532,27690,28096,28075,28084,28083,28276,28076,28137,28130,28087,28150,28116,28160,28104,28128,28127,28118,28094,28133,28124,28125,28123,28148,28106,28093,28141,28144,28090,28117,28098,28111,28105,28112,28146,28115,28157,28119,28109,28131,28091,28922,28941,28919,28951,28916,28940,28912,28932,28915,28944,28924,28927,28934,28947,28928,28920,28918,28939,28930,28942,29310,29307,29308,29311,29469,29463,29447,29457,29464,29450,29448,29439,29455,29470,29576,29686,29688,29685,29700,29697,29693,29703,29696,29690,29692,29695,29708,29707,29684,29704,30052,30051,30158,30162,30159,30155,30156,30161,30160,30351,30345,30419,30521,30511,30509,30513,30514,30516,30515,30525,30501,30523,30517,30792,30802,30793,30797,30794,30796,30758,30789,30800,31076,31079,31081,31082,31075,31083,31073,31163,31226,31224,31222,31223,31375,31380,31376,31541,31559,31540,31525,31536,31522,31524,31539,31512,31530,31517,31537,31531,31533,31535,31538,31544,31514,31523,31892,31896,31894,31907,32053,32061,32056,32054,32058,32069,32044,32041,32065,32071,32062,32063,32074,32059,32040,32611,32661,32668,32669,32667,32714,32715,32717,32720,32721,32711,32719,32713,32799,32798,32795,32839,32835,32840,33048,33061,33049,33051,33069,33055,33068,33054,33057,33045,33063,33053,33058,33297,33336,33331,33338,33332,33330,33396,33680,33699,33704,33677,33658,33651,33700,33652,33679,33665,33685,33689,33653,33684,33705,33661,33667,33676,33693,33691,33706,33675,33662,33701,33711,33672,33687,33712,33663,33702,33671,33710,33654,33690,34393,34390,34495,34487,34498,34497,34501,34490,34480,34504,34489,34483,34488,34508,34484,34491,34492,34499,34493,34494,34898,34953,34965,34984,34978,34986,34970,34961,34977,34975,34968,34983,34969,34971,34967,34980,34988,34956,34963,34958,35202,35286,35289,35285,35376,35367,35372,35358,35897,35899,35932,35933,35965,36005,36221,36219,36217,36284,36290,36281,36287,36289,36568,36574,36573,36572,36567,36576,36577,36900,36875,36881,36892,36876,36897,37103,37098,37104,37108,37106,37107,37076,37099,37100,37097,37206,37208,37210,37203,37205,37356,37364,37361,37363,37368,37348,37369,37354,37355,37367,37352,37358,38266,38278,38280,38524,38509,38507,38513,38511,38591,38762,38916,39141,39319,20635,20629,20628,20638,20619,20643,20611,20620,20622,20637,20584,20636,20626,20610,20615,20831,20948,21266,21265,21412,21415,21905,21928,21925,21933,21879,22085,21922,21907,21896,21903,21941,21889,21923,21906,21924,21885,21900,21926,21887,21909,21921,21902,22284,22569,22583,22553,22558,22567,22563,22568,22517,22600,22565,22556,22555,22579,22591,22582,22574,22585,22584,22573,22572,22587,22881,23215,23188,23199,23162,23202,23198,23160,23206,23164,23205,23212,23189,23214,23095,23172,23178,23191,23171,23179,23209,23163,23165,23180,23196,23183,23187,23197,23530,23501,23499,23508,23505,23498,23502,23564,23600,23863,23875,23915,23873,23883,23871,23861,23889,23886,23893,23859,23866,23890,23869,23857,23897,23874,23865,23881,23864,23868,23858,23862,23872,23877,24132,24129,24408,24486,24485,24491,24777,24761,24780,24802,24782,24772,24852,24818,24842,24854,24837,24821,24851,24824,24828,24830,24769,24835,24856,24861,24848,24831,24836,24843,25162,25492,25521,25520,25550,25573,25576,25583,25539,25757,25587,25546,25568,25590,25557,25586,25589,25697,25567,25534,25565,25564,25540,25560,25555,25538,25543,25548,25547,25544,25584,25559,25561,25906,25959,25962,25956,25948,25960,25957,25996,26013,26014,26030,26064,26066,26236,26220,26235,26240,26225,26233,26218,26226,26369,26892,26835,26884,26844,26922,26860,26858,26865,26895,26838,26871,26859,26852,26870,26899,26896,26867,26849,26887,26828,26888,26992,26804,26897,26863,26822,26900,26872,26832,26877,26876,26856,26891,26890,26903,26830,26824,26845,26846,26854,26868,26833,26886,26836,26857,26901,26917,26823,27449,27451,27455,27452,27540,27543,27545,27541,27581,27632,27634,27635,27696,28156,28230,28231,28191,28233,28296,28220,28221,28229,28258,28203,28223,28225,28253,28275,28188,28211,28235,28224,28241,28219,28163,28206,28254,28264,28252,28257,28209,28200,28256,28273,28267,28217,28194,28208,28243,28261,28199,28280,28260,28279,28245,28281,28242,28262,28213,28214,28250,28960,28958,28975,28923,28974,28977,28963,28965,28962,28978,28959,28968,28986,28955,29259,29274,29320,29321,29318,29317,29323,29458,29451,29488,29474,29489,29491,29479,29490,29485,29478,29475,29493,29452,29742,29740,29744,29739,29718,29722,29729,29741,29745,29732,29731,29725,29737,29728,29746,29947,29999,30063,30060,30183,30170,30177,30182,30173,30175,30180,30167,30357,30354,30426,30534,30535,30532,30541,30533,30538,30542,30539,30540,30686,30700,30816,30820,30821,30812,30829,30833,30826,30830,30832,30825,30824,30814,30818,31092,31091,31090,31088,31234,31242,31235,31244,31236,31385,31462,31460,31562,31547,31556,31560,31564,31566,31552,31576,31557,31906,31902,31912,31905,32088,32111,32099,32083,32086,32103,32106,32079,32109,32092,32107,32082,32084,32105,32081,32095,32078,32574,32575,32613,32614,32674,32672,32673,32727,32849,32847,32848,33022,32980,33091,33098,33106,33103,33095,33085,33101,33082,33254,33262,33271,33272,33273,33284,33340,33341,33343,33397,33595,33743,33785,33827,33728,33768,33810,33767,33764,33788,33782,33808,33734,33736,33771,33763,33727,33793,33757,33765,33752,33791,33761,33739,33742,33750,33781,33737,33801,33807,33758,33809,33798,33730,33779,33749,33786,33735,33745,33770,33811,33731,33772,33774,33732,33787,33751,33762,33819,33755,33790,34520,34530,34534,34515,34531,34522,34538,34525,34539,34524,34540,34537,34519,34536,34513,34888,34902,34901,35002,35031,35001,35000,35008,35006,34998,35004,34999,35005,34994,35073,35017,35221,35224,35223,35293,35290,35291,35406,35405,35385,35417,35392,35415,35416,35396,35397,35410,35400,35409,35402,35404,35407,35935,35969,35968,36026,36030,36016,36025,36021,36228,36224,36233,36312,36307,36301,36295,36310,36316,36303,36309,36313,36296,36311,36293,36591,36599,36602,36601,36582,36590,36581,36597,36583,36584,36598,36587,36593,36588,36596,36585,36909,36916,36911,37126,37164,37124,37119,37116,37128,37113,37115,37121,37120,37127,37125,37123,37217,37220,37215,37218,37216,37377,37386,37413,37379,37402,37414,37391,37388,37376,37394,37375,37373,37382,37380,37415,37378,37404,37412,37401,37399,37381,37398,38267,38285,38284,38288,38535,38526,38536,38537,38531,38528,38594,38600,38595,38641,38640,38764,38768,38766,38919,39081,39147,40166,40697,20099,20100,20150,20669,20671,20678,20654,20676,20682,20660,20680,20674,20656,20673,20666,20657,20683,20681,20662,20664,20951,21114,21112,21115,21116,21955,21979,21964,21968,21963,21962,21981,21952,21972,21956,21993,21951,21970,21901,21967,21973,21986,21974,21960,22002,21965,21977,21954,22292,22611,22632,22628,22607,22605,22601,22639,22613,22606,22621,22617,22629,22619,22589,22627,22641,22780,23239,23236,23243,23226,23224,23217,23221,23216,23231,23240,23227,23238,23223,23232,23242,23220,23222,23245,23225,23184,23510,23512,23513,23583,23603,23921,23907,23882,23909,23922,23916,23902,23912,23911,23906,24048,24143,24142,24138,24141,24139,24261,24268,24262,24267,24263,24384,24495,24493,24823,24905,24906,24875,24901,24886,24882,24878,24902,24879,24911,24873,24896,25120,37224,25123,25125,25124,25541,25585,25579,25616,25618,25609,25632,25636,25651,25667,25631,25621,25624,25657,25655,25634,25635,25612,25638,25648,25640,25665,25653,25647,25610,25626,25664,25637,25639,25611,25575,25627,25646,25633,25614,25967,26002,26067,26246,26252,26261,26256,26251,26250,26265,26260,26232,26400,26982,26975,26936,26958,26978,26993,26943,26949,26986,26937,26946,26967,26969,27002,26952,26953,26933,26988,26931,26941,26981,26864,27000,26932,26985,26944,26991,26948,26998,26968,26945,26996,26956,26939,26955,26935,26972,26959,26961,26930,26962,26927,27003,26940,27462,27461,27459,27458,27464,27457,27547,64013,27643,27644,27641,27639,27640,28315,28374,28360,28303,28352,28319,28307,28308,28320,28337,28345,28358,28370,28349,28353,28318,28361,28343,28336,28365,28326,28367,28338,28350,28355,28380,28376,28313,28306,28302,28301,28324,28321,28351,28339,28368,28362,28311,28334,28323,28999,29012,29010,29027,29024,28993,29021,29026,29042,29048,29034,29025,28994,29016,28995,29003,29040,29023,29008,29011,28996,29005,29018,29263,29325,29324,29329,29328,29326,29500,29506,29499,29498,29504,29514,29513,29764,29770,29771,29778,29777,29783,29760,29775,29776,29774,29762,29766,29773,29780,29921,29951,29950,29949,29981,30073,30071,27011,30191,30223,30211,30199,30206,30204,30201,30200,30224,30203,30198,30189,30197,30205,30361,30389,30429,30549,30559,30560,30546,30550,30554,30569,30567,30548,30553,30573,30688,30855,30874,30868,30863,30852,30869,30853,30854,30881,30851,30841,30873,30848,30870,30843,31100,31106,31101,31097,31249,31256,31257,31250,31255,31253,31266,31251,31259,31248,31395,31394,31390,31467,31590,31588,31597,31604,31593,31602,31589,31603,31601,31600,31585,31608,31606,31587,31922,31924,31919,32136,32134,32128,32141,32127,32133,32122,32142,32123,32131,32124,32140,32148,32132,32125,32146,32621,32619,32615,32616,32620,32678,32677,32679,32731,32732,32801,33124,33120,33143,33116,33129,33115,33122,33138,26401,33118,33142,33127,33135,33092,33121,33309,33353,33348,33344,33346,33349,34033,33855,33878,33910,33913,33935,33933,33893,33873,33856,33926,33895,33840,33869,33917,33882,33881,33908,33907,33885,34055,33886,33847,33850,33844,33914,33859,33912,33842,33861,33833,33753,33867,33839,33858,33837,33887,33904,33849,33870,33868,33874,33903,33989,33934,33851,33863,33846,33843,33896,33918,33860,33835,33888,33876,33902,33872,34571,34564,34551,34572,34554,34518,34549,34637,34552,34574,34569,34561,34550,34573,34565,35030,35019,35021,35022,35038,35035,35034,35020,35024,35205,35227,35295,35301,35300,35297,35296,35298,35292,35302,35446,35462,35455,35425,35391,35447,35458,35460,35445,35459,35457,35444,35450,35900,35915,35914,35941,35940,35942,35974,35972,35973,36044,36200,36201,36241,36236,36238,36239,36237,36243,36244,36240,36242,36336,36320,36332,36337,36334,36304,36329,36323,36322,36327,36338,36331,36340,36614,36607,36609,36608,36613,36615,36616,36610,36619,36946,36927,36932,36937,36925,37136,37133,37135,37137,37142,37140,37131,37134,37230,37231,37448,37458,37424,37434,37478,37427,37477,37470,37507,37422,37450,37446,37485,37484,37455,37472,37479,37487,37430,37473,37488,37425,37460,37475,37456,37490,37454,37459,37452,37462,37426,38303,38300,38302,38299,38546,38547,38545,38551,38606,38650,38653,38648,38645,38771,38775,38776,38770,38927,38925,38926,39084,39158,39161,39343,39346,39344,39349,39597,39595,39771,40170,40173,40167,40576,40701,20710,20692,20695,20712,20723,20699,20714,20701,20708,20691,20716,20720,20719,20707,20704,20952,21120,21121,21225,21227,21296,21420,22055,22037,22028,22034,22012,22031,22044,22017,22035,22018,22010,22045,22020,22015,22009,22665,22652,22672,22680,22662,22657,22655,22644,22667,22650,22663,22673,22670,22646,22658,22664,22651,22676,22671,22782,22891,23260,23278,23269,23253,23274,23258,23277,23275,23283,23266,23264,23259,23276,23262,23261,23257,23272,23263,23415,23520,23523,23651,23938,23936,23933,23942,23930,23937,23927,23946,23945,23944,23934,23932,23949,23929,23935,24152,24153,24147,24280,24273,24279,24270,24284,24277,24281,24274,24276,24388,24387,24431,24502,24876,24872,24897,24926,24945,24947,24914,24915,24946,24940,24960,24948,24916,24954,24923,24933,24891,24938,24929,24918,25129,25127,25131,25643,25677,25691,25693,25716,25718,25714,25715,25725,25717,25702,25766,25678,25730,25694,25692,25675,25683,25696,25680,25727,25663,25708,25707,25689,25701,25719,25971,26016,26273,26272,26271,26373,26372,26402,27057,27062,27081,27040,27086,27030,27056,27052,27068,27025,27033,27022,27047,27021,27049,27070,27055,27071,27076,27069,27044,27092,27065,27082,27034,27087,27059,27027,27050,27041,27038,27097,27031,27024,27074,27061,27045,27078,27466,27469,27467,27550,27551,27552,27587,27588,27646,28366,28405,28401,28419,28453,28408,28471,28411,28462,28425,28494,28441,28442,28455,28440,28475,28434,28397,28426,28470,28531,28409,28398,28461,28480,28464,28476,28469,28395,28423,28430,28483,28421,28413,28406,28473,28444,28412,28474,28447,28429,28446,28424,28449,29063,29072,29065,29056,29061,29058,29071,29051,29062,29057,29079,29252,29267,29335,29333,29331,29507,29517,29521,29516,29794,29811,29809,29813,29810,29799,29806,29952,29954,29955,30077,30096,30230,30216,30220,30229,30225,30218,30228,30392,30593,30588,30597,30594,30574,30592,30575,30590,30595,30898,30890,30900,30893,30888,30846,30891,30878,30885,30880,30892,30882,30884,31128,31114,31115,31126,31125,31124,31123,31127,31112,31122,31120,31275,31306,31280,31279,31272,31270,31400,31403,31404,31470,31624,31644,31626,31633,31632,31638,31629,31628,31643,31630,31621,31640,21124,31641,31652,31618,31931,31935,31932,31930,32167,32183,32194,32163,32170,32193,32192,32197,32157,32206,32196,32198,32203,32204,32175,32185,32150,32188,32159,32166,32174,32169,32161,32201,32627,32738,32739,32741,32734,32804,32861,32860,33161,33158,33155,33159,33165,33164,33163,33301,33943,33956,33953,33951,33978,33998,33986,33964,33966,33963,33977,33972,33985,33997,33962,33946,33969,34000,33949,33959,33979,33954,33940,33991,33996,33947,33961,33967,33960,34006,33944,33974,33999,33952,34007,34004,34002,34011,33968,33937,34401,34611,34595,34600,34667,34624,34606,34590,34593,34585,34587,34627,34604,34625,34622,34630,34592,34610,34602,34605,34620,34578,34618,34609,34613,34626,34598,34599,34616,34596,34586,34608,34577,35063,35047,35057,35058,35066,35070,35054,35068,35062,35067,35056,35052,35051,35229,35233,35231,35230,35305,35307,35304,35499,35481,35467,35474,35471,35478,35901,35944,35945,36053,36047,36055,36246,36361,36354,36351,36365,36349,36362,36355,36359,36358,36357,36350,36352,36356,36624,36625,36622,36621,37155,37148,37152,37154,37151,37149,37146,37156,37153,37147,37242,37234,37241,37235,37541,37540,37494,37531,37498,37536,37524,37546,37517,37542,37530,37547,37497,37527,37503,37539,37614,37518,37506,37525,37538,37501,37512,37537,37514,37510,37516,37529,37543,37502,37511,37545,37533,37515,37421,38558,38561,38655,38744,38781,38778,38782,38787,38784,38786,38779,38788,38785,38783,38862,38861,38934,39085,39086,39170,39168,39175,39325,39324,39363,39353,39355,39354,39362,39357,39367,39601,39651,39655,39742,39743,39776,39777,39775,40177,40178,40181,40615,20735,20739,20784,20728,20742,20743,20726,20734,20747,20748,20733,20746,21131,21132,21233,21231,22088,22082,22092,22069,22081,22090,22089,22086,22104,22106,22080,22067,22077,22060,22078,22072,22058,22074,22298,22699,22685,22705,22688,22691,22703,22700,22693,22689,22783,23295,23284,23293,23287,23286,23299,23288,23298,23289,23297,23303,23301,23311,23655,23961,23959,23967,23954,23970,23955,23957,23968,23964,23969,23962,23966,24169,24157,24160,24156,32243,24283,24286,24289,24393,24498,24971,24963,24953,25009,25008,24994,24969,24987,24979,25007,25005,24991,24978,25002,24993,24973,24934,25011,25133,25710,25712,25750,25760,25733,25751,25756,25743,25739,25738,25740,25763,25759,25704,25777,25752,25974,25978,25977,25979,26034,26035,26293,26288,26281,26290,26295,26282,26287,27136,27142,27159,27109,27128,27157,27121,27108,27168,27135,27116,27106,27163,27165,27134,27175,27122,27118,27156,27127,27111,27200,27144,27110,27131,27149,27132,27115,27145,27140,27160,27173,27151,27126,27174,27143,27124,27158,27473,27557,27555,27554,27558,27649,27648,27647,27650,28481,28454,28542,28551,28614,28562,28557,28553,28556,28514,28495,28549,28506,28566,28534,28524,28546,28501,28530,28498,28496,28503,28564,28563,28509,28416,28513,28523,28541,28519,28560,28499,28555,28521,28543,28565,28515,28535,28522,28539,29106,29103,29083,29104,29088,29082,29097,29109,29085,29093,29086,29092,29089,29098,29084,29095,29107,29336,29338,29528,29522,29534,29535,29536,29533,29531,29537,29530,29529,29538,29831,29833,29834,29830,29825,29821,29829,29832,29820,29817,29960,29959,30078,30245,30238,30233,30237,30236,30243,30234,30248,30235,30364,30365,30366,30363,30605,30607,30601,30600,30925,30907,30927,30924,30929,30926,30932,30920,30915,30916,30921,31130,31137,31136,31132,31138,31131,27510,31289,31410,31412,31411,31671,31691,31678,31660,31694,31663,31673,31690,31669,31941,31944,31948,31947,32247,32219,32234,32231,32215,32225,32259,32250,32230,32246,32241,32240,32238,32223,32630,32684,32688,32685,32749,32747,32746,32748,32742,32744,32868,32871,33187,33183,33182,33173,33186,33177,33175,33302,33359,33363,33362,33360,33358,33361,34084,34107,34063,34048,34089,34062,34057,34061,34079,34058,34087,34076,34043,34091,34042,34056,34060,34036,34090,34034,34069,34039,34027,34035,34044,34066,34026,34025,34070,34046,34088,34077,34094,34050,34045,34078,34038,34097,34086,34023,34024,34032,34031,34041,34072,34080,34096,34059,34073,34095,34402,34646,34659,34660,34679,34785,34675,34648,34644,34651,34642,34657,34650,34641,34654,34669,34666,34640,34638,34655,34653,34671,34668,34682,34670,34652,34661,34639,34683,34677,34658,34663,34665,34906,35077,35084,35092,35083,35095,35096,35097,35078,35094,35089,35086,35081,35234,35236,35235,35309,35312,35308,35535,35526,35512,35539,35537,35540,35541,35515,35543,35518,35520,35525,35544,35523,35514,35517,35545,35902,35917,35983,36069,36063,36057,36072,36058,36061,36071,36256,36252,36257,36251,36384,36387,36389,36388,36398,36373,36379,36374,36369,36377,36390,36391,36372,36370,36376,36371,36380,36375,36378,36652,36644,36632,36634,36640,36643,36630,36631,36979,36976,36975,36967,36971,37167,37163,37161,37162,37170,37158,37166,37253,37254,37258,37249,37250,37252,37248,37584,37571,37572,37568,37593,37558,37583,37617,37599,37592,37609,37591,37597,37580,37615,37570,37608,37578,37576,37582,37606,37581,37589,37577,37600,37598,37607,37585,37587,37557,37601,37574,37556,38268,38316,38315,38318,38320,38564,38562,38611,38661,38664,38658,38746,38794,38798,38792,38864,38863,38942,38941,38950,38953,38952,38944,38939,38951,39090,39176,39162,39185,39188,39190,39191,39189,39388,39373,39375,39379,39380,39374,39369,39382,39384,39371,39383,39372,39603,39660,39659,39667,39666,39665,39750,39747,39783,39796,39793,39782,39798,39797,39792,39784,39780,39788,40188,40186,40189,40191,40183,40199,40192,40185,40187,40200,40197,40196,40579,40659,40719,40720,20764,20755,20759,20762,20753,20958,21300,21473,22128,22112,22126,22131,22118,22115,22125,22130,22110,22135,22300,22299,22728,22717,22729,22719,22714,22722,22716,22726,23319,23321,23323,23329,23316,23315,23312,23318,23336,23322,23328,23326,23535,23980,23985,23977,23975,23989,23984,23982,23978,23976,23986,23981,23983,23988,24167,24168,24166,24175,24297,24295,24294,24296,24293,24395,24508,24989,25000,24982,25029,25012,25030,25025,25036,25018,25023,25016,24972,25815,25814,25808,25807,25801,25789,25737,25795,25819,25843,25817,25907,25983,25980,26018,26312,26302,26304,26314,26315,26319,26301,26299,26298,26316,26403,27188,27238,27209,27239,27186,27240,27198,27229,27245,27254,27227,27217,27176,27226,27195,27199,27201,27242,27236,27216,27215,27220,27247,27241,27232,27196,27230,27222,27221,27213,27214,27206,27477,27476,27478,27559,27562,27563,27592,27591,27652,27651,27654,28589,28619,28579,28615,28604,28622,28616,28510,28612,28605,28574,28618,28584,28676,28581,28590,28602,28588,28586,28623,28607,28600,28578,28617,28587,28621,28591,28594,28592,29125,29122,29119,29112,29142,29120,29121,29131,29140,29130,29127,29135,29117,29144,29116,29126,29146,29147,29341,29342,29545,29542,29543,29548,29541,29547,29546,29823,29850,29856,29844,29842,29845,29857,29963,30080,30255,30253,30257,30269,30259,30268,30261,30258,30256,30395,30438,30618,30621,30625,30620,30619,30626,30627,30613,30617,30615,30941,30953,30949,30954,30942,30947,30939,30945,30946,30957,30943,30944,31140,31300,31304,31303,31414,31416,31413,31409,31415,31710,31715,31719,31709,31701,31717,31706,31720,31737,31700,31722,31714,31708,31723,31704,31711,31954,31956,31959,31952,31953,32274,32289,32279,32268,32287,32288,32275,32270,32284,32277,32282,32290,32267,32271,32278,32269,32276,32293,32292,32579,32635,32636,32634,32689,32751,32810,32809,32876,33201,33190,33198,33209,33205,33195,33200,33196,33204,33202,33207,33191,33266,33365,33366,33367,34134,34117,34155,34125,34131,34145,34136,34112,34118,34148,34113,34146,34116,34129,34119,34147,34110,34139,34161,34126,34158,34165,34133,34151,34144,34188,34150,34141,34132,34149,34156,34403,34405,34404,34715,34703,34711,34707,34706,34696,34689,34710,34712,34681,34695,34723,34693,34704,34705,34717,34692,34708,34716,34714,34697,35102,35110,35120,35117,35118,35111,35121,35106,35113,35107,35119,35116,35103,35313,35552,35554,35570,35572,35573,35549,35604,35556,35551,35568,35528,35550,35553,35560,35583,35567,35579,35985,35986,35984,36085,36078,36081,36080,36083,36204,36206,36261,36263,36403,36414,36408,36416,36421,36406,36412,36413,36417,36400,36415,36541,36662,36654,36661,36658,36665,36663,36660,36982,36985,36987,36998,37114,37171,37173,37174,37267,37264,37265,37261,37263,37671,37662,37640,37663,37638,37647,37754,37688,37692,37659,37667,37650,37633,37702,37677,37646,37645,37579,37661,37626,37669,37651,37625,37623,37684,37634,37668,37631,37673,37689,37685,37674,37652,37644,37643,37630,37641,37632,37627,37654,38332,38349,38334,38329,38330,38326,38335,38325,38333,38569,38612,38667,38674,38672,38809,38807,38804,38896,38904,38965,38959,38962,39204,39199,39207,39209,39326,39406,39404,39397,39396,39408,39395,39402,39401,39399,39609,39615,39604,39611,39670,39674,39673,39671,39731,39808,39813,39815,39804,39806,39803,39810,39827,39826,39824,39802,39829,39805,39816,40229,40215,40224,40222,40212,40233,40221,40216,40226,40208,40217,40223,40584,40582,40583,40622,40621,40661,40662,40698,40722,40765,20774,20773,20770,20772,20768,20777,21236,22163,22156,22157,22150,22148,22147,22142,22146,22143,22145,22742,22740,22735,22738,23341,23333,23346,23331,23340,23335,23334,23343,23342,23419,23537,23538,23991,24172,24170,24510,24507,25027,25013,25020,25063,25056,25061,25060,25064,25054,25839,25833,25827,25835,25828,25832,25985,25984,26038,26074,26322,27277,27286,27265,27301,27273,27295,27291,27297,27294,27271,27283,27278,27285,27267,27304,27300,27281,27263,27302,27290,27269,27276,27282,27483,27565,27657,28620,28585,28660,28628,28643,28636,28653,28647,28646,28638,28658,28637,28642,28648,29153,29169,29160,29170,29156,29168,29154,29555,29550,29551,29847,29874,29867,29840,29866,29869,29873,29861,29871,29968,29969,29970,29967,30084,30275,30280,30281,30279,30372,30441,30645,30635,30642,30647,30646,30644,30641,30632,30704,30963,30973,30978,30971,30972,30962,30981,30969,30974,30980,31147,31144,31324,31323,31318,31320,31316,31322,31422,31424,31425,31749,31759,31730,31744,31743,31739,31758,31732,31755,31731,31746,31753,31747,31745,31736,31741,31750,31728,31729,31760,31754,31976,32301,32316,32322,32307,38984,32312,32298,32329,32320,32327,32297,32332,32304,32315,32310,32324,32314,32581,32639,32638,32637,32756,32754,32812,33211,33220,33228,33226,33221,33223,33212,33257,33371,33370,33372,34179,34176,34191,34215,34197,34208,34187,34211,34171,34212,34202,34206,34167,34172,34185,34209,34170,34168,34135,34190,34198,34182,34189,34201,34205,34177,34210,34178,34184,34181,34169,34166,34200,34192,34207,34408,34750,34730,34733,34757,34736,34732,34745,34741,34748,34734,34761,34755,34754,34764,34743,34735,34756,34762,34740,34742,34751,34744,34749,34782,34738,35125,35123,35132,35134,35137,35154,35127,35138,35245,35247,35246,35314,35315,35614,35608,35606,35601,35589,35595,35618,35599,35602,35605,35591,35597,35592,35590,35612,35603,35610,35919,35952,35954,35953,35951,35989,35988,36089,36207,36430,36429,36435,36432,36428,36423,36675,36672,36997,36990,37176,37274,37282,37275,37273,37279,37281,37277,37280,37793,37763,37807,37732,37718,37703,37756,37720,37724,37750,37705,37712,37713,37728,37741,37775,37708,37738,37753,37719,37717,37714,37711,37745,37751,37755,37729,37726,37731,37735,37760,37710,37721,38343,38336,38345,38339,38341,38327,38574,38576,38572,38688,38687,38680,38685,38681,38810,38817,38812,38814,38813,38869,38868,38897,38977,38980,38986,38985,38981,38979,39205,39211,39212,39210,39219,39218,39215,39213,39217,39216,39320,39331,39329,39426,39418,39412,39415,39417,39416,39414,39419,39421,39422,39420,39427,39614,39678,39677,39681,39676,39752,39834,39848,39838,39835,39846,39841,39845,39844,39814,39842,39840,39855,40243,40257,40295,40246,40238,40239,40241,40248,40240,40261,40258,40259,40254,40247,40256,40253,32757,40237,40586,40585,40589,40624,40648,40666,40699,40703,40740,40739,40738,40788,40864,20785,20781,20782,22168,22172,22167,22170,22173,22169,22896,23356,23657,23658,24000,24173,24174,25048,25055,25069,25070,25073,25066,25072,25067,25046,25065,25855,25860,25853,25848,25857,25859,25852,26004,26075,26330,26331,26328,27333,27321,27325,27361,27334,27322,27318,27319,27335,27316,27309,27486,27593,27659,28679,28684,28685,28673,28677,28692,28686,28671,28672,28667,28710,28668,28663,28682,29185,29183,29177,29187,29181,29558,29880,29888,29877,29889,29886,29878,29883,29890,29972,29971,30300,30308,30297,30288,30291,30295,30298,30374,30397,30444,30658,30650,30975,30988,30995,30996,30985,30992,30994,30993,31149,31148,31327,31772,31785,31769,31776,31775,31789,31773,31782,31784,31778,31781,31792,32348,32336,32342,32355,32344,32354,32351,32337,32352,32343,32339,32693,32691,32759,32760,32885,33233,33234,33232,33375,33374,34228,34246,34240,34243,34242,34227,34229,34237,34247,34244,34239,34251,34254,34248,34245,34225,34230,34258,34340,34232,34231,34238,34409,34791,34790,34786,34779,34795,34794,34789,34783,34803,34788,34772,34780,34771,34797,34776,34787,34724,34775,34777,34817,34804,34792,34781,35155,35147,35151,35148,35142,35152,35153,35145,35626,35623,35619,35635,35632,35637,35655,35631,35644,35646,35633,35621,35639,35622,35638,35630,35620,35643,35645,35642,35906,35957,35993,35992,35991,36094,36100,36098,36096,36444,36450,36448,36439,36438,36446,36453,36455,36443,36442,36449,36445,36457,36436,36678,36679,36680,36683,37160,37178,37179,37182,37288,37285,37287,37295,37290,37813,37772,37778,37815,37787,37789,37769,37799,37774,37802,37790,37798,37781,37768,37785,37791,37773,37809,37777,37810,37796,37800,37812,37795,37797,38354,38355,38353,38579,38615,38618,24002,38623,38616,38621,38691,38690,38693,38828,38830,38824,38827,38820,38826,38818,38821,38871,38873,38870,38872,38906,38992,38993,38994,39096,39233,39228,39226,39439,39435,39433,39437,39428,39441,39434,39429,39431,39430,39616,39644,39688,39684,39685,39721,39733,39754,39756,39755,39879,39878,39875,39871,39873,39861,39864,39891,39862,39876,39865,39869,40284,40275,40271,40266,40283,40267,40281,40278,40268,40279,40274,40276,40287,40280,40282,40590,40588,40671,40705,40704,40726,40741,40747,40746,40745,40744,40780,40789,20788,20789,21142,21239,21428,22187,22189,22182,22183,22186,22188,22746,22749,22747,22802,23357,23358,23359,24003,24176,24511,25083,25863,25872,25869,25865,25868,25870,25988,26078,26077,26334,27367,27360,27340,27345,27353,27339,27359,27356,27344,27371,27343,27341,27358,27488,27568,27660,28697,28711,28704,28694,28715,28705,28706,28707,28713,28695,28708,28700,28714,29196,29194,29191,29186,29189,29349,29350,29348,29347,29345,29899,29893,29879,29891,29974,30304,30665,30666,30660,30705,31005,31003,31009,31004,30999,31006,31152,31335,31336,31795,31804,31801,31788,31803,31980,31978,32374,32373,32376,32368,32375,32367,32378,32370,32372,32360,32587,32586,32643,32646,32695,32765,32766,32888,33239,33237,33380,33377,33379,34283,34289,34285,34265,34273,34280,34266,34263,34284,34290,34296,34264,34271,34275,34268,34257,34288,34278,34287,34270,34274,34816,34810,34819,34806,34807,34825,34828,34827,34822,34812,34824,34815,34826,34818,35170,35162,35163,35159,35169,35164,35160,35165,35161,35208,35255,35254,35318,35664,35656,35658,35648,35667,35670,35668,35659,35669,35665,35650,35666,35671,35907,35959,35958,35994,36102,36103,36105,36268,36266,36269,36267,36461,36472,36467,36458,36463,36475,36546,36690,36689,36687,36688,36691,36788,37184,37183,37296,37293,37854,37831,37839,37826,37850,37840,37881,37868,37836,37849,37801,37862,37834,37844,37870,37859,37845,37828,37838,37824,37842,37863,38269,38362,38363,38625,38697,38699,38700,38696,38694,38835,38839,38838,38877,38878,38879,39004,39001,39005,38999,39103,39101,39099,39102,39240,39239,39235,39334,39335,39450,39445,39461,39453,39460,39451,39458,39456,39463,39459,39454,39452,39444,39618,39691,39690,39694,39692,39735,39914,39915,39904,39902,39908,39910,39906,39920,39892,39895,39916,39900,39897,39909,39893,39905,39898,40311,40321,40330,40324,40328,40305,40320,40312,40326,40331,40332,40317,40299,40308,40309,40304,40297,40325,40307,40315,40322,40303,40313,40319,40327,40296,40596,40593,40640,40700,40749,40768,40769,40781,40790,40791,40792,21303,22194,22197,22195,22755,23365,24006,24007,24302,24303,24512,24513,25081,25879,25878,25877,25875,26079,26344,26339,26340,27379,27376,27370,27368,27385,27377,27374,27375,28732,28725,28719,28727,28724,28721,28738,28728,28735,28730,28729,28736,28731,28723,28737,29203,29204,29352,29565,29564,29882,30379,30378,30398,30445,30668,30670,30671,30669,30706,31013,31011,31015,31016,31012,31017,31154,31342,31340,31341,31479,31817,31816,31818,31815,31813,31982,32379,32382,32385,32384,32698,32767,32889,33243,33241,33291,33384,33385,34338,34303,34305,34302,34331,34304,34294,34308,34313,34309,34316,34301,34841,34832,34833,34839,34835,34838,35171,35174,35257,35319,35680,35690,35677,35688,35683,35685,35687,35693,36270,36486,36488,36484,36697,36694,36695,36693,36696,36698,37005,37187,37185,37303,37301,37298,37299,37899,37907,37883,37920,37903,37908,37886,37909,37904,37928,37913,37901,37877,37888,37879,37895,37902,37910,37906,37882,37897,37880,37898,37887,37884,37900,37878,37905,37894,38366,38368,38367,38702,38703,38841,38843,38909,38910,39008,39010,39011,39007,39105,39106,39248,39246,39257,39244,39243,39251,39474,39476,39473,39468,39466,39478,39465,39470,39480,39469,39623,39626,39622,39696,39698,39697,39947,39944,39927,39941,39954,39928,40000,39943,39950,39942,39959,39956,39945,40351,40345,40356,40349,40338,40344,40336,40347,40352,40340,40348,40362,40343,40353,40346,40354,40360,40350,40355,40383,40361,40342,40358,40359,40601,40603,40602,40677,40676,40679,40678,40752,40750,40795,40800,40798,40797,40793,40849,20794,20793,21144,21143,22211,22205,22206,23368,23367,24011,24015,24305,25085,25883,27394,27388,27395,27384,27392,28739,28740,28746,28744,28745,28741,28742,29213,29210,29209,29566,29975,30314,30672,31021,31025,31023,31828,31827,31986,32394,32391,32392,32395,32390,32397,32589,32699,32816,33245,34328,34346,34342,34335,34339,34332,34329,34343,34350,34337,34336,34345,34334,34341,34857,34845,34843,34848,34852,34844,34859,34890,35181,35177,35182,35179,35322,35705,35704,35653,35706,35707,36112,36116,36271,36494,36492,36702,36699,36701,37190,37188,37189,37305,37951,37947,37942,37929,37949,37948,37936,37945,37930,37943,37932,37952,37937,38373,38372,38371,38709,38714,38847,38881,39012,39113,39110,39104,39256,39254,39481,39485,39494,39492,39490,39489,39482,39487,39629,39701,39703,39704,39702,39738,39762,39979,39965,39964,39980,39971,39976,39977,39972,39969,40375,40374,40380,40385,40391,40394,40399,40382,40389,40387,40379,40373,40398,40377,40378,40364,40392,40369,40365,40396,40371,40397,40370,40570,40604,40683,40686,40685,40731,40728,40730,40753,40782,40805,40804,40850,20153,22214,22213,22219,22897,23371,23372,24021,24017,24306,25889,25888,25894,25890,27403,27400,27401,27661,28757,28758,28759,28754,29214,29215,29353,29567,29912,29909,29913,29911,30317,30381,31029,31156,31344,31345,31831,31836,31833,31835,31834,31988,31985,32401,32591,32647,33246,33387,34356,34357,34355,34348,34354,34358,34860,34856,34854,34858,34853,35185,35263,35262,35323,35710,35716,35714,35718,35717,35711,36117,36501,36500,36506,36498,36496,36502,36503,36704,36706,37191,37964,37968,37962,37963,37967,37959,37957,37960,37961,37958,38719,38883,39018,39017,39115,39252,39259,39502,39507,39508,39500,39503,39496,39498,39497,39506,39504,39632,39705,39723,39739,39766,39765,40006,40008,39999,40004,39993,39987,40001,39996,39991,39988,39986,39997,39990,40411,40402,40414,40410,40395,40400,40412,40401,40415,40425,40409,40408,40406,40437,40405,40413,40630,40688,40757,40755,40754,40770,40811,40853,40866,20797,21145,22760,22759,22898,23373,24024,34863,24399,25089,25091,25092,25897,25893,26006,26347,27409,27410,27407,27594,28763,28762,29218,29570,29569,29571,30320,30676,31847,31846,32405,33388,34362,34368,34361,34364,34353,34363,34366,34864,34866,34862,34867,35190,35188,35187,35326,35724,35726,35723,35720,35909,36121,36504,36708,36707,37308,37986,37973,37981,37975,37982,38852,38853,38912,39510,39513,39710,39711,39712,40018,40024,40016,40010,40013,40011,40021,40025,40012,40014,40443,40439,40431,40419,40427,40440,40420,40438,40417,40430,40422,40434,40432,40418,40428,40436,40435,40424,40429,40642,40656,40690,40691,40710,40732,40760,40759,40758,40771,40783,40817,40816,40814,40815,22227,22221,23374,23661,25901,26349,26350,27411,28767,28769,28765,28768,29219,29915,29925,30677,31032,31159,31158,31850,32407,32649,33389,34371,34872,34871,34869,34891,35732,35733,36510,36511,36512,36509,37310,37309,37314,37995,37992,37993,38629,38726,38723,38727,38855,38885,39518,39637,39769,40035,40039,40038,40034,40030,40032,40450,40446,40455,40451,40454,40453,40448,40449,40457,40447,40445,40452,40608,40734,40774,40820,40821,40822,22228,25902,26040,27416,27417,27415,27418,28770,29222,29354,30680,30681,31033,31849,31851,31990,32410,32408,32411,32409,33248,33249,34374,34375,34376,35193,35194,35196,35195,35327,35736,35737,36517,36516,36515,37998,37997,37999,38001,38003,38729,39026,39263,40040,40046,40045,40459,40461,40464,40463,40466,40465,40609,40693,40713,40775,40824,40827,40826,40825,22302,28774,31855,34876,36274,36518,37315,38004,38008,38006,38005,39520,40052,40051,40049,40053,40468,40467,40694,40714,40868,28776,28773,31991,34410,34878,34877,34879,35742,35996,36521,36553,38731,39027,39028,39116,39265,39339,39524,39526,39527,39716,40469,40471,40776,25095,27422,29223,34380,36520,38018,38016,38017,39529,39528,39726,40473,29225,34379,35743,38019,40057,40631,30325,39531,40058,40477,28777,28778,40612,40830,40777,40856,30849,37561,35023,22715,24658,31911,23290,9556,9574,9559,9568,9580,9571,9562,9577,9565,9554,9572,9557,9566,9578,9569,9560,9575,9563,9555,9573,9558,9567,9579,9570,9561,9576,9564,9553,9552,9581,9582,9584,9583,65517,132423,37595,132575,147397,34124,17077,29679,20917,13897,149826,166372,37700,137691,33518,146632,30780,26436,25311,149811,166314,131744,158643,135941,20395,140525,20488,159017,162436,144896,150193,140563,20521,131966,24484,131968,131911,28379,132127,20605,20737,13434,20750,39020,14147,33814,149924,132231,20832,144308,20842,134143,139516,131813,140592,132494,143923,137603,23426,34685,132531,146585,20914,20920,40244,20937,20943,20945,15580,20947,150182,20915,20962,21314,20973,33741,26942,145197,24443,21003,21030,21052,21173,21079,21140,21177,21189,31765,34114,21216,34317,158483,21253,166622,21833,28377,147328,133460,147436,21299,21316,134114,27851,136998,26651,29653,24650,16042,14540,136936,29149,17570,21357,21364,165547,21374,21375,136598,136723,30694,21395,166555,21408,21419,21422,29607,153458,16217,29596,21441,21445,27721,20041,22526,21465,15019,134031,21472,147435,142755,21494,134263,21523,28793,21803,26199,27995,21613,158547,134516,21853,21647,21668,18342,136973,134877,15796,134477,166332,140952,21831,19693,21551,29719,21894,21929,22021,137431,147514,17746,148533,26291,135348,22071,26317,144010,26276,26285,22093,22095,30961,22257,38791,21502,22272,22255,22253,166758,13859,135759,22342,147877,27758,28811,22338,14001,158846,22502,136214,22531,136276,148323,22566,150517,22620,22698,13665,22752,22748,135740,22779,23551,22339,172368,148088,37843,13729,22815,26790,14019,28249,136766,23076,21843,136850,34053,22985,134478,158849,159018,137180,23001,137211,137138,159142,28017,137256,136917,23033,159301,23211,23139,14054,149929,23159,14088,23190,29797,23251,159649,140628,15749,137489,14130,136888,24195,21200,23414,25992,23420,162318,16388,18525,131588,23509,24928,137780,154060,132517,23539,23453,19728,23557,138052,23571,29646,23572,138405,158504,23625,18653,23685,23785,23791,23947,138745,138807,23824,23832,23878,138916,23738,24023,33532,14381,149761,139337,139635,33415,14390,15298,24110,27274,24181,24186,148668,134355,21414,20151,24272,21416,137073,24073,24308,164994,24313,24315,14496,24316,26686,37915,24333,131521,194708,15070,18606,135994,24378,157832,140240,24408,140401,24419,38845,159342,24434,37696,166454,24487,23990,15711,152144,139114,159992,140904,37334,131742,166441,24625,26245,137335,14691,15815,13881,22416,141236,31089,15936,24734,24740,24755,149890,149903,162387,29860,20705,23200,24932,33828,24898,194726,159442,24961,20980,132694,24967,23466,147383,141407,25043,166813,170333,25040,14642,141696,141505,24611,24924,25886,25483,131352,25285,137072,25301,142861,25452,149983,14871,25656,25592,136078,137212,25744,28554,142902,38932,147596,153373,25825,25829,38011,14950,25658,14935,25933,28438,150056,150051,25989,25965,25951,143486,26037,149824,19255,26065,16600,137257,26080,26083,24543,144384,26136,143863,143864,26180,143780,143781,26187,134773,26215,152038,26227,26228,138813,143921,165364,143816,152339,30661,141559,39332,26370,148380,150049,15147,27130,145346,26462,26471,26466,147917,168173,26583,17641,26658,28240,37436,26625,144358,159136,26717,144495,27105,27147,166623,26995,26819,144845,26881,26880,15666,14849,144956,15232,26540,26977,166474,17148,26934,27032,15265,132041,33635,20624,27129,144985,139562,27205,145155,27293,15347,26545,27336,168348,15373,27421,133411,24798,27445,27508,141261,28341,146139,132021,137560,14144,21537,146266,27617,147196,27612,27703,140427,149745,158545,27738,33318,27769,146876,17605,146877,147876,149772,149760,146633,14053,15595,134450,39811,143865,140433,32655,26679,159013,159137,159211,28054,27996,28284,28420,149887,147589,159346,34099,159604,20935,27804,28189,33838,166689,28207,146991,29779,147330,31180,28239,23185,143435,28664,14093,28573,146992,28410,136343,147517,17749,37872,28484,28508,15694,28532,168304,15675,28575,147780,28627,147601,147797,147513,147440,147380,147775,20959,147798,147799,147776,156125,28747,28798,28839,28801,28876,28885,28886,28895,16644,15848,29108,29078,148087,28971,28997,23176,29002,29038,23708,148325,29007,37730,148161,28972,148570,150055,150050,29114,166888,28861,29198,37954,29205,22801,37955,29220,37697,153093,29230,29248,149876,26813,29269,29271,15957,143428,26637,28477,29314,29482,29483,149539,165931,18669,165892,29480,29486,29647,29610,134202,158254,29641,29769,147938,136935,150052,26147,14021,149943,149901,150011,29687,29717,26883,150054,29753,132547,16087,29788,141485,29792,167602,29767,29668,29814,33721,29804,14128,29812,37873,27180,29826,18771,150156,147807,150137,166799,23366,166915,137374,29896,137608,29966,29929,29982,167641,137803,23511,167596,37765,30029,30026,30055,30062,151426,16132,150803,30094,29789,30110,30132,30210,30252,30289,30287,30319,30326,156661,30352,33263,14328,157969,157966,30369,30373,30391,30412,159647,33890,151709,151933,138780,30494,30502,30528,25775,152096,30552,144044,30639,166244,166248,136897,30708,30729,136054,150034,26826,30895,30919,30931,38565,31022,153056,30935,31028,30897,161292,36792,34948,166699,155779,140828,31110,35072,26882,31104,153687,31133,162617,31036,31145,28202,160038,16040,31174,168205,31188],
+ "euc-kr":[44034,44035,44037,44038,44043,44044,44045,44046,44047,44056,44062,44063,44065,44066,44067,44069,44070,44071,44072,44073,44074,44075,44078,44082,44083,44084,null,null,null,null,null,null,44085,44086,44087,44090,44091,44093,44094,44095,44097,44098,44099,44100,44101,44102,44103,44104,44105,44106,44108,44110,44111,44112,44113,44114,44115,44117,null,null,null,null,null,null,44118,44119,44121,44122,44123,44125,44126,44127,44128,44129,44130,44131,44132,44133,44134,44135,44136,44137,44138,44139,44140,44141,44142,44143,44146,44147,44149,44150,44153,44155,44156,44157,44158,44159,44162,44167,44168,44173,44174,44175,44177,44178,44179,44181,44182,44183,44184,44185,44186,44187,44190,44194,44195,44196,44197,44198,44199,44203,44205,44206,44209,44210,44211,44212,44213,44214,44215,44218,44222,44223,44224,44226,44227,44229,44230,44231,44233,44234,44235,44237,44238,44239,44240,44241,44242,44243,44244,44246,44248,44249,44250,44251,44252,44253,44254,44255,44258,44259,44261,44262,44265,44267,44269,44270,44274,44276,44279,44280,44281,44282,44283,44286,44287,44289,44290,44291,44293,44295,44296,44297,44298,44299,44302,44304,44306,44307,44308,44309,44310,44311,44313,44314,44315,44317,44318,44319,44321,44322,44323,44324,44325,44326,44327,44328,44330,44331,44334,44335,44336,44337,44338,44339,null,null,null,null,null,null,44342,44343,44345,44346,44347,44349,44350,44351,44352,44353,44354,44355,44358,44360,44362,44363,44364,44365,44366,44367,44369,44370,44371,44373,44374,44375,null,null,null,null,null,null,44377,44378,44379,44380,44381,44382,44383,44384,44386,44388,44389,44390,44391,44392,44393,44394,44395,44398,44399,44401,44402,44407,44408,44409,44410,44414,44416,44419,44420,44421,44422,44423,44426,44427,44429,44430,44431,44433,44434,44435,44436,44437,44438,44439,44440,44441,44442,44443,44446,44447,44448,44449,44450,44451,44453,44454,44455,44456,44457,44458,44459,44460,44461,44462,44463,44464,44465,44466,44467,44468,44469,44470,44472,44473,44474,44475,44476,44477,44478,44479,44482,44483,44485,44486,44487,44489,44490,44491,44492,44493,44494,44495,44498,44500,44501,44502,44503,44504,44505,44506,44507,44509,44510,44511,44513,44514,44515,44517,44518,44519,44520,44521,44522,44523,44524,44525,44526,44527,44528,44529,44530,44531,44532,44533,44534,44535,44538,44539,44541,44542,44546,44547,44548,44549,44550,44551,44554,44556,44558,44559,44560,44561,44562,44563,44565,44566,44567,44568,44569,44570,44571,44572,null,null,null,null,null,null,44573,44574,44575,44576,44577,44578,44579,44580,44581,44582,44583,44584,44585,44586,44587,44588,44589,44590,44591,44594,44595,44597,44598,44601,44603,44604,null,null,null,null,null,null,44605,44606,44607,44610,44612,44615,44616,44617,44619,44623,44625,44626,44627,44629,44631,44632,44633,44634,44635,44638,44642,44643,44644,44646,44647,44650,44651,44653,44654,44655,44657,44658,44659,44660,44661,44662,44663,44666,44670,44671,44672,44673,44674,44675,44678,44679,44680,44681,44682,44683,44685,44686,44687,44688,44689,44690,44691,44692,44693,44694,44695,44696,44697,44698,44699,44700,44701,44702,44703,44704,44705,44706,44707,44708,44709,44710,44711,44712,44713,44714,44715,44716,44717,44718,44719,44720,44721,44722,44723,44724,44725,44726,44727,44728,44729,44730,44731,44735,44737,44738,44739,44741,44742,44743,44744,44745,44746,44747,44750,44754,44755,44756,44757,44758,44759,44762,44763,44765,44766,44767,44768,44769,44770,44771,44772,44773,44774,44775,44777,44778,44780,44782,44783,44784,44785,44786,44787,44789,44790,44791,44793,44794,44795,44797,44798,44799,44800,44801,44802,44803,44804,44805,null,null,null,null,null,null,44806,44809,44810,44811,44812,44814,44815,44817,44818,44819,44820,44821,44822,44823,44824,44825,44826,44827,44828,44829,44830,44831,44832,44833,44834,44835,null,null,null,null,null,null,44836,44837,44838,44839,44840,44841,44842,44843,44846,44847,44849,44851,44853,44854,44855,44856,44857,44858,44859,44862,44864,44868,44869,44870,44871,44874,44875,44876,44877,44878,44879,44881,44882,44883,44884,44885,44886,44887,44888,44889,44890,44891,44894,44895,44896,44897,44898,44899,44902,44903,44904,44905,44906,44907,44908,44909,44910,44911,44912,44913,44914,44915,44916,44917,44918,44919,44920,44922,44923,44924,44925,44926,44927,44929,44930,44931,44933,44934,44935,44937,44938,44939,44940,44941,44942,44943,44946,44947,44948,44950,44951,44952,44953,44954,44955,44957,44958,44959,44960,44961,44962,44963,44964,44965,44966,44967,44968,44969,44970,44971,44972,44973,44974,44975,44976,44977,44978,44979,44980,44981,44982,44983,44986,44987,44989,44990,44991,44993,44994,44995,44996,44997,44998,45002,45004,45007,45008,45009,45010,45011,45013,45014,45015,45016,45017,45018,45019,45021,45022,45023,45024,45025,null,null,null,null,null,null,45026,45027,45028,45029,45030,45031,45034,45035,45036,45037,45038,45039,45042,45043,45045,45046,45047,45049,45050,45051,45052,45053,45054,45055,45058,45059,null,null,null,null,null,null,45061,45062,45063,45064,45065,45066,45067,45069,45070,45071,45073,45074,45075,45077,45078,45079,45080,45081,45082,45083,45086,45087,45088,45089,45090,45091,45092,45093,45094,45095,45097,45098,45099,45100,45101,45102,45103,45104,45105,45106,45107,45108,45109,45110,45111,45112,45113,45114,45115,45116,45117,45118,45119,45120,45121,45122,45123,45126,45127,45129,45131,45133,45135,45136,45137,45138,45142,45144,45146,45147,45148,45150,45151,45152,45153,45154,45155,45156,45157,45158,45159,45160,45161,45162,45163,45164,45165,45166,45167,45168,45169,45170,45171,45172,45173,45174,45175,45176,45177,45178,45179,45182,45183,45185,45186,45187,45189,45190,45191,45192,45193,45194,45195,45198,45200,45202,45203,45204,45205,45206,45207,45211,45213,45214,45219,45220,45221,45222,45223,45226,45232,45234,45238,45239,45241,45242,45243,45245,45246,45247,45248,45249,45250,45251,45254,45258,45259,45260,45261,45262,45263,45266,null,null,null,null,null,null,45267,45269,45270,45271,45273,45274,45275,45276,45277,45278,45279,45281,45282,45283,45284,45286,45287,45288,45289,45290,45291,45292,45293,45294,45295,45296,null,null,null,null,null,null,45297,45298,45299,45300,45301,45302,45303,45304,45305,45306,45307,45308,45309,45310,45311,45312,45313,45314,45315,45316,45317,45318,45319,45322,45325,45326,45327,45329,45332,45333,45334,45335,45338,45342,45343,45344,45345,45346,45350,45351,45353,45354,45355,45357,45358,45359,45360,45361,45362,45363,45366,45370,45371,45372,45373,45374,45375,45378,45379,45381,45382,45383,45385,45386,45387,45388,45389,45390,45391,45394,45395,45398,45399,45401,45402,45403,45405,45406,45407,45409,45410,45411,45412,45413,45414,45415,45416,45417,45418,45419,45420,45421,45422,45423,45424,45425,45426,45427,45428,45429,45430,45431,45434,45435,45437,45438,45439,45441,45443,45444,45445,45446,45447,45450,45452,45454,45455,45456,45457,45461,45462,45463,45465,45466,45467,45469,45470,45471,45472,45473,45474,45475,45476,45477,45478,45479,45481,45482,45483,45484,45485,45486,45487,45488,45489,45490,45491,45492,45493,45494,45495,45496,null,null,null,null,null,null,45497,45498,45499,45500,45501,45502,45503,45504,45505,45506,45507,45508,45509,45510,45511,45512,45513,45514,45515,45517,45518,45519,45521,45522,45523,45525,null,null,null,null,null,null,45526,45527,45528,45529,45530,45531,45534,45536,45537,45538,45539,45540,45541,45542,45543,45546,45547,45549,45550,45551,45553,45554,45555,45556,45557,45558,45559,45560,45562,45564,45566,45567,45568,45569,45570,45571,45574,45575,45577,45578,45581,45582,45583,45584,45585,45586,45587,45590,45592,45594,45595,45596,45597,45598,45599,45601,45602,45603,45604,45605,45606,45607,45608,45609,45610,45611,45612,45613,45614,45615,45616,45617,45618,45619,45621,45622,45623,45624,45625,45626,45627,45629,45630,45631,45632,45633,45634,45635,45636,45637,45638,45639,45640,45641,45642,45643,45644,45645,45646,45647,45648,45649,45650,45651,45652,45653,45654,45655,45657,45658,45659,45661,45662,45663,45665,45666,45667,45668,45669,45670,45671,45674,45675,45676,45677,45678,45679,45680,45681,45682,45683,45686,45687,45688,45689,45690,45691,45693,45694,45695,45696,45697,45698,45699,45702,45703,45704,45706,45707,45708,45709,45710,null,null,null,null,null,null,45711,45714,45715,45717,45718,45719,45723,45724,45725,45726,45727,45730,45732,45735,45736,45737,45739,45741,45742,45743,45745,45746,45747,45749,45750,45751,null,null,null,null,null,null,45752,45753,45754,45755,45756,45757,45758,45759,45760,45761,45762,45763,45764,45765,45766,45767,45770,45771,45773,45774,45775,45777,45779,45780,45781,45782,45783,45786,45788,45790,45791,45792,45793,45795,45799,45801,45802,45808,45809,45810,45814,45820,45821,45822,45826,45827,45829,45830,45831,45833,45834,45835,45836,45837,45838,45839,45842,45846,45847,45848,45849,45850,45851,45853,45854,45855,45856,45857,45858,45859,45860,45861,45862,45863,45864,45865,45866,45867,45868,45869,45870,45871,45872,45873,45874,45875,45876,45877,45878,45879,45880,45881,45882,45883,45884,45885,45886,45887,45888,45889,45890,45891,45892,45893,45894,45895,45896,45897,45898,45899,45900,45901,45902,45903,45904,45905,45906,45907,45911,45913,45914,45917,45920,45921,45922,45923,45926,45928,45930,45932,45933,45935,45938,45939,45941,45942,45943,45945,45946,45947,45948,45949,45950,45951,45954,45958,45959,45960,45961,45962,45963,45965,null,null,null,null,null,null,45966,45967,45969,45970,45971,45973,45974,45975,45976,45977,45978,45979,45980,45981,45982,45983,45986,45987,45988,45989,45990,45991,45993,45994,45995,45997,null,null,null,null,null,null,45998,45999,46000,46001,46002,46003,46004,46005,46006,46007,46008,46009,46010,46011,46012,46013,46014,46015,46016,46017,46018,46019,46022,46023,46025,46026,46029,46031,46033,46034,46035,46038,46040,46042,46044,46046,46047,46049,46050,46051,46053,46054,46055,46057,46058,46059,46060,46061,46062,46063,46064,46065,46066,46067,46068,46069,46070,46071,46072,46073,46074,46075,46077,46078,46079,46080,46081,46082,46083,46084,46085,46086,46087,46088,46089,46090,46091,46092,46093,46094,46095,46097,46098,46099,46100,46101,46102,46103,46105,46106,46107,46109,46110,46111,46113,46114,46115,46116,46117,46118,46119,46122,46124,46125,46126,46127,46128,46129,46130,46131,46133,46134,46135,46136,46137,46138,46139,46140,46141,46142,46143,46144,46145,46146,46147,46148,46149,46150,46151,46152,46153,46154,46155,46156,46157,46158,46159,46162,46163,46165,46166,46167,46169,46170,46171,46172,46173,46174,46175,46178,46180,46182,null,null,null,null,null,null,46183,46184,46185,46186,46187,46189,46190,46191,46192,46193,46194,46195,46196,46197,46198,46199,46200,46201,46202,46203,46204,46205,46206,46207,46209,46210,null,null,null,null,null,null,46211,46212,46213,46214,46215,46217,46218,46219,46220,46221,46222,46223,46224,46225,46226,46227,46228,46229,46230,46231,46232,46233,46234,46235,46236,46238,46239,46240,46241,46242,46243,46245,46246,46247,46249,46250,46251,46253,46254,46255,46256,46257,46258,46259,46260,46262,46264,46266,46267,46268,46269,46270,46271,46273,46274,46275,46277,46278,46279,46281,46282,46283,46284,46285,46286,46287,46289,46290,46291,46292,46294,46295,46296,46297,46298,46299,46302,46303,46305,46306,46309,46311,46312,46313,46314,46315,46318,46320,46322,46323,46324,46325,46326,46327,46329,46330,46331,46332,46333,46334,46335,46336,46337,46338,46339,46340,46341,46342,46343,46344,46345,46346,46347,46348,46349,46350,46351,46352,46353,46354,46355,46358,46359,46361,46362,46365,46366,46367,46368,46369,46370,46371,46374,46379,46380,46381,46382,46383,46386,46387,46389,46390,46391,46393,46394,46395,46396,46397,46398,46399,46402,46406,null,null,null,null,null,null,46407,46408,46409,46410,46414,46415,46417,46418,46419,46421,46422,46423,46424,46425,46426,46427,46430,46434,46435,46436,46437,46438,46439,46440,46441,46442,null,null,null,null,null,null,46443,46444,46445,46446,46447,46448,46449,46450,46451,46452,46453,46454,46455,46456,46457,46458,46459,46460,46461,46462,46463,46464,46465,46466,46467,46468,46469,46470,46471,46472,46473,46474,46475,46476,46477,46478,46479,46480,46481,46482,46483,46484,46485,46486,46487,46488,46489,46490,46491,46492,46493,46494,46495,46498,46499,46501,46502,46503,46505,46508,46509,46510,46511,46514,46518,46519,46520,46521,46522,46526,46527,46529,46530,46531,46533,46534,46535,46536,46537,46538,46539,46542,46546,46547,46548,46549,46550,46551,46553,46554,46555,46556,46557,46558,46559,46560,46561,46562,46563,46564,46565,46566,46567,46568,46569,46570,46571,46573,46574,46575,46576,46577,46578,46579,46580,46581,46582,46583,46584,46585,46586,46587,46588,46589,46590,46591,46592,46593,46594,46595,46596,46597,46598,46599,46600,46601,46602,46603,46604,46605,46606,46607,46610,46611,46613,46614,46615,46617,46618,46619,46620,46621,null,null,null,null,null,null,46622,46623,46624,46625,46626,46627,46628,46630,46631,46632,46633,46634,46635,46637,46638,46639,46640,46641,46642,46643,46645,46646,46647,46648,46649,46650,null,null,null,null,null,null,46651,46652,46653,46654,46655,46656,46657,46658,46659,46660,46661,46662,46663,46665,46666,46667,46668,46669,46670,46671,46672,46673,46674,46675,46676,46677,46678,46679,46680,46681,46682,46683,46684,46685,46686,46687,46688,46689,46690,46691,46693,46694,46695,46697,46698,46699,46700,46701,46702,46703,46704,46705,46706,46707,46708,46709,46710,46711,46712,46713,46714,46715,46716,46717,46718,46719,46720,46721,46722,46723,46724,46725,46726,46727,46728,46729,46730,46731,46732,46733,46734,46735,46736,46737,46738,46739,46740,46741,46742,46743,46744,46745,46746,46747,46750,46751,46753,46754,46755,46757,46758,46759,46760,46761,46762,46765,46766,46767,46768,46770,46771,46772,46773,46774,46775,46776,46777,46778,46779,46780,46781,46782,46783,46784,46785,46786,46787,46788,46789,46790,46791,46792,46793,46794,46795,46796,46797,46798,46799,46800,46801,46802,46803,46805,46806,46807,46808,46809,46810,46811,46812,46813,null,null,null,null,null,null,46814,46815,46816,46817,46818,46819,46820,46821,46822,46823,46824,46825,46826,46827,46828,46829,46830,46831,46833,46834,46835,46837,46838,46839,46841,46842,null,null,null,null,null,null,46843,46844,46845,46846,46847,46850,46851,46852,46854,46855,46856,46857,46858,46859,46860,46861,46862,46863,46864,46865,46866,46867,46868,46869,46870,46871,46872,46873,46874,46875,46876,46877,46878,46879,46880,46881,46882,46883,46884,46885,46886,46887,46890,46891,46893,46894,46897,46898,46899,46900,46901,46902,46903,46906,46908,46909,46910,46911,46912,46913,46914,46915,46917,46918,46919,46921,46922,46923,46925,46926,46927,46928,46929,46930,46931,46934,46935,46936,46937,46938,46939,46940,46941,46942,46943,46945,46946,46947,46949,46950,46951,46953,46954,46955,46956,46957,46958,46959,46962,46964,46966,46967,46968,46969,46970,46971,46974,46975,46977,46978,46979,46981,46982,46983,46984,46985,46986,46987,46990,46995,46996,46997,47002,47003,47005,47006,47007,47009,47010,47011,47012,47013,47014,47015,47018,47022,47023,47024,47025,47026,47027,47030,47031,47033,47034,47035,47036,47037,47038,47039,47040,47041,null,null,null,null,null,null,47042,47043,47044,47045,47046,47048,47050,47051,47052,47053,47054,47055,47056,47057,47058,47059,47060,47061,47062,47063,47064,47065,47066,47067,47068,47069,null,null,null,null,null,null,47070,47071,47072,47073,47074,47075,47076,47077,47078,47079,47080,47081,47082,47083,47086,47087,47089,47090,47091,47093,47094,47095,47096,47097,47098,47099,47102,47106,47107,47108,47109,47110,47114,47115,47117,47118,47119,47121,47122,47123,47124,47125,47126,47127,47130,47132,47134,47135,47136,47137,47138,47139,47142,47143,47145,47146,47147,47149,47150,47151,47152,47153,47154,47155,47158,47162,47163,47164,47165,47166,47167,47169,47170,47171,47173,47174,47175,47176,47177,47178,47179,47180,47181,47182,47183,47184,47186,47188,47189,47190,47191,47192,47193,47194,47195,47198,47199,47201,47202,47203,47205,47206,47207,47208,47209,47210,47211,47214,47216,47218,47219,47220,47221,47222,47223,47225,47226,47227,47229,47230,47231,47232,47233,47234,47235,47236,47237,47238,47239,47240,47241,47242,47243,47244,47246,47247,47248,47249,47250,47251,47252,47253,47254,47255,47256,47257,47258,47259,47260,47261,47262,47263,null,null,null,null,null,null,47264,47265,47266,47267,47268,47269,47270,47271,47273,47274,47275,47276,47277,47278,47279,47281,47282,47283,47285,47286,47287,47289,47290,47291,47292,47293,null,null,null,null,null,null,47294,47295,47298,47300,47302,47303,47304,47305,47306,47307,47309,47310,47311,47313,47314,47315,47317,47318,47319,47320,47321,47322,47323,47324,47326,47328,47330,47331,47332,47333,47334,47335,47338,47339,47341,47342,47343,47345,47346,47347,47348,47349,47350,47351,47354,47356,47358,47359,47360,47361,47362,47363,47365,47366,47367,47368,47369,47370,47371,47372,47373,47374,47375,47376,47377,47378,47379,47380,47381,47382,47383,47385,47386,47387,47388,47389,47390,47391,47393,47394,47395,47396,47397,47398,47399,47400,47401,47402,47403,47404,47405,47406,47407,47408,47409,47410,47411,47412,47413,47414,47415,47416,47417,47418,47419,47422,47423,47425,47426,47427,47429,47430,47431,47432,47433,47434,47435,47437,47438,47440,47442,47443,47444,47445,47446,47447,47450,47451,47453,47454,47455,47457,47458,47459,47460,47461,47462,47463,47466,47468,47470,47471,47472,47473,47474,47475,47478,47479,47481,47482,47483,47485,null,null,null,null,null,null,47486,47487,47488,47489,47490,47491,47494,47496,47499,47500,47503,47504,47505,47506,47507,47508,47509,47510,47511,47512,47513,47514,47515,47516,47517,47518,null,null,null,null,null,null,47519,47520,47521,47522,47523,47524,47525,47526,47527,47528,47529,47530,47531,47534,47535,47537,47538,47539,47541,47542,47543,47544,47545,47546,47547,47550,47552,47554,47555,47556,47557,47558,47559,47562,47563,47565,47571,47572,47573,47574,47575,47578,47580,47583,47584,47586,47590,47591,47593,47594,47595,47597,47598,47599,47600,47601,47602,47603,47606,47611,47612,47613,47614,47615,47618,47619,47620,47621,47622,47623,47625,47626,47627,47628,47629,47630,47631,47632,47633,47634,47635,47636,47638,47639,47640,47641,47642,47643,47644,47645,47646,47647,47648,47649,47650,47651,47652,47653,47654,47655,47656,47657,47658,47659,47660,47661,47662,47663,47664,47665,47666,47667,47668,47669,47670,47671,47674,47675,47677,47678,47679,47681,47683,47684,47685,47686,47687,47690,47692,47695,47696,47697,47698,47702,47703,47705,47706,47707,47709,47710,47711,47712,47713,47714,47715,47718,47722,47723,47724,47725,47726,47727,null,null,null,null,null,null,47730,47731,47733,47734,47735,47737,47738,47739,47740,47741,47742,47743,47744,47745,47746,47750,47752,47753,47754,47755,47757,47758,47759,47760,47761,47762,null,null,null,null,null,null,47763,47764,47765,47766,47767,47768,47769,47770,47771,47772,47773,47774,47775,47776,47777,47778,47779,47780,47781,47782,47783,47786,47789,47790,47791,47793,47795,47796,47797,47798,47799,47802,47804,47806,47807,47808,47809,47810,47811,47813,47814,47815,47817,47818,47819,47820,47821,47822,47823,47824,47825,47826,47827,47828,47829,47830,47831,47834,47835,47836,47837,47838,47839,47840,47841,47842,47843,47844,47845,47846,47847,47848,47849,47850,47851,47852,47853,47854,47855,47856,47857,47858,47859,47860,47861,47862,47863,47864,47865,47866,47867,47869,47870,47871,47873,47874,47875,47877,47878,47879,47880,47881,47882,47883,47884,47886,47888,47890,47891,47892,47893,47894,47895,47897,47898,47899,47901,47902,47903,47905,47906,47907,47908,47909,47910,47911,47912,47914,47916,47917,47918,47919,47920,47921,47922,47923,47927,47929,47930,47935,47936,47937,47938,47939,47942,47944,47946,47947,47948,47950,47953,47954,null,null,null,null,null,null,47955,47957,47958,47959,47961,47962,47963,47964,47965,47966,47967,47968,47970,47972,47973,47974,47975,47976,47977,47978,47979,47981,47982,47983,47984,47985,null,null,null,null,null,null,47986,47987,47988,47989,47990,47991,47992,47993,47994,47995,47996,47997,47998,47999,48000,48001,48002,48003,48004,48005,48006,48007,48009,48010,48011,48013,48014,48015,48017,48018,48019,48020,48021,48022,48023,48024,48025,48026,48027,48028,48029,48030,48031,48032,48033,48034,48035,48037,48038,48039,48041,48042,48043,48045,48046,48047,48048,48049,48050,48051,48053,48054,48056,48057,48058,48059,48060,48061,48062,48063,48065,48066,48067,48069,48070,48071,48073,48074,48075,48076,48077,48078,48079,48081,48082,48084,48085,48086,48087,48088,48089,48090,48091,48092,48093,48094,48095,48096,48097,48098,48099,48100,48101,48102,48103,48104,48105,48106,48107,48108,48109,48110,48111,48112,48113,48114,48115,48116,48117,48118,48119,48122,48123,48125,48126,48129,48131,48132,48133,48134,48135,48138,48142,48144,48146,48147,48153,48154,48160,48161,48162,48163,48166,48168,48170,48171,48172,48174,48175,48178,48179,48181,null,null,null,null,null,null,48182,48183,48185,48186,48187,48188,48189,48190,48191,48194,48198,48199,48200,48202,48203,48206,48207,48209,48210,48211,48212,48213,48214,48215,48216,48217,null,null,null,null,null,null,48218,48219,48220,48222,48223,48224,48225,48226,48227,48228,48229,48230,48231,48232,48233,48234,48235,48236,48237,48238,48239,48240,48241,48242,48243,48244,48245,48246,48247,48248,48249,48250,48251,48252,48253,48254,48255,48256,48257,48258,48259,48262,48263,48265,48266,48269,48271,48272,48273,48274,48275,48278,48280,48283,48284,48285,48286,48287,48290,48291,48293,48294,48297,48298,48299,48300,48301,48302,48303,48306,48310,48311,48312,48313,48314,48315,48318,48319,48321,48322,48323,48325,48326,48327,48328,48329,48330,48331,48332,48334,48338,48339,48340,48342,48343,48345,48346,48347,48349,48350,48351,48352,48353,48354,48355,48356,48357,48358,48359,48360,48361,48362,48363,48364,48365,48366,48367,48368,48369,48370,48371,48375,48377,48378,48379,48381,48382,48383,48384,48385,48386,48387,48390,48392,48394,48395,48396,48397,48398,48399,48401,48402,48403,48405,48406,48407,48408,48409,48410,48411,48412,48413,null,null,null,null,null,null,48414,48415,48416,48417,48418,48419,48421,48422,48423,48424,48425,48426,48427,48429,48430,48431,48432,48433,48434,48435,48436,48437,48438,48439,48440,48441,null,null,null,null,null,null,48442,48443,48444,48445,48446,48447,48449,48450,48451,48452,48453,48454,48455,48458,48459,48461,48462,48463,48465,48466,48467,48468,48469,48470,48471,48474,48475,48476,48477,48478,48479,48480,48481,48482,48483,48485,48486,48487,48489,48490,48491,48492,48493,48494,48495,48496,48497,48498,48499,48500,48501,48502,48503,48504,48505,48506,48507,48508,48509,48510,48511,48514,48515,48517,48518,48523,48524,48525,48526,48527,48530,48532,48534,48535,48536,48539,48541,48542,48543,48544,48545,48546,48547,48549,48550,48551,48552,48553,48554,48555,48556,48557,48558,48559,48561,48562,48563,48564,48565,48566,48567,48569,48570,48571,48572,48573,48574,48575,48576,48577,48578,48579,48580,48581,48582,48583,48584,48585,48586,48587,48588,48589,48590,48591,48592,48593,48594,48595,48598,48599,48601,48602,48603,48605,48606,48607,48608,48609,48610,48611,48612,48613,48614,48615,48616,48618,48619,48620,48621,48622,48623,48625,null,null,null,null,null,null,48626,48627,48629,48630,48631,48633,48634,48635,48636,48637,48638,48639,48641,48642,48644,48646,48647,48648,48649,48650,48651,48654,48655,48657,48658,48659,null,null,null,null,null,null,48661,48662,48663,48664,48665,48666,48667,48670,48672,48673,48674,48675,48676,48677,48678,48679,48680,48681,48682,48683,48684,48685,48686,48687,48688,48689,48690,48691,48692,48693,48694,48695,48696,48697,48698,48699,48700,48701,48702,48703,48704,48705,48706,48707,48710,48711,48713,48714,48715,48717,48719,48720,48721,48722,48723,48726,48728,48732,48733,48734,48735,48738,48739,48741,48742,48743,48745,48747,48748,48749,48750,48751,48754,48758,48759,48760,48761,48762,48766,48767,48769,48770,48771,48773,48774,48775,48776,48777,48778,48779,48782,48786,48787,48788,48789,48790,48791,48794,48795,48796,48797,48798,48799,48800,48801,48802,48803,48804,48805,48806,48807,48809,48810,48811,48812,48813,48814,48815,48816,48817,48818,48819,48820,48821,48822,48823,48824,48825,48826,48827,48828,48829,48830,48831,48832,48833,48834,48835,48836,48837,48838,48839,48840,48841,48842,48843,48844,48845,48846,48847,48850,48851,null,null,null,null,null,null,48853,48854,48857,48858,48859,48860,48861,48862,48863,48865,48866,48870,48871,48872,48873,48874,48875,48877,48878,48879,48880,48881,48882,48883,48884,48885,null,null,null,null,null,null,48886,48887,48888,48889,48890,48891,48892,48893,48894,48895,48896,48898,48899,48900,48901,48902,48903,48906,48907,48908,48909,48910,48911,48912,48913,48914,48915,48916,48917,48918,48919,48922,48926,48927,48928,48929,48930,48931,48932,48933,48934,48935,48936,48937,48938,48939,48940,48941,48942,48943,48944,48945,48946,48947,48948,48949,48950,48951,48952,48953,48954,48955,48956,48957,48958,48959,48962,48963,48965,48966,48967,48969,48970,48971,48972,48973,48974,48975,48978,48979,48980,48982,48983,48984,48985,48986,48987,48988,48989,48990,48991,48992,48993,48994,48995,48996,48997,48998,48999,49000,49001,49002,49003,49004,49005,49006,49007,49008,49009,49010,49011,49012,49013,49014,49015,49016,49017,49018,49019,49020,49021,49022,49023,49024,49025,49026,49027,49028,49029,49030,49031,49032,49033,49034,49035,49036,49037,49038,49039,49040,49041,49042,49043,49045,49046,49047,49048,49049,49050,49051,49052,49053,null,null,null,null,null,null,49054,49055,49056,49057,49058,49059,49060,49061,49062,49063,49064,49065,49066,49067,49068,49069,49070,49071,49073,49074,49075,49076,49077,49078,49079,49080,null,null,null,null,null,null,49081,49082,49083,49084,49085,49086,49087,49088,49089,49090,49091,49092,49094,49095,49096,49097,49098,49099,49102,49103,49105,49106,49107,49109,49110,49111,49112,49113,49114,49115,49117,49118,49120,49122,49123,49124,49125,49126,49127,49128,49129,49130,49131,49132,49133,49134,49135,49136,49137,49138,49139,49140,49141,49142,49143,49144,49145,49146,49147,49148,49149,49150,49151,49152,49153,49154,49155,49156,49157,49158,49159,49160,49161,49162,49163,49164,49165,49166,49167,49168,49169,49170,49171,49172,49173,49174,49175,49176,49177,49178,49179,49180,49181,49182,49183,49184,49185,49186,49187,49188,49189,49190,49191,49192,49193,49194,49195,49196,49197,49198,49199,49200,49201,49202,49203,49204,49205,49206,49207,49208,49209,49210,49211,49213,49214,49215,49216,49217,49218,49219,49220,49221,49222,49223,49224,49225,49226,49227,49228,49229,49230,49231,49232,49234,49235,49236,49237,49238,49239,49241,49242,49243,null,null,null,null,null,null,49245,49246,49247,49249,49250,49251,49252,49253,49254,49255,49258,49259,49260,49261,49262,49263,49264,49265,49266,49267,49268,49269,49270,49271,49272,49273,null,null,null,null,null,null,49274,49275,49276,49277,49278,49279,49280,49281,49282,49283,49284,49285,49286,49287,49288,49289,49290,49291,49292,49293,49294,49295,49298,49299,49301,49302,49303,49305,49306,49307,49308,49309,49310,49311,49314,49316,49318,49319,49320,49321,49322,49323,49326,49329,49330,49335,49336,49337,49338,49339,49342,49346,49347,49348,49350,49351,49354,49355,49357,49358,49359,49361,49362,49363,49364,49365,49366,49367,49370,49374,49375,49376,49377,49378,49379,49382,49383,49385,49386,49387,49389,49390,49391,49392,49393,49394,49395,49398,49400,49402,49403,49404,49405,49406,49407,49409,49410,49411,49413,49414,49415,49417,49418,49419,49420,49421,49422,49423,49425,49426,49427,49428,49430,49431,49432,49433,49434,49435,49441,49442,49445,49448,49449,49450,49451,49454,49458,49459,49460,49461,49463,49466,49467,49469,49470,49471,49473,49474,49475,49476,49477,49478,49479,49482,49486,49487,49488,49489,49490,49491,49494,49495,null,null,null,null,null,null,49497,49498,49499,49501,49502,49503,49504,49505,49506,49507,49510,49514,49515,49516,49517,49518,49519,49521,49522,49523,49525,49526,49527,49529,49530,49531,null,null,null,null,null,null,49532,49533,49534,49535,49536,49537,49538,49539,49540,49542,49543,49544,49545,49546,49547,49551,49553,49554,49555,49557,49559,49560,49561,49562,49563,49566,49568,49570,49571,49572,49574,49575,49578,49579,49581,49582,49583,49585,49586,49587,49588,49589,49590,49591,49592,49593,49594,49595,49596,49598,49599,49600,49601,49602,49603,49605,49606,49607,49609,49610,49611,49613,49614,49615,49616,49617,49618,49619,49621,49622,49625,49626,49627,49628,49629,49630,49631,49633,49634,49635,49637,49638,49639,49641,49642,49643,49644,49645,49646,49647,49650,49652,49653,49654,49655,49656,49657,49658,49659,49662,49663,49665,49666,49667,49669,49670,49671,49672,49673,49674,49675,49678,49680,49682,49683,49684,49685,49686,49687,49690,49691,49693,49694,49697,49698,49699,49700,49701,49702,49703,49706,49708,49710,49712,49715,49717,49718,49719,49720,49721,49722,49723,49724,49725,49726,49727,49728,49729,49730,49731,49732,49733,null,null,null,null,null,null,49734,49735,49737,49738,49739,49740,49741,49742,49743,49746,49747,49749,49750,49751,49753,49754,49755,49756,49757,49758,49759,49761,49762,49763,49764,49766,null,null,null,null,null,null,49767,49768,49769,49770,49771,49774,49775,49777,49778,49779,49781,49782,49783,49784,49785,49786,49787,49790,49792,49794,49795,49796,49797,49798,49799,49802,49803,49804,49805,49806,49807,49809,49810,49811,49812,49813,49814,49815,49817,49818,49820,49822,49823,49824,49825,49826,49827,49830,49831,49833,49834,49835,49838,49839,49840,49841,49842,49843,49846,49848,49850,49851,49852,49853,49854,49855,49856,49857,49858,49859,49860,49861,49862,49863,49864,49865,49866,49867,49868,49869,49870,49871,49872,49873,49874,49875,49876,49877,49878,49879,49880,49881,49882,49883,49886,49887,49889,49890,49893,49894,49895,49896,49897,49898,49902,49904,49906,49907,49908,49909,49911,49914,49917,49918,49919,49921,49922,49923,49924,49925,49926,49927,49930,49931,49934,49935,49936,49937,49938,49942,49943,49945,49946,49947,49949,49950,49951,49952,49953,49954,49955,49958,49959,49962,49963,49964,49965,49966,49967,49968,49969,49970,null,null,null,null,null,null,49971,49972,49973,49974,49975,49976,49977,49978,49979,49980,49981,49982,49983,49984,49985,49986,49987,49988,49990,49991,49992,49993,49994,49995,49996,49997,null,null,null,null,null,null,49998,49999,50000,50001,50002,50003,50004,50005,50006,50007,50008,50009,50010,50011,50012,50013,50014,50015,50016,50017,50018,50019,50020,50021,50022,50023,50026,50027,50029,50030,50031,50033,50035,50036,50037,50038,50039,50042,50043,50046,50047,50048,50049,50050,50051,50053,50054,50055,50057,50058,50059,50061,50062,50063,50064,50065,50066,50067,50068,50069,50070,50071,50072,50073,50074,50075,50076,50077,50078,50079,50080,50081,50082,50083,50084,50085,50086,50087,50088,50089,50090,50091,50092,50093,50094,50095,50096,50097,50098,50099,50100,50101,50102,50103,50104,50105,50106,50107,50108,50109,50110,50111,50113,50114,50115,50116,50117,50118,50119,50120,50121,50122,50123,50124,50125,50126,50127,50128,50129,50130,50131,50132,50133,50134,50135,50138,50139,50141,50142,50145,50147,50148,50149,50150,50151,50154,50155,50156,50158,50159,50160,50161,50162,50163,50166,50167,50169,50170,50171,50172,50173,50174,null,null,null,null,null,null,50175,50176,50177,50178,50179,50180,50181,50182,50183,50185,50186,50187,50188,50189,50190,50191,50193,50194,50195,50196,50197,50198,50199,50200,50201,50202,null,null,null,null,null,null,50203,50204,50205,50206,50207,50208,50209,50210,50211,50213,50214,50215,50216,50217,50218,50219,50221,50222,50223,50225,50226,50227,50229,50230,50231,50232,50233,50234,50235,50238,50239,50240,50241,50242,50243,50244,50245,50246,50247,50249,50250,50251,50252,50253,50254,50255,50256,50257,50258,50259,50260,50261,50262,50263,50264,50265,50266,50267,50268,50269,50270,50271,50272,50273,50274,50275,50278,50279,50281,50282,50283,50285,50286,50287,50288,50289,50290,50291,50294,50295,50296,50298,50299,50300,50301,50302,50303,50305,50306,50307,50308,50309,50310,50311,50312,50313,50314,50315,50316,50317,50318,50319,50320,50321,50322,50323,50325,50326,50327,50328,50329,50330,50331,50333,50334,50335,50336,50337,50338,50339,50340,50341,50342,50343,50344,50345,50346,50347,50348,50349,50350,50351,50352,50353,50354,50355,50356,50357,50358,50359,50361,50362,50363,50365,50366,50367,50368,50369,50370,50371,50372,50373,null,null,null,null,null,null,50374,50375,50376,50377,50378,50379,50380,50381,50382,50383,50384,50385,50386,50387,50388,50389,50390,50391,50392,50393,50394,50395,50396,50397,50398,50399,null,null,null,null,null,null,50400,50401,50402,50403,50404,50405,50406,50407,50408,50410,50411,50412,50413,50414,50415,50418,50419,50421,50422,50423,50425,50427,50428,50429,50430,50434,50435,50436,50437,50438,50439,50440,50441,50442,50443,50445,50446,50447,50449,50450,50451,50453,50454,50455,50456,50457,50458,50459,50461,50462,50463,50464,50465,50466,50467,50468,50469,50470,50471,50474,50475,50477,50478,50479,50481,50482,50483,50484,50485,50486,50487,50490,50492,50494,50495,50496,50497,50498,50499,50502,50503,50507,50511,50512,50513,50514,50518,50522,50523,50524,50527,50530,50531,50533,50534,50535,50537,50538,50539,50540,50541,50542,50543,50546,50550,50551,50552,50553,50554,50555,50558,50559,50561,50562,50563,50565,50566,50568,50569,50570,50571,50574,50576,50578,50579,50580,50582,50585,50586,50587,50589,50590,50591,50593,50594,50595,50596,50597,50598,50599,50600,50602,50603,50604,50605,50606,50607,50608,50609,50610,50611,50614,null,null,null,null,null,null,50615,50618,50623,50624,50625,50626,50627,50635,50637,50639,50642,50643,50645,50646,50647,50649,50650,50651,50652,50653,50654,50655,50658,50660,50662,50663,null,null,null,null,null,null,50664,50665,50666,50667,50671,50673,50674,50675,50677,50680,50681,50682,50683,50690,50691,50692,50697,50698,50699,50701,50702,50703,50705,50706,50707,50708,50709,50710,50711,50714,50717,50718,50719,50720,50721,50722,50723,50726,50727,50729,50730,50731,50735,50737,50738,50742,50744,50746,50748,50749,50750,50751,50754,50755,50757,50758,50759,50761,50762,50763,50764,50765,50766,50767,50770,50774,50775,50776,50777,50778,50779,50782,50783,50785,50786,50787,50788,50789,50790,50791,50792,50793,50794,50795,50797,50798,50800,50802,50803,50804,50805,50806,50807,50810,50811,50813,50814,50815,50817,50818,50819,50820,50821,50822,50823,50826,50828,50830,50831,50832,50833,50834,50835,50838,50839,50841,50842,50843,50845,50846,50847,50848,50849,50850,50851,50854,50856,50858,50859,50860,50861,50862,50863,50866,50867,50869,50870,50871,50875,50876,50877,50878,50879,50882,50884,50886,50887,50888,50889,50890,50891,50894,null,null,null,null,null,null,50895,50897,50898,50899,50901,50902,50903,50904,50905,50906,50907,50910,50911,50914,50915,50916,50917,50918,50919,50922,50923,50925,50926,50927,50929,50930,null,null,null,null,null,null,50931,50932,50933,50934,50935,50938,50939,50940,50942,50943,50944,50945,50946,50947,50950,50951,50953,50954,50955,50957,50958,50959,50960,50961,50962,50963,50966,50968,50970,50971,50972,50973,50974,50975,50978,50979,50981,50982,50983,50985,50986,50987,50988,50989,50990,50991,50994,50996,50998,51000,51001,51002,51003,51006,51007,51009,51010,51011,51013,51014,51015,51016,51017,51019,51022,51024,51033,51034,51035,51037,51038,51039,51041,51042,51043,51044,51045,51046,51047,51049,51050,51052,51053,51054,51055,51056,51057,51058,51059,51062,51063,51065,51066,51067,51071,51072,51073,51074,51078,51083,51084,51085,51087,51090,51091,51093,51097,51099,51100,51101,51102,51103,51106,51111,51112,51113,51114,51115,51118,51119,51121,51122,51123,51125,51126,51127,51128,51129,51130,51131,51134,51138,51139,51140,51141,51142,51143,51146,51147,51149,51151,51153,51154,51155,51156,51157,51158,51159,51161,51162,51163,51164,null,null,null,null,null,null,51166,51167,51168,51169,51170,51171,51173,51174,51175,51177,51178,51179,51181,51182,51183,51184,51185,51186,51187,51188,51189,51190,51191,51192,51193,51194,null,null,null,null,null,null,51195,51196,51197,51198,51199,51202,51203,51205,51206,51207,51209,51211,51212,51213,51214,51215,51218,51220,51223,51224,51225,51226,51227,51230,51231,51233,51234,51235,51237,51238,51239,51240,51241,51242,51243,51246,51248,51250,51251,51252,51253,51254,51255,51257,51258,51259,51261,51262,51263,51265,51266,51267,51268,51269,51270,51271,51274,51275,51278,51279,51280,51281,51282,51283,51285,51286,51287,51288,51289,51290,51291,51292,51293,51294,51295,51296,51297,51298,51299,51300,51301,51302,51303,51304,51305,51306,51307,51308,51309,51310,51311,51314,51315,51317,51318,51319,51321,51323,51324,51325,51326,51327,51330,51332,51336,51337,51338,51342,51343,51344,51345,51346,51347,51349,51350,51351,51352,51353,51354,51355,51356,51358,51360,51362,51363,51364,51365,51366,51367,51369,51370,51371,51372,51373,51374,51375,51376,51377,51378,51379,51380,51381,51382,51383,51384,51385,51386,51387,51390,51391,51392,51393,null,null,null,null,null,null,51394,51395,51397,51398,51399,51401,51402,51403,51405,51406,51407,51408,51409,51410,51411,51414,51416,51418,51419,51420,51421,51422,51423,51426,51427,51429,null,null,null,null,null,null,51430,51431,51432,51433,51434,51435,51436,51437,51438,51439,51440,51441,51442,51443,51444,51446,51447,51448,51449,51450,51451,51454,51455,51457,51458,51459,51463,51464,51465,51466,51467,51470,12288,12289,12290,183,8229,8230,168,12291,173,8213,8741,65340,8764,8216,8217,8220,8221,12308,12309,12296,12297,12298,12299,12300,12301,12302,12303,12304,12305,177,215,247,8800,8804,8805,8734,8756,176,8242,8243,8451,8491,65504,65505,65509,9794,9792,8736,8869,8978,8706,8711,8801,8786,167,8251,9734,9733,9675,9679,9678,9671,9670,9633,9632,9651,9650,9661,9660,8594,8592,8593,8595,8596,12307,8810,8811,8730,8765,8733,8757,8747,8748,8712,8715,8838,8839,8834,8835,8746,8745,8743,8744,65506,51472,51474,51475,51476,51477,51478,51479,51481,51482,51483,51484,51485,51486,51487,51488,51489,51490,51491,51492,51493,51494,51495,51496,51497,51498,51499,null,null,null,null,null,null,51501,51502,51503,51504,51505,51506,51507,51509,51510,51511,51512,51513,51514,51515,51516,51517,51518,51519,51520,51521,51522,51523,51524,51525,51526,51527,null,null,null,null,null,null,51528,51529,51530,51531,51532,51533,51534,51535,51538,51539,51541,51542,51543,51545,51546,51547,51548,51549,51550,51551,51554,51556,51557,51558,51559,51560,51561,51562,51563,51565,51566,51567,8658,8660,8704,8707,180,65374,711,728,733,730,729,184,731,161,191,720,8750,8721,8719,164,8457,8240,9665,9664,9655,9654,9828,9824,9825,9829,9831,9827,8857,9672,9635,9680,9681,9618,9636,9637,9640,9639,9638,9641,9832,9743,9742,9756,9758,182,8224,8225,8597,8599,8601,8598,8600,9837,9833,9834,9836,12927,12828,8470,13255,8482,13250,13272,8481,8364,174,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,51569,51570,51571,51573,51574,51575,51576,51577,51578,51579,51581,51582,51583,51584,51585,51586,51587,51588,51589,51590,51591,51594,51595,51597,51598,51599,null,null,null,null,null,null,51601,51602,51603,51604,51605,51606,51607,51610,51612,51614,51615,51616,51617,51618,51619,51620,51621,51622,51623,51624,51625,51626,51627,51628,51629,51630,null,null,null,null,null,null,51631,51632,51633,51634,51635,51636,51637,51638,51639,51640,51641,51642,51643,51644,51645,51646,51647,51650,51651,51653,51654,51657,51659,51660,51661,51662,51663,51666,51668,51671,51672,51675,65281,65282,65283,65284,65285,65286,65287,65288,65289,65290,65291,65292,65293,65294,65295,65296,65297,65298,65299,65300,65301,65302,65303,65304,65305,65306,65307,65308,65309,65310,65311,65312,65313,65314,65315,65316,65317,65318,65319,65320,65321,65322,65323,65324,65325,65326,65327,65328,65329,65330,65331,65332,65333,65334,65335,65336,65337,65338,65339,65510,65341,65342,65343,65344,65345,65346,65347,65348,65349,65350,65351,65352,65353,65354,65355,65356,65357,65358,65359,65360,65361,65362,65363,65364,65365,65366,65367,65368,65369,65370,65371,65372,65373,65507,51678,51679,51681,51683,51685,51686,51688,51689,51690,51691,51694,51698,51699,51700,51701,51702,51703,51706,51707,51709,51710,51711,51713,51714,51715,51716,null,null,null,null,null,null,51717,51718,51719,51722,51726,51727,51728,51729,51730,51731,51733,51734,51735,51737,51738,51739,51740,51741,51742,51743,51744,51745,51746,51747,51748,51749,null,null,null,null,null,null,51750,51751,51752,51754,51755,51756,51757,51758,51759,51760,51761,51762,51763,51764,51765,51766,51767,51768,51769,51770,51771,51772,51773,51774,51775,51776,51777,51778,51779,51780,51781,51782,12593,12594,12595,12596,12597,12598,12599,12600,12601,12602,12603,12604,12605,12606,12607,12608,12609,12610,12611,12612,12613,12614,12615,12616,12617,12618,12619,12620,12621,12622,12623,12624,12625,12626,12627,12628,12629,12630,12631,12632,12633,12634,12635,12636,12637,12638,12639,12640,12641,12642,12643,12644,12645,12646,12647,12648,12649,12650,12651,12652,12653,12654,12655,12656,12657,12658,12659,12660,12661,12662,12663,12664,12665,12666,12667,12668,12669,12670,12671,12672,12673,12674,12675,12676,12677,12678,12679,12680,12681,12682,12683,12684,12685,12686,51783,51784,51785,51786,51787,51790,51791,51793,51794,51795,51797,51798,51799,51800,51801,51802,51803,51806,51810,51811,51812,51813,51814,51815,51817,51818,null,null,null,null,null,null,51819,51820,51821,51822,51823,51824,51825,51826,51827,51828,51829,51830,51831,51832,51833,51834,51835,51836,51838,51839,51840,51841,51842,51843,51845,51846,null,null,null,null,null,null,51847,51848,51849,51850,51851,51852,51853,51854,51855,51856,51857,51858,51859,51860,51861,51862,51863,51865,51866,51867,51868,51869,51870,51871,51872,51873,51874,51875,51876,51877,51878,51879,8560,8561,8562,8563,8564,8565,8566,8567,8568,8569,null,null,null,null,null,8544,8545,8546,8547,8548,8549,8550,8551,8552,8553,null,null,null,null,null,null,null,913,914,915,916,917,918,919,920,921,922,923,924,925,926,927,928,929,931,932,933,934,935,936,937,null,null,null,null,null,null,null,null,945,946,947,948,949,950,951,952,953,954,955,956,957,958,959,960,961,963,964,965,966,967,968,969,null,null,null,null,null,null,51880,51881,51882,51883,51884,51885,51886,51887,51888,51889,51890,51891,51892,51893,51894,51895,51896,51897,51898,51899,51902,51903,51905,51906,51907,51909,null,null,null,null,null,null,51910,51911,51912,51913,51914,51915,51918,51920,51922,51924,51925,51926,51927,51930,51931,51932,51933,51934,51935,51937,51938,51939,51940,51941,51942,51943,null,null,null,null,null,null,51944,51945,51946,51947,51949,51950,51951,51952,51953,51954,51955,51957,51958,51959,51960,51961,51962,51963,51964,51965,51966,51967,51968,51969,51970,51971,51972,51973,51974,51975,51977,51978,9472,9474,9484,9488,9496,9492,9500,9516,9508,9524,9532,9473,9475,9487,9491,9499,9495,9507,9523,9515,9531,9547,9504,9519,9512,9527,9535,9501,9520,9509,9528,9538,9490,9489,9498,9497,9494,9493,9486,9485,9502,9503,9505,9506,9510,9511,9513,9514,9517,9518,9521,9522,9525,9526,9529,9530,9533,9534,9536,9537,9539,9540,9541,9542,9543,9544,9545,9546,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,51979,51980,51981,51982,51983,51985,51986,51987,51989,51990,51991,51993,51994,51995,51996,51997,51998,51999,52002,52003,52004,52005,52006,52007,52008,52009,null,null,null,null,null,null,52010,52011,52012,52013,52014,52015,52016,52017,52018,52019,52020,52021,52022,52023,52024,52025,52026,52027,52028,52029,52030,52031,52032,52034,52035,52036,null,null,null,null,null,null,52037,52038,52039,52042,52043,52045,52046,52047,52049,52050,52051,52052,52053,52054,52055,52058,52059,52060,52062,52063,52064,52065,52066,52067,52069,52070,52071,52072,52073,52074,52075,52076,13205,13206,13207,8467,13208,13252,13219,13220,13221,13222,13209,13210,13211,13212,13213,13214,13215,13216,13217,13218,13258,13197,13198,13199,13263,13192,13193,13256,13223,13224,13232,13233,13234,13235,13236,13237,13238,13239,13240,13241,13184,13185,13186,13187,13188,13242,13243,13244,13245,13246,13247,13200,13201,13202,13203,13204,8486,13248,13249,13194,13195,13196,13270,13253,13229,13230,13231,13275,13225,13226,13227,13228,13277,13264,13267,13251,13257,13276,13254,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,52077,52078,52079,52080,52081,52082,52083,52084,52085,52086,52087,52090,52091,52092,52093,52094,52095,52096,52097,52098,52099,52100,52101,52102,52103,52104,null,null,null,null,null,null,52105,52106,52107,52108,52109,52110,52111,52112,52113,52114,52115,52116,52117,52118,52119,52120,52121,52122,52123,52125,52126,52127,52128,52129,52130,52131,null,null,null,null,null,null,52132,52133,52134,52135,52136,52137,52138,52139,52140,52141,52142,52143,52144,52145,52146,52147,52148,52149,52150,52151,52153,52154,52155,52156,52157,52158,52159,52160,52161,52162,52163,52164,198,208,170,294,null,306,null,319,321,216,338,186,222,358,330,null,12896,12897,12898,12899,12900,12901,12902,12903,12904,12905,12906,12907,12908,12909,12910,12911,12912,12913,12914,12915,12916,12917,12918,12919,12920,12921,12922,12923,9424,9425,9426,9427,9428,9429,9430,9431,9432,9433,9434,9435,9436,9437,9438,9439,9440,9441,9442,9443,9444,9445,9446,9447,9448,9449,9312,9313,9314,9315,9316,9317,9318,9319,9320,9321,9322,9323,9324,9325,9326,189,8531,8532,188,190,8539,8540,8541,8542,52165,52166,52167,52168,52169,52170,52171,52172,52173,52174,52175,52176,52177,52178,52179,52181,52182,52183,52184,52185,52186,52187,52188,52189,52190,52191,null,null,null,null,null,null,52192,52193,52194,52195,52197,52198,52200,52202,52203,52204,52205,52206,52207,52208,52209,52210,52211,52212,52213,52214,52215,52216,52217,52218,52219,52220,null,null,null,null,null,null,52221,52222,52223,52224,52225,52226,52227,52228,52229,52230,52231,52232,52233,52234,52235,52238,52239,52241,52242,52243,52245,52246,52247,52248,52249,52250,52251,52254,52255,52256,52259,52260,230,273,240,295,305,307,312,320,322,248,339,223,254,359,331,329,12800,12801,12802,12803,12804,12805,12806,12807,12808,12809,12810,12811,12812,12813,12814,12815,12816,12817,12818,12819,12820,12821,12822,12823,12824,12825,12826,12827,9372,9373,9374,9375,9376,9377,9378,9379,9380,9381,9382,9383,9384,9385,9386,9387,9388,9389,9390,9391,9392,9393,9394,9395,9396,9397,9332,9333,9334,9335,9336,9337,9338,9339,9340,9341,9342,9343,9344,9345,9346,185,178,179,8308,8319,8321,8322,8323,8324,52261,52262,52266,52267,52269,52271,52273,52274,52275,52276,52277,52278,52279,52282,52287,52288,52289,52290,52291,52294,52295,52297,52298,52299,52301,52302,null,null,null,null,null,null,52303,52304,52305,52306,52307,52310,52314,52315,52316,52317,52318,52319,52321,52322,52323,52325,52327,52329,52330,52331,52332,52333,52334,52335,52337,52338,null,null,null,null,null,null,52339,52340,52342,52343,52344,52345,52346,52347,52348,52349,52350,52351,52352,52353,52354,52355,52356,52357,52358,52359,52360,52361,52362,52363,52364,52365,52366,52367,52368,52369,52370,52371,12353,12354,12355,12356,12357,12358,12359,12360,12361,12362,12363,12364,12365,12366,12367,12368,12369,12370,12371,12372,12373,12374,12375,12376,12377,12378,12379,12380,12381,12382,12383,12384,12385,12386,12387,12388,12389,12390,12391,12392,12393,12394,12395,12396,12397,12398,12399,12400,12401,12402,12403,12404,12405,12406,12407,12408,12409,12410,12411,12412,12413,12414,12415,12416,12417,12418,12419,12420,12421,12422,12423,12424,12425,12426,12427,12428,12429,12430,12431,12432,12433,12434,12435,null,null,null,null,null,null,null,null,null,null,null,52372,52373,52374,52375,52378,52379,52381,52382,52383,52385,52386,52387,52388,52389,52390,52391,52394,52398,52399,52400,52401,52402,52403,52406,52407,52409,null,null,null,null,null,null,52410,52411,52413,52414,52415,52416,52417,52418,52419,52422,52424,52426,52427,52428,52429,52430,52431,52433,52434,52435,52437,52438,52439,52440,52441,52442,null,null,null,null,null,null,52443,52444,52445,52446,52447,52448,52449,52450,52451,52453,52454,52455,52456,52457,52458,52459,52461,52462,52463,52465,52466,52467,52468,52469,52470,52471,52472,52473,52474,52475,52476,52477,12449,12450,12451,12452,12453,12454,12455,12456,12457,12458,12459,12460,12461,12462,12463,12464,12465,12466,12467,12468,12469,12470,12471,12472,12473,12474,12475,12476,12477,12478,12479,12480,12481,12482,12483,12484,12485,12486,12487,12488,12489,12490,12491,12492,12493,12494,12495,12496,12497,12498,12499,12500,12501,12502,12503,12504,12505,12506,12507,12508,12509,12510,12511,12512,12513,12514,12515,12516,12517,12518,12519,12520,12521,12522,12523,12524,12525,12526,12527,12528,12529,12530,12531,12532,12533,12534,null,null,null,null,null,null,null,null,52478,52479,52480,52482,52483,52484,52485,52486,52487,52490,52491,52493,52494,52495,52497,52498,52499,52500,52501,52502,52503,52506,52508,52510,52511,52512,null,null,null,null,null,null,52513,52514,52515,52517,52518,52519,52521,52522,52523,52525,52526,52527,52528,52529,52530,52531,52532,52533,52534,52535,52536,52538,52539,52540,52541,52542,null,null,null,null,null,null,52543,52544,52545,52546,52547,52548,52549,52550,52551,52552,52553,52554,52555,52556,52557,52558,52559,52560,52561,52562,52563,52564,52565,52566,52567,52568,52569,52570,52571,52573,52574,52575,1040,1041,1042,1043,1044,1045,1025,1046,1047,1048,1049,1050,1051,1052,1053,1054,1055,1056,1057,1058,1059,1060,1061,1062,1063,1064,1065,1066,1067,1068,1069,1070,1071,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,1072,1073,1074,1075,1076,1077,1105,1078,1079,1080,1081,1082,1083,1084,1085,1086,1087,1088,1089,1090,1091,1092,1093,1094,1095,1096,1097,1098,1099,1100,1101,1102,1103,null,null,null,null,null,null,null,null,null,null,null,null,null,52577,52578,52579,52581,52582,52583,52584,52585,52586,52587,52590,52592,52594,52595,52596,52597,52598,52599,52601,52602,52603,52604,52605,52606,52607,52608,null,null,null,null,null,null,52609,52610,52611,52612,52613,52614,52615,52617,52618,52619,52620,52621,52622,52623,52624,52625,52626,52627,52630,52631,52633,52634,52635,52637,52638,52639,null,null,null,null,null,null,52640,52641,52642,52643,52646,52648,52650,52651,52652,52653,52654,52655,52657,52658,52659,52660,52661,52662,52663,52664,52665,52666,52667,52668,52669,52670,52671,52672,52673,52674,52675,52677,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,52678,52679,52680,52681,52682,52683,52685,52686,52687,52689,52690,52691,52692,52693,52694,52695,52696,52697,52698,52699,52700,52701,52702,52703,52704,52705,null,null,null,null,null,null,52706,52707,52708,52709,52710,52711,52713,52714,52715,52717,52718,52719,52721,52722,52723,52724,52725,52726,52727,52730,52732,52734,52735,52736,52737,52738,null,null,null,null,null,null,52739,52741,52742,52743,52745,52746,52747,52749,52750,52751,52752,52753,52754,52755,52757,52758,52759,52760,52762,52763,52764,52765,52766,52767,52770,52771,52773,52774,52775,52777,52778,52779,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,52780,52781,52782,52783,52786,52788,52790,52791,52792,52793,52794,52795,52796,52797,52798,52799,52800,52801,52802,52803,52804,52805,52806,52807,52808,52809,null,null,null,null,null,null,52810,52811,52812,52813,52814,52815,52816,52817,52818,52819,52820,52821,52822,52823,52826,52827,52829,52830,52834,52835,52836,52837,52838,52839,52842,52844,null,null,null,null,null,null,52846,52847,52848,52849,52850,52851,52854,52855,52857,52858,52859,52861,52862,52863,52864,52865,52866,52867,52870,52872,52874,52875,52876,52877,52878,52879,52882,52883,52885,52886,52887,52889,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,52890,52891,52892,52893,52894,52895,52898,52902,52903,52904,52905,52906,52907,52910,52911,52912,52913,52914,52915,52916,52917,52918,52919,52920,52921,52922,null,null,null,null,null,null,52923,52924,52925,52926,52927,52928,52930,52931,52932,52933,52934,52935,52936,52937,52938,52939,52940,52941,52942,52943,52944,52945,52946,52947,52948,52949,null,null,null,null,null,null,52950,52951,52952,52953,52954,52955,52956,52957,52958,52959,52960,52961,52962,52963,52966,52967,52969,52970,52973,52974,52975,52976,52977,52978,52979,52982,52986,52987,52988,52989,52990,52991,44032,44033,44036,44039,44040,44041,44042,44048,44049,44050,44051,44052,44053,44054,44055,44057,44058,44059,44060,44061,44064,44068,44076,44077,44079,44080,44081,44088,44089,44092,44096,44107,44109,44116,44120,44124,44144,44145,44148,44151,44152,44154,44160,44161,44163,44164,44165,44166,44169,44170,44171,44172,44176,44180,44188,44189,44191,44192,44193,44200,44201,44202,44204,44207,44208,44216,44217,44219,44220,44221,44225,44228,44232,44236,44245,44247,44256,44257,44260,44263,44264,44266,44268,44271,44272,44273,44275,44277,44278,44284,44285,44288,44292,44294,52994,52995,52997,52998,52999,53001,53002,53003,53004,53005,53006,53007,53010,53012,53014,53015,53016,53017,53018,53019,53021,53022,53023,53025,53026,53027,null,null,null,null,null,null,53029,53030,53031,53032,53033,53034,53035,53038,53042,53043,53044,53045,53046,53047,53049,53050,53051,53052,53053,53054,53055,53056,53057,53058,53059,53060,null,null,null,null,null,null,53061,53062,53063,53064,53065,53066,53067,53068,53069,53070,53071,53072,53073,53074,53075,53078,53079,53081,53082,53083,53085,53086,53087,53088,53089,53090,53091,53094,53096,53098,53099,53100,44300,44301,44303,44305,44312,44316,44320,44329,44332,44333,44340,44341,44344,44348,44356,44357,44359,44361,44368,44372,44376,44385,44387,44396,44397,44400,44403,44404,44405,44406,44411,44412,44413,44415,44417,44418,44424,44425,44428,44432,44444,44445,44452,44471,44480,44481,44484,44488,44496,44497,44499,44508,44512,44516,44536,44537,44540,44543,44544,44545,44552,44553,44555,44557,44564,44592,44593,44596,44599,44600,44602,44608,44609,44611,44613,44614,44618,44620,44621,44622,44624,44628,44630,44636,44637,44639,44640,44641,44645,44648,44649,44652,44656,44664,53101,53102,53103,53106,53107,53109,53110,53111,53113,53114,53115,53116,53117,53118,53119,53121,53122,53123,53124,53126,53127,53128,53129,53130,53131,53133,null,null,null,null,null,null,53134,53135,53136,53137,53138,53139,53140,53141,53142,53143,53144,53145,53146,53147,53148,53149,53150,53151,53152,53154,53155,53156,53157,53158,53159,53161,null,null,null,null,null,null,53162,53163,53164,53165,53166,53167,53169,53170,53171,53172,53173,53174,53175,53176,53177,53178,53179,53180,53181,53182,53183,53184,53185,53186,53187,53189,53190,53191,53192,53193,53194,53195,44665,44667,44668,44669,44676,44677,44684,44732,44733,44734,44736,44740,44748,44749,44751,44752,44753,44760,44761,44764,44776,44779,44781,44788,44792,44796,44807,44808,44813,44816,44844,44845,44848,44850,44852,44860,44861,44863,44865,44866,44867,44872,44873,44880,44892,44893,44900,44901,44921,44928,44932,44936,44944,44945,44949,44956,44984,44985,44988,44992,44999,45000,45001,45003,45005,45006,45012,45020,45032,45033,45040,45041,45044,45048,45056,45057,45060,45068,45072,45076,45084,45085,45096,45124,45125,45128,45130,45132,45134,45139,45140,45141,45143,45145,53196,53197,53198,53199,53200,53201,53202,53203,53204,53205,53206,53207,53208,53209,53210,53211,53212,53213,53214,53215,53218,53219,53221,53222,53223,53225,null,null,null,null,null,null,53226,53227,53228,53229,53230,53231,53234,53236,53238,53239,53240,53241,53242,53243,53245,53246,53247,53249,53250,53251,53253,53254,53255,53256,53257,53258,null,null,null,null,null,null,53259,53260,53261,53262,53263,53264,53266,53267,53268,53269,53270,53271,53273,53274,53275,53276,53277,53278,53279,53280,53281,53282,53283,53284,53285,53286,53287,53288,53289,53290,53291,53292,45149,45180,45181,45184,45188,45196,45197,45199,45201,45208,45209,45210,45212,45215,45216,45217,45218,45224,45225,45227,45228,45229,45230,45231,45233,45235,45236,45237,45240,45244,45252,45253,45255,45256,45257,45264,45265,45268,45272,45280,45285,45320,45321,45323,45324,45328,45330,45331,45336,45337,45339,45340,45341,45347,45348,45349,45352,45356,45364,45365,45367,45368,45369,45376,45377,45380,45384,45392,45393,45396,45397,45400,45404,45408,45432,45433,45436,45440,45442,45448,45449,45451,45453,45458,45459,45460,45464,45468,45480,45516,45520,45524,45532,45533,53294,53295,53296,53297,53298,53299,53302,53303,53305,53306,53307,53309,53310,53311,53312,53313,53314,53315,53318,53320,53322,53323,53324,53325,53326,53327,null,null,null,null,null,null,53329,53330,53331,53333,53334,53335,53337,53338,53339,53340,53341,53342,53343,53345,53346,53347,53348,53349,53350,53351,53352,53353,53354,53355,53358,53359,null,null,null,null,null,null,53361,53362,53363,53365,53366,53367,53368,53369,53370,53371,53374,53375,53376,53378,53379,53380,53381,53382,53383,53384,53385,53386,53387,53388,53389,53390,53391,53392,53393,53394,53395,53396,45535,45544,45545,45548,45552,45561,45563,45565,45572,45573,45576,45579,45580,45588,45589,45591,45593,45600,45620,45628,45656,45660,45664,45672,45673,45684,45685,45692,45700,45701,45705,45712,45713,45716,45720,45721,45722,45728,45729,45731,45733,45734,45738,45740,45744,45748,45768,45769,45772,45776,45778,45784,45785,45787,45789,45794,45796,45797,45798,45800,45803,45804,45805,45806,45807,45811,45812,45813,45815,45816,45817,45818,45819,45823,45824,45825,45828,45832,45840,45841,45843,45844,45845,45852,45908,45909,45910,45912,45915,45916,45918,45919,45924,45925,53397,53398,53399,53400,53401,53402,53403,53404,53405,53406,53407,53408,53409,53410,53411,53414,53415,53417,53418,53419,53421,53422,53423,53424,53425,53426,null,null,null,null,null,null,53427,53430,53432,53434,53435,53436,53437,53438,53439,53442,53443,53445,53446,53447,53450,53451,53452,53453,53454,53455,53458,53462,53463,53464,53465,53466,null,null,null,null,null,null,53467,53470,53471,53473,53474,53475,53477,53478,53479,53480,53481,53482,53483,53486,53490,53491,53492,53493,53494,53495,53497,53498,53499,53500,53501,53502,53503,53504,53505,53506,53507,53508,45927,45929,45931,45934,45936,45937,45940,45944,45952,45953,45955,45956,45957,45964,45968,45972,45984,45985,45992,45996,46020,46021,46024,46027,46028,46030,46032,46036,46037,46039,46041,46043,46045,46048,46052,46056,46076,46096,46104,46108,46112,46120,46121,46123,46132,46160,46161,46164,46168,46176,46177,46179,46181,46188,46208,46216,46237,46244,46248,46252,46261,46263,46265,46272,46276,46280,46288,46293,46300,46301,46304,46307,46308,46310,46316,46317,46319,46321,46328,46356,46357,46360,46363,46364,46372,46373,46375,46376,46377,46378,46384,46385,46388,46392,53509,53510,53511,53512,53513,53514,53515,53516,53518,53519,53520,53521,53522,53523,53524,53525,53526,53527,53528,53529,53530,53531,53532,53533,53534,53535,null,null,null,null,null,null,53536,53537,53538,53539,53540,53541,53542,53543,53544,53545,53546,53547,53548,53549,53550,53551,53554,53555,53557,53558,53559,53561,53563,53564,53565,53566,null,null,null,null,null,null,53567,53570,53574,53575,53576,53577,53578,53579,53582,53583,53585,53586,53587,53589,53590,53591,53592,53593,53594,53595,53598,53600,53602,53603,53604,53605,53606,53607,53609,53610,53611,53613,46400,46401,46403,46404,46405,46411,46412,46413,46416,46420,46428,46429,46431,46432,46433,46496,46497,46500,46504,46506,46507,46512,46513,46515,46516,46517,46523,46524,46525,46528,46532,46540,46541,46543,46544,46545,46552,46572,46608,46609,46612,46616,46629,46636,46644,46664,46692,46696,46748,46749,46752,46756,46763,46764,46769,46804,46832,46836,46840,46848,46849,46853,46888,46889,46892,46895,46896,46904,46905,46907,46916,46920,46924,46932,46933,46944,46948,46952,46960,46961,46963,46965,46972,46973,46976,46980,46988,46989,46991,46992,46993,46994,46998,46999,53614,53615,53616,53617,53618,53619,53620,53621,53622,53623,53624,53625,53626,53627,53629,53630,53631,53632,53633,53634,53635,53637,53638,53639,53641,53642,null,null,null,null,null,null,53643,53644,53645,53646,53647,53648,53649,53650,53651,53652,53653,53654,53655,53656,53657,53658,53659,53660,53661,53662,53663,53666,53667,53669,53670,53671,null,null,null,null,null,null,53673,53674,53675,53676,53677,53678,53679,53682,53684,53686,53687,53688,53689,53691,53693,53694,53695,53697,53698,53699,53700,53701,53702,53703,53704,53705,53706,53707,53708,53709,53710,53711,47000,47001,47004,47008,47016,47017,47019,47020,47021,47028,47029,47032,47047,47049,47084,47085,47088,47092,47100,47101,47103,47104,47105,47111,47112,47113,47116,47120,47128,47129,47131,47133,47140,47141,47144,47148,47156,47157,47159,47160,47161,47168,47172,47185,47187,47196,47197,47200,47204,47212,47213,47215,47217,47224,47228,47245,47272,47280,47284,47288,47296,47297,47299,47301,47308,47312,47316,47325,47327,47329,47336,47337,47340,47344,47352,47353,47355,47357,47364,47384,47392,47420,47421,47424,47428,47436,47439,47441,47448,47449,47452,47456,47464,47465,53712,53713,53714,53715,53716,53717,53718,53719,53721,53722,53723,53724,53725,53726,53727,53728,53729,53730,53731,53732,53733,53734,53735,53736,53737,53738,null,null,null,null,null,null,53739,53740,53741,53742,53743,53744,53745,53746,53747,53749,53750,53751,53753,53754,53755,53756,53757,53758,53759,53760,53761,53762,53763,53764,53765,53766,null,null,null,null,null,null,53768,53770,53771,53772,53773,53774,53775,53777,53778,53779,53780,53781,53782,53783,53784,53785,53786,53787,53788,53789,53790,53791,53792,53793,53794,53795,53796,53797,53798,53799,53800,53801,47467,47469,47476,47477,47480,47484,47492,47493,47495,47497,47498,47501,47502,47532,47533,47536,47540,47548,47549,47551,47553,47560,47561,47564,47566,47567,47568,47569,47570,47576,47577,47579,47581,47582,47585,47587,47588,47589,47592,47596,47604,47605,47607,47608,47609,47610,47616,47617,47624,47637,47672,47673,47676,47680,47682,47688,47689,47691,47693,47694,47699,47700,47701,47704,47708,47716,47717,47719,47720,47721,47728,47729,47732,47736,47747,47748,47749,47751,47756,47784,47785,47787,47788,47792,47794,47800,47801,47803,47805,47812,47816,47832,47833,47868,53802,53803,53806,53807,53809,53810,53811,53813,53814,53815,53816,53817,53818,53819,53822,53824,53826,53827,53828,53829,53830,53831,53833,53834,53835,53836,null,null,null,null,null,null,53837,53838,53839,53840,53841,53842,53843,53844,53845,53846,53847,53848,53849,53850,53851,53853,53854,53855,53856,53857,53858,53859,53861,53862,53863,53864,null,null,null,null,null,null,53865,53866,53867,53868,53869,53870,53871,53872,53873,53874,53875,53876,53877,53878,53879,53880,53881,53882,53883,53884,53885,53886,53887,53890,53891,53893,53894,53895,53897,53898,53899,53900,47872,47876,47885,47887,47889,47896,47900,47904,47913,47915,47924,47925,47926,47928,47931,47932,47933,47934,47940,47941,47943,47945,47949,47951,47952,47956,47960,47969,47971,47980,48008,48012,48016,48036,48040,48044,48052,48055,48064,48068,48072,48080,48083,48120,48121,48124,48127,48128,48130,48136,48137,48139,48140,48141,48143,48145,48148,48149,48150,48151,48152,48155,48156,48157,48158,48159,48164,48165,48167,48169,48173,48176,48177,48180,48184,48192,48193,48195,48196,48197,48201,48204,48205,48208,48221,48260,48261,48264,48267,48268,48270,48276,48277,48279,53901,53902,53903,53906,53907,53908,53910,53911,53912,53913,53914,53915,53917,53918,53919,53921,53922,53923,53925,53926,53927,53928,53929,53930,53931,53933,null,null,null,null,null,null,53934,53935,53936,53938,53939,53940,53941,53942,53943,53946,53947,53949,53950,53953,53955,53956,53957,53958,53959,53962,53964,53965,53966,53967,53968,53969,null,null,null,null,null,null,53970,53971,53973,53974,53975,53977,53978,53979,53981,53982,53983,53984,53985,53986,53987,53990,53991,53992,53993,53994,53995,53996,53997,53998,53999,54002,54003,54005,54006,54007,54009,54010,48281,48282,48288,48289,48292,48295,48296,48304,48305,48307,48308,48309,48316,48317,48320,48324,48333,48335,48336,48337,48341,48344,48348,48372,48373,48374,48376,48380,48388,48389,48391,48393,48400,48404,48420,48428,48448,48456,48457,48460,48464,48472,48473,48484,48488,48512,48513,48516,48519,48520,48521,48522,48528,48529,48531,48533,48537,48538,48540,48548,48560,48568,48596,48597,48600,48604,48617,48624,48628,48632,48640,48643,48645,48652,48653,48656,48660,48668,48669,48671,48708,48709,48712,48716,48718,48724,48725,48727,48729,48730,48731,48736,48737,48740,54011,54012,54013,54014,54015,54018,54020,54022,54023,54024,54025,54026,54027,54031,54033,54034,54035,54037,54039,54040,54041,54042,54043,54046,54050,54051,null,null,null,null,null,null,54052,54054,54055,54058,54059,54061,54062,54063,54065,54066,54067,54068,54069,54070,54071,54074,54078,54079,54080,54081,54082,54083,54086,54087,54088,54089,null,null,null,null,null,null,54090,54091,54092,54093,54094,54095,54096,54097,54098,54099,54100,54101,54102,54103,54104,54105,54106,54107,54108,54109,54110,54111,54112,54113,54114,54115,54116,54117,54118,54119,54120,54121,48744,48746,48752,48753,48755,48756,48757,48763,48764,48765,48768,48772,48780,48781,48783,48784,48785,48792,48793,48808,48848,48849,48852,48855,48856,48864,48867,48868,48869,48876,48897,48904,48905,48920,48921,48923,48924,48925,48960,48961,48964,48968,48976,48977,48981,49044,49072,49093,49100,49101,49104,49108,49116,49119,49121,49212,49233,49240,49244,49248,49256,49257,49296,49297,49300,49304,49312,49313,49315,49317,49324,49325,49327,49328,49331,49332,49333,49334,49340,49341,49343,49344,49345,49349,49352,49353,49356,49360,49368,49369,49371,49372,49373,49380,54122,54123,54124,54125,54126,54127,54128,54129,54130,54131,54132,54133,54134,54135,54136,54137,54138,54139,54142,54143,54145,54146,54147,54149,54150,54151,null,null,null,null,null,null,54152,54153,54154,54155,54158,54162,54163,54164,54165,54166,54167,54170,54171,54173,54174,54175,54177,54178,54179,54180,54181,54182,54183,54186,54188,54190,null,null,null,null,null,null,54191,54192,54193,54194,54195,54197,54198,54199,54201,54202,54203,54205,54206,54207,54208,54209,54210,54211,54214,54215,54218,54219,54220,54221,54222,54223,54225,54226,54227,54228,54229,54230,49381,49384,49388,49396,49397,49399,49401,49408,49412,49416,49424,49429,49436,49437,49438,49439,49440,49443,49444,49446,49447,49452,49453,49455,49456,49457,49462,49464,49465,49468,49472,49480,49481,49483,49484,49485,49492,49493,49496,49500,49508,49509,49511,49512,49513,49520,49524,49528,49541,49548,49549,49550,49552,49556,49558,49564,49565,49567,49569,49573,49576,49577,49580,49584,49597,49604,49608,49612,49620,49623,49624,49632,49636,49640,49648,49649,49651,49660,49661,49664,49668,49676,49677,49679,49681,49688,49689,49692,49695,49696,49704,49705,49707,49709,54231,54233,54234,54235,54236,54237,54238,54239,54240,54242,54244,54245,54246,54247,54248,54249,54250,54251,54254,54255,54257,54258,54259,54261,54262,54263,null,null,null,null,null,null,54264,54265,54266,54267,54270,54272,54274,54275,54276,54277,54278,54279,54281,54282,54283,54284,54285,54286,54287,54288,54289,54290,54291,54292,54293,54294,null,null,null,null,null,null,54295,54296,54297,54298,54299,54300,54302,54303,54304,54305,54306,54307,54308,54309,54310,54311,54312,54313,54314,54315,54316,54317,54318,54319,54320,54321,54322,54323,54324,54325,54326,54327,49711,49713,49714,49716,49736,49744,49745,49748,49752,49760,49765,49772,49773,49776,49780,49788,49789,49791,49793,49800,49801,49808,49816,49819,49821,49828,49829,49832,49836,49837,49844,49845,49847,49849,49884,49885,49888,49891,49892,49899,49900,49901,49903,49905,49910,49912,49913,49915,49916,49920,49928,49929,49932,49933,49939,49940,49941,49944,49948,49956,49957,49960,49961,49989,50024,50025,50028,50032,50034,50040,50041,50044,50045,50052,50056,50060,50112,50136,50137,50140,50143,50144,50146,50152,50153,50157,50164,50165,50168,50184,50192,50212,50220,50224,54328,54329,54330,54331,54332,54333,54334,54335,54337,54338,54339,54341,54342,54343,54344,54345,54346,54347,54348,54349,54350,54351,54352,54353,54354,54355,null,null,null,null,null,null,54356,54357,54358,54359,54360,54361,54362,54363,54365,54366,54367,54369,54370,54371,54373,54374,54375,54376,54377,54378,54379,54380,54382,54384,54385,54386,null,null,null,null,null,null,54387,54388,54389,54390,54391,54394,54395,54397,54398,54401,54403,54404,54405,54406,54407,54410,54412,54414,54415,54416,54417,54418,54419,54421,54422,54423,54424,54425,54426,54427,54428,54429,50228,50236,50237,50248,50276,50277,50280,50284,50292,50293,50297,50304,50324,50332,50360,50364,50409,50416,50417,50420,50424,50426,50431,50432,50433,50444,50448,50452,50460,50472,50473,50476,50480,50488,50489,50491,50493,50500,50501,50504,50505,50506,50508,50509,50510,50515,50516,50517,50519,50520,50521,50525,50526,50528,50529,50532,50536,50544,50545,50547,50548,50549,50556,50557,50560,50564,50567,50572,50573,50575,50577,50581,50583,50584,50588,50592,50601,50612,50613,50616,50617,50619,50620,50621,50622,50628,50629,50630,50631,50632,50633,50634,50636,50638,54430,54431,54432,54433,54434,54435,54436,54437,54438,54439,54440,54442,54443,54444,54445,54446,54447,54448,54449,54450,54451,54452,54453,54454,54455,54456,null,null,null,null,null,null,54457,54458,54459,54460,54461,54462,54463,54464,54465,54466,54467,54468,54469,54470,54471,54472,54473,54474,54475,54477,54478,54479,54481,54482,54483,54485,null,null,null,null,null,null,54486,54487,54488,54489,54490,54491,54493,54494,54496,54497,54498,54499,54500,54501,54502,54503,54505,54506,54507,54509,54510,54511,54513,54514,54515,54516,54517,54518,54519,54521,54522,54524,50640,50641,50644,50648,50656,50657,50659,50661,50668,50669,50670,50672,50676,50678,50679,50684,50685,50686,50687,50688,50689,50693,50694,50695,50696,50700,50704,50712,50713,50715,50716,50724,50725,50728,50732,50733,50734,50736,50739,50740,50741,50743,50745,50747,50752,50753,50756,50760,50768,50769,50771,50772,50773,50780,50781,50784,50796,50799,50801,50808,50809,50812,50816,50824,50825,50827,50829,50836,50837,50840,50844,50852,50853,50855,50857,50864,50865,50868,50872,50873,50874,50880,50881,50883,50885,50892,50893,50896,50900,50908,50909,50912,50913,50920,54526,54527,54528,54529,54530,54531,54533,54534,54535,54537,54538,54539,54541,54542,54543,54544,54545,54546,54547,54550,54552,54553,54554,54555,54556,54557,null,null,null,null,null,null,54558,54559,54560,54561,54562,54563,54564,54565,54566,54567,54568,54569,54570,54571,54572,54573,54574,54575,54576,54577,54578,54579,54580,54581,54582,54583,null,null,null,null,null,null,54584,54585,54586,54587,54590,54591,54593,54594,54595,54597,54598,54599,54600,54601,54602,54603,54606,54608,54610,54611,54612,54613,54614,54615,54618,54619,54621,54622,54623,54625,54626,54627,50921,50924,50928,50936,50937,50941,50948,50949,50952,50956,50964,50965,50967,50969,50976,50977,50980,50984,50992,50993,50995,50997,50999,51004,51005,51008,51012,51018,51020,51021,51023,51025,51026,51027,51028,51029,51030,51031,51032,51036,51040,51048,51051,51060,51061,51064,51068,51069,51070,51075,51076,51077,51079,51080,51081,51082,51086,51088,51089,51092,51094,51095,51096,51098,51104,51105,51107,51108,51109,51110,51116,51117,51120,51124,51132,51133,51135,51136,51137,51144,51145,51148,51150,51152,51160,51165,51172,51176,51180,51200,51201,51204,51208,51210,54628,54630,54631,54634,54636,54638,54639,54640,54641,54642,54643,54646,54647,54649,54650,54651,54653,54654,54655,54656,54657,54658,54659,54662,54666,54667,null,null,null,null,null,null,54668,54669,54670,54671,54673,54674,54675,54676,54677,54678,54679,54680,54681,54682,54683,54684,54685,54686,54687,54688,54689,54690,54691,54692,54694,54695,null,null,null,null,null,null,54696,54697,54698,54699,54700,54701,54702,54703,54704,54705,54706,54707,54708,54709,54710,54711,54712,54713,54714,54715,54716,54717,54718,54719,54720,54721,54722,54723,54724,54725,54726,54727,51216,51217,51219,51221,51222,51228,51229,51232,51236,51244,51245,51247,51249,51256,51260,51264,51272,51273,51276,51277,51284,51312,51313,51316,51320,51322,51328,51329,51331,51333,51334,51335,51339,51340,51341,51348,51357,51359,51361,51368,51388,51389,51396,51400,51404,51412,51413,51415,51417,51424,51425,51428,51445,51452,51453,51456,51460,51461,51462,51468,51469,51471,51473,51480,51500,51508,51536,51537,51540,51544,51552,51553,51555,51564,51568,51572,51580,51592,51593,51596,51600,51608,51609,51611,51613,51648,51649,51652,51655,51656,51658,51664,51665,51667,54730,54731,54733,54734,54735,54737,54739,54740,54741,54742,54743,54746,54748,54750,54751,54752,54753,54754,54755,54758,54759,54761,54762,54763,54765,54766,null,null,null,null,null,null,54767,54768,54769,54770,54771,54774,54776,54778,54779,54780,54781,54782,54783,54786,54787,54789,54790,54791,54793,54794,54795,54796,54797,54798,54799,54802,null,null,null,null,null,null,54806,54807,54808,54809,54810,54811,54813,54814,54815,54817,54818,54819,54821,54822,54823,54824,54825,54826,54827,54828,54830,54831,54832,54833,54834,54835,54836,54837,54838,54839,54842,54843,51669,51670,51673,51674,51676,51677,51680,51682,51684,51687,51692,51693,51695,51696,51697,51704,51705,51708,51712,51720,51721,51723,51724,51725,51732,51736,51753,51788,51789,51792,51796,51804,51805,51807,51808,51809,51816,51837,51844,51864,51900,51901,51904,51908,51916,51917,51919,51921,51923,51928,51929,51936,51948,51956,51976,51984,51988,51992,52000,52001,52033,52040,52041,52044,52048,52056,52057,52061,52068,52088,52089,52124,52152,52180,52196,52199,52201,52236,52237,52240,52244,52252,52253,52257,52258,52263,52264,52265,52268,52270,52272,52280,52281,52283,54845,54846,54847,54849,54850,54851,54852,54854,54855,54858,54860,54862,54863,54864,54866,54867,54870,54871,54873,54874,54875,54877,54878,54879,54880,54881,null,null,null,null,null,null,54882,54883,54884,54885,54886,54888,54890,54891,54892,54893,54894,54895,54898,54899,54901,54902,54903,54904,54905,54906,54907,54908,54909,54910,54911,54912,null,null,null,null,null,null,54913,54914,54916,54918,54919,54920,54921,54922,54923,54926,54927,54929,54930,54931,54933,54934,54935,54936,54937,54938,54939,54940,54942,54944,54946,54947,54948,54949,54950,54951,54953,54954,52284,52285,52286,52292,52293,52296,52300,52308,52309,52311,52312,52313,52320,52324,52326,52328,52336,52341,52376,52377,52380,52384,52392,52393,52395,52396,52397,52404,52405,52408,52412,52420,52421,52423,52425,52432,52436,52452,52460,52464,52481,52488,52489,52492,52496,52504,52505,52507,52509,52516,52520,52524,52537,52572,52576,52580,52588,52589,52591,52593,52600,52616,52628,52629,52632,52636,52644,52645,52647,52649,52656,52676,52684,52688,52712,52716,52720,52728,52729,52731,52733,52740,52744,52748,52756,52761,52768,52769,52772,52776,52784,52785,52787,52789,54955,54957,54958,54959,54961,54962,54963,54964,54965,54966,54967,54968,54970,54972,54973,54974,54975,54976,54977,54978,54979,54982,54983,54985,54986,54987,null,null,null,null,null,null,54989,54990,54991,54992,54994,54995,54997,54998,55000,55002,55003,55004,55005,55006,55007,55009,55010,55011,55013,55014,55015,55017,55018,55019,55020,55021,null,null,null,null,null,null,55022,55023,55025,55026,55027,55028,55030,55031,55032,55033,55034,55035,55038,55039,55041,55042,55043,55045,55046,55047,55048,55049,55050,55051,55052,55053,55054,55055,55056,55058,55059,55060,52824,52825,52828,52831,52832,52833,52840,52841,52843,52845,52852,52853,52856,52860,52868,52869,52871,52873,52880,52881,52884,52888,52896,52897,52899,52900,52901,52908,52909,52929,52964,52965,52968,52971,52972,52980,52981,52983,52984,52985,52992,52993,52996,53000,53008,53009,53011,53013,53020,53024,53028,53036,53037,53039,53040,53041,53048,53076,53077,53080,53084,53092,53093,53095,53097,53104,53105,53108,53112,53120,53125,53132,53153,53160,53168,53188,53216,53217,53220,53224,53232,53233,53235,53237,53244,53248,53252,53265,53272,53293,53300,53301,53304,53308,55061,55062,55063,55066,55067,55069,55070,55071,55073,55074,55075,55076,55077,55078,55079,55082,55084,55086,55087,55088,55089,55090,55091,55094,55095,55097,null,null,null,null,null,null,55098,55099,55101,55102,55103,55104,55105,55106,55107,55109,55110,55112,55114,55115,55116,55117,55118,55119,55122,55123,55125,55130,55131,55132,55133,55134,null,null,null,null,null,null,55135,55138,55140,55142,55143,55144,55146,55147,55149,55150,55151,55153,55154,55155,55157,55158,55159,55160,55161,55162,55163,55166,55167,55168,55170,55171,55172,55173,55174,55175,55178,55179,53316,53317,53319,53321,53328,53332,53336,53344,53356,53357,53360,53364,53372,53373,53377,53412,53413,53416,53420,53428,53429,53431,53433,53440,53441,53444,53448,53449,53456,53457,53459,53460,53461,53468,53469,53472,53476,53484,53485,53487,53488,53489,53496,53517,53552,53553,53556,53560,53562,53568,53569,53571,53572,53573,53580,53581,53584,53588,53596,53597,53599,53601,53608,53612,53628,53636,53640,53664,53665,53668,53672,53680,53681,53683,53685,53690,53692,53696,53720,53748,53752,53767,53769,53776,53804,53805,53808,53812,53820,53821,53823,53825,53832,53852,55181,55182,55183,55185,55186,55187,55188,55189,55190,55191,55194,55196,55198,55199,55200,55201,55202,55203,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,53860,53888,53889,53892,53896,53904,53905,53909,53916,53920,53924,53932,53937,53944,53945,53948,53951,53952,53954,53960,53961,53963,53972,53976,53980,53988,53989,54000,54001,54004,54008,54016,54017,54019,54021,54028,54029,54030,54032,54036,54038,54044,54045,54047,54048,54049,54053,54056,54057,54060,54064,54072,54073,54075,54076,54077,54084,54085,54140,54141,54144,54148,54156,54157,54159,54160,54161,54168,54169,54172,54176,54184,54185,54187,54189,54196,54200,54204,54212,54213,54216,54217,54224,54232,54241,54243,54252,54253,54256,54260,54268,54269,54271,54273,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,54280,54301,54336,54340,54364,54368,54372,54381,54383,54392,54393,54396,54399,54400,54402,54408,54409,54411,54413,54420,54441,54476,54480,54484,54492,54495,54504,54508,54512,54520,54523,54525,54532,54536,54540,54548,54549,54551,54588,54589,54592,54596,54604,54605,54607,54609,54616,54617,54620,54624,54629,54632,54633,54635,54637,54644,54645,54648,54652,54660,54661,54663,54664,54665,54672,54693,54728,54729,54732,54736,54738,54744,54745,54747,54749,54756,54757,54760,54764,54772,54773,54775,54777,54784,54785,54788,54792,54800,54801,54803,54804,54805,54812,54816,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,54820,54829,54840,54841,54844,54848,54853,54856,54857,54859,54861,54865,54868,54869,54872,54876,54887,54889,54896,54897,54900,54915,54917,54924,54925,54928,54932,54941,54943,54945,54952,54956,54960,54969,54971,54980,54981,54984,54988,54993,54996,54999,55001,55008,55012,55016,55024,55029,55036,55037,55040,55044,55057,55064,55065,55068,55072,55080,55081,55083,55085,55092,55093,55096,55100,55108,55111,55113,55120,55121,55124,55126,55127,55128,55129,55136,55137,55139,55141,55145,55148,55152,55156,55164,55165,55169,55176,55177,55180,55184,55192,55193,55195,55197,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,20285,20339,20551,20729,21152,21487,21621,21733,22025,23233,23478,26247,26550,26551,26607,27468,29634,30146,31292,33499,33540,34903,34952,35382,36040,36303,36603,36838,39381,21051,21364,21508,24682,24932,27580,29647,33050,35258,35282,38307,20355,21002,22718,22904,23014,24178,24185,25031,25536,26438,26604,26751,28567,30286,30475,30965,31240,31487,31777,32925,33390,33393,35563,38291,20075,21917,26359,28212,30883,31469,33883,35088,34638,38824,21208,22350,22570,23884,24863,25022,25121,25954,26577,27204,28187,29976,30131,30435,30640,32058,37039,37969,37970,40853,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,21283,23724,30002,32987,37440,38296,21083,22536,23004,23713,23831,24247,24378,24394,24951,27743,30074,30086,31968,32115,32177,32652,33108,33313,34193,35137,35611,37628,38477,40007,20171,20215,20491,20977,22607,24887,24894,24936,25913,27114,28433,30117,30342,30422,31623,33445,33995,63744,37799,38283,21888,23458,22353,63745,31923,32697,37301,20520,21435,23621,24040,25298,25454,25818,25831,28192,28844,31067,36317,36382,63746,36989,37445,37624,20094,20214,20581,24062,24314,24838,26967,33137,34388,36423,37749,39467,20062,20625,26480,26688,20745,21133,21138,27298,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,30652,37392,40660,21163,24623,36850,20552,25001,25581,25802,26684,27268,28608,33160,35233,38548,22533,29309,29356,29956,32121,32365,32937,35211,35700,36963,40273,25225,27770,28500,32080,32570,35363,20860,24906,31645,35609,37463,37772,20140,20435,20510,20670,20742,21185,21197,21375,22384,22659,24218,24465,24950,25004,25806,25964,26223,26299,26356,26775,28039,28805,28913,29855,29861,29898,30169,30828,30956,31455,31478,32069,32147,32789,32831,33051,33686,35686,36629,36885,37857,38915,38968,39514,39912,20418,21843,22586,22865,23395,23622,24760,25106,26690,26800,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,26856,28330,30028,30328,30926,31293,31995,32363,32380,35336,35489,35903,38542,40388,21476,21481,21578,21617,22266,22993,23396,23611,24235,25335,25911,25925,25970,26272,26543,27073,27837,30204,30352,30590,31295,32660,32771,32929,33167,33510,33533,33776,34241,34865,34996,35493,63747,36764,37678,38599,39015,39640,40723,21741,26011,26354,26767,31296,35895,40288,22256,22372,23825,26118,26801,26829,28414,29736,34974,39908,27752,63748,39592,20379,20844,20849,21151,23380,24037,24656,24685,25329,25511,25915,29657,31354,34467,36002,38799,20018,23521,25096,26524,29916,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,31185,33747,35463,35506,36328,36942,37707,38982,24275,27112,34303,37101,63749,20896,23448,23532,24931,26874,27454,28748,29743,29912,31649,32592,33733,35264,36011,38364,39208,21038,24669,25324,36866,20362,20809,21281,22745,24291,26336,27960,28826,29378,29654,31568,33009,37979,21350,25499,32619,20054,20608,22602,22750,24618,24871,25296,27088,39745,23439,32024,32945,36703,20132,20689,21676,21932,23308,23968,24039,25898,25934,26657,27211,29409,30350,30703,32094,32761,33184,34126,34527,36611,36686,37066,39171,39509,39851,19992,20037,20061,20167,20465,20855,21246,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,21312,21475,21477,21646,22036,22389,22434,23495,23943,24272,25084,25304,25937,26552,26601,27083,27472,27590,27628,27714,28317,28792,29399,29590,29699,30655,30697,31350,32127,32777,33276,33285,33290,33503,34914,35635,36092,36544,36881,37041,37476,37558,39378,39493,40169,40407,40860,22283,23616,33738,38816,38827,40628,21531,31384,32676,35033,36557,37089,22528,23624,25496,31391,23470,24339,31353,31406,33422,36524,20518,21048,21240,21367,22280,25331,25458,27402,28099,30519,21413,29527,34152,36470,38357,26426,27331,28528,35437,36556,39243,63750,26231,27512,36020,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,39740,63751,21483,22317,22862,25542,27131,29674,30789,31418,31429,31998,33909,35215,36211,36917,38312,21243,22343,30023,31584,33740,37406,63752,27224,20811,21067,21127,25119,26840,26997,38553,20677,21156,21220,25027,26020,26681,27135,29822,31563,33465,33771,35250,35641,36817,39241,63753,20170,22935,25810,26129,27278,29748,31105,31165,33449,34942,34943,35167,63754,37670,20235,21450,24613,25201,27762,32026,32102,20120,20834,30684,32943,20225,20238,20854,20864,21980,22120,22331,22522,22524,22804,22855,22931,23492,23696,23822,24049,24190,24524,25216,26071,26083,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,26398,26399,26462,26827,26820,27231,27450,27683,27773,27778,28103,29592,29734,29738,29826,29859,30072,30079,30849,30959,31041,31047,31048,31098,31637,32000,32186,32648,32774,32813,32908,35352,35663,35912,36215,37665,37668,39138,39249,39438,39439,39525,40594,32202,20342,21513,25326,26708,37329,21931,20794,63755,63756,23068,25062,63757,25295,25343,63758,63759,63760,63761,63762,63763,37027,63764,63765,63766,63767,63768,35582,63769,63770,63771,63772,26262,63773,29014,63774,63775,38627,63776,25423,25466,21335,63777,26511,26976,28275,63778,30007,63779,63780,63781,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,32013,63782,63783,34930,22218,23064,63784,63785,63786,63787,63788,20035,63789,20839,22856,26608,32784,63790,22899,24180,25754,31178,24565,24684,25288,25467,23527,23511,21162,63791,22900,24361,24594,63792,63793,63794,29785,63795,63796,63797,63798,63799,63800,39377,63801,63802,63803,63804,63805,63806,63807,63808,63809,63810,63811,28611,63812,63813,33215,36786,24817,63814,63815,33126,63816,63817,23615,63818,63819,63820,63821,63822,63823,63824,63825,23273,35365,26491,32016,63826,63827,63828,63829,63830,63831,33021,63832,63833,23612,27877,21311,28346,22810,33590,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,20025,20150,20294,21934,22296,22727,24406,26039,26086,27264,27573,28237,30701,31471,31774,32222,34507,34962,37170,37723,25787,28606,29562,30136,36948,21846,22349,25018,25812,26311,28129,28251,28525,28601,30192,32835,33213,34113,35203,35527,35674,37663,27795,30035,31572,36367,36957,21776,22530,22616,24162,25095,25758,26848,30070,31958,34739,40680,20195,22408,22382,22823,23565,23729,24118,24453,25140,25825,29619,33274,34955,36024,38538,40667,23429,24503,24755,20498,20992,21040,22294,22581,22615,23566,23648,23798,23947,24230,24466,24764,25361,25481,25623,26691,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,26873,27330,28120,28193,28372,28644,29182,30428,30585,31153,31291,33796,35241,36077,36339,36424,36867,36884,36947,37117,37709,38518,38876,27602,28678,29272,29346,29544,30563,31167,31716,32411,35712,22697,24775,25958,26109,26302,27788,28958,29129,35930,38931,20077,31361,20189,20908,20941,21205,21516,24999,26481,26704,26847,27934,28540,30140,30643,31461,33012,33891,37509,20828,26007,26460,26515,30168,31431,33651,63834,35910,36887,38957,23663,33216,33434,36929,36975,37389,24471,23965,27225,29128,30331,31561,34276,35588,37159,39472,21895,25078,63835,30313,32645,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,34367,34746,35064,37007,63836,27931,28889,29662,32097,33853,63837,37226,39409,63838,20098,21365,27396,27410,28734,29211,34349,40478,21068,36771,23888,25829,25900,27414,28651,31811,32412,34253,35172,35261,25289,33240,34847,24266,26391,28010,29436,29701,29807,34690,37086,20358,23821,24480,33802,20919,25504,30053,20142,20486,20841,20937,26753,27153,31918,31921,31975,33391,35538,36635,37327,20406,20791,21237,21570,24300,24942,25150,26053,27354,28670,31018,34268,34851,38317,39522,39530,40599,40654,21147,26310,27511,28701,31019,36706,38722,24976,25088,25891,28451,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,29001,29833,32244,32879,34030,36646,36899,37706,20925,21015,21155,27916,28872,35010,24265,25986,27566,28610,31806,29557,20196,20278,22265,63839,23738,23994,24604,29618,31533,32666,32718,32838,36894,37428,38646,38728,38936,40801,20363,28583,31150,37300,38583,21214,63840,25736,25796,27347,28510,28696,29200,30439,32769,34310,34396,36335,36613,38706,39791,40442,40565,30860,31103,32160,33737,37636,40575,40595,35542,22751,24324,26407,28711,29903,31840,32894,20769,28712,29282,30922,36034,36058,36084,38647,20102,20698,23534,24278,26009,29134,30274,30637,32842,34044,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,36988,39719,40845,22744,23105,23650,27155,28122,28431,30267,32047,32311,34078,35128,37860,38475,21129,26066,26611,27060,27969,28316,28687,29705,29792,30041,30244,30827,35628,39006,20845,25134,38520,20374,20523,23833,28138,32184,36650,24459,24900,26647,63841,38534,21202,32907,20956,20940,26974,31260,32190,33777,38517,20442,21033,21400,21519,21774,23653,24743,26446,26792,28012,29313,29432,29702,29827,63842,30178,31852,32633,32696,33673,35023,35041,37324,37328,38626,39881,21533,28542,29136,29848,34298,36522,38563,40023,40607,26519,28107,29747,33256,38678,30764,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,31435,31520,31890,25705,29802,30194,30908,30952,39340,39764,40635,23518,24149,28448,33180,33707,37000,19975,21325,23081,24018,24398,24930,25405,26217,26364,28415,28459,28771,30622,33836,34067,34875,36627,39237,39995,21788,25273,26411,27819,33545,35178,38778,20129,22916,24536,24537,26395,32178,32596,33426,33579,33725,36638,37017,22475,22969,23186,23504,26151,26522,26757,27599,29028,32629,36023,36067,36993,39749,33032,35978,38476,39488,40613,23391,27667,29467,30450,30431,33804,20906,35219,20813,20885,21193,26825,27796,30468,30496,32191,32236,38754,40629,28357,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,34065,20901,21517,21629,26126,26269,26919,28319,30399,30609,33559,33986,34719,37225,37528,40180,34946,20398,20882,21215,22982,24125,24917,25720,25721,26286,26576,27169,27597,27611,29279,29281,29761,30520,30683,32791,33468,33541,35584,35624,35980,26408,27792,29287,30446,30566,31302,40361,27519,27794,22818,26406,33945,21359,22675,22937,24287,25551,26164,26483,28218,29483,31447,33495,37672,21209,24043,25006,25035,25098,25287,25771,26080,26969,27494,27595,28961,29687,30045,32326,33310,33538,34154,35491,36031,38695,40289,22696,40664,20497,21006,21563,21839,25991,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,27766,32010,32011,32862,34442,38272,38639,21247,27797,29289,21619,23194,23614,23883,24396,24494,26410,26806,26979,28220,28228,30473,31859,32654,34183,35598,36855,38753,40692,23735,24758,24845,25003,25935,26107,26108,27665,27887,29599,29641,32225,38292,23494,34588,35600,21085,21338,25293,25615,25778,26420,27192,27850,29632,29854,31636,31893,32283,33162,33334,34180,36843,38649,39361,20276,21322,21453,21467,25292,25644,25856,26001,27075,27886,28504,29677,30036,30242,30436,30460,30928,30971,31020,32070,33324,34784,36820,38930,39151,21187,25300,25765,28196,28497,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,30332,36299,37297,37474,39662,39747,20515,20621,22346,22952,23592,24135,24439,25151,25918,26041,26049,26121,26507,27036,28354,30917,32033,32938,33152,33323,33459,33953,34444,35370,35607,37030,38450,40848,20493,20467,63843,22521,24472,25308,25490,26479,28227,28953,30403,32972,32986,35060,35061,35097,36064,36649,37197,38506,20271,20336,24091,26575,26658,30333,30334,39748,24161,27146,29033,29140,30058,63844,32321,34115,34281,39132,20240,31567,32624,38309,20961,24070,26805,27710,27726,27867,29359,31684,33539,27861,29754,20731,21128,22721,25816,27287,29863,30294,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,30887,34327,38370,38713,63845,21342,24321,35722,36776,36783,37002,21029,30629,40009,40712,19993,20482,20853,23643,24183,26142,26170,26564,26821,28851,29953,30149,31177,31453,36647,39200,39432,20445,22561,22577,23542,26222,27493,27921,28282,28541,29668,29995,33769,35036,35091,35676,36628,20239,20693,21264,21340,23443,24489,26381,31119,33145,33583,34068,35079,35206,36665,36667,39333,39954,26412,20086,20472,22857,23553,23791,23792,25447,26834,28925,29090,29739,32299,34028,34562,36898,37586,40179,19981,20184,20463,20613,21078,21103,21542,21648,22496,22827,23142,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,23386,23413,23500,24220,63846,25206,25975,26023,28014,28325,29238,31526,31807,32566,33104,33105,33178,33344,33433,33705,35331,36000,36070,36091,36212,36282,37096,37340,38428,38468,39385,40167,21271,20998,21545,22132,22707,22868,22894,24575,24996,25198,26128,27774,28954,30406,31881,31966,32027,33452,36033,38640,63847,20315,24343,24447,25282,23849,26379,26842,30844,32323,40300,19989,20633,21269,21290,21329,22915,23138,24199,24754,24970,25161,25209,26000,26503,27047,27604,27606,27607,27608,27832,63848,29749,30202,30738,30865,31189,31192,31875,32203,32737,32933,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,33086,33218,33778,34586,35048,35513,35692,36027,37145,38750,39131,40763,22188,23338,24428,25996,27315,27567,27996,28657,28693,29277,29613,36007,36051,38971,24977,27703,32856,39425,20045,20107,20123,20181,20282,20284,20351,20447,20735,21490,21496,21766,21987,22235,22763,22882,23057,23531,23546,23556,24051,24107,24473,24605,25448,26012,26031,26614,26619,26797,27515,27801,27863,28195,28681,29509,30722,31038,31040,31072,31169,31721,32023,32114,32902,33293,33678,34001,34503,35039,35408,35422,35613,36060,36198,36781,37034,39164,39391,40605,21066,63849,26388,63850,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,20632,21034,23665,25955,27733,29642,29987,30109,31639,33948,37240,38704,20087,25746,27578,29022,34217,19977,63851,26441,26862,28183,33439,34072,34923,25591,28545,37394,39087,19978,20663,20687,20767,21830,21930,22039,23360,23577,23776,24120,24202,24224,24258,24819,26705,27233,28248,29245,29248,29376,30456,31077,31665,32724,35059,35316,35443,35937,36062,38684,22622,29885,36093,21959,63852,31329,32034,33394,29298,29983,29989,63853,31513,22661,22779,23996,24207,24246,24464,24661,25234,25471,25933,26257,26329,26360,26646,26866,29312,29790,31598,32110,32214,32626,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,32997,33298,34223,35199,35475,36893,37604,40653,40736,22805,22893,24109,24796,26132,26227,26512,27728,28101,28511,30707,30889,33990,37323,37675,20185,20682,20808,21892,23307,23459,25159,25982,26059,28210,29053,29697,29764,29831,29887,30316,31146,32218,32341,32680,33146,33203,33337,34330,34796,35445,36323,36984,37521,37925,39245,39854,21352,23633,26964,27844,27945,28203,33292,34203,35131,35373,35498,38634,40807,21089,26297,27570,32406,34814,36109,38275,38493,25885,28041,29166,63854,22478,22995,23468,24615,24826,25104,26143,26207,29481,29689,30427,30465,31596,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,32854,32882,33125,35488,37266,19990,21218,27506,27927,31237,31545,32048,63855,36016,21484,22063,22609,23477,23567,23569,24034,25152,25475,25620,26157,26803,27836,28040,28335,28703,28836,29138,29990,30095,30094,30233,31505,31712,31787,32032,32057,34092,34157,34311,35380,36877,36961,37045,37559,38902,39479,20439,23660,26463,28049,31903,32396,35606,36118,36895,23403,24061,25613,33984,36956,39137,29575,23435,24730,26494,28126,35359,35494,36865,38924,21047,63856,28753,30862,37782,34928,37335,20462,21463,22013,22234,22402,22781,23234,23432,23723,23744,24101,24833,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,25101,25163,25480,25628,25910,25976,27193,27530,27700,27929,28465,29159,29417,29560,29703,29874,30246,30561,31168,31319,31466,31929,32143,32172,32353,32670,33065,33585,33936,34010,34282,34966,35504,35728,36664,36930,36995,37228,37526,37561,38539,38567,38568,38614,38656,38920,39318,39635,39706,21460,22654,22809,23408,23487,28113,28506,29087,29729,29881,32901,33789,24033,24455,24490,24642,26092,26642,26991,27219,27529,27957,28147,29667,30462,30636,31565,32020,33059,33308,33600,34036,34147,35426,35524,37255,37662,38918,39348,25100,34899,36848,37477,23815,23847,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,23913,29791,33181,34664,28629,25342,32722,35126,35186,19998,20056,20711,21213,21319,25215,26119,32361,34821,38494,20365,21273,22070,22987,23204,23608,23630,23629,24066,24337,24643,26045,26159,26178,26558,26612,29468,30690,31034,32709,33940,33997,35222,35430,35433,35553,35925,35962,22516,23508,24335,24687,25325,26893,27542,28252,29060,31698,34645,35672,36606,39135,39166,20280,20353,20449,21627,23072,23480,24892,26032,26216,29180,30003,31070,32051,33102,33251,33688,34218,34254,34563,35338,36523,36763,63857,36805,22833,23460,23526,24713,23529,23563,24515,27777,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,63858,28145,28683,29978,33455,35574,20160,21313,63859,38617,27663,20126,20420,20818,21854,23077,23784,25105,29273,33469,33706,34558,34905,35357,38463,38597,39187,40201,40285,22538,23731,23997,24132,24801,24853,25569,27138,28197,37122,37716,38990,39952,40823,23433,23736,25353,26191,26696,30524,38593,38797,38996,39839,26017,35585,36555,38332,21813,23721,24022,24245,26263,30284,33780,38343,22739,25276,29390,40232,20208,22830,24591,26171,27523,31207,40230,21395,21696,22467,23830,24859,26326,28079,30861,33406,38552,38724,21380,25212,25494,28082,32266,33099,38989,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,27387,32588,40367,40474,20063,20539,20918,22812,24825,25590,26928,29242,32822,63860,37326,24369,63861,63862,32004,33509,33903,33979,34277,36493,63863,20335,63864,63865,22756,23363,24665,25562,25880,25965,26264,63866,26954,27171,27915,28673,29036,30162,30221,31155,31344,63867,32650,63868,35140,63869,35731,37312,38525,63870,39178,22276,24481,26044,28417,30208,31142,35486,39341,39770,40812,20740,25014,25233,27277,33222,20547,22576,24422,28937,35328,35578,23420,34326,20474,20796,22196,22852,25513,28153,23978,26989,20870,20104,20313,63871,63872,63873,22914,63874,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,63875,27487,27741,63876,29877,30998,63877,33287,33349,33593,36671,36701,63878,39192,63879,63880,63881,20134,63882,22495,24441,26131,63883,63884,30123,32377,35695,63885,36870,39515,22181,22567,23032,23071,23476,63886,24310,63887,63888,25424,25403,63889,26941,27783,27839,28046,28051,28149,28436,63890,28895,28982,29017,63891,29123,29141,63892,30799,30831,63893,31605,32227,63894,32303,63895,34893,36575,63896,63897,63898,37467,63899,40182,63900,63901,63902,24709,28037,63903,29105,63904,63905,38321,21421,63906,63907,63908,26579,63909,28814,28976,29744,33398,33490,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,63910,38331,39653,40573,26308,63911,29121,33865,63912,63913,22603,63914,63915,23992,24433,63916,26144,26254,27001,27054,27704,27891,28214,28481,28634,28699,28719,29008,29151,29552,63917,29787,63918,29908,30408,31310,32403,63919,63920,33521,35424,36814,63921,37704,63922,38681,63923,63924,20034,20522,63925,21000,21473,26355,27757,28618,29450,30591,31330,33454,34269,34306,63926,35028,35427,35709,35947,63927,37555,63928,38675,38928,20116,20237,20425,20658,21320,21566,21555,21978,22626,22714,22887,23067,23524,24735,63929,25034,25942,26111,26212,26791,27738,28595,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,28879,29100,29522,31613,34568,35492,39986,40711,23627,27779,29508,29577,37434,28331,29797,30239,31337,32277,34314,20800,22725,25793,29934,29973,30320,32705,37013,38605,39252,28198,29926,31401,31402,33253,34521,34680,35355,23113,23436,23451,26785,26880,28003,29609,29715,29740,30871,32233,32747,33048,33109,33694,35916,38446,38929,26352,24448,26106,26505,27754,29579,20525,23043,27498,30702,22806,23916,24013,29477,30031,63930,63931,20709,20985,22575,22829,22934,23002,23525,63932,63933,23970,25303,25622,25747,25854,63934,26332,63935,27208,63936,29183,29796,63937,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,31368,31407,32327,32350,32768,33136,63938,34799,35201,35616,36953,63939,36992,39250,24958,27442,28020,32287,35109,36785,20433,20653,20887,21191,22471,22665,23481,24248,24898,27029,28044,28263,28342,29076,29794,29992,29996,32883,33592,33993,36362,37780,37854,63940,20110,20305,20598,20778,21448,21451,21491,23431,23507,23588,24858,24962,26100,29275,29591,29760,30402,31056,31121,31161,32006,32701,33419,34261,34398,36802,36935,37109,37354,38533,38632,38633,21206,24423,26093,26161,26671,29020,31286,37057,38922,20113,63941,27218,27550,28560,29065,32792,33464,34131,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,36939,38549,38642,38907,34074,39729,20112,29066,38596,20803,21407,21729,22291,22290,22435,23195,23236,23491,24616,24895,25588,27781,27961,28274,28304,29232,29503,29783,33489,34945,36677,36960,63942,38498,39000,40219,26376,36234,37470,20301,20553,20702,21361,22285,22996,23041,23561,24944,26256,28205,29234,29771,32239,32963,33806,33894,34111,34655,34907,35096,35586,36949,38859,39759,20083,20369,20754,20842,63943,21807,21929,23418,23461,24188,24189,24254,24736,24799,24840,24841,25540,25912,26377,63944,26580,26586,63945,26977,26978,27833,27943,63946,28216,63947,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,28641,29494,29495,63948,29788,30001,63949,30290,63950,63951,32173,33278,33848,35029,35480,35547,35565,36400,36418,36938,36926,36986,37193,37321,37742,63952,63953,22537,63954,27603,32905,32946,63955,63956,20801,22891,23609,63957,63958,28516,29607,32996,36103,63959,37399,38287,63960,63961,63962,63963,32895,25102,28700,32104,34701,63964,22432,24681,24903,27575,35518,37504,38577,20057,21535,28139,34093,38512,38899,39150,25558,27875,37009,20957,25033,33210,40441,20381,20506,20736,23452,24847,25087,25836,26885,27589,30097,30691,32681,33380,34191,34811,34915,35516,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,35696,37291,20108,20197,20234,63965,63966,22839,23016,63967,24050,24347,24411,24609,63968,63969,63970,63971,29246,29669,63972,30064,30157,63973,31227,63974,32780,32819,32900,33505,33617,63975,63976,36029,36019,36999,63977,63978,39156,39180,63979,63980,28727,30410,32714,32716,32764,35610,20154,20161,20995,21360,63981,21693,22240,23035,23493,24341,24525,28270,63982,63983,32106,33589,63984,34451,35469,63985,38765,38775,63986,63987,19968,20314,20350,22777,26085,28322,36920,37808,39353,20219,22764,22922,23001,24641,63988,63989,31252,63990,33615,36035,20837,21316,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,63991,63992,63993,20173,21097,23381,33471,20180,21050,21672,22985,23039,23376,23383,23388,24675,24904,28363,28825,29038,29574,29943,30133,30913,32043,32773,33258,33576,34071,34249,35566,36039,38604,20316,21242,22204,26027,26152,28796,28856,29237,32189,33421,37196,38592,40306,23409,26855,27544,28538,30430,23697,26283,28507,31668,31786,34870,38620,19976,20183,21280,22580,22715,22767,22892,23559,24115,24196,24373,25484,26290,26454,27167,27299,27404,28479,29254,63994,29520,29835,31456,31911,33144,33247,33255,33674,33900,34083,34196,34255,35037,36115,37292,38263,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,38556,20877,21705,22312,23472,25165,26448,26685,26771,28221,28371,28797,32289,35009,36001,36617,40779,40782,29229,31631,35533,37658,20295,20302,20786,21632,22992,24213,25269,26485,26990,27159,27822,28186,29401,29482,30141,31672,32053,33511,33785,33879,34295,35419,36015,36487,36889,37048,38606,40799,21219,21514,23265,23490,25688,25973,28404,29380,63995,30340,31309,31515,31821,32318,32735,33659,35627,36042,36196,36321,36447,36842,36857,36969,37841,20291,20346,20659,20840,20856,21069,21098,22625,22652,22880,23560,23637,24283,24731,25136,26643,27583,27656,28593,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,29006,29728,30000,30008,30033,30322,31564,31627,31661,31686,32399,35438,36670,36681,37439,37523,37666,37931,38651,39002,39019,39198,20999,25130,25240,27993,30308,31434,31680,32118,21344,23742,24215,28472,28857,31896,38673,39822,40670,25509,25722,34678,19969,20117,20141,20572,20597,21576,22979,23450,24128,24237,24311,24449,24773,25402,25919,25972,26060,26230,26232,26622,26984,27273,27491,27712,28096,28136,28191,28254,28702,28833,29582,29693,30010,30555,30855,31118,31243,31357,31934,32142,33351,35330,35562,35998,37165,37194,37336,37478,37580,37664,38662,38742,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,38748,38914,40718,21046,21137,21884,22564,24093,24351,24716,25552,26799,28639,31085,31532,33229,34234,35069,35576,36420,37261,38500,38555,38717,38988,40778,20430,20806,20939,21161,22066,24340,24427,25514,25805,26089,26177,26362,26361,26397,26781,26839,27133,28437,28526,29031,29157,29226,29866,30522,31062,31066,31199,31264,31381,31895,31967,32068,32368,32903,34299,34468,35412,35519,36249,36481,36896,36973,37347,38459,38613,40165,26063,31751,36275,37827,23384,23562,21330,25305,29469,20519,23447,24478,24752,24939,26837,28121,29742,31278,32066,32156,32305,33131,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,36394,36405,37758,37912,20304,22352,24038,24231,25387,32618,20027,20303,20367,20570,23005,32964,21610,21608,22014,22863,23449,24030,24282,26205,26417,26609,26666,27880,27954,28234,28557,28855,29664,30087,31820,32002,32044,32162,33311,34523,35387,35461,36208,36490,36659,36913,37198,37202,37956,39376,31481,31909,20426,20737,20934,22472,23535,23803,26201,27197,27994,28310,28652,28940,30063,31459,34850,36897,36981,38603,39423,33537,20013,20210,34886,37325,21373,27355,26987,27713,33914,22686,24974,26366,25327,28893,29969,30151,32338,33976,35657,36104,20043,21482,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,21675,22320,22336,24535,25345,25351,25711,25903,26088,26234,26525,26547,27490,27744,27802,28460,30693,30757,31049,31063,32025,32930,33026,33267,33437,33463,34584,35468,63996,36100,36286,36978,30452,31257,31287,32340,32887,21767,21972,22645,25391,25634,26185,26187,26733,27035,27524,27941,28337,29645,29800,29857,30043,30137,30433,30494,30603,31206,32265,32285,33275,34095,34967,35386,36049,36587,36784,36914,37805,38499,38515,38663,20356,21489,23018,23241,24089,26702,29894,30142,31209,31378,33187,34541,36074,36300,36845,26015,26389,63997,22519,28503,32221,36655,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,37878,38598,24501,25074,28548,19988,20376,20511,21449,21983,23919,24046,27425,27492,30923,31642,63998,36425,36554,36974,25417,25662,30528,31364,37679,38015,40810,25776,28591,29158,29864,29914,31428,31762,32386,31922,32408,35738,36106,38013,39184,39244,21049,23519,25830,26413,32046,20717,21443,22649,24920,24921,25082,26028,31449,35730,35734,20489,20513,21109,21809,23100,24288,24432,24884,25950,26124,26166,26274,27085,28356,28466,29462,30241,31379,33081,33369,33750,33980,20661,22512,23488,23528,24425,25505,30758,32181,33756,34081,37319,37365,20874,26613,31574,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,36012,20932,22971,24765,34389,20508,63999,21076,23610,24957,25114,25299,25842,26021,28364,30240,33034,36448,38495,38587,20191,21315,21912,22825,24029,25797,27849,28154,29588,31359,33307,34214,36068,36368,36983,37351,38369,38433,38854,20984,21746,21894,24505,25764,28552,32180,36639,36685,37941,20681,23574,27838,28155,29979,30651,31805,31844,35449,35522,22558,22974,24086,25463,29266,30090,30571,35548,36028,36626,24307,26228,28152,32893,33729,35531,38737,39894,64000,21059,26367,28053,28399,32224,35558,36910,36958,39636,21021,21119,21736,24980,25220,25307,26786,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,26898,26970,27189,28818,28966,30813,30977,30990,31186,31245,32918,33400,33493,33609,34121,35970,36229,37218,37259,37294,20419,22225,29165,30679,34560,35320,23544,24534,26449,37032,21474,22618,23541,24740,24961,25696,32317,32880,34085,37507,25774,20652,23828,26368,22684,25277,25512,26894,27000,27166,28267,30394,31179,33467,33833,35535,36264,36861,37138,37195,37276,37648,37656,37786,38619,39478,39949,19985,30044,31069,31482,31569,31689,32302,33988,36441,36468,36600,36880,26149,26943,29763,20986,26414,40668,20805,24544,27798,34802,34909,34935,24756,33205,33795,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,36101,21462,21561,22068,23094,23601,28810,32736,32858,33030,33261,36259,37257,39519,40434,20596,20164,21408,24827,28204,23652,20360,20516,21988,23769,24159,24677,26772,27835,28100,29118,30164,30196,30305,31258,31305,32199,32251,32622,33268,34473,36636,38601,39347,40786,21063,21189,39149,35242,19971,26578,28422,20405,23522,26517,27784,28024,29723,30759,37341,37756,34756,31204,31281,24555,20182,21668,21822,22702,22949,24816,25171,25302,26422,26965,33333,38464,39345,39389,20524,21331,21828,22396,64001,25176,64002,25826,26219,26589,28609,28655,29730,29752,35351,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,37944,21585,22022,22374,24392,24986,27470,28760,28845,32187,35477,22890,33067,25506,30472,32829,36010,22612,25645,27067,23445,24081,28271,64003,34153,20812,21488,22826,24608,24907,27526,27760,27888,31518,32974,33492,36294,37040,39089,64004,25799,28580,25745,25860,20814,21520,22303,35342,24927,26742,64005,30171,31570,32113,36890,22534,27084,33151,35114,36864,38969,20600,22871,22956,25237,36879,39722,24925,29305,38358,22369,23110,24052,25226,25773,25850,26487,27874,27966,29228,29750,30772,32631,33453,36315,38935,21028,22338,26495,29256,29923,36009,36774,37393,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,38442,20843,21485,25420,20329,21764,24726,25943,27803,28031,29260,29437,31255,35207,35997,24429,28558,28921,33192,24846,20415,20559,25153,29255,31687,32232,32745,36941,38829,39449,36022,22378,24179,26544,33805,35413,21536,23318,24163,24290,24330,25987,32954,34109,38281,38491,20296,21253,21261,21263,21638,21754,22275,24067,24598,25243,25265,25429,64006,27873,28006,30129,30770,32990,33071,33502,33889,33970,34957,35090,36875,37610,39165,39825,24133,26292,26333,28689,29190,64007,20469,21117,24426,24915,26451,27161,28418,29922,31080,34920,35961,39111,39108,39491,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,21697,31263,26963,35575,35914,39080,39342,24444,25259,30130,30382,34987,36991,38466,21305,24380,24517,27852,29644,30050,30091,31558,33534,39325,20047,36924,19979,20309,21414,22799,24264,26160,27827,29781,33655,34662,36032,36944,38686,39957,22737,23416,34384,35604,40372,23506,24680,24717,26097,27735,28450,28579,28698,32597,32752,38289,38290,38480,38867,21106,36676,20989,21547,21688,21859,21898,27323,28085,32216,33382,37532,38519,40569,21512,21704,30418,34532,38308,38356,38492,20130,20233,23022,23270,24055,24658,25239,26477,26689,27782,28207,32568,32923,33322,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,64008,64009,38917,20133,20565,21683,22419,22874,23401,23475,25032,26999,28023,28707,34809,35299,35442,35559,36994,39405,39608,21182,26680,20502,24184,26447,33607,34892,20139,21521,22190,29670,37141,38911,39177,39255,39321,22099,22687,34395,35377,25010,27382,29563,36562,27463,38570,39511,22869,29184,36203,38761,20436,23796,24358,25080,26203,27883,28843,29572,29625,29694,30505,30541,32067,32098,32291,33335,34898,64010,36066,37449,39023,23377,31348,34880,38913,23244,20448,21332,22846,23805,25406,28025,29433,33029,33031,33698,37583,38960,20136,20804,21009,22411,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,24418,27842,28366,28677,28752,28847,29074,29673,29801,33610,34722,34913,36872,37026,37795,39336,20846,24407,24800,24935,26291,34137,36426,37295,38795,20046,20114,21628,22741,22778,22909,23733,24359,25142,25160,26122,26215,27627,28009,28111,28246,28408,28564,28640,28649,28765,29392,29733,29786,29920,30355,31068,31946,32286,32993,33446,33899,33983,34382,34399,34676,35703,35946,37804,38912,39013,24785,25110,37239,23130,26127,28151,28222,29759,39746,24573,24794,31503,21700,24344,27742,27859,27946,28888,32005,34425,35340,40251,21270,21644,23301,27194,28779,30069,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,31117,31166,33457,33775,35441,35649,36008,38772,64011,25844,25899,30906,30907,31339,20024,21914,22864,23462,24187,24739,25563,27489,26213,26707,28185,29029,29872,32008,36996,39529,39973,27963,28369,29502,35905,38346,20976,24140,24488,24653,24822,24880,24908,26179,26180,27045,27841,28255,28361,28514,29004,29852,30343,31681,31783,33618,34647,36945,38541,40643,21295,22238,24315,24458,24674,24724,25079,26214,26371,27292,28142,28590,28784,29546,32362,33214,33588,34516,35496,36036,21123,29554,23446,27243,37892,21742,22150,23389,25928,25989,26313,26783,28045,28102,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,29243,32948,37237,39501,20399,20505,21402,21518,21564,21897,21957,24127,24460,26429,29030,29661,36869,21211,21235,22628,22734,28932,29071,29179,34224,35347,26248,34216,21927,26244,29002,33841,21321,21913,27585,24409,24509,25582,26249,28999,35569,36637,40638,20241,25658,28875,30054,34407,24676,35662,40440,20807,20982,21256,27958,33016,40657,26133,27427,28824,30165,21507,23673,32007,35350,27424,27453,27462,21560,24688,27965,32725,33288,20694,20958,21916,22123,22221,23020,23305,24076,24985,24984,25137,26206,26342,29081,29113,29114,29351,31143,31232,32690,35440,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],
+ "gb18030":[19970,19972,19973,19974,19983,19986,19991,19999,20000,20001,20003,20006,20009,20014,20015,20017,20019,20021,20023,20028,20032,20033,20034,20036,20038,20042,20049,20053,20055,20058,20059,20066,20067,20068,20069,20071,20072,20074,20075,20076,20077,20078,20079,20082,20084,20085,20086,20087,20088,20089,20090,20091,20092,20093,20095,20096,20097,20098,20099,20100,20101,20103,20106,20112,20118,20119,20121,20124,20125,20126,20131,20138,20143,20144,20145,20148,20150,20151,20152,20153,20156,20157,20158,20168,20172,20175,20176,20178,20186,20187,20188,20192,20194,20198,20199,20201,20205,20206,20207,20209,20212,20216,20217,20218,20220,20222,20224,20226,20227,20228,20229,20230,20231,20232,20235,20236,20242,20243,20244,20245,20246,20252,20253,20257,20259,20264,20265,20268,20269,20270,20273,20275,20277,20279,20281,20283,20286,20287,20288,20289,20290,20292,20293,20295,20296,20297,20298,20299,20300,20306,20308,20310,20321,20322,20326,20328,20330,20331,20333,20334,20337,20338,20341,20343,20344,20345,20346,20349,20352,20353,20354,20357,20358,20359,20362,20364,20366,20368,20370,20371,20373,20374,20376,20377,20378,20380,20382,20383,20385,20386,20388,20395,20397,20400,20401,20402,20403,20404,20406,20407,20408,20409,20410,20411,20412,20413,20414,20416,20417,20418,20422,20423,20424,20425,20427,20428,20429,20434,20435,20436,20437,20438,20441,20443,20448,20450,20452,20453,20455,20459,20460,20464,20466,20468,20469,20470,20471,20473,20475,20476,20477,20479,20480,20481,20482,20483,20484,20485,20486,20487,20488,20489,20490,20491,20494,20496,20497,20499,20501,20502,20503,20507,20509,20510,20512,20514,20515,20516,20519,20523,20527,20528,20529,20530,20531,20532,20533,20534,20535,20536,20537,20539,20541,20543,20544,20545,20546,20548,20549,20550,20553,20554,20555,20557,20560,20561,20562,20563,20564,20566,20567,20568,20569,20571,20573,20574,20575,20576,20577,20578,20579,20580,20582,20583,20584,20585,20586,20587,20589,20590,20591,20592,20593,20594,20595,20596,20597,20600,20601,20602,20604,20605,20609,20610,20611,20612,20614,20615,20617,20618,20619,20620,20622,20623,20624,20625,20626,20627,20628,20629,20630,20631,20632,20633,20634,20635,20636,20637,20638,20639,20640,20641,20642,20644,20646,20650,20651,20653,20654,20655,20656,20657,20659,20660,20661,20662,20663,20664,20665,20668,20669,20670,20671,20672,20673,20674,20675,20676,20677,20678,20679,20680,20681,20682,20683,20684,20685,20686,20688,20689,20690,20691,20692,20693,20695,20696,20697,20699,20700,20701,20702,20703,20704,20705,20706,20707,20708,20709,20712,20713,20714,20715,20719,20720,20721,20722,20724,20726,20727,20728,20729,20730,20732,20733,20734,20735,20736,20737,20738,20739,20740,20741,20744,20745,20746,20748,20749,20750,20751,20752,20753,20755,20756,20757,20758,20759,20760,20761,20762,20763,20764,20765,20766,20767,20768,20770,20771,20772,20773,20774,20775,20776,20777,20778,20779,20780,20781,20782,20783,20784,20785,20786,20787,20788,20789,20790,20791,20792,20793,20794,20795,20796,20797,20798,20802,20807,20810,20812,20814,20815,20816,20818,20819,20823,20824,20825,20827,20829,20830,20831,20832,20833,20835,20836,20838,20839,20841,20842,20847,20850,20858,20862,20863,20867,20868,20870,20871,20874,20875,20878,20879,20880,20881,20883,20884,20888,20890,20893,20894,20895,20897,20899,20902,20903,20904,20905,20906,20909,20910,20916,20920,20921,20922,20926,20927,20929,20930,20931,20933,20936,20938,20941,20942,20944,20946,20947,20948,20949,20950,20951,20952,20953,20954,20956,20958,20959,20962,20963,20965,20966,20967,20968,20969,20970,20972,20974,20977,20978,20980,20983,20990,20996,20997,21001,21003,21004,21007,21008,21011,21012,21013,21020,21022,21023,21025,21026,21027,21029,21030,21031,21034,21036,21039,21041,21042,21044,21045,21052,21054,21060,21061,21062,21063,21064,21065,21067,21070,21071,21074,21075,21077,21079,21080,21081,21082,21083,21085,21087,21088,21090,21091,21092,21094,21096,21099,21100,21101,21102,21104,21105,21107,21108,21109,21110,21111,21112,21113,21114,21115,21116,21118,21120,21123,21124,21125,21126,21127,21129,21130,21131,21132,21133,21134,21135,21137,21138,21140,21141,21142,21143,21144,21145,21146,21148,21156,21157,21158,21159,21166,21167,21168,21172,21173,21174,21175,21176,21177,21178,21179,21180,21181,21184,21185,21186,21188,21189,21190,21192,21194,21196,21197,21198,21199,21201,21203,21204,21205,21207,21209,21210,21211,21212,21213,21214,21216,21217,21218,21219,21221,21222,21223,21224,21225,21226,21227,21228,21229,21230,21231,21233,21234,21235,21236,21237,21238,21239,21240,21243,21244,21245,21249,21250,21251,21252,21255,21257,21258,21259,21260,21262,21265,21266,21267,21268,21272,21275,21276,21278,21279,21282,21284,21285,21287,21288,21289,21291,21292,21293,21295,21296,21297,21298,21299,21300,21301,21302,21303,21304,21308,21309,21312,21314,21316,21318,21323,21324,21325,21328,21332,21336,21337,21339,21341,21349,21352,21354,21356,21357,21362,21366,21369,21371,21372,21373,21374,21376,21377,21379,21383,21384,21386,21390,21391,21392,21393,21394,21395,21396,21398,21399,21401,21403,21404,21406,21408,21409,21412,21415,21418,21419,21420,21421,21423,21424,21425,21426,21427,21428,21429,21431,21432,21433,21434,21436,21437,21438,21440,21443,21444,21445,21446,21447,21454,21455,21456,21458,21459,21461,21466,21468,21469,21470,21473,21474,21479,21492,21498,21502,21503,21504,21506,21509,21511,21515,21524,21528,21529,21530,21532,21538,21540,21541,21546,21552,21555,21558,21559,21562,21565,21567,21569,21570,21572,21573,21575,21577,21580,21581,21582,21583,21585,21594,21597,21598,21599,21600,21601,21603,21605,21607,21609,21610,21611,21612,21613,21614,21615,21616,21620,21625,21626,21630,21631,21633,21635,21637,21639,21640,21641,21642,21645,21649,21651,21655,21656,21660,21662,21663,21664,21665,21666,21669,21678,21680,21682,21685,21686,21687,21689,21690,21692,21694,21699,21701,21706,21707,21718,21720,21723,21728,21729,21730,21731,21732,21739,21740,21743,21744,21745,21748,21749,21750,21751,21752,21753,21755,21758,21760,21762,21763,21764,21765,21768,21770,21771,21772,21773,21774,21778,21779,21781,21782,21783,21784,21785,21786,21788,21789,21790,21791,21793,21797,21798,21800,21801,21803,21805,21810,21812,21813,21814,21816,21817,21818,21819,21821,21824,21826,21829,21831,21832,21835,21836,21837,21838,21839,21841,21842,21843,21844,21847,21848,21849,21850,21851,21853,21854,21855,21856,21858,21859,21864,21865,21867,21871,21872,21873,21874,21875,21876,21881,21882,21885,21887,21893,21894,21900,21901,21902,21904,21906,21907,21909,21910,21911,21914,21915,21918,21920,21921,21922,21923,21924,21925,21926,21928,21929,21930,21931,21932,21933,21934,21935,21936,21938,21940,21942,21944,21946,21948,21951,21952,21953,21954,21955,21958,21959,21960,21962,21963,21966,21967,21968,21973,21975,21976,21977,21978,21979,21982,21984,21986,21991,21993,21997,21998,22000,22001,22004,22006,22008,22009,22010,22011,22012,22015,22018,22019,22020,22021,22022,22023,22026,22027,22029,22032,22033,22034,22035,22036,22037,22038,22039,22041,22042,22044,22045,22048,22049,22050,22053,22054,22056,22057,22058,22059,22062,22063,22064,22067,22069,22071,22072,22074,22076,22077,22078,22080,22081,22082,22083,22084,22085,22086,22087,22088,22089,22090,22091,22095,22096,22097,22098,22099,22101,22102,22106,22107,22109,22110,22111,22112,22113,22115,22117,22118,22119,22125,22126,22127,22128,22130,22131,22132,22133,22135,22136,22137,22138,22141,22142,22143,22144,22145,22146,22147,22148,22151,22152,22153,22154,22155,22156,22157,22160,22161,22162,22164,22165,22166,22167,22168,22169,22170,22171,22172,22173,22174,22175,22176,22177,22178,22180,22181,22182,22183,22184,22185,22186,22187,22188,22189,22190,22192,22193,22194,22195,22196,22197,22198,22200,22201,22202,22203,22205,22206,22207,22208,22209,22210,22211,22212,22213,22214,22215,22216,22217,22219,22220,22221,22222,22223,22224,22225,22226,22227,22229,22230,22232,22233,22236,22243,22245,22246,22247,22248,22249,22250,22252,22254,22255,22258,22259,22262,22263,22264,22267,22268,22272,22273,22274,22277,22279,22283,22284,22285,22286,22287,22288,22289,22290,22291,22292,22293,22294,22295,22296,22297,22298,22299,22301,22302,22304,22305,22306,22308,22309,22310,22311,22315,22321,22322,22324,22325,22326,22327,22328,22332,22333,22335,22337,22339,22340,22341,22342,22344,22345,22347,22354,22355,22356,22357,22358,22360,22361,22370,22371,22373,22375,22380,22382,22384,22385,22386,22388,22389,22392,22393,22394,22397,22398,22399,22400,22401,22407,22408,22409,22410,22413,22414,22415,22416,22417,22420,22421,22422,22423,22424,22425,22426,22428,22429,22430,22431,22437,22440,22442,22444,22447,22448,22449,22451,22453,22454,22455,22457,22458,22459,22460,22461,22462,22463,22464,22465,22468,22469,22470,22471,22472,22473,22474,22476,22477,22480,22481,22483,22486,22487,22491,22492,22494,22497,22498,22499,22501,22502,22503,22504,22505,22506,22507,22508,22510,22512,22513,22514,22515,22517,22518,22519,22523,22524,22526,22527,22529,22531,22532,22533,22536,22537,22538,22540,22542,22543,22544,22546,22547,22548,22550,22551,22552,22554,22555,22556,22557,22559,22562,22563,22565,22566,22567,22568,22569,22571,22572,22573,22574,22575,22577,22578,22579,22580,22582,22583,22584,22585,22586,22587,22588,22589,22590,22591,22592,22593,22594,22595,22597,22598,22599,22600,22601,22602,22603,22606,22607,22608,22610,22611,22613,22614,22615,22617,22618,22619,22620,22621,22623,22624,22625,22626,22627,22628,22630,22631,22632,22633,22634,22637,22638,22639,22640,22641,22642,22643,22644,22645,22646,22647,22648,22649,22650,22651,22652,22653,22655,22658,22660,22662,22663,22664,22666,22667,22668,22669,22670,22671,22672,22673,22676,22677,22678,22679,22680,22683,22684,22685,22688,22689,22690,22691,22692,22693,22694,22695,22698,22699,22700,22701,22702,22703,22704,22705,22706,22707,22708,22709,22710,22711,22712,22713,22714,22715,22717,22718,22719,22720,22722,22723,22724,22726,22727,22728,22729,22730,22731,22732,22733,22734,22735,22736,22738,22739,22740,22742,22743,22744,22745,22746,22747,22748,22749,22750,22751,22752,22753,22754,22755,22757,22758,22759,22760,22761,22762,22765,22767,22769,22770,22772,22773,22775,22776,22778,22779,22780,22781,22782,22783,22784,22785,22787,22789,22790,22792,22793,22794,22795,22796,22798,22800,22801,22802,22803,22807,22808,22811,22813,22814,22816,22817,22818,22819,22822,22824,22828,22832,22834,22835,22837,22838,22843,22845,22846,22847,22848,22851,22853,22854,22858,22860,22861,22864,22866,22867,22873,22875,22876,22877,22878,22879,22881,22883,22884,22886,22887,22888,22889,22890,22891,22892,22893,22894,22895,22896,22897,22898,22901,22903,22906,22907,22908,22910,22911,22912,22917,22921,22923,22924,22926,22927,22928,22929,22932,22933,22936,22938,22939,22940,22941,22943,22944,22945,22946,22950,22951,22956,22957,22960,22961,22963,22964,22965,22966,22967,22968,22970,22972,22973,22975,22976,22977,22978,22979,22980,22981,22983,22984,22985,22988,22989,22990,22991,22997,22998,23001,23003,23006,23007,23008,23009,23010,23012,23014,23015,23017,23018,23019,23021,23022,23023,23024,23025,23026,23027,23028,23029,23030,23031,23032,23034,23036,23037,23038,23040,23042,23050,23051,23053,23054,23055,23056,23058,23060,23061,23062,23063,23065,23066,23067,23069,23070,23073,23074,23076,23078,23079,23080,23082,23083,23084,23085,23086,23087,23088,23091,23093,23095,23096,23097,23098,23099,23101,23102,23103,23105,23106,23107,23108,23109,23111,23112,23115,23116,23117,23118,23119,23120,23121,23122,23123,23124,23126,23127,23128,23129,23131,23132,23133,23134,23135,23136,23137,23139,23140,23141,23142,23144,23145,23147,23148,23149,23150,23151,23152,23153,23154,23155,23160,23161,23163,23164,23165,23166,23168,23169,23170,23171,23172,23173,23174,23175,23176,23177,23178,23179,23180,23181,23182,23183,23184,23185,23187,23188,23189,23190,23191,23192,23193,23196,23197,23198,23199,23200,23201,23202,23203,23204,23205,23206,23207,23208,23209,23211,23212,23213,23214,23215,23216,23217,23220,23222,23223,23225,23226,23227,23228,23229,23231,23232,23235,23236,23237,23238,23239,23240,23242,23243,23245,23246,23247,23248,23249,23251,23253,23255,23257,23258,23259,23261,23262,23263,23266,23268,23269,23271,23272,23274,23276,23277,23278,23279,23280,23282,23283,23284,23285,23286,23287,23288,23289,23290,23291,23292,23293,23294,23295,23296,23297,23298,23299,23300,23301,23302,23303,23304,23306,23307,23308,23309,23310,23311,23312,23313,23314,23315,23316,23317,23320,23321,23322,23323,23324,23325,23326,23327,23328,23329,23330,23331,23332,23333,23334,23335,23336,23337,23338,23339,23340,23341,23342,23343,23344,23345,23347,23349,23350,23352,23353,23354,23355,23356,23357,23358,23359,23361,23362,23363,23364,23365,23366,23367,23368,23369,23370,23371,23372,23373,23374,23375,23378,23382,23390,23392,23393,23399,23400,23403,23405,23406,23407,23410,23412,23414,23415,23416,23417,23419,23420,23422,23423,23426,23430,23434,23437,23438,23440,23441,23442,23444,23446,23455,23463,23464,23465,23468,23469,23470,23471,23473,23474,23479,23482,23483,23484,23488,23489,23491,23496,23497,23498,23499,23501,23502,23503,23505,23508,23509,23510,23511,23512,23513,23514,23515,23516,23520,23522,23523,23526,23527,23529,23530,23531,23532,23533,23535,23537,23538,23539,23540,23541,23542,23543,23549,23550,23552,23554,23555,23557,23559,23560,23563,23564,23565,23566,23568,23570,23571,23575,23577,23579,23582,23583,23584,23585,23587,23590,23592,23593,23594,23595,23597,23598,23599,23600,23602,23603,23605,23606,23607,23619,23620,23622,23623,23628,23629,23634,23635,23636,23638,23639,23640,23642,23643,23644,23645,23647,23650,23652,23655,23656,23657,23658,23659,23660,23661,23664,23666,23667,23668,23669,23670,23671,23672,23675,23676,23677,23678,23680,23683,23684,23685,23686,23687,23689,23690,23691,23694,23695,23698,23699,23701,23709,23710,23711,23712,23713,23716,23717,23718,23719,23720,23722,23726,23727,23728,23730,23732,23734,23737,23738,23739,23740,23742,23744,23746,23747,23749,23750,23751,23752,23753,23754,23756,23757,23758,23759,23760,23761,23763,23764,23765,23766,23767,23768,23770,23771,23772,23773,23774,23775,23776,23778,23779,23783,23785,23787,23788,23790,23791,23793,23794,23795,23796,23797,23798,23799,23800,23801,23802,23804,23805,23806,23807,23808,23809,23812,23813,23816,23817,23818,23819,23820,23821,23823,23824,23825,23826,23827,23829,23831,23832,23833,23834,23836,23837,23839,23840,23841,23842,23843,23845,23848,23850,23851,23852,23855,23856,23857,23858,23859,23861,23862,23863,23864,23865,23866,23867,23868,23871,23872,23873,23874,23875,23876,23877,23878,23880,23881,23885,23886,23887,23888,23889,23890,23891,23892,23893,23894,23895,23897,23898,23900,23902,23903,23904,23905,23906,23907,23908,23909,23910,23911,23912,23914,23917,23918,23920,23921,23922,23923,23925,23926,23927,23928,23929,23930,23931,23932,23933,23934,23935,23936,23937,23939,23940,23941,23942,23943,23944,23945,23946,23947,23948,23949,23950,23951,23952,23953,23954,23955,23956,23957,23958,23959,23960,23962,23963,23964,23966,23967,23968,23969,23970,23971,23972,23973,23974,23975,23976,23977,23978,23979,23980,23981,23982,23983,23984,23985,23986,23987,23988,23989,23990,23992,23993,23994,23995,23996,23997,23998,23999,24000,24001,24002,24003,24004,24006,24007,24008,24009,24010,24011,24012,24014,24015,24016,24017,24018,24019,24020,24021,24022,24023,24024,24025,24026,24028,24031,24032,24035,24036,24042,24044,24045,24048,24053,24054,24056,24057,24058,24059,24060,24063,24064,24068,24071,24073,24074,24075,24077,24078,24082,24083,24087,24094,24095,24096,24097,24098,24099,24100,24101,24104,24105,24106,24107,24108,24111,24112,24114,24115,24116,24117,24118,24121,24122,24126,24127,24128,24129,24131,24134,24135,24136,24137,24138,24139,24141,24142,24143,24144,24145,24146,24147,24150,24151,24152,24153,24154,24156,24157,24159,24160,24163,24164,24165,24166,24167,24168,24169,24170,24171,24172,24173,24174,24175,24176,24177,24181,24183,24185,24190,24193,24194,24195,24197,24200,24201,24204,24205,24206,24210,24216,24219,24221,24225,24226,24227,24228,24232,24233,24234,24235,24236,24238,24239,24240,24241,24242,24244,24250,24251,24252,24253,24255,24256,24257,24258,24259,24260,24261,24262,24263,24264,24267,24268,24269,24270,24271,24272,24276,24277,24279,24280,24281,24282,24284,24285,24286,24287,24288,24289,24290,24291,24292,24293,24294,24295,24297,24299,24300,24301,24302,24303,24304,24305,24306,24307,24309,24312,24313,24315,24316,24317,24325,24326,24327,24329,24332,24333,24334,24336,24338,24340,24342,24345,24346,24348,24349,24350,24353,24354,24355,24356,24360,24363,24364,24366,24368,24370,24371,24372,24373,24374,24375,24376,24379,24381,24382,24383,24385,24386,24387,24388,24389,24390,24391,24392,24393,24394,24395,24396,24397,24398,24399,24401,24404,24409,24410,24411,24412,24414,24415,24416,24419,24421,24423,24424,24427,24430,24431,24434,24436,24437,24438,24440,24442,24445,24446,24447,24451,24454,24461,24462,24463,24465,24467,24468,24470,24474,24475,24477,24478,24479,24480,24482,24483,24484,24485,24486,24487,24489,24491,24492,24495,24496,24497,24498,24499,24500,24502,24504,24505,24506,24507,24510,24511,24512,24513,24514,24519,24520,24522,24523,24526,24531,24532,24533,24538,24539,24540,24542,24543,24546,24547,24549,24550,24552,24553,24556,24559,24560,24562,24563,24564,24566,24567,24569,24570,24572,24583,24584,24585,24587,24588,24592,24593,24595,24599,24600,24602,24606,24607,24610,24611,24612,24620,24621,24622,24624,24625,24626,24627,24628,24630,24631,24632,24633,24634,24637,24638,24640,24644,24645,24646,24647,24648,24649,24650,24652,24654,24655,24657,24659,24660,24662,24663,24664,24667,24668,24670,24671,24672,24673,24677,24678,24686,24689,24690,24692,24693,24695,24702,24704,24705,24706,24709,24710,24711,24712,24714,24715,24718,24719,24720,24721,24723,24725,24727,24728,24729,24732,24734,24737,24738,24740,24741,24743,24745,24746,24750,24752,24755,24757,24758,24759,24761,24762,24765,24766,24767,24768,24769,24770,24771,24772,24775,24776,24777,24780,24781,24782,24783,24784,24786,24787,24788,24790,24791,24793,24795,24798,24801,24802,24803,24804,24805,24810,24817,24818,24821,24823,24824,24827,24828,24829,24830,24831,24834,24835,24836,24837,24839,24842,24843,24844,24848,24849,24850,24851,24852,24854,24855,24856,24857,24859,24860,24861,24862,24865,24866,24869,24872,24873,24874,24876,24877,24878,24879,24880,24881,24882,24883,24884,24885,24886,24887,24888,24889,24890,24891,24892,24893,24894,24896,24897,24898,24899,24900,24901,24902,24903,24905,24907,24909,24911,24912,24914,24915,24916,24918,24919,24920,24921,24922,24923,24924,24926,24927,24928,24929,24931,24932,24933,24934,24937,24938,24939,24940,24941,24942,24943,24945,24946,24947,24948,24950,24952,24953,24954,24955,24956,24957,24958,24959,24960,24961,24962,24963,24964,24965,24966,24967,24968,24969,24970,24972,24973,24975,24976,24977,24978,24979,24981,24982,24983,24984,24985,24986,24987,24988,24990,24991,24992,24993,24994,24995,24996,24997,24998,25002,25003,25005,25006,25007,25008,25009,25010,25011,25012,25013,25014,25016,25017,25018,25019,25020,25021,25023,25024,25025,25027,25028,25029,25030,25031,25033,25036,25037,25038,25039,25040,25043,25045,25046,25047,25048,25049,25050,25051,25052,25053,25054,25055,25056,25057,25058,25059,25060,25061,25063,25064,25065,25066,25067,25068,25069,25070,25071,25072,25073,25074,25075,25076,25078,25079,25080,25081,25082,25083,25084,25085,25086,25088,25089,25090,25091,25092,25093,25095,25097,25107,25108,25113,25116,25117,25118,25120,25123,25126,25127,25128,25129,25131,25133,25135,25136,25137,25138,25141,25142,25144,25145,25146,25147,25148,25154,25156,25157,25158,25162,25167,25168,25173,25174,25175,25177,25178,25180,25181,25182,25183,25184,25185,25186,25188,25189,25192,25201,25202,25204,25205,25207,25208,25210,25211,25213,25217,25218,25219,25221,25222,25223,25224,25227,25228,25229,25230,25231,25232,25236,25241,25244,25245,25246,25251,25254,25255,25257,25258,25261,25262,25263,25264,25266,25267,25268,25270,25271,25272,25274,25278,25280,25281,25283,25291,25295,25297,25301,25309,25310,25312,25313,25316,25322,25323,25328,25330,25333,25336,25337,25338,25339,25344,25347,25348,25349,25350,25354,25355,25356,25357,25359,25360,25362,25363,25364,25365,25367,25368,25369,25372,25382,25383,25385,25388,25389,25390,25392,25393,25395,25396,25397,25398,25399,25400,25403,25404,25406,25407,25408,25409,25412,25415,25416,25418,25425,25426,25427,25428,25430,25431,25432,25433,25434,25435,25436,25437,25440,25444,25445,25446,25448,25450,25451,25452,25455,25456,25458,25459,25460,25461,25464,25465,25468,25469,25470,25471,25473,25475,25476,25477,25478,25483,25485,25489,25491,25492,25493,25495,25497,25498,25499,25500,25501,25502,25503,25505,25508,25510,25515,25519,25521,25522,25525,25526,25529,25531,25533,25535,25536,25537,25538,25539,25541,25543,25544,25546,25547,25548,25553,25555,25556,25557,25559,25560,25561,25562,25563,25564,25565,25567,25570,25572,25573,25574,25575,25576,25579,25580,25582,25583,25584,25585,25587,25589,25591,25593,25594,25595,25596,25598,25603,25604,25606,25607,25608,25609,25610,25613,25614,25617,25618,25621,25622,25623,25624,25625,25626,25629,25631,25634,25635,25636,25637,25639,25640,25641,25643,25646,25647,25648,25649,25650,25651,25653,25654,25655,25656,25657,25659,25660,25662,25664,25666,25667,25673,25675,25676,25677,25678,25679,25680,25681,25683,25685,25686,25687,25689,25690,25691,25692,25693,25695,25696,25697,25698,25699,25700,25701,25702,25704,25706,25707,25708,25710,25711,25712,25713,25714,25715,25716,25717,25718,25719,25723,25724,25725,25726,25727,25728,25729,25731,25734,25736,25737,25738,25739,25740,25741,25742,25743,25744,25747,25748,25751,25752,25754,25755,25756,25757,25759,25760,25761,25762,25763,25765,25766,25767,25768,25770,25771,25775,25777,25778,25779,25780,25782,25785,25787,25789,25790,25791,25793,25795,25796,25798,25799,25800,25801,25802,25803,25804,25807,25809,25811,25812,25813,25814,25817,25818,25819,25820,25821,25823,25824,25825,25827,25829,25831,25832,25833,25834,25835,25836,25837,25838,25839,25840,25841,25842,25843,25844,25845,25846,25847,25848,25849,25850,25851,25852,25853,25854,25855,25857,25858,25859,25860,25861,25862,25863,25864,25866,25867,25868,25869,25870,25871,25872,25873,25875,25876,25877,25878,25879,25881,25882,25883,25884,25885,25886,25887,25888,25889,25890,25891,25892,25894,25895,25896,25897,25898,25900,25901,25904,25905,25906,25907,25911,25914,25916,25917,25920,25921,25922,25923,25924,25926,25927,25930,25931,25933,25934,25936,25938,25939,25940,25943,25944,25946,25948,25951,25952,25953,25956,25957,25959,25960,25961,25962,25965,25966,25967,25969,25971,25973,25974,25976,25977,25978,25979,25980,25981,25982,25983,25984,25985,25986,25987,25988,25989,25990,25992,25993,25994,25997,25998,25999,26002,26004,26005,26006,26008,26010,26013,26014,26016,26018,26019,26022,26024,26026,26028,26030,26033,26034,26035,26036,26037,26038,26039,26040,26042,26043,26046,26047,26048,26050,26055,26056,26057,26058,26061,26064,26065,26067,26068,26069,26072,26073,26074,26075,26076,26077,26078,26079,26081,26083,26084,26090,26091,26098,26099,26100,26101,26104,26105,26107,26108,26109,26110,26111,26113,26116,26117,26119,26120,26121,26123,26125,26128,26129,26130,26134,26135,26136,26138,26139,26140,26142,26145,26146,26147,26148,26150,26153,26154,26155,26156,26158,26160,26162,26163,26167,26168,26169,26170,26171,26173,26175,26176,26178,26180,26181,26182,26183,26184,26185,26186,26189,26190,26192,26193,26200,26201,26203,26204,26205,26206,26208,26210,26211,26213,26215,26217,26218,26219,26220,26221,26225,26226,26227,26229,26232,26233,26235,26236,26237,26239,26240,26241,26243,26245,26246,26248,26249,26250,26251,26253,26254,26255,26256,26258,26259,26260,26261,26264,26265,26266,26267,26268,26270,26271,26272,26273,26274,26275,26276,26277,26278,26281,26282,26283,26284,26285,26287,26288,26289,26290,26291,26293,26294,26295,26296,26298,26299,26300,26301,26303,26304,26305,26306,26307,26308,26309,26310,26311,26312,26313,26314,26315,26316,26317,26318,26319,26320,26321,26322,26323,26324,26325,26326,26327,26328,26330,26334,26335,26336,26337,26338,26339,26340,26341,26343,26344,26346,26347,26348,26349,26350,26351,26353,26357,26358,26360,26362,26363,26365,26369,26370,26371,26372,26373,26374,26375,26380,26382,26383,26385,26386,26387,26390,26392,26393,26394,26396,26398,26400,26401,26402,26403,26404,26405,26407,26409,26414,26416,26418,26419,26422,26423,26424,26425,26427,26428,26430,26431,26433,26436,26437,26439,26442,26443,26445,26450,26452,26453,26455,26456,26457,26458,26459,26461,26466,26467,26468,26470,26471,26475,26476,26478,26481,26484,26486,26488,26489,26490,26491,26493,26496,26498,26499,26501,26502,26504,26506,26508,26509,26510,26511,26513,26514,26515,26516,26518,26521,26523,26527,26528,26529,26532,26534,26537,26540,26542,26545,26546,26548,26553,26554,26555,26556,26557,26558,26559,26560,26562,26565,26566,26567,26568,26569,26570,26571,26572,26573,26574,26581,26582,26583,26587,26591,26593,26595,26596,26598,26599,26600,26602,26603,26605,26606,26610,26613,26614,26615,26616,26617,26618,26619,26620,26622,26625,26626,26627,26628,26630,26637,26640,26642,26644,26645,26648,26649,26650,26651,26652,26654,26655,26656,26658,26659,26660,26661,26662,26663,26664,26667,26668,26669,26670,26671,26672,26673,26676,26677,26678,26682,26683,26687,26695,26699,26701,26703,26706,26710,26711,26712,26713,26714,26715,26716,26717,26718,26719,26730,26732,26733,26734,26735,26736,26737,26738,26739,26741,26744,26745,26746,26747,26748,26749,26750,26751,26752,26754,26756,26759,26760,26761,26762,26763,26764,26765,26766,26768,26769,26770,26772,26773,26774,26776,26777,26778,26779,26780,26781,26782,26783,26784,26785,26787,26788,26789,26793,26794,26795,26796,26798,26801,26802,26804,26806,26807,26808,26809,26810,26811,26812,26813,26814,26815,26817,26819,26820,26821,26822,26823,26824,26826,26828,26830,26831,26832,26833,26835,26836,26838,26839,26841,26843,26844,26845,26846,26847,26849,26850,26852,26853,26854,26855,26856,26857,26858,26859,26860,26861,26863,26866,26867,26868,26870,26871,26872,26875,26877,26878,26879,26880,26882,26883,26884,26886,26887,26888,26889,26890,26892,26895,26897,26899,26900,26901,26902,26903,26904,26905,26906,26907,26908,26909,26910,26913,26914,26915,26917,26918,26919,26920,26921,26922,26923,26924,26926,26927,26929,26930,26931,26933,26934,26935,26936,26938,26939,26940,26942,26944,26945,26947,26948,26949,26950,26951,26952,26953,26954,26955,26956,26957,26958,26959,26960,26961,26962,26963,26965,26966,26968,26969,26971,26972,26975,26977,26978,26980,26981,26983,26984,26985,26986,26988,26989,26991,26992,26994,26995,26996,26997,26998,27002,27003,27005,27006,27007,27009,27011,27013,27018,27019,27020,27022,27023,27024,27025,27026,27027,27030,27031,27033,27034,27037,27038,27039,27040,27041,27042,27043,27044,27045,27046,27049,27050,27052,27054,27055,27056,27058,27059,27061,27062,27064,27065,27066,27068,27069,27070,27071,27072,27074,27075,27076,27077,27078,27079,27080,27081,27083,27085,27087,27089,27090,27091,27093,27094,27095,27096,27097,27098,27100,27101,27102,27105,27106,27107,27108,27109,27110,27111,27112,27113,27114,27115,27116,27118,27119,27120,27121,27123,27124,27125,27126,27127,27128,27129,27130,27131,27132,27134,27136,27137,27138,27139,27140,27141,27142,27143,27144,27145,27147,27148,27149,27150,27151,27152,27153,27154,27155,27156,27157,27158,27161,27162,27163,27164,27165,27166,27168,27170,27171,27172,27173,27174,27175,27177,27179,27180,27181,27182,27184,27186,27187,27188,27190,27191,27192,27193,27194,27195,27196,27199,27200,27201,27202,27203,27205,27206,27208,27209,27210,27211,27212,27213,27214,27215,27217,27218,27219,27220,27221,27222,27223,27226,27228,27229,27230,27231,27232,27234,27235,27236,27238,27239,27240,27241,27242,27243,27244,27245,27246,27247,27248,27250,27251,27252,27253,27254,27255,27256,27258,27259,27261,27262,27263,27265,27266,27267,27269,27270,27271,27272,27273,27274,27275,27276,27277,27279,27282,27283,27284,27285,27286,27288,27289,27290,27291,27292,27293,27294,27295,27297,27298,27299,27300,27301,27302,27303,27304,27306,27309,27310,27311,27312,27313,27314,27315,27316,27317,27318,27319,27320,27321,27322,27323,27324,27325,27326,27327,27328,27329,27330,27331,27332,27333,27334,27335,27336,27337,27338,27339,27340,27341,27342,27343,27344,27345,27346,27347,27348,27349,27350,27351,27352,27353,27354,27355,27356,27357,27358,27359,27360,27361,27362,27363,27364,27365,27366,27367,27368,27369,27370,27371,27372,27373,27374,27375,27376,27377,27378,27379,27380,27381,27382,27383,27384,27385,27386,27387,27388,27389,27390,27391,27392,27393,27394,27395,27396,27397,27398,27399,27400,27401,27402,27403,27404,27405,27406,27407,27408,27409,27410,27411,27412,27413,27414,27415,27416,27417,27418,27419,27420,27421,27422,27423,27429,27430,27432,27433,27434,27435,27436,27437,27438,27439,27440,27441,27443,27444,27445,27446,27448,27451,27452,27453,27455,27456,27457,27458,27460,27461,27464,27466,27467,27469,27470,27471,27472,27473,27474,27475,27476,27477,27478,27479,27480,27482,27483,27484,27485,27486,27487,27488,27489,27496,27497,27499,27500,27501,27502,27503,27504,27505,27506,27507,27508,27509,27510,27511,27512,27514,27517,27518,27519,27520,27525,27528,27532,27534,27535,27536,27537,27540,27541,27543,27544,27545,27548,27549,27550,27551,27552,27554,27555,27556,27557,27558,27559,27560,27561,27563,27564,27565,27566,27567,27568,27569,27570,27574,27576,27577,27578,27579,27580,27581,27582,27584,27587,27588,27590,27591,27592,27593,27594,27596,27598,27600,27601,27608,27610,27612,27613,27614,27615,27616,27618,27619,27620,27621,27622,27623,27624,27625,27628,27629,27630,27632,27633,27634,27636,27638,27639,27640,27642,27643,27644,27646,27647,27648,27649,27650,27651,27652,27656,27657,27658,27659,27660,27662,27666,27671,27676,27677,27678,27680,27683,27685,27691,27692,27693,27697,27699,27702,27703,27705,27706,27707,27708,27710,27711,27715,27716,27717,27720,27723,27724,27725,27726,27727,27729,27730,27731,27734,27736,27737,27738,27746,27747,27749,27750,27751,27755,27756,27757,27758,27759,27761,27763,27765,27767,27768,27770,27771,27772,27775,27776,27780,27783,27786,27787,27789,27790,27793,27794,27797,27798,27799,27800,27802,27804,27805,27806,27808,27810,27816,27820,27823,27824,27828,27829,27830,27831,27834,27840,27841,27842,27843,27846,27847,27848,27851,27853,27854,27855,27857,27858,27864,27865,27866,27868,27869,27871,27876,27878,27879,27881,27884,27885,27890,27892,27897,27903,27904,27906,27907,27909,27910,27912,27913,27914,27917,27919,27920,27921,27923,27924,27925,27926,27928,27932,27933,27935,27936,27937,27938,27939,27940,27942,27944,27945,27948,27949,27951,27952,27956,27958,27959,27960,27962,27967,27968,27970,27972,27977,27980,27984,27989,27990,27991,27992,27995,27997,27999,28001,28002,28004,28005,28007,28008,28011,28012,28013,28016,28017,28018,28019,28021,28022,28025,28026,28027,28029,28030,28031,28032,28033,28035,28036,28038,28039,28042,28043,28045,28047,28048,28050,28054,28055,28056,28057,28058,28060,28066,28069,28076,28077,28080,28081,28083,28084,28086,28087,28089,28090,28091,28092,28093,28094,28097,28098,28099,28104,28105,28106,28109,28110,28111,28112,28114,28115,28116,28117,28119,28122,28123,28124,28127,28130,28131,28133,28135,28136,28137,28138,28141,28143,28144,28146,28148,28149,28150,28152,28154,28157,28158,28159,28160,28161,28162,28163,28164,28166,28167,28168,28169,28171,28175,28178,28179,28181,28184,28185,28187,28188,28190,28191,28194,28198,28199,28200,28202,28204,28206,28208,28209,28211,28213,28214,28215,28217,28219,28220,28221,28222,28223,28224,28225,28226,28229,28230,28231,28232,28233,28234,28235,28236,28239,28240,28241,28242,28245,28247,28249,28250,28252,28253,28254,28256,28257,28258,28259,28260,28261,28262,28263,28264,28265,28266,28268,28269,28271,28272,28273,28274,28275,28276,28277,28278,28279,28280,28281,28282,28283,28284,28285,28288,28289,28290,28292,28295,28296,28298,28299,28300,28301,28302,28305,28306,28307,28308,28309,28310,28311,28313,28314,28315,28317,28318,28320,28321,28323,28324,28326,28328,28329,28331,28332,28333,28334,28336,28339,28341,28344,28345,28348,28350,28351,28352,28355,28356,28357,28358,28360,28361,28362,28364,28365,28366,28368,28370,28374,28376,28377,28379,28380,28381,28387,28391,28394,28395,28396,28397,28398,28399,28400,28401,28402,28403,28405,28406,28407,28408,28410,28411,28412,28413,28414,28415,28416,28417,28419,28420,28421,28423,28424,28426,28427,28428,28429,28430,28432,28433,28434,28438,28439,28440,28441,28442,28443,28444,28445,28446,28447,28449,28450,28451,28453,28454,28455,28456,28460,28462,28464,28466,28468,28469,28471,28472,28473,28474,28475,28476,28477,28479,28480,28481,28482,28483,28484,28485,28488,28489,28490,28492,28494,28495,28496,28497,28498,28499,28500,28501,28502,28503,28505,28506,28507,28509,28511,28512,28513,28515,28516,28517,28519,28520,28521,28522,28523,28524,28527,28528,28529,28531,28533,28534,28535,28537,28539,28541,28542,28543,28544,28545,28546,28547,28549,28550,28551,28554,28555,28559,28560,28561,28562,28563,28564,28565,28566,28567,28568,28569,28570,28571,28573,28574,28575,28576,28578,28579,28580,28581,28582,28584,28585,28586,28587,28588,28589,28590,28591,28592,28593,28594,28596,28597,28599,28600,28602,28603,28604,28605,28606,28607,28609,28611,28612,28613,28614,28615,28616,28618,28619,28620,28621,28622,28623,28624,28627,28628,28629,28630,28631,28632,28633,28634,28635,28636,28637,28639,28642,28643,28644,28645,28646,28647,28648,28649,28650,28651,28652,28653,28656,28657,28658,28659,28660,28661,28662,28663,28664,28665,28666,28667,28668,28669,28670,28671,28672,28673,28674,28675,28676,28677,28678,28679,28680,28681,28682,28683,28684,28685,28686,28687,28688,28690,28691,28692,28693,28694,28695,28696,28697,28700,28701,28702,28703,28704,28705,28706,28708,28709,28710,28711,28712,28713,28714,28715,28716,28717,28718,28719,28720,28721,28722,28723,28724,28726,28727,28728,28730,28731,28732,28733,28734,28735,28736,28737,28738,28739,28740,28741,28742,28743,28744,28745,28746,28747,28749,28750,28752,28753,28754,28755,28756,28757,28758,28759,28760,28761,28762,28763,28764,28765,28767,28768,28769,28770,28771,28772,28773,28774,28775,28776,28777,28778,28782,28785,28786,28787,28788,28791,28793,28794,28795,28797,28801,28802,28803,28804,28806,28807,28808,28811,28812,28813,28815,28816,28817,28819,28823,28824,28826,28827,28830,28831,28832,28833,28834,28835,28836,28837,28838,28839,28840,28841,28842,28848,28850,28852,28853,28854,28858,28862,28863,28868,28869,28870,28871,28873,28875,28876,28877,28878,28879,28880,28881,28882,28883,28884,28885,28886,28887,28890,28892,28893,28894,28896,28897,28898,28899,28901,28906,28910,28912,28913,28914,28915,28916,28917,28918,28920,28922,28923,28924,28926,28927,28928,28929,28930,28931,28932,28933,28934,28935,28936,28939,28940,28941,28942,28943,28945,28946,28948,28951,28955,28956,28957,28958,28959,28960,28961,28962,28963,28964,28965,28967,28968,28969,28970,28971,28972,28973,28974,28978,28979,28980,28981,28983,28984,28985,28986,28987,28988,28989,28990,28991,28992,28993,28994,28995,28996,28998,28999,29000,29001,29003,29005,29007,29008,29009,29010,29011,29012,29013,29014,29015,29016,29017,29018,29019,29021,29023,29024,29025,29026,29027,29029,29033,29034,29035,29036,29037,29039,29040,29041,29044,29045,29046,29047,29049,29051,29052,29054,29055,29056,29057,29058,29059,29061,29062,29063,29064,29065,29067,29068,29069,29070,29072,29073,29074,29075,29077,29078,29079,29082,29083,29084,29085,29086,29089,29090,29091,29092,29093,29094,29095,29097,29098,29099,29101,29102,29103,29104,29105,29106,29108,29110,29111,29112,29114,29115,29116,29117,29118,29119,29120,29121,29122,29124,29125,29126,29127,29128,29129,29130,29131,29132,29133,29135,29136,29137,29138,29139,29142,29143,29144,29145,29146,29147,29148,29149,29150,29151,29153,29154,29155,29156,29158,29160,29161,29162,29163,29164,29165,29167,29168,29169,29170,29171,29172,29173,29174,29175,29176,29178,29179,29180,29181,29182,29183,29184,29185,29186,29187,29188,29189,29191,29192,29193,29194,29195,29196,29197,29198,29199,29200,29201,29202,29203,29204,29205,29206,29207,29208,29209,29210,29211,29212,29214,29215,29216,29217,29218,29219,29220,29221,29222,29223,29225,29227,29229,29230,29231,29234,29235,29236,29242,29244,29246,29248,29249,29250,29251,29252,29253,29254,29257,29258,29259,29262,29263,29264,29265,29267,29268,29269,29271,29272,29274,29276,29278,29280,29283,29284,29285,29288,29290,29291,29292,29293,29296,29297,29299,29300,29302,29303,29304,29307,29308,29309,29314,29315,29317,29318,29319,29320,29321,29324,29326,29328,29329,29331,29332,29333,29334,29335,29336,29337,29338,29339,29340,29341,29342,29344,29345,29346,29347,29348,29349,29350,29351,29352,29353,29354,29355,29358,29361,29362,29363,29365,29370,29371,29372,29373,29374,29375,29376,29381,29382,29383,29385,29386,29387,29388,29391,29393,29395,29396,29397,29398,29400,29402,29403,58566,58567,58568,58569,58570,58571,58572,58573,58574,58575,58576,58577,58578,58579,58580,58581,58582,58583,58584,58585,58586,58587,58588,58589,58590,58591,58592,58593,58594,58595,58596,58597,58598,58599,58600,58601,58602,58603,58604,58605,58606,58607,58608,58609,58610,58611,58612,58613,58614,58615,58616,58617,58618,58619,58620,58621,58622,58623,58624,58625,58626,58627,58628,58629,58630,58631,58632,58633,58634,58635,58636,58637,58638,58639,58640,58641,58642,58643,58644,58645,58646,58647,58648,58649,58650,58651,58652,58653,58654,58655,58656,58657,58658,58659,58660,58661,12288,12289,12290,183,713,711,168,12291,12293,8212,65374,8214,8230,8216,8217,8220,8221,12308,12309,12296,12297,12298,12299,12300,12301,12302,12303,12310,12311,12304,12305,177,215,247,8758,8743,8744,8721,8719,8746,8745,8712,8759,8730,8869,8741,8736,8978,8857,8747,8750,8801,8780,8776,8765,8733,8800,8814,8815,8804,8805,8734,8757,8756,9794,9792,176,8242,8243,8451,65284,164,65504,65505,8240,167,8470,9734,9733,9675,9679,9678,9671,9670,9633,9632,9651,9650,8251,8594,8592,8593,8595,12307,58662,58663,58664,58665,58666,58667,58668,58669,58670,58671,58672,58673,58674,58675,58676,58677,58678,58679,58680,58681,58682,58683,58684,58685,58686,58687,58688,58689,58690,58691,58692,58693,58694,58695,58696,58697,58698,58699,58700,58701,58702,58703,58704,58705,58706,58707,58708,58709,58710,58711,58712,58713,58714,58715,58716,58717,58718,58719,58720,58721,58722,58723,58724,58725,58726,58727,58728,58729,58730,58731,58732,58733,58734,58735,58736,58737,58738,58739,58740,58741,58742,58743,58744,58745,58746,58747,58748,58749,58750,58751,58752,58753,58754,58755,58756,58757,8560,8561,8562,8563,8564,8565,8566,8567,8568,8569,59238,59239,59240,59241,59242,59243,9352,9353,9354,9355,9356,9357,9358,9359,9360,9361,9362,9363,9364,9365,9366,9367,9368,9369,9370,9371,9332,9333,9334,9335,9336,9337,9338,9339,9340,9341,9342,9343,9344,9345,9346,9347,9348,9349,9350,9351,9312,9313,9314,9315,9316,9317,9318,9319,9320,9321,8364,59245,12832,12833,12834,12835,12836,12837,12838,12839,12840,12841,59246,59247,8544,8545,8546,8547,8548,8549,8550,8551,8552,8553,8554,8555,59248,59249,58758,58759,58760,58761,58762,58763,58764,58765,58766,58767,58768,58769,58770,58771,58772,58773,58774,58775,58776,58777,58778,58779,58780,58781,58782,58783,58784,58785,58786,58787,58788,58789,58790,58791,58792,58793,58794,58795,58796,58797,58798,58799,58800,58801,58802,58803,58804,58805,58806,58807,58808,58809,58810,58811,58812,58813,58814,58815,58816,58817,58818,58819,58820,58821,58822,58823,58824,58825,58826,58827,58828,58829,58830,58831,58832,58833,58834,58835,58836,58837,58838,58839,58840,58841,58842,58843,58844,58845,58846,58847,58848,58849,58850,58851,58852,12288,65281,65282,65283,65509,65285,65286,65287,65288,65289,65290,65291,65292,65293,65294,65295,65296,65297,65298,65299,65300,65301,65302,65303,65304,65305,65306,65307,65308,65309,65310,65311,65312,65313,65314,65315,65316,65317,65318,65319,65320,65321,65322,65323,65324,65325,65326,65327,65328,65329,65330,65331,65332,65333,65334,65335,65336,65337,65338,65339,65340,65341,65342,65343,65344,65345,65346,65347,65348,65349,65350,65351,65352,65353,65354,65355,65356,65357,65358,65359,65360,65361,65362,65363,65364,65365,65366,65367,65368,65369,65370,65371,65372,65373,65507,58854,58855,58856,58857,58858,58859,58860,58861,58862,58863,58864,58865,58866,58867,58868,58869,58870,58871,58872,58873,58874,58875,58876,58877,58878,58879,58880,58881,58882,58883,58884,58885,58886,58887,58888,58889,58890,58891,58892,58893,58894,58895,58896,58897,58898,58899,58900,58901,58902,58903,58904,58905,58906,58907,58908,58909,58910,58911,58912,58913,58914,58915,58916,58917,58918,58919,58920,58921,58922,58923,58924,58925,58926,58927,58928,58929,58930,58931,58932,58933,58934,58935,58936,58937,58938,58939,58940,58941,58942,58943,58944,58945,58946,58947,58948,58949,12353,12354,12355,12356,12357,12358,12359,12360,12361,12362,12363,12364,12365,12366,12367,12368,12369,12370,12371,12372,12373,12374,12375,12376,12377,12378,12379,12380,12381,12382,12383,12384,12385,12386,12387,12388,12389,12390,12391,12392,12393,12394,12395,12396,12397,12398,12399,12400,12401,12402,12403,12404,12405,12406,12407,12408,12409,12410,12411,12412,12413,12414,12415,12416,12417,12418,12419,12420,12421,12422,12423,12424,12425,12426,12427,12428,12429,12430,12431,12432,12433,12434,12435,59250,59251,59252,59253,59254,59255,59256,59257,59258,59259,59260,58950,58951,58952,58953,58954,58955,58956,58957,58958,58959,58960,58961,58962,58963,58964,58965,58966,58967,58968,58969,58970,58971,58972,58973,58974,58975,58976,58977,58978,58979,58980,58981,58982,58983,58984,58985,58986,58987,58988,58989,58990,58991,58992,58993,58994,58995,58996,58997,58998,58999,59000,59001,59002,59003,59004,59005,59006,59007,59008,59009,59010,59011,59012,59013,59014,59015,59016,59017,59018,59019,59020,59021,59022,59023,59024,59025,59026,59027,59028,59029,59030,59031,59032,59033,59034,59035,59036,59037,59038,59039,59040,59041,59042,59043,59044,59045,12449,12450,12451,12452,12453,12454,12455,12456,12457,12458,12459,12460,12461,12462,12463,12464,12465,12466,12467,12468,12469,12470,12471,12472,12473,12474,12475,12476,12477,12478,12479,12480,12481,12482,12483,12484,12485,12486,12487,12488,12489,12490,12491,12492,12493,12494,12495,12496,12497,12498,12499,12500,12501,12502,12503,12504,12505,12506,12507,12508,12509,12510,12511,12512,12513,12514,12515,12516,12517,12518,12519,12520,12521,12522,12523,12524,12525,12526,12527,12528,12529,12530,12531,12532,12533,12534,59261,59262,59263,59264,59265,59266,59267,59268,59046,59047,59048,59049,59050,59051,59052,59053,59054,59055,59056,59057,59058,59059,59060,59061,59062,59063,59064,59065,59066,59067,59068,59069,59070,59071,59072,59073,59074,59075,59076,59077,59078,59079,59080,59081,59082,59083,59084,59085,59086,59087,59088,59089,59090,59091,59092,59093,59094,59095,59096,59097,59098,59099,59100,59101,59102,59103,59104,59105,59106,59107,59108,59109,59110,59111,59112,59113,59114,59115,59116,59117,59118,59119,59120,59121,59122,59123,59124,59125,59126,59127,59128,59129,59130,59131,59132,59133,59134,59135,59136,59137,59138,59139,59140,59141,913,914,915,916,917,918,919,920,921,922,923,924,925,926,927,928,929,931,932,933,934,935,936,937,59269,59270,59271,59272,59273,59274,59275,59276,945,946,947,948,949,950,951,952,953,954,955,956,957,958,959,960,961,963,964,965,966,967,968,969,59277,59278,59279,59280,59281,59282,59283,65077,65078,65081,65082,65087,65088,65085,65086,65089,65090,65091,65092,59284,59285,65083,65084,65079,65080,65073,59286,65075,65076,59287,59288,59289,59290,59291,59292,59293,59294,59295,59142,59143,59144,59145,59146,59147,59148,59149,59150,59151,59152,59153,59154,59155,59156,59157,59158,59159,59160,59161,59162,59163,59164,59165,59166,59167,59168,59169,59170,59171,59172,59173,59174,59175,59176,59177,59178,59179,59180,59181,59182,59183,59184,59185,59186,59187,59188,59189,59190,59191,59192,59193,59194,59195,59196,59197,59198,59199,59200,59201,59202,59203,59204,59205,59206,59207,59208,59209,59210,59211,59212,59213,59214,59215,59216,59217,59218,59219,59220,59221,59222,59223,59224,59225,59226,59227,59228,59229,59230,59231,59232,59233,59234,59235,59236,59237,1040,1041,1042,1043,1044,1045,1025,1046,1047,1048,1049,1050,1051,1052,1053,1054,1055,1056,1057,1058,1059,1060,1061,1062,1063,1064,1065,1066,1067,1068,1069,1070,1071,59296,59297,59298,59299,59300,59301,59302,59303,59304,59305,59306,59307,59308,59309,59310,1072,1073,1074,1075,1076,1077,1105,1078,1079,1080,1081,1082,1083,1084,1085,1086,1087,1088,1089,1090,1091,1092,1093,1094,1095,1096,1097,1098,1099,1100,1101,1102,1103,59311,59312,59313,59314,59315,59316,59317,59318,59319,59320,59321,59322,59323,714,715,729,8211,8213,8229,8245,8453,8457,8598,8599,8600,8601,8725,8735,8739,8786,8806,8807,8895,9552,9553,9554,9555,9556,9557,9558,9559,9560,9561,9562,9563,9564,9565,9566,9567,9568,9569,9570,9571,9572,9573,9574,9575,9576,9577,9578,9579,9580,9581,9582,9583,9584,9585,9586,9587,9601,9602,9603,9604,9605,9606,9607,9608,9609,9610,9611,9612,9613,9614,9615,9619,9620,9621,9660,9661,9698,9699,9700,9701,9737,8853,12306,12317,12318,59324,59325,59326,59327,59328,59329,59330,59331,59332,59333,59334,257,225,462,224,275,233,283,232,299,237,464,236,333,243,466,242,363,250,468,249,470,472,474,476,252,234,593,7743,324,328,505,609,59337,59338,59339,59340,12549,12550,12551,12552,12553,12554,12555,12556,12557,12558,12559,12560,12561,12562,12563,12564,12565,12566,12567,12568,12569,12570,12571,12572,12573,12574,12575,12576,12577,12578,12579,12580,12581,12582,12583,12584,12585,59341,59342,59343,59344,59345,59346,59347,59348,59349,59350,59351,59352,59353,59354,59355,59356,59357,59358,59359,59360,59361,12321,12322,12323,12324,12325,12326,12327,12328,12329,12963,13198,13199,13212,13213,13214,13217,13252,13262,13265,13266,13269,65072,65506,65508,59362,8481,12849,59363,8208,59364,59365,59366,12540,12443,12444,12541,12542,12294,12445,12446,65097,65098,65099,65100,65101,65102,65103,65104,65105,65106,65108,65109,65110,65111,65113,65114,65115,65116,65117,65118,65119,65120,65121,65122,65123,65124,65125,65126,65128,65129,65130,65131,12350,12272,12273,12274,12275,12276,12277,12278,12279,12280,12281,12282,12283,12295,59380,59381,59382,59383,59384,59385,59386,59387,59388,59389,59390,59391,59392,9472,9473,9474,9475,9476,9477,9478,9479,9480,9481,9482,9483,9484,9485,9486,9487,9488,9489,9490,9491,9492,9493,9494,9495,9496,9497,9498,9499,9500,9501,9502,9503,9504,9505,9506,9507,9508,9509,9510,9511,9512,9513,9514,9515,9516,9517,9518,9519,9520,9521,9522,9523,9524,9525,9526,9527,9528,9529,9530,9531,9532,9533,9534,9535,9536,9537,9538,9539,9540,9541,9542,9543,9544,9545,9546,9547,59393,59394,59395,59396,59397,59398,59399,59400,59401,59402,59403,59404,59405,59406,59407,29404,29405,29407,29410,29411,29412,29413,29414,29415,29418,29419,29429,29430,29433,29437,29438,29439,29440,29442,29444,29445,29446,29447,29448,29449,29451,29452,29453,29455,29456,29457,29458,29460,29464,29465,29466,29471,29472,29475,29476,29478,29479,29480,29485,29487,29488,29490,29491,29493,29494,29498,29499,29500,29501,29504,29505,29506,29507,29508,29509,29510,29511,29512,29513,29514,29515,29516,29518,29519,29521,29523,29524,29525,29526,29528,29529,29530,29531,29532,29533,29534,29535,29537,29538,29539,29540,29541,29542,29543,29544,29545,29546,29547,29550,29552,29553,57344,57345,57346,57347,57348,57349,57350,57351,57352,57353,57354,57355,57356,57357,57358,57359,57360,57361,57362,57363,57364,57365,57366,57367,57368,57369,57370,57371,57372,57373,57374,57375,57376,57377,57378,57379,57380,57381,57382,57383,57384,57385,57386,57387,57388,57389,57390,57391,57392,57393,57394,57395,57396,57397,57398,57399,57400,57401,57402,57403,57404,57405,57406,57407,57408,57409,57410,57411,57412,57413,57414,57415,57416,57417,57418,57419,57420,57421,57422,57423,57424,57425,57426,57427,57428,57429,57430,57431,57432,57433,57434,57435,57436,57437,29554,29555,29556,29557,29558,29559,29560,29561,29562,29563,29564,29565,29567,29568,29569,29570,29571,29573,29574,29576,29578,29580,29581,29583,29584,29586,29587,29588,29589,29591,29592,29593,29594,29596,29597,29598,29600,29601,29603,29604,29605,29606,29607,29608,29610,29612,29613,29617,29620,29621,29622,29624,29625,29628,29629,29630,29631,29633,29635,29636,29637,29638,29639,29643,29644,29646,29650,29651,29652,29653,29654,29655,29656,29658,29659,29660,29661,29663,29665,29666,29667,29668,29670,29672,29674,29675,29676,29678,29679,29680,29681,29683,29684,29685,29686,29687,57438,57439,57440,57441,57442,57443,57444,57445,57446,57447,57448,57449,57450,57451,57452,57453,57454,57455,57456,57457,57458,57459,57460,57461,57462,57463,57464,57465,57466,57467,57468,57469,57470,57471,57472,57473,57474,57475,57476,57477,57478,57479,57480,57481,57482,57483,57484,57485,57486,57487,57488,57489,57490,57491,57492,57493,57494,57495,57496,57497,57498,57499,57500,57501,57502,57503,57504,57505,57506,57507,57508,57509,57510,57511,57512,57513,57514,57515,57516,57517,57518,57519,57520,57521,57522,57523,57524,57525,57526,57527,57528,57529,57530,57531,29688,29689,29690,29691,29692,29693,29694,29695,29696,29697,29698,29700,29703,29704,29707,29708,29709,29710,29713,29714,29715,29716,29717,29718,29719,29720,29721,29724,29725,29726,29727,29728,29729,29731,29732,29735,29737,29739,29741,29743,29745,29746,29751,29752,29753,29754,29755,29757,29758,29759,29760,29762,29763,29764,29765,29766,29767,29768,29769,29770,29771,29772,29773,29774,29775,29776,29777,29778,29779,29780,29782,29784,29789,29792,29793,29794,29795,29796,29797,29798,29799,29800,29801,29802,29803,29804,29806,29807,29809,29810,29811,29812,29813,29816,29817,29818,57532,57533,57534,57535,57536,57537,57538,57539,57540,57541,57542,57543,57544,57545,57546,57547,57548,57549,57550,57551,57552,57553,57554,57555,57556,57557,57558,57559,57560,57561,57562,57563,57564,57565,57566,57567,57568,57569,57570,57571,57572,57573,57574,57575,57576,57577,57578,57579,57580,57581,57582,57583,57584,57585,57586,57587,57588,57589,57590,57591,57592,57593,57594,57595,57596,57597,57598,57599,57600,57601,57602,57603,57604,57605,57606,57607,57608,57609,57610,57611,57612,57613,57614,57615,57616,57617,57618,57619,57620,57621,57622,57623,57624,57625,29819,29820,29821,29823,29826,29828,29829,29830,29832,29833,29834,29836,29837,29839,29841,29842,29843,29844,29845,29846,29847,29848,29849,29850,29851,29853,29855,29856,29857,29858,29859,29860,29861,29862,29866,29867,29868,29869,29870,29871,29872,29873,29874,29875,29876,29877,29878,29879,29880,29881,29883,29884,29885,29886,29887,29888,29889,29890,29891,29892,29893,29894,29895,29896,29897,29898,29899,29900,29901,29902,29903,29904,29905,29907,29908,29909,29910,29911,29912,29913,29914,29915,29917,29919,29921,29925,29927,29928,29929,29930,29931,29932,29933,29936,29937,29938,57626,57627,57628,57629,57630,57631,57632,57633,57634,57635,57636,57637,57638,57639,57640,57641,57642,57643,57644,57645,57646,57647,57648,57649,57650,57651,57652,57653,57654,57655,57656,57657,57658,57659,57660,57661,57662,57663,57664,57665,57666,57667,57668,57669,57670,57671,57672,57673,57674,57675,57676,57677,57678,57679,57680,57681,57682,57683,57684,57685,57686,57687,57688,57689,57690,57691,57692,57693,57694,57695,57696,57697,57698,57699,57700,57701,57702,57703,57704,57705,57706,57707,57708,57709,57710,57711,57712,57713,57714,57715,57716,57717,57718,57719,29939,29941,29944,29945,29946,29947,29948,29949,29950,29952,29953,29954,29955,29957,29958,29959,29960,29961,29962,29963,29964,29966,29968,29970,29972,29973,29974,29975,29979,29981,29982,29984,29985,29986,29987,29988,29990,29991,29994,29998,30004,30006,30009,30012,30013,30015,30017,30018,30019,30020,30022,30023,30025,30026,30029,30032,30033,30034,30035,30037,30038,30039,30040,30045,30046,30047,30048,30049,30050,30051,30052,30055,30056,30057,30059,30060,30061,30062,30063,30064,30065,30067,30069,30070,30071,30074,30075,30076,30077,30078,30080,30081,30082,30084,30085,30087,57720,57721,57722,57723,57724,57725,57726,57727,57728,57729,57730,57731,57732,57733,57734,57735,57736,57737,57738,57739,57740,57741,57742,57743,57744,57745,57746,57747,57748,57749,57750,57751,57752,57753,57754,57755,57756,57757,57758,57759,57760,57761,57762,57763,57764,57765,57766,57767,57768,57769,57770,57771,57772,57773,57774,57775,57776,57777,57778,57779,57780,57781,57782,57783,57784,57785,57786,57787,57788,57789,57790,57791,57792,57793,57794,57795,57796,57797,57798,57799,57800,57801,57802,57803,57804,57805,57806,57807,57808,57809,57810,57811,57812,57813,30088,30089,30090,30092,30093,30094,30096,30099,30101,30104,30107,30108,30110,30114,30118,30119,30120,30121,30122,30125,30134,30135,30138,30139,30143,30144,30145,30150,30155,30156,30158,30159,30160,30161,30163,30167,30169,30170,30172,30173,30175,30176,30177,30181,30185,30188,30189,30190,30191,30194,30195,30197,30198,30199,30200,30202,30203,30205,30206,30210,30212,30214,30215,30216,30217,30219,30221,30222,30223,30225,30226,30227,30228,30230,30234,30236,30237,30238,30241,30243,30247,30248,30252,30254,30255,30257,30258,30262,30263,30265,30266,30267,30269,30273,30274,30276,57814,57815,57816,57817,57818,57819,57820,57821,57822,57823,57824,57825,57826,57827,57828,57829,57830,57831,57832,57833,57834,57835,57836,57837,57838,57839,57840,57841,57842,57843,57844,57845,57846,57847,57848,57849,57850,57851,57852,57853,57854,57855,57856,57857,57858,57859,57860,57861,57862,57863,57864,57865,57866,57867,57868,57869,57870,57871,57872,57873,57874,57875,57876,57877,57878,57879,57880,57881,57882,57883,57884,57885,57886,57887,57888,57889,57890,57891,57892,57893,57894,57895,57896,57897,57898,57899,57900,57901,57902,57903,57904,57905,57906,57907,30277,30278,30279,30280,30281,30282,30283,30286,30287,30288,30289,30290,30291,30293,30295,30296,30297,30298,30299,30301,30303,30304,30305,30306,30308,30309,30310,30311,30312,30313,30314,30316,30317,30318,30320,30321,30322,30323,30324,30325,30326,30327,30329,30330,30332,30335,30336,30337,30339,30341,30345,30346,30348,30349,30351,30352,30354,30356,30357,30359,30360,30362,30363,30364,30365,30366,30367,30368,30369,30370,30371,30373,30374,30375,30376,30377,30378,30379,30380,30381,30383,30384,30387,30389,30390,30391,30392,30393,30394,30395,30396,30397,30398,30400,30401,30403,21834,38463,22467,25384,21710,21769,21696,30353,30284,34108,30702,33406,30861,29233,38552,38797,27688,23433,20474,25353,26263,23736,33018,26696,32942,26114,30414,20985,25942,29100,32753,34948,20658,22885,25034,28595,33453,25420,25170,21485,21543,31494,20843,30116,24052,25300,36299,38774,25226,32793,22365,38712,32610,29240,30333,26575,30334,25670,20336,36133,25308,31255,26001,29677,25644,25203,33324,39041,26495,29256,25198,25292,20276,29923,21322,21150,32458,37030,24110,26758,27036,33152,32465,26834,30917,34444,38225,20621,35876,33502,32990,21253,35090,21093,30404,30407,30409,30411,30412,30419,30421,30425,30426,30428,30429,30430,30432,30433,30434,30435,30436,30438,30439,30440,30441,30442,30443,30444,30445,30448,30451,30453,30454,30455,30458,30459,30461,30463,30464,30466,30467,30469,30470,30474,30476,30478,30479,30480,30481,30482,30483,30484,30485,30486,30487,30488,30491,30492,30493,30494,30497,30499,30500,30501,30503,30506,30507,30508,30510,30512,30513,30514,30515,30516,30521,30523,30525,30526,30527,30530,30532,30533,30534,30536,30537,30538,30539,30540,30541,30542,30543,30546,30547,30548,30549,30550,30551,30552,30553,30556,34180,38649,20445,22561,39281,23453,25265,25253,26292,35961,40077,29190,26479,30865,24754,21329,21271,36744,32972,36125,38049,20493,29384,22791,24811,28953,34987,22868,33519,26412,31528,23849,32503,29997,27893,36454,36856,36924,40763,27604,37145,31508,24444,30887,34006,34109,27605,27609,27606,24065,24199,30201,38381,25949,24330,24517,36767,22721,33218,36991,38491,38829,36793,32534,36140,25153,20415,21464,21342,36776,36777,36779,36941,26631,24426,33176,34920,40150,24971,21035,30250,24428,25996,28626,28392,23486,25672,20853,20912,26564,19993,31177,39292,28851,30557,30558,30559,30560,30564,30567,30569,30570,30573,30574,30575,30576,30577,30578,30579,30580,30581,30582,30583,30584,30586,30587,30588,30593,30594,30595,30598,30599,30600,30601,30602,30603,30607,30608,30611,30612,30613,30614,30615,30616,30617,30618,30619,30620,30621,30622,30625,30627,30628,30630,30632,30635,30637,30638,30639,30641,30642,30644,30646,30647,30648,30649,30650,30652,30654,30656,30657,30658,30659,30660,30661,30662,30663,30664,30665,30666,30667,30668,30670,30671,30672,30673,30674,30675,30676,30677,30678,30680,30681,30682,30685,30686,30687,30688,30689,30692,30149,24182,29627,33760,25773,25320,38069,27874,21338,21187,25615,38082,31636,20271,24091,33334,33046,33162,28196,27850,39539,25429,21340,21754,34917,22496,19981,24067,27493,31807,37096,24598,25830,29468,35009,26448,25165,36130,30572,36393,37319,24425,33756,34081,39184,21442,34453,27531,24813,24808,28799,33485,33329,20179,27815,34255,25805,31961,27133,26361,33609,21397,31574,20391,20876,27979,23618,36461,25554,21449,33580,33590,26597,30900,25661,23519,23700,24046,35815,25286,26612,35962,25600,25530,34633,39307,35863,32544,38130,20135,38416,39076,26124,29462,30694,30696,30698,30703,30704,30705,30706,30708,30709,30711,30713,30714,30715,30716,30723,30724,30725,30726,30727,30728,30730,30731,30734,30735,30736,30739,30741,30745,30747,30750,30752,30753,30754,30756,30760,30762,30763,30766,30767,30769,30770,30771,30773,30774,30781,30783,30785,30786,30787,30788,30790,30792,30793,30794,30795,30797,30799,30801,30803,30804,30808,30809,30810,30811,30812,30814,30815,30816,30817,30818,30819,30820,30821,30822,30823,30824,30825,30831,30832,30833,30834,30835,30836,30837,30838,30840,30841,30842,30843,30845,30846,30847,30848,30849,30850,30851,22330,23581,24120,38271,20607,32928,21378,25950,30021,21809,20513,36229,25220,38046,26397,22066,28526,24034,21557,28818,36710,25199,25764,25507,24443,28552,37108,33251,36784,23576,26216,24561,27785,38472,36225,34924,25745,31216,22478,27225,25104,21576,20056,31243,24809,28548,35802,25215,36894,39563,31204,21507,30196,25345,21273,27744,36831,24347,39536,32827,40831,20360,23610,36196,32709,26021,28861,20805,20914,34411,23815,23456,25277,37228,30068,36364,31264,24833,31609,20167,32504,30597,19985,33261,21021,20986,27249,21416,36487,38148,38607,28353,38500,26970,30852,30853,30854,30856,30858,30859,30863,30864,30866,30868,30869,30870,30873,30877,30878,30880,30882,30884,30886,30888,30889,30890,30891,30892,30893,30894,30895,30901,30902,30903,30904,30906,30907,30908,30909,30911,30912,30914,30915,30916,30918,30919,30920,30924,30925,30926,30927,30929,30930,30931,30934,30935,30936,30938,30939,30940,30941,30942,30943,30944,30945,30946,30947,30948,30949,30950,30951,30953,30954,30955,30957,30958,30959,30960,30961,30963,30965,30966,30968,30969,30971,30972,30973,30974,30975,30976,30978,30979,30980,30982,30983,30984,30985,30986,30987,30988,30784,20648,30679,25616,35302,22788,25571,24029,31359,26941,20256,33337,21912,20018,30126,31383,24162,24202,38383,21019,21561,28810,25462,38180,22402,26149,26943,37255,21767,28147,32431,34850,25139,32496,30133,33576,30913,38604,36766,24904,29943,35789,27492,21050,36176,27425,32874,33905,22257,21254,20174,19995,20945,31895,37259,31751,20419,36479,31713,31388,25703,23828,20652,33030,30209,31929,28140,32736,26449,23384,23544,30923,25774,25619,25514,25387,38169,25645,36798,31572,30249,25171,22823,21574,27513,20643,25140,24102,27526,20195,36151,34955,24453,36910,30989,30990,30991,30992,30993,30994,30996,30997,30998,30999,31000,31001,31002,31003,31004,31005,31007,31008,31009,31010,31011,31013,31014,31015,31016,31017,31018,31019,31020,31021,31022,31023,31024,31025,31026,31027,31029,31030,31031,31032,31033,31037,31039,31042,31043,31044,31045,31047,31050,31051,31052,31053,31054,31055,31056,31057,31058,31060,31061,31064,31065,31073,31075,31076,31078,31081,31082,31083,31084,31086,31088,31089,31090,31091,31092,31093,31094,31097,31099,31100,31101,31102,31103,31106,31107,31110,31111,31112,31113,31115,31116,31117,31118,31120,31121,31122,24608,32829,25285,20025,21333,37112,25528,32966,26086,27694,20294,24814,28129,35806,24377,34507,24403,25377,20826,33633,26723,20992,25443,36424,20498,23707,31095,23548,21040,31291,24764,36947,30423,24503,24471,30340,36460,28783,30331,31561,30634,20979,37011,22564,20302,28404,36842,25932,31515,29380,28068,32735,23265,25269,24213,22320,33922,31532,24093,24351,36882,32532,39072,25474,28359,30872,28857,20856,38747,22443,30005,20291,30008,24215,24806,22880,28096,27583,30857,21500,38613,20939,20993,25481,21514,38035,35843,36300,29241,30879,34678,36845,35853,21472,31123,31124,31125,31126,31127,31128,31129,31131,31132,31133,31134,31135,31136,31137,31138,31139,31140,31141,31142,31144,31145,31146,31147,31148,31149,31150,31151,31152,31153,31154,31156,31157,31158,31159,31160,31164,31167,31170,31172,31173,31175,31176,31178,31180,31182,31183,31184,31187,31188,31190,31191,31193,31194,31195,31196,31197,31198,31200,31201,31202,31205,31208,31210,31212,31214,31217,31218,31219,31220,31221,31222,31223,31225,31226,31228,31230,31231,31233,31236,31237,31239,31240,31241,31242,31244,31247,31248,31249,31250,31251,31253,31254,31256,31257,31259,31260,19969,30447,21486,38025,39030,40718,38189,23450,35746,20002,19996,20908,33891,25026,21160,26635,20375,24683,20923,27934,20828,25238,26007,38497,35910,36887,30168,37117,30563,27602,29322,29420,35835,22581,30585,36172,26460,38208,32922,24230,28193,22930,31471,30701,38203,27573,26029,32526,22534,20817,38431,23545,22697,21544,36466,25958,39039,22244,38045,30462,36929,25479,21702,22810,22842,22427,36530,26421,36346,33333,21057,24816,22549,34558,23784,40517,20420,39069,35769,23077,24694,21380,25212,36943,37122,39295,24681,32780,20799,32819,23572,39285,27953,20108,31261,31263,31265,31266,31268,31269,31270,31271,31272,31273,31274,31275,31276,31277,31278,31279,31280,31281,31282,31284,31285,31286,31288,31290,31294,31296,31297,31298,31299,31300,31301,31303,31304,31305,31306,31307,31308,31309,31310,31311,31312,31314,31315,31316,31317,31318,31320,31321,31322,31323,31324,31325,31326,31327,31328,31329,31330,31331,31332,31333,31334,31335,31336,31337,31338,31339,31340,31341,31342,31343,31345,31346,31347,31349,31355,31356,31357,31358,31362,31365,31367,31369,31370,31371,31372,31374,31375,31376,31379,31380,31385,31386,31387,31390,31393,31394,36144,21457,32602,31567,20240,20047,38400,27861,29648,34281,24070,30058,32763,27146,30718,38034,32321,20961,28902,21453,36820,33539,36137,29359,39277,27867,22346,33459,26041,32938,25151,38450,22952,20223,35775,32442,25918,33778,38750,21857,39134,32933,21290,35837,21536,32954,24223,27832,36153,33452,37210,21545,27675,20998,32439,22367,28954,27774,31881,22859,20221,24575,24868,31914,20016,23553,26539,34562,23792,38155,39118,30127,28925,36898,20911,32541,35773,22857,20964,20315,21542,22827,25975,32932,23413,25206,25282,36752,24133,27679,31526,20239,20440,26381,31395,31396,31399,31401,31402,31403,31406,31407,31408,31409,31410,31412,31413,31414,31415,31416,31417,31418,31419,31420,31421,31422,31424,31425,31426,31427,31428,31429,31430,31431,31432,31433,31434,31436,31437,31438,31439,31440,31441,31442,31443,31444,31445,31447,31448,31450,31451,31452,31453,31457,31458,31460,31463,31464,31465,31466,31467,31468,31470,31472,31473,31474,31475,31476,31477,31478,31479,31480,31483,31484,31486,31488,31489,31490,31493,31495,31497,31500,31501,31502,31504,31506,31507,31510,31511,31512,31514,31516,31517,31519,31521,31522,31523,31527,31529,31533,28014,28074,31119,34993,24343,29995,25242,36741,20463,37340,26023,33071,33105,24220,33104,36212,21103,35206,36171,22797,20613,20184,38428,29238,33145,36127,23500,35747,38468,22919,32538,21648,22134,22030,35813,25913,27010,38041,30422,28297,24178,29976,26438,26577,31487,32925,36214,24863,31174,25954,36195,20872,21018,38050,32568,32923,32434,23703,28207,26464,31705,30347,39640,33167,32660,31957,25630,38224,31295,21578,21733,27468,25601,25096,40509,33011,30105,21106,38761,33883,26684,34532,38401,38548,38124,20010,21508,32473,26681,36319,32789,26356,24218,32697,31535,31536,31538,31540,31541,31542,31543,31545,31547,31549,31551,31552,31553,31554,31555,31556,31558,31560,31562,31565,31566,31571,31573,31575,31577,31580,31582,31583,31585,31587,31588,31589,31590,31591,31592,31593,31594,31595,31596,31597,31599,31600,31603,31604,31606,31608,31610,31612,31613,31615,31617,31618,31619,31620,31622,31623,31624,31625,31626,31627,31628,31630,31631,31633,31634,31635,31638,31640,31641,31642,31643,31646,31647,31648,31651,31652,31653,31662,31663,31664,31666,31667,31669,31670,31671,31673,31674,31675,31676,31677,31678,31679,31680,31682,31683,31684,22466,32831,26775,24037,25915,21151,24685,40858,20379,36524,20844,23467,24339,24041,27742,25329,36129,20849,38057,21246,27807,33503,29399,22434,26500,36141,22815,36764,33735,21653,31629,20272,27837,23396,22993,40723,21476,34506,39592,35895,32929,25925,39038,22266,38599,21038,29916,21072,23521,25346,35074,20054,25296,24618,26874,20851,23448,20896,35266,31649,39302,32592,24815,28748,36143,20809,24191,36891,29808,35268,22317,30789,24402,40863,38394,36712,39740,35809,30328,26690,26588,36330,36149,21053,36746,28378,26829,38149,37101,22269,26524,35065,36807,21704,31685,31688,31689,31690,31691,31693,31694,31695,31696,31698,31700,31701,31702,31703,31704,31707,31708,31710,31711,31712,31714,31715,31716,31719,31720,31721,31723,31724,31725,31727,31728,31730,31731,31732,31733,31734,31736,31737,31738,31739,31741,31743,31744,31745,31746,31747,31748,31749,31750,31752,31753,31754,31757,31758,31760,31761,31762,31763,31764,31765,31767,31768,31769,31770,31771,31772,31773,31774,31776,31777,31778,31779,31780,31781,31784,31785,31787,31788,31789,31790,31791,31792,31793,31794,31795,31796,31797,31798,31799,31801,31802,31803,31804,31805,31806,31810,39608,23401,28023,27686,20133,23475,39559,37219,25000,37039,38889,21547,28085,23506,20989,21898,32597,32752,25788,25421,26097,25022,24717,28938,27735,27721,22831,26477,33322,22741,22158,35946,27627,37085,22909,32791,21495,28009,21621,21917,33655,33743,26680,31166,21644,20309,21512,30418,35977,38402,27827,28088,36203,35088,40548,36154,22079,40657,30165,24456,29408,24680,21756,20136,27178,34913,24658,36720,21700,28888,34425,40511,27946,23439,24344,32418,21897,20399,29492,21564,21402,20505,21518,21628,20046,24573,29786,22774,33899,32993,34676,29392,31946,28246,31811,31812,31813,31814,31815,31816,31817,31818,31819,31820,31822,31823,31824,31825,31826,31827,31828,31829,31830,31831,31832,31833,31834,31835,31836,31837,31838,31839,31840,31841,31842,31843,31844,31845,31846,31847,31848,31849,31850,31851,31852,31853,31854,31855,31856,31857,31858,31861,31862,31863,31864,31865,31866,31870,31871,31872,31873,31874,31875,31876,31877,31878,31879,31880,31882,31883,31884,31885,31886,31887,31888,31891,31892,31894,31897,31898,31899,31904,31905,31907,31910,31911,31912,31913,31915,31916,31917,31919,31920,31924,31925,31926,31927,31928,31930,31931,24359,34382,21804,25252,20114,27818,25143,33457,21719,21326,29502,28369,30011,21010,21270,35805,27088,24458,24576,28142,22351,27426,29615,26707,36824,32531,25442,24739,21796,30186,35938,28949,28067,23462,24187,33618,24908,40644,30970,34647,31783,30343,20976,24822,29004,26179,24140,24653,35854,28784,25381,36745,24509,24674,34516,22238,27585,24724,24935,21321,24800,26214,36159,31229,20250,28905,27719,35763,35826,32472,33636,26127,23130,39746,27985,28151,35905,27963,20249,28779,33719,25110,24785,38669,36135,31096,20987,22334,22522,26426,30072,31293,31215,31637,31935,31936,31938,31939,31940,31942,31945,31947,31950,31951,31952,31953,31954,31955,31956,31960,31962,31963,31965,31966,31969,31970,31971,31972,31973,31974,31975,31977,31978,31979,31980,31981,31982,31984,31985,31986,31987,31988,31989,31990,31991,31993,31994,31996,31997,31998,31999,32000,32001,32002,32003,32004,32005,32006,32007,32008,32009,32011,32012,32013,32014,32015,32016,32017,32018,32019,32020,32021,32022,32023,32024,32025,32026,32027,32028,32029,32030,32031,32033,32035,32036,32037,32038,32040,32041,32042,32044,32045,32046,32048,32049,32050,32051,32052,32053,32054,32908,39269,36857,28608,35749,40481,23020,32489,32521,21513,26497,26840,36753,31821,38598,21450,24613,30142,27762,21363,23241,32423,25380,20960,33034,24049,34015,25216,20864,23395,20238,31085,21058,24760,27982,23492,23490,35745,35760,26082,24524,38469,22931,32487,32426,22025,26551,22841,20339,23478,21152,33626,39050,36158,30002,38078,20551,31292,20215,26550,39550,23233,27516,30417,22362,23574,31546,38388,29006,20860,32937,33392,22904,32516,33575,26816,26604,30897,30839,25315,25441,31616,20461,21098,20943,33616,27099,37492,36341,36145,35265,38190,31661,20214,32055,32056,32057,32058,32059,32060,32061,32062,32063,32064,32065,32066,32067,32068,32069,32070,32071,32072,32073,32074,32075,32076,32077,32078,32079,32080,32081,32082,32083,32084,32085,32086,32087,32088,32089,32090,32091,32092,32093,32094,32095,32096,32097,32098,32099,32100,32101,32102,32103,32104,32105,32106,32107,32108,32109,32111,32112,32113,32114,32115,32116,32117,32118,32120,32121,32122,32123,32124,32125,32126,32127,32128,32129,32130,32131,32132,32133,32134,32135,32136,32137,32138,32139,32140,32141,32142,32143,32144,32145,32146,32147,32148,32149,32150,32151,32152,20581,33328,21073,39279,28176,28293,28071,24314,20725,23004,23558,27974,27743,30086,33931,26728,22870,35762,21280,37233,38477,34121,26898,30977,28966,33014,20132,37066,27975,39556,23047,22204,25605,38128,30699,20389,33050,29409,35282,39290,32564,32478,21119,25945,37237,36735,36739,21483,31382,25581,25509,30342,31224,34903,38454,25130,21163,33410,26708,26480,25463,30571,31469,27905,32467,35299,22992,25106,34249,33445,30028,20511,20171,30117,35819,23626,24062,31563,26020,37329,20170,27941,35167,32039,38182,20165,35880,36827,38771,26187,31105,36817,28908,28024,32153,32154,32155,32156,32157,32158,32159,32160,32161,32162,32163,32164,32165,32167,32168,32169,32170,32171,32172,32173,32175,32176,32177,32178,32179,32180,32181,32182,32183,32184,32185,32186,32187,32188,32189,32190,32191,32192,32193,32194,32195,32196,32197,32198,32199,32200,32201,32202,32203,32204,32205,32206,32207,32208,32209,32210,32211,32212,32213,32214,32215,32216,32217,32218,32219,32220,32221,32222,32223,32224,32225,32226,32227,32228,32229,32230,32231,32232,32233,32234,32235,32236,32237,32238,32239,32240,32241,32242,32243,32244,32245,32246,32247,32248,32249,32250,23613,21170,33606,20834,33550,30555,26230,40120,20140,24778,31934,31923,32463,20117,35686,26223,39048,38745,22659,25964,38236,24452,30153,38742,31455,31454,20928,28847,31384,25578,31350,32416,29590,38893,20037,28792,20061,37202,21417,25937,26087,33276,33285,21646,23601,30106,38816,25304,29401,30141,23621,39545,33738,23616,21632,30697,20030,27822,32858,25298,25454,24040,20855,36317,36382,38191,20465,21477,24807,28844,21095,25424,40515,23071,20518,30519,21367,32482,25733,25899,25225,25496,20500,29237,35273,20915,35776,32477,22343,33740,38055,20891,21531,23803,32251,32252,32253,32254,32255,32256,32257,32258,32259,32260,32261,32262,32263,32264,32265,32266,32267,32268,32269,32270,32271,32272,32273,32274,32275,32276,32277,32278,32279,32280,32281,32282,32283,32284,32285,32286,32287,32288,32289,32290,32291,32292,32293,32294,32295,32296,32297,32298,32299,32300,32301,32302,32303,32304,32305,32306,32307,32308,32309,32310,32311,32312,32313,32314,32316,32317,32318,32319,32320,32322,32323,32324,32325,32326,32328,32329,32330,32331,32332,32333,32334,32335,32336,32337,32338,32339,32340,32341,32342,32343,32344,32345,32346,32347,32348,32349,20426,31459,27994,37089,39567,21888,21654,21345,21679,24320,25577,26999,20975,24936,21002,22570,21208,22350,30733,30475,24247,24951,31968,25179,25239,20130,28821,32771,25335,28900,38752,22391,33499,26607,26869,30933,39063,31185,22771,21683,21487,28212,20811,21051,23458,35838,32943,21827,22438,24691,22353,21549,31354,24656,23380,25511,25248,21475,25187,23495,26543,21741,31391,33510,37239,24211,35044,22840,22446,25358,36328,33007,22359,31607,20393,24555,23485,27454,21281,31568,29378,26694,30719,30518,26103,20917,20111,30420,23743,31397,33909,22862,39745,20608,32350,32351,32352,32353,32354,32355,32356,32357,32358,32359,32360,32361,32362,32363,32364,32365,32366,32367,32368,32369,32370,32371,32372,32373,32374,32375,32376,32377,32378,32379,32380,32381,32382,32383,32384,32385,32387,32388,32389,32390,32391,32392,32393,32394,32395,32396,32397,32398,32399,32400,32401,32402,32403,32404,32405,32406,32407,32408,32409,32410,32412,32413,32414,32430,32436,32443,32444,32470,32484,32492,32505,32522,32528,32542,32567,32569,32571,32572,32573,32574,32575,32576,32577,32579,32582,32583,32584,32585,32586,32587,32588,32589,32590,32591,32594,32595,39304,24871,28291,22372,26118,25414,22256,25324,25193,24275,38420,22403,25289,21895,34593,33098,36771,21862,33713,26469,36182,34013,23146,26639,25318,31726,38417,20848,28572,35888,25597,35272,25042,32518,28866,28389,29701,27028,29436,24266,37070,26391,28010,25438,21171,29282,32769,20332,23013,37226,28889,28061,21202,20048,38647,38253,34174,30922,32047,20769,22418,25794,32907,31867,27882,26865,26974,20919,21400,26792,29313,40654,31729,29432,31163,28435,29702,26446,37324,40100,31036,33673,33620,21519,26647,20029,21385,21169,30782,21382,21033,20616,20363,20432,32598,32601,32603,32604,32605,32606,32608,32611,32612,32613,32614,32615,32619,32620,32621,32623,32624,32627,32629,32630,32631,32632,32634,32635,32636,32637,32639,32640,32642,32643,32644,32645,32646,32647,32648,32649,32651,32653,32655,32656,32657,32658,32659,32661,32662,32663,32664,32665,32667,32668,32672,32674,32675,32677,32678,32680,32681,32682,32683,32684,32685,32686,32689,32691,32692,32693,32694,32695,32698,32699,32702,32704,32706,32707,32708,32710,32711,32712,32713,32715,32717,32719,32720,32721,32722,32723,32726,32727,32729,32730,32731,32732,32733,32734,32738,32739,30178,31435,31890,27813,38582,21147,29827,21737,20457,32852,33714,36830,38256,24265,24604,28063,24088,25947,33080,38142,24651,28860,32451,31918,20937,26753,31921,33391,20004,36742,37327,26238,20142,35845,25769,32842,20698,30103,29134,23525,36797,28518,20102,25730,38243,24278,26009,21015,35010,28872,21155,29454,29747,26519,30967,38678,20020,37051,40158,28107,20955,36161,21533,25294,29618,33777,38646,40836,38083,20278,32666,20940,28789,38517,23725,39046,21478,20196,28316,29705,27060,30827,39311,30041,21016,30244,27969,26611,20845,40857,32843,21657,31548,31423,32740,32743,32744,32746,32747,32748,32749,32751,32754,32756,32757,32758,32759,32760,32761,32762,32765,32766,32767,32770,32775,32776,32777,32778,32782,32783,32785,32787,32794,32795,32797,32798,32799,32801,32803,32804,32811,32812,32813,32814,32815,32816,32818,32820,32825,32826,32828,32830,32832,32833,32836,32837,32839,32840,32841,32846,32847,32848,32849,32851,32853,32854,32855,32857,32859,32860,32861,32862,32863,32864,32865,32866,32867,32868,32869,32870,32871,32872,32875,32876,32877,32878,32879,32880,32882,32883,32884,32885,32886,32887,32888,32889,32890,32891,32892,32893,38534,22404,25314,38471,27004,23044,25602,31699,28431,38475,33446,21346,39045,24208,28809,25523,21348,34383,40065,40595,30860,38706,36335,36162,40575,28510,31108,24405,38470,25134,39540,21525,38109,20387,26053,23653,23649,32533,34385,27695,24459,29575,28388,32511,23782,25371,23402,28390,21365,20081,25504,30053,25249,36718,20262,20177,27814,32438,35770,33821,34746,32599,36923,38179,31657,39585,35064,33853,27931,39558,32476,22920,40635,29595,30721,34434,39532,39554,22043,21527,22475,20080,40614,21334,36808,33033,30610,39314,34542,28385,34067,26364,24930,28459,32894,32897,32898,32901,32904,32906,32909,32910,32911,32912,32913,32914,32916,32917,32919,32921,32926,32931,32934,32935,32936,32940,32944,32947,32949,32950,32952,32953,32955,32965,32967,32968,32969,32970,32971,32975,32976,32977,32978,32979,32980,32981,32984,32991,32992,32994,32995,32998,33006,33013,33015,33017,33019,33022,33023,33024,33025,33027,33028,33029,33031,33032,33035,33036,33045,33047,33049,33051,33052,33053,33055,33056,33057,33058,33059,33060,33061,33062,33063,33064,33065,33066,33067,33069,33070,33072,33075,33076,33077,33079,33081,33082,33083,33084,33085,33087,35881,33426,33579,30450,27667,24537,33725,29483,33541,38170,27611,30683,38086,21359,33538,20882,24125,35980,36152,20040,29611,26522,26757,37238,38665,29028,27809,30473,23186,38209,27599,32654,26151,23504,22969,23194,38376,38391,20204,33804,33945,27308,30431,38192,29467,26790,23391,30511,37274,38753,31964,36855,35868,24357,31859,31192,35269,27852,34588,23494,24130,26825,30496,32501,20885,20813,21193,23081,32517,38754,33495,25551,30596,34256,31186,28218,24217,22937,34065,28781,27665,25279,30399,25935,24751,38397,26126,34719,40483,38125,21517,21629,35884,25720,33088,33089,33090,33091,33092,33093,33095,33097,33101,33102,33103,33106,33110,33111,33112,33115,33116,33117,33118,33119,33121,33122,33123,33124,33126,33128,33130,33131,33132,33135,33138,33139,33141,33142,33143,33144,33153,33155,33156,33157,33158,33159,33161,33163,33164,33165,33166,33168,33170,33171,33172,33173,33174,33175,33177,33178,33182,33183,33184,33185,33186,33188,33189,33191,33193,33195,33196,33197,33198,33199,33200,33201,33202,33204,33205,33206,33207,33208,33209,33212,33213,33214,33215,33220,33221,33223,33224,33225,33227,33229,33230,33231,33232,33233,33234,33235,25721,34321,27169,33180,30952,25705,39764,25273,26411,33707,22696,40664,27819,28448,23518,38476,35851,29279,26576,25287,29281,20137,22982,27597,22675,26286,24149,21215,24917,26408,30446,30566,29287,31302,25343,21738,21584,38048,37027,23068,32435,27670,20035,22902,32784,22856,21335,30007,38590,22218,25376,33041,24700,38393,28118,21602,39297,20869,23273,33021,22958,38675,20522,27877,23612,25311,20320,21311,33147,36870,28346,34091,25288,24180,30910,25781,25467,24565,23064,37247,40479,23615,25423,32834,23421,21870,38218,38221,28037,24744,26592,29406,20957,23425,33236,33237,33238,33239,33240,33241,33242,33243,33244,33245,33246,33247,33248,33249,33250,33252,33253,33254,33256,33257,33259,33262,33263,33264,33265,33266,33269,33270,33271,33272,33273,33274,33277,33279,33283,33287,33288,33289,33290,33291,33294,33295,33297,33299,33301,33302,33303,33304,33305,33306,33309,33312,33316,33317,33318,33319,33321,33326,33330,33338,33340,33341,33343,33344,33345,33346,33347,33349,33350,33352,33354,33356,33357,33358,33360,33361,33362,33363,33364,33365,33366,33367,33369,33371,33372,33373,33374,33376,33377,33378,33379,33380,33381,33382,33383,33385,25319,27870,29275,25197,38062,32445,33043,27987,20892,24324,22900,21162,24594,22899,26262,34384,30111,25386,25062,31983,35834,21734,27431,40485,27572,34261,21589,20598,27812,21866,36276,29228,24085,24597,29750,25293,25490,29260,24472,28227,27966,25856,28504,30424,30928,30460,30036,21028,21467,20051,24222,26049,32810,32982,25243,21638,21032,28846,34957,36305,27873,21624,32986,22521,35060,36180,38506,37197,20329,27803,21943,30406,30768,25256,28921,28558,24429,34028,26842,30844,31735,33192,26379,40527,25447,30896,22383,30738,38713,25209,25259,21128,29749,27607,33386,33387,33388,33389,33393,33397,33398,33399,33400,33403,33404,33408,33409,33411,33413,33414,33415,33417,33420,33424,33427,33428,33429,33430,33434,33435,33438,33440,33442,33443,33447,33458,33461,33462,33466,33467,33468,33471,33472,33474,33475,33477,33478,33481,33488,33494,33497,33498,33501,33506,33511,33512,33513,33514,33516,33517,33518,33520,33522,33523,33525,33526,33528,33530,33532,33533,33534,33535,33536,33546,33547,33549,33552,33554,33555,33558,33560,33561,33565,33566,33567,33568,33569,33570,33571,33572,33573,33574,33577,33578,33582,33584,33586,33591,33595,33597,21860,33086,30130,30382,21305,30174,20731,23617,35692,31687,20559,29255,39575,39128,28418,29922,31080,25735,30629,25340,39057,36139,21697,32856,20050,22378,33529,33805,24179,20973,29942,35780,23631,22369,27900,39047,23110,30772,39748,36843,31893,21078,25169,38138,20166,33670,33889,33769,33970,22484,26420,22275,26222,28006,35889,26333,28689,26399,27450,26646,25114,22971,19971,20932,28422,26578,27791,20854,26827,22855,27495,30054,23822,33040,40784,26071,31048,31041,39569,36215,23682,20062,20225,21551,22865,30732,22120,27668,36804,24323,27773,27875,35755,25488,33598,33599,33601,33602,33604,33605,33608,33610,33611,33612,33613,33614,33619,33621,33622,33623,33624,33625,33629,33634,33648,33649,33650,33651,33652,33653,33654,33657,33658,33662,33663,33664,33665,33666,33667,33668,33671,33672,33674,33675,33676,33677,33679,33680,33681,33684,33685,33686,33687,33689,33690,33693,33695,33697,33698,33699,33700,33701,33702,33703,33708,33709,33710,33711,33717,33723,33726,33727,33730,33731,33732,33734,33736,33737,33739,33741,33742,33744,33745,33746,33747,33749,33751,33753,33754,33755,33758,33762,33763,33764,33766,33767,33768,33771,33772,33773,24688,27965,29301,25190,38030,38085,21315,36801,31614,20191,35878,20094,40660,38065,38067,21069,28508,36963,27973,35892,22545,23884,27424,27465,26538,21595,33108,32652,22681,34103,24378,25250,27207,38201,25970,24708,26725,30631,20052,20392,24039,38808,25772,32728,23789,20431,31373,20999,33540,19988,24623,31363,38054,20405,20146,31206,29748,21220,33465,25810,31165,23517,27777,38738,36731,27682,20542,21375,28165,25806,26228,27696,24773,39031,35831,24198,29756,31351,31179,19992,37041,29699,27714,22234,37195,27845,36235,21306,34502,26354,36527,23624,39537,28192,33774,33775,33779,33780,33781,33782,33783,33786,33787,33788,33790,33791,33792,33794,33797,33799,33800,33801,33802,33808,33810,33811,33812,33813,33814,33815,33817,33818,33819,33822,33823,33824,33825,33826,33827,33833,33834,33835,33836,33837,33838,33839,33840,33842,33843,33844,33845,33846,33847,33849,33850,33851,33854,33855,33856,33857,33858,33859,33860,33861,33863,33864,33865,33866,33867,33868,33869,33870,33871,33872,33874,33875,33876,33877,33878,33880,33885,33886,33887,33888,33890,33892,33893,33894,33895,33896,33898,33902,33903,33904,33906,33908,33911,33913,33915,33916,21462,23094,40843,36259,21435,22280,39079,26435,37275,27849,20840,30154,25331,29356,21048,21149,32570,28820,30264,21364,40522,27063,30830,38592,35033,32676,28982,29123,20873,26579,29924,22756,25880,22199,35753,39286,25200,32469,24825,28909,22764,20161,20154,24525,38887,20219,35748,20995,22922,32427,25172,20173,26085,25102,33592,33993,33635,34701,29076,28342,23481,32466,20887,25545,26580,32905,33593,34837,20754,23418,22914,36785,20083,27741,20837,35109,36719,38446,34122,29790,38160,38384,28070,33509,24369,25746,27922,33832,33134,40131,22622,36187,19977,21441,33917,33918,33919,33920,33921,33923,33924,33925,33926,33930,33933,33935,33936,33937,33938,33939,33940,33941,33942,33944,33946,33947,33949,33950,33951,33952,33954,33955,33956,33957,33958,33959,33960,33961,33962,33963,33964,33965,33966,33968,33969,33971,33973,33974,33975,33979,33980,33982,33984,33986,33987,33989,33990,33991,33992,33995,33996,33998,33999,34002,34004,34005,34007,34008,34009,34010,34011,34012,34014,34017,34018,34020,34023,34024,34025,34026,34027,34029,34030,34031,34033,34034,34035,34036,34037,34038,34039,34040,34041,34042,34043,34045,34046,34048,34049,34050,20254,25955,26705,21971,20007,25620,39578,25195,23234,29791,33394,28073,26862,20711,33678,30722,26432,21049,27801,32433,20667,21861,29022,31579,26194,29642,33515,26441,23665,21024,29053,34923,38378,38485,25797,36193,33203,21892,27733,25159,32558,22674,20260,21830,36175,26188,19978,23578,35059,26786,25422,31245,28903,33421,21242,38902,23569,21736,37045,32461,22882,36170,34503,33292,33293,36198,25668,23556,24913,28041,31038,35774,30775,30003,21627,20280,36523,28145,23072,32453,31070,27784,23457,23158,29978,32958,24910,28183,22768,29983,29989,29298,21319,32499,34051,34052,34053,34054,34055,34056,34057,34058,34059,34061,34062,34063,34064,34066,34068,34069,34070,34072,34073,34075,34076,34077,34078,34080,34082,34083,34084,34085,34086,34087,34088,34089,34090,34093,34094,34095,34096,34097,34098,34099,34100,34101,34102,34110,34111,34112,34113,34114,34116,34117,34118,34119,34123,34124,34125,34126,34127,34128,34129,34130,34131,34132,34133,34135,34136,34138,34139,34140,34141,34143,34144,34145,34146,34147,34149,34150,34151,34153,34154,34155,34156,34157,34158,34159,34160,34161,34163,34165,34166,34167,34168,34172,34173,34175,34176,34177,30465,30427,21097,32988,22307,24072,22833,29422,26045,28287,35799,23608,34417,21313,30707,25342,26102,20160,39135,34432,23454,35782,21490,30690,20351,23630,39542,22987,24335,31034,22763,19990,26623,20107,25325,35475,36893,21183,26159,21980,22124,36866,20181,20365,37322,39280,27663,24066,24643,23460,35270,35797,25910,25163,39318,23432,23551,25480,21806,21463,30246,20861,34092,26530,26803,27530,25234,36755,21460,33298,28113,30095,20070,36174,23408,29087,34223,26257,26329,32626,34560,40653,40736,23646,26415,36848,26641,26463,25101,31446,22661,24246,25968,28465,34178,34179,34182,34184,34185,34186,34187,34188,34189,34190,34192,34193,34194,34195,34196,34197,34198,34199,34200,34201,34202,34205,34206,34207,34208,34209,34210,34211,34213,34214,34215,34217,34219,34220,34221,34225,34226,34227,34228,34229,34230,34232,34234,34235,34236,34237,34238,34239,34240,34242,34243,34244,34245,34246,34247,34248,34250,34251,34252,34253,34254,34257,34258,34260,34262,34263,34264,34265,34266,34267,34269,34270,34271,34272,34273,34274,34275,34277,34278,34279,34280,34282,34283,34284,34285,34286,34287,34288,34289,34290,34291,34292,34293,34294,34295,34296,24661,21047,32781,25684,34928,29993,24069,26643,25332,38684,21452,29245,35841,27700,30561,31246,21550,30636,39034,33308,35828,30805,26388,28865,26031,25749,22070,24605,31169,21496,19997,27515,32902,23546,21987,22235,20282,20284,39282,24051,26494,32824,24578,39042,36865,23435,35772,35829,25628,33368,25822,22013,33487,37221,20439,32032,36895,31903,20723,22609,28335,23487,35785,32899,37240,33948,31639,34429,38539,38543,32485,39635,30862,23681,31319,36930,38567,31071,23385,25439,31499,34001,26797,21766,32553,29712,32034,38145,25152,22604,20182,23427,22905,22612,34297,34298,34300,34301,34302,34304,34305,34306,34307,34308,34310,34311,34312,34313,34314,34315,34316,34317,34318,34319,34320,34322,34323,34324,34325,34327,34328,34329,34330,34331,34332,34333,34334,34335,34336,34337,34338,34339,34340,34341,34342,34344,34346,34347,34348,34349,34350,34351,34352,34353,34354,34355,34356,34357,34358,34359,34361,34362,34363,34365,34366,34367,34368,34369,34370,34371,34372,34373,34374,34375,34376,34377,34378,34379,34380,34386,34387,34389,34390,34391,34392,34393,34395,34396,34397,34399,34400,34401,34403,34404,34405,34406,34407,34408,34409,34410,29549,25374,36427,36367,32974,33492,25260,21488,27888,37214,22826,24577,27760,22349,25674,36138,30251,28393,22363,27264,30192,28525,35885,35848,22374,27631,34962,30899,25506,21497,28845,27748,22616,25642,22530,26848,33179,21776,31958,20504,36538,28108,36255,28907,25487,28059,28372,32486,33796,26691,36867,28120,38518,35752,22871,29305,34276,33150,30140,35466,26799,21076,36386,38161,25552,39064,36420,21884,20307,26367,22159,24789,28053,21059,23625,22825,28155,22635,30000,29980,24684,33300,33094,25361,26465,36834,30522,36339,36148,38081,24086,21381,21548,28867,34413,34415,34416,34418,34419,34420,34421,34422,34423,34424,34435,34436,34437,34438,34439,34440,34441,34446,34447,34448,34449,34450,34452,34454,34455,34456,34457,34458,34459,34462,34463,34464,34465,34466,34469,34470,34475,34477,34478,34482,34483,34487,34488,34489,34491,34492,34493,34494,34495,34497,34498,34499,34501,34504,34508,34509,34514,34515,34517,34518,34519,34522,34524,34525,34528,34529,34530,34531,34533,34534,34535,34536,34538,34539,34540,34543,34549,34550,34551,34554,34555,34556,34557,34559,34561,34564,34565,34566,34571,34572,34574,34575,34576,34577,34580,34582,27712,24311,20572,20141,24237,25402,33351,36890,26704,37230,30643,21516,38108,24420,31461,26742,25413,31570,32479,30171,20599,25237,22836,36879,20984,31171,31361,22270,24466,36884,28034,23648,22303,21520,20820,28237,22242,25512,39059,33151,34581,35114,36864,21534,23663,33216,25302,25176,33073,40501,38464,39534,39548,26925,22949,25299,21822,25366,21703,34521,27964,23043,29926,34972,27498,22806,35916,24367,28286,29609,39037,20024,28919,23436,30871,25405,26202,30358,24779,23451,23113,19975,33109,27754,29579,20129,26505,32593,24448,26106,26395,24536,22916,23041,34585,34587,34589,34591,34592,34596,34598,34599,34600,34602,34603,34604,34605,34607,34608,34610,34611,34613,34614,34616,34617,34618,34620,34621,34624,34625,34626,34627,34628,34629,34630,34634,34635,34637,34639,34640,34641,34642,34644,34645,34646,34648,34650,34651,34652,34653,34654,34655,34657,34658,34662,34663,34664,34665,34666,34667,34668,34669,34671,34673,34674,34675,34677,34679,34680,34681,34682,34687,34688,34689,34692,34694,34695,34697,34698,34700,34702,34703,34704,34705,34706,34708,34709,34710,34712,34713,34714,34715,34716,34717,34718,34720,34721,34722,34723,34724,24013,24494,21361,38886,36829,26693,22260,21807,24799,20026,28493,32500,33479,33806,22996,20255,20266,23614,32428,26410,34074,21619,30031,32963,21890,39759,20301,28205,35859,23561,24944,21355,30239,28201,34442,25991,38395,32441,21563,31283,32010,38382,21985,32705,29934,25373,34583,28065,31389,25105,26017,21351,25569,27779,24043,21596,38056,20044,27745,35820,23627,26080,33436,26791,21566,21556,27595,27494,20116,25410,21320,33310,20237,20398,22366,25098,38654,26212,29289,21247,21153,24735,35823,26132,29081,26512,35199,30802,30717,26224,22075,21560,38177,29306,34725,34726,34727,34729,34730,34734,34736,34737,34738,34740,34742,34743,34744,34745,34747,34748,34750,34751,34753,34754,34755,34756,34757,34759,34760,34761,34764,34765,34766,34767,34768,34772,34773,34774,34775,34776,34777,34778,34780,34781,34782,34783,34785,34786,34787,34788,34790,34791,34792,34793,34795,34796,34797,34799,34800,34801,34802,34803,34804,34805,34806,34807,34808,34810,34811,34812,34813,34815,34816,34817,34818,34820,34821,34822,34823,34824,34825,34827,34828,34829,34830,34831,34832,34833,34834,34836,34839,34840,34841,34842,34844,34845,34846,34847,34848,34851,31232,24687,24076,24713,33181,22805,24796,29060,28911,28330,27728,29312,27268,34989,24109,20064,23219,21916,38115,27927,31995,38553,25103,32454,30606,34430,21283,38686,36758,26247,23777,20384,29421,19979,21414,22799,21523,25472,38184,20808,20185,40092,32420,21688,36132,34900,33335,38386,28046,24358,23244,26174,38505,29616,29486,21439,33146,39301,32673,23466,38519,38480,32447,30456,21410,38262,39321,31665,35140,28248,20065,32724,31077,35814,24819,21709,20139,39033,24055,27233,20687,21521,35937,33831,30813,38660,21066,21742,22179,38144,28040,23477,28102,26195,34852,34853,34854,34855,34856,34857,34858,34859,34860,34861,34862,34863,34864,34865,34867,34868,34869,34870,34871,34872,34874,34875,34877,34878,34879,34881,34882,34883,34886,34887,34888,34889,34890,34891,34894,34895,34896,34897,34898,34899,34901,34902,34904,34906,34907,34908,34909,34910,34911,34912,34918,34919,34922,34925,34927,34929,34931,34932,34933,34934,34936,34937,34938,34939,34940,34944,34947,34950,34951,34953,34954,34956,34958,34959,34960,34961,34963,34964,34965,34967,34968,34969,34970,34971,34973,34974,34975,34976,34977,34979,34981,34982,34983,34984,34985,34986,23567,23389,26657,32918,21880,31505,25928,26964,20123,27463,34638,38795,21327,25375,25658,37034,26012,32961,35856,20889,26800,21368,34809,25032,27844,27899,35874,23633,34218,33455,38156,27427,36763,26032,24571,24515,20449,34885,26143,33125,29481,24826,20852,21009,22411,24418,37026,34892,37266,24184,26447,24615,22995,20804,20982,33016,21256,27769,38596,29066,20241,20462,32670,26429,21957,38152,31168,34966,32483,22687,25100,38656,34394,22040,39035,24464,35768,33988,37207,21465,26093,24207,30044,24676,32110,23167,32490,32493,36713,21927,23459,24748,26059,29572,34988,34990,34991,34992,34994,34995,34996,34997,34998,35000,35001,35002,35003,35005,35006,35007,35008,35011,35012,35015,35016,35018,35019,35020,35021,35023,35024,35025,35027,35030,35031,35034,35035,35036,35037,35038,35040,35041,35046,35047,35049,35050,35051,35052,35053,35054,35055,35058,35061,35062,35063,35066,35067,35069,35071,35072,35073,35075,35076,35077,35078,35079,35080,35081,35083,35084,35085,35086,35087,35089,35092,35093,35094,35095,35096,35100,35101,35102,35103,35104,35106,35107,35108,35110,35111,35112,35113,35116,35117,35118,35119,35121,35122,35123,35125,35127,36873,30307,30505,32474,38772,34203,23398,31348,38634,34880,21195,29071,24490,26092,35810,23547,39535,24033,27529,27739,35757,35759,36874,36805,21387,25276,40486,40493,21568,20011,33469,29273,34460,23830,34905,28079,38597,21713,20122,35766,28937,21693,38409,28895,28153,30416,20005,30740,34578,23721,24310,35328,39068,38414,28814,27839,22852,25513,30524,34893,28436,33395,22576,29141,21388,30746,38593,21761,24422,28976,23476,35866,39564,27523,22830,40495,31207,26472,25196,20335,30113,32650,27915,38451,27687,20208,30162,20859,26679,28478,36992,33136,22934,29814,35128,35129,35130,35131,35132,35133,35134,35135,35136,35138,35139,35141,35142,35143,35144,35145,35146,35147,35148,35149,35150,35151,35152,35153,35154,35155,35156,35157,35158,35159,35160,35161,35162,35163,35164,35165,35168,35169,35170,35171,35172,35173,35175,35176,35177,35178,35179,35180,35181,35182,35183,35184,35185,35186,35187,35188,35189,35190,35191,35192,35193,35194,35196,35197,35198,35200,35202,35204,35205,35207,35208,35209,35210,35211,35212,35213,35214,35215,35216,35217,35218,35219,35220,35221,35222,35223,35224,35225,35226,35227,35228,35229,35230,35231,35232,35233,25671,23591,36965,31377,35875,23002,21676,33280,33647,35201,32768,26928,22094,32822,29239,37326,20918,20063,39029,25494,19994,21494,26355,33099,22812,28082,19968,22777,21307,25558,38129,20381,20234,34915,39056,22839,36951,31227,20202,33008,30097,27778,23452,23016,24413,26885,34433,20506,24050,20057,30691,20197,33402,25233,26131,37009,23673,20159,24441,33222,36920,32900,30123,20134,35028,24847,27589,24518,20041,30410,28322,35811,35758,35850,35793,24322,32764,32716,32462,33589,33643,22240,27575,38899,38452,23035,21535,38134,28139,23493,39278,23609,24341,38544,35234,35235,35236,35237,35238,35239,35240,35241,35242,35243,35244,35245,35246,35247,35248,35249,35250,35251,35252,35253,35254,35255,35256,35257,35258,35259,35260,35261,35262,35263,35264,35267,35277,35283,35284,35285,35287,35288,35289,35291,35293,35295,35296,35297,35298,35300,35303,35304,35305,35306,35308,35309,35310,35312,35313,35314,35316,35317,35318,35319,35320,35321,35322,35323,35324,35325,35326,35327,35329,35330,35331,35332,35333,35334,35336,35337,35338,35339,35340,35341,35342,35343,35344,35345,35346,35347,35348,35349,35350,35351,35352,35353,35354,35355,35356,35357,21360,33521,27185,23156,40560,24212,32552,33721,33828,33829,33639,34631,36814,36194,30408,24433,39062,30828,26144,21727,25317,20323,33219,30152,24248,38605,36362,34553,21647,27891,28044,27704,24703,21191,29992,24189,20248,24736,24551,23588,30001,37038,38080,29369,27833,28216,37193,26377,21451,21491,20305,37321,35825,21448,24188,36802,28132,20110,30402,27014,34398,24858,33286,20313,20446,36926,40060,24841,28189,28180,38533,20104,23089,38632,19982,23679,31161,23431,35821,32701,29577,22495,33419,37057,21505,36935,21947,23786,24481,24840,27442,29425,32946,35465,35358,35359,35360,35361,35362,35363,35364,35365,35366,35367,35368,35369,35370,35371,35372,35373,35374,35375,35376,35377,35378,35379,35380,35381,35382,35383,35384,35385,35386,35387,35388,35389,35391,35392,35393,35394,35395,35396,35397,35398,35399,35401,35402,35403,35404,35405,35406,35407,35408,35409,35410,35411,35412,35413,35414,35415,35416,35417,35418,35419,35420,35421,35422,35423,35424,35425,35426,35427,35428,35429,35430,35431,35432,35433,35434,35435,35436,35437,35438,35439,35440,35441,35442,35443,35444,35445,35446,35447,35448,35450,35451,35452,35453,35454,35455,35456,28020,23507,35029,39044,35947,39533,40499,28170,20900,20803,22435,34945,21407,25588,36757,22253,21592,22278,29503,28304,32536,36828,33489,24895,24616,38498,26352,32422,36234,36291,38053,23731,31908,26376,24742,38405,32792,20113,37095,21248,38504,20801,36816,34164,37213,26197,38901,23381,21277,30776,26434,26685,21705,28798,23472,36733,20877,22312,21681,25874,26242,36190,36163,33039,33900,36973,31967,20991,34299,26531,26089,28577,34468,36481,22122,36896,30338,28790,29157,36131,25321,21017,27901,36156,24590,22686,24974,26366,36192,25166,21939,28195,26413,36711,35457,35458,35459,35460,35461,35462,35463,35464,35467,35468,35469,35470,35471,35472,35473,35474,35476,35477,35478,35479,35480,35481,35482,35483,35484,35485,35486,35487,35488,35489,35490,35491,35492,35493,35494,35495,35496,35497,35498,35499,35500,35501,35502,35503,35504,35505,35506,35507,35508,35509,35510,35511,35512,35513,35514,35515,35516,35517,35518,35519,35520,35521,35522,35523,35524,35525,35526,35527,35528,35529,35530,35531,35532,35533,35534,35535,35536,35537,35538,35539,35540,35541,35542,35543,35544,35545,35546,35547,35548,35549,35550,35551,35552,35553,35554,35555,38113,38392,30504,26629,27048,21643,20045,28856,35784,25688,25995,23429,31364,20538,23528,30651,27617,35449,31896,27838,30415,26025,36759,23853,23637,34360,26632,21344,25112,31449,28251,32509,27167,31456,24432,28467,24352,25484,28072,26454,19976,24080,36134,20183,32960,30260,38556,25307,26157,25214,27836,36213,29031,32617,20806,32903,21484,36974,25240,21746,34544,36761,32773,38167,34071,36825,27993,29645,26015,30495,29956,30759,33275,36126,38024,20390,26517,30137,35786,38663,25391,38215,38453,33976,25379,30529,24449,29424,20105,24596,25972,25327,27491,25919,35556,35557,35558,35559,35560,35561,35562,35563,35564,35565,35566,35567,35568,35569,35570,35571,35572,35573,35574,35575,35576,35577,35578,35579,35580,35581,35582,35583,35584,35585,35586,35587,35588,35589,35590,35592,35593,35594,35595,35596,35597,35598,35599,35600,35601,35602,35603,35604,35605,35606,35607,35608,35609,35610,35611,35612,35613,35614,35615,35616,35617,35618,35619,35620,35621,35623,35624,35625,35626,35627,35628,35629,35630,35631,35632,35633,35634,35635,35636,35637,35638,35639,35640,35641,35642,35643,35644,35645,35646,35647,35648,35649,35650,35651,35652,35653,24103,30151,37073,35777,33437,26525,25903,21553,34584,30693,32930,33026,27713,20043,32455,32844,30452,26893,27542,25191,20540,20356,22336,25351,27490,36286,21482,26088,32440,24535,25370,25527,33267,33268,32622,24092,23769,21046,26234,31209,31258,36136,28825,30164,28382,27835,31378,20013,30405,24544,38047,34935,32456,31181,32959,37325,20210,20247,33311,21608,24030,27954,35788,31909,36724,32920,24090,21650,30385,23449,26172,39588,29664,26666,34523,26417,29482,35832,35803,36880,31481,28891,29038,25284,30633,22065,20027,33879,26609,21161,34496,36142,38136,31569,35654,35655,35656,35657,35658,35659,35660,35661,35662,35663,35664,35665,35666,35667,35668,35669,35670,35671,35672,35673,35674,35675,35676,35677,35678,35679,35680,35681,35682,35683,35684,35685,35687,35688,35689,35690,35691,35693,35694,35695,35696,35697,35698,35699,35700,35701,35702,35703,35704,35705,35706,35707,35708,35709,35710,35711,35712,35713,35714,35715,35716,35717,35718,35719,35720,35721,35722,35723,35724,35725,35726,35727,35728,35729,35730,35731,35732,35733,35734,35735,35736,35737,35738,35739,35740,35741,35742,35743,35756,35761,35771,35783,35792,35818,35849,35870,20303,27880,31069,39547,25235,29226,25341,19987,30742,36716,25776,36186,31686,26729,24196,35013,22918,25758,22766,29366,26894,38181,36861,36184,22368,32512,35846,20934,25417,25305,21331,26700,29730,33537,37196,21828,30528,28796,27978,20857,21672,36164,23039,28363,28100,23388,32043,20180,31869,28371,23376,33258,28173,23383,39683,26837,36394,23447,32508,24635,32437,37049,36208,22863,25549,31199,36275,21330,26063,31062,35781,38459,32452,38075,32386,22068,37257,26368,32618,23562,36981,26152,24038,20304,26590,20570,20316,22352,24231,59408,59409,59410,59411,59412,35896,35897,35898,35899,35900,35901,35902,35903,35904,35906,35907,35908,35909,35912,35914,35915,35917,35918,35919,35920,35921,35922,35923,35924,35926,35927,35928,35929,35931,35932,35933,35934,35935,35936,35939,35940,35941,35942,35943,35944,35945,35948,35949,35950,35951,35952,35953,35954,35956,35957,35958,35959,35963,35964,35965,35966,35967,35968,35969,35971,35972,35974,35975,35976,35979,35981,35982,35983,35984,35985,35986,35987,35989,35990,35991,35993,35994,35995,35996,35997,35998,35999,36000,36001,36002,36003,36004,36005,36006,36007,36008,36009,36010,36011,36012,36013,20109,19980,20800,19984,24319,21317,19989,20120,19998,39730,23404,22121,20008,31162,20031,21269,20039,22829,29243,21358,27664,22239,32996,39319,27603,30590,40727,20022,20127,40720,20060,20073,20115,33416,23387,21868,22031,20164,21389,21405,21411,21413,21422,38757,36189,21274,21493,21286,21294,21310,36188,21350,21347,20994,21000,21006,21037,21043,21055,21056,21068,21086,21089,21084,33967,21117,21122,21121,21136,21139,20866,32596,20155,20163,20169,20162,20200,20193,20203,20190,20251,20211,20258,20324,20213,20261,20263,20233,20267,20318,20327,25912,20314,20317,36014,36015,36016,36017,36018,36019,36020,36021,36022,36023,36024,36025,36026,36027,36028,36029,36030,36031,36032,36033,36034,36035,36036,36037,36038,36039,36040,36041,36042,36043,36044,36045,36046,36047,36048,36049,36050,36051,36052,36053,36054,36055,36056,36057,36058,36059,36060,36061,36062,36063,36064,36065,36066,36067,36068,36069,36070,36071,36072,36073,36074,36075,36076,36077,36078,36079,36080,36081,36082,36083,36084,36085,36086,36087,36088,36089,36090,36091,36092,36093,36094,36095,36096,36097,36098,36099,36100,36101,36102,36103,36104,36105,36106,36107,36108,36109,20319,20311,20274,20285,20342,20340,20369,20361,20355,20367,20350,20347,20394,20348,20396,20372,20454,20456,20458,20421,20442,20451,20444,20433,20447,20472,20521,20556,20467,20524,20495,20526,20525,20478,20508,20492,20517,20520,20606,20547,20565,20552,20558,20588,20603,20645,20647,20649,20666,20694,20742,20717,20716,20710,20718,20743,20747,20189,27709,20312,20325,20430,40864,27718,31860,20846,24061,40649,39320,20865,22804,21241,21261,35335,21264,20971,22809,20821,20128,20822,20147,34926,34980,20149,33044,35026,31104,23348,34819,32696,20907,20913,20925,20924,36110,36111,36112,36113,36114,36115,36116,36117,36118,36119,36120,36121,36122,36123,36124,36128,36177,36178,36183,36191,36197,36200,36201,36202,36204,36206,36207,36209,36210,36216,36217,36218,36219,36220,36221,36222,36223,36224,36226,36227,36230,36231,36232,36233,36236,36237,36238,36239,36240,36242,36243,36245,36246,36247,36248,36249,36250,36251,36252,36253,36254,36256,36257,36258,36260,36261,36262,36263,36264,36265,36266,36267,36268,36269,36270,36271,36272,36274,36278,36279,36281,36283,36285,36288,36289,36290,36293,36295,36296,36297,36298,36301,36304,36306,36307,36308,20935,20886,20898,20901,35744,35750,35751,35754,35764,35765,35767,35778,35779,35787,35791,35790,35794,35795,35796,35798,35800,35801,35804,35807,35808,35812,35816,35817,35822,35824,35827,35830,35833,35836,35839,35840,35842,35844,35847,35852,35855,35857,35858,35860,35861,35862,35865,35867,35864,35869,35871,35872,35873,35877,35879,35882,35883,35886,35887,35890,35891,35893,35894,21353,21370,38429,38434,38433,38449,38442,38461,38460,38466,38473,38484,38495,38503,38508,38514,38516,38536,38541,38551,38576,37015,37019,37021,37017,37036,37025,37044,37043,37046,37050,36309,36312,36313,36316,36320,36321,36322,36325,36326,36327,36329,36333,36334,36336,36337,36338,36340,36342,36348,36350,36351,36352,36353,36354,36355,36356,36358,36359,36360,36363,36365,36366,36368,36369,36370,36371,36373,36374,36375,36376,36377,36378,36379,36380,36384,36385,36388,36389,36390,36391,36392,36395,36397,36400,36402,36403,36404,36406,36407,36408,36411,36412,36414,36415,36419,36421,36422,36428,36429,36430,36431,36432,36435,36436,36437,36438,36439,36440,36442,36443,36444,36445,36446,36447,36448,36449,36450,36451,36452,36453,36455,36456,36458,36459,36462,36465,37048,37040,37071,37061,37054,37072,37060,37063,37075,37094,37090,37084,37079,37083,37099,37103,37118,37124,37154,37150,37155,37169,37167,37177,37187,37190,21005,22850,21154,21164,21165,21182,21759,21200,21206,21232,21471,29166,30669,24308,20981,20988,39727,21430,24321,30042,24047,22348,22441,22433,22654,22716,22725,22737,22313,22316,22314,22323,22329,22318,22319,22364,22331,22338,22377,22405,22379,22406,22396,22395,22376,22381,22390,22387,22445,22436,22412,22450,22479,22439,22452,22419,22432,22485,22488,22490,22489,22482,22456,22516,22511,22520,22500,22493,36467,36469,36471,36472,36473,36474,36475,36477,36478,36480,36482,36483,36484,36486,36488,36489,36490,36491,36492,36493,36494,36497,36498,36499,36501,36502,36503,36504,36505,36506,36507,36509,36511,36512,36513,36514,36515,36516,36517,36518,36519,36520,36521,36522,36525,36526,36528,36529,36531,36532,36533,36534,36535,36536,36537,36539,36540,36541,36542,36543,36544,36545,36546,36547,36548,36549,36550,36551,36552,36553,36554,36555,36556,36557,36559,36560,36561,36562,36563,36564,36565,36566,36567,36568,36569,36570,36571,36572,36573,36574,36575,36576,36577,36578,36579,36580,22539,22541,22525,22509,22528,22558,22553,22596,22560,22629,22636,22657,22665,22682,22656,39336,40729,25087,33401,33405,33407,33423,33418,33448,33412,33422,33425,33431,33433,33451,33464,33470,33456,33480,33482,33507,33432,33463,33454,33483,33484,33473,33449,33460,33441,33450,33439,33476,33486,33444,33505,33545,33527,33508,33551,33543,33500,33524,33490,33496,33548,33531,33491,33553,33562,33542,33556,33557,33504,33493,33564,33617,33627,33628,33544,33682,33596,33588,33585,33691,33630,33583,33615,33607,33603,33631,33600,33559,33632,33581,33594,33587,33638,33637,36581,36582,36583,36584,36585,36586,36587,36588,36589,36590,36591,36592,36593,36594,36595,36596,36597,36598,36599,36600,36601,36602,36603,36604,36605,36606,36607,36608,36609,36610,36611,36612,36613,36614,36615,36616,36617,36618,36619,36620,36621,36622,36623,36624,36625,36626,36627,36628,36629,36630,36631,36632,36633,36634,36635,36636,36637,36638,36639,36640,36641,36642,36643,36644,36645,36646,36647,36648,36649,36650,36651,36652,36653,36654,36655,36656,36657,36658,36659,36660,36661,36662,36663,36664,36665,36666,36667,36668,36669,36670,36671,36672,36673,36674,36675,36676,33640,33563,33641,33644,33642,33645,33646,33712,33656,33715,33716,33696,33706,33683,33692,33669,33660,33718,33705,33661,33720,33659,33688,33694,33704,33722,33724,33729,33793,33765,33752,22535,33816,33803,33757,33789,33750,33820,33848,33809,33798,33748,33759,33807,33795,33784,33785,33770,33733,33728,33830,33776,33761,33884,33873,33882,33881,33907,33927,33928,33914,33929,33912,33852,33862,33897,33910,33932,33934,33841,33901,33985,33997,34000,34022,33981,34003,33994,33983,33978,34016,33953,33977,33972,33943,34021,34019,34060,29965,34104,34032,34105,34079,34106,36677,36678,36679,36680,36681,36682,36683,36684,36685,36686,36687,36688,36689,36690,36691,36692,36693,36694,36695,36696,36697,36698,36699,36700,36701,36702,36703,36704,36705,36706,36707,36708,36709,36714,36736,36748,36754,36765,36768,36769,36770,36772,36773,36774,36775,36778,36780,36781,36782,36783,36786,36787,36788,36789,36791,36792,36794,36795,36796,36799,36800,36803,36806,36809,36810,36811,36812,36813,36815,36818,36822,36823,36826,36832,36833,36835,36839,36844,36847,36849,36850,36852,36853,36854,36858,36859,36860,36862,36863,36871,36872,36876,36878,36883,36885,36888,34134,34107,34047,34044,34137,34120,34152,34148,34142,34170,30626,34115,34162,34171,34212,34216,34183,34191,34169,34222,34204,34181,34233,34231,34224,34259,34241,34268,34303,34343,34309,34345,34326,34364,24318,24328,22844,22849,32823,22869,22874,22872,21263,23586,23589,23596,23604,25164,25194,25247,25275,25290,25306,25303,25326,25378,25334,25401,25419,25411,25517,25590,25457,25466,25486,25524,25453,25516,25482,25449,25518,25532,25586,25592,25568,25599,25540,25566,25550,25682,25542,25534,25669,25665,25611,25627,25632,25612,25638,25633,25694,25732,25709,25750,36889,36892,36899,36900,36901,36903,36904,36905,36906,36907,36908,36912,36913,36914,36915,36916,36919,36921,36922,36925,36927,36928,36931,36933,36934,36936,36937,36938,36939,36940,36942,36948,36949,36950,36953,36954,36956,36957,36958,36959,36960,36961,36964,36966,36967,36969,36970,36971,36972,36975,36976,36977,36978,36979,36982,36983,36984,36985,36986,36987,36988,36990,36993,36996,36997,36998,36999,37001,37002,37004,37005,37006,37007,37008,37010,37012,37014,37016,37018,37020,37022,37023,37024,37028,37029,37031,37032,37033,37035,37037,37042,37047,37052,37053,37055,37056,25722,25783,25784,25753,25786,25792,25808,25815,25828,25826,25865,25893,25902,24331,24530,29977,24337,21343,21489,21501,21481,21480,21499,21522,21526,21510,21579,21586,21587,21588,21590,21571,21537,21591,21593,21539,21554,21634,21652,21623,21617,21604,21658,21659,21636,21622,21606,21661,21712,21677,21698,21684,21714,21671,21670,21715,21716,21618,21667,21717,21691,21695,21708,21721,21722,21724,21673,21674,21668,21725,21711,21726,21787,21735,21792,21757,21780,21747,21794,21795,21775,21777,21799,21802,21863,21903,21941,21833,21869,21825,21845,21823,21840,21820,37058,37059,37062,37064,37065,37067,37068,37069,37074,37076,37077,37078,37080,37081,37082,37086,37087,37088,37091,37092,37093,37097,37098,37100,37102,37104,37105,37106,37107,37109,37110,37111,37113,37114,37115,37116,37119,37120,37121,37123,37125,37126,37127,37128,37129,37130,37131,37132,37133,37134,37135,37136,37137,37138,37139,37140,37141,37142,37143,37144,37146,37147,37148,37149,37151,37152,37153,37156,37157,37158,37159,37160,37161,37162,37163,37164,37165,37166,37168,37170,37171,37172,37173,37174,37175,37176,37178,37179,37180,37181,37182,37183,37184,37185,37186,37188,21815,21846,21877,21878,21879,21811,21808,21852,21899,21970,21891,21937,21945,21896,21889,21919,21886,21974,21905,21883,21983,21949,21950,21908,21913,21994,22007,21961,22047,21969,21995,21996,21972,21990,21981,21956,21999,21989,22002,22003,21964,21965,21992,22005,21988,36756,22046,22024,22028,22017,22052,22051,22014,22016,22055,22061,22104,22073,22103,22060,22093,22114,22105,22108,22092,22100,22150,22116,22129,22123,22139,22140,22149,22163,22191,22228,22231,22237,22241,22261,22251,22265,22271,22276,22282,22281,22300,24079,24089,24084,24081,24113,24123,24124,37189,37191,37192,37201,37203,37204,37205,37206,37208,37209,37211,37212,37215,37216,37222,37223,37224,37227,37229,37235,37242,37243,37244,37248,37249,37250,37251,37252,37254,37256,37258,37262,37263,37267,37268,37269,37270,37271,37272,37273,37276,37277,37278,37279,37280,37281,37284,37285,37286,37287,37288,37289,37291,37292,37296,37297,37298,37299,37302,37303,37304,37305,37307,37308,37309,37310,37311,37312,37313,37314,37315,37316,37317,37318,37320,37323,37328,37330,37331,37332,37333,37334,37335,37336,37337,37338,37339,37341,37342,37343,37344,37345,37346,37347,37348,37349,24119,24132,24148,24155,24158,24161,23692,23674,23693,23696,23702,23688,23704,23705,23697,23706,23708,23733,23714,23741,23724,23723,23729,23715,23745,23735,23748,23762,23780,23755,23781,23810,23811,23847,23846,23854,23844,23838,23814,23835,23896,23870,23860,23869,23916,23899,23919,23901,23915,23883,23882,23913,23924,23938,23961,23965,35955,23991,24005,24435,24439,24450,24455,24457,24460,24469,24473,24476,24488,24493,24501,24508,34914,24417,29357,29360,29364,29367,29368,29379,29377,29390,29389,29394,29416,29423,29417,29426,29428,29431,29441,29427,29443,29434,37350,37351,37352,37353,37354,37355,37356,37357,37358,37359,37360,37361,37362,37363,37364,37365,37366,37367,37368,37369,37370,37371,37372,37373,37374,37375,37376,37377,37378,37379,37380,37381,37382,37383,37384,37385,37386,37387,37388,37389,37390,37391,37392,37393,37394,37395,37396,37397,37398,37399,37400,37401,37402,37403,37404,37405,37406,37407,37408,37409,37410,37411,37412,37413,37414,37415,37416,37417,37418,37419,37420,37421,37422,37423,37424,37425,37426,37427,37428,37429,37430,37431,37432,37433,37434,37435,37436,37437,37438,37439,37440,37441,37442,37443,37444,37445,29435,29463,29459,29473,29450,29470,29469,29461,29474,29497,29477,29484,29496,29489,29520,29517,29527,29536,29548,29551,29566,33307,22821,39143,22820,22786,39267,39271,39272,39273,39274,39275,39276,39284,39287,39293,39296,39300,39303,39306,39309,39312,39313,39315,39316,39317,24192,24209,24203,24214,24229,24224,24249,24245,24254,24243,36179,24274,24273,24283,24296,24298,33210,24516,24521,24534,24527,24579,24558,24580,24545,24548,24574,24581,24582,24554,24557,24568,24601,24629,24614,24603,24591,24589,24617,24619,24586,24639,24609,24696,24697,24699,24698,24642,37446,37447,37448,37449,37450,37451,37452,37453,37454,37455,37456,37457,37458,37459,37460,37461,37462,37463,37464,37465,37466,37467,37468,37469,37470,37471,37472,37473,37474,37475,37476,37477,37478,37479,37480,37481,37482,37483,37484,37485,37486,37487,37488,37489,37490,37491,37493,37494,37495,37496,37497,37498,37499,37500,37501,37502,37503,37504,37505,37506,37507,37508,37509,37510,37511,37512,37513,37514,37515,37516,37517,37519,37520,37521,37522,37523,37524,37525,37526,37527,37528,37529,37530,37531,37532,37533,37534,37535,37536,37537,37538,37539,37540,37541,37542,37543,24682,24701,24726,24730,24749,24733,24707,24722,24716,24731,24812,24763,24753,24797,24792,24774,24794,24756,24864,24870,24853,24867,24820,24832,24846,24875,24906,24949,25004,24980,24999,25015,25044,25077,24541,38579,38377,38379,38385,38387,38389,38390,38396,38398,38403,38404,38406,38408,38410,38411,38412,38413,38415,38418,38421,38422,38423,38425,38426,20012,29247,25109,27701,27732,27740,27722,27811,27781,27792,27796,27788,27752,27753,27764,27766,27782,27817,27856,27860,27821,27895,27896,27889,27863,27826,27872,27862,27898,27883,27886,27825,27859,27887,27902,37544,37545,37546,37547,37548,37549,37551,37552,37553,37554,37555,37556,37557,37558,37559,37560,37561,37562,37563,37564,37565,37566,37567,37568,37569,37570,37571,37572,37573,37574,37575,37577,37578,37579,37580,37581,37582,37583,37584,37585,37586,37587,37588,37589,37590,37591,37592,37593,37594,37595,37596,37597,37598,37599,37600,37601,37602,37603,37604,37605,37606,37607,37608,37609,37610,37611,37612,37613,37614,37615,37616,37617,37618,37619,37620,37621,37622,37623,37624,37625,37626,37627,37628,37629,37630,37631,37632,37633,37634,37635,37636,37637,37638,37639,37640,37641,27961,27943,27916,27971,27976,27911,27908,27929,27918,27947,27981,27950,27957,27930,27983,27986,27988,27955,28049,28015,28062,28064,27998,28051,28052,27996,28000,28028,28003,28186,28103,28101,28126,28174,28095,28128,28177,28134,28125,28121,28182,28075,28172,28078,28203,28270,28238,28267,28338,28255,28294,28243,28244,28210,28197,28228,28383,28337,28312,28384,28461,28386,28325,28327,28349,28347,28343,28375,28340,28367,28303,28354,28319,28514,28486,28487,28452,28437,28409,28463,28470,28491,28532,28458,28425,28457,28553,28557,28556,28536,28530,28540,28538,28625,37642,37643,37644,37645,37646,37647,37648,37649,37650,37651,37652,37653,37654,37655,37656,37657,37658,37659,37660,37661,37662,37663,37664,37665,37666,37667,37668,37669,37670,37671,37672,37673,37674,37675,37676,37677,37678,37679,37680,37681,37682,37683,37684,37685,37686,37687,37688,37689,37690,37691,37692,37693,37695,37696,37697,37698,37699,37700,37701,37702,37703,37704,37705,37706,37707,37708,37709,37710,37711,37712,37713,37714,37715,37716,37717,37718,37719,37720,37721,37722,37723,37724,37725,37726,37727,37728,37729,37730,37731,37732,37733,37734,37735,37736,37737,37739,28617,28583,28601,28598,28610,28641,28654,28638,28640,28655,28698,28707,28699,28729,28725,28751,28766,23424,23428,23445,23443,23461,23480,29999,39582,25652,23524,23534,35120,23536,36423,35591,36790,36819,36821,36837,36846,36836,36841,36838,36851,36840,36869,36868,36875,36902,36881,36877,36886,36897,36917,36918,36909,36911,36932,36945,36946,36944,36968,36952,36962,36955,26297,36980,36989,36994,37000,36995,37003,24400,24407,24406,24408,23611,21675,23632,23641,23409,23651,23654,32700,24362,24361,24365,33396,24380,39739,23662,22913,22915,22925,22953,22954,22947,37740,37741,37742,37743,37744,37745,37746,37747,37748,37749,37750,37751,37752,37753,37754,37755,37756,37757,37758,37759,37760,37761,37762,37763,37764,37765,37766,37767,37768,37769,37770,37771,37772,37773,37774,37776,37777,37778,37779,37780,37781,37782,37783,37784,37785,37786,37787,37788,37789,37790,37791,37792,37793,37794,37795,37796,37797,37798,37799,37800,37801,37802,37803,37804,37805,37806,37807,37808,37809,37810,37811,37812,37813,37814,37815,37816,37817,37818,37819,37820,37821,37822,37823,37824,37825,37826,37827,37828,37829,37830,37831,37832,37833,37835,37836,37837,22935,22986,22955,22942,22948,22994,22962,22959,22999,22974,23045,23046,23005,23048,23011,23000,23033,23052,23049,23090,23092,23057,23075,23059,23104,23143,23114,23125,23100,23138,23157,33004,23210,23195,23159,23162,23230,23275,23218,23250,23252,23224,23264,23267,23281,23254,23270,23256,23260,23305,23319,23318,23346,23351,23360,23573,23580,23386,23397,23411,23377,23379,23394,39541,39543,39544,39546,39551,39549,39552,39553,39557,39560,39562,39568,39570,39571,39574,39576,39579,39580,39581,39583,39584,39586,39587,39589,39591,32415,32417,32419,32421,32424,32425,37838,37839,37840,37841,37842,37843,37844,37845,37847,37848,37849,37850,37851,37852,37853,37854,37855,37856,37857,37858,37859,37860,37861,37862,37863,37864,37865,37866,37867,37868,37869,37870,37871,37872,37873,37874,37875,37876,37877,37878,37879,37880,37881,37882,37883,37884,37885,37886,37887,37888,37889,37890,37891,37892,37893,37894,37895,37896,37897,37898,37899,37900,37901,37902,37903,37904,37905,37906,37907,37908,37909,37910,37911,37912,37913,37914,37915,37916,37917,37918,37919,37920,37921,37922,37923,37924,37925,37926,37927,37928,37929,37930,37931,37932,37933,37934,32429,32432,32446,32448,32449,32450,32457,32459,32460,32464,32468,32471,32475,32480,32481,32488,32491,32494,32495,32497,32498,32525,32502,32506,32507,32510,32513,32514,32515,32519,32520,32523,32524,32527,32529,32530,32535,32537,32540,32539,32543,32545,32546,32547,32548,32549,32550,32551,32554,32555,32556,32557,32559,32560,32561,32562,32563,32565,24186,30079,24027,30014,37013,29582,29585,29614,29602,29599,29647,29634,29649,29623,29619,29632,29641,29640,29669,29657,39036,29706,29673,29671,29662,29626,29682,29711,29738,29787,29734,29733,29736,29744,29742,29740,37935,37936,37937,37938,37939,37940,37941,37942,37943,37944,37945,37946,37947,37948,37949,37951,37952,37953,37954,37955,37956,37957,37958,37959,37960,37961,37962,37963,37964,37965,37966,37967,37968,37969,37970,37971,37972,37973,37974,37975,37976,37977,37978,37979,37980,37981,37982,37983,37984,37985,37986,37987,37988,37989,37990,37991,37992,37993,37994,37996,37997,37998,37999,38000,38001,38002,38003,38004,38005,38006,38007,38008,38009,38010,38011,38012,38013,38014,38015,38016,38017,38018,38019,38020,38033,38038,38040,38087,38095,38099,38100,38106,38118,38139,38172,38176,29723,29722,29761,29788,29783,29781,29785,29815,29805,29822,29852,29838,29824,29825,29831,29835,29854,29864,29865,29840,29863,29906,29882,38890,38891,38892,26444,26451,26462,26440,26473,26533,26503,26474,26483,26520,26535,26485,26536,26526,26541,26507,26487,26492,26608,26633,26584,26634,26601,26544,26636,26585,26549,26586,26547,26589,26624,26563,26552,26594,26638,26561,26621,26674,26675,26720,26721,26702,26722,26692,26724,26755,26653,26709,26726,26689,26727,26688,26686,26698,26697,26665,26805,26767,26740,26743,26771,26731,26818,26990,26876,26911,26912,26873,38183,38195,38205,38211,38216,38219,38229,38234,38240,38254,38260,38261,38263,38264,38265,38266,38267,38268,38269,38270,38272,38273,38274,38275,38276,38277,38278,38279,38280,38281,38282,38283,38284,38285,38286,38287,38288,38289,38290,38291,38292,38293,38294,38295,38296,38297,38298,38299,38300,38301,38302,38303,38304,38305,38306,38307,38308,38309,38310,38311,38312,38313,38314,38315,38316,38317,38318,38319,38320,38321,38322,38323,38324,38325,38326,38327,38328,38329,38330,38331,38332,38333,38334,38335,38336,38337,38338,38339,38340,38341,38342,38343,38344,38345,38346,38347,26916,26864,26891,26881,26967,26851,26896,26993,26937,26976,26946,26973,27012,26987,27008,27032,27000,26932,27084,27015,27016,27086,27017,26982,26979,27001,27035,27047,27067,27051,27053,27092,27057,27073,27082,27103,27029,27104,27021,27135,27183,27117,27159,27160,27237,27122,27204,27198,27296,27216,27227,27189,27278,27257,27197,27176,27224,27260,27281,27280,27305,27287,27307,29495,29522,27521,27522,27527,27524,27538,27539,27533,27546,27547,27553,27562,36715,36717,36721,36722,36723,36725,36726,36728,36727,36729,36730,36732,36734,36737,36738,36740,36743,36747,38348,38349,38350,38351,38352,38353,38354,38355,38356,38357,38358,38359,38360,38361,38362,38363,38364,38365,38366,38367,38368,38369,38370,38371,38372,38373,38374,38375,38380,38399,38407,38419,38424,38427,38430,38432,38435,38436,38437,38438,38439,38440,38441,38443,38444,38445,38447,38448,38455,38456,38457,38458,38462,38465,38467,38474,38478,38479,38481,38482,38483,38486,38487,38488,38489,38490,38492,38493,38494,38496,38499,38501,38502,38507,38509,38510,38511,38512,38513,38515,38520,38521,38522,38523,38524,38525,38526,38527,38528,38529,38530,38531,38532,38535,38537,38538,36749,36750,36751,36760,36762,36558,25099,25111,25115,25119,25122,25121,25125,25124,25132,33255,29935,29940,29951,29967,29969,29971,25908,26094,26095,26096,26122,26137,26482,26115,26133,26112,28805,26359,26141,26164,26161,26166,26165,32774,26207,26196,26177,26191,26198,26209,26199,26231,26244,26252,26279,26269,26302,26331,26332,26342,26345,36146,36147,36150,36155,36157,36160,36165,36166,36168,36169,36167,36173,36181,36185,35271,35274,35275,35276,35278,35279,35280,35281,29294,29343,29277,29286,29295,29310,29311,29316,29323,29325,29327,29330,25352,25394,25520,38540,38542,38545,38546,38547,38549,38550,38554,38555,38557,38558,38559,38560,38561,38562,38563,38564,38565,38566,38568,38569,38570,38571,38572,38573,38574,38575,38577,38578,38580,38581,38583,38584,38586,38587,38591,38594,38595,38600,38602,38603,38608,38609,38611,38612,38614,38615,38616,38617,38618,38619,38620,38621,38622,38623,38625,38626,38627,38628,38629,38630,38631,38635,38636,38637,38638,38640,38641,38642,38644,38645,38648,38650,38651,38652,38653,38655,38658,38659,38661,38666,38667,38668,38672,38673,38674,38676,38677,38679,38680,38681,38682,38683,38685,38687,38688,25663,25816,32772,27626,27635,27645,27637,27641,27653,27655,27654,27661,27669,27672,27673,27674,27681,27689,27684,27690,27698,25909,25941,25963,29261,29266,29270,29232,34402,21014,32927,32924,32915,32956,26378,32957,32945,32939,32941,32948,32951,32999,33000,33001,33002,32987,32962,32964,32985,32973,32983,26384,32989,33003,33009,33012,33005,33037,33038,33010,33020,26389,33042,35930,33078,33054,33068,33048,33074,33096,33100,33107,33140,33113,33114,33137,33120,33129,33148,33149,33133,33127,22605,23221,33160,33154,33169,28373,33187,33194,33228,26406,33226,33211,38689,38690,38691,38692,38693,38694,38695,38696,38697,38699,38700,38702,38703,38705,38707,38708,38709,38710,38711,38714,38715,38716,38717,38719,38720,38721,38722,38723,38724,38725,38726,38727,38728,38729,38730,38731,38732,38733,38734,38735,38736,38737,38740,38741,38743,38744,38746,38748,38749,38751,38755,38756,38758,38759,38760,38762,38763,38764,38765,38766,38767,38768,38769,38770,38773,38775,38776,38777,38778,38779,38781,38782,38783,38784,38785,38786,38787,38788,38790,38791,38792,38793,38794,38796,38798,38799,38800,38803,38805,38806,38807,38809,38810,38811,38812,38813,33217,33190,27428,27447,27449,27459,27462,27481,39121,39122,39123,39125,39129,39130,27571,24384,27586,35315,26000,40785,26003,26044,26054,26052,26051,26060,26062,26066,26070,28800,28828,28822,28829,28859,28864,28855,28843,28849,28904,28874,28944,28947,28950,28975,28977,29043,29020,29032,28997,29042,29002,29048,29050,29080,29107,29109,29096,29088,29152,29140,29159,29177,29213,29224,28780,28952,29030,29113,25150,25149,25155,25160,25161,31035,31040,31046,31049,31067,31068,31059,31066,31074,31063,31072,31087,31079,31098,31109,31114,31130,31143,31155,24529,24528,38814,38815,38817,38818,38820,38821,38822,38823,38824,38825,38826,38828,38830,38832,38833,38835,38837,38838,38839,38840,38841,38842,38843,38844,38845,38846,38847,38848,38849,38850,38851,38852,38853,38854,38855,38856,38857,38858,38859,38860,38861,38862,38863,38864,38865,38866,38867,38868,38869,38870,38871,38872,38873,38874,38875,38876,38877,38878,38879,38880,38881,38882,38883,38884,38885,38888,38894,38895,38896,38897,38898,38900,38903,38904,38905,38906,38907,38908,38909,38910,38911,38912,38913,38914,38915,38916,38917,38918,38919,38920,38921,38922,38923,38924,38925,38926,24636,24669,24666,24679,24641,24665,24675,24747,24838,24845,24925,25001,24989,25035,25041,25094,32896,32895,27795,27894,28156,30710,30712,30720,30729,30743,30744,30737,26027,30765,30748,30749,30777,30778,30779,30751,30780,30757,30764,30755,30761,30798,30829,30806,30807,30758,30800,30791,30796,30826,30875,30867,30874,30855,30876,30881,30883,30898,30905,30885,30932,30937,30921,30956,30962,30981,30964,30995,31012,31006,31028,40859,40697,40699,40700,30449,30468,30477,30457,30471,30472,30490,30498,30489,30509,30502,30517,30520,30544,30545,30535,30531,30554,30568,38927,38928,38929,38930,38931,38932,38933,38934,38935,38936,38937,38938,38939,38940,38941,38942,38943,38944,38945,38946,38947,38948,38949,38950,38951,38952,38953,38954,38955,38956,38957,38958,38959,38960,38961,38962,38963,38964,38965,38966,38967,38968,38969,38970,38971,38972,38973,38974,38975,38976,38977,38978,38979,38980,38981,38982,38983,38984,38985,38986,38987,38988,38989,38990,38991,38992,38993,38994,38995,38996,38997,38998,38999,39000,39001,39002,39003,39004,39005,39006,39007,39008,39009,39010,39011,39012,39013,39014,39015,39016,39017,39018,39019,39020,39021,39022,30562,30565,30591,30605,30589,30592,30604,30609,30623,30624,30640,30645,30653,30010,30016,30030,30027,30024,30043,30066,30073,30083,32600,32609,32607,35400,32616,32628,32625,32633,32641,32638,30413,30437,34866,38021,38022,38023,38027,38026,38028,38029,38031,38032,38036,38039,38037,38042,38043,38044,38051,38052,38059,38058,38061,38060,38063,38064,38066,38068,38070,38071,38072,38073,38074,38076,38077,38079,38084,38088,38089,38090,38091,38092,38093,38094,38096,38097,38098,38101,38102,38103,38105,38104,38107,38110,38111,38112,38114,38116,38117,38119,38120,38122,39023,39024,39025,39026,39027,39028,39051,39054,39058,39061,39065,39075,39080,39081,39082,39083,39084,39085,39086,39087,39088,39089,39090,39091,39092,39093,39094,39095,39096,39097,39098,39099,39100,39101,39102,39103,39104,39105,39106,39107,39108,39109,39110,39111,39112,39113,39114,39115,39116,39117,39119,39120,39124,39126,39127,39131,39132,39133,39136,39137,39138,39139,39140,39141,39142,39145,39146,39147,39148,39149,39150,39151,39152,39153,39154,39155,39156,39157,39158,39159,39160,39161,39162,39163,39164,39165,39166,39167,39168,39169,39170,39171,39172,39173,39174,39175,38121,38123,38126,38127,38131,38132,38133,38135,38137,38140,38141,38143,38147,38146,38150,38151,38153,38154,38157,38158,38159,38162,38163,38164,38165,38166,38168,38171,38173,38174,38175,38178,38186,38187,38185,38188,38193,38194,38196,38198,38199,38200,38204,38206,38207,38210,38197,38212,38213,38214,38217,38220,38222,38223,38226,38227,38228,38230,38231,38232,38233,38235,38238,38239,38237,38241,38242,38244,38245,38246,38247,38248,38249,38250,38251,38252,38255,38257,38258,38259,38202,30695,30700,38601,31189,31213,31203,31211,31238,23879,31235,31234,31262,31252,39176,39177,39178,39179,39180,39182,39183,39185,39186,39187,39188,39189,39190,39191,39192,39193,39194,39195,39196,39197,39198,39199,39200,39201,39202,39203,39204,39205,39206,39207,39208,39209,39210,39211,39212,39213,39215,39216,39217,39218,39219,39220,39221,39222,39223,39224,39225,39226,39227,39228,39229,39230,39231,39232,39233,39234,39235,39236,39237,39238,39239,39240,39241,39242,39243,39244,39245,39246,39247,39248,39249,39250,39251,39254,39255,39256,39257,39258,39259,39260,39261,39262,39263,39264,39265,39266,39268,39270,39283,39288,39289,39291,39294,39298,39299,39305,31289,31287,31313,40655,39333,31344,30344,30350,30355,30361,30372,29918,29920,29996,40480,40482,40488,40489,40490,40491,40492,40498,40497,40502,40504,40503,40505,40506,40510,40513,40514,40516,40518,40519,40520,40521,40523,40524,40526,40529,40533,40535,40538,40539,40540,40542,40547,40550,40551,40552,40553,40554,40555,40556,40561,40557,40563,30098,30100,30102,30112,30109,30124,30115,30131,30132,30136,30148,30129,30128,30147,30146,30166,30157,30179,30184,30182,30180,30187,30183,30211,30193,30204,30207,30224,30208,30213,30220,30231,30218,30245,30232,30229,30233,39308,39310,39322,39323,39324,39325,39326,39327,39328,39329,39330,39331,39332,39334,39335,39337,39338,39339,39340,39341,39342,39343,39344,39345,39346,39347,39348,39349,39350,39351,39352,39353,39354,39355,39356,39357,39358,39359,39360,39361,39362,39363,39364,39365,39366,39367,39368,39369,39370,39371,39372,39373,39374,39375,39376,39377,39378,39379,39380,39381,39382,39383,39384,39385,39386,39387,39388,39389,39390,39391,39392,39393,39394,39395,39396,39397,39398,39399,39400,39401,39402,39403,39404,39405,39406,39407,39408,39409,39410,39411,39412,39413,39414,39415,39416,39417,30235,30268,30242,30240,30272,30253,30256,30271,30261,30275,30270,30259,30285,30302,30292,30300,30294,30315,30319,32714,31462,31352,31353,31360,31366,31368,31381,31398,31392,31404,31400,31405,31411,34916,34921,34930,34941,34943,34946,34978,35014,34999,35004,35017,35042,35022,35043,35045,35057,35098,35068,35048,35070,35056,35105,35097,35091,35099,35082,35124,35115,35126,35137,35174,35195,30091,32997,30386,30388,30684,32786,32788,32790,32796,32800,32802,32805,32806,32807,32809,32808,32817,32779,32821,32835,32838,32845,32850,32873,32881,35203,39032,39040,39043,39418,39419,39420,39421,39422,39423,39424,39425,39426,39427,39428,39429,39430,39431,39432,39433,39434,39435,39436,39437,39438,39439,39440,39441,39442,39443,39444,39445,39446,39447,39448,39449,39450,39451,39452,39453,39454,39455,39456,39457,39458,39459,39460,39461,39462,39463,39464,39465,39466,39467,39468,39469,39470,39471,39472,39473,39474,39475,39476,39477,39478,39479,39480,39481,39482,39483,39484,39485,39486,39487,39488,39489,39490,39491,39492,39493,39494,39495,39496,39497,39498,39499,39500,39501,39502,39503,39504,39505,39506,39507,39508,39509,39510,39511,39512,39513,39049,39052,39053,39055,39060,39066,39067,39070,39071,39073,39074,39077,39078,34381,34388,34412,34414,34431,34426,34428,34427,34472,34445,34443,34476,34461,34471,34467,34474,34451,34473,34486,34500,34485,34510,34480,34490,34481,34479,34505,34511,34484,34537,34545,34546,34541,34547,34512,34579,34526,34548,34527,34520,34513,34563,34567,34552,34568,34570,34573,34569,34595,34619,34590,34597,34606,34586,34622,34632,34612,34609,34601,34615,34623,34690,34594,34685,34686,34683,34656,34672,34636,34670,34699,34643,34659,34684,34660,34649,34661,34707,34735,34728,34770,39514,39515,39516,39517,39518,39519,39520,39521,39522,39523,39524,39525,39526,39527,39528,39529,39530,39531,39538,39555,39561,39565,39566,39572,39573,39577,39590,39593,39594,39595,39596,39597,39598,39599,39602,39603,39604,39605,39609,39611,39613,39614,39615,39619,39620,39622,39623,39624,39625,39626,39629,39630,39631,39632,39634,39636,39637,39638,39639,39641,39642,39643,39644,39645,39646,39648,39650,39651,39652,39653,39655,39656,39657,39658,39660,39662,39664,39665,39666,39667,39668,39669,39670,39671,39672,39674,39676,39677,39678,39679,39680,39681,39682,39684,39685,39686,34758,34696,34693,34733,34711,34691,34731,34789,34732,34741,34739,34763,34771,34749,34769,34752,34762,34779,34794,34784,34798,34838,34835,34814,34826,34843,34849,34873,34876,32566,32578,32580,32581,33296,31482,31485,31496,31491,31492,31509,31498,31531,31503,31559,31544,31530,31513,31534,31537,31520,31525,31524,31539,31550,31518,31576,31578,31557,31605,31564,31581,31584,31598,31611,31586,31602,31601,31632,31654,31655,31672,31660,31645,31656,31621,31658,31644,31650,31659,31668,31697,31681,31692,31709,31706,31717,31718,31722,31756,31742,31740,31759,31766,31755,39687,39689,39690,39691,39692,39693,39694,39696,39697,39698,39700,39701,39702,39703,39704,39705,39706,39707,39708,39709,39710,39712,39713,39714,39716,39717,39718,39719,39720,39721,39722,39723,39724,39725,39726,39728,39729,39731,39732,39733,39734,39735,39736,39737,39738,39741,39742,39743,39744,39750,39754,39755,39756,39758,39760,39762,39763,39765,39766,39767,39768,39769,39770,39771,39772,39773,39774,39775,39776,39777,39778,39779,39780,39781,39782,39783,39784,39785,39786,39787,39788,39789,39790,39791,39792,39793,39794,39795,39796,39797,39798,39799,39800,39801,39802,39803,31775,31786,31782,31800,31809,31808,33278,33281,33282,33284,33260,34884,33313,33314,33315,33325,33327,33320,33323,33336,33339,33331,33332,33342,33348,33353,33355,33359,33370,33375,33384,34942,34949,34952,35032,35039,35166,32669,32671,32679,32687,32688,32690,31868,25929,31889,31901,31900,31902,31906,31922,31932,31933,31937,31943,31948,31949,31944,31941,31959,31976,33390,26280,32703,32718,32725,32741,32737,32742,32745,32750,32755,31992,32119,32166,32174,32327,32411,40632,40628,36211,36228,36244,36241,36273,36199,36205,35911,35913,37194,37200,37198,37199,37220,39804,39805,39806,39807,39808,39809,39810,39811,39812,39813,39814,39815,39816,39817,39818,39819,39820,39821,39822,39823,39824,39825,39826,39827,39828,39829,39830,39831,39832,39833,39834,39835,39836,39837,39838,39839,39840,39841,39842,39843,39844,39845,39846,39847,39848,39849,39850,39851,39852,39853,39854,39855,39856,39857,39858,39859,39860,39861,39862,39863,39864,39865,39866,39867,39868,39869,39870,39871,39872,39873,39874,39875,39876,39877,39878,39879,39880,39881,39882,39883,39884,39885,39886,39887,39888,39889,39890,39891,39892,39893,39894,39895,39896,39897,39898,39899,37218,37217,37232,37225,37231,37245,37246,37234,37236,37241,37260,37253,37264,37261,37265,37282,37283,37290,37293,37294,37295,37301,37300,37306,35925,40574,36280,36331,36357,36441,36457,36277,36287,36284,36282,36292,36310,36311,36314,36318,36302,36303,36315,36294,36332,36343,36344,36323,36345,36347,36324,36361,36349,36372,36381,36383,36396,36398,36387,36399,36410,36416,36409,36405,36413,36401,36425,36417,36418,36433,36434,36426,36464,36470,36476,36463,36468,36485,36495,36500,36496,36508,36510,35960,35970,35978,35973,35992,35988,26011,35286,35294,35290,35292,39900,39901,39902,39903,39904,39905,39906,39907,39908,39909,39910,39911,39912,39913,39914,39915,39916,39917,39918,39919,39920,39921,39922,39923,39924,39925,39926,39927,39928,39929,39930,39931,39932,39933,39934,39935,39936,39937,39938,39939,39940,39941,39942,39943,39944,39945,39946,39947,39948,39949,39950,39951,39952,39953,39954,39955,39956,39957,39958,39959,39960,39961,39962,39963,39964,39965,39966,39967,39968,39969,39970,39971,39972,39973,39974,39975,39976,39977,39978,39979,39980,39981,39982,39983,39984,39985,39986,39987,39988,39989,39990,39991,39992,39993,39994,39995,35301,35307,35311,35390,35622,38739,38633,38643,38639,38662,38657,38664,38671,38670,38698,38701,38704,38718,40832,40835,40837,40838,40839,40840,40841,40842,40844,40702,40715,40717,38585,38588,38589,38606,38610,30655,38624,37518,37550,37576,37694,37738,37834,37775,37950,37995,40063,40066,40069,40070,40071,40072,31267,40075,40078,40080,40081,40082,40084,40085,40090,40091,40094,40095,40096,40097,40098,40099,40101,40102,40103,40104,40105,40107,40109,40110,40112,40113,40114,40115,40116,40117,40118,40119,40122,40123,40124,40125,40132,40133,40134,40135,40138,40139,39996,39997,39998,39999,40000,40001,40002,40003,40004,40005,40006,40007,40008,40009,40010,40011,40012,40013,40014,40015,40016,40017,40018,40019,40020,40021,40022,40023,40024,40025,40026,40027,40028,40029,40030,40031,40032,40033,40034,40035,40036,40037,40038,40039,40040,40041,40042,40043,40044,40045,40046,40047,40048,40049,40050,40051,40052,40053,40054,40055,40056,40057,40058,40059,40061,40062,40064,40067,40068,40073,40074,40076,40079,40083,40086,40087,40088,40089,40093,40106,40108,40111,40121,40126,40127,40128,40129,40130,40136,40137,40145,40146,40154,40155,40160,40161,40140,40141,40142,40143,40144,40147,40148,40149,40151,40152,40153,40156,40157,40159,40162,38780,38789,38801,38802,38804,38831,38827,38819,38834,38836,39601,39600,39607,40536,39606,39610,39612,39617,39616,39621,39618,39627,39628,39633,39749,39747,39751,39753,39752,39757,39761,39144,39181,39214,39253,39252,39647,39649,39654,39663,39659,39675,39661,39673,39688,39695,39699,39711,39715,40637,40638,32315,40578,40583,40584,40587,40594,37846,40605,40607,40667,40668,40669,40672,40671,40674,40681,40679,40677,40682,40687,40738,40748,40751,40761,40759,40765,40766,40772,40163,40164,40165,40166,40167,40168,40169,40170,40171,40172,40173,40174,40175,40176,40177,40178,40179,40180,40181,40182,40183,40184,40185,40186,40187,40188,40189,40190,40191,40192,40193,40194,40195,40196,40197,40198,40199,40200,40201,40202,40203,40204,40205,40206,40207,40208,40209,40210,40211,40212,40213,40214,40215,40216,40217,40218,40219,40220,40221,40222,40223,40224,40225,40226,40227,40228,40229,40230,40231,40232,40233,40234,40235,40236,40237,40238,40239,40240,40241,40242,40243,40244,40245,40246,40247,40248,40249,40250,40251,40252,40253,40254,40255,40256,40257,40258,57908,57909,57910,57911,57912,57913,57914,57915,57916,57917,57918,57919,57920,57921,57922,57923,57924,57925,57926,57927,57928,57929,57930,57931,57932,57933,57934,57935,57936,57937,57938,57939,57940,57941,57942,57943,57944,57945,57946,57947,57948,57949,57950,57951,57952,57953,57954,57955,57956,57957,57958,57959,57960,57961,57962,57963,57964,57965,57966,57967,57968,57969,57970,57971,57972,57973,57974,57975,57976,57977,57978,57979,57980,57981,57982,57983,57984,57985,57986,57987,57988,57989,57990,57991,57992,57993,57994,57995,57996,57997,57998,57999,58000,58001,40259,40260,40261,40262,40263,40264,40265,40266,40267,40268,40269,40270,40271,40272,40273,40274,40275,40276,40277,40278,40279,40280,40281,40282,40283,40284,40285,40286,40287,40288,40289,40290,40291,40292,40293,40294,40295,40296,40297,40298,40299,40300,40301,40302,40303,40304,40305,40306,40307,40308,40309,40310,40311,40312,40313,40314,40315,40316,40317,40318,40319,40320,40321,40322,40323,40324,40325,40326,40327,40328,40329,40330,40331,40332,40333,40334,40335,40336,40337,40338,40339,40340,40341,40342,40343,40344,40345,40346,40347,40348,40349,40350,40351,40352,40353,40354,58002,58003,58004,58005,58006,58007,58008,58009,58010,58011,58012,58013,58014,58015,58016,58017,58018,58019,58020,58021,58022,58023,58024,58025,58026,58027,58028,58029,58030,58031,58032,58033,58034,58035,58036,58037,58038,58039,58040,58041,58042,58043,58044,58045,58046,58047,58048,58049,58050,58051,58052,58053,58054,58055,58056,58057,58058,58059,58060,58061,58062,58063,58064,58065,58066,58067,58068,58069,58070,58071,58072,58073,58074,58075,58076,58077,58078,58079,58080,58081,58082,58083,58084,58085,58086,58087,58088,58089,58090,58091,58092,58093,58094,58095,40355,40356,40357,40358,40359,40360,40361,40362,40363,40364,40365,40366,40367,40368,40369,40370,40371,40372,40373,40374,40375,40376,40377,40378,40379,40380,40381,40382,40383,40384,40385,40386,40387,40388,40389,40390,40391,40392,40393,40394,40395,40396,40397,40398,40399,40400,40401,40402,40403,40404,40405,40406,40407,40408,40409,40410,40411,40412,40413,40414,40415,40416,40417,40418,40419,40420,40421,40422,40423,40424,40425,40426,40427,40428,40429,40430,40431,40432,40433,40434,40435,40436,40437,40438,40439,40440,40441,40442,40443,40444,40445,40446,40447,40448,40449,40450,58096,58097,58098,58099,58100,58101,58102,58103,58104,58105,58106,58107,58108,58109,58110,58111,58112,58113,58114,58115,58116,58117,58118,58119,58120,58121,58122,58123,58124,58125,58126,58127,58128,58129,58130,58131,58132,58133,58134,58135,58136,58137,58138,58139,58140,58141,58142,58143,58144,58145,58146,58147,58148,58149,58150,58151,58152,58153,58154,58155,58156,58157,58158,58159,58160,58161,58162,58163,58164,58165,58166,58167,58168,58169,58170,58171,58172,58173,58174,58175,58176,58177,58178,58179,58180,58181,58182,58183,58184,58185,58186,58187,58188,58189,40451,40452,40453,40454,40455,40456,40457,40458,40459,40460,40461,40462,40463,40464,40465,40466,40467,40468,40469,40470,40471,40472,40473,40474,40475,40476,40477,40478,40484,40487,40494,40496,40500,40507,40508,40512,40525,40528,40530,40531,40532,40534,40537,40541,40543,40544,40545,40546,40549,40558,40559,40562,40564,40565,40566,40567,40568,40569,40570,40571,40572,40573,40576,40577,40579,40580,40581,40582,40585,40586,40588,40589,40590,40591,40592,40593,40596,40597,40598,40599,40600,40601,40602,40603,40604,40606,40608,40609,40610,40611,40612,40613,40615,40616,40617,40618,58190,58191,58192,58193,58194,58195,58196,58197,58198,58199,58200,58201,58202,58203,58204,58205,58206,58207,58208,58209,58210,58211,58212,58213,58214,58215,58216,58217,58218,58219,58220,58221,58222,58223,58224,58225,58226,58227,58228,58229,58230,58231,58232,58233,58234,58235,58236,58237,58238,58239,58240,58241,58242,58243,58244,58245,58246,58247,58248,58249,58250,58251,58252,58253,58254,58255,58256,58257,58258,58259,58260,58261,58262,58263,58264,58265,58266,58267,58268,58269,58270,58271,58272,58273,58274,58275,58276,58277,58278,58279,58280,58281,58282,58283,40619,40620,40621,40622,40623,40624,40625,40626,40627,40629,40630,40631,40633,40634,40636,40639,40640,40641,40642,40643,40645,40646,40647,40648,40650,40651,40652,40656,40658,40659,40661,40662,40663,40665,40666,40670,40673,40675,40676,40678,40680,40683,40684,40685,40686,40688,40689,40690,40691,40692,40693,40694,40695,40696,40698,40701,40703,40704,40705,40706,40707,40708,40709,40710,40711,40712,40713,40714,40716,40719,40721,40722,40724,40725,40726,40728,40730,40731,40732,40733,40734,40735,40737,40739,40740,40741,40742,40743,40744,40745,40746,40747,40749,40750,40752,40753,58284,58285,58286,58287,58288,58289,58290,58291,58292,58293,58294,58295,58296,58297,58298,58299,58300,58301,58302,58303,58304,58305,58306,58307,58308,58309,58310,58311,58312,58313,58314,58315,58316,58317,58318,58319,58320,58321,58322,58323,58324,58325,58326,58327,58328,58329,58330,58331,58332,58333,58334,58335,58336,58337,58338,58339,58340,58341,58342,58343,58344,58345,58346,58347,58348,58349,58350,58351,58352,58353,58354,58355,58356,58357,58358,58359,58360,58361,58362,58363,58364,58365,58366,58367,58368,58369,58370,58371,58372,58373,58374,58375,58376,58377,40754,40755,40756,40757,40758,40760,40762,40764,40767,40768,40769,40770,40771,40773,40774,40775,40776,40777,40778,40779,40780,40781,40782,40783,40786,40787,40788,40789,40790,40791,40792,40793,40794,40795,40796,40797,40798,40799,40800,40801,40802,40803,40804,40805,40806,40807,40808,40809,40810,40811,40812,40813,40814,40815,40816,40817,40818,40819,40820,40821,40822,40823,40824,40825,40826,40827,40828,40829,40830,40833,40834,40845,40846,40847,40848,40849,40850,40851,40852,40853,40854,40855,40856,40860,40861,40862,40865,40866,40867,40868,40869,63788,63865,63893,63975,63985,58378,58379,58380,58381,58382,58383,58384,58385,58386,58387,58388,58389,58390,58391,58392,58393,58394,58395,58396,58397,58398,58399,58400,58401,58402,58403,58404,58405,58406,58407,58408,58409,58410,58411,58412,58413,58414,58415,58416,58417,58418,58419,58420,58421,58422,58423,58424,58425,58426,58427,58428,58429,58430,58431,58432,58433,58434,58435,58436,58437,58438,58439,58440,58441,58442,58443,58444,58445,58446,58447,58448,58449,58450,58451,58452,58453,58454,58455,58456,58457,58458,58459,58460,58461,58462,58463,58464,58465,58466,58467,58468,58469,58470,58471,64012,64013,64014,64015,64017,64019,64020,64024,64031,64032,64033,64035,64036,64039,64040,64041,11905,59414,59415,59416,11908,13427,13383,11912,11915,59422,13726,13850,13838,11916,11927,14702,14616,59430,14799,14815,14963,14800,59435,59436,15182,15470,15584,11943,59441,59442,11946,16470,16735,11950,17207,11955,11958,11959,59451,17329,17324,11963,17373,17622,18017,17996,59459,18211,18217,18300,18317,11978,18759,18810,18813,18818,18819,18821,18822,18847,18843,18871,18870,59476,59477,19619,19615,19616,19617,19575,19618,19731,19732,19733,19734,19735,19736,19737,19886,59492,58472,58473,58474,58475,58476,58477,58478,58479,58480,58481,58482,58483,58484,58485,58486,58487,58488,58489,58490,58491,58492,58493,58494,58495,58496,58497,58498,58499,58500,58501,58502,58503,58504,58505,58506,58507,58508,58509,58510,58511,58512,58513,58514,58515,58516,58517,58518,58519,58520,58521,58522,58523,58524,58525,58526,58527,58528,58529,58530,58531,58532,58533,58534,58535,58536,58537,58538,58539,58540,58541,58542,58543,58544,58545,58546,58547,58548,58549,58550,58551,58552,58553,58554,58555,58556,58557,58558,58559,58560,58561,58562,58563,58564,58565],
+ "gb18030-ranges":[[0,128],[36,165],[38,169],[45,178],[50,184],[81,216],[89,226],[95,235],[96,238],[100,244],[103,248],[104,251],[105,253],[109,258],[126,276],[133,284],[148,300],[172,325],[175,329],[179,334],[208,364],[306,463],[307,465],[308,467],[309,469],[310,471],[311,473],[312,475],[313,477],[341,506],[428,594],[443,610],[544,712],[545,716],[558,730],[741,930],[742,938],[749,962],[750,970],[805,1026],[819,1104],[820,1106],[7922,8209],[7924,8215],[7925,8218],[7927,8222],[7934,8231],[7943,8241],[7944,8244],[7945,8246],[7950,8252],[8062,8365],[8148,8452],[8149,8454],[8152,8458],[8164,8471],[8174,8482],[8236,8556],[8240,8570],[8262,8596],[8264,8602],[8374,8713],[8380,8720],[8381,8722],[8384,8726],[8388,8731],[8390,8737],[8392,8740],[8393,8742],[8394,8748],[8396,8751],[8401,8760],[8406,8766],[8416,8777],[8419,8781],[8424,8787],[8437,8802],[8439,8808],[8445,8816],[8482,8854],[8485,8858],[8496,8870],[8521,8896],[8603,8979],[8936,9322],[8946,9372],[9046,9548],[9050,9588],[9063,9616],[9066,9622],[9076,9634],[9092,9652],[9100,9662],[9108,9672],[9111,9676],[9113,9680],[9131,9702],[9162,9735],[9164,9738],[9218,9793],[9219,9795],[11329,11906],[11331,11909],[11334,11913],[11336,11917],[11346,11928],[11361,11944],[11363,11947],[11366,11951],[11370,11956],[11372,11960],[11375,11964],[11389,11979],[11682,12284],[11686,12292],[11687,12312],[11692,12319],[11694,12330],[11714,12351],[11716,12436],[11723,12447],[11725,12535],[11730,12543],[11736,12586],[11982,12842],[11989,12850],[12102,12964],[12336,13200],[12348,13215],[12350,13218],[12384,13253],[12393,13263],[12395,13267],[12397,13270],[12510,13384],[12553,13428],[12851,13727],[12962,13839],[12973,13851],[13738,14617],[13823,14703],[13919,14801],[13933,14816],[14080,14964],[14298,15183],[14585,15471],[14698,15585],[15583,16471],[15847,16736],[16318,17208],[16434,17325],[16438,17330],[16481,17374],[16729,17623],[17102,17997],[17122,18018],[17315,18212],[17320,18218],[17402,18301],[17418,18318],[17859,18760],[17909,18811],[17911,18814],[17915,18820],[17916,18823],[17936,18844],[17939,18848],[17961,18872],[18664,19576],[18703,19620],[18814,19738],[18962,19887],[19043,40870],[33469,59244],[33470,59336],[33471,59367],[33484,59413],[33485,59417],[33490,59423],[33497,59431],[33501,59437],[33505,59443],[33513,59452],[33520,59460],[33536,59478],[33550,59493],[37845,63789],[37921,63866],[37948,63894],[38029,63976],[38038,63986],[38064,64016],[38065,64018],[38066,64021],[38069,64025],[38075,64034],[38076,64037],[38078,64042],[39108,65074],[39109,65093],[39113,65107],[39114,65112],[39115,65127],[39116,65132],[39265,65375],[39394,65510],[189000,65536]],
+ "jis0208":[12288,12289,12290,65292,65294,12539,65306,65307,65311,65281,12443,12444,180,65344,168,65342,65507,65343,12541,12542,12445,12446,12291,20189,12293,12294,12295,12540,8213,8208,65295,65340,65374,8741,65372,8230,8229,8216,8217,8220,8221,65288,65289,12308,12309,65339,65341,65371,65373,12296,12297,12298,12299,12300,12301,12302,12303,12304,12305,65291,65293,177,215,247,65309,8800,65308,65310,8806,8807,8734,8756,9794,9792,176,8242,8243,8451,65509,65284,65504,65505,65285,65283,65286,65290,65312,167,9734,9733,9675,9679,9678,9671,9670,9633,9632,9651,9650,9661,9660,8251,12306,8594,8592,8593,8595,12307,null,null,null,null,null,null,null,null,null,null,null,8712,8715,8838,8839,8834,8835,8746,8745,null,null,null,null,null,null,null,null,8743,8744,65506,8658,8660,8704,8707,null,null,null,null,null,null,null,null,null,null,null,8736,8869,8978,8706,8711,8801,8786,8810,8811,8730,8765,8733,8757,8747,8748,null,null,null,null,null,null,null,8491,8240,9839,9837,9834,8224,8225,182,null,null,null,null,9711,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,65296,65297,65298,65299,65300,65301,65302,65303,65304,65305,null,null,null,null,null,null,null,65313,65314,65315,65316,65317,65318,65319,65320,65321,65322,65323,65324,65325,65326,65327,65328,65329,65330,65331,65332,65333,65334,65335,65336,65337,65338,null,null,null,null,null,null,65345,65346,65347,65348,65349,65350,65351,65352,65353,65354,65355,65356,65357,65358,65359,65360,65361,65362,65363,65364,65365,65366,65367,65368,65369,65370,null,null,null,null,12353,12354,12355,12356,12357,12358,12359,12360,12361,12362,12363,12364,12365,12366,12367,12368,12369,12370,12371,12372,12373,12374,12375,12376,12377,12378,12379,12380,12381,12382,12383,12384,12385,12386,12387,12388,12389,12390,12391,12392,12393,12394,12395,12396,12397,12398,12399,12400,12401,12402,12403,12404,12405,12406,12407,12408,12409,12410,12411,12412,12413,12414,12415,12416,12417,12418,12419,12420,12421,12422,12423,12424,12425,12426,12427,12428,12429,12430,12431,12432,12433,12434,12435,null,null,null,null,null,null,null,null,null,null,null,12449,12450,12451,12452,12453,12454,12455,12456,12457,12458,12459,12460,12461,12462,12463,12464,12465,12466,12467,12468,12469,12470,12471,12472,12473,12474,12475,12476,12477,12478,12479,12480,12481,12482,12483,12484,12485,12486,12487,12488,12489,12490,12491,12492,12493,12494,12495,12496,12497,12498,12499,12500,12501,12502,12503,12504,12505,12506,12507,12508,12509,12510,12511,12512,12513,12514,12515,12516,12517,12518,12519,12520,12521,12522,12523,12524,12525,12526,12527,12528,12529,12530,12531,12532,12533,12534,null,null,null,null,null,null,null,null,913,914,915,916,917,918,919,920,921,922,923,924,925,926,927,928,929,931,932,933,934,935,936,937,null,null,null,null,null,null,null,null,945,946,947,948,949,950,951,952,953,954,955,956,957,958,959,960,961,963,964,965,966,967,968,969,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,1040,1041,1042,1043,1044,1045,1025,1046,1047,1048,1049,1050,1051,1052,1053,1054,1055,1056,1057,1058,1059,1060,1061,1062,1063,1064,1065,1066,1067,1068,1069,1070,1071,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,1072,1073,1074,1075,1076,1077,1105,1078,1079,1080,1081,1082,1083,1084,1085,1086,1087,1088,1089,1090,1091,1092,1093,1094,1095,1096,1097,1098,1099,1100,1101,1102,1103,null,null,null,null,null,null,null,null,null,null,null,null,null,9472,9474,9484,9488,9496,9492,9500,9516,9508,9524,9532,9473,9475,9487,9491,9499,9495,9507,9523,9515,9531,9547,9504,9519,9512,9527,9535,9501,9520,9509,9528,9538,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,9312,9313,9314,9315,9316,9317,9318,9319,9320,9321,9322,9323,9324,9325,9326,9327,9328,9329,9330,9331,8544,8545,8546,8547,8548,8549,8550,8551,8552,8553,null,13129,13076,13090,13133,13080,13095,13059,13110,13137,13143,13069,13094,13091,13099,13130,13115,13212,13213,13214,13198,13199,13252,13217,null,null,null,null,null,null,null,null,13179,12317,12319,8470,13261,8481,12964,12965,12966,12967,12968,12849,12850,12857,13182,13181,13180,8786,8801,8747,8750,8721,8730,8869,8736,8735,8895,8757,8745,8746,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,20124,21782,23043,38463,21696,24859,25384,23030,36898,33909,33564,31312,24746,25569,28197,26093,33894,33446,39925,26771,22311,26017,25201,23451,22992,34427,39156,32098,32190,39822,25110,31903,34999,23433,24245,25353,26263,26696,38343,38797,26447,20197,20234,20301,20381,20553,22258,22839,22996,23041,23561,24799,24847,24944,26131,26885,28858,30031,30064,31227,32173,32239,32963,33806,34915,35586,36949,36986,21307,20117,20133,22495,32946,37057,30959,19968,22769,28322,36920,31282,33576,33419,39983,20801,21360,21693,21729,22240,23035,24341,39154,28139,32996,34093,38498,38512,38560,38907,21515,21491,23431,28879,32701,36802,38632,21359,40284,31418,19985,30867,33276,28198,22040,21764,27421,34074,39995,23013,21417,28006,29916,38287,22082,20113,36939,38642,33615,39180,21473,21942,23344,24433,26144,26355,26628,27704,27891,27945,29787,30408,31310,38964,33521,34907,35424,37613,28082,30123,30410,39365,24742,35585,36234,38322,27022,21421,20870,22290,22576,22852,23476,24310,24616,25513,25588,27839,28436,28814,28948,29017,29141,29503,32257,33398,33489,34199,36960,37467,40219,22633,26044,27738,29989,20985,22830,22885,24448,24540,25276,26106,27178,27431,27572,29579,32705,35158,40236,40206,40644,23713,27798,33659,20740,23627,25014,33222,26742,29281,20057,20474,21368,24681,28201,31311,38899,19979,21270,20206,20309,20285,20385,20339,21152,21487,22025,22799,23233,23478,23521,31185,26247,26524,26550,27468,27827,28779,29634,31117,31166,31292,31623,33457,33499,33540,33655,33775,33747,34662,35506,22057,36008,36838,36942,38686,34442,20420,23784,25105,29273,30011,33253,33469,34558,36032,38597,39187,39381,20171,20250,35299,22238,22602,22730,24315,24555,24618,24724,24674,25040,25106,25296,25913,39745,26214,26800,28023,28784,30028,30342,32117,33445,34809,38283,38542,35997,20977,21182,22806,21683,23475,23830,24936,27010,28079,30861,33995,34903,35442,37799,39608,28012,39336,34521,22435,26623,34510,37390,21123,22151,21508,24275,25313,25785,26684,26680,27579,29554,30906,31339,35226,35282,36203,36611,37101,38307,38548,38761,23398,23731,27005,38989,38990,25499,31520,27179,27263,26806,39949,28511,21106,21917,24688,25324,27963,28167,28369,33883,35088,36676,19988,39993,21494,26907,27194,38788,26666,20828,31427,33970,37340,37772,22107,40232,26658,33541,33841,31909,21000,33477,29926,20094,20355,20896,23506,21002,21208,21223,24059,21914,22570,23014,23436,23448,23515,24178,24185,24739,24863,24931,25022,25563,25954,26577,26707,26874,27454,27475,27735,28450,28567,28485,29872,29976,30435,30475,31487,31649,31777,32233,32566,32752,32925,33382,33694,35251,35532,36011,36996,37969,38291,38289,38306,38501,38867,39208,33304,20024,21547,23736,24012,29609,30284,30524,23721,32747,36107,38593,38929,38996,39000,20225,20238,21361,21916,22120,22522,22855,23305,23492,23696,24076,24190,24524,25582,26426,26071,26082,26399,26827,26820,27231,24112,27589,27671,27773,30079,31048,23395,31232,32000,24509,35215,35352,36020,36215,36556,36637,39138,39438,39740,20096,20605,20736,22931,23452,25135,25216,25836,27450,29344,30097,31047,32681,34811,35516,35696,25516,33738,38816,21513,21507,21931,26708,27224,35440,30759,26485,40653,21364,23458,33050,34384,36870,19992,20037,20167,20241,21450,21560,23470,24339,24613,25937,26429,27714,27762,27875,28792,29699,31350,31406,31496,32026,31998,32102,26087,29275,21435,23621,24040,25298,25312,25369,28192,34394,35377,36317,37624,28417,31142,39770,20136,20139,20140,20379,20384,20689,20807,31478,20849,20982,21332,21281,21375,21483,21932,22659,23777,24375,24394,24623,24656,24685,25375,25945,27211,27841,29378,29421,30703,33016,33029,33288,34126,37111,37857,38911,39255,39514,20208,20957,23597,26241,26989,23616,26354,26997,29577,26704,31873,20677,21220,22343,24062,37670,26020,27427,27453,29748,31105,31165,31563,32202,33465,33740,34943,35167,35641,36817,37329,21535,37504,20061,20534,21477,21306,29399,29590,30697,33510,36527,39366,39368,39378,20855,24858,34398,21936,31354,20598,23507,36935,38533,20018,27355,37351,23633,23624,25496,31391,27795,38772,36705,31402,29066,38536,31874,26647,32368,26705,37740,21234,21531,34219,35347,32676,36557,37089,21350,34952,31041,20418,20670,21009,20804,21843,22317,29674,22411,22865,24418,24452,24693,24950,24935,25001,25522,25658,25964,26223,26690,28179,30054,31293,31995,32076,32153,32331,32619,33550,33610,34509,35336,35427,35686,36605,38938,40335,33464,36814,39912,21127,25119,25731,28608,38553,26689,20625,27424,27770,28500,31348,32080,34880,35363,26376,20214,20537,20518,20581,20860,21048,21091,21927,22287,22533,23244,24314,25010,25080,25331,25458,26908,27177,29309,29356,29486,30740,30831,32121,30476,32937,35211,35609,36066,36562,36963,37749,38522,38997,39443,40568,20803,21407,21427,24187,24358,28187,28304,29572,29694,32067,33335,35328,35578,38480,20046,20491,21476,21628,22266,22993,23396,24049,24235,24359,25144,25925,26543,28246,29392,31946,34996,32929,32993,33776,34382,35463,36328,37431,38599,39015,40723,20116,20114,20237,21320,21577,21566,23087,24460,24481,24735,26791,27278,29786,30849,35486,35492,35703,37264,20062,39881,20132,20348,20399,20505,20502,20809,20844,21151,21177,21246,21402,21475,21521,21518,21897,22353,22434,22909,23380,23389,23439,24037,24039,24055,24184,24195,24218,24247,24344,24658,24908,25239,25304,25511,25915,26114,26179,26356,26477,26657,26775,27083,27743,27946,28009,28207,28317,30002,30343,30828,31295,31968,32005,32024,32094,32177,32789,32771,32943,32945,33108,33167,33322,33618,34892,34913,35611,36002,36092,37066,37237,37489,30783,37628,38308,38477,38917,39321,39640,40251,21083,21163,21495,21512,22741,25335,28640,35946,36703,40633,20811,21051,21578,22269,31296,37239,40288,40658,29508,28425,33136,29969,24573,24794,39592,29403,36796,27492,38915,20170,22256,22372,22718,23130,24680,25031,26127,26118,26681,26801,28151,30165,32058,33390,39746,20123,20304,21449,21766,23919,24038,24046,26619,27801,29811,30722,35408,37782,35039,22352,24231,25387,20661,20652,20877,26368,21705,22622,22971,23472,24425,25165,25505,26685,27507,28168,28797,37319,29312,30741,30758,31085,25998,32048,33756,35009,36617,38555,21092,22312,26448,32618,36001,20916,22338,38442,22586,27018,32948,21682,23822,22524,30869,40442,20316,21066,21643,25662,26152,26388,26613,31364,31574,32034,37679,26716,39853,31545,21273,20874,21047,23519,25334,25774,25830,26413,27578,34217,38609,30352,39894,25420,37638,39851,30399,26194,19977,20632,21442,23665,24808,25746,25955,26719,29158,29642,29987,31639,32386,34453,35715,36059,37240,39184,26028,26283,27531,20181,20180,20282,20351,21050,21496,21490,21987,22235,22763,22987,22985,23039,23376,23629,24066,24107,24535,24605,25351,25903,23388,26031,26045,26088,26525,27490,27515,27663,29509,31049,31169,31992,32025,32043,32930,33026,33267,35222,35422,35433,35430,35468,35566,36039,36060,38604,39164,27503,20107,20284,20365,20816,23383,23546,24904,25345,26178,27425,28363,27835,29246,29885,30164,30913,31034,32780,32819,33258,33940,36766,27728,40575,24335,35672,40235,31482,36600,23437,38635,19971,21489,22519,22833,23241,23460,24713,28287,28422,30142,36074,23455,34048,31712,20594,26612,33437,23649,34122,32286,33294,20889,23556,25448,36198,26012,29038,31038,32023,32773,35613,36554,36974,34503,37034,20511,21242,23610,26451,28796,29237,37196,37320,37675,33509,23490,24369,24825,20027,21462,23432,25163,26417,27530,29417,29664,31278,33131,36259,37202,39318,20754,21463,21610,23551,25480,27193,32172,38656,22234,21454,21608,23447,23601,24030,20462,24833,25342,27954,31168,31179,32066,32333,32722,33261,33311,33936,34886,35186,35728,36468,36655,36913,37195,37228,38598,37276,20160,20303,20805,21313,24467,25102,26580,27713,28171,29539,32294,37325,37507,21460,22809,23487,28113,31069,32302,31899,22654,29087,20986,34899,36848,20426,23803,26149,30636,31459,33308,39423,20934,24490,26092,26991,27529,28147,28310,28516,30462,32020,24033,36981,37255,38918,20966,21021,25152,26257,26329,28186,24246,32210,32626,26360,34223,34295,35576,21161,21465,22899,24207,24464,24661,37604,38500,20663,20767,21213,21280,21319,21484,21736,21830,21809,22039,22888,22974,23100,23477,23558,23567,23569,23578,24196,24202,24288,24432,25215,25220,25307,25484,25463,26119,26124,26157,26230,26494,26786,27167,27189,27836,28040,28169,28248,28988,28966,29031,30151,30465,30813,30977,31077,31216,31456,31505,31911,32057,32918,33750,33931,34121,34909,35059,35359,35388,35412,35443,35937,36062,37284,37478,37758,37912,38556,38808,19978,19976,19998,20055,20887,21104,22478,22580,22732,23330,24120,24773,25854,26465,26454,27972,29366,30067,31331,33976,35698,37304,37664,22065,22516,39166,25325,26893,27542,29165,32340,32887,33394,35302,39135,34645,36785,23611,20280,20449,20405,21767,23072,23517,23529,24515,24910,25391,26032,26187,26862,27035,28024,28145,30003,30137,30495,31070,31206,32051,33251,33455,34218,35242,35386,36523,36763,36914,37341,38663,20154,20161,20995,22645,22764,23563,29978,23613,33102,35338,36805,38499,38765,31525,35535,38920,37218,22259,21416,36887,21561,22402,24101,25512,27700,28810,30561,31883,32736,34928,36930,37204,37648,37656,38543,29790,39620,23815,23913,25968,26530,36264,38619,25454,26441,26905,33733,38935,38592,35070,28548,25722,23544,19990,28716,30045,26159,20932,21046,21218,22995,24449,24615,25104,25919,25972,26143,26228,26866,26646,27491,28165,29298,29983,30427,31934,32854,22768,35069,35199,35488,35475,35531,36893,37266,38738,38745,25993,31246,33030,38587,24109,24796,25114,26021,26132,26512,30707,31309,31821,32318,33034,36012,36196,36321,36447,30889,20999,25305,25509,25666,25240,35373,31363,31680,35500,38634,32118,33292,34633,20185,20808,21315,21344,23459,23554,23574,24029,25126,25159,25776,26643,26676,27849,27973,27927,26579,28508,29006,29053,26059,31359,31661,32218,32330,32680,33146,33307,33337,34214,35438,36046,36341,36984,36983,37549,37521,38275,39854,21069,21892,28472,28982,20840,31109,32341,33203,31950,22092,22609,23720,25514,26366,26365,26970,29401,30095,30094,30990,31062,31199,31895,32032,32068,34311,35380,38459,36961,40736,20711,21109,21452,21474,20489,21930,22766,22863,29245,23435,23652,21277,24803,24819,25436,25475,25407,25531,25805,26089,26361,24035,27085,27133,28437,29157,20105,30185,30456,31379,31967,32207,32156,32865,33609,33624,33900,33980,34299,35013,36208,36865,36973,37783,38684,39442,20687,22679,24974,33235,34101,36104,36896,20419,20596,21063,21363,24687,25417,26463,28204,36275,36895,20439,23646,36042,26063,32154,21330,34966,20854,25539,23384,23403,23562,25613,26449,36956,20182,22810,22826,27760,35409,21822,22549,22949,24816,25171,26561,33333,26965,38464,39364,39464,20307,22534,23550,32784,23729,24111,24453,24608,24907,25140,26367,27888,28382,32974,33151,33492,34955,36024,36864,36910,38538,40667,39899,20195,21488,22823,31532,37261,38988,40441,28381,28711,21331,21828,23429,25176,25246,25299,27810,28655,29730,35351,37944,28609,35582,33592,20967,34552,21482,21481,20294,36948,36784,22890,33073,24061,31466,36799,26842,35895,29432,40008,27197,35504,20025,21336,22022,22374,25285,25506,26086,27470,28129,28251,28845,30701,31471,31658,32187,32829,32966,34507,35477,37723,22243,22727,24382,26029,26262,27264,27573,30007,35527,20516,30693,22320,24347,24677,26234,27744,30196,31258,32622,33268,34584,36933,39347,31689,30044,31481,31569,33988,36880,31209,31378,33590,23265,30528,20013,20210,23449,24544,25277,26172,26609,27880,34411,34935,35387,37198,37619,39376,27159,28710,29482,33511,33879,36015,19969,20806,20939,21899,23541,24086,24115,24193,24340,24373,24427,24500,25074,25361,26274,26397,28526,29266,30010,30522,32884,33081,33144,34678,35519,35548,36229,36339,37530,38263,38914,40165,21189,25431,30452,26389,27784,29645,36035,37806,38515,27941,22684,26894,27084,36861,37786,30171,36890,22618,26626,25524,27131,20291,28460,26584,36795,34086,32180,37716,26943,28528,22378,22775,23340,32044,29226,21514,37347,40372,20141,20302,20572,20597,21059,35998,21576,22564,23450,24093,24213,24237,24311,24351,24716,25269,25402,25552,26799,27712,30855,31118,31243,32224,33351,35330,35558,36420,36883,37048,37165,37336,40718,27877,25688,25826,25973,28404,30340,31515,36969,37841,28346,21746,24505,25764,36685,36845,37444,20856,22635,22825,23637,24215,28155,32399,29980,36028,36578,39003,28857,20253,27583,28593,30000,38651,20814,21520,22581,22615,22956,23648,24466,26007,26460,28193,30331,33759,36077,36884,37117,37709,30757,30778,21162,24230,22303,22900,24594,20498,20826,20908,20941,20992,21776,22612,22616,22871,23445,23798,23947,24764,25237,25645,26481,26691,26812,26847,30423,28120,28271,28059,28783,29128,24403,30168,31095,31561,31572,31570,31958,32113,21040,33891,34153,34276,35342,35588,35910,36367,36867,36879,37913,38518,38957,39472,38360,20685,21205,21516,22530,23566,24999,25758,27934,30643,31461,33012,33796,36947,37509,23776,40199,21311,24471,24499,28060,29305,30563,31167,31716,27602,29420,35501,26627,27233,20984,31361,26932,23626,40182,33515,23493,37193,28702,22136,23663,24775,25958,27788,35930,36929,38931,21585,26311,37389,22856,37027,20869,20045,20970,34201,35598,28760,25466,37707,26978,39348,32260,30071,21335,26976,36575,38627,27741,20108,23612,24336,36841,21250,36049,32905,34425,24319,26085,20083,20837,22914,23615,38894,20219,22922,24525,35469,28641,31152,31074,23527,33905,29483,29105,24180,24565,25467,25754,29123,31896,20035,24316,20043,22492,22178,24745,28611,32013,33021,33075,33215,36786,35223,34468,24052,25226,25773,35207,26487,27874,27966,29750,30772,23110,32629,33453,39340,20467,24259,25309,25490,25943,26479,30403,29260,32972,32954,36649,37197,20493,22521,23186,26757,26995,29028,29437,36023,22770,36064,38506,36889,34687,31204,30695,33833,20271,21093,21338,25293,26575,27850,30333,31636,31893,33334,34180,36843,26333,28448,29190,32283,33707,39361,40614,20989,31665,30834,31672,32903,31560,27368,24161,32908,30033,30048,20843,37474,28300,30330,37271,39658,20240,32624,25244,31567,38309,40169,22138,22617,34532,38588,20276,21028,21322,21453,21467,24070,25644,26001,26495,27710,27726,29256,29359,29677,30036,32321,33324,34281,36009,31684,37318,29033,38930,39151,25405,26217,30058,30436,30928,34115,34542,21290,21329,21542,22915,24199,24444,24754,25161,25209,25259,26000,27604,27852,30130,30382,30865,31192,32203,32631,32933,34987,35513,36027,36991,38750,39131,27147,31800,20633,23614,24494,26503,27608,29749,30473,32654,40763,26570,31255,21305,30091,39661,24422,33181,33777,32920,24380,24517,30050,31558,36924,26727,23019,23195,32016,30334,35628,20469,24426,27161,27703,28418,29922,31080,34920,35413,35961,24287,25551,30149,31186,33495,37672,37618,33948,34541,39981,21697,24428,25996,27996,28693,36007,36051,38971,25935,29942,19981,20184,22496,22827,23142,23500,20904,24067,24220,24598,25206,25975,26023,26222,28014,29238,31526,33104,33178,33433,35676,36000,36070,36212,38428,38468,20398,25771,27494,33310,33889,34154,37096,23553,26963,39080,33914,34135,20239,21103,24489,24133,26381,31119,33145,35079,35206,28149,24343,25173,27832,20175,29289,39826,20998,21563,22132,22707,24996,25198,28954,22894,31881,31966,32027,38640,25991,32862,19993,20341,20853,22592,24163,24179,24330,26564,20006,34109,38281,38491,31859,38913,20731,22721,30294,30887,21029,30629,34065,31622,20559,22793,29255,31687,32232,36794,36820,36941,20415,21193,23081,24321,38829,20445,33303,37610,22275,25429,27497,29995,35036,36628,31298,21215,22675,24917,25098,26286,27597,31807,33769,20515,20472,21253,21574,22577,22857,23453,23792,23791,23849,24214,25265,25447,25918,26041,26379,27861,27873,28921,30770,32299,32990,33459,33804,34028,34562,35090,35370,35914,37030,37586,39165,40179,40300,20047,20129,20621,21078,22346,22952,24125,24536,24537,25151,26292,26395,26576,26834,20882,32033,32938,33192,35584,35980,36031,37502,38450,21536,38956,21271,20693,21340,22696,25778,26420,29287,30566,31302,37350,21187,27809,27526,22528,24140,22868,26412,32763,20961,30406,25705,30952,39764,40635,22475,22969,26151,26522,27598,21737,27097,24149,33180,26517,39850,26622,40018,26717,20134,20451,21448,25273,26411,27819,36804,20397,32365,40639,19975,24930,28288,28459,34067,21619,26410,39749,24051,31637,23724,23494,34588,28234,34001,31252,33032,22937,31885,27665,30496,21209,22818,28961,29279,30683,38695,40289,26891,23167,23064,20901,21517,21629,26126,30431,36855,37528,40180,23018,29277,28357,20813,26825,32191,32236,38754,40634,25720,27169,33538,22916,23391,27611,29467,30450,32178,32791,33945,20786,26408,40665,30446,26466,21247,39173,23588,25147,31870,36016,21839,24758,32011,38272,21249,20063,20918,22812,29242,32822,37326,24357,30690,21380,24441,32004,34220,35379,36493,38742,26611,34222,37971,24841,24840,27833,30290,35565,36664,21807,20305,20778,21191,21451,23461,24189,24736,24962,25558,26377,26586,28263,28044,29494,29495,30001,31056,35029,35480,36938,37009,37109,38596,34701,22805,20104,20313,19982,35465,36671,38928,20653,24188,22934,23481,24248,25562,25594,25793,26332,26954,27096,27915,28342,29076,29992,31407,32650,32768,33865,33993,35201,35617,36362,36965,38525,39178,24958,25233,27442,27779,28020,32716,32764,28096,32645,34746,35064,26469,33713,38972,38647,27931,32097,33853,37226,20081,21365,23888,27396,28651,34253,34349,35239,21033,21519,23653,26446,26792,29702,29827,30178,35023,35041,37324,38626,38520,24459,29575,31435,33870,25504,30053,21129,27969,28316,29705,30041,30827,31890,38534,31452,40845,20406,24942,26053,34396,20102,20142,20698,20001,20940,23534,26009,26753,28092,29471,30274,30637,31260,31975,33391,35538,36988,37327,38517,38936,21147,32209,20523,21400,26519,28107,29136,29747,33256,36650,38563,40023,40607,29792,22593,28057,32047,39006,20196,20278,20363,20919,21169,23994,24604,29618,31036,33491,37428,38583,38646,38666,40599,40802,26278,27508,21015,21155,28872,35010,24265,24651,24976,28451,29001,31806,32244,32879,34030,36899,37676,21570,39791,27347,28809,36034,36335,38706,21172,23105,24266,24324,26391,27004,27028,28010,28431,29282,29436,31725,32769,32894,34635,37070,20845,40595,31108,32907,37682,35542,20525,21644,35441,27498,36036,33031,24785,26528,40434,20121,20120,39952,35435,34241,34152,26880,28286,30871,33109,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,24332,19984,19989,20010,20017,20022,20028,20031,20034,20054,20056,20098,20101,35947,20106,33298,24333,20110,20126,20127,20128,20130,20144,20147,20150,20174,20173,20164,20166,20162,20183,20190,20205,20191,20215,20233,20314,20272,20315,20317,20311,20295,20342,20360,20367,20376,20347,20329,20336,20369,20335,20358,20374,20760,20436,20447,20430,20440,20443,20433,20442,20432,20452,20453,20506,20520,20500,20522,20517,20485,20252,20470,20513,20521,20524,20478,20463,20497,20486,20547,20551,26371,20565,20560,20552,20570,20566,20588,20600,20608,20634,20613,20660,20658,20681,20682,20659,20674,20694,20702,20709,20717,20707,20718,20729,20725,20745,20737,20738,20758,20757,20756,20762,20769,20794,20791,20796,20795,20799,20800,20818,20812,20820,20834,31480,20841,20842,20846,20864,20866,22232,20876,20873,20879,20881,20883,20885,20886,20900,20902,20898,20905,20906,20907,20915,20913,20914,20912,20917,20925,20933,20937,20955,20960,34389,20969,20973,20976,20981,20990,20996,21003,21012,21006,21031,21034,21038,21043,21049,21071,21060,21067,21068,21086,21076,21098,21108,21097,21107,21119,21117,21133,21140,21138,21105,21128,21137,36776,36775,21164,21165,21180,21173,21185,21197,21207,21214,21219,21222,39149,21216,21235,21237,21240,21241,21254,21256,30008,21261,21264,21263,21269,21274,21283,21295,21297,21299,21304,21312,21318,21317,19991,21321,21325,20950,21342,21353,21358,22808,21371,21367,21378,21398,21408,21414,21413,21422,21424,21430,21443,31762,38617,21471,26364,29166,21486,21480,21485,21498,21505,21565,21568,21548,21549,21564,21550,21558,21545,21533,21582,21647,21621,21646,21599,21617,21623,21616,21650,21627,21632,21622,21636,21648,21638,21703,21666,21688,21669,21676,21700,21704,21672,21675,21698,21668,21694,21692,21720,21733,21734,21775,21780,21757,21742,21741,21754,21730,21817,21824,21859,21836,21806,21852,21829,21846,21847,21816,21811,21853,21913,21888,21679,21898,21919,21883,21886,21912,21918,21934,21884,21891,21929,21895,21928,21978,21957,21983,21956,21980,21988,21972,22036,22007,22038,22014,22013,22043,22009,22094,22096,29151,22068,22070,22066,22072,22123,22116,22063,22124,22122,22150,22144,22154,22176,22164,22159,22181,22190,22198,22196,22210,22204,22209,22211,22208,22216,22222,22225,22227,22231,22254,22265,22272,22271,22276,22281,22280,22283,22285,22291,22296,22294,21959,22300,22310,22327,22328,22350,22331,22336,22351,22377,22464,22408,22369,22399,22409,22419,22432,22451,22436,22442,22448,22467,22470,22484,22482,22483,22538,22486,22499,22539,22553,22557,22642,22561,22626,22603,22640,27584,22610,22589,22649,22661,22713,22687,22699,22714,22750,22715,22712,22702,22725,22739,22737,22743,22745,22744,22757,22748,22756,22751,22767,22778,22777,22779,22780,22781,22786,22794,22800,22811,26790,22821,22828,22829,22834,22840,22846,31442,22869,22864,22862,22874,22872,22882,22880,22887,22892,22889,22904,22913,22941,20318,20395,22947,22962,22982,23016,23004,22925,23001,23002,23077,23071,23057,23068,23049,23066,23104,23148,23113,23093,23094,23138,23146,23194,23228,23230,23243,23234,23229,23267,23255,23270,23273,23254,23290,23291,23308,23307,23318,23346,23248,23338,23350,23358,23363,23365,23360,23377,23381,23386,23387,23397,23401,23408,23411,23413,23416,25992,23418,23424,23427,23462,23480,23491,23495,23497,23508,23504,23524,23526,23522,23518,23525,23531,23536,23542,23539,23557,23559,23560,23565,23571,23584,23586,23592,23608,23609,23617,23622,23630,23635,23632,23631,23409,23660,23662,20066,23670,23673,23692,23697,23700,22939,23723,23739,23734,23740,23735,23749,23742,23751,23769,23785,23805,23802,23789,23948,23786,23819,23829,23831,23900,23839,23835,23825,23828,23842,23834,23833,23832,23884,23890,23886,23883,23916,23923,23926,23943,23940,23938,23970,23965,23980,23982,23997,23952,23991,23996,24009,24013,24019,24018,24022,24027,24043,24050,24053,24075,24090,24089,24081,24091,24118,24119,24132,24131,24128,24142,24151,24148,24159,24162,24164,24135,24181,24182,24186,40636,24191,24224,24257,24258,24264,24272,24271,24278,24291,24285,24282,24283,24290,24289,24296,24297,24300,24305,24307,24304,24308,24312,24318,24323,24329,24413,24412,24331,24337,24342,24361,24365,24376,24385,24392,24396,24398,24367,24401,24406,24407,24409,24417,24429,24435,24439,24451,24450,24447,24458,24456,24465,24455,24478,24473,24472,24480,24488,24493,24508,24534,24571,24548,24568,24561,24541,24755,24575,24609,24672,24601,24592,24617,24590,24625,24603,24597,24619,24614,24591,24634,24666,24641,24682,24695,24671,24650,24646,24653,24675,24643,24676,24642,24684,24683,24665,24705,24717,24807,24707,24730,24708,24731,24726,24727,24722,24743,24715,24801,24760,24800,24787,24756,24560,24765,24774,24757,24792,24909,24853,24838,24822,24823,24832,24820,24826,24835,24865,24827,24817,24845,24846,24903,24894,24872,24871,24906,24895,24892,24876,24884,24893,24898,24900,24947,24951,24920,24921,24922,24939,24948,24943,24933,24945,24927,24925,24915,24949,24985,24982,24967,25004,24980,24986,24970,24977,25003,25006,25036,25034,25033,25079,25032,25027,25030,25018,25035,32633,25037,25062,25059,25078,25082,25076,25087,25085,25084,25086,25088,25096,25097,25101,25100,25108,25115,25118,25121,25130,25134,25136,25138,25139,25153,25166,25182,25187,25179,25184,25192,25212,25218,25225,25214,25234,25235,25238,25300,25219,25236,25303,25297,25275,25295,25343,25286,25812,25288,25308,25292,25290,25282,25287,25243,25289,25356,25326,25329,25383,25346,25352,25327,25333,25424,25406,25421,25628,25423,25494,25486,25472,25515,25462,25507,25487,25481,25503,25525,25451,25449,25534,25577,25536,25542,25571,25545,25554,25590,25540,25622,25652,25606,25619,25638,25654,25885,25623,25640,25615,25703,25711,25718,25678,25898,25749,25747,25765,25769,25736,25788,25818,25810,25797,25799,25787,25816,25794,25841,25831,33289,25824,25825,25260,25827,25839,25900,25846,25844,25842,25850,25856,25853,25880,25884,25861,25892,25891,25899,25908,25909,25911,25910,25912,30027,25928,25942,25941,25933,25944,25950,25949,25970,25976,25986,25987,35722,26011,26015,26027,26039,26051,26054,26049,26052,26060,26066,26075,26073,26080,26081,26097,26482,26122,26115,26107,26483,26165,26166,26164,26140,26191,26180,26185,26177,26206,26205,26212,26215,26216,26207,26210,26224,26243,26248,26254,26249,26244,26264,26269,26305,26297,26313,26302,26300,26308,26296,26326,26330,26336,26175,26342,26345,26352,26357,26359,26383,26390,26398,26406,26407,38712,26414,26431,26422,26433,26424,26423,26438,26462,26464,26457,26467,26468,26505,26480,26537,26492,26474,26508,26507,26534,26529,26501,26551,26607,26548,26604,26547,26601,26552,26596,26590,26589,26594,26606,26553,26574,26566,26599,27292,26654,26694,26665,26688,26701,26674,26702,26803,26667,26713,26723,26743,26751,26783,26767,26797,26772,26781,26779,26755,27310,26809,26740,26805,26784,26810,26895,26765,26750,26881,26826,26888,26840,26914,26918,26849,26892,26829,26836,26855,26837,26934,26898,26884,26839,26851,26917,26873,26848,26863,26920,26922,26906,26915,26913,26822,27001,26999,26972,27000,26987,26964,27006,26990,26937,26996,26941,26969,26928,26977,26974,26973,27009,26986,27058,27054,27088,27071,27073,27091,27070,27086,23528,27082,27101,27067,27075,27047,27182,27025,27040,27036,27029,27060,27102,27112,27138,27163,27135,27402,27129,27122,27111,27141,27057,27166,27117,27156,27115,27146,27154,27329,27171,27155,27204,27148,27250,27190,27256,27207,27234,27225,27238,27208,27192,27170,27280,27277,27296,27268,27298,27299,27287,34327,27323,27331,27330,27320,27315,27308,27358,27345,27359,27306,27354,27370,27387,27397,34326,27386,27410,27414,39729,27423,27448,27447,30428,27449,39150,27463,27459,27465,27472,27481,27476,27483,27487,27489,27512,27513,27519,27520,27524,27523,27533,27544,27541,27550,27556,27562,27563,27567,27570,27569,27571,27575,27580,27590,27595,27603,27615,27628,27627,27635,27631,40638,27656,27667,27668,27675,27684,27683,27742,27733,27746,27754,27778,27789,27802,27777,27803,27774,27752,27763,27794,27792,27844,27889,27859,27837,27863,27845,27869,27822,27825,27838,27834,27867,27887,27865,27882,27935,34893,27958,27947,27965,27960,27929,27957,27955,27922,27916,28003,28051,28004,27994,28025,27993,28046,28053,28644,28037,28153,28181,28170,28085,28103,28134,28088,28102,28140,28126,28108,28136,28114,28101,28154,28121,28132,28117,28138,28142,28205,28270,28206,28185,28274,28255,28222,28195,28267,28203,28278,28237,28191,28227,28218,28238,28196,28415,28189,28216,28290,28330,28312,28361,28343,28371,28349,28335,28356,28338,28372,28373,28303,28325,28354,28319,28481,28433,28748,28396,28408,28414,28479,28402,28465,28399,28466,28364,28478,28435,28407,28550,28538,28536,28545,28544,28527,28507,28659,28525,28546,28540,28504,28558,28561,28610,28518,28595,28579,28577,28580,28601,28614,28586,28639,28629,28652,28628,28632,28657,28654,28635,28681,28683,28666,28689,28673,28687,28670,28699,28698,28532,28701,28696,28703,28720,28734,28722,28753,28771,28825,28818,28847,28913,28844,28856,28851,28846,28895,28875,28893,28889,28937,28925,28956,28953,29029,29013,29064,29030,29026,29004,29014,29036,29071,29179,29060,29077,29096,29100,29143,29113,29118,29138,29129,29140,29134,29152,29164,29159,29173,29180,29177,29183,29197,29200,29211,29224,29229,29228,29232,29234,29243,29244,29247,29248,29254,29259,29272,29300,29310,29314,29313,29319,29330,29334,29346,29351,29369,29362,29379,29382,29380,29390,29394,29410,29408,29409,29433,29431,20495,29463,29450,29468,29462,29469,29492,29487,29481,29477,29502,29518,29519,40664,29527,29546,29544,29552,29560,29557,29563,29562,29640,29619,29646,29627,29632,29669,29678,29662,29858,29701,29807,29733,29688,29746,29754,29781,29759,29791,29785,29761,29788,29801,29808,29795,29802,29814,29822,29835,29854,29863,29898,29903,29908,29681,29920,29923,29927,29929,29934,29938,29936,29937,29944,29943,29956,29955,29957,29964,29966,29965,29973,29971,29982,29990,29996,30012,30020,30029,30026,30025,30043,30022,30042,30057,30052,30055,30059,30061,30072,30070,30086,30087,30068,30090,30089,30082,30100,30106,30109,30117,30115,30146,30131,30147,30133,30141,30136,30140,30129,30157,30154,30162,30169,30179,30174,30206,30207,30204,30209,30192,30202,30194,30195,30219,30221,30217,30239,30247,30240,30241,30242,30244,30260,30256,30267,30279,30280,30278,30300,30296,30305,30306,30312,30313,30314,30311,30316,30320,30322,30326,30328,30332,30336,30339,30344,30347,30350,30358,30355,30361,30362,30384,30388,30392,30393,30394,30402,30413,30422,30418,30430,30433,30437,30439,30442,34351,30459,30472,30471,30468,30505,30500,30494,30501,30502,30491,30519,30520,30535,30554,30568,30571,30555,30565,30591,30590,30585,30606,30603,30609,30624,30622,30640,30646,30649,30655,30652,30653,30651,30663,30669,30679,30682,30684,30691,30702,30716,30732,30738,31014,30752,31018,30789,30862,30836,30854,30844,30874,30860,30883,30901,30890,30895,30929,30918,30923,30932,30910,30908,30917,30922,30956,30951,30938,30973,30964,30983,30994,30993,31001,31020,31019,31040,31072,31063,31071,31066,31061,31059,31098,31103,31114,31133,31143,40779,31146,31150,31155,31161,31162,31177,31189,31207,31212,31201,31203,31240,31245,31256,31257,31264,31263,31104,31281,31291,31294,31287,31299,31319,31305,31329,31330,31337,40861,31344,31353,31357,31368,31383,31381,31384,31382,31401,31432,31408,31414,31429,31428,31423,36995,31431,31434,31437,31439,31445,31443,31449,31450,31453,31457,31458,31462,31469,31472,31490,31503,31498,31494,31539,31512,31513,31518,31541,31528,31542,31568,31610,31492,31565,31499,31564,31557,31605,31589,31604,31591,31600,31601,31596,31598,31645,31640,31647,31629,31644,31642,31627,31634,31631,31581,31641,31691,31681,31692,31695,31668,31686,31709,31721,31761,31764,31718,31717,31840,31744,31751,31763,31731,31735,31767,31757,31734,31779,31783,31786,31775,31799,31787,31805,31820,31811,31828,31823,31808,31824,31832,31839,31844,31830,31845,31852,31861,31875,31888,31908,31917,31906,31915,31905,31912,31923,31922,31921,31918,31929,31933,31936,31941,31938,31960,31954,31964,31970,39739,31983,31986,31988,31990,31994,32006,32002,32028,32021,32010,32069,32075,32046,32050,32063,32053,32070,32115,32086,32078,32114,32104,32110,32079,32099,32147,32137,32091,32143,32125,32155,32186,32174,32163,32181,32199,32189,32171,32317,32162,32175,32220,32184,32159,32176,32216,32221,32228,32222,32251,32242,32225,32261,32266,32291,32289,32274,32305,32287,32265,32267,32290,32326,32358,32315,32309,32313,32323,32311,32306,32314,32359,32349,32342,32350,32345,32346,32377,32362,32361,32380,32379,32387,32213,32381,36782,32383,32392,32393,32396,32402,32400,32403,32404,32406,32398,32411,32412,32568,32570,32581,32588,32589,32590,32592,32593,32597,32596,32600,32607,32608,32616,32617,32615,32632,32642,32646,32643,32648,32647,32652,32660,32670,32669,32666,32675,32687,32690,32697,32686,32694,32696,35697,32709,32710,32714,32725,32724,32737,32742,32745,32755,32761,39132,32774,32772,32779,32786,32792,32793,32796,32801,32808,32831,32827,32842,32838,32850,32856,32858,32863,32866,32872,32883,32882,32880,32886,32889,32893,32895,32900,32902,32901,32923,32915,32922,32941,20880,32940,32987,32997,32985,32989,32964,32986,32982,33033,33007,33009,33051,33065,33059,33071,33099,38539,33094,33086,33107,33105,33020,33137,33134,33125,33126,33140,33155,33160,33162,33152,33154,33184,33173,33188,33187,33119,33171,33193,33200,33205,33214,33208,33213,33216,33218,33210,33225,33229,33233,33241,33240,33224,33242,33247,33248,33255,33274,33275,33278,33281,33282,33285,33287,33290,33293,33296,33302,33321,33323,33336,33331,33344,33369,33368,33373,33370,33375,33380,33378,33384,33386,33387,33326,33393,33399,33400,33406,33421,33426,33451,33439,33467,33452,33505,33507,33503,33490,33524,33523,33530,33683,33539,33531,33529,33502,33542,33500,33545,33497,33589,33588,33558,33586,33585,33600,33593,33616,33605,33583,33579,33559,33560,33669,33690,33706,33695,33698,33686,33571,33678,33671,33674,33660,33717,33651,33653,33696,33673,33704,33780,33811,33771,33742,33789,33795,33752,33803,33729,33783,33799,33760,33778,33805,33826,33824,33725,33848,34054,33787,33901,33834,33852,34138,33924,33911,33899,33965,33902,33922,33897,33862,33836,33903,33913,33845,33994,33890,33977,33983,33951,34009,33997,33979,34010,34000,33985,33990,34006,33953,34081,34047,34036,34071,34072,34092,34079,34069,34068,34044,34112,34147,34136,34120,34113,34306,34123,34133,34176,34212,34184,34193,34186,34216,34157,34196,34203,34282,34183,34204,34167,34174,34192,34249,34234,34255,34233,34256,34261,34269,34277,34268,34297,34314,34323,34315,34302,34298,34310,34338,34330,34352,34367,34381,20053,34388,34399,34407,34417,34451,34467,34473,34474,34443,34444,34486,34479,34500,34502,34480,34505,34851,34475,34516,34526,34537,34540,34527,34523,34543,34578,34566,34568,34560,34563,34555,34577,34569,34573,34553,34570,34612,34623,34615,34619,34597,34601,34586,34656,34655,34680,34636,34638,34676,34647,34664,34670,34649,34643,34659,34666,34821,34722,34719,34690,34735,34763,34749,34752,34768,38614,34731,34756,34739,34759,34758,34747,34799,34802,34784,34831,34829,34814,34806,34807,34830,34770,34833,34838,34837,34850,34849,34865,34870,34873,34855,34875,34884,34882,34898,34905,34910,34914,34923,34945,34942,34974,34933,34941,34997,34930,34946,34967,34962,34990,34969,34978,34957,34980,34992,35007,34993,35011,35012,35028,35032,35033,35037,35065,35074,35068,35060,35048,35058,35076,35084,35082,35091,35139,35102,35109,35114,35115,35137,35140,35131,35126,35128,35148,35101,35168,35166,35174,35172,35181,35178,35183,35188,35191,35198,35203,35208,35210,35219,35224,35233,35241,35238,35244,35247,35250,35258,35261,35263,35264,35290,35292,35293,35303,35316,35320,35331,35350,35344,35340,35355,35357,35365,35382,35393,35419,35410,35398,35400,35452,35437,35436,35426,35461,35458,35460,35496,35489,35473,35493,35494,35482,35491,35524,35533,35522,35546,35563,35571,35559,35556,35569,35604,35552,35554,35575,35550,35547,35596,35591,35610,35553,35606,35600,35607,35616,35635,38827,35622,35627,35646,35624,35649,35660,35663,35662,35657,35670,35675,35674,35691,35679,35692,35695,35700,35709,35712,35724,35726,35730,35731,35734,35737,35738,35898,35905,35903,35912,35916,35918,35920,35925,35938,35948,35960,35962,35970,35977,35973,35978,35981,35982,35988,35964,35992,25117,36013,36010,36029,36018,36019,36014,36022,36040,36033,36068,36067,36058,36093,36090,36091,36100,36101,36106,36103,36111,36109,36112,40782,36115,36045,36116,36118,36199,36205,36209,36211,36225,36249,36290,36286,36282,36303,36314,36310,36300,36315,36299,36330,36331,36319,36323,36348,36360,36361,36351,36381,36382,36368,36383,36418,36405,36400,36404,36426,36423,36425,36428,36432,36424,36441,36452,36448,36394,36451,36437,36470,36466,36476,36481,36487,36485,36484,36491,36490,36499,36497,36500,36505,36522,36513,36524,36528,36550,36529,36542,36549,36552,36555,36571,36579,36604,36603,36587,36606,36618,36613,36629,36626,36633,36627,36636,36639,36635,36620,36646,36659,36667,36665,36677,36674,36670,36684,36681,36678,36686,36695,36700,36706,36707,36708,36764,36767,36771,36781,36783,36791,36826,36837,36834,36842,36847,36999,36852,36869,36857,36858,36881,36885,36897,36877,36894,36886,36875,36903,36918,36917,36921,36856,36943,36944,36945,36946,36878,36937,36926,36950,36952,36958,36968,36975,36982,38568,36978,36994,36989,36993,36992,37002,37001,37007,37032,37039,37041,37045,37090,37092,25160,37083,37122,37138,37145,37170,37168,37194,37206,37208,37219,37221,37225,37235,37234,37259,37257,37250,37282,37291,37295,37290,37301,37300,37306,37312,37313,37321,37323,37328,37334,37343,37345,37339,37372,37365,37366,37406,37375,37396,37420,37397,37393,37470,37463,37445,37449,37476,37448,37525,37439,37451,37456,37532,37526,37523,37531,37466,37583,37561,37559,37609,37647,37626,37700,37678,37657,37666,37658,37667,37690,37685,37691,37724,37728,37756,37742,37718,37808,37804,37805,37780,37817,37846,37847,37864,37861,37848,37827,37853,37840,37832,37860,37914,37908,37907,37891,37895,37904,37942,37931,37941,37921,37946,37953,37970,37956,37979,37984,37986,37982,37994,37417,38000,38005,38007,38013,37978,38012,38014,38017,38015,38274,38279,38282,38292,38294,38296,38297,38304,38312,38311,38317,38332,38331,38329,38334,38346,28662,38339,38349,38348,38357,38356,38358,38364,38369,38373,38370,38433,38440,38446,38447,38466,38476,38479,38475,38519,38492,38494,38493,38495,38502,38514,38508,38541,38552,38549,38551,38570,38567,38577,38578,38576,38580,38582,38584,38585,38606,38603,38601,38605,35149,38620,38669,38613,38649,38660,38662,38664,38675,38670,38673,38671,38678,38681,38692,38698,38704,38713,38717,38718,38724,38726,38728,38722,38729,38748,38752,38756,38758,38760,21202,38763,38769,38777,38789,38780,38785,38778,38790,38795,38799,38800,38812,38824,38822,38819,38835,38836,38851,38854,38856,38859,38876,38893,40783,38898,31455,38902,38901,38927,38924,38968,38948,38945,38967,38973,38982,38991,38987,39019,39023,39024,39025,39028,39027,39082,39087,39089,39094,39108,39107,39110,39145,39147,39171,39177,39186,39188,39192,39201,39197,39198,39204,39200,39212,39214,39229,39230,39234,39241,39237,39248,39243,39249,39250,39244,39253,39319,39320,39333,39341,39342,39356,39391,39387,39389,39384,39377,39405,39406,39409,39410,39419,39416,39425,39439,39429,39394,39449,39467,39479,39493,39490,39488,39491,39486,39509,39501,39515,39511,39519,39522,39525,39524,39529,39531,39530,39597,39600,39612,39616,39631,39633,39635,39636,39646,39647,39650,39651,39654,39663,39659,39662,39668,39665,39671,39675,39686,39704,39706,39711,39714,39715,39717,39719,39720,39721,39722,39726,39727,39730,39748,39747,39759,39757,39758,39761,39768,39796,39827,39811,39825,39830,39831,39839,39840,39848,39860,39872,39882,39865,39878,39887,39889,39890,39907,39906,39908,39892,39905,39994,39922,39921,39920,39957,39956,39945,39955,39948,39942,39944,39954,39946,39940,39982,39963,39973,39972,39969,39984,40007,39986,40006,39998,40026,40032,40039,40054,40056,40167,40172,40176,40201,40200,40171,40195,40198,40234,40230,40367,40227,40223,40260,40213,40210,40257,40255,40254,40262,40264,40285,40286,40292,40273,40272,40281,40306,40329,40327,40363,40303,40314,40346,40356,40361,40370,40388,40385,40379,40376,40378,40390,40399,40386,40409,40403,40440,40422,40429,40431,40445,40474,40475,40478,40565,40569,40573,40577,40584,40587,40588,40594,40597,40593,40605,40613,40617,40632,40618,40621,38753,40652,40654,40655,40656,40660,40668,40670,40669,40672,40677,40680,40687,40692,40694,40695,40697,40699,40700,40701,40711,40712,30391,40725,40737,40748,40766,40778,40786,40788,40803,40799,40800,40801,40806,40807,40812,40810,40823,40818,40822,40853,40860,40864,22575,27079,36953,29796,20956,29081,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,32394,35100,37704,37512,34012,20425,28859,26161,26824,37625,26363,24389,20008,20193,20220,20224,20227,20281,20310,20370,20362,20378,20372,20429,20544,20514,20479,20510,20550,20592,20546,20628,20724,20696,20810,20836,20893,20926,20972,21013,21148,21158,21184,21211,21248,21255,21284,21362,21395,21426,21469,64014,21660,21642,21673,21759,21894,22361,22373,22444,22472,22471,64015,64016,22686,22706,22795,22867,22875,22877,22883,22948,22970,23382,23488,29999,23512,23532,23582,23718,23738,23797,23847,23891,64017,23874,23917,23992,23993,24016,24353,24372,24423,24503,24542,24669,24709,24714,24798,24789,24864,24818,24849,24887,24880,24984,25107,25254,25589,25696,25757,25806,25934,26112,26133,26171,26121,26158,26142,26148,26213,26199,26201,64018,26227,26265,26272,26290,26303,26362,26382,63785,26470,26555,26706,26560,26625,26692,26831,64019,26984,64020,27032,27106,27184,27243,27206,27251,27262,27362,27364,27606,27711,27740,27782,27759,27866,27908,28039,28015,28054,28076,28111,28152,28146,28156,28217,28252,28199,28220,28351,28552,28597,28661,28677,28679,28712,28805,28843,28943,28932,29020,28998,28999,64021,29121,29182,29361,29374,29476,64022,29559,29629,29641,29654,29667,29650,29703,29685,29734,29738,29737,29742,29794,29833,29855,29953,30063,30338,30364,30366,30363,30374,64023,30534,21167,30753,30798,30820,30842,31024,64024,64025,64026,31124,64027,31131,31441,31463,64028,31467,31646,64029,32072,32092,32183,32160,32214,32338,32583,32673,64030,33537,33634,33663,33735,33782,33864,33972,34131,34137,34155,64031,34224,64032,64033,34823,35061,35346,35383,35449,35495,35518,35551,64034,35574,35667,35711,36080,36084,36114,36214,64035,36559,64036,64037,36967,37086,64038,37141,37159,37338,37335,37342,37357,37358,37348,37349,37382,37392,37386,37434,37440,37436,37454,37465,37457,37433,37479,37543,37495,37496,37607,37591,37593,37584,64039,37589,37600,37587,37669,37665,37627,64040,37662,37631,37661,37634,37744,37719,37796,37830,37854,37880,37937,37957,37960,38290,63964,64041,38557,38575,38707,38715,38723,38733,38735,38737,38741,38999,39013,64042,64043,39207,64044,39326,39502,39641,39644,39797,39794,39823,39857,39867,39936,40304,40299,64045,40473,40657,null,null,8560,8561,8562,8563,8564,8565,8566,8567,8568,8569,65506,65508,65287,65282,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,8560,8561,8562,8563,8564,8565,8566,8567,8568,8569,8544,8545,8546,8547,8548,8549,8550,8551,8552,8553,65506,65508,65287,65282,12849,8470,8481,8757,32394,35100,37704,37512,34012,20425,28859,26161,26824,37625,26363,24389,20008,20193,20220,20224,20227,20281,20310,20370,20362,20378,20372,20429,20544,20514,20479,20510,20550,20592,20546,20628,20724,20696,20810,20836,20893,20926,20972,21013,21148,21158,21184,21211,21248,21255,21284,21362,21395,21426,21469,64014,21660,21642,21673,21759,21894,22361,22373,22444,22472,22471,64015,64016,22686,22706,22795,22867,22875,22877,22883,22948,22970,23382,23488,29999,23512,23532,23582,23718,23738,23797,23847,23891,64017,23874,23917,23992,23993,24016,24353,24372,24423,24503,24542,24669,24709,24714,24798,24789,24864,24818,24849,24887,24880,24984,25107,25254,25589,25696,25757,25806,25934,26112,26133,26171,26121,26158,26142,26148,26213,26199,26201,64018,26227,26265,26272,26290,26303,26362,26382,63785,26470,26555,26706,26560,26625,26692,26831,64019,26984,64020,27032,27106,27184,27243,27206,27251,27262,27362,27364,27606,27711,27740,27782,27759,27866,27908,28039,28015,28054,28076,28111,28152,28146,28156,28217,28252,28199,28220,28351,28552,28597,28661,28677,28679,28712,28805,28843,28943,28932,29020,28998,28999,64021,29121,29182,29361,29374,29476,64022,29559,29629,29641,29654,29667,29650,29703,29685,29734,29738,29737,29742,29794,29833,29855,29953,30063,30338,30364,30366,30363,30374,64023,30534,21167,30753,30798,30820,30842,31024,64024,64025,64026,31124,64027,31131,31441,31463,64028,31467,31646,64029,32072,32092,32183,32160,32214,32338,32583,32673,64030,33537,33634,33663,33735,33782,33864,33972,34131,34137,34155,64031,34224,64032,64033,34823,35061,35346,35383,35449,35495,35518,35551,64034,35574,35667,35711,36080,36084,36114,36214,64035,36559,64036,64037,36967,37086,64038,37141,37159,37338,37335,37342,37357,37358,37348,37349,37382,37392,37386,37434,37440,37436,37454,37465,37457,37433,37479,37543,37495,37496,37607,37591,37593,37584,64039,37589,37600,37587,37669,37665,37627,64040,37662,37631,37661,37634,37744,37719,37796,37830,37854,37880,37937,37957,37960,38290,63964,64041,38557,38575,38707,38715,38723,38733,38735,38737,38741,38999,39013,64042,64043,39207,64044,39326,39502,39641,39644,39797,39794,39823,39857,39867,39936,40304,40299,64045,40473,40657,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],
+ "jis0212":[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,728,711,184,729,733,175,731,730,65374,900,901,null,null,null,null,null,null,null,null,161,166,191,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,186,170,169,174,8482,164,8470,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,902,904,905,906,938,null,908,null,910,939,null,911,null,null,null,null,940,941,942,943,970,912,972,962,973,971,944,974,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,1026,1027,1028,1029,1030,1031,1032,1033,1034,1035,1036,1038,1039,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,1106,1107,1108,1109,1110,1111,1112,1113,1114,1115,1116,1118,1119,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,198,272,null,294,null,306,null,321,319,null,330,216,338,null,358,222,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,230,273,240,295,305,307,312,322,320,329,331,248,339,223,359,254,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,193,192,196,194,258,461,256,260,197,195,262,264,268,199,266,270,201,200,203,202,282,278,274,280,null,284,286,290,288,292,205,204,207,206,463,304,298,302,296,308,310,313,317,315,323,327,325,209,211,210,214,212,465,336,332,213,340,344,342,346,348,352,350,356,354,218,217,220,219,364,467,368,362,370,366,360,471,475,473,469,372,221,376,374,377,381,379,null,null,null,null,null,null,null,225,224,228,226,259,462,257,261,229,227,263,265,269,231,267,271,233,232,235,234,283,279,275,281,501,285,287,null,289,293,237,236,239,238,464,null,299,303,297,309,311,314,318,316,324,328,326,241,243,242,246,244,466,337,333,245,341,345,343,347,349,353,351,357,355,250,249,252,251,365,468,369,363,371,367,361,472,476,474,470,373,253,255,375,378,382,380,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,19970,19972,19973,19980,19986,19999,20003,20004,20008,20011,20014,20015,20016,20021,20032,20033,20036,20039,20049,20058,20060,20067,20072,20073,20084,20085,20089,20095,20109,20118,20119,20125,20143,20153,20163,20176,20186,20187,20192,20193,20194,20200,20207,20209,20211,20213,20221,20222,20223,20224,20226,20227,20232,20235,20236,20242,20245,20246,20247,20249,20270,20273,20320,20275,20277,20279,20281,20283,20286,20288,20290,20296,20297,20299,20300,20306,20308,20310,20312,20319,20323,20330,20332,20334,20337,20343,20344,20345,20346,20349,20350,20353,20354,20356,20357,20361,20362,20364,20366,20368,20370,20371,20372,20375,20377,20378,20382,20383,20402,20407,20409,20411,20412,20413,20414,20416,20417,20421,20422,20424,20425,20427,20428,20429,20431,20434,20444,20448,20450,20464,20466,20476,20477,20479,20480,20481,20484,20487,20490,20492,20494,20496,20499,20503,20504,20507,20508,20509,20510,20514,20519,20526,20528,20530,20531,20533,20544,20545,20546,20549,20550,20554,20556,20558,20561,20562,20563,20567,20569,20575,20576,20578,20579,20582,20583,20586,20589,20592,20593,20539,20609,20611,20612,20614,20618,20622,20623,20624,20626,20627,20628,20630,20635,20636,20638,20639,20640,20641,20642,20650,20655,20656,20665,20666,20669,20672,20675,20676,20679,20684,20686,20688,20691,20692,20696,20700,20701,20703,20706,20708,20710,20712,20713,20719,20721,20726,20730,20734,20739,20742,20743,20744,20747,20748,20749,20750,20722,20752,20759,20761,20763,20764,20765,20766,20771,20775,20776,20780,20781,20783,20785,20787,20788,20789,20792,20793,20802,20810,20815,20819,20821,20823,20824,20831,20836,20838,20862,20867,20868,20875,20878,20888,20893,20897,20899,20909,20920,20922,20924,20926,20927,20930,20936,20943,20945,20946,20947,20949,20952,20958,20962,20965,20974,20978,20979,20980,20983,20993,20994,20997,21010,21011,21013,21014,21016,21026,21032,21041,21042,21045,21052,21061,21065,21077,21079,21080,21082,21084,21087,21088,21089,21094,21102,21111,21112,21113,21120,21122,21125,21130,21132,21139,21141,21142,21143,21144,21146,21148,21156,21157,21158,21159,21167,21168,21174,21175,21176,21178,21179,21181,21184,21188,21190,21192,21196,21199,21201,21204,21206,21211,21212,21217,21221,21224,21225,21226,21228,21232,21233,21236,21238,21239,21248,21251,21258,21259,21260,21265,21267,21272,21275,21276,21278,21279,21285,21287,21288,21289,21291,21292,21293,21296,21298,21301,21308,21309,21310,21314,21324,21323,21337,21339,21345,21347,21349,21356,21357,21362,21369,21374,21379,21383,21384,21390,21395,21396,21401,21405,21409,21412,21418,21419,21423,21426,21428,21429,21431,21432,21434,21437,21440,21445,21455,21458,21459,21461,21466,21469,21470,21472,21478,21479,21493,21506,21523,21530,21537,21543,21544,21546,21551,21553,21556,21557,21571,21572,21575,21581,21583,21598,21602,21604,21606,21607,21609,21611,21613,21614,21620,21631,21633,21635,21637,21640,21641,21645,21649,21653,21654,21660,21663,21665,21670,21671,21673,21674,21677,21678,21681,21687,21689,21690,21691,21695,21702,21706,21709,21710,21728,21738,21740,21743,21750,21756,21758,21759,21760,21761,21765,21768,21769,21772,21773,21774,21781,21802,21803,21810,21813,21814,21819,21820,21821,21825,21831,21833,21834,21837,21840,21841,21848,21850,21851,21854,21856,21857,21860,21862,21887,21889,21890,21894,21896,21902,21903,21905,21906,21907,21908,21911,21923,21924,21933,21938,21951,21953,21955,21958,21961,21963,21964,21966,21969,21970,21971,21975,21976,21979,21982,21986,21993,22006,22015,22021,22024,22026,22029,22030,22031,22032,22033,22034,22041,22060,22064,22067,22069,22071,22073,22075,22076,22077,22079,22080,22081,22083,22084,22086,22089,22091,22093,22095,22100,22110,22112,22113,22114,22115,22118,22121,22125,22127,22129,22130,22133,22148,22149,22152,22155,22156,22165,22169,22170,22173,22174,22175,22182,22183,22184,22185,22187,22188,22189,22193,22195,22199,22206,22213,22217,22218,22219,22223,22224,22220,22221,22233,22236,22237,22239,22241,22244,22245,22246,22247,22248,22257,22251,22253,22262,22263,22273,22274,22279,22282,22284,22289,22293,22298,22299,22301,22304,22306,22307,22308,22309,22313,22314,22316,22318,22319,22323,22324,22333,22334,22335,22341,22342,22348,22349,22354,22370,22373,22375,22376,22379,22381,22382,22383,22384,22385,22387,22388,22389,22391,22393,22394,22395,22396,22398,22401,22403,22412,22420,22423,22425,22426,22428,22429,22430,22431,22433,22421,22439,22440,22441,22444,22456,22461,22471,22472,22476,22479,22485,22493,22494,22500,22502,22503,22505,22509,22512,22517,22518,22520,22525,22526,22527,22531,22532,22536,22537,22497,22540,22541,22555,22558,22559,22560,22566,22567,22573,22578,22585,22591,22601,22604,22605,22607,22608,22613,22623,22625,22628,22631,22632,22648,22652,22655,22656,22657,22663,22664,22665,22666,22668,22669,22671,22672,22676,22678,22685,22688,22689,22690,22694,22697,22705,22706,22724,22716,22722,22728,22733,22734,22736,22738,22740,22742,22746,22749,22753,22754,22761,22771,22789,22790,22795,22796,22802,22803,22804,34369,22813,22817,22819,22820,22824,22831,22832,22835,22837,22838,22847,22851,22854,22866,22867,22873,22875,22877,22878,22879,22881,22883,22891,22893,22895,22898,22901,22902,22905,22907,22908,22923,22924,22926,22930,22933,22935,22943,22948,22951,22957,22958,22959,22960,22963,22967,22970,22972,22977,22979,22980,22984,22986,22989,22994,23005,23006,23007,23011,23012,23015,23022,23023,23025,23026,23028,23031,23040,23044,23052,23053,23054,23058,23059,23070,23075,23076,23079,23080,23082,23085,23088,23108,23109,23111,23112,23116,23120,23125,23134,23139,23141,23143,23149,23159,23162,23163,23166,23179,23184,23187,23190,23193,23196,23198,23199,23200,23202,23207,23212,23217,23218,23219,23221,23224,23226,23227,23231,23236,23238,23240,23247,23258,23260,23264,23269,23274,23278,23285,23286,23293,23296,23297,23304,23319,23348,23321,23323,23325,23329,23333,23341,23352,23361,23371,23372,23378,23382,23390,23400,23406,23407,23420,23421,23422,23423,23425,23428,23430,23434,23438,23440,23441,23443,23444,23446,23464,23465,23468,23469,23471,23473,23474,23479,23482,23484,23488,23489,23501,23503,23510,23511,23512,23513,23514,23520,23535,23537,23540,23549,23564,23575,23582,23583,23587,23590,23593,23595,23596,23598,23600,23602,23605,23606,23641,23642,23644,23650,23651,23655,23656,23657,23661,23664,23668,23669,23674,23675,23676,23677,23687,23688,23690,23695,23698,23709,23711,23712,23714,23715,23718,23722,23730,23732,23733,23738,23753,23755,23762,23773,23767,23790,23793,23794,23796,23809,23814,23821,23826,23851,23843,23844,23846,23847,23857,23860,23865,23869,23871,23874,23875,23878,23880,23893,23889,23897,23882,23903,23904,23905,23906,23908,23914,23917,23920,23929,23930,23934,23935,23937,23939,23944,23946,23954,23955,23956,23957,23961,23963,23967,23968,23975,23979,23984,23988,23992,23993,24003,24007,24011,24016,24014,24024,24025,24032,24036,24041,24056,24057,24064,24071,24077,24082,24084,24085,24088,24095,24096,24110,24104,24114,24117,24126,24139,24144,24137,24145,24150,24152,24155,24156,24158,24168,24170,24171,24172,24173,24174,24176,24192,24203,24206,24226,24228,24229,24232,24234,24236,24241,24243,24253,24254,24255,24262,24268,24267,24270,24273,24274,24276,24277,24284,24286,24293,24299,24322,24326,24327,24328,24334,24345,24348,24349,24353,24354,24355,24356,24360,24363,24364,24366,24368,24372,24374,24379,24381,24383,24384,24388,24389,24391,24397,24400,24404,24408,24411,24416,24419,24420,24423,24431,24434,24436,24437,24440,24442,24445,24446,24457,24461,24463,24470,24476,24477,24482,24487,24491,24484,24492,24495,24496,24497,24504,24516,24519,24520,24521,24523,24528,24529,24530,24531,24532,24542,24545,24546,24552,24553,24554,24556,24557,24558,24559,24562,24563,24566,24570,24572,24583,24586,24589,24595,24596,24599,24600,24602,24607,24612,24621,24627,24629,24640,24647,24648,24649,24652,24657,24660,24662,24663,24669,24673,24679,24689,24702,24703,24706,24710,24712,24714,24718,24721,24723,24725,24728,24733,24734,24738,24740,24741,24744,24752,24753,24759,24763,24766,24770,24772,24776,24777,24778,24779,24782,24783,24788,24789,24793,24795,24797,24798,24802,24805,24818,24821,24824,24828,24829,24834,24839,24842,24844,24848,24849,24850,24851,24852,24854,24855,24857,24860,24862,24866,24874,24875,24880,24881,24885,24886,24887,24889,24897,24901,24902,24905,24926,24928,24940,24946,24952,24955,24956,24959,24960,24961,24963,24964,24971,24973,24978,24979,24983,24984,24988,24989,24991,24992,24997,25000,25002,25005,25016,25017,25020,25024,25025,25026,25038,25039,25045,25052,25053,25054,25055,25057,25058,25063,25065,25061,25068,25069,25071,25089,25091,25092,25095,25107,25109,25116,25120,25122,25123,25127,25129,25131,25145,25149,25154,25155,25156,25158,25164,25168,25169,25170,25172,25174,25178,25180,25188,25197,25199,25203,25210,25213,25229,25230,25231,25232,25254,25256,25267,25270,25271,25274,25278,25279,25284,25294,25301,25302,25306,25322,25330,25332,25340,25341,25347,25348,25354,25355,25357,25360,25363,25366,25368,25385,25386,25389,25397,25398,25401,25404,25409,25410,25411,25412,25414,25418,25419,25422,25426,25427,25428,25432,25435,25445,25446,25452,25453,25457,25460,25461,25464,25468,25469,25471,25474,25476,25479,25482,25488,25492,25493,25497,25498,25502,25508,25510,25517,25518,25519,25533,25537,25541,25544,25550,25553,25555,25556,25557,25564,25568,25573,25578,25580,25586,25587,25589,25592,25593,25609,25610,25616,25618,25620,25624,25630,25632,25634,25636,25637,25641,25642,25647,25648,25653,25661,25663,25675,25679,25681,25682,25683,25684,25690,25691,25692,25693,25695,25696,25697,25699,25709,25715,25716,25723,25725,25733,25735,25743,25744,25745,25752,25753,25755,25757,25759,25761,25763,25766,25768,25772,25779,25789,25790,25791,25796,25801,25802,25803,25804,25806,25808,25809,25813,25815,25828,25829,25833,25834,25837,25840,25845,25847,25851,25855,25857,25860,25864,25865,25866,25871,25875,25876,25878,25881,25883,25886,25887,25890,25894,25897,25902,25905,25914,25916,25917,25923,25927,25929,25936,25938,25940,25951,25952,25959,25963,25978,25981,25985,25989,25994,26002,26005,26008,26013,26016,26019,26022,26030,26034,26035,26036,26047,26050,26056,26057,26062,26064,26068,26070,26072,26079,26096,26098,26100,26101,26105,26110,26111,26112,26116,26120,26121,26125,26129,26130,26133,26134,26141,26142,26145,26146,26147,26148,26150,26153,26154,26155,26156,26158,26160,26161,26163,26169,26167,26176,26181,26182,26186,26188,26193,26190,26199,26200,26201,26203,26204,26208,26209,26363,26218,26219,26220,26238,26227,26229,26239,26231,26232,26233,26235,26240,26236,26251,26252,26253,26256,26258,26265,26266,26267,26268,26271,26272,26276,26285,26289,26290,26293,26299,26303,26304,26306,26307,26312,26316,26318,26319,26324,26331,26335,26344,26347,26348,26350,26362,26373,26375,26382,26387,26393,26396,26400,26402,26419,26430,26437,26439,26440,26444,26452,26453,26461,26470,26476,26478,26484,26486,26491,26497,26500,26510,26511,26513,26515,26518,26520,26521,26523,26544,26545,26546,26549,26555,26556,26557,26617,26560,26562,26563,26565,26568,26569,26578,26583,26585,26588,26593,26598,26608,26610,26614,26615,26706,26644,26649,26653,26655,26664,26663,26668,26669,26671,26672,26673,26675,26683,26687,26692,26693,26698,26700,26709,26711,26712,26715,26731,26734,26735,26736,26737,26738,26741,26745,26746,26747,26748,26754,26756,26758,26760,26774,26776,26778,26780,26785,26787,26789,26793,26794,26798,26802,26811,26821,26824,26828,26831,26832,26833,26835,26838,26841,26844,26845,26853,26856,26858,26859,26860,26861,26864,26865,26869,26870,26875,26876,26877,26886,26889,26890,26896,26897,26899,26902,26903,26929,26931,26933,26936,26939,26946,26949,26953,26958,26967,26971,26979,26980,26981,26982,26984,26985,26988,26992,26993,26994,27002,27003,27007,27008,27021,27026,27030,27032,27041,27045,27046,27048,27051,27053,27055,27063,27064,27066,27068,27077,27080,27089,27094,27095,27106,27109,27118,27119,27121,27123,27125,27134,27136,27137,27139,27151,27153,27157,27162,27165,27168,27172,27176,27184,27186,27188,27191,27195,27198,27199,27205,27206,27209,27210,27214,27216,27217,27218,27221,27222,27227,27236,27239,27242,27249,27251,27262,27265,27267,27270,27271,27273,27275,27281,27291,27293,27294,27295,27301,27307,27311,27312,27313,27316,27325,27326,27327,27334,27337,27336,27340,27344,27348,27349,27350,27356,27357,27364,27367,27372,27376,27377,27378,27388,27389,27394,27395,27398,27399,27401,27407,27408,27409,27415,27419,27422,27428,27432,27435,27436,27439,27445,27446,27451,27455,27462,27466,27469,27474,27478,27480,27485,27488,27495,27499,27502,27504,27509,27517,27518,27522,27525,27543,27547,27551,27552,27554,27555,27560,27561,27564,27565,27566,27568,27576,27577,27581,27582,27587,27588,27593,27596,27606,27610,27617,27619,27622,27623,27630,27633,27639,27641,27647,27650,27652,27653,27657,27661,27662,27664,27666,27673,27679,27686,27687,27688,27692,27694,27699,27701,27702,27706,27707,27711,27722,27723,27725,27727,27730,27732,27737,27739,27740,27755,27757,27759,27764,27766,27768,27769,27771,27781,27782,27783,27785,27796,27797,27799,27800,27804,27807,27824,27826,27828,27842,27846,27853,27855,27856,27857,27858,27860,27862,27866,27868,27872,27879,27881,27883,27884,27886,27890,27892,27908,27911,27914,27918,27919,27921,27923,27930,27942,27943,27944,27751,27950,27951,27953,27961,27964,27967,27991,27998,27999,28001,28005,28007,28015,28016,28028,28034,28039,28049,28050,28052,28054,28055,28056,28074,28076,28084,28087,28089,28093,28095,28100,28104,28106,28110,28111,28118,28123,28125,28127,28128,28130,28133,28137,28143,28144,28148,28150,28156,28160,28164,28190,28194,28199,28210,28214,28217,28219,28220,28228,28229,28232,28233,28235,28239,28241,28242,28243,28244,28247,28252,28253,28254,28258,28259,28264,28275,28283,28285,28301,28307,28313,28320,28327,28333,28334,28337,28339,28347,28351,28352,28353,28355,28359,28360,28362,28365,28366,28367,28395,28397,28398,28409,28411,28413,28420,28424,28426,28428,28429,28438,28440,28442,28443,28454,28457,28458,28463,28464,28467,28470,28475,28476,28461,28495,28497,28498,28499,28503,28505,28506,28509,28510,28513,28514,28520,28524,28541,28542,28547,28551,28552,28555,28556,28557,28560,28562,28563,28564,28566,28570,28575,28576,28581,28582,28583,28584,28590,28591,28592,28597,28598,28604,28613,28615,28616,28618,28634,28638,28648,28649,28656,28661,28665,28668,28669,28672,28677,28678,28679,28685,28695,28704,28707,28719,28724,28727,28729,28732,28739,28740,28744,28745,28746,28747,28756,28757,28765,28766,28750,28772,28773,28780,28782,28789,28790,28798,28801,28805,28806,28820,28821,28822,28823,28824,28827,28836,28843,28848,28849,28852,28855,28874,28881,28883,28884,28885,28886,28888,28892,28900,28922,28931,28932,28933,28934,28935,28939,28940,28943,28958,28960,28971,28973,28975,28976,28977,28984,28993,28997,28998,28999,29002,29003,29008,29010,29015,29018,29020,29022,29024,29032,29049,29056,29061,29063,29068,29074,29082,29083,29088,29090,29103,29104,29106,29107,29114,29119,29120,29121,29124,29131,29132,29139,29142,29145,29146,29148,29176,29182,29184,29191,29192,29193,29203,29207,29210,29213,29215,29220,29227,29231,29236,29240,29241,29249,29250,29251,29253,29262,29263,29264,29267,29269,29270,29274,29276,29278,29280,29283,29288,29291,29294,29295,29297,29303,29304,29307,29308,29311,29316,29321,29325,29326,29331,29339,29352,29357,29358,29361,29364,29374,29377,29383,29385,29388,29397,29398,29400,29407,29413,29427,29428,29434,29435,29438,29442,29444,29445,29447,29451,29453,29458,29459,29464,29465,29470,29474,29476,29479,29480,29484,29489,29490,29493,29498,29499,29501,29507,29517,29520,29522,29526,29528,29533,29534,29535,29536,29542,29543,29545,29547,29548,29550,29551,29553,29559,29561,29564,29568,29569,29571,29573,29574,29582,29584,29587,29589,29591,29592,29596,29598,29599,29600,29602,29605,29606,29610,29611,29613,29621,29623,29625,29628,29629,29631,29637,29638,29641,29643,29644,29647,29650,29651,29654,29657,29661,29665,29667,29670,29671,29673,29684,29685,29687,29689,29690,29691,29693,29695,29696,29697,29700,29703,29706,29713,29722,29723,29732,29734,29736,29737,29738,29739,29740,29741,29742,29743,29744,29745,29753,29760,29763,29764,29766,29767,29771,29773,29777,29778,29783,29789,29794,29798,29799,29800,29803,29805,29806,29809,29810,29824,29825,29829,29830,29831,29833,29839,29840,29841,29842,29848,29849,29850,29852,29855,29856,29857,29859,29862,29864,29865,29866,29867,29870,29871,29873,29874,29877,29881,29883,29887,29896,29897,29900,29904,29907,29912,29914,29915,29918,29919,29924,29928,29930,29931,29935,29940,29946,29947,29948,29951,29958,29970,29974,29975,29984,29985,29988,29991,29993,29994,29999,30006,30009,30013,30014,30015,30016,30019,30023,30024,30030,30032,30034,30039,30046,30047,30049,30063,30065,30073,30074,30075,30076,30077,30078,30081,30085,30096,30098,30099,30101,30105,30108,30114,30116,30132,30138,30143,30144,30145,30148,30150,30156,30158,30159,30167,30172,30175,30176,30177,30180,30183,30188,30190,30191,30193,30201,30208,30210,30211,30212,30215,30216,30218,30220,30223,30226,30227,30229,30230,30233,30235,30236,30237,30238,30243,30245,30246,30249,30253,30258,30259,30261,30264,30265,30266,30268,30282,30272,30273,30275,30276,30277,30281,30283,30293,30297,30303,30308,30309,30317,30318,30319,30321,30324,30337,30341,30348,30349,30357,30363,30364,30365,30367,30368,30370,30371,30372,30373,30374,30375,30376,30378,30381,30397,30401,30405,30409,30411,30412,30414,30420,30425,30432,30438,30440,30444,30448,30449,30454,30457,30460,30464,30470,30474,30478,30482,30484,30485,30487,30489,30490,30492,30498,30504,30509,30510,30511,30516,30517,30518,30521,30525,30526,30530,30533,30534,30538,30541,30542,30543,30546,30550,30551,30556,30558,30559,30560,30562,30564,30567,30570,30572,30576,30578,30579,30580,30586,30589,30592,30596,30604,30605,30612,30613,30614,30618,30623,30626,30631,30634,30638,30639,30641,30645,30654,30659,30665,30673,30674,30677,30681,30686,30687,30688,30692,30694,30698,30700,30704,30705,30708,30712,30715,30725,30726,30729,30733,30734,30737,30749,30753,30754,30755,30765,30766,30768,30773,30775,30787,30788,30791,30792,30796,30798,30802,30812,30814,30816,30817,30819,30820,30824,30826,30830,30842,30846,30858,30863,30868,30872,30881,30877,30878,30879,30884,30888,30892,30893,30896,30897,30898,30899,30907,30909,30911,30919,30920,30921,30924,30926,30930,30931,30933,30934,30948,30939,30943,30944,30945,30950,30954,30962,30963,30976,30966,30967,30970,30971,30975,30982,30988,30992,31002,31004,31006,31007,31008,31013,31015,31017,31021,31025,31028,31029,31035,31037,31039,31044,31045,31046,31050,31051,31055,31057,31060,31064,31067,31068,31079,31081,31083,31090,31097,31099,31100,31102,31115,31116,31121,31123,31124,31125,31126,31128,31131,31132,31137,31144,31145,31147,31151,31153,31156,31160,31163,31170,31172,31175,31176,31178,31183,31188,31190,31194,31197,31198,31200,31202,31205,31210,31211,31213,31217,31224,31228,31234,31235,31239,31241,31242,31244,31249,31253,31259,31262,31265,31271,31275,31277,31279,31280,31284,31285,31288,31289,31290,31300,31301,31303,31304,31308,31317,31318,31321,31324,31325,31327,31328,31333,31335,31338,31341,31349,31352,31358,31360,31362,31365,31366,31370,31371,31376,31377,31380,31390,31392,31395,31404,31411,31413,31417,31419,31420,31430,31433,31436,31438,31441,31451,31464,31465,31467,31468,31473,31476,31483,31485,31486,31495,31508,31519,31523,31527,31529,31530,31531,31533,31534,31535,31536,31537,31540,31549,31551,31552,31553,31559,31566,31573,31584,31588,31590,31593,31594,31597,31599,31602,31603,31607,31620,31625,31630,31632,31633,31638,31643,31646,31648,31653,31660,31663,31664,31666,31669,31670,31674,31675,31676,31677,31682,31685,31688,31690,31700,31702,31703,31705,31706,31707,31720,31722,31730,31732,31733,31736,31737,31738,31740,31742,31745,31746,31747,31748,31750,31753,31755,31756,31758,31759,31769,31771,31776,31781,31782,31784,31788,31793,31795,31796,31798,31801,31802,31814,31818,31829,31825,31826,31827,31833,31834,31835,31836,31837,31838,31841,31843,31847,31849,31853,31854,31856,31858,31865,31868,31869,31878,31879,31887,31892,31902,31904,31910,31920,31926,31927,31930,31931,31932,31935,31940,31943,31944,31945,31949,31951,31955,31956,31957,31959,31961,31962,31965,31974,31977,31979,31989,32003,32007,32008,32009,32015,32017,32018,32019,32022,32029,32030,32035,32038,32042,32045,32049,32060,32061,32062,32064,32065,32071,32072,32077,32081,32083,32087,32089,32090,32092,32093,32101,32103,32106,32112,32120,32122,32123,32127,32129,32130,32131,32133,32134,32136,32139,32140,32141,32145,32150,32151,32157,32158,32166,32167,32170,32179,32182,32183,32185,32194,32195,32196,32197,32198,32204,32205,32206,32215,32217,32256,32226,32229,32230,32234,32235,32237,32241,32245,32246,32249,32250,32264,32272,32273,32277,32279,32284,32285,32288,32295,32296,32300,32301,32303,32307,32310,32319,32324,32325,32327,32334,32336,32338,32344,32351,32353,32354,32357,32363,32366,32367,32371,32376,32382,32385,32390,32391,32394,32397,32401,32405,32408,32410,32413,32414,32572,32571,32573,32574,32575,32579,32580,32583,32591,32594,32595,32603,32604,32605,32609,32611,32612,32613,32614,32621,32625,32637,32638,32639,32640,32651,32653,32655,32656,32657,32662,32663,32668,32673,32674,32678,32682,32685,32692,32700,32703,32704,32707,32712,32718,32719,32731,32735,32739,32741,32744,32748,32750,32751,32754,32762,32765,32766,32767,32775,32776,32778,32781,32782,32783,32785,32787,32788,32790,32797,32798,32799,32800,32804,32806,32812,32814,32816,32820,32821,32823,32825,32826,32828,32830,32832,32836,32864,32868,32870,32877,32881,32885,32897,32904,32910,32924,32926,32934,32935,32939,32952,32953,32968,32973,32975,32978,32980,32981,32983,32984,32992,33005,33006,33008,33010,33011,33014,33017,33018,33022,33027,33035,33046,33047,33048,33052,33054,33056,33060,33063,33068,33072,33077,33082,33084,33093,33095,33098,33100,33106,33111,33120,33121,33127,33128,33129,33133,33135,33143,33153,33168,33156,33157,33158,33163,33166,33174,33176,33179,33182,33186,33198,33202,33204,33211,33227,33219,33221,33226,33230,33231,33237,33239,33243,33245,33246,33249,33252,33259,33260,33264,33265,33266,33269,33270,33272,33273,33277,33279,33280,33283,33295,33299,33300,33305,33306,33309,33313,33314,33320,33330,33332,33338,33347,33348,33349,33350,33355,33358,33359,33361,33366,33372,33376,33379,33383,33389,33396,33403,33405,33407,33408,33409,33411,33412,33415,33417,33418,33422,33425,33428,33430,33432,33434,33435,33440,33441,33443,33444,33447,33448,33449,33450,33454,33456,33458,33460,33463,33466,33468,33470,33471,33478,33488,33493,33498,33504,33506,33508,33512,33514,33517,33519,33526,33527,33533,33534,33536,33537,33543,33544,33546,33547,33620,33563,33565,33566,33567,33569,33570,33580,33581,33582,33584,33587,33591,33594,33596,33597,33602,33603,33604,33607,33613,33614,33617,33621,33622,33623,33648,33656,33661,33663,33664,33666,33668,33670,33677,33682,33684,33685,33688,33689,33691,33692,33693,33702,33703,33705,33708,33726,33727,33728,33735,33737,33743,33744,33745,33748,33757,33619,33768,33770,33782,33784,33785,33788,33793,33798,33802,33807,33809,33813,33817,33709,33839,33849,33861,33863,33864,33866,33869,33871,33873,33874,33878,33880,33881,33882,33884,33888,33892,33893,33895,33898,33904,33907,33908,33910,33912,33916,33917,33921,33925,33938,33939,33941,33950,33958,33960,33961,33962,33967,33969,33972,33978,33981,33982,33984,33986,33991,33992,33996,33999,34003,34012,34023,34026,34031,34032,34033,34034,34039,34098,34042,34043,34045,34050,34051,34055,34060,34062,34064,34076,34078,34082,34083,34084,34085,34087,34090,34091,34095,34099,34100,34102,34111,34118,34127,34128,34129,34130,34131,34134,34137,34140,34141,34142,34143,34144,34145,34146,34148,34155,34159,34169,34170,34171,34173,34175,34177,34181,34182,34185,34187,34188,34191,34195,34200,34205,34207,34208,34210,34213,34215,34228,34230,34231,34232,34236,34237,34238,34239,34242,34247,34250,34251,34254,34221,34264,34266,34271,34272,34278,34280,34285,34291,34294,34300,34303,34304,34308,34309,34317,34318,34320,34321,34322,34328,34329,34331,34334,34337,34343,34345,34358,34360,34362,34364,34365,34368,34370,34374,34386,34387,34390,34391,34392,34393,34397,34400,34401,34402,34403,34404,34409,34412,34415,34421,34422,34423,34426,34445,34449,34454,34456,34458,34460,34465,34470,34471,34472,34477,34481,34483,34484,34485,34487,34488,34489,34495,34496,34497,34499,34501,34513,34514,34517,34519,34522,34524,34528,34531,34533,34535,34440,34554,34556,34557,34564,34565,34567,34571,34574,34575,34576,34579,34580,34585,34590,34591,34593,34595,34600,34606,34607,34609,34610,34617,34618,34620,34621,34622,34624,34627,34629,34637,34648,34653,34657,34660,34661,34671,34673,34674,34683,34691,34692,34693,34694,34695,34696,34697,34699,34700,34704,34707,34709,34711,34712,34713,34718,34720,34723,34727,34732,34733,34734,34737,34741,34750,34751,34753,34760,34761,34762,34766,34773,34774,34777,34778,34780,34783,34786,34787,34788,34794,34795,34797,34801,34803,34808,34810,34815,34817,34819,34822,34825,34826,34827,34832,34841,34834,34835,34836,34840,34842,34843,34844,34846,34847,34856,34861,34862,34864,34866,34869,34874,34876,34881,34883,34885,34888,34889,34890,34891,34894,34897,34901,34902,34904,34906,34908,34911,34912,34916,34921,34929,34937,34939,34944,34968,34970,34971,34972,34975,34976,34984,34986,35002,35005,35006,35008,35018,35019,35020,35021,35022,35025,35026,35027,35035,35038,35047,35055,35056,35057,35061,35063,35073,35078,35085,35086,35087,35093,35094,35096,35097,35098,35100,35104,35110,35111,35112,35120,35121,35122,35125,35129,35130,35134,35136,35138,35141,35142,35145,35151,35154,35159,35162,35163,35164,35169,35170,35171,35179,35182,35184,35187,35189,35194,35195,35196,35197,35209,35213,35216,35220,35221,35227,35228,35231,35232,35237,35248,35252,35253,35254,35255,35260,35284,35285,35286,35287,35288,35301,35305,35307,35309,35313,35315,35318,35321,35325,35327,35332,35333,35335,35343,35345,35346,35348,35349,35358,35360,35362,35364,35366,35371,35372,35375,35381,35383,35389,35390,35392,35395,35397,35399,35401,35405,35406,35411,35414,35415,35416,35420,35421,35425,35429,35431,35445,35446,35447,35449,35450,35451,35454,35455,35456,35459,35462,35467,35471,35472,35474,35478,35479,35481,35487,35495,35497,35502,35503,35507,35510,35511,35515,35518,35523,35526,35528,35529,35530,35537,35539,35540,35541,35543,35549,35551,35564,35568,35572,35573,35574,35580,35583,35589,35590,35595,35601,35612,35614,35615,35594,35629,35632,35639,35644,35650,35651,35652,35653,35654,35656,35666,35667,35668,35673,35661,35678,35683,35693,35702,35704,35705,35708,35710,35713,35716,35717,35723,35725,35727,35732,35733,35740,35742,35743,35896,35897,35901,35902,35909,35911,35913,35915,35919,35921,35923,35924,35927,35928,35931,35933,35929,35939,35940,35942,35944,35945,35949,35955,35957,35958,35963,35966,35974,35975,35979,35984,35986,35987,35993,35995,35996,36004,36025,36026,36037,36038,36041,36043,36047,36054,36053,36057,36061,36065,36072,36076,36079,36080,36082,36085,36087,36088,36094,36095,36097,36099,36105,36114,36119,36123,36197,36201,36204,36206,36223,36226,36228,36232,36237,36240,36241,36245,36254,36255,36256,36262,36267,36268,36271,36274,36277,36279,36281,36283,36288,36293,36294,36295,36296,36298,36302,36305,36308,36309,36311,36313,36324,36325,36327,36332,36336,36284,36337,36338,36340,36349,36353,36356,36357,36358,36363,36369,36372,36374,36384,36385,36386,36387,36390,36391,36401,36403,36406,36407,36408,36409,36413,36416,36417,36427,36429,36430,36431,36436,36443,36444,36445,36446,36449,36450,36457,36460,36461,36463,36464,36465,36473,36474,36475,36482,36483,36489,36496,36498,36501,36506,36507,36509,36510,36514,36519,36521,36525,36526,36531,36533,36538,36539,36544,36545,36547,36548,36551,36559,36561,36564,36572,36584,36590,36592,36593,36599,36601,36602,36589,36608,36610,36615,36616,36623,36624,36630,36631,36632,36638,36640,36641,36643,36645,36647,36648,36652,36653,36654,36660,36661,36662,36663,36666,36672,36673,36675,36679,36687,36689,36690,36691,36692,36693,36696,36701,36702,36709,36765,36768,36769,36772,36773,36774,36789,36790,36792,36798,36800,36801,36806,36810,36811,36813,36816,36818,36819,36821,36832,36835,36836,36840,36846,36849,36853,36854,36859,36862,36866,36868,36872,36876,36888,36891,36904,36905,36911,36906,36908,36909,36915,36916,36919,36927,36931,36932,36940,36955,36957,36962,36966,36967,36972,36976,36980,36985,36997,37000,37003,37004,37006,37008,37013,37015,37016,37017,37019,37024,37025,37026,37029,37040,37042,37043,37044,37046,37053,37068,37054,37059,37060,37061,37063,37064,37077,37079,37080,37081,37084,37085,37087,37093,37074,37110,37099,37103,37104,37108,37118,37119,37120,37124,37125,37126,37128,37133,37136,37140,37142,37143,37144,37146,37148,37150,37152,37157,37154,37155,37159,37161,37166,37167,37169,37172,37174,37175,37177,37178,37180,37181,37187,37191,37192,37199,37203,37207,37209,37210,37211,37217,37220,37223,37229,37236,37241,37242,37243,37249,37251,37253,37254,37258,37262,37265,37267,37268,37269,37272,37278,37281,37286,37288,37292,37293,37294,37296,37297,37298,37299,37302,37307,37308,37309,37311,37314,37315,37317,37331,37332,37335,37337,37338,37342,37348,37349,37353,37354,37356,37357,37358,37359,37360,37361,37367,37369,37371,37373,37376,37377,37380,37381,37382,37383,37385,37386,37388,37392,37394,37395,37398,37400,37404,37405,37411,37412,37413,37414,37416,37422,37423,37424,37427,37429,37430,37432,37433,37434,37436,37438,37440,37442,37443,37446,37447,37450,37453,37454,37455,37457,37464,37465,37468,37469,37472,37473,37477,37479,37480,37481,37486,37487,37488,37493,37494,37495,37496,37497,37499,37500,37501,37503,37512,37513,37514,37517,37518,37522,37527,37529,37535,37536,37540,37541,37543,37544,37547,37551,37554,37558,37560,37562,37563,37564,37565,37567,37568,37569,37570,37571,37573,37574,37575,37576,37579,37580,37581,37582,37584,37587,37589,37591,37592,37593,37596,37597,37599,37600,37601,37603,37605,37607,37608,37612,37614,37616,37625,37627,37631,37632,37634,37640,37645,37649,37652,37653,37660,37661,37662,37663,37665,37668,37669,37671,37673,37674,37683,37684,37686,37687,37703,37704,37705,37712,37713,37714,37717,37719,37720,37722,37726,37732,37733,37735,37737,37738,37741,37743,37744,37745,37747,37748,37750,37754,37757,37759,37760,37761,37762,37768,37770,37771,37773,37775,37778,37781,37784,37787,37790,37793,37795,37796,37798,37800,37803,37812,37813,37814,37818,37801,37825,37828,37829,37830,37831,37833,37834,37835,37836,37837,37843,37849,37852,37854,37855,37858,37862,37863,37881,37879,37880,37882,37883,37885,37889,37890,37892,37896,37897,37901,37902,37903,37909,37910,37911,37919,37934,37935,37937,37938,37939,37940,37947,37951,37949,37955,37957,37960,37962,37964,37973,37977,37980,37983,37985,37987,37992,37995,37997,37998,37999,38001,38002,38020,38019,38264,38265,38270,38276,38280,38284,38285,38286,38301,38302,38303,38305,38310,38313,38315,38316,38324,38326,38330,38333,38335,38342,38344,38345,38347,38352,38353,38354,38355,38361,38362,38365,38366,38367,38368,38372,38374,38429,38430,38434,38436,38437,38438,38444,38449,38451,38455,38456,38457,38458,38460,38461,38465,38482,38484,38486,38487,38488,38497,38510,38516,38523,38524,38526,38527,38529,38530,38531,38532,38537,38545,38550,38554,38557,38559,38564,38565,38566,38569,38574,38575,38579,38586,38602,38610,23986,38616,38618,38621,38622,38623,38633,38639,38641,38650,38658,38659,38661,38665,38682,38683,38685,38689,38690,38691,38696,38705,38707,38721,38723,38730,38734,38735,38741,38743,38744,38746,38747,38755,38759,38762,38766,38771,38774,38775,38776,38779,38781,38783,38784,38793,38805,38806,38807,38809,38810,38814,38815,38818,38828,38830,38833,38834,38837,38838,38840,38841,38842,38844,38846,38847,38849,38852,38853,38855,38857,38858,38860,38861,38862,38864,38865,38868,38871,38872,38873,38877,38878,38880,38875,38881,38884,38895,38897,38900,38903,38904,38906,38919,38922,38937,38925,38926,38932,38934,38940,38942,38944,38947,38950,38955,38958,38959,38960,38962,38963,38965,38949,38974,38980,38983,38986,38993,38994,38995,38998,38999,39001,39002,39010,39011,39013,39014,39018,39020,39083,39085,39086,39088,39092,39095,39096,39098,39099,39103,39106,39109,39112,39116,39137,39139,39141,39142,39143,39146,39155,39158,39170,39175,39176,39185,39189,39190,39191,39194,39195,39196,39199,39202,39206,39207,39211,39217,39218,39219,39220,39221,39225,39226,39227,39228,39232,39233,39238,39239,39240,39245,39246,39252,39256,39257,39259,39260,39262,39263,39264,39323,39325,39327,39334,39344,39345,39346,39349,39353,39354,39357,39359,39363,39369,39379,39380,39385,39386,39388,39390,39399,39402,39403,39404,39408,39412,39413,39417,39421,39422,39426,39427,39428,39435,39436,39440,39441,39446,39454,39456,39458,39459,39460,39463,39469,39470,39475,39477,39478,39480,39495,39489,39492,39498,39499,39500,39502,39505,39508,39510,39517,39594,39596,39598,39599,39602,39604,39605,39606,39609,39611,39614,39615,39617,39619,39622,39624,39630,39632,39634,39637,39638,39639,39643,39644,39648,39652,39653,39655,39657,39660,39666,39667,39669,39673,39674,39677,39679,39680,39681,39682,39683,39684,39685,39688,39689,39691,39692,39693,39694,39696,39698,39702,39705,39707,39708,39712,39718,39723,39725,39731,39732,39733,39735,39737,39738,39741,39752,39755,39756,39765,39766,39767,39771,39774,39777,39779,39781,39782,39784,39786,39787,39788,39789,39790,39795,39797,39799,39800,39801,39807,39808,39812,39813,39814,39815,39817,39818,39819,39821,39823,39824,39828,39834,39837,39838,39846,39847,39849,39852,39856,39857,39858,39863,39864,39867,39868,39870,39871,39873,39879,39880,39886,39888,39895,39896,39901,39903,39909,39911,39914,39915,39919,39923,39927,39928,39929,39930,39933,39935,39936,39938,39947,39951,39953,39958,39960,39961,39962,39964,39966,39970,39971,39974,39975,39976,39977,39978,39985,39989,39990,39991,39997,40001,40003,40004,40005,40009,40010,40014,40015,40016,40019,40020,40022,40024,40027,40029,40030,40031,40035,40041,40042,40028,40043,40040,40046,40048,40050,40053,40055,40059,40166,40178,40183,40185,40203,40194,40209,40215,40216,40220,40221,40222,40239,40240,40242,40243,40244,40250,40252,40261,40253,40258,40259,40263,40266,40275,40276,40287,40291,40290,40293,40297,40298,40299,40304,40310,40311,40315,40316,40318,40323,40324,40326,40330,40333,40334,40338,40339,40341,40342,40343,40344,40353,40362,40364,40366,40369,40373,40377,40380,40383,40387,40391,40393,40394,40404,40405,40406,40407,40410,40414,40415,40416,40421,40423,40425,40427,40430,40432,40435,40436,40446,40458,40450,40455,40462,40464,40465,40466,40469,40470,40473,40476,40477,40570,40571,40572,40576,40578,40579,40580,40581,40583,40590,40591,40598,40600,40603,40606,40612,40616,40620,40622,40623,40624,40627,40628,40629,40646,40648,40651,40661,40671,40676,40679,40684,40685,40686,40688,40689,40690,40693,40696,40703,40706,40707,40713,40719,40720,40721,40722,40724,40726,40727,40729,40730,40731,40735,40738,40742,40746,40747,40751,40753,40754,40756,40759,40761,40762,40764,40765,40767,40769,40771,40772,40773,40774,40775,40787,40789,40790,40791,40792,40794,40797,40798,40808,40809,40813,40814,40815,40816,40817,40819,40821,40826,40829,40847,40848,40849,40850,40852,40854,40855,40862,40865,40866,40867,40869,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],
+ "ibm866":[1040,1041,1042,1043,1044,1045,1046,1047,1048,1049,1050,1051,1052,1053,1054,1055,1056,1057,1058,1059,1060,1061,1062,1063,1064,1065,1066,1067,1068,1069,1070,1071,1072,1073,1074,1075,1076,1077,1078,1079,1080,1081,1082,1083,1084,1085,1086,1087,9617,9618,9619,9474,9508,9569,9570,9558,9557,9571,9553,9559,9565,9564,9563,9488,9492,9524,9516,9500,9472,9532,9566,9567,9562,9556,9577,9574,9568,9552,9580,9575,9576,9572,9573,9561,9560,9554,9555,9579,9578,9496,9484,9608,9604,9612,9616,9600,1088,1089,1090,1091,1092,1093,1094,1095,1096,1097,1098,1099,1100,1101,1102,1103,1025,1105,1028,1108,1031,1111,1038,1118,176,8729,183,8730,8470,164,9632,160],
+ "iso-8859-2":[128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,260,728,321,164,317,346,167,168,352,350,356,377,173,381,379,176,261,731,322,180,318,347,711,184,353,351,357,378,733,382,380,340,193,194,258,196,313,262,199,268,201,280,203,282,205,206,270,272,323,327,211,212,336,214,215,344,366,218,368,220,221,354,223,341,225,226,259,228,314,263,231,269,233,281,235,283,237,238,271,273,324,328,243,244,337,246,247,345,367,250,369,252,253,355,729],
+ "iso-8859-3":[128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,294,728,163,164,null,292,167,168,304,350,286,308,173,null,379,176,295,178,179,180,181,293,183,184,305,351,287,309,189,null,380,192,193,194,null,196,266,264,199,200,201,202,203,204,205,206,207,null,209,210,211,212,288,214,215,284,217,218,219,220,364,348,223,224,225,226,null,228,267,265,231,232,233,234,235,236,237,238,239,null,241,242,243,244,289,246,247,285,249,250,251,252,365,349,729],
+ "iso-8859-4":[128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,260,312,342,164,296,315,167,168,352,274,290,358,173,381,175,176,261,731,343,180,297,316,711,184,353,275,291,359,330,382,331,256,193,194,195,196,197,198,302,268,201,280,203,278,205,206,298,272,325,332,310,212,213,214,215,216,370,218,219,220,360,362,223,257,225,226,227,228,229,230,303,269,233,281,235,279,237,238,299,273,326,333,311,244,245,246,247,248,371,250,251,252,361,363,729],
+ "iso-8859-5":[128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,1025,1026,1027,1028,1029,1030,1031,1032,1033,1034,1035,1036,173,1038,1039,1040,1041,1042,1043,1044,1045,1046,1047,1048,1049,1050,1051,1052,1053,1054,1055,1056,1057,1058,1059,1060,1061,1062,1063,1064,1065,1066,1067,1068,1069,1070,1071,1072,1073,1074,1075,1076,1077,1078,1079,1080,1081,1082,1083,1084,1085,1086,1087,1088,1089,1090,1091,1092,1093,1094,1095,1096,1097,1098,1099,1100,1101,1102,1103,8470,1105,1106,1107,1108,1109,1110,1111,1112,1113,1114,1115,1116,167,1118,1119],
+ "iso-8859-6":[128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,null,null,null,164,null,null,null,null,null,null,null,1548,173,null,null,null,null,null,null,null,null,null,null,null,null,null,1563,null,null,null,1567,null,1569,1570,1571,1572,1573,1574,1575,1576,1577,1578,1579,1580,1581,1582,1583,1584,1585,1586,1587,1588,1589,1590,1591,1592,1593,1594,null,null,null,null,null,1600,1601,1602,1603,1604,1605,1606,1607,1608,1609,1610,1611,1612,1613,1614,1615,1616,1617,1618,null,null,null,null,null,null,null,null,null,null,null,null,null],
+ "iso-8859-7":[128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,8216,8217,163,8364,8367,166,167,168,169,890,171,172,173,null,8213,176,177,178,179,900,901,902,183,904,905,906,187,908,189,910,911,912,913,914,915,916,917,918,919,920,921,922,923,924,925,926,927,928,929,null,931,932,933,934,935,936,937,938,939,940,941,942,943,944,945,946,947,948,949,950,951,952,953,954,955,956,957,958,959,960,961,962,963,964,965,966,967,968,969,970,971,972,973,974,null],
+ "iso-8859-8":[128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,null,162,163,164,165,166,167,168,169,215,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,247,187,188,189,190,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,8215,1488,1489,1490,1491,1492,1493,1494,1495,1496,1497,1498,1499,1500,1501,1502,1503,1504,1505,1506,1507,1508,1509,1510,1511,1512,1513,1514,null,null,8206,8207,null],
+ "iso-8859-10":[128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,260,274,290,298,296,310,167,315,272,352,358,381,173,362,330,176,261,275,291,299,297,311,183,316,273,353,359,382,8213,363,331,256,193,194,195,196,197,198,302,268,201,280,203,278,205,206,207,208,325,332,211,212,213,214,360,216,370,218,219,220,221,222,223,257,225,226,227,228,229,230,303,269,233,281,235,279,237,238,239,240,326,333,243,244,245,246,361,248,371,250,251,252,253,254,312],
+ "iso-8859-13":[128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,8221,162,163,164,8222,166,167,216,169,342,171,172,173,174,198,176,177,178,179,8220,181,182,183,248,185,343,187,188,189,190,230,260,302,256,262,196,197,280,274,268,201,377,278,290,310,298,315,352,323,325,211,332,213,214,215,370,321,346,362,220,379,381,223,261,303,257,263,228,229,281,275,269,233,378,279,291,311,299,316,353,324,326,243,333,245,246,247,371,322,347,363,252,380,382,8217],
+ "iso-8859-14":[128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,7682,7683,163,266,267,7690,167,7808,169,7810,7691,7922,173,174,376,7710,7711,288,289,7744,7745,182,7766,7809,7767,7811,7776,7923,7812,7813,7777,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,372,209,210,211,212,213,214,7786,216,217,218,219,220,221,374,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,373,241,242,243,244,245,246,7787,248,249,250,251,252,253,375,255],
+ "iso-8859-15":[128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,8364,165,352,167,353,169,170,171,172,173,174,175,176,177,178,179,381,181,182,183,382,185,186,187,338,339,376,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255],
+ "iso-8859-16":[128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,260,261,321,8364,8222,352,167,353,169,536,171,377,173,378,379,176,177,268,322,381,8221,182,183,382,269,537,187,338,339,376,380,192,193,194,258,196,262,198,199,200,201,202,203,204,205,206,207,272,323,210,211,212,336,214,346,368,217,218,219,220,280,538,223,224,225,226,259,228,263,230,231,232,233,234,235,236,237,238,239,273,324,242,243,244,337,246,347,369,249,250,251,252,281,539,255],
+ "koi8-r":[9472,9474,9484,9488,9492,9496,9500,9508,9516,9524,9532,9600,9604,9608,9612,9616,9617,9618,9619,8992,9632,8729,8730,8776,8804,8805,160,8993,176,178,183,247,9552,9553,9554,1105,9555,9556,9557,9558,9559,9560,9561,9562,9563,9564,9565,9566,9567,9568,9569,1025,9570,9571,9572,9573,9574,9575,9576,9577,9578,9579,9580,169,1102,1072,1073,1094,1076,1077,1092,1075,1093,1080,1081,1082,1083,1084,1085,1086,1087,1103,1088,1089,1090,1091,1078,1074,1100,1099,1079,1096,1101,1097,1095,1098,1070,1040,1041,1062,1044,1045,1060,1043,1061,1048,1049,1050,1051,1052,1053,1054,1055,1071,1056,1057,1058,1059,1046,1042,1068,1067,1047,1064,1069,1065,1063,1066],
+ "koi8-u":[9472,9474,9484,9488,9492,9496,9500,9508,9516,9524,9532,9600,9604,9608,9612,9616,9617,9618,9619,8992,9632,8729,8730,8776,8804,8805,160,8993,176,178,183,247,9552,9553,9554,1105,1108,9556,1110,1111,9559,9560,9561,9562,9563,1169,1118,9566,9567,9568,9569,1025,1028,9571,1030,1031,9574,9575,9576,9577,9578,1168,1038,169,1102,1072,1073,1094,1076,1077,1092,1075,1093,1080,1081,1082,1083,1084,1085,1086,1087,1103,1088,1089,1090,1091,1078,1074,1100,1099,1079,1096,1101,1097,1095,1098,1070,1040,1041,1062,1044,1045,1060,1043,1061,1048,1049,1050,1051,1052,1053,1054,1055,1071,1056,1057,1058,1059,1046,1042,1068,1067,1047,1064,1069,1065,1063,1066],
+ "macintosh":[196,197,199,201,209,214,220,225,224,226,228,227,229,231,233,232,234,235,237,236,238,239,241,243,242,244,246,245,250,249,251,252,8224,176,162,163,167,8226,182,223,174,169,8482,180,168,8800,198,216,8734,177,8804,8805,165,181,8706,8721,8719,960,8747,170,186,937,230,248,191,161,172,8730,402,8776,8710,171,187,8230,160,192,195,213,338,339,8211,8212,8220,8221,8216,8217,247,9674,255,376,8260,8364,8249,8250,64257,64258,8225,183,8218,8222,8240,194,202,193,203,200,205,206,207,204,211,212,63743,210,218,219,217,305,710,732,175,728,729,730,184,733,731,711],
+ "windows-874":[8364,129,130,131,132,8230,134,135,136,137,138,139,140,141,142,143,144,8216,8217,8220,8221,8226,8211,8212,152,153,154,155,156,157,158,159,160,3585,3586,3587,3588,3589,3590,3591,3592,3593,3594,3595,3596,3597,3598,3599,3600,3601,3602,3603,3604,3605,3606,3607,3608,3609,3610,3611,3612,3613,3614,3615,3616,3617,3618,3619,3620,3621,3622,3623,3624,3625,3626,3627,3628,3629,3630,3631,3632,3633,3634,3635,3636,3637,3638,3639,3640,3641,3642,null,null,null,null,3647,3648,3649,3650,3651,3652,3653,3654,3655,3656,3657,3658,3659,3660,3661,3662,3663,3664,3665,3666,3667,3668,3669,3670,3671,3672,3673,3674,3675,null,null,null,null],
+ "windows-1250":[8364,129,8218,131,8222,8230,8224,8225,136,8240,352,8249,346,356,381,377,144,8216,8217,8220,8221,8226,8211,8212,152,8482,353,8250,347,357,382,378,160,711,728,321,164,260,166,167,168,169,350,171,172,173,174,379,176,177,731,322,180,181,182,183,184,261,351,187,317,733,318,380,340,193,194,258,196,313,262,199,268,201,280,203,282,205,206,270,272,323,327,211,212,336,214,215,344,366,218,368,220,221,354,223,341,225,226,259,228,314,263,231,269,233,281,235,283,237,238,271,273,324,328,243,244,337,246,247,345,367,250,369,252,253,355,729],
+ "windows-1251":[1026,1027,8218,1107,8222,8230,8224,8225,8364,8240,1033,8249,1034,1036,1035,1039,1106,8216,8217,8220,8221,8226,8211,8212,152,8482,1113,8250,1114,1116,1115,1119,160,1038,1118,1032,164,1168,166,167,1025,169,1028,171,172,173,174,1031,176,177,1030,1110,1169,181,182,183,1105,8470,1108,187,1112,1029,1109,1111,1040,1041,1042,1043,1044,1045,1046,1047,1048,1049,1050,1051,1052,1053,1054,1055,1056,1057,1058,1059,1060,1061,1062,1063,1064,1065,1066,1067,1068,1069,1070,1071,1072,1073,1074,1075,1076,1077,1078,1079,1080,1081,1082,1083,1084,1085,1086,1087,1088,1089,1090,1091,1092,1093,1094,1095,1096,1097,1098,1099,1100,1101,1102,1103],
+ "windows-1252":[8364,129,8218,402,8222,8230,8224,8225,710,8240,352,8249,338,141,381,143,144,8216,8217,8220,8221,8226,8211,8212,732,8482,353,8250,339,157,382,376,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255],
+ "windows-1253":[8364,129,8218,402,8222,8230,8224,8225,136,8240,138,8249,140,141,142,143,144,8216,8217,8220,8221,8226,8211,8212,152,8482,154,8250,156,157,158,159,160,901,902,163,164,165,166,167,168,169,null,171,172,173,174,8213,176,177,178,179,900,181,182,183,904,905,906,187,908,189,910,911,912,913,914,915,916,917,918,919,920,921,922,923,924,925,926,927,928,929,null,931,932,933,934,935,936,937,938,939,940,941,942,943,944,945,946,947,948,949,950,951,952,953,954,955,956,957,958,959,960,961,962,963,964,965,966,967,968,969,970,971,972,973,974,null],
+ "windows-1254":[8364,129,8218,402,8222,8230,8224,8225,710,8240,352,8249,338,141,142,143,144,8216,8217,8220,8221,8226,8211,8212,732,8482,353,8250,339,157,158,376,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,286,209,210,211,212,213,214,215,216,217,218,219,220,304,350,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,287,241,242,243,244,245,246,247,248,249,250,251,252,305,351,255],
+ "windows-1255":[8364,129,8218,402,8222,8230,8224,8225,710,8240,138,8249,140,141,142,143,144,8216,8217,8220,8221,8226,8211,8212,732,8482,154,8250,156,157,158,159,160,161,162,163,8362,165,166,167,168,169,215,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,247,187,188,189,190,191,1456,1457,1458,1459,1460,1461,1462,1463,1464,1465,1466,1467,1468,1469,1470,1471,1472,1473,1474,1475,1520,1521,1522,1523,1524,null,null,null,null,null,null,null,1488,1489,1490,1491,1492,1493,1494,1495,1496,1497,1498,1499,1500,1501,1502,1503,1504,1505,1506,1507,1508,1509,1510,1511,1512,1513,1514,null,null,8206,8207,null],
+ "windows-1256":[8364,1662,8218,402,8222,8230,8224,8225,710,8240,1657,8249,338,1670,1688,1672,1711,8216,8217,8220,8221,8226,8211,8212,1705,8482,1681,8250,339,8204,8205,1722,160,1548,162,163,164,165,166,167,168,169,1726,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,1563,187,188,189,190,1567,1729,1569,1570,1571,1572,1573,1574,1575,1576,1577,1578,1579,1580,1581,1582,1583,1584,1585,1586,1587,1588,1589,1590,215,1591,1592,1593,1594,1600,1601,1602,1603,224,1604,226,1605,1606,1607,1608,231,232,233,234,235,1609,1610,238,239,1611,1612,1613,1614,244,1615,1616,247,1617,249,1618,251,252,8206,8207,1746],
+ "windows-1257":[8364,129,8218,131,8222,8230,8224,8225,136,8240,138,8249,140,168,711,184,144,8216,8217,8220,8221,8226,8211,8212,152,8482,154,8250,156,175,731,159,160,null,162,163,164,null,166,167,216,169,342,171,172,173,174,198,176,177,178,179,180,181,182,183,248,185,343,187,188,189,190,230,260,302,256,262,196,197,280,274,268,201,377,278,290,310,298,315,352,323,325,211,332,213,214,215,370,321,346,362,220,379,381,223,261,303,257,263,228,229,281,275,269,233,378,279,291,311,299,316,353,324,326,243,333,245,246,247,371,322,347,363,252,380,382,729],
+ "windows-1258":[8364,129,8218,402,8222,8230,8224,8225,710,8240,138,8249,338,141,142,143,144,8216,8217,8220,8221,8226,8211,8212,732,8482,154,8250,339,157,158,376,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,258,196,197,198,199,200,201,202,203,768,205,206,207,272,209,777,211,212,416,214,215,216,217,218,219,220,431,771,223,224,225,226,259,228,229,230,231,232,233,234,235,769,237,238,239,273,241,803,243,244,417,246,247,248,249,250,251,252,432,8363,255],
+ "x-mac-cyrillic":[1040,1041,1042,1043,1044,1045,1046,1047,1048,1049,1050,1051,1052,1053,1054,1055,1056,1057,1058,1059,1060,1061,1062,1063,1064,1065,1066,1067,1068,1069,1070,1071,8224,176,1168,163,167,8226,182,1030,174,169,8482,1026,1106,8800,1027,1107,8734,177,8804,8805,1110,181,1169,1032,1028,1108,1031,1111,1033,1113,1034,1114,1112,1029,172,8730,402,8776,8710,171,187,8230,160,1035,1115,1036,1116,1109,8211,8212,8220,8221,8216,8217,247,8222,1038,1118,1039,1119,8470,1025,1105,1103,1072,1073,1074,1075,1076,1077,1078,1079,1080,1081,1082,1083,1084,1085,1086,1087,1088,1089,1090,1091,1092,1093,1094,1095,1096,1097,1098,1099,1100,1101,1102,8364]
+}
+;}(this));
diff --git a/vendor/assets/javascripts/xterm/encoding.js b/vendor/assets/javascripts/xterm/encoding.js
new file mode 100644
index 00000000000..b5c8904f5a3
--- /dev/null
+++ b/vendor/assets/javascripts/xterm/encoding.js
@@ -0,0 +1,3309 @@
+// This is free and unencumbered software released into the public domain.
+// See LICENSE.md for more information.
+
+// If we're in node require encoding-indexes and attach it to the global.
+/**
+ * @fileoverview Global |this| required for resolving indexes in node.
+ * @suppress {globalThis}
+ */
+if (typeof module !== "undefined" && module.exports &&
+ !this["encoding-indexes"]) {
+ require("./encoding-indexes.js");
+}
+
+(function(global) {
+ 'use strict';
+
+ //
+ // Utilities
+ //
+
+ /**
+ * @param {number} a The number to test.
+ * @param {number} min The minimum value in the range, inclusive.
+ * @param {number} max The maximum value in the range, inclusive.
+ * @return {boolean} True if a >= min and a <= max.
+ */
+ function inRange(a, min, max) {
+ return min <= a && a <= max;
+ }
+
+ /**
+ * @param {!Array.<*>} array The array to check.
+ * @param {*} item The item to look for in the array.
+ * @return {boolean} True if the item appears in the array.
+ */
+ function includes(array, item) {
+ return array.indexOf(item) !== -1;
+ }
+
+ var floor = Math.floor;
+
+ /**
+ * @param {*} o
+ * @return {Object}
+ */
+ function ToDictionary(o) {
+ if (o === undefined) return {};
+ if (o === Object(o)) return o;
+ throw TypeError('Could not convert argument to dictionary');
+ }
+
+ /**
+ * @param {string} string Input string of UTF-16 code units.
+ * @return {!Array.<number>} Code points.
+ */
+ function stringToCodePoints(string) {
+ // https://heycam.github.io/webidl/#dfn-obtain-unicode
+
+ // 1. Let S be the DOMString value.
+ var s = String(string);
+
+ // 2. Let n be the length of S.
+ var n = s.length;
+
+ // 3. Initialize i to 0.
+ var i = 0;
+
+ // 4. Initialize U to be an empty sequence of Unicode characters.
+ var u = [];
+
+ // 5. While i < n:
+ while (i < n) {
+
+ // 1. Let c be the code unit in S at index i.
+ var c = s.charCodeAt(i);
+
+ // 2. Depending on the value of c:
+
+ // c < 0xD800 or c > 0xDFFF
+ if (c < 0xD800 || c > 0xDFFF) {
+ // Append to U the Unicode character with code point c.
+ u.push(c);
+ }
+
+ // 0xDC00 ≤ c ≤ 0xDFFF
+ else if (0xDC00 <= c && c <= 0xDFFF) {
+ // Append to U a U+FFFD REPLACEMENT CHARACTER.
+ u.push(0xFFFD);
+ }
+
+ // 0xD800 ≤ c ≤ 0xDBFF
+ else if (0xD800 <= c && c <= 0xDBFF) {
+ // 1. If i = n−1, then append to U a U+FFFD REPLACEMENT
+ // CHARACTER.
+ if (i === n - 1) {
+ u.push(0xFFFD);
+ }
+ // 2. Otherwise, i < n−1:
+ else {
+ // 1. Let d be the code unit in S at index i+1.
+ var d = s.charCodeAt(i + 1);
+
+ // 2. If 0xDC00 ≤ d ≤ 0xDFFF, then:
+ if (0xDC00 <= d && d <= 0xDFFF) {
+ // 1. Let a be c & 0x3FF.
+ var a = c & 0x3FF;
+
+ // 2. Let b be d & 0x3FF.
+ var b = d & 0x3FF;
+
+ // 3. Append to U the Unicode character with code point
+ // 2^16+2^10*a+b.
+ u.push(0x10000 + (a << 10) + b);
+
+ // 4. Set i to i+1.
+ i += 1;
+ }
+
+ // 3. Otherwise, d < 0xDC00 or d > 0xDFFF. Append to U a
+ // U+FFFD REPLACEMENT CHARACTER.
+ else {
+ u.push(0xFFFD);
+ }
+ }
+ }
+
+ // 3. Set i to i+1.
+ i += 1;
+ }
+
+ // 6. Return U.
+ return u;
+ }
+
+ /**
+ * @param {!Array.<number>} code_points Array of code points.
+ * @return {string} string String of UTF-16 code units.
+ */
+ function codePointsToString(code_points) {
+ var s = '';
+ for (var i = 0; i < code_points.length; ++i) {
+ var cp = code_points[i];
+ if (cp <= 0xFFFF) {
+ s += String.fromCharCode(cp);
+ } else {
+ cp -= 0x10000;
+ s += String.fromCharCode((cp >> 10) + 0xD800,
+ (cp & 0x3FF) + 0xDC00);
+ }
+ }
+ return s;
+ }
+
+
+ //
+ // Implementation of Encoding specification
+ // https://encoding.spec.whatwg.org/
+ //
+
+ //
+ // 4. Terminology
+ //
+
+ /**
+ * An ASCII byte is a byte in the range 0x00 to 0x7F, inclusive.
+ * @param {number} a The number to test.
+ * @return {boolean} True if a is in the range 0x00 to 0x7F, inclusive.
+ */
+ function isASCIIByte(a) {
+ return 0x00 <= a && a <= 0x7F;
+ }
+
+ /**
+ * An ASCII code point is a code point in the range U+0000 to
+ * U+007F, inclusive.
+ */
+ var isASCIICodePoint = isASCIIByte;
+
+
+ /**
+ * End-of-stream is a special token that signifies no more tokens
+ * are in the stream.
+ * @const
+ */ var end_of_stream = -1;
+
+ /**
+ * A stream represents an ordered sequence of tokens.
+ *
+ * @constructor
+ * @param {!(Array.<number>|Uint8Array)} tokens Array of tokens that provide
+ * the stream.
+ */
+ function Stream(tokens) {
+ /** @type {!Array.<number>} */
+ this.tokens = [].slice.call(tokens);
+ // Reversed as push/pop is more efficient than shift/unshift.
+ this.tokens.reverse();
+ }
+
+ Stream.prototype = {
+ /**
+ * @return {boolean} True if end-of-stream has been hit.
+ */
+ endOfStream: function() {
+ return !this.tokens.length;
+ },
+
+ /**
+ * When a token is read from a stream, the first token in the
+ * stream must be returned and subsequently removed, and
+ * end-of-stream must be returned otherwise.
+ *
+ * @return {number} Get the next token from the stream, or
+ * end_of_stream.
+ */
+ read: function() {
+ if (!this.tokens.length)
+ return end_of_stream;
+ return this.tokens.pop();
+ },
+
+ /**
+ * When one or more tokens are prepended to a stream, those tokens
+ * must be inserted, in given order, before the first token in the
+ * stream.
+ *
+ * @param {(number|!Array.<number>)} token The token(s) to prepend to the
+ * stream.
+ */
+ prepend: function(token) {
+ if (Array.isArray(token)) {
+ var tokens = /**@type {!Array.<number>}*/(token);
+ while (tokens.length)
+ this.tokens.push(tokens.pop());
+ } else {
+ this.tokens.push(token);
+ }
+ },
+
+ /**
+ * When one or more tokens are pushed to a stream, those tokens
+ * must be inserted, in given order, after the last token in the
+ * stream.
+ *
+ * @param {(number|!Array.<number>)} token The tokens(s) to push to the
+ * stream.
+ */
+ push: function(token) {
+ if (Array.isArray(token)) {
+ var tokens = /**@type {!Array.<number>}*/(token);
+ while (tokens.length)
+ this.tokens.unshift(tokens.shift());
+ } else {
+ this.tokens.unshift(token);
+ }
+ }
+ };
+
+ //
+ // 5. Encodings
+ //
+
+ // 5.1 Encoders and decoders
+
+ /** @const */
+ var finished = -1;
+
+ /**
+ * @param {boolean} fatal If true, decoding errors raise an exception.
+ * @param {number=} opt_code_point Override the standard fallback code point.
+ * @return {number} The code point to insert on a decoding error.
+ */
+ function decoderError(fatal, opt_code_point) {
+ if (fatal)
+ throw TypeError('Decoder error');
+ return opt_code_point || 0xFFFD;
+ }
+
+ /**
+ * @param {number} code_point The code point that could not be encoded.
+ * @return {number} Always throws, no value is actually returned.
+ */
+ function encoderError(code_point) {
+ throw TypeError('The code point ' + code_point + ' could not be encoded.');
+ }
+
+ /** @interface */
+ function Decoder() {}
+ Decoder.prototype = {
+ /**
+ * @param {Stream} stream The stream of bytes being decoded.
+ * @param {number} bite The next byte read from the stream.
+ * @return {?(number|!Array.<number>)} The next code point(s)
+ * decoded, or null if not enough data exists in the input
+ * stream to decode a complete code point, or |finished|.
+ */
+ handler: function(stream, bite) {}
+ };
+
+ /** @interface */
+ function Encoder() {}
+ Encoder.prototype = {
+ /**
+ * @param {Stream} stream The stream of code points being encoded.
+ * @param {number} code_point Next code point read from the stream.
+ * @return {(number|!Array.<number>)} Byte(s) to emit, or |finished|.
+ */
+ handler: function(stream, code_point) {}
+ };
+
+ // 5.2 Names and labels
+
+ // TODO: Define @typedef for Encoding: {name:string,labels:Array.<string>}
+ // https://github.com/google/closure-compiler/issues/247
+
+ /**
+ * @param {string} label The encoding label.
+ * @return {?{name:string,labels:Array.<string>}}
+ */
+ function getEncoding(label) {
+ // 1. Remove any leading and trailing ASCII whitespace from label.
+ label = String(label).trim().toLowerCase();
+
+ // 2. If label is an ASCII case-insensitive match for any of the
+ // labels listed in the table below, return the corresponding
+ // encoding, and failure otherwise.
+ if (Object.prototype.hasOwnProperty.call(label_to_encoding, label)) {
+ return label_to_encoding[label];
+ }
+ return null;
+ }
+
+ /**
+ * Encodings table: https://encoding.spec.whatwg.org/encodings.json
+ * @const
+ * @type {!Array.<{
+ * heading: string,
+ * encodings: Array.<{name:string,labels:Array.<string>}>
+ * }>}
+ */
+ var encodings = [
+ {
+ "encodings": [
+ {
+ "labels": [
+ "unicode-1-1-utf-8",
+ "utf-8",
+ "utf8"
+ ],
+ "name": "UTF-8"
+ }
+ ],
+ "heading": "The Encoding"
+ },
+ {
+ "encodings": [
+ {
+ "labels": [
+ "866",
+ "cp866",
+ "csibm866",
+ "ibm866"
+ ],
+ "name": "IBM866"
+ },
+ {
+ "labels": [
+ "csisolatin2",
+ "iso-8859-2",
+ "iso-ir-101",
+ "iso8859-2",
+ "iso88592",
+ "iso_8859-2",
+ "iso_8859-2:1987",
+ "l2",
+ "latin2"
+ ],
+ "name": "ISO-8859-2"
+ },
+ {
+ "labels": [
+ "csisolatin3",
+ "iso-8859-3",
+ "iso-ir-109",
+ "iso8859-3",
+ "iso88593",
+ "iso_8859-3",
+ "iso_8859-3:1988",
+ "l3",
+ "latin3"
+ ],
+ "name": "ISO-8859-3"
+ },
+ {
+ "labels": [
+ "csisolatin4",
+ "iso-8859-4",
+ "iso-ir-110",
+ "iso8859-4",
+ "iso88594",
+ "iso_8859-4",
+ "iso_8859-4:1988",
+ "l4",
+ "latin4"
+ ],
+ "name": "ISO-8859-4"
+ },
+ {
+ "labels": [
+ "csisolatincyrillic",
+ "cyrillic",
+ "iso-8859-5",
+ "iso-ir-144",
+ "iso8859-5",
+ "iso88595",
+ "iso_8859-5",
+ "iso_8859-5:1988"
+ ],
+ "name": "ISO-8859-5"
+ },
+ {
+ "labels": [
+ "arabic",
+ "asmo-708",
+ "csiso88596e",
+ "csiso88596i",
+ "csisolatinarabic",
+ "ecma-114",
+ "iso-8859-6",
+ "iso-8859-6-e",
+ "iso-8859-6-i",
+ "iso-ir-127",
+ "iso8859-6",
+ "iso88596",
+ "iso_8859-6",
+ "iso_8859-6:1987"
+ ],
+ "name": "ISO-8859-6"
+ },
+ {
+ "labels": [
+ "csisolatingreek",
+ "ecma-118",
+ "elot_928",
+ "greek",
+ "greek8",
+ "iso-8859-7",
+ "iso-ir-126",
+ "iso8859-7",
+ "iso88597",
+ "iso_8859-7",
+ "iso_8859-7:1987",
+ "sun_eu_greek"
+ ],
+ "name": "ISO-8859-7"
+ },
+ {
+ "labels": [
+ "csiso88598e",
+ "csisolatinhebrew",
+ "hebrew",
+ "iso-8859-8",
+ "iso-8859-8-e",
+ "iso-ir-138",
+ "iso8859-8",
+ "iso88598",
+ "iso_8859-8",
+ "iso_8859-8:1988",
+ "visual"
+ ],
+ "name": "ISO-8859-8"
+ },
+ {
+ "labels": [
+ "csiso88598i",
+ "iso-8859-8-i",
+ "logical"
+ ],
+ "name": "ISO-8859-8-I"
+ },
+ {
+ "labels": [
+ "csisolatin6",
+ "iso-8859-10",
+ "iso-ir-157",
+ "iso8859-10",
+ "iso885910",
+ "l6",
+ "latin6"
+ ],
+ "name": "ISO-8859-10"
+ },
+ {
+ "labels": [
+ "iso-8859-13",
+ "iso8859-13",
+ "iso885913"
+ ],
+ "name": "ISO-8859-13"
+ },
+ {
+ "labels": [
+ "iso-8859-14",
+ "iso8859-14",
+ "iso885914"
+ ],
+ "name": "ISO-8859-14"
+ },
+ {
+ "labels": [
+ "csisolatin9",
+ "iso-8859-15",
+ "iso8859-15",
+ "iso885915",
+ "iso_8859-15",
+ "l9"
+ ],
+ "name": "ISO-8859-15"
+ },
+ {
+ "labels": [
+ "iso-8859-16"
+ ],
+ "name": "ISO-8859-16"
+ },
+ {
+ "labels": [
+ "cskoi8r",
+ "koi",
+ "koi8",
+ "koi8-r",
+ "koi8_r"
+ ],
+ "name": "KOI8-R"
+ },
+ {
+ "labels": [
+ "koi8-ru",
+ "koi8-u"
+ ],
+ "name": "KOI8-U"
+ },
+ {
+ "labels": [
+ "csmacintosh",
+ "mac",
+ "macintosh",
+ "x-mac-roman"
+ ],
+ "name": "macintosh"
+ },
+ {
+ "labels": [
+ "dos-874",
+ "iso-8859-11",
+ "iso8859-11",
+ "iso885911",
+ "tis-620",
+ "windows-874"
+ ],
+ "name": "windows-874"
+ },
+ {
+ "labels": [
+ "cp1250",
+ "windows-1250",
+ "x-cp1250"
+ ],
+ "name": "windows-1250"
+ },
+ {
+ "labels": [
+ "cp1251",
+ "windows-1251",
+ "x-cp1251"
+ ],
+ "name": "windows-1251"
+ },
+ {
+ "labels": [
+ "ansi_x3.4-1968",
+ "ascii",
+ "cp1252",
+ "cp819",
+ "csisolatin1",
+ "ibm819",
+ "iso-8859-1",
+ "iso-ir-100",
+ "iso8859-1",
+ "iso88591",
+ "iso_8859-1",
+ "iso_8859-1:1987",
+ "l1",
+ "latin1",
+ "us-ascii",
+ "windows-1252",
+ "x-cp1252"
+ ],
+ "name": "windows-1252"
+ },
+ {
+ "labels": [
+ "cp1253",
+ "windows-1253",
+ "x-cp1253"
+ ],
+ "name": "windows-1253"
+ },
+ {
+ "labels": [
+ "cp1254",
+ "csisolatin5",
+ "iso-8859-9",
+ "iso-ir-148",
+ "iso8859-9",
+ "iso88599",
+ "iso_8859-9",
+ "iso_8859-9:1989",
+ "l5",
+ "latin5",
+ "windows-1254",
+ "x-cp1254"
+ ],
+ "name": "windows-1254"
+ },
+ {
+ "labels": [
+ "cp1255",
+ "windows-1255",
+ "x-cp1255"
+ ],
+ "name": "windows-1255"
+ },
+ {
+ "labels": [
+ "cp1256",
+ "windows-1256",
+ "x-cp1256"
+ ],
+ "name": "windows-1256"
+ },
+ {
+ "labels": [
+ "cp1257",
+ "windows-1257",
+ "x-cp1257"
+ ],
+ "name": "windows-1257"
+ },
+ {
+ "labels": [
+ "cp1258",
+ "windows-1258",
+ "x-cp1258"
+ ],
+ "name": "windows-1258"
+ },
+ {
+ "labels": [
+ "x-mac-cyrillic",
+ "x-mac-ukrainian"
+ ],
+ "name": "x-mac-cyrillic"
+ }
+ ],
+ "heading": "Legacy single-byte encodings"
+ },
+ {
+ "encodings": [
+ {
+ "labels": [
+ "chinese",
+ "csgb2312",
+ "csiso58gb231280",
+ "gb2312",
+ "gb_2312",
+ "gb_2312-80",
+ "gbk",
+ "iso-ir-58",
+ "x-gbk"
+ ],
+ "name": "GBK"
+ },
+ {
+ "labels": [
+ "gb18030"
+ ],
+ "name": "gb18030"
+ }
+ ],
+ "heading": "Legacy multi-byte Chinese (simplified) encodings"
+ },
+ {
+ "encodings": [
+ {
+ "labels": [
+ "big5",
+ "big5-hkscs",
+ "cn-big5",
+ "csbig5",
+ "x-x-big5"
+ ],
+ "name": "Big5"
+ }
+ ],
+ "heading": "Legacy multi-byte Chinese (traditional) encodings"
+ },
+ {
+ "encodings": [
+ {
+ "labels": [
+ "cseucpkdfmtjapanese",
+ "euc-jp",
+ "x-euc-jp"
+ ],
+ "name": "EUC-JP"
+ },
+ {
+ "labels": [
+ "csiso2022jp",
+ "iso-2022-jp"
+ ],
+ "name": "ISO-2022-JP"
+ },
+ {
+ "labels": [
+ "csshiftjis",
+ "ms932",
+ "ms_kanji",
+ "shift-jis",
+ "shift_jis",
+ "sjis",
+ "windows-31j",
+ "x-sjis"
+ ],
+ "name": "Shift_JIS"
+ }
+ ],
+ "heading": "Legacy multi-byte Japanese encodings"
+ },
+ {
+ "encodings": [
+ {
+ "labels": [
+ "cseuckr",
+ "csksc56011987",
+ "euc-kr",
+ "iso-ir-149",
+ "korean",
+ "ks_c_5601-1987",
+ "ks_c_5601-1989",
+ "ksc5601",
+ "ksc_5601",
+ "windows-949"
+ ],
+ "name": "EUC-KR"
+ }
+ ],
+ "heading": "Legacy multi-byte Korean encodings"
+ },
+ {
+ "encodings": [
+ {
+ "labels": [
+ "csiso2022kr",
+ "hz-gb-2312",
+ "iso-2022-cn",
+ "iso-2022-cn-ext",
+ "iso-2022-kr"
+ ],
+ "name": "replacement"
+ },
+ {
+ "labels": [
+ "utf-16be"
+ ],
+ "name": "UTF-16BE"
+ },
+ {
+ "labels": [
+ "utf-16",
+ "utf-16le"
+ ],
+ "name": "UTF-16LE"
+ },
+ {
+ "labels": [
+ "x-user-defined"
+ ],
+ "name": "x-user-defined"
+ }
+ ],
+ "heading": "Legacy miscellaneous encodings"
+ }
+ ];
+
+ // Label to encoding registry.
+ /** @type {Object.<string,{name:string,labels:Array.<string>}>} */
+ var label_to_encoding = {};
+ encodings.forEach(function(category) {
+ category.encodings.forEach(function(encoding) {
+ encoding.labels.forEach(function(label) {
+ label_to_encoding[label] = encoding;
+ });
+ });
+ });
+
+ // Registry of of encoder/decoder factories, by encoding name.
+ /** @type {Object.<string, function({fatal:boolean}): Encoder>} */
+ var encoders = {};
+ /** @type {Object.<string, function({fatal:boolean}): Decoder>} */
+ var decoders = {};
+
+ //
+ // 6. Indexes
+ //
+
+ /**
+ * @param {number} pointer The |pointer| to search for.
+ * @param {(!Array.<?number>|undefined)} index The |index| to search within.
+ * @return {?number} The code point corresponding to |pointer| in |index|,
+ * or null if |code point| is not in |index|.
+ */
+ function indexCodePointFor(pointer, index) {
+ if (!index) return null;
+ return index[pointer] || null;
+ }
+
+ /**
+ * @param {number} code_point The |code point| to search for.
+ * @param {!Array.<?number>} index The |index| to search within.
+ * @return {?number} The first pointer corresponding to |code point| in
+ * |index|, or null if |code point| is not in |index|.
+ */
+ function indexPointerFor(code_point, index) {
+ var pointer = index.indexOf(code_point);
+ return pointer === -1 ? null : pointer;
+ }
+
+ /**
+ * @param {string} name Name of the index.
+ * @return {(!Array.<number>|!Array.<Array.<number>>)}
+ * */
+ function index(name) {
+ if (!('encoding-indexes' in global)) {
+ throw Error("Indexes missing." +
+ " Did you forget to include encoding-indexes.js first?");
+ }
+ return global['encoding-indexes'][name];
+ }
+
+ /**
+ * @param {number} pointer The |pointer| to search for in the gb18030 index.
+ * @return {?number} The code point corresponding to |pointer| in |index|,
+ * or null if |code point| is not in the gb18030 index.
+ */
+ function indexGB18030RangesCodePointFor(pointer) {
+ // 1. If pointer is greater than 39419 and less than 189000, or
+ // pointer is greater than 1237575, return null.
+ if ((pointer > 39419 && pointer < 189000) || (pointer > 1237575))
+ return null;
+
+ // 2. If pointer is 7457, return code point U+E7C7.
+ if (pointer === 7457) return 0xE7C7;
+
+ // 3. Let offset be the last pointer in index gb18030 ranges that
+ // is equal to or less than pointer and let code point offset be
+ // its corresponding code point.
+ var offset = 0;
+ var code_point_offset = 0;
+ var idx = index('gb18030-ranges');
+ var i;
+ for (i = 0; i < idx.length; ++i) {
+ /** @type {!Array.<number>} */
+ var entry = idx[i];
+ if (entry[0] <= pointer) {
+ offset = entry[0];
+ code_point_offset = entry[1];
+ } else {
+ break;
+ }
+ }
+
+ // 4. Return a code point whose value is code point offset +
+ // pointer − offset.
+ return code_point_offset + pointer - offset;
+ }
+
+ /**
+ * @param {number} code_point The |code point| to locate in the gb18030 index.
+ * @return {number} The first pointer corresponding to |code point| in the
+ * gb18030 index.
+ */
+ function indexGB18030RangesPointerFor(code_point) {
+ // 1. If code point is U+E7C7, return pointer 7457.
+ if (code_point === 0xE7C7) return 7457;
+
+ // 2. Let offset be the last code point in index gb18030 ranges
+ // that is equal to or less than code point and let pointer offset
+ // be its corresponding pointer.
+ var offset = 0;
+ var pointer_offset = 0;
+ var idx = index('gb18030-ranges');
+ var i;
+ for (i = 0; i < idx.length; ++i) {
+ /** @type {!Array.<number>} */
+ var entry = idx[i];
+ if (entry[1] <= code_point) {
+ offset = entry[1];
+ pointer_offset = entry[0];
+ } else {
+ break;
+ }
+ }
+
+ // 3. Return a pointer whose value is pointer offset + code point
+ // − offset.
+ return pointer_offset + code_point - offset;
+ }
+
+ /**
+ * @param {number} code_point The |code_point| to search for in the Shift_JIS
+ * index.
+ * @return {?number} The code point corresponding to |pointer| in |index|,
+ * or null if |code point| is not in the Shift_JIS index.
+ */
+ function indexShiftJISPointerFor(code_point) {
+ // 1. Let index be index jis0208 excluding all entries whose
+ // pointer is in the range 8272 to 8835, inclusive.
+ shift_jis_index = shift_jis_index ||
+ index('jis0208').map(function(code_point, pointer) {
+ return inRange(pointer, 8272, 8835) ? null : code_point;
+ });
+ var index_ = shift_jis_index;
+
+ // 2. Return the index pointer for code point in index.
+ return index_.indexOf(code_point);
+ }
+ var shift_jis_index;
+
+ /**
+ * @param {number} code_point The |code_point| to search for in the big5
+ * index.
+ * @return {?number} The code point corresponding to |pointer| in |index|,
+ * or null if |code point| is not in the big5 index.
+ */
+ function indexBig5PointerFor(code_point) {
+ // 1. Let index be index Big5 excluding all entries whose pointer
+ big5_index_no_hkscs = big5_index_no_hkscs ||
+ index('big5').map(function(code_point, pointer) {
+ return (pointer < (0xA1 - 0x81) * 157) ? null : code_point;
+ });
+ var index_ = big5_index_no_hkscs;
+
+ // 2. If code point is U+2550, U+255E, U+2561, U+256A, U+5341, or
+ // U+5345, return the last pointer corresponding to code point in
+ // index.
+ if (code_point === 0x2550 || code_point === 0x255E ||
+ code_point === 0x2561 || code_point === 0x256A ||
+ code_point === 0x5341 || code_point === 0x5345) {
+ return index_.lastIndexOf(code_point);
+ }
+
+ // 3. Return the index pointer for code point in index.
+ return indexPointerFor(code_point, index_);
+ }
+ var big5_index_no_hkscs;
+
+ //
+ // 8. API
+ //
+
+ /** @const */ var DEFAULT_ENCODING = 'utf-8';
+
+ // 8.1 Interface TextDecoder
+
+ /**
+ * @constructor
+ * @param {string=} label The label of the encoding;
+ * defaults to 'utf-8'.
+ * @param {Object=} options
+ */
+ function TextDecoder(label, options) {
+ // Web IDL conventions
+ if (!(this instanceof TextDecoder))
+ throw TypeError('Called as a function. Did you forget \'new\'?');
+ label = label !== undefined ? String(label) : DEFAULT_ENCODING;
+ options = ToDictionary(options);
+
+ // A TextDecoder object has an associated encoding, decoder,
+ // stream, ignore BOM flag (initially unset), BOM seen flag
+ // (initially unset), error mode (initially replacement), and do
+ // not flush flag (initially unset).
+
+ /** @private */
+ this._encoding = null;
+ /** @private @type {?Decoder} */
+ this._decoder = null;
+ /** @private @type {boolean} */
+ this._ignoreBOM = false;
+ /** @private @type {boolean} */
+ this._BOMseen = false;
+ /** @private @type {string} */
+ this._error_mode = 'replacement';
+ /** @private @type {boolean} */
+ this._do_not_flush = false;
+
+
+ // 1. Let encoding be the result of getting an encoding from
+ // label.
+ var encoding = getEncoding(label);
+
+ // 2. If encoding is failure or replacement, throw a RangeError.
+ if (encoding === null || encoding.name === 'replacement')
+ throw RangeError('Unknown encoding: ' + label);
+ if (!decoders[encoding.name]) {
+ throw Error('Decoder not present.' +
+ ' Did you forget to include encoding-indexes.js first?');
+ }
+
+ // 3. Let dec be a new TextDecoder object.
+ var dec = this;
+
+ // 4. Set dec's encoding to encoding.
+ dec._encoding = encoding;
+
+ // 5. If options's fatal member is true, set dec's error mode to
+ // fatal.
+ if (Boolean(options['fatal']))
+ dec._error_mode = 'fatal';
+
+ // 6. If options's ignoreBOM member is true, set dec's ignore BOM
+ // flag.
+ if (Boolean(options['ignoreBOM']))
+ dec._ignoreBOM = true;
+
+ // For pre-ES5 runtimes:
+ if (!Object.defineProperty) {
+ this.encoding = dec._encoding.name.toLowerCase();
+ this.fatal = dec._error_mode === 'fatal';
+ this.ignoreBOM = dec._ignoreBOM;
+ }
+
+ // 7. Return dec.
+ return dec;
+ }
+
+ if (Object.defineProperty) {
+ // The encoding attribute's getter must return encoding's name.
+ Object.defineProperty(TextDecoder.prototype, 'encoding', {
+ /** @this {TextDecoder} */
+ get: function() { return this._encoding.name.toLowerCase(); }
+ });
+
+ // The fatal attribute's getter must return true if error mode
+ // is fatal, and false otherwise.
+ Object.defineProperty(TextDecoder.prototype, 'fatal', {
+ /** @this {TextDecoder} */
+ get: function() { return this._error_mode === 'fatal'; }
+ });
+
+ // The ignoreBOM attribute's getter must return true if ignore
+ // BOM flag is set, and false otherwise.
+ Object.defineProperty(TextDecoder.prototype, 'ignoreBOM', {
+ /** @this {TextDecoder} */
+ get: function() { return this._ignoreBOM; }
+ });
+ }
+
+ /**
+ * @param {BufferSource=} input The buffer of bytes to decode.
+ * @param {Object=} options
+ * @return {string} The decoded string.
+ */
+ TextDecoder.prototype.decode = function decode(input, options) {
+ var bytes;
+ if (typeof input === 'object' && input instanceof ArrayBuffer) {
+ bytes = new Uint8Array(input);
+ } else if (typeof input === 'object' && 'buffer' in input &&
+ input.buffer instanceof ArrayBuffer) {
+ bytes = new Uint8Array(input.buffer,
+ input.byteOffset,
+ input.byteLength);
+ } else {
+ bytes = new Uint8Array(0);
+ }
+
+ options = ToDictionary(options);
+
+ // 1. If the do not flush flag is unset, set decoder to a new
+ // encoding's decoder, set stream to a new stream, and unset the
+ // BOM seen flag.
+ if (!this._do_not_flush) {
+ this._decoder = decoders[this._encoding.name]({
+ fatal: this._error_mode === 'fatal'});
+ this._BOMseen = false;
+ }
+
+ // 2. If options's stream is true, set the do not flush flag, and
+ // unset the do not flush flag otherwise.
+ this._do_not_flush = Boolean(options['stream']);
+
+ // 3. If input is given, push a copy of input to stream.
+ // TODO: Align with spec algorithm - maintain stream on instance.
+ var input_stream = new Stream(bytes);
+
+ // 4. Let output be a new stream.
+ var output = [];
+
+ /** @type {?(number|!Array.<number>)} */
+ var result;
+
+ // 5. While true:
+ while (true) {
+ // 1. Let token be the result of reading from stream.
+ var token = input_stream.read();
+
+ // 2. If token is end-of-stream and the do not flush flag is
+ // set, return output, serialized.
+ // TODO: Align with spec algorithm.
+ if (token === end_of_stream)
+ break;
+
+ // 3. Otherwise, run these subsubsteps:
+
+ // 1. Let result be the result of processing token for decoder,
+ // stream, output, and error mode.
+ result = this._decoder.handler(input_stream, token);
+
+ // 2. If result is finished, return output, serialized.
+ if (result === finished)
+ break;
+
+ if (result !== null) {
+ if (Array.isArray(result))
+ output.push.apply(output, /**@type {!Array.<number>}*/(result));
+ else
+ output.push(result);
+ }
+
+ // 3. Otherwise, if result is error, throw a TypeError.
+ // (Thrown in handler)
+
+ // 4. Otherwise, do nothing.
+ }
+ // TODO: Align with spec algorithm.
+ if (!this._do_not_flush) {
+ do {
+ result = this._decoder.handler(input_stream, input_stream.read());
+ if (result === finished)
+ break;
+ if (result === null)
+ continue;
+ if (Array.isArray(result))
+ output.push.apply(output, /**@type {!Array.<number>}*/(result));
+ else
+ output.push(result);
+ } while (!input_stream.endOfStream());
+ this._decoder = null;
+ }
+
+ // A TextDecoder object also has an associated serialize stream
+ // algorithm...
+ /**
+ * @param {!Array.<number>} stream
+ * @return {string}
+ * @this {TextDecoder}
+ */
+ function serializeStream(stream) {
+ // 1. Let token be the result of reading from stream.
+ // (Done in-place on array, rather than as a stream)
+
+ // 2. If encoding is UTF-8, UTF-16BE, or UTF-16LE, and ignore
+ // BOM flag and BOM seen flag are unset, run these subsubsteps:
+ if (includes(['UTF-8', 'UTF-16LE', 'UTF-16BE'], this._encoding.name) &&
+ !this._ignoreBOM && !this._BOMseen) {
+ if (stream.length > 0 && stream[0] === 0xFEFF) {
+ // 1. If token is U+FEFF, set BOM seen flag.
+ this._BOMseen = true;
+ stream.shift();
+ } else if (stream.length > 0) {
+ // 2. Otherwise, if token is not end-of-stream, set BOM seen
+ // flag and append token to stream.
+ this._BOMseen = true;
+ } else {
+ // 3. Otherwise, if token is not end-of-stream, append token
+ // to output.
+ // (no-op)
+ }
+ }
+ // 4. Otherwise, return output.
+ return codePointsToString(stream);
+ }
+
+ return serializeStream.call(this, output);
+ };
+
+ // 8.2 Interface TextEncoder
+
+ /**
+ * @constructor
+ * @param {string=} label The label of the encoding. NONSTANDARD.
+ * @param {Object=} options NONSTANDARD.
+ */
+ function TextEncoder(label, options) {
+ // Web IDL conventions
+ if (!(this instanceof TextEncoder))
+ throw TypeError('Called as a function. Did you forget \'new\'?');
+ options = ToDictionary(options);
+
+ // A TextEncoder object has an associated encoding and encoder.
+
+ /** @private */
+ this._encoding = null;
+ /** @private @type {?Encoder} */
+ this._encoder = null;
+
+ // Non-standard
+ /** @private @type {boolean} */
+ this._do_not_flush = false;
+ /** @private @type {string} */
+ this._fatal = Boolean(options['fatal']) ? 'fatal' : 'replacement';
+
+ // 1. Let enc be a new TextEncoder object.
+ var enc = this;
+
+ // 2. Set enc's encoding to UTF-8's encoder.
+ if (Boolean(options['NONSTANDARD_allowLegacyEncoding'])) {
+ // NONSTANDARD behavior.
+ label = label !== undefined ? String(label) : DEFAULT_ENCODING;
+ var encoding = getEncoding(label);
+ if (encoding === null || encoding.name === 'replacement')
+ throw RangeError('Unknown encoding: ' + label);
+ if (!encoders[encoding.name]) {
+ throw Error('Encoder not present.' +
+ ' Did you forget to include encoding-indexes.js first?');
+ }
+ enc._encoding = encoding;
+ } else {
+ // Standard behavior.
+ enc._encoding = getEncoding('utf-8');
+
+ if (label !== undefined && 'console' in global) {
+ console.warn('TextEncoder constructor called with encoding label, '
+ + 'which is ignored.');
+ }
+ }
+
+ // For pre-ES5 runtimes:
+ if (!Object.defineProperty)
+ this.encoding = enc._encoding.name.toLowerCase();
+
+ // 3. Return enc.
+ return enc;
+ }
+
+ if (Object.defineProperty) {
+ // The encoding attribute's getter must return encoding's name.
+ Object.defineProperty(TextEncoder.prototype, 'encoding', {
+ /** @this {TextEncoder} */
+ get: function() { return this._encoding.name.toLowerCase(); }
+ });
+ }
+
+ /**
+ * @param {string=} opt_string The string to encode.
+ * @param {Object=} options
+ * @return {!Uint8Array} Encoded bytes, as a Uint8Array.
+ */
+ TextEncoder.prototype.encode = function encode(opt_string, options) {
+ opt_string = opt_string === undefined ? '' : String(opt_string);
+ options = ToDictionary(options);
+
+ // NOTE: This option is nonstandard. None of the encodings
+ // permitted for encoding (i.e. UTF-8, UTF-16) are stateful when
+ // the input is a USVString so streaming is not necessary.
+ if (!this._do_not_flush)
+ this._encoder = encoders[this._encoding.name]({
+ fatal: this._fatal === 'fatal'});
+ this._do_not_flush = Boolean(options['stream']);
+
+ // 1. Convert input to a stream.
+ var input = new Stream(stringToCodePoints(opt_string));
+
+ // 2. Let output be a new stream
+ var output = [];
+
+ /** @type {?(number|!Array.<number>)} */
+ var result;
+ // 3. While true, run these substeps:
+ while (true) {
+ // 1. Let token be the result of reading from input.
+ var token = input.read();
+ if (token === end_of_stream)
+ break;
+ // 2. Let result be the result of processing token for encoder,
+ // input, output.
+ result = this._encoder.handler(input, token);
+ if (result === finished)
+ break;
+ if (Array.isArray(result))
+ output.push.apply(output, /**@type {!Array.<number>}*/(result));
+ else
+ output.push(result);
+ }
+ // TODO: Align with spec algorithm.
+ if (!this._do_not_flush) {
+ while (true) {
+ result = this._encoder.handler(input, input.read());
+ if (result === finished)
+ break;
+ if (Array.isArray(result))
+ output.push.apply(output, /**@type {!Array.<number>}*/(result));
+ else
+ output.push(result);
+ }
+ this._encoder = null;
+ }
+ // 3. If result is finished, convert output into a byte sequence,
+ // and then return a Uint8Array object wrapping an ArrayBuffer
+ // containing output.
+ return new Uint8Array(output);
+ };
+
+
+ //
+ // 9. The encoding
+ //
+
+ // 9.1 utf-8
+
+ // 9.1.1 utf-8 decoder
+ /**
+ * @constructor
+ * @implements {Decoder}
+ * @param {{fatal: boolean}} options
+ */
+ function UTF8Decoder(options) {
+ var fatal = options.fatal;
+
+ // utf-8's decoder's has an associated utf-8 code point, utf-8
+ // bytes seen, and utf-8 bytes needed (all initially 0), a utf-8
+ // lower boundary (initially 0x80), and a utf-8 upper boundary
+ // (initially 0xBF).
+ var /** @type {number} */ utf8_code_point = 0,
+ /** @type {number} */ utf8_bytes_seen = 0,
+ /** @type {number} */ utf8_bytes_needed = 0,
+ /** @type {number} */ utf8_lower_boundary = 0x80,
+ /** @type {number} */ utf8_upper_boundary = 0xBF;
+
+ /**
+ * @param {Stream} stream The stream of bytes being decoded.
+ * @param {number} bite The next byte read from the stream.
+ * @return {?(number|!Array.<number>)} The next code point(s)
+ * decoded, or null if not enough data exists in the input
+ * stream to decode a complete code point.
+ */
+ this.handler = function(stream, bite) {
+ // 1. If byte is end-of-stream and utf-8 bytes needed is not 0,
+ // set utf-8 bytes needed to 0 and return error.
+ if (bite === end_of_stream && utf8_bytes_needed !== 0) {
+ utf8_bytes_needed = 0;
+ return decoderError(fatal);
+ }
+
+ // 2. If byte is end-of-stream, return finished.
+ if (bite === end_of_stream)
+ return finished;
+
+ // 3. If utf-8 bytes needed is 0, based on byte:
+ if (utf8_bytes_needed === 0) {
+
+ // 0x00 to 0x7F
+ if (inRange(bite, 0x00, 0x7F)) {
+ // Return a code point whose value is byte.
+ return bite;
+ }
+
+ // 0xC2 to 0xDF
+ else if (inRange(bite, 0xC2, 0xDF)) {
+ // 1. Set utf-8 bytes needed to 1.
+ utf8_bytes_needed = 1;
+
+ // 2. Set UTF-8 code point to byte & 0x1F.
+ utf8_code_point = bite & 0x1F;
+ }
+
+ // 0xE0 to 0xEF
+ else if (inRange(bite, 0xE0, 0xEF)) {
+ // 1. If byte is 0xE0, set utf-8 lower boundary to 0xA0.
+ if (bite === 0xE0)
+ utf8_lower_boundary = 0xA0;
+ // 2. If byte is 0xED, set utf-8 upper boundary to 0x9F.
+ if (bite === 0xED)
+ utf8_upper_boundary = 0x9F;
+ // 3. Set utf-8 bytes needed to 2.
+ utf8_bytes_needed = 2;
+ // 4. Set UTF-8 code point to byte & 0xF.
+ utf8_code_point = bite & 0xF;
+ }
+
+ // 0xF0 to 0xF4
+ else if (inRange(bite, 0xF0, 0xF4)) {
+ // 1. If byte is 0xF0, set utf-8 lower boundary to 0x90.
+ if (bite === 0xF0)
+ utf8_lower_boundary = 0x90;
+ // 2. If byte is 0xF4, set utf-8 upper boundary to 0x8F.
+ if (bite === 0xF4)
+ utf8_upper_boundary = 0x8F;
+ // 3. Set utf-8 bytes needed to 3.
+ utf8_bytes_needed = 3;
+ // 4. Set UTF-8 code point to byte & 0x7.
+ utf8_code_point = bite & 0x7;
+ }
+
+ // Otherwise
+ else {
+ // Return error.
+ return decoderError(fatal);
+ }
+
+ // Return continue.
+ return null;
+ }
+
+ // 4. If byte is not in the range utf-8 lower boundary to utf-8
+ // upper boundary, inclusive, run these substeps:
+ if (!inRange(bite, utf8_lower_boundary, utf8_upper_boundary)) {
+
+ // 1. Set utf-8 code point, utf-8 bytes needed, and utf-8
+ // bytes seen to 0, set utf-8 lower boundary to 0x80, and set
+ // utf-8 upper boundary to 0xBF.
+ utf8_code_point = utf8_bytes_needed = utf8_bytes_seen = 0;
+ utf8_lower_boundary = 0x80;
+ utf8_upper_boundary = 0xBF;
+
+ // 2. Prepend byte to stream.
+ stream.prepend(bite);
+
+ // 3. Return error.
+ return decoderError(fatal);
+ }
+
+ // 5. Set utf-8 lower boundary to 0x80 and utf-8 upper boundary
+ // to 0xBF.
+ utf8_lower_boundary = 0x80;
+ utf8_upper_boundary = 0xBF;
+
+ // 6. Set UTF-8 code point to (UTF-8 code point << 6) | (byte &
+ // 0x3F)
+ utf8_code_point = (utf8_code_point << 6) | (bite & 0x3F);
+
+ // 7. Increase utf-8 bytes seen by one.
+ utf8_bytes_seen += 1;
+
+ // 8. If utf-8 bytes seen is not equal to utf-8 bytes needed,
+ // continue.
+ if (utf8_bytes_seen !== utf8_bytes_needed)
+ return null;
+
+ // 9. Let code point be utf-8 code point.
+ var code_point = utf8_code_point;
+
+ // 10. Set utf-8 code point, utf-8 bytes needed, and utf-8 bytes
+ // seen to 0.
+ utf8_code_point = utf8_bytes_needed = utf8_bytes_seen = 0;
+
+ // 11. Return a code point whose value is code point.
+ return code_point;
+ };
+ }
+
+ // 9.1.2 utf-8 encoder
+ /**
+ * @constructor
+ * @implements {Encoder}
+ * @param {{fatal: boolean}} options
+ */
+ function UTF8Encoder(options) {
+ var fatal = options.fatal;
+ /**
+ * @param {Stream} stream Input stream.
+ * @param {number} code_point Next code point read from the stream.
+ * @return {(number|!Array.<number>)} Byte(s) to emit.
+ */
+ this.handler = function(stream, code_point) {
+ // 1. If code point is end-of-stream, return finished.
+ if (code_point === end_of_stream)
+ return finished;
+
+ // 2. If code point is an ASCII code point, return a byte whose
+ // value is code point.
+ if (isASCIICodePoint(code_point))
+ return code_point;
+
+ // 3. Set count and offset based on the range code point is in:
+ var count, offset;
+ // U+0080 to U+07FF, inclusive:
+ if (inRange(code_point, 0x0080, 0x07FF)) {
+ // 1 and 0xC0
+ count = 1;
+ offset = 0xC0;
+ }
+ // U+0800 to U+FFFF, inclusive:
+ else if (inRange(code_point, 0x0800, 0xFFFF)) {
+ // 2 and 0xE0
+ count = 2;
+ offset = 0xE0;
+ }
+ // U+10000 to U+10FFFF, inclusive:
+ else if (inRange(code_point, 0x10000, 0x10FFFF)) {
+ // 3 and 0xF0
+ count = 3;
+ offset = 0xF0;
+ }
+
+ // 4. Let bytes be a byte sequence whose first byte is (code
+ // point >> (6 × count)) + offset.
+ var bytes = [(code_point >> (6 * count)) + offset];
+
+ // 5. Run these substeps while count is greater than 0:
+ while (count > 0) {
+
+ // 1. Set temp to code point >> (6 × (count − 1)).
+ var temp = code_point >> (6 * (count - 1));
+
+ // 2. Append to bytes 0x80 | (temp & 0x3F).
+ bytes.push(0x80 | (temp & 0x3F));
+
+ // 3. Decrease count by one.
+ count -= 1;
+ }
+
+ // 6. Return bytes bytes, in order.
+ return bytes;
+ };
+ }
+
+ /** @param {{fatal: boolean}} options */
+ encoders['UTF-8'] = function(options) {
+ return new UTF8Encoder(options);
+ };
+ /** @param {{fatal: boolean}} options */
+ decoders['UTF-8'] = function(options) {
+ return new UTF8Decoder(options);
+ };
+
+ //
+ // 10. Legacy single-byte encodings
+ //
+
+ // 10.1 single-byte decoder
+ /**
+ * @constructor
+ * @implements {Decoder}
+ * @param {!Array.<number>} index The encoding index.
+ * @param {{fatal: boolean}} options
+ */
+ function SingleByteDecoder(index, options) {
+ var fatal = options.fatal;
+ /**
+ * @param {Stream} stream The stream of bytes being decoded.
+ * @param {number} bite The next byte read from the stream.
+ * @return {?(number|!Array.<number>)} The next code point(s)
+ * decoded, or null if not enough data exists in the input
+ * stream to decode a complete code point.
+ */
+ this.handler = function(stream, bite) {
+ // 1. If byte is end-of-stream, return finished.
+ if (bite === end_of_stream)
+ return finished;
+
+ // 2. If byte is an ASCII byte, return a code point whose value
+ // is byte.
+ if (isASCIIByte(bite))
+ return bite;
+
+ // 3. Let code point be the index code point for byte − 0x80 in
+ // index single-byte.
+ var code_point = index[bite - 0x80];
+
+ // 4. If code point is null, return error.
+ if (code_point === null)
+ return decoderError(fatal);
+
+ // 5. Return a code point whose value is code point.
+ return code_point;
+ };
+ }
+
+ // 10.2 single-byte encoder
+ /**
+ * @constructor
+ * @implements {Encoder}
+ * @param {!Array.<?number>} index The encoding index.
+ * @param {{fatal: boolean}} options
+ */
+ function SingleByteEncoder(index, options) {
+ var fatal = options.fatal;
+ /**
+ * @param {Stream} stream Input stream.
+ * @param {number} code_point Next code point read from the stream.
+ * @return {(number|!Array.<number>)} Byte(s) to emit.
+ */
+ this.handler = function(stream, code_point) {
+ // 1. If code point is end-of-stream, return finished.
+ if (code_point === end_of_stream)
+ return finished;
+
+ // 2. If code point is an ASCII code point, return a byte whose
+ // value is code point.
+ if (isASCIICodePoint(code_point))
+ return code_point;
+
+ // 3. Let pointer be the index pointer for code point in index
+ // single-byte.
+ var pointer = indexPointerFor(code_point, index);
+
+ // 4. If pointer is null, return error with code point.
+ if (pointer === null)
+ encoderError(code_point);
+
+ // 5. Return a byte whose value is pointer + 0x80.
+ return pointer + 0x80;
+ };
+ }
+
+ (function() {
+ if (!('encoding-indexes' in global))
+ return;
+ encodings.forEach(function(category) {
+ if (category.heading !== 'Legacy single-byte encodings')
+ return;
+ category.encodings.forEach(function(encoding) {
+ var name = encoding.name;
+ var idx = index(name.toLowerCase());
+ /** @param {{fatal: boolean}} options */
+ decoders[name] = function(options) {
+ return new SingleByteDecoder(idx, options);
+ };
+ /** @param {{fatal: boolean}} options */
+ encoders[name] = function(options) {
+ return new SingleByteEncoder(idx, options);
+ };
+ });
+ });
+ }());
+
+ //
+ // 11. Legacy multi-byte Chinese (simplified) encodings
+ //
+
+ // 11.1 gbk
+
+ // 11.1.1 gbk decoder
+ // gbk's decoder is gb18030's decoder.
+ /** @param {{fatal: boolean}} options */
+ decoders['GBK'] = function(options) {
+ return new GB18030Decoder(options);
+ };
+
+ // 11.1.2 gbk encoder
+ // gbk's encoder is gb18030's encoder with its gbk flag set.
+ /** @param {{fatal: boolean}} options */
+ encoders['GBK'] = function(options) {
+ return new GB18030Encoder(options, true);
+ };
+
+ // 11.2 gb18030
+
+ // 11.2.1 gb18030 decoder
+ /**
+ * @constructor
+ * @implements {Decoder}
+ * @param {{fatal: boolean}} options
+ */
+ function GB18030Decoder(options) {
+ var fatal = options.fatal;
+ // gb18030's decoder has an associated gb18030 first, gb18030
+ // second, and gb18030 third (all initially 0x00).
+ var /** @type {number} */ gb18030_first = 0x00,
+ /** @type {number} */ gb18030_second = 0x00,
+ /** @type {number} */ gb18030_third = 0x00;
+ /**
+ * @param {Stream} stream The stream of bytes being decoded.
+ * @param {number} bite The next byte read from the stream.
+ * @return {?(number|!Array.<number>)} The next code point(s)
+ * decoded, or null if not enough data exists in the input
+ * stream to decode a complete code point.
+ */
+ this.handler = function(stream, bite) {
+ // 1. If byte is end-of-stream and gb18030 first, gb18030
+ // second, and gb18030 third are 0x00, return finished.
+ if (bite === end_of_stream && gb18030_first === 0x00 &&
+ gb18030_second === 0x00 && gb18030_third === 0x00) {
+ return finished;
+ }
+ // 2. If byte is end-of-stream, and gb18030 first, gb18030
+ // second, or gb18030 third is not 0x00, set gb18030 first,
+ // gb18030 second, and gb18030 third to 0x00, and return error.
+ if (bite === end_of_stream &&
+ (gb18030_first !== 0x00 || gb18030_second !== 0x00 ||
+ gb18030_third !== 0x00)) {
+ gb18030_first = 0x00;
+ gb18030_second = 0x00;
+ gb18030_third = 0x00;
+ decoderError(fatal);
+ }
+ var code_point;
+ // 3. If gb18030 third is not 0x00, run these substeps:
+ if (gb18030_third !== 0x00) {
+ // 1. Let code point be null.
+ code_point = null;
+ // 2. If byte is in the range 0x30 to 0x39, inclusive, set
+ // code point to the index gb18030 ranges code point for
+ // (((gb18030 first − 0x81) × 10 + gb18030 second − 0x30) ×
+ // 126 + gb18030 third − 0x81) × 10 + byte − 0x30.
+ if (inRange(bite, 0x30, 0x39)) {
+ code_point = indexGB18030RangesCodePointFor(
+ (((gb18030_first - 0x81) * 10 + gb18030_second - 0x30) * 126 +
+ gb18030_third - 0x81) * 10 + bite - 0x30);
+ }
+
+ // 3. Let buffer be a byte sequence consisting of gb18030
+ // second, gb18030 third, and byte, in order.
+ var buffer = [gb18030_second, gb18030_third, bite];
+
+ // 4. Set gb18030 first, gb18030 second, and gb18030 third to
+ // 0x00.
+ gb18030_first = 0x00;
+ gb18030_second = 0x00;
+ gb18030_third = 0x00;
+
+ // 5. If code point is null, prepend buffer to stream and
+ // return error.
+ if (code_point === null) {
+ stream.prepend(buffer);
+ return decoderError(fatal);
+ }
+
+ // 6. Return a code point whose value is code point.
+ return code_point;
+ }
+
+ // 4. If gb18030 second is not 0x00, run these substeps:
+ if (gb18030_second !== 0x00) {
+
+ // 1. If byte is in the range 0x81 to 0xFE, inclusive, set
+ // gb18030 third to byte and return continue.
+ if (inRange(bite, 0x81, 0xFE)) {
+ gb18030_third = bite;
+ return null;
+ }
+
+ // 2. Prepend gb18030 second followed by byte to stream, set
+ // gb18030 first and gb18030 second to 0x00, and return error.
+ stream.prepend([gb18030_second, bite]);
+ gb18030_first = 0x00;
+ gb18030_second = 0x00;
+ return decoderError(fatal);
+ }
+
+ // 5. If gb18030 first is not 0x00, run these substeps:
+ if (gb18030_first !== 0x00) {
+
+ // 1. If byte is in the range 0x30 to 0x39, inclusive, set
+ // gb18030 second to byte and return continue.
+ if (inRange(bite, 0x30, 0x39)) {
+ gb18030_second = bite;
+ return null;
+ }
+
+ // 2. Let lead be gb18030 first, let pointer be null, and set
+ // gb18030 first to 0x00.
+ var lead = gb18030_first;
+ var pointer = null;
+ gb18030_first = 0x00;
+
+ // 3. Let offset be 0x40 if byte is less than 0x7F and 0x41
+ // otherwise.
+ var offset = bite < 0x7F ? 0x40 : 0x41;
+
+ // 4. If byte is in the range 0x40 to 0x7E, inclusive, or 0x80
+ // to 0xFE, inclusive, set pointer to (lead − 0x81) × 190 +
+ // (byte − offset).
+ if (inRange(bite, 0x40, 0x7E) || inRange(bite, 0x80, 0xFE))
+ pointer = (lead - 0x81) * 190 + (bite - offset);
+
+ // 5. Let code point be null if pointer is null and the index
+ // code point for pointer in index gb18030 otherwise.
+ code_point = pointer === null ? null :
+ indexCodePointFor(pointer, index('gb18030'));
+
+ // 6. If code point is null and byte is an ASCII byte, prepend
+ // byte to stream.
+ if (code_point === null && isASCIIByte(bite))
+ stream.prepend(bite);
+
+ // 7. If code point is null, return error.
+ if (code_point === null)
+ return decoderError(fatal);
+
+ // 8. Return a code point whose value is code point.
+ return code_point;
+ }
+
+ // 6. If byte is an ASCII byte, return a code point whose value
+ // is byte.
+ if (isASCIIByte(bite))
+ return bite;
+
+ // 7. If byte is 0x80, return code point U+20AC.
+ if (bite === 0x80)
+ return 0x20AC;
+
+ // 8. If byte is in the range 0x81 to 0xFE, inclusive, set
+ // gb18030 first to byte and return continue.
+ if (inRange(bite, 0x81, 0xFE)) {
+ gb18030_first = bite;
+ return null;
+ }
+
+ // 9. Return error.
+ return decoderError(fatal);
+ };
+ }
+
+ // 11.2.2 gb18030 encoder
+ /**
+ * @constructor
+ * @implements {Encoder}
+ * @param {{fatal: boolean}} options
+ * @param {boolean=} gbk_flag
+ */
+ function GB18030Encoder(options, gbk_flag) {
+ var fatal = options.fatal;
+ // gb18030's decoder has an associated gbk flag (initially unset).
+ /**
+ * @param {Stream} stream Input stream.
+ * @param {number} code_point Next code point read from the stream.
+ * @return {(number|!Array.<number>)} Byte(s) to emit.
+ */
+ this.handler = function(stream, code_point) {
+ // 1. If code point is end-of-stream, return finished.
+ if (code_point === end_of_stream)
+ return finished;
+
+ // 2. If code point is an ASCII code point, return a byte whose
+ // value is code point.
+ if (isASCIICodePoint(code_point))
+ return code_point;
+
+ // 3. If code point is U+E5E5, return error with code point.
+ if (code_point === 0xE5E5)
+ return encoderError(code_point);
+
+ // 4. If the gbk flag is set and code point is U+20AC, return
+ // byte 0x80.
+ if (gbk_flag && code_point === 0x20AC)
+ return 0x80;
+
+ // 5. Let pointer be the index pointer for code point in index
+ // gb18030.
+ var pointer = indexPointerFor(code_point, index('gb18030'));
+
+ // 6. If pointer is not null, run these substeps:
+ if (pointer !== null) {
+
+ // 1. Let lead be floor(pointer / 190) + 0x81.
+ var lead = floor(pointer / 190) + 0x81;
+
+ // 2. Let trail be pointer % 190.
+ var trail = pointer % 190;
+
+ // 3. Let offset be 0x40 if trail is less than 0x3F and 0x41 otherwise.
+ var offset = trail < 0x3F ? 0x40 : 0x41;
+
+ // 4. Return two bytes whose values are lead and trail + offset.
+ return [lead, trail + offset];
+ }
+
+ // 7. If gbk flag is set, return error with code point.
+ if (gbk_flag)
+ return encoderError(code_point);
+
+ // 8. Set pointer to the index gb18030 ranges pointer for code
+ // point.
+ pointer = indexGB18030RangesPointerFor(code_point);
+
+ // 9. Let byte1 be floor(pointer / 10 / 126 / 10).
+ var byte1 = floor(pointer / 10 / 126 / 10);
+
+ // 10. Set pointer to pointer − byte1 × 10 × 126 × 10.
+ pointer = pointer - byte1 * 10 * 126 * 10;
+
+ // 11. Let byte2 be floor(pointer / 10 / 126).
+ var byte2 = floor(pointer / 10 / 126);
+
+ // 12. Set pointer to pointer − byte2 × 10 × 126.
+ pointer = pointer - byte2 * 10 * 126;
+
+ // 13. Let byte3 be floor(pointer / 10).
+ var byte3 = floor(pointer / 10);
+
+ // 14. Let byte4 be pointer − byte3 × 10.
+ var byte4 = pointer - byte3 * 10;
+
+ // 15. Return four bytes whose values are byte1 + 0x81, byte2 +
+ // 0x30, byte3 + 0x81, byte4 + 0x30.
+ return [byte1 + 0x81,
+ byte2 + 0x30,
+ byte3 + 0x81,
+ byte4 + 0x30];
+ };
+ }
+
+ /** @param {{fatal: boolean}} options */
+ encoders['gb18030'] = function(options) {
+ return new GB18030Encoder(options);
+ };
+ /** @param {{fatal: boolean}} options */
+ decoders['gb18030'] = function(options) {
+ return new GB18030Decoder(options);
+ };
+
+
+ //
+ // 12. Legacy multi-byte Chinese (traditional) encodings
+ //
+
+ // 12.1 Big5
+
+ // 12.1.1 Big5 decoder
+ /**
+ * @constructor
+ * @implements {Decoder}
+ * @param {{fatal: boolean}} options
+ */
+ function Big5Decoder(options) {
+ var fatal = options.fatal;
+ // Big5's decoder has an associated Big5 lead (initially 0x00).
+ var /** @type {number} */ Big5_lead = 0x00;
+
+ /**
+ * @param {Stream} stream The stream of bytes being decoded.
+ * @param {number} bite The next byte read from the stream.
+ * @return {?(number|!Array.<number>)} The next code point(s)
+ * decoded, or null if not enough data exists in the input
+ * stream to decode a complete code point.
+ */
+ this.handler = function(stream, bite) {
+ // 1. If byte is end-of-stream and Big5 lead is not 0x00, set
+ // Big5 lead to 0x00 and return error.
+ if (bite === end_of_stream && Big5_lead !== 0x00) {
+ Big5_lead = 0x00;
+ return decoderError(fatal);
+ }
+
+ // 2. If byte is end-of-stream and Big5 lead is 0x00, return
+ // finished.
+ if (bite === end_of_stream && Big5_lead === 0x00)
+ return finished;
+
+ // 3. If Big5 lead is not 0x00, let lead be Big5 lead, let
+ // pointer be null, set Big5 lead to 0x00, and then run these
+ // substeps:
+ if (Big5_lead !== 0x00) {
+ var lead = Big5_lead;
+ var pointer = null;
+ Big5_lead = 0x00;
+
+ // 1. Let offset be 0x40 if byte is less than 0x7F and 0x62
+ // otherwise.
+ var offset = bite < 0x7F ? 0x40 : 0x62;
+
+ // 2. If byte is in the range 0x40 to 0x7E, inclusive, or 0xA1
+ // to 0xFE, inclusive, set pointer to (lead − 0x81) × 157 +
+ // (byte − offset).
+ if (inRange(bite, 0x40, 0x7E) || inRange(bite, 0xA1, 0xFE))
+ pointer = (lead - 0x81) * 157 + (bite - offset);
+
+ // 3. If there is a row in the table below whose first column
+ // is pointer, return the two code points listed in its second
+ // column
+ // Pointer | Code points
+ // --------+--------------
+ // 1133 | U+00CA U+0304
+ // 1135 | U+00CA U+030C
+ // 1164 | U+00EA U+0304
+ // 1166 | U+00EA U+030C
+ switch (pointer) {
+ case 1133: return [0x00CA, 0x0304];
+ case 1135: return [0x00CA, 0x030C];
+ case 1164: return [0x00EA, 0x0304];
+ case 1166: return [0x00EA, 0x030C];
+ }
+
+ // 4. Let code point be null if pointer is null and the index
+ // code point for pointer in index Big5 otherwise.
+ var code_point = (pointer === null) ? null :
+ indexCodePointFor(pointer, index('big5'));
+
+ // 5. If code point is null and byte is an ASCII byte, prepend
+ // byte to stream.
+ if (code_point === null && isASCIIByte(bite))
+ stream.prepend(bite);
+
+ // 6. If code point is null, return error.
+ if (code_point === null)
+ return decoderError(fatal);
+
+ // 7. Return a code point whose value is code point.
+ return code_point;
+ }
+
+ // 4. If byte is an ASCII byte, return a code point whose value
+ // is byte.
+ if (isASCIIByte(bite))
+ return bite;
+
+ // 5. If byte is in the range 0x81 to 0xFE, inclusive, set Big5
+ // lead to byte and return continue.
+ if (inRange(bite, 0x81, 0xFE)) {
+ Big5_lead = bite;
+ return null;
+ }
+
+ // 6. Return error.
+ return decoderError(fatal);
+ };
+ }
+
+ // 12.1.2 Big5 encoder
+ /**
+ * @constructor
+ * @implements {Encoder}
+ * @param {{fatal: boolean}} options
+ */
+ function Big5Encoder(options) {
+ var fatal = options.fatal;
+ /**
+ * @param {Stream} stream Input stream.
+ * @param {number} code_point Next code point read from the stream.
+ * @return {(number|!Array.<number>)} Byte(s) to emit.
+ */
+ this.handler = function(stream, code_point) {
+ // 1. If code point is end-of-stream, return finished.
+ if (code_point === end_of_stream)
+ return finished;
+
+ // 2. If code point is an ASCII code point, return a byte whose
+ // value is code point.
+ if (isASCIICodePoint(code_point))
+ return code_point;
+
+ // 3. Let pointer be the index Big5 pointer for code point.
+ var pointer = indexBig5PointerFor(code_point);
+
+ // 4. If pointer is null, return error with code point.
+ if (pointer === null)
+ return encoderError(code_point);
+
+ // 5. Let lead be floor(pointer / 157) + 0x81.
+ var lead = floor(pointer / 157) + 0x81;
+
+ // 6. If lead is less than 0xA1, return error with code point.
+ if (lead < 0xA1)
+ return encoderError(code_point);
+
+ // 7. Let trail be pointer % 157.
+ var trail = pointer % 157;
+
+ // 8. Let offset be 0x40 if trail is less than 0x3F and 0x62
+ // otherwise.
+ var offset = trail < 0x3F ? 0x40 : 0x62;
+
+ // Return two bytes whose values are lead and trail + offset.
+ return [lead, trail + offset];
+ };
+ }
+
+ /** @param {{fatal: boolean}} options */
+ encoders['Big5'] = function(options) {
+ return new Big5Encoder(options);
+ };
+ /** @param {{fatal: boolean}} options */
+ decoders['Big5'] = function(options) {
+ return new Big5Decoder(options);
+ };
+
+
+ //
+ // 13. Legacy multi-byte Japanese encodings
+ //
+
+ // 13.1 euc-jp
+
+ // 13.1.1 euc-jp decoder
+ /**
+ * @constructor
+ * @implements {Decoder}
+ * @param {{fatal: boolean}} options
+ */
+ function EUCJPDecoder(options) {
+ var fatal = options.fatal;
+
+ // euc-jp's decoder has an associated euc-jp jis0212 flag
+ // (initially unset) and euc-jp lead (initially 0x00).
+ var /** @type {boolean} */ eucjp_jis0212_flag = false,
+ /** @type {number} */ eucjp_lead = 0x00;
+
+ /**
+ * @param {Stream} stream The stream of bytes being decoded.
+ * @param {number} bite The next byte read from the stream.
+ * @return {?(number|!Array.<number>)} The next code point(s)
+ * decoded, or null if not enough data exists in the input
+ * stream to decode a complete code point.
+ */
+ this.handler = function(stream, bite) {
+ // 1. If byte is end-of-stream and euc-jp lead is not 0x00, set
+ // euc-jp lead to 0x00, and return error.
+ if (bite === end_of_stream && eucjp_lead !== 0x00) {
+ eucjp_lead = 0x00;
+ return decoderError(fatal);
+ }
+
+ // 2. If byte is end-of-stream and euc-jp lead is 0x00, return
+ // finished.
+ if (bite === end_of_stream && eucjp_lead === 0x00)
+ return finished;
+
+ // 3. If euc-jp lead is 0x8E and byte is in the range 0xA1 to
+ // 0xDF, inclusive, set euc-jp lead to 0x00 and return a code
+ // point whose value is 0xFF61 − 0xA1 + byte.
+ if (eucjp_lead === 0x8E && inRange(bite, 0xA1, 0xDF)) {
+ eucjp_lead = 0x00;
+ return 0xFF61 - 0xA1 + bite;
+ }
+
+ // 4. If euc-jp lead is 0x8F and byte is in the range 0xA1 to
+ // 0xFE, inclusive, set the euc-jp jis0212 flag, set euc-jp lead
+ // to byte, and return continue.
+ if (eucjp_lead === 0x8F && inRange(bite, 0xA1, 0xFE)) {
+ eucjp_jis0212_flag = true;
+ eucjp_lead = bite;
+ return null;
+ }
+
+ // 5. If euc-jp lead is not 0x00, let lead be euc-jp lead, set
+ // euc-jp lead to 0x00, and run these substeps:
+ if (eucjp_lead !== 0x00) {
+ var lead = eucjp_lead;
+ eucjp_lead = 0x00;
+
+ // 1. Let code point be null.
+ var code_point = null;
+
+ // 2. If lead and byte are both in the range 0xA1 to 0xFE,
+ // inclusive, set code point to the index code point for (lead
+ // − 0xA1) × 94 + byte − 0xA1 in index jis0208 if the euc-jp
+ // jis0212 flag is unset and in index jis0212 otherwise.
+ if (inRange(lead, 0xA1, 0xFE) && inRange(bite, 0xA1, 0xFE)) {
+ code_point = indexCodePointFor(
+ (lead - 0xA1) * 94 + (bite - 0xA1),
+ index(!eucjp_jis0212_flag ? 'jis0208' : 'jis0212'));
+ }
+
+ // 3. Unset the euc-jp jis0212 flag.
+ eucjp_jis0212_flag = false;
+
+ // 4. If byte is not in the range 0xA1 to 0xFE, inclusive,
+ // prepend byte to stream.
+ if (!inRange(bite, 0xA1, 0xFE))
+ stream.prepend(bite);
+
+ // 5. If code point is null, return error.
+ if (code_point === null)
+ return decoderError(fatal);
+
+ // 6. Return a code point whose value is code point.
+ return code_point;
+ }
+
+ // 6. If byte is an ASCII byte, return a code point whose value
+ // is byte.
+ if (isASCIIByte(bite))
+ return bite;
+
+ // 7. If byte is 0x8E, 0x8F, or in the range 0xA1 to 0xFE,
+ // inclusive, set euc-jp lead to byte and return continue.
+ if (bite === 0x8E || bite === 0x8F || inRange(bite, 0xA1, 0xFE)) {
+ eucjp_lead = bite;
+ return null;
+ }
+
+ // 8. Return error.
+ return decoderError(fatal);
+ };
+ }
+
+ // 13.1.2 euc-jp encoder
+ /**
+ * @constructor
+ * @implements {Encoder}
+ * @param {{fatal: boolean}} options
+ */
+ function EUCJPEncoder(options) {
+ var fatal = options.fatal;
+ /**
+ * @param {Stream} stream Input stream.
+ * @param {number} code_point Next code point read from the stream.
+ * @return {(number|!Array.<number>)} Byte(s) to emit.
+ */
+ this.handler = function(stream, code_point) {
+ // 1. If code point is end-of-stream, return finished.
+ if (code_point === end_of_stream)
+ return finished;
+
+ // 2. If code point is an ASCII code point, return a byte whose
+ // value is code point.
+ if (isASCIICodePoint(code_point))
+ return code_point;
+
+ // 3. If code point is U+00A5, return byte 0x5C.
+ if (code_point === 0x00A5)
+ return 0x5C;
+
+ // 4. If code point is U+203E, return byte 0x7E.
+ if (code_point === 0x203E)
+ return 0x7E;
+
+ // 5. If code point is in the range U+FF61 to U+FF9F, inclusive,
+ // return two bytes whose values are 0x8E and code point −
+ // 0xFF61 + 0xA1.
+ if (inRange(code_point, 0xFF61, 0xFF9F))
+ return [0x8E, code_point - 0xFF61 + 0xA1];
+
+ // 6. If code point is U+2212, set it to U+FF0D.
+ if (code_point === 0x2212)
+ code_point = 0xFF0D;
+
+ // 7. Let pointer be the index pointer for code point in index
+ // jis0208.
+ var pointer = indexPointerFor(code_point, index('jis0208'));
+
+ // 8. If pointer is null, return error with code point.
+ if (pointer === null)
+ return encoderError(code_point);
+
+ // 9. Let lead be floor(pointer / 94) + 0xA1.
+ var lead = floor(pointer / 94) + 0xA1;
+
+ // 10. Let trail be pointer % 94 + 0xA1.
+ var trail = pointer % 94 + 0xA1;
+
+ // 11. Return two bytes whose values are lead and trail.
+ return [lead, trail];
+ };
+ }
+
+ /** @param {{fatal: boolean}} options */
+ encoders['EUC-JP'] = function(options) {
+ return new EUCJPEncoder(options);
+ };
+ /** @param {{fatal: boolean}} options */
+ decoders['EUC-JP'] = function(options) {
+ return new EUCJPDecoder(options);
+ };
+
+ // 13.2 iso-2022-jp
+
+ // 13.2.1 iso-2022-jp decoder
+ /**
+ * @constructor
+ * @implements {Decoder}
+ * @param {{fatal: boolean}} options
+ */
+ function ISO2022JPDecoder(options) {
+ var fatal = options.fatal;
+ /** @enum */
+ var states = {
+ ASCII: 0,
+ Roman: 1,
+ Katakana: 2,
+ LeadByte: 3,
+ TrailByte: 4,
+ EscapeStart: 5,
+ Escape: 6
+ };
+ // iso-2022-jp's decoder has an associated iso-2022-jp decoder
+ // state (initially ASCII), iso-2022-jp decoder output state
+ // (initially ASCII), iso-2022-jp lead (initially 0x00), and
+ // iso-2022-jp output flag (initially unset).
+ var /** @type {number} */ iso2022jp_decoder_state = states.ASCII,
+ /** @type {number} */ iso2022jp_decoder_output_state = states.ASCII,
+ /** @type {number} */ iso2022jp_lead = 0x00,
+ /** @type {boolean} */ iso2022jp_output_flag = false;
+ /**
+ * @param {Stream} stream The stream of bytes being decoded.
+ * @param {number} bite The next byte read from the stream.
+ * @return {?(number|!Array.<number>)} The next code point(s)
+ * decoded, or null if not enough data exists in the input
+ * stream to decode a complete code point.
+ */
+ this.handler = function(stream, bite) {
+ // switching on iso-2022-jp decoder state:
+ switch (iso2022jp_decoder_state) {
+ default:
+ case states.ASCII:
+ // ASCII
+ // Based on byte:
+
+ // 0x1B
+ if (bite === 0x1B) {
+ // Set iso-2022-jp decoder state to escape start and return
+ // continue.
+ iso2022jp_decoder_state = states.EscapeStart;
+ return null;
+ }
+
+ // 0x00 to 0x7F, excluding 0x0E, 0x0F, and 0x1B
+ if (inRange(bite, 0x00, 0x7F) && bite !== 0x0E
+ && bite !== 0x0F && bite !== 0x1B) {
+ // Unset the iso-2022-jp output flag and return a code point
+ // whose value is byte.
+ iso2022jp_output_flag = false;
+ return bite;
+ }
+
+ // end-of-stream
+ if (bite === end_of_stream) {
+ // Return finished.
+ return finished;
+ }
+
+ // Otherwise
+ // Unset the iso-2022-jp output flag and return error.
+ iso2022jp_output_flag = false;
+ return decoderError(fatal);
+
+ case states.Roman:
+ // Roman
+ // Based on byte:
+
+ // 0x1B
+ if (bite === 0x1B) {
+ // Set iso-2022-jp decoder state to escape start and return
+ // continue.
+ iso2022jp_decoder_state = states.EscapeStart;
+ return null;
+ }
+
+ // 0x5C
+ if (bite === 0x5C) {
+ // Unset the iso-2022-jp output flag and return code point
+ // U+00A5.
+ iso2022jp_output_flag = false;
+ return 0x00A5;
+ }
+
+ // 0x7E
+ if (bite === 0x7E) {
+ // Unset the iso-2022-jp output flag and return code point
+ // U+203E.
+ iso2022jp_output_flag = false;
+ return 0x203E;
+ }
+
+ // 0x00 to 0x7F, excluding 0x0E, 0x0F, 0x1B, 0x5C, and 0x7E
+ if (inRange(bite, 0x00, 0x7F) && bite !== 0x0E && bite !== 0x0F
+ && bite !== 0x1B && bite !== 0x5C && bite !== 0x7E) {
+ // Unset the iso-2022-jp output flag and return a code point
+ // whose value is byte.
+ iso2022jp_output_flag = false;
+ return bite;
+ }
+
+ // end-of-stream
+ if (bite === end_of_stream) {
+ // Return finished.
+ return finished;
+ }
+
+ // Otherwise
+ // Unset the iso-2022-jp output flag and return error.
+ iso2022jp_output_flag = false;
+ return decoderError(fatal);
+
+ case states.Katakana:
+ // Katakana
+ // Based on byte:
+
+ // 0x1B
+ if (bite === 0x1B) {
+ // Set iso-2022-jp decoder state to escape start and return
+ // continue.
+ iso2022jp_decoder_state = states.EscapeStart;
+ return null;
+ }
+
+ // 0x21 to 0x5F
+ if (inRange(bite, 0x21, 0x5F)) {
+ // Unset the iso-2022-jp output flag and return a code point
+ // whose value is 0xFF61 − 0x21 + byte.
+ iso2022jp_output_flag = false;
+ return 0xFF61 - 0x21 + bite;
+ }
+
+ // end-of-stream
+ if (bite === end_of_stream) {
+ // Return finished.
+ return finished;
+ }
+
+ // Otherwise
+ // Unset the iso-2022-jp output flag and return error.
+ iso2022jp_output_flag = false;
+ return decoderError(fatal);
+
+ case states.LeadByte:
+ // Lead byte
+ // Based on byte:
+
+ // 0x1B
+ if (bite === 0x1B) {
+ // Set iso-2022-jp decoder state to escape start and return
+ // continue.
+ iso2022jp_decoder_state = states.EscapeStart;
+ return null;
+ }
+
+ // 0x21 to 0x7E
+ if (inRange(bite, 0x21, 0x7E)) {
+ // Unset the iso-2022-jp output flag, set iso-2022-jp lead
+ // to byte, iso-2022-jp decoder state to trail byte, and
+ // return continue.
+ iso2022jp_output_flag = false;
+ iso2022jp_lead = bite;
+ iso2022jp_decoder_state = states.TrailByte;
+ return null;
+ }
+
+ // end-of-stream
+ if (bite === end_of_stream) {
+ // Return finished.
+ return finished;
+ }
+
+ // Otherwise
+ // Unset the iso-2022-jp output flag and return error.
+ iso2022jp_output_flag = false;
+ return decoderError(fatal);
+
+ case states.TrailByte:
+ // Trail byte
+ // Based on byte:
+
+ // 0x1B
+ if (bite === 0x1B) {
+ // Set iso-2022-jp decoder state to escape start and return
+ // continue.
+ iso2022jp_decoder_state = states.EscapeStart;
+ return decoderError(fatal);
+ }
+
+ // 0x21 to 0x7E
+ if (inRange(bite, 0x21, 0x7E)) {
+ // 1. Set the iso-2022-jp decoder state to lead byte.
+ iso2022jp_decoder_state = states.LeadByte;
+
+ // 2. Let pointer be (iso-2022-jp lead − 0x21) × 94 + byte − 0x21.
+ var pointer = (iso2022jp_lead - 0x21) * 94 + bite - 0x21;
+
+ // 3. Let code point be the index code point for pointer in
+ // index jis0208.
+ var code_point = indexCodePointFor(pointer, index('jis0208'));
+
+ // 4. If code point is null, return error.
+ if (code_point === null)
+ return decoderError(fatal);
+
+ // 5. Return a code point whose value is code point.
+ return code_point;
+ }
+
+ // end-of-stream
+ if (bite === end_of_stream) {
+ // Set the iso-2022-jp decoder state to lead byte, prepend
+ // byte to stream, and return error.
+ iso2022jp_decoder_state = states.LeadByte;
+ stream.prepend(bite);
+ return decoderError(fatal);
+ }
+
+ // Otherwise
+ // Set iso-2022-jp decoder state to lead byte and return
+ // error.
+ iso2022jp_decoder_state = states.LeadByte;
+ return decoderError(fatal);
+
+ case states.EscapeStart:
+ // Escape start
+
+ // 1. If byte is either 0x24 or 0x28, set iso-2022-jp lead to
+ // byte, iso-2022-jp decoder state to escape, and return
+ // continue.
+ if (bite === 0x24 || bite === 0x28) {
+ iso2022jp_lead = bite;
+ iso2022jp_decoder_state = states.Escape;
+ return null;
+ }
+
+ // 2. Prepend byte to stream.
+ stream.prepend(bite);
+
+ // 3. Unset the iso-2022-jp output flag, set iso-2022-jp
+ // decoder state to iso-2022-jp decoder output state, and
+ // return error.
+ iso2022jp_output_flag = false;
+ iso2022jp_decoder_state = iso2022jp_decoder_output_state;
+ return decoderError(fatal);
+
+ case states.Escape:
+ // Escape
+
+ // 1. Let lead be iso-2022-jp lead and set iso-2022-jp lead to
+ // 0x00.
+ var lead = iso2022jp_lead;
+ iso2022jp_lead = 0x00;
+
+ // 2. Let state be null.
+ var state = null;
+
+ // 3. If lead is 0x28 and byte is 0x42, set state to ASCII.
+ if (lead === 0x28 && bite === 0x42)
+ state = states.ASCII;
+
+ // 4. If lead is 0x28 and byte is 0x4A, set state to Roman.
+ if (lead === 0x28 && bite === 0x4A)
+ state = states.Roman;
+
+ // 5. If lead is 0x28 and byte is 0x49, set state to Katakana.
+ if (lead === 0x28 && bite === 0x49)
+ state = states.Katakana;
+
+ // 6. If lead is 0x24 and byte is either 0x40 or 0x42, set
+ // state to lead byte.
+ if (lead === 0x24 && (bite === 0x40 || bite === 0x42))
+ state = states.LeadByte;
+
+ // 7. If state is non-null, run these substeps:
+ if (state !== null) {
+ // 1. Set iso-2022-jp decoder state and iso-2022-jp decoder
+ // output state to states.
+ iso2022jp_decoder_state = iso2022jp_decoder_state = state;
+
+ // 2. Let output flag be the iso-2022-jp output flag.
+ var output_flag = iso2022jp_output_flag;
+
+ // 3. Set the iso-2022-jp output flag.
+ iso2022jp_output_flag = true;
+
+ // 4. Return continue, if output flag is unset, and error
+ // otherwise.
+ return !output_flag ? null : decoderError(fatal);
+ }
+
+ // 8. Prepend lead and byte to stream.
+ stream.prepend([lead, bite]);
+
+ // 9. Unset the iso-2022-jp output flag, set iso-2022-jp
+ // decoder state to iso-2022-jp decoder output state and
+ // return error.
+ iso2022jp_output_flag = false;
+ iso2022jp_decoder_state = iso2022jp_decoder_output_state;
+ return decoderError(fatal);
+ }
+ };
+ }
+
+ // 13.2.2 iso-2022-jp encoder
+ /**
+ * @constructor
+ * @implements {Encoder}
+ * @param {{fatal: boolean}} options
+ */
+ function ISO2022JPEncoder(options) {
+ var fatal = options.fatal;
+ // iso-2022-jp's encoder has an associated iso-2022-jp encoder
+ // state which is one of ASCII, Roman, and jis0208 (initially
+ // ASCII).
+ /** @enum */
+ var states = {
+ ASCII: 0,
+ Roman: 1,
+ jis0208: 2
+ };
+ var /** @type {number} */ iso2022jp_state = states.ASCII;
+ /**
+ * @param {Stream} stream Input stream.
+ * @param {number} code_point Next code point read from the stream.
+ * @return {(number|!Array.<number>)} Byte(s) to emit.
+ */
+ this.handler = function(stream, code_point) {
+ // 1. If code point is end-of-stream and iso-2022-jp encoder
+ // state is not ASCII, prepend code point to stream, set
+ // iso-2022-jp encoder state to ASCII, and return three bytes
+ // 0x1B 0x28 0x42.
+ if (code_point === end_of_stream &&
+ iso2022jp_state !== states.ASCII) {
+ stream.prepend(code_point);
+ iso2022jp_state = states.ASCII;
+ return [0x1B, 0x28, 0x42];
+ }
+
+ // 2. If code point is end-of-stream and iso-2022-jp encoder
+ // state is ASCII, return finished.
+ if (code_point === end_of_stream && iso2022jp_state === states.ASCII)
+ return finished;
+
+ // 3. If ISO-2022-JP encoder state is ASCII or Roman, and code
+ // point is U+000E, U+000F, or U+001B, return error with U+FFFD.
+ if ((iso2022jp_state === states.ASCII ||
+ iso2022jp_state === states.Roman) &&
+ (code_point === 0x000E || code_point === 0x000F ||
+ code_point === 0x001B)) {
+ return encoderError(0xFFFD);
+ }
+
+ // 4. If iso-2022-jp encoder state is ASCII and code point is an
+ // ASCII code point, return a byte whose value is code point.
+ if (iso2022jp_state === states.ASCII &&
+ isASCIICodePoint(code_point))
+ return code_point;
+
+ // 5. If iso-2022-jp encoder state is Roman and code point is an
+ // ASCII code point, excluding U+005C and U+007E, or is U+00A5
+ // or U+203E, run these substeps:
+ if (iso2022jp_state === states.Roman &&
+ ((isASCIICodePoint(code_point) &&
+ code_point !== 0x005C && code_point !== 0x007E) ||
+ (code_point == 0x00A5 || code_point == 0x203E))) {
+
+ // 1. If code point is an ASCII code point, return a byte
+ // whose value is code point.
+ if (isASCIICodePoint(code_point))
+ return code_point;
+
+ // 2. If code point is U+00A5, return byte 0x5C.
+ if (code_point === 0x00A5)
+ return 0x5C;
+
+ // 3. If code point is U+203E, return byte 0x7E.
+ if (code_point === 0x203E)
+ return 0x7E;
+ }
+
+ // 6. If code point is an ASCII code point, and iso-2022-jp
+ // encoder state is not ASCII, prepend code point to stream, set
+ // iso-2022-jp encoder state to ASCII, and return three bytes
+ // 0x1B 0x28 0x42.
+ if (isASCIICodePoint(code_point) &&
+ iso2022jp_state !== states.ASCII) {
+ stream.prepend(code_point);
+ iso2022jp_state = states.ASCII;
+ return [0x1B, 0x28, 0x42];
+ }
+
+ // 7. If code point is either U+00A5 or U+203E, and iso-2022-jp
+ // encoder state is not Roman, prepend code point to stream, set
+ // iso-2022-jp encoder state to Roman, and return three bytes
+ // 0x1B 0x28 0x4A.
+ if ((code_point === 0x00A5 || code_point === 0x203E) &&
+ iso2022jp_state !== states.Roman) {
+ stream.prepend(code_point);
+ iso2022jp_state = states.Roman;
+ return [0x1B, 0x28, 0x4A];
+ }
+
+ // 8. If code point is U+2212, set it to U+FF0D.
+ if (code_point === 0x2212)
+ code_point = 0xFF0D;
+
+ // 9. Let pointer be the index pointer for code point in index
+ // jis0208.
+ var pointer = indexPointerFor(code_point, index('jis0208'));
+
+ // 10. If pointer is null, return error with code point.
+ if (pointer === null)
+ return encoderError(code_point);
+
+ // 11. If iso-2022-jp encoder state is not jis0208, prepend code
+ // point to stream, set iso-2022-jp encoder state to jis0208,
+ // and return three bytes 0x1B 0x24 0x42.
+ if (iso2022jp_state !== states.jis0208) {
+ stream.prepend(code_point);
+ iso2022jp_state = states.jis0208;
+ return [0x1B, 0x24, 0x42];
+ }
+
+ // 12. Let lead be floor(pointer / 94) + 0x21.
+ var lead = floor(pointer / 94) + 0x21;
+
+ // 13. Let trail be pointer % 94 + 0x21.
+ var trail = pointer % 94 + 0x21;
+
+ // 14. Return two bytes whose values are lead and trail.
+ return [lead, trail];
+ };
+ }
+
+ /** @param {{fatal: boolean}} options */
+ encoders['ISO-2022-JP'] = function(options) {
+ return new ISO2022JPEncoder(options);
+ };
+ /** @param {{fatal: boolean}} options */
+ decoders['ISO-2022-JP'] = function(options) {
+ return new ISO2022JPDecoder(options);
+ };
+
+ // 13.3 Shift_JIS
+
+ // 13.3.1 Shift_JIS decoder
+ /**
+ * @constructor
+ * @implements {Decoder}
+ * @param {{fatal: boolean}} options
+ */
+ function ShiftJISDecoder(options) {
+ var fatal = options.fatal;
+ // Shift_JIS's decoder has an associated Shift_JIS lead (initially
+ // 0x00).
+ var /** @type {number} */ Shift_JIS_lead = 0x00;
+ /**
+ * @param {Stream} stream The stream of bytes being decoded.
+ * @param {number} bite The next byte read from the stream.
+ * @return {?(number|!Array.<number>)} The next code point(s)
+ * decoded, or null if not enough data exists in the input
+ * stream to decode a complete code point.
+ */
+ this.handler = function(stream, bite) {
+ // 1. If byte is end-of-stream and Shift_JIS lead is not 0x00,
+ // set Shift_JIS lead to 0x00 and return error.
+ if (bite === end_of_stream && Shift_JIS_lead !== 0x00) {
+ Shift_JIS_lead = 0x00;
+ return decoderError(fatal);
+ }
+
+ // 2. If byte is end-of-stream and Shift_JIS lead is 0x00,
+ // return finished.
+ if (bite === end_of_stream && Shift_JIS_lead === 0x00)
+ return finished;
+
+ // 3. If Shift_JIS lead is not 0x00, let lead be Shift_JIS lead,
+ // let pointer be null, set Shift_JIS lead to 0x00, and then run
+ // these substeps:
+ if (Shift_JIS_lead !== 0x00) {
+ var lead = Shift_JIS_lead;
+ var pointer = null;
+ Shift_JIS_lead = 0x00;
+
+ // 1. Let offset be 0x40, if byte is less than 0x7F, and 0x41
+ // otherwise.
+ var offset = (bite < 0x7F) ? 0x40 : 0x41;
+
+ // 2. Let lead offset be 0x81, if lead is less than 0xA0, and
+ // 0xC1 otherwise.
+ var lead_offset = (lead < 0xA0) ? 0x81 : 0xC1;
+
+ // 3. If byte is in the range 0x40 to 0x7E, inclusive, or 0x80
+ // to 0xFC, inclusive, set pointer to (lead − lead offset) ×
+ // 188 + byte − offset.
+ if (inRange(bite, 0x40, 0x7E) || inRange(bite, 0x80, 0xFC))
+ pointer = (lead - lead_offset) * 188 + bite - offset;
+
+ // 4. If pointer is in the range 8836 to 10715, inclusive,
+ // return a code point whose value is 0xE000 − 8836 + pointer.
+ if (inRange(pointer, 8836, 10715))
+ return 0xE000 - 8836 + pointer;
+
+ // 5. Let code point be null, if pointer is null, and the
+ // index code point for pointer in index jis0208 otherwise.
+ var code_point = (pointer === null) ? null :
+ indexCodePointFor(pointer, index('jis0208'));
+
+ // 6. If code point is null and byte is an ASCII byte, prepend
+ // byte to stream.
+ if (code_point === null && isASCIIByte(bite))
+ stream.prepend(bite);
+
+ // 7. If code point is null, return error.
+ if (code_point === null)
+ return decoderError(fatal);
+
+ // 8. Return a code point whose value is code point.
+ return code_point;
+ }
+
+ // 4. If byte is an ASCII byte or 0x80, return a code point
+ // whose value is byte.
+ if (isASCIIByte(bite) || bite === 0x80)
+ return bite;
+
+ // 5. If byte is in the range 0xA1 to 0xDF, inclusive, return a
+ // code point whose value is 0xFF61 − 0xA1 + byte.
+ if (inRange(bite, 0xA1, 0xDF))
+ return 0xFF61 - 0xA1 + bite;
+
+ // 6. If byte is in the range 0x81 to 0x9F, inclusive, or 0xE0
+ // to 0xFC, inclusive, set Shift_JIS lead to byte and return
+ // continue.
+ if (inRange(bite, 0x81, 0x9F) || inRange(bite, 0xE0, 0xFC)) {
+ Shift_JIS_lead = bite;
+ return null;
+ }
+
+ // 7. Return error.
+ return decoderError(fatal);
+ };
+ }
+
+ // 13.3.2 Shift_JIS encoder
+ /**
+ * @constructor
+ * @implements {Encoder}
+ * @param {{fatal: boolean}} options
+ */
+ function ShiftJISEncoder(options) {
+ var fatal = options.fatal;
+ /**
+ * @param {Stream} stream Input stream.
+ * @param {number} code_point Next code point read from the stream.
+ * @return {(number|!Array.<number>)} Byte(s) to emit.
+ */
+ this.handler = function(stream, code_point) {
+ // 1. If code point is end-of-stream, return finished.
+ if (code_point === end_of_stream)
+ return finished;
+
+ // 2. If code point is an ASCII code point or U+0080, return a
+ // byte whose value is code point.
+ if (isASCIICodePoint(code_point) || code_point === 0x0080)
+ return code_point;
+
+ // 3. If code point is U+00A5, return byte 0x5C.
+ if (code_point === 0x00A5)
+ return 0x5C;
+
+ // 4. If code point is U+203E, return byte 0x7E.
+ if (code_point === 0x203E)
+ return 0x7E;
+
+ // 5. If code point is in the range U+FF61 to U+FF9F, inclusive,
+ // return a byte whose value is code point − 0xFF61 + 0xA1.
+ if (inRange(code_point, 0xFF61, 0xFF9F))
+ return code_point - 0xFF61 + 0xA1;
+
+ // 6. If code point is U+2212, set it to U+FF0D.
+ if (code_point === 0x2212)
+ code_point = 0xFF0D;
+
+ // 7. Let pointer be the index Shift_JIS pointer for code point.
+ var pointer = indexShiftJISPointerFor(code_point);
+
+ // 8. If pointer is null, return error with code point.
+ if (pointer === null)
+ return encoderError(code_point);
+
+ // 9. Let lead be floor(pointer / 188).
+ var lead = floor(pointer / 188);
+
+ // 10. Let lead offset be 0x81, if lead is less than 0x1F, and
+ // 0xC1 otherwise.
+ var lead_offset = (lead < 0x1F) ? 0x81 : 0xC1;
+
+ // 11. Let trail be pointer % 188.
+ var trail = pointer % 188;
+
+ // 12. Let offset be 0x40, if trail is less than 0x3F, and 0x41
+ // otherwise.
+ var offset = (trail < 0x3F) ? 0x40 : 0x41;
+
+ // 13. Return two bytes whose values are lead + lead offset and
+ // trail + offset.
+ return [lead + lead_offset, trail + offset];
+ };
+ }
+
+ /** @param {{fatal: boolean}} options */
+ encoders['Shift_JIS'] = function(options) {
+ return new ShiftJISEncoder(options);
+ };
+ /** @param {{fatal: boolean}} options */
+ decoders['Shift_JIS'] = function(options) {
+ return new ShiftJISDecoder(options);
+ };
+
+ //
+ // 14. Legacy multi-byte Korean encodings
+ //
+
+ // 14.1 euc-kr
+
+ // 14.1.1 euc-kr decoder
+ /**
+ * @constructor
+ * @implements {Decoder}
+ * @param {{fatal: boolean}} options
+ */
+ function EUCKRDecoder(options) {
+ var fatal = options.fatal;
+
+ // euc-kr's decoder has an associated euc-kr lead (initially 0x00).
+ var /** @type {number} */ euckr_lead = 0x00;
+ /**
+ * @param {Stream} stream The stream of bytes being decoded.
+ * @param {number} bite The next byte read from the stream.
+ * @return {?(number|!Array.<number>)} The next code point(s)
+ * decoded, or null if not enough data exists in the input
+ * stream to decode a complete code point.
+ */
+ this.handler = function(stream, bite) {
+ // 1. If byte is end-of-stream and euc-kr lead is not 0x00, set
+ // euc-kr lead to 0x00 and return error.
+ if (bite === end_of_stream && euckr_lead !== 0) {
+ euckr_lead = 0x00;
+ return decoderError(fatal);
+ }
+
+ // 2. If byte is end-of-stream and euc-kr lead is 0x00, return
+ // finished.
+ if (bite === end_of_stream && euckr_lead === 0)
+ return finished;
+
+ // 3. If euc-kr lead is not 0x00, let lead be euc-kr lead, let
+ // pointer be null, set euc-kr lead to 0x00, and then run these
+ // substeps:
+ if (euckr_lead !== 0x00) {
+ var lead = euckr_lead;
+ var pointer = null;
+ euckr_lead = 0x00;
+
+ // 1. If byte is in the range 0x41 to 0xFE, inclusive, set
+ // pointer to (lead − 0x81) × 190 + (byte − 0x41).
+ if (inRange(bite, 0x41, 0xFE))
+ pointer = (lead - 0x81) * 190 + (bite - 0x41);
+
+ // 2. Let code point be null, if pointer is null, and the
+ // index code point for pointer in index euc-kr otherwise.
+ var code_point = (pointer === null)
+ ? null : indexCodePointFor(pointer, index('euc-kr'));
+
+ // 3. If code point is null and byte is an ASCII byte, prepend
+ // byte to stream.
+ if (pointer === null && isASCIIByte(bite))
+ stream.prepend(bite);
+
+ // 4. If code point is null, return error.
+ if (code_point === null)
+ return decoderError(fatal);
+
+ // 5. Return a code point whose value is code point.
+ return code_point;
+ }
+
+ // 4. If byte is an ASCII byte, return a code point whose value
+ // is byte.
+ if (isASCIIByte(bite))
+ return bite;
+
+ // 5. If byte is in the range 0x81 to 0xFE, inclusive, set
+ // euc-kr lead to byte and return continue.
+ if (inRange(bite, 0x81, 0xFE)) {
+ euckr_lead = bite;
+ return null;
+ }
+
+ // 6. Return error.
+ return decoderError(fatal);
+ };
+ }
+
+ // 14.1.2 euc-kr encoder
+ /**
+ * @constructor
+ * @implements {Encoder}
+ * @param {{fatal: boolean}} options
+ */
+ function EUCKREncoder(options) {
+ var fatal = options.fatal;
+ /**
+ * @param {Stream} stream Input stream.
+ * @param {number} code_point Next code point read from the stream.
+ * @return {(number|!Array.<number>)} Byte(s) to emit.
+ */
+ this.handler = function(stream, code_point) {
+ // 1. If code point is end-of-stream, return finished.
+ if (code_point === end_of_stream)
+ return finished;
+
+ // 2. If code point is an ASCII code point, return a byte whose
+ // value is code point.
+ if (isASCIICodePoint(code_point))
+ return code_point;
+
+ // 3. Let pointer be the index pointer for code point in index
+ // euc-kr.
+ var pointer = indexPointerFor(code_point, index('euc-kr'));
+
+ // 4. If pointer is null, return error with code point.
+ if (pointer === null)
+ return encoderError(code_point);
+
+ // 5. Let lead be floor(pointer / 190) + 0x81.
+ var lead = floor(pointer / 190) + 0x81;
+
+ // 6. Let trail be pointer % 190 + 0x41.
+ var trail = (pointer % 190) + 0x41;
+
+ // 7. Return two bytes whose values are lead and trail.
+ return [lead, trail];
+ };
+ }
+
+ /** @param {{fatal: boolean}} options */
+ encoders['EUC-KR'] = function(options) {
+ return new EUCKREncoder(options);
+ };
+ /** @param {{fatal: boolean}} options */
+ decoders['EUC-KR'] = function(options) {
+ return new EUCKRDecoder(options);
+ };
+
+
+ //
+ // 15. Legacy miscellaneous encodings
+ //
+
+ // 15.1 replacement
+
+ // Not needed - API throws RangeError
+
+ // 15.2 Common infrastructure for utf-16be and utf-16le
+
+ /**
+ * @param {number} code_unit
+ * @param {boolean} utf16be
+ * @return {!Array.<number>} bytes
+ */
+ function convertCodeUnitToBytes(code_unit, utf16be) {
+ // 1. Let byte1 be code unit >> 8.
+ var byte1 = code_unit >> 8;
+
+ // 2. Let byte2 be code unit & 0x00FF.
+ var byte2 = code_unit & 0x00FF;
+
+ // 3. Then return the bytes in order:
+ // utf-16be flag is set: byte1, then byte2.
+ if (utf16be)
+ return [byte1, byte2];
+ // utf-16be flag is unset: byte2, then byte1.
+ return [byte2, byte1];
+ }
+
+ // 15.2.1 shared utf-16 decoder
+ /**
+ * @constructor
+ * @implements {Decoder}
+ * @param {boolean} utf16_be True if big-endian, false if little-endian.
+ * @param {{fatal: boolean}} options
+ */
+ function UTF16Decoder(utf16_be, options) {
+ var fatal = options.fatal;
+ var /** @type {?number} */ utf16_lead_byte = null,
+ /** @type {?number} */ utf16_lead_surrogate = null;
+ /**
+ * @param {Stream} stream The stream of bytes being decoded.
+ * @param {number} bite The next byte read from the stream.
+ * @return {?(number|!Array.<number>)} The next code point(s)
+ * decoded, or null if not enough data exists in the input
+ * stream to decode a complete code point.
+ */
+ this.handler = function(stream, bite) {
+ // 1. If byte is end-of-stream and either utf-16 lead byte or
+ // utf-16 lead surrogate is not null, set utf-16 lead byte and
+ // utf-16 lead surrogate to null, and return error.
+ if (bite === end_of_stream && (utf16_lead_byte !== null ||
+ utf16_lead_surrogate !== null)) {
+ return decoderError(fatal);
+ }
+
+ // 2. If byte is end-of-stream and utf-16 lead byte and utf-16
+ // lead surrogate are null, return finished.
+ if (bite === end_of_stream && utf16_lead_byte === null &&
+ utf16_lead_surrogate === null) {
+ return finished;
+ }
+
+ // 3. If utf-16 lead byte is null, set utf-16 lead byte to byte
+ // and return continue.
+ if (utf16_lead_byte === null) {
+ utf16_lead_byte = bite;
+ return null;
+ }
+
+ // 4. Let code unit be the result of:
+ var code_unit;
+ if (utf16_be) {
+ // utf-16be decoder flag is set
+ // (utf-16 lead byte << 8) + byte.
+ code_unit = (utf16_lead_byte << 8) + bite;
+ } else {
+ // utf-16be decoder flag is unset
+ // (byte << 8) + utf-16 lead byte.
+ code_unit = (bite << 8) + utf16_lead_byte;
+ }
+ // Then set utf-16 lead byte to null.
+ utf16_lead_byte = null;
+
+ // 5. If utf-16 lead surrogate is not null, let lead surrogate
+ // be utf-16 lead surrogate, set utf-16 lead surrogate to null,
+ // and then run these substeps:
+ if (utf16_lead_surrogate !== null) {
+ var lead_surrogate = utf16_lead_surrogate;
+ utf16_lead_surrogate = null;
+
+ // 1. If code unit is in the range U+DC00 to U+DFFF,
+ // inclusive, return a code point whose value is 0x10000 +
+ // ((lead surrogate − 0xD800) << 10) + (code unit − 0xDC00).
+ if (inRange(code_unit, 0xDC00, 0xDFFF)) {
+ return 0x10000 + (lead_surrogate - 0xD800) * 0x400 +
+ (code_unit - 0xDC00);
+ }
+
+ // 2. Prepend the sequence resulting of converting code unit
+ // to bytes using utf-16be decoder flag to stream and return
+ // error.
+ stream.prepend(convertCodeUnitToBytes(code_unit, utf16_be));
+ return decoderError(fatal);
+ }
+
+ // 6. If code unit is in the range U+D800 to U+DBFF, inclusive,
+ // set utf-16 lead surrogate to code unit and return continue.
+ if (inRange(code_unit, 0xD800, 0xDBFF)) {
+ utf16_lead_surrogate = code_unit;
+ return null;
+ }
+
+ // 7. If code unit is in the range U+DC00 to U+DFFF, inclusive,
+ // return error.
+ if (inRange(code_unit, 0xDC00, 0xDFFF))
+ return decoderError(fatal);
+
+ // 8. Return code point code unit.
+ return code_unit;
+ };
+ }
+
+ // 15.2.2 shared utf-16 encoder
+ /**
+ * @constructor
+ * @implements {Encoder}
+ * @param {boolean} utf16_be True if big-endian, false if little-endian.
+ * @param {{fatal: boolean}} options
+ */
+ function UTF16Encoder(utf16_be, options) {
+ var fatal = options.fatal;
+ /**
+ * @param {Stream} stream Input stream.
+ * @param {number} code_point Next code point read from the stream.
+ * @return {(number|!Array.<number>)} Byte(s) to emit.
+ */
+ this.handler = function(stream, code_point) {
+ // 1. If code point is end-of-stream, return finished.
+ if (code_point === end_of_stream)
+ return finished;
+
+ // 2. If code point is in the range U+0000 to U+FFFF, inclusive,
+ // return the sequence resulting of converting code point to
+ // bytes using utf-16be encoder flag.
+ if (inRange(code_point, 0x0000, 0xFFFF))
+ return convertCodeUnitToBytes(code_point, utf16_be);
+
+ // 3. Let lead be ((code point − 0x10000) >> 10) + 0xD800,
+ // converted to bytes using utf-16be encoder flag.
+ var lead = convertCodeUnitToBytes(
+ ((code_point - 0x10000) >> 10) + 0xD800, utf16_be);
+
+ // 4. Let trail be ((code point − 0x10000) & 0x3FF) + 0xDC00,
+ // converted to bytes using utf-16be encoder flag.
+ var trail = convertCodeUnitToBytes(
+ ((code_point - 0x10000) & 0x3FF) + 0xDC00, utf16_be);
+
+ // 5. Return a byte sequence of lead followed by trail.
+ return lead.concat(trail);
+ };
+ }
+
+ // 15.3 utf-16be
+ // 15.3.1 utf-16be decoder
+ /** @param {{fatal: boolean}} options */
+ encoders['UTF-16BE'] = function(options) {
+ return new UTF16Encoder(true, options);
+ };
+ // 15.3.2 utf-16be encoder
+ /** @param {{fatal: boolean}} options */
+ decoders['UTF-16BE'] = function(options) {
+ return new UTF16Decoder(true, options);
+ };
+
+ // 15.4 utf-16le
+ // 15.4.1 utf-16le decoder
+ /** @param {{fatal: boolean}} options */
+ encoders['UTF-16LE'] = function(options) {
+ return new UTF16Encoder(false, options);
+ };
+ // 15.4.2 utf-16le encoder
+ /** @param {{fatal: boolean}} options */
+ decoders['UTF-16LE'] = function(options) {
+ return new UTF16Decoder(false, options);
+ };
+
+ // 15.5 x-user-defined
+
+ // 15.5.1 x-user-defined decoder
+ /**
+ * @constructor
+ * @implements {Decoder}
+ * @param {{fatal: boolean}} options
+ */
+ function XUserDefinedDecoder(options) {
+ var fatal = options.fatal;
+ /**
+ * @param {Stream} stream The stream of bytes being decoded.
+ * @param {number} bite The next byte read from the stream.
+ * @return {?(number|!Array.<number>)} The next code point(s)
+ * decoded, or null if not enough data exists in the input
+ * stream to decode a complete code point.
+ */
+ this.handler = function(stream, bite) {
+ // 1. If byte is end-of-stream, return finished.
+ if (bite === end_of_stream)
+ return finished;
+
+ // 2. If byte is an ASCII byte, return a code point whose value
+ // is byte.
+ if (isASCIIByte(bite))
+ return bite;
+
+ // 3. Return a code point whose value is 0xF780 + byte − 0x80.
+ return 0xF780 + bite - 0x80;
+ };
+ }
+
+ // 15.5.2 x-user-defined encoder
+ /**
+ * @constructor
+ * @implements {Encoder}
+ * @param {{fatal: boolean}} options
+ */
+ function XUserDefinedEncoder(options) {
+ var fatal = options.fatal;
+ /**
+ * @param {Stream} stream Input stream.
+ * @param {number} code_point Next code point read from the stream.
+ * @return {(number|!Array.<number>)} Byte(s) to emit.
+ */
+ this.handler = function(stream, code_point) {
+ // 1.If code point is end-of-stream, return finished.
+ if (code_point === end_of_stream)
+ return finished;
+
+ // 2. If code point is an ASCII code point, return a byte whose
+ // value is code point.
+ if (isASCIICodePoint(code_point))
+ return code_point;
+
+ // 3. If code point is in the range U+F780 to U+F7FF, inclusive,
+ // return a byte whose value is code point − 0xF780 + 0x80.
+ if (inRange(code_point, 0xF780, 0xF7FF))
+ return code_point - 0xF780 + 0x80;
+
+ // 4. Return error with code point.
+ return encoderError(code_point);
+ };
+ }
+
+ /** @param {{fatal: boolean}} options */
+ encoders['x-user-defined'] = function(options) {
+ return new XUserDefinedEncoder(options);
+ };
+ /** @param {{fatal: boolean}} options */
+ decoders['x-user-defined'] = function(options) {
+ return new XUserDefinedDecoder(options);
+ };
+
+ if (!global['TextEncoder'])
+ global['TextEncoder'] = TextEncoder;
+ if (!global['TextDecoder'])
+ global['TextDecoder'] = TextDecoder;
+
+ if (typeof module !== "undefined" && module.exports) {
+ module.exports = {
+ TextEncoder: global['TextEncoder'],
+ TextDecoder: global['TextDecoder'],
+ EncodingIndexes: global["encoding-indexes"]
+ };
+ }
+}(this));
diff --git a/vendor/assets/javascripts/xterm/fit.js b/vendor/assets/javascripts/xterm/fit.js
index 7e24fd9b36e..55438452cad 100644
--- a/vendor/assets/javascripts/xterm/fit.js
+++ b/vendor/assets/javascripts/xterm/fit.js
@@ -16,12 +16,12 @@
/*
* CommonJS environment
*/
- module.exports = fit(require('../../xterm'));
+ module.exports = fit(require('./xterm'));
} else if (typeof define == 'function') {
/*
* Require.js is available
*/
- define(['../../xterm'], fit);
+ define(['./xterm'], fit);
} else {
/*
* Plain browser environment
diff --git a/vendor/assets/stylesheets/katex.css b/vendor/assets/stylesheets/katex.css
deleted file mode 100644
index 3e62df2329c..00000000000
--- a/vendor/assets/stylesheets/katex.css
+++ /dev/null
@@ -1,975 +0,0 @@
-/*
-The MIT License (MIT)
-
-Copyright (c) 2015 Khan Academy
-
-This software also uses portions of the underscore.js project, which is
-MIT licensed with the following copyright:
-
-Copyright (c) 2009-2015 Jeremy Ashkenas, DocumentCloud and Investigative
-Reporters & Editors
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
-*/
-
-/*
- Here is how to build a version of KaTeX that works with gitlab.
-
- The problem is that the standard procedure for changing font location doesn't work for the empty string.
-
- 1. Clone KaTeX. Anything later than 4fb9445a9 (is merged into master) will do.
- 2. make (requires node)
- 3. sed -i 's,fonts/,,' build/katex.css
- 4. Copy build/katex.js, build/katex.css and fonts/* to gitlab.
-*/
-
-@font-face {
- font-family: 'KaTeX_AMS';
- src: url('KaTeX_AMS-Regular.eot');
- src: url('KaTeX_AMS-Regular.eot#iefix') format('embedded-opentype'), url('KaTeX_AMS-Regular.woff2') format('woff2'), url('KaTeX_AMS-Regular.woff') format('woff'), url('KaTeX_AMS-Regular.ttf') format('truetype');
- font-weight: normal;
- font-style: normal;
-}
-@font-face {
- font-family: 'KaTeX_Caligraphic';
- src: url('KaTeX_Caligraphic-Bold.eot');
- src: url('KaTeX_Caligraphic-Bold.eot#iefix') format('embedded-opentype'), url('KaTeX_Caligraphic-Bold.woff2') format('woff2'), url('KaTeX_Caligraphic-Bold.woff') format('woff'), url('KaTeX_Caligraphic-Bold.ttf') format('truetype');
- font-weight: bold;
- font-style: normal;
-}
-@font-face {
- font-family: 'KaTeX_Caligraphic';
- src: url('KaTeX_Caligraphic-Regular.eot');
- src: url('KaTeX_Caligraphic-Regular.eot#iefix') format('embedded-opentype'), url('KaTeX_Caligraphic-Regular.woff2') format('woff2'), url('KaTeX_Caligraphic-Regular.woff') format('woff'), url('KaTeX_Caligraphic-Regular.ttf') format('truetype');
- font-weight: normal;
- font-style: normal;
-}
-@font-face {
- font-family: 'KaTeX_Fraktur';
- src: url('KaTeX_Fraktur-Bold.eot');
- src: url('KaTeX_Fraktur-Bold.eot#iefix') format('embedded-opentype'), url('KaTeX_Fraktur-Bold.woff2') format('woff2'), url('KaTeX_Fraktur-Bold.woff') format('woff'), url('KaTeX_Fraktur-Bold.ttf') format('truetype');
- font-weight: bold;
- font-style: normal;
-}
-@font-face {
- font-family: 'KaTeX_Fraktur';
- src: url('KaTeX_Fraktur-Regular.eot');
- src: url('KaTeX_Fraktur-Regular.eot#iefix') format('embedded-opentype'), url('KaTeX_Fraktur-Regular.woff2') format('woff2'), url('KaTeX_Fraktur-Regular.woff') format('woff'), url('KaTeX_Fraktur-Regular.ttf') format('truetype');
- font-weight: normal;
- font-style: normal;
-}
-@font-face {
- font-family: 'KaTeX_Main';
- src: url('KaTeX_Main-Bold.eot');
- src: url('KaTeX_Main-Bold.eot#iefix') format('embedded-opentype'), url('KaTeX_Main-Bold.woff2') format('woff2'), url('KaTeX_Main-Bold.woff') format('woff'), url('KaTeX_Main-Bold.ttf') format('truetype');
- font-weight: bold;
- font-style: normal;
-}
-@font-face {
- font-family: 'KaTeX_Main';
- src: url('KaTeX_Main-Italic.eot');
- src: url('KaTeX_Main-Italic.eot#iefix') format('embedded-opentype'), url('KaTeX_Main-Italic.woff2') format('woff2'), url('KaTeX_Main-Italic.woff') format('woff'), url('KaTeX_Main-Italic.ttf') format('truetype');
- font-weight: normal;
- font-style: italic;
-}
-@font-face {
- font-family: 'KaTeX_Main';
- src: url('KaTeX_Main-Regular.eot');
- src: url('KaTeX_Main-Regular.eot#iefix') format('embedded-opentype'), url('KaTeX_Main-Regular.woff2') format('woff2'), url('KaTeX_Main-Regular.woff') format('woff'), url('KaTeX_Main-Regular.ttf') format('truetype');
- font-weight: normal;
- font-style: normal;
-}
-@font-face {
- font-family: 'KaTeX_Math';
- src: url('KaTeX_Math-Italic.eot');
- src: url('KaTeX_Math-Italic.eot#iefix') format('embedded-opentype'), url('KaTeX_Math-Italic.woff2') format('woff2'), url('KaTeX_Math-Italic.woff') format('woff'), url('KaTeX_Math-Italic.ttf') format('truetype');
- font-weight: normal;
- font-style: italic;
-}
-@font-face {
- font-family: 'KaTeX_SansSerif';
- src: url('KaTeX_SansSerif-Regular.eot');
- src: url('KaTeX_SansSerif-Regular.eot#iefix') format('embedded-opentype'), url('KaTeX_SansSerif-Regular.woff2') format('woff2'), url('KaTeX_SansSerif-Regular.woff') format('woff'), url('KaTeX_SansSerif-Regular.ttf') format('truetype');
- font-weight: normal;
- font-style: normal;
-}
-@font-face {
- font-family: 'KaTeX_Script';
- src: url('KaTeX_Script-Regular.eot');
- src: url('KaTeX_Script-Regular.eot#iefix') format('embedded-opentype'), url('KaTeX_Script-Regular.woff2') format('woff2'), url('KaTeX_Script-Regular.woff') format('woff'), url('KaTeX_Script-Regular.ttf') format('truetype');
- font-weight: normal;
- font-style: normal;
-}
-@font-face {
- font-family: 'KaTeX_Size1';
- src: url('KaTeX_Size1-Regular.eot');
- src: url('KaTeX_Size1-Regular.eot#iefix') format('embedded-opentype'), url('KaTeX_Size1-Regular.woff2') format('woff2'), url('KaTeX_Size1-Regular.woff') format('woff'), url('KaTeX_Size1-Regular.ttf') format('truetype');
- font-weight: normal;
- font-style: normal;
-}
-@font-face {
- font-family: 'KaTeX_Size2';
- src: url('KaTeX_Size2-Regular.eot');
- src: url('KaTeX_Size2-Regular.eot#iefix') format('embedded-opentype'), url('KaTeX_Size2-Regular.woff2') format('woff2'), url('KaTeX_Size2-Regular.woff') format('woff'), url('KaTeX_Size2-Regular.ttf') format('truetype');
- font-weight: normal;
- font-style: normal;
-}
-@font-face {
- font-family: 'KaTeX_Size3';
- src: url('KaTeX_Size3-Regular.eot');
- src: url('KaTeX_Size3-Regular.eot#iefix') format('embedded-opentype'), url('KaTeX_Size3-Regular.woff2') format('woff2'), url('KaTeX_Size3-Regular.woff') format('woff'), url('KaTeX_Size3-Regular.ttf') format('truetype');
- font-weight: normal;
- font-style: normal;
-}
-@font-face {
- font-family: 'KaTeX_Size4';
- src: url('KaTeX_Size4-Regular.eot');
- src: url('KaTeX_Size4-Regular.eot#iefix') format('embedded-opentype'), url('KaTeX_Size4-Regular.woff2') format('woff2'), url('KaTeX_Size4-Regular.woff') format('woff'), url('KaTeX_Size4-Regular.ttf') format('truetype');
- font-weight: normal;
- font-style: normal;
-}
-@font-face {
- font-family: 'KaTeX_Typewriter';
- src: url('KaTeX_Typewriter-Regular.eot');
- src: url('KaTeX_Typewriter-Regular.eot#iefix') format('embedded-opentype'), url('KaTeX_Typewriter-Regular.woff2') format('woff2'), url('KaTeX_Typewriter-Regular.woff') format('woff'), url('KaTeX_Typewriter-Regular.ttf') format('truetype');
- font-weight: normal;
- font-style: normal;
-}
-.katex-display {
- display: block;
- margin: 1em 0;
- text-align: center;
-}
-.katex-display > .katex {
- display: inline-block;
- text-align: initial;
-}
-.katex {
- font: normal 1.21em KaTeX_Main, Times New Roman, serif;
- line-height: 1.2;
- white-space: nowrap;
- text-indent: 0;
-}
-.katex .katex-html {
- display: inline-block;
-}
-.katex .katex-mathml {
- position: absolute;
- clip: rect(1px, 1px, 1px, 1px);
- padding: 0;
- border: 0;
- height: 1px;
- width: 1px;
- overflow: hidden;
-}
-.katex .base {
- display: inline-block;
-}
-.katex .strut {
- display: inline-block;
-}
-.katex .mathit {
- font-family: KaTeX_Math;
- font-style: italic;
-}
-.katex .mathbf {
- font-family: KaTeX_Main;
- font-weight: bold;
-}
-.katex .amsrm {
- font-family: KaTeX_AMS;
-}
-.katex .mathbb {
- font-family: KaTeX_AMS;
-}
-.katex .mathcal {
- font-family: KaTeX_Caligraphic;
-}
-.katex .mathfrak {
- font-family: KaTeX_Fraktur;
-}
-.katex .mathtt {
- font-family: KaTeX_Typewriter;
-}
-.katex .mathscr {
- font-family: KaTeX_Script;
-}
-.katex .mathsf {
- font-family: KaTeX_SansSerif;
-}
-.katex .mainit {
- font-family: KaTeX_Main;
- font-style: italic;
-}
-.katex .textstyle > .mord + .mop {
- margin-left: 0.16667em;
-}
-.katex .textstyle > .mord + .mbin {
- margin-left: 0.22222em;
-}
-.katex .textstyle > .mord + .mrel {
- margin-left: 0.27778em;
-}
-.katex .textstyle > .mord + .minner {
- margin-left: 0.16667em;
-}
-.katex .textstyle > .mop + .mord {
- margin-left: 0.16667em;
-}
-.katex .textstyle > .mop + .mop {
- margin-left: 0.16667em;
-}
-.katex .textstyle > .mop + .mrel {
- margin-left: 0.27778em;
-}
-.katex .textstyle > .mop + .minner {
- margin-left: 0.16667em;
-}
-.katex .textstyle > .mbin + .mord {
- margin-left: 0.22222em;
-}
-.katex .textstyle > .mbin + .mop {
- margin-left: 0.22222em;
-}
-.katex .textstyle > .mbin + .mopen {
- margin-left: 0.22222em;
-}
-.katex .textstyle > .mbin + .minner {
- margin-left: 0.22222em;
-}
-.katex .textstyle > .mrel + .mord {
- margin-left: 0.27778em;
-}
-.katex .textstyle > .mrel + .mop {
- margin-left: 0.27778em;
-}
-.katex .textstyle > .mrel + .mopen {
- margin-left: 0.27778em;
-}
-.katex .textstyle > .mrel + .minner {
- margin-left: 0.27778em;
-}
-.katex .textstyle > .mclose + .mop {
- margin-left: 0.16667em;
-}
-.katex .textstyle > .mclose + .mbin {
- margin-left: 0.22222em;
-}
-.katex .textstyle > .mclose + .mrel {
- margin-left: 0.27778em;
-}
-.katex .textstyle > .mclose + .minner {
- margin-left: 0.16667em;
-}
-.katex .textstyle > .mpunct + .mord {
- margin-left: 0.16667em;
-}
-.katex .textstyle > .mpunct + .mop {
- margin-left: 0.16667em;
-}
-.katex .textstyle > .mpunct + .mrel {
- margin-left: 0.16667em;
-}
-.katex .textstyle > .mpunct + .mopen {
- margin-left: 0.16667em;
-}
-.katex .textstyle > .mpunct + .mclose {
- margin-left: 0.16667em;
-}
-.katex .textstyle > .mpunct + .mpunct {
- margin-left: 0.16667em;
-}
-.katex .textstyle > .mpunct + .minner {
- margin-left: 0.16667em;
-}
-.katex .textstyle > .minner + .mord {
- margin-left: 0.16667em;
-}
-.katex .textstyle > .minner + .mop {
- margin-left: 0.16667em;
-}
-.katex .textstyle > .minner + .mbin {
- margin-left: 0.22222em;
-}
-.katex .textstyle > .minner + .mrel {
- margin-left: 0.27778em;
-}
-.katex .textstyle > .minner + .mopen {
- margin-left: 0.16667em;
-}
-.katex .textstyle > .minner + .mpunct {
- margin-left: 0.16667em;
-}
-.katex .textstyle > .minner + .minner {
- margin-left: 0.16667em;
-}
-.katex .mord + .mop {
- margin-left: 0.16667em;
-}
-.katex .mop + .mord {
- margin-left: 0.16667em;
-}
-.katex .mop + .mop {
- margin-left: 0.16667em;
-}
-.katex .mclose + .mop {
- margin-left: 0.16667em;
-}
-.katex .minner + .mop {
- margin-left: 0.16667em;
-}
-.katex .reset-textstyle.textstyle {
- font-size: 1em;
-}
-.katex .reset-textstyle.scriptstyle {
- font-size: 0.7em;
-}
-.katex .reset-textstyle.scriptscriptstyle {
- font-size: 0.5em;
-}
-.katex .reset-scriptstyle.textstyle {
- font-size: 1.42857em;
-}
-.katex .reset-scriptstyle.scriptstyle {
- font-size: 1em;
-}
-.katex .reset-scriptstyle.scriptscriptstyle {
- font-size: 0.71429em;
-}
-.katex .reset-scriptscriptstyle.textstyle {
- font-size: 2em;
-}
-.katex .reset-scriptscriptstyle.scriptstyle {
- font-size: 1.4em;
-}
-.katex .reset-scriptscriptstyle.scriptscriptstyle {
- font-size: 1em;
-}
-.katex .style-wrap {
- position: relative;
-}
-.katex .vlist {
- display: inline-block;
-}
-.katex .vlist > span {
- display: block;
- height: 0;
- position: relative;
-}
-.katex .vlist > span > span {
- display: inline-block;
-}
-.katex .vlist .baseline-fix {
- display: inline-table;
- table-layout: fixed;
-}
-.katex .msupsub {
- text-align: left;
-}
-.katex .mfrac > span > span {
- text-align: center;
-}
-.katex .mfrac .frac-line {
- width: 100%;
-}
-.katex .mfrac .frac-line:before {
- border-bottom-style: solid;
- border-bottom-width: 1px;
- content: "";
- display: block;
-}
-.katex .mfrac .frac-line:after {
- border-bottom-style: solid;
- border-bottom-width: 0.04em;
- content: "";
- display: block;
- margin-top: -1px;
-}
-.katex .mspace {
- display: inline-block;
-}
-.katex .mspace.negativethinspace {
- margin-left: -0.16667em;
-}
-.katex .mspace.thinspace {
- width: 0.16667em;
-}
-.katex .mspace.mediumspace {
- width: 0.22222em;
-}
-.katex .mspace.thickspace {
- width: 0.27778em;
-}
-.katex .mspace.enspace {
- width: 0.5em;
-}
-.katex .mspace.quad {
- width: 1em;
-}
-.katex .mspace.qquad {
- width: 2em;
-}
-.katex .llap,
-.katex .rlap {
- width: 0;
- position: relative;
-}
-.katex .llap > .inner,
-.katex .rlap > .inner {
- position: absolute;
-}
-.katex .llap > .fix,
-.katex .rlap > .fix {
- display: inline-block;
-}
-.katex .llap > .inner {
- right: 0;
-}
-.katex .rlap > .inner {
- left: 0;
-}
-.katex .katex-logo .a {
- font-size: 0.75em;
- margin-left: -0.32em;
- position: relative;
- top: -0.2em;
-}
-.katex .katex-logo .t {
- margin-left: -0.23em;
-}
-.katex .katex-logo .e {
- margin-left: -0.1667em;
- position: relative;
- top: 0.2155em;
-}
-.katex .katex-logo .x {
- margin-left: -0.125em;
-}
-.katex .rule {
- display: inline-block;
- border: solid 0;
- position: relative;
-}
-.katex .overline .overline-line,
-.katex .underline .underline-line {
- width: 100%;
-}
-.katex .overline .overline-line:before,
-.katex .underline .underline-line:before {
- border-bottom-style: solid;
- border-bottom-width: 1px;
- content: "";
- display: block;
-}
-.katex .overline .overline-line:after,
-.katex .underline .underline-line:after {
- border-bottom-style: solid;
- border-bottom-width: 0.04em;
- content: "";
- display: block;
- margin-top: -1px;
-}
-.katex .sqrt > .sqrt-sign {
- position: relative;
-}
-.katex .sqrt .sqrt-line {
- width: 100%;
-}
-.katex .sqrt .sqrt-line:before {
- border-bottom-style: solid;
- border-bottom-width: 1px;
- content: "";
- display: block;
-}
-.katex .sqrt .sqrt-line:after {
- border-bottom-style: solid;
- border-bottom-width: 0.04em;
- content: "";
- display: block;
- margin-top: -1px;
-}
-.katex .sqrt > .root {
- margin-left: 0.27777778em;
- margin-right: -0.55555556em;
-}
-.katex .sizing,
-.katex .fontsize-ensurer {
- display: inline-block;
-}
-.katex .sizing.reset-size1.size1,
-.katex .fontsize-ensurer.reset-size1.size1 {
- font-size: 1em;
-}
-.katex .sizing.reset-size1.size2,
-.katex .fontsize-ensurer.reset-size1.size2 {
- font-size: 1.4em;
-}
-.katex .sizing.reset-size1.size3,
-.katex .fontsize-ensurer.reset-size1.size3 {
- font-size: 1.6em;
-}
-.katex .sizing.reset-size1.size4,
-.katex .fontsize-ensurer.reset-size1.size4 {
- font-size: 1.8em;
-}
-.katex .sizing.reset-size1.size5,
-.katex .fontsize-ensurer.reset-size1.size5 {
- font-size: 2em;
-}
-.katex .sizing.reset-size1.size6,
-.katex .fontsize-ensurer.reset-size1.size6 {
- font-size: 2.4em;
-}
-.katex .sizing.reset-size1.size7,
-.katex .fontsize-ensurer.reset-size1.size7 {
- font-size: 2.88em;
-}
-.katex .sizing.reset-size1.size8,
-.katex .fontsize-ensurer.reset-size1.size8 {
- font-size: 3.46em;
-}
-.katex .sizing.reset-size1.size9,
-.katex .fontsize-ensurer.reset-size1.size9 {
- font-size: 4.14em;
-}
-.katex .sizing.reset-size1.size10,
-.katex .fontsize-ensurer.reset-size1.size10 {
- font-size: 4.98em;
-}
-.katex .sizing.reset-size2.size1,
-.katex .fontsize-ensurer.reset-size2.size1 {
- font-size: 0.71428571em;
-}
-.katex .sizing.reset-size2.size2,
-.katex .fontsize-ensurer.reset-size2.size2 {
- font-size: 1em;
-}
-.katex .sizing.reset-size2.size3,
-.katex .fontsize-ensurer.reset-size2.size3 {
- font-size: 1.14285714em;
-}
-.katex .sizing.reset-size2.size4,
-.katex .fontsize-ensurer.reset-size2.size4 {
- font-size: 1.28571429em;
-}
-.katex .sizing.reset-size2.size5,
-.katex .fontsize-ensurer.reset-size2.size5 {
- font-size: 1.42857143em;
-}
-.katex .sizing.reset-size2.size6,
-.katex .fontsize-ensurer.reset-size2.size6 {
- font-size: 1.71428571em;
-}
-.katex .sizing.reset-size2.size7,
-.katex .fontsize-ensurer.reset-size2.size7 {
- font-size: 2.05714286em;
-}
-.katex .sizing.reset-size2.size8,
-.katex .fontsize-ensurer.reset-size2.size8 {
- font-size: 2.47142857em;
-}
-.katex .sizing.reset-size2.size9,
-.katex .fontsize-ensurer.reset-size2.size9 {
- font-size: 2.95714286em;
-}
-.katex .sizing.reset-size2.size10,
-.katex .fontsize-ensurer.reset-size2.size10 {
- font-size: 3.55714286em;
-}
-.katex .sizing.reset-size3.size1,
-.katex .fontsize-ensurer.reset-size3.size1 {
- font-size: 0.625em;
-}
-.katex .sizing.reset-size3.size2,
-.katex .fontsize-ensurer.reset-size3.size2 {
- font-size: 0.875em;
-}
-.katex .sizing.reset-size3.size3,
-.katex .fontsize-ensurer.reset-size3.size3 {
- font-size: 1em;
-}
-.katex .sizing.reset-size3.size4,
-.katex .fontsize-ensurer.reset-size3.size4 {
- font-size: 1.125em;
-}
-.katex .sizing.reset-size3.size5,
-.katex .fontsize-ensurer.reset-size3.size5 {
- font-size: 1.25em;
-}
-.katex .sizing.reset-size3.size6,
-.katex .fontsize-ensurer.reset-size3.size6 {
- font-size: 1.5em;
-}
-.katex .sizing.reset-size3.size7,
-.katex .fontsize-ensurer.reset-size3.size7 {
- font-size: 1.8em;
-}
-.katex .sizing.reset-size3.size8,
-.katex .fontsize-ensurer.reset-size3.size8 {
- font-size: 2.1625em;
-}
-.katex .sizing.reset-size3.size9,
-.katex .fontsize-ensurer.reset-size3.size9 {
- font-size: 2.5875em;
-}
-.katex .sizing.reset-size3.size10,
-.katex .fontsize-ensurer.reset-size3.size10 {
- font-size: 3.1125em;
-}
-.katex .sizing.reset-size4.size1,
-.katex .fontsize-ensurer.reset-size4.size1 {
- font-size: 0.55555556em;
-}
-.katex .sizing.reset-size4.size2,
-.katex .fontsize-ensurer.reset-size4.size2 {
- font-size: 0.77777778em;
-}
-.katex .sizing.reset-size4.size3,
-.katex .fontsize-ensurer.reset-size4.size3 {
- font-size: 0.88888889em;
-}
-.katex .sizing.reset-size4.size4,
-.katex .fontsize-ensurer.reset-size4.size4 {
- font-size: 1em;
-}
-.katex .sizing.reset-size4.size5,
-.katex .fontsize-ensurer.reset-size4.size5 {
- font-size: 1.11111111em;
-}
-.katex .sizing.reset-size4.size6,
-.katex .fontsize-ensurer.reset-size4.size6 {
- font-size: 1.33333333em;
-}
-.katex .sizing.reset-size4.size7,
-.katex .fontsize-ensurer.reset-size4.size7 {
- font-size: 1.6em;
-}
-.katex .sizing.reset-size4.size8,
-.katex .fontsize-ensurer.reset-size4.size8 {
- font-size: 1.92222222em;
-}
-.katex .sizing.reset-size4.size9,
-.katex .fontsize-ensurer.reset-size4.size9 {
- font-size: 2.3em;
-}
-.katex .sizing.reset-size4.size10,
-.katex .fontsize-ensurer.reset-size4.size10 {
- font-size: 2.76666667em;
-}
-.katex .sizing.reset-size5.size1,
-.katex .fontsize-ensurer.reset-size5.size1 {
- font-size: 0.5em;
-}
-.katex .sizing.reset-size5.size2,
-.katex .fontsize-ensurer.reset-size5.size2 {
- font-size: 0.7em;
-}
-.katex .sizing.reset-size5.size3,
-.katex .fontsize-ensurer.reset-size5.size3 {
- font-size: 0.8em;
-}
-.katex .sizing.reset-size5.size4,
-.katex .fontsize-ensurer.reset-size5.size4 {
- font-size: 0.9em;
-}
-.katex .sizing.reset-size5.size5,
-.katex .fontsize-ensurer.reset-size5.size5 {
- font-size: 1em;
-}
-.katex .sizing.reset-size5.size6,
-.katex .fontsize-ensurer.reset-size5.size6 {
- font-size: 1.2em;
-}
-.katex .sizing.reset-size5.size7,
-.katex .fontsize-ensurer.reset-size5.size7 {
- font-size: 1.44em;
-}
-.katex .sizing.reset-size5.size8,
-.katex .fontsize-ensurer.reset-size5.size8 {
- font-size: 1.73em;
-}
-.katex .sizing.reset-size5.size9,
-.katex .fontsize-ensurer.reset-size5.size9 {
- font-size: 2.07em;
-}
-.katex .sizing.reset-size5.size10,
-.katex .fontsize-ensurer.reset-size5.size10 {
- font-size: 2.49em;
-}
-.katex .sizing.reset-size6.size1,
-.katex .fontsize-ensurer.reset-size6.size1 {
- font-size: 0.41666667em;
-}
-.katex .sizing.reset-size6.size2,
-.katex .fontsize-ensurer.reset-size6.size2 {
- font-size: 0.58333333em;
-}
-.katex .sizing.reset-size6.size3,
-.katex .fontsize-ensurer.reset-size6.size3 {
- font-size: 0.66666667em;
-}
-.katex .sizing.reset-size6.size4,
-.katex .fontsize-ensurer.reset-size6.size4 {
- font-size: 0.75em;
-}
-.katex .sizing.reset-size6.size5,
-.katex .fontsize-ensurer.reset-size6.size5 {
- font-size: 0.83333333em;
-}
-.katex .sizing.reset-size6.size6,
-.katex .fontsize-ensurer.reset-size6.size6 {
- font-size: 1em;
-}
-.katex .sizing.reset-size6.size7,
-.katex .fontsize-ensurer.reset-size6.size7 {
- font-size: 1.2em;
-}
-.katex .sizing.reset-size6.size8,
-.katex .fontsize-ensurer.reset-size6.size8 {
- font-size: 1.44166667em;
-}
-.katex .sizing.reset-size6.size9,
-.katex .fontsize-ensurer.reset-size6.size9 {
- font-size: 1.725em;
-}
-.katex .sizing.reset-size6.size10,
-.katex .fontsize-ensurer.reset-size6.size10 {
- font-size: 2.075em;
-}
-.katex .sizing.reset-size7.size1,
-.katex .fontsize-ensurer.reset-size7.size1 {
- font-size: 0.34722222em;
-}
-.katex .sizing.reset-size7.size2,
-.katex .fontsize-ensurer.reset-size7.size2 {
- font-size: 0.48611111em;
-}
-.katex .sizing.reset-size7.size3,
-.katex .fontsize-ensurer.reset-size7.size3 {
- font-size: 0.55555556em;
-}
-.katex .sizing.reset-size7.size4,
-.katex .fontsize-ensurer.reset-size7.size4 {
- font-size: 0.625em;
-}
-.katex .sizing.reset-size7.size5,
-.katex .fontsize-ensurer.reset-size7.size5 {
- font-size: 0.69444444em;
-}
-.katex .sizing.reset-size7.size6,
-.katex .fontsize-ensurer.reset-size7.size6 {
- font-size: 0.83333333em;
-}
-.katex .sizing.reset-size7.size7,
-.katex .fontsize-ensurer.reset-size7.size7 {
- font-size: 1em;
-}
-.katex .sizing.reset-size7.size8,
-.katex .fontsize-ensurer.reset-size7.size8 {
- font-size: 1.20138889em;
-}
-.katex .sizing.reset-size7.size9,
-.katex .fontsize-ensurer.reset-size7.size9 {
- font-size: 1.4375em;
-}
-.katex .sizing.reset-size7.size10,
-.katex .fontsize-ensurer.reset-size7.size10 {
- font-size: 1.72916667em;
-}
-.katex .sizing.reset-size8.size1,
-.katex .fontsize-ensurer.reset-size8.size1 {
- font-size: 0.28901734em;
-}
-.katex .sizing.reset-size8.size2,
-.katex .fontsize-ensurer.reset-size8.size2 {
- font-size: 0.40462428em;
-}
-.katex .sizing.reset-size8.size3,
-.katex .fontsize-ensurer.reset-size8.size3 {
- font-size: 0.46242775em;
-}
-.katex .sizing.reset-size8.size4,
-.katex .fontsize-ensurer.reset-size8.size4 {
- font-size: 0.52023121em;
-}
-.katex .sizing.reset-size8.size5,
-.katex .fontsize-ensurer.reset-size8.size5 {
- font-size: 0.57803468em;
-}
-.katex .sizing.reset-size8.size6,
-.katex .fontsize-ensurer.reset-size8.size6 {
- font-size: 0.69364162em;
-}
-.katex .sizing.reset-size8.size7,
-.katex .fontsize-ensurer.reset-size8.size7 {
- font-size: 0.83236994em;
-}
-.katex .sizing.reset-size8.size8,
-.katex .fontsize-ensurer.reset-size8.size8 {
- font-size: 1em;
-}
-.katex .sizing.reset-size8.size9,
-.katex .fontsize-ensurer.reset-size8.size9 {
- font-size: 1.19653179em;
-}
-.katex .sizing.reset-size8.size10,
-.katex .fontsize-ensurer.reset-size8.size10 {
- font-size: 1.43930636em;
-}
-.katex .sizing.reset-size9.size1,
-.katex .fontsize-ensurer.reset-size9.size1 {
- font-size: 0.24154589em;
-}
-.katex .sizing.reset-size9.size2,
-.katex .fontsize-ensurer.reset-size9.size2 {
- font-size: 0.33816425em;
-}
-.katex .sizing.reset-size9.size3,
-.katex .fontsize-ensurer.reset-size9.size3 {
- font-size: 0.38647343em;
-}
-.katex .sizing.reset-size9.size4,
-.katex .fontsize-ensurer.reset-size9.size4 {
- font-size: 0.43478261em;
-}
-.katex .sizing.reset-size9.size5,
-.katex .fontsize-ensurer.reset-size9.size5 {
- font-size: 0.48309179em;
-}
-.katex .sizing.reset-size9.size6,
-.katex .fontsize-ensurer.reset-size9.size6 {
- font-size: 0.57971014em;
-}
-.katex .sizing.reset-size9.size7,
-.katex .fontsize-ensurer.reset-size9.size7 {
- font-size: 0.69565217em;
-}
-.katex .sizing.reset-size9.size8,
-.katex .fontsize-ensurer.reset-size9.size8 {
- font-size: 0.83574879em;
-}
-.katex .sizing.reset-size9.size9,
-.katex .fontsize-ensurer.reset-size9.size9 {
- font-size: 1em;
-}
-.katex .sizing.reset-size9.size10,
-.katex .fontsize-ensurer.reset-size9.size10 {
- font-size: 1.20289855em;
-}
-.katex .sizing.reset-size10.size1,
-.katex .fontsize-ensurer.reset-size10.size1 {
- font-size: 0.20080321em;
-}
-.katex .sizing.reset-size10.size2,
-.katex .fontsize-ensurer.reset-size10.size2 {
- font-size: 0.2811245em;
-}
-.katex .sizing.reset-size10.size3,
-.katex .fontsize-ensurer.reset-size10.size3 {
- font-size: 0.32128514em;
-}
-.katex .sizing.reset-size10.size4,
-.katex .fontsize-ensurer.reset-size10.size4 {
- font-size: 0.36144578em;
-}
-.katex .sizing.reset-size10.size5,
-.katex .fontsize-ensurer.reset-size10.size5 {
- font-size: 0.40160643em;
-}
-.katex .sizing.reset-size10.size6,
-.katex .fontsize-ensurer.reset-size10.size6 {
- font-size: 0.48192771em;
-}
-.katex .sizing.reset-size10.size7,
-.katex .fontsize-ensurer.reset-size10.size7 {
- font-size: 0.57831325em;
-}
-.katex .sizing.reset-size10.size8,
-.katex .fontsize-ensurer.reset-size10.size8 {
- font-size: 0.69477912em;
-}
-.katex .sizing.reset-size10.size9,
-.katex .fontsize-ensurer.reset-size10.size9 {
- font-size: 0.8313253em;
-}
-.katex .sizing.reset-size10.size10,
-.katex .fontsize-ensurer.reset-size10.size10 {
- font-size: 1em;
-}
-.katex .delimsizing.size1 {
- font-family: KaTeX_Size1;
-}
-.katex .delimsizing.size2 {
- font-family: KaTeX_Size2;
-}
-.katex .delimsizing.size3 {
- font-family: KaTeX_Size3;
-}
-.katex .delimsizing.size4 {
- font-family: KaTeX_Size4;
-}
-.katex .delimsizing.mult .delim-size1 > span {
- font-family: KaTeX_Size1;
-}
-.katex .delimsizing.mult .delim-size4 > span {
- font-family: KaTeX_Size4;
-}
-.katex .nulldelimiter {
- display: inline-block;
- width: 0.12em;
-}
-.katex .op-symbol {
- position: relative;
-}
-.katex .op-symbol.small-op {
- font-family: KaTeX_Size1;
-}
-.katex .op-symbol.large-op {
- font-family: KaTeX_Size2;
-}
-.katex .op-limits > .vlist > span {
- text-align: center;
-}
-.katex .accent > .vlist > span {
- text-align: center;
-}
-.katex .accent .accent-body > span {
- width: 0;
-}
-.katex .accent .accent-body.accent-vec > span {
- position: relative;
- left: 0.326em;
-}
-.katex .mtable .vertical-separator {
- display: inline-block;
- margin: 0 -0.025em;
- border-right: 0.05em solid black;
-}
-.katex .mtable .arraycolsep {
- display: inline-block;
-}
-.katex .mtable .col-align-c > .vlist {
- text-align: center;
-}
-.katex .mtable .col-align-l > .vlist {
- text-align: left;
-}
-.katex .mtable .col-align-r > .vlist {
- text-align: right;
-}
diff --git a/vendor/assets/stylesheets/katex.scss b/vendor/assets/stylesheets/katex.scss
new file mode 100644
index 00000000000..9dd8a30bf51
--- /dev/null
+++ b/vendor/assets/stylesheets/katex.scss
@@ -0,0 +1,977 @@
+/*
+The MIT License (MIT)
+
+Copyright (c) 2015 Khan Academy
+
+This software also uses portions of the underscore.js project, which is
+MIT licensed with the following copyright:
+
+Copyright (c) 2009-2015 Jeremy Ashkenas, DocumentCloud and Investigative
+Reporters & Editors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+
+/*
+ Here is how to build a version of KaTeX that works with gitlab.
+
+ The problem is that the standard procedure for changing font location doesn't work for the empty string.
+
+ 1. Clone KaTeX. Anything later than 4fb9445a9 (is merged into master) will do.
+ 2. make (requires node)
+ 3. sed -e 's,fonts/,,' -e 's/url\(([^)]*)\)/url(font-path\1)/g' build/katex.css > build/katex.scss
+ 4. Copy build/katex.js to gitlab/vendor/assets/javascripts/katex.js,
+ build/katex.scss to gitlab/vendor/assets/stylesheets/katex.scss and
+ fonts/* to gitlab/vendor/assets/fonts/.
+*/
+
+@font-face {
+ font-family: 'KaTeX_AMS';
+ src: url(font-path('KaTeX_AMS-Regular.eot'));
+ src: url(font-path('KaTeX_AMS-Regular.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_AMS-Regular.woff2')) format('woff2'), url(font-path('KaTeX_AMS-Regular.woff')) format('woff'), url(font-path('KaTeX_AMS-Regular.ttf')) format('truetype');
+ font-weight: normal;
+ font-style: normal;
+}
+@font-face {
+ font-family: 'KaTeX_Caligraphic';
+ src: url(font-path('KaTeX_Caligraphic-Bold.eot'));
+ src: url(font-path('KaTeX_Caligraphic-Bold.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_Caligraphic-Bold.woff2')) format('woff2'), url(font-path('KaTeX_Caligraphic-Bold.woff')) format('woff'), url(font-path('KaTeX_Caligraphic-Bold.ttf')) format('truetype');
+ font-weight: bold;
+ font-style: normal;
+}
+@font-face {
+ font-family: 'KaTeX_Caligraphic';
+ src: url(font-path('KaTeX_Caligraphic-Regular.eot'));
+ src: url(font-path('KaTeX_Caligraphic-Regular.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_Caligraphic-Regular.woff2')) format('woff2'), url(font-path('KaTeX_Caligraphic-Regular.woff')) format('woff'), url(font-path('KaTeX_Caligraphic-Regular.ttf')) format('truetype');
+ font-weight: normal;
+ font-style: normal;
+}
+@font-face {
+ font-family: 'KaTeX_Fraktur';
+ src: url(font-path('KaTeX_Fraktur-Bold.eot'));
+ src: url(font-path('KaTeX_Fraktur-Bold.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_Fraktur-Bold.woff2')) format('woff2'), url(font-path('KaTeX_Fraktur-Bold.woff')) format('woff'), url(font-path('KaTeX_Fraktur-Bold.ttf')) format('truetype');
+ font-weight: bold;
+ font-style: normal;
+}
+@font-face {
+ font-family: 'KaTeX_Fraktur';
+ src: url(font-path('KaTeX_Fraktur-Regular.eot'));
+ src: url(font-path('KaTeX_Fraktur-Regular.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_Fraktur-Regular.woff2')) format('woff2'), url(font-path('KaTeX_Fraktur-Regular.woff')) format('woff'), url(font-path('KaTeX_Fraktur-Regular.ttf')) format('truetype');
+ font-weight: normal;
+ font-style: normal;
+}
+@font-face {
+ font-family: 'KaTeX_Main';
+ src: url(font-path('KaTeX_Main-Bold.eot'));
+ src: url(font-path('KaTeX_Main-Bold.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_Main-Bold.woff2')) format('woff2'), url(font-path('KaTeX_Main-Bold.woff')) format('woff'), url(font-path('KaTeX_Main-Bold.ttf')) format('truetype');
+ font-weight: bold;
+ font-style: normal;
+}
+@font-face {
+ font-family: 'KaTeX_Main';
+ src: url(font-path('KaTeX_Main-Italic.eot'));
+ src: url(font-path('KaTeX_Main-Italic.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_Main-Italic.woff2')) format('woff2'), url(font-path('KaTeX_Main-Italic.woff')) format('woff'), url(font-path('KaTeX_Main-Italic.ttf')) format('truetype');
+ font-weight: normal;
+ font-style: italic;
+}
+@font-face {
+ font-family: 'KaTeX_Main';
+ src: url(font-path('KaTeX_Main-Regular.eot'));
+ src: url(font-path('KaTeX_Main-Regular.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_Main-Regular.woff2')) format('woff2'), url(font-path('KaTeX_Main-Regular.woff')) format('woff'), url(font-path('KaTeX_Main-Regular.ttf')) format('truetype');
+ font-weight: normal;
+ font-style: normal;
+}
+@font-face {
+ font-family: 'KaTeX_Math';
+ src: url(font-path('KaTeX_Math-Italic.eot'));
+ src: url(font-path('KaTeX_Math-Italic.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_Math-Italic.woff2')) format('woff2'), url(font-path('KaTeX_Math-Italic.woff')) format('woff'), url(font-path('KaTeX_Math-Italic.ttf')) format('truetype');
+ font-weight: normal;
+ font-style: italic;
+}
+@font-face {
+ font-family: 'KaTeX_SansSerif';
+ src: url(font-path('KaTeX_SansSerif-Regular.eot'));
+ src: url(font-path('KaTeX_SansSerif-Regular.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_SansSerif-Regular.woff2')) format('woff2'), url(font-path('KaTeX_SansSerif-Regular.woff')) format('woff'), url(font-path('KaTeX_SansSerif-Regular.ttf')) format('truetype');
+ font-weight: normal;
+ font-style: normal;
+}
+@font-face {
+ font-family: 'KaTeX_Script';
+ src: url(font-path('KaTeX_Script-Regular.eot'));
+ src: url(font-path('KaTeX_Script-Regular.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_Script-Regular.woff2')) format('woff2'), url(font-path('KaTeX_Script-Regular.woff')) format('woff'), url(font-path('KaTeX_Script-Regular.ttf')) format('truetype');
+ font-weight: normal;
+ font-style: normal;
+}
+@font-face {
+ font-family: 'KaTeX_Size1';
+ src: url(font-path('KaTeX_Size1-Regular.eot'));
+ src: url(font-path('KaTeX_Size1-Regular.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_Size1-Regular.woff2')) format('woff2'), url(font-path('KaTeX_Size1-Regular.woff')) format('woff'), url(font-path('KaTeX_Size1-Regular.ttf')) format('truetype');
+ font-weight: normal;
+ font-style: normal;
+}
+@font-face {
+ font-family: 'KaTeX_Size2';
+ src: url(font-path('KaTeX_Size2-Regular.eot'));
+ src: url(font-path('KaTeX_Size2-Regular.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_Size2-Regular.woff2')) format('woff2'), url(font-path('KaTeX_Size2-Regular.woff')) format('woff'), url(font-path('KaTeX_Size2-Regular.ttf')) format('truetype');
+ font-weight: normal;
+ font-style: normal;
+}
+@font-face {
+ font-family: 'KaTeX_Size3';
+ src: url(font-path('KaTeX_Size3-Regular.eot'));
+ src: url(font-path('KaTeX_Size3-Regular.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_Size3-Regular.woff2')) format('woff2'), url(font-path('KaTeX_Size3-Regular.woff')) format('woff'), url(font-path('KaTeX_Size3-Regular.ttf')) format('truetype');
+ font-weight: normal;
+ font-style: normal;
+}
+@font-face {
+ font-family: 'KaTeX_Size4';
+ src: url(font-path('KaTeX_Size4-Regular.eot'));
+ src: url(font-path('KaTeX_Size4-Regular.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_Size4-Regular.woff2')) format('woff2'), url(font-path('KaTeX_Size4-Regular.woff')) format('woff'), url(font-path('KaTeX_Size4-Regular.ttf')) format('truetype');
+ font-weight: normal;
+ font-style: normal;
+}
+@font-face {
+ font-family: 'KaTeX_Typewriter';
+ src: url(font-path('KaTeX_Typewriter-Regular.eot'));
+ src: url(font-path('KaTeX_Typewriter-Regular.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_Typewriter-Regular.woff2')) format('woff2'), url(font-path('KaTeX_Typewriter-Regular.woff')) format('woff'), url(font-path('KaTeX_Typewriter-Regular.ttf')) format('truetype');
+ font-weight: normal;
+ font-style: normal;
+}
+.katex-display {
+ display: block;
+ margin: 1em 0;
+ text-align: center;
+}
+.katex-display > .katex {
+ display: inline-block;
+ text-align: initial;
+}
+.katex {
+ font: normal 1.21em KaTeX_Main, Times New Roman, serif;
+ line-height: 1.2;
+ white-space: nowrap;
+ text-indent: 0;
+}
+.katex .katex-html {
+ display: inline-block;
+}
+.katex .katex-mathml {
+ position: absolute;
+ clip: rect(1px, 1px, 1px, 1px);
+ padding: 0;
+ border: 0;
+ height: 1px;
+ width: 1px;
+ overflow: hidden;
+}
+.katex .base {
+ display: inline-block;
+}
+.katex .strut {
+ display: inline-block;
+}
+.katex .mathit {
+ font-family: KaTeX_Math;
+ font-style: italic;
+}
+.katex .mathbf {
+ font-family: KaTeX_Main;
+ font-weight: bold;
+}
+.katex .amsrm {
+ font-family: KaTeX_AMS;
+}
+.katex .mathbb {
+ font-family: KaTeX_AMS;
+}
+.katex .mathcal {
+ font-family: KaTeX_Caligraphic;
+}
+.katex .mathfrak {
+ font-family: KaTeX_Fraktur;
+}
+.katex .mathtt {
+ font-family: KaTeX_Typewriter;
+}
+.katex .mathscr {
+ font-family: KaTeX_Script;
+}
+.katex .mathsf {
+ font-family: KaTeX_SansSerif;
+}
+.katex .mainit {
+ font-family: KaTeX_Main;
+ font-style: italic;
+}
+.katex .textstyle > .mord + .mop {
+ margin-left: 0.16667em;
+}
+.katex .textstyle > .mord + .mbin {
+ margin-left: 0.22222em;
+}
+.katex .textstyle > .mord + .mrel {
+ margin-left: 0.27778em;
+}
+.katex .textstyle > .mord + .minner {
+ margin-left: 0.16667em;
+}
+.katex .textstyle > .mop + .mord {
+ margin-left: 0.16667em;
+}
+.katex .textstyle > .mop + .mop {
+ margin-left: 0.16667em;
+}
+.katex .textstyle > .mop + .mrel {
+ margin-left: 0.27778em;
+}
+.katex .textstyle > .mop + .minner {
+ margin-left: 0.16667em;
+}
+.katex .textstyle > .mbin + .mord {
+ margin-left: 0.22222em;
+}
+.katex .textstyle > .mbin + .mop {
+ margin-left: 0.22222em;
+}
+.katex .textstyle > .mbin + .mopen {
+ margin-left: 0.22222em;
+}
+.katex .textstyle > .mbin + .minner {
+ margin-left: 0.22222em;
+}
+.katex .textstyle > .mrel + .mord {
+ margin-left: 0.27778em;
+}
+.katex .textstyle > .mrel + .mop {
+ margin-left: 0.27778em;
+}
+.katex .textstyle > .mrel + .mopen {
+ margin-left: 0.27778em;
+}
+.katex .textstyle > .mrel + .minner {
+ margin-left: 0.27778em;
+}
+.katex .textstyle > .mclose + .mop {
+ margin-left: 0.16667em;
+}
+.katex .textstyle > .mclose + .mbin {
+ margin-left: 0.22222em;
+}
+.katex .textstyle > .mclose + .mrel {
+ margin-left: 0.27778em;
+}
+.katex .textstyle > .mclose + .minner {
+ margin-left: 0.16667em;
+}
+.katex .textstyle > .mpunct + .mord {
+ margin-left: 0.16667em;
+}
+.katex .textstyle > .mpunct + .mop {
+ margin-left: 0.16667em;
+}
+.katex .textstyle > .mpunct + .mrel {
+ margin-left: 0.16667em;
+}
+.katex .textstyle > .mpunct + .mopen {
+ margin-left: 0.16667em;
+}
+.katex .textstyle > .mpunct + .mclose {
+ margin-left: 0.16667em;
+}
+.katex .textstyle > .mpunct + .mpunct {
+ margin-left: 0.16667em;
+}
+.katex .textstyle > .mpunct + .minner {
+ margin-left: 0.16667em;
+}
+.katex .textstyle > .minner + .mord {
+ margin-left: 0.16667em;
+}
+.katex .textstyle > .minner + .mop {
+ margin-left: 0.16667em;
+}
+.katex .textstyle > .minner + .mbin {
+ margin-left: 0.22222em;
+}
+.katex .textstyle > .minner + .mrel {
+ margin-left: 0.27778em;
+}
+.katex .textstyle > .minner + .mopen {
+ margin-left: 0.16667em;
+}
+.katex .textstyle > .minner + .mpunct {
+ margin-left: 0.16667em;
+}
+.katex .textstyle > .minner + .minner {
+ margin-left: 0.16667em;
+}
+.katex .mord + .mop {
+ margin-left: 0.16667em;
+}
+.katex .mop + .mord {
+ margin-left: 0.16667em;
+}
+.katex .mop + .mop {
+ margin-left: 0.16667em;
+}
+.katex .mclose + .mop {
+ margin-left: 0.16667em;
+}
+.katex .minner + .mop {
+ margin-left: 0.16667em;
+}
+.katex .reset-textstyle.textstyle {
+ font-size: 1em;
+}
+.katex .reset-textstyle.scriptstyle {
+ font-size: 0.7em;
+}
+.katex .reset-textstyle.scriptscriptstyle {
+ font-size: 0.5em;
+}
+.katex .reset-scriptstyle.textstyle {
+ font-size: 1.42857em;
+}
+.katex .reset-scriptstyle.scriptstyle {
+ font-size: 1em;
+}
+.katex .reset-scriptstyle.scriptscriptstyle {
+ font-size: 0.71429em;
+}
+.katex .reset-scriptscriptstyle.textstyle {
+ font-size: 2em;
+}
+.katex .reset-scriptscriptstyle.scriptstyle {
+ font-size: 1.4em;
+}
+.katex .reset-scriptscriptstyle.scriptscriptstyle {
+ font-size: 1em;
+}
+.katex .style-wrap {
+ position: relative;
+}
+.katex .vlist {
+ display: inline-block;
+}
+.katex .vlist > span {
+ display: block;
+ height: 0;
+ position: relative;
+}
+.katex .vlist > span > span {
+ display: inline-block;
+}
+.katex .vlist .baseline-fix {
+ display: inline-table;
+ table-layout: fixed;
+}
+.katex .msupsub {
+ text-align: left;
+}
+.katex .mfrac > span > span {
+ text-align: center;
+}
+.katex .mfrac .frac-line {
+ width: 100%;
+}
+.katex .mfrac .frac-line:before {
+ border-bottom-style: solid;
+ border-bottom-width: 1px;
+ content: "";
+ display: block;
+}
+.katex .mfrac .frac-line:after {
+ border-bottom-style: solid;
+ border-bottom-width: 0.04em;
+ content: "";
+ display: block;
+ margin-top: -1px;
+}
+.katex .mspace {
+ display: inline-block;
+}
+.katex .mspace.negativethinspace {
+ margin-left: -0.16667em;
+}
+.katex .mspace.thinspace {
+ width: 0.16667em;
+}
+.katex .mspace.mediumspace {
+ width: 0.22222em;
+}
+.katex .mspace.thickspace {
+ width: 0.27778em;
+}
+.katex .mspace.enspace {
+ width: 0.5em;
+}
+.katex .mspace.quad {
+ width: 1em;
+}
+.katex .mspace.qquad {
+ width: 2em;
+}
+.katex .llap,
+.katex .rlap {
+ width: 0;
+ position: relative;
+}
+.katex .llap > .inner,
+.katex .rlap > .inner {
+ position: absolute;
+}
+.katex .llap > .fix,
+.katex .rlap > .fix {
+ display: inline-block;
+}
+.katex .llap > .inner {
+ right: 0;
+}
+.katex .rlap > .inner {
+ left: 0;
+}
+.katex .katex-logo .a {
+ font-size: 0.75em;
+ margin-left: -0.32em;
+ position: relative;
+ top: -0.2em;
+}
+.katex .katex-logo .t {
+ margin-left: -0.23em;
+}
+.katex .katex-logo .e {
+ margin-left: -0.1667em;
+ position: relative;
+ top: 0.2155em;
+}
+.katex .katex-logo .x {
+ margin-left: -0.125em;
+}
+.katex .rule {
+ display: inline-block;
+ border: solid 0;
+ position: relative;
+}
+.katex .overline .overline-line,
+.katex .underline .underline-line {
+ width: 100%;
+}
+.katex .overline .overline-line:before,
+.katex .underline .underline-line:before {
+ border-bottom-style: solid;
+ border-bottom-width: 1px;
+ content: "";
+ display: block;
+}
+.katex .overline .overline-line:after,
+.katex .underline .underline-line:after {
+ border-bottom-style: solid;
+ border-bottom-width: 0.04em;
+ content: "";
+ display: block;
+ margin-top: -1px;
+}
+.katex .sqrt > .sqrt-sign {
+ position: relative;
+}
+.katex .sqrt .sqrt-line {
+ width: 100%;
+}
+.katex .sqrt .sqrt-line:before {
+ border-bottom-style: solid;
+ border-bottom-width: 1px;
+ content: "";
+ display: block;
+}
+.katex .sqrt .sqrt-line:after {
+ border-bottom-style: solid;
+ border-bottom-width: 0.04em;
+ content: "";
+ display: block;
+ margin-top: -1px;
+}
+.katex .sqrt > .root {
+ margin-left: 0.27777778em;
+ margin-right: -0.55555556em;
+}
+.katex .sizing,
+.katex .fontsize-ensurer {
+ display: inline-block;
+}
+.katex .sizing.reset-size1.size1,
+.katex .fontsize-ensurer.reset-size1.size1 {
+ font-size: 1em;
+}
+.katex .sizing.reset-size1.size2,
+.katex .fontsize-ensurer.reset-size1.size2 {
+ font-size: 1.4em;
+}
+.katex .sizing.reset-size1.size3,
+.katex .fontsize-ensurer.reset-size1.size3 {
+ font-size: 1.6em;
+}
+.katex .sizing.reset-size1.size4,
+.katex .fontsize-ensurer.reset-size1.size4 {
+ font-size: 1.8em;
+}
+.katex .sizing.reset-size1.size5,
+.katex .fontsize-ensurer.reset-size1.size5 {
+ font-size: 2em;
+}
+.katex .sizing.reset-size1.size6,
+.katex .fontsize-ensurer.reset-size1.size6 {
+ font-size: 2.4em;
+}
+.katex .sizing.reset-size1.size7,
+.katex .fontsize-ensurer.reset-size1.size7 {
+ font-size: 2.88em;
+}
+.katex .sizing.reset-size1.size8,
+.katex .fontsize-ensurer.reset-size1.size8 {
+ font-size: 3.46em;
+}
+.katex .sizing.reset-size1.size9,
+.katex .fontsize-ensurer.reset-size1.size9 {
+ font-size: 4.14em;
+}
+.katex .sizing.reset-size1.size10,
+.katex .fontsize-ensurer.reset-size1.size10 {
+ font-size: 4.98em;
+}
+.katex .sizing.reset-size2.size1,
+.katex .fontsize-ensurer.reset-size2.size1 {
+ font-size: 0.71428571em;
+}
+.katex .sizing.reset-size2.size2,
+.katex .fontsize-ensurer.reset-size2.size2 {
+ font-size: 1em;
+}
+.katex .sizing.reset-size2.size3,
+.katex .fontsize-ensurer.reset-size2.size3 {
+ font-size: 1.14285714em;
+}
+.katex .sizing.reset-size2.size4,
+.katex .fontsize-ensurer.reset-size2.size4 {
+ font-size: 1.28571429em;
+}
+.katex .sizing.reset-size2.size5,
+.katex .fontsize-ensurer.reset-size2.size5 {
+ font-size: 1.42857143em;
+}
+.katex .sizing.reset-size2.size6,
+.katex .fontsize-ensurer.reset-size2.size6 {
+ font-size: 1.71428571em;
+}
+.katex .sizing.reset-size2.size7,
+.katex .fontsize-ensurer.reset-size2.size7 {
+ font-size: 2.05714286em;
+}
+.katex .sizing.reset-size2.size8,
+.katex .fontsize-ensurer.reset-size2.size8 {
+ font-size: 2.47142857em;
+}
+.katex .sizing.reset-size2.size9,
+.katex .fontsize-ensurer.reset-size2.size9 {
+ font-size: 2.95714286em;
+}
+.katex .sizing.reset-size2.size10,
+.katex .fontsize-ensurer.reset-size2.size10 {
+ font-size: 3.55714286em;
+}
+.katex .sizing.reset-size3.size1,
+.katex .fontsize-ensurer.reset-size3.size1 {
+ font-size: 0.625em;
+}
+.katex .sizing.reset-size3.size2,
+.katex .fontsize-ensurer.reset-size3.size2 {
+ font-size: 0.875em;
+}
+.katex .sizing.reset-size3.size3,
+.katex .fontsize-ensurer.reset-size3.size3 {
+ font-size: 1em;
+}
+.katex .sizing.reset-size3.size4,
+.katex .fontsize-ensurer.reset-size3.size4 {
+ font-size: 1.125em;
+}
+.katex .sizing.reset-size3.size5,
+.katex .fontsize-ensurer.reset-size3.size5 {
+ font-size: 1.25em;
+}
+.katex .sizing.reset-size3.size6,
+.katex .fontsize-ensurer.reset-size3.size6 {
+ font-size: 1.5em;
+}
+.katex .sizing.reset-size3.size7,
+.katex .fontsize-ensurer.reset-size3.size7 {
+ font-size: 1.8em;
+}
+.katex .sizing.reset-size3.size8,
+.katex .fontsize-ensurer.reset-size3.size8 {
+ font-size: 2.1625em;
+}
+.katex .sizing.reset-size3.size9,
+.katex .fontsize-ensurer.reset-size3.size9 {
+ font-size: 2.5875em;
+}
+.katex .sizing.reset-size3.size10,
+.katex .fontsize-ensurer.reset-size3.size10 {
+ font-size: 3.1125em;
+}
+.katex .sizing.reset-size4.size1,
+.katex .fontsize-ensurer.reset-size4.size1 {
+ font-size: 0.55555556em;
+}
+.katex .sizing.reset-size4.size2,
+.katex .fontsize-ensurer.reset-size4.size2 {
+ font-size: 0.77777778em;
+}
+.katex .sizing.reset-size4.size3,
+.katex .fontsize-ensurer.reset-size4.size3 {
+ font-size: 0.88888889em;
+}
+.katex .sizing.reset-size4.size4,
+.katex .fontsize-ensurer.reset-size4.size4 {
+ font-size: 1em;
+}
+.katex .sizing.reset-size4.size5,
+.katex .fontsize-ensurer.reset-size4.size5 {
+ font-size: 1.11111111em;
+}
+.katex .sizing.reset-size4.size6,
+.katex .fontsize-ensurer.reset-size4.size6 {
+ font-size: 1.33333333em;
+}
+.katex .sizing.reset-size4.size7,
+.katex .fontsize-ensurer.reset-size4.size7 {
+ font-size: 1.6em;
+}
+.katex .sizing.reset-size4.size8,
+.katex .fontsize-ensurer.reset-size4.size8 {
+ font-size: 1.92222222em;
+}
+.katex .sizing.reset-size4.size9,
+.katex .fontsize-ensurer.reset-size4.size9 {
+ font-size: 2.3em;
+}
+.katex .sizing.reset-size4.size10,
+.katex .fontsize-ensurer.reset-size4.size10 {
+ font-size: 2.76666667em;
+}
+.katex .sizing.reset-size5.size1,
+.katex .fontsize-ensurer.reset-size5.size1 {
+ font-size: 0.5em;
+}
+.katex .sizing.reset-size5.size2,
+.katex .fontsize-ensurer.reset-size5.size2 {
+ font-size: 0.7em;
+}
+.katex .sizing.reset-size5.size3,
+.katex .fontsize-ensurer.reset-size5.size3 {
+ font-size: 0.8em;
+}
+.katex .sizing.reset-size5.size4,
+.katex .fontsize-ensurer.reset-size5.size4 {
+ font-size: 0.9em;
+}
+.katex .sizing.reset-size5.size5,
+.katex .fontsize-ensurer.reset-size5.size5 {
+ font-size: 1em;
+}
+.katex .sizing.reset-size5.size6,
+.katex .fontsize-ensurer.reset-size5.size6 {
+ font-size: 1.2em;
+}
+.katex .sizing.reset-size5.size7,
+.katex .fontsize-ensurer.reset-size5.size7 {
+ font-size: 1.44em;
+}
+.katex .sizing.reset-size5.size8,
+.katex .fontsize-ensurer.reset-size5.size8 {
+ font-size: 1.73em;
+}
+.katex .sizing.reset-size5.size9,
+.katex .fontsize-ensurer.reset-size5.size9 {
+ font-size: 2.07em;
+}
+.katex .sizing.reset-size5.size10,
+.katex .fontsize-ensurer.reset-size5.size10 {
+ font-size: 2.49em;
+}
+.katex .sizing.reset-size6.size1,
+.katex .fontsize-ensurer.reset-size6.size1 {
+ font-size: 0.41666667em;
+}
+.katex .sizing.reset-size6.size2,
+.katex .fontsize-ensurer.reset-size6.size2 {
+ font-size: 0.58333333em;
+}
+.katex .sizing.reset-size6.size3,
+.katex .fontsize-ensurer.reset-size6.size3 {
+ font-size: 0.66666667em;
+}
+.katex .sizing.reset-size6.size4,
+.katex .fontsize-ensurer.reset-size6.size4 {
+ font-size: 0.75em;
+}
+.katex .sizing.reset-size6.size5,
+.katex .fontsize-ensurer.reset-size6.size5 {
+ font-size: 0.83333333em;
+}
+.katex .sizing.reset-size6.size6,
+.katex .fontsize-ensurer.reset-size6.size6 {
+ font-size: 1em;
+}
+.katex .sizing.reset-size6.size7,
+.katex .fontsize-ensurer.reset-size6.size7 {
+ font-size: 1.2em;
+}
+.katex .sizing.reset-size6.size8,
+.katex .fontsize-ensurer.reset-size6.size8 {
+ font-size: 1.44166667em;
+}
+.katex .sizing.reset-size6.size9,
+.katex .fontsize-ensurer.reset-size6.size9 {
+ font-size: 1.725em;
+}
+.katex .sizing.reset-size6.size10,
+.katex .fontsize-ensurer.reset-size6.size10 {
+ font-size: 2.075em;
+}
+.katex .sizing.reset-size7.size1,
+.katex .fontsize-ensurer.reset-size7.size1 {
+ font-size: 0.34722222em;
+}
+.katex .sizing.reset-size7.size2,
+.katex .fontsize-ensurer.reset-size7.size2 {
+ font-size: 0.48611111em;
+}
+.katex .sizing.reset-size7.size3,
+.katex .fontsize-ensurer.reset-size7.size3 {
+ font-size: 0.55555556em;
+}
+.katex .sizing.reset-size7.size4,
+.katex .fontsize-ensurer.reset-size7.size4 {
+ font-size: 0.625em;
+}
+.katex .sizing.reset-size7.size5,
+.katex .fontsize-ensurer.reset-size7.size5 {
+ font-size: 0.69444444em;
+}
+.katex .sizing.reset-size7.size6,
+.katex .fontsize-ensurer.reset-size7.size6 {
+ font-size: 0.83333333em;
+}
+.katex .sizing.reset-size7.size7,
+.katex .fontsize-ensurer.reset-size7.size7 {
+ font-size: 1em;
+}
+.katex .sizing.reset-size7.size8,
+.katex .fontsize-ensurer.reset-size7.size8 {
+ font-size: 1.20138889em;
+}
+.katex .sizing.reset-size7.size9,
+.katex .fontsize-ensurer.reset-size7.size9 {
+ font-size: 1.4375em;
+}
+.katex .sizing.reset-size7.size10,
+.katex .fontsize-ensurer.reset-size7.size10 {
+ font-size: 1.72916667em;
+}
+.katex .sizing.reset-size8.size1,
+.katex .fontsize-ensurer.reset-size8.size1 {
+ font-size: 0.28901734em;
+}
+.katex .sizing.reset-size8.size2,
+.katex .fontsize-ensurer.reset-size8.size2 {
+ font-size: 0.40462428em;
+}
+.katex .sizing.reset-size8.size3,
+.katex .fontsize-ensurer.reset-size8.size3 {
+ font-size: 0.46242775em;
+}
+.katex .sizing.reset-size8.size4,
+.katex .fontsize-ensurer.reset-size8.size4 {
+ font-size: 0.52023121em;
+}
+.katex .sizing.reset-size8.size5,
+.katex .fontsize-ensurer.reset-size8.size5 {
+ font-size: 0.57803468em;
+}
+.katex .sizing.reset-size8.size6,
+.katex .fontsize-ensurer.reset-size8.size6 {
+ font-size: 0.69364162em;
+}
+.katex .sizing.reset-size8.size7,
+.katex .fontsize-ensurer.reset-size8.size7 {
+ font-size: 0.83236994em;
+}
+.katex .sizing.reset-size8.size8,
+.katex .fontsize-ensurer.reset-size8.size8 {
+ font-size: 1em;
+}
+.katex .sizing.reset-size8.size9,
+.katex .fontsize-ensurer.reset-size8.size9 {
+ font-size: 1.19653179em;
+}
+.katex .sizing.reset-size8.size10,
+.katex .fontsize-ensurer.reset-size8.size10 {
+ font-size: 1.43930636em;
+}
+.katex .sizing.reset-size9.size1,
+.katex .fontsize-ensurer.reset-size9.size1 {
+ font-size: 0.24154589em;
+}
+.katex .sizing.reset-size9.size2,
+.katex .fontsize-ensurer.reset-size9.size2 {
+ font-size: 0.33816425em;
+}
+.katex .sizing.reset-size9.size3,
+.katex .fontsize-ensurer.reset-size9.size3 {
+ font-size: 0.38647343em;
+}
+.katex .sizing.reset-size9.size4,
+.katex .fontsize-ensurer.reset-size9.size4 {
+ font-size: 0.43478261em;
+}
+.katex .sizing.reset-size9.size5,
+.katex .fontsize-ensurer.reset-size9.size5 {
+ font-size: 0.48309179em;
+}
+.katex .sizing.reset-size9.size6,
+.katex .fontsize-ensurer.reset-size9.size6 {
+ font-size: 0.57971014em;
+}
+.katex .sizing.reset-size9.size7,
+.katex .fontsize-ensurer.reset-size9.size7 {
+ font-size: 0.69565217em;
+}
+.katex .sizing.reset-size9.size8,
+.katex .fontsize-ensurer.reset-size9.size8 {
+ font-size: 0.83574879em;
+}
+.katex .sizing.reset-size9.size9,
+.katex .fontsize-ensurer.reset-size9.size9 {
+ font-size: 1em;
+}
+.katex .sizing.reset-size9.size10,
+.katex .fontsize-ensurer.reset-size9.size10 {
+ font-size: 1.20289855em;
+}
+.katex .sizing.reset-size10.size1,
+.katex .fontsize-ensurer.reset-size10.size1 {
+ font-size: 0.20080321em;
+}
+.katex .sizing.reset-size10.size2,
+.katex .fontsize-ensurer.reset-size10.size2 {
+ font-size: 0.2811245em;
+}
+.katex .sizing.reset-size10.size3,
+.katex .fontsize-ensurer.reset-size10.size3 {
+ font-size: 0.32128514em;
+}
+.katex .sizing.reset-size10.size4,
+.katex .fontsize-ensurer.reset-size10.size4 {
+ font-size: 0.36144578em;
+}
+.katex .sizing.reset-size10.size5,
+.katex .fontsize-ensurer.reset-size10.size5 {
+ font-size: 0.40160643em;
+}
+.katex .sizing.reset-size10.size6,
+.katex .fontsize-ensurer.reset-size10.size6 {
+ font-size: 0.48192771em;
+}
+.katex .sizing.reset-size10.size7,
+.katex .fontsize-ensurer.reset-size10.size7 {
+ font-size: 0.57831325em;
+}
+.katex .sizing.reset-size10.size8,
+.katex .fontsize-ensurer.reset-size10.size8 {
+ font-size: 0.69477912em;
+}
+.katex .sizing.reset-size10.size9,
+.katex .fontsize-ensurer.reset-size10.size9 {
+ font-size: 0.8313253em;
+}
+.katex .sizing.reset-size10.size10,
+.katex .fontsize-ensurer.reset-size10.size10 {
+ font-size: 1em;
+}
+.katex .delimsizing.size1 {
+ font-family: KaTeX_Size1;
+}
+.katex .delimsizing.size2 {
+ font-family: KaTeX_Size2;
+}
+.katex .delimsizing.size3 {
+ font-family: KaTeX_Size3;
+}
+.katex .delimsizing.size4 {
+ font-family: KaTeX_Size4;
+}
+.katex .delimsizing.mult .delim-size1 > span {
+ font-family: KaTeX_Size1;
+}
+.katex .delimsizing.mult .delim-size4 > span {
+ font-family: KaTeX_Size4;
+}
+.katex .nulldelimiter {
+ display: inline-block;
+ width: 0.12em;
+}
+.katex .op-symbol {
+ position: relative;
+}
+.katex .op-symbol.small-op {
+ font-family: KaTeX_Size1;
+}
+.katex .op-symbol.large-op {
+ font-family: KaTeX_Size2;
+}
+.katex .op-limits > .vlist > span {
+ text-align: center;
+}
+.katex .accent > .vlist > span {
+ text-align: center;
+}
+.katex .accent .accent-body > span {
+ width: 0;
+}
+.katex .accent .accent-body.accent-vec > span {
+ position: relative;
+ left: 0.326em;
+}
+.katex .mtable .vertical-separator {
+ display: inline-block;
+ margin: 0 -0.025em;
+ border-right: 0.05em solid black;
+}
+.katex .mtable .arraycolsep {
+ display: inline-block;
+}
+.katex .mtable .col-align-c > .vlist {
+ text-align: center;
+}
+.katex .mtable .col-align-l > .vlist {
+ text-align: left;
+}
+.katex .mtable .col-align-r > .vlist {
+ text-align: right;
+}
diff --git a/vendor/gitignore/Android.gitignore b/vendor/gitignore/Android.gitignore
index 935ceef0680..a1a65c2d72e 100644
--- a/vendor/gitignore/Android.gitignore
+++ b/vendor/gitignore/Android.gitignore
@@ -35,6 +35,8 @@ captures/
# Intellij
*.iml
.idea/workspace.xml
+.idea/tasks.xml
+.idea/gradle.xml
.idea/libraries
# Keystore files
@@ -42,3 +44,11 @@ captures/
# External native build folder generated in Android Studio 2.2 and later
.externalNativeBuild
+
+# Google Services (e.g. APIs or Firebase)
+google-services.json
+
+#Freeline
+freeline.py
+freeline/
+freeline_project_description.json
diff --git a/vendor/gitignore/Autotools.gitignore b/vendor/gitignore/Autotools.gitignore
index 1e9158e2a85..e3923f96fce 100644
--- a/vendor/gitignore/Autotools.gitignore
+++ b/vendor/gitignore/Autotools.gitignore
@@ -1,6 +1,11 @@
# http://www.gnu.org/software/automake
Makefile.in
+/ar-lib
+/mdate-sh
+/py-compile
+/test-driver
+/ylwrap
# http://www.gnu.org/software/autoconf
@@ -9,10 +14,20 @@ Makefile.in
/autoscan-*.log
/aclocal.m4
/compile
+/config.guess
/config.h.in
+/config.sub
/configure
/configure.scan
/depcomp
/install-sh
/missing
/stamp-h1
+
+# https://www.gnu.org/software/libtool/
+
+/ltmain.sh
+
+# http://www.gnu.org/software/texinfo
+
+/texinfo.tex
diff --git a/vendor/gitignore/CMake.gitignore b/vendor/gitignore/CMake.gitignore
index 0cc7e4b5275..9ea395f15ee 100644
--- a/vendor/gitignore/CMake.gitignore
+++ b/vendor/gitignore/CMake.gitignore
@@ -1,7 +1,9 @@
CMakeCache.txt
CMakeFiles
CMakeScripts
+Testing
Makefile
cmake_install.cmake
install_manifest.txt
+compile_commands.json
CTestTestfile.cmake
diff --git a/vendor/gitignore/CodeIgniter.gitignore b/vendor/gitignore/CodeIgniter.gitignore
index 0f77d9e1d17..bfea17cdc5b 100644
--- a/vendor/gitignore/CodeIgniter.gitignore
+++ b/vendor/gitignore/CodeIgniter.gitignore
@@ -4,3 +4,14 @@
*/cache/*
!*/cache/index.html
!*/cache/.htaccess
+
+user_guide_src/build/*
+user_guide_src/cilexer/build/*
+user_guide_src/cilexer/dist/*
+user_guide_src/cilexer/pycilexer.egg-info/*
+
+#codeigniter 3
+application/logs/*
+!application/logs/index.html
+!application/logs/.htaccess
+/vendor/
diff --git a/vendor/gitignore/CommonLisp.gitignore b/vendor/gitignore/CommonLisp.gitignore
index 4806e580b60..e7de127b014 100644
--- a/vendor/gitignore/CommonLisp.gitignore
+++ b/vendor/gitignore/CommonLisp.gitignore
@@ -1,3 +1,17 @@
*.FASL
*.fasl
*.lisp-temp
+*.dfsl
+*.pfsl
+*.d64fsl
+*.p64fsl
+*.lx64fsl
+*.lx32fsl
+*.dx64fsl
+*.dx32fsl
+*.fx64fsl
+*.fx32fsl
+*.sx64fsl
+*.sx32fsl
+*.wx64fsl
+*.wx32fsl
diff --git a/vendor/gitignore/Coq.gitignore b/vendor/gitignore/Coq.gitignore
index d3083b3a605..f25a61d9964 100644
--- a/vendor/gitignore/Coq.gitignore
+++ b/vendor/gitignore/Coq.gitignore
@@ -1,3 +1,30 @@
-*.vo
+.*.aux
+*.a
+*.cma
+*.cmi
+*.cmo
+*.cmx
+*.cmxa
+*.cmxs
*.glob
+*.ml.d
+*.ml4.d
+*.mli.d
+*.mllib.d
+*.mlpack.d
+*.native
+*.o
*.v.d
+*.vio
+*.vo
+.coq-native/
+.csdp.cache
+.lia.cache
+.nia.cache
+.nlia.cache
+.nra.cache
+csdp.cache
+lia.cache
+nia.cache
+nlia.cache
+nra.cache
diff --git a/vendor/gitignore/Dart.gitignore b/vendor/gitignore/Dart.gitignore
index 7c280441649..4b366585ddc 100644
--- a/vendor/gitignore/Dart.gitignore
+++ b/vendor/gitignore/Dart.gitignore
@@ -1,13 +1,19 @@
# See https://www.dartlang.org/tools/private-files.html
# Files and directories created by pub
-.buildlog
+
+# SDK 1.20 and later (no longer creates packages directories)
.packages
-.project
.pub/
build/
+
+# Older SDK versions
+# (Include if the minimum SDK version specified in pubsepc.yaml is earlier than 1.20)
+.project
+.buildlog
**/packages/
+
# Files created by dart2js
# (Most Dart developers will use pub build to compile Dart, use/modify these
# rules if you intend to use dart2js directly
diff --git a/vendor/gitignore/Elisp.gitignore b/vendor/gitignore/Elisp.gitignore
index 9b4291b7fe8..206569dc661 100644
--- a/vendor/gitignore/Elisp.gitignore
+++ b/vendor/gitignore/Elisp.gitignore
@@ -3,3 +3,9 @@
# Packaging
.cask
+
+# Backup files
+*~
+
+# Undo-tree save-files
+*.~undo-tree
diff --git a/vendor/gitignore/Elixir.gitignore b/vendor/gitignore/Elixir.gitignore
index 755b605549d..ac67aaf3243 100644
--- a/vendor/gitignore/Elixir.gitignore
+++ b/vendor/gitignore/Elixir.gitignore
@@ -3,3 +3,4 @@
/deps
erl_crash.dump
*.ez
+*.beam
diff --git a/vendor/gitignore/Global/Emacs.gitignore b/vendor/gitignore/Global/Emacs.gitignore
index 0c96c9ad060..3ac7904dcd2 100644
--- a/vendor/gitignore/Global/Emacs.gitignore
+++ b/vendor/gitignore/Global/Emacs.gitignore
@@ -39,4 +39,7 @@ flycheck_*.el
/server/
# projectiles files
-.projectile \ No newline at end of file
+.projectile
+
+# directory configuration
+.dir-locals.el
diff --git a/vendor/gitignore/Global/IPythonNotebook.gitignore b/vendor/gitignore/Global/IPythonNotebook.gitignore
deleted file mode 100644
index 27c13510bf5..00000000000
--- a/vendor/gitignore/Global/IPythonNotebook.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-# Temporary data
-.ipynb_checkpoints/
diff --git a/vendor/gitignore/Global/JetBrains.gitignore b/vendor/gitignore/Global/JetBrains.gitignore
index 0a254147875..401fee15748 100644
--- a/vendor/gitignore/Global/JetBrains.gitignore
+++ b/vendor/gitignore/Global/JetBrains.gitignore
@@ -2,23 +2,24 @@
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff:
-.idea/workspace.xml
-.idea/tasks.xml
+.idea/**/workspace.xml
+.idea/**/tasks.xml
# Sensitive or high-churn files:
-.idea/dataSources.ids
-.idea/dataSources.xml
-.idea/dataSources.local.xml
-.idea/sqlDataSources.xml
-.idea/dynamic.xml
-.idea/uiDesigner.xml
+.idea/**/dataSources/
+.idea/**/dataSources.ids
+.idea/**/dataSources.xml
+.idea/**/dataSources.local.xml
+.idea/**/sqlDataSources.xml
+.idea/**/dynamic.xml
+.idea/**/uiDesigner.xml
# Gradle:
-.idea/gradle.xml
-.idea/libraries
+.idea/**/gradle.xml
+.idea/**/libraries
# Mongo Explorer plugin:
-.idea/mongoSettings.xml
+.idea/**/mongoSettings.xml
## File-based project format:
*.iws
diff --git a/vendor/gitignore/Global/Matlab.gitignore b/vendor/gitignore/Global/Matlab.gitignore
index 32a5ad4c777..09dfde64b5f 100644
--- a/vendor/gitignore/Global/Matlab.gitignore
+++ b/vendor/gitignore/Global/Matlab.gitignore
@@ -17,3 +17,6 @@ slprj/
# Session info
octave-workspace
+
+# Simulink autosave extension
+.autosave
diff --git a/vendor/gitignore/Global/Stata.gitignore b/vendor/gitignore/Global/Stata.gitignore
new file mode 100644
index 00000000000..07997bb1201
--- /dev/null
+++ b/vendor/gitignore/Global/Stata.gitignore
@@ -0,0 +1,24 @@
+# .gitignore file for git projects containing Stata files
+# Commercial statistical software: http://www.stata.com
+
+# Stata dataset and output files
+*.dta
+*.gph
+*.log
+*.smcl
+*.stpr
+*.stsem
+
+# Graphic export files from Stata
+# Stata command graph export: http://www.stata.com/manuals14/g-2graphexport.pdf
+#
+# You may add graphic export files to your .gitignore. However you should be
+# aware that this will exclude all image files from this main directory
+# and subdirectories.
+# *.ps
+# *.eps
+# *.wmf
+# *.emf
+# *.pdf
+# *.png
+# *.tif
diff --git a/vendor/gitignore/Global/SublimeText.gitignore b/vendor/gitignore/Global/SublimeText.gitignore
index 69c8c2b29ce..95ff2244c99 100644
--- a/vendor/gitignore/Global/SublimeText.gitignore
+++ b/vendor/gitignore/Global/SublimeText.gitignore
@@ -20,6 +20,9 @@ Package Control.ca-bundle
Package Control.system-ca-bundle
Package Control.cache/
Package Control.ca-certs/
+Package Control.merged-ca-bundle
+Package Control.user-ca-bundle
+oscrypto-ca-bundle.crt
bh_unicode_properties.cache
# Sublime-github package stores a github token in this file
diff --git a/vendor/gitignore/Global/Vim.gitignore b/vendor/gitignore/Global/Vim.gitignore
index bdc04a0b529..42e7afc1005 100644
--- a/vendor/gitignore/Global/Vim.gitignore
+++ b/vendor/gitignore/Global/Vim.gitignore
@@ -1,6 +1,8 @@
# swap
-[._]*.s[a-w][a-z]
-[._]s[a-w][a-z]
+[._]*.s[a-v][a-z]
+[._]*.sw[a-p]
+[._]s[a-v][a-z]
+[._]sw[a-p]
# session
Session.vim
# temporary
diff --git a/vendor/gitignore/Global/VisualStudioCode.gitignore b/vendor/gitignore/Global/VisualStudioCode.gitignore
index d9960081c98..0511e2b51f0 100644
--- a/vendor/gitignore/Global/VisualStudioCode.gitignore
+++ b/vendor/gitignore/Global/VisualStudioCode.gitignore
@@ -2,3 +2,4 @@
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
+!.vscode/extensions.json
diff --git a/vendor/gitignore/Global/Windows.gitignore b/vendor/gitignore/Global/Windows.gitignore
index a0d31452b0e..ba26afd9653 100644
--- a/vendor/gitignore/Global/Windows.gitignore
+++ b/vendor/gitignore/Global/Windows.gitignore
@@ -1,6 +1,7 @@
-# Windows image file caches
+# Windows thumbnail cache files
Thumbs.db
ehthumbs.db
+ehthumbs_vista.db
# Folder config file
Desktop.ini
diff --git a/vendor/gitignore/Go.gitignore b/vendor/gitignore/Go.gitignore
index 397a0ed4acb..a1338d68517 100644
--- a/vendor/gitignore/Go.gitignore
+++ b/vendor/gitignore/Go.gitignore
@@ -1,30 +1,14 @@
-# Compiled Object files, Static and Dynamic libs (Shared Objects)
-*.o
-*.a
+# Binaries for programs and plugins
+*.exe
+*.dll
*.so
+*.dylib
-# Folders
-_obj
-_test
-
-# Architecture specific extensions/prefixes
-*.[568vq]
-[568vq].out
-
-*.cgo1.go
-*.cgo2.c
-_cgo_defun.c
-_cgo_gotypes.go
-_cgo_export.*
-
-_testmain.go
-
-*.exe
+# Test binary, build with `go test -c`
*.test
-*.prof
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
-# external packages folder
-vendor/
+# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736
+.glide/
diff --git a/vendor/gitignore/Java.gitignore b/vendor/gitignore/Java.gitignore
index 32858aad3c3..dbb4a2dfa1a 100644
--- a/vendor/gitignore/Java.gitignore
+++ b/vendor/gitignore/Java.gitignore
@@ -1,5 +1,11 @@
*.class
+# Log file
+*.log
+
+# BlueJ files
+*.ctxt
+
# Mobile Tools for Java (J2ME)
.mtj.tmp/
@@ -7,6 +13,9 @@
*.jar
*.war
*.ear
+*.zip
+*.tar.gz
+*.rar
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
diff --git a/vendor/gitignore/Joomla.gitignore b/vendor/gitignore/Joomla.gitignore
index 93103fdbe77..53a74e74657 100644
--- a/vendor/gitignore/Joomla.gitignore
+++ b/vendor/gitignore/Joomla.gitignore
@@ -29,8 +29,6 @@
/administrator/components/com_search/*
/administrator/components/com_templates/*
/administrator/components/com_users/*
-/administrator/components/com_weblinks/*
-/administrator/components/index.html
/administrator/help/*
/administrator/includes/*
/administrator/language/en-GB/en-GB.com_ajax.ini
@@ -41,7 +39,6 @@
/administrator/language/en-GB/en-GB.com_joomlaupdate.sys.ini
/administrator/language/en-GB/en-GB.com_postinstall.ini
/administrator/language/en-GB/en-GB.com_postinstall.sys.ini
-/administrator/language/en-GB/en-GB.com_sitemapjen.sys.ini
/administrator/language/en-GB/en-GB.com_tags.ini
/administrator/language/en-GB/en-GB.com_tags.sys.ini
/administrator/language/en-GB/en-GB.mod_stats_admin.ini
@@ -250,15 +247,10 @@
/administrator/language/en-GB/en-GB.plg_user_joomla.sys.ini
/administrator/language/en-GB/en-GB.plg_user_profile.ini
/administrator/language/en-GB/en-GB.plg_user_profile.sys.ini
-/administrator/language/en-GB/en-GB.tpl_bluestork.ini
-/administrator/language/en-GB/en-GB.tpl_bluestork.sys.ini
/administrator/language/en-GB/en-GB.tpl_hathor.ini
/administrator/language/en-GB/en-GB.tpl_hathor.sys.ini
/administrator/language/en-GB/en-GB.xml
-/administrator/language/en-GB/index.html
-/administrator/language/ru-RU/index.html
/administrator/language/overrides/*
-/administrator/language/index.html
/administrator/logs/index.html
/administrator/manifests/*
/administrator/modules/mod_custom/*
@@ -278,12 +270,9 @@
/administrator/modules/mod_unread/*
/administrator/modules/mod_version/*
/administrator/modules/mod_stats_admin/*
-/administrator/modules/index.html
-/administrator/templates/bluestork/*
/administrator/templates/isis/*
/administrator/templates/hathor/*
/administrator/templates/system/*
-/administrator/templates/index.html
/administrator/index.php
/cache/*
/bin/*
@@ -302,7 +291,6 @@
/components/com_newsfeeds/*
/components/com_search/*
/components/com_users/*
-/components/com_weblinks/*
/components/com_wrapper/*
/components/index.html
/images/banners/*
@@ -403,7 +391,6 @@
/language/en-GB/en-GB.tpl_beez5.ini
/language/en-GB/en-GB.tpl_beez5.sys.ini
/language/en-GB/en-GB.xml
-/language/en-GB/index.html
/language/en-GB/install.xml
/language/overrides/*
/language/index.html
@@ -428,8 +415,6 @@
/libraries/index.html
/libraries/import.php
/libraries/loader.php
-/libraries/platform.php
-/logs/*
/media/cms/*
/media/com_contenthistory/*
/media/com_finder/*
@@ -472,7 +457,6 @@
/modules/mod_tags_popular/*
/modules/mod_tags_similar/*
/modules/mod_users_latest/*
-/modules/mod_weblinks/*
/modules/mod_whosonline/*
/modules/mod_wrapper/*
/modules/index.html
@@ -481,9 +465,7 @@
/plugins/authentication/joomla/*
/plugins/authentication/ldap/*
/plugins/authentication/cookie/*
-/plugins/authentication/index.html
/plugins/captcha/recaptcha/*
-/plugins/captcha/index.html
/plugins/content/emailcloak/*
/plugins/content/example/*
/plugins/content/finder/*
@@ -494,27 +476,21 @@
/plugins/content/pagenavigation/*
/plugins/content/vote/*
/plugins/content/contact/*
-/plugins/content/index.html
/plugins/editors/codemirror/*
/plugins/editors/none/*
/plugins/editors/tinymce/*
-/plugins/editors/index.html
/plugins/editors-xtd/module/*
/plugins/editors-xtd/article/*
/plugins/editors-xtd/image/*
/plugins/editors-xtd/pagebreak/*
/plugins/editors-xtd/readmore/*
-/plugins/editors-xtd/index.html
/plugins/extension/example/*
/plugins/extension/joomla/*
-/plugins/extension/index.html
-/plugins/finder/index.html
/plugins/finder/categories/*
/plugins/finder/contacts/*
/plugins/finder/content/*
/plugins/finder/newsfeeds/*
/plugins/finder/tags/*
-/plugins/finder/weblinks/*
/plugins/installer/*
/plugins/quickicon/extensionupdate/*
/plugins/quickicon/joomlaupdate/*
@@ -547,10 +523,7 @@
/plugins/user/profile/*
/plugins/user/index.html
/plugins/index.html
-/templates/atomic/*
/templates/beez3/*
-/templates/beez_20/*
-/templates/beez5/*
/templates/protostar/*
/templates/system/*
/templates/index.html
diff --git a/vendor/gitignore/KiCad.gitignore b/vendor/gitignore/KiCad.gitignore
index 606ed1c7b4d..208bc4fc591 100644
--- a/vendor/gitignore/KiCad.gitignore
+++ b/vendor/gitignore/KiCad.gitignore
@@ -13,7 +13,8 @@ _autosave-*
*.net
# Autorouter files (exported from Pcbnew)
-.dsn
+*.dsn
+*.ses
# Exported BOM files
*.xml
diff --git a/vendor/gitignore/Laravel.gitignore b/vendor/gitignore/Laravel.gitignore
index e7c594fa3e2..a4854bef534 100644
--- a/vendor/gitignore/Laravel.gitignore
+++ b/vendor/gitignore/Laravel.gitignore
@@ -1,16 +1,20 @@
vendor/
node_modules/
+npm-debug.log
# Laravel 4 specific
bootstrap/compiled.php
app/storage/
# Laravel 5 & Lumen specific
-bootstrap/cache/
public/storage
+public/hot
+storage/*.key
.env.*.php
.env.php
.env
+Homestead.yaml
+Homestead.json
# Rocketeer PHP task runner and deployment package. https://github.com/rocketeers/rocketeer
.rocketeer/
diff --git a/vendor/gitignore/Magento.gitignore b/vendor/gitignore/Magento.gitignore
index 195c9b7a029..b282f5cf547 100644
--- a/vendor/gitignore/Magento.gitignore
+++ b/vendor/gitignore/Magento.gitignore
@@ -1,104 +1,16 @@
-.htaccess.sample
-.modgit/
-.modman/
-app/code/community/Phoenix/Moneybookers/
-app/code/community/Cm/RedisSession/
-app/code/core/
-app/design/adminhtml/default/default/
-app/design/frontend/base/
-app/design/frontend/rwd/
-app/design/frontend/default/blank/
-app/design/frontend/default/default/
-app/design/frontend/default/iphone/
-app/design/frontend/default/modern/
-app/design/frontend/enterprise/default
-app/design/install/
-app/etc/modules/Enterprise_*
-app/etc/modules/Mage_*.xml
-app/etc/modules/Phoenix_Moneybookers.xml
-app/etc/modules/Cm_RedisSession.xml
-app/etc/applied.patches.list
-app/etc/config.xml
-app/etc/enterprise.xml
-app/etc/local.xml.additional
-app/etc/local.xml.template
-app/etc/local.xml
-app/.htaccess
-app/bootstrap.php
-app/locale/en_US/
-app/Mage.php
-/cron.php
-cron.sh
-dev/.htaccess
-dev/tests/functional/
-downloader/
-errors/
-favicon.ico
-/get.php
-includes/
-/index.php
-index.php.sample
-/install.php
-js/blank.html
-js/calendar/
-js/enterprise/
-js/extjs/
-js/firebug/
-js/flash/
-js/index.php
-js/jscolor/
-js/lib/
-js/mage/
-js/prototype/
-js/scriptaculous/
-js/spacer.gif
-js/tiny_mce/
-js/varien/
-lib/3Dsecure/
-lib/Apache/
-lib/flex/
-lib/googlecheckout/
-lib/.htaccess
-lib/LinLibertineFont/
-lib/Mage/
-lib/PEAR/
-lib/Pelago/
-lib/phpseclib/
-lib/Varien/
-lib/Zend/
-lib/Cm/
-lib/Credis/
-lib/Magento/
-LICENSE_AFL.txt
-LICENSE.html
-LICENSE.txt
-LICENSE_EE*
-/mage
-media/
-/api.php
-nbproject/
-pear
-pear/
-php.ini.sample
-pkginfo/
-RELEASE_NOTES.txt
-shell/.htaccess
-shell/abstract.php
-shell/compiler.php
-shell/indexer.php
-shell/log.php
-sitemap.xml
-skin/adminhtml/default/default/
-skin/adminhtml/default/enterprise
-skin/frontend/base/
-skin/frontend/rwd/
-skin/frontend/default/blank/
-skin/frontend/default/blue/
-skin/frontend/default/default/
-skin/frontend/default/french/
-skin/frontend/default/german/
-skin/frontend/default/iphone/
-skin/frontend/default/modern/
-skin/frontend/enterprise
-skin/install/
-var/
+#--------------------------#
+# Magento Default Files #
+#--------------------------#
+
+/app/etc/local.xml
+/media/*
+!/media/.htaccess
+!/media/customer/.htaccess
+!/media/dhl/logo.jpg
+!/media/downloadable/.htaccess
+!/media/xmlconnect/custom/ok.gif
+!/media/xmlconnect/original/ok.gif
+!/media/xmlconnect/system/ok.gif
+/var/*
+!/var/.htaccess
+!/var/package/*.xml
diff --git a/vendor/gitignore/Maven.gitignore b/vendor/gitignore/Maven.gitignore
index 1cdc9f7fd45..9af45b175ae 100644
--- a/vendor/gitignore/Maven.gitignore
+++ b/vendor/gitignore/Maven.gitignore
@@ -7,3 +7,6 @@ release.properties
dependency-reduced-pom.xml
buildNumber.properties
.mvn/timing.properties
+
+# Exclude maven wrapper
+!/.mvn/wrapper/maven-wrapper.jar
diff --git a/vendor/gitignore/Node.gitignore b/vendor/gitignore/Node.gitignore
index bc7fc55724c..38ac77e405e 100644
--- a/vendor/gitignore/Node.gitignore
+++ b/vendor/gitignore/Node.gitignore
@@ -21,6 +21,9 @@ coverage
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
+# Bower dependency directory (https://bower.io/)
+bower_components
+
# node-waf configuration
.lock-wscript
@@ -28,8 +31,11 @@ coverage
build/Release
# Dependency directories
-node_modules
-jspm_packages
+node_modules/
+jspm_packages/
+
+# Typescript v1 declaration files
+typings/
# Optional npm cache directory
.npm
@@ -42,3 +48,10 @@ jspm_packages
# Output of 'npm pack'
*.tgz
+
+# Yarn Integrity file
+.yarn-integrity
+
+# dotenv environment variables file
+.env
+
diff --git a/vendor/gitignore/Objective-C.gitignore b/vendor/gitignore/Objective-C.gitignore
index 58c51ecaed4..af90c007a3f 100644
--- a/vendor/gitignore/Objective-C.gitignore
+++ b/vendor/gitignore/Objective-C.gitignore
@@ -19,7 +19,8 @@ xcuserdata/
## Other
*.moved-aside
-*.xcuserstate
+*.xccheckout
+*.xcscmblueprint
## Obj-C/Swift specific
*.hmap
diff --git a/vendor/gitignore/Perl.gitignore b/vendor/gitignore/Perl.gitignore
index ae2ad536abb..9bf1537f6ae 100644
--- a/vendor/gitignore/Perl.gitignore
+++ b/vendor/gitignore/Perl.gitignore
@@ -1,20 +1,35 @@
-/blib/
-/.build/
-_build/
-cover_db/
-inc/
-Build
!Build/
-Build.bat
.last_cover_stats
-/Makefile
-/Makefile.old
-/MANIFEST.bak
/META.yml
/META.json
/MYMETA.*
-nytprof.out
-/pm_to_blib
*.o
+*.pm.tdy
*.bs
+
+# Devel::Cover
+cover_db/
+
+# Devel::NYTProf
+nytprof.out
+
+# Dizt::Zilla
+/.build/
+
+# Module::Build
+_build/
+Build
+Build.bat
+
+# Module::Install
+inc/
+
+# ExtUitls::MakeMaker
+/blib/
/_eumm/
+/*.gz
+/Makefile
+/Makefile.old
+/MANIFEST.bak
+/pm_to_blib
+/*.zip
diff --git a/vendor/gitignore/PureScript.gitignore b/vendor/gitignore/PureScript.gitignore
new file mode 100644
index 00000000000..361cf5277ba
--- /dev/null
+++ b/vendor/gitignore/PureScript.gitignore
@@ -0,0 +1,8 @@
+# Dependencies
+.psci_modules
+bower_components
+node_modules
+
+# Generated files
+.psci
+output
diff --git a/vendor/gitignore/Python.gitignore b/vendor/gitignore/Python.gitignore
index 6a2bf47ade9..cf3102d6b00 100644
--- a/vendor/gitignore/Python.gitignore
+++ b/vendor/gitignore/Python.gitignore
@@ -20,6 +20,7 @@ lib64/
parts/
sdist/
var/
+wheels/
*.egg-info/
.installed.cfg
*.egg
@@ -79,7 +80,7 @@ celerybeat-schedule
.env
# virtualenv
-.venv/
+.venv
venv/
ENV/
diff --git a/vendor/gitignore/Scala.gitignore b/vendor/gitignore/Scala.gitignore
index a02d882cb88..006a7b247fe 100644
--- a/vendor/gitignore/Scala.gitignore
+++ b/vendor/gitignore/Scala.gitignore
@@ -13,6 +13,8 @@ project/boot/
project/plugins/project/
# Scala-IDE specific
+.ensime
+.ensime_cache/
.scala_dependencies
.worksheet
diff --git a/vendor/gitignore/Swift.gitignore b/vendor/gitignore/Swift.gitignore
index 2c22487b5e3..099d22ae2f4 100644
--- a/vendor/gitignore/Swift.gitignore
+++ b/vendor/gitignore/Swift.gitignore
@@ -19,7 +19,8 @@ xcuserdata/
## Other
*.moved-aside
-*.xcuserstate
+*.xccheckout
+*.xcscmblueprint
## Obj-C/Swift specific
*.hmap
@@ -35,6 +36,7 @@ playground.xcworkspace
#
# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
# Packages/
+# Package.pins
.build/
# CocoaPods
diff --git a/vendor/gitignore/Symfony.gitignore b/vendor/gitignore/Symfony.gitignore
index 7d56f982f81..ed4d3c6c28d 100644
--- a/vendor/gitignore/Symfony.gitignore
+++ b/vendor/gitignore/Symfony.gitignore
@@ -31,9 +31,6 @@
/web/bundles/
/web/uploads/
-# Assets managed by Bower
-/web/assets/vendor/
-
# PHPUnit
/app/phpunit.xml
/phpunit.xml
@@ -45,4 +42,4 @@
/composer.phar
# Backup entities generated with doctrine:generate:entities command
-*/Entity/*~
+**/Entity/*~
diff --git a/vendor/gitignore/TeX.gitignore b/vendor/gitignore/TeX.gitignore
index 1afbaf197f4..69bfb1eec3e 100644
--- a/vendor/gitignore/TeX.gitignore
+++ b/vendor/gitignore/TeX.gitignore
@@ -52,12 +52,22 @@ acs-*.bib
# beamer
*.nav
+*.pre
*.snm
*.vrb
+# changes
+*.soc
+
# cprotect
*.cpt
+# elsarticle (documentclass of Elsevier journals)
+*.spl
+
+# endnotes
+*.ent
+
# fixme
*.lox
@@ -123,9 +133,7 @@ acs-*.bib
*.maf
*.mlf
*.mlt
-*.mtc
-*.mtc[0-9]
-*.mtc[1-9][0-9]
+*.mtc[0-9]*
# minted
_minted*
@@ -140,6 +148,9 @@ _minted*
# nomencl
*.nlo
+# pax
+*.pax
+
# sagetex
*.sagetex.sage
*.sagetex.py
@@ -202,5 +213,8 @@ TSWLatexianTemp*
# KBibTeX
*~[0-9]*
-# auto folder when using emacs and auctex
+# auto folder when using emacs and auctex
/auto/*
+
+# expex forward references with \gathertags
+*-tags.tex
diff --git a/vendor/gitignore/Unity.gitignore b/vendor/gitignore/Unity.gitignore
index 1c10388911b..b829399ae85 100644
--- a/vendor/gitignore/Unity.gitignore
+++ b/vendor/gitignore/Unity.gitignore
@@ -5,6 +5,9 @@
/[Bb]uilds/
/Assets/AssetStoreTools*
+# Visual Studio 2015 cache directory
+/.vs/
+
# Autogenerated VS/MD/Consulo solution and project files
ExportedObj/
.consulo/
@@ -18,6 +21,7 @@ ExportedObj/
*.pidb
*.booproj
*.svd
+*.pdb
# Unity3D generated meta files
diff --git a/vendor/gitignore/UnrealEngine.gitignore b/vendor/gitignore/UnrealEngine.gitignore
index beec7b91f15..2f096001fec 100644
--- a/vendor/gitignore/UnrealEngine.gitignore
+++ b/vendor/gitignore/UnrealEngine.gitignore
@@ -36,6 +36,7 @@
# These project files can be generated by the engine
*.xcodeproj
+*.xcworkspace
*.sln
*.suo
*.opensdf
@@ -56,6 +57,9 @@ Build/*
# Don't ignore icon files in Build
!Build/**/*.ico
+# Built data for maps
+*_BuiltData.uasset
+
# Configuration files generated by the Editor
Saved/*
diff --git a/vendor/gitignore/VisualStudio.gitignore b/vendor/gitignore/VisualStudio.gitignore
index 09e407344ca..8054980d742 100644
--- a/vendor/gitignore/VisualStudio.gitignore
+++ b/vendor/gitignore/VisualStudio.gitignore
@@ -8,7 +8,6 @@
*.user
*.userosscache
*.sln.docstates
-*.vcxproj.filters
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
@@ -43,11 +42,11 @@ TestResult.xml
[Rr]eleasePS/
dlldata.c
-# DNX
+# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
-Properties/launchSettings.json
+**/Properties/launchSettings.json
*_i.c
*_p.c
@@ -200,7 +199,6 @@ ClientBin/
*.jfm
*.pfx
*.publishsettings
-node_modules/
orleans.codegen.cs
# Since there are multiple workflows, uncomment next line to ignore bower_components
@@ -235,6 +233,10 @@ FakesAssemblies/
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
+node_modules/
+
+# Typescript v1 declaration files
+typings/
# Visual Studio 6 build log
*.plg
@@ -272,4 +274,5 @@ __pycache__/
*.pyc
# Cake - Uncomment if you are using it
-# tools/
+# tools/**
+# !tools/packages.config
diff --git a/vendor/gitignore/Waf.gitignore b/vendor/gitignore/Waf.gitignore
index 48e8d8f7be4..dad2b56bdda 100644
--- a/vendor/gitignore/Waf.gitignore
+++ b/vendor/gitignore/Waf.gitignore
@@ -1,4 +1,9 @@
-# for projects that use Waf for building: http://code.google.com/p/waf/
-.waf-*
-.waf3-*
-.lock-*
+# For projects that use the Waf build system: https://waf.io/
+# Dot-hidden on Unix-like systems
+.waf-*-*/
+.waf3-*-*/
+# Hidden directory on Windows (no dot)
+waf-*-*/
+waf3-*-*/
+# Lockfile
+.lock-waf_*_build
diff --git a/vendor/gitlab-ci-yml/Docker.gitlab-ci.yml b/vendor/gitlab-ci-yml/Docker.gitlab-ci.yml
index f3fa3949656..8c590579934 100644
--- a/vendor/gitlab-ci-yml/Docker.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/Docker.gitlab-ci.yml
@@ -7,6 +7,7 @@ services:
build:
stage: build
script:
+ - export IMAGE_TAG=$(echo -en $CI_BUILD_REF_NAME | tr -c '[:alnum:]_.-' '-')
- docker login -u "gitlab-ci-token" -p "$CI_BUILD_TOKEN" $CI_REGISTRY
- - docker build --pull -t "$CI_REGISTRY_IMAGE:$CI_BUILD_REF_NAME" .
- - docker push "$CI_REGISTRY_IMAGE:$CI_BUILD_REF_NAME"
+ - docker build --pull -t "$CI_REGISTRY_IMAGE:$IMAGE_TAG" .
+ - docker push "$CI_REGISTRY_IMAGE:$IMAGE_TAG"
diff --git a/vendor/gitlab-ci-yml/Elixir.gitlab-ci.yml b/vendor/gitlab-ci-yml/Elixir.gitlab-ci.yml
index 00f9541e89b..981a77497e2 100644
--- a/vendor/gitlab-ci-yml/Elixir.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/Elixir.gitlab-ci.yml
@@ -1,6 +1,4 @@
-# This template uses the non default language docker image
-# The image already has Hex installed. You might want to consider to use `elixir:latest`
-image: trenpixster/elixir:latest
+image: elixir:latest
# Pick zero or more services to be used on all builds.
# Only needed when using a docker container to run your tests in.
@@ -11,6 +9,8 @@ services:
- postgres:latest
before_script:
+ - mix local.rebar --force
+ - mix local.hex --force
- mix deps.get
mix:
diff --git a/vendor/gitlab-ci-yml/Go.gitlab-ci.yml b/vendor/gitlab-ci-yml/Go.gitlab-ci.yml
new file mode 100644
index 00000000000..e23b6e212f0
--- /dev/null
+++ b/vendor/gitlab-ci-yml/Go.gitlab-ci.yml
@@ -0,0 +1,37 @@
+image: golang:latest
+
+# The problem is that to be able to use go get, one needs to put
+# the repository in the $GOPATH. So for example if your gitlab domain
+# is mydomainperso.com, and that your repository is repos/projectname, and
+# the default GOPATH being /go, then you'd need to have your
+# repository in /go/src/mydomainperso.com/repos/projectname
+# Thus, making a symbolic link corrects this.
+before_script:
+ - ln -s /builds /go/src/mydomainperso.com
+ - cd /go/src/mydomainperso.com/repos/projectname
+
+stages:
+ - test
+ - build
+
+format:
+ stage: test
+ script:
+ # Add here all the dependencies, or use glide/govendor to get
+ # them automatically.
+ # - curl https://glide.sh/get | sh
+ - go get github.com/alecthomas/kingpin
+ - go tool vet -composites=false -shadow=true *.go
+ - go test -race $(go list ./... | grep -v /vendor/)
+
+compile:
+ stage: build
+ script:
+ # Add here all the dependencies, or use glide/govendor/...
+ # to get them automatically.
+ - go get github.com/alecthomas/kingpin
+ # Better put this in a Makefile
+ - go build -race -ldflags "-extldflags '-static'" -o mybinary
+ artifacts:
+ paths:
+ - mybinary
diff --git a/vendor/gitlab-ci-yml/Gradle.gitlab-ci.yml b/vendor/gitlab-ci-yml/Gradle.gitlab-ci.yml
index 263c4c19999..98d3039ad06 100644
--- a/vendor/gitlab-ci-yml/Gradle.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/Gradle.gitlab-ci.yml
@@ -31,4 +31,4 @@ build:
test:
stage: test
script:
- - ./gradlew -g /cache./gradle check
+ - ./gradlew -g /cache/.gradle check
diff --git a/vendor/gitlab-ci-yml/Openshift.gitlab-ci.yml b/vendor/gitlab-ci-yml/Openshift.gitlab-ci.yml
new file mode 100644
index 00000000000..2ba5cad9682
--- /dev/null
+++ b/vendor/gitlab-ci-yml/Openshift.gitlab-ci.yml
@@ -0,0 +1,92 @@
+# This file is a template, and might need editing before it works on your project.
+image: ayufan/openshift-cli
+
+stages:
+ - test
+ - review
+ - staging
+ - production
+
+variables:
+ OPENSHIFT_SERVER: openshift.default.svc.cluster.local
+ # OPENSHIFT_DOMAIN: apps.example.com
+ # Configure this variable in Secure Variables:
+ # OPENSHIFT_TOKEN: my.openshift.token
+
+test1:
+ stage: test
+ before_script: []
+ script:
+ - echo run tests
+
+test2:
+ stage: test
+ before_script: []
+ script:
+ - echo run tests
+
+.deploy: &deploy
+ before_script:
+ - oc login "$OPENSHIFT_SERVER" --token="$OPENSHIFT_TOKEN" --insecure-skip-tls-verify
+ - oc project "$CI_PROJECT_NAME" 2> /dev/null || oc new-project "$CI_PROJECT_NAME"
+ script:
+ - "oc get services $APP 2> /dev/null || oc new-app . --name=$APP --strategy=docker"
+ - "oc start-build $APP --from-dir=. --follow || sleep 3s || oc start-build $APP --from-dir=. --follow"
+ - "oc get routes $APP 2> /dev/null || oc expose service $APP --hostname=$APP_HOST"
+
+review:
+ <<: *deploy
+ stage: review
+ variables:
+ APP: $CI_BUILD_REF_NAME
+ APP_HOST: $CI_PROJECT_NAME-$CI_BUILD_REF_NAME.$OPENSHIFT_DOMAIN
+ environment:
+ name: review/$CI_BUILD_REF_NAME
+ url: http://$CI_PROJECT_NAME-$CI_BUILD_REF_NAME.$OPENSHIFT_DOMAIN
+ on_stop: stop-review
+ only:
+ - branches
+ except:
+ - master
+
+stop-review:
+ <<: *deploy
+ stage: review
+ script:
+ - oc delete all -l "app=$APP"
+ when: manual
+ variables:
+ APP: $CI_BUILD_REF_NAME
+ GIT_STRATEGY: none
+ environment:
+ name: review/$CI_BUILD_REF_NAME
+ action: stop
+ only:
+ - branches
+ except:
+ - master
+
+staging:
+ <<: *deploy
+ stage: staging
+ variables:
+ APP: staging
+ APP_HOST: $CI_PROJECT_NAME-staging.$OPENSHIFT_DOMAIN
+ environment:
+ name: staging
+ url: http://$CI_PROJECT_NAME-staging.$OPENSHIFT_DOMAIN
+ only:
+ - master
+
+production:
+ <<: *deploy
+ stage: production
+ variables:
+ APP: production
+ APP_HOST: $CI_PROJECT_NAME.$OPENSHIFT_DOMAIN
+ when: manual
+ environment:
+ name: production
+ url: http://$CI_PROJECT_NAME.$OPENSHIFT_DOMAIN
+ only:
+ - master
diff --git a/vendor/gitlab-ci-yml/autodeploy/Kubernetes.gitlab-ci.yml b/vendor/gitlab-ci-yml/autodeploy/Kubernetes.gitlab-ci.yml
new file mode 100644
index 00000000000..7298ea73bab
--- /dev/null
+++ b/vendor/gitlab-ci-yml/autodeploy/Kubernetes.gitlab-ci.yml
@@ -0,0 +1,76 @@
+# Explanation on the scripts:
+# https://gitlab.com/gitlab-examples/kubernetes-deploy/blob/master/README.md
+image: registry.gitlab.com/gitlab-examples/kubernetes-deploy
+
+variables:
+ # Application deployment domain
+ KUBE_DOMAIN: domain.example.com
+
+stages:
+ - build
+ - test
+ - review
+ - staging
+ - production
+
+build:
+ stage: build
+ script:
+ - command build
+ only:
+ - branches
+
+production:
+ stage: production
+ variables:
+ CI_ENVIRONMENT_URL: http://production.$KUBE_DOMAIN
+ script:
+ - command deploy
+ environment:
+ name: production
+ url: http://production.$KUBE_DOMAIN
+ when: manual
+ only:
+ - master
+
+staging:
+ stage: staging
+ variables:
+ CI_ENVIRONMENT_URL: http://staging.$KUBE_DOMAIN
+ script:
+ - command deploy
+ environment:
+ name: staging
+ url: http://staging.$KUBE_DOMAIN
+ only:
+ - master
+
+review:
+ stage: review
+ variables:
+ CI_ENVIRONMENT_URL: http://$CI_ENVIRONMENT_SLUG.$KUBE_DOMAIN
+ script:
+ - command deploy
+ environment:
+ name: review/$CI_BUILD_REF_NAME
+ url: http://$CI_ENVIRONMENT_SLUG.$KUBE_DOMAIN
+ on_stop: stop_review
+ only:
+ - branches
+ except:
+ - master
+
+stop_review:
+ stage: review
+ variables:
+ GIT_STRATEGY: none
+ script:
+ - command destroy
+ environment:
+ name: review/$CI_BUILD_REF_NAME
+ action: stop
+ when: manual
+ only:
+ - branches
+ except:
+ - master
diff --git a/vendor/gitlab-ci-yml/autodeploy/OpenShift.gitlab-ci.yml b/vendor/gitlab-ci-yml/autodeploy/OpenShift.gitlab-ci.yml
new file mode 100644
index 00000000000..249adbc9f4a
--- /dev/null
+++ b/vendor/gitlab-ci-yml/autodeploy/OpenShift.gitlab-ci.yml
@@ -0,0 +1,76 @@
+# Explaination on the scripts:
+# https://gitlab.com/gitlab-examples/openshift-deploy/blob/master/README.md
+image: registry.gitlab.com/gitlab-examples/openshift-deploy
+
+variables:
+ # Application deployment domain
+ KUBE_DOMAIN: domain.example.com
+
+stages:
+ - build
+ - test
+ - review
+ - staging
+ - production
+
+build:
+ stage: build
+ script:
+ - command build
+ only:
+ - branches
+
+production:
+ stage: production
+ variables:
+ CI_ENVIRONMENT_URL: http://production.$KUBE_DOMAIN
+ script:
+ - command deploy
+ environment:
+ name: production
+ url: http://production.$KUBE_DOMAIN
+ when: manual
+ only:
+ - master
+
+staging:
+ stage: staging
+ variables:
+ CI_ENVIRONMENT_URL: http://staging.$KUBE_DOMAIN
+ script:
+ - command deploy
+ environment:
+ name: staging
+ url: http://staging.$KUBE_DOMAIN
+ only:
+ - master
+
+review:
+ stage: review
+ variables:
+ CI_ENVIRONMENT_URL: http://$CI_ENVIRONMENT_SLUG.$KUBE_DOMAIN
+ script:
+ - command deploy
+ environment:
+ name: review/$CI_BUILD_REF_NAME
+ url: http://$CI_ENVIRONMENT_SLUG.$KUBE_DOMAIN
+ on_stop: stop_review
+ only:
+ - branches
+ except:
+ - master
+
+stop_review:
+ stage: review
+ variables:
+ GIT_STRATEGY: none
+ script:
+ - command destroy
+ environment:
+ name: review/$CI_BUILD_REF_NAME
+ action: stop
+ when: manual
+ only:
+ - branches
+ except:
+ - master