Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--.gitlab-ci.yml27
-rw-r--r--.gitlab/issue_templates/Research Proposal.md17
-rw-r--r--.rubocop.yml278
-rw-r--r--.rubocop_todo.yml324
-rw-r--r--CHANGELOG.md225
-rw-r--r--CONTRIBUTING.md85
-rw-r--r--GITLAB_PAGES_VERSION2
-rw-r--r--Gemfile18
-rw-r--r--Gemfile.lock143
-rw-r--r--README.md2
-rw-r--r--VERSION2
-rwxr-xr-xapp/assets/images/favicon-blue.icobin0 -> 5430 bytes
-rw-r--r--app/assets/images/icon-merge-request-unmerged.svg1
-rw-r--r--app/assets/images/mailers/gitlab_footer_logo.gifbin0 -> 3654 bytes
-rw-r--r--app/assets/images/mailers/gitlab_header_logo.gifbin0 -> 3040 bytes
-rw-r--r--app/assets/javascripts/admin.js2
-rw-r--r--app/assets/javascripts/ajax_loading_spinner.js35
-rw-r--r--app/assets/javascripts/api.js2
-rw-r--r--app/assets/javascripts/application.js194
-rw-r--r--app/assets/javascripts/aside.js2
-rw-r--r--app/assets/javascripts/autosave.js2
-rw-r--r--app/assets/javascripts/awards_handler.js2
-rw-r--r--app/assets/javascripts/behaviors/autosize.js2
-rw-r--r--app/assets/javascripts/behaviors/details_behavior.js2
-rw-r--r--app/assets/javascripts/behaviors/quick_submit.js2
-rw-r--r--app/assets/javascripts/behaviors/requires_input.js2
-rw-r--r--app/assets/javascripts/blob/blob_file_dropzone.js2
-rw-r--r--app/assets/javascripts/blob/blob_gitignore_selector.js2
-rw-r--r--app/assets/javascripts/blob/blob_gitignore_selectors.js2
-rw-r--r--app/assets/javascripts/blob/blob_license_selector.js2
-rw-r--r--app/assets/javascripts/blob_edit/blob_edit_bundle.js2
-rw-r--r--app/assets/javascripts/blob_edit/edit_blob.js2
-rw-r--r--app/assets/javascripts/boards/boards_bundle.js.es664
-rw-r--r--app/assets/javascripts/boards/components/board_card.js69
-rw-r--r--app/assets/javascripts/boards/components/board_card.js.es661
-rw-r--r--app/assets/javascripts/boards/components/board_list.js.es64
-rw-r--r--app/assets/javascripts/boards/models/list.js.es612
-rw-r--r--app/assets/javascripts/boards/stores/boards_store.js.es65
-rw-r--r--app/assets/javascripts/breakpoints.js2
-rw-r--r--app/assets/javascripts/broadcast_message.js2
-rw-r--r--app/assets/javascripts/build.js37
-rw-r--r--app/assets/javascripts/build_artifacts.js2
-rw-r--r--app/assets/javascripts/commit.js2
-rw-r--r--app/assets/javascripts/commit/file.js2
-rw-r--r--app/assets/javascripts/commit/image_file.js83
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_store.js.es68
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_table.js.es612
-rw-r--r--app/assets/javascripts/commits.js2
-rw-r--r--app/assets/javascripts/compare.js2
-rw-r--r--app/assets/javascripts/compare_autocomplete.js.es62
-rw-r--r--app/assets/javascripts/confirm_danger_modal.js2
-rw-r--r--app/assets/javascripts/copy_to_clipboard.js2
-rw-r--r--app/assets/javascripts/create_label.js.es66
-rw-r--r--app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es622
-rw-r--r--app/assets/javascripts/cycle_analytics/cycle_analytics_store.js.es650
-rw-r--r--app/assets/javascripts/cycle_analytics/default_event_objects.js.es698
-rw-r--r--app/assets/javascripts/diff_notes/diff_notes_bundle.js.es618
-rw-r--r--app/assets/javascripts/dispatcher.js.es613
-rw-r--r--app/assets/javascripts/dropzone_input.js7
-rw-r--r--app/assets/javascripts/environments/components/environment.js.es617
-rw-r--r--app/assets/javascripts/environments/components/environment_actions.js.es638
-rw-r--r--app/assets/javascripts/environments/components/environment_item.js.es663
-rw-r--r--app/assets/javascripts/environments/components/environments_table.js.es64
-rw-r--r--app/assets/javascripts/environments/environments_bundle.js.es61
-rw-r--r--app/assets/javascripts/environments/folder/environments_folder_bundle.js.es61
-rw-r--r--app/assets/javascripts/environments/folder/environments_folder_view.js.es65
-rw-r--r--app/assets/javascripts/extensions/jquery.js2
-rw-r--r--app/assets/javascripts/files_comment_button.js73
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_hint.js.es629
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_bundle.js12
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es633
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es66
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_manager.js.es615
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_tokenizer.js.es65
-rw-r--r--app/assets/javascripts/flash.js2
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js.es69
-rw-r--r--app/assets/javascripts/gl_dropdown.js11
-rw-r--r--app/assets/javascripts/graphs/graphs_bundle.js7
-rw-r--r--app/assets/javascripts/graphs/stat_graph.js18
-rw-r--r--app/assets/javascripts/graphs/stat_graph_contributors.js201
-rw-r--r--app/assets/javascripts/graphs/stat_graph_contributors_graph.js542
-rw-r--r--app/assets/javascripts/graphs/stat_graph_contributors_util.js265
-rw-r--r--app/assets/javascripts/group_avatar.js2
-rw-r--r--app/assets/javascripts/groups_select.js2
-rw-r--r--app/assets/javascripts/header.js2
-rw-r--r--app/assets/javascripts/importer_status.js2
-rw-r--r--app/assets/javascripts/issuable.js.es62
-rw-r--r--app/assets/javascripts/issuable_context.js2
-rw-r--r--app/assets/javascripts/issuable_form.js2
-rw-r--r--app/assets/javascripts/issue.js45
-rw-r--r--app/assets/javascripts/issue_status_select.js2
-rw-r--r--app/assets/javascripts/labels.js2
-rw-r--r--app/assets/javascripts/labels_select.js2
-rw-r--r--app/assets/javascripts/layout_nav.js2
-rw-r--r--app/assets/javascripts/lib/cropper.js2
-rw-r--r--app/assets/javascripts/lib/raphael.js2
-rw-r--r--app/assets/javascripts/lib/utils/animate.js2
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js.es655
-rw-r--r--app/assets/javascripts/lib/utils/datetime_utility.js.es62
-rw-r--r--app/assets/javascripts/lib/utils/http_status.js10
-rw-r--r--app/assets/javascripts/lib/utils/notify.js2
-rw-r--r--app/assets/javascripts/lib/utils/text_utility.js15
-rw-r--r--app/assets/javascripts/lib/utils/type_utility.js2
-rw-r--r--app/assets/javascripts/lib/utils/url_utility.js.es62
-rw-r--r--app/assets/javascripts/line_highlighter.js2
-rw-r--r--app/assets/javascripts/logo.js2
-rw-r--r--app/assets/javascripts/member_expiration_date.js.es62
-rw-r--r--app/assets/javascripts/merge_request.js45
-rw-r--r--app/assets/javascripts/merge_request_widget.js.es65
-rw-r--r--app/assets/javascripts/merged_buttons.js2
-rw-r--r--app/assets/javascripts/milestone.js3
-rw-r--r--app/assets/javascripts/milestone_select.js7
-rw-r--r--app/assets/javascripts/mini_pipeline_graph_dropdown.js.es62
-rw-r--r--app/assets/javascripts/namespace_select.js2
-rw-r--r--app/assets/javascripts/network/branch_graph.js2
-rw-r--r--app/assets/javascripts/network/network.js2
-rw-r--r--app/assets/javascripts/network/network_bundle.js7
-rw-r--r--app/assets/javascripts/new_branch_form.js34
-rw-r--r--app/assets/javascripts/new_commit_form.js2
-rw-r--r--app/assets/javascripts/notes.js34
-rw-r--r--app/assets/javascripts/notifications_dropdown.js2
-rw-r--r--app/assets/javascripts/notifications_form.js2
-rw-r--r--app/assets/javascripts/profile/profile.js.es67
-rw-r--r--app/assets/javascripts/profile/profile_bundle.js5
-rw-r--r--app/assets/javascripts/project.js4
-rw-r--r--app/assets/javascripts/project_avatar.js2
-rw-r--r--app/assets/javascripts/project_find_file.js2
-rw-r--r--app/assets/javascripts/project_fork.js2
-rw-r--r--app/assets/javascripts/project_import.js2
-rw-r--r--app/assets/javascripts/project_label_subscription.js.es66
-rw-r--r--app/assets/javascripts/project_new.js2
-rw-r--r--app/assets/javascripts/project_select.js2
-rw-r--r--app/assets/javascripts/project_show.js2
-rw-r--r--app/assets/javascripts/projects_list.js2
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_edit.js.es69
-rw-r--r--app/assets/javascripts/protected_branches/protected_branches_bundle.js8
-rw-r--r--app/assets/javascripts/render_gfm.js2
-rw-r--r--app/assets/javascripts/render_math.js2
-rw-r--r--app/assets/javascripts/right_sidebar.js20
-rw-r--r--app/assets/javascripts/search.js2
-rw-r--r--app/assets/javascripts/search_autocomplete.js.es64
-rw-r--r--app/assets/javascripts/shortcuts.js2
-rw-r--r--app/assets/javascripts/shortcuts_dashboard_navigation.js2
-rw-r--r--app/assets/javascripts/shortcuts_find_file.js2
-rw-r--r--app/assets/javascripts/shortcuts_issuable.js2
-rw-r--r--app/assets/javascripts/shortcuts_navigation.js2
-rw-r--r--app/assets/javascripts/shortcuts_network.js2
-rw-r--r--app/assets/javascripts/sidebar.js.es6111
-rw-r--r--app/assets/javascripts/single_file_diff.js2
-rw-r--r--app/assets/javascripts/snippet/snippet_bundle.js6
-rw-r--r--app/assets/javascripts/star.js2
-rw-r--r--app/assets/javascripts/subscription_select.js2
-rw-r--r--app/assets/javascripts/syntax_highlight.js2
-rw-r--r--app/assets/javascripts/task_list.js52
-rw-r--r--app/assets/javascripts/todos.js.es6143
-rw-r--r--app/assets/javascripts/tree.js2
-rw-r--r--app/assets/javascripts/u2f/error.js2
-rw-r--r--app/assets/javascripts/u2f/register.js2
-rw-r--r--app/assets/javascripts/u2f/util.js2
-rw-r--r--app/assets/javascripts/user_callout.js58
-rw-r--r--app/assets/javascripts/users/calendar.js2
-rw-r--r--app/assets/javascripts/users/users_bundle.js4
-rw-r--r--app/assets/javascripts/users_select.js2
-rw-r--r--app/assets/javascripts/version_check_image.js.es616
-rw-r--r--app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es697
-rw-r--r--app/assets/javascripts/vue_pipelines_index/pipelines.js.es628
-rw-r--r--app/assets/javascripts/vue_pipelines_index/stage.js.es636
-rw-r--r--app/assets/javascripts/vue_pipelines_index/store.js.es656
-rw-r--r--app/assets/javascripts/vue_pipelines_index/time_ago.js.es65
-rw-r--r--app/assets/javascripts/vue_shared/components/pipelines_table.js.es62
-rw-r--r--app/assets/javascripts/vue_shared/components/table_pagination.js.es616
-rw-r--r--app/assets/javascripts/wikis.js.es66
-rw-r--r--app/assets/javascripts/zen_mode.js2
-rw-r--r--app/assets/stylesheets/application.scss1
-rw-r--r--app/assets/stylesheets/framework.scss1
-rw-r--r--app/assets/stylesheets/framework/animations.scss3
-rw-r--r--app/assets/stylesheets/framework/blocks.scss2
-rw-r--r--app/assets/stylesheets/framework/filters.scss46
-rw-r--r--app/assets/stylesheets/framework/gitlab-theme.scss144
-rw-r--r--app/assets/stylesheets/framework/header.scss72
-rw-r--r--app/assets/stylesheets/framework/jquery.scss11
-rw-r--r--app/assets/stylesheets/framework/lists.scss10
-rw-r--r--app/assets/stylesheets/framework/mixins.scss7
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss187
-rw-r--r--app/assets/stylesheets/framework/typography.scss25
-rw-r--r--app/assets/stylesheets/framework/variables.scss4
-rw-r--r--app/assets/stylesheets/highlight/dark.scss13
-rw-r--r--app/assets/stylesheets/highlight/monokai.scss13
-rw-r--r--app/assets/stylesheets/highlight/solarized_dark.scss13
-rw-r--r--app/assets/stylesheets/highlight/solarized_light.scss13
-rw-r--r--app/assets/stylesheets/highlight/white.scss11
-rw-r--r--app/assets/stylesheets/pages/builds.scss2
-rw-r--r--app/assets/stylesheets/pages/diff.scss8
-rw-r--r--app/assets/stylesheets/pages/environments.scss177
-rw-r--r--app/assets/stylesheets/pages/issuable.scss4
-rw-r--r--app/assets/stylesheets/pages/issues.scss5
-rw-r--r--app/assets/stylesheets/pages/milestone.scss20
-rw-r--r--app/assets/stylesheets/pages/notes.scss51
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss203
-rw-r--r--app/assets/stylesheets/pages/profile.scss38
-rw-r--r--app/assets/stylesheets/pages/profiles/preferences.scss39
-rw-r--r--app/assets/stylesheets/pages/projects.scss16
-rw-r--r--app/assets/stylesheets/pages/todos.scss58
-rw-r--r--app/assets/stylesheets/pages/tree.scss2
-rw-r--r--app/assets/stylesheets/print.scss1
-rw-r--r--app/controllers/admin/application_settings_controller.rb1
-rw-r--r--app/controllers/admin/background_jobs_controller.rb1
-rw-r--r--app/controllers/admin/runners_controller.rb6
-rw-r--r--app/controllers/admin/system_info_controller.rb5
-rw-r--r--app/controllers/admin/users_controller.rb1
-rw-r--r--app/controllers/application_controller.rb10
-rw-r--r--app/controllers/concerns/creates_commit.rb27
-rw-r--r--app/controllers/concerns/issuable_actions.rb17
-rw-r--r--app/controllers/concerns/issuable_collections.rb20
-rw-r--r--app/controllers/concerns/issues_action.rb2
-rw-r--r--app/controllers/concerns/merge_requests_action.rb2
-rw-r--r--app/controllers/concerns/service_params.rb5
-rw-r--r--app/controllers/concerns/snippets_actions.rb21
-rw-r--r--app/controllers/concerns/spammable_actions.rb30
-rw-r--r--app/controllers/dashboard/todos_controller.rb17
-rw-r--r--app/controllers/import/fogbugz_controller.rb2
-rw-r--r--app/controllers/import/google_code_controller.rb4
-rw-r--r--app/controllers/invites_controller.rb4
-rw-r--r--app/controllers/jwt_controller.rb8
-rw-r--r--app/controllers/omniauth_callbacks_controller.rb9
-rw-r--r--app/controllers/profiles/keys_controller.rb4
-rw-r--r--app/controllers/profiles/preferences_controller.rb1
-rw-r--r--app/controllers/profiles/two_factor_auths_controller.rb2
-rw-r--r--app/controllers/projects/application_controller.rb1
-rw-r--r--app/controllers/projects/blob_controller.rb2
-rw-r--r--app/controllers/projects/branches_controller.rb30
-rw-r--r--app/controllers/projects/git_http_client_controller.rb11
-rw-r--r--app/controllers/projects/issues_controller.rb39
-rw-r--r--app/controllers/projects/merge_requests_controller.rb56
-rw-r--r--app/controllers/projects/pipelines_controller.rb12
-rw-r--r--app/controllers/projects/runners_controller.rb6
-rw-r--r--app/controllers/projects/snippets_controller.rb34
-rw-r--r--app/controllers/projects/tree_controller.rb4
-rw-r--r--app/controllers/sessions_controller.rb11
-rw-r--r--app/controllers/snippets_controller.rb35
-rw-r--r--app/finders/issuable_finder.rb17
-rw-r--r--app/finders/issues_finder.rb4
-rw-r--r--app/finders/merge_requests_finder.rb10
-rw-r--r--app/finders/notes_finder.rb11
-rw-r--r--app/finders/pipelines_finder.rb6
-rw-r--r--app/finders/todos_finder.rb4
-rw-r--r--app/helpers/application_helper.rb20
-rw-r--r--app/helpers/application_settings_helper.rb25
-rw-r--r--app/helpers/blob_helper.rb21
-rw-r--r--app/helpers/builds_helper.rb7
-rw-r--r--app/helpers/button_helper.rb2
-rw-r--r--app/helpers/emails_helper.rb19
-rw-r--r--app/helpers/issuables_helper.rb6
-rw-r--r--app/helpers/javascript_helper.rb1
-rw-r--r--app/helpers/milestones_helper.rb2
-rw-r--r--app/helpers/namespaces_helper.rb6
-rw-r--r--app/helpers/nav_helper.rb14
-rw-r--r--app/helpers/page_layout_helper.rb4
-rw-r--r--app/helpers/preferences_helper.rb4
-rw-r--r--app/helpers/projects_helper.rb11
-rw-r--r--app/helpers/sorting_helper.rb2
-rw-r--r--app/helpers/submodule_helper.rb8
-rw-r--r--app/helpers/tab_helper.rb2
-rw-r--r--app/helpers/todos_helper.rb6
-rw-r--r--app/helpers/visibility_level_helper.rb10
-rw-r--r--app/mailers/emails/pipelines.rb4
-rw-r--r--app/mailers/repository_check_mailer.rb11
-rw-r--r--app/models/application_setting.rb21
-rw-r--r--app/models/ci/build.rb45
-rw-r--r--app/models/ci/pipeline.rb33
-rw-r--r--app/models/ci/runner.rb20
-rw-r--r--app/models/ci/runner_project.rb2
-rw-r--r--app/models/ci/trigger.rb4
-rw-r--r--app/models/commit.rb17
-rw-r--r--app/models/commit_status.rb11
-rw-r--r--app/models/concerns/cache_markdown_field.rb11
-rw-r--r--app/models/concerns/case_sensitivity.rb11
-rw-r--r--app/models/concerns/has_status.rb12
-rw-r--r--app/models/concerns/issuable.rb32
-rw-r--r--app/models/concerns/reactive_service.rb2
-rw-r--r--app/models/concerns/sortable.rb11
-rw-r--r--app/models/concerns/spammable.rb6
-rw-r--r--app/models/concerns/uniquify.rb30
-rw-r--r--app/models/diff_note.rb2
-rw-r--r--app/models/event.rb4
-rw-r--r--app/models/external_issue.rb2
-rw-r--r--app/models/issue.rb2
-rw-r--r--app/models/label.rb2
-rw-r--r--app/models/member.rb4
-rw-r--r--app/models/members/group_member.rb4
-rw-r--r--app/models/members/project_member.rb4
-rw-r--r--app/models/merge_request.rb12
-rw-r--r--app/models/merge_request_diff.rb2
-rw-r--r--app/models/merge_requests_closing_issues.rb8
-rw-r--r--app/models/namespace.rb22
-rw-r--r--app/models/network/graph.rb11
-rw-r--r--app/models/note.rb2
-rw-r--r--app/models/notification_setting.rb4
-rw-r--r--app/models/pages_domain.rb2
-rw-r--r--app/models/project.rb58
-rw-r--r--app/models/project_feature.rb2
-rw-r--r--app/models/project_group_link.rb11
-rw-r--r--app/models/project_services/buildkite_service.rb2
-rw-r--r--app/models/project_services/chat_message/base_message.rb4
-rw-r--r--app/models/project_services/chat_message/build_message.rb28
-rw-r--r--app/models/project_services/chat_message/issue_message.rb7
-rw-r--r--app/models/project_services/chat_message/merge_message.rb4
-rw-r--r--app/models/project_services/chat_message/note_message.rb9
-rw-r--r--app/models/project_services/drone_ci_service.rb8
-rw-r--r--app/models/project_services/hipchat_service.rb4
-rw-r--r--app/models/project_services/irker_service.rb5
-rw-r--r--app/models/project_services/kubernetes_service.rb16
-rw-r--r--app/models/project_services/mattermost_service.rb14
-rw-r--r--app/models/project_services/mock_ci_service.rb82
-rw-r--r--app/models/project_services/pivotaltracker_service.rb2
-rw-r--r--app/models/project_services/pushover_service.rb45
-rw-r--r--app/models/project_services/slack_service.rb14
-rw-r--r--app/models/project_snippet.rb4
-rw-r--r--app/models/project_statistics.rb2
-rw-r--r--app/models/project_wiki.rb9
-rw-r--r--app/models/protected_branch.rb4
-rw-r--r--app/models/repository.rb231
-rw-r--r--app/models/service.rb5
-rw-r--r--app/models/todo.rb2
-rw-r--r--app/models/user.rb66
-rw-r--r--app/policies/base_policy.rb4
-rw-r--r--app/policies/user_policy.rb8
-rw-r--r--app/serializers/analytics_stage_entity.rb1
-rw-r--r--app/services/access_token_validation_service.rb8
-rw-r--r--app/services/auth/container_registry_authentication_service.rb2
-rw-r--r--app/services/base_service.rb9
-rw-r--r--app/services/ci/create_pipeline_builds_service.rb4
-rw-r--r--app/services/ci/create_pipeline_service.rb3
-rw-r--r--app/services/ci/register_build_service.rb36
-rw-r--r--app/services/ci/retry_build_service.rb43
-rw-r--r--app/services/ci/retry_pipeline_service.rb28
-rw-r--r--app/services/ci/update_runner_service.rb15
-rw-r--r--app/services/create_branch_service.rb14
-rw-r--r--app/services/create_snippet_service.rb10
-rw-r--r--app/services/files/create_dir_service.rb2
-rw-r--r--app/services/files/create_service.rb7
-rw-r--r--app/services/files/destroy_service.rb2
-rw-r--r--app/services/files/multi_service.rb41
-rw-r--r--app/services/files/update_service.rb4
-rw-r--r--app/services/groups/destroy_service.rb7
-rw-r--r--app/services/issuable_base_service.rb32
-rw-r--r--app/services/issues/create_service.rb21
-rw-r--r--app/services/issues/update_service.rb8
-rw-r--r--app/services/members/destroy_service.rb2
-rw-r--r--app/services/merge_requests/add_todo_when_build_fails_service.rb6
-rw-r--r--app/services/merge_requests/build_service.rb26
-rw-r--r--app/services/merge_requests/merge_service.rb22
-rw-r--r--app/services/merge_requests/merge_when_pipeline_succeeds_service.rb6
-rw-r--r--app/services/notes/slash_commands_service.rb2
-rw-r--r--app/services/projects/destroy_service.rb5
-rw-r--r--app/services/projects/download_service.rb4
-rw-r--r--app/services/projects/update_pages_service.rb2
-rw-r--r--app/services/projects/upload_service.rb2
-rw-r--r--app/services/protected_branches/api_update_service.rb8
-rw-r--r--app/services/spam_check_service.rb24
-rw-r--r--app/services/spam_service.rb31
-rw-r--r--app/services/system_hooks_service.rb4
-rw-r--r--app/services/system_note_service.rb17
-rw-r--r--app/services/todo_service.rb25
-rw-r--r--app/services/update_snippet_service.rb10
-rw-r--r--app/services/users/destroy_service.rb23
-rw-r--r--app/services/users/refresh_authorized_projects_service.rb16
-rw-r--r--app/uploaders/artifact_uploader.rb4
-rw-r--r--app/uploaders/attachment_uploader.rb2
-rw-r--r--app/uploaders/avatar_uploader.rb2
-rw-r--r--app/uploaders/file_uploader.rb25
-rw-r--r--app/uploaders/gitlab_uploader.rb10
-rw-r--r--app/uploaders/uploader_helper.rb12
-rw-r--r--app/validators/addressable_url_validator.rb2
-rw-r--r--app/validators/duration_validator.rb17
-rw-r--r--app/views/admin/abuse_reports/index.html.haml1
-rw-r--r--app/views/admin/application_settings/_form.html.haml16
-rw-r--r--app/views/admin/background_jobs/show.html.haml2
-rw-r--r--app/views/admin/logs/show.html.haml5
-rw-r--r--app/views/admin/projects/index.html.haml6
-rw-r--r--app/views/admin/runners/_runner.html.haml2
-rw-r--r--app/views/admin/runners/index.html.haml3
-rw-r--r--app/views/admin/users/_user.html.haml2
-rw-r--r--app/views/admin/users/index.html.haml14
-rw-r--r--app/views/admin/users/show.html.haml5
-rw-r--r--app/views/dashboard/_activities.html.haml2
-rw-r--r--app/views/dashboard/_activity_head.html.haml4
-rw-r--r--app/views/dashboard/projects/index.html.haml2
-rw-r--r--app/views/dashboard/todos/_todo.html.haml22
-rw-r--r--app/views/dashboard/todos/index.html.haml6
-rw-r--r--app/views/devise/shared/_signin_box.html.haml2
-rw-r--r--app/views/devise/shared/_signup_box.html.haml2
-rw-r--r--app/views/devise/shared/_tabs_ldap.html.haml2
-rw-r--r--app/views/events/event/_push.html.haml8
-rw-r--r--app/views/explore/projects/_filter.html.haml4
-rw-r--r--app/views/groups/_activities.html.haml2
-rw-r--r--app/views/groups/_head.html.haml14
-rw-r--r--app/views/groups/_head_issues.html.haml19
-rw-r--r--app/views/groups/activity.html.haml3
-rw-r--r--app/views/groups/issues.html.haml1
-rw-r--r--app/views/groups/labels/index.html.haml1
-rw-r--r--app/views/groups/milestones/index.html.haml1
-rw-r--r--app/views/groups/show.html.haml1
-rw-r--r--app/views/groups/subgroups.html.haml1
-rw-r--r--app/views/kaminari/gitlab/_page.html.haml2
-rw-r--r--app/views/layouts/_head.html.haml2
-rw-r--r--app/views/layouts/_page.html.haml19
-rw-r--r--app/views/layouts/_recaptcha_verification.html.haml23
-rw-r--r--app/views/layouts/application.html.haml2
-rw-r--r--app/views/layouts/header/_default.html.haml25
-rw-r--r--app/views/layouts/mailer.html.haml72
-rw-r--r--app/views/layouts/mailer.text.haml5
-rw-r--r--app/views/layouts/nav/_admin.html.haml2
-rw-r--r--app/views/layouts/nav/_dashboard.html.haml76
-rw-r--r--app/views/layouts/nav/_explore.html.haml2
-rw-r--r--app/views/layouts/nav/_group.html.haml18
-rw-r--r--app/views/notify/pipeline_failed_email.html.haml280
-rw-r--r--app/views/notify/pipeline_failed_email.text.erb4
-rw-r--r--app/views/notify/pipeline_success_email.html.haml230
-rw-r--r--app/views/notify/pipeline_success_email.text.erb4
-rw-r--r--app/views/profiles/accounts/show.html.haml5
-rw-r--r--app/views/profiles/personal_access_tokens/index.html.haml2
-rw-r--r--app/views/profiles/preferences/show.html.haml13
-rw-r--r--app/views/profiles/preferences/update.js.erb4
-rw-r--r--app/views/projects/_activity.html.haml2
-rw-r--r--app/views/projects/blob/diff.html.haml6
-rw-r--r--app/views/projects/boards/_show.html.haml1
-rw-r--r--app/views/projects/boards/components/_card.html.haml10
-rw-r--r--app/views/projects/branches/_branch.html.haml2
-rw-r--r--app/views/projects/branches/new.html.haml8
-rw-r--r--app/views/projects/builds/_header.html.haml9
-rw-r--r--app/views/projects/buttons/_download.html.haml10
-rw-r--r--app/views/projects/ci/pipelines/_pipeline.html.haml92
-rw-r--r--app/views/projects/commit/_pipeline.html.haml2
-rw-r--r--app/views/projects/diffs/_content.html.haml7
-rw-r--r--app/views/projects/diffs/_file_header.html.haml4
-rw-r--r--app/views/projects/diffs/_parallel_view.html.haml7
-rw-r--r--app/views/projects/diffs/_text_file.html.haml7
-rw-r--r--app/views/projects/edit.html.haml2
-rw-r--r--app/views/projects/issues/show.html.haml4
-rw-r--r--app/views/projects/issues/verify.html.haml22
-rw-r--r--app/views/projects/merge_requests/index.html.haml7
-rw-r--r--app/views/projects/merge_requests/widget/_heading.html.haml2
-rw-r--r--app/views/projects/milestones/show.html.haml5
-rw-r--r--app/views/projects/new.html.haml2
-rw-r--r--app/views/projects/notes/_note.html.haml2
-rw-r--r--app/views/projects/pages/_use.html.haml4
-rw-r--r--app/views/projects/pipelines/_info.html.haml6
-rw-r--r--app/views/projects/pipelines/index.html.haml22
-rw-r--r--app/views/projects/pipelines/new.html.haml6
-rw-r--r--app/views/projects/pipelines_settings/_badge.html.haml7
-rw-r--r--app/views/projects/pipelines_settings/_show.html.haml2
-rw-r--r--app/views/projects/snippets/_actions.html.haml4
-rw-r--r--app/views/projects/snippets/verify.html.haml4
-rw-r--r--app/views/projects/tags/index.html.haml2
-rw-r--r--app/views/projects/variables/_form.html.haml2
-rw-r--r--app/views/projects/wikis/_sidebar_wiki_page.html.haml2
-rw-r--r--app/views/search/_category.html.haml26
-rw-r--r--app/views/search/_results.html.haml2
-rw-r--r--app/views/search/results/_snippet_blob.html.haml55
-rw-r--r--app/views/shared/_group_form.html.haml2
-rw-r--r--app/views/shared/_issuable_meta_data.html.haml6
-rw-r--r--app/views/shared/_label.html.haml10
-rw-r--r--app/views/shared/builds/_tabs.html.haml8
-rw-r--r--app/views/shared/icons/_icon_customization.svg1
-rw-r--r--app/views/shared/issuable/_nav.html.haml10
-rw-r--r--app/views/shared/issuable/_search_bar.html.haml10
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml2
-rw-r--r--app/views/shared/members/_member.html.haml2
-rw-r--r--app/views/shared/milestones/_issuables.html.haml6
-rw-r--r--app/views/snippets/_actions.html.haml4
-rw-r--r--app/views/snippets/_snippets_scope_menu.html.haml8
-rw-r--r--app/views/snippets/verify.html.haml4
-rw-r--r--app/views/users/show.html.haml1
-rw-r--r--app/workers/delete_user_worker.rb2
-rw-r--r--app/workers/irker_worker.rb6
-rw-r--r--changelogs/unreleased/12726-preserve-issues-after-deleting-users.yml4
-rw-r--r--changelogs/unreleased/1363-redo-mailroom-support.yml4
-rw-r--r--changelogs/unreleased/14748-runner-version-in-admin-views.yml4
-rw-r--r--changelogs/unreleased/17662-rename-builds.yml4
-rw-r--r--changelogs/unreleased/1937-https-clone-url-username.yml4
-rw-r--r--changelogs/unreleased/20452-remove-require-from-request_profiler-initializer.yml4
-rw-r--r--changelogs/unreleased/20852-getting-started-project-better-blank-state-for-labels-view.yml4
-rw-r--r--changelogs/unreleased/21240_snippets_line_ending.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/22018-api-milestone-merge-requests.yml4
-rw-r--r--changelogs/unreleased/22132-rename-branch-name-params-to-branch.yml4
-rw-r--r--changelogs/unreleased/22466-task-list-alignment.yml4
-rw-r--r--changelogs/unreleased/22638-creating-a-branch-matching-a-wildcard-fails.yml4
-rw-r--r--changelogs/unreleased/22974-trigger-service-events-through-api.yml4
-rw-r--r--changelogs/unreleased/23062-allow-git-log-to-accept-follow-and-skip.yml4
-rw-r--r--changelogs/unreleased/23524-notify-automerge-user-of-failed-build.yml4
-rw-r--r--changelogs/unreleased/23634-remove-project-grouping.yml4
-rw-r--r--changelogs/unreleased/23767-disable-storing-of-sensitive-information.yml4
-rw-r--r--changelogs/unreleased/23819-fix-milestone-counters-to-top-right-of-panel-headings.yml4
-rw-r--r--changelogs/unreleased/24147-delete-env-button.yml4
-rw-r--r--changelogs/unreleased/24462-reduce_ldap_queries_for_lfs.yml4
-rw-r--r--changelogs/unreleased/24606-force-password-reset-on-next-login.yml4
-rw-r--r--changelogs/unreleased/24716-fix-ctrl-click-links.yml4
-rw-r--r--changelogs/unreleased/24795_refactor_merge_request_build_service.yml4
-rw-r--r--changelogs/unreleased/24833-Allow-to-search-by-commit-hash-within-project.yml4
-rw-r--r--changelogs/unreleased/24923_nested_tasks.yml4
-rw-r--r--changelogs/unreleased/25134-mobile-issue-view-doesn-t-show-organization-membership.yml4
-rw-r--r--changelogs/unreleased/25312-search-input-cmd-click-issue.yml4
-rw-r--r--changelogs/unreleased/25360-remove-flash-warning-from-login-page.yml4
-rw-r--r--changelogs/unreleased/25460-replace-word-users-with-members.yml4
-rw-r--r--changelogs/unreleased/25465-todo-done-clicking-is-kind-of-unsafe.yml4
-rw-r--r--changelogs/unreleased/25624-anticipate-obstacles-to-removing-turbolinks.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/25920-create-issue-from-failing-build.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/26087-asciidoc-cicd-badges-snippet.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/26206-fix-download-dropdown.yml4
-rw-r--r--changelogs/unreleased/26315-unify-labels-filter-behavior.yml4
-rw-r--r--changelogs/unreleased/26348-cleanup-navigation-order-groups.yml4
-rw-r--r--changelogs/unreleased/26379-iid-param.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/26500-informative-slack-notifications.yml4
-rw-r--r--changelogs/unreleased/26703-todos-count.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/26875-builds-api-endpoint-skipped-scope.yml4
-rw-r--r--changelogs/unreleased/26900-pipelines-tabs.yml4
-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/26957-tanuki-anim-hang.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/27032-add-a-house-keeping-api-call.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/27287-label-dropdown-error-messages.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/27354-navigation-new-button.yml4
-rw-r--r--changelogs/unreleased/27395-reduce-group-activity-sql-queries-2.yml4
-rw-r--r--changelogs/unreleased/27395-reduce-group-activity-sql-queries.yml4
-rw-r--r--changelogs/unreleased/27480-deploy_keys_should_not_show_up_in_users_keys_list.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/27530-fix-job-dropdown-pipeline-console-error.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/27656-doc-ci-enable-ci.yml4
-rw-r--r--changelogs/unreleased/27762-add-default-artifacts-expiration.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/27840-improve-search-bar-experience.yml4
-rw-r--r--changelogs/unreleased/27873-when-a-commit-appears-in-several-projects-commit-comments-are-shared-across-projects.yml4
-rw-r--r--changelogs/unreleased/27880-pipelines-table-not-showing-commit-branch.yml4
-rw-r--r--changelogs/unreleased/27920-both-wip-messages-showing.yml4
-rw-r--r--changelogs/unreleased/27922-cmd-click-todo-doesn-t-work.yml5
-rw-r--r--changelogs/unreleased/27925-fix-mr-stray-pipelines-api-request.yml4
-rw-r--r--changelogs/unreleased/27932-merge-request-pipelines-displays-json.yml4
-rw-r--r--changelogs/unreleased/27934-left-align-logo.yml4
-rw-r--r--changelogs/unreleased/27939-fix-current-build-arrow.yml4
-rw-r--r--changelogs/unreleased/27943-contribution-list-on-profile-page-is-aligned-right.yml4
-rw-r--r--changelogs/unreleased/27947-missing-margin-between-loading-icon-and-text-in-merge-request-widget.yml4
-rw-r--r--changelogs/unreleased/27955-mr-notification-use-pipeline-language.yml4
-rw-r--r--changelogs/unreleased/27963-tooltips-jobs.yml4
-rw-r--r--changelogs/unreleased/27966-branch-ref-switcher-input-filter-broken.yml4
-rw-r--r--changelogs/unreleased/27987-skipped-pipeline-mr-graph.yml4
-rw-r--r--changelogs/unreleased/27991-success-with-warnings-caret.yml4
-rw-r--r--changelogs/unreleased/28029-improve-blockquote-formatting-on-emails.yml4
-rw-r--r--changelogs/unreleased/28032-tooltips-file-name.yml5
-rw-r--r--changelogs/unreleased/28082-deleted-branch-event-404.yml4
-rw-r--r--changelogs/unreleased/28124-mrs-don-t-show-all-merge-errors.yml4
-rw-r--r--changelogs/unreleased/28186-long-group-names-overflow-out-of-todos-view.yml4
-rw-r--r--changelogs/unreleased/28204-option-to-disable-webpack-dev-server-livereload.yml4
-rw-r--r--changelogs/unreleased/28236-browse-button-dropping.yml4
-rw-r--r--changelogs/unreleased/28247-timeloops-bug.yml4
-rw-r--r--changelogs/unreleased/28253-fix-buid-scroll-button-position.yml4
-rw-r--r--changelogs/unreleased/28262-horizontal-scrolling-issue-on-long-project-names.yml4
-rw-r--r--changelogs/unreleased/28303-change-development-tanuki-favicon-colors-to-match-logo.yml4
-rw-r--r--changelogs/unreleased/28329-allow-slash-in-slash-command-args.yml4
-rw-r--r--changelogs/unreleased/28353-little-grammar-issue.yml4
-rw-r--r--changelogs/unreleased/28366-renamed-file-tooltip-contains-html.yml4
-rw-r--r--changelogs/unreleased/28367-fix-unfold-diff-line-number-copy-paste.yml4
-rw-r--r--changelogs/unreleased/28389-ux-problem-with-pipeline-coverage-placeholder.yml4
-rw-r--r--changelogs/unreleased/28450-test-compiling-frontend-assets-for-production-in-ci.yml4
-rw-r--r--changelogs/unreleased/28458-present-gitlab-version-for-v4-changes-on-docs.yml4
-rw-r--r--changelogs/unreleased/28462-fix-delimiter-removes-issue-in-todo-counter.yml4
-rw-r--r--changelogs/unreleased/28524-gitlab-ci-yml-coverage-key-is-unknown.yml4
-rw-r--r--changelogs/unreleased/28696-improve-grammar-gitlab-flow-doc.yml4
-rw-r--r--changelogs/unreleased/28723-consistent-handling-indexof.yml4
-rw-r--r--changelogs/unreleased/28837-remove-help-duplicate.yml4
-rw-r--r--changelogs/unreleased/3874-correctly-return-json-on-delete-responses.yml4
-rw-r--r--changelogs/unreleased/395-fix-notification-when-group-set-to-watch.yml4
-rw-r--r--changelogs/unreleased/6073_project_api.yml4
-rw-r--r--changelogs/unreleased/8-15-stable.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/9381-authentiq-backchannel-logout.yml4
-rw-r--r--changelogs/unreleased/Add-a-shash-command-for-target-merge-request-branch.yml4
-rw-r--r--changelogs/unreleased/add-filtered-search-to-mr.yml4
-rw-r--r--changelogs/unreleased/add-yarn-documentation.yml4
-rw-r--r--changelogs/unreleased/add_mr_info_to_issues_list.yml4
-rw-r--r--changelogs/unreleased/add_project_update_hook.yml4
-rw-r--r--changelogs/unreleased/api-empty-return.yml4
-rw-r--r--changelogs/unreleased/api-fix-files.yml4
-rw-r--r--changelogs/unreleased/api-notes-entity-fields.yml4
-rw-r--r--changelogs/unreleased/api-post-block.yml4
-rw-r--r--changelogs/unreleased/api-remove-deploy-key-disable.yml4
-rw-r--r--changelogs/unreleased/api-remove-owned-groups.yml4
-rw-r--r--changelogs/unreleased/api-remove-snippets-expires-at.yml4
-rw-r--r--changelogs/unreleased/api-star-restful.yml4
-rw-r--r--changelogs/unreleased/api-subscription-restful.yml4
-rw-r--r--changelogs/unreleased/api-todos-restful.yml4
-rw-r--r--changelogs/unreleased/artifactsdoc.yml4
-rw-r--r--changelogs/unreleased/babel-all-the-things.yml5
-rw-r--r--changelogs/unreleased/branch_deletion.yml4
-rw-r--r--changelogs/unreleased/broken_iamge_when_doing_offline_update_check-help_page-.yml4
-rw-r--r--changelogs/unreleased/clipboard-button-commit-sha.yml3
-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_branch_repo_less.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/document-how-to-vue.yml4
-rw-r--r--changelogs/unreleased/dont-delete-assigned-issuables.yml4
-rw-r--r--changelogs/unreleased/dynamic-project-title-fixture.yml4
-rw-r--r--changelogs/unreleased/dynamic-todos-fixture.yml4
-rw-r--r--changelogs/unreleased/dz-nested-groups-improvements-2.yml4
-rw-r--r--changelogs/unreleased/empty-selection-reply-shortcut.yml4
-rw-r--r--changelogs/unreleased/fe-commit-mr-pipelines.yml4
-rw-r--r--changelogs/unreleased/feature-brand-logo-in-emails.yml4
-rw-r--r--changelogs/unreleased/feature-github-find-users-by-email.yml4
-rw-r--r--changelogs/unreleased/feature-runners-registration-deletion-v4-api.yml4
-rw-r--r--changelogs/unreleased/feature-success-warning-icons-in-stages-builds.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-ar-connection-leaks.yml4
-rw-r--r--changelogs/unreleased/fix-ci-build-policy.yml4
-rw-r--r--changelogs/unreleased/fix-deleting-project-again.yml4
-rw-r--r--changelogs/unreleased/fix-depr-warn.yml4
-rw-r--r--changelogs/unreleased/fix-gb-backwards-compatibility-coverage-ci-yml.yml4
-rw-r--r--changelogs/unreleased/fix-gb-pipeline-retry-builds-started.yml4
-rw-r--r--changelogs/unreleased/fix-gb-pipeline-retry-cancel-buttons-consistency.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-group-members.yml4
-rw-r--r--changelogs/unreleased/fix-job-to-pipeline-renaming.yml4
-rw-r--r--changelogs/unreleased/fix-references-header-parsing.yml5
-rw-r--r--changelogs/unreleased/fix-scroll-test.yml4
-rw-r--r--changelogs/unreleased/fix-users-deleting-public-deployment-keys.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/get-rid-of-water-from-notification_service_spec-to-make-it-DRY.yml4
-rw-r--r--changelogs/unreleased/gfm-autocomplete-fixes.yml4
-rw-r--r--changelogs/unreleased/git_to_html_redirection.yml4
-rw-r--r--changelogs/unreleased/go-go-gadget-webpack.yml4
-rw-r--r--changelogs/unreleased/group-label-sidebar-link.yml4
-rw-r--r--changelogs/unreleased/group-memebrs-owner-level.yml4
-rw-r--r--changelogs/unreleased/hardcode-title-system-note.yml4
-rw-r--r--changelogs/unreleased/improve-ci-example-php-doc.yml4
-rw-r--r--changelogs/unreleased/improve-handleLocationHash-tests.yml4
-rw-r--r--changelogs/unreleased/instrument-in-karma.yml4
-rw-r--r--changelogs/unreleased/issuable-sidebar-bug.yml4
-rw-r--r--changelogs/unreleased/issue-20428.yml4
-rw-r--r--changelogs/unreleased/issue-sidebar-empty-assignee.yml4
-rw-r--r--changelogs/unreleased/issue-tags-layout.yml4
-rw-r--r--changelogs/unreleased/issue_19262.yml4
-rw-r--r--changelogs/unreleased/issue_23317.yml4
-rw-r--r--changelogs/unreleased/issue_24815.yml4
-rw-r--r--changelogs/unreleased/issue_25112.yml4
-rw-r--r--changelogs/unreleased/issue_27211.yml4
-rw-r--r--changelogs/unreleased/jej-pages-picked-from-ee.yml4
-rw-r--r--changelogs/unreleased/label-promotion.yml4
-rw-r--r--changelogs/unreleased/lfs-noauth-public-repo.yml4
-rw-r--r--changelogs/unreleased/lnovy-gitlab-ce-empty-variables.yml4
-rw-r--r--changelogs/unreleased/markdown-plantuml.yml4
-rw-r--r--changelogs/unreleased/merge-request-tabs-fixture.yml4
-rw-r--r--changelogs/unreleased/misalinged-discussion-with-no-avatar-26854.yml4
-rw-r--r--changelogs/unreleased/mock-ci-service.yml4
-rw-r--r--changelogs/unreleased/moving-issue-with-two-list-labels.yml4
-rw-r--r--changelogs/unreleased/mr-diff-comment-button.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/no-sidebar-on-action-btn-click.yml4
-rw-r--r--changelogs/unreleased/no_project_notes.yml4
-rw-r--r--changelogs/unreleased/only-create-unmergeable-todo-once.yml4
-rw-r--r--changelogs/unreleased/only-yield-valid-reference-matches.yml4
-rw-r--r--changelogs/unreleased/paginate-all-the-things.yml4
-rw-r--r--changelogs/unreleased/pms-lowercase-system-notes.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/refresh-permissions-when-moving-projects.yml4
-rw-r--r--changelogs/unreleased/relative-url-assets.yml4
-rw-r--r--changelogs/unreleased/remove-deploy-key-endpoint.yml4
-rw-r--r--changelogs/unreleased/remove-jquery-ui-plugins.yml4
-rw-r--r--changelogs/unreleased/remove-sidekiq-backup-ar-threads.yml4
-rw-r--r--changelogs/unreleased/rename-retry-failed-pipeline-to-retry.yml4
-rw-r--r--changelogs/unreleased/route-map.yml4
-rw-r--r--changelogs/unreleased/rs-warden-blocked-users.yml4
-rw-r--r--changelogs/unreleased/rss-btn-alignment-fix.yml4
-rw-r--r--changelogs/unreleased/seed-abuse-reports.yml4
-rw-r--r--changelogs/unreleased/sh-add-index-to-ci-trigger-requests.yml4
-rw-r--r--changelogs/unreleased/sh-add-labels-index.yml4
-rw-r--r--changelogs/unreleased/sh-bump-hashie-to-3-5-5.yml4
-rw-r--r--changelogs/unreleased/sh-delete-user-permission-check.yml4
-rw-r--r--changelogs/unreleased/slash-commands-typo.yml4
-rw-r--r--changelogs/unreleased/small-screen-fullscreen-button.yml4
-rw-r--r--changelogs/unreleased/snippets-search-performance.yml4
-rw-r--r--changelogs/unreleased/snippets-search.yml4
-rw-r--r--changelogs/unreleased/ssh-key-paste.yml4
-rw-r--r--changelogs/unreleased/task_list_refactor.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/unified-member-api-response.yml4
-rw-r--r--changelogs/unreleased/update-vue-2-1.yml4
-rw-r--r--changelogs/unreleased/upgrade-babel-v6.yml4
-rw-r--r--changelogs/unreleased/upgrade-omniauth.yml4
-rw-r--r--changelogs/unreleased/upgrade-webpack-v2-2.yml4
-rw-r--r--changelogs/unreleased/user-callouts.yml4
-rw-r--r--changelogs/unreleased/wip-mr-from-commits.yml4
-rw-r--r--changelogs/unreleased/zj-format-chat-messages.yml4
-rw-r--r--changelogs/unreleased/zj-remove-deprecated-ci-service.yml4
-rw-r--r--changelogs/unreleased/zj-requeue-pending-delete.yml4
-rw-r--r--config/dependency_decisions.yml6
-rw-r--r--config/gitlab.yml.example10
-rw-r--r--config/initializers/1_settings.rb39
-rw-r--r--config/initializers/4_ci_app.rb8
-rw-r--r--config/initializers/acts_as_taggable.rb5
-rw-r--r--config/initializers/devise.rb19
-rw-r--r--config/initializers/gollum.rb2
-rw-r--r--config/initializers/health_check.rb4
-rw-r--r--config/initializers/metrics.rb14
-rw-r--r--config/initializers/mysql_ignore_postgresql_options.rb2
-rw-r--r--config/initializers/rack_lineprof.rb2
-rw-r--r--config/initializers/trusted_proxies.rb2
-rw-r--r--config/initializers/workhorse_multipart.rb2
-rw-r--r--config/karma.config.js11
-rw-r--r--config/mail_room.yml5
-rw-r--r--config/routes/dashboard.rb3
-rw-r--r--config/routes/sidekiq.rb2
-rw-r--r--config/routes/wiki.rb2
-rw-r--r--config/sidekiq_queues.yml2
-rw-r--r--config/webpack.config.js34
-rw-r--r--db/fixtures/development/17_cycle_analytics.rb10
-rw-r--r--db/fixtures/development/18_abuse_reports.rb5
-rw-r--r--db/fixtures/development/19_nested_groups.rb69
-rw-r--r--db/migrate/20160610201627_migrate_users_notification_level.rb4
-rw-r--r--db/migrate/20160829114652_add_markdown_cache_columns.rb2
-rw-r--r--db/migrate/20160831214543_migrate_project_features.rb2
-rw-r--r--db/migrate/20161019190736_migrate_sidekiq_queues_from_default.rb4
-rw-r--r--db/migrate/20161024042317_migrate_mailroom_queue_from_default.rb4
-rw-r--r--db/migrate/20161124141322_migrate_process_commit_worker_jobs.rb4
-rw-r--r--db/migrate/20161128142110_remove_unnecessary_indexes.rb4
-rw-r--r--db/migrate/20161207231620_fixup_environment_name_uniqueness.rb8
-rw-r--r--db/migrate/20170206115204_add_column_ghost_to_users.rb11
-rw-r--r--db/migrate/20170210103609_add_index_to_user_agent_detail.rb14
-rw-r--r--db/migrate/20170214084746_add_default_artifacts_expiration_to_application_settings.rb11
-rw-r--r--db/migrate/20170216135621_add_index_for_latest_successful_pipeline.rb10
-rw-r--r--db/migrate/20170216141440_drop_index_for_builds_project_status.rb8
-rw-r--r--db/post_migrate/20161109150329_fix_project_records_with_invalid_visibility.rb14
-rw-r--r--db/post_migrate/20161221153951_rename_reserved_project_names.rb2
-rw-r--r--db/post_migrate/20170215200045_remove_theme_id_from_users.rb9
-rw-r--r--db/schema.rb13
-rw-r--r--doc/README.md2
-rw-r--r--doc/administration/auth/authentiq.md2
-rw-r--r--doc/administration/container_registry.md40
-rw-r--r--doc/administration/monitoring/prometheus/gitlab_monitor_exporter.md2
-rw-r--r--doc/administration/pages/index.md173
-rw-r--r--doc/administration/pages/source.md251
-rw-r--r--doc/administration/reply_by_email.md4
-rw-r--r--doc/api/README.md1
-rw-r--r--doc/api/award_emoji.md42
-rw-r--r--doc/api/boards.md13
-rw-r--r--doc/api/branches.md12
-rw-r--r--doc/api/broadcast_messages.md14
-rw-r--r--doc/api/build_triggers.md10
-rw-r--r--doc/api/build_variables.md7
-rw-r--r--doc/api/commits.md8
-rw-r--r--doc/api/deploy_keys.md41
-rw-r--r--doc/api/enviroments.md11
-rw-r--r--doc/api/groups.md15
-rw-r--r--doc/api/issues.md49
-rw-r--r--doc/api/keys.md1
-rw-r--r--doc/api/labels.md42
-rw-r--r--doc/api/merge_requests.md8
-rw-r--r--doc/api/milestones.md13
-rw-r--r--doc/api/notes.md84
-rw-r--r--doc/api/pipelines.md2
-rw-r--r--doc/api/projects.md26
-rw-r--r--doc/api/repository_files.md24
-rw-r--r--doc/api/runners.md24
-rw-r--r--doc/api/services.md35
-rw-r--r--doc/api/session.md1
-rw-r--r--doc/api/system_hooks.md19
-rw-r--r--doc/api/tags.md5
-rw-r--r--doc/api/todos.md15
-rw-r--r--doc/api/users.md15
-rw-r--r--doc/api/v3_to_v4.md42
-rw-r--r--doc/ci/docker/using_docker_build.md24
-rw-r--r--doc/ci/docker/using_docker_images.md15
-rw-r--r--doc/ci/pipelines.md2
-rw-r--r--doc/ci/variables/README.md3
-rw-r--r--doc/ci/yaml/README.md6
-rw-r--r--doc/customization/branded_page_and_email_header.md15
-rw-r--r--doc/customization/branded_page_and_email_header/appearance.pngbin0 -> 10253 bytes
-rw-r--r--doc/customization/branded_page_and_email_header/custom_brand_header.pngbin0 -> 10014 bytes
-rw-r--r--doc/customization/branded_page_and_email_header/custom_email_header.pngbin0 -> 37472 bytes
-rw-r--r--doc/development/ci_setup.md3
-rw-r--r--doc/development/frontend.md5
-rw-r--r--doc/development/licensing.md7
-rw-r--r--doc/development/limit_ee_conflicts.md31
-rw-r--r--doc/development/testing.md27
-rw-r--r--doc/development/ux_guide/copy.md2
-rw-r--r--doc/development/ux_guide/img/harry-robison.pngbin0 -> 10712 bytes
-rw-r--r--doc/development/ux_guide/img/james-mackey.pngbin0 -> 11147 bytes
-rw-r--r--doc/development/ux_guide/img/steven-lyons.pngbin0 -> 9323 bytes
-rw-r--r--doc/development/ux_guide/users.md170
-rw-r--r--doc/install/installation.md33
-rw-r--r--doc/integration/auth0.md2
-rw-r--r--doc/integration/ldap.md4
-rw-r--r--doc/integration/saml.md10
-rw-r--r--doc/pages/README.md2
-rw-r--r--doc/pages/getting_started_part_one.md1
-rw-r--r--doc/pages/getting_started_part_three.md1
-rw-r--r--doc/pages/getting_started_part_two.md1
-rw-r--r--doc/profile/preferences.md7
-rw-r--r--doc/raketasks/backup_restore.md22
-rw-r--r--doc/update/8.16-to-8.17.md31
-rw-r--r--doc/user/admin_area/settings/continuous_integration.md24
-rw-r--r--doc/user/admin_area/settings/img/admin_area_default_artifacts_expiration.pngbin0 -> 14656 bytes
-rw-r--r--doc/user/admin_area/settings/img/admin_area_maximum_artifacts_size.pngbin3447 -> 12917 bytes
-rw-r--r--doc/user/profile/account/two_factor_authentication.md2
-rw-r--r--doc/user/project/integrations/img/mattermost_configuration.pngbin73502 -> 249592 bytes
-rw-r--r--doc/user/project/integrations/img/slack_configuration.pngbin29825 -> 229050 bytes
-rw-r--r--doc/user/project/integrations/mattermost.md11
-rw-r--r--doc/user/project/integrations/mock_ci.md13
-rw-r--r--doc/user/project/integrations/slack.md10
-rw-r--r--doc/user/project/pages/getting_started_part_one.md266
-rw-r--r--doc/user/project/pages/getting_started_part_three.md383
-rw-r--r--doc/user/project/pages/getting_started_part_two.md152
-rw-r--r--doc/user/project/pages/img/add_certificate_to_pages.pngbin0 -> 14608 bytes
-rw-r--r--doc/user/project/pages/img/choose_ci_template.pngbin0 -> 23532 bytes
-rw-r--r--doc/user/project/pages/img/dns_a_record_example.pngbin0 -> 4709 bytes
-rw-r--r--doc/user/project/pages/img/dns_cname_record_example.pngbin0 -> 4983 bytes
-rw-r--r--doc/user/project/pages/img/pages_create_project.pngbin33597 -> 6063 bytes
-rw-r--r--doc/user/project/pages/img/pages_create_user_page.pngbin87071 -> 14435 bytes
-rw-r--r--doc/user/project/pages/img/pages_dns_details.pngbin34686 -> 5351 bytes
-rw-r--r--doc/user/project/pages/img/pages_multiple_domains.pngbin63716 -> 12936 bytes
-rw-r--r--doc/user/project/pages/img/pages_new_domain_button.pngbin51136 -> 8763 bytes
-rw-r--r--doc/user/project/pages/img/pages_remove.pngbin27259 -> 3810 bytes
-rw-r--r--doc/user/project/pages/img/pages_upload_cert.pngbin103730 -> 22907 bytes
-rw-r--r--doc/user/project/pages/img/remove_fork_relashionship.pngbin0 -> 13642 bytes
-rw-r--r--doc/user/project/pages/img/setup_ci.pngbin0 -> 10032 bytes
-rw-r--r--doc/user/project/pages/index.md462
-rw-r--r--doc/user/project/pages/introduction.md447
-rw-r--r--doc/user/project/pipelines/job_artifacts.md31
-rw-r--r--doc/user/project/pipelines/settings.md8
-rw-r--r--doc/user/snippets.md19
-rw-r--r--doc/workflow/README.md1
-rw-r--r--doc/workflow/gitlab_flow.md4
-rw-r--r--doc/workflow/lfs/manage_large_binaries_with_git_lfs.md6
-rw-r--r--doc/workflow/todos.md29
-rw-r--r--features/dashboard/dashboard.feature1
-rw-r--r--features/dashboard/issues.feature21
-rw-r--r--features/project/commits/branches.feature8
-rw-r--r--features/project/labels.feature15
-rw-r--r--features/project/merge_requests.feature7
-rw-r--r--features/project/merge_requests/revert.feature1
-rw-r--r--features/steps/dashboard/issues.rb91
-rw-r--r--features/steps/dashboard/todos.rb23
-rw-r--r--features/steps/explore/projects.rb2
-rw-r--r--features/steps/group/milestones.rb4
-rw-r--r--features/steps/project/builds/artifacts.rb2
-rw-r--r--features/steps/project/commits/branches.rb24
-rw-r--r--features/steps/project/labels.rb32
-rw-r--r--features/steps/shared/builds.rb2
-rw-r--r--features/steps/shared/issuable.rb2
-rw-r--r--lib/api/api.rb23
-rw-r--r--lib/api/api_guard.rb6
-rw-r--r--lib/api/award_emoji.rb10
-rw-r--r--lib/api/boards.rb17
-rw-r--r--lib/api/branches.rb22
-rw-r--r--lib/api/broadcast_messages.rb2
-rw-r--r--lib/api/builds.rb2
-rw-r--r--lib/api/commit_statuses.rb2
-rw-r--r--lib/api/commits.rb21
-rw-r--r--lib/api/deploy_keys.rb36
-rw-r--r--lib/api/entities.rb10
-rw-r--r--lib/api/environments.rb2
-rw-r--r--lib/api/files.rb14
-rw-r--r--lib/api/groups.rb16
-rw-r--r--lib/api/helpers.rb28
-rw-r--r--lib/api/helpers/pagination.rb2
-rw-r--r--lib/api/helpers/runner.rb23
-rw-r--r--lib/api/issues.rb20
-rw-r--r--lib/api/labels.rb10
-rw-r--r--lib/api/members.rb34
-rw-r--r--lib/api/merge_request_diffs.rb6
-rw-r--r--lib/api/merge_requests.rb3
-rw-r--r--lib/api/milestones.rb22
-rw-r--r--lib/api/notes.rb6
-rw-r--r--lib/api/pagination_params.rb4
-rw-r--r--lib/api/pipelines.rb6
-rw-r--r--lib/api/project_hooks.rb13
-rw-r--r--lib/api/project_snippets.rb8
-rw-r--r--lib/api/projects.rb24
-rw-r--r--lib/api/repositories.rb16
-rw-r--r--lib/api/runner.rb52
-rw-r--r--lib/api/runners.rb10
-rw-r--r--lib/api/services.rb19
-rw-r--r--lib/api/settings.rb7
-rw-r--r--lib/api/snippets.rb10
-rw-r--r--lib/api/subscriptions.rb5
-rw-r--r--lib/api/system_hooks.rb12
-rw-r--r--lib/api/tags.rb16
-rw-r--r--lib/api/templates.rb12
-rw-r--r--lib/api/todos.rb8
-rw-r--r--lib/api/triggers.rb13
-rw-r--r--lib/api/users.rb26
-rw-r--r--lib/api/v3/award_emoji.rb59
-rw-r--r--lib/api/v3/boards.rb72
-rw-r--r--lib/api/v3/branches.rb51
-rw-r--r--lib/api/v3/broadcast_messages.rb31
-rw-r--r--lib/api/v3/commits.rb198
-rw-r--r--lib/api/v3/entities.rb34
-rw-r--r--lib/api/v3/environments.rb29
-rw-r--r--lib/api/v3/files.rb138
-rw-r--r--lib/api/v3/groups.rb38
-rw-r--r--lib/api/v3/issues.rb15
-rw-r--r--lib/api/v3/labels.rb34
-rw-r--r--lib/api/v3/members.rb8
-rw-r--r--lib/api/v3/merge_requests.rb2
-rw-r--r--lib/api/v3/notes.rb148
-rw-r--r--lib/api/v3/project_snippets.rb10
-rw-r--r--lib/api/v3/projects.rb13
-rw-r--r--lib/api/v3/repositories.rb55
-rw-r--r--lib/api/v3/runners.rb65
-rw-r--r--lib/api/v3/services.rb573
-rw-r--r--lib/api/v3/subscriptions.rb53
-rw-r--r--lib/api/v3/system_hooks.rb32
-rw-r--r--lib/api/v3/tags.rb40
-rw-r--r--lib/api/v3/todos.rb30
-rw-r--r--lib/api/v3/triggers.rb30
-rw-r--r--lib/api/v3/users.rb149
-rw-r--r--lib/api/v3/variables.rb29
-rw-r--r--lib/api/variables.rb6
-rw-r--r--lib/backup/database.rb70
-rw-r--r--lib/backup/files.rb21
-rw-r--r--lib/backup/manager.rb26
-rw-r--r--lib/backup/repository.rb15
-rw-r--r--lib/backup/uploads.rb1
-rw-r--r--lib/banzai/filter/abstract_reference_filter.rb18
-rw-r--r--lib/banzai/filter/autolink_filter.rb2
-rw-r--r--lib/banzai/filter/gollum_tags_filter.rb11
-rw-r--r--lib/banzai/filter/image_link_filter.rb9
-rw-r--r--lib/banzai/filter/issue_reference_filter.rb11
-rw-r--r--lib/banzai/filter/plantuml_filter.rb2
-rw-r--r--lib/bitbucket/connection.rb10
-rw-r--r--lib/bitbucket/representation/repo.rb2
-rw-r--r--lib/ci/ansi2html.rb52
-rw-r--r--lib/ci/api/builds.rb6
-rw-r--r--lib/ci/api/helpers.rb6
-rw-r--r--lib/ci/api/runners.rb46
-rw-r--r--lib/ci/api/triggers.rb43
-rw-r--r--lib/container_registry/client.rb2
-rw-r--r--lib/extracts_path.rb2
-rw-r--r--lib/file_size_validator.rb4
-rw-r--r--lib/gitlab/access.rb4
-rw-r--r--lib/gitlab/auth.rb4
-rw-r--r--lib/gitlab/award_emoji.rb11
-rw-r--r--lib/gitlab/badge/build/template.rb2
-rw-r--r--lib/gitlab/badge/coverage/template.rb2
-rw-r--r--lib/gitlab/badge/metadata.rb4
-rw-r--r--lib/gitlab/changes_list.rb2
-rw-r--r--lib/gitlab/chat_commands/presenters/issue_base.rb2
-rw-r--r--lib/gitlab/chat_commands/presenters/issue_new.rb4
-rw-r--r--lib/gitlab/ci/build/artifacts/metadata/entry.rb6
-rw-r--r--lib/gitlab/ci/config/entry/artifacts.rb2
-rw-r--r--lib/gitlab/ci/config/entry/cache.rb2
-rw-r--r--lib/gitlab/ci/config/entry/environment.rb8
-rw-r--r--lib/gitlab/ci/config/entry/job.rb2
-rw-r--r--lib/gitlab/conflict/file.rb11
-rw-r--r--lib/gitlab/current_settings.rb4
-rw-r--r--lib/gitlab/cycle_analytics/code_stage.rb4
-rw-r--r--lib/gitlab/cycle_analytics/issue_stage.rb4
-rw-r--r--lib/gitlab/cycle_analytics/plan_stage.rb4
-rw-r--r--lib/gitlab/cycle_analytics/production_stage.rb4
-rw-r--r--lib/gitlab/cycle_analytics/review_stage.rb4
-rw-r--r--lib/gitlab/cycle_analytics/staging_stage.rb4
-rw-r--r--lib/gitlab/cycle_analytics/test_stage.rb4
-rw-r--r--lib/gitlab/data_builder/build.rb10
-rw-r--r--lib/gitlab/database.rb7
-rw-r--r--lib/gitlab/database/median.rb1
-rw-r--r--lib/gitlab/database/migration_helpers.rb11
-rw-r--r--lib/gitlab/diff/highlight.rb2
-rw-r--r--lib/gitlab/diff/inline_diff_marker.rb2
-rw-r--r--lib/gitlab/diff/parser.rb2
-rw-r--r--lib/gitlab/diff/position.rb19
-rw-r--r--lib/gitlab/downtime_check/message.rb4
-rw-r--r--lib/gitlab/ee_compat_check.rb2
-rw-r--r--lib/gitlab/email/handler.rb2
-rw-r--r--lib/gitlab/email/reply_parser.rb11
-rw-r--r--lib/gitlab/exclusive_lease.rb2
-rw-r--r--lib/gitlab/file_detector.rb2
-rw-r--r--lib/gitlab/git/blob.rb157
-rw-r--r--lib/gitlab/git/commit.rb6
-rw-r--r--lib/gitlab/git/index.rb126
-rw-r--r--lib/gitlab/git/repository.rb109
-rw-r--r--lib/gitlab/git_access.rb6
-rw-r--r--lib/gitlab/github_import/base_formatter.rb18
-rw-r--r--lib/gitlab/github_import/client.rb8
-rw-r--r--lib/gitlab/github_import/comment_formatter.rb10
-rw-r--r--lib/gitlab/github_import/importer.rb21
-rw-r--r--lib/gitlab/github_import/issuable_formatter.rb30
-rw-r--r--lib/gitlab/github_import/user_formatter.rb45
-rw-r--r--lib/gitlab/gon_helper.rb2
-rw-r--r--lib/gitlab/import_export.rb4
-rw-r--r--lib/gitlab/import_export/command_line_util.rb12
-rw-r--r--lib/gitlab/import_export/repo_restorer.rb21
-rw-r--r--lib/gitlab/ldap/person.rb4
-rw-r--r--lib/gitlab/metrics/instrumentation.rb11
-rw-r--r--lib/gitlab/metrics/rack_middleware.rb6
-rw-r--r--lib/gitlab/metrics/subscribers/action_view.rb2
-rw-r--r--lib/gitlab/metrics/system.rb2
-rw-r--r--lib/gitlab/metrics/transaction.rb2
-rw-r--r--lib/gitlab/middleware/multipart.rb2
-rw-r--r--lib/gitlab/middleware/webpack_proxy.rb12
-rw-r--r--lib/gitlab/o_auth/user.rb11
-rw-r--r--lib/gitlab/optimistic_locking.rb6
-rw-r--r--lib/gitlab/redis.rb21
-rw-r--r--lib/gitlab/reference_extractor.rb2
-rw-r--r--lib/gitlab/regex.rb11
-rw-r--r--lib/gitlab/request_profiler.rb2
-rw-r--r--lib/gitlab/saml/user.rb11
-rw-r--r--lib/gitlab/sanitizers/svg/whitelist.rb25
-rw-r--r--lib/gitlab/search_results.rb22
-rw-r--r--lib/gitlab/seeder.rb2
-rw-r--r--lib/gitlab/shell.rb8
-rw-r--r--lib/gitlab/sherlock/query.rb11
-rw-r--r--lib/gitlab/slash_commands/extractor.rb2
-rw-r--r--lib/gitlab/template/gitlab_ci_yml_template.rb2
-rw-r--r--lib/gitlab/themes.rb87
-rw-r--r--lib/gitlab/upgrader.rb15
-rw-r--r--lib/gitlab/url_sanitizer.rb2
-rw-r--r--lib/gitlab/visibility_level.rb4
-rw-r--r--lib/gitlab/workhorse.rb8
-rw-r--r--lib/tasks/brakeman.rake2
-rw-r--r--lib/tasks/cache.rake2
-rw-r--r--lib/tasks/dev.rake2
-rw-r--r--lib/tasks/downtime_check.rake10
-rw-r--r--lib/tasks/eslint.rake7
-rw-r--r--lib/tasks/flay.rake2
-rw-r--r--lib/tasks/gitlab/assets.rake24
-rw-r--r--lib/tasks/gitlab/check.rake65
-rw-r--r--lib/tasks/gitlab/cleanup.rake11
-rw-r--r--lib/tasks/gitlab/db.rake8
-rw-r--r--lib/tasks/gitlab/git.rake2
-rw-r--r--lib/tasks/gitlab/import.rake2
-rw-r--r--lib/tasks/gitlab/import_export.rake2
-rw-r--r--lib/tasks/gitlab/info.rake19
-rw-r--r--lib/tasks/gitlab/shell.rake6
-rw-r--r--lib/tasks/gitlab/sidekiq.rake6
-rw-r--r--lib/tasks/gitlab/task_helpers.rb49
-rw-r--r--lib/tasks/gitlab/test.rake12
-rw-r--r--lib/tasks/gitlab/track_deployment.rake4
-rw-r--r--lib/tasks/gitlab/update_templates.rake2
-rw-r--r--lib/tasks/gitlab/web_hook.rake6
-rw-r--r--lib/tasks/karma.rake9
-rw-r--r--lib/tasks/lint.rake1
-rw-r--r--lib/tasks/migrate/migrate_iids.rake2
-rw-r--r--lib/tasks/services.rake10
-rw-r--r--lib/tasks/sidekiq.rake8
-rw-r--r--lib/tasks/spec.rake30
-rw-r--r--lib/tasks/spinach.rake2
-rw-r--r--lib/tasks/yarn.rake40
-rw-r--r--package.json28
-rw-r--r--rubocop/cop/gem_fetcher.rb4
-rw-r--r--rubocop/cop/migration/add_column.rb4
-rw-r--r--rubocop/cop/migration/add_column_with_default.rb2
-rw-r--r--rubocop/cop/migration/add_concurrent_foreign_key.rb2
-rw-r--r--rubocop/cop/migration/add_index.rb2
-rw-r--r--spec/config/mail_room_spec.rb4
-rw-r--r--spec/controllers/admin/runners_controller_spec.rb85
-rw-r--r--spec/controllers/blob_controller_spec.rb8
-rw-r--r--spec/controllers/dashboard/todos_controller_spec.rb25
-rw-r--r--spec/controllers/profiles/personal_access_tokens_spec.rb4
-rw-r--r--spec/controllers/profiles/preferences_controller_spec.rb6
-rw-r--r--spec/controllers/projects/blame_controller_spec.rb4
-rw-r--r--spec/controllers/projects/blob_controller_spec.rb73
-rw-r--r--spec/controllers/projects/boards/issues_controller_spec.rb6
-rw-r--r--spec/controllers/projects/boards/lists_controller_spec.rb10
-rw-r--r--spec/controllers/projects/boards_controller_spec.rb8
-rw-r--r--spec/controllers/projects/branches_controller_spec.rb67
-rw-r--r--spec/controllers/projects/commit_controller_spec.rb52
-rw-r--r--spec/controllers/projects/commits_controller_spec.rb8
-rw-r--r--spec/controllers/projects/compare_controller_spec.rb32
-rw-r--r--spec/controllers/projects/cycle_analytics_controller_spec.rb8
-rw-r--r--spec/controllers/projects/find_file_controller_spec.rb8
-rw-r--r--spec/controllers/projects/forks_controller_spec.rb12
-rw-r--r--spec/controllers/projects/graphs_controller_spec.rb2
-rw-r--r--spec/controllers/projects/group_links_controller_spec.rb12
-rw-r--r--spec/controllers/projects/imports_controller_spec.rb18
-rw-r--r--spec/controllers/projects/issues_controller_spec.rb175
-rw-r--r--spec/controllers/projects/labels_controller_spec.rb16
-rw-r--r--spec/controllers/projects/mattermosts_controller_spec.rb4
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb75
-rw-r--r--spec/controllers/projects/pipelines_controller_spec.rb21
-rw-r--r--spec/controllers/projects/protected_branches_controller_spec.rb2
-rw-r--r--spec/controllers/projects/raw_controller_spec.rb8
-rw-r--r--spec/controllers/projects/refs_controller_spec.rb4
-rw-r--r--spec/controllers/projects/releases_controller_spec.rb6
-rw-r--r--spec/controllers/projects/repositories_controller_spec.rb6
-rw-r--r--spec/controllers/projects/runners_controller_spec.rb75
-rw-r--r--spec/controllers/projects/snippets_controller_spec.rb249
-rw-r--r--spec/controllers/projects/tags_controller_spec.rb4
-rw-r--r--spec/controllers/projects/templates_controller_spec.rb12
-rw-r--r--spec/controllers/projects/todo_controller_spec.rb8
-rw-r--r--spec/controllers/projects/tree_controller_spec.rb6
-rw-r--r--spec/controllers/projects/uploads_controller_spec.rb8
-rw-r--r--spec/controllers/projects/variables_controller_spec.rb8
-rw-r--r--spec/controllers/projects_controller_spec.rb72
-rw-r--r--spec/controllers/snippets_controller_spec.rb177
-rw-r--r--spec/factories/ci/builds.rb25
-rw-r--r--spec/factories/commits.rb10
-rw-r--r--spec/factories/projects.rb15
-rw-r--r--spec/factories/services.rb10
-rw-r--r--spec/factories/todos.rb4
-rw-r--r--spec/factories/users.rb5
-rw-r--r--spec/features/admin/admin_abuse_reports_spec.rb19
-rw-r--r--spec/features/admin/admin_disables_git_access_protocol_spec.rb2
-rw-r--r--spec/features/admin/admin_runners_spec.rb2
-rw-r--r--spec/features/atom/dashboard_issues_spec.rb2
-rw-r--r--spec/features/atom/issues_spec.rb4
-rw-r--r--spec/features/atom/users_spec.rb2
-rw-r--r--spec/features/boards/add_issues_modal_spec.rb6
-rw-r--r--spec/features/boards/boards_spec.rb6
-rw-r--r--spec/features/boards/sidebar_spec.rb139
-rw-r--r--spec/features/commits_spec.rb4
-rw-r--r--spec/features/dashboard/active_tab_spec.rb7
-rw-r--r--spec/features/dashboard/issuables_counter_spec.rb5
-rw-r--r--spec/features/dashboard/issues_spec.rb48
-rw-r--r--spec/features/dashboard/project_member_activity_index_spec.rb2
-rw-r--r--spec/features/dashboard/shortcuts_spec.rb10
-rw-r--r--spec/features/dashboard_issues_spec.rb4
-rw-r--r--spec/features/groups/members/list_spec.rb15
-rw-r--r--spec/features/help_pages_spec.rb26
-rw-r--r--spec/features/issuables/issuable_list_spec.rb28
-rw-r--r--spec/features/issues/bulk_assignment_labels_spec.rb6
-rw-r--r--spec/features/issues/filtered_search/dropdown_label_spec.rb8
-rw-r--r--spec/features/issues/filtered_search/filter_issues_spec.rb14
-rw-r--r--spec/features/issues/gfm_autocomplete_spec.rb14
-rw-r--r--spec/features/issues/issue_sidebar_spec.rb65
-rw-r--r--spec/features/issues_spec.rb41
-rw-r--r--spec/features/merge_requests/conflicts_spec.rb2
-rw-r--r--spec/features/merge_requests/filter_by_labels_spec.rb86
-rw-r--r--spec/features/merge_requests/filter_by_milestone_spec.rb35
-rw-r--r--spec/features/merge_requests/filter_merge_requests_spec.rb281
-rw-r--r--spec/features/merge_requests/reset_filters_spec.rb29
-rw-r--r--spec/features/milestone_spec.rb2
-rw-r--r--spec/features/profile_spec.rb4
-rw-r--r--spec/features/profiles/keys_spec.rb2
-rw-r--r--spec/features/profiles/preferences_spec.rb29
-rw-r--r--spec/features/projects/badges/list_spec.rb6
-rw-r--r--spec/features/projects/developer_views_empty_project_instructions_spec.rb12
-rw-r--r--spec/features/projects/files/project_owner_creates_license_file_spec.rb10
-rw-r--r--spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb4
-rw-r--r--spec/features/projects/import_export/export_file_spec.rb2
-rw-r--r--spec/features/projects/import_export/import_file_spec.rb2
-rw-r--r--spec/features/projects/import_export/namespace_export_file_spec.rb2
-rw-r--r--spec/features/projects/issuable_templates_spec.rb25
-rw-r--r--spec/features/projects/labels/issues_sorted_by_priority_spec.rb4
-rw-r--r--spec/features/projects/main/download_buttons_spec.rb7
-rw-r--r--spec/features/projects/members/sorting_spec.rb2
-rw-r--r--spec/features/projects/new_project_spec.rb45
-rw-r--r--spec/features/projects/pipelines/pipeline_spec.rb12
-rw-r--r--spec/features/projects/pipelines/pipelines_spec.rb138
-rw-r--r--spec/features/projects/ref_switcher_spec.rb2
-rw-r--r--spec/features/search_spec.rb4
-rw-r--r--spec/features/todos/todos_spec.rb100
-rw-r--r--spec/features/uploads/user_uploads_avatar_to_group_spec.rb26
-rw-r--r--spec/features/uploads/user_uploads_avatar_to_profile_spec.rb24
-rw-r--r--spec/features/uploads/user_uploads_file_to_note_spec.rb22
-rw-r--r--spec/features/user_callout_spec.rb37
-rw-r--r--spec/features/variables_spec.rb28
-rw-r--r--spec/finders/issues_finder_spec.rb6
-rw-r--r--spec/finders/merge_requests_finder_spec.rb8
-rw-r--r--spec/finders/notes_finder_spec.rb2
-rw-r--r--spec/finders/pipelines_finder_spec.rb4
-rw-r--r--spec/fixtures/api/schemas/user/login.json1
-rw-r--r--spec/fixtures/api/schemas/user/public.json16
-rw-r--r--spec/fixtures/config/mail_room_disabled.yml (renamed from spec/fixtures/mail_room_disabled.yml)0
-rw-r--r--spec/fixtures/config/mail_room_enabled.yml (renamed from spec/fixtures/mail_room_enabled.yml)0
-rw-r--r--spec/helpers/application_helper_spec.rb9
-rw-r--r--spec/helpers/auth_helper_spec.rb2
-rw-r--r--spec/helpers/emails_helper_spec.rb32
-rw-r--r--spec/helpers/issuables_helper_spec.rb2
-rw-r--r--spec/helpers/issues_helper_spec.rb6
-rw-r--r--spec/helpers/milestones_helper_spec.rb2
-rw-r--r--spec/helpers/page_layout_helper_spec.rb12
-rw-r--r--spec/helpers/preferences_helper_spec.rb26
-rw-r--r--spec/helpers/submodule_helper_spec.rb44
-rw-r--r--spec/helpers/version_check_helper_spec.rb34
-rw-r--r--spec/initializers/trusted_proxies_spec.rb4
-rw-r--r--spec/javascripts/.eslintrc3
-rw-r--r--spec/javascripts/ajax_loading_spinner_spec.js58
-rw-r--r--spec/javascripts/awards_handler_spec.js2
-rw-r--r--spec/javascripts/behaviors/autosize_spec.js2
-rw-r--r--spec/javascripts/behaviors/quick_submit_spec.js2
-rw-r--r--spec/javascripts/behaviors/requires_input_spec.js2
-rw-r--r--spec/javascripts/boards/board_card_spec.js168
-rw-r--r--spec/javascripts/boards/list_spec.js.es621
-rw-r--r--spec/javascripts/commit/pipelines/pipelines_store_spec.js.es64
-rw-r--r--spec/javascripts/dashboard_spec.js.es637
-rw-r--r--spec/javascripts/extensions/array_spec.js.es62
-rw-r--r--spec/javascripts/extensions/jquery_spec.js2
-rw-r--r--spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js.es623
-rw-r--r--spec/javascripts/fixtures/ajax_loading_spinner.html.haml2
-rw-r--r--spec/javascripts/fixtures/branches.rb2
-rw-r--r--spec/javascripts/fixtures/builds.rb2
-rw-r--r--spec/javascripts/fixtures/dashboard.html.haml45
-rw-r--r--spec/javascripts/fixtures/emoji_menu.js2
-rw-r--r--spec/javascripts/fixtures/issues.rb2
-rw-r--r--spec/javascripts/fixtures/merge_requests.rb2
-rw-r--r--spec/javascripts/fixtures/project_title.html.haml20
-rw-r--r--spec/javascripts/fixtures/projects.rb2
-rw-r--r--spec/javascripts/fixtures/todos.rb4
-rw-r--r--spec/javascripts/fixtures/user_callout.html.haml2
-rw-r--r--spec/javascripts/gfm_auto_complete_spec.js.es657
-rw-r--r--spec/javascripts/graphs/stat_graph_contributors_graph_spec.js8
-rw-r--r--spec/javascripts/graphs/stat_graph_contributors_util_spec.js3
-rw-r--r--spec/javascripts/graphs/stat_graph_spec.js20
-rw-r--r--spec/javascripts/header_spec.js6
-rw-r--r--spec/javascripts/helpers/class_spec_helper.js.es62
-rw-r--r--spec/javascripts/issue_spec.js2
-rw-r--r--spec/javascripts/lib/utils/text_utility_spec.js.es611
-rw-r--r--spec/javascripts/line_highlighter_spec.js2
-rw-r--r--spec/javascripts/merge_request_spec.js8
-rw-r--r--spec/javascripts/merge_request_tabs_spec.js2
-rw-r--r--spec/javascripts/merge_request_widget_spec.js2
-rw-r--r--spec/javascripts/new_branch_spec.js2
-rw-r--r--spec/javascripts/notes_spec.js14
-rw-r--r--spec/javascripts/project_dashboard_spec.js.es686
-rw-r--r--spec/javascripts/project_title_spec.js21
-rw-r--r--spec/javascripts/right_sidebar_spec.js2
-rw-r--r--spec/javascripts/search_autocomplete_spec.js6
-rw-r--r--spec/javascripts/shortcuts_issuable_spec.js2
-rw-r--r--spec/javascripts/syntax_highlight_spec.js2
-rw-r--r--spec/javascripts/u2f/authenticate_spec.js2
-rw-r--r--spec/javascripts/u2f/mock_u2f_device.js2
-rw-r--r--spec/javascripts/u2f/register_spec.js2
-rw-r--r--spec/javascripts/user_callout_spec.js.es637
-rw-r--r--spec/javascripts/version_check_image_spec.js.es633
-rw-r--r--spec/javascripts/vue_shared/components/table_pagination_spec.js.es610
-rw-r--r--spec/javascripts/zen_mode_spec.js2
-rw-r--r--spec/lib/banzai/filter/image_link_filter_spec.rb10
-rw-r--r--spec/lib/banzai/filter/issue_reference_filter_spec.rb24
-rw-r--r--spec/lib/bitbucket/collection_spec.rb2
-rw-r--r--spec/lib/bitbucket/representation/repo_spec.rb4
-rw-r--r--spec/lib/ci/gitlab_ci_yaml_processor_spec.rb34
-rw-r--r--spec/lib/constraints/project_url_constrainer_spec.rb4
-rw-r--r--spec/lib/expand_variables_spec.rb27
-rw-r--r--spec/lib/extracts_path_spec.rb4
-rw-r--r--spec/lib/gitlab/badge/shared/metadata.rb10
-rw-r--r--spec/lib/gitlab/bitbucket_import/importer_spec.rb8
-rw-r--r--spec/lib/gitlab/chat_commands/presenters/issue_show_spec.rb15
-rw-r--r--spec/lib/gitlab/ci/config/entry/commands_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/config/entry/factory_spec.rb10
-rw-r--r--spec/lib/gitlab/ci/config/entry/global_spec.rb10
-rw-r--r--spec/lib/gitlab/ci/config/entry/key_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/config/entry/paths_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/config/entry/script_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/config/entry/variables_spec.rb2
-rw-r--r--spec/lib/gitlab/conflict/file_spec.rb6
-rw-r--r--spec/lib/gitlab/data_builder/build_spec.rb26
-rw-r--r--spec/lib/gitlab/database/migration_helpers_spec.rb10
-rw-r--r--spec/lib/gitlab/database_spec.rb21
-rw-r--r--spec/lib/gitlab/diff/highlight_spec.rb6
-rw-r--r--spec/lib/gitlab/git/blob_snippet_spec.rb2
-rw-r--r--spec/lib/gitlab/git/blob_spec.rb185
-rw-r--r--spec/lib/gitlab/git/diff_collection_spec.rb2
-rw-r--r--spec/lib/gitlab/git/index_spec.rb220
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb199
-rw-r--r--spec/lib/gitlab/git_access_spec.rb26
-rw-r--r--spec/lib/gitlab/git_spec.rb6
-rw-r--r--spec/lib/gitlab/github_import/comment_formatter_spec.rb18
-rw-r--r--spec/lib/gitlab/github_import/importer_spec.rb5
-rw-r--r--spec/lib/gitlab/github_import/issue_formatter_spec.rb27
-rw-r--r--spec/lib/gitlab/github_import/pull_request_formatter_spec.rb27
-rw-r--r--spec/lib/gitlab/github_import/user_formatter_spec.rb39
-rw-r--r--spec/lib/gitlab/import_export/attribute_configuration_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/avatar_saver_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/file_importer_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/import_export_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/model_configuration_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/project.light.json48
-rw-r--r--spec/lib/gitlab/import_export/project_tree_restorer_spec.rb165
-rw-r--r--spec/lib/gitlab/import_export/project_tree_saver_spec.rb4
-rw-r--r--spec/lib/gitlab/import_export/repo_bundler_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/repo_restorer_spec.rb40
-rw-r--r--spec/lib/gitlab/import_export/wiki_repo_bundler_spec.rb2
-rw-r--r--spec/lib/gitlab/import_sources_spec.rb38
-rw-r--r--spec/lib/gitlab/kubernetes_spec.rb2
-rw-r--r--spec/lib/gitlab/ldap/auth_hash_spec.rb2
-rw-r--r--spec/lib/gitlab/ldap/user_spec.rb4
-rw-r--r--spec/lib/gitlab/metrics/instrumentation_spec.rb2
-rw-r--r--spec/lib/gitlab/metrics/method_call_spec.rb2
-rw-r--r--spec/lib/gitlab/metrics/metric_spec.rb2
-rw-r--r--spec/lib/gitlab/metrics/system_spec.rb6
-rw-r--r--spec/lib/gitlab/metrics/transaction_spec.rb4
-rw-r--r--spec/lib/gitlab/o_auth/user_spec.rb16
-rw-r--r--spec/lib/gitlab/optimistic_locking_spec.rb19
-rw-r--r--spec/lib/gitlab/regex_spec.rb4
-rw-r--r--spec/lib/gitlab/saml/user_spec.rb21
-rw-r--r--spec/lib/gitlab/serializer/ci/variables_spec.rb3
-rw-r--r--spec/lib/gitlab/slash_commands/extractor_spec.rb8
-rw-r--r--spec/lib/gitlab/template/issue_template_spec.rb3
-rw-r--r--spec/lib/gitlab/template/merge_request_template_spec.rb3
-rw-r--r--spec/lib/gitlab/themes_spec.rb48
-rw-r--r--spec/lib/gitlab/upgrader_spec.rb3
-rw-r--r--spec/lib/gitlab/utils_spec.rb4
-rw-r--r--spec/lib/mattermost/command_spec.rb6
-rw-r--r--spec/lib/mattermost/team_spec.rb25
-rw-r--r--spec/migrations/migrate_process_commit_worker_jobs_spec.rb4
-rw-r--r--spec/models/abuse_report_spec.rb2
-rw-r--r--spec/models/application_setting_spec.rb40
-rw-r--r--spec/models/ci/build_spec.rb62
-rw-r--r--spec/models/ci/pipeline_spec.rb71
-rw-r--r--spec/models/ci/runner_spec.rb29
-rw-r--r--spec/models/commit_status_spec.rb22
-rw-r--r--spec/models/concerns/cache_markdown_field_spec.rb5
-rw-r--r--spec/models/concerns/routable_spec.rb4
-rw-r--r--spec/models/concerns/spammable_spec.rb19
-rw-r--r--spec/models/concerns/uniquify_spec.rb33
-rw-r--r--spec/models/cycle_analytics/production_spec.rb5
-rw-r--r--spec/models/cycle_analytics/staging_spec.rb7
-rw-r--r--spec/models/environment_spec.rb7
-rw-r--r--spec/models/members/project_member_spec.rb8
-rw-r--r--spec/models/merge_request_diff_spec.rb2
-rw-r--r--spec/models/merge_request_spec.rb50
-rw-r--r--spec/models/namespace_spec.rb4
-rw-r--r--spec/models/project_group_link_spec.rb17
-rw-r--r--spec/models/project_services/bamboo_service_spec.rb2
-rw-r--r--spec/models/project_services/buildkite_service_spec.rb4
-rw-r--r--spec/models/project_services/chat_message/build_message_spec.rb28
-rw-r--r--spec/models/project_services/drone_ci_service_spec.rb10
-rw-r--r--spec/models/project_services/irker_service_spec.rb4
-rw-r--r--spec/models/project_services/kubernetes_service_spec.rb4
-rw-r--r--spec/models/project_services/mattermost_slash_commands_service_spec.rb3
-rw-r--r--spec/models/project_services/teamcity_service_spec.rb4
-rw-r--r--spec/models/project_spec.rb39
-rw-r--r--spec/models/repository_spec.rb101
-rw-r--r--spec/models/user_spec.rb67
-rw-r--r--spec/policies/user_policy_spec.rb37
-rw-r--r--spec/requests/api/access_requests_spec.rb5
-rw-r--r--spec/requests/api/award_emoji_spec.rb17
-rw-r--r--spec/requests/api/boards_spec.rb5
-rw-r--r--spec/requests/api/branches_spec.rb29
-rw-r--r--spec/requests/api/broadcast_messages_spec.rb8
-rw-r--r--spec/requests/api/builds_spec.rb16
-rw-r--r--spec/requests/api/commit_statuses_spec.rb16
-rw-r--r--spec/requests/api/commits_spec.rb25
-rw-r--r--spec/requests/api/deploy_keys_spec.rb25
-rw-r--r--spec/requests/api/deployments_spec.rb5
-rw-r--r--spec/requests/api/environments_spec.rb7
-rw-r--r--spec/requests/api/files_spec.rb23
-rw-r--r--spec/requests/api/groups_spec.rb43
-rw-r--r--spec/requests/api/issues_spec.rb204
-rw-r--r--spec/requests/api/labels_spec.rb40
-rw-r--r--spec/requests/api/members_spec.rb29
-rw-r--r--spec/requests/api/merge_request_diffs_spec.rb2
-rw-r--r--spec/requests/api/merge_requests_spec.rb54
-rw-r--r--spec/requests/api/milestones_spec.rb75
-rw-r--r--spec/requests/api/namespaces_spec.rb12
-rw-r--r--spec/requests/api/notes_spec.rb15
-rw-r--r--spec/requests/api/pipelines_spec.rb5
-rw-r--r--spec/requests/api/project_hooks_spec.rb9
-rw-r--r--spec/requests/api/project_snippets_spec.rb103
-rw-r--r--spec/requests/api/projects_spec.rb128
-rw-r--r--spec/requests/api/repositories_spec.rb7
-rw-r--r--spec/requests/api/runner_spec.rb151
-rw-r--r--spec/requests/api/runners_spec.rb37
-rw-r--r--spec/requests/api/services_spec.rb2
-rw-r--r--spec/requests/api/settings_spec.rb11
-rw-r--r--spec/requests/api/snippets_spec.rb75
-rw-r--r--spec/requests/api/system_hooks_spec.rb3
-rw-r--r--spec/requests/api/tags_spec.rb14
-rw-r--r--spec/requests/api/templates_spec.rb12
-rw-r--r--spec/requests/api/todos_spec.rb33
-rw-r--r--spec/requests/api/triggers_spec.rb4
-rw-r--r--spec/requests/api/users_spec.rb78
-rw-r--r--spec/requests/api/v3/award_emoji_spec.rb74
-rw-r--r--spec/requests/api/v3/boards_spec.rb113
-rw-r--r--spec/requests/api/v3/branches_spec.rb83
-rw-r--r--spec/requests/api/v3/broadcast_messages_spec.rb34
-rw-r--r--spec/requests/api/v3/commits_spec.rb578
-rw-r--r--spec/requests/api/v3/environments_spec.rb39
-rw-r--r--spec/requests/api/v3/files_spec.rb271
-rw-r--r--spec/requests/api/v3/groups_spec.rb35
-rw-r--r--spec/requests/api/v3/issues_spec.rb29
-rw-r--r--spec/requests/api/v3/labels_spec.rb171
-rw-r--r--spec/requests/api/v3/members_spec.rb2
-rw-r--r--spec/requests/api/v3/merge_requests_spec.rb2
-rw-r--r--spec/requests/api/v3/notes_spec.rb433
-rw-r--r--spec/requests/api/v3/project_snippets_spec.rb92
-rw-r--r--spec/requests/api/v3/projects_spec.rb35
-rw-r--r--spec/requests/api/v3/repositories_spec.rb144
-rw-r--r--spec/requests/api/v3/runners_spec.rb154
-rw-r--r--spec/requests/api/v3/services_spec.rb22
-rw-r--r--spec/requests/api/v3/system_hooks_spec.rb57
-rw-r--r--spec/requests/api/v3/tags_spec.rb89
-rw-r--r--spec/requests/api/v3/templates_spec.rb8
-rw-r--r--spec/requests/api/v3/todos_spec.rb73
-rw-r--r--spec/requests/api/v3/triggers_spec.rb47
-rw-r--r--spec/requests/api/v3/users_spec.rb266
-rw-r--r--spec/requests/api/variables_spec.rb3
-rw-r--r--spec/requests/ci/api/builds_spec.rb35
-rw-r--r--spec/requests/ci/api/runners_spec.rb2
-rw-r--r--spec/requests/ci/api/triggers_spec.rb3
-rw-r--r--spec/requests/lfs_http_spec.rb178
-rw-r--r--spec/services/auth/container_registry_authentication_service_spec.rb2
-rw-r--r--spec/services/ci/create_pipeline_service_spec.rb71
-rw-r--r--spec/services/ci/process_pipeline_service_spec.rb18
-rw-r--r--spec/services/ci/register_build_service_spec.rb45
-rw-r--r--spec/services/ci/retry_build_service_spec.rb117
-rw-r--r--spec/services/ci/retry_pipeline_service_spec.rb196
-rw-r--r--spec/services/ci/update_runner_service_spec.rb41
-rw-r--r--spec/services/create_deployment_service_spec.rb17
-rw-r--r--spec/services/groups/destroy_service_spec.rb21
-rw-r--r--spec/services/issues/create_service_spec.rb10
-rw-r--r--spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb25
-rw-r--r--spec/services/merge_requests/build_service_spec.rb13
-rw-r--r--spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb25
-rw-r--r--spec/services/merge_requests/resolve_service_spec.rb13
-rw-r--r--spec/services/projects/create_service_spec.rb4
-rw-r--r--spec/services/projects/destroy_service_spec.rb19
-rw-r--r--spec/services/protected_branches/create_service_spec.rb4
-rw-r--r--spec/services/spam_service_spec.rb71
-rw-r--r--spec/services/system_note_service_spec.rb2
-rw-r--r--spec/services/todo_service_spec.rb36
-rw-r--r--spec/services/users/destroy_spec.rb79
-rw-r--r--spec/services/users/refresh_authorized_projects_service_spec.rb74
-rw-r--r--spec/support/api/pagination_shared_examples.rb20
-rw-r--r--spec/support/cycle_analytics_helpers.rb14
-rw-r--r--spec/support/db_cleaner.rb4
-rw-r--r--spec/support/dropzone_helper.rb37
-rw-r--r--spec/support/filtered_search_helpers.rb37
-rw-r--r--spec/support/gitlab_stubs/session.json4
-rw-r--r--spec/support/gitlab_stubs/user.json4
-rw-r--r--spec/support/issuables_list_metadata_shared_examples.rb13
-rw-r--r--spec/support/javascript_fixtures_helpers.rb2
-rw-r--r--spec/support/jira_service_helper.rb2
-rw-r--r--spec/support/kubernetes_helpers.rb13
-rw-r--r--spec/support/login_helpers.rb11
-rw-r--r--spec/support/markdown_feature.rb4
-rw-r--r--spec/support/matchers/access_matchers.rb2
-rw-r--r--spec/support/matchers/pagination_matcher.rb5
-rw-r--r--spec/support/merge_request_helpers.rb9
-rw-r--r--spec/support/repo_helpers.rb16
-rw-r--r--spec/support/seed_helper.rb2
-rw-r--r--spec/support/seed_repo.rb76
-rw-r--r--spec/support/select2_helper.rb2
-rw-r--r--spec/support/services/issuable_create_service_slash_commands_shared_examples.rb2
-rw-r--r--spec/support/stub_gitlab_calls.rb2
-rw-r--r--spec/support/test_env.rb8
-rw-r--r--spec/support/update_invalid_issuable.rb57
-rw-r--r--spec/uploaders/attachment_uploader_spec.rb7
-rw-r--r--spec/uploaders/avatar_uploader_spec.rb7
-rw-r--r--spec/uploaders/file_uploader_spec.rb46
-rw-r--r--spec/uploaders/uploader_helper_spec.rb35
-rw-r--r--spec/views/projects/builds/show.html.haml_spec.rb42
-rw-r--r--spec/workers/git_garbage_collect_worker_spec.rb4
-rw-r--r--spec/workers/repository_fork_worker_spec.rb20
-rw-r--r--spec/workers/repository_import_worker_spec.rb2
-rw-r--r--yarn.lock82
1491 files changed, 20211 insertions, 10379 deletions
diff --git a/.gitignore b/.gitignore
index 0b602d613c7..680651986e8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -51,3 +51,4 @@ eslint-report.html
/builds/*
/shared/*
/.gitlab_workhorse_secret
+/webpack-report/
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index b8619d7d3f8..e7a279c828b 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -240,7 +240,26 @@ rake db:seed_fu:
paths:
- log/development.log
-karma:
+rake gitlab:assets:compile:
+ stage: test
+ <<: *dedicated-runner
+ dependencies: []
+ variables:
+ NODE_ENV: "production"
+ RAILS_ENV: "production"
+ SETUP_DB: "false"
+ USE_DB: "false"
+ SKIP_STORAGE_VALIDATION: "true"
+ WEBPACK_REPORT: "true"
+ script:
+ - bundle exec rake yarn:install gitlab:assets:compile
+ artifacts:
+ name: webpack-report
+ expire_in: 31d
+ paths:
+ - webpack-report/
+
+rake karma:
cache:
paths:
- vendor/ruby
@@ -281,7 +300,7 @@ bundler:audit:
- master@gitlab/gitlabhq
- master@gitlab/gitlab-ee
script:
- - "bundle exec bundle-audit check --update --ignore OSVDB-115941"
+ - "bundle exec bundle-audit check --update --ignore OSVDB-115941 CVE-2016-6316 CVE-2016-6317"
migration paths:
stage: test
@@ -387,7 +406,8 @@ pages:
<<: *dedicated-runner
dependencies:
- coverage
- - karma
+ - rake karma
+ - rake gitlab:assets:compile
- lint:javascript:report
script:
- mv public/ .public/
@@ -395,6 +415,7 @@ pages:
- mv coverage/ public/coverage-ruby/ || true
- mv coverage-javascript/ public/coverage-javascript/ || true
- mv eslint-report.html public/ || true
+ - mv webpack-report/ public/webpack-report/ || true
artifacts:
paths:
- public
diff --git a/.gitlab/issue_templates/Research Proposal.md b/.gitlab/issue_templates/Research Proposal.md
new file mode 100644
index 00000000000..5676656793d
--- /dev/null
+++ b/.gitlab/issue_templates/Research Proposal.md
@@ -0,0 +1,17 @@
+### Background:
+
+(Include problem, use cases, benefits, and/or goals)
+
+**What questions are you trying to answer?**
+
+**Are you looking to verify an existing hypothesis or uncover new issues you should be exploring?**
+
+**What is the backstory of this project and how does it impact the approach?**
+
+**What do you already know about the areas you are exploring?**
+
+**What does success look like at the end of the project?**
+
+### Links / references:
+
+/label ~"UX research"
diff --git a/.rubocop.yml b/.rubocop.yml
index b093d4d25d4..fa1370ea1f3 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -22,14 +22,12 @@ AllCops:
- 'db/fixtures/**/*'
- 'tmp/**/*'
- 'bin/**/*'
- - 'lib/backup/**/*'
- - 'lib/ci/backup/**/*'
- - 'lib/tasks/**/*'
- - 'lib/ci/migrate/**/*'
- - 'lib/email_validator.rb'
- - 'lib/gitlab/upgrader.rb'
- - 'lib/gitlab/seeder.rb'
- 'generator_templates/**/*'
+ - 'builds/**/*'
+
+# Gems in consecutive lines should be alphabetically sorted
+Bundler/OrderedGems:
+ Enabled: false
# Style #######################################################################
@@ -54,6 +52,16 @@ Style/AlignArray:
Style/AlignHash:
Enabled: true
+# Here we check if the parameters on a multi-line method call or
+# definition are aligned.
+Style/AlignParameters:
+ Enabled: false
+
+# Whether `and` and `or` are banned only in conditionals (conditionals)
+# or completely (always).
+Style/AndOr:
+ Enabled: true
+
# Use `Array#join` instead of `Array#*`.
Style/ArrayJoin:
Enabled: true
@@ -78,15 +86,24 @@ Style/BeginBlock:
Style/BlockComments:
Enabled: true
-# Put end statement of multiline block on its own line.
-Style/BlockEndNewline:
- Enabled: true
-
# Avoid using {...} for multi-line blocks (multiline chaining is # always
# ugly). Prefer {...} over do...end for single-line blocks.
Style/BlockDelimiters:
Enabled: true
+# Put end statement of multiline block on its own line.
+Style/BlockEndNewline:
+ Enabled: true
+
+ # This cop checks for braces around the last parameter in a method call
+# if the last parameter is a hash.
+Style/BracesAroundHashParameters:
+ Enabled: false
+
+# This cop checks for uses of the case equality operator(===).
+Style/CaseEquality:
+ Enabled: false
+
# Indentation of when in a case/when/[else/]end.
Style/CaseIndentation:
Enabled: true
@@ -105,7 +122,7 @@ Style/ClassAndModuleChildren:
# Enforces consistent use of `Object#is_a?` or `Object#kind_of?`.
Style/ClassCheck:
- Enabled: false
+ Enabled: true
# Use self when defining module/class methods.
Style/ClassMethods:
@@ -115,10 +132,26 @@ Style/ClassMethods:
Style/ClassVars:
Enabled: true
+# This cop checks for methods invoked via the :: operator instead
+# of the . operator (like FileUtils::rmdir instead of FileUtils.rmdir).
+Style/ColonMethodCall:
+ Enabled: true
+
+# This cop checks that comment annotation keywords are written according
+# to guidelines.
+Style/CommentAnnotation:
+ Enabled: false
+
# Indentation of comments.
Style/CommentIndentation:
Enabled: true
+# Check for `if` and `case` statements where each branch is used for
+# assignment to the same variable when using the return of the
+# condition can be used instead.
+Style/ConditionalAssignment:
+ Enabled: true
+
# Constants should use SCREAMING_SNAKE_CASE.
Style/ConstantName:
Enabled: true
@@ -131,13 +164,19 @@ Style/DefWithParentheses:
Style/Documentation:
Enabled: false
+# This cop checks for uses of double negation (!!) to convert something
+# to a boolean value. As this is both cryptic and usually redundant, it
+# should be avoided.
+Style/DoubleNegation:
+ Enabled: false
+
# Align elses and elsifs correctly.
Style/ElseAlignment:
Enabled: true
# Use empty lines between defs.
Style/EmptyLineBetweenDefs:
- Enabled: false
+ Enabled: true
# Don't use several empty lines in a row.
Style/EmptyLines:
@@ -155,14 +194,14 @@ Style/EmptyLinesAroundBlockBody:
Style/EmptyLinesAroundClassBody:
Enabled: true
-# Keeps track of empty lines around module bodies.
-Style/EmptyLinesAroundModuleBody:
- Enabled: true
-
# Keeps track of empty lines around method bodies.
Style/EmptyLinesAroundMethodBody:
Enabled: true
+# Keeps track of empty lines around module bodies.
+Style/EmptyLinesAroundModuleBody:
+ Enabled: true
+
# Avoid the use of END blocks.
Style/EndBlock:
Enabled: true
@@ -195,24 +234,28 @@ Style/For:
# Checks if there is a magic comment to enforce string literals
Style/FrozenStringLiteralComment:
Enabled: false
+
# Do not introduce global variables.
Style/GlobalVars:
Enabled: true
+ Exclude:
+ - 'lib/backup/**/*'
+ - 'lib/tasks/**/*'
# Prefer Ruby 1.9 hash syntax `{ a: 1, b: 2 }`
# over 1.8 syntax `{ :a => 1, :b => 2 }`.
Style/HashSyntax:
Enabled: true
-# Do not use if x; .... Use the ternary operator instead.
-Style/IfWithSemicolon:
- Enabled: true
-
# Checks that conditional statements do not have an identical line at the
# end of each branch, which can validly be moved out of the conditional.
Style/IdenticalConditionalBranches:
Enabled: true
+# Do not use if x; .... Use the ternary operator instead.
+Style/IfWithSemicolon:
+ Enabled: true
+
# Checks the indentation of the first line of the right-hand-side of a
# multi-line assignment.
Style/IndentAssignment:
@@ -253,7 +296,7 @@ Style/ModuleFunction:
# Checks that the closing brace in an array literal is either on the same line
# as the last array element, or a new line.
Style/MultilineArrayBraceLayout:
- Enabled: false
+ Enabled: true
EnforcedStyle: symmetrical
# Avoid multi-line chains of blocks.
@@ -267,7 +310,7 @@ Style/MultilineBlockLayout:
# Checks that the closing brace in a hash literal is either on the same line as
# the last hash element, or a new line.
Style/MultilineHashBraceLayout:
- Enabled: false
+ Enabled: true
EnforcedStyle: symmetrical
# Do not use then for multi-line if/unless.
@@ -299,6 +342,14 @@ Style/MultilineOperationIndentation:
Style/MultilineTernaryOperator:
Enabled: true
+# This cop checks whether some constant value isn't a
+# mutable literal (e.g. array or hash).
+Style/MutableConstant:
+ Enabled: true
+ Exclude:
+ - 'db/migrate/**/*'
+ - 'db/post_migrate/**/*'
+
# Favor unless over if for negative conditions (or control flow or).
Style/NegatedIf:
Enabled: true
@@ -401,6 +452,10 @@ Style/SpaceBeforeComment:
Style/SpaceBeforeSemicolon:
Enabled: true
+# Checks for spaces inside square brackets.
+Style/SpaceInsideBrackets:
+ Enabled: true
+
# Use spaces inside hash literal braces - or don't.
Style/SpaceInsideHashLiteralBraces:
Enabled: true
@@ -437,6 +492,10 @@ Style/Tab:
Style/TrailingBlankLines:
Enabled: true
+# This cop checks for trailing comma in array and hash literals.
+Style/TrailingCommaInLiteral:
+ Enabled: false
+
# Checks for %W when interpolation is not needed.
Style/UnneededCapitalW:
Enabled: true
@@ -472,7 +531,7 @@ Style/WhileUntilModifier:
# Use %w or %W for arrays of words.
Style/WordArray:
- Enabled: false
+ Enabled: true
# Metrics #####################################################################
@@ -482,6 +541,10 @@ Metrics/AbcSize:
Enabled: true
Max: 60
+# This cop checks if the length of a block exceeds some maximum value.
+Metrics/BlockLength:
+ Enabled: false
+
# Avoid excessive block nesting.
Metrics/BlockNesting:
Enabled: true
@@ -521,20 +584,21 @@ Metrics/PerceivedComplexity:
# Lint ########################################################################
-# Checks for useless access modifiers.
-Lint/UselessAccessModifier:
- Enabled: true
-
-# Checks for attempts to use `private` or `protected` to set the visibility
-# of a class method, which does not work.
-Lint/IneffectiveAccessModifier:
- Enabled: false
-
# Checks for ambiguous operators in the first argument of a method invocation
# without parentheses.
Lint/AmbiguousOperator:
Enabled: true
+# This cop checks for ambiguous regexp literals in the first argument of
+# a method invocation without parentheses.
+Lint/AmbiguousRegexpLiteral:
+ Enabled: false
+
+# This cop checks for assignments in the conditions of
+# if/while/until.
+Lint/AssignmentInCondition:
+ Enabled: false
+
# Align block ends correctly.
Lint/BlockAlignment:
Enabled: true
@@ -588,10 +652,6 @@ Lint/EndInMethod:
Lint/EnsureReturn:
Enabled: true
-# The use of eval represents a serious security risk.
-Lint/Eval:
- Enabled: true
-
# Catches floating-point literals too large or small for Ruby to represent.
Lint/FloatOutOfRange:
Enabled: true
@@ -600,11 +660,20 @@ Lint/FloatOutOfRange:
Lint/FormatParameterMismatch:
Enabled: true
+# This cop checks for *rescue* blocks with no body.
+Lint/HandleExceptions:
+ Enabled: false
+
# Checks for adjacent string literals on the same line, which could better be
# represented as a single string literal.
Lint/ImplicitStringConcatenation:
Enabled: true
+# Checks for attempts to use `private` or `protected` to set the visibility
+# of a class method, which does not work.
+Lint/IneffectiveAccessModifier:
+ Enabled: false
+
# Checks for invalid character literals with a non-escaped whitespace
# character.
Lint/InvalidCharacterLiteral:
@@ -618,6 +687,10 @@ Lint/LiteralInCondition:
Lint/LiteralInInterpolation:
Enabled: true
+# This cop checks for uses of *begin...end while/until something*.
+Lint/Loop:
+ Enabled: false
+
# Do not use nested method definitions.
Lint/NestedMethodDefinition:
Enabled: true
@@ -647,6 +720,11 @@ Lint/RescueException:
Lint/ShadowedException:
Enabled: false
+# This cop looks for use of the same name as outer local variables
+# for block arguments or block local variables.
+Lint/ShadowingOuterLocalVariable:
+ Enabled: false
+
# Checks for Object#to_s usage in string interpolation.
Lint/StringConversionInInterpolation:
Enabled: true
@@ -655,16 +733,36 @@ Lint/StringConversionInInterpolation:
Lint/UnderscorePrefixedVariableName:
Enabled: true
+# This cop checks for using Fixnum or Bignum constant
+Lint/UnifiedInteger:
+ Enabled: true
+
# Checks for rubocop:disable comments that can be removed.
# Note: this cop is not disabled when disabling all cops.
# It must be explicitly disabled.
Lint/UnneededDisable:
Enabled: false
+# This cop checks for unneeded usages of splat expansion
+Lint/UnneededSplatExpansion:
+ Enabled: false
+
# Unreachable code.
Lint/UnreachableCode:
Enabled: true
+# This cop checks for unused block arguments.
+Lint/UnusedBlockArgument:
+ Enabled: false
+
+# This cop checks for unused method arguments.
+Lint/UnusedMethodArgument:
+ Enabled: false
+
+# Checks for useless access modifiers.
+Lint/UselessAccessModifier:
+ Enabled: true
+
# Checks for useless assignment to a local variable.
Lint/UselessAssignment:
Enabled: true
@@ -704,6 +802,22 @@ Performance/LstripRstrip:
Performance/RangeInclude:
Enabled: true
+# This cop identifies the use of a `&block` parameter and `block.call`
+# where `yield` would do just as well.
+Performance/RedundantBlockCall:
+ Enabled: true
+
+# This cop identifies use of `Regexp#match` or `String#match in a context
+# where the integral return value of `=~` would do just as well.
+Performance/RedundantMatch:
+ Enabled: true
+
+# This cop identifies places where `Hash#merge!` can be replaced by
+# `Hash#[]=`.
+Performance/RedundantMerge:
+ Enabled: true
+ MaxKeyValuePairs: 1
+
# Use `sort` instead of `sort_by { |x| x }`.
Performance/RedundantSortBy:
Enabled: true
@@ -723,6 +837,17 @@ Performance/StringReplacement:
Performance/TimesMap:
Enabled: true
+# Security ####################################################################
+
+# This cop checks for the use of JSON class methods which have potential
+# security issues.
+Security/JSONLoad:
+ Enabled: true
+
+# This cop checks for the use of *Kernel#eval*.
+Security/Eval:
+ Enabled: true
+
# Rails #######################################################################
# Enables Rails cops.
@@ -741,8 +866,19 @@ Rails/Date:
# Prefer delegate method for delegations.
Rails/Delegate:
+ Enabled: true
+
+# This cop checks dynamic `find_by_*` methods.
+Rails/DynamicFindBy:
Enabled: false
+# This cop enforces that 'exit' calls are not used within a rails app.
+Rails/Exit:
+ Enabled: true
+ Exclude:
+ - lib/gitlab/upgrader.rb
+ - 'lib/backup/**/*'
+
# Prefer `find_by` over `where.first`.
Rails/FindBy:
Enabled: true
@@ -755,9 +891,25 @@ Rails/FindEach:
Rails/HasAndBelongsToMany:
Enabled: true
+# This cop is used to identify usages of http methods like `get`, `post`,
+# `put`, `patch` without the usage of keyword arguments in your tests and
+# change them to use keyword args.
+Rails/HttpPositionalArguments:
+ Enabled: false
+
# Checks for calls to puts, print, etc.
Rails/Output:
Enabled: true
+ Exclude:
+ - lib/gitlab/seeder.rb
+ - lib/gitlab/upgrader.rb
+ - 'lib/backup/**/*'
+ - 'lib/tasks/**/*'
+
+# This cop checks for the use of output safety calls like html_safe and
+# raw.
+Rails/OutputSafety:
+ Enabled: false
# Checks for incorrect grammar when using methods like `3.day.ago`.
Rails/PluralizationGrammar:
@@ -771,6 +923,14 @@ Rails/ReadWriteAttribute:
Rails/ScopeArgs:
Enabled: true
+# This cop checks for the use of Time methods without zone.
+Rails/TimeZone:
+ Enabled: false
+
+# This cop checks for the use of old-style attribute validation macros.
+Rails/Validation:
+ Enabled: true
+
# RSpec #######################################################################
# Check that instances are not being stubbed globally.
@@ -779,7 +939,7 @@ RSpec/AnyInstance:
# Check for expectations where `be(...)` can replace `eql(...)`.
RSpec/BeEql:
- Enabled: false
+ Enabled: true
# Check that the first argument to the top level describe is the tested class or
# module.
@@ -828,21 +988,51 @@ RSpec/Focus:
RSpec/InstanceVariable:
Enabled: false
+# Checks for `subject` definitions that come after `let` definitions.
+RSpec/LeadingSubject:
+ Enabled: false
+
+# Checks unreferenced `let!` calls being used for test setup.
+RSpec/LetSetup:
+ Enabled: false
+
+# Check that chains of messages are not being stubbed.
+RSpec/MessageChain:
+ Enabled: false
+
+# Checks that message expectations are set using spies.
+RSpec/MessageSpies:
+ Enabled: false
+
# Checks for multiple top-level describes.
RSpec/MultipleDescribes:
Enabled: false
+# Checks if examples contain too many `expect` calls.
+RSpec/MultipleExpectations:
+ Enabled: false
+
+# Checks for explicitly referenced test subjects.
+RSpec/NamedSubject:
+ Enabled: false
+
+# Checks for nested example groups.
+RSpec/NestedGroups:
+ Enabled: false
+
# Enforces the usage of the same method on all negative message expectations.
RSpec/NotToNot:
EnforcedStyle: not_to
Enabled: true
-# Prefer using verifying doubles over normal doubles.
-RSpec/VerifiedDoubles:
+# Check for repeated description strings in example groups.
+RSpec/RepeatedDescription:
Enabled: false
-# Custom ######################################################################
+# Checks for stubbed test subjects.
+RSpec/SubjectStub:
+ Enabled: false
-# Disallow the `git` and `github` arguments in the Gemfile.
-GemFetcher:
- Enabled: true
+# Prefer using verifying doubles over normal doubles.
+RSpec/VerifiedDoubles:
+ Enabled: false
diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml
index 648b3fc49d2..c24142c0a11 100644
--- a/.rubocop_todo.yml
+++ b/.rubocop_todo.yml
@@ -1,79 +1,13 @@
# This configuration was generated by
# `rubocop --auto-gen-config --exclude-limit 0`
-# on 2017-01-11 09:38:25 +0000 using RuboCop version 0.46.0.
+# on 2017-02-22 13:02:35 -0600 using RuboCop version 0.47.1.
# 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: 27
-# Configuration parameters: Include.
-# Include: **/Gemfile, **/gems.rb
-Bundler/OrderedGems:
- Enabled: false
-
-# Offense count: 175
-Lint/AmbiguousRegexpLiteral:
- Enabled: false
-
-# Offense count: 53
-# Configuration parameters: AllowSafeAssignment.
-Lint/AssignmentInCondition:
- Enabled: false
-
-# Offense count: 20
-Lint/HandleExceptions:
- Enabled: false
-
-# Offense count: 1
-Lint/Loop:
- Enabled: false
-
-# Offense count: 27
-Lint/ShadowingOuterLocalVariable:
- Enabled: false
-
-# Offense count: 10
-# Cop supports --auto-correct.
-Lint/UnifiedInteger:
- Enabled: false
-
-# Offense count: 21
-# Cop supports --auto-correct.
-Lint/UnneededSplatExpansion:
- Enabled: false
-
-# Offense count: 82
-# Cop supports --auto-correct.
-# Configuration parameters: IgnoreEmptyBlocks, AllowUnusedKeywordArguments.
-Lint/UnusedBlockArgument:
- Enabled: false
-
-# Offense count: 173
-# Cop supports --auto-correct.
-# Configuration parameters: AllowUnusedKeywordArguments, IgnoreEmptyMethods.
-Lint/UnusedMethodArgument:
- Enabled: false
-
-# Offense count: 93
-# Configuration parameters: CountComments.
-Metrics/BlockLength:
- Enabled: false
-
-# Offense count: 3
-# Cop supports --auto-correct.
-Performance/RedundantBlockCall:
- Enabled: false
-
-# Offense count: 5
-# Cop supports --auto-correct.
-Performance/RedundantMatch:
- Enabled: false
-
-# Offense count: 32
-# Cop supports --auto-correct.
-# Configuration parameters: MaxKeyValuePairs.
-Performance/RedundantMerge:
+# Offense count: 51
+RSpec/BeforeAfterAll:
Enabled: false
# Offense count: 15
@@ -81,7 +15,11 @@ Performance/RedundantMerge:
RSpec/EmptyExampleGroup:
Enabled: false
-# Offense count: 58
+# Offense count: 1
+RSpec/ExpectOutput:
+ Enabled: false
+
+# Offense count: 63
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: implicit, each, example
RSpec/HookArgument:
@@ -93,154 +31,59 @@ RSpec/HookArgument:
RSpec/ImplicitExpect:
Enabled: false
-# Offense count: 237
-RSpec/LeadingSubject:
- Enabled: false
-
-# Offense count: 253
-RSpec/LetSetup:
- Enabled: false
-
-# Offense count: 13
-RSpec/MessageChain:
- Enabled: false
-
-# Offense count: 479
-# Configuration parameters: EnforcedStyle, SupportedStyles.
-# SupportedStyles: have_received, receive
-RSpec/MessageSpies:
- Enabled: false
-
-# Offense count: 3036
-RSpec/MultipleExpectations:
- Enabled: false
-
-# Offense count: 2133
-RSpec/NamedSubject:
- Enabled: false
-
-# Offense count: 1974
-# Configuration parameters: MaxNesting.
-RSpec/NestedGroups:
+# Offense count: 36
+RSpec/RepeatedExample:
Enabled: false
-# Offense count: 32
-RSpec/RepeatedDescription:
+# Offense count: 34
+RSpec/ScatteredSetup:
Enabled: false
# Offense count: 1
RSpec/SingleArgumentMessageChain:
Enabled: false
-# Offense count: 133
-RSpec/SubjectStub:
- Enabled: false
-
-# 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:
+# Offense count: 163
+Rails/FilePath:
Enabled: false
-# Offense count: 182
-# Configuration parameters: EnforcedStyle, SupportedStyles.
-# SupportedStyles: strict, flexible
-Rails/TimeZone:
- Enabled: false
-
-# Offense count: 15
-# Cop supports --auto-correct.
+# Offense count: 2
# Configuration parameters: Include.
-# Include: app/models/**/*.rb
-Rails/Validation:
+# Include: db/migrate/*.rb
+Rails/ReversibleMigration:
Enabled: false
-# Offense count: 8
-# Cop supports --auto-correct.
-# Configuration parameters: AutoCorrect.
-Security/JSONLoad:
- Enabled: false
-
-# Offense count: 346
-# Cop supports --auto-correct.
-# Configuration parameters: EnforcedStyle, SupportedStyles, IndentationWidth.
-# SupportedStyles: with_first_parameter, with_fixed_indentation
-Style/AlignParameters:
+# Offense count: 278
+# Configuration parameters: Blacklist.
+# Blacklist: decrement!, decrement_counter, increment!, increment_counter, toggle!, touch, update_all, update_attribute, update_column, update_columns, update_counters
+Rails/SkipsModelValidations:
Enabled: false
-# Offense count: 27
+# Offense count: 7
# Cop supports --auto-correct.
-# Configuration parameters: EnforcedStyle, SupportedStyles.
-# SupportedStyles: always, conditionals
-Style/AndOr:
+Security/YAMLLoad:
Enabled: false
-# Offense count: 54
+# Offense count: 55
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: percent_q, bare_percent
Style/BarePercentLiterals:
Enabled: false
-# Offense count: 358
-# Cop supports --auto-correct.
-# Configuration parameters: EnforcedStyle, SupportedStyles.
-# SupportedStyles: braces, no_braces, context_dependent
-Style/BracesAroundHashParameters:
- Enabled: false
-
-# Offense count: 6
-Style/CaseEquality:
- Enabled: false
-
-# Offense count: 37
-# Cop supports --auto-correct.
-Style/ColonMethodCall:
- Enabled: false
-
-# Offense count: 4
-# Cop supports --auto-correct.
-# Configuration parameters: Keywords.
-# Keywords: TODO, FIXME, OPTIMIZE, HACK, REVIEW
-Style/CommentAnnotation:
- Enabled: false
-
-# 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: 1210
+# Offense count: 1304
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: leading, trailing
Style/DotPosition:
Enabled: false
-# Offense count: 18
-Style/DoubleNegation:
- Enabled: false
-
-# Offense count: 7
+# Offense count: 6
# Cop supports --auto-correct.
Style/EachWithObject:
Enabled: false
-# Offense count: 24
+# Offense count: 25
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: empty, nil, both
@@ -252,14 +95,14 @@ Style/EmptyElse:
Style/EmptyLiteral:
Enabled: false
-# Offense count: 57
+# Offense count: 56
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: compact, expanded
Style/EmptyMethod:
Enabled: false
-# Offense count: 147
+# Offense count: 184
# Cop supports --auto-correct.
# Configuration parameters: AllowForAlignment, ForceEqualSignAlignment.
Style/ExtraSpacing:
@@ -271,50 +114,50 @@ Style/ExtraSpacing:
Style/FormatString:
Enabled: false
-# Offense count: 238
+# Offense count: 268
# Configuration parameters: MinBodyLength.
Style/GuardClause:
Enabled: false
-# Offense count: 11
+# Offense count: 14
Style/IfInsideElse:
Enabled: false
-# Offense count: 173
+# Offense count: 179
# Cop supports --auto-correct.
# Configuration parameters: MaxLineLength.
Style/IfUnlessModifier:
Enabled: false
-# Offense count: 55
+# Offense count: 57
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles, IndentationWidth.
# SupportedStyles: special_inside_parentheses, consistent, align_brackets
Style/IndentArray:
Enabled: false
-# Offense count: 101
+# Offense count: 120
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles, IndentationWidth.
# SupportedStyles: special_inside_parentheses, consistent, align_braces
Style/IndentHash:
Enabled: false
-# Offense count: 41
+# Offense count: 45
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: line_count_dependent, lambda, literal
Style/Lambda:
Enabled: false
-# Offense count: 5
+# Offense count: 7
# Cop supports --auto-correct.
Style/LineEndConcatenation:
Enabled: false
-# Offense count: 19
+# Offense count: 22
# Cop supports --auto-correct.
-Style/MethodCallParentheses:
+Style/MethodCallWithoutArgsParentheses:
Enabled: false
# Offense count: 9
@@ -326,61 +169,49 @@ Style/MethodMissing:
Style/MultilineIfModifier:
Enabled: false
-# Offense count: 179
-# Cop supports --auto-correct.
-Style/MutableConstant:
- Enabled: false
-
-# Offense count: 8
+# Offense count: 22
# Cop supports --auto-correct.
Style/NestedParenthesizedCalls:
Enabled: false
-# Offense count: 13
+# Offense count: 17
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, MinBodyLength, SupportedStyles.
# SupportedStyles: skip_modifier_ifs, always
Style/Next:
Enabled: false
-# Offense count: 19
+# Offense count: 31
# Cop supports --auto-correct.
# Configuration parameters: EnforcedOctalStyle, SupportedOctalStyles.
# SupportedOctalStyles: zero_with_o, zero_only
Style/NumericLiteralPrefix:
Enabled: false
-# Offense count: 19
+# Offense count: 77
# Cop supports --auto-correct.
# Configuration parameters: AutoCorrect, EnforcedStyle, SupportedStyles.
# SupportedStyles: predicate, comparison
Style/NumericPredicate:
Enabled: false
-# Offense count: 34
+# Offense count: 36
# Cop supports --auto-correct.
Style/ParallelAssignment:
Enabled: false
-# Offense count: 417
+# Offense count: 477
# Cop supports --auto-correct.
# Configuration parameters: PreferredDelimiters.
Style/PercentLiteralDelimiters:
Enabled: false
-# Offense count: 10
-# Cop supports --auto-correct.
-# Configuration parameters: EnforcedStyle, SupportedStyles.
-# SupportedStyles: lower_case_q, upper_case_q
-Style/PercentQLiterals:
- Enabled: false
-
-# Offense count: 13
+# Offense count: 14
# Cop supports --auto-correct.
Style/PerlBackrefs:
Enabled: false
-# Offense count: 64
+# Offense count: 72
# Configuration parameters: NamePrefix, NamePrefixBlacklist, NameWhitelist.
# NamePrefix: is_, has_, have_
# NamePrefixBlacklist: is_, has_, have_
@@ -388,7 +219,7 @@ Style/PerlBackrefs:
Style/PredicateName:
Enabled: false
-# Offense count: 33
+# Offense count: 39
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: short, verbose
@@ -400,7 +231,7 @@ Style/PreferredHashMethods:
Style/Proc:
Enabled: false
-# Offense count: 50
+# Offense count: 62
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: compact, exploded
@@ -412,30 +243,30 @@ Style/RaiseArgs:
Style/RedundantBegin:
Enabled: false
-# Offense count: 29
+# Offense count: 32
# Cop supports --auto-correct.
Style/RedundantFreeze:
Enabled: false
-# Offense count: 11
+# Offense count: 15
# Cop supports --auto-correct.
# Configuration parameters: AllowMultipleReturnValues.
Style/RedundantReturn:
Enabled: false
-# Offense count: 359
+# Offense count: 365
# Cop supports --auto-correct.
Style/RedundantSelf:
Enabled: false
-# Offense count: 105
+# Offense count: 108
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles, AllowInnerSlashes.
# SupportedStyles: slashes, percent_r, mixed
Style/RegexpLiteral:
Enabled: false
-# Offense count: 19
+# Offense count: 22
# Cop supports --auto-correct.
Style/RescueModifier:
Enabled: false
@@ -445,19 +276,13 @@ Style/RescueModifier:
Style/SelfAssignment:
Enabled: false
-# Offense count: 2
-# Configuration parameters: Methods.
-# Methods: {"reduce"=>["acc", "elem"]}, {"inject"=>["acc", "elem"]}
-Style/SingleLineBlockParams:
- Enabled: false
-
# Offense count: 50
# Cop supports --auto-correct.
# Configuration parameters: AllowIfMethodIsEmpty.
Style/SingleLineMethods:
Enabled: false
-# Offense count: 138
+# Offense count: 155
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: space, no_space
@@ -470,26 +295,22 @@ Style/SpaceBeforeBlockBraces:
Style/SpaceBeforeFirstArg:
Enabled: false
-# Offense count: 37
+# Offense count: 38
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: require_no_space, require_space
Style/SpaceInLambdaLiteral:
Enabled: false
-# Offense count: 174
+# Offense count: 203
# Cop supports --auto-correct.
-# Configuration parameters: EnforcedStyle, SupportedStyles, EnforcedStyleForEmptyBraces, SpaceBeforeBlockParameters.
+# Configuration parameters: EnforcedStyle, SupportedStyles, EnforcedStyleForEmptyBraces, SupportedStylesForEmptyBraces, SpaceBeforeBlockParameters.
# SupportedStyles: space, no_space
+# SupportedStylesForEmptyBraces: space, no_space
Style/SpaceInsideBlockBraces:
Enabled: false
-# Offense count: 115
-# Cop supports --auto-correct.
-Style/SpaceInsideBrackets:
- Enabled: false
-
-# Offense count: 77
+# Offense count: 91
# Cop supports --auto-correct.
Style/SpaceInsideParens:
Enabled: false
@@ -499,21 +320,21 @@ Style/SpaceInsideParens:
Style/SpaceInsidePercentLiteralDelimiters:
Enabled: false
-# Offense count: 53
+# Offense count: 55
# Cop supports --auto-correct.
# Configuration parameters: SupportedStyles.
# SupportedStyles: use_perl_names, use_english_names
Style/SpecialGlobalVars:
EnforcedStyle: use_perl_names
-# Offense count: 25
+# Offense count: 40
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: single_quotes, double_quotes
Style/StringLiteralsInInterpolation:
Enabled: false
-# Offense count: 54
+# Offense count: 57
# Cop supports --auto-correct.
# Configuration parameters: IgnoredMethods.
# IgnoredMethods: respond_to, define_method
@@ -527,27 +348,20 @@ Style/SymbolProc:
Style/TernaryParentheses:
Enabled: false
-# Offense count: 36
+# Offense count: 43
# Cop supports --auto-correct.
-# Configuration parameters: EnforcedStyleForMultiline, SupportedStyles.
-# SupportedStyles: comma, consistent_comma, no_comma
+# Configuration parameters: EnforcedStyleForMultiline, SupportedStylesForMultiline.
+# SupportedStylesForMultiline: comma, consistent_comma, no_comma
Style/TrailingCommaInArguments:
Enabled: false
-# Offense count: 150
-# Cop supports --auto-correct.
-# Configuration parameters: EnforcedStyleForMultiline, SupportedStyles.
-# SupportedStyles: comma, consistent_comma, no_comma
-Style/TrailingCommaInLiteral:
- Enabled: false
-
-# Offense count: 7
+# Offense count: 13
# Cop supports --auto-correct.
# Configuration parameters: AllowNamedUnderscoreVariables.
Style/TrailingUnderscoreVariable:
Enabled: false
-# Offense count: 67
+# Offense count: 70
# Cop supports --auto-correct.
Style/TrailingWhitespace:
Enabled: false
@@ -559,12 +373,12 @@ Style/TrailingWhitespace:
Style/TrivialAccessors:
Enabled: false
-# Offense count: 2
+# Offense count: 6
# Cop supports --auto-correct.
Style/UnlessElse:
Enabled: false
-# Offense count: 17
+# Offense count: 22
# Cop supports --auto-correct.
Style/UnneededInterpolation:
Enabled: false
diff --git a/CHANGELOG.md b/CHANGELOG.md
index cbaac0f69d3..f279a57105c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,223 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
+## 8.17.1 (2017-02-28)
+
+- Replace setInterval with setTimeout to prevent highly frequent requests. !9271 (Takuya Noguchi)
+- Disable unused tags count cache for Projects, Builds and Runners.
+- Spam check and reCAPTCHA improvements.
+- Allow searching issues for strings containing colons.
+- Disabled tooltip on add issues button in usse boards.
+- Fixed commit search UI.
+- Fix MR changes tab size count when there are over 100 files in the diff.
+- Disable invalid service templates.
+- Use default branch as target_branch when parameter is missing.
+- Upgrade GitLab Pages to v0.3.2.
+- Add performance query regression fix for !9088 affecting #27267.
+- Chat slash commands show labels correctly.
+
+## 8.17.0 (2017-02-22)
+
+- API: Fix file downloading. !0 (8267)
+- Changed composer installer script in the CI PHP example doc. !4342 (Jeffrey Cafferata)
+- Display fullscreen button on small screens. !5302 (winniehell)
+- Add system hook for when a project is updated (other than rename/transfer). !5711 (Tommy Beadle)
+- Fix notifications when set at group level. !6813 (Alexandre Maia)
+- Project labels can now be promoted to group labels. !7242 (Olaf Tomalka)
+- use webpack to bundle frontend assets and use karma for frontend testing. !7288
+- Adds back ability to stop all environments. !7379
+- Added labels empty state. !7443
+- Add ability to define a coverage regex in the .gitlab-ci.yml. !7447 (Leandro Camargo)
+- Disable automatic login after clicking email confirmation links. !7472
+- Search feature: redirects to commit page if query is commit sha and only commit found. !8028 (YarNayar)
+- Create a TODO for user who set auto-merge when a build fails, merge conflict occurs. !8056 (twonegatives)
+- Don't group issues by project on group-level and dashboard issue indexes. !8111 (Bernardo Castro)
+- Mark MR as WIP when pushing WIP commits. !8124 (Jurre Stender @jurre)
+- Flag multiple empty lines in eslint, fix offenses. !8137
+- Add sorting pipeline for a commit. !8319 (Takuya Noguchi)
+- Adds service trigger events to api. !8324
+- Update pipeline and commit links when CI status is updated. !8351
+- Hide version check image if there is no internet connection. !8355 (Ken Ding)
+- Prevent removal of input fields if it is the parent dropdown element. !8397
+- Introduce maximum session time for terminal websocket connection. !8413
+- Allow creating protected branches when user can merge to such branch. !8458
+- Refactor MergeRequests::BuildService. !8462 (Rydkin Maxim)
+- Added GitLab Pages to CE. !8463
+- Support notes when a project is not specified (personal snippet notes). !8468
+- Use warning icon in mini-graph if stage passed conditionally. !8503
+- Don’t count tasks that are not defined as list items correctly. !8526
+- Reformat messages ChatOps. !8528
+- Copy commit SHA to clipboard. !8547
+- Improve button accessibility on pipelines page. !8561
+- Display project ID in project settings. !8572 (winniehell)
+- PlantUML support for Markdown. !8588 (Horacio Sanson)
+- Fix reply by email without sub-addressing for some clients from Microsoft and Apple. !8620
+- Fix nested tasks in ordered list. !8626
+- Fix Sort by Recent Sign-in in Admin Area. !8637 (Poornima M)
+- Avoid repeated dashes in $CI_ENVIRONMENT_SLUG. !8638
+- Only show Merge Request button when user can create a MR. !8639
+- Prevent copying of line numbers in parallel diff view. !8706
+- Improve build policy and access abilities. !8711
+- API: Remove /projects/:id/keys/.. endpoints. !8716 (Robert Schilling)
+- API: Remove deprecated 'expires_at' from project snippets. !8723 (Robert Schilling)
+- Add `copy` backup strategy to combat file changed errors. !8728
+- adds avatar for discussion note. !8734
+- Add link verification to badge partial in order to render a badge without a link. !8740
+- Reduce hits to LDAP on Git HTTP auth by reordering auth mechanisms. !8752
+- prevent diff unfolding link from appearing when there are no more lines to show. !8761
+- Redesign searchbar in admin project list. !8776
+- Rename Builds to Pipelines, CI/CD Pipelines, or Jobs everywhere. !8787
+- dismiss sidebar on repo buttons click. !8798 (Adam Pahlevi)
+- fixed small mini pipeline graph line glitch. !8804
+- Make all system notes lowercase. !8807
+- Support unauthenticated LFS object downloads for public projects. !8824 (Ben Boeckel)
+- Add read-only full_path and full_name attributes to Group API. !8827
+- allow relative url change without recompiling frontend assets. !8831
+- Use vue.js Pipelines table in commit and merge request view. !8844
+- Use reCaptcha when an issue is identified as a spam. !8846
+- resolve deprecation warnings. !8855 (Adam Pahlevi)
+- Cop for gem fetched from a git source. !8856 (Adam Pahlevi)
+- Remove flash warning from login page. !8864 (Gerald J. Padilla)
+- Adds documentation for how to use Vue.js. !8866
+- Add 'View on [env]' link to blobs and individual files in diffs. !8867
+- Replace word user with member. !8872
+- Change the reply shortcut to focus the field even without a selection. !8873 (Brian Hall)
+- Unify MR diff file button style. !8874
+- Unify projects search by removing /projects/:search endpoint. !8877
+- Fix disable storing of sensitive information when importing a new repo. !8885 (Bernard Pietraga)
+- Fix pipeline graph vertical spacing in Firefox and Safari. !8886
+- Fix filtered search user autocomplete for gitlab instances that are hosted on a subdirectory. !8891
+- Fix Ctrl+Click support for Todos and Merge Request page tabs. !8898
+- Fix wrong call to ProjectCacheWorker.perform. !8910
+- Don't perform Devise trackable updates on blocked User records. !8915
+- Add ability to export project inherited group members to Import/Export. !8923
+- replace `find_with_namespace` with `find_by_full_path`. !8949 (Adam Pahlevi)
+- Fixes flickering of avatar border in mention dropdown. !8950
+- Remove unnecessary queries for .atom and .json in Dashboard::ProjectsController#index. !8956
+- Fix deleting projects with pipelines and builds. !8960
+- Fix broken anchor links when special characters are used. !8961 (Andrey Krivko)
+- Ensure autogenerated title does not cause failing spec. !8963 (brian m. carlson)
+- Update doc for enabling or disabling GitLab CI. !8965 (Takuya Noguchi)
+- Remove deprecated MR and Issue endpoints and preserve V3 namespace. !8967
+- Fixed "substract" typo on /help/user/project/slash_commands. !8976 (Jason Aquino)
+- Preserve backward compatibility CI/CD and disallow setting `coverage` regexp in global context. !8981
+- use babel to transpile all non-vendor javascript assets regardless of file extension. !8988
+- Fix MR widget url. !8989
+- Fixes hover cursor on pipeline pagenation. !9003
+- Layer award emoji dropdown over the right sidebar. !9004
+- Do not display deploy keys in user's own ssh keys list. !9024
+- upgrade babel 5.8.x to babel 6.22.x. !9072
+- upgrade to webpack v2.2. !9078
+- Trigger autocomplete after selecting a slash command. !9117
+- Add space between text and loading icon in Megre Request Widget. !9119
+- Fix job to pipeline renaming. !9147
+- Replace static fixture for merge_request_tabs_spec.js. !9172 (winniehell)
+- Replace static fixture for right_sidebar_spec.js. !9211 (winniehell)
+- Show merge errors in merge request widget. !9229
+- Increase process_commit queue weight from 2 to 3. !9326 (blackst0ne)
+- Don't require lib/gitlab/request_profiler/middleware.rb in config/initializers/request_profiler.rb.
+- Force new password after password reset via API. (George Andrinopoulos)
+- Allows to search within project by commit hash. (YarNayar)
+- Show organisation membership and delete comment on smaller viewports, plus change comment author name to username.
+- Remove turbolinks.
+- Convert pipeline action icons to svg to have them propperly positioned.
+- Remove rogue scrollbars for issue comments with inline elements.
+- Align Segoe UI label text.
+- Color + and - signs in diffs to increase code legibility.
+- Fix tab index order on branch commits list page. (Ryan Harris)
+- Add hover style to copy icon on commit page header. (Ryan Harris)
+- Remove hover animation from row elements.
+- Improve pipeline status icon linking in widgets.
+- Fix commit title bar and repository view copy clipboard button order on last commit in repository view.
+- Fix mini-pipeline stage tooltip text wrapping.
+- Updated builds info link on the project settings page. (Ryan Harris)
+- 27240 Make progress bars consistent.
+- Only render hr when user can't archive project.
+- 27352-search-label-filter-header.
+- Include :author, :project, and :target in Event.with_associations.
+- Don't instantiate AR objects in Event.in_projects.
+- Don't capitalize environment name in show page.
+- Update and pin the `jwt` gem to ~> 1.5.6.
+- Edited the column header for the environments list from created to updated and added created to environments detail page colum header titles.
+- Give ci status text on pipeline graph a better font-weight.
+- Add default labels to bulk assign dropdowns.
+- Only return target project's comments for a commit.
+- Fixes Pipelines table is not showing branch name for commit.
+- Fix regression where cmd-click stopped working for todos and merge request tabs.
+- Fix stray pipelines API request when showing MR.
+- Fix Merge request pipelines displays JSON.
+- Fix current build arrow indicator.
+- Fix contribution activity alignment.
+- Show Pipeline(not Job) in MR desktop notification.
+- Fix tooltips in mini pipeline graph.
+- Display loading indicator when filtering ref switcher dropdown.
+- Show pipeline graph in MR widget if there are any stages.
+- Fix icon colors in merge request widget mini graph.
+- Improve blockquote formatting in notification emails.
+- Adds container to tooltip in order to make it work with overflow:hidden in parent element.
+- Restore pagination to admin abuse reports.
+- Ensure export files are removed after a namespace is deleted.
+- Add `y` keyboard shortcut to move to file permalink.
+- Adds /target_branch slash command functionality for merge requests. (YarNayar)
+- Patch Asciidocs rendering to block XSS.
+- contribution calendar scrolls from right to left.
+- Copying a rendered issue/comment will paste into GFM textareas as actual GFM.
+- Don't delete assigned MRs/issues when user is deleted.
+- Remove new branch button for confidential issues.
+- Don't allow project guests to subscribe to merge requests through the API. (Robert Schilling)
+- Don't connect in Gitlab::Database.adapter_name.
+- Prevent users from creating notes on resources they can't access.
+- Ignore encrypted attributes in Import/Export.
+- Change rspec test to guarantee window is resized before visiting page.
+- Prevent users from deleting system deploy keys via the project deploy key API.
+- Fix XSS vulnerability in SVG attachments.
+- Make MR-review-discussions more reliable.
+- fix incorrect sidekiq concurrency count in admin background page. (wendy0402)
+- Make notification_service spec DRYer by making test reusable. (YarNayar)
+- Redirect http://someproject.git to http://someproject. (blackst0ne)
+- Fixed group label links in issue/merge request sidebar.
+- Improve gl.utils.handleLocationHash tests.
+- Fixed Issuable sidebar not closing on smaller/mobile sized screens.
+- Resets assignee dropdown when sidebar is open.
+- Disallow system notes for closed issuables.
+- Fix timezone on issue boards due date.
+- Remove unused js response from refs controller.
+- Prevent the GitHub importer from assigning labels and comments to merge requests or issues belonging to other projects.
+- Fixed merge requests tab extra margin when fixed to window.
+- Patch XSS vulnerability in RDOC support.
+- Refresh authorizations when transferring projects.
+- Remove issue and MR counts from labels index.
+- Don't use backup Active Record connections for Sidekiq.
+- Add index to ci_trigger_requests for commit_id.
+- Add indices to improve loading of labels page.
+- Reduced query count for snippet search.
+- Update GitLab Pages to v0.3.1.
+- Upgrade omniauth gem to 1.3.2.
+- Remove deprecated GitlabCiService.
+- Requeue pending deletion projects.
+
+## 8.16.7 (2017-02-27)
+
+- No changes.
+- No changes.
+- Fix MR changes tab size count when there are over 100 files in the diff.
+
+## 8.16.6 (2017-02-17)
+
+- API: Fix file downloading. !0 (8267)
+- Reduce hits to LDAP on Git HTTP auth by reordering auth mechanisms. !8752
+- Fix filtered search user autocomplete for gitlab instances that are hosted on a subdirectory. !8891
+- Fix wrong call to ProjectCacheWorker.perform. !8910
+- Remove unnecessary queries for .atom and .json in Dashboard::ProjectsController#index. !8956
+- Fix broken anchor links when special characters are used. !8961 (Andrey Krivko)
+- Do not display deploy keys in user's own ssh keys list. !9024
+- Show merge errors in merge request widget. !9229
+- Don't delete assigned MRs/issues when user is deleted.
+- backport of EE fix !954.
+- Refresh authorizations when transferring projects.
+- Don't use backup Active Record connections for Sidekiq.
+- Check public snippets for spam.
+
## 8.16.5 (2017-02-14)
- Patch Asciidocs rendering to block XSS.
@@ -181,6 +398,10 @@ entry.
- Add margin to markdown math blocks.
- Add hover state to MR comment reply button.
+## 8.15.7 (2017-02-15)
+
+- No changes.
+
## 8.15.6 (2017-02-14)
- Patch Asciidocs rendering to block XSS.
@@ -451,6 +672,10 @@ entry.
- Whitelist next project names: help, ci, admin, search. !8227
- Adds back CSS for progress-bars. !8237
+## 8.14.10 (2017-02-15)
+
+- No changes.
+
## 8.14.9 (2017-02-14)
- Patch Asciidocs rendering to block XSS.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 72cd57ad7ff..3cbc826e6db 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,32 +1,48 @@
+## Contributor license agreement
+
+By submitting code as an individual you agree to the
+[individual contributor license agreement](doc/legal/individual_contributor_license_agreement.md).
+By submitting code as an entity you agree to the
+[corporate contributor license agreement](doc/legal/corporate_contributor_license_agreement.md).
+
+_This notice should stay as the first item in the CONTRIBUTING.MD file._
+
+---
+
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)*
+- [Contributor license agreement](#contributor-license-agreement)
- [Contribute to GitLab](#contribute-to-gitlab)
- - [Contributor license agreement](#contributor-license-agreement)
- - [Security vulnerability disclosure](#security-vulnerability-disclosure)
- - [Closing policy for issues and merge requests](#closing-policy-for-issues-and-merge-requests)
- - [Helping others](#helping-others)
- - [I want to contribute!](#i-want-to-contribute)
- - [Implement design & UI elements](#implement-design-ui-elements)
- - [Issue tracker](#issue-tracker)
- - [Feature proposals](#feature-proposals)
- - [Issue tracker guidelines](#issue-tracker-guidelines)
- - [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)
- - [Changes for Stable Releases](#changes-for-stable-releases)
- - [Definition of done](#definition-of-done)
- - [Style guides](#style-guides)
- - [Code of conduct](#code-of-conduct)
+- [Security vulnerability disclosure](#security-vulnerability-disclosure)
+- [Closing policy for issues and merge requests](#closing-policy-for-issues-and-merge-requests)
+- [Helping others](#helping-others)
+- [I want to contribute!](#i-want-to-contribute)
+- [Implement design & UI elements](#implement-design-ui-elements)
+- [Release retrospective and kickoff](#release-retrospective-and-kickoff)
+ - [Retrospective](#retrospective)
+ - [Kickoff](#kickoff)
+- [Issue tracker](#issue-tracker)
+ - [Feature proposals](#feature-proposals)
+ - [Issue tracker guidelines](#issue-tracker-guidelines)
+ - [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)
+- [Changes for Stable Releases](#changes-for-stable-releases)
+- [Definition of done](#definition-of-done)
+- [Style guides](#style-guides)
+- [Code of conduct](#code-of-conduct)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
-# Contribute to GitLab
+---
+
+## Contribute to GitLab
Thank you for your interest in contributing to GitLab. This guide details how
to contribute to GitLab in a way that is efficient for everyone.
@@ -41,13 +57,6 @@ operates please see [the GitLab contributing process](PROCESS.md).
- [GitLab Inc engineers should refer to the engineering workflow document](https://about.gitlab.com/handbook/engineering/workflow/)
-## Contributor license agreement
-
-By submitting code as an individual you agree to the
-[individual contributor license agreement](doc/legal/individual_contributor_license_agreement.md).
-By submitting code as an entity you agree to the
-[corporate contributor license agreement](doc/legal/corporate_contributor_license_agreement.md).
-
## Security vulnerability disclosure
Please report suspected security vulnerabilities in private to
@@ -93,18 +102,20 @@ Please see the [UX Guide for GitLab].
### 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].
+After each release, 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 on them.
+If you're interested, you can even join the
+[retrospective call][retro-kickoff-call], on the first working day after the
+22nd at 6pm CET / 9am PST.
### Kickoff
-Before working on the next release (usually on the 8th of each month), we have a
+Before working on the next release, 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].
+[kickoff notes] are public and you are invited to comment on them.
+If you're interested, you can even join the [kickoff call][retro-kickoff-call],
+on the first working day after the 7th at 6pm CET / 9am PST..
[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
@@ -424,7 +435,7 @@ merge request:
1. [Ruby](https://github.com/bbatsov/ruby-style-guide).
Important sections include [Source Code Layout][rss-source] and
[Naming][rss-naming]. Use:
- - multi-line method chaining style **Option B**: dot `.` on previous line
+ - multi-line method chaining style **Option A**: dot `.` on the second line
- string literal quoting style **Option A**: single quoted by default
1. [Rails](https://github.com/bbatsov/rails-style-guide)
1. [Newlines styleguide][newlines-styleguide]
diff --git a/GITLAB_PAGES_VERSION b/GITLAB_PAGES_VERSION
index 0d91a54c7d4..d15723fbe8d 100644
--- a/GITLAB_PAGES_VERSION
+++ b/GITLAB_PAGES_VERSION
@@ -1 +1 @@
-0.3.0
+0.3.2
diff --git a/Gemfile b/Gemfile
index 0060f122512..429454b9ed6 100644
--- a/Gemfile
+++ b/Gemfile
@@ -1,6 +1,6 @@
source 'https://rubygems.org'
-gem 'rails', '4.2.7.1'
+gem 'rails', '4.2.8'
gem 'rails-deprecated_sanitizer', '~> 1.0.3'
# Responders respond_to and respond_with
@@ -20,7 +20,7 @@ gem 'rugged', '~> 0.24.0'
# Authentication libraries
gem 'devise', '~> 4.2'
gem 'doorkeeper', '~> 4.2.0'
-gem 'omniauth', '~> 1.3.2'
+gem 'omniauth', '~> 1.4.2'
gem 'omniauth-auth0', '~> 1.4.1'
gem 'omniauth-azure-oauth2', '~> 0.0.6'
gem 'omniauth-cas3', '~> 1.1.2'
@@ -34,7 +34,7 @@ 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 'omniauth-authentiq', '~> 0.3.0'
gem 'rack-oauth2', '~> 1.2.1'
gem 'jwt', '~> 1.5.6'
@@ -68,7 +68,7 @@ gem 'gollum-rugged_adapter', '~> 0.4.2', require: false
gem 'github-linguist', '~> 4.7.0', require: 'linguist'
# API
-gem 'grape', '~> 0.18.0'
+gem 'grape', '~> 0.19.0'
gem 'grape-entity', '~> 0.6.0'
gem 'rack-cors', '~> 0.4.0', require: 'rack/cors'
@@ -201,7 +201,7 @@ gem 'babosa', '~> 1.0.2'
gem 'loofah', '~> 2.0.3'
# Working with license
-gem 'licensee', '~> 8.0.0'
+gem 'licensee', '~> 8.7.0'
# Protect against bruteforcing
gem 'rack-attack', '~> 4.4.1'
@@ -301,10 +301,10 @@ group :development, :test do
gem 'spring-commands-rspec', '~> 1.0.4'
gem 'spring-commands-spinach', '~> 1.1.0'
- gem 'rubocop', '~> 0.46.0', require: false
- gem 'rubocop-rspec', '~> 1.9.1', require: false
+ gem 'rubocop', '~> 0.47.1', require: false
+ gem 'rubocop-rspec', '~> 1.12.0', require: false
gem 'scss_lint', '~> 0.47.0', require: false
- gem 'haml_lint', '~> 0.18.2', require: false
+ gem 'haml_lint', '~> 0.21.0', require: false
gem 'simplecov', '0.12.0', require: false
gem 'flay', '~> 2.6.1', require: false
gem 'bundler-audit', '~> 0.5.0', require: false
@@ -333,7 +333,7 @@ gem 'newrelic_rpm', '~> 3.16'
gem 'octokit', '~> 4.6.2'
-gem 'mail_room', '~> 0.9.0'
+gem 'mail_room', '~> 0.9.1'
gem 'email_reply_trimmer', '~> 0.1'
gem 'html2text'
diff --git a/Gemfile.lock b/Gemfile.lock
index a3c2fad41ba..472cee510cb 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -3,40 +3,39 @@ GEM
specs:
RedCloth (4.3.2)
ace-rails-ap (4.1.0)
- actionmailer (4.2.7.1)
- actionpack (= 4.2.7.1)
- actionview (= 4.2.7.1)
- activejob (= 4.2.7.1)
+ actionmailer (4.2.8)
+ actionpack (= 4.2.8)
+ actionview (= 4.2.8)
+ activejob (= 4.2.8)
mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 1.0, >= 1.0.5)
- actionpack (4.2.7.1)
- actionview (= 4.2.7.1)
- activesupport (= 4.2.7.1)
+ actionpack (4.2.8)
+ actionview (= 4.2.8)
+ activesupport (= 4.2.8)
rack (~> 1.6)
rack-test (~> 0.6.2)
rails-dom-testing (~> 1.0, >= 1.0.5)
rails-html-sanitizer (~> 1.0, >= 1.0.2)
- actionview (4.2.7.1)
- activesupport (= 4.2.7.1)
+ actionview (4.2.8)
+ activesupport (= 4.2.8)
builder (~> 3.1)
erubis (~> 2.7.0)
rails-dom-testing (~> 1.0, >= 1.0.5)
- rails-html-sanitizer (~> 1.0, >= 1.0.2)
- activejob (4.2.7.1)
- activesupport (= 4.2.7.1)
+ rails-html-sanitizer (~> 1.0, >= 1.0.3)
+ activejob (4.2.8)
+ activesupport (= 4.2.8)
globalid (>= 0.3.0)
- activemodel (4.2.7.1)
- activesupport (= 4.2.7.1)
+ activemodel (4.2.8)
+ activesupport (= 4.2.8)
builder (~> 3.1)
- activerecord (4.2.7.1)
- activemodel (= 4.2.7.1)
- activesupport (= 4.2.7.1)
+ activerecord (4.2.8)
+ activemodel (= 4.2.8)
+ activesupport (= 4.2.8)
arel (~> 6.0)
activerecord_sane_schema_dumper (0.2)
rails (>= 4, < 5)
- activesupport (4.2.7.1)
+ activesupport (4.2.8)
i18n (~> 0.7)
- json (~> 1.7, >= 1.7.7)
minitest (~> 5.1)
thread_safe (~> 0.3, >= 0.3.4)
tzinfo (~> 1.1)
@@ -47,7 +46,7 @@ GEM
activerecord (>= 3.0)
akismet (2.0.0)
allocations (1.0.5)
- arel (6.0.3)
+ arel (6.0.4)
asana (0.4.0)
faraday (~> 0.9)
faraday_middleware (~> 0.9)
@@ -86,7 +85,7 @@ GEM
sass (>= 3.3.4)
brakeman (3.4.1)
browser (2.2.0)
- builder (3.2.2)
+ builder (3.2.3)
bullet (5.2.0)
activesupport (>= 3.0.0)
uniform_notifier (~> 1.10.0)
@@ -127,7 +126,7 @@ GEM
execjs
coffee-script-source (1.10.0)
colorize (0.7.7)
- concurrent-ruby (1.0.2)
+ concurrent-ruby (1.0.4)
connection_pool (2.2.1)
crack (0.4.3)
safe_yaml (~> 1.0.0)
@@ -305,7 +304,7 @@ GEM
multi_json (~> 1.11)
os (~> 0.9)
signet (~> 0.7)
- grape (0.18.0)
+ grape (0.19.1)
activesupport
builder
hashie (>= 2.1.0)
@@ -320,16 +319,16 @@ GEM
multi_json (>= 1.3.2)
haml (4.0.7)
tilt
- haml_lint (0.18.2)
+ haml_lint (0.21.0)
haml (~> 4.0)
- rake (>= 10, < 12)
- rubocop (>= 0.36.0)
+ rake (>= 10, < 13)
+ rubocop (>= 0.47.0)
sysexits (~> 1.1)
hamlit (2.6.1)
temple (~> 0.7.6)
thor
tilt
- hashie (3.4.4)
+ hashie (3.5.5)
health_check (2.2.1)
rails (>= 4.0)
hipchat (1.5.2)
@@ -354,8 +353,8 @@ GEM
json (~> 1.8)
multi_xml (>= 0.5.2)
httpclient (2.8.2)
- i18n (0.7.0)
- ice_nine (0.11.1)
+ i18n (0.8.1)
+ ice_nine (0.11.2)
influxdb (0.2.3)
cause
json
@@ -370,7 +369,7 @@ GEM
thor (>= 0.14, < 2.0)
jquery-ui-rails (5.0.5)
railties (>= 3.2.16)
- json (1.8.3)
+ json (1.8.6)
json-schema (2.6.2)
addressable (~> 2.3.8)
jwt (1.5.6)
@@ -399,8 +398,8 @@ GEM
rubyzip
thor
xml-simple
- licensee (8.0.0)
- rugged (>= 0.24b)
+ licensee (8.7.0)
+ rugged (~> 0.24)
little-plugger (1.1.4)
logging (2.1.0)
little-plugger (~> 1.1)
@@ -409,7 +408,7 @@ GEM
nokogiri (>= 1.5.9)
mail (2.6.4)
mime-types (>= 1.16, < 4)
- mail_room (0.9.0)
+ mail_room (0.9.1)
memoist (0.15.0)
method_source (0.8.2)
mime-types (2.99.3)
@@ -418,7 +417,7 @@ GEM
minitest (5.7.0)
mousetrap-rails (1.4.6)
multi_json (1.12.1)
- multi_xml (0.5.5)
+ multi_xml (0.6.0)
multipart-post (2.0.0)
mustermann (0.4.0)
tool (~> 0.2)
@@ -429,9 +428,8 @@ GEM
net-ssh (3.0.1)
netrc (0.11.0)
newrelic_rpm (3.16.0.318)
- nokogiri (1.6.8)
+ nokogiri (1.6.8.1)
mini_portile2 (~> 2.1.0)
- pkg-config (~> 1.1.7)
numerizer (0.1.1)
oauth (0.5.1)
oauth2 (1.2.0)
@@ -443,12 +441,12 @@ GEM
octokit (4.6.2)
sawyer (~> 0.8.0, >= 0.5.3)
oj (2.17.4)
- omniauth (1.3.2)
+ omniauth (1.4.2)
hashie (>= 1.2, < 4)
rack (>= 1.0, < 3)
omniauth-auth0 (1.4.1)
omniauth-oauth2 (~> 1.1)
- omniauth-authentiq (0.2.2)
+ omniauth-authentiq (0.3.0)
omniauth-oauth2 (~> 1.3, >= 1.3.1)
omniauth-azure-oauth2 (0.0.6)
jwt (~> 1.0)
@@ -503,10 +501,9 @@ GEM
os (0.9.6)
paranoia (2.2.0)
activerecord (>= 4.0, < 5.1)
- parser (2.3.1.4)
+ parser (2.4.0.0)
ast (~> 2.2)
pg (0.18.4)
- pkg-config (1.1.7)
poltergeist (1.9.0)
capybara (~> 2.1)
cliver (~> 0.3.1)
@@ -548,28 +545,28 @@ GEM
rack
rack-test (0.6.3)
rack (>= 1.0)
- rails (4.2.7.1)
- actionmailer (= 4.2.7.1)
- actionpack (= 4.2.7.1)
- actionview (= 4.2.7.1)
- activejob (= 4.2.7.1)
- activemodel (= 4.2.7.1)
- activerecord (= 4.2.7.1)
- activesupport (= 4.2.7.1)
+ rails (4.2.8)
+ actionmailer (= 4.2.8)
+ actionpack (= 4.2.8)
+ actionview (= 4.2.8)
+ activejob (= 4.2.8)
+ activemodel (= 4.2.8)
+ activerecord (= 4.2.8)
+ activesupport (= 4.2.8)
bundler (>= 1.3.0, < 2.0)
- railties (= 4.2.7.1)
+ railties (= 4.2.8)
sprockets-rails
rails-deprecated_sanitizer (1.0.3)
activesupport (>= 4.2.0.alpha)
- rails-dom-testing (1.0.7)
+ rails-dom-testing (1.0.8)
activesupport (>= 4.2.0.beta, < 5.0)
- nokogiri (~> 1.6.0)
+ nokogiri (~> 1.6)
rails-deprecated_sanitizer (>= 1.0.1)
rails-html-sanitizer (1.0.3)
loofah (~> 2.0)
- railties (4.2.7.1)
- actionpack (= 4.2.7.1)
- activesupport (= 4.2.7.1)
+ railties (4.2.8)
+ actionpack (= 4.2.8)
+ activesupport (= 4.2.8)
rake (>= 0.8.7)
thor (>= 0.18.1, < 2.0)
rainbow (2.1.0)
@@ -645,13 +642,13 @@ GEM
pg
rails
sqlite3
- rubocop (0.46.0)
- parser (>= 2.3.1.1, < 3.0)
+ rubocop (0.47.1)
+ parser (>= 2.3.3.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.9.1)
+ rubocop-rspec (1.12.0)
rubocop (>= 0.42.0)
ruby-fogbugz (0.2.1)
crack (~> 0.4)
@@ -733,10 +730,10 @@ GEM
spring (>= 0.9.1)
spring-commands-spinach (1.1.0)
spring (>= 0.9.1)
- sprockets (3.7.0)
+ sprockets (3.7.1)
concurrent-ruby (~> 1.0)
rack (> 1, < 3)
- sprockets-rails (3.1.1)
+ sprockets-rails (3.2.0)
actionpack (>= 4.0)
activesupport (>= 4.0)
sprockets (>= 3.0.0)
@@ -760,9 +757,9 @@ GEM
daemons (~> 1.0, >= 1.0.9)
eventmachine (~> 1.0, >= 1.0.4)
rack (>= 1, < 3)
- thor (0.19.1)
- thread_safe (0.3.5)
- tilt (2.0.5)
+ thor (0.19.4)
+ thread_safe (0.3.6)
+ tilt (2.0.6)
timecop (0.8.1)
timfel-krb5-auth (0.8.3)
tool (0.2.3)
@@ -779,7 +776,7 @@ GEM
unf (0.1.4)
unf_ext
unf_ext (0.0.7.2)
- unicode-display_width (1.1.1)
+ unicode-display_width (1.1.3)
unicorn (5.1.0)
kgio (~> 2.6)
raindrops (~> 0.7)
@@ -889,9 +886,9 @@ DEPENDENCIES
gollum-rugged_adapter (~> 0.4.2)
gon (~> 6.1.0)
google-api-client (~> 0.8.6)
- grape (~> 0.18.0)
+ grape (~> 0.19.0)
grape-entity (~> 0.6.0)
- haml_lint (~> 0.18.2)
+ haml_lint (~> 0.21.0)
hamlit (~> 2.6.1)
health_check (~> 2.2.0)
hipchat (~> 1.5.0)
@@ -910,9 +907,9 @@ DEPENDENCIES
kubeclient (~> 2.2.0)
letter_opener_web (~> 1.3.0)
license_finder (~> 2.1.0)
- licensee (~> 8.0.0)
+ licensee (~> 8.7.0)
loofah (~> 2.0.3)
- mail_room (~> 0.9.0)
+ mail_room (~> 0.9.1)
method_source (~> 0.8)
minitest (~> 5.7.0)
mousetrap-rails (~> 1.4.6)
@@ -923,9 +920,9 @@ DEPENDENCIES
oauth2 (~> 1.2.0)
octokit (~> 4.6.2)
oj (~> 2.17.4)
- omniauth (~> 1.3.2)
+ omniauth (~> 1.4.2)
omniauth-auth0 (~> 1.4.1)
- omniauth-authentiq (~> 0.2.0)
+ omniauth-authentiq (~> 0.3.0)
omniauth-azure-oauth2 (~> 0.0.6)
omniauth-cas3 (~> 1.1.2)
omniauth-facebook (~> 4.0.0)
@@ -949,7 +946,7 @@ DEPENDENCIES
rack-cors (~> 0.4.0)
rack-oauth2 (~> 1.2.1)
rack-proxy (~> 0.6.0)
- rails (= 4.2.7.1)
+ rails (= 4.2.8)
rails-deprecated_sanitizer (~> 1.0.3)
rainbow (~> 2.1.0)
rblineprof (~> 0.3.6)
@@ -966,8 +963,8 @@ DEPENDENCIES
rspec-rails (~> 3.5.0)
rspec-retry (~> 0.4.5)
rspec_profiling (~> 0.0.5)
- rubocop (~> 0.46.0)
- rubocop-rspec (~> 1.9.1)
+ rubocop (~> 0.47.1)
+ rubocop-rspec (~> 1.12.0)
ruby-fogbugz (~> 0.2.1)
ruby-prof (~> 0.16.2)
rugged (~> 0.24.0)
@@ -1014,4 +1011,4 @@ DEPENDENCIES
wikicloth (= 0.8.1)
BUNDLED WITH
- 1.14.3
+ 1.14.4
diff --git a/README.md b/README.md
index 4f85fac4a56..09e08adbb73 100644
--- a/README.md
+++ b/README.md
@@ -29,7 +29,7 @@ We're hiring developers, support people, and production engineers all the time,
There are two editions of GitLab:
- GitLab Community Edition (CE) is available freely under the MIT Expat license.
-- GitLab Enterprise Edition (EE) includes [extra features](https://about.gitlab.com/features/#compare) that are more useful for organizations with more than 100 users. To use EE and get official support please [become a subscriber](https://about.gitlab.com/pricing/).
+- GitLab Enterprise Edition (EE) includes [extra features](https://about.gitlab.com/products/#compare-options) that are more useful for organizations with more than 100 users. To use EE and get official support please [become a subscriber](https://about.gitlab.com/products/).
## Website
diff --git a/VERSION b/VERSION
index 5c99c061a47..64de8316674 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-8.17.0-pre
+8.18.0-pre
diff --git a/app/assets/images/favicon-blue.ico b/app/assets/images/favicon-blue.ico
new file mode 100755
index 00000000000..156fcf07588
--- /dev/null
+++ b/app/assets/images/favicon-blue.ico
Binary files differ
diff --git a/app/assets/images/icon-merge-request-unmerged.svg b/app/assets/images/icon-merge-request-unmerged.svg
new file mode 100644
index 00000000000..c4d8e65122d
--- /dev/null
+++ b/app/assets/images/icon-merge-request-unmerged.svg
@@ -0,0 +1 @@
+<svg width="12" height="15" viewBox="0 0 12 15" xmlns="http://www.w3.org/2000/svg"><path d="M10.267 11.028V5.167c-.028-.728-.318-1.372-.878-1.923-.56-.55-1.194-.85-1.922-.877h-.934V.5l-2.8 2.8 2.8 2.8V4.233h.934a.976.976 0 0 1 .644.29.88.88 0 0 1 .289.644v5.861a1.86 1.86 0 0 0 .933 3.472 1.86 1.86 0 0 0 .934-3.472zM3.733 3.3a1.86 1.86 0 0 0-1.866-1.867 1.86 1.86 0 0 0-.934 3.472v6.123a1.86 1.86 0 0 0 .933 3.472 1.86 1.86 0 0 0 .934-3.472V4.905c.55-.317.933-.914.933-1.605z" fill-rule="nonzero"/></svg>
diff --git a/app/assets/images/mailers/gitlab_footer_logo.gif b/app/assets/images/mailers/gitlab_footer_logo.gif
new file mode 100644
index 00000000000..3f4ef31947b
--- /dev/null
+++ b/app/assets/images/mailers/gitlab_footer_logo.gif
Binary files differ
diff --git a/app/assets/images/mailers/gitlab_header_logo.gif b/app/assets/images/mailers/gitlab_header_logo.gif
new file mode 100644
index 00000000000..387628f831c
--- /dev/null
+++ b/app/assets/images/mailers/gitlab_header_logo.gif
Binary files differ
diff --git a/app/assets/javascripts/admin.js b/app/assets/javascripts/admin.js
index 424dc719c78..aaed74d6073 100644
--- a/app/assets/javascripts/admin.js
+++ b/app/assets/javascripts/admin.js
@@ -61,4 +61,4 @@
return Admin;
})();
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/ajax_loading_spinner.js b/app/assets/javascripts/ajax_loading_spinner.js
new file mode 100644
index 00000000000..38a8317dbd7
--- /dev/null
+++ b/app/assets/javascripts/ajax_loading_spinner.js
@@ -0,0 +1,35 @@
+class AjaxLoadingSpinner {
+ static init() {
+ const $elements = $('.js-ajax-loading-spinner');
+
+ $elements.on('ajax:beforeSend', AjaxLoadingSpinner.ajaxBeforeSend);
+ $elements.on('ajax:complete', AjaxLoadingSpinner.ajaxComplete);
+ }
+
+ static ajaxBeforeSend(e) {
+ e.target.setAttribute('disabled', '');
+ const iconElement = e.target.querySelector('i');
+ // get first fa- icon
+ const originalIcon = iconElement.className.match(/(fa-)([^\s]+)/g).first();
+ iconElement.dataset.icon = originalIcon;
+ AjaxLoadingSpinner.toggleLoadingIcon(iconElement);
+ $(e.target).off('ajax:beforeSend', AjaxLoadingSpinner.ajaxBeforeSend);
+ }
+
+ static ajaxComplete(e) {
+ e.target.removeAttribute('disabled');
+ const iconElement = e.target.querySelector('i');
+ AjaxLoadingSpinner.toggleLoadingIcon(iconElement);
+ $(e.target).off('ajax:complete', AjaxLoadingSpinner.ajaxComplete);
+ }
+
+ static toggleLoadingIcon(iconElement) {
+ const classList = iconElement.classList;
+ classList.toggle(iconElement.dataset.icon);
+ classList.toggle('fa-spinner');
+ classList.toggle('fa-spin');
+ }
+}
+
+window.gl = window.gl || {};
+gl.AjaxLoadingSpinner = AjaxLoadingSpinner;
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index 84bbe90f3b1..86e0ad89431 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -147,4 +147,4 @@
};
window.Api = Api;
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js
index 4b5c9686cab..c51860d1604 100644
--- a/app/assets/javascripts/application.js
+++ b/app/assets/javascripts/application.js
@@ -6,13 +6,7 @@
/* global AwardsHandler */
/* global Aside */
-function requireAll(context) { return context.keys().map(context); }
-
window.$ = window.jQuery = require('jquery');
-require('jquery-ui/ui/autocomplete');
-require('jquery-ui/ui/draggable');
-require('jquery-ui/ui/effect-highlight');
-require('jquery-ui/ui/sortable');
require('jquery-ujs');
require('vendor/jquery.endless-scroll');
require('vendor/jquery.highlight');
@@ -46,15 +40,176 @@ 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)$/));
+
+// behaviors
+require('./behaviors/autosize');
+require('./behaviors/details_behavior');
+require('./behaviors/quick_submit');
+require('./behaviors/requires_input');
+require('./behaviors/toggler_behavior');
+
+// blob
+require('./blob/blob_ci_yaml');
+require('./blob/blob_dockerfile_selector');
+require('./blob/blob_dockerfile_selectors');
+require('./blob/blob_file_dropzone');
+require('./blob/blob_gitignore_selector');
+require('./blob/blob_gitignore_selectors');
+require('./blob/blob_license_selector');
+require('./blob/blob_license_selectors');
+require('./blob/template_selector');
+
+// templates
+require('./templates/issuable_template_selector');
+require('./templates/issuable_template_selectors');
+
+// commit
+require('./commit/file.js');
+require('./commit/image_file.js');
+
+// extensions
+require('./extensions/array');
+require('./extensions/custom_event');
+require('./extensions/element');
+require('./extensions/jquery');
+require('./extensions/object');
+
+// lib/utils
+require('./lib/utils/animate');
+require('./lib/utils/bootstrap_linked_tabs');
+require('./lib/utils/common_utils');
+require('./lib/utils/datetime_utility');
+require('./lib/utils/notify');
+require('./lib/utils/pretty_time');
+require('./lib/utils/text_utility');
+require('./lib/utils/type_utility');
+require('./lib/utils/url_utility');
+
+// u2f
+require('./u2f/authenticate');
+require('./u2f/error');
+require('./u2f/register');
+require('./u2f/util');
+
+// droplab
+require('./droplab/droplab');
+require('./droplab/droplab_ajax');
+require('./droplab/droplab_ajax_filter');
+require('./droplab/droplab_filter');
+
+// everything else
+require('./abuse_reports');
+require('./activities');
+require('./admin');
+require('./ajax_loading_spinner');
+require('./api');
+require('./aside');
+require('./autosave');
+require('./awards_handler');
+require('./breakpoints');
+require('./broadcast_message');
+require('./build');
+require('./build_artifacts');
+require('./build_variables');
+require('./ci_lint_editor');
+require('./commit');
+require('./commits');
+require('./compare');
+require('./compare_autocomplete');
+require('./confirm_danger_modal');
+require('./copy_as_gfm');
+require('./copy_to_clipboard');
+require('./create_label');
+require('./diff');
+require('./dispatcher');
+require('./dropzone_input');
+require('./due_date_select');
+require('./files_comment_button');
+require('./flash');
+require('./gfm_auto_complete');
+require('./gl_dropdown');
+require('./gl_field_error');
+require('./gl_field_errors');
+require('./gl_form');
+require('./group_avatar');
+require('./group_label_subscription');
+require('./groups_select');
+require('./header');
+require('./importer_status');
+require('./issuable');
+require('./issuable_context');
+require('./issuable_form');
+require('./issue');
+require('./issue_status_select');
+require('./issues_bulk_assignment');
+require('./label_manager');
+require('./labels');
+require('./labels_select');
+require('./layout_nav');
+require('./line_highlighter');
+require('./logo');
+require('./member_expiration_date');
+require('./members');
+require('./merge_request');
+require('./merge_request_tabs');
+require('./merge_request_widget');
+require('./merged_buttons');
+require('./milestone');
+require('./milestone_select');
+require('./mini_pipeline_graph_dropdown');
+require('./namespace_select');
+require('./new_branch_form');
+require('./new_commit_form');
+require('./notes');
+require('./notifications_dropdown');
+require('./notifications_form');
+require('./pager');
+require('./pipelines');
+require('./preview_markdown');
+require('./project');
+require('./project_avatar');
+require('./project_find_file');
+require('./project_fork');
+require('./project_import');
+require('./project_label_subscription');
+require('./project_new');
+require('./project_select');
+require('./project_show');
+require('./project_variables');
+require('./projects_list');
+require('./render_gfm');
+require('./render_math');
+require('./right_sidebar');
+require('./search');
+require('./search_autocomplete');
+require('./shortcuts');
+require('./shortcuts_blob');
+require('./shortcuts_dashboard_navigation');
+require('./shortcuts_find_file');
+require('./shortcuts_issuable');
+require('./shortcuts_navigation');
+require('./shortcuts_network');
+require('./signin_tabs_memoizer');
+require('./single_file_diff');
+require('./smart_interval');
+require('./snippets_list');
+require('./star');
+require('./subbable_resource');
+require('./subscription');
+require('./subscription_select');
+require('./syntax_highlight');
+require('./task_list');
+require('./todos');
+require('./tree');
+require('./user');
+require('./user_tabs');
+require('./username_validator');
+require('./users_select');
+require('./version_check_image');
+require('./visibility_select');
+require('./wikis');
+require('./zen_mode');
+
require('vendor/fuzzaldrin-plus');
require('es6-promise').polyfill();
@@ -101,11 +256,6 @@ require('es6-promise').polyfill();
}
});
- $('.nav-sidebar').niceScroll({
- cursoropacitymax: '0.4',
- cursorcolor: '#FFF',
- cursorborder: '1px solid #FFF'
- });
$('.js-select-on-focus').on('focusin', function () {
return $(this).select().one('mouseup', function (e) {
return e.preventDefault();
@@ -245,9 +395,7 @@ require('es6-promise').polyfill();
});
gl.awardsHandler = new AwardsHandler();
new Aside();
- // bind sidebar events
- new gl.Sidebar();
gl.utils.initTimeagoTimeout();
});
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/aside.js b/app/assets/javascripts/aside.js
index 8438de6cdf1..448e6e2cc78 100644
--- a/app/assets/javascripts/aside.js
+++ b/app/assets/javascripts/aside.js
@@ -22,4 +22,4 @@
return Aside;
})();
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/autosave.js b/app/assets/javascripts/autosave.js
index b16a2c0f73a..e55405135fb 100644
--- a/app/assets/javascripts/autosave.js
+++ b/app/assets/javascripts/autosave.js
@@ -59,4 +59,4 @@
return Autosave;
})();
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js
index 9d776b74965..a4ccb30e447 100644
--- a/app/assets/javascripts/awards_handler.js
+++ b/app/assets/javascripts/awards_handler.js
@@ -377,4 +377,4 @@ var emojiAliases = require('emoji-aliases');
return AwardsHandler;
})();
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/behaviors/autosize.js b/app/assets/javascripts/behaviors/autosize.js
index a489523b802..f7f41d55b52 100644
--- a/app/assets/javascripts/behaviors/autosize.js
+++ b/app/assets/javascripts/behaviors/autosize.js
@@ -25,4 +25,4 @@ var autosize = require('vendor/autosize');
autosize.update($fields);
return $fields.css('resize', 'vertical');
});
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/behaviors/details_behavior.js b/app/assets/javascripts/behaviors/details_behavior.js
index 6af8f593872..fd0840fa117 100644
--- a/app/assets/javascripts/behaviors/details_behavior.js
+++ b/app/assets/javascripts/behaviors/details_behavior.js
@@ -23,4 +23,4 @@
return e.preventDefault();
});
});
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/behaviors/quick_submit.js b/app/assets/javascripts/behaviors/quick_submit.js
index 7747306688c..a7e68ae5cb9 100644
--- a/app/assets/javascripts/behaviors/quick_submit.js
+++ b/app/assets/javascripts/behaviors/quick_submit.js
@@ -74,4 +74,4 @@ require('../extensions/jquery');
return $this.tooltip('hide');
});
});
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/behaviors/requires_input.js b/app/assets/javascripts/behaviors/requires_input.js
index 6276933e93e..6b21695d082 100644
--- a/app/assets/javascripts/behaviors/requires_input.js
+++ b/app/assets/javascripts/behaviors/requires_input.js
@@ -59,4 +59,4 @@ require('../extensions/jquery');
return hideOrShowHelpBlock($form);
});
});
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/blob/blob_file_dropzone.js b/app/assets/javascripts/blob/blob_file_dropzone.js
index 04bfe363929..5f14ff40eee 100644
--- a/app/assets/javascripts/blob/blob_file_dropzone.js
+++ b/app/assets/javascripts/blob/blob_file_dropzone.js
@@ -63,4 +63,4 @@
return BlobFileDropzone;
})();
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/blob/blob_gitignore_selector.js b/app/assets/javascripts/blob/blob_gitignore_selector.js
index 1d0bcf6471f..de20eab9cd1 100644
--- a/app/assets/javascripts/blob/blob_gitignore_selector.js
+++ b/app/assets/javascripts/blob/blob_gitignore_selector.js
@@ -20,4 +20,4 @@ require('./template_selector');
return BlobGitignoreSelector;
})(gl.TemplateSelector);
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/blob/blob_gitignore_selectors.js b/app/assets/javascripts/blob/blob_gitignore_selectors.js
index 8236457f0f1..43e5c0a5641 100644
--- a/app/assets/javascripts/blob/blob_gitignore_selectors.js
+++ b/app/assets/javascripts/blob/blob_gitignore_selectors.js
@@ -23,4 +23,4 @@
return BlobGitignoreSelectors;
})();
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/blob/blob_license_selector.js b/app/assets/javascripts/blob/blob_license_selector.js
index 1d5672d4c48..b582052a76e 100644
--- a/app/assets/javascripts/blob/blob_license_selector.js
+++ b/app/assets/javascripts/blob/blob_license_selector.js
@@ -25,4 +25,4 @@ require('./template_selector');
return BlobLicenseSelector;
})(gl.TemplateSelector);
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/blob_edit/blob_edit_bundle.js b/app/assets/javascripts/blob_edit/blob_edit_bundle.js
index 9e0754819fa..0436bbb0eaf 100644
--- a/app/assets/javascripts/blob_edit/blob_edit_bundle.js
+++ b/app/assets/javascripts/blob_edit/blob_edit_bundle.js
@@ -12,4 +12,4 @@ require('./edit_blob');
var blob = new EditBlob(url, $('.js-edit-blob-form').data('blob-language'));
new NewCommitForm($('.js-edit-blob-form'));
});
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/blob_edit/edit_blob.js b/app/assets/javascripts/blob_edit/edit_blob.js
index 079445e8278..a1127b9e30e 100644
--- a/app/assets/javascripts/blob_edit/edit_blob.js
+++ b/app/assets/javascripts/blob_edit/edit_blob.js
@@ -85,4 +85,4 @@
return EditBlob;
})();
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/boards/boards_bundle.js.es6 b/app/assets/javascripts/boards/boards_bundle.js.es6
index 8f30900198e..55d13be6e5f 100644
--- a/app/assets/javascripts/boards/boards_bundle.js.es6
+++ b/app/assets/javascripts/boards/boards_bundle.js.es6
@@ -1,16 +1,20 @@
-/* eslint-disable one-var, quote-props, comma-dangle, space-before-function-paren, import/newline-after-import, no-multi-spaces, max-len */
+/* eslint-disable one-var, quote-props, comma-dangle, space-before-function-paren */
/* global Vue */
/* global BoardService */
-function requireAll(context) { return context.keys().map(context); }
-
window.Vue = require('vue');
window.Vue.use(require('vue-resource'));
-requireAll(require.context('./models', true, /^\.\/.*\.(js|es6)$/));
-requireAll(require.context('./stores', true, /^\.\/.*\.(js|es6)$/));
-requireAll(require.context('./services', true, /^\.\/.*\.(js|es6)$/));
-requireAll(require.context('./mixins', true, /^\.\/.*\.(js|es6)$/));
-requireAll(require.context('./filters', true, /^\.\/.*\.(js|es6)$/));
+require('./models/issue');
+require('./models/label');
+require('./models/list');
+require('./models/milestone');
+require('./models/user');
+require('./stores/boards_store');
+require('./stores/modal_store');
+require('./services/board_service');
+require('./mixins/modal_mixins');
+require('./mixins/sortable_default_options');
+require('./filters/due_date_filters');
require('./components/board');
require('./components/board_sidebar');
require('./components/new_list_dropdown');
@@ -93,17 +97,53 @@ $(() => {
modal: ModalStore.store,
store: Store.state,
},
+ watch: {
+ disabled() {
+ this.updateTooltip();
+ },
+ },
computed: {
disabled() {
- return Store.shouldAddBlankState();
+ return !this.store.lists.filter(list => list.type !== 'blank' && list.type !== 'done').length;
+ },
+ tooltipTitle() {
+ if (this.disabled) {
+ return 'Please add a list to your board first';
+ }
+
+ return '';
},
},
+ methods: {
+ updateTooltip() {
+ const $tooltip = $(this.$el);
+
+ this.$nextTick(() => {
+ if (this.disabled) {
+ $tooltip.tooltip();
+ } else {
+ $tooltip.tooltip('destroy');
+ }
+ });
+ },
+ openModal() {
+ if (!this.disabled) {
+ this.toggleModal(true);
+ }
+ },
+ },
+ mounted() {
+ this.updateTooltip();
+ },
template: `
<button
- class="btn btn-create pull-right prepend-left-10 has-tooltip"
+ class="btn btn-create pull-right prepend-left-10"
type="button"
- :disabled="disabled"
- @click="toggleModal(true)">
+ data-placement="bottom"
+ :class="{ 'disabled': disabled }"
+ :title="tooltipTitle"
+ :aria-disabled="disabled"
+ @click="openModal">
Add issues
</button>
`,
diff --git a/app/assets/javascripts/boards/components/board_card.js b/app/assets/javascripts/boards/components/board_card.js
new file mode 100644
index 00000000000..52f61d84517
--- /dev/null
+++ b/app/assets/javascripts/boards/components/board_card.js
@@ -0,0 +1,69 @@
+/* global Vue */
+require('./issue_card_inner');
+
+const Store = gl.issueBoards.BoardsStore;
+
+module.exports = {
+ name: 'BoardsIssueCard',
+ template: `
+ <li class="card"
+ :class="{ 'user-can-drag': !disabled && issue.id, 'is-disabled': disabled || !issue.id, 'is-active': issueDetailVisible }"
+ :index="index"
+ :data-issue-id="issue.id"
+ @mousedown="mouseDown"
+ @mousemove="mouseMove"
+ @mouseup="showIssue($event)">
+ <issue-card-inner
+ :list="list"
+ :issue="issue"
+ :issue-link-base="issueLinkBase"
+ :root-path="rootPath" />
+ </li>
+ `,
+ components: {
+ 'issue-card-inner': gl.issueBoards.IssueCardInner,
+ },
+ props: {
+ list: Object,
+ issue: Object,
+ issueLinkBase: String,
+ disabled: Boolean,
+ index: Number,
+ rootPath: String,
+ },
+ data() {
+ return {
+ showDetail: false,
+ detailIssue: Store.detail,
+ };
+ },
+ computed: {
+ issueDetailVisible() {
+ return this.detailIssue.issue && this.detailIssue.issue.id === this.issue.id;
+ },
+ },
+ methods: {
+ mouseDown() {
+ this.showDetail = true;
+ },
+ mouseMove() {
+ this.showDetail = false;
+ },
+ showIssue(e) {
+ const targetTagName = e.target.tagName.toLowerCase();
+
+ if (targetTagName === 'a' || targetTagName === 'button') return;
+
+ if (this.showDetail) {
+ this.showDetail = false;
+
+ if (Store.detail.issue && Store.detail.issue.id === this.issue.id) {
+ Store.detail.issue = {};
+ } else {
+ Store.detail.issue = this.issue;
+ Store.detail.list = this.list;
+ }
+ }
+ },
+ },
+};
diff --git a/app/assets/javascripts/boards/components/board_card.js.es6 b/app/assets/javascripts/boards/components/board_card.js.es6
deleted file mode 100644
index 0ea66bd027c..00000000000
--- a/app/assets/javascripts/boards/components/board_card.js.es6
+++ /dev/null
@@ -1,61 +0,0 @@
-/* eslint-disable comma-dangle, space-before-function-paren, dot-notation */
-/* global Vue */
-
-require('./issue_card_inner');
-
-(() => {
- const Store = gl.issueBoards.BoardsStore;
-
- window.gl = window.gl || {};
- window.gl.issueBoards = window.gl.issueBoards || {};
-
- 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,
- rootPath: String,
- },
- data () {
- return {
- showDetail: false,
- detailIssue: Store.detail
- };
- },
- computed: {
- issueDetailVisible () {
- return this.detailIssue.issue && this.detailIssue.issue.id === this.issue.id;
- }
- },
- methods: {
- mouseDown () {
- this.showDetail = true;
- },
- mouseMove() {
- this.showDetail = false;
- },
- showIssue (e) {
- const targetTagName = e.target.tagName.toLowerCase();
-
- if (targetTagName === 'a' || targetTagName === 'button') return;
-
- if (this.showDetail) {
- this.showDetail = false;
-
- if (Store.detail.issue && Store.detail.issue.id === this.issue.id) {
- 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 60b0a30af3f..d92047cc0f8 100644
--- a/app/assets/javascripts/boards/components/board_list.js.es6
+++ b/app/assets/javascripts/boards/components/board_list.js.es6
@@ -2,7 +2,7 @@
/* global Vue */
/* global Sortable */
-require('./board_card');
+const boardCard = require('./board_card');
require('./board_new_issue');
(() => {
@@ -14,7 +14,7 @@ require('./board_new_issue');
gl.issueBoards.BoardList = Vue.extend({
template: '#js-board-list-template',
components: {
- 'board-card': gl.issueBoards.BoardCard,
+ boardCard,
'board-new-issue': gl.issueBoards.BoardNewIssue
},
props: {
diff --git a/app/assets/javascripts/boards/models/list.js.es6 b/app/assets/javascripts/boards/models/list.js.es6
index 5152be56b66..8158ed4ec2c 100644
--- a/app/assets/javascripts/boards/models/list.js.es6
+++ b/app/assets/javascripts/boards/models/list.js.es6
@@ -123,14 +123,18 @@ class List {
if (listFrom) {
this.issuesSize += 1;
- gl.boardService.moveIssue(issue.id, listFrom.id, this.id)
- .then(() => {
- listFrom.getIssues(false);
- });
+ this.updateIssueLabel(issue, listFrom);
}
}
}
+ updateIssueLabel(issue, listFrom) {
+ gl.boardService.moveIssue(issue.id, listFrom.id, this.id)
+ .then(() => {
+ listFrom.getIssues(false);
+ });
+ }
+
findIssue (id) {
return this.issues.filter(issue => issue.id === id)[0];
}
diff --git a/app/assets/javascripts/boards/stores/boards_store.js.es6 b/app/assets/javascripts/boards/stores/boards_store.js.es6
index 50842ecbaaa..56436c8fdc7 100644
--- a/app/assets/javascripts/boards/stores/boards_store.js.es6
+++ b/app/assets/javascripts/boards/stores/boards_store.js.es6
@@ -92,9 +92,12 @@
const issueLists = issue.getLists();
const listLabels = issueLists.map(listIssue => listIssue.label);
- // Add to new lists issues if it doesn't already exist
if (!issueTo) {
+ // Add to new lists issues if it doesn't already exist
listTo.addIssue(issue, listFrom, newIndex);
+ } else {
+ listTo.updateIssueLabel(issue, listFrom);
+ issueTo.removeLabel(listFrom.label);
}
if (listTo.type === 'done') {
diff --git a/app/assets/javascripts/breakpoints.js b/app/assets/javascripts/breakpoints.js
index f8dac1ff56e..22e93328548 100644
--- a/app/assets/javascripts/breakpoints.js
+++ b/app/assets/javascripts/breakpoints.js
@@ -69,4 +69,4 @@
})(this));
window.Breakpoints = Breakpoints;
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/broadcast_message.js b/app/assets/javascripts/broadcast_message.js
index dbdadc73c3f..e8531c43b4b 100644
--- a/app/assets/javascripts/broadcast_message.js
+++ b/app/assets/javascripts/broadcast_message.js
@@ -31,4 +31,4 @@
}
});
});
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js
index c5a962dd199..6e6e9b18686 100644
--- a/app/assets/javascripts/build.js
+++ b/app/assets/javascripts/build.js
@@ -7,7 +7,7 @@
var DOWN_BUILD_TRACE = '#down-build-trace';
this.Build = (function() {
- Build.interval = null;
+ Build.timeout = null;
Build.state = null;
@@ -31,7 +31,7 @@
this.$scrollBottomBtn = $('#scroll-bottom');
this.$buildRefreshAnimation = $('.js-build-refresh');
- clearInterval(Build.interval);
+ clearTimeout(Build.timeout);
// Init breakpoint checker
this.bp = Breakpoints.get();
@@ -52,17 +52,7 @@
this.getInitialBuildTrace();
this.initScrollButtonAffix();
}
- if (this.buildStatus === "running" || this.buildStatus === "pending") {
- Build.interval = setInterval((function(_this) {
- // Check for new build output if user still watching build page
- // Only valid for runnig build when output changes during time
- return function() {
- if (_this.location() === _this.pageUrl) {
- return _this.getBuildTrace();
- }
- };
- })(this), 4000);
- }
+ this.invokeBuildTrace();
}
Build.prototype.initSidebar = function() {
@@ -75,6 +65,22 @@
return window.location.href.split("#")[0];
};
+ Build.prototype.invokeBuildTrace = function() {
+ var continueRefreshStatuses = ['running', 'pending'];
+ // Continue to update build trace when build is running or pending
+ if (continueRefreshStatuses.indexOf(this.buildStatus) !== -1) {
+ // Check for new build output if user still watching build page
+ // Only valid for runnig build when output changes during time
+ Build.timeout = setTimeout((function(_this) {
+ return function() {
+ if (_this.location() === _this.pageUrl) {
+ return _this.getBuildTrace();
+ }
+ };
+ })(this), 4000);
+ }
+ };
+
Build.prototype.getInitialBuildTrace = function() {
var removeRefreshStatuses = ['success', 'failed', 'canceled', 'skipped'];
@@ -86,7 +92,7 @@
if (window.location.hash === DOWN_BUILD_TRACE) {
$("html,body").scrollTop(this.$buildTrace.height());
}
- if (removeRefreshStatuses.indexOf(buildData.status) >= 0) {
+ if (removeRefreshStatuses.indexOf(buildData.status) !== -1) {
this.$buildRefreshAnimation.remove();
return this.initScrollMonitor();
}
@@ -105,6 +111,7 @@
if (log.state) {
_this.state = log.state;
}
+ _this.invokeBuildTrace();
if (log.status === "running") {
if (log.append) {
$('.js-build-output').append(log.html);
@@ -275,4 +282,4 @@
return Build;
})();
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/build_artifacts.js b/app/assets/javascripts/build_artifacts.js
index 083448552b6..cae9a0ffca4 100644
--- a/app/assets/javascripts/build_artifacts.js
+++ b/app/assets/javascripts/build_artifacts.js
@@ -23,4 +23,4 @@
return BuildArtifacts;
})();
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/commit.js b/app/assets/javascripts/commit.js
index c656ae4e241..566b322eb49 100644
--- a/app/assets/javascripts/commit.js
+++ b/app/assets/javascripts/commit.js
@@ -11,4 +11,4 @@
return Commit;
})();
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/commit/file.js b/app/assets/javascripts/commit/file.js
index 184b4561d2e..ee087c978dd 100644
--- a/app/assets/javascripts/commit/file.js
+++ b/app/assets/javascripts/commit/file.js
@@ -11,4 +11,4 @@
return CommitFile;
})();
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/commit/image_file.js b/app/assets/javascripts/commit/image_file.js
index f09a6b1e676..17d14dc1e79 100644
--- a/app/assets/javascripts/commit/image_file.js
+++ b/app/assets/javascripts/commit/image_file.js
@@ -52,6 +52,30 @@
return this.views[viewMode].call(this);
};
+ ImageFile.prototype.initDraggable = function($el, padding, callback) {
+ var dragging = false;
+ var $body = $('body');
+ var $offsetEl = $el.parent();
+
+ $el.off('mousedown').on('mousedown', function() {
+ dragging = true;
+ $body.css('user-select', 'none');
+ });
+
+ $body.off('mouseup').off('mousemove').on('mouseup', function() {
+ dragging = false;
+ $body.css('user-select', '');
+ })
+ .on('mousemove', function(e) {
+ var left;
+ if (!dragging) return;
+
+ left = e.pageX - ($offsetEl.offset().left + padding);
+
+ callback(e, left);
+ });
+ };
+
prepareFrames = function(view) {
var maxHeight, maxWidth;
maxWidth = 0;
@@ -96,26 +120,30 @@
maxHeight = 0;
return $('.swipe.view', this.file).each((function(_this) {
return function(index, view) {
- var ref;
+ var $swipeWrap, $swipeBar, $swipeFrame, wrapPadding, ref;
ref = prepareFrames(view), maxWidth = ref[0], maxHeight = ref[1];
- $('.swipe-frame', view).css({
+ $swipeFrame = $('.swipe-frame', view);
+ $swipeWrap = $('.swipe-wrap', view);
+ $swipeBar = $('.swipe-bar', view);
+
+ $swipeFrame.css({
width: maxWidth + 16,
height: maxHeight + 28
});
- $('.swipe-wrap', view).css({
+ $swipeWrap.css({
width: maxWidth + 1,
height: maxHeight + 2
});
- return $('.swipe-bar', view).css({
+ $swipeBar.css({
left: 0
- }).draggable({
- axis: 'x',
- containment: 'parent',
- drag: function(event) {
- return $('.swipe-wrap', view).width((maxWidth + 1) - $(this).position().left);
- },
- stop: function(event) {
- return $('.swipe-wrap', view).width((maxWidth + 1) - $(this).position().left);
+ });
+
+ wrapPadding = parseInt($swipeWrap.css('right').replace('px', ''), 10);
+
+ _this.initDraggable($swipeBar, wrapPadding, function(e, left) {
+ if (left > 0 && left < $swipeFrame.width() - (wrapPadding * 2)) {
+ $swipeWrap.width((maxWidth + 1) - left);
+ $swipeBar.css('left', left);
}
});
};
@@ -128,9 +156,14 @@
dragTrackWidth = $('.drag-track', this.file).width() - $('.dragger', this.file).width();
return $('.onion-skin.view', this.file).each((function(_this) {
return function(index, view) {
- var ref;
+ var $frame, $track, $dragger, $frameAdded, framePadding, ref, dragging = false;
ref = prepareFrames(view), maxWidth = ref[0], maxHeight = ref[1];
- $('.onion-skin-frame', view).css({
+ $frame = $('.onion-skin-frame', view);
+ $frameAdded = $('.frame.added', view);
+ $track = $('.drag-track', view);
+ $dragger = $('.dragger', $track);
+
+ $frame.css({
width: maxWidth + 16,
height: maxHeight + 28
});
@@ -138,16 +171,18 @@
width: maxWidth + 1,
height: maxHeight + 2
});
- return $('.dragger', view).css({
+ $dragger.css({
left: dragTrackWidth
- }).draggable({
- axis: 'x',
- containment: 'parent',
- drag: function(event) {
- return $('.frame.added', view).css('opacity', $(this).position().left / dragTrackWidth);
- },
- stop: function(event) {
- return $('.frame.added', view).css('opacity', $(this).position().left / dragTrackWidth);
+ });
+
+ framePadding = parseInt($frameAdded.css('right').replace('px', ''), 10);
+
+ _this.initDraggable($dragger, framePadding, function(e, left) {
+ var opacity = left / dragTrackWidth;
+
+ if (opacity >= 0 && opacity <= 1) {
+ $dragger.css('left', left);
+ $frameAdded.css('opacity', opacity);
}
});
};
@@ -173,4 +208,4 @@
return ImageFile;
})();
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_store.js.es6 b/app/assets/javascripts/commit/pipelines/pipelines_store.js.es6
index f1b41911b73..f1b80e45444 100644
--- a/app/assets/javascripts/commit/pipelines/pipelines_store.js.es6
+++ b/app/assets/javascripts/commit/pipelines/pipelines_store.js.es6
@@ -4,6 +4,7 @@
*
* Used to store the Pipelines rendered in the commit view in the pipelines table.
*/
+require('../../vue_realtime_listener');
class PipelinesStore {
constructor() {
@@ -24,7 +25,7 @@ class PipelinesStore {
* update the time to show how long as passed.
*
*/
- startTimeAgoLoops() {
+ static startTimeAgoLoops() {
const startTimeLoops = () => {
this.timeLoopInterval = setInterval(() => {
this.$children[0].$children.reduce((acc, component) => {
@@ -44,7 +45,4 @@ class PipelinesStore {
}
}
-window.gl = window.gl || {};
-gl.commits = gl.commits || {};
-gl.commits.pipelines = gl.commits.pipelines || {};
-gl.commits.pipelines.PipelinesStore = PipelinesStore;
+module.exports = PipelinesStore;
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.js.es6 b/app/assets/javascripts/commit/pipelines/pipelines_table.js.es6
index 5c1a7eb1052..e7c6c063413 100644
--- a/app/assets/javascripts/commit/pipelines/pipelines_table.js.es6
+++ b/app/assets/javascripts/commit/pipelines/pipelines_table.js.es6
@@ -6,9 +6,8 @@ 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');
+const PipelineStore = require('./pipelines_store');
/**
*
@@ -41,7 +40,7 @@ require('./pipelines_store');
data() {
const pipelinesTableData = document.querySelector('#commit-pipeline-table-view').dataset;
const svgsData = document.querySelector('.pipeline-svgs').dataset;
- const store = new gl.commits.pipelines.PipelinesStore();
+ const store = new PipelineStore();
// Transform svgs DOMStringMap to a plain Object.
const svgsObject = gl.utils.DOMStringMapToObject(svgsData);
@@ -71,7 +70,6 @@ require('./pipelines_store');
.then(response => response.json())
.then((json) => {
this.store.storePipelines(json);
- this.store.startTimeAgoLoops.call(this, Vue);
this.isLoading = false;
})
.catch(() => {
@@ -80,6 +78,12 @@ require('./pipelines_store');
});
},
+ beforeUpdate() {
+ if (this.state.pipelines.length && this.$children) {
+ PipelineStore.startTimeAgoLoops.call(this, Vue);
+ }
+ },
+
template: `
<div class="pipelines">
<div class="realtime-loading" v-if="isLoading">
diff --git a/app/assets/javascripts/commits.js b/app/assets/javascripts/commits.js
index c6fdfbcaa10..ccd895f3bf4 100644
--- a/app/assets/javascripts/commits.js
+++ b/app/assets/javascripts/commits.js
@@ -65,4 +65,4 @@
return CommitsList;
})();
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/compare.js b/app/assets/javascripts/compare.js
index 9591df70e9c..15df105d4cc 100644
--- a/app/assets/javascripts/compare.js
+++ b/app/assets/javascripts/compare.js
@@ -88,4 +88,4 @@
return Compare;
})();
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/compare_autocomplete.js.es6 b/app/assets/javascripts/compare_autocomplete.js.es6
index 3587431ab69..1eca973e069 100644
--- a/app/assets/javascripts/compare_autocomplete.js.es6
+++ b/app/assets/javascripts/compare_autocomplete.js.es6
@@ -66,4 +66,4 @@
return CompareAutocomplete;
})();
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/confirm_danger_modal.js b/app/assets/javascripts/confirm_danger_modal.js
index 35d98492012..a1c1b721228 100644
--- a/app/assets/javascripts/confirm_danger_modal.js
+++ b/app/assets/javascripts/confirm_danger_modal.js
@@ -28,4 +28,4 @@
return ConfirmDangerModal;
})();
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/copy_to_clipboard.js b/app/assets/javascripts/copy_to_clipboard.js
index 0029c59e550..615f485e18a 100644
--- a/app/assets/javascripts/copy_to_clipboard.js
+++ b/app/assets/javascripts/copy_to_clipboard.js
@@ -46,4 +46,4 @@ window.Clipboard = require('vendor/clipboard');
clipboard.on('success', genericSuccess);
return clipboard.on('error', genericError);
});
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/create_label.js.es6 b/app/assets/javascripts/create_label.js.es6
index 947c129d5b5..85384d98126 100644
--- a/app/assets/javascripts/create_label.js.es6
+++ b/app/assets/javascripts/create_label.js.es6
@@ -107,9 +107,9 @@
if (typeof label.message === 'string') {
errors = label.message;
} else {
- errors = label.message.map(function (value, key) {
- return key + " " + value[0];
- }).join("<br/>");
+ errors = Object.keys(label.message).map(key =>
+ `${gl.text.humanize(key)} ${label.message[key].join(', ')}`
+ ).join("<br/>");
}
this.$newLabelError
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 dbdb01c8c68..411ac7b24b2 100644
--- a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6
+++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6
@@ -4,10 +4,20 @@
window.Vue = require('vue');
window.Cookies = require('js-cookie');
-
-function requireAll(context) { return context.keys().map(context); }
-requireAll(require.context('./svg', false, /^\.\/.*\.(js|es6)$/));
-requireAll(require.context('.', true, /^\.\/(?!cycle_analytics_bundle).*\.(js|es6)$/));
+require('./svg/icon_branch');
+require('./svg/icon_build_status');
+require('./svg/icon_commit');
+require('./components/stage_code_component');
+require('./components/stage_issue_component');
+require('./components/stage_plan_component');
+require('./components/stage_production_component');
+require('./components/stage_review_component');
+require('./components/stage_staging_component');
+require('./components/stage_test_component');
+require('./components/total_time_component');
+require('./cycle_analytics_service');
+require('./cycle_analytics_store');
+require('./default_event_objects');
$(() => {
const OVERVIEW_DIALOG_COOKIE = 'cycle_analytics_help_dismissed';
@@ -97,7 +107,7 @@ $(() => {
}
this.isLoadingStage = true;
- cycleAnalyticsStore.setStageEvents([]);
+ cycleAnalyticsStore.setStageEvents([], stage);
cycleAnalyticsStore.setActiveStage(stage);
cycleAnalyticsService
@@ -107,7 +117,7 @@ $(() => {
})
.done((response) => {
this.isEmptyStage = !response.events.length;
- cycleAnalyticsStore.setStageEvents(response.events);
+ cycleAnalyticsStore.setStageEvents(response.events, stage);
})
.error(() => {
this.isEmptyStage = true;
diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js.es6 b/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js.es6
index be732971c7f..7ae9de7297c 100644
--- a/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js.es6
+++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js.es6
@@ -1,4 +1,8 @@
/* eslint-disable no-param-reassign */
+
+require('../lib/utils/text_utility');
+const DEFAULT_EVENT_OBJECTS = require('./default_event_objects');
+
((global) => {
global.cycleAnalytics = global.cycleAnalytics || {};
@@ -34,11 +38,12 @@
});
newData.stages.forEach((item) => {
- const stageName = item.title.toLowerCase();
+ const stageSlug = gl.text.dasherize(item.title.toLowerCase());
item.active = false;
- item.isUserAllowed = data.permissions[stageName];
- item.emptyStageText = EMPTY_STAGE_TEXTS[stageName];
- item.component = `stage-${stageName}-component`;
+ item.isUserAllowed = data.permissions[stageSlug];
+ item.emptyStageText = EMPTY_STAGE_TEXTS[stageSlug];
+ item.component = `stage-${stageSlug}-component`;
+ item.slug = stageSlug;
});
newData.analytics = data;
return newData;
@@ -58,31 +63,36 @@
this.deactivateAllStages();
stage.active = true;
},
- setStageEvents(events) {
- this.state.events = this.decorateEvents(events);
+ setStageEvents(events, stage) {
+ this.state.events = this.decorateEvents(events, stage);
},
- decorateEvents(events) {
+ decorateEvents(events, stage) {
const newEvents = [];
events.forEach((item) => {
if (!item) return;
- item.totalTime = item.total_time;
- item.author.webUrl = item.author.web_url;
- item.author.avatarUrl = item.author.avatar_url;
+ const eventItem = Object.assign({}, DEFAULT_EVENT_OBJECTS[stage.slug], item);
+
+ eventItem.totalTime = eventItem.total_time;
+
+ if (eventItem.author) {
+ eventItem.author.webUrl = eventItem.author.web_url;
+ eventItem.author.avatarUrl = eventItem.author.avatar_url;
+ }
- if (item.created_at) item.createdAt = item.created_at;
- if (item.short_sha) item.shortSha = item.short_sha;
- if (item.commit_url) item.commitUrl = item.commit_url;
+ if (eventItem.created_at) eventItem.createdAt = eventItem.created_at;
+ if (eventItem.short_sha) eventItem.shortSha = eventItem.short_sha;
+ if (eventItem.commit_url) eventItem.commitUrl = eventItem.commit_url;
- delete item.author.web_url;
- delete item.author.avatar_url;
- delete item.total_time;
- delete item.created_at;
- delete item.short_sha;
- delete item.commit_url;
+ delete eventItem.author.web_url;
+ delete eventItem.author.avatar_url;
+ delete eventItem.total_time;
+ delete eventItem.created_at;
+ delete eventItem.short_sha;
+ delete eventItem.commit_url;
- newEvents.push(item);
+ newEvents.push(eventItem);
});
return newEvents;
diff --git a/app/assets/javascripts/cycle_analytics/default_event_objects.js.es6 b/app/assets/javascripts/cycle_analytics/default_event_objects.js.es6
new file mode 100644
index 00000000000..cfaf9835bf8
--- /dev/null
+++ b/app/assets/javascripts/cycle_analytics/default_event_objects.js.es6
@@ -0,0 +1,98 @@
+module.exports = {
+ issue: {
+ created_at: '',
+ url: '',
+ iid: '',
+ title: '',
+ total_time: {},
+ author: {
+ avatar_url: '',
+ id: '',
+ name: '',
+ web_url: '',
+ },
+ },
+ plan: {
+ title: '',
+ commit_url: '',
+ short_sha: '',
+ total_time: {},
+ author: {
+ name: '',
+ id: '',
+ avatar_url: '',
+ web_url: '',
+ },
+ },
+ code: {
+ title: '',
+ iid: '',
+ created_at: '',
+ url: '',
+ total_time: {},
+ author: {
+ name: '',
+ id: '',
+ avatar_url: '',
+ web_url: '',
+ },
+ },
+ test: {
+ name: '',
+ id: '',
+ date: '',
+ url: '',
+ short_sha: '',
+ commit_url: '',
+ total_time: {},
+ branch: {
+ name: '',
+ url: '',
+ },
+ },
+ review: {
+ title: '',
+ iid: '',
+ created_at: '',
+ url: '',
+ state: '',
+ total_time: {},
+ author: {
+ name: '',
+ id: '',
+ avatar_url: '',
+ web_url: '',
+ },
+ },
+ staging: {
+ id: '',
+ short_sha: '',
+ date: '',
+ url: '',
+ commit_url: '',
+ total_time: {},
+ author: {
+ name: '',
+ id: '',
+ avatar_url: '',
+ web_url: '',
+ },
+ branch: {
+ name: '',
+ url: '',
+ },
+ },
+ production: {
+ title: '',
+ created_at: '',
+ url: '',
+ iid: '',
+ total_time: {},
+ author: {
+ name: '',
+ id: '',
+ avatar_url: '',
+ web_url: '',
+ },
+ },
+};
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 190461451d5..cadf8b96b87 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,18 @@
-/* eslint-disable func-names, comma-dangle, new-cap, no-new, import/newline-after-import, no-multi-spaces, max-len */
+/* eslint-disable func-names, comma-dangle, new-cap, no-new, max-len */
/* global Vue */
/* global ResolveCount */
-function requireAll(context) { return context.keys().map(context); }
const Vue = require('vue');
-requireAll(require.context('./models', false, /^\.\/.*\.(js|es6)$/));
-requireAll(require.context('./stores', false, /^\.\/.*\.(js|es6)$/));
-requireAll(require.context('./services', false, /^\.\/.*\.(js|es6)$/));
-requireAll(require.context('./mixins', false, /^\.\/.*\.(js|es6)$/));
-requireAll(require.context('./components', false, /^\.\/.*\.(js|es6)$/));
+require('./models/discussion');
+require('./models/note');
+require('./stores/comments');
+require('./services/resolve');
+require('./mixins/discussion');
+require('./components/comment_resolve_btn');
+require('./components/jump_to_discussion');
+require('./components/resolve_btn');
+require('./components/resolve_count');
+require('./components/resolve_discussion_btn');
$(() => {
const projectPath = document.querySelector('.merge-request').dataset.projectPath;
diff --git a/app/assets/javascripts/dispatcher.js.es6 b/app/assets/javascripts/dispatcher.js.es6
index 7eec2d39a9c..0f678492d4c 100644
--- a/app/assets/javascripts/dispatcher.js.es6
+++ b/app/assets/javascripts/dispatcher.js.es6
@@ -36,6 +36,7 @@
/* global Shortcuts */
const ShortcutsBlob = require('./shortcuts_blob');
+const UserCallout = require('./user_callout');
(function() {
var Dispatcher;
@@ -74,7 +75,7 @@ const ShortcutsBlob = require('./shortcuts_blob');
case 'projects:merge_requests:index':
case 'projects:issues:index':
if (gl.FilteredSearchManager) {
- new gl.FilteredSearchManager();
+ new gl.FilteredSearchManager(page === 'projects:issues:index' ? 'issues' : 'merge_requests');
}
Issuable.init();
new gl.IssuableBulkActions({
@@ -108,6 +109,9 @@ const ShortcutsBlob = require('./shortcuts_blob');
case 'projects:compare:show':
new gl.Diff();
break;
+ case 'projects:branches:index':
+ gl.AjaxLoadingSpinner.init();
+ break;
case 'projects:issues:new':
case 'projects:issues:edit':
shortcut_handler = new ShortcutsNavigation();
@@ -118,6 +122,7 @@ const ShortcutsBlob = require('./shortcuts_blob');
new gl.IssuableTemplateSelectors();
break;
case 'projects:merge_requests:new':
+ case 'projects:merge_requests:new_diffs':
case 'projects:merge_requests:edit':
new gl.Diff();
shortcut_handler = new ShortcutsNavigation();
@@ -273,6 +278,9 @@ const ShortcutsBlob = require('./shortcuts_blob');
case 'ci:lints:show':
new gl.CILintEditor();
break;
+ case 'users:show':
+ new UserCallout();
+ break;
}
switch (path.first()) {
case 'sessions':
@@ -309,6 +317,7 @@ const ShortcutsBlob = require('./shortcuts_blob');
case 'dashboard':
case 'root':
shortcut_handler = new ShortcutsDashboardNavigation();
+ new UserCallout();
break;
case 'profiles':
new NotificationsForm();
@@ -382,4 +391,4 @@ const ShortcutsBlob = require('./shortcuts_blob');
return Dispatcher;
})();
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js
index a510eebae1a..646f836aff0 100644
--- a/app/assets/javascripts/dropzone_input.js
+++ b/app/assets/javascripts/dropzone_input.js
@@ -126,13 +126,14 @@ require('./preview_markdown');
};
pasteText = function(text) {
var afterSelection, beforeSelection, caretEnd, caretStart, textEnd;
+ var formattedText = text + "\n\n";
caretStart = $(child)[0].selectionStart;
caretEnd = $(child)[0].selectionEnd;
textEnd = $(child).val().length;
beforeSelection = $(child).val().substring(0, caretStart);
afterSelection = $(child).val().substring(caretEnd, textEnd);
- $(child).val(beforeSelection + text + afterSelection);
- child.get(0).setSelectionRange(caretStart + text.length, caretEnd + text.length);
+ $(child).val(beforeSelection + formattedText + afterSelection);
+ child.get(0).setSelectionRange(caretStart + formattedText.length, caretEnd + formattedText.length);
return form_textarea.trigger("input");
};
getFilename = function(e) {
@@ -216,4 +217,4 @@ require('./preview_markdown');
return DropzoneInput;
})();
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/environments/components/environment.js.es6 b/app/assets/javascripts/environments/components/environment.js.es6
index 0cbf952ea5c..5869323d1e2 100644
--- a/app/assets/javascripts/environments/components/environment.js.es6
+++ b/app/assets/javascripts/environments/components/environment.js.es6
@@ -1,13 +1,14 @@
/* eslint-disable no-param-reassign, no-new */
/* global Flash */
-const Vue = require('vue');
-Vue.use(require('vue-resource'));
+const Vue = window.Vue = require('vue');
+window.Vue.use(require('vue-resource'));
const EnvironmentsService = require('../services/environments_service');
const EnvironmentTable = require('./environments_table');
const EnvironmentsStore = require('../stores/environments_store');
require('../../vue_shared/components/table_pagination');
require('../../lib/utils/common_utils');
+require('../../vue_shared/vue_resource_interceptor');
module.exports = Vue.component('environment-component', {
@@ -144,7 +145,7 @@ module.exports = Vue.component('environment-component', {
</div>
</div>
- <div class="environments-container">
+ <div class="content-list environments-container">
<div class="environments-list-loading text-center" v-if="isLoading">
<i class="fa fa-spinner fa-spin"></i>
</div>
@@ -180,12 +181,12 @@ module.exports = Vue.component('environment-component', {
:terminal-icon-svg="terminalIconSvg"
:commit-icon-svg="commitIconSvg">
</environment-table>
-
- <table-pagination v-if="state.paginationInformation && state.paginationInformation.totalPages > 1"
- :change="changePage"
- :pageInfo="state.paginationInformation">
- </table-pagination>
</div>
+
+ <table-pagination v-if="state.paginationInformation && state.paginationInformation.totalPages > 1"
+ :change="changePage"
+ :pageInfo="state.paginationInformation">
+ </table-pagination>
</div>
</div>
`,
diff --git a/app/assets/javascripts/environments/components/environment_actions.js.es6 b/app/assets/javascripts/environments/components/environment_actions.js.es6
index c5a714d9673..978d4dd8b6b 100644
--- a/app/assets/javascripts/environments/components/environment_actions.js.es6
+++ b/app/assets/javascripts/environments/components/environment_actions.js.es6
@@ -15,29 +15,29 @@ module.exports = Vue.component('actions-component', {
},
template: `
- <div class="inline">
- <div class="dropdown">
- <a class="dropdown-new btn btn-default" data-toggle="dropdown">
+ <div class="btn-group" role="group">
+ <button class="dropdown btn btn-default dropdown-new" data-toggle="dropdown">
+ <span>
<span class="js-dropdown-play-icon-container" v-html="playIconSvg"></span>
<i class="fa fa-caret-down"></i>
- </a>
+ </span>
- <ul class="dropdown-menu dropdown-menu-align-right">
- <li v-for="action in actions">
- <a :href="action.play_path"
- data-method="post"
- rel="nofollow"
- class="js-manual-action-link">
+ <ul class="dropdown-menu dropdown-menu-align-right">
+ <li v-for="action in actions">
+ <a :href="action.play_path"
+ data-method="post"
+ rel="nofollow"
+ class="js-manual-action-link">
- <span class="js-action-play-icon-container" v-html="playIconSvg"></span>
+ <span class="js-action-play-icon-container" v-html="playIconSvg"></span>
- <span>
- {{action.name}}
- </span>
- </a>
- </li>
- </ul>
- </div>
- </div>
+ <span>
+ {{action.name}}
+ </span>
+ </a>
+ </li>
+ </ul>
+ </button>
+ </div>
`,
});
diff --git a/app/assets/javascripts/environments/components/environment_item.js.es6 b/app/assets/javascripts/environments/components/environment_item.js.es6
index 24fd58a301a..3f782742c56 100644
--- a/app/assets/javascripts/environments/components/environment_item.js.es6
+++ b/app/assets/javascripts/environments/components/environment_item.js.es6
@@ -503,45 +503,30 @@ module.exports = Vue.component('environment-item', {
</span>
</td>
- <td class="hidden-xs">
- <div v-if="!model.isFolder">
- <div v-if="hasManualActions && canCreateDeployment"
- class="inline js-manual-actions-container">
- <actions-component
- :play-icon-svg="playIconSvg"
- :actions="manualActions">
- </actions-component>
- </div>
-
- <div v-if="externalURL && canReadEnvironment"
- class="inline js-external-url-container">
- <external-url-component
- :external-url="externalURL">
- </external-url-component>
- </div>
-
- <div v-if="hasStopAction && canCreateDeployment"
- class="inline js-stop-component-container">
- <stop-component
- :stop-url="model.stop_path">
- </stop-component>
- </div>
-
- <div v-if="model && model.terminal_path"
- class="inline js-terminal-button-container">
- <terminal-button-component
- :terminal-icon-svg="terminalIconSvg"
- :terminal-path="model.terminal_path">
- </terminal-button-component>
- </div>
-
- <div v-if="canRetry && canCreateDeployment"
- class="inline js-rollback-component-container">
- <rollback-component
- :is-last-deployment="isLastDeployment"
- :retry-url="retryUrl">
- </rollback-component>
- </div>
+ <td class="environments-actions">
+ <div v-if="!model.isFolder" class="btn-group pull-right" role="group">
+ <actions-component v-if="hasManualActions && canCreateDeployment"
+ :play-icon-svg="playIconSvg"
+ :actions="manualActions">
+ </actions-component>
+
+ <external-url-component v-if="externalURL && canReadEnvironment"
+ :external-url="externalURL">
+ </external-url-component>
+
+ <stop-component v-if="hasStopAction && canCreateDeployment"
+ :stop-url="model.stop_path">
+ </stop-component>
+
+ <terminal-button-component v-if="model && model.terminal_path"
+ :terminal-icon-svg="terminalIconSvg"
+ :terminal-path="model.terminal_path">
+ </terminal-button-component>
+
+ <rollback-component v-if="canRetry && canCreateDeployment"
+ :is-last-deployment="isLastDeployment"
+ :retry-url="retryUrl">
+ </rollback-component>
</div>
</td>
</tr>
diff --git a/app/assets/javascripts/environments/components/environments_table.js.es6 b/app/assets/javascripts/environments/components/environments_table.js.es6
index fd35d77fd3d..33ebca19f5d 100644
--- a/app/assets/javascripts/environments/components/environments_table.js.es6
+++ b/app/assets/javascripts/environments/components/environments_table.js.es6
@@ -46,7 +46,7 @@ module.exports = Vue.component('environment-table-component', {
},
template: `
- <table class="table ci-table environments">
+ <table class="table ci-table">
<thead>
<tr>
<th class="environments-name">Environment</th>
@@ -54,7 +54,7 @@ module.exports = Vue.component('environment-table-component', {
<th class="environments-build">Job</th>
<th class="environments-commit">Commit</th>
<th class="environments-date">Updated</th>
- <th class="hidden-xs environments-actions"></th>
+ <th class="environments-actions"></th>
</tr>
</thead>
<tbody>
diff --git a/app/assets/javascripts/environments/environments_bundle.js.es6 b/app/assets/javascripts/environments/environments_bundle.js.es6
index 867eba1d384..7bbba91bc10 100644
--- a/app/assets/javascripts/environments/environments_bundle.js.es6
+++ b/app/assets/javascripts/environments/environments_bundle.js.es6
@@ -1,5 +1,4 @@
const EnvironmentsComponent = require('./components/environment');
-require('../vue_shared/vue_resource_interceptor');
$(() => {
window.gl = window.gl || {};
diff --git a/app/assets/javascripts/environments/folder/environments_folder_bundle.js.es6 b/app/assets/javascripts/environments/folder/environments_folder_bundle.js.es6
index 29f704c1a37..d2ca465351a 100644
--- a/app/assets/javascripts/environments/folder/environments_folder_bundle.js.es6
+++ b/app/assets/javascripts/environments/folder/environments_folder_bundle.js.es6
@@ -1,5 +1,4 @@
const EnvironmentsFolderComponent = require('./environments_folder_view');
-require('../../vue_shared/vue_resource_interceptor');
$(() => {
window.gl = window.gl || {};
diff --git a/app/assets/javascripts/environments/folder/environments_folder_view.js.es6 b/app/assets/javascripts/environments/folder/environments_folder_view.js.es6
index 0b1204559da..53d52965758 100644
--- a/app/assets/javascripts/environments/folder/environments_folder_view.js.es6
+++ b/app/assets/javascripts/environments/folder/environments_folder_view.js.es6
@@ -1,13 +1,14 @@
/* eslint-disable no-param-reassign, no-new */
/* global Flash */
-const Vue = require('vue');
-Vue.use(require('vue-resource'));
+const Vue = window.Vue = require('vue');
+window.Vue.use(require('vue-resource'));
const EnvironmentsService = require('../services/environments_service');
const EnvironmentTable = require('../components/environments_table');
const EnvironmentsStore = require('../stores/environments_store');
require('../../vue_shared/components/table_pagination');
require('../../lib/utils/common_utils');
+require('../../vue_shared/vue_resource_interceptor');
module.exports = Vue.component('environment-folder-view', {
diff --git a/app/assets/javascripts/extensions/jquery.js b/app/assets/javascripts/extensions/jquery.js
index d3b58b2707a..1a489b859e8 100644
--- a/app/assets/javascripts/extensions/jquery.js
+++ b/app/assets/javascripts/extensions/jquery.js
@@ -13,4 +13,4 @@
return $(this).removeAttr('disabled').removeClass('disabled');
}
});
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/files_comment_button.js b/app/assets/javascripts/files_comment_button.js
index 895a872568d..6d86888dcb8 100644
--- a/app/assets/javascripts/files_comment_button.js
+++ b/app/assets/javascripts/files_comment_button.js
@@ -1,16 +1,16 @@
/* 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 */
+/* global notes */
(function() {
+ let $commentButtonTemplate;
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;
+ var COMMENT_BUTTON_CLASS, EMPTY_CELL_CLASS, LINE_COLUMN_CLASSES, LINE_CONTENT_CLASS, LINE_HOLDER_CLASS, LINE_NUMBER_CLASS, OLD_LINE_CLASS, TEXT_FILE_SELECTOR, UNFOLDABLE_LINE_CLASS;
COMMENT_BUTTON_CLASS = '.add-diff-note';
- COMMENT_BUTTON_TEMPLATE = _.template('<button name="button" type="submit" class="btn <%- COMMENT_BUTTON_CLASS %> js-add-diff-note-button" title="Add a comment to this line"><i class="fa fa-comment-o"></i></button>');
-
LINE_HOLDER_CLASS = '.line_holder';
LINE_NUMBER_CLASS = 'diff-line-num';
@@ -27,26 +27,29 @@
TEXT_FILE_SELECTOR = '.text-file';
- DEBOUNCE_TIMEOUT_DURATION = 100;
-
function FilesCommentButton(filesContainerElement) {
- var debounce;
- this.filesContainerElement = filesContainerElement;
- this.destroy = bind(this.destroy, this);
this.render = bind(this.render, this);
- this.VIEW_TYPE = $('input#view[type=hidden]').val();
- debounce = _.debounce(this.render, DEBOUNCE_TIMEOUT_DURATION);
- $(this.filesContainerElement).off('mouseover', LINE_COLUMN_CLASSES).off('mouseleave', LINE_COLUMN_CLASSES).on('mouseover', LINE_COLUMN_CLASSES, debounce).on('mouseleave', LINE_COLUMN_CLASSES, this.destroy);
+ this.hideButton = bind(this.hideButton, this);
+ this.isParallelView = notes.isParallelView();
+ filesContainerElement.on('mouseover', LINE_COLUMN_CLASSES, this.render)
+ .on('mouseleave', LINE_COLUMN_CLASSES, this.hideButton);
}
FilesCommentButton.prototype.render = function(e) {
- var $currentTarget, buttonParentElement, lineContentElement, textFileElement;
+ var $currentTarget, buttonParentElement, lineContentElement, textFileElement, $button;
$currentTarget = $(e.currentTarget);
-
- buttonParentElement = this.getButtonParent($currentTarget);
- if (!this.validateButtonParent(buttonParentElement)) return;
lineContentElement = this.getLineContent($currentTarget);
- if (!this.validateLineContent(lineContentElement)) return;
+ buttonParentElement = this.getButtonParent($currentTarget);
+
+ if (!this.validateButtonParent(buttonParentElement) || !this.validateLineContent(lineContentElement)) return;
+
+ $button = $(COMMENT_BUTTON_CLASS, buttonParentElement);
+ buttonParentElement.addClass('is-over')
+ .nextUntil(`.${LINE_CONTENT_CLASS}`).addClass('is-over');
+
+ if ($button.length) {
+ return;
+ }
textFileElement = this.getTextFileElement($currentTarget);
buttonParentElement.append(this.buildButton({
@@ -61,19 +64,16 @@
}));
};
- FilesCommentButton.prototype.destroy = function(e) {
- if (this.isMovingToSameType(e)) {
- return;
- }
- $(COMMENT_BUTTON_CLASS, this.getButtonParent($(e.currentTarget))).remove();
+ FilesCommentButton.prototype.hideButton = function(e) {
+ var $currentTarget = $(e.currentTarget);
+ var buttonParentElement = this.getButtonParent($currentTarget);
+
+ buttonParentElement.removeClass('is-over')
+ .nextUntil(`.${LINE_CONTENT_CLASS}`).removeClass('is-over');
};
FilesCommentButton.prototype.buildButton = function(buttonAttributes) {
- var initializedButtonTemplate;
- initializedButtonTemplate = COMMENT_BUTTON_TEMPLATE({
- COMMENT_BUTTON_CLASS: COMMENT_BUTTON_CLASS.substr(1)
- });
- return $(initializedButtonTemplate).attr({
+ return $commentButtonTemplate.clone().attr({
'data-noteable-type': buttonAttributes.noteableType,
'data-noteable-id': buttonAttributes.noteableID,
'data-commit-id': buttonAttributes.commitID,
@@ -86,14 +86,14 @@
};
FilesCommentButton.prototype.getTextFileElement = function(hoveredElement) {
- return $(hoveredElement.closest(TEXT_FILE_SELECTOR));
+ return hoveredElement.closest(TEXT_FILE_SELECTOR);
};
FilesCommentButton.prototype.getLineContent = function(hoveredElement) {
if (hoveredElement.hasClass(LINE_CONTENT_CLASS)) {
return hoveredElement;
}
- if (this.VIEW_TYPE === 'inline') {
+ if (!this.isParallelView) {
return $(hoveredElement).closest(LINE_HOLDER_CLASS).find("." + LINE_CONTENT_CLASS);
} else {
return $(hoveredElement).next("." + LINE_CONTENT_CLASS);
@@ -101,7 +101,7 @@
};
FilesCommentButton.prototype.getButtonParent = function(hoveredElement) {
- if (this.VIEW_TYPE === 'inline') {
+ if (!this.isParallelView) {
if (hoveredElement.hasClass(OLD_LINE_CLASS)) {
return hoveredElement;
}
@@ -114,17 +114,8 @@
}
};
- FilesCommentButton.prototype.isMovingToSameType = function(e) {
- var newButtonParent;
- newButtonParent = this.getButtonParent($(e.toElement));
- if (!newButtonParent) {
- return false;
- }
- return newButtonParent.is(this.getButtonParent($(e.currentTarget)));
- };
-
FilesCommentButton.prototype.validateButtonParent = function(buttonParentElement) {
- return !buttonParentElement.hasClass(EMPTY_CELL_CLASS) && !buttonParentElement.hasClass(UNFOLDABLE_LINE_CLASS) && $(COMMENT_BUTTON_CLASS, buttonParentElement).length === 0;
+ return !buttonParentElement.hasClass(EMPTY_CELL_CLASS) && !buttonParentElement.hasClass(UNFOLDABLE_LINE_CLASS);
};
FilesCommentButton.prototype.validateLineContent = function(lineContentElement) {
@@ -135,6 +126,8 @@
})();
$.fn.filesCommentButton = function() {
+ $commentButtonTemplate = $('<button name="button" type="submit" class="add-diff-note js-add-diff-note-button" title="Add a comment to this line"><i class="fa fa-comment-o"></i></button>');
+
if (!(this && (this.parent().data('can-create-note') != null))) {
return;
}
@@ -144,4 +137,4 @@
}
});
};
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6
index 572c221929a..9e92d544bef 100644
--- a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6
+++ b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6
@@ -37,23 +37,18 @@ require('./filtered_search_dropdown');
}
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;',
- }];
+ const dropdownData = [];
+
+ [].forEach.call(this.input.parentElement.querySelectorAll('.dropdown-menu'), (dropdownMenu) => {
+ const { icon, hint, tag } = dropdownMenu.dataset;
+ if (icon && hint && tag) {
+ dropdownData.push({
+ icon: `fa-${icon}`,
+ hint,
+ tag: `&lt;${tag}&gt;`,
+ });
+ }
+ });
this.droplab.changeHookList(this.hookId, this.dropdown, [droplabFilter], this.config);
this.droplab.setData(this.hookId, dropdownData);
diff --git a/app/assets/javascripts/filtered_search/filtered_search_bundle.js b/app/assets/javascripts/filtered_search/filtered_search_bundle.js
index 392f1835966..faaba994f46 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_bundle.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_bundle.js
@@ -1,3 +1,9 @@
-function requireAll(context) { return context.keys().map(context); }
-
-requireAll(require.context('./', true, /^\.\/(?!filtered_search_bundle).*\.(js|es6)$/));
+require('./dropdown_hint');
+require('./dropdown_non_user');
+require('./dropdown_user');
+require('./dropdown_utils');
+require('./filtered_search_dropdown_manager');
+require('./filtered_search_dropdown');
+require('./filtered_search_manager');
+require('./filtered_search_token_keys');
+require('./filtered_search_tokenizer');
diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6
index e8c2df03a46..dd565da507e 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6
+++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6
@@ -48,12 +48,17 @@
}
setOffset(offset = 0) {
- this.dropdown.style.left = `${offset}px`;
+ if (window.innerWidth > 480) {
+ this.dropdown.style.left = `${offset}px`;
+ } else {
+ this.dropdown.style.left = '0px';
+ }
}
renderContent(forceShowList = false) {
- if (forceShowList && this.getCurrentHook().list.hidden) {
- this.getCurrentHook().list.show();
+ const currentHook = this.getCurrentHook();
+ if (forceShowList && currentHook && currentHook.list.hidden) {
+ currentHook.list.show();
}
}
@@ -92,18 +97,24 @@
}
hideDropdown() {
- this.getCurrentHook().list.hide();
+ const currentHook = this.getCurrentHook();
+ if (currentHook) {
+ currentHook.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);
+
+ if (hook) {
+ const data = hook.list.data;
+ const results = data.map((o) => {
+ const updated = o;
+ updated.droplab_hidden = false;
+ return updated;
+ });
+ hook.list.render(results);
+ }
}
}
diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6
index 8ce4cf4fc36..cecd3518ce3 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6
+++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6
@@ -2,10 +2,12 @@
(() => {
class FilteredSearchDropdownManager {
- constructor(baseEndpoint = '') {
+ constructor(baseEndpoint = '', page) {
this.baseEndpoint = baseEndpoint.replace(/\/$/, '');
this.tokenizer = gl.FilteredSearchTokenizer;
+ this.filteredSearchTokenKeys = gl.FilteredSearchTokenKeys;
this.filteredSearchInput = document.querySelector('.filtered-search');
+ this.page = page;
this.setupMapping();
@@ -150,7 +152,7 @@
this.droplab = new DropLab();
}
- const match = gl.FilteredSearchTokenKeys.searchByKey(dropdownName.toLowerCase());
+ const match = this.filteredSearchTokenKeys.searchByKey(dropdownName.toLowerCase());
const shouldOpenFilterDropdown = match && this.currentDropdown !== match.key
&& this.mapping[match.key];
const shouldOpenHintDropdown = !match && this.currentDropdown !== 'hint';
diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6
index ffc7d29e4c5..bbafead0305 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6
+++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6
@@ -1,12 +1,13 @@
(() => {
class FilteredSearchManager {
- constructor() {
+ constructor(page) {
this.filteredSearchInput = document.querySelector('.filtered-search');
this.clearSearchButton = document.querySelector('.clear-search');
+ this.filteredSearchTokenKeys = gl.FilteredSearchTokenKeys;
if (this.filteredSearchInput) {
this.tokenizer = gl.FilteredSearchTokenizer;
- this.dropdownManager = new gl.FilteredSearchDropdownManager(this.filteredSearchInput.getAttribute('data-base-endpoint') || '');
+ this.dropdownManager = new gl.FilteredSearchDropdownManager(this.filteredSearchInput.getAttribute('data-base-endpoint') || '', page);
this.bindEvents();
this.loadSearchParamsFromURL();
@@ -117,8 +118,8 @@
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);
+ // Check if it matches edge conditions listed in this.filteredSearchTokenKeys
+ const condition = this.filteredSearchTokenKeys.searchByConditionUrl(p);
if (condition) {
inputValues.push(`${condition.tokenKey}:${condition.value}`);
@@ -126,7 +127,7 @@
// 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);
+ const match = this.filteredSearchTokenKeys.searchByKeyParam(keyParam);
if (match) {
const indexOf = keyParam.indexOf('_');
@@ -171,9 +172,9 @@
paths.push(`state=${currentState}`);
tokens.forEach((token) => {
- const condition = gl.FilteredSearchTokenKeys
+ const condition = this.filteredSearchTokenKeys
.searchByConditionKeyValue(token.key, token.value.toLowerCase());
- const { param } = gl.FilteredSearchTokenKeys.searchByKey(token.key);
+ const { param } = this.filteredSearchTokenKeys.searchByKey(token.key) || {};
const keyParam = param ? `${token.key}_${param}` : token.key;
let tokenPath = '';
diff --git a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js.es6
index cf53845a48b..9bf1b1ced88 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js.es6
+++ b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js.es6
@@ -1,9 +1,12 @@
+require('./filtered_search_token_keys');
+
(() => {
class FilteredSearchTokenizer {
static processTokens(input) {
+ const allowedKeys = gl.FilteredSearchTokenKeys.get().map(i => i.key);
// 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 tokenRegex = new RegExp(`(${allowedKeys.join('|')}):([~%@]?)(?:('[^']*'{0,1})|("[^"]*"{0,1})|(\\S+))`, 'g');
const tokens = [];
let lastToken = null;
const searchToken = input.replace(tokenRegex, (match, key, symbol, v1, v2, v3) => {
diff --git a/app/assets/javascripts/flash.js b/app/assets/javascripts/flash.js
index 249fe23d4cb..730104b89f9 100644
--- a/app/assets/javascripts/flash.js
+++ b/app/assets/javascripts/flash.js
@@ -39,4 +39,4 @@
return Flash;
})();
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/gfm_auto_complete.js.es6 b/app/assets/javascripts/gfm_auto_complete.js.es6
index 7f1f2a5d278..60d6658dc16 100644
--- a/app/assets/javascripts/gfm_auto_complete.js.es6
+++ b/app/assets/javascripts/gfm_auto_complete.js.es6
@@ -83,12 +83,12 @@
_a = decodeURI("%C3%80");
_y = decodeURI("%C3%BF");
- regexp = new RegExp("^(?:\\B|[^a-zA-Z0-9_" + atSymbolsWithoutBar + "]|\\s)" + flag + "(?![" + atSymbolsWithBar + "])(([A-Za-z" + _a + "-" + _y + "0-9_\'\.\+\-]|[^\\x00-\\x7a])*)$", '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[1] || match[1] === "") ? match[1] : match[2];
+ return match[1];
} else {
return null;
}
@@ -103,6 +103,9 @@
this.input.each((i, input) => {
const $input = $(input);
$input.off('focus.setupAtWho').on('focus.setupAtWho', this.setupAtWho.bind(this, $input));
+ // This triggers at.js again
+ // Needed for slash commands with suffixes (ex: /label ~)
+ $input.on('inserted-commands.atwho', $input.trigger.bind($input, 'keyup'));
});
},
setupAtWho: function($input) {
@@ -377,4 +380,4 @@
(dataToInspect === loadingState || dataToInspect.name === loadingState);
}
};
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js
index 0d618caf350..9e6ed06054b 100644
--- a/app/assets/javascripts/gl_dropdown.js
+++ b/app/assets/javascripts/gl_dropdown.js
@@ -47,9 +47,10 @@
}
// Only filter asynchronously only if option remote is set
if (this.options.remote) {
- $inputContainer.parent().addClass('is-loading');
clearTimeout(timeout);
return timeout = setTimeout(function() {
+ $inputContainer.parent().addClass('is-loading');
+
return this.options.query(this.input.val(), function(data) {
$inputContainer.parent().removeClass('is-loading');
return this.options.callback(data);
@@ -62,7 +63,7 @@
}
GitLabDropdownFilter.prototype.shouldBlur = function(keyCode) {
- return BLUR_KEYCODES.indexOf(keyCode) >= 0;
+ return BLUR_KEYCODES.indexOf(keyCode) !== -1;
};
GitLabDropdownFilter.prototype.filter = function(search_text) {
@@ -604,7 +605,7 @@
var occurrences;
occurrences = fuzzaldrinPlus.match(text, term);
return text.split('').map(function(character, i) {
- if (indexOf.call(occurrences, i) >= 0) {
+ if (indexOf.call(occurrences, i) !== -1) {
return "<b>" + character + "</b>";
} else {
return character;
@@ -747,7 +748,7 @@
return function(e) {
var $listItems, PREV_INDEX, currentKeyCode;
currentKeyCode = e.which;
- if (ARROW_KEY_CODES.indexOf(currentKeyCode) >= 0) {
+ if (ARROW_KEY_CODES.indexOf(currentKeyCode) !== -1) {
e.preventDefault();
e.stopImmediatePropagation();
PREV_INDEX = currentIndex;
@@ -846,4 +847,4 @@
}
});
};
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/graphs/graphs_bundle.js b/app/assets/javascripts/graphs/graphs_bundle.js
index 4f7777aa5bc..ea5afbd9d29 100644
--- a/app/assets/javascripts/graphs/graphs_bundle.js
+++ b/app/assets/javascripts/graphs/graphs_bundle.js
@@ -1,3 +1,4 @@
-// require everything else in this directory
-function requireAll(context) { return context.keys().map(context); }
-requireAll(require.context('.', false, /^\.\/(?!graphs_bundle).*\.(js|es6)$/));
+import ContributorsStatGraph from './stat_graph_contributors';
+
+// export to global scope
+window.ContributorsStatGraph = ContributorsStatGraph;
diff --git a/app/assets/javascripts/graphs/stat_graph.js b/app/assets/javascripts/graphs/stat_graph.js
deleted file mode 100644
index 2e6da5750de..00000000000
--- a/app/assets/javascripts/graphs/stat_graph.js
+++ /dev/null
@@ -1,18 +0,0 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-return-assign, max-len */
-(function() {
- this.StatGraph = (function() {
- function StatGraph() {}
-
- StatGraph.log = {};
-
- StatGraph.get_log = function() {
- return this.log;
- };
-
- StatGraph.set_log = function(data) {
- return this.log = data;
- };
-
- return StatGraph;
- })();
-}).call(this);
diff --git a/app/assets/javascripts/graphs/stat_graph_contributors.js b/app/assets/javascripts/graphs/stat_graph_contributors.js
index d06a1a5dae4..c6be4c9e8fe 100644
--- a/app/assets/javascripts/graphs/stat_graph_contributors.js
+++ b/app/assets/javascripts/graphs/stat_graph_contributors.js
@@ -1,116 +1,111 @@
-/* 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 */
+/* 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, no-shadow */
-window.d3 = require('d3');
+import d3 from 'd3';
+import { ContributorsGraph, ContributorsAuthorGraph, ContributorsMasterGraph } from './stat_graph_contributors_graph';
+import ContributorsStatGraphUtil from './stat_graph_contributors_util';
-(function() {
- this.ContributorsStatGraph = (function() {
- function ContributorsStatGraph() {}
+export default (function() {
+ function ContributorsStatGraph() {}
- ContributorsStatGraph.prototype.init = function(log) {
- var author_commits, total_commits;
- this.parsed_log = ContributorsStatGraphUtil.parse_log(log);
- this.set_current_field("commits");
- total_commits = ContributorsStatGraphUtil.get_total_data(this.parsed_log, this.field);
- author_commits = ContributorsStatGraphUtil.get_author_data(this.parsed_log, this.field);
- this.add_master_graph(total_commits);
- this.add_authors_graph(author_commits);
- return this.change_date_header();
- };
+ ContributorsStatGraph.prototype.init = function(log) {
+ var author_commits, total_commits;
+ this.parsed_log = ContributorsStatGraphUtil.parse_log(log);
+ this.set_current_field("commits");
+ total_commits = ContributorsStatGraphUtil.get_total_data(this.parsed_log, this.field);
+ author_commits = ContributorsStatGraphUtil.get_author_data(this.parsed_log, this.field);
+ this.add_master_graph(total_commits);
+ this.add_authors_graph(author_commits);
+ return this.change_date_header();
+ };
- ContributorsStatGraph.prototype.add_master_graph = function(total_data) {
- this.master_graph = new ContributorsMasterGraph(total_data);
- return this.master_graph.draw();
- };
+ ContributorsStatGraph.prototype.add_master_graph = function(total_data) {
+ this.master_graph = new ContributorsMasterGraph(total_data);
+ return this.master_graph.draw();
+ };
- ContributorsStatGraph.prototype.add_authors_graph = function(author_data) {
- var limited_author_data;
- this.authors = [];
- limited_author_data = author_data.slice(0, 100);
- return _.each(limited_author_data, (function(_this) {
- return function(d) {
- var author_graph, author_header;
- author_header = _this.create_author_header(d);
- $(".contributors-list").append(author_header);
- _this.authors[d.author_name] = author_graph = new ContributorsAuthorGraph(d.dates);
- return author_graph.draw();
- };
- })(this));
- };
+ ContributorsStatGraph.prototype.add_authors_graph = function(author_data) {
+ var limited_author_data;
+ this.authors = [];
+ limited_author_data = author_data.slice(0, 100);
+ return _.each(limited_author_data, (function(_this) {
+ return function(d) {
+ var author_graph, author_header;
+ author_header = _this.create_author_header(d);
+ $(".contributors-list").append(author_header);
+ _this.authors[d.author_name] = author_graph = new ContributorsAuthorGraph(d.dates);
+ return author_graph.draw();
+ };
+ })(this));
+ };
- ContributorsStatGraph.prototype.format_author_commit_info = function(author) {
- var commits;
- commits = $('<span/>', {
- "class": 'graph-author-commits-count'
- });
- commits.text(author.commits + " commits");
- return $('<span/>').append(commits);
- };
+ ContributorsStatGraph.prototype.format_author_commit_info = function(author) {
+ var commits;
+ commits = $('<span/>', {
+ "class": 'graph-author-commits-count'
+ });
+ commits.text(author.commits + " commits");
+ return $('<span/>').append(commits);
+ };
- ContributorsStatGraph.prototype.create_author_header = function(author) {
- var author_commit_info, author_commit_info_span, author_email, author_name, list_item;
- list_item = $('<li/>', {
- "class": 'person',
- style: 'display: block;'
- });
- author_name = $('<h4>' + author.author_name + '</h4>');
- author_email = $('<p class="graph-author-email">' + author.author_email + '</p>');
- author_commit_info_span = $('<span/>', {
- "class": 'commits'
- });
- author_commit_info = this.format_author_commit_info(author);
- author_commit_info_span.html(author_commit_info);
- list_item.append(author_name);
- list_item.append(author_email);
- list_item.append(author_commit_info_span);
- return list_item;
- };
+ ContributorsStatGraph.prototype.create_author_header = function(author) {
+ var author_commit_info, author_commit_info_span, author_email, author_name, list_item;
+ list_item = $('<li/>', {
+ "class": 'person',
+ style: 'display: block;'
+ });
+ author_name = $('<h4>' + author.author_name + '</h4>');
+ author_email = $('<p class="graph-author-email">' + author.author_email + '</p>');
+ author_commit_info_span = $('<span/>', {
+ "class": 'commits'
+ });
+ author_commit_info = this.format_author_commit_info(author);
+ author_commit_info_span.html(author_commit_info);
+ list_item.append(author_name);
+ list_item.append(author_email);
+ list_item.append(author_commit_info_span);
+ return list_item;
+ };
- ContributorsStatGraph.prototype.redraw_master = function() {
- var total_data;
- total_data = ContributorsStatGraphUtil.get_total_data(this.parsed_log, this.field);
- this.master_graph.set_data(total_data);
- return this.master_graph.redraw();
- };
+ ContributorsStatGraph.prototype.redraw_master = function() {
+ var total_data;
+ total_data = ContributorsStatGraphUtil.get_total_data(this.parsed_log, this.field);
+ this.master_graph.set_data(total_data);
+ return this.master_graph.redraw();
+ };
- ContributorsStatGraph.prototype.redraw_authors = function() {
- var author_commits, x_domain;
- $("ol").html("");
- x_domain = ContributorsGraph.prototype.x_domain;
- author_commits = ContributorsStatGraphUtil.get_author_data(this.parsed_log, this.field, x_domain);
- return _.each(author_commits, (function(_this) {
- return function(d) {
- _this.redraw_author_commit_info(d);
- $(_this.authors[d.author_name].list_item).appendTo("ol");
- _this.authors[d.author_name].set_data(d.dates);
- return _this.authors[d.author_name].redraw();
- };
- })(this));
- };
+ ContributorsStatGraph.prototype.redraw_authors = function() {
+ var author_commits, x_domain;
+ $("ol").html("");
+ x_domain = ContributorsGraph.prototype.x_domain;
+ author_commits = ContributorsStatGraphUtil.get_author_data(this.parsed_log, this.field, x_domain);
+ return _.each(author_commits, (function(_this) {
+ return function(d) {
+ _this.redraw_author_commit_info(d);
+ $(_this.authors[d.author_name].list_item).appendTo("ol");
+ _this.authors[d.author_name].set_data(d.dates);
+ return _this.authors[d.author_name].redraw();
+ };
+ })(this));
+ };
- ContributorsStatGraph.prototype.set_current_field = function(field) {
- return this.field = field;
- };
+ ContributorsStatGraph.prototype.set_current_field = function(field) {
+ return this.field = field;
+ };
- ContributorsStatGraph.prototype.change_date_header = function() {
- var print, print_date_format, x_domain;
- x_domain = ContributorsGraph.prototype.x_domain;
- print_date_format = d3.time.format("%B %e %Y");
- print = print_date_format(x_domain[0]) + " - " + print_date_format(x_domain[1]);
- return $("#date_header").text(print);
- };
+ ContributorsStatGraph.prototype.change_date_header = function() {
+ var print, print_date_format, x_domain;
+ x_domain = ContributorsGraph.prototype.x_domain;
+ print_date_format = d3.time.format("%B %e %Y");
+ print = print_date_format(x_domain[0]) + " - " + print_date_format(x_domain[1]);
+ return $("#date_header").text(print);
+ };
- ContributorsStatGraph.prototype.redraw_author_commit_info = function(author) {
- var author_commit_info, author_list_item;
- author_list_item = $(this.authors[author.author_name].list_item);
- author_commit_info = this.format_author_commit_info(author);
- return author_list_item.find("span").html(author_commit_info);
- };
+ ContributorsStatGraph.prototype.redraw_author_commit_info = function(author) {
+ var author_commit_info, author_list_item;
+ author_list_item = $(this.authors[author.author_name].list_item);
+ author_commit_info = this.format_author_commit_info(author);
+ return author_list_item.find("span").html(author_commit_info);
+ };
- return ContributorsStatGraph;
- })();
-}).call(this);
+ return ContributorsStatGraph;
+})();
diff --git a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js
index 241249fae63..521bc77db66 100644
--- a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js
+++ b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js
@@ -1,276 +1,272 @@
-/* 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 */
-
-window.d3 = require('d3');
-
-(function() {
- var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; },
- extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
- hasProp = {}.hasOwnProperty;
-
- this.ContributorsGraph = (function() {
- function ContributorsGraph() {}
-
- ContributorsGraph.prototype.MARGIN = {
- top: 20,
- right: 20,
- bottom: 30,
- left: 50
- };
-
- ContributorsGraph.prototype.x_domain = null;
-
- ContributorsGraph.prototype.y_domain = null;
-
- ContributorsGraph.prototype.dates = [];
-
- ContributorsGraph.set_x_domain = function(data) {
- return ContributorsGraph.prototype.x_domain = data;
- };
-
- ContributorsGraph.set_y_domain = function(data) {
- return ContributorsGraph.prototype.y_domain = [
- 0, d3.max(data, function(d) {
- return d.commits = d.commits || d.additions || d.deletions;
- })
- ];
- };
-
- ContributorsGraph.init_x_domain = function(data) {
- return ContributorsGraph.prototype.x_domain = d3.extent(data, function(d) {
- return d.date;
- });
- };
-
- ContributorsGraph.init_y_domain = function(data) {
- return ContributorsGraph.prototype.y_domain = [
- 0, d3.max(data, function(d) {
- return d.commits = d.commits || d.additions || d.deletions;
- })
- ];
- };
-
- ContributorsGraph.init_domain = function(data) {
- ContributorsGraph.init_x_domain(data);
- return ContributorsGraph.init_y_domain(data);
- };
-
- ContributorsGraph.set_dates = function(data) {
- return ContributorsGraph.prototype.dates = data;
- };
-
- ContributorsGraph.prototype.set_x_domain = function() {
- return this.x.domain(this.x_domain);
- };
-
- ContributorsGraph.prototype.set_y_domain = function() {
- return this.y.domain(this.y_domain);
- };
-
- ContributorsGraph.prototype.set_domain = function() {
- this.set_x_domain();
- return this.set_y_domain();
- };
-
- ContributorsGraph.prototype.create_scale = function(width, height) {
- this.x = d3.time.scale().range([0, width]).clamp(true);
- return this.y = d3.scale.linear().range([height, 0]).nice();
- };
-
- ContributorsGraph.prototype.draw_x_axis = function() {
- return this.svg.append("g").attr("class", "x axis").attr("transform", "translate(0, " + this.height + ")").call(this.x_axis);
- };
-
- ContributorsGraph.prototype.draw_y_axis = function() {
- return this.svg.append("g").attr("class", "y axis").call(this.y_axis);
- };
-
- ContributorsGraph.prototype.set_data = function(data) {
- return this.data = data;
- };
-
- return ContributorsGraph;
- })();
-
- this.ContributorsMasterGraph = (function(superClass) {
- extend(ContributorsMasterGraph, superClass);
-
- function ContributorsMasterGraph(data1) {
- this.data = data1;
- this.update_content = bind(this.update_content, this);
- this.width = $('.content').width() - 70;
- this.height = 200;
- this.x = null;
- this.y = null;
- this.x_axis = null;
- this.y_axis = null;
- this.area = null;
- this.svg = null;
- this.brush = null;
- this.x_max_domain = null;
+/* eslint-disable func-names, space-before-function-paren, 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, no-shadow */
+
+import d3 from 'd3';
+
+const bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
+const 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; };
+const hasProp = {}.hasOwnProperty;
+
+export const ContributorsGraph = (function() {
+ function ContributorsGraph() {}
+
+ ContributorsGraph.prototype.MARGIN = {
+ top: 20,
+ right: 20,
+ bottom: 30,
+ left: 50
+ };
+
+ ContributorsGraph.prototype.x_domain = null;
+
+ ContributorsGraph.prototype.y_domain = null;
+
+ ContributorsGraph.prototype.dates = [];
+
+ ContributorsGraph.set_x_domain = function(data) {
+ return ContributorsGraph.prototype.x_domain = data;
+ };
+
+ ContributorsGraph.set_y_domain = function(data) {
+ return ContributorsGraph.prototype.y_domain = [
+ 0, d3.max(data, function(d) {
+ return d.commits = d.commits || d.additions || d.deletions;
+ })
+ ];
+ };
+
+ ContributorsGraph.init_x_domain = function(data) {
+ return ContributorsGraph.prototype.x_domain = d3.extent(data, function(d) {
+ return d.date;
+ });
+ };
+
+ ContributorsGraph.init_y_domain = function(data) {
+ return ContributorsGraph.prototype.y_domain = [
+ 0, d3.max(data, function(d) {
+ return d.commits = d.commits || d.additions || d.deletions;
+ })
+ ];
+ };
+
+ ContributorsGraph.init_domain = function(data) {
+ ContributorsGraph.init_x_domain(data);
+ return ContributorsGraph.init_y_domain(data);
+ };
+
+ ContributorsGraph.set_dates = function(data) {
+ return ContributorsGraph.prototype.dates = data;
+ };
+
+ ContributorsGraph.prototype.set_x_domain = function() {
+ return this.x.domain(this.x_domain);
+ };
+
+ ContributorsGraph.prototype.set_y_domain = function() {
+ return this.y.domain(this.y_domain);
+ };
+
+ ContributorsGraph.prototype.set_domain = function() {
+ this.set_x_domain();
+ return this.set_y_domain();
+ };
+
+ ContributorsGraph.prototype.create_scale = function(width, height) {
+ this.x = d3.time.scale().range([0, width]).clamp(true);
+ return this.y = d3.scale.linear().range([height, 0]).nice();
+ };
+
+ ContributorsGraph.prototype.draw_x_axis = function() {
+ return this.svg.append("g").attr("class", "x axis").attr("transform", "translate(0, " + this.height + ")").call(this.x_axis);
+ };
+
+ ContributorsGraph.prototype.draw_y_axis = function() {
+ return this.svg.append("g").attr("class", "y axis").call(this.y_axis);
+ };
+
+ ContributorsGraph.prototype.set_data = function(data) {
+ return this.data = data;
+ };
+
+ return ContributorsGraph;
+})();
+
+export const ContributorsMasterGraph = (function(superClass) {
+ extend(ContributorsMasterGraph, superClass);
+
+ function ContributorsMasterGraph(data1) {
+ this.data = data1;
+ this.update_content = bind(this.update_content, this);
+ this.width = $('.content').width() - 70;
+ this.height = 200;
+ this.x = null;
+ this.y = null;
+ this.x_axis = null;
+ this.y_axis = null;
+ this.area = null;
+ this.svg = null;
+ this.brush = null;
+ this.x_max_domain = null;
+ }
+
+ ContributorsMasterGraph.prototype.process_dates = function(data) {
+ var dates;
+ dates = this.get_dates(data);
+ this.parse_dates(data);
+ return ContributorsGraph.set_dates(dates);
+ };
+
+ ContributorsMasterGraph.prototype.get_dates = function(data) {
+ return _.pluck(data, 'date');
+ };
+
+ ContributorsMasterGraph.prototype.parse_dates = function(data) {
+ var parseDate;
+ parseDate = d3.time.format("%Y-%m-%d").parse;
+ return data.forEach(function(d) {
+ return d.date = parseDate(d.date);
+ });
+ };
+
+ ContributorsMasterGraph.prototype.create_scale = function() {
+ return ContributorsMasterGraph.__super__.create_scale.call(this, this.width, this.height);
+ };
+
+ ContributorsMasterGraph.prototype.create_axes = function() {
+ this.x_axis = d3.svg.axis().scale(this.x).orient("bottom");
+ return this.y_axis = d3.svg.axis().scale(this.y).orient("left").ticks(5);
+ };
+
+ ContributorsMasterGraph.prototype.create_svg = function() {
+ return this.svg = d3.select("#contributors-master").append("svg").attr("width", this.width + this.MARGIN.left + this.MARGIN.right).attr("height", this.height + this.MARGIN.top + this.MARGIN.bottom).attr("class", "tint-box").append("g").attr("transform", "translate(" + this.MARGIN.left + "," + this.MARGIN.top + ")");
+ };
+
+ ContributorsMasterGraph.prototype.create_area = function(x, y) {
+ return this.area = d3.svg.area().x(function(d) {
+ return x(d.date);
+ }).y0(this.height).y1(function(d) {
+ d.commits = d.commits || d.additions || d.deletions;
+ return y(d.commits);
+ }).interpolate("basis");
+ };
+
+ ContributorsMasterGraph.prototype.create_brush = function() {
+ return this.brush = d3.svg.brush().x(this.x).on("brushend", this.update_content);
+ };
+
+ ContributorsMasterGraph.prototype.draw_path = function(data) {
+ return this.svg.append("path").datum(data).attr("class", "area").attr("d", this.area);
+ };
+
+ ContributorsMasterGraph.prototype.add_brush = function() {
+ return this.svg.append("g").attr("class", "selection").call(this.brush).selectAll("rect").attr("height", this.height);
+ };
+
+ ContributorsMasterGraph.prototype.update_content = function() {
+ ContributorsGraph.set_x_domain(this.brush.empty() ? this.x_max_domain : this.brush.extent());
+ return $("#brush_change").trigger('change');
+ };
+
+ ContributorsMasterGraph.prototype.draw = function() {
+ this.process_dates(this.data);
+ this.create_scale();
+ this.create_axes();
+ ContributorsGraph.init_domain(this.data);
+ this.x_max_domain = this.x_domain;
+ this.set_domain();
+ this.create_area(this.x, this.y);
+ this.create_svg();
+ this.create_brush();
+ this.draw_path(this.data);
+ this.draw_x_axis();
+ this.draw_y_axis();
+ return this.add_brush();
+ };
+
+ ContributorsMasterGraph.prototype.redraw = function() {
+ this.process_dates(this.data);
+ ContributorsGraph.set_y_domain(this.data);
+ this.set_y_domain();
+ this.svg.select("path").datum(this.data);
+ this.svg.select("path").attr("d", this.area);
+ return this.svg.select(".y.axis").call(this.y_axis);
+ };
+
+ return ContributorsMasterGraph;
+})(ContributorsGraph);
+
+export const ContributorsAuthorGraph = (function(superClass) {
+ extend(ContributorsAuthorGraph, superClass);
+
+ function ContributorsAuthorGraph(data1) {
+ this.data = data1;
+ // Don't split graph size in half for mobile devices.
+ if ($(window).width() < 768) {
+ this.width = $('.content').width() - 80;
+ } else {
+ this.width = ($('.content').width() / 2) - 100;
}
-
- ContributorsMasterGraph.prototype.process_dates = function(data) {
- var dates;
- dates = this.get_dates(data);
- this.parse_dates(data);
- return ContributorsGraph.set_dates(dates);
- };
-
- ContributorsMasterGraph.prototype.get_dates = function(data) {
- return _.pluck(data, 'date');
- };
-
- ContributorsMasterGraph.prototype.parse_dates = function(data) {
+ this.height = 200;
+ this.x = null;
+ this.y = null;
+ this.x_axis = null;
+ this.y_axis = null;
+ this.area = null;
+ this.svg = null;
+ this.list_item = null;
+ }
+
+ ContributorsAuthorGraph.prototype.create_scale = function() {
+ return ContributorsAuthorGraph.__super__.create_scale.call(this, this.width, this.height);
+ };
+
+ ContributorsAuthorGraph.prototype.create_axes = function() {
+ this.x_axis = d3.svg.axis().scale(this.x).orient("bottom").ticks(8);
+ return this.y_axis = d3.svg.axis().scale(this.y).orient("left").ticks(5);
+ };
+
+ ContributorsAuthorGraph.prototype.create_area = function(x, y) {
+ return this.area = d3.svg.area().x(function(d) {
var parseDate;
parseDate = d3.time.format("%Y-%m-%d").parse;
- return data.forEach(function(d) {
- return d.date = parseDate(d.date);
- });
- };
-
- ContributorsMasterGraph.prototype.create_scale = function() {
- return ContributorsMasterGraph.__super__.create_scale.call(this, this.width, this.height);
- };
-
- ContributorsMasterGraph.prototype.create_axes = function() {
- this.x_axis = d3.svg.axis().scale(this.x).orient("bottom");
- return this.y_axis = d3.svg.axis().scale(this.y).orient("left").ticks(5);
- };
-
- ContributorsMasterGraph.prototype.create_svg = function() {
- return this.svg = d3.select("#contributors-master").append("svg").attr("width", this.width + this.MARGIN.left + this.MARGIN.right).attr("height", this.height + this.MARGIN.top + this.MARGIN.bottom).attr("class", "tint-box").append("g").attr("transform", "translate(" + this.MARGIN.left + "," + this.MARGIN.top + ")");
- };
-
- ContributorsMasterGraph.prototype.create_area = function(x, y) {
- return this.area = d3.svg.area().x(function(d) {
- return x(d.date);
- }).y0(this.height).y1(function(d) {
- d.commits = d.commits || d.additions || d.deletions;
- return y(d.commits);
- }).interpolate("basis");
- };
-
- ContributorsMasterGraph.prototype.create_brush = function() {
- return this.brush = d3.svg.brush().x(this.x).on("brushend", this.update_content);
- };
-
- ContributorsMasterGraph.prototype.draw_path = function(data) {
- return this.svg.append("path").datum(data).attr("class", "area").attr("d", this.area);
- };
-
- ContributorsMasterGraph.prototype.add_brush = function() {
- return this.svg.append("g").attr("class", "selection").call(this.brush).selectAll("rect").attr("height", this.height);
- };
-
- ContributorsMasterGraph.prototype.update_content = function() {
- ContributorsGraph.set_x_domain(this.brush.empty() ? this.x_max_domain : this.brush.extent());
- return $("#brush_change").trigger('change');
- };
-
- ContributorsMasterGraph.prototype.draw = function() {
- this.process_dates(this.data);
- this.create_scale();
- this.create_axes();
- ContributorsGraph.init_domain(this.data);
- this.x_max_domain = this.x_domain;
- this.set_domain();
- this.create_area(this.x, this.y);
- this.create_svg();
- this.create_brush();
- this.draw_path(this.data);
- this.draw_x_axis();
- this.draw_y_axis();
- return this.add_brush();
- };
-
- ContributorsMasterGraph.prototype.redraw = function() {
- this.process_dates(this.data);
- ContributorsGraph.set_y_domain(this.data);
- this.set_y_domain();
- this.svg.select("path").datum(this.data);
- this.svg.select("path").attr("d", this.area);
- return this.svg.select(".y.axis").call(this.y_axis);
- };
-
- return ContributorsMasterGraph;
- })(ContributorsGraph);
-
- this.ContributorsAuthorGraph = (function(superClass) {
- extend(ContributorsAuthorGraph, superClass);
-
- function ContributorsAuthorGraph(data1) {
- this.data = data1;
- // Don't split graph size in half for mobile devices.
- if ($(window).width() < 768) {
- this.width = $('.content').width() - 80;
- } else {
- this.width = ($('.content').width() / 2) - 100;
- }
- this.height = 200;
- this.x = null;
- this.y = null;
- this.x_axis = null;
- this.y_axis = null;
- this.area = null;
- this.svg = null;
- this.list_item = null;
- }
-
- ContributorsAuthorGraph.prototype.create_scale = function() {
- return ContributorsAuthorGraph.__super__.create_scale.call(this, this.width, this.height);
- };
-
- ContributorsAuthorGraph.prototype.create_axes = function() {
- this.x_axis = d3.svg.axis().scale(this.x).orient("bottom").ticks(8);
- return this.y_axis = d3.svg.axis().scale(this.y).orient("left").ticks(5);
- };
-
- ContributorsAuthorGraph.prototype.create_area = function(x, y) {
- return this.area = d3.svg.area().x(function(d) {
- var parseDate;
- parseDate = d3.time.format("%Y-%m-%d").parse;
- return x(parseDate(d));
- }).y0(this.height).y1((function(_this) {
- return function(d) {
- if (_this.data[d] != null) {
- return y(_this.data[d]);
- } else {
- return y(0);
- }
- };
- })(this)).interpolate("basis");
- };
-
- ContributorsAuthorGraph.prototype.create_svg = function() {
- this.list_item = d3.selectAll(".person")[0].pop();
- return this.svg = d3.select(this.list_item).append("svg").attr("width", this.width + this.MARGIN.left + this.MARGIN.right).attr("height", this.height + this.MARGIN.top + this.MARGIN.bottom).attr("class", "spark").append("g").attr("transform", "translate(" + this.MARGIN.left + "," + this.MARGIN.top + ")");
- };
-
- ContributorsAuthorGraph.prototype.draw_path = function(data) {
- return this.svg.append("path").datum(data).attr("class", "area-contributor").attr("d", this.area);
- };
-
- ContributorsAuthorGraph.prototype.draw = function() {
- this.create_scale();
- this.create_axes();
- this.set_domain();
- this.create_area(this.x, this.y);
- this.create_svg();
- this.draw_path(this.dates);
- this.draw_x_axis();
- return this.draw_y_axis();
- };
-
- ContributorsAuthorGraph.prototype.redraw = function() {
- this.set_domain();
- this.svg.select("path").datum(this.dates);
- this.svg.select("path").attr("d", this.area);
- this.svg.select(".x.axis").call(this.x_axis);
- return this.svg.select(".y.axis").call(this.y_axis);
- };
-
- return ContributorsAuthorGraph;
- })(ContributorsGraph);
-}).call(this);
+ return x(parseDate(d));
+ }).y0(this.height).y1((function(_this) {
+ return function(d) {
+ if (_this.data[d] != null) {
+ return y(_this.data[d]);
+ } else {
+ return y(0);
+ }
+ };
+ })(this)).interpolate("basis");
+ };
+
+ ContributorsAuthorGraph.prototype.create_svg = function() {
+ this.list_item = d3.selectAll(".person")[0].pop();
+ return this.svg = d3.select(this.list_item).append("svg").attr("width", this.width + this.MARGIN.left + this.MARGIN.right).attr("height", this.height + this.MARGIN.top + this.MARGIN.bottom).attr("class", "spark").append("g").attr("transform", "translate(" + this.MARGIN.left + "," + this.MARGIN.top + ")");
+ };
+
+ ContributorsAuthorGraph.prototype.draw_path = function(data) {
+ return this.svg.append("path").datum(data).attr("class", "area-contributor").attr("d", this.area);
+ };
+
+ ContributorsAuthorGraph.prototype.draw = function() {
+ this.create_scale();
+ this.create_axes();
+ this.set_domain();
+ this.create_area(this.x, this.y);
+ this.create_svg();
+ this.draw_path(this.dates);
+ this.draw_x_axis();
+ return this.draw_y_axis();
+ };
+
+ ContributorsAuthorGraph.prototype.redraw = function() {
+ this.set_domain();
+ this.svg.select("path").datum(this.dates);
+ this.svg.select("path").attr("d", this.area);
+ this.svg.select(".x.axis").call(this.x_axis);
+ return this.svg.select(".y.axis").call(this.y_axis);
+ };
+
+ return ContributorsAuthorGraph;
+})(ContributorsGraph);
diff --git a/app/assets/javascripts/graphs/stat_graph_contributors_util.js b/app/assets/javascripts/graphs/stat_graph_contributors_util.js
index 29c3163328f..c583757f3f2 100644
--- a/app/assets/javascripts/graphs/stat_graph_contributors_util.js
+++ b/app/assets/javascripts/graphs/stat_graph_contributors_util.js
@@ -1,138 +1,137 @@
/* 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) {
- var by_author, by_email, data, entry, i, len, total, normalized_email;
- total = {};
- by_author = {};
- by_email = {};
- for (i = 0, len = log.length; i < len; i += 1) {
- entry = log[i];
- if (total[entry.date] == null) {
- this.add_date(entry.date, total);
- }
- normalized_email = entry.author_email.toLowerCase();
- data = by_author[entry.author_name] || by_email[normalized_email];
- if (data == null) {
- data = this.add_author(entry, by_author, by_email);
- }
- if (!data[entry.date]) {
- this.add_date(entry.date, data);
- }
- this.store_data(entry, total[entry.date], data[entry.date]);
- }
- total = _.toArray(total);
- by_author = _.toArray(by_author);
- return {
- total: total,
- by_author: by_author
- };
- },
- add_date: function(date, collection) {
- collection[date] = {};
- return collection[date].date = date;
- },
- add_author: function(author, by_author, by_email) {
- var data, normalized_email;
- data = {};
- data.author_name = author.author_name;
- data.author_email = author.author_email;
- normalized_email = author.author_email.toLowerCase();
- by_author[author.author_name] = data;
- by_email[normalized_email] = data;
- return data;
- },
- store_data: function(entry, total, by_author) {
- this.store_commits(total, by_author);
- this.store_additions(entry, total, by_author);
- return this.store_deletions(entry, total, by_author);
- },
- store_commits: function(total, by_author) {
- this.add(total, "commits", 1);
- return this.add(by_author, "commits", 1);
- },
- add: function(collection, field, value) {
- if (collection[field] == null) {
- collection[field] = 0;
- }
- return collection[field] += value;
- },
- store_additions: function(entry, total, by_author) {
- if (entry.additions == null) {
- entry.additions = 0;
+
+export default {
+ parse_log: function(log) {
+ var by_author, by_email, data, entry, i, len, total, normalized_email;
+ total = {};
+ by_author = {};
+ by_email = {};
+ for (i = 0, len = log.length; i < len; i += 1) {
+ entry = log[i];
+ if (total[entry.date] == null) {
+ this.add_date(entry.date, total);
}
- this.add(total, "additions", entry.additions);
- return this.add(by_author, "additions", entry.additions);
- },
- store_deletions: function(entry, total, by_author) {
- if (entry.deletions == null) {
- entry.deletions = 0;
+ normalized_email = entry.author_email.toLowerCase();
+ data = by_author[entry.author_name] || by_email[normalized_email];
+ if (data == null) {
+ data = this.add_author(entry, by_author, by_email);
}
- this.add(total, "deletions", entry.deletions);
- return this.add(by_author, "deletions", entry.deletions);
- },
- get_total_data: function(parsed_log, field) {
- var log, total_data;
- log = parsed_log.total;
- total_data = this.pick_field(log, field);
- return _.sortBy(total_data, function(d) {
- return d.date;
- });
- },
- pick_field: function(log, field) {
- var total_data;
- total_data = [];
- _.each(log, function(d) {
- return total_data.push(_.pick(d, [field, 'date']));
- });
- return total_data;
- },
- get_author_data: function(parsed_log, field, date_range) {
- var author_data, log;
- if (date_range == null) {
- date_range = null;
- }
- log = parsed_log.by_author;
- author_data = [];
- _.each(log, (function(_this) {
- return function(log_entry) {
- var parsed_log_entry;
- parsed_log_entry = _this.parse_log_entry(log_entry, field, date_range);
- if (!_.isEmpty(parsed_log_entry.dates)) {
- return author_data.push(parsed_log_entry);
- }
- };
- })(this));
- return _.sortBy(author_data, function(d) {
- return d[field];
- }).reverse();
- },
- parse_log_entry: function(log_entry, field, date_range) {
- var parsed_entry;
- parsed_entry = {};
- parsed_entry.author_name = log_entry.author_name;
- parsed_entry.author_email = log_entry.author_email;
- parsed_entry.dates = {};
- parsed_entry.commits = parsed_entry.additions = parsed_entry.deletions = 0;
- _.each(_.omit(log_entry, 'author_name', 'author_email'), (function(_this) {
- return function(value, key) {
- if (_this.in_range(value.date, date_range)) {
- parsed_entry.dates[value.date] = value[field];
- parsed_entry.commits += value.commits;
- parsed_entry.additions += value.additions;
- return parsed_entry.deletions += value.deletions;
- }
- };
- })(this));
- return parsed_entry;
- },
- in_range: function(date, date_range) {
- var ref;
- if (date_range === null || (date_range[0] <= (ref = new Date(date)) && ref <= date_range[1])) {
- return true;
- } else {
- return false;
+ if (!data[entry.date]) {
+ this.add_date(entry.date, data);
}
+ this.store_data(entry, total[entry.date], data[entry.date]);
+ }
+ total = _.toArray(total);
+ by_author = _.toArray(by_author);
+ return {
+ total: total,
+ by_author: by_author
+ };
+ },
+ add_date: function(date, collection) {
+ collection[date] = {};
+ return collection[date].date = date;
+ },
+ add_author: function(author, by_author, by_email) {
+ var data, normalized_email;
+ data = {};
+ data.author_name = author.author_name;
+ data.author_email = author.author_email;
+ normalized_email = author.author_email.toLowerCase();
+ by_author[author.author_name] = data;
+ by_email[normalized_email] = data;
+ return data;
+ },
+ store_data: function(entry, total, by_author) {
+ this.store_commits(total, by_author);
+ this.store_additions(entry, total, by_author);
+ return this.store_deletions(entry, total, by_author);
+ },
+ store_commits: function(total, by_author) {
+ this.add(total, "commits", 1);
+ return this.add(by_author, "commits", 1);
+ },
+ add: function(collection, field, value) {
+ if (collection[field] == null) {
+ collection[field] = 0;
+ }
+ return collection[field] += value;
+ },
+ store_additions: function(entry, total, by_author) {
+ if (entry.additions == null) {
+ entry.additions = 0;
+ }
+ this.add(total, "additions", entry.additions);
+ return this.add(by_author, "additions", entry.additions);
+ },
+ store_deletions: function(entry, total, by_author) {
+ if (entry.deletions == null) {
+ entry.deletions = 0;
+ }
+ this.add(total, "deletions", entry.deletions);
+ return this.add(by_author, "deletions", entry.deletions);
+ },
+ get_total_data: function(parsed_log, field) {
+ var log, total_data;
+ log = parsed_log.total;
+ total_data = this.pick_field(log, field);
+ return _.sortBy(total_data, function(d) {
+ return d.date;
+ });
+ },
+ pick_field: function(log, field) {
+ var total_data;
+ total_data = [];
+ _.each(log, function(d) {
+ return total_data.push(_.pick(d, [field, 'date']));
+ });
+ return total_data;
+ },
+ get_author_data: function(parsed_log, field, date_range) {
+ var author_data, log;
+ if (date_range == null) {
+ date_range = null;
+ }
+ log = parsed_log.by_author;
+ author_data = [];
+ _.each(log, (function(_this) {
+ return function(log_entry) {
+ var parsed_log_entry;
+ parsed_log_entry = _this.parse_log_entry(log_entry, field, date_range);
+ if (!_.isEmpty(parsed_log_entry.dates)) {
+ return author_data.push(parsed_log_entry);
+ }
+ };
+ })(this));
+ return _.sortBy(author_data, function(d) {
+ return d[field];
+ }).reverse();
+ },
+ parse_log_entry: function(log_entry, field, date_range) {
+ var parsed_entry;
+ parsed_entry = {};
+ parsed_entry.author_name = log_entry.author_name;
+ parsed_entry.author_email = log_entry.author_email;
+ parsed_entry.dates = {};
+ parsed_entry.commits = parsed_entry.additions = parsed_entry.deletions = 0;
+ _.each(_.omit(log_entry, 'author_name', 'author_email'), (function(_this) {
+ return function(value, key) {
+ if (_this.in_range(value.date, date_range)) {
+ parsed_entry.dates[value.date] = value[field];
+ parsed_entry.commits += value.commits;
+ parsed_entry.additions += value.additions;
+ return parsed_entry.deletions += value.deletions;
+ }
+ };
+ })(this));
+ return parsed_entry;
+ },
+ in_range: function(date, date_range) {
+ var ref;
+ if (date_range === null || (date_range[0] <= (ref = new Date(date)) && ref <= date_range[1])) {
+ return true;
+ } else {
+ return false;
}
- };
-}).call(this);
+ }
+};
diff --git a/app/assets/javascripts/group_avatar.js b/app/assets/javascripts/group_avatar.js
index 10dfd05fe3c..c5cb273c5b2 100644
--- a/app/assets/javascripts/group_avatar.js
+++ b/app/assets/javascripts/group_avatar.js
@@ -17,4 +17,4 @@
return GroupAvatar;
})();
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/groups_select.js b/app/assets/javascripts/groups_select.js
index bc88dc2d092..6b937e7fa0f 100644
--- a/app/assets/javascripts/groups_select.js
+++ b/app/assets/javascripts/groups_select.js
@@ -68,4 +68,4 @@
return GroupsSelect;
})();
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/header.js b/app/assets/javascripts/header.js
index fa85f9a6c86..a853c3aeb1f 100644
--- a/app/assets/javascripts/header.js
+++ b/app/assets/javascripts/header.js
@@ -2,7 +2,7 @@
(function() {
$(document).on('todo:toggle', function(e, count) {
var $todoPendingCount = $('.todos-pending-count');
- $todoPendingCount.text(gl.text.addDelimiter(count));
+ $todoPendingCount.text(gl.text.highCountTrim(count));
$todoPendingCount.toggleClass('hidden', count === 0);
});
})();
diff --git a/app/assets/javascripts/importer_status.js b/app/assets/javascripts/importer_status.js
index 9390136d3d8..34e4a257ff9 100644
--- a/app/assets/javascripts/importer_status.js
+++ b/app/assets/javascripts/importer_status.js
@@ -78,4 +78,4 @@
new window.ImporterStatus(jobsImportPath, importPath);
}
});
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/issuable.js.es6 b/app/assets/javascripts/issuable.js.es6
index 8df86f68218..3bfce32768a 100644
--- a/app/assets/javascripts/issuable.js.es6
+++ b/app/assets/javascripts/issuable.js.es6
@@ -116,7 +116,7 @@
formData = $.param(formData);
formAction = form.attr('action');
issuesUrl = formAction;
- issuesUrl += "" + (formAction.indexOf('?') < 0 ? '?' : '&');
+ issuesUrl += "" + (formAction.indexOf('?') === -1 ? '?' : '&');
issuesUrl += formData;
return gl.utils.visitUrl(issuesUrl);
};
diff --git a/app/assets/javascripts/issuable_context.js b/app/assets/javascripts/issuable_context.js
index c77fbb6a1c7..115312d4b83 100644
--- a/app/assets/javascripts/issuable_context.js
+++ b/app/assets/javascripts/issuable_context.js
@@ -76,4 +76,4 @@
return IssuableContext;
})();
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/issuable_form.js b/app/assets/javascripts/issuable_form.js
index c7c744ef61f..de184ab2675 100644
--- a/app/assets/javascripts/issuable_form.js
+++ b/app/assets/javascripts/issuable_form.js
@@ -156,4 +156,4 @@
return IssuableForm;
})();
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js
index 1776b3d61f6..52457f70d90 100644
--- a/app/assets/javascripts/issue.js
+++ b/app/assets/javascripts/issue.js
@@ -3,7 +3,7 @@
require('./flash');
require('vendor/jquery.waitforimages');
-require('vendor/task_list');
+require('./task_list');
(function() {
var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
@@ -11,10 +11,16 @@ require('vendor/task_list');
this.Issue = (function() {
function Issue() {
this.submitNoteForm = bind(this.submitNoteForm, this);
- // Prevent duplicate event bindings
- this.disableTaskList();
if ($('a.btn-close').length) {
- this.initTaskList();
+ this.taskList = new gl.TaskList({
+ dataType: 'issue',
+ fieldName: 'description',
+ selector: '.detail-page-description',
+ onSuccess: (result) => {
+ document.querySelector('#task_status').innerText = result.task_status;
+ document.querySelector('#task_status_short').innerText = result.task_status_short;
+ }
+ });
this.initIssueBtnEventListeners();
}
this.initMergeRequests();
@@ -22,11 +28,6 @@ require('vendor/task_list');
this.initCanCreateBranch();
}
- Issue.prototype.initTaskList = function() {
- $('.detail-page-description .js-task-list-container').taskList('enable');
- return $(document).on('tasklist:changed', '.detail-page-description .js-task-list-container', this.updateTaskList);
- };
-
Issue.prototype.initIssueBtnEventListeners = function() {
var _this, issueFailMessage;
_this = this;
@@ -85,30 +86,6 @@ require('vendor/task_list');
}
};
- Issue.prototype.disableTaskList = function() {
- $('.detail-page-description .js-task-list-container').taskList('disable');
- return $(document).off('tasklist:changed', '.detail-page-description .js-task-list-container');
- };
-
- Issue.prototype.updateTaskList = function() {
- var patchData;
- patchData = {};
- patchData['issue'] = {
- 'description': $('.js-task-list-field', this).val()
- };
- return $.ajax({
- type: 'PATCH',
- url: $('form.js-issuable-update').attr('action'),
- data: patchData,
- success: function(issue) {
- document.querySelector('#task_status').innerText = issue.task_status;
- document.querySelector('#task_status_short').innerText = issue.task_status_short;
- }
- });
- // TODO (rspeicher): Make the issue description inline-editable like a note so
- // that we can re-use its form here
- };
-
Issue.prototype.initMergeRequests = function() {
var $container;
$container = $('#merge-requests');
@@ -155,4 +132,4 @@ require('vendor/task_list');
return Issue;
})();
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/issue_status_select.js b/app/assets/javascripts/issue_status_select.js
index 1d6eff11403..b2cfd3ef2a3 100644
--- a/app/assets/javascripts/issue_status_select.js
+++ b/app/assets/javascripts/issue_status_select.js
@@ -31,4 +31,4 @@
return IssueStatusSelect;
})();
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/labels.js b/app/assets/javascripts/labels.js
index 40ad6fc348e..17a3fc1b1e4 100644
--- a/app/assets/javascripts/labels.js
+++ b/app/assets/javascripts/labels.js
@@ -43,4 +43,4 @@
return Labels;
})();
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js
index e4cf9057e6d..9e2d14c7f87 100644
--- a/app/assets/javascripts/labels_select.js
+++ b/app/assets/javascripts/labels_select.js
@@ -504,4 +504,4 @@
return LabelsSelect;
})();
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/layout_nav.js b/app/assets/javascripts/layout_nav.js
index 1c0ea317c1a..08ca9e4fa4d 100644
--- a/app/assets/javascripts/layout_nav.js
+++ b/app/assets/javascripts/layout_nav.js
@@ -44,4 +44,4 @@
}
});
});
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/lib/cropper.js b/app/assets/javascripts/lib/cropper.js
index 5221f85ba7a..7862c6797c3 100644
--- a/app/assets/javascripts/lib/cropper.js
+++ b/app/assets/javascripts/lib/cropper.js
@@ -4,4 +4,4 @@
(function() {
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/lib/raphael.js b/app/assets/javascripts/lib/raphael.js
index 5a9a501efe3..ebe1e2ae98d 100644
--- a/app/assets/javascripts/lib/raphael.js
+++ b/app/assets/javascripts/lib/raphael.js
@@ -6,4 +6,4 @@
(function() {
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/lib/utils/animate.js b/app/assets/javascripts/lib/utils/animate.js
index ce090a2e4fd..d93c1d0da59 100644
--- a/app/assets/javascripts/lib/utils/animate.js
+++ b/app/assets/javascripts/lib/utils/animate.js
@@ -46,4 +46,4 @@
return dfd.promise();
};
})(window);
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/lib/utils/common_utils.js.es6 b/app/assets/javascripts/lib/utils/common_utils.js.es6
index 764aff51fee..0242350f718 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js.es6
+++ b/app/assets/javascripts/lib/utils/common_utils.js.es6
@@ -296,5 +296,58 @@
* @returns {Boolean}
*/
w.gl.utils.convertPermissionToBoolean = permission => permission === 'true';
+
+ /**
+ * Back Off exponential algorithm
+ * backOff :: (Function<next, stop>, Number) -> Promise<Any, Error>
+ *
+ * @param {Function<next, stop>} fn function to be called
+ * @param {Number} timeout
+ * @return {Promise<Any, Error>}
+ * @example
+ * ```
+ * backOff(function (next, stop) {
+ * // Let's perform this function repeatedly for 60s or for the timeout provided.
+ *
+ * ourFunction()
+ * .then(function (result) {
+ * // continue if result is not what we need
+ * next();
+ *
+ * // when result is what we need let's stop with the repetions and jump out of the cycle
+ * stop(result);
+ * })
+ * .catch(function (error) {
+ * // if there is an error, we need to stop this with an error.
+ * stop(error);
+ * })
+ * }, 60000)
+ * .then(function (result) {})
+ * .catch(function (error) {
+ * // deal with errors passed to stop()
+ * })
+ * ```
+ */
+ w.gl.utils.backOff = (fn, timeout = 60000) => {
+ const maxInterval = 32000;
+ let nextInterval = 2000;
+
+ const startTime = Date.now();
+
+ return new Promise((resolve, reject) => {
+ const stop = arg => ((arg instanceof Error) ? reject(arg) : resolve(arg));
+
+ const next = () => {
+ if (Date.now() - startTime < timeout) {
+ setTimeout(fn.bind(null, next, stop), nextInterval);
+ nextInterval = Math.min(nextInterval + nextInterval, maxInterval);
+ } else {
+ reject(new Error('BACKOFF_TIMEOUT'));
+ }
+ };
+
+ fn(next, stop);
+ });
+ };
})(window);
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js.es6 b/app/assets/javascripts/lib/utils/datetime_utility.js.es6
index f41fa15b147..82dcbdc26c8 100644
--- a/app/assets/javascripts/lib/utils/datetime_utility.js.es6
+++ b/app/assets/javascripts/lib/utils/datetime_utility.js.es6
@@ -123,4 +123,4 @@ window.dateFormat = require('vendor/date.format');
return Math.floor((date2 - date1) / millisecondsPerDay);
};
})(window);
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/lib/utils/http_status.js b/app/assets/javascripts/lib/utils/http_status.js
new file mode 100644
index 00000000000..bc109a69c20
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/http_status.js
@@ -0,0 +1,10 @@
+/**
+ * exports HTTP status codes
+ */
+
+const statusCodes = {
+ NO_CONTENT: 204,
+ OK: 200,
+};
+
+module.exports = statusCodes;
diff --git a/app/assets/javascripts/lib/utils/notify.js b/app/assets/javascripts/lib/utils/notify.js
index 6d5979603b9..66f39122a66 100644
--- a/app/assets/javascripts/lib/utils/notify.js
+++ b/app/assets/javascripts/lib/utils/notify.js
@@ -44,4 +44,4 @@
w.notify = notifyMe;
return w.notifyPermissions = notifyPermissions;
})(window);
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js
index d9370db0cf2..579d322e3fb 100644
--- a/app/assets/javascripts/lib/utils/text_utility.js
+++ b/app/assets/javascripts/lib/utils/text_utility.js
@@ -1,5 +1,7 @@
/* 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 */
+require('vendor/latinise');
+
(function() {
(function(w) {
var base;
@@ -12,6 +14,9 @@
gl.text.addDelimiter = function(text) {
return text ? text.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") : text;
};
+ gl.text.highCountTrim = function(count) {
+ return count > 99 ? '99+' : count;
+ };
gl.text.randomString = function() {
return Math.random().toString(36).substring(7);
};
@@ -164,8 +169,14 @@
gl.text.pluralize = function(str, count) {
return str + (count > 1 || count === 0 ? 's' : '');
};
- return gl.text.truncate = function(string, maxLength) {
+ gl.text.truncate = function(string, maxLength) {
return string.substr(0, (maxLength - 3)) + '...';
};
+ gl.text.dasherize = function(str) {
+ return str.replace(/[_\s]+/g, '-');
+ };
+ gl.text.slugify = function(str) {
+ return str.trim().toLowerCase().latinise();
+ };
})(window);
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/lib/utils/type_utility.js b/app/assets/javascripts/lib/utils/type_utility.js
index 6d813d61601..db62e0be324 100644
--- a/app/assets/javascripts/lib/utils/type_utility.js
+++ b/app/assets/javascripts/lib/utils/type_utility.js
@@ -12,4 +12,4 @@
return (obj != null) && (obj.constructor === Object);
};
})(window);
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/lib/utils/url_utility.js.es6 b/app/assets/javascripts/lib/utils/url_utility.js.es6
index a1558b371f0..1bc81d2e4a4 100644
--- a/app/assets/javascripts/lib/utils/url_utility.js.es6
+++ b/app/assets/javascripts/lib/utils/url_utility.js.es6
@@ -83,4 +83,4 @@
document.location.href = url;
};
})(window);
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/line_highlighter.js b/app/assets/javascripts/line_highlighter.js
index d7137ec63e4..966fcd8ec47 100644
--- a/app/assets/javascripts/line_highlighter.js
+++ b/app/assets/javascripts/line_highlighter.js
@@ -179,4 +179,4 @@ require('vendor/jquery.scrollTo');
return LineHighlighter;
})();
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/logo.js b/app/assets/javascripts/logo.js
index 1b0d0768db8..729baa2e1a7 100644
--- a/app/assets/javascripts/logo.js
+++ b/app/assets/javascripts/logo.js
@@ -4,4 +4,4 @@
window.addEventListener('beforeunload', function() {
$('.tanuki-logo').addClass('animate');
});
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/member_expiration_date.js.es6 b/app/assets/javascripts/member_expiration_date.js.es6
index efe7c78a8ec..129d2dc5f0a 100644
--- a/app/assets/javascripts/member_expiration_date.js.es6
+++ b/app/assets/javascripts/member_expiration_date.js.es6
@@ -49,4 +49,4 @@
inputs.each(toggleClearInput);
};
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js
index e65378cd610..5e01aacf2ba 100644
--- a/app/assets/javascripts/merge_request.js
+++ b/app/assets/javascripts/merge_request.js
@@ -2,7 +2,7 @@
/* global MergeRequestTabs */
require('vendor/jquery.waitforimages');
-require('vendor/task_list');
+require('./task_list');
require('./merge_request_tabs');
(function() {
@@ -24,12 +24,18 @@ require('./merge_request_tabs');
};
})(this));
this.initTabs();
- // Prevent duplicate event bindings
- this.disableTaskList();
this.initMRBtnListeners();
this.initCommitMessageListeners();
if ($("a.btn-close").length) {
- this.initTaskList();
+ this.taskList = new gl.TaskList({
+ dataType: 'merge_request',
+ fieldName: 'description',
+ selector: '.detail-page-description',
+ onSuccess: (result) => {
+ document.querySelector('#task_status').innerText = result.task_status;
+ document.querySelector('#task_status_short').innerText = result.task_status_short;
+ }
+ });
}
}
@@ -50,11 +56,6 @@ require('./merge_request_tabs');
return this.$('.all-commits').removeClass('hide');
};
- MergeRequest.prototype.initTaskList = function() {
- $('.detail-page-description .js-task-list-container').taskList('enable');
- return $(document).on('tasklist:changed', '.detail-page-description .js-task-list-container', this.updateTaskList);
- };
-
MergeRequest.prototype.initMRBtnListeners = function() {
var _this;
_this = this;
@@ -85,30 +86,6 @@ require('./merge_request_tabs');
}
};
- MergeRequest.prototype.disableTaskList = function() {
- $('.detail-page-description .js-task-list-container').taskList('disable');
- return $(document).off('tasklist:changed', '.detail-page-description .js-task-list-container');
- };
-
- MergeRequest.prototype.updateTaskList = function() {
- var patchData;
- patchData = {};
- patchData['merge_request'] = {
- 'description': $('.js-task-list-field', this).val()
- };
- return $.ajax({
- type: 'PATCH',
- url: $('form.js-issuable-update').attr('action'),
- data: patchData,
- success: function(mergeRequest) {
- document.querySelector('#task_status').innerText = mergeRequest.task_status;
- document.querySelector('#task_status_short').innerText = mergeRequest.task_status_short;
- }
- });
- // TODO (rspeicher): Make the merge request description inline-editable like a
- // note so that we can re-use its form here
- };
-
MergeRequest.prototype.initCommitMessageListeners = function() {
$(document).on('click', 'a.js-with-description-link', function(e) {
var textarea = $('textarea.js-commit-message');
@@ -131,4 +108,4 @@ require('./merge_request_tabs');
return MergeRequest;
})();
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/merge_request_widget.js.es6 b/app/assets/javascripts/merge_request_widget.js.es6
index 4ab33420e59..00c6c050612 100644
--- a/app/assets/javascripts/merge_request_widget.js.es6
+++ b/app/assets/javascripts/merge_request_widget.js.es6
@@ -83,7 +83,7 @@ require('./smart_interval');
return function() {
var page;
page = $('body').data('page').split(':').last();
- if (allowedPages.indexOf(page) < 0) {
+ if (allowedPages.indexOf(page) === -1) {
return _this.clearEventListeners();
}
};
@@ -233,7 +233,7 @@ require('./smart_interval');
}
$('.ci_widget').hide();
allowed_states = ["failed", "canceled", "running", "pending", "success", "success_with_warnings", "skipped", "not_found"];
- if (indexOf.call(allowed_states, state) >= 0) {
+ if (indexOf.call(allowed_states, state) !== -1) {
$('.ci_widget.ci-' + state).show();
switch (state) {
case "failed":
@@ -252,7 +252,6 @@ require('./smart_interval');
$('.ci_widget.ci-error').show();
this.setMergeButtonClass('btn-danger');
}
- this.initMiniPipelineGraph();
};
MergeRequestWidget.prototype.showCICoverage = function(coverage) {
diff --git a/app/assets/javascripts/merged_buttons.js b/app/assets/javascripts/merged_buttons.js
index 527cdc9b698..9548a98f499 100644
--- a/app/assets/javascripts/merged_buttons.js
+++ b/app/assets/javascripts/merged_buttons.js
@@ -42,4 +42,4 @@
return MergedButtons;
})();
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/milestone.js b/app/assets/javascripts/milestone.js
index 051cb9fe5c5..38c673e8907 100644
--- a/app/assets/javascripts/milestone.js
+++ b/app/assets/javascripts/milestone.js
@@ -78,7 +78,6 @@
} else {
$(element).find('.assignee-icon').empty();
}
- return $(element).effect('highlight');
};
function Milestone() {
@@ -172,4 +171,4 @@
return Milestone;
})();
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js
index 2f08aa7fe8b..51fa5c828b3 100644
--- a/app/assets/javascripts/milestone_select.js
+++ b/app/assets/javascripts/milestone_select.js
@@ -39,7 +39,7 @@
$value = $block.find('.value');
$loading = $block.find('.block-loading').fadeOut();
if (issueUpdateURL) {
- milestoneLinkTemplate = _.template('<a href="/<%- namespace %>/<%- path %>/milestones/<%- iid %>" class="bold has-tooltip" data-container="body" title="<%- remaining %>"><%- title %></a>');
+ milestoneLinkTemplate = _.template('<a href="/<%- full_path %>/milestones/<%- iid %>" class="bold has-tooltip" data-container="body" title="<%- remaining %>"><%- title %></a>');
milestoneLinkNoneTemplate = '<span class="no-value">None</span>';
collapsedSidebarLabelTemplate = _.template('<span class="has-tooltip" data-container="body" title="<%- remaining %>" data-placement="left"> <%- title %> </span>');
}
@@ -181,8 +181,7 @@
$selectbox.hide();
$value.css('display', '');
if (data.milestone != null) {
- data.milestone.namespace = _this.currentProject.namespace;
- data.milestone.path = _this.currentProject.path;
+ data.milestone.full_path = _this.currentProject.full_path;
data.milestone.remaining = gl.utils.timeFor(data.milestone.due_date);
$value.html(milestoneLinkTemplate(data.milestone));
return $sidebarCollapsedValue.find('span').html(collapsedSidebarLabelTemplate(data.milestone));
@@ -199,4 +198,4 @@
return MilestoneSelect;
})();
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6 b/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6
index 919fcd0a07b..2145e531331 100644
--- a/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6
+++ b/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6
@@ -28,7 +28,7 @@
* All dropdown events are fired at the .dropdown-menu's parent element.
*/
bindEvents() {
- $(document).on('shown.bs.dropdown', this.container, this.getBuildsList);
+ $(document).off('shown.bs.dropdown', this.container).on('shown.bs.dropdown', this.container, this.getBuildsList);
}
/**
diff --git a/app/assets/javascripts/namespace_select.js b/app/assets/javascripts/namespace_select.js
index 2ae5617206e..b98e6121967 100644
--- a/app/assets/javascripts/namespace_select.js
+++ b/app/assets/javascripts/namespace_select.js
@@ -83,4 +83,4 @@
return NamespaceSelects;
})();
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/network/branch_graph.js b/app/assets/javascripts/network/branch_graph.js
index a7ccd03b60c..43dc9838977 100644
--- a/app/assets/javascripts/network/branch_graph.js
+++ b/app/assets/javascripts/network/branch_graph.js
@@ -421,4 +421,4 @@
y: h
});
};
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/network/network.js b/app/assets/javascripts/network/network.js
index 37bf6436fd1..8e7027b44e7 100644
--- a/app/assets/javascripts/network/network.js
+++ b/app/assets/javascripts/network/network.js
@@ -17,4 +17,4 @@
return Network;
})();
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/network/network_bundle.js b/app/assets/javascripts/network/network_bundle.js
index b4491354472..e5947586583 100644
--- a/app/assets/javascripts/network/network_bundle.js
+++ b/app/assets/javascripts/network/network_bundle.js
@@ -2,9 +2,8 @@
/* global Network */
/* global ShortcutsNetwork */
-// require everything else in this directory
-function requireAll(context) { return context.keys().map(context); }
-requireAll(require.context('.', false, /^\.\/(?!network_bundle).*\.(js|es6)$/));
+require('./branch_graph');
+require('./network');
(function() {
$(function() {
@@ -19,4 +18,4 @@ requireAll(require.context('.', false, /^\.\/(?!network_bundle).*\.(js|es6)$/));
});
return new ShortcutsNetwork(network_graph.branch_graph);
});
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/new_branch_form.js b/app/assets/javascripts/new_branch_form.js
index 7f763c13b50..5828f460a23 100644
--- a/app/assets/javascripts/new_branch_form.js
+++ b/app/assets/javascripts/new_branch_form.js
@@ -1,4 +1,4 @@
-/* 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 */
+/* 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, object-shorthand */
(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 += 1) { if (i in this && this[i] === item) return i; } return -1; };
@@ -20,15 +20,35 @@
};
NewBranchForm.prototype.init = function() {
- if (this.name.val().length > 0) {
+ if (this.name.length && this.name.val().length > 0) {
return this.name.trigger('blur');
}
};
NewBranchForm.prototype.setupAvailableRefs = function(availableRefs) {
- return this.ref.autocomplete({
- source: availableRefs,
- minLength: 1
+ var $branchSelect = $('.js-branch-select');
+
+ $branchSelect.glDropdown({
+ data: availableRefs,
+ filterable: true,
+ filterByText: true,
+ remote: false,
+ fieldName: $branchSelect.data('field-name'),
+ selectable: true,
+ isSelectable: function(branch, $el) {
+ return !$el.hasClass('is-active');
+ },
+ text: function(branch) {
+ return branch;
+ },
+ id: function(branch) {
+ return branch;
+ },
+ toggleLabel: function(branch) {
+ if (branch) {
+ return branch;
+ }
+ }
});
};
@@ -61,7 +81,7 @@
var errorMessage, errors, formatter, unique, validator;
this.branchNameError.empty();
unique = function(values, value) {
- if (indexOf.call(values, value) < 0) {
+ if (indexOf.call(values, value) === -1) {
values.push(value);
}
return values;
@@ -100,4 +120,4 @@
return NewBranchForm;
})();
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/new_commit_form.js b/app/assets/javascripts/new_commit_form.js
index 41eea78a3e6..747f693726e 100644
--- a/app/assets/javascripts/new_commit_form.js
+++ b/app/assets/javascripts/new_commit_form.js
@@ -30,4 +30,4 @@
return NewCommitForm;
})();
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index 0464b895d6d..03504255bda 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -11,7 +11,7 @@ require('./dropzone_input');
require('./gfm_auto_complete');
require('vendor/jquery.caret'); // required by jquery.atwho
require('vendor/jquery.atwho');
-require('vendor/task_list');
+require('./task_list');
(function() {
var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
@@ -51,7 +51,11 @@ require('vendor/task_list');
this.addBinding();
this.setPollingInterval();
this.setupMainTargetNoteForm();
- this.initTaskList();
+ this.taskList = new gl.TaskList({
+ dataType: 'note',
+ fieldName: 'note',
+ selector: '.notes'
+ });
this.collapseLongCommitList();
// We are in the Merge Requests page so we need another edit form for Changes tab
@@ -125,8 +129,6 @@ require('vendor/task_list');
$(document).off("keydown", ".js-note-text");
$(document).off('click', '.js-comment-resolve-button');
$(document).off("click", '.system-note-commit-list-toggler');
- $('.note .js-task-list-container').taskList('disable');
- return $(document).off('tasklist:changed', '.note .js-task-list-container');
};
Notes.prototype.keydownNoteText = function(e) {
@@ -286,7 +288,7 @@ require('vendor/task_list');
// Update datetime format on the recent note
gl.utils.localTimeAgo($notesList.find("#note_" + note.id + " .js-timeago"), false);
this.collapseLongCommitList();
- this.initTaskList();
+ this.taskList.init();
this.refresh();
return this.updateNotesCount(1);
}
@@ -863,15 +865,6 @@ require('vendor/task_list');
}
};
- Notes.prototype.initTaskList = function() {
- this.enableTaskList();
- 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.putEditFormInPlace = function($el) {
var $editForm = $(this.getEditFormSelector($el));
var $note = $el.closest('.note');
@@ -896,17 +889,6 @@ require('vendor/task_list');
$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(), 10) + updateCount);
};
@@ -955,4 +937,4 @@ require('vendor/task_list');
return Notes;
})();
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/notifications_dropdown.js b/app/assets/javascripts/notifications_dropdown.js
index 926dc35fee8..838356133cd 100644
--- a/app/assets/javascripts/notifications_dropdown.js
+++ b/app/assets/javascripts/notifications_dropdown.js
@@ -28,4 +28,4 @@
return NotificationsDropdown;
})();
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/notifications_form.js b/app/assets/javascripts/notifications_form.js
index c3d7cc0adfb..5005af90d48 100644
--- a/app/assets/javascripts/notifications_form.js
+++ b/app/assets/javascripts/notifications_form.js
@@ -54,4 +54,4 @@
return NotificationsForm;
})();
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/profile/profile.js.es6 b/app/assets/javascripts/profile/profile.js.es6
index 81374296522..4ccea0624ee 100644
--- a/app/assets/javascripts/profile/profile.js.es6
+++ b/app/assets/javascripts/profile/profile.js.es6
@@ -84,13 +84,14 @@
}
$(function() {
- $(document).on('focusout.ssh_key', '#key_key', function() {
+ $(document).on('input.ssh_key', '#key_key', function() {
const $title = $('#key_title');
const comment = $(this).val().match(/^\S+ \S+ (.+)\n?$/);
- if (comment && comment.length > 1 && $title.val() === '') {
+
+ // Extract the SSH Key title from its comment
+ if (comment && comment.length > 1) {
return $title.val(comment[1]).change();
}
- // Extract the SSH Key title from its comment
});
if (global.utils.getPagePath() === 'profiles') {
return new Profile();
diff --git a/app/assets/javascripts/profile/profile_bundle.js b/app/assets/javascripts/profile/profile_bundle.js
index d7f3c9fd37e..15d32825583 100644
--- a/app/assets/javascripts/profile/profile_bundle.js
+++ b/app/assets/javascripts/profile/profile_bundle.js
@@ -1,3 +1,2 @@
-// require everything else in this directory
-function requireAll(context) { return context.keys().map(context); }
-requireAll(require.context('.', false, /^\.\/(?!profile_bundle).*\.(js|es6)$/));
+require('./gl_crop');
+require('./profile');
diff --git a/app/assets/javascripts/project.js b/app/assets/javascripts/project.js
index 71719917d0c..db7ceaa2421 100644
--- a/app/assets/javascripts/project.js
+++ b/app/assets/javascripts/project.js
@@ -116,7 +116,7 @@
if ($('input[name="ref"]').length) {
var $form = $dropdown.closest('form');
var action = $form.attr('action');
- var divider = action.indexOf('?') < 0 ? '?' : '&';
+ var divider = action.indexOf('?') === -1 ? '?' : '&';
gl.utils.visitUrl(action + '' + divider + '' + $form.serialize());
}
}
@@ -126,4 +126,4 @@
return Project;
})();
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/project_avatar.js b/app/assets/javascripts/project_avatar.js
index a6d3ba9eb86..aabdfbf65e2 100644
--- a/app/assets/javascripts/project_avatar.js
+++ b/app/assets/javascripts/project_avatar.js
@@ -17,4 +17,4 @@
return ProjectAvatar;
})();
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/project_find_file.js b/app/assets/javascripts/project_find_file.js
index 04fe84683f3..e01668eabef 100644
--- a/app/assets/javascripts/project_find_file.js
+++ b/app/assets/javascripts/project_find_file.js
@@ -168,4 +168,4 @@
return ProjectFindFile;
})();
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/project_fork.js b/app/assets/javascripts/project_fork.js
index 208f25a0e33..47197db39d3 100644
--- a/app/assets/javascripts/project_fork.js
+++ b/app/assets/javascripts/project_fork.js
@@ -10,4 +10,4 @@
return ProjectFork;
})();
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/project_import.js b/app/assets/javascripts/project_import.js
index d7943959238..08334bf1ec5 100644
--- a/app/assets/javascripts/project_import.js
+++ b/app/assets/javascripts/project_import.js
@@ -10,4 +10,4 @@
return ProjectImport;
})();
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/project_label_subscription.js.es6 b/app/assets/javascripts/project_label_subscription.js.es6
index 8365f7118d5..0a811627600 100644
--- a/app/assets/javascripts/project_label_subscription.js.es6
+++ b/app/assets/javascripts/project_label_subscription.js.es6
@@ -38,13 +38,15 @@
this.$buttons.attr('data-status', newStatus);
this.$buttons.find('> span').text(newAction);
- for (const button of this.$buttons) {
+ this.$buttons.map((button) => {
const $button = $(button);
if ($button.attr('data-original-title')) {
$button.tooltip('hide').attr('data-original-title', newAction).tooltip('fixTitle');
}
- }
+
+ return button;
+ });
});
}
}
diff --git a/app/assets/javascripts/project_new.js b/app/assets/javascripts/project_new.js
index 3aa6f6771ce..e9927c1bf51 100644
--- a/app/assets/javascripts/project_new.js
+++ b/app/assets/javascripts/project_new.js
@@ -101,4 +101,4 @@
return ProjectNew;
})();
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/project_select.js b/app/assets/javascripts/project_select.js
index 7b5e9953598..f80e765ce30 100644
--- a/app/assets/javascripts/project_select.js
+++ b/app/assets/javascripts/project_select.js
@@ -101,4 +101,4 @@
return ProjectSelect;
})();
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/project_show.js b/app/assets/javascripts/project_show.js
index aad130cf267..3a51c1f26ac 100644
--- a/app/assets/javascripts/project_show.js
+++ b/app/assets/javascripts/project_show.js
@@ -6,6 +6,6 @@
return ProjectShow;
})();
-}).call(this);
+}).call(window);
// I kept class for future
diff --git a/app/assets/javascripts/projects_list.js b/app/assets/javascripts/projects_list.js
index 69a11dfaf39..acdf9b7eb5a 100644
--- a/app/assets/javascripts/projects_list.js
+++ b/app/assets/javascripts/projects_list.js
@@ -47,4 +47,4 @@
});
}
};
-}).call(this);
+}).call(window);
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 149e511451e..6ef59e94384 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_edit.js.es6
+++ b/app/assets/javascripts/protected_branches/protected_branch_edit.js.es6
@@ -36,6 +36,9 @@
// Do not update if one dropdown has not selected any option
if (!($allowedToMergeInput.length && $allowedToPushInput.length)) return;
+ this.$allowedToMergeDropdown.disable();
+ this.$allowedToPushDropdown.disable();
+
$.ajax({
type: 'POST',
url: this.$wrap.data('url'),
@@ -53,13 +56,13 @@
}]
}
},
- success: () => {
- this.$wrap.effect('highlight');
- },
error() {
$.scrollTo(0);
new Flash('Failed to update branch!');
}
+ }).always(() => {
+ this.$allowedToMergeDropdown.enable();
+ this.$allowedToPushDropdown.enable();
});
}
};
diff --git a/app/assets/javascripts/protected_branches/protected_branches_bundle.js b/app/assets/javascripts/protected_branches/protected_branches_bundle.js
index ffb66caf5f4..849c1e31623 100644
--- a/app/assets/javascripts/protected_branches/protected_branches_bundle.js
+++ b/app/assets/javascripts/protected_branches/protected_branches_bundle.js
@@ -1,3 +1,5 @@
-// require everything else in this directory
-function requireAll(context) { return context.keys().map(context); }
-requireAll(require.context('.', false, /^\.\/(?!protected_branches_bundle).*\.(js|es6)$/));
+require('./protected_branch_access_dropdown');
+require('./protected_branch_create');
+require('./protected_branch_dropdown');
+require('./protected_branch_edit');
+require('./protected_branch_edit_list');
diff --git a/app/assets/javascripts/render_gfm.js b/app/assets/javascripts/render_gfm.js
index bdbad93ad04..48cae8a4fa9 100644
--- a/app/assets/javascripts/render_gfm.js
+++ b/app/assets/javascripts/render_gfm.js
@@ -12,4 +12,4 @@
$(document).on('ready load', function() {
return $('body').renderGFM();
});
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/render_math.js b/app/assets/javascripts/render_math.js
index 6cef449babf..76c61c001ba 100644
--- a/app/assets/javascripts/render_math.js
+++ b/app/assets/javascripts/render_math.js
@@ -51,4 +51,4 @@
});
}
};
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js
index 76a0f993ea0..903862cac6b 100644
--- a/app/assets/javascripts/right_sidebar.js
+++ b/app/assets/javascripts/right_sidebar.js
@@ -21,11 +21,16 @@
};
Sidebar.prototype.addEventListeners = function() {
+ const $document = $(document);
+ const throttledSetSidebarHeight = _.throttle(this.setSidebarHeight, 10);
+
this.sidebar.on('click', '.sidebar-collapsed-icon', this, this.sidebarCollapseClicked);
$('.dropdown').on('hidden.gl.dropdown', this, this.onSidebarDropdownHidden);
$('.dropdown').on('loading.gl.dropdown', this.sidebarDropdownLoading);
$('.dropdown').on('loaded.gl.dropdown', this.sidebarDropdownLoaded);
- $(document).on('click', '.js-sidebar-toggle', function(e, triggered) {
+ $(window).on('resize', () => throttledSetSidebarHeight());
+ $document.on('scroll', () => throttledSetSidebarHeight());
+ $document.on('click', '.js-sidebar-toggle', function(e, triggered) {
var $allGutterToggleIcons, $this, $thisIcon;
e.preventDefault();
$this = $(this);
@@ -191,6 +196,17 @@
}
};
+ Sidebar.prototype.setSidebarHeight = function() {
+ const $navHeight = $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight();
+ const $rightSidebar = $('.js-right-sidebar');
+ const diff = $navHeight - $('body').scrollTop();
+ if (diff > 0) {
+ $rightSidebar.outerHeight($(window).height() - diff);
+ } else {
+ $rightSidebar.outerHeight('100%');
+ }
+ };
+
Sidebar.prototype.isOpen = function() {
return this.sidebar.is('.right-sidebar-expanded');
};
@@ -201,4 +217,4 @@
return Sidebar;
})();
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/search.js b/app/assets/javascripts/search.js
index b1c0dc37b4d..e66418beeab 100644
--- a/app/assets/javascripts/search.js
+++ b/app/assets/javascripts/search.js
@@ -97,4 +97,4 @@
return Search;
})();
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/search_autocomplete.js.es6 b/app/assets/javascripts/search_autocomplete.js.es6
index 6250e75d407..6fd5345a0a6 100644
--- a/app/assets/javascripts/search_autocomplete.js.es6
+++ b/app/assets/javascripts/search_autocomplete.js.es6
@@ -169,10 +169,10 @@
url: issuesPath + "/?author_username=" + userName
}, 'separator', {
text: 'Merge requests assigned to me',
- url: mrPath + "/?assignee_id=" + userId
+ url: mrPath + "/?assignee_username=" + userName
}, {
text: "Merge requests I've created",
- url: mrPath + "/?author_id=" + userId
+ url: mrPath + "/?author_username=" + userName
}
];
if (!name) {
diff --git a/app/assets/javascripts/shortcuts.js b/app/assets/javascripts/shortcuts.js
index c6d9b007ad1..81766f4bd55 100644
--- a/app/assets/javascripts/shortcuts.js
+++ b/app/assets/javascripts/shortcuts.js
@@ -97,4 +97,4 @@
}
};
})();
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/shortcuts_dashboard_navigation.js b/app/assets/javascripts/shortcuts_dashboard_navigation.js
index 7378b322426..e7baea894f6 100644
--- a/app/assets/javascripts/shortcuts_dashboard_navigation.js
+++ b/app/assets/javascripts/shortcuts_dashboard_navigation.js
@@ -37,4 +37,4 @@ require('./shortcuts');
return ShortcutsDashboardNavigation;
})(Shortcuts);
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/shortcuts_find_file.js b/app/assets/javascripts/shortcuts_find_file.js
index 36e379d634d..a27ac264a5c 100644
--- a/app/assets/javascripts/shortcuts_find_file.js
+++ b/app/assets/javascripts/shortcuts_find_file.js
@@ -35,4 +35,4 @@ require('./shortcuts_navigation');
return ShortcutsFindFile;
})(ShortcutsNavigation);
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/shortcuts_issuable.js b/app/assets/javascripts/shortcuts_issuable.js
index b841abb754d..fe58e98cee5 100644
--- a/app/assets/javascripts/shortcuts_issuable.js
+++ b/app/assets/javascripts/shortcuts_issuable.js
@@ -89,4 +89,4 @@ require('./shortcuts_navigation');
return ShortcutsIssuable;
})(ShortcutsNavigation);
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/shortcuts_navigation.js b/app/assets/javascripts/shortcuts_navigation.js
index cb5f2c53ea6..542cd586df0 100644
--- a/app/assets/javascripts/shortcuts_navigation.js
+++ b/app/assets/javascripts/shortcuts_navigation.js
@@ -65,4 +65,4 @@ require('./shortcuts');
return ShortcutsNavigation;
})(Shortcuts);
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/shortcuts_network.js b/app/assets/javascripts/shortcuts_network.js
index 651957f5325..4c2bf8bf001 100644
--- a/app/assets/javascripts/shortcuts_network.js
+++ b/app/assets/javascripts/shortcuts_network.js
@@ -25,4 +25,4 @@ require('./shortcuts_navigation');
return ShortcutsNetwork;
})(ShortcutsNavigation);
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/sidebar.js.es6 b/app/assets/javascripts/sidebar.js.es6
deleted file mode 100644
index 33e4b7db681..00000000000
--- a/app/assets/javascripts/sidebar.js.es6
+++ /dev/null
@@ -1,111 +0,0 @@
-/* eslint-disable arrow-parens, class-methods-use-this, no-param-reassign */
-/* global Cookies */
-
-(() => {
- const pinnedStateCookie = 'pin_nav';
- const sidebarBreakpoint = 1024;
-
- const pageSelector = '.page-with-sidebar';
- const navbarSelector = '.navbar-gitlab';
- const sidebarWrapperSelector = '.sidebar-wrapper';
- const sidebarContentSelector = '.nav-sidebar';
-
- const pinnedToggleSelector = '.js-nav-pin';
- const sidebarToggleSelector = '.toggle-nav-collapse, .side-nav-toggle';
-
- const pinnedPageClass = 'page-sidebar-pinned';
- const expandedPageClass = 'page-sidebar-expanded';
-
- const pinnedNavbarClass = 'header-sidebar-pinned';
- const expandedNavbarClass = 'header-sidebar-expanded';
-
- class Sidebar {
- constructor() {
- if (!Sidebar.singleton) {
- Sidebar.singleton = this;
- Sidebar.singleton.init();
- }
-
- return Sidebar.singleton;
- }
-
- init() {
- this.isPinned = Cookies.get(pinnedStateCookie) === 'true';
- this.isExpanded = (
- window.innerWidth >= sidebarBreakpoint &&
- $(pageSelector).hasClass(expandedPageClass)
- );
- $(window).on('resize', () => this.setSidebarHeight());
- $(document)
- .on('click', sidebarToggleSelector, () => this.toggleSidebar())
- .on('click', pinnedToggleSelector, () => this.togglePinnedState())
- .on('click', 'html, body, a, button', (e) => this.handleClickEvent(e))
- .on('DOMContentLoaded', () => this.renderState())
- .on('scroll', () => this.setSidebarHeight())
- .on('todo:toggle', (e, count) => this.updateTodoCount(count));
- this.renderState();
- this.setSidebarHeight();
- }
-
- handleClickEvent(e) {
- if (this.isExpanded && (!this.isPinned || window.innerWidth < sidebarBreakpoint)) {
- const $target = $(e.target);
- const targetIsToggle = $target.closest(sidebarToggleSelector).length > 0;
- const targetIsSidebar = $target.closest(sidebarWrapperSelector).length > 0;
- if (!targetIsToggle && (!targetIsSidebar || $target.closest('a'))) {
- this.toggleSidebar();
- }
- }
- }
-
- updateTodoCount(count) {
- $('.js-todos-count').text(gl.text.addDelimiter(count));
- }
-
- toggleSidebar() {
- this.isExpanded = !this.isExpanded;
- this.renderState();
- }
-
- setSidebarHeight() {
- const $navHeight = $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight();
- const diff = $navHeight - $('body').scrollTop();
- if (diff > 0) {
- $('.js-right-sidebar').outerHeight($(window).height() - diff);
- } else {
- $('.js-right-sidebar').outerHeight('100%');
- }
- }
-
- togglePinnedState() {
- this.isPinned = !this.isPinned;
- if (!this.isPinned) {
- this.isExpanded = false;
- }
- Cookies.set(pinnedStateCookie, this.isPinned ? 'true' : 'false', { expires: 3650 });
- this.renderState();
- }
-
- renderState() {
- $(pageSelector)
- .toggleClass(pinnedPageClass, this.isPinned && this.isExpanded)
- .toggleClass(expandedPageClass, this.isExpanded);
- $(navbarSelector)
- .toggleClass(pinnedNavbarClass, this.isPinned && this.isExpanded)
- .toggleClass(expandedNavbarClass, this.isExpanded);
-
- const $pinnedToggle = $(pinnedToggleSelector);
- const tooltipText = this.isPinned ? 'Unpin navigation' : 'Pin navigation';
- const tooltipState = $pinnedToggle.attr('aria-describedby') && this.isExpanded ? 'show' : 'hide';
- $pinnedToggle.attr('title', tooltipText).tooltip('fixTitle').tooltip(tooltipState);
-
- if (this.isExpanded) {
- const sidebarContent = $(sidebarContentSelector);
- setTimeout(() => { sidebarContent.niceScroll().updateScrollBar(); }, 200);
- }
- }
- }
-
- 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 3ee0c73a8d2..294d087554e 100644
--- a/app/assets/javascripts/single_file_diff.js
+++ b/app/assets/javascripts/single_file_diff.js
@@ -95,4 +95,4 @@
}
});
};
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/snippet/snippet_bundle.js b/app/assets/javascripts/snippet/snippet_bundle.js
index 64f9065be42..a98403f4cf2 100644
--- a/app/assets/javascripts/snippet/snippet_bundle.js
+++ b/app/assets/javascripts/snippet/snippet_bundle.js
@@ -1,10 +1,6 @@
/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, quotes, max-len */
/* global ace */
-// 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");
@@ -13,4 +9,4 @@ requireAll(require.context('.', false, /^\.\/(?!snippet_bundle).*\.(js|es6)$/));
$(".snippet-file-content").val(editor.getValue());
});
});
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/star.js b/app/assets/javascripts/star.js
index 531fd0e9c32..c75b44cc2fd 100644
--- a/app/assets/javascripts/star.js
+++ b/app/assets/javascripts/star.js
@@ -27,4 +27,4 @@
return Star;
})();
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/subscription_select.js b/app/assets/javascripts/subscription_select.js
index 187356f0bf9..8b25f43ffc7 100644
--- a/app/assets/javascripts/subscription_select.js
+++ b/app/assets/javascripts/subscription_select.js
@@ -31,4 +31,4 @@
return SubscriptionSelect;
})();
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/syntax_highlight.js b/app/assets/javascripts/syntax_highlight.js
index 115716bff6a..7c063fae045 100644
--- a/app/assets/javascripts/syntax_highlight.js
+++ b/app/assets/javascripts/syntax_highlight.js
@@ -24,4 +24,4 @@
}
}
};
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/task_list.js b/app/assets/javascripts/task_list.js
new file mode 100644
index 00000000000..b1402c0a880
--- /dev/null
+++ b/app/assets/javascripts/task_list.js
@@ -0,0 +1,52 @@
+/* global Flash */
+require('vendor/task_list');
+
+class TaskList {
+ constructor(options = {}) {
+ this.selector = options.selector;
+ this.dataType = options.dataType;
+ this.fieldName = options.fieldName;
+ this.onSuccess = options.onSuccess || (() => {});
+ this.onError = function showFlash(response) {
+ let errorMessages = '';
+
+ if (response.responseJSON) {
+ errorMessages = response.responseJSON.errors.join(' ');
+ }
+
+ return new Flash(errorMessages || 'Update failed', 'alert');
+ };
+
+ this.init();
+ }
+
+ init() {
+ // Prevent duplicate event bindings
+ this.disable();
+ $(`${this.selector} .js-task-list-container`).taskList('enable');
+ $(document).on('tasklist:changed', `${this.selector} .js-task-list-container`, this.update.bind(this));
+ }
+
+ disable() {
+ $(`${this.selector} .js-task-list-container`).taskList('disable');
+ $(document).off('tasklist:changed', `${this.selector} .js-task-list-container`);
+ }
+
+ update(e) {
+ const $target = $(e.target);
+ const patchData = {};
+ patchData[this.dataType] = {
+ [this.fieldName]: $target.val(),
+ };
+ return $.ajax({
+ type: 'PATCH',
+ url: $target.data('update-url') || $('form.js-issuable-update').attr('action'),
+ data: patchData,
+ success: this.onSuccess,
+ error: this.onError,
+ });
+ }
+}
+
+window.gl = window.gl || {};
+window.gl.TaskList = TaskList;
diff --git a/app/assets/javascripts/todos.js.es6 b/app/assets/javascripts/todos.js.es6
index ded683f2ca1..e9513725d9d 100644
--- a/app/assets/javascripts/todos.js.es6
+++ b/app/assets/javascripts/todos.js.es6
@@ -1,28 +1,34 @@
-/* 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 */
+/* eslint-disable class-methods-use-this, no-new, func-names, no-unneeded-ternary, object-shorthand, quote-props, no-param-reassign, max-len */
/* global UsersSelect */
((global) => {
class Todos {
- constructor({ el } = {}) {
- this.allDoneClicked = this.allDoneClicked.bind(this);
- this.doneClicked = this.doneClicked.bind(this);
- this.el = el || $('.js-todos-options');
- this.perPage = this.el.data('perPage');
- this.clearListeners();
- this.initBtnListeners();
+ constructor() {
this.initFilters();
+ this.bindEvents();
+
+ this.cleanupWrapper = this.cleanup.bind(this);
+ document.addEventListener('beforeunload', this.cleanupWrapper);
}
- clearListeners() {
- $('.done-todo').off('click');
- $('.js-todos-mark-all').off('click');
- return $('.todo').off('click');
+ cleanup() {
+ this.unbindEvents();
+ document.removeEventListener('beforeunload', this.cleanupWrapper);
}
- initBtnListeners() {
- $('.done-todo').on('click', this.doneClicked);
- $('.js-todos-mark-all').on('click', this.allDoneClicked);
- return $('.todo').on('click', this.goToTodoUrl);
+ unbindEvents() {
+ $('.js-done-todo, .js-undo-todo').off('click', this.updateStateClickedWrapper);
+ $('.js-todos-mark-all').off('click', this.allDoneClickedWrapper);
+ $('.todo').off('click', this.goToTodoUrl);
+ }
+
+ bindEvents() {
+ this.updateStateClickedWrapper = this.updateStateClicked.bind(this);
+ this.allDoneClickedWrapper = this.allDoneClicked.bind(this);
+
+ $('.js-done-todo, .js-undo-todo').on('click', this.updateStateClickedWrapper);
+ $('.js-todos-mark-all').on('click', this.allDoneClickedWrapper);
+ $('.todo').on('click', this.goToTodoUrl);
}
initFilters() {
@@ -33,7 +39,7 @@
$('form.filter-form').on('submit', function (event) {
event.preventDefault();
- gl.utils.visitUrl(this.action + '&' + $(this).serialize());
+ gl.utils.visitUrl(`${this.action}&${$(this).serialize()}`);
});
}
@@ -44,105 +50,72 @@
filterable: searchFields ? true : false,
search: { fields: searchFields },
data: $dropdown.data('data'),
- clicked: function() {
+ clicked: function () {
return $dropdown.closest('form.filter-form').submit();
- }
+ },
});
}
- doneClicked(e) {
+ updateStateClicked(e) {
e.preventDefault();
- e.stopImmediatePropagation();
- const $target = $(e.currentTarget);
- $target.disable();
- return $.ajax({
+ const target = e.target;
+ target.setAttribute('disabled', '');
+ target.classList.add('disabled');
+ $.ajax({
type: 'POST',
- url: $target.attr('href'),
+ url: target.getAttribute('href'),
dataType: 'json',
data: {
- '_method': 'delete'
+ '_method': target.getAttribute('data-method'),
},
success: (data) => {
- this.redirectIfNeeded(data.count);
- this.clearDone($target.closest('li'));
- return this.updateBadges(data);
- }
+ this.updateState(target);
+ this.updateBadges(data);
+ },
});
}
allDoneClicked(e) {
e.preventDefault();
- e.stopImmediatePropagation();
const $target = $(e.currentTarget);
$target.disable();
- return $.ajax({
+ $.ajax({
type: 'POST',
url: $target.attr('href'),
dataType: 'json',
data: {
- '_method': 'delete'
+ '_method': 'delete',
},
success: (data) => {
$target.remove();
$('.js-todos-all').html('<div class="nothing-here-block">You\'re all done!</div>');
- return this.updateBadges(data);
- }
+ this.updateBadges(data);
+ },
});
}
- clearDone($row) {
- const $ul = $row.closest('ul');
- $row.remove();
- if (!$ul.find('li').length) {
- return $ul.parents('.panel').remove();
+ updateState(target) {
+ const row = target.closest('li');
+ const restoreBtn = row.querySelector('.js-undo-todo');
+ const doneBtn = row.querySelector('.js-done-todo');
+
+ target.removeAttribute('disabled');
+ target.classList.remove('disabled');
+ target.classList.add('hidden');
+
+ if (target === doneBtn) {
+ row.classList.add('done-reversible');
+ restoreBtn.classList.remove('hidden');
+ } else {
+ row.classList.remove('done-reversible');
+ doneBtn.classList.remove('hidden');
}
}
updateBadges(data) {
$(document).trigger('todo:toggle', data.count);
$('.todos-pending .badge').text(data.count);
- return $('.todos-done .badge').text(data.done_count);
- }
-
- getTotalPages() {
- return this.el.data('totalPages');
- }
-
- getCurrentPage() {
- return this.el.data('currentPage');
- }
-
- getTodosPerPage() {
- return this.el.data('perPage');
- }
-
- redirectIfNeeded(total) {
- const currPages = this.getTotalPages();
- const currPage = this.getCurrentPage();
-
- // Refresh if no remaining Todos
- if (!total) {
- window.location.reload();
- return;
- }
- // Do nothing if no pagination
- if (!currPages) {
- return;
- }
-
- const newPages = Math.ceil(total / this.getTodosPerPage());
- let url = location.href;
-
- if (newPages !== currPages) {
- // Redirect to previous page if there's one available
- if (currPages > 1 && currPage === currPages) {
- const pageParams = {
- page: currPages - 1
- };
- url = gl.utils.mergeUrlParams(pageParams, url);
- }
- return gl.utils.visitUrl(url);
- }
+ $('.todos-done .badge').text(data.done_count);
}
goToTodoUrl(e) {
@@ -159,12 +132,12 @@
if (selected.tagName === 'IMG') {
const avatarUrl = selected.parentElement.getAttribute('href');
- return window.open(avatarUrl, windowTarget);
+ window.open(avatarUrl, windowTarget);
} else {
- return window.open(todoLink, windowTarget);
+ window.open(todoLink, windowTarget);
}
} else {
- return gl.utils.visitUrl(todoLink);
+ gl.utils.visitUrl(todoLink);
}
}
}
diff --git a/app/assets/javascripts/tree.js b/app/assets/javascripts/tree.js
index b1b35fdbd6c..76a821c7a17 100644
--- a/app/assets/javascripts/tree.js
+++ b/app/assets/javascripts/tree.js
@@ -65,4 +65,4 @@
return TreeView;
})();
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/u2f/error.js b/app/assets/javascripts/u2f/error.js
index 86b459e1866..fd1829efe18 100644
--- a/app/assets/javascripts/u2f/error.js
+++ b/app/assets/javascripts/u2f/error.js
@@ -24,4 +24,4 @@
return U2FError;
})();
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/u2f/register.js b/app/assets/javascripts/u2f/register.js
index 69d1ff3a39e..17631f2908d 100644
--- a/app/assets/javascripts/u2f/register.js
+++ b/app/assets/javascripts/u2f/register.js
@@ -95,4 +95,4 @@
return U2FRegister;
})();
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/u2f/util.js b/app/assets/javascripts/u2f/util.js
index 34e88220b12..813d363db00 100644
--- a/app/assets/javascripts/u2f/util.js
+++ b/app/assets/javascripts/u2f/util.js
@@ -9,4 +9,4 @@
return U2FUtil;
})();
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/user_callout.js b/app/assets/javascripts/user_callout.js
new file mode 100644
index 00000000000..74b869502a4
--- /dev/null
+++ b/app/assets/javascripts/user_callout.js
@@ -0,0 +1,58 @@
+/* global Cookies */
+
+const userCalloutElementName = '.user-callout';
+const closeButton = '.close-user-callout';
+const userCalloutBtn = '.user-callout-btn';
+const userCalloutSvgAttrName = 'callout-svg';
+
+const USER_CALLOUT_COOKIE = 'user_callout_dismissed';
+
+const USER_CALLOUT_TEMPLATE = `
+ <div class="bordered-box landing content-block">
+ <button class="btn btn-default close close-user-callout" type="button">
+ <i class="fa fa-times dismiss-icon"></i>
+ </button>
+ <div class="row">
+ <div class="col-sm-3 col-xs-12 svg-container">
+ </div>
+ <div class="col-sm-8 col-xs-12 inner-content">
+ <h4>
+ Customize your experience
+ </h4>
+ <p>
+ Change syntax themes, default project pages, and more in preferences.
+ </p>
+ <a class="btn user-callout-btn" href="/profile/preferences">Check it out</a>
+ </div>
+ </div>
+</div>`;
+
+class UserCallout {
+ constructor() {
+ this.isCalloutDismissed = Cookies.get(USER_CALLOUT_COOKIE);
+ this.userCalloutBody = $(userCalloutElementName);
+ this.userCalloutSvg = $(userCalloutElementName).attr(userCalloutSvgAttrName);
+ $(userCalloutElementName).removeAttr(userCalloutSvgAttrName);
+ this.init();
+ }
+
+ init() {
+ const $template = $(USER_CALLOUT_TEMPLATE);
+ if (!this.isCalloutDismissed || this.isCalloutDismissed === 'false') {
+ $template.find('.svg-container').append(this.userCalloutSvg);
+ this.userCalloutBody.append($template);
+ $template.find(closeButton).on('click', e => this.dismissCallout(e));
+ $template.find(userCalloutBtn).on('click', e => this.dismissCallout(e));
+ }
+ }
+
+ dismissCallout(e) {
+ Cookies.set(USER_CALLOUT_COOKIE, 'true');
+ const $currentTarget = $(e.currentTarget);
+ if ($currentTarget.hasClass('close-user-callout')) {
+ this.userCalloutBody.empty();
+ }
+ }
+}
+
+module.exports = UserCallout;
diff --git a/app/assets/javascripts/users/calendar.js b/app/assets/javascripts/users/calendar.js
index 6e40dfdf3d8..5111b260e1c 100644
--- a/app/assets/javascripts/users/calendar.js
+++ b/app/assets/javascripts/users/calendar.js
@@ -221,4 +221,4 @@
return Calendar;
})();
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/users/users_bundle.js b/app/assets/javascripts/users/users_bundle.js
index 4cad60a59b1..580e2d84be5 100644
--- a/app/assets/javascripts/users/users_bundle.js
+++ b/app/assets/javascripts/users/users_bundle.js
@@ -1,3 +1 @@
-// require everything else in this directory
-function requireAll(context) { return context.keys().map(context); }
-requireAll(require.context('.', false, /^\.\/(?!users_bundle).*\.(js|es6)$/));
+require('./calendar');
diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js
index d4b24d13299..de33a31b411 100644
--- a/app/assets/javascripts/users_select.js
+++ b/app/assets/javascripts/users_select.js
@@ -432,4 +432,4 @@
return UsersSelect;
})();
-}).call(this);
+}).call(window);
diff --git a/app/assets/javascripts/version_check_image.js.es6 b/app/assets/javascripts/version_check_image.js.es6
index 1fa2b5ac399..d4f716acb72 100644
--- a/app/assets/javascripts/version_check_image.js.es6
+++ b/app/assets/javascripts/version_check_image.js.es6
@@ -1,10 +1,10 @@
-(() => {
- class VersionCheckImage {
- static bindErrorEvent(imageElement) {
- imageElement.off('error').on('error', () => imageElement.hide());
- }
+class VersionCheckImage {
+ static bindErrorEvent(imageElement) {
+ imageElement.off('error').on('error', () => imageElement.hide());
}
+}
- window.gl = window.gl || {};
- gl.VersionCheckImage = VersionCheckImage;
-})();
+window.gl = window.gl || {};
+gl.VersionCheckImage = VersionCheckImage;
+
+module.exports = VersionCheckImage;
diff --git a/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 b/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6
index 8106934e864..b50afe7c594 100644
--- a/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6
+++ b/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6
@@ -1,5 +1,5 @@
/* global Vue, Flash, gl */
-/* eslint-disable no-param-reassign */
+/* eslint-disable no-param-reassign, no-alert */
((gl) => {
gl.VuePipelineActions = Vue.extend({
@@ -16,21 +16,33 @@
download(name) {
return `Download ${name} artifacts`;
},
+
+ /**
+ * Shows a dialog when the user clicks in the cancel button.
+ * We need to prevent the default behavior and stop propagation because the
+ * link relies on UJS.
+ *
+ * @param {Event} event
+ */
+ confirmAction(event) {
+ if (!confirm('Are you sure you want to cancel this pipeline?')) {
+ event.preventDefault();
+ event.stopPropagation();
+ }
+ },
},
template: `
- <td class="pipeline-actions hidden-xs">
- <div class="controls pull-right">
- <div class="btn-group inline">
- <div class="btn-group">
+ <td class="pipeline-actions">
+ <div class="pull-right">
+ <div class="btn-group">
+ <div class="btn-group" v-if="actions">
<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>
+ 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">
@@ -38,23 +50,21 @@
<a
rel="nofollow"
data-method="post"
- :href='action.path'
- >
- <span v-html='svgs.iconPlay' aria-hidden="true"></span>
+ :href="action.path">
+ <span v-html="svgs.iconPlay" aria-hidden="true"></span>
<span>{{action.name}}</span>
</a>
</li>
</ul>
</div>
- <div class="btn-group">
+
+ <div class="btn-group" v-if="artifacts">
<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"
- >
+ aria-label="Artifacts">
<i class="fa fa-download" aria-hidden="true"></i>
<i class="fa fa-caret-down" aria-hidden="true"></i>
</button>
@@ -62,40 +72,39 @@
<li v-for='artifact in pipeline.details.artifacts'>
<a
rel="nofollow"
- :href='artifact.path'
- >
+ :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 class="btn-group" v-if="pipeline.flags.retryable">
+ <a
+ class="btn btn-default btn-retry 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>
+ </div>
+ <div class="btn-group" v-if="pipeline.flags.cancelable">
+ <a
+ 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>
</div>
</td>
diff --git a/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6 b/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6
index 39935b08dc0..9275cdf78f7 100644
--- a/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6
+++ b/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6
@@ -5,6 +5,7 @@ window.Vue = require('vue');
require('../vue_shared/components/table_pagination');
require('./store');
require('../vue_shared/components/pipelines_table');
+const CommitPipelinesStoreWithTimeAgo = require('../commit/pipelines/pipelines_store');
((gl) => {
gl.VuePipelines = Vue.extend({
@@ -22,30 +23,37 @@ require('../vue_shared/components/pipelines_table');
apiScope: 'all',
pageInfo: {},
pagenum: 1,
- count: { all: 0, running_or_pending: 0 },
+ count: {},
pageRequest: false,
};
},
props: ['scope', 'store', 'svgs'],
created() {
- const pagenum = gl.utils.getParameterByName('p');
+ const pagenum = gl.utils.getParameterByName('page');
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: {
+ beforeUpdate() {
+ if (this.pipelines.length && this.$children) {
+ CommitPipelinesStoreWithTimeAgo.startTimeAgoLoops.call(this, Vue);
+ }
+ },
+
+ methods: {
/**
- * Changes the URL according to the pagination component.
- *
- * If no scope is provided, 'all' is assumed.
+ * Will change the page number and update the URL.
*
- * @param {Number} pagenum
- * @param {String} apiScope = 'all'
+ * @param {Number} pageNumber desired page to go to.
*/
- change(pagenum, apiScope = 'all') {
- gl.utils.visitUrl(`?scope=${apiScope}&p=${pagenum}`);
+ change(pageNumber) {
+ const param = gl.utils.setParamInURL('page', pageNumber);
+
+ gl.utils.visitUrl(param);
+ return param;
},
},
template: `
diff --git a/app/assets/javascripts/vue_pipelines_index/stage.js.es6 b/app/assets/javascripts/vue_pipelines_index/stage.js.es6
index 8cc417a9966..67fdd729e41 100644
--- a/app/assets/javascripts/vue_pipelines_index/stage.js.es6
+++ b/app/assets/javascripts/vue_pipelines_index/stage.js.es6
@@ -23,6 +23,13 @@
required: true,
},
},
+
+ updated() {
+ if (this.builds) {
+ this.stopDropdownClickPropagation();
+ }
+ },
+
methods: {
fetchBuilds(e) {
const areaExpanded = e.currentTarget.attributes['aria-expanded'];
@@ -37,17 +44,19 @@
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();
+ /**
+ * When the user right clicks or cmd/ctrl + click in the job name
+ * the dropdown should not be closed and the link should open in another tab,
+ * so we stop propagation of the click event inside the dropdown.
+ *
+ * Since this component is rendered multiple times per page we need to guarantee we only
+ * target the click event of this component.
+ */
+ stopDropdownClickPropagation() {
+ $(this.$el.querySelectorAll('.js-builds-dropdown-list a.mini-pipeline-graph-dropdown-item')).on('click', (e) => {
+ e.stopPropagation();
+ });
},
},
computed: {
@@ -76,13 +85,13 @@
template: `
<div>
<button
- @click='fetchBuilds($event)'
+ @click="fetchBuilds($event)"
:class="triggerButtonClass"
- :title='stage.title'
+ :title="stage.title"
data-placement="top"
data-toggle="dropdown"
type="button"
- :aria-label='stage.title'
+ :aria-label="stage.title"
>
<span v-html="svg" aria-hidden="true"></span>
<i class="fa fa-caret-down" aria-hidden="true"></i>
@@ -90,7 +99,6 @@
<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"
diff --git a/app/assets/javascripts/vue_pipelines_index/store.js.es6 b/app/assets/javascripts/vue_pipelines_index/store.js.es6
index 572f0493c9f..909007267b9 100644
--- a/app/assets/javascripts/vue_pipelines_index/store.js.es6
+++ b/app/assets/javascripts/vue_pipelines_index/store.js.es6
@@ -1,59 +1,31 @@
/* global gl, Flash */
-/* eslint-disable no-param-reassign, no-underscore-dangle */
-require('../vue_realtime_listener');
+/* eslint-disable no-param-reassign */
((gl) => {
const pageValues = (headers) => {
const normalized = gl.utils.normalizeHeaders(headers);
- const paginationInfo = gl.utils.normalizeHeaders(normalized);
+ const paginationInfo = gl.utils.parseIntPagination(normalized);
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);
+ return 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);
+ 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);
+ this.pageRequest = false;
+ }, () => {
+ this.pageRequest = false;
+ return new Flash('An error occurred while fetching the pipelines, please reload the page again.');
+ });
}
};
})(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
index 3598da11573..6048fa691dc 100644
--- a/app/assets/javascripts/vue_pipelines_index/time_ago.js.es6
+++ b/app/assets/javascripts/vue_pipelines_index/time_ago.js.es6
@@ -54,7 +54,7 @@ require('../lib/utils/datetime_utility');
},
},
template: `
- <td>
+ <td class="pipelines-time-ago">
<p class="duration" v-if='duration'>
<span v-html='svgs.iconTimer'></span>
{{duration}}
@@ -65,8 +65,7 @@ require('../lib/utils/datetime_utility');
data-toggle="tooltip"
data-placement="top"
data-container="body"
- :data-original-title='localTimeFinished'
- >
+ :data-original-title='localTimeFinished'>
{{timeStopped.words}}
</time>
</p>
diff --git a/app/assets/javascripts/vue_shared/components/pipelines_table.js.es6 b/app/assets/javascripts/vue_shared/components/pipelines_table.js.es6
index 4bdaef31ee9..34d3bbdd80d 100644
--- a/app/assets/javascripts/vue_shared/components/pipelines_table.js.es6
+++ b/app/assets/javascripts/vue_shared/components/pipelines_table.js.es6
@@ -44,7 +44,7 @@ require('./pipelines_table_row');
<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>
+ <th class="js-pipeline-actions pipeline-actions"></th>
</tr>
</thead>
<tbody>
diff --git a/app/assets/javascripts/vue_shared/components/table_pagination.js.es6 b/app/assets/javascripts/vue_shared/components/table_pagination.js.es6
index d8042a9b7fc..dd046405575 100644
--- a/app/assets/javascripts/vue_shared/components/table_pagination.js.es6
+++ b/app/assets/javascripts/vue_shared/components/table_pagination.js.es6
@@ -23,8 +23,8 @@ window.Vue = require('vue');
Here is an example `change` method:
- change(pagenum, apiScope) {
- gl.utils.visitUrl(`?scope=${apiScope}&p=${pagenum}`);
+ change(pagenum) {
+ gl.utils.visitUrl(`?page=${pagenum}`);
},
*/
@@ -57,8 +57,6 @@ window.Vue = require('vue');
},
methods: {
changePage(e) {
- const apiScope = gl.utils.getParameterByName('scope');
-
const text = e.target.innerText;
const { totalPages, nextPage, previousPage } = this.pageInfo;
@@ -66,19 +64,19 @@ window.Vue = require('vue');
case SPREAD:
break;
case LAST:
- this.change(totalPages, apiScope);
+ this.change(totalPages);
break;
case NEXT:
- this.change(nextPage, apiScope);
+ this.change(nextPage);
break;
case PREV:
- this.change(previousPage, apiScope);
+ this.change(previousPage);
break;
case FIRST:
- this.change(1, apiScope);
+ this.change(1);
break;
default:
- this.change(+text, apiScope);
+ this.change(+text);
break;
}
},
diff --git a/app/assets/javascripts/wikis.js.es6 b/app/assets/javascripts/wikis.js.es6
index ef99b2e92f0..75fd1394a03 100644
--- a/app/assets/javascripts/wikis.js.es6
+++ b/app/assets/javascripts/wikis.js.es6
@@ -1,14 +1,10 @@
/* eslint-disable no-param-reassign */
/* global Breakpoints */
-require('vendor/latinise');
require('./breakpoints');
require('vendor/jquery.nicescroll');
((global) => {
- const dasherize = str => str.replace(/[_\s]+/g, '-');
- const slugify = str => dasherize(str.trim().toLowerCase().latinise());
-
class Wikis {
constructor() {
this.bp = Breakpoints.get();
@@ -34,7 +30,7 @@ require('vendor/jquery.nicescroll');
if (!this.newWikiForm) return;
const slugInput = this.newWikiForm.querySelector('#new_wiki_path');
- const slug = slugify(slugInput.value);
+ const slug = gl.text.slugify(slugInput.value);
if (slug.length > 0) {
const wikisPath = slugInput.getAttribute('data-wikis-path');
diff --git a/app/assets/javascripts/zen_mode.js b/app/assets/javascripts/zen_mode.js
index d9261cda1b1..ce626cf7b46 100644
--- a/app/assets/javascripts/zen_mode.js
+++ b/app/assets/javascripts/zen_mode.js
@@ -94,4 +94,4 @@ require('mousetrap/plugins/pause/mousetrap-pause');
return ZenMode;
})();
-}).call(this);
+}).call(window);
diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss
index 1dcd1f8a6fc..83a8eeaafde 100644
--- a/app/assets/stylesheets/application.scss
+++ b/app/assets/stylesheets/application.scss
@@ -2,7 +2,6 @@
* This is a manifest file that'll automatically include all the stylesheets available in this directory
* and any sub-directories. You're free to add application-wide styles to this file and they'll appear at
* the top of the compiled file, but it's generally better to create a new file per style scope.
- *= require jquery-ui/autocomplete
*= require jquery.atwho
*= require select2
*= require_self
diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss
index 08f203a1bf6..39cf3b5f8ae 100644
--- a/app/assets/stylesheets/framework.scss
+++ b/app/assets/stylesheets/framework.scss
@@ -19,7 +19,6 @@
@import "framework/flash.scss";
@import "framework/forms.scss";
@import "framework/gfm.scss";
-@import "framework/gitlab-theme.scss";
@import "framework/header.scss";
@import "framework/highlight.scss";
@import "framework/issue_box.scss";
diff --git a/app/assets/stylesheets/framework/animations.scss b/app/assets/stylesheets/framework/animations.scss
index 0ca5a9343f7..90935b9616b 100644
--- a/app/assets/stylesheets/framework/animations.scss
+++ b/app/assets/stylesheets/framework/animations.scss
@@ -116,7 +116,7 @@
}
.btn,
-.side-nav-toggle {
+.global-dropdown-toggle {
@include transition(background-color, border-color, color, box-shadow);
}
@@ -140,7 +140,6 @@ a {
@include transition(background-color, box-shadow);
}
-.nav-sidebar a,
.dropdown-menu a,
.dropdown-menu button,
.dropdown-menu-nav a {
diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss
index 0f9213b98e3..9a4129cdc8d 100644
--- a/app/assets/stylesheets/framework/blocks.scss
+++ b/app/assets/stylesheets/framework/blocks.scss
@@ -229,7 +229,7 @@
.controls {
float: right;
margin-top: 8px;
- padding-bottom: 7px;
+ padding-bottom: 8px;
border-bottom: 1px solid $border-color;
}
}
diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss
index e3da467a27c..d2be8dc7a39 100644
--- a/app/assets/stylesheets/framework/filters.scss
+++ b/app/assets/stylesheets/framework/filters.scss
@@ -26,6 +26,11 @@
.filtered-search-container {
display: -webkit-flex;
display: flex;
+
+ @media (max-width: $screen-xs-min) {
+ -webkit-flex-direction: column;
+ flex-direction: column;
+ }
}
.filtered-search-input-container {
@@ -34,6 +39,20 @@
position: relative;
width: 100%;
+ @media (max-width: $screen-xs-min) {
+ -webkit-flex: 1 1 100%;
+ flex: 1 1 100%;
+ margin-bottom: 10px;
+
+ .dropdown-menu {
+ width: auto;
+ left: 0;
+ right: 0;
+ max-width: none;
+ min-width: 100%;
+ }
+ }
+
.form-control {
padding-left: 25px;
padding-right: 25px;
@@ -79,6 +98,31 @@
overflow: auto;
}
+@media (max-width: $screen-xs-min) {
+ .issues-details-filters {
+ padding: 0 0 10px;
+ background-color: $white-light;
+ border-top: 0;
+ }
+
+ .filter-dropdown-container {
+ .dropdown-toggle,
+ .dropdown {
+ width: 100%;
+ }
+
+ .dropdown {
+ margin-left: 0;
+ }
+
+ .fa-chevron-down {
+ position: absolute;
+ right: 10px;
+ top: 10px;
+ }
+ }
+}
+
%filter-dropdown-item-btn-hover {
background-color: $dropdown-hover-color;
color: $white-light;
@@ -148,4 +192,4 @@
.filter-dropdown-loading {
padding: 8px 16px;
-}
+} \ No newline at end of file
diff --git a/app/assets/stylesheets/framework/gitlab-theme.scss b/app/assets/stylesheets/framework/gitlab-theme.scss
deleted file mode 100644
index d6566dc4ec9..00000000000
--- a/app/assets/stylesheets/framework/gitlab-theme.scss
+++ /dev/null
@@ -1,144 +0,0 @@
-/**
- * Styles the GitLab application with a specific color theme
- *
- * $color-light -
- * $color -
- * $color-darker -
- * $color-dark -
- */
-@mixin gitlab-theme($color-light, $color, $color-darker, $color-dark) {
- .page-with-sidebar {
- .toggle-nav-collapse,
- .pin-nav-btn {
- color: $color-light;
-
- &:hover {
- color: $white-light;
- }
- }
-
- .sidebar-wrapper {
- background: $color-darker;
- }
-
- .sidebar-action-buttons {
- color: $color-light;
- background-color: lighten($color-darker, 5%);
- }
-
- .nav-sidebar {
- li {
- a {
- color: $color-light;
-
- &:hover,
- &:focus,
- &:active {
- background: $color-dark;
- }
-
- i {
- color: $color-light;
- }
-
- path,
- polygon {
- fill: $color-light;
- }
-
- .count {
- color: $color-light;
- background: $color-dark;
- }
-
- svg {
- position: relative;
- top: 3px;
- }
- }
-
- &.separate-item {
- border-top: 1px solid $color;
- }
-
- &.active a {
- color: $white-light;
- background: $color-dark;
-
- &.no-highlight {
- border: none;
- }
-
- i {
- color: $white-light;
- }
-
- path,
- polygon {
- fill: $white-light;
- }
- }
- }
-
- .about-gitlab {
- color: $color-light;
- }
- }
- }
-}
-
-$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-light: #ccc;
-$theme-graphite: #777;
-$theme-graphite-dark: #666;
-$theme-graphite-darker: #555;
-
-$theme-black-light: #979797;
-$theme-black: #373737;
-$theme-black-dark: #272727;
-$theme-black-darker: #222;
-
-$theme-green-light: #adc;
-$theme-green: #019875;
-$theme-green-dark: #018865;
-$theme-green-darker: #017855;
-
-$theme-violet-light: #98c;
-$theme-violet: #548;
-$theme-violet-dark: #436;
-$theme-violet-darker: #325;
-
-body {
- &.ui_blue {
- @include gitlab-theme($theme-blue-light, $theme-blue, $theme-blue-dark, $theme-blue-darker);
- }
-
- &.ui_charcoal {
- @include gitlab-theme($theme-charcoal-light, $theme-charcoal, $theme-charcoal-dark, $theme-charcoal-darker);
- }
-
- &.ui_graphite {
- @include gitlab-theme($theme-graphite-light, $theme-graphite, $theme-graphite-dark, $theme-graphite-darker);
- }
-
- &.ui_black {
- @include gitlab-theme($theme-black-light, $theme-black, $theme-black-dark, $theme-black-darker);
- }
-
- &.ui_green {
- @include gitlab-theme($theme-green-light, $theme-green, $theme-green-dark, $theme-green-darker);
- }
-
- &.ui_violet {
- @include gitlab-theme($theme-violet-light, $theme-violet, $theme-violet-dark, $theme-violet-darker);
- }
-}
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index 34e010e0e8a..685a4847731 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -100,23 +100,40 @@ header {
}
}
}
+ }
- .side-nav-toggle {
- position: absolute;
- left: -10px;
- margin: 7px 0;
- font-size: 18px;
- padding: 6px 10px;
- border: none;
- background-color: $gray-light;
+ .global-dropdown {
+ position: absolute;
+ left: -10px;
- &:hover {
- background-color: $white-normal;
- color: $gl-header-nav-hover-color;
+ .badge {
+ font-size: 11px;
+ }
+
+ li {
+ &.active a {
+ font-weight: bold;
}
}
}
+ .global-dropdown-toggle {
+ margin: 7px 0;
+ font-size: 18px;
+ padding: 6px 10px;
+ border: none;
+ background-color: $gray-light;
+
+ &:hover {
+ background-color: $white-normal;
+ }
+
+ &:focus {
+ outline: none;
+ background-color: $white-normal;
+ }
+ }
+
.header-content {
position: relative;
height: $header-height;
@@ -131,16 +148,11 @@ header {
}
.header-logo {
- position: absolute;
- left: 50%;
+ display: inline-block;
+ margin: 0 8px 0 3px;
+ position: relative;
top: 7px;
transition-duration: .3s;
- z-index: 999;
-
- #logo {
- position: relative;
- left: -50%;
- }
svg,
img {
@@ -150,15 +162,6 @@ header {
&:hover {
cursor: pointer;
}
-
- @media (max-width: $screen-xs-max) {
- right: 20px;
- left: auto;
-
- #logo {
- left: auto;
- }
- }
}
.title {
@@ -166,7 +169,6 @@ header {
padding-right: 20px;
margin: 0;
font-size: 18px;
- max-width: 385px;
display: inline-block;
line-height: $header-height;
font-weight: normal;
@@ -176,14 +178,18 @@ header {
vertical-align: top;
white-space: nowrap;
- @media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
- max-width: 300px;
- }
-
@media (max-width: $screen-xs-max) {
max-width: 190px;
}
+ @media (min-width: $screen-sm-min) and (max-width: $screen-md-max) {
+ max-width: 428px;
+ }
+
+ @media (min-width: $screen-lg-min) {
+ max-width: 685px;
+ }
+
a {
color: $gl-text-color;
diff --git a/app/assets/stylesheets/framework/jquery.scss b/app/assets/stylesheets/framework/jquery.scss
index d335fedefe2..300ba4f2de6 100644
--- a/app/assets/stylesheets/framework/jquery.scss
+++ b/app/assets/stylesheets/framework/jquery.scss
@@ -2,17 +2,6 @@
font-family: $regular_font;
font-size: $font-size-base;
- &.ui-autocomplete {
- border-color: $jq-ui-border;
- padding: 0;
- margin-top: 2px;
- z-index: 1001;
-
- .ui-menu-item a {
- padding: 4px 10px;
- }
- }
-
.ui-state-default {
border: 1px solid $white-light;
background: $white-light;
diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss
index 2bfdb9f9601..55ed4b7b06c 100644
--- a/app/assets/stylesheets/framework/lists.scss
+++ b/app/assets/stylesheets/framework/lists.scss
@@ -96,16 +96,6 @@ ul.unstyled-list > li {
border-bottom: none;
}
-ul.task-list {
- li.task-list-item {
- list-style-type: none;
- }
-
- ul:not(.task-list) {
- padding-left: 1.3em;
- }
-}
-
// Generic content list
ul.content-list {
@include basic-list;
diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss
index 1acd06122a3..df78bbdea51 100644
--- a/app/assets/stylesheets/framework/mixins.scss
+++ b/app/assets/stylesheets/framework/mixins.scss
@@ -76,6 +76,13 @@
#{$property}: $value;
}
+/* http://phrappe.com/css/conditional-css-for-webkit-based-browsers/ */
+@mixin on-webkit-only {
+ @media screen and (-webkit-min-device-pixel-ratio:0) {
+ @content;
+ }
+}
+
@mixin keyframes($animation-name) {
@-webkit-keyframes #{$animation-name} {
@content;
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index 20bcb1eeb23..8978e284f55 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -1,36 +1,3 @@
-.page-with-sidebar {
- padding-bottom: 25px;
- transition: padding $sidebar-transition-duration;
-
- &.page-sidebar-pinned {
- .sidebar-wrapper {
- box-shadow: none;
- }
- }
-
- .sidebar-wrapper {
- position: fixed;
- top: 0;
- bottom: 0;
- left: 0;
- height: 100%;
- width: 0;
- overflow: hidden;
- transition: width $sidebar-transition-duration;
- box-shadow: 2px 0 16px 0 $black-transparent;
- }
-}
-
-.sidebar-wrapper {
- z-index: 1000;
- background: $gray-light;
-
- .nicescroll-rails-hr {
- // TODO: Figure out why nicescroll doesn't hide horizontal bar
- display: none!important;
- }
-}
-
.content-wrapper {
width: 100%;
transition: padding $sidebar-transition-duration;
@@ -47,105 +14,6 @@
}
}
-.nav-sidebar {
- position: absolute;
- top: 50px;
- bottom: 0;
- width: $sidebar_width;
- overflow-y: auto;
- overflow-x: hidden;
-
- &.navbar-collapse {
- padding: 0 !important;
- }
-
- li {
- &.separate-item {
- padding-top: 10px;
- margin-top: 10px;
- }
-
- .icon-container {
- width: 34px;
- display: inline-block;
- text-align: center;
- }
-
- a {
- padding: 7px $gl-sidebar-padding;
- font-size: $gl-font-size;
- line-height: 24px;
- display: block;
- text-decoration: none;
- font-weight: normal;
-
- &:hover,
- &:active,
- &:focus {
- text-decoration: none;
- }
-
- i {
- font-size: 16px;
- }
-
- i,
- svg {
- margin-right: 13px;
- }
- }
- }
-
- .count {
- float: right;
- 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 {
- width: $sidebar_width;
- position: absolute;
- top: 0;
- left: 0;
- min-height: 50px;
- padding: 5px 0;
- font-size: 18px;
- line-height: 30px;
-
- .toggle-nav-collapse {
- left: 0;
- }
-
- .pin-nav-btn {
- right: 0;
- display: none;
-
- @media (min-width: $sidebar-breakpoint) {
- display: block;
- }
-
- .fa {
- transition: transform .15s;
-
- .page-sidebar-pinned & {
- transform: rotate(90deg);
- }
- }
- }
-}
-
.nav-header-btn {
padding: 10px $gl-sidebar-padding;
color: inherit;
@@ -161,59 +29,16 @@
}
}
-.page-sidebar-expanded {
- .sidebar-wrapper {
- width: $sidebar_width;
- }
-}
-
-.page-sidebar-pinned {
- .content-wrapper,
- .layout-nav {
- @media (min-width: $sidebar-breakpoint) {
- padding-left: $sidebar_width;
- }
- }
-
- .merge-request-tabs-holder.affix {
- @media (min-width: $sidebar-breakpoint) {
- left: $sidebar_width;
- }
- }
-
- &.right-sidebar-expanded {
- .line-resolve-all-container {
- @media (min-width: $sidebar-breakpoint) {
- display: none;
- }
- }
- }
-}
-
-header.header-sidebar-pinned {
- @media (min-width: $sidebar-breakpoint) {
- padding-left: ($sidebar_width + $gl-padding);
-
- .side-nav-toggle {
- display: none;
- }
-
- .header-content {
- padding-left: 0;
- }
- }
-}
-
.right-sidebar-collapsed {
padding-right: 0;
@media (min-width: $screen-sm-min) {
.content-wrapper {
- padding-right: $sidebar_collapsed_width;
+ padding-right: $gutter_collapsed_width;
}
.merge-request-tabs-holder.affix {
- right: $sidebar_collapsed_width;
+ right: $gutter_collapsed_width;
}
}
@@ -230,8 +55,8 @@ header.header-sidebar-pinned {
padding-right: 0;
@media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
- &:not(.build-sidebar):not(.wiki-sidebar) {
- padding-right: $sidebar_collapsed_width;
+ .content-wrapper {
+ padding-right: $gutter_collapsed_width;
}
}
@@ -245,12 +70,12 @@ header.header-sidebar-pinned {
}
&.with-overlay .merge-request-tabs-holder.affix {
- right: $sidebar_collapsed_width;
+ right: $gutter_collapsed_width;
}
}
&.with-overlay {
- padding-right: $sidebar_collapsed_width;
+ padding-right: $gutter_collapsed_width;
}
}
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index 54958973f15..db5e2c51fe7 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -134,7 +134,7 @@
ul,
ol {
padding: 0;
- margin: 3px 0 3px 28px !important;
+ margin: 3px 0 !important;
}
ul:dir(rtl),
@@ -144,6 +144,29 @@
li {
line-height: 1.6em;
+ margin-left: 25px;
+ padding-left: 3px;
+
+ /* Normalize the bullet position on webkit. */
+ @include on-webkit-only {
+ margin-left: 28px;
+ padding-left: 0;
+ }
+ }
+
+ ul.task-list {
+ li.task-list-item {
+ list-style-type: none;
+ position: relative;
+ padding-left: 28px;
+ margin-left: 0 !important;
+
+ input.task-list-item-checkbox {
+ position: absolute;
+ left: 8px;
+ top: 5px;
+ }
+ }
}
a[href*="/uploads/"],
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 7809d4866f1..ba0af072716 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -1,8 +1,6 @@
/*
* Layout
*/
-$sidebar_collapsed_width: 62px;
-$sidebar_width: 220px;
$gutter_collapsed_width: 62px;
$gutter_width: 290px;
$gutter_inner_width: 250px;
@@ -541,4 +539,4 @@ Pipeline Graph
*/
$stage-hover-bg: #eaf3fc;
$stage-hover-border: #d1e7fc;
-$action-icon-color: #d6d6d6; \ No newline at end of file
+$action-icon-color: #d6d6d6;
diff --git a/app/assets/stylesheets/highlight/dark.scss b/app/assets/stylesheets/highlight/dark.scss
index 6f2e746d4b0..9c76e58dfc8 100644
--- a/app/assets/stylesheets/highlight/dark.scss
+++ b/app/assets/stylesheets/highlight/dark.scss
@@ -20,6 +20,7 @@ $dark-highlight-bg: #ffe792;
$dark-highlight-color: $black;
$dark-pre-hll-bg: #373b41;
$dark-hll-bg: #373b41;
+$dark-over-bg: #9f9ab5;
$dark-c: #969896;
$dark-err: #c66;
$dark-k: #b294bb;
@@ -139,6 +140,18 @@ $dark-il: #de935f;
}
}
+ .diff-line-num {
+ &.is-over,
+ &.hll:not(.empty-cell).is-over {
+ background-color: $dark-over-bg;
+ border-color: darken($dark-over-bg, 5%);
+
+ a {
+ color: darken($dark-over-bg, 15%);
+ }
+ }
+ }
+
.line_content.match {
@include dark-diff-match-line;
}
diff --git a/app/assets/stylesheets/highlight/monokai.scss b/app/assets/stylesheets/highlight/monokai.scss
index 2144a5f7466..6488a099c74 100644
--- a/app/assets/stylesheets/highlight/monokai.scss
+++ b/app/assets/stylesheets/highlight/monokai.scss
@@ -13,6 +13,7 @@ $monokai-line-empty-bg: #49483e;
$monokai-line-empty-border: darken($monokai-line-empty-bg, 15%);
$monokai-diff-border: #808080;
$monokai-highlight-bg: #ffe792;
+$monokai-over-bg: #9f9ab5;
$monokai-new-bg: rgba(166, 226, 46, 0.1);
$monokai-new-idiff: rgba(166, 226, 46, 0.15);
@@ -139,6 +140,18 @@ $monokai-gi: #a6e22e;
}
}
+ .diff-line-num {
+ &.is-over,
+ &.hll:not(.empty-cell).is-over {
+ background-color: $monokai-over-bg;
+ border-color: darken($monokai-over-bg, 5%);
+
+ a {
+ color: darken($monokai-over-bg, 15%);
+ }
+ }
+ }
+
.line_content.match {
@include dark-diff-match-line;
}
diff --git a/app/assets/stylesheets/highlight/solarized_dark.scss b/app/assets/stylesheets/highlight/solarized_dark.scss
index 2cb1d18f12f..00079cc2b5b 100644
--- a/app/assets/stylesheets/highlight/solarized_dark.scss
+++ b/app/assets/stylesheets/highlight/solarized_dark.scss
@@ -17,6 +17,7 @@ $solarized-dark-line-color-new: #5a766c;
$solarized-dark-line-color-old: #7a6c71;
$solarized-dark-highlight: #094554;
$solarized-dark-hll-bg: #174652;
+$solarized-dark-over-bg: #9f9ab5;
$solarized-dark-c: #586e75;
$solarized-dark-err: #93a1a1;
$solarized-dark-g: #93a1a1;
@@ -143,6 +144,18 @@ $solarized-dark-il: #2aa198;
}
}
+ .diff-line-num {
+ &.is-over,
+ &.hll:not(.empty-cell).is-over {
+ background-color: $solarized-dark-over-bg;
+ border-color: darken($solarized-dark-over-bg, 5%);
+
+ a {
+ color: darken($solarized-dark-over-bg, 15%);
+ }
+ }
+ }
+
.line_content.match {
@include dark-diff-match-line;
}
diff --git a/app/assets/stylesheets/highlight/solarized_light.scss b/app/assets/stylesheets/highlight/solarized_light.scss
index b72c4326730..2e3b68f1a58 100644
--- a/app/assets/stylesheets/highlight/solarized_light.scss
+++ b/app/assets/stylesheets/highlight/solarized_light.scss
@@ -18,6 +18,7 @@ $solarized-light-line-color-new: #a1a080;
$solarized-light-line-color-old: #ad9186;
$solarized-light-highlight: #eee8d5;
$solarized-light-hll-bg: #ddd8c5;
+$solarized-light-over-bg: #ded7fc;
$solarized-light-c: #93a1a1;
$solarized-light-err: #586e75;
$solarized-light-g: #586e75;
@@ -150,6 +151,18 @@ $solarized-light-il: #2aa198;
}
}
+ .diff-line-num {
+ &.is-over,
+ &.hll:not(.empty-cell).is-over {
+ background-color: $solarized-light-over-bg;
+ border-color: darken($solarized-light-over-bg, 5%);
+
+ a {
+ color: darken($solarized-light-over-bg, 15%);
+ }
+ }
+ }
+
.line_content.match {
@include matchLine;
}
diff --git a/app/assets/stylesheets/highlight/white.scss b/app/assets/stylesheets/highlight/white.scss
index 398fbfd3b18..0eca30e570f 100644
--- a/app/assets/stylesheets/highlight/white.scss
+++ b/app/assets/stylesheets/highlight/white.scss
@@ -7,6 +7,7 @@ $white-code-color: $gl-text-color;
$white-highlight: #fafe3d;
$white-pre-hll-bg: #f8eec7;
$white-hll-bg: #f8f8f8;
+$white-over-bg: #ded7fc;
$white-c: #998;
$white-err: #a61717;
$white-err-bg: #e3d2d2;
@@ -123,6 +124,16 @@ $white-gc-bg: #eaf2f5;
}
}
+ &.is-over,
+ &.hll:not(.empty-cell).is-over {
+ background-color: $white-over-bg;
+ border-color: darken($white-over-bg, 5%);
+
+ a {
+ color: darken($white-over-bg, 15%);
+ }
+ }
+
&.hll:not(.empty-cell) {
background-color: $line-number-select;
border-color: $line-select-yellow-dark;
diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss
index fd101d43b5b..a24292a7c8c 100644
--- a/app/assets/stylesheets/pages/builds.scss
+++ b/app/assets/stylesheets/pages/builds.scss
@@ -91,7 +91,7 @@
}
&.scroll-top {
- top: 110px;
+ top: 10px;
}
&.scroll-bottom {
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index 92d7772da57..339cdcde480 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -89,6 +89,10 @@
.diff-line-num {
width: 50px;
+
+ a {
+ transition: none;
+ }
}
.line_holder td {
@@ -109,10 +113,6 @@
td.line_content.parallel {
width: 46%;
}
-
- .add-diff-note {
- margin-left: -65px;
- }
}
.old_line,
diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss
index 181dcb7721f..77e09e66340 100644
--- a/app/assets/stylesheets/pages/environments.scss
+++ b/app/assets/stylesheets/pages/environments.scss
@@ -15,96 +15,97 @@
padding-top: 20px;
}
-@media (max-width: $screen-xs-max) {
- .environments-container {
+.environments-container {
+ .table-holder {
width: 100%;
overflow: auto;
}
-}
-.environments {
- table-layout: fixed;
-
- .environments-commit,
- .environments-actions,
- .environments-deploy,
- .environments-build,
- .environments-date {
- position: static;
- float: none;
- display: table-cell;
- }
+ .table.ci-table {
+ .environments-actions {
+ min-width: 200px;
+ }
- .environments-name,
- .environments-commit,
- .environments-actions {
- width: 20%;
- }
+ .environments-commit,
+ .environments-actions {
+ width: 20%;
+ }
- .environments-date {
- width: 10%;
- }
+ .environments-date {
+ width: 10%;
+ }
- .environments-deploy,
- .environments-build {
- width: 15%;
- }
+ .environments-name,
+ .environments-deploy,
+ .environments-build {
+ width: 15%;
+ }
- .environment-name,
- .environments-build-cell,
- .deployment-column {
- word-break: break-all;
- }
+ .deployment-column {
+ > span {
+ word-break: break-all;
+ }
- .deployment-column {
- .avatar {
- float: none;
+ .avatar {
+ float: none;
+ }
}
- }
- .commit-title {
- margin: 0;
- }
+ .btn-group {
- .avatar-image-container {
- text-decoration: none;
- }
+ > a {
+ color: $gl-text-color-secondary;
+ }
- .icon-play {
- height: 13px;
- width: 12px;
- }
+ svg path {
+ fill: $gl-text-color-secondary;
+ }
- .external-url,
- .dropdown-new {
- color: $gl-text-color-secondary;
- }
+ .dropdown {
+ outline: none;
+ }
+ }
- .dropdown-menu {
+ .commit-title {
+ margin: 0;
+ }
- .fa {
- margin-right: 6px;
- color: $gl-text-color-secondary;
+ .avatar-image-container {
+ text-decoration: none;
}
- }
- .build-link,
- .branch-name {
- color: $gl-text-color;
- }
+ .icon-play {
+ height: 13px;
+ width: 12px;
+ }
- .stop-env-link,
- .external-url {
- color: $gl-text-color-secondary;
+ .external-url,
+ .dropdown-new {
+ color: $gl-text-color-secondary;
+ }
- .stop-env-icon {
- font-size: 14px;
+ .dropdown-menu {
+ .fa {
+ margin-right: 6px;
+ color: $gl-text-color-secondary;
+ }
}
- }
- .deployment {
- .build-column {
+ .build-link,
+ .branch-name {
+ color: $gl-text-color;
+ }
+ .stop-env-link,
+ .external-url {
+ color: $gl-text-color-secondary;
+
+ .stop-env-icon {
+ font-size: 14px;
+ }
+ }
+
+ .deployment .build-column {
.build-link {
color: $gl-text-color;
}
@@ -113,34 +114,32 @@
float: none;
}
}
- }
- .folder-icon {
- margin-right: 3px;
- color: $gl-text-color-secondary;
- display: inline-block;
-
- .fa:nth-child(1) {
+ .folder-icon {
margin-right: 3px;
+ color: $gl-text-color-secondary;
+ display: inline-block;
+
+ .fa:nth-child(1) {
+ margin-right: 3px;
+ }
}
- }
- .folder-name {
- cursor: pointer;
- color: $gl-text-color-secondary;
- display: inline-block;
- }
-}
+ .folder-name {
+ cursor: pointer;
+ color: $gl-text-color-secondary;
+ display: inline-block;
+ }
-.table.ci-table.environments {
- .icon-container {
- width: 20px;
- text-align: center;
- }
+ .icon-container {
+ width: 20px;
+ text-align: center;
+ }
- .branch-commit {
- .commit-id {
- margin-right: 0;
+ .branch-commit {
+ .commit-id {
+ margin-right: 0;
+ }
}
}
}
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index a53cc27fac9..4426169ef5a 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -253,11 +253,11 @@
display: block;
}
- width: $sidebar_collapsed_width;
+ width: $gutter_collapsed_width;
padding-top: 0;
.block {
- width: $sidebar_collapsed_width - 2px;
+ width: $gutter_collapsed_width - 2px;
margin-left: -19px;
padding: 15px 0 0;
border-bottom: none;
diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss
index 80b0c9493d8..b595480561b 100644
--- a/app/assets/stylesheets/pages/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -10,6 +10,11 @@
.issue-labels {
display: inline-block;
}
+
+ .icon-merge-request-unmerged {
+ height: 13px;
+ margin-bottom: 3px;
+ }
}
}
diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss
index 3da1150f89b..27c47d36818 100644
--- a/app/assets/stylesheets/pages/milestone.scss
+++ b/app/assets/stylesheets/pages/milestone.scss
@@ -30,6 +30,26 @@
word-wrap: break-word;
}
}
+
+ .panel-heading {
+ line-height: $line-height-base;
+ padding: 14px 16px;
+ display: -webkit-flex;
+ display: flex;
+
+ .title {
+ -webkit-flex: 1;
+ -webkit-flex-grow: 1;
+ flex: 1;
+ flex-grow: 2;
+ }
+
+ .counter {
+ -webkit-flex: 1;
+ flex: 0;
+ padding-left: 16px;
+ }
+ }
}
.milestone-summary {
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index aa130a1abb0..00f5f2645b3 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -452,36 +452,37 @@ ul.notes {
* Line note button on the side of diffs
*/
-.diff-file tr.line_holder {
- @mixin show-add-diff-note {
- display: inline-block;
- }
+.add-diff-note {
+ display: none;
+ margin-top: -2px;
+ border-radius: 50%;
+ background: $white-light;
+ padding: 1px 5px;
+ font-size: 12px;
+ color: $gl-link-color;
+ margin-left: -55px;
+ position: absolute;
+ z-index: 10;
+ width: 23px;
+ height: 23px;
+ border: 1px solid $border-color;
+ transition: transform .1s ease-in-out;
- .add-diff-note {
- margin-top: -8px;
- border-radius: 40px;
- background: $white-light;
- padding: 4px;
- font-size: 16px;
- color: $gl-link-color;
- margin-left: -56px;
- position: absolute;
- z-index: 10;
- width: 32px;
- // "hide" it by default
- display: none;
+ &:hover {
+ background: $gl-info;
+ color: $white-light;
+ transform: scale(1.15);
+ }
- &:hover {
- background: $gl-info;
- color: $white-light;
- @include show-add-diff-note;
- }
+ &:active {
+ outline: 0;
}
+}
- // "show" the icon also if we just hover somewhere over the line
- &:hover > td {
+.diff-file {
+ .is-over {
.add-diff-note {
- @include show-add-diff-note;
+ display: inline-block;
}
}
}
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index 00eb5b30fd5..69eea1b2217 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -13,21 +13,16 @@
white-space: nowrap;
}
- .commit-title {
- margin: 0;
- }
-
- .controls {
- white-space: nowrap;
+ .table-holder {
+ width: 100%;
+ overflow: auto;
}
- .btn {
- margin: 4px;
+ .commit-title {
+ margin: 0;
}
.table.ci-table {
- min-width: 1200px;
- table-layout: fixed;
.label {
margin-bottom: 3px;
@@ -37,16 +32,72 @@
color: $black;
}
- .pipeline-date,
- .pipeline-status {
- width: 10%;
+ .stage-cell {
+ min-width: 130px; // Guarantees we show at least 4 stages in line
+ width: 20%;
+ }
+
+ .pipelines-time-ago {
+ text-align: right;
}
- .pipeline-info,
- .pipeline-commit,
- .pipeline-stages,
.pipeline-actions {
- width: 20%;
+ padding-right: 0;
+ min-width: 170px; //Guarantees buttons don't break in several lines.
+
+ .btn-default {
+ color: $gl-text-color-secondary;
+ }
+
+ .btn.btn-retry:hover,
+ .btn.btn-retry:focus {
+ border-color: $gray-darkest;
+ background-color: $white-normal;
+ }
+
+ svg path {
+ fill: $gl-text-color-secondary;
+ }
+
+ .dropdown-menu {
+ max-height: 250px;
+ overflow-y: auto;
+ }
+
+ .dropdown-toggle,
+ .dropdown-menu {
+ color: $gl-text-color-secondary;
+
+ .fa {
+ color: $gl-text-color-secondary;
+ font-size: 14px;
+ }
+
+ svg,
+ .fa {
+ margin-right: 0;
+ }
+ }
+
+ .btn-group {
+ &.open {
+ .btn-default {
+ background-color: $white-normal;
+ border-color: $border-white-normal;
+ }
+ }
+
+ .btn {
+ .icon-play {
+ height: 13px;
+ width: 12px;
+ }
+ }
+ }
+
+ .tooltip {
+ white-space: nowrap;
+ }
}
}
}
@@ -54,6 +105,7 @@
@media (max-width: $screen-md-max) {
.content-list {
&.pipelines,
+ &.environments-container,
&.builds-content-list {
width: 100%;
overflow: auto;
@@ -61,27 +113,10 @@
}
}
-.content-list.pipelines .table-holder {
- min-height: 300px;
-}
-
-.pipeline-holder {
- width: 100%;
- overflow: auto;
-}
-
.table.ci-table {
- min-width: 900px;
- &.pipeline {
- min-width: 650px;
- }
-
- &.builds-page {
-
- tr {
- height: 71px;
- }
+ &.builds-page tr {
+ height: 71px;
}
tr {
@@ -94,12 +129,16 @@
padding: 10px 8px;
}
+ td.environments-actions {
+ padding-right: 0;
+ }
+
td.stage-cell {
padding: 10px 0;
}
.commit-link {
- padding: 9px 8px 10px;
+ padding: 9px 8px 10px 2px;
}
}
@@ -206,67 +245,8 @@
}
}
- .pipeline-actions {
- min-width: 140px;
-
- .btn {
- margin: 0;
- color: $gl-text-color-secondary;
- }
-
- .cancel-retry-btns {
- vertical-align: middle;
-
- .btn:not(:first-child) {
- margin-left: 8px;
- }
- }
-
- .dropdown-toggle,
- .dropdown-menu {
- color: $gl-text-color-secondary;
-
- .fa {
- color: $gl-text-color-secondary;
- font-size: 14px;
- }
-
- svg,
- .fa {
- margin-right: 0;
- }
- }
-
- .btn-remove {
- color: $white-light;
- }
-
- .btn-group {
- &.open {
- .btn-default {
- background-color: $white-normal;
- border-color: $border-white-normal;
- }
- }
-
- .btn {
- .icon-play {
- height: 13px;
- width: 12px;
- }
- }
- }
-
- .tooltip {
- white-space: nowrap;
- }
- }
-
- .build-link {
-
- a {
- color: $gl-text-color;
- }
+ .build-link a {
+ color: $gl-text-color;
}
.btn-group.open .dropdown-toggle {
@@ -330,31 +310,8 @@
}
.tab-pane {
- &.pipelines {
- .ci-table {
- min-width: 900px;
- }
-
- .content-list.pipelines {
- overflow: auto;
- }
-
- .stage {
- max-width: 100px;
- width: 100px;
- }
-
- .pipeline-actions {
- min-width: initial;
- }
- }
-
- &.builds {
- .ci-table {
- tr {
- height: 71px;
- }
- }
+ &.builds .ci-table tr {
+ height: 71px;
}
}
diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss
index 8031c4467a4..aad1a8986b0 100644
--- a/app/assets/stylesheets/pages/profile.scss
+++ b/app/assets/stylesheets/pages/profile.scss
@@ -277,3 +277,41 @@ table.u2f-registrations {
padding-left: 18px;
}
}
+
+.user-callout {
+ margin: 24px auto 0;
+
+ .bordered-box {
+ border: 1px solid $border-color;
+ border-radius: $border-radius-default;
+ }
+
+ .landing {
+ margin-bottom: $gl-padding;
+
+ .close {
+ margin-right: 20px;
+ }
+
+ .dismiss-icon {
+ float: right;
+ cursor: pointer;
+ color: $cycle-analytics-dismiss-icon-color;
+ }
+
+ .svg-container {
+ text-align: center;
+
+ svg {
+ width: 136px;
+ height: 136px;
+ }
+ }
+ }
+
+ @media(max-width: $screen-xs-max) {
+ .inner-content {
+ padding-left: 30px;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/pages/profiles/preferences.scss b/app/assets/stylesheets/pages/profiles/preferences.scss
index 100ace41f2a..305feaacaa1 100644
--- a/app/assets/stylesheets/pages/profiles/preferences.scss
+++ b/app/assets/stylesheets/pages/profiles/preferences.scss
@@ -1,42 +1,3 @@
-.application-theme {
- label {
- margin-right: 20px;
- text-align: center;
-
- .preview {
- border-radius: 4px;
-
- height: 80px;
- margin-bottom: 10px;
- width: 160px;
-
- &.ui_blue {
- background: $theme-blue;
- }
-
- &.ui_charcoal {
- background: $theme-charcoal;
- }
-
- &.ui_graphite {
- background: $theme-graphite;
- }
-
- &.ui_black {
- background: $theme-black;
- }
-
- &.ui_green {
- background: $theme-green;
- }
-
- &.ui_violet {
- background: $theme-violet;
- }
- }
- }
-}
-
.syntax-theme {
label {
margin-right: 20px;
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 9a72148f408..07b93430442 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -102,6 +102,7 @@
font-size: 24px;
font-weight: 400;
line-height: 1;
+ word-wrap: break-word;
.fa {
margin-left: 2px;
@@ -267,6 +268,13 @@
}
}
+.project-repo-buttons {
+ .project-action-button .dropdown-menu {
+ max-height: 250px;
+ overflow-y: auto;
+ }
+}
+
.split-one {
display: inline-table;
margin-right: 12px;
@@ -630,14 +638,6 @@ pre.light-well {
margin: 0;
}
-.activity-filter-block {
- .controls {
- padding-bottom: 7px;
- margin-top: 8px;
- border-bottom: 1px solid $border-color;
- }
-}
-
.commits-search-form {
.input-short {
min-width: 200px;
diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss
index 0d5604aae69..af9ddb9ff80 100644
--- a/app/assets/stylesheets/pages/todos.scss
+++ b/app/assets/stylesheets/pages/todos.scss
@@ -6,6 +6,8 @@
.navbar-nav {
li {
.badge.todos-pending-count {
+ position: inherit;
+ top: -6px;
margin-top: -5px;
font-weight: normal;
background: $todo-alert-blue;
@@ -43,6 +45,12 @@
}
}
+ .todo-avatar,
+ .todo-actions {
+ -webkit-flex: 0 0 auto;
+ flex: 0 0 auto;
+ }
+
.todo-actions {
display: -webkit-flex;
display: flex;
@@ -55,15 +63,49 @@
}
.todo-item {
- -webkit-flex: auto;
- flex: auto;
+ -webkit-flex: 0 1 100%;
+ flex: 0 1 100%;
+ min-width: 0;
+ }
+}
+
+.todos-list > .todo.todo-pending.done-reversible {
+ background-color: $gray-light;
+
+ &:hover {
+ border-color: $border-color;
+ }
+
+ .title {
+ font-weight: normal;
}
}
.todo-item {
.todo-title {
- @include str-truncated(calc(100% - 174px));
- overflow: visible;
+ display: flex;
+
+ & > .title-item {
+ -webkit-flex: 0 0 auto;
+ flex: 0 0 auto;
+ margin: 0 2px;
+
+ &:first-child {
+ margin-left: 0;
+ }
+
+ &:last-child {
+ margin-right: 0;
+ }
+ }
+
+ .todo-label {
+ -webkit-flex: 0 1 auto;
+ flex: 0 1 auto;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
}
.status-box {
@@ -142,10 +184,12 @@
.todo-item {
.todo-title {
- white-space: normal;
- overflow: visible;
- max-width: 100%;
+ flex-flow: row wrap;
margin-bottom: 10px;
+
+ .todo-label {
+ white-space: normal;
+ }
}
.todo-body {
diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss
index 948921efc0b..e4487dbcb87 100644
--- a/app/assets/stylesheets/pages/tree.scss
+++ b/app/assets/stylesheets/pages/tree.scss
@@ -149,7 +149,7 @@
}
.commit-actions {
- width: 200px;
+ width: 260px;
}
}
diff --git a/app/assets/stylesheets/print.scss b/app/assets/stylesheets/print.scss
index 0ff3c3f5472..6cc1cc8e263 100644
--- a/app/assets/stylesheets/print.scss
+++ b/app/assets/stylesheets/print.scss
@@ -31,7 +31,6 @@ nav.navbar-collapse.collapse,
.blob-commit-info,
.file-title,
.file-holder,
-.sidebar-wrapper,
.nav,
.btn,
ul.notes-form,
diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb
index b0f5d4a9933..d807e6263ee 100644
--- a/app/controllers/admin/application_settings_controller.rb
+++ b/app/controllers/admin/application_settings_controller.rb
@@ -83,6 +83,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:akismet_api_key,
:akismet_enabled,
:container_registry_token_expire_delay,
+ :default_artifacts_expire_in,
:default_branch_protection,
:default_group_visibility,
:default_project_visibility,
diff --git a/app/controllers/admin/background_jobs_controller.rb b/app/controllers/admin/background_jobs_controller.rb
index 338496013a0..c09095b9849 100644
--- a/app/controllers/admin/background_jobs_controller.rb
+++ b/app/controllers/admin/background_jobs_controller.rb
@@ -2,5 +2,6 @@ class Admin::BackgroundJobsController < Admin::ApplicationController
def show
ps_output, _ = Gitlab::Popen.popen(%W(ps -U #{Gitlab.config.gitlab.user} -o pid,pcpu,pmem,stat,start,command))
@sidekiq_processes = ps_output.split("\n").grep(/sidekiq/)
+ @concurrency = Sidekiq.options[:concurrency]
end
end
diff --git a/app/controllers/admin/runners_controller.rb b/app/controllers/admin/runners_controller.rb
index 7345c91f67d..348641e5ecb 100644
--- a/app/controllers/admin/runners_controller.rb
+++ b/app/controllers/admin/runners_controller.rb
@@ -13,7 +13,7 @@ class Admin::RunnersController < Admin::ApplicationController
end
def update
- if @runner.update_attributes(runner_params)
+ if Ci::UpdateRunnerService.new(@runner).update(runner_params)
respond_to do |format|
format.js
format.html { redirect_to admin_runner_path(@runner) }
@@ -31,7 +31,7 @@ class Admin::RunnersController < Admin::ApplicationController
end
def resume
- if @runner.update_attributes(active: true)
+ if Ci::UpdateRunnerService.new(@runner).update(active: true)
redirect_to admin_runners_path, notice: 'Runner was successfully updated.'
else
redirect_to admin_runners_path, alert: 'Runner was not updated.'
@@ -39,7 +39,7 @@ class Admin::RunnersController < Admin::ApplicationController
end
def pause
- if @runner.update_attributes(active: false)
+ if Ci::UpdateRunnerService.new(@runner).update(active: false)
redirect_to admin_runners_path, notice: 'Runner was successfully updated.'
else
redirect_to admin_runners_path, alert: 'Runner was not updated.'
diff --git a/app/controllers/admin/system_info_controller.rb b/app/controllers/admin/system_info_controller.rb
index ca04a17caa1..99039724521 100644
--- a/app/controllers/admin/system_info_controller.rb
+++ b/app/controllers/admin/system_info_controller.rb
@@ -3,7 +3,7 @@ class Admin::SystemInfoController < Admin::ApplicationController
'nobrowse',
'read-only',
'ro'
- ]
+ ].freeze
EXCLUDED_MOUNT_TYPES = [
'autofs',
@@ -21,12 +21,13 @@ class Admin::SystemInfoController < Admin::ApplicationController
'mqueue',
'proc',
'pstore',
+ 'rpc_pipefs',
'securityfs',
'sysfs',
'tmpfs',
'tracefs',
'vfat'
- ]
+ ].freeze
def show
@cpus = Vmstat.cpu rescue nil
diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb
index 1cd50852e89..7ffde71c3b1 100644
--- a/app/controllers/admin/users_controller.rb
+++ b/app/controllers/admin/users_controller.rb
@@ -194,7 +194,6 @@ class Admin::UsersController < Admin::ApplicationController
:provider,
:remember_me,
:skype,
- :theme_id,
:twitter,
:username,
:website_url
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index bf6be3d516b..e42e48f87d2 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -74,7 +74,7 @@ class ApplicationController < ActionController::Base
def authenticate_user!(*args)
if redirect_to_home_page_url?
- redirect_to current_application_settings.home_page_url and return
+ return redirect_to current_application_settings.home_page_url
end
super(*args)
@@ -131,7 +131,7 @@ class ApplicationController < ActionController::Base
headers['X-UA-Compatible'] = 'IE=edge'
headers['X-Content-Type-Options'] = 'nosniff'
# Enabling HSTS for non-standard ports would send clients to the wrong port
- if Gitlab.config.gitlab.https and Gitlab.config.gitlab.port == 443
+ if Gitlab.config.gitlab.https && Gitlab.config.gitlab.port == 443
headers['Strict-Transport-Security'] = 'max-age=31536000'
end
end
@@ -152,7 +152,7 @@ class ApplicationController < ActionController::Base
def check_password_expiration
if current_user && current_user.password_expires_at && current_user.password_expires_at < Time.now && !current_user.ldap_user?
- redirect_to new_profile_password_path and return
+ return redirect_to new_profile_password_path
end
end
@@ -181,7 +181,7 @@ class ApplicationController < ActionController::Base
end
def gitlab_ldap_access(&block)
- Gitlab::LDAP::Access.open { |access| block.call(access) }
+ Gitlab::LDAP::Access.open { |access| yield(access) }
end
# JSON for infinite scroll via Pager object
@@ -218,7 +218,7 @@ class ApplicationController < ActionController::Base
def require_email
if current_user && current_user.temp_oauth_email? && session[:impersonator_id].nil?
- redirect_to profile_path, notice: 'Please complete your profile with email address' and return
+ return redirect_to profile_path, notice: 'Please complete your profile with email address'
end
end
diff --git a/app/controllers/concerns/creates_commit.rb b/app/controllers/concerns/creates_commit.rb
index 6286d67d30c..2fe03020d2d 100644
--- a/app/controllers/concerns/creates_commit.rb
+++ b/app/controllers/concerns/creates_commit.rb
@@ -101,25 +101,18 @@ module CreatesCommit
# TODO: We should really clean this up
def set_commit_variables
- if can?(current_user, :push_code, @project)
- # Edit file in this project
- @mr_source_project = @project
-
- if @project.forked?
- # Merge request from this project to fork origin
- @mr_target_project = @project.forked_from_project
- @mr_target_branch = @mr_target_project.repository.root_ref
+ @mr_source_project =
+ if can?(current_user, :push_code, @project)
+ # Edit file in this project
+ @project
else
- # Merge request to this project
- @mr_target_project = @project
- @mr_target_branch = @ref || @target_branch
+ # Merge request from fork to this project
+ current_user.fork_of(@project)
end
- else
- # Merge request from fork to this project
- @mr_source_project = current_user.fork_of(@project)
- @mr_target_project = @project
- @mr_target_branch = @ref || @target_branch
- end
+
+ # Merge request to this project
+ @mr_target_project = @project
+ @mr_target_branch = @ref || @target_branch
@mr_source_branch = guess_mr_source_branch
end
diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb
index 0821974aa93..3ccf2a9ce33 100644
--- a/app/controllers/concerns/issuable_actions.rb
+++ b/app/controllers/concerns/issuable_actions.rb
@@ -26,6 +26,23 @@ module IssuableActions
private
+ def render_conflict_response
+ respond_to do |format|
+ format.html do
+ @conflict = true
+ render :edit
+ end
+
+ format.json do
+ render json: {
+ errors: [
+ "Someone edited this #{issuable.human_class_name} at the same time you did. Please refresh your browser and make sure your changes will not unintentionally remove theirs."
+ ]
+ }, status: 409
+ end
+ end
+ end
+
def labels
@labels ||= LabelsFinder.new(current_user, project_id: @project.id).execute
end
diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb
index a6e158ebae6..85ae4985e58 100644
--- a/app/controllers/concerns/issuable_collections.rb
+++ b/app/controllers/concerns/issuable_collections.rb
@@ -9,24 +9,32 @@ module IssuableCollections
private
- def issuable_meta_data(issuable_collection)
+ def issuable_meta_data(issuable_collection, collection_type)
# map has to be used here since using pluck or select will
# throw an error when ordering issuables by priority which inserts
# a new order into the collection.
# We cannot use reorder to not mess up the paginated collection.
- issuable_ids = issuable_collection.map(&:id)
- issuable_note_count = Note.count_for_collection(issuable_ids, @collection_type)
+ issuable_ids = issuable_collection.map(&:id)
+ issuable_note_count = Note.count_for_collection(issuable_ids, @collection_type)
issuable_votes_count = AwardEmoji.votes_for_collection(issuable_ids, @collection_type)
+ issuable_merge_requests_count =
+ if collection_type == 'Issue'
+ MergeRequestsClosingIssues.count_for_collection(issuable_ids)
+ else
+ []
+ end
issuable_ids.each_with_object({}) do |id, issuable_meta|
downvotes = issuable_votes_count.find { |votes| votes.awardable_id == id && votes.downvote? }
- upvotes = issuable_votes_count.find { |votes| votes.awardable_id == id && votes.upvote? }
- notes = issuable_note_count.find { |notes| notes.noteable_id == id }
+ upvotes = issuable_votes_count.find { |votes| votes.awardable_id == id && votes.upvote? }
+ notes = issuable_note_count.find { |notes| notes.noteable_id == id }
+ merge_requests = issuable_merge_requests_count.find { |mr| mr.first == id }
issuable_meta[id] = Issuable::IssuableMeta.new(
upvotes.try(:count).to_i,
downvotes.try(:count).to_i,
- notes.try(:count).to_i
+ notes.try(:count).to_i,
+ merge_requests.try(:last).to_i
)
end
end
diff --git a/app/controllers/concerns/issues_action.rb b/app/controllers/concerns/issues_action.rb
index fb5edb34370..b17c138d5c7 100644
--- a/app/controllers/concerns/issues_action.rb
+++ b/app/controllers/concerns/issues_action.rb
@@ -10,7 +10,7 @@ module IssuesAction
.page(params[:page])
@collection_type = "Issue"
- @issuable_meta_data = issuable_meta_data(@issues)
+ @issuable_meta_data = issuable_meta_data(@issues, @collection_type)
respond_to do |format|
format.html
diff --git a/app/controllers/concerns/merge_requests_action.rb b/app/controllers/concerns/merge_requests_action.rb
index 6229759dcf1..d3c8e4888bc 100644
--- a/app/controllers/concerns/merge_requests_action.rb
+++ b/app/controllers/concerns/merge_requests_action.rb
@@ -9,7 +9,7 @@ module MergeRequestsAction
.page(params[:page])
@collection_type = "MergeRequest"
- @issuable_meta_data = issuable_meta_data(@merge_requests)
+ @issuable_meta_data = issuable_meta_data(@merge_requests, @collection_type)
end
private
diff --git a/app/controllers/concerns/service_params.rb b/app/controllers/concerns/service_params.rb
index d7f5a4e4682..2992568ae66 100644
--- a/app/controllers/concerns/service_params.rb
+++ b/app/controllers/concerns/service_params.rb
@@ -33,6 +33,7 @@ module ServiceParams
:issues_url,
:jira_issue_transition_id,
:merge_requests_events,
+ :mock_service_url,
:namespace,
:new_issue_url,
:notify,
@@ -59,10 +60,10 @@ module ServiceParams
:user_key,
:username,
:webhook
- ]
+ ].freeze
# Parameters to ignore if no value is specified
- FILTER_BLANK_PARAMS = [:password]
+ FILTER_BLANK_PARAMS = [:password].freeze
def service_params
dynamic_params = @service.event_channel_names + @service.event_names
diff --git a/app/controllers/concerns/snippets_actions.rb b/app/controllers/concerns/snippets_actions.rb
new file mode 100644
index 00000000000..ca6dffe1cc5
--- /dev/null
+++ b/app/controllers/concerns/snippets_actions.rb
@@ -0,0 +1,21 @@
+module SnippetsActions
+ extend ActiveSupport::Concern
+
+ def edit
+ end
+
+ def raw
+ send_data(
+ convert_line_endings(@snippet.content),
+ type: 'text/plain; charset=utf-8',
+ disposition: 'inline',
+ filename: @snippet.sanitized_file_name
+ )
+ end
+
+ private
+
+ def convert_line_endings(content)
+ params[:line_ending] == 'raw' ? content : content.gsub(/\r\n/, "\n")
+ end
+end
diff --git a/app/controllers/concerns/spammable_actions.rb b/app/controllers/concerns/spammable_actions.rb
index a6891149bfa..d0a692070d9 100644
--- a/app/controllers/concerns/spammable_actions.rb
+++ b/app/controllers/concerns/spammable_actions.rb
@@ -17,13 +17,31 @@ module SpammableActions
private
- def recaptcha_params
- return {} unless params[:recaptcha_verification] && Gitlab::Recaptcha.load_configurations! && verify_recaptcha
+ def recaptcha_check_with_fallback(&fallback)
+ if spammable.valid?
+ redirect_to spammable
+ 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
+ yield
+ end
+ end
+
+ def spammable_params
+ default_params = { request: request }
+
+ recaptcha_check = params[:recaptcha_verification] &&
+ Gitlab::Recaptcha.load_configurations! &&
+ verify_recaptcha
+
+ return default_params unless recaptcha_check
- {
- recaptcha_verified: true,
- spam_log_id: params[:spam_log_id]
- }
+ { recaptcha_verified: true,
+ spam_log_id: params[:spam_log_id] }.merge(default_params)
end
def spammable
diff --git a/app/controllers/dashboard/todos_controller.rb b/app/controllers/dashboard/todos_controller.rb
index e3933e3d7b1..5848ca62777 100644
--- a/app/controllers/dashboard/todos_controller.rb
+++ b/app/controllers/dashboard/todos_controller.rb
@@ -1,4 +1,6 @@
class Dashboard::TodosController < Dashboard::ApplicationController
+ include ActionView::Helpers::NumberHelper
+
before_action :find_todos, only: [:index, :destroy_all]
def index
@@ -29,6 +31,17 @@ class Dashboard::TodosController < Dashboard::ApplicationController
end
end
+ def restore
+ TodoService.new.mark_todos_as_pending_by_ids([params[:id]], current_user)
+
+ render json: todos_counts
+ end
+
+ # Used in TodosHelper also
+ def self.todos_count_format(count)
+ count >= 100 ? '99+' : count
+ end
+
private
def find_todos
@@ -37,8 +50,8 @@ class Dashboard::TodosController < Dashboard::ApplicationController
def todos_counts
{
- count: current_user.todos_pending_count,
- done_count: current_user.todos_done_count
+ count: number_with_delimiter(current_user.todos_pending_count),
+ done_count: number_with_delimiter(current_user.todos_done_count)
}
end
end
diff --git a/app/controllers/import/fogbugz_controller.rb b/app/controllers/import/fogbugz_controller.rb
index 99b10b2f9b3..5df6bd34185 100644
--- a/app/controllers/import/fogbugz_controller.rb
+++ b/app/controllers/import/fogbugz_controller.rb
@@ -29,7 +29,7 @@ class Import::FogbugzController < Import::BaseController
unless user_map.is_a?(Hash) && user_map.all? { |k, v| !v[:name].blank? }
flash.now[:alert] = 'All users must have a name.'
- render 'new_user_map' and return
+ return render 'new_user_map'
end
session[:fogbugz_user_map] = user_map
diff --git a/app/controllers/import/google_code_controller.rb b/app/controllers/import/google_code_controller.rb
index 8d0de158f98..7d7f13ce5d5 100644
--- a/app/controllers/import/google_code_controller.rb
+++ b/app/controllers/import/google_code_controller.rb
@@ -44,13 +44,13 @@ class Import::GoogleCodeController < Import::BaseController
rescue
flash.now[:alert] = "The entered user map is not a valid JSON user map."
- render "new_user_map" and return
+ return render "new_user_map"
end
unless user_map.is_a?(Hash) && user_map.all? { |k, v| k.is_a?(String) && v.is_a?(String) }
flash.now[:alert] = "The entered user map is not a valid JSON user map."
- render "new_user_map" and return
+ return render "new_user_map"
end
# This is the default, so let's not save it into the database.
diff --git a/app/controllers/invites_controller.rb b/app/controllers/invites_controller.rb
index 58964a0e65d..7625187c7be 100644
--- a/app/controllers/invites_controller.rb
+++ b/app/controllers/invites_controller.rb
@@ -42,9 +42,7 @@ class InvitesController < ApplicationController
@token = params[:id]
@member = Member.find_by_invite_token(@token)
- unless @member
- render_404 and return
- end
+ return render_404 unless @member
@member
end
diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb
index c2e4d62b50b..3109439b2ff 100644
--- a/app/controllers/jwt_controller.rb
+++ b/app/controllers/jwt_controller.rb
@@ -5,7 +5,7 @@ class JwtController < ApplicationController
SERVICES = {
Auth::ContainerRegistryAuthenticationService::AUDIENCE => Auth::ContainerRegistryAuthenticationService,
- }
+ }.freeze
def auth
service = SERVICES[params[:service]]
@@ -39,7 +39,8 @@ class JwtController < ApplicationController
message: "HTTP Basic: Access denied\n" \
"You have 2FA enabled, please use a personal access token for Git over HTTP.\n" \
"You can generate one at #{profile_personal_access_tokens_url}" }
- ] }, status: 401
+ ]
+ }, status: 401
end
def render_unauthorized
@@ -47,7 +48,8 @@ class JwtController < ApplicationController
errors: [
{ code: 'UNAUTHORIZED',
message: 'HTTP Basic: Access denied' }
- ] }, status: 401
+ ]
+ }, status: 401
end
def auth_params
diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb
index f54c79c2e37..58d50ad647b 100644
--- a/app/controllers/omniauth_callbacks_controller.rb
+++ b/app/controllers/omniauth_callbacks_controller.rb
@@ -78,6 +78,13 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
handle_omniauth
end
+ def authentiq
+ if params['sid']
+ handle_service_ticket oauth['provider'], params['sid']
+ end
+ handle_omniauth
+ end
+
private
def handle_omniauth
@@ -115,7 +122,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
else
error_message = @user.errors.full_messages.to_sentence
- redirect_to omniauth_error_path(oauth['provider'], error: error_message) and return
+ return redirect_to omniauth_error_path(oauth['provider'], error: error_message)
end
end
diff --git a/app/controllers/profiles/keys_controller.rb b/app/controllers/profiles/keys_controller.rb
index 830e0b9591b..c8663a3c38e 100644
--- a/app/controllers/profiles/keys_controller.rb
+++ b/app/controllers/profiles/keys_controller.rb
@@ -45,13 +45,13 @@ class Profiles::KeysController < Profiles::ApplicationController
if user.present?
render text: user.all_ssh_keys.join("\n"), content_type: "text/plain"
else
- render_404 and return
+ return render_404
end
rescue => e
render text: e.message
end
else
- render_404 and return
+ return render_404
end
end
diff --git a/app/controllers/profiles/preferences_controller.rb b/app/controllers/profiles/preferences_controller.rb
index a9a06ecc808..0d891ef4004 100644
--- a/app/controllers/profiles/preferences_controller.rb
+++ b/app/controllers/profiles/preferences_controller.rb
@@ -34,7 +34,6 @@ class Profiles::PreferencesController < Profiles::ApplicationController
:layout,
:dashboard,
:project_view,
- :theme_id
)
end
end
diff --git a/app/controllers/profiles/two_factor_auths_controller.rb b/app/controllers/profiles/two_factor_auths_controller.rb
index 18044ca78e2..26e7e93533e 100644
--- a/app/controllers/profiles/two_factor_auths_controller.rb
+++ b/app/controllers/profiles/two_factor_auths_controller.rb
@@ -80,7 +80,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
def build_qr_code
uri = current_user.otp_provisioning_uri(account_string, issuer: issuer_host)
- RQRCode::render_qrcode(uri, :svg, level: :m, unit: 3)
+ RQRCode.render_qrcode(uri, :svg, level: :m, unit: 3)
end
def account_string
diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb
index db33b60b229..e2f81b09adc 100644
--- a/app/controllers/projects/application_controller.rb
+++ b/app/controllers/projects/application_controller.rb
@@ -83,7 +83,6 @@ class Projects::ApplicationController < ApplicationController
end
def apply_diff_view_cookie!
- @show_changes_tab = params[:view].present?
cookies.permanent[:diff_view] = params.delete(:view) if params[:view].present?
end
diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb
index a1db856dcfb..39ba815cfca 100644
--- a/app/controllers/projects/blob_controller.rb
+++ b/app/controllers/projects/blob_controller.rb
@@ -95,7 +95,7 @@ class Projects::BlobController < Projects::ApplicationController
else
if tree = @repository.tree(@commit.id, @path)
if tree.entries.any?
- redirect_to namespace_project_tree_path(@project.namespace, @project, File.join(@ref, @path)) and return
+ return redirect_to namespace_project_tree_path(@project.namespace, @project, File.join(@ref, @path))
end
end
diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb
index 89d84809e3a..a01c0caa959 100644
--- a/app/controllers/projects/branches_controller.rb
+++ b/app/controllers/projects/branches_controller.rb
@@ -1,8 +1,9 @@
class Projects::BranchesController < Projects::ApplicationController
include ActionView::Helpers::SanitizeHelper
include SortingHelper
+
# Authorize
- before_action :require_non_empty_project
+ before_action :require_non_empty_project, except: :create
before_action :authorize_download_code!
before_action :authorize_push_code!, only: [:new, :create, :destroy, :destroy_all_merged]
@@ -32,6 +33,8 @@ class Projects::BranchesController < Projects::ApplicationController
branch_name = sanitize(strip_tags(params[:branch_name]))
branch_name = Addressable::URI.unescape(branch_name)
+ redirect_to_autodeploy = project.empty_repo? && project.deployment_services.present?
+
result = CreateBranchService.new(project, current_user).
execute(branch_name, ref)
@@ -42,8 +45,15 @@ class Projects::BranchesController < Projects::ApplicationController
if result[:status] == :success
@branch = result[:branch]
- redirect_to namespace_project_tree_path(@project.namespace, @project,
- @branch.name)
+
+ if redirect_to_autodeploy
+ redirect_to(
+ url_to_autodeploy_setup(project, branch_name),
+ notice: view_context.autodeploy_flash_notice(branch_name))
+ else
+ redirect_to namespace_project_tree_path(@project.namespace, @project,
+ @branch.name)
+ end
else
@error = result[:message]
render action: 'new'
@@ -76,7 +86,19 @@ class Projects::BranchesController < Projects::ApplicationController
ref_escaped = sanitize(strip_tags(params[:ref]))
Addressable::URI.unescape(ref_escaped)
else
- @project.default_branch
+ @project.default_branch || 'master'
end
end
+
+ def url_to_autodeploy_setup(project, branch_name)
+ namespace_project_new_blob_path(
+ project.namespace,
+ project,
+ branch_name,
+ file_name: '.gitlab-ci.yml',
+ commit_message: 'Set up auto deploy',
+ target_branch: branch_name,
+ context: 'autodeploy'
+ )
+ end
end
diff --git a/app/controllers/projects/git_http_client_controller.rb b/app/controllers/projects/git_http_client_controller.rb
index 216c158e41e..9a1bf037a95 100644
--- a/app/controllers/projects/git_http_client_controller.rb
+++ b/app/controllers/projects/git_http_client_controller.rb
@@ -76,11 +76,12 @@ class Projects::GitHttpClientController < Projects::ApplicationController
return @project if defined?(@project)
project_id, _ = project_id_with_suffix
- if project_id.blank?
- @project = nil
- else
- @project = Project.find_by_full_path("#{params[:namespace_id]}/#{project_id}")
- end
+ @project =
+ if project_id.blank?
+ nil
+ else
+ Project.find_by_full_path("#{params[:namespace_id]}/#{project_id}")
+ end
end
# This method returns two values so that we can parse
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index 744a4af1c51..1151555b8fa 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -26,7 +26,7 @@ class Projects::IssuesController < Projects::ApplicationController
@collection_type = "Issue"
@issues = issues_collection
@issues = @issues.page(params[:page])
- @issuable_meta_data = issuable_meta_data(@issues)
+ @issuable_meta_data = issuable_meta_data(@issues, @collection_type)
if @issues.out_of_range? && @issues.total_pages != 0
return redirect_to url_for(params.merge(page: @issues.total_pages))
@@ -94,15 +94,15 @@ class Projects::IssuesController < Projects::ApplicationController
end
def create
- extra_params = { request: request,
- merge_request_for_resolving_discussions: merge_request_for_resolving_discussions }
- extra_params.merge!(recaptcha_params)
+ create_params = issue_params
+ .merge(merge_request_for_resolving_discussions: merge_request_for_resolving_discussions)
+ .merge(spammable_params)
- @issue = Issues::CreateService.new(project, current_user, issue_params.merge(extra_params)).execute
+ @issue = Issues::CreateService.new(project, current_user, create_params).execute
respond_to do |format|
format.html do
- html_response_create
+ recaptcha_check_with_fallback { render :new }
end
format.js do
@link = @issue.attachment.url.to_js
@@ -111,7 +111,9 @@ class Projects::IssuesController < Projects::ApplicationController
end
def update
- @issue = Issues::UpdateService.new(project, current_user, issue_params).execute(issue)
+ update_params = issue_params.merge(spammable_params)
+
+ @issue = Issues::UpdateService.new(project, current_user, update_params).execute(issue)
if params[:move_to_project_id].to_i > 0
new_project = Project.find(params[:move_to_project_id])
@@ -123,11 +125,7 @@ class Projects::IssuesController < Projects::ApplicationController
respond_to do |format|
format.html do
- if @issue.valid?
- redirect_to issue_path(@issue)
- else
- render :edit
- end
+ recaptcha_check_with_fallback { render :edit }
end
format.json do
@@ -136,8 +134,7 @@ class Projects::IssuesController < Projects::ApplicationController
end
rescue ActiveRecord::StaleObjectError
- @conflict = true
- render :edit
+ render_conflict_response
end
def referenced_merge_requests
@@ -179,20 +176,6 @@ 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/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 63b5bcbb586..53f30a24312 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -39,7 +39,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@collection_type = "MergeRequest"
@merge_requests = merge_requests_collection
@merge_requests = @merge_requests.page(params[:page])
- @issuable_meta_data = issuable_meta_data(@merge_requests)
+ @issuable_meta_data = issuable_meta_data(@merge_requests, @collection_type)
if @merge_requests.out_of_range? && @merge_requests.total_pages != 0
return redirect_to url_for(params.merge(page: @merge_requests.total_pages))
@@ -50,6 +50,17 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@labels = LabelsFinder.new(current_user, labels_params).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.json do
@@ -245,6 +256,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
respond_to do |format|
format.html do
define_new_vars
+ @show_changes_tab = true
render "new"
end
format.json do
@@ -284,22 +296,21 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def update
@merge_request = MergeRequests::UpdateService.new(project, current_user, merge_request_params).execute(@merge_request)
- if @merge_request.valid?
- respond_to do |format|
- format.html do
- redirect_to([@merge_request.target_project.namespace.becomes(Namespace),
- @merge_request.target_project, @merge_request])
- end
- format.json do
- render json: @merge_request.to_json(include: { milestone: {}, assignee: { methods: :avatar_url }, labels: { methods: :text_color } }, methods: [:task_status, :task_status_short])
+ respond_to do |format|
+ format.html do
+ if @merge_request.valid?
+ redirect_to([@merge_request.target_project.namespace.becomes(Namespace), @merge_request.target_project, @merge_request])
+ else
+ render :edit
end
end
- else
- render "edit"
+
+ format.json do
+ render json: @merge_request.to_json(include: { milestone: {}, assignee: { methods: :avatar_url }, labels: { methods: :text_color } }, methods: [:task_status, :task_status_short])
+ end
end
rescue ActiveRecord::StaleObjectError
- @conflict = true
- render :edit
+ render_conflict_response
end
def remove_wip
@@ -369,14 +380,15 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
def merge_widget_refresh
- if merge_request.merge_when_build_succeeds
- @status = :merge_when_build_succeeds
- else
- # Only MRs that can be merged end in this action
- # MR can be already picked up for merge / merged already or can be waiting for worker to be picked up
- # in last case it does not have any special status. Possible error is handled inside widget js function
- @status = :success
- end
+ @status =
+ if merge_request.merge_when_build_succeeds
+ :merge_when_build_succeeds
+ else
+ # Only MRs that can be merged end in this action
+ # MR can be already picked up for merge / merged already or can be waiting for worker to be picked up
+ # in last case it does not have any special status. Possible error is handled inside widget js function
+ :success
+ end
render 'merge'
end
@@ -616,6 +628,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@labels = LabelsFinder.new(current_user, project_id: @project.id).execute
+ @show_changes_tab = params[:show_changes].present?
+
define_pipelines_vars
end
diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb
index 84451257b98..8657bc4dfdc 100644
--- a/app/controllers/projects/pipelines_controller.rb
+++ b/app/controllers/projects/pipelines_controller.rb
@@ -13,9 +13,15 @@ class Projects::PipelinesController < Projects::ApplicationController
.page(params[:page])
.per(30)
- @running_or_pending_count = PipelinesFinder
+ @running_count = PipelinesFinder
.new(project).execute(scope: 'running').count
+ @pending_count = PipelinesFinder
+ .new(project).execute(scope: 'pending').count
+
+ @finished_count = PipelinesFinder
+ .new(project).execute(scope: 'finished').count
+
@pipelines_count = PipelinesFinder
.new(project).execute.count
@@ -29,7 +35,9 @@ class Projects::PipelinesController < Projects::ApplicationController
.represent(@pipelines),
count: {
all: @pipelines_count,
- running_or_pending: @running_or_pending_count
+ running: @running_count,
+ pending: @pending_count,
+ finished: @finished_count,
}
}
end
diff --git a/app/controllers/projects/runners_controller.rb b/app/controllers/projects/runners_controller.rb
index 74c54037ba9..8b50ea207a5 100644
--- a/app/controllers/projects/runners_controller.rb
+++ b/app/controllers/projects/runners_controller.rb
@@ -12,7 +12,7 @@ class Projects::RunnersController < Projects::ApplicationController
end
def update
- if @runner.update_attributes(runner_params)
+ if Ci::UpdateRunnerService.new(@runner).update(runner_params)
redirect_to runner_path(@runner), notice: 'Runner was successfully updated.'
else
render 'edit'
@@ -28,7 +28,7 @@ class Projects::RunnersController < Projects::ApplicationController
end
def resume
- if @runner.update_attributes(active: true)
+ if Ci::UpdateRunnerService.new(@runner).update(active: true)
redirect_to runner_path(@runner), notice: 'Runner was successfully updated.'
else
redirect_to runner_path(@runner), alert: 'Runner was not updated.'
@@ -36,7 +36,7 @@ class Projects::RunnersController < Projects::ApplicationController
end
def pause
- if @runner.update_attributes(active: false)
+ if Ci::UpdateRunnerService.new(@runner).update(active: false)
redirect_to runner_path(@runner), notice: 'Runner was successfully updated.'
else
redirect_to runner_path(@runner), alert: 'Runner was not updated.'
diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb
index 5d193f26a8e..ea1a97b7cf0 100644
--- a/app/controllers/projects/snippets_controller.rb
+++ b/app/controllers/projects/snippets_controller.rb
@@ -1,6 +1,7 @@
class Projects::SnippetsController < Projects::ApplicationController
include ToggleAwardEmoji
include SpammableActions
+ include SnippetsActions
before_action :module_enabled
before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :toggle_award_emoji, :mark_as_spam]
@@ -37,27 +38,19 @@ class Projects::SnippetsController < Projects::ApplicationController
end
def create
- create_params = snippet_params.merge(request: request)
- @snippet = CreateSnippetService.new(@project, current_user, create_params).execute
+ create_params = snippet_params.merge(spammable_params)
- if @snippet.valid?
- respond_with(@snippet,
- location: namespace_project_snippet_path(@project.namespace,
- @project, @snippet))
- else
- render :new
- end
- end
+ @snippet = CreateSnippetService.new(@project, current_user, create_params).execute
- def edit
+ recaptcha_check_with_fallback { render :new }
end
def update
- UpdateSnippetService.new(project, current_user, @snippet,
- snippet_params).execute
- respond_with(@snippet,
- location: namespace_project_snippet_path(@project.namespace,
- @project, @snippet))
+ update_params = snippet_params.merge(spammable_params)
+
+ UpdateSnippetService.new(project, current_user, @snippet, update_params).execute
+
+ recaptcha_check_with_fallback { render :edit }
end
def show
@@ -74,15 +67,6 @@ class Projects::SnippetsController < Projects::ApplicationController
redirect_to namespace_project_snippets_path(@project.namespace, @project)
end
- def raw
- send_data(
- @snippet.content,
- type: 'text/plain; charset=utf-8',
- disposition: 'inline',
- filename: @snippet.sanitized_file_name
- )
- end
-
protected
def snippet
diff --git a/app/controllers/projects/tree_controller.rb b/app/controllers/projects/tree_controller.rb
index cb3ed0f6f9c..4f094146348 100644
--- a/app/controllers/projects/tree_controller.rb
+++ b/app/controllers/projects/tree_controller.rb
@@ -15,10 +15,10 @@ class Projects::TreeController < Projects::ApplicationController
if tree.entries.empty?
if @repository.blob_at(@commit.id, @path)
- redirect_to(
+ return redirect_to(
namespace_project_blob_path(@project.namespace, @project,
File.join(@ref, @path))
- ) and return
+ )
elsif @path.present?
return render_404
end
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
index 93a180b9036..7d81c96262f 100644
--- a/app/controllers/sessions_controller.rb
+++ b/app/controllers/sessions_controller.rb
@@ -15,11 +15,12 @@ class SessionsController < Devise::SessionsController
def new
set_minimum_password_length
- if Gitlab.config.ldap.enabled
- @ldap_servers = Gitlab::LDAP::Config.servers
- else
- @ldap_servers = []
- end
+ @ldap_servers =
+ if Gitlab.config.ldap.enabled
+ Gitlab::LDAP::Config.servers
+ else
+ []
+ end
super
end
diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb
index b169d993688..f3fd3da8b20 100644
--- a/app/controllers/snippets_controller.rb
+++ b/app/controllers/snippets_controller.rb
@@ -1,6 +1,7 @@
class SnippetsController < ApplicationController
include ToggleAwardEmoji
include SpammableActions
+ include SnippetsActions
before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :download]
@@ -22,13 +23,14 @@ class SnippetsController < ApplicationController
if params[:username].present?
@user = User.find_by(username: params[:username])
- render_404 and return unless @user
+ return render_404 unless @user
@snippets = SnippetsFinder.new.execute(current_user, {
filter: :by_user,
user: @user,
- scope: params[:scope] }).
- page(params[:page])
+ scope: params[:scope]
+ })
+ .page(params[:page])
render 'index'
else
@@ -41,19 +43,19 @@ class SnippetsController < ApplicationController
end
def create
- create_params = snippet_params.merge(request: request)
- @snippet = CreateSnippetService.new(nil, current_user, create_params).execute
+ create_params = snippet_params.merge(spammable_params)
- respond_with @snippet.becomes(Snippet)
- end
+ @snippet = CreateSnippetService.new(nil, current_user, create_params).execute
- def edit
+ recaptcha_check_with_fallback { render :new }
end
def update
- UpdateSnippetService.new(nil, current_user, @snippet,
- snippet_params).execute
- respond_with @snippet.becomes(Snippet)
+ update_params = snippet_params.merge(spammable_params)
+
+ UpdateSnippetService.new(nil, current_user, @snippet, update_params).execute
+
+ recaptcha_check_with_fallback { render :edit }
end
def show
@@ -67,18 +69,9 @@ class SnippetsController < ApplicationController
redirect_to snippets_path
end
- def raw
- send_data(
- @snippet.content,
- type: 'text/plain; charset=utf-8',
- disposition: 'inline',
- filename: @snippet.sanitized_file_name
- )
- end
-
def download
send_data(
- @snippet.content,
+ convert_line_endings(@snippet.content),
type: 'text/plain; charset=utf-8',
filename: @snippet.sanitized_file_name
)
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index 1576fc80a6b..f49301e2631 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -16,9 +16,10 @@
# label_name: string
# sort: string
# non_archived: boolean
+# iids: integer[]
#
class IssuableFinder
- NONE = '0'
+ NONE = '0'.freeze
attr_accessor :current_user, :params
@@ -40,6 +41,7 @@ class IssuableFinder
items = by_label(items)
items = by_due_date(items)
items = by_non_archived(items)
+ items = by_iids(items)
sort(items)
end
@@ -266,16 +268,11 @@ class IssuableFinder
end
def by_search(items)
- if search
- items =
- if search =~ iid_pattern
- items.where(iid: $~[:iid])
- else
- items.full_search(search)
- end
- end
+ search ? items.full_search(search) : items
+ end
- items
+ def by_iids(items)
+ params[:iids].present? ? items.where(iid: params[:iids]) : items
end
def sort(items)
diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb
index 707eddd4d29..f542f72a386 100644
--- a/app/finders/issues_finder.rb
+++ b/app/finders/issues_finder.rb
@@ -26,10 +26,6 @@ class IssuesFinder < IssuableFinder
IssuesFinder.not_restricted_by_confidentiality(current_user)
end
- def iid_pattern
- @iid_pattern ||= %r{\A#{Regexp.escape(Issue.reference_prefix)}(?<iid>\d+)\z}
- end
-
def self.not_restricted_by_confidentiality(user)
return Issue.where('issues.confidential IS NULL OR issues.confidential IS FALSE') if user.blank?
diff --git a/app/finders/merge_requests_finder.rb b/app/finders/merge_requests_finder.rb
index 8b82255445e..b76ca389f38 100644
--- a/app/finders/merge_requests_finder.rb
+++ b/app/finders/merge_requests_finder.rb
@@ -20,14 +20,4 @@ class MergeRequestsFinder < IssuableFinder
def klass
MergeRequest
end
-
- private
-
- def iid_pattern
- @iid_pattern ||= %r{\A[
- #{Regexp.escape(MergeRequest.reference_prefix)}
- #{Regexp.escape(Issue.reference_prefix)}
- ](?<iid>\d+)\z
- }x
- end
end
diff --git a/app/finders/notes_finder.rb b/app/finders/notes_finder.rb
index 4bd8c83081a..6630c6384f2 100644
--- a/app/finders/notes_finder.rb
+++ b/app/finders/notes_finder.rb
@@ -28,11 +28,12 @@ class NotesFinder
private
def init_collection
- if @params[:target_id]
- @notes = on_target(@params[:target_type], @params[:target_id])
- else
- @notes = notes_of_any_type
- end
+ @notes =
+ if @params[:target_id]
+ on_target(@params[:target_type], @params[:target_id])
+ else
+ notes_of_any_type
+ end
end
def notes_of_any_type
diff --git a/app/finders/pipelines_finder.rb b/app/finders/pipelines_finder.rb
index 32aea75486d..a9172f6767f 100644
--- a/app/finders/pipelines_finder.rb
+++ b/app/finders/pipelines_finder.rb
@@ -10,7 +10,11 @@ class PipelinesFinder
scoped_pipelines =
case scope
when 'running'
- pipelines.running_or_pending
+ pipelines.running
+ when 'pending'
+ pipelines.pending
+ when 'finished'
+ pipelines.finished
when 'branches'
from_ids(ids_for_ref(branches))
when 'tags'
diff --git a/app/finders/todos_finder.rb b/app/finders/todos_finder.rb
index a93a63bdb9b..b7f091f334d 100644
--- a/app/finders/todos_finder.rb
+++ b/app/finders/todos_finder.rb
@@ -13,7 +13,7 @@
#
class TodosFinder
- NONE = '0'
+ NONE = '0'.freeze
attr_accessor :current_user, :params
@@ -99,7 +99,7 @@ class TodosFinder
end
def type?
- type.present? && ['Issue', 'MergeRequest'].include?(type)
+ type.present? && %w(Issue MergeRequest).include?(type)
end
def type
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index bee323993a0..70419eb4bde 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -69,11 +69,12 @@ module ApplicationHelper
end
def avatar_icon(user_or_email = nil, size = nil, scale = 2)
- if user_or_email.is_a?(User)
- user = user_or_email
- else
- user = User.find_by_any_email(user_or_email.try(:downcase))
- end
+ user =
+ if user_or_email.is_a?(User)
+ user_or_email
+ else
+ User.find_by_any_email(user_or_email.try(:downcase))
+ end
if user
user.avatar_url(size) || default_avatar
@@ -296,4 +297,13 @@ module ApplicationHelper
def page_class
"issue-boards-page" if current_controller?(:boards)
end
+
+ # Returns active css class when condition returns true
+ # otherwise returns nil.
+ #
+ # Example:
+ # %li{ class: active_when(params[:filter] == '1') }
+ def active_when(condition)
+ 'active' if condition
+ end
end
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index 60485160495..4b025669f69 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -1,28 +1,15 @@
module ApplicationSettingsHelper
- def gravatar_enabled?
- current_application_settings.gravatar_enabled?
- end
-
- def signup_enabled?
- current_application_settings.signup_enabled?
- end
-
- def signin_enabled?
- current_application_settings.signin_enabled?
- end
+ delegate :gravatar_enabled?,
+ :signup_enabled?,
+ :signin_enabled?,
+ :akismet_enabled?,
+ :koding_enabled?,
+ to: :current_application_settings
def user_oauth_applications?
current_application_settings.user_oauth_applications
end
- def askimet_enabled?
- current_application_settings.akismet_enabled?
- end
-
- def koding_enabled?
- current_application_settings.koding_enabled?
- end
-
def allowed_protocols_present?
current_application_settings.enabled_git_access_protocol.present?
end
diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb
index 311a70725ab..7f32c1b5300 100644
--- a/app/helpers/blob_helper.rb
+++ b/app/helpers/blob_helper.rb
@@ -153,16 +153,17 @@ module BlobHelper
# Because we are opionated we set the cache headers ourselves.
response.cache_control[:public] = @project.public?
- if @ref && @commit && @ref == @commit.id
- # This is a link to a commit by its commit SHA. That means that the blob
- # is immutable. The only reason to invalidate the cache is if the commit
- # was deleted or if the user lost access to the repository.
- response.cache_control[:max_age] = Blob::CACHE_TIME_IMMUTABLE
- else
- # A branch or tag points at this blob. That means that the expected blob
- # value may change over time.
- response.cache_control[:max_age] = Blob::CACHE_TIME
- end
+ response.cache_control[:max_age] =
+ if @ref && @commit && @ref == @commit.id
+ # This is a link to a commit by its commit SHA. That means that the blob
+ # is immutable. The only reason to invalidate the cache is if the commit
+ # was deleted or if the user lost access to the repository.
+ Blob::CACHE_TIME_IMMUTABLE
+ else
+ # A branch or tag points at this blob. That means that the expected blob
+ # value may change over time.
+ Blob::CACHE_TIME
+ end
response.etag = @blob.id
!stale
diff --git a/app/helpers/builds_helper.rb b/app/helpers/builds_helper.rb
index ff937b5ebd2..5ac3e66bb1f 100644
--- a/app/helpers/builds_helper.rb
+++ b/app/helpers/builds_helper.rb
@@ -15,4 +15,11 @@ module BuildsHelper
log_state: @build.trace_with_state[:state].to_s
}
end
+
+ def build_failed_issue_options
+ {
+ title: "Build Failed ##{@build.id}",
+ description: namespace_project_build_url(@project.namespace, @project, @build)
+ }
+ end
end
diff --git a/app/helpers/button_helper.rb b/app/helpers/button_helper.rb
index 4c7c16d694c..195094730aa 100644
--- a/app/helpers/button_helper.rb
+++ b/app/helpers/button_helper.rb
@@ -34,7 +34,7 @@ module ButtonHelper
content_tag (append_link ? :a : :span), protocol,
class: klass,
- href: (project.http_url_to_repo if append_link),
+ href: (project.http_url_to_repo(current_user) if append_link),
data: {
html: true,
placement: placement,
diff --git a/app/helpers/emails_helper.rb b/app/helpers/emails_helper.rb
index 2843ad96efa..f927cfc998f 100644
--- a/app/helpers/emails_helper.rb
+++ b/app/helpers/emails_helper.rb
@@ -1,4 +1,6 @@
module EmailsHelper
+ include AppearancesHelper
+
# Google Actions
# https://developers.google.com/gmail/markup/reference/go-to-action
def email_action(url)
@@ -22,7 +24,7 @@ module EmailsHelper
def action_title(url)
return unless url
- ["merge_requests", "issues", "commit"].each do |action|
+ %w(merge_requests issues commit).each do |action|
if url.split("/").include?(action)
return "View #{action.humanize.singularize}"
end
@@ -49,4 +51,19 @@ module EmailsHelper
msg = "This link is valid for #{password_reset_token_valid_time}. "
msg << "After it expires, you can #{link_tag}."
end
+
+ def header_logo
+ if brand_item && brand_item.header_logo?
+ image_tag(
+ brand_item.header_logo,
+ style: 'height: 50px'
+ )
+ else
+ image_tag(
+ image_url('mailers/gitlab_header_logo.gif'),
+ size: "55x50",
+ alt: "GitLab"
+ )
+ end
+ end
end
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index 03354c235eb..860a665ae26 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -23,7 +23,7 @@ module IssuablesHelper
def issuable_json_path(issuable)
project = issuable.project
- if issuable.kind_of?(MergeRequest)
+ if issuable.is_a?(MergeRequest)
namespace_project_merge_request_path(project.namespace, project, issuable.iid, :json)
else
namespace_project_issue_path(project.namespace, project, issuable.iid, :json)
@@ -52,7 +52,7 @@ module IssuablesHelper
field_name: 'issuable_template',
selected: selected_template(issuable),
project_path: ref_project.path,
- namespace_path: ref_project.namespace.path
+ namespace_path: ref_project.namespace.full_path
}
}
@@ -198,7 +198,7 @@ module IssuablesHelper
@counts[issuable_type][state]
end
- IRRELEVANT_PARAMS_FOR_CACHE_KEY = %i[utf8 sort page]
+ IRRELEVANT_PARAMS_FOR_CACHE_KEY = %i[utf8 sort page].freeze
private_constant :IRRELEVANT_PARAMS_FOR_CACHE_KEY
def issuables_state_counter_cache_key(issuable_type, state)
diff --git a/app/helpers/javascript_helper.rb b/app/helpers/javascript_helper.rb
index 320dd89c9d3..68c09c922a6 100644
--- a/app/helpers/javascript_helper.rb
+++ b/app/helpers/javascript_helper.rb
@@ -2,6 +2,7 @@ module JavascriptHelper
def page_specific_javascript_tag(js)
javascript_include_tag asset_path(js)
end
+
def page_specific_javascript_bundle_tag(js)
javascript_include_tag(*webpack_asset_paths(js))
end
diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb
index 729928ce1dd..7011e670cee 100644
--- a/app/helpers/milestones_helper.rb
+++ b/app/helpers/milestones_helper.rb
@@ -97,7 +97,7 @@ module MilestonesHelper
def milestone_date_range(milestone)
if milestone.start_date && milestone.due_date
- "#{milestone.start_date.to_s(:medium)} - #{milestone.due_date.to_s(:medium)}"
+ "#{milestone.start_date.to_s(:medium)}–#{milestone.due_date.to_s(:medium)}"
elsif milestone.due_date
if milestone.due_date.past?
"expired on #{milestone.due_date.to_s(:medium)}"
diff --git a/app/helpers/namespaces_helper.rb b/app/helpers/namespaces_helper.rb
index 0676767d910..2e3a15bc1b9 100644
--- a/app/helpers/namespaces_helper.rb
+++ b/app/helpers/namespaces_helper.rb
@@ -1,4 +1,8 @@
module NamespacesHelper
+ def namespace_id_from(params)
+ params.dig(:project, :namespace_id) || params[:namespace_id]
+ end
+
def namespaces_options(selected = :current_user, display_path: false, extra_group: nil)
groups = current_user.owned_groups + current_user.masters_groups
@@ -29,7 +33,7 @@ module NamespacesHelper
end
def namespace_icon(namespace, size = 40)
- if namespace.kind_of?(Group)
+ if namespace.is_a?(Group)
group_icon(namespace)
else
avatar_icon(namespace.owner.email, size)
diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb
index e21178c7377..c1523b4dabf 100644
--- a/app/helpers/nav_helper.rb
+++ b/app/helpers/nav_helper.rb
@@ -1,10 +1,4 @@
module NavHelper
- def page_sidebar_class
- if pinned_nav?
- "page-sidebar-expanded page-sidebar-pinned"
- end
- end
-
def page_gutter_class
if current_path?('merge_requests#show') ||
current_path?('merge_requests#diffs') ||
@@ -32,10 +26,6 @@ module NavHelper
class_name = ''
class_name << " with-horizontal-nav" if defined?(nav) && nav
- if pinned_nav?
- class_name << " header-sidebar-expanded header-sidebar-pinned"
- end
-
class_name
end
@@ -46,8 +36,4 @@ module NavHelper
def nav_control_class
"nav-control" if current_user
end
-
- def pinned_nav?
- cookies[:pin_nav] == 'true'
- end
end
diff --git a/app/helpers/page_layout_helper.rb b/app/helpers/page_layout_helper.rb
index 7d4d049101a..3286a92a8a7 100644
--- a/app/helpers/page_layout_helper.rb
+++ b/app/helpers/page_layout_helper.rb
@@ -34,6 +34,10 @@ module PageLayoutHelper
end
end
+ def favicon
+ Rails.env.development? ? 'favicon-blue.ico' : 'favicon.ico'
+ end
+
def page_image
default = image_url('gitlab_logo.png')
diff --git a/app/helpers/preferences_helper.rb b/app/helpers/preferences_helper.rb
index dd0a4ea03f0..c3a08d76318 100644
--- a/app/helpers/preferences_helper.rb
+++ b/app/helpers/preferences_helper.rb
@@ -41,10 +41,6 @@ module PreferencesHelper
]
end
- def user_application_theme
- Gitlab::Themes.for_user(current_user).css_class
- end
-
def user_color_scheme
Gitlab::ColorSchemes.for_user(current_user).css_class
end
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index eb98204285d..4befeacc135 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -150,6 +150,15 @@ module ProjectsHelper
).html_safe
end
+ def link_to_autodeploy_doc
+ link_to 'About auto deploy', help_page_path('ci/autodeploy/index'), target: '_blank'
+ end
+
+ def autodeploy_flash_notice(branch_name)
+ "Branch <strong>#{truncate(sanitize(branch_name))}</strong> was created. To set up auto deploy, \
+ choose a GitLab CI Yaml template and commit your changes. #{link_to_autodeploy_doc}".html_safe
+ end
+
private
def repo_children_classes(field)
@@ -232,7 +241,7 @@ module ProjectsHelper
when 'ssh'
project.ssh_url_to_repo
else
- project.http_url_to_repo
+ project.http_url_to_repo(current_user)
end
end
diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb
index ff787fb4131..8ad3851fb9a 100644
--- a/app/helpers/sorting_helper.rb
+++ b/app/helpers/sorting_helper.rb
@@ -30,7 +30,7 @@ module SortingHelper
}
if current_controller?('admin/projects')
- options.merge!(sort_value_largest_repo => sort_title_largest_repo)
+ options[sort_value_largest_repo] = sort_title_largest_repo
end
options
diff --git a/app/helpers/submodule_helper.rb b/app/helpers/submodule_helper.rb
index 9a748aaaf33..fb95f2b565e 100644
--- a/app/helpers/submodule_helper.rb
+++ b/app/helpers/submodule_helper.rb
@@ -37,8 +37,8 @@ module SubmoduleHelper
end
def self_url?(url, namespace, project)
- return true if url == [ Gitlab.config.gitlab.url, '/', namespace, '/',
- project, '.git' ].join('')
+ return true if url == [Gitlab.config.gitlab.url, '/', namespace, '/',
+ project, '.git'].join('')
url == gitlab_shell.url_to_repo([namespace, '/', project].join(''))
end
@@ -48,8 +48,8 @@ module SubmoduleHelper
end
def standard_links(host, namespace, project, commit)
- base = [ 'https://', host, '/', namespace, '/', project ].join('')
- [base, [ base, '/tree/', commit ].join('')]
+ base = ['https://', host, '/', namespace, '/', project].join('')
+ [base, [base, '/tree/', commit].join('')]
end
def relative_self_links(url, commit)
diff --git a/app/helpers/tab_helper.rb b/app/helpers/tab_helper.rb
index 547f6258909..1a55ee05996 100644
--- a/app/helpers/tab_helper.rb
+++ b/app/helpers/tab_helper.rb
@@ -99,7 +99,7 @@ module TabHelper
return 'active'
end
- if ['services', 'hooks', 'deploy_keys', 'protected_branches'].include? controller.controller_name
+ if %w(services hooks deploy_keys protected_branches).include? controller.controller_name
"active"
end
end
diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb
index 845f1a0e840..7f8efb0a4ac 100644
--- a/app/helpers/todos_helper.rb
+++ b/app/helpers/todos_helper.rb
@@ -3,6 +3,10 @@ module TodosHelper
@todos_pending_count ||= current_user.todos_pending_count
end
+ def todos_count_format(count)
+ count > 99 ? '99+' : count
+ end
+
def todos_done_count
@todos_done_count ||= current_user.todos_done_count
end
@@ -146,6 +150,6 @@ module TodosHelper
private
def show_todo_state?(todo)
- (todo.target.is_a?(MergeRequest) || todo.target.is_a?(Issue)) && ['closed', 'merged'].include?(todo.target.state)
+ (todo.target.is_a?(MergeRequest) || todo.target.is_a?(Issue)) && %w(closed merged).include?(todo.target.state)
end
end
diff --git a/app/helpers/visibility_level_helper.rb b/app/helpers/visibility_level_helper.rb
index fc93acfe63e..169cedeb796 100644
--- a/app/helpers/visibility_level_helper.rb
+++ b/app/helpers/visibility_level_helper.rb
@@ -89,13 +89,9 @@ module VisibilityLevelHelper
current_application_settings.restricted_visibility_levels || []
end
- def default_project_visibility
- current_application_settings.default_project_visibility
- end
-
- def default_group_visibility
- current_application_settings.default_group_visibility
- end
+ delegate :default_project_visibility,
+ :default_group_visibility,
+ to: :current_application_settings
def skip_level?(form_model, level)
form_model.is_a?(Project) && !form_model.visibility_level_allowed?(level)
diff --git a/app/mailers/emails/pipelines.rb b/app/mailers/emails/pipelines.rb
index 9460a6cd2be..f9f45ab987b 100644
--- a/app/mailers/emails/pipelines.rb
+++ b/app/mailers/emails/pipelines.rb
@@ -22,8 +22,8 @@ module Emails
mail(bcc: recipients,
subject: pipeline_subject(status),
skip_premailer: true) do |format|
- format.html { render layout: false }
- format.text
+ format.html { render layout: 'mailer' }
+ format.text { render layout: 'mailer' }
end
end
diff --git a/app/mailers/repository_check_mailer.rb b/app/mailers/repository_check_mailer.rb
index 21db2fe04a0..22a9f5da646 100644
--- a/app/mailers/repository_check_mailer.rb
+++ b/app/mailers/repository_check_mailer.rb
@@ -1,10 +1,11 @@
class RepositoryCheckMailer < BaseMailer
def notify(failed_count)
- if failed_count == 1
- @message = "One project failed its last repository check"
- else
- @message = "#{failed_count} projects failed their last repository check"
- end
+ @message =
+ if failed_count == 1
+ "One project failed its last repository check"
+ else
+ "#{failed_count} projects failed their last repository check"
+ end
mail(
to: User.admins.pluck(:email),
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 74b358d8c40..dc36c754438 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -5,7 +5,7 @@ class ApplicationSetting < ActiveRecord::Base
add_authentication_token_field :runners_registration_token
add_authentication_token_field :health_check_access_token
- CACHE_KEY = 'application_setting.last'
+ CACHE_KEY = 'application_setting.last'.freeze
DOMAIN_LIST_SEPARATOR = %r{\s*[,;]\s* # comma or semicolon, optionally surrounded by whitespace
| # or
\s # any whitespace character
@@ -76,6 +76,12 @@ class ApplicationSetting < ActiveRecord::Base
presence: true,
numericality: { only_integer: true, greater_than: 0 }
+ validates :max_artifacts_size,
+ presence: true,
+ numericality: { only_integer: true, greater_than: 0 }
+
+ validates :default_artifacts_expire_in, presence: true, duration: true
+
validates :container_registry_token_expire_delay,
presence: true,
numericality: { only_integer: true, greater_than: 0 }
@@ -168,6 +174,7 @@ class ApplicationSetting < ActiveRecord::Base
after_sign_up_text: nil,
akismet_enabled: false,
container_registry_token_expire_delay: 5,
+ default_artifacts_expire_in: '30 days',
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'],
@@ -201,9 +208,9 @@ class ApplicationSetting < ActiveRecord::Base
sign_in_text: nil,
signin_enabled: Settings.gitlab['signin_enabled'],
signup_enabled: Settings.gitlab['signup_enabled'],
+ terminal_max_session_time: 0,
two_factor_grace_period: 48,
- user_default_external: false,
- terminal_max_session_time: 0
+ user_default_external: false
}
end
@@ -215,6 +222,14 @@ class ApplicationSetting < ActiveRecord::Base
create(defaults)
end
+ def self.human_attribute_name(attr, _options = {})
+ if attr == :default_artifacts_expire_in
+ 'Default artifacts expiration'
+ else
+ super
+ end
+ end
+
def home_page_url_column_exist
ActiveRecord::Base.connection.column_exists?(:application_settings, :home_page_url)
end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 8c1b076c2d7..77aba91f65c 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -22,8 +22,10 @@ module Ci
serialize :options
serialize :yaml_variables, Gitlab::Serializer::Ci::Variables
+ delegate :name, to: :project, prefix: true
+
validates :coverage, numericality: true, allow_blank: true
- validates_presence_of :ref
+ validates :ref, presence: true
scope :unstarted, ->() { where(runner_id: nil) }
scope :ignore_failures, ->() { where(allow_failure: false) }
@@ -62,33 +64,10 @@ module Ci
new_build.save
end
- def retry(build, user = nil)
- new_build = Ci::Build.create(
- ref: build.ref,
- tag: build.tag,
- options: build.options,
- commands: build.commands,
- tag_list: build.tag_list,
- project: build.project,
- pipeline: build.pipeline,
- name: build.name,
- allow_failure: build.allow_failure,
- stage: build.stage,
- stage_idx: build.stage_idx,
- trigger_request: build.trigger_request,
- yaml_variables: build.yaml_variables,
- when: build.when,
- user: user,
- environment: build.environment,
- status_event: 'enqueue'
- )
-
- MergeRequests::AddTodoWhenBuildFailsService
- .new(build.project, nil)
- .close(new_build)
-
- build.pipeline.mark_as_processable_after_stage(build.stage_idx)
- new_build
+ def retry(build, current_user)
+ Ci::RetryBuildService
+ .new(build.project, current_user)
+ .execute(build)
end
end
@@ -136,7 +115,7 @@ module Ci
project.builds_enabled? && commands.present? && manual? && skipped?
end
- def play(current_user = nil)
+ def play(current_user)
# Try to queue a current build
if self.enqueue
self.update(user: current_user)
@@ -256,10 +235,6 @@ module Ci
gl_project_id
end
- def project_name
- project.name
- end
-
def repo_url
auth = "gitlab-ci-token:#{ensure_token!}@"
project.http_url_to_repo.sub(/^https?:\/\//) do |prefix|
@@ -280,7 +255,7 @@ module Ci
return unless regex
matches = text.scan(Regexp.new(regex)).last
- matches = matches.last if matches.kind_of?(Array)
+ matches = matches.last if matches.is_a?(Array)
coverage = matches.gsub(/\d+(\.\d+)?/).first
if coverage.present?
@@ -509,7 +484,7 @@ module Ci
def artifacts_expire_in=(value)
self.artifacts_expire_at =
if value
- Time.now + ChronicDuration.parse(value)
+ ChronicDuration.parse(value)&.seconds&.from_now
end
end
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index bbc358adb83..80e11a5b58f 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -14,9 +14,11 @@ module Ci
has_many :builds, foreign_key: :commit_id
has_many :trigger_requests, dependent: :destroy, foreign_key: :commit_id
- validates_presence_of :sha, unless: :importing?
- validates_presence_of :ref, unless: :importing?
- validates_presence_of :status, unless: :importing?
+ delegate :id, to: :project, prefix: true
+
+ validates :sha, presence: { unless: :importing? }
+ validates :ref, presence: { unless: :importing? }
+ validates :status, presence: { unless: :importing? }
validate :valid_commit_sha, unless: :importing?
after_create :keep_around_commits, unless: :importing?
@@ -93,8 +95,11 @@ module Ci
.select("max(#{quoted_table_name}.id)")
.group(:ref, :sha)
- relation = ref ? where(ref: ref) : self
- relation.where(id: max_id)
+ if ref
+ where(ref: ref, id: max_id.where(ref: ref))
+ else
+ where(id: max_id)
+ end
end
def self.latest_status(ref = nil)
@@ -150,10 +155,6 @@ module Ci
builds.latest.with_artifacts_not_expired.includes(project: [:namespace])
end
- def project_id
- project.id
- end
-
# For now the only user who participates is the user who triggered
def participants(_current_user = nil)
Array(user)
@@ -214,21 +215,17 @@ module Ci
def cancel_running
Gitlab::OptimisticLocking.retry_lock(
statuses.cancelable) do |cancelable|
- cancelable.each(&:cancel)
+ cancelable.find_each(&:cancel)
end
end
- def retry_failed(user)
- Gitlab::OptimisticLocking.retry_lock(
- builds.latest.failed_or_canceled) do |failed_or_canceled|
- failed_or_canceled.select(&:retryable?).each do |build|
- Ci::Build.retry(build, user)
- end
- end
+ def retry_failed(current_user)
+ Ci::RetryPipelineService.new(project, current_user)
+ .execute(self)
end
def mark_as_processable_after_stage(stage_idx)
- builds.skipped.where('stage_idx > ?', stage_idx).find_each(&:process)
+ builds.skipped.after_stage(stage_idx).find_each(&:process)
end
def latest?
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index ed1843ba005..4863c34a6a6 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -4,8 +4,8 @@ module Ci
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]
+ AVAILABLE_SCOPES = %w[specific shared active paused online].freeze
+ FORM_EDITABLE = %i[description tag_list active run_untagged locked].freeze
has_many :builds
has_many :runner_projects, dependent: :destroy
@@ -22,8 +22,6 @@ 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)
@@ -40,6 +38,8 @@ module Ci
acts_as_taggable
+ after_destroy :cleanup_runner_queue
+
# Searches for runners matching the given query.
#
# This method uses ILIKE on PostgreSQL and LIKE on MySQL.
@@ -147,14 +147,14 @@ module Ci
private
- def runner_queue_key
- "runner:build_queue:#{self.token}"
+ def cleanup_runner_queue
+ Gitlab::Redis.with do |redis|
+ redis.del(runner_queue_key)
+ end
end
- def form_editable_changed?
- FORM_EDITABLE.any? do |editable|
- public_send("#{editable}_changed?")
- end
+ def runner_queue_key
+ "runner:build_queue:#{self.token}"
end
def tag_constraints
diff --git a/app/models/ci/runner_project.rb b/app/models/ci/runner_project.rb
index 1f9baeca5b1..234376a7e4c 100644
--- a/app/models/ci/runner_project.rb
+++ b/app/models/ci/runner_project.rb
@@ -5,6 +5,6 @@ module Ci
belongs_to :runner
belongs_to :project, foreign_key: :gl_project_id
- validates_uniqueness_of :runner_id, scope: :gl_project_id
+ validates :runner_id, uniqueness: { scope: :gl_project_id }
end
end
diff --git a/app/models/ci/trigger.rb b/app/models/ci/trigger.rb
index 62889fe80d8..39a1dd86241 100644
--- a/app/models/ci/trigger.rb
+++ b/app/models/ci/trigger.rb
@@ -7,8 +7,8 @@ module Ci
belongs_to :project, foreign_key: :gl_project_id
has_many :trigger_requests, dependent: :destroy
- validates_presence_of :token
- validates_uniqueness_of :token
+ validates :token, presence: true
+ validates :token, uniqueness: true
before_validation :set_default_values
diff --git a/app/models/commit.rb b/app/models/commit.rb
index 46f06733da1..0a18986ef26 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -22,12 +22,12 @@ class Commit
DIFF_HARD_LIMIT_LINES = 50000
# The SHA can be between 7 and 40 hex characters.
- COMMIT_SHA_PATTERN = '\h{7,40}'
+ COMMIT_SHA_PATTERN = '\h{7,40}'.freeze
class << self
def decorate(commits, project)
commits.map do |commit|
- if commit.kind_of?(Commit)
+ if commit.is_a?(Commit)
commit
else
self.new(commit, project)
@@ -105,7 +105,7 @@ class Commit
end
def diff_line_count
- @diff_line_count ||= Commit::diff_line_count(raw_diffs)
+ @diff_line_count ||= Commit.diff_line_count(raw_diffs)
@diff_line_count
end
@@ -122,11 +122,12 @@ class Commit
def full_title
return @full_title if @full_title
- if safe_message.blank?
- @full_title = no_commit_message
- else
- @full_title = safe_message.split("\n", 2).first
- end
+ @full_title =
+ if safe_message.blank?
+ no_commit_message
+ else
+ safe_message.split("\n", 2).first
+ end
end
# Returns the commits description
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index 9547c57b2ae..fc750a3e5e9 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -10,10 +10,11 @@ class CommitStatus < ActiveRecord::Base
belongs_to :user
delegate :commit, to: :pipeline
+ delegate :sha, :short_sha, to: :pipeline
validates :pipeline, presence: true, unless: :importing?
- validates_presence_of :name
+ validates :name, presence: true
alias_attribute :author, :user
@@ -23,9 +24,6 @@ class CommitStatus < ActiveRecord::Base
where(id: max_id.group(:name, :commit_id))
end
- scope :retried, -> { where.not(id: latest) }
- scope :ordered, -> { order(:name) }
-
scope :failed_but_allowed, -> do
where(allow_failure: true, status: [:failed, :canceled])
end
@@ -36,8 +34,11 @@ class CommitStatus < ActiveRecord::Base
false, all_state_names - [:failed, :canceled])
end
+ scope :retried, -> { where.not(id: latest) }
+ scope :ordered, -> { order(:name) }
scope :latest_ordered, -> { latest.ordered.includes(project: :namespace) }
scope :retried_ordered, -> { retried.ordered.includes(project: :namespace) }
+ scope :after_stage, -> (index) { where('stage_idx > ?', index) }
state_machine :status do
event :enqueue do
@@ -102,8 +103,6 @@ class CommitStatus < ActiveRecord::Base
end
end
- delegate :sha, :short_sha, to: :pipeline
-
def before_sha
pipeline.before_sha || Gitlab::Git::BLANK_SHA
end
diff --git a/app/models/concerns/cache_markdown_field.rb b/app/models/concerns/cache_markdown_field.rb
index a600f9c14c5..8ea95beed79 100644
--- a/app/models/concerns/cache_markdown_field.rb
+++ b/app/models/concerns/cache_markdown_field.rb
@@ -11,14 +11,15 @@ module CacheMarkdownField
# Knows about the relationship between markdown and html field names, and
# stores the rendering contexts for the latter
class FieldData
- extend Forwardable
-
def initialize
@data = {}
end
- def_delegators :@data, :[], :[]=
- def_delegator :@data, :keys, :markdown_fields
+ delegate :[], :[]=, to: :@data
+
+ def markdown_fields
+ @data.keys
+ end
def html_field(markdown_field)
"#{markdown_field}_html"
@@ -45,7 +46,7 @@ module CacheMarkdownField
Project
Release
Snippet
- ]
+ ].freeze
def self.caching_classes
CACHING_CLASSES.map(&:constantize)
diff --git a/app/models/concerns/case_sensitivity.rb b/app/models/concerns/case_sensitivity.rb
index fe0cea8465f..034e9f40ff0 100644
--- a/app/models/concerns/case_sensitivity.rb
+++ b/app/models/concerns/case_sensitivity.rb
@@ -13,11 +13,12 @@ module CaseSensitivity
params.each do |key, value|
column = ActiveRecord::Base.connection.quote_table_name(key)
- if cast_lower
- condition = "LOWER(#{column}) = LOWER(:value)"
- else
- condition = "#{column} = :value"
- end
+ condition =
+ if cast_lower
+ "LOWER(#{column}) = LOWER(:value)"
+ else
+ "#{column} = :value"
+ end
criteria = criteria.where(condition, value: value)
end
diff --git a/app/models/concerns/has_status.rb b/app/models/concerns/has_status.rb
index 431c0354969..aea359e70bb 100644
--- a/app/models/concerns/has_status.rb
+++ b/app/models/concerns/has_status.rb
@@ -1,12 +1,12 @@
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]
- COMPLETED_STATUSES = %w[success failed canceled skipped]
- ORDERED_STATUSES = %w[failed pending running canceled success skipped]
+ DEFAULT_STATUS = 'created'.freeze
+ AVAILABLE_STATUSES = %w[created pending running success failed canceled skipped].freeze
+ STARTED_STATUSES = %w[running success failed skipped].freeze
+ ACTIVE_STATUSES = %w[pending running].freeze
+ COMPLETED_STATUSES = %w[success failed canceled skipped].freeze
+ ORDERED_STATUSES = %w[failed pending running canceled success skipped].freeze
class_methods do
def status_sql
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 5f53c48fc88..37c727b5d9f 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -16,9 +16,9 @@ module Issuable
include TimeTrackable
# This object is used to gather issuable meta data for displaying
- # upvotes, downvotes and notes count for issues and merge requests
+ # upvotes, downvotes, notes and closing merge requests count for issues and merge requests
# lists avoiding n+1 queries and improving performance.
- IssuableMeta = Struct.new(:upvotes, :downvotes, :notes_count)
+ IssuableMeta = Struct.new(:upvotes, :downvotes, :notes_count, :merge_requests_count)
included do
cache_markdown_field :title, pipeline: :single_line
@@ -46,6 +46,17 @@ module Issuable
has_one :metrics
+ delegate :name,
+ :email,
+ to: :author,
+ prefix: true
+
+ delegate :name,
+ :email,
+ to: :assignee,
+ allow_nil: true,
+ prefix: true
+
validates :author, presence: true
validates :title, presence: true, length: { maximum: 255 }
@@ -68,21 +79,10 @@ module Issuable
scope :without_label, -> { joins("LEFT OUTER JOIN label_links ON label_links.target_type = '#{name}' AND label_links.target_id = #{table_name}.id").where(label_links: { id: nil }) }
scope :join_project, -> { joins(:project) }
- scope :inc_notes_with_associations, -> { includes(notes: [ :project, :author, :award_emoji ]) }
+ scope :inc_notes_with_associations, -> { includes(notes: [:project, :author, :award_emoji]) }
scope :references_project, -> { references(:project) }
scope :non_archived, -> { join_project.where(projects: { archived: false }) }
- delegate :name,
- :email,
- to: :author,
- prefix: true
-
- delegate :name,
- :email,
- to: :assignee,
- allow_nil: true,
- prefix: true
-
attr_mentionable :title, pipeline: :single_line
attr_mentionable :description
@@ -182,7 +182,7 @@ module Issuable
def grouping_columns(sort)
grouping_columns = [arel_table[:id]]
- if ["milestone_due_desc", "milestone_due_asc"].include?(sort)
+ if %w(milestone_due_desc milestone_due_asc).include?(sort)
milestone_table = Milestone.arel_table
grouping_columns << milestone_table[:id]
grouping_columns << milestone_table[:due_date]
@@ -235,7 +235,7 @@ module Issuable
# DEPRECATED
repository: project.hook_attrs.slice(:name, :url, :description, :homepage)
}
- hook_data.merge!(assignee: assignee.hook_attrs) if assignee
+ hook_data[:assignee] = assignee.hook_attrs if assignee
hook_data
end
diff --git a/app/models/concerns/reactive_service.rb b/app/models/concerns/reactive_service.rb
index e1f868a299b..713246039c1 100644
--- a/app/models/concerns/reactive_service.rb
+++ b/app/models/concerns/reactive_service.rb
@@ -5,6 +5,6 @@ module ReactiveService
include ReactiveCaching
# Default cache key: class name + project_id
- self.reactive_cache_key = ->(service) { [ service.class.model_name.singular, service.project_id ] }
+ self.reactive_cache_key = ->(service) { [service.class.model_name.singular, service.project_id] }
end
end
diff --git a/app/models/concerns/sortable.rb b/app/models/concerns/sortable.rb
index 7edb0acd56c..b9a2d812edd 100644
--- a/app/models/concerns/sortable.rb
+++ b/app/models/concerns/sortable.rb
@@ -46,11 +46,12 @@ module Sortable
where("label_links.target_id = #{target_column}").
reorder(nil)
- if target_type_column
- query = query.where("label_links.target_type = #{target_type_column}")
- else
- query = query.where(label_links: { target_type: target_type })
- end
+ query =
+ if target_type_column
+ query.where("label_links.target_type = #{target_type_column}")
+ else
+ query.where(label_links: { target_type: target_type })
+ end
query = query.where.not(title: excluded_labels) if excluded_labels.present?
diff --git a/app/models/concerns/spammable.rb b/app/models/concerns/spammable.rb
index 423ae98a60e..107e6764ba2 100644
--- a/app/models/concerns/spammable.rb
+++ b/app/models/concerns/spammable.rb
@@ -13,7 +13,7 @@ module Spammable
attr_accessor :spam
attr_accessor :spam_log
- after_validation :check_for_spam, on: :create
+ after_validation :check_for_spam, on: [:create, :update]
cattr_accessor :spammable_attrs, instance_accessor: false do
[]
@@ -22,6 +22,10 @@ module Spammable
delegate :ip_address, :user_agent, to: :user_agent_detail, allow_nil: true
end
+ def submittable_as_spam_by?(current_user)
+ current_user && current_user.admin? && submittable_as_spam?
+ end
+
def submittable_as_spam?
if user_agent_detail
user_agent_detail.submittable? && current_application_settings.akismet_enabled
diff --git a/app/models/concerns/uniquify.rb b/app/models/concerns/uniquify.rb
new file mode 100644
index 00000000000..a7fe5951b6e
--- /dev/null
+++ b/app/models/concerns/uniquify.rb
@@ -0,0 +1,30 @@
+class Uniquify
+ # Return a version of the given 'base' string that is unique
+ # by appending a counter to it. Uniqueness is determined by
+ # repeated calls to the passed block.
+ #
+ # If `base` is a function/proc, we expect that calling it with a
+ # candidate counter returns a string to test/return.
+ def string(base)
+ @base = base
+ @counter = nil
+
+ increment_counter! while yield(base_string)
+ base_string
+ end
+
+ private
+
+ def base_string
+ if @base.respond_to?(:call)
+ @base.call(@counter)
+ else
+ "#{@base}#{@counter}"
+ end
+ end
+
+ def increment_counter!
+ @counter ||= 0
+ @counter += 1
+ end
+end
diff --git a/app/models/diff_note.rb b/app/models/diff_note.rb
index 559b3075905..895a91139c9 100644
--- a/app/models/diff_note.rb
+++ b/app/models/diff_note.rb
@@ -8,7 +8,7 @@ class DiffNote < Note
validates :position, presence: true
validates :diff_line, presence: true
validates :line_code, presence: true, line_code: true
- validates :noteable_type, inclusion: { in: ['Commit', 'MergeRequest'] }
+ validates :noteable_type, inclusion: { in: %w(Commit MergeRequest) }
validates :resolved_by, presence: true, if: :resolved?
validate :positions_complete
validate :verify_supported
diff --git a/app/models/event.rb b/app/models/event.rb
index e5027df3f8a..d7ca8e3c599 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -36,7 +36,7 @@ class Event < ActiveRecord::Base
scope :code_push, -> { where(action: PUSHED) }
scope :in_projects, ->(projects) do
- where(project_id: projects).recent
+ where(project_id: projects.pluck(:id)).recent
end
scope :with_associations, -> { includes(:author, :project, project: :namespace).preload(:target) }
@@ -47,7 +47,7 @@ class Event < ActiveRecord::Base
def contributions
where("action = ? OR (target_type IN (?) AND action IN (?)) OR (target_type = ? AND action = ?)",
Event::PUSHED,
- ["MergeRequest", "Issue"], [Event::CREATED, Event::CLOSED, Event::MERGED],
+ %w(MergeRequest Issue), [Event::CREATED, Event::CLOSED, Event::MERGED],
"Note", Event::COMMENTED)
end
diff --git a/app/models/external_issue.rb b/app/models/external_issue.rb
index 26712c19b5a..b973bbcd8da 100644
--- a/app/models/external_issue.rb
+++ b/app/models/external_issue.rb
@@ -43,7 +43,7 @@ class ExternalIssue
end
def reference_link_text(from_project = nil)
- return "##{id}" if /^\d+$/.match(id)
+ return "##{id}" if id =~ /^\d+$/
id
end
diff --git a/app/models/issue.rb b/app/models/issue.rb
index d8826b65fcc..de90f19f854 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -15,8 +15,6 @@ class Issue < ActiveRecord::Base
DueThisWeek = DueDateStruct.new('Due This Week', 'week').freeze
DueThisMonth = DueDateStruct.new('Due This Month', 'month').freeze
- ActsAsTaggableOn.strict_case_match = true
-
belongs_to :project
belongs_to :moved_to, class_name: 'Issue'
diff --git a/app/models/label.rb b/app/models/label.rb
index 5b6b9a7a736..f68a8c9cff2 100644
--- a/app/models/label.rb
+++ b/app/models/label.rb
@@ -11,7 +11,7 @@ class Label < ActiveRecord::Base
cache_markdown_field :description, pipeline: :single_line
- DEFAULT_COLOR = '#428BCA'
+ DEFAULT_COLOR = '#428BCA'.freeze
default_value_for :color, DEFAULT_COLOR
diff --git a/app/models/member.rb b/app/models/member.rb
index d07f270b757..0545bd4eedf 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -10,6 +10,8 @@ class Member < ActiveRecord::Base
belongs_to :user
belongs_to :source, polymorphic: true
+ delegate :name, :username, :email, to: :user, prefix: true
+
validates :user, presence: true, unless: :invite?
validates :source, presence: true
validates :user_id, uniqueness: { scope: [:source_type, :source_id],
@@ -73,8 +75,6 @@ class Member < ActiveRecord::Base
after_destroy :post_destroy_hook, unless: :pending?
after_commit :refresh_member_authorized_projects
- delegate :name, :username, :email, to: :user, prefix: true
-
default_value_for :notification_level, NotificationSetting.levels[:global]
class << self
diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb
index 204f34f0269..446f9f8f8a7 100644
--- a/app/models/members/group_member.rb
+++ b/app/models/members/group_member.rb
@@ -1,11 +1,11 @@
class GroupMember < Member
- SOURCE_TYPE = 'Namespace'
+ SOURCE_TYPE = 'Namespace'.freeze
belongs_to :group, foreign_key: 'source_id'
# Make sure group member points only to group as it source
default_value_for :source_type, SOURCE_TYPE
- validates_format_of :source_type, with: /\ANamespace\z/
+ validates :source_type, format: { with: /\ANamespace\z/ }
default_scope { where(source_type: SOURCE_TYPE) }
def self.access_level_roles
diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb
index 008fff0857c..912820b51ac 100644
--- a/app/models/members/project_member.rb
+++ b/app/models/members/project_member.rb
@@ -1,5 +1,5 @@
class ProjectMember < Member
- SOURCE_TYPE = 'Project'
+ SOURCE_TYPE = 'Project'.freeze
include Gitlab::ShellAdapter
@@ -7,7 +7,7 @@ class ProjectMember < Member
# Make sure project member points only to project as it source
default_value_for :source_type, SOURCE_TYPE
- validates_format_of :source_type, with: /\AProject\z/
+ validates :source_type, format: { with: /\AProject\z/ }
validates :access_level, inclusion: { in: Gitlab::Access.values }
default_scope { where(source_type: SOURCE_TYPE) }
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 204d2b153ad..d6e7ed87555 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -91,10 +91,6 @@ 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?]
@@ -203,7 +199,11 @@ class MergeRequest < ActiveRecord::Base
end
def diff_size
- opts = diff_options || {}
+ # The `#diffs` method ends up at an instance of a class inheriting from
+ # `Gitlab::Diff::FileCollection::Base`, so use those options as defaults
+ # here too, to get the same diff size without performing highlighting.
+ #
+ opts = Gitlab::Diff::FileCollection::Base.default_options.merge(diff_options || {})
raw_diffs(opts).size
end
@@ -527,7 +527,7 @@ class MergeRequest < ActiveRecord::Base
}
if diff_head_commit
- attrs.merge!(last_commit: diff_head_commit.hook_attrs)
+ attrs[:last_commit] = diff_head_commit.hook_attrs
end
attributes.merge!(attrs)
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index 70bad2a4396..baee00b8fcd 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -7,7 +7,7 @@ class MergeRequestDiff < ActiveRecord::Base
COMMITS_SAFE_SIZE = 100
# Valid types of serialized diffs allowed by Gitlab::Git::Diff
- VALID_CLASSES = [Hash, Rugged::Patch, Rugged::Diff::Delta]
+ VALID_CLASSES = [Hash, Rugged::Patch, Rugged::Diff::Delta].freeze
belongs_to :merge_request
diff --git a/app/models/merge_requests_closing_issues.rb b/app/models/merge_requests_closing_issues.rb
index ab597c37947..daafb137be4 100644
--- a/app/models/merge_requests_closing_issues.rb
+++ b/app/models/merge_requests_closing_issues.rb
@@ -4,4 +4,12 @@ class MergeRequestsClosingIssues < ActiveRecord::Base
validates :merge_request_id, uniqueness: { scope: :issue_id }, presence: true
validates :issue_id, presence: true
+
+ class << self
+ def count_for_collection(ids)
+ group(:issue_id).
+ where(issue_id: ids).
+ pluck('issue_id', 'COUNT(*) as count')
+ end
+ end
end
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 6de4d08fc28..3137dd32f93 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -42,7 +42,7 @@ class Namespace < ActiveRecord::Base
after_commit :refresh_access_of_projects_invited_groups, on: :update, if: -> { previous_changes.key?('share_with_group_lock') }
# Save the storage paths before the projects are destroyed to use them on after destroy
- before_destroy(prepend: true) { @old_repository_storage_paths = repository_storage_paths }
+ before_destroy(prepend: true) { prepare_for_destroy }
after_destroy :rm_dir
scope :root, -> { where('type IS NULL') }
@@ -98,14 +98,8 @@ class Namespace < ActiveRecord::Base
# Work around that by setting their username to "blank", followed by a counter.
path = "blank" if path.blank?
- counter = 0
- base = path
- while Namespace.find_by_path_or_name(path)
- counter += 1
- path = "#{base}#{counter}"
- end
-
- path
+ uniquify = Uniquify.new
+ uniquify.string(path) { |s| Namespace.find_by_path_or_name(s) }
end
end
@@ -211,6 +205,14 @@ class Namespace < ActiveRecord::Base
parent_id_changed?
end
+ def prepare_for_destroy
+ old_repository_storage_paths
+ end
+
+ def old_repository_storage_paths
+ @old_repository_storage_paths ||= repository_storage_paths
+ end
+
private
def repository_storage_paths
@@ -224,7 +226,7 @@ class Namespace < ActiveRecord::Base
def rm_dir
# Remove the namespace directory in all storages paths used by member projects
- @old_repository_storage_paths.each do |repository_storage_path|
+ old_repository_storage_paths.each do |repository_storage_path|
# Move namespace directory into trash.
# We will remove it later async
new_path = "#{path}+#{id}+deleted"
diff --git a/app/models/network/graph.rb b/app/models/network/graph.rb
index b524ca50ee8..0bbc9451ffd 100644
--- a/app/models/network/graph.rb
+++ b/app/models/network/graph.rb
@@ -188,11 +188,12 @@ module Network
end
# and mark it as reserved
- if parent_time.nil?
- min_time = leaves.first.time
- else
- min_time = parent_time + 1
- end
+ min_time =
+ if parent_time.nil?
+ leaves.first.time
+ else
+ parent_time + 1
+ end
max_time = leaves.last.time
leaves.last.parents(@map).each do |parent|
diff --git a/app/models/note.rb b/app/models/note.rb
index 029fe667a45..d6d5396afa5 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -72,7 +72,7 @@ class Note < ActiveRecord::Base
scope :inc_author, ->{ includes(:author) }
scope :inc_relations_for_view, ->{ includes(:project, :author, :updated_by, :resolved_by, :award_emoji) }
- scope :diff_notes, ->{ where(type: ['LegacyDiffNote', 'DiffNote']) }
+ scope :diff_notes, ->{ where(type: %w(LegacyDiffNote DiffNote)) }
scope :non_diff_notes, ->{ where(type: ['Note', nil]) }
scope :with_associations, -> do
diff --git a/app/models/notification_setting.rb b/app/models/notification_setting.rb
index 58f6214bea7..52577bd52ea 100644
--- a/app/models/notification_setting.rb
+++ b/app/models/notification_setting.rb
@@ -35,11 +35,11 @@ class NotificationSetting < ActiveRecord::Base
:merge_merge_request,
:failed_pipeline,
:success_pipeline
- ]
+ ].freeze
EXCLUDED_WATCHER_EVENTS = [
:success_pipeline
- ]
+ ].freeze
store :events, accessors: EMAIL_EVENTS, coder: JSON
diff --git a/app/models/pages_domain.rb b/app/models/pages_domain.rb
index 0b9ebf1ffe2..f2f2fc1e32a 100644
--- a/app/models/pages_domain.rb
+++ b/app/models/pages_domain.rb
@@ -2,7 +2,7 @@ class PagesDomain < ActiveRecord::Base
belongs_to :project
validates :domain, hostname: true
- validates_uniqueness_of :domain, case_sensitive: false
+ validates :domain, uniqueness: { case_sensitive: false }
validates :certificate, certificate: true, allow_nil: true, allow_blank: true
validates :key, certificate_key: true, allow_nil: true, allow_blank: true
diff --git a/app/models/project.rb b/app/models/project.rb
index ed43fc2e575..e06fc30dc8a 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -22,7 +22,7 @@ class Project < ActiveRecord::Base
class BoardLimitExceeded < StandardError; end
NUMBER_OF_PERMITTED_BOARDS = 1
- UNKNOWN_IMPORT_URL = 'http://unknown.git'
+ UNKNOWN_IMPORT_URL = 'http://unknown.git'.freeze
cache_markdown_field :description, pipeline: :description
@@ -70,8 +70,7 @@ class Project < ActiveRecord::Base
after_validation :check_pending_delete
- ActsAsTaggableOn.strict_case_match = true
- acts_as_taggable_on :tags
+ acts_as_taggable
attr_accessor :new_default_branch
attr_accessor :old_path_with_namespace
@@ -172,9 +171,11 @@ class Project < ActiveRecord::Base
accepts_nested_attributes_for :project_feature
delegate :name, to: :owner, allow_nil: true, prefix: true
+ delegate :count, to: :forks, prefix: true
delegate :members, to: :team, prefix: true
delegate :add_user, to: :team
delegate :add_guest, :add_reporter, :add_developer, :add_master, to: :team
+ delegate :empty_repo?, to: :repository
# Validations
validates :creator, presence: true, on: :create
@@ -191,8 +192,8 @@ class Project < ActiveRecord::Base
format: { with: Gitlab::Regex.project_path_regex,
message: Gitlab::Regex.project_path_regex_message }
validates :namespace, presence: true
- validates_uniqueness_of :name, scope: :namespace_id
- validates_uniqueness_of :path, scope: :namespace_id
+ validates :name, uniqueness: { scope: :namespace_id }
+ validates :path, uniqueness: { scope: :namespace_id }
validates :import_url, addressable_url: true, if: :external_import?
validates :star_count, numericality: { greater_than_or_equal_to: 0 }
validate :check_limit, on: :create
@@ -214,6 +215,8 @@ class Project < ActiveRecord::Base
# Scopes
default_scope { where(pending_delete: false) }
+ scope :with_deleted, -> { unscope(where: :pending_delete) }
+
scope :sorted_by_activity, -> { reorder(last_activity_at: :desc) }
scope :sorted_by_stars, -> { reorder('projects.star_count DESC') }
@@ -356,7 +359,7 @@ class Project < ActiveRecord::Base
end
def reference_pattern
- name_pattern = Gitlab::Regex::NAMESPACE_REGEX_STR
+ name_pattern = Gitlab::Regex::FULL_NAMESPACE_REGEX_STR
%r{
((?<namespace>#{name_pattern})\/)?
@@ -451,13 +454,14 @@ class Project < ActiveRecord::Base
end
def add_import_job
- if forked?
- job_id = RepositoryForkWorker.perform_async(id, forked_from_project.repository_storage_path,
- forked_from_project.path_with_namespace,
- self.namespace.full_path)
- else
- job_id = RepositoryImportWorker.perform_async(self.id)
- end
+ job_id =
+ if forked?
+ RepositoryForkWorker.perform_async(id, forked_from_project.repository_storage_path,
+ forked_from_project.path_with_namespace,
+ self.namespace.full_path)
+ else
+ RepositoryImportWorker.perform_async(self.id)
+ end
if job_id
Rails.logger.info "Import job started for #{path_with_namespace} with job ID #{job_id}"
@@ -550,7 +554,7 @@ class Project < ActiveRecord::Base
end
def check_limit
- unless creator.can_create_project? or namespace.kind == 'group'
+ unless creator.can_create_project? || namespace.kind == 'group'
projects_limit = creator.projects_limit
if projects_limit == 0
@@ -835,10 +839,6 @@ class Project < ActiveRecord::Base
false
end
- def empty_repo?
- repository.empty_repo?
- end
-
def repo
repository.raw
end
@@ -847,10 +847,6 @@ class Project < ActiveRecord::Base
gitlab_shell.url_to_repo(path_with_namespace)
end
- def namespace_dir
- namespace.try(:path) || ''
- end
-
def repo_exists?
@repo_exists ||= repository.exists?
rescue
@@ -873,8 +869,14 @@ class Project < ActiveRecord::Base
url_to_repo
end
- def http_url_to_repo
- "#{web_url}.git"
+ def http_url_to_repo(user = nil)
+ url = web_url
+
+ if user
+ url.sub!(%r{\Ahttps?://}) { |protocol| "#{protocol}#{user.username}@" }
+ end
+
+ "#{url}.git"
end
# Check if current branch name is marked as protected in the system
@@ -899,8 +901,8 @@ class Project < ActiveRecord::Base
def rename_repo
path_was = previous_changes['path'].first
- old_path_with_namespace = File.join(namespace_dir, path_was)
- new_path_with_namespace = File.join(namespace_dir, path)
+ old_path_with_namespace = File.join(namespace.full_path, path_was)
+ new_path_with_namespace = File.join(namespace.full_path, path)
Rails.logger.error "Attempting to rename #{old_path_with_namespace} -> #{new_path_with_namespace}"
@@ -1026,10 +1028,6 @@ class Project < ActiveRecord::Base
forked? && project == forked_from_project
end
- def forks_count
- forks.count
- end
-
def origin_merge_requests
merge_requests.where(source_project_id: self.id)
end
diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb
index 03194fc2141..e3ef4919b28 100644
--- a/app/models/project_feature.rb
+++ b/app/models/project_feature.rb
@@ -18,7 +18,7 @@ class ProjectFeature < ActiveRecord::Base
PRIVATE = 10
ENABLED = 20
- FEATURES = %i(issues merge_requests wiki snippets builds repository)
+ FEATURES = %i(issues merge_requests wiki snippets builds repository).freeze
class << self
def access_level_attribute(feature)
diff --git a/app/models/project_group_link.rb b/app/models/project_group_link.rb
index 5cb6b0c527d..ac1e9ab2b0b 100644
--- a/app/models/project_group_link.rb
+++ b/app/models/project_group_link.rb
@@ -33,8 +33,15 @@ class ProjectGroupLink < ActiveRecord::Base
private
def different_group
- if self.group && self.project && self.project.group == self.group
- errors.add(:base, "Project cannot be shared with the project it is in.")
+ return unless self.group && self.project
+
+ project_group = self.project.group
+ return unless project_group
+
+ group_ids = project_group.ancestors.map(&:id).push(project_group.id)
+
+ if group_ids.include?(self.group.id)
+ errors.add(:base, "Project cannot be shared with the group it is in or one of its ancestors.")
end
end
diff --git a/app/models/project_services/buildkite_service.rb b/app/models/project_services/buildkite_service.rb
index 0956c4a4ede..5fb95050b83 100644
--- a/app/models/project_services/buildkite_service.rb
+++ b/app/models/project_services/buildkite_service.rb
@@ -3,7 +3,7 @@ require "addressable/uri"
class BuildkiteService < CiService
include ReactiveService
- ENDPOINT = "https://buildkite.com"
+ ENDPOINT = "https://buildkite.com".freeze
prop_accessor :project_url, :token
boolean_accessor :enable_ssl_verification
diff --git a/app/models/project_services/chat_message/base_message.rb b/app/models/project_services/chat_message/base_message.rb
index a03605d01fb..86d271a3f69 100644
--- a/app/models/project_services/chat_message/base_message.rb
+++ b/app/models/project_services/chat_message/base_message.rb
@@ -30,5 +30,9 @@ module ChatMessage
def attachment_color
'#345'
end
+
+ def link(text, url)
+ "[#{text}](#{url})"
+ end
end
end
diff --git a/app/models/project_services/chat_message/build_message.rb b/app/models/project_services/chat_message/build_message.rb
index 53e35cb21bf..c776e0a20c4 100644
--- a/app/models/project_services/chat_message/build_message.rb
+++ b/app/models/project_services/chat_message/build_message.rb
@@ -7,7 +7,11 @@ module ChatMessage
attr_reader :project_name
attr_reader :project_url
attr_reader :user_name
+ attr_reader :user_url
attr_reader :duration
+ attr_reader :stage
+ attr_reader :build_id
+ attr_reader :build_name
def initialize(params)
@sha = params[:sha]
@@ -17,7 +21,11 @@ module ChatMessage
@project_url = params[:project_url]
@status = params[:commit][:status]
@user_name = params[:commit][:author_name]
+ @user_url = params[:commit][:author_url]
@duration = params[:commit][:duration]
+ @stage = params[:build_stage]
+ @build_name = params[:build_name]
+ @build_id = params[:build_id]
end
def pretext
@@ -35,7 +43,19 @@ module ChatMessage
private
def message
- "#{project_link}: Commit #{commit_link} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status} in #{duration} #{'second'.pluralize(duration)}"
+ "#{project_link}: Commit #{commit_link} of #{branch_link} #{ref_type} by #{user_link} #{humanized_status} on build #{build_link} of stage #{stage} in #{duration} #{'second'.pluralize(duration)}"
+ end
+
+ def build_url
+ "#{project_url}/builds/#{build_id}"
+ end
+
+ def build_link
+ link(build_name, build_url)
+ end
+
+ def user_link
+ link(user_name, user_url)
end
def format(string)
@@ -64,11 +84,11 @@ module ChatMessage
end
def branch_link
- "[#{ref}](#{branch_url})"
+ link(ref, branch_url)
end
def project_link
- "[#{project_name}](#{project_url})"
+ link(project_name, project_url)
end
def commit_url
@@ -76,7 +96,7 @@ module ChatMessage
end
def commit_link
- "[#{Commit.truncate_sha(sha)}](#{commit_url})"
+ link(Commit.truncate_sha(sha), commit_url)
end
end
end
diff --git a/app/models/project_services/chat_message/issue_message.rb b/app/models/project_services/chat_message/issue_message.rb
index 14fd64e5332..791e5b0cec7 100644
--- a/app/models/project_services/chat_message/issue_message.rb
+++ b/app/models/project_services/chat_message/issue_message.rb
@@ -51,15 +51,16 @@ module ChatMessage
title: issue_title,
title_link: issue_url,
text: format(description),
- color: "#C95823" }]
+ color: "#C95823"
+ }]
end
def project_link
- "[#{project_name}](#{project_url})"
+ link(project_name, project_url)
end
def issue_link
- "[#{issue_title}](#{issue_url})"
+ link(issue_title, issue_url)
end
def issue_title
diff --git a/app/models/project_services/chat_message/merge_message.rb b/app/models/project_services/chat_message/merge_message.rb
index ab5e8b24167..5e5efca7bec 100644
--- a/app/models/project_services/chat_message/merge_message.rb
+++ b/app/models/project_services/chat_message/merge_message.rb
@@ -42,7 +42,7 @@ module ChatMessage
end
def project_link
- "[#{project_name}](#{project_url})"
+ link(project_name, project_url)
end
def merge_request_message
@@ -50,7 +50,7 @@ module ChatMessage
end
def merge_request_link
- "[merge request !#{merge_request_id}](#{merge_request_url})"
+ link("merge request !#{merge_request_id}", merge_request_url)
end
def merge_request_url
diff --git a/app/models/project_services/chat_message/note_message.rb b/app/models/project_services/chat_message/note_message.rb
index ca1d7207034..552113bac29 100644
--- a/app/models/project_services/chat_message/note_message.rb
+++ b/app/models/project_services/chat_message/note_message.rb
@@ -3,10 +3,9 @@ module ChatMessage
attr_reader :message
attr_reader :user_name
attr_reader :project_name
- attr_reader :project_link
+ attr_reader :project_url
attr_reader :note
attr_reader :note_url
- attr_reader :title
def initialize(params)
params = HashWithIndifferentAccess.new(params)
@@ -69,15 +68,15 @@ module ChatMessage
end
def description_message
- [{ text: format(@note), color: attachment_color }]
+ [{ text: format(note), color: attachment_color }]
end
def project_link
- "[#{@project_name}](#{@project_url})"
+ link(project_name, project_url)
end
def commented_on_message(target, title)
- @message = "#{@user_name} [commented on #{target}](#{@note_url}) in #{project_link}: *#{title}*"
+ @message = "#{user_name} #{link('commented on ' + target, note_url)} in #{project_link}: *#{title}*"
end
end
end
diff --git a/app/models/project_services/drone_ci_service.rb b/app/models/project_services/drone_ci_service.rb
index 942ec9371e5..2717c240f05 100644
--- a/app/models/project_services/drone_ci_service.rb
+++ b/app/models/project_services/drone_ci_service.rb
@@ -39,7 +39,7 @@ class DroneCiService < CiService
def commit_status_path(sha, ref)
url = [drone_url,
"gitlab/#{project.full_path}/commits/#{sha}",
- "?branch=#{URI::encode(ref.to_s)}&access_token=#{token}"]
+ "?branch=#{URI.encode(ref.to_s)}&access_token=#{token}"]
URI.join(*url).to_s
end
@@ -52,7 +52,7 @@ class DroneCiService < CiService
response = HTTParty.get(commit_status_path(sha, ref), verify: enable_ssl_verification)
status =
- if response.code == 200 and response['status']
+ if response.code == 200 && response['status']
case response['status']
when 'killed'
:canceled
@@ -74,7 +74,7 @@ class DroneCiService < CiService
def build_page(sha, ref)
url = [drone_url,
"gitlab/#{project.full_path}/redirect/commits/#{sha}",
- "?branch=#{URI::encode(ref.to_s)}"]
+ "?branch=#{URI.encode(ref.to_s)}"]
URI.join(*url).to_s
end
@@ -114,7 +114,7 @@ class DroneCiService < CiService
end
def merge_request_valid?(data)
- ['opened', 'reopened'].include?(data[:object_attributes][:state]) &&
+ %w(opened reopened).include?(data[:object_attributes][:state]) &&
data[:object_attributes][:merge_status] == 'unchecked'
end
end
diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb
index 72da219df28..c4142c38b2f 100644
--- a/app/models/project_services/hipchat_service.rb
+++ b/app/models/project_services/hipchat_service.rb
@@ -6,7 +6,7 @@ class HipchatService < Service
a b i strong em br img pre code
table th tr td caption colgroup col thead tbody tfoot
ul ol li dl dt dd
- ]
+ ].freeze
prop_accessor :token, :room, :server, :color, :api_version
boolean_accessor :notify_only_broken_builds, :notify
@@ -36,7 +36,7 @@ class HipchatService < Service
{ type: 'text', name: 'token', placeholder: 'Room token' },
{ type: 'text', name: 'room', placeholder: 'Room name or ID' },
{ type: 'checkbox', name: 'notify' },
- { type: 'select', name: 'color', choices: ['yellow', 'red', 'green', 'purple', 'gray', 'random'] },
+ { type: 'select', name: 'color', choices: %w(yellow red green purple gray random) },
{ type: 'text', name: 'api_version',
placeholder: 'Leave blank for default (v2)' },
{ type: 'text', name: 'server',
diff --git a/app/models/project_services/irker_service.rb b/app/models/project_services/irker_service.rb
index 5d93064f9b3..c62bb4fa120 100644
--- a/app/models/project_services/irker_service.rb
+++ b/app/models/project_services/irker_service.rb
@@ -33,7 +33,8 @@ class IrkerService < Service
end
def settings
- { server_host: server_host.present? ? server_host : 'localhost',
+ {
+ server_host: server_host.present? ? server_host : 'localhost',
server_port: server_port.present? ? server_port : 6659
}
end
@@ -96,7 +97,7 @@ class IrkerService < Service
rescue URI::InvalidURIError
end
- unless uri.present? and default_irc_uri.nil?
+ unless uri.present? && default_irc_uri.nil?
begin
new_recipient = URI.join(default_irc_uri, '/', recipient).to_s
uri = consider_uri(URI.parse(new_recipient))
diff --git a/app/models/project_services/kubernetes_service.rb b/app/models/project_services/kubernetes_service.rb
index f2f019c43bb..9819e723fe8 100644
--- a/app/models/project_services/kubernetes_service.rb
+++ b/app/models/project_services/kubernetes_service.rb
@@ -3,7 +3,7 @@ class KubernetesService < DeploymentService
include Gitlab::Kubernetes
include ReactiveCaching
- self.reactive_cache_key = ->(service) { [ service.class.model_name.singular, service.project_id ] }
+ self.reactive_cache_key = ->(service) { [service.class.model_name.singular, service.project_id] }
# Namespace defaults to the project path, but can be overridden in case that
# is an invalid or inappropriate name
@@ -62,23 +62,19 @@ class KubernetesService < DeploymentService
{ type: 'text',
name: 'namespace',
title: 'Kubernetes namespace',
- placeholder: 'Kubernetes namespace',
- },
+ placeholder: 'Kubernetes namespace' },
{ type: 'text',
name: 'api_url',
title: 'API URL',
- placeholder: 'Kubernetes API URL, like https://kube.example.com/',
- },
+ placeholder: 'Kubernetes API URL, like https://kube.example.com/' },
{ type: 'text',
name: 'token',
title: 'Service token',
- placeholder: 'Service token',
- },
+ placeholder: 'Service token' },
{ type: 'textarea',
name: 'ca_pem',
title: 'Custom CA bundle',
- placeholder: 'Certificate Authority bundle (PEM format)',
- },
+ placeholder: 'Certificate Authority bundle (PEM format)' },
]
end
@@ -167,7 +163,7 @@ class KubernetesService < DeploymentService
url = URI.parse(api_url)
prefix = url.path.sub(%r{/+\z}, '')
- url.path = [ prefix, *parts ].join("/")
+ url.path = [prefix, *parts].join("/")
url.to_s
end
diff --git a/app/models/project_services/mattermost_service.rb b/app/models/project_services/mattermost_service.rb
index 4ebc5318da1..c13538e9fea 100644
--- a/app/models/project_services/mattermost_service.rb
+++ b/app/models/project_services/mattermost_service.rb
@@ -15,10 +15,10 @@ class MattermostService < ChatNotificationService
'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>
+ <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 below.</li>
+ <li>Select events below to enable notifications. The <strong>Channel handle</strong> and <strong>Username</strong> fields are optional.</li>
</ol>'
end
@@ -28,14 +28,14 @@ class MattermostService < ChatNotificationService
def default_fields
[
- { type: 'text', name: 'webhook', placeholder: 'http://mattermost_host/hooks/...' },
- { type: 'text', name: 'username', placeholder: 'username' },
+ { type: 'text', name: 'webhook', placeholder: 'e.g. http://mattermost_host/hooks/…' },
+ { type: 'text', name: 'username', placeholder: 'e.g. GitLab' },
{ type: 'checkbox', name: 'notify_only_broken_builds' },
{ type: 'checkbox', name: 'notify_only_broken_pipelines' },
]
end
def default_channel_placeholder
- "town-square"
+ "Channel handle (e.g. town-square)"
end
end
diff --git a/app/models/project_services/mock_ci_service.rb b/app/models/project_services/mock_ci_service.rb
new file mode 100644
index 00000000000..a8d581a1f67
--- /dev/null
+++ b/app/models/project_services/mock_ci_service.rb
@@ -0,0 +1,82 @@
+# For an example companion mocking service, see https://gitlab.com/gitlab-org/gitlab-mock-ci-service
+class MockCiService < CiService
+ ALLOWED_STATES = %w[failed canceled running pending success success_with_warnings skipped not_found].freeze
+
+ prop_accessor :mock_service_url
+ validates :mock_service_url, presence: true, url: true, if: :activated?
+
+ def title
+ 'MockCI'
+ end
+
+ def description
+ 'Mock an external CI'
+ end
+
+ def self.to_param
+ 'mock_ci'
+ end
+
+ def fields
+ [
+ { type: 'text',
+ name: 'mock_service_url',
+ placeholder: 'http://localhost:4004' },
+ ]
+ end
+
+ # Return complete url to build page
+ #
+ # Ex.
+ # http://jenkins.example.com:8888/job/test1/scm/bySHA1/12d65c
+ #
+ def build_page(sha, ref)
+ url = [mock_service_url,
+ "#{project.namespace.path}/#{project.path}/status/#{sha}"]
+
+ URI.join(*url).to_s
+ end
+
+ # Return string with build status or :error symbol
+ #
+ # Allowed states: 'success', 'failed', 'running', 'pending', 'skipped'
+ #
+ #
+ # Ex.
+ # @service.commit_status('13be4ac', 'master')
+ # # => 'success'
+ #
+ # @service.commit_status('2abe4ac', 'dev')
+ # # => 'running'
+ #
+ #
+ def commit_status(sha, ref)
+ response = HTTParty.get(commit_status_path(sha), verify: false)
+ read_commit_status(response)
+ rescue Errno::ECONNREFUSED
+ :error
+ end
+
+ def commit_status_path(sha)
+ url = [mock_service_url,
+ "#{project.namespace.path}/#{project.path}/status/#{sha}.json"]
+
+ URI.join(*url).to_s
+ end
+
+ def read_commit_status(response)
+ return :error unless response.code == 200 || response.code == 404
+
+ status = if response.code == 404
+ 'pending'
+ else
+ response['status']
+ end
+
+ if status.present? && ALLOWED_STATES.include?(status)
+ status
+ else
+ :error
+ end
+ end
+end
diff --git a/app/models/project_services/pivotaltracker_service.rb b/app/models/project_services/pivotaltracker_service.rb
index 9cc642591f4..d86f4f6f448 100644
--- a/app/models/project_services/pivotaltracker_service.rb
+++ b/app/models/project_services/pivotaltracker_service.rb
@@ -1,7 +1,7 @@
class PivotaltrackerService < Service
include HTTParty
- API_ENDPOINT = 'https://www.pivotaltracker.com/services/v5/source_commits'
+ API_ENDPOINT = 'https://www.pivotaltracker.com/services/v5/source_commits'.freeze
prop_accessor :token, :restrict_to_branch
validates :token, presence: true, if: :activated?
diff --git a/app/models/project_services/pushover_service.rb b/app/models/project_services/pushover_service.rb
index a963d27a376..3e618a8dbf1 100644
--- a/app/models/project_services/pushover_service.rb
+++ b/app/models/project_services/pushover_service.rb
@@ -29,25 +29,24 @@ class PushoverService < Service
['Normal Priority', 0],
['High Priority', 1]
],
- default_choice: 0
- },
+ default_choice: 0 },
{ type: 'select', name: 'sound', choices:
[
['Device default sound', nil],
['Pushover (default)', 'pushover'],
- ['Bike', 'bike'],
- ['Bugle', 'bugle'],
+ %w(Bike bike),
+ %w(Bugle bugle),
['Cash Register', 'cashregister'],
- ['Classical', 'classical'],
- ['Cosmic', 'cosmic'],
- ['Falling', 'falling'],
- ['Gamelan', 'gamelan'],
- ['Incoming', 'incoming'],
- ['Intermission', 'intermission'],
- ['Magic', 'magic'],
- ['Mechanical', 'mechanical'],
+ %w(Classical classical),
+ %w(Cosmic cosmic),
+ %w(Falling falling),
+ %w(Gamelan gamelan),
+ %w(Incoming incoming),
+ %w(Intermission intermission),
+ %w(Magic magic),
+ %w(Mechanical mechanical),
['Piano Bar', 'pianobar'],
- ['Siren', 'siren'],
+ %w(Siren siren),
['Space Alarm', 'spacealarm'],
['Tug Boat', 'tugboat'],
['Alien Alarm (long)', 'alien'],
@@ -56,8 +55,7 @@ class PushoverService < Service
['Pushover Echo (long)', 'echo'],
['Up Down (long)', 'updown'],
['None (silent)', 'none']
- ]
- },
+ ] },
]
end
@@ -72,13 +70,14 @@ class PushoverService < Service
before = data[:before]
after = data[:after]
- if Gitlab::Git.blank_ref?(before)
- message = "#{data[:user_name]} pushed new branch \"#{ref}\"."
- elsif Gitlab::Git.blank_ref?(after)
- message = "#{data[:user_name]} deleted branch \"#{ref}\"."
- else
- message = "#{data[:user_name]} push to branch \"#{ref}\"."
- end
+ message =
+ if Gitlab::Git.blank_ref?(before)
+ "#{data[:user_name]} pushed new branch \"#{ref}\"."
+ elsif Gitlab::Git.blank_ref?(after)
+ "#{data[:user_name]} deleted branch \"#{ref}\"."
+ else
+ "#{data[:user_name]} push to branch \"#{ref}\"."
+ end
if data[:total_commits_count] > 0
message << "\nTotal commits count: #{data[:total_commits_count]}"
@@ -97,7 +96,7 @@ class PushoverService < Service
# Sound parameter MUST NOT be sent to API if not selected
if sound
- pushover_data.merge!(sound: sound)
+ pushover_data[:sound] = sound
end
PushoverService.post('/messages.json', body: pushover_data)
diff --git a/app/models/project_services/slack_service.rb b/app/models/project_services/slack_service.rb
index f77d2d7c60b..da7496573ef 100644
--- a/app/models/project_services/slack_service.rb
+++ b/app/models/project_services/slack_service.rb
@@ -13,11 +13,11 @@ class SlackService < ChatNotificationService
def help
'This service sends notifications about projects events to Slack channels.<br />
- To setup this service:
+ To set up 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>
+ <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 <strong>Channel name</strong> and <strong>Username</strong> fields are optional.</li>
</ol>'
end
@@ -27,14 +27,14 @@ class SlackService < ChatNotificationService
def default_fields
[
- { type: 'text', name: 'webhook', placeholder: 'https://hooks.slack.com/services/...' },
- { type: 'text', name: 'username', placeholder: 'username' },
+ { type: 'text', name: 'webhook', placeholder: 'e.g. https://hooks.slack.com/services/…' },
+ { type: 'text', name: 'username', placeholder: 'e.g. GitLab' },
{ type: 'checkbox', name: 'notify_only_broken_builds' },
{ type: 'checkbox', name: 'notify_only_broken_pipelines' },
]
end
def default_channel_placeholder
- "#general"
+ "Channel name (e.g. general)"
end
end
diff --git a/app/models/project_snippet.rb b/app/models/project_snippet.rb
index 9bb456eee24..25b5d777641 100644
--- a/app/models/project_snippet.rb
+++ b/app/models/project_snippet.rb
@@ -9,8 +9,4 @@ 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
index 06abd406523..aeaf63abab9 100644
--- a/app/models/project_statistics.rb
+++ b/app/models/project_statistics.rb
@@ -4,7 +4,7 @@ class ProjectStatistics < ActiveRecord::Base
before_save :update_storage_size
- STORAGE_COLUMNS = [:repository_size, :lfs_objects_size, :build_artifacts_size]
+ STORAGE_COLUMNS = [:repository_size, :lfs_objects_size, :build_artifacts_size].freeze
STATISTICS_COLUMNS = [:commit_count] + STORAGE_COLUMNS
def total_repository_size
diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb
index 9db96347322..9891f5edf41 100644
--- a/app/models/project_wiki.rb
+++ b/app/models/project_wiki.rb
@@ -5,7 +5,7 @@ class ProjectWiki
'Markdown' => :markdown,
'RDoc' => :rdoc,
'AsciiDoc' => :asciidoc
- } unless defined?(MARKUPS)
+ }.freeze unless defined?(MARKUPS)
class CouldNotCreateWikiError < StandardError; end
@@ -19,6 +19,9 @@ class ProjectWiki
@user = user
end
+ delegate :empty?, to: :pages
+ delegate :repository_storage_path, to: :project
+
def path
@project.path + '.wiki'
end
@@ -60,10 +63,6 @@ class ProjectWiki
!!repository.exists?
end
- def empty?
- pages.empty?
- end
-
# Returns an Array of Gitlab WikiPage instances or an
# empty Array if this Wiki has no pages.
def pages
diff --git a/app/models/protected_branch.rb b/app/models/protected_branch.rb
index 6240912a6e1..39e979ef15b 100644
--- a/app/models/protected_branch.rb
+++ b/app/models/protected_branch.rb
@@ -8,8 +8,8 @@ class ProtectedBranch < ActiveRecord::Base
has_many :merge_access_levels, dependent: :destroy
has_many :push_access_levels, dependent: :destroy
- validates_length_of :merge_access_levels, is: 1, message: "are restricted to a single instance per protected branch."
- validates_length_of :push_access_levels, is: 1, message: "are restricted to a single instance per protected branch."
+ validates :merge_access_levels, length: { is: 1, message: "are restricted to a single instance per protected branch." }
+ validates :push_access_levels, length: { is: 1, message: "are restricted to a single instance per protected branch." }
accepts_nested_attributes_for :push_access_levels
accepts_nested_attributes_for :merge_access_levels
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 56c582cd9be..0dbf246c3a4 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -18,7 +18,7 @@ class Repository
CACHED_METHODS = %i(size commit_count readme version contribution_guide
changelog license_blob license_key gitignore koding_yml
gitlab_ci_yml branch_names tag_names branch_count
- tag_count avatar exists? empty? root_ref)
+ tag_count avatar exists? empty? root_ref).freeze
# Certain method caches should be refreshed when certain types of files are
# changed. This Hash maps file types (as returned by Gitlab::FileDetector) to
@@ -33,7 +33,7 @@ class Repository
koding: :koding_yml,
gitlab_ci: :gitlab_ci_yml,
avatar: :avatar
- }
+ }.freeze
# Wraps around the given method and caches its output in Redis and an instance
# variable.
@@ -109,9 +109,7 @@ class Repository
offset: offset,
after: after,
before: before,
- # --follow doesn't play well with --skip. See:
- # https://gitlab.com/gitlab-org/gitlab-ce/issues/3574#note_3040520
- follow: false,
+ follow: path.present?,
skip_merges: skip_merges
}
@@ -487,9 +485,7 @@ class Repository
end
cache_method :exists?
- def empty?
- raw_repository.empty?
- end
+ delegate :empty?, to: :raw_repository
cache_method :empty?
# The size of this repository in megabytes.
@@ -508,9 +504,7 @@ class Repository
end
cache_method :branch_names, fallback: []
- def tag_names
- raw_repository.tag_names
- end
+ delegate :tag_names, to: :raw_repository
cache_method :tag_names, fallback: []
def branch_count
@@ -750,136 +744,63 @@ class Repository
@tags ||= raw_repository.tags
end
- # 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)
-
- if start_branch_name
- start_project.repository.
- check_tree_entry_for_dir(start_branch_name, path)
- end
+ def create_dir(user, path, **options)
+ options[:user] = user
+ options[:actions] = [{ action: :create_dir, file_path: path }]
- 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)
+ multi_action(**options)
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"
+ def create_file(user, path, content, **options)
+ options[:user] = user
+ options[:actions] = [{ action: :create, file_path: path, content: content }]
- if tree_entry_at(branch_name, path)
- raise Gitlab::Git::Repository::InvalidBlobName.new(error_message)
- end
+ multi_action(**options)
+ end
- if start_branch_name &&
- start_project.repository.tree_entry_at(start_branch_name, path)
- raise Gitlab::Git::Repository::InvalidBlobName.new(error_message)
- end
- end
+ def update_file(user, path, content, **options)
+ previous_path = options.delete(:previous_path)
+ action = previous_path && previous_path != path ? :move : :update
- 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
+ options[:user] = user
+ options[:actions] = [{ action: action, file_path: path, previous_path: previous_path, content: content }]
- # 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 }])
+ multi_action(**options)
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 }])
+ def delete_file(user, path, **options)
+ options[:user] = user
+ options[:actions] = [{ action: :delete, file_path: path }]
+
+ multi_action(**options)
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 = if start_commit
- index.read_tree(start_commit.raw_commit.tree)
- [start_commit.sha]
- else
- []
- end
+ index = Gitlab::Git::Index.new(raw_repository)
- actions.each do |act|
- git_action(index, act)
+ if start_commit
+ index.read_tree(start_commit.raw_commit.tree)
+ parents = [start_commit.sha]
+ else
+ parents = []
+ end
+
+ actions.each do |options|
+ index.public_send(options.delete(:action), options)
end
options = {
- tree: index.write_tree(rugged),
+ tree: index.write_tree,
message: message,
parents: parents
}
@@ -892,7 +813,7 @@ class Repository
def get_committer_and_author(user, email: nil, name: nil)
committer = user_to_committer(user)
- author = Gitlab::Git::committer_hash(email: email, name: name) || committer
+ author = Gitlab::Git.committer_hash(email: email, name: name) || committer
{
author: author,
@@ -1170,30 +1091,6 @@ class Repository
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)
@@ -1204,58 +1101,6 @@ class Repository
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
-
- 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 self.autocrlf
- end
-
- oid = rugged.write(content, :blob)
-
- index.add(path: path, oid: oid, mode: mode)
- when :delete
- index.remove(path)
- end
- end
-
- def normalize_path(path)
- pathname = Gitlab::Git::PathHelper.normalize_path(path)
-
- if pathname.each_filename.include?('..')
- raise Gitlab::Git::Repository::InvalidBlobName.new('Invalid path')
- end
-
- pathname.to_s
- end
-
def refs_directory_exists?
return false unless path_with_namespace
diff --git a/app/models/service.rb b/app/models/service.rb
index facaaf9b331..3ef4cbead10 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -210,7 +210,7 @@ class Service < ActiveRecord::Base
end
def self.available_services_names
- %w[
+ service_names = %w[
asana
assembla
bamboo
@@ -238,6 +238,9 @@ class Service < ActiveRecord::Base
slack
teamcity
]
+ service_names << 'mock_ci' if Rails.env.development?
+
+ service_names.sort_by(&:downcase)
end
def self.build_from_template(project_id, template)
diff --git a/app/models/todo.rb b/app/models/todo.rb
index 3dda7948d0b..47789a21133 100644
--- a/app/models/todo.rb
+++ b/app/models/todo.rb
@@ -17,7 +17,7 @@ class Todo < ActiveRecord::Base
APPROVAL_REQUIRED => :approval_required,
UNMERGEABLE => :unmergeable,
DIRECTLY_ADDRESSED => :directly_addressed
- }
+ }.freeze
belongs_to :author, class_name: "User"
belongs_to :note
diff --git a/app/models/user.rb b/app/models/user.rb
index ad997ce2b13..6fb5ac4a4ef 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -21,7 +21,6 @@ class User < ActiveRecord::Base
default_value_for :can_create_team, false
default_value_for :hide_no_ssh_key, false
default_value_for :hide_no_password, false
- default_value_for :theme_id, gitlab_config.default_theme
attr_encrypted :otp_secret,
key: Gitlab::Application.secrets.otp_key_base,
@@ -82,7 +81,6 @@ class User < ActiveRecord::Base
has_many :authorized_projects, through: :project_authorizations, source: :project
has_many :snippets, dependent: :destroy, foreign_key: :author_id
- has_many :issues, dependent: :destroy, foreign_key: :author_id
has_many :notes, dependent: :destroy, foreign_key: :author_id
has_many :merge_requests, dependent: :destroy, foreign_key: :author_id
has_many :events, dependent: :destroy, foreign_key: :author_id
@@ -100,12 +98,17 @@ class User < ActiveRecord::Base
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"
+ # Issues that a user owns are expected to be moved to the "ghost" user before
+ # the user is destroyed. If the user owns any issues during deletion, this
+ # should be treated as an exceptional condition.
+ has_many :issues, dependent: :restrict_with_exception, foreign_key: :author_id
+
#
# Validations
#
# Note: devise :validatable above adds validations for :email and :password
validates :name, presence: true
- validates_confirmation_of :email
+ validates :email, confirmation: true
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
@@ -121,6 +124,7 @@ class User < ActiveRecord::Base
validate :unique_email, if: ->(user) { user.email_changed? }
validate :owns_notification_email, if: ->(user) { user.notification_email_changed? }
validate :owns_public_email, if: ->(user) { user.public_email_changed? }
+ validate :ghost_users_must_be_blocked
validates :avatar, file_size: { maximum: 200.kilobytes.to_i }
before_validation :generate_password, on: :create
@@ -335,9 +339,15 @@ class User < ActiveRecord::Base
def reference_pattern
%r{
#{Regexp.escape(reference_prefix)}
- (?<user>#{Gitlab::Regex::NAMESPACE_REF_REGEX_STR})
+ (?<user>#{Gitlab::Regex::FULL_NAMESPACE_REGEX_STR})
}x
end
+
+ # Return (create if necessary) the ghost user. The ghost user
+ # owns records previously belonging to deleted users.
+ def ghost
+ User.find_by_ghost(true) || create_ghost_user
+ end
end
#
@@ -436,6 +446,12 @@ class User < ActiveRecord::Base
errors.add(:public_email, "is not an email you own") unless all_emails.include?(public_email)
end
+ def ghost_users_must_be_blocked
+ if ghost? && !blocked?
+ errors.add(:ghost, 'cannot be enabled for a user who is not blocked.')
+ end
+ end
+
def update_emails_with_primary_email
primary_email_record = emails.find_by(email: email)
if primary_email_record
@@ -458,7 +474,7 @@ class User < ActiveRecord::Base
Group.member_descendants(id)
end
- def nested_projects
+ def nested_groups_projects
Project.joins(:namespace).where('namespaces.parent_id IS NOT NULL').
member_descendants(id)
end
@@ -581,8 +597,8 @@ class User < ActiveRecord::Base
if project.repository.branch_exists?(event.branch_name)
merge_requests = MergeRequest.where("created_at >= ?", event.created_at).
- where(source_project_id: project.id,
- source_branch: event.branch_name)
+ where(source_project_id: project.id,
+ source_branch: event.branch_name)
merge_requests.empty?
end
end
@@ -1000,4 +1016,40 @@ class User < ActiveRecord::Base
super
end
end
+
+ def self.create_ghost_user
+ # Since we only want a single ghost user in an instance, we use an
+ # exclusive lease to ensure than this block is never run concurrently.
+ lease_key = "ghost_user_creation"
+ lease = Gitlab::ExclusiveLease.new(lease_key, timeout: 1.minute.to_i)
+
+ until uuid = lease.try_obtain
+ # Keep trying until we obtain the lease. To prevent hammering Redis too
+ # much we'll wait for a bit between retries.
+ sleep(1)
+ end
+
+ # Recheck if a ghost user is already present. One might have been
+ # added between the time we last checked (first line of this method)
+ # and the time we acquired the lock.
+ ghost_user = User.find_by_ghost(true)
+ return ghost_user if ghost_user.present?
+
+ uniquify = Uniquify.new
+
+ username = uniquify.string("ghost") { |s| User.find_by_username(s) }
+
+ email = uniquify.string(-> (n) { "ghost#{n}@example.com" }) do |s|
+ User.find_by_email(s)
+ end
+
+ bio = 'This is a "Ghost User", created to hold all issues authored by users that have since been deleted. This user cannot be removed.'
+
+ User.create(
+ username: username, password: Devise.friendly_token, bio: bio,
+ email: email, name: "Ghost User", state: :blocked, ghost: true
+ )
+ ensure
+ Gitlab::ExclusiveLease.cancel(lease_key, uuid)
+ end
end
diff --git a/app/policies/base_policy.rb b/app/policies/base_policy.rb
index b9f1c29c32e..e07b144355a 100644
--- a/app/policies/base_policy.rb
+++ b/app/policies/base_policy.rb
@@ -6,9 +6,7 @@ class BasePolicy
@cannot_set = cannot_set
end
- def size
- to_set.size
- end
+ delegate :size, to: :to_set
def self.empty
new(Set.new, Set.new)
diff --git a/app/policies/user_policy.rb b/app/policies/user_policy.rb
index 03a2499e263..229846e368c 100644
--- a/app/policies/user_policy.rb
+++ b/app/policies/user_policy.rb
@@ -3,6 +3,14 @@ class UserPolicy < BasePolicy
def rules
can! :read_user if @user || !restricted_public_level?
+
+ if @user
+ if @user.admin? || @subject == @user
+ can! :destroy_user
+ end
+
+ cannot! :destroy_user if @subject.ghost?
+ end
end
def restricted_public_level?
diff --git a/app/serializers/analytics_stage_entity.rb b/app/serializers/analytics_stage_entity.rb
index a559d0850c4..69bf693de8d 100644
--- a/app/serializers/analytics_stage_entity.rb
+++ b/app/serializers/analytics_stage_entity.rb
@@ -2,6 +2,7 @@ class AnalyticsStageEntity < Grape::Entity
include EntityDateHelper
expose :title
+ expose :legend
expose :description
expose :median, as: :value do |stage|
diff --git a/app/services/access_token_validation_service.rb b/app/services/access_token_validation_service.rb
index ddaaed90e5b..b2a543daa00 100644
--- a/app/services/access_token_validation_service.rb
+++ b/app/services/access_token_validation_service.rb
@@ -1,10 +1,16 @@
-AccessTokenValidationService = Struct.new(:token) do
+class AccessTokenValidationService
# Results:
VALID = :valid
EXPIRED = :expired
REVOKED = :revoked
INSUFFICIENT_SCOPE = :insufficient_scope
+ attr_reader :token
+
+ def initialize(token)
+ @token = token
+ end
+
def validate(scopes: [])
if token.expired?
return EXPIRED
diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb
index 5cb7a86a5ee..db82b8f6c30 100644
--- a/app/services/auth/container_registry_authentication_service.rb
+++ b/app/services/auth/container_registry_authentication_service.rb
@@ -2,7 +2,7 @@ module Auth
class ContainerRegistryAuthenticationService < BaseService
include Gitlab::CurrentSettings
- AUDIENCE = 'container_registry'
+ AUDIENCE = 'container_registry'.freeze
def execute(authentication_abilities:)
@authentication_abilities = authentication_abilities
diff --git a/app/services/base_service.rb b/app/services/base_service.rb
index 1a2bad77a02..745c2c4b681 100644
--- a/app/services/base_service.rb
+++ b/app/services/base_service.rb
@@ -1,4 +1,5 @@
class BaseService
+ include Gitlab::Allowable
include Gitlab::CurrentSettings
attr_accessor :project, :current_user, :params
@@ -7,10 +8,6 @@ class BaseService
@project, @current_user, @params = project, user, params.dup
end
- def can?(object, action, subject)
- Ability.allowed?(object, action, subject)
- end
-
def notification_service
NotificationService.new
end
@@ -31,9 +28,7 @@ class BaseService
SystemHooksService.new
end
- def repository
- project.repository
- end
+ delegate :repository, to: :project
# Add an error to the specified model for restricted visibility levels
def deny_visibility_level(model, denied_visibility_level = nil)
diff --git a/app/services/ci/create_pipeline_builds_service.rb b/app/services/ci/create_pipeline_builds_service.rb
index b7da3f8e7eb..70fb2c5e38f 100644
--- a/app/services/ci/create_pipeline_builds_service.rb
+++ b/app/services/ci/create_pipeline_builds_service.rb
@@ -10,9 +10,7 @@ module Ci
end
end
- def project
- pipeline.project
- end
+ delegate :project, to: :pipeline
private
diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb
index e3bc9847200..38a85e9fc42 100644
--- a/app/services/ci/create_pipeline_service.rb
+++ b/app/services/ci/create_pipeline_service.rb
@@ -59,7 +59,8 @@ module Ci
private
def skip_ci?
- pipeline.git_commit_message =~ /\[(ci skip|skip ci)\]/i if pipeline.git_commit_message
+ return false unless pipeline.git_commit_message
+ pipeline.git_commit_message =~ /\[(ci[ _-]skip|skip[ _-]ci)\]/i
end
def commit
diff --git a/app/services/ci/register_build_service.rb b/app/services/ci/register_build_service.rb
index 6f03bf2be13..5b52a0425de 100644
--- a/app/services/ci/register_build_service.rb
+++ b/app/services/ci/register_build_service.rb
@@ -20,21 +20,33 @@ module Ci
builds_for_specific_runner
end
- build = builds.find do |build|
- runner.can_pick?(build)
- end
+ valid = true
- 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 = runner.id
- build.run!
- end
+ builds.find do |build|
+ next unless runner.can_pick?(build)
+
+ begin
+ # 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 = runner.id
+ build.run!
- Result.new(build, true)
+ return Result.new(build, true)
+ rescue StateMachines::InvalidTransition, ActiveRecord::StaleObjectError
+ # We are looping to find another build that is not conflicting
+ # It also indicates that this build can be picked and passed to runner.
+ # If we don't do it, basically a bunch of runners would be competing for a build
+ # and thus we will generate a lot of 409. This will increase
+ # the number of generated requests, also will reduce significantly
+ # how many builds can be picked by runner in a unit of time.
+ # In case we hit the concurrency-access lock,
+ # we still have to return 409 in the end,
+ # to make sure that this is properly handled by runner.
+ valid = false
+ end
+ end
- rescue StateMachines::InvalidTransition, ActiveRecord::StaleObjectError
- Result.new(build, false)
+ Result.new(nil, valid)
end
private
diff --git a/app/services/ci/retry_build_service.rb b/app/services/ci/retry_build_service.rb
new file mode 100644
index 00000000000..38ef323f6e5
--- /dev/null
+++ b/app/services/ci/retry_build_service.rb
@@ -0,0 +1,43 @@
+module Ci
+ class RetryBuildService < ::BaseService
+ CLONE_ATTRIBUTES = %i[pipeline project ref tag options commands name
+ allow_failure stage stage_idx trigger_request
+ yaml_variables when environment coverage_regex]
+ .freeze
+
+ REJECT_ATTRIBUTES = %i[id status user token coverage trace runner
+ artifacts_expire_at artifacts_file
+ artifacts_metadata artifacts_size
+ created_at updated_at started_at finished_at
+ queued_at erased_by erased_at].freeze
+
+ IGNORE_ATTRIBUTES = %i[type lock_version gl_project_id target_url
+ deploy job_id description].freeze
+
+ def execute(build)
+ reprocess(build).tap do |new_build|
+ build.pipeline.mark_as_processable_after_stage(build.stage_idx)
+
+ new_build.enqueue!
+
+ MergeRequests::AddTodoWhenBuildFailsService
+ .new(project, current_user)
+ .close(new_build)
+ end
+ end
+
+ def reprocess(build)
+ unless can?(current_user, :update_build, build)
+ raise Gitlab::Access::AccessDeniedError
+ end
+
+ attributes = CLONE_ATTRIBUTES.map do |attribute|
+ [attribute, build.send(attribute)]
+ end
+
+ attributes.push([:user, current_user])
+
+ project.builds.create(Hash[attributes])
+ end
+ end
+end
diff --git a/app/services/ci/retry_pipeline_service.rb b/app/services/ci/retry_pipeline_service.rb
new file mode 100644
index 00000000000..574561adc4c
--- /dev/null
+++ b/app/services/ci/retry_pipeline_service.rb
@@ -0,0 +1,28 @@
+module Ci
+ class RetryPipelineService < ::BaseService
+ include Gitlab::OptimisticLocking
+
+ def execute(pipeline)
+ unless can?(current_user, :update_pipeline, pipeline)
+ raise Gitlab::Access::AccessDeniedError
+ end
+
+ pipeline.builds.failed_or_canceled.find_each do |build|
+ next unless build.retryable?
+
+ Ci::RetryBuildService.new(project, current_user)
+ .reprocess(build)
+ end
+
+ pipeline.builds.skipped.find_each do |skipped|
+ retry_optimistic_lock(skipped) { |build| build.process }
+ end
+
+ MergeRequests::AddTodoWhenBuildFailsService
+ .new(project, current_user)
+ .close_all(pipeline)
+
+ pipeline.process!
+ end
+ end
+end
diff --git a/app/services/ci/update_runner_service.rb b/app/services/ci/update_runner_service.rb
new file mode 100644
index 00000000000..450ee7da1c9
--- /dev/null
+++ b/app/services/ci/update_runner_service.rb
@@ -0,0 +1,15 @@
+module Ci
+ class UpdateRunnerService
+ attr_reader :runner
+
+ def initialize(runner)
+ @runner = runner
+ end
+
+ def update(params)
+ runner.update(params).tap do |updated|
+ runner.tick_runner_queue if updated
+ end
+ end
+ end
+end
diff --git a/app/services/create_branch_service.rb b/app/services/create_branch_service.rb
index 77459d8779d..b07338d500a 100644
--- a/app/services/create_branch_service.rb
+++ b/app/services/create_branch_service.rb
@@ -1,5 +1,7 @@
class CreateBranchService < BaseService
def execute(branch_name, ref)
+ create_master_branch if project.empty_repo?
+
result = ValidateNewBranchService.new(project, current_user)
.execute(branch_name)
@@ -19,4 +21,16 @@ class CreateBranchService < BaseService
def success(branch)
super().merge(branch: branch)
end
+
+ private
+
+ def create_master_branch
+ project.repository.commit_file(
+ current_user,
+ '/README.md',
+ '',
+ message: 'Add README.md',
+ branch_name: 'master',
+ update: false)
+ end
end
diff --git a/app/services/create_snippet_service.rb b/app/services/create_snippet_service.rb
index 14f5ba064ff..40286dbf3bf 100644
--- a/app/services/create_snippet_service.rb
+++ b/app/services/create_snippet_service.rb
@@ -1,7 +1,8 @@
class CreateSnippetService < BaseService
+ include SpamCheckService
+
def execute
- request = params.delete(:request)
- api = params.delete(:api)
+ filter_spam_check_params
snippet = if project
project.snippets.build(params)
@@ -15,10 +16,11 @@ class CreateSnippetService < BaseService
end
snippet.author = current_user
- snippet.spam = SpamService.new(snippet, request).check(api)
+
+ spam_check(snippet, current_user)
if snippet.save
- UserAgentDetailService.new(snippet, request).create
+ UserAgentDetailService.new(snippet, @request).create
end
snippet
diff --git a/app/services/files/create_dir_service.rb b/app/services/files/create_dir_service.rb
index 858de5f0538..083ffdc634c 100644
--- a/app/services/files/create_dir_service.rb
+++ b/app/services/files/create_dir_service.rb
@@ -1,7 +1,7 @@
module Files
class CreateDirService < Files::BaseService
def commit
- repository.commit_dir(
+ repository.create_dir(
current_user,
@file_path,
message: @commit_message,
diff --git a/app/services/files/create_service.rb b/app/services/files/create_service.rb
index 88dd7bbaedb..65b5537fb68 100644
--- a/app/services/files/create_service.rb
+++ b/app/services/files/create_service.rb
@@ -1,13 +1,12 @@
module Files
class CreateService < Files::BaseService
def commit
- repository.commit_file(
+ repository.create_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,
@@ -17,6 +16,10 @@ module Files
def validate
super
+ if @file_content.nil?
+ raise_error("You must provide content.")
+ end
+
if @file_path =~ Gitlab::Regex.directory_traversal_regex
raise_error(
'Your changes could not be committed, because the file name ' +
diff --git a/app/services/files/destroy_service.rb b/app/services/files/destroy_service.rb
index c3be806a42d..e294659bc98 100644
--- a/app/services/files/destroy_service.rb
+++ b/app/services/files/destroy_service.rb
@@ -1,7 +1,7 @@
module Files
class DestroyService < Files::BaseService
def commit
- repository.remove_file(
+ repository.delete_file(
current_user,
@file_path,
message: @commit_message,
diff --git a/app/services/files/multi_service.rb b/app/services/files/multi_service.rb
index 6ba868df04d..0609c6219e7 100644
--- a/app/services/files/multi_service.rb
+++ b/app/services/files/multi_service.rb
@@ -2,6 +2,8 @@ module Files
class MultiService < Files::BaseService
class FileChangedError < StandardError; end
+ ACTIONS = %w[create update delete move].freeze
+
def commit
repository.multi_action(
user: current_user,
@@ -21,10 +23,19 @@ module Files
super
params[:actions].each_with_index do |action, index|
+ if ACTIONS.include?(action[:action].to_s)
+ action[:action] = action[:action].to_sym
+ else
+ raise_error("Unknown action type `#{action[:action]}`.")
+ end
+
unless action[:file_path].present?
raise_error("You must specify a file_path.")
end
+ action[:file_path].slice!(0) if action[:file_path] && action[:file_path].start_with?('/')
+ action[:previous_path].slice!(0) if action[:previous_path] && action[:previous_path].start_with?('/')
+
regex_check(action[:file_path])
regex_check(action[:previous_path]) if action[:previous_path]
@@ -43,8 +54,6 @@ module Files
validate_delete(action)
when :move
validate_move(action, index)
- else
- raise_error("Unknown action type `#{action[:action]}`.")
end
end
end
@@ -55,7 +64,7 @@ module Files
file_path = action[:file_path]
file_path = action[:previous_path] if action[:action] == :move
- blob = repository.blob_at_branch(params[:branch_name], file_path)
+ blob = repository.blob_at_branch(params[:branch], file_path)
unless blob
raise_error("File to be #{action[:action]}d `#{file_path}` does not exist.")
@@ -89,9 +98,23 @@ module Files
def validate_create(action)
return if project.empty_repo?
- if repository.blob_at_branch(params[:branch_name], action[:file_path])
+ if repository.blob_at_branch(params[:branch], action[:file_path])
raise_error("Your changes could not be committed because a file with the name `#{action[:file_path]}` already exists.")
end
+
+ if action[:content].nil?
+ raise_error("You must provide content.")
+ end
+ end
+
+ def validate_update(action)
+ if action[:content].nil?
+ raise_error("You must provide content.")
+ end
+
+ if file_has_changed?
+ raise FileChangedError.new("You are attempting to update a file `#{action[:file_path]}` that has changed since you started editing it.")
+ end
end
def validate_delete(action)
@@ -102,23 +125,17 @@ module Files
raise_error("You must supply the original file path when moving file `#{action[:file_path]}`.")
end
- blob = repository.blob_at_branch(params[:branch_name], action[:file_path])
+ blob = repository.blob_at_branch(params[:branch], action[:file_path])
if blob
raise_error("Move destination `#{action[:file_path]}` already exists.")
end
if action[:content].nil?
- blob = repository.blob_at_branch(params[:branch_name], action[:previous_path])
+ blob = repository.blob_at_branch(params[:branch], action[:previous_path])
blob.load_all_data!(repository) if blob.truncated?
params[:actions][index][:content] = blob.data
end
end
-
- def validate_update(action)
- if file_has_changed?
- raise FileChangedError.new("You are attempting to update a file `#{action[:file_path]}` that has changed since you started editing it.")
- end
- end
end
end
diff --git a/app/services/files/update_service.rb b/app/services/files/update_service.rb
index a71fe61a4b6..54e1aaf3f67 100644
--- a/app/services/files/update_service.rb
+++ b/app/services/files/update_service.rb
@@ -18,6 +18,10 @@ module Files
def validate
super
+ if @file_content.nil?
+ raise_error("You must provide content.")
+ end
+
if file_has_changed?
raise FileChangedError.new("You are attempting to update a file that has changed since you started editing it.")
end
diff --git a/app/services/groups/destroy_service.rb b/app/services/groups/destroy_service.rb
index 7f2d28086f5..497fdb09cdc 100644
--- a/app/services/groups/destroy_service.rb
+++ b/app/services/groups/destroy_service.rb
@@ -8,7 +8,9 @@ module Groups
end
def execute
- group.projects.each do |project|
+ group.prepare_for_destroy
+
+ group.projects.with_deleted.each do |project|
# Execute the destruction of the models immediately to ensure atomic cleanup.
# Skip repository removal because we remove directory with namespace
# that contain all these repositories
@@ -16,7 +18,8 @@ module Groups
end
group.children.each do |group|
- DestroyService.new(group, current_user).async_execute
+ # This needs to be synchronous since the namespace gets destroyed below
+ DestroyService.new(group, current_user).execute
end
group.really_destroy!
diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb
index 5f3ced49665..9500faf2862 100644
--- a/app/services/issuable_base_service.rb
+++ b/app/services/issuable_base_service.rb
@@ -191,14 +191,12 @@ class IssuableBaseService < BaseService
# To be overridden by subclasses
end
- def after_update(issuable)
+ def before_update(issuable)
# To be overridden by subclasses
end
- def update_issuable(issuable, attributes)
- issuable.with_transaction_returning_status do
- issuable.update(attributes.merge(updated_by: current_user))
- end
+ def after_update(issuable)
+ # To be overridden by subclasses
end
def update(issuable)
@@ -212,16 +210,22 @@ class IssuableBaseService < BaseService
label_ids = process_label_ids(params, existing_label_ids: issuable.label_ids)
params[:label_ids] = label_ids if labels_changing?(issuable.label_ids, label_ids)
- if params.present? && update_issuable(issuable, params)
- # We do not touch as it will affect a update on updated_at field
- ActiveRecord::Base.no_touching do
- handle_common_system_notes(issuable, old_labels: old_labels)
- end
+ if params.present?
+ issuable.assign_attributes(params.merge(updated_by: current_user))
+
+ before_update(issuable)
- handle_changes(issuable, old_labels: old_labels, old_mentioned_users: old_mentioned_users)
- after_update(issuable)
- issuable.create_new_cross_references!(current_user)
- execute_hooks(issuable, 'update')
+ if issuable.with_transaction_returning_status { issuable.save }
+ # We do not touch as it will affect a update on updated_at field
+ ActiveRecord::Base.no_touching do
+ handle_common_system_notes(issuable, old_labels: old_labels)
+ end
+
+ handle_changes(issuable, old_labels: old_labels, old_mentioned_users: old_mentioned_users)
+ after_update(issuable)
+ issuable.create_new_cross_references!(current_user)
+ execute_hooks(issuable, 'update')
+ end
end
issuable
diff --git a/app/services/issues/create_service.rb b/app/services/issues/create_service.rb
index 961605a1005..366b3572738 100644
--- a/app/services/issues/create_service.rb
+++ b/app/services/issues/create_service.rb
@@ -1,10 +1,9 @@
module Issues
class CreateService < Issues::BaseService
+ include SpamCheckService
+
def execute
- @request = params.delete(:request)
- @api = params.delete(:api)
- @recaptcha_verified = params.delete(:recaptcha_verified)
- @spam_log_id = params.delete(:spam_log_id)
+ filter_spam_check_params
issue_attributes = params.merge(merge_request_for_resolving_discussions: merge_request_for_resolving_discussions)
@issue = BuildService.new(project, current_user, issue_attributes).execute
@@ -12,14 +11,8 @@ module Issues
create(@issue)
end
- def before_create(issuable)
- if @recaptcha_verified
- spam_log = current_user.spam_logs.find_by(id: @spam_log_id, title: issuable.title)
- spam_log&.update!(recaptcha_verified: true)
- else
- issuable.spam = spam_service.check(@api)
- issuable.spam_log = spam_service.spam_log
- end
+ def before_create(issue)
+ spam_check(issue, current_user)
end
def after_create(issuable)
@@ -42,10 +35,6 @@ module Issues
private
- def spam_service
- @spam_service ||= SpamService.new(@issue, @request)
- end
-
def user_agent_detail_service
UserAgentDetailService.new(@issue, @request)
end
diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb
index 78cbf94ec69..22e32b13259 100644
--- a/app/services/issues/update_service.rb
+++ b/app/services/issues/update_service.rb
@@ -1,9 +1,17 @@
module Issues
class UpdateService < Issues::BaseService
+ include SpamCheckService
+
def execute(issue)
+ filter_spam_check_params
+
update(issue)
end
+ def before_update(issue)
+ spam_check(issue, current_user)
+ end
+
def handle_changes(issue, old_labels: [], old_mentioned_users: [])
if has_changes?(issue, old_labels: old_labels)
todo_service.mark_pending_todos_as_done(issue, current_user)
diff --git a/app/services/members/destroy_service.rb b/app/services/members/destroy_service.rb
index 431da8372c9..2e089149ca8 100644
--- a/app/services/members/destroy_service.rb
+++ b/app/services/members/destroy_service.rb
@@ -4,7 +4,7 @@ module Members
attr_accessor :source
- ALLOWED_SCOPES = %i[members requesters all]
+ ALLOWED_SCOPES = %i[members requesters all].freeze
def initialize(source, current_user, params = {})
@source = source
diff --git a/app/services/merge_requests/add_todo_when_build_fails_service.rb b/app/services/merge_requests/add_todo_when_build_fails_service.rb
index 12a8415d9a5..727768b1a39 100644
--- a/app/services/merge_requests/add_todo_when_build_fails_service.rb
+++ b/app/services/merge_requests/add_todo_when_build_fails_service.rb
@@ -18,5 +18,11 @@ module MergeRequests
todo_service.merge_request_build_retried(merge_request)
end
end
+
+ def close_all(pipeline)
+ pipeline_merge_requests(pipeline) do |merge_request|
+ todo_service.merge_request_build_retried(merge_request)
+ end
+ end
end
end
diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb
index f4d52e3ebbd..9d4739e37bb 100644
--- a/app/services/merge_requests/build_service.rb
+++ b/app/services/merge_requests/build_service.rb
@@ -2,18 +2,14 @@ module MergeRequests
class BuildService < MergeRequests::BaseService
def execute
self.merge_request = MergeRequest.new(params)
- merge_request.can_be_created = true
merge_request.compare_commits = []
merge_request.source_project = find_source_project
merge_request.target_project = find_target_project
merge_request.target_branch = find_target_branch
+ merge_request.can_be_created = branches_valid? && source_branch_specified? && target_branch_specified?
- if branches_specified? && branches_valid?
- compare_branches
- assign_title_and_description
- else
- merge_request.can_be_created = false
- end
+ compare_branches if branches_present?
+ assign_title_and_description if merge_request.can_be_created
merge_request
end
@@ -37,11 +33,17 @@ module MergeRequests
target_branch || target_project.default_branch
end
- def branches_specified?
- params[:source_branch] && params[:target_branch]
+ def source_branch_specified?
+ params[:source_branch].present?
+ end
+
+ def target_branch_specified?
+ params[:target_branch].present?
end
def branches_valid?
+ return false unless source_branch_specified? || target_branch_specified?
+
validate_branches
errors.blank?
end
@@ -55,8 +57,10 @@ module MergeRequests
target_branch
)
- merge_request.compare_commits = compare.commits
- merge_request.compare = compare
+ if compare
+ merge_request.compare_commits = compare.commits
+ merge_request.compare = compare
+ end
end
def validate_branches
diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb
index 177b714b734..fac3ac7a4c7 100644
--- a/app/services/merge_requests/merge_service.rb
+++ b/app/services/merge_requests/merge_service.rb
@@ -6,6 +6,8 @@ module MergeRequests
# Executed when you do merge via GitLab UI
#
class MergeService < MergeRequests::BaseService
+ MergeError = Class.new(StandardError)
+
attr_reader :merge_request, :source
def execute(merge_request)
@@ -18,7 +20,7 @@ module MergeRequests
@source = find_merge_source
unless @source
- log_merge_error('No source for merge', save_message_on_model: true)
+ return log_merge_error('No source for merge', save_message_on_model: true)
end
merge_request.in_locked_state do
@@ -27,6 +29,8 @@ module MergeRequests
success
end
end
+ rescue MergeError => e
+ log_merge_error(e.message, save_message_on_model: true)
end
private
@@ -42,19 +46,13 @@ module MergeRequests
commit_id = repository.merge(current_user, source, merge_request, options)
- if commit_id
- merge_request.update(merge_commit_sha: commit_id)
- else
- log_merge_error('Conflicts detected during merge', save_message_on_model: true)
- false
- end
+ raise MergeError, 'Conflicts detected during merge' unless commit_id
+
+ merge_request.update(merge_commit_sha: commit_id)
rescue GitHooksService::PreReceiveError => e
- log_merge_error(e.message, save_message_on_model: true)
- false
+ raise MergeError, e.message
rescue StandardError => e
- merge_request.update(merge_error: "Something went wrong during merge: #{e.message}")
- log_merge_error(e.message)
- false
+ raise MergeError, "Something went wrong during merge: #{e.message}"
ensure
merge_request.update(in_progress_merge_commit_sha: nil)
end
diff --git a/app/services/merge_requests/merge_when_pipeline_succeeds_service.rb b/app/services/merge_requests/merge_when_pipeline_succeeds_service.rb
index 5616edf8b4a..5081dd5a0c4 100644
--- a/app/services/merge_requests/merge_when_pipeline_succeeds_service.rb
+++ b/app/services/merge_requests/merge_when_pipeline_succeeds_service.rb
@@ -24,7 +24,11 @@ module MergeRequests
pipeline_merge_requests(pipeline) do |merge_request|
next unless merge_request.merge_when_build_succeeds?
- next unless merge_request.mergeable?
+
+ unless merge_request.mergeable?
+ todo_service.merge_request_became_unmergeable(merge_request)
+ next
+ end
MergeWorker.perform_async(merge_request.id, merge_request.merge_user_id, merge_request.merge_params)
end
diff --git a/app/services/notes/slash_commands_service.rb b/app/services/notes/slash_commands_service.rb
index 56913568cae..ad1e6f6774a 100644
--- a/app/services/notes/slash_commands_service.rb
+++ b/app/services/notes/slash_commands_service.rb
@@ -3,7 +3,7 @@ module Notes
UPDATE_SERVICES = {
'Issue' => Issues::UpdateService,
'MergeRequest' => MergeRequests::UpdateService
- }
+ }.freeze
def self.noteable_update_service(note)
UPDATE_SERVICES[note.noteable_type]
diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb
index a08c6fcd94b..2e06826c311 100644
--- a/app/services/projects/destroy_service.rb
+++ b/app/services/projects/destroy_service.rb
@@ -4,7 +4,7 @@ module Projects
class DestroyError < StandardError; end
- DELETED_FLAG = '+deleted'
+ DELETED_FLAG = '+deleted'.freeze
def async_execute
project.transaction do
@@ -17,8 +17,6 @@ module Projects
def execute
return false unless can?(current_user, :remove_project, project)
- project.team.truncate
-
repo_path = project.path_with_namespace
wiki_path = repo_path + '.wiki'
@@ -30,6 +28,7 @@ module Projects
Projects::UnlinkForkService.new(project, current_user).execute
Project.transaction do
+ project.team.truncate
project.destroy!
unless remove_registry_tags
diff --git a/app/services/projects/download_service.rb b/app/services/projects/download_service.rb
index f06a3d44c17..604747e39d0 100644
--- a/app/services/projects/download_service.rb
+++ b/app/services/projects/download_service.rb
@@ -2,7 +2,7 @@ module Projects
class DownloadService < BaseService
WHITELIST = [
/^[^.]+\.fogbugz.com$/
- ]
+ ].freeze
def initialize(project, url)
@project, @url = project, url
@@ -25,7 +25,7 @@ module Projects
end
def http?(url)
- url =~ /\A#{URI::regexp(['http', 'https'])}\z/
+ url =~ /\A#{URI.regexp(%w(http https))}\z/
end
def valid_domain?(url)
diff --git a/app/services/projects/update_pages_service.rb b/app/services/projects/update_pages_service.rb
index f5f9ee88912..2d42c4fc04a 100644
--- a/app/services/projects/update_pages_service.rb
+++ b/app/services/projects/update_pages_service.rb
@@ -2,7 +2,7 @@ module Projects
class UpdatePagesService < BaseService
BLOCK_SIZE = 32.kilobytes
MAX_SIZE = 1.terabyte
- SITE_PATH = 'public/'
+ SITE_PATH = 'public/'.freeze
attr_reader :build
diff --git a/app/services/projects/upload_service.rb b/app/services/projects/upload_service.rb
index 012e82a7704..be34d4fa9b8 100644
--- a/app/services/projects/upload_service.rb
+++ b/app/services/projects/upload_service.rb
@@ -5,7 +5,7 @@ module Projects
end
def execute
- return nil unless @file and @file.size <= max_attachment_size
+ return nil unless @file && @file.size <= max_attachment_size
uploader = FileUploader.new(@project)
uploader.store!(@file)
diff --git a/app/services/protected_branches/api_update_service.rb b/app/services/protected_branches/api_update_service.rb
index 050cb3b738b..bdb0e0cc8bf 100644
--- a/app/services/protected_branches/api_update_service.rb
+++ b/app/services/protected_branches/api_update_service.rb
@@ -15,16 +15,16 @@ module ProtectedBranches
case @developers_can_push
when true
- params.merge!(push_access_levels_attributes: [{ access_level: Gitlab::Access::DEVELOPER }])
+ params[:push_access_levels_attributes] = [{ access_level: Gitlab::Access::DEVELOPER }]
when false
- params.merge!(push_access_levels_attributes: [{ access_level: Gitlab::Access::MASTER }])
+ params[:push_access_levels_attributes] = [{ access_level: Gitlab::Access::MASTER }]
end
case @developers_can_merge
when true
- params.merge!(merge_access_levels_attributes: [{ access_level: Gitlab::Access::DEVELOPER }])
+ params[:merge_access_levels_attributes] = [{ access_level: Gitlab::Access::DEVELOPER }]
when false
- params.merge!(merge_access_levels_attributes: [{ access_level: Gitlab::Access::MASTER }])
+ params[:merge_access_levels_attributes] = [{ access_level: Gitlab::Access::MASTER }]
end
service = ProtectedBranches::UpdateService.new(@project, @current_user, @params)
diff --git a/app/services/spam_check_service.rb b/app/services/spam_check_service.rb
new file mode 100644
index 00000000000..023e0824e85
--- /dev/null
+++ b/app/services/spam_check_service.rb
@@ -0,0 +1,24 @@
+# SpamCheckService
+#
+# Provide helper methods for checking if a given spammable object has
+# potential spam data.
+#
+# Dependencies:
+# - params with :request
+#
+module SpamCheckService
+ def filter_spam_check_params
+ @request = params.delete(:request)
+ @api = params.delete(:api)
+ @recaptcha_verified = params.delete(:recaptcha_verified)
+ @spam_log_id = params.delete(:spam_log_id)
+ end
+
+ def spam_check(spammable, user)
+ spam_service = SpamService.new(spammable, @request)
+
+ spam_service.when_recaptcha_verified(@recaptcha_verified, @api) do
+ user.spam_logs.find_by(id: @spam_log_id)&.update!(recaptcha_verified: true)
+ end
+ end
+end
diff --git a/app/services/spam_service.rb b/app/services/spam_service.rb
index 024a7c19d33..3e65b7d31a3 100644
--- a/app/services/spam_service.rb
+++ b/app/services/spam_service.rb
@@ -17,15 +17,6 @@ class SpamService
end
end
- def check(api = false)
- return false unless request && check_for_spam?
-
- return false unless akismet.is_spam?
-
- create_spam_log(api)
- true
- end
-
def mark_as_spam!
return false unless spammable.submittable_as_spam?
@@ -36,8 +27,30 @@ class SpamService
end
end
+ def when_recaptcha_verified(recaptcha_verified, api = false)
+ # In case it's a request which is already verified through recaptcha, yield
+ # block.
+ if recaptcha_verified
+ yield
+ else
+ # Otherwise, it goes to Akismet and check if it's a spam. If that's the
+ # case, it assigns spammable record as "spam" and create a SpamLog record.
+ spammable.spam = check(api)
+ spammable.spam_log = spam_log
+ end
+ end
+
private
+ def check(api)
+ return false unless request && check_for_spam?
+
+ return false unless akismet.is_spam?
+
+ create_spam_log(api)
+ true
+ end
+
def akismet
@akismet ||= AkismetService.new(
spammable_owner,
diff --git a/app/services/system_hooks_service.rb b/app/services/system_hooks_service.rb
index a2bfa422c9d..9b6dd013e3a 100644
--- a/app/services/system_hooks_service.rb
+++ b/app/services/system_hooks_service.rb
@@ -33,9 +33,7 @@ class SystemHooksService
data.merge!(project_data(model))
if event == :rename || event == :transfer
- data.merge!({
- old_path_with_namespace: model.old_path_with_namespace
- })
+ data[:old_path_with_namespace] = model.old_path_with_namespace
end
data
diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb
index 87ba72cf991..55b548a12f9 100644
--- a/app/services/system_note_service.rb
+++ b/app/services/system_note_service.rb
@@ -356,10 +356,10 @@ module SystemNoteService
note: cross_reference_note_content(gfm_reference)
}
- if noteable.kind_of?(Commit)
+ if noteable.is_a?(Commit)
note_options.merge!(noteable_type: 'Commit', commit_id: noteable.id)
else
- note_options.merge!(noteable: noteable)
+ note_options[:noteable] = noteable
end
if noteable.is_a?(ExternalIssue)
@@ -408,12 +408,13 @@ module SystemNoteService
# Initial scope should be system notes of this noteable type
notes = Note.system.where(noteable_type: noteable.class)
- if noteable.is_a?(Commit)
- # Commits have non-integer IDs, so they're stored in `commit_id`
- notes = notes.where(commit_id: noteable.id)
- else
- notes = notes.where(noteable_id: noteable.id)
- end
+ notes =
+ if noteable.is_a?(Commit)
+ # Commits have non-integer IDs, so they're stored in `commit_id`
+ notes.where(commit_id: noteable.id)
+ else
+ notes.where(noteable_id: noteable.id)
+ end
notes_for_mentioner(mentioner, noteable, notes).exists?
end
diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb
index 8ab943f4639..ad86b4f9f42 100644
--- a/app/services/todo_service.rb
+++ b/app/services/todo_service.rb
@@ -170,16 +170,20 @@ class TodoService
# When user marks some todos as done
def mark_todos_as_done(todos, current_user)
- mark_todos_as_done_by_ids(todos.select(&:id), current_user)
+ update_todos_state_by_ids(todos.select(&:id), current_user, :done)
end
def mark_todos_as_done_by_ids(ids, current_user)
- todos = current_user.todos.where(id: ids)
+ update_todos_state_by_ids(ids, current_user, :done)
+ end
- # Only return those that are not really on that state
- marked_todos = todos.where.not(state: :done).update_all(state: :done)
- current_user.update_todos_count_cache
- marked_todos
+ # When user marks some todos as pending
+ def mark_todos_as_pending(todos, current_user)
+ update_todos_state_by_ids(todos.select(&:id), current_user, :pending)
+ end
+
+ def mark_todos_as_pending_by_ids(ids, current_user)
+ update_todos_state_by_ids(ids, current_user, :pending)
end
# When user marks an issue as todo
@@ -194,6 +198,15 @@ class TodoService
private
+ def update_todos_state_by_ids(ids, current_user, state)
+ todos = current_user.todos.where(id: ids)
+
+ # Only return those that are not really on that state
+ marked_todos = todos.where.not(state: state).update_all(state: state)
+ current_user.update_todos_count_cache
+ marked_todos
+ end
+
def create_todos(users, attributes)
Array(users).map do |user|
next if pending_todos(user, attributes).exists?
diff --git a/app/services/update_snippet_service.rb b/app/services/update_snippet_service.rb
index a6bb36821c3..358bca73aec 100644
--- a/app/services/update_snippet_service.rb
+++ b/app/services/update_snippet_service.rb
@@ -1,4 +1,6 @@
class UpdateSnippetService < BaseService
+ include SpamCheckService
+
attr_accessor :snippet
def initialize(project, user, snippet, params)
@@ -9,7 +11,7 @@ class UpdateSnippetService < BaseService
def execute
# check that user is allowed to set specified visibility_level
new_visibility = params[:visibility_level]
-
+
if new_visibility && new_visibility.to_i != snippet.visibility_level
unless Gitlab::VisibilityLevel.allowed_for?(current_user, new_visibility)
deny_visibility_level(snippet, new_visibility)
@@ -17,6 +19,10 @@ class UpdateSnippetService < BaseService
end
end
- snippet.update_attributes(params)
+ filter_spam_check_params
+ snippet.assign_attributes(params)
+ spam_check(snippet, current_user)
+
+ snippet.save
end
end
diff --git a/app/services/users/destroy_service.rb b/app/services/users/destroy_service.rb
index 2d11305be13..833da5bc5d1 100644
--- a/app/services/users/destroy_service.rb
+++ b/app/services/users/destroy_service.rb
@@ -7,6 +7,10 @@ module Users
end
def execute(user, options = {})
+ unless Ability.allowed?(current_user, :destroy_user, user)
+ raise Gitlab::Access::AccessDeniedError, "#{current_user} tried to destroy user #{user}!"
+ end
+
if !options[:delete_solo_owned_groups] && user.solo_owned_groups.present?
user.errors[:base] << 'You must transfer ownership or delete groups before you can remove user'
return user
@@ -22,6 +26,8 @@ module Users
::Projects::DestroyService.new(project, current_user, skip_repo: true).async_execute
end
+ move_issues_to_ghost_user(user)
+
# Destroy the namespace after destroying the user since certain methods may depend on the namespace existing
namespace = user.namespace
user_data = user.destroy
@@ -29,5 +35,22 @@ module Users
user_data
end
+
+ private
+
+ def move_issues_to_ghost_user(user)
+ # Block the user before moving issues to prevent a data race.
+ # If the user creates an issue after `move_issues_to_ghost_user`
+ # runs and before the user is destroyed, the destroy will fail with
+ # an exception. We block the user so that issues can't be created
+ # after `move_issues_to_ghost_user` runs and before the destroy happens.
+ user.block
+
+ ghost_user = User.ghost
+
+ user.issues.update_all(author_id: ghost_user.id)
+
+ user.reload
+ end
end
end
diff --git a/app/services/users/refresh_authorized_projects_service.rb b/app/services/users/refresh_authorized_projects_service.rb
index fad741531ea..d9370bbb598 100644
--- a/app/services/users/refresh_authorized_projects_service.rb
+++ b/app/services/users/refresh_authorized_projects_service.rb
@@ -115,11 +115,23 @@ module Users
# Returns a union query of projects that the user is authorized to access
def project_authorizations_union
relations = [
+ # Personal projects
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,
+
+ # Projects the user is a member of
user.projects.select_for_project_authorization,
+
+ # Projects of groups the user is a member of
+ user.groups_projects.select_for_project_authorization,
+
+ # Projects of subgroups of groups the user is a member of
+ user.nested_groups_projects.select_for_project_authorization,
+
+ # Projects shared with groups the user is a member of
user.groups.joins(:shared_projects).select_for_project_authorization,
- user.nested_projects.select_for_project_authorization
+
+ # Projects shared with subgroups of groups the user is a member of
+ user.nested_groups.joins(:shared_projects).select_for_project_authorization
]
Gitlab::SQL::Union.new(relations)
diff --git a/app/uploaders/artifact_uploader.rb b/app/uploaders/artifact_uploader.rb
index 86f317dcd18..e84944ed411 100644
--- a/app/uploaders/artifact_uploader.rb
+++ b/app/uploaders/artifact_uploader.rb
@@ -27,10 +27,6 @@ class ArtifactUploader < GitlabUploader
File.join(self.class.artifacts_cache_path, @build.artifacts_path)
end
- def file_storage?
- self.class.storage == CarrierWave::Storage::File
- end
-
def filename
file.try(:filename)
end
diff --git a/app/uploaders/attachment_uploader.rb b/app/uploaders/attachment_uploader.rb
index cfcb877cc3e..6aa1f5a8c50 100644
--- a/app/uploaders/attachment_uploader.rb
+++ b/app/uploaders/attachment_uploader.rb
@@ -4,6 +4,6 @@ class AttachmentUploader < GitlabUploader
storage :file
def store_dir
- "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
+ "#{base_dir}/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
end
end
diff --git a/app/uploaders/avatar_uploader.rb b/app/uploaders/avatar_uploader.rb
index 265cea2d2c6..b4c393c6f2c 100644
--- a/app/uploaders/avatar_uploader.rb
+++ b/app/uploaders/avatar_uploader.rb
@@ -4,7 +4,7 @@ class AvatarUploader < GitlabUploader
storage :file
def store_dir
- "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
+ "#{base_dir}/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
end
def exists?
diff --git a/app/uploaders/file_uploader.rb b/app/uploaders/file_uploader.rb
index 23b7318827c..0d2edaeff3b 100644
--- a/app/uploaders/file_uploader.rb
+++ b/app/uploaders/file_uploader.rb
@@ -4,15 +4,12 @@ class FileUploader < GitlabUploader
storage :file
- attr_accessor :project, :secret
+ attr_accessor :project
+ attr_reader :secret
def initialize(project, secret = nil)
@project = project
- @secret = secret || self.class.generate_secret
- end
-
- def base_dir
- "uploads"
+ @secret = secret || generate_secret
end
def store_dir
@@ -23,10 +20,6 @@ class FileUploader < GitlabUploader
File.join(base_dir, 'tmp', @project.path_with_namespace, @secret)
end
- def secure_url
- File.join("/uploads", @secret, file.filename)
- end
-
def to_markdown
to_h[:markdown]
end
@@ -35,17 +28,23 @@ class FileUploader < GitlabUploader
filename = image_or_video? ? self.file.basename : self.file.filename
escaped_filename = filename.gsub("]", "\\]")
- markdown = "[#{escaped_filename}](#{self.secure_url})"
+ markdown = "[#{escaped_filename}](#{secure_url})"
markdown.prepend("!") if image_or_video? || dangerous?
{
alt: filename,
- url: self.secure_url,
+ url: secure_url,
markdown: markdown
}
end
- def self.generate_secret
+ private
+
+ def generate_secret
SecureRandom.hex
end
+
+ def secure_url
+ File.join('/uploads', @secret, file.filename)
+ end
end
diff --git a/app/uploaders/gitlab_uploader.rb b/app/uploaders/gitlab_uploader.rb
index 02d7c601d6c..bd7de4ed562 100644
--- a/app/uploaders/gitlab_uploader.rb
+++ b/app/uploaders/gitlab_uploader.rb
@@ -1,4 +1,14 @@
class GitlabUploader < CarrierWave::Uploader::Base
+ def self.base_dir
+ 'uploads'
+ end
+
+ delegate :base_dir, to: :class
+
+ def file_storage?
+ self.class.storage == CarrierWave::Storage::File
+ end
+
# Reduce disk IO
def move_to_cache
true
diff --git a/app/uploaders/uploader_helper.rb b/app/uploaders/uploader_helper.rb
index 35fd1ed23f8..7635c20ab3a 100644
--- a/app/uploaders/uploader_helper.rb
+++ b/app/uploaders/uploader_helper.rb
@@ -1,15 +1,15 @@
# Extra methods for uploader
module UploaderHelper
- IMAGE_EXT = %w[png jpg jpeg gif bmp tiff]
+ IMAGE_EXT = %w[png jpg jpeg gif bmp tiff].freeze
# We recommend using the .mp4 format over .mov. Videos in .mov format can
# still be used but you really need to make sure they are served with the
# proper MIME type video/mp4 and not video/quicktime or your videos won't play
# on IE >= 9.
# http://archive.sublimevideo.info/20150912/docs.sublimevideo.net/troubleshooting.html
- VIDEO_EXT = %w[mp4 m4v mov webm ogv]
+ VIDEO_EXT = %w[mp4 m4v mov webm ogv].freeze
# These extension types can contain dangerous code and should only be embedded inline with
# proper filtering. They should always be tagged as "Content-Disposition: attachment", not "inline".
- DANGEROUS_EXT = %w[svg]
+ DANGEROUS_EXT = %w[svg].freeze
def image?
extension_match?(IMAGE_EXT)
@@ -27,6 +27,8 @@ module UploaderHelper
extension_match?(DANGEROUS_EXT)
end
+ private
+
def extension_match?(extensions)
return false unless file
@@ -40,8 +42,4 @@ module UploaderHelper
extensions.include?(extension.downcase)
end
-
- def file_storage?
- self.class.storage == CarrierWave::Storage::File
- end
end
diff --git a/app/validators/addressable_url_validator.rb b/app/validators/addressable_url_validator.rb
index 09bfa613cbe..94542125d43 100644
--- a/app/validators/addressable_url_validator.rb
+++ b/app/validators/addressable_url_validator.rb
@@ -18,7 +18,7 @@
# end
#
class AddressableUrlValidator < ActiveModel::EachValidator
- DEFAULT_OPTIONS = { protocols: %w(http https ssh git) }
+ DEFAULT_OPTIONS = { protocols: %w(http https ssh git) }.freeze
def validate_each(record, attribute, value)
unless valid_url?(value)
diff --git a/app/validators/duration_validator.rb b/app/validators/duration_validator.rb
new file mode 100644
index 00000000000..10ff44031c6
--- /dev/null
+++ b/app/validators/duration_validator.rb
@@ -0,0 +1,17 @@
+# DurationValidator
+#
+# Validate the format conforms with ChronicDuration
+#
+# Example:
+#
+# class ApplicationSetting < ActiveRecord::Base
+# validates :default_artifacts_expire_in, presence: true, duration: true
+# end
+#
+class DurationValidator < ActiveModel::EachValidator
+ def validate_each(record, attribute, value)
+ ChronicDuration.parse(value)
+ rescue ChronicDuration::DurationParseError
+ record.errors.add(attribute, "is not a correct duration")
+ end
+end
diff --git a/app/views/admin/abuse_reports/index.html.haml b/app/views/admin/abuse_reports/index.html.haml
index c4b748d0ab8..6c48328da4f 100644
--- a/app/views/admin/abuse_reports/index.html.haml
+++ b/app/views/admin/abuse_reports/index.html.haml
@@ -12,6 +12,7 @@
%th.wide Message
%th Action
= render @abuse_reports
+ = paginate @abuse_reports, theme: 'gitlab'
- else
.empty-state
.text-center
diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml
index 816035ec442..057b584e1bc 100644
--- a/app/views/admin/application_settings/_form.html.haml
+++ b/app/views/admin/application_settings/_form.html.haml
@@ -192,7 +192,7 @@
= 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
+ .help-block 0 for unlimited
%fieldset
%legend Continuous Integration
@@ -212,8 +212,16 @@
.col-sm-10
= f.number_field :max_artifacts_size, class: 'form-control'
.help-block
- 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")
+ Set the maximum file size for each job's artifacts
+ = link_to icon('question-circle'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'maximum-artifacts-size')
+ .form-group
+ = f.label :default_artifacts_expire_in, 'Default artifacts expiration', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.text_field :default_artifacts_expire_in, class: 'form-control'
+ .help-block
+ Set the default expiration time for each job's artifacts.
+ 0 for unlimited.
+ = link_to icon('question-circle'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'default-artifacts-expiration')
- if Gitlab.config.registry.enabled
%fieldset
@@ -525,7 +533,7 @@
= 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.
+ 0 for unlimited.
.form-actions
= f.submit 'Save', class: 'btn btn-save'
diff --git a/app/views/admin/background_jobs/show.html.haml b/app/views/admin/background_jobs/show.html.haml
index 4f982a6e369..ac36bb5bb17 100644
--- a/app/views/admin/background_jobs/show.html.haml
+++ b/app/views/admin/background_jobs/show.html.haml
@@ -35,7 +35,7 @@
.clearfix
%p
%i.fa.fa-exclamation-circle
- If '[25 of 25 busy]' is shown, restart GitLab with 'sudo service gitlab reload'.
+ If '[#{@concurrency} of #{@concurrency} busy]' is shown, restart GitLab with 'sudo service gitlab reload'.
%p
%i.fa.fa-exclamation-circle
If more than one sidekiq process is listed, stop GitLab, kill the remaining sidekiq processes (sudo pkill -u #{gitlab_config.user} -f sidekiq) and restart GitLab.
diff --git a/app/views/admin/logs/show.html.haml b/app/views/admin/logs/show.html.haml
index 13d00dd1fcb..5e585ce789b 100644
--- a/app/views/admin/logs/show.html.haml
+++ b/app/views/admin/logs/show.html.haml
@@ -8,15 +8,14 @@
%div{ class: container_class }
%ul.nav-links.log-tabs
- loggers.each do |klass|
- %li{ class: (klass == Gitlab::GitLogger ? 'active' : '') }>
+ %li{ class: active_when(klass == Gitlab::GitLogger) }>
= link_to klass::file_name, "##{klass::file_name_noext}",
'data-toggle' => 'tab'
.row-content-block
To prevent performance issues admin logs output the last 2000 lines
.tab-content
- loggers.each do |klass|
- .tab-pane{ class: (klass == Gitlab::GitLogger ? 'active' : ''),
- id: klass::file_name_noext }
+ .tab-pane{ class: active_when(klass == Gitlab::GitLogger), id: klass::file_name_noext }
.file-holder#README
.js-file-title.file-title
%i.fa.fa-file
diff --git a/app/views/admin/projects/index.html.haml b/app/views/admin/projects/index.html.haml
index cdef63daca9..c35945c5a35 100644
--- a/app/views/admin/projects/index.html.haml
+++ b/app/views/admin/projects/index.html.haml
@@ -48,13 +48,13 @@
= link_to admin_projects_path do
All
- = nav_link(html_options: { class: params[:visibility_level] == Gitlab::VisibilityLevel::PRIVATE.to_s ? 'active' : '' }) do
+ = nav_link(html_options: { class: active_when(params[:visibility_level] == Gitlab::VisibilityLevel::PRIVATE.to_s) }) do
= link_to admin_projects_path(visibility_level: Gitlab::VisibilityLevel::PRIVATE) do
Private
- = nav_link(html_options: { class: params[:visibility_level] == Gitlab::VisibilityLevel::INTERNAL.to_s ? 'active' : '' }) do
+ = nav_link(html_options: { class: active_when(params[:visibility_level] == Gitlab::VisibilityLevel::INTERNAL.to_s) }) do
= link_to admin_projects_path(visibility_level: Gitlab::VisibilityLevel::INTERNAL) do
Internal
- = nav_link(html_options: { class: params[:visibility_level] == Gitlab::VisibilityLevel::PUBLIC.to_s ? 'active' : '' }) do
+ = nav_link(html_options: { class: active_when(params[:visibility_level] == Gitlab::VisibilityLevel::PUBLIC.to_s) }) do
= link_to admin_projects_path(visibility_level: Gitlab::VisibilityLevel::PUBLIC) do
Public
diff --git a/app/views/admin/runners/_runner.html.haml b/app/views/admin/runners/_runner.html.haml
index deb62845e1c..d4d166ab7b6 100644
--- a/app/views/admin/runners/_runner.html.haml
+++ b/app/views/admin/runners/_runner.html.haml
@@ -15,6 +15,8 @@
%td
= runner.description
%td
+ = runner.version
+ %td
- if runner.shared?
n/a
- else
diff --git a/app/views/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml
index 721bc77cc2f..7d26864d0f3 100644
--- a/app/views/admin/runners/index.html.haml
+++ b/app/views/admin/runners/index.html.haml
@@ -56,7 +56,7 @@
= submit_tag 'Search', class: 'btn'
.pull-right.light
- Runners with last contact less than a minute ago: #{@active_runners_cnt}
+ Runners with last contact more than a minute ago: #{@active_runners_cnt}
%br
@@ -67,6 +67,7 @@
%th Type
%th Runner token
%th Description
+ %th Version
%th Projects
%th Jobs
%th Tags
diff --git a/app/views/admin/users/_user.html.haml b/app/views/admin/users/_user.html.haml
index 3b5c713ac2d..a756cb7243a 100644
--- a/app/views/admin/users/_user.html.haml
+++ b/app/views/admin/users/_user.html.haml
@@ -34,7 +34,7 @@
- if user.access_locked?
%li
= link_to 'Unlock', unlock_admin_user_path(user), method: :put, class: 'btn-grouped btn btn-xs btn-success', data: { confirm: 'Are you sure?' }
- - if user.can_be_removed?
+ - if user.can_be_removed? && can?(current_user, :destroy_user, @user)
%li.divider
%li
= link_to 'Delete User', [:admin, user], data: { confirm: "USER #{user.name} WILL BE REMOVED! All issues, merge requests and groups linked to this user will also be removed! Consider cancelling this deletion and blocking the user instead. Are you sure?" },
diff --git a/app/views/admin/users/index.html.haml b/app/views/admin/users/index.html.haml
index 4dc44225d49..298cf0fa950 100644
--- a/app/views/admin/users/index.html.haml
+++ b/app/views/admin/users/index.html.haml
@@ -38,31 +38,31 @@
.nav-block
%ul.nav-links.wide.scrolling-tabs.white.scrolling-tabs
.fade-left
- = nav_link(html_options: { class: ('active' unless params[:filter]) }) do
+ = nav_link(html_options: { class: active_when(params[:filter].nil?) }) do
= link_to admin_users_path do
Active
%small.badge= number_with_delimiter(User.active.count)
- = nav_link(html_options: { class: ('active' if params[:filter] == 'admins') }) do
+ = nav_link(html_options: { class: active_when(params[:filter] == 'admins') }) do
= link_to admin_users_path(filter: "admins") do
Admins
%small.badge= number_with_delimiter(User.admins.count)
- = nav_link(html_options: { class: "#{'active' if params[:filter] == 'two_factor_enabled'} filter-two-factor-enabled" }) do
+ = nav_link(html_options: { class: "#{active_when(params[:filter] == 'two_factor_enabled')} filter-two-factor-enabled" }) do
= link_to admin_users_path(filter: 'two_factor_enabled') do
2FA Enabled
%small.badge= number_with_delimiter(User.with_two_factor.count)
- = nav_link(html_options: { class: "#{'active' if params[:filter] == 'two_factor_disabled'} filter-two-factor-disabled" }) do
+ = nav_link(html_options: { class: "#{active_when(params[:filter] == 'two_factor_disabled')} filter-two-factor-disabled" }) do
= link_to admin_users_path(filter: 'two_factor_disabled') do
2FA Disabled
%small.badge= number_with_delimiter(User.without_two_factor.count)
- = nav_link(html_options: { class: ('active' if params[:filter] == 'external') }) do
+ = nav_link(html_options: { class: active_when(params[:filter] == 'external') }) do
= link_to admin_users_path(filter: 'external') do
External
%small.badge= number_with_delimiter(User.external.count)
- = nav_link(html_options: { class: ('active' if params[:filter] == 'blocked') }) do
+ = nav_link(html_options: { class: active_when(params[:filter] == 'blocked') }) do
= link_to admin_users_path(filter: "blocked") do
Blocked
%small.badge= number_with_delimiter(User.blocked.count)
- = nav_link(html_options: { class: ('active' if params[:filter] == 'wop') }) do
+ = nav_link(html_options: { class: active_when(params[:filter] == 'wop') }) do
= link_to admin_users_path(filter: "wop") do
Without projects
%small.badge= number_with_delimiter(User.without_projects.count)
diff --git a/app/views/admin/users/show.html.haml b/app/views/admin/users/show.html.haml
index 76b1291fe10..840d843f069 100644
--- a/app/views/admin/users/show.html.haml
+++ b/app/views/admin/users/show.html.haml
@@ -173,7 +173,7 @@
.panel-heading
Remove user
.panel-body
- - if @user.can_be_removed?
+ - if @user.can_be_removed? && can?(current_user, :destroy_user, @user)
%p Deleting a user has the following effects:
%ul
%li All user content like authored issues, snippets, comments will be removed
@@ -189,3 +189,6 @@
%strong= @user.solo_owned_groups.map(&:name).join(', ')
%p
You must transfer ownership or delete these groups before you can delete this user.
+ - else
+ %p
+ You don't have access to delete this user.
diff --git a/app/views/dashboard/_activities.html.haml b/app/views/dashboard/_activities.html.haml
index dc76599b776..0dbb0ca6958 100644
--- a/app/views/dashboard/_activities.html.haml
+++ b/app/views/dashboard/_activities.html.haml
@@ -4,7 +4,7 @@
.nav-block
- if current_user
.controls
- = link_to dashboard_projects_path(:atom, { private_token: current_user.private_token }), class: 'btn rss-btn' do
+ = link_to dashboard_projects_path(:atom, { private_token: current_user.private_token }), class: 'btn rss-btn has-tooltip', title: 'Subscribe' do
%i.fa.fa-rss
= render 'shared/event_filter'
diff --git a/app/views/dashboard/_activity_head.html.haml b/app/views/dashboard/_activity_head.html.haml
index 68a46f61eb7..ecdf76ef5c5 100644
--- a/app/views/dashboard/_activity_head.html.haml
+++ b/app/views/dashboard/_activity_head.html.haml
@@ -1,8 +1,8 @@
.top-area
%ul.nav-links
- %li{ class: ("active" unless params[:filter]) }>
+ %li{ class: active_when(params[:filter].nil?) }>
= link_to activity_dashboard_path, class: 'shortcuts-activity', data: {placement: 'right'} do
Your Projects
- %li{ class: ("active" if params[:filter] == 'starred') }>
+ %li{ class: active_when(params[:filter] == 'starred') }>
= link_to activity_dashboard_path(filter: 'starred'), data: {placement: 'right'} do
Starred Projects
diff --git a/app/views/dashboard/projects/index.html.haml b/app/views/dashboard/projects/index.html.haml
index 4f36a4a1c73..b82b933c3ad 100644
--- a/app/views/dashboard/projects/index.html.haml
+++ b/app/views/dashboard/projects/index.html.haml
@@ -5,6 +5,8 @@
- page_title "Projects"
- header_title "Projects", dashboard_projects_path
+.user-callout{ 'callout-svg' => custom_icon('icon_customization') }
+
- if @projects.any? || params[:filter_projects]
= render 'dashboard/projects_head'
diff --git a/app/views/dashboard/todos/_todo.html.haml b/app/views/dashboard/todos/_todo.html.haml
index 605bfd0cf8d..a3993d5ef16 100644
--- a/app/views/dashboard/todos/_todo.html.haml
+++ b/app/views/dashboard/todos/_todo.html.haml
@@ -1,28 +1,33 @@
%li{ class: "todo todo-#{todo.done? ? 'done' : 'pending'}", id: dom_id(todo), data: { url: todo_target_path(todo) } }
- = author_avatar(todo, size: 40)
+ .todo-avatar
+ = author_avatar(todo, size: 40)
.todo-item.todo-block
.todo-title.title
- unless todo.build_failed? || todo.unmergeable?
= todo_target_state_pill(todo)
- %span.author-name
+ .title-item.author-name
- if todo.author
= link_to_author(todo)
- else
(removed)
- %span.action-name
+ .title-item.action-name
= todo_action_name(todo)
- %span.todo-label
+ .title-item.todo-label
- if todo.target
= todo_target_link(todo)
- else
(removed)
- &middot; #{time_ago_with_tooltip(todo.created_at)}
- = todo_due_date(todo)
+ .title-item
+ &middot;
+
+ .title-item
+ #{time_ago_with_tooltip(todo.created_at)}
+ = todo_due_date(todo)
.todo-body
.todo-note
@@ -31,6 +36,9 @@
- if todo.pending?
.todo-actions
- = link_to [:dashboard, todo], method: :delete, class: 'btn btn-loading done-todo' do
+ = link_to [:dashboard, todo], method: :delete, class: 'btn btn-loading js-done-todo' do
Done
= icon('spinner spin')
+ = link_to restore_dashboard_todo_path(todo), method: :patch, class: 'btn btn-loading js-undo-todo hidden' do
+ Undo
+ = icon('spinner spin')
diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml
index c4bf2c90cc2..16a5713948a 100644
--- a/app/views/dashboard/todos/index.html.haml
+++ b/app/views/dashboard/todos/index.html.haml
@@ -4,15 +4,13 @@
- if current_user.todos.any?
.top-area
%ul.nav-links
- - todo_pending_active = ('active' if params[:state].blank? || params[:state] == 'pending')
- %li{ class: "todos-pending #{todo_pending_active}" }>
+ %li.todos-pending{ class: active_when(params[:state].blank? || params[:state] == 'pending') }>
= 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.todos-done{ class: active_when(params[:state] == 'done') }>
= link_to todos_filter_path(state: 'done') do
%span
Done
diff --git a/app/views/devise/shared/_signin_box.html.haml b/app/views/devise/shared/_signin_box.html.haml
index eddfce363a7..da4769e214e 100644
--- a/app/views/devise/shared/_signin_box.html.haml
+++ b/app/views/devise/shared/_signin_box.html.haml
@@ -4,7 +4,7 @@
.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_when(i.zero? && !crowd_enabled?) }
.login-body
= render 'devise/sessions/new_ldap', server: server
- if signin_enabled?
diff --git a/app/views/devise/shared/_signup_box.html.haml b/app/views/devise/shared/_signup_box.html.haml
index 5a44ec45b7b..30e63d991bb 100644
--- a/app/views/devise/shared/_signup_box.html.haml
+++ b/app/views/devise/shared/_signup_box.html.haml
@@ -8,7 +8,7 @@
= f.text_field :name, class: "form-control top", required: true, title: "This field is required."
.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.'
+ = f.text_field :username, class: "form-control middle", pattern: Gitlab::Regex::NAMESPACE_REGEX_STR_JS, required: true, title: 'Please create a username with only alphanumeric characters.'
%p.validation-error.hide Username is already taken.
%p.validation-success.hide Username is available.
%p.validation-pending.hide Checking username availability...
diff --git a/app/views/devise/shared/_tabs_ldap.html.haml b/app/views/devise/shared/_tabs_ldap.html.haml
index 8c4ad30c832..dd34600490e 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_when(i.zero? && !crowd_enabled?) }
= link_to server['label'], "##{server['provider_name']}", 'data-toggle' => 'tab'
- if signin_enabled?
%li
diff --git a/app/views/events/event/_push.html.haml b/app/views/events/event/_push.html.haml
index 64ca3c32e01..efd13aabf20 100644
--- a/app/views/events/event/_push.html.haml
+++ b/app/views/events/event/_push.html.haml
@@ -3,11 +3,9 @@
.event-title
%span.author_name= link_to_author event
%span.pushed #{event.action_name} #{event.ref_type}
- - if event.rm_ref?
- %strong= event.ref_name
- - else
- %strong
- = link_to event.ref_name, namespace_project_commits_path(project.namespace, project, event.ref_name), title: h(event.target_title)
+ %strong
+ - commits_link = namespace_project_commits_path(project.namespace, project, event.ref_name)
+ = link_to_if project.repository.branch_exists?(event.ref_name), event.ref_name, commits_link
= render "events/event_scope", event: event
diff --git a/app/views/explore/projects/_filter.html.haml b/app/views/explore/projects/_filter.html.haml
index e3088848492..56f463572bb 100644
--- a/app/views/explore/projects/_filter.html.haml
+++ b/app/views/explore/projects/_filter.html.haml
@@ -13,7 +13,7 @@
= link_to filter_projects_path(visibility_level: nil) do
Any
- Gitlab::VisibilityLevel.values.each do |level|
- %li{ class: (level.to_s == params[:visibility_level]) ? 'active' : 'light' }
+ %li{ class: active_when(level.to_s == params[:visibility_level]) || 'light' }
= link_to filter_projects_path(visibility_level: level) do
= visibility_level_icon(level)
= visibility_level_label(level)
@@ -34,7 +34,7 @@
Any
- @tags.each do |tag|
- %li{ class: (tag.name == params[:tag]) ? 'active' : 'light' }
+ %li{ class: active_when(tag.name == params[:tag]) || 'light' }
= link_to filter_projects_path(tag: tag.name) do
= icon('tag')
= tag.name
diff --git a/app/views/groups/_activities.html.haml b/app/views/groups/_activities.html.haml
index 71cc4d87b1f..c442cf056c3 100644
--- a/app/views/groups/_activities.html.haml
+++ b/app/views/groups/_activities.html.haml
@@ -4,7 +4,7 @@
.nav-block
- if current_user
.controls
- = link_to group_path(@group, format: :atom, private_token: current_user.private_token), class: 'btn rss-btn' do
+ = link_to group_path(@group, format: :atom, private_token: current_user.private_token), class: 'btn rss-btn has-tooltip' , title: 'Subscribe' do
%i.fa.fa-rss
= render 'shared/event_filter'
diff --git a/app/views/groups/_head.html.haml b/app/views/groups/_head.html.haml
new file mode 100644
index 00000000000..873504099d4
--- /dev/null
+++ b/app/views/groups/_head.html.haml
@@ -0,0 +1,14 @@
+= content_for :sub_nav do
+ .scrolling-tabs-container.sub-nav-scroll
+ = render 'shared/nav_scroll'
+ .nav-links.sub-nav.scrolling-tabs
+ %ul{ class: container_class }
+ = nav_link(path: ['groups#show', 'groups#subgroups'], html_options: { class: 'home' }) do
+ = link_to group_path(@group), title: 'Group Home' do
+ %span
+ Home
+
+ = nav_link(path: 'groups#activity') do
+ = link_to activity_group_path(@group), title: 'Activity' do
+ %span
+ Activity
diff --git a/app/views/groups/_head_issues.html.haml b/app/views/groups/_head_issues.html.haml
new file mode 100644
index 00000000000..d554bc23743
--- /dev/null
+++ b/app/views/groups/_head_issues.html.haml
@@ -0,0 +1,19 @@
+= content_for :sub_nav do
+ .scrolling-tabs-container.sub-nav-scroll
+ = render 'shared/nav_scroll'
+ .nav-links.sub-nav.scrolling-tabs
+ %ul{ class: container_class }
+ = nav_link(path: 'groups#issues', html_options: { class: 'home' }) do
+ = link_to issues_group_path(@group), title: 'List' do
+ %span
+ List
+
+ = nav_link(path: 'labels#index') do
+ = link_to group_labels_path(@group), title: 'Labels' do
+ %span
+ Labels
+
+ = nav_link(path: 'milestones#index') do
+ = link_to group_milestones_path(@group), title: 'Milestones' do
+ %span
+ Milestones
diff --git a/app/views/groups/activity.html.haml b/app/views/groups/activity.html.haml
index aaad265b3ee..d7375b23524 100644
--- a/app/views/groups/activity.html.haml
+++ b/app/views/groups/activity.html.haml
@@ -2,7 +2,8 @@
- if current_user
= auto_discovery_link_tag(:atom, group_url(@group, format: :atom, private_token: current_user.private_token), title: "#{@group.name} activity")
-- page_title "Activity"
+- page_title "Activity"
+= render 'groups/head'
%section.activities
= render 'activities'
diff --git a/app/views/groups/issues.html.haml b/app/views/groups/issues.html.haml
index 83edb719692..939bddf3fe9 100644
--- a/app/views/groups/issues.html.haml
+++ b/app/views/groups/issues.html.haml
@@ -1,4 +1,5 @@
- page_title "Issues"
+= render "head_issues"
= 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: "#{@group.name} issues")
diff --git a/app/views/groups/labels/index.html.haml b/app/views/groups/labels/index.html.haml
index 45325d6bc4b..2bc00fb16c8 100644
--- a/app/views/groups/labels/index.html.haml
+++ b/app/views/groups/labels/index.html.haml
@@ -1,4 +1,5 @@
- page_title 'Labels'
+= render "groups/head_issues"
.top-area.adjust
.nav-text
diff --git a/app/views/groups/milestones/index.html.haml b/app/views/groups/milestones/index.html.haml
index cd5388fe402..644895c56a1 100644
--- a/app/views/groups/milestones/index.html.haml
+++ b/app/views/groups/milestones/index.html.haml
@@ -1,4 +1,5 @@
- page_title "Milestones"
+= render "groups/head_issues"
.top-area
= render 'shared/milestones_filter'
diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml
index b040f404ac4..3d7b469660a 100644
--- a/app/views/groups/show.html.haml
+++ b/app/views/groups/show.html.haml
@@ -4,6 +4,7 @@
- if current_user
= auto_discovery_link_tag(:atom, group_url(@group, format: :atom, private_token: current_user.private_token), title: "#{@group.name} activity")
+= render 'groups/head'
= render 'groups/home_panel'
diff --git a/app/views/groups/subgroups.html.haml b/app/views/groups/subgroups.html.haml
index 8610ae7e0ef..be809083139 100644
--- a/app/views/groups/subgroups.html.haml
+++ b/app/views/groups/subgroups.html.haml
@@ -1,5 +1,6 @@
- @no_container = true
+= render 'head'
= render 'groups/home_panel'
.groups-header{ class: container_class }
diff --git a/app/views/kaminari/gitlab/_page.html.haml b/app/views/kaminari/gitlab/_page.html.haml
index cefe0066a8f..5c5be03a7cd 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?}" }
+%li.page{ class: [active_when(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/layouts/_head.html.haml b/app/views/layouts/_head.html.haml
index f2d355587bd..302c1794628 100644
--- a/app/views/layouts/_head.html.haml
+++ b/app/views/layouts/_head.html.haml
@@ -23,7 +23,7 @@
%title= page_title(site_name)
%meta{ name: "description", content: page_description }
- = favicon_link_tag 'favicon.ico'
+ = favicon_link_tag favicon
= stylesheet_link_tag "application", media: "all"
= stylesheet_link_tag "print", media: "print"
diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml
index 54d02ee8e4b..a35a918d501 100644
--- a/app/views/layouts/_page.html.haml
+++ b/app/views/layouts/_page.html.haml
@@ -1,21 +1,4 @@
-.page-with-sidebar{ class: "#{page_sidebar_class} #{page_gutter_class}" }
- .sidebar-wrapper.nicescroll
- .sidebar-action-buttons
- .nav-header-btn.toggle-nav-collapse{ title: "Open/Close" }
- %span.sr-only Toggle navigation
- = icon('bars')
-
- %div{ class: "nav-header-btn pin-nav-btn has-tooltip #{'is-active' if pinned_nav?} js-nav-pin", title: pinned_nav? ? "Unpin navigation" : "Pin Navigation", data: { placement: 'right', container: 'body' } }
- %span.sr-only Toggle navigation pinning
- = icon('fw thumb-tack')
-
- - if defined?(sidebar) && sidebar
- = render "layouts/nav/#{sidebar}"
- - elsif current_user
- = render 'layouts/nav/dashboard'
- - else
- = render 'layouts/nav/explore'
-
+.page-with-sidebar{ class: page_gutter_class }
- if defined?(nav) && nav
.layout-nav
.container-fluid
diff --git a/app/views/layouts/_recaptcha_verification.html.haml b/app/views/layouts/_recaptcha_verification.html.haml
new file mode 100644
index 00000000000..77c77dc6754
--- /dev/null
+++ b/app/views/layouts/_recaptcha_verification.html.haml
@@ -0,0 +1,23 @@
+- humanized_resource_name = spammable.class.model_name.human.downcase
+- resource_name = spammable.class.model_name.singular
+
+%h3.page-title
+ Anti-spam verification
+%hr
+
+%p
+ #{"We detected potential spam in the #{humanized_resource_name}. Please solve the reCAPTCHA to proceed."}
+
+= form_for form do |f|
+ .recaptcha
+ - params[resource_name].each do |field, value|
+ = hidden_field(resource_name, field, value: value)
+ = hidden_field_tag(:spam_log_id, spammable.spam_log.id)
+ = hidden_field_tag(:recaptcha_verification, true)
+ = recaptcha_tags
+
+ -# Yields a block with given extra params.
+ = yield
+
+ .row-content-block.footer-block
+ = f.submit "Submit #{humanized_resource_name}", class: 'btn btn-create'
diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml
index 248d439cd05..19bd9b6d5c9 100644
--- a/app/views/layouts/application.html.haml
+++ b/app/views/layouts/application.html.haml
@@ -1,7 +1,7 @@
!!! 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{ data: { page: body_data_page, project: "#{@project.path if @project}", group: "#{@group.path if @group}" } }
= Gon::Base.render_data
= render "layouts/header/default", title: header_title
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index 59082ce5fd5..555ec8ad079 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -2,9 +2,15 @@
%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')
+ .dropdown.global-dropdown
+ %button.global-dropdown-toggle{ type: 'button', 'data-toggle' => 'dropdown' }
+ %span.sr-only Toggle navigation
+ = icon('bars')
+ .dropdown-menu-nav.global-dropdown-menu
+ - if current_user
+ = render 'layouts/nav/dashboard'
+ - else
+ = render 'layouts/nav/explore'
%button.navbar-toggle{ type: 'button' }
%span.sr-only Toggle navigation
= icon('ellipsis-v')
@@ -29,7 +35,11 @@
= link_to dashboard_todos_path, title: 'Todos', aria: { label: "Todos" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= icon('bell fw')
%span.badge.todos-pending-count{ class: ("hidden" if todos_pending_count == 0) }
- = todos_pending_count
+ = todos_count_format(todos_pending_count)
+ - if current_user.can_create_project?
+ %li
+ = link_to new_project_path, title: 'New project', aria: { label: "New project" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
+ = icon('plus fw')
- if Gitlab::Sherlock.enabled?
%li
= link_to sherlock_transactions_path, title: 'Sherlock Transactions',
@@ -45,8 +55,6 @@
= link_to "Profile", current_user, class: 'profile-link', aria: { label: "Profile" }, data: { user: current_user.username }
%li
= link_to "Settings", profile_path, aria: { label: "Settings" }
- %li
- = link_to "Help", help_path, aria: { label: "Help" }
%li.divider
%li
= link_to "Sign out", destroy_user_session_path, method: :delete, class: "sign-out-link", aria: { label: "Sign out" }
@@ -55,13 +63,12 @@
%div
= link_to "Sign in", new_session_path(:user, redirect_to_referer: 'yes'), class: 'btn btn-sign-in btn-success'
-
- %h1.title= title
-
.header-logo
= link_to root_path, class: 'home', title: 'Dashboard', id: 'logo' do
= brand_header_logo
+ %h1.title= title
+
= yield :header_content
= render 'shared/outdated_browser'
diff --git a/app/views/layouts/mailer.html.haml b/app/views/layouts/mailer.html.haml
new file mode 100644
index 00000000000..53268cc22f8
--- /dev/null
+++ b/app/views/layouts/mailer.html.haml
@@ -0,0 +1,72 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional //EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+%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" }/
+ %title= message.subject
+ :css
+ /* CLIENT-SPECIFIC STYLES */
+ body, table, td, a { -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; }
+ table, td { mso-table-lspace: 0pt; mso-table-rspace: 0pt; }
+ img { -ms-interpolation-mode: bicubic; }
+
+ /* iOS BLUE LINKS */
+ a[x-apple-data-detectors] {
+ color: inherit !important;
+ text-decoration: none !important;
+ font-size: inherit !important;
+ font-family: inherit !important;
+ font-weight: inherit !important;
+ line-height: inherit !important;
+ }
+
+ /* ANDROID MARGIN HACK */
+ body { margin:0 !important; }
+ div[style*="margin: 16px 0"] { margin:0 !important; }
+
+ @media only screen and (max-width: 639px) {
+ body, #body {
+ min-width: 320px !important;
+ }
+ table.wrapper {
+ width: 100% !important;
+ min-width: 320px !important;
+ }
+ table.wrapper > tbody > tr > td {
+ border-left: 0 !important;
+ border-right: 0 !important;
+ border-radius: 0 !important;
+ padding-left: 10px !important;
+ 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%;" }
+ %tbody
+ %tr.line
+ %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;" }
+ = header_logo
+ %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;" }
+ %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;" }
+ %tbody
+ = yield
+
+ %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/gitlab_footer_logo.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
+ &middot;
+ %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
diff --git a/app/views/layouts/mailer.text.haml b/app/views/layouts/mailer.text.haml
new file mode 100644
index 00000000000..6a9c6ced9cc
--- /dev/null
+++ b/app/views/layouts/mailer.text.haml
@@ -0,0 +1,5 @@
+= yield
+
+You're receiving this email because of your account on #{Gitlab.config.gitlab.host}.
+Manage all notifications: #{profile_notifications_url}
+Help: #{help_url}
diff --git a/app/views/layouts/nav/_admin.html.haml b/app/views/layouts/nav/_admin.html.haml
index 19a947af4ca..d068c895fa3 100644
--- a/app/views/layouts/nav/_admin.html.haml
+++ b/app/views/layouts/nav/_admin.html.haml
@@ -33,7 +33,7 @@
Abuse Reports
%span.badge.count= number_with_delimiter(AbuseReport.count(:all))
- - if askimet_enabled?
+ - if akismet_enabled?
= nav_link(controller: :spam_logs) do
= link_to admin_spam_logs_path, title: "Spam Logs" do
%span
diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml
index 205d23178d2..4c9749205de 100644
--- a/app/views/layouts/nav/_dashboard.html.haml
+++ b/app/views/layouts/nav/_dashboard.html.haml
@@ -1,41 +1,39 @@
-.nav-sidebar
- %ul.nav
- = nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: {class: "#{project_tab_class} home"}) do
- = link_to dashboard_projects_path, title: 'Projects', class: 'dashboard-shortcuts-projects' do
- %span
- Projects
- = nav_link(path: 'dashboard#activity') do
- = link_to activity_dashboard_path, class: 'dashboard-shortcuts-activity', title: 'Activity' do
- %span
- Activity
- - if koding_enabled?
- = nav_link(controller: :koding) do
- = link_to koding_path, title: 'Koding' do
- %span
- Koding
- = nav_link(controller: [:groups, 'groups/milestones', 'groups/group_members']) do
- = link_to dashboard_groups_path, title: 'Groups' do
- %span
- Groups
- = nav_link(controller: 'dashboard/milestones') do
- = link_to dashboard_milestones_path, title: 'Milestones' do
- %span
- Milestones
- = nav_link(path: 'dashboard#issues') do
- = link_to assigned_issues_dashboard_path, title: 'Issues', class: 'dashboard-shortcuts-issues' do
- %span
- Issues
- %span.count= number_with_delimiter(cached_assigned_issuables_count(current_user, :issues, :opened))
- = nav_link(path: 'dashboard#merge_requests') do
- = link_to assigned_mrs_dashboard_path, title: 'Merge Requests', class: 'dashboard-shortcuts-merge_requests' do
- %span
- Merge Requests
- %span.count= number_with_delimiter(cached_assigned_issuables_count(current_user, :merge_requests, :opened))
- = nav_link(controller: 'dashboard/snippets') do
- = link_to dashboard_snippets_path, title: 'Snippets' do
+%ul
+ = nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: {class: "#{project_tab_class} home"}) do
+ = link_to dashboard_projects_path, title: 'Projects', class: 'dashboard-shortcuts-projects' do
+ %span
+ Projects
+ = nav_link(path: 'dashboard#activity') do
+ = link_to activity_dashboard_path, class: 'dashboard-shortcuts-activity', title: 'Activity' do
+ %span
+ Activity
+ - if koding_enabled?
+ = nav_link(controller: :koding) do
+ = link_to koding_path, title: 'Koding' do
%span
- Snippets
-
- = link_to help_path, title: 'About GitLab CE', class: 'about-gitlab' do
+ Koding
+ = nav_link(controller: [:groups, 'groups/milestones', 'groups/group_members']) do
+ = link_to dashboard_groups_path, title: 'Groups' do
+ %span
+ Groups
+ = nav_link(controller: 'dashboard/milestones') do
+ = link_to dashboard_milestones_path, title: 'Milestones' do
+ %span
+ Milestones
+ = nav_link(path: 'dashboard#issues') do
+ = link_to assigned_issues_dashboard_path, title: 'Issues', class: 'dashboard-shortcuts-issues' do
+ %span
+ Issues
+ (#{number_with_delimiter(cached_assigned_issuables_count(current_user, :issues, :opened))})
+ = nav_link(path: 'dashboard#merge_requests') do
+ = link_to assigned_mrs_dashboard_path, title: 'Merge Requests', class: 'dashboard-shortcuts-merge_requests' do
+ %span
+ Merge Requests
+ (#{number_with_delimiter(cached_assigned_issuables_count(current_user, :merge_requests, :opened))})
+ = nav_link(controller: 'dashboard/snippets') do
+ = link_to dashboard_snippets_path, title: 'Snippets' do
%span
- About GitLab CE
+ Snippets
+ %li.divider
+ %li
+ = link_to "Help", help_path, title: 'About GitLab CE', class: 'about-gitlab'
diff --git a/app/views/layouts/nav/_explore.html.haml b/app/views/layouts/nav/_explore.html.haml
index e5bda7b3a6f..3a1fcd00e9c 100644
--- a/app/views/layouts/nav/_explore.html.haml
+++ b/app/views/layouts/nav/_explore.html.haml
@@ -1,4 +1,4 @@
-%ul.nav.nav-sidebar
+%ul
= nav_link(path: ['dashboard#show', 'root#show', 'projects#trending', 'projects#starred', 'projects#index'], html_options: {class: 'home'}) do
= link_to explore_root_path, title: 'Projects' do
%span
diff --git a/app/views/layouts/nav/_group.html.haml b/app/views/layouts/nav/_group.html.haml
index f3539fd372d..a6e96942021 100644
--- a/app/views/layouts/nav/_group.html.haml
+++ b/app/views/layouts/nav/_group.html.haml
@@ -5,23 +5,11 @@
.fade-right
= icon('angle-right')
%ul.nav-links.scrolling-tabs
- = nav_link(path: 'groups#show', html_options: {class: 'home'}) do
+ = nav_link(path: ['groups#show', 'groups#activity', 'groups#subgroups'], html_options: { class: 'home' }) do
= link_to group_path(@group), title: 'Home' do
%span
Group
- = nav_link(path: 'groups#activity') do
- = link_to activity_group_path(@group), title: 'Activity' do
- %span
- Activity
- = nav_link(controller: [:group, :labels]) do
- = link_to group_labels_path(@group), title: 'Labels' do
- %span
- Labels
- = nav_link(controller: [:group, :milestones]) do
- = link_to group_milestones_path(@group), title: 'Milestones' do
- %span
- Milestones
- = nav_link(path: 'groups#issues') do
+ = nav_link(path: ['groups#issues', 'labels#index', 'milestones#index']) do
= link_to issues_group_path(@group), title: 'Issues' do
%span
Issues
@@ -33,7 +21,7 @@
Merge Requests
- merge_requests = MergeRequestsFinder.new(current_user, group_id: @group.id, state: 'opened', non_archived: true).execute
%span.badge.count= number_with_delimiter(merge_requests.count)
- = nav_link(controller: [:group_members]) do
+ = nav_link(path: 'group_members#index') do
= link_to group_group_members_path(@group), title: 'Members' do
%span
Members
diff --git a/app/views/notify/pipeline_failed_email.html.haml b/app/views/notify/pipeline_failed_email.html.haml
index d9ebbaa2704..85a1aea3a61 100644
--- a/app/views/notify/pipeline_failed_email.html.haml
+++ b/app/views/notify/pipeline_failed_email.html.haml
@@ -1,179 +1,109 @@
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional //EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
-%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" }/
- %title= message.subject
- :css
- /* CLIENT-SPECIFIC STYLES */
- body, table, td, a { -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; }
- table, td { mso-table-lspace: 0pt; mso-table-rspace: 0pt; }
- img { -ms-interpolation-mode: bicubic; }
-
- /* iOS BLUE LINKS */
- a[x-apple-data-detectors] {
- color: inherit !important;
- text-decoration: none !important;
- font-size: inherit !important;
- font-family: inherit !important;
- font-weight: inherit !important;
- line-height: inherit !important;
- }
-
- /* ANDROID MARGIN HACK */
- body { margin:0 !important; }
- div[style*="margin: 16px 0"] { margin:0 !important; }
-
- @media only screen and (max-width: 639px) {
- body, #body {
- min-width: 320px !important;
- }
- table.wrapper {
- width: 100% !important;
- min-width: 320px !important;
- }
- table.wrapper > tbody > tr > td {
- border-left: 0 !important;
- border-right: 0 !important;
- border-radius: 0 !important;
- padding-left: 10px !important;
- 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%;" }
+%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;" }
%tbody
- %tr.line
- %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" }/
%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;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;" }
+ &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%;" }
+ %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;" }
+ - 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;" }
+ = namespace_name
+ \/
+ %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;" }
+ %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", 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;" }
%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;" }
- %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;" }
- %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;" }
- Your pipeline has failed.
- %tr.spacer
- %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%;" }
- %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;" }
- - 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;" }
- = namespace_name
- \/
- %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;" }
- %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", 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;" }
- %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", 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;" }
- = @merge_request.to_reference
- .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;" }
- %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", 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;" }
- = 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;" }
- &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;" }
- Pipeline
- %a{ href: pipeline_url(@pipeline), style: "color:#3777b0;text-decoration:none;" }
- = "\##{@pipeline.id}"
- had
- = failed.size
- failed
- #{'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;" }
- 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;" }
- %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;" }
- %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;" }
- = 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;" }
- = 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;" }
- = build.trace_html(last_lines: 10).html_safe
- - else
- %td{ colspan: "2" }
- %tr.footer
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" }
- %img{ alt: "GitLab", height: "33", src: image_url('mailers/ci_pipeline_notif_v1/gitlab-logo-full-horizontal.gif'), style: "display:block;margin:0 auto 1em;", width: "90" }/
- %div
- %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
- %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
+ %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;" }
+ = @merge_request.to_reference
+ .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;" }
+ %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", 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;" }
+ = 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;" }
+ &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;" }
+ Pipeline
+ %a{ href: pipeline_url(@pipeline), style: "color:#3777b0;text-decoration:none;" }
+ = "\##{@pipeline.id}"
+ had
+ = failed.size
+ failed
+ #{'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;" }
+ 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;" }
+ %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;" }
+ %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;" }
+ = 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;" }
+ = 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;" }
+ = build.trace_html(last_lines: 10).html_safe
+ - else
+ %td{ colspan: "2" }
diff --git a/app/views/notify/pipeline_failed_email.text.erb b/app/views/notify/pipeline_failed_email.text.erb
index ab91c7ef350..520a2fc7d68 100644
--- a/app/views/notify/pipeline_failed_email.text.erb
+++ b/app/views/notify/pipeline_failed_email.text.erb
@@ -27,7 +27,3 @@ Trace: <%= build.trace_with_state(last_lines: 10)[:text] %>
<% end -%>
<% end -%>
-
-You're receiving this email because of your account on <%= Gitlab.config.gitlab.host %>.
-Manage all notifications: <%= profile_notifications_url %>
-Help: <%= help_url %>
diff --git a/app/views/notify/pipeline_success_email.html.haml b/app/views/notify/pipeline_success_email.html.haml
index 8add2e18206..19d4add06f5 100644
--- a/app/views/notify/pipeline_success_email.html.haml
+++ b/app/views/notify/pipeline_success_email.html.haml
@@ -1,154 +1,84 @@
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional //EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
-%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" }/
- %title= message.subject
- :css
- /* CLIENT-SPECIFIC STYLES */
- body, table, td, a { -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; }
- table, td { mso-table-lspace: 0pt; mso-table-rspace: 0pt; }
- img { -ms-interpolation-mode: bicubic; }
-
- /* iOS BLUE LINKS */
- a[x-apple-data-detectors] {
- color: inherit !important;
- text-decoration: none !important;
- font-size: inherit !important;
- font-family: inherit !important;
- font-weight: inherit !important;
- line-height: inherit !important;
- }
-
- /* ANDROID MARGIN HACK */
- body { margin:0 !important; }
- div[style*="margin: 16px 0"] { margin:0 !important; }
-
- @media only screen and (max-width: 639px) {
- body, #body {
- min-width: 320px !important;
- }
- table.wrapper {
- width: 100% !important;
- min-width: 320px !important;
- }
- table.wrapper > tbody > tr > td {
- border-left: 0 !important;
- border-right: 0 !important;
- border-radius: 0 !important;
- padding-left: 10px !important;
- 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%;" }
+%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;" }
%tbody
- %tr.line
- %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" }/
%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;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;" }
+ &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%;" }
+ %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;" }
+ - 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;" }
+ = namespace_name
+ \/
+ %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;" }
+ %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", 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;" }
+ %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", 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;" }
+ = @merge_request.to_reference
+ .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;" }
%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;" }
- %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;" }
- %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;" }
- Your pipeline has passed.
- %tr.spacer
- %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%;" }
- %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;" }
- - 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;" }
- = namespace_name
- \/
- %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;" }
- %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", 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;" }
- %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", 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;" }
- = @merge_request.to_reference
- .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;" }
- %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", 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;" }
- = 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;" }
- &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;" }
- - build_count = @pipeline.statuses.latest.size
- - stage_count = @pipeline.stages_count
- Pipeline
- %a{ href: pipeline_url(@pipeline), style: "color:#3777b0;text-decoration:none;" }
- = "\##{@pipeline.id}"
- successfully completed
- #{build_count} #{'build'.pluralize(build_count)}
- in
- #{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" }/
- %div
- %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
- %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
+ - 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", 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;" }
+ = 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;" }
+ &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;" }
+ - build_count = @pipeline.statuses.latest.size
+ - stage_count = @pipeline.stages_count
+ Pipeline
+ %a{ href: pipeline_url(@pipeline), style: "color:#3777b0;text-decoration:none;" }
+ = "\##{@pipeline.id}"
+ successfully completed
+ #{build_count} #{'build'.pluralize(build_count)}
+ in
+ #{stage_count} #{'stage'.pluralize(stage_count)}.
diff --git a/app/views/notify/pipeline_success_email.text.erb b/app/views/notify/pipeline_success_email.text.erb
index 40e5e306426..0970a3a4e09 100644
--- a/app/views/notify/pipeline_success_email.text.erb
+++ b/app/views/notify/pipeline_success_email.text.erb
@@ -18,7 +18,3 @@ Commit Author: <%= commit.author_name %>
<% build_count = @pipeline.statuses.latest.size -%>
<% stage_count = @pipeline.stages_count -%>
Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) successfully completed <%= build_count %> <%= 'build'.pluralize(build_count) %> in <%= stage_count %> <%= 'stage'.pluralize(stage_count) %>.
-
-You're receiving this email because of your account on <%= Gitlab.config.gitlab.host %>.
-Manage all notifications: <%= profile_notifications_url %>
-Help: <%= help_url %>
diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml
index a4f4079d556..02fb47ec981 100644
--- a/app/views/profiles/accounts/show.html.haml
+++ b/app/views/profiles/accounts/show.html.haml
@@ -115,7 +115,7 @@
%h4.prepend-top-0.danger-title
Remove account
.col-lg-9
- - if @user.can_be_removed?
+ - if @user.can_be_removed? && can?(current_user, :destroy_user, @user)
%p
Deleting an account has the following effects:
%ul
@@ -131,4 +131,7 @@
%strong= @user.solo_owned_groups.map(&:name).join(', ')
%p
You must transfer ownership or delete these groups before you can delete your account.
+ - else
+ %p
+ You don't have access to delete this user.
.append-bottom-default
diff --git a/app/views/profiles/personal_access_tokens/index.html.haml b/app/views/profiles/personal_access_tokens/index.html.haml
index b10f5fc08e2..903b957c26b 100644
--- a/app/views/profiles/personal_access_tokens/index.html.haml
+++ b/app/views/profiles/personal_access_tokens/index.html.haml
@@ -101,5 +101,3 @@
$("#created-personal-access-token").click(function() {
this.select();
});
-
- $("#created-personal-access-token").effect('highlight', { color: '#ffff99' }, 2000);
diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml
index feadd863b00..df0a0212f3d 100644
--- a/app/views/profiles/preferences/show.html.haml
+++ b/app/views/profiles/preferences/show.html.haml
@@ -4,19 +4,6 @@
= 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
- %p
- This setting allows you to customize the appearance of the site, e.g. the sidebar.
- .col-lg-9.application-theme
- - Gitlab::Themes.each do |theme|
- = label_tag do
- .preview{ class: theme.css_class }
- = f.radio_button :theme_id, theme.id
- = theme.name
- .col-sm-12
- %hr
- .col-lg-3.profile-settings-sidebar
- %h4.prepend-top-0
Syntax highlighting theme
%p
This setting allow you to customize the appearance of the syntax.
diff --git a/app/views/profiles/preferences/update.js.erb b/app/views/profiles/preferences/update.js.erb
index 8966dd3fd86..431ab9d052b 100644
--- a/app/views/profiles/preferences/update.js.erb
+++ b/app/views/profiles/preferences/update.js.erb
@@ -1,7 +1,3 @@
-// Remove body class for any previous theme, re-add current one
-$('body').removeClass('<%= Gitlab::Themes.body_classes %>')
-$('body').addClass('<%= user_application_theme %>')
-
// Toggle container-fluid class
if ('<%= current_user.layout %>' === 'fluid') {
$('.content-wrapper .container-fluid').removeClass('container-limited')
diff --git a/app/views/projects/_activity.html.haml b/app/views/projects/_activity.html.haml
index 0ea733cb978..4268337fd6d 100644
--- a/app/views/projects/_activity.html.haml
+++ b/app/views/projects/_activity.html.haml
@@ -4,7 +4,7 @@
.nav-block.activity-filter-block
- if current_user
.controls
- = link_to namespace_project_path(@project.namespace, @project, format: :atom, private_token: current_user.private_token), title: "Feed", class: 'btn rss-btn' do
+ = link_to namespace_project_path(@project.namespace, @project, format: :atom, private_token: current_user.private_token), title: "Subscribe", class: 'btn rss-btn has-tooltip' do
= icon('rss')
= render 'shared/event_filter'
diff --git a/app/views/projects/blob/diff.html.haml b/app/views/projects/blob/diff.html.haml
index 3b1a2e54ec2..c48d9dd982c 100644
--- a/app/views/projects/blob/diff.html.haml
+++ b/app/views/projects/blob/diff.html.haml
@@ -19,12 +19,12 @@
= line_content
- when :parallel
%td.old_line.diff-line-num{ data: { linenumber: line_old } }
- = link_to raw(line_old), "##{line_old}"
+ %a{ href: "##{line_old}", data: { linenumber: line_old } }
= line_content
%td.new_line.diff-line-num{ data: { linenumber: line_new } }
- = link_to raw(line_new), "##{line_new}"
+ %a{ href: "##{line_new}", data: { linenumber: line_new } }
= line_content
- - if @form.unfold? && @form.bottom? && @form.to < @blob.loc
+ - if @form.unfold? && @form.bottom? && @form.to < @blob.lines.size
%tr.line_holder{ id: @form.to, class: line_class }
= diff_match_line @form.to - @form.offset, @form.to, text: @match_line, view: diff_view, bottom: true
diff --git a/app/views/projects/boards/_show.html.haml b/app/views/projects/boards/_show.html.haml
index f5ca9607823..b3bc6010efb 100644
--- a/app/views/projects/boards/_show.html.haml
+++ b/app/views/projects/boards/_show.html.haml
@@ -8,7 +8,6 @@
%script#js-board-template{ type: "text/x-template" }= render "projects/boards/components/board"
%script#js-board-list-template{ type: "text/x-template" }= render "projects/boards/components/board_list"
- %script#js-board-list-card{ type: "text/x-template" }= render "projects/boards/components/card"
= render "projects/issues/head"
diff --git a/app/views/projects/boards/components/_card.html.haml b/app/views/projects/boards/components/_card.html.haml
deleted file mode 100644
index 891c2c46251..00000000000
--- a/app/views/projects/boards/components/_card.html.haml
+++ /dev/null
@@ -1,10 +0,0 @@
-%li.card{ ":class" => '{ "user-can-drag": !disabled && issue.id, "is-disabled": disabled || !issue.id, "is-active": issueDetailVisible }',
- ":index" => "index",
- ":data-issue-id" => "issue.id",
- "@mousedown" => "mouseDown",
- "@mousemove" => "mouseMove",
- "@mouseup" => "showIssue($event)" }
- %issue-card-inner{ ":list" => "list",
- ":issue" => "issue",
- ":issue-link-base" => "issueLinkBase",
- ":root-path" => "rootPath" }
diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml
index 19ffe73a08d..ae63f8184df 100644
--- a/app/views/projects/branches/_branch.html.haml
+++ b/app/views/projects/branches/_branch.html.haml
@@ -31,7 +31,7 @@
- if can?(current_user, :push_code, @project)
= link_to namespace_project_branch_path(@project.namespace, @project, branch.name),
- class: "btn btn-remove remove-row #{can_remove_branch?(@project, branch.name) ? '' : 'disabled'}",
+ class: "btn btn-remove remove-row js-ajax-loading-spinner #{can_remove_branch?(@project, branch.name) ? '' : 'disabled'}",
method: :delete,
data: { confirm: "Deleting the '#{branch.name}' branch cannot be undone. Are you sure?" },
remote: true,
diff --git a/app/views/projects/branches/new.html.haml b/app/views/projects/branches/new.html.haml
index e63bdb38bd8..d3c3e40d518 100644
--- a/app/views/projects/branches/new.html.haml
+++ b/app/views/projects/branches/new.html.haml
@@ -12,12 +12,16 @@
.form-group
= label_tag :branch_name, nil, class: 'control-label'
.col-sm-10
- = text_field_tag :branch_name, params[:branch_name], required: true, tabindex: 1, autofocus: true, class: 'form-control js-branch-name'
+ = text_field_tag :branch_name, params[:branch_name], required: true, autofocus: true, class: 'form-control js-branch-name'
.help-block.text-danger.js-branch-name-error
.form-group
= label_tag :ref, 'Create from', class: 'control-label'
.col-sm-10
- = text_field_tag :ref, params[:ref] || @project.default_branch, required: true, tabindex: 2, class: 'form-control'
+ = hidden_field_tag :ref, params[:ref] || @project.default_branch
+ = dropdown_tag(params[:ref] || @project.default_branch,
+ options: { toggle_class: 'js-branch-select wide',
+ filter: true, dropdown_class: "dropdown-menu-selectable", placeholder: "Search branches",
+ data: { selected: params[:ref] || @project.default_branch, field_name: 'ref' } })
.help-block Existing branch name, tag, or commit SHA
.form-actions
= button_tag 'Create branch', class: 'btn btn-create', tabindex: 3
diff --git a/app/views/projects/builds/_header.html.haml b/app/views/projects/builds/_header.html.haml
index 27e81c2bec3..7eb17e887e7 100644
--- a/app/views/projects/builds/_header.html.haml
+++ b/app/views/projects/builds/_header.html.haml
@@ -1,4 +1,4 @@
-.content-block.build-header
+.content-block.build-header.top-area
.header-content
= render 'ci/status/badge', status: @build.detailed_status(current_user), link: false
Job
@@ -16,7 +16,10 @@
- if @build.user
= render "user"
= time_ago_with_tooltip(@build.created_at)
- - if can?(current_user, :update_build, @build) && @build.retryable?
- = link_to "Retry job", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-inverted-secondary pull-right', method: :post
+ .nav-controls
+ - if can?(current_user, :create_issue, @project) && @build.failed?
+ = link_to "New issue", new_namespace_project_issue_path(@project.namespace, @project, issue: build_failed_issue_options), class: 'btn btn-new btn-inverted'
+ - if can?(current_user, :update_build, @build) && @build.retryable?
+ = link_to "Retry job", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-inverted-secondary', 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/buttons/_download.html.haml b/app/views/projects/buttons/_download.html.haml
index 762ff34a9ec..b560ed21f1d 100644
--- a/app/views/projects/buttons/_download.html.haml
+++ b/app/views/projects/buttons/_download.html.haml
@@ -8,19 +8,19 @@
%ul.dropdown-menu.dropdown-menu-align-right{ role: 'menu' }
%li.dropdown-header Source code
%li
- = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'zip'), rel: 'nofollow' do
+ = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'zip'), rel: 'nofollow', download: '' do
%i.fa.fa-download
%span Download zip
%li
- = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar.gz'), rel: 'nofollow' do
+ = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar.gz'), rel: 'nofollow', download: '' do
%i.fa.fa-download
%span Download tar.gz
%li
- = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar.bz2'), rel: 'nofollow' do
+ = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar.bz2'), rel: 'nofollow', download: '' do
%i.fa.fa-download
%span Download tar.bz2
%li
- = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar'), rel: 'nofollow' do
+ = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar'), rel: 'nofollow', download: '' do
%i.fa.fa-download
%span Download tar
@@ -36,6 +36,6 @@
%li.dropdown-header Previous Artifacts
- artifacts.each do |job|
%li
- = link_to latest_succeeded_namespace_project_artifacts_path(project.namespace, project, "#{ref}/download", job: job.name), rel: 'nofollow' do
+ = link_to latest_succeeded_namespace_project_artifacts_path(project.namespace, project, "#{ref}/download", job: job.name), rel: 'nofollow', download: '' do
%i.fa.fa-download
%span Download '#{job.name}'
diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml
deleted file mode 100644
index f852f2e3fd7..00000000000
--- a/app/views/projects/ci/pipelines/_pipeline.html.haml
+++ /dev/null
@@ -1,92 +0,0 @@
-- status = pipeline.status
-- show_commit = local_assigns.fetch(:show_commit, true)
-- show_branch = local_assigns.fetch(:show_branch, true)
-
-%tr.commit
- %td.commit-link
- = render 'ci/status/badge', status: pipeline.detailed_status(current_user)
-
- %td
- = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id) do
- %span.pipeline-id ##{pipeline.id}
- %span by
- - if pipeline.user
- = user_avatar(user: pipeline.user, size: 20)
- - else
- %span.api.monospace API
- - if pipeline.latest?
- %span.label.label-success.has-tooltip{ title: 'Latest pipeline for this branch' } latest
- - if pipeline.triggered?
- %span.label.label-primary triggered
- - if pipeline.yaml_errors.present?
- %span.label.label-danger.has-tooltip{ title: "#{pipeline.yaml_errors}" } yaml invalid
- - if pipeline.builds.any?(&:stuck?)
- %span.label.label-warning stuck
-
- %td.branch-commit
- - if pipeline.ref && show_branch
- .icon-container
- = pipeline.tag? ? icon('tag') : icon('code-fork')
- = link_to pipeline.ref, namespace_project_commits_path(pipeline.project.namespace, pipeline.project, pipeline.ref), class: "monospace branch-name"
- - if show_commit
- .icon-container.commit-icon
- = custom_icon("icon_commit")
- = link_to pipeline.short_sha, namespace_project_commit_path(pipeline.project.namespace, pipeline.project, pipeline.sha), class: "commit-id monospace"
-
- %p.commit-title
- - if commit = pipeline.commit
- = author_avatar(commit, size: 20)
- = link_to_gfm truncate(commit.title, length: 60, escape: false), namespace_project_commit_path(pipeline.project.namespace, pipeline.project, commit.id), class: "commit-row-message"
- - else
- Cant find HEAD commit for this branch
-
- %td
- = render 'shared/mini_pipeline_graph', pipeline: pipeline, klass: 'js-mini-pipeline-graph'
-
- %td
- - if pipeline.duration
- %p.duration
- = custom_icon("icon_timer")
- = duration_in_numbers(pipeline.duration)
- - if pipeline.finished_at
- %p.finished-at
- = icon("calendar")
- #{time_ago_with_tooltip(pipeline.finished_at, short_format: false)}
-
- %td.pipeline-actions.hidden-xs
- .controls.pull-right
- - artifacts = pipeline.builds.latest.with_artifacts_not_expired
- - actions = pipeline.manual_actions
- - if artifacts.present? || actions.any?
- .btn-group.inline
- - if actions.any?
- .btn-group
- %button.dropdown-toggle.btn.btn-default.has-tooltip.js-pipeline-dropdown-manual-actions{ type: 'button', title: 'Manual pipeline', data: { toggle: 'dropdown', placement: 'top' }, 'aria-label' => 'Manual pipeline' }
- = custom_icon('icon_play')
- = icon('caret-down', 'aria-hidden' => 'true')
- %ul.dropdown-menu.dropdown-menu-align-right
- - 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
- - if artifacts.present?
- .btn-group
- %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
- - artifacts.each do |build|
- %li
- = link_to download_namespace_project_build_artifacts_path(pipeline.project.namespace, pipeline.project, build), rel: 'nofollow' do
- = icon("download")
- %span Download '#{build.name}' artifacts
-
- - if can?(current_user, :update_pipeline, 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', 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', data: { toggle: 'dropdown', placement: 'top' }, 'aria-label' => 'Cancel' , method: :post do
- = icon("remove")
diff --git a/app/views/projects/commit/_pipeline.html.haml b/app/views/projects/commit/_pipeline.html.haml
index 6abff6aaf95..c2b32a22170 100644
--- a/app/views/projects/commit/_pipeline.html.haml
+++ b/app/views/projects/commit/_pipeline.html.haml
@@ -3,7 +3,7 @@
.pull-right
- if can?(current_user, :update_pipeline, pipeline.project)
- if pipeline.builds.latest.failed.any?(&:retryable?)
- = link_to "Retry failed", retry_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: 'btn btn-grouped btn-primary', method: :post
+ = link_to "Retry", retry_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: 'js-retry-button btn btn-grouped btn-primary', method: :post
- if pipeline.builds.running_or_pending.any?
= link_to "Cancel running", cancel_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), data: { confirm: 'Are you sure?' }, class: 'btn btn-grouped btn-danger', method: :post
diff --git a/app/views/projects/diffs/_content.html.haml b/app/views/projects/diffs/_content.html.haml
index b87b79b170e..5c38b5ad9c0 100644
--- a/app/views/projects/diffs/_content.html.haml
+++ b/app/views/projects/diffs/_content.html.haml
@@ -15,10 +15,13 @@
%a.click-to-expand
Click to expand it.
- elsif diff_file.diff_lines.length > 0
+ - total_lines = 0
+ - if blob.lines.any?
+ - total_lines = blob.lines.last.chomp == '' ? blob.lines.size - 1 : blob.lines.size
- if diff_view == :parallel
- = render "projects/diffs/parallel_view", diff_file: diff_file, project: project, blob: blob
+ = render "projects/diffs/parallel_view", diff_file: diff_file, total_lines: total_lines
- else
- = render "projects/diffs/text_file", diff_file: diff_file
+ = render "projects/diffs/text_file", diff_file: diff_file, total_lines: total_lines
- else
- if diff_file.mode_changed?
.nothing-here-block File mode changed
diff --git a/app/views/projects/diffs/_file_header.html.haml b/app/views/projects/diffs/_file_header.html.haml
index 1dbfe830d52..f809c52c367 100644
--- a/app/views/projects/diffs/_file_header.html.haml
+++ b/app/views/projects/diffs/_file_header.html.haml
@@ -10,10 +10,10 @@
- if diff_file.renamed_file
- old_path, new_path = mark_inline_diffs(diff_file.old_path, diff_file.new_path)
- %strong.file-title-name.has-tooltip{ data: { title: old_path, container: 'body' } }
+ %strong.file-title-name.has-tooltip{ data: { title: diff_file.old_path, container: 'body' } }
= old_path
&rarr;
- %strong.file-title-name.has-tooltip{ data: { title: new_path, container: 'body' } }
+ %strong.file-title-name.has-tooltip{ data: { title: diff_file.new_path, container: 'body' } }
= new_path
- else
%strong.file-title-name.has-tooltip{ data: { title: diff_file.new_path, container: 'body' } }
diff --git a/app/views/projects/diffs/_parallel_view.html.haml b/app/views/projects/diffs/_parallel_view.html.haml
index 074f1f634ae..997bf0fc560 100644
--- a/app/views/projects/diffs/_parallel_view.html.haml
+++ b/app/views/projects/diffs/_parallel_view.html.haml
@@ -43,7 +43,8 @@
- 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 && diff_file.diff_lines.any?
+ - if !diff_file.new_file && !diff_file.deleted_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
+ - if last_line.new_pos < total_lines
+ %tr.line_holder.parallel
+ = diff_match_line last_line.old_pos, last_line.new_pos, bottom: true, view: :parallel
diff --git a/app/views/projects/diffs/_text_file.html.haml b/app/views/projects/diffs/_text_file.html.haml
index 2eea1db169a..ebd1a914ee7 100644
--- a/app/views/projects/diffs/_text_file.html.haml
+++ b/app/views/projects/diffs/_text_file.html.haml
@@ -10,7 +10,8 @@
as: :line,
locals: { diff_file: diff_file, discussions: discussions }
- - if !diff_file.new_file && diff_file.highlighted_diff_lines.any?
+ - if !diff_file.new_file && !diff_file.deleted_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
+ - if last_line.new_pos < total_lines
+ %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 b9300efd04f..83ae9fd10ec 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -120,7 +120,7 @@
.form-group
- if @project.avatar?
.avatar-container.s160
- = project_icon("#{@project.namespace.to_param}/#{@project.to_param}", alt: '', class: 'avatar project-avatar s160')
+ = project_icon(@project.full_path, alt: '', class: 'avatar project-avatar s160')
%p.light
- if @project.avatar_in_git
Project avatar in repository: #{ @project.avatar_in_git }
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index d3eb3b7055b..069f3d97943 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -40,7 +40,7 @@
= 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?
+ - if @issue.submittable_as_spam_by?(current_user)
%li
= link_to 'Submit as spam', mark_as_spam_namespace_project_issue_path(@project.namespace, @project, @issue), method: :post, class: 'btn-spam', title: 'Submit as spam'
@@ -50,7 +50,7 @@
- if can?(current_user, :update_issue, @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?
+ - if @issue.submittable_as_spam_by?(current_user)
= 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'
diff --git a/app/views/projects/issues/verify.html.haml b/app/views/projects/issues/verify.html.haml
index 1934b18c086..09aa401e44a 100644
--- a/app/views/projects/issues/verify.html.haml
+++ b/app/views/projects/issues/verify.html.haml
@@ -1,20 +1,4 @@
-- page_title "Anti-spam verification"
+- form = [@project.namespace.becomes(Namespace), @project, @issue]
-%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'
+= render layout: 'layouts/recaptcha_verification', locals: { spammable: @issue, form: form } do
+ = hidden_field_tag(:merge_request_for_resolving_discussions, params[:merge_request_for_resolving_discussions])
diff --git a/app/views/projects/merge_requests/index.html.haml b/app/views/projects/merge_requests/index.html.haml
index 144b3a9c8c8..83e6c026ba7 100644
--- a/app/views/projects/merge_requests/index.html.haml
+++ b/app/views/projects/merge_requests/index.html.haml
@@ -5,18 +5,19 @@
= render "projects/issues/head"
= render 'projects/last_push'
+- content_for :page_specific_javascripts do
+ = page_specific_javascript_bundle_tag('filtered_search')
+
%div{ class: container_class }
.top-area
= render 'shared/issuable/nav', type: :merge_requests
.nav-controls
- = render 'shared/issuable/search_form', path: namespace_project_merge_requests_path(@project.namespace, @project)
-
- merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project))
- if merge_project
= link_to new_namespace_project_merge_request_path(merge_project.namespace, merge_project), class: "btn btn-new", title: "New Merge Request" do
New Merge Request
- = render 'shared/issuable/filter', type: :merge_requests
+ = render 'shared/issuable/search_bar', type: :merge_requests
.merge-requests-holder
= render 'merge_requests'
diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml
index e3062f47788..c676953f729 100644
--- a/app/views/projects/merge_requests/widget/_heading.html.haml
+++ b/app/views/projects/merge_requests/widget/_heading.html.haml
@@ -28,7 +28,7 @@
%span
CI job
= ci_label_for_status(status)
- for
+ for
- commit = @merge_request.diff_head_commit
= succeed "." do
= link_to commit.short_id, namespace_project_commit_path(@merge_request.source_project.namespace, @merge_request.source_project, commit), class: "monospace"
diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml
index 06a31698ee6..a216d59bc74 100644
--- a/app/views/projects/milestones/show.html.haml
+++ b/app/views/projects/milestones/show.html.haml
@@ -19,10 +19,9 @@
Open
.header-text-content
%span.identifier
- Milestone ##{@milestone.iid}
+ %strong
+ Milestone %#{@milestone.iid}
- if @milestone.due_date || @milestone.start_date
- %span.creator
- &middot;
= milestone_date_range(@milestone)
.milestone-buttons
- if can?(current_user, :admin_milestone, @project)
diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml
index a07885537b9..2a98bba05ee 100644
--- a/app/views/projects/new.html.haml
+++ b/app/views/projects/new.html.haml
@@ -22,7 +22,7 @@
- if current_user.can_select_namespace?
.input-group-addon
= root_url
- = f.select :namespace_id, namespaces_options(params[:namespace_id] || :current_user, display_path: true), {}, {class: 'select2 js-select-namespace', tabindex: 1}
+ = f.select :namespace_id, namespaces_options(namespace_id_from(params) || :current_user, display_path: true), {}, {class: 'select2 js-select-namespace', tabindex: 1}
- else
.input-group-addon.static-namespace
diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml
index 1b08165c14c..a73e8f345e0 100644
--- a/app/views/projects/notes/_note.html.haml
+++ b/app/views/projects/notes/_note.html.haml
@@ -71,7 +71,7 @@
- if note_editable
.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
+ %textarea.hidden.js-task-list-field.original-task-list{ data: {update_url: namespace_project_note_path(@project.namespace, @project, note) } }= note.note
.note-awards
= render 'award_emoji/awards_block', awardable: note, inline: false
- if note.system
diff --git a/app/views/projects/pages/_use.html.haml b/app/views/projects/pages/_use.html.haml
index 9db46f0b1fc..e442e6e9a09 100644
--- a/app/views/projects/pages/_use.html.haml
+++ b/app/views/projects/pages/_use.html.haml
@@ -5,4 +5,6 @@
.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}.
+ GitLab by following the
+ = succeed '.' do
+ = link_to 'documentation on GitLab Pages', help_page_path('user/project/pages/index.md'), target: '_blank'
diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml
index a6cd2d83bd5..0605af4fcd3 100644
--- a/app/views/projects/pipelines/_info.html.haml
+++ b/app/views/projects/pipelines/_info.html.haml
@@ -7,9 +7,9 @@
= commit_author_link(@commit)
.header-action-buttons
- if can?(current_user, :update_pipeline, @pipeline.project)
- - if @pipeline.builds.latest.failed.any?(&:retryable?)
- = link_to "Retry failed", retry_namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), class: 'btn btn-inverted-secondary', method: :post
- - if @pipeline.builds.running_or_pending.any?
+ - if @pipeline.retryable?
+ = link_to "Retry", retry_namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), class: 'js-retry-button btn btn-inverted-secondary', method: :post
+ - if @pipeline.cancelable?
= link_to "Cancel running", cancel_namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post
- if @commit
diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml
index 81e393d7626..4147a617d95 100644
--- a/app/views/projects/pipelines/index.html.haml
+++ b/app/views/projects/pipelines/index.html.haml
@@ -5,23 +5,35 @@
%div{ class: container_class }
.top-area
%ul.nav-links
- %li{ class: ('active' if @scope.nil?) }>
+ %li.js-pipelines-tab-all{ class: active_when(@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.js-pipelines-tab-pending{ class: active_when(@scope == 'pending') }>
+ = link_to project_pipelines_path(@project, scope: :pending) do
+ Pending
+ %span.badge
+ = number_with_delimiter(@pending_count)
+
+ %li.js-pipelines-tab-running{ class: active_when(@scope == 'running') }>
= link_to project_pipelines_path(@project, scope: :running) do
Running
%span.badge.js-running-count
- = number_with_delimiter(@running_or_pending_count)
+ = number_with_delimiter(@running_count)
+
+ %li.js-pipelines-tab-finished{ class: active_when(@scope == 'finished') }>
+ = link_to project_pipelines_path(@project, scope: :finished) do
+ Finished
+ %span.badge
+ = number_with_delimiter(@finished_count)
- %li{ class: ('active' if @scope == 'branches') }>
+ %li.js-pipelines-tab-branches{ class: active_when(@scope == 'branches') }>
= link_to project_pipelines_path(@project, scope: :branches) do
Branches
- %li{ class: ('active' if @scope == 'tags') }>
+ %li.js-pipelines-tab-tags{ class: active_when(@scope == 'tags') }>
= link_to project_pipelines_path(@project, scope: :tags) do
Tags
diff --git a/app/views/projects/pipelines/new.html.haml b/app/views/projects/pipelines/new.html.haml
index 55202725b9e..14a270a3039 100644
--- a/app/views/projects/pipelines/new.html.haml
+++ b/app/views/projects/pipelines/new.html.haml
@@ -9,7 +9,11 @@
.form-group
= f.label :ref, 'Create for', class: 'control-label'
.col-sm-10
- = f.text_field :ref, required: true, tabindex: 2, class: 'form-control js-branch-name ui-autocomplete-input', autocomplete: :false, id: :ref
+ = hidden_field_tag 'pipeline[ref]', params[:ref] || @project.default_branch
+ = dropdown_tag(params[:ref] || @project.default_branch,
+ options: { toggle_class: 'js-branch-select wide',
+ filter: true, dropdown_class: "dropdown-menu-selectable", placeholder: "Search branches",
+ data: { selected: params[:ref] || @project.default_branch, field_name: 'pipeline[ref]' } })
.help-block Existing branch name, tag
.form-actions
= f.submit 'Create pipeline', class: 'btn btn-create', tabindex: 3
diff --git a/app/views/projects/pipelines_settings/_badge.html.haml b/app/views/projects/pipelines_settings/_badge.html.haml
index 22a3b884520..43bbd735059 100644
--- a/app/views/projects/pipelines_settings/_badge.html.haml
+++ b/app/views/projects/pipelines_settings/_badge.html.haml
@@ -25,3 +25,10 @@
HTML
.col-md-10.code.js-syntax-highlight
= highlight('.html', badge.to_html)
+ .row
+ %hr
+ .row
+ .col-md-2.text-center
+ AsciiDoc
+ .col-md-10.code.js-syntax-highlight
+ = highlight('.adoc', badge.to_asciidoc)
diff --git a/app/views/projects/pipelines_settings/_show.html.haml b/app/views/projects/pipelines_settings/_show.html.haml
index 8024fb8979d..132f6372e40 100644
--- a/app/views/projects/pipelines_settings/_show.html.haml
+++ b/app/views/projects/pipelines_settings/_show.html.haml
@@ -60,7 +60,7 @@
= 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'
+ = f.text_field :build_coverage_regex, class: 'form-control', placeholder: 'Regular expression'
%span.input-group-addon /
%p.help-block
A regular expression that will be used to find the test coverage
diff --git a/app/views/projects/snippets/_actions.html.haml b/app/views/projects/snippets/_actions.html.haml
index dde2e2b644d..34ee4ff1937 100644
--- a/app/views/projects/snippets/_actions.html.haml
+++ b/app/views/projects/snippets/_actions.html.haml
@@ -10,7 +10,7 @@
- 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?
+ - if @snippet.submittable_as_spam_by?(current_user)
= 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
@@ -31,6 +31,6 @@
%li
= link_to edit_namespace_project_snippet_path(@project.namespace, @project, @snippet) do
Edit
- - if @snippet.submittable_as_spam? && current_user.admin?
+ - if @snippet.submittable_as_spam_by?(current_user)
%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/verify.html.haml b/app/views/projects/snippets/verify.html.haml
new file mode 100644
index 00000000000..eb56f03b3f4
--- /dev/null
+++ b/app/views/projects/snippets/verify.html.haml
@@ -0,0 +1,4 @@
+- form = [@project.namespace.becomes(Namespace), @project, @snippet.becomes(Snippet)]
+
+= render 'layouts/recaptcha_verification', spammable: @snippet, form: form
+
diff --git a/app/views/projects/tags/index.html.haml b/app/views/projects/tags/index.html.haml
index e2f132f7742..7f9a44e565f 100644
--- a/app/views/projects/tags/index.html.haml
+++ b/app/views/projects/tags/index.html.haml
@@ -3,7 +3,7 @@
= render "projects/commits/head"
.flex-list{ class: container_class }
- .top-area.flex-row
+ .top-area.adjust
.nav-text.row-main-content
Tags give the ability to mark specific points in history as being important
diff --git a/app/views/projects/variables/_form.html.haml b/app/views/projects/variables/_form.html.haml
index a5bae83e0ce..1ae86d258af 100644
--- a/app/views/projects/variables/_form.html.haml
+++ b/app/views/projects/variables/_form.html.haml
@@ -6,5 +6,5 @@
= f.text_field :key, class: "form-control", placeholder: "PROJECT_VARIABLE", required: true
.form-group
= f.label :value, "Value", class: "label-light"
- = f.text_area :value, class: "form-control", placeholder: "PROJECT_VARIABLE", required: true
+ = f.text_area :value, class: "form-control", placeholder: "PROJECT_VARIABLE"
= f.submit btn_text, class: "btn btn-save"
diff --git a/app/views/projects/wikis/_sidebar_wiki_page.html.haml b/app/views/projects/wikis/_sidebar_wiki_page.html.haml
index eb9bd14920d..0a61d90177b 100644
--- a/app/views/projects/wikis/_sidebar_wiki_page.html.haml
+++ b/app/views/projects/wikis/_sidebar_wiki_page.html.haml
@@ -1,3 +1,3 @@
-%li{ class: params[:id] == wiki_page.slug ? 'active' : '' }
+%li{ class: active_when(params[:id] == wiki_page.slug) }
= link_to namespace_project_wiki_path(@project.namespace, @project, wiki_page) do
= wiki_page.title.capitalize
diff --git a/app/views/search/_category.html.haml b/app/views/search/_category.html.haml
index 8cbecb725b5..5afb95ac430 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_when(@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_when(@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_when(@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_when(@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_when(@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_when(@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_when(@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_when(@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_when(@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_when(@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_when(@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_when(@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_when(@scope == 'milestones') }
= link_to search_filter_path(scope: 'milestones') do
Milestones
%span.badge
diff --git a/app/views/search/_results.html.haml b/app/views/search/_results.html.haml
index 7fe2bce3e7c..22004ecacbc 100644
--- a/app/views/search/_results.html.haml
+++ b/app/views/search/_results.html.haml
@@ -11,7 +11,7 @@
.results.prepend-top-10
- if @scope == 'commits'
- %ul.list-unstyled
+ %ul.content-list.commit-list.table-list.table-wide
= render partial: "search/results/commit", collection: @search_objects
- else
.search-results
diff --git a/app/views/search/results/_snippet_blob.html.haml b/app/views/search/results/_snippet_blob.html.haml
index e977c1f1698..f84be600df8 100644
--- a/app/views/search/results/_snippet_blob.html.haml
+++ b/app/views/search/results/_snippet_blob.html.haml
@@ -12,41 +12,34 @@
%span.light= time_ago_with_tooltip(snippet.created_at)
%h4.snippet-title
- snippet_path = reliable_snippet_path(snippet)
- = link_to snippet_path do
- .file-holder
- .js-file-title.file-title
+ .file-holder
+ .js-file-title.file-title
+ = link_to snippet_path do
%i.fa.fa-file
%strong= snippet.file_name
- - if markup?(snippet.file_name)
- .file-content.wiki
+ - if markup?(snippet.file_name)
+ .file-content.wiki
+ - snippet_chunks.each do |chunk|
+ - unless chunk[:data].empty?
+ = render_markup(snippet.file_name, chunk[:data])
+ - else
+ .file-content.code
+ .nothing-here-block Empty file
+ - else
+ .file-content.code.js-syntax-highlight
+ .line-numbers
- snippet_chunks.each do |chunk|
- unless chunk[:data].empty?
- = render_markup(snippet.file_name, chunk[:data])
+ - Gitlab::Git::Util.count_lines(chunk[:data]).times do |index|
+ - offset = defined?(chunk[:start_line]) ? chunk[:start_line] : 1
+ - i = index + offset
+ = link_to snippet_path+"#L#{i}", id: "L#{i}", rel: "#L#{i}", class: "diff-line-num" do
+ %i.fa.fa-link
+ = i
+ .blob-content
+ - snippet_chunks.each do |chunk|
+ - unless chunk[:data].empty?
+ = highlight(snippet.file_name, chunk[:data], repository: nil, plain: snippet.no_highlighting?)
- else
.file-content.code
.nothing-here-block Empty file
- - else
- .file-content.code.js-syntax-highlight
- .line-numbers
- - snippet_chunks.each do |chunk|
- - unless chunk[:data].empty?
- - Gitlab::Git::Util.count_lines(chunk[:data]).times do |index|
- - offset = defined?(chunk[:start_line]) ? chunk[:start_line] : 1
- - i = index + offset
- = link_to snippet_path+"#L#{i}", id: "L#{i}", rel: "#L#{i}", class: "diff-line-num" do
- %i.fa.fa-link
- = i
- - unless snippet == snippet_chunks.last
- %a.diff-line-num
- = "."
- %pre.code
- %code
- - snippet_chunks.each do |chunk|
- - unless chunk[:data].empty?
- = chunk[:data]
- - unless chunk == snippet_chunks.last
- %a
- = "..."
- - else
- .file-content.code
- .nothing-here-block Empty file
diff --git a/app/views/shared/_group_form.html.haml b/app/views/shared/_group_form.html.haml
index efb207b9916..02b7b2447ed 100644
--- a/app/views/shared/_group_form.html.haml
+++ b/app/views/shared/_group_form.html.haml
@@ -17,7 +17,7 @@
%strong= parent.full_path + '/'
= f.text_field :path, placeholder: 'open-source', class: 'form-control',
autofocus: local_assigns[:autofocus] || false, required: true,
- pattern: Gitlab::Regex::NAMESPACE_REGEX_STR_SIMPLE,
+ pattern: Gitlab::Regex::NAMESPACE_REGEX_STR_JS,
title: 'Please choose a group name with no special characters.'
- if parent
= f.hidden_field :parent_id, value: parent.id
diff --git a/app/views/shared/_issuable_meta_data.html.haml b/app/views/shared/_issuable_meta_data.html.haml
index 1264e524d86..66310da5cd6 100644
--- a/app/views/shared/_issuable_meta_data.html.haml
+++ b/app/views/shared/_issuable_meta_data.html.haml
@@ -2,6 +2,12 @@
- issue_votes = @issuable_meta_data[issuable.id]
- upvotes, downvotes = issue_votes.upvotes, issue_votes.downvotes
- issuable_url = @collection_type == "Issue" ? issue_path(issuable, anchor: 'notes') : merge_request_path(issuable, anchor: 'notes')
+- issuable_mr = @issuable_meta_data[issuable.id].merge_requests_count
+
+- if issuable_mr > 0
+ %li
+ = image_tag('icon-merge-request-unmerged', class: 'icon-merge-request-unmerged')
+ = issuable_mr
- if upvotes > 0
%li
diff --git a/app/views/shared/_label.html.haml b/app/views/shared/_label.html.haml
index ead9b84b991..1744a597c51 100644
--- a/app/views/shared/_label.html.haml
+++ b/app/views/shared/_label.html.haml
@@ -1,6 +1,4 @@
- label_css_id = dom_id(label)
-- open_issues_count = label.open_issues_count(current_user)
-- open_merge_requests_count = label.open_merge_requests_count(current_user)
- status = label_subscription_status(label, @project).inquiry if current_user
- subject = local_assigns[:subject]
@@ -15,10 +13,10 @@
%ul
%li
= link_to_label(label, subject: subject, type: :merge_request) do
- = pluralize open_merge_requests_count, 'merge request'
+ view merge requests
%li
= link_to_label(label, subject: subject) do
- = pluralize open_issues_count, 'open issue'
+ view open issues
- if current_user && defined?(@project)
%li.label-subscription
- if label.is_a?(ProjectLabel)
@@ -40,9 +38,9 @@
.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
- = pluralize open_merge_requests_count, 'merge request'
+ view merge requests
= link_to_label(label, subject: subject, css_class: 'btn btn-transparent btn-action') do
- = pluralize open_issues_count, 'open issue'
+ view open issues
- if current_user && defined?(@project)
.label-subscription.inline
diff --git a/app/views/shared/builds/_tabs.html.haml b/app/views/shared/builds/_tabs.html.haml
index b6047ece592..3baa956b910 100644
--- a/app/views/shared/builds/_tabs.html.haml
+++ b/app/views/shared/builds/_tabs.html.haml
@@ -1,23 +1,23 @@
%ul.nav-links
- %li{ class: ('active' if scope.nil?) }>
+ %li{ class: active_when(scope.nil?) }>
= link_to build_path_proc.call(nil) do
All
%span.badge.js-totalbuilds-count
= number_with_delimiter(all_builds.count(:id))
- %li{ class: ('active' if scope == 'pending') }>
+ %li{ class: active_when(scope == 'pending') }>
= link_to build_path_proc.call('pending') do
Pending
%span.badge
= number_with_delimiter(all_builds.pending.count(:id))
- %li{ class: ('active' if scope == 'running') }>
+ %li{ class: active_when(scope == 'running') }>
= link_to build_path_proc.call('running') do
Running
%span.badge
= number_with_delimiter(all_builds.running.count(:id))
- %li{ class: ('active' if scope == 'finished') }>
+ %li{ class: active_when(scope == 'finished') }>
= link_to build_path_proc.call('finished') do
Finished
%span.badge
diff --git a/app/views/shared/icons/_icon_customization.svg b/app/views/shared/icons/_icon_customization.svg
new file mode 100644
index 00000000000..eb1f8ba129b
--- /dev/null
+++ b/app/views/shared/icons/_icon_customization.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 112 90" xmlns:xlink="http://www.w3.org/1999/xlink"><g fill="none" fill-rule="evenodd"><rect width="112" height="90" fill="#fff" rx="6"/><path fill="#eee" fill-rule="nonzero" d="m4 6.01v77.98c0 1.11.899 2.01 2 2.01h100c1.105 0 2-.898 2-2.01v-77.98c0-1.11-.899-2.01-2-2.01h-100c-1.105 0-2 .898-2 2.01m-4 0c0-3.319 2.686-6.01 6-6.01h100c3.315 0 6 2.694 6 6.01v77.98c0 3.319-2.686 6.01-6 6.01h-100c-3.315 0-6-2.694-6-6.01v-77.98"/><g transform="translate(26 35)"><rect width="4" height="39" x="5" fill="#eee" rx="2" id="0"/><rect width="4" height="21" x="5" y="18" fill="#fef0ea" rx="2"/><circle cx="7" cy="13" r="5" fill="#fff"/><path fill="#fb722e" fill-rule="nonzero" d="m7 20c-3.866 0-7-3.134-7-7 0-3.866 3.134-7 7-7 3.866 0 7 3.134 7 7 0 3.866-3.134 7-7 7m0-4c1.657 0 3-1.343 3-3 0-1.657-1.343-3-3-3-1.657 0-3 1.343-3 3 0 1.657 1.343 3 3 3"/></g><g transform="translate(49 35)"><use xlink:href="#0"/><rect width="4" height="21" x="5" y="18" fill="#b5a7dd" rx="2"/><circle cx="7" cy="25" r="5" fill="#fff"/><path fill="#6b4fbb" fill-rule="nonzero" d="m7 32c-3.866 0-7-3.134-7-7 0-3.866 3.134-7 7-7 3.866 0 7 3.134 7 7 0 3.866-3.134 7-7 7m0-4c1.657 0 3-1.343 3-3 0-1.657-1.343-3-3-3-1.657 0-3 1.343-3 3 0 1.657 1.343 3 3 3"/></g><g transform="translate(72 33)"><rect width="4" height="39" x="5" y="2" fill="#eee" rx="2"/><rect width="4" height="34" x="5" y="7" fill="#fef0ea" rx="2"/><circle cx="7" cy="7" r="5" fill="#fff"/><path fill="#fb722e" fill-rule="nonzero" d="m7 14c-3.866 0-7-3.134-7-7 0-3.866 3.134-7 7-7 3.866 0 7 3.134 7 7 0 3.866-3.134 7-7 7m0-4c1.657 0 3-1.343 3-3 0-1.657-1.343-3-3-3-1.657 0-3 1.343-3 3 0 1.657 1.343 3 3 3"/></g><g fill="#6b4fbb"><circle cx="13.5" cy="11.5" r="2.5"/><circle cx="23.5" cy="11.5" r="2.5" opacity=".5"/><circle cx="33.5" cy="11.5" r="2.5" opacity=".5"/></g><path fill="#eee" d="m0 19h111v4h-111z"/></g></svg>
diff --git a/app/views/shared/issuable/_nav.html.haml b/app/views/shared/issuable/_nav.html.haml
index 1154316c03f..ad995cbe962 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_when(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_when(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_when(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_when(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_when(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/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml
index 6e417aa2251..62f09cc2dc1 100644
--- a/app/views/shared/issuable/_search_bar.html.haml
+++ b/app/views/shared/issuable/_search_bar.html.haml
@@ -32,7 +32,7 @@
{{hint}}
%span.js-filter-tag.dropdown-light-content
{{tag}}
- #js-dropdown-author.dropdown-menu
+ #js-dropdown-author.dropdown-menu{ data: { icon: 'pencil', hint: 'author', tag: '@author' } }
%ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true }
%li.filter-dropdown-item
%button.btn.btn-link.dropdown-user
@@ -42,7 +42,7 @@
{{name}}
%span.dropdown-light-content
@{{username}}
- #js-dropdown-assignee.dropdown-menu
+ #js-dropdown-assignee.dropdown-menu{ data: { icon: 'user', hint: 'assignee', tag: '@assignee' } }
%ul{ 'data-dropdown' => true }
%li.filter-dropdown-item{ 'data-value' => 'none' }
%button.btn.btn-link
@@ -57,7 +57,7 @@
{{name}}
%span.dropdown-light-content
@{{username}}
- #js-dropdown-milestone.dropdown-menu{ 'data-dropdown' => true }
+ #js-dropdown-milestone.dropdown-menu{ data: { icon: 'clock-o', hint: 'milestone', tag: '%milestone' } }
%ul{ 'data-dropdown' => true }
%li.filter-dropdown-item{ 'data-value' => 'none' }
%button.btn.btn-link
@@ -70,7 +70,7 @@
%li.filter-dropdown-item
%button.btn.btn-link.js-data-value
{{title}}
- #js-dropdown-label.dropdown-menu{ 'data-dropdown' => true }
+ #js-dropdown-label.dropdown-menu{ data: { icon: 'tag', hint: 'label', tag: '~label' } }
%ul{ 'data-dropdown' => true }
%li.filter-dropdown-item{ 'data-value' => 'none' }
%button.btn.btn-link
@@ -82,7 +82,7 @@
%span.dropdown-label-box{ style: 'background: {{color}}' }
%span.label-title.js-data-value
{{title}}
- .pull-right
+ .pull-right.filter-dropdown-container
= render 'shared/sort_dropdown'
- if @bulk_edit
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 3f7f1a86b9f..6c730e16f67 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -173,7 +173,7 @@
: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 MilestoneSelect('{"full_path":"#{@project.full_path}"}');
new LabelsSelect();
new IssuableContext('#{escape_javascript(current_user.to_json(only: [:username, :id, :name]))}');
gl.Subscription.bindAll('.subscription');
diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml
index 239387fc9fa..8e721c9c8dd 100644
--- a/app/views/shared/members/_member.html.haml
+++ b/app/views/shared/members/_member.html.haml
@@ -61,7 +61,7 @@
= dropdown_title("Change permissions")
.dropdown-content
%ul
- - Gitlab::Access.options.each do |role, role_id|
+ - member.class.access_level_roles.each do |role, role_id|
%li
= link_to role, "javascript:void(0)",
class: ("is-active" if member.access_level == role_id),
diff --git a/app/views/shared/milestones/_issuables.html.haml b/app/views/shared/milestones/_issuables.html.haml
index 31eb07ca666..a93cbd1041f 100644
--- a/app/views/shared/milestones/_issuables.html.haml
+++ b/app/views/shared/milestones/_issuables.html.haml
@@ -3,11 +3,11 @@
- panel_class = primary ? 'panel-primary' : 'panel-default'
.panel{ class: panel_class }
- .panel-heading.split
- .left
+ .panel-heading
+ .title
= title
- if show_counter
- .right
+ .counter
= number_with_delimiter(issuables.size)
- class_prefix = dom_class(issuables).pluralize
diff --git a/app/views/snippets/_actions.html.haml b/app/views/snippets/_actions.html.haml
index 855a995afa9..a7f118d3f7d 100644
--- a/app/views/snippets/_actions.html.haml
+++ b/app/views/snippets/_actions.html.haml
@@ -9,7 +9,7 @@
Delete
= 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?
+ - if @snippet.submittable_as_spam_by?(current_user)
= 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" } }
@@ -28,6 +28,6 @@
%li
= link_to edit_snippet_path(@snippet) do
Edit
- - if @snippet.submittable_as_spam? && current_user.admin?
+ - if @snippet.submittable_as_spam_by?(current_user)
%li
= link_to 'Submit as spam', mark_as_spam_snippet_path(@snippet), method: :post
diff --git a/app/views/snippets/_snippets_scope_menu.html.haml b/app/views/snippets/_snippets_scope_menu.html.haml
index 2dda5fed647..8b6a98a054a 100644
--- a/app/views/snippets/_snippets_scope_menu.html.haml
+++ b/app/views/snippets/_snippets_scope_menu.html.haml
@@ -2,7 +2,7 @@
- include_private = local_assigns.fetch(:include_private, false)
.nav-links.snippet-scope-menu
- %li{ class: ("active" unless params[:scope]) }
+ %li{ class: active_when(params[:scope].nil?) }
= link_to subject_snippets_path(subject) do
All
%span.badge
@@ -12,19 +12,19 @@
= subject.snippets.public_and_internal.count
- if include_private
- %li{ class: ("active" if params[:scope] == "are_private") }
+ %li{ class: active_when(params[:scope] == "are_private") }
= link_to subject_snippets_path(subject, scope: 'are_private') do
Private
%span.badge
= subject.snippets.are_private.count
- %li{ class: ("active" if params[:scope] == "are_internal") }
+ %li{ class: active_when(params[:scope] == "are_internal") }
= link_to subject_snippets_path(subject, scope: 'are_internal') do
Internal
%span.badge
= subject.snippets.are_internal.count
- %li{ class: ("active" if params[:scope] == "are_public") }
+ %li{ class: active_when(params[:scope] == "are_public") }
= link_to subject_snippets_path(subject, scope: 'are_public') do
Public
%span.badge
diff --git a/app/views/snippets/verify.html.haml b/app/views/snippets/verify.html.haml
new file mode 100644
index 00000000000..cb623ccab57
--- /dev/null
+++ b/app/views/snippets/verify.html.haml
@@ -0,0 +1,4 @@
+- form = [@snippet.becomes(Snippet)]
+
+= render 'layouts/recaptcha_verification', spammable: @snippet, form: form
+
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index dc2fea450bd..c130f3d9e17 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -98,6 +98,7 @@
Snippets
%div{ class: container_class }
+ .user-callout{ 'callout-svg' => custom_icon('icon_customization') }
.tab-content
#activity.tab-pane
.row-content-block.calender-block.white.second-block.hidden-xs
diff --git a/app/workers/delete_user_worker.rb b/app/workers/delete_user_worker.rb
index 5483bbb210b..3340a7be4fe 100644
--- a/app/workers/delete_user_worker.rb
+++ b/app/workers/delete_user_worker.rb
@@ -7,5 +7,7 @@ class DeleteUserWorker
current_user = User.find(current_user_id)
Users::DestroyService.new(current_user).execute(delete_user, options.symbolize_keys)
+ rescue Gitlab::Access::AccessDeniedError => e
+ Rails.logger.warn("User could not be destroyed: #{e}")
end
end
diff --git a/app/workers/irker_worker.rb b/app/workers/irker_worker.rb
index 7e44b241743..c9658b3fe17 100644
--- a/app/workers/irker_worker.rb
+++ b/app/workers/irker_worker.rb
@@ -120,8 +120,8 @@ class IrkerWorker
end
def compare_url(data, repo_path)
- sha1 = Commit::truncate_sha(data['before'])
- sha2 = Commit::truncate_sha(data['after'])
+ sha1 = Commit.truncate_sha(data['before'])
+ sha2 = Commit.truncate_sha(data['after'])
compare_url = "#{Gitlab.config.gitlab.url}/#{repo_path}/compare"
compare_url += "/#{sha1}...#{sha2}"
colorize_url compare_url
@@ -129,7 +129,7 @@ class IrkerWorker
def send_one_commit(project, hook_attrs, repo_name, branch)
commit = commit_from_id project, hook_attrs['id']
- sha = colorize_sha Commit::truncate_sha(hook_attrs['id'])
+ sha = colorize_sha Commit.truncate_sha(hook_attrs['id'])
author = hook_attrs['author']['name']
files = colorize_nb_files(files_count commit)
title = commit.title
diff --git a/changelogs/unreleased/12726-preserve-issues-after-deleting-users.yml b/changelogs/unreleased/12726-preserve-issues-after-deleting-users.yml
new file mode 100644
index 00000000000..4a1a199673c
--- /dev/null
+++ b/changelogs/unreleased/12726-preserve-issues-after-deleting-users.yml
@@ -0,0 +1,4 @@
+---
+title: Deleting a user doesn't delete issues they've created/are assigned to
+merge_request: 7393
+author:
diff --git a/changelogs/unreleased/1363-redo-mailroom-support.yml b/changelogs/unreleased/1363-redo-mailroom-support.yml
new file mode 100644
index 00000000000..8ed206f4fdb
--- /dev/null
+++ b/changelogs/unreleased/1363-redo-mailroom-support.yml
@@ -0,0 +1,4 @@
+---
+title: Redo internals of Incoming Mail Support
+merge_request: 9385
+author:
diff --git a/changelogs/unreleased/14748-runner-version-in-admin-views.yml b/changelogs/unreleased/14748-runner-version-in-admin-views.yml
new file mode 100644
index 00000000000..2478a81c824
--- /dev/null
+++ b/changelogs/unreleased/14748-runner-version-in-admin-views.yml
@@ -0,0 +1,4 @@
+---
+title: Add runner version to /admin/runners view
+merge_request: 8733
+author: Jonathon Reinhart
diff --git a/changelogs/unreleased/17662-rename-builds.yml b/changelogs/unreleased/17662-rename-builds.yml
deleted file mode 100644
index 12f2998d1c8..00000000000
--- a/changelogs/unreleased/17662-rename-builds.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Rename Builds to Pipelines, CI/CD Pipelines, or Jobs everywhere
-merge_request: 8787
-author:
diff --git a/changelogs/unreleased/1937-https-clone-url-username.yml b/changelogs/unreleased/1937-https-clone-url-username.yml
new file mode 100644
index 00000000000..fa89d94e0f3
--- /dev/null
+++ b/changelogs/unreleased/1937-https-clone-url-username.yml
@@ -0,0 +1,4 @@
+---
+title: Add the Username to the HTTP(S) clone URL of a Repository
+merge_request: 9347
+author: Jan Christophersen
diff --git a/changelogs/unreleased/20452-remove-require-from-request_profiler-initializer.yml b/changelogs/unreleased/20452-remove-require-from-request_profiler-initializer.yml
deleted file mode 100644
index 965d0648adf..00000000000
--- a/changelogs/unreleased/20452-remove-require-from-request_profiler-initializer.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Don't require lib/gitlab/request_profiler/middleware.rb in config/initializers/request_profiler.rb
-merge_request:
-author:
diff --git a/changelogs/unreleased/20852-getting-started-project-better-blank-state-for-labels-view.yml b/changelogs/unreleased/20852-getting-started-project-better-blank-state-for-labels-view.yml
deleted file mode 100644
index eda872049fd..00000000000
--- a/changelogs/unreleased/20852-getting-started-project-better-blank-state-for-labels-view.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Added labels empty state
-merge_request: 7443
-author:
diff --git a/changelogs/unreleased/21240_snippets_line_ending.yml b/changelogs/unreleased/21240_snippets_line_ending.yml
new file mode 100644
index 00000000000..880fdd2c9ed
--- /dev/null
+++ b/changelogs/unreleased/21240_snippets_line_ending.yml
@@ -0,0 +1,4 @@
+---
+title: Download snippets with LF line-endings by default
+merge_request: 8999
+author:
diff --git a/changelogs/unreleased/21518_recaptcha_spam_issues.yml b/changelogs/unreleased/21518_recaptcha_spam_issues.yml
deleted file mode 100644
index bd6c9d7521e..00000000000
--- a/changelogs/unreleased/21518_recaptcha_spam_issues.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-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
deleted file mode 100644
index f43c1925ad0..00000000000
--- a/changelogs/unreleased/22007-unify-projects-search.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Unify projects search by removing /projects/:search endpoint
-merge_request: 8877
-author:
diff --git a/changelogs/unreleased/22018-api-milestone-merge-requests.yml b/changelogs/unreleased/22018-api-milestone-merge-requests.yml
new file mode 100644
index 00000000000..ccad2ec838c
--- /dev/null
+++ b/changelogs/unreleased/22018-api-milestone-merge-requests.yml
@@ -0,0 +1,4 @@
+---
+title: Adds API endpoint to fetch all merge request for a single milestone
+merge_request:
+author: Joren De Groof
diff --git a/changelogs/unreleased/22132-rename-branch-name-params-to-branch.yml b/changelogs/unreleased/22132-rename-branch-name-params-to-branch.yml
new file mode 100644
index 00000000000..028923b83cf
--- /dev/null
+++ b/changelogs/unreleased/22132-rename-branch-name-params-to-branch.yml
@@ -0,0 +1,4 @@
+---
+title: Standardize branch name params as branch on V4 API
+merge_request: 8936
+author:
diff --git a/changelogs/unreleased/22466-task-list-alignment.yml b/changelogs/unreleased/22466-task-list-alignment.yml
new file mode 100644
index 00000000000..6e6ccb873ec
--- /dev/null
+++ b/changelogs/unreleased/22466-task-list-alignment.yml
@@ -0,0 +1,4 @@
+---
+title: Align task list checkboxes
+merge_request: 6487
+author: Jared Deckard <jared.deckard@gmail.com>
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
deleted file mode 100644
index 2c6883bcf7b..00000000000
--- a/changelogs/unreleased/22638-creating-a-branch-matching-a-wildcard-fails.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Allow creating protected branches when user can merge to such branch
-merge_request: 8458
-author:
diff --git a/changelogs/unreleased/22974-trigger-service-events-through-api.yml b/changelogs/unreleased/22974-trigger-service-events-through-api.yml
deleted file mode 100644
index 57106e8c676..00000000000
--- a/changelogs/unreleased/22974-trigger-service-events-through-api.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Adds service trigger events to api
-merge_request: 8324
-author:
diff --git a/changelogs/unreleased/23062-allow-git-log-to-accept-follow-and-skip.yml b/changelogs/unreleased/23062-allow-git-log-to-accept-follow-and-skip.yml
new file mode 100644
index 00000000000..f7c856040e0
--- /dev/null
+++ b/changelogs/unreleased/23062-allow-git-log-to-accept-follow-and-skip.yml
@@ -0,0 +1,4 @@
+---
+title: Make Git history follow renames again by performing the --skip in Ruby
+merge_request:
+author:
diff --git a/changelogs/unreleased/23524-notify-automerge-user-of-failed-build.yml b/changelogs/unreleased/23524-notify-automerge-user-of-failed-build.yml
deleted file mode 100644
index 268be6b9b83..00000000000
--- a/changelogs/unreleased/23524-notify-automerge-user-of-failed-build.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-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/23634-remove-project-grouping.yml b/changelogs/unreleased/23634-remove-project-grouping.yml
deleted file mode 100644
index dde8b2d1815..00000000000
--- a/changelogs/unreleased/23634-remove-project-grouping.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Don't group issues by project on group-level and dashboard issue indexes.
-merge_request: 8111
-author: Bernardo Castro
diff --git a/changelogs/unreleased/23767-disable-storing-of-sensitive-information.yml b/changelogs/unreleased/23767-disable-storing-of-sensitive-information.yml
deleted file mode 100644
index 587ef4f9a73..00000000000
--- a/changelogs/unreleased/23767-disable-storing-of-sensitive-information.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix disable storing of sensitive information when importing a new repo
-merge_request: 8885
-author: Bernard Pietraga
diff --git a/changelogs/unreleased/23819-fix-milestone-counters-to-top-right-of-panel-headings.yml b/changelogs/unreleased/23819-fix-milestone-counters-to-top-right-of-panel-headings.yml
new file mode 100644
index 00000000000..628db8a5419
--- /dev/null
+++ b/changelogs/unreleased/23819-fix-milestone-counters-to-top-right-of-panel-headings.yml
@@ -0,0 +1,4 @@
+---
+title: Fix position of counter in milestone panels
+merge_request: 7842
+author: Andrew Smith (EspadaV8)
diff --git a/changelogs/unreleased/24147-delete-env-button.yml b/changelogs/unreleased/24147-delete-env-button.yml
deleted file mode 100644
index 14e80cacbfb..00000000000
--- a/changelogs/unreleased/24147-delete-env-button.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Adds back ability to stop all environments
-merge_request: 7379
-author:
diff --git a/changelogs/unreleased/24462-reduce_ldap_queries_for_lfs.yml b/changelogs/unreleased/24462-reduce_ldap_queries_for_lfs.yml
deleted file mode 100644
index 05fbd8f0bf2..00000000000
--- a/changelogs/unreleased/24462-reduce_ldap_queries_for_lfs.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Reduce hits to LDAP on Git HTTP auth by reordering auth mechanisms
-merge_request: 8752
-author:
diff --git a/changelogs/unreleased/24606-force-password-reset-on-next-login.yml b/changelogs/unreleased/24606-force-password-reset-on-next-login.yml
deleted file mode 100644
index fd671d04a9f..00000000000
--- a/changelogs/unreleased/24606-force-password-reset-on-next-login.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Force new password after password reset via API
-merge_request:
-author: George Andrinopoulos
diff --git a/changelogs/unreleased/24716-fix-ctrl-click-links.yml b/changelogs/unreleased/24716-fix-ctrl-click-links.yml
deleted file mode 100644
index 13de5db5e41..00000000000
--- a/changelogs/unreleased/24716-fix-ctrl-click-links.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix Ctrl+Click support for Todos and Merge Request page tabs
-merge_request: 8898
-author:
diff --git a/changelogs/unreleased/24795_refactor_merge_request_build_service.yml b/changelogs/unreleased/24795_refactor_merge_request_build_service.yml
deleted file mode 100644
index b735fb57649..00000000000
--- a/changelogs/unreleased/24795_refactor_merge_request_build_service.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Refactor MergeRequests::BuildService
-merge_request: 8462
-author: Rydkin Maxim
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
deleted file mode 100644
index be66c370f36..00000000000
--- a/changelogs/unreleased/24833-Allow-to-search-by-commit-hash-within-project.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: 'Allows to search within project by commit hash'
-merge_request:
-author: YarNayar
diff --git a/changelogs/unreleased/24923_nested_tasks.yml b/changelogs/unreleased/24923_nested_tasks.yml
deleted file mode 100644
index de35cad3dd6..00000000000
--- a/changelogs/unreleased/24923_nested_tasks.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix nested tasks in ordered list
-merge_request: 8626
-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
deleted file mode 100644
index d35ad0be0db..00000000000
--- a/changelogs/unreleased/25134-mobile-issue-view-doesn-t-show-organization-membership.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-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/25312-search-input-cmd-click-issue.yml b/changelogs/unreleased/25312-search-input-cmd-click-issue.yml
deleted file mode 100644
index 56e03a48692..00000000000
--- a/changelogs/unreleased/25312-search-input-cmd-click-issue.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Prevent removal of input fields if it is the parent dropdown element
-merge_request: 8397
-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
deleted file mode 100644
index 50a5c879446..00000000000
--- a/changelogs/unreleased/25360-remove-flash-warning-from-login-page.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Remove flash warning from login page
-merge_request: 8864
-author: Gerald J. Padilla
diff --git a/changelogs/unreleased/25460-replace-word-users-with-members.yml b/changelogs/unreleased/25460-replace-word-users-with-members.yml
deleted file mode 100644
index dac90eaa34d..00000000000
--- a/changelogs/unreleased/25460-replace-word-users-with-members.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Replace word user with member
-merge_request: 8872
-author:
diff --git a/changelogs/unreleased/25465-todo-done-clicking-is-kind-of-unsafe.yml b/changelogs/unreleased/25465-todo-done-clicking-is-kind-of-unsafe.yml
new file mode 100644
index 00000000000..e9d46f6b122
--- /dev/null
+++ b/changelogs/unreleased/25465-todo-done-clicking-is-kind-of-unsafe.yml
@@ -0,0 +1,4 @@
+---
+title: Todo done clicking is kind of unusable
+merge_request: 8691
+author: Jacopo Beschi @jacopo-beschi
diff --git a/changelogs/unreleased/25624-anticipate-obstacles-to-removing-turbolinks.yml b/changelogs/unreleased/25624-anticipate-obstacles-to-removing-turbolinks.yml
deleted file mode 100644
index d7f950d7be9..00000000000
--- a/changelogs/unreleased/25624-anticipate-obstacles-to-removing-turbolinks.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Remove turbolinks.
-merge_request: !8570
-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
deleted file mode 100644
index f74e9fa8b6d..00000000000
--- a/changelogs/unreleased/25811-pipeline-number-and-url-do-not-update-when-new-pipeline-is-triggered.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-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
deleted file mode 100644
index 9506692dd40..00000000000
--- a/changelogs/unreleased/25910-convert-manual-action-icons-to-svg-to-propperly-position-them.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Convert pipeline action icons to svg to have them propperly positioned
-merge_request:
-author:
diff --git a/changelogs/unreleased/25920-create-issue-from-failing-build.yml b/changelogs/unreleased/25920-create-issue-from-failing-build.yml
new file mode 100644
index 00000000000..580d1074aa7
--- /dev/null
+++ b/changelogs/unreleased/25920-create-issue-from-failing-build.yml
@@ -0,0 +1,4 @@
+---
+title: Add button to create issue for failing build
+merge_request: 9391
+author: Alex Sanford
diff --git a/changelogs/unreleased/25989-fix-rogue-scrollbars-on-comments.yml b/changelogs/unreleased/25989-fix-rogue-scrollbars-on-comments.yml
deleted file mode 100644
index e67a9c0da15..00000000000
--- a/changelogs/unreleased/25989-fix-rogue-scrollbars-on-comments.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-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
deleted file mode 100644
index fc3f1af5b61..00000000000
--- a/changelogs/unreleased/26059-segoe-ui-vertical.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-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
deleted file mode 100644
index c938351b8a7..00000000000
--- a/changelogs/unreleased/26068_tasklist_issue.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Don’t count tasks that are not defined as list items correctly
-merge_request: 8526
-author:
diff --git a/changelogs/unreleased/26087-asciidoc-cicd-badges-snippet.yml b/changelogs/unreleased/26087-asciidoc-cicd-badges-snippet.yml
new file mode 100644
index 00000000000..799c5277207
--- /dev/null
+++ b/changelogs/unreleased/26087-asciidoc-cicd-badges-snippet.yml
@@ -0,0 +1,4 @@
+---
+title: Added AsciiDoc Snippet to CI/CD Badges
+merge_request: 9164
+author: Jan Christophersen
diff --git a/changelogs/unreleased/26117-sort-pipeline-for-commit.yml b/changelogs/unreleased/26117-sort-pipeline-for-commit.yml
deleted file mode 100644
index b2f5294d380..00000000000
--- a/changelogs/unreleased/26117-sort-pipeline-for-commit.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-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
deleted file mode 100644
index 565672917b2..00000000000
--- a/changelogs/unreleased/26186-diff-view-plus-and-minus-signs-as-part-of-line-number.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Color + and - signs in diffs to increase code legibility
-merge_request:
-author:
diff --git a/changelogs/unreleased/26206-fix-download-dropdown.yml b/changelogs/unreleased/26206-fix-download-dropdown.yml
new file mode 100644
index 00000000000..a6c101375bb
--- /dev/null
+++ b/changelogs/unreleased/26206-fix-download-dropdown.yml
@@ -0,0 +1,4 @@
+---
+title: Set dropdown height fixed to 250px and make it scrollable
+merge_request: 9063
+author:
diff --git a/changelogs/unreleased/26315-unify-labels-filter-behavior.yml b/changelogs/unreleased/26315-unify-labels-filter-behavior.yml
new file mode 100644
index 00000000000..cd2f40c94fe
--- /dev/null
+++ b/changelogs/unreleased/26315-unify-labels-filter-behavior.yml
@@ -0,0 +1,4 @@
+---
+title: Unify issues search behavior by always filtering when ALL labels matches
+merge_request: 8849
+author:
diff --git a/changelogs/unreleased/26348-cleanup-navigation-order-groups.yml b/changelogs/unreleased/26348-cleanup-navigation-order-groups.yml
new file mode 100644
index 00000000000..ce888baa32f
--- /dev/null
+++ b/changelogs/unreleased/26348-cleanup-navigation-order-groups.yml
@@ -0,0 +1,4 @@
+---
+title: Clean-up Groups navigation order
+merge_request: 9309
+author:
diff --git a/changelogs/unreleased/26379-iid-param.yml b/changelogs/unreleased/26379-iid-param.yml
new file mode 100644
index 00000000000..ac743e68d6f
--- /dev/null
+++ b/changelogs/unreleased/26379-iid-param.yml
@@ -0,0 +1,4 @@
+---
+title: add :iids param to IssuableFinder (resolve technical dept)
+merge_request: 9222
+author: mhasbini
diff --git a/changelogs/unreleased/26445-accessible-piplelines-buttons.yml b/changelogs/unreleased/26445-accessible-piplelines-buttons.yml
deleted file mode 100644
index fb5274e5253..00000000000
--- a/changelogs/unreleased/26445-accessible-piplelines-buttons.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-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
deleted file mode 100644
index 351c53bd076..00000000000
--- a/changelogs/unreleased/26447-fix-tab-list-order.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-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
deleted file mode 100644
index 87ae8233c4a..00000000000
--- a/changelogs/unreleased/26468-fix-users-sort-in-admin-area.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix Sort by Recent Sign-in in Admin Area
-merge_request: 8637
-author: Poornima M
diff --git a/changelogs/unreleased/26500-informative-slack-notifications.yml b/changelogs/unreleased/26500-informative-slack-notifications.yml
new file mode 100644
index 00000000000..342235424f4
--- /dev/null
+++ b/changelogs/unreleased/26500-informative-slack-notifications.yml
@@ -0,0 +1,4 @@
+---
+title: Add user & build links in Slack Notifications
+merge_request: 8641
+author: Poornima M
diff --git a/changelogs/unreleased/26703-todos-count.yml b/changelogs/unreleased/26703-todos-count.yml
new file mode 100644
index 00000000000..24fd0c406e2
--- /dev/null
+++ b/changelogs/unreleased/26703-todos-count.yml
@@ -0,0 +1,4 @@
+---
+title: show 99+ for large count in todos notification bell
+merge_request: 9171
+author: mhasbini
diff --git a/changelogs/unreleased/26787-add-copy-icon-hover-state.yml b/changelogs/unreleased/26787-add-copy-icon-hover-state.yml
deleted file mode 100644
index 31f1812c6f8..00000000000
--- a/changelogs/unreleased/26787-add-copy-icon-hover-state.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-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
deleted file mode 100644
index fb65b068b23..00000000000
--- a/changelogs/unreleased/26852-fix-slug-for-openshift.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-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
deleted file mode 100644
index 8dfabf87c2a..00000000000
--- a/changelogs/unreleased/26863-Remove-hover-animation-from-row-elements.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Remove hover animation from row elements
-merge_request:
-author:
diff --git a/changelogs/unreleased/26875-builds-api-endpoint-skipped-scope.yml b/changelogs/unreleased/26875-builds-api-endpoint-skipped-scope.yml
new file mode 100644
index 00000000000..3d6400cba76
--- /dev/null
+++ b/changelogs/unreleased/26875-builds-api-endpoint-skipped-scope.yml
@@ -0,0 +1,4 @@
+---
+title: Add all available statuses to scope filter for project builds endpoint
+merge_request:
+author: George Andrinopoulos
diff --git a/changelogs/unreleased/26900-pipelines-tabs.yml b/changelogs/unreleased/26900-pipelines-tabs.yml
new file mode 100644
index 00000000000..f08514c621f
--- /dev/null
+++ b/changelogs/unreleased/26900-pipelines-tabs.yml
@@ -0,0 +1,4 @@
+---
+title: Adds Pending and Finished tabs to pipelines page
+merge_request:
+author:
diff --git a/changelogs/unreleased/26920-hover-cursor-on-pagination-element.yml b/changelogs/unreleased/26920-hover-cursor-on-pagination-element.yml
deleted file mode 100644
index ea567437ac2..00000000000
--- a/changelogs/unreleased/26920-hover-cursor-on-pagination-element.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-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
deleted file mode 100644
index 15c5821874e..00000000000
--- a/changelogs/unreleased/26947-build-status-self-link.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-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/26957-tanuki-anim-hang.yml b/changelogs/unreleased/26957-tanuki-anim-hang.yml
new file mode 100644
index 00000000000..c7b4b9ebdfd
--- /dev/null
+++ b/changelogs/unreleased/26957-tanuki-anim-hang.yml
@@ -0,0 +1,4 @@
+---
+title: don't animate logo when downloading files
+merge_request:
+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
deleted file mode 100644
index c5c57af5aaf..00000000000
--- a/changelogs/unreleased/26982-improve-pipeline-status-icon-linking-in-widgets.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-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
deleted file mode 100644
index 7cb5e4b273d..00000000000
--- a/changelogs/unreleased/27013-regression-in-commit-title-bar.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-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
deleted file mode 100644
index f0301c849b6..00000000000
--- a/changelogs/unreleased/27014-fix-pipeline-tooltip-wrapping.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-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
deleted file mode 100644
index b5584749098..00000000000
--- a/changelogs/unreleased/27021-line-numbers-now-in-copy-pasta-data.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Prevent copying of line numbers in parallel diff view
-merge_request: 8706
-author:
diff --git a/changelogs/unreleased/27032-add-a-house-keeping-api-call.yml b/changelogs/unreleased/27032-add-a-house-keeping-api-call.yml
new file mode 100644
index 00000000000..a9f70e339c0
--- /dev/null
+++ b/changelogs/unreleased/27032-add-a-house-keeping-api-call.yml
@@ -0,0 +1,4 @@
+---
+title: Add housekeeping endpoint for Projects API
+merge_request: 9421
+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
deleted file mode 100644
index 52406bba464..00000000000
--- a/changelogs/unreleased/27178-update-builds-link-in-project-settings.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-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
deleted file mode 100644
index 3f902fb324e..00000000000
--- a/changelogs/unreleased/27240-make-progress-bars-consistent.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-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
deleted file mode 100644
index 7b307b501f4..00000000000
--- a/changelogs/unreleased/27267-unnecessary-queries-from-projects-dashboard-atom-and-json.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-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
deleted file mode 100644
index 9456251025b..00000000000
--- a/changelogs/unreleased/27277-small-mini-pipeline-graph-glitch-upon-hover.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: fixed small mini pipeline graph line glitch
-merge_request: 8804
-author:
diff --git a/changelogs/unreleased/27287-label-dropdown-error-messages.yml b/changelogs/unreleased/27287-label-dropdown-error-messages.yml
new file mode 100644
index 00000000000..dfd4102c324
--- /dev/null
+++ b/changelogs/unreleased/27287-label-dropdown-error-messages.yml
@@ -0,0 +1,4 @@
+---
+title: Fix displaying error messages for create label dropdown
+merge_request: 9058
+author: Tom Koole
diff --git a/changelogs/unreleased/27291-unify-mr-diff-file-buttons.yml b/changelogs/unreleased/27291-unify-mr-diff-file-buttons.yml
deleted file mode 100644
index 293aab67d39..00000000000
--- a/changelogs/unreleased/27291-unify-mr-diff-file-buttons.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-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
deleted file mode 100644
index 502927cd160..00000000000
--- a/changelogs/unreleased/27321-double-separator-line-in-edit-projects-settings.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-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
deleted file mode 100644
index 79316abbaf7..00000000000
--- a/changelogs/unreleased/27332-mini-pipeline-graph-with-many-stages-has-no-line-spacing-in-firefox-and-safari.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-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
deleted file mode 100644
index 8f061a34ac0..00000000000
--- a/changelogs/unreleased/27343-autocomplete-post-to-wrong-url-when-not-hosting-in-root.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-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
deleted file mode 100644
index 191b530aee8..00000000000
--- a/changelogs/unreleased/27352-search-label-filter-header.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: 27352-search-label-filter-header
-merge_request:
-author:
diff --git a/changelogs/unreleased/27354-navigation-new-button.yml b/changelogs/unreleased/27354-navigation-new-button.yml
new file mode 100644
index 00000000000..62cac9bbbd3
--- /dev/null
+++ b/changelogs/unreleased/27354-navigation-new-button.yml
@@ -0,0 +1,4 @@
+---
+title: Re-add the New Project button in nav bar
+merge_request:
+author:
diff --git a/changelogs/unreleased/27395-reduce-group-activity-sql-queries-2.yml b/changelogs/unreleased/27395-reduce-group-activity-sql-queries-2.yml
deleted file mode 100644
index f3ce1709518..00000000000
--- a/changelogs/unreleased/27395-reduce-group-activity-sql-queries-2.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Include :author, :project, and :target in Event.with_associations
-merge_request:
-author:
diff --git a/changelogs/unreleased/27395-reduce-group-activity-sql-queries.yml b/changelogs/unreleased/27395-reduce-group-activity-sql-queries.yml
deleted file mode 100644
index 3f6d922f2a0..00000000000
--- a/changelogs/unreleased/27395-reduce-group-activity-sql-queries.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Don't instantiate AR objects in Event.in_projects
-merge_request:
-author:
diff --git a/changelogs/unreleased/27480-deploy_keys_should_not_show_up_in_users_keys_list.yml b/changelogs/unreleased/27480-deploy_keys_should_not_show_up_in_users_keys_list.yml
deleted file mode 100644
index 6e9192cb632..00000000000
--- a/changelogs/unreleased/27480-deploy_keys_should_not_show_up_in_users_keys_list.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Do not display deploy keys in user's own ssh keys list
-merge_request: 9024
-author:
diff --git a/changelogs/unreleased/27484-environment-show-name.yml b/changelogs/unreleased/27484-environment-show-name.yml
deleted file mode 100644
index dc400d65006..00000000000
--- a/changelogs/unreleased/27484-environment-show-name.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-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
deleted file mode 100644
index 5135ff0fd60..00000000000
--- a/changelogs/unreleased/27488-fix-jwt-version.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-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
deleted file mode 100644
index 798c01f3238..00000000000
--- a/changelogs/unreleased/27494-environment-list-column-headers.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-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
deleted file mode 100644
index bc990c66866..00000000000
--- a/changelogs/unreleased/27516-fix-wrong-call-to-project_cache_worker-method.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix wrong call to ProjectCacheWorker.perform
-merge_request: 8910
-author:
diff --git a/changelogs/unreleased/27530-fix-job-dropdown-pipeline-console-error.yml b/changelogs/unreleased/27530-fix-job-dropdown-pipeline-console-error.yml
new file mode 100644
index 00000000000..4436b4bee68
--- /dev/null
+++ b/changelogs/unreleased/27530-fix-job-dropdown-pipeline-console-error.yml
@@ -0,0 +1,4 @@
+---
+title: Fixes job dropdown action throws error in js console
+merge_request: 9182
+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
deleted file mode 100644
index a5bb37ec8a9..00000000000
--- a/changelogs/unreleased/27602-fix-avatar-border-flicker-mention-dropdown.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-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
deleted file mode 100644
index 958621a43a1..00000000000
--- a/changelogs/unreleased/27632_fix_mr_widget_url.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-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
deleted file mode 100644
index 0531ef2c038..00000000000
--- a/changelogs/unreleased/27639-emoji-panel-under-the-side-panel-in-the-merge-request.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Layer award emoji dropdown over the right sidebar
-merge_request: 9004
-author:
diff --git a/changelogs/unreleased/27656-doc-ci-enable-ci.yml b/changelogs/unreleased/27656-doc-ci-enable-ci.yml
deleted file mode 100644
index e6315d683d4..00000000000
--- a/changelogs/unreleased/27656-doc-ci-enable-ci.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Update doc for enabling or disabling GitLab CI
-merge_request: 8965
-author: Takuya Noguchi
diff --git a/changelogs/unreleased/27762-add-default-artifacts-expiration.yml b/changelogs/unreleased/27762-add-default-artifacts-expiration.yml
new file mode 100644
index 00000000000..27fa77ed04d
--- /dev/null
+++ b/changelogs/unreleased/27762-add-default-artifacts-expiration.yml
@@ -0,0 +1,4 @@
+---
+title: Add admin setting for default artifacts expiration
+merge_request: 9219
+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
deleted file mode 100644
index aa89d9f9850..00000000000
--- a/changelogs/unreleased/27774-text-color-contrast-is-barely-readable-for-pipelines-visualization-graph-page-with-roboto-fonts.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-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
deleted file mode 100644
index ee2431869f0..00000000000
--- a/changelogs/unreleased/27822-default-bulk-assign-labels.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add default labels to bulk assign dropdowns
-merge_request:
-author:
diff --git a/changelogs/unreleased/27840-improve-search-bar-experience.yml b/changelogs/unreleased/27840-improve-search-bar-experience.yml
new file mode 100644
index 00000000000..87b1f0c5572
--- /dev/null
+++ b/changelogs/unreleased/27840-improve-search-bar-experience.yml
@@ -0,0 +1,4 @@
+---
+title: Enhanced filter issues layout for better mobile experiance
+merge_request: 9280
+author: Pratik Borsadiya
diff --git a/changelogs/unreleased/27873-when-a-commit-appears-in-several-projects-commit-comments-are-shared-across-projects.yml b/changelogs/unreleased/27873-when-a-commit-appears-in-several-projects-commit-comments-are-shared-across-projects.yml
deleted file mode 100644
index 89e2bdc69bc..00000000000
--- a/changelogs/unreleased/27873-when-a-commit-appears-in-several-projects-commit-comments-are-shared-across-projects.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Only return target project's comments for a commit
-merge_request:
-author:
diff --git a/changelogs/unreleased/27880-pipelines-table-not-showing-commit-branch.yml b/changelogs/unreleased/27880-pipelines-table-not-showing-commit-branch.yml
deleted file mode 100644
index 4251754618b..00000000000
--- a/changelogs/unreleased/27880-pipelines-table-not-showing-commit-branch.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fixes Pipelines table is not showing branch name for commit
-merge_request:
-author:
diff --git a/changelogs/unreleased/27920-both-wip-messages-showing.yml b/changelogs/unreleased/27920-both-wip-messages-showing.yml
new file mode 100644
index 00000000000..497fda8c8ba
--- /dev/null
+++ b/changelogs/unreleased/27920-both-wip-messages-showing.yml
@@ -0,0 +1,4 @@
+---
+title: Dispatch needed JS when creating a new MR in diff view
+merge_request:
+author:
diff --git a/changelogs/unreleased/27922-cmd-click-todo-doesn-t-work.yml b/changelogs/unreleased/27922-cmd-click-todo-doesn-t-work.yml
deleted file mode 100644
index 79a54429ee8..00000000000
--- a/changelogs/unreleased/27922-cmd-click-todo-doesn-t-work.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix regression where cmd-click stopped working for todos and merge request
- tabs
-merge_request:
-author:
diff --git a/changelogs/unreleased/27925-fix-mr-stray-pipelines-api-request.yml b/changelogs/unreleased/27925-fix-mr-stray-pipelines-api-request.yml
deleted file mode 100644
index f7bdb62b7f3..00000000000
--- a/changelogs/unreleased/27925-fix-mr-stray-pipelines-api-request.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix stray pipelines API request when showing MR
-merge_request:
-author:
diff --git a/changelogs/unreleased/27932-merge-request-pipelines-displays-json.yml b/changelogs/unreleased/27932-merge-request-pipelines-displays-json.yml
deleted file mode 100644
index b7505e28401..00000000000
--- a/changelogs/unreleased/27932-merge-request-pipelines-displays-json.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix Merge request pipelines displays JSON
-merge_request:
-author:
diff --git a/changelogs/unreleased/27934-left-align-logo.yml b/changelogs/unreleased/27934-left-align-logo.yml
new file mode 100644
index 00000000000..d4e5e169465
--- /dev/null
+++ b/changelogs/unreleased/27934-left-align-logo.yml
@@ -0,0 +1,4 @@
+---
+title: Left align logo
+merge_request:
+author:
diff --git a/changelogs/unreleased/27939-fix-current-build-arrow.yml b/changelogs/unreleased/27939-fix-current-build-arrow.yml
deleted file mode 100644
index 280ab090f2c..00000000000
--- a/changelogs/unreleased/27939-fix-current-build-arrow.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix current build arrow indicator
-merge_request:
-author:
diff --git a/changelogs/unreleased/27943-contribution-list-on-profile-page-is-aligned-right.yml b/changelogs/unreleased/27943-contribution-list-on-profile-page-is-aligned-right.yml
deleted file mode 100644
index fcbd48b0357..00000000000
--- a/changelogs/unreleased/27943-contribution-list-on-profile-page-is-aligned-right.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix contribution activity alignment
-merge_request:
-author:
diff --git a/changelogs/unreleased/27947-missing-margin-between-loading-icon-and-text-in-merge-request-widget.yml b/changelogs/unreleased/27947-missing-margin-between-loading-icon-and-text-in-merge-request-widget.yml
deleted file mode 100644
index 1dfabd3813b..00000000000
--- a/changelogs/unreleased/27947-missing-margin-between-loading-icon-and-text-in-merge-request-widget.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add space between text and loading icon in Megre Request Widget
-merge_request: 9119
-author:
diff --git a/changelogs/unreleased/27955-mr-notification-use-pipeline-language.yml b/changelogs/unreleased/27955-mr-notification-use-pipeline-language.yml
deleted file mode 100644
index d9f78db4bec..00000000000
--- a/changelogs/unreleased/27955-mr-notification-use-pipeline-language.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Show Pipeline(not Job) in MR desktop notification
-merge_request:
-author:
diff --git a/changelogs/unreleased/27963-tooltips-jobs.yml b/changelogs/unreleased/27963-tooltips-jobs.yml
deleted file mode 100644
index ba418d86433..00000000000
--- a/changelogs/unreleased/27963-tooltips-jobs.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix tooltips in mini pipeline graph
-merge_request:
-author:
diff --git a/changelogs/unreleased/27966-branch-ref-switcher-input-filter-broken.yml b/changelogs/unreleased/27966-branch-ref-switcher-input-filter-broken.yml
deleted file mode 100644
index 6fa13395a7d..00000000000
--- a/changelogs/unreleased/27966-branch-ref-switcher-input-filter-broken.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Display loading indicator when filtering ref switcher dropdown
-merge_request:
-author:
diff --git a/changelogs/unreleased/27987-skipped-pipeline-mr-graph.yml b/changelogs/unreleased/27987-skipped-pipeline-mr-graph.yml
deleted file mode 100644
index e4287d6276c..00000000000
--- a/changelogs/unreleased/27987-skipped-pipeline-mr-graph.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Show pipeline graph in MR widget if there are any stages
-merge_request:
-author:
diff --git a/changelogs/unreleased/27991-success-with-warnings-caret.yml b/changelogs/unreleased/27991-success-with-warnings-caret.yml
deleted file mode 100644
index 703d34a5ede..00000000000
--- a/changelogs/unreleased/27991-success-with-warnings-caret.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix icon colors in merge request widget mini graph
-merge_request:
-author:
diff --git a/changelogs/unreleased/28029-improve-blockquote-formatting-on-emails.yml b/changelogs/unreleased/28029-improve-blockquote-formatting-on-emails.yml
deleted file mode 100644
index be2a0afbc52..00000000000
--- a/changelogs/unreleased/28029-improve-blockquote-formatting-on-emails.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Improve blockquote formatting in notification emails
-merge_request:
-author:
diff --git a/changelogs/unreleased/28032-tooltips-file-name.yml b/changelogs/unreleased/28032-tooltips-file-name.yml
deleted file mode 100644
index 9fe11e7c2b6..00000000000
--- a/changelogs/unreleased/28032-tooltips-file-name.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Adds container to tooltip in order to make it work with overflow:hidden in
- parent element
-merge_request:
-author:
diff --git a/changelogs/unreleased/28082-deleted-branch-event-404.yml b/changelogs/unreleased/28082-deleted-branch-event-404.yml
new file mode 100644
index 00000000000..e989ca34784
--- /dev/null
+++ b/changelogs/unreleased/28082-deleted-branch-event-404.yml
@@ -0,0 +1,4 @@
+---
+title: Stop linking to deleted Branches in Activity tabs
+merge_request: 9203
+author: Jan Christophersen
diff --git a/changelogs/unreleased/28124-mrs-don-t-show-all-merge-errors.yml b/changelogs/unreleased/28124-mrs-don-t-show-all-merge-errors.yml
deleted file mode 100644
index cd61c38e1bc..00000000000
--- a/changelogs/unreleased/28124-mrs-don-t-show-all-merge-errors.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Show merge errors in merge request widget
-merge_request: 9229
-author:
diff --git a/changelogs/unreleased/28186-long-group-names-overflow-out-of-todos-view.yml b/changelogs/unreleased/28186-long-group-names-overflow-out-of-todos-view.yml
new file mode 100644
index 00000000000..3bcf0e06d08
--- /dev/null
+++ b/changelogs/unreleased/28186-long-group-names-overflow-out-of-todos-view.yml
@@ -0,0 +1,4 @@
+---
+title: Truncate long Todo titles for non-mobile screens
+merge_request: 9311
+author:
diff --git a/changelogs/unreleased/28204-option-to-disable-webpack-dev-server-livereload.yml b/changelogs/unreleased/28204-option-to-disable-webpack-dev-server-livereload.yml
new file mode 100644
index 00000000000..df2478a3f28
--- /dev/null
+++ b/changelogs/unreleased/28204-option-to-disable-webpack-dev-server-livereload.yml
@@ -0,0 +1,4 @@
+---
+title: Pick up option from GDK to disable webpack dev server livereload
+merge_request:
+author:
diff --git a/changelogs/unreleased/28236-browse-button-dropping.yml b/changelogs/unreleased/28236-browse-button-dropping.yml
new file mode 100644
index 00000000000..3a3d755f40c
--- /dev/null
+++ b/changelogs/unreleased/28236-browse-button-dropping.yml
@@ -0,0 +1,4 @@
+---
+title: Increase right side of file header to button stays on same line
+merge_request:
+author:
diff --git a/changelogs/unreleased/28247-timeloops-bug.yml b/changelogs/unreleased/28247-timeloops-bug.yml
new file mode 100644
index 00000000000..12ab523b7c7
--- /dev/null
+++ b/changelogs/unreleased/28247-timeloops-bug.yml
@@ -0,0 +1,4 @@
+---
+title: Only run timeago loops after rendering timeago components
+merge_request:
+author:
diff --git a/changelogs/unreleased/28253-fix-buid-scroll-button-position.yml b/changelogs/unreleased/28253-fix-buid-scroll-button-position.yml
new file mode 100644
index 00000000000..b13d115dab9
--- /dev/null
+++ b/changelogs/unreleased/28253-fix-buid-scroll-button-position.yml
@@ -0,0 +1,4 @@
+---
+title: Fix positioning of `Scroll to top` button
+merge_request:
+author:
diff --git a/changelogs/unreleased/28262-horizontal-scrolling-issue-on-long-project-names.yml b/changelogs/unreleased/28262-horizontal-scrolling-issue-on-long-project-names.yml
new file mode 100644
index 00000000000..fa1674453de
--- /dev/null
+++ b/changelogs/unreleased/28262-horizontal-scrolling-issue-on-long-project-names.yml
@@ -0,0 +1,4 @@
+---
+title: Wrap long Project and Group titles
+merge_request: 9301
+author:
diff --git a/changelogs/unreleased/28303-change-development-tanuki-favicon-colors-to-match-logo.yml b/changelogs/unreleased/28303-change-development-tanuki-favicon-colors-to-match-logo.yml
new file mode 100644
index 00000000000..b97e9a59b2a
--- /dev/null
+++ b/changelogs/unreleased/28303-change-development-tanuki-favicon-colors-to-match-logo.yml
@@ -0,0 +1,4 @@
+---
+title: Change development tanuki favicon colors to match logo color order
+merge_request:
+author:
diff --git a/changelogs/unreleased/28329-allow-slash-in-slash-command-args.yml b/changelogs/unreleased/28329-allow-slash-in-slash-command-args.yml
new file mode 100644
index 00000000000..fed02139a5c
--- /dev/null
+++ b/changelogs/unreleased/28329-allow-slash-in-slash-command-args.yml
@@ -0,0 +1,4 @@
+---
+title: Allow slashes in slash command arguments
+merge_request:
+author:
diff --git a/changelogs/unreleased/28353-little-grammar-issue.yml b/changelogs/unreleased/28353-little-grammar-issue.yml
new file mode 100644
index 00000000000..10bdb17b266
--- /dev/null
+++ b/changelogs/unreleased/28353-little-grammar-issue.yml
@@ -0,0 +1,4 @@
+---
+title: Fix grammer issue in admin/runners
+merge_request:
+author:
diff --git a/changelogs/unreleased/28366-renamed-file-tooltip-contains-html.yml b/changelogs/unreleased/28366-renamed-file-tooltip-contains-html.yml
new file mode 100644
index 00000000000..faf1e89ed94
--- /dev/null
+++ b/changelogs/unreleased/28366-renamed-file-tooltip-contains-html.yml
@@ -0,0 +1,4 @@
+---
+title: Remove markup that was showing in tooltip for renamed files
+merge_request: 9374
+author:
diff --git a/changelogs/unreleased/28367-fix-unfold-diff-line-number-copy-paste.yml b/changelogs/unreleased/28367-fix-unfold-diff-line-number-copy-paste.yml
new file mode 100644
index 00000000000..6fc89fd91dd
--- /dev/null
+++ b/changelogs/unreleased/28367-fix-unfold-diff-line-number-copy-paste.yml
@@ -0,0 +1,4 @@
+---
+title: Fixes includes line number during unfold copy n paste in parallel diff view
+merge_request: 9365
+author:
diff --git a/changelogs/unreleased/28389-ux-problem-with-pipeline-coverage-placeholder.yml b/changelogs/unreleased/28389-ux-problem-with-pipeline-coverage-placeholder.yml
new file mode 100644
index 00000000000..ed357d86fe3
--- /dev/null
+++ b/changelogs/unreleased/28389-ux-problem-with-pipeline-coverage-placeholder.yml
@@ -0,0 +1,4 @@
+---
+title: Changed coverage reg expression placeholder text to be more like a placeholder
+merge_request:
+author:
diff --git a/changelogs/unreleased/28450-test-compiling-frontend-assets-for-production-in-ci.yml b/changelogs/unreleased/28450-test-compiling-frontend-assets-for-production-in-ci.yml
new file mode 100644
index 00000000000..196a9b788ea
--- /dev/null
+++ b/changelogs/unreleased/28450-test-compiling-frontend-assets-for-production-in-ci.yml
@@ -0,0 +1,4 @@
+---
+title: test compiling production assets and generate webpack bundle report in CI
+merge_request: 9396
+author:
diff --git a/changelogs/unreleased/28458-present-gitlab-version-for-v4-changes-on-docs.yml b/changelogs/unreleased/28458-present-gitlab-version-for-v4-changes-on-docs.yml
new file mode 100644
index 00000000000..dbbe8a19204
--- /dev/null
+++ b/changelogs/unreleased/28458-present-gitlab-version-for-v4-changes-on-docs.yml
@@ -0,0 +1,4 @@
+---
+title: Present GitLab version for each V3 to V4 API change on v3_to_v4.md
+merge_request:
+author:
diff --git a/changelogs/unreleased/28462-fix-delimiter-removes-issue-in-todo-counter.yml b/changelogs/unreleased/28462-fix-delimiter-removes-issue-in-todo-counter.yml
new file mode 100644
index 00000000000..80995d75c23
--- /dev/null
+++ b/changelogs/unreleased/28462-fix-delimiter-removes-issue-in-todo-counter.yml
@@ -0,0 +1,4 @@
+---
+title: Fixes delimiter removes when todo marked as done
+merge_request: 9435
+author:
diff --git a/changelogs/unreleased/28524-gitlab-ci-yml-coverage-key-is-unknown.yml b/changelogs/unreleased/28524-gitlab-ci-yml-coverage-key-is-unknown.yml
new file mode 100644
index 00000000000..eda5764c13e
--- /dev/null
+++ b/changelogs/unreleased/28524-gitlab-ci-yml-coverage-key-is-unknown.yml
@@ -0,0 +1,4 @@
+---
+title: Document when current coverage configuration option was introduced
+merge_request: 9443
+author:
diff --git a/changelogs/unreleased/28696-improve-grammar-gitlab-flow-doc.yml b/changelogs/unreleased/28696-improve-grammar-gitlab-flow-doc.yml
new file mode 100644
index 00000000000..e38e5d0db5b
--- /dev/null
+++ b/changelogs/unreleased/28696-improve-grammar-gitlab-flow-doc.yml
@@ -0,0 +1,4 @@
+---
+title: Improve grammar in GitLab flow documentation
+merge_request: 9552
+author: infogrind
diff --git a/changelogs/unreleased/28723-consistent-handling-indexof.yml b/changelogs/unreleased/28723-consistent-handling-indexof.yml
new file mode 100644
index 00000000000..95d6181d5fa
--- /dev/null
+++ b/changelogs/unreleased/28723-consistent-handling-indexof.yml
@@ -0,0 +1,4 @@
+---
+title: Keep consistent in handling indexOf results
+merge_request: 9531
+author: Takuya Noguchi
diff --git a/changelogs/unreleased/28837-remove-help-duplicate.yml b/changelogs/unreleased/28837-remove-help-duplicate.yml
new file mode 100644
index 00000000000..b1001245663
--- /dev/null
+++ b/changelogs/unreleased/28837-remove-help-duplicate.yml
@@ -0,0 +1,4 @@
+---
+title: Remove help link from right dropdown
+merge_request:
+author:
diff --git a/changelogs/unreleased/3874-correctly-return-json-on-delete-responses.yml b/changelogs/unreleased/3874-correctly-return-json-on-delete-responses.yml
new file mode 100644
index 00000000000..4a4932288b4
--- /dev/null
+++ b/changelogs/unreleased/3874-correctly-return-json-on-delete-responses.yml
@@ -0,0 +1,4 @@
+---
+title: Return 202 with JSON body on async removals on V4 API
+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
deleted file mode 100644
index 11d1f55172b..00000000000
--- a/changelogs/unreleased/395-fix-notification-when-group-set-to-watch.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix notifications when set at group level
-merge_request: 6813
-author: Alexandre Maia
diff --git a/changelogs/unreleased/6073_project_api.yml b/changelogs/unreleased/6073_project_api.yml
new file mode 100644
index 00000000000..fd6792a406e
--- /dev/null
+++ b/changelogs/unreleased/6073_project_api.yml
@@ -0,0 +1,4 @@
+---
+title: 'API project create: Make name or path required'
+merge_request: 9416
+author:
diff --git a/changelogs/unreleased/8-15-stable.yml b/changelogs/unreleased/8-15-stable.yml
deleted file mode 100644
index 75502e139e7..00000000000
--- a/changelogs/unreleased/8-15-stable.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Ensure export files are removed after a namespace is deleted
-merge_request:
-author:
diff --git a/changelogs/unreleased/8082-permalink-to-file.yml b/changelogs/unreleased/8082-permalink-to-file.yml
deleted file mode 100644
index 136d2108c63..00000000000
--- a/changelogs/unreleased/8082-permalink-to-file.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-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
deleted file mode 100644
index 2f0f1887257..00000000000
--- a/changelogs/unreleased/9-0-api-changes.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Remove deprecated MR and Issue endpoints and preserve V3 namespace
-merge_request: 8967
-author:
diff --git a/changelogs/unreleased/9381-authentiq-backchannel-logout.yml b/changelogs/unreleased/9381-authentiq-backchannel-logout.yml
new file mode 100644
index 00000000000..4dbf36cd096
--- /dev/null
+++ b/changelogs/unreleased/9381-authentiq-backchannel-logout.yml
@@ -0,0 +1,4 @@
+---
+title: Adds remote logout functionality to the Authentiq OAuth provider
+merge_request: 9381
+author: Alexandros Keramidas
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
deleted file mode 100644
index 9fd6ea5bc52..00000000000
--- a/changelogs/unreleased/Add-a-shash-command-for-target-merge-request-branch.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Adds /target_branch slash command functionality for merge requests
-merge_request:
-author: YarNayar
diff --git a/changelogs/unreleased/add-filtered-search-to-mr.yml b/changelogs/unreleased/add-filtered-search-to-mr.yml
new file mode 100644
index 00000000000..e3577e2aec7
--- /dev/null
+++ b/changelogs/unreleased/add-filtered-search-to-mr.yml
@@ -0,0 +1,4 @@
+---
+title: Add filtered search to MR page
+merge_request:
+author:
diff --git a/changelogs/unreleased/add-yarn-documentation.yml b/changelogs/unreleased/add-yarn-documentation.yml
new file mode 100644
index 00000000000..5bcc01ac177
--- /dev/null
+++ b/changelogs/unreleased/add-yarn-documentation.yml
@@ -0,0 +1,4 @@
+---
+title: add rake tasks to handle yarn dependencies and update documentation
+merge_request: 9316
+author:
diff --git a/changelogs/unreleased/add_mr_info_to_issues_list.yml b/changelogs/unreleased/add_mr_info_to_issues_list.yml
new file mode 100644
index 00000000000..8087aa6296c
--- /dev/null
+++ b/changelogs/unreleased/add_mr_info_to_issues_list.yml
@@ -0,0 +1,4 @@
+---
+title: Add merge request count to each issue on issues list
+merge_request: 9252
+author: blackst0ne
diff --git a/changelogs/unreleased/add_project_update_hook.yml b/changelogs/unreleased/add_project_update_hook.yml
deleted file mode 100644
index 915c9538843..00000000000
--- a/changelogs/unreleased/add_project_update_hook.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-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/api-empty-return.yml b/changelogs/unreleased/api-empty-return.yml
new file mode 100644
index 00000000000..7810e83eb0e
--- /dev/null
+++ b/changelogs/unreleased/api-empty-return.yml
@@ -0,0 +1,4 @@
+---
+title: 'API: Return 204 for all delete endpoints'
+merge_request: 9397
+author: Robert Schilling
diff --git a/changelogs/unreleased/api-fix-files.yml b/changelogs/unreleased/api-fix-files.yml
deleted file mode 100644
index 8a9e29109a8..00000000000
--- a/changelogs/unreleased/api-fix-files.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: 'API: Fix file downloading'
-merge_request: Robert Schilling
-author: 8267
diff --git a/changelogs/unreleased/api-notes-entity-fields.yml b/changelogs/unreleased/api-notes-entity-fields.yml
new file mode 100644
index 00000000000..f7631df31e2
--- /dev/null
+++ b/changelogs/unreleased/api-notes-entity-fields.yml
@@ -0,0 +1,4 @@
+---
+title: 'API: Remove deprecated fields Notes#upvotes and Notes#downvotes'
+merge_request: 9384
+author: Robert Schilling
diff --git a/changelogs/unreleased/api-post-block.yml b/changelogs/unreleased/api-post-block.yml
new file mode 100644
index 00000000000..dfc61ffa9e3
--- /dev/null
+++ b/changelogs/unreleased/api-post-block.yml
@@ -0,0 +1,4 @@
+---
+title: 'API: Use POST to (un)block a user'
+merge_request: 9371
+author: Robert Schilling
diff --git a/changelogs/unreleased/api-remove-deploy-key-disable.yml b/changelogs/unreleased/api-remove-deploy-key-disable.yml
new file mode 100644
index 00000000000..f471ad2aa20
--- /dev/null
+++ b/changelogs/unreleased/api-remove-deploy-key-disable.yml
@@ -0,0 +1,4 @@
+---
+title: 'API: Remove `DELETE projects/:id/deploy_keys/:key_id/disable`'
+merge_request: 9365
+author: Robert Schilling
diff --git a/changelogs/unreleased/api-remove-owned-groups.yml b/changelogs/unreleased/api-remove-owned-groups.yml
new file mode 100644
index 00000000000..cf0301b7fe0
--- /dev/null
+++ b/changelogs/unreleased/api-remove-owned-groups.yml
@@ -0,0 +1,4 @@
+---
+title: 'API: Remove /groups/owned endpoint'
+merge_request: 9505
+author: Robert Schilling
diff --git a/changelogs/unreleased/api-remove-snippets-expires-at.yml b/changelogs/unreleased/api-remove-snippets-expires-at.yml
deleted file mode 100644
index 67603bfab3b..00000000000
--- a/changelogs/unreleased/api-remove-snippets-expires-at.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: 'API: Remove deprecated ''expires_at'' from project snippets'
-merge_request: 8723
-author: Robert Schilling
diff --git a/changelogs/unreleased/api-star-restful.yml b/changelogs/unreleased/api-star-restful.yml
new file mode 100644
index 00000000000..3e7de8cd822
--- /dev/null
+++ b/changelogs/unreleased/api-star-restful.yml
@@ -0,0 +1,4 @@
+---
+title: 'API: Moved `DELETE /projects/:id/star` to `POST /projects/:id/unstar`'
+merge_request: 9328
+author: Robert Schilling
diff --git a/changelogs/unreleased/api-subscription-restful.yml b/changelogs/unreleased/api-subscription-restful.yml
new file mode 100644
index 00000000000..95db470e6c9
--- /dev/null
+++ b/changelogs/unreleased/api-subscription-restful.yml
@@ -0,0 +1,4 @@
+---
+title: 'API: - Make subscription API more RESTful. Use `post ":project_id/:subscribable_type/:subscribable_id/subscribe"` to subscribe and `post ":project_id/:subscribable_type/:subscribable_id/unsubscribe"` to unsubscribe from a resource.'
+merge_request: 9325
+author: Robert Schilling
diff --git a/changelogs/unreleased/api-todos-restful.yml b/changelogs/unreleased/api-todos-restful.yml
new file mode 100644
index 00000000000..dba1350a495
--- /dev/null
+++ b/changelogs/unreleased/api-todos-restful.yml
@@ -0,0 +1,4 @@
+---
+title: 'API: Use POST requests to mark todos as done'
+merge_request: 9410
+author: Robert Schilling
diff --git a/changelogs/unreleased/artifactsdoc.yml b/changelogs/unreleased/artifactsdoc.yml
new file mode 100644
index 00000000000..4ef32d5256f
--- /dev/null
+++ b/changelogs/unreleased/artifactsdoc.yml
@@ -0,0 +1,4 @@
+---
+title: Added documentation for permalinks to most recent build artifacts.
+merge_request: 8934
+author: Christian Godenschwager
diff --git a/changelogs/unreleased/babel-all-the-things.yml b/changelogs/unreleased/babel-all-the-things.yml
deleted file mode 100644
index fda1c3bd562..00000000000
--- a/changelogs/unreleased/babel-all-the-things.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: use babel to transpile all non-vendor javascript assets regardless of file
- extension
-merge_request: 8988
-author:
diff --git a/changelogs/unreleased/branch_deletion.yml b/changelogs/unreleased/branch_deletion.yml
new file mode 100644
index 00000000000..dbc9265a1fb
--- /dev/null
+++ b/changelogs/unreleased/branch_deletion.yml
@@ -0,0 +1,4 @@
+---
+title: on branch deletion show loading icon and disabled the button
+merge_request: 6761
+author: wendy0402
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
deleted file mode 100644
index 77750b55e7e..00000000000
--- a/changelogs/unreleased/broken_iamge_when_doing_offline_update_check-help_page-.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Hide version check image if there is no internet connection
-merge_request: 8355
-author: Ken Ding
diff --git a/changelogs/unreleased/clipboard-button-commit-sha.yml b/changelogs/unreleased/clipboard-button-commit-sha.yml
deleted file mode 100644
index 6aa4a5664e7..00000000000
--- a/changelogs/unreleased/clipboard-button-commit-sha.yml
+++ /dev/null
@@ -1,3 +0,0 @@
----
-title: 'Copy commit SHA to clipboard'
-merge_request: 8547
diff --git a/changelogs/unreleased/contribution-calendar-scroll.yml b/changelogs/unreleased/contribution-calendar-scroll.yml
deleted file mode 100644
index a504d59e61c..00000000000
--- a/changelogs/unreleased/contribution-calendar-scroll.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-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
deleted file mode 100644
index 506815a5b54..00000000000
--- a/changelogs/unreleased/cop-gem-fetcher.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-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
deleted file mode 100644
index 637e9dc36e2..00000000000
--- a/changelogs/unreleased/copy-as-md.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Copying a rendered issue/comment will paste into GFM textareas as actual GFM
-merge_request:
-author:
diff --git a/changelogs/unreleased/create_branch_repo_less.yml b/changelogs/unreleased/create_branch_repo_less.yml
new file mode 100644
index 00000000000..e8b14fa3b67
--- /dev/null
+++ b/changelogs/unreleased/create_branch_repo_less.yml
@@ -0,0 +1,4 @@
+---
+title: Creating a new branch from an issue will automatically initialize a repository if one doesn't already exist.
+merge_request:
+author:
diff --git a/changelogs/unreleased/disable-autologin-on-email-confirmation-links.yml b/changelogs/unreleased/disable-autologin-on-email-confirmation-links.yml
deleted file mode 100644
index 6dd0d748001..00000000000
--- a/changelogs/unreleased/disable-autologin-on-email-confirmation-links.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-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
deleted file mode 100644
index 8705ed28400..00000000000
--- a/changelogs/unreleased/display-project-id.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Display project ID in project settings
-merge_request: 8572
-author: winniehell
diff --git a/changelogs/unreleased/document-how-to-vue.yml b/changelogs/unreleased/document-how-to-vue.yml
deleted file mode 100644
index 863e41b6413..00000000000
--- a/changelogs/unreleased/document-how-to-vue.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-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
deleted file mode 100644
index fb589a053c0..00000000000
--- a/changelogs/unreleased/dont-delete-assigned-issuables.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Don't delete assigned MRs/issues when user is deleted
-merge_request:
-author:
diff --git a/changelogs/unreleased/dynamic-project-title-fixture.yml b/changelogs/unreleased/dynamic-project-title-fixture.yml
new file mode 100644
index 00000000000..2404cbb891c
--- /dev/null
+++ b/changelogs/unreleased/dynamic-project-title-fixture.yml
@@ -0,0 +1,4 @@
+---
+title: Replace static fixture for project_title_spec.js
+merge_request: 9175
+author: winniehell
diff --git a/changelogs/unreleased/dynamic-todos-fixture.yml b/changelogs/unreleased/dynamic-todos-fixture.yml
deleted file mode 100644
index 580bc729e3c..00000000000
--- a/changelogs/unreleased/dynamic-todos-fixture.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Replace static fixture for right_sidebar_spec.js
-merge_request: 9211
-author: winniehell
diff --git a/changelogs/unreleased/dz-nested-groups-improvements-2.yml b/changelogs/unreleased/dz-nested-groups-improvements-2.yml
deleted file mode 100644
index 8e4eb7f1fff..00000000000
--- a/changelogs/unreleased/dz-nested-groups-improvements-2.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add read-only full_path and full_name attributes to Group API
-merge_request: 8827
-author:
diff --git a/changelogs/unreleased/empty-selection-reply-shortcut.yml b/changelogs/unreleased/empty-selection-reply-shortcut.yml
deleted file mode 100644
index 5a42c98a800..00000000000
--- a/changelogs/unreleased/empty-selection-reply-shortcut.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Change the reply shortcut to focus the field even without a selection.
-merge_request: 8873
-author: Brian Hall
diff --git a/changelogs/unreleased/fe-commit-mr-pipelines.yml b/changelogs/unreleased/fe-commit-mr-pipelines.yml
deleted file mode 100644
index b5cc6bbf8b6..00000000000
--- a/changelogs/unreleased/fe-commit-mr-pipelines.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Use vue.js Pipelines table in commit and merge request view
-merge_request: 8844
-author:
diff --git a/changelogs/unreleased/feature-brand-logo-in-emails.yml b/changelogs/unreleased/feature-brand-logo-in-emails.yml
new file mode 100644
index 00000000000..a7674b9b25e
--- /dev/null
+++ b/changelogs/unreleased/feature-brand-logo-in-emails.yml
@@ -0,0 +1,4 @@
+---
+title: Brand header logo for pipeline emails
+merge_request: 9049
+author: Alexis Reigel
diff --git a/changelogs/unreleased/feature-github-find-users-by-email.yml b/changelogs/unreleased/feature-github-find-users-by-email.yml
new file mode 100644
index 00000000000..1503cf2b9f7
--- /dev/null
+++ b/changelogs/unreleased/feature-github-find-users-by-email.yml
@@ -0,0 +1,4 @@
+---
+title: GitHub Importer - Find users based on GitHub email address
+merge_request: 8958
+author:
diff --git a/changelogs/unreleased/feature-runners-registration-deletion-v4-api.yml b/changelogs/unreleased/feature-runners-registration-deletion-v4-api.yml
new file mode 100644
index 00000000000..e646a6a17b7
--- /dev/null
+++ b/changelogs/unreleased/feature-runners-registration-deletion-v4-api.yml
@@ -0,0 +1,4 @@
+---
+title: Add Runner's registration/deletion v4 API
+merge_request: 9246
+author:
diff --git a/changelogs/unreleased/feature-success-warning-icons-in-stages-builds.yml b/changelogs/unreleased/feature-success-warning-icons-in-stages-builds.yml
deleted file mode 100644
index 5fba0332881..00000000000
--- a/changelogs/unreleased/feature-success-warning-icons-in-stages-builds.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Use warning icon in mini-graph if stage passed conditionally
-merge_request: 8503
-author:
diff --git a/changelogs/unreleased/fix-27479.yml b/changelogs/unreleased/fix-27479.yml
deleted file mode 100644
index cc72a830695..00000000000
--- a/changelogs/unreleased/fix-27479.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-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
deleted file mode 100644
index 43b3b9bf96e..00000000000
--- a/changelogs/unreleased/fix-anchor-scrolling.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-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
deleted file mode 100644
index 33b677b1f29..00000000000
--- a/changelogs/unreleased/fix-api-mr-permissions.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-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-ar-connection-leaks.yml b/changelogs/unreleased/fix-ar-connection-leaks.yml
deleted file mode 100644
index 9da715560ad..00000000000
--- a/changelogs/unreleased/fix-ar-connection-leaks.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Don't connect in Gitlab::Database.adapter_name
-merge_request:
-author:
diff --git a/changelogs/unreleased/fix-ci-build-policy.yml b/changelogs/unreleased/fix-ci-build-policy.yml
deleted file mode 100644
index 26003713ed4..00000000000
--- a/changelogs/unreleased/fix-ci-build-policy.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Improve build policy and access abilities
-merge_request: 8711
-author:
diff --git a/changelogs/unreleased/fix-deleting-project-again.yml b/changelogs/unreleased/fix-deleting-project-again.yml
deleted file mode 100644
index e13215f22a7..00000000000
--- a/changelogs/unreleased/fix-deleting-project-again.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-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
deleted file mode 100644
index 61817027720..00000000000
--- a/changelogs/unreleased/fix-depr-warn.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: resolve deprecation warnings
-merge_request: 8855
-author: Adam Pahlevi
diff --git a/changelogs/unreleased/fix-gb-backwards-compatibility-coverage-ci-yml.yml b/changelogs/unreleased/fix-gb-backwards-compatibility-coverage-ci-yml.yml
deleted file mode 100644
index df7e3776700..00000000000
--- a/changelogs/unreleased/fix-gb-backwards-compatibility-coverage-ci-yml.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Preserve backward compatibility CI/CD and disallow setting `coverage` regexp in global context
-merge_request: 8981
-author:
diff --git a/changelogs/unreleased/fix-gb-pipeline-retry-builds-started.yml b/changelogs/unreleased/fix-gb-pipeline-retry-builds-started.yml
new file mode 100644
index 00000000000..49e243ca6bb
--- /dev/null
+++ b/changelogs/unreleased/fix-gb-pipeline-retry-builds-started.yml
@@ -0,0 +1,4 @@
+---
+title: Fix CI/CD pipeline retry and take stages order into account
+merge_request: 9021
+author:
diff --git a/changelogs/unreleased/fix-gb-pipeline-retry-cancel-buttons-consistency.yml b/changelogs/unreleased/fix-gb-pipeline-retry-cancel-buttons-consistency.yml
new file mode 100644
index 00000000000..d747e0e63a3
--- /dev/null
+++ b/changelogs/unreleased/fix-gb-pipeline-retry-cancel-buttons-consistency.yml
@@ -0,0 +1,4 @@
+---
+title: Fix pipeline retry and cancel buttons on pipeline details page
+merge_request: 9225
+author:
diff --git a/changelogs/unreleased/fix-guest-access-posting-to-notes.yml b/changelogs/unreleased/fix-guest-access-posting-to-notes.yml
deleted file mode 100644
index 81377c0c6f0..00000000000
--- a/changelogs/unreleased/fix-guest-access-posting-to-notes.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-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
deleted file mode 100644
index e34d895570b..00000000000
--- a/changelogs/unreleased/fix-import-encrypt-atts.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Ignore encrypted attributes in Import/Export
-merge_request:
-author:
diff --git a/changelogs/unreleased/fix-import-group-members.yml b/changelogs/unreleased/fix-import-group-members.yml
deleted file mode 100644
index fe580af31b3..00000000000
--- a/changelogs/unreleased/fix-import-group-members.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add ability to export project inherited group members to Import/Export
-merge_request: 8923
-author:
diff --git a/changelogs/unreleased/fix-job-to-pipeline-renaming.yml b/changelogs/unreleased/fix-job-to-pipeline-renaming.yml
deleted file mode 100644
index d5f34b4b25d..00000000000
--- a/changelogs/unreleased/fix-job-to-pipeline-renaming.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix job to pipeline renaming
-merge_request: 9147
-author:
diff --git a/changelogs/unreleased/fix-references-header-parsing.yml b/changelogs/unreleased/fix-references-header-parsing.yml
deleted file mode 100644
index b927279cdf4..00000000000
--- a/changelogs/unreleased/fix-references-header-parsing.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix reply by email without sub-addressing for some clients from
- Microsoft and Apple
-merge_request: 8620
-author:
diff --git a/changelogs/unreleased/fix-scroll-test.yml b/changelogs/unreleased/fix-scroll-test.yml
deleted file mode 100644
index e98ac755b88..00000000000
--- a/changelogs/unreleased/fix-scroll-test.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Change rspec test to guarantee window is resized before visiting page
-merge_request:
-author:
diff --git a/changelogs/unreleased/fix-users-deleting-public-deployment-keys.yml b/changelogs/unreleased/fix-users-deleting-public-deployment-keys.yml
deleted file mode 100644
index c9edd1de86c..00000000000
--- a/changelogs/unreleased/fix-users-deleting-public-deployment-keys.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Prevent users from deleting system deploy keys via the project deploy key API
-merge_request:
-author:
diff --git a/changelogs/unreleased/fix_broken_diff_discussions.yml b/changelogs/unreleased/fix_broken_diff_discussions.yml
deleted file mode 100644
index 4551212759f..00000000000
--- a/changelogs/unreleased/fix_broken_diff_discussions.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-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
deleted file mode 100644
index 1427e4e7624..00000000000
--- a/changelogs/unreleased/fwn-to-find-by-full-path.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: replace `find_with_namespace` with `find_by_full_path`
-merge_request: 8949
-author: Adam Pahlevi
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
deleted file mode 100644
index f60417d185e..00000000000
--- a/changelogs/unreleased/get-rid-of-water-from-notification_service_spec-to-make-it-DRY.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Make notification_service spec DRYer by making test reusable
-merge_request:
-author: YarNayar
diff --git a/changelogs/unreleased/gfm-autocomplete-fixes.yml b/changelogs/unreleased/gfm-autocomplete-fixes.yml
new file mode 100644
index 00000000000..737e2ad5234
--- /dev/null
+++ b/changelogs/unreleased/gfm-autocomplete-fixes.yml
@@ -0,0 +1,4 @@
+---
+title: Fix errors in slash commands matcher, add simple test coverage
+merge_request:
+author: YarNayar
diff --git a/changelogs/unreleased/git_to_html_redirection.yml b/changelogs/unreleased/git_to_html_redirection.yml
deleted file mode 100644
index b2959c02c07..00000000000
--- a/changelogs/unreleased/git_to_html_redirection.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Redirect http://someproject.git to http://someproject
-merge_request:
-author: blackst0ne
diff --git a/changelogs/unreleased/go-go-gadget-webpack.yml b/changelogs/unreleased/go-go-gadget-webpack.yml
deleted file mode 100644
index 7f372ccb428..00000000000
--- a/changelogs/unreleased/go-go-gadget-webpack.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-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
deleted file mode 100644
index c11c2d4ede1..00000000000
--- a/changelogs/unreleased/group-label-sidebar-link.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fixed group label links in issue/merge request sidebar
-merge_request:
-author:
diff --git a/changelogs/unreleased/group-memebrs-owner-level.yml b/changelogs/unreleased/group-memebrs-owner-level.yml
new file mode 100644
index 00000000000..ba77f38eb6d
--- /dev/null
+++ b/changelogs/unreleased/group-memebrs-owner-level.yml
@@ -0,0 +1,4 @@
+---
+title: Added option to update to owner for group members
+merge_request:
+author:
diff --git a/changelogs/unreleased/hardcode-title-system-note.yml b/changelogs/unreleased/hardcode-title-system-note.yml
deleted file mode 100644
index 1b0a63efa51..00000000000
--- a/changelogs/unreleased/hardcode-title-system-note.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Ensure autogenerated title does not cause failing spec
-merge_request: 8963
-author: brian m. carlson
diff --git a/changelogs/unreleased/improve-ci-example-php-doc.yml b/changelogs/unreleased/improve-ci-example-php-doc.yml
deleted file mode 100644
index 39a85e3d261..00000000000
--- a/changelogs/unreleased/improve-ci-example-php-doc.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-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
deleted file mode 100644
index 8ae3dfe079c..00000000000
--- a/changelogs/unreleased/improve-handleLocationHash-tests.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Improve gl.utils.handleLocationHash tests
-merge_request:
-author:
diff --git a/changelogs/unreleased/instrument-in-karma.yml b/changelogs/unreleased/instrument-in-karma.yml
new file mode 100644
index 00000000000..cfabf2569fe
--- /dev/null
+++ b/changelogs/unreleased/instrument-in-karma.yml
@@ -0,0 +1,4 @@
+---
+title: Move babel config for instanbul to karma config
+merge_request: 9286
+author: winniehell
diff --git a/changelogs/unreleased/issuable-sidebar-bug.yml b/changelogs/unreleased/issuable-sidebar-bug.yml
deleted file mode 100644
index 4086292eb89..00000000000
--- a/changelogs/unreleased/issuable-sidebar-bug.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fixed Issuable sidebar not closing on smaller/mobile sized screens
-merge_request:
-author:
diff --git a/changelogs/unreleased/issue-20428.yml b/changelogs/unreleased/issue-20428.yml
deleted file mode 100644
index 60da1c14702..00000000000
--- a/changelogs/unreleased/issue-20428.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-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-sidebar-empty-assignee.yml b/changelogs/unreleased/issue-sidebar-empty-assignee.yml
deleted file mode 100644
index 263af75b9e9..00000000000
--- a/changelogs/unreleased/issue-sidebar-empty-assignee.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Resets assignee dropdown when sidebar is open
-merge_request:
-author:
diff --git a/changelogs/unreleased/issue-tags-layout.yml b/changelogs/unreleased/issue-tags-layout.yml
new file mode 100644
index 00000000000..abf4a609932
--- /dev/null
+++ b/changelogs/unreleased/issue-tags-layout.yml
@@ -0,0 +1,4 @@
+---
+title: Fix 'New Tag' layout on Tags page
+merge_request:
+author: Robert Marcano
diff --git a/changelogs/unreleased/issue_19262.yml b/changelogs/unreleased/issue_19262.yml
deleted file mode 100644
index 5dea1493f23..00000000000
--- a/changelogs/unreleased/issue_19262.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Disallow system notes for closed issuables
-merge_request:
-author:
diff --git a/changelogs/unreleased/issue_23317.yml b/changelogs/unreleased/issue_23317.yml
deleted file mode 100644
index 788ae159f5e..00000000000
--- a/changelogs/unreleased/issue_23317.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix timezone on issue boards due date
-merge_request:
-author:
diff --git a/changelogs/unreleased/issue_24815.yml b/changelogs/unreleased/issue_24815.yml
new file mode 100644
index 00000000000..916e47d36a9
--- /dev/null
+++ b/changelogs/unreleased/issue_24815.yml
@@ -0,0 +1,4 @@
+---
+title: Fix issuable stale object error handler for js when updating tasklists
+merge_request:
+author:
diff --git a/changelogs/unreleased/issue_25112.yml b/changelogs/unreleased/issue_25112.yml
deleted file mode 100644
index c43d2732b9a..00000000000
--- a/changelogs/unreleased/issue_25112.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Disable invalid service templates
-merge_request:
-author:
diff --git a/changelogs/unreleased/issue_27211.yml b/changelogs/unreleased/issue_27211.yml
deleted file mode 100644
index ad48fec5d85..00000000000
--- a/changelogs/unreleased/issue_27211.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Remove unused js response from refs controller
-merge_request:
-author:
diff --git a/changelogs/unreleased/jej-pages-picked-from-ee.yml b/changelogs/unreleased/jej-pages-picked-from-ee.yml
deleted file mode 100644
index ee4a43a93db..00000000000
--- a/changelogs/unreleased/jej-pages-picked-from-ee.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Added GitLab Pages to CE
-merge_request: 8463
-author:
diff --git a/changelogs/unreleased/label-promotion.yml b/changelogs/unreleased/label-promotion.yml
deleted file mode 100644
index 2ab997bf420..00000000000
--- a/changelogs/unreleased/label-promotion.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: "Project labels can now be promoted to group labels"
-merge_request: 7242
-author: Olaf Tomalka
diff --git a/changelogs/unreleased/lfs-noauth-public-repo.yml b/changelogs/unreleased/lfs-noauth-public-repo.yml
deleted file mode 100644
index 60f62d7691b..00000000000
--- a/changelogs/unreleased/lfs-noauth-public-repo.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Support unauthenticated LFS object downloads for public projects
-merge_request: 8824
-author: Ben Boeckel
diff --git a/changelogs/unreleased/lnovy-gitlab-ce-empty-variables.yml b/changelogs/unreleased/lnovy-gitlab-ce-empty-variables.yml
new file mode 100644
index 00000000000..bd5db5ac7af
--- /dev/null
+++ b/changelogs/unreleased/lnovy-gitlab-ce-empty-variables.yml
@@ -0,0 +1,4 @@
+---
+title: 'UI: Allow a project variable to be set to an empty value'
+merge_request: 6044
+author: Lukáš Nový
diff --git a/changelogs/unreleased/markdown-plantuml.yml b/changelogs/unreleased/markdown-plantuml.yml
deleted file mode 100644
index c855f0cbcf7..00000000000
--- a/changelogs/unreleased/markdown-plantuml.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: PlantUML support for Markdown
-merge_request: 8588
-author: Horacio Sanson
diff --git a/changelogs/unreleased/merge-request-tabs-fixture.yml b/changelogs/unreleased/merge-request-tabs-fixture.yml
deleted file mode 100644
index 289cd7b604a..00000000000
--- a/changelogs/unreleased/merge-request-tabs-fixture.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Replace static fixture for merge_request_tabs_spec.js
-merge_request: 9172
-author: winniehell
diff --git a/changelogs/unreleased/misalinged-discussion-with-no-avatar-26854.yml b/changelogs/unreleased/misalinged-discussion-with-no-avatar-26854.yml
deleted file mode 100644
index f32b3aea3c8..00000000000
--- a/changelogs/unreleased/misalinged-discussion-with-no-avatar-26854.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: adds avatar for discussion note
-merge_request: 8734
-author:
diff --git a/changelogs/unreleased/mock-ci-service.yml b/changelogs/unreleased/mock-ci-service.yml
new file mode 100644
index 00000000000..24c6366177f
--- /dev/null
+++ b/changelogs/unreleased/mock-ci-service.yml
@@ -0,0 +1,4 @@
+---
+title: Add Mock CI service/integration for development
+merge_request:
+author:
diff --git a/changelogs/unreleased/moving-issue-with-two-list-labels.yml b/changelogs/unreleased/moving-issue-with-two-list-labels.yml
new file mode 100644
index 00000000000..d5ea81e3810
--- /dev/null
+++ b/changelogs/unreleased/moving-issue-with-two-list-labels.yml
@@ -0,0 +1,4 @@
+---
+title: Removes label when moving issue to another list that it is currently in
+merge_request:
+author:
diff --git a/changelogs/unreleased/mr-diff-comment-button.yml b/changelogs/unreleased/mr-diff-comment-button.yml
new file mode 100644
index 00000000000..1dc6ed1c495
--- /dev/null
+++ b/changelogs/unreleased/mr-diff-comment-button.yml
@@ -0,0 +1,4 @@
+---
+title: Improved diff comment button UX
+merge_request:
+author:
diff --git a/changelogs/unreleased/mr-tabs-container-offset.yml b/changelogs/unreleased/mr-tabs-container-offset.yml
deleted file mode 100644
index c5df8abfcf2..00000000000
--- a/changelogs/unreleased/mr-tabs-container-offset.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-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
deleted file mode 100644
index 5ce080b6912..00000000000
--- a/changelogs/unreleased/newline-eslint-rule.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Flag multiple empty lines in eslint, fix offenses.
-merge_request: 8137
-author:
diff --git a/changelogs/unreleased/no-sidebar-on-action-btn-click.yml b/changelogs/unreleased/no-sidebar-on-action-btn-click.yml
deleted file mode 100644
index 09e0b3a12d8..00000000000
--- a/changelogs/unreleased/no-sidebar-on-action-btn-click.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-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
deleted file mode 100644
index 6106c027360..00000000000
--- a/changelogs/unreleased/no_project_notes.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Support notes when a project is not specified (personal snippet notes)
-merge_request: 8468
-author:
diff --git a/changelogs/unreleased/only-create-unmergeable-todo-once.yml b/changelogs/unreleased/only-create-unmergeable-todo-once.yml
new file mode 100644
index 00000000000..e675ed945ad
--- /dev/null
+++ b/changelogs/unreleased/only-create-unmergeable-todo-once.yml
@@ -0,0 +1,4 @@
+---
+title: Only create unmergeable todos once when MR fails to merge
+merge_request:
+author:
diff --git a/changelogs/unreleased/only-yield-valid-reference-matches.yml b/changelogs/unreleased/only-yield-valid-reference-matches.yml
new file mode 100644
index 00000000000..95da3cc56fd
--- /dev/null
+++ b/changelogs/unreleased/only-yield-valid-reference-matches.yml
@@ -0,0 +1,4 @@
+---
+title: Only yield valid references in ReferenceFilter.references_in
+merge_request:
+author:
diff --git a/changelogs/unreleased/paginate-all-the-things.yml b/changelogs/unreleased/paginate-all-the-things.yml
new file mode 100644
index 00000000000..52f23ba52a9
--- /dev/null
+++ b/changelogs/unreleased/paginate-all-the-things.yml
@@ -0,0 +1,4 @@
+---
+title: 'API: Paginate all endpoints that return an array'
+merge_request: 8606
+author: Robert Schilling
diff --git a/changelogs/unreleased/pms-lowercase-system-notes.yml b/changelogs/unreleased/pms-lowercase-system-notes.yml
deleted file mode 100644
index c2fa1a7fad0..00000000000
--- a/changelogs/unreleased/pms-lowercase-system-notes.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Make all system notes lowercase
-merge_request: 8807
-author:
diff --git a/changelogs/unreleased/redesign-searchbar-admin-project-26794.yml b/changelogs/unreleased/redesign-searchbar-admin-project-26794.yml
deleted file mode 100644
index 547a7c6755c..00000000000
--- a/changelogs/unreleased/redesign-searchbar-admin-project-26794.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-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
deleted file mode 100644
index e0f7e11b6d1..00000000000
--- a/changelogs/unreleased/redirect-to-commit-when-only-commit-found.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-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/refresh-permissions-when-moving-projects.yml b/changelogs/unreleased/refresh-permissions-when-moving-projects.yml
deleted file mode 100644
index a94bcdaa9a3..00000000000
--- a/changelogs/unreleased/refresh-permissions-when-moving-projects.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-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
deleted file mode 100644
index 0877664aca4..00000000000
--- a/changelogs/unreleased/relative-url-assets.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: allow relative url change without recompiling frontend assets
-merge_request: 8831
-author:
diff --git a/changelogs/unreleased/remove-deploy-key-endpoint.yml b/changelogs/unreleased/remove-deploy-key-endpoint.yml
deleted file mode 100644
index 3ff69adb4d3..00000000000
--- a/changelogs/unreleased/remove-deploy-key-endpoint.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: 'API: Remove /projects/:id/keys/.. endpoints'
-merge_request: 8716
-author: Robert Schilling
diff --git a/changelogs/unreleased/remove-jquery-ui-plugins.yml b/changelogs/unreleased/remove-jquery-ui-plugins.yml
new file mode 100644
index 00000000000..c768f702ba2
--- /dev/null
+++ b/changelogs/unreleased/remove-jquery-ui-plugins.yml
@@ -0,0 +1,4 @@
+---
+title: Removed jQuery UI highlight & autocomplete
+merge_request:
+author:
diff --git a/changelogs/unreleased/remove-sidekiq-backup-ar-threads.yml b/changelogs/unreleased/remove-sidekiq-backup-ar-threads.yml
deleted file mode 100644
index f42aa6fae79..00000000000
--- a/changelogs/unreleased/remove-sidekiq-backup-ar-threads.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Don't use backup Active Record connections for Sidekiq
-merge_request:
-author:
diff --git a/changelogs/unreleased/rename-retry-failed-pipeline-to-retry.yml b/changelogs/unreleased/rename-retry-failed-pipeline-to-retry.yml
new file mode 100644
index 00000000000..b813127b1e6
--- /dev/null
+++ b/changelogs/unreleased/rename-retry-failed-pipeline-to-retry.yml
@@ -0,0 +1,4 @@
+---
+title: Rename retry failed button on pipeline page to just retry
+merge_request:
+author:
diff --git a/changelogs/unreleased/route-map.yml b/changelogs/unreleased/route-map.yml
deleted file mode 100644
index 9b6df0c54af..00000000000
--- a/changelogs/unreleased/route-map.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add 'View on [env]' link to blobs and individual files in diffs
-merge_request: 8867
-author:
diff --git a/changelogs/unreleased/rs-warden-blocked-users.yml b/changelogs/unreleased/rs-warden-blocked-users.yml
deleted file mode 100644
index c0c23fb6f11..00000000000
--- a/changelogs/unreleased/rs-warden-blocked-users.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Don't perform Devise trackable updates on blocked User records
-merge_request: 8915
-author:
diff --git a/changelogs/unreleased/rss-btn-alignment-fix.yml b/changelogs/unreleased/rss-btn-alignment-fix.yml
new file mode 100644
index 00000000000..c8f57ec0b7c
--- /dev/null
+++ b/changelogs/unreleased/rss-btn-alignment-fix.yml
@@ -0,0 +1,4 @@
+---
+title: Fixed RSS button alignment on activity pages
+merge_request:
+author:
diff --git a/changelogs/unreleased/seed-abuse-reports.yml b/changelogs/unreleased/seed-abuse-reports.yml
new file mode 100644
index 00000000000..6fbcb81ae3f
--- /dev/null
+++ b/changelogs/unreleased/seed-abuse-reports.yml
@@ -0,0 +1,4 @@
+---
+title: Seed abuse reports for development
+merge_request:
+author:
diff --git a/changelogs/unreleased/sh-add-index-to-ci-trigger-requests.yml b/changelogs/unreleased/sh-add-index-to-ci-trigger-requests.yml
deleted file mode 100644
index bab76812a17..00000000000
--- a/changelogs/unreleased/sh-add-index-to-ci-trigger-requests.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add index to ci_trigger_requests for commit_id
-merge_request:
-author:
diff --git a/changelogs/unreleased/sh-add-labels-index.yml b/changelogs/unreleased/sh-add-labels-index.yml
deleted file mode 100644
index b948a75081c..00000000000
--- a/changelogs/unreleased/sh-add-labels-index.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add indices to improve loading of labels page
-merge_request:
-author:
diff --git a/changelogs/unreleased/sh-bump-hashie-to-3-5-5.yml b/changelogs/unreleased/sh-bump-hashie-to-3-5-5.yml
new file mode 100644
index 00000000000..57f1474093a
--- /dev/null
+++ b/changelogs/unreleased/sh-bump-hashie-to-3-5-5.yml
@@ -0,0 +1,4 @@
+---
+title: Bump Hashie to 3.5.5 and omniauth to 1.4.2 to eliminate warning noise
+merge_request:
+author:
diff --git a/changelogs/unreleased/sh-delete-user-permission-check.yml b/changelogs/unreleased/sh-delete-user-permission-check.yml
new file mode 100644
index 00000000000..c0e79aae2a8
--- /dev/null
+++ b/changelogs/unreleased/sh-delete-user-permission-check.yml
@@ -0,0 +1,4 @@
+---
+title: Add user deletion permission check in `Users::DestroyService`
+merge_request:
+author:
diff --git a/changelogs/unreleased/slash-commands-typo.yml b/changelogs/unreleased/slash-commands-typo.yml
deleted file mode 100644
index e6ffb94bd08..00000000000
--- a/changelogs/unreleased/slash-commands-typo.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fixed "substract" typo on /help/user/project/slash_commands
-merge_request: 8976
-author: Jason Aquino
diff --git a/changelogs/unreleased/small-screen-fullscreen-button.yml b/changelogs/unreleased/small-screen-fullscreen-button.yml
deleted file mode 100644
index f4c269bc473..00000000000
--- a/changelogs/unreleased/small-screen-fullscreen-button.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Display fullscreen button on small screens
-merge_request: 5302
-author: winniehell
diff --git a/changelogs/unreleased/snippets-search-performance.yml b/changelogs/unreleased/snippets-search-performance.yml
deleted file mode 100644
index 2895478abfd..00000000000
--- a/changelogs/unreleased/snippets-search-performance.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Reduced query count for snippet search
-merge_request:
-author:
diff --git a/changelogs/unreleased/snippets-search.yml b/changelogs/unreleased/snippets-search.yml
new file mode 100644
index 00000000000..00cf34f4a48
--- /dev/null
+++ b/changelogs/unreleased/snippets-search.yml
@@ -0,0 +1,4 @@
+---
+title: Fix snippets search result spacing
+merge_request:
+author:
diff --git a/changelogs/unreleased/ssh-key-paste.yml b/changelogs/unreleased/ssh-key-paste.yml
new file mode 100644
index 00000000000..1e34ef60f6e
--- /dev/null
+++ b/changelogs/unreleased/ssh-key-paste.yml
@@ -0,0 +1,4 @@
+---
+title: SSH key field updates title after pasting key
+merge_request:
+author:
diff --git a/changelogs/unreleased/task_list_refactor.yml b/changelogs/unreleased/task_list_refactor.yml
new file mode 100644
index 00000000000..68942dadaa8
--- /dev/null
+++ b/changelogs/unreleased/task_list_refactor.yml
@@ -0,0 +1,4 @@
+---
+title: Deduplicate markdown task lists
+merge_request:
+author:
diff --git a/changelogs/unreleased/tc-only-mr-button-if-allowed.yml b/changelogs/unreleased/tc-only-mr-button-if-allowed.yml
deleted file mode 100644
index a7f5dcb560c..00000000000
--- a/changelogs/unreleased/tc-only-mr-button-if-allowed.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-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
deleted file mode 100644
index db1e66770d1..00000000000
--- a/changelogs/unreleased/terminal-max-session-time.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Introduce maximum session time for terminal websocket connection
-merge_request: 8413
-author:
diff --git a/changelogs/unreleased/unified-member-api-response.yml b/changelogs/unreleased/unified-member-api-response.yml
new file mode 100644
index 00000000000..0a60b4d46a3
--- /dev/null
+++ b/changelogs/unreleased/unified-member-api-response.yml
@@ -0,0 +1,4 @@
+---
+title: 'API: Return 400 for all validation erros in the mebers API'
+merge_request: 9523
+author: Robert Schilling
diff --git a/changelogs/unreleased/update-vue-2-1.yml b/changelogs/unreleased/update-vue-2-1.yml
new file mode 100644
index 00000000000..acc42bf00b1
--- /dev/null
+++ b/changelogs/unreleased/update-vue-2-1.yml
@@ -0,0 +1,4 @@
+---
+title: update Vue to v2.1.10
+merge_request: 9386
+author:
diff --git a/changelogs/unreleased/upgrade-babel-v6.yml b/changelogs/unreleased/upgrade-babel-v6.yml
deleted file mode 100644
index 55f9b3e407c..00000000000
--- a/changelogs/unreleased/upgrade-babel-v6.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: upgrade babel 5.8.x to babel 6.22.x
-merge_request: 9072
-author:
diff --git a/changelogs/unreleased/upgrade-omniauth.yml b/changelogs/unreleased/upgrade-omniauth.yml
deleted file mode 100644
index 7e0334566dc..00000000000
--- a/changelogs/unreleased/upgrade-omniauth.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Upgrade omniauth gem to 1.3.2
-merge_request:
-author:
diff --git a/changelogs/unreleased/upgrade-webpack-v2-2.yml b/changelogs/unreleased/upgrade-webpack-v2-2.yml
deleted file mode 100644
index 6a49859d68c..00000000000
--- a/changelogs/unreleased/upgrade-webpack-v2-2.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: upgrade to webpack v2.2
-merge_request: 9078
-author:
diff --git a/changelogs/unreleased/user-callouts.yml b/changelogs/unreleased/user-callouts.yml
new file mode 100644
index 00000000000..f6ce06a3d8f
--- /dev/null
+++ b/changelogs/unreleased/user-callouts.yml
@@ -0,0 +1,4 @@
+---
+title: Added user callouts to the projects dashboard and user profile
+merge_request:
+author:
diff --git a/changelogs/unreleased/wip-mr-from-commits.yml b/changelogs/unreleased/wip-mr-from-commits.yml
deleted file mode 100644
index 0083798be08..00000000000
--- a/changelogs/unreleased/wip-mr-from-commits.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Mark MR as WIP when pushing WIP commits
-merge_request: 8124
-author: Jurre Stender @jurre
diff --git a/changelogs/unreleased/zj-format-chat-messages.yml b/changelogs/unreleased/zj-format-chat-messages.yml
deleted file mode 100644
index 2494884f5c9..00000000000
--- a/changelogs/unreleased/zj-format-chat-messages.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Reformat messages ChatOps
-merge_request: 8528
-author:
diff --git a/changelogs/unreleased/zj-remove-deprecated-ci-service.yml b/changelogs/unreleased/zj-remove-deprecated-ci-service.yml
deleted file mode 100644
index 044f4ae627d..00000000000
--- a/changelogs/unreleased/zj-remove-deprecated-ci-service.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Remove deprecated GitlabCiService
-merge_request:
-author:
diff --git a/changelogs/unreleased/zj-requeue-pending-delete.yml b/changelogs/unreleased/zj-requeue-pending-delete.yml
deleted file mode 100644
index 464c5948f8c..00000000000
--- a/changelogs/unreleased/zj-requeue-pending-delete.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Requeue pending deletion projects
-merge_request:
-author:
diff --git a/config/dependency_decisions.yml b/config/dependency_decisions.yml
index 7336d7c842a..072ed8a3864 100644
--- a/config/dependency_decisions.yml
+++ b/config/dependency_decisions.yml
@@ -320,3 +320,9 @@
:why: https://github.com/shinnn/spdx-license-ids/blob/v1.2.2/LICENSE
:versions: []
:when: 2017-02-08 22:35:00.225232000 Z
+- - :approve
+ - opener
+ - :who: Mike Greiling
+ :why: https://github.com/domenic/opener/blob/1.4.3/LICENSE.txt
+ :versions: []
+ :when: 2017-02-21 22:33:41.729629000 Z
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index cc1af77a1de..a82ff605a70 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -76,14 +76,6 @@ production: &base
# default_can_create_group: false # default: true
# username_changing_enabled: false # default: true - User can change her username/namespace
- ## Default theme ID
- ## 1 - Graphite
- ## 2 - Charcoal
- ## 3 - Green
- ## 4 - Gray
- ## 5 - Violet
- ## 6 - Blue
- # default_theme: 2 # default: 2
## Automatic issue closing
# If a commit message matches this regular expression, all issues referenced from the matched text will be closed.
@@ -611,4 +603,4 @@ test:
admin_group: ''
staging:
- <<: *base \ No newline at end of file
+ <<: *base
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index ab59394cb0c..c64ae15fa92 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -14,12 +14,15 @@ class Settings < Settingslogic
end
def build_gitlab_ci_url
- if on_standard_port?(gitlab)
- custom_port = nil
- else
- custom_port = ":#{gitlab.port}"
- end
- [ gitlab.protocol,
+ custom_port =
+ if on_standard_port?(gitlab)
+ nil
+ else
+ ":#{gitlab.port}"
+ end
+
+ [
+ gitlab.protocol,
"://",
gitlab.host,
custom_port,
@@ -80,7 +83,9 @@ class Settings < Settingslogic
def base_url(config)
custom_port = on_standard_port?(config) ? nil : ":#{config.port}"
- [ config.protocol,
+
+ [
+ config.protocol,
"://",
config.host,
custom_port
@@ -160,15 +165,16 @@ if github_settings
github_settings["args"] ||= Settingslogic.new({})
- if github_settings["url"].include?(github_default_url)
- github_settings["args"]["client_options"] = OmniAuth::Strategies::GitHub.default_options[:client_options]
- else
- github_settings["args"]["client_options"] = {
- "site" => File.join(github_settings["url"], "api/v3"),
- "authorize_url" => File.join(github_settings["url"], "login/oauth/authorize"),
- "token_url" => File.join(github_settings["url"], "login/oauth/access_token")
- }
- end
+ github_settings["args"]["client_options"] =
+ if github_settings["url"].include?(github_default_url)
+ OmniAuth::Strategies::GitHub.default_options[:client_options]
+ else
+ {
+ "site" => File.join(github_settings["url"], "api/v3"),
+ "authorize_url" => File.join(github_settings["url"], "login/oauth/authorize"),
+ "token_url" => File.join(github_settings["url"], "login/oauth/access_token")
+ }
+ end
end
Settings['shared'] ||= Settingslogic.new({})
@@ -183,7 +189,6 @@ Settings['gitlab'] ||= Settingslogic.new({})
Settings.gitlab['default_projects_limit'] ||= 10
Settings.gitlab['default_branch_protection'] ||= 2
Settings.gitlab['default_can_create_group'] = true if Settings.gitlab['default_can_create_group'].nil?
-Settings.gitlab['default_theme'] = Gitlab::Themes::APPLICATION_DEFAULT if Settings.gitlab['default_theme'].nil?
Settings.gitlab['host'] ||= ENV['GITLAB_HOST'] || 'localhost'
Settings.gitlab['ssh_host'] ||= Settings.gitlab.host
Settings.gitlab['https'] = false if Settings.gitlab['https'].nil?
diff --git a/config/initializers/4_ci_app.rb b/config/initializers/4_ci_app.rb
deleted file mode 100644
index d252e403102..00000000000
--- a/config/initializers/4_ci_app.rb
+++ /dev/null
@@ -1,8 +0,0 @@
-module GitlabCi
- VERSION = Gitlab::VERSION
- REVISION = Gitlab::REVISION
-
- def self.config
- Settings
- end
-end
diff --git a/config/initializers/acts_as_taggable.rb b/config/initializers/acts_as_taggable.rb
new file mode 100644
index 00000000000..c564c0cab11
--- /dev/null
+++ b/config/initializers/acts_as_taggable.rb
@@ -0,0 +1,5 @@
+ActsAsTaggableOn.strict_case_match = true
+
+# tags_counter enables caching count of tags which results in an update whenever a tag is added or removed
+# since the count is not used anywhere its better performance wise to disable this cache
+ActsAsTaggableOn.tags_counter = false
diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb
index a8afc36fc78..3b1317030bc 100644
--- a/config/initializers/devise.rb
+++ b/config/initializers/devise.rb
@@ -24,7 +24,7 @@ Devise.setup do |config|
# session. If you need permissions, you should implement that in a before filter.
# You can also supply a hash where the value is a boolean determining whether
# or not authentication should be aborted when the value is not present.
- config.authentication_keys = [ :login ]
+ config.authentication_keys = [:login]
# Configure parameters from the request object used for authentication. Each entry
# given should be a request method and it will automatically be passed to the
@@ -36,12 +36,12 @@ Devise.setup do |config|
# Configure which authentication keys should be case-insensitive.
# These keys will be downcased upon creating or modifying a user and when used
# to authenticate or find a user. Default is :email.
- config.case_insensitive_keys = [ :email ]
+ config.case_insensitive_keys = [:email]
# Configure which authentication keys should have whitespace stripped.
# These keys will have whitespace before and after removed upon creating or
# modifying a user and when used to authenticate or find a user. Default is :email.
- config.strip_whitespace_keys = [ :email ]
+ config.strip_whitespace_keys = [:email]
# Tell if authentication through request.params is enabled. True by default.
# config.params_authenticatable = true
@@ -124,7 +124,7 @@ Devise.setup do |config|
config.lock_strategy = :failed_attempts
# Defines which key will be used when locking and unlocking an account
- config.unlock_keys = [ :email ]
+ config.unlock_keys = [:email]
# Defines which strategy will be used to unlock an account.
# :email = Sends an unlock link to the user email
@@ -240,6 +240,17 @@ Devise.setup do |config|
true
end
end
+ if provider['name'] == 'authentiq'
+ provider['args'][:remote_sign_out_handler] = lambda do |request|
+ authentiq_session = request.params['sid']
+ if Gitlab::OAuth::Session.valid?(:authentiq, authentiq_session)
+ Gitlab::OAuth::Session.destroy(:authentiq, authentiq_session)
+ true
+ else
+ false
+ end
+ end
+ end
if provider['name'] == 'shibboleth'
provider['args'][:fail_with_empty_uid] = true
diff --git a/config/initializers/gollum.rb b/config/initializers/gollum.rb
index 703f24f93b2..1ebe3c7a742 100644
--- a/config/initializers/gollum.rb
+++ b/config/initializers/gollum.rb
@@ -1,5 +1,5 @@
module Gollum
- GIT_ADAPTER = "rugged"
+ GIT_ADAPTER = "rugged".freeze
end
require "gollum-lib"
diff --git a/config/initializers/health_check.rb b/config/initializers/health_check.rb
index 4c91a61fb4a..959daa93f78 100644
--- a/config/initializers/health_check.rb
+++ b/config/initializers/health_check.rb
@@ -1,4 +1,4 @@
HealthCheck.setup do |config|
- config.standard_checks = ['database', 'migrations', 'cache']
- config.full_checks = ['database', 'migrations', 'cache']
+ config.standard_checks = %w(database migrations cache)
+ config.full_checks = %w(database migrations cache)
end
diff --git a/config/initializers/metrics.rb b/config/initializers/metrics.rb
index e0702e06cc9..a1517e6afc8 100644
--- a/config/initializers/metrics.rb
+++ b/config/initializers/metrics.rb
@@ -20,13 +20,13 @@ def instrument_classes(instrumentation)
# 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']
+ %w(app finders) => %w(app finders),
+ %w(app mailers emails) => %w(app mailers),
+ %w(app services **) => %w(app services),
+ %w(lib gitlab conflicts) => ['lib'],
+ %w(lib gitlab diff) => ['lib'],
+ %w(lib gitlab email message) => ['lib'],
+ %w(lib gitlab checks) => ['lib']
}
paths_to_instrument.each do |(path, prefix)|
diff --git a/config/initializers/mysql_ignore_postgresql_options.rb b/config/initializers/mysql_ignore_postgresql_options.rb
index 835f3ec5574..9a569be7674 100644
--- a/config/initializers/mysql_ignore_postgresql_options.rb
+++ b/config/initializers/mysql_ignore_postgresql_options.rb
@@ -31,7 +31,7 @@ if defined?(ActiveRecord::ConnectionAdapters::Mysql2Adapter)
end
def add_index_options(table_name, column_name, options = {})
- if options[:using] and options[:using] == :gin
+ if options[:using] && options[:using] == :gin
options = options.dup
options.delete(:using)
end
diff --git a/config/initializers/rack_lineprof.rb b/config/initializers/rack_lineprof.rb
index 22e77a32c61..f7172fce9bc 100644
--- a/config/initializers/rack_lineprof.rb
+++ b/config/initializers/rack_lineprof.rb
@@ -1,7 +1,7 @@
# The default colors of rack-lineprof can be very hard to look at in terminals
# with darker backgrounds. This patch tweaks the colors a bit so the output is
# actually readable.
-if Rails.env.development? and RUBY_ENGINE == 'ruby' and ENV['ENABLE_LINEPROF']
+if Rails.env.development? && RUBY_ENGINE == 'ruby' && ENV['ENABLE_LINEPROF']
Rails.application.config.middleware.use(Rack::Lineprof)
module Rack
diff --git a/config/initializers/trusted_proxies.rb b/config/initializers/trusted_proxies.rb
index cd869657c53..fc4f02453d7 100644
--- a/config/initializers/trusted_proxies.rb
+++ b/config/initializers/trusted_proxies.rb
@@ -21,4 +21,4 @@ gitlab_trusted_proxies = Array(Gitlab.config.gitlab.trusted_proxies).map do |pro
end.compact
Rails.application.config.action_dispatch.trusted_proxies = (
- [ '127.0.0.1', '::1' ] + gitlab_trusted_proxies)
+ ['127.0.0.1', '::1'] + gitlab_trusted_proxies)
diff --git a/config/initializers/workhorse_multipart.rb b/config/initializers/workhorse_multipart.rb
index 84d809741c4..064e5964f09 100644
--- a/config/initializers/workhorse_multipart.rb
+++ b/config/initializers/workhorse_multipart.rb
@@ -10,7 +10,7 @@ end
#
module Gitlab
module StrongParameterScalars
- GITLAB_PERMITTED_SCALAR_TYPES = [::UploadedFile]
+ GITLAB_PERMITTED_SCALAR_TYPES = [::UploadedFile].freeze
def permitted_scalar?(value)
super || GITLAB_PERMITTED_SCALAR_TYPES.any? { |type| value.is_a?(type) }
diff --git a/config/karma.config.js b/config/karma.config.js
index 5b472780aed..2f3cc932413 100644
--- a/config/karma.config.js
+++ b/config/karma.config.js
@@ -2,6 +2,17 @@ var path = require('path');
var webpackConfig = require('./webpack.config.js');
var ROOT_PATH = path.resolve(__dirname, '..');
+// add coverage instrumentation to babel config
+if (webpackConfig && webpackConfig.module && webpackConfig.module.rules) {
+ var babelConfig = webpackConfig.module.rules.find(function (rule) {
+ return rule.loader === 'babel-loader';
+ });
+
+ babelConfig.options = babelConfig.options || {};
+ babelConfig.options.plugins = babelConfig.options.plugins || [];
+ babelConfig.options.plugins.push('istanbul');
+}
+
// Karma configuration
module.exports = function(config) {
var progressReporter = process.env.CI ? 'mocha' : 'progress';
diff --git a/config/mail_room.yml b/config/mail_room.yml
index 774c5350a45..88d93d4bc6b 100644
--- a/config/mail_room.yml
+++ b/config/mail_room.yml
@@ -1,9 +1,6 @@
-# If you change this file in a Merge Request, please also create
-# a Merge Request on https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests
-#
:mailboxes:
<%
- require_relative "lib/gitlab/mail_room" unless defined?(Gitlab::MailRoom)
+ require_relative "../lib/gitlab/mail_room" unless defined?(Gitlab::MailRoom)
config = Gitlab::MailRoom.config
if Gitlab::MailRoom.enabled?
diff --git a/config/routes/dashboard.rb b/config/routes/dashboard.rb
index fb20c63bc63..adc3ad207cc 100644
--- a/config/routes/dashboard.rb
+++ b/config/routes/dashboard.rb
@@ -14,6 +14,9 @@ resource :dashboard, controller: 'dashboard', only: [] do
collection do
delete :destroy_all
end
+ member do
+ patch :restore
+ end
end
resources :projects, only: [:index] do
diff --git a/config/routes/sidekiq.rb b/config/routes/sidekiq.rb
index d3e6bc4c292..0fa23f2b3d0 100644
--- a/config/routes/sidekiq.rb
+++ b/config/routes/sidekiq.rb
@@ -1,4 +1,4 @@
-constraint = lambda { |request| request.env['warden'].authenticate? and request.env['warden'].user.admin? }
+constraint = lambda { |request| request.env['warden'].authenticate? && request.env['warden'].user.admin? }
constraints constraint do
mount Sidekiq::Web, at: '/admin/sidekiq', as: :sidekiq
end
diff --git a/config/routes/wiki.rb b/config/routes/wiki.rb
index dad746d59a1..a6b3f5d4693 100644
--- a/config/routes/wiki.rb
+++ b/config/routes/wiki.rb
@@ -1,4 +1,4 @@
-WIKI_SLUG_ID = { id: /\S+/ } unless defined? WIKI_SLUG_ID
+WIKI_SLUG_ID = { id: /\S+/ }.freeze unless defined? WIKI_SLUG_ID
scope(controller: :wikis) do
scope(path: 'wikis', as: :wikis) do
diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
index 56bf4e6b1de..97620cc9c7f 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -21,7 +21,7 @@
- [post_receive, 5]
- [merge, 5]
- [update_merge_requests, 3]
- - [process_commit, 2]
+ - [process_commit, 3]
- [new_note, 2]
- [build, 2]
- [pipeline, 2]
diff --git a/config/webpack.config.js b/config/webpack.config.js
index 8d981c76c1a..a71ec0c5f52 100644
--- a/config/webpack.config.js
+++ b/config/webpack.config.js
@@ -5,11 +5,14 @@ var path = require('path');
var webpack = require('webpack');
var StatsPlugin = require('stats-webpack-plugin');
var CompressionPlugin = require('compression-webpack-plugin');
+var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
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 DEV_SERVER_LIVERELOAD = process.env.DEV_SERVER_LIVERELOAD !== 'false';
+var WEBPACK_REPORT = process.env.WEBPACK_REPORT;
var config = {
context: path.join(ROOT_PATH, 'app/assets/javascripts'),
@@ -55,18 +58,11 @@ var config = {
exclude: /(node_modules|vendor\/assets)/,
loader: 'babel-loader',
options: {
- plugins: IS_PRODUCTION ? [] : ['istanbul'],
presets: [
["es2015", {"modules": false}],
'stage-2'
]
}
- },
- {
- test: /\.(js|es6)$/,
- exclude: /node_modules/,
- loader: 'imports-loader',
- options: 'this=>window'
}
]
},
@@ -81,9 +77,6 @@ var config = {
modules: false,
assets: true
}),
- new CompressionPlugin({
- asset: '[path].gz[query]',
- }),
new webpack.IgnorePlugin(/moment/, /pikaday/),
],
@@ -94,8 +87,7 @@ var config = {
'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'
+ 'vue$': 'vue/dist/vue.common.js',
}
}
}
@@ -103,7 +95,7 @@ var config = {
if (IS_PRODUCTION) {
config.devtool = 'source-map';
config.plugins.push(
- new webpack.NoErrorsPlugin(),
+ new webpack.NoEmitOnErrorsPlugin(),
new webpack.LoaderOptionsPlugin({
minimize: true,
debug: false
@@ -113,6 +105,9 @@ if (IS_PRODUCTION) {
}),
new webpack.DefinePlugin({
'process.env': { NODE_ENV: JSON.stringify('production') }
+ }),
+ new CompressionPlugin({
+ asset: '[path].gz[query]',
})
);
}
@@ -122,8 +117,21 @@ if (IS_DEV_SERVER) {
port: DEV_SERVER_PORT,
headers: { 'Access-Control-Allow-Origin': '*' },
stats: 'errors-only',
+ inline: DEV_SERVER_LIVERELOAD
};
config.output.publicPath = '//localhost:' + DEV_SERVER_PORT + config.output.publicPath;
}
+if (WEBPACK_REPORT) {
+ config.plugins.push(
+ new BundleAnalyzerPlugin({
+ analyzerMode: 'static',
+ generateStatsFile: true,
+ openAnalyzer: false,
+ reportFilename: path.join(ROOT_PATH, 'webpack-report/index.html'),
+ statsFilename: path.join(ROOT_PATH, 'webpack-report/stats.json'),
+ })
+ );
+}
+
module.exports = config;
diff --git a/db/fixtures/development/17_cycle_analytics.rb b/db/fixtures/development/17_cycle_analytics.rb
index 747901dd634..aea0a72b633 100644
--- a/db/fixtures/development/17_cycle_analytics.rb
+++ b/db/fixtures/development/17_cycle_analytics.rb
@@ -155,17 +155,9 @@ class Gitlab::Seeder::CycleAnalytics
issue.project.repository.add_branch(@user, branch_name, 'master')
- options = {
- committer: issue.project.repository.user_to_committer(@user),
- author: issue.project.repository.user_to_committer(@user),
- commit: { message: "Commit for ##{issue.iid}", branch: branch_name, update_ref: true },
- file: { content: "content", path: filename, update: false }
- }
-
- commit_sha = Gitlab::Git::Blob.commit(issue.project.repository, options)
+ commit_sha = issue.project.repository.create_file(@user, filename, "content", options, message: "Commit for ##{issue.iid}", branch_name: branch_name)
issue.project.repository.commit(commit_sha)
-
GitPushService.new(issue.project,
@user,
oldrev: issue.project.repository.commit("master").sha,
diff --git a/db/fixtures/development/18_abuse_reports.rb b/db/fixtures/development/18_abuse_reports.rb
new file mode 100644
index 00000000000..8618d10387a
--- /dev/null
+++ b/db/fixtures/development/18_abuse_reports.rb
@@ -0,0 +1,5 @@
+require 'factory_girl_rails'
+
+(AbuseReport.default_per_page + 3).times do
+ FactoryGirl.create(:abuse_report)
+end
diff --git a/db/fixtures/development/19_nested_groups.rb b/db/fixtures/development/19_nested_groups.rb
new file mode 100644
index 00000000000..d8dddc3fee9
--- /dev/null
+++ b/db/fixtures/development/19_nested_groups.rb
@@ -0,0 +1,69 @@
+require './spec/support/sidekiq'
+
+def create_group_with_parents(user, full_path)
+ parent_path = nil
+ group = nil
+
+ until full_path.blank?
+ path, _, full_path = full_path.partition('/')
+
+ if parent_path
+ parent = Group.find_by_full_path(parent_path)
+
+ parent_path += '/'
+ parent_path += path
+
+ group = Groups::CreateService.new(user, path: path, parent_id: parent.id).execute
+ else
+ parent_path = path
+
+ group = Group.find_by_full_path(parent_path) ||
+ Groups::CreateService.new(user, path: path).execute
+ end
+ end
+
+ group
+end
+
+Sidekiq::Testing.inline! do
+ Gitlab::Seeder.quiet do
+ project_urls = [
+ 'https://android.googlesource.com/platform/hardware/broadcom/libbt.git',
+ 'https://android.googlesource.com/platform/hardware/broadcom/wlan.git',
+ 'https://android.googlesource.com/platform/hardware/bsp/bootloader/intel/edison-u-boot.git',
+ 'https://android.googlesource.com/platform/hardware/bsp/broadcom.git',
+ 'https://android.googlesource.com/platform/hardware/bsp/freescale.git',
+ 'https://android.googlesource.com/platform/hardware/bsp/imagination.git',
+ 'https://android.googlesource.com/platform/hardware/bsp/intel.git',
+ 'https://android.googlesource.com/platform/hardware/bsp/kernel/common/v4.1.git',
+ 'https://android.googlesource.com/platform/hardware/bsp/kernel/common/v4.4.git'
+ ]
+
+ user = User.admins.first
+
+ project_urls.each_with_index do |url, i|
+ full_path = url.sub('https://android.googlesource.com/', '')
+ full_path = full_path.sub(/\.git\z/, '')
+ full_path, _, project_path = full_path.rpartition('/')
+ group = Group.find_by_full_path(full_path) || create_group_with_parents(user, full_path)
+
+ params = {
+ import_url: url,
+ namespace_id: group.id,
+ path: project_path,
+ name: project_path,
+ description: FFaker::Lorem.sentence,
+ visibility_level: Gitlab::VisibilityLevel.values.sample
+ }
+
+ project = Projects::CreateService.new(user, params).execute
+ project.send(:_run_after_commit_queue)
+
+ if project.valid?
+ print '.'
+ else
+ print 'F'
+ end
+ end
+ end
+end
diff --git a/db/migrate/20160610201627_migrate_users_notification_level.rb b/db/migrate/20160610201627_migrate_users_notification_level.rb
index 760b766828e..cd8b505de9f 100644
--- a/db/migrate/20160610201627_migrate_users_notification_level.rb
+++ b/db/migrate/20160610201627_migrate_users_notification_level.rb
@@ -1,7 +1,11 @@
class MigrateUsersNotificationLevel < ActiveRecord::Migration
+ DOWNTIME = false
+
# Migrates only users who changed their default notification level :participating
# creating a new record on notification settings table
+ DOWNTIME = false
+
def up
execute(%Q{
INSERT INTO notification_settings
diff --git a/db/migrate/20160829114652_add_markdown_cache_columns.rb b/db/migrate/20160829114652_add_markdown_cache_columns.rb
index 8753e55e058..9cb44dfa9f9 100644
--- a/db/migrate/20160829114652_add_markdown_cache_columns.rb
+++ b/db/migrate/20160829114652_add_markdown_cache_columns.rb
@@ -26,7 +26,7 @@ class AddMarkdownCacheColumns < ActiveRecord::Migration
projects: [:description],
releases: [:description],
snippets: [:title, :content],
- }
+ }.freeze
def change
COLUMNS.each do |table, columns|
diff --git a/db/migrate/20160831214543_migrate_project_features.rb b/db/migrate/20160831214543_migrate_project_features.rb
index 93f9821bc76..79a5fb29d64 100644
--- a/db/migrate/20160831214543_migrate_project_features.rb
+++ b/db/migrate/20160831214543_migrate_project_features.rb
@@ -3,7 +3,7 @@ class MigrateProjectFeatures < ActiveRecord::Migration
DOWNTIME = true
DOWNTIME_REASON =
- <<-EOT
+ <<-EOT.freeze
Migrating issues_enabled, merge_requests_enabled, wiki_enabled, builds_enabled, snippets_enabled fields from projects to
a new table called project_features.
EOT
diff --git a/db/migrate/20161019190736_migrate_sidekiq_queues_from_default.rb b/db/migrate/20161019190736_migrate_sidekiq_queues_from_default.rb
index e875213ab96..9f502a8df73 100644
--- a/db/migrate/20161019190736_migrate_sidekiq_queues_from_default.rb
+++ b/db/migrate/20161019190736_migrate_sidekiq_queues_from_default.rb
@@ -71,7 +71,7 @@ class MigrateSidekiqQueuesFromDefault < ActiveRecord::Migration
'StuckCiBuildsWorker' => :cronjob,
'UpdateMergeRequestsWorker' => :update_merge_requests
}
- }
+ }.freeze
def up
Sidekiq.redis do |redis|
@@ -93,7 +93,7 @@ class MigrateSidekiqQueuesFromDefault < ActiveRecord::Migration
def migrate_from_queue(redis, queue, job_mapping)
while job = redis.lpop("queue:#{queue}")
- payload = JSON.load(job)
+ payload = JSON.parse(job)
new_queue = job_mapping[payload['class']]
# If we have no target queue to migrate to we're probably dealing with
diff --git a/db/migrate/20161024042317_migrate_mailroom_queue_from_default.rb b/db/migrate/20161024042317_migrate_mailroom_queue_from_default.rb
index 06d07bdb835..fc2e4c12b30 100644
--- a/db/migrate/20161024042317_migrate_mailroom_queue_from_default.rb
+++ b/db/migrate/20161024042317_migrate_mailroom_queue_from_default.rb
@@ -25,7 +25,7 @@ class MigrateMailroomQueueFromDefault < ActiveRecord::Migration
incoming_email: {
'EmailReceiverWorker' => :email_receiver
}
- }
+ }.freeze
def up
Sidekiq.redis do |redis|
@@ -47,7 +47,7 @@ class MigrateMailroomQueueFromDefault < ActiveRecord::Migration
def migrate_from_queue(redis, queue, job_mapping)
while job = redis.lpop("queue:#{queue}")
- payload = JSON.load(job)
+ payload = JSON.parse(job)
new_queue = job_mapping[payload['class']]
# If we have no target queue to migrate to we're probably dealing with
diff --git a/db/migrate/20161124141322_migrate_process_commit_worker_jobs.rb b/db/migrate/20161124141322_migrate_process_commit_worker_jobs.rb
index 77e0c40d850..3e1f6b1627d 100644
--- a/db/migrate/20161124141322_migrate_process_commit_worker_jobs.rb
+++ b/db/migrate/20161124141322_migrate_process_commit_worker_jobs.rb
@@ -34,7 +34,7 @@ class MigrateProcessCommitWorkerJobs < ActiveRecord::Migration
new_jobs = []
while job = redis.lpop('queue:process_commit')
- payload = JSON.load(job)
+ payload = JSON.parse(job)
project = Project.find_including_path(payload['args'][0])
next unless project
@@ -75,7 +75,7 @@ class MigrateProcessCommitWorkerJobs < ActiveRecord::Migration
new_jobs = []
while job = redis.lpop('queue:process_commit')
- payload = JSON.load(job)
+ payload = JSON.parse(job)
payload['args'][2] = payload['args'][2]['id']
diff --git a/db/migrate/20161128142110_remove_unnecessary_indexes.rb b/db/migrate/20161128142110_remove_unnecessary_indexes.rb
index 9deab19782e..8100287ef48 100644
--- a/db/migrate/20161128142110_remove_unnecessary_indexes.rb
+++ b/db/migrate/20161128142110_remove_unnecessary_indexes.rb
@@ -12,7 +12,7 @@ class RemoveUnnecessaryIndexes < ActiveRecord::Migration
remove_index :award_emoji, column: :user_id if index_exists?(:award_emoji, :user_id)
remove_index :ci_builds, column: :commit_id if index_exists?(:ci_builds, :commit_id)
remove_index :deployments, column: :project_id if index_exists?(:deployments, :project_id)
- remove_index :deployments, column: ["project_id", "environment_id"] if index_exists?(:deployments, ["project_id", "environment_id"])
+ remove_index :deployments, column: %w(project_id environment_id) if index_exists?(:deployments, %w(project_id environment_id))
remove_index :lists, column: :board_id if index_exists?(:lists, :board_id)
remove_index :milestones, column: :project_id if index_exists?(:milestones, :project_id)
remove_index :notes, column: :project_id if index_exists?(:notes, :project_id)
@@ -24,7 +24,7 @@ class RemoveUnnecessaryIndexes < ActiveRecord::Migration
add_concurrent_index :award_emoji, :user_id
add_concurrent_index :ci_builds, :commit_id
add_concurrent_index :deployments, :project_id
- add_concurrent_index :deployments, ["project_id", "environment_id"]
+ add_concurrent_index :deployments, %w(project_id environment_id)
add_concurrent_index :lists, :board_id
add_concurrent_index :milestones, :project_id
add_concurrent_index :notes, :project_id
diff --git a/db/migrate/20161207231620_fixup_environment_name_uniqueness.rb b/db/migrate/20161207231620_fixup_environment_name_uniqueness.rb
index b74552e762d..a20a903a752 100644
--- a/db/migrate/20161207231620_fixup_environment_name_uniqueness.rb
+++ b/db/migrate/20161207231620_fixup_environment_name_uniqueness.rb
@@ -42,10 +42,10 @@ class FixupEnvironmentNameUniqueness < ActiveRecord::Migration
conflicts.each do |id, name|
update_sql =
Arel::UpdateManager.new(ActiveRecord::Base).
- table(environments).
- set(environments[:name] => name + "-" + id.to_s).
- where(environments[:id].eq(id)).
- to_sql
+ table(environments).
+ set(environments[:name] => name + "-" + id.to_s).
+ where(environments[:id].eq(id)).
+ to_sql
connection.exec_update(update_sql, self.class.name, [])
end
diff --git a/db/migrate/20170206115204_add_column_ghost_to_users.rb b/db/migrate/20170206115204_add_column_ghost_to_users.rb
new file mode 100644
index 00000000000..cc1eeda1160
--- /dev/null
+++ b/db/migrate/20170206115204_add_column_ghost_to_users.rb
@@ -0,0 +1,11 @@
+class AddColumnGhostToUsers < ActiveRecord::Migration
+ DOWNTIME = false
+
+ def up
+ add_column :users, :ghost, :boolean
+ end
+
+ def down
+ remove_column :users, :ghost
+ end
+end
diff --git a/db/migrate/20170210103609_add_index_to_user_agent_detail.rb b/db/migrate/20170210103609_add_index_to_user_agent_detail.rb
new file mode 100644
index 00000000000..c01753cfbd2
--- /dev/null
+++ b/db/migrate/20170210103609_add_index_to_user_agent_detail.rb
@@ -0,0 +1,14 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddIndexToUserAgentDetail < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def change
+ add_concurrent_index(:user_agent_details, [:subject_id, :subject_type])
+ end
+end
diff --git a/db/migrate/20170214084746_add_default_artifacts_expiration_to_application_settings.rb b/db/migrate/20170214084746_add_default_artifacts_expiration_to_application_settings.rb
new file mode 100644
index 00000000000..e0e3ff8957a
--- /dev/null
+++ b/db/migrate/20170214084746_add_default_artifacts_expiration_to_application_settings.rb
@@ -0,0 +1,11 @@
+class AddDefaultArtifactsExpirationToApplicationSettings < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ add_column :application_settings,
+ :default_artifacts_expire_in, :string,
+ null: false, default: '0'
+ end
+end
diff --git a/db/migrate/20170216135621_add_index_for_latest_successful_pipeline.rb b/db/migrate/20170216135621_add_index_for_latest_successful_pipeline.rb
new file mode 100644
index 00000000000..7b1e687977b
--- /dev/null
+++ b/db/migrate/20170216135621_add_index_for_latest_successful_pipeline.rb
@@ -0,0 +1,10 @@
+class AddIndexForLatestSuccessfulPipeline < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def change
+ add_concurrent_index(:ci_commits, [:gl_project_id, :ref, :status])
+ end
+end
diff --git a/db/migrate/20170216141440_drop_index_for_builds_project_status.rb b/db/migrate/20170216141440_drop_index_for_builds_project_status.rb
new file mode 100644
index 00000000000..906711b9f3f
--- /dev/null
+++ b/db/migrate/20170216141440_drop_index_for_builds_project_status.rb
@@ -0,0 +1,8 @@
+class DropIndexForBuildsProjectStatus < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+ DOWNTIME = false
+
+ def change
+ remove_index(:ci_commits, [:gl_project_id, :status])
+ end
+end
diff --git a/db/post_migrate/20161109150329_fix_project_records_with_invalid_visibility.rb b/db/post_migrate/20161109150329_fix_project_records_with_invalid_visibility.rb
index df38591a333..14b5ef476f0 100644
--- a/db/post_migrate/20161109150329_fix_project_records_with_invalid_visibility.rb
+++ b/db/post_migrate/20161109150329_fix_project_records_with_invalid_visibility.rb
@@ -14,15 +14,15 @@ class FixProjectRecordsWithInvalidVisibility < ActiveRecord::Migration
finder_sql =
projects.
- join(namespaces, Arel::Nodes::InnerJoin).
- on(projects[:namespace_id].eq(namespaces[:id])).
- where(projects[:visibility_level].gt(namespaces[:visibility_level])).
- project(projects[:id], namespaces[:visibility_level]).
- take(BATCH_SIZE).
- to_sql
+ join(namespaces, Arel::Nodes::InnerJoin).
+ on(projects[:namespace_id].eq(namespaces[:id])).
+ where(projects[:visibility_level].gt(namespaces[:visibility_level])).
+ project(projects[:id], namespaces[:visibility_level]).
+ take(BATCH_SIZE).
+ to_sql
# Update matching rows in batches. Each batch can cause up to 3 UPDATE
- # statements, in addition to the SELECT: one per visibility_level
+ # statements, in addition to the SELECT: one per visibility_level
loop do
to_update = connection.exec_query(finder_sql)
break if to_update.rows.count == 0
diff --git a/db/post_migrate/20161221153951_rename_reserved_project_names.rb b/db/post_migrate/20161221153951_rename_reserved_project_names.rb
index 282837be1fa..49a6bc884a8 100644
--- a/db/post_migrate/20161221153951_rename_reserved_project_names.rb
+++ b/db/post_migrate/20161221153951_rename_reserved_project_names.rb
@@ -37,7 +37,7 @@ class RenameReservedProjectNames < ActiveRecord::Migration
unsubscribes
update
users
- wikis)
+ wikis).freeze
def up
queues = Array.new(THREAD_COUNT) { Queue.new }
diff --git a/db/post_migrate/20170215200045_remove_theme_id_from_users.rb b/db/post_migrate/20170215200045_remove_theme_id_from_users.rb
new file mode 100644
index 00000000000..c51646fbe52
--- /dev/null
+++ b/db/post_migrate/20170215200045_remove_theme_id_from_users.rb
@@ -0,0 +1,9 @@
+class RemoveThemeIdFromUsers < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ remove_column :users, :theme_id, :integer
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 52672406ec6..1d94368f66e 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: 20170214111112) do
+ActiveRecord::Schema.define(version: 20170216141440) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -111,6 +111,7 @@ ActiveRecord::Schema.define(version: 20170214111112) do
t.boolean "plantuml_enabled"
t.integer "max_pages_size", default: 100, null: false
t.integer "terminal_max_session_time", default: 0, null: false
+ t.string "default_artifacts_expire_in", default: '0', null: false
end
create_table "audit_events", force: :cascade do |t|
@@ -251,8 +252,8 @@ ActiveRecord::Schema.define(version: 20170214111112) do
t.integer "lock_version"
end
+ add_index "ci_commits", ["gl_project_id", "ref", "status"], name: "index_ci_commits_on_gl_project_id_and_ref_and_status", using: :btree
add_index "ci_commits", ["gl_project_id", "sha"], name: "index_ci_commits_on_gl_project_id_and_sha", using: :btree
- add_index "ci_commits", ["gl_project_id", "status"], name: "index_ci_commits_on_gl_project_id_and_status", using: :btree
add_index "ci_commits", ["gl_project_id"], name: "index_ci_commits_on_gl_project_id", using: :btree
add_index "ci_commits", ["status"], name: "index_ci_commits_on_status", using: :btree
add_index "ci_commits", ["user_id"], name: "index_ci_commits_on_user_id", using: :btree
@@ -1218,6 +1219,8 @@ ActiveRecord::Schema.define(version: 20170214111112) do
t.datetime "updated_at", null: false
end
+ add_index "user_agent_details", ["subject_id", "subject_type"], name: "index_user_agent_details_on_subject_id_and_subject_type", using: :btree
+
create_table "users", force: :cascade do |t|
t.string "email", default: "", null: false
t.string "encrypted_password", default: "", null: false
@@ -1238,7 +1241,6 @@ ActiveRecord::Schema.define(version: 20170214111112) do
t.string "linkedin", default: "", null: false
t.string "twitter", default: "", null: false
t.string "authentication_token"
- t.integer "theme_id", default: 1, null: false
t.string "bio"
t.integer "failed_attempts", default: 0
t.datetime "locked_at"
@@ -1276,10 +1278,11 @@ ActiveRecord::Schema.define(version: 20170214111112) do
t.datetime "otp_grace_period_started_at"
t.boolean "ldap_email", default: false, null: false
t.boolean "external", default: false
- t.string "organization"
t.string "incoming_email_token"
+ t.string "organization"
t.boolean "authorized_projects_populated"
t.boolean "notified_of_own_activity", default: false, null: false
+ t.boolean "ghost"
end
add_index "users", ["admin"], name: "index_users_on_admin", using: :btree
@@ -1351,4 +1354,4 @@ ActiveRecord::Schema.define(version: 20170214111112) do
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 \ No newline at end of file
+end
diff --git a/doc/README.md b/doc/README.md
index 1943d656aa7..46a1ed0e148 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -21,6 +21,7 @@
- [Profile Settings](profile/README.md)
- [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.
+- [Snippets](user/snippets.md) Snippets allow you to create little bits of code.
- [SSH](ssh/README.md) Setup your ssh keys and deploy keys for secure access to your projects.
- [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.
@@ -50,6 +51,7 @@
- [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.
- [Welcome message](customization/welcome_message.md) Add a custom welcome message to the sign-in page.
+- [Header logo](customization/branded_page_and_email_header.md) Change the logo on the overall page and email header.
- [Reply by email](administration/reply_by_email.md) Allow users to comment on issues and merge requests by replying to notification emails.
- [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)
diff --git a/doc/administration/auth/authentiq.md b/doc/administration/auth/authentiq.md
index 3f39539da95..fb1a16b0f96 100644
--- a/doc/administration/auth/authentiq.md
+++ b/doc/administration/auth/authentiq.md
@@ -54,7 +54,7 @@ Authentiq will generate a Client ID and the accompanying Client Secret for you t
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.
+6. Change `YOUR_CLIENT_ID` and `YOUR_CLIENT_SECRET` to the Client credentials you received in step 1.
7. Save the configuration file.
diff --git a/doc/administration/container_registry.md b/doc/administration/container_registry.md
index a6300e18dc0..28e413ef447 100644
--- a/doc/administration/container_registry.md
+++ b/doc/administration/container_registry.md
@@ -466,6 +466,46 @@ If Registry is enabled in your GitLab instance, but you don't need it for your
project, you can disable it from your project's settings. Read the user guide
on how to achieve that.
+## Disable Container Registry but use GitLab as an auth endpoint
+
+You can disable the embedded Container Registry to use an external one, but
+still use GitLab as an auth endpoint.
+
+**Omnibus GitLab**
+1. Open `/etc/gitlab/gitlab.rb` and set necessary configurations:
+
+ ```ruby
+ registry['enable'] = false
+ gitlab_rails['registry_enabled'] = true
+ gitlab_rails['registry_host'] = "registry.gitlab.example.com"
+ gitlab_rails['registry_port'] = "5005"
+ gitlab_rails['registry_api_url'] = "http://localhost:5000"
+ gitlab_rails['registry_key_path'] = "/var/opt/gitlab/gitlab-rails/certificate.key"
+ gitlab_rails['registry_path'] = "/var/opt/gitlab/gitlab-rails/shared/registry"
+ gitlab_rails['registry_issuer'] = "omnibus-gitlab-issuer"
+ ```
+
+1. Save the file and [reconfigure GitLab][] for the changes to take effect.
+
+**Installations from source**
+
+1. Open `/home/git/gitlab/config/gitlab.yml`, and edit the configuration settings under `registry`:
+
+ ```
+ ## Container Registry
+
+ registry:
+ enabled: true
+ host: "registry.gitlab.example.com"
+ port: "5005"
+ api_url: "http://localhost:5000"
+ path: /var/opt/gitlab/gitlab-rails/shared/registry
+ key: /var/opt/gitlab/gitlab-rails/certificate.key
+ issuer: omnibus-gitlab-issuer
+ ```
+
+1. Save the file and [restart GitLab][] for the changes to take effect.
+
## Storage limitations
Currently, there is no storage limitation, which means a user can upload an
diff --git a/doc/administration/monitoring/prometheus/gitlab_monitor_exporter.md b/doc/administration/monitoring/prometheus/gitlab_monitor_exporter.md
index 86ef9d167e2..edb9c911aac 100644
--- a/doc/administration/monitoring/prometheus/gitlab_monitor_exporter.md
+++ b/doc/administration/monitoring/prometheus/gitlab_monitor_exporter.md
@@ -13,7 +13,7 @@ To enable the GitLab monitor exporter:
1. Add or find and uncomment the following line, making sure it's set to `true`:
```ruby
- gitlab_monitor_exporter['enable'] = true
+ gitlab_monitor['enable'] = true
```
1. Save the file and [reconfigure GitLab][reconfigure] for the changes to
diff --git a/doc/administration/pages/index.md b/doc/administration/pages/index.md
index 19316458d49..62b0468da79 100644
--- a/doc/administration/pages/index.md
+++ b/doc/administration/pages/index.md
@@ -1,4 +1,4 @@
-# GitLab Pages Administration
+# GitLab Pages administration
> **Notes:**
- [Introduced][ee-80] in GitLab EE 8.3.
@@ -6,6 +6,7 @@
- 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).
+- To learn how to use GitLab Pages, read the [user documentation][pages-userguide].
---
@@ -14,9 +15,6 @@ 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
@@ -28,22 +26,24 @@ 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:
+In the case of [custom domains](#custom-domains) (but not
+[wildcard domains](#wildcard-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
+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
+ 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.
+In this document, we will proceed assuming the first option. If you are not
+supporting custom domains a secondary IP is not needed.
## Prerequisites
@@ -56,6 +56,7 @@ Before proceeding with the Pages configuration, you will need 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.
+1. (Only for custom domains) Have a **secondary IP**.
### DNS configuration
@@ -64,11 +65,11 @@ 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
+*.example.io. 1800 IN A 1.1.1.1
```
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.
+and `1.1.1.1` is the IP address of your GitLab instance.
> **Note:**
You should not use the GitLab domain to serve user pages. For more information
@@ -78,69 +79,46 @@ see the [security section](#security).
## Configuration
-Depending on your needs, you can install GitLab Pages in four different ways.
+Depending on your needs, you can set up GitLab Pages in 4 different ways.
+The following options are listed from the easiest setup to the most
+advanced one. The absolute minimum requirement is to set up the wildcard DNS
+since that is needed in all configurations.
-### Option 1. Custom domains with HTTPS support
+### Wildcard domains
-| 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 |
+>**Requirements:**
+- [Wildcard DNS setup](#dns-configuration)
+>
+>---
+>
+URL scheme: `http://page.example.io`
-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.
+This is the minimum setup that you can use Pages with. It is the base for all
+other setups as described below. Nginx will proxy all requests to the daemon.
+The Pages daemon doesn't listen to the outside world.
-1. Edit `/etc/gitlab/gitlab.rb`:
+1. Set the external URL for GitLab Pages in `/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'
+ pages_external_url 'http://example.io'
```
- 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'
- ```
+Watch the [video tutorial][video-admin] for this configuration.
- 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.
+### Wildcard domains with TLS support
-1. [Reconfigure GitLab][reconfigure]
+>**Requirements:**
+- [Wildcard DNS setup](#dns-configuration)
+- Wildcard TLS certificate
+>
+>---
+>
+URL scheme: `https://page.example.io`
-### 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.
+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:
@@ -158,21 +136,71 @@ daemon. Pages daemon doesn't listen to the outside world.
1. [Reconfigure GitLab][reconfigure]
-### Option 4. Wildcard HTTP domain without custom domains
+## Advanced configuration
-| URL scheme | Wildcard certificate | Custom domain with HTTP support | Custom domain with HTTPS support | Secondary IP |
-| --- |:---:|:---:|:---:|:---:|:---:|:---:|:---:|
-| `http://page.example.io` | no | no | no | no |
+In addition to the wildcard domains, you can also have the option to configure
+GitLab Pages to work with custom domains. Again, there are two options here:
+support custom domains with and without TLS certificates. The easiest setup is
+that without TLS certificates.
-Pages enabled, daemon is enabled and NGINX will proxy all requests to the
-daemon. Pages daemon doesn't listen to the outside world.
+### Custom domains
-1. Set the external URL for GitLab Pages in `/etc/gitlab/gitlab.rb`:
+>**Requirements:**
+- [Wildcard DNS setup](#dns-configuration)
+- Secondary IP
+>
+---
+>
+URL scheme: `http://page.example.io` and `http://domain.com`
+
+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 are supported, but no TLS.
+
+1. Edit `/etc/gitlab/gitlab.rb`:
```ruby
- pages_external_url 'http://example.io'
+ 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]
+
+### Custom domains with TLS support
+
+>**Requirements:**
+- [Wildcard DNS setup](#dns-configuration)
+- Wildcard TLS certificate
+- Secondary IP
+>
+---
+>
+URL scheme: `https://page.example.io` and `https://domain.com`
+
+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]
## Change storage path
@@ -247,3 +275,4 @@ latest previous version.
[reconfigure]: ../restart_gitlab.md#omnibus-gitlab-reconfigure
[restart]: ../restart_gitlab.md#installations-from-source
[gitlab-pages]: https://gitlab.com/gitlab-org/gitlab-pages/tree/v0.2.4
+[video-admin]: https://youtu.be/dD8c7WNcc6s
diff --git a/doc/administration/pages/source.md b/doc/administration/pages/source.md
index 0718c05a6f4..463715e48ca 100644
--- a/doc/administration/pages/source.md
+++ b/doc/administration/pages/source.md
@@ -17,22 +17,54 @@ Pages to the latest supported version.
## Prerequisites
-[Read the Omnibus prerequisites section.](index.md#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.1.1.1
+```
+
+where `example.io` is the domain under which GitLab Pages will be served
+and `1.1.1.1` 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.
+Depending on your needs, you can set up GitLab Pages in 4 different ways.
+The following options are listed from the easiest setup to the most
+advanced one. The absolute minimum requirement is to set up the wildcard DNS
+since that is needed in all configurations.
-### Option 1. Custom domains with HTTPS support
+### Wildcard domains
-| 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 |
+>**Requirements:**
+- [Wildcard DNS setup](#dns-configuration)
+>
+>---
+>
+URL scheme: `http://page.example.io`
-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.
+This is the minimum setup that you can use Pages with. It is the base for all
+other setups as described below. Nginx will proxy all requests to the daemon.
+The Pages daemon doesn't listen to the outside world.
1. Install the Pages daemon:
@@ -44,10 +76,14 @@ world. Custom domains and TLS are supported.
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:
+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
@@ -57,25 +93,10 @@ world. Custom domains and TLS are supported.
# path: shared/pages
host: example.io
- port: 443
- https: true
-
- external_http: 1.1.1.2:80
- external_https: 1.1.1.2:443
+ port: 80
+ https: false
```
-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
@@ -85,22 +106,21 @@ world. Custom domains and TLS are supported.
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
+### Wildcard domains with TLS 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 |
+>**Requirements:**
+- [Wildcard DNS setup](#dns-configuration)
+- Wildcard TLS certificate
+>
+>---
+>
+URL scheme: `https://page.example.io`
-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.
+Nginx will proxy all requests to the daemon. Pages daemon doesn't listen to the
+outside world.
1. Install the Pages daemon:
@@ -112,34 +132,20 @@ world. Custom domains and TLS are supported.
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:
+1. In `gitlab.yml`, set the port to `443` and https to `true`:
- ```yaml
+ ```bash
+ ## GitLab Pages
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
+ port: 443
+ https: true
```
-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
@@ -149,20 +155,30 @@ world. Custom domains and TLS are supported.
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 |
+## Advanced configuration
+
+In addition to the wildcard domains, you can also have the option to configure
+GitLab Pages to work with custom domains. Again, there are two options here:
+support custom domains with and without TLS certificates. The easiest setup is
+that without TLS certificates.
+
+### Custom domains
-Pages enabled, daemon is enabled and NGINX will proxy all requests to the
-daemon. Pages daemon doesn't listen to the outside world.
+>**Requirements:**
+- [Wildcard DNS setup](#dns-configuration)
+- Secondary IP
+>
+---
+>
+URL scheme: `http://page.example.io` and `http://domain.com`
+
+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 are supported, but no TLS.
1. Install the Pages daemon:
@@ -173,20 +189,35 @@ daemon. Pages daemon doesn't listen to the outside world.
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
+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: 443
- https: true
+ 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
@@ -196,17 +227,26 @@ daemon. Pages daemon doesn't listen to the outside world.
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 4. Wildcard HTTP domain without custom domains
+### Custom domains with TLS support
-| URL scheme | Wildcard certificate | Custom domain with HTTP support | Custom domain with HTTPS support | Secondary IP |
-| --- |:---:|:---:|:---:|:---:|:---:|:---:|:---:|
-| `http://page.example.io` | no | no | no | no |
+>**Requirements:**
+- [Wildcard DNS setup](#dns-configuration)
+- Wildcard TLS certificate
+- Secondary IP
+>
+---
+>
+URL scheme: `https://page.example.io` and `https://domain.com`
-Pages enabled, daemon is enabled and NGINX will proxy all requests to the
-daemon. Pages daemon doesn't listen to the outside world.
+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:
@@ -218,14 +258,10 @@ daemon. Pages daemon doesn't listen to the outside world.
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:
+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
@@ -235,10 +271,25 @@ daemon. Pages daemon doesn't listen to the outside world.
# path: shared/pages
host: example.io
- port: 80
- https: false
+ 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
@@ -248,9 +299,27 @@ daemon. Pages daemon doesn't listen to the outside world.
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]
+## 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]
+
## NGINX caveats
>**Note:**
diff --git a/doc/administration/reply_by_email.md b/doc/administration/reply_by_email.md
index 00494e7e9d6..4f5c22e2d29 100644
--- a/doc/administration/reply_by_email.md
+++ b/doc/administration/reply_by_email.md
@@ -69,7 +69,9 @@ please consult [RFC 5322](https://tools.ietf.org/html/rfc5322#section-3.6.4).
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).
+and [allowed less secure apps to access the account](https://support.google.com/accounts/answer/6010255)
+or [turn-on 2-step validation](https://support.google.com/accounts/answer/185839)
+and use [an application password](https://support.google.com/mail/answer/185833).
To set up a basic Postfix mail server with IMAP access on Ubuntu, follow the
[Postfix setup documentation](reply_by_email_postfix_setup.md).
diff --git a/doc/api/README.md b/doc/api/README.md
index b334ca46caf..1c3b2ad0fbc 100644
--- a/doc/api/README.md
+++ b/doc/api/README.md
@@ -159,6 +159,7 @@ The following table shows the possible return codes for API requests.
| Return values | Description |
| ------------- | ----------- |
| `200 OK` | The `GET`, `PUT` or `DELETE` request was successful, the resource(s) itself is returned as JSON. |
+| `204 OK` | The server has successfully fulfilled the request and that there is no additional content to send in the response payload body. |
| `201 Created` | The `POST` request was successful and the resource is returned as JSON. |
| `304 Not Modified` | Indicates that the resource has not been modified since the last request. |
| `400 Bad Request` | A required attribute of the API request is missing, e.g., the title of an issue is not given. |
diff --git a/doc/api/award_emoji.md b/doc/api/award_emoji.md
index 58092bdd400..c6fd8c5fa53 100644
--- a/doc/api/award_emoji.md
+++ b/doc/api/award_emoji.md
@@ -178,27 +178,6 @@ Parameters:
curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/award_emoji/344
```
-Example Response:
-
-```json
-{
- "id": 344,
- "name": "blowfish",
- "user": {
- "name": "Administrator",
- "username": "root",
- "id": 1,
- "state": "active",
- "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
- "web_url": "http://gitlab.example.com/root"
- },
- "created_at": "2016-06-17T17:47:29.266Z",
- "updated_at": "2016-06-17T17:47:29.266Z",
- "awardable_id": 80,
- "awardable_type": "Issue"
-}
-```
-
## Award Emoji on Notes
The endpoints documented above are available for Notes as well. Notes
@@ -350,25 +329,4 @@ Parameters:
curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/award_emoji/345
```
-Example Response:
-
-```json
-{
- "id": 345,
- "name": "rocket",
- "user": {
- "name": "Administrator",
- "username": "root",
- "id": 1,
- "state": "active",
- "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
- "web_url": "http://gitlab.example.com/root"
- },
- "created_at": "2016-06-17T19:59:55.888Z",
- "updated_at": "2016-06-17T19:59:55.888Z",
- "awardable_id": 1,
- "awardable_type": "Note"
-}
-```
-
[ce-4575]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4575
diff --git a/doc/api/boards.md b/doc/api/boards.md
index c83db6df80c..f80b98f960b 100644
--- a/doc/api/boards.md
+++ b/doc/api/boards.md
@@ -226,16 +226,3 @@ DELETE /projects/:id/boards/:board_id/lists/:list_id
```bash
curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/boards/1/lists/1
```
-Example response:
-
-```json
-{
- "id" : 1,
- "label" : {
- "name" : "Testing",
- "color" : "#F0AD4E",
- "description" : null
- },
- "position" : 1
-}
-```
diff --git a/doc/api/branches.md b/doc/api/branches.md
index 5eaa8d2e920..f29a8518945 100644
--- a/doc/api/branches.md
+++ b/doc/api/branches.md
@@ -193,11 +193,11 @@ POST /projects/:id/repository/branches
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer | yes | The ID of a project |
-| `branch_name` | string | yes | The name of the branch |
+| `branch` | string | yes | The name of the branch |
| `ref` | string | yes | The branch name or commit SHA to create branch from |
```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/repository/branches?branch_name=newbranch&ref=master"
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/repository/branches?branch=newbranch&ref=master"
```
Example response:
@@ -244,14 +244,6 @@ In case of an error, an explaining message is provided.
curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/repository/branches/newbranch"
```
-Example response:
-
-```json
-{
- "branch_name": "newbranch"
-}
-```
-
## Delete merged branches
Will delete all branches that are merged into the project's default branch.
diff --git a/doc/api/broadcast_messages.md b/doc/api/broadcast_messages.md
index a3e9c01f335..fecfb142ab1 100644
--- a/doc/api/broadcast_messages.md
+++ b/doc/api/broadcast_messages.md
@@ -138,17 +138,3 @@ DELETE /broadcast_messages/:id
```bash
curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/broadcast_messages/1
```
-
-Example response:
-
-```json
-{
- "message":"Update message",
- "starts_at":"2016-08-26T00:41:35.060Z",
- "ends_at":"2016-08-26T01:41:35.060Z",
- "color":"#000",
- "font":"#FFFFFF",
- "id":1,
- "active": true
-}
-```
diff --git a/doc/api/build_triggers.md b/doc/api/build_triggers.md
index b6459971420..6adefe8c58c 100644
--- a/doc/api/build_triggers.md
+++ b/doc/api/build_triggers.md
@@ -106,13 +106,3 @@ DELETE /projects/:id/triggers/:token
```
curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/triggers/7b9148c158980bbd9bcea92c17522d"
```
-
-```json
-{
- "created_at": "2015-12-23T16:25:56.760Z",
- "deleted_at": "2015-12-24T12:32:20.100Z",
- "last_used": null,
- "token": "7b9148c158980bbd9bcea92c17522d",
- "updated_at": "2015-12-24T12:32:20.100Z"
-}
-```
diff --git a/doc/api/build_variables.md b/doc/api/build_variables.md
index 917e9773913..c21d5ab2787 100644
--- a/doc/api/build_variables.md
+++ b/doc/api/build_variables.md
@@ -119,10 +119,3 @@ DELETE /projects/:id/variables/:key
```
curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables/VARIABLE_1"
```
-
-```json
-{
- "key": "VARIABLE_1",
- "value": "VALUE_1"
-}
-```
diff --git a/doc/api/commits.md b/doc/api/commits.md
index 3223b82f60a..18bc2873678 100644
--- a/doc/api/commits.md
+++ b/doc/api/commits.md
@@ -12,8 +12,8 @@ GET /projects/:id/repository/commits
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID of a project or NAMESPACE/PROJECT_NAME owned by the authenticated user
| `ref_name` | string | no | The name of a repository branch or tag or if not given the default branch |
-| `since` | string | no | Only commits after or in this date will be returned in ISO 8601 format YYYY-MM-DDTHH:MM:SSZ |
-| `until` | string | no | Only commits before or in this date will be returned in ISO 8601 format YYYY-MM-DDTHH:MM:SSZ |
+| `since` | string | no | Only commits after or on this date will be returned in ISO 8601 format YYYY-MM-DDTHH:MM:SSZ |
+| `until` | string | no | Only commits before or on this date will be returned in ISO 8601 format YYYY-MM-DDTHH:MM:SSZ |
```bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/repository/commits"
@@ -69,7 +69,7 @@ POST /projects/:id/repository/commits
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID of a project or NAMESPACE/PROJECT_NAME |
-| `branch_name` | string | yes | The name of a branch |
+| `branch` | string | yes | The name of a branch |
| `commit_message` | string | yes | Commit message |
| `actions[]` | array | yes | An array of action hashes to commit as a batch. See the next table for what attributes it can take. |
| `author_email` | string | no | Specify the commit author's email address |
@@ -87,7 +87,7 @@ POST /projects/:id/repository/commits
```bash
PAYLOAD=$(cat << 'JSON'
{
- "branch_name": "master",
+ "branch": "master",
"commit_message": "some commit message",
"actions": [
{
diff --git a/doc/api/deploy_keys.md b/doc/api/deploy_keys.md
index 284d5f88c55..d03d94cb867 100644
--- a/doc/api/deploy_keys.md
+++ b/doc/api/deploy_keys.md
@@ -137,7 +137,7 @@ Example response:
## Delete deploy key
-Delete a deploy key from a project
+Removes a deploy key from the project. If the deploy key is used only for this project, it will be deleted from the system.
```
DELETE /projects/:id/deploy_keys/:key_id
@@ -152,21 +152,6 @@ DELETE /projects/:id/deploy_keys/:key_id
curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/deploy_keys/13"
```
-Example response:
-
-```json
-{
- "updated_at" : "2015-08-29T12:50:57.259Z",
- "key" : "ssh-rsa AAAA...",
- "public" : false,
- "title" : "My deploy key",
- "user_id" : null,
- "created_at" : "2015-08-29T12:50:57.259Z",
- "fingerprint" : "6a:33:1f:74:51:c0:39:81:79:ec:7a:31:f8:40:20:43",
- "id" : 13
-}
-```
-
## Enable a deploy key
Enables a deploy key for a project so this can be used. Returns the enabled key, with a status code 201 when successful.
@@ -190,27 +175,3 @@ Example response:
"created_at" : "2015-08-29T12:44:31.550Z"
}
```
-
-## Disable a deploy key
-
-Disable a deploy key for a project. Returns the disabled key.
-
-```bash
-curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/deploy_keys/13/disable
-```
-
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of the project |
-| `key_id` | integer | yes | The ID of the deploy key |
-
-Example response:
-
-```json
-{
- "key" : "ssh-rsa AAAA...",
- "id" : 12,
- "title" : "My deploy key",
- "created_at" : "2015-08-29T12:44:31.550Z"
-}
-```
diff --git a/doc/api/enviroments.md b/doc/api/enviroments.md
index e0ee20d9610..e510f723e26 100644
--- a/doc/api/enviroments.md
+++ b/doc/api/enviroments.md
@@ -108,14 +108,3 @@ DELETE /projects/:id/environments/:environment_id
```bash
curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/environments/1"
```
-
-Example response:
-
-```json
-{
- "id": 1,
- "name": "deploy",
- "slug": "deploy",
- "external_url": "https://deploy.example.gitlab.com"
-}
-```
diff --git a/doc/api/groups.md b/doc/api/groups.md
index 4a39dbc5555..39adb5be502 100644
--- a/doc/api/groups.md
+++ b/doc/api/groups.md
@@ -14,6 +14,7 @@ Parameters:
| `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) |
+| `owned` | boolean | no | Limit by groups owned by the current user |
```
GET /groups
@@ -40,20 +41,6 @@ GET /groups
You can search for groups by name or path, see below.
-## List owned groups
-
-Get a list of groups which are owned by the authenticated user.
-
-```
-GET /groups/owned
-```
-
-Parameters:
-
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `statistics` | boolean | no | Include group statistics |
-
## List a group's projects
Get a list of projects in this group.
diff --git a/doc/api/issues.md b/doc/api/issues.md
index 7c0a444d4fa..b6798fba0ae 100644
--- a/doc/api/issues.md
+++ b/doc/api/issues.md
@@ -30,7 +30,7 @@ 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 |
+| `labels` | string | no | Comma-separated list of label names, issues must have all labels to 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` |
@@ -188,7 +188,7 @@ GET /projects/:id/issues?milestone=1.0.0&state=opened
| `id` | integer | yes | The ID of a project |
| `iid` | integer | no | Return the issue having the given `iid` |
| `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 |
+| `labels` | string | no | Comma-separated list of label names, issues must have all labels to 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` |
@@ -514,7 +514,7 @@ If the user is already subscribed to the issue, the status code `304`
is returned.
```
-POST /projects/:id/issues/:issue_id/subscription
+POST /projects/:id/issues/:issue_id/subscribe
```
| Attribute | Type | Required | Description |
@@ -523,7 +523,7 @@ POST /projects/:id/issues/:issue_id/subscription
| `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/subscription
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/subscribe
```
Example response:
@@ -569,7 +569,7 @@ from it. If the user is not subscribed to the issue, the
status code `304` is returned.
```
-DELETE /projects/:id/issues/:issue_id/subscription
+POST /projects/:id/issues/:issue_id/unsubscribe
```
| Attribute | Type | Required | Description |
@@ -578,44 +578,7 @@ DELETE /projects/:id/issues/:issue_id/subscription
| `issue_id` | integer | yes | The ID of a project's issue |
```bash
-curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/subscription
-```
-
-Example response:
-
-```json
-{
- "id": 93,
- "iid": 12,
- "project_id": 5,
- "title": "Incidunt et rerum ea expedita iure quibusdam.",
- "description": "Et cumque architecto sed aut ipsam.",
- "state": "opened",
- "created_at": "2016-04-05T21:41:45.217Z",
- "updated_at": "2016-04-07T13:02:37.905Z",
- "labels": [],
- "milestone": null,
- "assignee": {
- "name": "Edwardo Grady",
- "username": "keyon",
- "id": 21,
- "state": "active",
- "avatar_url": "http://www.gravatar.com/avatar/3e6f06a86cf27fa8b56f3f74f7615987?s=80&d=identicon",
- "web_url": "https://gitlab.example.com/keyon"
- },
- "author": {
- "name": "Vivian Hermann",
- "username": "orville",
- "id": 11,
- "state": "active",
- "avatar_url": "http://www.gravatar.com/avatar/5224fd70153710e92fb8bcf79ac29d67?s=80&d=identicon",
- "web_url": "https://gitlab.example.com/orville"
- },
- "subscribed": false,
- "due_date": null,
- "web_url": "http://example.com/example/example/issues/12",
- "confidential": false
-}
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/unsubscribe
```
## Create a todo
diff --git a/doc/api/keys.md b/doc/api/keys.md
index b68f08a007d..3b55c2baf56 100644
--- a/doc/api/keys.md
+++ b/doc/api/keys.md
@@ -33,7 +33,6 @@ Parameters:
"twitter": "",
"website_url": "",
"email": "john@example.com",
- "theme_id": 2,
"color_scheme_id": 1,
"projects_limit": 10,
"current_sign_in_at": null,
diff --git a/doc/api/labels.md b/doc/api/labels.md
index 863b28c23b7..85bd9647a7b 100644
--- a/doc/api/labels.md
+++ b/doc/api/labels.md
@@ -131,22 +131,6 @@ DELETE /projects/:id/labels
curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/labels?name=bug"
```
-Example response:
-
-```json
-{
- "id" : 1,
- "name" : "bug",
- "color" : "#d9534f",
- "description": "Bug reported by user",
- "open_issues_count": 1,
- "closed_issues_count": 0,
- "open_merge_requests_count": 1,
- "subscribed": false,
- "priority": null
-}
-```
-
## Edit an existing label
Updates an existing label with new name or new color. At least one parameter
@@ -188,12 +172,12 @@ Example response:
## Subscribe to a label
-Subscribes the authenticated user to a label to receive notifications.
+Subscribes the authenticated user to a label to receive notifications.
If the user is already subscribed to the label, the status code `304`
is returned.
```
-POST /projects/:id/labels/:label_id/subscription
+POST /projects/:id/labels/:label_id/subscribe
```
| Attribute | Type | Required | Description |
@@ -202,7 +186,7 @@ POST /projects/:id/labels/:label_id/subscription
| `label_id` | integer or string | yes | The ID or title of a project's label |
```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/labels/1/subscription
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/labels/1/subscribe
```
Example response:
@@ -228,7 +212,7 @@ from it. If the user is not subscribed to the label, the
status code `304` is returned.
```
-DELETE /projects/:id/labels/:label_id/subscription
+POST /projects/:id/labels/:label_id/unsubscribe
```
| Attribute | Type | Required | Description |
@@ -237,21 +221,5 @@ DELETE /projects/:id/labels/:label_id/subscription
| `label_id` | integer or string | yes | The ID or title of a project's label |
```bash
-curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/labels/1/subscription
-```
-
-Example response:
-
-```json
-{
- "id" : 1,
- "name" : "bug",
- "color" : "#d9534f",
- "description": "Bug reported by user",
- "open_issues_count": 1,
- "closed_issues_count": 0,
- "open_merge_requests_count": 1,
- "subscribed": false,
- "priority": null
-}
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/labels/1/unsubscribe
```
diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md
index 6ee377125d6..ea30a163a12 100644
--- a/doc/api/merge_requests.md
+++ b/doc/api/merge_requests.md
@@ -667,7 +667,7 @@ Subscribes the authenticated user to a merge request to receive notification. If
status code `304` is returned.
```
-POST /projects/:id/merge_requests/:merge_request_id/subscription
+POST /projects/:id/merge_requests/:merge_request_id/subscribe
```
| Attribute | Type | Required | Description |
@@ -676,7 +676,7 @@ POST /projects/:id/merge_requests/:merge_request_id/subscription
| `merge_request_id` | integer | yes | The ID of the merge request |
```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/17/subscription
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/17/subscribe
```
Example response:
@@ -741,7 +741,7 @@ notifications from that merge request. If the user is
not subscribed to the merge request, the status code `304` is returned.
```
-DELETE /projects/:id/merge_requests/:merge_request_id/subscription
+POST /projects/:id/merge_requests/:merge_request_id/unsubscribe
```
| Attribute | Type | Required | Description |
@@ -750,7 +750,7 @@ DELETE /projects/:id/merge_requests/:merge_request_id/subscription
| `merge_request_id` | integer | yes | The ID of the merge request |
```bash
-curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/17/subscription
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/17/unsubscribe
```
Example response:
diff --git a/doc/api/milestones.md b/doc/api/milestones.md
index 12497acff98..bf7dcc008e9 100644
--- a/doc/api/milestones.md
+++ b/doc/api/milestones.md
@@ -103,3 +103,16 @@ Parameters:
- `id` (required) - The ID of a project
- `milestone_id` (required) - The ID of a project milestone
+
+## Get all merge requests assigned to a single milestone
+
+Gets all merge requests assigned to a single project milestone.
+
+```
+GET /projects/:id/milestones/:milestone_id/merge_requests
+```
+
+Parameters:
+
+- `id` (required) - The ID of a project
+- `milestone_id` (required) - The ID of a project milestone \ No newline at end of file
diff --git a/doc/api/notes.md b/doc/api/notes.md
index 214dfa4068d..7dc1fd930de 100644
--- a/doc/api/notes.md
+++ b/doc/api/notes.md
@@ -34,8 +34,6 @@ Parameters:
"created_at": "2013-10-02T09:22:45Z",
"updated_at": "2013-10-02T10:22:45Z",
"system": true,
- "upvote": false,
- "downvote": false,
"noteable_id": 377,
"noteable_type": "Issue"
},
@@ -54,8 +52,6 @@ Parameters:
"created_at": "2013-10-02T09:56:03Z",
"updated_at": "2013-10-02T09:56:03Z",
"system": true,
- "upvote": false,
- "downvote": false,
"noteable_id": 121,
"noteable_type": "Issue"
}
@@ -127,32 +123,6 @@ Parameters:
curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/11/notes/636
```
-Example Response:
-
-```json
-{
- "id": 636,
- "body": "This is a good idea.",
- "attachment": null,
- "author": {
- "id": 1,
- "username": "pipin",
- "email": "admin@example.com",
- "name": "Pip",
- "state": "active",
- "created_at": "2013-09-30T13:46:01Z",
- "avatar_url": "http://www.gravatar.com/avatar/5224fd70153710e92fb8bcf79ac29d67?s=80&d=identicon",
- "web_url": "https://gitlab.example.com/pipin"
- },
- "created_at": "2016-04-05T22:10:44.164Z",
- "system": false,
- "noteable_id": 11,
- "noteable_type": "Issue",
- "upvote": false,
- "downvote": false
-}
-```
-
## Snippets
### List all snippet notes
@@ -251,32 +221,6 @@ Parameters:
curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/snippets/52/notes/1659
```
-Example Response:
-
-```json
-{
- "id": 1659,
- "body": "This is a good idea.",
- "attachment": null,
- "author": {
- "id": 1,
- "username": "pipin",
- "email": "admin@example.com",
- "name": "Pip",
- "state": "active",
- "created_at": "2013-09-30T13:46:01Z",
- "avatar_url": "http://www.gravatar.com/avatar/5224fd70153710e92fb8bcf79ac29d67?s=80&d=identicon",
- "web_url": "https://gitlab.example.com/pipin"
- },
- "created_at": "2016-04-06T16:51:53.239Z",
- "system": false,
- "noteable_id": 52,
- "noteable_type": "Snippet",
- "upvote": false,
- "downvote": false
-}
-```
-
## Merge Requests
### List all merge request notes
@@ -322,8 +266,6 @@ Parameters:
"created_at": "2013-10-02T08:57:14Z",
"updated_at": "2013-10-02T08:57:14Z",
"system": false,
- "upvote": false,
- "downvote": false,
"noteable_id": 2,
"noteable_type": "MergeRequest"
}
@@ -379,29 +321,3 @@ Parameters:
```bash
curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/7/notes/1602
```
-
-Example Response:
-
-```json
-{
- "id": 1602,
- "body": "This is a good idea.",
- "attachment": null,
- "author": {
- "id": 1,
- "username": "pipin",
- "email": "admin@example.com",
- "name": "Pip",
- "state": "active",
- "created_at": "2013-09-30T13:46:01Z",
- "avatar_url": "http://www.gravatar.com/avatar/5224fd70153710e92fb8bcf79ac29d67?s=80&d=identicon",
- "web_url": "https://gitlab.example.com/pipin"
- },
- "created_at": "2016-04-05T22:11:59.923Z",
- "system": false,
- "noteable_id": 7,
- "noteable_type": "MergeRequest",
- "upvote": false,
- "downvote": false
-}
-```
diff --git a/doc/api/pipelines.md b/doc/api/pipelines.md
index 82351ae688f..f3c9827f742 100644
--- a/doc/api/pipelines.md
+++ b/doc/api/pipelines.md
@@ -163,7 +163,7 @@ Example of response
}
```
-## Retry failed builds in a pipeline
+## Retry builds in a pipeline
> [Introduced][ce-5837] in GitLab 8.11
diff --git a/doc/api/projects.md b/doc/api/projects.md
index b3136be6731..5de23908b42 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -407,8 +407,6 @@ Parameters:
},
"created_at": "2015-12-04T10:33:56.698Z",
"system": false,
- "upvote": false,
- "downvote": false,
"noteable_id": 377,
"noteable_type": "Issue"
},
@@ -437,8 +435,8 @@ Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `name` | string | yes | The name of the new project |
-| `path` | string | no | Custom repository name for new project. By default generated based on name |
+| `name` | string | yes if path is not provided | The name of the new project. Equals path if not provided. |
+| `path` | string | yes if name is not provided | Repository name for new project. Generated based on name if not provided (generated lowercased with dashes). |
| `namespace_id` | integer | no | Namespace for the new project (defaults to the current user's namespace) |
| `description` | string | no | Short project description |
| `issues_enabled` | boolean | no | Enable issues for this project |
@@ -609,7 +607,7 @@ Example response:
Unstars a given project. Returns status code `304` if the project is not starred.
```
-DELETE /projects/:id/star
+POST /projects/:id/unstar
```
| Attribute | Type | Required | Description |
@@ -617,7 +615,7 @@ DELETE /projects/:id/star
| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
```bash
-curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/star"
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/unstar"
```
Example response:
@@ -1194,4 +1192,18 @@ Parameters:
| --------- | ---- | -------- | ----------- |
| `query` | string | yes | A string contained in the project name |
| `order_by` | string | no | Return requests ordered by `id`, `name`, `created_at` or `last_activity_at` fields |
-| `sort` | string | no | Return requests sorted in `asc` or `desc` order | \ No newline at end of file
+| `sort` | string | no | Return requests sorted in `asc` or `desc` order |
+
+## Start the Housekeeping task for a Project
+
+>**Note:** This feature was introduced in GitLab 9.0
+
+```
+POST /projects/:id/housekeeping
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
diff --git a/doc/api/repository_files.md b/doc/api/repository_files.md
index dbb3c1113e8..677e209ccd9 100644
--- a/doc/api/repository_files.md
+++ b/doc/api/repository_files.md
@@ -46,22 +46,22 @@ POST /projects/:id/repository/files
```
```bash
-curl --request POST --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v3/projects/13083/repository/files?file_path=app/project.rb&branch_name=master&author_email=author%40example.com&author_name=Firstname%20Lastname&content=some%20content&commit_message=create%20a%20new%20file'
+curl --request POST --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v3/projects/13083/repository/files?file_path=app/project.rb&branch=master&author_email=author%40example.com&author_name=Firstname%20Lastname&content=some%20content&commit_message=create%20a%20new%20file'
```
Example response:
```json
{
- "file_path": "app/project.rb",
- "branch_name": "master"
+ "file_name": "app/project.rb",
+ "branch": "master"
}
```
Parameters:
- `file_path` (required) - Full path to new file. Ex. lib/class.rb
-- `branch_name` (required) - The name of branch
+- `branch` (required) - The name of branch
- `encoding` (optional) - Change encoding to 'base64'. Default is text.
- `author_email` (optional) - Specify the commit author's email address
- `author_name` (optional) - Specify the commit author's name
@@ -75,22 +75,22 @@ PUT /projects/:id/repository/files
```
```bash
-curl --request PUT --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v3/projects/13083/repository/files?file_path=app/project.rb&branch_name=master&author_email=author%40example.com&author_name=Firstname%20Lastname&content=some%20other%20content&commit_message=update%20file'
+curl --request PUT --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v3/projects/13083/repository/files?file_path=app/project.rb&branch=master&author_email=author%40example.com&author_name=Firstname%20Lastname&content=some%20other%20content&commit_message=update%20file'
```
Example response:
```json
{
- "file_path": "app/project.rb",
- "branch_name": "master"
+ "file_name": "app/project.rb",
+ "branch": "master"
}
```
Parameters:
- `file_path` (required) - Full path to file. Ex. lib/class.rb
-- `branch_name` (required) - The name of branch
+- `branch` (required) - The name of branch
- `encoding` (optional) - Change encoding to 'base64'. Default is text.
- `author_email` (optional) - Specify the commit author's email address
- `author_name` (optional) - Specify the commit author's name
@@ -113,22 +113,22 @@ DELETE /projects/:id/repository/files
```
```bash
-curl --request DELETE --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v3/projects/13083/repository/files?file_path=app/project.rb&branch_name=master&author_email=author%40example.com&author_name=Firstname%20Lastname&commit_message=delete%20file'
+curl --request DELETE --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v3/projects/13083/repository/files?file_path=app/project.rb&branch=master&author_email=author%40example.com&author_name=Firstname%20Lastname&commit_message=delete%20file'
```
Example response:
```json
{
- "file_path": "app/project.rb",
- "branch_name": "master"
+ "file_name": "app/project.rb",
+ "branch": "master"
}
```
Parameters:
- `file_path` (required) - Full path to file. Ex. lib/class.rb
-- `branch_name` (required) - The name of branch
+- `branch` (required) - The name of branch
- `author_email` (optional) - Specify the commit author's email address
- `author_name` (optional) - Specify the commit author's name
- `commit_message` (required) - Commit message
diff --git a/doc/api/runners.md b/doc/api/runners.md
index 28610762dca..27d8e7640b2 100644
--- a/doc/api/runners.md
+++ b/doc/api/runners.md
@@ -210,18 +210,6 @@ DELETE /runners/:id
curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/runners/6"
```
-Example response:
-
-```json
-{
- "active": true,
- "description": "test-1-20150125-test",
- "id": 6,
- "is_shared": false,
- "name": null,
-}
-```
-
## List project's runners
List all runners (specific and shared) available in the project. Shared runners
@@ -308,15 +296,3 @@ DELETE /projects/:id/runners/:runner_id
```
curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/9/runners/9"
```
-
-Example response:
-
-```json
-{
- "active": true,
- "description": "test-2016-02-01",
- "id": 9,
- "is_shared": false,
- "name": null
-}
-```
diff --git a/doc/api/services.md b/doc/api/services.md
index fba5da6587d..b030a425a7a 100644
--- a/doc/api/services.md
+++ b/doc/api/services.md
@@ -810,3 +810,38 @@ GET /projects/:id/services/teamcity
[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
+
+
+## MockCI
+
+Mock an external CI. See [`gitlab-org/gitlab-mock-ci-service`](https://gitlab.com/gitlab-org/gitlab-mock-ci-service) for an example of a companion mock service.
+
+This service is only available when your environment is set to development.
+
+### Create/Edit MockCI service
+
+Set MockCI service for a project.
+
+```
+PUT /projects/:id/services/mock-ci
+```
+
+Parameters:
+
+- `mock_service_url` (**required**) - http://localhost:4004
+
+### Delete MockCI service
+
+Delete MockCI service for a project.
+
+```
+DELETE /projects/:id/services/mock-ci
+```
+
+### Get MockCI service settings
+
+Get MockCI service settings for a project.
+
+```
+GET /projects/:id/services/mock-ci
+```
diff --git a/doc/api/session.md b/doc/api/session.md
index f776424023e..d7809716fbe 100644
--- a/doc/api/session.md
+++ b/doc/api/session.md
@@ -41,7 +41,6 @@ Example response:
"twitter": "",
"website_url": "",
"email": "john@example.com",
- "theme_id": 1,
"color_scheme_id": 1,
"projects_limit": 10,
"current_sign_in_at": "2015-07-07T07:10:58.392Z",
diff --git a/doc/api/system_hooks.md b/doc/api/system_hooks.md
index 3fb8b73be6d..a9edff799ac 100644
--- a/doc/api/system_hooks.md
+++ b/doc/api/system_hooks.md
@@ -125,22 +125,3 @@ Example request:
```bash
curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/hooks/2
```
-
-Example response:
-
-```json
-{
- "note_events" : false,
- "project_id" : null,
- "enable_ssl_verification" : true,
- "url" : "https://gitlab.example.com/hook",
- "updated_at" : "2015-11-04T20:12:15.931Z",
- "issues_events" : false,
- "merge_requests_events" : false,
- "created_at" : "2015-11-04T20:12:15.931Z",
- "service_id" : null,
- "id" : 2,
- "push_events" : true,
- "tag_push_events" : false
-}
-```
diff --git a/doc/api/tags.md b/doc/api/tags.md
index 7f78ffc2390..abeb4bfb40e 100644
--- a/doc/api/tags.md
+++ b/doc/api/tags.md
@@ -141,11 +141,6 @@ Parameters:
- `id` (required) - The ID of a project
- `tag_name` (required) - The name of a tag
-```json
-{
- "tag_name": "v4.3.0"
-}
-```
## Create a new release
diff --git a/doc/api/todos.md b/doc/api/todos.md
index a5e81801024..a2fbbc7e1f8 100644
--- a/doc/api/todos.md
+++ b/doc/api/todos.md
@@ -184,7 +184,7 @@ Marks a single pending todo given by its ID for the current user as done. The
todo marked as done is returned in the response.
```
-DELETE /todos/:id
+POST /todos/:id/mark_as_done
```
Parameters:
@@ -194,7 +194,7 @@ Parameters:
| `id` | integer | yes | The ID of a todo |
```bash
-curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/todos/130
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/todos/130/mark_as_done
```
Example Response:
@@ -277,20 +277,15 @@ Example Response:
## Mark all todos as done
-Marks all pending todos for the current user as done. It returns the number of marked todos.
+Marks all pending todos for the current user as done. It returns the HTTP status code `204` with an empty response.
```
-DELETE /todos
+POST /todos/mark_as_done
```
```bash
-curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/todos
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/todos/donmark_as_donee
```
-Example Response:
-
-```json
-3
-```
[ce-3188]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3188
diff --git a/doc/api/users.md b/doc/api/users.md
index ed3469521fc..d14548e8bbb 100644
--- a/doc/api/users.md
+++ b/doc/api/users.md
@@ -72,7 +72,6 @@ GET /users
"organization": "",
"last_sign_in_at": "2012-06-01T11:41:01Z",
"confirmed_at": "2012-05-23T09:05:22Z",
- "theme_id": 1,
"color_scheme_id": 2,
"projects_limit": 100,
"current_sign_in_at": "2012-06-02T06:36:55Z",
@@ -105,7 +104,6 @@ GET /users
"organization": "",
"last_sign_in_at": null,
"confirmed_at": "2012-05-30T16:53:06.148Z",
- "theme_id": 1,
"color_scheme_id": 3,
"projects_limit": 100,
"current_sign_in_at": "2014-03-19T17:54:13Z",
@@ -198,7 +196,6 @@ Parameters:
"organization": "",
"last_sign_in_at": "2012-06-01T11:41:01Z",
"confirmed_at": "2012-05-23T09:05:22Z",
- "theme_id": 1,
"color_scheme_id": 2,
"projects_limit": 100,
"current_sign_in_at": "2012-06-02T06:36:55Z",
@@ -323,7 +320,6 @@ GET /user
"organization": "",
"last_sign_in_at": "2012-06-01T11:41:01Z",
"confirmed_at": "2012-05-23T09:05:22Z",
- "theme_id": 1,
"color_scheme_id": 2,
"projects_limit": 100,
"current_sign_in_at": "2012-06-02T06:36:55Z",
@@ -369,7 +365,6 @@ GET /user
"organization": "",
"last_sign_in_at": "2012-06-01T11:41:01Z",
"confirmed_at": "2012-05-23T09:05:22Z",
- "theme_id": 1,
"color_scheme_id": 2,
"projects_limit": 100,
"current_sign_in_at": "2012-06-02T06:36:55Z",
@@ -664,14 +659,14 @@ Will return `200 OK` on success, or `404 Not found` if either user or email cann
Blocks the specified user. Available only for admin.
```
-PUT /users/:id/block
+POST /users/:id/block
```
Parameters:
- `id` (required) - id of specified user
-Will return `200 OK` on success, `404 User Not Found` is user cannot be found or
+Will return `201 OK` on success, `404 User Not Found` is user cannot be found or
`403 Forbidden` when trying to block an already blocked user by LDAP synchronization.
## Unblock user
@@ -679,14 +674,14 @@ Will return `200 OK` on success, `404 User Not Found` is user cannot be found or
Unblocks the specified user. Available only for admin.
```
-PUT /users/:id/unblock
+POST /users/:id/unblock
```
Parameters:
- `id` (required) - id of specified user
-Will return `200 OK` on success, `404 User Not Found` is user cannot be found or
+Will return `201 OK` on success, `404 User Not Found` is user cannot be found or
`403 Forbidden` when trying to unblock a user blocked by LDAP synchronization.
### Get user contribution events
@@ -817,8 +812,6 @@ Example response:
},
"created_at": "2015-12-04T10:33:56.698Z",
"system": false,
- "upvote": false,
- "downvote": false,
"noteable_id": 377,
"noteable_type": "Issue"
},
diff --git a/doc/api/v3_to_v4.md b/doc/api/v3_to_v4.md
index 84ff72bc36c..c178e224cc5 100644
--- a/doc/api/v3_to_v4.md
+++ b/doc/api/v3_to_v4.md
@@ -4,16 +4,20 @@ 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
+### 8.17
-- Removed `/projects/:search` (use: `/projects?search=x`)
-- `iid` filter has been removed from `projects/:id/issues`
-- `projects/:id/merge_requests?iid[]=x&iid[]=y` array filter has been renamed to `iids`
-- Endpoints under `projects/merge_request/:id` have been removed (use: `projects/merge_requests/:id`)
-- Project snippets do not return deprecated field `expires_at`
-- Endpoints under `projects/:id/keys` have been removed (use `projects/:id/deploy_keys`)
-- Status 409 returned for POST `project/:id/members` when a member already exists
-- Removed the following deprecated Templates endpoints (these are still accessible with `/templates` prefix)
+- Removed `/projects/:search` (use: `/projects?search=x`) [!8877](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8877)
+- `iid` filter has been removed from `projects/:id/issues` [!8967](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8967)
+- `projects/:id/merge_requests?iid[]=x&iid[]=y` array filter has been renamed to `iids` [!8793](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8793)
+- Endpoints under `projects/merge_request/:id` have been removed (use: `projects/merge_requests/:id`) [!8793](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8793)
+- Project snippets do not return deprecated field `expires_at` [!8723](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8723)
+- Endpoints under `projects/:id/keys` have been removed (use `projects/:id/deploy_keys`) [!8716](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8716)
+
+### 9.0
+
+- Status 409 returned for POST `project/:id/members` when a member already exists [!9093](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9093)
+- Moved `DELETE /projects/:id/star` to `POST /projects/:id/unstar` [!9328](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9328)
+- Removed the following deprecated Templates endpoints (these are still accessible with `/templates` prefix) [!8853](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8853)
- `/licences`
- `/licences/:key`
- `/gitignores`
@@ -22,5 +26,21 @@ changes are in V4:
- `/gitignores/:key`
- `/gitlab_ci_ymls/:key`
- `/dockerfiles/:key`
-- Moved `/projects/fork/:id` to `/projects/:id/fork`
-- Endpoints `/projects/owned`, `/projects/visible`, `/projects/starred` & `/projects/all` are consolidated into `/projects` using query parameters
+- Moved `/projects/fork/:id` to `/projects/:id/fork` [!8940](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8940)
+- Moved `DELETE /todos` to `POST /todos/mark_as_done` and `DELETE /todos/:todo_id` to `POST /todos/:todo_id/mark_as_done` [!9410](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9410)
+- Endpoints `/projects/owned`, `/projects/visible`, `/projects/starred` & `/projects/all` are consolidated into `/projects` using query parameters [!8962](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8962)
+- Return pagination headers for all endpoints that return an array [!8606](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8606)
+- Removed `DELETE projects/:id/deploy_keys/:key_id/disable`. Use `DELETE projects/:id/deploy_keys/:key_id` instead [!9366](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9366)
+- Moved `PUT /users/:id/(block|unblock)` to `POST /users/:id/(block|unblock)` [!9371](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9371)
+- Make subscription API more RESTful. Use `post ":project_id/:subscribable_type/:subscribable_id/subscribe"` to subscribe and `post ":project_id/:subscribable_type/:subscribable_id/unsubscribe"` to unsubscribe from a resource. [!9325](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9325)
+- Labels filter on `projects/:id/issues` and `/issues` now matches only issues containing all labels (i.e.: Logical AND, not OR) [!8849](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8849)
+- Renamed param `branch_name` to `branch` on the following endpoints [!8936](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8936)
+ - POST `:id/repository/branches`
+ - POST `:id/repository/commits`
+ - POST/PUT/DELETE `:id/repository/files`
+- Renamed `branch_name` to `branch` on DELETE `id/repository/branches/:branch` response [!8936](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8936)
+- Remove `public` param from create and edit actions of projects [!8736](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8736)
+- Notes do not return deprecated field `upvote` and `downvote` [!9384](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9384)
+- Return HTTP status code `400` for all validation errors when creating or updating a member instead of sometimes `422` error. [!9523](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9523)
+- Remove `GET /groups/owned`. Use `GET /groups?owned=true` instead [!9505](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9505)
+- Return 202 with JSON body on async removals on V4 API (DELETE `/projects/:id/repository/merged_branches` and DELETE `/projects/:id`) [!9449](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9449)
diff --git a/doc/ci/docker/using_docker_build.md b/doc/ci/docker/using_docker_build.md
index 2b3082acd5d..8620984d40d 100644
--- a/doc/ci/docker/using_docker_build.md
+++ b/doc/ci/docker/using_docker_build.md
@@ -308,6 +308,30 @@ push to the Registry connected to your project. Its password is provided in the
`$CI_BUILD_TOKEN` variable. This allows you to automate building and deployment
of your Docker images.
+You can also make use of [other variables](../variables/README.md) to avoid hardcoding:
+
+```yaml
+services:
+ - docker:dind
+
+variables:
+ IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_BUILD_REF_NAME
+
+before_script:
+ - docker login -u gitlab-ci-token -p $CI_BUILD_TOKEN $CI_REGISTRY
+
+build:
+ stage: build
+ script:
+ - docker build -t $IMAGE_TAG .
+ - docker push $IMAGE_TAG
+```
+
+Here, `$CI_REGISTRY_IMAGE` would be resolved to the address of the registry tied
+to this project, and `$CI_BUILD_REF_NAME` would be resolved to the branch or
+tag name for this particular job. We also declare our own variable, `$IMAGE_TAG`,
+combining the two to save us some typing in the `script` section.
+
Here's a more elaborate example that splits up the tasks into 4 pipeline stages,
including two tests that run in parallel. The `build` is stored in the container
registry and used by subsequent stages, downloading the image
diff --git a/doc/ci/docker/using_docker_images.md b/doc/ci/docker/using_docker_images.md
index 9dee61bfa1f..00787323b6b 100644
--- a/doc/ci/docker/using_docker_images.md
+++ b/doc/ci/docker/using_docker_images.md
@@ -39,13 +39,15 @@ accessible during the build process.
## What is an image
-The `image` keyword is the name of the docker image that is present in the
-local Docker Engine (list all images with `docker images`) or any image that
-can be found at [Docker Hub][hub]. For more information about images and Docker
-Hub please read the [Docker Fundamentals][] documentation.
+The `image` keyword is the name of the docker image the docker executor
+will run to perform the CI tasks.
-In short, with `image` we refer to the docker image, which will be used to
-create a container on which your job will run.
+By default the executor will only pull images from [Docker Hub][hub],
+but this can be configured in the `gitlab-runner/config.toml` by setting
+the [docker pull policy][] to allow using local images.
+
+For more information about images and Docker Hub please read
+the [Docker Fundamentals][] documentation.
## What is a service
@@ -271,6 +273,7 @@ containers as well as all volumes (`-v`) that were created with the container
creation.
[Docker Fundamentals]: https://docs.docker.com/engine/understanding-docker/
+[docker pull policy]: https://docs.gitlab.com/runner/executors/docker.html#how-pull-policies-work
[hub]: https://hub.docker.com/
[linking-containers]: https://docs.docker.com/engine/userguide/networking/default_network/dockerlinks/
[tutum/wordpress]: https://hub.docker.com/r/tutum/wordpress/
diff --git a/doc/ci/pipelines.md b/doc/ci/pipelines.md
index 9d294240d9d..db92a4b0d80 100644
--- a/doc/ci/pipelines.md
+++ b/doc/ci/pipelines.md
@@ -91,7 +91,7 @@ total running time should be:
## Badges
-Job status and test coverage report badges are available. You can find their
+Pipeline status and test coverage report badges are available. You can find their
respective link in the [Pipelines settings] page.
[jobs]: #jobs
diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md
index 8a638ed3df8..620d4744685 100644
--- a/doc/ci/variables/README.md
+++ b/doc/ci/variables/README.md
@@ -148,7 +148,8 @@ available in the build environment. It's the recommended method to use for
storing things like passwords, secret keys and credentials.
Secret variables can be added by going to your project's
-**Settings ➔ Variables ➔ Add variable**.
+**Settings ➔ CI/CD Pipelines**, then finding the section called
+**Secret Variables**.
Once you set them, they will be available for all subsequent jobs.
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index a73598df812..a586b095ef5 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -1003,6 +1003,9 @@ job:
### coverage
+**Notes:**
+- [Introduced][ce-7447] in GitLab 8.17.
+
`coverage` allows you to configure how code coverage will be extracted from the
job output.
@@ -1015,7 +1018,7 @@ A simple example:
```yaml
job1:
- coverage: /Code coverage: \d+\.\d+/
+ coverage: '/Code coverage: \d+\.\d+/'
```
## Git Strategy
@@ -1361,3 +1364,4 @@ CI with various languages.
[ce-6669]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/6669
[variables]: ../variables/README.md
[ce-7983]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7983
+[ce-7447]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7447
diff --git a/doc/customization/branded_page_and_email_header.md b/doc/customization/branded_page_and_email_header.md
new file mode 100644
index 00000000000..9a0f0b382fa
--- /dev/null
+++ b/doc/customization/branded_page_and_email_header.md
@@ -0,0 +1,15 @@
+# Changing the logo on the overall page and email header
+
+Navigate to the **Admin** area and go to the **Appearance** page.
+
+Upload the custom logo (**Header logo**) in the section **Navigation bar**.
+
+![appearance](branded_page_and_email_header/appearance.png)
+
+After saving the page, your GitLab navigation bar will contain the custom logo:
+
+![custom_brand_header](branded_page_and_email_header/custom_brand_header.png)
+
+The GitLab pipeline emails will also have the custom logo:
+
+![custom_email_header](branded_page_and_email_header/custom_email_header.png)
diff --git a/doc/customization/branded_page_and_email_header/appearance.png b/doc/customization/branded_page_and_email_header/appearance.png
new file mode 100644
index 00000000000..abbba6f9ac9
--- /dev/null
+++ b/doc/customization/branded_page_and_email_header/appearance.png
Binary files differ
diff --git a/doc/customization/branded_page_and_email_header/custom_brand_header.png b/doc/customization/branded_page_and_email_header/custom_brand_header.png
new file mode 100644
index 00000000000..7390f8a5e4e
--- /dev/null
+++ b/doc/customization/branded_page_and_email_header/custom_brand_header.png
Binary files differ
diff --git a/doc/customization/branded_page_and_email_header/custom_email_header.png b/doc/customization/branded_page_and_email_header/custom_email_header.png
new file mode 100644
index 00000000000..705698ef4a8
--- /dev/null
+++ b/doc/customization/branded_page_and_email_header/custom_email_header.png
Binary files differ
diff --git a/doc/development/ci_setup.md b/doc/development/ci_setup.md
index 2f49b3564ab..b03216fec95 100644
--- a/doc/development/ci_setup.md
+++ b/doc/development/ci_setup.md
@@ -2,11 +2,12 @@
This document describes what services we use for testing GitLab and GitLab CI.
-We currently use three CI services to test GitLab:
+We currently use four CI services to test GitLab:
1. GitLab CI on [GitHost.io](https://gitlab-ce.githost.io/projects/4/) for the [GitLab.com repo](https://gitlab.com/gitlab-org/gitlab-ce)
2. GitLab CI at ci.gitlab.org to test the private GitLab B.V. repo at dev.gitlab.org
3. [Semephore](https://semaphoreapp.com/gitlabhq/gitlabhq/) for [GitHub.com repo](https://github.com/gitlabhq/gitlabhq)
+4. [Mock CI Service](user/project/integrations/mock_ci.md) for local development
| Software @ configuration being tested | GitLab CI (ci.gitlab.org) | GitLab CI (GitHost.io) | Semaphore |
|---------------------------------------|---------------------------|---------------------------------------------------------------------------|-----------|
diff --git a/doc/development/frontend.md b/doc/development/frontend.md
index ba47998de49..9ba820eaee5 100644
--- a/doc/development/frontend.md
+++ b/doc/development/frontend.md
@@ -238,6 +238,9 @@ readability.
See the relevant style guides for our guidelines and for information on linting:
- [SCSS][scss-style-guide]
+- JavaScript - We defer to [AirBnb][airbnb-js-style-guide] on most style-related
+conventions and enforce them with eslint. See [our current .eslintrc][eslistrc]
+for specific rules and patterns.
## Testing
@@ -434,3 +437,5 @@ Scenario: Developer can approve merge request
[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
+[airbnb-js-style-guide]: https://github.com/airbnb/javascript
+[eslintrc]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.eslintrc
diff --git a/doc/development/licensing.md b/doc/development/licensing.md
index 5d177eb26ee..1f115059fb8 100644
--- a/doc/development/licensing.md
+++ b/doc/development/licensing.md
@@ -64,6 +64,10 @@ Libraries with the following licenses are unacceptable for use:
- [GNU AGPLv3][AGPLv3]: AGPL-licensed libraries cannot be linked to from non-GPL projects.
- [Open Software License (OSL)][OSL]: is a copyleft license. In addition, the FSF [recommend against its use][OSL-GNU].
+## Requesting Approval for Licenses
+
+Libraries that are not listed in the [Acceptable Licenses][Acceptable-Licenses] or [Unacceptable Licenses][Unacceptable-Licenses] list can be submitted to the legal team for review. Please create an issue in the [Organization Repository][Org-Repo] and cc `@gl-legal`. After a decision has been made, the original requestor is responsible for updating this document.
+
## Notes
Decisions regarding the GNU GPL licenses are based on information provided by [The GNU Project][GNU-GPL-FAQ], as well as [the Open Source Initiative][OSI-GPL], which both state that linking GPL libraries makes the program itself GPL.
@@ -96,3 +100,6 @@ Gems which are included only in the "development" or "test" groups by Bundler ar
[OSI-GPL]: https://opensource.org/faq#linking-proprietary-code
[OSL]: https://opensource.org/licenses/OSL-3.0
[OSL-GNU]: https://www.gnu.org/licenses/license-list.en.html#OSL
+[Org-Repo]: https://gitlab.com/gitlab-com/organization
+[Acceptable-Licenses]: #acceptable-licenses
+[Unacceptable-Licenses]: #unacceptable-licenses
diff --git a/doc/development/limit_ee_conflicts.md b/doc/development/limit_ee_conflicts.md
index 568dedf1669..e3568b65b18 100644
--- a/doc/development/limit_ee_conflicts.md
+++ b/doc/development/limit_ee_conflicts.md
@@ -2,19 +2,26 @@
This guide contains best-practices for avoiding conflicts between CE and EE.
-## Context
+## Daily CE Upstream merge
-Usually, GitLab Community Edition is merged into the Enterprise Edition once a
-week. During these merges, it's very common to get conflicts when some changes
-in CE do not apply cleanly to EE.
+GitLab Community Edition is merged daily into the Enterprise Edition (look for
+the [`CE Upstream` merge requests]). The daily merge is currently done manually
+by four individuals.
-There are a few things that can help you as a developer to:
+**If a developer pings you in a `CE Upstream` merge request for help with
+resolving conflicts, please help them because it means that you didn't do your
+job to reduce the conflicts nor to ease their resolution in the first place!**
-- know when your merge request to CE will conflict when merged to EE
-- avoid such conflicts in the first place
-- ease future conflict resolutions if conflict is inevitable
+To avoid the conflicts beforehand when working on CE, there are a few tools and
+techniques that can help you:
-## Check the `rake ee_compat_check` in your merge requests
+- know what are the usual types of conflicts and how to prevent them
+- the CI `rake ee_compat_check` job tells you if you need to open an EE-version
+ of your CE merge request
+
+[`CE Upstream` merge requests]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests?label_name%5B%5D=CE+upstream
+
+## Check the status of the CI `rake ee_compat_check` job
For each commit (except on `master`), the `rake ee_compat_check` CI job tries to
detect if the current branch's changes will conflict during the CE->EE merge.
@@ -43,6 +50,12 @@ Notes:
asking a GitLab developer to do it once the merge request is merged.
- If you branch is more than 500 commits behind `master`, the job will fail and
you should rebase your branch upon latest `master`.
+- Code reviews for merge requests often consist of multiple iterations of
+ feedback and fixes. There is no need to update your EE MR after each
+ iteration. Instead, create an EE MR as soon as you see the
+ `rake ee_compat_check` job failing and update it after the CE MR is merged.
+ This helps to identify significant conflicts sooner, but also reduces the
+ number of times you have to resolve conflicts.
## Possible type of conflicts
diff --git a/doc/development/testing.md b/doc/development/testing.md
index 761847b2bab..9b545d7f0f1 100644
--- a/doc/development/testing.md
+++ b/doc/development/testing.md
@@ -95,6 +95,25 @@ so we need to set some guidelines for their use going forward:
[lets-not]: https://robots.thoughtbot.com/lets-not
+### Time-sensitive tests
+
+[Timecop](https://github.com/travisjeffery/timecop) is available in our
+Ruby-based tests for verifying things that are time-sensitive. Any test that
+exercises or verifies something time-sensitive should make use of Timecop to
+prevent transient test failures.
+
+Example:
+
+```ruby
+it 'is overdue' do
+ issue = build(:issue, due_date: Date.tomorrow)
+
+ Timecop.freeze(3.days.from_now) do
+ expect(issue).to be_overdue
+ end
+end
+```
+
### Test speed
GitLab has a massive test suite that, without parallelization, can take more
@@ -115,6 +134,10 @@ Here are some things to keep in mind regarding test performance:
### Features / Integration
+GitLab uses [rspec-rails feature specs] to test features in a browser
+environment. These are [capybara] specs running on the headless [poltergeist]
+driver.
+
- Feature specs live in `spec/features/` and should be named
`ROLE_ACTION_spec.rb`, such as `user_changes_password_spec.rb`.
- Use only one `feature` block per feature spec file.
@@ -122,6 +145,10 @@ Here are some things to keep in mind regarding test performance:
- Avoid scenario titles that add no information, such as "successfully."
- Avoid scenario titles that repeat the feature title.
+[rspec-rails feature specs]: https://github.com/rspec/rspec-rails#feature-specs
+[capybara]: https://github.com/teamcapybara/capybara
+[poltergeist]: https://github.com/teampoltergeist/poltergeist
+
## Spinach (feature) tests
GitLab [moved from Cucumber to Spinach](https://github.com/gitlabhq/gitlabhq/pull/1426)
diff --git a/doc/development/ux_guide/copy.md b/doc/development/ux_guide/copy.md
index 205b3e8a835..ead79ba6a10 100644
--- a/doc/development/ux_guide/copy.md
+++ b/doc/development/ux_guide/copy.md
@@ -176,4 +176,4 @@ Portions of this page are modifications based on work created and shared by the
[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
+[creative commons]: http://creativecommons.org/licenses/by/2.5/
diff --git a/doc/development/ux_guide/img/harry-robison.png b/doc/development/ux_guide/img/harry-robison.png
new file mode 100644
index 00000000000..702a8b02262
--- /dev/null
+++ b/doc/development/ux_guide/img/harry-robison.png
Binary files differ
diff --git a/doc/development/ux_guide/img/james-mackey.png b/doc/development/ux_guide/img/james-mackey.png
new file mode 100644
index 00000000000..6db257c5b39
--- /dev/null
+++ b/doc/development/ux_guide/img/james-mackey.png
Binary files differ
diff --git a/doc/development/ux_guide/img/steven-lyons.png b/doc/development/ux_guide/img/steven-lyons.png
new file mode 100644
index 00000000000..2efe1d0b168
--- /dev/null
+++ b/doc/development/ux_guide/img/steven-lyons.png
Binary files differ
diff --git a/doc/development/ux_guide/users.md b/doc/development/ux_guide/users.md
index 717a902c424..137154e24f3 100644
--- a/doc/development/ux_guide/users.md
+++ b/doc/development/ux_guide/users.md
@@ -1,16 +1,164 @@
-# Users
+## UX Personas
+* [Nazim Ramesh](#nazim-ramesh)
+ - Small to medium size organisations using GitLab CE
+* [James Mackey](#james-mackey)
+ - Medium to large size organisations using CE or EE
+ - Small organisations using EE
+* [Karolina Plaskaty](#karolina-plaskaty)
+ - Using GitLab.com for personal/hobby projects
+ - Would like to use GitLab at work
+ - Working for a medium to large size organisation
-> TODO: Create personas. Understand the similarities and differences across the below spectrums.
+<hr>
-## Users by organization
+### Nazim Ramesh
+- Small to medium size organisations using GitLab CE
-- Enterprise
-- Medium company
-- Small company
-- Open source communities
+<img src="img/steven-lyons.png" width="300px">
-## Users by role
+#### Demographics
-- Admin
-- Manager
-- Developer
+- **Age**<br>32 years old
+- **Location**<br>Germany
+- **Education**<br>Bachelor of Science in Computer Science
+- **Occupation**<br>Full-stack web developer
+- **Programming experience**<br>Over 10 years
+- **Frequently used programming languages**<br>JavaScript, SQL, PHP
+- **Hobbies / interests**<br>Functional programming, open source, gaming, web development and web security.
+
+#### Motivations
+Nazim works for a software development company which currently hires around 80 people. When Nazim first joined the company, the engineering team were using Subversion (SVN) as their primary form of source control. However, Nazim felt SVN was not flexible enough to work with many feature branches and noticed that developers with less experience of source control struggled with the central-repository nature of SVN. Armed with a wishlist of features, Nazim began comparing source control tools. A search for “self-hosted Git server repository management” returned GitLab. In his own words, Nazim explains why he wanted the engineering team to start using GitLab:
+
+>
+“I wanted them to switch away from SVN. I needed a server application to manage repositories. The common tools that were around just didn’t meet the requirements. Most of them were too simple or plain...GitLab provided all the required features. Also costs had to be low, since we don’t have a big budget for those things...the Community Edition was perfect in this regard.”
+>
+
+In his role as a full-stack web developer, Nazim could recommend products that he would like the engineering team to use, but final approval lay with his line manager, Mike, VP of Engineering. Nazim recalls that he was met with reluctance from his colleagues when he raised moving to Git and using GitLab.
+
+>
+“The biggest challenge...why should we change anything at all from the status quo? We needed to switch from SVN to Git. They knew they needed to learn Git and a Git workflow...using Git was scary to my colleagues...they thought it was more complex than SVN to use.”
+>
+
+Undeterred, Nazim decided to migrate a couple of projects across to GitLab.
+
+>
+“Old SVN users couldn’t see the benefits of Git at first. It took a month or two to convince them.”
+>
+
+Slowly, by showing his colleagues how easy it was to use Git, the majority of the team’s projects were migrated to GitLab.
+
+The engineering team have been using GitLab CE for around 2 years now. Nazim credits himself as being entirely responsible for his company’s decision to move to GitLab.
+
+#### Frustrations
+##### Adoption to GitLab has been slow
+Not only has the engineering team had to get to grips with Git, they’ve also had to adapt to using GitLab. Due to lack of training and existing skills in other tools, the full feature set of GitLab CE is not being utilised. Nazim sold GitLab to his manager as an ‘all in one’ tool which would replace multiple tools used within the company, thus saving costs. Nazim hasn’t had the time to integrate the legacy tools to GitLab and he’s struggling to convince his peers to change their habits.
+
+##### Missing Features
+Nazim’s company want GitLab to be able to do everything. There isn’t a large budget for software, so they’re selective about what tools are implemented. It needs to add real value to the company. In order for GitLab to be widely adopted and to meet the requirements of different roles within the company, it needs a host of features. When an individual within Nazim’s company wants to know if GitLab has a specific feature or does a particular thing, Nazim is the person to ask. He becomes the point of contact to investigate, build or sometimes just raise the feature request. Nazim gets frustrated when GitLab isn’t able to do what he or his colleagues need it to do.
+
+##### Regressions and bugs
+Nazim often has to calm down his colleagues, when a release contains regressions or new bugs. As he puts it “every new version adds something awesome, but breaks something”. He feels that “old issues for "minor" annoyances get quickly buried in the mass of open issues and linger for a very long time. More generally, I have the feeling that GitLab focus on adding new functionalities, but overlook a bunch of annoying minor regressions or introduced bugs.” Due to limited resource and expertise within the team, not only is it difficult to remain up-to-date with the frequent release cycle, it’s also counterproductive to fix workflows every month.
+
+##### Uses too much RAM and CPU
+>
+“Memory usages mean that if we host it from a cloud based host like AWS, we spend almost as much on the instance as what we would pay GitHub”
+>
+
+##### UI/UX
+GitLab’s interface initially attracted Nazim when he was comparing version control software. He thought it would help his less technical colleagues to adapt to using Git and perhaps, GitLab could be rolled out to other areas of the business, beyond engineering. However, using GitLab’s interface daily has left him frustrated at the lack of personalisation / control over his user experience. He’s also regularly lost in a maze of navigation. Whilst he acknowledges that GitLab listens to its users and that the interface is improving, he becomes annoyed when the changes are too progressive. “Too frequent UI changes. Most of them tend to turn out great after a few cycles of fixes, but the frequency is still far too high for me to feel comfortable to always stay on the current release.”
+
+#### Goals
+* To convince his colleagues to fully adopt GitLab CE, thus improving workflow and collaboration.
+* To use a feature rich version control platform that covers all stages of the development lifecycle, in order to reduce dependencies on other tools.
+* To use an intuitive and stable product, so he can spend more time on his core job responsibilities and less time bug-fixing, guiding colleagues, etc.
+
+<hr>
+
+### James Mackey
+- Medium to large size organisations using CE or EE
+- Small organisations using EE
+
+<img src="img/james-mackey.png" width="300px">
+
+#### Demographics
+
+- **Age**<br>36 years old
+- **Location**<br>US
+- **Education**<br>Masters degree in Computer Science
+- **Occupation**<br>Full-stack web developer
+- **Programming experience**<br>Over 10 years
+- **Frequently used programming languages**<br>JavaScript, SQL, Node.js, Java, PHP, Python
+- **Hobbies / interests**<br>DevOps, open source, web development, science, automation and electronics.
+
+#### Motivations
+James works for a research company which currently hires around 800 staff. He began using GitLab.com back in 2013 for his own open source, hobby projects and loved “the simplicity of installation, administration and use”. After using GitLab for over a year, he began to wonder about using it at work. James explains:
+
+>
+“We first installed the CE edition...on a staging server for a PoC and asked a beta team to use it, specifically for the Merge Request features. Soon other teams began asking us to be beta users too, because the team that was already using GitLab was really enjoying it.”
+>
+
+James and his colleagues also reviewed competitor products including GitHub Enterprise, but they found it “less innovative and with considerable costs...GitLab had the features we wanted at a much lower cost per head than GitHub”.
+
+The company James works for provides employees with a discretionary budget to spend how they want on software, so James and his team decided to upgrade to EE.
+
+James feels partially responsible for his organisation’s decision to start using GitLab.
+
+>
+“It's still up to the teams themselves [to decide] which tools to use. We just had a great experience moving our daily development to GitLab, so other teams have followed the path or are thinking about switching.”
+>
+
+#### Frustrations
+##### Third Party Integration
+Some of GitLab EE’s features are too basic, in particular, issues boards which do not have the level of reporting that James and his team need. Subsequently, they still need to use GitLab EE in conjunction with other tools, such as JIRA. Whilst James feels it isn’t essential for GitLab to meet all his needs (his company are happy for him to use, and pay for, multiple tools), he sometimes isn’t sure what is/isn’t possible with plugins and what level of custom development he and his team will need to do.
+
+##### UX/UI
+James and his team use CI quite heavily for several projects. Whilst they’ve welcomed improvements to the builds and pipelines interface, they still have some difficulty following build process on the different tabs under Pipelines. Some confusion has arisen from not knowing where to find different pieces of information or how to get to the next stages logs from the current stage’s log output screen. They feel more intuitive linking and flow may alleviate the problem. Generally, they feel GitLab’s navigation needs to reviewed and optimised.
+
+##### Permissions
+>
+“There is no granular control over user or group permissions. The permissions for a project are too tightly coupled to the permissions for Gitlab CI/build pipelines.”
+>
+
+#### Goals
+* To be able to integrate third party tools easily with GitLab EE and to create custom integrations and patches where needed.
+* To use GitLab EE primarily for code hosting, merge requests, continuous integration and issue management. James and his team want to be able to understand and use these particular features easily.
+* To able to share one instance of GitLab EE with multiple teams across the business. Advanced user management, the ability to separate permissions on different parts of the source code, etc are important to James.
+
+<hr>
+
+### Karolina Plaskaty
+- Using GitLab.com for personal/hobby projects
+- Would like to use GitLab at work
+- Working for a medium to large size organisation
+
+<img src="img/harry-robison.png" width="300px">
+
+#### Demographics
+
+- **Age**<br>26 years old
+- **Location**<br>UK
+- **Education**<br>Self taught
+- **Occupation**<br>Junior web-developer
+- **Programming experience**<br>6 years
+- **Frequently used programming languages**<br>JavaScript and SQL
+- **Hobbies / interests**<br>Web development, mobile development, UX, open source, gaming and travel.
+
+#### Motivations
+Karolina has been using GitLab.com for around a year. She roughly spends 8 hours every week programming, of that, 2 hours is spent contributing to open source projects. Karolina contributes to open source projects to gain programming experience and to give back to the community. She likes GitLab.com for its free private repositories and range of features which provide her with everything she needs for her personal projects. Karolina is also a massive fan of GitLab’s values and the fact that it isn’t a “behemoth of a company”. She explains that “displaying every single thing (doc, culture, assumptions, development...) in the open gives me greater confidence to choose Gitlab personally and to recommend it at work.” She’s also an avid reader of GitLab’s blog.
+
+Karolina works for a software development company which currently hires around 500 people. Karolina would love to use GitLab at work but the company has used GitHub Enterprise for a number of years. She describes management at her company as “old fashioned” and explains that it’s “less of a technical issue and more of a cultural issue” to convince upper management to move to GitLab. Karolina is also relatively new to the company so she’s apprehensive about pushing too hard to change version control platforms.
+
+#### Frustrations
+##### Unable to use GitLab at work
+Karolina wants to use GitLab at work but isn’t sure how to approach the subject with management. In her current role, she doesn’t feel that she has the authority to request GitLab.
+
+##### Performance
+GitLab.com is frequently slow and unavailable. Karolina has also heard that GitLab is a “memory hog” which has deterred her from running GitLab on her own machine for just hobby / personal projects.
+
+##### UX/UI
+Karolina has an interest in UX and therefore has strong opinions about how GitLab should look and feel. She feels the interface is cluttered, “it has too many links/buttons” and the navigation “feels a bit weird sometimes. I get lost if I don’t pay attention.” As Karolina also enjoys contributing to open-source projects, it’s important to her that GitLab is well designed for public repositories, she doesn’t feel that GitLab currently achieves this.
+
+#### Goals
+* To develop her programming experience and to learn from other developers.
+* To contribute to both her own and other open source projects.
+* To use a fast and intuitive version control platform. \ No newline at end of file
diff --git a/doc/install/installation.md b/doc/install/installation.md
index 0f07085942a..5ba338ba7d1 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -39,6 +39,7 @@ The GitLab installation consists of setting up the following components:
1. Packages / Dependencies
1. Ruby
1. Go
+1. Node
1. System Users
1. Database
1. Redis
@@ -63,7 +64,7 @@ up-to-date and install it.
Install the required packages (needed to compile Ruby and native extensions to Ruby gems):
- sudo apt-get install -y build-essential zlib1g-dev libyaml-dev libssl-dev libgdbm-dev libreadline-dev libncurses5-dev libffi-dev curl openssh-server checkinstall libxml2-dev libxslt-dev libcurl4-openssl-dev libicu-dev logrotate python-docutils pkg-config cmake nodejs
+ sudo apt-get install -y build-essential zlib1g-dev libyaml-dev libssl-dev libgdbm-dev libreadline-dev libncurses5-dev libffi-dev curl openssh-server checkinstall libxml2-dev libxslt-dev libcurl4-openssl-dev libicu-dev logrotate python-docutils pkg-config cmake
If you want to use Kerberos for user authentication, then install libkrb5-dev:
@@ -151,13 +152,30 @@ page](https://golang.org/dl).
sudo ln -sf /usr/local/go/bin/{go,godoc,gofmt} /usr/local/bin/
rm go1.5.3.linux-amd64.tar.gz
-## 4. System Users
+## 4. Node
+
+Since GitLab 8.17, GitLab requires the use of node >= v4.3.0 to compile
+javascript assets, and starting in GitLab 9.0, yarn >= v0.17.0 is required to
+manage javascript dependencies. In many distros the versions provided by the
+official package repositories are out of date, so we'll need to install through
+the following commands:
+
+ # install node v7.x
+ curl --location https://deb.nodesource.com/setup_7.x | bash -
+ sudo apt-get install -y nodejs
+
+ # install yarn
+ curl --location https://yarnpkg.com/install.sh | bash -
+
+Visit the official websites for [node](https://nodejs.org/en/download/package-manager/) and [yarn](https://yarnpkg.com/en/docs/install/) if you have any trouble with these steps.
+
+## 5. System Users
Create a `git` user for GitLab:
sudo adduser --disabled-login --gecos 'GitLab' git
-## 5. Database
+## 6. Database
We recommend using a PostgreSQL database. For MySQL check the
[MySQL setup guide](database_mysql.md).
@@ -218,7 +236,7 @@ We recommend using a PostgreSQL database. For MySQL check the
gitlabhq_production> \q
```
-## 6. Redis
+## 7. Redis
GitLab requires at least Redis 2.8.
@@ -263,7 +281,7 @@ sudo service redis-server restart
sudo usermod -aG redis git
```
-## 7. GitLab
+## 8. GitLab
# We'll install GitLab into home directory of the user "git"
cd /home/git
@@ -451,7 +469,8 @@ Check if GitLab and its environment are configured correctly:
### Compile Assets
- sudo -u git -H bundle exec rake gitlab:assets:compile RAILS_ENV=production
+ sudo -u git -H yarn install --production --pure-lockfile
+ sudo -u git -H bundle exec rake gitlab:assets:compile RAILS_ENV=production NODE_ENV=production
### Start Your GitLab Instance
@@ -459,7 +478,7 @@ Check if GitLab and its environment are configured correctly:
# or
sudo /etc/init.d/gitlab restart
-## 8. Nginx
+## 9. Nginx
**Note:** Nginx is the officially supported web server for GitLab. If you cannot or do not want to use Nginx as your web server, have a look at the [GitLab recipes](https://gitlab.com/gitlab-org/gitlab-recipes/).
diff --git a/doc/integration/auth0.md b/doc/integration/auth0.md
index 212b4854dd7..c39d7ab57c6 100644
--- a/doc/integration/auth0.md
+++ b/doc/integration/auth0.md
@@ -54,7 +54,7 @@ for initial settings.
gitlab_rails['omniauth_providers'] = [
{
"name" => "auth0",
- "args" => { client_id: 'YOUR_AUTH0_CLIENT_ID'',
+ "args" => { client_id: 'YOUR_AUTH0_CLIENT_ID',
client_secret: 'YOUR_AUTH0_CLIENT_SECRET',
namespace: 'YOUR_AUTH0_DOMAIN'
}
diff --git a/doc/integration/ldap.md b/doc/integration/ldap.md
index 30f0c15dacc..242890af981 100644
--- a/doc/integration/ldap.md
+++ b/doc/integration/ldap.md
@@ -1,3 +1 @@
-# GitLab LDAP integration
-
-This document was moved under [`administration/auth/ldap`](../administration/auth/ldap.md).
+This document was moved to [`administration/auth/ldap`](../administration/auth/ldap.md).
diff --git a/doc/integration/saml.md b/doc/integration/saml.md
index 7a809eddac0..2277aa827b7 100644
--- a/doc/integration/saml.md
+++ b/doc/integration/saml.md
@@ -74,7 +74,7 @@ in your SAML IdP:
idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8',
idp_sso_target_url: 'https://login.example.com/idp',
issuer: 'https://gitlab.example.com',
- name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient'
+ name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent'
},
label: 'Company Login' # optional label for SAML login button, defaults to "Saml"
}
@@ -91,7 +91,7 @@ in your SAML IdP:
idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8',
idp_sso_target_url: 'https://login.example.com/idp',
issuer: 'https://gitlab.example.com',
- name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient'
+ name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent'
},
label: 'Company Login' # optional label for SAML login button, defaults to "Saml"
}
@@ -172,7 +172,7 @@ tell GitLab which groups are external via the `external_groups:` element:
idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8',
idp_sso_target_url: 'https://login.example.com/idp',
issuer: 'https://gitlab.example.com',
- name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient'
+ name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent'
} }
```
@@ -227,7 +227,7 @@ args: {
idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8',
idp_sso_target_url: 'https://login.example.com/idp',
issuer: 'https://gitlab.example.com',
- name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient',
+ name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent',
attribute_statements: { email: ['EmailAddress'] }
}
```
@@ -245,7 +245,7 @@ args: {
idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8',
idp_sso_target_url: 'https://login.example.com/idp',
issuer: 'https://gitlab.example.com',
- name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient',
+ name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent',
attribute_statements: { email: ['EmailAddress'] },
allowed_clock_drift: 1 # for one second clock drift
}
diff --git a/doc/pages/README.md b/doc/pages/README.md
index c9715eed598..7878bce3f10 100644
--- a/doc/pages/README.md
+++ b/doc/pages/README.md
@@ -1 +1 @@
-This document was moved to [user/project/pages](../user/project/pages/index.md).
+This document was moved to [pages/index.md](../user/project/pages/index.md).
diff --git a/doc/pages/getting_started_part_one.md b/doc/pages/getting_started_part_one.md
new file mode 100644
index 00000000000..1d63ccb4d2f
--- /dev/null
+++ b/doc/pages/getting_started_part_one.md
@@ -0,0 +1 @@
+This document was moved to [another location](../user/project/pages/getting_started_part_one.md).
diff --git a/doc/pages/getting_started_part_three.md b/doc/pages/getting_started_part_three.md
new file mode 100644
index 00000000000..1697b5cd6b4
--- /dev/null
+++ b/doc/pages/getting_started_part_three.md
@@ -0,0 +1 @@
+This document was moved to [another location](../user/project/pages/getting_started_part_three.md).
diff --git a/doc/pages/getting_started_part_two.md b/doc/pages/getting_started_part_two.md
new file mode 100644
index 00000000000..a58affec73d
--- /dev/null
+++ b/doc/pages/getting_started_part_two.md
@@ -0,0 +1 @@
+This document was moved to [another location](../user/project/pages/getting_started_part_two.md).
diff --git a/doc/profile/preferences.md b/doc/profile/preferences.md
index 073b8797508..4f2b00f3dd1 100644
--- a/doc/profile/preferences.md
+++ b/doc/profile/preferences.md
@@ -3,13 +3,6 @@
Settings in the **Profile > Preferences** page allow the user to customize
various aspects of the site to their liking.
-## Application theme
-
-Changing this setting allows the user to customize the color scheme used for the
-navigation bar on the left side of the screen.
-
-The default is **Charcoal**.
-
## Syntax highlighting theme
_GitLab uses the [rouge ruby library][rouge] for syntax highlighting. For a
diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md
index b4e13f5812a..a5b8cd6455c 100644
--- a/doc/raketasks/backup_restore.md
+++ b/doc/raketasks/backup_restore.md
@@ -84,6 +84,28 @@ Deleting tmp directories...[DONE]
Deleting old backups... [SKIPPING]
```
+## Backup Strategy Option
+
+> **Note:** Introduced as an option in 8.17
+
+The default backup strategy is to essentially stream data from the respective
+data locations to the backup using the Linux command `tar` and `gzip`. This works
+fine in most cases, but can cause problems when data is rapidly changing.
+
+When data changes while `tar` is reading it, the error `file changed as we read
+it` may occur, and will cause the backup process to fail. To combat this, 8.17
+introduces a new backup strategy called `copy`. The strategy copies data files
+to a temporary location before calling `tar` and `gzip`, avoiding the error.
+
+A side-effect is that the backup process with take up to an additional 1X disk
+space. The process does its best to clean up the temporary files at each stage
+so the problem doesn't compound, but it could be a considerable change for large
+installations. This is why the `copy` strategy is not the default in 8.17.
+
+To use the `copy` strategy instead of the default streaming strategy, specify
+`STRATEGY=copy` in the Rake task command. For example,
+`sudo gitlab-rake gitlab:backup:create STRATEGY=copy`.
+
## Exclude specific directories from the backup
You can choose what should be backed up by adding the environment variable `SKIP`.
diff --git a/doc/update/8.16-to-8.17.md b/doc/update/8.16-to-8.17.md
index 53c2bc560e8..954109ba18f 100644
--- a/doc/update/8.16-to-8.17.md
+++ b/doc/update/8.16-to-8.17.md
@@ -49,7 +49,19 @@ Install Bundler:
sudo gem install bundler --no-ri --no-rdoc
```
-### 4. Get latest code
+### 4. Update Node
+
+GitLab now runs [webpack](http://webpack.js.org) to compile frontend assets and
+it has a minimum requirement of node v4.3.0.
+
+You can check which version you are running with `node -v`. If you are running
+a version older than `v4.3.0` you will need to update to a newer version. You
+can find instructions to install from community maintained packages or compile
+from source at the nodejs.org website.
+
+<https://nodejs.org/en/download/>
+
+### 5. Get latest code
```bash
cd /home/git/gitlab
@@ -76,7 +88,7 @@ cd /home/git/gitlab
sudo -u git -H git checkout 8-17-stable-ee
```
-### 5. Install libs, migrations, etc.
+### 6. Install libs, migrations, etc.
```bash
cd /home/git/gitlab
@@ -93,13 +105,16 @@ sudo -u git -H bundle clean
# Run database migrations
sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production
+# Install/update frontend asset dependencies
+sudo -u git -H npm install --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
```
**MySQL installations**: Run through the `MySQL strings limits` and `Tables and data conversion to utf8mb4` [tasks](../install/database_mysql.md).
-### 6. Update gitlab-workhorse
+### 7. 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
@@ -111,7 +126,7 @@ 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
+### 8. Update gitlab-shell
```bash
cd /home/git/gitlab-shell
@@ -120,7 +135,7 @@ sudo -u git -H git fetch --all --tags
sudo -u git -H git checkout v4.1.1
```
-### 8. Update configuration files
+### 9. Update configuration files
#### New configuration options for `gitlab.yml`
@@ -194,14 +209,14 @@ For Ubuntu 16.04.1 LTS:
sudo systemctl daemon-reload
```
-### 9. Start application
+### 10. Start application
```bash
sudo service gitlab start
sudo service nginx restart
```
-### 10. Check application status
+### 11. Check application status
Check if GitLab and its environment are configured correctly:
diff --git a/doc/user/admin_area/settings/continuous_integration.md b/doc/user/admin_area/settings/continuous_integration.md
index b8d24cb2d3b..eb6f915f3f4 100644
--- a/doc/user/admin_area/settings/continuous_integration.md
+++ b/doc/user/admin_area/settings/continuous_integration.md
@@ -3,18 +3,36 @@
## Maximum artifacts size
The maximum size of the [job artifacts][art-yml] can be set in the Admin area
-of your GitLab instance. The value is in MB and the default is 100MB. Note that
-this setting is set for each job.
+of your GitLab instance. The value is in *MB* and the default is 100MB. Note
+that this setting is set for each job.
1. Go to **Admin area > Settings** (`/admin/application_settings`).
![Admin area settings button](img/admin_area_settings_button.png)
-1. Change the value of the maximum artifacts size (in MB):
+1. Change the value of maximum artifacts size (in MB):
![Admin area maximum artifacts size](img/admin_area_maximum_artifacts_size.png)
1. Hit **Save** for the changes to take effect.
+## Default artifacts expiration
+
+The default expiration time of the [job artifacts][art-yml] can be set in
+the Admin area of your GitLab instance. The syntax of duration is described
+in [artifacts:expire_in][duration-syntax]. The default is `30 days`. Note that
+this setting is set for each job. Set it to 0 if you don't want default
+expiration.
+
+1. Go to **Admin area > Settings** (`/admin/application_settings`).
+
+ ![Admin area settings button](img/admin_area_settings_button.png)
+
+1. Change the value of default expiration time ([syntax][duration-syntax]):
+
+ ![Admin area default artifacts expiration](img/admin_area_default_artifacts_expiration.png)
+
+1. Hit **Save** for the changes to take effect.
[art-yml]: ../../../administration/job_artifacts.md
+[duration-syntax]: ../../../ci/yaml/README.md#artifactsexpire_in
diff --git a/doc/user/admin_area/settings/img/admin_area_default_artifacts_expiration.png b/doc/user/admin_area/settings/img/admin_area_default_artifacts_expiration.png
new file mode 100644
index 00000000000..50a86ede56b
--- /dev/null
+++ b/doc/user/admin_area/settings/img/admin_area_default_artifacts_expiration.png
Binary files differ
diff --git a/doc/user/admin_area/settings/img/admin_area_maximum_artifacts_size.png b/doc/user/admin_area/settings/img/admin_area_maximum_artifacts_size.png
index b7d6671902a..33fd29e2039 100644
--- a/doc/user/admin_area/settings/img/admin_area_maximum_artifacts_size.png
+++ b/doc/user/admin_area/settings/img/admin_area_maximum_artifacts_size.png
Binary files differ
diff --git a/doc/user/profile/account/two_factor_authentication.md b/doc/user/profile/account/two_factor_authentication.md
index a23ad79ae1d..eaa39a0c4ea 100644
--- a/doc/user/profile/account/two_factor_authentication.md
+++ b/doc/user/profile/account/two_factor_authentication.md
@@ -213,5 +213,5 @@ 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/
+[FreeOTP]: https://freeotp.github.io/
[YubiKey]: https://www.yubico.com/products/yubikey-hardware/
diff --git a/doc/user/project/integrations/img/mattermost_configuration.png b/doc/user/project/integrations/img/mattermost_configuration.png
index 3c5ff5ee317..f52acf4ef3b 100644
--- a/doc/user/project/integrations/img/mattermost_configuration.png
+++ b/doc/user/project/integrations/img/mattermost_configuration.png
Binary files differ
diff --git a/doc/user/project/integrations/img/slack_configuration.png b/doc/user/project/integrations/img/slack_configuration.png
index fc8e58e686b..527824fc3eb 100644
--- a/doc/user/project/integrations/img/slack_configuration.png
+++ b/doc/user/project/integrations/img/slack_configuration.png
Binary files differ
diff --git a/doc/user/project/integrations/mattermost.md b/doc/user/project/integrations/mattermost.md
index 09ba9994d3a..cfb0931273d 100644
--- a/doc/user/project/integrations/mattermost.md
+++ b/doc/user/project/integrations/mattermost.md
@@ -24,23 +24,24 @@ There, you will see a checkbox with the following events that can be triggered:
- Push
- Issue
+- Confidential issue
- Merge request
- Note
- Tag push
- Build
+- Pipeline
- 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.
+Below each of these event checkboxes, you have an input field to enter
+which Mattermost channel you want to send that event message. Enter your preferred channel handle (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... |
+| **Webhook** | The incoming webhook URL 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. |
-
+| **Notify only broken pipelines** | If you choose to enable the **Pipeline** event and you want to be only notified about failed pipelines. |
![Mattermost configuration](img/mattermost_configuration.png)
diff --git a/doc/user/project/integrations/mock_ci.md b/doc/user/project/integrations/mock_ci.md
new file mode 100644
index 00000000000..6aefe5dbded
--- /dev/null
+++ b/doc/user/project/integrations/mock_ci.md
@@ -0,0 +1,13 @@
+# Mock CI Service
+
+**NB: This service is only listed if you are in a development environment!**
+
+To setup the mock CI service server, respond to the following endpoints
+
+- `commit_status`: `#{project.namespace.path}/#{project.path}/status/#{sha}.json`
+ - Have your service return `200 { status: ['failed'|'canceled'|'running'|'pending'|'success'|'success_with_warnings'|'skipped'|'not_found'] }`
+ - If the service returns a 404, it is interpreted as `pending`
+- `build_page`: `#{project.namespace.path}/#{project.path}/status/#{sha}`
+ - Just where the build is linked to, doesn't matter if implemented
+
+For an example of a mock CI server, see [`gitlab-org/gitlab-mock-ci-service`](https://gitlab.com/gitlab-org/gitlab-mock-ci-service)
diff --git a/doc/user/project/integrations/slack.md b/doc/user/project/integrations/slack.md
index 57a9492044b..f27f9a726fc 100644
--- a/doc/user/project/integrations/slack.md
+++ b/doc/user/project/integrations/slack.md
@@ -21,23 +21,25 @@ There, you will see a checkbox with the following events that can be triggered:
- Push
- Issue
+- Confidential issue
- Merge request
- Note
- Tag push
- Build
+- Pipeline
- 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 (`#`).
+Below each of these event checkboxes, you have an input field to enter
+which Slack channel you want to send that event message. Enter your preferred channel name **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. |
+| **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. |
+| **Notify only broken pipelines** | If you choose to enable the **Pipeline** event and you want to be only notified about failed pipelines. |
After you are all done, click **Save changes** for the changes to take effect.
diff --git a/doc/user/project/pages/getting_started_part_one.md b/doc/user/project/pages/getting_started_part_one.md
new file mode 100644
index 00000000000..c5b1aa4b654
--- /dev/null
+++ b/doc/user/project/pages/getting_started_part_one.md
@@ -0,0 +1,266 @@
+# GitLab Pages from A to Z: Part 1
+
+- **Part 1: Static Sites, Domains, DNS Records, and SSL/TLS Certificates**
+- _[Part 2: Quick Start Guide - Setting Up GitLab Pages](getting_started_part_two.md)_
+- _[Part 3: Creating and Tweaking `.gitlab-ci.yml` for GitLab Pages](getting_started_part_three.md)_
+
+----
+
+This is a comprehensive guide, made for those who want to
+publish a website with GitLab Pages but aren't familiar with
+the entire process involved.
+
+To **enable** GitLab Pages for GitLab CE (Community Edition)
+and GitLab EE (Enterprise Edition), please read the
+[admin documentation](https://docs.gitlab.com/ce/administration/pages/index.html),
+and/or watch this [video tutorial](https://youtu.be/dD8c7WNcc6s).
+
+>**Note:**
+For this guide, we assume you already have GitLab Pages
+server up and running for your GitLab instance.
+
+## What you need to know before getting started
+
+Before we begin, let's understand a few concepts first.
+
+### Static sites
+
+GitLab Pages only supports static websites, meaning,
+your output files must be HTML, CSS, and JavaScript only.
+
+To create your static site, you can either hardcode in HTML,
+CSS, and JS, or use a [Static Site Generator (SSG)](https://www.staticgen.com/)
+to simplify your code and build the static site for you,
+which is highly recommendable and much faster than hardcoding.
+
+---
+
+- Read through this technical overview on [Static versus Dynamic Websites](https://about.gitlab.com/2016/06/03/ssg-overview-gitlab-pages-part-1-dynamic-x-static/)
+- Understand [how modern Static Site Generators work](https://about.gitlab.com/2016/06/10/ssg-overview-gitlab-pages-part-2/) and what you can add to your static site
+- You can use [any SSG with GitLab Pages](https://about.gitlab.com/2016/06/17/ssg-overview-gitlab-pages-part-3-examples-ci/)
+- Fork an [example project](https://gitlab.com/pages) to build your website based upon
+
+### GitLab Pages domain
+
+If you set up a GitLab Pages project on GitLab.com,
+it will automatically be accessible under a
+[subdomain of `namespace.pages.io`](https://docs.gitlab.com/ce/user/project/pages/).
+The `namespace` is defined by your username on GitLab.com,
+or the group name you created this project under.
+
+>**Note:**
+If you use your own GitLab instance to deploy your
+site with GitLab Pages, check with your sysadmin what's your
+Pages wildcard domain. This guide is valid for any GitLab instance,
+you just need to replace Pages wildcard domain on GitLab.com
+(`*.gitlab.io`) with your own.
+
+#### Practical examples
+
+**Project Websites:**
+
+- You created a project called `blog` under your username `john`,
+therefore your project URL is `https://gitlab.com/john/blog/`.
+Once you enable GitLab Pages for this project, and build your site,
+it will be available under `https://john.gitlab.io/blog/`.
+- You created a group for all your websites called `websites`,
+and a project within this group is called `blog`. Your project
+URL is `https://gitlab.com/websites/blog/`. Once you enable
+GitLab Pages for this project, the site will live under
+`https://websites.gitlab.io/blog/`.
+
+**User and Group Websites:**
+
+- Under your username, `john`, you created a project called
+`john.gitlab.io`. Your project URL will be `https://gitlab.com/john/john.gitlab.io`.
+Once you enable GitLab Pages for your project, your website
+will be published under `https://john.gitlab.io`.
+- Under your group `websites`, you created a project called
+`websites.gitlab.io`. your project's URL will be `https://gitlab.com/websites/websites.gitlab.io`. Once you enable GitLab Pages for your project,
+your website will be published under `https://websites.gitlab.io`.
+
+**General example:**
+
+- On GitLab.com, a project site will always be available under
+`https://namespace.gitlab.io/project-name`
+- On GitLab.com, a user or group website will be available under
+`https://namespace.gitlab.io/`
+- On your GitLab instance, replace `gitlab.io` above with your
+Pages server domain. Ask your sysadmin for this information.
+
+### DNS Records
+
+A Domain Name System (DNS) web service routes visitors to websites
+by translating domain names (such as `www.example.com`) into the
+numeric IP addresses (such as `192.0.2.1`) that computers use to
+connect to each other.
+
+A DNS record is created to point a (sub)domain to a certain location,
+which can be an IP address or another domain. In case you want to use
+GitLab Pages with your own (sub)domain, you need to access your domain's
+registrar control panel to add a DNS record pointing it back to your
+GitLab Pages site.
+
+Note that **how to** add DNS records depends on which server your domain
+is hosted on. Every control panel has its own place to do it. If you are
+not an admin of your domain, and don't have access to your registrar,
+you'll need to ask for the technical support of your hosting service
+to do it for you.
+
+To help you out, we've gathered some instructions on how to do that
+for the most popular hosting services:
+
+- [Amazon](http://docs.aws.amazon.com/gettingstarted/latest/swh/getting-started-configure-route53.html)
+- [Bluehost](https://my.bluehost.com/cgi/help/559)
+- [CloudFlare](https://support.cloudflare.com/hc/en-us/articles/200169096-How-do-I-add-A-records-)
+- [cPanel](https://documentation.cpanel.net/display/ALD/Edit+DNS+Zone)
+- [DreamHost](https://help.dreamhost.com/hc/en-us/articles/215414867-How-do-I-add-custom-DNS-records-)
+- [Go Daddy](https://www.godaddy.com/help/add-an-a-record-19238)
+- [Hostgator](http://support.hostgator.com/articles/changing-dns-records)
+- [Inmotion hosting](https://my.bluehost.com/cgi/help/559)
+- [Media Temple](https://mediatemple.net/community/products/dv/204403794/how-can-i-change-the-dns-records-for-my-domain)
+- [Microsoft](https://msdn.microsoft.com/en-us/library/bb727018.aspx)
+
+If your hosting service is not listed above, you can just try to
+search the web for "how to add dns record on <my hosting service>".
+
+#### DNS A record
+
+In case you want to point a root domain (`example.com`) to your
+GitLab Pages site, deployed to `namespace.gitlab.io`, you need to
+log into your domain's admin control panel and add a DNS `A` record
+pointing your domain to Pages' server IP address. For projects on
+GitLab.com, this IP is `104.208.235.32`. For projects leaving in
+other GitLab instances (CE or EE), please contact your sysadmin
+asking for this information (which IP address is Pages server
+running on your instance).
+
+**Practical Example:**
+
+![DNS A record pointing to GitLab.com Pages server](img/dns_a_record_example.png)
+
+#### DNS CNAME record
+
+In case you want to point a subdomain (`hello-world.example.com`)
+to your GitLab Pages site initially deployed to `namespace.gitlab.io`,
+you need to log into your domain's admin control panel and add a DNS
+`CNAME` record pointing your subdomain to your website URL
+(`namespace.gitlab.io`) address.
+
+Notice that, despite it's a user or project website, the `CNAME`
+should point to your Pages domain (`namespace.gitlab.io`),
+without any `/project-name`.
+
+**Practical Example:**
+
+![DNS CNAME record pointing to GitLab.com project](img/dns_cname_record_example.png)
+
+#### TL;DR
+
+| From | DNS Record | To |
+| ---- | ---------- | -- |
+| domain.com | A | 104.208.235.32 |
+| subdomain.domain.com | CNAME | namespace.gitlab.io |
+
+> **Notes**:
+>
+> - **Do not** use a CNAME record if you want to point your
+`domain.com` to your GitLab Pages site. Use an `A` record instead.
+> - **Do not** add any special chars after the default Pages
+domain. E.g., **do not** point your `subdomain.domain.com` to
+`namespace.gitlab.io.` or `namespace.gitlab.io/`.
+
+### SSL/TLS Certificates
+
+Every GitLab Pages project on GitLab.com will be available under
+HTTPS for the default Pages domain (`*.gitlab.io`). Once you set
+up your Pages project with your custom (sub)domain, if you want
+it secured by HTTPS, you will have to issue a certificate for that
+(sub)domain and install it on your project.
+
+>**Note:**
+Certificates are NOT required to add to your custom
+(sub)domain on your GitLab Pages project, though they are
+highly recommendable.
+
+The importance of having any website securely served under HTTPS
+is explained on the introductory section of the blog post
+[Secure GitLab Pages with StartSSL](https://about.gitlab.com/2016/06/24/secure-gitlab-pages-with-startssl/#https-a-quick-overview).
+
+The reason why certificates are so important is that they encrypt
+the connection between the **client** (you, me, your visitors)
+and the **server** (where you site lives), through a keychain of
+authentications and validations.
+
+### Issuing Certificates
+
+GitLab Pages accepts [PEM](https://support.quovadisglobal.com/kb/a37/what-is-pem-format.aspx) certificates issued by
+[Certificate Authorities (CA)](https://en.wikipedia.org/wiki/Certificate_authority)
+and self-signed certificates. Of course,
+[you'd rather issue a certificate than generate a self-signed](https://en.wikipedia.org/wiki/Self-signed_certificate),
+for security reasons and for having browsers trusting your
+site's certificate.
+
+There are several different kinds of certificates, each one
+with certain security level. A static personal website will
+not require the same security level as an online banking web app,
+for instance. There are a couple Certificate Authorities that
+offer free certificates, aiming to make the internet more secure
+to everyone. The most popular is [Let's Encrypt](https://letsencrypt.org/),
+which issues certificates trusted by most of browsers, it's open
+source, and free to use. Please read through this tutorial to
+understand [how to secure your GitLab Pages website with Let's Encrypt](https://about.gitlab.com/2016/04/11/tutorial-securing-your-gitlab-pages-with-tls-and-letsencrypt/).
+
+With the same popularity, there are [certificates issued by CloudFlare](https://www.cloudflare.com/ssl/),
+which also offers a [free CDN service](https://blog.cloudflare.com/cloudflares-free-cdn-and-you/).
+Their certs are valid up to 15 years. Read through the tutorial on
+[how to add a CloudFlare Certificate to your GitLab Pages website](https://about.gitlab.com/2017/02/07/setting-up-gitlab-pages-with-cloudflare-certificates/).
+
+### Adding certificates to your project
+
+Regardless the CA you choose, the steps to add your certificate to
+your Pages project are the same.
+
+#### What do you need
+
+1. A PEM certificate
+1. An intermediate certificate
+1. A public key
+
+![Pages project - adding certificates](img/add_certificate_to_pages.png)
+
+These fields are found under your **Project**'s **Settings** > **Pages** > **New Domain**.
+
+#### What's what?
+
+- A PEM certificate is the certificate generated by the CA,
+which needs to be added to the field **Certificate (PEM)**.
+- An [intermediate certificate](https://en.wikipedia.org/wiki/Intermediate_certificate_authority) (aka "root certificate") is
+the part of the encryption keychain that identifies the CA.
+Usually it's combined with the PEM certificate, but there are
+some cases in which you need to add them manually.
+[CloudFlare certs](https://about.gitlab.com/2017/02/07/setting-up-gitlab-pages-with-cloudflare-certificates/)
+are one of these cases.
+- A public key is an encrypted key which validates
+your PEM against your domain.
+
+#### Now what?
+
+Now that you hopefully understand why you need all
+of this, it's simple:
+
+- Your PEM certificate needs to be added to the first field
+- If your certificate is missing its intermediate, copy
+and paste the root certificate (usually available from your CA website)
+and paste it in the [same field as your PEM certificate](https://about.gitlab.com/2017/02/07/setting-up-gitlab-pages-with-cloudflare-certificates/),
+just jumping a line between them.
+- Copy your public key and paste it in the last field
+
+>**Note:**
+**Do not** open certificates or encryption keys in
+regular text editors. Always use code editors (such as
+Sublime Text, Atom, Dreamweaver, Brackets, etc).
+
+|||
+|:--|--:|
+||[**Part 2: Quick start guide - Setting up GitLab Pages →**](getting_started_part_two.md)|
diff --git a/doc/user/project/pages/getting_started_part_three.md b/doc/user/project/pages/getting_started_part_three.md
new file mode 100644
index 00000000000..ef47abef3a0
--- /dev/null
+++ b/doc/user/project/pages/getting_started_part_three.md
@@ -0,0 +1,383 @@
+# GitLab Pages from A to Z: Part 3
+
+- _[Part 1: Static Sites, Domains, DNS Records, and SSL/TLS Certificates](getting_started_part_one.md)_
+- _[Part 2: Quick Start Guide - Setting Up GitLab Pages](getting_started_part_two.md)_
+- **Part 3: Creating and Tweaking `.gitlab-ci.yml` for GitLab Pages**
+
+---
+
+## Creating and Tweaking `.gitlab-ci.yml` for GitLab Pages
+
+[GitLab CI](https://about.gitlab.com/gitlab-ci/) serves
+numerous purposes, to build, test, and deploy your app
+from GitLab through
+[Continuous Integration, Continuous Delivery, and Continuous Deployment](https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/)
+methods. You will need it to build your website with GitLab Pages,
+and deploy it to the Pages server.
+
+What this file actually does is telling the
+[GitLab Runner](https://docs.gitlab.com/runner/) to run scripts
+as you would do from the command line. The Runner acts as your
+terminal. GitLab CI tells the Runner which commands to run.
+Both are built-in in GitLab, and you don't need to set up
+anything for them to work.
+
+Explaining [every detail of GitLab CI](https://docs.gitlab.com/ce/ci/yaml/README.html)
+and GitLab Runner is out of the scope of this guide, but we'll
+need to understand just a few things to be able to write our own
+`.gitlab-ci.yml` or tweak an existing one. It's an
+[Yaml](http://docs.ansible.com/ansible/YAMLSyntax.html) file,
+with its own syntax. You can always check your CI syntax with
+the [GitLab CI Lint Tool](https://gitlab.com/ci/lint).
+
+**Practical Example:**
+
+Let's consider you have a [Jekyll](https://jekyllrb.com/) site.
+To build it locally, you would open your terminal, and run `jekyll build`.
+Of course, before building it, you had to install Jekyll in your computer.
+For that, you had to open your terminal and run `gem install jekyll`.
+Right? GitLab CI + GitLab Runner do the same thing. But you need to
+write in the `.gitlab-ci.yml` the script you want to run so
+GitLab Runner will do it for you. It looks more complicated then it
+is. What you need to tell the Runner:
+
+```
+$ gem install jekyll
+$ jekyll build
+```
+
+### Script
+
+To transpose this script to Yaml, it would be like this:
+
+```yaml
+script:
+ - gem install jekyll
+ - jekyll build
+```
+
+### Job
+
+So far so good. Now, each `script`, in GitLab is organized by
+a `job`, which is a bunch of scripts and settings you want to
+apply to that specific task.
+
+```yaml
+job:
+ script:
+ - gem install jekyll
+ - jekyll build
+```
+
+For GitLab Pages, this `job` has a specific name, called `pages`,
+which tells the Runner you want that task to deploy your website
+with GitLab Pages:
+
+```yaml
+pages:
+ script:
+ - gem install jekyll
+ - jekyll build
+```
+
+### The `public` directory
+
+We also need to tell Jekyll where do you want the website to build,
+and GitLab Pages will only consider files in a directory called `public`.
+To do that with Jekyll, we need to add a flag specifying the
+[destination (`-d`)](https://jekyllrb.com/docs/usage/) of the
+built website: `jekyll build -d public`. Of course, we need
+to tell this to our Runner:
+
+```yaml
+pages:
+ script:
+ - gem install jekyll
+ - jekyll build -d public
+```
+
+### Artifacts
+
+We also need to tell the Runner that this _job_ generates
+_artifacts_, which is the site built by Jekyll.
+Where are these artifacts stored? In the `public` directory:
+
+```yaml
+pages:
+ script:
+ - gem install jekyll
+ - jekyll build -d public
+ artifacts:
+ paths:
+ - public
+```
+
+The script above would be enough to build your Jekyll
+site with GitLab Pages. But, from Jekyll 3.4.0 on, its default
+template originated by `jekyll new project` requires
+[Bundler](http://bundler.io/) to install Jekyll dependencies
+and the default theme. To adjust our script to meet these new
+requirements, we only need to install and build Jekyll with Bundler:
+
+```yaml
+pages:
+ script:
+ - bundle install
+ - bundle exec jekyll build -d public
+ artifacts:
+ paths:
+ - public
+```
+
+That's it! A `.gitlab-ci.yml` with the content above would deploy
+your Jekyll 3.4.0 site with GitLab Pages. This is the minimum
+configuration for our example. On the steps below, we'll refine
+the script by adding extra options to our GitLab CI.
+
+### Image
+
+At this point, you probably ask yourself: "okay, but to install Jekyll
+I need Ruby. Where is Ruby on that script?". The answer is simple: the
+first thing GitLab Runner will look for in your `.gitlab-ci.yml` is a
+[Docker](https://www.docker.com/) image specifying what do you need in
+your container to run that script:
+
+```yaml
+image: ruby:2.3
+
+pages:
+ script:
+ - bundle install
+ - bundle exec jekyll build -d public
+ artifacts:
+ paths:
+ - public
+```
+
+In this case, you're telling the Runner to pull this image, which
+contains Ruby 2.3 as part of its file system. When you don't specify
+this image in your configuration, the Runner will use a default
+image, which is Ruby 2.1.
+
+If your SSG needs [NodeJS](https://nodejs.org/) to build, you'll
+need to specify which image you want to use, and this image should
+contain NodeJS as part of its file system. E.g., for a
+[Hexo](https://gitlab.com/pages/hexo) site, you can use `image: node:4.2.2`.
+
+>**Note:**
+We're not trying to explain what a Docker image is,
+we just need to introduce the concept with a minimum viable
+explanation. To know more about Docker images, please visit
+their website or take a look at a
+[summarized explanation](http://paislee.io/how-to-automate-docker-deployments/) here.
+
+Let's go a little further.
+
+### Branching
+
+If you use GitLab as a version control platform, you will have your
+branching strategy to work on your project. Meaning, you will have
+other branches in your project, but you'll want only pushes to the
+default branch (usually `master`) to be deployed to your website.
+To do that, we need to add another line to our CI, telling the Runner
+to only perform that _job_ called `pages` on the `master` branch `only`:
+
+```yaml
+image: ruby:2.3
+
+pages:
+ script:
+ - bundle install
+ - bundle exec jekyll build -d public
+ artifacts:
+ paths:
+ - public
+ only:
+ - master
+```
+
+### Stages
+
+Another interesting concept to keep in mind are build stages.
+Your web app can pass through a lot of tests and other tasks
+until it's deployed to staging or production environments.
+There are three default stages on GitLab CI: build, test,
+and deploy. To specify which stage your _job_ is running,
+simply add another line to your CI:
+
+```yaml
+image: ruby:2.3
+
+pages:
+ stage: deploy
+ script:
+ - bundle install
+ - bundle exec jekyll build -d public
+ artifacts:
+ paths:
+ - public
+ only:
+ - master
+```
+
+You might ask yourself: "why should I bother with stages
+at all?" Well, let's say you want to be able to test your
+script and check the built site before deploying your site
+to production. You want to run the test exactly as your
+script will do when you push to `master`. It's simple,
+let's add another task (_job_) to our CI, telling it to
+test every push to other branches, `except` the `master` branch:
+
+```yaml
+image: ruby:2.3
+
+pages:
+ stage: deploy
+ script:
+ - bundle install
+ - bundle exec jekyll build -d public
+ artifacts:
+ paths:
+ - public
+ only:
+ - master
+
+test:
+ stage: test
+ script:
+ - bundle install
+ - bundle exec jekyll build -d test
+ artifacts:
+ paths:
+ - test
+ except:
+ - master
+```
+
+The `test` job is running on the stage `test`, Jekyll
+will build the site in a directory called `test`, and
+this job will affect all the branches except `master`.
+
+The best benefit of applying _stages_ to different
+_jobs_ is that every job in the same stage builds in
+parallel. So, if your web app needs more than one test
+before being deployed, you can run all your test at the
+same time, it's not necessary to wait one test to finish
+to run the other. Of course, this is just a brief
+introduction of GitLab CI and GitLab Runner, which are
+tools much more powerful than that. This is what you
+need to be able to create and tweak your builds for
+your GitLab Pages site.
+
+### Before Script
+
+To avoid running the same script multiple times across
+your _jobs_, you can add the parameter `before_script`,
+in which you specify which commands you want to run for
+every single _job_. In our example, notice that we run
+`bundle install` for both jobs, `pages` and `test`.
+We don't need to repeat it:
+
+```yaml
+image: ruby:2.3
+
+before_script:
+ - bundle install
+
+pages:
+ stage: deploy
+ script:
+ - bundle exec jekyll build -d public
+ artifacts:
+ paths:
+ - public
+ only:
+ - master
+
+test:
+ stage: test
+ script:
+ - bundle exec jekyll build -d test
+ artifacts:
+ paths:
+ - test
+ except:
+ - master
+```
+
+### Caching Dependencies
+
+If you want to cache the installation files for your
+projects dependencies, for building faster, you can
+use the parameter `cache`. For this example, we'll
+cache Jekyll dependencies in a `vendor` directory
+when we run `bundle install`:
+
+```yaml
+image: ruby:2.3
+
+cache:
+ paths:
+ - vendor/
+
+before_script:
+ - bundle install --path vendor
+
+pages:
+ stage: deploy
+ script:
+ - bundle exec jekyll build -d public
+ artifacts:
+ paths:
+ - public
+ only:
+ - master
+
+test:
+ stage: test
+ script:
+ - bundle exec jekyll build -d test
+ artifacts:
+ paths:
+ - test
+ except:
+ - master
+```
+
+For this specific case, we need to exclude `/vendor`
+from Jekyll `_config.yml` file, otherwise Jekyll will
+understand it as a regular directory to build
+together with the site:
+
+```yml
+exclude:
+ - vendor
+```
+
+There we go! Now our GitLab CI not only builds our website,
+but also **continuously test** pushes to feature-branches,
+**caches** dependencies installed with Bundler, and
+**continuously deploy** every push to the `master` branch.
+
+## Advanced GitLab CI for GitLab Pages
+
+What you can do with GitLab CI is pretty much up to your
+creativity. Once you get used to it, you start creating
+awesome scripts that automate most of tasks you'd do
+manually in the past. Read through the
+[documentation of GitLab CI](https://docs.gitlab.com/ce/ci/yaml/README.html)
+to understand how to go even further on your scripts.
+
+- On this blog post, understand the concept of
+[using GitLab CI `environments` to deploy your
+web app to staging and production](https://about.gitlab.com/2016/08/26/ci-deployment-and-environments/).
+- On this post, learn [how to run jobs sequentially,
+in parallel, or build a custom pipeline](https://about.gitlab.com/2016/07/29/the-basics-of-gitlab-ci/)
+- On this blog post, we go through the process of
+[pulling specific directories from different projects](https://about.gitlab.com/2016/12/07/building-a-new-gitlab-docs-site-with-nanoc-gitlab-ci-and-gitlab-pages/)
+to deploy this website you're looking at, docs.gitlab.com.
+- On this blog post, we teach you [how to use GitLab Pages to produce a code coverage report](https://about.gitlab.com/2016/11/03/publish-code-coverage-report-with-gitlab-pages/).
+
+|||
+|:--|--:|
+|[**← Part 2: Quick start guide - Setting up GitLab Pages**](getting_started_part_two.md)||
diff --git a/doc/user/project/pages/getting_started_part_two.md b/doc/user/project/pages/getting_started_part_two.md
new file mode 100644
index 00000000000..07dd24122c4
--- /dev/null
+++ b/doc/user/project/pages/getting_started_part_two.md
@@ -0,0 +1,152 @@
+# GitLab Pages from A to Z: Part 2
+
+> Type: user guide
+>
+> Level: beginner
+
+- _[Part 1: Static Sites, Domains, DNS Records, and SSL/TLS Certificates](getting_started_part_one.md)_
+- **Part 2: Quick Start Guide - Setting Up GitLab Pages**
+- _[Part 3: Creating and Tweaking `.gitlab-ci.yml` for GitLab Pages](getting_started_part_three.md)_
+
+----
+
+## Setting up GitLab Pages
+
+For a complete step-by-step tutorial, please read the
+blog post [Hosting on GitLab.com with GitLab Pages](https://about.gitlab.com/2016/04/07/gitlab-pages-setup/). The following sections will explain
+what do you need and why do you need them.
+
+## What you need to get started
+
+1. A project
+1. A configuration file (`.gitlab-ci.yml`) to deploy your site
+1. A specific `job` called `pages` in the configuration file
+that will make GitLab aware that you are deploying a GitLab Pages website
+
+Optional Features:
+
+1. A custom domain or subdomain
+1. A DNS pointing your (sub)domain to your Pages site
+ 1. **Optional**: an SSL/TLS certificate so your custom
+ domain is accessible under HTTPS.
+
+## Project
+
+Your GitLab Pages project is a regular project created the
+same way you do for the other ones. To get started with GitLab Pages, you have two ways:
+
+- Fork one of the templates from Page Examples, or
+- Create a new project from scratch
+
+Let's go over both options.
+
+### Fork a project to get started from
+
+To make things easy for you, we've created this
+[group](https://gitlab.com/pages) of default projects
+containing the most popular SSGs templates.
+
+Watch the [video tutorial](https://youtu.be/TWqh9MtT4Bg) we've
+created for the steps below.
+
+1. Choose your SSG template
+1. Fork a project from the [Pages group](https://gitlab.com/pages)
+1. Remove the fork relationship by navigating to your **Project**'s **Settings** > **Edit Project**
+
+ ![remove fork relashionship](img/remove_fork_relashionship.png)
+
+1. Enable Shared Runners for your fork: navigate to your **Project**'s **Settings** > **CI/CD Pipelines**
+1. Trigger a build (push a change to any file)
+1. As soon as the build passes, your website will have been deployed with GitLab Pages. Your website URL will be available under your **Project**'s **Settings** > **Pages**
+
+To turn a **project website** forked from the Pages group into a **user/group** website, you'll need to:
+
+- Rename it to `namespace.gitlab.io`: navigate to **Project**'s **Settings** > **Edit Project** > **Rename repository**
+- Adjust your SSG's [base URL](#urls-and-baseurls) to from `"project-name"` to `""`. This setting will be at a different place for each SSG, as each of them have their own structure and file tree. Most likelly, it will be in the SSG's config file.
+
+> **Notes:**
+>
+>1. Why do I need to remove the fork relationship?
+>
+> Unless you want to contribute to the original project,
+you won't need it connected to the upstream. A
+[fork](https://about.gitlab.com/2016/12/01/how-to-keep-your-fork-up-to-date-with-its-origin/#fork)
+is useful for submitting merge requests to the upstream.
+>
+> 2. Why do I need to enable Shared Runners?
+>
+> Shared Runners will run the script set by your GitLab CI
+configuration file. They're enabled by default to new projects,
+but not to forks.
+
+### Create a project from scratch
+
+1. From your **Project**'s **[Dashboard](https://gitlab.com/dashboard/projects)**,
+click **New project**, and name it considering the
+[practical examples](getting_started_part_one.md#practical-examples).
+1. Clone it to your local computer, add your website
+files to your project, add, commit and push to GitLab.
+1. From the your **Project**'s page, click **Set up CI**:
+
+ ![setup GitLab CI](img/setup_ci.png)
+
+1. Choose one of the templates from the dropbox menu.
+Pick up the template corresponding to the SSG you're using (or plain HTML).
+
+ ![gitlab-ci templates](img/choose_ci_template.png)
+
+Once you have both site files and `.gitlab-ci.yml` in your project's
+root, GitLab CI will build your site and deploy it with Pages.
+Once the first build passes, you see your site is live by
+navigating to your **Project**'s **Settings** > **Pages**,
+where you'll find its default URL.
+
+> **Notes:**
+>
+> - GitLab Pages [supports any SSG](https://about.gitlab.com/2016/06/17/ssg-overview-gitlab-pages-part-3-examples-ci/), but,
+if you don't find yours among the templates, you'll need
+to configure your own `.gitlab-ci.yml`. Do do that, please
+read through the article [Creating and Tweaking `.gitlab-ci.yml` for GitLab Pages](getting_started_part_three.md). New SSGs are very welcome among
+the [example projects](https://gitlab.com/pages). If you set
+up a new one, please
+[contribute](https://gitlab.com/pages/pages.gitlab.io/blob/master/CONTRIBUTING.md)
+to our examples.
+>
+> - The second step _"Clone it to your local computer"_, can be done
+differently, achieving the same results: instead of cloning the bare
+repository to you local computer and moving your site files into it,
+you can run `git init` in your local website directory, add the
+remote URL: `git remote add origin git@gitlab.com:namespace/project-name.git`,
+then add, commit, and push.
+
+### URLs and Baseurls
+
+Every Static Site Generator (SSG) default configuration expects
+to find your website under a (sub)domain (`example.com`), not
+in a subdirectory of that domain (`example.com/subdir`). Therefore,
+whenever you publish a project website (`namespace.gitlab.io/project-name`),
+you'll have to look for this configuration (base URL) on your SSG's
+documentation and set it up to reflect this pattern.
+
+For example, for a Jekyll site, the `baseurl` is defined in the Jekyll
+configuration file, `_config.yml`. If your website URL is
+`https://john.gitlab.io/blog/`, you need to add this line to `_config.yml`:
+
+```yaml
+baseurl: "/blog"
+```
+
+On the contrary, if you deploy your website after forking one of
+our [default examples](https://gitlab.com/pages), the baseurl will
+already be configured this way, as all examples there are project
+websites. If you decide to make yours a user or group website, you'll
+have to remove this configuration from your project. For the Jekyll
+example we've just mentioned, you'd have to change Jekyll's `_config.yml` to:
+
+```yaml
+baseurl: ""
+```
+
+|||
+|:--|--:|
+|[**← Part 1: Static sites, domains, DNS records, and SSL/TLS certificates**](getting_started_part_one.md)|[**Part 3: Creating and tweaking `.gitlab-ci.yml` for GitLab Pages →**](getting_started_part_three.md)|
diff --git a/doc/user/project/pages/img/add_certificate_to_pages.png b/doc/user/project/pages/img/add_certificate_to_pages.png
new file mode 100644
index 00000000000..d92a981dc60
--- /dev/null
+++ b/doc/user/project/pages/img/add_certificate_to_pages.png
Binary files differ
diff --git a/doc/user/project/pages/img/choose_ci_template.png b/doc/user/project/pages/img/choose_ci_template.png
new file mode 100644
index 00000000000..0697542abc8
--- /dev/null
+++ b/doc/user/project/pages/img/choose_ci_template.png
Binary files differ
diff --git a/doc/user/project/pages/img/dns_a_record_example.png b/doc/user/project/pages/img/dns_a_record_example.png
new file mode 100644
index 00000000000..b923730388a
--- /dev/null
+++ b/doc/user/project/pages/img/dns_a_record_example.png
Binary files differ
diff --git a/doc/user/project/pages/img/dns_cname_record_example.png b/doc/user/project/pages/img/dns_cname_record_example.png
new file mode 100644
index 00000000000..43d1a838544
--- /dev/null
+++ b/doc/user/project/pages/img/dns_cname_record_example.png
Binary files differ
diff --git a/doc/user/project/pages/img/pages_create_project.png b/doc/user/project/pages/img/pages_create_project.png
index a936d8e5dbd..be47f9d2a44 100644
--- a/doc/user/project/pages/img/pages_create_project.png
+++ 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
index 3f615d3757d..2f1a19ae424 100644
--- a/doc/user/project/pages/img/pages_create_user_page.png
+++ 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
index 8d34f3b7f38..274e98fde4d 100644
--- a/doc/user/project/pages/img/pages_dns_details.png
+++ 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
index 2bc7cee07a6..6bc92db6b41 100644
--- a/doc/user/project/pages/img/pages_multiple_domains.png
+++ 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
index c3640133bb2..cd59defa006 100644
--- a/doc/user/project/pages/img/pages_new_domain_button.png
+++ 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
index adbfb654877..b064310380e 100644
--- a/doc/user/project/pages/img/pages_remove.png
+++ 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
index 06d85ab1971..dc431ea3fef 100644
--- a/doc/user/project/pages/img/pages_upload_cert.png
+++ b/doc/user/project/pages/img/pages_upload_cert.png
Binary files differ
diff --git a/doc/user/project/pages/img/remove_fork_relashionship.png b/doc/user/project/pages/img/remove_fork_relashionship.png
new file mode 100644
index 00000000000..67c45491f08
--- /dev/null
+++ b/doc/user/project/pages/img/remove_fork_relashionship.png
Binary files differ
diff --git a/doc/user/project/pages/img/setup_ci.png b/doc/user/project/pages/img/setup_ci.png
new file mode 100644
index 00000000000..214c1cc668f
--- /dev/null
+++ b/doc/user/project/pages/img/setup_ci.png
Binary files differ
diff --git a/doc/user/project/pages/index.md b/doc/user/project/pages/index.md
index 4c4f15aad40..dbb9d9ad9c4 100644
--- a/doc/user/project/pages/index.md
+++ b/doc/user/project/pages/index.md
@@ -1,437 +1,49 @@
-# GitLab Pages
+# All you need to know about 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 create static websites for your GitLab projects,
+groups, or user accounts. You can use any static website generator: Jekyll,
+Middleman, Hexo, Hugo, Pelican, you name it! Connect as many customs domains
+as you like and bring your own TLS certificate to secure them.
-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.
+Here's some info we have gathered to get you started.
-Read [GitLab Pages on GitLab.com](#gitlab-pages-on-gitlab-com) for specific
-information, if you are using GitLab.com to host your website.
+## General info
-## Getting started with GitLab Pages
+- [Product webpage](https://pages.gitlab.io)
+- [We're bringing GitLab Pages to CE](https://about.gitlab.com/2016/12/24/were-bringing-gitlab-pages-to-community-edition/)
+- [Pages group - templates](https://gitlab.com/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`.
+## Getting started
-In general there are two types of pages one might create:
+- GitLab Pages from A to Z
+ - [Part 1: Static sites, domains, DNS records, and SSL/TLS certificates](getting_started_part_one.md)
+ - [Part 2: Quick start guide - Setting up GitLab Pages](getting_started_part_two.md)
+ - [Part 3: Creating and tweaking `.gitlab-ci.yml` for GitLab Pages](getting_started_part_three.md)
+- [Hosting on GitLab.com with GitLab Pages](https://about.gitlab.com/2016/04/07/gitlab-pages-setup/) a comprehensive step-by-step guide
+- Secure GitLab Pages custom domain with SSL/TLS certificates
+ - [Let's Encrypt](https://about.gitlab.com/2016/04/11/tutorial-securing-your-gitlab-pages-with-tls-and-letsencrypt/)
+ - [CloudFlare](https://about.gitlab.com/2017/02/07/setting-up-gitlab-pages-with-cloudflare-certificates/)
+ - [StartSSL](https://about.gitlab.com/2016/06/24/secure-gitlab-pages-with-startssl/)
+- Static Site Generators - Blog posts series
+ - [SSGs part 1: Static vs dynamic websites](https://about.gitlab.com/2016/06/03/ssg-overview-gitlab-pages-part-1-dynamic-x-static/)
+ - [SSGs part 2: Modern static site generators](https://about.gitlab.com/2016/06/10/ssg-overview-gitlab-pages-part-2/)
+ - [SSGs part 3: Build any SSG site with GitLab Pages](https://about.gitlab.com/2016/06/17/ssg-overview-gitlab-pages-part-3-examples-ci/)
+- [Posting to your GitLab Pages blog from iOS](https://about.gitlab.com/2016/08/19/posting-to-your-gitlab-pages-blog-from-ios/)
-- Pages per user (`username.example.io`) or per group (`groupname.example.io`)
-- Pages per project (`username.example.io/projectname` or `groupname.example.io/projectname`)
+## Video tutorials
-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.
+- [How to publish a website with GitLab Pages on GitLab.com: from a forked project](https://youtu.be/TWqh9MtT4Bg)
+- [How to Enable GitLab Pages for GitLab CE and EE](https://youtu.be/dD8c7WNcc6s)
-| 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`|
+## Advanced use
-> **Warning:**
-> There are some known [limitations](#limitations) regarding namespaces served
-> under the general domain name and HTTPS. Make sure to read that section.
+- Blog Posts:
+ - [GitLab CI: Run jobs sequentially, in parallel, or build a custom pipeline](https://about.gitlab.com/2016/07/29/the-basics-of-gitlab-ci/)
+ - [GitLab CI: Deployment & environments](https://about.gitlab.com/2016/08/26/ci-deployment-and-environments/)
+ - [Building a new GitLab docs site with Nanoc, GitLab CI, and GitLab Pages](https://about.gitlab.com/2016/12/07/building-a-new-gitlab-docs-site-with-nanoc-gitlab-ci-and-gitlab-pages/)
+ - [Publish code coverage reports with GitLab Pages](https://about.gitlab.com/2016/11/03/publish-code-coverage-report-with-gitlab-pages/)
-### GitLab Pages requirements
+## Specific documentation
-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][yaml] 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][yaml] 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 job traces.
-
-> **Note:**
-> Before reading this section, make sure you familiarize yourself with GitLab CI
-> and the specific syntax of[`.gitlab-ci.yml`][yaml] by
-> following our [quick start guide].
-
-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 job 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 jobs 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 jobs 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 job 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://docs.gitlab.com/runner
-[pages]: ../../../ci/yaml/README.md#pages
-[yaml]: ../../../ci/yaml/README.md
-[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
-[quick start guide]: ../../../ci/quick_start/README.md
+- [User docs](introduction.md)
+- [Admin docs](../../../administration/pages/index.md)
diff --git a/doc/user/project/pages/introduction.md b/doc/user/project/pages/introduction.md
new file mode 100644
index 00000000000..deaceabb7c5
--- /dev/null
+++ b/doc/user/project/pages/introduction.md
@@ -0,0 +1,447 @@
+# 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.
+
+Read through [All you Need to Know About GitLab Pages][pages-index-guide] for a list of all learning materials we have prepared for GitLab Pages (webpages, articles, guides, blog posts, video tutorials).
+
+## 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][yaml] 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][yaml] 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`.
+
+## Quick Start
+
+Read through [GitLab Pages Quick Start Guide][pages-quick] or watch the video tutorial on
+[how to publish a website with GitLab Pages on GitLab.com from a forked project][video-pages-fork].
+
+See also [All you Need to Know About GitLab Pages][pages-index-guide] for a list with all the resources we have for GitLab Pages.
+
+### 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 job traces.
+
+> **Note:**
+> Before reading this section, make sure you familiarize yourself with GitLab CI
+> and the specific syntax of[`.gitlab-ci.yml`][yaml] by
+> following our [quick start guide].
+
+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 job 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 jobs 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 jobs 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 job 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://docs.gitlab.com/runner/
+[pages]: ../../../ci/yaml/README.md#pages
+[yaml]: ../../../ci/yaml/README.md
+[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-ce/issues?label_name=pages
+[ce-14605]: https://gitlab.com/gitlab-org/gitlab-ce/issues/14605
+[quick start guide]: ../../../ci/quick_start/README.md
+[pages-index-guide]: index.md
+[pages-quick]: getting_started_part_one.md
+[video-pages-fork]: https://youtu.be/TWqh9MtT4Bg
diff --git a/doc/user/project/pipelines/job_artifacts.md b/doc/user/project/pipelines/job_artifacts.md
index f85f4bf8e1e..5ce99843301 100644
--- a/doc/user/project/pipelines/job_artifacts.md
+++ b/doc/user/project/pipelines/job_artifacts.md
@@ -90,18 +90,43 @@ inside GitLab that make that possible.
It is possible to download the latest artifacts of a job via a well known URL
so you can use it for scripting purposes.
-The structure of the URL is the following:
+The structure of the URL to download the whole artifacts archive is the following:
```
https://example.com/<namespace>/<project>/builds/artifacts/<ref>/download?job=<job_name>
```
-For example, to download the latest artifacts of the job named `rspec 6 20` of
+To download a single file from the artifacts use the following URL:
+
+```
+https://example.com/<namespace>/<project>/builds/artifacts/<ref>/file/<path_to_file>?job=<job_name>
+```
+
+For example, to download the latest artifacts of the job named `coverage` of
the `master` branch of the `gitlab-ce` project that belongs to the `gitlab-org`
namespace, the URL would be:
```
-https://gitlab.com/gitlab-org/gitlab-ce/builds/artifacts/master/download?job=rspec+6+20
+https://gitlab.com/gitlab-org/gitlab-ce/builds/artifacts/master/download?job=coverage
+```
+
+To download the file `coverage/index.html` from the same
+artifacts use the following URL:
+
+```
+https://gitlab.com/gitlab-org/gitlab-ce/builds/artifacts/master/file/coverage/index.html?job=coverage
+```
+
+There is also a URL to browse the latest job artifacts:
+
+```
+https://example.com/<namespace>/<project>/builds/artifacts/<ref>/browse?job=<job_name>
+```
+
+For example:
+
+```
+https://gitlab.com/gitlab-org/gitlab-ce/builds/artifacts/master/browse?job=coverage
```
The latest builds are also exposed in the UI in various places. Specifically,
diff --git a/doc/user/project/pipelines/settings.md b/doc/user/project/pipelines/settings.md
index 80cdb49a1d3..c398ac2eb25 100644
--- a/doc/user/project/pipelines/settings.md
+++ b/doc/user/project/pipelines/settings.md
@@ -62,9 +62,9 @@ pipelines** checkbox and save the changes.
## Badges
-In the pipelines settings page you can find job status and test coverage
+In the pipelines settings page you can find pipeline status and test coverage
badges for your project. The latest successful pipeline will be used to read
-the job status and test coverage values.
+the pipeline status and test coverage values.
Visit the pipelines settings page in your project to see the exact link to
your badges, as well as ways to embed the badge image in your HTML or Markdown
@@ -72,7 +72,7 @@ pages.
![Pipelines badges](img/pipelines_settings_badges.png)
-### Job status badge
+### Pipeline status badge
Depending on the status of your job, a badge can have the following values:
@@ -82,7 +82,7 @@ Depending on the status of your job, a badge can have the following values:
- skipped
- unknown
-You can access a job status badge image using the following link:
+You can access a pipeline status badge image using the following link:
```
https://example.gitlab.com/<namespace>/<project>/badges/<branch>/build.svg
diff --git a/doc/user/snippets.md b/doc/user/snippets.md
new file mode 100644
index 00000000000..417360e08ac
--- /dev/null
+++ b/doc/user/snippets.md
@@ -0,0 +1,19 @@
+# Snippets
+
+Snippets are little bits of code or text.
+
+There are 2 types of snippets - project snippets and personal snippets.
+
+## Project snippets
+
+Project snippets are always related to a specific project - see [Project features](../workflow/project_features.md) for more information.
+
+## Personal snippets
+
+Personal snippets are not related to any project and can be created completely independently. There are 3 visibility levels that can be set (public, internal, private - see [Public Access](../public_access/public_access.md) for more information).
+
+## Downloading snippets
+
+You can download the raw content of a snippet.
+
+By default snippets will be downloaded with Linux-style line endings (`LF`). If you want to preserve the original line endings you need to add a parameter `line_ending=raw` (eg. `https://gitlab.com/snippets/SNIPPET_ID/raw?line_ending=raw`). In case a snippet was created using the GitLab web interface the original line ending is Windows-like (`CRLF`).
diff --git a/doc/workflow/README.md b/doc/workflow/README.md
index 7a97b87f1c5..9e7ee47387c 100644
--- a/doc/workflow/README.md
+++ b/doc/workflow/README.md
@@ -39,3 +39,4 @@
- [Manage large binaries with Git LFS](lfs/manage_large_binaries_with_git_lfs.md)
- [Importing from SVN, GitHub, Bitbucket, etc](importing/README.md)
- [Todos](todos.md)
+- [Snippets](../user/snippets.md)
diff --git a/doc/workflow/gitlab_flow.md b/doc/workflow/gitlab_flow.md
index c228ea72f22..d12c0c6d0c4 100644
--- a/doc/workflow/gitlab_flow.md
+++ b/doc/workflow/gitlab_flow.md
@@ -67,7 +67,7 @@ With GitLab flow we offer additional guidance for these questions.
![Master branch and production branch with arrow that indicate deployments](production_branch.png)
GitHub flow does assume you are able to deploy to production every time you merge a feature branch.
-This is possible for SaaS applications but are many cases where this is not possible.
+This is possible for SaaS applications but there are many cases where this is not possible.
One would be a situation where you are not in control of the exact release moment, for example an iOS application that needs to pass App Store validation.
Another example is when you have deployment windows (workdays from 10am to 4pm when the operations team is at full capacity) but you also merge code at other times.
In these cases you can make a production branch that reflects the deployed code.
@@ -203,7 +203,7 @@ But the advantages of having stable identifiers outweigh this drawback.
And to understand a change in context one can always look at the merge commit that groups all the commits together when the code is merged into the master branch.
After you merge multiple commits from a feature branch into the master branch this is harder to undo.
-If you would have squashed all the commits into one you could have just reverted this commit but as we indicated you should not rebase commits after they are pushed.
+If you had squashed all the commits into one you could have just reverted this commit but as we indicated you should not rebase commits after they are pushed.
Fortunately [reverting a merge made some time ago](https://git-scm.com/blog/2010/03/02/undoing-merges.html) can be done with git.
This however, requires having specific merge commits for the commits your want to revert.
If you revert a merge and you change your mind, revert the revert instead of merging again since git will not allow you to merge the code again otherwise.
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 8c5020bee37..9cc45065eb2 100644
--- a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md
+++ b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md
@@ -63,6 +63,12 @@ git commit -am "Added Debian iso" # commit the file meta data
git push origin master # sync the git repo and large file to the GitLab server
```
+>**Note**: Make sure that `.gitattributes` is tracked by git. Otherwise Git
+ LFS will not be working properly for people cloning the project.
+ ```bash
+ git add .gitattributes
+ ```
+
Cloning the repository works the same as before. Git automatically detects the
LFS-tracked files and clones them via HTTP. If you performed the git clone
command with a SSH URL, you have to enter your GitLab credentials for HTTP
diff --git a/doc/workflow/todos.md b/doc/workflow/todos.md
index 99d7c18f072..4b0fba842e9 100644
--- a/doc/workflow/todos.md
+++ b/doc/workflow/todos.md
@@ -16,7 +16,8 @@ in a simple dashboard.
You can quickly access the Todos dashboard using the bell icon next to the
search bar in the upper right corner. The number in blue is the number of Todos
-you still have open.
+you still have open if the count is < 100, else it's 99+. The exact number
+will still be shown in the body of the _To do_ tab.
![Todos icon](img/todos_icon.png)
@@ -32,6 +33,29 @@ A Todo appears in your Todos dashboard when:
>**Note:** Commenting on a commit will _not_ trigger a Todo.
+### Directly addressed Todos
+
+> [Introduced][ce-7926] in GitLab 9.0.
+
+If you are mentioned at the start of a line, the todo you receive will be listed
+as 'directly addressed'. For instance, in this comment:
+
+```markdown
+@alice What do you think? cc: @bob
+
+- @carol can you please have a look?
+
+>>>
+@dan what do you think?
+>>>
+
+@erin @frank thank you!
+```
+
+The people receiving directly addressed todos are `@alice`, `@erin`, and
+`@frank`. Directly addressed todos only differ from mention todos in their type,
+for filtering; otherwise, they appear as normal.
+
### Manually creating a Todo
You can also add an issue or merge request to your Todos dashboard by clicking
@@ -85,8 +109,9 @@ There are four kinds of filters you can use on your Todos dashboard.
| Project | Filter by project |
| Author | Filter by the author that triggered the Todo |
| Type | Filter by issue or merge request |
-| Action | Filter by the action that triggered the Todo (Assigned or Mentioned)|
+| Action | Filter by the action that triggered the Todo |
You can also filter by more than one of these at the same time.
[ce-2817]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2817
+[ce-7926]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7926
diff --git a/features/dashboard/dashboard.feature b/features/dashboard/dashboard.feature
index 92061dac7f4..b1d5e4a7acb 100644
--- a/features/dashboard/dashboard.feature
+++ b/features/dashboard/dashboard.feature
@@ -11,6 +11,7 @@ Feature: Dashboard
And I visit dashboard page
Scenario: I should see projects list
+ Then I should see "New Project" link
Then I should see "Shop" project link
Then I should see "Shop" project CI status
diff --git a/features/dashboard/issues.feature b/features/dashboard/issues.feature
deleted file mode 100644
index 99dad88a402..00000000000
--- a/features/dashboard/issues.feature
+++ /dev/null
@@ -1,21 +0,0 @@
-@dashboard
-Feature: Dashboard Issues
- Background:
- Given I sign in as a user
- And I have authored issues
- And I have assigned issues
- And I have other issues
- And I visit dashboard issues page
-
- Scenario: I should see assigned issues
- Then I should see issues assigned to me
-
- @javascript
- Scenario: I should see authored issues
- When I click "Authored by me" link
- Then I should see issues authored by me
-
- @javascript
- Scenario: I should see all issues
- When I click "All" link
- Then I should see all issues
diff --git a/features/project/commits/branches.feature b/features/project/commits/branches.feature
index 88fef674c0c..c57376aecff 100644
--- a/features/project/commits/branches.feature
+++ b/features/project/commits/branches.feature
@@ -13,6 +13,7 @@ Feature: Project Commits Branches
Given I visit project protected branches page
Then I should see "Shop" protected branches list
+ @javascript
Scenario: I create a branch
Given I visit project branches page
And I click new branch link
@@ -33,12 +34,7 @@ Feature: Project Commits Branches
And I submit new branch form with invalid name
Then I should see new an error that branch is invalid
- Scenario: I create a branch with invalid reference
- Given I visit project branches page
- And I click new branch link
- And I submit new branch form with invalid reference
- Then I should see new an error that ref is invalid
-
+ @javascript
Scenario: I create a branch that already exists
Given I visit project branches page
And I click new branch link
diff --git a/features/project/labels.feature b/features/project/labels.feature
deleted file mode 100644
index 955bc3d8b1b..00000000000
--- a/features/project/labels.feature
+++ /dev/null
@@ -1,15 +0,0 @@
-@labels
-Feature: Labels
- Background:
- Given I sign in as a user
- And I own project "Shop"
- And project "Shop" has labels: "bug", "feature", "enhancement"
- When I visit project "Shop" labels page
-
- @javascript
- Scenario: I can subscribe to a label
- Then I should see that I am not subscribed to the "bug" label
- When I click button "Subscribe" for the "bug" label
- Then I should see that I am subscribed to the "bug" label
- When I click button "Unsubscribe" for the "bug" label
- Then I should see that I am not subscribed to the "bug" label
diff --git a/features/project/merge_requests.feature b/features/project/merge_requests.feature
index 5aa592e9067..bcde497553b 100644
--- a/features/project/merge_requests.feature
+++ b/features/project/merge_requests.feature
@@ -294,13 +294,6 @@ Feature: Project Merge Requests
Then I should see the Markdown write tab
@javascript
- Scenario: I search merge request
- Given I click link "All"
- When I fill in merge request search with "Fe"
- Then I should see "Feature NS-03" in merge requests
- And I should not see "Bug NS-04" in merge requests
-
- @javascript
Scenario: I can unsubscribe from merge request
Given I visit merge request page "Bug NS-04"
Then I should see that I am subscribed
diff --git a/features/project/merge_requests/revert.feature b/features/project/merge_requests/revert.feature
index d767b088883..ec6666f227f 100644
--- a/features/project/merge_requests/revert.feature
+++ b/features/project/merge_requests/revert.feature
@@ -5,6 +5,7 @@ Feature: Revert Merge Requests
And I am signed in as a developer of the project
And I am on the Merge Request detail page
And I click on Accept Merge Request
+ And I am on the Merge Request detail page
@javascript
Scenario: I revert a merge request
diff --git a/features/steps/dashboard/issues.rb b/features/steps/dashboard/issues.rb
deleted file mode 100644
index 4e15d79ae74..00000000000
--- a/features/steps/dashboard/issues.rb
+++ /dev/null
@@ -1,91 +0,0 @@
-class Spinach::Features::DashboardIssues < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedPaths
- include Select2Helper
-
- step 'I should see issues assigned to me' do
- should_see(assigned_issue)
- should_not_see(authored_issue)
- should_not_see(other_issue)
- end
-
- step 'I should see issues authored by me' do
- should_see(authored_issue)
- should_see(authored_issue_on_public_project)
- should_not_see(assigned_issue)
- should_not_see(other_issue)
- end
-
- step 'I should see all issues' do
- should_see(authored_issue)
- should_see(assigned_issue)
- should_see(other_issue)
- end
-
- step 'I have authored issues' do
- authored_issue
- authored_issue_on_public_project
- end
-
- step 'I have assigned issues' do
- assigned_issue
- end
-
- step 'I have other issues' do
- other_issue
- end
-
- step 'I click "Authored by me" link' do
- find("#assignee_id").set("")
- find(".js-author-search", match: :first).click
- find(".dropdown-menu-author li a", match: :first, text: current_user.to_reference).click
- end
-
- step 'I click "All" link' do
- find(".js-author-search").click
- expect(page).to have_selector(".dropdown-menu-author li a")
- find(".dropdown-menu-author li a", match: :first).click
- expect(page).not_to have_selector(".dropdown-menu-author li a")
-
- find(".js-assignee-search").click
- expect(page).to have_selector(".dropdown-menu-assignee li a")
- find(".dropdown-menu-assignee li a", match: :first).click
- expect(page).not_to have_selector(".dropdown-menu-assignee li a")
- end
-
- def should_see(issue)
- expect(page).to have_content(issue.title[0..10])
- end
-
- def should_not_see(issue)
- expect(page).not_to have_content(issue.title[0..10])
- end
-
- def assigned_issue
- @assigned_issue ||= create :issue, assignee: current_user, project: project
- end
-
- def authored_issue
- @authored_issue ||= create :issue, author: current_user, project: project
- end
-
- def other_issue
- @other_issue ||= create :issue, project: project
- end
-
- def authored_issue_on_public_project
- @authored_issue_on_public_project ||= create :issue, author: current_user, project: public_project
- end
-
- def project
- @project ||= begin
- project = create(:empty_project)
- project.team << [current_user, :master]
- project
- end
- end
-
- def public_project
- @public_project ||= create(:empty_project, :public)
- end
-end
diff --git a/features/steps/dashboard/todos.rb b/features/steps/dashboard/todos.rb
index 2bbc43b491f..eb906a55a83 100644
--- a/features/steps/dashboard/todos.rb
+++ b/features/steps/dashboard/todos.rb
@@ -47,7 +47,7 @@ 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(full: true)}"
+ should_see_todo(1, "John Doe assigned you merge request #{merge_request.to_reference(full: true)}", merge_request.title, state: :done_reversible)
end
step 'I mark all todos as done' do
@@ -71,7 +71,7 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps
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(full: true)}", merge_request.title, false)
+ should_see_todo(1, "John Doe assigned you merge request #{merge_request.to_reference(full: true)}", merge_request.title, state: :done_irreversible)
end
step 'I should see all todos marked as done' do
@@ -81,10 +81,10 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps
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_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)
+ should_see_todo(1, "John Doe assigned you merge request #{merge_request_reference}", merge_request.title, state: :done_irreversible)
+ should_see_todo(2, "John Doe mentioned you on issue #{issue_reference}", "#{current_user.to_reference} Wdyt?", state: :done_irreversible)
+ should_see_todo(3, "John Doe assigned you issue #{issue_reference}", issue.title, state: :done_irreversible)
+ should_see_todo(4, "Mary Jane mentioned you on issue #{issue_reference}", issue.title, state: :done_irreversible)
end
step 'I filter by "Enterprise"' do
@@ -140,15 +140,20 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps
page.should have_css('.identifier', text: 'Merge Request !1')
end
- def should_see_todo(position, title, body, pending = true)
+ def should_see_todo(position, title, body, state: :pending)
page.within(".todo:nth-child(#{position})") do
expect(page).to have_content title
expect(page).to have_content body
- if pending
+ if state == :pending
expect(page).to have_link 'Done'
- else
+ elsif state == :done_reversible
+ expect(page).to have_link 'Undo'
+ elsif state == :done_irreversible
+ expect(page).not_to have_link 'Undo'
expect(page).not_to have_link 'Done'
+ else
+ raise 'Invalid state given, valid states: :pending, :done_reversible, :done_irreversible'
end
end
end
diff --git a/features/steps/explore/projects.rb b/features/steps/explore/projects.rb
index 2b4a5ab0864..7dc33ab5683 100644
--- a/features/steps/explore/projects.rb
+++ b/features/steps/explore/projects.rb
@@ -49,7 +49,7 @@ class Spinach::Features::ExploreProjects < Spinach::FeatureSteps
step 'I should see an http link to the repository' do
project = Project.find_by(name: 'Community')
- expect(page).to have_field('project_clone', with: project.http_url_to_repo)
+ expect(page).to have_field('project_clone', with: project.http_url_to_repo(@user))
end
step 'I should see an ssh link to the repository' do
diff --git a/features/steps/group/milestones.rb b/features/steps/group/milestones.rb
index 70e23098dde..20204ad8654 100644
--- a/features/steps/group/milestones.rb
+++ b/features/steps/group/milestones.rb
@@ -5,9 +5,7 @@ class Spinach::Features::GroupMilestones < Spinach::FeatureSteps
include SharedUser
step 'I click on group milestones' do
- page.within('.layout-nav') do
- click_link 'Milestones'
- end
+ visit group_milestones_path('owned')
end
step 'I should see group milestones index page has no milestones' do
diff --git a/features/steps/project/builds/artifacts.rb b/features/steps/project/builds/artifacts.rb
index 055fca036d3..be0f6eee55a 100644
--- a/features/steps/project/builds/artifacts.rb
+++ b/features/steps/project/builds/artifacts.rb
@@ -76,7 +76,7 @@ class Spinach::Features::ProjectBuildsArtifacts < Spinach::FeatureSteps
base64_params = send_data.sub(/\Aartifacts\-entry:/, '')
params = JSON.parse(Base64.urlsafe_decode64(base64_params))
- expect(params.keys).to eq(['Archive', 'Entry'])
+ expect(params.keys).to eq(%w(Archive Entry))
expect(params['Archive']).to end_with('build_artifacts.zip')
expect(params['Entry']).to eq(Base64.encode64('ci_artifacts.txt'))
end
diff --git a/features/steps/project/commits/branches.rb b/features/steps/project/commits/branches.rb
index 5f9b9e0445e..ccaf3237815 100644
--- a/features/steps/project/commits/branches.rb
+++ b/features/steps/project/commits/branches.rb
@@ -34,25 +34,19 @@ class Spinach::Features::ProjectCommitsBranches < Spinach::FeatureSteps
step 'I submit new branch form' do
fill_in 'branch_name', with: 'deploy_keys'
- fill_in 'ref', with: 'master'
+ select_branch('master')
click_button 'Create branch'
end
step 'I submit new branch form with invalid name' do
fill_in 'branch_name', with: '1.0 stable'
- fill_in 'ref', with: 'master'
- click_button 'Create branch'
- end
-
- step 'I submit new branch form with invalid reference' do
- fill_in 'branch_name', with: 'foo'
- fill_in 'ref', with: 'foo'
+ select_branch('master')
click_button 'Create branch'
end
step 'I submit new branch form with branch that already exists' do
fill_in 'branch_name', with: 'master'
- fill_in 'ref', with: 'master'
+ select_branch('master')
click_button 'Create branch'
end
@@ -65,10 +59,6 @@ class Spinach::Features::ProjectCommitsBranches < Spinach::FeatureSteps
expect(page).to have_content "can't contain spaces"
end
- step 'I should see new an error that ref is invalid' do
- expect(page).to have_content 'Invalid reference name'
- end
-
step 'I should see new an error that branch already exists' do
expect(page).to have_content 'Branch already exists'
end
@@ -88,4 +78,12 @@ class Spinach::Features::ProjectCommitsBranches < Spinach::FeatureSteps
step "I should not see branch 'improve/awesome'" do
expect(page.all(visible: true)).not_to have_content 'improve/awesome'
end
+
+ def select_branch(branch_name)
+ click_button 'master'
+
+ page.within '#new-branch-form .dropdown-menu' do
+ click_link branch_name
+ end
+ end
end
diff --git a/features/steps/project/labels.rb b/features/steps/project/labels.rb
deleted file mode 100644
index dbeb07c78db..00000000000
--- a/features/steps/project/labels.rb
+++ /dev/null
@@ -1,32 +0,0 @@
-class Spinach::Features::Labels < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedIssuable
- include SharedProject
- include SharedPaths
-
- step 'And I visit project "Shop" labels page' do
- visit namespace_project_labels_path(project.namespace, project)
- end
-
- step 'I should see that I am subscribed to the "bug" label' do
- expect(subscribe_button).to have_content 'Unsubscribe'
- end
-
- step 'I should see that I am not subscribed to the "bug" label' do
- expect(subscribe_button).to have_content 'Subscribe'
- end
-
- step 'I click button "Unsubscribe" for the "bug" label' do
- subscribe_button.click
- end
-
- step 'I click button "Subscribe" for the "bug" label' do
- subscribe_button.click
- end
-
- private
-
- def subscribe_button
- first('.js-subscribe-button', visible: true)
- end
-end
diff --git a/features/steps/shared/builds.rb b/features/steps/shared/builds.rb
index d008a8a26af..5bc3a1f5ac4 100644
--- a/features/steps/shared/builds.rb
+++ b/features/steps/shared/builds.rb
@@ -11,7 +11,7 @@ module SharedBuilds
step 'project has a recent build' do
@pipeline = create(:ci_empty_pipeline, project: @project, sha: @project.commit.sha, ref: 'master')
- @build = create(:ci_build_with_coverage, pipeline: @pipeline)
+ @build = create(:ci_build, :coverage, pipeline: @pipeline)
end
step 'recent build is successful' do
diff --git a/features/steps/shared/issuable.rb b/features/steps/shared/issuable.rb
index 79dde620265..3d9cedf5c2d 100644
--- a/features/steps/shared/issuable.rb
+++ b/features/steps/shared/issuable.rb
@@ -153,7 +153,7 @@ module SharedIssuable
case type
when :issue
- attrs.merge!(project: project)
+ attrs[:project] = project
when :merge_request
attrs.merge!(
source_project: project,
diff --git a/lib/api/api.rb b/lib/api/api.rb
index 06346ae822a..b27ac3f1d15 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -5,13 +5,35 @@ module API
version %w(v3 v4), using: :path
version 'v3', using: :path do
+ mount ::API::V3::AwardEmoji
+ mount ::API::V3::Boards
+ mount ::API::V3::Branches
+ mount ::API::V3::BroadcastMessages
+ mount ::API::V3::Commits
mount ::API::V3::DeployKeys
+ mount ::API::V3::Environments
+ mount ::API::V3::Files
+ mount ::API::V3::Groups
mount ::API::V3::Issues
+ mount ::API::V3::Labels
mount ::API::V3::Members
+ mount ::API::V3::MergeRequestDiffs
mount ::API::V3::MergeRequests
+ mount ::API::V3::Notes
+ mount ::API::V3::ProjectHooks
mount ::API::V3::Projects
mount ::API::V3::ProjectSnippets
+ mount ::API::V3::Repositories
+ mount ::API::V3::Runners
+ mount ::API::V3::Services
+ mount ::API::V3::Subscriptions
+ mount ::API::V3::SystemHooks
+ mount ::API::V3::Tags
mount ::API::V3::Templates
+ mount ::API::V3::Todos
+ mount ::API::V3::Triggers
+ mount ::API::V3::Users
+ mount ::API::V3::Variables
end
before { allow_access_with_scope :api }
@@ -76,6 +98,7 @@ module API
mount ::API::Projects
mount ::API::ProjectSnippets
mount ::API::Repositories
+ mount ::API::Runner
mount ::API::Runners
mount ::API::Services
mount ::API::Session
diff --git a/lib/api/api_guard.rb b/lib/api/api_guard.rb
index df6db140d0e..c11f8529183 100644
--- a/lib/api/api_guard.rb
+++ b/lib/api/api_guard.rb
@@ -6,7 +6,7 @@ module API
module APIGuard
extend ActiveSupport::Concern
- PRIVATE_TOKEN_HEADER = "HTTP_PRIVATE_TOKEN"
+ PRIVATE_TOKEN_HEADER = "HTTP_PRIVATE_TOKEN".freeze
PRIVATE_TOKEN_PARAM = :private_token
included do |base|
@@ -114,8 +114,8 @@ module API
private
def install_error_responders(base)
- error_classes = [ MissingTokenError, TokenNotFoundError,
- ExpiredError, RevokedError, InsufficientScopeError]
+ error_classes = [MissingTokenError, TokenNotFoundError,
+ ExpiredError, RevokedError, InsufficientScopeError]
base.send :rescue_from, *error_classes, oauth2_bearer_token_error_handler
end
diff --git a/lib/api/award_emoji.rb b/lib/api/award_emoji.rb
index 58a4df54bea..07a1bcdbe18 100644
--- a/lib/api/award_emoji.rb
+++ b/lib/api/award_emoji.rb
@@ -3,7 +3,7 @@ module API
include PaginationParams
before { authenticate! }
- AWARDABLES = %w[issue merge_request snippet]
+ AWARDABLES = %w[issue merge_request snippet].freeze
resource :projects do
AWARDABLES.each do |awardable_type|
@@ -15,7 +15,8 @@ module API
requires :"#{awardable_id_string}", type: Integer, desc: "The ID of an Issue, Merge Request or Snippet"
end
- [ ":id/#{awardable_string}/:#{awardable_id_string}/award_emoji",
+ [
+ ":id/#{awardable_string}/:#{awardable_id_string}/award_emoji",
":id/#{awardable_string}/:#{awardable_id_string}/notes/:note_id/award_emoji"
].each do |endpoint|
@@ -28,8 +29,8 @@ module API
end
get endpoint do
if can_read_awardable?
- awards = paginate(awardable.award_emoji)
- present awards, with: Entities::AwardEmoji
+ awards = awardable.award_emoji
+ present paginate(awards), with: Entities::AwardEmoji
else
not_found!("Award Emoji")
end
@@ -82,7 +83,6 @@ module API
unauthorized! unless award.user == current_user || current_user.admin?
award.destroy
- present award, with: Entities::AwardEmoji
end
end
end
diff --git a/lib/api/boards.rb b/lib/api/boards.rb
index 13752eb4947..b6843c1b6af 100644
--- a/lib/api/boards.rb
+++ b/lib/api/boards.rb
@@ -1,6 +1,7 @@
module API
- # Boards API
class Boards < Grape::API
+ include PaginationParams
+
before { authenticate! }
params do
@@ -11,9 +12,12 @@ module API
detail 'This feature was introduced in 8.13'
success Entities::Board
end
+ params do
+ use :pagination
+ end
get ':id/boards' do
authorize!(:read_board, user_project)
- present user_project.boards, with: Entities::Board
+ present paginate(user_project.boards), with: Entities::Board
end
params do
@@ -40,9 +44,12 @@ module API
detail 'Does not include `done` list. This feature was introduced in 8.13'
success Entities::List
end
+ params do
+ use :pagination
+ end
get '/lists' do
authorize!(:read_board, user_project)
- present board_lists, with: Entities::List
+ present paginate(board_lists), with: Entities::List
end
desc 'Get a list of a project board' do
@@ -120,9 +127,7 @@ module API
service = ::Boards::Lists::DestroyService.new(user_project, current_user)
- if service.execute(list)
- present list, with: Entities::List
- else
+ unless service.execute(list)
render_api_error!({ error: 'List could not be deleted!' }, 400)
end
end
diff --git a/lib/api/branches.rb b/lib/api/branches.rb
index 9331be1f7de..73a7e939627 100644
--- a/lib/api/branches.rb
+++ b/lib/api/branches.rb
@@ -1,8 +1,9 @@
require 'mime/types'
module API
- # Projects API
class Branches < Grape::API
+ include PaginationParams
+
before { authenticate! }
before { authorize! :download_code, user_project }
@@ -13,10 +14,13 @@ module API
desc 'Get a project repository branches' do
success Entities::RepoBranch
end
+ params do
+ use :pagination
+ end
get ":id/repository/branches" do
- branches = user_project.repository.branches.sort_by(&:name)
+ branches = ::Kaminari.paginate_array(user_project.repository.branches.sort_by(&:name))
- present branches, with: Entities::RepoBranch, project: user_project
+ present paginate(branches), with: Entities::RepoBranch, project: user_project
end
desc 'Get a single branch' do
@@ -93,13 +97,13 @@ module API
success Entities::RepoBranch
end
params do
- requires :branch_name, type: String, desc: 'The name of the branch'
+ requires :branch, type: String, desc: 'The name of the branch'
requires :ref, type: String, desc: 'Create branch from commit sha or existing branch'
end
post ":id/repository/branches" do
authorize_push_project
result = CreateBranchService.new(user_project, current_user).
- execute(params[:branch_name], params[:ref])
+ execute(params[:branch], params[:ref])
if result[:status] == :success
present result[:branch],
@@ -120,11 +124,7 @@ module API
result = DeleteBranchService.new(user_project, current_user).
execute(params[:branch])
- if result[:status] == :success
- {
- branch_name: params[:branch]
- }
- else
+ if result[:status] != :success
render_api_error!(result[:message], result[:return_code])
end
end
@@ -133,7 +133,7 @@ module API
delete ":id/repository/merged_branches" do
DeleteMergedBranchesService.new(user_project, current_user).async_execute
- status(200)
+ accepted!
end
end
end
diff --git a/lib/api/broadcast_messages.rb b/lib/api/broadcast_messages.rb
index 1217002bf8e..395c401203c 100644
--- a/lib/api/broadcast_messages.rb
+++ b/lib/api/broadcast_messages.rb
@@ -91,7 +91,7 @@ module API
delete ':id' do
message = find_message
- present message.destroy, with: Entities::BroadcastMessage
+ message.destroy
end
end
end
diff --git a/lib/api/builds.rb b/lib/api/builds.rb
index 44fe0fc4a95..5b76913fe45 100644
--- a/lib/api/builds.rb
+++ b/lib/api/builds.rb
@@ -11,7 +11,7 @@ module API
helpers do
params :optional_scope do
optional :scope, types: [String, Array[String]], desc: 'The scope of builds to show',
- values: ['pending', 'running', 'failed', 'success', 'canceled'],
+ values: ::CommitStatus::AVAILABLE_STATUSES,
coerce_with: ->(scope) {
if scope.is_a?(String)
[scope]
diff --git a/lib/api/commit_statuses.rb b/lib/api/commit_statuses.rb
index 0b6076bd28c..dba0831664c 100644
--- a/lib/api/commit_statuses.rb
+++ b/lib/api/commit_statuses.rb
@@ -40,7 +40,7 @@ module API
requires :id, type: String, desc: 'The ID of a project'
requires :sha, type: String, desc: 'The commit hash'
requires :state, type: String, desc: 'The state of the status',
- values: ['pending', 'running', 'success', 'failed', 'canceled']
+ values: %w(pending running success failed canceled)
optional :ref, type: String, desc: 'The ref'
optional :target_url, type: String, desc: 'The target URL to associate with this status'
optional :description, type: String, desc: 'A short description of the status'
diff --git a/lib/api/commits.rb b/lib/api/commits.rb
index 173083d0ade..fd03e92264d 100644
--- a/lib/api/commits.rb
+++ b/lib/api/commits.rb
@@ -16,16 +16,13 @@ module API
end
params do
optional :ref_name, type: String, desc: 'The name of a repository branch or tag, if not given the default branch is used'
- optional :since, type: String, desc: 'Only commits after or in this date will be returned'
- optional :until, type: String, desc: 'Only commits before or in this date will be returned'
+ optional :since, type: DateTime, desc: 'Only commits after or on this date will be returned'
+ optional :until, type: DateTime, desc: 'Only commits before or on this date will be returned'
optional :page, type: Integer, default: 0, desc: 'The page for pagination'
optional :per_page, type: Integer, default: 20, desc: 'The number of results per page'
optional :path, type: String, desc: 'The file path'
end
get ":id/repository/commits" do
- # TODO remove the next line for 9.0, use DateTime type in the params block
- datetime_attributes! :since, :until
-
ref = params[:ref_name] || user_project.try(:default_branch) || 'master'
offset = params[:page] * params[:per_page]
@@ -44,7 +41,7 @@ module API
detail 'This feature was introduced in GitLab 8.13'
end
params do
- requires :branch_name, type: String, desc: 'The name of branch'
+ requires :branch, 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'
optional :author_email, type: String, desc: 'Author email for commit'
@@ -53,15 +50,7 @@ module API
post ":id/repository/commits" do
authorize! :push_code, user_project
- attrs = declared_params
- attrs[:start_branch] = attrs[:branch_name]
- attrs[:target_branch] = attrs[:branch_name]
- attrs[:actions].map! do |action|
- action[:action] = action[:action].to_sym
- action[:file_path].slice!(0) if action[:file_path] && action[:file_path].start_with?('/')
- action[:previous_path].slice!(0) if action[:previous_path] && action[:previous_path].start_with?('/')
- action
- end
+ attrs = declared_params.merge(start_branch: declared_params[:branch], target_branch: declared_params[:branch])
result = ::Files::MultiService.new(user_project, current_user, attrs).execute
@@ -161,7 +150,7 @@ module API
optional :path, type: String, desc: 'The file path'
given :path do
requires :line, type: Integer, desc: 'The line number'
- requires :line_type, type: String, values: ['new', 'old'], default: 'new', desc: 'The type of the line'
+ requires :line_type, type: String, values: %w(new old), default: 'new', desc: 'The type of the line'
end
end
post ':id/repository/commits/:sha/comments' do
diff --git a/lib/api/deploy_keys.rb b/lib/api/deploy_keys.rb
index 3f5183d46a2..69e85c27a65 100644
--- a/lib/api/deploy_keys.rb
+++ b/lib/api/deploy_keys.rb
@@ -1,12 +1,17 @@
module API
class DeployKeys < Grape::API
+ include PaginationParams
+
before { authenticate! }
+ desc 'Return all deploy keys'
+ params do
+ use :pagination
+ end
get "deploy_keys" do
authenticated_as_admin!
- keys = DeployKey.all
- present keys, with: Entities::SSHKey
+ present paginate(DeployKey.all), with: Entities::SSHKey
end
params do
@@ -18,8 +23,11 @@ module API
desc "Get a specific project's deploy keys" do
success Entities::SSHKey
end
+ params do
+ use :pagination
+ end
get ":id/deploy_keys" do
- present user_project.deploy_keys, with: Entities::SSHKey
+ present paginate(user_project.deploy_keys), with: Entities::SSHKey
end
desc 'Get single deploy key' do
@@ -85,20 +93,6 @@ module API
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/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
-
desc 'Delete deploy key for a project' do
success Key
end
@@ -107,11 +101,9 @@ module API
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
+ not_found!('Deploy Key') unless key
+
+ key.destroy
end
end
end
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 536d3ee49d4..b839d72a0e1 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -26,7 +26,7 @@ module API
expose :last_sign_in_at
expose :confirmed_at
expose :email
- expose :theme_id, :color_scheme_id, :projects_limit, :current_sign_in_at
+ expose :color_scheme_id, :projects_limit, :current_sign_in_at
expose :identities, using: Entities::Identity
expose :can_create_group?, as: :can_create_group
expose :can_create_project?, as: :can_create_project
@@ -339,9 +339,6 @@ module API
expose :created_at, :updated_at
expose :system?, as: :system
expose :noteable_id, :noteable_type
- # upvote? and downvote? are deprecated, always return false
- expose(:upvote?) { |note| false }
- expose(:downvote?) { |note| false }
end
class AwardEmoji < Grape::Entity
@@ -561,6 +558,7 @@ module API
expose :default_project_visibility
expose :default_snippet_visibility
expose :default_group_visibility
+ expose :default_artifacts_expire_in
expose :domain_whitelist
expose :domain_blacklist_enabled
expose :domain_blacklist
@@ -621,6 +619,10 @@ module API
end
end
+ class RunnerRegistrationDetails < Grape::Entity
+ expose :id, :token
+ end
+
class BuildArtifactFile < Grape::Entity
expose :filename, :size
end
diff --git a/lib/api/environments.rb b/lib/api/environments.rb
index 1a7e68f0528..dbdf29a9640 100644
--- a/lib/api/environments.rb
+++ b/lib/api/environments.rb
@@ -79,7 +79,7 @@ module API
environment = user_project.environments.find(params[:environment_id])
- present environment.destroy, with: Entities::Environment
+ environment.destroy
end
end
end
diff --git a/lib/api/files.rb b/lib/api/files.rb
index 2ecdd747c8e..9c4e43d77cc 100644
--- a/lib/api/files.rb
+++ b/lib/api/files.rb
@@ -1,12 +1,11 @@
module API
- # Projects API
class Files < Grape::API
helpers do
def commit_params(attrs)
{
file_path: attrs[:file_path],
- start_branch: attrs[:branch_name],
- target_branch: attrs[:branch_name],
+ start_branch: attrs[:branch],
+ target_branch: attrs[:branch],
commit_message: attrs[:commit_message],
file_content: attrs[:content],
file_content_encoding: attrs[:encoding],
@@ -18,13 +17,13 @@ module API
def commit_response(attrs)
{
file_path: attrs[:file_path],
- branch_name: attrs[:branch_name]
+ branch: attrs[:branch]
}
end
params :simple_file_params do
requires :file_path, type: String, desc: 'The path to new file. Ex. lib/class.rb'
- requires :branch_name, type: String, desc: 'The name of branch'
+ requires :branch, type: String, desc: 'The name of branch'
requires :commit_message, type: String, desc: 'Commit Message'
optional :author_email, type: String, desc: 'The email of the author'
optional :author_name, type: String, desc: 'The name of the author'
@@ -119,10 +118,7 @@ module API
file_params = declared_params(include_missing: false)
result = ::Files::DestroyService.new(user_project, current_user, commit_params(file_params)).execute
- if result[:status] == :success
- status(200)
- commit_response(file_params)
- else
+ if result[:status] != :success
render_api_error!(result[:message], 400)
end
end
diff --git a/lib/api/groups.rb b/lib/api/groups.rb
index 9f29c4466ab..9cffd6180ae 100644
--- a/lib/api/groups.rb
+++ b/lib/api/groups.rb
@@ -36,12 +36,15 @@ module API
optional :skip_groups, type: Array[Integer], desc: 'Array of group ids to exclude from list'
optional :all_available, type: Boolean, desc: 'Show all group that you have access to'
optional :search, type: String, desc: 'Search for a specific group'
+ optional :owned, type: Boolean, default: false, desc: 'Limit by owned by authenticated user'
optional :order_by, type: String, values: %w[name path], default: 'name', desc: 'Order by name or path'
optional :sort, type: String, values: %w[asc desc], default: 'asc', desc: 'Sort by asc (ascending) or desc (descending)'
use :pagination
end
get do
- groups = if current_user.admin
+ groups = if params[:owned]
+ current_user.owned_groups
+ elsif current_user.admin
Group.all
elsif params[:all_available]
GroupsFinder.new.execute(current_user)
@@ -56,17 +59,6 @@ module API
present_groups groups, statistics: params[:statistics] && current_user.is_admin?
end
- desc 'Get list of owned groups for authenticated user' do
- success Entities::Group
- end
- params do
- use :pagination
- use :statistics_params
- end
- get '/owned' do
- present_groups current_user.owned_groups, statistics: params[:statistics]
- end
-
desc 'Create a group. Available only for users who can create groups.' do
success Entities::Group
end
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index 13896dd91b9..72d2b320077 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -3,7 +3,7 @@ module API
include Gitlab::Utils
include Helpers::Pagination
- SUDO_HEADER = "HTTP_SUDO"
+ SUDO_HEADER = "HTTP_SUDO".freeze
SUDO_PARAM = :sudo
def declared_params(options = {})
@@ -153,29 +153,13 @@ module API
params_hash = custom_params || params
attrs = {}
keys.each do |key|
- if params_hash[key].present? or (params_hash.has_key?(key) and params_hash[key] == false)
+ if params_hash[key].present? || (params_hash.has_key?(key) && params_hash[key] == false)
attrs[key] = params_hash[key]
end
end
ActionController::Parameters.new(attrs).permit!
end
- # Checks the occurrences of datetime attributes, each attribute if present in the params hash must be in ISO 8601
- # format (YYYY-MM-DDTHH:MM:SSZ) or a Bad Request error is invoked.
- #
- # Parameters:
- # keys (required) - An array consisting of elements that must be parseable as dates from the params hash
- def datetime_attributes!(*keys)
- keys.each do |key|
- begin
- params[key] = Time.xmlschema(params[key]) if params[key].present?
- rescue ArgumentError
- message = "\"" + key.to_s + "\" must be a timestamp in ISO 8601 format: YYYY-MM-DDTHH:MM:SSZ"
- render_api_error!(message, 400)
- end
- end
- end
-
def filter_by_iid(items, iid)
items.where(iid: iid)
end
@@ -225,12 +209,20 @@ module API
render_api_error!('204 No Content', 204)
end
+ def accepted!
+ render_api_error!('202 Accepted', 202)
+ end
+
def render_validation_error!(model)
if model.errors.any?
render_api_error!(model.errors.messages || '400 Bad Request', 400)
end
end
+ def render_spam_error!
+ render_api_error!({ error: 'Spam detected' }, 400)
+ end
+
def render_api_error!(message, status)
error!({ 'message' => message }, status, header)
end
diff --git a/lib/api/helpers/pagination.rb b/lib/api/helpers/pagination.rb
index 2199eea7e5f..0764b58fb4c 100644
--- a/lib/api/helpers/pagination.rb
+++ b/lib/api/helpers/pagination.rb
@@ -2,7 +2,7 @@ module API
module Helpers
module Pagination
def paginate(relation)
- relation.page(params[:page]).per(params[:per_page].to_i).tap do |data|
+ relation.page(params[:page]).per(params[:per_page]).tap do |data|
add_pagination_headers(data)
end
end
diff --git a/lib/api/helpers/runner.rb b/lib/api/helpers/runner.rb
new file mode 100644
index 00000000000..119ca81b883
--- /dev/null
+++ b/lib/api/helpers/runner.rb
@@ -0,0 +1,23 @@
+module API
+ module Helpers
+ module Runner
+ def runner_registration_token_valid?
+ ActiveSupport::SecurityUtils.variable_size_secure_compare(params[:token],
+ current_application_settings.runners_registration_token)
+ end
+
+ def get_runner_version_from_params
+ return unless params['info'].present?
+ attributes_for_keys(%w(name version revision platform architecture), params['info'])
+ end
+
+ def authenticate_runner!
+ forbidden! unless current_runner
+ end
+
+ def current_runner
+ @runner ||= ::Ci::Runner.find_by_token(params[:token].to_s)
+ end
+ end
+ end
+end
diff --git a/lib/api/issues.rb b/lib/api/issues.rb
index 90fca20d4fa..6d30c5d81b1 100644
--- a/lib/api/issues.rb
+++ b/lib/api/issues.rb
@@ -10,17 +10,9 @@ module API
args.delete(:id)
args[:milestone_title] = args.delete(:milestone)
+ args[:label_name] = args.delete(:labels)
- 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
-
- # 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 = IssuesFinder.new(current_user, args).execute
issues.reorder(args[:order_by] => args[:sort])
end
@@ -77,7 +69,7 @@ module API
get ":id/issues" do
group = find_group!(params[:id])
- issues = find_issues(group_id: group.id, state: params[:state] || 'opened', match_all_labels: true)
+ issues = find_issues(group_id: group.id, state: params[:state] || 'opened')
present paginate(issues), with: Entities::Issue, current_user: current_user
end
@@ -177,9 +169,13 @@ module API
params.delete(:updated_at)
end
+ update_params = declared_params(include_missing: false).merge(request: request, api: true)
+
issue = ::Issues::UpdateService.new(user_project,
current_user,
- declared_params(include_missing: false)).execute(issue)
+ update_params).execute(issue)
+
+ render_spam_error! if issue.spam?
if issue.valid?
present issue, with: Entities::Issue, current_user: current_user, project: user_project
diff --git a/lib/api/labels.rb b/lib/api/labels.rb
index 652786d4e3e..59f0e7cb647 100644
--- a/lib/api/labels.rb
+++ b/lib/api/labels.rb
@@ -1,6 +1,7 @@
module API
- # Labels API
class Labels < Grape::API
+ include PaginationParams
+
before { authenticate! }
params do
@@ -10,8 +11,11 @@ module API
desc 'Get all labels of the project' do
success Entities::Label
end
+ params do
+ use :pagination
+ end
get ':id/labels' do
- present available_labels, with: Entities::Label, current_user: current_user, project: user_project
+ present paginate(available_labels), with: Entities::Label, current_user: current_user, project: user_project
end
desc 'Create a new label' do
@@ -52,7 +56,7 @@ module API
label = user_project.labels.find_by(title: params[:name])
not_found!('Label') unless label
- present label.destroy, with: Entities::Label, current_user: current_user, project: user_project
+ label.destroy
end
desc 'Update an existing label. At least one optional parameter is required.' do
diff --git a/lib/api/members.rb b/lib/api/members.rb
index d1d78775c6d..baf85e6075a 100644
--- a/lib/api/members.rb
+++ b/lib/api/members.rb
@@ -55,7 +55,6 @@ module API
authorize_admin_source!(source_type, source)
member = source.members.find_by(user_id: params[:user_id])
-
conflict!('Member already exists') if member
member = source.add_user(params[:user_id], params[:access_level], current_user: current_user, expires_at: params[:expires_at])
@@ -63,9 +62,6 @@ module API
if member.persisted? && member.valid?
present member.user, with: Entities::Member, member: member
else
- # This is to ensure back-compatibility but 400 behavior should be used
- # for all validation errors in 9.0!
- render_api_error!('Access level is not known', 422) if member.errors.key?(:access_level)
render_validation_error!(member)
end
end
@@ -79,18 +75,14 @@ module API
optional :expires_at, type: DateTime, desc: 'Date string in the format YEAR-MONTH-DAY'
end
put ":id/members/:user_id" do
- source = find_source(source_type, params[:id])
+ source = find_source(source_type, params.delete(:id))
authorize_admin_source!(source_type, source)
- member = source.members.find_by!(user_id: params[:user_id])
- attrs = attributes_for_keys [:access_level, :expires_at]
+ member = source.members.find_by!(user_id: params.delete(:user_id))
- if member.update_attributes(attrs)
+ if member.update_attributes(declared_params(include_missing: false))
present member.user, with: Entities::Member, member: member
else
- # This is to ensure back-compatibility but 400 behavior should be used
- # for all validation errors in 9.0!
- render_api_error!('Access level is not known', 422) if member.errors.key?(:access_level)
render_validation_error!(member)
end
end
@@ -101,24 +93,10 @@ module API
end
delete ":id/members/:user_id" do
source = find_source(source_type, params[:id])
+ # Ensure that memeber exists
+ source.members.find_by!(user_id: params[:user_id])
- # This is to ensure back-compatibility but find_by! should be used
- # in that casse in 9.0!
- member = source.members.find_by(user_id: params[:user_id])
-
- # This is to ensure back-compatibility but this should be removed in
- # favor of find_by! in 9.0!
- not_found!("Member: user_id:#{params[:user_id]}") if source_type == 'group' && member.nil?
-
- # This is to ensure back-compatibility but 204 behavior should be used
- # for all DELETE endpoints in 9.0!
- if member.nil?
- { message: "Access revoked", id: params[:user_id].to_i }
- else
- ::Members::DestroyService.new(source, current_user, declared_params).execute
-
- present member.user, with: Entities::Member, member: member
- end
+ ::Members::DestroyService.new(source, current_user, declared_params).execute
end
end
end
diff --git a/lib/api/merge_request_diffs.rb b/lib/api/merge_request_diffs.rb
index bc3d69f6904..4901a7cfea6 100644
--- a/lib/api/merge_request_diffs.rb
+++ b/lib/api/merge_request_diffs.rb
@@ -1,6 +1,8 @@
module API
# MergeRequestDiff API
class MergeRequestDiffs < Grape::API
+ include PaginationParams
+
before { authenticate! }
resource :projects do
@@ -12,12 +14,12 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
requires :merge_request_id, type: Integer, desc: 'The ID of a merge request'
+ use :pagination
end
-
get ":id/merge_requests/:merge_request_id/versions" do
merge_request = find_merge_request_with_access(params[:merge_request_id])
- present merge_request.merge_request_diffs, with: Entities::MergeRequestDiff
+ present paginate(merge_request.merge_request_diffs), with: Entities::MergeRequestDiff
end
desc 'Get a single merge request diff version' do
diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb
index 8e09a6f7354..bdd764abfeb 100644
--- a/lib/api/merge_requests.rb
+++ b/lib/api/merge_requests.rb
@@ -119,8 +119,9 @@ module API
end
get ':id/merge_requests/:merge_request_id/commits' do
merge_request = find_merge_request_with_access(params[:merge_request_id])
+ commits = ::Kaminari.paginate_array(merge_request.commits)
- present merge_request.commits, with: Entities::RepoCommit
+ present paginate(commits), with: Entities::RepoCommit
end
desc 'Show the merge request changes' do
diff --git a/lib/api/milestones.rb b/lib/api/milestones.rb
index 3c373a84ec5..0b4ed76b35c 100644
--- a/lib/api/milestones.rb
+++ b/lib/api/milestones.rb
@@ -120,6 +120,28 @@ module API
issues = IssuesFinder.new(current_user, finder_params).execute
present paginate(issues), with: Entities::Issue, current_user: current_user, project: user_project
end
+
+ desc 'Get all merge requests for a single project milestone' do
+ detail 'This feature was introduced in GitLab 9.'
+ success Entities::MergeRequest
+ end
+ params do
+ requires :milestone_id, type: Integer, desc: 'The ID of a project milestone'
+ use :pagination
+ end
+ get ':id/milestones/:milestone_id/merge_requests' do
+ authorize! :read_milestone, user_project
+
+ milestone = user_project.milestones.find(params[:milestone_id])
+
+ finder_params = {
+ project_id: user_project.id,
+ milestone_id: milestone.id
+ }
+
+ merge_requests = MergeRequestsFinder.new(current_user, finder_params).execute
+ present paginate(merge_requests), with: Entities::MergeRequest, current_user: current_user, project: user_project
+ end
end
end
end
diff --git a/lib/api/notes.rb b/lib/api/notes.rb
index 8beccaaabd1..3b3e45cbd06 100644
--- a/lib/api/notes.rb
+++ b/lib/api/notes.rb
@@ -4,7 +4,7 @@ module API
before { authenticate! }
- NOTEABLE_TYPES = [Issue, MergeRequest, Snippet]
+ NOTEABLE_TYPES = [Issue, MergeRequest, Snippet].freeze
params do
requires :id, type: String, desc: 'The ID of a project'
@@ -85,7 +85,7 @@ module API
note = ::Notes::CreateService.new(user_project, current_user, opts).execute
if note.valid?
- present note, with: Entities::const_get(note.class.name)
+ present note, with: Entities.const_get(note.class.name)
else
not_found!("Note #{note.errors.messages}")
end
@@ -132,8 +132,6 @@ module API
authorize! :admin_note, note
::Notes::DestroyService.new(user_project, current_user).execute(note)
-
- present note, with: Entities::Note
end
end
end
diff --git a/lib/api/pagination_params.rb b/lib/api/pagination_params.rb
index 8c1e4381a74..f566eb3ed2b 100644
--- a/lib/api/pagination_params.rb
+++ b/lib/api/pagination_params.rb
@@ -15,8 +15,8 @@ module API
included do
helpers do
params :pagination do
- optional :page, type: Integer, desc: 'Current page number'
- optional :per_page, type: Integer, desc: 'Number of items per page'
+ optional :page, type: Integer, default: 1, desc: 'Current page number'
+ optional :per_page, type: Integer, default: 20, desc: 'Number of items per page'
end
end
end
diff --git a/lib/api/pipelines.rb b/lib/api/pipelines.rb
index b634b1d0222..3afc1e385fe 100644
--- a/lib/api/pipelines.rb
+++ b/lib/api/pipelines.rb
@@ -14,7 +14,7 @@ module API
end
params do
use :pagination
- optional :scope, type: String, values: ['running', 'branches', 'tags'],
+ optional :scope, type: String, values: %w(running branches tags),
desc: 'Either running, branches, or tags'
end
get ':id/pipelines' do
@@ -23,7 +23,7 @@ module API
pipelines = PipelinesFinder.new(user_project).execute(scope: params[:scope])
present paginate(pipelines), with: Entities::Pipeline
end
-
+
desc 'Create a new pipeline' do
detail 'This feature was introduced in GitLab 8.14'
success Entities::Pipeline
@@ -58,7 +58,7 @@ module API
present pipeline, with: Entities::Pipeline
end
- desc 'Retry failed builds in the pipeline' do
+ desc 'Retry builds in the pipeline' do
detail 'This feature was introduced in GitLab 8.11.'
success Entities::Pipeline
end
diff --git a/lib/api/project_hooks.rb b/lib/api/project_hooks.rb
index cb679e6658a..57a5f97dc7f 100644
--- a/lib/api/project_hooks.rb
+++ b/lib/api/project_hooks.rb
@@ -32,9 +32,7 @@ module API
use :pagination
end
get ":id/hooks" do
- hooks = paginate user_project.hooks
-
- present hooks, with: Entities::ProjectHook
+ present paginate(user_project.hooks), with: Entities::ProjectHook
end
desc 'Get a project hook' do
@@ -92,12 +90,9 @@ module API
requires :hook_id, type: Integer, desc: 'The ID of the hook to delete'
end
delete ":id/hooks/:hook_id" do
- begin
- present user_project.hooks.destroy(params[:hook_id]), with: Entities::ProjectHook
- rescue
- # ProjectHook can raise Error if hook_id not found
- not_found!("Error deleting hook #{params[:hook_id]}")
- end
+ hook = user_project.hooks.find(params.delete(:hook_id))
+
+ hook.destroy
end
end
end
diff --git a/lib/api/project_snippets.rb b/lib/api/project_snippets.rb
index dcc0c82ee27..2a1cce73f3f 100644
--- a/lib/api/project_snippets.rb
+++ b/lib/api/project_snippets.rb
@@ -63,6 +63,8 @@ module API
snippet = CreateSnippetService.new(user_project, current_user, snippet_params).execute
+ render_spam_error! if snippet.spam?
+
if snippet.persisted?
present snippet, with: Entities::ProjectSnippet
else
@@ -92,12 +94,16 @@ module API
authorize! :update_project_snippet, snippet
snippet_params = declared_params(include_missing: false)
+ .merge(request: request, api: true)
+
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?
+ render_spam_error! if snippet.spam?
+
+ if snippet.valid?
present snippet, with: Entities::ProjectSnippet
else
render_validation_error!(snippet)
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index 68c2732ec80..b8a8cee0cea 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -19,7 +19,8 @@ module API
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.'
+ 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'
@@ -93,8 +94,9 @@ module API
success Entities::Project
end
params do
- requires :name, type: String, desc: 'The name of the project'
+ optional :name, type: String, desc: 'The name of the project'
optional :path, type: String, desc: 'The path of the repository'
+ at_least_one_of :name, :path
use :optional_params
use :create_params
end
@@ -266,7 +268,7 @@ module API
desc 'Unstar a project' do
success Entities::Project
end
- delete ':id/star' do
+ post ':id/unstar' do
if current_user.starred?(user_project)
current_user.toggle_star(user_project)
user_project.reload
@@ -281,6 +283,8 @@ module API
delete ":id" do
authorize! :remove_project, user_project
::Projects::DestroyService.new(user_project, current_user, {}).async_execute
+
+ accepted!
end
desc 'Mark this project as forked from another'
@@ -350,7 +354,6 @@ module API
not_found!('Group Link') unless link
link.destroy
- no_content!
end
desc 'Upload a file'
@@ -374,6 +377,19 @@ module API
present paginate(users), with: Entities::UserBasic
end
+
+ desc 'Start the housekeeping task for a project' do
+ detail 'This feature was introduced in GitLab 9.0.'
+ end
+ post ':id/housekeeping' do
+ authorize_admin_project
+
+ begin
+ ::Projects::HousekeepingService.new(user_project).execute
+ rescue ::Projects::HousekeepingService::LeaseTaken => error
+ conflict!(error.message)
+ end
+ end
end
end
end
diff --git a/lib/api/repositories.rb b/lib/api/repositories.rb
index 4ca6646a6f1..36166780149 100644
--- a/lib/api/repositories.rb
+++ b/lib/api/repositories.rb
@@ -2,6 +2,8 @@ require 'mime/types'
module API
class Repositories < Grape::API
+ include PaginationParams
+
before { authorize! :download_code, user_project }
params do
@@ -24,6 +26,7 @@ module API
optional :ref_name, type: String, desc: 'The name of a repository branch or tag, if not given the default branch is used'
optional :path, type: String, desc: 'The path of the tree'
optional :recursive, type: Boolean, default: false, desc: 'Used to get a recursive tree'
+ use :pagination
end
get ':id/repository/tree' do
ref = params[:ref_name] || user_project.try(:default_branch) || 'master'
@@ -33,8 +36,8 @@ module API
not_found!('Tree') unless commit
tree = user_project.repository.tree(commit.id, path, recursive: params[:recursive])
-
- present tree.sorted_entries, with: Entities::RepoTreeObject
+ entries = ::Kaminari.paginate_array(tree.sorted_entries)
+ present paginate(entries), with: Entities::RepoTreeObject
end
desc 'Get a raw file contents'
@@ -42,7 +45,7 @@ module API
requires :sha, type: String, desc: 'The commit, branch name, or tag name'
requires :filepath, type: String, desc: 'The path to the file to display'
end
- get [ ":id/repository/blobs/:sha", ":id/repository/commits/:sha/blob" ] do
+ get [":id/repository/blobs/:sha", ":id/repository/commits/:sha/blob"] do
repo = user_project.repository
commit = repo.commit(params[:sha])
@@ -100,10 +103,13 @@ module API
desc 'Get repository contributors' do
success Entities::Contributor
end
+ params do
+ use :pagination
+ end
get ':id/repository/contributors' do
begin
- present user_project.repository.contributors,
- with: Entities::Contributor
+ contributors = ::Kaminari.paginate_array(user_project.repository.contributors)
+ present paginate(contributors), with: Entities::Contributor
rescue
not_found!
end
diff --git a/lib/api/runner.rb b/lib/api/runner.rb
new file mode 100644
index 00000000000..47858f1866b
--- /dev/null
+++ b/lib/api/runner.rb
@@ -0,0 +1,52 @@
+module API
+ class Runner < Grape::API
+ helpers ::API::Helpers::Runner
+
+ resource :runners do
+ desc 'Registers a new Runner' do
+ success Entities::RunnerRegistrationDetails
+ http_codes [[201, 'Runner was created'], [403, 'Forbidden']]
+ end
+ params do
+ requires :token, type: String, desc: 'Registration token'
+ optional :description, type: String, desc: %q(Runner's description)
+ optional :info, type: Hash, desc: %q(Runner's metadata)
+ optional :locked, type: Boolean, desc: 'Should Runner be locked for current project'
+ optional :run_untagged, type: Boolean, desc: 'Should Runner handle untagged jobs'
+ optional :tag_list, type: Array[String], desc: %q(List of Runner's tags)
+ end
+ post '/' do
+ attributes = attributes_for_keys [:description, :locked, :run_untagged, :tag_list]
+
+ runner =
+ if runner_registration_token_valid?
+ # Create shared runner. Requires admin access
+ Ci::Runner.create(attributes.merge(is_shared: true))
+ elsif project = Project.find_by(runners_token: params[:token])
+ # Create a specific runner for project.
+ project.runners.create(attributes)
+ end
+
+ return forbidden! unless runner
+
+ if runner.id
+ runner.update(get_runner_version_from_params)
+ present runner, with: Entities::RunnerRegistrationDetails
+ else
+ not_found!
+ end
+ end
+
+ desc 'Deletes a registered Runner' do
+ http_codes [[204, 'Runner was deleted'], [403, 'Forbidden']]
+ end
+ params do
+ requires :token, type: String, desc: %q(Runner's authentication token)
+ end
+ delete '/' do
+ authenticate_runner!
+ Ci::Runner.find_by_token(params[:token]).destroy
+ end
+ end
+ end
+end
diff --git a/lib/api/runners.rb b/lib/api/runners.rb
index 4816b5ed1b7..2e41f16f8c6 100644
--- a/lib/api/runners.rb
+++ b/lib/api/runners.rb
@@ -14,7 +14,7 @@ module API
use :pagination
end
get do
- runners = filter_runners(current_user.ci_authorized_runners, params[:scope], without: ['specific', 'shared'])
+ runners = filter_runners(current_user.ci_authorized_runners, params[:scope], without: %w(specific shared))
present paginate(runners), with: Entities::Runner
end
@@ -60,8 +60,9 @@ module API
put ':id' do
runner = get_runner(params.delete(:id))
authenticate_update_runner!(runner)
+ update_service = Ci::UpdateRunnerService.new(runner)
- if runner.update(declared_params(include_missing: false))
+ if update_service.update(declared_params(include_missing: false))
present runner, with: Entities::RunnerDetails, current_user: current_user
else
render_validation_error!(runner)
@@ -77,9 +78,8 @@ module API
delete ':id' do
runner = get_runner(params[:id])
authenticate_delete_runner!(runner)
- runner.destroy!
- present runner, with: Entities::Runner
+ runner.destroy!
end
end
@@ -135,8 +135,6 @@ module API
forbidden!("Only one project associated with the runner. Please remove the runner instead") if runner.projects.count == 1
runner_project.destroy
-
- present runner, with: Entities::Runner
end
end
diff --git a/lib/api/services.rb b/lib/api/services.rb
index 1456fe4688b..79a5f27dc4d 100644
--- a/lib/api/services.rb
+++ b/lib/api/services.rb
@@ -563,7 +563,20 @@ module API
SlackService,
MattermostService,
TeamcityService,
- ].freeze
+ ]
+
+ if Rails.env.development?
+ services['mock-ci'] = [
+ {
+ required: true,
+ name: :mock_service_url,
+ type: String,
+ desc: 'URL to the mock service'
+ }
+ ]
+
+ service_classes << MockCiService
+ end
trigger_services = {
'mattermost-slash-commands' => [
@@ -641,9 +654,7 @@ module API
hash.merge!(key => nil)
end
- if service.update_attributes(attrs.merge(active: false))
- true
- else
+ unless service.update_attributes(attrs.merge(active: false))
render_api_error!('400 Bad Request', 400)
end
end
diff --git a/lib/api/settings.rb b/lib/api/settings.rb
index 747ceb4e3e0..936c7e0930b 100644
--- a/lib/api/settings.rb
+++ b/lib/api/settings.rb
@@ -56,7 +56,8 @@ module API
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_artifacts_size, type: Integer, desc: "Set the maximum file size for each job's artifacts"
+ optional :default_artifacts_expire_in, type: String, desc: "Set the default expiration time for each job's artifacts"
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'
@@ -117,7 +118,9 @@ module API
:send_user_confirmation_email, :domain_whitelist, :domain_blacklist_enabled,
:after_sign_up_text, :signin_enabled, :require_two_factor_authentication,
:home_page_url, :after_sign_out_path, :sign_in_text, :help_page_text,
- :shared_runners_enabled, :max_artifacts_size, :max_pages_size, :container_registry_token_expire_delay,
+ :shared_runners_enabled, :max_artifacts_size,
+ :default_artifacts_expire_in, :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,
diff --git a/lib/api/snippets.rb b/lib/api/snippets.rb
index eb9ece49e7f..0f86fdb3075 100644
--- a/lib/api/snippets.rb
+++ b/lib/api/snippets.rb
@@ -67,6 +67,8 @@ module API
attrs = declared_params(include_missing: false).merge(request: request, api: true)
snippet = CreateSnippetService.new(nil, current_user, attrs).execute
+ render_spam_error! if snippet.spam?
+
if snippet.persisted?
present snippet, with: Entities::PersonalSnippet
else
@@ -93,9 +95,12 @@ module API
return not_found!('Snippet') unless snippet
authorize! :update_personal_snippet, snippet
- attrs = declared_params(include_missing: false)
+ attrs = declared_params(include_missing: false).merge(request: request, api: true)
UpdateSnippetService.new(nil, current_user, snippet, attrs).execute
+
+ render_spam_error! if snippet.spam?
+
if snippet.persisted?
present snippet, with: Entities::PersonalSnippet
else
@@ -113,9 +118,10 @@ module API
delete ':id' do
snippet = snippets_for_current_user.find_by(id: params.delete(:id))
return not_found!('Snippet') unless snippet
+
authorize! :destroy_personal_snippet, snippet
+
snippet.destroy
- no_content!
end
desc 'Get a raw snippet' do
diff --git a/lib/api/subscriptions.rb b/lib/api/subscriptions.rb
index e11d7537cc9..772b5cca017 100644
--- a/lib/api/subscriptions.rb
+++ b/lib/api/subscriptions.rb
@@ -3,7 +3,6 @@ module API
before { authenticate! }
subscribable_types = {
- '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) },
@@ -21,7 +20,7 @@ module API
desc 'Subscribe to a resource' do
success entity_class
end
- post ":id/#{type}/:subscribable_id/subscription" do
+ post ":id/#{type}/:subscribable_id/subscribe" do
resource = instance_exec(params[:subscribable_id], &finder)
if resource.subscribed?(current_user, user_project)
@@ -35,7 +34,7 @@ module API
desc 'Unsubscribe from a resource' do
success entity_class
end
- delete ":id/#{type}/:subscribable_id/subscription" do
+ post ":id/#{type}/:subscribable_id/unsubscribe" do
resource = instance_exec(params[:subscribable_id], &finder)
if !resource.subscribed?(current_user, user_project)
diff --git a/lib/api/system_hooks.rb b/lib/api/system_hooks.rb
index 708ec8cfe70..ed7b23b474a 100644
--- a/lib/api/system_hooks.rb
+++ b/lib/api/system_hooks.rb
@@ -1,6 +1,7 @@
module API
- # Hooks API
class SystemHooks < Grape::API
+ include PaginationParams
+
before do
authenticate!
authenticated_as_admin!
@@ -10,10 +11,11 @@ module API
desc 'Get the list of system hooks' do
success Entities::Hook
end
+ params do
+ use :pagination
+ end
get do
- hooks = SystemHook.all
-
- present hooks, with: Entities::Hook
+ present paginate(SystemHook.all), with: Entities::Hook
end
desc 'Create a new system hook' do
@@ -64,7 +66,7 @@ module API
hook = SystemHook.find_by(id: params[:id])
not_found!('System hook') unless hook
- present hook.destroy, with: Entities::Hook
+ hook.destroy
end
end
end
diff --git a/lib/api/tags.rb b/lib/api/tags.rb
index b6fd8f569a9..d31ef9de26b 100644
--- a/lib/api/tags.rb
+++ b/lib/api/tags.rb
@@ -1,6 +1,7 @@
module API
- # Git Tags API
class Tags < Grape::API
+ include PaginationParams
+
before { authorize! :download_code, user_project }
params do
@@ -10,9 +11,12 @@ module API
desc 'Get a project repository tags' do
success Entities::RepoTag
end
+ params do
+ use :pagination
+ end
get ":id/repository/tags" do
- present user_project.repository.tags.sort_by(&:name).reverse,
- with: Entities::RepoTag, project: user_project
+ tags = ::Kaminari.paginate_array(user_project.repository.tags.sort_by(&:name).reverse)
+ present paginate(tags), with: Entities::RepoTag, project: user_project
end
desc 'Get a single repository tag' do
@@ -62,11 +66,7 @@ module API
result = ::Tags::DestroyService.new(user_project, current_user).
execute(params[:tag_name])
- if result[:status] == :success
- {
- tag_name: params[:tag_name]
- }
- else
+ if result[:status] != :success
render_api_error!(result[:message], result[:return_code])
end
end
diff --git a/lib/api/templates.rb b/lib/api/templates.rb
index 8a2d66efd89..0fc13b35d5b 100644
--- a/lib/api/templates.rb
+++ b/lib/api/templates.rb
@@ -1,5 +1,7 @@
module API
class Templates < Grape::API
+ include PaginationParams
+
GLOBAL_TEMPLATE_TYPES = {
gitignores: {
klass: Gitlab::Template::GitignoreTemplate,
@@ -51,12 +53,14 @@ module API
end
params do
optional :popular, type: Boolean, desc: 'If passed, returns only popular licenses'
+ use :pagination
end
get "templates/licenses" do
options = {
featured: declared(params).popular.present? ? true : nil
}
- present Licensee::License.all(options), with: ::API::Entities::RepoLicense
+ licences = ::Kaminari.paginate_array(Licensee::License.all(options))
+ present paginate(licences), with: Entities::RepoLicense
end
desc 'Get the text for a specific license' do
@@ -82,8 +86,12 @@ module API
detail "This feature was introduced in GitLab #{gitlab_version}."
success Entities::TemplatesList
end
+ params do
+ use :pagination
+ end
get "templates/#{template_type}" do
- present klass.all, with: Entities::TemplatesList
+ templates = ::Kaminari.paginate_array(klass.all)
+ present paginate(templates), with: Entities::TemplatesList
end
desc 'Get the text for a specific template present in local filesystem' do
diff --git a/lib/api/todos.rb b/lib/api/todos.rb
index 9bd077263a7..e59030428da 100644
--- a/lib/api/todos.rb
+++ b/lib/api/todos.rb
@@ -7,7 +7,7 @@ module API
ISSUABLE_TYPES = {
'merge_requests' => ->(id) { find_merge_request_with_access(id) },
'issues' => ->(id) { find_project_issue(id) }
- }
+ }.freeze
params do
requires :id, type: String, desc: 'The ID of a project'
@@ -58,7 +58,7 @@ module API
params do
requires :id, type: Integer, desc: 'The ID of the todo being marked as done'
end
- delete ':id' do
+ post ':id/mark_as_done' do
todo = current_user.todos.find(params[:id])
TodoService.new.mark_todos_as_done([todo], current_user)
@@ -66,9 +66,11 @@ module API
end
desc 'Mark all todos as done'
- delete do
+ post '/mark_as_done' do
todos = find_todos
TodoService.new.mark_todos_as_done(todos, current_user)
+
+ no_content!
end
end
end
diff --git a/lib/api/triggers.rb b/lib/api/triggers.rb
index 87a717ba751..b7c9c5f2b7f 100644
--- a/lib/api/triggers.rb
+++ b/lib/api/triggers.rb
@@ -21,14 +21,9 @@ module API
unauthorized! unless trigger.project == project
# validate variables
- variables = params[:variables]
- if variables
- unless variables.all? { |key, value| key.is_a?(String) && value.is_a?(String) }
- render_api_error!('variables needs to be a map of key-valued strings', 400)
- end
-
- # convert variables from Mash to Hash
- variables = variables.to_h
+ variables = params[:variables].to_h
+ unless variables.all? { |key, value| key.is_a?(String) && value.is_a?(String) }
+ render_api_error!('variables needs to be a map of key-valued strings', 400)
end
# create request and trigger builds
@@ -98,8 +93,6 @@ module API
return not_found!('Trigger') unless trigger
trigger.destroy
-
- present trigger, with: Entities::Trigger
end
end
end
diff --git a/lib/api/users.rb b/lib/api/users.rb
index 82ac3886ac3..7bb4b76f830 100644
--- a/lib/api/users.rb
+++ b/lib/api/users.rb
@@ -172,7 +172,7 @@ module API
end
end
- user_params.merge!(password_expires_at: Time.now) if user_params[:password].present?
+ user_params[:password_expires_at] = Time.now if user_params[:password].present?
if user.update_attributes(user_params.except(:extern_uid, :provider))
present user, with: Entities::UserPublic
@@ -209,6 +209,7 @@ module API
end
params do
requires :id, type: Integer, desc: 'The ID of the user'
+ use :pagination
end
get ':id/keys' do
authenticated_as_admin!
@@ -216,7 +217,7 @@ module API
user = User.find_by(id: params[:id])
not_found!('User') unless user
- present user.keys, with: Entities::SSHKey
+ present paginate(user.keys), with: Entities::SSHKey
end
desc 'Delete an existing SSH key from a specified user. Available only for admins.' do
@@ -235,7 +236,7 @@ module API
key = user.keys.find_by(id: params[:key_id])
not_found!('Key') unless key
- present key.destroy, with: Entities::SSHKey
+ key.destroy
end
desc 'Add an email address to a specified user. Available only for admins.' do
@@ -266,13 +267,14 @@ module API
end
params do
requires :id, type: Integer, desc: 'The ID of the user'
+ use :pagination
end
get ':id/emails' do
authenticated_as_admin!
user = User.find_by(id: params[:id])
not_found!('User') unless user
- present user.emails, with: Entities::Email
+ present paginate(user.emails), with: Entities::Email
end
desc 'Delete an email address of a specified user. Available only for admins.' do
@@ -312,7 +314,7 @@ module API
params do
requires :id, type: Integer, desc: 'The ID of the user'
end
- put ':id/block' do
+ post ':id/block' do
authenticated_as_admin!
user = User.find_by(id: params[:id])
not_found!('User') unless user
@@ -328,7 +330,7 @@ module API
params do
requires :id, type: Integer, desc: 'The ID of the user'
end
- put ':id/unblock' do
+ post ':id/unblock' do
authenticated_as_admin!
user = User.find_by(id: params[:id])
not_found!('User') unless user
@@ -373,8 +375,11 @@ module API
desc "Get the currently authenticated user's SSH keys" do
success Entities::SSHKey
end
+ params do
+ use :pagination
+ end
get "keys" do
- present current_user.keys, with: Entities::SSHKey
+ present paginate(current_user.keys), with: Entities::SSHKey
end
desc 'Get a single key owned by currently authenticated user' do
@@ -417,14 +422,17 @@ module API
key = current_user.keys.find_by(id: params[:key_id])
not_found!('Key') unless key
- present key.destroy, with: Entities::SSHKey
+ key.destroy
end
desc "Get the currently authenticated user's email addresses" do
success Entities::Email
end
+ params do
+ use :pagination
+ end
get "emails" do
- present current_user.emails, with: Entities::Email
+ present paginate(current_user.emails), with: Entities::Email
end
desc 'Get a single email address owned by the currently authenticated user' do
diff --git a/lib/api/v3/award_emoji.rb b/lib/api/v3/award_emoji.rb
new file mode 100644
index 00000000000..1e35283631f
--- /dev/null
+++ b/lib/api/v3/award_emoji.rb
@@ -0,0 +1,59 @@
+module API
+ module V3
+ class AwardEmoji < Grape::API
+ include PaginationParams
+
+ before { authenticate! }
+ AWARDABLES = %w[issue merge_request snippet].freeze
+
+ resource :projects do
+ AWARDABLES.each do |awardable_type|
+ awardable_string = awardable_type.pluralize
+ awardable_id_string = "#{awardable_type}_id"
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ requires :"#{awardable_id_string}", type: Integer, desc: "The ID of an Issue, Merge Request or Snippet"
+ end
+
+ [":id/#{awardable_string}/:#{awardable_id_string}/award_emoji",
+ ":id/#{awardable_string}/:#{awardable_id_string}/notes/:note_id/award_emoji"].each do |endpoint|
+ desc 'Delete a +awardables+ award emoji' do
+ detail 'This feature was introduced in 8.9'
+ success ::API::Entities::AwardEmoji
+ end
+ params do
+ requires :award_id, type: Integer, desc: 'The ID of an award emoji'
+ end
+ delete "#{endpoint}/:award_id" do
+ award = awardable.award_emoji.find(params[:award_id])
+
+ unauthorized! unless award.user == current_user || current_user.admin?
+
+ present award.destroy, with: ::API::Entities::AwardEmoji
+ end
+ end
+ end
+ end
+
+ helpers do
+ def awardable
+ @awardable ||=
+ begin
+ if params.include?(:note_id)
+ note_id = params.delete(:note_id)
+
+ awardable.notes.find(note_id)
+ elsif params.include?(:issue_id)
+ user_project.issues.find(params[:issue_id])
+ elsif params.include?(:merge_request_id)
+ user_project.merge_requests.find(params[:merge_request_id])
+ else
+ user_project.snippets.find(params[:snippet_id])
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/v3/boards.rb b/lib/api/v3/boards.rb
new file mode 100644
index 00000000000..b1c2a3c59f2
--- /dev/null
+++ b/lib/api/v3/boards.rb
@@ -0,0 +1,72 @@
+module API
+ module V3
+ class Boards < Grape::API
+ before { authenticate! }
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+ resource :projects do
+ desc 'Get all project boards' do
+ detail 'This feature was introduced in 8.13'
+ success ::API::Entities::Board
+ end
+ get ':id/boards' do
+ authorize!(:read_board, user_project)
+ present user_project.boards, with: ::API::Entities::Board
+ end
+
+ params do
+ requires :board_id, type: Integer, desc: 'The ID of a board'
+ end
+ segment ':id/boards/:board_id' do
+ helpers do
+ def project_board
+ board = user_project.boards.first
+
+ if params[:board_id] == board.id
+ board
+ else
+ not_found!('Board')
+ end
+ end
+
+ def board_lists
+ project_board.lists.destroyable
+ end
+ end
+
+ desc 'Get the lists of a project board' do
+ detail 'Does not include `done` list. This feature was introduced in 8.13'
+ success ::API::Entities::List
+ end
+ get '/lists' do
+ authorize!(:read_board, user_project)
+ present board_lists, with: ::API::Entities::List
+ end
+
+ desc 'Delete a board list' do
+ detail 'This feature was introduced in 8.13'
+ success ::API::Entities::List
+ end
+ params do
+ requires :list_id, type: Integer, desc: 'The ID of a board list'
+ end
+ delete "/lists/:list_id" do
+ authorize!(:admin_list, user_project)
+
+ list = board_lists.find(params[:list_id])
+
+ service = ::Boards::Lists::DestroyService.new(user_project, current_user)
+
+ if service.execute(list)
+ present list, with: ::API::Entities::List
+ else
+ render_api_error!({ error: 'List could not be deleted!' }, 400)
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/v3/branches.rb b/lib/api/v3/branches.rb
new file mode 100644
index 00000000000..699e41b5537
--- /dev/null
+++ b/lib/api/v3/branches.rb
@@ -0,0 +1,51 @@
+require 'mime/types'
+
+module API
+ module V3
+ class Branches < Grape::API
+ before { authenticate! }
+ before { authorize! :download_code, user_project }
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+ resource :projects do
+ desc 'Get a project repository branches' do
+ success ::API::Entities::RepoBranch
+ end
+ get ":id/repository/branches" do
+ branches = user_project.repository.branches.sort_by(&:name)
+
+ present branches, with: ::API::Entities::RepoBranch, project: user_project
+ end
+
+ desc 'Delete a branch'
+ params do
+ requires :branch, type: String, desc: 'The name of the branch'
+ end
+ delete ":id/repository/branches/:branch", requirements: { branch: /.+/ } do
+ authorize_push_project
+
+ result = DeleteBranchService.new(user_project, current_user).
+ execute(params[:branch])
+
+ if result[:status] == :success
+ status(200)
+ {
+ branch_name: params[:branch]
+ }
+ else
+ render_api_error!(result[:message], result[:return_code])
+ end
+ end
+
+ desc 'Delete all merged branches'
+ delete ":id/repository/merged_branches" do
+ DeleteMergedBranchesService.new(user_project, current_user).async_execute
+
+ status(200)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/v3/broadcast_messages.rb b/lib/api/v3/broadcast_messages.rb
new file mode 100644
index 00000000000..417e4ad0b26
--- /dev/null
+++ b/lib/api/v3/broadcast_messages.rb
@@ -0,0 +1,31 @@
+module API
+ module V3
+ class BroadcastMessages < Grape::API
+ include PaginationParams
+
+ before { authenticate! }
+ before { authenticated_as_admin! }
+
+ resource :broadcast_messages do
+ helpers do
+ def find_message
+ BroadcastMessage.find(params[:id])
+ end
+ end
+
+ desc 'Delete a broadcast message' do
+ detail 'This feature was introduced in GitLab 8.12.'
+ success ::API::Entities::BroadcastMessage
+ end
+ params do
+ requires :id, type: Integer, desc: 'Broadcast message ID'
+ end
+ delete ':id' do
+ message = find_message
+
+ present message.destroy, with: ::API::Entities::BroadcastMessage
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/v3/commits.rb b/lib/api/v3/commits.rb
new file mode 100644
index 00000000000..506204b3517
--- /dev/null
+++ b/lib/api/v3/commits.rb
@@ -0,0 +1,198 @@
+require 'mime/types'
+
+module API
+ module V3
+ class Commits < Grape::API
+ include PaginationParams
+
+ before { authenticate! }
+ before { authorize! :download_code, user_project }
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+ resource :projects do
+ desc 'Get a project repository commits' do
+ success ::API::Entities::RepoCommit
+ end
+ params do
+ optional :ref_name, type: String, desc: 'The name of a repository branch or tag, if not given the default branch is used'
+ optional :since, type: DateTime, desc: 'Only commits after or in this date will be returned'
+ optional :until, type: DateTime, desc: 'Only commits before or in this date will be returned'
+ optional :page, type: Integer, default: 0, desc: 'The page for pagination'
+ optional :per_page, type: Integer, default: 20, desc: 'The number of results per page'
+ optional :path, type: String, desc: 'The file path'
+ end
+ get ":id/repository/commits" do
+ ref = params[:ref_name] || user_project.try(:default_branch) || 'master'
+ offset = params[:page] * params[:per_page]
+
+ commits = user_project.repository.commits(ref,
+ path: params[:path],
+ limit: params[:per_page],
+ offset: offset,
+ after: params[:since],
+ before: params[:until])
+
+ present commits, with: ::API::Entities::RepoCommit
+ end
+
+ desc 'Commit multiple file changes as one commit' do
+ success ::API::Entities::RepoCommitDetail
+ detail 'This feature was introduced in GitLab 8.13'
+ end
+ params do
+ 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'
+ optional :author_email, type: String, desc: 'Author email for commit'
+ optional :author_name, type: String, desc: 'Author name for commit'
+ end
+ post ":id/repository/commits" do
+ authorize! :push_code, user_project
+
+ attrs = declared_params.dup
+ branch = attrs.delete(:branch_name)
+ attrs.merge!(branch: branch, start_branch: branch, target_branch: branch)
+
+ result = ::Files::MultiService.new(user_project, current_user, attrs).execute
+
+ if result[:status] == :success
+ commit_detail = user_project.repository.commits(result[:result], limit: 1).first
+ present commit_detail, with: ::API::Entities::RepoCommitDetail
+ else
+ render_api_error!(result[:message], 400)
+ end
+ end
+
+ desc 'Get a specific commit of a project' do
+ success ::API::Entities::RepoCommitDetail
+ failure [[404, 'Not Found']]
+ end
+ params do
+ requires :sha, type: String, desc: 'A commit sha, or the name of a branch or tag'
+ end
+ get ":id/repository/commits/:sha" do
+ commit = user_project.commit(params[:sha])
+
+ not_found! "Commit" unless commit
+
+ present commit, with: ::API::Entities::RepoCommitDetail
+ end
+
+ desc 'Get the diff for a specific commit of a project' do
+ failure [[404, 'Not Found']]
+ end
+ params do
+ requires :sha, type: String, desc: 'A commit sha, or the name of a branch or tag'
+ end
+ get ":id/repository/commits/:sha/diff" do
+ commit = user_project.commit(params[:sha])
+
+ not_found! "Commit" unless commit
+
+ commit.raw_diffs.to_a
+ end
+
+ desc "Get a commit's comments" do
+ success ::API::Entities::CommitNote
+ failure [[404, 'Not Found']]
+ end
+ params do
+ use :pagination
+ requires :sha, type: String, desc: 'A commit sha, or the name of a branch or tag'
+ end
+ get ':id/repository/commits/:sha/comments' do
+ commit = user_project.commit(params[:sha])
+
+ not_found! 'Commit' unless commit
+ notes = Note.where(commit_id: commit.id).order(:created_at)
+
+ present paginate(notes), with: ::API::Entities::CommitNote
+ end
+
+ desc 'Cherry pick commit into a branch' do
+ detail 'This feature was introduced in GitLab 8.15'
+ success ::API::Entities::RepoCommit
+ end
+ params do
+ requires :sha, type: String, desc: 'A commit sha to be cherry picked'
+ requires :branch, type: String, desc: 'The name of the branch'
+ end
+ post ':id/repository/commits/:sha/cherry_pick' do
+ authorize! :push_code, user_project
+
+ commit = user_project.commit(params[:sha])
+ not_found!('Commit') unless commit
+
+ branch = user_project.repository.find_branch(params[:branch])
+ not_found!('Branch') unless branch
+
+ commit_params = {
+ commit: commit,
+ create_merge_request: false,
+ source_project: user_project,
+ source_branch: commit.cherry_pick_branch_name,
+ target_branch: params[:branch]
+ }
+
+ result = ::Commits::CherryPickService.new(user_project, current_user, commit_params).execute
+
+ if result[:status] == :success
+ branch = user_project.repository.find_branch(params[:branch])
+ present user_project.repository.commit(branch.dereferenced_target), with: ::API::Entities::RepoCommit
+ else
+ render_api_error!(result[:message], 400)
+ end
+ end
+
+ desc 'Post comment to commit' do
+ success ::API::Entities::CommitNote
+ end
+ params do
+ requires :sha, type: String, regexp: /\A\h{6,40}\z/, desc: "The commit's SHA"
+ requires :note, type: String, desc: 'The text of the comment'
+ optional :path, type: String, desc: 'The file path'
+ given :path do
+ requires :line, type: Integer, desc: 'The line number'
+ requires :line_type, type: String, values: %w(new old), default: 'new', desc: 'The type of the line'
+ end
+ end
+ post ':id/repository/commits/:sha/comments' do
+ commit = user_project.commit(params[:sha])
+ not_found! 'Commit' unless commit
+
+ opts = {
+ note: params[:note],
+ noteable_type: 'Commit',
+ commit_id: commit.id
+ }
+
+ if params[:path]
+ commit.raw_diffs(all_diffs: true).each do |diff|
+ next unless diff.new_path == params[:path]
+ lines = Gitlab::Diff::Parser.new.parse(diff.diff.each_line)
+
+ lines.each do |line|
+ next unless line.new_pos == params[:line] && line.type == params[:line_type]
+ break opts[:line_code] = Gitlab::Diff::LineCode.generate(diff.new_path, line.new_pos, line.old_pos)
+ end
+
+ break if opts[:line_code]
+ end
+
+ opts[:type] = LegacyDiffNote.name if opts[:line_code]
+ end
+
+ note = ::Notes::CreateService.new(user_project, current_user, opts).execute
+
+ if note.save
+ present note, with: ::API::Entities::CommitNote
+ else
+ render_api_error!("Failed to save note #{note.errors.messages}", 400)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/v3/entities.rb b/lib/api/v3/entities.rb
index 3cc0dc968a8..11d0e6dbf71 100644
--- a/lib/api/v3/entities.rb
+++ b/lib/api/v3/entities.rb
@@ -11,6 +11,40 @@ module API
Gitlab::UrlBuilder.build(snippet)
end
end
+
+ class Note < Grape::Entity
+ expose :id
+ expose :note, as: :body
+ expose :attachment_identifier, as: :attachment
+ expose :author, using: ::API::Entities::UserBasic
+ expose :created_at, :updated_at
+ expose :system?, as: :system
+ expose :noteable_id, :noteable_type
+ # upvote? and downvote? are deprecated, always return false
+ expose(:upvote?) { |note| false }
+ expose(:downvote?) { |note| false }
+ end
+
+ class Event < Grape::Entity
+ expose :title, :project_id, :action_name
+ expose :target_id, :target_type, :author_id
+ expose :data, :target_title
+ expose :created_at
+ expose :note, using: Entities::Note, if: ->(event, options) { event.note? }
+ expose :author, using: ::API::Entities::UserBasic, if: ->(event, options) { event.author }
+
+ expose :author_username do |event, options|
+ event.author&.username
+ end
+ end
+
+ class AwardEmoji < Grape::Entity
+ expose :id
+ expose :name
+ expose :user, using: ::API::Entities::UserBasic
+ expose :created_at, :updated_at
+ expose :awardable_id, :awardable_type
+ end
end
end
end
diff --git a/lib/api/v3/environments.rb b/lib/api/v3/environments.rb
new file mode 100644
index 00000000000..3effccfa708
--- /dev/null
+++ b/lib/api/v3/environments.rb
@@ -0,0 +1,29 @@
+module API
+ module V3
+ class Environments < Grape::API
+ include PaginationParams
+
+ before { authenticate! }
+
+ params do
+ requires :id, type: String, desc: 'The project ID'
+ end
+ resource :projects do
+ desc 'Deletes an existing environment' do
+ detail 'This feature was introduced in GitLab 8.11.'
+ success ::API::Entities::Environment
+ end
+ params do
+ requires :environment_id, type: Integer, desc: 'The environment ID'
+ end
+ delete ':id/environments/:environment_id' do
+ authorize! :update_environment, user_project
+
+ environment = user_project.environments.find(params[:environment_id])
+
+ present environment.destroy, with: ::API::Entities::Environment
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/v3/files.rb b/lib/api/v3/files.rb
new file mode 100644
index 00000000000..4f8d58d37c8
--- /dev/null
+++ b/lib/api/v3/files.rb
@@ -0,0 +1,138 @@
+module API
+ module V3
+ class Files < Grape::API
+ helpers do
+ def commit_params(attrs)
+ {
+ file_path: attrs[:file_path],
+ start_branch: attrs[:branch],
+ target_branch: attrs[:branch],
+ commit_message: attrs[:commit_message],
+ file_content: attrs[:content],
+ file_content_encoding: attrs[:encoding],
+ author_email: attrs[:author_email],
+ author_name: attrs[:author_name]
+ }
+ end
+
+ def commit_response(attrs)
+ {
+ file_path: attrs[:file_path],
+ branch: attrs[:branch]
+ }
+ end
+
+ params :simple_file_params do
+ requires :file_path, type: String, desc: 'The path to new file. Ex. lib/class.rb'
+ requires :branch_name, type: String, desc: 'The name of branch'
+ requires :commit_message, type: String, desc: 'Commit Message'
+ optional :author_email, type: String, desc: 'The email of the author'
+ optional :author_name, type: String, desc: 'The name of the author'
+ end
+
+ params :extended_file_params do
+ use :simple_file_params
+ requires :content, type: String, desc: 'File content'
+ optional :encoding, type: String, values: %w[base64], desc: 'File encoding'
+ end
+ end
+
+ params do
+ requires :id, type: String, desc: 'The project ID'
+ end
+ resource :projects do
+ desc 'Get a file from repository'
+ params do
+ requires :file_path, type: String, desc: 'The path to the file. Ex. lib/class.rb'
+ requires :ref, type: String, desc: 'The name of branch, tag, or commit'
+ end
+ get ":id/repository/files" do
+ authorize! :download_code, user_project
+
+ commit = user_project.commit(params[:ref])
+ not_found!('Commit') unless commit
+
+ repo = user_project.repository
+ blob = repo.blob_at(commit.sha, params[:file_path])
+ not_found!('File') unless blob
+
+ blob.load_all_data!(repo)
+ status(200)
+
+ {
+ file_name: blob.name,
+ file_path: blob.path,
+ size: blob.size,
+ encoding: "base64",
+ content: Base64.strict_encode64(blob.data),
+ ref: params[:ref],
+ blob_id: blob.id,
+ commit_id: commit.id,
+ last_commit_id: repo.last_commit_id_for_path(commit.sha, params[:file_path])
+ }
+ end
+
+ desc 'Create new file in repository'
+ params do
+ use :extended_file_params
+ end
+ post ":id/repository/files" do
+ authorize! :push_code, user_project
+
+ file_params = declared_params(include_missing: false)
+ file_params[:branch] = file_params.delete(:branch_name)
+
+ result = ::Files::CreateService.new(user_project, current_user, commit_params(file_params)).execute
+
+ if result[:status] == :success
+ status(201)
+ commit_response(file_params)
+ else
+ render_api_error!(result[:message], 400)
+ end
+ end
+
+ desc 'Update existing file in repository'
+ params do
+ use :extended_file_params
+ end
+ put ":id/repository/files" do
+ authorize! :push_code, user_project
+
+ file_params = declared_params(include_missing: false)
+ file_params[:branch] = file_params.delete(:branch_name)
+
+ result = ::Files::UpdateService.new(user_project, current_user, commit_params(file_params)).execute
+
+ if result[:status] == :success
+ status(200)
+ commit_response(file_params)
+ else
+ http_status = result[:http_status] || 400
+ render_api_error!(result[:message], http_status)
+ end
+ end
+
+ desc 'Delete an existing file in repository'
+ params do
+ use :simple_file_params
+ end
+ delete ":id/repository/files" do
+ authorize! :push_code, user_project
+
+ file_params = declared_params(include_missing: false)
+ file_params[:branch] = file_params.delete(:branch_name)
+
+ result = ::Files::DestroyService.new(user_project, current_user, commit_params(file_params)).execute
+
+ if result[:status] == :success
+ status(200)
+ commit_response(file_params)
+ else
+ render_api_error!(result[:message], 400)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/v3/groups.rb b/lib/api/v3/groups.rb
new file mode 100644
index 00000000000..c826bc4fe0b
--- /dev/null
+++ b/lib/api/v3/groups.rb
@@ -0,0 +1,38 @@
+module API
+ module V3
+ class Groups < Grape::API
+ include PaginationParams
+
+ before { authenticate! }
+
+ helpers do
+ 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: ::API::Entities::Group,
+ current_user: current_user,
+ )
+
+ groups = groups.with_statistics if options[:statistics]
+ present paginate(groups), options
+ end
+ end
+
+ resource :groups do
+ desc 'Get list of owned groups for authenticated user' do
+ success ::API::Entities::Group
+ end
+ params do
+ use :pagination
+ use :statistics_params
+ end
+ get '/owned' do
+ present_groups current_user.owned_groups, statistics: params[:statistics]
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/v3/issues.rb b/lib/api/v3/issues.rb
index 081d45165e8..5d7dfabfcd6 100644
--- a/lib/api/v3/issues.rb
+++ b/lib/api/v3/issues.rb
@@ -16,7 +16,8 @@ module API
labels = args.delete(:labels)
args[:label_name] = labels if match_all_labels
- args[:search] = "#{Issue.reference_prefix}#{args.delete(:iid)}" if args.key?(:iid)
+ # IssuesFinder expects iids
+ args[:iids] = args.delete(:iid) if args.key?(:iid)
issues = IssuesFinder.new(current_user, args).execute.inc_notes_with_associations
@@ -148,9 +149,7 @@ module API
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
+ render_spam_error! if issue.spam?
if issue.valid?
present issue, with: ::API::Entities::Issue, current_user: current_user, project: user_project
@@ -181,9 +180,13 @@ module API
params.delete(:updated_at)
end
+ update_params = declared_params(include_missing: false).merge(request: request, api: true)
+
issue = ::Issues::UpdateService.new(user_project,
current_user,
- declared_params(include_missing: false)).execute(issue)
+ update_params).execute(issue)
+
+ render_spam_error! if issue.spam?
if issue.valid?
present issue, with: ::API::Entities::Issue, current_user: current_user, project: user_project
@@ -223,6 +226,8 @@ module API
not_found!('Issue') unless issue
authorize!(:destroy_issue, issue)
+
+ status(200)
issue.destroy
end
end
diff --git a/lib/api/v3/labels.rb b/lib/api/v3/labels.rb
new file mode 100644
index 00000000000..41f45d244e3
--- /dev/null
+++ b/lib/api/v3/labels.rb
@@ -0,0 +1,34 @@
+module API
+ module V3
+ class Labels < Grape::API
+ before { authenticate! }
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+ resource :projects do
+ desc 'Get all labels of the project' do
+ success ::API::Entities::Label
+ end
+ get ':id/labels' do
+ present available_labels, with: ::API::Entities::Label, current_user: current_user, project: user_project
+ end
+
+ desc 'Delete an existing label' do
+ success ::API::Entities::Label
+ end
+ params do
+ requires :name, type: String, desc: 'The name of the label to be deleted'
+ end
+ delete ':id/labels' do
+ authorize! :admin_label, user_project
+
+ label = user_project.labels.find_by(title: params[:name])
+ not_found!('Label') unless label
+
+ present label.destroy, with: ::API::Entities::Label, current_user: current_user, project: user_project
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/v3/members.rb b/lib/api/v3/members.rb
index 4e6cb2e3c52..3d4972afd9d 100644
--- a/lib/api/v3/members.rb
+++ b/lib/api/v3/members.rb
@@ -86,13 +86,12 @@ module API
optional :expires_at, type: DateTime, desc: 'Date string in the format YEAR-MONTH-DAY'
end
put ":id/members/:user_id" do
- source = find_source(source_type, params[:id])
+ source = find_source(source_type, params.delete(:id))
authorize_admin_source!(source_type, source)
- member = source.members.find_by!(user_id: params[:user_id])
- attrs = attributes_for_keys [:access_level, :expires_at]
+ member = source.members.find_by!(user_id: params.delete(:user_id))
- if member.update_attributes(attrs)
+ if member.update_attributes(declared_params(include_missing: false))
present member.user, with: ::API::Entities::Member, member: member
else
# This is to ensure back-compatibility but 400 behavior should be used
@@ -120,6 +119,7 @@ module API
# This is to ensure back-compatibility but 204 behavior should be used
# for all DELETE endpoints in 9.0!
if member.nil?
+ status(200 )
{ message: "Access revoked", id: params[:user_id].to_i }
else
::Members::DestroyService.new(source, current_user, declared_params).execute
diff --git a/lib/api/v3/merge_requests.rb b/lib/api/v3/merge_requests.rb
index 129f9d850e9..c6574a9104b 100644
--- a/lib/api/v3/merge_requests.rb
+++ b/lib/api/v3/merge_requests.rb
@@ -103,6 +103,8 @@ module API
merge_request = find_project_merge_request(params[:merge_request_id])
authorize!(:destroy_merge_request, merge_request)
+
+ status(200)
merge_request.destroy
end
diff --git a/lib/api/v3/notes.rb b/lib/api/v3/notes.rb
new file mode 100644
index 00000000000..0796bb62e68
--- /dev/null
+++ b/lib/api/v3/notes.rb
@@ -0,0 +1,148 @@
+module API
+ module V3
+ class Notes < Grape::API
+ include PaginationParams
+
+ before { authenticate! }
+
+ NOTEABLE_TYPES = [Issue, MergeRequest, Snippet].freeze
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+ resource :projects do
+ NOTEABLE_TYPES.each do |noteable_type|
+ noteables_str = noteable_type.to_s.underscore.pluralize
+
+ desc 'Get a list of project +noteable+ notes' do
+ success ::API::V3::Entities::Note
+ end
+ params do
+ requires :noteable_id, type: Integer, desc: 'The ID of the noteable'
+ use :pagination
+ end
+ get ":id/#{noteables_str}/:noteable_id/notes" do
+ noteable = user_project.send(noteables_str.to_sym).find(params[:noteable_id])
+
+ if can?(current_user, noteable_read_ability_name(noteable), noteable)
+ # We exclude notes that are cross-references and that cannot be viewed
+ # by the current user. By doing this exclusion at this level and not
+ # at the DB query level (which we cannot in that case), the current
+ # page can have less elements than :per_page even if
+ # there's more than one page.
+ notes =
+ # paginate() only works with a relation. This could lead to a
+ # mismatch between the pagination headers info and the actual notes
+ # array returned, but this is really a edge-case.
+ paginate(noteable.notes).
+ reject { |n| n.cross_reference_not_visible_for?(current_user) }
+ present notes, with: ::API::V3::Entities::Note
+ else
+ not_found!("Notes")
+ end
+ end
+
+ desc 'Get a single +noteable+ note' do
+ success ::API::V3::Entities::Note
+ end
+ params do
+ requires :note_id, type: Integer, desc: 'The ID of a note'
+ requires :noteable_id, type: Integer, desc: 'The ID of the noteable'
+ end
+ get ":id/#{noteables_str}/:noteable_id/notes/:note_id" do
+ noteable = user_project.send(noteables_str.to_sym).find(params[:noteable_id])
+ note = noteable.notes.find(params[:note_id])
+ can_read_note = can?(current_user, noteable_read_ability_name(noteable), noteable) && !note.cross_reference_not_visible_for?(current_user)
+
+ if can_read_note
+ present note, with: ::API::V3::Entities::Note
+ else
+ not_found!("Note")
+ end
+ end
+
+ desc 'Create a new +noteable+ note' do
+ success ::API::V3::Entities::Note
+ end
+ params do
+ requires :noteable_id, type: Integer, desc: 'The ID of the noteable'
+ requires :body, type: String, desc: 'The content of a note'
+ optional :created_at, type: String, desc: 'The creation date of the note'
+ end
+ post ":id/#{noteables_str}/:noteable_id/notes" do
+ opts = {
+ note: params[:body],
+ noteable_type: noteables_str.classify,
+ noteable_id: params[:noteable_id]
+ }
+
+ noteable = user_project.send(noteables_str.to_sym).find(params[:noteable_id])
+
+ 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
+
+ note = ::Notes::CreateService.new(user_project, current_user, opts).execute
+ if note.valid?
+ present note, with: ::API::V3::Entities.const_get(note.class.name)
+ else
+ not_found!("Note #{note.errors.messages}")
+ end
+ else
+ not_found!("Note")
+ end
+ end
+
+ desc 'Update an existing +noteable+ note' do
+ success ::API::V3::Entities::Note
+ end
+ params do
+ requires :noteable_id, type: Integer, desc: 'The ID of the noteable'
+ requires :note_id, type: Integer, desc: 'The ID of a note'
+ requires :body, type: String, desc: 'The content of a note'
+ end
+ put ":id/#{noteables_str}/:noteable_id/notes/:note_id" do
+ note = user_project.notes.find(params[:note_id])
+
+ authorize! :admin_note, note
+
+ opts = {
+ note: params[:body]
+ }
+
+ note = ::Notes::UpdateService.new(user_project, current_user, opts).execute(note)
+
+ if note.valid?
+ present note, with: ::API::V3::Entities::Note
+ else
+ render_api_error!("Failed to save note #{note.errors.messages}", 400)
+ end
+ end
+
+ desc 'Delete a +noteable+ note' do
+ success ::API::V3::Entities::Note
+ end
+ params do
+ requires :noteable_id, type: Integer, desc: 'The ID of the noteable'
+ requires :note_id, type: Integer, desc: 'The ID of a note'
+ end
+ delete ":id/#{noteables_str}/:noteable_id/notes/:note_id" do
+ note = user_project.notes.find(params[:note_id])
+ authorize! :admin_note, note
+
+ ::Notes::DestroyService.new(user_project, current_user).execute(note)
+
+ present note, with: ::API::V3::Entities::Note
+ end
+ end
+ end
+
+ helpers do
+ def noteable_read_ability_name(noteable)
+ "read_#{noteable.class.to_s.underscore}".to_sym
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/v3/project_snippets.rb b/lib/api/v3/project_snippets.rb
index 9f95d4395fa..809ca4f37ba 100644
--- a/lib/api/v3/project_snippets.rb
+++ b/lib/api/v3/project_snippets.rb
@@ -64,6 +64,8 @@ module API
snippet = CreateSnippetService.new(user_project, current_user, snippet_params).execute
+ render_spam_error! if snippet.spam?
+
if snippet.persisted?
present snippet, with: ::API::V3::Entities::ProjectSnippet
else
@@ -93,12 +95,16 @@ module API
authorize! :update_project_snippet, snippet
snippet_params = declared_params(include_missing: false)
+ .merge(request: request, api: true)
+
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?
+ render_spam_error! if snippet.spam?
+
+ if snippet.valid?
present snippet, with: ::API::V3::Entities::ProjectSnippet
else
render_validation_error!(snippet)
@@ -115,6 +121,8 @@ module API
authorize! :admin_project_snippet, snippet
snippet.destroy
+
+ status(200)
end
desc 'Get a raw project snippet'
diff --git a/lib/api/v3/projects.rb b/lib/api/v3/projects.rb
index 6796da83f07..881d52e4aa4 100644
--- a/lib/api/v3/projects.rb
+++ b/lib/api/v3/projects.rb
@@ -20,7 +20,8 @@ module API
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.'
+ 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'
@@ -171,8 +172,9 @@ module API
success ::API::Entities::Project
end
params do
- requires :name, type: String, desc: 'The name of the project'
+ optional :name, type: String, desc: 'The name of the project'
optional :path, type: String, desc: 'The path of the repository'
+ at_least_one_of :name, :path
use :optional_params
use :create_params
end
@@ -232,13 +234,13 @@ module API
end
desc 'Get events for a single project' do
- success ::API::Entities::Event
+ success ::API::V3::Entities::Event
end
params do
use :pagination
end
get ":id/events" do
- present paginate(user_project.events.recent), with: ::API::Entities::Event
+ present paginate(user_project.events.recent), with: ::API::V3::Entities::Event
end
desc 'Fork new project for the current user or provided namespace.' do
@@ -358,6 +360,8 @@ module API
desc 'Remove a project'
delete ":id" do
authorize! :remove_project, user_project
+
+ status(200)
::Projects::DestroyService.new(user_project, current_user, {}).async_execute
end
@@ -383,6 +387,7 @@ module API
authorize! :remove_fork_project, user_project
if user_project.forked?
+ status(200)
user_project.forked_project_link.destroy
else
not_modified!
diff --git a/lib/api/v3/repositories.rb b/lib/api/v3/repositories.rb
new file mode 100644
index 00000000000..3549ea225ef
--- /dev/null
+++ b/lib/api/v3/repositories.rb
@@ -0,0 +1,55 @@
+require 'mime/types'
+
+module API
+ module V3
+ class Repositories < Grape::API
+ before { authorize! :download_code, user_project }
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+ resource :projects do
+ helpers do
+ def handle_project_member_errors(errors)
+ if errors[:project_access].any?
+ error!(errors[:project_access], 422)
+ end
+ not_found!
+ end
+ end
+
+ desc 'Get a project repository tree' do
+ success ::API::Entities::RepoTreeObject
+ end
+ params do
+ optional :ref_name, type: String, desc: 'The name of a repository branch or tag, if not given the default branch is used'
+ optional :path, type: String, desc: 'The path of the tree'
+ optional :recursive, type: Boolean, default: false, desc: 'Used to get a recursive tree'
+ end
+ get ':id/repository/tree' do
+ ref = params[:ref_name] || user_project.try(:default_branch) || 'master'
+ path = params[:path] || nil
+
+ commit = user_project.commit(ref)
+ not_found!('Tree') unless commit
+
+ tree = user_project.repository.tree(commit.id, path, recursive: params[:recursive])
+
+ present tree.sorted_entries, with: ::API::Entities::RepoTreeObject
+ end
+
+ desc 'Get repository contributors' do
+ success ::API::Entities::Contributor
+ end
+ get ':id/repository/contributors' do
+ begin
+ present user_project.repository.contributors,
+ with: ::API::Entities::Contributor
+ rescue
+ not_found!
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/v3/runners.rb b/lib/api/v3/runners.rb
new file mode 100644
index 00000000000..8967141fe3d
--- /dev/null
+++ b/lib/api/v3/runners.rb
@@ -0,0 +1,65 @@
+module API
+ module V3
+ class Runners < Grape::API
+ include PaginationParams
+
+ before { authenticate! }
+
+ resource :runners do
+ desc 'Remove a runner' do
+ success ::API::Entities::Runner
+ end
+ params do
+ requires :id, type: Integer, desc: 'The ID of the runner'
+ end
+ delete ':id' do
+ runner = Ci::Runner.find(params[:id])
+ not_found!('Runner') unless runner
+
+ authenticate_delete_runner!(runner)
+
+ status(200)
+ runner.destroy
+ end
+ end
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+ resource :projects do
+ before { authorize_admin_project }
+
+ desc "Disable project's runner" do
+ success ::API::Entities::Runner
+ end
+ params do
+ requires :runner_id, type: Integer, desc: 'The ID of the runner'
+ end
+ delete ':id/runners/:runner_id' do
+ runner_project = user_project.runner_projects.find_by(runner_id: params[:runner_id])
+ not_found!('Runner') unless runner_project
+
+ runner = runner_project.runner
+ forbidden!("Only one project associated with the runner. Please remove the runner instead") if runner.projects.count == 1
+
+ runner_project.destroy
+
+ present runner, with: ::API::Entities::Runner
+ end
+ end
+
+ helpers do
+ def authenticate_delete_runner!(runner)
+ return if current_user.is_admin?
+ forbidden!("Runner is shared") if runner.is_shared?
+ forbidden!("Runner associated with more than one project") if runner.projects.count > 1
+ forbidden!("No access granted") unless user_can_access_runner?(runner)
+ end
+
+ def user_can_access_runner?(runner)
+ current_user.ci_authorized_runners.exists?(runner.id)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/v3/services.rb b/lib/api/v3/services.rb
new file mode 100644
index 00000000000..af0a058f69b
--- /dev/null
+++ b/lib/api/v3/services.rb
@@ -0,0 +1,573 @@
+module API
+ module V3
+ class Services < Grape::API
+ services = {
+ 'asana' => [
+ {
+ required: true,
+ name: :api_key,
+ type: String,
+ desc: 'User API token'
+ },
+ {
+ required: false,
+ name: :restrict_to_branch,
+ type: String,
+ desc: 'Comma-separated list of branches which will be automatically inspected. Leave blank to include all branches'
+ }
+ ],
+ 'assembla' => [
+ {
+ required: true,
+ name: :token,
+ type: String,
+ desc: 'The authentication token'
+ },
+ {
+ required: false,
+ name: :subdomain,
+ type: String,
+ desc: 'Subdomain setting'
+ }
+ ],
+ 'bamboo' => [
+ {
+ required: true,
+ name: :bamboo_url,
+ type: String,
+ desc: 'Bamboo root URL like https://bamboo.example.com'
+ },
+ {
+ required: true,
+ name: :build_key,
+ type: String,
+ desc: 'Bamboo build plan key like'
+ },
+ {
+ required: true,
+ name: :username,
+ type: String,
+ desc: 'A user with API access, if applicable'
+ },
+ {
+ required: true,
+ name: :password,
+ type: String,
+ desc: 'Passord of the user'
+ }
+ ],
+ 'bugzilla' => [
+ {
+ required: true,
+ name: :new_issue_url,
+ type: String,
+ desc: 'New issue URL'
+ },
+ {
+ required: true,
+ name: :issues_url,
+ type: String,
+ desc: 'Issues URL'
+ },
+ {
+ required: true,
+ name: :project_url,
+ type: String,
+ desc: 'Project URL'
+ },
+ {
+ required: false,
+ name: :description,
+ type: String,
+ desc: 'Description'
+ },
+ {
+ required: false,
+ name: :title,
+ type: String,
+ desc: 'Title'
+ }
+ ],
+ 'buildkite' => [
+ {
+ required: true,
+ name: :token,
+ type: String,
+ desc: 'Buildkite project GitLab token'
+ },
+ {
+ required: true,
+ name: :project_url,
+ type: String,
+ desc: 'The buildkite project URL'
+ },
+ {
+ required: false,
+ name: :enable_ssl_verification,
+ type: Boolean,
+ desc: 'Enable SSL verification for communication'
+ }
+ ],
+ 'builds-email' => [
+ {
+ required: true,
+ name: :recipients,
+ type: String,
+ desc: 'Comma-separated list of recipient email addresses'
+ },
+ {
+ required: false,
+ name: :add_pusher,
+ type: Boolean,
+ desc: 'Add pusher to recipients list'
+ },
+ {
+ required: false,
+ name: :notify_only_broken_builds,
+ type: Boolean,
+ desc: 'Notify only broken builds'
+ }
+ ],
+ 'campfire' => [
+ {
+ required: true,
+ name: :token,
+ type: String,
+ desc: 'Campfire token'
+ },
+ {
+ required: false,
+ name: :subdomain,
+ type: String,
+ desc: 'Campfire subdomain'
+ },
+ {
+ required: false,
+ name: :room,
+ type: String,
+ desc: 'Campfire room'
+ }
+ ],
+ 'custom-issue-tracker' => [
+ {
+ required: true,
+ name: :new_issue_url,
+ type: String,
+ desc: 'New issue URL'
+ },
+ {
+ required: true,
+ name: :issues_url,
+ type: String,
+ desc: 'Issues URL'
+ },
+ {
+ required: true,
+ name: :project_url,
+ type: String,
+ desc: 'Project URL'
+ },
+ {
+ required: false,
+ name: :description,
+ type: String,
+ desc: 'Description'
+ },
+ {
+ required: false,
+ name: :title,
+ type: String,
+ desc: 'Title'
+ }
+ ],
+ 'drone-ci' => [
+ {
+ required: true,
+ name: :token,
+ type: String,
+ desc: 'Drone CI token'
+ },
+ {
+ required: true,
+ name: :drone_url,
+ type: String,
+ desc: 'Drone CI URL'
+ },
+ {
+ required: false,
+ name: :enable_ssl_verification,
+ type: Boolean,
+ desc: 'Enable SSL verification for communication'
+ }
+ ],
+ 'emails-on-push' => [
+ {
+ required: true,
+ name: :recipients,
+ type: String,
+ desc: 'Comma-separated list of recipient email addresses'
+ },
+ {
+ required: false,
+ name: :disable_diffs,
+ type: Boolean,
+ desc: 'Disable code diffs'
+ },
+ {
+ required: false,
+ name: :send_from_committer_email,
+ type: Boolean,
+ desc: 'Send from committer'
+ }
+ ],
+ 'external-wiki' => [
+ {
+ required: true,
+ name: :external_wiki_url,
+ type: String,
+ desc: 'The URL of the external Wiki'
+ }
+ ],
+ 'flowdock' => [
+ {
+ required: true,
+ name: :token,
+ type: String,
+ desc: 'Flowdock token'
+ }
+ ],
+ 'gemnasium' => [
+ {
+ required: true,
+ name: :api_key,
+ type: String,
+ desc: 'Your personal API key on gemnasium.com'
+ },
+ {
+ required: true,
+ name: :token,
+ type: String,
+ desc: "The project's slug on gemnasium.com"
+ }
+ ],
+ 'hipchat' => [
+ {
+ required: true,
+ name: :token,
+ type: String,
+ desc: 'The room token'
+ },
+ {
+ required: false,
+ name: :room,
+ type: String,
+ desc: 'The room name or ID'
+ },
+ {
+ required: false,
+ name: :color,
+ type: String,
+ desc: 'The room color'
+ },
+ {
+ required: false,
+ name: :notify,
+ type: Boolean,
+ desc: 'Enable notifications'
+ },
+ {
+ required: false,
+ name: :api_version,
+ type: String,
+ desc: 'Leave blank for default (v2)'
+ },
+ {
+ required: false,
+ name: :server,
+ type: String,
+ desc: 'Leave blank for default. https://hipchat.example.com'
+ }
+ ],
+ 'irker' => [
+ {
+ required: true,
+ name: :recipients,
+ type: String,
+ desc: 'Recipients/channels separated by whitespaces'
+ },
+ {
+ required: false,
+ name: :default_irc_uri,
+ type: String,
+ desc: 'Default: irc://irc.network.net:6697'
+ },
+ {
+ required: false,
+ name: :server_host,
+ type: String,
+ desc: 'Server host. Default localhost'
+ },
+ {
+ required: false,
+ name: :server_port,
+ type: Integer,
+ desc: 'Server port. Default 6659'
+ },
+ {
+ required: false,
+ name: :colorize_messages,
+ type: Boolean,
+ desc: 'Colorize messages'
+ }
+ ],
+ 'jira' => [
+ {
+ required: true,
+ name: :url,
+ type: String,
+ desc: 'The URL to the JIRA project which is being linked to this GitLab project, e.g., https://jira.example.com'
+ },
+ {
+ required: true,
+ name: :project_key,
+ type: String,
+ desc: 'The short identifier for your JIRA project, all uppercase, e.g., PROJ'
+ },
+ {
+ required: false,
+ name: :username,
+ type: String,
+ desc: 'The username of the user created to be used with GitLab/JIRA'
+ },
+ {
+ required: false,
+ name: :password,
+ type: String,
+ desc: 'The password of the user created to be used with GitLab/JIRA'
+ },
+ {
+ required: false,
+ name: :jira_issue_transition_id,
+ type: Integer,
+ desc: 'The ID of a transition that moves issues to a closed state. You can find this number under the JIRA workflow administration (**Administration > Issues > Workflows**) by selecting **View** under **Operations** of the desired workflow of your project. The ID of each state can be found inside the parenthesis of each transition name under the **Transitions (id)** column ([see screenshot][trans]). By default, this ID is set to `2`'
+ }
+ ],
+
+ 'kubernetes' => [
+ {
+ required: true,
+ name: :namespace,
+ type: String,
+ desc: 'The Kubernetes namespace to use'
+ },
+ {
+ required: true,
+ name: :api_url,
+ type: String,
+ desc: 'The URL to the Kubernetes cluster API, e.g., https://kubernetes.example.com'
+ },
+ {
+ required: true,
+ name: :token,
+ type: String,
+ desc: 'The service token to authenticate against the Kubernetes cluster with'
+ },
+ {
+ required: false,
+ name: :ca_pem,
+ type: String,
+ desc: 'A custom certificate authority bundle to verify the Kubernetes cluster with (PEM format)'
+ },
+ ],
+ 'mattermost-slash-commands' => [
+ {
+ required: true,
+ name: :token,
+ type: String,
+ desc: 'The Mattermost token'
+ }
+ ],
+ 'slack-slash-commands' => [
+ {
+ required: true,
+ name: :token,
+ type: String,
+ desc: 'The Slack token'
+ }
+ ],
+ 'pipelines-email' => [
+ {
+ required: true,
+ name: :recipients,
+ type: String,
+ desc: 'Comma-separated list of recipient email addresses'
+ },
+ {
+ required: false,
+ name: :notify_only_broken_builds,
+ type: Boolean,
+ desc: 'Notify only broken builds'
+ }
+ ],
+ 'pivotaltracker' => [
+ {
+ required: true,
+ name: :token,
+ type: String,
+ desc: 'The Pivotaltracker token'
+ },
+ {
+ required: false,
+ name: :restrict_to_branch,
+ type: String,
+ desc: 'Comma-separated list of branches which will be automatically inspected. Leave blank to include all branches.'
+ }
+ ],
+ 'pushover' => [
+ {
+ required: true,
+ name: :api_key,
+ type: String,
+ desc: 'The application key'
+ },
+ {
+ required: true,
+ name: :user_key,
+ type: String,
+ desc: 'The user key'
+ },
+ {
+ required: true,
+ name: :priority,
+ type: String,
+ desc: 'The priority'
+ },
+ {
+ required: true,
+ name: :device,
+ type: String,
+ desc: 'Leave blank for all active devices'
+ },
+ {
+ required: true,
+ name: :sound,
+ type: String,
+ desc: 'The sound of the notification'
+ }
+ ],
+ 'redmine' => [
+ {
+ required: true,
+ name: :new_issue_url,
+ type: String,
+ desc: 'The new issue URL'
+ },
+ {
+ required: true,
+ name: :project_url,
+ type: String,
+ desc: 'The project URL'
+ },
+ {
+ required: true,
+ name: :issues_url,
+ type: String,
+ desc: 'The issues URL'
+ },
+ {
+ required: false,
+ name: :description,
+ type: String,
+ desc: 'The description of the tracker'
+ }
+ ],
+ 'slack' => [
+ {
+ required: true,
+ name: :webhook,
+ type: String,
+ desc: 'The Slack webhook. e.g. https://hooks.slack.com/services/...'
+ },
+ {
+ required: false,
+ name: :new_issue_url,
+ type: String,
+ desc: 'The user name'
+ },
+ {
+ required: false,
+ name: :channel,
+ type: String,
+ desc: 'The channel name'
+ }
+ ],
+ 'mattermost' => [
+ {
+ required: true,
+ name: :webhook,
+ type: String,
+ desc: 'The Mattermost webhook. e.g. http://mattermost_host/hooks/...'
+ }
+ ],
+ 'teamcity' => [
+ {
+ required: true,
+ name: :teamcity_url,
+ type: String,
+ desc: 'TeamCity root URL like https://teamcity.example.com'
+ },
+ {
+ required: true,
+ name: :build_type,
+ type: String,
+ desc: 'Build configuration ID'
+ },
+ {
+ required: true,
+ name: :username,
+ type: String,
+ desc: 'A user with permissions to trigger a manual build'
+ },
+ {
+ required: true,
+ name: :password,
+ type: String,
+ desc: 'The password of the user'
+ }
+ ]
+ }
+
+ resource :projects do
+ before { authenticate! }
+ before { authorize_admin_project }
+
+ helpers do
+ def service_attributes(service)
+ service.fields.inject([]) do |arr, hash|
+ arr << hash[:name].to_sym
+ end
+ end
+ end
+
+ desc "Delete a service for project"
+ params do
+ requires :service_slug, type: String, values: services.keys, desc: 'The name of the service'
+ end
+ delete ":id/services/:service_slug" do
+ service = user_project.find_or_initialize_service(params[:service_slug].underscore)
+
+ attrs = service_attributes(service).inject({}) do |hash, key|
+ hash.merge!(key => nil)
+ end
+
+ if service.update_attributes(attrs.merge(active: false))
+ status(200)
+ true
+ else
+ render_api_error!('400 Bad Request', 400)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/v3/subscriptions.rb b/lib/api/v3/subscriptions.rb
new file mode 100644
index 00000000000..02a4157c26e
--- /dev/null
+++ b/lib/api/v3/subscriptions.rb
@@ -0,0 +1,53 @@
+module API
+ module V3
+ class Subscriptions < Grape::API
+ before { authenticate! }
+
+ subscribable_types = {
+ '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) },
+ }
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ requires :subscribable_id, type: String, desc: 'The ID of a resource'
+ end
+ resource :projects do
+ subscribable_types.each do |type, finder|
+ type_singularized = type.singularize
+ entity_class = ::API::Entities.const_get(type_singularized.camelcase)
+
+ desc 'Subscribe to a resource' do
+ success entity_class
+ end
+ post ":id/#{type}/:subscribable_id/subscription" do
+ resource = instance_exec(params[:subscribable_id], &finder)
+
+ if resource.subscribed?(current_user, user_project)
+ not_modified!
+ else
+ resource.subscribe(current_user, user_project)
+ present resource, with: entity_class, current_user: current_user, project: user_project
+ end
+ end
+
+ desc 'Unsubscribe from a resource' do
+ success entity_class
+ end
+ delete ":id/#{type}/:subscribable_id/subscription" do
+ resource = instance_exec(params[:subscribable_id], &finder)
+
+ if !resource.subscribed?(current_user, user_project)
+ not_modified!
+ else
+ resource.unsubscribe(current_user, user_project)
+ present resource, with: entity_class, current_user: current_user, project: user_project
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/v3/system_hooks.rb b/lib/api/v3/system_hooks.rb
new file mode 100644
index 00000000000..5787c06fc12
--- /dev/null
+++ b/lib/api/v3/system_hooks.rb
@@ -0,0 +1,32 @@
+module API
+ module V3
+ class SystemHooks < Grape::API
+ before do
+ authenticate!
+ authenticated_as_admin!
+ end
+
+ resource :hooks do
+ desc 'Get the list of system hooks' do
+ success ::API::Entities::Hook
+ end
+ get do
+ present SystemHook.all, with: ::API::Entities::Hook
+ end
+
+ desc 'Delete a hook' do
+ success ::API::Entities::Hook
+ end
+ params do
+ requires :id, type: Integer, desc: 'The ID of the system hook'
+ end
+ delete ":id" do
+ hook = SystemHook.find_by(id: params[:id])
+ not_found!('System hook') unless hook
+
+ present hook.destroy, with: ::API::Entities::Hook
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/v3/tags.rb b/lib/api/v3/tags.rb
new file mode 100644
index 00000000000..6913720d9c5
--- /dev/null
+++ b/lib/api/v3/tags.rb
@@ -0,0 +1,40 @@
+module API
+ module V3
+ class Tags < Grape::API
+ before { authorize! :download_code, user_project }
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+ resource :projects do
+ desc 'Get a project repository tags' do
+ success ::API::Entities::RepoTag
+ end
+ get ":id/repository/tags" do
+ tags = user_project.repository.tags.sort_by(&:name).reverse
+ present tags, with: ::API::Entities::RepoTag, project: user_project
+ end
+
+ desc 'Delete a repository tag'
+ params do
+ requires :tag_name, type: String, desc: 'The name of the tag'
+ end
+ delete ":id/repository/tags/:tag_name", requirements: { tag_name: /.+/ } do
+ authorize_push_project
+
+ result = ::Tags::DestroyService.new(user_project, current_user).
+ execute(params[:tag_name])
+
+ if result[:status] == :success
+ status(200)
+ {
+ tag_name: params[:tag_name]
+ }
+ else
+ render_api_error!(result[:message], result[:return_code])
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/v3/todos.rb b/lib/api/v3/todos.rb
new file mode 100644
index 00000000000..e60cb25e57b
--- /dev/null
+++ b/lib/api/v3/todos.rb
@@ -0,0 +1,30 @@
+module API
+ module V3
+ class Todos < Grape::API
+ before { authenticate! }
+
+ resource :todos do
+ desc 'Mark a todo as done' do
+ success ::API::Entities::Todo
+ end
+ params do
+ requires :id, type: Integer, desc: 'The ID of the todo being marked as done'
+ end
+ delete ':id' do
+ todo = current_user.todos.find(params[:id])
+ TodoService.new.mark_todos_as_done([todo], current_user)
+
+ present todo.reload, with: ::API::Entities::Todo, current_user: current_user
+ end
+
+ desc 'Mark all todos as done'
+ delete do
+ status(200)
+
+ todos = TodosFinder.new(current_user, params).execute
+ TodoService.new.mark_todos_as_done(todos, current_user)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/v3/triggers.rb b/lib/api/v3/triggers.rb
new file mode 100644
index 00000000000..4051d4bca8d
--- /dev/null
+++ b/lib/api/v3/triggers.rb
@@ -0,0 +1,30 @@
+module API
+ module V3
+ class Triggers < Grape::API
+ include PaginationParams
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+ resource :projects do
+ desc 'Delete a trigger' do
+ success ::API::Entities::Trigger
+ end
+ params do
+ requires :token, type: String, desc: 'The unique token of trigger'
+ end
+ delete ':id/triggers/:token' do
+ authenticate!
+ authorize! :admin_build, user_project
+
+ trigger = user_project.triggers.find_by(token: params[:token].to_s)
+ return not_found!('Trigger') unless trigger
+
+ trigger.destroy
+
+ present trigger, with: ::API::Entities::Trigger
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/v3/users.rb b/lib/api/v3/users.rb
new file mode 100644
index 00000000000..14f54731730
--- /dev/null
+++ b/lib/api/v3/users.rb
@@ -0,0 +1,149 @@
+module API
+ module V3
+ class Users < Grape::API
+ include PaginationParams
+
+ before do
+ allow_access_with_scope :read_user if request.get?
+ authenticate!
+ end
+
+ resource :users, requirements: { uid: /[0-9]*/, id: /[0-9]*/ } do
+ desc 'Get the SSH keys of a specified user. Available only for admins.' do
+ success ::API::Entities::SSHKey
+ end
+ params do
+ requires :id, type: Integer, desc: 'The ID of the user'
+ use :pagination
+ end
+ get ':id/keys' do
+ authenticated_as_admin!
+
+ user = User.find_by(id: params[:id])
+ not_found!('User') unless user
+
+ present paginate(user.keys), with: ::API::Entities::SSHKey
+ end
+
+ desc 'Get the emails addresses of a specified user. Available only for admins.' do
+ success ::API::Entities::Email
+ end
+ params do
+ requires :id, type: Integer, desc: 'The ID of the user'
+ use :pagination
+ end
+ get ':id/emails' do
+ authenticated_as_admin!
+ user = User.find_by(id: params[:id])
+ not_found!('User') unless user
+
+ present user.emails, with: ::API::Entities::Email
+ end
+
+ desc 'Block a user. Available only for admins.'
+ params do
+ requires :id, type: Integer, desc: 'The ID of the user'
+ end
+ put ':id/block' do
+ authenticated_as_admin!
+ user = User.find_by(id: params[:id])
+ not_found!('User') unless user
+
+ if !user.ldap_blocked?
+ user.block
+ else
+ forbidden!('LDAP blocked users cannot be modified by the API')
+ end
+ end
+
+ desc 'Unblock a user. Available only for admins.'
+ params do
+ requires :id, type: Integer, desc: 'The ID of the user'
+ end
+ put ':id/unblock' do
+ authenticated_as_admin!
+ user = User.find_by(id: params[:id])
+ not_found!('User') unless user
+
+ if user.ldap_blocked?
+ forbidden!('LDAP blocked users cannot be unblocked by the API')
+ else
+ user.activate
+ end
+ end
+
+ desc 'Get the contribution events of a specified user' do
+ detail 'This feature was introduced in GitLab 8.13.'
+ success ::API::V3::Entities::Event
+ end
+ params do
+ requires :id, type: Integer, desc: 'The ID of the user'
+ use :pagination
+ end
+ get ':id/events' do
+ user = User.find_by(id: params[:id])
+ not_found!('User') unless user
+
+ events = user.events.
+ merge(ProjectsFinder.new.execute(current_user)).
+ references(:project).
+ with_associations.
+ recent
+
+ present paginate(events), with: ::API::V3::Entities::Event
+ end
+
+ desc 'Delete an existing SSH key from a specified user. Available only for admins.' do
+ success ::API::Entities::SSHKey
+ end
+ params do
+ requires :id, type: Integer, desc: 'The ID of the user'
+ requires :key_id, type: Integer, desc: 'The ID of the SSH key'
+ end
+ delete ':id/keys/:key_id' do
+ authenticated_as_admin!
+
+ user = User.find_by(id: params[:id])
+ not_found!('User') unless user
+
+ key = user.keys.find_by(id: params[:key_id])
+ not_found!('Key') unless key
+
+ present key.destroy, with: ::API::Entities::SSHKey
+ end
+ end
+
+ resource :user do
+ desc "Get the currently authenticated user's SSH keys" do
+ success ::API::Entities::SSHKey
+ end
+ params do
+ use :pagination
+ end
+ get "keys" do
+ present current_user.keys, with: ::API::Entities::SSHKey
+ end
+
+ desc "Get the currently authenticated user's email addresses" do
+ success ::API::Entities::Email
+ end
+ get "emails" do
+ present current_user.emails, with: ::API::Entities::Email
+ end
+
+ desc 'Delete an SSH key from the currently authenticated user' do
+ success ::API::Entities::SSHKey
+ end
+ params do
+ requires :key_id, type: Integer, desc: 'The ID of the SSH key'
+ end
+ delete "keys/:key_id" do
+ key = current_user.keys.find_by(id: params[:key_id])
+ not_found!('Key') unless key
+
+ present key.destroy, with: ::API::Entities::SSHKey
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/v3/variables.rb b/lib/api/v3/variables.rb
new file mode 100644
index 00000000000..0f55a14fb28
--- /dev/null
+++ b/lib/api/v3/variables.rb
@@ -0,0 +1,29 @@
+module API
+ module V3
+ class Variables < Grape::API
+ include PaginationParams
+
+ before { authenticate! }
+ before { authorize! :admin_build, user_project }
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+
+ resource :projects do
+ desc 'Delete an existing variable from a project' do
+ success ::API::Entities::Variable
+ end
+ params do
+ requires :key, type: String, desc: 'The key of the variable'
+ end
+ delete ':id/variables/:key' do
+ variable = user_project.variables.find_by(key: params[:key])
+ not_found!('Variable') unless variable
+
+ present variable.destroy, with: ::API::Entities::Variable
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/variables.rb b/lib/api/variables.rb
index f623b1dfe9f..77e5d54c225 100644
--- a/lib/api/variables.rb
+++ b/lib/api/variables.rb
@@ -1,5 +1,4 @@
module API
- # Projects variables API
class Variables < Grape::API
include PaginationParams
@@ -81,10 +80,9 @@ module API
end
delete ':id/variables/:key' do
variable = user_project.variables.find_by(key: params[:key])
+ not_found!('Variable') unless variable
- return not_found!('Variable') unless variable
-
- present variable.destroy, with: Entities::Variable
+ variable.destroy
end
end
end
diff --git a/lib/backup/database.rb b/lib/backup/database.rb
index 22319ec6623..4016ac76348 100644
--- a/lib/backup/database.rb
+++ b/lib/backup/database.rb
@@ -5,7 +5,7 @@ module Backup
attr_reader :config, :db_file_name
def initialize
- @config = YAML.load_file(File.join(Rails.root,'config','database.yml'))[Rails.env]
+ @config = YAML.load_file(File.join(Rails.root, 'config', 'database.yml'))[Rails.env]
@db_file_name = File.join(Gitlab.config.backup.path, 'db', 'database.sql.gz')
end
@@ -13,28 +13,32 @@ module Backup
FileUtils.mkdir_p(File.dirname(db_file_name))
FileUtils.rm_f(db_file_name)
compress_rd, compress_wr = IO.pipe
- compress_pid = spawn(*%W(gzip -1 -c), in: compress_rd, out: [db_file_name, 'w', 0600])
+ compress_pid = spawn(*%w(gzip -1 -c), in: compress_rd, out: [db_file_name, 'w', 0600])
compress_rd.close
- dump_pid = case config["adapter"]
- when /^mysql/ then
- $progress.print "Dumping MySQL database #{config['database']} ... "
- # Workaround warnings from MySQL 5.6 about passwords on cmd line
- ENV['MYSQL_PWD'] = config["password"].to_s if config["password"]
- spawn('mysqldump', *mysql_args, config['database'], out: compress_wr)
- when "postgresql" then
- $progress.print "Dumping PostgreSQL database #{config['database']} ... "
- pg_env
- pgsql_args = ["--clean"] # Pass '--clean' to include 'DROP TABLE' statements in the DB dump.
- if Gitlab.config.backup.pg_schema
- pgsql_args << "-n"
- pgsql_args << Gitlab.config.backup.pg_schema
+ dump_pid =
+ case config["adapter"]
+ when /^mysql/ then
+ $progress.print "Dumping MySQL database #{config['database']} ... "
+ # Workaround warnings from MySQL 5.6 about passwords on cmd line
+ ENV['MYSQL_PWD'] = config["password"].to_s if config["password"]
+ spawn('mysqldump', *mysql_args, config['database'], out: compress_wr)
+ when "postgresql" then
+ $progress.print "Dumping PostgreSQL database #{config['database']} ... "
+ pg_env
+ pgsql_args = ["--clean"] # Pass '--clean' to include 'DROP TABLE' statements in the DB dump.
+ if Gitlab.config.backup.pg_schema
+ pgsql_args << "-n"
+ pgsql_args << Gitlab.config.backup.pg_schema
+ end
+ spawn('pg_dump', *pgsql_args, config['database'], out: compress_wr)
end
- spawn('pg_dump', *pgsql_args, config['database'], out: compress_wr)
- end
compress_wr.close
- success = [compress_pid, dump_pid].all? { |pid| Process.waitpid(pid); $?.success? }
+ success = [compress_pid, dump_pid].all? do |pid|
+ Process.waitpid(pid)
+ $?.success?
+ end
report_success(success)
abort 'Backup failed' unless success
@@ -42,23 +46,27 @@ module Backup
def restore
decompress_rd, decompress_wr = IO.pipe
- decompress_pid = spawn(*%W(gzip -cd), out: decompress_wr, in: db_file_name)
+ decompress_pid = spawn(*%w(gzip -cd), out: decompress_wr, in: db_file_name)
decompress_wr.close
- restore_pid = case config["adapter"]
- when /^mysql/ then
- $progress.print "Restoring MySQL database #{config['database']} ... "
- # Workaround warnings from MySQL 5.6 about passwords on cmd line
- ENV['MYSQL_PWD'] = config["password"].to_s if config["password"]
- spawn('mysql', *mysql_args, config['database'], in: decompress_rd)
- when "postgresql" then
- $progress.print "Restoring PostgreSQL database #{config['database']} ... "
- pg_env
- spawn('psql', config['database'], in: decompress_rd)
- end
+ restore_pid =
+ case config["adapter"]
+ when /^mysql/ then
+ $progress.print "Restoring MySQL database #{config['database']} ... "
+ # Workaround warnings from MySQL 5.6 about passwords on cmd line
+ ENV['MYSQL_PWD'] = config["password"].to_s if config["password"]
+ spawn('mysql', *mysql_args, config['database'], in: decompress_rd)
+ when "postgresql" then
+ $progress.print "Restoring PostgreSQL database #{config['database']} ... "
+ pg_env
+ spawn('psql', config['database'], in: decompress_rd)
+ end
decompress_rd.close
- success = [decompress_pid, restore_pid].all? { |pid| Process.waitpid(pid); $?.success? }
+ success = [decompress_pid, restore_pid].all? do |pid|
+ Process.waitpid(pid)
+ $?.success?
+ end
report_success(success)
abort 'Restore failed' unless success
diff --git a/lib/backup/files.rb b/lib/backup/files.rb
index cedbb289f6a..30a91647b77 100644
--- a/lib/backup/files.rb
+++ b/lib/backup/files.rb
@@ -8,6 +8,7 @@ module Backup
@name = name
@app_files_dir = File.realpath(app_files_dir)
@files_parent_dir = File.realpath(File.join(@app_files_dir, '..'))
+ @backup_files_dir = File.join(Gitlab.config.backup.path, File.basename(@app_files_dir) )
@backup_tarball = File.join(Gitlab.config.backup.path, name + '.tar.gz')
end
@@ -15,14 +16,28 @@ module Backup
def dump
FileUtils.mkdir_p(Gitlab.config.backup.path)
FileUtils.rm_f(backup_tarball)
- run_pipeline!([%W(tar -C #{app_files_dir} -cf - .), %W(gzip -c -1)], out: [backup_tarball, 'w', 0600])
+
+ if ENV['STRATEGY'] == 'copy'
+ cmd = %W(cp -a #{app_files_dir} #{Gitlab.config.backup.path})
+ output, status = Gitlab::Popen.popen(cmd)
+
+ unless status.zero?
+ puts output
+ abort 'Backup failed'
+ end
+
+ run_pipeline!([%W(tar -C #{@backup_files_dir} -cf - .), %w(gzip -c -1)], out: [backup_tarball, 'w', 0600])
+ FileUtils.rm_rf(@backup_files_dir)
+ else
+ run_pipeline!([%W(tar -C #{app_files_dir} -cf - .), %w(gzip -c -1)], out: [backup_tarball, 'w', 0600])
+ end
end
def restore
backup_existing_files_dir
create_files_dir
- run_pipeline!([%W(gzip -cd), %W(tar -C #{app_files_dir} -xf -)], in: backup_tarball)
+ run_pipeline!([%w(gzip -cd), %W(tar -C #{app_files_dir} -xf -)], in: backup_tarball)
end
def backup_existing_files_dir
@@ -32,7 +47,7 @@ module Backup
end
end
- def run_pipeline!(cmd_list, options={})
+ def run_pipeline!(cmd_list, options = {})
status_list = Open3.pipeline(*cmd_list, options)
abort 'Backup failed' unless status_list.compact.all?(&:success?)
end
diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb
index f099c0651ac..5cc164a6325 100644
--- a/lib/backup/manager.rb
+++ b/lib/backup/manager.rb
@@ -1,8 +1,8 @@
module Backup
class Manager
- ARCHIVES_TO_BACKUP = %w[uploads builds artifacts pages lfs registry]
- FOLDERS_TO_BACKUP = %w[repositories db]
- FILE_NAME_SUFFIX = '_gitlab_backup.tar'
+ ARCHIVES_TO_BACKUP = %w[uploads builds artifacts pages lfs registry].freeze
+ FOLDERS_TO_BACKUP = %w[repositories db].freeze
+ FILE_NAME_SUFFIX = '_gitlab_backup.tar'.freeze
def pack
# Make sure there is a connection
@@ -20,13 +20,13 @@ module Backup
Dir.chdir(Gitlab.config.backup.path) do
File.open("#{Gitlab.config.backup.path}/backup_information.yml",
"w+") do |file|
- file << s.to_yaml.gsub(/^---\n/,'')
+ file << s.to_yaml.gsub(/^---\n/, '')
end
# create archive
$progress.print "Creating backup archive: #{tar_file} ... "
# Set file permissions on open to prevent chmod races.
- tar_system_options = {out: [tar_file, 'w', Gitlab.config.backup.archive_permissions]}
+ tar_system_options = { out: [tar_file, 'w', Gitlab.config.backup.archive_permissions] }
if Kernel.system('tar', '-cf', '-', *backup_contents, tar_system_options)
$progress.puts "done".color(:green)
else
@@ -50,8 +50,8 @@ module Backup
directory = connect_to_remote_directory(connection_settings)
if directory.files.create(key: tar_file, body: File.open(tar_file), public: false,
- multipart_chunk_size: Gitlab.config.backup.upload.multipart_chunk_size,
- encryption: Gitlab.config.backup.upload.encryption)
+ multipart_chunk_size: Gitlab.config.backup.upload.multipart_chunk_size,
+ encryption: Gitlab.config.backup.upload.encryption)
$progress.puts "done".color(:green)
else
puts "uploading backup to #{remote_directory} failed".color(:red)
@@ -123,11 +123,11 @@ module Backup
exit 1
end
- if ENV['BACKUP'].present?
- tar_file = "#{ENV['BACKUP']}#{FILE_NAME_SUFFIX}"
- else
- tar_file = file_list.first
- end
+ tar_file = if ENV['BACKUP'].present?
+ "#{ENV['BACKUP']}#{FILE_NAME_SUFFIX}"
+ else
+ file_list.first
+ end
unless File.exist?(tar_file)
$progress.puts "The backup file #{tar_file} does not exist!"
@@ -158,7 +158,7 @@ module Backup
end
def tar_version
- tar_version, _ = Gitlab::Popen.popen(%W(tar --version))
+ tar_version, _ = Gitlab::Popen.popen(%w(tar --version))
tar_version.force_encoding('locale').split("\n").first
end
diff --git a/lib/backup/repository.rb b/lib/backup/repository.rb
index 91e43dcb114..d16d5ba4960 100644
--- a/lib/backup/repository.rb
+++ b/lib/backup/repository.rb
@@ -2,7 +2,7 @@ require 'yaml'
module Backup
class Repository
-
+ # rubocop:disable Metrics/AbcSize
def dump
prepare
@@ -85,11 +85,11 @@ module Backup
project.ensure_dir_exist
- if File.exists?(path_to_project_bundle)
- cmd = %W(#{Gitlab.config.git.bin_path} clone --bare #{path_to_project_bundle} #{path_to_project_repo})
- else
- cmd = %W(#{Gitlab.config.git.bin_path} init --bare #{path_to_project_repo})
- end
+ cmd = if File.exist?(path_to_project_bundle)
+ %W(#{Gitlab.config.git.bin_path} clone --bare #{path_to_project_bundle} #{path_to_project_repo})
+ else
+ %W(#{Gitlab.config.git.bin_path} init --bare #{path_to_project_repo})
+ end
output, status = Gitlab::Popen.popen(cmd)
if status.zero?
@@ -150,6 +150,7 @@ module Backup
puts output
end
end
+ # rubocop:enable Metrics/AbcSize
protected
@@ -193,7 +194,7 @@ module Backup
end
def silent
- {err: '/dev/null', out: '/dev/null'}
+ { err: '/dev/null', out: '/dev/null' }
end
private
diff --git a/lib/backup/uploads.rb b/lib/backup/uploads.rb
index 9261f77f3c9..35118375499 100644
--- a/lib/backup/uploads.rb
+++ b/lib/backup/uploads.rb
@@ -2,7 +2,6 @@ require 'backup/files'
module Backup
class Uploads < Files
-
def initialize
super('uploads', Rails.root.join('public/uploads'))
end
diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb
index 955d857c679..02d5ad70fa7 100644
--- a/lib/banzai/filter/abstract_reference_filter.rb
+++ b/lib/banzai/filter/abstract_reference_filter.rb
@@ -33,7 +33,12 @@ module Banzai
# Returns a String replaced with the return of the block.
def self.references_in(text, pattern = object_class.reference_pattern)
text.gsub(pattern) do |match|
- yield match, $~[object_sym].to_i, $~[:project], $~[:namespace], $~
+ symbol = $~[object_sym]
+ if object_class.reference_valid?(symbol)
+ yield match, symbol.to_i, $~[:project], $~[:namespace], $~
+ else
+ match
+ end
end
end
@@ -155,11 +160,12 @@ module Banzai
data = data_attributes_for(link_content || match, project, object, link: !!link_content)
- if matches.names.include?("url") && matches[:url]
- url = matches[:url]
- else
- url = url_for_object_cached(object, project)
- end
+ url =
+ if matches.names.include?("url") && matches[:url]
+ matches[:url]
+ else
+ url_for_object_cached(object, project)
+ end
content = link_content || object_link_text(object, matches)
diff --git a/lib/banzai/filter/autolink_filter.rb b/lib/banzai/filter/autolink_filter.rb
index 80c844baecd..b8d2673c1a6 100644
--- a/lib/banzai/filter/autolink_filter.rb
+++ b/lib/banzai/filter/autolink_filter.rb
@@ -37,7 +37,7 @@ module Banzai
and contains(., '://')
and not(starts-with(., 'http'))
and not(starts-with(., 'ftp'))
- ])
+ ]).freeze
def call
return doc if context[:autolink] == false
diff --git a/lib/banzai/filter/gollum_tags_filter.rb b/lib/banzai/filter/gollum_tags_filter.rb
index d08267a9d6c..0ea4eeaed5b 100644
--- a/lib/banzai/filter/gollum_tags_filter.rb
+++ b/lib/banzai/filter/gollum_tags_filter.rb
@@ -149,11 +149,12 @@ module Banzai
name, reference = *parts.compact.map(&:strip)
end
- if url?(reference)
- href = reference
- else
- href = ::File.join(project_wiki_base_path, reference)
- end
+ href =
+ if url?(reference)
+ reference
+ else
+ ::File.join(project_wiki_base_path, reference)
+ end
content_tag(:a, name || reference, href: href, class: 'gfm')
end
diff --git a/lib/banzai/filter/image_link_filter.rb b/lib/banzai/filter/image_link_filter.rb
index f0fb6084a35..651b55523c0 100644
--- a/lib/banzai/filter/image_link_filter.rb
+++ b/lib/banzai/filter/image_link_filter.rb
@@ -8,11 +8,6 @@ module Banzai
# of the anchor, and then replace the img with the link-wrapped version.
def call
doc.xpath('descendant-or-self::img[not(ancestor::a)]').each do |img|
- div = doc.document.create_element(
- 'div',
- class: 'image-container'
- )
-
link = doc.document.create_element(
'a',
class: 'no-attachment-icon',
@@ -22,9 +17,7 @@ module Banzai
link.children = img.clone
- div.children = link
-
- img.replace(div)
+ img.replace(link)
end
doc
diff --git a/lib/banzai/filter/issue_reference_filter.rb b/lib/banzai/filter/issue_reference_filter.rb
index fd6b9704132..044d18ff824 100644
--- a/lib/banzai/filter/issue_reference_filter.rb
+++ b/lib/banzai/filter/issue_reference_filter.rb
@@ -39,11 +39,12 @@ module Banzai
projects_per_reference.each do |path, project|
issue_ids = references_per_project[path]
- if project.default_issues_tracker?
- issues = project.issues.where(iid: issue_ids.to_a)
- else
- issues = issue_ids.map { |id| ExternalIssue.new(id, project) }
- end
+ issues =
+ if project.default_issues_tracker?
+ project.issues.where(iid: issue_ids.to_a)
+ else
+ issue_ids.map { |id| ExternalIssue.new(id, project) }
+ end
issues.each do |issue|
hash[project][issue.iid.to_i] = issue
diff --git a/lib/banzai/filter/plantuml_filter.rb b/lib/banzai/filter/plantuml_filter.rb
index e194cf59275..b2537117558 100644
--- a/lib/banzai/filter/plantuml_filter.rb
+++ b/lib/banzai/filter/plantuml_filter.rb
@@ -7,7 +7,7 @@ module Banzai
#
class PlantumlFilter < HTML::Pipeline::Filter
def call
- return doc unless doc.at('pre.plantuml') and settings.plantuml_enabled
+ return doc unless doc.at('pre.plantuml') && settings.plantuml_enabled
plantuml_setup
diff --git a/lib/bitbucket/connection.rb b/lib/bitbucket/connection.rb
index 7e55cf4deab..b9279c33f5b 100644
--- a/lib/bitbucket/connection.rb
+++ b/lib/bitbucket/connection.rb
@@ -1,8 +1,8 @@
module Bitbucket
class Connection
- DEFAULT_API_VERSION = '2.0'
- DEFAULT_BASE_URI = 'https://api.bitbucket.org/'
- DEFAULT_QUERY = {}
+ DEFAULT_API_VERSION = '2.0'.freeze
+ DEFAULT_BASE_URI = 'https://api.bitbucket.org/'.freeze
+ DEFAULT_QUERY = {}.freeze
attr_reader :expires_at, :expires_in, :refresh_token, :token
@@ -24,9 +24,7 @@ module Bitbucket
response.parsed
end
- def expired?
- connection.expired?
- end
+ delegate :expired?, to: :connection
def refresh!
response = connection.refresh!
diff --git a/lib/bitbucket/representation/repo.rb b/lib/bitbucket/representation/repo.rb
index 423eff8f2a5..59b0fda8e14 100644
--- a/lib/bitbucket/representation/repo.rb
+++ b/lib/bitbucket/representation/repo.rb
@@ -23,7 +23,7 @@ module Bitbucket
url = raw['links']['clone'].find { |link| link['name'] == 'https' }.fetch('href')
if token.present?
- clone_url = URI::parse(url)
+ clone_url = URI.parse(url)
clone_url.user = "x-token-auth:#{token}"
clone_url.to_s
else
diff --git a/lib/ci/ansi2html.rb b/lib/ci/ansi2html.rb
index c10d3616f31..b3ccad7b28d 100644
--- a/lib/ci/ansi2html.rb
+++ b/lib/ci/ansi2html.rb
@@ -13,7 +13,7 @@ module Ci
5 => 'magenta',
6 => 'cyan',
7 => 'white', # not that this is gray in the dark (aka default) color table
- }
+ }.freeze
STYLE_SWITCHES = {
bold: 0x01,
@@ -21,7 +21,7 @@ module Ci
underline: 0x04,
conceal: 0x08,
cross: 0x10,
- }
+ }.freeze
def self.convert(ansi, state = nil)
Converter.new.convert(ansi, state)
@@ -29,64 +29,108 @@ module Ci
class Converter
def on_0(s) reset() end
+
def on_1(s) enable(STYLE_SWITCHES[:bold]) end
+
def on_3(s) enable(STYLE_SWITCHES[:italic]) end
+
def on_4(s) enable(STYLE_SWITCHES[:underline]) end
+
def on_8(s) enable(STYLE_SWITCHES[:conceal]) end
+
def on_9(s) enable(STYLE_SWITCHES[:cross]) end
def on_21(s) disable(STYLE_SWITCHES[:bold]) end
+
def on_22(s) disable(STYLE_SWITCHES[:bold]) end
+
def on_23(s) disable(STYLE_SWITCHES[:italic]) end
+
def on_24(s) disable(STYLE_SWITCHES[:underline]) end
+
def on_28(s) disable(STYLE_SWITCHES[:conceal]) end
+
def on_29(s) disable(STYLE_SWITCHES[:cross]) end
def on_30(s) set_fg_color(0) end
+
def on_31(s) set_fg_color(1) end
+
def on_32(s) set_fg_color(2) end
+
def on_33(s) set_fg_color(3) end
+
def on_34(s) set_fg_color(4) end
+
def on_35(s) set_fg_color(5) end
+
def on_36(s) set_fg_color(6) end
+
def on_37(s) set_fg_color(7) end
+
def on_38(s) set_fg_color_256(s) end
+
def on_39(s) set_fg_color(9) end
def on_40(s) set_bg_color(0) end
+
def on_41(s) set_bg_color(1) end
+
def on_42(s) set_bg_color(2) end
+
def on_43(s) set_bg_color(3) end
+
def on_44(s) set_bg_color(4) end
+
def on_45(s) set_bg_color(5) end
+
def on_46(s) set_bg_color(6) end
+
def on_47(s) set_bg_color(7) end
+
def on_48(s) set_bg_color_256(s) end
+
def on_49(s) set_bg_color(9) end
def on_90(s) set_fg_color(0, 'l') end
+
def on_91(s) set_fg_color(1, 'l') end
+
def on_92(s) set_fg_color(2, 'l') end
+
def on_93(s) set_fg_color(3, 'l') end
+
def on_94(s) set_fg_color(4, 'l') end
+
def on_95(s) set_fg_color(5, 'l') end
+
def on_96(s) set_fg_color(6, 'l') end
+
def on_97(s) set_fg_color(7, 'l') end
+
def on_99(s) set_fg_color(9, 'l') end
def on_100(s) set_bg_color(0, 'l') end
+
def on_101(s) set_bg_color(1, 'l') end
+
def on_102(s) set_bg_color(2, 'l') end
+
def on_103(s) set_bg_color(3, 'l') end
+
def on_104(s) set_bg_color(4, 'l') end
+
def on_105(s) set_bg_color(5, 'l') end
+
def on_106(s) set_bg_color(6, 'l') end
+
def on_107(s) set_bg_color(7, 'l') end
+
def on_109(s) set_bg_color(9, 'l') end
attr_accessor :offset, :n_open_tags, :fg_color, :bg_color, :style_mask
- STATE_PARAMS = [:offset, :n_open_tags, :fg_color, :bg_color, :style_mask]
+ STATE_PARAMS = [:offset, :n_open_tags, :fg_color, :bg_color, :style_mask].freeze
def convert(raw, new_state)
reset_state
@@ -126,7 +170,7 @@ module Ci
# We are only interested in color and text style changes - triggered by
# sequences starting with '\e[' and ending with 'm'. Any other control
# sequence gets stripped (including stuff like "delete last line")
- return unless indicator == '[' and terminator == 'm'
+ return unless indicator == '[' && terminator == 'm'
close_open_tags()
diff --git a/lib/ci/api/builds.rb b/lib/ci/api/builds.rb
index 8b939663ffd..b51e76d93f2 100644
--- a/lib/ci/api/builds.rb
+++ b/lib/ci/api/builds.rb
@@ -167,7 +167,10 @@ module Ci
build.artifacts_file = artifacts
build.artifacts_metadata = metadata
- build.artifacts_expire_in = params['expire_in']
+ build.artifacts_expire_in =
+ params['expire_in'] ||
+ Gitlab::CurrentSettings.current_application_settings
+ .default_artifacts_expire_in
if build.save
present(build, with: Entities::BuildDetails)
@@ -214,6 +217,7 @@ module Ci
build = Ci::Build.find_by_id(params[:id])
authenticate_build!(build)
+ status(200)
build.erase_artifacts!
end
end
diff --git a/lib/ci/api/helpers.rb b/lib/ci/api/helpers.rb
index 5ff25a3a9b2..996990b464f 100644
--- a/lib/ci/api/helpers.rb
+++ b/lib/ci/api/helpers.rb
@@ -1,7 +1,7 @@
module Ci
module API
module Helpers
- BUILD_TOKEN_HEADER = "HTTP_BUILD_TOKEN"
+ BUILD_TOKEN_HEADER = "HTTP_BUILD_TOKEN".freeze
BUILD_TOKEN_PARAM = :token
UPDATE_RUNNER_EVERY = 10 * 60
@@ -60,7 +60,7 @@ module Ci
end
def build_not_found!
- if headers['User-Agent'].to_s.match(/gitlab-ci-multi-runner \d+\.\d+\.\d+(~beta\.\d+\.g[0-9a-f]+)? /)
+ if headers['User-Agent'].to_s =~ /gitlab-ci-multi-runner \d+\.\d+\.\d+(~beta\.\d+\.g[0-9a-f]+)? /
no_content!
else
not_found!
@@ -73,7 +73,7 @@ module Ci
def get_runner_version_from_params
return unless params["info"].present?
- attributes_for_keys(["name", "version", "revision", "platform", "architecture"], params["info"])
+ attributes_for_keys(%w(name version revision platform architecture), params["info"])
end
def max_artifacts_size
diff --git a/lib/ci/api/runners.rb b/lib/ci/api/runners.rb
index bcc82969eb3..c1fd959ef14 100644
--- a/lib/ci/api/runners.rb
+++ b/lib/ci/api/runners.rb
@@ -1,44 +1,38 @@
module Ci
module API
- # Runners API
class Runners < Grape::API
resource :runners do
- # Delete runner
- # Parameters:
- # token (required) - The unique token of runner
- #
- # Example Request:
- # GET /runners/delete
+ desc 'Delete a runner'
+ params do
+ requires :token, type: String, desc: 'The unique token of the runner'
+ end
delete "delete" do
- required_attributes! [:token]
authenticate_runner!
+
+ status(200)
Ci::Runner.find_by_token(params[:token]).destroy
end
- # Register a new runner
- #
- # Note: This is an "internal" API called when setting up
- # runners, so it is authenticated differently.
- #
- # Parameters:
- # token (required) - The unique token of runner
- #
- # Example Request:
- # POST /runners/register
+ desc 'Register a new runner' do
+ success Entities::Runner
+ end
+ params do
+ requires :token, type: String, desc: 'The unique token of the runner'
+ optional :description, type: String, desc: 'The description of the runner'
+ optional :tag_list, type: Array[String], desc: 'A list of tags the runner should run for'
+ optional :run_untagged, type: Boolean, desc: 'Flag if the runner should execute untagged jobs'
+ optional :locked, type: Boolean, desc: 'Lock this runner for this specific project'
+ end
post "register" do
- required_attributes! [:token]
-
- attributes = attributes_for_keys(
- [:description, :tag_list, :run_untagged, :locked]
- )
+ runner_params = declared(params, include_missing: false)
runner =
if runner_registration_token_valid?
# Create shared runner. Requires admin access
- Ci::Runner.create(attributes.merge(is_shared: true))
- elsif project = Project.find_by(runners_token: params[:token])
+ Ci::Runner.create(runner_params.merge(is_shared: true))
+ elsif project = Project.find_by(runners_token: runner_params[:token])
# Create a specific runner for project.
- project.runners.create(attributes)
+ project.runners.create(runner_params)
end
return forbidden! unless runner
diff --git a/lib/ci/api/triggers.rb b/lib/ci/api/triggers.rb
index 63b42113513..6e622601680 100644
--- a/lib/ci/api/triggers.rb
+++ b/lib/ci/api/triggers.rb
@@ -1,41 +1,30 @@
module Ci
module API
- # Build Trigger API
class Triggers < Grape::API
resource :projects do
- # Trigger a GitLab CI project build
- #
- # Parameters:
- # id (required) - The ID of a CI project
- # ref (required) - The name of project's branch or tag
- # token (required) - The uniq token of trigger
- # Example Request:
- # POST /projects/:id/ref/:ref/trigger
+ desc 'Trigger a GitLab CI project build' do
+ success Entities::TriggerRequest
+ end
+ params do
+ requires :id, type: Integer, desc: 'The ID of a CI project'
+ requires :ref, type: String, desc: "The name of project's branch or tag"
+ requires :token, type: String, desc: 'The unique token of the trigger'
+ optional :variables, type: Hash, desc: 'Optional build variables'
+ end
post ":id/refs/:ref/trigger" do
- required_attributes! [:token]
-
- project = Project.find_by(ci_id: params[:id].to_i)
- trigger = Ci::Trigger.find_by_token(params[:token].to_s)
+ project = Project.find_by(ci_id: params[:id])
+ trigger = Ci::Trigger.find_by_token(params[:token])
not_found! unless project && trigger
unauthorized! unless trigger.project == project
- # validate variables
- variables = params[:variables]
- if variables
- unless variables.is_a?(Hash)
- render_api_error!('variables needs to be a hash', 400)
- end
-
- unless variables.all? { |key, value| key.is_a?(String) && value.is_a?(String) }
- render_api_error!('variables needs to be a map of key-valued strings', 400)
- end
-
- # convert variables from Mash to Hash
- variables = variables.to_h
+ # Validate variables
+ variables = params[:variables].to_h
+ unless variables.all? { |key, value| key.is_a?(String) && value.is_a?(String) }
+ render_api_error!('variables needs to be a map of key-valued strings', 400)
end
# create request and trigger builds
- trigger_request = Ci::CreateTriggerRequestService.new.execute(project, trigger, params[:ref].to_s, variables)
+ trigger_request = Ci::CreateTriggerRequestService.new.execute(project, trigger, params[:ref], variables)
if trigger_request
present trigger_request, with: Entities::TriggerRequest
else
diff --git a/lib/container_registry/client.rb b/lib/container_registry/client.rb
index 2edddb84fc3..7f5f6d9ddb6 100644
--- a/lib/container_registry/client.rb
+++ b/lib/container_registry/client.rb
@@ -5,7 +5,7 @@ module ContainerRegistry
class Client
attr_accessor :uri
- MANIFEST_VERSION = 'application/vnd.docker.distribution.manifest.v2+json'
+ MANIFEST_VERSION = 'application/vnd.docker.distribution.manifest.v2+json'.freeze
# Taken from: FaradayMiddleware::FollowRedirects
REDIRECT_CODES = Set.new [301, 302, 303, 307]
diff --git a/lib/extracts_path.rb b/lib/extracts_path.rb
index 82551f1f222..9ece84cc469 100644
--- a/lib/extracts_path.rb
+++ b/lib/extracts_path.rb
@@ -42,7 +42,7 @@ module ExtractsPath
return pair unless @project
- if id.match(/^([[:alnum:]]{40})(.+)/)
+ if id =~ /^(\h{40})(.+)/
# If the ref appears to be a SHA, we're done, just split the string
pair = $~.captures
else
diff --git a/lib/file_size_validator.rb b/lib/file_size_validator.rb
index 440dd44ece7..eb19ab45ac3 100644
--- a/lib/file_size_validator.rb
+++ b/lib/file_size_validator.rb
@@ -32,9 +32,9 @@ class FileSizeValidator < ActiveModel::EachValidator
end
def validate_each(record, attribute, value)
- raise(ArgumentError, "A CarrierWave::Uploader::Base object was expected") unless value.kind_of? CarrierWave::Uploader::Base
+ raise(ArgumentError, "A CarrierWave::Uploader::Base object was expected") unless value.is_a? CarrierWave::Uploader::Base
- value = (options[:tokenizer] || DEFAULT_TOKENIZER).call(value) if value.kind_of?(String)
+ value = (options[:tokenizer] || DEFAULT_TOKENIZER).call(value) if value.is_a?(String)
CHECKS.each do |key, validity_check|
next unless check_value = options[key]
diff --git a/lib/gitlab/access.rb b/lib/gitlab/access.rb
index 9b484a2ecfd..3b210eeda9d 100644
--- a/lib/gitlab/access.rb
+++ b/lib/gitlab/access.rb
@@ -21,9 +21,7 @@ module Gitlab
PROTECTION_DEV_CAN_MERGE = 3
class << self
- def values
- options.values
- end
+ delegate :values, to: :options
def all_values
options_with_owner.values
diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb
index f638905a1e0..89db6c3da46 100644
--- a/lib/gitlab/auth.rb
+++ b/lib/gitlab/auth.rb
@@ -2,8 +2,8 @@ module Gitlab
module Auth
class MissingPersonalTokenError < StandardError; end
- SCOPES = [:api, :read_user]
- DEFAULT_SCOPES = [:api]
+ SCOPES = [:api, :read_user].freeze
+ DEFAULT_SCOPES = [:api].freeze
OPTIONAL_SCOPES = SCOPES - DEFAULT_SCOPES
class << self
diff --git a/lib/gitlab/award_emoji.rb b/lib/gitlab/award_emoji.rb
index 39b43ab5489..7555326d384 100644
--- a/lib/gitlab/award_emoji.rb
+++ b/lib/gitlab/award_emoji.rb
@@ -69,11 +69,12 @@ module Gitlab
end
JSON.parse(File.read(path)).map do |hash|
- if digest
- fname = "#{hash['unicode']}-#{hash['digest']}"
- else
- fname = hash['unicode']
- end
+ fname =
+ if digest
+ "#{hash['unicode']}-#{hash['digest']}"
+ else
+ hash['unicode']
+ end
{ name: hash['name'], path: File.join(base, prefix, "#{fname}.png") }
end
diff --git a/lib/gitlab/badge/build/template.rb b/lib/gitlab/badge/build/template.rb
index 2b95ddfcb53..bc0e0cd441d 100644
--- a/lib/gitlab/badge/build/template.rb
+++ b/lib/gitlab/badge/build/template.rb
@@ -15,7 +15,7 @@ module Gitlab
canceled: '#9f9f9f',
skipped: '#9f9f9f',
unknown: '#9f9f9f'
- }
+ }.freeze
def initialize(badge)
@entity = badge.entity
diff --git a/lib/gitlab/badge/coverage/template.rb b/lib/gitlab/badge/coverage/template.rb
index 06e0d084e9f..fcecb1d9665 100644
--- a/lib/gitlab/badge/coverage/template.rb
+++ b/lib/gitlab/badge/coverage/template.rb
@@ -13,7 +13,7 @@ module Gitlab
medium: '#dfb317',
low: '#e05d44',
unknown: '#9f9f9f'
- }
+ }.freeze
def initialize(badge)
@entity = badge.entity
diff --git a/lib/gitlab/badge/metadata.rb b/lib/gitlab/badge/metadata.rb
index 548f85b78bb..4a049ef758d 100644
--- a/lib/gitlab/badge/metadata.rb
+++ b/lib/gitlab/badge/metadata.rb
@@ -20,6 +20,10 @@ module Gitlab
"[![#{title}](#{image_url})](#{link_url})"
end
+ def to_asciidoc
+ "image:#{image_url}[link=\"#{link_url}\",title=\"#{title}\"]"
+ end
+
def title
raise NotImplementedError
end
diff --git a/lib/gitlab/changes_list.rb b/lib/gitlab/changes_list.rb
index 95308aca95f..5b32fca00a4 100644
--- a/lib/gitlab/changes_list.rb
+++ b/lib/gitlab/changes_list.rb
@@ -5,7 +5,7 @@ module Gitlab
attr_reader :raw_changes
def initialize(changes)
- @raw_changes = changes.kind_of?(String) ? changes.lines : changes
+ @raw_changes = changes.is_a?(String) ? changes.lines : changes
end
def each(&block)
diff --git a/lib/gitlab/chat_commands/presenters/issue_base.rb b/lib/gitlab/chat_commands/presenters/issue_base.rb
index a0058407fb2..054f7f4be0c 100644
--- a/lib/gitlab/chat_commands/presenters/issue_base.rb
+++ b/lib/gitlab/chat_commands/presenters/issue_base.rb
@@ -32,7 +32,7 @@ module Gitlab
},
{
title: "Labels",
- value: @resource.labels.any? ? @resource.label_names : "_None_",
+ value: @resource.labels.any? ? @resource.label_names.join(', ') : "_None_",
short: true
}
]
diff --git a/lib/gitlab/chat_commands/presenters/issue_new.rb b/lib/gitlab/chat_commands/presenters/issue_new.rb
index 0d31660039a..3674ba25641 100644
--- a/lib/gitlab/chat_commands/presenters/issue_new.rb
+++ b/lib/gitlab/chat_commands/presenters/issue_new.rb
@@ -10,7 +10,7 @@ module Gitlab
private
- def new_issue
+ def new_issue
{
attachments: [
{
@@ -38,7 +38,7 @@ module Gitlab
end
def project_link
- "[#{project.name_with_namespace}](#{projects_url(project)})"
+ "[#{project.name_with_namespace}](#{project.web_url})"
end
def author_profile_link
diff --git a/lib/gitlab/ci/build/artifacts/metadata/entry.rb b/lib/gitlab/ci/build/artifacts/metadata/entry.rb
index 7f4c750b6fd..6f799c2f031 100644
--- a/lib/gitlab/ci/build/artifacts/metadata/entry.rb
+++ b/lib/gitlab/ci/build/artifacts/metadata/entry.rb
@@ -27,6 +27,8 @@ module Gitlab
end
end
+ delegate :empty?, to: :children
+
def directory?
blank_node? || @path.end_with?('/')
end
@@ -91,10 +93,6 @@ module Gitlab
blank_node? || @entries.include?(@path)
end
- def empty?
- children.empty?
- end
-
def total_size
descendant_pattern = %r{^#{Regexp.escape(@path)}}
entries.sum do |path, entry|
diff --git a/lib/gitlab/ci/config/entry/artifacts.rb b/lib/gitlab/ci/config/entry/artifacts.rb
index b756b0d4555..8275aacee9b 100644
--- a/lib/gitlab/ci/config/entry/artifacts.rb
+++ b/lib/gitlab/ci/config/entry/artifacts.rb
@@ -9,7 +9,7 @@ module Gitlab
include Validatable
include Attributable
- ALLOWED_KEYS = %i[name untracked paths when expire_in]
+ ALLOWED_KEYS = %i[name untracked paths when expire_in].freeze
attributes ALLOWED_KEYS
diff --git a/lib/gitlab/ci/config/entry/cache.rb b/lib/gitlab/ci/config/entry/cache.rb
index 7653cab668b..066643ccfcc 100644
--- a/lib/gitlab/ci/config/entry/cache.rb
+++ b/lib/gitlab/ci/config/entry/cache.rb
@@ -8,7 +8,7 @@ module Gitlab
class Cache < Node
include Configurable
- ALLOWED_KEYS = %i[key untracked paths]
+ ALLOWED_KEYS = %i[key untracked paths].freeze
validations do
validates :config, allowed_keys: ALLOWED_KEYS
diff --git a/lib/gitlab/ci/config/entry/environment.rb b/lib/gitlab/ci/config/entry/environment.rb
index f7c530c7d9f..0c1f9eb7cbf 100644
--- a/lib/gitlab/ci/config/entry/environment.rb
+++ b/lib/gitlab/ci/config/entry/environment.rb
@@ -8,7 +8,7 @@ module Gitlab
class Environment < Node
include Validatable
- ALLOWED_KEYS = %i[name url action on_stop]
+ ALLOWED_KEYS = %i[name url action on_stop].freeze
validations do
validate do
@@ -21,12 +21,14 @@ module Gitlab
validates :name,
type: {
with: String,
- message: Gitlab::Regex.environment_name_regex_message }
+ message: Gitlab::Regex.environment_name_regex_message
+ }
validates :name,
format: {
with: Gitlab::Regex.environment_name_regex,
- message: Gitlab::Regex.environment_name_regex_message }
+ message: Gitlab::Regex.environment_name_regex_message
+ }
with_options if: :hash? do
validates :config, allowed_keys: ALLOWED_KEYS
diff --git a/lib/gitlab/ci/config/entry/job.rb b/lib/gitlab/ci/config/entry/job.rb
index 69a5e6f433d..7f7662f2776 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 coverage]
+ after_script variables environment coverage].freeze
validations do
validates :config, allowed_keys: ALLOWED_KEYS
diff --git a/lib/gitlab/conflict/file.rb b/lib/gitlab/conflict/file.rb
index c843315782d..d80bc748209 100644
--- a/lib/gitlab/conflict/file.rb
+++ b/lib/gitlab/conflict/file.rb
@@ -91,11 +91,12 @@ module Gitlab
our_highlight = Gitlab::Highlight.highlight(our_path, our_file, repository: repository).lines
lines.each do |line|
- if line.type == 'old'
- line.rich_text = their_highlight[line.old_line - 1].try(:html_safe)
- else
- line.rich_text = our_highlight[line.new_line - 1].try(:html_safe)
- end
+ line.rich_text =
+ if line.type == 'old'
+ their_highlight[line.old_line - 1].try(:html_safe)
+ else
+ our_highlight[line.new_line - 1].try(:html_safe)
+ end
end
end
diff --git a/lib/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb
index e20f5f6f514..82576d197fe 100644
--- a/lib/gitlab/current_settings.rb
+++ b/lib/gitlab/current_settings.rb
@@ -25,9 +25,7 @@ module Gitlab
settings || in_memory_application_settings
end
- def sidekiq_throttling_enabled?
- current_application_settings.sidekiq_throttling_enabled?
- end
+ delegate :sidekiq_throttling_enabled?, to: :current_application_settings
def in_memory_application_settings
@in_memory_application_settings ||= ::ApplicationSetting.new(::ApplicationSetting.defaults)
diff --git a/lib/gitlab/cycle_analytics/code_stage.rb b/lib/gitlab/cycle_analytics/code_stage.rb
index d1bc2055ba8..1e52b6614a1 100644
--- a/lib/gitlab/cycle_analytics/code_stage.rb
+++ b/lib/gitlab/cycle_analytics/code_stage.rb
@@ -13,6 +13,10 @@ module Gitlab
:code
end
+ def legend
+ "Related Merge Requests"
+ end
+
def description
"Time until first merge request"
end
diff --git a/lib/gitlab/cycle_analytics/issue_stage.rb b/lib/gitlab/cycle_analytics/issue_stage.rb
index d2068fbc38f..213994988a5 100644
--- a/lib/gitlab/cycle_analytics/issue_stage.rb
+++ b/lib/gitlab/cycle_analytics/issue_stage.rb
@@ -14,6 +14,10 @@ module Gitlab
:issue
end
+ def legend
+ "Related Issues"
+ end
+
def description
"Time before an issue gets scheduled"
end
diff --git a/lib/gitlab/cycle_analytics/plan_stage.rb b/lib/gitlab/cycle_analytics/plan_stage.rb
index 3b4dfc6a30e..45d51d30ccc 100644
--- a/lib/gitlab/cycle_analytics/plan_stage.rb
+++ b/lib/gitlab/cycle_analytics/plan_stage.rb
@@ -14,6 +14,10 @@ module Gitlab
:plan
end
+ def legend
+ "Related Commits"
+ end
+
def description
"Time before an issue starts implementation"
end
diff --git a/lib/gitlab/cycle_analytics/production_stage.rb b/lib/gitlab/cycle_analytics/production_stage.rb
index 2a6bcc80116..9f387a02945 100644
--- a/lib/gitlab/cycle_analytics/production_stage.rb
+++ b/lib/gitlab/cycle_analytics/production_stage.rb
@@ -15,6 +15,10 @@ module Gitlab
:production
end
+ def legend
+ "Related Issues"
+ end
+
def description
"From issue creation until deploy to production"
end
diff --git a/lib/gitlab/cycle_analytics/review_stage.rb b/lib/gitlab/cycle_analytics/review_stage.rb
index fbaa3010d81..4744be834de 100644
--- a/lib/gitlab/cycle_analytics/review_stage.rb
+++ b/lib/gitlab/cycle_analytics/review_stage.rb
@@ -13,6 +13,10 @@ module Gitlab
:review
end
+ def legend
+ "Relative Merged Requests"
+ end
+
def description
"Time between merge request creation and merge/close"
end
diff --git a/lib/gitlab/cycle_analytics/staging_stage.rb b/lib/gitlab/cycle_analytics/staging_stage.rb
index 945909a4d62..3cdbe04fbaf 100644
--- a/lib/gitlab/cycle_analytics/staging_stage.rb
+++ b/lib/gitlab/cycle_analytics/staging_stage.rb
@@ -14,6 +14,10 @@ module Gitlab
:staging
end
+ def legend
+ "Relative Deployed Builds"
+ end
+
def description
"From merge request merge until deploy to production"
end
diff --git a/lib/gitlab/cycle_analytics/test_stage.rb b/lib/gitlab/cycle_analytics/test_stage.rb
index 0079d56e0e4..e96943833bc 100644
--- a/lib/gitlab/cycle_analytics/test_stage.rb
+++ b/lib/gitlab/cycle_analytics/test_stage.rb
@@ -13,6 +13,10 @@ module Gitlab
:test
end
+ def legend
+ "Relative Builds Trigger by Commits"
+ end
+
def description
"Total test time for all commits/merges"
end
diff --git a/lib/gitlab/data_builder/build.rb b/lib/gitlab/data_builder/build.rb
index 6548e6475c6..f78106f5b10 100644
--- a/lib/gitlab/data_builder/build.rb
+++ b/lib/gitlab/data_builder/build.rb
@@ -8,6 +8,8 @@ module Gitlab
commit = build.pipeline
user = build.user
+ author_url = build_author_url(build.commit, commit)
+
data = {
object_kind: 'build',
@@ -43,6 +45,7 @@ module Gitlab
message: commit.git_commit_message,
author_name: commit.git_author_name,
author_email: commit.git_author_email,
+ author_url: author_url,
status: commit.status,
duration: commit.duration,
started_at: commit.started_at,
@@ -62,6 +65,13 @@ module Gitlab
data
end
+
+ private
+
+ def build_author_url(commit, pipeline)
+ author = commit.try(:author)
+ author ? Gitlab::Routing.url_helpers.user_url(author) : "mailto:#{pipeline.git_author_email}"
+ end
end
end
end
diff --git a/lib/gitlab/database.rb b/lib/gitlab/database.rb
index a47d7e98a62..d160cadc2d0 100644
--- a/lib/gitlab/database.rb
+++ b/lib/gitlab/database.rb
@@ -79,11 +79,16 @@ module Gitlab
end
end
- def self.create_connection_pool(pool_size)
+ # pool_size - The size of the DB pool.
+ # host - An optional host name to use instead of the default one.
+ def self.create_connection_pool(pool_size, host = nil)
# See activerecord-4.2.7.1/lib/active_record/connection_adapters/connection_specification.rb
env = Rails.env
original_config = ActiveRecord::Base.configurations
+
env_config = original_config[env].merge('pool' => pool_size)
+ env_config['host'] = host if host
+
config = original_config.merge(env => env_config)
spec =
diff --git a/lib/gitlab/database/median.rb b/lib/gitlab/database/median.rb
index 08607c27c09..23890e5f493 100644
--- a/lib/gitlab/database/median.rb
+++ b/lib/gitlab/database/median.rb
@@ -108,6 +108,7 @@ module Gitlab
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/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb
index 4800a509b37..fc445ab9483 100644
--- a/lib/gitlab/database/migration_helpers.rb
+++ b/lib/gitlab/database/migration_helpers.rb
@@ -54,7 +54,7 @@ module Gitlab
disable_statement_timeout
- key_name = "fk_#{source}_#{target}_#{column}"
+ key_name = concurrent_foreign_key_name(source, column)
# Using NOT VALID allows us to create a key without immediately
# validating it. This means we keep the ALTER TABLE lock only for a
@@ -74,6 +74,15 @@ module Gitlab
execute("ALTER TABLE #{source} VALIDATE CONSTRAINT #{key_name};")
end
+ # Returns the name for a concurrent foreign key.
+ #
+ # PostgreSQL constraint names have a limit of 63 bytes. The logic used
+ # here is based on Rails' foreign_key_name() method, which unfortunately
+ # is private so we can't rely on it directly.
+ def concurrent_foreign_key_name(table, column)
+ "fk_#{Digest::SHA256.hexdigest("#{table}_#{column}_fk").first(10)}"
+ end
+
# Long-running migrations may take more than the timeout allowed by
# the database. Disable the session's statement timeout to ensure
# migrations don't get killed prematurely. (PostgreSQL only)
diff --git a/lib/gitlab/diff/highlight.rb b/lib/gitlab/diff/highlight.rb
index 9ea976e18fa..7db896522a9 100644
--- a/lib/gitlab/diff/highlight.rb
+++ b/lib/gitlab/diff/highlight.rb
@@ -50,7 +50,7 @@ module Gitlab
# Only update text if line is found. This will prevent
# issues with submodules given the line only exists in diff content.
if rich_line
- line_prefix = diff_line.text.match(/\A(.)/) ? $1 : ' '
+ line_prefix = diff_line.text =~ /\A(.)/ ? $1 : ' '
"#{line_prefix}#{rich_line}".html_safe
end
end
diff --git a/lib/gitlab/diff/inline_diff_marker.rb b/lib/gitlab/diff/inline_diff_marker.rb
index 87a9b1e23ac..736933b1c4b 100644
--- a/lib/gitlab/diff/inline_diff_marker.rb
+++ b/lib/gitlab/diff/inline_diff_marker.rb
@@ -4,7 +4,7 @@ module Gitlab
MARKDOWN_SYMBOLS = {
addition: "+",
deletion: "-"
- }
+ }.freeze
attr_accessor :raw_line, :rich_line
diff --git a/lib/gitlab/diff/parser.rb b/lib/gitlab/diff/parser.rb
index 89320f5d9dc..8f844224a7a 100644
--- a/lib/gitlab/diff/parser.rb
+++ b/lib/gitlab/diff/parser.rb
@@ -20,7 +20,7 @@ module Gitlab
full_line = line.delete("\n")
- if line.match(/^@@ -/)
+ if line =~ /^@@ -/
type = "match"
line_old = line.match(/\-[0-9]*/)[0].to_i.abs rescue 0
diff --git a/lib/gitlab/diff/position.rb b/lib/gitlab/diff/position.rb
index ecf62dead35..fc728123c97 100644
--- a/lib/gitlab/diff/position.rb
+++ b/lib/gitlab/diff/position.rb
@@ -140,15 +140,16 @@ module Gitlab
def find_diff_file(repository)
# We're at the initial commit, so just get that as we can't compare to anything.
- if Gitlab::Git.blank_ref?(start_sha)
- compare = Gitlab::Git::Commit.find(repository.raw_repository, head_sha)
- else
- compare = Gitlab::Git::Compare.new(
- repository.raw_repository,
- start_sha,
- head_sha
- )
- end
+ compare =
+ if Gitlab::Git.blank_ref?(start_sha)
+ Gitlab::Git::Commit.find(repository.raw_repository, head_sha)
+ else
+ Gitlab::Git::Compare.new(
+ repository.raw_repository,
+ start_sha,
+ head_sha
+ )
+ end
diff = compare.diffs(paths: paths).first
diff --git a/lib/gitlab/downtime_check/message.rb b/lib/gitlab/downtime_check/message.rb
index 40a4815a9a0..543e62794c5 100644
--- a/lib/gitlab/downtime_check/message.rb
+++ b/lib/gitlab/downtime_check/message.rb
@@ -3,8 +3,8 @@ module Gitlab
class Message
attr_reader :path, :offline
- OFFLINE = "\e[31moffline\e[0m"
- ONLINE = "\e[32monline\e[0m"
+ OFFLINE = "\e[31moffline\e[0m".freeze
+ ONLINE = "\e[32monline\e[0m".freeze
# path - The file path of the migration.
# offline - When set to `true` the migration will require downtime.
diff --git a/lib/gitlab/ee_compat_check.rb b/lib/gitlab/ee_compat_check.rb
index c8e36d8ff4a..e0fdf3f3d64 100644
--- a/lib/gitlab/ee_compat_check.rb
+++ b/lib/gitlab/ee_compat_check.rb
@@ -119,7 +119,7 @@ module Gitlab
step("Reseting to latest master", %w[git reset --hard origin/master])
step("Checking if #{patch_path} applies cleanly to EE/master")
- output, status = Gitlab::Popen.popen(%W[git apply --check #{patch_path}])
+ output, status = Gitlab::Popen.popen(%W[git apply --check --3way #{patch_path}])
unless status.zero?
failed_files = output.lines.reduce([]) do |memo, line|
diff --git a/lib/gitlab/email/handler.rb b/lib/gitlab/email/handler.rb
index bd2f5d3615e..35ea2e0ef59 100644
--- a/lib/gitlab/email/handler.rb
+++ b/lib/gitlab/email/handler.rb
@@ -5,7 +5,7 @@ require 'gitlab/email/handler/unsubscribe_handler'
module Gitlab
module Email
module Handler
- HANDLERS = [UnsubscribeHandler, CreateNoteHandler, CreateIssueHandler]
+ HANDLERS = [UnsubscribeHandler, CreateNoteHandler, CreateIssueHandler].freeze
def self.for(mail, mail_key)
HANDLERS.find do |klass|
diff --git a/lib/gitlab/email/reply_parser.rb b/lib/gitlab/email/reply_parser.rb
index 8c8dd1b9cef..558df87f36d 100644
--- a/lib/gitlab/email/reply_parser.rb
+++ b/lib/gitlab/email/reply_parser.rb
@@ -31,11 +31,12 @@ module Gitlab
private
def select_body(message)
- if message.multipart?
- part = message.text_part || message.html_part || message
- else
- part = message
- end
+ part =
+ if message.multipart?
+ message.text_part || message.html_part || message
+ else
+ message
+ end
decoded = fix_charset(part)
diff --git a/lib/gitlab/exclusive_lease.rb b/lib/gitlab/exclusive_lease.rb
index 2dd42704396..62ddd45785d 100644
--- a/lib/gitlab/exclusive_lease.rb
+++ b/lib/gitlab/exclusive_lease.rb
@@ -10,7 +10,7 @@ module Gitlab
# ExclusiveLease.
#
class ExclusiveLease
- LUA_CANCEL_SCRIPT = <<-EOS
+ LUA_CANCEL_SCRIPT = <<-EOS.freeze
local key, uuid = KEYS[1], ARGV[1]
if redis.call("get", key) == uuid then
redis.call("del", key)
diff --git a/lib/gitlab/file_detector.rb b/lib/gitlab/file_detector.rb
index 1d93a67dc56..c9ca4cadd1c 100644
--- a/lib/gitlab/file_detector.rb
+++ b/lib/gitlab/file_detector.rb
@@ -14,7 +14,7 @@ module Gitlab
koding: '.koding.yml',
gitlab_ci: '.gitlab-ci.yml',
avatar: /\Alogo\.(png|jpg|gif)\z/
- }
+ }.freeze
# Returns an Array of file types based on the given paths.
#
diff --git a/lib/gitlab/git/blob.rb b/lib/gitlab/git/blob.rb
index b742d9e1e4b..e56eb0d3beb 100644
--- a/lib/gitlab/git/blob.rb
+++ b/lib/gitlab/git/blob.rb
@@ -93,163 +93,6 @@ module Gitlab
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)
diff --git a/lib/gitlab/git/commit.rb b/lib/gitlab/git/commit.rb
index d785516ebdd..3a73697dc5d 100644
--- a/lib/gitlab/git/commit.rb
+++ b/lib/gitlab/git/commit.rb
@@ -14,6 +14,8 @@ module Gitlab
attr_accessor *SERIALIZE_KEYS # rubocop:disable Lint/AmbiguousOperator
+ delegate :tree, to: :raw_commit
+
def ==(other)
return false unless other.is_a?(Gitlab::Git::Commit)
@@ -218,10 +220,6 @@ module Gitlab
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
diff --git a/lib/gitlab/git/index.rb b/lib/gitlab/git/index.rb
new file mode 100644
index 00000000000..af1744c9c46
--- /dev/null
+++ b/lib/gitlab/git/index.rb
@@ -0,0 +1,126 @@
+module Gitlab
+ module Git
+ class Index
+ DEFAULT_MODE = 0o100644
+
+ attr_reader :repository, :raw_index
+
+ def initialize(repository)
+ @repository = repository
+ @raw_index = repository.rugged.index
+ end
+
+ delegate :read_tree, :get, to: :raw_index
+
+ def write_tree
+ raw_index.write_tree(repository.rugged)
+ end
+
+ def dir_exists?(path)
+ raw_index.find { |entry| entry[:path].start_with?("#{path}/") }
+ end
+
+ def create(options)
+ options = normalize_options(options)
+
+ file_entry = get(options[:file_path])
+ if file_entry
+ raise Gitlab::Git::Repository::InvalidBlobName.new("Filename already exists")
+ end
+
+ add_blob(options)
+ end
+
+ def create_dir(options)
+ options = normalize_options(options)
+
+ file_entry = get(options[:file_path])
+ if file_entry
+ raise Gitlab::Git::Repository::InvalidBlobName.new("Directory already exists as a file")
+ end
+
+ if dir_exists?(options[:file_path])
+ raise Gitlab::Git::Repository::InvalidBlobName.new("Directory already exists")
+ end
+
+ options = options.dup
+ options[:file_path] += '/.gitkeep'
+ options[:content] = ''
+
+ add_blob(options)
+ end
+
+ def update(options)
+ options = normalize_options(options)
+
+ file_entry = get(options[:file_path])
+ unless file_entry
+ raise Gitlab::Git::Repository::InvalidBlobName.new("File doesn't exist")
+ end
+
+ add_blob(options, mode: file_entry[:mode])
+ end
+
+ def move(options)
+ options = normalize_options(options)
+
+ file_entry = get(options[:previous_path])
+ unless file_entry
+ raise Gitlab::Git::Repository::InvalidBlobName.new("File doesn't exist")
+ end
+
+ raw_index.remove(options[:previous_path])
+
+ add_blob(options, mode: file_entry[:mode])
+ end
+
+ def delete(options)
+ options = normalize_options(options)
+
+ file_entry = get(options[:file_path])
+ unless file_entry
+ raise Gitlab::Git::Repository::InvalidBlobName.new("File doesn't exist")
+ end
+
+ raw_index.remove(options[:file_path])
+ end
+
+ private
+
+ def normalize_options(options)
+ options = options.dup
+ options[:file_path] = normalize_path(options[:file_path]) if options[:file_path]
+ options[:previous_path] = normalize_path(options[:previous_path]) if options[:previous_path]
+ options
+ end
+
+ def normalize_path(path)
+ pathname = Gitlab::Git::PathHelper.normalize_path(path.dup)
+
+ if pathname.each_filename.include?('..')
+ raise Gitlab::Git::Repository::InvalidBlobName.new('Invalid path')
+ end
+
+ pathname.to_s
+ end
+
+ def add_blob(options, mode: nil)
+ content = options[:content]
+ content = Base64.decode64(content) if options[:encoding] == 'base64'
+
+ detect = CharlockHolmes::EncodingDetector.new.detect(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 = repository.rugged.write(content, :blob)
+
+ raw_index.add(path: options[:file_path], oid: oid, mode: mode || DEFAULT_MODE)
+ rescue Rugged::IndexError => e
+ raise Gitlab::Git::Repository::InvalidBlobName.new(e.message)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index 7068e68a855..8ec90885231 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -1,5 +1,4 @@
# Gitlab::Git::Repository is a wrapper around native Rugged::Repository object
-require 'forwardable'
require 'tempfile'
require 'forwardable'
require "rubygems/package"
@@ -7,7 +6,6 @@ require "rubygems/package"
module Gitlab
module Git
class Repository
- extend Forwardable
include Gitlab::Git::Popen
SEARCH_CONTEXT_LINES = 3
@@ -33,6 +31,10 @@ module Gitlab
@attributes = Gitlab::Git::Attributes.new(path)
end
+ delegate :empty?,
+ :bare?,
+ to: :rugged
+
# Default branch in the repository
def root_ref
@root_ref ||= discover_default_branch
@@ -162,14 +164,6 @@ module Gitlab
!empty?
end
- def empty?
- rugged.empty?
- end
-
- def bare?
- rugged.bare?
- end
-
def repo_exists?
!!rugged
end
@@ -330,24 +324,30 @@ module Gitlab
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
+ limit = options[:limit].to_i
+ offset = options[:offset].to_i
+ use_follow_flag = options[:follow] && options[:path].present?
- log.is_a?(Array) ? log : []
+ # We will perform the offset in Ruby because --follow doesn't play well with --skip.
+ # See: https://gitlab.com/gitlab-org/gitlab-ce/issues/3574#note_3040520
+ offset_in_ruby = use_follow_flag && options[:offset].present?
+ limit += offset if offset_in_ruby
+
+ cmd = %W[#{Gitlab.config.git.bin_path} --git-dir=#{path} log]
+ cmd << "--max-count=#{limit}"
+ cmd << '--format=%H'
+ cmd << "--skip=#{offset}" unless offset_in_ruby
+ cmd << '--follow' if use_follow_flag
+ cmd << '--no-merges' if options[:skip_merges]
+ cmd << "--after=#{options[:after].iso8601}" if options[:after]
+ cmd << "--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 }
+ lines = offset_in_ruby ? raw_output.lines.drop(offset) : raw_output.lines
+
+ lines.map! { |c| Rugged::Commit.new(rugged, c.strip) }
end
def sha_from_ref(ref)
@@ -565,9 +565,7 @@ module Gitlab
# 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
+ delegate :reset, to: :rugged
# Mimic the `git clean` command and recursively delete untracked files.
# Valid keys that can be passed in the +options+ hash are:
@@ -845,57 +843,6 @@ module Gitlab
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.
diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb
index 7e1484613f2..ffb178334bc 100644
--- a/lib/gitlab/git_access.rb
+++ b/lib/gitlab/git_access.rb
@@ -10,10 +10,10 @@ module Gitlab
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.'
- }
+ }.freeze
- DOWNLOAD_COMMANDS = %w{ git-upload-pack git-upload-archive }
- PUSH_COMMANDS = %w{ git-receive-pack }
+ DOWNLOAD_COMMANDS = %w{ git-upload-pack git-upload-archive }.freeze
+ PUSH_COMMANDS = %w{ git-receive-pack }.freeze
ALL_COMMANDS = DOWNLOAD_COMMANDS + PUSH_COMMANDS
attr_reader :actor, :project, :protocol, :user_access, :authentication_abilities
diff --git a/lib/gitlab/github_import/base_formatter.rb b/lib/gitlab/github_import/base_formatter.rb
index 95dba9a327b..8c80791e7c9 100644
--- a/lib/gitlab/github_import/base_formatter.rb
+++ b/lib/gitlab/github_import/base_formatter.rb
@@ -1,11 +1,12 @@
module Gitlab
module GithubImport
class BaseFormatter
- attr_reader :formatter, :project, :raw_data
+ attr_reader :client, :formatter, :project, :raw_data
- def initialize(project, raw_data)
+ def initialize(project, raw_data, client = nil)
@project = project
@raw_data = raw_data
+ @client = client
@formatter = Gitlab::ImportFormatter.new
end
@@ -18,19 +19,6 @@ module Gitlab
def url
raw_data.url || ''
end
-
- private
-
- def gitlab_user_id(github_id)
- User.joins(:identities).
- find_by("identities.extern_uid = ? AND identities.provider = 'github'", github_id.to_s).
- try(:id)
- end
-
- def gitlab_author_id
- return @gitlab_author_id if defined?(@gitlab_author_id)
- @gitlab_author_id = gitlab_user_id(raw_data.user.id)
- end
end
end
end
diff --git a/lib/gitlab/github_import/client.rb b/lib/gitlab/github_import/client.rb
index ba869faa92e..7dbeec5b010 100644
--- a/lib/gitlab/github_import/client.rb
+++ b/lib/gitlab/github_import/client.rb
@@ -10,6 +10,7 @@ module Gitlab
@access_token = access_token
@host = host.to_s.sub(%r{/+\z}, '')
@api_version = api_version
+ @users = {}
if access_token
::Octokit.auto_paginate = false
@@ -64,6 +65,13 @@ module Gitlab
api.respond_to?(method) || super
end
+ def user(login)
+ return nil unless login.present?
+ return @users[login] if @users.key?(login)
+
+ @users[login] = api.user(login)
+ end
+
private
def api_endpoint
diff --git a/lib/gitlab/github_import/comment_formatter.rb b/lib/gitlab/github_import/comment_formatter.rb
index 2bddcde2b7c..e21922070c1 100644
--- a/lib/gitlab/github_import/comment_formatter.rb
+++ b/lib/gitlab/github_import/comment_formatter.rb
@@ -1,6 +1,8 @@
module Gitlab
module GithubImport
class CommentFormatter < BaseFormatter
+ attr_writer :author_id
+
def attributes
{
project: project,
@@ -17,11 +19,11 @@ module Gitlab
private
def author
- raw_data.user.login
+ @author ||= UserFormatter.new(client, raw_data.user)
end
def author_id
- gitlab_author_id || project.creator_id
+ author.gitlab_id || project.creator_id
end
def body
@@ -52,10 +54,10 @@ module Gitlab
end
def note
- if gitlab_author_id
+ if author.gitlab_id
body
else
- formatter.author_line(author) + body
+ formatter.author_line(author.login) + body
end
end
diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb
index 9a4ffd28438..dc73cad93a5 100644
--- a/lib/gitlab/github_import/importer.rb
+++ b/lib/gitlab/github_import/importer.rb
@@ -110,7 +110,7 @@ module Gitlab
def import_issues
fetch_resources(:issues, repo, state: :all, sort: :created, direction: :asc, per_page: 100) do |issues|
issues.each do |raw|
- gh_issue = IssueFormatter.new(project, raw)
+ gh_issue = IssueFormatter.new(project, raw, client)
begin
issuable =
@@ -131,7 +131,8 @@ module Gitlab
def import_pull_requests
fetch_resources(:pull_requests, repo, state: :all, sort: :created, direction: :asc, per_page: 100) do |pull_requests|
pull_requests.each do |raw|
- gh_pull_request = PullRequestFormatter.new(project, raw)
+ gh_pull_request = PullRequestFormatter.new(project, raw, client)
+
next unless gh_pull_request.valid?
begin
@@ -209,14 +210,16 @@ module Gitlab
ActiveRecord::Base.no_touching do
comments.each do |raw|
begin
- comment = CommentFormatter.new(project, raw)
+ comment = CommentFormatter.new(project, raw, client)
+
# GH does not return info about comment's parent, so we guess it by checking its URL!
*_, parent, iid = URI(raw.html_url).path.split('/')
- if parent == 'issues'
- issuable = Issue.find_by(project_id: project.id, iid: iid)
- else
- issuable = MergeRequest.find_by(target_project_id: project.id, iid: iid)
- end
+
+ issuable = if parent == 'issues'
+ Issue.find_by(project_id: project.id, iid: iid)
+ else
+ MergeRequest.find_by(target_project_id: project.id, iid: iid)
+ end
next unless issuable
@@ -282,7 +285,7 @@ module Gitlab
def fetch_resources(resource_type, *opts)
return if imported?(resource_type)
- opts.last.merge!(page: current_page(resource_type))
+ opts.last[:page] = current_page(resource_type)
client.public_send(resource_type, *opts) do |resources|
yield resources
diff --git a/lib/gitlab/github_import/issuable_formatter.rb b/lib/gitlab/github_import/issuable_formatter.rb
index 256f360efc7..27b171d6ddb 100644
--- a/lib/gitlab/github_import/issuable_formatter.rb
+++ b/lib/gitlab/github_import/issuable_formatter.rb
@@ -1,13 +1,13 @@
module Gitlab
module GithubImport
class IssuableFormatter < BaseFormatter
+ attr_writer :assignee_id, :author_id
+
def project_association
raise NotImplementedError
end
- def number
- raw_data.number
- end
+ delegate :number, to: :raw_data
def find_condition
{ iid: number }
@@ -23,18 +23,24 @@ module Gitlab
raw_data.assignee.present?
end
- def assignee_id
+ def author
+ @author ||= UserFormatter.new(client, raw_data.user)
+ end
+
+ def author_id
+ @author_id ||= author.gitlab_id || project.creator_id
+ end
+
+ def assignee
if assigned?
- gitlab_user_id(raw_data.assignee.id)
+ @assignee ||= UserFormatter.new(client, raw_data.assignee)
end
end
- def author
- raw_data.user.login
- end
+ def assignee_id
+ return @assignee_id if defined?(@assignee_id)
- def author_id
- gitlab_author_id || project.creator_id
+ @assignee_id = assignee.try(:gitlab_id)
end
def body
@@ -42,10 +48,10 @@ module Gitlab
end
def description
- if gitlab_author_id
+ if author.gitlab_id
body
else
- formatter.author_line(author) + body
+ formatter.author_line(author.login) + body
end
end
diff --git a/lib/gitlab/github_import/user_formatter.rb b/lib/gitlab/github_import/user_formatter.rb
new file mode 100644
index 00000000000..04c2964da20
--- /dev/null
+++ b/lib/gitlab/github_import/user_formatter.rb
@@ -0,0 +1,45 @@
+module Gitlab
+ module GithubImport
+ class UserFormatter
+ attr_reader :client, :raw
+
+ delegate :id, :login, to: :raw, allow_nil: true
+
+ def initialize(client, raw)
+ @client = client
+ @raw = raw
+ end
+
+ def gitlab_id
+ return @gitlab_id if defined?(@gitlab_id)
+
+ @gitlab_id = find_by_external_uid || find_by_email
+ end
+
+ private
+
+ def email
+ @email ||= client.user(raw.login).try(:email)
+ end
+
+ def find_by_email
+ return nil unless email
+
+ User.find_by_any_email(email)
+ .try(:id)
+ end
+
+ def find_by_external_uid
+ return nil unless id
+
+ identities = ::Identity.arel_table
+
+ User.select(:id)
+ .joins(:identities).where(identities[:provider].eq(:github)
+ .and(identities[:extern_uid].eq(id)))
+ .first
+ .try(:id)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb
index b8a5ac907a4..101b1b80c1e 100644
--- a/lib/gitlab/gon_helper.rb
+++ b/lib/gitlab/gon_helper.rb
@@ -2,7 +2,7 @@ module Gitlab
module GonHelper
def add_gon_variables
gon.api_version = API::API.version
- gon.default_avatar_url = URI::join(Gitlab.config.gitlab.url, ActionController::Base.helpers.image_path('no_avatar.png')).to_s
+ gon.default_avatar_url = URI.join(Gitlab.config.gitlab.url, ActionController::Base.helpers.image_path('no_avatar.png')).to_s
gon.max_file_size = current_application_settings.max_attachment_size
gon.relative_url_root = Gitlab.config.gitlab.relative_url_root
gon.shortcuts_path = help_page_path('shortcuts')
diff --git a/lib/gitlab/import_export.rb b/lib/gitlab/import_export.rb
index a46a41bc56e..8b327cfc226 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.6'
+ VERSION = '0.1.6'.freeze
FILENAME_LIMIT = 50
def export_path(relative_path:)
@@ -35,7 +35,7 @@ module Gitlab
end
def export_filename(project:)
- basename = "#{Time.now.strftime('%Y-%m-%d_%H-%M-%3N')}_#{project.namespace.full_path}_#{project.path}"
+ basename = "#{Time.now.strftime('%Y-%m-%d_%H-%M-%3N')}_#{project.full_path.tr('/', '_')}"
"#{basename[0..FILENAME_LIMIT]}_export.tar.gz"
end
diff --git a/lib/gitlab/import_export/command_line_util.rb b/lib/gitlab/import_export/command_line_util.rb
index f00c7460e82..90942774a2e 100644
--- a/lib/gitlab/import_export/command_line_util.rb
+++ b/lib/gitlab/import_export/command_line_util.rb
@@ -15,14 +15,6 @@ module Gitlab
execute(%W(#{git_bin_path} --git-dir=#{repo_path} bundle create #{bundle_path} --all))
end
- def git_unbundle(repo_path:, bundle_path:)
- execute(%W(#{git_bin_path} clone --bare #{bundle_path} #{repo_path}))
- end
-
- def git_restore_hooks
- execute(%W(#{Gitlab.config.gitlab_shell.path}/bin/create-hooks) + repository_storage_paths_args)
- end
-
def mkdir_p(path)
FileUtils.mkdir_p(path, mode: DEFAULT_MODE)
FileUtils.chmod(DEFAULT_MODE, path)
@@ -56,10 +48,6 @@ module Gitlab
FileUtils.copy_entry(source, destination)
true
end
-
- def repository_storage_paths_args
- Gitlab.config.repositories.storages.values
- end
end
end
end
diff --git a/lib/gitlab/import_export/repo_restorer.rb b/lib/gitlab/import_export/repo_restorer.rb
index 48a9a6fa5e2..c824d3ea9fc 100644
--- a/lib/gitlab/import_export/repo_restorer.rb
+++ b/lib/gitlab/import_export/repo_restorer.rb
@@ -2,6 +2,7 @@ module Gitlab
module ImportExport
class RepoRestorer
include Gitlab::ImportExport::CommandLineUtil
+ include Gitlab::ShellAdapter
def initialize(project:, shared:, path_to_bundle:)
@project = project
@@ -12,29 +13,11 @@ module Gitlab
def restore
return true unless File.exist?(@path_to_bundle)
- mkdir_p(path_to_repo)
-
- git_unbundle(repo_path: path_to_repo, bundle_path: @path_to_bundle) && repo_restore_hooks
+ gitlab_shell.import_repository(@project.repository_storage_path, @project.path_with_namespace, @path_to_bundle)
rescue => e
@shared.error(e)
false
end
-
- private
-
- def path_to_repo
- @project.repository.path_to_repo
- end
-
- def repo_restore_hooks
- return true if wiki?
-
- git_restore_hooks
- end
-
- def wiki?
- @project.class.name == 'ProjectWiki'
- end
end
end
end
diff --git a/lib/gitlab/ldap/person.rb b/lib/gitlab/ldap/person.rb
index 7084fd1767d..43eb73250b7 100644
--- a/lib/gitlab/ldap/person.rb
+++ b/lib/gitlab/ldap/person.rb
@@ -43,9 +43,7 @@ module Gitlab
attribute_value(:email)
end
- def dn
- entry.dn
- end
+ delegate :dn, to: :entry
private
diff --git a/lib/gitlab/metrics/instrumentation.rb b/lib/gitlab/metrics/instrumentation.rb
index 4b7a791e497..6aa38542cb4 100644
--- a/lib/gitlab/metrics/instrumentation.rb
+++ b/lib/gitlab/metrics/instrumentation.rb
@@ -143,11 +143,12 @@ module Gitlab
# signature this would break things. As a result we'll make sure the
# generated method _only_ accepts regular arguments if the underlying
# method also accepts them.
- if method.arity == 0
- args_signature = ''
- else
- args_signature = '*args'
- end
+ args_signature =
+ if method.arity == 0
+ ''
+ else
+ '*args'
+ end
proxy_module.class_eval <<-EOF, __FILE__, __LINE__ + 1
def #{name}(#{args_signature})
diff --git a/lib/gitlab/metrics/rack_middleware.rb b/lib/gitlab/metrics/rack_middleware.rb
index 47f88727fc8..adc0db1a874 100644
--- a/lib/gitlab/metrics/rack_middleware.rb
+++ b/lib/gitlab/metrics/rack_middleware.rb
@@ -2,8 +2,8 @@ module Gitlab
module Metrics
# Rack middleware for tracking Rails and Grape requests.
class RackMiddleware
- CONTROLLER_KEY = 'action_controller.instance'
- ENDPOINT_KEY = 'api.endpoint'
+ CONTROLLER_KEY = 'action_controller.instance'.freeze
+ ENDPOINT_KEY = 'api.endpoint'.freeze
CONTENT_TYPES = {
'text/html' => :html,
'text/plain' => :txt,
@@ -14,7 +14,7 @@ module Gitlab
'image/jpeg' => :jpeg,
'image/gif' => :gif,
'image/svg+xml' => :svg
- }
+ }.freeze
def initialize(app)
@app = app
diff --git a/lib/gitlab/metrics/subscribers/action_view.rb b/lib/gitlab/metrics/subscribers/action_view.rb
index 2e9dd4645e3..d435a33e9c7 100644
--- a/lib/gitlab/metrics/subscribers/action_view.rb
+++ b/lib/gitlab/metrics/subscribers/action_view.rb
@@ -5,7 +5,7 @@ module Gitlab
class ActionView < ActiveSupport::Subscriber
attach_to :action_view
- SERIES = 'views'
+ SERIES = 'views'.freeze
def render_template(event)
track(event) if current_transaction
diff --git a/lib/gitlab/metrics/system.rb b/lib/gitlab/metrics/system.rb
index 287b7a83547..3aaebb3e9c3 100644
--- a/lib/gitlab/metrics/system.rb
+++ b/lib/gitlab/metrics/system.rb
@@ -11,7 +11,7 @@ module Gitlab
mem = 0
match = File.read('/proc/self/status').match(/VmRSS:\s+(\d+)/)
- if match and match[1]
+ if match && match[1]
mem = match[1].to_f * 1024
end
diff --git a/lib/gitlab/metrics/transaction.rb b/lib/gitlab/metrics/transaction.rb
index 7bc16181be6..4f9fb1c7853 100644
--- a/lib/gitlab/metrics/transaction.rb
+++ b/lib/gitlab/metrics/transaction.rb
@@ -5,7 +5,7 @@ module Gitlab
THREAD_KEY = :_gitlab_metrics_transaction
# The series to store events (e.g. Git pushes) in.
- EVENT_SERIES = 'events'
+ EVENT_SERIES = 'events'.freeze
attr_reader :tags, :values, :method, :metrics
diff --git a/lib/gitlab/middleware/multipart.rb b/lib/gitlab/middleware/multipart.rb
index dd99f9bb7d7..fee741b47be 100644
--- a/lib/gitlab/middleware/multipart.rb
+++ b/lib/gitlab/middleware/multipart.rb
@@ -26,7 +26,7 @@
module Gitlab
module Middleware
class Multipart
- RACK_ENV_KEY = 'HTTP_GITLAB_WORKHORSE_MULTIPART_FIELDS'
+ RACK_ENV_KEY = 'HTTP_GITLAB_WORKHORSE_MULTIPART_FIELDS'.freeze
class Handler
def initialize(env, message)
diff --git a/lib/gitlab/middleware/webpack_proxy.rb b/lib/gitlab/middleware/webpack_proxy.rb
index 3fe32adeade..6105d165810 100644
--- a/lib/gitlab/middleware/webpack_proxy.rb
+++ b/lib/gitlab/middleware/webpack_proxy.rb
@@ -8,16 +8,16 @@ module Gitlab
@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)
+
+ super(app, backend: "http://#{@proxy_host}:#{@proxy_port}", **opts)
end
def perform_request(env)
- unless @proxy_path && env['PATH_INFO'].start_with?("/#{@proxy_path}")
- return @app.call(env)
+ if @proxy_path && env['PATH_INFO'].start_with?("/#{@proxy_path}")
+ super(env)
+ else
+ @app.call(env)
end
-
- env['HTTP_HOST'] = "#{@proxy_host}:#{@proxy_port}"
- super(env)
end
end
end
diff --git a/lib/gitlab/o_auth/user.rb b/lib/gitlab/o_auth/user.rb
index 96ed20af918..95d2f559588 100644
--- a/lib/gitlab/o_auth/user.rb
+++ b/lib/gitlab/o_auth/user.rb
@@ -29,12 +29,11 @@ module Gitlab
def save(provider = 'OAuth')
unauthorized_to_create unless gl_user
- if needs_blocking?
- gl_user.save!
- gl_user.block
- else
- gl_user.save!
- end
+ block_after_save = needs_blocking?
+
+ gl_user.save!
+
+ gl_user.block if block_after_save
log.info "(#{provider}) saving user #{auth_hash.email} from login with extern_uid => #{auth_hash.uid}"
gl_user
diff --git a/lib/gitlab/optimistic_locking.rb b/lib/gitlab/optimistic_locking.rb
index 879d46446b3..962ff4d3985 100644
--- a/lib/gitlab/optimistic_locking.rb
+++ b/lib/gitlab/optimistic_locking.rb
@@ -1,12 +1,12 @@
module Gitlab
module OptimisticLocking
- extend self
+ module_function
def retry_lock(subject, retries = 100, &block)
loop do
begin
ActiveRecord::Base.transaction do
- return block.call(subject)
+ return yield(subject)
end
rescue ActiveRecord::StaleObjectError
retries -= 1
@@ -15,5 +15,7 @@ module Gitlab
end
end
end
+
+ alias_method :retry_optimistic_lock, :retry_lock
end
end
diff --git a/lib/gitlab/redis.rb b/lib/gitlab/redis.rb
index 9384102acec..62dbd429156 100644
--- a/lib/gitlab/redis.rb
+++ b/lib/gitlab/redis.rb
@@ -4,24 +4,15 @@ require 'active_support/core_ext/hash/keys'
module Gitlab
class Redis
- CACHE_NAMESPACE = 'cache:gitlab'
- SESSION_NAMESPACE = 'session:gitlab'
- SIDEKIQ_NAMESPACE = 'resque:gitlab'
- MAILROOM_NAMESPACE = 'mail_room:gitlab'
- DEFAULT_REDIS_URL = 'redis://localhost:6379'
+ CACHE_NAMESPACE = 'cache:gitlab'.freeze
+ SESSION_NAMESPACE = 'session:gitlab'.freeze
+ SIDEKIQ_NAMESPACE = 'resque:gitlab'.freeze
+ MAILROOM_NAMESPACE = 'mail_room:gitlab'.freeze
+ DEFAULT_REDIS_URL = 'redis://localhost:6379'.freeze
CONFIG_FILE = File.expand_path('../../config/resque.yml', __dir__)
class << self
- # Do NOT cache in an instance variable. Result may be mutated by caller.
- def params
- new.params
- end
-
- # Do NOT cache in an instance variable. Result may be mutated by caller.
- # @deprecated Use .params instead to get sentinel support
- def url
- new.url
- end
+ delegate :params, :url, to: :new
def with
@pool ||= ConnectionPool.new(size: pool_size) { ::Redis.new(params) }
diff --git a/lib/gitlab/reference_extractor.rb b/lib/gitlab/reference_extractor.rb
index 437a339dd2b..7668ecacc4b 100644
--- a/lib/gitlab/reference_extractor.rb
+++ b/lib/gitlab/reference_extractor.rb
@@ -1,7 +1,7 @@
module Gitlab
# Extract possible GFM references from an arbitrary String for further processing.
class ReferenceExtractor < Banzai::ReferenceExtractor
- REFERABLES = %i(user issue label milestone merge_request snippet commit commit_range directly_addressed_user)
+ REFERABLES = %i(user issue label milestone merge_request snippet commit commit_range directly_addressed_user).freeze
attr_accessor :project, :current_user, :author
def initialize(project, current_user = nil)
diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb
index c77fe2d8bdc..5e5f5ff1589 100644
--- a/lib/gitlab/regex.rb
+++ b/lib/gitlab/regex.rb
@@ -5,17 +5,18 @@ module Gitlab
# The namespace regex is used in Javascript to validate usernames in the "Register" form. However, Javascript
# does not support the negative lookbehind assertion (?<!) that disallows usernames ending in `.git` and `.atom`.
# Since this is a non-trivial problem to solve in Javascript (heavily complicate the regex, modify view code to
- # allow non-regex validatiions, etc), `NAMESPACE_REGEX_STR_SIMPLE` serves as a Javascript-compatible version of
+ # allow non-regex validatiions, etc), `NAMESPACE_REGEX_STR_JS` serves as a Javascript-compatible version of
# `NAMESPACE_REGEX_STR`, with the negative lookbehind assertion removed. This means that the client-side validation
# will pass for usernames ending in `.atom` and `.git`, but will be caught by the server-side validation.
PATH_REGEX_STR = '[a-zA-Z0-9_\.][a-zA-Z0-9_\-\.]*'.freeze
- NAMESPACE_REGEX_STR_SIMPLE = PATH_REGEX_STR + '[a-zA-Z0-9_\-]|[a-zA-Z0-9_]'.freeze
- NAMESPACE_REGEX_STR = '(?:' + NAMESPACE_REGEX_STR_SIMPLE + ')(?<!\.git|\.atom)'.freeze
- PROJECT_REGEX_STR = PATH_REGEX_STR + '(?<!\.git|\.atom)'.freeze
+ NAMESPACE_REGEX_STR_JS = PATH_REGEX_STR + '[a-zA-Z0-9_\-]|[a-zA-Z0-9_]'.freeze
+ NO_SUFFIX_REGEX_STR = '(?<!\.git|\.atom)'.freeze
+ NAMESPACE_REGEX_STR = "(?:#{NAMESPACE_REGEX_STR_JS})#{NO_SUFFIX_REGEX_STR}".freeze
+ PROJECT_REGEX_STR = "(?:#{PATH_REGEX_STR})#{NO_SUFFIX_REGEX_STR}".freeze
# Same as NAMESPACE_REGEX_STR but allows `/` in the path.
# So `group/subgroup` will match this regex but not NAMESPACE_REGEX_STR
- NAMESPACE_REF_REGEX_STR = '(?:[a-zA-Z0-9_\.][a-zA-Z0-9_\-\.\/]*[a-zA-Z0-9_\-]|[a-zA-Z0-9_])(?<!\.git|\.atom)'.freeze
+ FULL_NAMESPACE_REGEX_STR = "(?:#{NAMESPACE_REGEX_STR}/)*#{NAMESPACE_REGEX_STR}".freeze
def namespace_regex
@namespace_regex ||= /\A#{NAMESPACE_REGEX_STR}\z/.freeze
diff --git a/lib/gitlab/request_profiler.rb b/lib/gitlab/request_profiler.rb
index 8130e55351e..0c9ab759e81 100644
--- a/lib/gitlab/request_profiler.rb
+++ b/lib/gitlab/request_profiler.rb
@@ -2,7 +2,7 @@ require 'fileutils'
module Gitlab
module RequestProfiler
- PROFILES_DIR = "#{Gitlab.config.shared.path}/tmp/requests_profiles"
+ PROFILES_DIR = "#{Gitlab.config.shared.path}/tmp/requests_profiles".freeze
def profile_token
Rails.cache.fetch('profile-token') do
diff --git a/lib/gitlab/saml/user.rb b/lib/gitlab/saml/user.rb
index f253dc7477e..8a7cc690046 100644
--- a/lib/gitlab/saml/user.rb
+++ b/lib/gitlab/saml/user.rb
@@ -28,11 +28,12 @@ module Gitlab
if external_users_enabled? && @user
# Check if there is overlap between the user's groups and the external groups
# setting then set user as external or internal.
- if (auth_hash.groups & Gitlab::Saml::Config.external_groups).empty?
- @user.external = false
- else
- @user.external = true
- end
+ @user.external =
+ if (auth_hash.groups & Gitlab::Saml::Config.external_groups).empty?
+ false
+ else
+ true
+ end
end
@user
diff --git a/lib/gitlab/sanitizers/svg/whitelist.rb b/lib/gitlab/sanitizers/svg/whitelist.rb
index 7b6b70d8dbc..d50f826f924 100644
--- a/lib/gitlab/sanitizers/svg/whitelist.rb
+++ b/lib/gitlab/sanitizers/svg/whitelist.rb
@@ -6,18 +6,19 @@ module Gitlab
module SVG
class Whitelist
ALLOWED_ELEMENTS = %w[
- a altGlyph altGlyphDef altGlyphItem animate
- animateColor animateMotion animateTransform circle clipPath color-profile
- cursor defs desc ellipse feBlend feColorMatrix feComponentTransfer
- feComposite feConvolveMatrix feDiffuseLighting feDisplacementMap
- feDistantLight feFlood feFuncA feFuncB feFuncG feFuncR feGaussianBlur
- feImage feMerge feMergeNode feMorphology feOffset fePointLight
- feSpecularLighting feSpotLight feTile feTurbulence filter font font-face
- font-face-format font-face-name font-face-src font-face-uri foreignObject
- g glyph glyphRef hkern image line linearGradient marker mask metadata
- missing-glyph mpath path pattern polygon polyline radialGradient rect
- script set stop style svg switch symbol text textPath title tref tspan use
- view vkern].freeze
+ a altGlyph altGlyphDef altGlyphItem animate
+ animateColor animateMotion animateTransform circle clipPath color-profile
+ cursor defs desc ellipse feBlend feColorMatrix feComponentTransfer
+ feComposite feConvolveMatrix feDiffuseLighting feDisplacementMap
+ feDistantLight feFlood feFuncA feFuncB feFuncG feFuncR feGaussianBlur
+ feImage feMerge feMergeNode feMorphology feOffset fePointLight
+ feSpecularLighting feSpotLight feTile feTurbulence filter font font-face
+ font-face-format font-face-name font-face-src font-face-uri foreignObject
+ g glyph glyphRef hkern image line linearGradient marker mask metadata
+ missing-glyph mpath path pattern polygon polyline radialGradient rect
+ script set stop style svg switch symbol text textPath title tref tspan use
+ view vkern
+ ].freeze
ALLOWED_DATA_ATTRIBUTES_IN_ELEMENTS = %w[svg].freeze
diff --git a/lib/gitlab/search_results.rb b/lib/gitlab/search_results.rb
index c9c65f76f4b..ccfa517e04b 100644
--- a/lib/gitlab/search_results.rb
+++ b/lib/gitlab/search_results.rb
@@ -56,11 +56,12 @@ module Gitlab
def issues
issues = IssuesFinder.new(current_user).execute.where(project_id: project_ids_relation)
- if query =~ /#(\d+)\z/
- issues = issues.where(iid: $1)
- else
- issues = issues.full_search(query)
- end
+ issues =
+ if query =~ /#(\d+)\z/
+ issues.where(iid: $1)
+ else
+ issues.full_search(query)
+ end
issues.order('updated_at DESC')
end
@@ -73,11 +74,12 @@ module Gitlab
def merge_requests
merge_requests = MergeRequestsFinder.new(current_user).execute.in_projects(project_ids_relation)
- if query =~ /[#!](\d+)\z/
- merge_requests = merge_requests.where(iid: $1)
- else
- merge_requests = merge_requests.full_search(query)
- end
+ merge_requests =
+ if query =~ /[#!](\d+)\z/
+ merge_requests.where(iid: $1)
+ else
+ merge_requests.full_search(query)
+ end
merge_requests.order('updated_at DESC')
end
diff --git a/lib/gitlab/seeder.rb b/lib/gitlab/seeder.rb
index 7cf506ebe64..b7f825e8284 100644
--- a/lib/gitlab/seeder.rb
+++ b/lib/gitlab/seeder.rb
@@ -18,7 +18,7 @@ def Notify.deliver_later
self
end
eos
- eval(code)
+ eval(code) # rubocop:disable Security/Eval
end
end
end
diff --git a/lib/gitlab/shell.rb b/lib/gitlab/shell.rb
index 942cedd6cd4..7374d2bc8b8 100644
--- a/lib/gitlab/shell.rb
+++ b/lib/gitlab/shell.rb
@@ -80,8 +80,10 @@ module Gitlab
# import_repository("/path/to/storage", "gitlab/gitlab-ci", "https://github.com/randx/six.git")
#
def import_repository(storage, name, url)
- output, status = Popen::popen([gitlab_shell_projects_path, 'import-project',
- storage, "#{name}.git", url, '900'])
+ # Timeout should be less than 900 ideally, to prevent the memory killer
+ # to silently kill the process without knowing we are timing out here.
+ output, status = Popen.popen([gitlab_shell_projects_path, 'import-project',
+ storage, "#{name}.git", url, '800'])
raise Error, output unless status.zero?
true
end
@@ -143,7 +145,7 @@ module Gitlab
# batch_add_keys { |adder| adder.add_key("key-42", "sha-rsa ...") }
def batch_add_keys(&block)
IO.popen(%W(#{gitlab_shell_path}/bin/gitlab-keys batch-add-keys), 'w') do |io|
- block.call(KeyAdder.new(io))
+ yield(KeyAdder.new(io))
end
end
diff --git a/lib/gitlab/sherlock/query.rb b/lib/gitlab/sherlock/query.rb
index 4917c4ae2ac..99e56e923eb 100644
--- a/lib/gitlab/sherlock/query.rb
+++ b/lib/gitlab/sherlock/query.rb
@@ -94,11 +94,12 @@ module Gitlab
private
def raw_explain(query)
- if Gitlab::Database.postgresql?
- explain = "EXPLAIN ANALYZE #{query};"
- else
- explain = "EXPLAIN #{query};"
- end
+ explain =
+ if Gitlab::Database.postgresql?
+ "EXPLAIN ANALYZE #{query};"
+ else
+ "EXPLAIN #{query};"
+ end
ActiveRecord::Base.connection.execute(explain)
end
diff --git a/lib/gitlab/slash_commands/extractor.rb b/lib/gitlab/slash_commands/extractor.rb
index a672e5e4855..6dbb467d70d 100644
--- a/lib/gitlab/slash_commands/extractor.rb
+++ b/lib/gitlab/slash_commands/extractor.rb
@@ -103,7 +103,7 @@ module Gitlab
(?<cmd>#{Regexp.union(names)})
(?:
[ ]
- (?<arg>[^\/\n]*)
+ (?<arg>[^\n]*)
)?
(?:\n|$)
)
diff --git a/lib/gitlab/template/gitlab_ci_yml_template.rb b/lib/gitlab/template/gitlab_ci_yml_template.rb
index 9d2ecee9756..fd040148a1e 100644
--- a/lib/gitlab/template/gitlab_ci_yml_template.rb
+++ b/lib/gitlab/template/gitlab_ci_yml_template.rb
@@ -28,7 +28,7 @@ module Gitlab
end
def dropdown_names(context)
- categories = context == 'autodeploy' ? ['Auto deploy'] : ['General', 'Pages']
+ categories = context == 'autodeploy' ? ['Auto deploy'] : %w(General Pages)
super().slice(*categories)
end
end
diff --git a/lib/gitlab/themes.rb b/lib/gitlab/themes.rb
deleted file mode 100644
index 19ab76ae80f..00000000000
--- a/lib/gitlab/themes.rb
+++ /dev/null
@@ -1,87 +0,0 @@
-module Gitlab
- # Module containing GitLab's application theme definitions and helper methods
- # for accessing them.
- module Themes
- extend self
-
- # Theme ID used when no `default_theme` configuration setting is provided.
- APPLICATION_DEFAULT = 2
-
- # Struct class representing a single Theme
- Theme = Struct.new(:id, :name, :css_class)
-
- # All available Themes
- THEMES = [
- Theme.new(1, 'Graphite', 'ui_graphite'),
- Theme.new(2, 'Charcoal', 'ui_charcoal'),
- Theme.new(3, 'Green', 'ui_green'),
- Theme.new(4, 'Black', 'ui_black'),
- Theme.new(5, 'Violet', 'ui_violet'),
- Theme.new(6, 'Blue', 'ui_blue')
- ].freeze
-
- # Convenience method to get a space-separated String of all the theme
- # classes that might be applied to the `body` element
- #
- # Returns a String
- def body_classes
- THEMES.collect(&:css_class).uniq.join(' ')
- end
-
- # Get a Theme by its ID
- #
- # If the ID is invalid, returns the default Theme.
- #
- # id - Integer ID
- #
- # Returns a Theme
- def by_id(id)
- THEMES.detect { |t| t.id == id } || default
- end
-
- # Returns the number of defined Themes
- def count
- THEMES.size
- end
-
- # Get the default Theme
- #
- # Returns a Theme
- def default
- by_id(default_id)
- end
-
- # Iterate through each Theme
- #
- # Yields the Theme object
- def each(&block)
- THEMES.each(&block)
- end
-
- # Get the Theme for the specified user, or the default
- #
- # user - User record
- #
- # Returns a Theme
- def for_user(user)
- if user
- by_id(user.theme_id)
- else
- default
- end
- end
-
- private
-
- def default_id
- id = Gitlab.config.gitlab.default_theme.to_i
-
- # Prevent an invalid configuration setting from causing an infinite loop
- if id < THEMES.first.id || id > THEMES.last.id
- APPLICATION_DEFAULT
- else
- id
- end
- end
- end
-end
diff --git a/lib/gitlab/upgrader.rb b/lib/gitlab/upgrader.rb
index e78d0c34a02..961df0468a4 100644
--- a/lib/gitlab/upgrader.rb
+++ b/lib/gitlab/upgrader.rb
@@ -46,7 +46,7 @@ module Gitlab
git_tags = fetch_git_tags
git_tags = git_tags.select { |version| version =~ /v\d+\.\d+\.\d+\Z/ }
git_versions = git_tags.map { |tag| Gitlab::VersionInfo.parse(tag.match(/v\d+\.\d+\.\d+/).to_s) }
- "v#{git_versions.sort.last.to_s}"
+ "v#{git_versions.sort.last}"
end
def fetch_git_tags
@@ -59,15 +59,18 @@ module Gitlab
"Stash changed files" => %W(#{Gitlab.config.git.bin_path} stash),
"Get latest code" => %W(#{Gitlab.config.git.bin_path} fetch),
"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 gitlab:assets:clean gitlab:assets:compile),
- "Clear cache" => %W(bundle exec rake cache:clear)
+ "Install gems" => %w(bundle),
+ "Migrate DB" => %w(bundle exec rake db:migrate),
+ "Recompile assets" => %w(bundle exec rake yarn:install gitlab:assets:clean gitlab:assets:compile),
+ "Clear cache" => %w(bundle exec rake cache:clear)
}
end
def env
- { 'RAILS_ENV' => 'production' }
+ {
+ 'RAILS_ENV' => 'production',
+ 'NODE_ENV' => 'production'
+ }
end
def upgrade
diff --git a/lib/gitlab/url_sanitizer.rb b/lib/gitlab/url_sanitizer.rb
index 19dad699edf..1f0d96088cf 100644
--- a/lib/gitlab/url_sanitizer.rb
+++ b/lib/gitlab/url_sanitizer.rb
@@ -1,7 +1,7 @@
module Gitlab
class UrlSanitizer
def self.sanitize(content)
- regexp = URI::Parser.new.make_regexp(['http', 'https', 'ssh', 'git'])
+ regexp = URI::Parser.new.make_regexp(%w(http https ssh git))
content.gsub(regexp) { |url| new(url).masked_url }
rescue Addressable::URI::InvalidURIError
diff --git a/lib/gitlab/visibility_level.rb b/lib/gitlab/visibility_level.rb
index a4e966e4016..b28708c34e1 100644
--- a/lib/gitlab/visibility_level.rb
+++ b/lib/gitlab/visibility_level.rb
@@ -33,9 +33,7 @@ module Gitlab
PUBLIC = 20 unless const_defined?(:PUBLIC)
class << self
- def values
- options.values
- end
+ delegate :values, to: :options
def options
{
diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb
index c8872df8a93..3ff9f9eb5e7 100644
--- a/lib/gitlab/workhorse.rb
+++ b/lib/gitlab/workhorse.rb
@@ -4,10 +4,10 @@ require 'securerandom'
module Gitlab
class Workhorse
- SEND_DATA_HEADER = 'Gitlab-Workhorse-Send-Data'
- VERSION_FILE = 'GITLAB_WORKHORSE_VERSION'
- INTERNAL_API_CONTENT_TYPE = 'application/vnd.gitlab-workhorse+json'
- INTERNAL_API_REQUEST_HEADER = 'Gitlab-Workhorse-Api-Request'
+ SEND_DATA_HEADER = 'Gitlab-Workhorse-Send-Data'.freeze
+ VERSION_FILE = 'GITLAB_WORKHORSE_VERSION'.freeze
+ INTERNAL_API_CONTENT_TYPE = 'application/vnd.gitlab-workhorse+json'.freeze
+ INTERNAL_API_REQUEST_HEADER = 'Gitlab-Workhorse-Api-Request'.freeze
# Supposedly the effective key size for HMAC-SHA256 is 256 bits, i.e. 32
# bytes https://tools.ietf.org/html/rfc4868#section-2.6
diff --git a/lib/tasks/brakeman.rake b/lib/tasks/brakeman.rake
index d5a402907d8..2301ec9b228 100644
--- a/lib/tasks/brakeman.rake
+++ b/lib/tasks/brakeman.rake
@@ -2,7 +2,7 @@ desc 'Security check via brakeman'
task :brakeman do
# We get 0 warnings at level 'w3' but we would like to reach 'w2'. Merge
# requests are welcome!
- if system(*%W(brakeman --no-progress --skip-files lib/backup/repository.rb -w3 -z))
+ if system(*%w(brakeman --no-progress --skip-files lib/backup/repository.rb -w3 -z))
puts 'Security check succeed'
else
puts 'Security check failed'
diff --git a/lib/tasks/cache.rake b/lib/tasks/cache.rake
index 78ae187817a..d55923673b1 100644
--- a/lib/tasks/cache.rake
+++ b/lib/tasks/cache.rake
@@ -1,7 +1,7 @@
namespace :cache do
namespace :clear do
REDIS_CLEAR_BATCH_SIZE = 1000 # There seems to be no speedup when pushing beyond 1,000
- REDIS_SCAN_START_STOP = '0' # Magic value, see http://redis.io/commands/scan
+ REDIS_SCAN_START_STOP = '0'.freeze # Magic value, see http://redis.io/commands/scan
desc "GitLab | Clear redis cache"
task redis: :environment do
diff --git a/lib/tasks/dev.rake b/lib/tasks/dev.rake
index 5e94fba97bf..e65609d7001 100644
--- a/lib/tasks/dev.rake
+++ b/lib/tasks/dev.rake
@@ -2,7 +2,7 @@ task dev: ["dev:setup"]
namespace :dev do
desc "GitLab | Setup developer environment (db, fixtures)"
- task :setup => :environment do
+ task setup: :environment do
ENV['force'] = 'yes'
Rake::Task["gitlab:setup"].invoke
Rake::Task["gitlab:shell:setup"].invoke
diff --git a/lib/tasks/downtime_check.rake b/lib/tasks/downtime_check.rake
index afe5d42910c..557f4fef10b 100644
--- a/lib/tasks/downtime_check.rake
+++ b/lib/tasks/downtime_check.rake
@@ -1,10 +1,10 @@
desc 'Checks if migrations in a branch require downtime'
task downtime_check: :environment do
- if defined?(Gitlab::License)
- repo = 'gitlab-ee'
- else
- repo = 'gitlab-ce'
- end
+ repo = if defined?(Gitlab::License)
+ 'gitlab-ee'
+ else
+ 'gitlab-ce'
+ end
`git fetch https://gitlab.com/gitlab-org/#{repo}.git --depth 1`
diff --git a/lib/tasks/eslint.rake b/lib/tasks/eslint.rake
index 2514b050695..51f5d768102 100644
--- a/lib/tasks/eslint.rake
+++ b/lib/tasks/eslint.rake
@@ -1,7 +1,8 @@
unless Rails.env.production?
desc "GitLab | Run ESLint"
- task :eslint do
- system("yarn", "run", "eslint")
+ task eslint: ['yarn:check'] do
+ unless system('yarn run eslint')
+ abort('rake eslint failed')
+ end
end
end
-
diff --git a/lib/tasks/flay.rake b/lib/tasks/flay.rake
index e9587595fef..7ad2b2e4d39 100644
--- a/lib/tasks/flay.rake
+++ b/lib/tasks/flay.rake
@@ -1,6 +1,6 @@
desc 'Code duplication analyze via flay'
task :flay do
- output = %x(bundle exec flay --mass 35 app/ lib/gitlab/)
+ output = `bundle exec flay --mass 35 app/ lib/gitlab/`
if output.include? "Similar code found"
puts output
diff --git a/lib/tasks/gitlab/assets.rake b/lib/tasks/gitlab/assets.rake
index b6ef8260191..098f9851b45 100644
--- a/lib/tasks/gitlab/assets.rake
+++ b/lib/tasks/gitlab/assets.rake
@@ -1,26 +1,26 @@
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
+ task compile: [
+ 'yarn:check',
+ 'assets:precompile',
+ 'webpack:compile',
+ 'gitlab:assets:fix_urls'
+ ]
desc 'GitLab | Assets | Clean up old compiled frontend assets'
- task :clean do
- Rake::Task['assets:clean'].invoke
- end
+ task clean: ['assets:clean']
desc 'GitLab | Assets | Remove all compiled frontend assets'
- task :purge do
- Rake::Task['assets:clobber'].invoke
- end
+ task purge: ['assets:clobber']
+
+ desc 'GitLab | Assets | Uninstall frontend dependencies'
+ task purge_modules: ['yarn:clobber']
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 |
+ css_files.each do |file|
# replace url(/assets/*) with url(./*)
puts "Fixing #{file}"
system "sed", "-i", "-e", 's/url(\([\"\']\?\)\/assets\//url(\1.\//g', file
diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake
index 35c4194e87c..38edd49b6ed 100644
--- a/lib/tasks/gitlab/check.rake
+++ b/lib/tasks/gitlab/check.rake
@@ -6,8 +6,6 @@ namespace :gitlab do
gitlab:ldap:check
gitlab:app:check}
-
-
namespace :app do
desc "GitLab | Check the configuration of the GitLab Rails app"
task check: :environment do
@@ -34,7 +32,6 @@ namespace :gitlab do
finished_checking "GitLab"
end
-
# Checks
########################
@@ -194,7 +191,7 @@ namespace :gitlab do
def check_migrations_are_up
print "All migrations up? ... "
- migration_status, _ = Gitlab::Popen.popen(%W(bundle exec rake db:migrate:status))
+ migration_status, _ = Gitlab::Popen.popen(%w(bundle exec rake db:migrate:status))
unless migration_status =~ /down\s+\d{14}/
puts "yes".color(:green)
@@ -279,7 +276,7 @@ namespace :gitlab do
upload_path_tmp = File.join(upload_path, 'tmp')
if File.stat(upload_path).mode == 040700
- unless Dir.exists?(upload_path_tmp)
+ unless Dir.exist?(upload_path_tmp)
puts 'skipped (no tmp uploads folder yet)'.color(:magenta)
return
end
@@ -316,7 +313,7 @@ namespace :gitlab do
min_redis_version = "2.8.0"
print "Redis version >= #{min_redis_version}? ... "
- redis_version = run_command(%W(redis-cli --version))
+ redis_version = run_command(%w(redis-cli --version))
redis_version = redis_version.try(:match, /redis-cli (\d+\.\d+\.\d+)/)
if redis_version &&
(Gem::Version.new(redis_version[1]) > Gem::Version.new(min_redis_version))
@@ -351,7 +348,6 @@ namespace :gitlab do
finished_checking "GitLab Shell"
end
-
# Checks
########################
@@ -387,7 +383,7 @@ namespace :gitlab do
unless File.exist?(repo_base_path)
puts "can't check because of previous errors".color(:magenta)
- return
+ break
end
unless File.symlink?(repo_base_path)
@@ -410,7 +406,7 @@ namespace :gitlab do
unless File.exist?(repo_base_path)
puts "can't check because of previous errors".color(:magenta)
- return
+ break
end
if File.stat(repo_base_path).mode.to_s(8).ends_with?("2770")
@@ -440,7 +436,7 @@ namespace :gitlab do
unless File.exist?(repo_base_path)
puts "can't check because of previous errors".color(:magenta)
- return
+ break
end
uid = uid_for(gitlab_shell_ssh_user)
@@ -493,7 +489,6 @@ namespace :gitlab do
)
fix_and_rerun
end
-
end
end
@@ -565,8 +560,6 @@ namespace :gitlab do
end
end
-
-
namespace :sidekiq do
desc "GitLab | Check the configuration of Sidekiq"
task check: :environment do
@@ -579,7 +572,6 @@ namespace :gitlab do
finished_checking "Sidekiq"
end
-
# Checks
########################
@@ -621,12 +613,11 @@ namespace :gitlab do
end
def sidekiq_process_count
- ps_ux, _ = Gitlab::Popen.popen(%W(ps ux))
+ ps_ux, _ = Gitlab::Popen.popen(%w(ps ux))
ps_ux.scan(/sidekiq \d+\.\d+\.\d+/).count
end
end
-
namespace :incoming_email do
desc "GitLab | Check the configuration of Reply by email"
task check: :environment do
@@ -649,7 +640,6 @@ namespace :gitlab do
finished_checking "Reply by email"
end
-
# Checks
########################
@@ -724,8 +714,11 @@ namespace :gitlab do
def check_imap_authentication
print "IMAP server credentials are correct? ... "
- config_path = Rails.root.join('config', 'mail_room.yml')
- config_file = YAML.load(ERB.new(File.read(config_path)).result)
+ config_path = Rails.root.join('config', 'mail_room.yml').to_s
+ erb = ERB.new(File.read(config_path))
+ erb.filename = config_path
+ config_file = YAML.load(erb.result)
+
config = config_file[:mailboxes].first
if config
@@ -754,7 +747,7 @@ namespace :gitlab do
end
def mail_room_running?
- ps_ux, _ = Gitlab::Popen.popen(%W(ps ux))
+ ps_ux, _ = Gitlab::Popen.popen(%w(ps ux))
ps_ux.include?("mail_room")
end
end
@@ -802,13 +795,13 @@ namespace :gitlab do
def check_ldap_auth(adapter)
auth = adapter.config.has_auth?
- if auth && adapter.ldap.bind
- message = 'Success'.color(:green)
- elsif auth
- message = 'Failed. Check `bind_dn` and `password` configuration values'.color(:red)
- else
- message = 'Anonymous. No `bind_dn` or `password` configured'.color(:yellow)
- end
+ message = if auth && adapter.ldap.bind
+ 'Success'.color(:green)
+ elsif auth
+ 'Failed. Check `bind_dn` and `password` configuration values'.color(:red)
+ else
+ 'Anonymous. No `bind_dn` or `password` configured'.color(:yellow)
+ end
puts "LDAP authentication... #{message}"
end
@@ -835,11 +828,11 @@ namespace :gitlab do
user = User.find_by(username: username)
if user
repo_dirs = user.authorized_projects.map do |p|
- File.join(
- p.repository_storage_path,
- "#{p.path_with_namespace}.git"
- )
- end
+ File.join(
+ p.repository_storage_path,
+ "#{p.path_with_namespace}.git"
+ )
+ end
repo_dirs.each { |repo_dir| check_repo_integrity(repo_dir) }
else
@@ -852,7 +845,7 @@ namespace :gitlab do
##########################
def fix_and_rerun
- puts " Please #{"fix the error above"} and rerun the checks.".color(:red)
+ puts " Please fix the error above and rerun the checks.".color(:red)
end
def for_more_information(*sources)
@@ -914,7 +907,7 @@ namespace :gitlab do
def check_ruby_version
required_version = Gitlab::VersionInfo.new(2, 1, 0)
- current_version = Gitlab::VersionInfo.parse(run_command(%W(ruby --version)))
+ current_version = Gitlab::VersionInfo.parse(run_command(%w(ruby --version)))
print "Ruby version >= #{required_version} ? ... "
@@ -985,13 +978,13 @@ namespace :gitlab do
end
def check_config_lock(repo_dir)
- config_exists = File.exist?(File.join(repo_dir,'config.lock'))
+ config_exists = File.exist?(File.join(repo_dir, 'config.lock'))
config_output = config_exists ? 'yes'.color(:red) : 'no'.color(:green)
puts "'config.lock' file exists?".color(:yellow) + " ... #{config_output}"
end
def check_ref_locks(repo_dir)
- lock_files = Dir.glob(File.join(repo_dir,'refs/heads/*.lock'))
+ lock_files = Dir.glob(File.join(repo_dir, 'refs/heads/*.lock'))
if lock_files.present?
puts "Ref lock files exist:".color(:red)
lock_files.each do |lock_file|
diff --git a/lib/tasks/gitlab/cleanup.rake b/lib/tasks/gitlab/cleanup.rake
index 967f630ef20..daf7382dd02 100644
--- a/lib/tasks/gitlab/cleanup.rake
+++ b/lib/tasks/gitlab/cleanup.rake
@@ -25,7 +25,6 @@ namespace :gitlab do
end
all_dirs.each do |dir_path|
-
if remove_flag
if FileUtils.rm_rf dir_path
puts "Removed...#{dir_path}".color(:red)
@@ -53,11 +52,11 @@ namespace :gitlab do
IO.popen(%W(find #{repo_root} -mindepth 1 -maxdepth 2 -name *.git)) do |find|
find.each_line do |path|
path.chomp!
- repo_with_namespace = path.
- sub(repo_root, '').
- sub(%r{^/*}, '').
- chomp('.git').
- chomp('.wiki')
+ repo_with_namespace = path
+ .sub(repo_root, '')
+ .sub(%r{^/*}, '')
+ .chomp('.git')
+ .chomp('.wiki')
next if Project.find_by_full_path(repo_with_namespace)
new_path = path + move_suffix
puts path.inspect + ' -> ' + new_path.inspect
diff --git a/lib/tasks/gitlab/db.rake b/lib/tasks/gitlab/db.rake
index 7c96bc864ce..ecf6b6e068b 100644
--- a/lib/tasks/gitlab/db.rake
+++ b/lib/tasks/gitlab/db.rake
@@ -23,7 +23,7 @@ namespace :gitlab do
end
desc 'Drop all tables'
- task :drop_tables => :environment do
+ task drop_tables: :environment do
connection = ActiveRecord::Base.connection
# If MySQL, turn off foreign key checks
@@ -62,9 +62,9 @@ namespace :gitlab do
ref = Shellwords.escape(args[:ref])
- migrations = `git diff #{ref}.. --name-only -- db/migrate`.lines.
- map { |file| Rails.root.join(file.strip).to_s }.
- select { |file| File.file?(file) }
+ migrations = `git diff #{ref}.. --name-only -- db/migrate`.lines
+ .map { |file| Rails.root.join(file.strip).to_s }
+ .select { |file| File.file?(file) }
Gitlab::DowntimeCheck.new.check_and_print(migrations)
end
diff --git a/lib/tasks/gitlab/git.rake b/lib/tasks/gitlab/git.rake
index a67c1fe1f27..cf82134d97e 100644
--- a/lib/tasks/gitlab/git.rake
+++ b/lib/tasks/gitlab/git.rake
@@ -1,6 +1,5 @@
namespace :gitlab do
namespace :git do
-
desc "GitLab | Git | Repack"
task repack: :environment do
failures = perform_git_cmd(%W(#{Gitlab.config.git.bin_path} repack -a --quiet), "Repacking repo")
@@ -50,6 +49,5 @@ namespace :gitlab do
puts "The following repositories reported errors:".color(:red)
failures.each { |f| puts "- #{f}" }
end
-
end
end
diff --git a/lib/tasks/gitlab/import.rake b/lib/tasks/gitlab/import.rake
index b4015f5238e..66e7b7685f7 100644
--- a/lib/tasks/gitlab/import.rake
+++ b/lib/tasks/gitlab/import.rake
@@ -46,7 +46,7 @@ namespace :gitlab do
group = Namespace.find_by(path: group_name)
# create group namespace
unless group
- group = Group.new(:name => group_name)
+ group = Group.new(name: group_name)
group.path = group_name
group.owner = user
if group.save
diff --git a/lib/tasks/gitlab/import_export.rake b/lib/tasks/gitlab/import_export.rake
index c2c6031db67..dd1825c8a9e 100644
--- a/lib/tasks/gitlab/import_export.rake
+++ b/lib/tasks/gitlab/import_export.rake
@@ -7,7 +7,7 @@ namespace :gitlab do
desc "GitLab | Display exported DB structure"
task data: :environment do
- puts YAML.load_file(Gitlab::ImportExport.config_file)['project_tree'].to_yaml(:SortKeys => true)
+ puts YAML.load_file(Gitlab::ImportExport.config_file)['project_tree'].to_yaml(SortKeys: true)
end
end
end
diff --git a/lib/tasks/gitlab/info.rake b/lib/tasks/gitlab/info.rake
index f7c831892ee..ae78fe64eb8 100644
--- a/lib/tasks/gitlab/info.rake
+++ b/lib/tasks/gitlab/info.rake
@@ -2,24 +2,23 @@ namespace :gitlab do
namespace :env do
desc "GitLab | Show information about GitLab and its environment"
task info: :environment do
-
# check if there is an RVM environment
- rvm_version = run_and_match(%W(rvm --version), /[\d\.]+/).try(:to_s)
+ rvm_version = run_and_match(%w(rvm --version), /[\d\.]+/).try(:to_s)
# check Ruby version
- ruby_version = run_and_match(%W(ruby --version), /[\d\.p]+/).try(:to_s)
+ ruby_version = run_and_match(%w(ruby --version), /[\d\.p]+/).try(:to_s)
# check Gem version
- gem_version = run_command(%W(gem --version))
+ gem_version = run_command(%w(gem --version))
# check Bundler version
- bunder_version = run_and_match(%W(bundle --version), /[\d\.]+/).try(:to_s)
+ bunder_version = run_and_match(%w(bundle --version), /[\d\.]+/).try(:to_s)
# check Rake version
- rake_version = run_and_match(%W(rake --version), /[\d\.]+/).try(:to_s)
+ 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
+ redis_version = run_and_match(%w(redis-cli --version), /redis-cli (\d+\.\d+\.\d+)/).to_a
puts ""
puts "System information".color(:yellow)
puts "System:\t\t#{os_name || "unknown".color(:red)}"
- puts "Current User:\t#{run_command(%W(whoami))}"
+ puts "Current User:\t#{run_command(%w(whoami))}"
puts "Using RVM:\t#{rvm_version.present? ? "yes".color(:green) : "no"}"
puts "RVM Version:\t#{rvm_version}" if rvm_version.present?
puts "Ruby Version:\t#{ruby_version || "unknown".color(:red)}"
@@ -29,7 +28,6 @@ namespace :gitlab do
puts "Redis Version:\t#{redis_version[1] || "unknown".color(:red)}"
puts "Sidekiq Version:#{Sidekiq::VERSION}"
-
# check database adapter
database_adapter = ActiveRecord::Base.connection.adapter_name.downcase
@@ -54,8 +52,6 @@ namespace :gitlab do
puts "Using Omniauth:\t#{Gitlab.config.omniauth.enabled ? "yes".color(:green) : "no"}"
puts "Omniauth Providers: #{omniauth_providers.join(', ')}" if Gitlab.config.omniauth.enabled
-
-
# check Gitolite version
gitlab_shell_version_file = "#{Gitlab.config.gitlab_shell.hooks_path}/../VERSION"
if File.readable?(gitlab_shell_version_file)
@@ -71,7 +67,6 @@ namespace :gitlab do
end
puts "Hooks:\t\t#{Gitlab.config.gitlab_shell.hooks_path}"
puts "Git:\t\t#{Gitlab.config.git.bin_path}"
-
end
end
end
diff --git a/lib/tasks/gitlab/shell.rake b/lib/tasks/gitlab/shell.rake
index 5a09cd7ce41..dd2fda54e62 100644
--- a/lib/tasks/gitlab/shell.rake
+++ b/lib/tasks/gitlab/shell.rake
@@ -20,10 +20,10 @@ namespace :gitlab do
config = {
user: Gitlab.config.gitlab.user,
gitlab_url: gitlab_url,
- http_settings: {self_signed_cert: false}.stringify_keys,
+ http_settings: { self_signed_cert: false }.stringify_keys,
auth_file: File.join(user_home, ".ssh", "authorized_keys"),
redis: {
- bin: %x{which redis-cli}.chomp,
+ bin: `which redis-cli`.chomp,
namespace: "resque:gitlab"
}.stringify_keys,
log_level: "INFO",
@@ -43,7 +43,7 @@ namespace :gitlab do
File.open("config.yml", "w+") {|f| f.puts config.to_yaml}
# Launch installation process
- system(*%W(bin/install) + repository_storage_paths_args)
+ system(*%w(bin/install) + repository_storage_paths_args)
end
# (Re)create hooks
diff --git a/lib/tasks/gitlab/sidekiq.rake b/lib/tasks/gitlab/sidekiq.rake
index f2e12d85045..6cbc83b8973 100644
--- a/lib/tasks/gitlab/sidekiq.rake
+++ b/lib/tasks/gitlab/sidekiq.rake
@@ -1,9 +1,9 @@
namespace :gitlab do
namespace :sidekiq do
- QUEUE = 'queue:post_receive'
+ QUEUE = 'queue:post_receive'.freeze
desc 'Drop all Sidekiq PostReceive jobs for a given project'
- task :drop_post_receive , [:project] => :environment do |t, args|
+ task :drop_post_receive, [:project] => :environment do |t, args|
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
@@ -21,7 +21,7 @@ namespace :gitlab do
# new jobs already. We will repopulate it with the old jobs, skipping the
# ones we want to drop.
dropped = 0
- while (job = redis.lpop(temp_queue)) do
+ while (job = redis.lpop(temp_queue))
if repo_path(job) == project_path
dropped += 1
else
diff --git a/lib/tasks/gitlab/task_helpers.rb b/lib/tasks/gitlab/task_helpers.rb
index e128738b5f8..2a999ad6959 100644
--- a/lib/tasks/gitlab/task_helpers.rb
+++ b/lib/tasks/gitlab/task_helpers.rb
@@ -19,23 +19,20 @@ module Gitlab
# It will primarily use lsb_relase to determine the OS.
# It has fallbacks to Debian, SuSE, OS X and systems running systemd.
def os_name
- os_name = run_command(%W(lsb_release -irs))
- os_name ||= if File.readable?('/etc/system-release')
- File.read('/etc/system-release')
- end
- os_name ||= if File.readable?('/etc/debian_version')
- debian_version = File.read('/etc/debian_version')
- "Debian #{debian_version}"
- end
- os_name ||= if File.readable?('/etc/SuSE-release')
- File.read('/etc/SuSE-release')
- end
- os_name ||= if os_x_version = run_command(%W(sw_vers -productVersion))
- "Mac OS X #{os_x_version}"
- end
- os_name ||= if File.readable?('/etc/os-release')
- File.read('/etc/os-release').match(/PRETTY_NAME=\"(.+)\"/)[1]
- end
+ os_name = run_command(%w(lsb_release -irs))
+ os_name ||=
+ if File.readable?('/etc/system-release')
+ File.read('/etc/system-release')
+ elsif File.readable?('/etc/debian_version')
+ "Debian #{File.read('/etc/debian_version')}"
+ elsif File.readable?('/etc/SuSE-release')
+ File.read('/etc/SuSE-release')
+ elsif os_x_version = run_command(%w(sw_vers -productVersion))
+ "Mac OS X #{os_x_version}"
+ elsif File.readable?('/etc/os-release')
+ File.read('/etc/os-release').match(/PRETTY_NAME=\"(.+)\"/)[1]
+ end
+
os_name.try(:squish!)
end
@@ -104,7 +101,7 @@ module Gitlab
def warn_user_is_not_gitlab
unless @warned_user_not_gitlab
gitlab_user = Gitlab.config.gitlab.user
- current_user = run_command(%W(whoami)).chomp
+ current_user = run_command(%w(whoami)).chomp
unless current_user == gitlab_user
puts " Warning ".color(:black).background(:yellow)
puts " You are running as user #{current_user.color(:magenta)}, we hope you know what you are doing."
@@ -171,14 +168,14 @@ module Gitlab
def reset_to_tag(tag_wanted, target_dir)
tag =
- begin
- # First try to checkout without fetching
- # to avoid stalling tests if the Internet is down.
- run_command!(%W[#{Gitlab.config.git.bin_path} -C #{target_dir} describe -- #{tag_wanted}])
- rescue Gitlab::TaskFailedError
- run_command!(%W[#{Gitlab.config.git.bin_path} -C #{target_dir} fetch origin])
- run_command!(%W[#{Gitlab.config.git.bin_path} -C #{target_dir} describe -- origin/#{tag_wanted}])
- end
+ begin
+ # First try to checkout without fetching
+ # to avoid stalling tests if the Internet is down.
+ run_command!(%W[#{Gitlab.config.git.bin_path} -C #{target_dir} describe -- #{tag_wanted}])
+ rescue Gitlab::TaskFailedError
+ run_command!(%W[#{Gitlab.config.git.bin_path} -C #{target_dir} fetch origin])
+ run_command!(%W[#{Gitlab.config.git.bin_path} -C #{target_dir} describe -- origin/#{tag_wanted}])
+ end
if tag
run_command!(%W[#{Gitlab.config.git.bin_path} -C #{target_dir} reset --hard #{tag.strip}])
diff --git a/lib/tasks/gitlab/test.rake b/lib/tasks/gitlab/test.rake
index 84810b489ce..523b0fa055b 100644
--- a/lib/tasks/gitlab/test.rake
+++ b/lib/tasks/gitlab/test.rake
@@ -2,15 +2,15 @@ namespace :gitlab do
desc "GitLab | Run all tests"
task :test do
cmds = [
- %W(rake brakeman),
- %W(rake rubocop),
- %W(rake spinach),
- %W(rake spec),
- %W(rake karma)
+ %w(rake brakeman),
+ %w(rake rubocop),
+ %w(rake spinach),
+ %w(rake spec),
+ %w(rake karma)
]
cmds.each do |cmd|
- system({'RAILS_ENV' => 'test', 'force' => 'yes'}, *cmd) or raise("#{cmd} failed!")
+ system({ 'RAILS_ENV' => 'test', 'force' => 'yes' }, *cmd) || raise("#{cmd} failed!")
end
end
end
diff --git a/lib/tasks/gitlab/track_deployment.rake b/lib/tasks/gitlab/track_deployment.rake
index 84aa2e8507a..6f101aea303 100644
--- a/lib/tasks/gitlab/track_deployment.rake
+++ b/lib/tasks/gitlab/track_deployment.rake
@@ -1,8 +1,8 @@
namespace :gitlab do
desc 'GitLab | Tracks a deployment in GitLab Performance Monitoring'
task track_deployment: :environment do
- metric = Gitlab::Metrics::Metric.
- new('deployments', version: Gitlab::VERSION)
+ metric = Gitlab::Metrics::Metric
+ .new('deployments', version: Gitlab::VERSION)
Gitlab::Metrics.submit_metrics([metric.to_hash])
end
diff --git a/lib/tasks/gitlab/update_templates.rake b/lib/tasks/gitlab/update_templates.rake
index b77a5bb62d1..dbdfb335a5c 100644
--- a/lib/tasks/gitlab/update_templates.rake
+++ b/lib/tasks/gitlab/update_templates.rake
@@ -46,7 +46,7 @@ namespace :gitlab do
"https://gitlab.com/gitlab-org/gitlab-ci-yml.git",
/(\.{1,2}|LICENSE|Pages|autodeploy|\.gitlab-ci.yml)\z/
)
- ]
+ ].freeze
def vendor_directory
Rails.root.join('vendor')
diff --git a/lib/tasks/gitlab/web_hook.rake b/lib/tasks/gitlab/web_hook.rake
index 49530e7a372..5a1c8006052 100644
--- a/lib/tasks/gitlab/web_hook.rake
+++ b/lib/tasks/gitlab/web_hook.rake
@@ -1,7 +1,7 @@
namespace :gitlab do
namespace :web_hook do
desc "GitLab | Adds a webhook to the projects"
- task :add => :environment do
+ task add: :environment do
web_hook_url = ENV['URL']
namespace_path = ENV['NAMESPACE']
@@ -21,7 +21,7 @@ namespace :gitlab do
end
desc "GitLab | Remove a webhook from the projects"
- task :rm => :environment do
+ task rm: :environment do
web_hook_url = ENV['URL']
namespace_path = ENV['NAMESPACE']
@@ -34,7 +34,7 @@ namespace :gitlab do
end
desc "GitLab | List webhooks"
- task :list => :environment do
+ task list: :environment do
namespace_path = ENV['NAMESPACE']
projects = find_projects(namespace_path)
diff --git a/lib/tasks/karma.rake b/lib/tasks/karma.rake
index 35cfed9dc75..40465ea3bf0 100644
--- a/lib/tasks/karma.rake
+++ b/lib/tasks/karma.rake
@@ -1,6 +1,4 @@
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|
@@ -10,7 +8,7 @@ unless Rails.env.production?
end
desc 'GitLab | Karma | Run JavaScript tests'
- task :tests do
+ task tests: ['yarn:check'] do
sh "yarn run karma" do |ok, res|
abort('rake karma:tests failed') unless ok
end
@@ -18,8 +16,5 @@ unless Rails.env.production?
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
+ task karma: ['karma:fixtures', 'karma:tests']
end
diff --git a/lib/tasks/lint.rake b/lib/tasks/lint.rake
index 32b668df3bf..7b63e93db0e 100644
--- a/lib/tasks/lint.rake
+++ b/lib/tasks/lint.rake
@@ -6,4 +6,3 @@ unless Rails.env.production?
end
end
end
-
diff --git a/lib/tasks/migrate/migrate_iids.rake b/lib/tasks/migrate/migrate_iids.rake
index 4f2486157b7..fc2cea8c016 100644
--- a/lib/tasks/migrate/migrate_iids.rake
+++ b/lib/tasks/migrate/migrate_iids.rake
@@ -24,7 +24,7 @@ task migrate_iids: :environment do
else
print 'F'
end
- rescue => ex
+ rescue
print 'F'
end
end
diff --git a/lib/tasks/services.rake b/lib/tasks/services.rake
index 39541c0b9c6..56b81106c5f 100644
--- a/lib/tasks/services.rake
+++ b/lib/tasks/services.rake
@@ -76,23 +76,23 @@ namespace :services do
end
param_hash
- end.sort_by { |p| p[:required] ? 0 : 1 }
+ end
+ service_hash[:params].sort_by! { |p| p[:required] ? 0 : 1 }
- puts "Collected data for: #{service.title}, #{Time.now-service_start}"
+ puts "Collected data for: #{service.title}, #{Time.now - service_start}"
service_hash
end
doc_start = Time.now
doc_path = File.join(Rails.root, 'doc', 'api', 'services.md')
- result = ERB.new(services_template, 0 , '>')
+ result = ERB.new(services_template, 0, '>')
.result(OpenStruct.new(services: services).instance_eval { binding })
File.open(doc_path, 'w') do |f|
f.write result
end
- puts "write a new service.md to: #{doc_path.to_s}, #{Time.now-doc_start}"
-
+ puts "write a new service.md to: #{doc_path}, #{Time.now - doc_start}"
end
end
diff --git a/lib/tasks/sidekiq.rake b/lib/tasks/sidekiq.rake
index d1f6ed87704..dd9ce86f7ca 100644
--- a/lib/tasks/sidekiq.rake
+++ b/lib/tasks/sidekiq.rake
@@ -1,21 +1,21 @@
namespace :sidekiq do
desc "GitLab | Stop sidekiq"
task :stop do
- system *%W(bin/background_jobs stop)
+ system(*%w(bin/background_jobs stop))
end
desc "GitLab | Start sidekiq"
task :start do
- system *%W(bin/background_jobs start)
+ system(*%w(bin/background_jobs start))
end
desc 'GitLab | Restart sidekiq'
task :restart do
- system *%W(bin/background_jobs restart)
+ system(*%w(bin/background_jobs restart))
end
desc "GitLab | Start sidekiq with launchd on Mac OS X"
task :launchd do
- system *%W(bin/background_jobs start_no_deamonize)
+ system(*%w(bin/background_jobs start_no_deamonize))
end
end
diff --git a/lib/tasks/spec.rake b/lib/tasks/spec.rake
index 2cf7a25a0fd..602c60be828 100644
--- a/lib/tasks/spec.rake
+++ b/lib/tasks/spec.rake
@@ -4,8 +4,8 @@ namespace :spec do
desc 'GitLab | Rspec | Run request specs'
task :api do
cmds = [
- %W(rake gitlab:setup),
- %W(rspec spec --tag @api)
+ %w(rake gitlab:setup),
+ %w(rspec spec --tag @api)
]
run_commands(cmds)
end
@@ -13,8 +13,8 @@ namespace :spec do
desc 'GitLab | Rspec | Run feature specs'
task :feature do
cmds = [
- %W(rake gitlab:setup),
- %W(rspec spec --tag @feature)
+ %w(rake gitlab:setup),
+ %w(rspec spec --tag @feature)
]
run_commands(cmds)
end
@@ -22,8 +22,8 @@ namespace :spec do
desc 'GitLab | Rspec | Run model specs'
task :models do
cmds = [
- %W(rake gitlab:setup),
- %W(rspec spec --tag @models)
+ %w(rake gitlab:setup),
+ %w(rspec spec --tag @models)
]
run_commands(cmds)
end
@@ -31,8 +31,8 @@ namespace :spec do
desc 'GitLab | Rspec | Run service specs'
task :services do
cmds = [
- %W(rake gitlab:setup),
- %W(rspec spec --tag @services)
+ %w(rake gitlab:setup),
+ %w(rspec spec --tag @services)
]
run_commands(cmds)
end
@@ -40,8 +40,8 @@ namespace :spec do
desc 'GitLab | Rspec | Run lib specs'
task :lib do
cmds = [
- %W(rake gitlab:setup),
- %W(rspec spec --tag @lib)
+ %w(rake gitlab:setup),
+ %w(rspec spec --tag @lib)
]
run_commands(cmds)
end
@@ -49,8 +49,8 @@ namespace :spec do
desc 'GitLab | Rspec | Run other specs'
task :other do
cmds = [
- %W(rake gitlab:setup),
- %W(rspec spec --tag ~@api --tag ~@feature --tag ~@models --tag ~@lib --tag ~@services)
+ %w(rake gitlab:setup),
+ %w(rspec spec --tag ~@api --tag ~@feature --tag ~@models --tag ~@lib --tag ~@services)
]
run_commands(cmds)
end
@@ -59,14 +59,14 @@ end
desc "GitLab | Run specs"
task :spec do
cmds = [
- %W(rake gitlab:setup),
- %W(rspec spec),
+ %w(rake gitlab:setup),
+ %w(rspec spec),
]
run_commands(cmds)
end
def run_commands(cmds)
cmds.each do |cmd|
- system({'RAILS_ENV' => 'test', 'force' => 'yes'}, *cmd) or raise("#{cmd} failed!")
+ system({ 'RAILS_ENV' => 'test', 'force' => 'yes' }, *cmd) || raise("#{cmd} failed!")
end
end
diff --git a/lib/tasks/spinach.rake b/lib/tasks/spinach.rake
index 8dbfa7751dc..19ff13f06c0 100644
--- a/lib/tasks/spinach.rake
+++ b/lib/tasks/spinach.rake
@@ -35,7 +35,7 @@ task :spinach do
end
def run_system_command(cmd)
- system({'RAILS_ENV' => 'test', 'force' => 'yes'}, *cmd)
+ system({ 'RAILS_ENV' => 'test', 'force' => 'yes' }, *cmd)
end
def run_spinach_command(args)
diff --git a/lib/tasks/yarn.rake b/lib/tasks/yarn.rake
new file mode 100644
index 00000000000..2ac88a039e7
--- /dev/null
+++ b/lib/tasks/yarn.rake
@@ -0,0 +1,40 @@
+
+namespace :yarn do
+ desc 'Ensure Yarn is installed'
+ task :available do
+ unless system('yarn --version', out: File::NULL)
+ warn(
+ 'Error: Yarn executable was not detected in the system.'.color(:red),
+ 'Download Yarn at https://yarnpkg.com/en/docs/install'.color(:green)
+ )
+ abort
+ end
+ end
+
+ desc 'Ensure Node dependencies are installed'
+ task check: ['yarn:available'] do
+ unless system('yarn check --ignore-engines', out: File::NULL)
+ warn(
+ 'Error: You have unmet dependencies. (`yarn check` command failed)'.color(:red),
+ 'Run `yarn install` to install missing modules.'.color(:green)
+ )
+ abort
+ end
+ end
+
+ desc 'Install Node dependencies with Yarn'
+ task install: ['yarn:available'] do
+ unless system('yarn install --pure-lockfile --ignore-engines')
+ abort 'Error: Unable to install node modules.'.color(:red)
+ end
+ end
+
+ desc 'Remove Node dependencies'
+ task :clobber do
+ warn 'Purging ./node_modules directory'.color(:red)
+ FileUtils.rm_rf 'node_modules'
+ end
+end
+
+desc 'Install Node dependencies with Yarn'
+task yarn: ['yarn:install']
diff --git a/package.json b/package.json
index 08bde1bc313..5528303ab21 100644
--- a/package.json
+++ b/package.json
@@ -15,27 +15,25 @@
"babel-loader": "^6.2.10",
"babel-preset-es2015": "^6.22.0",
"babel-preset-stage-2": "^6.22.0",
- "bootstrap-sass": "3.3.6",
+ "bootstrap-sass": "^3.3.6",
"compression-webpack-plugin": "^0.3.2",
- "d3": "3.5.11",
- "dropzone": "4.2.0",
+ "d3": "^3.5.11",
+ "dropzone": "^4.2.0",
"es6-promise": "^4.0.5",
- "imports-loader": "^0.6.5",
- "jquery": "2.2.1",
- "jquery-ui": "github:jquery/jquery-ui#1.11.4",
- "jquery-ujs": "1.2.1",
+ "jquery": "^2.2.1",
+ "jquery-ui": "git+https://github.com/jquery/jquery-ui#1.11.4",
+ "jquery-ujs": "^1.2.1",
"js-cookie": "^2.1.3",
- "karma-mocha-reporter": "^2.2.2",
- "mousetrap": "1.4.6",
+ "mousetrap": "^1.4.6",
"pikaday": "^1.5.1",
"select2": "3.5.2-browserify",
"stats-webpack-plugin": "^0.4.3",
"timeago.js": "^2.0.5",
- "underscore": "1.8.3",
- "vue": "2.0.3",
- "vue-resource": "0.9.3",
+ "underscore": "^1.8.3",
+ "vue": "^2.1.10",
+ "vue-resource": "^0.9.3",
"webpack": "^2.2.1",
- "webpack-dev-server": "^2.3.0"
+ "webpack-bundle-analyzer": "^2.3.0"
},
"devDependencies": {
"babel-plugin-istanbul": "^4.0.0",
@@ -51,9 +49,11 @@
"karma": "^1.4.1",
"karma-coverage-istanbul-reporter": "^0.2.0",
"karma-jasmine": "^1.1.0",
+ "karma-mocha-reporter": "^2.2.2",
"karma-phantomjs-launcher": "^1.0.2",
"karma-sourcemap-loader": "^0.3.7",
- "karma-webpack": "^2.0.2"
+ "karma-webpack": "^2.0.2",
+ "webpack-dev-server": "^2.3.0"
},
"nyc": {
"exclude": [
diff --git a/rubocop/cop/gem_fetcher.rb b/rubocop/cop/gem_fetcher.rb
index c199f6acab2..e157d8e0791 100644
--- a/rubocop/cop/gem_fetcher.rb
+++ b/rubocop/cop/gem_fetcher.rb
@@ -4,9 +4,9 @@ module RuboCop
# `Gemfile` in order to avoid additional points of failure beyond
# rubygems.org.
class GemFetcher < RuboCop::Cop::Cop
- MSG = 'Do not use gems from git repositories, only use gems from RubyGems.'
+ MSG = 'Do not use gems from git repositories, only use gems from RubyGems.'.freeze
- GIT_KEYS = [:git, :github]
+ GIT_KEYS = [:git, :github].freeze
def on_send(node)
return unless gemfile?(node)
diff --git a/rubocop/cop/migration/add_column.rb b/rubocop/cop/migration/add_column.rb
index 1490fcdd814..d2cf36c454a 100644
--- a/rubocop/cop/migration/add_column.rb
+++ b/rubocop/cop/migration/add_column.rb
@@ -8,10 +8,10 @@ module RuboCop
class AddColumn < RuboCop::Cop::Cop
include MigrationHelpers
- WHITELISTED_TABLES = [:application_settings]
+ WHITELISTED_TABLES = [:application_settings].freeze
MSG = '`add_column` with a default value requires downtime, ' \
- 'use `add_column_with_default` instead'
+ 'use `add_column_with_default` instead'.freeze
def on_send(node)
return unless in_migration?(node)
diff --git a/rubocop/cop/migration/add_column_with_default.rb b/rubocop/cop/migration/add_column_with_default.rb
index 747d7caf1ef..54a920d4b49 100644
--- a/rubocop/cop/migration/add_column_with_default.rb
+++ b/rubocop/cop/migration/add_column_with_default.rb
@@ -9,7 +9,7 @@ module RuboCop
include MigrationHelpers
MSG = '`add_column_with_default` is not reversible so you must manually define ' \
- 'the `up` and `down` methods in your migration class, using `remove_column` in `down`'
+ 'the `up` and `down` methods in your migration class, using `remove_column` in `down`'.freeze
def on_send(node)
return unless in_migration?(node)
diff --git a/rubocop/cop/migration/add_concurrent_foreign_key.rb b/rubocop/cop/migration/add_concurrent_foreign_key.rb
index e40a7087a47..d1fc94d55be 100644
--- a/rubocop/cop/migration/add_concurrent_foreign_key.rb
+++ b/rubocop/cop/migration/add_concurrent_foreign_key.rb
@@ -8,7 +8,7 @@ module RuboCop
class AddConcurrentForeignKey < RuboCop::Cop::Cop
include MigrationHelpers
- MSG = '`add_foreign_key` requires downtime, use `add_concurrent_foreign_key` instead'
+ MSG = '`add_foreign_key` requires downtime, use `add_concurrent_foreign_key` instead'.freeze
def on_send(node)
return unless in_migration?(node)
diff --git a/rubocop/cop/migration/add_index.rb b/rubocop/cop/migration/add_index.rb
index 5e6766f6994..fa21a0d6555 100644
--- a/rubocop/cop/migration/add_index.rb
+++ b/rubocop/cop/migration/add_index.rb
@@ -7,7 +7,7 @@ module RuboCop
class AddIndex < RuboCop::Cop::Cop
include MigrationHelpers
- MSG = '`add_index` requires downtime, use `add_concurrent_index` instead'
+ MSG = '`add_index` requires downtime, use `add_concurrent_index` instead'.freeze
def on_def(node)
return unless in_migration?(node)
diff --git a/spec/config/mail_room_spec.rb b/spec/config/mail_room_spec.rb
index 294fae95752..0b8ff006d22 100644
--- a/spec/config/mail_room_spec.rb
+++ b/spec/config/mail_room_spec.rb
@@ -8,7 +8,7 @@ describe 'mail_room.yml' do
context 'when incoming email is disabled' do
before do
- ENV['MAIL_ROOM_GITLAB_CONFIG_FILE'] = Rails.root.join('spec/fixtures/mail_room_disabled.yml').to_s
+ ENV['MAIL_ROOM_GITLAB_CONFIG_FILE'] = Rails.root.join('spec/fixtures/config/mail_room_disabled.yml').to_s
Gitlab::MailRoom.reset_config!
end
@@ -26,7 +26,7 @@ describe 'mail_room.yml' do
let(:gitlab_redis) { Gitlab::Redis.new(Rails.env) }
before do
- ENV['MAIL_ROOM_GITLAB_CONFIG_FILE'] = Rails.root.join('spec/fixtures/mail_room_enabled.yml').to_s
+ ENV['MAIL_ROOM_GITLAB_CONFIG_FILE'] = Rails.root.join('spec/fixtures/config/mail_room_enabled.yml').to_s
Gitlab::MailRoom.reset_config!
end
diff --git a/spec/controllers/admin/runners_controller_spec.rb b/spec/controllers/admin/runners_controller_spec.rb
new file mode 100644
index 00000000000..b5fe40d0510
--- /dev/null
+++ b/spec/controllers/admin/runners_controller_spec.rb
@@ -0,0 +1,85 @@
+require 'spec_helper'
+
+describe Admin::RunnersController do
+ let(:runner) { create(:ci_runner) }
+
+ before do
+ sign_in(create(:admin))
+ end
+
+ describe '#index' do
+ it 'lists all runners' do
+ get :index
+
+ expect(response).to have_http_status(200)
+ end
+ end
+
+ describe '#show' do
+ it 'shows a particular runner' do
+ get :show, id: runner.id
+
+ expect(response).to have_http_status(200)
+ end
+
+ it 'shows 404 for unknown runner' do
+ get :show, id: 0
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ describe '#update' do
+ it 'updates the runner and ticks the queue' do
+ new_desc = runner.description.swapcase
+
+ expect do
+ post :update, id: runner.id, runner: { description: new_desc }
+ end.to change { runner.ensure_runner_queue_value }
+
+ runner.reload
+
+ expect(response).to have_http_status(302)
+ expect(runner.description).to eq(new_desc)
+ end
+ end
+
+ describe '#destroy' do
+ it 'destroys the runner' do
+ delete :destroy, id: runner.id
+
+ expect(response).to have_http_status(302)
+ expect(Ci::Runner.find_by(id: runner.id)).to be_nil
+ end
+ end
+
+ describe '#resume' do
+ it 'marks the runner as active and ticks the queue' do
+ runner.update(active: false)
+
+ expect do
+ post :resume, id: runner.id
+ end.to change { runner.ensure_runner_queue_value }
+
+ runner.reload
+
+ expect(response).to have_http_status(302)
+ expect(runner.active).to eq(true)
+ end
+ end
+
+ describe '#pause' do
+ it 'marks the runner as inactive and ticks the queue' do
+ runner.update(active: true)
+
+ expect do
+ post :pause, id: runner.id
+ end.to change { runner.ensure_runner_queue_value }
+
+ runner.reload
+
+ expect(response).to have_http_status(302)
+ expect(runner.active).to eq(false)
+ end
+ end
+end
diff --git a/spec/controllers/blob_controller_spec.rb b/spec/controllers/blob_controller_spec.rb
index 2fcb4a6a528..44e011fd3a8 100644
--- a/spec/controllers/blob_controller_spec.rb
+++ b/spec/controllers/blob_controller_spec.rb
@@ -19,8 +19,8 @@ describe Projects::BlobController do
before do
get(:show,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ namespace_id: project.namespace,
+ project_id: project,
id: id)
end
@@ -50,8 +50,8 @@ describe Projects::BlobController do
before do
get(:show,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ namespace_id: project.namespace,
+ project_id: project,
id: id)
controller.instance_variable_set(:@blob, nil)
end
diff --git a/spec/controllers/dashboard/todos_controller_spec.rb b/spec/controllers/dashboard/todos_controller_spec.rb
index 79ef3a1adad..7072bd5e87c 100644
--- a/spec/controllers/dashboard/todos_controller_spec.rb
+++ b/spec/controllers/dashboard/todos_controller_spec.rb
@@ -1,16 +1,19 @@
require 'spec_helper'
describe Dashboard::TodosController do
+ include ApiHelpers
+
let(:user) { create(:user) }
+ let(:author) { 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
+ before do
+ sign_in(user)
+ project.team << [user, :developer]
+ end
+ describe 'GET #index' do
context 'when using pagination' do
let(:last_page) { user.todos.page.total_pages }
let!(:issues) { create_list(:issue, 2, project: project, assignee: user) }
@@ -34,4 +37,16 @@ describe Dashboard::TodosController do
end
end
end
+
+ describe 'PATCH #restore' do
+ let(:todo) { create(:todo, :done, user: user, project: project, author: author) }
+
+ it 'restores the todo to pending state' do
+ patch :restore, id: todo.id
+
+ expect(todo.reload).to be_pending
+ expect(response).to have_http_status(200)
+ expect(json_response).to eq({ "count" => "1", "done_count" => "0" })
+ end
+ end
end
diff --git a/spec/controllers/profiles/personal_access_tokens_spec.rb b/spec/controllers/profiles/personal_access_tokens_spec.rb
index 45534a3a587..9d5f4c99f6d 100644
--- a/spec/controllers/profiles/personal_access_tokens_spec.rb
+++ b/spec/controllers/profiles/personal_access_tokens_spec.rb
@@ -32,10 +32,10 @@ describe Profiles::PersonalAccessTokensController do
context "scopes" do
it "allows creation of a token with scopes" do
- post :create, personal_access_token: { name: FFaker::Product.brand, scopes: ['api', 'read_user'] }
+ post :create, personal_access_token: { name: FFaker::Product.brand, scopes: %w(api read_user) }
expect(created_token).not_to be_nil
- expect(created_token.scopes).to eq(['api', 'read_user'])
+ expect(created_token.scopes).to eq(%w(api read_user))
end
it "allows creation of a token with no scopes" do
diff --git a/spec/controllers/profiles/preferences_controller_spec.rb b/spec/controllers/profiles/preferences_controller_spec.rb
index 8f02003992a..7b3aa0491c7 100644
--- a/spec/controllers/profiles/preferences_controller_spec.rb
+++ b/spec/controllers/profiles/preferences_controller_spec.rb
@@ -25,8 +25,7 @@ describe Profiles::PreferencesController do
def go(params: {}, format: :js)
params.reverse_merge!(
color_scheme_id: '1',
- dashboard: 'stars',
- theme_id: '1'
+ dashboard: 'stars'
)
patch :update, user: params, format: format
@@ -41,8 +40,7 @@ describe Profiles::PreferencesController do
it "changes the user's preferences" do
prefs = {
color_scheme_id: '1',
- dashboard: 'stars',
- theme_id: '2'
+ dashboard: 'stars'
}.with_indifferent_access
expect(user).to receive(:update_attributes).with(prefs)
diff --git a/spec/controllers/projects/blame_controller_spec.rb b/spec/controllers/projects/blame_controller_spec.rb
index addc5e7ec33..c086b386381 100644
--- a/spec/controllers/projects/blame_controller_spec.rb
+++ b/spec/controllers/projects/blame_controller_spec.rb
@@ -16,8 +16,8 @@ describe Projects::BlameController do
before do
get(:show,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ namespace_id: project.namespace,
+ project_id: project,
id: id)
end
diff --git a/spec/controllers/projects/blob_controller_spec.rb b/spec/controllers/projects/blob_controller_spec.rb
index b36d0e69330..ec36a64b415 100644
--- a/spec/controllers/projects/blob_controller_spec.rb
+++ b/spec/controllers/projects/blob_controller_spec.rb
@@ -14,8 +14,8 @@ describe Projects::BlobController do
render_views
def do_get(opts = {})
- params = { namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ params = { namespace_id: project.namespace,
+ project_id: project,
id: 'master/CHANGELOG' }
get :diff, params.merge(opts)
end
@@ -40,8 +40,8 @@ describe Projects::BlobController do
describe 'PUT update' do
let(:default_params) do
{
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ namespace_id: project.namespace,
+ project_id: project,
id: 'master/CHANGELOG',
target_branch: 'master',
content: 'Added changes',
@@ -86,32 +86,47 @@ describe Projects::BlobController do
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"
- }
+ let(:forked_project_link) { create(:forked_project_link, forked_from_project: project) }
+ let!(:forked_project) { forked_project_link.forked_to_project }
+ let(:guest) { forked_project.owner }
+
+ before do
+ sign_in(guest)
+ end
+
+ context 'when editing on the fork' do
+ before do
+ default_params[:namespace_id] = forked_project.namespace
+ default_params[:project_id] = forked_project
+ end
+
+ it 'redirects to blob' do
+ put :update, default_params
+
+ expect(response).to redirect_to(namespace_project_blob_path(forked_project.namespace, forked_project, 'master/CHANGELOG'))
+ end
+ end
+
+ context 'when editing on the original repository' do
+ it "redirects to forked project new merge request" do
+ default_params[:target_branch] = "fork-test-1"
+ default_params[:create_merge_request] = 1
+
+ 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 ad15e3942a5..3d0533cb516 100644
--- a/spec/controllers/projects/boards/issues_controller_spec.rb
+++ b/spec/controllers/projects/boards/issues_controller_spec.rb
@@ -90,7 +90,7 @@ describe Projects::Boards::IssuesController do
params = {
namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ project_id: project,
board_id: board.to_param,
list_id: list.try(:to_param)
}
@@ -146,7 +146,7 @@ describe Projects::Boards::IssuesController do
sign_in(user)
post :create, namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ project_id: project,
board_id: board.to_param,
list_id: list.to_param,
issue: { title: title },
@@ -209,7 +209,7 @@ describe Projects::Boards::IssuesController do
sign_in(user)
patch :update, namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ project_id: project,
board_id: board.to_param,
id: issue.to_param,
from_list_id: from_list_id,
diff --git a/spec/controllers/projects/boards/lists_controller_spec.rb b/spec/controllers/projects/boards/lists_controller_spec.rb
index b3f9f76a50c..432f3c53c90 100644
--- a/spec/controllers/projects/boards/lists_controller_spec.rb
+++ b/spec/controllers/projects/boards/lists_controller_spec.rb
@@ -47,7 +47,7 @@ describe Projects::Boards::ListsController do
sign_in(user)
get :index, namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ project_id: project,
board_id: board.to_param,
format: :json
end
@@ -104,7 +104,7 @@ describe Projects::Boards::ListsController do
sign_in(user)
post :create, namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ project_id: project,
board_id: board.to_param,
list: { label_id: label_id },
format: :json
@@ -157,7 +157,7 @@ describe Projects::Boards::ListsController do
sign_in(user)
patch :update, namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ project_id: project,
board_id: board.to_param,
id: list.to_param,
list: { position: position },
@@ -200,7 +200,7 @@ describe Projects::Boards::ListsController do
sign_in(user)
delete :destroy, namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ project_id: project,
board_id: board.to_param,
id: list.to_param,
format: :json
@@ -244,7 +244,7 @@ describe Projects::Boards::ListsController do
sign_in(user)
post :generate, namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ project_id: project,
board_id: board.to_param,
format: :json
end
diff --git a/spec/controllers/projects/boards_controller_spec.rb b/spec/controllers/projects/boards_controller_spec.rb
index cc19035740e..aed3a45c413 100644
--- a/spec/controllers/projects/boards_controller_spec.rb
+++ b/spec/controllers/projects/boards_controller_spec.rb
@@ -50,8 +50,8 @@ describe Projects::BoardsController do
end
def list_boards(format: :html)
- get :index, namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ get :index, namespace_id: project.namespace,
+ project_id: project,
format: format
end
end
@@ -100,8 +100,8 @@ describe Projects::BoardsController do
end
def read_board(board:, format: :html)
- get :show, namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ get :show, namespace_id: project.namespace,
+ project_id: project,
id: board.to_param,
format: format
end
diff --git a/spec/controllers/projects/branches_controller_spec.rb b/spec/controllers/projects/branches_controller_spec.rb
index 9de03876755..e70737376af 100644
--- a/spec/controllers/projects/branches_controller_spec.rb
+++ b/spec/controllers/projects/branches_controller_spec.rb
@@ -22,8 +22,8 @@ describe Projects::BranchesController do
sign_in(user)
post :create,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ namespace_id: project.namespace,
+ project_id: project,
branch_name: branch,
ref: ref
end
@@ -68,7 +68,7 @@ describe Projects::BranchesController do
describe "created from the new branch button on issues" do
let(:branch) { "1-feature-branch" }
- let!(:issue) { create(:issue, project: project) }
+ let(:issue) { create(:issue, project: project) }
before do
sign_in(user)
@@ -76,8 +76,8 @@ describe Projects::BranchesController do
it 'redirects' do
post :create,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ namespace_id: project.namespace,
+ project_id: project,
branch_name: branch,
issue_iid: issue.iid
@@ -89,12 +89,49 @@ describe Projects::BranchesController do
expect(SystemNoteService).to receive(:new_issue_branch).with(issue, project, user, "1-feature-branch")
post :create,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ namespace_id: project.namespace,
+ project_id: project,
branch_name: branch,
issue_iid: issue.iid
end
+ context 'repository-less project' do
+ let(:project) { create :empty_project }
+
+ it 'redirects to newly created branch' do
+ result = { status: :success, branch: double(name: branch) }
+
+ expect_any_instance_of(CreateBranchService).to receive(:execute).and_return(result)
+ expect(SystemNoteService).to receive(:new_issue_branch).and_return(true)
+
+ post :create,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ branch_name: branch,
+ issue_iid: issue.iid
+
+ expect(response).to redirect_to namespace_project_tree_path(project.namespace, project, branch)
+ end
+
+ it 'redirects to autodeploy setup page' do
+ result = { status: :success, branch: double(name: branch) }
+
+ project.services << build(:kubernetes_service)
+
+ expect_any_instance_of(CreateBranchService).to receive(:execute).and_return(result)
+ expect(SystemNoteService).to receive(:new_issue_branch).and_return(true)
+
+ post :create,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ branch_name: branch,
+ issue_iid: issue.iid
+
+ expect(response.location).to include(namespace_project_new_blob_path(project.namespace, project, branch))
+ expect(response).to have_http_status(302)
+ end
+ end
+
context 'without issue feature access' do
before do
project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
@@ -106,8 +143,8 @@ describe Projects::BranchesController do
expect(SystemNoteService).not_to receive(:new_issue_branch)
post :create,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ namespace_id: project.namespace,
+ project_id: project,
branch_name: branch,
issue_iid: issue.iid
end
@@ -126,8 +163,8 @@ describe Projects::BranchesController do
post :destroy,
format: :html,
id: 'foo/bar/baz',
- namespace_id: project.namespace.to_param,
- project_id: project.to_param
+ namespace_id: project.namespace,
+ project_id: project
expect(response).to have_http_status(303)
end
@@ -142,8 +179,8 @@ describe Projects::BranchesController do
post :destroy,
format: :js,
id: branch,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param
+ namespace_id: project.namespace,
+ project_id: project
end
context "valid branch name, valid source" do
@@ -173,8 +210,8 @@ describe Projects::BranchesController do
describe "DELETE destroy_all_merged" do
def destroy_all_merged
delete :destroy_all_merged,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param
+ namespace_id: project.namespace,
+ project_id: project
end
context 'when user is allowed to push' do
diff --git a/spec/controllers/projects/commit_controller_spec.rb b/spec/controllers/projects/commit_controller_spec.rb
index ebd2d0e092b..640baa3a01c 100644
--- a/spec/controllers/projects/commit_controller_spec.rb
+++ b/spec/controllers/projects/commit_controller_spec.rb
@@ -17,8 +17,8 @@ describe Projects::CommitController do
def go(extra_params = {})
params = {
- namespace_id: project.namespace.to_param,
- project_id: project.to_param
+ namespace_id: project.namespace,
+ project_id: project
}
get :show, params.merge(extra_params)
@@ -125,8 +125,8 @@ describe Projects::CommitController do
it 'renders it' do
get(:show,
- namespace_id: fork_project.namespace.to_param,
- project_id: fork_project.to_param,
+ namespace_id: fork_project.namespace,
+ project_id: fork_project,
id: commit.id)
expect(response).to be_success
@@ -139,8 +139,8 @@ describe Projects::CommitController do
commit = project.commit('5937ac0a7beb003549fc5fd26fc247adbce4a52e')
get(:branches,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ namespace_id: project.namespace,
+ project_id: project,
id: commit.id)
expect(assigns(:branches)).to include("master", "feature_conflict")
@@ -152,8 +152,8 @@ describe Projects::CommitController do
context 'when target branch is not provided' do
it 'renders the 404 page' do
post(:revert,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ namespace_id: project.namespace,
+ project_id: project,
id: commit.id)
expect(response).not_to be_success
@@ -164,8 +164,8 @@ describe Projects::CommitController do
context 'when the revert was successful' do
it 'redirects to the commits page' do
post(:revert,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ namespace_id: project.namespace,
+ project_id: project,
target_branch: 'master',
id: commit.id)
@@ -177,8 +177,8 @@ describe Projects::CommitController do
context 'when the revert failed' do
before do
post(:revert,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ namespace_id: project.namespace,
+ project_id: project,
target_branch: 'master',
id: commit.id)
end
@@ -186,8 +186,8 @@ describe Projects::CommitController do
it 'redirects to the commit page' do
# Reverting a commit that has been already reverted.
post(:revert,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ namespace_id: project.namespace,
+ project_id: project,
target_branch: 'master',
id: commit.id)
@@ -201,8 +201,8 @@ describe Projects::CommitController do
context 'when target branch is not provided' do
it 'renders the 404 page' do
post(:cherry_pick,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ namespace_id: project.namespace,
+ project_id: project,
id: master_pickable_commit.id)
expect(response).not_to be_success
@@ -213,8 +213,8 @@ describe Projects::CommitController do
context 'when the cherry-pick was successful' do
it 'redirects to the commits page' do
post(:cherry_pick,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ namespace_id: project.namespace,
+ project_id: project,
target_branch: 'master',
id: master_pickable_commit.id)
@@ -226,8 +226,8 @@ describe Projects::CommitController do
context 'when the cherry_pick failed' do
before do
post(:cherry_pick,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ namespace_id: project.namespace,
+ project_id: project,
target_branch: 'master',
id: master_pickable_commit.id)
end
@@ -235,8 +235,8 @@ describe Projects::CommitController do
it 'redirects to the commit page' do
# Cherry-picking a commit that has been already cherry-picked.
post(:cherry_pick,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ namespace_id: project.namespace,
+ project_id: project,
target_branch: 'master',
id: master_pickable_commit.id)
@@ -249,8 +249,8 @@ describe Projects::CommitController do
describe 'GET diff_for_path' do
def diff_for_path(extra_params = {})
params = {
- namespace_id: project.namespace.to_param,
- project_id: project.to_param
+ namespace_id: project.namespace,
+ project_id: project
}
get :diff_for_path, params.merge(extra_params)
@@ -313,8 +313,8 @@ describe Projects::CommitController do
describe 'GET pipelines' do
def get_pipelines(extra_params = {})
params = {
- namespace_id: project.namespace.to_param,
- project_id: project.to_param
+ namespace_id: project.namespace,
+ project_id: project
}
get :pipelines, params.merge(extra_params)
diff --git a/spec/controllers/projects/commits_controller_spec.rb b/spec/controllers/projects/commits_controller_spec.rb
index 54b8d1108a5..e26731fb691 100644
--- a/spec/controllers/projects/commits_controller_spec.rb
+++ b/spec/controllers/projects/commits_controller_spec.rb
@@ -16,8 +16,8 @@ describe Projects::CommitsController do
context "when the ref does not exist with the suffix" do
it "renders as atom" do
get(:show,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ namespace_id: project.namespace,
+ project_id: project,
id: "master.atom")
expect(response).to be_success
@@ -33,8 +33,8 @@ describe Projects::CommitsController do
allow_any_instance_of(Repository).to receive(:commit).with('master.atom').and_return(commit)
get(:show,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ namespace_id: project.namespace,
+ project_id: project,
id: "master.atom")
end
diff --git a/spec/controllers/projects/compare_controller_spec.rb b/spec/controllers/projects/compare_controller_spec.rb
index e811c76fb31..15ac4e0925a 100644
--- a/spec/controllers/projects/compare_controller_spec.rb
+++ b/spec/controllers/projects/compare_controller_spec.rb
@@ -13,8 +13,8 @@ describe Projects::CompareController do
it 'compare shows some diffs' do
get(:show,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ namespace_id: project.namespace,
+ project_id: project,
from: ref_from,
to: ref_to)
@@ -25,8 +25,8 @@ describe Projects::CompareController do
it 'compare shows some diffs with ignore whitespace change option' do
get(:show,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ namespace_id: project.namespace,
+ project_id: project,
from: '08f22f25',
to: '66eceea0',
w: 1)
@@ -43,8 +43,8 @@ describe Projects::CompareController do
describe 'non-existent refs' do
it 'uses invalid source ref' do
get(:show,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ namespace_id: project.namespace,
+ project_id: project,
from: 'non-existent',
to: ref_to)
@@ -55,8 +55,8 @@ describe Projects::CompareController do
it 'uses invalid target ref' do
get(:show,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ namespace_id: project.namespace,
+ project_id: project,
from: ref_from,
to: 'non-existent')
@@ -67,8 +67,8 @@ describe Projects::CompareController do
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,
+ namespace_id: project.namespace,
+ project_id: project,
from: '',
to: 'master')
@@ -77,8 +77,8 @@ describe Projects::CompareController do
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,
+ namespace_id: project.namespace,
+ project_id: project,
from: 'master',
to: '')
@@ -87,8 +87,8 @@ describe Projects::CompareController do
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,
+ namespace_id: project.namespace,
+ project_id: project,
from: '',
to: '')
@@ -99,8 +99,8 @@ describe Projects::CompareController do
describe 'GET diff_for_path' do
def diff_for_path(extra_params = {})
params = {
- namespace_id: project.namespace.to_param,
- project_id: project.to_param
+ namespace_id: project.namespace,
+ project_id: project
}
get :diff_for_path, params.merge(extra_params)
diff --git a/spec/controllers/projects/cycle_analytics_controller_spec.rb b/spec/controllers/projects/cycle_analytics_controller_spec.rb
index 6a6d71a16ee..6fae52edbad 100644
--- a/spec/controllers/projects/cycle_analytics_controller_spec.rb
+++ b/spec/controllers/projects/cycle_analytics_controller_spec.rb
@@ -13,8 +13,8 @@ describe Projects::CycleAnalyticsController do
context 'with no data' do
it 'is true' do
get(:show,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param)
+ namespace_id: project.namespace,
+ project_id: project)
expect(response).to be_success
expect(assigns(:cycle_analytics_no_data)).to eq(true)
@@ -32,8 +32,8 @@ describe Projects::CycleAnalyticsController do
it 'is false' do
get(:show,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param)
+ namespace_id: project.namespace,
+ project_id: project)
expect(response).to be_success
expect(assigns(:cycle_analytics_no_data)).to eq(false)
diff --git a/spec/controllers/projects/find_file_controller_spec.rb b/spec/controllers/projects/find_file_controller_spec.rb
index a4884256c92..6a5433bcc9c 100644
--- a/spec/controllers/projects/find_file_controller_spec.rb
+++ b/spec/controllers/projects/find_file_controller_spec.rb
@@ -17,8 +17,8 @@ describe Projects::FindFileController do
before do
get(:show,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ namespace_id: project.namespace,
+ project_id: project,
id: id)
end
@@ -36,8 +36,8 @@ describe Projects::FindFileController do
describe "GET #list" do
def go(format: 'json')
get :list,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ namespace_id: project.namespace,
+ project_id: project,
id: id,
format: format
end
diff --git a/spec/controllers/projects/forks_controller_spec.rb b/spec/controllers/projects/forks_controller_spec.rb
index a867668d97b..8282d79298f 100644
--- a/spec/controllers/projects/forks_controller_spec.rb
+++ b/spec/controllers/projects/forks_controller_spec.rb
@@ -9,8 +9,8 @@ describe Projects::ForksController do
describe 'GET index' do
def get_forks
get :index,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param
+ namespace_id: project.namespace,
+ project_id: project
end
context 'when fork is public' do
@@ -71,8 +71,8 @@ describe Projects::ForksController do
describe 'GET new' do
def get_new
get :new,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param
+ namespace_id: project.namespace,
+ project_id: project
end
context 'when user is signed in' do
@@ -99,8 +99,8 @@ describe Projects::ForksController do
describe 'POST create' do
def post_create
post :create,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ namespace_id: project.namespace,
+ project_id: project,
namespace_key: user.namespace.id
end
diff --git a/spec/controllers/projects/graphs_controller_spec.rb b/spec/controllers/projects/graphs_controller_spec.rb
index bbe8e4bf6b2..c4a7aa7d63e 100644
--- a/spec/controllers/projects/graphs_controller_spec.rb
+++ b/spec/controllers/projects/graphs_controller_spec.rb
@@ -34,7 +34,7 @@ describe Projects::GraphsController do
end
it 'sets the correct colour according to language' do
- get(:languages, namespace_id: project.namespace.path, project_id: project.path, id: 'master')
+ get(:languages, namespace_id: project.namespace, project_id: project, id: 'master')
expected_values.each do |val|
expect(assigns(:languages)).to include(a_hash_including(val))
diff --git a/spec/controllers/projects/group_links_controller_spec.rb b/spec/controllers/projects/group_links_controller_spec.rb
index a976a9c27ab..ca4a8e871c0 100644
--- a/spec/controllers/projects/group_links_controller_spec.rb
+++ b/spec/controllers/projects/group_links_controller_spec.rb
@@ -14,8 +14,8 @@ describe Projects::GroupLinksController do
describe '#create' do
shared_context 'link project to group' do
before do
- post(:create, namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ post(:create, namespace_id: project.namespace,
+ project_id: project,
link_group_id: group.id,
link_group_access: ProjectGroupLink.default_access)
end
@@ -50,8 +50,8 @@ describe Projects::GroupLinksController do
context 'when project group id equal link group id' do
before do
- post(:create, namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ post(:create, namespace_id: project.namespace,
+ project_id: project,
link_group_id: group2.id,
link_group_access: ProjectGroupLink.default_access)
end
@@ -69,8 +69,8 @@ describe Projects::GroupLinksController do
context 'when link group id is not present' do
before do
- post(:create, namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ post(:create, namespace_id: project.namespace,
+ project_id: project,
link_group_access: ProjectGroupLink.default_access)
end
diff --git a/spec/controllers/projects/imports_controller_spec.rb b/spec/controllers/projects/imports_controller_spec.rb
index 2acbba469e3..7c75815f3c4 100644
--- a/spec/controllers/projects/imports_controller_spec.rb
+++ b/spec/controllers/projects/imports_controller_spec.rb
@@ -13,13 +13,13 @@ describe Projects::ImportsController do
end
it 'renders template' do
- get :show, namespace_id: project.namespace.to_param, project_id: project.to_param
+ get :show, namespace_id: project.namespace.to_param, project_id: project
expect(response).to render_template :show
end
it 'sets flash.now if params is present' do
- get :show, namespace_id: project.namespace.to_param, project_id: project.to_param, continue: { to: '/', notice_now: 'Started' }
+ get :show, namespace_id: project.namespace.to_param, project_id: project, continue: { to: '/', notice_now: 'Started' }
expect(flash.now[:notice]).to eq 'Started'
end
@@ -39,13 +39,13 @@ describe Projects::ImportsController do
end
it 'renders template' do
- get :show, namespace_id: project.namespace.to_param, project_id: project.to_param
+ get :show, namespace_id: project.namespace.to_param, project_id: project
expect(response).to render_template :show
end
it 'sets flash.now if params is present' do
- get :show, namespace_id: project.namespace.to_param, project_id: project.to_param, continue: { to: '/', notice_now: 'In progress' }
+ get :show, namespace_id: project.namespace.to_param, project_id: project, continue: { to: '/', notice_now: 'In progress' }
expect(flash.now[:notice]).to eq 'In progress'
end
@@ -57,7 +57,7 @@ describe Projects::ImportsController do
end
it 'redirects to new_namespace_project_import_path' do
- get :show, namespace_id: project.namespace.to_param, project_id: project.to_param
+ get :show, namespace_id: project.namespace.to_param, project_id: project
expect(response).to redirect_to new_namespace_project_import_path(project.namespace, project)
end
@@ -72,7 +72,7 @@ describe Projects::ImportsController do
it 'redirects to namespace_project_path' do
allow_any_instance_of(Project).to receive(:forked?).and_return(true)
- get :show, namespace_id: project.namespace.to_param, project_id: project.to_param
+ get :show, namespace_id: project.namespace.to_param, project_id: project
expect(flash[:notice]).to eq 'The project was successfully forked.'
expect(response).to redirect_to namespace_project_path(project.namespace, project)
@@ -81,7 +81,7 @@ describe Projects::ImportsController do
context 'when project is external' do
it 'redirects to namespace_project_path' do
- get :show, namespace_id: project.namespace.to_param, project_id: project.to_param
+ get :show, namespace_id: project.namespace.to_param, project_id: project
expect(flash[:notice]).to eq 'The project was successfully imported.'
expect(response).to redirect_to namespace_project_path(project.namespace, project)
@@ -97,7 +97,7 @@ describe Projects::ImportsController do
end
it 'redirects to params[:to]' do
- get :show, namespace_id: project.namespace.to_param, project_id: project.to_param, continue: params
+ get :show, namespace_id: project.namespace.to_param, project_id: project, continue: params
expect(flash[:notice]).to eq params[:notice]
expect(response).to redirect_to params[:to]
@@ -111,7 +111,7 @@ describe Projects::ImportsController do
end
it 'redirects to namespace_project_path' do
- get :show, namespace_id: project.namespace.to_param, project_id: project.to_param
+ get :show, namespace_id: project.namespace.to_param, project_id: project
expect(response).to redirect_to namespace_project_path(project.namespace, project)
end
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index e576bf9ef79..46c758b4654 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -12,7 +12,7 @@ describe Projects::IssuesController do
allow(project).to receive(:external_issue_tracker).and_return(external)
controller.instance_variable_set(:@project, project)
- get :index, namespace_id: project.namespace.path, project_id: project
+ get :index, namespace_id: project.namespace, project_id: project
expect(response).to redirect_to('https://example.com/project')
end
@@ -27,13 +27,13 @@ describe Projects::IssuesController do
it_behaves_like "issuables list meta-data", :issue
it "returns index" do
- get :index, namespace_id: project.namespace.path, project_id: project.path
+ get :index, namespace_id: project.namespace, project_id: project
expect(response).to have_http_status(200)
end
it "returns 301 if request path doesn't match project path" do
- get :index, namespace_id: project.namespace.path, project_id: project.path.upcase
+ get :index, namespace_id: project.namespace, project_id: project.path.upcase
expect(response).to redirect_to(namespace_project_issues_path(project.namespace, project))
end
@@ -42,7 +42,7 @@ describe Projects::IssuesController do
project.issues_enabled = false
project.save
- get :index, namespace_id: project.namespace.path, project_id: project.path
+ get :index, namespace_id: project.namespace, project_id: project
expect(response).to have_http_status(404)
end
@@ -50,7 +50,7 @@ describe Projects::IssuesController do
controller.instance_variable_set(:@project, project)
allow(project).to receive(:default_issues_tracker?).and_return(false)
- get :index, namespace_id: project.namespace.path, project_id: project.path
+ get :index, namespace_id: project.namespace, project_id: project
expect(response).to have_http_status(404)
end
end
@@ -67,8 +67,8 @@ describe Projects::IssuesController do
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,
+ namespace_id: project.namespace.to_param,
+ project_id: project,
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]))
@@ -76,8 +76,8 @@ describe Projects::IssuesController do
it 'redirects to specified page' do
get :index,
- namespace_id: project.namespace.path.to_param,
- project_id: project.path.to_param,
+ namespace_id: project.namespace.to_param,
+ project_id: project,
page: last_page.to_param
expect(assigns(:issues).current_page).to eq(last_page)
@@ -94,7 +94,7 @@ describe Projects::IssuesController do
end
it 'builds a new issue' do
- get :new, namespace_id: project.namespace.path, project_id: project
+ get :new, namespace_id: project.namespace, project_id: project
expect(assigns(:issue)).to be_a_new(Issue)
end
@@ -104,7 +104,7 @@ describe Projects::IssuesController do
project_with_repository.team << [user, :developer]
mr = create(:merge_request_with_diff_notes, source_project: project_with_repository)
- get :new, namespace_id: project_with_repository.namespace.path, project_id: project_with_repository, merge_request_for_resolving_discussions: mr.iid
+ get :new, namespace_id: project_with_repository.namespace, project_id: project_with_repository, merge_request_for_resolving_discussions: mr.iid
expect(assigns(:issue).title).not_to be_empty
expect(assigns(:issue).description).not_to be_empty
@@ -117,7 +117,7 @@ describe Projects::IssuesController do
allow(project).to receive(:external_issue_tracker).and_return(external)
controller.instance_variable_set(:@project, project)
- get :new, namespace_id: project.namespace.path, project_id: project
+ get :new, namespace_id: project.namespace, project_id: project
expect(response).to redirect_to('https://example.com/issues/new')
end
@@ -125,14 +125,16 @@ describe Projects::IssuesController do
end
describe 'PUT #update' do
+ before do
+ sign_in(user)
+ project.team << [user, :developer]
+ end
+
+ it_behaves_like 'update invalid issuable', Issue
+
context 'when moving issue to another private project' do
let(:another_project) { create(:empty_project, :private) }
- before do
- sign_in(user)
- project.team << [user, :developer]
- end
-
context 'when user has access to move issue' do
before { another_project.team << [user, :reporter] }
@@ -152,10 +154,117 @@ describe Projects::IssuesController do
end
end
+ context 'Akismet is enabled' do
+ let(:project) { create(:project_empty_repo, :public) }
+
+ before do
+ stub_application_setting(recaptcha_enabled: true)
+ allow_any_instance_of(SpamService).to receive(:check_for_spam?).and_return(true)
+ end
+
+ context 'when an issue is not identified as 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 'normally updates the issue' do
+ expect { update_issue(title: 'Foo') }.to change { issue.reload.title }.to('Foo')
+ end
+ end
+
+ context 'when an issue is identified as spam' do
+ before { allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true) }
+
+ context 'when captcha is not verified' do
+ def update_spam_issue
+ update_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 { update_spam_issue }.not_to change{ issue.reload.title }
+ end
+
+ it 'rejects an issue recognized as a spam when recaptcha disabled' do
+ stub_application_setting(recaptcha_enabled: false)
+
+ expect { update_spam_issue }.not_to change{ issue.reload.title }
+ end
+
+ it 'creates a spam log' do
+ update_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 'renders verify template' do
+ update_spam_issue
+
+ expect(response).to render_template(:verify)
+ end
+ end
+
+ context 'when captcha is verified' do
+ let(:spammy_title) { 'Whatever' }
+ let!(:spam_logs) { create_list(:spam_log, 2, user: user, title: spammy_title) }
+
+ def update_verified_issue
+ update_issue({ title: spammy_title },
+ { 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 'redirect to issue page' do
+ update_verified_issue
+
+ expect(response).
+ to redirect_to(namespace_project_issue_path(project.namespace, project, issue))
+ end
+
+ it 'accepts an issue after recaptcha is verified' do
+ expect{ update_verified_issue }.to change{ issue.reload.title }.to(spammy_title)
+ end
+
+ it 'marks spam log as recaptcha_verified' do
+ expect { update_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 { update_issue(spam_log_id: spam_log.id, recaptcha_verification: true) }.
+ not_to change { SpamLog.last.recaptcha_verified }
+ end
+ end
+ end
+ end
+
+ def update_issue(issue_params = {}, additional_params = {})
+ params = {
+ namespace_id: project.namespace.to_param,
+ project_id: project,
+ id: issue.iid,
+ issue: issue_params
+ }.merge(additional_params)
+
+ put :update, params
+ end
+
def move_issue
put :update,
namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ project_id: project,
id: issue.iid,
issue: { title: 'New title' },
move_to_project_id: another_project.id
@@ -235,7 +344,7 @@ describe Projects::IssuesController do
def get_issues
get :index,
namespace_id: project.namespace.to_param,
- project_id: project.to_param
+ project_id: project
end
end
@@ -298,7 +407,7 @@ describe Projects::IssuesController do
def go(id:)
get :show,
namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ project_id: project,
id: id
end
end
@@ -309,7 +418,7 @@ describe Projects::IssuesController do
def go(id:)
get :edit,
namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ project_id: project,
id: id
end
end
@@ -320,7 +429,7 @@ describe Projects::IssuesController do
def go(id:)
put :update,
namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ project_id: project,
id: id,
issue: { title: 'New title' }
end
@@ -335,7 +444,7 @@ describe Projects::IssuesController do
post :create, {
namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ project_id: project,
issue: { title: 'Title', description: 'Description' }.merge(issue_attrs)
}.merge(additional_params)
@@ -357,7 +466,7 @@ describe Projects::IssuesController do
end
def post_issue(issue_params)
- post :create, namespace_id: project.namespace.to_param, project_id: project.to_param, issue: issue_params, merge_request_for_resolving_discussions: merge_request.iid
+ post :create, namespace_id: project.namespace.to_param, project_id: project, issue: issue_params, merge_request_for_resolving_discussions: merge_request.iid
end
it 'creates an issue for the project' do
@@ -384,7 +493,7 @@ describe Projects::IssuesController do
allow_any_instance_of(SpamService).to receive(:check_for_spam?).and_return(true)
end
- context 'when an issue is not identified as a spam' do
+ context 'when an issue is not identified as 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)
@@ -395,7 +504,7 @@ describe Projects::IssuesController do
end
end
- context 'when an issue is identified as a spam' do
+ context 'when an issue is identified as spam' do
before { allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true) }
context 'when captcha is not verified' do
@@ -500,8 +609,8 @@ describe Projects::IssuesController do
project.team << [admin, :master]
sign_in(admin)
post :mark_as_spam, {
- namespace_id: project.namespace.path,
- project_id: project.path,
+ namespace_id: project.namespace,
+ project_id: project,
id: issue.iid
}
end
@@ -517,7 +626,7 @@ describe Projects::IssuesController do
context "when the user is a developer" do
before { sign_in(user) }
it "rejects a developer to destroy an issue" do
- delete :destroy, namespace_id: project.namespace.path, project_id: project.path, id: issue.iid
+ delete :destroy, namespace_id: project.namespace, project_id: project, id: issue.iid
expect(response).to have_http_status(404)
end
end
@@ -530,7 +639,7 @@ describe Projects::IssuesController do
before { sign_in(owner) }
it "deletes the issue" do
- delete :destroy, namespace_id: project.namespace.path, project_id: project.path, id: issue.iid
+ delete :destroy, namespace_id: project.namespace, project_id: project, id: issue.iid
expect(response).to have_http_status(302)
expect(controller).to set_flash[:notice].to(/The issue was successfully deleted\./).now
@@ -539,7 +648,7 @@ describe Projects::IssuesController do
it 'delegates the update of the todos count cache to TodoService' do
expect_any_instance_of(TodoService).to receive(:destroy_issue).with(issue, owner).once
- delete :destroy, namespace_id: project.namespace.path, project_id: project.path, id: issue.iid
+ delete :destroy, namespace_id: project.namespace, project_id: project, id: issue.iid
end
end
end
@@ -552,8 +661,8 @@ describe Projects::IssuesController do
it "toggles the award emoji" do
expect do
- post(:toggle_award_emoji, namespace_id: project.namespace.path,
- project_id: project.path, id: issue.iid, name: "thumbsup")
+ post(:toggle_award_emoji, namespace_id: project.namespace,
+ project_id: project, id: issue.iid, name: "thumbsup")
end.to change { issue.award_emoji.count }.by(1)
expect(response).to have_http_status(200)
diff --git a/spec/controllers/projects/labels_controller_spec.rb b/spec/controllers/projects/labels_controller_spec.rb
index 3e0326dd47d..6a6e9bf378a 100644
--- a/spec/controllers/projects/labels_controller_spec.rb
+++ b/spec/controllers/projects/labels_controller_spec.rb
@@ -67,7 +67,7 @@ describe Projects::LabelsController do
end
def list_labels
- get :index, namespace_id: project.namespace.to_param, project_id: project.to_param
+ get :index, namespace_id: project.namespace.to_param, project_id: project
end
end
@@ -76,7 +76,7 @@ describe Projects::LabelsController do
let(:personal_project) { create(:empty_project, namespace: user.namespace) }
it 'creates labels' do
- post :generate, namespace_id: personal_project.namespace.to_param, project_id: personal_project.to_param
+ post :generate, namespace_id: personal_project.namespace.to_param, project_id: personal_project
expect(response).to have_http_status(302)
end
@@ -84,7 +84,7 @@ describe Projects::LabelsController do
context 'project belonging to a group' do
it 'creates labels' do
- post :generate, namespace_id: project.namespace.to_param, project_id: project.to_param
+ post :generate, namespace_id: project.namespace.to_param, project_id: project
expect(response).to have_http_status(302)
end
@@ -109,7 +109,7 @@ describe Projects::LabelsController do
end
def toggle_subscription(label)
- post :toggle_subscription, namespace_id: project.namespace.to_param, project_id: project.to_param, id: label.to_param
+ post :toggle_subscription, namespace_id: project.namespace.to_param, project_id: project, id: label.to_param
end
end
@@ -119,7 +119,7 @@ describe Projects::LabelsController do
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
+ post :promote, namespace_id: project.namespace.to_param, project_id: project, id: label_1.to_param
expect(response).to have_http_status(404)
end
@@ -131,13 +131,13 @@ describe Projects::LabelsController do
end
it 'gives access' do
- post :promote, namespace_id: project.namespace.to_param, project_id: project.to_param, id: label_1.to_param
+ post :promote, namespace_id: project.namespace.to_param, project_id: project, 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
+ post :promote, namespace_id: project.namespace.to_param, project_id: project, 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
@@ -151,7 +151,7 @@ describe Projects::LabelsController do
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
+ post :promote, namespace_id: project.namespace.to_param, project_id: project, id: label_1.to_param
expect(response).to redirect_to(namespace_project_labels_path)
end
end
diff --git a/spec/controllers/projects/mattermosts_controller_spec.rb b/spec/controllers/projects/mattermosts_controller_spec.rb
index cae733f0cfb..c5abf11cfa5 100644
--- a/spec/controllers/projects/mattermosts_controller_spec.rb
+++ b/spec/controllers/projects/mattermosts_controller_spec.rb
@@ -18,7 +18,7 @@ describe Projects::MattermostsController do
it 'accepts the request' do
get(:new,
namespace_id: project.namespace.to_param,
- project_id: project.to_param)
+ project_id: project)
expect(response).to have_http_status(200)
end
@@ -30,7 +30,7 @@ describe Projects::MattermostsController do
subject do
post(:create,
namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ project_id: project,
mattermost: mattermost_params)
end
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index f84f922ba5e..1ced666bb36 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -51,10 +51,11 @@ describe Projects::MergeRequestsController do
def submit_new_merge_request(format: :html)
get :new,
namespace_id: fork_project.namespace.to_param,
- project_id: fork_project.to_param,
+ project_id: fork_project,
merge_request: {
source_branch: 'remove-submodule',
- target_branch: 'master' },
+ target_branch: 'master'
+ },
format: format
end
end
@@ -63,7 +64,7 @@ describe Projects::MergeRequestsController do
it "loads labels into the @labels variable" do
get action,
namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ project_id: project,
id: merge_request.iid,
format: 'html'
expect(assigns(:labels)).not_to be_nil
@@ -75,7 +76,7 @@ describe Projects::MergeRequestsController do
it "does generally work" do
get(:show,
namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ project_id: project,
id: merge_request.iid,
format: format)
@@ -89,7 +90,7 @@ describe Projects::MergeRequestsController do
get(:show,
namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ project_id: project,
id: merge_request.iid,
format: format)
end
@@ -97,7 +98,7 @@ describe Projects::MergeRequestsController do
it "renders it" do
get(:show,
namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ project_id: project,
id: merge_request.iid,
format: format)
@@ -110,7 +111,7 @@ describe Projects::MergeRequestsController do
get(:show,
namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ project_id: project,
id: merge_request.iid,
format: format)
@@ -125,7 +126,7 @@ describe Projects::MergeRequestsController do
it "triggers workhorse to serve the request" do
get(:show,
namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ project_id: project,
id: merge_request.iid,
format: :diff)
@@ -137,7 +138,7 @@ describe Projects::MergeRequestsController do
it 'triggers workhorse to serve the request' do
get(:show,
namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ project_id: project,
id: merge_request.iid,
format: :patch)
@@ -152,7 +153,7 @@ describe Projects::MergeRequestsController do
def get_merge_requests(page = nil)
get :index,
namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ project_id: project,
state: 'opened', page: page.to_param
end
@@ -215,8 +216,8 @@ describe Projects::MergeRequestsController do
it 'closes MR without errors' do
post :update,
- namespace_id: project.namespace.path,
- project_id: project.path,
+ namespace_id: project.namespace,
+ project_id: project,
id: merge_request.iid,
merge_request: {
state_event: 'close'
@@ -230,8 +231,8 @@ describe Projects::MergeRequestsController do
merge_request.close!
put :update,
- namespace_id: project.namespace.path,
- project_id: project.path,
+ namespace_id: project.namespace,
+ project_id: project,
id: merge_request.iid,
merge_request: {
title: 'New title'
@@ -245,8 +246,8 @@ describe Projects::MergeRequestsController do
merge_request.close!
put :update,
- namespace_id: project.namespace.path,
- project_id: project.path,
+ namespace_id: project.namespace,
+ project_id: project,
id: merge_request.iid,
merge_request: {
target_branch: 'new_branch'
@@ -254,14 +255,16 @@ describe Projects::MergeRequestsController do
expect { merge_request.reload.target_branch }.not_to change { merge_request.target_branch }
end
+
+ it_behaves_like 'update invalid issuable', MergeRequest
end
end
describe 'POST merge' do
let(:base_params) do
{
- namespace_id: project.namespace.path,
- project_id: project.path,
+ namespace_id: project.namespace,
+ project_id: project,
id: merge_request.iid,
format: 'raw'
}
@@ -425,7 +428,7 @@ describe Projects::MergeRequestsController do
describe "DELETE destroy" do
it "denies access to users unless they're admin or project owner" do
- delete :destroy, namespace_id: project.namespace.path, project_id: project.path, id: merge_request.iid
+ delete :destroy, namespace_id: project.namespace, project_id: project, id: merge_request.iid
expect(response).to have_http_status(404)
end
@@ -438,7 +441,7 @@ describe Projects::MergeRequestsController do
before { sign_in owner }
it "deletes the merge request" do
- delete :destroy, namespace_id: project.namespace.path, project_id: project.path, id: merge_request.iid
+ delete :destroy, namespace_id: project.namespace, project_id: project, id: merge_request.iid
expect(response).to have_http_status(302)
expect(controller).to set_flash[:notice].to(/The merge request was successfully deleted\./).now
@@ -447,7 +450,7 @@ describe Projects::MergeRequestsController do
it 'delegates the update of the todos count cache to TodoService' do
expect_any_instance_of(TodoService).to receive(:destroy_merge_request).with(merge_request, owner).once
- delete :destroy, namespace_id: project.namespace.path, project_id: project.path, id: merge_request.iid
+ delete :destroy, namespace_id: project.namespace, project_id: project, id: merge_request.iid
end
end
end
@@ -456,7 +459,7 @@ describe Projects::MergeRequestsController do
def go(extra_params = {})
params = {
namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ project_id: project,
id: merge_request.iid
}
@@ -536,7 +539,7 @@ describe Projects::MergeRequestsController do
def diff_for_path(extra_params = {})
params = {
namespace_id: project.namespace.to_param,
- project_id: project.to_param
+ project_id: project
}
get :diff_for_path, params.merge(extra_params)
@@ -600,7 +603,7 @@ describe Projects::MergeRequestsController do
before do
other_project.team << [user, :master]
- diff_for_path(id: merge_request.iid, old_path: existing_path, new_path: existing_path, project_id: other_project.to_param)
+ diff_for_path(id: merge_request.iid, old_path: existing_path, new_path: existing_path, project_id: other_project)
end
it 'returns a 404' do
@@ -666,7 +669,7 @@ describe Projects::MergeRequestsController do
def go(format: 'html')
get :commits,
namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ project_id: project,
id: merge_request.iid,
format: format
end
@@ -706,7 +709,7 @@ describe Projects::MergeRequestsController do
before do
get :pipelines,
namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ project_id: project,
id: merge_request.iid,
format: :json
end
@@ -725,7 +728,7 @@ describe Projects::MergeRequestsController do
get :conflicts,
namespace_id: merge_request_with_conflicts.project.namespace.to_param,
- project_id: merge_request_with_conflicts.project.to_param,
+ project_id: merge_request_with_conflicts.project,
id: merge_request_with_conflicts.iid,
format: 'json'
end
@@ -743,7 +746,7 @@ describe Projects::MergeRequestsController do
before do
get :conflicts,
namespace_id: merge_request_with_conflicts.project.namespace.to_param,
- project_id: merge_request_with_conflicts.project.to_param,
+ project_id: merge_request_with_conflicts.project,
id: merge_request_with_conflicts.iid,
format: 'json'
end
@@ -772,7 +775,7 @@ describe Projects::MergeRequestsController do
section['lines'].each do |line|
if section['conflict']
- expect(line['type']).to be_in(['old', 'new'])
+ expect(line['type']).to be_in(%w(old new))
expect(line.values_at('old_line', 'new_line')).to contain_exactly(nil, a_kind_of(Integer))
else
if line['type'].nil?
@@ -806,7 +809,7 @@ describe Projects::MergeRequestsController do
post :remove_wip,
namespace_id: merge_request.project.namespace.to_param,
- project_id: merge_request.project.to_param,
+ project_id: merge_request.project,
id: merge_request.iid
expect(merge_request.reload.title).to eq(merge_request.wipless_title)
@@ -817,7 +820,7 @@ describe Projects::MergeRequestsController do
def conflict_for_path(path)
get :conflict_for_path,
namespace_id: merge_request_with_conflicts.project.namespace.to_param,
- project_id: merge_request_with_conflicts.project.to_param,
+ project_id: merge_request_with_conflicts.project,
id: merge_request_with_conflicts.iid,
old_path: path,
new_path: path,
@@ -873,7 +876,7 @@ describe Projects::MergeRequestsController do
def resolve_conflicts(files)
post :resolve_conflicts,
namespace_id: merge_request_with_conflicts.project.namespace.to_param,
- project_id: merge_request_with_conflicts.project.to_param,
+ project_id: merge_request_with_conflicts.project,
id: merge_request_with_conflicts.iid,
format: 'json',
files: files,
@@ -1024,7 +1027,7 @@ describe Projects::MergeRequestsController do
post :assign_related_issues,
namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ project_id: project,
id: merge_request.iid
end
@@ -1079,7 +1082,7 @@ describe Projects::MergeRequestsController do
get :ci_environments_status,
namespace_id: merge_request.project.namespace.to_param,
- project_id: merge_request.project.to_param,
+ project_id: merge_request.project,
id: merge_request.iid, format: 'json'
end
@@ -1092,8 +1095,8 @@ describe Projects::MergeRequestsController do
describe 'GET merge_widget_refresh' do
let(:params) do
{
- namespace_id: project.namespace.path,
- project_id: project.path,
+ namespace_id: project.namespace,
+ project_id: project,
id: merge_request.iid,
format: :raw
}
diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb
index 1ed2ee3ab4a..04bb5cbbd59 100644
--- a/spec/controllers/projects/pipelines_controller_spec.rb
+++ b/spec/controllers/projects/pipelines_controller_spec.rb
@@ -12,10 +12,13 @@ describe Projects::PipelinesController do
describe 'GET index.json' do
before do
- create_list(:ci_empty_pipeline, 2, project: project)
+ create(:ci_empty_pipeline, status: 'pending', project: project)
+ create(:ci_empty_pipeline, status: 'running', project: project)
+ create(:ci_empty_pipeline, status: 'created', project: project)
+ create(:ci_empty_pipeline, status: 'success', project: project)
- get :index, namespace_id: project.namespace.path,
- project_id: project.path,
+ get :index, namespace_id: project.namespace,
+ project_id: project,
format: :json
end
@@ -23,9 +26,11 @@ describe Projects::PipelinesController 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
+ expect(json_response['pipelines'].count).to eq 4
+ expect(json_response['count']['all']).to eq 4
+ expect(json_response['count']['running']).to eq 1
+ expect(json_response['count']['pending']).to eq 1
+ expect(json_response['count']['finished']).to eq 1
end
end
@@ -57,8 +62,8 @@ describe Projects::PipelinesController do
end
def get_stage(name)
- get :stage, namespace_id: project.namespace.path,
- project_id: project.path,
+ get :stage, namespace_id: project.namespace,
+ project_id: project,
id: pipeline.id,
stage: name,
format: :json
diff --git a/spec/controllers/projects/protected_branches_controller_spec.rb b/spec/controllers/projects/protected_branches_controller_spec.rb
index da6112a13f7..e378b5714fe 100644
--- a/spec/controllers/projects/protected_branches_controller_spec.rb
+++ b/spec/controllers/projects/protected_branches_controller_spec.rb
@@ -4,7 +4,7 @@ describe Projects::ProtectedBranchesController do
describe "GET #index" do
let(:project) { create(:project_empty_repo, :public) }
it "redirects empty repo to projects page" do
- get(:index, namespace_id: project.namespace.to_param, project_id: project.to_param)
+ get(:index, namespace_id: project.namespace.to_param, project_id: project)
end
end
end
diff --git a/spec/controllers/projects/raw_controller_spec.rb b/spec/controllers/projects/raw_controller_spec.rb
index b23d6e257ba..4cebe3884bf 100644
--- a/spec/controllers/projects/raw_controller_spec.rb
+++ b/spec/controllers/projects/raw_controller_spec.rb
@@ -10,7 +10,7 @@ describe Projects::RawController do
it 'delivers ASCII file' do
get(:show,
namespace_id: public_project.namespace.to_param,
- project_id: public_project.to_param,
+ project_id: public_project,
id: id)
expect(response).to have_http_status(200)
@@ -27,7 +27,7 @@ describe Projects::RawController do
it 'sets image content type header' do
get(:show,
namespace_id: public_project.namespace.to_param,
- project_id: public_project.to_param,
+ project_id: public_project,
id: id)
expect(response).to have_http_status(200)
@@ -51,7 +51,7 @@ describe Projects::RawController do
expect(controller).to receive(:send_file).with("#{Gitlab.config.shared.path}/lfs-objects/91/ef/f75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897", filename: "lfs_object.iso", disposition: 'attachment')
get(:show,
namespace_id: public_project.namespace.to_param,
- project_id: public_project.to_param,
+ project_id: public_project,
id: id)
expect(response).to have_http_status(200)
@@ -62,7 +62,7 @@ describe Projects::RawController do
it 'does not serve the file' do
get(:show,
namespace_id: public_project.namespace.to_param,
- project_id: public_project.to_param,
+ project_id: public_project,
id: id)
expect(response).to have_http_status(404)
diff --git a/spec/controllers/projects/refs_controller_spec.rb b/spec/controllers/projects/refs_controller_spec.rb
index d8fb4667c67..3a3e7467ef2 100644
--- a/spec/controllers/projects/refs_controller_spec.rb
+++ b/spec/controllers/projects/refs_controller_spec.rb
@@ -13,7 +13,7 @@ describe Projects::RefsController do
def default_get(format = :html)
get :logs_tree,
namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ project_id: project,
id: 'master',
path: 'foo/bar/baz.html',
format: format
@@ -23,7 +23,7 @@ describe Projects::RefsController do
xhr :get,
:logs_tree,
namespace_id: project.namespace.to_param,
- project_id: project.to_param, id: 'master',
+ project_id: project, id: 'master',
path: 'foo/bar/baz.html', format: format
end
diff --git a/spec/controllers/projects/releases_controller_spec.rb b/spec/controllers/projects/releases_controller_spec.rb
index 69fcc26c77e..358f26dfb02 100644
--- a/spec/controllers/projects/releases_controller_spec.rb
+++ b/spec/controllers/projects/releases_controller_spec.rb
@@ -16,7 +16,7 @@ describe Projects::ReleasesController do
tag_id = release.tag
project.releases.destroy_all
- get :edit, namespace_id: project.namespace.path, project_id: project.path, tag_id: tag_id
+ get :edit, namespace_id: project.namespace, project_id: project, tag_id: tag_id
release = assigns(:release)
expect(release).not_to be_nil
@@ -24,7 +24,7 @@ describe Projects::ReleasesController do
end
it 'retrieves an existing release' do
- get :edit, namespace_id: project.namespace.path, project_id: project.path, tag_id: release.tag
+ get :edit, namespace_id: project.namespace, project_id: project, tag_id: release.tag
release = assigns(:release)
expect(release).not_to be_nil
@@ -48,7 +48,7 @@ describe Projects::ReleasesController do
def update_release(description)
put :update,
namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ project_id: project,
tag_id: release.tag,
release: { description: description }
end
diff --git a/spec/controllers/projects/repositories_controller_spec.rb b/spec/controllers/projects/repositories_controller_spec.rb
index 04e88879fb8..9c55d159fa0 100644
--- a/spec/controllers/projects/repositories_controller_spec.rb
+++ b/spec/controllers/projects/repositories_controller_spec.rb
@@ -6,7 +6,7 @@ describe Projects::RepositoriesController do
describe "GET archive" do
context 'as a guest' do
it 'responds with redirect in correct format' do
- get :archive, namespace_id: project.namespace.path, project_id: project.path, format: "zip"
+ get :archive, namespace_id: project.namespace, project_id: project, format: "zip"
expect(response.header["Content-Type"]).to start_with('text/html')
expect(response).to be_redirect
@@ -22,7 +22,7 @@ describe Projects::RepositoriesController do
end
it "uses Gitlab::Workhorse" do
- get :archive, namespace_id: project.namespace.path, project_id: project.path, ref: "master", format: "zip"
+ get :archive, namespace_id: project.namespace, project_id: project, ref: "master", format: "zip"
expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("git-archive:")
end
@@ -33,7 +33,7 @@ describe Projects::RepositoriesController do
end
it "renders Not Found" do
- get :archive, namespace_id: project.namespace.path, project_id: project.path, ref: "master", format: "zip"
+ get :archive, namespace_id: project.namespace, project_id: project, ref: "master", format: "zip"
expect(response).to have_http_status(404)
end
diff --git a/spec/controllers/projects/runners_controller_spec.rb b/spec/controllers/projects/runners_controller_spec.rb
new file mode 100644
index 00000000000..0fa249e4405
--- /dev/null
+++ b/spec/controllers/projects/runners_controller_spec.rb
@@ -0,0 +1,75 @@
+require 'spec_helper'
+
+describe Projects::RunnersController do
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project) }
+ let(:runner) { create(:ci_runner) }
+
+ let(:params) do
+ {
+ namespace_id: project.namespace,
+ project_id: project,
+ id: runner
+ }
+ end
+
+ before do
+ sign_in(user)
+ project.add_master(user)
+ project.runners << runner
+ end
+
+ describe '#update' do
+ it 'updates the runner and ticks the queue' do
+ new_desc = runner.description.swapcase
+
+ expect do
+ post :update, params.merge(runner: { description: new_desc } )
+ end.to change { runner.ensure_runner_queue_value }
+
+ runner.reload
+
+ expect(response).to have_http_status(302)
+ expect(runner.description).to eq(new_desc)
+ end
+ end
+
+ describe '#destroy' do
+ it 'destroys the runner' do
+ delete :destroy, params
+
+ expect(response).to have_http_status(302)
+ expect(Ci::Runner.find_by(id: runner.id)).to be_nil
+ end
+ end
+
+ describe '#resume' do
+ it 'marks the runner as active and ticks the queue' do
+ runner.update(active: false)
+
+ expect do
+ post :resume, params
+ end.to change { runner.ensure_runner_queue_value }
+
+ runner.reload
+
+ expect(response).to have_http_status(302)
+ expect(runner.active).to eq(true)
+ end
+ end
+
+ describe '#pause' do
+ it 'marks the runner as inactive and ticks the queue' do
+ runner.update(active: true)
+
+ expect do
+ post :pause, params
+ end.to change { runner.ensure_runner_queue_value }
+
+ runner.reload
+
+ expect(response).to have_http_status(302)
+ expect(runner.active).to eq(false)
+ end
+ end
+end
diff --git a/spec/controllers/projects/snippets_controller_spec.rb b/spec/controllers/projects/snippets_controller_spec.rb
index 19e948d8fb8..24a59caff4e 100644
--- a/spec/controllers/projects/snippets_controller_spec.rb
+++ b/spec/controllers/projects/snippets_controller_spec.rb
@@ -17,16 +17,16 @@ describe Projects::SnippetsController do
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
+ namespace_id: project.namespace,
+ project_id: project, 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
+ namespace_id: project.namespace,
+ project_id: project, page: last_page.to_param
expect(assigns(:snippets).current_page).to eq(last_page)
expect(response).to have_http_status(200)
@@ -38,7 +38,7 @@ describe Projects::SnippetsController do
context 'when anonymous' do
it 'does not include the private snippet' do
- get :index, namespace_id: project.namespace.path, project_id: project.path
+ get :index, namespace_id: project.namespace, project_id: project
expect(assigns(:snippets)).not_to include(project_snippet)
expect(response).to have_http_status(200)
@@ -49,7 +49,7 @@ describe Projects::SnippetsController do
before { sign_in(user) }
it 'renders the snippet' do
- get :index, namespace_id: project.namespace.path, project_id: project.path
+ get :index, namespace_id: project.namespace, project_id: project
expect(assigns(:snippets)).to include(project_snippet)
expect(response).to have_http_status(200)
@@ -60,7 +60,7 @@ describe Projects::SnippetsController do
before { sign_in(user2) }
it 'renders the snippet' do
- get :index, namespace_id: project.namespace.path, project_id: project.path
+ get :index, namespace_id: project.namespace, project_id: project
expect(assigns(:snippets)).to include(project_snippet)
expect(response).to have_http_status(200)
@@ -70,16 +70,16 @@ describe Projects::SnippetsController do
end
describe 'POST #create' do
- def create_snippet(project, snippet_params = {})
+ def create_snippet(project, snippet_params = {}, additional_params = {})
sign_in(user)
project.add_developer(user)
post :create, {
namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ project_id: project,
project_snippet: { title: 'Title', content: 'Content' }.merge(snippet_params)
- }
+ }.merge(additional_params)
end
context 'when the snippet is spam' do
@@ -87,35 +87,179 @@ describe Projects::SnippetsController 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 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
- 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)
+ it 'renders :new with recaptcha disabled' do
+ stub_application_setting(recaptcha_enabled: false)
+
+ create_snippet(project, visibility_level: Snippet::PUBLIC)
+
+ expect(response).to render_template(:new)
+ end
+
+ context 'recaptcha enabled' do
+ before do
+ stub_application_setting(recaptcha_enabled: true)
+ end
+
+ it 'renders :verify with recaptcha enabled' do
+ create_snippet(project, visibility_level: Snippet::PUBLIC)
+
+ expect(response).to render_template(:verify)
+ end
+
+ it 'renders snippet page when recaptcha verified' do
+ spammy_title = 'Whatever'
+
+ spam_logs = create_list(:spam_log, 2, user: user, title: spammy_title)
+ create_snippet(project,
+ { visibility_level: Snippet::PUBLIC },
+ { spam_log_id: spam_logs.last.id,
+ recaptcha_verification: true })
+
+ expect(response).to redirect_to(Snippet.last)
end
end
end
+ end
+ end
+
+ describe 'PUT #update' do
+ let(:project) { create :project, :public }
+ let(:snippet) { create :project_snippet, author: user, project: project, visibility_level: visibility_level }
+
+ def update_snippet(snippet_params = {}, additional_params = {})
+ sign_in(user)
+
+ project.add_developer(user)
+
+ put :update, {
+ namespace_id: project.namespace.to_param,
+ project_id: project,
+ id: snippet.id,
+ project_snippet: { title: 'Title', content: 'Content' }.merge(snippet_params)
+ }.merge(additional_params)
+
+ snippet.reload
+ 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
+ let(:visibility_level) { Snippet::PRIVATE }
+
+ it 'updates the snippet' do
+ expect { update_snippet(title: 'Foo') }.
+ to change { snippet.reload.title }.to('Foo')
+ end
+ end
+
+ context 'when the snippet is public' do
+ let(:visibility_level) { Snippet::PUBLIC }
+
+ it 'rejects the shippet' do
+ expect { update_snippet(title: 'Foo') }.
+ not_to change { snippet.reload.title }
+ 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)
+ it 'creates a spam log' do
+ expect { update_snippet(title: 'Foo') }.
+ to change { SpamLog.count }.by(1)
+ end
+
+ it 'renders :edit with recaptcha disabled' do
+ stub_application_setting(recaptcha_enabled: false)
+
+ update_snippet(title: 'Foo')
+
+ expect(response).to render_template(:edit)
+ end
+
+ context 'recaptcha enabled' do
+ before do
+ stub_application_setting(recaptcha_enabled: true)
+ end
+
+ it 'renders :verify with recaptcha enabled' do
+ update_snippet(title: 'Foo')
+
+ expect(response).to render_template(:verify)
+ end
+
+ it 'renders snippet page when recaptcha verified' do
+ spammy_title = 'Whatever'
+
+ spam_logs = create_list(:spam_log, 2, user: user, title: spammy_title)
+ snippet = update_snippet({ title: spammy_title },
+ { spam_log_id: spam_logs.last.id,
+ recaptcha_verification: true })
+
+ expect(response).to redirect_to(snippet)
end
end
+ end
+
+ context 'when the private snippet is made public' do
+ let(:visibility_level) { Snippet::PRIVATE }
+
+ it 'rejects the shippet' do
+ expect { update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC) }.
+ not_to change { snippet.reload.title }
+ end
+
+ it 'creates a spam log' do
+ expect { update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC) }.
+ to change { SpamLog.count }.by(1)
+ end
+
+ it 'renders :edit with recaptcha disabled' do
+ stub_application_setting(recaptcha_enabled: false)
+
+ update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC)
+
+ expect(response).to render_template(:edit)
+ end
+
+ context 'recaptcha enabled' do
+ before do
+ stub_application_setting(recaptcha_enabled: true)
+ 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)
+ it 'renders :verify with recaptcha enabled' do
+ update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC)
+
+ expect(response).to render_template(:verify)
end
- it 'creates a spam log' do
- expect { create_snippet(project, visibility_level: Snippet::PUBLIC) }.
- to change { SpamLog.count }.by(1)
+ it 'renders snippet page when recaptcha verified' do
+ spammy_title = 'Whatever'
+
+ spam_logs = create_list(:spam_log, 2, user: user, title: spammy_title)
+ snippet = update_snippet({ title: spammy_title, visibility_level: Snippet::PUBLIC },
+ { spam_log_id: spam_logs.last.id,
+ recaptcha_verification: true })
+
+ expect(response).to redirect_to(snippet)
end
end
end
@@ -137,8 +281,8 @@ describe Projects::SnippetsController do
sign_in(admin)
post :mark_as_spam,
- namespace_id: project.namespace.path,
- project_id: project.path,
+ namespace_id: project.namespace,
+ project_id: project,
id: snippet.id
end
@@ -156,7 +300,7 @@ describe Projects::SnippetsController do
context 'when anonymous' do
it 'responds with status 404' do
- get action, namespace_id: project.namespace.path, project_id: project.path, id: project_snippet.to_param
+ get action, namespace_id: project.namespace, project_id: project, id: project_snippet.to_param
expect(response).to have_http_status(404)
end
@@ -166,7 +310,7 @@ describe Projects::SnippetsController do
before { sign_in(user) }
it 'renders the snippet' do
- get action, namespace_id: project.namespace.path, project_id: project.path, id: project_snippet.to_param
+ get action, namespace_id: project.namespace, project_id: project, id: project_snippet.to_param
expect(assigns(:snippet)).to eq(project_snippet)
expect(response).to have_http_status(200)
@@ -177,7 +321,7 @@ describe Projects::SnippetsController do
before { sign_in(user2) }
it 'renders the snippet' do
- get action, namespace_id: project.namespace.path, project_id: project.path, id: project_snippet.to_param
+ get action, namespace_id: project.namespace, project_id: project, id: project_snippet.to_param
expect(assigns(:snippet)).to eq(project_snippet)
expect(response).to have_http_status(200)
@@ -188,7 +332,7 @@ describe Projects::SnippetsController do
context 'when the project snippet does not exist' do
context 'when anonymous' do
it 'responds with status 404' do
- get action, namespace_id: project.namespace.path, project_id: project.path, id: 42
+ get action, namespace_id: project.namespace, project_id: project, id: 42
expect(response).to have_http_status(404)
end
@@ -198,7 +342,7 @@ describe Projects::SnippetsController do
before { sign_in(user) }
it 'responds with status 404' do
- get action, namespace_id: project.namespace.path, project_id: project.path, id: 42
+ get action, namespace_id: project.namespace, project_id: project, id: 42
expect(response).to have_http_status(404)
end
@@ -206,4 +350,37 @@ describe Projects::SnippetsController do
end
end
end
+
+ describe 'GET #raw' do
+ let(:project_snippet) do
+ create(
+ :project_snippet, :public,
+ project: project,
+ author: user,
+ content: "first line\r\nsecond line\r\nthird line"
+ )
+ end
+
+ context 'CRLF line ending' do
+ let(:params) do
+ {
+ namespace_id: project.namespace,
+ project_id: project,
+ id: project_snippet.to_param
+ }
+ end
+
+ it 'returns LF line endings by default' do
+ get :raw, params
+
+ expect(response.body).to eq("first line\nsecond line\nthird line")
+ end
+
+ it 'does not convert line endings when parameter present' do
+ get :raw, params.merge(line_ending: :raw)
+
+ expect(response.body).to eq("first line\r\nsecond line\r\nthird line")
+ end
+ end
+ end
end
diff --git a/spec/controllers/projects/tags_controller_spec.rb b/spec/controllers/projects/tags_controller_spec.rb
index c36a5fdd66c..fc97bac64cd 100644
--- a/spec/controllers/projects/tags_controller_spec.rb
+++ b/spec/controllers/projects/tags_controller_spec.rb
@@ -6,7 +6,7 @@ describe Projects::TagsController do
let!(:invalid_release) { create(:release, project: project, tag: 'does-not-exist') }
describe 'GET index' do
- before { get :index, namespace_id: project.namespace.to_param, project_id: project.to_param }
+ before { get :index, namespace_id: project.namespace.to_param, project_id: project }
it 'returns the tags for the page' do
expect(assigns(:tags).map(&:name)).to eq(['v1.1.0', 'v1.0.0'])
@@ -19,7 +19,7 @@ describe Projects::TagsController do
end
describe 'GET show' do
- before { get :show, namespace_id: project.namespace.to_param, project_id: project.to_param, id: id }
+ before { get :show, namespace_id: project.namespace.to_param, project_id: project, id: id }
context "valid tag" do
let(:id) { 'v1.0.0' }
diff --git a/spec/controllers/projects/templates_controller_spec.rb b/spec/controllers/projects/templates_controller_spec.rb
index 80f84a388ce..70e7f9ca96e 100644
--- a/spec/controllers/projects/templates_controller_spec.rb
+++ b/spec/controllers/projects/templates_controller_spec.rb
@@ -14,13 +14,13 @@ describe Projects::TemplatesController do
before do
project.add_user(user, Gitlab::Access::MASTER)
- project.repository.commit_file(user, file_path_1, 'something valid',
- message: 'test 3', branch_name: 'master', update: false)
+ project.repository.create_file(user, file_path_1, 'something valid',
+ message: 'test 3', branch_name: 'master')
end
describe '#show' do
it 'renders template name and content as json' do
- get(:show, namespace_id: project.namespace.to_param, template_type: "issue", key: "bug", project_id: project.path, format: :json)
+ get(:show, namespace_id: project.namespace.to_param, template_type: "issue", key: "bug", project_id: project, format: :json)
expect(response.status).to eq(200)
expect(body["name"]).to eq("bug")
@@ -29,21 +29,21 @@ describe Projects::TemplatesController do
it 'renders 404 when unauthorized' do
sign_in(user2)
- get(:show, namespace_id: project.namespace.to_param, template_type: "issue", key: "bug", project_id: project.path, format: :json)
+ get(:show, namespace_id: project.namespace.to_param, template_type: "issue", key: "bug", project_id: project, format: :json)
expect(response.status).to eq(404)
end
it 'renders 404 when template type is not found' do
sign_in(user)
- get(:show, namespace_id: project.namespace.to_param, template_type: "dont_exist", key: "bug", project_id: project.path, format: :json)
+ get(:show, namespace_id: project.namespace.to_param, template_type: "dont_exist", key: "bug", project_id: project, format: :json)
expect(response.status).to eq(404)
end
it 'renders 404 without errors' do
sign_in(user)
- expect { get(:show, namespace_id: project.namespace.to_param, template_type: "dont_exist", key: "bug", project_id: project.path, format: :json) }.not_to raise_error
+ expect { get(:show, namespace_id: project.namespace.to_param, template_type: "dont_exist", key: "bug", project_id: project, format: :json) }.not_to raise_error
end
end
end
diff --git a/spec/controllers/projects/todo_controller_spec.rb b/spec/controllers/projects/todo_controller_spec.rb
index 415c264e0dd..9a7beeff6fe 100644
--- a/spec/controllers/projects/todo_controller_spec.rb
+++ b/spec/controllers/projects/todo_controller_spec.rb
@@ -12,8 +12,8 @@ describe Projects::TodosController do
describe 'POST create' do
def go
post :create,
- namespace_id: project.namespace.path,
- project_id: project.path,
+ namespace_id: project.namespace,
+ project_id: project,
issuable_id: issue.id,
issuable_type: 'issue',
format: 'html'
@@ -80,8 +80,8 @@ describe Projects::TodosController do
describe 'POST create' do
def go
post :create,
- namespace_id: project.namespace.path,
- project_id: project.path,
+ namespace_id: project.namespace,
+ project_id: project,
issuable_id: merge_request.id,
issuable_type: 'merge_request',
format: 'html'
diff --git a/spec/controllers/projects/tree_controller_spec.rb b/spec/controllers/projects/tree_controller_spec.rb
index b81645a3d2d..ab94e292e48 100644
--- a/spec/controllers/projects/tree_controller_spec.rb
+++ b/spec/controllers/projects/tree_controller_spec.rb
@@ -18,7 +18,7 @@ describe Projects::TreeController do
before do
get(:show,
namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ project_id: project,
id: id)
end
@@ -74,7 +74,7 @@ describe Projects::TreeController do
before do
get(:show,
namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ project_id: project,
id: id)
end
@@ -94,7 +94,7 @@ describe Projects::TreeController do
before do
post(:create_dir,
namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ project_id: project,
id: 'master',
dir_name: path,
target_branch: target_branch,
diff --git a/spec/controllers/projects/uploads_controller_spec.rb b/spec/controllers/projects/uploads_controller_spec.rb
index 0347e789576..699c6f77cec 100644
--- a/spec/controllers/projects/uploads_controller_spec.rb
+++ b/spec/controllers/projects/uploads_controller_spec.rb
@@ -16,7 +16,7 @@ describe Projects::UploadsController do
it "returns an error" do
post :create,
namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ project_id: project,
format: :json
expect(response).to have_http_status(422)
end
@@ -26,7 +26,7 @@ describe Projects::UploadsController do
before do
post :create,
namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ project_id: project,
file: jpg,
format: :json
end
@@ -41,7 +41,7 @@ describe Projects::UploadsController do
before do
post :create,
namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ project_id: project,
file: txt,
format: :json
end
@@ -57,7 +57,7 @@ describe Projects::UploadsController do
let(:go) do
get :show,
namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ project_id: project,
secret: "123456",
filename: "image.jpg"
end
diff --git a/spec/controllers/projects/variables_controller_spec.rb b/spec/controllers/projects/variables_controller_spec.rb
index 9fa358f7d62..e3f3b4fe8eb 100644
--- a/spec/controllers/projects/variables_controller_spec.rb
+++ b/spec/controllers/projects/variables_controller_spec.rb
@@ -12,7 +12,7 @@ describe Projects::VariablesController do
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,
+ post :create, namespace_id: project.namespace.to_param, project_id: project,
variable: { key: "one", value: "two" }
expect(flash[:notice]).to include 'Variables were successfully updated.'
@@ -22,7 +22,7 @@ describe Projects::VariablesController do
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,
+ post :create, namespace_id: project.namespace.to_param, project_id: project,
variable: { key: "..one", value: "two" }
expect(response).to render_template("projects/variables/show")
@@ -40,7 +40,7 @@ describe Projects::VariablesController do
end
it 'shows a success flash message' do
- post :update, namespace_id: project.namespace.to_param, project_id: project.to_param,
+ post :update, namespace_id: project.namespace.to_param, project_id: project,
id: variable.id, variable: { key: variable.key, value: 'two' }
expect(flash[:notice]).to include 'Variable was successfully updated.'
@@ -48,7 +48,7 @@ describe Projects::VariablesController do
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,
+ post :update, namespace_id: project.namespace.to_param, project_id: project,
id: variable.id, variable: { key: '?', value: variable.value }
expect(response).to have_http_status(200)
diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb
index e7aa8745b99..202759664a0 100644
--- a/spec/controllers/projects_controller_spec.rb
+++ b/spec/controllers/projects_controller_spec.rb
@@ -35,7 +35,7 @@ describe ProjectsController do
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
+ get :show, namespace_id: private_project.namespace, id: private_project
expect(assigns(:notification_setting)).to be_nil
end
end
@@ -43,7 +43,7 @@ describe ProjectsController do
context "user has access to project" do
context "and does not have notification setting" do
it "initializes notification as disabled" do
- get :show, namespace_id: public_project.namespace.path, id: public_project.path
+ get :show, namespace_id: public_project.namespace, id: public_project
expect(assigns(:notification_setting).level).to eq("global")
end
end
@@ -56,7 +56,7 @@ describe ProjectsController do
end
it "shows current notification setting" do
- get :show, namespace_id: public_project.namespace.path, id: public_project.path
+ get :show, namespace_id: public_project.namespace, id: public_project
expect(assigns(:notification_setting).level).to eq("watch")
end
end
@@ -71,7 +71,7 @@ describe ProjectsController do
end
it 'shows wiki homepage' do
- get :show, namespace_id: project.namespace.path, id: project.path
+ get :show, namespace_id: project.namespace, id: project
expect(response).to render_template('projects/_wiki')
end
@@ -79,7 +79,7 @@ describe ProjectsController do
it 'shows issues list page if wiki is disabled' do
project.project_feature.update_attribute(:wiki_access_level, ProjectFeature::DISABLED)
- get :show, namespace_id: project.namespace.path, id: project.path
+ get :show, namespace_id: project.namespace, id: project
expect(response).to render_template('projects/issues/_issues')
end
@@ -88,7 +88,7 @@ describe ProjectsController do
project.project_feature.update_attribute(:wiki_access_level, ProjectFeature::DISABLED)
project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED)
- get :show, namespace_id: project.namespace.path, id: project.path
+ get :show, namespace_id: project.namespace, id: project
expect(response).to render_template("projects/_customize_workflow")
end
@@ -96,7 +96,7 @@ describe ProjectsController do
it 'shows activity if enabled by user' do
user.update_attribute(:project_view, 'activity')
- get :show, namespace_id: project.namespace.path, id: project.path
+ get :show, namespace_id: project.namespace, id: project
expect(response).to render_template("projects/_activity")
end
@@ -113,7 +113,7 @@ describe ProjectsController do
before do
user.update_attributes(project_view: project_view)
- get :show, namespace_id: empty_project.namespace.path, id: empty_project.path
+ get :show, namespace_id: empty_project.namespace, id: empty_project
end
it "renders the empty project view" do
@@ -133,7 +133,7 @@ describe ProjectsController do
before do
user.update_attributes(project_view: project_view)
- get :show, namespace_id: empty_project.namespace.path, id: empty_project.path
+ get :show, namespace_id: empty_project.namespace, id: empty_project
end
it "renders the empty project view" do
@@ -154,7 +154,7 @@ describe ProjectsController do
allow(controller).to receive(:current_user).and_return(user)
allow(user).to receive(:project_view).and_return('activity')
- get :show, namespace_id: public_project.namespace.path, id: public_project.path
+ get :show, namespace_id: public_project.namespace, id: public_project
expect(response).to render_template('_activity')
end
@@ -162,7 +162,7 @@ describe ProjectsController do
allow(controller).to receive(:current_user).and_return(user)
allow(user).to receive(:project_view).and_return('readme')
- get :show, namespace_id: public_project.namespace.path, id: public_project.path
+ get :show, namespace_id: public_project.namespace, id: public_project
expect(response).to render_template('_readme')
end
@@ -170,7 +170,7 @@ describe ProjectsController do
allow(controller).to receive(:current_user).and_return(user)
allow(user).to receive(:project_view).and_return('files')
- get :show, namespace_id: public_project.namespace.path, id: public_project.path
+ get :show, namespace_id: public_project.namespace, id: public_project
expect(response).to render_template('_files')
end
end
@@ -178,7 +178,7 @@ describe ProjectsController do
context "when requested with case sensitive namespace and project path" do
context "when there is a match with the same casing" do
it "loads the project" do
- get :show, namespace_id: public_project.namespace.path, id: public_project.path
+ get :show, namespace_id: public_project.namespace, id: public_project
expect(assigns(:project)).to eq(public_project)
expect(response).to have_http_status(200)
@@ -187,10 +187,10 @@ describe ProjectsController do
context "when there is a match with different casing" do
it "redirects to the normalized path" do
- get :show, namespace_id: public_project.namespace.path, id: public_project.path.upcase
+ get :show, namespace_id: public_project.namespace, id: public_project.path.upcase
expect(assigns(:project)).to eq(public_project)
- expect(response).to redirect_to("/#{public_project.path_with_namespace}")
+ expect(response).to redirect_to("/#{public_project.full_path}")
end
end
end
@@ -208,7 +208,7 @@ describe ProjectsController do
project = create(:empty_project, pending_delete: true)
sign_in(user)
- get :show, namespace_id: project.namespace.path, id: project.path
+ get :show, namespace_id: project.namespace, id: project
expect(response.status).to eq 404
end
@@ -218,7 +218,7 @@ describe ProjectsController 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
+ get :show, namespace_id: project.namespace, id: project, format: :git
expect(response).to have_http_status(302)
expect(response).to redirect_to(namespace_project_path)
@@ -239,7 +239,7 @@ describe ProjectsController do
sign_in(admin)
put :update,
- namespace_id: project.namespace.to_param,
+ namespace_id: project.namespace,
id: project.id,
project: project_params
@@ -257,7 +257,7 @@ describe ProjectsController do
sign_in(admin)
orig_id = project.id
- delete :destroy, namespace_id: project.namespace.path, id: project.path
+ delete :destroy, namespace_id: project.namespace, id: project
expect { Project.find(orig_id) }.to raise_error(ActiveRecord::RecordNotFound)
expect(response).to have_http_status(302)
@@ -277,7 +277,7 @@ describe ProjectsController do
project.merge_requests << merge_request
sign_in(admin)
- delete :destroy, namespace_id: fork_project.namespace.path, id: fork_project.path
+ delete :destroy, namespace_id: fork_project.namespace, id: fork_project
expect(merge_request.reload.state).to eq('closed')
end
@@ -287,8 +287,8 @@ describe ProjectsController do
describe 'PUT #new_issue_address' do
subject do
put :new_issue_address,
- namespace_id: project.namespace.to_param,
- id: project.to_param
+ namespace_id: project.namespace,
+ id: project
user.reload
end
@@ -316,23 +316,23 @@ describe ProjectsController do
sign_in(user)
expect(user.starred?(public_project)).to be_falsey
post(:toggle_star,
- namespace_id: public_project.namespace.to_param,
- id: public_project.to_param)
+ namespace_id: public_project.namespace,
+ id: public_project)
expect(user.starred?(public_project)).to be_truthy
post(:toggle_star,
- namespace_id: public_project.namespace.to_param,
- id: public_project.to_param)
+ namespace_id: public_project.namespace,
+ id: public_project)
expect(user.starred?(public_project)).to be_falsey
end
it "does nothing if user is not signed in" do
post(:toggle_star,
- namespace_id: project.namespace.to_param,
- id: public_project.to_param)
+ namespace_id: project.namespace,
+ id: public_project)
expect(user.starred?(public_project)).to be_falsey
post(:toggle_star,
- namespace_id: project.namespace.to_param,
- id: public_project.to_param)
+ namespace_id: project.namespace,
+ id: public_project)
expect(user.starred?(public_project)).to be_falsey
end
end
@@ -366,8 +366,8 @@ describe ProjectsController do
it 'does nothing if project was not forked' do
delete(:remove_fork,
- namespace_id: unforked_project.namespace.to_param,
- id: unforked_project.to_param, format: :js)
+ namespace_id: unforked_project.namespace,
+ id: unforked_project, format: :js)
expect(flash[:notice]).to be_nil
expect(response).to render_template(:remove_fork)
@@ -377,8 +377,8 @@ describe ProjectsController do
it "does nothing if user is not signed in" do
delete(:remove_fork,
- namespace_id: project.namespace.to_param,
- id: project.to_param, format: :js)
+ namespace_id: project.namespace,
+ id: project, format: :js)
expect(response).to have_http_status(401)
end
end
@@ -387,7 +387,7 @@ describe ProjectsController 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
+ get :refs, namespace_id: public_project.namespace, id: public_project
parsed_body = JSON.parse(response.body)
expect(parsed_body["Branches"]).to include("master")
@@ -396,7 +396,7 @@ describe ProjectsController do
end
it "gets a list of branches, tags and commits" do
- get :refs, namespace_id: public_project.namespace.path, id: public_project.path, ref: "123456"
+ get :refs, namespace_id: public_project.namespace, id: public_project, ref: "123456"
parsed_body = JSON.parse(response.body)
expect(parsed_body["Branches"]).to include("master")
diff --git a/spec/controllers/snippets_controller_spec.rb b/spec/controllers/snippets_controller_spec.rb
index dadcb90cfc2..5de3b9890ef 100644
--- a/spec/controllers/snippets_controller_spec.rb
+++ b/spec/controllers/snippets_controller_spec.rb
@@ -139,12 +139,14 @@ describe SnippetsController do
end
describe 'POST #create' do
- def create_snippet(snippet_params = {})
+ def create_snippet(snippet_params = {}, additional_params = {})
sign_in(user)
post :create, {
personal_snippet: { title: 'Title', content: 'Content' }.merge(snippet_params)
- }
+ }.merge(additional_params)
+
+ Snippet.last
end
context 'when the snippet is spam' do
@@ -163,13 +165,164 @@ describe SnippetsController 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
+
+ it 'renders :new with recaptcha disabled' do
+ stub_application_setting(recaptcha_enabled: false)
+
+ create_snippet(visibility_level: Snippet::PUBLIC)
+
+ expect(response).to render_template(:new)
+ end
+
+ context 'recaptcha enabled' do
+ before do
+ stub_application_setting(recaptcha_enabled: true)
+ end
+
+ it 'renders :verify with recaptcha enabled' do
+ create_snippet(visibility_level: Snippet::PUBLIC)
+
+ expect(response).to render_template(:verify)
+ end
+
+ it 'renders snippet page when recaptcha verified' do
+ spammy_title = 'Whatever'
+
+ spam_logs = create_list(:spam_log, 2, user: user, title: spammy_title)
+ snippet = create_snippet({ title: spammy_title },
+ { spam_log_id: spam_logs.last.id,
+ recaptcha_verification: true })
+
+ expect(response).to redirect_to(snippet_path(snippet))
+ end
+ end
+ end
+ end
+ end
+
+ describe 'PUT #update' do
+ let(:project) { create :project }
+ let(:snippet) { create :personal_snippet, author: user, project: project, visibility_level: visibility_level }
+
+ def update_snippet(snippet_params = {}, additional_params = {})
+ sign_in(user)
+
+ put :update, {
+ id: snippet.id,
+ personal_snippet: { title: 'Title', content: 'Content' }.merge(snippet_params)
+ }.merge(additional_params)
+
+ snippet.reload
+ 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
+ let(:visibility_level) { Snippet::PRIVATE }
+
+ it 'updates the snippet' do
+ expect { update_snippet(title: 'Foo') }.
+ to change { snippet.reload.title }.to('Foo')
+ end
+ end
+
+ context 'when a private snippet is made public' do
+ let(:visibility_level) { Snippet::PRIVATE }
+
+ it 'rejects the snippet' do
+ expect { update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC) }.
+ not_to change { snippet.reload.title }
+ end
+
+ it 'creates a spam log' do
+ expect { update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC) }.
+ to change { SpamLog.count }.by(1)
+ end
+
+ it 'renders :edit with recaptcha disabled' do
+ stub_application_setting(recaptcha_enabled: false)
+
+ update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC)
+
+ expect(response).to render_template(:edit)
+ end
+
+ context 'recaptcha enabled' do
+ before do
+ stub_application_setting(recaptcha_enabled: true)
+ end
+
+ it 'renders :verify with recaptcha enabled' do
+ update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC)
+
+ expect(response).to render_template(:verify)
+ end
+
+ it 'renders snippet page when recaptcha verified' do
+ spammy_title = 'Whatever'
+
+ spam_logs = create_list(:spam_log, 2, user: user, title: spammy_title)
+ snippet = update_snippet({ title: spammy_title, visibility_level: Snippet::PUBLIC },
+ { spam_log_id: spam_logs.last.id,
+ recaptcha_verification: true })
+
+ expect(response).to redirect_to(snippet)
+ end
+ end
+ end
+
+ context 'when the snippet is public' do
+ let(:visibility_level) { Snippet::PUBLIC }
+
+ it 'rejects the shippet' do
+ expect { update_snippet(title: 'Foo') }.
+ not_to change { snippet.reload.title }
+ end
+
+ it 'creates a spam log' do
+ expect { update_snippet(title: 'Foo') }.
+ to change { SpamLog.count }.by(1)
+ end
+
+ it 'renders :edit with recaptcha disabled' do
+ stub_application_setting(recaptcha_enabled: false)
+
+ update_snippet(title: 'Foo')
+
+ expect(response).to render_template(:edit)
+ end
+
+ context 'recaptcha enabled' do
+ before do
+ stub_application_setting(recaptcha_enabled: true)
+ end
+
+ it 'renders :verify with recaptcha enabled' do
+ update_snippet(title: 'Foo')
+
+ expect(response).to render_template(:verify)
+ end
+
+ it 'renders snippet page when recaptcha verified' do
+ spammy_title = 'Whatever'
+
+ spam_logs = create_list(:spam_log, 2, user: user, title: spammy_title)
+ snippet = update_snippet({ title: spammy_title },
+ { spam_log_id: spam_logs.last.id,
+ recaptcha_verification: true })
+
+ expect(response).to redirect_to(snippet_path(snippet))
+ end
+ end
end
end
end
@@ -286,6 +439,24 @@ describe SnippetsController do
expect(assigns(:snippet)).to eq(personal_snippet)
expect(response).to have_http_status(200)
end
+
+ context 'CRLF line ending' do
+ let(:personal_snippet) do
+ create(:personal_snippet, :public, author: user, content: "first line\r\nsecond line\r\nthird line")
+ end
+
+ it 'returns LF line endings by default' do
+ get action, id: personal_snippet.to_param
+
+ expect(response.body).to eq("first line\nsecond line\nthird line")
+ end
+
+ it 'does not convert line endings when parameter present' do
+ get action, id: personal_snippet.to_param, line_ending: :raw
+
+ expect(response.body).to eq("first line\r\nsecond line\r\nthird line")
+ end
+ end
end
context 'when not signed in' do
diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb
index 0397d5d4001..a90534d10ba 100644
--- a/spec/factories/ci/builds.rb
+++ b/spec/factories/ci/builds.rb
@@ -89,8 +89,9 @@ FactoryGirl.define do
tag true
end
- factory :ci_build_with_coverage do
+ trait :coverage do
coverage 99.9
+ coverage_regex '/(d+)/'
end
trait :trace do
@@ -99,6 +100,16 @@ FactoryGirl.define do
end
end
+ trait :erased do
+ erased_at Time.now
+ erased_by factory: :user
+ end
+
+ trait :queued do
+ queued_at Time.now
+ runner factory: :ci_runner
+ end
+
trait :artifacts do
after(:create) do |build, _|
build.artifacts_file =
@@ -128,5 +139,17 @@ FactoryGirl.define do
build.save!
end
end
+
+ trait :with_commit do
+ after(:build) do |build|
+ allow(build).to receive(:commit).and_return build(:commit, :without_author)
+ end
+ end
+
+ trait :with_commit_and_author do
+ after(:build) do |build|
+ allow(build).to receive(:commit).and_return build(:commit)
+ end
+ end
end
end
diff --git a/spec/factories/commits.rb b/spec/factories/commits.rb
index ac6eb0a7897..89e260cf65b 100644
--- a/spec/factories/commits.rb
+++ b/spec/factories/commits.rb
@@ -8,5 +8,15 @@ FactoryGirl.define do
initialize_with do
new(git_commit, project)
end
+
+ after(:build) do |commit|
+ allow(commit).to receive(:author).and_return build(:author)
+ end
+
+ trait :without_author do
+ after(:build) do |commit|
+ allow(commit).to receive(:author).and_return nil
+ end
+ end
end
end
diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb
index c80b09e9b9d..586efdefdb3 100644
--- a/spec/factories/projects.rb
+++ b/spec/factories/projects.rb
@@ -138,27 +138,24 @@ FactoryGirl.define do
project.add_user(args[:user], args[:access])
- project.repository.commit_file(
+ project.repository.create_file(
args[:user],
".gitlab/#{args[:path]}/bug.md",
'something valid',
message: 'test 3',
- branch_name: 'master',
- update: false)
- project.repository.commit_file(
+ branch_name: 'master')
+ project.repository.create_file(
args[:user],
".gitlab/#{args[:path]}/template_test.md",
'template_test',
message: 'test 1',
- branch_name: 'master',
- update: false)
- project.repository.commit_file(
+ branch_name: 'master')
+ project.repository.create_file(
args[:user],
".gitlab/#{args[:path]}/feature_proposal.md",
'feature_proposal',
message: 'test 2',
- branch_name: 'master',
- update: false)
+ branch_name: 'master')
end
end
end
diff --git a/spec/factories/services.rb b/spec/factories/services.rb
index a14a46c803e..51335bdcf1d 100644
--- a/spec/factories/services.rb
+++ b/spec/factories/services.rb
@@ -2,4 +2,14 @@ FactoryGirl.define do
factory :service do
project factory: :empty_project
end
+
+ factory :kubernetes_service do
+ project factory: :empty_project
+ active true
+ properties({
+ namespace: 'somepath',
+ api_url: 'https://kubernetes.example.com',
+ token: 'a' * 40,
+ })
+ end
end
diff --git a/spec/factories/todos.rb b/spec/factories/todos.rb
index 299d09d2181..c1ac3bb84ad 100644
--- a/spec/factories/todos.rb
+++ b/spec/factories/todos.rb
@@ -35,6 +35,10 @@ FactoryGirl.define do
action { Todo::UNMERGEABLE }
end
+ trait :pending do
+ state :pending
+ end
+
trait :done do
state :done
end
diff --git a/spec/factories/users.rb b/spec/factories/users.rb
index 1732b1a0081..249dabbaae8 100644
--- a/spec/factories/users.rb
+++ b/spec/factories/users.rb
@@ -26,6 +26,11 @@ FactoryGirl.define do
two_factor_via_otp
end
+ trait :ghost do
+ ghost true
+ after(:build) { |user, _| user.block! }
+ end
+
trait :two_factor_via_otp do
before(:create) do |user|
user.otp_required_for_login = true
diff --git a/spec/features/admin/admin_abuse_reports_spec.rb b/spec/features/admin/admin_abuse_reports_spec.rb
index 7fcfe5a54c7..340884fc986 100644
--- a/spec/features/admin/admin_abuse_reports_spec.rb
+++ b/spec/features/admin/admin_abuse_reports_spec.rb
@@ -30,5 +30,24 @@ describe "Admin::AbuseReports", feature: true, js: true do
end
end
end
+
+ describe 'if a many users have been reported for abuse' do
+ let(:report_count) { AbuseReport.default_per_page + 3 }
+
+ before do
+ report_count.times do
+ create(:abuse_report, user: create(:user))
+ end
+ end
+
+ describe 'in the abuse report view' do
+ it 'presents information about abuse report' do
+ visit admin_abuse_reports_path
+
+ expect(page).to have_selector('.pagination')
+ expect(page).to have_selector('.pagination .page', count: (report_count.to_f / AbuseReport.default_per_page).ceil)
+ end
+ end
+ 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 e8e080ce3e2..273cacd82cd 100644
--- a/spec/features/admin/admin_disables_git_access_protocol_spec.rb
+++ b/spec/features/admin/admin_disables_git_access_protocol_spec.rb
@@ -32,7 +32,7 @@ feature 'Admin disables Git access protocol', feature: true do
scenario 'shows only HTTP url' do
visit_project
- expect(page).to have_content("git clone #{project.http_url_to_repo}")
+ expect(page).to have_content("git clone #{project.http_url_to_repo(admin)}")
expect(page).not_to have_selector('#clone-dropdown')
end
end
diff --git a/spec/features/admin/admin_runners_spec.rb b/spec/features/admin/admin_runners_spec.rb
index f05fbe3d062..5dcc7d35d82 100644
--- a/spec/features/admin/admin_runners_spec.rb
+++ b/spec/features/admin/admin_runners_spec.rb
@@ -18,7 +18,7 @@ describe "Admin Runners" do
it 'has all necessary texts' do
expect(page).to have_text "To register a new Runner"
- expect(page).to have_text "Runners with last contact less than a minute ago: 1"
+ expect(page).to have_text "Runners with last contact more than a minute ago: 1"
end
describe 'search' do
diff --git a/spec/features/atom/dashboard_issues_spec.rb b/spec/features/atom/dashboard_issues_spec.rb
index 21ee6cedbae..a7c22615b89 100644
--- a/spec/features/atom/dashboard_issues_spec.rb
+++ b/spec/features/atom/dashboard_issues_spec.rb
@@ -23,7 +23,7 @@ describe "Dashboard Issues Feed", feature: true do
visit issues_dashboard_path(:atom, private_token: user.private_token, state: 'opened', assignee_id: user.id)
link = find('link[type="application/atom+xml"]')
- params = CGI::parse(URI.parse(link[:href]).query)
+ params = CGI.parse(URI.parse(link[:href]).query)
expect(params).to include('private_token' => [user.private_token])
expect(params).to include('state' => ['opened'])
diff --git a/spec/features/atom/issues_spec.rb b/spec/features/atom/issues_spec.rb
index 863412d18eb..a01a050a013 100644
--- a/spec/features/atom/issues_spec.rb
+++ b/spec/features/atom/issues_spec.rb
@@ -43,7 +43,7 @@ describe 'Issues Feed', feature: true do
:atom, private_token: user.private_token, state: 'opened', assignee_id: user.id)
link = find('link[type="application/atom+xml"]')
- params = CGI::parse(URI.parse(link[:href]).query)
+ params = CGI.parse(URI.parse(link[:href]).query)
expect(params).to include('private_token' => [user.private_token])
expect(params).to include('state' => ['opened'])
@@ -54,7 +54,7 @@ describe 'Issues Feed', feature: true do
visit issues_group_path(group, :atom, private_token: user.private_token, state: 'opened', assignee_id: user.id)
link = find('link[type="application/atom+xml"]')
- params = CGI::parse(URI.parse(link[:href]).query)
+ params = CGI.parse(URI.parse(link[:href]).query)
expect(params).to include('private_token' => [user.private_token])
expect(params).to include('state' => ['opened'])
diff --git a/spec/features/atom/users_spec.rb b/spec/features/atom/users_spec.rb
index f8c3ccb416b..b740e191f48 100644
--- a/spec/features/atom/users_spec.rb
+++ b/spec/features/atom/users_spec.rb
@@ -61,7 +61,7 @@ describe "User Feed", feature: true do
end
it 'has XHTML summaries in merge request descriptions' do
- expect(body).to match /Here is the fix: <\/p><div[^>]*><a[^>]*><img[^>]*\/><\/a><\/div>/
+ expect(body).to match /Here is the fix: <a[^>]*><img[^>]*\/><\/a>/
end
end
end
diff --git a/spec/features/boards/add_issues_modal_spec.rb b/spec/features/boards/add_issues_modal_spec.rb
index 2875fc1e533..a3e24bb5ffa 100644
--- a/spec/features/boards/add_issues_modal_spec.rb
+++ b/spec/features/boards/add_issues_modal_spec.rb
@@ -49,6 +49,12 @@ describe 'Issue Boards add issue modal', :feature, :js do
expect(page).not_to have_selector('.add-issues-modal')
end
+
+ it 'does not show tooltip on add issues button' do
+ button = page.find('.issue-boards-search button', text: 'Add issues')
+
+ expect(button[:title]).not_to eq("Please add a list to your board first")
+ end
end
context 'issues list' do
diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb
index 7225f38b7e5..e247bfa2980 100644
--- a/spec/features/boards/boards_spec.rb
+++ b/spec/features/boards/boards_spec.rb
@@ -28,6 +28,12 @@ describe 'Issue Boards', feature: true, js: true do
expect(page).to have_content('Welcome to your Issue Board!')
end
+ it 'shows tooltip on add issues button' do
+ button = page.find('.issue-boards-search button', text: 'Add issues')
+
+ expect(button[:"data-original-title"]).to eq("Please add a list to your board first")
+ end
+
it 'hides the blank state when clicking nevermind button' do
page.within(find('.board-blank-state')) do
click_button("Nevermind, I'll use my own")
diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb
index 7651364703e..59e87b3f69c 100644
--- a/spec/features/boards/sidebar_spec.rb
+++ b/spec/features/boards/sidebar_spec.rb
@@ -15,8 +15,11 @@ describe 'Issue Boards', feature: true, js: true do
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) }
+ let(:card) { first('.board').first('.card') }
before do
+ Timecop.freeze
+
project.team << [user, :master]
login_as(user)
@@ -25,32 +28,28 @@ describe 'Issue Boards', feature: true, js: true do
wait_for_vue_resource
end
+ after do
+ Timecop.return
+ end
+
it 'shows sidebar when clicking issue' do
- page.within(first('.board')) do
- first('.card').click
- end
+ click_card(card)
expect(page).to have_selector('.issue-boards-sidebar')
end
it 'closes sidebar when clicking issue' do
- page.within(first('.board')) do
- first('.card').click
- end
+ click_card(card)
expect(page).to have_selector('.issue-boards-sidebar')
- page.within(first('.board')) do
- first('.card').click
- end
+ click_card(card)
expect(page).not_to have_selector('.issue-boards-sidebar')
end
it 'closes sidebar when clicking close button' do
- page.within(first('.board')) do
- first('.card').click
- end
+ click_card(card)
expect(page).to have_selector('.issue-boards-sidebar')
@@ -60,9 +59,7 @@ describe 'Issue Boards', feature: true, js: true do
end
it 'shows issue details when sidebar is open' do
- page.within(first('.board')) do
- first('.card').click
- end
+ click_card(card)
page.within('.issue-boards-sidebar') do
expect(page).to have_content(issue2.title)
@@ -70,15 +67,15 @@ describe 'Issue Boards', feature: true, js: true do
end
end
- it 'removes card from board when clicking remove button' do
- page.within(first('.board')) do
- first('.card').click
- end
+ it 'removes card from board when clicking ' do
+ click_card(card)
page.within('.issue-boards-sidebar') do
click_button 'Remove from board'
end
+ wait_for_vue_resource
+
page.within(first('.board')) do
expect(page).to have_selector('.card', count: 1)
end
@@ -86,9 +83,7 @@ describe 'Issue Boards', feature: true, js: true do
context 'assignee' do
it 'updates the issues assignee' do
- page.within(first('.board')) do
- first('.card').click
- end
+ click_card(card)
page.within('.assignee') do
click_link 'Edit'
@@ -104,17 +99,12 @@ describe 'Issue Boards', feature: true, js: true do
expect(page).to have_content(user.name)
end
- page.within(first('.board')) do
- page.within(first('.card')) do
- expect(page).to have_selector('.avatar')
- end
- end
+ expect(card).to have_selector('.avatar')
end
it 'removes the assignee' do
- page.within(first('.board')) do
- find('.card:nth-child(2)').click
- end
+ card_two = first('.board').find('.card:nth-child(2)')
+ click_card(card_two)
page.within('.assignee') do
click_link 'Edit'
@@ -130,17 +120,11 @@ describe 'Issue Boards', feature: true, js: true do
expect(page).to have_content('No assignee')
end
- page.within(first('.board')) do
- page.within(find('.card:nth-child(2)')) do
- expect(page).not_to have_selector('.avatar')
- end
- end
+ expect(card_two).not_to have_selector('.avatar')
end
it 'assignees to current user' do
- page.within(first('.board')) do
- first('.card').click
- end
+ click_card(card)
page.within(find('.assignee')) do
expect(page).to have_content('No assignee')
@@ -152,17 +136,11 @@ describe 'Issue Boards', feature: true, js: true do
expect(page).to have_content(user.name)
end
- page.within(first('.board')) do
- page.within(first('.card')) do
- expect(page).to have_selector('.avatar')
- end
- end
+ expect(card).to have_selector('.avatar')
end
it 'resets assignee dropdown' do
- page.within(first('.board')) do
- first('.card').click
- end
+ click_card(card)
page.within('.assignee') do
click_link 'Edit'
@@ -192,9 +170,7 @@ describe 'Issue Boards', feature: true, js: true do
context 'milestone' do
it 'adds a milestone' do
- page.within(first('.board')) do
- first('.card').click
- end
+ click_card(card)
page.within('.milestone') do
click_link 'Edit'
@@ -212,9 +188,7 @@ describe 'Issue Boards', feature: true, js: true do
end
it 'removes a milestone' do
- page.within(first('.board')) do
- find('.card:nth-child(2)').click
- end
+ click_card(card)
page.within('.milestone') do
click_link 'Edit'
@@ -234,9 +208,7 @@ describe 'Issue Boards', feature: true, js: true do
context 'due date' do
it 'updates due date' do
- page.within(first('.board')) do
- first('.card').click
- end
+ click_card(card)
page.within('.due_date') do
click_link 'Edit'
@@ -252,9 +224,7 @@ describe 'Issue Boards', feature: true, js: true do
context 'labels' do
it 'adds a single label' do
- page.within(first('.board')) do
- first('.card').click
- end
+ click_card(card)
page.within('.labels') do
click_link 'Edit'
@@ -273,18 +243,12 @@ describe 'Issue Boards', feature: true, js: true do
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(bug.title)
- end
- end
+ expect(card).to have_selector('.label', count: 2)
+ expect(card).to have_content(bug.title)
end
it 'adds a multiple labels' do
- page.within(first('.board')) do
- first('.card').click
- end
+ click_card(card)
page.within('.labels') do
click_link 'Edit'
@@ -305,19 +269,13 @@ describe 'Issue Boards', feature: true, js: true do
end
end
- page.within(first('.board')) do
- page.within(first('.card')) do
- expect(page).to have_selector('.label', count: 3)
- expect(page).to have_content(bug.title)
- expect(page).to have_content(regression.title)
- end
- end
+ expect(card).to have_selector('.label', count: 3)
+ expect(card).to have_content(bug.title)
+ expect(card).to have_content(regression.title)
end
it 'removes a label' do
- page.within(first('.board')) do
- first('.card').click
- end
+ click_card(card)
page.within('.labels') do
click_link 'Edit'
@@ -336,20 +294,14 @@ describe 'Issue Boards', feature: true, js: true do
end
end
- page.within(first('.board')) do
- page.within(first('.card')) do
- expect(page).not_to have_selector('.label')
- expect(page).not_to have_content(stretch.title)
- end
- end
+ expect(card).not_to have_selector('.label')
+ expect(card).not_to have_content(stretch.title)
end
end
context 'subscription' do
it 'changes issue subscription' do
- page.within(first('.board')) do
- first('.card').click
- end
+ click_card(card)
page.within('.subscription') do
click_button 'Subscribe'
@@ -358,4 +310,19 @@ describe 'Issue Boards', feature: true, js: true do
end
end
end
+
+ def click_card(card)
+ page.within(card) do
+ first('.card-number').click
+ end
+
+ wait_for_sidebar
+ end
+
+ def wait_for_sidebar
+ # loop until the CSS transition is complete
+ Timeout.timeout(0.5) do
+ loop until evaluate_script('$(".right-sidebar").outerWidth()') == 290
+ end
+ end
end
diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb
index 8f561c8f90b..324ede798fe 100644
--- a/spec/features/commits_spec.rb
+++ b/spec/features/commits_spec.rb
@@ -153,7 +153,7 @@ describe 'Commits' do
expect(page).to have_content pipeline.git_author_name
expect(page).to have_link('Download artifacts')
expect(page).not_to have_link('Cancel running')
- expect(page).not_to have_link('Retry failed')
+ expect(page).not_to have_link('Retry')
end
end
@@ -172,7 +172,7 @@ describe 'Commits' do
expect(page).to have_content pipeline.git_author_name
expect(page).not_to have_link('Download artifacts')
expect(page).not_to have_link('Cancel running')
- expect(page).not_to have_link('Retry failed')
+ expect(page).not_to have_link('Retry')
end
end
end
diff --git a/spec/features/dashboard/active_tab_spec.rb b/spec/features/dashboard/active_tab_spec.rb
index 7d59fcac517..ae750be4d4a 100644
--- a/spec/features/dashboard/active_tab_spec.rb
+++ b/spec/features/dashboard/active_tab_spec.rb
@@ -1,14 +1,15 @@
require 'spec_helper'
-RSpec.describe 'Dashboard Active Tab', feature: true do
+RSpec.describe 'Dashboard Active Tab', js: true, 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)
+ find('.global-dropdown-toggle').trigger('click')
+ expect(page).to have_selector('.global-dropdown-menu li.active', count: 1)
+ expect(find('.global-dropdown-menu li.active')).to have_content(title)
end
end
diff --git a/spec/features/dashboard/issuables_counter_spec.rb b/spec/features/dashboard/issuables_counter_spec.rb
index 41dcfe439c2..a1718912fc6 100644
--- a/spec/features/dashboard/issuables_counter_spec.rb
+++ b/spec/features/dashboard/issuables_counter_spec.rb
@@ -35,8 +35,9 @@ describe 'Navigation bar counter', feature: true, js: true, caching: true do
end
def expect_counters(issuable_type, count)
- dashboard_count = find('li.active span.badge')
- nav_count = find(".dashboard-shortcuts-#{issuable_type} span.count")
+ dashboard_count = find('li.active')
+ find('.global-dropdown-toggle').click
+ nav_count = find(".dashboard-shortcuts-#{issuable_type}")
expect(nav_count).to have_content(count)
expect(dashboard_count).to have_content(count)
diff --git a/spec/features/dashboard/issues_spec.rb b/spec/features/dashboard/issues_spec.rb
new file mode 100644
index 00000000000..2db1cf71209
--- /dev/null
+++ b/spec/features/dashboard/issues_spec.rb
@@ -0,0 +1,48 @@
+require 'spec_helper'
+
+RSpec.describe 'Dashboard Issues', feature: true do
+ let(:current_user) { create :user }
+ let(:public_project) { create(:empty_project, :public) }
+ let(:project) do
+ create(:empty_project) do |project|
+ project.team << [current_user, :master]
+ end
+ end
+
+ let!(:authored_issue) { create :issue, author: current_user, project: project }
+ let!(:authored_issue_on_public_project) { create :issue, author: current_user, project: public_project }
+ let!(:assigned_issue) { create :issue, assignee: current_user, project: project }
+ let!(:other_issue) { create :issue, project: project }
+
+ before do
+ login_as(current_user)
+
+ visit issues_dashboard_path(assignee_id: current_user.id)
+ end
+
+ it 'shows issues assigned to current user' do
+ expect(page).to have_content(assigned_issue.title)
+ expect(page).not_to have_content(authored_issue.title)
+ expect(page).not_to have_content(other_issue.title)
+ end
+
+ it 'shows issues when current user is author', js: true do
+ find('#assignee_id', visible: false).set('')
+ find('.js-author-search', match: :first).click
+ find('.dropdown-menu-author li a', match: :first, text: current_user.to_reference).click
+
+ expect(page).to have_content(authored_issue.title)
+ expect(page).to have_content(authored_issue_on_public_project.title)
+ expect(page).not_to have_content(assigned_issue.title)
+ expect(page).not_to have_content(other_issue.title)
+ end
+
+ it 'shows all issues' do
+ click_link('Reset filters')
+
+ expect(page).to have_content(authored_issue.title)
+ expect(page).to have_content(authored_issue_on_public_project.title)
+ expect(page).to have_content(assigned_issue.title)
+ expect(page).to have_content(other_issue.title)
+ end
+end
diff --git a/spec/features/dashboard/project_member_activity_index_spec.rb b/spec/features/dashboard/project_member_activity_index_spec.rb
index ba77093a6d4..49d93db58a9 100644
--- a/spec/features/dashboard/project_member_activity_index_spec.rb
+++ b/spec/features/dashboard/project_member_activity_index_spec.rb
@@ -12,7 +12,7 @@ feature 'Project member activity', feature: true, js: true do
def visit_activities_and_wait_with_event(event_type)
Event.create(project: project, author_id: user.id, action: event_type)
- visit activity_namespace_project_path(project.namespace.path, project.path)
+ visit activity_namespace_project_path(project.namespace, project)
wait_for_ajax
end
diff --git a/spec/features/dashboard/shortcuts_spec.rb b/spec/features/dashboard/shortcuts_spec.rb
index d9be4e5dbdd..62a2c54c94c 100644
--- a/spec/features/dashboard/shortcuts_spec.rb
+++ b/spec/features/dashboard/shortcuts_spec.rb
@@ -10,20 +10,20 @@ feature 'Dashboard shortcuts', feature: true, js: true do
find('body').native.send_key('g')
find('body').native.send_key('p')
- ensure_active_main_tab('Projects')
+ check_page_title('Projects')
find('body').native.send_key('g')
find('body').native.send_key('i')
- ensure_active_main_tab('Issues')
+ check_page_title('Issues')
find('body').native.send_key('g')
find('body').native.send_key('m')
- ensure_active_main_tab('Merge Requests')
+ check_page_title('Merge Requests')
end
- def ensure_active_main_tab(content)
- expect(find('.nav-sidebar li.active')).to have_content(content)
+ def check_page_title(title)
+ expect(find('.header-content .title')).to have_content(title)
end
end
diff --git a/spec/features/dashboard_issues_spec.rb b/spec/features/dashboard_issues_spec.rb
index b898f9bc64f..aa75e1140f6 100644
--- a/spec/features/dashboard_issues_spec.rb
+++ b/spec/features/dashboard_issues_spec.rb
@@ -49,9 +49,9 @@ describe "Dashboard Issues filtering", feature: true, js: true do
visit_issues(milestone_title: '', assignee_id: user.id)
link = find('.nav-controls a', text: 'Subscribe')
- params = CGI::parse(URI.parse(link[:href]).query)
+ 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)
+ 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' => [''])
diff --git a/spec/features/groups/members/list_spec.rb b/spec/features/groups/members/list_spec.rb
index 109de39b2dd..14c193f7450 100644
--- a/spec/features/groups/members/list_spec.rb
+++ b/spec/features/groups/members/list_spec.rb
@@ -30,6 +30,21 @@ feature 'Groups members list', feature: true do
expect(second_row).to be_blank
end
+ it 'updates user to owner level', :js do
+ group.add_owner(user1)
+ group.add_developer(user2)
+
+ visit group_group_members_path(group)
+
+ page.within(second_row) do
+ click_button('Developer')
+
+ click_link('Owner')
+
+ expect(page).to have_button('Owner')
+ end
+ end
+
def first_row
page.all('ul.content-list > li')[0]
end
diff --git a/spec/features/help_pages_spec.rb b/spec/features/help_pages_spec.rb
index 40a1fced8d8..e0b2404e60a 100644
--- a/spec/features/help_pages_spec.rb
+++ b/spec/features/help_pages_spec.rb
@@ -33,4 +33,30 @@ describe 'Help Pages', feature: true do
it_behaves_like 'help page', prefix: '/gitlab'
end
end
+
+ context 'in a production environment with version check enabled', js: true do
+ before do
+ allow(Rails.env).to receive(:production?) { true }
+ allow(current_application_settings).to receive(:version_check_enabled) { true }
+ allow_any_instance_of(VersionCheck).to receive(:url) { '/version-check-url' }
+
+ login_as :user
+ visit help_path
+ end
+
+ it 'should display a version check image' do
+ expect(find('.js-version-status-badge')).to be_visible
+ end
+
+ it 'should have a src url' do
+ expect(find('.js-version-status-badge')['src']).to match(/\/version-check-url/)
+ end
+
+ it 'should hide the version check image if the image request fails' do
+ # We use '--load-images=no' with poltergeist so we must trigger manually
+ execute_script("$('.js-version-status-badge').trigger('error');")
+
+ expect(find('.js-version-status-badge', visible: false)).not_to be_visible
+ end
+ end
end
diff --git a/spec/features/issuables/issuable_list_spec.rb b/spec/features/issuables/issuable_list_spec.rb
index e31bc40adc3..b90bf6268fd 100644
--- a/spec/features/issuables/issuable_list_spec.rb
+++ b/spec/features/issuables/issuable_list_spec.rb
@@ -30,6 +30,13 @@ describe 'issuable list', feature: true do
end
end
+ it "counts merge requests closing issues icons for each issue" do
+ visit_issuable_list(:issue)
+
+ expect(page).to have_selector('.icon-merge-request-unmerged', count: 1)
+ expect(first('.icon-merge-request-unmerged').find(:xpath, '..')).to have_content(1)
+ end
+
def visit_issuable_list(issuable_type)
if issuable_type == :issue
visit namespace_project_issues_path(project.namespace, project)
@@ -40,11 +47,12 @@ describe 'issuable list', feature: true do
def create_issuables(issuable_type)
3.times do
- if issuable_type == :issue
- issuable = create(:issue, project: project, author: user)
- else
- issuable = create(:merge_request, title: FFaker::Lorem.sentence, source_project: project, source_branch: FFaker::Name.name)
- end
+ issuable =
+ if issuable_type == :issue
+ create(:issue, project: project, author: user)
+ else
+ create(:merge_request, title: FFaker::Lorem.sentence, source_project: project, source_branch: FFaker::Name.name)
+ end
2.times do
create(:note_on_issue, noteable: issuable, project: project, note: 'Test note')
@@ -53,5 +61,15 @@ describe 'issuable list', feature: true do
create(:award_emoji, :downvote, awardable: issuable)
create(:award_emoji, :upvote, awardable: issuable)
end
+
+ if issuable_type == :issue
+ issue = Issue.reorder(:iid).first
+ merge_request = create(:merge_request,
+ title: FFaker::Lorem.sentence,
+ source_project: project,
+ source_branch: FFaker::Name.name)
+
+ MergeRequestsClosingIssues.create!(issue: issue, merge_request: merge_request)
+ end
end
end
diff --git a/spec/features/issues/bulk_assignment_labels_spec.rb b/spec/features/issues/bulk_assignment_labels_spec.rb
index 832757b24d4..2f59630b4fb 100644
--- a/spec/features/issues/bulk_assignment_labels_spec.rb
+++ b/spec/features/issues/bulk_assignment_labels_spec.rb
@@ -55,7 +55,7 @@ feature 'Issues > Labels bulk assignment', feature: true do
context 'to all issues' do
before do
check 'check_all_issues'
- open_labels_dropdown ['bug', 'feature']
+ open_labels_dropdown %w(bug feature)
update_issues
end
@@ -70,7 +70,7 @@ feature 'Issues > Labels bulk assignment', feature: true do
context 'to a issue' do
before do
check "selected_issue_#{issue1.id}"
- open_labels_dropdown ['bug', 'feature']
+ open_labels_dropdown %w(bug feature)
update_issues
end
@@ -112,7 +112,7 @@ feature 'Issues > Labels bulk assignment', feature: true do
visit namespace_project_issues_path(project.namespace, project)
check 'check_all_issues'
- unmark_labels_in_dropdown ['bug', 'feature']
+ unmark_labels_in_dropdown %w(bug feature)
update_issues
end
diff --git a/spec/features/issues/filtered_search/dropdown_label_spec.rb b/spec/features/issues/filtered_search/dropdown_label_spec.rb
index c6a88e1b7b0..ab3b868fd3a 100644
--- a/spec/features/issues/filtered_search/dropdown_label_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_label_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe 'Dropdown label', js: true, feature: true do
+ include FilteredSearchHelpers
+
let(:project) { create(:empty_project) }
let(:user) { create(:user) }
let(:filtered_search) { find('.filtered-search') }
@@ -17,12 +19,6 @@ describe 'Dropdown label', js: true, feature: true do
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)
diff --git a/spec/features/issues/filtered_search/filter_issues_spec.rb b/spec/features/issues/filtered_search/filter_issues_spec.rb
index 64f448a83b7..0420e64d42c 100644
--- a/spec/features/issues/filtered_search/filter_issues_spec.rb
+++ b/spec/features/issues/filtered_search/filter_issues_spec.rb
@@ -1,6 +1,7 @@
require 'rails_helper'
describe 'Filter issues', js: true, feature: true do
+ include FilteredSearchHelpers
include WaitForAjax
let!(:group) { create(:group) }
@@ -17,19 +18,6 @@ describe 'Filter issues', js: true, feature: true do
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
diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb
index 93139dc9e94..7135565294b 100644
--- a/spec/features/issues/gfm_autocomplete_spec.rb
+++ b/spec/features/issues/gfm_autocomplete_spec.rb
@@ -182,6 +182,20 @@ feature 'GFM autocomplete', feature: true, js: true do
expect(page).not_to have_selector('.atwho-view')
end
+ it 'triggers autocomplete after selecting a slash command' do
+ note = find('#note_note')
+ page.within '.timeline-content-form' do
+ note.native.send_keys('')
+ note.native.send_keys('/as')
+ note.click
+ end
+
+ find('.atwho-view li', text: '/assign').native.send_keys(:tab)
+
+ user_item = find('.atwho-view li', text: user.username)
+ expect(user_item).to have_content(user.username)
+ end
+
def expect_to_wrap(should_wrap, item, note, value)
expect(item).to have_content(value)
expect(item).not_to have_content("\"#{value}\"")
diff --git a/spec/features/issues/issue_sidebar_spec.rb b/spec/features/issues/issue_sidebar_spec.rb
index 1eb981942ea..7b9d4534ada 100644
--- a/spec/features/issues/issue_sidebar_spec.rb
+++ b/spec/features/issues/issue_sidebar_spec.rb
@@ -7,9 +7,9 @@ feature 'Issue Sidebar', feature: true do
let(:project) { create(:project, :public) }
let(:issue) { create(:issue, project: project) }
let!(:user) { create(:user)}
+ let!(:label) { create(:label, project: project, title: 'bug') }
before do
- create(:label, project: project, title: 'bug')
login_as(user)
end
@@ -50,16 +50,6 @@ feature 'Issue Sidebar', feature: true do
visit_issue(project, issue)
end
- describe 'when clicking on edit labels', js: true do
- it 'shows dropdown option to create a new label' do
- find('.block.labels .edit-link').click
-
- page.within('.block.labels') do
- expect(page).to have_content 'Create new'
- end
- 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'
@@ -77,36 +67,53 @@ feature 'Issue Sidebar', feature: true do
end
end
- context 'creating a new label', js: true do
- it 'shows option to crate a new label is present' do
+ context 'editing issue labels', js: true do
+ before do
page.within('.block.labels') do
find('.edit-link').click
+ end
+ end
+ it 'shows option to create a new label' do
+ page.within('.block.labels') do
expect(page).to have_content 'Create new'
end
end
- it 'shows dropdown switches to "create label" section' do
- page.within('.block.labels') do
- find('.edit-link').click
- click_link 'Create new'
+ context 'creating a new label', js: true do
+ before do
+ page.within('.block.labels') do
+ click_link 'Create new'
+ end
+ end
- expect(page).to have_content 'Create new label'
+ it 'shows dropdown switches to "create label" section' do
+ page.within('.block.labels') do
+ expect(page).to have_content 'Create new label'
+ end
end
- end
- it 'adds new label' do
- page.within('.block.labels') do
- find('.edit-link').click
- sleep 1
- click_link 'Create new'
+ it 'adds new label' do
+ page.within('.block.labels') do
+ fill_in 'new_label_name', with: 'wontfix'
+ page.find(".suggest-colors a", match: :first).click
+ click_button 'Create'
+
+ page.within('.dropdown-page-one') do
+ expect(page).to have_content 'wontfix'
+ end
+ end
+ end
- fill_in 'new_label_name', with: 'wontfix'
- page.find(".suggest-colors a", match: :first).click
- click_button 'Create'
+ it 'shows error message if label title is taken' do
+ page.within('.block.labels') do
+ fill_in 'new_label_name', with: label.title
+ page.find('.suggest-colors a', match: :first).click
+ click_button 'Create'
- page.within('.dropdown-page-one') do
- expect(page).to have_content 'wontfix'
+ page.within('.dropdown-page-two') do
+ expect(page).to have_content 'Title has already been taken'
+ end
end
end
end
diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb
index 755162a1eb5..1c8267b1593 100644
--- a/spec/features/issues_spec.rb
+++ b/spec/features/issues_spec.rb
@@ -1,6 +1,7 @@
require 'spec_helper'
describe 'Issues', feature: true do
+ include DropzoneHelper
include IssueHelpers
include SortingHelper
include WaitForAjax
@@ -150,7 +151,7 @@ describe 'Issues', feature: true do
describe 'Filter issue' do
before do
- ['foobar', 'barbaz', 'gitlab'].each do |title|
+ %w(foobar barbaz gitlab).each do |title|
create(:issue,
author: @user,
assignee: @user,
@@ -382,7 +383,9 @@ describe 'Issues', feature: true do
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
+ find('.incoming-email-token-reset').trigger('click')
+
+ wait_for_ajax
expect(page).to have_no_field('issue_email', with: previous_token)
new_token = project1.new_issue_address(@user.reload)
@@ -568,13 +571,16 @@ describe 'Issues', feature: true do
end
it 'uploads file when dragging into textarea' do
- drop_in_dropzone test_image_file
-
- # Wait for the file to upload
- sleep 1
+ dropzone_file Rails.root.join('spec', 'fixtures', 'banana_sample.gif')
expect(page.find_field("issue_description").value).to have_content 'banana_sample'
end
+
+ it 'adds double newline to end of attachment markdown' do
+ dropzone_file Rails.root.join('spec', 'fixtures', 'banana_sample.gif')
+
+ expect(page.find_field("issue_description").value).to match /\n\n$/
+ end
end
end
@@ -636,7 +642,7 @@ describe 'Issues', feature: true do
it 'removes due date from issue' do
date = Date.today.at_beginning_of_month + 2.days
-
+
page.within '.due_date' do
click_link 'Edit'
@@ -654,25 +660,4 @@ describe 'Issues', feature: true do
end
end
end
-
- def drop_in_dropzone(file_path)
- # Generate a fake input selector
- page.execute_script <<-JS
- var fakeFileInput = window.$('<input/>').attr(
- {id: 'fakeFileInput', type: 'file'}
- ).appendTo('body');
- JS
- # Attach the file to the fake input selector with Capybara
- attach_file("fakeFileInput", file_path)
- # Add the file to a fileList array and trigger the fake drop event
- page.execute_script <<-JS
- var fileList = [$('#fakeFileInput')[0].files[0]];
- var e = jQuery.Event('drop', { dataTransfer : { files : fileList } });
- $('.div-dropzone')[0].dropzone.listeners[0].events.drop(e);
- JS
- end
-
- def test_image_file
- File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif')
- end
end
diff --git a/spec/features/merge_requests/conflicts_spec.rb b/spec/features/merge_requests/conflicts_spec.rb
index d710a780111..18508a44184 100644
--- a/spec/features/merge_requests/conflicts_spec.rb
+++ b/spec/features/merge_requests/conflicts_spec.rb
@@ -154,7 +154,7 @@ feature 'Merge request conflict resolution', js: true, feature: true do
'conflict-binary-file' => 'when the conflicts contain a binary file',
'conflict-missing-side' => 'when the conflicts contain a file edited in one branch and deleted in another',
'conflict-non-utf8' => 'when the conflicts contain a non-UTF-8 file',
- }
+ }.freeze
UNRESOLVABLE_CONFLICTS.each do |source_branch, description|
context description do
diff --git a/spec/features/merge_requests/filter_by_labels_spec.rb b/spec/features/merge_requests/filter_by_labels_spec.rb
index 4c60329865c..55f3c1863ff 100644
--- a/spec/features/merge_requests/filter_by_labels_spec.rb
+++ b/spec/features/merge_requests/filter_by_labels_spec.rb
@@ -1,6 +1,8 @@
require 'rails_helper'
feature 'Issue filtering by Labels', feature: true, js: true do
+ include FilteredSearchHelpers
+ include MergeRequestHelpers
include WaitForAjax
let(:project) { create(:project, :public) }
@@ -32,123 +34,77 @@ feature 'Issue filtering by Labels', feature: true, js: true do
context 'filter by label bug' do
before do
- select_labels('bug')
+ input_filtered_search('label:~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')
+ input_filtered_search('label:~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')
+ input_filtered_search('label:~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')
+ input_filtered_search('label:~bug label:~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
+ context 'clear button' 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
+ input_filtered_search('label:~bug')
end
it 'allows user to remove filtered labels' do
- first('.js-label-filter-remove').click
- wait_for_ajax
+ first('.clear-search').click
+ filtered_search.send_keys(:enter)
- expect(find('.filtered-labels', visible: false)).not_to have_content 'bug'
- expect(find('.labels-filter')).not_to have_content 'bug'
+ expect(page).to have_issuable_counts(open: 3, closed: 0, all: 3)
+ expect(page).to have_content "Bugfix2"
+ expect(page).to have_content "Feature1"
+ expect(page).to have_content "Bugfix1"
end
end
- context 'dropdown filtering' do
+ context 'filter dropdown' 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
+ init_label_search
+ filtered_search.send_keys('~bug')
- 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()")
+ page.within '.filter-dropdown' do
+ expect(page).not_to have_content 'enhancement'
+ expect(page).to have_content 'bug'
+ end
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_by_milestone_spec.rb b/spec/features/merge_requests/filter_by_milestone_spec.rb
index f6e9230c8da..5608cda28f8 100644
--- a/spec/features/merge_requests/filter_by_milestone_spec.rb
+++ b/spec/features/merge_requests/filter_by_milestone_spec.rb
@@ -1,10 +1,18 @@
require 'rails_helper'
feature 'Merge Request filtering by Milestone', feature: true do
+ include FilteredSearchHelpers
+ include MergeRequestHelpers
+
let(:project) { create(:project, :public) }
let!(:user) { create(:user)}
let(:milestone) { create(:milestone, project: project) }
+ def filter_by_milestone(title)
+ find(".js-milestone-select").click
+ find(".milestone-filter a", text: title).click
+ end
+
before do
project.team << [user, :master]
login_as(user)
@@ -15,42 +23,42 @@ feature 'Merge Request filtering by Milestone', feature: true do
create(:merge_request, :simple, source_project: project, milestone: milestone)
visit_merge_requests(project)
- filter_by_milestone(Milestone::None.title)
+ input_filtered_search('milestone:none')
expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
expect(page).to have_css('.merge-request', count: 1)
end
context 'filters by upcoming milestone', js: true do
- it 'does not show issues with no expiry' do
+ it 'does not show merge requests with no expiry' do
create(:merge_request, :with_diffs, source_project: project)
create(:merge_request, :simple, source_project: project, milestone: milestone)
visit_merge_requests(project)
- filter_by_milestone(Milestone::Upcoming.title)
+ input_filtered_search('milestone:upcoming')
expect(page).to have_css('.merge-request', count: 0)
end
- it 'shows issues in future' do
+ it 'shows merge requests in future' do
milestone = create(:milestone, project: project, due_date: Date.tomorrow)
create(:merge_request, :with_diffs, source_project: project)
create(:merge_request, :simple, source_project: project, milestone: milestone)
visit_merge_requests(project)
- filter_by_milestone(Milestone::Upcoming.title)
+ input_filtered_search('milestone:upcoming')
expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
expect(page).to have_css('.merge-request', count: 1)
end
- it 'does not show issues in past' do
+ it 'does not show merge requests in past' do
milestone = create(:milestone, project: project, due_date: Date.yesterday)
create(:merge_request, :with_diffs, source_project: project)
create(:merge_request, :simple, source_project: project, milestone: milestone)
visit_merge_requests(project)
- filter_by_milestone(Milestone::Upcoming.title)
+ input_filtered_search('milestone:upcoming')
expect(page).to have_css('.merge-request', count: 0)
end
@@ -61,7 +69,7 @@ feature 'Merge Request filtering by Milestone', feature: true do
create(:merge_request, :simple, source_project: project)
visit_merge_requests(project)
- filter_by_milestone(milestone.title)
+ input_filtered_search("milestone:%'#{milestone.title}'")
expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
expect(page).to have_css('.merge-request', count: 1)
@@ -77,19 +85,10 @@ feature 'Merge Request filtering by Milestone', feature: true do
create(:merge_request, :simple, source_project: project)
visit_merge_requests(project)
- filter_by_milestone(milestone.title)
+ input_filtered_search("milestone:%\"#{milestone.title}\"")
expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
expect(page).to have_css('.merge-request', count: 1)
end
end
-
- def visit_merge_requests(project)
- visit namespace_project_merge_requests_path(project.namespace, project)
- end
-
- def filter_by_milestone(title)
- find(".js-milestone-select").click
- find(".milestone-filter a", text: title).click
- end
end
diff --git a/spec/features/merge_requests/filter_merge_requests_spec.rb b/spec/features/merge_requests/filter_merge_requests_spec.rb
index 4642b5a530d..6579a88d4ab 100644
--- a/spec/features/merge_requests/filter_merge_requests_spec.rb
+++ b/spec/features/merge_requests/filter_merge_requests_spec.rb
@@ -1,11 +1,13 @@
require 'rails_helper'
describe 'Filter merge requests', feature: true do
+ include FilteredSearchHelpers
+ include MergeRequestHelpers
include WaitForAjax
let!(:project) { create(:project) }
let!(:group) { create(:group) }
- let!(:user) { create(:user)}
+ 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") }
@@ -15,183 +17,134 @@ describe 'Filter merge requests', feature: true do
group.add_developer(user)
login_as(user)
create(:merge_request, source_project: project, target_project: project)
+
+ visit namespace_project_merge_requests_path(project.namespace, project)
end
describe 'for assignee from mr#index' do
- before do
- visit namespace_project_merge_requests_path(project.namespace, project)
+ let(:search_query) { "assignee:@#{user.username}" }
- find('.js-assignee-search').click
-
- find('.dropdown-menu-user-link', text: user.username).click
+ before do
+ input_filtered_search(search_query)
- wait_for_ajax
+ expect_mr_list_count(0)
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)
+ expect_filtered_search_input(search_query)
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_filtered_search_input(search_query)
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_filtered_search_input(search_query)
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
+ let(:search_query) { "milestone:%#{milestone.title}" }
- find('.milestone-filter .dropdown-content a', text: milestone.title).click
+ before do
+ input_filtered_search(search_query)
- wait_for_ajax
+ expect_mr_list_count(0)
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)
+ expect_filtered_search_input(search_query)
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)
+ expect_filtered_search_input(search_query)
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)
+ expect_filtered_search_input(search_query)
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
+ input_filtered_search('label:none')
- page.within '.labels-filter' do
- expect(page).to have_content 'Labels'
- end
- expect(find('.js-label-select .dropdown-toggle-text')).to have_content('Labels')
+ expect_mr_list_count(1)
+ expect_filtered_search_input('label:none')
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)
+ input_filtered_search("label:~#{label.title}")
+
+ expect_mr_list_count(0)
+ expect_filtered_search_input("label:~#{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
+ input_filtered_search("label:~\"#{wontfix.title}\" label:~#{label.title}")
- expect(find('.js-label-select .dropdown-toggle-text')).to have_content("#{wontfix.title} +1 more")
+ expect_mr_list_count(0)
+ expect_filtered_search_input("label:~\"#{wontfix.title}\" label:~#{label.title}")
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
+ input_filtered_search("label:~\"#{wontfix.title}\"")
- expect(find('.filtered-labels')).to have_content(wontfix.title)
- expect(find('.filtered-labels')).to have_content(label.title)
+ expect_mr_list_count(0)
+ expect_filtered_search_input("label:~\"#{wontfix.title}\"")
- find('.js-label-select').click
- wait_for_ajax
+ input_filtered_search_keys(" label:~#{label.title}")
- 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
+ expect_filtered_search_input("label:~\"#{wontfix.title}\" label:~#{label.title}")
- 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')
+ expect_mr_list_count(0)
+ expect_filtered_search_input("label:~\"#{wontfix.title}\" label:~#{label.title}")
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
+ let(:search_query) { "assignee:@#{user.username} label:~#{label.title}" }
- find('.dropdown-menu-user-link', text: user.username).click
+ before do
+ input_filtered_search("assignee:@#{user.username}")
- expect(page).not_to have_selector('.mr-list .merge-request')
+ expect_mr_list_count(1)
+ expect_filtered_search_input("assignee:@#{user.username}")
- find('.js-label-select').click
+ input_filtered_search_keys(" label:~#{label.title}")
- find('.dropdown-menu-labels .dropdown-content a', text: label.title).click
- page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click
+ expect_mr_list_count(1)
- wait_for_ajax
+ find("#state-opened[href=\"#{URI.parse(current_url).path}?assignee_username=#{user.username}&label_name%5B%5D=#{label.title}&scope=all&state=opened\"]")
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)
+ expect_filtered_search_input(search_query)
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)
+ expect_filtered_search_input(search_query)
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)
+ expect_filtered_search_input(search_query)
end
end
end
@@ -203,11 +156,11 @@ describe 'Filter merge requests', feature: true do
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",
+ mr = create(:merge_request,
+ title: "Bug 2",
+ source_project: project,
+ target_project: project,
+ source_branch: "bug2",
milestone: milestone,
author: user,
assignee: user)
@@ -218,15 +171,13 @@ describe 'Filter merge requests', feature: true do
context 'only text', js: true do
it 'filters merge requests by searched text' do
- fill_in 'issuable_search', with: 'Bug'
+ input_filtered_search('bug')
- page.within '.mr-list' do
- expect(page).to have_selector('.merge-request', count: 2)
- end
+ expect_mr_list_count(2)
end
it 'does not show any merge requests' do
- fill_in 'issuable_search', with: 'testing'
+ input_filtered_search('testing')
page.within '.mr-list' do
expect(page).not_to have_selector('.merge-request')
@@ -234,82 +185,49 @@ describe 'Filter merge requests', feature: true do
end
end
- context 'text and dropdown options', js: true do
+ context 'filters and searches', js: true do
it 'filters by text and label' do
- fill_in 'issuable_search', with: 'Bug'
+ input_filtered_search('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
+ expect_mr_list_count(2)
+ expect_filtered_search_input('Bug')
- click_button 'Label'
- page.within '.labels-filter' do
- click_link 'bug'
- end
- find('.dropdown-menu-close-icon').click
+ input_filtered_search_keys(' label:~bug')
- 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
+ expect_mr_list_count(1)
end
it 'filters by text and milestone' do
- fill_in 'issuable_search', with: 'Bug'
+ input_filtered_search('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
+ expect_mr_list_count(2)
+ expect_filtered_search_input('Bug')
- click_button 'Milestone'
- page.within '.milestone-filter' do
- click_link '8'
- end
+ input_filtered_search_keys(' milestone:%8')
- 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
+ expect_mr_list_count(1)
end
it 'filters by text and assignee' do
- fill_in 'issuable_search', with: 'Bug'
+ input_filtered_search('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
+ expect_mr_list_count(2)
+ expect_filtered_search_input('Bug')
- click_button 'Assignee'
- page.within '.dropdown-menu-assignee' do
- click_link user.name
- end
+ input_filtered_search_keys(" assignee:@#{user.username}")
- 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
+ expect_mr_list_count(1)
end
it 'filters by text and author' do
- fill_in 'issuable_search', with: 'Bug'
+ input_filtered_search('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
+ expect_mr_list_count(2)
+ expect_filtered_search_input('Bug')
- click_button 'Author'
- page.within '.dropdown-menu-author' do
- click_link user.name
- end
+ input_filtered_search_keys(" author:@#{user.username}")
- 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
+ expect_mr_list_count(1)
end
end
end
@@ -328,18 +246,9 @@ describe 'Filter merge requests', feature: true do
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
+ input_filtered_search('label:~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
+ expect_mr_list_count(2)
click_button 'Last created'
page.within '.dropdown-menu-sort' do
@@ -352,4 +261,38 @@ describe 'Filter merge requests', feature: true do
end
end
end
+
+ describe 'filter by assignee id', js: true do
+ it 'filter by current user' do
+ visit namespace_project_merge_requests_path(project.namespace, project, assignee_id: user.id)
+
+ expect_filtered_search_input("assignee:@#{user.username}")
+ end
+
+ it 'filter by new user' do
+ new_user = create(:user)
+ project.add_developer(new_user)
+
+ visit namespace_project_merge_requests_path(project.namespace, project, assignee_id: new_user.id)
+
+ expect_filtered_search_input("assignee:@#{new_user.username}")
+ end
+ end
+
+ describe 'filter by author id', js: true do
+ it 'filter by current user' do
+ visit namespace_project_merge_requests_path(project.namespace, project, author_id: user.id)
+
+ expect_filtered_search_input("author:@#{user.username}")
+ end
+
+ it 'filter by new user' do
+ new_user = create(:user)
+ project.add_developer(new_user)
+
+ visit namespace_project_merge_requests_path(project.namespace, project, author_id: new_user.id)
+
+ expect_filtered_search_input("author:@#{new_user.username}")
+ end
+ end
end
diff --git a/spec/features/merge_requests/reset_filters_spec.rb b/spec/features/merge_requests/reset_filters_spec.rb
index 3a7ece7e1d6..58f11499e3f 100644
--- a/spec/features/merge_requests/reset_filters_spec.rb
+++ b/spec/features/merge_requests/reset_filters_spec.rb
@@ -1,17 +1,20 @@
require 'rails_helper'
feature 'Issues filter reset button', feature: true, js: true do
+ include FilteredSearchHelpers
+ include MergeRequestHelpers
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!(: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' }
+ let(:merge_request_css) { '.merge-request' }
+ let(:clear_search_css) { '.filtered-search-input-container .clear-search' }
before do
mr2.labels << bug
@@ -50,7 +53,7 @@ feature 'Issues filter reset button', feature: true, js: true do
context 'when author filter has been applied' do
it 'resets the author filter' do
- visit_merge_requests(project, author_id: user.id)
+ visit_merge_requests(project, author_username: user.username)
expect(page).to have_css(merge_request_css, count: 1)
reset_filters
@@ -60,7 +63,7 @@ feature 'Issues filter reset button', feature: true, js: true do
context 'when assignee filter has been applied' do
it 'resets the assignee filter' do
- visit_merge_requests(project, assignee_id: user.id)
+ visit_merge_requests(project, assignee_username: user.username)
expect(page).to have_css(merge_request_css, count: 1)
reset_filters
@@ -70,7 +73,7 @@ feature 'Issues filter reset button', feature: true, js: true do
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')
+ visit_merge_requests(project, assignee_username: user.username, author_username: user.username, milestone_title: milestone.title, label_name: bug.name, search: 'Bug')
expect(page).to have_css(merge_request_css, count: 0)
reset_filters
@@ -82,15 +85,7 @@ feature 'Issues filter reset button', feature: true, js: true 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'
+ expect(page).not_to have_css(clear_search_css)
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/milestone_spec.rb b/spec/features/milestone_spec.rb
index a2e40546588..c3297de709a 100644
--- a/spec/features/milestone_spec.rb
+++ b/spec/features/milestone_spec.rb
@@ -24,7 +24,7 @@ feature 'Milestone', feature: true do
find('input[name="commit"]').click
expect(find('.alert-success')).to have_content('Assign some issues to this milestone.')
- expect(page).to have_content('Nov 16, 2016 - Dec 16, 2016')
+ expect(page).to have_content('Nov 16, 2016–Dec 16, 2016')
end
end
diff --git a/spec/features/profile_spec.rb b/spec/features/profile_spec.rb
index 7a562b5e03d..406d7cf791c 100644
--- a/spec/features/profile_spec.rb
+++ b/spec/features/profile_spec.rb
@@ -4,7 +4,7 @@ describe 'Profile account page', feature: true do
let(:user) { create(:user) }
before do
- login_as :user
+ login_as(user)
end
describe 'when signup is enabled' do
@@ -16,7 +16,7 @@ describe 'Profile account page', feature: true do
it { expect(page).to have_content('Remove account') }
it 'deletes the account' do
- expect { click_link 'Delete account' }.to change { User.count }.by(-1)
+ expect { click_link 'Delete account' }.to change { User.where(id: user.id).count }.by(-1)
expect(current_path).to eq(new_user_session_path)
end
end
diff --git a/spec/features/profiles/keys_spec.rb b/spec/features/profiles/keys_spec.rb
index eb1050d21c6..2f436f153aa 100644
--- a/spec/features/profiles/keys_spec.rb
+++ b/spec/features/profiles/keys_spec.rb
@@ -15,7 +15,7 @@ feature 'Profile > SSH Keys', feature: true do
scenario 'auto-populates the title', js: true do
fill_in('Key', with: attributes_for(:key).fetch(:key))
- expect(find_field('Title').value).to eq 'dummy@gitlab.com'
+ expect(page).to have_field("Title", with: "dummy@gitlab.com")
end
scenario 'saves the new key' do
diff --git a/spec/features/profiles/preferences_spec.rb b/spec/features/profiles/preferences_spec.rb
index a6b841c0210..15c8677fcd3 100644
--- a/spec/features/profiles/preferences_spec.rb
+++ b/spec/features/profiles/preferences_spec.rb
@@ -8,35 +8,6 @@ describe 'Profile > Preferences', feature: true do
visit profile_preferences_path
end
- describe 'User changes their application theme', js: true do
- let(:default) { Gitlab::Themes.default }
- let(:theme) { Gitlab::Themes.by_id(5) }
-
- it 'creates a flash message' do
- choose "user_theme_id_#{theme.id}"
-
- expect_preferences_saved_message
- end
-
- it 'updates their preference' do
- choose "user_theme_id_#{theme.id}"
-
- allowing_for_delay do
- visit page.current_path
- expect(page).to have_checked_field("user_theme_id_#{theme.id}")
- end
- end
-
- it 'reflects the changes immediately' do
- expect(page).to have_selector("body.#{default.css_class}")
-
- choose "user_theme_id_#{theme.id}"
-
- expect(page).not_to have_selector("body.#{default.css_class}")
- expect(page).to have_selector("body.#{theme.css_class}")
- end
- end
-
describe 'User changes their syntax highlighting theme', js: true do
it 'creates a flash message' do
choose 'user_color_scheme_id_5'
diff --git a/spec/features/projects/badges/list_spec.rb b/spec/features/projects/badges/list_spec.rb
index 67a4a5d1ab1..ae9db0c0d6e 100644
--- a/spec/features/projects/badges/list_spec.rb
+++ b/spec/features/projects/badges/list_spec.rb
@@ -14,7 +14,8 @@ feature 'list of badges' do
expect(page).to have_content 'build status'
expect(page).to have_content 'Markdown'
expect(page).to have_content 'HTML'
- expect(page).to have_css('.highlight', count: 2)
+ expect(page).to have_content 'AsciiDoc'
+ expect(page).to have_css('.highlight', count: 3)
expect(page).to have_xpath("//img[@alt='build status']")
page.within('.highlight', match: :first) do
@@ -28,7 +29,8 @@ feature 'list of badges' do
expect(page).to have_content 'coverage report'
expect(page).to have_content 'Markdown'
expect(page).to have_content 'HTML'
- expect(page).to have_css('.highlight', count: 2)
+ expect(page).to have_content 'AsciiDoc'
+ expect(page).to have_css('.highlight', count: 3)
expect(page).to have_xpath("//img[@alt='coverage report']")
page.within('.highlight', match: :first) do
diff --git a/spec/features/projects/developer_views_empty_project_instructions_spec.rb b/spec/features/projects/developer_views_empty_project_instructions_spec.rb
index 0c51fe72ca4..2352329d58c 100644
--- a/spec/features/projects/developer_views_empty_project_instructions_spec.rb
+++ b/spec/features/projects/developer_views_empty_project_instructions_spec.rb
@@ -56,8 +56,14 @@ feature 'Developer views empty project instructions', feature: true do
end
def expect_instructions_for(protocol)
- msg = :"#{protocol.downcase}_url_to_repo"
-
- expect(page).to have_content("git clone #{project.send(msg)}")
+ url =
+ case protocol
+ when 'ssh'
+ project.ssh_url_to_repo
+ when 'http'
+ project.http_url_to_repo(developer)
+ end
+
+ expect(page).to have_content("git clone #{url}")
end
end
diff --git a/spec/features/projects/files/project_owner_creates_license_file_spec.rb b/spec/features/projects/files/project_owner_creates_license_file_spec.rb
index 64094af29c0..ccadc936567 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,7 @@ 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',
+ project.repository.delete_file(project_master, 'LICENSE',
message: 'Remove LICENSE', branch_name: 'master')
project.team << [project_master, :master]
login_as(project_master)
@@ -25,7 +25,7 @@ feature 'project owner creates a license file', feature: true, js: true do
select_template('MIT License')
file_content = first('.file-editor')
- expect(file_content).to have_content('The MIT License (MIT)')
+ expect(file_content).to have_content('MIT License')
expect(file_content).to have_content("Copyright (c) #{Time.now.year} #{project.namespace.human_name}")
fill_in :commit_message, with: 'Add a LICENSE file', visible: true
@@ -33,7 +33,7 @@ feature 'project owner creates a license file', feature: true, js: true do
expect(current_path).to eq(
namespace_project_blob_path(project.namespace, project, 'master/LICENSE'))
- expect(page).to have_content('The MIT License (MIT)')
+ expect(page).to have_content('MIT License')
expect(page).to have_content("Copyright (c) #{Time.now.year} #{project.namespace.human_name}")
end
@@ -49,7 +49,7 @@ feature 'project owner creates a license file', feature: true, js: true do
select_template('MIT License')
file_content = first('.file-editor')
- expect(file_content).to have_content('The MIT License (MIT)')
+ expect(file_content).to have_content('MIT License')
expect(file_content).to have_content("Copyright (c) #{Time.now.year} #{project.namespace.human_name}")
fill_in :commit_message, with: 'Add a LICENSE file', visible: true
@@ -57,7 +57,7 @@ feature 'project owner creates a license file', feature: true, js: true do
expect(current_path).to eq(
namespace_project_blob_path(project.namespace, project, 'master/LICENSE'))
- expect(page).to have_content('The MIT License (MIT)')
+ expect(page).to have_content('MIT License')
expect(page).to have_content("Copyright (c) #{Time.now.year} #{project.namespace.human_name}")
end
diff --git a/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb b/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb
index 4453b6d485f..420db962318 100644
--- a/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb
+++ b/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb
@@ -24,7 +24,7 @@ feature 'project owner sees a link to create a license file in empty project', f
select_template('MIT License')
file_content = first('.file-editor')
- expect(file_content).to have_content('The MIT License (MIT)')
+ expect(file_content).to have_content('MIT License')
expect(file_content).to have_content("Copyright (c) #{Time.now.year} #{project.namespace.human_name}")
fill_in :commit_message, with: 'Add a LICENSE file', visible: true
@@ -34,7 +34,7 @@ feature 'project owner sees a link to create a license file in empty project', f
expect(current_path).to eq(
namespace_project_blob_path(project.namespace, project, 'master/LICENSE'))
- expect(page).to have_content('The MIT License (MIT)')
+ expect(page).to have_content('MIT License')
expect(page).to have_content("Copyright (c) #{Time.now.year} #{project.namespace.human_name}")
end
diff --git a/spec/features/projects/import_export/export_file_spec.rb b/spec/features/projects/import_export/export_file_spec.rb
index 16dddb2a86b..40caf89dd54 100644
--- a/spec/features/projects/import_export/export_file_spec.rb
+++ b/spec/features/projects/import_export/export_file_spec.rb
@@ -9,7 +9,7 @@ feature 'Import/Export - project export integration test', feature: true, js: tr
include ExportFileHelper
let(:user) { create(:admin) }
- let(:export_path) { "#{Dir::tmpdir}/import_file_spec" }
+ let(:export_path) { "#{Dir.tmpdir}/import_file_spec" }
let(:config_hash) { YAML.load_file(Gitlab::ImportExport.config_file).deep_stringify_keys }
let(:sensitive_words) { %w[pass secret token key] }
diff --git a/spec/features/projects/import_export/import_file_spec.rb b/spec/features/projects/import_export/import_file_spec.rb
index 3015576f6f8..2d1106ea3e8 100644
--- a/spec/features/projects/import_export/import_file_spec.rb
+++ b/spec/features/projects/import_export/import_file_spec.rb
@@ -4,7 +4,7 @@ feature 'Import/Export - project import integration test', feature: true, js: tr
include Select2Helper
let(:file) { File.join(Rails.root, 'spec', 'features', 'projects', 'import_export', 'test_project_export.tar.gz') }
- let(:export_path) { "#{Dir::tmpdir}/import_file_spec" }
+ let(:export_path) { "#{Dir.tmpdir}/import_file_spec" }
background do
allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
diff --git a/spec/features/projects/import_export/namespace_export_file_spec.rb b/spec/features/projects/import_export/namespace_export_file_spec.rb
index d0bafc6168c..cb399ea55df 100644
--- a/spec/features/projects/import_export/namespace_export_file_spec.rb
+++ b/spec/features/projects/import_export/namespace_export_file_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
feature 'Import/Export - Namespace export file cleanup', feature: true, js: true do
- let(:export_path) { "#{Dir::tmpdir}/import_file_spec" }
+ 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) }
diff --git a/spec/features/projects/issuable_templates_spec.rb b/spec/features/projects/issuable_templates_spec.rb
index e90a033b8c4..62d0aedda48 100644
--- a/spec/features/projects/issuable_templates_spec.rb
+++ b/spec/features/projects/issuable_templates_spec.rb
@@ -18,20 +18,18 @@ feature 'issuable templates', feature: true, js: true do
let(:description_addition) { ' appending to description' }
background do
- project.repository.commit_file(
+ project.repository.create_file(
user,
'.gitlab/issue_templates/bug.md',
template_content,
message: 'added issue template',
- branch_name: 'master',
- update: false)
- project.repository.commit_file(
+ branch_name: 'master')
+ project.repository.create_file(
user,
'.gitlab/issue_templates/test.md',
longtemplate_content,
message: 'added issue template',
- branch_name: 'master',
- update: false)
+ branch_name: 'master')
visit edit_namespace_project_issue_path project.namespace, project, issue
fill_in :'issue[title]', with: 'test issue title'
end
@@ -79,13 +77,12 @@ feature 'issuable templates', feature: true, js: true do
let(:issue) { create(:issue, author: user, assignee: user, project: project) }
background do
- project.repository.commit_file(
+ project.repository.create_file(
user,
'.gitlab/issue_templates/bug.md',
template_content,
message: 'added issue template',
- branch_name: 'master',
- update: false)
+ branch_name: 'master')
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
@@ -104,13 +101,12 @@ 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(
+ project.repository.create_file(
user,
'.gitlab/merge_request_templates/feature-proposal.md',
template_content,
message: 'added merge request template',
- branch_name: 'master',
- update: false)
+ branch_name: 'master')
visit edit_namespace_project_merge_request_path project.namespace, project, merge_request
fill_in :'merge_request[title]', with: 'test merge request title'
end
@@ -135,13 +131,12 @@ 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(
+ project.repository.create_file(
fork_user,
'.gitlab/merge_request_templates/feature-proposal.md',
template_content,
message: 'added merge request template',
- branch_name: 'master',
- update: false)
+ branch_name: 'master')
visit edit_namespace_project_merge_request_path project.namespace, project, merge_request
fill_in :'merge_request[title]', with: 'test merge request title'
end
diff --git a/spec/features/projects/labels/issues_sorted_by_priority_spec.rb b/spec/features/projects/labels/issues_sorted_by_priority_spec.rb
index 81b0c991d4f..7414ce21f59 100644
--- a/spec/features/projects/labels/issues_sorted_by_priority_spec.rb
+++ b/spec/features/projects/labels/issues_sorted_by_priority_spec.rb
@@ -37,7 +37,7 @@ feature 'Issue prioritization', feature: true do
page.within('.issues-holder') do
issue_titles = all('.issues-list .issue-title-text').map(&:text)
- expect(issue_titles).to eq(['issue_4', 'issue_3', 'issue_5', 'issue_2', 'issue_1'])
+ expect(issue_titles).to eq(%w(issue_4 issue_3 issue_5 issue_2 issue_1))
end
end
end
@@ -77,7 +77,7 @@ feature 'Issue prioritization', feature: true do
expect(issue_titles[0..1]).to contain_exactly('issue_5', 'issue_8')
expect(issue_titles[2..4]).to contain_exactly('issue_1', 'issue_3', 'issue_7')
- expect(issue_titles[5..-1]).to eq(['issue_2', 'issue_4', 'issue_6'])
+ expect(issue_titles[5..-1]).to eq(%w(issue_2 issue_4 issue_6))
end
end
end
diff --git a/spec/features/projects/main/download_buttons_spec.rb b/spec/features/projects/main/download_buttons_spec.rb
index 227ccf9459c..02198ff3e41 100644
--- a/spec/features/projects/main/download_buttons_spec.rb
+++ b/spec/features/projects/main/download_buttons_spec.rb
@@ -39,6 +39,13 @@ feature 'Download buttons in project main page', feature: true do
expect(page).to have_link "Download '#{build.name}'", href: href
end
+
+ scenario 'download links have download attribute' do
+ expect(page).to have_selector('a', text: 'Download')
+ page.all('a', text: 'Download').each do |link|
+ expect(link[:download]).to eq ''
+ end
+ end
end
end
end
diff --git a/spec/features/projects/members/sorting_spec.rb b/spec/features/projects/members/sorting_spec.rb
index d6ebb523f95..c7a32a65e49 100644
--- a/spec/features/projects/members/sorting_spec.rb
+++ b/spec/features/projects/members/sorting_spec.rb
@@ -85,7 +85,7 @@ feature 'Projects > Members > Sorting', feature: true do
end
def visit_members_list(sort:)
- visit namespace_project_project_members_path(project.namespace.to_param, project.to_param, sort: sort)
+ visit namespace_project_project_members_path(project.namespace.to_param, project, sort: sort)
end
def first_member
diff --git a/spec/features/projects/new_project_spec.rb b/spec/features/projects/new_project_spec.rb
index b56e562b2b6..45185f2dd1f 100644
--- a/spec/features/projects/new_project_spec.rb
+++ b/spec/features/projects/new_project_spec.rb
@@ -19,6 +19,51 @@ feature "New project", feature: true do
end
end
+ context "Namespace selector" do
+ context "with user namespace" do
+ before do
+ visit new_project_path
+ end
+
+ it "selects the user namespace" do
+ namespace = find("#project_namespace_id")
+
+ expect(namespace.text).to eq user.username
+ end
+ end
+
+ context "with group namespace" do
+ let(:group) { create(:group, :private, owner: user) }
+
+ before do
+ group.add_owner(user)
+ visit new_project_path(namespace_id: group.id)
+ end
+
+ it "selects the group namespace" do
+ namespace = find("#project_namespace_id option[selected]")
+
+ expect(namespace.text).to eq group.name
+ end
+
+ context "on validation error" do
+ before do
+ fill_in('project_path', with: 'private-group-project')
+ choose('Internal')
+ click_button('Create project')
+
+ expect(page).to have_css '.project-edit-errors .alert.alert-danger'
+ end
+
+ it "selects the group namespace" do
+ namespace = find("#project_namespace_id option[selected]")
+
+ expect(namespace.text).to eq group.name
+ end
+ end
+ end
+ end
+
context 'Import project options' do
before do
visit new_project_path
diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb
index 0b5ccc8c515..9f06e52ab55 100644
--- a/spec/features/projects/pipelines/pipeline_spec.rb
+++ b/spec/features/projects/pipelines/pipeline_spec.rb
@@ -54,7 +54,7 @@ describe 'Pipeline', :feature, :js do
expect(page).to have_content('Build')
expect(page).to have_content('Test')
expect(page).to have_content('Deploy')
- expect(page).to have_content('Retry failed')
+ expect(page).to have_content('Retry')
expect(page).to have_content('Cancel running')
end
@@ -164,9 +164,9 @@ describe 'Pipeline', :feature, :js do
it { expect(page).not_to have_content('retried') }
context 'when retrying' do
- before { click_on 'Retry failed' }
+ before { find('.js-retry-button').trigger('click') }
- it { expect(page).not_to have_content('Retry failed') }
+ it { expect(page).not_to have_content('Retry') }
end
end
@@ -198,7 +198,7 @@ describe 'Pipeline', :feature, :js do
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('Retry')
expect(page).to have_content('Cancel running')
expect(page).to have_link('Play')
end
@@ -226,9 +226,9 @@ describe 'Pipeline', :feature, :js do
it { expect(page).not_to have_content('retried') }
context 'when retrying' do
- before { click_on 'Retry failed' }
+ before { find('.js-retry-button').trigger('click') }
- it { expect(page).not_to have_content('Retry failed') }
+ it { expect(page).not_to have_content('Retry') }
it { expect(page).to have_selector('.retried') }
end
end
diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb
index 6555b2fc6c1..22bf1bfbdf0 100644
--- a/spec/features/projects/pipelines/pipelines_spec.rb
+++ b/spec/features/projects/pipelines/pipelines_spec.rb
@@ -26,22 +26,66 @@ describe 'Pipelines', :feature, :js do
)
end
- [:all, :running, :branches].each do |scope|
- context "when displaying #{scope}" do
- before do
- visit_project_pipelines(scope: scope)
- end
+ context 'scope' do
+ before do
+ create(:ci_empty_pipeline, status: 'pending', project: project, sha: project.commit.id, ref: 'master')
+ create(:ci_empty_pipeline, status: 'running', project: project, sha: project.commit.id, ref: 'master')
+ create(:ci_empty_pipeline, status: 'created', project: project, sha: project.commit.id, ref: 'master')
+ create(:ci_empty_pipeline, status: 'success', project: project, sha: project.commit.id, ref: 'master')
+ end
- it 'contains pipeline commit short SHA' do
- expect(page).to have_content(pipeline.short_sha)
- end
+ [:all, :running, :pending, :finished, :branches].each do |scope|
+ context "when displaying #{scope}" do
+ before do
+ visit_project_pipelines(scope: scope)
+ end
+
+ it 'contains pipeline commit short SHA' do
+ expect(page).to have_content(pipeline.short_sha)
+ end
- it 'contains branch name' do
- expect(page).to have_content(pipeline.ref)
+ it 'contains branch name' do
+ expect(page).to have_content(pipeline.ref)
+ end
end
end
end
+ context 'header tabs' do
+ before do
+ visit namespace_project_pipelines_path(project.namespace, project)
+ wait_for_vue_resource
+ end
+
+ it 'shows a tab for All pipelines and count' do
+ expect(page.find('.js-pipelines-tab-all a').text).to include('All')
+ expect(page.find('.js-pipelines-tab-all .badge').text).to include('1')
+ end
+
+ it 'shows a tab for Pending pipelines and count' do
+ expect(page.find('.js-pipelines-tab-pending a').text).to include('Pending')
+ expect(page.find('.js-pipelines-tab-pending .badge').text).to include('0')
+ end
+
+ it 'shows a tab for Running pipelines and count' do
+ expect(page.find('.js-pipelines-tab-running a').text).to include('Running')
+ expect(page.find('.js-pipelines-tab-running .badge').text).to include('1')
+ end
+
+ it 'shows a tab for Finished pipelines and count' do
+ expect(page.find('.js-pipelines-tab-finished a').text).to include('Finished')
+ expect(page.find('.js-pipelines-tab-finished .badge').text).to include('0')
+ end
+
+ it 'shows a tab for Branches' do
+ expect(page.find('.js-pipelines-tab-branches a').text).to include('Branches')
+ end
+
+ it 'shows a tab for Tags' do
+ expect(page.find('.js-pipelines-tab-tags a').text).to include('Tags')
+ end
+ end
+
context 'when pipeline is cancelable' do
let!(:build) do
create(:ci_build, pipeline: pipeline,
@@ -218,6 +262,14 @@ describe 'Pipelines', :feature, :js do
expect(page).to have_link(with_artifacts.name)
end
+
+ it 'has download attribute on download links' do
+ find('.js-pipeline-dropdown-download').click
+ expect(page).to have_selector('a', text: 'Download')
+ page.all('.build-artifacts a', text: 'Download').each do |link|
+ expect(link[:download]).to eq ''
+ end
+ end
end
context 'with artifacts expired' do
@@ -276,6 +328,39 @@ describe 'Pipelines', :feature, :js do
expect(build.reload).to be_canceled
end
end
+
+ context 'dropdown jobs list' do
+ it 'should keep the dropdown open when the user ctr/cmd + clicks in the job name' do
+ find('.js-builds-dropdown-button').trigger('click')
+
+ execute_script('var e = $.Event("keydown", { keyCode: 64 }); $("body").trigger(e);')
+
+ find('.mini-pipeline-graph-dropdown-item').trigger('click')
+
+ expect(page).to have_selector('.js-ci-action-icon')
+ end
+ end
+ end
+
+ context 'with pagination' do
+ before do
+ allow(Ci::Pipeline).to receive(:default_per_page).and_return(1)
+ create(:ci_empty_pipeline, project: project)
+ end
+
+ it 'should render pagination' do
+ visit namespace_project_pipelines_path(project.namespace, project)
+ wait_for_vue_resource
+
+ expect(page).to have_selector('.gl-pagination')
+ end
+
+ it 'should render second page of pipelines' do
+ visit namespace_project_pipelines_path(project.namespace, project, page: '2')
+ wait_for_vue_resource
+
+ expect(page).to have_selector('.gl-pagination .page', count: 2)
+ end
end
end
@@ -286,8 +371,14 @@ describe 'Pipelines', :feature, :js do
visit new_namespace_project_pipeline_path(project.namespace, project)
end
- context 'for valid commit' do
- before { fill_in('pipeline[ref]', with: 'master') }
+ context 'for valid commit', js: true do
+ before do
+ click_button project.default_branch
+
+ page.within '.dropdown-menu' do
+ click_link 'master'
+ end
+ end
context 'with gitlab-ci.yml' do
before { stub_ci_pipeline_to_return_yaml_file }
@@ -304,15 +395,6 @@ describe 'Pipelines', :feature, :js do
it { expect(page).to have_content('Missing .gitlab-ci.yml file') }
end
end
-
- context 'for invalid commit' do
- before do
- fill_in('pipeline[ref]', with: 'invalid-reference')
- click_on 'Create pipeline'
- end
-
- it { expect(page).to have_content('Reference not found') }
- end
end
describe 'Create pipelines' do
@@ -324,18 +406,22 @@ describe 'Pipelines', :feature, :js do
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_selector('.js-branch-select')
+ expect(find('.js-branch-select')).to have_content project.default_branch
expect(page).to have_content('Create for')
end
end
describe 'find pipelines' do
it 'shows filtered pipelines', js: true do
- fill_in('pipeline[ref]', with: 'fix')
- find('input#ref').native.send_keys(:keydown)
+ click_button project.default_branch
+
+ page.within '.dropdown-menu' do
+ find('.dropdown-input-field').native.send_keys('fix')
- within('.ui-autocomplete') do
- expect(page).to have_selector('li', text: 'fix')
+ page.within '.dropdown-content' do
+ expect(page).to have_content('fix')
+ end
end
end
end
diff --git a/spec/features/projects/ref_switcher_spec.rb b/spec/features/projects/ref_switcher_spec.rb
index 4eafac1acd8..3b8f0b2d3f8 100644
--- a/spec/features/projects/ref_switcher_spec.rb
+++ b/spec/features/projects/ref_switcher_spec.rb
@@ -20,6 +20,8 @@ feature 'Ref switcher', feature: true, js: true do
input.set 'binary'
wait_for_ajax
+ expect(find('.dropdown-content ul')).to have_selector('li', count: 6)
+
page.within '.dropdown-content ul' do
input.native.send_keys :enter
end
diff --git a/spec/features/search_spec.rb b/spec/features/search_spec.rb
index 0fe5a897565..7da05defa81 100644
--- a/spec/features/search_spec.rb
+++ b/spec/features/search_spec.rb
@@ -186,7 +186,7 @@ describe "Search", feature: true do
sleep 2
expect(page).to have_selector('.merge-requests-holder')
- expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name)
+ expect(find('.filtered-search').value).to eq("assignee:@#{user.username}")
end
it 'takes user to her MR page when MR authored is clicked' do
@@ -194,7 +194,7 @@ describe "Search", feature: true do
sleep 2
expect(page).to have_selector('.merge-requests-holder')
- expect(find('.js-author-search .dropdown-toggle-text')).to have_content(user.name)
+ expect(find('.filtered-search').value).to eq("author:@#{user.username}")
end
end
diff --git a/spec/features/todos/todos_spec.rb b/spec/features/todos/todos_spec.rb
index 1b352be9331..3495091a0d5 100644
--- a/spec/features/todos/todos_spec.rb
+++ b/spec/features/todos/todos_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe 'Dashboard Todos', feature: true do
+ include WaitForAjax
+
let(:user) { create(:user) }
let(:author) { create(:user) }
let(:project) { create(:project, visibility_level: Gitlab::VisibilityLevel::PUBLIC) }
@@ -34,39 +36,64 @@ describe 'Dashboard Todos', feature: true do
end
end
- describe 'deleting the todo' do
+ shared_examples 'deleting the todo' do
before do
- first('.done-todo').click
+ first('.js-done-todo').click
+ end
+
+ it 'is marked as done-reversible in the list' do
+ expect(page).to have_selector('.todos-list .todo.todo-pending.done-reversible')
+ end
+
+ it 'shows Undo button' do
+ expect(page).to have_selector('.js-undo-todo', visible: true)
+ expect(page).to have_selector('.js-done-todo', visible: false)
end
- it 'is removed from the list' do
- expect(page).not_to have_selector('.todos-list .todo')
+ it 'updates todo count' do
+ expect(page).to have_content 'To do 0'
+ expect(page).to have_content 'Done 1'
end
- it 'shows "All done" message' do
- expect(page).to have_selector('.todos-all-done', count: 1)
+ it 'has not "All done" message' do
+ expect(page).not_to have_selector('.todos-all-done')
end
end
- context 'todo is stale on the page' do
+ shared_examples 'deleting and restoring the todo' do
before do
- todos = TodosFinder.new(user, state: :pending).execute
- TodoService.new.mark_todos_as_done(todos, user)
+ first('.js-done-todo').click
+ wait_for_ajax
+ first('.js-undo-todo').click
+ end
+
+ it 'is marked back as pending in the list' do
+ expect(page).not_to have_selector('.todos-list .todo.todo-pending.done-reversible')
+ expect(page).to have_selector('.todos-list .todo.todo-pending')
+ end
+
+ it 'shows Done button' do
+ expect(page).to have_selector('.js-undo-todo', visible: false)
+ expect(page).to have_selector('.js-done-todo', visible: true)
end
- describe 'deleting the todo' do
- before do
- first('.done-todo').click
- end
+ it 'updates todo count' do
+ expect(page).to have_content 'To do 1'
+ expect(page).to have_content 'Done 0'
+ end
+ end
- it 'is removed from the list' do
- expect(page).not_to have_selector('.todos-list .todo')
- end
+ it_behaves_like 'deleting the todo'
+ it_behaves_like 'deleting and restoring the todo'
- it 'shows "All done" message' do
- expect(page).to have_selector('.todos-all-done', count: 1)
- end
+ context 'todo is stale on the page' do
+ before do
+ todos = TodosFinder.new(user, state: :pending).execute
+ TodoService.new.mark_todos_as_done(todos, user)
end
+
+ it_behaves_like 'deleting the todo'
+ it_behaves_like 'deleting and restoring the todo'
end
end
@@ -113,18 +140,6 @@ describe 'Dashboard Todos', feature: true do
expect(page).to have_selector('.gl-pagination .page', count: 2)
end
- describe 'completing last todo from last page', js: true do
- it 'redirects to the previous page' do
- visit dashboard_todos_path(page: 2)
- expect(page).to have_css("#todo_#{Todo.last.id}")
-
- click_link('Done')
-
- expect(current_path).to eq dashboard_todos_path
- expect(page).to have_css("#todo_#{Todo.first.id}")
- end
- end
-
describe 'mark all as done', js: true do
before do
visit dashboard_todos_path
@@ -156,6 +171,29 @@ describe 'Dashboard Todos', feature: true do
end
end
+ context 'User have large number of todos' do
+ before do
+ create_list(:todo, 101, :mentioned, user: user, project: project, target: issue, author: author)
+
+ login_as(user)
+ visit dashboard_todos_path
+ end
+
+ it 'shows 99+ for count >= 100 in notification' do
+ expect(page).to have_selector('.todos-pending-count', text: '99+')
+ end
+
+ it 'shows exact number in To do tab' do
+ expect(page).to have_selector('.todos-pending .badge', text: '101')
+ end
+
+ it 'shows exact number for count < 100' do
+ 3.times { first('.js-done-todo').click }
+
+ expect(page).to have_selector('.todos-pending-count', text: '98')
+ end
+ end
+
context 'User has a Build Failed todo' do
let!(:todo) { create(:todo, :build_failed, user: user, project: project, author: author) }
diff --git a/spec/features/uploads/user_uploads_avatar_to_group_spec.rb b/spec/features/uploads/user_uploads_avatar_to_group_spec.rb
new file mode 100644
index 00000000000..f88a515f7fc
--- /dev/null
+++ b/spec/features/uploads/user_uploads_avatar_to_group_spec.rb
@@ -0,0 +1,26 @@
+require 'rails_helper'
+
+feature 'User uploads avatar to group', feature: true do
+ scenario 'they see the new avatar' do
+ user = create(:user)
+ group = create(:group)
+ group.add_owner(user)
+ login_as(user)
+
+ visit edit_group_path(group)
+ attach_file(
+ 'group_avatar',
+ Rails.root.join('spec', 'fixtures', 'dk.png'),
+ visible: false
+ )
+
+ click_button 'Save group'
+
+ visit group_path(group)
+
+ expect(page).to have_selector(%Q(img[src$="/uploads/group/avatar/#{group.id}/dk.png"]))
+
+ # Cheating here to verify something that isn't user-facing, but is important
+ expect(group.reload.avatar.file).to exist
+ end
+end
diff --git a/spec/features/uploads/user_uploads_avatar_to_profile_spec.rb b/spec/features/uploads/user_uploads_avatar_to_profile_spec.rb
new file mode 100644
index 00000000000..0dfd29045e5
--- /dev/null
+++ b/spec/features/uploads/user_uploads_avatar_to_profile_spec.rb
@@ -0,0 +1,24 @@
+require 'rails_helper'
+
+feature 'User uploads avatar to profile', feature: true do
+ scenario 'they see their new avatar' do
+ user = create(:user)
+ login_as(user)
+
+ visit profile_path
+ attach_file(
+ 'user_avatar',
+ Rails.root.join('spec', 'fixtures', 'dk.png'),
+ visible: false
+ )
+
+ click_button 'Update profile settings'
+
+ visit user_path(user)
+
+ expect(page).to have_selector(%Q(img[src$="/uploads/user/avatar/#{user.id}/dk.png"]))
+
+ # Cheating here to verify something that isn't user-facing, but is important
+ expect(user.reload.avatar.file).to exist
+ end
+end
diff --git a/spec/features/uploads/user_uploads_file_to_note_spec.rb b/spec/features/uploads/user_uploads_file_to_note_spec.rb
new file mode 100644
index 00000000000..0c160dd74b4
--- /dev/null
+++ b/spec/features/uploads/user_uploads_file_to_note_spec.rb
@@ -0,0 +1,22 @@
+require 'rails_helper'
+
+feature 'User uploads file to note', feature: true do
+ include DropzoneHelper
+
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project, creator: user, namespace: user.namespace) }
+
+ scenario 'they see the attached file', js: true do
+ issue = create(:issue, project: project, author: user)
+
+ login_as(user)
+ visit namespace_project_issue_path(project.namespace, project, issue)
+
+ dropzone_file(Rails.root.join('spec', 'fixtures', 'dk.png'))
+ click_button 'Comment'
+ wait_for_ajax
+
+ expect(find('a.no-attachment-icon img[alt="dk"]')['src'])
+ .to match(%r{/#{project.full_path}/uploads/\h{32}/dk\.png$})
+ end
+end
diff --git a/spec/features/user_callout_spec.rb b/spec/features/user_callout_spec.rb
new file mode 100644
index 00000000000..336c4092c98
--- /dev/null
+++ b/spec/features/user_callout_spec.rb
@@ -0,0 +1,37 @@
+require 'spec_helper'
+
+describe 'User Callouts', js: true do
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project, path: 'gitlab', name: 'sample') }
+
+ before do
+ login_as(user)
+ project.team << [user, :master]
+ end
+
+ it 'takes you to the profile preferences when the link is clicked' do
+ visit dashboard_projects_path
+ click_link 'Check it out'
+ expect(current_path).to eq profile_preferences_path
+ end
+
+ describe 'user callout should appear in two routes' do
+ it 'shows up on the user profile' do
+ visit user_path(user)
+ expect(find('.user-callout')).to have_content 'Customize your experience'
+ end
+
+ it 'shows up on the dashboard projects' do
+ visit dashboard_projects_path
+ expect(find('.user-callout')).to have_content 'Customize your experience'
+ end
+ end
+
+ it 'hides the user callout when click on the dismiss icon' do
+ visit user_path(user)
+ within('.user-callout') do
+ find('.close-user-callout').click
+ end
+ expect(page).not_to have_selector('#user-callout')
+ end
+end
diff --git a/spec/features/variables_spec.rb b/spec/features/variables_spec.rb
index 9a4bc027004..a362d6fd3b6 100644
--- a/spec/features/variables_spec.rb
+++ b/spec/features/variables_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe 'Project variables', js: true do
let(:user) { create(:user) }
let(:project) { create(:project) }
- let(:variable) { create(:ci_variable, key: 'test') }
+ let(:variable) { create(:ci_variable, key: 'test_key', value: 'test value') }
before do
login_as(user)
@@ -24,11 +24,23 @@ describe 'Project variables', js: true do
fill_in('variable_value', with: 'key value')
click_button('Add new variable')
+ expect(page).to have_content('Variables were successfully updated.')
page.within('.variables-table') do
expect(page).to have_content('key')
end
end
+ it 'adds empty variable' do
+ fill_in('variable_key', with: 'new_key')
+ fill_in('variable_value', with: '')
+ click_button('Add new variable')
+
+ expect(page).to have_content('Variables were successfully updated.')
+ page.within('.variables-table') do
+ expect(page).to have_content('new_key')
+ end
+ end
+
it 'reveals and hides new variable' do
fill_in('variable_key', with: 'key')
fill_in('variable_value', with: 'key value')
@@ -72,8 +84,20 @@ describe 'Project variables', js: true do
fill_in('variable_value', with: 'key value')
click_button('Save variable')
+ expect(page).to have_content('Variable was successfully updated.')
+ expect(project.variables.first.value).to eq('key value')
+ end
+
+ it 'edits variable with empty value' do
page.within('.variables-table') do
- expect(page).to have_content('key')
+ find('.btn-variable-edit').click
end
+
+ expect(page).to have_content('Update variable')
+ fill_in('variable_value', with: '')
+ click_button('Save variable')
+
+ expect(page).to have_content('Variable was successfully updated.')
+ expect(project.variables.first.value).to eq('')
end
end
diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb
index 12ab1d6dde8..2a008427478 100644
--- a/spec/finders/issues_finder_spec.rb
+++ b/spec/finders/issues_finder_spec.rb
@@ -136,10 +136,10 @@ describe IssuesFinder do
end
end
- context 'filtering by issue iid' do
- let(:params) { { search: issue3.to_reference } }
+ context 'filtering by issues iids' do
+ let(:params) { { iids: issue3.iid } }
- it 'returns issue with iid match' do
+ it 'returns issues with iids match' do
expect(issues).to contain_exactly(issue3)
end
end
diff --git a/spec/finders/merge_requests_finder_spec.rb b/spec/finders/merge_requests_finder_spec.rb
index 3dcd7781e5b..21ef94ac5d1 100644
--- a/spec/finders/merge_requests_finder_spec.rb
+++ b/spec/finders/merge_requests_finder_spec.rb
@@ -38,5 +38,13 @@ describe MergeRequestsFinder do
merge_requests = MergeRequestsFinder.new(user, params).execute
expect(merge_requests.size).to eq(3)
end
+
+ it 'filters by iid' do
+ params = { project_id: project1.id, iids: merge_request1.iid }
+
+ merge_requests = MergeRequestsFinder.new(user, params).execute
+
+ expect(merge_requests).to contain_exactly(merge_request1)
+ end
end
end
diff --git a/spec/finders/notes_finder_spec.rb b/spec/finders/notes_finder_spec.rb
index f8b05d4e9bc..77a04507be1 100644
--- a/spec/finders/notes_finder_spec.rb
+++ b/spec/finders/notes_finder_spec.rb
@@ -111,7 +111,7 @@ describe NotesFinder do
end
it 'raises an exception for an invalid target_type' do
- params.merge!(target_type: 'invalid')
+ params[:target_type] = 'invalid'
expect { described_class.new(project, user, params).execute }.to raise_error('invalid target_type')
end
diff --git a/spec/finders/pipelines_finder_spec.rb b/spec/finders/pipelines_finder_spec.rb
index fdc8215aa47..6bada7b3eb9 100644
--- a/spec/finders/pipelines_finder_spec.rb
+++ b/spec/finders/pipelines_finder_spec.rb
@@ -39,8 +39,8 @@ describe PipelinesFinder do
end
end
- # Scoping to running will speed up the test as it doesn't hit the FS
- let(:params) { { scope: 'running' } }
+ # Scoping to pending will speed up the test as it doesn't hit the FS
+ let(:params) { { scope: 'pending' } }
it 'orders in descending order on ID' do
feature_pipeline = create(:ci_pipeline, project: project, ref: 'feature')
diff --git a/spec/fixtures/api/schemas/user/login.json b/spec/fixtures/api/schemas/user/login.json
index e6c1d9c9d84..6181b3ccc86 100644
--- a/spec/fixtures/api/schemas/user/login.json
+++ b/spec/fixtures/api/schemas/user/login.json
@@ -19,7 +19,6 @@
"organization",
"last_sign_in_at",
"confirmed_at",
- "theme_id",
"color_scheme_id",
"projects_limit",
"current_sign_in_at",
diff --git a/spec/fixtures/api/schemas/user/public.json b/spec/fixtures/api/schemas/user/public.json
index dbd5d32e89c..5587cfec61a 100644
--- a/spec/fixtures/api/schemas/user/public.json
+++ b/spec/fixtures/api/schemas/user/public.json
@@ -19,7 +19,6 @@
"organization",
"last_sign_in_at",
"confirmed_at",
- "theme_id",
"color_scheme_id",
"projects_limit",
"current_sign_in_at",
@@ -32,14 +31,14 @@
"properties": {
"id": { "type": "integer" },
"username": { "type": "string" },
- "email": {
+ "email": {
"type": "string",
"pattern": "^[^@]+@[^@]+$"
},
"name": { "type": "string" },
- "state": {
+ "state": {
"type": "string",
- "enum": ["active", "blocked"]
+ "enum": ["active", "blocked"]
},
"avatar_url": { "type": "string" },
"web_url": { "type": "string" },
@@ -54,18 +53,17 @@
"organization": { "type": ["string", "null"] },
"last_sign_in_at": { "type": "date" },
"confirmed_at": { "type": ["date", "null"] },
- "theme_id": { "type": "integer" },
"color_scheme_id": { "type": "integer" },
"projects_limit": { "type": "integer" },
"current_sign_in_at": { "type": "date" },
- "identities": {
+ "identities": {
"type": "array",
"items": {
"type": "object",
"properties": {
- "provider": {
+ "provider": {
"type": "string",
- "enum": ["github", "bitbucket", "google_oauth2"]
+ "enum": ["github", "bitbucket", "google_oauth2"]
},
"extern_uid": { "type": ["number", "string"] }
}
@@ -74,6 +72,6 @@
"can_create_group": { "type": "boolean" },
"can_create_project": { "type": "boolean" },
"two_factor_enabled": { "type": "boolean" },
- "external": { "type": "boolean" }
+ "external": { "type": "boolean" }
}
}
diff --git a/spec/fixtures/mail_room_disabled.yml b/spec/fixtures/config/mail_room_disabled.yml
index 97f8cff051f..97f8cff051f 100644
--- a/spec/fixtures/mail_room_disabled.yml
+++ b/spec/fixtures/config/mail_room_disabled.yml
diff --git a/spec/fixtures/mail_room_enabled.yml b/spec/fixtures/config/mail_room_enabled.yml
index 9c94649244d..9c94649244d 100644
--- a/spec/fixtures/mail_room_enabled.yml
+++ b/spec/fixtures/config/mail_room_enabled.yml
diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb
index 8b201f348f1..4ffdd530171 100644
--- a/spec/helpers/application_helper_spec.rb
+++ b/spec/helpers/application_helper_spec.rb
@@ -58,7 +58,7 @@ describe ApplicationHelper do
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).
+ expect(helper.project_icon(project.full_path).to_s).
to eq "<img src=\"#{avatar_url}\" alt=\"Banana sample\" />"
end
@@ -68,7 +68,7 @@ describe ApplicationHelper do
allow_any_instance_of(Project).to receive(:avatar_in_git).and_return(true)
avatar_url = "http://#{Gitlab.config.gitlab.host}#{namespace_project_avatar_path(project.namespace, project)}"
- expect(helper.project_icon("#{project.namespace.to_param}/#{project.to_param}").to_s).to match(
+ expect(helper.project_icon(project.full_path).to_s).to match(
image_tag(avatar_url))
end
end
@@ -265,4 +265,9 @@ describe ApplicationHelper do
expect(helper.render_markup('foo.adoc', content)).to eq('NOEL')
end
end
+
+ describe '#active_when' do
+ it { expect(helper.active_when(true)).to eq('active') }
+ it { expect(helper.active_when(false)).to eq(nil) }
+ end
end
diff --git a/spec/helpers/auth_helper_spec.rb b/spec/helpers/auth_helper_spec.rb
index 49ea4fa6d3e..cd3281d6f51 100644
--- a/spec/helpers/auth_helper_spec.rb
+++ b/spec/helpers/auth_helper_spec.rb
@@ -55,7 +55,7 @@ describe AuthHelper do
context 'all the button based providers are disabled via application_setting' do
it 'returns false' do
stub_application_setting(
- disabled_oauth_sign_in_sources: ['github', 'twitter']
+ disabled_oauth_sign_in_sources: %w(github twitter)
)
expect(helper.button_based_providers_enabled?).to be false
diff --git a/spec/helpers/emails_helper_spec.rb b/spec/helpers/emails_helper_spec.rb
index 3223556e1d3..cd112dbb2fb 100644
--- a/spec/helpers/emails_helper_spec.rb
+++ b/spec/helpers/emails_helper_spec.rb
@@ -43,4 +43,36 @@ describe EmailsHelper do
end
end
end
+
+ describe '#header_logo' do
+ context 'there is a brand item with a logo' do
+ it 'returns the brand header logo' do
+ appearance = create :appearance, header_logo: fixture_file_upload(
+ Rails.root.join('spec/fixtures/dk.png')
+ )
+
+ expect(header_logo).to eq(
+ %{<img style="height: 50px" src="/uploads/appearance/header_logo/#{appearance.id}/dk.png" alt="Dk" />}
+ )
+ end
+ end
+
+ context 'there is a brand item without a logo' do
+ it 'returns the default header logo' do
+ create :appearance, header_logo: nil
+
+ expect(header_logo).to eq(
+ %{<img alt="GitLab" src="/images/mailers/gitlab_header_logo.gif" width="55" height="50" />}
+ )
+ end
+ end
+
+ context 'there is no brand item' do
+ it 'returns the default header logo' do
+ expect(header_logo).to eq(
+ %{<img alt="GitLab" src="/images/mailers/gitlab_header_logo.gif" width="55" height="50" />}
+ )
+ end
+ end
+ end
end
diff --git a/spec/helpers/issuables_helper_spec.rb b/spec/helpers/issuables_helper_spec.rb
index df71680e44c..93bb711f29a 100644
--- a/spec/helpers/issuables_helper_spec.rb
+++ b/spec/helpers/issuables_helper_spec.rb
@@ -51,7 +51,7 @@ describe IssuablesHelper do
utf8: '✓',
author_id: '11',
assignee_id: '18',
- label_name: ['bug', 'discussion', 'documentation'],
+ label_name: %w(bug discussion documentation),
milestone_title: 'v4.0',
sort: 'due_date_asc',
namespace_id: 'gitlab-org',
diff --git a/spec/helpers/issues_helper_spec.rb b/spec/helpers/issues_helper_spec.rb
index 13fb9c1f1a7..88d853935c7 100644
--- a/spec/helpers/issues_helper_spec.rb
+++ b/spec/helpers/issues_helper_spec.rb
@@ -55,8 +55,8 @@ describe IssuesHelper do
describe "merge_requests_sentence" do
subject { merge_requests_sentence(merge_requests)}
let(:merge_requests) do
- [ build(:merge_request, iid: 1), build(:merge_request, iid: 2),
- build(:merge_request, iid: 3)]
+ [build(:merge_request, iid: 1), build(:merge_request, iid: 2),
+ build(:merge_request, iid: 3)]
end
it { is_expected.to eq("!1, !2, or !3") }
@@ -113,7 +113,7 @@ describe IssuesHelper do
describe "awards_sort" do
it "sorts a hash so thumbsup and thumbsdown are always on top" do
data = { "thumbsdown" => "some value", "lifter" => "some value", "thumbsup" => "some value" }
- expect(awards_sort(data).keys).to eq(["thumbsup", "thumbsdown", "lifter"])
+ expect(awards_sort(data).keys).to eq(%w(thumbsup thumbsdown lifter))
end
end
diff --git a/spec/helpers/milestones_helper_spec.rb b/spec/helpers/milestones_helper_spec.rb
index 14a95479339..68b20a1e4fc 100644
--- a/spec/helpers/milestones_helper_spec.rb
+++ b/spec/helpers/milestones_helper_spec.rb
@@ -17,7 +17,7 @@ describe MilestonesHelper do
it { expect(result_for(due_date: yesterday)).to eq("expired on #{yesterday_formatted}") }
it { expect(result_for(start_date: tomorrow)).to eq("starts on #{tomorrow_formatted}") }
it { expect(result_for(start_date: yesterday)).to eq("started on #{yesterday_formatted}") }
- it { expect(result_for(start_date: yesterday, due_date: tomorrow)).to eq("#{yesterday_formatted} - #{tomorrow_formatted}") }
+ it { expect(result_for(start_date: yesterday, due_date: tomorrow)).to eq("#{yesterday_formatted}–#{tomorrow_formatted}") }
end
describe '#milestone_counts' do
diff --git a/spec/helpers/page_layout_helper_spec.rb b/spec/helpers/page_layout_helper_spec.rb
index dc07657e101..2cc0b40b2d0 100644
--- a/spec/helpers/page_layout_helper_spec.rb
+++ b/spec/helpers/page_layout_helper_spec.rb
@@ -40,6 +40,18 @@ describe PageLayoutHelper do
end
end
+ describe 'favicon' do
+ it 'defaults to favicon.ico' do
+ allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new('production'))
+ expect(helper.favicon).to eq 'favicon.ico'
+ end
+
+ it 'has blue favicon for development' do
+ allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new('development'))
+ expect(helper.favicon).to eq 'favicon-blue.ico'
+ end
+ end
+
describe 'page_image' do
it 'defaults to the GitLab logo' do
expect(helper.page_image).to end_with 'assets/gitlab_logo.png'
diff --git a/spec/helpers/preferences_helper_spec.rb b/spec/helpers/preferences_helper_spec.rb
index 1f02e06e312..f3e79cc7290 100644
--- a/spec/helpers/preferences_helper_spec.rb
+++ b/spec/helpers/preferences_helper_spec.rb
@@ -26,32 +26,6 @@ describe PreferencesHelper do
end
end
- describe 'user_application_theme' do
- context 'with a user' do
- it "returns user's theme's css_class" do
- stub_user(theme_id: 3)
-
- expect(helper.user_application_theme).to eq 'ui_green'
- end
-
- it 'returns the default when id is invalid' do
- stub_user(theme_id: Gitlab::Themes.count + 5)
-
- allow(Gitlab.config.gitlab).to receive(:default_theme).and_return(2)
-
- expect(helper.user_application_theme).to eq 'ui_charcoal'
- end
- end
-
- context 'without a user' do
- it 'returns the default theme' do
- stub_user
-
- expect(helper.user_application_theme).to eq Gitlab::Themes.default.css_class
- end
- end
- end
-
describe 'user_color_scheme' do
context 'with a user' do
it "returns user's scheme's css_class" do
diff --git a/spec/helpers/submodule_helper_spec.rb b/spec/helpers/submodule_helper_spec.rb
index 4da1569e59f..28b8def331d 100644
--- a/spec/helpers/submodule_helper_spec.rb
+++ b/spec/helpers/submodule_helper_spec.rb
@@ -20,97 +20,97 @@ describe SubmoduleHelper do
it 'detects ssh on standard port' do
allow(Gitlab.config.gitlab_shell).to receive(:ssh_port).and_return(22) # set this just to be sure
allow(Gitlab.config.gitlab_shell).to receive(:ssh_path_prefix).and_return(Settings.send(:build_gitlab_shell_ssh_path_prefix))
- stub_url([ config.user, '@', config.host, ':gitlab-org/gitlab-ce.git' ].join(''))
- expect(submodule_links(submodule_item)).to eq([ namespace_project_path('gitlab-org', 'gitlab-ce'), namespace_project_tree_path('gitlab-org', 'gitlab-ce', 'hash') ])
+ stub_url([config.user, '@', config.host, ':gitlab-org/gitlab-ce.git'].join(''))
+ expect(submodule_links(submodule_item)).to eq([namespace_project_path('gitlab-org', 'gitlab-ce'), namespace_project_tree_path('gitlab-org', 'gitlab-ce', 'hash')])
end
it 'detects ssh on non-standard port' do
allow(Gitlab.config.gitlab_shell).to receive(:ssh_port).and_return(2222)
allow(Gitlab.config.gitlab_shell).to receive(:ssh_path_prefix).and_return(Settings.send(:build_gitlab_shell_ssh_path_prefix))
- stub_url([ 'ssh://', config.user, '@', config.host, ':2222/gitlab-org/gitlab-ce.git' ].join(''))
- expect(submodule_links(submodule_item)).to eq([ namespace_project_path('gitlab-org', 'gitlab-ce'), namespace_project_tree_path('gitlab-org', 'gitlab-ce', 'hash') ])
+ stub_url(['ssh://', config.user, '@', config.host, ':2222/gitlab-org/gitlab-ce.git'].join(''))
+ expect(submodule_links(submodule_item)).to eq([namespace_project_path('gitlab-org', 'gitlab-ce'), namespace_project_tree_path('gitlab-org', 'gitlab-ce', 'hash')])
end
it 'detects http on standard port' do
allow(Gitlab.config.gitlab).to receive(:port).and_return(80)
allow(Gitlab.config.gitlab).to receive(:url).and_return(Settings.send(:build_gitlab_url))
- stub_url([ 'http://', config.host, '/gitlab-org/gitlab-ce.git' ].join(''))
- expect(submodule_links(submodule_item)).to eq([ namespace_project_path('gitlab-org', 'gitlab-ce'), namespace_project_tree_path('gitlab-org', 'gitlab-ce', 'hash') ])
+ stub_url(['http://', config.host, '/gitlab-org/gitlab-ce.git'].join(''))
+ expect(submodule_links(submodule_item)).to eq([namespace_project_path('gitlab-org', 'gitlab-ce'), namespace_project_tree_path('gitlab-org', 'gitlab-ce', 'hash')])
end
it 'detects http on non-standard port' do
allow(Gitlab.config.gitlab).to receive(:port).and_return(3000)
allow(Gitlab.config.gitlab).to receive(:url).and_return(Settings.send(:build_gitlab_url))
- stub_url([ 'http://', config.host, ':3000/gitlab-org/gitlab-ce.git' ].join(''))
- expect(submodule_links(submodule_item)).to eq([ namespace_project_path('gitlab-org', 'gitlab-ce'), namespace_project_tree_path('gitlab-org', 'gitlab-ce', 'hash') ])
+ stub_url(['http://', config.host, ':3000/gitlab-org/gitlab-ce.git'].join(''))
+ expect(submodule_links(submodule_item)).to eq([namespace_project_path('gitlab-org', 'gitlab-ce'), namespace_project_tree_path('gitlab-org', 'gitlab-ce', 'hash')])
end
it 'works with relative_url_root' do
allow(Gitlab.config.gitlab).to receive(:port).and_return(80) # set this just to be sure
allow(Gitlab.config.gitlab).to receive(:relative_url_root).and_return('/gitlab/root')
allow(Gitlab.config.gitlab).to receive(:url).and_return(Settings.send(:build_gitlab_url))
- stub_url([ 'http://', config.host, '/gitlab/root/gitlab-org/gitlab-ce.git' ].join(''))
- expect(submodule_links(submodule_item)).to eq([ namespace_project_path('gitlab-org', 'gitlab-ce'), namespace_project_tree_path('gitlab-org', 'gitlab-ce', 'hash') ])
+ stub_url(['http://', config.host, '/gitlab/root/gitlab-org/gitlab-ce.git'].join(''))
+ expect(submodule_links(submodule_item)).to eq([namespace_project_path('gitlab-org', 'gitlab-ce'), namespace_project_tree_path('gitlab-org', 'gitlab-ce', 'hash')])
end
end
context 'submodule on github.com' do
it 'detects ssh' do
stub_url('git@github.com:gitlab-org/gitlab-ce.git')
- expect(submodule_links(submodule_item)).to eq([ 'https://github.com/gitlab-org/gitlab-ce', 'https://github.com/gitlab-org/gitlab-ce/tree/hash' ])
+ expect(submodule_links(submodule_item)).to eq(['https://github.com/gitlab-org/gitlab-ce', 'https://github.com/gitlab-org/gitlab-ce/tree/hash'])
end
it 'detects http' do
stub_url('http://github.com/gitlab-org/gitlab-ce.git')
- expect(submodule_links(submodule_item)).to eq([ 'https://github.com/gitlab-org/gitlab-ce', 'https://github.com/gitlab-org/gitlab-ce/tree/hash' ])
+ expect(submodule_links(submodule_item)).to eq(['https://github.com/gitlab-org/gitlab-ce', 'https://github.com/gitlab-org/gitlab-ce/tree/hash'])
end
it 'detects https' do
stub_url('https://github.com/gitlab-org/gitlab-ce.git')
- expect(submodule_links(submodule_item)).to eq([ 'https://github.com/gitlab-org/gitlab-ce', 'https://github.com/gitlab-org/gitlab-ce/tree/hash' ])
+ expect(submodule_links(submodule_item)).to eq(['https://github.com/gitlab-org/gitlab-ce', 'https://github.com/gitlab-org/gitlab-ce/tree/hash'])
end
it 'returns original with non-standard url' do
stub_url('http://github.com/gitlab-org/gitlab-ce')
- expect(submodule_links(submodule_item)).to eq([ repo.submodule_url_for, nil ])
+ expect(submodule_links(submodule_item)).to eq([repo.submodule_url_for, nil])
stub_url('http://github.com/another/gitlab-org/gitlab-ce.git')
- expect(submodule_links(submodule_item)).to eq([ repo.submodule_url_for, nil ])
+ expect(submodule_links(submodule_item)).to eq([repo.submodule_url_for, nil])
end
end
context 'submodule on gitlab.com' do
it 'detects ssh' do
stub_url('git@gitlab.com:gitlab-org/gitlab-ce.git')
- expect(submodule_links(submodule_item)).to eq([ 'https://gitlab.com/gitlab-org/gitlab-ce', 'https://gitlab.com/gitlab-org/gitlab-ce/tree/hash' ])
+ expect(submodule_links(submodule_item)).to eq(['https://gitlab.com/gitlab-org/gitlab-ce', 'https://gitlab.com/gitlab-org/gitlab-ce/tree/hash'])
end
it 'detects http' do
stub_url('http://gitlab.com/gitlab-org/gitlab-ce.git')
- expect(submodule_links(submodule_item)).to eq([ 'https://gitlab.com/gitlab-org/gitlab-ce', 'https://gitlab.com/gitlab-org/gitlab-ce/tree/hash' ])
+ expect(submodule_links(submodule_item)).to eq(['https://gitlab.com/gitlab-org/gitlab-ce', 'https://gitlab.com/gitlab-org/gitlab-ce/tree/hash'])
end
it 'detects https' do
stub_url('https://gitlab.com/gitlab-org/gitlab-ce.git')
- expect(submodule_links(submodule_item)).to eq([ 'https://gitlab.com/gitlab-org/gitlab-ce', 'https://gitlab.com/gitlab-org/gitlab-ce/tree/hash' ])
+ expect(submodule_links(submodule_item)).to eq(['https://gitlab.com/gitlab-org/gitlab-ce', 'https://gitlab.com/gitlab-org/gitlab-ce/tree/hash'])
end
it 'returns original with non-standard url' do
stub_url('http://gitlab.com/gitlab-org/gitlab-ce')
- expect(submodule_links(submodule_item)).to eq([ repo.submodule_url_for, nil ])
+ expect(submodule_links(submodule_item)).to eq([repo.submodule_url_for, nil])
stub_url('http://gitlab.com/another/gitlab-org/gitlab-ce.git')
- expect(submodule_links(submodule_item)).to eq([ repo.submodule_url_for, nil ])
+ expect(submodule_links(submodule_item)).to eq([repo.submodule_url_for, nil])
end
end
context 'submodule on unsupported' do
it 'returns original' do
stub_url('http://mygitserver.com/gitlab-org/gitlab-ce')
- expect(submodule_links(submodule_item)).to eq([ repo.submodule_url_for, nil ])
+ expect(submodule_links(submodule_item)).to eq([repo.submodule_url_for, nil])
stub_url('http://mygitserver.com/gitlab-org/gitlab-ce.git')
- expect(submodule_links(submodule_item)).to eq([ repo.submodule_url_for, nil ])
+ expect(submodule_links(submodule_item)).to eq([repo.submodule_url_for, nil])
end
end
diff --git a/spec/helpers/version_check_helper_spec.rb b/spec/helpers/version_check_helper_spec.rb
new file mode 100644
index 00000000000..889fe441171
--- /dev/null
+++ b/spec/helpers/version_check_helper_spec.rb
@@ -0,0 +1,34 @@
+require 'spec_helper'
+
+describe VersionCheckHelper do
+ describe '#version_status_badge' do
+ it 'should return nil if not dev environment and not enabled' do
+ allow(Rails.env).to receive(:production?) { false }
+ allow(current_application_settings).to receive(:version_check_enabled) { false }
+
+ expect(helper.version_status_badge).to be(nil)
+ end
+
+ context 'when production and enabled' do
+ before do
+ allow(Rails.env).to receive(:production?) { true }
+ allow(current_application_settings).to receive(:version_check_enabled) { true }
+ allow_any_instance_of(VersionCheck).to receive(:url) { 'https://version.host.com/check.svg?gitlab_info=xxx' }
+
+ @image_tag = helper.version_status_badge
+ end
+
+ it 'should return an image tag' do
+ expect(@image_tag).to match(/^<img/)
+ end
+
+ it 'should have a js prefixed css class' do
+ expect(@image_tag).to match(/class="js-version-status-badge"/)
+ end
+
+ it 'should have a VersionCheck url as the src' do
+ expect(@image_tag).to match(/src="https:\/\/version\.host\.com\/check\.svg\?gitlab_info=xxx"/)
+ end
+ end
+ end
+end
diff --git a/spec/initializers/trusted_proxies_spec.rb b/spec/initializers/trusted_proxies_spec.rb
index 290e47763eb..ff8b8daa347 100644
--- a/spec/initializers/trusted_proxies_spec.rb
+++ b/spec/initializers/trusted_proxies_spec.rb
@@ -27,7 +27,7 @@ describe 'trusted_proxies', lib: true do
context 'with private IP ranges added' do
before do
- set_trusted_proxies([ "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16" ])
+ set_trusted_proxies(["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"])
end
it 'filters out private and local IPs' do
@@ -39,7 +39,7 @@ describe 'trusted_proxies', lib: true do
context 'with proxy IP added' do
before do
- set_trusted_proxies([ "60.98.25.47" ])
+ set_trusted_proxies(["60.98.25.47"])
end
it 'filters out proxy IP' do
diff --git a/spec/javascripts/.eslintrc b/spec/javascripts/.eslintrc
index fbd9bb9f0ff..3d922021978 100644
--- a/spec/javascripts/.eslintrc
+++ b/spec/javascripts/.eslintrc
@@ -18,7 +18,8 @@
"sandbox": false,
"setFixtures": false,
"setStyleFixtures": false,
- "spyOnEvent": false
+ "spyOnEvent": false,
+ "ClassSpecHelper": false
},
"plugins": ["jasmine"],
"rules": {
diff --git a/spec/javascripts/ajax_loading_spinner_spec.js b/spec/javascripts/ajax_loading_spinner_spec.js
new file mode 100644
index 00000000000..a68bccb16f4
--- /dev/null
+++ b/spec/javascripts/ajax_loading_spinner_spec.js
@@ -0,0 +1,58 @@
+require('~/extensions/array');
+require('jquery');
+require('jquery-ujs');
+require('~/ajax_loading_spinner');
+
+describe('Ajax Loading Spinner', () => {
+ const fixtureTemplate = 'static/ajax_loading_spinner.html.raw';
+ preloadFixtures(fixtureTemplate);
+
+ beforeEach(() => {
+ loadFixtures(fixtureTemplate);
+ gl.AjaxLoadingSpinner.init();
+ });
+
+ it('change current icon with spinner icon and disable link while waiting ajax response', (done) => {
+ spyOn(jQuery, 'ajax').and.callFake((req) => {
+ const xhr = new XMLHttpRequest();
+ const ajaxLoadingSpinner = document.querySelector('.js-ajax-loading-spinner');
+ const icon = ajaxLoadingSpinner.querySelector('i');
+
+ req.beforeSend(xhr, { dataType: 'text/html' });
+
+ expect(icon).not.toHaveClass('fa-trash-o');
+ expect(icon).toHaveClass('fa-spinner');
+ expect(icon).toHaveClass('fa-spin');
+ expect(icon.dataset.icon).toEqual('fa-trash-o');
+ expect(ajaxLoadingSpinner.getAttribute('disabled')).toEqual('');
+
+ req.complete({});
+
+ done();
+ const deferred = $.Deferred();
+ return deferred.promise();
+ });
+ document.querySelector('.js-ajax-loading-spinner').click();
+ });
+
+ it('use original icon again and enabled the link after complete the ajax request', (done) => {
+ spyOn(jQuery, 'ajax').and.callFake((req) => {
+ const xhr = new XMLHttpRequest();
+ const ajaxLoadingSpinner = document.querySelector('.js-ajax-loading-spinner');
+
+ req.beforeSend(xhr, { dataType: 'text/html' });
+ req.complete({});
+
+ const icon = ajaxLoadingSpinner.querySelector('i');
+ expect(icon).toHaveClass('fa-trash-o');
+ expect(icon).not.toHaveClass('fa-spinner');
+ expect(icon).not.toHaveClass('fa-spin');
+ expect(ajaxLoadingSpinner.getAttribute('disabled')).toEqual(null);
+
+ done();
+ const deferred = $.Deferred();
+ return deferred.promise();
+ });
+ document.querySelector('.js-ajax-loading-spinner').click();
+ });
+});
diff --git a/spec/javascripts/awards_handler_spec.js b/spec/javascripts/awards_handler_spec.js
index 001cd8d6325..e5826f9c29f 100644
--- a/spec/javascripts/awards_handler_spec.js
+++ b/spec/javascripts/awards_handler_spec.js
@@ -229,4 +229,4 @@ require('./fixtures/emoji_menu');
});
});
});
-}).call(this);
+}).call(window);
diff --git a/spec/javascripts/behaviors/autosize_spec.js b/spec/javascripts/behaviors/autosize_spec.js
index 4a3da9e318b..3deaf258cae 100644
--- a/spec/javascripts/behaviors/autosize_spec.js
+++ b/spec/javascripts/behaviors/autosize_spec.js
@@ -18,4 +18,4 @@ require('~/behaviors/autosize');
return $(document).trigger('load');
};
});
-}).call(this);
+}).call(window);
diff --git a/spec/javascripts/behaviors/quick_submit_spec.js b/spec/javascripts/behaviors/quick_submit_spec.js
index 0e4c2c560cc..4820ce41ade 100644
--- a/spec/javascripts/behaviors/quick_submit_spec.js
+++ b/spec/javascripts/behaviors/quick_submit_spec.js
@@ -103,4 +103,4 @@ require('~/behaviors/quick_submit');
return $.Event('keydown', $.extend({}, defaults, options));
};
});
-}).call(this);
+}).call(window);
diff --git a/spec/javascripts/behaviors/requires_input_spec.js b/spec/javascripts/behaviors/requires_input_spec.js
index 631fca06514..3a84013a2ed 100644
--- a/spec/javascripts/behaviors/requires_input_spec.js
+++ b/spec/javascripts/behaviors/requires_input_spec.js
@@ -36,4 +36,4 @@ require('~/behaviors/requires_input');
return expect($('.submit')).not.toBeDisabled();
});
});
-}).call(this);
+}).call(window);
diff --git a/spec/javascripts/boards/board_card_spec.js b/spec/javascripts/boards/board_card_spec.js
new file mode 100644
index 00000000000..192916fbc6a
--- /dev/null
+++ b/spec/javascripts/boards/board_card_spec.js
@@ -0,0 +1,168 @@
+/* global Vue */
+/* global List */
+/* global ListLabel */
+/* global listObj */
+/* global boardsMockInterceptor */
+/* global BoardService */
+
+require('~/boards/models/list');
+require('~/boards/models/label');
+require('~/boards/stores/boards_store');
+const boardCard = require('~/boards/components/board_card');
+require('./mock_data');
+
+describe('Issue card', () => {
+ let vm;
+
+ beforeEach((done) => {
+ Vue.http.interceptors.push(boardsMockInterceptor);
+
+ gl.boardService = new BoardService('/test/issue-boards/board', '', '1');
+ gl.issueBoards.BoardsStore.create();
+ gl.issueBoards.BoardsStore.detail.issue = {};
+
+ const BoardCardComp = Vue.extend(boardCard);
+ const list = new List(listObj);
+ const label1 = new ListLabel({
+ id: 3,
+ title: 'testing 123',
+ color: 'blue',
+ text_color: 'white',
+ description: 'test',
+ });
+
+ setTimeout(() => {
+ list.issues[0].labels.push(label1);
+
+ vm = new BoardCardComp({
+ propsData: {
+ list,
+ issue: list.issues[0],
+ issueLinkBase: '/',
+ disabled: false,
+ index: 0,
+ rootPath: '/',
+ },
+ }).$mount();
+ done();
+ }, 0);
+ });
+
+ afterEach(() => {
+ Vue.http.interceptors = _.without(Vue.http.interceptors, boardsMockInterceptor);
+ });
+
+ it('returns false when detailIssue is empty', () => {
+ expect(vm.issueDetailVisible).toBe(false);
+ });
+
+ it('returns true when detailIssue is equal to card issue', () => {
+ gl.issueBoards.BoardsStore.detail.issue = vm.issue;
+
+ expect(vm.issueDetailVisible).toBe(true);
+ });
+
+ it('adds user-can-drag class if not disabled', () => {
+ expect(vm.$el.classList.contains('user-can-drag')).toBe(true);
+ });
+
+ it('does not add user-can-drag class disabled', (done) => {
+ vm.disabled = true;
+
+ setTimeout(() => {
+ expect(vm.$el.classList.contains('user-can-drag')).toBe(false);
+ done();
+ }, 0);
+ });
+
+ it('does not add disabled class', () => {
+ expect(vm.$el.classList.contains('is-disabled')).toBe(false);
+ });
+
+ it('adds disabled class is disabled is true', (done) => {
+ vm.disabled = true;
+
+ setTimeout(() => {
+ expect(vm.$el.classList.contains('is-disabled')).toBe(true);
+ done();
+ }, 0);
+ });
+
+ describe('mouse events', () => {
+ const triggerEvent = (eventName, el = vm.$el) => {
+ const event = document.createEvent('MouseEvents');
+ event.initMouseEvent(eventName, true, true, window, 1, 0, 0, 0, 0, false, false,
+ false, false, 0, null);
+
+ el.dispatchEvent(event);
+ };
+
+ it('sets showDetail to true on mousedown', () => {
+ triggerEvent('mousedown');
+
+ expect(vm.showDetail).toBe(true);
+ });
+
+ it('sets showDetail to false on mousemove', () => {
+ triggerEvent('mousedown');
+
+ expect(vm.showDetail).toBe(true);
+
+ triggerEvent('mousemove');
+
+ expect(vm.showDetail).toBe(false);
+ });
+
+ it('does not set detail issue if showDetail is false', () => {
+ expect(gl.issueBoards.BoardsStore.detail.issue).toEqual({});
+ });
+
+ it('does not set detail issue if link is clicked', () => {
+ triggerEvent('mouseup', vm.$el.querySelector('a'));
+
+ expect(gl.issueBoards.BoardsStore.detail.issue).toEqual({});
+ });
+
+ it('does not set detail issue if button is clicked', () => {
+ triggerEvent('mouseup', vm.$el.querySelector('button'));
+
+ expect(gl.issueBoards.BoardsStore.detail.issue).toEqual({});
+ });
+
+ it('does not set detail issue if showDetail is false after mouseup', () => {
+ triggerEvent('mouseup');
+
+ expect(gl.issueBoards.BoardsStore.detail.issue).toEqual({});
+ });
+
+ it('sets detail issue to card issue on mouse up', () => {
+ triggerEvent('mousedown');
+ triggerEvent('mouseup');
+
+ expect(gl.issueBoards.BoardsStore.detail.issue).toEqual(vm.issue);
+ expect(gl.issueBoards.BoardsStore.detail.list).toEqual(vm.list);
+ });
+
+ it('adds active class if detail issue is set', (done) => {
+ triggerEvent('mousedown');
+ triggerEvent('mouseup');
+
+ setTimeout(() => {
+ expect(vm.$el.classList.contains('is-active')).toBe(true);
+ done();
+ }, 0);
+ });
+
+ it('resets detail issue to empty if already set', () => {
+ triggerEvent('mousedown');
+ triggerEvent('mouseup');
+
+ expect(gl.issueBoards.BoardsStore.detail.issue).toEqual(vm.issue);
+
+ triggerEvent('mousedown');
+ triggerEvent('mouseup');
+
+ expect(gl.issueBoards.BoardsStore.detail.issue).toEqual({});
+ });
+ });
+});
diff --git a/spec/javascripts/boards/list_spec.js.es6 b/spec/javascripts/boards/list_spec.js.es6
index 4397a32fedc..c8a18af7198 100644
--- a/spec/javascripts/boards/list_spec.js.es6
+++ b/spec/javascripts/boards/list_spec.js.es6
@@ -3,7 +3,9 @@
/* global boardsMockInterceptor */
/* global BoardService */
/* global List */
+/* global ListIssue */
/* global listObj */
+/* global listObjDuplicate */
require('~/lib/utils/url_utility');
require('~/boards/models/issue');
@@ -84,4 +86,23 @@ describe('List model', () => {
done();
}, 0);
});
+
+ it('sends service request to update issue label', () => {
+ const listDup = new List(listObjDuplicate);
+ const issue = new ListIssue({
+ title: 'Testing',
+ iid: 1,
+ confidential: false,
+ labels: [list.label, listDup.label]
+ });
+
+ list.issues.push(issue);
+ listDup.issues.push(issue);
+
+ spyOn(gl.boardService, 'moveIssue').and.callThrough();
+
+ listDup.updateIssueLabel(list, issue);
+
+ expect(gl.boardService.moveIssue).toHaveBeenCalledWith(issue.id, list.id, listDup.id);
+ });
});
diff --git a/spec/javascripts/commit/pipelines/pipelines_store_spec.js.es6 b/spec/javascripts/commit/pipelines/pipelines_store_spec.js.es6
index 789f5dc9f49..94973419979 100644
--- a/spec/javascripts/commit/pipelines/pipelines_store_spec.js.es6
+++ b/spec/javascripts/commit/pipelines/pipelines_store_spec.js.es6
@@ -1,10 +1,10 @@
-require('~/commit/pipelines/pipelines_store');
+const PipelinesStore = require('~/commit/pipelines/pipelines_store');
describe('Store', () => {
let store;
beforeEach(() => {
- store = new gl.commits.pipelines.PipelinesStore();
+ store = new PipelinesStore();
});
// unregister intervals and event handlers
diff --git a/spec/javascripts/dashboard_spec.js.es6 b/spec/javascripts/dashboard_spec.js.es6
deleted file mode 100644
index c0bdb89ed63..00000000000
--- a/spec/javascripts/dashboard_spec.js.es6
+++ /dev/null
@@ -1,37 +0,0 @@
-/* eslint-disable no-new */
-
-require('~/sidebar');
-require('~/lib/utils/text_utility');
-
-((global) => {
- describe('Dashboard', () => {
- const fixtureTemplate = 'static/dashboard.html.raw';
-
- function todosCountText() {
- return $('.js-todos-count').text();
- }
-
- function triggerToggle(newCount) {
- $(document).trigger('todo:toggle', newCount);
- }
-
- preloadFixtures(fixtureTemplate);
- beforeEach(() => {
- loadFixtures(fixtureTemplate);
- new global.Sidebar();
- });
-
- it('should update todos-count after receiving the todo:toggle event', () => {
- triggerToggle(5);
- expect(todosCountText()).toEqual('5');
- });
-
- it('should display todos-count with delimiter', () => {
- triggerToggle(1000);
- expect(todosCountText()).toEqual('1,000');
-
- triggerToggle(1000000);
- expect(todosCountText()).toEqual('1,000,000');
- });
- });
-})(window.gl);
diff --git a/spec/javascripts/extensions/array_spec.js.es6 b/spec/javascripts/extensions/array_spec.js.es6
index ba5eb81defc..60f6b9b78e3 100644
--- a/spec/javascripts/extensions/array_spec.js.es6
+++ b/spec/javascripts/extensions/array_spec.js.es6
@@ -42,4 +42,4 @@ require('~/extensions/array');
});
});
});
-}).call(this);
+}).call(window);
diff --git a/spec/javascripts/extensions/jquery_spec.js b/spec/javascripts/extensions/jquery_spec.js
index c0bb0419814..096d3272eac 100644
--- a/spec/javascripts/extensions/jquery_spec.js
+++ b/spec/javascripts/extensions/jquery_spec.js
@@ -39,4 +39,4 @@ require('~/extensions/jquery');
});
});
});
-}).call(this);
+}).call(window);
diff --git a/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js.es6 b/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js.es6
index 84c0e9cbfe2..a91801cfc89 100644
--- a/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js.es6
+++ b/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js.es6
@@ -99,6 +99,29 @@ require('~/filtered_search/filtered_search_tokenizer');
expect(results.tokens[2].value).toBe('Doing');
expect(results.tokens[2].symbol).toBe('~');
});
+
+ it('returns search value for invalid tokens', () => {
+ const results = gl.FilteredSearchTokenizer.processTokens('fake:token');
+ expect(results.lastToken).toBe('fake:token');
+ expect(results.searchToken).toBe('fake:token');
+ expect(results.tokens.length).toEqual(0);
+ });
+
+ it('returns search value and token for mix of valid and invalid tokens', () => {
+ const results = gl.FilteredSearchTokenizer.processTokens('label:real fake:token');
+ expect(results.tokens.length).toEqual(1);
+ expect(results.tokens[0].key).toBe('label');
+ expect(results.tokens[0].value).toBe('real');
+ expect(results.tokens[0].symbol).toBe('');
+ expect(results.lastToken).toBe('fake:token');
+ expect(results.searchToken).toBe('fake:token');
+ });
+
+ it('returns search value for invalid symbols', () => {
+ const results = gl.FilteredSearchTokenizer.processTokens('std::includes');
+ expect(results.lastToken).toBe('std::includes');
+ expect(results.searchToken).toBe('std::includes');
+ });
});
});
})();
diff --git a/spec/javascripts/fixtures/ajax_loading_spinner.html.haml b/spec/javascripts/fixtures/ajax_loading_spinner.html.haml
new file mode 100644
index 00000000000..09d8c9df3b2
--- /dev/null
+++ b/spec/javascripts/fixtures/ajax_loading_spinner.html.haml
@@ -0,0 +1,2 @@
+%a.js-ajax-loading-spinner{href: "http://goesnowhere.nothing/whereami", data: {remote: true}}
+ %i.fa.fa-trash-o
diff --git a/spec/javascripts/fixtures/branches.rb b/spec/javascripts/fixtures/branches.rb
index 0e7c2351b66..a059818145b 100644
--- a/spec/javascripts/fixtures/branches.rb
+++ b/spec/javascripts/fixtures/branches.rb
@@ -20,7 +20,7 @@ describe Projects::BranchesController, '(JavaScript fixtures)', type: :controlle
it 'branches/new_branch.html.raw' do |example|
get :new,
namespace_id: project.namespace.to_param,
- project_id: project.to_param
+ project_id: project
expect(response).to be_success
store_frontend_fixture(response, example.description)
diff --git a/spec/javascripts/fixtures/builds.rb b/spec/javascripts/fixtures/builds.rb
index 978e25a1c32..320de791b08 100644
--- a/spec/javascripts/fixtures/builds.rb
+++ b/spec/javascripts/fixtures/builds.rb
@@ -24,7 +24,7 @@ describe Projects::BuildsController, '(JavaScript fixtures)', type: :controller
it 'builds/build-with-artifacts.html.raw' do |example|
get :show,
namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ project_id: project,
id: build_with_artifacts.to_param
expect(response).to be_success
diff --git a/spec/javascripts/fixtures/dashboard.html.haml b/spec/javascripts/fixtures/dashboard.html.haml
deleted file mode 100644
index 32446acfd60..00000000000
--- a/spec/javascripts/fixtures/dashboard.html.haml
+++ /dev/null
@@ -1,45 +0,0 @@
-%ul.nav.nav-sidebar
- %li.home.active
- %a.dashboard-shortcuts-projects
- %span
- Projects
- %li
- %a
- %span
- Todos
- %span.count.js-todos-count
- 1
- %li
- %a.dashboard-shortcuts-activity
- %span
- Activity
- %li
- %a
- %span
- Groups
- %li
- %a
- %span
- Milestones
- %li
- %a.dashboard-shortcuts-issues
- %span
- Issues
- %span
- 1
- %li
- %a.dashboard-shortcuts-merge_requests
- %span
- Merge Requests
- %li
- %a
- %span
- Snippets
- %li
- %a
- %span
- Help
- %li
- %a
- %span
- Profile Settings
diff --git a/spec/javascripts/fixtures/emoji_menu.js b/spec/javascripts/fixtures/emoji_menu.js
index 2ef242901e8..a50812d9517 100644
--- a/spec/javascripts/fixtures/emoji_menu.js
+++ b/spec/javascripts/fixtures/emoji_menu.js
@@ -1,4 +1,4 @@
/* 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);
+}).call(window);
diff --git a/spec/javascripts/fixtures/issues.rb b/spec/javascripts/fixtures/issues.rb
index 06f708f9e15..88e3f860809 100644
--- a/spec/javascripts/fixtures/issues.rb
+++ b/spec/javascripts/fixtures/issues.rb
@@ -41,7 +41,7 @@ describe Projects::IssuesController, '(JavaScript fixtures)', type: :controller
def render_issue(fixture_file_name, issue)
get :show,
namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ project_id: project,
id: issue.to_param
expect(response).to be_success
diff --git a/spec/javascripts/fixtures/merge_requests.rb b/spec/javascripts/fixtures/merge_requests.rb
index 62984097099..ee893b76c84 100644
--- a/spec/javascripts/fixtures/merge_requests.rb
+++ b/spec/javascripts/fixtures/merge_requests.rb
@@ -27,7 +27,7 @@ describe Projects::MergeRequestsController, '(JavaScript fixtures)', type: :cont
def render_merge_request(fixture_file_name, merge_request)
get :show,
namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ project_id: project,
id: merge_request.to_param
expect(response).to be_success
diff --git a/spec/javascripts/fixtures/project_title.html.haml b/spec/javascripts/fixtures/project_title.html.haml
deleted file mode 100644
index 9d1f7877116..00000000000
--- a/spec/javascripts/fixtures/project_title.html.haml
+++ /dev/null
@@ -1,20 +0,0 @@
-.header-content
- %h1.title
- %a
- 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", "data-order-by" => "last_activity_at" }
- .js-dropdown-menu-projects
- .dropdown-menu.dropdown-select.dropdown-menu-projects
- .dropdown-title
- %span Go to a project
- %button.dropdown-title-button.dropdown-menu-close{"aria-label" => "Close", type: "button"}
- %i.fa.fa-times.dropdown-menu-close-icon
- .dropdown-input
- %input.dropdown-input-field{id: "", placeholder: "Search your projects", type: "search", value: ""}
- %i.fa.fa-search.dropdown-input-search
- %i.fa.fa-times.dropdown-input-clear.js-dropdown-input-clear{role: "button"}
- .dropdown-content
- .dropdown-loading
- %i.fa.fa-spinner.fa-spin
diff --git a/spec/javascripts/fixtures/projects.rb b/spec/javascripts/fixtures/projects.rb
index 56513219e1e..6c33b240e5c 100644
--- a/spec/javascripts/fixtures/projects.rb
+++ b/spec/javascripts/fixtures/projects.rb
@@ -20,7 +20,7 @@ describe ProjectsController, '(JavaScript fixtures)', type: :controller do
it 'projects/dashboard.html.raw' do |example|
get :show,
namespace_id: project.namespace.to_param,
- id: project.to_param
+ id: project
expect(response).to be_success
store_frontend_fixture(response, example.description)
diff --git a/spec/javascripts/fixtures/todos.rb b/spec/javascripts/fixtures/todos.rb
index 2c08b06ea9e..a81ef8c5492 100644
--- a/spec/javascripts/fixtures/todos.rb
+++ b/spec/javascripts/fixtures/todos.rb
@@ -39,8 +39,8 @@ describe 'Todos (JavaScript fixtures)' do
it 'todos/todos.json' do |example|
post :create,
- namespace_id: namespace.path,
- project_id: project.path,
+ namespace_id: namespace,
+ project_id: project,
issuable_type: 'issue',
issuable_id: issue_2.id,
format: 'json'
diff --git a/spec/javascripts/fixtures/user_callout.html.haml b/spec/javascripts/fixtures/user_callout.html.haml
new file mode 100644
index 00000000000..275359bde0a
--- /dev/null
+++ b/spec/javascripts/fixtures/user_callout.html.haml
@@ -0,0 +1,2 @@
+.user-callout{ 'callout-svg' => custom_icon('icon_customization') }
+
diff --git a/spec/javascripts/gfm_auto_complete_spec.js.es6 b/spec/javascripts/gfm_auto_complete_spec.js.es6
index c61c32f8a13..5dfa4008fbd 100644
--- a/spec/javascripts/gfm_auto_complete_spec.js.es6
+++ b/spec/javascripts/gfm_auto_complete_spec.js.es6
@@ -1,3 +1,5 @@
+/* eslint no-param-reassign: "off" */
+
require('~/gfm_auto_complete');
require('vendor/jquery.caret');
require('vendor/jquery.atwho');
@@ -63,6 +65,61 @@ describe('GfmAutoComplete', function () {
});
});
+ describe('DefaultOptions.matcher', function () {
+ const defaultMatcher = (context, flag, subtext) => (
+ GfmAutoComplete.DefaultOptions.matcher.call(context, flag, subtext)
+ );
+
+ const flagsUseDefaultMatcher = ['@', '#', '!', '~', '%'];
+ const otherFlags = ['/', ':'];
+ const flags = flagsUseDefaultMatcher.concat(otherFlags);
+
+ const flagsHash = flags.reduce((hash, el) => { hash[el] = null; return hash; }, {});
+ const atwhoInstance = { setting: {}, app: { controllers: flagsHash } };
+
+ const minLen = 1;
+ const maxLen = 20;
+ const argumentSize = [minLen, maxLen / 2, maxLen];
+
+ const allowedSymbols = ['', 'a', 'n', 'z', 'A', 'Z', 'N', '0', '5', '9', 'А', 'а', 'Я', 'я', '.', '\'', '+', '-', '_'];
+ const jointAllowedSymbols = allowedSymbols.join('');
+
+ describe('should match regular symbols', () => {
+ flagsUseDefaultMatcher.forEach((flag) => {
+ allowedSymbols.forEach((symbol) => {
+ argumentSize.forEach((size) => {
+ const query = new Array(size + 1).join(symbol);
+ const subtext = flag + query;
+
+ it(`matches argument "${flag}" with query "${subtext}"`, () => {
+ expect(defaultMatcher(atwhoInstance, flag, subtext)).toBe(query);
+ });
+ });
+ });
+
+ it(`matches combination of allowed symbols for flag "${flag}"`, () => {
+ const subtext = flag + jointAllowedSymbols;
+
+ expect(defaultMatcher(atwhoInstance, flag, subtext)).toBe(jointAllowedSymbols);
+ });
+ });
+ });
+
+ describe('should not match special sequences', () => {
+ const ShouldNotBeFollowedBy = flags.concat(['\x00', '\x10', '\x3f', '\n', ' ']);
+
+ flagsUseDefaultMatcher.forEach((atSign) => {
+ ShouldNotBeFollowedBy.forEach((followedSymbol) => {
+ const seq = atSign + followedSymbol;
+
+ it(`should not match "${seq}"`, () => {
+ expect(defaultMatcher(atwhoInstance, atSign, seq)).toBe(null);
+ });
+ });
+ });
+ });
+ });
+
describe('isLoading', function () {
it('should be true with loading data object item', function () {
expect(GfmAutoComplete.isLoading({ name: 'loading' })).toBe(true);
diff --git a/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js b/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js
index a954bb60560..861f26e162f 100644
--- a/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js
+++ b/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js
@@ -1,9 +1,7 @@
-/* eslint-disable quotes, jasmine/no-suite-dupes, vars-on-top, no-var, max-len */
-/* global d3 */
-/* global ContributorsGraph */
-/* global ContributorsMasterGraph */
+/* eslint-disable quotes, jasmine/no-suite-dupes, vars-on-top, no-var */
-require('~/graphs/stat_graph_contributors_graph');
+import d3 from 'd3';
+import { ContributorsGraph, ContributorsMasterGraph } from '~/graphs/stat_graph_contributors_graph';
describe("ContributorsGraph", function () {
describe("#set_x_domain", function () {
diff --git a/spec/javascripts/graphs/stat_graph_contributors_util_spec.js b/spec/javascripts/graphs/stat_graph_contributors_util_spec.js
index b15764abe8c..9b47ab62181 100644
--- a/spec/javascripts/graphs/stat_graph_contributors_util_spec.js
+++ b/spec/javascripts/graphs/stat_graph_contributors_util_spec.js
@@ -1,7 +1,6 @@
/* 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');
+import ContributorsStatGraphUtil from '~/graphs/stat_graph_contributors_util';
describe("ContributorsStatGraphUtil", function () {
describe("#parse_log", function () {
diff --git a/spec/javascripts/graphs/stat_graph_spec.js b/spec/javascripts/graphs/stat_graph_spec.js
deleted file mode 100644
index 876c23361bc..00000000000
--- a/spec/javascripts/graphs/stat_graph_spec.js
+++ /dev/null
@@ -1,20 +0,0 @@
-/* eslint-disable quotes */
-/* global StatGraph */
-
-require('~/graphs/stat_graph');
-
-describe("StatGraph", function () {
- describe("#get_log", function () {
- it("returns log", function () {
- StatGraph.log = "test";
- expect(StatGraph.get_log()).toBe("test");
- });
- });
-
- describe("#set_log", 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 2b263b71b7d..46a27b8c98f 100644
--- a/spec/javascripts/header_spec.js
+++ b/spec/javascripts/header_spec.js
@@ -45,9 +45,9 @@ require('~/lib/utils/text_utility');
expect(isTodosCountHidden()).toEqual(false);
});
- it('should add delimiter to todos-pending-count', function() {
- expect($(todosPendingCount).text()).toEqual('1,000');
+ it('should show 99+ for todos-pending-count', function() {
+ expect($(todosPendingCount).text()).toEqual('99+');
});
});
});
-}).call(this);
+}).call(window);
diff --git a/spec/javascripts/helpers/class_spec_helper.js.es6 b/spec/javascripts/helpers/class_spec_helper.js.es6
index d3c37d39431..61db27a8fcc 100644
--- a/spec/javascripts/helpers/class_spec_helper.js.es6
+++ b/spec/javascripts/helpers/class_spec_helper.js.es6
@@ -7,3 +7,5 @@ class ClassSpecHelper {
}
window.ClassSpecHelper = ClassSpecHelper;
+
+module.exports = ClassSpecHelper;
diff --git a/spec/javascripts/issue_spec.js b/spec/javascripts/issue_spec.js
index beb544468ef..e7530f61385 100644
--- a/spec/javascripts/issue_spec.js
+++ b/spec/javascripts/issue_spec.js
@@ -165,4 +165,4 @@ require('~/issue');
expect($('.issue_counter')).toHaveText(1);
});
});
-}).call(this);
+}).call(window);
diff --git a/spec/javascripts/lib/utils/text_utility_spec.js.es6 b/spec/javascripts/lib/utils/text_utility_spec.js.es6
index 86ade66ec29..06b69b8ac17 100644
--- a/spec/javascripts/lib/utils/text_utility_spec.js.es6
+++ b/spec/javascripts/lib/utils/text_utility_spec.js.es6
@@ -35,5 +35,16 @@ require('~/lib/utils/text_utility');
expect(gl.text.pluralize('test', 1)).toBe('test');
});
});
+
+ describe('gl.text.highCountTrim', () => {
+ it('returns 99+ for count >= 100', () => {
+ expect(gl.text.highCountTrim(105)).toBe('99+');
+ expect(gl.text.highCountTrim(100)).toBe('99+');
+ });
+
+ it('returns exact number for count < 100', () => {
+ expect(gl.text.highCountTrim(45)).toBe(45);
+ });
+ });
});
})();
diff --git a/spec/javascripts/line_highlighter_spec.js b/spec/javascripts/line_highlighter_spec.js
index 8b196f7720f..a0b2ebc221b 100644
--- a/spec/javascripts/line_highlighter_spec.js
+++ b/spec/javascripts/line_highlighter_spec.js
@@ -227,4 +227,4 @@ require('~/line_highlighter');
});
});
});
-}).call(this);
+}).call(window);
diff --git a/spec/javascripts/merge_request_spec.js b/spec/javascripts/merge_request_spec.js
index 25cfa9e9479..fd97dced870 100644
--- a/spec/javascripts/merge_request_spec.js
+++ b/spec/javascripts/merge_request_spec.js
@@ -6,9 +6,9 @@ require('~/merge_request');
(function() {
describe('MergeRequest', function() {
return describe('task lists', function() {
- preloadFixtures('static/merge_requests_show.html.raw');
+ preloadFixtures('merge_requests/merge_request_with_task_list.html.raw');
beforeEach(function() {
- loadFixtures('static/merge_requests_show.html.raw');
+ loadFixtures('merge_requests/merge_request_with_task_list.html.raw');
return this.merge = new MergeRequest();
});
it('modifies the Markdown field', function() {
@@ -19,11 +19,11 @@ require('~/merge_request');
return it('submits an ajax request on tasklist:changed', function() {
spyOn(jQuery, 'ajax').and.callFake(function(req) {
expect(req.type).toBe('PATCH');
- expect(req.url).toBe('/foo');
+ expect(req.url).toBe(`${gl.TEST_HOST}/frontend-fixtures/merge-requests-project/merge_requests/1.json`);
return expect(req.data.merge_request.description).not.toBe(null);
});
return $('.js-task-list-field').trigger('tasklist:changed');
});
});
});
-}).call(this);
+}).call(window);
diff --git a/spec/javascripts/merge_request_tabs_spec.js b/spec/javascripts/merge_request_tabs_spec.js
index 5b0c124962c..7506e6ab49e 100644
--- a/spec/javascripts/merge_request_tabs_spec.js
+++ b/spec/javascripts/merge_request_tabs_spec.js
@@ -209,4 +209,4 @@ require('vendor/jquery.scrollTo');
});
});
});
-}).call(this);
+}).call(window);
diff --git a/spec/javascripts/merge_request_widget_spec.js b/spec/javascripts/merge_request_widget_spec.js
index 8cefdd2409d..d5193b41c33 100644
--- a/spec/javascripts/merge_request_widget_spec.js
+++ b/spec/javascripts/merge_request_widget_spec.js
@@ -189,4 +189,4 @@ require('~/lib/utils/datetime_utility');
});
});
});
-}).call(this);
+}).call(window);
diff --git a/spec/javascripts/new_branch_spec.js b/spec/javascripts/new_branch_spec.js
index 1d014502c2a..f132537b943 100644
--- a/spec/javascripts/new_branch_spec.js
+++ b/spec/javascripts/new_branch_spec.js
@@ -166,4 +166,4 @@ require('~/new_branch_form');
});
});
});
-}).call(this);
+}).call(window);
diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js
index af495787c54..d81a5bbb6a5 100644
--- a/spec/javascripts/notes_spec.js
+++ b/spec/javascripts/notes_spec.js
@@ -35,15 +35,13 @@ require('~/lib/utils/text_utility');
expect($('.js-task-list-field').val()).toBe('- [x] Task List Item');
});
- it('submits the form on tasklist:changed', function() {
- var submitted = false;
- $('form').on('submit', function(e) {
- submitted = true;
- e.preventDefault();
+ it('submits an ajax request on tasklist:changed', function() {
+ spyOn(jQuery, 'ajax').and.callFake(function(req) {
+ expect(req.type).toBe('PATCH');
+ expect(req.url).toBe('http://test.host/frontend-fixtures/issues-project/notes/1');
+ return expect(req.data.note).not.toBe(null);
});
-
$('.js-task-list-field').trigger('tasklist:changed');
- expect(submitted).toBe(true);
});
});
@@ -75,4 +73,4 @@ require('~/lib/utils/text_utility');
});
});
});
-}).call(this);
+}).call(window);
diff --git a/spec/javascripts/project_dashboard_spec.js.es6 b/spec/javascripts/project_dashboard_spec.js.es6
deleted file mode 100644
index 24833b4eb57..00000000000
--- a/spec/javascripts/project_dashboard_spec.js.es6
+++ /dev/null
@@ -1,86 +0,0 @@
-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 bfe3d2df79d..69d9587771f 100644
--- a/spec/javascripts/project_title_spec.js
+++ b/spec/javascripts/project_title_spec.js
@@ -10,11 +10,11 @@ require('~/project');
(function() {
describe('Project Title', function() {
- preloadFixtures('static/project_title.html.raw');
+ preloadFixtures('issues/open-issue.html.raw');
loadJSONFixtures('projects.json');
beforeEach(function() {
- loadFixtures('static/project_title.html.raw');
+ loadFixtures('issues/open-issue.html.raw');
window.gon = {};
window.gon.api_version = 'v3';
@@ -38,15 +38,12 @@ require('~/project');
return spyOn(jQuery, 'ajax').and.callFake(fakeAjaxResponse.bind(_this));
};
})(this));
- it('to show on toggle click', (function(_this) {
- return function() {
- $('.js-projects-dropdown-toggle').click();
- return expect($('.header-content').hasClass('open')).toBe(true);
- };
- })(this));
- return it('hide dropdown', function() {
- $(".dropdown-menu-close-icon").click();
- return expect($('.header-content').hasClass('open')).toBe(false);
+ it('toggles dropdown', function() {
+ var menu = $('.js-dropdown-menu-projects');
+ $('.js-projects-dropdown-toggle').click();
+ expect(menu).toHaveClass('open');
+ menu.find('.dropdown-menu-close-icon').click();
+ expect(menu).not.toHaveClass('open');
});
});
@@ -54,4 +51,4 @@ require('~/project');
window.gon = {};
});
});
-}).call(this);
+}).call(window);
diff --git a/spec/javascripts/right_sidebar_spec.js b/spec/javascripts/right_sidebar_spec.js
index 9284af8a8d9..4ac7e911740 100644
--- a/spec/javascripts/right_sidebar_spec.js
+++ b/spec/javascripts/right_sidebar_spec.js
@@ -79,4 +79,4 @@ require('~/extensions/jquery.js');
expect(todoToggleSpy.calls.count()).toEqual(1);
});
});
-}).call(this);
+}).call(window);
diff --git a/spec/javascripts/search_autocomplete_spec.js b/spec/javascripts/search_autocomplete_spec.js
index 9572b52ec1e..aaf058bd755 100644
--- a/spec/javascripts/search_autocomplete_spec.js
+++ b/spec/javascripts/search_autocomplete_spec.js
@@ -89,8 +89,8 @@ require('vendor/fuzzaldrin-plus');
var a1, a2, a3, a4, issuesAssignedToMeLink, issuesIHaveCreatedLink, mrsAssignedToMeLink, mrsIHaveCreatedLink;
issuesAssignedToMeLink = issuesPath + "/?assignee_username=" + userName;
issuesIHaveCreatedLink = issuesPath + "/?author_username=" + userName;
- mrsAssignedToMeLink = mrsPath + "/?assignee_id=" + userId;
- mrsIHaveCreatedLink = mrsPath + "/?author_id=" + userId;
+ mrsAssignedToMeLink = mrsPath + "/?assignee_username=" + userName;
+ mrsIHaveCreatedLink = mrsPath + "/?author_username=" + userName;
a1 = "a[href='" + issuesAssignedToMeLink + "']";
a2 = "a[href='" + issuesIHaveCreatedLink + "']";
a3 = "a[href='" + mrsAssignedToMeLink + "']";
@@ -175,4 +175,4 @@ require('vendor/fuzzaldrin-plus');
expect(enterKeyEvent.isDefaultPrevented()).toBe(true);
});
});
-}).call(this);
+}).call(window);
diff --git a/spec/javascripts/shortcuts_issuable_spec.js b/spec/javascripts/shortcuts_issuable_spec.js
index 602ac01aec3..ffff643e371 100644
--- a/spec/javascripts/shortcuts_issuable_spec.js
+++ b/spec/javascripts/shortcuts_issuable_spec.js
@@ -79,4 +79,4 @@ require('~/shortcuts_issuable');
});
});
});
-}).call(this);
+}).call(window);
diff --git a/spec/javascripts/syntax_highlight_spec.js b/spec/javascripts/syntax_highlight_spec.js
index c0c3837d1f4..cea223bd243 100644
--- a/spec/javascripts/syntax_highlight_spec.js
+++ b/spec/javascripts/syntax_highlight_spec.js
@@ -41,4 +41,4 @@ require('~/syntax_highlight');
});
});
});
-}).call(this);
+}).call(window);
diff --git a/spec/javascripts/u2f/authenticate_spec.js b/spec/javascripts/u2f/authenticate_spec.js
index cba1af4daa4..af2d02b6b29 100644
--- a/spec/javascripts/u2f/authenticate_spec.js
+++ b/spec/javascripts/u2f/authenticate_spec.js
@@ -69,4 +69,4 @@ require('./mock_u2f_device');
});
});
});
-}).call(this);
+}).call(window);
diff --git a/spec/javascripts/u2f/mock_u2f_device.js b/spec/javascripts/u2f/mock_u2f_device.js
index 287bfb4138b..6677fe9c1ee 100644
--- a/spec/javascripts/u2f/mock_u2f_device.js
+++ b/spec/javascripts/u2f/mock_u2f_device.js
@@ -30,4 +30,4 @@
return MockU2FDevice;
})();
-}).call(this);
+}).call(window);
diff --git a/spec/javascripts/u2f/register_spec.js b/spec/javascripts/u2f/register_spec.js
index 10578c2c4b5..0f390c8b980 100644
--- a/spec/javascripts/u2f/register_spec.js
+++ b/spec/javascripts/u2f/register_spec.js
@@ -74,4 +74,4 @@ require('./mock_u2f_device');
});
});
});
-}).call(this);
+}).call(window);
diff --git a/spec/javascripts/user_callout_spec.js.es6 b/spec/javascripts/user_callout_spec.js.es6
new file mode 100644
index 00000000000..6ee63f56a26
--- /dev/null
+++ b/spec/javascripts/user_callout_spec.js.es6
@@ -0,0 +1,37 @@
+const UserCallout = require('~/user_callout');
+
+const USER_CALLOUT_COOKIE = 'user_callout_dismissed';
+const Cookie = window.Cookies;
+
+describe('UserCallout', () => {
+ const fixtureName = 'static/user_callout.html.raw';
+ preloadFixtures(fixtureName);
+
+ beforeEach(function () {
+ loadFixtures(fixtureName);
+ this.userCallout = new UserCallout();
+ this.closeButton = $('.close-user-callout');
+ this.userCalloutBtn = $('.user-callout-btn');
+ this.userCalloutContainer = $('.user-callout');
+ Cookie.set(USER_CALLOUT_COOKIE, 'false');
+ });
+
+ afterEach(function () {
+ Cookie.set(USER_CALLOUT_COOKIE, 'false');
+ });
+
+ it('shows when cookie is set to false', function () {
+ expect(Cookie.get(USER_CALLOUT_COOKIE)).toBeDefined();
+ expect(this.userCalloutContainer.is(':visible')).toBe(true);
+ });
+
+ it('hides when user clicks on the dismiss-icon', function () {
+ this.closeButton.click();
+ expect(Cookie.get(USER_CALLOUT_COOKIE)).toBe('true');
+ });
+
+ it('hides when user clicks on the "check it out" button', function () {
+ this.userCalloutBtn.click();
+ expect(Cookie.get(USER_CALLOUT_COOKIE)).toBe('true');
+ });
+});
diff --git a/spec/javascripts/version_check_image_spec.js.es6 b/spec/javascripts/version_check_image_spec.js.es6
new file mode 100644
index 00000000000..464c1fce210
--- /dev/null
+++ b/spec/javascripts/version_check_image_spec.js.es6
@@ -0,0 +1,33 @@
+const ClassSpecHelper = require('./helpers/class_spec_helper');
+const VersionCheckImage = require('~/version_check_image');
+require('jquery');
+
+describe('VersionCheckImage', function () {
+ describe('.bindErrorEvent', function () {
+ ClassSpecHelper.itShouldBeAStaticMethod(VersionCheckImage, 'bindErrorEvent');
+
+ beforeEach(function () {
+ this.imageElement = $('<div></div>');
+ });
+
+ it('registers an error event', function () {
+ spyOn($.prototype, 'on');
+ spyOn($.prototype, 'off').and.callFake(function () { return this; });
+
+ VersionCheckImage.bindErrorEvent(this.imageElement);
+
+ expect($.prototype.off).toHaveBeenCalledWith('error');
+ expect($.prototype.on).toHaveBeenCalledWith('error', jasmine.any(Function));
+ });
+
+ it('hides the imageElement on error', function () {
+ spyOn($.prototype, 'hide');
+
+ VersionCheckImage.bindErrorEvent(this.imageElement);
+
+ this.imageElement.trigger('error');
+
+ expect($.prototype.hide).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/table_pagination_spec.js.es6 b/spec/javascripts/vue_shared/components/table_pagination_spec.js.es6
index dd495cb43bc..9cb067921a7 100644
--- a/spec/javascripts/vue_shared/components/table_pagination_spec.js.es6
+++ b/spec/javascripts/vue_shared/components/table_pagination_spec.js.es6
@@ -6,12 +6,10 @@ describe('Pagination component', () => {
const changeChanges = {
one: '',
- two: '',
};
- const change = (one, two) => {
+ const change = (one) => {
changeChanges.one = one;
- changeChanges.two = two;
};
it('should render and start at page 1', () => {
@@ -34,7 +32,6 @@ describe('Pagination component', () => {
component.changePage({ target: { innerText: '1' } });
expect(changeChanges.one).toEqual(1);
- expect(changeChanges.two).toEqual(null);
});
it('should go to the previous page', () => {
@@ -55,7 +52,6 @@ describe('Pagination component', () => {
component.changePage({ target: { innerText: 'Prev' } });
expect(changeChanges.one).toEqual(1);
- expect(changeChanges.two).toEqual(null);
});
it('should go to the next page', () => {
@@ -76,7 +72,6 @@ describe('Pagination component', () => {
component.changePage({ target: { innerText: 'Next' } });
expect(changeChanges.one).toEqual(5);
- expect(changeChanges.two).toEqual(null);
});
it('should go to the last page', () => {
@@ -97,7 +92,6 @@ describe('Pagination component', () => {
component.changePage({ target: { innerText: 'Last >>' } });
expect(changeChanges.one).toEqual(10);
- expect(changeChanges.two).toEqual(null);
});
it('should go to the first page', () => {
@@ -118,7 +112,6 @@ describe('Pagination component', () => {
component.changePage({ target: { innerText: '<< First' } });
expect(changeChanges.one).toEqual(1);
- expect(changeChanges.two).toEqual(null);
});
it('should do nothing', () => {
@@ -139,7 +132,6 @@ describe('Pagination component', () => {
component.changePage({ target: { innerText: '...' } });
expect(changeChanges.one).toEqual(1);
- expect(changeChanges.two).toEqual(null);
});
});
diff --git a/spec/javascripts/zen_mode_spec.js b/spec/javascripts/zen_mode_spec.js
index ce33a6814aa..99515f2e5f2 100644
--- a/spec/javascripts/zen_mode_spec.js
+++ b/spec/javascripts/zen_mode_spec.js
@@ -76,4 +76,4 @@ require('~/zen_mode');
keyCode: 27
}));
};
-}).call(this);
+}).call(window);
diff --git a/spec/lib/banzai/filter/image_link_filter_spec.rb b/spec/lib/banzai/filter/image_link_filter_spec.rb
index a2a1ed58d1b..294558b3db2 100644
--- a/spec/lib/banzai/filter/image_link_filter_spec.rb
+++ b/spec/lib/banzai/filter/image_link_filter_spec.rb
@@ -13,8 +13,8 @@ describe Banzai::Filter::ImageLinkFilter, lib: true do
end
it 'does not wrap a duplicate link' do
- exp = act = %q(<a href="/whatever">#{image('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg')}</a>)
- expect(filter(act).to_html).to eq exp
+ doc = filter(%Q(<a href="/whatever">#{image('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg')}</a>))
+ expect(doc.to_html).to match /^<a href="\/whatever"><img[^>]*><\/a>$/
end
it 'works with external images' do
@@ -22,8 +22,8 @@ describe Banzai::Filter::ImageLinkFilter, lib: true do
expect(doc.at_css('img')['src']).to eq doc.at_css('a')['href']
end
- it 'wraps the image with a link and a div' do
- doc = filter(image('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg'))
- expect(doc.to_html).to include('<div class="image-container">')
+ it 'works with inline images' do
+ doc = filter(%Q(<p>test #{image('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg')} inline</p>))
+ expect(doc.to_html).to match /^<p>test <a[^>]*><img[^>]*><\/a> inline<\/p>$/
end
end
diff --git a/spec/lib/banzai/filter/issue_reference_filter_spec.rb b/spec/lib/banzai/filter/issue_reference_filter_spec.rb
index 456dbac0698..11607d4fb26 100644
--- a/spec/lib/banzai/filter/issue_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/issue_reference_filter_spec.rb
@@ -311,7 +311,7 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do
end
end
- describe '#issues_per_Project' do
+ describe '#issues_per_project' do
context 'using an internal issue tracker' do
it 'returns a Hash containing the issues per project' do
doc = Nokogiri::HTML.fragment('')
@@ -346,4 +346,26 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do
end
end
end
+
+ describe '.references_in' do
+ let(:merge_request) { create(:merge_request) }
+
+ it 'yields valid references' do
+ expect do |b|
+ described_class.references_in(issue.to_reference, &b)
+ end.to yield_with_args(issue.to_reference, issue.iid, nil, nil, MatchData)
+ end
+
+ it "doesn't yield invalid references" do
+ expect do |b|
+ described_class.references_in('#0', &b)
+ end.not_to yield_control
+ end
+
+ it "doesn't yield unsupported references" do
+ expect do |b|
+ described_class.references_in(merge_request.to_reference, &b)
+ end.not_to yield_control
+ end
+ end
end
diff --git a/spec/lib/bitbucket/collection_spec.rb b/spec/lib/bitbucket/collection_spec.rb
index 015a7f80e03..9008cb3e870 100644
--- a/spec/lib/bitbucket/collection_spec.rb
+++ b/spec/lib/bitbucket/collection_spec.rb
@@ -19,6 +19,6 @@ describe Bitbucket::Collection do
it "iterates paginator" do
collection = described_class.new(TestPaginator.new)
- expect(collection.to_a).to match(["result_1_page_1", "result_2_page_1", "result_1_page_2", "result_2_page_2"])
+ expect(collection.to_a).to match(%w(result_1_page_1 result_2_page_1 result_1_page_2 result_2_page_2))
end
end
diff --git a/spec/lib/bitbucket/representation/repo_spec.rb b/spec/lib/bitbucket/representation/repo_spec.rb
index adcd978e1b3..405265cc669 100644
--- a/spec/lib/bitbucket/representation/repo_spec.rb
+++ b/spec/lib/bitbucket/representation/repo_spec.rb
@@ -29,7 +29,7 @@ describe Bitbucket::Representation::Repo do
end
describe '#owner_and_slug' do
- it { expect(described_class.new({ 'full_name' => 'ben/test' }).owner_and_slug).to eq(['ben', 'test']) }
+ it { expect(described_class.new({ 'full_name' => 'ben/test' }).owner_and_slug).to eq(%w(ben test)) }
end
describe '#owner' do
@@ -42,7 +42,7 @@ describe Bitbucket::Representation::Repo do
describe '#clone_url' do
it 'builds url' do
- data = { 'links' => { 'clone' => [ { 'name' => 'https', 'href' => 'https://bibucket.org/test/test.git' }] } }
+ data = { 'links' => { 'clone' => [{ 'name' => 'https', 'href' => 'https://bibucket.org/test/test.git' }] } }
expect(described_class.new(data).clone_url('abc')).to eq('https://x-token-auth:abc@bibucket.org/test/test.git')
end
end
diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
index 68ad429608d..7145f0da1d3 100644
--- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
+++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
@@ -96,7 +96,7 @@ module Ci
it "returns builds if only has a list of branches including specified" do
config = YAML.dump({
before_script: ["pwd"],
- rspec: { script: "rspec", type: type, only: ["master", "deploy"] }
+ rspec: { script: "rspec", type: type, only: %w(master deploy) }
})
config_processor = GitlabCiYamlProcessor.new(config, path)
@@ -173,8 +173,8 @@ module Ci
it "returns build only for specified type" do
config = YAML.dump({
before_script: ["pwd"],
- rspec: { script: "rspec", type: "test", only: ["master", "deploy"] },
- staging: { script: "deploy", type: "deploy", only: ["master", "deploy"] },
+ rspec: { script: "rspec", type: "test", only: %w(master deploy) },
+ staging: { script: "deploy", type: "deploy", only: %w(master deploy) },
production: { script: "deploy", type: "deploy", only: ["master@path", "deploy"] },
})
@@ -252,7 +252,7 @@ module Ci
it "does not return builds if except has a list of branches including specified" do
config = YAML.dump({
before_script: ["pwd"],
- rspec: { script: "rspec", type: type, except: ["master", "deploy"] }
+ rspec: { script: "rspec", type: type, except: %w(master deploy) }
})
config_processor = GitlabCiYamlProcessor.new(config, path)
@@ -580,7 +580,7 @@ module Ci
context 'when syntax is incorrect' do
context 'when variables defined but invalid' do
let(:variables) do
- [ 'VAR1', 'value1', 'VAR2', 'value2' ]
+ %w(VAR1 value1 VAR2 value2)
end
it 'raises error' do
@@ -909,7 +909,7 @@ module Ci
end
context 'dependencies to builds' do
- let(:dependencies) { ['build1', 'build2'] }
+ let(:dependencies) { %w(build1 build2) }
it { expect { subject }.not_to raise_error }
end
@@ -1215,7 +1215,7 @@ EOT
end
it "returns errors if job stage is not a defined stage" do
- config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", type: "acceptance" } })
+ config = YAML.dump({ types: %w(build test), rspec: { script: "test", type: "acceptance" } })
expect do
GitlabCiYamlProcessor.new(config, path)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: stage parameter should be build, test")
@@ -1257,42 +1257,42 @@ EOT
end
it "returns errors if job artifacts:name is not an a string" do
- config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", artifacts: { name: 1 } } })
+ config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { name: 1 } } })
expect do
GitlabCiYamlProcessor.new(config)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts name should be a string")
end
it "returns errors if job artifacts:when is not an a predefined value" do
- config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", artifacts: { when: 1 } } })
+ config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { when: 1 } } })
expect do
GitlabCiYamlProcessor.new(config)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts when should be on_success, on_failure or always")
end
it "returns errors if job artifacts:expire_in is not an a string" do
- config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", artifacts: { expire_in: 1 } } })
+ config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { expire_in: 1 } } })
expect do
GitlabCiYamlProcessor.new(config)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts expire in should be a duration")
end
it "returns errors if job artifacts:expire_in is not an a valid duration" do
- config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", artifacts: { expire_in: "7 elephants" } } })
+ config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { expire_in: "7 elephants" } } })
expect do
GitlabCiYamlProcessor.new(config)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts expire in should be a duration")
end
it "returns errors if job artifacts:untracked is not an array of strings" do
- config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", artifacts: { untracked: "string" } } })
+ config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { untracked: "string" } } })
expect do
GitlabCiYamlProcessor.new(config)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts untracked should be a boolean value")
end
it "returns errors if job artifacts:paths is not an array of strings" do
- config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", artifacts: { paths: "string" } } })
+ config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { paths: "string" } } })
expect do
GitlabCiYamlProcessor.new(config)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts paths should be an array of strings")
@@ -1320,28 +1320,28 @@ EOT
end
it "returns errors if job cache:key is not an a string" do
- config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", cache: { key: 1 } } })
+ config = YAML.dump({ types: %w(build test), rspec: { script: "test", cache: { key: 1 } } })
expect do
GitlabCiYamlProcessor.new(config)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:cache:key config should be a string or symbol")
end
it "returns errors if job cache:untracked is not an array of strings" do
- config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", cache: { untracked: "string" } } })
+ config = YAML.dump({ types: %w(build test), rspec: { script: "test", cache: { untracked: "string" } } })
expect do
GitlabCiYamlProcessor.new(config)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:cache:untracked config should be a boolean value")
end
it "returns errors if job cache:paths is not an array of strings" do
- config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", cache: { paths: "string" } } })
+ config = YAML.dump({ types: %w(build test), rspec: { script: "test", cache: { paths: "string" } } })
expect do
GitlabCiYamlProcessor.new(config)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:cache:paths config should be an array of strings")
end
it "returns errors if job dependencies is not an array of strings" do
- config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", dependencies: "string" } })
+ config = YAML.dump({ types: %w(build test), rspec: { script: "test", dependencies: "string" } })
expect do
GitlabCiYamlProcessor.new(config)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec dependencies should be an array of strings")
diff --git a/spec/lib/constraints/project_url_constrainer_spec.rb b/spec/lib/constraints/project_url_constrainer_spec.rb
index a5251e9a8c2..4f25ad88960 100644
--- a/spec/lib/constraints/project_url_constrainer_spec.rb
+++ b/spec/lib/constraints/project_url_constrainer_spec.rb
@@ -6,7 +6,7 @@ describe ProjectUrlConstrainer, lib: true do
describe '#matches?' do
context 'valid request' do
- let(:request) { build_request(namespace.path, project.path) }
+ let(:request) { build_request(namespace.full_path, project.path) }
it { expect(subject.matches?(request)).to be_truthy }
end
@@ -19,7 +19,7 @@ describe ProjectUrlConstrainer, lib: true do
end
context "project id ending with .git" do
- let(:request) { build_request(namespace.path, project.path + '.git') }
+ let(:request) { build_request(namespace.full_path, project.path + '.git') }
it { expect(subject.matches?(request)).to be_falsey }
end
diff --git a/spec/lib/expand_variables_spec.rb b/spec/lib/expand_variables_spec.rb
index 90bc7dad379..730ca1f7c0a 100644
--- a/spec/lib/expand_variables_spec.rb
+++ b/spec/lib/expand_variables_spec.rb
@@ -7,58 +7,49 @@ describe ExpandVariables do
tests = [
{ value: 'key',
result: 'key',
- variables: []
- },
+ variables: [] },
{ value: 'key$variable',
result: 'key',
- variables: []
- },
+ variables: [] },
{ value: 'key$variable',
result: 'keyvalue',
variables: [
{ key: 'variable', value: 'value' }
- ]
- },
+ ] },
{ value: 'key${variable}',
result: 'keyvalue',
variables: [
{ key: 'variable', value: 'value' }
- ]
- },
+ ] },
{ value: 'key$variable$variable2',
result: 'keyvalueresult',
variables: [
{ key: 'variable', value: 'value' },
{ key: 'variable2', value: 'result' },
- ]
- },
+ ] },
{ value: 'key${variable}${variable2}',
result: 'keyvalueresult',
variables: [
{ key: 'variable', value: 'value' },
{ key: 'variable2', value: 'result' }
- ]
- },
+ ] },
{ value: 'key$variable2$variable',
result: 'keyresultvalue',
variables: [
{ key: 'variable', value: 'value' },
{ key: 'variable2', value: 'result' },
- ]
- },
+ ] },
{ value: 'key${variable2}${variable}',
result: 'keyresultvalue',
variables: [
{ key: 'variable', value: 'value' },
{ key: 'variable2', value: 'result' }
- ]
- },
+ ] },
{ value: 'review/$CI_BUILD_REF_NAME',
result: 'review/feature/add-review-apps',
variables: [
{ key: 'CI_BUILD_REF_NAME', value: 'feature/add-review-apps' }
- ]
- },
+ ] },
]
tests.each do |test|
diff --git a/spec/lib/extracts_path_spec.rb b/spec/lib/extracts_path_spec.rb
index 29c07655ae8..33ab005667a 100644
--- a/spec/lib/extracts_path_spec.rb
+++ b/spec/lib/extracts_path_spec.rb
@@ -177,12 +177,12 @@ describe ExtractsPath, lib: true do
it "extracts a valid commit SHA" do
expect(extract_ref('f4b14494ef6abf3d144c28e4af0c20143383e062/CHANGELOG')).to eq(
- ['f4b14494ef6abf3d144c28e4af0c20143383e062', 'CHANGELOG']
+ %w(f4b14494ef6abf3d144c28e4af0c20143383e062 CHANGELOG)
)
end
it "falls back to a primitive split for an invalid ref" do
- expect(extract_ref('stable/CHANGELOG')).to eq(['stable', 'CHANGELOG'])
+ expect(extract_ref('stable/CHANGELOG')).to eq(%w(stable CHANGELOG))
end
end
end
diff --git a/spec/lib/gitlab/badge/shared/metadata.rb b/spec/lib/gitlab/badge/shared/metadata.rb
index 0cf18514251..63c7ca5a915 100644
--- a/spec/lib/gitlab/badge/shared/metadata.rb
+++ b/spec/lib/gitlab/badge/shared/metadata.rb
@@ -18,4 +18,14 @@ shared_examples 'badge metadata' do
it { is_expected.to include metadata.image_url }
it { is_expected.to include metadata.link_url }
end
+
+ describe '#to_asciidoc' do
+ subject { metadata.to_asciidoc }
+
+ it { is_expected.to include metadata.image_url }
+ it { is_expected.to include metadata.link_url }
+ it { is_expected.to include 'image:' }
+ it { is_expected.to include 'link=' }
+ it { is_expected.to include 'title=' }
+ end
end
diff --git a/spec/lib/gitlab/bitbucket_import/importer_spec.rb b/spec/lib/gitlab/bitbucket_import/importer_spec.rb
index 0a2fe5af2c3..a7ee7f53a6b 100644
--- a/spec/lib/gitlab/bitbucket_import/importer_spec.rb
+++ b/spec/lib/gitlab/bitbucket_import/importer_spec.rb
@@ -87,10 +87,10 @@ describe Gitlab::BitbucketImport::Importer, lib: true do
body: issues_statuses_sample_data.to_json)
stub_request(:get, "https://api.bitbucket.org/2.0/repositories/namespace/repo?pagelen=50&sort=created_on").
- with(headers: { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization' => 'Bearer', 'User-Agent' => 'Faraday v0.9.2' }).
- to_return(status: 200,
- body: "",
- headers: {})
+ with(headers: { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization' => 'Bearer', 'User-Agent' => 'Faraday v0.9.2' }).
+ to_return(status: 200,
+ body: "",
+ headers: {})
sample_issues_statuses.each_with_index do |issue, index|
stub_request(
diff --git a/spec/lib/gitlab/chat_commands/presenters/issue_show_spec.rb b/spec/lib/gitlab/chat_commands/presenters/issue_show_spec.rb
index 5b678d31fce..3916fc704a4 100644
--- a/spec/lib/gitlab/chat_commands/presenters/issue_show_spec.rb
+++ b/spec/lib/gitlab/chat_commands/presenters/issue_show_spec.rb
@@ -26,6 +26,21 @@ describe Gitlab::ChatCommands::Presenters::IssueShow do
end
end
+ context 'with labels' do
+ let(:label) { create(:label, project: project, title: 'mep') }
+ let(:label1) { create(:label, project: project, title: 'mop') }
+
+ before do
+ issue.labels << [label, label1]
+ end
+
+ it 'shows the labels' do
+ labels = attachment[:fields].find { |f| f[:title] == 'Labels' }
+
+ expect(labels[:value]).to eq("mep, mop")
+ end
+ end
+
context 'confidential issue' do
let(:issue) { create(:issue, project: project) }
diff --git a/spec/lib/gitlab/ci/config/entry/commands_spec.rb b/spec/lib/gitlab/ci/config/entry/commands_spec.rb
index b8b0825a1c7..afa4a089418 100644
--- a/spec/lib/gitlab/ci/config/entry/commands_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/commands_spec.rb
@@ -4,7 +4,7 @@ describe Gitlab::Ci::Config::Entry::Commands do
let(:entry) { described_class.new(config) }
context 'when entry config value is an array' do
- let(:config) { ['ls', 'pwd'] }
+ let(:config) { %w(ls pwd) }
describe '#value' do
it 'returns array of strings' do
diff --git a/spec/lib/gitlab/ci/config/entry/factory_spec.rb b/spec/lib/gitlab/ci/config/entry/factory_spec.rb
index 00dad5d9591..3395b3c645b 100644
--- a/spec/lib/gitlab/ci/config/entry/factory_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/factory_spec.rb
@@ -8,20 +8,20 @@ describe Gitlab::Ci::Config::Entry::Factory do
context 'when setting a concrete value' do
it 'creates entry with valid value' do
entry = factory
- .value(['ls', 'pwd'])
+ .value(%w(ls pwd))
.create!
- expect(entry.value).to eq ['ls', 'pwd']
+ expect(entry.value).to eq %w(ls pwd)
end
context 'when setting description' do
it 'creates entry with description' do
entry = factory
- .value(['ls', 'pwd'])
+ .value(%w(ls pwd))
.with(description: 'test description')
.create!
- expect(entry.value).to eq ['ls', 'pwd']
+ expect(entry.value).to eq %w(ls pwd)
expect(entry.description).to eq 'test description'
end
end
@@ -29,7 +29,7 @@ describe Gitlab::Ci::Config::Entry::Factory do
context 'when setting key' do
it 'creates entry with custom key' do
entry = factory
- .value(['ls', 'pwd'])
+ .value(%w(ls pwd))
.with(key: 'test key')
.create!
diff --git a/spec/lib/gitlab/ci/config/entry/global_spec.rb b/spec/lib/gitlab/ci/config/entry/global_spec.rb
index 432a99dce33..ebd80ac5e1d 100644
--- a/spec/lib/gitlab/ci/config/entry/global_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/global_spec.rb
@@ -21,12 +21,12 @@ describe Gitlab::Ci::Config::Entry::Global do
context 'when configuration is valid' do
context 'when some entries defined' do
let(:hash) do
- { before_script: ['ls', 'pwd'],
+ { before_script: %w(ls pwd),
image: 'ruby:2.2',
services: ['postgres:9.1', 'mysql:5.5'],
variables: { VAR: 'value' },
after_script: ['make clean'],
- stages: ['build', 'pages'],
+ stages: %w(build pages),
cache: { key: 'k', untracked: true, paths: ['public/'] },
rspec: { script: %w[rspec ls] },
spinach: { before_script: [], variables: {}, script: 'spinach' } }
@@ -89,7 +89,7 @@ describe Gitlab::Ci::Config::Entry::Global do
describe '#before_script_value' do
it 'returns correct script' do
- expect(global.before_script_value).to eq ['ls', 'pwd']
+ expect(global.before_script_value).to eq %w(ls pwd)
end
end
@@ -126,7 +126,7 @@ describe Gitlab::Ci::Config::Entry::Global do
context 'when deprecated types key defined' do
let(:hash) do
- { types: ['test', 'deploy'],
+ { types: %w(test deploy),
rspec: { script: 'rspec' } }
end
@@ -148,7 +148,7 @@ describe Gitlab::Ci::Config::Entry::Global do
expect(global.jobs_value).to eq(
rspec: { name: :rspec,
script: %w[rspec ls],
- before_script: ['ls', 'pwd'],
+ before_script: %w(ls pwd),
commands: "ls\npwd\nrspec\nls",
image: 'ruby:2.2',
services: ['postgres:9.1', 'mysql:5.5'],
diff --git a/spec/lib/gitlab/ci/config/entry/key_spec.rb b/spec/lib/gitlab/ci/config/entry/key_spec.rb
index a55e5b4b8ac..0dd36fe1f44 100644
--- a/spec/lib/gitlab/ci/config/entry/key_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/key_spec.rb
@@ -21,7 +21,7 @@ describe Gitlab::Ci::Config::Entry::Key do
end
context 'when entry value is not correct' do
- let(:config) { [ 'incorrect' ] }
+ let(:config) { ['incorrect'] }
describe '#errors' do
it 'saves errors' do
diff --git a/spec/lib/gitlab/ci/config/entry/paths_spec.rb b/spec/lib/gitlab/ci/config/entry/paths_spec.rb
index e60c9aaf661..1d9c5ddee9b 100644
--- a/spec/lib/gitlab/ci/config/entry/paths_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/paths_spec.rb
@@ -21,7 +21,7 @@ describe Gitlab::Ci::Config::Entry::Paths do
end
context 'when entry value is not valid' do
- let(:config) { [ 1 ] }
+ let(:config) { [1] }
describe '#errors' do
it 'saves errors' do
diff --git a/spec/lib/gitlab/ci/config/entry/script_spec.rb b/spec/lib/gitlab/ci/config/entry/script_spec.rb
index aa99cee2690..069eaa26422 100644
--- a/spec/lib/gitlab/ci/config/entry/script_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/script_spec.rb
@@ -5,7 +5,7 @@ describe Gitlab::Ci::Config::Entry::Script do
describe 'validations' do
context 'when entry config value is correct' do
- let(:config) { ['ls', 'pwd'] }
+ let(:config) { %w(ls pwd) }
describe '#value' do
it 'returns array of strings' do
diff --git a/spec/lib/gitlab/ci/config/entry/variables_spec.rb b/spec/lib/gitlab/ci/config/entry/variables_spec.rb
index 58327d08904..f15f02f403e 100644
--- a/spec/lib/gitlab/ci/config/entry/variables_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/variables_spec.rb
@@ -29,7 +29,7 @@ describe Gitlab::Ci::Config::Entry::Variables do
end
context 'when entry value is not correct' do
- let(:config) { [ :VAR, 'test' ] }
+ let(:config) { [:VAR, 'test'] }
describe '#errors' do
it 'saves errors' do
diff --git a/spec/lib/gitlab/conflict/file_spec.rb b/spec/lib/gitlab/conflict/file_spec.rb
index fbf679c5215..780ac0ad97e 100644
--- a/spec/lib/gitlab/conflict/file_spec.rb
+++ b/spec/lib/gitlab/conflict/file_spec.rb
@@ -44,7 +44,7 @@ describe Gitlab::Conflict::File, lib: true do
it 'returns a file containing only the chosen parts of the resolved sections' do
expect(resolved_lines.chunk { |line| line.type || 'both' }.map(&:first)).
- to eq(['both', 'new', 'both', 'old', 'both', 'new', 'both'])
+ to eq(%w(both new both old both new both))
end
end
@@ -123,7 +123,7 @@ describe Gitlab::Conflict::File, lib: true do
it 'sets conflict to true for sections with only changed lines' do
conflict_file.sections.select { |section| section[:conflict] }.each do |section|
section[:lines].each do |line|
- expect(line.type).to be_in(['new', 'old'])
+ expect(line.type).to be_in(%w(new old))
end
end
end
@@ -251,7 +251,7 @@ FILE
describe '#as_json' do
it 'includes the blob path for the file' do
expect(conflict_file.as_json[:blob_path]).
- to eq("/#{project.namespace.to_param}/#{merge_request.project.to_param}/blob/#{our_commit.oid}/files/ruby/regex.rb")
+ to eq("/#{project.full_path}/blob/#{our_commit.oid}/files/ruby/regex.rb")
end
it 'includes the blob icon for the file' do
diff --git a/spec/lib/gitlab/data_builder/build_spec.rb b/spec/lib/gitlab/data_builder/build_spec.rb
index 6c71e98066b..91c43f2bdc0 100644
--- a/spec/lib/gitlab/data_builder/build_spec.rb
+++ b/spec/lib/gitlab/data_builder/build_spec.rb
@@ -17,5 +17,31 @@ describe Gitlab::DataBuilder::Build do
it { expect(data[:build_allow_failure]).to eq(false) }
it { expect(data[:project_id]).to eq(build.project.id) }
it { expect(data[:project_name]).to eq(build.project.name_with_namespace) }
+
+ context 'commit author_url' do
+ context 'when no commit present' do
+ let(:build) { create(:ci_build) }
+
+ it 'sets to mailing address of git_author_email' do
+ expect(data[:commit][:author_url]).to eq("mailto:#{build.pipeline.git_author_email}")
+ end
+ end
+
+ context 'when commit present but has no author' do
+ let(:build) { create(:ci_build, :with_commit) }
+
+ it 'sets to mailing address of git_author_email' do
+ expect(data[:commit][:author_url]).to eq("mailto:#{build.pipeline.git_author_email}")
+ end
+ end
+
+ context 'when commit and author are present' do
+ let(:build) { create(:ci_build, :with_commit_and_author) }
+
+ it 'sets to GitLab user url' do
+ expect(data[:commit][:author_url]).to eq(Gitlab::Routing.url_helpers.user_url(username: build.commit.author.username))
+ end
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb
index e94ca4fcfd2..e007044868c 100644
--- a/spec/lib/gitlab/database/migration_helpers_spec.rb
+++ b/spec/lib/gitlab/database/migration_helpers_spec.rb
@@ -101,6 +101,16 @@ describe Gitlab::Database::MigrationHelpers, lib: true do
end
end
+ describe '#concurrent_foreign_key_name' do
+ it 'returns the name for a foreign key' do
+ name = model.concurrent_foreign_key_name(:this_is_a_very_long_table_name,
+ :with_a_very_long_column_name)
+
+ expect(name).to be_an_instance_of(String)
+ expect(name.length).to eq(13)
+ end
+ end
+
describe '#disable_statement_timeout' do
context 'using PostgreSQL' do
it 'disables statement timeouts' do
diff --git a/spec/lib/gitlab/database_spec.rb b/spec/lib/gitlab/database_spec.rb
index f01c42aff91..edd01d032c8 100644
--- a/spec/lib/gitlab/database_spec.rb
+++ b/spec/lib/gitlab/database_spec.rb
@@ -119,9 +119,24 @@ describe Gitlab::Database, lib: true do
it 'creates a new connection pool with specific pool size' do
pool = described_class.create_connection_pool(5)
- expect(pool)
- .to be_kind_of(ActiveRecord::ConnectionAdapters::ConnectionPool)
- expect(pool.spec.config[:pool]).to eq(5)
+ begin
+ expect(pool)
+ .to be_kind_of(ActiveRecord::ConnectionAdapters::ConnectionPool)
+
+ expect(pool.spec.config[:pool]).to eq(5)
+ ensure
+ pool.disconnect!
+ end
+ end
+
+ it 'allows setting of a custom hostname' do
+ pool = described_class.create_connection_pool(5, '127.0.0.1')
+
+ begin
+ expect(pool.spec.config[:host]).to eq('127.0.0.1')
+ ensure
+ pool.disconnect!
+ end
end
end
diff --git a/spec/lib/gitlab/diff/highlight_spec.rb b/spec/lib/gitlab/diff/highlight_spec.rb
index 5893485634d..0e9309d278e 100644
--- a/spec/lib/gitlab/diff/highlight_spec.rb
+++ b/spec/lib/gitlab/diff/highlight_spec.rb
@@ -53,21 +53,21 @@ describe Gitlab::Diff::Highlight, lib: true do
end
it 'marks unchanged lines' do
- code = %Q{ def popen(cmd, path=nil)}
+ code = %q{ def popen(cmd, path=nil)}
expect(subject[2].text).to eq(code)
expect(subject[2].text).not_to be_html_safe
end
it 'marks removed lines' do
- code = %Q{- raise "System commands must be given as an array of strings"}
+ code = %q{- raise "System commands must be given as an array of strings"}
expect(subject[4].text).to eq(code)
expect(subject[4].text).not_to be_html_safe
end
it 'marks added lines' do
- code = %Q{+ raise <span class='idiff left right'>RuntimeError, </span>&quot;System commands must be given as an array of strings&quot;}
+ code = %q{+ raise <span class='idiff left right'>RuntimeError, </span>&quot;System commands must be given as an array of strings&quot;}
expect(subject[5].text).to eq(code)
expect(subject[5].text).to be_html_safe
diff --git a/spec/lib/gitlab/git/blob_snippet_spec.rb b/spec/lib/gitlab/git/blob_snippet_spec.rb
index 79b1311ac91..17d6be470ac 100644
--- a/spec/lib/gitlab/git/blob_snippet_spec.rb
+++ b/spec/lib/gitlab/git/blob_snippet_spec.rb
@@ -11,7 +11,7 @@ describe Gitlab::Git::BlobSnippet, seed_helper: true do
end
context 'present lines' do
- let(:snippet) { Gitlab::Git::BlobSnippet.new('master', ['wow', 'much'], 1, 'wow.rb') }
+ let(:snippet) { Gitlab::Git::BlobSnippet.new('master', %w(wow much), 1, 'wow.rb') }
it { expect(snippet.data).to eq("wow\nmuch") }
end
diff --git a/spec/lib/gitlab/git/blob_spec.rb b/spec/lib/gitlab/git/blob_spec.rb
index 84f79ec2391..8049e2c120d 100644
--- a/spec/lib/gitlab/git/blob_spec.rb
+++ b/spec/lib/gitlab/git/blob_spec.rb
@@ -222,191 +222,6 @@ describe Gitlab::Git::Blob, seed_helper: true do
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
diff --git a/spec/lib/gitlab/git/diff_collection_spec.rb b/spec/lib/gitlab/git/diff_collection_spec.rb
index 4fa72c565ae..47bdd7310d5 100644
--- a/spec/lib/gitlab/git/diff_collection_spec.rb
+++ b/spec/lib/gitlab/git/diff_collection_spec.rb
@@ -365,7 +365,7 @@ describe Gitlab::Git::DiffCollection, seed_helper: true do
end
context 'when go over safe limits on files' do
- let(:iterator) { [ fake_diff(1, 1) ] * 4 }
+ let(:iterator) { [fake_diff(1, 1)] * 4 }
before(:each) do
stub_const('Gitlab::Git::DiffCollection::DEFAULT_LIMITS', { max_files: 2, max_lines: max_lines })
diff --git a/spec/lib/gitlab/git/index_spec.rb b/spec/lib/gitlab/git/index_spec.rb
new file mode 100644
index 00000000000..d0c7ca60ddc
--- /dev/null
+++ b/spec/lib/gitlab/git/index_spec.rb
@@ -0,0 +1,220 @@
+require 'spec_helper'
+
+describe Gitlab::Git::Index, seed_helper: true do
+ let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) }
+ let(:index) { described_class.new(repository) }
+
+ before do
+ index.read_tree(repository.lookup('master').tree)
+ end
+
+ describe '#create' do
+ let(:options) do
+ {
+ content: 'Lorem ipsum...',
+ file_path: 'documents/story.txt'
+ }
+ end
+
+ context 'when no file at that path exists' do
+ it 'creates the file in the index' do
+ index.create(options)
+
+ entry = index.get(options[:file_path])
+
+ expect(entry).not_to be_nil
+ expect(repository.lookup(entry[:oid]).content).to eq(options[:content])
+ end
+ end
+
+ context 'when a file at that path exists' do
+ before do
+ options[:file_path] = 'files/executables/ls'
+ end
+
+ it 'raises an error' do
+ expect { index.create(options) }.to raise_error('Filename already exists')
+ end
+ end
+
+ context 'when content is in base64' do
+ before do
+ options[:content] = Base64.encode64(options[:content])
+ options[:encoding] = 'base64'
+ end
+
+ it 'decodes base64' do
+ index.create(options)
+
+ entry = index.get(options[:file_path])
+ expect(repository.lookup(entry[:oid]).content).to eq(Base64.decode64(options[:content]))
+ end
+ end
+
+ context 'when content contains CRLF' do
+ before do
+ repository.autocrlf = :input
+ options[:content] = "Hello,\r\nWorld"
+ end
+
+ it 'converts to LF' do
+ index.create(options)
+
+ entry = index.get(options[:file_path])
+ expect(repository.lookup(entry[:oid]).content).to eq("Hello,\nWorld")
+ end
+ end
+ end
+
+ describe '#create_dir' do
+ let(:options) do
+ {
+ file_path: 'newdir'
+ }
+ end
+
+ context 'when no file or dir at that path exists' do
+ it 'creates the dir in the index' do
+ index.create_dir(options)
+
+ entry = index.get(options[:file_path] + '/.gitkeep')
+
+ expect(entry).not_to be_nil
+ end
+ end
+
+ context 'when a file at that path exists' do
+ before do
+ options[:file_path] = 'files/executables/ls'
+ end
+
+ it 'raises an error' do
+ expect { index.create_dir(options) }.to raise_error('Directory already exists as a file')
+ end
+ end
+
+ context 'when a directory at that path exists' do
+ before do
+ options[:file_path] = 'files/executables'
+ end
+
+ it 'raises an error' do
+ expect { index.create_dir(options) }.to raise_error('Directory already exists')
+ end
+ end
+ end
+
+ describe '#update' do
+ let(:options) do
+ {
+ content: 'Lorem ipsum...',
+ file_path: 'README.md'
+ }
+ end
+
+ context 'when no file at that path exists' do
+ before do
+ options[:file_path] = 'documents/story.txt'
+ end
+
+ it 'raises an error' do
+ expect { index.update(options) }.to raise_error("File doesn't exist")
+ end
+ end
+
+ context 'when a file at that path exists' do
+ it 'updates the file in the index' do
+ index.update(options)
+
+ entry = index.get(options[:file_path])
+
+ expect(repository.lookup(entry[:oid]).content).to eq(options[:content])
+ end
+
+ it 'preserves file mode' do
+ options[:file_path] = 'files/executables/ls'
+
+ index.update(options)
+
+ entry = index.get(options[:file_path])
+
+ expect(entry[:mode]).to eq(0100755)
+ end
+ end
+ end
+
+ describe '#move' do
+ let(:options) do
+ {
+ content: 'Lorem ipsum...',
+ previous_path: 'README.md',
+ file_path: 'NEWREADME.md'
+ }
+ end
+
+ context 'when no file at that path exists' do
+ it 'raises an error' do
+ options[:previous_path] = 'documents/story.txt'
+
+ expect { index.move(options) }.to raise_error("File doesn't exist")
+ end
+ end
+
+ context 'when a file at that path exists' do
+ it 'removes the old file in the index' do
+ index.move(options)
+
+ entry = index.get(options[:previous_path])
+
+ expect(entry).to be_nil
+ end
+
+ it 'creates the new file in the index' do
+ index.move(options)
+
+ entry = index.get(options[:file_path])
+
+ expect(entry).not_to be_nil
+ expect(repository.lookup(entry[:oid]).content).to eq(options[:content])
+ end
+
+ it 'preserves file mode' do
+ options[:previous_path] = 'files/executables/ls'
+
+ index.move(options)
+
+ entry = index.get(options[:file_path])
+
+ expect(entry[:mode]).to eq(0100755)
+ end
+ end
+ end
+
+ describe '#delete' do
+ let(:options) do
+ {
+ file_path: 'README.md'
+ }
+ end
+
+ context 'when no file at that path exists' do
+ before do
+ options[:file_path] = 'documents/story.txt'
+ end
+
+ it 'raises an error' do
+ expect { index.delete(options) }.to raise_error("File doesn't exist")
+ end
+ end
+
+ context 'when a file at that path exists' do
+ it 'removes the file in the index' do
+ index.delete(options)
+
+ entry = index.get(options[:file_path])
+
+ expect(entry).to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
index 2a915bf426f..1919542ca25 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -529,7 +529,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
commit_with_new_name = nil
rename_commit = nil
- before(:all) do
+ before(:context) do
# Add new commits so that there's a renamed file in the commit history
repo = Gitlab::Git::Repository.new(TEST_REPO_PATH).rugged
@@ -538,49 +538,119 @@ describe Gitlab::Git::Repository, seed_helper: true do
commit_with_new_name = new_commit_edit_new_file(repo)
end
+ after(:context) 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
+
context "where 'follow' == true" do
- options = { ref: "master", follow: true }
+ let(:options) { { ref: "master", follow: true } }
context "and 'path' is a directory" do
- let(:log_commits) do
- repository.log(options.merge(path: "encoding"))
- end
+ it "does not follow renames" do
+ log_commits = repository.log(options.merge(path: "encoding"))
- 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)
+ aggregate_failures 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
end
context "and 'path' is a file that matches the new filename" do
- let(:log_commits) do
- repository.log(options.merge(path: "encoding/CHANGELOG"))
+ context 'without offset' do
+ it "follows renames" do
+ log_commits = repository.log(options.merge(path: "encoding/CHANGELOG"))
+
+ aggregate_failures 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
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)
+ context 'with offset=1' do
+ it "follows renames and skip the latest commit" do
+ log_commits = repository.log(options.merge(path: "encoding/CHANGELOG", offset: 1))
+
+ aggregate_failures do
+ expect(log_commits).not_to include(commit_with_new_name)
+ expect(log_commits).to include(rename_commit)
+ expect(log_commits).to include(commit_with_old_name)
+ end
+ end
+ end
+
+ context 'with offset=1', 'and limit=1' do
+ it "follows renames, skip the latest commit and return only one commit" do
+ log_commits = repository.log(options.merge(path: "encoding/CHANGELOG", offset: 1, limit: 1))
+
+ expect(log_commits).to contain_exactly(rename_commit)
+ end
+ end
+
+ context 'with offset=1', 'and limit=2' do
+ it "follows renames, skip the latest commit and return only two commits" do
+ log_commits = repository.log(options.merge(path: "encoding/CHANGELOG", offset: 1, limit: 2))
+
+ aggregate_failures do
+ expect(log_commits).to contain_exactly(rename_commit, commit_with_old_name)
+ end
+ end
+ end
+
+ context 'with offset=2' do
+ it "follows renames and skip the latest commit" do
+ log_commits = repository.log(options.merge(path: "encoding/CHANGELOG", offset: 2))
+
+ aggregate_failures do
+ expect(log_commits).not_to include(commit_with_new_name)
+ expect(log_commits).not_to include(rename_commit)
+ expect(log_commits).to include(commit_with_old_name)
+ end
+ end
+ end
+
+ context 'with offset=2', 'and limit=1' do
+ it "follows renames, skip the two latest commit and return only one commit" do
+ log_commits = repository.log(options.merge(path: "encoding/CHANGELOG", offset: 2, limit: 1))
+
+ expect(log_commits).to contain_exactly(commit_with_old_name)
+ end
+ end
+
+ context 'with offset=2', 'and limit=2' do
+ it "follows renames, skip the two latest commit and return only one commit" do
+ log_commits = repository.log(options.merge(path: "encoding/CHANGELOG", offset: 2, limit: 2))
+
+ aggregate_failures do
+ expect(log_commits).not_to include(commit_with_new_name)
+ expect(log_commits).not_to include(rename_commit)
+ expect(log_commits).to include(commit_with_old_name)
+ end
+ end
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 "does not follow renames" do
+ log_commits = repository.log(options.merge(path: "CHANGELOG"))
- 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)
+ aggregate_failures do
+ expect(log_commits).not_to include(commit_with_new_name)
+ expect(log_commits).to include(rename_commit)
+ expect(log_commits).to include(commit_with_old_name)
+ end
end
end
context "unknown ref" do
- let(:log_commits) { repository.log(options.merge(ref: 'unknown')) }
+ it "returns an empty array" do
+ log_commits = repository.log(options.merge(ref: 'unknown'))
- it "should return empty" do
expect(log_commits).to eq([])
end
end
@@ -699,12 +769,6 @@ describe Gitlab::Git::Repository, seed_helper: true do
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
@@ -844,81 +908,6 @@ describe Gitlab::Git::Repository, seed_helper: true do
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") }
diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb
index a55bd4387e0..48f7754bed8 100644
--- a/spec/lib/gitlab/git_access_spec.rb
+++ b/spec/lib/gitlab/git_access_spec.rb
@@ -199,7 +199,9 @@ describe Gitlab::GitAccess, lib: true do
def stub_git_hooks
# Running the `pre-receive` hook is expensive, and not necessary for this test.
- allow_any_instance_of(GitHooksService).to receive(:execute).and_yield
+ allow_any_instance_of(GitHooksService).to receive(:execute) do |service, &block|
+ block.call(service)
+ end
end
def merge_into_protected_branch
@@ -207,13 +209,12 @@ 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(
+ source_branch = project.repository.create_file(
user,
FFaker::InternetSE.login_user_name,
FFaker::HipsterIpsum.paragraph,
message: FFaker::HipsterIpsum.sentence,
- branch_name: unprotected_branch,
- update: false)
+ branch_name: unprotected_branch)
rugged = project.repository.rugged
author = { email: "email@example.com", time: Time.now, name: "Example Git User" }
@@ -232,11 +233,18 @@ describe Gitlab::GitAccess, lib: true do
else
project.team << [user, role]
end
+ end
+
+ permissions_matrix[role].each do |action, allowed|
+ context action do
+ subject { access.send(:check_push_access!, changes[action]) }
- permissions_matrix[role].each do |action, allowed|
- context action do
- subject { access.send(:check_push_access!, changes[action]) }
- it { expect(subject.allowed?).to allowed ? be_truthy : be_falsey }
+ it do
+ if allowed
+ expect { subject }.not_to raise_error
+ else
+ expect { subject }.to raise_error(Gitlab::GitAccess::UnauthorizedError)
+ end
end
end
end
@@ -301,7 +309,7 @@ describe Gitlab::GitAccess, lib: true do
}
}
- [['feature', 'exact'], ['feat*', 'wildcard']].each do |protected_branch_name, protected_branch_type|
+ [%w(feature exact), ['feat*', 'wildcard']].each do |protected_branch_name, protected_branch_type|
context do
before { create(:protected_branch, name: protected_branch_name, project: project) }
diff --git a/spec/lib/gitlab/git_spec.rb b/spec/lib/gitlab/git_spec.rb
index 219198eff60..8eaf7aac264 100644
--- a/spec/lib/gitlab/git_spec.rb
+++ b/spec/lib/gitlab/git_spec.rb
@@ -19,7 +19,7 @@ describe Gitlab::Git, lib: true do
describe 'committer_hash' do
it "returns a hash containing the given email and name" do
- committer_hash = Gitlab::Git::committer_hash(email: committer_email, name: committer_name)
+ committer_hash = Gitlab::Git.committer_hash(email: committer_email, name: committer_name)
expect(committer_hash[:email]).to eq(committer_email)
expect(committer_hash[:name]).to eq(committer_name)
@@ -28,7 +28,7 @@ describe Gitlab::Git, lib: true do
context 'when email is nil' do
it "returns nil" do
- committer_hash = Gitlab::Git::committer_hash(email: nil, name: committer_name)
+ committer_hash = Gitlab::Git.committer_hash(email: nil, name: committer_name)
expect(committer_hash).to be_nil
end
@@ -36,7 +36,7 @@ describe Gitlab::Git, lib: true do
context 'when name is nil' do
it "returns nil" do
- committer_hash = Gitlab::Git::committer_hash(email: committer_email, name: nil)
+ committer_hash = Gitlab::Git.committer_hash(email: committer_email, name: nil)
expect(committer_hash).to be_nil
end
diff --git a/spec/lib/gitlab/github_import/comment_formatter_spec.rb b/spec/lib/gitlab/github_import/comment_formatter_spec.rb
index e6e33d3686a..cc38872e426 100644
--- a/spec/lib/gitlab/github_import/comment_formatter_spec.rb
+++ b/spec/lib/gitlab/github_import/comment_formatter_spec.rb
@@ -1,8 +1,9 @@
require 'spec_helper'
describe Gitlab::GithubImport::CommentFormatter, lib: true do
+ let(:client) { double }
let(:project) { create(:empty_project) }
- let(:octocat) { double(id: 123456, login: 'octocat') }
+ let(:octocat) { double(id: 123456, login: 'octocat', email: 'octocat@example.com') }
let(:created_at) { DateTime.strptime('2013-04-10T20:09:31Z') }
let(:updated_at) { DateTime.strptime('2014-03-03T18:58:10Z') }
let(:base) do
@@ -16,7 +17,11 @@ describe Gitlab::GithubImport::CommentFormatter, lib: true do
}
end
- subject(:comment) { described_class.new(project, raw)}
+ subject(:comment) { described_class.new(project, raw, client) }
+
+ before do
+ allow(client).to receive(:user).and_return(octocat)
+ end
describe '#attributes' do
context 'when do not reference a portion of the diff' do
@@ -69,8 +74,15 @@ describe Gitlab::GithubImport::CommentFormatter, lib: true do
context 'when author is a GitLab user' do
let(:raw) { double(base.merge(user: octocat)) }
- it 'returns GitLab user id as author_id' do
+ it 'returns GitLab user id associated with GitHub id as author_id' do
gl_user = create(:omniauth_user, extern_uid: octocat.id, provider: 'github')
+
+ expect(comment.attributes.fetch(:author_id)).to eq gl_user.id
+ end
+
+ it 'returns GitLab user id associated with GitHub email as author_id' do
+ gl_user = create(:user, email: octocat.email)
+
expect(comment.attributes.fetch(:author_id)).to eq gl_user.id
end
diff --git a/spec/lib/gitlab/github_import/importer_spec.rb b/spec/lib/gitlab/github_import/importer_spec.rb
index afd78abdc9b..33d83d6d2f1 100644
--- a/spec/lib/gitlab/github_import/importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer_spec.rb
@@ -44,6 +44,7 @@ describe Gitlab::GithubImport::Importer, lib: true do
allow_any_instance_of(Octokit::Client).to receive(:rate_limit!).and_raise(Octokit::NotFound)
allow_any_instance_of(Gitlab::Shell).to receive(:import_repository).and_raise(Gitlab::Shell::Error)
+ allow_any_instance_of(Octokit::Client).to receive(:user).and_return(octocat)
allow_any_instance_of(Octokit::Client).to receive(:labels).and_return([label1, label2])
allow_any_instance_of(Octokit::Client).to receive(:milestones).and_return([milestone, milestone])
allow_any_instance_of(Octokit::Client).to receive(:issues).and_return([issue1, issue2])
@@ -53,7 +54,8 @@ describe Gitlab::GithubImport::Importer, lib: true do
allow_any_instance_of(Octokit::Client).to receive(:last_response).and_return(double(rels: { next: nil }))
allow_any_instance_of(Octokit::Client).to receive(:releases).and_return([release1, release2])
end
- let(:octocat) { double(id: 123456, login: 'octocat') }
+
+ let(:octocat) { double(id: 123456, login: 'octocat', email: 'octocat@example.com') }
let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') }
let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') }
let(:label1) do
@@ -125,6 +127,7 @@ describe Gitlab::GithubImport::Importer, lib: true do
)
end
+ let!(:user) { create(:user, email: octocat.email) }
let(:repository) { double(id: 1, fork: false) }
let(:source_sha) { create(:commit, project: project).id }
let(:source_branch) { double(ref: 'feature', repo: repository, sha: source_sha) }
diff --git a/spec/lib/gitlab/github_import/issue_formatter_spec.rb b/spec/lib/gitlab/github_import/issue_formatter_spec.rb
index eec1fabab54..f34d09f2c1d 100644
--- a/spec/lib/gitlab/github_import/issue_formatter_spec.rb
+++ b/spec/lib/gitlab/github_import/issue_formatter_spec.rb
@@ -1,8 +1,9 @@
require 'spec_helper'
describe Gitlab::GithubImport::IssueFormatter, lib: true do
+ let(:client) { double }
let!(:project) { create(:empty_project, namespace: create(:namespace, path: 'octocat')) }
- let(:octocat) { double(id: 123456, login: 'octocat') }
+ let(:octocat) { double(id: 123456, login: 'octocat', email: 'octocat@example.com') }
let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') }
let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') }
@@ -23,7 +24,11 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do
}
end
- subject(:issue) { described_class.new(project, raw_data) }
+ subject(:issue) { described_class.new(project, raw_data, client) }
+
+ before do
+ allow(client).to receive(:user).and_return(octocat)
+ end
shared_examples 'Gitlab::GithubImport::IssueFormatter#attributes' do
context 'when issue is open' do
@@ -75,11 +80,17 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do
expect(issue.attributes.fetch(:assignee_id)).to be_nil
end
- it 'returns GitLab user id as assignee_id when is a GitLab user' do
+ it 'returns GitLab user id associated with GitHub id as assignee_id' do
gl_user = create(:omniauth_user, extern_uid: octocat.id, provider: 'github')
expect(issue.attributes.fetch(:assignee_id)).to eq gl_user.id
end
+
+ it 'returns GitLab user id associated with GitHub email as assignee_id' do
+ gl_user = create(:user, email: octocat.email)
+
+ expect(issue.attributes.fetch(:assignee_id)).to eq gl_user.id
+ end
end
context 'when it has a milestone' do
@@ -100,16 +111,22 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do
context 'when author is a GitLab user' do
let(:raw_data) { double(base_data.merge(user: octocat)) }
- it 'returns project#creator_id as author_id when is not a GitLab user' do
+ it 'returns project creator_id as author_id when is not a GitLab user' do
expect(issue.attributes.fetch(:author_id)).to eq project.creator_id
end
- it 'returns GitLab user id as author_id when is a GitLab user' do
+ it 'returns GitLab user id associated with GitHub id as author_id' do
gl_user = create(:omniauth_user, extern_uid: octocat.id, provider: 'github')
expect(issue.attributes.fetch(:author_id)).to eq gl_user.id
end
+ it 'returns GitLab user id associated with GitHub email as author_id' do
+ gl_user = create(:user, email: octocat.email)
+
+ expect(issue.attributes.fetch(:author_id)).to eq gl_user.id
+ end
+
it 'returns description without created at tag line' do
create(:omniauth_user, extern_uid: octocat.id, provider: 'github')
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 90947ff4707..e46be18aa99 100644
--- a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb
+++ b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb
@@ -1,6 +1,7 @@
require 'spec_helper'
describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
+ let(:client) { double }
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 }
@@ -10,7 +11,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
let(:target_repo) { repository }
let(:target_branch) { double(ref: 'master', repo: target_repo, sha: target_sha) }
let(:removed_branch) { double(ref: 'removed-branch', repo: source_repo, sha: '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b') }
- let(:octocat) { double(id: 123456, login: 'octocat') }
+ let(:octocat) { double(id: 123456, login: 'octocat', email: 'octocat@example.com') }
let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') }
let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') }
let(:base_data) do
@@ -32,7 +33,11 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
}
end
- subject(:pull_request) { described_class.new(project, raw_data) }
+ subject(:pull_request) { described_class.new(project, raw_data, client) }
+
+ before do
+ allow(client).to receive(:user).and_return(octocat)
+ end
shared_examples 'Gitlab::GithubImport::PullRequestFormatter#attributes' do
context 'when pull request is open' do
@@ -121,26 +126,38 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
expect(pull_request.attributes.fetch(:assignee_id)).to be_nil
end
- it 'returns GitLab user id as assignee_id when is a GitLab user' do
+ it 'returns GitLab user id associated with GitHub id as assignee_id' do
gl_user = create(:omniauth_user, extern_uid: octocat.id, provider: 'github')
expect(pull_request.attributes.fetch(:assignee_id)).to eq gl_user.id
end
+
+ it 'returns GitLab user id associated with GitHub email as assignee_id' do
+ gl_user = create(:user, email: octocat.email)
+
+ expect(pull_request.attributes.fetch(:assignee_id)).to eq gl_user.id
+ end
end
context 'when author is a GitLab user' do
let(:raw_data) { double(base_data.merge(user: octocat)) }
- it 'returns project#creator_id as author_id when is not a GitLab user' do
+ it 'returns project creator_id as author_id when is not a GitLab user' do
expect(pull_request.attributes.fetch(:author_id)).to eq project.creator_id
end
- it 'returns GitLab user id as author_id when is a GitLab user' do
+ it 'returns GitLab user id associated with GitHub id as author_id' do
gl_user = create(:omniauth_user, extern_uid: octocat.id, provider: 'github')
expect(pull_request.attributes.fetch(:author_id)).to eq gl_user.id
end
+ it 'returns GitLab user id associated with GitHub email as author_id' do
+ gl_user = create(:user, email: octocat.email)
+
+ expect(pull_request.attributes.fetch(:author_id)).to eq gl_user.id
+ end
+
it 'returns description without created at tag line' do
create(:omniauth_user, extern_uid: octocat.id, provider: 'github')
diff --git a/spec/lib/gitlab/github_import/user_formatter_spec.rb b/spec/lib/gitlab/github_import/user_formatter_spec.rb
new file mode 100644
index 00000000000..db792233657
--- /dev/null
+++ b/spec/lib/gitlab/github_import/user_formatter_spec.rb
@@ -0,0 +1,39 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::UserFormatter, lib: true do
+ let(:client) { double }
+ let(:octocat) { double(id: 123456, login: 'octocat', email: 'octocat@example.com') }
+
+ subject(:user) { described_class.new(client, octocat) }
+
+ before do
+ allow(client).to receive(:user).and_return(octocat)
+ end
+
+ describe '#gitlab_id' do
+ context 'when GitHub user is a GitLab user' do
+ it 'return GitLab user id when user associated their account with GitHub' do
+ gl_user = create(:omniauth_user, extern_uid: octocat.id, provider: 'github')
+
+ expect(user.gitlab_id).to eq gl_user.id
+ end
+
+ it 'returns GitLab user id when user primary email matches GitHub email' do
+ gl_user = create(:user, email: octocat.email)
+
+ expect(user.gitlab_id).to eq gl_user.id
+ end
+
+ it 'returns GitLab user id when any of user linked emails matches GitHub email' do
+ gl_user = create(:user, email: 'johndoe@example.com')
+ create(:email, user: gl_user, email: octocat.email)
+
+ expect(user.gitlab_id).to eq gl_user.id
+ end
+ end
+
+ it 'returns nil when GitHub user is not a GitLab user' do
+ expect(user.gitlab_id).to be_nil
+ end
+ end
+end
diff --git a/spec/lib/gitlab/import_export/attribute_configuration_spec.rb b/spec/lib/gitlab/import_export/attribute_configuration_spec.rb
index ea65a5dfed1..e24d070706a 100644
--- a/spec/lib/gitlab/import_export/attribute_configuration_spec.rb
+++ b/spec/lib/gitlab/import_export/attribute_configuration_spec.rb
@@ -17,7 +17,7 @@ describe 'Import/Export attribute configuration', lib: true do
# Remove duplicated or add missing models
# - project is not part of the tree, so it has to be added manually.
# - milestone, labels have both singular and plural versions in the tree, so remove the duplicates.
- names.flatten.uniq - ['milestones', 'labels'] + ['project']
+ names.flatten.uniq - %w(milestones labels) + ['project']
end
let(:safe_attributes_file) { 'spec/lib/gitlab/import_export/safe_model_attributes.yml' }
diff --git a/spec/lib/gitlab/import_export/avatar_saver_spec.rb b/spec/lib/gitlab/import_export/avatar_saver_spec.rb
index d6ee94442cb..579a31ead58 100644
--- a/spec/lib/gitlab/import_export/avatar_saver_spec.rb
+++ b/spec/lib/gitlab/import_export/avatar_saver_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe Gitlab::ImportExport::AvatarSaver, lib: true do
let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: 'test') }
- let(:export_path) { "#{Dir::tmpdir}/project_tree_saver_spec" }
+ let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" }
let(:project_with_avatar) { create(:empty_project, avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png")) }
let(:project) { create(:empty_project) }
diff --git a/spec/lib/gitlab/import_export/file_importer_spec.rb b/spec/lib/gitlab/import_export/file_importer_spec.rb
index a88ddd17aca..b88b9c18c15 100644
--- a/spec/lib/gitlab/import_export/file_importer_spec.rb
+++ b/spec/lib/gitlab/import_export/file_importer_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe Gitlab::ImportExport::FileImporter, lib: true do
let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: 'test') }
- let(:export_path) { "#{Dir::tmpdir}/file_importer_spec" }
+ let(:export_path) { "#{Dir.tmpdir}/file_importer_spec" }
let(:valid_file) { "#{shared.export_path}/valid.json" }
let(:symlink_file) { "#{shared.export_path}/invalid.json" }
let(:subfolder_symlink_file) { "#{shared.export_path}/subfolder/invalid.json" }
diff --git a/spec/lib/gitlab/import_export/import_export_spec.rb b/spec/lib/gitlab/import_export/import_export_spec.rb
index 20743811dab..f3fd0d82875 100644
--- a/spec/lib/gitlab/import_export/import_export_spec.rb
+++ b/spec/lib/gitlab/import_export/import_export_spec.rb
@@ -10,7 +10,7 @@ describe Gitlab::ImportExport, services: true do
end
it 'contains the namespace path' do
- expect(described_class.export_filename(project: project)).to include(project.namespace.full_path)
+ expect(described_class.export_filename(project: project)).to include(project.namespace.full_path.tr('/', '_'))
end
it 'does not go over a certain length' do
diff --git a/spec/lib/gitlab/import_export/model_configuration_spec.rb b/spec/lib/gitlab/import_export/model_configuration_spec.rb
index 9b492d1b9c7..2ede5cdd2ad 100644
--- a/spec/lib/gitlab/import_export/model_configuration_spec.rb
+++ b/spec/lib/gitlab/import_export/model_configuration_spec.rb
@@ -14,7 +14,7 @@ describe 'Import/Export model configuration', lib: true do
# - project is not part of the tree, so it has to be added manually.
# - milestone, labels have both singular and plural versions in the tree, so remove the duplicates.
# - User, Author... Models we do not care about for checking models
- names.flatten.uniq - ['milestones', 'labels', 'user', 'author'] + ['project']
+ names.flatten.uniq - %w(milestones labels user author) + ['project']
end
let(:all_models_yml) { 'spec/lib/gitlab/import_export/all_models.yml' }
diff --git a/spec/lib/gitlab/import_export/project.light.json b/spec/lib/gitlab/import_export/project.light.json
new file mode 100644
index 00000000000..a78836c3c34
--- /dev/null
+++ b/spec/lib/gitlab/import_export/project.light.json
@@ -0,0 +1,48 @@
+{
+ "description": "Nisi et repellendus ut enim quo accusamus vel magnam.",
+ "visibility_level": 10,
+ "archived": false,
+ "labels": [
+ {
+ "id": 2,
+ "title": "test2",
+ "color": "#428bca",
+ "project_id": 8,
+ "created_at": "2016-07-22T08:55:44.161Z",
+ "updated_at": "2016-07-22T08:55:44.161Z",
+ "template": false,
+ "description": "",
+ "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"
+ }
+ ]
+ }
+ ],
+ "snippets": [
+
+ ],
+ "hooks": [
+
+ ]
+} \ No newline at end of file
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 0af13ba8e47..f4a21c24fa1 100644
--- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
@@ -3,24 +3,24 @@ include ImportExport::CommonUtil
describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do
describe 'restore project tree' 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, :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 }
+ before(:context) do
+ @user = create(:user)
- before do
- allow(shared).to receive(:export_path).and_return('spec/lib/gitlab/import_export/')
+ RSpec::Mocks.with_temporary_scope do
+ @shared = Gitlab::ImportExport::Shared.new(relative_path: "", project_path: 'path')
+ allow(@shared).to receive(:export_path).and_return('spec/lib/gitlab/import_export/')
+ @project = create(:empty_project, :builds_disabled, :issues_disabled, name: 'project', path: 'project')
+ project_tree_restorer = described_class.new(user: @user, shared: @shared, project: @project)
+ @restored_project_json = project_tree_restorer.restore
+ end
end
context 'JSON' do
it 'restores models based on JSON' do
- expect(restored_project_json).to be true
+ expect(@restored_project_json).to be true
end
it 'restore correct project features' do
- restored_project_json
project = Project.find_by_path('project')
expect(project.project_feature.issues_access_level).to eq(ProjectFeature::DISABLED)
@@ -31,62 +31,42 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do
end
it 'has the same label associated to two issues' do
- restored_project_json
-
expect(ProjectLabel.find_by_title('test2').issues.count).to eq(2)
end
it 'has milestones associated to two separate issues' do
- restored_project_json
-
expect(Milestone.find_by_description('test milestone').issues.count).to eq(2)
end
it 'creates a valid pipeline note' do
- restored_project_json
-
expect(Ci::Pipeline.first.notes).not_to be_empty
end
it 'restores pipelines with missing ref' do
- restored_project_json
-
expect(Ci::Pipeline.where(ref: nil)).not_to be_empty
end
it 'restores the correct event with symbolised data' do
- restored_project_json
-
expect(Event.where.not(data: nil).first.data[:ref]).not_to be_empty
end
it 'preserves updated_at on issues' do
- restored_project_json
-
issue = Issue.where(description: 'Aliquam enim illo et possimus.').first
expect(issue.reload.updated_at.to_s).to eq('2016-06-14 15:02:47 UTC')
end
it 'contains the merge access levels on a protected branch' do
- restored_project_json
-
expect(ProtectedBranch.first.merge_access_levels).not_to be_empty
end
it 'contains the push access levels on a protected branch' do
- restored_project_json
-
expect(ProtectedBranch.first.push_access_levels).not_to be_empty
end
context 'event at forth level of the tree' do
let(:event) { Event.where(title: 'test levels').first }
- before do
- restored_project_json
- end
-
it 'restores the event' do
expect(event).not_to be_nil
end
@@ -99,77 +79,40 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do
it 'has the correct data for merge request st_diffs' do
# makes sure we are renaming the custom method +utf8_st_diffs+ into +st_diffs+
- expect { restored_project_json }.to change(MergeRequestDiff.where.not(st_diffs: nil), :count).by(9)
+ expect(MergeRequestDiff.where.not(st_diffs: nil).count).to eq(9)
end
it 'has labels associated to label links, associated to issues' do
- restored_project_json
-
expect(Label.first.label_links.first.target).not_to be_nil
end
it 'has project labels' do
- restored_project_json
-
expect(ProjectLabel.count).to eq(2)
end
it 'has no group labels' do
- restored_project_json
-
expect(GroupLabel.count).to eq(0)
end
- context 'with group' do
- let!(:project) do
- create(:empty_project,
- :builds_disabled,
- :issues_disabled,
- name: 'project',
- path: 'project',
- group: create(:group))
- end
-
- it 'has group labels' do
- restored_project_json
-
- expect(GroupLabel.count).to eq(1)
- end
-
- it 'has label priorities' do
- restored_project_json
-
- expect(GroupLabel.first.priorities).not_to be_empty
- end
- end
-
it 'has a project feature' do
- restored_project_json
-
- expect(project.project_feature).not_to be_nil
+ expect(@project.project_feature).not_to be_nil
end
it 'restores the correct service' do
- restored_project_json
-
expect(CustomIssueTrackerService.first).not_to be_nil
end
context 'Merge requests' do
- before do
- restored_project_json
- end
-
it 'always has the new project as a target' do
- expect(MergeRequest.find_by_title('MR1').target_project).to eq(project)
+ expect(MergeRequest.find_by_title('MR1').target_project).to eq(@project)
end
it 'has the same source project as originally if source/target are the same' do
- expect(MergeRequest.find_by_title('MR1').source_project).to eq(project)
+ expect(MergeRequest.find_by_title('MR1').source_project).to eq(@project)
end
it 'has the new project as target if source/target differ' do
- expect(MergeRequest.find_by_title('MR2').target_project).to eq(project)
+ expect(MergeRequest.find_by_title('MR2').target_project).to eq(@project)
end
it 'has no source if source/target differ' do
@@ -177,39 +120,71 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do
end
end
- context 'project.json file access check' do
- it 'does not read a symlink' do
- Dir.mktmpdir do |tmpdir|
- setup_symlink(tmpdir, 'project.json')
- allow(shared).to receive(:export_path).and_call_original
-
- restored_project_json
+ context 'tokens are regenerated' do
+ it 'has a new CI trigger token' do
+ expect(Ci::Trigger.where(token: 'cdbfasdf44a5958c83654733449e585')).to be_empty
+ end
- expect(shared.errors.first).not_to include('test')
- end
+ it 'has a new CI build token' do
+ expect(Ci::Build.where(token: 'abcd')).to be_empty
end
end
+ end
+ end
- context 'when there is an existing build with build token' do
- it 'restores project json correctly' do
- create(:ci_build, token: 'abcd')
+ context 'Light JSON' do
+ let(:user) { create(:user) }
+ let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: "", project_path: 'path') }
+ 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 }
- expect(restored_project_json).to be true
- end
- end
+ before do
+ allow(ImportExport).to receive(:project_filename).and_return('project.light.json')
+ allow(shared).to receive(:export_path).and_return('spec/lib/gitlab/import_export/')
+ end
+
+ context 'project.json file access check' do
+ it 'does not read a symlink' do
+ Dir.mktmpdir do |tmpdir|
+ setup_symlink(tmpdir, 'project.json')
+ allow(shared).to receive(:export_path).and_call_original
- 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
+ expect(shared.errors.first).not_to include('test')
end
+ end
+ end
- it 'has a new CI build token' do
- expect(Ci::Build.where(token: 'abcd')).to be_empty
- end
+ context 'when there is an existing build with build token' do
+ it 'restores project json correctly' do
+ create(:ci_build, token: 'abcd')
+
+ expect(restored_project_json).to be true
+ end
+ end
+
+ context 'with group' do
+ let!(:project) do
+ create(:empty_project,
+ :builds_disabled,
+ :issues_disabled,
+ name: 'project',
+ path: 'project',
+ group: create(:group))
+ end
+
+ before do
+ restored_project_json
+ end
+
+ it 'has group labels' do
+ expect(GroupLabel.count).to eq(1)
+ end
+
+ it 'has label priorities' do
+ expect(GroupLabel.first.priorities).not_to be_empty
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 3628adefc0c..012c22ec5ad 100644
--- a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
@@ -4,7 +4,7 @@ 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, current_user: user, shared: shared) }
- let(:export_path) { "#{Dir::tmpdir}/project_tree_saver_spec" }
+ let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" }
let(:user) { create(:user) }
let(:project) { setup_project }
@@ -114,7 +114,7 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do
it 'has project and group labels' do
label_types = saved_project_json['issues'].first['label_links'].map { |link| link['label']['type'] }
- expect(label_types).to match_array(['ProjectLabel', 'GroupLabel'])
+ expect(label_types).to match_array(%w(ProjectLabel GroupLabel))
end
it 'has priorities associated to labels' do
diff --git a/spec/lib/gitlab/import_export/repo_bundler_spec.rb b/spec/lib/gitlab/import_export/repo_bundler_spec.rb
index d39ea60ff7f..a7f4e11271e 100644
--- a/spec/lib/gitlab/import_export/repo_bundler_spec.rb
+++ b/spec/lib/gitlab/import_export/repo_bundler_spec.rb
@@ -4,7 +4,7 @@ describe Gitlab::ImportExport::RepoSaver, services: true do
describe 'bundle a project Git repo' do
let(:user) { create(:user) }
let!(:project) { create(:empty_project, :public, name: 'searchable_project') }
- let(:export_path) { "#{Dir::tmpdir}/project_tree_saver_spec" }
+ 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/repo_restorer_spec.rb b/spec/lib/gitlab/import_export/repo_restorer_spec.rb
new file mode 100644
index 00000000000..168a59e5139
--- /dev/null
+++ b/spec/lib/gitlab/import_export/repo_restorer_spec.rb
@@ -0,0 +1,40 @@
+require 'spec_helper'
+
+describe Gitlab::ImportExport::RepoRestorer, services: true do
+ describe 'bundle a project Git repo' do
+ let(:user) { create(:user) }
+ let!(:project_with_repo) { create(:project, :test_repo, name: 'test-repo-restorer', path: 'test-repo-restorer') }
+ let!(:project) { create(:empty_project) }
+ let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" }
+ let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: project.path_with_namespace) }
+ let(:bundler) { Gitlab::ImportExport::RepoSaver.new(project: project_with_repo, shared: shared) }
+ let(:bundle_path) { File.join(shared.export_path, Gitlab::ImportExport.project_bundle_filename) }
+ let(:restorer) do
+ described_class.new(path_to_bundle: bundle_path,
+ shared: shared,
+ project: project)
+ end
+
+ before do
+ allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
+
+ bundler.save
+ end
+
+ after do
+ FileUtils.rm_rf(export_path)
+ FileUtils.rm_rf(project_with_repo.repository.path_to_repo)
+ FileUtils.rm_rf(project.repository.path_to_repo)
+ end
+
+ it 'restores the repo successfully' do
+ expect(restorer.restore).to be true
+ end
+
+ it 'has the webhooks' do
+ restorer.restore
+
+ expect(Gitlab::Git::Hook.new('post-receive', project.repository.path_to_repo)).to exist
+ end
+ end
+end
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 47d5d2fc150..071e5fac3f0 100644
--- a/spec/lib/gitlab/import_export/wiki_repo_bundler_spec.rb
+++ b/spec/lib/gitlab/import_export/wiki_repo_bundler_spec.rb
@@ -4,7 +4,7 @@ describe Gitlab::ImportExport::WikiRepoSaver, services: true do
describe 'bundle a wiki Git repo' do
let(:user) { create(:user) }
let!(:project) { create(:empty_project, :public, name: 'searchable_project') }
- let(:export_path) { "#{Dir::tmpdir}/project_tree_saver_spec" }
+ 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) }
let!(:project_wiki) { ProjectWiki.new(project, user) }
diff --git a/spec/lib/gitlab/import_sources_spec.rb b/spec/lib/gitlab/import_sources_spec.rb
index 8cea38e9ff8..b3b5e5e7e33 100644
--- a/spec/lib/gitlab/import_sources_spec.rb
+++ b/spec/lib/gitlab/import_sources_spec.rb
@@ -22,16 +22,16 @@ describe Gitlab::ImportSources do
describe '.values' do
it 'returns an array' do
expected =
- [
- 'github',
- 'bitbucket',
- 'gitlab',
- 'google_code',
- 'fogbugz',
- 'git',
- 'gitlab_project',
- 'gitea'
- ]
+ %w(
+ github
+ bitbucket
+ gitlab
+ google_code
+ fogbugz
+ git
+ gitlab_project
+ gitea
+ )
expect(described_class.values).to eq(expected)
end
@@ -40,15 +40,15 @@ describe Gitlab::ImportSources do
describe '.importer_names' do
it 'returns an array of importer names' do
expected =
- [
- 'github',
- 'bitbucket',
- 'gitlab',
- 'google_code',
- 'fogbugz',
- 'gitlab_project',
- 'gitea'
- ]
+ %w(
+ github
+ bitbucket
+ gitlab
+ google_code
+ fogbugz
+ gitlab_project
+ gitea
+ )
expect(described_class.importer_names).to eq(expected)
end
diff --git a/spec/lib/gitlab/kubernetes_spec.rb b/spec/lib/gitlab/kubernetes_spec.rb
index c9bd52a3b8f..91f9d06b85a 100644
--- a/spec/lib/gitlab/kubernetes_spec.rb
+++ b/spec/lib/gitlab/kubernetes_spec.rb
@@ -9,7 +9,7 @@ describe Gitlab::Kubernetes do
let(:pod_name) { 'pod1' }
let(:container_name) { 'container1' }
- subject(:result) { URI::parse(container_exec_url(api_url, namespace, pod_name, container_name)) }
+ subject(:result) { URI.parse(container_exec_url(api_url, namespace, pod_name, container_name)) }
it { expect(result.scheme).to eq('wss') }
it { expect(result.host).to eq('example.com') }
diff --git a/spec/lib/gitlab/ldap/auth_hash_spec.rb b/spec/lib/gitlab/ldap/auth_hash_spec.rb
index 69c49051156..7a2f774b948 100644
--- a/spec/lib/gitlab/ldap/auth_hash_spec.rb
+++ b/spec/lib/gitlab/ldap/auth_hash_spec.rb
@@ -44,7 +44,7 @@ describe Gitlab::LDAP::AuthHash, lib: true do
context "with overridden attributes" do
let(:attributes) do
{
- 'username' => ['mail', 'email'],
+ 'username' => %w(mail email),
'name' => 'fullName'
}
end
diff --git a/spec/lib/gitlab/ldap/user_spec.rb b/spec/lib/gitlab/ldap/user_spec.rb
index 89790c9e1af..2f3bd4393b7 100644
--- a/spec/lib/gitlab/ldap/user_spec.rb
+++ b/spec/lib/gitlab/ldap/user_spec.rb
@@ -95,10 +95,10 @@ describe Gitlab::LDAP::User, lib: true do
it 'maintains an identity per provider' do
existing_user = create(:omniauth_user, email: 'john@example.com', provider: 'twitter')
- expect(existing_user.identities.count).to eql(1)
+ expect(existing_user.identities.count).to be(1)
ldap_user.save
- expect(ldap_user.gl_user.identities.count).to eql(2)
+ expect(ldap_user.gl_user.identities.count).to be(2)
# Expect that find_by provider only returns a single instance of an identity and not an Enumerable
expect(ldap_user.gl_user.identities.find_by(provider: 'twitter')).to be_instance_of Identity
diff --git a/spec/lib/gitlab/metrics/instrumentation_spec.rb b/spec/lib/gitlab/metrics/instrumentation_spec.rb
index d88bcae41fb..a986cb520fb 100644
--- a/spec/lib/gitlab/metrics/instrumentation_spec.rb
+++ b/spec/lib/gitlab/metrics/instrumentation_spec.rb
@@ -197,11 +197,13 @@ describe Gitlab::Metrics::Instrumentation do
@child1 = Class.new(@dummy) do
def self.child1_foo; end
+
def child1_bar; end
end
@child2 = Class.new(@child1) do
def self.child2_foo; end
+
def child2_bar; end
end
end
diff --git a/spec/lib/gitlab/metrics/method_call_spec.rb b/spec/lib/gitlab/metrics/method_call_spec.rb
index 8d05081eecb..a247f03b2da 100644
--- a/spec/lib/gitlab/metrics/method_call_spec.rb
+++ b/spec/lib/gitlab/metrics/method_call_spec.rb
@@ -23,7 +23,7 @@ describe Gitlab::Metrics::MethodCall do
expect(metric.values[:duration]).to be_a_kind_of(Numeric)
expect(metric.values[:cpu_duration]).to be_a_kind_of(Numeric)
- expect(metric.values[:call_count]).to an_instance_of(Fixnum)
+ expect(metric.values[:call_count]).to be_an(Integer)
expect(metric.tags).to eq({ method: 'Foo#bar' })
end
diff --git a/spec/lib/gitlab/metrics/metric_spec.rb b/spec/lib/gitlab/metrics/metric_spec.rb
index f26fca52c50..d240b8a01fd 100644
--- a/spec/lib/gitlab/metrics/metric_spec.rb
+++ b/spec/lib/gitlab/metrics/metric_spec.rb
@@ -62,7 +62,7 @@ describe Gitlab::Metrics::Metric do
end
it 'includes the timestamp' do
- expect(hash[:timestamp]).to be_an_instance_of(Fixnum)
+ expect(hash[:timestamp]).to be_an(Integer)
end
end
end
diff --git a/spec/lib/gitlab/metrics/system_spec.rb b/spec/lib/gitlab/metrics/system_spec.rb
index 9e2ea89a712..4d94d8705fb 100644
--- a/spec/lib/gitlab/metrics/system_spec.rb
+++ b/spec/lib/gitlab/metrics/system_spec.rb
@@ -29,19 +29,19 @@ describe Gitlab::Metrics::System do
describe '.cpu_time' do
it 'returns a Fixnum' do
- expect(described_class.cpu_time).to be_an_instance_of(Fixnum)
+ expect(described_class.cpu_time).to be_an(Integer)
end
end
describe '.real_time' do
it 'returns a Fixnum' do
- expect(described_class.real_time).to be_an_instance_of(Fixnum)
+ expect(described_class.real_time).to be_an(Integer)
end
end
describe '.monotonic_time' do
it 'returns a Fixnum' do
- expect(described_class.monotonic_time).to be_an_instance_of(Fixnum)
+ expect(described_class.monotonic_time).to be_an(Integer)
end
end
end
diff --git a/spec/lib/gitlab/metrics/transaction_spec.rb b/spec/lib/gitlab/metrics/transaction_spec.rb
index 3887c04c832..0c5a6246d85 100644
--- a/spec/lib/gitlab/metrics/transaction_spec.rb
+++ b/spec/lib/gitlab/metrics/transaction_spec.rb
@@ -134,7 +134,7 @@ describe Gitlab::Metrics::Transaction do
series: 'rails_transactions',
tags: { action: 'Foo#bar' },
values: { duration: 0.0, allocated_memory: a_kind_of(Numeric) },
- timestamp: an_instance_of(Fixnum)
+ timestamp: a_kind_of(Integer)
}
expect(Gitlab::Metrics).to receive(:submit_metrics).
@@ -151,7 +151,7 @@ describe Gitlab::Metrics::Transaction do
series: 'events',
tags: { event: :meow },
values: { count: 1 },
- timestamp: an_instance_of(Fixnum)
+ timestamp: a_kind_of(Integer)
}
expect(Gitlab::Metrics).to receive(:submit_metrics).
diff --git a/spec/lib/gitlab/o_auth/user_spec.rb b/spec/lib/gitlab/o_auth/user_spec.rb
index fc9e1cb430a..6c84a4c8b73 100644
--- a/spec/lib/gitlab/o_auth/user_spec.rb
+++ b/spec/lib/gitlab/o_auth/user_spec.rb
@@ -148,12 +148,14 @@ describe Gitlab::OAuth::User, lib: true do
expect(gl_user).to be_valid
expect(gl_user.username).to eql uid
expect(gl_user.email).to eql 'johndoe@example.com'
- expect(gl_user.identities.length).to eql 2
+ expect(gl_user.identities.length).to be 2
identities_as_hash = gl_user.identities.map { |id| { provider: id.provider, extern_uid: id.extern_uid } }
expect(identities_as_hash).to match_array(
- [ { provider: 'ldapmain', extern_uid: 'uid=user1,ou=People,dc=example' },
+ [
+ { provider: 'ldapmain', extern_uid: 'uid=user1,ou=People,dc=example' },
{ provider: 'twitter', extern_uid: uid }
- ])
+ ]
+ )
end
end
@@ -167,12 +169,14 @@ describe Gitlab::OAuth::User, lib: true do
expect(gl_user).to be_valid
expect(gl_user.username).to eql 'john'
expect(gl_user.email).to eql 'john@example.com'
- expect(gl_user.identities.length).to eql 2
+ expect(gl_user.identities.length).to be 2
identities_as_hash = gl_user.identities.map { |id| { provider: id.provider, extern_uid: id.extern_uid } }
expect(identities_as_hash).to match_array(
- [ { provider: 'ldapmain', extern_uid: 'uid=user1,ou=People,dc=example' },
+ [
+ { provider: 'ldapmain', extern_uid: 'uid=user1,ou=People,dc=example' },
{ provider: 'twitter', extern_uid: uid }
- ])
+ ]
+ )
end
end
diff --git a/spec/lib/gitlab/optimistic_locking_spec.rb b/spec/lib/gitlab/optimistic_locking_spec.rb
index 498dc514c8c..acce2be93f2 100644
--- a/spec/lib/gitlab/optimistic_locking_spec.rb
+++ b/spec/lib/gitlab/optimistic_locking_spec.rb
@@ -1,10 +1,10 @@
require 'spec_helper'
describe Gitlab::OptimisticLocking, lib: true do
- describe '#retry_lock' do
- let!(:pipeline) { create(:ci_pipeline) }
- let!(:pipeline2) { Ci::Pipeline.find(pipeline.id) }
+ let!(:pipeline) { create(:ci_pipeline) }
+ let!(:pipeline2) { Ci::Pipeline.find(pipeline.id) }
+ describe '#retry_lock' do
it 'does not reload object if state changes' do
expect(pipeline).not_to receive(:reload)
expect(pipeline).to receive(:succeed).and_call_original
@@ -36,4 +36,17 @@ describe Gitlab::OptimisticLocking, lib: true do
end.to raise_error(ActiveRecord::StaleObjectError)
end
end
+
+ describe '#retry_optimistic_lock' do
+ context 'when locking module is mixed in' do
+ let(:unlockable) do
+ Class.new.include(described_class).new
+ end
+
+ it 'is an alias for retry_lock' do
+ expect(unlockable.method(:retry_optimistic_lock))
+ .to eq unlockable.method(:retry_lock)
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/regex_spec.rb b/spec/lib/gitlab/regex_spec.rb
index 089ec4e2737..ba45e2d758c 100644
--- a/spec/lib/gitlab/regex_spec.rb
+++ b/spec/lib/gitlab/regex_spec.rb
@@ -51,8 +51,8 @@ describe Gitlab::Regex, lib: true do
it { is_expected.not_to match('foo-') }
end
- describe 'NAMESPACE_REF_REGEX_STR' do
- subject { %r{\A#{Gitlab::Regex::NAMESPACE_REF_REGEX_STR}\z} }
+ describe 'FULL_NAMESPACE_REGEX_STR' do
+ subject { %r{\A#{Gitlab::Regex::FULL_NAMESPACE_REGEX_STR}\z} }
it { is_expected.to match('gitlab.org') }
it { is_expected.to match('gitlab.org/gitlab-git') }
diff --git a/spec/lib/gitlab/saml/user_spec.rb b/spec/lib/gitlab/saml/user_spec.rb
index 02c139f1a0d..4f6ef3c10fc 100644
--- a/spec/lib/gitlab/saml/user_spec.rb
+++ b/spec/lib/gitlab/saml/user_spec.rb
@@ -155,11 +155,10 @@ describe Gitlab::Saml::User, lib: true do
expect(gl_user).to be_valid
expect(gl_user.username).to eql uid
expect(gl_user.email).to eql 'john@mail.com'
- expect(gl_user.identities.length).to eql 2
+ expect(gl_user.identities.length).to be 2
identities_as_hash = gl_user.identities.map { |id| { provider: id.provider, extern_uid: id.extern_uid } }
- expect(identities_as_hash).to match_array([ { provider: 'ldapmain', extern_uid: 'uid=user1,ou=People,dc=example' },
- { provider: 'saml', extern_uid: uid }
- ])
+ expect(identities_as_hash).to match_array([{ provider: 'ldapmain', extern_uid: 'uid=user1,ou=People,dc=example' },
+ { provider: 'saml', extern_uid: uid }])
end
end
@@ -178,11 +177,10 @@ describe Gitlab::Saml::User, lib: true do
expect(gl_user).to be_valid
expect(gl_user.username).to eql 'john'
expect(gl_user.email).to eql 'john@mail.com'
- expect(gl_user.identities.length).to eql 2
+ expect(gl_user.identities.length).to be 2
identities_as_hash = gl_user.identities.map { |id| { provider: id.provider, extern_uid: id.extern_uid } }
- expect(identities_as_hash).to match_array([ { provider: 'ldapmain', extern_uid: 'uid=user1,ou=People,dc=example' },
- { provider: 'saml', extern_uid: uid }
- ])
+ expect(identities_as_hash).to match_array([{ provider: 'ldapmain', extern_uid: 'uid=user1,ou=People,dc=example' },
+ { provider: 'saml', extern_uid: uid }])
end
it 'saves successfully on subsequent tries, when both identities are present' do
@@ -204,11 +202,10 @@ describe Gitlab::Saml::User, lib: true do
local_gl_user = local_saml_user.gl_user
expect(local_gl_user).to be_valid
- expect(local_gl_user.identities.length).to eql 2
+ expect(local_gl_user.identities.length).to be 2
identities_as_hash = local_gl_user.identities.map { |id| { provider: id.provider, extern_uid: id.extern_uid } }
- expect(identities_as_hash).to match_array([ { provider: 'ldapmain', extern_uid: 'uid=user1,ou=People,dc=example' },
- { provider: 'saml', extern_uid: 'uid=user1,ou=People,dc=example' }
- ])
+ expect(identities_as_hash).to match_array([{ provider: 'ldapmain', extern_uid: 'uid=user1,ou=People,dc=example' },
+ { provider: 'saml', extern_uid: 'uid=user1,ou=People,dc=example' }])
end
end
end
diff --git a/spec/lib/gitlab/serializer/ci/variables_spec.rb b/spec/lib/gitlab/serializer/ci/variables_spec.rb
index b810c68ea03..c4b7fda5dbb 100644
--- a/spec/lib/gitlab/serializer/ci/variables_spec.rb
+++ b/spec/lib/gitlab/serializer/ci/variables_spec.rb
@@ -13,6 +13,7 @@ describe Gitlab::Serializer::Ci::Variables do
it 'converts keys into strings' do
is_expected.to eq([
{ key: 'key', value: 'value', public: true },
- { key: 'wee', value: 1, public: false }])
+ { key: 'wee', value: 1, public: false }
+ ])
end
end
diff --git a/spec/lib/gitlab/slash_commands/extractor_spec.rb b/spec/lib/gitlab/slash_commands/extractor_spec.rb
index 1e4954c4af8..d7f77486b3e 100644
--- a/spec/lib/gitlab/slash_commands/extractor_spec.rb
+++ b/spec/lib/gitlab/slash_commands/extractor_spec.rb
@@ -81,6 +81,14 @@ describe Gitlab::SlashCommands::Extractor do
let(:original_msg) { "/assign @joe\nworld" }
let(:final_msg) { "world" }
end
+
+ it 'allows slash in command arguments' do
+ msg = "/assign @joe / @jane\nworld"
+ msg, commands = extractor.extract_commands(msg)
+
+ expect(commands).to eq [['assign', '@joe / @jane']]
+ expect(msg).to eq 'world'
+ end
end
context 'in the middle of content' do
diff --git a/spec/lib/gitlab/template/issue_template_spec.rb b/spec/lib/gitlab/template/issue_template_spec.rb
index 1335a2b8f35..9213ced7b19 100644
--- a/spec/lib/gitlab/template/issue_template_spec.rb
+++ b/spec/lib/gitlab/template/issue_template_spec.rb
@@ -11,7 +11,8 @@ describe Gitlab::Template::IssueTemplate do
create_template: {
user: user,
access: Gitlab::Access::MASTER,
- path: 'issue_templates' })
+ 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 320b870309a..77dd3079e22 100644
--- a/spec/lib/gitlab/template/merge_request_template_spec.rb
+++ b/spec/lib/gitlab/template/merge_request_template_spec.rb
@@ -11,7 +11,8 @@ describe Gitlab::Template::MergeRequestTemplate do
create_template: {
user: user,
access: Gitlab::Access::MASTER,
- path: 'merge_request_templates' })
+ path: 'merge_request_templates'
+ })
end
describe '.all' do
diff --git a/spec/lib/gitlab/themes_spec.rb b/spec/lib/gitlab/themes_spec.rb
deleted file mode 100644
index 7a140518dd2..00000000000
--- a/spec/lib/gitlab/themes_spec.rb
+++ /dev/null
@@ -1,48 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::Themes, lib: true do
- describe '.body_classes' do
- it 'returns a space-separated list of class names' do
- css = described_class.body_classes
-
- expect(css).to include('ui_graphite')
- expect(css).to include(' ui_charcoal ')
- expect(css).to include(' ui_blue')
- end
- end
-
- describe '.by_id' do
- it 'returns a Theme by its ID' do
- expect(described_class.by_id(1).name).to eq 'Graphite'
- expect(described_class.by_id(6).name).to eq 'Blue'
- end
- end
-
- describe '.default' do
- it 'returns the default application theme' do
- allow(described_class).to receive(:default_id).and_return(2)
- expect(described_class.default.id).to eq 2
- end
-
- it 'prevents an infinite loop when configuration default is invalid' do
- default = described_class::APPLICATION_DEFAULT
- themes = described_class::THEMES
-
- config = double(default_theme: 0).as_null_object
- allow(Gitlab).to receive(:config).and_return(config)
- expect(described_class.default.id).to eq default
-
- config = double(default_theme: themes.size + 5).as_null_object
- allow(Gitlab).to receive(:config).and_return(config)
- expect(described_class.default.id).to eq default
- end
- end
-
- describe '.each' do
- it 'passes the block to the THEMES Array' do
- ids = []
- described_class.each { |theme| ids << theme.id }
- expect(ids).not_to be_empty
- end
- end
-end
diff --git a/spec/lib/gitlab/upgrader_spec.rb b/spec/lib/gitlab/upgrader_spec.rb
index edadab043d7..fcfd8d58b70 100644
--- a/spec/lib/gitlab/upgrader_spec.rb
+++ b/spec/lib/gitlab/upgrader_spec.rb
@@ -32,7 +32,8 @@ describe Gitlab::Upgrader, lib: true do
'43af3e65a486a9237f29f56d96c3b3da59c24ae0 refs/tags/v7.11.2',
'dac18e7728013a77410e926a1e64225703754a2d refs/tags/v7.11.2^{}',
'0bf21fd4b46c980c26fd8c90a14b86a4d90cc950 refs/tags/v7.9.4',
- 'b10de29edbaff7219547dc506cb1468ee35065c3 refs/tags/v7.9.4^{}'])
+ 'b10de29edbaff7219547dc506cb1468ee35065c3 refs/tags/v7.9.4^{}'
+ ])
expect(upgrader.latest_version_raw).to eq("v7.11.2")
end
end
diff --git a/spec/lib/gitlab/utils_spec.rb b/spec/lib/gitlab/utils_spec.rb
index d5d87310874..56772409989 100644
--- a/spec/lib/gitlab/utils_spec.rb
+++ b/spec/lib/gitlab/utils_spec.rb
@@ -1,7 +1,5 @@
describe Gitlab::Utils, lib: true do
- def to_boolean(value)
- described_class.to_boolean(value)
- end
+ delegate :to_boolean, to: :described_class
describe '.to_boolean' do
it 'accepts booleans' do
diff --git a/spec/lib/mattermost/command_spec.rb b/spec/lib/mattermost/command_spec.rb
index 5ccf1100898..4b5938edeb9 100644
--- a/spec/lib/mattermost/command_spec.rb
+++ b/spec/lib/mattermost/command_spec.rb
@@ -13,8 +13,7 @@ describe Mattermost::Command do
describe '#create' do
let(:params) do
{ team_id: 'abc',
- trigger: 'gitlab'
- }
+ trigger: 'gitlab' }
end
subject { described_class.new(nil).create(params) }
@@ -24,7 +23,8 @@ describe Mattermost::Command do
stub_request(:post, 'http://mattermost.example.com/api/v3/teams/abc/commands/create').
with(body: {
team_id: 'abc',
- trigger: 'gitlab' }.to_json).
+ trigger: 'gitlab'
+ }.to_json).
to_return(
status: 200,
headers: { 'Content-Type' => 'application/json' },
diff --git a/spec/lib/mattermost/team_spec.rb b/spec/lib/mattermost/team_spec.rb
index 2d14be6bcc2..6a9c7cebfff 100644
--- a/spec/lib/mattermost/team_spec.rb
+++ b/spec/lib/mattermost/team_spec.rb
@@ -14,18 +14,19 @@ describe Mattermost::Team do
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 }]
+ "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
diff --git a/spec/migrations/migrate_process_commit_worker_jobs_spec.rb b/spec/migrations/migrate_process_commit_worker_jobs_spec.rb
index 6a93deb5412..b6d678bac18 100644
--- a/spec/migrations/migrate_process_commit_worker_jobs_spec.rb
+++ b/spec/migrations/migrate_process_commit_worker_jobs_spec.rb
@@ -62,7 +62,7 @@ describe MigrateProcessCommitWorkerJobs do
end
def pop_job
- JSON.load(Sidekiq.redis { |r| r.lpop('queue:process_commit') })
+ JSON.parse(Sidekiq.redis { |r| r.lpop('queue:process_commit') })
end
before do
@@ -198,7 +198,7 @@ describe MigrateProcessCommitWorkerJobs do
let(:job) do
migration.down
- JSON.load(Sidekiq.redis { |r| r.lpop('queue:process_commit') })
+ JSON.parse(Sidekiq.redis { |r| r.lpop('queue:process_commit') })
end
it 'includes the project ID' do
diff --git a/spec/models/abuse_report_spec.rb b/spec/models/abuse_report_spec.rb
index c4486a32082..4e71597521d 100644
--- a/spec/models/abuse_report_spec.rb
+++ b/spec/models/abuse_report_spec.rb
@@ -2,7 +2,7 @@ require 'rails_helper'
RSpec.describe AbuseReport, type: :model do
subject { create(:abuse_report) }
- let(:user) { create(:user) }
+ let(:user) { create(:admin) }
it { expect(subject).to be_valid }
diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb
index b950fcdd81a..01ca1584ed2 100644
--- a/spec/models/application_setting_spec.rb
+++ b/spec/models/application_setting_spec.rb
@@ -29,6 +29,40 @@ describe ApplicationSetting, models: true do
it { is_expected.not_to allow_value(['test']).for(:disabled_oauth_sign_in_sources) }
end
+ describe 'default_artifacts_expire_in' do
+ it 'sets an error if it cannot parse' do
+ setting.update(default_artifacts_expire_in: 'a')
+
+ expect_invalid
+ end
+
+ it 'sets an error if it is blank' do
+ setting.update(default_artifacts_expire_in: ' ')
+
+ expect_invalid
+ end
+
+ it 'sets the value if it is valid' do
+ setting.update(default_artifacts_expire_in: '30 days')
+
+ expect(setting).to be_valid
+ expect(setting.default_artifacts_expire_in).to eq('30 days')
+ end
+
+ it 'sets the value if it is 0' do
+ setting.update(default_artifacts_expire_in: '0')
+
+ expect(setting).to be_valid
+ expect(setting.default_artifacts_expire_in).to eq('0')
+ end
+
+ def expect_invalid
+ expect(setting).to be_invalid
+ expect(setting.errors.messages)
+ .to have_key(:default_artifacts_expire_in)
+ end
+ end
+
it { is_expected.to validate_presence_of(:max_attachment_size) }
it do
@@ -62,9 +96,9 @@ describe ApplicationSetting, models: true do
describe 'inclusion' do
it { is_expected.to allow_value('custom1').for(:repository_storages) }
- it { is_expected.to allow_value(['custom2', 'custom3']).for(:repository_storages) }
+ it { is_expected.to allow_value(%w(custom2 custom3)).for(:repository_storages) }
it { is_expected.not_to allow_value('alternative').for(:repository_storages) }
- it { is_expected.not_to allow_value(['alternative', 'custom1']).for(:repository_storages) }
+ it { is_expected.not_to allow_value(%w(alternative custom1)).for(:repository_storages) }
end
describe 'presence' do
@@ -83,7 +117,7 @@ describe ApplicationSetting, models: true do
describe '#repository_storage' do
it 'returns the first storage' do
- setting.repository_storages = ['good', 'bad']
+ setting.repository_storages = %w(good bad)
expect(setting.repository_storage).to eq('good')
end
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 4080092405d..b963ca4e542 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -1,6 +1,7 @@
require 'spec_helper'
describe Ci::Build, :models do
+ let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
let(:build) { create(:ci_build, pipeline: pipeline) }
let(:test_trace) { 'This is a test' }
@@ -161,11 +162,17 @@ describe Ci::Build, :models do
is_expected.to be_nil
end
- it 'when resseting value' do
+ it 'when resetting value' do
build.artifacts_expire_in = nil
is_expected.to be_nil
end
+
+ it 'when setting to 0' do
+ build.artifacts_expire_in = '0'
+
+ is_expected.to be_nil
+ end
end
describe '#commit' do
@@ -207,14 +214,16 @@ describe Ci::Build, :models do
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)
+ project.add_developer(user)
+
+ retried_rspec = Ci::Build.retry(rspec_test, user)
+
+ 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
@@ -813,12 +822,16 @@ describe Ci::Build, :models do
subject { build.other_actions }
+ before do
+ project.add_developer(user)
+ end
+
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) }
+ let!(:new_build) { Ci::Build.retry(build, user) }
it 'does not return any of them' do
is_expected.not_to include(build, new_build)
@@ -826,7 +839,7 @@ describe Ci::Build, :models do
end
context 'when other build is retried' do
- let!(:retried_build) { Ci::Build.retry(other_build) }
+ let!(:retried_build) { Ci::Build.retry(other_build, user) }
it 'returns a retried build' do
is_expected.to contain_exactly(retried_build)
@@ -857,21 +870,29 @@ describe Ci::Build, :models do
describe '#play' do
let(:build) { create(:ci_build, :manual, pipeline: pipeline) }
- subject { build.play }
+ before do
+ project.add_developer(user)
+ end
+
+ context 'when build is manual' do
+ it 'enqueues a build' do
+ new_build = build.play(user)
- it 'enqueues a build' do
- is_expected.to be_pending
- is_expected.to eq(build)
+ expect(new_build).to be_pending
+ expect(new_build).to eq(build)
+ end
end
- context 'for successful build' do
+ context 'when build is passed' 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)
+ new_build = build.play(user)
+
+ expect(new_build).to be_pending
+ expect(new_build).not_to eq(build)
end
end
end
@@ -1224,8 +1245,8 @@ describe Ci::Build, :models do
{ 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_PATH', value: project.full_path, public: true },
+ { key: 'CI_PROJECT_NAMESPACE', value: project.namespace.full_path, public: true },
{ key: 'CI_PROJECT_URL', value: project.web_url, public: true },
{ key: 'CI_PIPELINE_ID', value: pipeline.id.to_s, public: true }
]
@@ -1246,12 +1267,9 @@ describe Ci::Build, :models do
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 }
- ]
+ [{ key: 'GITLAB_USER_ID', value: user.id.to_s, public: true },
+ { key: 'GITLAB_USER_EMAIL', value: user.email, public: true }]
end
before do
@@ -1408,7 +1426,7 @@ describe Ci::Build, :models do
end
context 'when runner is assigned to build' do
- let(:runner) { create(:ci_runner, description: 'description', tag_list: ['docker', 'linux']) }
+ let(:runner) { create(:ci_runner, description: 'description', tag_list: %w(docker linux)) }
before do
build.update(runner: runner)
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 426be74cd02..c2fc8c02bb3 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -3,8 +3,12 @@ require 'spec_helper'
describe Ci::Pipeline, models: true do
include EmailHelpers
- let(:project) { FactoryGirl.create :empty_project }
- let(:pipeline) { FactoryGirl.create :ci_empty_pipeline, status: 'created', project: project }
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project) }
+
+ let(:pipeline) do
+ create(:ci_empty_pipeline, status: :created, project: project)
+ end
it { is_expected.to belong_to(:project) }
it { is_expected.to belong_to(:user) }
@@ -164,9 +168,9 @@ describe Ci::Pipeline, models: true do
end
it 'returns list of stages with correct statuses' do
- expect(statuses).to eq([['build', 'failed'],
- ['test', 'success'],
- ['deploy', 'running']])
+ expect(statuses).to eq([%w(build failed),
+ %w(test success),
+ %w(deploy running)])
end
context 'when commit status is retried' do
@@ -179,9 +183,9 @@ describe Ci::Pipeline, models: true do
end
it 'ignores the previous state' do
- expect(statuses).to eq([['build', 'success'],
- ['test', 'success'],
- ['deploy', 'running']])
+ expect(statuses).to eq([%w(build success),
+ %w(test success),
+ %w(deploy running)])
end
end
end
@@ -195,7 +199,7 @@ describe Ci::Pipeline, models: true do
describe '#stages_name' do
it 'returns a valid names of stages' do
- expect(pipeline.stages_name).to eq(['build', 'test', 'deploy'])
+ expect(pipeline.stages_name).to eq(%w(build test deploy))
end
end
end
@@ -503,7 +507,9 @@ describe Ci::Pipeline, models: true do
end
describe '#status' do
- let!(:build) { create(:ci_build, :created, pipeline: pipeline, name: 'test') }
+ let(:build) do
+ create(:ci_build, :created, pipeline: pipeline, name: 'test')
+ end
subject { pipeline.reload.status }
@@ -545,13 +551,21 @@ describe Ci::Pipeline, models: true do
build.cancel
end
- it { is_expected.to eq('canceled') }
+ context 'when build is pending' do
+ let(:build) do
+ create(:ci_build, :pending, pipeline: pipeline)
+ end
+
+ it { is_expected.to eq('canceled') }
+ end
end
context 'on failure and build retry' do
before do
build.drop
- Ci::Build.retry(build)
+ project.add_developer(user)
+
+ Ci::Build.retry(build, user)
end
# We are changing a state: created > failed > running
@@ -563,8 +577,6 @@ describe Ci::Pipeline, models: true do
end
describe '#detailed_status' do
- let(:user) { create(:user) }
-
subject { pipeline.detailed_status(user) }
context 'when pipeline is created' do
@@ -720,7 +732,7 @@ describe Ci::Pipeline, models: true do
describe '#cancel_running' do
let(:latest_status) { pipeline.statuses.pluck(:status) }
- context 'when there is a running external job and created build' do
+ context 'when there is a running external job and a regular job' do
before do
create(:ci_build, :running, pipeline: pipeline)
create(:generic_commit_status, :running, pipeline: pipeline)
@@ -733,7 +745,7 @@ describe Ci::Pipeline, models: true do
end
end
- context 'when builds are in different stages' do
+ context 'when jobs are in different stages' do
before do
create(:ci_build, :running, stage_idx: 0, pipeline: pipeline)
create(:ci_build, :running, stage_idx: 1, pipeline: pipeline)
@@ -745,17 +757,34 @@ describe Ci::Pipeline, models: true do
expect(latest_status).to contain_exactly('canceled', 'canceled')
end
end
+
+ context 'when there are created builds present in the pipeline' do
+ before do
+ create(:ci_build, :running, stage_idx: 0, pipeline: pipeline)
+ create(:ci_build, :created, stage_idx: 1, pipeline: pipeline)
+
+ pipeline.cancel_running
+ end
+
+ it 'cancels created builds' do
+ expect(latest_status).to eq %w(canceled canceled)
+ end
+ end
end
describe '#retry_failed' do
let(:latest_status) { pipeline.statuses.latest.pluck(:status) }
+ before do
+ project.add_developer(user)
+ end
+
context 'when there is a failed build and failed external status' do
before do
create(:ci_build, :failed, name: 'build', pipeline: pipeline)
create(:generic_commit_status, :failed, name: 'jenkins', pipeline: pipeline)
- pipeline.retry_failed(create(:user))
+ pipeline.retry_failed(user)
end
it 'retries only build' do
@@ -768,11 +797,11 @@ describe Ci::Pipeline, models: true do
create(:ci_build, :failed, name: 'build', stage_idx: 0, pipeline: pipeline)
create(:ci_build, :failed, name: 'jenkins', stage_idx: 1, pipeline: pipeline)
- pipeline.retry_failed(create(:user))
+ pipeline.retry_failed(user)
end
it 'retries both builds' do
- expect(latest_status).to contain_exactly('pending', 'pending')
+ expect(latest_status).to contain_exactly('pending', 'created')
end
end
@@ -781,11 +810,11 @@ describe Ci::Pipeline, models: true do
create(:ci_build, :failed, name: 'build', stage_idx: 0, pipeline: pipeline)
create(:ci_build, :canceled, name: 'jenkins', stage_idx: 1, pipeline: pipeline)
- pipeline.retry_failed(create(:user))
+ pipeline.retry_failed(user)
end
it 'retries both builds' do
- expect(latest_status).to contain_exactly('pending', 'pending')
+ expect(latest_status).to contain_exactly('pending', 'created')
end
end
end
diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb
index 3f32248e52b..76ce558eea0 100644
--- a/spec/models/ci/runner_spec.rb
+++ b/spec/models/ci/runner_spec.rb
@@ -113,7 +113,7 @@ describe Ci::Runner, models: true do
context 'when runner has tags' do
before do
- runner.tag_list = ['bb', 'cc']
+ runner.tag_list = %w(bb cc)
end
shared_examples 'tagged build picker' do
@@ -169,7 +169,7 @@ describe Ci::Runner, models: true do
context 'when having runner tags' do
before do
- runner.tag_list = ['bb', 'cc']
+ runner.tag_list = %w(bb cc)
end
it 'cannot handle it for builds without matching tags' do
@@ -189,7 +189,7 @@ describe Ci::Runner, models: true do
context 'when having runner tags' do
before do
- runner.tag_list = ['bb', 'cc']
+ runner.tag_list = %w(bb cc)
build.tag_list = ['bb']
end
@@ -212,7 +212,7 @@ describe Ci::Runner, models: true do
context 'when having runner tags' do
before do
- runner.tag_list = ['bb', 'cc']
+ runner.tag_list = %w(bb cc)
build.tag_list = ['bb']
end
@@ -290,7 +290,7 @@ describe Ci::Runner, models: true do
let!(:last_update) { runner.ensure_runner_queue_value }
before do
- runner.update(description: 'new runner')
+ Ci::UpdateRunnerService.new(runner).update(description: 'new runner')
end
it 'sets a new last_update value' do
@@ -318,6 +318,25 @@ describe Ci::Runner, models: true do
end
end
+ describe '#destroy' do
+ let(:runner) { create(:ci_runner) }
+
+ context 'when there is a tick in the queue' do
+ let!(:queue_key) { runner.send(:runner_queue_key) }
+
+ before do
+ runner.tick_runner_queue
+ runner.destroy
+ end
+
+ it 'cleans up the queue' do
+ Gitlab::Redis.with do |redis|
+ expect(redis.get(queue_key)).to be_nil
+ end
+ end
+ end
+ end
+
describe '.assignable_for' do
let(:runner) { create(:ci_runner) }
let(:project) { create(:empty_project) }
diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb
index bf4394f7d5b..36533bdd11e 100644
--- a/spec/models/commit_status_spec.rb
+++ b/spec/models/commit_status_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe CommitStatus, models: true do
+describe CommitStatus, :models do
let(:project) { create(:project, :repository) }
let(:pipeline) do
@@ -127,7 +127,7 @@ describe CommitStatus, models: true do
end
describe '.latest' do
- subject { CommitStatus.latest.order(:id) }
+ subject { described_class.latest.order(:id) }
let(:statuses) do
[create_status(name: 'aa', ref: 'bb', status: 'running'),
@@ -143,7 +143,7 @@ describe CommitStatus, models: true do
end
describe '.running_or_pending' do
- subject { CommitStatus.running_or_pending.order(:id) }
+ subject { described_class.running_or_pending.order(:id) }
let(:statuses) do
[create_status(name: 'aa', ref: 'bb', status: 'running'),
@@ -159,7 +159,21 @@ describe CommitStatus, models: true do
end
describe '.exclude_ignored' do
- subject { CommitStatus.exclude_ignored.order(:id) }
+ subject { described_class.after_stage(0) }
+
+ let(:statuses) do
+ [create_status(name: 'aa', stage_idx: 0),
+ create_status(name: 'cc', stage_idx: 1),
+ create_status(name: 'aa', stage_idx: 2)]
+ end
+
+ it 'returns statuses from second and third stage' do
+ is_expected.to eq(statuses.values_at(1, 2))
+ end
+ end
+
+ describe '.exclude_ignored' do
+ subject { described_class.exclude_ignored.order(:id) }
let(:statuses) do
[create_status(when: 'manual', status: 'skipped'),
diff --git a/spec/models/concerns/cache_markdown_field_spec.rb b/spec/models/concerns/cache_markdown_field_spec.rb
index 2e3702f7520..6151d53cd91 100644
--- a/spec/models/concerns/cache_markdown_field_spec.rb
+++ b/spec/models/concerns/cache_markdown_field_spec.rb
@@ -1,7 +1,8 @@
require 'spec_helper'
describe CacheMarkdownField do
- CacheMarkdownField::CACHING_CLASSES << "ThingWithMarkdownFields"
+ caching_classes = CacheMarkdownField::CACHING_CLASSES
+ CacheMarkdownField::CACHING_CLASSES = ["ThingWithMarkdownFields"].freeze
# The minimum necessary ActiveModel to test this concern
class ThingWithMarkdownFields
@@ -54,7 +55,7 @@ describe CacheMarkdownField do
end
end
- CacheMarkdownField::CACHING_CLASSES.delete("ThingWithMarkdownFields")
+ CacheMarkdownField::CACHING_CLASSES = caching_classes
def thing_subclass(new_attr)
Class.new(ThingWithMarkdownFields) { add_attr(new_attr) }
diff --git a/spec/models/concerns/routable_spec.rb b/spec/models/concerns/routable_spec.rb
index e008ec28fa4..677e60e1282 100644
--- a/spec/models/concerns/routable_spec.rb
+++ b/spec/models/concerns/routable_spec.rb
@@ -86,7 +86,7 @@ describe Group, 'Routable' do
let(:nested_group) { create(:group, parent: group) }
it { expect(group.full_path).to eq(group.path) }
- it { expect(nested_group.full_path).to eq("#{group.path}/#{nested_group.path}") }
+ it { expect(nested_group.full_path).to eq("#{group.full_path}/#{nested_group.path}") }
end
describe '#full_name' do
@@ -102,7 +102,7 @@ describe Project, 'Routable' do
describe '#full_path' do
let(:project) { build_stubbed(:empty_project) }
- it { expect(project.full_path).to eq "#{project.namespace.path}/#{project.path}" }
+ it { expect(project.full_path).to eq "#{project.namespace.full_path}/#{project.path}" }
end
describe '#full_name' do
diff --git a/spec/models/concerns/spammable_spec.rb b/spec/models/concerns/spammable_spec.rb
index 32935bc0b09..fd3b8307571 100644
--- a/spec/models/concerns/spammable_spec.rb
+++ b/spec/models/concerns/spammable_spec.rb
@@ -14,14 +14,16 @@ describe Issue, 'Spammable' do
end
describe 'InstanceMethods' do
+ let(:issue) { build(:issue, spam: true) }
+
it 'should be invalid if spam' do
- issue = build(:issue, spam: true)
expect(issue.valid?).to be_falsey
end
describe '#check_for_spam?' do
it 'returns true for public project' do
issue.project.update_attribute(:visibility_level, Gitlab::VisibilityLevel::PUBLIC)
+
expect(issue.check_for_spam?).to eq(true)
end
@@ -29,5 +31,20 @@ describe Issue, 'Spammable' do
expect(issue.check_for_spam?).to eq(false)
end
end
+
+ describe '#submittable_as_spam_by?' do
+ let(:admin) { build(:admin) }
+ let(:user) { build(:user) }
+
+ before do
+ allow(issue).to receive(:submittable_as_spam?).and_return(true)
+ end
+
+ it 'tests if the user can submit spam' do
+ expect(issue.submittable_as_spam_by?(admin)).to be(true)
+ expect(issue.submittable_as_spam_by?(user)).to be(false)
+ expect(issue.submittable_as_spam_by?(nil)).to be_nil
+ end
+ end
end
end
diff --git a/spec/models/concerns/uniquify_spec.rb b/spec/models/concerns/uniquify_spec.rb
new file mode 100644
index 00000000000..83187d732e4
--- /dev/null
+++ b/spec/models/concerns/uniquify_spec.rb
@@ -0,0 +1,33 @@
+require 'spec_helper'
+
+describe Uniquify, models: true do
+ let(:uniquify) { described_class.new }
+
+ describe "#string" do
+ it 'returns the given string if it does not exist' do
+ result = uniquify.string('test_string') { |s| false }
+
+ expect(result).to eq('test_string')
+ end
+
+ it 'returns the given string with a counter attached if the string exists' do
+ result = uniquify.string('test_string') { |s| s == 'test_string' }
+
+ expect(result).to eq('test_string1')
+ end
+
+ it 'increments the counter for each candidate string that also exists' do
+ result = uniquify.string('test_string') { |s| s == 'test_string' || s == 'test_string1' }
+
+ expect(result).to eq('test_string2')
+ end
+
+ it 'allows passing in a base function that defines the location of the counter' do
+ result = uniquify.string(-> (counter) { "test_#{counter}_string" }) do |s|
+ s == 'test__string'
+ end
+
+ expect(result).to eq('test_1_string')
+ end
+ end
+end
diff --git a/spec/models/cycle_analytics/production_spec.rb b/spec/models/cycle_analytics/production_spec.rb
index b9fe492fe2c..e6a826a9418 100644
--- a/spec/models/cycle_analytics/production_spec.rb
+++ b/spec/models/cycle_analytics/production_spec.rb
@@ -21,13 +21,12 @@ 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(
+ sha = context.project.repository.create_file(
context.user,
context.random_git_name,
'content',
message: 'commit message',
- branch_name: 'master',
- update: false)
+ branch_name: 'master')
context.project.repository.commit(sha)
context.deploy_master
diff --git a/spec/models/cycle_analytics/staging_spec.rb b/spec/models/cycle_analytics/staging_spec.rb
index 9a024d533a1..3a02ed81adb 100644
--- a/spec/models/cycle_analytics/staging_spec.rb
+++ b/spec/models/cycle_analytics/staging_spec.rb
@@ -18,7 +18,7 @@ describe 'CycleAnalytics#staging', feature: true do
start_time_conditions: [["merge request that closes issue is merged",
-> (context, data) do
context.merge_merge_requests_closing_issue(data[:issue])
- end ]],
+ end]],
end_time_conditions: [["merge request that closes issue is deployed to production",
-> (context, data) do
context.deploy_master
@@ -26,13 +26,12 @@ describe 'CycleAnalytics#staging', 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(
+ sha = context.project.repository.create_file(
context.user,
context.random_git_name,
'content',
message: 'commit message',
- branch_name: 'master',
- update: false)
+ branch_name: 'master')
context.project.repository.commit(sha)
context.deploy_master
diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb
index 960f29f3805..dce18f008f8 100644
--- a/spec/models/environment_spec.rb
+++ b/spec/models/environment_spec.rb
@@ -76,7 +76,8 @@ describe Environment, models: true do
end
describe '#update_merge_request_metrics?' do
- { 'production' => true,
+ {
+ 'production' => true,
'production/eu' => true,
'production/www.gitlab.com' => true,
'productioneu' => false,
@@ -155,7 +156,7 @@ describe Environment, models: true do
end
describe '#stop_with_action!' do
- let(:user) { create(:user) }
+ let(:user) { create(:admin) }
subject { environment.stop_with_action!(user) }
@@ -311,7 +312,7 @@ describe Environment, models: true do
end
describe '#generate_slug' do
- SUFFIX = "-[a-z0-9]{6}"
+ SUFFIX = "-[a-z0-9]{6}".freeze
{
"staging-12345678901234567" => "staging-123456789" + SUFFIX,
"9-staging-123456789012345" => "env-9-staging-123" + SUFFIX,
diff --git a/spec/models/members/project_member_spec.rb b/spec/models/members/project_member_spec.rb
index e4be0aba7a6..87ea2e70680 100644
--- a/spec/models/members/project_member_spec.rb
+++ b/spec/models/members/project_member_spec.rb
@@ -89,8 +89,8 @@ describe ProjectMember, models: true do
@user_1 = create :user
@user_2 = create :user
- @project_1.team << [ @user_1, :developer ]
- @project_2.team << [ @user_2, :reporter ]
+ @project_1.team << [@user_1, :developer]
+ @project_2.team << [@user_2, :reporter]
@status = @project_2.team.import(@project_1)
end
@@ -137,8 +137,8 @@ describe ProjectMember, models: true do
@user_1 = create :user
@user_2 = create :user
- @project_1.team << [ @user_1, :developer]
- @project_2.team << [ @user_2, :reporter]
+ @project_1.team << [@user_1, :developer]
+ @project_2.team << [@user_2, :reporter]
ProjectMember.truncate_teams([@project_1.id, @project_2.id])
end
diff --git a/spec/models/merge_request_diff_spec.rb b/spec/models/merge_request_diff_spec.rb
index 6d599e148a2..0a10ee01506 100644
--- a/spec/models/merge_request_diff_spec.rb
+++ b/spec/models/merge_request_diff_spec.rb
@@ -109,7 +109,7 @@ describe MergeRequestDiff, models: true do
{ id: 'sha2' }
]
- expect(subject.commits_sha).to eq(['sha1', 'sha2'])
+ expect(subject.commits_sha).to eq(%w(sha1 sha2))
end
end
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index a01741a9971..9331dc41a5e 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -209,6 +209,50 @@ describe MergeRequest, models: true do
end
end
+ describe '#diff_size' do
+ let(:merge_request) do
+ build(:merge_request, source_branch: 'expand-collapse-files', target_branch: 'master')
+ end
+
+ context 'when there are MR diffs' do
+ before do
+ merge_request.save
+ end
+
+ it 'returns the correct count' do
+ expect(merge_request.diff_size).to eq(105)
+ end
+
+ it 'does not perform highlighting' do
+ expect(Gitlab::Diff::Highlight).not_to receive(:new)
+
+ merge_request.diff_size
+ end
+ end
+
+ context 'when there are no MR diffs' do
+ before do
+ merge_request.compare = CompareService.new(
+ merge_request.source_project,
+ merge_request.source_branch
+ ).execute(
+ merge_request.target_project,
+ merge_request.target_branch
+ )
+ end
+
+ it 'returns the correct count' do
+ expect(merge_request.diff_size).to eq(105)
+ end
+
+ it 'does not perform highlighting' do
+ expect(Gitlab::Diff::Highlight).not_to receive(:new)
+
+ merge_request.diff_size
+ end
+ end
+ end
+
describe "#related_notes" do
let!(:merge_request) { create(:merge_request) }
@@ -789,12 +833,6 @@ 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
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index 35d932f1c64..3f9c4289de9 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -36,7 +36,7 @@ describe Namespace, models: true do
end
describe '#to_param' do
- it { expect(namespace.to_param).to eq(namespace.path) }
+ it { expect(namespace.to_param).to eq(namespace.full_path) }
end
describe '#human_name' do
@@ -151,7 +151,7 @@ describe Namespace, models: true do
describe :rm_dir do
let!(:project) { create(:empty_project, namespace: namespace) }
- let!(:path) { File.join(Gitlab.config.repositories.storages.default, namespace.path) }
+ let!(:path) { File.join(Gitlab.config.repositories.storages.default, namespace.full_path) }
it "removes its dirs when deleted" do
namespace.destroy
diff --git a/spec/models/project_group_link_spec.rb b/spec/models/project_group_link_spec.rb
index 59a4ae1b799..9b711bfc007 100644
--- a/spec/models/project_group_link_spec.rb
+++ b/spec/models/project_group_link_spec.rb
@@ -7,12 +7,27 @@ describe ProjectGroupLink do
end
describe "Validation" do
- let!(:project_group_link) { create(:project_group_link) }
+ let(:parent_group) { create(:group) }
+ let(:group) { create(:group, parent: parent_group) }
+ let(:project) { create(:project, group: group) }
+ let!(:project_group_link) { create(:project_group_link, project: project) }
it { should validate_presence_of(:project_id) }
it { should validate_uniqueness_of(:group_id).scoped_to(:project_id).with_message(/already shared/) }
it { should validate_presence_of(:group) }
it { should validate_presence_of(:group_access) }
+
+ it "doesn't allow a project to be shared with the group it is in" do
+ project_group_link.group = group
+
+ expect(project_group_link).not_to be_valid
+ end
+
+ it "doesn't allow a project to be shared with an ancestor of the group it is in" do
+ project_group_link.group = parent_group
+
+ expect(project_group_link).not_to be_valid
+ end
end
describe "destroying a record", truncate: true do
diff --git a/spec/models/project_services/bamboo_service_spec.rb b/spec/models/project_services/bamboo_service_spec.rb
index 497a626a418..4014d6129ee 100644
--- a/spec/models/project_services/bamboo_service_spec.rb
+++ b/spec/models/project_services/bamboo_service_spec.rb
@@ -181,7 +181,7 @@ describe BambooService, models: true, caching: true do
end
it 'sets commit status to "pending" when response has no results' do
- stub_request(body: %Q({"results":{"results":{"size":"0"}}}))
+ stub_request(body: %q({"results":{"results":{"size":"0"}}}))
is_expected.to eq('pending')
end
diff --git a/spec/models/project_services/buildkite_service_spec.rb b/spec/models/project_services/buildkite_service_spec.rb
index dbd23ff5491..05b602d8106 100644
--- a/spec/models/project_services/buildkite_service_spec.rb
+++ b/spec/models/project_services/buildkite_service_spec.rb
@@ -92,7 +92,7 @@ describe BuildkiteService, models: true, caching: true do
end
it 'passes through build status untouched when status is 200' do
- stub_request(body: %Q({"status":"Great Success"}))
+ stub_request(body: %q({"status":"Great Success"}))
is_expected.to eq('Great Success')
end
@@ -101,7 +101,7 @@ describe BuildkiteService, models: true, caching: true do
end
def stub_request(status: 200, body: nil)
- body ||= %Q({"status":"success"})
+ 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(
diff --git a/spec/models/project_services/chat_message/build_message_spec.rb b/spec/models/project_services/chat_message/build_message_spec.rb
index 50ad5013df9..3bd7ec18ae0 100644
--- a/spec/models/project_services/chat_message/build_message_spec.rb
+++ b/spec/models/project_services/chat_message/build_message_spec.rb
@@ -11,21 +11,28 @@ describe ChatMessage::BuildMessage do
project_name: 'project_name',
project_url: 'http://example.gitlab.com',
+ build_id: 1,
+ build_name: build_name,
+ build_stage: stage,
commit: {
status: status,
author_name: 'hacker',
+ author_url: 'http://example.gitlab.com/hacker',
duration: duration,
},
}
end
let(:message) { build_message }
+ let(:stage) { 'test' }
+ let(:status) { 'success' }
+ let(:build_name) { 'rspec' }
+ let(:duration) { 10 }
context 'build succeeded' do
let(:status) { 'success' }
let(:color) { 'good' }
- let(:duration) { 10 }
let(:message) { build_message('passed') }
it 'returns a message with information about succeeded build' do
@@ -38,7 +45,6 @@ describe ChatMessage::BuildMessage do
context 'build failed' do
let(:status) { 'failed' }
let(:color) { 'danger' }
- let(:duration) { 10 }
it 'returns a message with information about failed build' do
expect(subject.pretext).to be_empty
@@ -47,11 +53,25 @@ describe ChatMessage::BuildMessage do
end
end
- def build_message(status_text = status)
+ it 'returns a message with information on build' do
+ expect(subject.fallback).to include("on build <http://example.gitlab.com/builds/1|#{build_name}>")
+ end
+
+ it 'returns a message with stage name' do
+ expect(subject.fallback).to include("of stage #{stage}")
+ end
+
+ it 'returns a message with link to author' do
+ expect(subject.fallback).to include("by <http://example.gitlab.com/hacker|hacker>")
+ end
+
+ def build_message(status_text = status, stage_text = stage, build_text = build_name)
"<http://example.gitlab.com|project_name>:" \
" Commit <http://example.gitlab.com/commit/" \
"97de212e80737a608d939f648d959671fb0a0142/builds|97de212e>" \
" of <http://example.gitlab.com/commits/develop|develop> branch" \
- " by hacker #{status_text} in #{duration} #{'second'.pluralize(duration)}"
+ " by <http://example.gitlab.com/hacker|hacker> #{status_text}" \
+ " on build <http://example.gitlab.com/builds/1|#{build_text}>" \
+ " of stage #{stage_text} in #{duration} #{'second'.pluralize(duration)}"
end
end
diff --git a/spec/models/project_services/drone_ci_service_spec.rb b/spec/models/project_services/drone_ci_service_spec.rb
index f9307d6de7b..044737c6026 100644
--- a/spec/models/project_services/drone_ci_service_spec.rb
+++ b/spec/models/project_services/drone_ci_service_spec.rb
@@ -28,7 +28,7 @@ describe DroneCiService, models: true, caching: true do
shared_context :drone_ci_service do
let(:drone) { DroneCiService.new }
let(:project) { create(:project, :repository, name: 'project') }
- let(:path) { "#{project.namespace.path}/#{project.path}" }
+ let(:path) { project.full_path }
let(:drone_url) { 'http://drone.example.com' }
let(:sha) { '2ab7834c' }
let(:branch) { 'dev' }
@@ -50,7 +50,7 @@ describe DroneCiService, models: true, caching: true do
end
def stub_request(status: 200, body: nil)
- body ||= %Q({"status":"success"})
+ body ||= %q({"status":"success"})
WebMock.stub_request(:get, commit_status_path).to_return(
status: status,
@@ -95,12 +95,12 @@ describe DroneCiService, models: true, caching: true do
is_expected.to eq(:error)
end
- { "killed" => :canceled,
+ {
+ "killed" => :canceled,
"failure" => :failed,
"error" => :failed,
- "success" => "success",
+ "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}"}))
diff --git a/spec/models/project_services/irker_service_spec.rb b/spec/models/project_services/irker_service_spec.rb
index b9fb6f3f6f4..d5a16226d9d 100644
--- a/spec/models/project_services/irker_service_spec.rb
+++ b/spec/models/project_services/irker_service_spec.rb
@@ -59,8 +59,8 @@ describe IrkerService, models: true do
conn = @irker_server.accept
conn.readlines.each do |line|
- msg = JSON.load(line.chomp("\n"))
- expect(msg.keys).to match_array(['to', 'privmsg'])
+ msg = JSON.parse(line.chomp("\n"))
+ expect(msg.keys).to match_array(%w(to privmsg))
expect(msg['to']).to match_array(["irc://chat.freenode.net/#commits",
"irc://test.net/#test"])
end
diff --git a/spec/models/project_services/kubernetes_service_spec.rb b/spec/models/project_services/kubernetes_service_spec.rb
index 9052479d35e..24356447fed 100644
--- a/spec/models/project_services/kubernetes_service_spec.rb
+++ b/spec/models/project_services/kubernetes_service_spec.rb
@@ -171,7 +171,7 @@ describe KubernetesService, models: true, caching: true do
context 'with invalid pods' do
it 'returns no terminals' do
- stub_reactive_cache(service, pods: [ { "bad" => "pod" } ])
+ stub_reactive_cache(service, pods: [{ "bad" => "pod" }])
is_expected.to be_empty
end
@@ -184,7 +184,7 @@ describe KubernetesService, models: true, caching: true do
before do
stub_reactive_cache(
service,
- pods: [ pod, pod, kube_pod(app: "should-be-filtered-out") ]
+ pods: [pod, pod, kube_pod(app: "should-be-filtered-out")]
)
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 98f3d420c8a..598ff232c82 100644
--- a/spec/models/project_services/mattermost_slash_commands_service_spec.rb
+++ b/spec/models/project_services/mattermost_slash_commands_service_spec.rb
@@ -36,7 +36,8 @@ describe MattermostSlashCommandsService, :models do
description: "Perform common operations on: #{project.name_with_namespace}",
display_name: "GitLab / #{project.name_with_namespace}",
method: 'P',
- username: 'GitLab' }.to_json).
+ username: 'GitLab'
+ }.to_json).
to_return(
status: 200,
headers: { 'Content-Type' => 'application/json' },
diff --git a/spec/models/project_services/teamcity_service_spec.rb b/spec/models/project_services/teamcity_service_spec.rb
index a1edd083aa1..77b18e1c7d0 100644
--- a/spec/models/project_services/teamcity_service_spec.rb
+++ b/spec/models/project_services/teamcity_service_spec.rb
@@ -143,7 +143,7 @@ describe TeamcityService, models: true, caching: true do
end
it 'returns a build URL when teamcity_url has no trailing slash' do
- stub_request(body: %Q({"build":{"id":"666"}}))
+ stub_request(body: %q({"build":{"id":"666"}}))
is_expected.to eq('http://gitlab.com/teamcity/viewLog.html?buildId=666&buildTypeId=foo')
end
@@ -152,7 +152,7 @@ describe TeamcityService, models: true, caching: true do
let(:teamcity_url) { 'http://gitlab.com/teamcity/' }
it 'returns a build URL' do
- stub_request(body: %Q({"build":{"id":"666"}}))
+ stub_request(body: %q({"build":{"id":"666"}}))
is_expected.to eq('http://gitlab.com/teamcity/viewLog.html?buildId=666&buildTypeId=foo')
end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index b0087a9e15d..ee4f4092062 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -402,7 +402,7 @@ describe Project, models: true do
let(:project) { create(:empty_project, path: "somewhere") }
it 'returns the full web URL for this repo' do
- expect(project.web_url).to eq("#{Gitlab.config.gitlab.url}/#{project.namespace.path}/somewhere")
+ expect(project.web_url).to eq("#{Gitlab.config.gitlab.url}/#{project.namespace.full_path}/somewhere")
end
end
@@ -803,7 +803,7 @@ describe Project, models: true do
end
let(:avatar_path) do
- "/#{project.namespace.name}/#{project.path}/avatar"
+ "/#{project.full_path}/avatar"
end
it { should eq "http://#{Gitlab.config.gitlab.host}#{avatar_path}" }
@@ -1148,16 +1148,14 @@ describe Project, models: true do
end
it 'renames a repository' do
- ns = project.namespace_dir
-
expect(gitlab_shell).to receive(:mv_repository).
ordered.
- with(project.repository_storage_path, "#{ns}/foo", "#{ns}/#{project.path}").
+ with(project.repository_storage_path, "#{project.namespace.full_path}/foo", "#{project.full_path}").
and_return(true)
expect(gitlab_shell).to receive(:mv_repository).
ordered.
- with(project.repository_storage_path, "#{ns}/foo.wiki", "#{ns}/#{project.path}.wiki").
+ with(project.repository_storage_path, "#{project.namespace.full_path}/foo.wiki", "#{project.full_path}.wiki").
and_return(true)
expect_any_instance_of(SystemHooksService).
@@ -1166,7 +1164,7 @@ describe Project, models: true do
expect_any_instance_of(Gitlab::UploadsTransfer).
to receive(:rename_project).
- with('foo', project.path, ns)
+ with('foo', project.path, project.namespace.full_path)
expect(project).to receive(:expire_caches_before_rename)
@@ -1538,7 +1536,7 @@ describe Project, models: true do
it 'schedules a RepositoryForkWorker job' do
expect(RepositoryForkWorker).to receive(:perform_async).
with(project.id, forked_from_project.repository_storage_path,
- forked_from_project.path_with_namespace, project.namespace.path)
+ forked_from_project.path_with_namespace, project.namespace.full_path)
project.add_import_job
end
@@ -1752,7 +1750,7 @@ describe Project, models: true do
describe 'inside_path' do
let!(:project1) { create(:empty_project) }
let!(:project2) { create(:empty_project) }
- let!(:path) { project1.namespace.path }
+ let!(:path) { project1.namespace.full_path }
it { expect(Project.inside_path(path)).to eq([project1]) }
end
@@ -1767,7 +1765,7 @@ describe Project, models: true do
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)
+ project.repository.create_file(User.last, '.gitlab/route-map.yml', route_map, message: 'Add .gitlab/route-map.yml', branch_name: 'master')
end
context 'when there is a .gitlab/route-map.yml at the commit' do
@@ -1896,4 +1894,25 @@ describe Project, models: true do
end
end
end
+
+ describe '#http_url_to_repo' do
+ let(:project) { create :empty_project }
+
+ context 'when no user is given' do
+ it 'returns the url to the repo without a username' do
+ url = project.http_url_to_repo
+
+ expect(url).to eq(project.http_url_to_repo)
+ expect(url).not_to include('@')
+ end
+ end
+
+ context 'when user is given' do
+ it 'returns the url to the repo with the username' do
+ user = build_stubbed(:user)
+
+ expect(project.http_url_to_repo(user)).to match(%r{https?:\/\/#{user.username}@})
+ end
+ end
+ end
end
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index 838fd3754b2..ae203fada12 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -291,10 +291,10 @@ describe Repository, models: true do
end
end
- describe "#commit_dir" do
+ describe "#create_dir" do
it "commits a change that creates a new directory" do
expect do
- repository.commit_dir(user, 'newdir',
+ repository.create_dir(user, 'newdir',
message: 'Create newdir', branch_name: 'master')
end.to change { repository.commits('master').count }.by(1)
@@ -307,7 +307,7 @@ describe Repository, models: true do
it "creates a fork and commit to the forked project" do
expect do
- repository.commit_dir(user, 'newdir',
+ repository.create_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)
@@ -323,7 +323,7 @@ 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_dir(user, 'newdir',
+ repository.create_dir(user, 'newdir',
message: 'Add newdir',
branch_name: 'master',
author_email: author_email, author_name: author_name)
@@ -337,25 +337,23 @@ describe Repository, models: true do
end
end
- describe "#commit_file" do
- it 'commits change to a file successfully' do
+ describe "#create_file" do
+ it 'commits new file successfully' do
expect do
- repository.commit_file(user, 'CHANGELOG', 'Changelog!',
- message: 'Updates file content',
- branch_name: 'master',
- update: true)
+ repository.create_file(user, 'NEWCHANGELOG', 'Changelog!',
+ message: 'Create changelog',
+ branch_name: 'master')
end.to change { repository.commits('master').count }.by(1)
- blob = repository.blob_at('master', 'CHANGELOG')
+ blob = repository.blob_at('master', 'NEWCHANGELOG')
expect(blob.data).to eq('Changelog!')
end
it 'respects the autocrlf setting' do
- repository.commit_file(user, 'hello.txt', "Hello,\r\nWorld",
+ repository.create_file(user, 'hello.txt', "Hello,\r\nWorld",
message: 'Add hello world',
- branch_name: 'master',
- update: true)
+ branch_name: 'master')
blob = repository.blob_at('master', 'hello.txt')
@@ -365,10 +363,9 @@ 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!',
+ repository.create_file(user, 'NEWREADME', '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)
@@ -382,6 +379,18 @@ describe Repository, models: true do
end
describe "#update_file" do
+ it 'updates file successfully' do
+ expect do
+ repository.update_file(user, 'CHANGELOG', 'Changelog!',
+ message: 'Update changelog',
+ branch_name: 'master')
+ end.to change { repository.commits('master').count }.by(1)
+
+ blob = repository.blob_at('master', 'CHANGELOG')
+
+ expect(blob.data).to eq('Changelog!')
+ end
+
it 'updates filename successfully' do
expect do
repository.update_file(user, 'NEWLICENSE', 'Copyright!',
@@ -398,9 +407,6 @@ 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!',
- message: 'Add README', branch_name: 'master', update: true)
-
expect do
repository.update_file(user, 'README', 'Updated README!',
branch_name: 'master',
@@ -418,13 +424,10 @@ describe Repository, models: true do
end
end
- describe "#remove_file" do
+ describe "#delete_file" do
it 'removes file successfully' do
- repository.commit_file(user, 'README', 'README!',
- message: 'Add README', branch_name: 'master', update: true)
-
expect do
- repository.remove_file(user, 'README',
+ repository.delete_file(user, 'README',
message: 'Remove README', branch_name: 'master')
end.to change { repository.commits('master').count }.by(1)
@@ -433,11 +436,8 @@ 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!',
- message: 'Add README', branch_name: 'master', update: true)
-
expect do
- repository.remove_file(user, 'README',
+ repository.delete_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)
@@ -587,14 +587,14 @@ describe Repository, models: true do
describe "#license_blob", caching: true do
before do
- repository.remove_file(
+ repository.delete_file(
user, 'LICENSE', message: 'Remove LICENSE', branch_name: 'master')
end
it 'handles when HEAD points to non-existent ref' do
- repository.commit_file(
+ repository.create_file(
user, 'LICENSE', 'Copyright!',
- message: 'Add LICENSE', branch_name: 'master', update: false)
+ message: 'Add LICENSE', branch_name: 'master')
allow(repository).to receive(:file_on_head).
and_raise(Rugged::ReferenceError)
@@ -603,27 +603,27 @@ describe Repository, models: true do
end
it 'looks in the root_ref only' do
- repository.remove_file(user, 'LICENSE',
+ repository.delete_file(user, 'LICENSE',
message: 'Remove LICENSE', branch_name: 'markdown')
- repository.commit_file(user, 'LICENSE',
+ repository.create_file(user, 'LICENSE',
Licensee::License.new('mit').content,
- message: 'Add LICENSE', branch_name: 'markdown', update: false)
+ message: 'Add LICENSE', branch_name: 'markdown')
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!',
- message: 'Add LICENSE', branch_name: 'master', update: false)
+ repository.create_file(user, 'LICENSE', 'Copyright!',
+ message: 'Add LICENSE', branch_name: 'master')
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,
+ repository.create_file(user, filename,
Licensee::License.new('mit').content,
- message: "Add #{filename}", branch_name: 'master', update: false)
+ message: "Add #{filename}", branch_name: 'master')
expect(repository.license_blob.name).to eq(filename)
end
@@ -632,7 +632,7 @@ describe Repository, models: true do
describe '#license_key', caching: true do
before do
- repository.remove_file(user, 'LICENSE',
+ repository.delete_file(user, 'LICENSE',
message: 'Remove LICENSE', branch_name: 'master')
end
@@ -647,16 +647,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!',
- message: 'Add LICENSE', branch_name: 'master', update: false)
+ repository.create_file(user, 'LICENSE', 'Copyright!',
+ message: 'Add LICENSE', branch_name: 'master')
expect(repository.license_key).to be_nil
end
it 'returns the license key' do
- repository.commit_file(user, 'LICENSE',
+ repository.create_file(user, 'LICENSE',
Licensee::License.new('mit').content,
- message: 'Add LICENSE', branch_name: 'master', update: false)
+ message: 'Add LICENSE', branch_name: 'master')
expect(repository.license_key).to eq('mit')
end
@@ -913,10 +913,9 @@ describe Repository, models: true do
expect(empty_repository).to receive(:expire_emptiness_caches)
expect(empty_repository).to receive(:expire_branches_cache)
- empty_repository.commit_file(user, 'CHANGELOG', 'Changelog!',
+ empty_repository.create_file(user, 'CHANGELOG', 'Changelog!',
message: 'Updates file content',
- branch_name: 'master',
- update: false)
+ branch_name: 'master')
end
end
end
@@ -1382,13 +1381,13 @@ describe Repository, models: true do
describe '#branch_count' do
it 'returns the number of branches' do
- expect(repository.branch_count).to be_an_instance_of(Fixnum)
+ expect(repository.branch_count).to be_an(Integer)
end
end
describe '#tag_count' do
it 'returns the number of tags' do
- expect(repository.tag_count).to be_an_instance_of(Fixnum)
+ expect(repository.tag_count).to be_an(Integer)
end
end
@@ -1738,7 +1737,7 @@ describe Repository, models: true do
context 'with an existing repository' do
it 'returns the commit count' do
- expect(repository.commit_count).to be_an_instance_of(Fixnum)
+ expect(repository.commit_count).to be_an(Integer)
end
end
end
@@ -1796,7 +1795,7 @@ describe Repository, models: true do
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)
+ repository.create_file(User.last, '.gitlab-ci.yml', 'CONTENT', message: 'Add .gitlab-ci.yml', branch_name: 'master')
end
context 'when there is a .gitlab-ci.yml at the commit' do
@@ -1814,7 +1813,7 @@ describe Repository, models: true do
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)
+ repository.create_file(User.last, '.gitlab/route-map.yml', 'CONTENT', message: 'Add .gitlab/route-map.yml', branch_name: 'master')
end
context 'when there is a .gitlab/route-map.yml at the commit' do
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 89cef7ab978..e86b4a761d9 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -22,7 +22,7 @@ describe User, models: true do
it { is_expected.to have_many(:deploy_keys).dependent(:destroy) }
it { is_expected.to have_many(:events).dependent(:destroy) }
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(:issues).dependent(:restrict_with_exception) }
it { is_expected.to have_many(:notes).dependent(:destroy) }
it { is_expected.to have_many(:assigned_issues).dependent(:nullify) }
it { is_expected.to have_many(:merge_requests).dependent(:destroy) }
@@ -208,6 +208,22 @@ describe User, models: true do
end
end
end
+
+ describe 'ghost users' do
+ it 'does not allow a non-blocked ghost user' do
+ user = build(:user, :ghost)
+ user.state = 'active'
+
+ expect(user).to be_invalid
+ end
+
+ it 'allows a blocked ghost user' do
+ user = build(:user, :ghost)
+ user.state = 'blocked'
+
+ expect(user).to be_valid
+ end
+ end
end
describe "scopes" do
@@ -582,18 +598,16 @@ describe User, models: true do
it "applies defaults to user" do
expect(user.projects_limit).to eq(Gitlab.config.gitlab.default_projects_limit)
expect(user.can_create_group).to eq(Gitlab.config.gitlab.default_can_create_group)
- expect(user.theme_id).to eq(Gitlab.config.gitlab.default_theme)
expect(user.external).to be_falsey
end
end
describe 'with default overrides' do
- let(:user) { User.new(projects_limit: 123, can_create_group: false, can_create_team: true, theme_id: 1) }
+ let(:user) { User.new(projects_limit: 123, can_create_group: false, can_create_team: true) }
it "applies defaults to user" do
expect(user.projects_limit).to eq(123)
expect(user.can_create_group).to be_falsey
- expect(user.theme_id).to eq(1)
end
end
@@ -695,9 +709,7 @@ describe User, models: true do
end
describe '.search_with_secondary_emails' do
- def search_with_secondary_emails(query)
- described_class.search_with_secondary_emails(query)
- end
+ delegate :search_with_secondary_emails, to: :described_class
let!(:user) { create(:user) }
let!(:email) { create(:email) }
@@ -1417,7 +1429,7 @@ describe User, models: true do
it { expect(user.nested_groups).to eq([nested_group]) }
end
- describe '#nested_projects' do
+ describe '#nested_groups_projects' do
let!(:user) { create(:user) }
let!(:group) { create(:group) }
let!(:nested_group) { create(:group, parent: group) }
@@ -1432,7 +1444,7 @@ describe User, models: true do
other_project.add_developer(create(:user))
end
- it { expect(user.nested_projects).to eq([nested_project]) }
+ it { expect(user.nested_groups_projects).to eq([nested_project]) }
end
describe '#refresh_authorized_projects', redis: true do
@@ -1494,4 +1506,41 @@ describe User, models: true do
expect(user.admin).to be true
end
end
+
+ describe '.ghost' do
+ it "creates a ghost user if one isn't already present" do
+ ghost = User.ghost
+
+ expect(ghost).to be_ghost
+ expect(ghost).to be_persisted
+ end
+
+ it "does not create a second ghost user if one is already present" do
+ expect do
+ User.ghost
+ User.ghost
+ end.to change { User.count }.by(1)
+ expect(User.ghost).to eq(User.ghost)
+ end
+
+ context "when a regular user exists with the username 'ghost'" do
+ it "creates a ghost user with a non-conflicting username" do
+ create(:user, username: 'ghost')
+ ghost = User.ghost
+
+ expect(ghost).to be_persisted
+ expect(ghost.username).to eq('ghost1')
+ end
+ end
+
+ context "when a regular user exists with the email 'ghost@example.com'" do
+ it "creates a ghost user with a non-conflicting email" do
+ create(:user, email: 'ghost@example.com')
+ ghost = User.ghost
+
+ expect(ghost).to be_persisted
+ expect(ghost.email).to eq('ghost1@example.com')
+ end
+ end
+ end
end
diff --git a/spec/policies/user_policy_spec.rb b/spec/policies/user_policy_spec.rb
new file mode 100644
index 00000000000..d5761390d39
--- /dev/null
+++ b/spec/policies/user_policy_spec.rb
@@ -0,0 +1,37 @@
+require 'spec_helper'
+
+describe UserPolicy, models: true do
+ let(:current_user) { create(:user) }
+ let(:user) { create(:user) }
+
+ subject { described_class.abilities(current_user, user).to_set }
+
+ describe "reading a user's information" do
+ it { is_expected.to include(:read_user) }
+ end
+
+ describe "destroying a user" do
+ context "when a regular user tries to destroy another regular user" do
+ it { is_expected.not_to include(:destroy_user) }
+ end
+
+ context "when a regular user tries to destroy themselves" do
+ let(:current_user) { user }
+
+ it { is_expected.to include(:destroy_user) }
+ end
+
+ context "when an admin user tries to destroy a regular user" do
+ let(:current_user) { create(:user, :admin) }
+
+ it { is_expected.to include(:destroy_user) }
+ end
+
+ context "when an admin user tries to destroy a ghost user" do
+ let(:current_user) { create(:user, :admin) }
+ let(:user) { create(:user, :ghost) }
+
+ it { is_expected.not_to include(:destroy_user) }
+ end
+ end
+end
diff --git a/spec/requests/api/access_requests_spec.rb b/spec/requests/api/access_requests_spec.rb
index e487297748b..46edbd49b28 100644
--- a/spec/requests/api/access_requests_spec.rb
+++ b/spec/requests/api/access_requests_spec.rb
@@ -48,6 +48,7 @@ describe API::AccessRequests, api: true do
get api("/#{source_type.pluralize}/#{source.id}/access_requests", master)
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.size).to eq(1)
end
@@ -199,7 +200,7 @@ describe API::AccessRequests, api: true do
expect do
delete api("/#{source_type.pluralize}/#{source.id}/access_requests/#{access_requester.id}", access_requester)
- expect(response).to have_http_status(200)
+ expect(response).to have_http_status(204)
end.to change { source.requesters.count }.by(-1)
end
end
@@ -209,7 +210,7 @@ describe API::AccessRequests, api: true do
expect do
delete api("/#{source_type.pluralize}/#{source.id}/access_requests/#{access_requester.id}", master)
- expect(response).to have_http_status(200)
+ expect(response).to have_http_status(204)
end.to change { source.requesters.count }.by(-1)
end
diff --git a/spec/requests/api/award_emoji_spec.rb b/spec/requests/api/award_emoji_spec.rb
index c8e8f31cc1f..9756991162e 100644
--- a/spec/requests/api/award_emoji_spec.rb
+++ b/spec/requests/api/award_emoji_spec.rb
@@ -34,6 +34,7 @@ describe API::AwardEmoji, api: true do
get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/award_emoji", user)
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.first['name']).to eq(downvote.name)
end
@@ -241,9 +242,9 @@ describe API::AwardEmoji, api: true do
it 'deletes the award' do
expect do
delete api("/projects/#{project.id}/issues/#{issue.id}/award_emoji/#{award_emoji.id}", user)
- end.to change { issue.award_emoji.count }.from(1).to(0)
- expect(response).to have_http_status(200)
+ expect(response).to have_http_status(204)
+ end.to change { issue.award_emoji.count }.from(1).to(0)
end
it 'returns a 404 error when the award emoji can not be found' do
@@ -257,9 +258,9 @@ describe API::AwardEmoji, api: true do
it 'deletes the award' do
expect do
delete api("/projects/#{project.id}/merge_requests/#{merge_request.id}/award_emoji/#{downvote.id}", user)
- end.to change { merge_request.award_emoji.count }.from(1).to(0)
- expect(response).to have_http_status(200)
+ expect(response).to have_http_status(204)
+ end.to change { merge_request.award_emoji.count }.from(1).to(0)
end
it 'returns a 404 error when note id not found' do
@@ -276,9 +277,9 @@ describe API::AwardEmoji, api: true do
it 'deletes the award' do
expect do
delete api("/projects/#{project.id}/snippets/#{snippet.id}/award_emoji/#{award.id}", user)
- end.to change { snippet.award_emoji.count }.from(1).to(0)
- expect(response).to have_http_status(200)
+ expect(response).to have_http_status(204)
+ end.to change { snippet.award_emoji.count }.from(1).to(0)
end
end
end
@@ -289,9 +290,9 @@ describe API::AwardEmoji, api: true do
it 'deletes the award' do
expect do
delete api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji/#{rocket.id}", user)
- end.to change { note.award_emoji.count }.from(1).to(0)
- expect(response).to have_http_status(200)
+ expect(response).to have_http_status(204)
+ end.to change { note.award_emoji.count }.from(1).to(0)
end
end
end
diff --git a/spec/requests/api/boards_spec.rb b/spec/requests/api/boards_spec.rb
index c14c3cb1ce7..87c36639cd4 100644
--- a/spec/requests/api/boards_spec.rb
+++ b/spec/requests/api/boards_spec.rb
@@ -55,6 +55,7 @@ describe API::Boards, api: true do
get api(base_url, user)
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.first['id']).to eq(board.id)
@@ -72,6 +73,7 @@ describe API::Boards, api: true do
get api(base_url, user)
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(2)
expect(json_response.first['label']['name']).to eq(dev_label.title)
@@ -193,8 +195,7 @@ describe API::Boards, api: true do
it "deletes the list if an admin requests it" do
delete api("#{base_url}/#{dev_list.id}", owner)
- expect(response).to have_http_status(200)
- expect(json_response['position']).to eq(1)
+ expect(response).to have_http_status(204)
end
end
end
diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb
index 3e66236f6ae..ab5a7e4d3de 100644
--- a/spec/requests/api/branches_spec.rb
+++ b/spec/requests/api/branches_spec.rb
@@ -17,8 +17,10 @@ describe API::Branches, api: true do
it "returns an array of project branches" do
project.repository.expire_all_method_caches
- get api("/projects/#{project.id}/repository/branches", user)
+ get api("/projects/#{project.id}/repository/branches", user), per_page: 100
+
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
branch_names = json_response.map { |x| x['name'] }
expect(branch_names).to match_array(project.repository.branch_names)
@@ -270,7 +272,7 @@ describe API::Branches, api: true do
describe "POST /projects/:id/repository/branches" do
it "creates a new branch" do
post api("/projects/#{project.id}/repository/branches", user),
- branch_name: 'feature1',
+ branch: 'feature1',
ref: branch_sha
expect(response).to have_http_status(201)
@@ -281,14 +283,14 @@ describe API::Branches, api: true do
it "denies for user without push access" do
post api("/projects/#{project.id}/repository/branches", user2),
- branch_name: branch_name,
+ branch: branch_name,
ref: branch_sha
expect(response).to have_http_status(403)
end
it 'returns 400 if branch name is invalid' do
post api("/projects/#{project.id}/repository/branches", user),
- branch_name: 'new design',
+ branch: 'new design',
ref: branch_sha
expect(response).to have_http_status(400)
expect(json_response['message']).to eq('Branch name is invalid')
@@ -296,12 +298,12 @@ describe API::Branches, api: true do
it 'returns 400 if branch already exists' do
post api("/projects/#{project.id}/repository/branches", user),
- branch_name: 'new_design1',
+ branch: 'new_design1',
ref: branch_sha
expect(response).to have_http_status(201)
post api("/projects/#{project.id}/repository/branches", user),
- branch_name: 'new_design1',
+ branch: 'new_design1',
ref: branch_sha
expect(response).to have_http_status(400)
expect(json_response['message']).to eq('Branch already exists')
@@ -309,7 +311,7 @@ describe API::Branches, api: true do
it 'returns 400 if ref name is invalid' do
post api("/projects/#{project.id}/repository/branches", user),
- branch_name: 'new_design3',
+ branch: 'new_design3',
ref: 'foo'
expect(response).to have_http_status(400)
expect(json_response['message']).to eq('Invalid reference name')
@@ -323,15 +325,14 @@ describe API::Branches, api: true do
it "removes branch" do
delete api("/projects/#{project.id}/repository/branches/#{branch_name}", user)
- expect(response).to have_http_status(200)
- expect(json_response['branch_name']).to eq(branch_name)
+
+ expect(response).to have_http_status(204)
end
it "removes a branch with dots in the branch name" do
delete api("/projects/#{project.id}/repository/branches/with.1.2.3", user)
- expect(response).to have_http_status(200)
- expect(json_response['branch_name']).to eq("with.1.2.3")
+ expect(response).to have_http_status(204)
end
it 'returns 404 if branch not exists' do
@@ -358,9 +359,11 @@ describe API::Branches, api: true do
allow_any_instance_of(Repository).to receive(:rm_branch).and_return(true)
end
- it 'returns 200' do
+ it 'returns 202 with json body' do
delete api("/projects/#{project.id}/repository/merged_branches", user)
- expect(response).to have_http_status(200)
+
+ expect(response).to have_http_status(202)
+ expect(json_response['message']).to eql('202 Accepted')
end
it 'returns a 403 error if guest' do
diff --git a/spec/requests/api/broadcast_messages_spec.rb b/spec/requests/api/broadcast_messages_spec.rb
index 7c9078b2864..024fa66848c 100644
--- a/spec/requests/api/broadcast_messages_spec.rb
+++ b/spec/requests/api/broadcast_messages_spec.rb
@@ -25,6 +25,7 @@ describe API::BroadcastMessages, api: true do
get api('/broadcast_messages', admin)
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_kind_of(Array)
expect(json_response.first.keys)
.to match_array(%w(id message starts_at ends_at color font active))
@@ -173,8 +174,11 @@ describe API::BroadcastMessages, api: true do
end
it 'deletes the broadcast message for admins' do
- expect { delete api("/broadcast_messages/#{message.id}", admin) }
- .to change { BroadcastMessage.count }.by(-1)
+ expect do
+ delete api("/broadcast_messages/#{message.id}", admin)
+
+ expect(response).to have_http_status(204)
+ end.to change { BroadcastMessage.count }.by(-1)
end
end
end
diff --git a/spec/requests/api/builds_spec.rb b/spec/requests/api/builds_spec.rb
index 834c4e52693..76a10a2374c 100644
--- a/spec/requests/api/builds_spec.rb
+++ b/spec/requests/api/builds_spec.rb
@@ -16,12 +16,15 @@ describe API::Builds, api: true do
let(:query) { '' }
before do
+ create(:ci_build, :skipped, pipeline: pipeline)
+
get api("/projects/#{project.id}/builds?#{query}", api_user)
end
context 'authorized user' do
it 'returns project builds' do
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
end
@@ -48,6 +51,18 @@ describe API::Builds, api: true do
end
end
+ context 'filter project with scope skipped' do
+ let(:query) { 'scope=skipped' }
+ let(:json_build) { json_response.first }
+
+ it 'return builds with status skipped' do
+ expect(response).to have_http_status 200
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq 1
+ expect(json_build['status']).to eq 'skipped'
+ end
+ end
+
context 'filter project with array of scope elements' do
let(:query) { 'scope[0]=pending&scope[1]=running' }
@@ -97,6 +112,7 @@ describe API::Builds, api: true do
it 'returns project jobs for specific commit' do
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.size).to eq 2
end
diff --git a/spec/requests/api/commit_statuses_spec.rb b/spec/requests/api/commit_statuses_spec.rb
index eb53fd71872..81a8856b8f1 100644
--- a/spec/requests/api/commit_statuses_spec.rb
+++ b/spec/requests/api/commit_statuses_spec.rb
@@ -5,12 +5,15 @@ describe API::CommitStatuses, api: true do
let!(:project) { create(:project, :repository) }
let(:commit) { project.repository.commit }
- let(:commit_status) { create(:commit_status, pipeline: pipeline) }
let(:guest) { create_user(:guest) }
let(:reporter) { create_user(:reporter) }
let(:developer) { create_user(:developer) }
let(:sha) { commit.id }
+ let(:commit_status) do
+ create(:commit_status, status: :pending, pipeline: pipeline)
+ end
+
describe "GET /projects/:id/repository/commits/:sha/statuses" do
let(:get_url) { "/projects/#{project.id}/repository/commits/#{sha}/statuses" }
@@ -18,10 +21,6 @@ describe API::CommitStatuses, api: true do
let!(:master) { project.pipelines.create(sha: commit.id, ref: 'master') }
let!(:develop) { project.pipelines.create(sha: commit.id, ref: 'develop') }
- it_behaves_like 'a paginated resources' do
- let(:request) { get api(get_url, reporter) }
- end
-
context "reporter user" do
let(:statuses_id) { json_response.map { |status| status['id'] } }
@@ -42,6 +41,7 @@ describe API::CommitStatuses, api: true do
it 'returns latest commit statuses' do
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(statuses_id).to contain_exactly(status3.id, status4.id, status5.id, status6.id)
json_response.sort_by!{ |status| status['id'] }
@@ -54,7 +54,7 @@ describe API::CommitStatuses, api: true do
it 'returns all commit statuses' do
expect(response).to have_http_status(200)
-
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(statuses_id).to contain_exactly(status1.id, status2.id,
status3.id, status4.id,
@@ -67,7 +67,7 @@ describe API::CommitStatuses, api: true do
it 'returns latest commit statuses for specific ref' do
expect(response).to have_http_status(200)
-
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(statuses_id).to contain_exactly(status3.id, status5.id)
end
@@ -78,7 +78,7 @@ describe API::CommitStatuses, api: true do
it 'return latest commit statuses for specific name' do
expect(response).to have_http_status(200)
-
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(statuses_id).to contain_exactly(status4.id, status5.id)
end
diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb
index 3eef10c0698..5190fcca2d1 100644
--- a/spec/requests/api/commits_spec.rb
+++ b/spec/requests/api/commits_spec.rb
@@ -72,7 +72,7 @@ describe API::Commits, api: true do
get api("/projects/#{project.id}/repository/commits?since=invalid-date", user)
expect(response).to have_http_status(400)
- expect(json_response['message']).to include "\"since\" must be a timestamp in ISO 8601 format"
+ expect(json_response['error']).to eq('since is invalid')
end
end
@@ -107,7 +107,7 @@ describe API::Commits, api: true do
let(:message) { 'Created file' }
let!(:invalid_c_params) do
{
- branch_name: 'master',
+ branch: 'master',
commit_message: message,
actions: [
{
@@ -120,7 +120,7 @@ describe API::Commits, api: true do
end
let!(:valid_c_params) do
{
- branch_name: 'master',
+ branch: 'master',
commit_message: message,
actions: [
{
@@ -148,7 +148,7 @@ describe API::Commits, api: true do
end
context 'with project path in URL' do
- let(:url) { "/projects/#{project.namespace.path}%2F#{project.path}/repository/commits" }
+ let(:url) { "/projects/#{project.full_path.gsub('/', '%2F')}/repository/commits" }
it 'a new file in project repo' do
post api(url, user), valid_c_params
@@ -162,7 +162,7 @@ describe API::Commits, api: true do
let(:message) { 'Deleted file' }
let!(:invalid_d_params) do
{
- branch_name: 'markdown',
+ branch: 'markdown',
commit_message: message,
actions: [
{
@@ -174,7 +174,7 @@ describe API::Commits, api: true do
end
let!(:valid_d_params) do
{
- branch_name: 'markdown',
+ branch: 'markdown',
commit_message: message,
actions: [
{
@@ -203,7 +203,7 @@ describe API::Commits, api: true do
let(:message) { 'Moved file' }
let!(:invalid_m_params) do
{
- branch_name: 'feature',
+ branch: 'feature',
commit_message: message,
actions: [
{
@@ -217,7 +217,7 @@ describe API::Commits, api: true do
end
let!(:valid_m_params) do
{
- branch_name: 'feature',
+ branch: 'feature',
commit_message: message,
actions: [
{
@@ -248,7 +248,7 @@ describe API::Commits, api: true do
let(:message) { 'Updated file' }
let!(:invalid_u_params) do
{
- branch_name: 'master',
+ branch: 'master',
commit_message: message,
actions: [
{
@@ -261,7 +261,7 @@ describe API::Commits, api: true do
end
let!(:valid_u_params) do
{
- branch_name: 'master',
+ branch: 'master',
commit_message: message,
actions: [
{
@@ -291,7 +291,7 @@ describe API::Commits, api: true do
let(:message) { 'Multiple actions' }
let!(:invalid_mo_params) do
{
- branch_name: 'master',
+ branch: 'master',
commit_message: message,
actions: [
{
@@ -319,7 +319,7 @@ describe API::Commits, api: true do
end
let!(:valid_mo_params) do
{
- branch_name: 'master',
+ branch: 'master',
commit_message: message,
actions: [
{
@@ -456,6 +456,7 @@ describe API::Commits, api: true do
it 'returns merge_request comments' do
get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments", user)
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
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 commit')
diff --git a/spec/requests/api/deploy_keys_spec.rb b/spec/requests/api/deploy_keys_spec.rb
index 766234d7104..4f4b18cf0e0 100644
--- a/spec/requests/api/deploy_keys_spec.rb
+++ b/spec/requests/api/deploy_keys_spec.rb
@@ -35,6 +35,7 @@ describe API::DeployKeys, api: true do
get api('/deploy_keys', admin)
expect(response.status).to eq(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.first['id']).to eq(deploy_keys_project.deploy_key.id)
end
@@ -48,6 +49,7 @@ describe API::DeployKeys, api: true do
get api("/projects/#{project.id}/deploy_keys", admin)
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.first['title']).to eq(deploy_key.title)
end
@@ -114,6 +116,8 @@ describe API::DeployKeys, api: true do
it 'should delete existing key' do
expect do
delete api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}", admin)
+
+ expect(response).to have_http_status(204)
end.to change{ project.deploy_keys.count }.by(-1)
end
@@ -146,25 +150,4 @@ describe API::DeployKeys, api: true do
end
end
end
-
- describe 'DELETE /projects/:id/deploy_keys/:key_id/disable' do
- context 'when the user can admin the project' do
- it 'disables the key' do
- expect do
- delete api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}/disable", admin)
- end.to change { project.deploy_keys.count }.from(1).to(0)
-
- expect(response).to have_http_status(200)
- expect(json_response['id']).to eq(deploy_key.id)
- end
- end
-
- context 'when authenticated as non-admin user' do
- it 'should return a 404 error' do
- delete api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}/disable", user)
-
- expect(response).to have_http_status(404)
- end
- end
- end
end
diff --git a/spec/requests/api/deployments_spec.rb b/spec/requests/api/deployments_spec.rb
index 31e3cfa1b2f..e55575ffbda 100644
--- a/spec/requests/api/deployments_spec.rb
+++ b/spec/requests/api/deployments_spec.rb
@@ -14,14 +14,11 @@ describe API::Deployments, api: true do
describe 'GET /projects/:id/deployments' do
context 'as member of the project' do
- it_behaves_like 'a paginated resources' do
- let(:request) { get api("/projects/#{project.id}/deployments", user) }
- end
-
it 'returns projects deployments' do
get api("/projects/#{project.id}/deployments", user)
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.size).to eq(1)
expect(json_response.first['iid']).to eq(deployment.iid)
diff --git a/spec/requests/api/environments_spec.rb b/spec/requests/api/environments_spec.rb
index 8168b613766..d66eb63fd0a 100644
--- a/spec/requests/api/environments_spec.rb
+++ b/spec/requests/api/environments_spec.rb
@@ -14,14 +14,11 @@ describe API::Environments, api: true do
describe 'GET /projects/:id/environments' do
context 'as member of the project' do
- it_behaves_like 'a paginated resources' do
- let(:request) { get api("/projects/#{project.id}/environments", user) }
- end
-
it 'returns project environments' do
get api("/projects/#{project.id}/environments", user)
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.size).to eq(1)
expect(json_response.first['name']).to eq(environment.name)
@@ -125,7 +122,7 @@ describe API::Environments, api: true do
it 'returns a 200 for an existing environment' do
delete api("/projects/#{project.id}/environments/#{environment.id}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_http_status(204)
end
it 'returns a 404 for non existing id' do
diff --git a/spec/requests/api/files_spec.rb b/spec/requests/api/files_spec.rb
index 5e26e779366..31b1aca6d73 100644
--- a/spec/requests/api/files_spec.rb
+++ b/spec/requests/api/files_spec.rb
@@ -104,7 +104,7 @@ describe API::Files, api: true do
let(:valid_params) do
{
file_path: 'newfile.rb',
- branch_name: 'master',
+ branch: 'master',
content: 'puts 8',
commit_message: 'Added newfile'
}
@@ -127,7 +127,7 @@ describe API::Files, api: true do
end
it "returns a 400 if editor fails to create file" do
- allow_any_instance_of(Repository).to receive(:commit_file).
+ allow_any_instance_of(Repository).to receive(:create_file).
and_return(false)
post api("/projects/#{project.id}/repository/files", user), valid_params
@@ -153,7 +153,7 @@ describe API::Files, api: true do
let(:valid_params) do
{
file_path: file_path,
- branch_name: 'master',
+ branch: 'master',
content: 'puts 8',
commit_message: 'Changed file'
}
@@ -193,7 +193,7 @@ describe API::Files, api: true do
let(:valid_params) do
{
file_path: file_path,
- branch_name: 'master',
+ branch: 'master',
commit_message: 'Changed file'
}
end
@@ -201,11 +201,7 @@ describe API::Files, api: true do
it "deletes existing file in project repo" do
delete api("/projects/#{project.id}/repository/files", user), valid_params
- expect(response).to have_http_status(200)
- expect(json_response['file_path']).to eq(file_path)
- last_commit = project.repository.commit.raw
- expect(last_commit.author_email).to eq(user.email)
- expect(last_commit.author_name).to eq(user.name)
+ expect(response).to have_http_status(204)
end
it "returns a 400 bad request if no params given" do
@@ -215,7 +211,7 @@ describe API::Files, api: true do
end
it "returns a 400 if fails to create file" do
- allow_any_instance_of(Repository).to receive(:remove_file).and_return(false)
+ allow_any_instance_of(Repository).to receive(:delete_file).and_return(false)
delete api("/projects/#{project.id}/repository/files", user), valid_params
@@ -228,10 +224,7 @@ describe API::Files, api: true do
delete api("/projects/#{project.id}/repository/files", user), valid_params
- expect(response).to have_http_status(200)
- last_commit = project.repository.commit.raw
- expect(last_commit.author_email).to eq(author_email)
- expect(last_commit.author_name).to eq(author_name)
+ expect(response).to have_http_status(204)
end
end
end
@@ -241,7 +234,7 @@ describe API::Files, api: true do
let(:put_params) do
{
file_path: file_path,
- branch_name: 'master',
+ branch: 'master',
content: 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUAAACnej3aAAAAAXRSTlMAQObYZgAAAApJREFUCNdjYAAAAAIAAeIhvDMAAAAASUVORK5CYII=',
commit_message: 'Binary file with a \n should not be touched',
encoding: 'base64'
diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb
index ccd7898586c..b0ba3ea912d 100644
--- a/spec/requests/api/groups_spec.rb
+++ b/spec/requests/api/groups_spec.rb
@@ -33,6 +33,7 @@ describe API::Groups, api: true do
get api("/groups", user1)
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response)
@@ -43,6 +44,7 @@ describe API::Groups, api: true do
get api("/groups", user1), statistics: true
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.first).not_to include 'statistics'
end
@@ -53,6 +55,7 @@ describe API::Groups, api: true do
get api("/groups", admin)
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(2)
end
@@ -61,6 +64,7 @@ describe API::Groups, api: true do
get api("/groups", admin)
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.first).not_to include('statistics')
end
@@ -78,6 +82,7 @@ describe API::Groups, api: true do
get api("/groups", admin), statistics: true
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response)
.to satisfy_one { |group| group['statistics'] == attributes }
@@ -89,6 +94,7 @@ describe API::Groups, api: true do
get api("/groups", admin), skip_groups: [group2.id]
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
end
@@ -103,6 +109,7 @@ describe API::Groups, api: true do
get api("/groups", user1), all_available: true
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(response_groups).to contain_exactly(public_group.name, group1.name)
end
@@ -120,6 +127,7 @@ describe API::Groups, api: true do
get api("/groups", user1)
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(response_groups).to eq([group3.name, group1.name])
end
@@ -128,6 +136,7 @@ describe API::Groups, api: true do
get api("/groups", user1), sort: "desc"
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(response_groups).to eq([group1.name, group3.name])
end
@@ -136,26 +145,18 @@ describe API::Groups, api: true do
get api("/groups", user1), order_by: "path"
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(response_groups).to eq([group1.name, group3.name])
end
end
- end
-
- describe 'GET /groups/owned' do
- context 'when unauthenticated' do
- it 'returns authentication error' do
- get api('/groups/owned')
-
- expect(response).to have_http_status(401)
- end
- end
- context 'when authenticated as group owner' do
+ context 'when using owned in the request' do
it 'returns an array of groups the user owns' do
- get api('/groups/owned', user2)
+ get api('/groups', user2), owned: true
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.first['name']).to eq(group2.name)
end
@@ -290,8 +291,9 @@ describe API::Groups, api: true do
get api("/groups/#{group1.id}/projects", user1)
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response.length).to eq(2)
- project_names = json_response.map { |proj| proj['name' ] }
+ project_names = json_response.map { |proj| proj['name'] }
expect(project_names).to match_array([project1.name, project3.name])
expect(json_response.first['visibility_level']).to be_present
end
@@ -300,8 +302,9 @@ describe API::Groups, api: true do
get api("/groups/#{group1.id}/projects", user1), simple: true
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response.length).to eq(2)
- project_names = json_response.map { |proj| proj['name' ] }
+ project_names = json_response.map { |proj| proj['name'] }
expect(project_names).to match_array([project1.name, project3.name])
expect(json_response.first['visibility_level']).not_to be_present
end
@@ -312,6 +315,7 @@ describe API::Groups, api: true do
get api("/groups/#{group1.id}/projects", user1), visibility: 'public'
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an(Array)
expect(json_response.length).to eq(1)
expect(json_response.first['name']).to eq(public_project.name)
@@ -335,6 +339,7 @@ describe API::Groups, api: true do
get api("/groups/#{group1.id}/projects", user3)
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response.length).to eq(1)
expect(json_response.first['name']).to eq(project3.name)
end
@@ -365,6 +370,7 @@ describe API::Groups, api: true do
get api("/groups/#{group2.id}/projects", admin)
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response.length).to eq(1)
expect(json_response.first['name']).to eq(project2.name)
end
@@ -381,7 +387,8 @@ describe API::Groups, api: true do
get api("/groups/#{group1.path}/projects", admin)
expect(response).to have_http_status(200)
- project_names = json_response.map { |proj| proj['name' ] }
+ expect(response).to include_pagination_headers
+ project_names = json_response.map { |proj| proj['name'] }
expect(project_names).to match_array([project1.name, project3.name])
end
@@ -460,7 +467,7 @@ describe API::Groups, api: true do
it "removes group" do
delete api("/groups/#{group1.id}", user1)
- expect(response).to have_http_status(200)
+ expect(response).to have_http_status(204)
end
it "does not remove a group if not an owner" do
@@ -489,7 +496,7 @@ describe API::Groups, api: true do
it "removes any existing group" do
delete api("/groups/#{group2.id}", admin)
- expect(response).to have_http_status(200)
+ expect(response).to have_http_status(204)
end
it "does not remove a non existing group" do
@@ -502,7 +509,7 @@ describe API::Groups, api: true do
describe "POST /groups/:id/projects/:project_id" do
let(:project) { create(:empty_project) }
- let(:project_path) { "#{project.namespace.path}%2F#{project.path}" }
+ let(:project_path) { project.full_path.gsub('/', '%2F') }
before(:each) do
allow_any_instance_of(Projects::TransferService).
diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb
index cca00df9591..ddc2e51821e 100644
--- a/spec/requests/api/issues_spec.rb
+++ b/spec/requests/api/issues_spec.rb
@@ -68,7 +68,9 @@ describe API::Issues, api: true do
context "when authenticated" do
it "returns an array of issues" do
get api("/issues", user)
+
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
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')
@@ -76,7 +78,9 @@ describe API::Issues, api: true do
it 'returns an array of closed issues' do
get api('/issues?state=closed', user)
+
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.first['id']).to eq(closed_issue.id)
@@ -84,7 +88,9 @@ describe API::Issues, api: true do
it 'returns an array of opened issues' do
get api('/issues?state=opened', user)
+
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.first['id']).to eq(issue.id)
@@ -92,7 +98,9 @@ describe API::Issues, api: true do
it 'returns an array of all issues' do
get api('/issues?state=all', user)
+
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(2)
expect(json_response.first['id']).to eq(issue.id)
@@ -101,31 +109,44 @@ describe API::Issues, api: true do
it 'returns an array of labeled issues' do
get api("/issues?labels=#{label.title}", user)
+
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
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 api("/issues?labels=#{label.title},foo,bar", user)
+ it 'returns an array of labeled issues when all labels matches' do
+ label_b = create(:label, title: 'foo', project: project)
+ label_c = create(:label, title: 'bar', project: project)
+
+ create(:label_link, label: label_b, target: issue)
+ create(:label_link, label: label_c, target: issue)
+
+ get api("/issues", user), labels: "#{label.title},#{label_b.title},#{label_c.title}"
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
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['labels']).to eq([label_c.title, label_b.title, label.title])
end
it 'returns an empty array if no issue matches labels' do
get api('/issues?labels=foo,bar', user)
+
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
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 api("/issues?labels=#{label.title}&state=opened", user)
+
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.first['labels']).to eq([label.title])
@@ -134,7 +155,9 @@ describe API::Issues, api: true do
it 'returns an empty array if no issue matches labels and state filters' do
get api("/issues?labels=#{label.title}&state=closed", user)
+
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(0)
end
@@ -143,6 +166,7 @@ describe API::Issues, api: true do
get api("/issues?milestone=#{empty_milestone.title}", user)
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(0)
end
@@ -151,6 +175,7 @@ describe API::Issues, api: true do
get api("/issues?milestone=foo", user)
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(0)
end
@@ -159,6 +184,7 @@ describe API::Issues, api: true do
get api("/issues?milestone=#{milestone.title}", user)
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(2)
expect(json_response.first['id']).to eq(issue.id)
@@ -170,6 +196,7 @@ describe API::Issues, api: true do
'&state=closed', user)
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.first['id']).to eq(closed_issue.id)
@@ -179,6 +206,7 @@ describe API::Issues, api: true do
get api("/issues?milestone=#{no_milestone_title}", author)
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.first['id']).to eq(confidential_issue.id)
@@ -186,36 +214,40 @@ describe API::Issues, api: true do
it 'sorts by created_at descending by default' do
get api('/issues', user)
- response_dates = json_response.map { |issue| issue['created_at'] }
+ response_dates = json_response.map { |issue| issue['created_at'] }
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(response_dates).to eq(response_dates.sort.reverse)
end
it 'sorts ascending when requested' do
get api('/issues?sort=asc', user)
- response_dates = json_response.map { |issue| issue['created_at'] }
+ response_dates = json_response.map { |issue| issue['created_at'] }
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
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 api('/issues?order_by=updated_at', user)
- response_dates = json_response.map { |issue| issue['updated_at'] }
+ response_dates = json_response.map { |issue| issue['updated_at'] }
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
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 api('/issues?order_by=updated_at&sort=asc', user)
- response_dates = json_response.map { |issue| issue['updated_at'] }
+ response_dates = json_response.map { |issue| issue['updated_at'] }
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(response_dates).to eq(response_dates.sort)
end
@@ -269,6 +301,7 @@ describe API::Issues, api: true do
get api(base_url, non_member)
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.first['title']).to eq(group_issue.title)
@@ -278,6 +311,7 @@ describe API::Issues, api: true do
get api(base_url, author)
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(2)
end
@@ -286,6 +320,7 @@ describe API::Issues, api: true do
get api(base_url, assignee)
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(2)
end
@@ -294,6 +329,7 @@ describe API::Issues, api: true do
get api(base_url, user)
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(2)
end
@@ -302,6 +338,7 @@ describe API::Issues, api: true do
get api(base_url, admin)
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(2)
end
@@ -310,6 +347,7 @@ describe API::Issues, api: true do
get api("#{base_url}?labels=#{group_label.title}", user)
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.first['labels']).to eq([group_label.title])
@@ -319,14 +357,31 @@ describe API::Issues, api: true do
get api("#{base_url}?labels=#{group_label.title},foo,bar", user)
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(0)
end
+ it 'returns an array of labeled issues when all labels matches' do
+ label_b = create(:label, title: 'foo', project: group_project)
+ label_c = create(:label, title: 'bar', project: group_project)
+
+ create(:label_link, label: label_b, target: group_issue)
+ create(:label_link, label: label_c, target: group_issue)
+
+ get api("#{base_url}", user), labels: "#{group_label.title},#{label_b.title},#{label_c.title}"
+
+ 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_c.title, label_b.title, group_label.title])
+ end
+
it 'returns an empty array if no group issue matches labels' do
get api("#{base_url}?labels=foo,bar", user)
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(0)
end
@@ -335,6 +390,7 @@ describe API::Issues, api: true do
get api("#{base_url}?milestone=#{group_empty_milestone.title}", user)
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(0)
end
@@ -343,6 +399,7 @@ describe API::Issues, api: true do
get api("#{base_url}?milestone=foo", user)
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(0)
end
@@ -351,6 +408,7 @@ describe API::Issues, api: true do
get api("#{base_url}?milestone=#{group_milestone.title}", user)
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.first['id']).to eq(group_issue.id)
@@ -361,6 +419,7 @@ describe API::Issues, api: true do
'&state=closed', user)
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
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)
@@ -370,6 +429,7 @@ describe API::Issues, api: true do
get api("#{base_url}?milestone=#{no_milestone_title}", user)
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
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)
@@ -377,36 +437,40 @@ describe API::Issues, api: true do
it 'sorts by created_at descending by default' do
get api(base_url, user)
- response_dates = json_response.map { |issue| issue['created_at'] }
+ response_dates = json_response.map { |issue| issue['created_at'] }
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(response_dates).to eq(response_dates.sort.reverse)
end
it 'sorts ascending when requested' do
get api("#{base_url}?sort=asc", user)
- response_dates = json_response.map { |issue| issue['created_at'] }
+ response_dates = json_response.map { |issue| issue['created_at'] }
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
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 api("#{base_url}?order_by=updated_at", user)
- response_dates = json_response.map { |issue| issue['updated_at'] }
+ response_dates = json_response.map { |issue| issue['updated_at'] }
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
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 api("#{base_url}?order_by=updated_at&sort=asc", user)
- response_dates = json_response.map { |issue| issue['updated_at'] }
+ response_dates = json_response.map { |issue| issue['updated_at'] }
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(response_dates).to eq(response_dates.sort)
end
@@ -430,12 +494,17 @@ describe API::Issues, api: true do
get api("/projects/#{restricted_project.id}/issues", non_member)
+ expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
expect(json_response).to eq([])
end
it 'returns project issues without confidential issues for non project members' do
get api("#{base_url}/issues", non_member)
+
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(2)
expect(json_response.first['title']).to eq(issue.title)
@@ -443,7 +512,9 @@ describe API::Issues, api: true do
it 'returns project issues without confidential issues for project members with guest role' do
get api("#{base_url}/issues", guest)
+
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(2)
expect(json_response.first['title']).to eq(issue.title)
@@ -451,7 +522,9 @@ describe API::Issues, api: true do
it 'returns project confidential issues for author' do
get api("#{base_url}/issues", author)
+
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(3)
expect(json_response.first['title']).to eq(issue.title)
@@ -459,7 +532,9 @@ describe API::Issues, api: true do
it 'returns project confidential issues for assignee' do
get api("#{base_url}/issues", assignee)
+
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(3)
expect(json_response.first['title']).to eq(issue.title)
@@ -467,7 +542,9 @@ describe API::Issues, api: true do
it 'returns project issues with confidential issues for project members' do
get api("#{base_url}/issues", user)
+
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(3)
expect(json_response.first['title']).to eq(issue.title)
@@ -475,7 +552,9 @@ describe API::Issues, api: true do
it 'returns project confidential issues for admin' do
get api("#{base_url}/issues", admin)
+
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(3)
expect(json_response.first['title']).to eq(issue.title)
@@ -483,38 +562,61 @@ describe API::Issues, api: true do
it 'returns an array of labeled project issues' do
get api("#{base_url}/issues?labels=#{label.title}", user)
+
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
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 api("#{base_url}/issues?labels=#{label.title},foo,bar", user)
+ it 'returns an array of labeled issues when all labels matches' do
+ label_b = create(:label, title: 'foo', project: project)
+ label_c = create(:label, title: 'bar', project: project)
+
+ create(:label_link, label: label_b, target: issue)
+ create(:label_link, label: label_c, target: issue)
+
+ get api("#{base_url}/issues", user), labels: "#{label.title},#{label_b.title},#{label_c.title}"
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
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['labels']).to eq([label_c.title, label_b.title, label.title])
+ end
+
+ it 'returns an empty array if not all labels matches' do
+ get api("#{base_url}/issues?labels=#{label.title},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 empty array if no project issue matches labels' do
get api("#{base_url}/issues?labels=foo,bar", user)
+
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
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 api("#{base_url}/issues?milestone=#{empty_milestone.title}", user)
+
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
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("#{base_url}/issues?milestone=foo", user)
+
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(0)
end
@@ -523,6 +625,7 @@ describe API::Issues, api: true do
get api("#{base_url}/issues?milestone=#{milestone.title}", user)
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(2)
expect(json_response.first['id']).to eq(issue.id)
@@ -530,9 +633,10 @@ describe API::Issues, api: true do
end
it 'returns an array of issues matching state in milestone' do
- get api("#{base_url}/issues?milestone=#{milestone.title}"\
- '&state=closed', user)
+ get api("#{base_url}/issues?milestone=#{milestone.title}&state=closed", user)
+
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.first['id']).to eq(closed_issue.id)
@@ -542,6 +646,7 @@ describe API::Issues, api: true do
get api("#{base_url}/issues?milestone=#{no_milestone_title}", user)
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.first['id']).to eq(confidential_issue.id)
@@ -549,36 +654,40 @@ describe API::Issues, api: true do
it 'sorts by created_at descending by default' do
get api("#{base_url}/issues", user)
- response_dates = json_response.map { |issue| issue['created_at'] }
+ response_dates = json_response.map { |issue| issue['created_at'] }
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(response_dates).to eq(response_dates.sort.reverse)
end
it 'sorts ascending when requested' do
get api("#{base_url}/issues?sort=asc", user)
- response_dates = json_response.map { |issue| issue['created_at'] }
+ response_dates = json_response.map { |issue| issue['created_at'] }
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
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 api("#{base_url}/issues?order_by=updated_at", user)
- response_dates = json_response.map { |issue| issue['updated_at'] }
+ response_dates = json_response.map { |issue| issue['updated_at'] }
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
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 api("#{base_url}/issues?order_by=updated_at&sort=asc", user)
- response_dates = json_response.map { |issue| issue['updated_at'] }
+ response_dates = json_response.map { |issue| issue['updated_at'] }
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(response_dates).to eq(response_dates.sort)
end
@@ -666,7 +775,7 @@ describe API::Issues, api: true do
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['labels']).to eq(%w(label label2))
expect(json_response['confidential']).to be_falsy
end
@@ -919,6 +1028,33 @@ describe API::Issues, api: true do
end
end
+ describe 'PUT /projects/:id/issues/:issue_id with spam filtering' do
+ let(:params) do
+ {
+ title: 'updated title',
+ description: 'content here',
+ labels: 'label, label2'
+ }
+ end
+
+ it "does not create a new project issue" do
+ allow_any_instance_of(SpamService).to receive_messages(check_for_spam?: true)
+ allow_any_instance_of(AkismetService).to receive_messages(is_spam?: true)
+
+ put api("/projects/#{project.id}/issues/#{issue.id}", user), params
+
+ 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('updated title')
+ 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 labels' do
let!(:label) { create(:label, title: 'dummy', project: project) }
let!(:label_link) { create(:label_link, label: label, target: issue) }
@@ -1039,8 +1175,8 @@ describe API::Issues, api: true do
it "deletes the issue if an admin requests it" do
delete api("/projects/#{project.id}/issues/#{issue.id}", owner)
- expect(response).to have_http_status(200)
- expect(json_response['state']).to eq 'opened'
+
+ expect(response).to have_http_status(204)
end
end
@@ -1123,55 +1259,55 @@ describe API::Issues, api: true do
end
end
- describe 'POST :id/issues/:issue_id/subscription' do
+ describe 'POST :id/issues/:issue_id/subscribe' do
it 'subscribes to an issue' do
- post api("/projects/#{project.id}/issues/#{issue.id}/subscription", user2)
+ post api("/projects/#{project.id}/issues/#{issue.id}/subscribe", user2)
expect(response).to have_http_status(201)
expect(json_response['subscribed']).to eq(true)
end
it 'returns 304 if already subscribed' do
- post api("/projects/#{project.id}/issues/#{issue.id}/subscription", user)
+ post api("/projects/#{project.id}/issues/#{issue.id}/subscribe", user)
expect(response).to have_http_status(304)
end
it 'returns 404 if the issue is not found' do
- post api("/projects/#{project.id}/issues/123/subscription", user)
+ post api("/projects/#{project.id}/issues/123/subscribe", user)
expect(response).to have_http_status(404)
end
it 'returns 404 if the issue is confidential' do
- post api("/projects/#{project.id}/issues/#{confidential_issue.id}/subscription", non_member)
+ post api("/projects/#{project.id}/issues/#{confidential_issue.id}/subscribe", non_member)
expect(response).to have_http_status(404)
end
end
- describe 'DELETE :id/issues/:issue_id/subscription' do
+ describe 'POST :id/issues/:issue_id/unsubscribe' do
it 'unsubscribes from an issue' do
- delete api("/projects/#{project.id}/issues/#{issue.id}/subscription", user)
+ post api("/projects/#{project.id}/issues/#{issue.id}/unsubscribe", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_http_status(201)
expect(json_response['subscribed']).to eq(false)
end
it 'returns 304 if not subscribed' do
- delete api("/projects/#{project.id}/issues/#{issue.id}/subscription", user2)
+ post api("/projects/#{project.id}/issues/#{issue.id}/unsubscribe", user2)
expect(response).to have_http_status(304)
end
it 'returns 404 if the issue is not found' do
- delete api("/projects/#{project.id}/issues/123/subscription", user)
+ post api("/projects/#{project.id}/issues/123/unsubscribe", user)
expect(response).to have_http_status(404)
end
it 'returns 404 if the issue is confidential' do
- delete api("/projects/#{project.id}/issues/#{confidential_issue.id}/subscription", non_member)
+ post api("/projects/#{project.id}/issues/#{confidential_issue.id}/unsubscribe", non_member)
expect(response).to have_http_status(404)
end
diff --git a/spec/requests/api/labels_spec.rb b/spec/requests/api/labels_spec.rb
index a8cd787f398..a1adaba7b98 100644
--- a/spec/requests/api/labels_spec.rb
+++ b/spec/requests/api/labels_spec.rb
@@ -21,15 +21,16 @@ describe API::Labels, api: true do
create(:labeled_issue, project: project, labels: [label1], author: user, state: :closed)
create(:labeled_merge_request, labels: [priority_label], author: user, source_project: project )
- expected_keys = [
- 'id', 'name', 'color', 'description',
- 'open_issues_count', 'closed_issues_count', 'open_merge_requests_count',
- 'subscribed', 'priority'
- ]
+ expected_keys = %w(
+ id name color description
+ open_issues_count closed_issues_count open_merge_requests_count
+ subscribed priority
+ )
get api("/projects/#{project.id}/labels", user)
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.size).to eq(3)
expect(json_response.first.keys).to match_array expected_keys
@@ -174,9 +175,10 @@ describe API::Labels, api: true do
end
describe 'DELETE /projects/:id/labels' do
- it 'returns 200 for existing label' do
+ it 'returns 204 for existing label' do
delete api("/projects/#{project.id}/labels", user), name: 'label1'
- expect(response).to have_http_status(200)
+
+ expect(response).to have_http_status(204)
end
it 'returns 404 for non existing label' do
@@ -317,10 +319,10 @@ describe API::Labels, api: true do
end
end
- describe "POST /projects/:id/labels/:label_id/subscription" do
+ describe "POST /projects/:id/labels/:label_id/subscribe" do
context "when label_id is a label title" do
it "subscribes to the label" do
- post api("/projects/#{project.id}/labels/#{label1.title}/subscription", user)
+ post api("/projects/#{project.id}/labels/#{label1.title}/subscribe", user)
expect(response).to have_http_status(201)
expect(json_response["name"]).to eq(label1.title)
@@ -330,7 +332,7 @@ describe API::Labels, api: true do
context "when label_id is a label ID" do
it "subscribes to the label" do
- post api("/projects/#{project.id}/labels/#{label1.id}/subscription", user)
+ post api("/projects/#{project.id}/labels/#{label1.id}/subscribe", user)
expect(response).to have_http_status(201)
expect(json_response["name"]).to eq(label1.title)
@@ -342,7 +344,7 @@ describe API::Labels, api: true do
before { label1.subscribe(user, project) }
it "returns 304" do
- post api("/projects/#{project.id}/labels/#{label1.id}/subscription", user)
+ post api("/projects/#{project.id}/labels/#{label1.id}/subscribe", user)
expect(response).to have_http_status(304)
end
@@ -350,21 +352,21 @@ describe API::Labels, api: true do
context "when label ID is not found" do
it "returns 404 error" do
- post api("/projects/#{project.id}/labels/1234/subscription", user)
+ post api("/projects/#{project.id}/labels/1234/subscribe", user)
expect(response).to have_http_status(404)
end
end
end
- describe "DELETE /projects/:id/labels/:label_id/subscription" do
+ describe "POST /projects/:id/labels/:label_id/unsubscribe" do
before { label1.subscribe(user, project) }
context "when label_id is a label title" do
it "unsubscribes from the label" do
- delete api("/projects/#{project.id}/labels/#{label1.title}/subscription", user)
+ post api("/projects/#{project.id}/labels/#{label1.title}/unsubscribe", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_http_status(201)
expect(json_response["name"]).to eq(label1.title)
expect(json_response["subscribed"]).to be_falsey
end
@@ -372,9 +374,9 @@ describe API::Labels, api: true do
context "when label_id is a label ID" do
it "unsubscribes from the label" do
- delete api("/projects/#{project.id}/labels/#{label1.id}/subscription", user)
+ post api("/projects/#{project.id}/labels/#{label1.id}/unsubscribe", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_http_status(201)
expect(json_response["name"]).to eq(label1.title)
expect(json_response["subscribed"]).to be_falsey
end
@@ -384,7 +386,7 @@ describe API::Labels, api: true do
before { label1.unsubscribe(user, project) }
it "returns 304" do
- delete api("/projects/#{project.id}/labels/#{label1.id}/subscription", user)
+ post api("/projects/#{project.id}/labels/#{label1.id}/unsubscribe", user)
expect(response).to have_http_status(304)
end
@@ -392,7 +394,7 @@ describe API::Labels, api: true do
context "when label ID is not found" do
it "returns 404 error" do
- delete api("/projects/#{project.id}/labels/1234/subscription", user)
+ post api("/projects/#{project.id}/labels/1234/unsubscribe", user)
expect(response).to have_http_status(404)
end
diff --git a/spec/requests/api/members_spec.rb b/spec/requests/api/members_spec.rb
index 3e9bcfd1a60..2d37d026a39 100644
--- a/spec/requests/api/members_spec.rb
+++ b/spec/requests/api/members_spec.rb
@@ -34,9 +34,12 @@ describe API::Members, api: true do
context "when authenticated as a #{type}" do
it 'returns 200' do
user = public_send(type)
+
get api("/#{source_type.pluralize}/#{source.id}/members", user)
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
expect(json_response.size).to eq(2)
expect(json_response.map { |u| u['id'] }).to match_array [master.id, developer.id]
end
@@ -49,6 +52,8 @@ describe API::Members, api: true do
get api("/#{source_type.pluralize}/#{source.id}/members", developer)
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
expect(json_response.size).to eq(2)
expect(json_response.map { |u| u['id'] }).to match_array [master.id, developer.id]
end
@@ -57,6 +62,8 @@ describe API::Members, api: true do
get api("/#{source_type.pluralize}/#{source.id}/members", developer), query: master.username
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
expect(json_response.count).to eq(1)
expect(json_response.first['username']).to eq(master.username)
end
@@ -166,11 +173,11 @@ describe API::Members, api: true do
expect(response).to have_http_status(400)
end
- it 'returns 422 when access_level is not valid' do
+ it 'returns 400 when access_level is not valid' do
post api("/#{source_type.pluralize}/#{source.id}/members", master),
user_id: stranger.id, access_level: 1234
- expect(response).to have_http_status(422)
+ expect(response).to have_http_status(400)
end
end
end
@@ -223,11 +230,11 @@ describe API::Members, api: true do
expect(response).to have_http_status(400)
end
- it 'returns 422 when access level is not valid' do
+ it 'returns 400 when access level is not valid' do
put api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", master),
access_level: 1234
- expect(response).to have_http_status(422)
+ expect(response).to have_http_status(400)
end
end
end
@@ -256,18 +263,18 @@ describe API::Members, api: true do
expect do
delete api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", developer)
- expect(response).to have_http_status(200)
+ expect(response).to have_http_status(204)
end.to change { source.members.count }.by(-1)
end
end
context 'when authenticated as a master/owner' do
context 'and member is a requester' do
- it "returns #{source_type == 'project' ? 200 : 404}" do
+ it 'returns 404' do
expect do
delete api("/#{source_type.pluralize}/#{source.id}/members/#{access_requester.id}", master)
- expect(response).to have_http_status(source_type == 'project' ? 200 : 404)
+ expect(response).to have_http_status(404)
end.not_to change { source.requesters.count }
end
end
@@ -276,15 +283,15 @@ describe API::Members, api: true do
expect do
delete api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", master)
- expect(response).to have_http_status(200)
+ expect(response).to have_http_status(204)
end.to change { source.members.count }.by(-1)
end
end
- it "returns #{source_type == 'project' ? 200 : 404} if member does not exist" do
+ it 'returns 404 if member does not exist' do
delete api("/#{source_type.pluralize}/#{source.id}/members/123", master)
- expect(response).to have_http_status(source_type == 'project' ? 200 : 404)
+ expect(response).to have_http_status(404)
end
end
end
@@ -335,7 +342,7 @@ describe API::Members, api: true do
post api("/projects/#{project.id}/members", master),
user_id: stranger.id, access_level: Member::OWNER
- expect(response).to have_http_status(422)
+ expect(response).to have_http_status(400)
end.to change { project.members.count }.by(0)
end
end
diff --git a/spec/requests/api/merge_request_diffs_spec.rb b/spec/requests/api/merge_request_diffs_spec.rb
index e1887138aab..1d02e827183 100644
--- a/spec/requests/api/merge_request_diffs_spec.rb
+++ b/spec/requests/api/merge_request_diffs_spec.rb
@@ -19,6 +19,8 @@ describe API::MergeRequestDiffs, 'MergeRequestDiffs', api: true do
merge_request_diff = merge_request.merge_request_diffs.first
expect(response.status).to eq 200
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
expect(json_response.size).to eq(merge_request.merge_request_diffs.size)
expect(json_response.first['id']).to eq(merge_request_diff.id)
expect(json_response.first['head_commit_sha']).to eq(merge_request_diff.head_commit_sha)
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index ff10e79e417..5522154899c 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -27,7 +27,9 @@ describe API::MergeRequests, api: true do
context "when authenticated" do
it "returns an array of all merge_requests" do
get api("/projects/#{project.id}/merge_requests", user)
+
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(3)
expect(json_response.last['title']).to eq(merge_request.title)
@@ -43,7 +45,9 @@ describe API::MergeRequests, api: true do
it "returns an array of all merge_requests" do
get api("/projects/#{project.id}/merge_requests?state", user)
+
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(3)
expect(json_response.last['title']).to eq(merge_request.title)
@@ -51,7 +55,9 @@ describe API::MergeRequests, api: true do
it "returns an array of open merge_requests" do
get api("/projects/#{project.id}/merge_requests?state=opened", user)
+
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.last['title']).to eq(merge_request.title)
@@ -59,7 +65,9 @@ describe API::MergeRequests, api: true do
it "returns an array of closed merge_requests" do
get api("/projects/#{project.id}/merge_requests?state=closed", user)
+
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
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)
@@ -67,7 +75,9 @@ describe API::MergeRequests, api: true do
it "returns an array of merged merge_requests" do
get api("/projects/#{project.id}/merge_requests?state=merged", user)
+
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
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)
@@ -91,7 +101,9 @@ describe API::MergeRequests, api: true do
it "returns an array of merge_requests in ascending order" do
get api("/projects/#{project.id}/merge_requests?sort=asc", user)
+
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
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'] }
@@ -100,7 +112,9 @@ describe API::MergeRequests, api: true do
it "returns an array of merge_requests in descending order" do
get api("/projects/#{project.id}/merge_requests?sort=desc", user)
+
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
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'] }
@@ -109,7 +123,9 @@ describe API::MergeRequests, api: true do
it "returns an array of merge_requests ordered by updated_at" do
get api("/projects/#{project.id}/merge_requests?order_by=updated_at", user)
+
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
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'] }
@@ -118,7 +134,9 @@ describe API::MergeRequests, api: true do
it "returns an array of merge_requests ordered by created_at" do
get api("/projects/#{project.id}/merge_requests?order_by=created_at&sort=asc", user)
+
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
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'] }
@@ -191,6 +209,8 @@ describe API::MergeRequests, api: true do
commit = merge_request.commits.first
expect(response.status).to eq 200
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
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)
@@ -205,6 +225,7 @@ describe API::MergeRequests, api: true do
describe 'GET /projects/:id/merge_requests/:merge_request_id/changes' do
it 'returns the change information of the merge_request' do
get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/changes", user)
+
expect(response.status).to eq 200
expect(json_response['changes'].size).to eq(merge_request.diffs.size)
end
@@ -229,7 +250,7 @@ describe API::MergeRequests, api: true do
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['labels']).to eq(%w(label label2))
expect(json_response['milestone']['id']).to eq(milestone.id)
expect(json_response['force_remove_source_branch']).to be_truthy
end
@@ -390,7 +411,7 @@ describe API::MergeRequests, api: true do
it "destroys the merge request owners can destroy" do
delete api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_http_status(204)
end
end
end
@@ -572,7 +593,9 @@ describe API::MergeRequests, api: true do
it "returns merge_request comments ordered by created_at" do
get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/comments", user)
+
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
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")
@@ -594,7 +617,9 @@ describe API::MergeRequests, api: true do
end
get api("/projects/#{project.id}/merge_requests/#{mr.id}/closes_issues", user)
+
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.first['id']).to eq(issue.id)
@@ -602,7 +627,9 @@ describe API::MergeRequests, api: true do
it 'returns an empty array when there are no issues to be closed' do
get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/closes_issues", user)
+
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(0)
end
@@ -616,6 +643,7 @@ describe API::MergeRequests, api: true do
get api("/projects/#{jira_project.id}/merge_requests/#{merge_request.id}/closes_issues", user)
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.first['title']).to eq(issue.title)
@@ -634,22 +662,22 @@ describe API::MergeRequests, api: true do
end
end
- describe 'POST :id/merge_requests/:merge_request_id/subscription' do
+ describe 'POST :id/merge_requests/:merge_request_id/subscribe' do
it 'subscribes to a merge request' do
- post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", admin)
+ post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscribe", admin)
expect(response).to have_http_status(201)
expect(json_response['subscribed']).to eq(true)
end
it 'returns 304 if already subscribed' do
- post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", user)
+ post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscribe", user)
expect(response).to have_http_status(304)
end
it 'returns 404 if the merge request is not found' do
- post api("/projects/#{project.id}/merge_requests/123/subscription", user)
+ post api("/projects/#{project.id}/merge_requests/123/subscribe", user)
expect(response).to have_http_status(404)
end
@@ -658,28 +686,28 @@ describe API::MergeRequests, api: true do
guest = create(:user)
project.team << [guest, :guest]
- post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", guest)
+ post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscribe", guest)
expect(response).to have_http_status(403)
end
end
- describe 'DELETE :id/merge_requests/:merge_request_id/subscription' do
+ describe 'POST :id/merge_requests/:merge_request_id/unsubscribe' do
it 'unsubscribes from a merge request' do
- delete api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", user)
+ post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/unsubscribe", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_http_status(201)
expect(json_response['subscribed']).to eq(false)
end
it 'returns 304 if not subscribed' do
- delete api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", admin)
+ post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/unsubscribe", admin)
expect(response).to have_http_status(304)
end
it 'returns 404 if the merge request is not found' do
- post api("/projects/#{project.id}/merge_requests/123/subscription", user)
+ post api("/projects/#{project.id}/merge_requests/123/unsubscribe", user)
expect(response).to have_http_status(404)
end
@@ -688,7 +716,7 @@ describe API::MergeRequests, api: true do
guest = create(:user)
project.team << [guest, :guest]
- delete api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", guest)
+ post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/unsubscribe", guest)
expect(response).to have_http_status(403)
end
diff --git a/spec/requests/api/milestones_spec.rb b/spec/requests/api/milestones_spec.rb
index 8beef821d6c..418bf5a507c 100644
--- a/spec/requests/api/milestones_spec.rb
+++ b/spec/requests/api/milestones_spec.rb
@@ -14,6 +14,7 @@ describe API::Milestones, api: true do
get api("/projects/#{project.id}/milestones", user)
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.first['title']).to eq(milestone.title)
end
@@ -28,6 +29,7 @@ describe API::Milestones, api: true do
get api("/projects/#{project.id}/milestones?state=active", user)
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.first['id']).to eq(milestone.id)
@@ -37,25 +39,18 @@ describe API::Milestones, api: true do
get api("/projects/#{project.id}/milestones?state=closed", user)
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.first['id']).to eq(closed_milestone.id)
end
- end
-
- describe 'GET /projects/:id/milestones/:milestone_id' do
- it 'returns a project milestone by id' do
- get api("/projects/#{project.id}/milestones/#{milestone.id}", user)
-
- expect(response).to have_http_status(200)
- expect(json_response['title']).to eq(milestone.title)
- expect(json_response['iid']).to eq(milestone.iid)
- end
it 'returns a project milestone by iid' do
get api("/projects/#{project.id}/milestones?iid=#{closed_milestone.iid}", user)
expect(response.status).to eq 200
+ expect(response).to include_pagination_headers
+ expect(json_response.size).to eq(1)
expect(json_response.size).to eq(1)
expect(json_response.first['title']).to eq closed_milestone.title
expect(json_response.first['id']).to eq closed_milestone.id
@@ -70,6 +65,26 @@ describe API::Milestones, api: true do
expect(json_response.first['id']).to eq milestone.id
end
+ it 'returns a project milestone by iid array' do
+ get api("/projects/#{project.id}/milestones", user), iid: [milestone.iid, closed_milestone.iid]
+
+ expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response.size).to eq(2)
+ expect(json_response.first['title']).to eq milestone.title
+ expect(json_response.first['id']).to eq milestone.id
+ end
+ end
+
+ describe 'GET /projects/:id/milestones/:milestone_id' do
+ it 'returns a project milestone by id' do
+ get api("/projects/#{project.id}/milestones/#{milestone.id}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['title']).to eq(milestone.title)
+ expect(json_response['iid']).to eq(milestone.iid)
+ end
+
it 'returns 401 error if user not authenticated' do
get api("/projects/#{project.id}/milestones/#{milestone.id}")
@@ -177,6 +192,7 @@ describe API::Milestones, api: true do
get api("/projects/#{project.id}/milestones/#{milestone.id}/issues", user)
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.first['milestone']['title']).to eq(milestone.title)
end
@@ -202,6 +218,7 @@ describe API::Milestones, api: true do
get api("/projects/#{public_project.id}/milestones/#{milestone.id}/issues", user)
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.size).to eq(2)
expect(json_response.map { |issue| issue['id'] }).to include(issue.id, confidential_issue.id)
@@ -214,6 +231,7 @@ describe API::Milestones, api: true do
get api("/projects/#{public_project.id}/milestones/#{milestone.id}/issues", member)
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.size).to eq(1)
expect(json_response.map { |issue| issue['id'] }).to include(issue.id)
@@ -223,10 +241,47 @@ describe API::Milestones, api: true do
get api("/projects/#{public_project.id}/milestones/#{milestone.id}/issues", create(:user))
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.size).to eq(1)
expect(json_response.map { |issue| issue['id'] }).to include(issue.id)
end
end
end
+
+ describe 'GET /projects/:id/milestones/:milestone_id/merge_requests' do
+ let(:merge_request) { create(:merge_request, source_project: project) }
+ before do
+ milestone.merge_requests << merge_request
+ end
+
+ it 'returns project merge_requests for a particular milestone' do
+ get api("/projects/#{project.id}/milestones/#{milestone.id}/merge_requests", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.size).to eq(1)
+ expect(json_response.first['title']).to eq(merge_request.title)
+ expect(json_response.first['milestone']['title']).to eq(milestone.title)
+ end
+
+ it 'returns a 404 error if milestone id not found' do
+ get api("/projects/#{project.id}/milestones/1234/merge_requests", user)
+
+ expect(response).to have_http_status(404)
+ end
+
+ it 'returns a 404 if the user has no access to the milestone' do
+ new_user = create :user
+ get api("/projects/#{project.id}/milestones/#{milestone.id}/merge_requests", new_user)
+
+ expect(response).to have_http_status(404)
+ end
+
+ it 'returns a 401 error if user not authenticated' do
+ get api("/projects/#{project.id}/milestones/#{milestone.id}/merge_requests")
+
+ expect(response).to have_http_status(401)
+ end
+ end
end
diff --git a/spec/requests/api/namespaces_spec.rb b/spec/requests/api/namespaces_spec.rb
index a945d56f529..da8fa06d0af 100644
--- a/spec/requests/api/namespaces_spec.rb
+++ b/spec/requests/api/namespaces_spec.rb
@@ -18,17 +18,19 @@ describe API::Namespaces, api: true do
context "when authenticated as admin" do
it "admin: returns an array of all namespaces" do
get api("/namespaces", admin)
+
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
-
expect(json_response.length).to eq(Namespace.count)
end
it "admin: returns an array of matched namespaces" do
get api("/namespaces?search=#{group2.name}", admin)
+
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
-
expect(json_response.length).to eq(1)
expect(json_response.last['path']).to eq(group2.path)
expect(json_response.last['full_path']).to eq(group2.full_path)
@@ -38,17 +40,19 @@ describe API::Namespaces, api: true do
context "when authenticated as a regular user" do
it "user: returns an array of namespaces" do
get api("/namespaces", user)
+
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
-
expect(json_response.length).to eq(1)
end
it "admin: returns an array of matched namespaces" do
get api("/namespaces?search=#{user.username}", user)
+
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
-
expect(json_response.length).to eq(1)
end
end
diff --git a/spec/requests/api/notes_spec.rb b/spec/requests/api/notes_spec.rb
index 0353ebea9e5..9d3c821b692 100644
--- a/spec/requests/api/notes_spec.rb
+++ b/spec/requests/api/notes_spec.rb
@@ -32,15 +32,12 @@ describe API::Notes, api: true do
before { project.team << [user, :reporter] }
describe "GET /projects/:id/noteable/:noteable_id/notes" do
- it_behaves_like 'a paginated resources' do
- let(:request) { get api("/projects/#{project.id}/issues/#{issue.id}/notes", user) }
- end
-
context "when noteable is an Issue" do
it "returns an array of issue notes" do
get api("/projects/#{project.id}/issues/#{issue.id}/notes", user)
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.first['body']).to eq(issue_note.note)
end
@@ -56,6 +53,7 @@ describe API::Notes, api: true do
get api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes", user)
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response).to be_empty
end
@@ -75,6 +73,7 @@ describe API::Notes, api: true do
get api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes", private_user)
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.first['body']).to eq(cross_reference_note.note)
end
@@ -87,6 +86,7 @@ describe API::Notes, api: true do
get api("/projects/#{project.id}/snippets/#{snippet.id}/notes", user)
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.first['body']).to eq(snippet_note.note)
end
@@ -109,6 +109,7 @@ describe API::Notes, api: true do
get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/notes", user)
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.first['body']).to eq(merge_request_note.note)
end
@@ -372,7 +373,7 @@ describe API::Notes, api: true do
delete api("/projects/#{project.id}/issues/#{issue.id}/"\
"notes/#{issue_note.id}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_http_status(204)
# Check if note is really deleted
delete api("/projects/#{project.id}/issues/#{issue.id}/"\
"notes/#{issue_note.id}", user)
@@ -391,7 +392,7 @@ describe API::Notes, api: true do
delete api("/projects/#{project.id}/snippets/#{snippet.id}/"\
"notes/#{snippet_note.id}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_http_status(204)
# Check if note is really deleted
delete api("/projects/#{project.id}/snippets/#{snippet.id}/"\
"notes/#{snippet_note.id}", user)
@@ -411,7 +412,7 @@ describe API::Notes, api: true do
delete api("/projects/#{project.id}/merge_requests/"\
"#{merge_request.id}/notes/#{merge_request_note.id}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_http_status(204)
# Check if note is really deleted
delete api("/projects/#{project.id}/merge_requests/"\
"#{merge_request.id}/notes/#{merge_request_note.id}", user)
diff --git a/spec/requests/api/pipelines_spec.rb b/spec/requests/api/pipelines_spec.rb
index b7a0b5a9e13..98d004b572e 100644
--- a/spec/requests/api/pipelines_spec.rb
+++ b/spec/requests/api/pipelines_spec.rb
@@ -15,15 +15,12 @@ describe API::Pipelines, api: true do
before { project.team << [user, :master] }
describe 'GET /projects/:id/pipelines ' do
- it_behaves_like 'a paginated resources' do
- let(:request) { get api("/projects/#{project.id}/pipelines", user) }
- end
-
context 'authorized user' do
it 'returns project pipelines' do
get api("/projects/#{project.id}/pipelines", user)
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.first['sha']).to match /\A\h{40}\z/
expect(json_response.first['id']).to eq pipeline.id
diff --git a/spec/requests/api/project_hooks_spec.rb b/spec/requests/api/project_hooks_spec.rb
index f4973d71088..f286568547d 100644
--- a/spec/requests/api/project_hooks_spec.rb
+++ b/spec/requests/api/project_hooks_spec.rb
@@ -25,6 +25,7 @@ describe API::ProjectHooks, 'ProjectHooks', api: true do
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
+ expect(response).to include_pagination_headers
expect(json_response.count).to eq(1)
expect(json_response.first['url']).to eq("http://example.com")
expect(json_response.first['issues_events']).to eq(true)
@@ -182,13 +183,9 @@ describe API::ProjectHooks, 'ProjectHooks', api: true do
it "deletes hook from project" do
expect do
delete api("/projects/#{project.id}/hooks/#{hook.id}", user)
- end.to change {project.hooks.count}.by(-1)
- expect(response).to have_http_status(200)
- end
- it "returns success when deleting hook" do
- delete api("/projects/#{project.id}/hooks/#{hook.id}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_http_status(204)
+ end.to change {project.hooks.count}.by(-1)
end
it "returns a 404 error when deleting non existent hook" do
diff --git a/spec/requests/api/project_snippets_spec.rb b/spec/requests/api/project_snippets_spec.rb
index eea76c7bb94..2c4602faf2c 100644
--- a/spec/requests/api/project_snippets_spec.rb
+++ b/spec/requests/api/project_snippets_spec.rb
@@ -16,9 +16,11 @@ describe API::ProjectSnippets, api: true do
internal_snippet = create(:project_snippet, :internal, project: project)
private_snippet = create(:project_snippet, :private, project: project)
- get api("/projects/#{project.id}/snippets/", user)
+ get api("/projects/#{project.id}/snippets", user)
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
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')
@@ -28,7 +30,10 @@ describe API::ProjectSnippets, api: true do
create(:project_snippet, :private, project: project)
get api("/projects/#{project.id}/snippets/", user)
+
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
expect(json_response.size).to eq(0)
end
end
@@ -73,43 +78,33 @@ describe API::ProjectSnippets, api: true 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
+ 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 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
+ 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)
+ expect(json_response['message']).to eq({ "error" => "Spam detected" })
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
+ it 'creates a spam log' do
+ expect { create_snippet(project, visibility_level: Snippet::PUBLIC) }.
+ to change { SpamLog.count }.by(1)
end
end
end
end
describe 'PUT /projects/:project_id/snippets/:id/' do
- let(:snippet) { create(:project_snippet, author: admin) }
+ let(:visibility_level) { Snippet::PUBLIC }
+ let(:snippet) { create(:project_snippet, author: admin, visibility_level: visibility_level) }
it 'updates snippet' do
new_content = 'New content'
@@ -133,6 +128,56 @@ describe API::ProjectSnippets, api: true do
expect(response).to have_http_status(400)
end
+
+ context 'when the snippet is spam' do
+ def update_snippet(snippet_params = {})
+ put api("/projects/#{snippet.project.id}/snippets/#{snippet.id}", admin), 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
+ let(:visibility_level) { Snippet::PRIVATE }
+
+ it 'creates the snippet' do
+ expect { update_snippet(title: 'Foo') }.
+ to change { snippet.reload.title }.to('Foo')
+ end
+ end
+
+ context 'when the snippet is public' do
+ let(:visibility_level) { Snippet::PUBLIC }
+
+ it 'rejects the snippet' do
+ expect { update_snippet(title: 'Foo') }.
+ not_to change { snippet.reload.title }
+ end
+
+ it 'creates a spam log' do
+ expect { update_snippet(title: 'Foo') }.
+ to change { SpamLog.count }.by(1)
+ end
+ end
+
+ context 'when the private snippet is made public' do
+ let(:visibility_level) { Snippet::PRIVATE }
+
+ it 'rejects the snippet' do
+ expect { update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC) }.
+ not_to change { snippet.reload.title }
+
+ expect(response).to have_http_status(400)
+ expect(json_response['message']).to eq({ "error" => "Spam detected" })
+ end
+
+ it 'creates a spam log' do
+ expect { update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC) }.
+ to change { SpamLog.count }.by(1)
+ end
+ end
+ end
end
describe 'DELETE /projects/:project_id/snippets/:id/' do
@@ -144,7 +189,7 @@ describe API::ProjectSnippets, api: true do
delete api("/projects/#{snippet.project.id}/snippets/#{snippet.id}/", admin)
- expect(response).to have_http_status(200)
+ expect(response).to have_http_status(204)
end
it 'returns 404 for invalid snippet id' do
@@ -167,7 +212,7 @@ describe API::ProjectSnippets, api: true do
end
it 'returns 404 for invalid snippet id' do
- delete api("/projects/#{snippet.project.id}/snippets/1234", admin)
+ get api("/projects/#{snippet.project.id}/snippets/1234/raw", admin)
expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 Snippet Not Found')
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 741815a780e..3a00d974633 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -76,6 +76,7 @@ describe API::Projects, api: true do
get api('/projects', user)
expect(response.status).to eq 200
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.first.keys).to include('tag_list')
end
@@ -84,6 +85,7 @@ describe API::Projects, api: true do
get api('/projects', user)
expect(response.status).to eq 200
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.first.keys).to include('open_issues_count')
end
@@ -94,6 +96,7 @@ describe API::Projects, api: true do
get api('/projects', user)
expect(response.status).to eq 200
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.find { |hash| hash['id'] == project.id }.keys).not_to include('open_issues_count')
end
@@ -102,6 +105,7 @@ describe API::Projects, api: true do
get api('/projects', user)
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.first).not_to include('statistics')
end
@@ -110,17 +114,19 @@ describe API::Projects, api: true do
get api('/projects', user), statistics: true
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.first).to include 'statistics'
end
context 'and with 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"]
+ expected_keys = %w(id http_url_to_repo web_url name name_with_namespace path path_with_namespace)
get api('/projects?simple=true', user)
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.first.keys).to match_array expected_keys
end
@@ -131,6 +137,7 @@ describe API::Projects, api: true do
get api('/projects', user), { search: project.name }
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
end
@@ -141,6 +148,7 @@ describe API::Projects, api: true do
get api('/projects', user), { visibility: 'private' }
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.map { |p| p['id'] }).to contain_exactly(project.id, project2.id, project3.id)
end
@@ -151,6 +159,7 @@ describe API::Projects, api: true do
get api('/projects', user), { visibility: 'internal' }
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.map { |p| p['id'] }).to contain_exactly(project2.id)
end
@@ -159,6 +168,7 @@ describe API::Projects, api: true do
get api('/projects', user), { visibility: 'public' }
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.map { |p| p['id'] }).to contain_exactly(public_project.id)
end
@@ -169,6 +179,7 @@ describe API::Projects, api: true do
get api('/projects', user), { order_by: 'id', sort: 'desc' }
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.first['id']).to eq(project3.id)
end
@@ -179,6 +190,7 @@ describe API::Projects, api: true do
get api('/projects', user4), owned: true
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
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)
@@ -197,6 +209,7 @@ describe API::Projects, api: true do
get api('/projects', user3), starred: true
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.map { |project| project['id'] }).to contain_exactly(project.id, public_project.id)
end
@@ -223,6 +236,7 @@ describe API::Projects, api: true do
get api('/projects', user), { visibility: 'public', owned: true, starred: true, search: 'gitlab' }
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.size).to eq(1)
expect(json_response.first['id']).to eq(project5.id)
@@ -255,10 +269,37 @@ describe API::Projects, api: true do
end
end
- it 'creates new project without path and return 201' do
- expect { post api('/projects', user), name: 'foo' }.
+ it 'creates new project without path but with name and returns 201' do
+ expect { post api('/projects', user), name: 'Foo Project' }.
+ to change { Project.count }.by(1)
+ expect(response).to have_http_status(201)
+
+ project = Project.first
+
+ expect(project.name).to eq('Foo Project')
+ expect(project.path).to eq('foo-project')
+ end
+
+ it 'creates new project without name but with path and returns 201' do
+ expect { post api('/projects', user), path: 'foo_project' }.
+ to change { Project.count }.by(1)
+ expect(response).to have_http_status(201)
+
+ project = Project.first
+
+ expect(project.name).to eq('foo_project')
+ expect(project.path).to eq('foo_project')
+ end
+
+ it 'creates new project name and path and returns 201' do
+ expect { post api('/projects', user), path: 'foo-Project', name: 'Foo Project' }.
to change { Project.count }.by(1)
expect(response).to have_http_status(201)
+
+ project = Project.first
+
+ expect(project.name).to eq('Foo Project')
+ expect(project.path).to eq('foo-Project')
end
it 'creates last project before reaching project limit' do
@@ -267,7 +308,7 @@ describe API::Projects, api: true do
expect(response).to have_http_status(201)
end
- it 'does not create new project without name and return 400' do
+ it 'does not create new project without name or path and returns 400' do
expect { post api('/projects', user) }.not_to change { Project.count }
expect(response).to have_http_status(400)
end
@@ -644,9 +685,10 @@ describe API::Projects, api: true do
get api("/projects/#{project.id}/events", current_user)
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
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!')
@@ -699,11 +741,11 @@ describe API::Projects, api: true do
get api("/projects/#{project.id}/users", current_user)
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
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])
@@ -746,7 +788,9 @@ describe API::Projects, api: true do
it 'returns an array of project snippets' do
get api("/projects/#{project.id}/snippets", user)
+
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.first['title']).to eq(snippet.title)
end
@@ -803,8 +847,9 @@ describe API::Projects, api: true do
it 'deletes existing project snippet' do
expect do
delete api("/projects/#{project.id}/snippets/#{snippet.id}", user)
+
+ expect(response).to have_http_status(204)
end.to change { Snippet.count }.by(-1)
- expect(response).to have_http_status(200)
end
it 'returns 404 when deleting unknown snippet id' do
@@ -888,8 +933,10 @@ describe API::Projects, api: true do
project_fork_target.reload
expect(project_fork_target.forked_from_project).not_to be_nil
expect(project_fork_target.forked?).to be_truthy
+
delete api("/projects/#{project_fork_target.id}/fork", admin)
- expect(response).to have_http_status(200)
+
+ expect(response).to have_http_status(204)
project_fork_target.reload
expect(project_fork_target.forked_from_project).to be_nil
expect(project_fork_target.forked?).not_to be_truthy
@@ -1218,7 +1265,7 @@ describe API::Projects, api: true do
end
end
- describe 'DELETE /projects/:id/star' do
+ describe 'POST /projects/:id/unstar' do
context 'on a starred project' do
before do
user.toggle_star(project)
@@ -1226,16 +1273,16 @@ describe API::Projects, api: true do
end
it 'unstars the project' do
- expect { delete api("/projects/#{project.id}/star", user) }.to change { project.reload.star_count }.by(-1)
+ expect { post api("/projects/#{project.id}/unstar", user) }.to change { project.reload.star_count }.by(-1)
- expect(response).to have_http_status(200)
+ expect(response).to have_http_status(201)
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 api("/projects/#{project.id}/star", user) }.not_to change { project.reload.star_count }
+ expect { post api("/projects/#{project.id}/unstar", user) }.not_to change { project.reload.star_count }
expect(response).to have_http_status(304)
end
@@ -1246,7 +1293,9 @@ describe API::Projects, api: true do
context 'when authenticated as user' do
it 'removes project' do
delete api("/projects/#{project.id}", user)
- expect(response).to have_http_status(200)
+
+ expect(response).to have_http_status(202)
+ expect(json_response['message']).to eql('202 Accepted')
end
it 'does not remove a project if not an owner' do
@@ -1270,7 +1319,9 @@ describe API::Projects, api: true do
context 'when authenticated as admin' do
it 'removes any existing project' do
delete api("/projects/#{project.id}", admin)
- expect(response).to have_http_status(200)
+
+ expect(response).to have_http_status(202)
+ expect(json_response['message']).to eql('202 Accepted')
end
it 'does not remove a non existing project' do
@@ -1405,4 +1456,53 @@ describe API::Projects, api: true do
end
end
end
+
+ describe 'POST /projects/:id/housekeeping' do
+ let(:housekeeping) { Projects::HousekeepingService.new(project) }
+
+ before do
+ allow(Projects::HousekeepingService).to receive(:new).with(project).and_return(housekeeping)
+ end
+
+ context 'when authenticated as owner' do
+ it 'starts the housekeeping process' do
+ expect(housekeeping).to receive(:execute).once
+
+ post api("/projects/#{project.id}/housekeeping", user)
+
+ expect(response).to have_http_status(201)
+ end
+
+ context 'when housekeeping lease is taken' do
+ it 'returns conflict' do
+ expect(housekeeping).to receive(:execute).once.and_raise(Projects::HousekeepingService::LeaseTaken)
+
+ post api("/projects/#{project.id}/housekeeping", user)
+
+ expect(response).to have_http_status(409)
+ expect(json_response['message']).to match(/Somebody already triggered housekeeping for this project/)
+ end
+ end
+ end
+
+ context 'when authenticated as developer' do
+ before do
+ project_member2
+ end
+
+ it 'returns forbidden error' do
+ post api("/projects/#{project.id}/housekeeping", user3)
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ context 'when unauthenticated' do
+ it 'returns authentication error' do
+ post api("/projects/#{project.id}/housekeeping")
+
+ expect(response).to have_http_status(401)
+ end
+ end
+ end
end
diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb
index c61208e395c..7652606a491 100644
--- a/spec/requests/api/repositories_spec.rb
+++ b/spec/requests/api/repositories_spec.rb
@@ -19,10 +19,10 @@ describe API::Repositories, api: true do
get api(route, current_user)
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
first_commit = json_response.first
-
- expect(json_response).to be_an Array
expect(first_commit['name']).to eq('bar')
expect(first_commit['type']).to eq('tree')
expect(first_commit['mode']).to eq('040000')
@@ -49,6 +49,7 @@ describe API::Repositories, api: true do
expect(response.status).to eq(200)
expect(json_response).to be_an Array
+ expect(response).to include_pagination_headers
expect(json_response[4]['name']).to eq('html')
expect(json_response[4]['path']).to eq('files/html')
expect(json_response[4]['type']).to eq('tree')
@@ -380,10 +381,10 @@ describe API::Repositories, api: true do
get api(route, current_user)
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
first_contributor = json_response.first
-
expect(first_contributor['email']).to eq('tiagonbotelho@hotmail.com')
expect(first_contributor['name']).to eq('tiagonbotelho')
expect(first_contributor['commits']).to eq(1)
diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb
new file mode 100644
index 00000000000..e83202e4196
--- /dev/null
+++ b/spec/requests/api/runner_spec.rb
@@ -0,0 +1,151 @@
+require 'spec_helper'
+
+describe API::Runner do
+ include ApiHelpers
+ include StubGitlabCalls
+
+ let(:registration_token) { 'abcdefg123456' }
+
+ before do
+ stub_gitlab_calls
+ stub_application_setting(runners_registration_token: registration_token)
+ end
+
+ describe '/api/v4/runners' do
+ describe 'POST /api/v4/runners' do
+ context 'when no token is provided' do
+ it 'returns 400 error' do
+ post api('/runners')
+ expect(response).to have_http_status 400
+ end
+ end
+
+ context 'when invalid token is provided' do
+ it 'returns 403 error' do
+ post api('/runners'), token: 'invalid'
+ expect(response).to have_http_status 403
+ end
+ end
+
+ context 'when valid token is provided' do
+ it 'creates runner with default values' do
+ post api('/runners'), token: registration_token
+
+ runner = Ci::Runner.first
+
+ expect(response).to have_http_status 201
+ expect(json_response['id']).to eq(runner.id)
+ expect(json_response['token']).to eq(runner.token)
+ expect(runner.run_untagged).to be true
+ end
+
+ context 'when project token is used' do
+ let(:project) { create(:empty_project) }
+
+ it 'creates runner' do
+ post api('/runners'), token: project.runners_token
+
+ expect(response).to have_http_status 201
+ expect(project.runners.size).to eq(1)
+ end
+ end
+ end
+
+ context 'when runner description is provided' do
+ it 'creates runner' do
+ post api('/runners'), token: registration_token,
+ description: 'server.hostname'
+
+ expect(response).to have_http_status 201
+ expect(Ci::Runner.first.description).to eq('server.hostname')
+ end
+ end
+
+ context 'when runner tags are provided' do
+ it 'creates runner' do
+ post api('/runners'), token: registration_token,
+ tag_list: 'tag1, tag2'
+
+ expect(response).to have_http_status 201
+ expect(Ci::Runner.first.tag_list.sort).to eq(%w(tag1 tag2))
+ end
+ end
+
+ context 'when option for running untagged jobs is provided' do
+ context 'when tags are provided' do
+ it 'creates runner' do
+ post api('/runners'), token: registration_token,
+ run_untagged: false,
+ tag_list: ['tag']
+
+ expect(response).to have_http_status 201
+ expect(Ci::Runner.first.run_untagged).to be false
+ expect(Ci::Runner.first.tag_list.sort).to eq(['tag'])
+ end
+ end
+
+ context 'when tags are not provided' do
+ it 'returns 404 error' do
+ post api('/runners'), token: registration_token,
+ run_untagged: false
+
+ expect(response).to have_http_status 404
+ end
+ end
+ end
+
+ context 'when option for locking Runner is provided' do
+ it 'creates runner' do
+ post api('/runners'), token: registration_token,
+ locked: true
+
+ expect(response).to have_http_status 201
+ expect(Ci::Runner.first.locked).to be true
+ end
+ end
+
+ %w(name version revision platform architecture).each do |param|
+ context "when info parameter '#{param}' info is present" do
+ let(:value) { "#{param}_value" }
+
+ it %q(updates provided Runner's parameter) do
+ post api('/runners'), token: registration_token,
+ info: { param => value }
+
+ expect(response).to have_http_status 201
+ expect(Ci::Runner.first.read_attribute(param.to_sym)).to eq(value)
+ end
+ end
+ end
+ end
+
+ describe 'DELETE /api/v4/runners' do
+ context 'when no token is provided' do
+ it 'returns 400 error' do
+ delete api('/runners')
+
+ expect(response).to have_http_status 400
+ end
+ end
+
+ context 'when invalid token is provided' do
+ it 'returns 403 error' do
+ delete api('/runners'), token: 'invalid'
+
+ expect(response).to have_http_status 403
+ end
+ end
+
+ context 'when valid token is provided' do
+ let(:runner) { create(:ci_runner) }
+
+ it 'deletes Runner' do
+ delete api('/runners'), token: runner.token
+
+ expect(response).to have_http_status 204
+ expect(Ci::Runner.count).to eq(0)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/runners_spec.rb b/spec/requests/api/runners_spec.rb
index f2d81a28cb8..8a82543a830 100644
--- a/spec/requests/api/runners_spec.rb
+++ b/spec/requests/api/runners_spec.rb
@@ -37,18 +37,20 @@ describe API::Runners, api: true do
context 'authorized user' do
it 'returns user available runners' do
get api('/runners', user)
- shared = json_response.any?{ |r| r['is_shared'] }
+ shared = json_response.any?{ |r| r['is_shared'] }
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(shared).to be_falsey
end
it 'filters runners by scope' do
get api('/runners?scope=active', user)
- shared = json_response.any?{ |r| r['is_shared'] }
+ shared = json_response.any?{ |r| r['is_shared'] }
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(shared).to be_falsey
end
@@ -73,9 +75,10 @@ describe API::Runners, api: true do
context 'with admin privileges' do
it 'returns all runners' do
get api('/runners/all', admin)
- shared = json_response.any?{ |r| r['is_shared'] }
+ shared = json_response.any?{ |r| r['is_shared'] }
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(shared).to be_truthy
end
@@ -91,9 +94,10 @@ describe API::Runners, api: true do
it 'filters runners by scope' do
get api('/runners/all?scope=specific', admin)
- shared = json_response.any?{ |r| r['is_shared'] }
+ shared = json_response.any?{ |r| r['is_shared'] }
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(shared).to be_falsey
end
@@ -183,6 +187,7 @@ describe API::Runners, api: true do
it 'updates runner' do
description = shared_runner.description
active = shared_runner.active
+ runner_queue_value = shared_runner.ensure_runner_queue_value
update_runner(shared_runner.id, admin, description: "#{description}_updated",
active: !active,
@@ -197,18 +202,24 @@ describe API::Runners, api: true do
expect(shared_runner.tag_list).to include('ruby2.1', 'pgsql', 'mysql')
expect(shared_runner.run_untagged?).to be(false)
expect(shared_runner.locked?).to be(true)
+ expect(shared_runner.ensure_runner_queue_value)
+ .not_to eq(runner_queue_value)
end
end
context 'when runner is not shared' do
it 'updates runner' do
description = specific_runner.description
+ runner_queue_value = specific_runner.ensure_runner_queue_value
+
update_runner(specific_runner.id, admin, description: 'test')
specific_runner.reload
expect(response).to have_http_status(200)
expect(specific_runner.description).to eq('test')
expect(specific_runner.description).not_to eq(description)
+ expect(specific_runner.ensure_runner_queue_value)
+ .not_to eq(runner_queue_value)
end
end
@@ -266,8 +277,9 @@ describe API::Runners, api: true do
it 'deletes runner' do
expect do
delete api("/runners/#{shared_runner.id}", admin)
+
+ expect(response).to have_http_status(204)
end.to change{ Ci::Runner.shared.count }.by(-1)
- expect(response).to have_http_status(200)
end
end
@@ -275,15 +287,17 @@ describe API::Runners, api: true do
it 'deletes unused runner' do
expect do
delete api("/runners/#{unused_specific_runner.id}", admin)
+
+ expect(response).to have_http_status(204)
end.to change{ Ci::Runner.specific.count }.by(-1)
- expect(response).to have_http_status(200)
end
it 'deletes used runner' do
expect do
delete api("/runners/#{specific_runner.id}", admin)
+
+ expect(response).to have_http_status(204)
end.to change{ Ci::Runner.specific.count }.by(-1)
- expect(response).to have_http_status(200)
end
end
@@ -316,8 +330,9 @@ describe API::Runners, api: true do
it 'deletes runner for one owned project' do
expect do
delete api("/runners/#{specific_runner.id}", user)
+
+ expect(response).to have_http_status(204)
end.to change{ Ci::Runner.specific.count }.by(-1)
- expect(response).to have_http_status(200)
end
end
end
@@ -335,9 +350,10 @@ describe API::Runners, api: true do
context 'authorized user with master privileges' do
it "returns project's runners" do
get api("/projects/#{project.id}/runners", user)
- shared = json_response.any?{ |r| r['is_shared'] }
+ shared = json_response.any?{ |r| r['is_shared'] }
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(shared).to be_truthy
end
@@ -445,8 +461,9 @@ describe API::Runners, api: true do
it "disables project's runner" do
expect do
delete api("/projects/#{project.id}/runners/#{two_projects_runner.id}", user)
+
+ expect(response).to have_http_status(204)
end.to change{ project.runners.count }.by(-1)
- expect(response).to have_http_status(200)
end
end
diff --git a/spec/requests/api/services_spec.rb b/spec/requests/api/services_spec.rb
index 776dc655650..fd334934ca5 100644
--- a/spec/requests/api/services_spec.rb
+++ b/spec/requests/api/services_spec.rb
@@ -55,7 +55,7 @@ describe API::Services, api: true do
it "deletes #{service}" do
delete api("/projects/#{project.id}/services/#{dashed_service}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_http_status(204)
project.send(service_method).reload
expect(project.send(service_method).activated?).to be_falsey
end
diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb
index 91e3c333a02..411905edb49 100644
--- a/spec/requests/api/settings_spec.rb
+++ b/spec/requests/api/settings_spec.rb
@@ -30,8 +30,14 @@ 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',
- plantuml_enabled: true, plantuml_url: 'http://plantuml.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',
+ default_artifacts_expire_in: '2 days'
expect(response).to have_http_status(200)
expect(json_response['default_projects_limit']).to eq(3)
expect(json_response['signin_enabled']).to be_falsey
@@ -41,6 +47,7 @@ describe API::Settings, 'Settings', api: true do
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')
+ expect(json_response['default_artifacts_expire_in']).to eq('2 days')
end
end
diff --git a/spec/requests/api/snippets_spec.rb b/spec/requests/api/snippets_spec.rb
index 6b9a739b439..5219f6eed42 100644
--- a/spec/requests/api/snippets_spec.rb
+++ b/spec/requests/api/snippets_spec.rb
@@ -13,6 +13,8 @@ describe API::Snippets, api: true do
get api("/snippets/", user)
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
expect(json_response.map { |snippet| snippet['id']} ).to contain_exactly(
public_snippet.id,
internal_snippet.id,
@@ -25,7 +27,10 @@ describe API::Snippets, api: true do
create(:personal_snippet, :private)
get api("/snippets/", user)
+
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
expect(json_response.size).to eq(0)
end
end
@@ -43,6 +48,8 @@ describe API::Snippets, api: true do
get api("/snippets/public", user)
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
expect(json_response.map { |snippet| snippet['id']} ).to contain_exactly(
public_snippet.id,
public_snippet_other.id)
@@ -67,7 +74,7 @@ describe API::Snippets, api: true do
end
it 'returns 404 for invalid snippet id' do
- delete api("/snippets/1234", user)
+ get api("/snippets/1234/raw", user)
expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 Snippet Not Found')
@@ -122,7 +129,9 @@ describe API::Snippets, api: true 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)
+ expect(json_response['message']).to eq({ "error" => "Spam detected" })
end
it 'creates a spam log' do
@@ -134,16 +143,20 @@ describe API::Snippets, api: true do
end
describe 'PUT /snippets/:id' do
+ let(:visibility_level) { Snippet::PUBLIC }
let(:other_user) { create(:user) }
- let(:public_snippet) { create(:personal_snippet, :public, author: user) }
+ let(:snippet) do
+ create(:personal_snippet, author: user, visibility_level: visibility_level)
+ end
+
it 'updates snippet' do
new_content = 'New content'
- put api("/snippets/#{public_snippet.id}", user), content: new_content
+ put api("/snippets/#{snippet.id}", user), content: new_content
expect(response).to have_http_status(200)
- public_snippet.reload
- expect(public_snippet.content).to eq(new_content)
+ snippet.reload
+ expect(snippet.content).to eq(new_content)
end
it 'returns 404 for invalid snippet id' do
@@ -154,7 +167,7 @@ describe API::Snippets, api: true do
end
it "returns 404 for another user's snippet" do
- put api("/snippets/#{public_snippet.id}", other_user), title: 'fubar'
+ put api("/snippets/#{snippet.id}", other_user), title: 'fubar'
expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 Snippet Not Found')
@@ -165,6 +178,56 @@ describe API::Snippets, api: true do
expect(response).to have_http_status(400)
end
+
+ context 'when the snippet is spam' do
+ def update_snippet(snippet_params = {})
+ put api("/snippets/#{snippet.id}", user), 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
+ let(:visibility_level) { Snippet::PRIVATE }
+
+ it 'updates the snippet' do
+ expect { update_snippet(title: 'Foo') }.
+ to change { snippet.reload.title }.to('Foo')
+ end
+ end
+
+ context 'when the snippet is public' do
+ let(:visibility_level) { Snippet::PUBLIC }
+
+ it 'rejects the shippet' do
+ expect { update_snippet(title: 'Foo') }.
+ not_to change { snippet.reload.title }
+
+ expect(response).to have_http_status(400)
+ expect(json_response['message']).to eq({ "error" => "Spam detected" })
+ end
+
+ it 'creates a spam log' do
+ expect { update_snippet(title: 'Foo') }.
+ to change { SpamLog.count }.by(1)
+ end
+ end
+
+ context 'when a private snippet is made public' do
+ let(:visibility_level) { Snippet::PRIVATE }
+
+ it 'rejects the snippet' do
+ expect { update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC) }.
+ not_to change { snippet.reload.title }
+ end
+
+ it 'creates a spam log' do
+ expect { update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC) }.
+ to change { SpamLog.count }.by(1)
+ end
+ end
+ end
end
describe 'DELETE /snippets/:id' do
diff --git a/spec/requests/api/system_hooks_spec.rb b/spec/requests/api/system_hooks_spec.rb
index b3e5afdadb1..d1e10f12657 100644
--- a/spec/requests/api/system_hooks_spec.rb
+++ b/spec/requests/api/system_hooks_spec.rb
@@ -31,6 +31,7 @@ describe API::SystemHooks, api: true do
get api("/hooks", admin)
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.first['url']).to eq(hook.url)
expect(json_response.first['push_events']).to be true
@@ -90,6 +91,8 @@ describe API::SystemHooks, api: true do
it "deletes a hook" do
expect do
delete api("/hooks/#{hook.id}", admin)
+
+ expect(response).to have_http_status(204)
end.to change { SystemHook.count }.by(-1)
end
diff --git a/spec/requests/api/tags_spec.rb b/spec/requests/api/tags_spec.rb
index 898d2b27e5c..b132d033a61 100644
--- a/spec/requests/api/tags_spec.rb
+++ b/spec/requests/api/tags_spec.rb
@@ -20,10 +20,9 @@ describe API::Tags, api: true do
get api("/projects/#{project.id}/repository/tags", current_user)
expect(response).to have_http_status(200)
-
- first_tag = json_response.first
-
- expect(first_tag['name']).to eq(tag_name)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.first['name']).to eq(tag_name)
end
end
@@ -43,7 +42,9 @@ describe API::Tags, api: true do
context 'without releases' do
it "returns an array of project tags" do
get api("/projects/#{project.id}/repository/tags", user)
+
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.first['name']).to eq(tag_name)
end
@@ -59,6 +60,7 @@ describe API::Tags, api: true do
get api("/projects/#{project.id}/repository/tags", user)
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.first['name']).to eq(tag_name)
expect(json_response.first['message']).to eq('Version 1.1.0')
@@ -135,8 +137,8 @@ describe API::Tags, api: true do
context 'delete tag' do
it 'deletes an existing tag' do
delete api("/projects/#{project.id}/repository/tags/#{tag_name}", user)
- expect(response).to have_http_status(200)
- expect(json_response['tag_name']).to eq(tag_name)
+
+ expect(response).to have_http_status(204)
end
it 'raises 404 if the tag does not exist' do
diff --git a/spec/requests/api/templates_spec.rb b/spec/requests/api/templates_spec.rb
index c0a8c0832bb..2c83e119065 100644
--- a/spec/requests/api/templates_spec.rb
+++ b/spec/requests/api/templates_spec.rb
@@ -22,6 +22,7 @@ describe API::Templates, api: true do
get api('/templates/gitignores')
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.size).to be > 15
end
@@ -32,6 +33,7 @@ describe API::Templates, api: true do
get api('/templates/gitlab_ci_ymls')
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.first['name']).not_to be_nil
end
@@ -56,11 +58,11 @@ describe API::Templates, api: true do
expect(json_response['popular']).to be true
expect(json_response['html_url']).to eq('http://choosealicense.com/licenses/mit/')
expect(json_response['source_url']).to eq('https://opensource.org/licenses/MIT')
- expect(json_response['description']).to include('A permissive license that is short and to the point.')
+ expect(json_response['description']).to include('A short and simple permissive license with conditions')
expect(json_response['conditions']).to eq(%w[include-copyright])
expect(json_response['permissions']).to eq(%w[commercial-use modifications distribution private-use])
expect(json_response['limitations']).to eq(%w[no-liability])
- expect(json_response['content']).to include('The MIT License (MIT)')
+ expect(json_response['content']).to include('MIT License')
end
end
@@ -69,8 +71,9 @@ describe API::Templates, api: true do
get api('/templates/licenses')
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
- expect(json_response.size).to eq(15)
+ expect(json_response.size).to eq(12)
expect(json_response.map { |l| l['key'] }).to include('agpl-3.0')
end
@@ -80,6 +83,7 @@ describe API::Templates, api: true do
get api('/templates/licenses?popular=1')
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.size).to eq(3)
expect(json_response.map { |l| l['key'] }).to include('apache-2.0')
@@ -98,7 +102,7 @@ describe API::Templates, api: true do
let(:license_type) { 'mit' }
it 'returns the license text' do
- expect(json_response['content']).to include('The MIT License (MIT)')
+ expect(json_response['content']).to include('MIT License')
end
it 'replaces placeholder values' do
diff --git a/spec/requests/api/todos_spec.rb b/spec/requests/api/todos_spec.rb
index baf6e4d98d3..1e401935662 100644
--- a/spec/requests/api/todos_spec.rb
+++ b/spec/requests/api/todos_spec.rb
@@ -33,6 +33,7 @@ describe API::Todos, api: true do
get api('/todos', john_doe)
expect(response.status).to eq(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(3)
expect(json_response[0]['id']).to eq(pending_3.id)
@@ -52,6 +53,7 @@ describe API::Todos, api: true do
get api('/todos', john_doe), { author_id: author_2.id }
expect(response.status).to eq(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(2)
end
@@ -64,6 +66,7 @@ describe API::Todos, api: true do
get api('/todos', john_doe), { type: 'MergeRequest' }
expect(response.status).to eq(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
end
@@ -74,6 +77,7 @@ describe API::Todos, api: true do
get api('/todos', john_doe), { state: 'done' }
expect(response.status).to eq(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
end
@@ -84,6 +88,7 @@ describe API::Todos, api: true do
get api('/todos', john_doe), { project_id: project_2.id }
expect(response.status).to eq(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
end
@@ -94,6 +99,7 @@ describe API::Todos, api: true do
get api('/todos', john_doe), { action: 'mentioned' }
expect(response.status).to eq(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
end
@@ -101,46 +107,47 @@ describe API::Todos, api: true do
end
end
- describe 'DELETE /todos/:id' do
+ describe 'POST /todos/:id/mark_as_done' do
context 'when unauthenticated' do
it 'returns authentication error' do
- delete api("/todos/#{pending_1.id}")
+ post api("/todos/#{pending_1.id}/mark_as_done")
- expect(response.status).to eq(401)
+ expect(response).to have_http_status(401)
end
end
context 'when authenticated' do
it 'marks a todo as done' do
- delete api("/todos/#{pending_1.id}", john_doe)
+ post api("/todos/#{pending_1.id}/mark_as_done", john_doe)
- expect(response.status).to eq(200)
+ expect(response).to have_http_status(201)
+ expect(json_response['id']).to eq(pending_1.id)
+ expect(json_response['state']).to eq('done')
expect(pending_1.reload).to be_done
end
it 'updates todos cache' do
expect_any_instance_of(User).to receive(:update_todos_count_cache).and_call_original
- delete api("/todos/#{pending_1.id}", john_doe)
+ post api("/todos/#{pending_1.id}/mark_as_done", john_doe)
end
end
end
- describe 'DELETE /todos' do
+ describe 'POST /mark_as_done' do
context 'when unauthenticated' do
it 'returns authentication error' do
- delete api('/todos')
+ post api('/todos/mark_as_done')
- expect(response.status).to eq(401)
+ expect(response).to have_http_status(401)
end
end
context 'when authenticated' do
it 'marks all todos as done' do
- delete api('/todos', john_doe)
+ post api('/todos/mark_as_done', john_doe)
- expect(response.status).to eq(200)
- expect(response.body).to eq('3')
+ expect(response).to have_http_status(204)
expect(pending_1.reload).to be_done
expect(pending_2.reload).to be_done
expect(pending_3.reload).to be_done
@@ -149,7 +156,7 @@ describe API::Todos, api: true do
it 'updates todos cache' do
expect_any_instance_of(User).to receive(:update_todos_count_cache).and_call_original
- delete api("/todos", john_doe)
+ post api("/todos/mark_as_done", john_doe)
end
end
end
diff --git a/spec/requests/api/triggers_spec.rb b/spec/requests/api/triggers_spec.rb
index 84104aa66ee..153e2791cbe 100644
--- a/spec/requests/api/triggers_spec.rb
+++ b/spec/requests/api/triggers_spec.rb
@@ -100,6 +100,7 @@ describe API::Triggers do
get api("/projects/#{project.id}/triggers", user)
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_a(Array)
expect(json_response[0]).to have_key('token')
end
@@ -189,8 +190,9 @@ describe API::Triggers do
it 'deletes trigger' do
expect do
delete api("/projects/#{project.id}/triggers/#{trigger.token}", user)
+
+ expect(response).to have_http_status(204)
end.to change{project.triggers.count}.by(-1)
- expect(response).to have_http_status(200)
end
it 'responds with 404 Not Found if requesting non-existing trigger' do
diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb
index 5958012672e..e5e4c84755f 100644
--- a/spec/requests/api/users_spec.rb
+++ b/spec/requests/api/users_spec.rb
@@ -40,7 +40,9 @@ describe API::Users, api: true do
it "returns an array of users" do
get api("/users", user)
+
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
username = user.username
expect(json_response.detect do |user|
@@ -55,13 +57,16 @@ describe API::Users, api: true do
get api("/users?blocked=true", user)
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response).to all(include('state' => /(blocked|ldap_blocked)/))
end
it "returns one user" do
get api("/users?username=#{omniauth_user.username}", user)
+
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.first['username']).to eq(omniauth_user.username)
end
@@ -70,7 +75,9 @@ describe API::Users, api: true do
context "when admin" do
it "returns an array of users" do
get api("/users", admin)
+
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.first.keys).to include 'email'
expect(json_response.first.keys).to include 'organization'
@@ -87,6 +94,7 @@ describe API::Users, api: true do
get api("/users?external=true", admin)
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response).to all(include('external' => true))
end
@@ -507,8 +515,11 @@ describe API::Users, api: true do
it 'returns array of ssh keys' do
user.keys << key
user.save
+
get api("/users/#{user.id}/keys", admin)
+
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.first['title']).to eq(key.title)
end
@@ -529,10 +540,12 @@ describe API::Users, api: true do
it 'deletes existing key' do
user.keys << key
user.save
+
expect do
delete api("/users/#{user.id}/keys/#{key.id}", admin)
+
+ expect(response).to have_http_status(204)
end.to change { user.keys.count }.by(-1)
- expect(response).to have_http_status(200)
end
it 'returns 404 error if user not found' do
@@ -595,8 +608,11 @@ describe API::Users, api: true do
it 'returns array of emails' do
user.emails << email
user.save
+
get api("/users/#{user.id}/emails", admin)
+
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.first['email']).to eq(email.email)
end
@@ -623,10 +639,12 @@ describe API::Users, api: true do
it 'deletes existing email' do
user.emails << email
user.save
+
expect do
delete api("/users/#{user.id}/emails/#{email.id}", admin)
+
+ expect(response).to have_http_status(204)
end.to change { user.emails.count }.by(-1)
- expect(response).to have_http_status(200)
end
it 'returns 404 error if user not found' do
@@ -657,10 +675,10 @@ describe API::Users, api: true do
it "deletes user" do
delete api("/users/#{user.id}", admin)
- expect(response).to have_http_status(200)
+
+ expect(response).to have_http_status(204)
expect { User.find(user.id) }.to raise_error ActiveRecord::RecordNotFound
expect { Namespace.find(namespace.id) }.to raise_error ActiveRecord::RecordNotFound
- expect(json_response['email']).to eq(user.email)
end
it "does not delete for unauthenticated user" do
@@ -774,8 +792,11 @@ describe API::Users, api: true do
it "returns array of ssh keys" do
user.keys << key
user.save
+
get api("/user/keys", user)
+
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.first["title"]).to eq(key.title)
end
@@ -852,10 +873,12 @@ describe API::Users, api: true do
it "deletes existed key" do
user.keys << key
user.save
+
expect do
delete api("/user/keys/#{key.id}", user)
+
+ expect(response).to have_http_status(204)
end.to change{user.keys.count}.by(-1)
- expect(response).to have_http_status(200)
end
it "returns 404 if key ID not found" do
@@ -891,8 +914,11 @@ describe API::Users, api: true do
it "returns array of emails" do
user.emails << email
user.save
+
get api("/user/emails", user)
+
expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.first["email"]).to eq(email.email)
end
@@ -956,10 +982,12 @@ describe API::Users, api: true do
it "deletes existed email" do
user.emails << email
user.save
+
expect do
delete api("/user/emails/#{email.id}", user)
+
+ expect(response).to have_http_status(204)
end.to change{user.emails.count}.by(-1)
- expect(response).to have_http_status(200)
end
it "returns 404 if email ID not found" do
@@ -983,69 +1011,69 @@ describe API::Users, api: true do
end
end
- describe 'PUT /users/:id/block' do
+ describe 'POST /users/:id/block' do
before { admin }
it 'blocks existing user' do
- put api("/users/#{user.id}/block", admin)
- expect(response).to have_http_status(200)
+ post api("/users/#{user.id}/block", admin)
+ expect(response).to have_http_status(201)
expect(user.reload.state).to eq('blocked')
end
it 'does not re-block ldap blocked users' do
- put api("/users/#{ldap_blocked_user.id}/block", admin)
+ post api("/users/#{ldap_blocked_user.id}/block", admin)
expect(response).to have_http_status(403)
expect(ldap_blocked_user.reload.state).to eq('ldap_blocked')
end
it 'does not be available for non admin users' do
- put api("/users/#{user.id}/block", user)
+ post api("/users/#{user.id}/block", user)
expect(response).to have_http_status(403)
expect(user.reload.state).to eq('active')
end
it 'returns a 404 error if user id not found' do
- put api('/users/9999/block', admin)
+ post api('/users/9999/block', admin)
expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 User Not Found')
end
end
- describe 'PUT /users/:id/unblock' do
+ describe 'POST /users/:id/unblock' do
let(:blocked_user) { create(:user, state: 'blocked') }
before { admin }
it 'unblocks existing user' do
- put api("/users/#{user.id}/unblock", admin)
- expect(response).to have_http_status(200)
+ post api("/users/#{user.id}/unblock", admin)
+ expect(response).to have_http_status(201)
expect(user.reload.state).to eq('active')
end
it 'unblocks a blocked user' do
- put api("/users/#{blocked_user.id}/unblock", admin)
- expect(response).to have_http_status(200)
+ post api("/users/#{blocked_user.id}/unblock", admin)
+ expect(response).to have_http_status(201)
expect(blocked_user.reload.state).to eq('active')
end
it 'does not unblock ldap blocked users' do
- put api("/users/#{ldap_blocked_user.id}/unblock", admin)
+ post api("/users/#{ldap_blocked_user.id}/unblock", admin)
expect(response).to have_http_status(403)
expect(ldap_blocked_user.reload.state).to eq('ldap_blocked')
end
it 'does not be available for non admin users' do
- put api("/users/#{user.id}/unblock", user)
+ post api("/users/#{user.id}/unblock", user)
expect(response).to have_http_status(403)
expect(user.reload.state).to eq('active')
end
it 'returns a 404 error if user id not found' do
- put api('/users/9999/block', admin)
+ post api('/users/9999/block', admin)
expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 User Not Found')
end
it "returns a 404 for invalid ID" do
- put api("/users/ASDF/block", admin)
+ post api("/users/ASDF/block", admin)
expect(response).to have_http_status(404)
end
@@ -1073,14 +1101,14 @@ describe API::Users, api: true do
end
context "as a user than can see the event's project" do
- it_behaves_like 'a paginated resources' do
- let(:request) { get api("/users/#{user.id}/events", user) }
- end
-
context 'joined event' do
it 'returns the "joined" event' do
get api("/users/#{user.id}/events", user)
+ expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+
comment_event = json_response.find { |e| e['action_name'] == 'commented on' }
expect(comment_event['project_id'].to_i).to eq(project.id)
diff --git a/spec/requests/api/v3/award_emoji_spec.rb b/spec/requests/api/v3/award_emoji_spec.rb
new file mode 100644
index 00000000000..91145c8e72c
--- /dev/null
+++ b/spec/requests/api/v3/award_emoji_spec.rb
@@ -0,0 +1,74 @@
+require 'spec_helper'
+
+describe API::V3::AwardEmoji, api: true do
+ include ApiHelpers
+
+ let(:user) { create(:user) }
+ let!(:project) { create(:empty_project) }
+ let(:issue) { create(:issue, project: project) }
+ let!(:award_emoji) { create(:award_emoji, awardable: issue, user: user) }
+ let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
+ let!(:downvote) { create(:award_emoji, :downvote, awardable: merge_request, user: user) }
+ let!(:note) { create(:note, project: project, noteable: issue) }
+
+ before { project.team << [user, :master] }
+
+ describe 'DELETE /projects/:id/awardable/:awardable_id/award_emoji/:award_id' do
+ context 'when the awardable is an Issue' do
+ it 'deletes the award' do
+ expect do
+ delete v3_api("/projects/#{project.id}/issues/#{issue.id}/award_emoji/#{award_emoji.id}", user)
+
+ expect(response).to have_http_status(200)
+ end.to change { issue.award_emoji.count }.from(1).to(0)
+ end
+
+ it 'returns a 404 error when the award emoji can not be found' do
+ delete v3_api("/projects/#{project.id}/issues/#{issue.id}/award_emoji/12345", user)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'when the awardable is a Merge Request' do
+ it 'deletes the award' do
+ expect do
+ delete v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/award_emoji/#{downvote.id}", user)
+
+ expect(response).to have_http_status(200)
+ end.to change { merge_request.award_emoji.count }.from(1).to(0)
+ end
+
+ it 'returns a 404 error when note id not found' do
+ delete v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/notes/12345", user)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'when the awardable is a Snippet' do
+ let(:snippet) { create(:project_snippet, :public, project: project) }
+ let!(:award) { create(:award_emoji, awardable: snippet, user: user) }
+
+ it 'deletes the award' do
+ expect do
+ delete v3_api("/projects/#{project.id}/snippets/#{snippet.id}/award_emoji/#{award.id}", user)
+
+ expect(response).to have_http_status(200)
+ end.to change { snippet.award_emoji.count }.from(1).to(0)
+ end
+ end
+ end
+
+ describe 'DELETE /projects/:id/awardable/:awardable_id/award_emoji/:award_emoji_id' do
+ let!(:rocket) { create(:award_emoji, awardable: note, name: 'rocket', user: user) }
+
+ it 'deletes the award' do
+ expect do
+ delete v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji/#{rocket.id}", user)
+
+ expect(response).to have_http_status(200)
+ end.to change { note.award_emoji.count }.from(1).to(0)
+ end
+ end
+end
diff --git a/spec/requests/api/v3/boards_spec.rb b/spec/requests/api/v3/boards_spec.rb
new file mode 100644
index 00000000000..eb95934f354
--- /dev/null
+++ b/spec/requests/api/v3/boards_spec.rb
@@ -0,0 +1,113 @@
+require 'spec_helper'
+
+describe API::V3::Boards, api: true do
+ include ApiHelpers
+
+ let(:user) { create(:user) }
+ let(:guest) { create(:user) }
+ let(:non_member) { create(:user) }
+ 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)
+ end
+
+ let!(:test_label) do
+ create(:label, title: 'Testing', color: '#FFAACC', project: project)
+ end
+
+ let!(:dev_list) do
+ create(:list, label: dev_label, position: 1)
+ end
+
+ let!(:test_list) do
+ create(:list, label: test_label, position: 2)
+ end
+
+ let!(:board) do
+ create(:board, project: project, lists: [dev_list, test_list])
+ end
+
+ before do
+ project.team << [user, :reporter]
+ project.team << [guest, :guest]
+ end
+
+ describe "GET /projects/:id/boards" do
+ let(:base_url) { "/projects/#{project.id}/boards" }
+
+ context "when unauthenticated" do
+ it "returns authentication error" do
+ get v3_api(base_url)
+
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ context "when authenticated" do
+ it "returns the project issue board" 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(1)
+ expect(json_response.first['id']).to eq(board.id)
+ expect(json_response.first['lists']).to be_an Array
+ expect(json_response.first['lists'].length).to eq(2)
+ expect(json_response.first['lists'].last).to have_key('position')
+ end
+ end
+ end
+
+ describe "GET /projects/:id/boards/:board_id/lists" do
+ let(:base_url) { "/projects/#{project.id}/boards/#{board.id}/lists" }
+
+ it 'returns issue board lists' 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)
+ expect(json_response.first['label']['name']).to eq(dev_label.title)
+ end
+
+ it 'returns 404 if board not found' do
+ get v3_api("/projects/#{project.id}/boards/22343/lists", user)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ describe "DELETE /projects/:id/board/lists/:list_id" do
+ let(:base_url) { "/projects/#{project.id}/boards/#{board.id}/lists" }
+
+ it "rejects a non member from deleting a list" do
+ delete v3_api("#{base_url}/#{dev_list.id}", non_member)
+
+ expect(response).to have_http_status(403)
+ end
+
+ it "rejects a user with guest role from deleting a list" do
+ delete v3_api("#{base_url}/#{dev_list.id}", guest)
+
+ expect(response).to have_http_status(403)
+ end
+
+ it "returns 404 error if list id not found" do
+ delete v3_api("#{base_url}/44444", user)
+
+ expect(response).to have_http_status(404)
+ end
+
+ context "when the user is project owner" do
+ let(:owner) { create(:user) }
+ let(:project) { create(:empty_project, namespace: owner.namespace) }
+
+ it "deletes the list if an admin requests it" do
+ delete v3_api("#{base_url}/#{dev_list.id}", owner)
+
+ expect(response).to have_http_status(200)
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/v3/branches_spec.rb b/spec/requests/api/v3/branches_spec.rb
new file mode 100644
index 00000000000..e4cedf98e64
--- /dev/null
+++ b/spec/requests/api/v3/branches_spec.rb
@@ -0,0 +1,83 @@
+require 'spec_helper'
+require 'mime/types'
+
+describe API::V3::Branches, api: true do
+ include ApiHelpers
+
+ let(:user) { create(:user) }
+ let(:user2) { create(:user) }
+ 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' }
+ let!(:branch_with_dot) { CreateBranchService.new(project, user).execute("with.1.2.3", "master") }
+
+ describe "GET /projects/:id/repository/branches" do
+ it "returns an array of project branches" do
+ project.repository.expire_all_method_caches
+
+ get v3_api("/projects/#{project.id}/repository/branches", user), per_page: 100
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ branch_names = json_response.map { |x| x['name'] }
+ expect(branch_names).to match_array(project.repository.branch_names)
+ end
+ end
+
+ describe "DELETE /projects/:id/repository/branches/:branch" do
+ before do
+ allow_any_instance_of(Repository).to receive(:rm_branch).and_return(true)
+ end
+
+ it "removes branch" do
+ delete v3_api("/projects/#{project.id}/repository/branches/#{branch_name}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['branch_name']).to eq(branch_name)
+ end
+
+ it "removes a branch with dots in the branch name" do
+ delete v3_api("/projects/#{project.id}/repository/branches/with.1.2.3", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['branch_name']).to eq("with.1.2.3")
+ end
+
+ it 'returns 404 if branch not exists' do
+ delete v3_api("/projects/#{project.id}/repository/branches/foobar", user)
+ expect(response).to have_http_status(404)
+ end
+
+ it "removes protected branch" do
+ create(:protected_branch, project: project, name: branch_name)
+ delete v3_api("/projects/#{project.id}/repository/branches/#{branch_name}", user)
+ expect(response).to have_http_status(405)
+ expect(json_response['message']).to eq('Protected branch cant be removed')
+ end
+
+ it "does not remove HEAD branch" do
+ delete v3_api("/projects/#{project.id}/repository/branches/master", user)
+ expect(response).to have_http_status(405)
+ expect(json_response['message']).to eq('Cannot remove HEAD branch')
+ end
+ end
+
+ describe "DELETE /projects/:id/repository/merged_branches" do
+ before do
+ allow_any_instance_of(Repository).to receive(:rm_branch).and_return(true)
+ end
+
+ it 'returns 200' do
+ delete v3_api("/projects/#{project.id}/repository/merged_branches", user)
+
+ expect(response).to have_http_status(200)
+ end
+
+ it 'returns a 403 error if guest' do
+ delete v3_api("/projects/#{project.id}/repository/merged_branches", user2)
+
+ expect(response).to have_http_status(403)
+ end
+ end
+end
diff --git a/spec/requests/api/v3/broadcast_messages_spec.rb b/spec/requests/api/v3/broadcast_messages_spec.rb
new file mode 100644
index 00000000000..06556401a29
--- /dev/null
+++ b/spec/requests/api/v3/broadcast_messages_spec.rb
@@ -0,0 +1,34 @@
+require 'spec_helper'
+
+describe API::V3::BroadcastMessages, api: true do
+ include ApiHelpers
+
+ let(:user) { create(:user) }
+ let(:admin) { create(:admin) }
+
+ describe 'DELETE /broadcast_messages/:id' do
+ let!(:message) { create(:broadcast_message) }
+
+ it 'returns a 401 for anonymous users' do
+ delete v3_api("/broadcast_messages/#{message.id}"),
+ attributes_for(:broadcast_message)
+
+ expect(response).to have_http_status(401)
+ end
+
+ it 'returns a 403 for users' do
+ delete v3_api("/broadcast_messages/#{message.id}", user),
+ attributes_for(:broadcast_message)
+
+ expect(response).to have_http_status(403)
+ end
+
+ it 'deletes the broadcast message for admins' do
+ expect do
+ delete v3_api("/broadcast_messages/#{message.id}", admin)
+
+ expect(response).to have_http_status(200)
+ end.to change { BroadcastMessage.count }.by(-1)
+ end
+ end
+end
diff --git a/spec/requests/api/v3/commits_spec.rb b/spec/requests/api/v3/commits_spec.rb
new file mode 100644
index 00000000000..e298ef055e1
--- /dev/null
+++ b/spec/requests/api/v3/commits_spec.rb
@@ -0,0 +1,578 @@
+require 'spec_helper'
+require 'mime/types'
+
+describe API::V3::Commits, api: true do
+ include ApiHelpers
+ let(:user) { create(:user) }
+ let(:user2) { create(:user) }
+ 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') }
+ let!(:another_note) { create(:note_on_commit, author: user, project: project, commit_id: project.repository.commit.id, note: 'another comment on a commit') }
+
+ before { project.team << [user, :reporter] }
+
+ describe "List repository commits" do
+ context "authorized user" do
+ before { project.team << [user2, :reporter] }
+
+ it "returns project commits" do
+ commit = project.repository.commit
+ get v3_api("/projects/#{project.id}/repository/commits", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first['id']).to eq(commit.id)
+ expect(json_response.first['committer_name']).to eq(commit.committer_name)
+ expect(json_response.first['committer_email']).to eq(commit.committer_email)
+ end
+ end
+
+ context "unauthorized user" do
+ it "does not return project commits" do
+ get v3_api("/projects/#{project.id}/repository/commits")
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ context "since optional parameter" do
+ it "returns project commits since provided parameter" do
+ commits = project.repository.commits("master")
+ since = commits.second.created_at
+
+ get v3_api("/projects/#{project.id}/repository/commits?since=#{since.utc.iso8601}", user)
+
+ expect(json_response.size).to eq 2
+ expect(json_response.first["id"]).to eq(commits.first.id)
+ expect(json_response.second["id"]).to eq(commits.second.id)
+ end
+ end
+
+ context "until optional parameter" do
+ it "returns project commits until provided parameter" do
+ commits = project.repository.commits("master")
+ before = commits.second.created_at
+
+ get v3_api("/projects/#{project.id}/repository/commits?until=#{before.utc.iso8601}", user)
+
+ if commits.size >= 20
+ expect(json_response.size).to eq(20)
+ else
+ expect(json_response.size).to eq(commits.size - 1)
+ end
+
+ expect(json_response.first["id"]).to eq(commits.second.id)
+ expect(json_response.second["id"]).to eq(commits.third.id)
+ end
+ end
+
+ context "invalid xmlschema date parameters" do
+ it "returns an invalid parameter error message" do
+ get v3_api("/projects/#{project.id}/repository/commits?since=invalid-date", user)
+
+ expect(response).to have_http_status(400)
+ expect(json_response['error']).to eq('since is invalid')
+ end
+ end
+
+ context "path optional parameter" do
+ it "returns project commits matching provided path parameter" do
+ path = 'files/ruby/popen.rb'
+
+ get v3_api("/projects/#{project.id}/repository/commits?path=#{path}", user)
+
+ expect(json_response.size).to eq(3)
+ expect(json_response.first["id"]).to eq("570e7b2abdd848b95f2f578043fc23bd6f6fd24d")
+ end
+ end
+ end
+
+ describe "Create a commit with multiple files and actions" do
+ let!(:url) { "/projects/#{project.id}/repository/commits" }
+
+ it 'returns a 403 unauthorized for user without permissions' do
+ post v3_api(url, user2)
+
+ expect(response).to have_http_status(403)
+ end
+
+ it 'returns a 400 bad request if no params are given' do
+ post v3_api(url, user)
+
+ expect(response).to have_http_status(400)
+ end
+
+ context :create do
+ let(:message) { 'Created file' }
+ let!(:invalid_c_params) do
+ {
+ branch_name: 'master',
+ commit_message: message,
+ actions: [
+ {
+ action: 'create',
+ file_path: 'files/ruby/popen.rb',
+ content: 'puts 8'
+ }
+ ]
+ }
+ end
+ let!(:valid_c_params) do
+ {
+ branch_name: 'master',
+ commit_message: message,
+ actions: [
+ {
+ action: 'create',
+ file_path: 'foo/bar/baz.txt',
+ content: 'puts 8'
+ }
+ ]
+ }
+ end
+
+ it 'a new file in project repo' do
+ post v3_api(url, user), valid_c_params
+
+ expect(response).to have_http_status(201)
+ expect(json_response['title']).to eq(message)
+ expect(json_response['committer_name']).to eq(user.name)
+ expect(json_response['committer_email']).to eq(user.email)
+ end
+
+ it 'returns a 400 bad request if file exists' do
+ post v3_api(url, user), invalid_c_params
+
+ expect(response).to have_http_status(400)
+ end
+
+ context 'with project path in URL' do
+ let(:url) { "/projects/#{project.full_path.gsub('/', '%2F')}/repository/commits" }
+
+ it 'a new file in project repo' do
+ post v3_api(url, user), valid_c_params
+
+ expect(response).to have_http_status(201)
+ end
+ end
+ end
+
+ context :delete do
+ let(:message) { 'Deleted file' }
+ let!(:invalid_d_params) do
+ {
+ branch_name: 'markdown',
+ commit_message: message,
+ actions: [
+ {
+ action: 'delete',
+ file_path: 'doc/api/projects.md'
+ }
+ ]
+ }
+ end
+ let!(:valid_d_params) do
+ {
+ branch_name: 'markdown',
+ commit_message: message,
+ actions: [
+ {
+ action: 'delete',
+ file_path: 'doc/api/users.md'
+ }
+ ]
+ }
+ end
+
+ it 'an existing file in project repo' do
+ post v3_api(url, user), valid_d_params
+
+ expect(response).to have_http_status(201)
+ expect(json_response['title']).to eq(message)
+ end
+
+ it 'returns a 400 bad request if file does not exist' do
+ post v3_api(url, user), invalid_d_params
+
+ expect(response).to have_http_status(400)
+ end
+ end
+
+ context :move do
+ let(:message) { 'Moved file' }
+ let!(:invalid_m_params) do
+ {
+ branch_name: 'feature',
+ commit_message: message,
+ actions: [
+ {
+ action: 'move',
+ file_path: 'CHANGELOG',
+ previous_path: 'VERSION',
+ content: '6.7.0.pre'
+ }
+ ]
+ }
+ end
+ let!(:valid_m_params) do
+ {
+ branch_name: 'feature',
+ commit_message: message,
+ actions: [
+ {
+ action: 'move',
+ file_path: 'VERSION.txt',
+ previous_path: 'VERSION',
+ content: '6.7.0.pre'
+ }
+ ]
+ }
+ end
+
+ it 'an existing file in project repo' do
+ post v3_api(url, user), valid_m_params
+
+ expect(response).to have_http_status(201)
+ expect(json_response['title']).to eq(message)
+ end
+
+ it 'returns a 400 bad request if file does not exist' do
+ post v3_api(url, user), invalid_m_params
+
+ expect(response).to have_http_status(400)
+ end
+ end
+
+ context :update do
+ let(:message) { 'Updated file' }
+ let!(:invalid_u_params) do
+ {
+ branch_name: 'master',
+ commit_message: message,
+ actions: [
+ {
+ action: 'update',
+ file_path: 'foo/bar.baz',
+ content: 'puts 8'
+ }
+ ]
+ }
+ end
+ let!(:valid_u_params) do
+ {
+ branch_name: 'master',
+ commit_message: message,
+ actions: [
+ {
+ action: 'update',
+ file_path: 'files/ruby/popen.rb',
+ content: 'puts 8'
+ }
+ ]
+ }
+ end
+
+ it 'an existing file in project repo' do
+ post v3_api(url, user), valid_u_params
+
+ expect(response).to have_http_status(201)
+ expect(json_response['title']).to eq(message)
+ end
+
+ it 'returns a 400 bad request if file does not exist' do
+ post v3_api(url, user), invalid_u_params
+
+ expect(response).to have_http_status(400)
+ end
+ end
+
+ context "multiple operations" do
+ let(:message) { 'Multiple actions' }
+ let!(:invalid_mo_params) do
+ {
+ branch_name: 'master',
+ commit_message: message,
+ actions: [
+ {
+ action: 'create',
+ file_path: 'files/ruby/popen.rb',
+ content: 'puts 8'
+ },
+ {
+ action: 'delete',
+ file_path: 'doc/v3_api/projects.md'
+ },
+ {
+ action: 'move',
+ file_path: 'CHANGELOG',
+ previous_path: 'VERSION',
+ content: '6.7.0.pre'
+ },
+ {
+ action: 'update',
+ file_path: 'foo/bar.baz',
+ content: 'puts 8'
+ }
+ ]
+ }
+ end
+ let!(:valid_mo_params) do
+ {
+ branch_name: 'master',
+ commit_message: message,
+ actions: [
+ {
+ action: 'create',
+ file_path: 'foo/bar/baz.txt',
+ content: 'puts 8'
+ },
+ {
+ action: 'delete',
+ file_path: 'Gemfile.zip'
+ },
+ {
+ action: 'move',
+ file_path: 'VERSION.txt',
+ previous_path: 'VERSION',
+ content: '6.7.0.pre'
+ },
+ {
+ action: 'update',
+ file_path: 'files/ruby/popen.rb',
+ content: 'puts 8'
+ }
+ ]
+ }
+ end
+
+ it 'are commited as one in project repo' do
+ post v3_api(url, user), valid_mo_params
+
+ expect(response).to have_http_status(201)
+ expect(json_response['title']).to eq(message)
+ end
+
+ it 'return a 400 bad request if there are any issues' do
+ post v3_api(url, user), invalid_mo_params
+
+ expect(response).to have_http_status(400)
+ end
+ end
+ end
+
+ describe "Get a single commit" do
+ context "authorized user" do
+ it "returns a commit by sha" do
+ get v3_api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['id']).to eq(project.repository.commit.id)
+ expect(json_response['title']).to eq(project.repository.commit.title)
+ expect(json_response['stats']['additions']).to eq(project.repository.commit.stats.additions)
+ expect(json_response['stats']['deletions']).to eq(project.repository.commit.stats.deletions)
+ expect(json_response['stats']['total']).to eq(project.repository.commit.stats.total)
+ end
+
+ it "returns a 404 error if not found" do
+ get v3_api("/projects/#{project.id}/repository/commits/invalid_sha", user)
+ expect(response).to have_http_status(404)
+ end
+
+ it "returns nil for commit without CI" do
+ get v3_api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['status']).to be_nil
+ end
+
+ it "returns status for CI" do
+ pipeline = project.ensure_pipeline('master', project.repository.commit.sha)
+ pipeline.update(status: 'success')
+
+ get v3_api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['status']).to eq(pipeline.status)
+ end
+
+ it "returns status for CI when pipeline is created" do
+ project.ensure_pipeline('master', project.repository.commit.sha)
+
+ get v3_api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['status']).to eq("created")
+ end
+ end
+
+ context "unauthorized user" do
+ it "does not return the selected commit" do
+ get v3_api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}")
+ expect(response).to have_http_status(401)
+ end
+ end
+ end
+
+ describe "Get the diff of a commit" do
+ context "authorized user" do
+ before { project.team << [user2, :reporter] }
+
+ it "returns the diff of the selected commit" do
+ get v3_api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/diff", user)
+ expect(response).to have_http_status(200)
+
+ expect(json_response).to be_an Array
+ expect(json_response.length).to be >= 1
+ expect(json_response.first.keys).to include "diff"
+ end
+
+ it "returns a 404 error if invalid commit" do
+ get v3_api("/projects/#{project.id}/repository/commits/invalid_sha/diff", user)
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context "unauthorized user" do
+ it "does not return the diff of the selected commit" do
+ get v3_api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/diff")
+ expect(response).to have_http_status(401)
+ end
+ end
+ end
+
+ describe 'Get the comments of a commit' do
+ context 'authorized user' do
+ it 'returns merge_request comments' do
+ get v3_api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments", user)
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(2)
+ expect(json_response.first['note']).to eq('a comment on a commit')
+ expect(json_response.first['author']['id']).to eq(user.id)
+ end
+
+ it 'returns a 404 error if merge_request_id not found' do
+ get v3_api("/projects/#{project.id}/repository/commits/1234ab/comments", user)
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'unauthorized user' do
+ it 'does not return the diff of the selected commit' do
+ get v3_api("/projects/#{project.id}/repository/commits/1234ab/comments")
+ expect(response).to have_http_status(401)
+ end
+ end
+ end
+
+ describe 'POST :id/repository/commits/:sha/cherry_pick' do
+ let(:master_pickable_commit) { project.commit('7d3b0f7cff5f37573aea97cebfd5692ea1689924') }
+
+ context 'authorized user' do
+ it 'cherry picks a commit' do
+ post v3_api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick", user), branch: 'master'
+
+ expect(response).to have_http_status(201)
+ expect(json_response['title']).to eq(master_pickable_commit.title)
+ expect(json_response['message']).to eq(master_pickable_commit.message)
+ expect(json_response['author_name']).to eq(master_pickable_commit.author_name)
+ expect(json_response['committer_name']).to eq(user.name)
+ end
+
+ it 'returns 400 if commit is already included in the target branch' do
+ post v3_api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick", user), branch: 'markdown'
+
+ expect(response).to have_http_status(400)
+ expect(json_response['message']).to eq('Sorry, we cannot cherry-pick this commit automatically.
+ A cherry-pick may have already been performed with this commit, or a more recent commit may have updated some of its content.')
+ end
+
+ it 'returns 400 if you are not allowed to push to the target branch' do
+ project.team << [user2, :developer]
+ protected_branch = create(:protected_branch, project: project, name: 'feature')
+
+ post v3_api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick", user2), branch: protected_branch.name
+
+ expect(response).to have_http_status(400)
+ expect(json_response['message']).to eq('You are not allowed to push into this branch')
+ end
+
+ it 'returns 400 for missing parameters' do
+ post v3_api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick", user)
+
+ expect(response).to have_http_status(400)
+ expect(json_response['error']).to eq('branch is missing')
+ end
+
+ it 'returns 404 if commit is not found' do
+ post v3_api("/projects/#{project.id}/repository/commits/abcd0123/cherry_pick", user), branch: 'master'
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 Commit Not Found')
+ end
+
+ it 'returns 404 if branch is not found' do
+ post v3_api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick", user), branch: 'foo'
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 Branch Not Found')
+ end
+
+ it 'returns 400 for missing parameters' do
+ post v3_api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick", user)
+
+ expect(response).to have_http_status(400)
+ expect(json_response['error']).to eq('branch is missing')
+ end
+ end
+
+ context 'unauthorized user' do
+ it 'does not cherry pick the commit' do
+ post v3_api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick"), branch: 'master'
+
+ expect(response).to have_http_status(401)
+ end
+ end
+ end
+
+ describe 'Post comment to commit' do
+ context 'authorized user' do
+ it 'returns comment' do
+ post v3_api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments", user), note: 'My comment'
+ expect(response).to have_http_status(201)
+ expect(json_response['note']).to eq('My comment')
+ expect(json_response['path']).to be_nil
+ expect(json_response['line']).to be_nil
+ expect(json_response['line_type']).to be_nil
+ end
+
+ it 'returns the inline comment' do
+ post v3_api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments", user), note: 'My comment', path: project.repository.commit.raw_diffs.first.new_path, line: 1, line_type: 'new'
+
+ expect(response).to have_http_status(201)
+ expect(json_response['note']).to eq('My comment')
+ expect(json_response['path']).to eq(project.repository.commit.raw_diffs.first.new_path)
+ expect(json_response['line']).to eq(1)
+ expect(json_response['line_type']).to eq('new')
+ end
+
+ it 'returns 400 if note is missing' do
+ post v3_api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments", user)
+ expect(response).to have_http_status(400)
+ end
+
+ it 'returns 404 if note is attached to non existent commit' do
+ post v3_api("/projects/#{project.id}/repository/commits/1234ab/comments", user), note: 'My comment'
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'unauthorized user' do
+ it 'does not return the diff of the selected commit' do
+ post v3_api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments")
+ expect(response).to have_http_status(401)
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/v3/environments_spec.rb b/spec/requests/api/v3/environments_spec.rb
new file mode 100644
index 00000000000..1ac666ab240
--- /dev/null
+++ b/spec/requests/api/v3/environments_spec.rb
@@ -0,0 +1,39 @@
+require 'spec_helper'
+
+describe API::V3::Environments, api: true do
+ include ApiHelpers
+
+ let(:user) { create(:user) }
+ let(:non_member) { create(:user) }
+ let(:project) { create(:empty_project, :private, namespace: user.namespace) }
+ let!(:environment) { create(:environment, project: project) }
+
+ before do
+ project.team << [user, :master]
+ end
+
+ describe 'DELETE /projects/:id/environments/:environment_id' do
+ context 'as a master' do
+ it 'returns a 200 for an existing environment' do
+ delete v3_api("/projects/#{project.id}/environments/#{environment.id}", user)
+
+ expect(response).to have_http_status(200)
+ end
+
+ it 'returns a 404 for non existing id' do
+ delete v3_api("/projects/#{project.id}/environments/12345", user)
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 Not found')
+ end
+ end
+
+ context 'a non member' do
+ it 'rejects the request' do
+ delete v3_api("/projects/#{project.id}/environments/#{environment.id}", non_member)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/v3/files_spec.rb b/spec/requests/api/v3/files_spec.rb
new file mode 100644
index 00000000000..93637053626
--- /dev/null
+++ b/spec/requests/api/v3/files_spec.rb
@@ -0,0 +1,271 @@
+require 'spec_helper'
+
+describe API::V3::Files, api: true do
+ include ApiHelpers
+
+ # I have to remove periods from the end of the name
+ # This happened when the user's name had a suffix (i.e. "Sr.")
+ # This seems to be what git does under the hood. For example, this commit:
+ #
+ # $ git commit --author='Foo Sr. <foo@example.com>' -m 'Where's my trailing period?'
+ #
+ # results in this:
+ #
+ # $ git show --pretty
+ # ...
+ # Author: Foo Sr <foo@example.com>
+ # ...
+
+ let(:user) { create(:user) }
+ 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
+ {
+ file_path: file_path,
+ ref: 'master'
+ }
+ end
+ let(:author_email) { FFaker::Internet.email }
+ let(:author_name) { FFaker::Name.name.chomp("\.") }
+
+ before { project.team << [user, :developer] }
+
+ describe "GET /projects/:id/repository/files" do
+ let(:route) { "/projects/#{project.id}/repository/files" }
+
+ shared_examples_for 'repository files' do
+ it "returns file info" do
+ get v3_api(route, current_user), params
+
+ expect(response).to have_http_status(200)
+ expect(json_response['file_path']).to eq(file_path)
+ expect(json_response['file_name']).to eq('popen.rb')
+ expect(json_response['last_commit_id']).to eq('570e7b2abdd848b95f2f578043fc23bd6f6fd24d')
+ expect(Base64.decode64(json_response['content']).lines.first).to eq("require 'fileutils'\n")
+ end
+
+ context 'when no params are given' do
+ it_behaves_like '400 response' do
+ let(:request) { get v3_api(route, current_user) }
+ end
+ end
+
+ context 'when file_path does not exist' do
+ let(:params) do
+ {
+ file_path: 'app/models/application.rb',
+ ref: 'master',
+ }
+ end
+
+ it_behaves_like '404 response' do
+ let(:request) { get v3_api(route, current_user), params }
+ let(:message) { '404 File Not Found' }
+ end
+ end
+
+ context 'when repository is disabled' do
+ include_context 'disabled repository'
+
+ it_behaves_like '403 response' do
+ let(:request) { get v3_api(route, current_user), params }
+ end
+ end
+ end
+
+ context 'when unauthenticated', 'and project is public' do
+ it_behaves_like 'repository files' do
+ let(:project) { create(:project, :public) }
+ let(:current_user) { nil }
+ end
+ end
+
+ context 'when unauthenticated', 'and project is private' do
+ it_behaves_like '404 response' do
+ let(:request) { get v3_api(route), params }
+ let(:message) { '404 Project Not Found' }
+ end
+ end
+
+ context 'when authenticated', 'as a developer' do
+ it_behaves_like 'repository files' do
+ let(:current_user) { user }
+ end
+ end
+
+ context 'when authenticated', 'as a guest' do
+ it_behaves_like '403 response' do
+ let(:request) { get v3_api(route, guest), params }
+ end
+ end
+ end
+
+ describe "POST /projects/:id/repository/files" do
+ let(:valid_params) do
+ {
+ file_path: 'newfile.rb',
+ branch_name: 'master',
+ content: 'puts 8',
+ commit_message: 'Added newfile'
+ }
+ end
+
+ it "creates a new file in project repo" do
+ post v3_api("/projects/#{project.id}/repository/files", user), valid_params
+
+ expect(response).to have_http_status(201)
+ expect(json_response['file_path']).to eq('newfile.rb')
+ last_commit = project.repository.commit.raw
+ expect(last_commit.author_email).to eq(user.email)
+ expect(last_commit.author_name).to eq(user.name)
+ end
+
+ it "returns a 400 bad request if no params given" do
+ post v3_api("/projects/#{project.id}/repository/files", user)
+
+ expect(response).to have_http_status(400)
+ end
+
+ it "returns a 400 if editor fails to create file" do
+ allow_any_instance_of(Repository).to receive(:create_file).
+ and_return(false)
+
+ post v3_api("/projects/#{project.id}/repository/files", user), valid_params
+
+ expect(response).to have_http_status(400)
+ end
+
+ context "when specifying an author" do
+ it "creates a new file with the specified author" do
+ valid_params.merge!(author_email: author_email, author_name: author_name)
+
+ post v3_api("/projects/#{project.id}/repository/files", user), valid_params
+
+ expect(response).to have_http_status(201)
+ last_commit = project.repository.commit.raw
+ expect(last_commit.author_email).to eq(author_email)
+ expect(last_commit.author_name).to eq(author_name)
+ end
+ end
+ end
+
+ describe "PUT /projects/:id/repository/files" do
+ let(:valid_params) do
+ {
+ file_path: file_path,
+ branch_name: 'master',
+ content: 'puts 8',
+ commit_message: 'Changed file'
+ }
+ end
+
+ it "updates existing file in project repo" do
+ put v3_api("/projects/#{project.id}/repository/files", user), valid_params
+
+ expect(response).to have_http_status(200)
+ expect(json_response['file_path']).to eq(file_path)
+ last_commit = project.repository.commit.raw
+ expect(last_commit.author_email).to eq(user.email)
+ expect(last_commit.author_name).to eq(user.name)
+ end
+
+ it "returns a 400 bad request if no params given" do
+ put v3_api("/projects/#{project.id}/repository/files", user)
+
+ expect(response).to have_http_status(400)
+ end
+
+ context "when specifying an author" do
+ it "updates a file with the specified author" do
+ valid_params.merge!(author_email: author_email, author_name: author_name, content: "New content")
+
+ put v3_api("/projects/#{project.id}/repository/files", user), valid_params
+
+ expect(response).to have_http_status(200)
+ last_commit = project.repository.commit.raw
+ expect(last_commit.author_email).to eq(author_email)
+ expect(last_commit.author_name).to eq(author_name)
+ end
+ end
+ end
+
+ describe "DELETE /projects/:id/repository/files" do
+ let(:valid_params) do
+ {
+ file_path: file_path,
+ branch_name: 'master',
+ commit_message: 'Changed file'
+ }
+ end
+
+ it "deletes existing file in project repo" do
+ delete v3_api("/projects/#{project.id}/repository/files", user), valid_params
+
+ expect(response).to have_http_status(200)
+ expect(json_response['file_path']).to eq(file_path)
+ last_commit = project.repository.commit.raw
+ expect(last_commit.author_email).to eq(user.email)
+ expect(last_commit.author_name).to eq(user.name)
+ end
+
+ it "returns a 400 bad request if no params given" do
+ delete v3_api("/projects/#{project.id}/repository/files", user)
+
+ expect(response).to have_http_status(400)
+ end
+
+ it "returns a 400 if fails to create file" do
+ allow_any_instance_of(Repository).to receive(:delete_file).and_return(false)
+
+ delete v3_api("/projects/#{project.id}/repository/files", user), valid_params
+
+ expect(response).to have_http_status(400)
+ end
+
+ context "when specifying an author" do
+ it "removes a file with the specified author" do
+ valid_params.merge!(author_email: author_email, author_name: author_name)
+
+ delete v3_api("/projects/#{project.id}/repository/files", user), valid_params
+
+ expect(response).to have_http_status(200)
+ last_commit = project.repository.commit.raw
+ expect(last_commit.author_email).to eq(author_email)
+ expect(last_commit.author_name).to eq(author_name)
+ end
+ end
+ end
+
+ describe "POST /projects/:id/repository/files with binary file" do
+ let(:file_path) { 'test.bin' }
+ let(:put_params) do
+ {
+ file_path: file_path,
+ branch_name: 'master',
+ content: 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUAAACnej3aAAAAAXRSTlMAQObYZgAAAApJREFUCNdjYAAAAAIAAeIhvDMAAAAASUVORK5CYII=',
+ commit_message: 'Binary file with a \n should not be touched',
+ encoding: 'base64'
+ }
+ end
+ let(:get_params) do
+ {
+ file_path: file_path,
+ ref: 'master',
+ }
+ end
+
+ before do
+ post v3_api("/projects/#{project.id}/repository/files", user), put_params
+ end
+
+ it "remains unchanged" do
+ get v3_api("/projects/#{project.id}/repository/files", user), get_params
+
+ expect(response).to have_http_status(200)
+ expect(json_response['file_path']).to eq(file_path)
+ expect(json_response['file_name']).to eq(file_path)
+ expect(json_response['content']).to eq(put_params[:content])
+ end
+ end
+end
diff --git a/spec/requests/api/v3/groups_spec.rb b/spec/requests/api/v3/groups_spec.rb
new file mode 100644
index 00000000000..8b29ad03737
--- /dev/null
+++ b/spec/requests/api/v3/groups_spec.rb
@@ -0,0 +1,35 @@
+require 'spec_helper'
+
+describe API::V3::Groups, api: true do
+ include ApiHelpers
+ include UploadHelpers
+
+ let(:user2) { create(:user) }
+ let!(:group2) { create(:group, :private) }
+ let!(:project2) { create(:empty_project, namespace: group2) }
+
+ before do
+ group2.add_owner(user2)
+ end
+
+ describe 'GET /groups/owned' do
+ context 'when unauthenticated' do
+ it 'returns authentication error' do
+ get v3_api('/groups/owned')
+
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ context 'when authenticated as group owner' do
+ it 'returns an array of groups the user owns' do
+ get v3_api('/groups/owned', user2)
+
+ expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.first['name']).to eq(group2.name)
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/v3/issues_spec.rb b/spec/requests/api/v3/issues_spec.rb
index 33a127de98a..803acd55470 100644
--- a/spec/requests/api/v3/issues_spec.rb
+++ b/spec/requests/api/v3/issues_spec.rb
@@ -722,7 +722,7 @@ describe API::V3::Issues, api: true do
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['labels']).to eq(%w(label label2))
expect(json_response['confidential']).to be_falsy
end
@@ -986,6 +986,33 @@ describe API::V3::Issues, api: true do
end
end
+ describe 'PUT /projects/:id/issues/:issue_id with spam filtering' do
+ let(:params) do
+ {
+ title: 'updated title',
+ description: 'content here',
+ labels: 'label, label2'
+ }
+ end
+
+ it "does not create a new project issue" do
+ allow_any_instance_of(SpamService).to receive_messages(check_for_spam?: true)
+ allow_any_instance_of(AkismetService).to receive_messages(is_spam?: true)
+
+ put v3_api("/projects/#{project.id}/issues/#{issue.id}", user), params
+
+ 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('updated title')
+ 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 labels' do
let!(:label) { create(:label, title: 'dummy', project: project) }
let!(:label_link) { create(:label_link, label: label, target: issue) }
diff --git a/spec/requests/api/v3/labels_spec.rb b/spec/requests/api/v3/labels_spec.rb
new file mode 100644
index 00000000000..dfac357d37c
--- /dev/null
+++ b/spec/requests/api/v3/labels_spec.rb
@@ -0,0 +1,171 @@
+require 'spec_helper'
+
+describe API::V3::Labels, api: true do
+ include ApiHelpers
+
+ let(:user) { create(:user) }
+ 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) }
+
+ before do
+ project.team << [user, :master]
+ end
+
+ describe 'GET /projects/:id/labels' do
+ it 'returns all available labels to the project' do
+ group = create(:group)
+ group_label = create(:group_label, title: 'feature', group: group)
+ project.update(group: group)
+ create(:labeled_issue, project: project, labels: [group_label], author: user)
+ create(:labeled_issue, project: project, labels: [label1], author: user, state: :closed)
+ create(:labeled_merge_request, labels: [priority_label], author: user, source_project: project )
+
+ expected_keys = %w(
+ id name color description
+ open_issues_count closed_issues_count open_merge_requests_count
+ subscribed priority
+ )
+
+ get v3_api("/projects/#{project.id}/labels", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.size).to eq(3)
+ expect(json_response.first.keys).to match_array expected_keys
+ expect(json_response.map { |l| l['name'] }).to match_array([group_label.name, priority_label.name, label1.name])
+
+ label1_response = json_response.find { |l| l['name'] == label1.title }
+ group_label_response = json_response.find { |l| l['name'] == group_label.title }
+ priority_label_response = json_response.find { |l| l['name'] == priority_label.title }
+
+ expect(label1_response['open_issues_count']).to eq(0)
+ expect(label1_response['closed_issues_count']).to eq(1)
+ expect(label1_response['open_merge_requests_count']).to eq(0)
+ expect(label1_response['name']).to eq(label1.name)
+ expect(label1_response['color']).to be_present
+ expect(label1_response['description']).to be_nil
+ expect(label1_response['priority']).to be_nil
+ expect(label1_response['subscribed']).to be_falsey
+
+ expect(group_label_response['open_issues_count']).to eq(1)
+ expect(group_label_response['closed_issues_count']).to eq(0)
+ expect(group_label_response['open_merge_requests_count']).to eq(0)
+ expect(group_label_response['name']).to eq(group_label.name)
+ expect(group_label_response['color']).to be_present
+ expect(group_label_response['description']).to be_nil
+ expect(group_label_response['priority']).to be_nil
+ expect(group_label_response['subscribed']).to be_falsey
+
+ expect(priority_label_response['open_issues_count']).to eq(0)
+ expect(priority_label_response['closed_issues_count']).to eq(0)
+ expect(priority_label_response['open_merge_requests_count']).to eq(1)
+ expect(priority_label_response['name']).to eq(priority_label.name)
+ expect(priority_label_response['color']).to be_present
+ expect(priority_label_response['description']).to be_nil
+ expect(priority_label_response['priority']).to eq(3)
+ expect(priority_label_response['subscribed']).to be_falsey
+ end
+ end
+
+ describe "POST /projects/:id/labels/:label_id/subscription" do
+ context "when label_id is a label title" do
+ it "subscribes to the label" do
+ post v3_api("/projects/#{project.id}/labels/#{label1.title}/subscription", user)
+
+ expect(response).to have_http_status(201)
+ expect(json_response["name"]).to eq(label1.title)
+ expect(json_response["subscribed"]).to be_truthy
+ end
+ end
+
+ context "when label_id is a label ID" do
+ it "subscribes to the label" do
+ post v3_api("/projects/#{project.id}/labels/#{label1.id}/subscription", user)
+
+ expect(response).to have_http_status(201)
+ expect(json_response["name"]).to eq(label1.title)
+ expect(json_response["subscribed"]).to be_truthy
+ end
+ end
+
+ context "when user is already subscribed to label" do
+ before { label1.subscribe(user, project) }
+
+ it "returns 304" do
+ post v3_api("/projects/#{project.id}/labels/#{label1.id}/subscription", user)
+
+ expect(response).to have_http_status(304)
+ end
+ end
+
+ context "when label ID is not found" do
+ it "returns 404 error" do
+ post v3_api("/projects/#{project.id}/labels/1234/subscription", user)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ describe "DELETE /projects/:id/labels/:label_id/subscription" do
+ before { label1.subscribe(user, project) }
+
+ context "when label_id is a label title" do
+ it "unsubscribes from the label" do
+ delete v3_api("/projects/#{project.id}/labels/#{label1.title}/subscription", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response["name"]).to eq(label1.title)
+ expect(json_response["subscribed"]).to be_falsey
+ end
+ end
+
+ context "when label_id is a label ID" do
+ it "unsubscribes from the label" do
+ delete v3_api("/projects/#{project.id}/labels/#{label1.id}/subscription", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response["name"]).to eq(label1.title)
+ expect(json_response["subscribed"]).to be_falsey
+ end
+ end
+
+ context "when user is already unsubscribed from label" do
+ before { label1.unsubscribe(user, project) }
+
+ it "returns 304" do
+ delete v3_api("/projects/#{project.id}/labels/#{label1.id}/subscription", user)
+
+ expect(response).to have_http_status(304)
+ end
+ end
+
+ context "when label ID is not found" do
+ it "returns 404 error" do
+ delete v3_api("/projects/#{project.id}/labels/1234/subscription", user)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ describe 'DELETE /projects/:id/labels' do
+ it 'returns 200 for existing label' do
+ delete v3_api("/projects/#{project.id}/labels", user), name: 'label1'
+
+ expect(response).to have_http_status(200)
+ end
+
+ it 'returns 404 for non existing label' do
+ delete v3_api("/projects/#{project.id}/labels", user), name: 'label2'
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 Label Not Found')
+ end
+
+ it 'returns 400 for wrong parameters' do
+ delete v3_api("/projects/#{project.id}/labels", user)
+ expect(response).to have_http_status(400)
+ end
+ end
+end
diff --git a/spec/requests/api/v3/members_spec.rb b/spec/requests/api/v3/members_spec.rb
index 28c3ca03960..13814ed10c3 100644
--- a/spec/requests/api/v3/members_spec.rb
+++ b/spec/requests/api/v3/members_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe API::Members, api: true do
+describe API::V3::Members, api: true do
include ApiHelpers
let(:master) { create(:user) }
diff --git a/spec/requests/api/v3/merge_requests_spec.rb b/spec/requests/api/v3/merge_requests_spec.rb
index b94e1ef4ced..51764d1000e 100644
--- a/spec/requests/api/v3/merge_requests_spec.rb
+++ b/spec/requests/api/v3/merge_requests_spec.rb
@@ -237,7 +237,7 @@ describe API::MergeRequests, api: true do
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['labels']).to eq(%w(label label2))
expect(json_response['milestone']['id']).to eq(milestone.id)
expect(json_response['force_remove_source_branch']).to be_truthy
end
diff --git a/spec/requests/api/v3/notes_spec.rb b/spec/requests/api/v3/notes_spec.rb
new file mode 100644
index 00000000000..b8f0260c6a2
--- /dev/null
+++ b/spec/requests/api/v3/notes_spec.rb
@@ -0,0 +1,433 @@
+require 'spec_helper'
+
+describe API::V3::Notes, api: true do
+ include ApiHelpers
+
+ let(:user) { create(:user) }
+ 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) }
+ let!(:issue_note) { create(:note, noteable: issue, project: project, author: user) }
+ let!(:merge_request_note) { create(:note, noteable: merge_request, project: project, author: user) }
+ let!(:snippet_note) { create(:note, noteable: snippet, project: project, author: user) }
+
+ # For testing the cross-reference of a private issue in a public issue
+ let(:private_user) { create(:user) }
+ let(:private_project) do
+ 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(:empty_project, :public) }
+ let(:ext_issue) { create(:issue, project: ext_proj) }
+
+ let!(:cross_reference_note) do
+ create :note,
+ noteable: ext_issue, project: ext_proj,
+ note: "mentioned in issue #{private_issue.to_reference(ext_proj)}",
+ system: true
+ end
+
+ before { project.team << [user, :reporter] }
+
+ describe "GET /projects/:id/noteable/:noteable_id/notes" do
+ context "when noteable is an Issue" do
+ it "returns an array of issue notes" do
+ get v3_api("/projects/#{project.id}/issues/#{issue.id}/notes", user)
+
+ expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.first['body']).to eq(issue_note.note)
+ expect(json_response.first['upvote']).to be_falsey
+ expect(json_response.first['downvote']).to be_falsey
+ end
+
+ it "returns a 404 error when issue id not found" do
+ get v3_api("/projects/#{project.id}/issues/12345/notes", user)
+
+ expect(response).to have_http_status(404)
+ end
+
+ context "and current user cannot view the notes" do
+ it "returns an empty array" do
+ get v3_api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes", user)
+
+ expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response).to be_empty
+ end
+
+ context "and issue is confidential" do
+ before { ext_issue.update_attributes(confidential: true) }
+
+ it "returns 404" do
+ get v3_api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes", user)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context "and current user can view the note" do
+ it "returns an empty array" do
+ get v3_api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes", private_user)
+
+ expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.first['body']).to eq(cross_reference_note.note)
+ end
+ end
+ end
+ end
+
+ context "when noteable is a Snippet" do
+ it "returns an array of snippet notes" do
+ get v3_api("/projects/#{project.id}/snippets/#{snippet.id}/notes", user)
+
+ expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.first['body']).to eq(snippet_note.note)
+ end
+
+ it "returns a 404 error when snippet id not found" do
+ get v3_api("/projects/#{project.id}/snippets/42/notes", user)
+
+ expect(response).to have_http_status(404)
+ end
+
+ it "returns 404 when not authorized" do
+ get v3_api("/projects/#{project.id}/snippets/#{snippet.id}/notes", private_user)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context "when noteable is a Merge Request" do
+ it "returns an array of merge_requests notes" do
+ get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/notes", user)
+
+ expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.first['body']).to eq(merge_request_note.note)
+ end
+
+ it "returns a 404 error if merge request id not found" do
+ get v3_api("/projects/#{project.id}/merge_requests/4444/notes", user)
+
+ expect(response).to have_http_status(404)
+ end
+
+ it "returns 404 when not authorized" do
+ get v3_api("/projects/#{project.id}/merge_requests/4444/notes", private_user)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ describe "GET /projects/:id/noteable/:noteable_id/notes/:note_id" do
+ context "when noteable is an Issue" do
+ it "returns an issue note by id" do
+ get v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/#{issue_note.id}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['body']).to eq(issue_note.note)
+ end
+
+ it "returns a 404 error if issue note not found" do
+ get v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/12345", user)
+
+ expect(response).to have_http_status(404)
+ end
+
+ context "and current user cannot view the note" do
+ it "returns a 404 error" do
+ get v3_api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes/#{cross_reference_note.id}", user)
+
+ expect(response).to have_http_status(404)
+ end
+
+ context "when issue is confidential" do
+ before { issue.update_attributes(confidential: true) }
+
+ it "returns 404" do
+ get v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/#{issue_note.id}", private_user)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context "and current user can view the note" do
+ it "returns an issue note by id" do
+ get v3_api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes/#{cross_reference_note.id}", private_user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['body']).to eq(cross_reference_note.note)
+ end
+ end
+ end
+ end
+
+ context "when noteable is a Snippet" do
+ it "returns a snippet note by id" do
+ get v3_api("/projects/#{project.id}/snippets/#{snippet.id}/notes/#{snippet_note.id}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['body']).to eq(snippet_note.note)
+ end
+
+ it "returns a 404 error if snippet note not found" do
+ get v3_api("/projects/#{project.id}/snippets/#{snippet.id}/notes/12345", user)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ describe "POST /projects/:id/noteable/:noteable_id/notes" do
+ context "when noteable is an Issue" do
+ it "creates a new issue note" do
+ post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes", user), body: 'hi!'
+
+ expect(response).to have_http_status(201)
+ expect(json_response['body']).to eq('hi!')
+ expect(json_response['author']['username']).to eq(user.username)
+ end
+
+ it "returns a 400 bad request error if body not given" do
+ post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes", user)
+
+ expect(response).to have_http_status(400)
+ end
+
+ it "returns a 401 unauthorized error if user not authenticated" do
+ post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes"), body: 'hi!'
+
+ expect(response).to have_http_status(401)
+ 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/#{issue.id}/notes", user),
+ body: 'hi!', created_at: creation_time
+
+ expect(response).to have_http_status(201)
+ expect(json_response['body']).to eq('hi!')
+ expect(json_response['author']['username']).to eq(user.username)
+ expect(Time.parse(json_response['created_at'])).to be_like_time(creation_time)
+ end
+ end
+
+ context 'when the user is posting an award emoji on an issue created by someone else' do
+ let(:issue2) { create(:issue, project: project) }
+
+ it 'returns an award emoji' do
+ post v3_api("/projects/#{project.id}/issues/#{issue2.id}/notes", user), body: ':+1:'
+
+ expect(response).to have_http_status(201)
+ expect(json_response['awardable_id']).to eq issue2.id
+ end
+ end
+
+ context 'when the user is posting an award emoji on his/her own issue' do
+ it 'creates a new issue note' do
+ post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes", user), body: ':+1:'
+
+ expect(response).to have_http_status(201)
+ expect(json_response['body']).to eq(':+1:')
+ end
+ end
+ end
+
+ context "when noteable is a Snippet" do
+ it "creates a new snippet note" do
+ post v3_api("/projects/#{project.id}/snippets/#{snippet.id}/notes", user), body: 'hi!'
+
+ expect(response).to have_http_status(201)
+ expect(json_response['body']).to eq('hi!')
+ expect(json_response['author']['username']).to eq(user.username)
+ end
+
+ it "returns a 400 bad request error if body not given" do
+ post v3_api("/projects/#{project.id}/snippets/#{snippet.id}/notes", user)
+
+ expect(response).to have_http_status(400)
+ end
+
+ it "returns a 401 unauthorized error if user not authenticated" do
+ post v3_api("/projects/#{project.id}/snippets/#{snippet.id}/notes"), body: 'hi!'
+
+ expect(response).to have_http_status(401)
+ 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 v3_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(:empty_project, :private)) }
+
+ ##
+ # We are posting to project user has access to, but we use issue id
+ # from a different project, see #15577
+ #
+ before do
+ post v3_api("/projects/#{project.id}/issues/#{private_issue.id}/notes", user),
+ body: 'Hi!'
+ end
+
+ it 'responds with resource not found error' do
+ expect(response.status).to eq 404
+ end
+
+ it 'does not create new note' do
+ expect(private_issue.notes.reload).to be_empty
+ end
+ end
+ end
+
+ describe "POST /projects/:id/noteable/:noteable_id/notes to test observer on create" do
+ it "creates an activity event when an issue note is created" do
+ expect(Event).to receive(:create)
+
+ post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes", user), body: 'hi!'
+ end
+ end
+
+ describe 'PUT /projects/:id/noteable/:noteable_id/notes/:note_id' do
+ context 'when noteable is an Issue' do
+ it 'returns modified note' do
+ put v3_api("/projects/#{project.id}/issues/#{issue.id}/"\
+ "notes/#{issue_note.id}", user), body: 'Hello!'
+
+ expect(response).to have_http_status(200)
+ expect(json_response['body']).to eq('Hello!')
+ end
+
+ it 'returns a 404 error when note id not found' do
+ put v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/12345", user),
+ body: 'Hello!'
+
+ expect(response).to have_http_status(404)
+ end
+
+ it 'returns a 400 bad request error if body not given' do
+ put v3_api("/projects/#{project.id}/issues/#{issue.id}/"\
+ "notes/#{issue_note.id}", user)
+
+ expect(response).to have_http_status(400)
+ end
+ end
+
+ context 'when noteable is a Snippet' do
+ it 'returns modified note' do
+ put v3_api("/projects/#{project.id}/snippets/#{snippet.id}/"\
+ "notes/#{snippet_note.id}", user), body: 'Hello!'
+
+ expect(response).to have_http_status(200)
+ expect(json_response['body']).to eq('Hello!')
+ end
+
+ it 'returns a 404 error when note id not found' do
+ put v3_api("/projects/#{project.id}/snippets/#{snippet.id}/"\
+ "notes/12345", user), body: "Hello!"
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'when noteable is a Merge Request' do
+ it 'returns modified note' do
+ put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/"\
+ "notes/#{merge_request_note.id}", user), body: 'Hello!'
+
+ expect(response).to have_http_status(200)
+ expect(json_response['body']).to eq('Hello!')
+ end
+
+ it 'returns a 404 error when note id not found' do
+ put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/"\
+ "notes/12345", user), body: "Hello!"
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ describe 'DELETE /projects/:id/noteable/:noteable_id/notes/:note_id' do
+ context 'when noteable is an Issue' do
+ it 'deletes a note' do
+ delete v3_api("/projects/#{project.id}/issues/#{issue.id}/"\
+ "notes/#{issue_note.id}", user)
+
+ expect(response).to have_http_status(200)
+ # Check if note is really deleted
+ delete v3_api("/projects/#{project.id}/issues/#{issue.id}/"\
+ "notes/#{issue_note.id}", user)
+ expect(response).to have_http_status(404)
+ end
+
+ it 'returns a 404 error when note id not found' do
+ delete v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/12345", user)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'when noteable is a Snippet' do
+ it 'deletes a note' do
+ delete v3_api("/projects/#{project.id}/snippets/#{snippet.id}/"\
+ "notes/#{snippet_note.id}", user)
+
+ expect(response).to have_http_status(200)
+ # Check if note is really deleted
+ delete v3_api("/projects/#{project.id}/snippets/#{snippet.id}/"\
+ "notes/#{snippet_note.id}", user)
+ expect(response).to have_http_status(404)
+ end
+
+ it 'returns a 404 error when note id not found' do
+ delete v3_api("/projects/#{project.id}/snippets/#{snippet.id}/"\
+ "notes/12345", user)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'when noteable is a Merge Request' do
+ it 'deletes a note' do
+ delete v3_api("/projects/#{project.id}/merge_requests/"\
+ "#{merge_request.id}/notes/#{merge_request_note.id}", user)
+
+ expect(response).to have_http_status(200)
+ # Check if note is really deleted
+ delete v3_api("/projects/#{project.id}/merge_requests/"\
+ "#{merge_request.id}/notes/#{merge_request_note.id}", user)
+ expect(response).to have_http_status(404)
+ end
+
+ it 'returns a 404 error when note id not found' do
+ delete v3_api("/projects/#{project.id}/merge_requests/"\
+ "#{merge_request.id}/notes/12345", user)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/v3/project_snippets_spec.rb b/spec/requests/api/v3/project_snippets_spec.rb
index 3700477f0db..957a3bf97ef 100644
--- a/spec/requests/api/v3/project_snippets_spec.rb
+++ b/spec/requests/api/v3/project_snippets_spec.rb
@@ -85,43 +85,33 @@ describe API::ProjectSnippets, api: true 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
+ 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 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
+ 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)
+ expect(json_response['message']).to eq({ "error" => "Spam detected" })
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
+ it 'creates a spam log' do
+ expect { create_snippet(project, visibility_level: Snippet::PUBLIC) }.
+ to change { SpamLog.count }.by(1)
end
end
end
end
describe 'PUT /projects/:project_id/snippets/:id/' do
- let(:snippet) { create(:project_snippet, author: admin) }
+ let(:visibility_level) { Snippet::PUBLIC }
+ let(:snippet) { create(:project_snippet, author: admin, visibility_level: visibility_level) }
it 'updates snippet' do
new_content = 'New content'
@@ -145,6 +135,56 @@ describe API::ProjectSnippets, api: true do
expect(response).to have_http_status(400)
end
+
+ context 'when the snippet is spam' do
+ def update_snippet(snippet_params = {})
+ put v3_api("/projects/#{snippet.project.id}/snippets/#{snippet.id}", admin), 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
+ let(:visibility_level) { Snippet::PRIVATE }
+
+ it 'creates the snippet' do
+ expect { update_snippet(title: 'Foo') }.
+ to change { snippet.reload.title }.to('Foo')
+ end
+ end
+
+ context 'when the snippet is public' do
+ let(:visibility_level) { Snippet::PUBLIC }
+
+ it 'rejects the snippet' do
+ expect { update_snippet(title: 'Foo') }.
+ not_to change { snippet.reload.title }
+ end
+
+ it 'creates a spam log' do
+ expect { update_snippet(title: 'Foo') }.
+ to change { SpamLog.count }.by(1)
+ end
+ end
+
+ context 'when the private snippet is made public' do
+ let(:visibility_level) { Snippet::PRIVATE }
+
+ it 'rejects the snippet' do
+ expect { update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC) }.
+ not_to change { snippet.reload.title }
+
+ expect(response).to have_http_status(400)
+ expect(json_response['message']).to eq({ "error" => "Spam detected" })
+ end
+
+ it 'creates a spam log' do
+ expect { update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC) }.
+ to change { SpamLog.count }.by(1)
+ end
+ end
+ end
end
describe 'DELETE /projects/:project_id/snippets/:id/' do
diff --git a/spec/requests/api/v3/projects_spec.rb b/spec/requests/api/v3/projects_spec.rb
index 36d99d80e79..34940b2f1c7 100644
--- a/spec/requests/api/v3/projects_spec.rb
+++ b/spec/requests/api/v3/projects_spec.rb
@@ -84,7 +84,7 @@ describe API::V3::Projects, api: true do
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"]
+ expected_keys = %w(id http_url_to_repo web_url name name_with_namespace path path_with_namespace)
get v3_api('/projects?simple=true', user)
@@ -309,10 +309,37 @@ describe API::V3::Projects, api: true do
end
end
- it 'creates new project without path and return 201' do
- expect { post v3_api('/projects', user), name: 'foo' }.
+ it 'creates new project without path but with name and returns 201' do
+ expect { post v3_api('/projects', user), name: 'Foo Project' }.
to change { Project.count }.by(1)
expect(response).to have_http_status(201)
+
+ project = Project.first
+
+ expect(project.name).to eq('Foo Project')
+ expect(project.path).to eq('foo-project')
+ end
+
+ it 'creates new project without name but with path and returns 201' do
+ expect { post v3_api('/projects', user), path: 'foo_project' }.
+ to change { Project.count }.by(1)
+ expect(response).to have_http_status(201)
+
+ project = Project.first
+
+ expect(project.name).to eq('foo_project')
+ expect(project.path).to eq('foo_project')
+ end
+
+ it 'creates new project name and path and returns 201' do
+ expect { post v3_api('/projects', user), path: 'foo-Project', name: 'Foo Project' }.
+ to change { Project.count }.by(1)
+ expect(response).to have_http_status(201)
+
+ project = Project.first
+
+ expect(project.name).to eq('Foo Project')
+ expect(project.path).to eq('foo-Project')
end
it 'creates last project before reaching project limit' do
@@ -321,7 +348,7 @@ describe API::V3::Projects, api: true do
expect(response).to have_http_status(201)
end
- it 'does not create new project without name and return 400' do
+ it 'does not create new project without name or path and return 400' do
expect { post v3_api('/projects', user) }.not_to change { Project.count }
expect(response).to have_http_status(400)
end
diff --git a/spec/requests/api/v3/repositories_spec.rb b/spec/requests/api/v3/repositories_spec.rb
new file mode 100644
index 00000000000..c696721c1c9
--- /dev/null
+++ b/spec/requests/api/v3/repositories_spec.rb
@@ -0,0 +1,144 @@
+require 'spec_helper'
+require 'mime/types'
+
+describe API::V3::Repositories, api: true do
+ include ApiHelpers
+
+ let(:user) { create(:user) }
+ let(:guest) { create(:user).tap { |u| create(:project_member, :guest, user: u, project: project) } }
+ let!(:project) { create(:project, :repository, creator: user) }
+ let!(:master) { create(:project_member, :master, user: user, project: project) }
+
+ describe "GET /projects/:id/repository/tree" do
+ let(:route) { "/projects/#{project.id}/repository/tree" }
+
+ shared_examples_for 'repository tree' do
+ it 'returns the repository tree' do
+ get v3_api(route, current_user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+
+ first_commit = json_response.first
+ expect(first_commit['name']).to eq('bar')
+ expect(first_commit['type']).to eq('tree')
+ expect(first_commit['mode']).to eq('040000')
+ end
+
+ context 'when ref does not exist' do
+ it_behaves_like '404 response' do
+ let(:request) { get v3_api("#{route}?ref_name=foo", current_user) }
+ let(:message) { '404 Tree Not Found' }
+ end
+ end
+
+ context 'when repository is disabled' do
+ include_context 'disabled repository'
+
+ it_behaves_like '403 response' do
+ let(:request) { get v3_api(route, current_user) }
+ end
+ end
+
+ context 'with recursive=1' do
+ it 'returns recursive project paths tree' do
+ get v3_api("#{route}?recursive=1", current_user)
+
+ expect(response.status).to eq(200)
+ expect(json_response).to be_an Array
+ expect(json_response[4]['name']).to eq('html')
+ expect(json_response[4]['path']).to eq('files/html')
+ expect(json_response[4]['type']).to eq('tree')
+ expect(json_response[4]['mode']).to eq('040000')
+ end
+
+ context 'when repository is disabled' do
+ include_context 'disabled repository'
+
+ it_behaves_like '403 response' do
+ let(:request) { get v3_api(route, current_user) }
+ end
+ end
+
+ context 'when ref does not exist' do
+ it_behaves_like '404 response' do
+ let(:request) { get v3_api("#{route}?recursive=1&ref_name=foo", current_user) }
+ let(:message) { '404 Tree Not Found' }
+ end
+ end
+ end
+ end
+
+ context 'when unauthenticated', 'and project is public' do
+ it_behaves_like 'repository tree' do
+ let(:project) { create(:project, :public, :repository) }
+ let(:current_user) { nil }
+ end
+ end
+
+ context 'when unauthenticated', 'and project is private' do
+ it_behaves_like '404 response' do
+ let(:request) { get v3_api(route) }
+ let(:message) { '404 Project Not Found' }
+ end
+ end
+
+ context 'when authenticated', 'as a developer' do
+ it_behaves_like 'repository tree' do
+ let(:current_user) { user }
+ end
+ end
+
+ context 'when authenticated', 'as a guest' do
+ it_behaves_like '403 response' do
+ let(:request) { get v3_api(route, guest) }
+ end
+ end
+ end
+
+ describe 'GET /projects/:id/repository/contributors' do
+ let(:route) { "/projects/#{project.id}/repository/contributors" }
+
+ shared_examples_for 'repository contributors' do
+ it 'returns valid data' do
+ get v3_api(route, current_user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+
+ first_contributor = json_response.first
+ expect(first_contributor['email']).to eq('tiagonbotelho@hotmail.com')
+ expect(first_contributor['name']).to eq('tiagonbotelho')
+ expect(first_contributor['commits']).to eq(1)
+ expect(first_contributor['additions']).to eq(0)
+ expect(first_contributor['deletions']).to eq(0)
+ end
+ end
+
+ context 'when unauthenticated', 'and project is public' do
+ it_behaves_like 'repository contributors' do
+ let(:project) { create(:project, :public, :repository) }
+ let(:current_user) { nil }
+ end
+ end
+
+ context 'when unauthenticated', 'and project is private' do
+ it_behaves_like '404 response' do
+ let(:request) { get v3_api(route) }
+ let(:message) { '404 Project Not Found' }
+ end
+ end
+
+ context 'when authenticated', 'as a developer' do
+ it_behaves_like 'repository contributors' do
+ let(:current_user) { user }
+ end
+ end
+
+ context 'when authenticated', 'as a guest' do
+ it_behaves_like '403 response' do
+ let(:request) { get v3_api(route, guest) }
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/v3/runners_spec.rb b/spec/requests/api/v3/runners_spec.rb
new file mode 100644
index 00000000000..ca335ce9cf0
--- /dev/null
+++ b/spec/requests/api/v3/runners_spec.rb
@@ -0,0 +1,154 @@
+require 'spec_helper'
+
+describe API::V3::Runners, api: true do
+ include ApiHelpers
+
+ let(:admin) { create(:user, :admin) }
+ let(:user) { create(:user) }
+ let(:user2) { create(:user) }
+
+ 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) }
+
+ let!(:specific_runner) do
+ create(:ci_runner).tap do |runner|
+ create(:ci_runner_project, runner: runner, project: project)
+ end
+ end
+
+ let!(:two_projects_runner) do
+ create(:ci_runner).tap do |runner|
+ create(:ci_runner_project, runner: runner, project: project)
+ create(:ci_runner_project, runner: runner, project: project2)
+ end
+ end
+
+ before do
+ # Set project access for users
+ create(:project_member, :master, user: user, project: project)
+ create(:project_member, :reporter, user: user2, project: project)
+ end
+
+ describe 'DELETE /runners/:id' do
+ context 'admin user' do
+ context 'when runner is shared' do
+ it 'deletes runner' do
+ expect do
+ delete v3_api("/runners/#{shared_runner.id}", admin)
+
+ expect(response).to have_http_status(200)
+ end.to change{ Ci::Runner.shared.count }.by(-1)
+ end
+ end
+
+ context 'when runner is not shared' do
+ it 'deletes unused runner' do
+ expect do
+ delete v3_api("/runners/#{unused_specific_runner.id}", admin)
+
+ expect(response).to have_http_status(200)
+ end.to change{ Ci::Runner.specific.count }.by(-1)
+ end
+
+ it 'deletes used runner' do
+ expect do
+ delete v3_api("/runners/#{specific_runner.id}", admin)
+
+ expect(response).to have_http_status(200)
+ end.to change{ Ci::Runner.specific.count }.by(-1)
+ end
+ end
+
+ it 'returns 404 if runner does not exists' do
+ delete v3_api('/runners/9999', admin)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'authorized user' do
+ context 'when runner is shared' do
+ it 'does not delete runner' do
+ delete v3_api("/runners/#{shared_runner.id}", user)
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ context 'when runner is not shared' do
+ it 'does not delete runner without access to it' do
+ delete v3_api("/runners/#{specific_runner.id}", user2)
+ expect(response).to have_http_status(403)
+ end
+
+ it 'does not delete runner with more than one associated project' do
+ delete v3_api("/runners/#{two_projects_runner.id}", user)
+ expect(response).to have_http_status(403)
+ end
+
+ it 'deletes runner for one owned project' do
+ expect do
+ delete v3_api("/runners/#{specific_runner.id}", user)
+
+ expect(response).to have_http_status(200)
+ end.to change{ Ci::Runner.specific.count }.by(-1)
+ end
+ end
+ end
+
+ context 'unauthorized user' do
+ it 'does not delete runner' do
+ delete v3_api("/runners/#{specific_runner.id}")
+
+ expect(response).to have_http_status(401)
+ end
+ end
+ end
+
+ describe 'DELETE /projects/:id/runners/:runner_id' do
+ context 'authorized user' do
+ context 'when runner have more than one associated projects' do
+ it "disables project's runner" do
+ expect do
+ delete v3_api("/projects/#{project.id}/runners/#{two_projects_runner.id}", user)
+
+ expect(response).to have_http_status(200)
+ end.to change{ project.runners.count }.by(-1)
+ end
+ end
+
+ context 'when runner have one associated projects' do
+ it "does not disable project's runner" do
+ expect do
+ delete v3_api("/projects/#{project.id}/runners/#{specific_runner.id}", user)
+ end.to change{ project.runners.count }.by(0)
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ it 'returns 404 is runner is not found' do
+ delete v3_api("/projects/#{project.id}/runners/9999", user)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'authorized user without permissions' do
+ it "does not disable project's runner" do
+ delete v3_api("/projects/#{project.id}/runners/#{specific_runner.id}", user2)
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ context 'unauthorized user' do
+ it "does not disable project's runner" do
+ delete v3_api("/projects/#{project.id}/runners/#{specific_runner.id}")
+
+ expect(response).to have_http_status(401)
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/v3/services_spec.rb b/spec/requests/api/v3/services_spec.rb
new file mode 100644
index 00000000000..7e8c8753d02
--- /dev/null
+++ b/spec/requests/api/v3/services_spec.rb
@@ -0,0 +1,22 @@
+require "spec_helper"
+
+describe API::V3::Services, api: true do
+ include ApiHelpers
+
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project, creator_id: user.id, namespace: user.namespace) }
+
+ Service.available_services_names.each do |service|
+ describe "DELETE /projects/:id/services/#{service.dasherize}" do
+ include_context service
+
+ it "deletes #{service}" do
+ delete v3_api("/projects/#{project.id}/services/#{dashed_service}", user)
+
+ expect(response).to have_http_status(200)
+ project.send(service_method).reload
+ expect(project.send(service_method).activated?).to be_falsey
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/v3/system_hooks_spec.rb b/spec/requests/api/v3/system_hooks_spec.rb
new file mode 100644
index 00000000000..91038977c82
--- /dev/null
+++ b/spec/requests/api/v3/system_hooks_spec.rb
@@ -0,0 +1,57 @@
+require 'spec_helper'
+
+describe API::V3::SystemHooks, api: true do
+ include ApiHelpers
+
+ let(:user) { create(:user) }
+ let(:admin) { create(:admin) }
+ let!(:hook) { create(:system_hook, url: "http://example.com") }
+
+ before { stub_request(:post, hook.url) }
+
+ describe "GET /hooks" do
+ context "when no user" do
+ it "returns authentication error" do
+ get v3_api("/hooks")
+
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ context "when not an admin" do
+ it "returns forbidden error" do
+ get v3_api("/hooks", user)
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ context "when authenticated as admin" do
+ it "returns an array of hooks" do
+ get v3_api("/hooks", admin)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first['url']).to eq(hook.url)
+ expect(json_response.first['push_events']).to be true
+ expect(json_response.first['tag_push_events']).to be false
+ end
+ end
+ end
+
+ describe "DELETE /hooks/:id" do
+ it "deletes a hook" do
+ expect do
+ delete v3_api("/hooks/#{hook.id}", admin)
+
+ expect(response).to have_http_status(200)
+ end.to change { SystemHook.count }.by(-1)
+ end
+
+ it 'returns 404 if the system hook does not exist' do
+ delete v3_api('/hooks/12345', admin)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+end
diff --git a/spec/requests/api/v3/tags_spec.rb b/spec/requests/api/v3/tags_spec.rb
new file mode 100644
index 00000000000..6870cfd2668
--- /dev/null
+++ b/spec/requests/api/v3/tags_spec.rb
@@ -0,0 +1,89 @@
+require 'spec_helper'
+require 'mime/types'
+
+describe API::V3::Tags, api: true do
+ include ApiHelpers
+ include RepoHelpers
+
+ let(:user) { create(:user) }
+ let(:user2) { create(:user) }
+ let!(:project) { create(:project, :repository, creator: user) }
+ let!(:master) { create(:project_member, :master, user: user, project: project) }
+
+ describe "GET /projects/:id/repository/tags" do
+ let(:tag_name) { project.repository.tag_names.sort.reverse.first }
+ let(:description) { 'Awesome release!' }
+
+ shared_examples_for 'repository tags' do
+ it 'returns the repository tags' do
+ get v3_api("/projects/#{project.id}/repository/tags", current_user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first['name']).to eq(tag_name)
+ end
+ end
+
+ context 'when unauthenticated' do
+ it_behaves_like 'repository tags' do
+ let(:project) { create(:project, :public, :repository) }
+ let(:current_user) { nil }
+ end
+ end
+
+ context 'when authenticated' do
+ it_behaves_like 'repository tags' do
+ let(:current_user) { user }
+ end
+ end
+
+ context 'without releases' do
+ it "returns an array of project tags" do
+ get v3_api("/projects/#{project.id}/repository/tags", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first['name']).to eq(tag_name)
+ end
+ end
+
+ context 'with releases' do
+ before do
+ release = project.releases.find_or_initialize_by(tag: tag_name)
+ release.update_attributes(description: description)
+ end
+
+ it "returns an array of project tags with release info" do
+ get v3_api("/projects/#{project.id}/repository/tags", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first['name']).to eq(tag_name)
+ expect(json_response.first['message']).to eq('Version 1.1.0')
+ expect(json_response.first['release']['description']).to eq(description)
+ end
+ end
+ end
+
+ describe 'DELETE /projects/:id/repository/tags/:tag_name' do
+ let(:tag_name) { project.repository.tag_names.sort.reverse.first }
+
+ before do
+ allow_any_instance_of(Repository).to receive(:rm_tag).and_return(true)
+ end
+
+ context 'delete tag' do
+ it 'deletes an existing tag' do
+ delete v3_api("/projects/#{project.id}/repository/tags/#{tag_name}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['tag_name']).to eq(tag_name)
+ end
+
+ it 'raises 404 if the tag does not exist' do
+ delete v3_api("/projects/#{project.id}/repository/tags/foobar", user)
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/v3/templates_spec.rb b/spec/requests/api/v3/templates_spec.rb
index 4fd4e70bedd..f1e554b98cc 100644
--- a/spec/requests/api/v3/templates_spec.rb
+++ b/spec/requests/api/v3/templates_spec.rb
@@ -56,11 +56,11 @@ describe API::V3::Templates, api: true do
expect(json_response['popular']).to be true
expect(json_response['html_url']).to eq('http://choosealicense.com/licenses/mit/')
expect(json_response['source_url']).to eq('https://opensource.org/licenses/MIT')
- expect(json_response['description']).to include('A permissive license that is short and to the point.')
+ expect(json_response['description']).to include('A short and simple permissive license with conditions')
expect(json_response['conditions']).to eq(%w[include-copyright])
expect(json_response['permissions']).to eq(%w[commercial-use modifications distribution private-use])
expect(json_response['limitations']).to eq(%w[no-liability])
- expect(json_response['content']).to include('The MIT License (MIT)')
+ expect(json_response['content']).to include('MIT License')
end
end
@@ -70,7 +70,7 @@ describe API::V3::Templates, api: true do
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
- expect(json_response.size).to eq(15)
+ expect(json_response.size).to eq(12)
expect(json_response.map { |l| l['key'] }).to include('agpl-3.0')
end
@@ -98,7 +98,7 @@ describe API::V3::Templates, api: true do
let(:license_type) { 'mit' }
it 'returns the license text' do
- expect(json_response['content']).to include('The MIT License (MIT)')
+ expect(json_response['content']).to include('MIT License')
end
it 'replaces placeholder values' do
diff --git a/spec/requests/api/v3/todos_spec.rb b/spec/requests/api/v3/todos_spec.rb
new file mode 100644
index 00000000000..80fa697e949
--- /dev/null
+++ b/spec/requests/api/v3/todos_spec.rb
@@ -0,0 +1,73 @@
+require 'spec_helper'
+
+describe API::V3::Todos, api: true do
+ include ApiHelpers
+
+ 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') }
+ let!(:pending_1) { create(:todo, :mentioned, project: project_1, author: author_1, user: john_doe) }
+ let!(:pending_2) { create(:todo, project: project_2, author: author_2, user: john_doe) }
+ let!(:pending_3) { create(:todo, project: project_1, author: author_2, user: john_doe) }
+ let!(:done) { create(:todo, :done, project: project_1, author: author_1, user: john_doe) }
+
+ before do
+ project_1.team << [john_doe, :developer]
+ project_2.team << [john_doe, :developer]
+ end
+
+ describe 'DELETE /todos/:id' do
+ context 'when unauthenticated' do
+ it 'returns authentication error' do
+ delete v3_api("/todos/#{pending_1.id}")
+
+ expect(response.status).to eq(401)
+ end
+ end
+
+ context 'when authenticated' do
+ it 'marks a todo as done' do
+ delete v3_api("/todos/#{pending_1.id}", john_doe)
+
+ expect(response.status).to eq(200)
+ expect(pending_1.reload).to be_done
+ end
+
+ it 'updates todos cache' do
+ expect_any_instance_of(User).to receive(:update_todos_count_cache).and_call_original
+
+ delete v3_api("/todos/#{pending_1.id}", john_doe)
+ end
+ end
+ end
+
+ describe 'DELETE /todos' do
+ context 'when unauthenticated' do
+ it 'returns authentication error' do
+ delete v3_api('/todos')
+
+ expect(response.status).to eq(401)
+ end
+ end
+
+ context 'when authenticated' do
+ it 'marks all todos as done' do
+ delete v3_api('/todos', john_doe)
+
+ expect(response.status).to eq(200)
+ expect(response.body).to eq('3')
+ expect(pending_1.reload).to be_done
+ expect(pending_2.reload).to be_done
+ expect(pending_3.reload).to be_done
+ end
+
+ it 'updates todos cache' do
+ expect_any_instance_of(User).to receive(:update_todos_count_cache).and_call_original
+
+ delete v3_api("/todos", john_doe)
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/v3/triggers_spec.rb b/spec/requests/api/v3/triggers_spec.rb
new file mode 100644
index 00000000000..721ce4a361b
--- /dev/null
+++ b/spec/requests/api/v3/triggers_spec.rb
@@ -0,0 +1,47 @@
+require 'spec_helper'
+
+describe API::V3::Triggers do
+ include ApiHelpers
+
+ let(:user) { create(:user) }
+ let(:user2) { create(:user) }
+ let!(:trigger_token) { 'secure_token' }
+ 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) }
+
+ describe 'DELETE /projects/:id/triggers/:token' do
+ context 'authenticated user with valid permissions' do
+ it 'deletes trigger' do
+ expect do
+ delete v3_api("/projects/#{project.id}/triggers/#{trigger.token}", user)
+
+ expect(response).to have_http_status(200)
+ end.to change{project.triggers.count}.by(-1)
+ end
+
+ it 'responds with 404 Not Found if requesting non-existing trigger' do
+ delete v3_api("/projects/#{project.id}/triggers/abcdef012345", user)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'authenticated user with invalid permissions' do
+ it 'does not delete trigger' do
+ delete v3_api("/projects/#{project.id}/triggers/#{trigger.token}", user2)
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ context 'unauthenticated user' do
+ it 'does not delete trigger' do
+ delete v3_api("/projects/#{project.id}/triggers/#{trigger.token}")
+
+ expect(response).to have_http_status(401)
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/v3/users_spec.rb b/spec/requests/api/v3/users_spec.rb
new file mode 100644
index 00000000000..17bbb0b53c1
--- /dev/null
+++ b/spec/requests/api/v3/users_spec.rb
@@ -0,0 +1,266 @@
+require 'spec_helper'
+
+describe API::V3::Users, api: true do
+ include ApiHelpers
+
+ let(:user) { create(:user) }
+ let(:admin) { create(:admin) }
+ let(:key) { create(:key, user: user) }
+ let(:email) { create(:email, user: user) }
+ let(:ldap_blocked_user) { create(:omniauth_user, provider: 'ldapmain', state: 'ldap_blocked') }
+
+ describe 'GET /user/:id/keys' do
+ before { admin }
+
+ context 'when unauthenticated' do
+ it 'returns authentication error' do
+ get v3_api("/users/#{user.id}/keys")
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ context 'when authenticated' do
+ it 'returns 404 for non-existing user' do
+ get v3_api('/users/999999/keys', admin)
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 User Not Found')
+ end
+
+ it 'returns array of ssh keys' do
+ user.keys << key
+ user.save
+
+ get v3_api("/users/#{user.id}/keys", admin)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first['title']).to eq(key.title)
+ end
+ end
+ end
+
+ describe 'GET /user/:id/emails' do
+ before { admin }
+
+ context 'when unauthenticated' do
+ it 'returns authentication error' do
+ get v3_api("/users/#{user.id}/emails")
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ context 'when authenticated' do
+ it 'returns 404 for non-existing user' do
+ get v3_api('/users/999999/emails', admin)
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 User Not Found')
+ end
+
+ it 'returns array of emails' do
+ user.emails << email
+ user.save
+
+ get v3_api("/users/#{user.id}/emails", admin)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first['email']).to eq(email.email)
+ end
+
+ it "returns a 404 for invalid ID" do
+ put v3_api("/users/ASDF/emails", admin)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ describe "GET /user/keys" do
+ context "when unauthenticated" do
+ it "returns authentication error" do
+ get v3_api("/user/keys")
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ context "when authenticated" do
+ it "returns array of ssh keys" do
+ user.keys << key
+ user.save
+
+ get v3_api("/user/keys", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first["title"]).to eq(key.title)
+ end
+ end
+ end
+
+ describe "GET /user/emails" do
+ context "when unauthenticated" do
+ it "returns authentication error" do
+ get v3_api("/user/emails")
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ context "when authenticated" do
+ it "returns array of emails" do
+ user.emails << email
+ user.save
+
+ get v3_api("/user/emails", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first["email"]).to eq(email.email)
+ end
+ end
+ end
+
+ describe 'PUT /users/:id/block' do
+ before { admin }
+ it 'blocks existing user' do
+ put v3_api("/users/#{user.id}/block", admin)
+ expect(response).to have_http_status(200)
+ expect(user.reload.state).to eq('blocked')
+ end
+
+ it 'does not re-block ldap blocked users' do
+ put v3_api("/users/#{ldap_blocked_user.id}/block", admin)
+ expect(response).to have_http_status(403)
+ expect(ldap_blocked_user.reload.state).to eq('ldap_blocked')
+ end
+
+ it 'does not be available for non admin users' do
+ put v3_api("/users/#{user.id}/block", user)
+ expect(response).to have_http_status(403)
+ expect(user.reload.state).to eq('active')
+ end
+
+ it 'returns a 404 error if user id not found' do
+ put v3_api('/users/9999/block', admin)
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 User Not Found')
+ end
+ end
+
+ describe 'PUT /users/:id/unblock' do
+ let(:blocked_user) { create(:user, state: 'blocked') }
+ before { admin }
+
+ it 'unblocks existing user' do
+ put v3_api("/users/#{user.id}/unblock", admin)
+ expect(response).to have_http_status(200)
+ expect(user.reload.state).to eq('active')
+ end
+
+ it 'unblocks a blocked user' do
+ put v3_api("/users/#{blocked_user.id}/unblock", admin)
+ expect(response).to have_http_status(200)
+ expect(blocked_user.reload.state).to eq('active')
+ end
+
+ it 'does not unblock ldap blocked users' do
+ put v3_api("/users/#{ldap_blocked_user.id}/unblock", admin)
+ expect(response).to have_http_status(403)
+ expect(ldap_blocked_user.reload.state).to eq('ldap_blocked')
+ end
+
+ it 'does not be available for non admin users' do
+ put v3_api("/users/#{user.id}/unblock", user)
+ expect(response).to have_http_status(403)
+ expect(user.reload.state).to eq('active')
+ end
+
+ it 'returns a 404 error if user id not found' do
+ put v3_api('/users/9999/block', admin)
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 User Not Found')
+ end
+
+ it "returns a 404 for invalid ID" do
+ put v3_api("/users/ASDF/block", admin)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ describe 'GET /users/:id/events' do
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project) }
+ let(:note) { create(:note_on_issue, note: 'What an awesome day!', project: project) }
+
+ before do
+ project.add_user(user, :developer)
+ EventCreateService.new.leave_note(note, user)
+ end
+
+ context "as a user than cannot see the event's project" do
+ it 'returns no events' do
+ other_user = create(:user)
+
+ get api("/users/#{user.id}/events", other_user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_empty
+ end
+ end
+
+ context "as a user than can see the event's project" do
+ context 'joined event' do
+ it 'returns the "joined" event' do
+ get v3_api("/users/#{user.id}/events", user)
+
+ expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+
+ comment_event = json_response.find { |e| e['action_name'] == 'commented on' }
+
+ expect(comment_event['project_id'].to_i).to eq(project.id)
+ expect(comment_event['author_username']).to eq(user.username)
+ expect(comment_event['note']['id']).to eq(note.id)
+ expect(comment_event['note']['body']).to eq('What an awesome day!')
+
+ joined_event = json_response.find { |e| e['action_name'] == 'joined' }
+
+ expect(joined_event['project_id'].to_i).to eq(project.id)
+ expect(joined_event['author_username']).to eq(user.username)
+ expect(joined_event['author']['name']).to eq(user.name)
+ end
+ end
+
+ context 'when there are multiple events from different projects' do
+ let(:second_note) { create(:note_on_issue, project: create(:empty_project)) }
+ let(:third_note) { create(:note_on_issue, project: project) }
+
+ before do
+ second_note.project.add_user(user, :developer)
+
+ [second_note, third_note].each do |note|
+ EventCreateService.new.leave_note(note, user)
+ end
+ end
+
+ it 'returns events in the correct order (from newest to oldest)' do
+ get v3_api("/users/#{user.id}/events", user)
+
+ comment_events = json_response.select { |e| e['action_name'] == 'commented on' }
+
+ expect(comment_events[0]['target_id']).to eq(third_note.id)
+ expect(comment_events[1]['target_id']).to eq(second_note.id)
+ expect(comment_events[2]['target_id']).to eq(note.id)
+ end
+ end
+ end
+
+ it 'returns a 404 error if not found' do
+ get v3_api('/users/42/events', user)
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 User Not Found')
+ end
+ end
+end
diff --git a/spec/requests/api/variables_spec.rb b/spec/requests/api/variables_spec.rb
index 769f04c5057..0c1413119e0 100644
--- a/spec/requests/api/variables_spec.rb
+++ b/spec/requests/api/variables_spec.rb
@@ -152,8 +152,9 @@ describe API::Variables, api: true do
it 'deletes variable' do
expect do
delete api("/projects/#{project.id}/variables/#{variable.key}", user)
+
+ expect(response).to have_http_status(204)
end.to change{project.variables.count}.by(-1)
- expect(response).to have_http_status(200)
end
it 'responds with 404 Not Found if requesting non-existing variable' do
diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb
index d85afdeab42..9948d1a9ea0 100644
--- a/spec/requests/ci/api/builds_spec.rb
+++ b/spec/requests/ci/api/builds_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe Ci::API::Builds do
include ApiHelpers
- let(:runner) { FactoryGirl.create(:ci_runner, tag_list: ["mysql", "ruby"]) }
+ let(:runner) { FactoryGirl.create(:ci_runner, tag_list: %w(mysql ruby)) }
let(:project) { FactoryGirl.create(:empty_project, shared_runners_enabled: false) }
let(:last_update) { nil }
@@ -630,6 +630,7 @@ describe Ci::API::Builds do
context 'with an expire date' do
let!(:artifacts) { file_upload }
+ let(:default_artifacts_expire_in) {}
let(:post_data) do
{ 'file.path' => artifacts.path,
@@ -638,6 +639,9 @@ describe Ci::API::Builds do
end
before do
+ stub_application_setting(
+ default_artifacts_expire_in: default_artifacts_expire_in)
+
post(post_url, post_data, headers_with_token)
end
@@ -648,7 +652,8 @@ describe Ci::API::Builds do
build.reload
expect(response).to have_http_status(201)
expect(json_response['artifacts_expire_at']).not_to be_empty
- expect(build.artifacts_expire_at).to be_within(5.minutes).of(Time.now + 7.days)
+ expect(build.artifacts_expire_at).
+ to be_within(5.minutes).of(7.days.from_now)
end
end
@@ -661,6 +666,32 @@ describe Ci::API::Builds do
expect(json_response['artifacts_expire_at']).to be_nil
expect(build.artifacts_expire_at).to be_nil
end
+
+ context 'with application default' do
+ context 'default to 5 days' do
+ let(:default_artifacts_expire_in) { '5 days' }
+
+ it 'sets to application default' do
+ build.reload
+ expect(response).to have_http_status(201)
+ expect(json_response['artifacts_expire_at'])
+ .not_to be_empty
+ expect(build.artifacts_expire_at)
+ .to be_within(5.minutes).of(5.days.from_now)
+ end
+ end
+
+ context 'default to 0' do
+ let(:default_artifacts_expire_in) { '0' }
+
+ it 'does not set expire_in' do
+ build.reload
+ expect(response).to have_http_status(201)
+ expect(json_response['artifacts_expire_at']).to be_nil
+ expect(build.artifacts_expire_at).to be_nil
+ end
+ end
+ end
end
end
diff --git a/spec/requests/ci/api/runners_spec.rb b/spec/requests/ci/api/runners_spec.rb
index bd55934d0c8..8719313783e 100644
--- a/spec/requests/ci/api/runners_spec.rb
+++ b/spec/requests/ci/api/runners_spec.rb
@@ -41,7 +41,7 @@ describe Ci::API::Runners do
it 'creates runner' do
expect(response).to have_http_status 201
- expect(Ci::Runner.first.tag_list.sort).to eq(["tag1", "tag2"])
+ expect(Ci::Runner.first.tag_list.sort).to eq(%w(tag1 tag2))
end
end
diff --git a/spec/requests/ci/api/triggers_spec.rb b/spec/requests/ci/api/triggers_spec.rb
index a30be767119..5321f8b134f 100644
--- a/spec/requests/ci/api/triggers_spec.rb
+++ b/spec/requests/ci/api/triggers_spec.rb
@@ -60,7 +60,8 @@ describe Ci::API::Triggers do
it 'validates variables to be a hash' do
post ci_api("/projects/#{project.ci_id}/refs/master/trigger"), options.merge(variables: 'value')
expect(response).to have_http_status(400)
- expect(json_response['message']).to eq('variables needs to be a hash')
+
+ expect(json_response['error']).to eq('variables is invalid')
end
it 'validates variables needs to be a map of key-valued strings' do
diff --git a/spec/requests/lfs_http_spec.rb b/spec/requests/lfs_http_spec.rb
index c0e7bab8199..5d495bc9e7d 100644
--- a/spec/requests/lfs_http_spec.rb
+++ b/spec/requests/lfs_http_spec.rb
@@ -25,11 +25,9 @@ describe 'Git LFS API and storage' do
{
'objects' => [
{ 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
- 'size' => 1575078
- },
+ 'size' => 1575078 },
{ 'oid' => sample_oid,
- 'size' => sample_size
- }
+ 'size' => sample_size }
],
'operation' => 'upload'
}
@@ -53,11 +51,9 @@ describe 'Git LFS API and storage' do
{
'objects' => [
{ 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
- 'size' => 1575078
- },
+ 'size' => 1575078 },
{ 'oid' => sample_oid,
- 'size' => sample_size
- }
+ 'size' => sample_size }
],
'operation' => 'upload'
}
@@ -374,11 +370,12 @@ describe 'Git LFS API and storage' do
describe 'download' do
let(:project) { create(:empty_project) }
let(:body) do
- { 'operation' => 'download',
+ {
+ 'operation' => 'download',
'objects' => [
{ 'oid' => sample_oid,
- 'size' => sample_size
- }]
+ 'size' => sample_size }
+ ]
}
end
@@ -393,16 +390,20 @@ describe 'Git LFS API and storage' do
end
it 'with href to download' do
- expect(json_response).to eq('objects' => [
- { 'oid' => sample_oid,
- 'size' => sample_size,
- 'actions' => {
- 'download' => {
- 'href' => "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}",
- 'header' => { 'Authorization' => authorization }
+ expect(json_response).to eq({
+ 'objects' => [
+ {
+ 'oid' => sample_oid,
+ 'size' => sample_size,
+ 'actions' => {
+ 'download' => {
+ 'href' => "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}",
+ 'header' => { 'Authorization' => authorization }
+ }
}
}
- }])
+ ]
+ })
end
end
@@ -417,24 +418,29 @@ describe 'Git LFS API and storage' do
end
it 'with href to download' do
- expect(json_response).to eq('objects' => [
- { 'oid' => sample_oid,
- 'size' => sample_size,
- 'error' => {
- 'code' => 404,
- 'message' => "Object does not exist on the server or you don't have permissions to access it",
+ expect(json_response).to eq({
+ 'objects' => [
+ {
+ 'oid' => sample_oid,
+ 'size' => sample_size,
+ 'error' => {
+ 'code' => 404,
+ 'message' => "Object does not exist on the server or you don't have permissions to access it",
+ }
}
- }])
+ ]
+ })
end
end
context 'when downloading a lfs object that does not exist' do
let(:body) do
- { 'operation' => 'download',
+ {
+ 'operation' => 'download',
'objects' => [
{ 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
- 'size' => 1575078
- }]
+ 'size' => 1575078 }
+ ]
}
end
@@ -443,27 +449,30 @@ describe 'Git LFS API and storage' do
end
it 'with an 404 for specific object' do
- expect(json_response).to eq('objects' => [
- { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
- 'size' => 1575078,
- 'error' => {
- 'code' => 404,
- 'message' => "Object does not exist on the server or you don't have permissions to access it",
+ expect(json_response).to eq({
+ 'objects' => [
+ {
+ 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
+ 'size' => 1575078,
+ 'error' => {
+ 'code' => 404,
+ 'message' => "Object does not exist on the server or you don't have permissions to access it",
+ }
}
- }])
+ ]
+ })
end
end
context 'when downloading one new and one existing lfs object' do
let(:body) do
- { 'operation' => 'download',
+ {
+ 'operation' => 'download',
'objects' => [
{ 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
- 'size' => 1575078
- },
+ 'size' => 1575078 },
{ 'oid' => sample_oid,
- 'size' => sample_size
- }
+ 'size' => sample_size }
]
}
end
@@ -477,23 +486,28 @@ describe 'Git LFS API and storage' do
end
it 'responds with upload hypermedia link for the new object' do
- expect(json_response).to eq('objects' => [
- { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
- 'size' => 1575078,
- 'error' => {
- 'code' => 404,
- 'message' => "Object does not exist on the server or you don't have permissions to access it",
- }
- },
- { 'oid' => sample_oid,
- 'size' => sample_size,
- 'actions' => {
- 'download' => {
- 'href' => "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}",
- 'header' => { 'Authorization' => authorization }
+ expect(json_response).to eq({
+ 'objects' => [
+ {
+ 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
+ 'size' => 1575078,
+ 'error' => {
+ 'code' => 404,
+ 'message' => "Object does not exist on the server or you don't have permissions to access it",
+ }
+ },
+ {
+ 'oid' => sample_oid,
+ 'size' => sample_size,
+ 'actions' => {
+ 'download' => {
+ 'href' => "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}",
+ 'header' => { 'Authorization' => authorization }
+ }
}
}
- }])
+ ]
+ })
end
end
end
@@ -597,17 +611,21 @@ describe 'Git LFS API and storage' do
end
it 'responds with status 200 and href to download' 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}",
- 'header' => {}
+ 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}",
+ 'header' => {}
+ }
}
}
- }])
+ ]
+ })
end
end
@@ -626,11 +644,12 @@ describe 'Git LFS API and storage' do
describe 'upload' do
let(:project) { create(:project, :public) }
let(:body) do
- { 'operation' => 'upload',
+ {
+ 'operation' => 'upload',
'objects' => [
{ 'oid' => sample_oid,
- 'size' => sample_size
- }]
+ 'size' => sample_size }
+ ]
}
end
@@ -665,11 +684,12 @@ describe 'Git LFS API and storage' do
context 'when pushing a lfs object that does not exist' do
let(:body) do
- { 'operation' => 'upload',
+ {
+ 'operation' => 'upload',
'objects' => [
{ 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
- 'size' => 1575078
- }]
+ 'size' => 1575078 }
+ ]
}
end
@@ -688,14 +708,13 @@ describe 'Git LFS API and storage' do
context 'when pushing one new and one existing lfs object' do
let(:body) do
- { 'operation' => 'upload',
+ {
+ 'operation' => 'upload',
'objects' => [
{ 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
- 'size' => 1575078
- },
+ 'size' => 1575078 },
{ 'oid' => sample_oid,
- 'size' => sample_size
- }
+ 'size' => sample_size }
]
}
end
@@ -789,11 +808,12 @@ describe 'Git LFS API and storage' do
let(:project) { create(:empty_project) }
let(:authorization) { authorize_user }
let(:body) do
- { 'operation' => 'other',
+ {
+ 'operation' => 'other',
'objects' => [
{ 'oid' => sample_oid,
- 'size' => sample_size
- }]
+ 'size' => sample_size }
+ ]
}
end
diff --git a/spec/services/auth/container_registry_authentication_service_spec.rb b/spec/services/auth/container_registry_authentication_service_spec.rb
index bb26513103d..b91234ddb1e 100644
--- a/spec/services/auth/container_registry_authentication_service_spec.rb
+++ b/spec/services/auth/container_registry_authentication_service_spec.rb
@@ -72,7 +72,7 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do
shared_examples 'a pullable and pushable' do
it_behaves_like 'a accessible' do
- let(:actions) { ['pull', 'push'] }
+ let(:actions) { %w(pull push) }
end
end
diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb
index ceaca96e25b..8459a3d8cfb 100644
--- a/spec/services/ci/create_pipeline_service_spec.rb
+++ b/spec/services/ci/create_pipeline_service_spec.rb
@@ -79,66 +79,53 @@ describe Ci::CreatePipelineService, services: true do
context 'when commit contains a [ci skip] directive' do
let(:message) { "some message[ci skip]" }
- let(:messageFlip) { "some message[skip ci]" }
- let(:capMessage) { "some message[CI SKIP]" }
- let(:capMessageFlip) { "some message[SKIP CI]" }
+
+ ci_messages = [
+ "some message[ci skip]",
+ "some message[skip ci]",
+ "some message[CI SKIP]",
+ "some message[SKIP CI]",
+ "some message[ci_skip]",
+ "some message[skip_ci]",
+ "some message[ci-skip]",
+ "some message[skip-ci]"
+ ]
before do
allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { message }
end
- it "skips builds creation if there is [ci skip] tag in commit message" do
- commits = [{ message: message }]
- pipeline = execute(ref: 'refs/heads/master',
- before: '00000000',
- after: project.commit.id,
- commits: commits)
+ ci_messages.each do |ci_message|
+ it "skips builds creation if the commit message is #{ci_message}" do
+ commits = [{ message: ci_message }]
+ pipeline = execute(ref: 'refs/heads/master',
+ before: '00000000',
+ after: project.commit.id,
+ commits: commits)
- expect(pipeline).to be_persisted
- expect(pipeline.builds.any?).to be false
- expect(pipeline.status).to eq("skipped")
- end
-
- it "skips builds creation if there is [skip ci] tag in commit message" do
- commits = [{ message: messageFlip }]
- pipeline = execute(ref: 'refs/heads/master',
- before: '00000000',
- after: project.commit.id,
- commits: commits)
-
- expect(pipeline).to be_persisted
- expect(pipeline.builds.any?).to be false
- expect(pipeline.status).to eq("skipped")
+ expect(pipeline).to be_persisted
+ expect(pipeline.builds.any?).to be false
+ expect(pipeline.status).to eq("skipped")
+ end
end
- it "skips builds creation if there is [CI SKIP] tag in commit message" do
- commits = [{ message: capMessage }]
- pipeline = execute(ref: 'refs/heads/master',
- before: '00000000',
- after: project.commit.id,
- commits: commits)
-
- expect(pipeline).to be_persisted
- expect(pipeline.builds.any?).to be false
- expect(pipeline.status).to eq("skipped")
- end
+ it "does not skips builds creation if there is no [ci skip] or [skip ci] tag in commit message" do
+ allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { "some message" }
- it "skips builds creation if there is [SKIP CI] tag in commit message" do
- commits = [{ message: capMessageFlip }]
+ commits = [{ message: "some message" }]
pipeline = execute(ref: 'refs/heads/master',
before: '00000000',
after: project.commit.id,
commits: commits)
expect(pipeline).to be_persisted
- expect(pipeline.builds.any?).to be false
- expect(pipeline.status).to eq("skipped")
+ expect(pipeline.builds.first.name).to eq("rspec")
end
- it "does not skips builds creation if there is no [ci skip] or [skip ci] tag in commit message" do
- allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { "some message" }
+ it "does not skip builds creation if the commit message is nil" do
+ allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { nil }
- commits = [{ message: "some message" }]
+ commits = [{ message: nil }]
pipeline = execute(ref: 'refs/heads/master',
before: '00000000',
after: project.commit.id,
diff --git a/spec/services/ci/process_pipeline_service_spec.rb b/spec/services/ci/process_pipeline_service_spec.rb
index ebb11166964..b818dfdd50c 100644
--- a/spec/services/ci/process_pipeline_service_spec.rb
+++ b/spec/services/ci/process_pipeline_service_spec.rb
@@ -1,8 +1,16 @@
require 'spec_helper'
-describe Ci::ProcessPipelineService, services: true do
- let(:pipeline) { create(:ci_empty_pipeline, ref: 'master') }
+describe Ci::ProcessPipelineService, :services do
let(:user) { create(:user) }
+ let(:project) { create(:empty_project) }
+
+ let(:pipeline) do
+ create(:ci_empty_pipeline, ref: 'master', project: project)
+ end
+
+ before do
+ project.add_developer(user)
+ end
describe '#execute' do
context 'start queuing next builds' do
@@ -285,7 +293,7 @@ describe Ci::ProcessPipelineService, services: true do
expect(builds.pluck(:name))
.to contain_exactly('build:1', 'build:2', 'test:1', 'test:2')
- Ci::Build.retry(pipeline.builds.find_by(name: 'test:2')).success
+ Ci::Build.retry(pipeline.builds.find_by(name: 'test:2'), user).success
expect(builds.pluck(:name)).to contain_exactly(
'build:1', 'build:2', 'test:1', 'test:2', 'test:2', 'deploy:1', 'deploy:2')
@@ -369,9 +377,7 @@ describe Ci::ProcessPipelineService, services: true do
builds.pending.update_all(status: 'success')
end
- def manual_actions
- pipeline.manual_actions
- end
+ delegate :manual_actions, to: :pipeline
def create_build(name, stage_idx, when_value = nil)
create(:ci_build,
diff --git a/spec/services/ci/register_build_service_spec.rb b/spec/services/ci/register_build_service_spec.rb
index d9f774a1095..cd7dd53025c 100644
--- a/spec/services/ci/register_build_service_spec.rb
+++ b/spec/services/ci/register_build_service_spec.rb
@@ -170,6 +170,51 @@ module Ci
end
end
+ context 'when first build is stalled' do
+ before do
+ pending_build.lock_version = 10
+ end
+
+ subject { described_class.new(specific_runner).execute }
+
+ context 'with multiple builds are in queue' do
+ let!(:other_build) { create :ci_build, pipeline: pipeline }
+
+ before do
+ allow_any_instance_of(Ci::RegisterBuildService).to receive(:builds_for_specific_runner)
+ .and_return([pending_build, other_build])
+ end
+
+ it "receives second build from the queue" do
+ expect(subject).to be_valid
+ expect(subject.build).to eq(other_build)
+ end
+ end
+
+ context 'when single build is in queue' do
+ before do
+ allow_any_instance_of(Ci::RegisterBuildService).to receive(:builds_for_specific_runner)
+ .and_return([pending_build])
+ end
+
+ it "does not receive any valid result" do
+ expect(subject).not_to be_valid
+ end
+ end
+
+ context 'when there is no build in queue' do
+ before do
+ allow_any_instance_of(Ci::RegisterBuildService).to receive(:builds_for_specific_runner)
+ .and_return([])
+ end
+
+ it "does not receive builds but result is valid" do
+ expect(subject).to be_valid
+ expect(subject.build).to be_nil
+ end
+ end
+ end
+
def execute(runner)
described_class.new(runner).execute.build
end
diff --git a/spec/services/ci/retry_build_service_spec.rb b/spec/services/ci/retry_build_service_spec.rb
new file mode 100644
index 00000000000..d03f7505eac
--- /dev/null
+++ b/spec/services/ci/retry_build_service_spec.rb
@@ -0,0 +1,117 @@
+require 'spec_helper'
+
+describe Ci::RetryBuildService, :services do
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project) }
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+ let(:build) { create(:ci_build, pipeline: pipeline) }
+
+ let(:service) do
+ described_class.new(project, user)
+ end
+
+ shared_examples 'build duplication' do
+ let(:build) do
+ create(:ci_build, :failed, :artifacts_expired, :erased, :trace,
+ :queued, :coverage, pipeline: pipeline)
+ end
+
+ describe 'clone attributes' do
+ described_class::CLONE_ATTRIBUTES.each do |attribute|
+ it "clones #{attribute} build attribute" do
+ expect(new_build.send(attribute)).to eq build.send(attribute)
+ end
+ end
+ end
+
+ describe 'reject attributes' do
+ described_class::REJECT_ATTRIBUTES.each do |attribute|
+ it "does not clone #{attribute} build attribute" do
+ expect(new_build.send(attribute)).not_to eq build.send(attribute)
+ end
+ end
+ end
+
+ it 'has correct number of known attributes' do
+ attributes =
+ described_class::CLONE_ATTRIBUTES +
+ described_class::IGNORE_ATTRIBUTES +
+ described_class::REJECT_ATTRIBUTES
+
+ expect(build.attributes.size).to eq(attributes.size)
+ end
+ end
+
+ describe '#execute' do
+ let(:new_build) { service.execute(build) }
+
+ context 'when user has ability to execute build' do
+ before do
+ project.add_developer(user)
+ end
+
+ it_behaves_like 'build duplication'
+
+ it 'creates a new build that represents the old one' do
+ expect(new_build.name).to eq build.name
+ end
+
+ it 'enqueues the new build' do
+ expect(new_build).to be_pending
+ end
+
+ it 'resolves todos for old build that failed' do
+ expect(MergeRequests::AddTodoWhenBuildFailsService)
+ .to receive_message_chain(:new, :close)
+
+ service.execute(build)
+ end
+
+ context 'when there are subsequent builds that are skipped' do
+ let!(:subsequent_build) do
+ create(:ci_build, :skipped, stage_idx: 1, pipeline: pipeline)
+ end
+
+ it 'resumes pipeline processing in subsequent stages' do
+ service.execute(build)
+
+ expect(subsequent_build.reload).to be_created
+ end
+ end
+ end
+
+ context 'when user does not have ability to execute build' do
+ it 'raises an error' do
+ expect { service.execute(build) }
+ .to raise_error Gitlab::Access::AccessDeniedError
+ end
+ end
+ end
+
+ describe '#reprocess' do
+ let(:new_build) { service.reprocess(build) }
+
+ context 'when user has ability to execute build' do
+ before do
+ project.add_developer(user)
+ end
+
+ it_behaves_like 'build duplication'
+
+ it 'creates a new build that represents the old one' do
+ expect(new_build.name).to eq build.name
+ end
+
+ it 'does not enqueue the new build' do
+ expect(new_build).to be_created
+ end
+ end
+
+ context 'when user does not have ability to execute build' do
+ it 'raises an error' do
+ expect { service.reprocess(build) }
+ .to raise_error Gitlab::Access::AccessDeniedError
+ end
+ end
+ end
+end
diff --git a/spec/services/ci/retry_pipeline_service_spec.rb b/spec/services/ci/retry_pipeline_service_spec.rb
new file mode 100644
index 00000000000..8b1ed6470e4
--- /dev/null
+++ b/spec/services/ci/retry_pipeline_service_spec.rb
@@ -0,0 +1,196 @@
+require 'spec_helper'
+
+describe Ci::RetryPipelineService, '#execute', :services do
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project) }
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+ let(:service) { described_class.new(project, user) }
+
+ context 'when user has ability to modify pipeline' do
+ let(:user) { create(:admin) }
+
+ context 'when there are failed builds in the last stage' do
+ before do
+ create_build('rspec 1', :success, 0)
+ create_build('rspec 2', :failed, 1)
+ create_build('rspec 3', :canceled, 1)
+ end
+
+ it 'enqueues all builds in the last stage' do
+ service.execute(pipeline)
+
+ expect(build('rspec 2')).to be_pending
+ expect(build('rspec 3')).to be_pending
+ expect(pipeline.reload).to be_running
+ end
+ end
+
+ context 'when there are failed or canceled builds in the first stage' do
+ before do
+ create_build('rspec 1', :failed, 0)
+ create_build('rspec 2', :canceled, 0)
+ create_build('rspec 3', :canceled, 1)
+ create_build('spinach 1', :canceled, 2)
+ end
+
+ it 'retries builds failed builds and marks subsequent for processing' do
+ service.execute(pipeline)
+
+ expect(build('rspec 1')).to be_pending
+ expect(build('rspec 2')).to be_pending
+ expect(build('rspec 3')).to be_created
+ expect(build('spinach 1')).to be_created
+ expect(pipeline.reload).to be_running
+ end
+ end
+
+ context 'when there is failed build present which was run on failure' do
+ before do
+ create_build('rspec 1', :failed, 0)
+ create_build('rspec 2', :canceled, 0)
+ create_build('rspec 3', :canceled, 1)
+ create_build('report 1', :failed, 2)
+ end
+
+ it 'retries builds only in the first stage' do
+ service.execute(pipeline)
+
+ expect(build('rspec 1')).to be_pending
+ expect(build('rspec 2')).to be_pending
+ expect(build('rspec 3')).to be_created
+ expect(build('report 1')).to be_created
+ expect(pipeline.reload).to be_running
+ end
+
+ it 'creates a new job for report job in this case' do
+ service.execute(pipeline)
+
+ expect(statuses.where(name: 'report 1').first).to be_retried
+ end
+ end
+
+ context 'when the last stage was skipepd' do
+ before do
+ create_build('build 1', :success, 0)
+ create_build('test 2', :failed, 1)
+ create_build('report 3', :skipped, 2)
+ create_build('report 4', :skipped, 2)
+ end
+
+ it 'retries builds only in the first stage' do
+ service.execute(pipeline)
+
+ expect(build('build 1')).to be_success
+ expect(build('test 2')).to be_pending
+ expect(build('report 3')).to be_created
+ expect(build('report 4')).to be_created
+ expect(pipeline.reload).to be_running
+ end
+ end
+
+ context 'when pipeline contains manual actions' do
+ context 'when there is a canceled manual action in first stage' do
+ before do
+ create_build('rspec 1', :failed, 0)
+ create_build('staging', :canceled, 0, :manual)
+ create_build('rspec 2', :canceled, 1)
+ end
+
+ it 'retries builds failed builds and marks subsequent for processing' do
+ service.execute(pipeline)
+
+ expect(build('rspec 1')).to be_pending
+ expect(build('staging')).to be_skipped
+ expect(build('rspec 2')).to be_created
+ expect(pipeline.reload).to be_running
+ end
+ end
+
+ context 'when there is a skipped manual action in last stage' do
+ before do
+ create_build('rspec 1', :canceled, 0)
+ create_build('rspec 2', :skipped, 0, :manual)
+ create_build('staging', :skipped, 1, :manual)
+ end
+
+ it 'retries canceled job and reprocesses manual actions' do
+ service.execute(pipeline)
+
+ expect(build('rspec 1')).to be_pending
+ expect(build('rspec 2')).to be_skipped
+ expect(build('staging')).to be_created
+ expect(pipeline.reload).to be_running
+ end
+ end
+
+ context 'when there is a created manual action in the last stage' do
+ before do
+ create_build('rspec 1', :canceled, 0)
+ create_build('staging', :created, 1, :manual)
+ end
+
+ it 'retries canceled job and does not update the manual action' do
+ service.execute(pipeline)
+
+ expect(build('rspec 1')).to be_pending
+ expect(build('staging')).to be_created
+ expect(pipeline.reload).to be_running
+ end
+ end
+
+ context 'when there is a created manual action in the first stage' do
+ before do
+ create_build('rspec 1', :canceled, 0)
+ create_build('staging', :created, 0, :manual)
+ end
+
+ it 'retries canceled job and skipps the manual action' do
+ service.execute(pipeline)
+
+ expect(build('rspec 1')).to be_pending
+ expect(build('staging')).to be_skipped
+ expect(pipeline.reload).to be_running
+ end
+ end
+ end
+
+ it 'closes all todos about failed jobs for pipeline' do
+ expect(MergeRequests::AddTodoWhenBuildFailsService)
+ .to receive_message_chain(:new, :close_all)
+
+ service.execute(pipeline)
+ end
+
+ it 'reprocesses the pipeline' do
+ expect(pipeline).to receive(:process!)
+
+ service.execute(pipeline)
+ end
+ end
+
+ context 'when user is not allowed to retry pipeline' do
+ it 'raises an error' do
+ expect { service.execute(pipeline) }
+ .to raise_error Gitlab::Access::AccessDeniedError
+ end
+ end
+
+ def statuses
+ pipeline.reload.statuses
+ end
+
+ def build(name)
+ statuses.latest.find_by(name: name)
+ end
+
+ def create_build(name, status, stage_num, on = 'on_success')
+ create(:ci_build, name: name,
+ status: status,
+ stage: "stage_#{stage_num}",
+ stage_idx: stage_num,
+ when: on,
+ pipeline: pipeline) do |build|
+ pipeline.update_status
+ end
+ end
+end
diff --git a/spec/services/ci/update_runner_service_spec.rb b/spec/services/ci/update_runner_service_spec.rb
new file mode 100644
index 00000000000..e429fcfc72f
--- /dev/null
+++ b/spec/services/ci/update_runner_service_spec.rb
@@ -0,0 +1,41 @@
+require 'spec_helper'
+
+describe Ci::UpdateRunnerService, :services do
+ let(:runner) { create(:ci_runner) }
+
+ describe '#update' do
+ before do
+ allow(runner).to receive(:tick_runner_queue)
+ end
+
+ context 'with description params' do
+ let(:params) { { description: 'new runner' } }
+
+ it 'updates the runner and ticking the queue' do
+ expect(update).to be_truthy
+
+ runner.reload
+
+ expect(runner).to have_received(:tick_runner_queue)
+ expect(runner.description).to eq('new runner')
+ end
+ end
+
+ context 'when params are not valid' do
+ let(:params) { { run_untagged: false } }
+
+ it 'does not update and give false because it is not valid' do
+ expect(update).to be_falsey
+
+ runner.reload
+
+ expect(runner).not_to have_received(:tick_runner_queue)
+ expect(runner.run_untagged).to be_truthy
+ end
+ end
+
+ def update
+ described_class.new(runner).update(params)
+ end
+ end
+end
diff --git a/spec/services/create_deployment_service_spec.rb b/spec/services/create_deployment_service_spec.rb
index cf0a18aacec..18b964e2453 100644
--- a/spec/services/create_deployment_service_spec.rb
+++ b/spec/services/create_deployment_service_spec.rb
@@ -9,7 +9,8 @@ describe CreateDeploymentService, services: true do
describe '#execute' do
let(:options) { nil }
let(:params) do
- { environment: 'production',
+ {
+ environment: 'production',
ref: 'master',
tag: false,
sha: '97de212e80737a608d939f648d959671fb0a0142',
@@ -83,10 +84,11 @@ describe CreateDeploymentService, services: true do
context 'for environment with invalid name' do
let(:params) do
- { environment: 'name,with,commas',
+ {
+ environment: 'name,with,commas',
ref: 'master',
tag: false,
- sha: '97de212e80737a608d939f648d959671fb0a0142',
+ sha: '97de212e80737a608d939f648d959671fb0a0142'
}
end
@@ -101,7 +103,8 @@ describe CreateDeploymentService, services: true do
context 'when variables are used' do
let(:params) do
- { environment: 'review-apps/$CI_BUILD_REF_NAME',
+ {
+ environment: 'review-apps/$CI_BUILD_REF_NAME',
ref: 'master',
tag: false,
sha: '97de212e80737a608d939f648d959671fb0a0142',
@@ -234,7 +237,11 @@ describe CreateDeploymentService, services: true do
context 'when build is retried' do
it_behaves_like 'does create environment and deployment' do
- let(:deployable) { Ci::Build.retry(build) }
+ before do
+ project.add_developer(user)
+ end
+
+ let(:deployable) { Ci::Build.retry(build, user) }
subject { deployable.success }
end
diff --git a/spec/services/groups/destroy_service_spec.rb b/spec/services/groups/destroy_service_spec.rb
index f86189b68e9..98c560ffb26 100644
--- a/spec/services/groups/destroy_service_spec.rb
+++ b/spec/services/groups/destroy_service_spec.rb
@@ -5,18 +5,24 @@ describe Groups::DestroyService, services: true do
let!(:user) { create(:user) }
let!(:group) { create(:group) }
+ let!(:nested_group) { create(:group, parent: group) }
let!(:project) { create(:project, namespace: group) }
let!(:gitlab_shell) { Gitlab::Shell.new }
let!(:remove_path) { group.path + "+#{group.id}+deleted" }
+ before do
+ group.add_user(user, Gitlab::Access::OWNER)
+ end
+
shared_examples 'group destruction' do |async|
context 'database records' do
before do
destroy_group(group, user, async)
end
- it { expect(Group.all).not_to include(group) }
- it { expect(Project.all).not_to include(project) }
+ it { expect(Group.unscoped.all).not_to include(group) }
+ it { expect(Group.unscoped.all).not_to include(nested_group) }
+ it { expect(Project.unscoped.all).not_to include(project) }
end
context 'file system' do
@@ -32,7 +38,7 @@ describe Groups::DestroyService, services: true do
context 'Sidekiq fake' do
before do
- # Dont run sidekiq to check if renamed repository exists
+ # Don't run sidekiq to check if renamed repository exists
Sidekiq::Testing.fake! { destroy_group(group, user, async) }
end
@@ -95,4 +101,13 @@ describe Groups::DestroyService, services: true do
describe 'synchronous delete' do
it_behaves_like 'group destruction', false
end
+
+ context 'projects in pending_delete' do
+ before do
+ project.pending_delete = true
+ project.save
+ end
+
+ it_behaves_like 'group destruction', false
+ end
end
diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb
index e1feeed8a67..6045d00ff09 100644
--- a/spec/services/issues/create_service_spec.rb
+++ b/spec/services/issues/create_service_spec.rb
@@ -230,16 +230,6 @@ describe Issues::CreateService, services: true 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
diff --git a/spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb b/spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb
index bb7830c7eea..d80fb8a1af1 100644
--- a/spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb
+++ b/spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb
@@ -17,7 +17,7 @@ describe MergeRequests::AddTodoWhenBuildFailsService do
described_class.new(project, user, commit_message: 'Awesome message')
end
- let(:todo_service) { TodoService.new }
+ let(:todo_service) { spy('todo service') }
let(:merge_request) do
create(:merge_request, merge_user: user,
@@ -107,4 +107,27 @@ describe MergeRequests::AddTodoWhenBuildFailsService do
end
end
end
+
+ describe '#close_all' do
+ context 'when using pipeline that belongs to merge request' do
+ it 'resolves todos about failed builds for pipeline' do
+ service.close_all(pipeline)
+
+ expect(todo_service)
+ .to have_received(:merge_request_build_retried)
+ .with(merge_request)
+ end
+ end
+
+ context 'when pipeline is not related to merge request' do
+ let(:pipeline) { create(:ci_empty_pipeline) }
+
+ it 'does not resolve any todos about failed builds' do
+ service.close_all(pipeline)
+
+ expect(todo_service)
+ .not_to have_received(:merge_request_build_retried)
+ end
+ end
+ end
end
diff --git a/spec/services/merge_requests/build_service_spec.rb b/spec/services/merge_requests/build_service_spec.rb
index dc945ca4868..0768f644036 100644
--- a/spec/services/merge_requests/build_service_spec.rb
+++ b/spec/services/merge_requests/build_service_spec.rb
@@ -44,15 +44,14 @@ describe MergeRequests::BuildService, services: true do
end
end
- context 'missing target branch' do
- let(:target_branch) { '' }
+ context 'when target branch is missing' do
+ let(:target_branch) { nil }
+ let(:commits) { Commit.decorate([commit_1], project) }
- it 'forbids the merge request from being created' do
+ it 'creates compare object with target branch as default branch' do
expect(merge_request.can_be_created).to eq(false)
- end
-
- it 'adds an error message to the merge request' do
- expect(merge_request.errors).to contain_exactly('You must select source and target branch')
+ expect(merge_request.compare).to be_present
+ expect(merge_request.target_branch).to eq(project.default_branch)
end
end
diff --git a/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb b/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb
index f92978a33a3..0ff6e8fda16 100644
--- a/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb
+++ b/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb
@@ -111,6 +111,31 @@ describe MergeRequests::MergeWhenPipelineSucceedsService do
service.trigger(unrelated_pipeline)
end
end
+
+ context 'when the merge request is not mergeable' do
+ let(:mr_conflict) do
+ create(:merge_request, merge_when_build_succeeds: true, merge_user: user,
+ source_branch: 'master', target_branch: 'feature-conflict',
+ source_project: project, target_project: project)
+ end
+
+ let(:conflict_pipeline) do
+ create(:ci_pipeline, project: project, ref: mr_conflict.source_branch,
+ sha: mr_conflict.diff_head_sha, status: 'success')
+ end
+
+ it 'does not merge the merge request' do
+ expect(MergeWorker).not_to receive(:perform_async)
+
+ service.trigger(conflict_pipeline)
+ end
+
+ it 'creates todos for unmergeability' do
+ expect_any_instance_of(TodoService).to receive(:merge_request_became_unmergeable).with(mr_conflict)
+
+ service.trigger(conflict_pipeline)
+ end
+ end
end
describe "#cancel" do
diff --git a/spec/services/merge_requests/resolve_service_spec.rb b/spec/services/merge_requests/resolve_service_spec.rb
index a0e51681725..d33535d22af 100644
--- a/spec/services/merge_requests/resolve_service_spec.rb
+++ b/spec/services/merge_requests/resolve_service_spec.rb
@@ -59,20 +59,19 @@ describe MergeRequests::ResolveService do
it 'creates a commit with the correct parents' do
expect(merge_request.source_branch_head.parents.map(&:id)).
- to eq(['1450cd639e0bc6721eb02800169e464f212cde06',
- '824be604a34828eb682305f0d963056cfac87b2d'])
+ to eq(%w(1450cd639e0bc6721eb02800169e464f212cde06
+ 824be604a34828eb682305f0d963056cfac87b2d))
end
end
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(
+ project.repository.create_file(
user,
'new-file-in-target',
'',
message: 'Add new file in target',
- branch_name: 'conflict-start',
- update: false)
+ branch_name: 'conflict-start')
end
before do
@@ -125,8 +124,8 @@ describe MergeRequests::ResolveService do
it 'creates a commit with the correct parents' do
expect(merge_request.source_branch_head.parents.map(&:id)).
- to eq(['1450cd639e0bc6721eb02800169e464f212cde06',
- '824be604a34828eb682305f0d963056cfac87b2d'])
+ to eq(%w(1450cd639e0bc6721eb02800169e464f212cde06
+ 824be604a34828eb682305f0d963056cfac87b2d))
end
it 'sets the content to the content given' do
diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb
index af515ad2e0e..62f21049b0b 100644
--- a/spec/services/projects/create_service_spec.rb
+++ b/spec/services/projects/create_service_spec.rb
@@ -50,7 +50,7 @@ describe Projects::CreateService, '#execute', services: true do
context 'error handling' do
it 'handles invalid options' do
- opts.merge!({ default_branch: 'master' } )
+ opts[:default_branch] = 'master'
expect(create_project(user, opts)).to eq(nil)
end
end
@@ -67,7 +67,7 @@ describe Projects::CreateService, '#execute', services: true do
context 'wiki_enabled false does not create wiki repository directory' do
it do
- opts.merge!(wiki_enabled: false)
+ opts[:wiki_enabled] = false
project = create_project(user, opts)
path = ProjectWiki.new(project, user).send(:path_to_repo)
diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb
index 3faa88c00a1..74bfba44dfd 100644
--- a/spec/services/projects/destroy_service_spec.rb
+++ b/spec/services/projects/destroy_service_spec.rb
@@ -50,6 +50,25 @@ describe Projects::DestroyService, services: true do
it { expect(Dir.exist?(remove_path)).to be_truthy }
end
+ context 'when flushing caches fail' do
+ before do
+ new_user = create(:user)
+ project.team.add_user(new_user, Gitlab::Access::DEVELOPER)
+ allow_any_instance_of(Projects::DestroyService).to receive(:flush_caches).and_raise(Redis::CannotConnectError)
+ end
+
+ it 'keeps project team intact upon an error' do
+ Sidekiq::Testing.inline! do
+ begin
+ destroy_project(project, user, {})
+ rescue Redis::CannotConnectError
+ end
+ end
+
+ expect(project.team.members.count).to eq 1
+ end
+ end
+
context 'with async_execute' do
let(:async) { true }
diff --git a/spec/services/protected_branches/create_service_spec.rb b/spec/services/protected_branches/create_service_spec.rb
index 7d4eff3b6ef..6ea8f309981 100644
--- a/spec/services/protected_branches/create_service_spec.rb
+++ b/spec/services/protected_branches/create_service_spec.rb
@@ -6,8 +6,8 @@ describe ProtectedBranches::CreateService, services: true do
let(:params) do
{
name: 'master',
- merge_access_levels_attributes: [ { access_level: Gitlab::Access::MASTER } ],
- push_access_levels_attributes: [ { access_level: Gitlab::Access::MASTER } ]
+ merge_access_levels_attributes: [{ access_level: Gitlab::Access::MASTER }],
+ push_access_levels_attributes: [{ access_level: Gitlab::Access::MASTER }]
}
end
diff --git a/spec/services/spam_service_spec.rb b/spec/services/spam_service_spec.rb
index 271c17dd8c0..4ce3b95aa87 100644
--- a/spec/services/spam_service_spec.rb
+++ b/spec/services/spam_service_spec.rb
@@ -1,46 +1,61 @@
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: {}) }
+ describe '#when_recaptcha_verified' do
+ def check_spam(issue, request, recaptcha_verified)
+ described_class.new(issue, request).when_recaptcha_verified(recaptcha_verified) do
+ 'yielded'
+ end
+ end
+
+ it 'yields block when recaptcha was already verified' do
+ issue = build_stubbed(:issue)
- def check_spam(issue, request)
- described_class.new(issue, request).check
+ expect(check_spam(issue, nil, true)).to eql('yielded')
end
- context 'when indicated as spam by akismet' do
- before { allow(AkismetService).to receive(:new).and_return(double(is_spam?: true)) }
+ context 'when recaptcha was not verified' do
+ let(:project) { create(:project, :public) }
+ let(:issue) { create(:issue, project: project) }
+ let(:request) { double(:request, env: {}) }
- it 'returns false when request is missing' do
- expect(check_spam(issue, nil)).to be_falsey
- 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 issue is not public' do
- issue = create(:issue, project: create(:project, :private))
+ it 'doesnt check as spam when request is missing' do
+ check_spam(issue, nil, false)
- expect(check_spam(issue, request)).to be_falsey
- end
+ expect(issue.spam).to be_falsey
+ end
- it 'returns true' do
- expect(check_spam(issue, request)).to be_truthy
- end
+ it 'checks as spam' do
+ check_spam(issue, request, false)
- it 'creates a spam log' do
- expect { check_spam(issue, request) }.to change { SpamLog.count }.from(0).to(1)
- end
- end
+ expect(issue.spam).to be_truthy
+ end
- context 'when not indicated as spam by akismet' do
- before { allow(AkismetService).to receive(:new).and_return(double(is_spam?: false)) }
+ it 'creates a spam log' do
+ expect { check_spam(issue, request, false) }
+ .to change { SpamLog.count }.from(0).to(1)
+ end
- it 'returns false' do
- expect(check_spam(issue, request)).to be_falsey
+ it 'doesnt yield block' do
+ expect(check_spam(issue, request, false))
+ .to eql(SpamLog.last)
+ end
end
- it 'does not create a spam log' do
- expect { check_spam(issue, request) }.not_to change { SpamLog.count }
+ 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, false)).to be_falsey
+ end
+
+ it 'does not create a spam log' do
+ expect { check_spam(issue, request, false) }
+ .not_to change { SpamLog.count }
+ end
end
end
end
diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb
index 7f027ae02a2..eca5a418f2a 100644
--- a/spec/services/system_note_service_spec.rb
+++ b/spec/services/system_note_service_spec.rb
@@ -631,7 +631,7 @@ describe SystemNoteService, services: true do
jira_service_settings
end
- noteable_types = ["merge_requests", "commit"]
+ noteable_types = %w(merge_requests commit)
noteable_types.each do |type|
context "when noteable is a #{type}" do
diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb
index 4320365ab57..9f24cc0f3f2 100644
--- a/spec/services/todo_service_spec.rb
+++ b/spec/services/todo_service_spec.rb
@@ -287,39 +287,51 @@ describe TodoService, services: true do
end
end
- shared_examples 'marking todos as done' do |meth|
- let!(:first_todo) { create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) }
- let!(:second_todo) { create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) }
+ shared_examples 'updating todos state' do |meth, state, new_state|
+ let!(:first_todo) { create(:todo, state, user: john_doe, project: project, target: issue, author: author) }
+ let!(:second_todo) { create(:todo, state, user: john_doe, project: project, target: issue, author: author) }
- it 'marks related todos for the user as done' do
+ it 'updates related todos for the user with the new_state' do
service.send(meth, collection, john_doe)
- expect(first_todo.reload).to be_done
- expect(second_todo.reload).to be_done
+ expect(first_todo.reload.state?(new_state)).to be true
+ expect(second_todo.reload.state?(new_state)).to be true
end
describe 'cached counts' do
it 'updates when todos change' do
- expect(john_doe.todos_done_count).to eq(0)
- expect(john_doe.todos_pending_count).to eq(2)
+ expect(john_doe.todos.where(state: new_state).count).to eq(0)
+ expect(john_doe.todos.where(state: state).count).to eq(2)
expect(john_doe).to receive(:update_todos_count_cache).and_call_original
service.send(meth, collection, john_doe)
- expect(john_doe.todos_done_count).to eq(2)
- expect(john_doe.todos_pending_count).to eq(0)
+ expect(john_doe.todos.where(state: new_state).count).to eq(2)
+ expect(john_doe.todos.where(state: state).count).to eq(0)
end
end
end
describe '#mark_todos_as_done' do
- it_behaves_like 'marking todos as done', :mark_todos_as_done do
+ it_behaves_like 'updating todos state', :mark_todos_as_done, :pending, :done do
let(:collection) { [first_todo, second_todo] }
end
end
describe '#mark_todos_as_done_by_ids' do
- it_behaves_like 'marking todos as done', :mark_todos_as_done_by_ids do
+ it_behaves_like 'updating todos state', :mark_todos_as_done_by_ids, :pending, :done do
+ let(:collection) { [first_todo, second_todo].map(&:id) }
+ end
+ end
+
+ describe '#mark_todos_as_pending' do
+ it_behaves_like 'updating todos state', :mark_todos_as_pending, :done, :pending do
+ let(:collection) { [first_todo, second_todo] }
+ end
+ end
+
+ describe '#mark_todos_as_pending_by_ids' do
+ it_behaves_like 'updating todos state', :mark_todos_as_pending_by_ids, :done, :pending do
let(:collection) { [first_todo, second_todo].map(&:id) }
end
end
diff --git a/spec/services/users/destroy_spec.rb b/spec/services/users/destroy_spec.rb
index 46e58393218..922e82445d0 100644
--- a/spec/services/users/destroy_spec.rb
+++ b/spec/services/users/destroy_spec.rb
@@ -2,11 +2,11 @@ require 'spec_helper'
describe Users::DestroyService, services: true do
describe "Deletes a user and all their personal projects" do
- let!(:user) { create(:user) }
- let!(:current_user) { create(:user) }
- let!(:namespace) { create(:namespace, owner: user) }
- let!(:project) { create(:project, namespace: namespace) }
- let(:service) { described_class.new(current_user) }
+ let!(:user) { create(:user) }
+ let!(:admin) { create(:admin) }
+ let!(:namespace) { create(:namespace, owner: user) }
+ let!(:project) { create(:project, namespace: namespace) }
+ let(:service) { described_class.new(admin) }
context 'no options are given' do
it 'deletes the user' do
@@ -24,6 +24,54 @@ describe Users::DestroyService, services: true do
end
end
+ context "a deleted user's issues" do
+ let(:project) { create :project }
+
+ before do
+ project.add_developer(user)
+ end
+
+ context "for an issue the user has created" do
+ let!(:issue) { create(:issue, project: project, author: user) }
+
+ before do
+ service.execute(user)
+ end
+
+ it 'does not delete the issue' do
+ expect(Issue.find_by_id(issue.id)).to be_present
+ end
+
+ it 'migrates the issue so that the "Ghost User" is the issue owner' do
+ migrated_issue = Issue.find_by_id(issue.id)
+
+ expect(migrated_issue.author).to eq(User.ghost)
+ end
+
+ it 'blocks the user before migrating issues to the "Ghost User' do
+ expect(user).to be_blocked
+ end
+ end
+
+ context "for an issue the user was assigned to" do
+ let!(:issue) { create(:issue, project: project, assignee: user) }
+
+ before do
+ service.execute(user)
+ end
+
+ it 'does not delete issues the user is assigned to' do
+ expect(Issue.find_by_id(issue.id)).to be_present
+ end
+
+ it 'migrates the issue so that it is "Unassigned"' do
+ migrated_issue = Issue.find_by_id(issue.id)
+
+ expect(migrated_issue.assignee).to be_nil
+ end
+ end
+ end
+
context "solo owned groups present" do
let(:solo_owned) { create(:group) }
let(:member) { create(:group_member) }
@@ -57,5 +105,26 @@ describe Users::DestroyService, services: true do
expect { User.find(user.id) }.to raise_error(ActiveRecord::RecordNotFound)
end
end
+
+ context "deletion permission checks" do
+ it 'does not delete the user when user is not an admin' do
+ other_user = create(:user)
+
+ expect { described_class.new(other_user).execute(user) }.to raise_error(Gitlab::Access::AccessDeniedError)
+ expect(User.exists?(user.id)).to be(true)
+ end
+
+ it 'allows admins to delete anyone' do
+ described_class.new(admin).execute(user)
+
+ expect(User.exists?(user.id)).to be(false)
+ end
+
+ it 'allows users to delete their own account' do
+ described_class.new(user).execute(user)
+
+ expect(User.exists?(user.id)).to be(false)
+ end
+ 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 690fe979492..08733d6dcf1 100644
--- a/spec/services/users/refresh_authorized_projects_service_spec.rb
+++ b/spec/services/users/refresh_authorized_projects_service_spec.rb
@@ -131,6 +131,80 @@ describe Users::RefreshAuthorizedProjectsService do
it 'sets the values to the access levels' do
expect(hash.values).to eq([Gitlab::Access::MASTER])
end
+
+ context 'personal projects' do
+ it 'includes the project with the right access level' do
+ expect(hash[project.id]).to eq(Gitlab::Access::MASTER)
+ end
+ end
+
+ context 'projects the user is a member of' do
+ let!(:other_project) { create(:empty_project) }
+
+ before do
+ other_project.team.add_reporter(user)
+ end
+
+ it 'includes the project with the right access level' do
+ expect(hash[other_project.id]).to eq(Gitlab::Access::REPORTER)
+ end
+ end
+
+ context 'projects of groups the user is a member of' do
+ let(:group) { create(:group) }
+ let!(:other_project) { create(:project, group: group) }
+
+ before do
+ group.add_owner(user)
+ end
+
+ it 'includes the project with the right access level' do
+ expect(hash[other_project.id]).to eq(Gitlab::Access::OWNER)
+ end
+ end
+
+ context 'projects of subgroups of groups the user is a member of' do
+ let(:group) { create(:group) }
+ let(:nested_group) { create(:group, parent: group) }
+ let!(:other_project) { create(:project, group: nested_group) }
+
+ before do
+ group.add_master(user)
+ end
+
+ it 'includes the project with the right access level' do
+ expect(hash[other_project.id]).to eq(Gitlab::Access::MASTER)
+ end
+ end
+
+ context 'projects shared with groups the user is a member of' do
+ let(:group) { create(:group) }
+ let(:other_project) { create(:empty_project) }
+ let!(:project_group_link) { create(:project_group_link, project: other_project, group: group, group_access: Gitlab::Access::GUEST) }
+
+ before do
+ group.add_master(user)
+ end
+
+ it 'includes the project with the right access level' do
+ expect(hash[other_project.id]).to eq(Gitlab::Access::GUEST)
+ end
+ end
+
+ context 'projects shared with subgroups of groups the user is a member of' do
+ let(:group) { create(:group) }
+ let(:nested_group) { create(:group, parent: group) }
+ let(:other_project) { create(:empty_project) }
+ let!(:project_group_link) { create(:project_group_link, project: other_project, group: nested_group, group_access: Gitlab::Access::DEVELOPER) }
+
+ before do
+ group.add_master(user)
+ end
+
+ it 'includes the project with the right access level' do
+ expect(hash[other_project.id]).to eq(Gitlab::Access::DEVELOPER)
+ end
+ end
end
describe '#current_authorizations_per_project' do
diff --git a/spec/support/api/pagination_shared_examples.rb b/spec/support/api/pagination_shared_examples.rb
deleted file mode 100644
index 352a6eeec79..00000000000
--- a/spec/support/api/pagination_shared_examples.rb
+++ /dev/null
@@ -1,20 +0,0 @@
-# Specs for paginated resources.
-#
-# Requires an API request:
-# let(:request) { get api("/projects/#{project.id}/repository/branches", user) }
-shared_examples 'a paginated resources' do
- before do
- # Fires the request
- request
- end
-
- it 'has pagination headers' do
- expect(response.headers).to include('X-Total')
- expect(response.headers).to include('X-Total-Pages')
- expect(response.headers).to include('X-Per-Page')
- expect(response.headers).to include('X-Page')
- expect(response.headers).to include('X-Next-Page')
- expect(response.headers).to include('X-Prev-Page')
- expect(response.headers).to include('Link')
- end
-end
diff --git a/spec/support/cycle_analytics_helpers.rb b/spec/support/cycle_analytics_helpers.rb
index 6ed55289ed9..c864a705ca4 100644
--- a/spec/support/cycle_analytics_helpers.rb
+++ b/spec/support/cycle_analytics_helpers.rb
@@ -9,14 +9,7 @@ module CycleAnalyticsHelpers
commit_shas = Array.new(count) do |index|
filename = random_git_name
- options = {
- committer: project.repository.user_to_committer(user),
- author: project.repository.user_to_committer(user),
- commit: { message: message, branch: branch_name, update_ref: true },
- file: { content: "content", path: filename, update: false }
- }
-
- commit_sha = Gitlab::Git::Blob.commit(project.repository, options)
+ commit_sha = project.repository.create_file(user, filename, "content", message: message, branch_name: branch_name)
project.repository.commit(commit_sha)
commit_sha
@@ -35,13 +28,12 @@ module CycleAnalyticsHelpers
project.repository.add_branch(user, source_branch, 'master')
end
- sha = project.repository.commit_file(
+ sha = project.repository.create_file(
user,
random_git_name,
'content',
message: 'commit message',
- branch_name: source_branch,
- update: false)
+ branch_name: source_branch)
project.repository.commit(sha)
opts = {
diff --git a/spec/support/db_cleaner.rb b/spec/support/db_cleaner.rb
index 247f0954221..6f31828b825 100644
--- a/spec/support/db_cleaner.rb
+++ b/spec/support/db_cleaner.rb
@@ -3,6 +3,10 @@ RSpec.configure do |config|
DatabaseCleaner.clean_with(:truncation)
end
+ config.append_after(:context) do
+ DatabaseCleaner.clean_with(:truncation)
+ end
+
config.before(:each) do
DatabaseCleaner.strategy = :transaction
end
diff --git a/spec/support/dropzone_helper.rb b/spec/support/dropzone_helper.rb
new file mode 100644
index 00000000000..984ec7d2741
--- /dev/null
+++ b/spec/support/dropzone_helper.rb
@@ -0,0 +1,37 @@
+module DropzoneHelper
+ # Provides a way to perform `attach_file` for a Dropzone-based file input
+ #
+ # This is accomplished by creating a standard HTML file input on the page,
+ # performing `attach_file` on that field, and then triggering the appropriate
+ # Dropzone events to perform the actual upload.
+ #
+ # This method waits for the upload to complete before returning.
+ def dropzone_file(file_path)
+ # Generate a fake file input that Capybara can attach to
+ page.execute_script <<-JS.strip_heredoc
+ var fakeFileInput = window.$('<input/>').attr(
+ {id: 'fakeFileInput', type: 'file'}
+ ).appendTo('body');
+
+ window._dropzoneComplete = false;
+ JS
+
+ # Attach the file to the fake input selector with Capybara
+ attach_file('fakeFileInput', file_path)
+
+ # Manually trigger a Dropzone "drop" event with the fake input's file list
+ page.execute_script <<-JS.strip_heredoc
+ var fileList = [$('#fakeFileInput')[0].files[0]];
+ var e = jQuery.Event('drop', { dataTransfer : { files : fileList } });
+
+ var dropzone = $('.div-dropzone')[0].dropzone;
+ dropzone.on('queuecomplete', function() {
+ window._dropzoneComplete = true;
+ });
+ dropzone.listeners[0].events.drop(e);
+ JS
+
+ # Wait until Dropzone's fired `queuecomplete`
+ loop until page.evaluate_script('window._dropzoneComplete === true')
+ end
+end
diff --git a/spec/support/filtered_search_helpers.rb b/spec/support/filtered_search_helpers.rb
new file mode 100644
index 00000000000..58f6636e680
--- /dev/null
+++ b/spec/support/filtered_search_helpers.rb
@@ -0,0 +1,37 @@
+module FilteredSearchHelpers
+ def filtered_search
+ page.find('.filtered-search')
+ end
+
+ def input_filtered_search(search_term, submit: true)
+ filtered_search.set(search_term)
+
+ if submit
+ filtered_search.send_keys(:enter)
+ end
+ end
+
+ def input_filtered_search_keys(search_term)
+ filtered_search.send_keys(search_term)
+ filtered_search.send_keys(:enter)
+ end
+
+ def expect_filtered_search_input(input)
+ expect(find('.filtered-search').value).to eq(input)
+ end
+
+ def clear_search_field
+ find('.filtered-search-input-container .clear-search').click
+ end
+
+ def reset_filters
+ clear_search_field
+ filtered_search.send_keys(:enter)
+ 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
+end
diff --git a/spec/support/gitlab_stubs/session.json b/spec/support/gitlab_stubs/session.json
index ce8dfe5ae75..cd55d63125e 100644
--- a/spec/support/gitlab_stubs/session.json
+++ b/spec/support/gitlab_stubs/session.json
@@ -7,7 +7,7 @@
"skype":"aertert",
"linkedin":"",
"twitter":"",
- "theme_id":2,"color_scheme_id":2,
+ "color_scheme_id":2,
"state":"active",
"created_at":"2012-12-21T13:02:20Z",
"extern_uid":null,
@@ -17,4 +17,4 @@
"can_create_project":false,
"private_token":"Wvjy2Krpb7y8xi93owUz",
"access_token":"Wvjy2Krpb7y8xi93owUz"
-} \ No newline at end of file
+}
diff --git a/spec/support/gitlab_stubs/user.json b/spec/support/gitlab_stubs/user.json
index ce8dfe5ae75..cd55d63125e 100644
--- a/spec/support/gitlab_stubs/user.json
+++ b/spec/support/gitlab_stubs/user.json
@@ -7,7 +7,7 @@
"skype":"aertert",
"linkedin":"",
"twitter":"",
- "theme_id":2,"color_scheme_id":2,
+ "color_scheme_id":2,
"state":"active",
"created_at":"2012-12-21T13:02:20Z",
"extern_uid":null,
@@ -17,4 +17,4 @@
"can_create_project":false,
"private_token":"Wvjy2Krpb7y8xi93owUz",
"access_token":"Wvjy2Krpb7y8xi93owUz"
-} \ No newline at end of file
+}
diff --git a/spec/support/issuables_list_metadata_shared_examples.rb b/spec/support/issuables_list_metadata_shared_examples.rb
index dac94dfc31e..4c0f556e736 100644
--- a/spec/support/issuables_list_metadata_shared_examples.rb
+++ b/spec/support/issuables_list_metadata_shared_examples.rb
@@ -3,11 +3,12 @@ shared_examples 'issuables list meta-data' do |issuable_type, action = nil|
@issuable_ids = []
2.times do
- if issuable_type == :issue
- issuable = create(issuable_type, project: project)
- else
- issuable = create(issuable_type, title: FFaker::Lorem.sentence, source_project: project, source_branch: FFaker::Name.name)
- end
+ issuable =
+ if issuable_type == :issue
+ create(issuable_type, project: project)
+ else
+ create(issuable_type, title: FFaker::Lorem.sentence, source_project: project, source_branch: FFaker::Name.name)
+ end
@issuable_ids << issuable.id
@@ -21,7 +22,7 @@ shared_examples 'issuables list meta-data' do |issuable_type, action = nil|
if action
get action
else
- get :index, namespace_id: project.namespace.path, project_id: project.path
+ get :index, namespace_id: project.namespace, project_id: project
end
meta_data = assigns(:issuable_meta_data)
diff --git a/spec/support/javascript_fixtures_helpers.rb b/spec/support/javascript_fixtures_helpers.rb
index 0b8729db0f9..a982b159b48 100644
--- a/spec/support/javascript_fixtures_helpers.rb
+++ b/spec/support/javascript_fixtures_helpers.rb
@@ -5,7 +5,7 @@ require 'gitlab/popen'
module JavaScriptFixturesHelpers
include Gitlab::Popen
- FIXTURE_PATH = 'spec/javascripts/fixtures'
+ FIXTURE_PATH = 'spec/javascripts/fixtures'.freeze
# Public: Removes all fixture files from given directory
#
diff --git a/spec/support/jira_service_helper.rb b/spec/support/jira_service_helper.rb
index 929fc0c5182..97ae0b6afc5 100644
--- a/spec/support/jira_service_helper.rb
+++ b/spec/support/jira_service_helper.rb
@@ -1,5 +1,5 @@
module JiraServiceHelper
- JIRA_URL = "http://jira.example.net"
+ JIRA_URL = "http://jira.example.net".freeze
JIRA_API = JIRA_URL + "/rest/api/2"
def jira_service_settings
diff --git a/spec/support/kubernetes_helpers.rb b/spec/support/kubernetes_helpers.rb
index 444612cf871..b5ed71ba3be 100644
--- a/spec/support/kubernetes_helpers.rb
+++ b/spec/support/kubernetes_helpers.rb
@@ -2,23 +2,24 @@ module KubernetesHelpers
include Gitlab::Kubernetes
def kube_discovery_body
- { "kind" => "APIResourceList",
+ {
+ "kind" => "APIResourceList",
"resources" => [
{ "name" => "pods", "namespaced" => true, "kind" => "Pod" },
- ],
+ ]
}
end
def kube_pods_body(*pods)
{ "kind" => "PodList",
- "items" => [ kube_pod ],
- }
+ "items" => [kube_pod] }
end
# This is a partial response, it will have many more elements in reality but
# these are the ones we care about at the moment
def kube_pod(app: "valid-pod-label")
- { "metadata" => {
+ {
+ "metadata" => {
"name" => "kube-pod",
"creationTimestamp" => "2016-11-25T19:55:19Z",
"labels" => { "app" => app },
@@ -29,7 +30,7 @@ module KubernetesHelpers
{ "name" => "container-1" },
],
},
- "status" => { "phase" => "Running" },
+ "status" => { "phase" => "Running" }
}
end
diff --git a/spec/support/login_helpers.rb b/spec/support/login_helpers.rb
index ad1eed5b369..9ffb00be0b8 100644
--- a/spec/support/login_helpers.rb
+++ b/spec/support/login_helpers.rb
@@ -15,11 +15,12 @@ module LoginHelpers
# user = create(:user)
# login_as(user)
def login_as(user_or_role)
- if user_or_role.kind_of?(User)
- @user = user_or_role
- else
- @user = create(user_or_role)
- end
+ @user =
+ if user_or_role.is_a?(User)
+ user_or_role
+ else
+ create(user_or_role)
+ end
login_with(@user)
end
diff --git a/spec/support/markdown_feature.rb b/spec/support/markdown_feature.rb
index a79386b5db9..dea0015f105 100644
--- a/spec/support/markdown_feature.rb
+++ b/spec/support/markdown_feature.rb
@@ -79,8 +79,8 @@ class MarkdownFeature
def xproject
@xproject ||= begin
- namespace = create(:namespace, name: 'cross-reference')
- create(:project, namespace: namespace) do |project|
+ group = create(:group, :nested)
+ create(:project, namespace: group) do |project|
project.team << [user, :developer]
end
end
diff --git a/spec/support/matchers/access_matchers.rb b/spec/support/matchers/access_matchers.rb
index ceddb656596..7d238850520 100644
--- a/spec/support/matchers/access_matchers.rb
+++ b/spec/support/matchers/access_matchers.rb
@@ -38,7 +38,7 @@ module AccessMatchers
end
def description_for(user, type)
- if user.kind_of?(User)
+ if user.is_a?(User)
# User#inspect displays too much information for RSpec's descriptions
"be #{type} for the specified user"
else
diff --git a/spec/support/matchers/pagination_matcher.rb b/spec/support/matchers/pagination_matcher.rb
new file mode 100644
index 00000000000..60f5e8239a7
--- /dev/null
+++ b/spec/support/matchers/pagination_matcher.rb
@@ -0,0 +1,5 @@
+RSpec::Matchers.define :include_pagination_headers do |expected|
+ match do |actual|
+ expect(actual.headers).to include('X-Total', 'X-Total-Pages', 'X-Per-Page', 'X-Page', 'X-Next-Page', 'X-Prev-Page', 'Link')
+ end
+end
diff --git a/spec/support/merge_request_helpers.rb b/spec/support/merge_request_helpers.rb
index d5801c8272f..326b85eabd0 100644
--- a/spec/support/merge_request_helpers.rb
+++ b/spec/support/merge_request_helpers.rb
@@ -10,4 +10,13 @@ module MergeRequestHelpers
def last_merge_request
page.all('ul.mr-list > li').last.text
end
+
+ def expect_mr_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 '.mr-list' do
+ expect(page).to have_selector('.merge-request', count: open_count)
+ end
+ end
end
diff --git a/spec/support/repo_helpers.rb b/spec/support/repo_helpers.rb
index 73f375c481b..e9d5c7b12ae 100644
--- a/spec/support/repo_helpers.rb
+++ b/spec/support/repo_helpers.rb
@@ -42,7 +42,7 @@ Signed-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>
eos
)
end
-
+
def another_sample_commit
OpenStruct.new(
id: "e56497bb5f03a90a51293fc6d516788730953899",
@@ -100,13 +100,13 @@ eos
}
]
- commits = [
- '5937ac0a7beb003549fc5fd26fc247adbce4a52e',
- '570e7b2abdd848b95f2f578043fc23bd6f6fd24d',
- '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9',
- 'd14d6c0abdd253381df51a723d58691b2ee1ab08',
- 'c1acaa58bbcbc3eafe538cb8274ba387047b69f8',
- ].reverse # last commit is recent one
+ commits = %w(
+ 5937ac0a7beb003549fc5fd26fc247adbce4a52e
+ 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
+ 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ d14d6c0abdd253381df51a723d58691b2ee1ab08
+ c1acaa58bbcbc3eafe538cb8274ba387047b69f8
+ ).reverse # last commit is recent one
OpenStruct.new(
source_branch: 'master',
diff --git a/spec/support/seed_helper.rb b/spec/support/seed_helper.rb
index 03fa0a66b9a..07f81e9c4f3 100644
--- a/spec/support/seed_helper.rb
+++ b/spec/support/seed_helper.rb
@@ -7,7 +7,7 @@ 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"
+ GITLAB_URL = "https://gitlab.com/gitlab-org/gitlab-git-test.git".freeze
def ensure_seeds
if File.exist?(SEED_REPOSITORY_PATH)
diff --git a/spec/support/seed_repo.rb b/spec/support/seed_repo.rb
index 9f2cd7c67c5..99a500bbbb1 100644
--- a/spec/support/seed_repo.rb
+++ b/spec/support/seed_repo.rb
@@ -25,64 +25,64 @@
module SeedRepo
module BigCommit
- ID = "913c66a37b4a45b9769037c55c2d238bd0942d2e"
- PARENT_ID = "cfe32cf61b73a0d5e9f13e774abde7ff789b1660"
- MESSAGE = "Files, encoding and much more"
- AUTHOR_FULL_NAME = "Dmitriy Zaporozhets"
+ ID = "913c66a37b4a45b9769037c55c2d238bd0942d2e".freeze
+ PARENT_ID = "cfe32cf61b73a0d5e9f13e774abde7ff789b1660".freeze
+ MESSAGE = "Files, encoding and much more".freeze
+ AUTHOR_FULL_NAME = "Dmitriy Zaporozhets".freeze
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"]
+ ID = "570e7b2abdd848b95f2f578043fc23bd6f6fd24d".freeze
+ PARENT_ID = "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9".freeze
+ MESSAGE = "Change some files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n".freeze
+ AUTHOR_FULL_NAME = "Dmitriy Zaporozhets".freeze
+ FILES = ["files/ruby/popen.rb", "files/ruby/regex.rb"].freeze
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"
+ C_FILE_PATH = "files/ruby".freeze
+ C_FILES = ["popen.rb", "regex.rb", "version_info.rb"].freeze
+ 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}.freeze
+ BLOB_FILE_PATH = "app/views/keys/show.html.haml".freeze
end
module EmptyCommit
- ID = "b0e52af38d7ea43cf41d8a6f2471351ac036d6c9"
- PARENT_ID = "40f4a7a617393735a95a0bb67b08385bc1e7c66d"
- MESSAGE = "Empty commit"
- AUTHOR_FULL_NAME = "Rémy Coutable"
- FILES = []
+ ID = "b0e52af38d7ea43cf41d8a6f2471351ac036d6c9".freeze
+ PARENT_ID = "40f4a7a617393735a95a0bb67b08385bc1e7c66d".freeze
+ MESSAGE = "Empty commit".freeze
+ AUTHOR_FULL_NAME = "Rémy Coutable".freeze
+ FILES = [].freeze
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"]
+ ID = "40f4a7a617393735a95a0bb67b08385bc1e7c66d".freeze
+ PARENT_ID = "66028349a123e695b589e09a36634d976edcc5e8".freeze
+ MESSAGE = "Add ISO-8859-encoded file".freeze
+ AUTHOR_FULL_NAME = "Stan Hu".freeze
+ FILES = ["encoding/iso8859.txt"].freeze
FILES_COUNT = FILES.count
end
module FirstCommit
- ID = "1a0b36b3cdad1d2ee32457c102a8c0b7056fa863"
+ ID = "1a0b36b3cdad1d2ee32457c102a8c0b7056fa863".freeze
PARENT_ID = nil
- MESSAGE = "Initial commit"
- AUTHOR_FULL_NAME = "Dmitriy Zaporozhets"
- FILES = ["LICENSE", ".gitignore", "README.md"]
+ MESSAGE = "Initial commit".freeze
+ AUTHOR_FULL_NAME = "Dmitriy Zaporozhets".freeze
+ FILES = ["LICENSE", ".gitignore", "README.md"].freeze
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"]
+ ID = "4b4918a572fa86f9771e5ba40fbd48e1eb03e2c6".freeze
+ PARENT_ID = "0e1b353b348f8477bdbec1ef47087171c5032cd9".freeze
+ MESSAGE = "Merge branch 'master' into 'master'".freeze
+ AUTHOR_FULL_NAME = "Stan Hu".freeze
+ FILES = ["bin/executable"].freeze
FILES_COUNT = FILES.count
end
module Repo
- HEAD = "master"
+ HEAD = "master".freeze
BRANCHES = %w[
feature
fix
@@ -93,14 +93,14 @@ module SeedRepo
gitattributes-updated
master
merge-test
- ]
- TAGS = %w[v1.0.0 v1.1.0 v1.2.0 v1.2.1]
+ ].freeze
+ TAGS = %w[v1.0.0 v1.1.0 v1.2.0 v1.2.1].freeze
end
module RubyBlob
- ID = "7e3e39ebb9b2bf433b4ad17313770fbe4051649c"
- NAME = "popen.rb"
- CONTENT = <<-eos
+ ID = "7e3e39ebb9b2bf433b4ad17313770fbe4051649c".freeze
+ NAME = "popen.rb".freeze
+ CONTENT = <<-eos.freeze
require 'fileutils'
require 'open3'
diff --git a/spec/support/select2_helper.rb b/spec/support/select2_helper.rb
index d30cc8ff9f2..0d526045012 100644
--- a/spec/support/select2_helper.rb
+++ b/spec/support/select2_helper.rb
@@ -12,7 +12,7 @@
module Select2Helper
def select2(value, options = {})
- raise ArgumentError, 'options must be a Hash' unless options.kind_of?(Hash)
+ raise ArgumentError, 'options must be a Hash' unless options.is_a?(Hash)
selector = options.fetch(:from)
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 c64574679b6..81d06dc2a3d 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,7 +11,7 @@ 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 ] }
+ before { project.team << [assignee, :master] }
context 'with labels in command only' do
let(:example_params) do
diff --git a/spec/support/stub_gitlab_calls.rb b/spec/support/stub_gitlab_calls.rb
index 93f96cacc00..a01ef576234 100644
--- a/spec/support/stub_gitlab_calls.rb
+++ b/spec/support/stub_gitlab_calls.rb
@@ -35,7 +35,7 @@ module StubGitlabCalls
{ "tags" => tags }
)
allow_any_instance_of(ContainerRegistry::Client).to receive(:repository_manifest).and_return(
- JSON.load(File.read(Rails.root + 'spec/fixtures/container_registry/tag_manifest.json'))
+ JSON.parse(File.read(Rails.root + 'spec/fixtures/container_registry/tag_manifest.json'))
)
allow_any_instance_of(ContainerRegistry::Client).to receive(:blob).and_return(
File.read(Rails.root + 'spec/fixtures/container_registry/config_blob.json')
diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb
index b87232a350b..c3aa3ef44c2 100644
--- a/spec/support/test_env.rb
+++ b/spec/support/test_env.rb
@@ -38,7 +38,7 @@ module TestEnv
'deleted-image-test' => '6c17798',
'wip' => 'b9238ee',
'csv' => '3dd0896'
- }
+ }.freeze
# gitlab-test-fork is a fork of gitlab-fork, but we don't necessarily
# need to keep all the branches in sync.
@@ -48,7 +48,7 @@ module TestEnv
'master' => '5937ac0',
'remove-submodule' => '2a33e0c',
'conflict-resolvable-fork' => '404fa3f'
- }
+ }.freeze
# Test environment
#
@@ -135,7 +135,7 @@ module TestEnv
def copy_repo(project)
base_repo_path = File.expand_path(factory_repo_path_bare)
- target_repo_path = File.expand_path(project.repository_storage_path + "/#{project.namespace.path}/#{project.path}.git")
+ target_repo_path = File.expand_path(project.repository_storage_path + "/#{project.full_path}.git")
FileUtils.mkdir_p(target_repo_path)
FileUtils.cp_r("#{base_repo_path}/.", target_repo_path)
FileUtils.chmod_R 0755, target_repo_path
@@ -152,7 +152,7 @@ module TestEnv
def copy_forked_repo_with_submodules(project)
base_repo_path = File.expand_path(forked_repo_path_bare)
- target_repo_path = File.expand_path(project.repository_storage_path + "/#{project.namespace.path}/#{project.path}.git")
+ target_repo_path = File.expand_path(project.repository_storage_path + "/#{project.full_path}.git")
FileUtils.mkdir_p(target_repo_path)
FileUtils.cp_r("#{base_repo_path}/.", target_repo_path)
FileUtils.chmod_R 0755, target_repo_path
diff --git a/spec/support/update_invalid_issuable.rb b/spec/support/update_invalid_issuable.rb
new file mode 100644
index 00000000000..365c34448ac
--- /dev/null
+++ b/spec/support/update_invalid_issuable.rb
@@ -0,0 +1,57 @@
+shared_examples 'update invalid issuable' do |klass|
+ let(:params) do
+ {
+ namespace_id: project.namespace.path,
+ project_id: project.path,
+ id: issuable.iid
+ }
+ end
+
+ let(:issuable) do
+ klass == Issue ? issue : merge_request
+ end
+
+ before do
+ if klass == Issue
+ params.merge!(issue: { title: "any" })
+ else
+ params.merge!(merge_request: { title: "any" })
+ end
+ end
+
+ context 'when updating causes conflicts' do
+ before do
+ allow_any_instance_of(issuable.class).to receive(:save).
+ and_raise(ActiveRecord::StaleObjectError.new(issuable, :save))
+ end
+
+ it 'renders edit when format is html' do
+ put :update, params
+
+ expect(response).to render_template(:edit)
+ expect(assigns[:conflict]).to be_truthy
+ end
+
+ it 'renders json error message when format is json' do
+ params[:format] = "json"
+
+ put :update, params
+
+ expect(response.status).to eq(409)
+ expect(JSON.parse(response.body)).to have_key('errors')
+ end
+ end
+
+ context 'when updating an invalid issuable' do
+ before do
+ key = klass == Issue ? :issue : :merge_request
+ params[key][:title] = ""
+ end
+
+ it 'renders edit when merge request is invalid' do
+ put :update, params
+
+ expect(response).to render_template(:edit)
+ end
+ end
+end
diff --git a/spec/uploaders/attachment_uploader_spec.rb b/spec/uploaders/attachment_uploader_spec.rb
index 6098be5cd45..ea714fb08f0 100644
--- a/spec/uploaders/attachment_uploader_spec.rb
+++ b/spec/uploaders/attachment_uploader_spec.rb
@@ -1,18 +1,17 @@
require 'spec_helper'
describe AttachmentUploader do
- let(:issue) { build(:issue) }
- subject { described_class.new(issue) }
+ let(:uploader) { described_class.new(build_stubbed(:user)) }
describe '#move_to_cache' do
it 'is true' do
- expect(subject.move_to_cache).to eq(true)
+ expect(uploader.move_to_cache).to eq(true)
end
end
describe '#move_to_store' do
it 'is true' do
- expect(subject.move_to_store).to eq(true)
+ expect(uploader.move_to_store).to eq(true)
end
end
end
diff --git a/spec/uploaders/avatar_uploader_spec.rb b/spec/uploaders/avatar_uploader_spec.rb
index 76f5a4b42ed..c4d558805ab 100644
--- a/spec/uploaders/avatar_uploader_spec.rb
+++ b/spec/uploaders/avatar_uploader_spec.rb
@@ -1,18 +1,17 @@
require 'spec_helper'
describe AvatarUploader do
- let(:user) { build(:user) }
- subject { described_class.new(user) }
+ let(:uploader) { described_class.new(build_stubbed(:user)) }
describe '#move_to_cache' do
it 'is false' do
- expect(subject.move_to_cache).to eq(false)
+ expect(uploader.move_to_cache).to eq(false)
end
end
describe '#move_to_store' do
it 'is false' do
- expect(subject.move_to_store).to eq(false)
+ expect(uploader.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 6a712e33c96..b0f5be55c33 100644
--- a/spec/uploaders/file_uploader_spec.rb
+++ b/spec/uploaders/file_uploader_spec.rb
@@ -1,57 +1,35 @@
require 'spec_helper'
describe FileUploader do
- let(:project) { create(:project) }
+ let(:uploader) { described_class.new(build_stubbed(:project)) }
- before do
- @previous_enable_processing = FileUploader.enable_processing
- FileUploader.enable_processing = false
- @uploader = FileUploader.new(project)
- end
-
- after do
- FileUploader.enable_processing = @previous_enable_processing
- @uploader.remove!
- end
+ describe 'initialize' do
+ it 'generates a secret if none is provided' do
+ expect(SecureRandom).to receive(:hex).and_return('secret')
- describe '#image_or_video?' do
- context 'given an image file' do
- before do
- @uploader.store!(fixture_file_upload(Rails.root.join('spec', 'fixtures', 'rails_sample.jpg')))
- end
+ uploader = described_class.new(double)
- it 'detects an image based on file extension' do
- expect(@uploader.image_or_video?).to be true
- end
+ expect(uploader.secret).to eq 'secret'
end
- context 'given an video file' do
- before do
- video_file = fixture_file_upload(Rails.root.join('spec', 'fixtures', 'video_sample.mp4'))
- @uploader.store!(video_file)
- end
-
- it 'detects a video based on file extension' do
- expect(@uploader.image_or_video?).to be true
- end
- end
+ it 'accepts a secret parameter' do
+ expect(SecureRandom).not_to receive(:hex)
- it 'does not return image_or_video? for other types' do
- @uploader.store!(fixture_file_upload(Rails.root.join('spec', 'fixtures', 'doc_sample.txt')))
+ uploader = described_class.new(double, 'secret')
- expect(@uploader.image_or_video?).to be false
+ expect(uploader.secret).to eq 'secret'
end
end
describe '#move_to_cache' do
it 'is true' do
- expect(@uploader.move_to_cache).to eq(true)
+ expect(uploader.move_to_cache).to eq(true)
end
end
describe '#move_to_store' do
it 'is true' do
- expect(@uploader.move_to_store).to eq(true)
+ expect(uploader.move_to_store).to eq(true)
end
end
end
diff --git a/spec/uploaders/uploader_helper_spec.rb b/spec/uploaders/uploader_helper_spec.rb
new file mode 100644
index 00000000000..e9efd13b9aa
--- /dev/null
+++ b/spec/uploaders/uploader_helper_spec.rb
@@ -0,0 +1,35 @@
+require 'rails_helper'
+
+describe UploaderHelper do
+ class ExampleUploader < CarrierWave::Uploader::Base
+ include UploaderHelper
+
+ storage :file
+ end
+
+ def upload_fixture(filename)
+ fixture_file_upload(Rails.root.join('spec', 'fixtures', filename))
+ end
+
+ describe '#image_or_video?' do
+ let(:uploader) { ExampleUploader.new }
+
+ it 'returns true for an image file' do
+ uploader.store!(upload_fixture('dk.png'))
+
+ expect(uploader).to be_image_or_video
+ end
+
+ it 'it returns true for a video file' do
+ uploader.store!(upload_fixture('video_sample.mp4'))
+
+ expect(uploader).to be_image_or_video
+ end
+
+ it 'returns false for other extensions' do
+ uploader.store!(upload_fixture('doc_sample.txt'))
+
+ expect(uploader).not_to be_image_or_video
+ end
+ end
+end
diff --git a/spec/views/projects/builds/show.html.haml_spec.rb b/spec/views/projects/builds/show.html.haml_spec.rb
index b6f6e7b7a2b..ec78ac30593 100644
--- a/spec/views/projects/builds/show.html.haml_spec.rb
+++ b/spec/views/projects/builds/show.html.haml_spec.rb
@@ -209,6 +209,10 @@ describe 'projects/builds/show', :view do
it 'does not show retry button' do
expect(rendered).not_to have_link('Retry')
end
+
+ it 'does not show New issue button' do
+ expect(rendered).not_to have_link('New issue')
+ end
end
context 'when job is not running' do
@@ -220,6 +224,23 @@ describe 'projects/builds/show', :view do
it 'shows retry button' do
expect(rendered).to have_link('Retry')
end
+
+ context 'if build passed' do
+ it 'does not show New issue button' do
+ expect(rendered).not_to have_link('New issue')
+ end
+ end
+
+ context 'if build failed' do
+ before do
+ build.status = 'failed'
+ render
+ end
+
+ it 'shows New issue button' do
+ expect(rendered).to have_link('New issue')
+ end
+ end
end
describe 'commit title in sidebar' do
@@ -248,4 +269,25 @@ describe 'projects/builds/show', :view do
expect(rendered).to have_css('.js-build-value', visible: false, text: 'TRIGGER_VALUE_2')
end
end
+
+ describe 'New issue button' do
+ before do
+ build.status = 'failed'
+ render
+ end
+
+ it 'links to issues/new with the title and description filled in' do
+ title = "Build Failed ##{build.id}"
+ build_url = namespace_project_build_url(project.namespace, project, build)
+ href = new_namespace_project_issue_path(
+ project.namespace,
+ project,
+ issue: {
+ title: title,
+ description: build_url
+ }
+ )
+ expect(rendered).to have_link('New issue', href: href)
+ end
+ end
end
diff --git a/spec/workers/git_garbage_collect_worker_spec.rb b/spec/workers/git_garbage_collect_worker_spec.rb
index 5ef8cf1105b..a60af574a08 100644
--- a/spec/workers/git_garbage_collect_worker_spec.rb
+++ b/spec/workers/git_garbage_collect_worker_spec.rb
@@ -102,8 +102,8 @@ describe GitGarbageCollectWorker do
new_commit_sha = Rugged::Commit.create(
rugged,
message: "hello world #{SecureRandom.hex(6)}",
- author: Gitlab::Git::committer_hash(email: 'foo@bar', name: 'baz'),
- committer: Gitlab::Git::committer_hash(email: 'foo@bar', name: 'baz'),
+ author: Gitlab::Git.committer_hash(email: 'foo@bar', name: 'baz'),
+ committer: Gitlab::Git.committer_hash(email: 'foo@bar', name: 'baz'),
tree: old_commit.tree,
parents: [old_commit],
)
diff --git a/spec/workers/repository_fork_worker_spec.rb b/spec/workers/repository_fork_worker_spec.rb
index 60605460adb..87521ae408e 100644
--- a/spec/workers/repository_fork_worker_spec.rb
+++ b/spec/workers/repository_fork_worker_spec.rb
@@ -15,24 +15,24 @@ describe RepositoryForkWorker do
it "creates a new repository from a fork" do
expect(shell).to receive(:fork_repository).with(
'/test/path',
- project.path_with_namespace,
+ project.full_path,
project.repository_storage_path,
- fork_project.namespace.path
+ fork_project.namespace.full_path
).and_return(true)
subject.perform(
project.id,
'/test/path',
- project.path_with_namespace,
- fork_project.namespace.path)
+ project.full_path,
+ fork_project.namespace.full_path)
end
it 'flushes various caches' do
expect(shell).to receive(:fork_repository).with(
'/test/path',
- project.path_with_namespace,
+ project.full_path,
project.repository_storage_path,
- fork_project.namespace.path
+ fork_project.namespace.full_path
).and_return(true)
expect_any_instance_of(Repository).to receive(:expire_emptiness_caches).
@@ -41,8 +41,8 @@ describe RepositoryForkWorker do
expect_any_instance_of(Repository).to receive(:expire_exists_cache).
and_call_original
- subject.perform(project.id, '/test/path', project.path_with_namespace,
- fork_project.namespace.path)
+ subject.perform(project.id, '/test/path', project.full_path,
+ fork_project.namespace.full_path)
end
it "handles bad fork" do
@@ -53,8 +53,8 @@ describe RepositoryForkWorker do
subject.perform(
project.id,
'/test/path',
- project.path_with_namespace,
- fork_project.namespace.path)
+ project.full_path,
+ fork_project.namespace.full_path)
end
end
end
diff --git a/spec/workers/repository_import_worker_spec.rb b/spec/workers/repository_import_worker_spec.rb
index f1b1574abf4..c42f3147b7a 100644
--- a/spec/workers/repository_import_worker_spec.rb
+++ b/spec/workers/repository_import_worker_spec.rb
@@ -20,7 +20,7 @@ describe RepositoryImportWorker do
context 'when the import has failed' do
it 'hide the credentials that were used in the import URL' do
- error = %Q{remote: Not Found fatal: repository 'https://user:pass@test.com/root/repoC.git/' not found }
+ error = %q{remote: Not Found fatal: repository 'https://user:pass@test.com/root/repoC.git/' not found }
expect_any_instance_of(Projects::ImportService).to receive(:execute).
and_return({ status: :error, message: error })
diff --git a/yarn.lock b/yarn.lock
index 99db6f61bcd..cb4ef36119f 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -33,6 +33,10 @@ acorn@^3.0.4:
version "3.3.0"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a"
+acorn@^4.0.11:
+ version "4.0.11"
+ resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.11.tgz#edcda3bd937e7556410d42ed5860f67399c794c0"
+
after@0.8.2:
version "0.8.2"
resolved "https://registry.yarnpkg.com/after/-/after-0.8.2.tgz#fedb394f9f0e02aa9768e702bda23b505fae7e1f"
@@ -852,7 +856,7 @@ boom@2.x.x:
dependencies:
hoek "2.x.x"
-bootstrap-sass@3.3.6:
+bootstrap-sass@^3.3.6:
version "3.3.6"
resolved "https://registry.yarnpkg.com/bootstrap-sass/-/bootstrap-sass-3.3.6.tgz#363b0d300e868d3e70134c1a742bb17288444fd1"
@@ -1265,7 +1269,7 @@ custom-event@~1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/custom-event/-/custom-event-1.0.1.tgz#5d02a46850adf1b4a317946a3928fccb5bfd0425"
-d3@3.5.11:
+d3@^3.5.11:
version "3.5.11"
resolved "https://registry.yarnpkg.com/d3/-/d3-3.5.11.tgz#d130750eed0554db70e8432102f920a12407b69c"
@@ -1404,10 +1408,14 @@ domain-browser@^1.1.1:
version "1.1.7"
resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.1.7.tgz#867aa4b093faa05f1de08c06f4d7b21fdf8698bc"
-dropzone@4.2.0:
+dropzone@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/dropzone/-/dropzone-4.2.0.tgz#fbe7acbb9918e0706489072ef663effeef8a79f3"
+duplexer@^0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.1.tgz#ace6ff808c1ce66b57d1ebf97977acb02334cfc1"
+
ecc-jsbn@~0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz#0fc73a9ed5f0d53c38193398523ef7e543777505"
@@ -1418,6 +1426,10 @@ ee-first@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
+ejs@^2.5.5:
+ version "2.5.6"
+ resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.5.6.tgz#479636bfa3fe3b1debd52087f0acb204b4f19c88"
+
elliptic@^6.0.0:
version "6.3.3"
resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.3.3.tgz#5482d9646d54bcb89fd7d994fc9e2e9568876e3f"
@@ -1792,7 +1804,7 @@ expand-range@^1.8.1:
dependencies:
fill-range "^2.1.0"
-express@^4.13.3:
+express@^4.13.3, express@^4.14.1:
version "4.14.1"
resolved "https://registry.yarnpkg.com/express/-/express-4.14.1.tgz#646c237f766f148c2120aff073817b9e4d7e0d33"
dependencies:
@@ -1893,6 +1905,10 @@ fileset@^2.0.2:
glob "^7.0.3"
minimatch "^3.0.3"
+filesize@^3.5.4:
+ version "3.5.4"
+ resolved "https://registry.yarnpkg.com/filesize/-/filesize-3.5.4.tgz#742fc7fb6aef4ee3878682600c22f840731e1fda"
+
fill-range@^2.1.0:
version "2.2.3"
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-2.2.3.tgz#50b77dfd7e469bc7492470963699fe7a8485a723"
@@ -2118,6 +2134,12 @@ graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9:
version "1.0.1"
resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725"
+gzip-size@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-3.0.0.tgz#546188e9bdc337f673772f81660464b389dce520"
+ dependencies:
+ duplexer "^0.1.1"
+
handle-thing@^1.2.4:
version "1.2.5"
resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-1.2.5.tgz#fd7aad726bf1a5fd16dfc29b2f7a6601d27139c4"
@@ -2273,13 +2295,6 @@ ignore@^3.2.0:
version "3.2.2"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.2.2.tgz#1c51e1ef53bab6ddc15db4d9ac4ec139eceb3410"
-imports-loader@^0.6.5:
- version "0.6.5"
- resolved "https://registry.yarnpkg.com/imports-loader/-/imports-loader-0.6.5.tgz#ae74653031d59e37b3c2fb2544ac61aeae3530a6"
- dependencies:
- loader-utils "0.2.x"
- source-map "0.1.x"
-
imurmurhash@^0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
@@ -2624,17 +2639,17 @@ jodid25519@^1.0.0:
dependencies:
jsbn "~0.1.0"
-"jquery-ui@github:jquery/jquery-ui#1.11.4":
+"jquery-ui@git+https://github.com/jquery/jquery-ui#1.11.4":
version "1.11.4"
- resolved "https://codeload.github.com/jquery/jquery-ui/tar.gz/d6713024e16de90ea71dc0544ba34e1df01b4d8a"
+ resolved "git+https://github.com/jquery/jquery-ui#d6713024e16de90ea71dc0544ba34e1df01b4d8a"
-jquery-ujs@1.2.1:
+jquery-ujs@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/jquery-ujs/-/jquery-ujs-1.2.1.tgz#6ee75b1ef4e9ac95e7124f8d71f7d351f5548e92"
dependencies:
jquery ">=1.8.0"
-jquery@2.2.1, jquery@>=1.8.0:
+jquery@>=1.8.0, jquery@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/jquery/-/jquery-2.2.1.tgz#3c3e16854ad3d2ac44ac65021b17426d22ad803f"
@@ -2831,7 +2846,7 @@ loader-runner@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.3.0.tgz#f482aea82d543e07921700d5a46ef26fdac6b8a2"
-loader-utils@0.2.x, loader-utils@^0.2.11, loader-utils@^0.2.16, loader-utils@^0.2.5:
+loader-utils@^0.2.11, loader-utils@^0.2.16, loader-utils@^0.2.5:
version "0.2.16"
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-0.2.16.tgz#f08632066ed8282835dff88dfb52704765adee6d"
dependencies:
@@ -2910,7 +2925,7 @@ lodash@^3.8.0:
version "3.10.1"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6"
-lodash@^4.0.0, lodash@^4.0.1, lodash@^4.14.0, lodash@^4.17.2, lodash@^4.2.0, lodash@^4.3.0, lodash@^4.5.0:
+lodash@^4.0.0, lodash@^4.0.1, lodash@^4.14.0, lodash@^4.17.2, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.3.0, lodash@^4.5.0:
version "4.17.4"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae"
@@ -3031,7 +3046,7 @@ moment@2.x:
version "2.17.1"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.17.1.tgz#fed9506063f36b10f066c8b59a144d7faebe1d82"
-mousetrap@1.4.6:
+mousetrap@^1.4.6:
version "1.4.6"
resolved "https://registry.yarnpkg.com/mousetrap/-/mousetrap-1.4.6.tgz#eaca72e22e56d5b769b7555873b688c3332e390a"
@@ -3223,6 +3238,10 @@ onetime@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/onetime/-/onetime-1.1.0.tgz#a1f7838f8314c516f05ecefcbc4ccfe04b4ed789"
+opener@^1.4.2:
+ version "1.4.3"
+ resolved "https://registry.yarnpkg.com/opener/-/opener-1.4.3.tgz#5c6da2c5d7e5831e8ffa3964950f8d6674ac90b8"
+
opn@4.0.2:
version "4.0.2"
resolved "https://registry.yarnpkg.com/opn/-/opn-4.0.2.tgz#7abc22e644dff63b0a96d5ab7f2790c0f01abc95"
@@ -3971,7 +3990,7 @@ source-map-support@^0.4.2:
dependencies:
source-map "^0.5.3"
-source-map@0.1.x, source-map@^0.1.41:
+source-map@^0.1.41:
version "0.1.43"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.1.43.tgz#c24bc146ca517c1471f5dacbe2571b2b7f9e3346"
dependencies:
@@ -4296,7 +4315,7 @@ unc-path-regex@^0.1.0:
version "0.1.2"
resolved "https://registry.yarnpkg.com/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa"
-underscore@1.8.3:
+underscore@^1.8.3:
version "1.8.3"
resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.8.3.tgz#4f3fb53b106e6097fcf9cb4109f2a5e9bdfa5022"
@@ -4387,13 +4406,13 @@ void-elements@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec"
-vue-resource@0.9.3:
+vue-resource@^0.9.3:
version "0.9.3"
resolved "https://registry.yarnpkg.com/vue-resource/-/vue-resource-0.9.3.tgz#ab46e1c44ea219142dcc28ae4043b3b04c80959d"
-vue@2.0.3:
- version "2.0.3"
- resolved "https://registry.yarnpkg.com/vue/-/vue-2.0.3.tgz#3f7698f83d6ad1f0e35955447901672876c63fde"
+vue@^2.1.10:
+ version "2.1.10"
+ resolved "https://registry.yarnpkg.com/vue/-/vue-2.1.10.tgz#c9235ca48c7925137be5807832ac4e3ac180427b"
watchpack@^1.2.0:
version "1.2.1"
@@ -4409,6 +4428,21 @@ wbuf@^1.1.0, wbuf@^1.4.0:
dependencies:
minimalistic-assert "^1.0.0"
+webpack-bundle-analyzer@^2.3.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-2.3.0.tgz#0d05e96a43033f7cc57f6855b725782ba61e93a4"
+ dependencies:
+ acorn "^4.0.11"
+ chalk "^1.1.3"
+ commander "^2.9.0"
+ ejs "^2.5.5"
+ express "^4.14.1"
+ filesize "^3.5.4"
+ gzip-size "^3.0.0"
+ lodash "^4.17.4"
+ mkdirp "^0.5.1"
+ opener "^1.4.2"
+
webpack-dev-middleware@^1.0.11, webpack-dev-middleware@^1.9.0:
version "1.10.0"
resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-1.10.0.tgz#7d5be2651e692fddfafd8aaed177c16ff51f0eb8"