summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore2
-rw-r--r--.gitlab-ci.yml241
-rw-r--r--.gitlab/issue_templates/Bug.md2
-rw-r--r--.rubocop.yml18
-rw-r--r--.rubocop_todo.yml16
-rw-r--r--CHANGELOG.md12
-rw-r--r--Gemfile2
-rw-r--r--Gemfile.lock4
-rw-r--r--app/assets/javascripts/blob/balsamiq/balsamiq_viewer.js32
-rw-r--r--app/assets/javascripts/blob/balsamiq_viewer.js24
-rw-r--r--app/assets/javascripts/blob/viewer/index.js101
-rw-r--r--app/assets/javascripts/boards/components/board_sidebar.js21
-rw-r--r--app/assets/javascripts/boards/components/issue_card_inner.js27
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_table.js25
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_code_component.js11
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_issue_component.js12
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_plan_component.js15
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_production_component.js12
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_review_component.js12
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_staging_component.js13
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_test_component.js4
-rw-r--r--app/assets/javascripts/diff_notes/components/diff_note_avatars.js30
-rw-r--r--app/assets/javascripts/diff_notes/diff_notes_bundle.js2
-rw-r--r--app/assets/javascripts/dispatcher.js24
-rw-r--r--app/assets/javascripts/droplab/drop_down.js128
-rw-r--r--app/assets/javascripts/droplab/drop_lab.js126
-rw-r--r--app/assets/javascripts/droplab/hook.js29
-rw-r--r--app/assets/javascripts/droplab/hook_button.js59
-rw-r--r--app/assets/javascripts/droplab/hook_input.js68
-rw-r--r--app/assets/javascripts/dropzone_input.js252
-rw-r--r--app/assets/javascripts/environments/components/environment_item.vue50
-rw-r--r--app/assets/javascripts/environments/components/environment_monitoring.vue1
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_manager.js10
-rw-r--r--app/assets/javascripts/filtered_search/stores/recent_searches_store.js1
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js482
-rw-r--r--app/assets/javascripts/gl_dropdown.js5
-rw-r--r--app/assets/javascripts/gl_form.js15
-rw-r--r--app/assets/javascripts/issuable_context.js3
-rw-r--r--app/assets/javascripts/issuable_form.js6
-rw-r--r--app/assets/javascripts/issue_show/actions/tasks.js27
-rw-r--r--app/assets/javascripts/issue_show/components/app.vue96
-rw-r--r--app/assets/javascripts/issue_show/components/description.vue105
-rw-r--r--app/assets/javascripts/issue_show/components/title.vue53
-rw-r--r--app/assets/javascripts/issue_show/index.js50
-rw-r--r--app/assets/javascripts/issue_show/issue_title_description.vue180
-rw-r--r--app/assets/javascripts/issue_show/mixins/animate.js13
-rw-r--r--app/assets/javascripts/issue_show/services/index.js14
-rw-r--r--app/assets/javascripts/issue_show/stores/index.js25
-rw-r--r--app/assets/javascripts/layout_nav.js10
-rw-r--r--app/assets/javascripts/lib/utils/ajax_cache.js70
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js5
-rw-r--r--app/assets/javascripts/lib/utils/type_utility.js17
-rw-r--r--app/assets/javascripts/main.js2
-rw-r--r--app/assets/javascripts/merge_request_tabs.js32
-rw-r--r--app/assets/javascripts/merge_request_widget.js303
-rw-r--r--app/assets/javascripts/milestone_select.js37
-rw-r--r--app/assets/javascripts/new_branch_form.js47
-rw-r--r--app/assets/javascripts/notes.js184
-rw-r--r--app/assets/javascripts/pipeline_schedules/components/interval_pattern_input.js118
-rw-r--r--app/assets/javascripts/pipeline_schedules/components/pipeline_schedules_callout.js12
-rw-r--r--app/assets/javascripts/pipeline_schedules/components/target_branch_dropdown.js16
-rw-r--r--app/assets/javascripts/pipeline_schedules/components/timezone_dropdown.js18
-rw-r--r--app/assets/javascripts/pipeline_schedules/pipeline_schedules_index_bundle.js15
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_url.js24
-rw-r--r--app/assets/javascripts/pipelines/components/stage.js104
-rw-r--r--app/assets/javascripts/pipelines/components/status.js60
-rw-r--r--app/assets/javascripts/pipelines/pipelines.js27
-rw-r--r--app/assets/javascripts/ref_select_dropdown.js46
-rw-r--r--app/assets/javascripts/test.js1
-rw-r--r--app/assets/javascripts/todos.js3
-rw-r--r--app/assets/javascripts/users_select.js1202
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js6
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js60
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.js28
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.js28
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_locked.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_nothing_to_merge.js47
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_sha_mismatch.js16
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/dependencies.js1
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js1
-rw-r--r--app/assets/javascripts/vue_shared/ci_action_icons.js31
-rw-r--r--app/assets/javascripts/vue_shared/ci_status_icons.js12
-rw-r--r--app/assets/javascripts/vue_shared/components/ci_badge_link.vue52
-rw-r--r--app/assets/javascripts/vue_shared/components/ci_icon.vue25
-rw-r--r--app/assets/javascripts/vue_shared/components/commit.js24
-rw-r--r--app/assets/javascripts/vue_shared/components/pipeline_status_icon.js23
-rw-r--r--app/assets/javascripts/vue_shared/components/pipelines_table_row.js30
-rw-r--r--app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue80
-rw-r--r--app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue80
-rw-r--r--app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_svg.vue45
-rw-r--r--app/assets/javascripts/vue_shared/pipeline_svg_icons.js43
-rw-r--r--app/assets/stylesheets/framework/avatar.scss2
-rw-r--r--app/assets/stylesheets/framework/blocks.scss21
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss2
-rw-r--r--app/assets/stylesheets/framework/filters.scss25
-rw-r--r--app/assets/stylesheets/framework/gfm.scss4
-rw-r--r--app/assets/stylesheets/framework/header.scss18
-rw-r--r--app/assets/stylesheets/framework/icons.scss4
-rw-r--r--app/assets/stylesheets/framework/layout.scss4
-rw-r--r--app/assets/stylesheets/framework/mobile.scss2
-rw-r--r--app/assets/stylesheets/framework/nav.scss27
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss3
-rw-r--r--app/assets/stylesheets/framework/timeline.scss63
-rw-r--r--app/assets/stylesheets/framework/typography.scss44
-rw-r--r--app/assets/stylesheets/framework/wells.scss6
-rw-r--r--app/assets/stylesheets/highlight/dark.scss5
-rw-r--r--app/assets/stylesheets/highlight/monokai.scss5
-rw-r--r--app/assets/stylesheets/highlight/solarized_dark.scss5
-rw-r--r--app/assets/stylesheets/highlight/solarized_light.scss5
-rw-r--r--app/assets/stylesheets/highlight/white.scss5
-rw-r--r--app/assets/stylesheets/pages/builds.scss2
-rw-r--r--app/assets/stylesheets/pages/commits.scss10
-rw-r--r--app/assets/stylesheets/pages/cycle_analytics.scss4
-rw-r--r--app/assets/stylesheets/pages/diff.scss5
-rw-r--r--app/assets/stylesheets/pages/environments.scss8
-rw-r--r--app/assets/stylesheets/pages/issuable.scss31
-rw-r--r--app/assets/stylesheets/pages/issues.scss10
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss130
-rw-r--r--app/assets/stylesheets/pages/note_form.scss43
-rw-r--r--app/assets/stylesheets/pages/notes.scss20
-rw-r--r--app/assets/stylesheets/pages/pipeline_schedules.scss21
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss15
-rw-r--r--app/assets/stylesheets/pages/projects.scss64
-rw-r--r--app/assets/stylesheets/print.scss8
-rw-r--r--app/assets/stylesheets/test.scss17
-rw-r--r--app/controllers/admin/hooks_controller.rb1
-rw-r--r--app/controllers/autocomplete_controller.rb2
-rw-r--r--app/controllers/concerns/issuable_actions.rb13
-rw-r--r--app/controllers/concerns/lfs_request.rb6
-rw-r--r--app/controllers/concerns/renders_blob.rb7
-rw-r--r--app/controllers/concerns/routable_actions.rb16
-rw-r--r--app/controllers/dashboard/snippets_controller.rb7
-rw-r--r--app/controllers/explore/groups_controller.rb2
-rw-r--r--app/controllers/explore/snippets_controller.rb2
-rw-r--r--app/controllers/groups/application_controller.rb6
-rw-r--r--app/controllers/groups_controller.rb10
-rw-r--r--app/controllers/health_controller.rb2
-rw-r--r--app/controllers/jwt_controller.rb2
-rw-r--r--app/controllers/profiles/preferences_controller.rb2
-rw-r--r--app/controllers/projects/application_controller.rb7
-rw-r--r--app/controllers/projects/blob_controller.rb2
-rw-r--r--app/controllers/projects/deployments_controller.rb8
-rw-r--r--app/controllers/projects/issues_controller.rb11
-rw-r--r--app/controllers/projects/lfs_api_controller.rb4
-rw-r--r--[-rwxr-xr-x]app/controllers/projects/merge_requests_controller.rb18
-rw-r--r--app/controllers/projects/pipelines_controller.rb2
-rw-r--r--app/controllers/projects/snippets_controller.rb5
-rw-r--r--app/controllers/projects/tags_controller.rb2
-rw-r--r--app/controllers/projects/tree_controller.rb4
-rw-r--r--app/controllers/projects_controller.rb9
-rw-r--r--app/controllers/snippets_controller.rb26
-rw-r--r--app/controllers/uploads_controller.rb2
-rw-r--r--app/controllers/users_controller.rb11
-rw-r--r--app/finders/groups_finder.rb20
-rw-r--r--app/finders/notes_finder.rb2
-rw-r--r--app/finders/snippets_finder.rb102
-rw-r--r--app/finders/users_finder.rb74
-rw-r--r--app/helpers/application_helper.rb18
-rw-r--r--app/helpers/blob_helper.rb23
-rw-r--r--app/helpers/button_helper.rb5
-rw-r--r--app/helpers/commits_helper.rb23
-rw-r--r--app/helpers/diff_helper.rb4
-rw-r--r--app/helpers/emails_helper.rb2
-rw-r--r--app/helpers/events_helper.rb13
-rw-r--r--app/helpers/explore_helper.rb2
-rw-r--r--app/helpers/gitlab_routing_helper.rb4
-rw-r--r--app/helpers/icons_helper.rb7
-rw-r--r--app/helpers/issuables_helper.rb13
-rw-r--r--app/helpers/markup_helper.rb17
-rw-r--r--app/helpers/merge_requests_helper.rb2
-rw-r--r--app/helpers/notes_helper.rb4
-rw-r--r--app/helpers/projects_helper.rb17
-rw-r--r--app/helpers/search_helper.rb6
-rw-r--r--app/helpers/selects_helper.rb2
-rw-r--r--app/helpers/sorting_helper.rb2
-rw-r--r--app/helpers/submodule_helper.rb52
-rw-r--r--app/helpers/todos_helper.rb13
-rw-r--r--app/models/application_setting.rb10
-rw-r--r--app/models/blob.rb52
-rw-r--r--app/models/blob_viewer/auxiliary.rb18
-rw-r--r--app/models/blob_viewer/base.rb80
-rw-r--r--app/models/blob_viewer/cartfile.rb15
-rw-r--r--app/models/blob_viewer/changelog.rb16
-rw-r--r--app/models/blob_viewer/client_side.rb6
-rw-r--r--app/models/blob_viewer/composer_json.rb23
-rw-r--r--app/models/blob_viewer/contributing.rb10
-rw-r--r--app/models/blob_viewer/dependency_manager.rb43
-rw-r--r--app/models/blob_viewer/download.rb10
-rw-r--r--app/models/blob_viewer/gemfile.rb15
-rw-r--r--app/models/blob_viewer/gemspec.rb27
-rw-r--r--app/models/blob_viewer/gitlab_ci_yml.rb23
-rw-r--r--app/models/blob_viewer/godeps_json.rb15
-rw-r--r--app/models/blob_viewer/license.rb20
-rw-r--r--app/models/blob_viewer/markup.rb1
-rw-r--r--app/models/blob_viewer/package_json.rb23
-rw-r--r--app/models/blob_viewer/podfile.rb15
-rw-r--r--app/models/blob_viewer/podspec.rb27
-rw-r--r--app/models/blob_viewer/podspec_json.rb9
-rw-r--r--app/models/blob_viewer/readme.rb14
-rw-r--r--app/models/blob_viewer/requirements_txt.rb15
-rw-r--r--app/models/blob_viewer/route_map.rb30
-rw-r--r--app/models/blob_viewer/server_side.rb25
-rw-r--r--app/models/blob_viewer/static.rb14
-rw-r--r--app/models/blob_viewer/text.rb4
-rw-r--r--app/models/blob_viewer/yarn_lock.rb15
-rw-r--r--app/models/ci/build.rb8
-rw-r--r--app/models/ci/pipeline_schedule.rb8
-rw-r--r--app/models/commit.rb19
-rw-r--r--app/models/commit_status.rb11
-rw-r--r--app/models/concerns/mentionable.rb13
-rw-r--r--app/models/concerns/protected_branch_access.rb22
-rw-r--r--app/models/deployment.rb9
-rw-r--r--app/models/diff_discussion.rb2
-rw-r--r--app/models/environment.rb4
-rw-r--r--app/models/hooks/project_hook.rb2
-rw-r--r--app/models/hooks/system_hook.rb5
-rw-r--r--app/models/hooks/web_hook.rb5
-rw-r--r--app/models/issue.rb4
-rw-r--r--app/models/issue_assignee.rb7
-rw-r--r--app/models/key.rb2
-rw-r--r--app/models/merge_request.rb38
-rw-r--r--app/models/namespace.rb4
-rw-r--r--app/models/project.rb4
-rw-r--r--app/models/project_services/bamboo_service.rb2
-rw-r--r--app/models/project_services/chat_message/pipeline_message.rb6
-rw-r--r--app/models/project_services/chat_message/push_message.rb4
-rw-r--r--app/models/project_services/chat_notification_service.rb2
-rw-r--r--app/models/project_services/emails_on_push_service.rb2
-rw-r--r--app/models/project_services/external_wiki_service.rb2
-rw-r--r--app/models/project_services/flowdock_service.rb2
-rw-r--r--app/models/project_services/hipchat_service.rb2
-rw-r--r--app/models/project_services/irker_service.rb2
-rw-r--r--app/models/project_services/jira_service.rb2
-rw-r--r--app/models/project_services/kubernetes_service.rb2
-rw-r--r--app/models/project_services/microsoft_teams_service.rb2
-rw-r--r--app/models/project_services/mock_ci_service.rb2
-rw-r--r--app/models/project_services/monitoring_service.rb7
-rw-r--r--app/models/project_services/pipelines_email_service.rb2
-rw-r--r--app/models/project_services/prometheus_service.rb36
-rw-r--r--app/models/project_services/pushover_service.rb2
-rw-r--r--app/models/project_services/teamcity_service.rb4
-rw-r--r--app/models/protected_branch/merge_access_level.rb10
-rw-r--r--app/models/protected_branch/push_access_level.rb18
-rw-r--r--app/models/readme_blob.rb13
-rw-r--r--app/models/repository.rb51
-rw-r--r--app/models/route.rb2
-rw-r--r--app/models/sent_notification.rb2
-rw-r--r--app/models/service.rb4
-rw-r--r--app/models/snippet.rb13
-rw-r--r--app/models/tree.rb5
-rw-r--r--app/models/user.rb24
-rw-r--r--app/policies/project_snippet_policy.rb2
-rw-r--r--app/presenters/merge_request_presenter.rb6
-rw-r--r--app/serializers/merge_request_entity.rb8
-rw-r--r--app/serializers/pipeline_entity.rb5
-rw-r--r--app/serializers/request_aware_entity.rb1
-rw-r--r--app/services/akismet_service.rb2
-rw-r--r--app/services/audit_event_service.rb2
-rw-r--r--app/services/boards/issues/move_service.rb2
-rw-r--r--app/services/ci/process_pipeline_service.rb20
-rw-r--r--app/services/ci/retry_build_service.rb13
-rw-r--r--app/services/ci/retry_pipeline_service.rb2
-rw-r--r--app/services/issuable/bulk_update_service.rb14
-rw-r--r--app/services/issuable_base_service.rb12
-rw-r--r--app/services/members/authorized_destroy_service.rb28
-rw-r--r--app/services/merge_requests/conflicts/base_service.rb11
-rw-r--r--app/services/merge_requests/conflicts/list_service.rb36
-rw-r--r--app/services/merge_requests/conflicts/resolve_service.rb53
-rw-r--r--app/services/merge_requests/resolve_service.rb65
-rw-r--r--app/services/notification_service.rb2
-rw-r--r--app/services/projects/update_pages_configuration_service.rb2
-rw-r--r--app/services/search/snippet_service.rb2
-rw-r--r--app/services/system_hooks_service.rb4
-rw-r--r--app/services/system_note_service.rb6
-rw-r--r--app/views/admin/application_settings/_form.html.haml21
-rw-r--r--app/views/admin/health_check/show.html.haml19
-rw-r--r--app/views/admin/hooks/_form.html.haml11
-rw-r--r--app/views/admin/hooks/index.html.haml2
-rw-r--r--app/views/devise/passwords/edit.html.haml4
-rw-r--r--app/views/devise/sessions/_new_base.html.haml2
-rw-r--r--app/views/discussions/_diff_with_notes.html.haml2
-rw-r--r--app/views/discussions/_discussion.html.haml2
-rw-r--r--app/views/events/_commit.html.haml2
-rw-r--r--app/views/import/base/create.js.haml2
-rw-r--r--app/views/import/fogbugz/new_user_map.html.haml3
-rw-r--r--app/views/layouts/_head.html.haml2
-rw-r--r--app/views/layouts/_init_auto_complete.html.haml3
-rw-r--r--app/views/layouts/_search.html.haml2
-rw-r--r--app/views/layouts/application.html.haml2
-rw-r--r--app/views/layouts/header/_default.html.haml1
-rw-r--r--app/views/layouts/nav/_profile.html.haml4
-rw-r--r--app/views/profiles/_event_table.html.haml3
-rw-r--r--app/views/profiles/audit_log.html.haml2
-rw-r--r--app/views/projects/_files.html.haml10
-rw-r--r--app/views/projects/_last_commit.html.haml12
-rw-r--r--app/views/projects/_last_push.html.haml2
-rw-r--r--app/views/projects/_readme.html.haml21
-rw-r--r--app/views/projects/_zen.html.haml3
-rw-r--r--app/views/projects/blame/show.html.haml2
-rw-r--r--app/views/projects/blob/_auxiliary_viewer.html.haml5
-rw-r--r--app/views/projects/blob/_blob.html.haml5
-rw-r--r--app/views/projects/blob/_viewer.html.haml11
-rw-r--r--app/views/projects/blob/viewers/_changelog.html.haml4
-rw-r--r--app/views/projects/blob/viewers/_contributing.html.haml9
-rw-r--r--app/views/projects/blob/viewers/_dependency_manager.html.haml11
-rw-r--r--app/views/projects/blob/viewers/_gitlab_ci_yml.html.haml9
-rw-r--r--app/views/projects/blob/viewers/_gitlab_ci_yml_loading.html.haml4
-rw-r--r--app/views/projects/blob/viewers/_license.html.haml8
-rw-r--r--app/views/projects/blob/viewers/_loading.html.haml2
-rw-r--r--app/views/projects/blob/viewers/_loading_auxiliary.html.haml2
-rw-r--r--app/views/projects/blob/viewers/_readme.html.haml4
-rw-r--r--app/views/projects/blob/viewers/_route_map.html.haml9
-rw-r--r--app/views/projects/blob/viewers/_route_map_loading.html.haml4
-rw-r--r--app/views/projects/boards/_show.html.haml6
-rw-r--r--app/views/projects/boards/components/sidebar/_assignee.html.haml3
-rw-r--r--app/views/projects/boards/components/sidebar/_milestone.html.haml3
-rw-r--r--app/views/projects/branches/_branch.html.haml3
-rw-r--r--app/views/projects/branches/_commit.html.haml2
-rw-r--r--app/views/projects/branches/new.html.haml12
-rw-r--r--app/views/projects/builds/_header.html.haml18
-rw-r--r--app/views/projects/builds/_sidebar.html.haml4
-rw-r--r--app/views/projects/ci/builds/_build.html.haml6
-rw-r--r--app/views/projects/commit/_commit_box.html.haml9
-rw-r--r--app/views/projects/commit/_pipeline.html.haml52
-rw-r--r--app/views/projects/commit/branches.html.haml28
-rw-r--r--app/views/projects/commits/_commit.html.haml2
-rw-r--r--app/views/projects/commits/_inline_commit.html.haml2
-rw-r--r--app/views/projects/compare/_form.html.haml4
-rw-r--r--app/views/projects/compare/index.html.haml6
-rw-r--r--app/views/projects/compare/show.html.haml4
-rw-r--r--app/views/projects/cycle_analytics/show.html.haml2
-rw-r--r--app/views/projects/deployments/_commit.html.haml4
-rw-r--r--app/views/projects/diffs/_file_header.html.haml6
-rw-r--r--app/views/projects/edit.html.haml6
-rw-r--r--app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml4
-rw-r--r--app/views/projects/imports/new.html.haml2
-rw-r--r--app/views/projects/issues/_new_branch.html.haml4
-rw-r--r--app/views/projects/issues/_related_branches.html.haml3
-rw-r--r--app/views/projects/issues/index.html.haml3
-rw-r--r--app/views/projects/issues/show.html.haml13
-rw-r--r--app/views/projects/merge_requests/_merge_request.html.haml2
-rw-r--r--app/views/projects/merge_requests/_new_compare.html.haml8
-rw-r--r--app/views/projects/merge_requests/_new_submit.html.haml4
-rw-r--r--app/views/projects/merge_requests/_show.html.haml73
-rw-r--r--app/views/projects/merge_requests/index.html.haml3
-rw-r--r--app/views/projects/merge_requests/show/_how_to_merge.html.haml2
-rw-r--r--app/views/projects/merge_requests/show/_versions.html.haml58
-rw-r--r--app/views/projects/new.html.haml2
-rw-r--r--app/views/projects/pipeline_schedules/_form.html.haml17
-rw-r--r--app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml3
-rw-r--r--app/views/projects/pipeline_schedules/index.html.haml6
-rw-r--r--app/views/projects/pipelines/_info.html.haml6
-rw-r--r--app/views/projects/pipelines/new.html.haml4
-rw-r--r--app/views/projects/protected_branches/_dropdown.html.haml4
-rw-r--r--app/views/projects/protected_branches/_matching_branch.html.haml5
-rw-r--r--app/views/projects/protected_branches/_protected_branch.html.haml5
-rw-r--r--app/views/projects/protected_branches/show.html.haml2
-rw-r--r--app/views/projects/protected_tags/_dropdown.html.haml4
-rw-r--r--app/views/projects/protected_tags/_matching_tag.html.haml5
-rw-r--r--app/views/projects/protected_tags/_protected_tag.html.haml5
-rw-r--r--app/views/projects/protected_tags/show.html.haml2
-rw-r--r--app/views/projects/runners/_runner.html.haml21
-rw-r--r--app/views/projects/runners/_specific_runners.html.haml2
-rw-r--r--app/views/projects/settings/integrations/_project_hook.html.haml2
-rw-r--r--app/views/projects/show.html.haml5
-rw-r--r--app/views/projects/tags/_tag.html.haml7
-rw-r--r--app/views/projects/tags/new.html.haml23
-rw-r--r--app/views/projects/tags/show.html.haml4
-rw-r--r--app/views/projects/tree/_readme.html.haml17
-rw-r--r--app/views/projects/tree/show.html.haml11
-rw-r--r--app/views/projects/wikis/_sidebar.html.haml2
-rw-r--r--app/views/projects/wikis/git_access.html.haml2
-rw-r--r--app/views/shared/_clone_panel.html.haml2
-rw-r--r--app/views/shared/_ref_dropdown.html.haml2
-rw-r--r--app/views/shared/_ref_switcher.html.haml4
-rw-r--r--app/views/shared/empty_states/_issues.html.haml5
-rw-r--r--app/views/shared/empty_states/_labels.html.haml4
-rw-r--r--app/views/shared/empty_states/_merge_requests.html.haml4
-rw-r--r--app/views/shared/empty_states/icons/_pipelines_empty.svg2
-rw-r--r--app/views/shared/icons/_icon_history.svg1
-rw-r--r--app/views/shared/icons/_mr_widget_empty_state.svg1
-rw-r--r--app/views/shared/issuable/_assignees.html.haml5
-rw-r--r--app/views/shared/issuable/_filter.html.haml1
-rw-r--r--app/views/shared/issuable/_milestone_dropdown.html.haml2
-rw-r--r--app/views/shared/issuable/_search_bar.html.haml5
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml4
-rw-r--r--app/views/shared/issuable/_sidebar_assignees.html.haml22
-rw-r--r--app/views/shared/issuable/form/_branch_chooser.html.haml4
-rw-r--r--app/views/shared/issuable/form/_issue_assignee.html.haml9
-rw-r--r--app/views/shared/milestones/_sidebar.html.haml2
-rw-r--r--app/views/shared/notes/_edit.html.haml2
-rw-r--r--app/views/shared/notes/_hints.html.haml27
-rw-r--r--app/views/shared/notes/_note.html.haml2
-rw-r--r--app/views/shared/notes/_notes_with_form.html.haml2
-rw-r--r--app/views/shared/notifications/_custom_notifications.html.haml6
-rw-r--r--app/views/shared/web_hooks/_form.html.haml6
-rw-r--r--app/views/users/show.html.haml8
-rw-r--r--app/workers/namespaceless_project_destroy_worker.rb43
-rw-r--r--app/workers/pipeline_schedule_worker.rb8
-rw-r--r--app/workers/post_receive.rb25
-rw-r--r--app/workers/repository_check/clear_worker.rb2
-rw-r--r--app/workers/repository_check/single_repository_worker.rb2
-rw-r--r--changelogs/unreleased/2247-emails-forwarded-to-service-desk-email-don-t-come.yml4
-rw-r--r--changelogs/unreleased/24373-warning-message-go-away.yml4
-rw-r--r--changelogs/unreleased/26325-system-hooks.yml4
-rw-r--r--changelogs/unreleased/30286-ci-badge-component.yml4
-rw-r--r--changelogs/unreleased/30827-changes-to-audit-log.yml4
-rw-r--r--changelogs/unreleased/30949-empty-states.yml4
-rw-r--r--changelogs/unreleased/30973-network-graph-sorted-by-date-and-topo.yml4
-rw-r--r--changelogs/unreleased/31106-tabs-alignment.yml4
-rw-r--r--changelogs/unreleased/31157-respect-project-features-in-wiki-search.yml4
-rw-r--r--changelogs/unreleased/31274-creating-schedule-trigger--causes-http-500-when-accessing-settings-ci_cd.yml4
-rw-r--r--changelogs/unreleased/31384-new-issue-button-on-no-results-page-after-search-doesn-t-go-to-correct-form.yml4
-rw-r--r--changelogs/unreleased/31474-issue-boards-sidebar-milestone-dropdown-should-not-be-multi-select.yml4
-rw-r--r--changelogs/unreleased/31483-ordered-task-list.yml4
-rw-r--r--changelogs/unreleased/31625-tag-editor-loses-all-inputs-when-you-try-to-add-a-tag-that-already-exists.yml4
-rw-r--r--changelogs/unreleased/31781-print-rendered-files-not-possible.yml4
-rw-r--r--changelogs/unreleased/31886-remover-comment-load-spinner.yml4
-rw-r--r--changelogs/unreleased/31902-namespace-recent-searches-to-project.yml4
-rw-r--r--changelogs/unreleased/31998-pipelines-empty-state.yml4
-rw-r--r--changelogs/unreleased/32086-atwho-is-still-enabled-for-personal-snippet-comments-form.yml4
-rw-r--r--changelogs/unreleased/32178-prevent-merge-on-sha-change.yml4
-rw-r--r--changelogs/unreleased/32219-speed-up-yarn-install-in-ci-by-utilizing-inter-pipeline-cache.yml4
-rw-r--r--changelogs/unreleased/32340-correct-jobs-api-documentation4
-rw-r--r--changelogs/unreleased/32395-duplicate-string-in-https-docs-gitlab-com-ce-administration-environment_variables-html.yml4
-rw-r--r--changelogs/unreleased/adam-influxdb-hostname.yml4
-rw-r--r--changelogs/unreleased/add-index-for-auto_canceled_by_id-mysql.yml4
-rw-r--r--changelogs/unreleased/add_ability_to_cancel_attaching_file_and_redesign_attaching_files_ui.yml4
-rw-r--r--changelogs/unreleased/allow_numeric_values_in_gitlab_ci_yml.yml4
-rw-r--r--changelogs/unreleased/branch-name-escape.yml4
-rw-r--r--changelogs/unreleased/bvl-markup-pipeline.yml4
-rw-r--r--changelogs/unreleased/bvl-rename-build-events-to-job-events.yml4
-rw-r--r--changelogs/unreleased/bvl-validate-urls-in-markdown-using-uri.yml4
-rw-r--r--changelogs/unreleased/counters_cache_invalidation.yml4
-rw-r--r--changelogs/unreleased/disable-usage-ping-2.yml4
-rw-r--r--changelogs/unreleased/disable-usage-ping.yml4
-rw-r--r--changelogs/unreleased/dm-async-tree-readme.yml4
-rw-r--r--changelogs/unreleased/dm-auxiliary-viewers.yml5
-rw-r--r--changelogs/unreleased/dm-catch-uri-errors.yml4
-rw-r--r--changelogs/unreleased/dm-consistent-commit-sha-style.yml4
-rw-r--r--changelogs/unreleased/dm-copy-mr-source-branch-as-gfm.yml4
-rw-r--r--changelogs/unreleased/dm-dependency-linker-gemfile.yml4
-rw-r--r--changelogs/unreleased/dm-tree-last-commit.yml4
-rw-r--r--changelogs/unreleased/dont-blow-up-when-email-has-no-references-header.yml5
-rw-r--r--changelogs/unreleased/dturner-username.yml4
-rw-r--r--changelogs/unreleased/dz-project-list-cache-key.yml4
-rw-r--r--changelogs/unreleased/enable-auto-cancelling-by-default.yml4
-rw-r--r--changelogs/unreleased/environments-button-open-same-tab.yml5
-rw-r--r--changelogs/unreleased/fix-allow-accessing-appearance-images.yml4
-rw-r--r--changelogs/unreleased/fix-conflict-resolution-with-corrupt-repos.yml5
-rw-r--r--changelogs/unreleased/fix-github-import.yml4
-rw-r--r--changelogs/unreleased/fix-import-export-missing-attributes.yml4
-rw-r--r--changelogs/unreleased/fix-search-not-highlighting.yml4
-rw-r--r--changelogs/unreleased/fix-trailing-space-mr-widget.yml4
-rw-r--r--changelogs/unreleased/get_rid_of_pluck.yml4
-rw-r--r--changelogs/unreleased/gitaly-local-branches.yml4
-rw-r--r--changelogs/unreleased/hamlit-xss-fix.yml4
-rw-r--r--changelogs/unreleased/issue-templates-summary-lines.yml4
-rw-r--r--changelogs/unreleased/issue_api_change.yml5
-rw-r--r--changelogs/unreleased/merge-request-poll-json-endpoint.yml4
-rw-r--r--changelogs/unreleased/omega-submodules.yml4
-rw-r--r--changelogs/unreleased/pipeline-schedules-callout-docs-url.yml4
-rw-r--r--changelogs/unreleased/protected-branches-no-one-merge.yml4
-rw-r--r--changelogs/unreleased/remove-old-isobject.yml4
-rw-r--r--changelogs/unreleased/rs-sanitize-submodule-urls.yml4
-rw-r--r--changelogs/unreleased/sh-fix-container-registry-s3-redirects.yml4
-rw-r--r--changelogs/unreleased/snippets-finder-visibility.yml4
-rw-r--r--changelogs/unreleased/snippets_visibility.yml4
-rw-r--r--changelogs/unreleased/store-retried-in-database-for-ci-builds.yml4
-rw-r--r--changelogs/unreleased/tc-clean-pending-delete-projects.yml4
-rw-r--r--changelogs/unreleased/tc-fix-private-subgroups-shown.yml4
-rw-r--r--changelogs/unreleased/update-admin-health-page.yml5
-rw-r--r--changelogs/unreleased/winh-pipeline-author-link.yml4
-rw-r--r--changelogs/unreleased/zj-clean-up-ci-variables-table.yml4
-rw-r--r--changelogs/unreleased/zj-pipeline-schedule-owner.yml4
-rw-r--r--config/application.rb1
-rw-r--r--config/environments/production.rb2
-rw-r--r--config/initializers/1_settings.rb3
-rw-r--r--config/initializers/8_gitaly.rb6
-rw-r--r--config/initializers/ar_monkey_patch.rb2
-rw-r--r--config/initializers/hamlit.rb4
-rw-r--r--config/initializers/rspec_profiling.rb2
-rw-r--r--config/initializers/static_files.rb6
-rw-r--r--config/routes/project.rb2
-rw-r--r--config/sidekiq_queues.yml1
-rw-r--r--config/webpack.config.js15
-rw-r--r--db/migrate/20160810142633_remove_redundant_indexes.rb2
-rw-r--r--db/migrate/20160829114652_add_markdown_cache_columns.rb2
-rw-r--r--db/migrate/20170320171632_create_issue_assignees_table.rb40
-rw-r--r--db/migrate/20170320173259_migrate_assignees.rb8
-rw-r--r--db/migrate/20170502065653_make_auto_cancel_pending_pipelines_on_by_default.rb13
-rw-r--r--db/migrate/20170502135553_create_index_ci_pipelines_auto_canceled_by_id.rb21
-rw-r--r--db/migrate/20170502140503_create_index_ci_builds_auto_canceled_by_id.rb21
-rw-r--r--db/migrate/20170503004426_add_retried_to_ci_build.rb9
-rw-r--r--db/migrate/20170503023315_add_repository_update_events_to_web_hooks.rb15
-rw-r--r--db/migrate/20170508153950_add_not_null_contraints_to_ci_variables.rb12
-rw-r--r--db/migrate/20170508190732_add_foreign_key_to_ci_variables.rb24
-rw-r--r--db/migrate/20170511082759_rename_web_hooks_build_events_to_job_events.rb18
-rw-r--r--db/migrate/20170511083824_rename_services_build_events_to_job_events.rb18
-rw-r--r--db/migrate/20170516153305_migrate_assignee_to_separate_table.rb83
-rw-r--r--db/migrate/20170516183131_add_indices_to_issue_assignees.rb41
-rw-r--r--db/post_migrate/20170425121605_migrate_trigger_schedules_to_pipeline_schedules.rb7
-rw-r--r--db/post_migrate/20170502070007_enable_auto_cancel_pending_pipelines_for_all.rb15
-rw-r--r--db/post_migrate/20170502101023_cleanup_namespaceless_pending_delete_projects.rb47
-rw-r--r--db/post_migrate/20170503004427_upate_retried_for_ci_build.rb43
-rw-r--r--db/post_migrate/20170510101043_add_foreign_key_on_pipeline_schedule_owner.rb35
-rw-r--r--db/post_migrate/20170511100900_cleanup_rename_web_hooks_build_events_to_job_events.rb18
-rw-r--r--db/post_migrate/20170511101000_cleanup_rename_services_build_events_to_job_events.rb18
-rw-r--r--db/post_migrate/20170516165238_cleanup_trigger_for_issues.rb39
-rw-r--r--db/post_migrate/20170516181025_add_constraints_to_issue_assignees_table.rb37
-rw-r--r--db/schema.rb24
-rw-r--r--doc/administration/environment_variables.md1
-rw-r--r--doc/administration/high_availability/README.md18
-rw-r--r--doc/administration/high_availability/nfs.md19
-rw-r--r--doc/api/README.md5
-rw-r--r--doc/api/access_requests.md2
-rw-r--r--doc/api/award_emoji.md2
-rw-r--r--doc/api/boards.md2
-rw-r--r--doc/api/branches.md2
-rw-r--r--doc/api/broadcast_messages.md2
-rw-r--r--doc/api/build_variables.md2
-rw-r--r--doc/api/ci/lint.md2
-rw-r--r--doc/api/ci/runners.md2
-rw-r--r--doc/api/deploy_key_multiple_projects.md2
-rw-r--r--doc/api/deploy_keys.md2
-rw-r--r--doc/api/enviroments.md2
-rw-r--r--doc/api/groups.md2
-rw-r--r--doc/api/issues.md24
-rw-r--r--doc/api/jobs.md4
-rw-r--r--doc/api/keys.md2
-rw-r--r--doc/api/labels.md2
-rw-r--r--doc/api/members.md2
-rw-r--r--doc/api/merge_requests.md2
-rw-r--r--doc/api/milestones.md2
-rw-r--r--doc/api/namespaces.md2
-rw-r--r--doc/api/notes.md2
-rw-r--r--doc/api/notification_settings.md2
-rw-r--r--doc/api/pipeline_triggers.md2
-rw-r--r--doc/api/projects.md5
-rw-r--r--doc/api/repositories.md2
-rw-r--r--doc/api/repository_files.md2
-rw-r--r--doc/api/services.md4
-rw-r--r--doc/api/session.md2
-rw-r--r--doc/api/settings.md2
-rw-r--r--doc/api/sidekiq_metrics.md2
-rw-r--r--doc/api/snippets.md2
-rw-r--r--doc/api/system_hooks.md2
-rw-r--r--doc/api/tags.md2
-rw-r--r--doc/api/templates/gitignores.md2
-rw-r--r--doc/api/templates/gitlab_ci_ymls.md2
-rw-r--r--doc/api/templates/licenses.md2
-rw-r--r--doc/api/todos.md2
-rw-r--r--doc/api/users.md2
-rw-r--r--doc/api/v3_to_v4.md6
-rw-r--r--doc/articles/how_to_configure_ldap_gitlab_ce/index.md2
-rw-r--r--doc/articles/how_to_install_git/index.md66
-rw-r--r--doc/articles/index.md4
-rw-r--r--doc/ci/README.md4
-rw-r--r--doc/ci/api/README.md2
-rw-r--r--doc/ci/api/builds.md2
-rw-r--r--doc/ci/api/runners.md2
-rw-r--r--doc/ci/docker/using_docker_build.md10
-rw-r--r--doc/ci/docker/using_docker_images.md2
-rw-r--r--doc/ci/environments.md3
-rw-r--r--doc/ci/examples/README.md1
-rw-r--r--doc/ci/examples/code_climate.md28
-rw-r--r--doc/ci/examples/test-and-deploy-python-application-to-heroku.md4
-rw-r--r--doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md4
-rw-r--r--doc/ci/img/pipeline_schedules_list.pngbin67555 -> 0 bytes
-rw-r--r--doc/ci/pipeline_schedules.md40
-rw-r--r--doc/ci/quick_start/README.md6
-rw-r--r--doc/ci/triggers/README.md20
-rw-r--r--doc/ci/variables/README.md2
-rw-r--r--doc/ci/yaml/README.md6
-rw-r--r--doc/development/README.md4
-rw-r--r--doc/development/architecture.md2
-rw-r--r--doc/development/doc_styleguide.md9
-rw-r--r--doc/development/fe_guide/img/testing_triangle.pngbin0 -> 11836 bytes
-rw-r--r--doc/development/fe_guide/index.md14
-rw-r--r--doc/development/fe_guide/testing.md102
-rw-r--r--doc/development/i18n_guide.md239
-rw-r--r--doc/development/ux_guide/basics.md2
-rw-r--r--doc/development/what_requires_downtime.md2
-rw-r--r--doc/development/writing_documentation.md25
-rw-r--r--doc/install/README.md2
-rw-r--r--doc/install/google_cloud_platform/index.md4
-rw-r--r--doc/install/installation.md13
-rw-r--r--doc/install/kubernetes/gitlab_chart.md4
-rw-r--r--doc/install/requirements.md20
-rw-r--r--doc/integration/github.md46
-rw-r--r--doc/system_hooks/system_hooks.md50
-rw-r--r--doc/topics/authentication/index.md2
-rw-r--r--doc/topics/git/index.md1
-rw-r--r--doc/university/high-availability/aws/README.md22
-rw-r--r--doc/user/admin_area/settings/usage_statistics.md81
-rw-r--r--doc/user/group/subgroups/index.md8
-rw-r--r--doc/user/project/integrations/img/merge_request_performance.pngbin0 -> 66775 bytes
-rw-r--r--doc/user/project/integrations/prometheus.md10
-rw-r--r--doc/user/project/integrations/webhooks.md1
-rw-r--r--doc/user/project/issues/img/create_new_merge_request.pngbin0 -> 26285 bytes
-rw-r--r--doc/user/project/issues/issues_functionalities.md9
-rw-r--r--doc/user/project/new_ci_build_permissions_model.md2
-rw-r--r--doc/user/project/pages/getting_started_part_four.md5
-rw-r--r--doc/user/project/pages/getting_started_part_one.md7
-rw-r--r--doc/user/project/pages/getting_started_part_three.md5
-rw-r--r--doc/user/project/pages/getting_started_part_two.md5
-rw-r--r--doc/user/project/pipelines/img/pipeline_schedules_list.pngbin0 -> 14665 bytes
-rw-r--r--doc/user/project/pipelines/img/pipeline_schedules_new_form.png (renamed from doc/ci/img/pipeline_schedules_new_form.png)bin49873 -> 49873 bytes
-rw-r--r--doc/user/project/pipelines/img/pipeline_schedules_ownership.pngbin0 -> 12043 bytes
-rw-r--r--doc/user/project/pipelines/job_artifacts.md4
-rw-r--r--doc/user/project/pipelines/schedules.md62
-rw-r--r--doc/user/search/img/filter_issues_project.gifbin1430218 -> 0 bytes
-rw-r--r--doc/user/search/img/issue_search_filter.pngbin0 -> 69559 bytes
-rw-r--r--doc/user/search/index.md8
-rw-r--r--features/profile/active_tab.feature6
-rw-r--r--features/profile/profile.feature2
-rw-r--r--features/project/project.feature1
-rw-r--r--features/project/source/markdown_render.feature3
-rw-r--r--features/search.feature4
-rw-r--r--features/steps/dashboard/todos.rb6
-rw-r--r--features/steps/explore/projects.rb2
-rw-r--r--features/steps/profile/active_tab.rb4
-rw-r--r--features/steps/project/builds/artifacts.rb2
-rw-r--r--features/steps/project/forked_merge_requests.rb33
-rw-r--r--features/steps/project/merge_requests.rb4
-rw-r--r--features/steps/project/project.rb2
-rw-r--r--features/steps/project/source/markdown_render.rb1
-rw-r--r--features/steps/search.rb8
-rw-r--r--features/steps/shared/paths.rb2
-rw-r--r--features/steps/shared/project.rb4
-rw-r--r--features/support/env.rb4
-rw-r--r--lib/api/entities.rb6
-rw-r--r--lib/api/groups.rb4
-rw-r--r--lib/api/helpers.rb6
-rw-r--r--lib/api/helpers/common_helpers.rb8
-rw-r--r--lib/api/internal.rb2
-rw-r--r--lib/api/project_hooks.rb2
-rw-r--r--lib/api/project_snippets.rb3
-rw-r--r--lib/api/projects.rb4
-rw-r--r--lib/api/services.rb6
-rw-r--r--lib/api/snippets.rb4
-rw-r--r--lib/api/subscriptions.rb2
-rw-r--r--lib/api/users.rb11
-rw-r--r--lib/api/v3/entities.rb6
-rw-r--r--lib/api/v3/groups.rb4
-rw-r--r--lib/api/v3/project_snippets.rb3
-rw-r--r--lib/api/v3/projects.rb2
-rw-r--r--lib/api/v3/services.rb2
-rw-r--r--lib/api/v3/snippets.rb4
-rw-r--r--lib/api/v3/subscriptions.rb2
-rw-r--r--lib/banzai/filter/external_link_filter.rb36
-rw-r--r--lib/banzai/pipeline/markup_pipeline.rb13
-rw-r--r--lib/ci/ansi2html.rb2
-rw-r--r--lib/ci/gitlab_ci_yaml_processor.rb2
-rw-r--r--lib/container_registry/client.rb14
-rw-r--r--lib/github/import.rb5
-rw-r--r--lib/gitlab/access.rb6
-rw-r--r--lib/gitlab/asciidoc.rb8
-rw-r--r--lib/gitlab/chat_commands/command.rb2
-rw-r--r--lib/gitlab/ci/config/entry/legacy_validation_helpers.rb8
-rw-r--r--lib/gitlab/ci/config/entry/variables.rb4
-rw-r--r--lib/gitlab/conflict/file_collection.rb42
-rw-r--r--lib/gitlab/cycle_analytics/permissions.rb2
-rw-r--r--lib/gitlab/data_builder/build.rb6
-rw-r--r--lib/gitlab/data_builder/push.rb2
-rw-r--r--lib/gitlab/data_builder/repository.rb35
-rw-r--r--lib/gitlab/database/migration_helpers.rb25
-rw-r--r--lib/gitlab/database/rename_reserved_paths_migration/v1/migration_classes.rb8
-rw-r--r--lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base.rb5
-rw-r--r--lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces.rb6
-rw-r--r--lib/gitlab/dependency_linker.rb18
-rw-r--r--lib/gitlab/dependency_linker/base_linker.rb109
-rw-r--r--lib/gitlab/dependency_linker/gemfile_linker.rb29
-rw-r--r--lib/gitlab/diff/inline_diff_markdown_marker.rb17
-rw-r--r--lib/gitlab/diff/inline_diff_marker.rb130
-rw-r--r--lib/gitlab/diff/position_tracer.rb2
-rw-r--r--lib/gitlab/ee_compat_check.rb13
-rw-r--r--lib/gitlab/etag_caching/router.rb8
-rw-r--r--lib/gitlab/file_detector.rb20
-rw-r--r--lib/gitlab/file_finder.rb32
-rw-r--r--lib/gitlab/git/blob.rb2
-rw-r--r--lib/gitlab/git/branch.rb34
-rw-r--r--lib/gitlab/git/commit.rb9
-rw-r--r--lib/gitlab/git/diff.rb20
-rw-r--r--lib/gitlab/git/diff_collection.rb11
-rw-r--r--lib/gitlab/git/repository.rb81
-rw-r--r--lib/gitlab/git/tree.rb2
-rw-r--r--lib/gitlab/git_post_receive.rb10
-rw-r--r--lib/gitlab/gitaly_client.rb64
-rw-r--r--lib/gitlab/gitaly_client/commit.rb54
-rw-r--r--lib/gitlab/gitaly_client/notifications.rb2
-rw-r--r--lib/gitlab/gitaly_client/ref.rb20
-rw-r--r--lib/gitlab/gitaly_client/util.rb2
-rw-r--r--lib/gitlab/gon_helper.rb1
-rw-r--r--lib/gitlab/highlight.rb37
-rw-r--r--lib/gitlab/kubernetes.rb4
-rw-r--r--lib/gitlab/ldap/config.rb2
-rw-r--r--lib/gitlab/metrics.rb3
-rw-r--r--lib/gitlab/other_markup.rb6
-rw-r--r--lib/gitlab/project_search_results.rb20
-rw-r--r--lib/gitlab/prometheus/queries/base_query.rb26
-rw-r--r--lib/gitlab/prometheus/queries/deployment_query.rb26
-rw-r--r--lib/gitlab/prometheus/queries/environment_query.rb20
-rw-r--r--lib/gitlab/prometheus_client.rb (renamed from lib/gitlab/prometheus.rb)4
-rw-r--r--lib/gitlab/sentry.rb2
-rw-r--r--lib/gitlab/string_range_marker.rb102
-rw-r--r--lib/gitlab/string_regex_marker.rb13
-rw-r--r--lib/gitlab/usage_data.rb2
-rw-r--r--lib/gitlab/workhorse.rb12
-rw-r--r--lib/tasks/gemojione.rake2
-rw-r--r--lib/tasks/gitlab/update_templates.rake2
-rw-r--r--lib/tasks/spec.rake2
-rw-r--r--scripts/prepare_build.sh30
-rwxr-xr-xscripts/trigger-build2
-rw-r--r--spec/bin/changelog_spec.rb4
-rw-r--r--spec/controllers/admin/hooks_controller_spec.rb28
-rw-r--r--spec/controllers/groups/milestones_controller_spec.rb135
-rw-r--r--spec/controllers/groups_controller_spec.rb298
-rw-r--r--spec/controllers/projects/deployments_controller_spec.rb71
-rw-r--r--spec/controllers/projects/labels_controller_spec.rb70
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb10
-rw-r--r--spec/controllers/projects_controller_spec.rb203
-rw-r--r--spec/controllers/snippets_controller_spec.rb34
-rw-r--r--spec/controllers/uploads_controller_spec.rb40
-rw-r--r--spec/controllers/users_controller_spec.rb257
-rw-r--r--spec/factories/ci/variables.rb2
-rw-r--r--spec/factories/group_members.rb6
-rw-r--r--spec/factories/project_hooks.rb2
-rw-r--r--spec/factories/project_members.rb6
-rw-r--r--spec/factories/projects.rb12
-rw-r--r--spec/factories/services.rb2
-rw-r--r--spec/features/admin/admin_uses_repository_checks_spec.rb2
-rw-r--r--spec/features/auto_deploy_spec.rb9
-rw-r--r--spec/features/boards/sidebar_spec.rb3
-rw-r--r--spec/features/copy_as_gfm_spec.rb2
-rw-r--r--spec/features/dashboard/group_spec.rb2
-rw-r--r--spec/features/dashboard/issuables_counter_spec.rb4
-rw-r--r--spec/features/dashboard/issues_spec.rb11
-rw-r--r--spec/features/dashboard/milestone_filter_spec.rb60
-rw-r--r--spec/features/dashboard/shortcuts_spec.rb31
-rw-r--r--spec/features/dashboard/snippets_spec.rb47
-rw-r--r--spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb2
-rw-r--r--spec/features/issues/filtered_search/recent_searches_spec.rb54
-rw-r--r--spec/features/issues/form_spec.rb40
-rw-r--r--spec/features/issues/note_polling_spec.rb115
-rw-r--r--spec/features/issues/notes_on_issues_spec.rb77
-rw-r--r--spec/features/issues_spec.rb22
-rw-r--r--spec/features/merge_requests/conflicts_spec.rb2
-rw-r--r--spec/features/merge_requests/diff_notes_avatars_spec.rb2
-rw-r--r--spec/features/merge_requests/diff_notes_resolve_spec.rb2
-rw-r--r--spec/features/merge_requests/diffs_spec.rb28
-rw-r--r--spec/features/merge_requests/merge_immediately_with_pipeline_spec.rb8
-rw-r--r--spec/features/profiles/preferences_spec.rb2
-rw-r--r--spec/features/projects/blobs/blob_show_spec.rb134
-rw-r--r--spec/features/projects/branches/new_branch_ref_dropdown_spec.rb2
-rw-r--r--spec/features/projects/features_visibility_spec.rb2
-rw-r--r--spec/features/projects/files/browse_files_spec.rb12
-rw-r--r--spec/features/projects/gfm_autocomplete_load_spec.rb2
-rw-r--r--spec/features/projects/pipeline_schedules_spec.rb12
-rw-r--r--spec/features/projects/pipelines/pipelines_spec.rb2
-rw-r--r--spec/features/projects/settings/integration_settings_spec.rb2
-rw-r--r--spec/features/projects/snippets_spec.rb24
-rw-r--r--spec/features/projects/wiki/markdown_preview_spec.rb8
-rw-r--r--spec/features/projects/wiki/user_creates_wiki_page_spec.rb4
-rw-r--r--spec/features/protected_branches_spec.rb1
-rw-r--r--spec/features/protected_tags_spec.rb1
-rw-r--r--spec/features/search_spec.rb8
-rw-r--r--spec/features/snippets/explore_spec.rb25
-rw-r--r--spec/features/snippets/internal_snippet_spec.rb23
-rw-r--r--spec/features/tags/master_creates_tag_spec.rb16
-rw-r--r--spec/features/todos/todos_spec.rb6
-rw-r--r--spec/features/uploads/user_uploads_file_to_note_spec.rb76
-rw-r--r--spec/features/user_callout_spec.rb2
-rw-r--r--spec/features/users/snippets_spec.rb46
-rw-r--r--spec/finders/groups_finder_spec.rb57
-rw-r--r--spec/finders/snippets_finder_spec.rb125
-rw-r--r--spec/finders/users_finder_spec.rb66
-rw-r--r--spec/fixtures/api/schemas/entities/merge_request.json1
-rw-r--r--spec/helpers/blob_helper_spec.rb7
-rw-r--r--spec/helpers/diff_helper_spec.rb4
-rw-r--r--spec/helpers/projects_helper_spec.rb4
-rw-r--r--spec/helpers/submodule_helper_spec.rb25
-rw-r--r--spec/javascripts/blob/balsamiq/balsamiq_viewer_integration_spec.js51
-rw-r--r--spec/javascripts/blob/balsamiq/balsamiq_viewer_spec.js38
-rw-r--r--spec/javascripts/blob/create_branch_dropdown_spec.js1
-rw-r--r--spec/javascripts/blob/target_branch_dropdown_spec.js1
-rw-r--r--spec/javascripts/blob/viewer/index_spec.js31
-rw-r--r--spec/javascripts/boards/issue_card_spec.js2
-rw-r--r--spec/javascripts/commit/pipelines/mock_data.js90
-rw-r--r--spec/javascripts/commit/pipelines/pipelines_spec.js7
-rw-r--r--spec/javascripts/droplab/drop_down_spec.js53
-rw-r--r--spec/javascripts/droplab/hook_spec.js8
-rw-r--r--spec/javascripts/fixtures/balsamiq.rb18
-rw-r--r--spec/javascripts/fixtures/balsamiq_viewer.html.haml1
-rw-r--r--spec/javascripts/fixtures/pipelines.rb35
-rw-r--r--spec/javascripts/gfm_auto_complete_spec.js20
-rw-r--r--spec/javascripts/gl_dropdown_spec.js1
-rw-r--r--spec/javascripts/issue_show/components/app_spec.js60
-rw-r--r--spec/javascripts/issue_show/components/description_spec.js99
-rw-r--r--spec/javascripts/issue_show/components/title_spec.js67
-rw-r--r--spec/javascripts/issue_show/issue_title_description_spec.js60
-rw-r--r--spec/javascripts/issue_show/mock_data.js6
-rw-r--r--spec/javascripts/labels_issue_sidebar_spec.js1
-rw-r--r--spec/javascripts/lib/utils/ajax_cache_spec.js57
-rw-r--r--spec/javascripts/lib/utils/common_utils_spec.js10
-rw-r--r--spec/javascripts/lib/utils/poll_spec.js93
-rw-r--r--spec/javascripts/notes_spec.js196
-rw-r--r--spec/javascripts/pipeline_schedules/interval_pattern_input_spec.js42
-rw-r--r--spec/javascripts/pipeline_schedules/pipeline_schedule_callout_spec.js15
-rw-r--r--spec/javascripts/pipelines/mock_data.js107
-rw-r--r--spec/javascripts/pipelines/pipeline_url_spec.js2
-rw-r--r--spec/javascripts/pipelines/pipelines_spec.js9
-rw-r--r--spec/javascripts/project_title_spec.js1
-rw-r--r--spec/javascripts/search_autocomplete_spec.js1
-rw-r--r--spec/javascripts/sidebar/sidebar_assignees_spec.js1
-rw-r--r--spec/javascripts/sidebar/sidebar_bundle_spec.js42
-rw-r--r--spec/javascripts/sidebar/sidebar_mediator_spec.js1
-rw-r--r--spec/javascripts/sidebar/sidebar_service_spec.js1
-rw-r--r--spec/javascripts/vue_mr_widget/components/mr_widget_deployment_spec.js4
-rw-r--r--spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js13
-rw-r--r--spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js6
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_closed_spec.js4
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_nothing_to_merge_spec.js16
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js2
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_sha_mismatch_spec.js16
-rw-r--r--spec/javascripts/vue_mr_widget/stores/get_state_key_spec.js3
-rw-r--r--spec/javascripts/vue_mr_widget/stores/mr_widget_store_spec.js22
-rw-r--r--spec/javascripts/vue_shared/components/ci_badge_link_spec.js89
-rw-r--r--spec/javascripts/vue_shared/components/ci_icon_spec.js9
-rw-r--r--spec/javascripts/vue_shared/components/commit_spec.js10
-rw-r--r--spec/javascripts/vue_shared/components/pipelines_table_row_spec.js94
-rw-r--r--spec/javascripts/vue_shared/components/pipelines_table_spec.js8
-rw-r--r--spec/javascripts/vue_shared/components/user_avatar_image_spec.js54
-rw-r--r--spec/javascripts/vue_shared/components/user_avatar_link_spec.js50
-rw-r--r--spec/javascripts/vue_shared/components/user_avatar_svg_spec.js29
-rw-r--r--spec/lib/banzai/filter/external_link_filter_spec.rb90
-rw-r--r--spec/lib/ci/gitlab_ci_yaml_processor_spec.rb12
-rw-r--r--spec/lib/container_registry/blob_spec.rb2
-rw-r--r--spec/lib/container_registry/client_spec.rb39
-rw-r--r--spec/lib/expand_variables_spec.rb6
-rw-r--r--spec/lib/gitlab/asciidoc_spec.rb29
-rw-r--r--spec/lib/gitlab/auth_spec.rb4
-rw-r--r--spec/lib/gitlab/backup/manager_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/config/entry/global_spec.rb6
-rw-r--r--spec/lib/gitlab/ci/config/entry/variables_spec.rb8
-rw-r--r--spec/lib/gitlab/conflict/file_collection_spec.rb2
-rw-r--r--spec/lib/gitlab/contributions_calendar_spec.rb2
-rw-r--r--spec/lib/gitlab/data_builder/push_spec.rb1
-rw-r--r--spec/lib/gitlab/database/migration_helpers_spec.rb14
-rw-r--r--spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb9
-rw-r--r--spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb58
-rw-r--r--spec/lib/gitlab/dependency_linker/gemfile_linker_spec.rb60
-rw-r--r--spec/lib/gitlab/dependency_linker_spec.rb13
-rw-r--r--spec/lib/gitlab/diff/highlight_spec.rb4
-rw-r--r--spec/lib/gitlab/diff/inline_diff_markdown_marker_spec.rb14
-rw-r--r--spec/lib/gitlab/diff/inline_diff_marker_spec.rb18
-rw-r--r--spec/lib/gitlab/diff/position_tracer_spec.rb8
-rw-r--r--spec/lib/gitlab/etag_caching/router_spec.rb2
-rw-r--r--spec/lib/gitlab/file_finder_spec.rb21
-rw-r--r--spec/lib/gitlab/git/branch_spec.rb45
-rw-r--r--spec/lib/gitlab/git/diff_spec.rb2
-rw-r--r--spec/lib/gitlab/git/encoding_helper_spec.rb16
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb54
-rw-r--r--spec/lib/gitlab/git/util_spec.rb2
-rw-r--r--spec/lib/gitlab/gitaly_client/commit_spec.rb54
-rw-r--r--spec/lib/gitlab/gitaly_client/ref_spec.rb30
-rw-r--r--spec/lib/gitlab/gitaly_client_spec.rb21
-rw-r--r--spec/lib/gitlab/highlight_spec.rb11
-rw-r--r--spec/lib/gitlab/import_export/project.json36
-rw-r--r--spec/lib/gitlab/import_export/relation_factory_spec.rb4
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml6
-rw-r--r--spec/lib/gitlab/other_markup_spec.rb2
-rw-r--r--spec/lib/gitlab/project_search_results_spec.rb77
-rw-r--r--spec/lib/gitlab/prometheus/queries/deployment_query_spec.rb37
-rw-r--r--spec/lib/gitlab/prometheus_client_spec.rb (renamed from spec/lib/gitlab/prometheus_spec.rb)2
-rw-r--r--spec/lib/gitlab/repo_path_spec.rb2
-rw-r--r--spec/lib/gitlab/string_range_marker_spec.rb36
-rw-r--r--spec/lib/gitlab/string_regex_marker_spec.rb18
-rw-r--r--spec/lib/gitlab/usage_data_spec.rb2
-rw-r--r--spec/lib/gitlab/workhorse_spec.rb4
-rw-r--r--spec/migrations/cleanup_namespaceless_pending_delete_projects_spec.rb32
-rw-r--r--spec/migrations/migrate_build_events_to_pipeline_events_spec.rb74
-rw-r--r--spec/migrations/migrate_user_project_view_spec.rb7
-rw-r--r--spec/models/application_setting_spec.rb64
-rw-r--r--spec/models/blob_spec.rb46
-rw-r--r--spec/models/blob_viewer/base_spec.rb129
-rw-r--r--spec/models/blob_viewer/changelog_spec.rb27
-rw-r--r--spec/models/blob_viewer/composer_json_spec.rb25
-rw-r--r--spec/models/blob_viewer/gemspec_spec.rb25
-rw-r--r--spec/models/blob_viewer/gitlab_ci_yml_spec.rb32
-rw-r--r--spec/models/blob_viewer/license_spec.rb34
-rw-r--r--spec/models/blob_viewer/package_json_spec.rb25
-rw-r--r--spec/models/blob_viewer/podspec_json_spec.rb25
-rw-r--r--spec/models/blob_viewer/podspec_spec.rb25
-rw-r--r--spec/models/blob_viewer/route_map_spec.rb38
-rw-r--r--spec/models/blob_viewer/server_side_spec.rb41
-rw-r--r--spec/models/ci/build_spec.rb4
-rw-r--r--spec/models/ci/pipeline_spec.rb34
-rw-r--r--spec/models/ci/stage_spec.rb4
-rw-r--r--spec/models/ci/variable_spec.rb2
-rw-r--r--spec/models/commit_spec.rb28
-rw-r--r--spec/models/commit_status_spec.rb24
-rw-r--r--spec/models/deployment_spec.rb9
-rw-r--r--spec/models/environment_spec.rb4
-rw-r--r--spec/models/global_milestone_spec.rb2
-rw-r--r--spec/models/hooks/system_hook_spec.rb21
-rw-r--r--spec/models/issue_spec.rb40
-rw-r--r--spec/models/merge_request_spec.rb107
-rw-r--r--spec/models/project_authorization_spec.rb2
-rw-r--r--spec/models/project_services/asana_service_spec.rb2
-rw-r--r--spec/models/project_services/chat_message/issue_message_spec.rb2
-rw-r--r--spec/models/project_services/chat_message/merge_message_spec.rb2
-rw-r--r--spec/models/project_services/chat_message/note_message_spec.rb2
-rw-r--r--spec/models/project_services/chat_message/pipeline_message_spec.rb12
-rw-r--r--spec/models/project_services/chat_message/push_message_spec.rb20
-rw-r--r--spec/models/project_services/chat_message/wiki_page_message_spec.rb4
-rw-r--r--spec/models/project_services/kubernetes_service_spec.rb6
-rw-r--r--spec/models/project_services/pivotaltracker_service_spec.rb2
-rw-r--r--spec/models/project_services/prometheus_service_spec.rb32
-rw-r--r--spec/models/project_snippet_spec.rb3
-rw-r--r--spec/models/project_spec.rb2
-rw-r--r--spec/models/project_statistics_spec.rb4
-rw-r--r--spec/models/protected_branch/merge_access_level_spec.rb5
-rw-r--r--spec/models/protected_branch/push_access_level_spec.rb5
-rw-r--r--spec/models/protected_branch_spec.rb3
-rw-r--r--spec/models/repository_spec.rb103
-rw-r--r--spec/models/snippet_spec.rb40
-rw-r--r--spec/models/user_spec.rb46
-rw-r--r--spec/policies/project_snippet_policy_spec.rb80
-rw-r--r--spec/presenters/merge_request_presenter_spec.rb14
-rw-r--r--spec/requests/api/commit_statuses_spec.rb4
-rw-r--r--spec/requests/api/files_spec.rb2
-rw-r--r--spec/requests/api/groups_spec.rb2
-rw-r--r--spec/requests/api/issues_spec.rb2
-rw-r--r--spec/requests/api/project_hooks_spec.rb4
-rw-r--r--spec/requests/api/projects_spec.rb2
-rw-r--r--spec/requests/api/system_hooks_spec.rb3
-rw-r--r--spec/requests/api/v3/files_spec.rb4
-rw-r--r--spec/requests/api/v3/groups_spec.rb2
-rw-r--r--spec/requests/api/v3/project_hooks_spec.rb4
-rw-r--r--spec/requests/api/v3/projects_spec.rb4
-rw-r--r--spec/requests/api/v3/system_hooks_spec.rb3
-rw-r--r--spec/requests/ci/api/builds_spec.rb2
-rw-r--r--spec/requests/lfs_http_spec.rb6
-rw-r--r--spec/requests/openid_connect_spec.rb4
-rw-r--r--spec/routing/routing_spec.rb37
-rw-r--r--spec/serializers/analytics_issue_entity_spec.rb2
-rw-r--r--spec/serializers/analytics_issue_serializer_spec.rb2
-rw-r--r--spec/serializers/merge_request_entity_spec.rb17
-rw-r--r--spec/services/ci/create_pipeline_service_spec.rb14
-rw-r--r--spec/services/ci/process_pipeline_service_spec.rb15
-rw-r--r--spec/services/ci/retry_build_service_spec.rb11
-rw-r--r--spec/services/ci/retry_pipeline_service_spec.rb2
-rw-r--r--spec/services/cohorts_service_spec.rb2
-rw-r--r--spec/services/create_deployment_service_spec.rb2
-rw-r--r--spec/services/issuable/bulk_update_service_spec.rb6
-rw-r--r--spec/services/issues/build_service_spec.rb2
-rw-r--r--spec/services/issues/close_service_spec.rb14
-rw-r--r--spec/services/issues/create_service_spec.rb16
-rw-r--r--spec/services/issues/resolve_discussions_spec.rb2
-rw-r--r--spec/services/issues/update_service_spec.rb7
-rw-r--r--spec/services/members/authorized_destroy_service_spec.rb21
-rw-r--r--spec/services/merge_requests/conflicts/list_service_spec.rb80
-rw-r--r--spec/services/merge_requests/conflicts/resolve_service_spec.rb (renamed from spec/services/merge_requests/resolve_service_spec.rb)41
-rw-r--r--spec/services/merge_requests/create_service_spec.rb30
-rw-r--r--spec/services/merge_requests/refresh_service_spec.rb2
-rw-r--r--spec/services/merge_requests/update_service_spec.rb47
-rw-r--r--spec/services/notification_service_spec.rb2
-rw-r--r--spec/services/system_note_service_spec.rb2
-rw-r--r--spec/spec_helper.rb2
-rw-r--r--spec/support/dropzone_helper.rb40
-rw-r--r--spec/support/filtered_search_helpers.rb6
-rwxr-xr-xspec/support/generate-seed-repo-rb162
-rw-r--r--spec/support/kubernetes_helpers.rb8
-rw-r--r--spec/support/matchers/gitaly_matchers.rb6
-rw-r--r--spec/support/milestone_tabs_examples.rb2
-rw-r--r--spec/support/prometheus_helpers.rb8
-rw-r--r--spec/support/protected_branches/access_control_ce_shared_examples.rb (renamed from spec/features/protected_branches/access_control_ce_spec.rb)2
-rw-r--r--spec/support/protected_tags/access_control_ce_shared_examples.rb (renamed from spec/features/protected_tags/access_control_ce_spec.rb)0
-rw-r--r--spec/support/repo_helpers.rb4
-rw-r--r--spec/support/seed_repo.rb11
-rw-r--r--spec/support/test_env.rb66
-rw-r--r--spec/support/wait_for_requests.rb7
-rw-r--r--spec/support/workhorse_helpers.rb2
-rw-r--r--spec/tasks/gitlab/backup_rake_spec.rb3
-rw-r--r--spec/tasks/gitlab/gitaly_rake_spec.rb24
-rw-r--r--spec/views/projects/_last_commit.html.haml_spec.rb22
-rw-r--r--spec/views/projects/blob/_viewer.html.haml_spec.rb18
-rw-r--r--spec/views/projects/imports/new.html.haml_spec.rb22
-rw-r--r--spec/views/projects/pipelines/_stage.html.haml_spec.rb5
-rw-r--r--spec/views/projects/tree/show.html.haml_spec.rb8
-rw-r--r--spec/workers/git_garbage_collect_worker_spec.rb2
-rw-r--r--spec/workers/namespaceless_project_destroy_worker_spec.rb79
-rw-r--r--spec/workers/pipeline_schedule_worker_spec.rb51
-rw-r--r--spec/workers/post_receive_spec.rb30
-rw-r--r--spec/workers/project_cache_worker_spec.rb12
-rw-r--r--spec/workers/repository_check/clear_worker_spec.rb2
1003 files changed, 13016 insertions, 6710 deletions
diff --git a/.gitignore b/.gitignore
index 0fb97ffb98e..89da29fd790 100644
--- a/.gitignore
+++ b/.gitignore
@@ -18,6 +18,7 @@ eslint-report.html
.sass-cache/
/.secret
/.vagrant
+/.yarn-cache
/.byebug_history
/Vagrantfile
/backups/*
@@ -48,6 +49,7 @@ eslint-report.html
/public/uploads/
/shared/artifacts/
/spec/javascripts/fixtures/blob/pdf/
+/spec/javascripts/fixtures/blob/balsamiq/
/rails_best_practices_output.html
/tags
/tmp/*
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 88d536fa9b3..45f1638f871 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,9 +1,10 @@
image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.3-golang-1.8-git-2.7-phantomjs-2.1-node-7.1-postgresql-9.6"
cache:
- key: "ruby-233"
+ key: "ruby-233-with-yarn"
paths:
- vendor/ruby
+ - .yarn-cache/
variables:
MYSQL_ALLOW_EMPTY_PASSWORD: "1"
@@ -74,7 +75,7 @@ stages:
# https://docs.gitlab.com/ce/development/writing_documentation.html#testing
.except-docs: &except-docs
except:
- - /^docs\/.*/
+ - /(^docs[\/-].*|.*-docs$)/
.rspec-knapsack: &rspec-knapsack
stage: test
@@ -83,7 +84,7 @@ stages:
- JOB_NAME=( $CI_JOB_NAME )
- export CI_NODE_INDEX=${JOB_NAME[-2]}
- export CI_NODE_TOTAL=${JOB_NAME[-1]}
- - export KNAPSACK_REPORT_PATH=knapsack/${CI_PROJECT_NAME}/${JOB_NAME[0]}_${JOB_NAME[1]}_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
+ - export KNAPSACK_REPORT_PATH=knapsack/${CI_PROJECT_NAME}/${JOB_NAME[0]}_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
- export KNAPSACK_GENERATE_REPORT=true
- export CACHE_CLASSES=true
- cp ${KNAPSACK_RSPEC_SUITE_REPORT_PATH} ${KNAPSACK_REPORT_PATH}
@@ -114,7 +115,7 @@ stages:
- JOB_NAME=( $CI_JOB_NAME )
- export CI_NODE_INDEX=${JOB_NAME[-2]}
- export CI_NODE_TOTAL=${JOB_NAME[-1]}
- - export KNAPSACK_REPORT_PATH=knapsack/${CI_PROJECT_NAME}/${JOB_NAME[0]}_${JOB_NAME[1]}_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
+ - export KNAPSACK_REPORT_PATH=knapsack/${CI_PROJECT_NAME}/${JOB_NAME[0]}_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
- export KNAPSACK_GENERATE_REPORT=true
- export CACHE_CLASSES=true
- cp ${KNAPSACK_SPINACH_SUITE_REPORT_PATH} ${KNAPSACK_REPORT_PATH}
@@ -138,6 +139,13 @@ stages:
<<: *only-master-and-ee-or-mysql
<<: *except-docs
+.only-canonical-masters: &only-canonical-masters
+ only:
+ - master@gitlab-org/gitlab-ce
+ - master@gitlab-org/gitlab-ee
+ - master@gitlab/gitlabhq
+ - master@gitlab/gitlab-ee
+
# Trigger a package build on omnibus-gitlab repository
build-package:
@@ -167,17 +175,13 @@ knapsack:
update-knapsack:
<<: *knapsack-state
<<: *dedicated-runner
+ <<: *only-canonical-masters
stage: post-test
script:
- - scripts/merge-reports ${KNAPSACK_RSPEC_SUITE_REPORT_PATH} knapsack/${CI_PROJECT_NAME}/rspec_pg_node_*.json
- - scripts/merge-reports ${KNAPSACK_SPINACH_SUITE_REPORT_PATH} knapsack/${CI_PROJECT_NAME}/spinach_pg_node_*.json
+ - scripts/merge-reports ${KNAPSACK_RSPEC_SUITE_REPORT_PATH} knapsack/${CI_PROJECT_NAME}/rspec-pg_node_*.json
+ - scripts/merge-reports ${KNAPSACK_SPINACH_SUITE_REPORT_PATH} knapsack/${CI_PROJECT_NAME}/spinach-pg_node_*.json
- '[[ -z ${KNAPSACK_S3_BUCKET} ]] || scripts/sync-reports put $KNAPSACK_S3_BUCKET $KNAPSACK_RSPEC_SUITE_REPORT_PATH $KNAPSACK_SPINACH_SUITE_REPORT_PATH'
- rm -f knapsack/${CI_PROJECT_NAME}/*_node_*.json
- only:
- - master@gitlab-org/gitlab-ce
- - master@gitlab-org/gitlab-ee
- - master@gitlab/gitlabhq
- - master@gitlab/gitlab-ee
setup-test-env:
<<: *use-pg
@@ -186,7 +190,7 @@ setup-test-env:
stage: prepare
script:
- node --version
- - yarn install --pure-lockfile
+ - yarn install --pure-lockfile --cache-folder .yarn-cache
- bundle exec rake gitlab:assets:compile
- bundle exec ruby -Ispec -e 'require "spec_helper" ; TestEnv.init'
artifacts:
@@ -196,76 +200,75 @@ setup-test-env:
- public/assets
- tmp/tests
-rspec pg 0 20: *rspec-knapsack-pg
-rspec pg 1 20: *rspec-knapsack-pg
-rspec pg 2 20: *rspec-knapsack-pg
-rspec pg 3 20: *rspec-knapsack-pg
-rspec pg 4 20: *rspec-knapsack-pg
-rspec pg 5 20: *rspec-knapsack-pg
-rspec pg 6 20: *rspec-knapsack-pg
-rspec pg 7 20: *rspec-knapsack-pg
-rspec pg 8 20: *rspec-knapsack-pg
-rspec pg 9 20: *rspec-knapsack-pg
-rspec pg 10 20: *rspec-knapsack-pg
-rspec pg 11 20: *rspec-knapsack-pg
-rspec pg 12 20: *rspec-knapsack-pg
-rspec pg 13 20: *rspec-knapsack-pg
-rspec pg 14 20: *rspec-knapsack-pg
-rspec pg 15 20: *rspec-knapsack-pg
-rspec pg 16 20: *rspec-knapsack-pg
-rspec pg 17 20: *rspec-knapsack-pg
-rspec pg 18 20: *rspec-knapsack-pg
-rspec pg 19 20: *rspec-knapsack-pg
-
-rspec mysql 0 20: *rspec-knapsack-mysql
-rspec mysql 1 20: *rspec-knapsack-mysql
-rspec mysql 2 20: *rspec-knapsack-mysql
-rspec mysql 3 20: *rspec-knapsack-mysql
-rspec mysql 4 20: *rspec-knapsack-mysql
-rspec mysql 5 20: *rspec-knapsack-mysql
-rspec mysql 6 20: *rspec-knapsack-mysql
-rspec mysql 7 20: *rspec-knapsack-mysql
-rspec mysql 8 20: *rspec-knapsack-mysql
-rspec mysql 9 20: *rspec-knapsack-mysql
-rspec mysql 10 20: *rspec-knapsack-mysql
-rspec mysql 11 20: *rspec-knapsack-mysql
-rspec mysql 12 20: *rspec-knapsack-mysql
-rspec mysql 13 20: *rspec-knapsack-mysql
-rspec mysql 14 20: *rspec-knapsack-mysql
-rspec mysql 15 20: *rspec-knapsack-mysql
-rspec mysql 16 20: *rspec-knapsack-mysql
-rspec mysql 17 20: *rspec-knapsack-mysql
-rspec mysql 18 20: *rspec-knapsack-mysql
-rspec mysql 19 20: *rspec-knapsack-mysql
-
-spinach pg 0 10: *spinach-knapsack-pg
-spinach pg 1 10: *spinach-knapsack-pg
-spinach pg 2 10: *spinach-knapsack-pg
-spinach pg 3 10: *spinach-knapsack-pg
-spinach pg 4 10: *spinach-knapsack-pg
-spinach pg 5 10: *spinach-knapsack-pg
-spinach pg 6 10: *spinach-knapsack-pg
-spinach pg 7 10: *spinach-knapsack-pg
-spinach pg 8 10: *spinach-knapsack-pg
-spinach pg 9 10: *spinach-knapsack-pg
-
-spinach mysql 0 10: *spinach-knapsack-mysql
-spinach mysql 1 10: *spinach-knapsack-mysql
-spinach mysql 2 10: *spinach-knapsack-mysql
-spinach mysql 3 10: *spinach-knapsack-mysql
-spinach mysql 4 10: *spinach-knapsack-mysql
-spinach mysql 5 10: *spinach-knapsack-mysql
-spinach mysql 6 10: *spinach-knapsack-mysql
-spinach mysql 7 10: *spinach-knapsack-mysql
-spinach mysql 8 10: *spinach-knapsack-mysql
-spinach mysql 9 10: *spinach-knapsack-mysql
-
-# Other generic tests
+rspec-pg 0 20: *rspec-knapsack-pg
+rspec-pg 1 20: *rspec-knapsack-pg
+rspec-pg 2 20: *rspec-knapsack-pg
+rspec-pg 3 20: *rspec-knapsack-pg
+rspec-pg 4 20: *rspec-knapsack-pg
+rspec-pg 5 20: *rspec-knapsack-pg
+rspec-pg 6 20: *rspec-knapsack-pg
+rspec-pg 7 20: *rspec-knapsack-pg
+rspec-pg 8 20: *rspec-knapsack-pg
+rspec-pg 9 20: *rspec-knapsack-pg
+rspec-pg 10 20: *rspec-knapsack-pg
+rspec-pg 11 20: *rspec-knapsack-pg
+rspec-pg 12 20: *rspec-knapsack-pg
+rspec-pg 13 20: *rspec-knapsack-pg
+rspec-pg 14 20: *rspec-knapsack-pg
+rspec-pg 15 20: *rspec-knapsack-pg
+rspec-pg 16 20: *rspec-knapsack-pg
+rspec-pg 17 20: *rspec-knapsack-pg
+rspec-pg 18 20: *rspec-knapsack-pg
+rspec-pg 19 20: *rspec-knapsack-pg
+
+rspec-mysql 0 20: *rspec-knapsack-mysql
+rspec-mysql 1 20: *rspec-knapsack-mysql
+rspec-mysql 2 20: *rspec-knapsack-mysql
+rspec-mysql 3 20: *rspec-knapsack-mysql
+rspec-mysql 4 20: *rspec-knapsack-mysql
+rspec-mysql 5 20: *rspec-knapsack-mysql
+rspec-mysql 6 20: *rspec-knapsack-mysql
+rspec-mysql 7 20: *rspec-knapsack-mysql
+rspec-mysql 8 20: *rspec-knapsack-mysql
+rspec-mysql 9 20: *rspec-knapsack-mysql
+rspec-mysql 10 20: *rspec-knapsack-mysql
+rspec-mysql 11 20: *rspec-knapsack-mysql
+rspec-mysql 12 20: *rspec-knapsack-mysql
+rspec-mysql 13 20: *rspec-knapsack-mysql
+rspec-mysql 14 20: *rspec-knapsack-mysql
+rspec-mysql 15 20: *rspec-knapsack-mysql
+rspec-mysql 16 20: *rspec-knapsack-mysql
+rspec-mysql 17 20: *rspec-knapsack-mysql
+rspec-mysql 18 20: *rspec-knapsack-mysql
+rspec-mysql 19 20: *rspec-knapsack-mysql
+
+spinach-pg 0 10: *spinach-knapsack-pg
+spinach-pg 1 10: *spinach-knapsack-pg
+spinach-pg 2 10: *spinach-knapsack-pg
+spinach-pg 3 10: *spinach-knapsack-pg
+spinach-pg 4 10: *spinach-knapsack-pg
+spinach-pg 5 10: *spinach-knapsack-pg
+spinach-pg 6 10: *spinach-knapsack-pg
+spinach-pg 7 10: *spinach-knapsack-pg
+spinach-pg 8 10: *spinach-knapsack-pg
+spinach-pg 9 10: *spinach-knapsack-pg
+
+spinach-mysql 0 10: *spinach-knapsack-mysql
+spinach-mysql 1 10: *spinach-knapsack-mysql
+spinach-mysql 2 10: *spinach-knapsack-mysql
+spinach-mysql 3 10: *spinach-knapsack-mysql
+spinach-mysql 4 10: *spinach-knapsack-mysql
+spinach-mysql 5 10: *spinach-knapsack-mysql
+spinach-mysql 6 10: *spinach-knapsack-mysql
+spinach-mysql 7 10: *spinach-knapsack-mysql
+spinach-mysql 8 10: *spinach-knapsack-mysql
+spinach-mysql 9 10: *spinach-knapsack-mysql
+
+# Static analysis jobs
.ruby-static-analysis: &ruby-static-analysis
variables:
SIMPLECOV: "false"
SETUP_DB: "false"
- USE_BUNDLE_INSTALL: "true"
.rake-exec: &rake-exec
<<: *ruby-static-analysis
@@ -308,7 +311,7 @@ downtime_check:
- master
- tags
- /^[\d-]+-stable(-ee)?$/
- - /^docs\/*/
+ - /(^docs[\/-].*|.*-docs$)/
ee_compat_check:
<<: *rake-exec
@@ -330,6 +333,7 @@ ee_compat_check:
paths:
- ee_compat_check/patches/*.patch
+# DB migration, rollback, and seed jobs
.db-migrate-reset: &db-migrate-reset
stage: test
<<: *dedicated-runner
@@ -337,14 +341,38 @@ ee_compat_check:
script:
- bundle exec rake db:migrate:reset
-rake pg db:migrate:reset:
+db:migrate:reset-pg:
<<: *db-migrate-reset
<<: *use-pg
-rake mysql db:migrate:reset:
+db:migrate:reset-mysql:
<<: *db-migrate-reset
<<: *use-mysql
+.migration-paths: &migration-paths
+ stage: test
+ <<: *dedicated-runner
+ variables:
+ SETUP_DB: "false"
+ <<: *only-canonical-masters
+ script:
+ - git fetch origin v8.14.10
+ - git checkout -f FETCH_HEAD
+ - bundle install $BUNDLE_INSTALL_FLAGS
+ - bundle exec rake db:drop db:create db:schema:load db:seed_fu
+ - git checkout $CI_COMMIT_SHA
+ - bundle install $BUNDLE_INSTALL_FLAGS
+ - . scripts/prepare_build.sh
+ - bundle exec rake db:migrate
+
+migration:path-pg:
+ <<: *migration-paths
+ <<: *use-pg
+
+migration:path-mysql:
+ <<: *migration-paths
+ <<: *use-mysql
+
.db-rollback: &db-rollback
stage: test
<<: *dedicated-runner
@@ -353,11 +381,11 @@ rake mysql db:migrate:reset:
- bundle exec rake db:rollback STEP=120
- bundle exec rake db:migrate
-rake pg db:rollback:
+db:rollback-pg:
<<: *db-rollback
<<: *use-pg
-rake mysql db:rollback:
+db:rollback-mysql:
<<: *db-rollback
<<: *use-mysql
@@ -379,15 +407,16 @@ rake mysql db:rollback:
paths:
- log/development.log
-rake pg db:seed_fu:
+db:seed_fu-pg:
<<: *db-seed_fu
<<: *use-pg
-rake mysql db:seed_fu:
+db:seed_fu-mysql:
<<: *db-seed_fu
<<: *use-mysql
-rake gitlab:assets:compile:
+# Frontend-related jobs
+gitlab:assets:compile:
stage: test
<<: *dedicated-runner
<<: *except-docs
@@ -400,17 +429,15 @@ rake gitlab:assets:compile:
SKIP_STORAGE_VALIDATION: "true"
WEBPACK_REPORT: "true"
script:
- - bundle exec rake yarn:install gitlab:assets:compile
+ - yarn install --pure-lockfile --production --cache-folder .yarn-cache
+ - bundle exec rake gitlab:assets:compile
artifacts:
name: webpack-report
expire_in: 31d
paths:
- webpack-report/
-rake karma:
- cache:
- paths:
- - vendor/ruby
+karma:
stage: test
<<: *use-pg
<<: *dedicated-runner
@@ -426,34 +453,6 @@ rake karma:
paths:
- coverage-javascript/
-.migration-paths: &migration-paths
- stage: test
- <<: *dedicated-runner
- variables:
- SETUP_DB: "false"
- only:
- - master@gitlab-org/gitlab-ce
- - master@gitlab-org/gitlab-ee
- - master@gitlab/gitlabhq
- - master@gitlab/gitlab-ee
- script:
- - git fetch origin v8.14.10
- - git checkout -f FETCH_HEAD
- - bundle install $BUNDLE_INSTALL_FLAGS
- - bundle exec rake db:drop db:create db:schema:load db:seed_fu
- - git checkout $CI_COMMIT_SHA
- - bundle install $BUNDLE_INSTALL_FLAGS
- - . scripts/prepare_build.sh
- - bundle exec rake db:migrate
-
-migration pg paths:
- <<: *migration-paths
- <<: *use-pg
-
-migration mysql paths:
- <<: *migration-paths
- <<: *use-mysql
-
coverage:
stage: post-test
services: []
@@ -511,8 +510,8 @@ pages:
<<: *dedicated-runner
dependencies:
- coverage
- - rake karma
- - rake gitlab:assets:compile
+ - karma
+ - gitlab:assets:compile
- lint:javascript:report
script:
- mv public/ .public/
diff --git a/.gitlab/issue_templates/Bug.md b/.gitlab/issue_templates/Bug.md
index 66e1e0e20b3..58af062e75e 100644
--- a/.gitlab/issue_templates/Bug.md
+++ b/.gitlab/issue_templates/Bug.md
@@ -40,6 +40,7 @@ logs, and code as it's very hard to read otherwise.)
#### Results of GitLab environment info
<details>
+<summary>Expand for output related to GitLab environment info</summary>
<pre>
(For installations with omnibus-gitlab package run and paste the output of:
@@ -54,6 +55,7 @@ logs, and code as it's very hard to read otherwise.)
#### Results of GitLab application Check
<details>
+<summary>Expand for output related to the GitLab application check</summary>
<pre>
(For installations with omnibus-gitlab package run and paste the output of:
diff --git a/.rubocop.yml b/.rubocop.yml
index e53af97a92c..3cdafd96456 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -494,7 +494,13 @@ Style/TrailingBlankLines:
# This cop checks for trailing comma in array and hash literals.
Style/TrailingCommaInLiteral:
- Enabled: false
+ Enabled: true
+ EnforcedStyleForMultiline: no_comma
+
+# This cop checks for trailing comma in argument lists.
+Style/TrailingCommaInArguments:
+ Enabled: true
+ EnforcedStyleForMultiline: no_comma
# Checks for %W when interpolation is not needed.
Style/UnneededCapitalW:
@@ -963,6 +969,12 @@ RSpec/DescribeSymbol:
RSpec/DescribedClass:
Enabled: true
+# Checks if an example group does not include any tests.
+RSpec/EmptyExampleGroup:
+ Enabled: true
+ CustomIncludeMethods:
+ - run_permission_checks
+
# Checks for long example.
RSpec/ExampleLength:
Enabled: false
@@ -981,6 +993,10 @@ RSpec/ExampleWording:
RSpec/ExpectActual:
Enabled: true
+# Checks for opportunities to use `expect { … }.to output`.
+RSpec/ExpectOutput:
+ Enabled: true
+
# Checks the file and folder naming of the spec file.
RSpec/FilePath:
Enabled: true
diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml
index 38b22afdf82..cf30f5728c0 100644
--- a/.rubocop_todo.yml
+++ b/.rubocop_todo.yml
@@ -10,11 +10,6 @@
RSpec/BeforeAfterAll:
Enabled: false
-# Offense count: 15
-# Configuration parameters: CustomIncludeMethods.
-RSpec/EmptyExampleGroup:
- Enabled: false
-
# Offense count: 233
RSpec/EmptyLineAfterFinalLet:
Enabled: false
@@ -23,10 +18,6 @@ RSpec/EmptyLineAfterFinalLet:
RSpec/EmptyLineAfterSubject:
Enabled: false
-# Offense count: 3
-RSpec/ExpectOutput:
- Enabled: false
-
# Offense count: 72
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: implicit, each, example
@@ -369,13 +360,6 @@ Style/SymbolProc:
Style/TernaryParentheses:
Enabled: false
-# Offense count: 53
-# Cop supports --auto-correct.
-# Configuration parameters: EnforcedStyleForMultiline, SupportedStylesForMultiline.
-# SupportedStylesForMultiline: comma, consistent_comma, no_comma
-Style/TrailingCommaInArguments:
- Enabled: false
-
# Offense count: 18
# Cop supports --auto-correct.
# Configuration parameters: AllowNamedUnderscoreVariables.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e625278a796..2843e3a3955 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,17 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
+## 9.1.4 (2017-05-12)
+
+- Fix error on CI/CD Settings page related to invalid pipeline trigger. !10948 (dosuken123)
+- Sort the network graph both by commit date and topographically. !11057
+- Fix cross referencing for private and internal projects. !11243
+- Handle incoming emails from aliases correctly.
+- Gracefully handle failures for incoming emails which do not match on the To header, and have no References header.
+- Add missing project attributes to Import/Export.
+- Fixed search terms not correctly highlighting.
+- Fixed bug where merge request JSON would be displayed.
+
## 9.1.3 (2017-05-05)
- Do not show private groups on subgroups page if user doesn't have access to.
@@ -42,6 +53,7 @@ entry.
## 9.1.0 (2017-04-22)
+- Add Jupyter notebook rendering !10017
- Added merge requests empty state. !7342
- Add option to start a new resolvable discussion in an MR. !7527
- Hide form inputs for group member without editing rights. !7816
diff --git a/Gemfile b/Gemfile
index 2be46e99c6f..9efb362e494 100644
--- a/Gemfile
+++ b/Gemfile
@@ -367,6 +367,6 @@ gem 'vmstat', '~> 2.3.0'
gem 'sys-filesystem', '~> 1.1.6'
# Gitaly GRPC client
-gem 'gitaly', '~> 0.6.0'
+gem 'gitaly', '~> 0.7.0'
gem 'toml-rb', '~> 0.3.15', require: false
diff --git a/Gemfile.lock b/Gemfile.lock
index 6c52f1d70d9..873cd8781ef 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -265,7 +265,7 @@ GEM
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
gherkin-ruby (0.3.2)
- gitaly (0.6.0)
+ gitaly (0.7.0)
google-protobuf (~> 3.1)
grpc (~> 1.0)
github-linguist (4.7.6)
@@ -924,7 +924,7 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.2.0)
- gitaly (~> 0.6.0)
+ gitaly (~> 0.7.0)
github-linguist (~> 4.7.0)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-markup (~> 1.5.1)
diff --git a/app/assets/javascripts/blob/balsamiq/balsamiq_viewer.js b/app/assets/javascripts/blob/balsamiq/balsamiq_viewer.js
index cdbfe36ca1c..c17877a276d 100644
--- a/app/assets/javascripts/blob/balsamiq/balsamiq_viewer.js
+++ b/app/assets/javascripts/blob/balsamiq/balsamiq_viewer.js
@@ -1,5 +1,3 @@
-/* global Flash */
-
import sqljs from 'sql.js';
import { template as _template } from 'underscore';
@@ -15,19 +13,27 @@ const PREVIEW_TEMPLATE = _template(`
class BalsamiqViewer {
constructor(viewer) {
this.viewer = viewer;
- this.endpoint = this.viewer.dataset.endpoint;
}
- loadFile() {
- const xhr = new XMLHttpRequest();
+ loadFile(endpoint) {
+ return new Promise((resolve, reject) => {
+ const xhr = new XMLHttpRequest();
+
+ xhr.open('GET', endpoint, true);
+ xhr.responseType = 'arraybuffer';
+ xhr.onload = loadEvent => this.fileLoaded(loadEvent, resolve, reject);
+ xhr.onerror = reject;
+
+ xhr.send();
+ });
+ }
- xhr.open('GET', this.endpoint, true);
- xhr.responseType = 'arraybuffer';
+ fileLoaded(loadEvent, resolve, reject) {
+ if (loadEvent.target.status !== 200) return reject();
- xhr.onload = this.renderFile.bind(this);
- xhr.onerror = BalsamiqViewer.onError;
+ this.renderFile(loadEvent);
- xhr.send();
+ return resolve();
}
renderFile(loadEvent) {
@@ -103,12 +109,6 @@ class BalsamiqViewer {
static parseTitle(resource) {
return JSON.parse(resource.values[0][2]).name;
}
-
- static onError() {
- const flash = new Flash('Balsamiq file could not be loaded.');
-
- return flash;
- }
}
export default BalsamiqViewer;
diff --git a/app/assets/javascripts/blob/balsamiq_viewer.js b/app/assets/javascripts/blob/balsamiq_viewer.js
index 1dacf84470f..8641a6fdae6 100644
--- a/app/assets/javascripts/blob/balsamiq_viewer.js
+++ b/app/assets/javascripts/blob/balsamiq_viewer.js
@@ -1,6 +1,22 @@
+/* global Flash */
+
import BalsamiqViewer from './balsamiq/balsamiq_viewer';
-document.addEventListener('DOMContentLoaded', () => {
- const balsamiqViewer = new BalsamiqViewer(document.getElementById('js-balsamiq-viewer'));
- balsamiqViewer.loadFile();
-});
+function onError() {
+ const flash = new window.Flash('Balsamiq file could not be loaded.');
+
+ return flash;
+}
+
+function loadBalsamiqFile() {
+ const viewer = document.getElementById('js-balsamiq-viewer');
+
+ if (!(viewer instanceof Element)) return;
+
+ const endpoint = viewer.dataset.endpoint;
+
+ const balsamiqViewer = new BalsamiqViewer(viewer);
+ balsamiqViewer.loadFile(endpoint).catch(onError);
+}
+
+$(loadBalsamiqFile);
diff --git a/app/assets/javascripts/blob/viewer/index.js b/app/assets/javascripts/blob/viewer/index.js
index 07d67d49aa5..d7c62889dde 100644
--- a/app/assets/javascripts/blob/viewer/index.js
+++ b/app/assets/javascripts/blob/viewer/index.js
@@ -1,17 +1,38 @@
/* global Flash */
export default class BlobViewer {
constructor() {
+ BlobViewer.initAuxiliaryViewer();
+
+ this.initMainViewers();
+ }
+
+ static initAuxiliaryViewer() {
+ const auxiliaryViewer = document.querySelector('.blob-viewer[data-type="auxiliary"]');
+ if (!auxiliaryViewer) return;
+
+ BlobViewer.loadViewer(auxiliaryViewer);
+ }
+
+ initMainViewers() {
+ this.$fileHolder = $('.file-holder');
+ if (!this.$fileHolder.length) return;
+
this.switcher = document.querySelector('.js-blob-viewer-switcher');
this.switcherBtns = document.querySelectorAll('.js-blob-viewer-switch-btn');
this.copySourceBtn = document.querySelector('.js-copy-blob-source-btn');
- this.simpleViewer = document.querySelector('.blob-viewer[data-type="simple"]');
- this.richViewer = document.querySelector('.blob-viewer[data-type="rich"]');
- this.$fileHolder = $('.file-holder');
- let initialViewerName = document.querySelector('.blob-viewer:not(.hidden)').getAttribute('data-type');
+ this.simpleViewer = this.$fileHolder[0].querySelector('.blob-viewer[data-type="simple"]');
+ this.richViewer = this.$fileHolder[0].querySelector('.blob-viewer[data-type="rich"]');
this.initBindings();
+ this.switchToInitialViewer();
+ }
+
+ switchToInitialViewer() {
+ const initialViewer = this.$fileHolder[0].querySelector('.blob-viewer:not(.hidden)');
+ let initialViewerName = initialViewer.getAttribute('data-type');
+
if (this.switcher && location.hash.indexOf('#L') === 0) {
initialViewerName = 'simple';
}
@@ -29,9 +50,9 @@ export default class BlobViewer {
if (this.copySourceBtn) {
this.copySourceBtn.addEventListener('click', () => {
- if (this.copySourceBtn.classList.contains('disabled')) return;
+ if (this.copySourceBtn.classList.contains('disabled')) return this.copySourceBtn.blur();
- this.switchToViewer('simple');
+ return this.switchToViewer('simple');
});
}
}
@@ -61,40 +82,13 @@ export default class BlobViewer {
$(this.copySourceBtn).tooltip('fixTitle');
}
- loadViewer(viewerParam) {
- const viewer = viewerParam;
- const url = viewer.getAttribute('data-url');
-
- if (!url || viewer.getAttribute('data-loaded') || viewer.getAttribute('data-loading')) {
- return;
- }
-
- viewer.setAttribute('data-loading', 'true');
-
- $.ajax({
- url,
- dataType: 'JSON',
- })
- .fail(() => new Flash('Error loading source view'))
- .done((data) => {
- viewer.innerHTML = data.html;
- $(viewer).syntaxHighlight();
-
- viewer.setAttribute('data-loaded', 'true');
-
- this.$fileHolder.trigger('highlight:line');
-
- this.toggleCopyButtonState();
- });
- }
-
switchToViewer(name) {
- const newViewer = document.querySelector(`.blob-viewer[data-type='${name}']`);
+ const newViewer = this.$fileHolder[0].querySelector(`.blob-viewer[data-type='${name}']`);
if (this.activeViewer === newViewer) return;
const oldButton = document.querySelector('.js-blob-viewer-switch-btn.active');
const newButton = document.querySelector(`.js-blob-viewer-switch-btn[data-viewer='${name}']`);
- const oldViewer = document.querySelector(`.blob-viewer:not([data-type='${name}'])`);
+ const oldViewer = this.$fileHolder[0].querySelector(`.blob-viewer:not([data-type='${name}'])`);
if (oldButton) {
oldButton.classList.remove('active');
@@ -115,6 +109,41 @@ export default class BlobViewer {
this.toggleCopyButtonState();
- this.loadViewer(newViewer);
+ BlobViewer.loadViewer(newViewer)
+ .then((viewer) => {
+ $(viewer).syntaxHighlight();
+
+ this.$fileHolder.trigger('highlight:line');
+ gl.utils.handleLocationHash();
+
+ this.toggleCopyButtonState();
+ })
+ .catch(() => new Flash('Error loading viewer'));
+ }
+
+ static loadViewer(viewerParam) {
+ const viewer = viewerParam;
+ const url = viewer.getAttribute('data-url');
+
+ return new Promise((resolve, reject) => {
+ if (!url || viewer.getAttribute('data-loaded') || viewer.getAttribute('data-loading')) {
+ resolve(viewer);
+ return;
+ }
+
+ viewer.setAttribute('data-loading', 'true');
+
+ $.ajax({
+ url,
+ dataType: 'JSON',
+ })
+ .fail(reject)
+ .done((data) => {
+ viewer.innerHTML = data.html;
+ viewer.setAttribute('data-loaded', 'true');
+
+ resolve(viewer);
+ });
+ });
}
}
diff --git a/app/assets/javascripts/boards/components/board_sidebar.js b/app/assets/javascripts/boards/components/board_sidebar.js
index 317cef9f227..9e2102eb077 100644
--- a/app/assets/javascripts/boards/components/board_sidebar.js
+++ b/app/assets/javascripts/boards/components/board_sidebar.js
@@ -36,12 +36,21 @@ gl.issueBoards.BoardSidebar = Vue.extend({
},
assigneeId() {
return this.issue.assignee ? this.issue.assignee.id : 0;
+ },
+ milestoneTitle() {
+ return this.issue.milestone ? this.issue.milestone.title : 'No Milestone';
}
},
watch: {
detail: {
handler () {
if (this.issue.id !== this.detail.issue.id) {
+ $('.block.assignee')
+ .find('input:not(.js-vue)[name="issue[assignee_ids][]"]')
+ .each((i, el) => {
+ $(el).remove();
+ });
+
$('.js-issue-board-sidebar', this.$el).each((i, el) => {
$(el).data('glDropdown').clearMenu();
});
@@ -56,18 +65,6 @@ gl.issueBoards.BoardSidebar = Vue.extend({
},
deep: true
},
- issue () {
- if (this.showSidebar) {
- this.$nextTick(() => {
- $('.right-sidebar').getNiceScroll(0).doScrollTop(0, 0);
- $('.right-sidebar').getNiceScroll().resize();
- });
- }
-
- this.issue = this.detail.issue;
- this.list = this.detail.list;
- },
- deep: true
},
methods: {
closeSidebar () {
diff --git a/app/assets/javascripts/boards/components/issue_card_inner.js b/app/assets/javascripts/boards/components/issue_card_inner.js
index 710207db0c7..4699ef5a51c 100644
--- a/app/assets/javascripts/boards/components/issue_card_inner.js
+++ b/app/assets/javascripts/boards/components/issue_card_inner.js
@@ -1,4 +1,5 @@
import Vue from 'vue';
+import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import eventHub from '../eventhub';
const Store = gl.issueBoards.BoardsStore;
@@ -38,6 +39,9 @@ gl.issueBoards.IssueCardInner = Vue.extend({
maxCounter: 99,
};
},
+ components: {
+ userAvatarLink,
+ },
computed: {
numberOverLimit() {
return this.issue.assignees.length - this.limitBeforeCounter;
@@ -146,23 +150,16 @@ gl.issueBoards.IssueCardInner = Vue.extend({
</span>
</h4>
<div class="card-assignee">
- <a
- class="has-tooltip js-no-trigger"
- :href="assigneeUrl(assignee)"
- :title="assigneeUrlTitle(assignee)"
+ <user-avatar-link
v-for="(assignee, index) in issue.assignees"
v-if="shouldRenderAssignee(index)"
- data-container="body"
- data-placement="bottom"
- >
- <img
- class="avatar avatar-inline s20"
- :src="assignee.avatar"
- width="20"
- height="20"
- :alt="avatarUrlTitle(assignee)"
- />
- </a>
+ class="js-no-trigger"
+ :link-href="assigneeUrl(assignee)"
+ :img-alt="avatarUrlTitle(assignee)"
+ :img-src="assignee.avatar"
+ :tooltip-text="assigneeUrlTitle(assignee)"
+ tooltip-placement="bottom"
+ />
<span
class="avatar-counter has-tooltip"
:title="assigneeCounterTooltip"
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.js b/app/assets/javascripts/commit/pipelines/pipelines_table.js
index b8be0d8a301..98698143d22 100644
--- a/app/assets/javascripts/commit/pipelines/pipelines_table.js
+++ b/app/assets/javascripts/commit/pipelines/pipelines_table.js
@@ -1,11 +1,11 @@
import Vue from 'vue';
import Visibility from 'visibilityjs';
-import PipelinesTableComponent from '../../vue_shared/components/pipelines_table';
+import pipelinesTableComponent from '../../vue_shared/components/pipelines_table';
import PipelinesService from '../../pipelines/services/pipelines_service';
import PipelineStore from '../../pipelines/stores/pipelines_store';
import eventHub from '../../pipelines/event_hub';
-import EmptyState from '../../pipelines/components/empty_state.vue';
-import ErrorState from '../../pipelines/components/error_state.vue';
+import emptyState from '../../pipelines/components/empty_state.vue';
+import errorState from '../../pipelines/components/error_state.vue';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import '../../lib/utils/common_utils';
import '../../vue_shared/vue_resource_interceptor';
@@ -23,9 +23,9 @@ import Poll from '../../lib/utils/poll';
export default Vue.component('pipelines-table', {
components: {
- 'pipelines-table-component': PipelinesTableComponent,
- 'error-state': ErrorState,
- 'empty-state': EmptyState,
+ pipelinesTableComponent,
+ errorState,
+ emptyState,
loadingIcon,
},
@@ -47,6 +47,7 @@ export default Vue.component('pipelines-table', {
hasError: false,
isMakingRequest: false,
updateGraphDropdown: false,
+ hasMadeRequest: false,
};
},
@@ -55,9 +56,15 @@ export default Vue.component('pipelines-table', {
return this.hasError && !this.isLoading;
},
+ /**
+ * Empty state is only rendered if after the first request we receive no pipelines.
+ *
+ * @return {Boolean}
+ */
shouldRenderEmptyState() {
return !this.state.pipelines.length &&
!this.isLoading &&
+ this.hasMadeRequest &&
!this.hasError;
},
@@ -94,6 +101,10 @@ export default Vue.component('pipelines-table', {
if (!Visibility.hidden()) {
this.isLoading = true;
this.poll.makeRequest();
+ } else {
+ // If tab is not visible we need to make the first request so we don't show the empty
+ // state without knowing if there are any pipelines
+ this.fetchPipelines();
}
Visibility.change(() => {
@@ -127,6 +138,8 @@ export default Vue.component('pipelines-table', {
successCallback(resp) {
const response = resp.json();
+ this.hasMadeRequest = true;
+
// depending of the endpoint the response can either bring a `pipelines` key or not.
const pipelines = response.pipelines || response;
this.store.storePipelines(pipelines);
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_code_component.js b/app/assets/javascripts/cycle_analytics/components/stage_code_component.js
index 0d9ad197abf..7c32a38fbe7 100644
--- a/app/assets/javascripts/cycle_analytics/components/stage_code_component.js
+++ b/app/assets/javascripts/cycle_analytics/components/stage_code_component.js
@@ -1,6 +1,7 @@
/* eslint-disable no-param-reassign */
import Vue from 'vue';
+import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
const global = window.gl || (window.gl = {});
global.cycleAnalytics = global.cycleAnalytics || {};
@@ -10,6 +11,9 @@ global.cycleAnalytics.StageCodeComponent = Vue.extend({
items: Array,
stage: Object,
},
+ components: {
+ userAvatarImage,
+ },
template: `
<div>
<div class="events-description">
@@ -19,7 +23,8 @@ global.cycleAnalytics.StageCodeComponent = Vue.extend({
<ul class="stage-event-list">
<li v-for="mergeRequest in items" class="stage-event-item">
<div class="item-details">
- <img class="avatar" :src="mergeRequest.author.avatarUrl">
+ <!-- FIXME: Pass an alt attribute here for accessibility -->
+ <user-avatar-image :img-src="mergeRequest.author.avatarUrl"/>
<h5 class="item-title merge-merquest-title">
<a :href="mergeRequest.url">
{{ mergeRequest.title }}
@@ -28,11 +33,11 @@ global.cycleAnalytics.StageCodeComponent = Vue.extend({
<a :href="mergeRequest.url" class="issue-link">!{{ mergeRequest.iid }}</a>
&middot;
<span>
- {{ __('OpenedNDaysAgo|Opened') }}
+ {{ s__('OpenedNDaysAgo|Opened') }}
<a :href="mergeRequest.url" class="issue-date">{{ mergeRequest.createdAt }}</a>
</span>
<span>
- {{ __('ByAuthor|by') }}
+ {{ s__('ByAuthor|by') }}
<a :href="mergeRequest.author.webUrl" class="issue-author-link">{{ mergeRequest.author.name }}</a>
</span>
</div>
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js b/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js
index ad285874643..5f4a0ac8590 100644
--- a/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js
+++ b/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js
@@ -1,6 +1,6 @@
/* eslint-disable no-param-reassign */
-
import Vue from 'vue';
+import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
const global = window.gl || (window.gl = {});
global.cycleAnalytics = global.cycleAnalytics || {};
@@ -10,6 +10,9 @@ global.cycleAnalytics.StageIssueComponent = Vue.extend({
items: Array,
stage: Object,
},
+ components: {
+ userAvatarImage,
+ },
template: `
<div>
<div class="events-description">
@@ -19,7 +22,8 @@ global.cycleAnalytics.StageIssueComponent = Vue.extend({
<ul class="stage-event-list">
<li v-for="issue in items" class="stage-event-item">
<div class="item-details">
- <img class="avatar" :src="issue.author.avatarUrl">
+ <!-- FIXME: Pass an alt attribute here for accessibility -->
+ <user-avatar-image :img-src="issue.author.avatarUrl"/>
<h5 class="item-title issue-title">
<a class="issue-title" :href="issue.url">
{{ issue.title }}
@@ -28,11 +32,11 @@ global.cycleAnalytics.StageIssueComponent = Vue.extend({
<a :href="issue.url" class="issue-link">#{{ issue.iid }}</a>
&middot;
<span>
- {{ __('OpenedNDaysAgo|Opened') }}
+ {{ s__('OpenedNDaysAgo|Opened') }}
<a :href="issue.url" class="issue-date">{{ issue.createdAt }}</a>
</span>
<span>
- {{ __('ByAuthor|by') }}
+ {{ s__('ByAuthor|by') }}
<a :href="issue.author.webUrl" class="issue-author-link">
{{ issue.author.name }}
</a>
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js b/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js
index 222084deee9..11fee5410d9 100644
--- a/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js
+++ b/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js
@@ -1,5 +1,6 @@
/* eslint-disable no-param-reassign */
import Vue from 'vue';
+import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
import iconCommit from '../svg/icon_commit.svg';
const global = window.gl || (window.gl = {});
@@ -10,11 +11,12 @@ global.cycleAnalytics.StagePlanComponent = Vue.extend({
items: Array,
stage: Object,
},
-
+ components: {
+ userAvatarImage,
+ },
data() {
return { iconCommit };
},
-
template: `
<div>
<div class="events-description">
@@ -24,17 +26,18 @@ global.cycleAnalytics.StagePlanComponent = Vue.extend({
<ul class="stage-event-list">
<li v-for="commit in items" class="stage-event-item">
<div class="item-details item-conmmit-component">
- <img class="avatar" :src="commit.author.avatarUrl">
+ <!-- FIXME: Pass an alt attribute here for accessibility -->
+ <user-avatar-image :img-src="commit.author.avatarUrl"/>
<h5 class="item-title commit-title">
<a :href="commit.commitUrl">
{{ commit.title }}
</a>
</h5>
<span>
- {{ __('FirstPushedBy|First') }}
+ {{ s__('FirstPushedBy|First') }}
<span class="commit-icon">${iconCommit}</span>
- <a :href="commit.commitUrl" class="commit-hash-link monospace">{{ commit.shortSha }}</a>
- {{ __('FirstPushedBy|pushed by') }}
+ <a :href="commit.commitUrl" class="commit-hash-link commit-sha">{{ commit.shortSha }}</a>
+ {{ s__('FirstPushedBy|pushed by') }}
<a :href="commit.author.webUrl" class="commit-author-link">
{{ commit.author.name }}
</a>
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_production_component.js b/app/assets/javascripts/cycle_analytics/components/stage_production_component.js
index a14ebc3ece9..b7ba9360f70 100644
--- a/app/assets/javascripts/cycle_analytics/components/stage_production_component.js
+++ b/app/assets/javascripts/cycle_analytics/components/stage_production_component.js
@@ -1,6 +1,6 @@
/* eslint-disable no-param-reassign */
-
import Vue from 'vue';
+import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
const global = window.gl || (window.gl = {});
global.cycleAnalytics = global.cycleAnalytics || {};
@@ -10,6 +10,9 @@ global.cycleAnalytics.StageProductionComponent = Vue.extend({
items: Array,
stage: Object,
},
+ components: {
+ userAvatarImage,
+ },
template: `
<div>
<div class="events-description">
@@ -19,7 +22,8 @@ global.cycleAnalytics.StageProductionComponent = Vue.extend({
<ul class="stage-event-list">
<li v-for="issue in items" class="stage-event-item">
<div class="item-details">
- <img class="avatar" :src="issue.author.avatarUrl">
+ <!-- FIXME: Pass an alt attribute here for accessibility -->
+ <user-avatar-image :img-src="issue.author.avatarUrl"/>
<h5 class="item-title issue-title">
<a class="issue-title" :href="issue.url">
{{ issue.title }}
@@ -28,11 +32,11 @@ global.cycleAnalytics.StageProductionComponent = Vue.extend({
<a :href="issue.url" class="issue-link">#{{ issue.iid }}</a>
&middot;
<span>
- {{ __('OpenedNDaysAgo|Opened') }}
+ {{ s__('OpenedNDaysAgo|Opened') }}
<a :href="issue.url" class="issue-date">{{ issue.createdAt }}</a>
</span>
<span>
- {{ __('ByAuthor|by') }}
+ {{ s__('ByAuthor|by') }}
<a :href="issue.author.webUrl" class="issue-author-link">
{{ issue.author.name }}
</a>
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_review_component.js b/app/assets/javascripts/cycle_analytics/components/stage_review_component.js
index 1a5bf9bc0b5..f41a0d0e4ff 100644
--- a/app/assets/javascripts/cycle_analytics/components/stage_review_component.js
+++ b/app/assets/javascripts/cycle_analytics/components/stage_review_component.js
@@ -1,6 +1,6 @@
/* eslint-disable no-param-reassign */
-
import Vue from 'vue';
+import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
const global = window.gl || (window.gl = {});
global.cycleAnalytics = global.cycleAnalytics || {};
@@ -10,6 +10,9 @@ global.cycleAnalytics.StageReviewComponent = Vue.extend({
items: Array,
stage: Object,
},
+ components: {
+ userAvatarImage,
+ },
template: `
<div>
<div class="events-description">
@@ -19,7 +22,8 @@ global.cycleAnalytics.StageReviewComponent = Vue.extend({
<ul class="stage-event-list">
<li v-for="mergeRequest in items" class="stage-event-item">
<div class="item-details">
- <img class="avatar" :src="mergeRequest.author.avatarUrl">
+ <!-- FIXME: Pass an alt attribute here for accessibility -->
+ <user-avatar-image :img-src="mergeRequest.author.avatarUrl"/>
<h5 class="item-title merge-merquest-title">
<a :href="mergeRequest.url">
{{ mergeRequest.title }}
@@ -28,11 +32,11 @@ global.cycleAnalytics.StageReviewComponent = Vue.extend({
<a :href="mergeRequest.url" class="issue-link">!{{ mergeRequest.iid }}</a>
&middot;
<span>
- {{ __('OpenedNDaysAgo|Opened') }}
+ {{ s__('OpenedNDaysAgo|Opened') }}
<a :href="mergeRequest.url" class="issue-date">{{ mergeRequest.createdAt }}</a>
</span>
<span>
- {{ __('ByAuthor|by') }}
+ {{ s__('ByAuthor|by') }}
<a :href="mergeRequest.author.webUrl" class="issue-author-link">{{ mergeRequest.author.name }}</a>
</span>
<template v-if="mergeRequest.state === 'closed'">
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js b/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js
index b1e9362434f..d7c906c9d39 100644
--- a/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js
+++ b/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js
@@ -1,5 +1,6 @@
/* eslint-disable no-param-reassign */
import Vue from 'vue';
+import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
import iconBranch from '../svg/icon_branch.svg';
const global = window.gl || (window.gl = {});
@@ -13,6 +14,9 @@ global.cycleAnalytics.StageStagingComponent = Vue.extend({
data() {
return { iconBranch };
},
+ components: {
+ userAvatarImage,
+ },
template: `
<div>
<div class="events-description">
@@ -22,17 +26,18 @@ global.cycleAnalytics.StageStagingComponent = Vue.extend({
<ul class="stage-event-list">
<li v-for="build in items" class="stage-event-item item-build-component">
<div class="item-details">
- <img class="avatar" :src="build.author.avatarUrl">
+ <!-- FIXME: Pass an alt attribute here for accessibility -->
+ <user-avatar-image :img-src="build.author.avatarUrl"/>
<h5 class="item-title">
<a :href="build.url" class="pipeline-id">#{{ build.id }}</a>
<i class="fa fa-code-fork"></i>
- <a :href="build.branch.url" class="branch-name monospace">{{ build.branch.name }}</a>
+ <a :href="build.branch.url" class="ref-name">{{ build.branch.name }}</a>
<span class="icon-branch">${iconBranch}</span>
- <a :href="build.commitUrl" class="short-sha monospace">{{ build.shortSha }}</a>
+ <a :href="build.commitUrl" class="commit-sha">{{ build.shortSha }}</a>
</h5>
<span>
<a :href="build.url" class="build-date">{{ build.date }}</a>
- {{ __('ByAuthor|by') }}
+ {{ s__('ByAuthor|by') }}
<a :href="build.author.webUrl" class="issue-author-link">
{{ build.author.name }}
</a>
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_test_component.js b/app/assets/javascripts/cycle_analytics/components/stage_test_component.js
index e306026429e..78cc97eea0b 100644
--- a/app/assets/javascripts/cycle_analytics/components/stage_test_component.js
+++ b/app/assets/javascripts/cycle_analytics/components/stage_test_component.js
@@ -29,9 +29,9 @@ global.cycleAnalytics.StageTestComponent = Vue.extend({
&middot;
<a :href="build.url" class="pipeline-id">#{{ build.id }}</a>
<i class="fa fa-code-fork"></i>
- <a :href="build.branch.url" class="branch-name monospace">{{ build.branch.name }}</a>
+ <a :href="build.branch.url" class="ref-name">{{ build.branch.name }}</a>
<span class="icon-branch">${iconBranch}</span>
- <a :href="build.commitUrl" class="short-sha monospace">{{ build.shortSha }}</a>
+ <a :href="build.commitUrl" class="commit-sha">{{ build.shortSha }}</a>
</h5>
<span>
<a :href="build.url" class="issue-date">
diff --git a/app/assets/javascripts/diff_notes/components/diff_note_avatars.js b/app/assets/javascripts/diff_notes/components/diff_note_avatars.js
index f3a688fbf2f..517bdb6be09 100644
--- a/app/assets/javascripts/diff_notes/components/diff_note_avatars.js
+++ b/app/assets/javascripts/diff_notes/components/diff_note_avatars.js
@@ -3,6 +3,7 @@
import Vue from 'vue';
import collapseIcon from '../icons/collapse_icon.svg';
+import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
const DiffNoteAvatars = Vue.extend({
props: ['discussionId'],
@@ -15,22 +16,24 @@ const DiffNoteAvatars = Vue.extend({
collapseIcon,
};
},
+ components: {
+ userAvatarImage,
+ },
template: `
<div class="diff-comment-avatar-holders"
v-show="notesCount !== 0">
<div v-if="!isVisible">
- <img v-for="note in notesSubset"
- class="avatar diff-comment-avatar has-tooltip js-diff-comment-avatar"
- width="19"
- height="19"
- role="button"
- data-container="body"
- data-placement="top"
- data-html="true"
+ <!-- FIXME: Pass an alt attribute here for accessibility -->
+ <user-avatar-image
+ v-for="note in notesSubset"
+ class="diff-comment-avatar js-diff-comment-avatar"
+ @click.native="clickedAvatar($event)"
+ :img-src="note.authorAvatar"
+ :tooltip-text="getTooltipText(note)"
:data-line-type="lineType"
- :title="note.authorName + ': ' + note.noteTruncated"
- :src="note.authorAvatar"
- @click="clickedAvatar($event)" />
+ :size="19"
+ data-html="true"
+ />
<span v-if="notesCount > shownAvatars"
class="diff-comments-more-count has-tooltip js-diff-comment-avatar"
data-container="body"
@@ -120,7 +123,7 @@ const DiffNoteAvatars = Vue.extend({
},
methods: {
clickedAvatar(e) {
- notes.addDiffNote(e);
+ notes.onAddDiffNote(e);
// Toggle the active state of the toggle all button
this.toggleDiscussionsToggleState();
@@ -150,6 +153,9 @@ const DiffNoteAvatars = Vue.extend({
setDiscussionVisible() {
this.isVisible = $(`.diffs .notes[data-discussion-id="${this.discussion.id}"]`).is(':visible');
},
+ getTooltipText(note) {
+ return `${note.authorName}: ${note.noteTruncated}`;
+ },
},
});
diff --git a/app/assets/javascripts/diff_notes/diff_notes_bundle.js b/app/assets/javascripts/diff_notes/diff_notes_bundle.js
index b6b47e2da6f..fdd27534e0e 100644
--- a/app/assets/javascripts/diff_notes/diff_notes_bundle.js
+++ b/app/assets/javascripts/diff_notes/diff_notes_bundle.js
@@ -65,4 +65,6 @@ $(() => {
'resolve-count': ResolveCount
}
});
+
+ $(window).trigger('resize.nav');
});
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js
index abb871c3af0..360a1a237f7 100644
--- a/app/assets/javascripts/dispatcher.js
+++ b/app/assets/javascripts/dispatcher.js
@@ -14,7 +14,6 @@
/* global NotificationsForm */
/* global TreeView */
/* global NotificationsDropdown */
-/* global UsersSelect */
/* global GroupAvatar */
/* global LineHighlighter */
/* global ProjectFork */
@@ -52,6 +51,9 @@ import ShortcutsWiki from './shortcuts_wiki';
import Pipelines from './pipelines';
import BlobViewer from './blob/viewer/index';
import AutoWidthDropdownSelect from './issuable/auto_width_dropdown_select';
+import UsersSelect from './users_select';
+import RefSelectDropdown from './ref_select_dropdown';
+import GfmAutoComplete from './gfm_auto_complete';
const ShortcutsBlob = require('./shortcuts_blob');
@@ -78,6 +80,8 @@ const ShortcutsBlob = require('./shortcuts_blob');
path = page.split(':');
shortcut_handler = null;
+ new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources).setup();
+
function initBlob() {
new LineHighlighter();
@@ -113,6 +117,7 @@ const ShortcutsBlob = require('./shortcuts_blob');
case 'projects:boards:show':
case 'projects:boards:index':
shortcut_handler = new ShortcutsNavigation();
+ new UsersSelect();
break;
case 'projects:builds:show':
new Build();
@@ -127,6 +132,7 @@ const ShortcutsBlob = require('./shortcuts_blob');
prefixId: page === 'projects:merge_requests:index' ? 'merge_request_' : 'issue_',
});
shortcut_handler = new ShortcutsNavigation();
+ new UsersSelect();
break;
case 'projects:issues:show':
new Issue();
@@ -139,6 +145,10 @@ const ShortcutsBlob = require('./shortcuts_blob');
new Milestone();
new Sidebar();
break;
+ case 'groups:issues':
+ case 'groups:merge_requests':
+ new UsersSelect();
+ break;
case 'dashboard:todos:index':
new gl.Todos();
break;
@@ -206,6 +216,7 @@ const ShortcutsBlob = require('./shortcuts_blob');
case 'projects:tags:new':
new ZenMode();
new gl.GLForm($('.tag-form'));
+ new RefSelectDropdown($('.js-branch-select'), window.gl.availableRefs);
break;
case 'projects:releases:edit':
new ZenMode();
@@ -223,6 +234,10 @@ const ShortcutsBlob = require('./shortcuts_blob');
case 'dashboard:activity':
new gl.Activities();
break;
+ case 'dashboard:issues':
+ case 'dashboard:merge_requests':
+ new UsersSelect();
+ break;
case 'projects:commit:show':
new Commit();
new gl.Diff();
@@ -247,6 +262,9 @@ const ShortcutsBlob = require('./shortcuts_blob');
if ($('#tree-slider').length) {
new TreeView();
}
+ if ($('.blob-viewer').length) {
+ new BlobViewer();
+ }
break;
case 'projects:pipelines:builds':
case 'projects:pipelines:failures':
@@ -300,6 +318,7 @@ const ShortcutsBlob = require('./shortcuts_blob');
case 'projects:tree:show':
shortcut_handler = new ShortcutsNavigation();
new TreeView();
+ new BlobViewer();
gl.TargetBranchDropDown.bootstrap();
break;
case 'projects:find_file:show':
@@ -375,6 +394,9 @@ const ShortcutsBlob = require('./shortcuts_blob');
new LineHighlighter();
new BlobViewer();
break;
+ case 'import:fogbugz:new_user_map':
+ new UsersSelect();
+ break;
}
switch (path.first()) {
case 'sessions':
diff --git a/app/assets/javascripts/droplab/drop_down.js b/app/assets/javascripts/droplab/drop_down.js
index de3927d683c..70cd337fb8a 100644
--- a/app/assets/javascripts/droplab/drop_down.js
+++ b/app/assets/javascripts/droplab/drop_down.js
@@ -1,44 +1,42 @@
-/* eslint-disable */
-
import utils from './utils';
import { SELECTED_CLASS, IGNORE_CLASS } from './constants';
-var DropDown = function(list) {
- this.currentIndex = 0;
- this.hidden = true;
- this.list = typeof list === 'string' ? document.querySelector(list) : list;
- this.items = [];
+class DropDown {
+ constructor(list) {
+ this.currentIndex = 0;
+ this.hidden = true;
+ this.list = typeof list === 'string' ? document.querySelector(list) : list;
+ this.items = [];
- this.eventWrapper = {};
+ this.eventWrapper = {};
- this.getItems();
- this.initTemplateString();
- this.addEvents();
+ this.getItems();
+ this.initTemplateString();
+ this.addEvents();
- this.initialState = list.innerHTML;
-};
+ this.initialState = list.innerHTML;
+ }
-Object.assign(DropDown.prototype, {
- getItems: function() {
+ getItems() {
this.items = [].slice.call(this.list.querySelectorAll('li'));
return this.items;
- },
+ }
- initTemplateString: function() {
- var items = this.items || this.getItems();
+ initTemplateString() {
+ const items = this.items || this.getItems();
- var templateString = '';
+ let templateString = '';
if (items.length > 0) templateString = items[items.length - 1].outerHTML;
this.templateString = templateString;
return this.templateString;
- },
+ }
- clickEvent: function(e) {
+ clickEvent(e) {
if (e.target.tagName === 'UL') return;
if (e.target.classList.contains(IGNORE_CLASS)) return;
- var selected = utils.closest(e.target, 'LI');
+ const selected = utils.closest(e.target, 'LI');
if (!selected) return;
this.addSelectedClass(selected);
@@ -46,95 +44,95 @@ Object.assign(DropDown.prototype, {
e.preventDefault();
this.hide();
- var listEvent = new CustomEvent('click.dl', {
+ const listEvent = new CustomEvent('click.dl', {
detail: {
list: this,
- selected: selected,
+ selected,
data: e.target.dataset,
},
});
this.list.dispatchEvent(listEvent);
- },
+ }
- addSelectedClass: function (selected) {
+ addSelectedClass(selected) {
this.removeSelectedClasses();
selected.classList.add(SELECTED_CLASS);
- },
+ }
- removeSelectedClasses: function () {
+ removeSelectedClasses() {
const items = this.items || this.getItems();
items.forEach(item => item.classList.remove(SELECTED_CLASS));
- },
+ }
- addEvents: function() {
- this.eventWrapper.clickEvent = this.clickEvent.bind(this)
+ addEvents() {
+ this.eventWrapper.clickEvent = this.clickEvent.bind(this);
this.list.addEventListener('click', this.eventWrapper.clickEvent);
- },
-
- toggle: function() {
- this.hidden ? this.show() : this.hide();
- },
+ }
- setData: function(data) {
+ setData(data) {
this.data = data;
this.render(data);
- },
+ }
- addData: function(data) {
+ addData(data) {
this.data = (this.data || []).concat(data);
this.render(this.data);
- },
+ }
- render: function(data) {
+ render(data) {
const children = data ? data.map(this.renderChildren.bind(this)) : [];
const renderableList = this.list.querySelector('ul[data-dynamic]') || this.list;
renderableList.innerHTML = children.join('');
- },
+ }
- renderChildren: function(data) {
- var html = utils.template(this.templateString, data);
- var template = document.createElement('div');
+ renderChildren(data) {
+ const html = utils.template(this.templateString, data);
+ const template = document.createElement('div');
template.innerHTML = html;
- this.setImagesSrc(template);
+ DropDown.setImagesSrc(template);
template.firstChild.style.display = data.droplab_hidden ? 'none' : 'block';
return template.firstChild.outerHTML;
- },
-
- setImagesSrc: function(template) {
- const images = [].slice.call(template.querySelectorAll('img[data-src]'));
-
- images.forEach((image) => {
- image.src = image.getAttribute('data-src');
- image.removeAttribute('data-src');
- });
- },
+ }
- show: function() {
+ show() {
if (!this.hidden) return;
this.list.style.display = 'block';
this.currentIndex = 0;
this.hidden = false;
- },
+ }
- hide: function() {
+ hide() {
if (this.hidden) return;
this.list.style.display = 'none';
this.currentIndex = 0;
this.hidden = true;
- },
+ }
- toggle: function () {
- this.hidden ? this.show() : this.hide();
- },
+ toggle() {
+ if (this.hidden) return this.show();
- destroy: function() {
+ return this.hide();
+ }
+
+ destroy() {
this.hide();
this.list.removeEventListener('click', this.eventWrapper.clickEvent);
}
-});
+
+ static setImagesSrc(template) {
+ const images = [...template.querySelectorAll('img[data-src]')];
+
+ images.forEach((image) => {
+ const img = image;
+
+ img.src = img.getAttribute('data-src');
+ img.removeAttribute('data-src');
+ });
+ }
+}
export default DropDown;
diff --git a/app/assets/javascripts/droplab/drop_lab.js b/app/assets/javascripts/droplab/drop_lab.js
index 6eb9f314af7..2a02ede72bf 100644
--- a/app/assets/javascripts/droplab/drop_lab.js
+++ b/app/assets/javascripts/droplab/drop_lab.js
@@ -1,99 +1,99 @@
-/* eslint-disable */
-
import HookButton from './hook_button';
import HookInput from './hook_input';
import utils from './utils';
import Keyboard from './keyboard';
import { DATA_TRIGGER } from './constants';
-var DropLab = function() {
- this.ready = false;
- this.hooks = [];
- this.queuedData = [];
- this.config = {};
+class DropLab {
+ constructor() {
+ this.ready = false;
+ this.hooks = [];
+ this.queuedData = [];
+ this.config = {};
- this.eventWrapper = {};
-};
+ this.eventWrapper = {};
+ }
-Object.assign(DropLab.prototype, {
- loadStatic: function(){
- var dropdownTriggers = [].slice.apply(document.querySelectorAll(`[${DATA_TRIGGER}]`));
+ loadStatic() {
+ const dropdownTriggers = [].slice.apply(document.querySelectorAll(`[${DATA_TRIGGER}]`));
this.addHooks(dropdownTriggers);
- },
+ }
- addData: function () {
- var args = [].slice.apply(arguments);
- this.applyArgs(args, '_addData');
- },
+ addData(...args) {
+ this.applyArgs(args, 'processAddData');
+ }
- setData: function() {
- var args = [].slice.apply(arguments);
- this.applyArgs(args, '_setData');
- },
+ setData(...args) {
+ this.applyArgs(args, 'processSetData');
+ }
- destroy: function() {
+ destroy() {
this.hooks.forEach(hook => hook.destroy());
this.hooks = [];
this.removeEvents();
- },
+ }
- applyArgs: function(args, methodName) {
- if (this.ready) return this[methodName].apply(this, args);
+ applyArgs(args, methodName) {
+ if (this.ready) return this[methodName](...args);
this.queuedData = this.queuedData || [];
this.queuedData.push(args);
- },
- _addData: function(trigger, data) {
- this._processData(trigger, data, 'addData');
- },
+ return this.ready;
+ }
+
+ processAddData(trigger, data) {
+ this.processData(trigger, data, 'addData');
+ }
- _setData: function(trigger, data) {
- this._processData(trigger, data, 'setData');
- },
+ processSetData(trigger, data) {
+ this.processData(trigger, data, 'setData');
+ }
- _processData: function(trigger, data, methodName) {
+ processData(trigger, data, methodName) {
this.hooks.forEach((hook) => {
if (Array.isArray(trigger)) hook.list[methodName](trigger);
if (hook.trigger.id === trigger) hook.list[methodName](data);
});
- },
+ }
- addEvents: function() {
- this.eventWrapper.documentClicked = this.documentClicked.bind(this)
+ addEvents() {
+ this.eventWrapper.documentClicked = this.documentClicked.bind(this);
document.addEventListener('click', this.eventWrapper.documentClicked);
- },
+ }
- documentClicked: function(e) {
+ documentClicked(e) {
let thisTag = e.target;
if (thisTag.tagName !== 'UL') thisTag = utils.closest(thisTag, 'UL');
- if (utils.isDropDownParts(thisTag, this.hooks) || utils.isDropDownParts(e.target, this.hooks)) return;
+ if (utils.isDropDownParts(thisTag, this.hooks)) return;
+ if (utils.isDropDownParts(e.target, this.hooks)) return;
this.hooks.forEach(hook => hook.list.hide());
- },
+ }
- removeEvents: function(){
+ removeEvents() {
document.removeEventListener('click', this.eventWrapper.documentClicked);
- },
-
- changeHookList: function(trigger, list, plugins, config) {
- const availableTrigger = typeof trigger === 'string' ? document.getElementById(trigger) : trigger;
+ }
+ changeHookList(trigger, list, plugins, config) {
+ const availableTrigger = typeof trigger === 'string' ? document.getElementById(trigger) : trigger;
this.hooks.forEach((hook, i) => {
- hook.list.list.dataset.dropdownActive = false;
+ const aHook = hook;
+
+ aHook.list.list.dataset.dropdownActive = false;
- if (hook.trigger !== availableTrigger) return;
+ if (aHook.trigger !== availableTrigger) return;
- hook.destroy();
+ aHook.destroy();
this.hooks.splice(i, 1);
this.addHook(availableTrigger, list, plugins, config);
});
- },
+ }
- addHook: function(hook, list, plugins, config) {
+ addHook(hook, list, plugins, config) {
const availableHook = typeof hook === 'string' ? document.querySelector(hook) : hook;
let availableList;
@@ -111,18 +111,18 @@ Object.assign(DropLab.prototype, {
this.hooks.push(new HookObject(availableHook, availableList, plugins, config));
return this;
- },
+ }
- addHooks: function(hooks, plugins, config) {
+ addHooks(hooks, plugins, config) {
hooks.forEach(hook => this.addHook(hook, null, plugins, config));
return this;
- },
+ }
- setConfig: function(obj){
+ setConfig(obj) {
this.config = obj;
- },
+ }
- fireReady: function() {
+ fireReady() {
const readyEvent = new CustomEvent('ready.dl', {
detail: {
dropdown: this,
@@ -131,10 +131,14 @@ Object.assign(DropLab.prototype, {
document.dispatchEvent(readyEvent);
this.ready = true;
- },
+ }
- init: function (hook, list, plugins, config) {
- hook ? this.addHook(hook, list, plugins, config) : this.loadStatic();
+ init(hook, list, plugins, config) {
+ if (hook) {
+ this.addHook(hook, list, plugins, config);
+ } else {
+ this.loadStatic();
+ }
this.addEvents();
@@ -146,7 +150,7 @@ Object.assign(DropLab.prototype, {
this.queuedData = [];
return this;
- },
-});
+ }
+}
export default DropLab;
diff --git a/app/assets/javascripts/droplab/hook.js b/app/assets/javascripts/droplab/hook.js
index 2f840083571..cf78165b0d8 100644
--- a/app/assets/javascripts/droplab/hook.js
+++ b/app/assets/javascripts/droplab/hook.js
@@ -1,22 +1,15 @@
-/* eslint-disable */
-
import DropDown from './drop_down';
-var Hook = function(trigger, list, plugins, config){
- this.trigger = trigger;
- this.list = new DropDown(list);
- this.type = 'Hook';
- this.event = 'click';
- this.plugins = plugins || [];
- this.config = config || {};
- this.id = trigger.id;
-};
-
-Object.assign(Hook.prototype, {
-
- addEvents: function(){},
-
- constructor: Hook,
-});
+class Hook {
+ constructor(trigger, list, plugins, config) {
+ this.trigger = trigger;
+ this.list = new DropDown(list);
+ this.type = 'Hook';
+ this.event = 'click';
+ this.plugins = plugins || [];
+ this.config = config || {};
+ this.id = trigger.id;
+ }
+}
export default Hook;
diff --git a/app/assets/javascripts/droplab/hook_button.js b/app/assets/javascripts/droplab/hook_button.js
index be8aead1303..af45eba74e7 100644
--- a/app/assets/javascripts/droplab/hook_button.js
+++ b/app/assets/javascripts/droplab/hook_button.js
@@ -1,65 +1,58 @@
-/* eslint-disable */
-
import Hook from './hook';
-var HookButton = function(trigger, list, plugins, config) {
- Hook.call(this, trigger, list, plugins, config);
-
- this.type = 'button';
- this.event = 'click';
+class HookButton extends Hook {
+ constructor(trigger, list, plugins, config) {
+ super(trigger, list, plugins, config);
- this.eventWrapper = {};
+ this.type = 'button';
+ this.event = 'click';
- this.addEvents();
- this.addPlugins();
-};
+ this.eventWrapper = {};
-HookButton.prototype = Object.create(Hook.prototype);
+ this.addEvents();
+ this.addPlugins();
+ }
-Object.assign(HookButton.prototype, {
- addPlugins: function() {
+ addPlugins() {
this.plugins.forEach(plugin => plugin.init(this));
- },
+ }
- clicked: function(e){
- var buttonEvent = new CustomEvent('click.dl', {
+ clicked(e) {
+ const buttonEvent = new CustomEvent('click.dl', {
detail: {
hook: this,
},
bubbles: true,
- cancelable: true
+ cancelable: true,
});
e.target.dispatchEvent(buttonEvent);
this.list.toggle();
- },
+ }
- addEvents: function(){
+ addEvents() {
this.eventWrapper.clicked = this.clicked.bind(this);
this.trigger.addEventListener('click', this.eventWrapper.clicked);
- },
+ }
- removeEvents: function(){
+ removeEvents() {
this.trigger.removeEventListener('click', this.eventWrapper.clicked);
- },
+ }
- restoreInitialState: function() {
+ restoreInitialState() {
this.list.list.innerHTML = this.list.initialState;
- },
+ }
- removePlugins: function() {
+ removePlugins() {
this.plugins.forEach(plugin => plugin.destroy());
- },
+ }
- destroy: function() {
+ destroy() {
this.restoreInitialState();
this.removeEvents();
this.removePlugins();
- },
-
- constructor: HookButton,
-});
-
+ }
+}
export default HookButton;
diff --git a/app/assets/javascripts/droplab/hook_input.js b/app/assets/javascripts/droplab/hook_input.js
index 05082334045..19131a64f2c 100644
--- a/app/assets/javascripts/droplab/hook_input.js
+++ b/app/assets/javascripts/droplab/hook_input.js
@@ -1,25 +1,23 @@
-/* eslint-disable */
-
import Hook from './hook';
-var HookInput = function(trigger, list, plugins, config) {
- Hook.call(this, trigger, list, plugins, config);
+class HookInput extends Hook {
+ constructor(trigger, list, plugins, config) {
+ super(trigger, list, plugins, config);
- this.type = 'input';
- this.event = 'input';
+ this.type = 'input';
+ this.event = 'input';
- this.eventWrapper = {};
+ this.eventWrapper = {};
- this.addEvents();
- this.addPlugins();
-};
+ this.addEvents();
+ this.addPlugins();
+ }
-Object.assign(HookInput.prototype, {
- addPlugins: function() {
+ addPlugins() {
this.plugins.forEach(plugin => plugin.init(this));
- },
+ }
- addEvents: function(){
+ addEvents() {
this.eventWrapper.mousedown = this.mousedown.bind(this);
this.eventWrapper.input = this.input.bind(this);
this.eventWrapper.keyup = this.keyup.bind(this);
@@ -29,19 +27,19 @@ Object.assign(HookInput.prototype, {
this.trigger.addEventListener('input', this.eventWrapper.input);
this.trigger.addEventListener('keyup', this.eventWrapper.keyup);
this.trigger.addEventListener('keydown', this.eventWrapper.keydown);
- },
+ }
- removeEvents: function() {
+ removeEvents() {
this.hasRemovedEvents = true;
this.trigger.removeEventListener('mousedown', this.eventWrapper.mousedown);
this.trigger.removeEventListener('input', this.eventWrapper.input);
this.trigger.removeEventListener('keyup', this.eventWrapper.keyup);
this.trigger.removeEventListener('keydown', this.eventWrapper.keydown);
- },
+ }
- input: function(e) {
- if(this.hasRemovedEvents) return;
+ input(e) {
+ if (this.hasRemovedEvents) return;
this.list.show();
@@ -51,12 +49,12 @@ Object.assign(HookInput.prototype, {
text: e.target.value,
},
bubbles: true,
- cancelable: true
+ cancelable: true,
});
e.target.dispatchEvent(inputEvent);
- },
+ }
- mousedown: function(e) {
+ mousedown(e) {
if (this.hasRemovedEvents) return;
const mouseEvent = new CustomEvent('mousedown.dl', {
@@ -68,21 +66,21 @@ Object.assign(HookInput.prototype, {
cancelable: true,
});
e.target.dispatchEvent(mouseEvent);
- },
+ }
- keyup: function(e) {
+ keyup(e) {
if (this.hasRemovedEvents) return;
this.keyEvent(e, 'keyup.dl');
- },
+ }
- keydown: function(e) {
+ keydown(e) {
if (this.hasRemovedEvents) return;
this.keyEvent(e, 'keydown.dl');
- },
+ }
- keyEvent: function(e, eventName) {
+ keyEvent(e, eventName) {
this.list.show();
const keyEvent = new CustomEvent(eventName, {
@@ -96,17 +94,17 @@ Object.assign(HookInput.prototype, {
cancelable: true,
});
e.target.dispatchEvent(keyEvent);
- },
+ }
- restoreInitialState: function() {
+ restoreInitialState() {
this.list.list.innerHTML = this.list.initialState;
- },
+ }
- removePlugins: function() {
+ removePlugins() {
this.plugins.forEach(plugin => plugin.destroy());
- },
+ }
- destroy: function() {
+ destroy() {
this.restoreInitialState();
this.removeEvents();
@@ -114,6 +112,6 @@ Object.assign(HookInput.prototype, {
this.list.destroy();
}
-});
+}
export default HookInput;
diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js
index b3a76fbb43e..988c484ed85 100644
--- a/app/assets/javascripts/dropzone_input.js
+++ b/app/assets/javascripts/dropzone_input.js
@@ -5,104 +5,154 @@ require('./preview_markdown');
window.DropzoneInput = (function() {
function DropzoneInput(form) {
- var $mdArea, alertAttr, alertClass, appendToTextArea, btnAlert, child, closeAlertMessage, closeSpinner, divAlert, divHover, divSpinner, dropzone, form_dropzone, form_textarea, getFilename, handlePaste, iconPaperclip, iconSpinner, insertToTextArea, isImage, max_file_size, pasteText, uploads_path, showError, showSpinner, uploadFile, uploadProgress;
+ var updateAttachingMessage, $attachingFileMessage, $mdArea, $attachButton, $cancelButton, $retryLink, $uploadingErrorContainer, $uploadingErrorMessage, $uploadProgress, $uploadingProgressContainer, appendToTextArea, btnAlert, child, closeAlertMessage, closeSpinner, divHover, divSpinner, dropzone, $formDropzone, formTextarea, getFilename, handlePaste, iconPaperclip, iconSpinner, insertToTextArea, isImage, maxFileSize, pasteText, uploadsPath, showError, showSpinner, uploadFile;
Dropzone.autoDiscover = false;
- alertClass = "alert alert-danger alert-dismissable div-dropzone-alert";
- alertAttr = "class=\"close\" data-dismiss=\"alert\"" + "aria-hidden=\"true\"";
- divHover = "<div class=\"div-dropzone-hover\"></div>";
- divSpinner = "<div class=\"div-dropzone-spinner\"></div>";
- divAlert = "<div class=\"" + alertClass + "\"></div>";
- iconPaperclip = "<i class=\"fa fa-paperclip div-dropzone-icon\"></i>";
- iconSpinner = "<i class=\"fa fa-spinner fa-spin div-dropzone-icon\"></i>";
- uploadProgress = $("<div class=\"div-dropzone-progress\"></div>");
- btnAlert = "<button type=\"button\"" + alertAttr + ">&times;</button>";
- uploads_path = window.uploads_path || null;
- max_file_size = gon.max_file_size || 10;
- form_textarea = $(form).find(".js-gfm-input");
- form_textarea.wrap("<div class=\"div-dropzone\"></div>");
- form_textarea.on('paste', (function(_this) {
+ divHover = '<div class="div-dropzone-hover"></div>';
+ iconPaperclip = '<i class="fa fa-paperclip div-dropzone-icon"></i>';
+ $attachButton = form.find('.button-attach-file');
+ $attachingFileMessage = form.find('.attaching-file-message');
+ $cancelButton = form.find('.button-cancel-uploading-files');
+ $retryLink = form.find('.retry-uploading-link');
+ $uploadProgress = form.find('.uploading-progress');
+ $uploadingErrorContainer = form.find('.uploading-error-container');
+ $uploadingErrorMessage = form.find('.uploading-error-message');
+ $uploadingProgressContainer = form.find('.uploading-progress-container');
+ uploadsPath = window.uploads_path || null;
+ maxFileSize = gon.max_file_size || 10;
+ formTextarea = form.find('.js-gfm-input');
+ formTextarea.wrap('<div class="div-dropzone"></div>');
+ formTextarea.on('paste', (function(_this) {
return function(event) {
return handlePaste(event);
};
})(this));
- $mdArea = $(form_textarea).closest('.md-area');
- $(form).setupMarkdownPreview();
- form_dropzone = $(form).find('.div-dropzone');
- form_dropzone.parent().addClass("div-dropzone-wrapper");
- form_dropzone.append(divHover);
- form_dropzone.find(".div-dropzone-hover").append(iconPaperclip);
- form_dropzone.append(divSpinner);
- form_dropzone.find(".div-dropzone-spinner").append(iconSpinner);
- form_dropzone.find(".div-dropzone-spinner").append(uploadProgress);
- form_dropzone.find(".div-dropzone-spinner").css({
- "opacity": 0,
- "display": "none"
- });
- if (!uploads_path) return;
+ // Add dropzone area to the form.
+ $mdArea = formTextarea.closest('.md-area');
+ form.setupMarkdownPreview();
+ $formDropzone = form.find('.div-dropzone');
+ $formDropzone.parent().addClass('div-dropzone-wrapper');
+ $formDropzone.append(divHover);
+ $formDropzone.find('.div-dropzone-hover').append(iconPaperclip);
+
+ if (!uploadsPath) return;
- dropzone = form_dropzone.dropzone({
- url: uploads_path,
- dictDefaultMessage: "",
+ dropzone = $formDropzone.dropzone({
+ url: uploadsPath,
+ dictDefaultMessage: '',
clickable: true,
- paramName: "file",
- maxFilesize: max_file_size,
+ paramName: 'file',
+ maxFilesize: maxFileSize,
uploadMultiple: false,
headers: {
- "X-CSRF-Token": $("meta[name=\"csrf-token\"]").attr("content")
+ 'X-CSRF-Token': $('meta[name="csrf-token"]').attr('content')
},
previewContainer: false,
processing: function() {
- return $(".div-dropzone-alert").alert("close");
+ return $('.div-dropzone-alert').alert('close');
},
dragover: function() {
$mdArea.addClass('is-dropzone-hover');
- form.find(".div-dropzone-hover").css("opacity", 0.7);
+ form.find('.div-dropzone-hover').css('opacity', 0.7);
},
dragleave: function() {
$mdArea.removeClass('is-dropzone-hover');
- form.find(".div-dropzone-hover").css("opacity", 0);
+ form.find('.div-dropzone-hover').css('opacity', 0);
},
drop: function() {
$mdArea.removeClass('is-dropzone-hover');
- form.find(".div-dropzone-hover").css("opacity", 0);
- form_textarea.focus();
+ form.find('.div-dropzone-hover').css('opacity', 0);
+ formTextarea.focus();
},
success: function(header, response) {
const processingFileCount = this.getQueuedFiles().length + this.getUploadingFiles().length;
const shouldPad = processingFileCount >= 1;
pasteText(response.link.markdown, shouldPad);
+ // Show 'Attach a file' link only when all files have been uploaded.
+ if (!processingFileCount) $attachButton.removeClass('hide');
},
- error: function(temp) {
- var checkIfMsgExists, errorAlert;
- errorAlert = $(form).find('.error-alert');
- checkIfMsgExists = errorAlert.children().length;
- if (checkIfMsgExists === 0) {
- errorAlert.append(divAlert);
- $(".div-dropzone-alert").append(btnAlert + "Attaching the file failed.");
- }
+ error: function(file, errorMessage = 'Attaching the file failed.', xhr) {
+ // If 'error' event is fired by dropzone, the second parameter is error message.
+ // If the 'errorMessage' parameter is empty, the default error message is set.
+ // If the 'error' event is fired by backend (xhr) error response, the third parameter is
+ // xhr object (xhr.responseText is error message).
+ // On error we hide the 'Attach' and 'Cancel' buttons
+ // and show an error.
+
+ // If there's xhr error message, let's show it instead of dropzone's one.
+ const message = xhr ? xhr.responseText : errorMessage;
+
+ $uploadingErrorContainer.removeClass('hide');
+ $uploadingErrorMessage.html(message);
+ $attachButton.addClass('hide');
+ $cancelButton.addClass('hide');
},
totaluploadprogress: function(totalUploadProgress) {
- uploadProgress.text(Math.round(totalUploadProgress) + "%");
+ updateAttachingMessage(this.files, $attachingFileMessage);
+ $uploadProgress.text(Math.round(totalUploadProgress) + '%');
+ },
+ sending: function(file) {
+ // DOM elements already exist.
+ // Instead of dynamically generating them,
+ // we just either hide or show them.
+ $attachButton.addClass('hide');
+ $uploadingErrorContainer.addClass('hide');
+ $uploadingProgressContainer.removeClass('hide');
+ $cancelButton.removeClass('hide');
},
- sending: function() {
- form_dropzone.find(".div-dropzone-spinner").css({
- "opacity": 0.7,
- "display": "inherit"
- });
+ removedfile: function() {
+ $attachButton.removeClass('hide');
+ $cancelButton.addClass('hide');
+ $uploadingProgressContainer.addClass('hide');
+ $uploadingErrorContainer.addClass('hide');
},
queuecomplete: function() {
- uploadProgress.text("");
- $(".dz-preview").remove();
- $(".markdown-area").trigger("input");
- $(".div-dropzone-spinner").css({
- "opacity": 0,
- "display": "none"
- });
+ $('.dz-preview').remove();
+ $('.markdown-area').trigger('input');
+
+ $uploadingProgressContainer.addClass('hide');
+ $cancelButton.addClass('hide');
}
});
- child = $(dropzone[0]).children("textarea");
+
+ child = $(dropzone[0]).children('textarea');
+
+ // removeAllFiles(true) stops uploading files (if any)
+ // and remove them from dropzone files queue.
+ $cancelButton.on('click', (e) => {
+ const target = e.target.closest('form').querySelector('.div-dropzone');
+
+ e.preventDefault();
+ e.stopPropagation();
+ Dropzone.forElement(target).removeAllFiles(true);
+ });
+
+ // If 'error' event is fired, we store a failed files,
+ // clear dropzone files queue, change status of failed files to undefined,
+ // and add that files to the dropzone files queue again.
+ // addFile() adds file to dropzone files queue and upload it.
+ $retryLink.on('click', (e) => {
+ const dropzoneInstance = Dropzone.forElement(e.target.closest('form').querySelector('.div-dropzone'));
+ const failedFiles = dropzoneInstance.files;
+
+ e.preventDefault();
+
+ // 'true' parameter of removeAllFiles() cancels uploading of files that are being uploaded at the moment.
+ dropzoneInstance.removeAllFiles(true);
+
+ failedFiles.map((failedFile, i) => {
+ const file = failedFile;
+
+ if (file.status === Dropzone.ERROR) {
+ file.status = undefined;
+ file.accepted = undefined;
+ }
+
+ return dropzoneInstance.addFile(file);
+ });
+ });
+
handlePaste = function(event) {
var filename, image, pasteEvent, text;
pasteEvent = event.originalEvent;
@@ -110,25 +160,27 @@ window.DropzoneInput = (function() {
image = isImage(pasteEvent);
if (image) {
event.preventDefault();
- filename = getFilename(pasteEvent) || "image.png";
- text = "{{" + filename + "}}";
+ filename = getFilename(pasteEvent) || 'image.png';
+ text = `{{${filename}}}`;
pasteText(text);
return uploadFile(image.getAsFile(), filename);
}
}
};
+
isImage = function(data) {
var i, item;
i = 0;
while (i < data.clipboardData.items.length) {
item = data.clipboardData.items[i];
- if (item.type.indexOf("image") !== -1) {
+ if (item.type.indexOf('image') !== -1) {
return item;
}
i += 1;
}
return false;
};
+
pasteText = function(text, shouldPad) {
var afterSelection, beforeSelection, caretEnd, caretStart, textEnd;
var formattedText = text;
@@ -142,31 +194,33 @@ window.DropzoneInput = (function() {
$(child).val(beforeSelection + formattedText + afterSelection);
textarea.setSelectionRange(caretStart + formattedText.length, caretEnd + formattedText.length);
textarea.style.height = `${textarea.scrollHeight}px`;
- return form_textarea.trigger("input");
+ return formTextarea.trigger('input');
};
+
getFilename = function(e) {
var value;
if (window.clipboardData && window.clipboardData.getData) {
- value = window.clipboardData.getData("Text");
+ value = window.clipboardData.getData('Text');
} else if (e.clipboardData && e.clipboardData.getData) {
- value = e.clipboardData.getData("text/plain");
+ value = e.clipboardData.getData('text/plain');
}
value = value.split("\r");
return value.first();
};
+
uploadFile = function(item, filename) {
var formData;
formData = new FormData();
- formData.append("file", item, filename);
+ formData.append('file', item, filename);
return $.ajax({
- url: uploads_path,
- type: "POST",
+ url: uploadsPath,
+ type: 'POST',
data: formData,
- dataType: "json",
+ dataType: 'json',
processData: false,
contentType: false,
headers: {
- "X-CSRF-Token": $("meta[name=\"csrf-token\"]").attr("content")
+ 'X-CSRF-Token': $('meta[name="csrf-token"]').attr('content')
},
beforeSend: function() {
showSpinner();
@@ -183,44 +237,54 @@ window.DropzoneInput = (function() {
}
});
};
+
+ updateAttachingMessage = (files, messageContainer) => {
+ let attachingMessage;
+ const filesCount = files.filter(function(file) {
+ return file.status === 'uploading' ||
+ file.status === 'queued';
+ }).length;
+
+ // Dinamycally change uploading files text depending on files number in
+ // dropzone files queue.
+ if (filesCount > 1) {
+ attachingMessage = 'Attaching ' + filesCount + ' files -';
+ } else {
+ attachingMessage = 'Attaching a file -';
+ }
+
+ messageContainer.text(attachingMessage);
+ };
+
insertToTextArea = function(filename, url) {
return $(child).val(function(index, val) {
- return val.replace("{{" + filename + "}}", url);
+ return val.replace(`{{${filename}}}`, url);
});
};
+
appendToTextArea = function(url) {
return $(child).val(function(index, val) {
return val + url + "\n";
});
};
+
showSpinner = function(e) {
- return form.find(".div-dropzone-spinner").css({
- "opacity": 0.7,
- "display": "inherit"
- });
+ return $uploadingProgressContainer.removeClass('hide');
};
+
closeSpinner = function() {
- return form.find(".div-dropzone-spinner").css({
- "opacity": 0,
- "display": "none"
- });
+ return $uploadingProgressContainer.addClass('hide');
};
+
showError = function(message) {
- var checkIfMsgExists, errorAlert;
- errorAlert = $(form).find('.error-alert');
- checkIfMsgExists = errorAlert.children().length;
- if (checkIfMsgExists === 0) {
- errorAlert.append(divAlert);
- return $(".div-dropzone-alert").append(btnAlert + message);
- }
+ $uploadingErrorContainer.removeClass('hide');
+ $uploadingErrorMessage.html(message);
};
- closeAlertMessage = function() {
- return form.find(".div-dropzone-alert").alert("close");
- };
- form.find(".markdown-selector").click(function(e) {
+
+ form.find('.markdown-selector').click(function(e) {
e.preventDefault();
$(this).closest('.gfm-form').find('.div-dropzone').click();
- form_textarea.focus();
+ formTextarea.focus();
});
}
diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue
index 0ffe9ea17fa..012ff1f975b 100644
--- a/app/assets/javascripts/environments/components/environment_item.vue
+++ b/app/assets/javascripts/environments/components/environment_item.vue
@@ -1,5 +1,7 @@
<script>
import Timeago from 'timeago.js';
+import _ from 'underscore';
+import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import '../../lib/utils/text_utility';
import ActionsComponent from './environment_actions.vue';
import ExternalUrlComponent from './environment_external_url.vue';
@@ -19,6 +21,7 @@ const timeagoInstance = new Timeago();
export default {
components: {
+ userAvatarLink,
'commit-component': CommitComponent,
'actions-component': ActionsComponent,
'external-url-component': ExternalUrlComponent,
@@ -59,7 +62,7 @@ export default {
hasLastDeploymentKey() {
if (this.model &&
this.model.last_deployment &&
- !this.$options.isObjectEmpty(this.model.last_deployment)) {
+ !_.isEmpty(this.model.last_deployment)) {
return true;
}
return false;
@@ -310,8 +313,8 @@ export default {
*/
deploymentHasUser() {
return this.model &&
- !this.$options.isObjectEmpty(this.model.last_deployment) &&
- !this.$options.isObjectEmpty(this.model.last_deployment.user);
+ !_.isEmpty(this.model.last_deployment) &&
+ !_.isEmpty(this.model.last_deployment.user);
},
/**
@@ -322,8 +325,8 @@ export default {
*/
deploymentUser() {
if (this.model &&
- !this.$options.isObjectEmpty(this.model.last_deployment) &&
- !this.$options.isObjectEmpty(this.model.last_deployment.user)) {
+ !_.isEmpty(this.model.last_deployment) &&
+ !_.isEmpty(this.model.last_deployment.user)) {
return this.model.last_deployment.user;
}
return {};
@@ -338,8 +341,8 @@ export default {
*/
shouldRenderBuildName() {
return !this.model.isFolder &&
- !this.$options.isObjectEmpty(this.model.last_deployment) &&
- !this.$options.isObjectEmpty(this.model.last_deployment.deployable);
+ !_.isEmpty(this.model.last_deployment) &&
+ !_.isEmpty(this.model.last_deployment.deployable);
},
/**
@@ -380,7 +383,7 @@ export default {
*/
shouldRenderDeploymentID() {
return !this.model.isFolder &&
- !this.$options.isObjectEmpty(this.model.last_deployment) &&
+ !_.isEmpty(this.model.last_deployment) &&
this.model.last_deployment.iid !== undefined;
},
@@ -410,21 +413,6 @@ export default {
},
},
- /**
- * Helper to verify if certain given object are empty.
- * Should be replaced by lodash _.isEmpty - https://lodash.com/docs/4.17.2#isEmpty
- * @param {Object} object
- * @returns {Bollean}
- */
- isObjectEmpty(object) {
- for (const key in object) { // eslint-disable-line
- if (hasOwnProperty.call(object, key)) {
- return false;
- }
- }
- return true;
- },
-
methods: {
onClickFolder() {
eventHub.$emit('toggleFolder', this.model, this.folderUrl);
@@ -482,15 +470,13 @@ export default {
<span v-if="!model.isFolder && deploymentHasUser">
by
- <a
- :href="deploymentUser.web_url"
- class="js-deploy-user-container">
- <img
- class="avatar has-tooltip s20"
- :src="deploymentUser.avatar_url"
- :alt="userImageAltDescription"
- :title="deploymentUser.username" />
- </a>
+ <user-avatar-link
+ class="js-deploy-user-container"
+ :link-href="deploymentUser.web_url"
+ :img-src="deploymentUser.avatar_url"
+ :img-alt="userImageAltDescription"
+ :tooltip-text="deploymentUser.username"
+ />
</span>
</td>
diff --git a/app/assets/javascripts/environments/components/environment_monitoring.vue b/app/assets/javascripts/environments/components/environment_monitoring.vue
index 4b030a27900..79c019b3491 100644
--- a/app/assets/javascripts/environments/components/environment_monitoring.vue
+++ b/app/assets/javascripts/environments/components/environment_monitoring.vue
@@ -21,7 +21,6 @@ export default {
<a
class="btn monitoring-url has-tooltip"
data-container="body"
- target="_blank"
rel="noopener noreferrer nofollow"
:href="monitoringUrl"
:title="title"
diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js
index 9fea563370f..57d247e11a9 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js
@@ -16,10 +16,14 @@ class FilteredSearchManager {
this.recentSearchesStore = new RecentSearchesStore({
isLocalStorageAvailable: RecentSearchesService.isAvailable(),
});
- let recentSearchesKey = 'issue-recent-searches';
+ const searchHistoryDropdownElement = document.querySelector('.js-filtered-search-history-dropdown');
+ const projectPath = searchHistoryDropdownElement ?
+ searchHistoryDropdownElement.dataset.projectFullPath : 'project';
+ let recentSearchesPagePrefix = 'issue-recent-searches';
if (page === 'merge_requests') {
- recentSearchesKey = 'merge-request-recent-searches';
+ recentSearchesPagePrefix = 'merge-request-recent-searches';
}
+ const recentSearchesKey = `${projectPath}-${recentSearchesPagePrefix}`;
this.recentSearchesService = new RecentSearchesService(recentSearchesKey);
// Fetch recent searches from localStorage
@@ -47,7 +51,7 @@ class FilteredSearchManager {
this.recentSearchesRoot = new RecentSearchesRoot(
this.recentSearchesStore,
this.recentSearchesService,
- document.querySelector('.js-filtered-search-history-dropdown'),
+ searchHistoryDropdownElement,
);
this.recentSearchesRoot.init();
diff --git a/app/assets/javascripts/filtered_search/stores/recent_searches_store.js b/app/assets/javascripts/filtered_search/stores/recent_searches_store.js
index 066be69766a..35fc15e4c87 100644
--- a/app/assets/javascripts/filtered_search/stores/recent_searches_store.js
+++ b/app/assets/javascripts/filtered_search/stores/recent_searches_store.js
@@ -3,6 +3,7 @@ import _ from 'underscore';
class RecentSearchesStore {
constructor(initialState = {}) {
this.state = Object.assign({
+ isLocalStorageAvailable: true,
recentSearches: [],
}, initialState);
}
diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js
index f1b99023c72..b8a923cf619 100644
--- a/app/assets/javascripts/gfm_auto_complete.js
+++ b/app/assets/javascripts/gfm_auto_complete.js
@@ -1,119 +1,33 @@
-/* eslint-disable func-names, space-before-function-paren, no-template-curly-in-string, comma-dangle, object-shorthand, quotes, dot-notation, no-else-return, one-var, no-var, no-underscore-dangle, one-var-declaration-per-line, no-param-reassign, no-useless-escape, prefer-template, consistent-return, wrap-iife, prefer-arrow-callback, camelcase, no-unused-vars, no-useless-return, vars-on-top, max-len */
-
import emojiMap from 'emojis/digests.json';
import emojiAliases from 'emojis/aliases.json';
import { glEmojiTag } from '~/behaviors/gl_emoji';
import glRegexp from '~/lib/utils/regexp';
-// Creates the variables for setting up GFM auto-completion
-window.gl = window.gl || {};
-
function sanitize(str) {
return str.replace(/<(?:.|\n)*?>/gm, '');
}
-window.gl.GfmAutoComplete = {
- dataSources: {},
- defaultLoadingData: ['loading'],
- cachedData: {},
- isLoadingData: {},
- atTypeMap: {
- ':': 'emojis',
- '@': 'members',
- '#': 'issues',
- '!': 'mergeRequests',
- '~': 'labels',
- '%': 'milestones',
- '/': 'commands'
- },
- // Emoji
- Emoji: {
- templateFunction: function(name) {
- return `<li>
- ${name} ${glEmojiTag(name)}
- </li>
- `;
- }
- },
- // Team Members
- Members: {
- template: '<li>${avatarTag} ${username} <small>${title}</small></li>'
- },
- Labels: {
- template: '<li><span class="dropdown-label-box" style="background: ${color}"></span> ${title}</li>'
- },
- // Issues and MergeRequests
- Issues: {
- template: '<li><small>${id}</small> ${title}</li>'
- },
- // Milestones
- Milestones: {
- template: '<li>${title}</li>'
- },
- Loading: {
- template: '<li style="pointer-events: none;"><i class="fa fa-spinner fa-spin"></i> Loading...</li>'
- },
- DefaultOptions: {
- sorter: function(query, items, searchKey) {
- this.setting.highlightFirst = this.setting.alwaysHighlightFirst || query.length > 0;
- if (gl.GfmAutoComplete.isLoading(items)) {
- this.setting.highlightFirst = false;
- return items;
- }
- return $.fn.atwho["default"].callbacks.sorter(query, items, searchKey);
- },
- filter: function(query, data, searchKey) {
- if (gl.GfmAutoComplete.isLoading(data)) {
- gl.GfmAutoComplete.fetchData(this.$inputor, this.at);
- return data;
- } else {
- return $.fn.atwho["default"].callbacks.filter(query, data, searchKey);
- }
- },
- beforeInsert: function(value) {
- if (value && !this.setting.skipSpecialCharacterTest) {
- var withoutAt = value.substring(1);
- if (withoutAt && /[^\w\d]/.test(withoutAt)) value = value.charAt() + '"' + withoutAt + '"';
- }
- return value;
- },
- matcher: function (flag, subtext) {
- // The below is taken from At.js source
- // Tweaked to commands to start without a space only if char before is a non-word character
- // https://github.com/ichord/At.js
- var _a, _y, regexp, match, atSymbolsWithBar, atSymbolsWithoutBar;
- atSymbolsWithBar = Object.keys(this.app.controllers).join('|');
- atSymbolsWithoutBar = Object.keys(this.app.controllers).join('');
- subtext = subtext.split(/\s+/g).pop();
- flag = flag.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
-
- _a = decodeURI("%C3%80");
- _y = decodeURI("%C3%BF");
-
- regexp = new RegExp("^(?:\\B|[^a-zA-Z0-9_" + atSymbolsWithoutBar + "]|\\s)" + flag + "(?!" + atSymbolsWithBar + ")((?:[A-Za-z" + _a + "-" + _y + "0-9_\'\.\+\-]|[^\\x00-\\x7a])*)$", 'gi');
-
- match = regexp.exec(subtext);
+class GfmAutoComplete {
+ constructor(dataSources) {
+ this.dataSources = dataSources || {};
+ this.cachedData = {};
+ this.isLoadingData = {};
+ }
- if (match) {
- return match[1];
- } else {
- return null;
- }
- }
- },
- setup: function(input, enableMap = {
+ setup(input, enableMap = {
emojis: true,
members: true,
issues: true,
milestones: true,
mergeRequests: true,
- labels: true
+ labels: true,
}) {
// Add GFM auto-completion to all input fields, that accept GFM input.
this.input = input || $('.js-gfm-input');
this.enableMap = enableMap;
this.setupLifecycle();
- },
+ }
+
setupLifecycle() {
this.input.each((i, input) => {
const $input = $(input);
@@ -122,9 +36,9 @@ window.gl.GfmAutoComplete = {
// Needed for slash commands with suffixes (ex: /label ~)
$input.on('inserted-commands.atwho', $input.trigger.bind($input, 'keyup'));
});
- },
+ }
- setupAtWho: function($input) {
+ setupAtWho($input) {
if (this.enableMap.emojis) this.setupEmoji($input);
if (this.enableMap.members) this.setupMembers($input);
if (this.enableMap.issues) this.setupIssues($input);
@@ -138,10 +52,11 @@ window.gl.GfmAutoComplete = {
alias: 'commands',
searchKey: 'search',
skipSpecialCharacterTest: true,
- data: this.defaultLoadingData,
- displayTpl: function(value) {
- if (this.isLoading(value)) return this.Loading.template;
- var tpl = '<li>/${name}';
+ data: GfmAutoComplete.defaultLoadingData,
+ displayTpl(value) {
+ if (GfmAutoComplete.isLoading(value)) return GfmAutoComplete.Loading.template;
+ // eslint-disable-next-line no-template-curly-in-string
+ let tpl = '<li>/${name}';
if (value.aliases.length > 0) {
tpl += ' <small>(or /<%- aliases.join(", /") %>)</small>';
}
@@ -153,105 +68,106 @@ window.gl.GfmAutoComplete = {
}
tpl += '</li>';
return _.template(tpl)(value);
- }.bind(this),
- insertTpl: function(value) {
- var tpl = "/${name} ";
- var reference_prefix = null;
+ },
+ insertTpl(value) {
+ // eslint-disable-next-line no-template-curly-in-string
+ let tpl = '/${name} ';
+ let referencePrefix = null;
if (value.params.length > 0) {
- reference_prefix = value.params[0][0];
- if (/^[@%~]/.test(reference_prefix)) {
- tpl += '<%- reference_prefix %>';
+ referencePrefix = value.params[0][0];
+ if (/^[@%~]/.test(referencePrefix)) {
+ tpl += '<%- referencePrefix %>';
}
}
- return _.template(tpl)({ reference_prefix: reference_prefix });
+ return _.template(tpl)({ referencePrefix });
},
suffix: '',
callbacks: {
- sorter: this.DefaultOptions.sorter,
- filter: this.DefaultOptions.filter,
- beforeInsert: this.DefaultOptions.beforeInsert,
- beforeSave: function(commands) {
- if (gl.GfmAutoComplete.isLoading(commands)) return commands;
- return $.map(commands, function(c) {
- var search = c.name;
+ ...this.getDefaultCallbacks(),
+ beforeSave(commands) {
+ if (GfmAutoComplete.isLoading(commands)) return commands;
+ return $.map(commands, (c) => {
+ let search = c.name;
if (c.aliases.length > 0) {
- search = search + " " + c.aliases.join(" ");
+ search = `${search} ${c.aliases.join(' ')}`;
}
return {
name: c.name,
aliases: c.aliases,
params: c.params,
description: c.description,
- search: search
+ search,
};
});
},
- matcher: function(flag, subtext, should_startWithSpace, acceptSpaceBar) {
- var regexp = /(?:^|\n)\/([A-Za-z_]*)$/gi;
- var match = regexp.exec(subtext);
+ matcher(flag, subtext) {
+ const regexp = /(?:^|\n)\/([A-Za-z_]*)$/gi;
+ const match = regexp.exec(subtext);
if (match) {
return match[1];
- } else {
- return null;
}
- }
- }
+ return null;
+ },
+ },
});
- return;
- },
+ }
setupEmoji($input) {
// Emoji
$input.atwho({
at: ':',
- displayTpl: function(value) {
- return value && value.name ? this.Emoji.templateFunction(value.name) : this.Loading.template;
- }.bind(this),
+ displayTpl(value) {
+ let tmpl = GfmAutoComplete.Loading.template;
+ if (value && value.name) {
+ tmpl = GfmAutoComplete.Emoji.templateFunction(value.name);
+ }
+ return tmpl;
+ },
+ // eslint-disable-next-line no-template-curly-in-string
insertTpl: ':${name}:',
skipSpecialCharacterTest: true,
- data: this.defaultLoadingData,
+ data: GfmAutoComplete.defaultLoadingData,
callbacks: {
- sorter: this.DefaultOptions.sorter,
- beforeInsert: this.DefaultOptions.beforeInsert,
- filter: this.DefaultOptions.filter,
-
- matcher: (flag, subtext) => {
+ ...this.getDefaultCallbacks(),
+ matcher(flag, subtext) {
const relevantText = subtext.trim().split(/\s/).pop();
const regexp = new RegExp(`(?:[^${glRegexp.unicodeLetters}0-9:]|\n|^):([^:]*)$`, 'gi');
const match = regexp.exec(relevantText);
return match && match.length ? match[1] : null;
- }
- }
+ },
+ },
});
- },
+ }
setupMembers($input) {
// Team Members
$input.atwho({
at: '@',
- displayTpl: function(value) {
- return value.username != null ? this.Members.template : this.Loading.template;
- }.bind(this),
+ displayTpl(value) {
+ let tmpl = GfmAutoComplete.Loading.template;
+ if (value.username != null) {
+ tmpl = GfmAutoComplete.Members.template;
+ }
+ return tmpl;
+ },
+ // eslint-disable-next-line no-template-curly-in-string
insertTpl: '${atwho-at}${username}',
searchKey: 'search',
alwaysHighlightFirst: true,
skipSpecialCharacterTest: true,
- data: this.defaultLoadingData,
+ data: GfmAutoComplete.defaultLoadingData,
callbacks: {
- sorter: this.DefaultOptions.sorter,
- filter: this.DefaultOptions.filter,
- beforeInsert: this.DefaultOptions.beforeInsert,
- matcher: this.DefaultOptions.matcher,
- beforeSave: function(members) {
- return $.map(members, function(m) {
+ ...this.getDefaultCallbacks(),
+ beforeSave(members) {
+ return $.map(members, (m) => {
let title = '';
if (m.username == null) {
return m;
}
title = m.name;
if (m.count) {
- title += " (" + m.count + ")";
+ title += ` (${m.count})`;
}
const autoCompleteAvatar = m.avatar_url || m.username.charAt(0).toUpperCase();
@@ -262,173 +178,271 @@ window.gl.GfmAutoComplete = {
username: m.username,
avatarTag: autoCompleteAvatar.length === 1 ? txtAvatar : imgAvatar,
title: sanitize(title),
- search: sanitize(m.username + " " + m.name)
+ search: sanitize(`${m.username} ${m.name}`),
};
});
- }
- }
+ },
+ },
});
- },
+ }
setupIssues($input) {
$input.atwho({
at: '#',
alias: 'issues',
searchKey: 'search',
- displayTpl: function(value) {
- return value.title != null ? this.Issues.template : this.Loading.template;
- }.bind(this),
- data: this.defaultLoadingData,
+ displayTpl(value) {
+ let tmpl = GfmAutoComplete.Loading.template;
+ if (value.title != null) {
+ tmpl = GfmAutoComplete.Issues.template;
+ }
+ return tmpl;
+ },
+ data: GfmAutoComplete.defaultLoadingData,
+ // eslint-disable-next-line no-template-curly-in-string
insertTpl: '${atwho-at}${id}',
callbacks: {
- sorter: this.DefaultOptions.sorter,
- filter: this.DefaultOptions.filter,
- beforeInsert: this.DefaultOptions.beforeInsert,
- matcher: this.DefaultOptions.matcher,
- beforeSave: function(issues) {
- return $.map(issues, function(i) {
+ ...this.getDefaultCallbacks(),
+ beforeSave(issues) {
+ return $.map(issues, (i) => {
if (i.title == null) {
return i;
}
return {
id: i.iid,
title: sanitize(i.title),
- search: i.iid + " " + i.title
+ search: `${i.iid} ${i.title}`,
};
});
- }
- }
+ },
+ },
});
- },
+ }
setupMilestones($input) {
$input.atwho({
at: '%',
alias: 'milestones',
searchKey: 'search',
+ // eslint-disable-next-line no-template-curly-in-string
insertTpl: '${atwho-at}${title}',
- displayTpl: function(value) {
- return value.title != null ? this.Milestones.template : this.Loading.template;
- }.bind(this),
- data: this.defaultLoadingData,
+ displayTpl(value) {
+ let tmpl = GfmAutoComplete.Loading.template;
+ if (value.title != null) {
+ tmpl = GfmAutoComplete.Milestones.template;
+ }
+ return tmpl;
+ },
+ data: GfmAutoComplete.defaultLoadingData,
callbacks: {
- matcher: this.DefaultOptions.matcher,
- sorter: this.DefaultOptions.sorter,
- beforeInsert: this.DefaultOptions.beforeInsert,
- filter: this.DefaultOptions.filter,
- beforeSave: function(milestones) {
- return $.map(milestones, function(m) {
+ ...this.getDefaultCallbacks(),
+ beforeSave(milestones) {
+ return $.map(milestones, (m) => {
if (m.title == null) {
return m;
}
return {
id: m.iid,
title: sanitize(m.title),
- search: "" + m.title
+ search: m.title,
};
});
- }
- }
+ },
+ },
});
- },
+ }
setupMergeRequests($input) {
$input.atwho({
at: '!',
alias: 'mergerequests',
searchKey: 'search',
- displayTpl: function(value) {
- return value.title != null ? this.Issues.template : this.Loading.template;
- }.bind(this),
- data: this.defaultLoadingData,
+ displayTpl(value) {
+ let tmpl = GfmAutoComplete.Loading.template;
+ if (value.title != null) {
+ tmpl = GfmAutoComplete.Issues.template;
+ }
+ return tmpl;
+ },
+ data: GfmAutoComplete.defaultLoadingData,
+ // eslint-disable-next-line no-template-curly-in-string
insertTpl: '${atwho-at}${id}',
callbacks: {
- sorter: this.DefaultOptions.sorter,
- filter: this.DefaultOptions.filter,
- beforeInsert: this.DefaultOptions.beforeInsert,
- matcher: this.DefaultOptions.matcher,
- beforeSave: function(merges) {
- return $.map(merges, function(m) {
+ ...this.getDefaultCallbacks(),
+ beforeSave(merges) {
+ return $.map(merges, (m) => {
if (m.title == null) {
return m;
}
return {
id: m.iid,
title: sanitize(m.title),
- search: m.iid + " " + m.title
+ search: `${m.iid} ${m.title}`,
};
});
- }
- }
+ },
+ },
});
- },
+ }
setupLabels($input) {
$input.atwho({
at: '~',
alias: 'labels',
searchKey: 'search',
- data: this.defaultLoadingData,
- displayTpl: function(value) {
- return this.isLoading(value) ? this.Loading.template : this.Labels.template;
- }.bind(this),
+ data: GfmAutoComplete.defaultLoadingData,
+ displayTpl(value) {
+ let tmpl = GfmAutoComplete.Labels.template;
+ if (GfmAutoComplete.isLoading(value)) {
+ tmpl = GfmAutoComplete.Loading.template;
+ }
+ return tmpl;
+ },
+ // eslint-disable-next-line no-template-curly-in-string
insertTpl: '${atwho-at}${title}',
callbacks: {
- matcher: this.DefaultOptions.matcher,
- beforeInsert: this.DefaultOptions.beforeInsert,
- filter: this.DefaultOptions.filter,
- sorter: this.DefaultOptions.sorter,
- beforeSave: function(merges) {
- if (gl.GfmAutoComplete.isLoading(merges)) return merges;
- var sanitizeLabelTitle;
- sanitizeLabelTitle = function(title) {
- if (/[\w\?&]+\s+[\w\?&]+/g.test(title)) {
- return "\"" + (sanitize(title)) + "\"";
- } else {
- return sanitize(title);
- }
- };
- return $.map(merges, function(m) {
- return {
- title: sanitize(m.title),
- color: m.color,
- search: "" + m.title
- };
- });
- }
- }
+ ...this.getDefaultCallbacks(),
+ beforeSave(merges) {
+ if (GfmAutoComplete.isLoading(merges)) return merges;
+ return $.map(merges, m => ({
+ title: sanitize(m.title),
+ color: m.color,
+ search: m.title,
+ }));
+ },
+ },
});
- },
+ }
- fetchData: function($input, at) {
+ getDefaultCallbacks() {
+ const fetchData = this.fetchData.bind(this);
+
+ return {
+ sorter(query, items, searchKey) {
+ this.setting.highlightFirst = this.setting.alwaysHighlightFirst || query.length > 0;
+ if (GfmAutoComplete.isLoading(items)) {
+ this.setting.highlightFirst = false;
+ return items;
+ }
+ return $.fn.atwho.default.callbacks.sorter(query, items, searchKey);
+ },
+ filter(query, data, searchKey) {
+ if (GfmAutoComplete.isLoading(data)) {
+ fetchData(this.$inputor, this.at);
+ return data;
+ }
+ return $.fn.atwho.default.callbacks.filter(query, data, searchKey);
+ },
+ beforeInsert(value) {
+ let resultantValue = value;
+ if (value && !this.setting.skipSpecialCharacterTest) {
+ const withoutAt = value.substring(1);
+ if (withoutAt && /[^\w\d]/.test(withoutAt)) {
+ resultantValue = `${value.charAt()}"${withoutAt}"`;
+ }
+ }
+ return resultantValue;
+ },
+ matcher(flag, subtext) {
+ // The below is taken from At.js source
+ // Tweaked to commands to start without a space only if char before is a non-word character
+ // https://github.com/ichord/At.js
+ const atSymbolsWithBar = Object.keys(this.app.controllers).join('|');
+ const atSymbolsWithoutBar = Object.keys(this.app.controllers).join('');
+ const targetSubtext = subtext.split(/\s+/g).pop();
+ const resultantFlag = flag.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&');
+
+ const accentAChar = decodeURI('%C3%80');
+ const accentYChar = decodeURI('%C3%BF');
+
+ const regexp = new RegExp(`^(?:\\B|[^a-zA-Z0-9_${atSymbolsWithoutBar}]|\\s)${resultantFlag}(?!${atSymbolsWithBar})((?:[A-Za-z${accentAChar}-${accentYChar}0-9_'.+-]|[^\\x00-\\x7a])*)$`, 'gi');
+
+ const match = regexp.exec(targetSubtext);
+
+ if (match) {
+ return match[1];
+ }
+ return null;
+ },
+ };
+ }
+
+ fetchData($input, at) {
if (this.isLoadingData[at]) return;
this.isLoadingData[at] = true;
if (this.cachedData[at]) {
this.loadData($input, at, this.cachedData[at]);
- } else if (this.atTypeMap[at] === 'emojis') {
+ } else if (GfmAutoComplete.atTypeMap[at] === 'emojis') {
this.loadData($input, at, Object.keys(emojiMap).concat(Object.keys(emojiAliases)));
} else {
- $.getJSON(this.dataSources[this.atTypeMap[at]], (data) => {
+ $.getJSON(this.dataSources[GfmAutoComplete.atTypeMap[at]], (data) => {
this.loadData($input, at, data);
}).fail(() => { this.isLoadingData[at] = false; });
}
- },
- loadData: function($input, at, data) {
+ }
+ loadData($input, at, data) {
this.isLoadingData[at] = false;
this.cachedData[at] = data;
$input.atwho('load', at, data);
// This trigger at.js again
// otherwise we would be stuck with loading until the user types
return $input.trigger('keyup');
- },
- isLoading(data) {
- var dataToInspect = data;
+ }
+
+ static isLoading(data) {
+ let dataToInspect = data;
if (data && data.length > 0) {
dataToInspect = data[0];
}
- var loadingState = this.defaultLoadingData[0];
+ const loadingState = GfmAutoComplete.defaultLoadingData[0];
return dataToInspect &&
(dataToInspect === loadingState || dataToInspect.name === loadingState);
}
+}
+
+GfmAutoComplete.defaultLoadingData = ['loading'];
+
+GfmAutoComplete.atTypeMap = {
+ ':': 'emojis',
+ '@': 'members',
+ '#': 'issues',
+ '!': 'mergeRequests',
+ '~': 'labels',
+ '%': 'milestones',
+ '/': 'commands',
+};
+
+// Emoji
+GfmAutoComplete.Emoji = {
+ templateFunction(name) {
+ return `<li>
+ ${name} ${glEmojiTag(name)}
+ </li>
+ `;
+ },
};
+// Team Members
+GfmAutoComplete.Members = {
+ // eslint-disable-next-line no-template-curly-in-string
+ template: '<li>${avatarTag} ${username} <small>${title}</small></li>',
+};
+GfmAutoComplete.Labels = {
+ // eslint-disable-next-line no-template-curly-in-string
+ template: '<li><span class="dropdown-label-box" style="background: ${color}"></span> ${title}</li>',
+};
+// Issues and MergeRequests
+GfmAutoComplete.Issues = {
+ // eslint-disable-next-line no-template-curly-in-string
+ template: '<li><small>${id}</small> ${title}</li>',
+};
+// Milestones
+GfmAutoComplete.Milestones = {
+ // eslint-disable-next-line no-template-curly-in-string
+ template: '<li>${title}</li>',
+};
+GfmAutoComplete.Loading = {
+ template: '<li style="pointer-events: none;"><i class="fa fa-spinner fa-spin"></i> Loading...</li>',
+};
+
+export default GfmAutoComplete;
diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js
index 9666eeeb5ad..24c423dd01e 100644
--- a/app/assets/javascripts/gl_dropdown.js
+++ b/app/assets/javascripts/gl_dropdown.js
@@ -1,5 +1,6 @@
/* eslint-disable func-names, space-before-function-paren, no-var, one-var, one-var-declaration-per-line, prefer-rest-params, max-len, vars-on-top, wrap-iife, no-unused-vars, quotes, no-shadow, no-cond-assign, prefer-arrow-callback, no-return-assign, no-else-return, camelcase, comma-dangle, no-lonely-if, guard-for-in, no-restricted-syntax, consistent-return, prefer-template, no-param-reassign, no-loop-func, no-mixed-operators */
/* global fuzzaldrinPlus */
+import { isObject } from './lib/utils/type_utility';
var GitLabDropdown, GitLabDropdownFilter, GitLabDropdownRemote;
@@ -93,7 +94,7 @@ GitLabDropdownFilter = (function() {
// { prop: 'def' }
// ]
// }
- if (gl.utils.isObject(data)) {
+ if (isObject(data)) {
results = {};
for (key in data) {
group = data[key];
@@ -396,7 +397,7 @@ GitLabDropdown = (function() {
html = [this.noResults()];
} else {
// Handle array groups
- if (gl.utils.isObject(data)) {
+ if (isObject(data)) {
html = [];
for (name in data) {
groupData = data[name];
diff --git a/app/assets/javascripts/gl_form.js b/app/assets/javascripts/gl_form.js
index ff06092e4d6..dc9f114af99 100644
--- a/app/assets/javascripts/gl_form.js
+++ b/app/assets/javascripts/gl_form.js
@@ -3,11 +3,14 @@
/* global DropzoneInput */
/* global autosize */
+import GfmAutoComplete from './gfm_auto_complete';
+
window.gl = window.gl || {};
-function GLForm(form) {
+function GLForm(form, enableGFM = false) {
this.form = form;
this.textarea = this.form.find('textarea.js-gfm-input');
+ this.enableGFM = enableGFM;
// Before we start, we should clean up any previous data for this form
this.destroy();
// Setup the form
@@ -30,8 +33,14 @@ GLForm.prototype.setupForm = function() {
this.form.addClass('gfm-form');
// remove notify commit author checkbox for non-commit notes
gl.utils.disableButtonIfEmptyField(this.form.find('.js-note-text'), this.form.find('.js-comment-button, .js-note-new-discussion'));
-
- gl.GfmAutoComplete.setup(this.form.find('.js-gfm-input'));
+ new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources).setup(this.form.find('.js-gfm-input'), {
+ emojis: true,
+ members: this.enableGFM,
+ issues: this.enableGFM,
+ milestones: this.enableGFM,
+ mergeRequests: this.enableGFM,
+ labels: this.enableGFM,
+ });
new DropzoneInput(this.form);
autosize(this.textarea);
}
diff --git a/app/assets/javascripts/issuable_context.js b/app/assets/javascripts/issuable_context.js
index 834b98e8601..a4d7bf096ef 100644
--- a/app/assets/javascripts/issuable_context.js
+++ b/app/assets/javascripts/issuable_context.js
@@ -1,8 +1,8 @@
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-new, comma-dangle, quotes, prefer-arrow-callback, consistent-return, one-var, no-var, one-var-declaration-per-line, no-underscore-dangle, max-len */
-/* global UsersSelect */
/* global bp */
import Cookies from 'js-cookie';
+import UsersSelect from './users_select';
(function() {
this.IssuableContext = (function() {
@@ -47,7 +47,6 @@ import Cookies from 'js-cookie';
Cookies.set('collapsed_gutter', true);
}
});
- $(".right-sidebar").niceScroll();
}
IssuableContext.prototype.initParticipants = function() {
diff --git a/app/assets/javascripts/issuable_form.js b/app/assets/javascripts/issuable_form.js
index 544fc91876a..92f6f0d4117 100644
--- a/app/assets/javascripts/issuable_form.js
+++ b/app/assets/javascripts/issuable_form.js
@@ -1,11 +1,13 @@
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-use-before-define, no-useless-escape, no-new, quotes, object-shorthand, no-unused-vars, comma-dangle, no-alert, consistent-return, no-else-return, prefer-template, one-var, one-var-declaration-per-line, curly, max-len */
/* global GitLab */
-/* global UsersSelect */
/* global ZenMode */
/* global Autosave */
/* global dateFormat */
/* global Pikaday */
+import UsersSelect from './users_select';
+import GfmAutoComplete from './gfm_auto_complete';
+
(function() {
this.IssuableForm = (function() {
IssuableForm.prototype.issueMoveConfirmMsg = 'Are you sure you want to move this issue to another project?';
@@ -19,7 +21,7 @@
this.renderWipExplanation = this.renderWipExplanation.bind(this);
this.resetAutosave = this.resetAutosave.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
- gl.GfmAutoComplete.setup();
+ new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources).setup();
new UsersSelect();
new ZenMode();
this.titleField = this.form.find("input[name*='[title]']");
diff --git a/app/assets/javascripts/issue_show/actions/tasks.js b/app/assets/javascripts/issue_show/actions/tasks.js
deleted file mode 100644
index 0740a9f559c..00000000000
--- a/app/assets/javascripts/issue_show/actions/tasks.js
+++ /dev/null
@@ -1,27 +0,0 @@
-export default (newStateData, tasks) => {
- const $tasks = $('#task_status');
- const $tasksShort = $('#task_status_short');
- const $issueableHeader = $('.issuable-header');
- const tasksStates = { newState: null, currentState: null };
-
- if ($tasks.length === 0) {
- if (!(newStateData.task_status.indexOf('0 of 0') === 0)) {
- $issueableHeader.append(`<span id="task_status">${newStateData.task_status}</span>`);
- } else {
- $issueableHeader.append('<span id="task_status"></span>');
- }
- } else {
- tasksStates.newState = newStateData.task_status.indexOf('0 of 0') === 0;
- tasksStates.currentState = tasks.indexOf('0 of 0') === 0;
- }
-
- if ($tasks.length !== 0 && !tasksStates.newState) {
- $tasks.text(newStateData.task_status);
- $tasksShort.text(newStateData.task_status);
- } else if (tasksStates.currentState) {
- $issueableHeader.append(`<span id="task_status">${newStateData.task_status}</span>`);
- } else if (tasksStates.newState) {
- $tasks.remove();
- $tasksShort.remove();
- }
-};
diff --git a/app/assets/javascripts/issue_show/components/app.vue b/app/assets/javascripts/issue_show/components/app.vue
new file mode 100644
index 00000000000..770a0dcd27e
--- /dev/null
+++ b/app/assets/javascripts/issue_show/components/app.vue
@@ -0,0 +1,96 @@
+<script>
+import Visibility from 'visibilityjs';
+import Poll from '../../lib/utils/poll';
+import Service from '../services/index';
+import Store from '../stores';
+import titleComponent from './title.vue';
+import descriptionComponent from './description.vue';
+
+export default {
+ props: {
+ endpoint: {
+ required: true,
+ type: String,
+ },
+ canUpdate: {
+ required: true,
+ type: Boolean,
+ },
+ issuableRef: {
+ type: String,
+ required: true,
+ },
+ initialTitle: {
+ type: String,
+ required: true,
+ },
+ initialDescriptionHtml: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ initialDescriptionText: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ data() {
+ const store = new Store({
+ titleHtml: this.initialTitle,
+ descriptionHtml: this.initialDescriptionHtml,
+ descriptionText: this.initialDescriptionText,
+ });
+
+ return {
+ store,
+ state: store.state,
+ };
+ },
+ components: {
+ descriptionComponent,
+ titleComponent,
+ },
+ created() {
+ const resource = new Service(this.endpoint);
+ const poll = new Poll({
+ resource,
+ method: 'getData',
+ successCallback: (res) => {
+ this.store.updateState(res.json());
+ },
+ errorCallback(err) {
+ throw new Error(err);
+ },
+ });
+
+ if (!Visibility.hidden()) {
+ poll.makeRequest();
+ }
+
+ Visibility.change(() => {
+ if (!Visibility.hidden()) {
+ poll.restart();
+ } else {
+ poll.stop();
+ }
+ });
+ },
+};
+</script>
+
+<template>
+ <div>
+ <title-component
+ :issuable-ref="issuableRef"
+ :title-html="state.titleHtml"
+ :title-text="state.titleText" />
+ <description-component
+ v-if="state.descriptionHtml"
+ :can-update="canUpdate"
+ :description-html="state.descriptionHtml"
+ :description-text="state.descriptionText"
+ :updated-at="state.updatedAt"
+ :task-status="state.taskStatus" />
+ </div>
+</template>
diff --git a/app/assets/javascripts/issue_show/components/description.vue b/app/assets/javascripts/issue_show/components/description.vue
new file mode 100644
index 00000000000..4ad3eb7dfd7
--- /dev/null
+++ b/app/assets/javascripts/issue_show/components/description.vue
@@ -0,0 +1,105 @@
+<script>
+ import animateMixin from '../mixins/animate';
+
+ export default {
+ mixins: [animateMixin],
+ props: {
+ canUpdate: {
+ type: Boolean,
+ required: true,
+ },
+ descriptionHtml: {
+ type: String,
+ required: true,
+ },
+ descriptionText: {
+ type: String,
+ required: true,
+ },
+ updatedAt: {
+ type: String,
+ required: true,
+ },
+ taskStatus: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ preAnimation: false,
+ pulseAnimation: false,
+ timeAgoEl: $('.js-issue-edited-ago'),
+ };
+ },
+ watch: {
+ descriptionHtml() {
+ this.animateChange();
+
+ this.$nextTick(() => {
+ const toolTipTime = gl.utils.formatDate(this.updatedAt);
+
+ this.timeAgoEl.attr('datetime', this.updatedAt)
+ .attr('title', toolTipTime)
+ .tooltip('fixTitle');
+
+ this.renderGFM();
+ });
+ },
+ taskStatus() {
+ const taskRegexMatches = this.taskStatus.match(/(\d+) of (\d+)/);
+ const $issuableHeader = $('.issuable-meta');
+ const $tasks = $('#task_status', $issuableHeader);
+ const $tasksShort = $('#task_status_short', $issuableHeader);
+
+ if (taskRegexMatches) {
+ $tasks.text(this.taskStatus);
+ $tasksShort.text(`${taskRegexMatches[1]}/${taskRegexMatches[2]} task${taskRegexMatches[2] > 1 ? 's' : ''}`);
+ } else {
+ $tasks.text('');
+ $tasksShort.text('');
+ }
+ },
+ },
+ methods: {
+ renderGFM() {
+ $(this.$refs['gfm-entry-content']).renderGFM();
+
+ if (this.canUpdate) {
+ // eslint-disable-next-line no-new
+ new gl.TaskList({
+ dataType: 'issue',
+ fieldName: 'description',
+ selector: '.detail-page-description',
+ });
+ }
+ },
+ },
+ mounted() {
+ this.renderGFM();
+ },
+ };
+</script>
+
+<template>
+ <div
+ class="description"
+ :class="{
+ 'js-task-list-container': canUpdate
+ }">
+ <div
+ class="wiki"
+ :class="{
+ 'issue-realtime-pre-pulse': preAnimation,
+ 'issue-realtime-trigger-pulse': pulseAnimation
+ }"
+ v-html="descriptionHtml"
+ ref="gfm-content">
+ </div>
+ <textarea
+ class="hidden js-task-list-field"
+ v-if="descriptionText"
+ v-model="descriptionText">
+ </textarea>
+ </div>
+</template>
diff --git a/app/assets/javascripts/issue_show/components/title.vue b/app/assets/javascripts/issue_show/components/title.vue
new file mode 100644
index 00000000000..a9dabd4cff1
--- /dev/null
+++ b/app/assets/javascripts/issue_show/components/title.vue
@@ -0,0 +1,53 @@
+<script>
+ import animateMixin from '../mixins/animate';
+
+ export default {
+ mixins: [animateMixin],
+ data() {
+ return {
+ preAnimation: false,
+ pulseAnimation: false,
+ titleEl: document.querySelector('title'),
+ };
+ },
+ props: {
+ issuableRef: {
+ type: String,
+ required: true,
+ },
+ titleHtml: {
+ type: String,
+ required: true,
+ },
+ titleText: {
+ type: String,
+ required: true,
+ },
+ },
+ watch: {
+ titleHtml() {
+ this.setPageTitle();
+ this.animateChange();
+ },
+ },
+ methods: {
+ setPageTitle() {
+ const currentPageTitleScope = this.titleEl.innerText.split('·');
+ currentPageTitleScope[0] = `${this.titleText} (${this.issuableRef}) `;
+ this.titleEl.textContent = currentPageTitleScope.join('·');
+ },
+ },
+ };
+</script>
+
+<template>
+ <h2
+ class="title"
+ :class="{
+ 'issue-realtime-pre-pulse': preAnimation,
+ 'issue-realtime-trigger-pulse': pulseAnimation
+ }"
+ v-html="titleHtml"
+ >
+ </h2>
+</template>
diff --git a/app/assets/javascripts/issue_show/index.js b/app/assets/javascripts/issue_show/index.js
index eb20a597bb5..f06e33dee60 100644
--- a/app/assets/javascripts/issue_show/index.js
+++ b/app/assets/javascripts/issue_show/index.js
@@ -1,20 +1,42 @@
import Vue from 'vue';
-import IssueTitle from './issue_title_description.vue';
+import issuableApp from './components/app.vue';
import '../vue_shared/vue_resource_interceptor';
-(() => {
- const issueTitleData = document.querySelector('.issue-title-data').dataset;
- const { canUpdateTasksClass, endpoint } = issueTitleData;
+document.addEventListener('DOMContentLoaded', () => new Vue({
+ el: document.getElementById('js-issuable-app'),
+ components: {
+ issuableApp,
+ },
+ data() {
+ const issuableElement = this.$options.el;
+ const issuableTitleElement = issuableElement.querySelector('.title');
+ const issuableDescriptionElement = issuableElement.querySelector('.wiki');
+ const issuableDescriptionTextarea = issuableElement.querySelector('.js-task-list-field');
+ const {
+ canUpdate,
+ endpoint,
+ issuableRef,
+ } = issuableElement.dataset;
- const vm = new Vue({
- el: '.issue-title-entrypoint',
- render: createElement => createElement(IssueTitle, {
+ return {
+ canUpdate: gl.utils.convertPermissionToBoolean(canUpdate),
+ endpoint,
+ issuableRef,
+ initialTitle: issuableTitleElement.innerHTML,
+ initialDescriptionHtml: issuableDescriptionElement ? issuableDescriptionElement.innerHTML : '',
+ initialDescriptionText: issuableDescriptionTextarea ? issuableDescriptionTextarea.textContent : '',
+ };
+ },
+ render(createElement) {
+ return createElement('issuable-app', {
props: {
- canUpdateTasksClass,
- endpoint,
+ canUpdate: this.canUpdate,
+ endpoint: this.endpoint,
+ issuableRef: this.issuableRef,
+ initialTitle: this.initialTitle,
+ initialDescriptionHtml: this.initialDescriptionHtml,
+ initialDescriptionText: this.initialDescriptionText,
},
- }),
- });
-
- return vm;
-})();
+ });
+ },
+}));
diff --git a/app/assets/javascripts/issue_show/issue_title_description.vue b/app/assets/javascripts/issue_show/issue_title_description.vue
deleted file mode 100644
index dc3ba2550c5..00000000000
--- a/app/assets/javascripts/issue_show/issue_title_description.vue
+++ /dev/null
@@ -1,180 +0,0 @@
-<script>
-import Visibility from 'visibilityjs';
-import Poll from './../lib/utils/poll';
-import Service from './services/index';
-import tasks from './actions/tasks';
-
-export default {
- props: {
- endpoint: {
- required: true,
- type: String,
- },
- canUpdateTasksClass: {
- required: true,
- type: String,
- },
- },
- data() {
- const resource = new Service(this.$http, this.endpoint);
-
- const poll = new Poll({
- resource,
- method: 'getTitle',
- successCallback: (res) => {
- this.renderResponse(res);
- },
- errorCallback: (err) => {
- throw new Error(err);
- },
- });
-
- return {
- poll,
- apiData: {},
- tasks: '0 of 0',
- title: null,
- titleText: '',
- titleFlag: {
- pre: true,
- pulse: false,
- },
- description: null,
- descriptionText: '',
- descriptionChange: false,
- descriptionFlag: {
- pre: true,
- pulse: false,
- },
- timeAgoEl: $('.issue_edited_ago'),
- titleEl: document.querySelector('title'),
- };
- },
- methods: {
- updateFlag(key, toggle) {
- this[key].pre = toggle;
- this[key].pulse = !toggle;
- },
- renderResponse(res) {
- this.apiData = res.json();
- this.triggerAnimation();
- },
- updateTaskHTML() {
- tasks(this.apiData, this.tasks);
- },
- elementsToVisualize(noTitleChange, noDescriptionChange) {
- if (!noTitleChange) {
- this.titleText = this.apiData.title_text;
- this.updateFlag('titleFlag', true);
- }
-
- if (!noDescriptionChange) {
- // only change to true when we need to bind TaskLists the html of description
- this.descriptionChange = true;
- this.updateTaskHTML();
- this.tasks = this.apiData.task_status;
- this.updateFlag('descriptionFlag', true);
- }
- },
- setTabTitle() {
- const currentTabTitleScope = this.titleEl.innerText.split('·');
- currentTabTitleScope[0] = `${this.titleText} (#${this.apiData.issue_number}) `;
- this.titleEl.innerText = currentTabTitleScope.join('·');
- },
- animate(title, description) {
- this.title = title;
- this.description = description;
- this.setTabTitle();
-
- this.$nextTick(() => {
- this.updateFlag('titleFlag', false);
- this.updateFlag('descriptionFlag', false);
- });
- },
- triggerAnimation() {
- // always reset to false before checking the change
- this.descriptionChange = false;
-
- const { title, description } = this.apiData;
- this.descriptionText = this.apiData.description_text;
-
- const noTitleChange = this.title === title;
- const noDescriptionChange = this.description === description;
-
- /**
- * since opacity is changed, even if there is no diff for Vue to update
- * we must check the title/description even on a 304 to ensure no visual change
- */
- if (noTitleChange && noDescriptionChange) return;
-
- this.elementsToVisualize(noTitleChange, noDescriptionChange);
- this.animate(title, description);
- },
- updateEditedTimeAgo() {
- const toolTipTime = gl.utils.formatDate(this.apiData.updated_at);
- this.timeAgoEl.attr('datetime', this.apiData.updated_at);
- this.timeAgoEl.attr('title', toolTipTime).tooltip('fixTitle');
- },
- },
- created() {
- if (!Visibility.hidden()) {
- this.poll.makeRequest();
- }
-
- Visibility.change(() => {
- if (!Visibility.hidden()) {
- this.poll.restart();
- } else {
- this.poll.stop();
- }
- });
- },
- updated() {
- // if new html is injected (description changed) - bind TaskList and call renderGFM
- if (this.descriptionChange) {
- this.updateEditedTimeAgo();
-
- $(this.$refs['issue-content-container-gfm-entry']).renderGFM();
-
- const tl = new gl.TaskList({
- dataType: 'issue',
- fieldName: 'description',
- selector: '.detail-page-description',
- });
-
- return tl && null;
- }
-
- return null;
- },
-};
-</script>
-
-<template>
- <div>
- <h2
- class="title"
- :class="{ 'issue-realtime-pre-pulse': titleFlag.pre, 'issue-realtime-trigger-pulse': titleFlag.pulse }"
- ref="issue-title"
- v-html="title"
- >
- </h2>
- <div
- class="description is-task-list-enabled"
- :class="canUpdateTasksClass"
- v-if="description"
- >
- <div
- class="wiki"
- :class="{ 'issue-realtime-pre-pulse': descriptionFlag.pre, 'issue-realtime-trigger-pulse': descriptionFlag.pulse }"
- v-html="description"
- ref="issue-content-container-gfm-entry"
- >
- </div>
- <textarea
- class="hidden js-task-list-field"
- v-if="descriptionText"
- >{{descriptionText}}</textarea>
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/issue_show/mixins/animate.js b/app/assets/javascripts/issue_show/mixins/animate.js
new file mode 100644
index 00000000000..eda6302aa8b
--- /dev/null
+++ b/app/assets/javascripts/issue_show/mixins/animate.js
@@ -0,0 +1,13 @@
+export default {
+ methods: {
+ animateChange() {
+ this.preAnimation = true;
+ this.pulseAnimation = false;
+
+ this.$nextTick(() => {
+ this.preAnimation = false;
+ this.pulseAnimation = true;
+ });
+ },
+ },
+};
diff --git a/app/assets/javascripts/issue_show/services/index.js b/app/assets/javascripts/issue_show/services/index.js
index c4ab0b1e07a..348ad8d6813 100644
--- a/app/assets/javascripts/issue_show/services/index.js
+++ b/app/assets/javascripts/issue_show/services/index.js
@@ -1,10 +1,16 @@
+import Vue from 'vue';
+import VueResource from 'vue-resource';
+
+Vue.use(VueResource);
+
export default class Service {
- constructor(resource, endpoint) {
- this.resource = resource;
+ constructor(endpoint) {
this.endpoint = endpoint;
+
+ this.resource = Vue.resource(this.endpoint);
}
- getTitle() {
- return this.resource.get(this.endpoint);
+ getData() {
+ return this.resource.get();
}
}
diff --git a/app/assets/javascripts/issue_show/stores/index.js b/app/assets/javascripts/issue_show/stores/index.js
new file mode 100644
index 00000000000..8e89a2b7730
--- /dev/null
+++ b/app/assets/javascripts/issue_show/stores/index.js
@@ -0,0 +1,25 @@
+export default class Store {
+ constructor({
+ titleHtml,
+ descriptionHtml,
+ descriptionText,
+ }) {
+ this.state = {
+ titleHtml,
+ titleText: '',
+ descriptionHtml,
+ descriptionText,
+ taskStatus: '',
+ updatedAt: '',
+ };
+ }
+
+ updateState(data) {
+ this.state.titleHtml = data.title;
+ this.state.titleText = data.title_text;
+ this.state.descriptionHtml = data.description;
+ this.state.descriptionText = data.description_text;
+ this.state.taskStatus = data.task_status;
+ this.state.updatedAt = data.updated_at;
+ }
+}
diff --git a/app/assets/javascripts/layout_nav.js b/app/assets/javascripts/layout_nav.js
index a5f99bcdd8f..71064ccc539 100644
--- a/app/assets/javascripts/layout_nav.js
+++ b/app/assets/javascripts/layout_nav.js
@@ -1,4 +1,5 @@
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-arrow-callback, no-unused-vars, one-var, one-var-declaration-per-line, vars-on-top, max-len */
+import _ from 'underscore';
(function() {
var hideEndFade;
@@ -45,4 +46,13 @@
}
});
});
+
+ function applyScrollNavClass() {
+ const scrollOpacityHeight = 40;
+ $('.navbar-border').css('opacity', Math.min($(window).scrollTop() / scrollOpacityHeight, 1));
+ }
+
+ $(() => {
+ $(window).on('scroll', _.throttle(applyScrollNavClass, 100));
+ });
}).call(window);
diff --git a/app/assets/javascripts/lib/utils/ajax_cache.js b/app/assets/javascripts/lib/utils/ajax_cache.js
index d99eefb5089..cf030d613df 100644
--- a/app/assets/javascripts/lib/utils/ajax_cache.js
+++ b/app/assets/javascripts/lib/utils/ajax_cache.js
@@ -1,32 +1,54 @@
-const AjaxCache = {
- internalStorage: { },
+class AjaxCache {
+ constructor() {
+ this.internalStorage = { };
+ this.pendingRequests = { };
+ }
+
get(endpoint) {
return this.internalStorage[endpoint];
- },
+ }
+
hasData(endpoint) {
return Object.prototype.hasOwnProperty.call(this.internalStorage, endpoint);
- },
- purge(endpoint) {
+ }
+
+ remove(endpoint) {
delete this.internalStorage[endpoint];
- },
+ }
+
retrieve(endpoint) {
- if (AjaxCache.hasData(endpoint)) {
- return Promise.resolve(AjaxCache.get(endpoint));
+ if (this.hasData(endpoint)) {
+ return Promise.resolve(this.get(endpoint));
}
- return new Promise((resolve, reject) => {
- $.ajax(endpoint) // eslint-disable-line promise/catch-or-return
- .then(data => resolve(data),
- (jqXHR, textStatus, errorThrown) => {
- const error = new Error(`${endpoint}: ${errorThrown}`);
- error.textStatus = textStatus;
- reject(error);
- },
- );
- })
- .then((data) => { this.internalStorage[endpoint] = data; })
- .then(() => AjaxCache.get(endpoint));
- },
-};
-
-export default AjaxCache;
+ let pendingRequest = this.pendingRequests[endpoint];
+
+ if (!pendingRequest) {
+ pendingRequest = new Promise((resolve, reject) => {
+ // jQuery 2 is not Promises/A+ compatible (missing catch)
+ $.ajax(endpoint) // eslint-disable-line promise/catch-or-return
+ .then(data => resolve(data),
+ (jqXHR, textStatus, errorThrown) => {
+ const error = new Error(`${endpoint}: ${errorThrown}`);
+ error.textStatus = textStatus;
+ reject(error);
+ },
+ );
+ })
+ .then((data) => {
+ this.internalStorage[endpoint] = data;
+ delete this.pendingRequests[endpoint];
+ })
+ .catch((error) => {
+ delete this.pendingRequests[endpoint];
+ throw error;
+ });
+
+ this.pendingRequests[endpoint] = pendingRequest;
+ }
+
+ return pendingRequest.then(() => this.get(endpoint));
+ }
+}
+
+export default new AjaxCache();
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index 2f682fbd2fb..7e62773ae6c 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -135,7 +135,10 @@
gl.utils.getUrlParamsArray = function () {
// We can trust that each param has one & since values containing & will be encoded
// Remove the first character of search as it is always ?
- return window.location.search.slice(1).split('&');
+ return window.location.search.slice(1).split('&').map((param) => {
+ const split = param.split('=');
+ return [decodeURI(split[0]), split[1]].join('=');
+ });
};
gl.utils.isMetaKey = function(e) {
diff --git a/app/assets/javascripts/lib/utils/type_utility.js b/app/assets/javascripts/lib/utils/type_utility.js
index db62e0be324..be86f336bcd 100644
--- a/app/assets/javascripts/lib/utils/type_utility.js
+++ b/app/assets/javascripts/lib/utils/type_utility.js
@@ -1,15 +1,2 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, no-return-assign, max-len */
-(function() {
- (function(w) {
- var base;
- if (w.gl == null) {
- w.gl = {};
- }
- if ((base = w.gl).utils == null) {
- base.utils = {};
- }
- return w.gl.utils.isObject = function(obj) {
- return (obj != null) && (obj.constructor === Object);
- };
- })(window);
-}).call(window);
+// eslint-disable-next-line import/prefer-default-export
+export const isObject = obj => obj && obj.constructor === Object;
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index a07aa047293..9b9fc31ae93 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -59,7 +59,6 @@ import './lib/utils/datetime_utility';
import './lib/utils/notify';
import './lib/utils/pretty_time';
import './lib/utils/text_utility';
-import './lib/utils/type_utility';
import './lib/utils/url_utility';
// u2f
@@ -97,7 +96,6 @@ import './dropzone_input';
import './due_date_select';
import './files_comment_button';
import './flash';
-import './gfm_auto_complete';
import './gl_dropdown';
import './gl_field_error';
import './gl_field_errors';
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index 93c30c54a8e..22032d0f914 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -1,6 +1,7 @@
/* eslint-disable no-new, class-methods-use-this */
/* global Breakpoints */
/* global Flash */
+/* global notes */
import Cookies from 'js-cookie';
import './breakpoints';
@@ -251,7 +252,8 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion';
this.ajaxGet({
url: `${urlPathname}.json${location.search}`,
success: (data) => {
- $('#diffs').html(data.html);
+ const $container = $('#diffs');
+ $container.html(data.html);
if (typeof gl.diffNotesCompileComponents !== 'undefined') {
gl.diffNotesCompileComponents();
@@ -278,6 +280,24 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion';
})
.init();
});
+
+ // Scroll any linked note into view
+ // Similar to `toggler_behavior` in the discussion tab
+ const hash = window.gl.utils.getLocationHash();
+ const anchor = hash && $container.find(`[id="${hash}"]`);
+ if (anchor) {
+ const notesContent = anchor.closest('.notes_content');
+ const lineType = notesContent.hasClass('new') ? 'new' : 'old';
+ notes.toggleDiffNote({
+ target: anchor,
+ lineType,
+ forceShow: true,
+ });
+ anchor[0].scrollIntoView();
+ // We have multiple elements on the page with `#note_xxx`
+ // (discussion and diff tabs) and `:target` only applies to the first
+ anchor.addClass('target');
+ }
},
});
}
@@ -353,18 +373,26 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion';
initAffix() {
const $tabs = $('.js-tabs-affix');
+ const $fixedNav = $('.navbar-gitlab');
// Screen space on small screens is usually very sparse
// So we dont affix the tabs on these
if (Breakpoints.get().getBreakpointSize() === 'xs' || !$tabs.length) return;
+ /**
+ If the browser does not support position sticky, it returns the position as static.
+ If the browser does support sticky, then we allow the browser to handle it, if not
+ then we default back to Bootstraps affix
+ **/
+ if ($tabs.css('position') !== 'static') return;
+
const $diffTabs = $('#diff-notes-app');
$tabs.off('affix.bs.affix affix-top.bs.affix')
.affix({
offset: {
top: () => (
- $diffTabs.offset().top - $tabs.height()
+ $diffTabs.offset().top - $tabs.height() - $fixedNav.height()
),
},
})
diff --git a/app/assets/javascripts/merge_request_widget.js b/app/assets/javascripts/merge_request_widget.js
deleted file mode 100644
index 3f976680b9d..00000000000
--- a/app/assets/javascripts/merge_request_widget.js
+++ /dev/null
@@ -1,303 +0,0 @@
-/* eslint-disable max-len, no-var, func-names, space-before-function-paren, vars-on-top, comma-dangle, no-return-assign, consistent-return, no-param-reassign, one-var, one-var-declaration-per-line, quotes, prefer-template, no-else-return, prefer-arrow-callback, no-unused-vars, no-underscore-dangle, no-shadow, no-mixed-operators, camelcase, default-case, wrap-iife */
-/* global notify */
-/* global notifyPermissions */
-/* global merge_request_widget */
-
-import './smart_interval';
-import MiniPipelineGraph from './mini_pipeline_graph_dropdown';
-
-((global) => {
- const DEPLOYMENT_TEMPLATE = `<div class="mr-widget-heading" id="<%- id %>">
- <div class="ci_widget ci-success">
- <%= ci_success_icon %>
- <span>
- Deployed to
- <a href="<%- url %>" target="_blank" rel="noopener noreferrer" class="environment">
- <%- name %>
- </a>
- <span class="js-environment-timeago" data-toggle="tooltip" data-placement="top" data-title="<%- deployed_at_formatted %>">
- <%- deployed_at %>
- </span>
- <a class="js-environment-link" href="<%- external_url %>" target="_blank" rel="noopener noreferrer">
- <i class="fa fa-external-link"></i>
- View on <%- external_url_formatted %>
- </a>
- </span>
- <span class="stop-env-container js-stop-env-link">
- <a href="<%- stop_url %>" class="close-evn-link" data-method="post" rel="nofollow" data-confirm="Are you sure you want to stop this environment?">
- <i class="fa fa-stop-circle-o"/>
- Stop environment
- </a>
- </span>
- </div>
- </div>`;
-
- global.MergeRequestWidget = (function() {
- function MergeRequestWidget(opts) {
- // Initialize MergeRequestWidget behavior
- //
- // check_enable - Boolean, whether to check automerge status
- // merge_check_url - String, URL to use to check automerge status
- // ci_status_url - String, URL to use to check CI status
- // pipeline_status_url - String, URL to use to get CI status for Favicon
- //
- this.opts = opts;
- this.opts.pipeline_status_url = `${this.opts.pipeline_status_url}.json`;
- this.$widgetBody = $('.mr-widget-body');
- $('#modal_merge_info').modal({
- show: false
- });
- this.clearEventListeners();
- this.addEventListeners();
- this.getCIStatus(false);
- this.retrieveSuccessIcon();
-
- this.initMiniPipelineGraph();
-
- this.ciStatusInterval = new global.SmartInterval({
- callback: this.getCIStatus.bind(this, true),
- startingInterval: 10000,
- maxInterval: 30000,
- hiddenInterval: 120000,
- incrementByFactorOf: 5000,
- });
- this.ciEnvironmentStatusInterval = new global.SmartInterval({
- callback: this.getCIEnvironmentsStatus.bind(this),
- startingInterval: 30000,
- maxInterval: 120000,
- hiddenInterval: 240000,
- incrementByFactorOf: 15000,
- immediateExecution: true,
- });
-
- notifyPermissions();
- }
-
- MergeRequestWidget.prototype.clearEventListeners = function() {
- return $(document).off('DOMContentLoaded');
- };
-
- MergeRequestWidget.prototype.addEventListeners = function() {
- var allowedPages;
- allowedPages = ['show', 'commits', 'pipelines', 'changes'];
- $(document).on('DOMContentLoaded', (function(_this) {
- return function() {
- var page;
- page = $('body').data('page').split(':').last();
- if (allowedPages.indexOf(page) === -1) {
- return _this.clearEventListeners();
- }
- };
- })(this));
- };
-
- MergeRequestWidget.prototype.retrieveSuccessIcon = function() {
- const $ciSuccessIcon = $('.js-success-icon');
- this.$ciSuccessIcon = $ciSuccessIcon.html();
- $ciSuccessIcon.remove();
- };
-
- MergeRequestWidget.prototype.mergeInProgress = function(deleteSourceBranch) {
- if (deleteSourceBranch == null) {
- deleteSourceBranch = false;
- }
- return $.ajax({
- type: 'GET',
- url: $('.merge-request').data('url'),
- success: (function(_this) {
- return function(data) {
- var callback, urlSuffix;
- if (data.state === "merged") {
- urlSuffix = deleteSourceBranch ? '?deleted_source_branch=true' : '';
- return window.location.href = window.location.pathname + urlSuffix;
- } else if (data.merge_error) {
- return $('.mr-widget-body').html("<h4>" + data.merge_error + "</h4>");
- } else {
- callback = function() {
- return merge_request_widget.mergeInProgress(deleteSourceBranch);
- };
- return setTimeout(callback, 2000);
- }
- };
- })(this),
- dataType: 'json'
- });
- };
-
- MergeRequestWidget.prototype.cancelPolling = function () {
- this.ciStatusInterval.cancel();
- this.ciEnvironmentStatusInterval.cancel();
- };
-
- MergeRequestWidget.prototype.getMergeStatus = function() {
- return $.get(this.opts.merge_check_url, (data) => {
- var $html = $(data);
- this.updateMergeButton(this.status, this.hasCi, $html);
- $('.mr-widget-body').replaceWith($html.find('.mr-widget-body'));
- $('.mr-widget-footer').replaceWith($html.find('.mr-widget-footer'));
- });
- };
-
- MergeRequestWidget.prototype.ciLabelForStatus = function(status) {
- switch (status) {
- case 'success':
- return 'passed';
- case 'success_with_warnings':
- return 'passed with warnings';
- default:
- return status;
- }
- };
-
- MergeRequestWidget.prototype.getCIStatus = function(showNotification) {
- var _this;
- _this = this;
- $('.ci-widget-fetching').show();
- return $.getJSON(this.opts.ci_status_url, (function(_this) {
- return function(data) {
- var message, status, title, callback;
- _this.status = data.status;
- _this.hasCi = data.has_ci;
- _this.updateMergeButton(_this.status, _this.hasCi);
- gl.utils.setCiStatusFavicon(_this.opts.pipeline_status_url);
- if (data.environments && data.environments.length) _this.renderEnvironments(data.environments);
- if (data.status !== _this.opts.ci_status ||
- data.sha !== _this.opts.ci_sha ||
- data.pipeline !== _this.opts.ci_pipeline) {
- _this.opts.ci_status = data.status;
- _this.showCIStatus(data.status);
- if (data.coverage) {
- _this.showCICoverage(data.coverage);
- }
- if (data.pipeline) {
- _this.opts.ci_pipeline = data.pipeline;
- _this.updatePipelineUrls(data.pipeline);
- }
- if (data.sha) {
- _this.opts.ci_sha = data.sha;
- _this.updateCommitUrls(data.sha);
- }
- if (data.status === "success" || data.status === "failed") {
- callback = function() {
- return _this.getMergeStatus();
- };
- return setTimeout(callback, 2000);
- }
- if (showNotification && data.status) {
- status = _this.ciLabelForStatus(data.status);
- if (status === "preparing") {
- title = _this.opts.ci_title.preparing;
- status = status.charAt(0).toUpperCase() + status.slice(1);
- message = _this.opts.ci_message.preparing.replace('{{status}}', status);
- } else {
- title = _this.opts.ci_title.normal;
- message = _this.opts.ci_message.normal.replace('{{status}}', status);
- }
- title = title.replace('{{status}}', status);
- message = message.replace('{{sha}}', data.sha);
- message = message.replace('{{title}}', data.title);
- notify(title, message, _this.opts.gitlab_icon, function() {
- this.close();
- });
- }
- }
- };
- })(this));
- };
-
- MergeRequestWidget.prototype.getCIEnvironmentsStatus = function() {
- $.getJSON(this.opts.ci_environments_status_url, (environments) => {
- if (environments && environments.length) this.renderEnvironments(environments);
- });
- };
-
- MergeRequestWidget.prototype.renderEnvironments = function(environments) {
- for (let i = 0; i < environments.length; i += 1) {
- const environment = environments[i];
- if ($(`.mr-state-widget #${environment.id}`).length) return;
- const $template = $(DEPLOYMENT_TEMPLATE);
- if (!environment.external_url || !environment.external_url_formatted) $('.js-environment-link', $template).remove();
-
- if (!environment.stop_url) {
- $('.js-stop-env-link', $template).remove();
- }
-
- if (environment.deployed_at && environment.deployed_at_formatted) {
- environment.deployed_at = gl.utils.getTimeago().format(environment.deployed_at, 'gl_en') + '.';
- } else {
- $('.js-environment-timeago', $template).remove();
- environment.name += '.';
- }
- environment.ci_success_icon = this.$ciSuccessIcon;
- const templateString = _.unescape($template[0].outerHTML);
- const template = _.template(templateString)(environment);
- this.$widgetBody.before(template);
- }
- };
-
- MergeRequestWidget.prototype.showCIStatus = function(state) {
- var allowed_states;
- if (state == null) {
- return;
- }
- $('.ci_widget').hide();
- $('.ci_widget.ci-' + state).show();
-
- this.initMiniPipelineGraph();
- };
-
- MergeRequestWidget.prototype.showCICoverage = function(coverage) {
- var text = `Coverage ${coverage}%`;
- return $('.ci_widget:visible .ci-coverage').text(text);
- };
-
- MergeRequestWidget.prototype.updateMergeButton = function(state, hasCi, $html) {
- const allowed_states = ["failed", "canceled", "running", "pending", "success", "success_with_warnings", "skipped", "not_found"];
- let stateClass = 'btn-danger';
- if (!hasCi) {
- stateClass = 'btn-create';
- } else if (allowed_states.indexOf(state) !== -1) {
- switch (state) {
- case "failed":
- case "canceled":
- case "not_found":
- stateClass = 'btn-danger';
- break;
- case "running":
- stateClass = 'btn-info';
- break;
- case "success":
- case "success_with_warnings":
- stateClass = 'btn-create';
- }
- } else {
- $('.ci_widget.ci-error').show();
- stateClass = 'btn-danger';
- }
-
- this.setMergeButtonClass(stateClass, $html);
- };
-
- MergeRequestWidget.prototype.setMergeButtonClass = function(css_class, $html = $('.mr-state-widget')) {
- return $html.find('.js-merge-button').removeClass('btn-danger btn-info btn-create').addClass(css_class);
- };
-
- MergeRequestWidget.prototype.updatePipelineUrls = function(id) {
- const pipelineUrl = this.opts.pipeline_path;
- $('.pipeline').text(`#${id}`).attr('href', [pipelineUrl, id].join('/'));
- };
-
- MergeRequestWidget.prototype.updateCommitUrls = function(id) {
- const commitsUrl = this.opts.commits_path;
- $('.js-commit-link').text(id).attr('href', [commitsUrl, id].join('/'));
- };
-
- MergeRequestWidget.prototype.initMiniPipelineGraph = function() {
- new MiniPipelineGraph({
- container: '.js-pipeline-inline-mr-widget-graph:visible',
- }).bindEvents();
- };
-
- return MergeRequestWidget;
- })();
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js
index 11e68c0a3be..9d481d7c003 100644
--- a/app/assets/javascripts/milestone_select.js
+++ b/app/assets/javascripts/milestone_select.js
@@ -18,12 +18,11 @@
}
$els.each(function(i, dropdown) {
- var $block, $dropdown, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, collapsedSidebarLabelTemplate, defaultLabel, issuableId, issueUpdateURL, milestoneLinkNoneTemplate, milestoneLinkTemplate, milestonesUrl, projectId, selectedMilestone, showAny, showNo, showUpcoming, showStarted, useId, showMenuAbove;
+ var $block, $dropdown, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, collapsedSidebarLabelTemplate, defaultLabel, defaultNo, issuableId, issueUpdateURL, milestoneLinkNoneTemplate, milestoneLinkTemplate, milestonesUrl, projectId, selectedMilestone, selectedMilestoneDefault, showAny, showNo, showUpcoming, showStarted, useId, showMenuAbove;
$dropdown = $(dropdown);
projectId = $dropdown.data('project-id');
milestonesUrl = $dropdown.data('milestones');
issueUpdateURL = $dropdown.data('issueUpdate');
- selectedMilestone = $dropdown.data('selected');
showNo = $dropdown.data('show-no');
showAny = $dropdown.data('show-any');
showMenuAbove = $dropdown.data('showMenuAbove');
@@ -31,6 +30,7 @@
showStarted = $dropdown.data('show-started');
useId = $dropdown.data('use-id');
defaultLabel = $dropdown.data('default-label');
+ defaultNo = $dropdown.data('default-no');
issuableId = $dropdown.data('issuable-id');
abilityName = $dropdown.data('ability-name');
$selectbox = $dropdown.closest('.selectbox');
@@ -38,6 +38,9 @@
$sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon');
$value = $block.find('.value');
$loading = $block.find('.block-loading').fadeOut();
+ selectedMilestoneDefault = (showAny ? '' : null);
+ selectedMilestoneDefault = (showNo && defaultNo ? 'No Milestone' : selectedMilestoneDefault);
+ selectedMilestone = $dropdown.data('selected') || selectedMilestoneDefault;
if (issueUpdateURL) {
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>';
@@ -86,8 +89,18 @@
if (showMenuAbove) {
$dropdown.data('glDropdown').positionMenuAbove();
}
+ $(`[data-milestone-id="${selectedMilestone}"] > a`).addClass('is-active');
});
},
+ renderRow: function(milestone) {
+ return `
+ <li data-milestone-id="${milestone.name}">
+ <a href='#' class='dropdown-menu-milestone-link'>
+ ${_.escape(milestone.title)}
+ </a>
+ </li>
+ `;
+ },
filterable: true,
search: {
fields: ['title']
@@ -120,15 +133,24 @@
// display:block overrides the hide-collapse rule
return $value.css('display', '');
},
+ opened: function(e) {
+ const $el = $(e.currentTarget);
+ if ($dropdown.hasClass('js-issue-board-sidebar')) {
+ selectedMilestone = $dropdown[0].dataset.selected || selectedMilestoneDefault;
+ }
+ $('a.is-active', $el).removeClass('is-active');
+ $(`[data-milestone-id="${selectedMilestone}"] > a`, $el).addClass('is-active');
+ },
vue: $dropdown.hasClass('js-issue-board-sidebar'),
clicked: function(options) {
const { $el, e } = options;
let selected = options.selectedObj;
-
- var data, isIssueIndex, isMRIndex, page, boardsStore;
+ var data, isIssueIndex, isMRIndex, isSelecting, page, boardsStore;
page = $('body').data('page');
isIssueIndex = page === 'projects:issues:index';
isMRIndex = (page === page && page === 'projects:merge_requests:index');
+ isSelecting = (selected.name !== selectedMilestone);
+ selectedMilestone = isSelecting ? selected.name : selectedMilestoneDefault;
if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) {
e.preventDefault();
return;
@@ -142,16 +164,11 @@
boardsStore[$dropdown.data('field-name')] = selected.name;
e.preventDefault();
} else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
- if (selected.name != null) {
- selectedMilestone = selected.name;
- } else {
- selectedMilestone = '';
- }
return Issuable.filterResults($dropdown.closest('form'));
} else if ($dropdown.hasClass('js-filter-submit')) {
return $dropdown.closest('form').submit();
} else if ($dropdown.hasClass('js-issue-board-sidebar')) {
- if (selected.id !== -1) {
+ if (selected.id !== -1 && isSelecting) {
gl.issueBoards.boardStoreIssueSet('milestone', new ListMilestone({
id: selected.id,
title: selected.name
diff --git a/app/assets/javascripts/new_branch_form.js b/app/assets/javascripts/new_branch_form.js
index 9d614cdee3a..39fb302b644 100644
--- a/app/assets/javascripts/new_branch_form.js
+++ b/app/assets/javascripts/new_branch_form.js
@@ -1,4 +1,6 @@
/* 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 */
+import RefSelectDropdown from '~/ref_select_dropdown';
+
(function() {
this.NewBranchForm = (function() {
function NewBranchForm(form, availableRefs) {
@@ -6,7 +8,7 @@
this.branchNameError = form.find('.js-branch-name-error');
this.name = form.find('.js-branch-name');
this.ref = form.find('#ref');
- this.setupAvailableRefs(availableRefs);
+ new RefSelectDropdown($('.js-branch-select'), availableRefs); // eslint-disable-line no-new
this.setupRestrictions();
this.addBinding();
this.init();
@@ -22,49 +24,6 @@
}
};
- NewBranchForm.prototype.setupAvailableRefs = function(availableRefs) {
- var $branchSelect = $('.js-branch-select');
-
- $branchSelect.glDropdown({
- data: availableRefs,
- filterable: true,
- filterByText: true,
- remote: false,
- fieldName: $branchSelect.data('field-name'),
- filterInput: 'input[type="search"]',
- 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;
- }
- }
- });
-
- const $dropdownContainer = $branchSelect.closest('.dropdown');
- const $fieldInput = $(`input[name="${$branchSelect.data('field-name')}"]`, $dropdownContainer);
- const $filterInput = $('input[type="search"]', $dropdownContainer);
-
- $filterInput.on('keyup', (e) => {
- const keyCode = e.keyCode || e.which;
- if (keyCode !== 13) return;
-
- const text = $filterInput.val();
- $fieldInput.val(text);
- $('.dropdown-toggle-text', $branchSelect).text(text);
-
- $dropdownContainer.removeClass('open');
- });
- };
-
NewBranchForm.prototype.setupRestrictions = function() {
var endsWith, invalid, single, startsWith;
startsWith = {
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index f6fe6d9f0fd..79293fe2d4a 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -12,7 +12,6 @@ require('./autosave');
window.autosize = require('vendor/autosize');
window.Dropzone = require('dropzone');
require('./dropzone_input');
-require('./gfm_auto_complete');
require('vendor/jquery.caret'); // required by jquery.atwho
require('vendor/jquery.atwho');
require('./task_list');
@@ -24,18 +23,18 @@ const normalizeNewlines = function(str) {
(function() {
this.Notes = (function() {
const MAX_VISIBLE_COMMIT_LIST_COUNT = 3;
- const REGEX_SLASH_COMMANDS = /\/\w+/g;
+ const REGEX_SLASH_COMMANDS = /^\/\w+.*$/gm;
Notes.interval = null;
- function Notes(notes_url, note_ids, last_fetched_at, view) {
+ function Notes(notes_url, note_ids, last_fetched_at, view, enableGFM = true) {
this.updateTargetButtons = this.updateTargetButtons.bind(this);
this.updateComment = this.updateComment.bind(this);
this.visibilityChange = this.visibilityChange.bind(this);
this.cancelDiscussionForm = this.cancelDiscussionForm.bind(this);
- this.addDiffNote = this.addDiffNote.bind(this);
+ this.onAddDiffNote = this.onAddDiffNote.bind(this);
this.setupDiscussionNoteForm = this.setupDiscussionNoteForm.bind(this);
- this.replyToDiscussionNote = this.replyToDiscussionNote.bind(this);
+ this.onReplyToDiscussionNote = this.onReplyToDiscussionNote.bind(this);
this.removeNote = this.removeNote.bind(this);
this.cancelEdit = this.cancelEdit.bind(this);
this.updateNote = this.updateNote.bind(this);
@@ -47,9 +46,11 @@ const normalizeNewlines = function(str) {
this.keydownNoteText = this.keydownNoteText.bind(this);
this.toggleCommitList = this.toggleCommitList.bind(this);
this.postComment = this.postComment.bind(this);
+ this.clearFlashWrapper = this.clearFlash.bind(this);
this.notes_url = notes_url;
this.note_ids = note_ids;
+ this.enableGFM = enableGFM;
// Used to keep track of updated notes while people are editing things
this.updatedNotesTrackingMap = {};
this.last_fetched_at = last_fetched_at;
@@ -57,6 +58,7 @@ const normalizeNewlines = function(str) {
this.notesCountBadge || (this.notesCountBadge = $(".issuable-details").find(".notes-tab .badge"));
this.basePollingInterval = 15000;
this.maxPollingSteps = 4;
+ this.flashErrors = [];
this.cleanBinding();
this.addBinding();
@@ -100,9 +102,9 @@ const normalizeNewlines = function(str) {
// update the file name when an attachment is selected
$(document).on("change", ".js-note-attachment-input", this.updateFormAttachment);
// reply to diff/discussion notes
- $(document).on("click", ".js-discussion-reply-button", this.replyToDiscussionNote);
+ $(document).on("click", ".js-discussion-reply-button", this.onReplyToDiscussionNote);
// add diff note
- $(document).on("click", ".js-add-diff-note-button", this.addDiffNote);
+ $(document).on("click", ".js-add-diff-note-button", this.onAddDiffNote);
// hide diff note form
$(document).on("click", ".js-close-discussion-note-form", this.cancelDiscussionForm);
// toggle commit list
@@ -285,6 +287,13 @@ const normalizeNewlines = function(str) {
}
};
+ Notes.prototype.setupNewNote = function($note) {
+ // Update datetime format on the recent note
+ gl.utils.localTimeAgo($note.find('.js-timeago'), false);
+ this.collapseLongCommitList();
+ this.taskList.init();
+ };
+
/*
Render note in main comments area.
@@ -298,27 +307,24 @@ const normalizeNewlines = function(str) {
if (!noteEntity.valid) {
if (noteEntity.errors.commands_only) {
- new Flash(noteEntity.errors.commands_only, 'notice', this.parentTimeline);
+ this.addFlash(noteEntity.errors.commands_only, 'notice', this.parentTimeline);
this.refresh();
}
return;
}
const $note = $notesList.find(`#note_${noteEntity.id}`);
- if (this.isNewNote(noteEntity)) {
+ if (Notes.isNewNote(noteEntity, this.note_ids)) {
this.note_ids.push(noteEntity.id);
const $newNote = Notes.animateAppendNote(noteEntity.html, $notesList);
- // Update datetime format on the recent note
- gl.utils.localTimeAgo($newNote.find('.js-timeago'), false);
- this.collapseLongCommitList();
- this.taskList.init();
+ this.setupNewNote($newNote);
this.refresh();
return this.updateNotesCount(1);
}
// The server can send the same update multiple times so we need to make sure to only update once per actual update.
- else if (this.isUpdatedNote(noteEntity, $note)) {
+ else if (Notes.isUpdatedNote(noteEntity, $note)) {
const isEditing = $note.hasClass('is-editing');
const initialContent = normalizeNewlines(
$note.find('.original-note-content').text().trim()
@@ -339,30 +345,11 @@ const normalizeNewlines = function(str) {
}
else {
const $updatedNote = Notes.animateUpdateNote(noteEntity.html, $note);
-
- // Update datetime format on the recent note
- gl.utils.localTimeAgo($updatedNote.find('.js-timeago'), false);
+ this.setupNewNote($updatedNote);
}
}
};
- /*
- Check if note does not exists on page
- */
-
- Notes.prototype.isNewNote = function(noteEntity) {
- return $.inArray(noteEntity.id, this.note_ids) === -1;
- };
-
- Notes.prototype.isUpdatedNote = function(noteEntity, $note) {
- // There can be CRLF vs LF mismatches if we don't sanitize and compare the same way
- const sanitizedNoteNote = normalizeNewlines(noteEntity.note);
- const currentNoteText = normalizeNewlines(
- $note.find('.original-note-content').text().trim()
- );
- return sanitizedNoteNote !== currentNoteText;
- };
-
Notes.prototype.isParallelView = function() {
return Cookies.get('diff_view') === 'parallel';
};
@@ -375,7 +362,7 @@ const normalizeNewlines = function(str) {
Notes.prototype.renderDiscussionNote = function(noteEntity, $form) {
var discussionContainer, form, row, lineType, diffAvatarContainer;
- if (!this.isNewNote(noteEntity)) {
+ if (!Notes.isNewNote(noteEntity, this.note_ids)) {
return;
}
this.note_ids.push(noteEntity.id);
@@ -522,7 +509,7 @@ const normalizeNewlines = function(str) {
Notes.prototype.setupNoteForm = function(form) {
var textarea, key;
- new gl.GLForm(form);
+ new gl.GLForm(form, this.enableGFM);
textarea = form.find(".js-note-text");
key = [
"Note",
@@ -551,14 +538,14 @@ const normalizeNewlines = function(str) {
return this.renderNote(note);
};
- Notes.prototype.addNoteError = ($form) => {
+ Notes.prototype.addNoteError = function($form) {
let formParentTimeline;
if ($form.hasClass('js-main-target-form')) {
formParentTimeline = $form.parents('.timeline');
} else if ($form.hasClass('js-discussion-note-form')) {
formParentTimeline = $form.closest('.discussion-notes').find('.notes');
}
- return new Flash('Your comment could not be submitted! Please check your network connection and try again.', 'alert', formParentTimeline);
+ return this.addFlash('Your comment could not be submitted! Please check your network connection and try again.', 'alert', formParentTimeline);
};
Notes.prototype.updateNoteError = $parentTimeline => new Flash('Your comment could not be updated! Please check your network connection and try again.');
@@ -593,12 +580,12 @@ const normalizeNewlines = function(str) {
Updates the current note field.
*/
- Notes.prototype.updateNote = function(_xhr, noteEntity, _status) {
+ Notes.prototype.updateNote = function(noteEntity, $targetNote) {
var $noteEntityEl, $note_li;
// Convert returned HTML to a jQuery object so we can modify it further
$noteEntityEl = $(noteEntity.html);
$noteEntityEl.addClass('fade-in-full');
- this.revertNoteEditForm();
+ this.revertNoteEditForm($targetNote);
gl.utils.localTimeAgo($('.js-timeago', $noteEntityEl));
$noteEntityEl.renderGFM();
$noteEntityEl.find('.js-task-list-container').taskList('enable');
@@ -680,10 +667,8 @@ const normalizeNewlines = function(str) {
if (this.updatedNotesTrackingMap[noteId]) {
const $newNote = $(this.updatedNotesTrackingMap[noteId].html);
$note.replaceWith($newNote);
+ this.setupNewNote($newNote);
this.updatedNotesTrackingMap[noteId] = null;
-
- // Update datetime format on the recent note
- gl.utils.localTimeAgo($newNote.find('.js-timeago'), false);
}
else {
$note.find('.js-finish-edit-warning').hide();
@@ -794,10 +779,14 @@ const normalizeNewlines = function(str) {
Shows the note form below the notes.
*/
- Notes.prototype.replyToDiscussionNote = function(e) {
+ Notes.prototype.onReplyToDiscussionNote = function(e) {
+ this.replyToDiscussionNote(e.target);
+ };
+
+ Notes.prototype.replyToDiscussionNote = function(target) {
var form, replyLink;
form = this.cleanForm(this.formClone.clone());
- replyLink = $(e.target).closest(".js-discussion-reply-button");
+ replyLink = $(target).closest(".js-discussion-reply-button");
// insert the form after the button
replyLink
.closest('.discussion-reply-holder')
@@ -867,35 +856,52 @@ const normalizeNewlines = function(str) {
Sets up the form and shows it.
*/
- Notes.prototype.addDiffNote = function(e) {
- var $link, addForm, hasNotes, lineType, newForm, nextRow, noteForm, notesContent, notesContentSelector, replyButton, row, rowCssToAdd, targetContent, isDiffCommentAvatar;
+ Notes.prototype.onAddDiffNote = function(e) {
e.preventDefault();
- $link = $(e.currentTarget || e.target);
+ const $link = $(e.currentTarget || e.target);
+ const showReplyInput = !$link.hasClass('js-diff-comment-avatar');
+ this.toggleDiffNote({
+ target: $link,
+ lineType: $link.data('lineType'),
+ showReplyInput
+ });
+ };
+
+ Notes.prototype.toggleDiffNote = function({
+ target,
+ lineType,
+ forceShow,
+ showReplyInput = false,
+ }) {
+ var $link, addForm, hasNotes, newForm, noteForm, replyButton, row, rowCssToAdd, targetContent, isDiffCommentAvatar;
+ $link = $(target);
row = $link.closest("tr");
- nextRow = row.next();
- hasNotes = nextRow.is(".notes_holder");
+ const nextRow = row.next();
+ let targetRow = row;
+ if (nextRow.is('.notes_holder')) {
+ targetRow = nextRow;
+ }
+
+ hasNotes = targetRow.is(".notes_holder");
addForm = false;
- notesContentSelector = ".notes_content";
+ let lineTypeSelector = '';
rowCssToAdd = "<tr class=\"notes_holder js-temp-notes-holder\"><td class=\"notes_line\" colspan=\"2\"></td><td class=\"notes_content\"><div class=\"content\"></div></td></tr>";
- isDiffCommentAvatar = $link.hasClass('js-diff-comment-avatar');
// In parallel view, look inside the correct left/right pane
if (this.isParallelView()) {
- lineType = $link.data("lineType");
- notesContentSelector += "." + lineType;
+ lineTypeSelector = `.${lineType}`;
rowCssToAdd = "<tr class=\"notes_holder js-temp-notes-holder\"><td class=\"notes_line old\"></td><td class=\"notes_content parallel old\"><div class=\"content\"></div></td><td class=\"notes_line new\"></td><td class=\"notes_content parallel new\"><div class=\"content\"></div></td></tr>";
}
- notesContentSelector += " .content";
- notesContent = nextRow.find(notesContentSelector);
+ const notesContentSelector = `.notes_content${lineTypeSelector} .content`;
+ let notesContent = targetRow.find(notesContentSelector);
- if (hasNotes && !isDiffCommentAvatar) {
- nextRow.show();
- notesContent = nextRow.find(notesContentSelector);
+ if (hasNotes && showReplyInput) {
+ targetRow.show();
+ notesContent = targetRow.find(notesContentSelector);
if (notesContent.length) {
notesContent.show();
replyButton = notesContent.find(".js-discussion-reply-button:visible");
if (replyButton.length) {
- e.target = replyButton[0];
- $.proxy(this.replyToDiscussionNote, replyButton[0], e).call();
+ this.replyToDiscussionNote(replyButton[0]);
} else {
// In parallel view, the form may not be present in one of the panes
noteForm = notesContent.find(".js-discussion-note-form");
@@ -904,19 +910,19 @@ const normalizeNewlines = function(str) {
}
}
}
- } else if (!isDiffCommentAvatar) {
+ } else if (showReplyInput) {
// add a notes row and insert the form
row.after(rowCssToAdd);
- nextRow = row.next();
- notesContent = nextRow.find(notesContentSelector);
+ targetRow = row.next();
+ notesContent = targetRow.find(notesContentSelector);
addForm = true;
} else {
- nextRow.show();
- notesContent.toggle(!notesContent.is(':visible'));
+ const isCurrentlyShown = targetRow.find('.content:not(:empty)').is(':visible');
+ const isForced = forceShow === true || forceShow === false;
+ const showNow = forceShow === true || (!isCurrentlyShown && !isForced);
- if (!nextRow.find('.content:not(:empty)').is(':visible')) {
- nextRow.hide();
- }
+ targetRow.toggle(showNow);
+ notesContent.toggle(showNow);
}
if (addForm) {
@@ -1101,6 +1107,15 @@ const normalizeNewlines = function(str) {
});
};
+ Notes.prototype.addFlash = function(...flashParams) {
+ this.flashErrors.push(new Flash(...flashParams));
+ };
+
+ Notes.prototype.clearFlash = function() {
+ this.flashErrors.forEach(flash => flash.flashContainer.remove());
+ this.flashErrors = [];
+ };
+
Notes.prototype.cleanForm = function($form) {
// Remove JS classes that are not needed here
$form
@@ -1115,6 +1130,25 @@ const normalizeNewlines = function(str) {
return $form;
};
+ /**
+ * Check if note does not exists on page
+ */
+ Notes.isNewNote = function(noteEntity, noteIds) {
+ return $.inArray(noteEntity.id, noteIds) === -1;
+ };
+
+ /**
+ * Check if $note already contains the `noteEntity` content
+ */
+ Notes.isUpdatedNote = function(noteEntity, $note) {
+ // There can be CRLF vs LF mismatches if we don't sanitize and compare the same way
+ const sanitizedNoteEntityText = normalizeNewlines(noteEntity.note.trim());
+ const currentNoteText = normalizeNewlines(
+ $note.find('.original-note-content').first().text().trim()
+ );
+ return sanitizedNoteEntityText !== currentNoteText;
+ };
+
Notes.checkMergeRequestStatus = function() {
if (gl.utils.getPagePath(1) === 'merge_requests') {
gl.mrWidget.checkStatus();
@@ -1170,6 +1204,7 @@ const normalizeNewlines = function(str) {
*/
Notes.prototype.createPlaceholderNote = function({ formContent, uniqueId, isDiscussionNote, currentUsername, currentUserFullname }) {
const discussionClass = isDiscussionNote ? 'discussion' : '';
+ const escapedFormContent = _.escape(formContent);
const $tempNote = $(
`<li id="${uniqueId}" class="note being-posted fade-in-half timeline-entry">
<div class="timeline-entry-inner">
@@ -1183,14 +1218,11 @@ const normalizeNewlines = function(str) {
<span class="hidden-xs">${currentUserFullname}</span>
<span class="note-headline-light">@${currentUsername}</span>
</a>
- <span class="note-headline-light">
- <i class="fa fa-spinner fa-spin" aria-label="Comment is being posted" aria-hidden="true"></i>
- </span>
</div>
</div>
<div class="note-body">
<div class="note-text">
- <p>${formContent}</p>
+ <p>${escapedFormContent}</p>
</div>
</div>
</div>
@@ -1281,6 +1313,8 @@ const normalizeNewlines = function(str) {
.then((note) => {
// Submission successful! remove placeholder
$notesContainer.find(`#${uniqueId}`).remove();
+ // Clear previous form errors
+ this.clearFlashWrapper();
// Check if this was discussion comment
if (isDiscussionForm) {
@@ -1320,7 +1354,7 @@ const normalizeNewlines = function(str) {
// Show form again on UI on failure
if (isDiscussionForm && $notesContainer.length) {
const replyButton = $notesContainer.parent().find('.js-discussion-reply-button');
- $.proxy(this.replyToDiscussionNote, replyButton[0], { target: replyButton[0] }).call();
+ this.replyToDiscussionNote(replyButton[0]);
$form = $notesContainer.parent().find('form');
}
@@ -1370,7 +1404,7 @@ const normalizeNewlines = function(str) {
gl.utils.ajaxPost(formAction, formData)
.then((note) => {
// Submission successful! render final note element
- this.updateNote(null, note, null);
+ this.updateNote(note, $editingNote);
})
.fail(() => {
// Submission failed, revert back to original note
diff --git a/app/assets/javascripts/pipeline_schedules/components/interval_pattern_input.js b/app/assets/javascripts/pipeline_schedules/components/interval_pattern_input.js
index 152e75b747e..4d623763ca7 100644
--- a/app/assets/javascripts/pipeline_schedules/components/interval_pattern_input.js
+++ b/app/assets/javascripts/pipeline_schedules/components/interval_pattern_input.js
@@ -24,9 +24,6 @@ export default {
};
},
computed: {
- showUnsetWarning() {
- return this.cronInterval === '';
- },
intervalIsPreset() {
return _.contains(this.cronIntervalPresets, this.cronInterval);
},
@@ -63,67 +60,75 @@ export default {
},
template: `
<div class="interval-pattern-form-group">
- <input
- id="custom"
- class="label-light"
- type="radio"
- :name="inputNameAttribute"
- :value="cronInterval"
- :checked="isEditable"
- @click="toggleCustomInput(true)"
- />
+ <div class="cron-preset-radio-input">
+ <input
+ id="custom"
+ class="label-light"
+ type="radio"
+ :name="inputNameAttribute"
+ :value="cronInterval"
+ :checked="isEditable"
+ @click="toggleCustomInput(true)"
+ />
- <label for="custom">
- Custom
- </label>
+ <label for="custom">
+ Custom
+ </label>
- <span class="cron-syntax-link-wrap">
- (<a :href="cronSyntaxUrl" target="_blank">Cron syntax</a>)
- </span>
+ <span class="cron-syntax-link-wrap">
+ (<a :href="cronSyntaxUrl" target="_blank">Cron syntax</a>)
+ </span>
+ </div>
- <input
- id="every-day"
- class="label-light"
- type="radio"
- v-model="cronInterval"
- :name="inputNameAttribute"
- :value="cronIntervalPresets.everyDay"
- @click="toggleCustomInput(false)"
- />
+ <div class="cron-preset-radio-input">
+ <input
+ id="every-day"
+ class="label-light"
+ type="radio"
+ v-model="cronInterval"
+ :name="inputNameAttribute"
+ :value="cronIntervalPresets.everyDay"
+ @click="toggleCustomInput(false)"
+ />
- <label class="label-light" for="every-day">
- Every day (at 4:00am)
- </label>
+ <label class="label-light" for="every-day">
+ Every day (at 4:00am)
+ </label>
+ </div>
- <input
- id="every-week"
- class="label-light"
- type="radio"
- v-model="cronInterval"
- :name="inputNameAttribute"
- :value="cronIntervalPresets.everyWeek"
- @click="toggleCustomInput(false)"
- />
+ <div class="cron-preset-radio-input">
+ <input
+ id="every-week"
+ class="label-light"
+ type="radio"
+ v-model="cronInterval"
+ :name="inputNameAttribute"
+ :value="cronIntervalPresets.everyWeek"
+ @click="toggleCustomInput(false)"
+ />
- <label class="label-light" for="every-week">
- Every week (Sundays at 4:00am)
- </label>
+ <label class="label-light" for="every-week">
+ Every week (Sundays at 4:00am)
+ </label>
+ </div>
- <input
- id="every-month"
- class="label-light"
- type="radio"
- v-model="cronInterval"
- :name="inputNameAttribute"
- :value="cronIntervalPresets.everyMonth"
- @click="toggleCustomInput(false)"
- />
+ <div class="cron-preset-radio-input">
+ <input
+ id="every-month"
+ class="label-light"
+ type="radio"
+ v-model="cronInterval"
+ :name="inputNameAttribute"
+ :value="cronIntervalPresets.everyMonth"
+ @click="toggleCustomInput(false)"
+ />
- <label class="label-light" for="every-month">
- Every month (on the 1st at 4:00am)
- </label>
+ <label class="label-light" for="every-month">
+ Every month (on the 1st at 4:00am)
+ </label>
+ </div>
- <div class="cron-interval-input-wrapper col-md-6">
+ <div class="cron-interval-input-wrapper">
<input
id="schedule_cron"
class="form-control inline cron-interval-input"
@@ -135,9 +140,6 @@ export default {
:disabled="!isEditable"
/>
</div>
- <span class="cron-unset-status col-md-3" v-if="showUnsetWarning">
- Schedule not yet set
- </span>
</div>
`,
};
diff --git a/app/assets/javascripts/pipeline_schedules/components/pipeline_schedules_callout.js b/app/assets/javascripts/pipeline_schedules/components/pipeline_schedules_callout.js
index 27ffe6ea304..5109b110b31 100644
--- a/app/assets/javascripts/pipeline_schedules/components/pipeline_schedules_callout.js
+++ b/app/assets/javascripts/pipeline_schedules/components/pipeline_schedules_callout.js
@@ -4,8 +4,10 @@ import illustrationSvg from '../icons/intro_illustration.svg';
const cookieKey = 'pipeline_schedules_callout_dismissed';
export default {
+ name: 'PipelineSchedulesCallout',
data() {
return {
+ docsUrl: document.getElementById('pipeline-schedules-callout').dataset.docsUrl,
illustrationSvg,
calloutDismissed: Cookies.get(cookieKey) === 'true',
};
@@ -28,13 +30,15 @@ export default {
<div class="svg-container" v-html="illustrationSvg"></div>
<div class="user-callout-copy">
<h4>Scheduling Pipelines</h4>
- <p>
- The pipelines schedule runs pipelines in the future, repeatedly, for specific branches or tags.
+ <p>
+ The pipelines schedule runs pipelines in the future, repeatedly, for specific branches or tags.
Those scheduled pipelines will inherit limited project access based on their associated user.
</p>
<p> Learn more in the
- <!-- FIXME -->
- <a href="random.com">pipeline schedules documentation</a>.
+ <a
+ :href="docsUrl"
+ target="_blank"
+ rel="nofollow">pipeline schedules documentation</a>. <!-- oneline to prevent extra space before period -->
</p>
</div>
</div>
diff --git a/app/assets/javascripts/pipeline_schedules/components/target_branch_dropdown.js b/app/assets/javascripts/pipeline_schedules/components/target_branch_dropdown.js
index 22e746ad2c3..0c3926d76b5 100644
--- a/app/assets/javascripts/pipeline_schedules/components/target_branch_dropdown.js
+++ b/app/assets/javascripts/pipeline_schedules/components/target_branch_dropdown.js
@@ -3,7 +3,7 @@ export default class TargetBranchDropdown {
this.$dropdown = $('.js-target-branch-dropdown');
this.$dropdownToggle = this.$dropdown.find('.dropdown-toggle-text');
this.$input = $('#schedule_ref');
- this.initialValue = this.$input.val();
+ this.initDefaultBranch();
this.initDropdown();
}
@@ -29,13 +29,23 @@ export default class TargetBranchDropdown {
}
setDropdownToggle() {
- if (this.initialValue) {
- this.$dropdownToggle.text(this.initialValue);
+ const initialValue = this.$input.val();
+
+ this.$dropdownToggle.text(initialValue);
+ }
+
+ initDefaultBranch() {
+ const initialValue = this.$input.val();
+ const defaultBranch = this.$dropdown.data('defaultBranch');
+
+ if (!initialValue) {
+ this.$input.val(defaultBranch);
}
}
updateInputValue({ selectedObj, e }) {
e.preventDefault();
+
this.$input.val(selectedObj.name);
gl.pipelineScheduleFieldErrors.updateFormValidityState();
}
diff --git a/app/assets/javascripts/pipeline_schedules/components/timezone_dropdown.js b/app/assets/javascripts/pipeline_schedules/components/timezone_dropdown.js
index c70e0502cf8..95ed9c7dc21 100644
--- a/app/assets/javascripts/pipeline_schedules/components/timezone_dropdown.js
+++ b/app/assets/javascripts/pipeline_schedules/components/timezone_dropdown.js
@@ -1,12 +1,14 @@
/* eslint-disable class-methods-use-this */
+const defaultTimezone = 'UTC';
+
export default class TimezoneDropdown {
constructor() {
this.$dropdown = $('.js-timezone-dropdown');
this.$dropdownToggle = this.$dropdown.find('.dropdown-toggle-text');
this.$input = $('#schedule_cron_timezone');
this.timezoneData = this.$dropdown.data('data');
- this.initialValue = this.$input.val();
+ this.initDefaultTimezone();
this.initDropdown();
}
@@ -42,12 +44,20 @@ export default class TimezoneDropdown {
return `[UTC ${this.formatUtcOffset(item.offset)}] ${item.name}`;
}
- setDropdownToggle() {
- if (this.initialValue) {
- this.$dropdownToggle.text(this.initialValue);
+ initDefaultTimezone() {
+ const initialValue = this.$input.val();
+
+ if (!initialValue) {
+ this.$input.val(defaultTimezone);
}
}
+ setDropdownToggle() {
+ const initialValue = this.$input.val();
+
+ this.$dropdownToggle.text(initialValue);
+ }
+
updateInputValue({ selectedObj, e }) {
e.preventDefault();
this.$input.val(selectedObj.identifier);
diff --git a/app/assets/javascripts/pipeline_schedules/pipeline_schedules_index_bundle.js b/app/assets/javascripts/pipeline_schedules/pipeline_schedules_index_bundle.js
index e36dc5db2ab..6584549ad06 100644
--- a/app/assets/javascripts/pipeline_schedules/pipeline_schedules_index_bundle.js
+++ b/app/assets/javascripts/pipeline_schedules/pipeline_schedules_index_bundle.js
@@ -1,9 +1,12 @@
import Vue from 'vue';
import PipelineSchedulesCallout from './components/pipeline_schedules_callout';
-const PipelineSchedulesCalloutComponent = Vue.extend(PipelineSchedulesCallout);
-
-document.addEventListener('DOMContentLoaded', () => {
- new PipelineSchedulesCalloutComponent()
- .$mount('#scheduling-pipelines-callout');
-});
+document.addEventListener('DOMContentLoaded', () => new Vue({
+ el: '#pipeline-schedules-callout',
+ components: {
+ 'pipeline-schedules-callout': PipelineSchedulesCallout,
+ },
+ render(createElement) {
+ return createElement('pipeline-schedules-callout');
+ },
+}));
diff --git a/app/assets/javascripts/pipelines/components/pipeline_url.js b/app/assets/javascripts/pipelines/components/pipeline_url.js
index 4e183d5c8ec..7cd2e0f9366 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_url.js
+++ b/app/assets/javascripts/pipelines/components/pipeline_url.js
@@ -1,3 +1,5 @@
+import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
+
export default {
props: [
'pipeline',
@@ -7,6 +9,9 @@ export default {
return !!this.pipeline.user;
},
},
+ components: {
+ userAvatarLink,
+ },
template: `
<td>
<a
@@ -15,21 +20,16 @@ export default {
<span class="pipeline-id">#{{pipeline.id}}</span>
</a>
<span>by</span>
- <a
- class="js-pipeline-url-user"
+ <user-avatar-link
v-if="user"
- :href="pipeline.user.web_url">
- <img
- v-if="user"
- class="avatar has-tooltip s20 "
- :title="pipeline.user.name"
- data-container="body"
- :src="pipeline.user.avatar_url"
- >
- </a>
+ class="js-pipeline-url-user"
+ :link-href="pipeline.user.web_url"
+ :img-src="pipeline.user.avatar_url"
+ :tooltip-text="pipeline.user.name"
+ />
<span
v-if="!user"
- class="js-pipeline-url-api api monospace">
+ class="js-pipeline-url-api api">
API
</span>
<span
diff --git a/app/assets/javascripts/pipelines/components/stage.js b/app/assets/javascripts/pipelines/components/stage.js
deleted file mode 100644
index 034e8d3280e..00000000000
--- a/app/assets/javascripts/pipelines/components/stage.js
+++ /dev/null
@@ -1,104 +0,0 @@
-/* global Flash */
-import { borderlessStatusIconEntityMap } from '../../vue_shared/ci_status_icons';
-
-export default {
- data() {
- return {
- builds: '',
- spinner: '<span class="fa fa-spinner fa-spin"></span>',
- };
- },
-
- props: {
- stage: {
- type: Object,
- required: true,
- },
- },
-
- updated() {
- if (this.builds) {
- this.stopDropdownClickPropagation();
- }
- },
-
- methods: {
- fetchBuilds(e) {
- const ariaExpanded = e.currentTarget.attributes['aria-expanded'];
-
- if (ariaExpanded && (ariaExpanded.textContent === 'true')) return null;
-
- return this.$http.get(this.stage.dropdown_path)
- .then((response) => {
- this.builds = JSON.parse(response.body).html;
- }, () => {
- const flash = new Flash('Something went wrong on our end.');
- return flash;
- });
- },
-
- /**
- * 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: {
- buildsOrSpinner() {
- return this.builds ? this.builds : this.spinner;
- },
- dropdownClass() {
- if (this.builds) return 'js-builds-dropdown-container';
- return 'js-builds-dropdown-loading builds-dropdown-loading';
- },
- buildStatus() {
- return `Build: ${this.stage.status.label}`;
- },
- tooltip() {
- return `has-tooltip ci-status-icon ci-status-icon-${this.stage.status.group}`;
- },
- triggerButtonClass() {
- return `mini-pipeline-graph-dropdown-toggle has-tooltip js-builds-dropdown-button ci-status-icon-${this.stage.status.group}`;
- },
- svgHTML() {
- return borderlessStatusIconEntityMap[this.stage.status.icon];
- },
- },
- watch: {
- 'stage.title': function stageTitle() {
- $(this.$refs.button).tooltip('destroy').tooltip();
- },
- },
- template: `
- <div>
- <button
- @click="fetchBuilds($event)"
- :class="triggerButtonClass"
- :title="stage.title"
- data-placement="top"
- data-toggle="dropdown"
- type="button"
- ref="button"
- :aria-label="stage.title">
- <span v-html="svgHTML" aria-hidden="true"></span>
- <i class="fa fa-caret-down" aria-hidden="true"></i>
- </button>
- <ul class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container">
- <div class="arrow-up" aria-hidden="true"></div>
- <div
- :class="dropdownClass"
- class="js-builds-dropdown-list scrollable-menu"
- v-html="buildsOrSpinner">
- </div>
- </ul>
- </div>
- `,
-};
diff --git a/app/assets/javascripts/pipelines/components/status.js b/app/assets/javascripts/pipelines/components/status.js
deleted file mode 100644
index 21a281af438..00000000000
--- a/app/assets/javascripts/pipelines/components/status.js
+++ /dev/null
@@ -1,60 +0,0 @@
-import canceledSvg from 'icons/_icon_status_canceled.svg';
-import createdSvg from 'icons/_icon_status_created.svg';
-import failedSvg from 'icons/_icon_status_failed.svg';
-import manualSvg from 'icons/_icon_status_manual.svg';
-import pendingSvg from 'icons/_icon_status_pending.svg';
-import runningSvg from 'icons/_icon_status_running.svg';
-import skippedSvg from 'icons/_icon_status_skipped.svg';
-import successSvg from 'icons/_icon_status_success.svg';
-import warningSvg from 'icons/_icon_status_warning.svg';
-
-export default {
- props: {
- pipeline: {
- type: Object,
- required: true,
- },
- },
-
- data() {
- const svgsDictionary = {
- icon_status_canceled: canceledSvg,
- icon_status_created: createdSvg,
- icon_status_failed: failedSvg,
- icon_status_manual: manualSvg,
- icon_status_pending: pendingSvg,
- icon_status_running: runningSvg,
- icon_status_skipped: skippedSvg,
- icon_status_success: successSvg,
- icon_status_warning: warningSvg,
- };
-
- return {
- svg: svgsDictionary[this.pipeline.details.status.icon],
- };
- },
-
- computed: {
- cssClasses() {
- return `ci-status ci-${this.pipeline.details.status.group}`;
- },
-
- detailsPath() {
- const { status } = this.pipeline.details;
- return status.has_details ? status.details_path : false;
- },
-
- content() {
- return `${this.svg} ${this.pipeline.details.status.text}`;
- },
- },
- template: `
- <td class="commit-link">
- <a
- :class="cssClasses"
- :href="detailsPath"
- v-html="content">
- </a>
- </td>
- `,
-};
diff --git a/app/assets/javascripts/pipelines/pipelines.js b/app/assets/javascripts/pipelines/pipelines.js
index 050551e5075..d6952d1ee5f 100644
--- a/app/assets/javascripts/pipelines/pipelines.js
+++ b/app/assets/javascripts/pipelines/pipelines.js
@@ -1,12 +1,12 @@
import Visibility from 'visibilityjs';
import PipelinesService from './services/pipelines_service';
import eventHub from './event_hub';
-import PipelinesTableComponent from '../vue_shared/components/pipelines_table';
+import pipelinesTableComponent from '../vue_shared/components/pipelines_table';
import tablePagination from '../vue_shared/components/table_pagination.vue';
-import EmptyState from './components/empty_state.vue';
-import ErrorState from './components/error_state.vue';
-import NavigationTabs from './components/navigation_tabs';
-import NavigationControls from './components/nav_controls';
+import emptyState from './components/empty_state.vue';
+import errorState from './components/error_state.vue';
+import navigationTabs from './components/navigation_tabs';
+import navigationControls from './components/nav_controls';
import loadingIcon from '../vue_shared/components/loading_icon.vue';
import Poll from '../lib/utils/poll';
@@ -20,11 +20,11 @@ export default {
components: {
tablePagination,
- 'pipelines-table-component': PipelinesTableComponent,
- 'empty-state': EmptyState,
- 'error-state': ErrorState,
- 'navigation-tabs': NavigationTabs,
- 'navigation-controls': NavigationControls,
+ pipelinesTableComponent,
+ emptyState,
+ errorState,
+ navigationTabs,
+ navigationControls,
loadingIcon,
},
@@ -52,6 +52,7 @@ export default {
hasError: false,
isMakingRequest: false,
updateGraphDropdown: false,
+ hasMadeRequest: false,
};
},
@@ -78,6 +79,7 @@ export default {
shouldRenderEmptyState() {
return !this.isLoading &&
!this.hasError &&
+ this.hasMadeRequest &&
!this.state.pipelines.length &&
(this.scope === 'all' || this.scope === null);
},
@@ -150,6 +152,10 @@ export default {
if (!Visibility.hidden()) {
this.isLoading = true;
poll.makeRequest();
+ } else {
+ // If tab is not visible we need to make the first request so we don't show the empty
+ // state without knowing if there are any pipelines
+ this.fetchPipelines();
}
Visibility.change(() => {
@@ -202,6 +208,7 @@ export default {
this.isLoading = false;
this.updateGraphDropdown = true;
+ this.hasMadeRequest = true;
},
errorCallback() {
diff --git a/app/assets/javascripts/ref_select_dropdown.js b/app/assets/javascripts/ref_select_dropdown.js
new file mode 100644
index 00000000000..215cd6fbdfd
--- /dev/null
+++ b/app/assets/javascripts/ref_select_dropdown.js
@@ -0,0 +1,46 @@
+class RefSelectDropdown {
+ constructor($dropdownButton, availableRefs) {
+ $dropdownButton.glDropdown({
+ data: availableRefs,
+ filterable: true,
+ filterByText: true,
+ remote: false,
+ fieldName: $dropdownButton.data('field-name'),
+ filterInput: 'input[type="search"]',
+ selectable: true,
+ isSelectable(branch, $el) {
+ return !$el.hasClass('is-active');
+ },
+ text(branch) {
+ return branch;
+ },
+ id(branch) {
+ return branch;
+ },
+ toggleLabel(branch) {
+ return branch;
+ },
+ });
+
+ const $dropdownContainer = $dropdownButton.closest('.dropdown');
+ const $fieldInput = $(`input[name="${$dropdownButton.data('field-name')}"]`, $dropdownContainer);
+ const $filterInput = $('input[type="search"]', $dropdownContainer);
+
+ $filterInput.on('keyup', (e) => {
+ const keyCode = e.keyCode || e.which;
+ if (keyCode !== 13) return;
+
+ const ref = $filterInput.val().trim();
+ if (ref === '') {
+ return;
+ }
+
+ $fieldInput.val(ref);
+ $('.dropdown-toggle-text', $dropdownButton).text(ref);
+
+ $dropdownContainer.removeClass('open');
+ });
+ }
+}
+
+export default RefSelectDropdown;
diff --git a/app/assets/javascripts/test.js b/app/assets/javascripts/test.js
new file mode 100644
index 00000000000..c4c7918a68f
--- /dev/null
+++ b/app/assets/javascripts/test.js
@@ -0,0 +1 @@
+$.fx.off = true;
diff --git a/app/assets/javascripts/todos.js b/app/assets/javascripts/todos.js
index 8be58023c84..7230946b484 100644
--- a/app/assets/javascripts/todos.js
+++ b/app/assets/javascripts/todos.js
@@ -1,5 +1,6 @@
/* eslint-disable class-methods-use-this, no-unneeded-ternary, quote-props */
-/* global UsersSelect */
+
+import UsersSelect from './users_select';
class Todos {
constructor() {
diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js
index 452578d5b98..aea3592c6ba 100644
--- a/app/assets/javascripts/users_select.js
+++ b/app/assets/javascripts/users_select.js
@@ -5,655 +5,669 @@
// TODO: remove eventHub hack after code splitting refactor
window.emitSidebarEvent = window.emitSidebarEvent || $.noop;
-(function() {
- const slice = [].slice;
-
- this.UsersSelect = (function() {
- function UsersSelect(currentUser, els) {
- var $els;
- this.users = this.users.bind(this);
- this.user = this.user.bind(this);
- this.usersPath = "/autocomplete/users.json";
- this.userPath = "/autocomplete/users/:id.json";
- if (currentUser != null) {
- if (typeof currentUser === 'object') {
- this.currentUser = currentUser;
- } else {
- this.currentUser = JSON.parse(currentUser);
- }
+function UsersSelect(currentUser, els) {
+ var $els;
+ this.users = this.users.bind(this);
+ this.user = this.user.bind(this);
+ this.usersPath = "/autocomplete/users.json";
+ this.userPath = "/autocomplete/users/:id.json";
+ if (currentUser != null) {
+ if (typeof currentUser === 'object') {
+ this.currentUser = currentUser;
+ } else {
+ this.currentUser = JSON.parse(currentUser);
+ }
+ }
+
+ $els = $(els);
+
+ if (!els) {
+ $els = $('.js-user-search');
+ }
+
+ $els.each((function(_this) {
+ return function(i, dropdown) {
+ var options = {};
+ var $block, $collapsedSidebar, $dropdown, $loading, $selectbox, $value, abilityName, assignTo, assigneeTemplate, collapsedAssigneeTemplate, defaultLabel, defaultNullUser, firstUser, issueURL, selectedId, selectedIdDefault, showAnyUser, showNullUser, showMenuAbove;
+ $dropdown = $(dropdown);
+ options.projectId = $dropdown.data('project-id');
+ options.groupId = $dropdown.data('group-id');
+ options.showCurrentUser = $dropdown.data('current-user');
+ options.todoFilter = $dropdown.data('todo-filter');
+ options.todoStateFilter = $dropdown.data('todo-state-filter');
+ showNullUser = $dropdown.data('null-user');
+ defaultNullUser = $dropdown.data('null-user-default');
+ showMenuAbove = $dropdown.data('showMenuAbove');
+ showAnyUser = $dropdown.data('any-user');
+ firstUser = $dropdown.data('first-user');
+ options.authorId = $dropdown.data('author-id');
+ defaultLabel = $dropdown.data('default-label');
+ issueURL = $dropdown.data('issueUpdate');
+ $selectbox = $dropdown.closest('.selectbox');
+ $block = $selectbox.closest('.block');
+ abilityName = $dropdown.data('ability-name');
+ $value = $block.find('.value');
+ $collapsedSidebar = $block.find('.sidebar-collapsed-user');
+ $loading = $block.find('.block-loading').fadeOut();
+ selectedIdDefault = (defaultNullUser && showNullUser) ? 0 : null;
+ selectedId = $dropdown.data('selected');
+
+ if (selectedId === undefined) {
+ selectedId = selectedIdDefault;
}
- $els = $(els);
+ const assignYourself = function () {
+ const unassignedSelected = $dropdown.closest('.selectbox')
+ .find(`input[name='${$dropdown.data('field-name')}'][value=0]`);
- if (!els) {
- $els = $('.js-user-search');
- }
+ if (unassignedSelected) {
+ unassignedSelected.remove();
+ }
- $els.each((function(_this) {
- return function(i, dropdown) {
- var options = {};
- var $block, $collapsedSidebar, $dropdown, $loading, $selectbox, $value, abilityName, assignTo, assigneeTemplate, collapsedAssigneeTemplate, defaultLabel, defaultNullUser, firstUser, issueURL, selectedId, selectedIdDefault, showAnyUser, showNullUser, showMenuAbove;
- $dropdown = $(dropdown);
- options.projectId = $dropdown.data('project-id');
- options.groupId = $dropdown.data('group-id');
- options.showCurrentUser = $dropdown.data('current-user');
- options.todoFilter = $dropdown.data('todo-filter');
- options.todoStateFilter = $dropdown.data('todo-state-filter');
- showNullUser = $dropdown.data('null-user');
- defaultNullUser = $dropdown.data('null-user-default');
- showMenuAbove = $dropdown.data('showMenuAbove');
- showAnyUser = $dropdown.data('any-user');
- firstUser = $dropdown.data('first-user');
- options.authorId = $dropdown.data('author-id');
- defaultLabel = $dropdown.data('default-label');
- issueURL = $dropdown.data('issueUpdate');
- $selectbox = $dropdown.closest('.selectbox');
- $block = $selectbox.closest('.block');
- abilityName = $dropdown.data('ability-name');
- $value = $block.find('.value');
- $collapsedSidebar = $block.find('.sidebar-collapsed-user');
- $loading = $block.find('.block-loading').fadeOut();
- selectedIdDefault = (defaultNullUser && showNullUser) ? 0 : null;
- selectedId = $dropdown.data('selected') || selectedIdDefault;
-
- const assignYourself = function () {
- const unassignedSelected = $dropdown.closest('.selectbox')
- .find(`input[name='${$dropdown.data('field-name')}'][value=0]`);
-
- if (unassignedSelected) {
- unassignedSelected.remove();
- }
+ // Save current selected user to the DOM
+ const input = document.createElement('input');
+ input.type = 'hidden';
+ input.name = $dropdown.data('field-name');
- // Save current selected user to the DOM
- const input = document.createElement('input');
- input.type = 'hidden';
- input.name = $dropdown.data('field-name');
+ const currentUserInfo = $dropdown.data('currentUserInfo');
- const currentUserInfo = $dropdown.data('currentUserInfo');
+ if (currentUserInfo) {
+ input.value = currentUserInfo.id;
+ input.dataset.meta = currentUserInfo.name;
+ } else if (_this.currentUser) {
+ input.value = _this.currentUser.id;
+ }
- if (currentUserInfo) {
- input.value = currentUserInfo.id;
- input.dataset.meta = currentUserInfo.name;
- } else if (_this.currentUser) {
- input.value = _this.currentUser.id;
- }
+ if ($selectbox) {
+ $dropdown.parent().before(input);
+ } else {
+ $dropdown.after(input);
+ }
+ };
- if ($selectbox) {
- $dropdown.parent().before(input);
- } else {
- $dropdown.after(input);
- }
- };
+ if ($block[0]) {
+ $block[0].addEventListener('assignYourself', assignYourself);
+ }
- if ($block[0]) {
- $block[0].addEventListener('assignYourself', assignYourself);
+ const getSelectedUserInputs = function() {
+ return $selectbox
+ .find(`input[name="${$dropdown.data('field-name')}"]`);
+ };
+
+ const getSelected = function() {
+ return getSelectedUserInputs()
+ .map((index, input) => parseInt(input.value, 10))
+ .get();
+ };
+
+ const checkMaxSelect = function() {
+ const maxSelect = $dropdown.data('max-select');
+ if (maxSelect) {
+ const selected = getSelected();
+
+ if (selected.length > maxSelect) {
+ const firstSelectedId = selected[0];
+ const firstSelected = $dropdown.closest('.selectbox')
+ .find(`input[name='${$dropdown.data('field-name')}'][value=${firstSelectedId}]`);
+
+ firstSelected.remove();
+ emitSidebarEvent('sidebar.removeAssignee', {
+ id: firstSelectedId,
+ });
}
+ }
+ };
+
+ const getMultiSelectDropdownTitle = function(selectedUser, isSelected) {
+ const selectedUsers = getSelected()
+ .filter(u => u !== 0);
+
+ const firstUser = getSelectedUserInputs()
+ .map((index, input) => ({
+ name: input.dataset.meta,
+ value: parseInt(input.value, 10),
+ }))
+ .filter(u => u.id !== 0)
+ .get(0);
+
+ if (selectedUsers.length === 0) {
+ return 'Unassigned';
+ } else if (selectedUsers.length === 1) {
+ return firstUser.name;
+ } else if (isSelected) {
+ const otherSelected = selectedUsers.filter(s => s !== selectedUser.id);
+ return `${selectedUser.name} + ${otherSelected.length} more`;
+ } else {
+ return `${firstUser.name} + ${selectedUsers.length - 1} more`;
+ }
+ };
- const getSelectedUserInputs = function() {
- return $selectbox
- .find(`input[name="${$dropdown.data('field-name')}"]`);
- };
-
- const getSelected = function() {
- return getSelectedUserInputs()
- .map((index, input) => parseInt(input.value, 10))
- .get();
- };
-
- const checkMaxSelect = function() {
- const maxSelect = $dropdown.data('max-select');
- if (maxSelect) {
- const selected = getSelected();
-
- if (selected.length > maxSelect) {
- const firstSelectedId = selected[0];
- const firstSelected = $dropdown.closest('.selectbox')
- .find(`input[name='${$dropdown.data('field-name')}'][value=${firstSelectedId}]`);
-
- firstSelected.remove();
- emitSidebarEvent('sidebar.removeAssignee', {
- id: firstSelectedId,
- });
+ $('.assign-to-me-link').on('click', (e) => {
+ e.preventDefault();
+ $(e.currentTarget).hide();
+
+ if ($dropdown.data('multiSelect')) {
+ assignYourself();
+ checkMaxSelect();
+
+ const currentUserInfo = $dropdown.data('currentUserInfo');
+ $dropdown.find('.dropdown-toggle-text').text(getMultiSelectDropdownTitle(currentUserInfo)).removeClass('is-default');
+ } else {
+ const $input = $(`input[name="${$dropdown.data('field-name')}"]`);
+ $input.val(gon.current_user_id);
+ selectedId = $input.val();
+ $dropdown.find('.dropdown-toggle-text').text(gon.current_user_fullname).removeClass('is-default');
+ }
+ });
+
+ $block.on('click', '.js-assign-yourself', (e) => {
+ e.preventDefault();
+ return assignTo(_this.currentUser.id);
+ });
+
+ assignTo = function(selected) {
+ var data;
+ data = {};
+ data[abilityName] = {};
+ data[abilityName].assignee_id = selected != null ? selected : null;
+ $loading.removeClass('hidden').fadeIn();
+ $dropdown.trigger('loading.gl.dropdown');
+
+ return $.ajax({
+ type: 'PUT',
+ dataType: 'json',
+ url: issueURL,
+ data: data
+ }).done(function(data) {
+ var user;
+ $dropdown.trigger('loaded.gl.dropdown');
+ $loading.fadeOut();
+ if (data.assignee) {
+ user = {
+ name: data.assignee.name,
+ username: data.assignee.username,
+ avatar: data.assignee.avatar_url
+ };
+ } else {
+ user = {
+ name: 'Unassigned',
+ username: '',
+ avatar: ''
+ };
+ }
+ $value.html(assigneeTemplate(user));
+ $collapsedSidebar.attr('title', user.name).tooltip('fixTitle');
+ return $collapsedSidebar.html(collapsedAssigneeTemplate(user));
+ });
+ };
+ collapsedAssigneeTemplate = _.template('<% if( avatar ) { %> <a class="author_link" href="/<%- username %>"> <img width="24" class="avatar avatar-inline s24" alt="" src="<%- avatar %>"> </a> <% } else { %> <i class="fa fa-user"></i> <% } %>');
+ assigneeTemplate = _.template('<% if (username) { %> <a class="author_link bold" href="/<%- username %>"> <% if( avatar ) { %> <img width="32" class="avatar avatar-inline s32" alt="" src="<%- avatar %>"> <% } %> <span class="author"><%- name %></span> <span class="username"> @<%- username %> </span> </a> <% } else { %> <span class="no-value assign-yourself"> No assignee - <a href="#" class="js-assign-yourself"> assign yourself </a> </span> <% } %>');
+ return $dropdown.glDropdown({
+ showMenuAbove: showMenuAbove,
+ data: function(term, callback) {
+ var isAuthorFilter;
+ isAuthorFilter = $('.js-author-search');
+ return _this.users(term, options, function(users) {
+ // GitLabDropdownFilter returns this.instance
+ // GitLabDropdownRemote returns this.options.instance
+ const glDropdown = this.instance || this.options.instance;
+ glDropdown.options.processData(term, users, callback);
+ }.bind(this));
+ },
+ processData: function(term, users, callback) {
+ let anyUser;
+ let index;
+ let j;
+ let len;
+ let name;
+ let obj;
+ let showDivider;
+ if (term.length === 0) {
+ showDivider = 0;
+ if (firstUser) {
+ // Move current user to the front of the list
+ for (index = j = 0, len = users.length; j < len; index = (j += 1)) {
+ obj = users[index];
+ if (obj.username === firstUser) {
+ users.splice(index, 1);
+ users.unshift(obj);
+ break;
+ }
}
}
- };
-
- const getMultiSelectDropdownTitle = function(selectedUser, isSelected) {
- const selectedUsers = getSelected()
- .filter(u => u !== 0);
-
- const firstUser = getSelectedUserInputs()
- .map((index, input) => ({
- name: input.dataset.meta,
- value: parseInt(input.value, 10),
- }))
- .filter(u => u.id !== 0)
- .get(0);
-
- if (selectedUsers.length === 0) {
- return 'Unassigned';
- } else if (selectedUsers.length === 1) {
- return firstUser.name;
- } else if (isSelected) {
- const otherSelected = selectedUsers.filter(s => s !== selectedUser.id);
- return `${selectedUser.name} + ${otherSelected.length} more`;
- } else {
- return `${firstUser.name} + ${selectedUsers.length - 1} more`;
+ if (showNullUser) {
+ showDivider += 1;
+ users.unshift({
+ beforeDivider: true,
+ name: 'Unassigned',
+ id: 0
+ });
+ }
+ if (showAnyUser) {
+ showDivider += 1;
+ name = showAnyUser;
+ if (name === true) {
+ name = 'Any User';
+ }
+ anyUser = {
+ beforeDivider: true,
+ name: name,
+ id: null
+ };
+ users.unshift(anyUser);
}
- };
-
- $('.assign-to-me-link').on('click', (e) => {
- e.preventDefault();
- $(e.currentTarget).hide();
-
- if ($dropdown.data('multiSelect')) {
- assignYourself();
- checkMaxSelect();
- const currentUserInfo = $dropdown.data('currentUserInfo');
- $dropdown.find('.dropdown-toggle-text').text(getMultiSelectDropdownTitle(currentUserInfo)).removeClass('is-default');
- } else {
- const $input = $(`input[name="${$dropdown.data('field-name')}"]`);
- $input.val(gon.current_user_id);
- selectedId = $input.val();
- $dropdown.find('.dropdown-toggle-text').text(gon.current_user_fullname).removeClass('is-default');
+ if (showDivider) {
+ users.splice(showDivider, 0, 'divider');
}
- });
- $block.on('click', '.js-assign-yourself', (e) => {
- e.preventDefault();
- return assignTo(_this.currentUser.id);
- });
+ if ($dropdown.hasClass('js-multiselect')) {
+ const selected = getSelected().filter(i => i !== 0);
- assignTo = function(selected) {
- var data;
- data = {};
- data[abilityName] = {};
- data[abilityName].assignee_id = selected != null ? selected : null;
- $loading.removeClass('hidden').fadeIn();
- $dropdown.trigger('loading.gl.dropdown');
-
- return $.ajax({
- type: 'PUT',
- dataType: 'json',
- url: issueURL,
- data: data
- }).done(function(data) {
- var user;
- $dropdown.trigger('loaded.gl.dropdown');
- $loading.fadeOut();
- if (data.assignee) {
- user = {
- name: data.assignee.name,
- username: data.assignee.username,
- avatar: data.assignee.avatar_url
- };
- } else {
- user = {
- name: 'Unassigned',
- username: '',
- avatar: ''
- };
- }
- $value.html(assigneeTemplate(user));
- $collapsedSidebar.attr('title', user.name).tooltip('fixTitle');
- return $collapsedSidebar.html(collapsedAssigneeTemplate(user));
- });
- };
- collapsedAssigneeTemplate = _.template('<% if( avatar ) { %> <a class="author_link" href="/<%- username %>"> <img width="24" class="avatar avatar-inline s24" alt="" src="<%- avatar %>"> </a> <% } else { %> <i class="fa fa-user"></i> <% } %>');
- assigneeTemplate = _.template('<% if (username) { %> <a class="author_link bold" href="/<%- username %>"> <% if( avatar ) { %> <img width="32" class="avatar avatar-inline s32" alt="" src="<%- avatar %>"> <% } %> <span class="author"><%- name %></span> <span class="username"> @<%- username %> </span> </a> <% } else { %> <span class="no-value assign-yourself"> No assignee - <a href="#" class="js-assign-yourself"> assign yourself </a> </span> <% } %>');
- return $dropdown.glDropdown({
- showMenuAbove: showMenuAbove,
- data: function(term, callback) {
- var isAuthorFilter;
- isAuthorFilter = $('.js-author-search');
- return _this.users(term, options, function(users) {
- // GitLabDropdownFilter returns this.instance
- // GitLabDropdownRemote returns this.options.instance
- const glDropdown = this.instance || this.options.instance;
- glDropdown.options.processData(term, users, callback);
- }.bind(this));
- },
- processData: function(term, users, callback) {
- let anyUser;
- let index;
- let j;
- let len;
- let name;
- let obj;
- let showDivider;
- if (term.length === 0) {
- showDivider = 0;
- if (firstUser) {
- // Move current user to the front of the list
- for (index = j = 0, len = users.length; j < len; index = (j += 1)) {
- obj = users[index];
- if (obj.username === firstUser) {
- users.splice(index, 1);
- users.unshift(obj);
- break;
- }
- }
- }
- if (showNullUser) {
+ if (selected.length > 0) {
+ if ($dropdown.data('dropdown-header')) {
showDivider += 1;
- users.unshift({
- beforeDivider: true,
- name: 'Unassigned',
- id: 0
+ users.splice(showDivider, 0, {
+ header: $dropdown.data('dropdown-header'),
});
}
- if (showAnyUser) {
- showDivider += 1;
- name = showAnyUser;
- if (name === true) {
- name = 'Any User';
- }
- anyUser = {
- beforeDivider: true,
- name: name,
- id: null
- };
- users.unshift(anyUser);
- }
- if (showDivider) {
- users.splice(showDivider, 0, 'divider');
- }
+ const selectedUsers = users
+ .filter(u => selected.indexOf(u.id) !== -1)
+ .sort((a, b) => a.name > b.name);
- if ($dropdown.hasClass('js-multiselect')) {
- const selected = getSelected().filter(i => i !== 0);
+ users = users.filter(u => selected.indexOf(u.id) === -1);
- if (selected.length > 0) {
- if ($dropdown.data('dropdown-header')) {
- showDivider += 1;
- users.splice(showDivider, 0, {
- header: $dropdown.data('dropdown-header'),
- });
- }
+ selectedUsers.forEach((selectedUser) => {
+ showDivider += 1;
+ users.splice(showDivider, 0, selectedUser);
+ });
- const selectedUsers = users
- .filter(u => selected.indexOf(u.id) !== -1)
- .sort((a, b) => a.name > b.name);
+ users.splice(showDivider + 1, 0, 'divider');
+ }
+ }
+ }
- users = users.filter(u => selected.indexOf(u.id) === -1);
+ callback(users);
+ if (showMenuAbove) {
+ $dropdown.data('glDropdown').positionMenuAbove();
+ }
+ },
+ filterable: true,
+ filterRemote: true,
+ search: {
+ fields: ['name', 'username']
+ },
+ selectable: true,
+ fieldName: $dropdown.data('field-name'),
+ toggleLabel: function(selected, el, glDropdown) {
+ const inputValue = glDropdown.filterInput.val();
+
+ if (this.multiSelect && inputValue === '') {
+ // Remove non-users from the fullData array
+ const users = glDropdown.filteredFullData();
+ const callback = glDropdown.parseData.bind(glDropdown);
+
+ // Update the data model
+ this.processData(inputValue, users, callback);
+ }
- selectedUsers.forEach((selectedUser) => {
- showDivider += 1;
- users.splice(showDivider, 0, selectedUser);
- });
+ if (this.multiSelect) {
+ return getMultiSelectDropdownTitle(selected, $(el).hasClass('is-active'));
+ }
- users.splice(showDivider + 1, 0, 'divider');
- }
- }
- }
+ if (selected && 'id' in selected && $(el).hasClass('is-active')) {
+ $dropdown.find('.dropdown-toggle-text').removeClass('is-default');
+ if (selected.text) {
+ return selected.text;
+ } else {
+ return selected.name;
+ }
+ } else {
+ $dropdown.find('.dropdown-toggle-text').addClass('is-default');
+ return defaultLabel;
+ }
+ },
+ defaultLabel: defaultLabel,
+ hidden: function(e) {
+ if ($dropdown.hasClass('js-multiselect')) {
+ emitSidebarEvent('sidebar.saveAssignees');
+ }
- callback(users);
- if (showMenuAbove) {
- $dropdown.data('glDropdown').positionMenuAbove();
- }
- },
- filterable: true,
- filterRemote: true,
- search: {
- fields: ['name', 'username']
- },
- selectable: true,
- fieldName: $dropdown.data('field-name'),
- toggleLabel: function(selected, el, glDropdown) {
- const inputValue = glDropdown.filterInput.val();
-
- if (this.multiSelect && inputValue === '') {
- // Remove non-users from the fullData array
- const users = glDropdown.filteredFullData();
- const callback = glDropdown.parseData.bind(glDropdown);
-
- // Update the data model
- this.processData(inputValue, users, callback);
- }
+ if (!$dropdown.data('always-show-selectbox')) {
+ $selectbox.hide();
- if (this.multiSelect) {
- return getMultiSelectDropdownTitle(selected, $(el).hasClass('is-active'));
- }
+ // Recalculate where .value is because vue might have changed it
+ $block = $selectbox.closest('.block');
+ $value = $block.find('.value');
+ // display:block overrides the hide-collapse rule
+ $value.css('display', '');
+ }
+ },
+ multiSelect: $dropdown.hasClass('js-multiselect'),
+ inputMeta: $dropdown.data('input-meta'),
+ clicked: function(options) {
+ const { $el, e, isMarking } = options;
+ const user = options.selectedObj;
+
+ if ($dropdown.hasClass('js-multiselect')) {
+ const isActive = $el.hasClass('is-active');
+ const previouslySelected = $dropdown.closest('.selectbox')
+ .find("input[name='" + ($dropdown.data('field-name')) + "'][value!=0]");
+
+ // Enables support for limiting the number of users selected
+ // Automatically removes the first on the list if more users are selected
+ checkMaxSelect();
+
+ if (user.beforeDivider && user.name.toLowerCase() === 'unassigned') {
+ // Unassigned selected
+ previouslySelected.each((index, element) => {
+ const id = parseInt(element.value, 10);
+ element.remove();
+ });
+ emitSidebarEvent('sidebar.removeAllAssignees');
+ } else if (isActive) {
+ // user selected
+ emitSidebarEvent('sidebar.addAssignee', user);
- if (selected && 'id' in selected && $(el).hasClass('is-active')) {
- $dropdown.find('.dropdown-toggle-text').removeClass('is-default');
- if (selected.text) {
- return selected.text;
- } else {
- return selected.name;
- }
- } else {
- $dropdown.find('.dropdown-toggle-text').addClass('is-default');
- return defaultLabel;
+ // Remove unassigned selection (if it was previously selected)
+ const unassignedSelected = $dropdown.closest('.selectbox')
+ .find("input[name='" + ($dropdown.data('field-name')) + "'][value=0]");
+
+ if (unassignedSelected) {
+ unassignedSelected.remove();
}
- },
- defaultLabel: defaultLabel,
- hidden: function(e) {
- if ($dropdown.hasClass('js-multiselect')) {
- emitSidebarEvent('sidebar.saveAssignees');
+ } else {
+ if (previouslySelected.length === 0) {
+ // Select unassigned because there is no more selected users
+ this.addInput($dropdown.data('field-name'), 0, {});
}
- if (!$dropdown.data('always-show-selectbox')) {
- $selectbox.hide();
+ // User unselected
+ emitSidebarEvent('sidebar.removeAssignee', user);
+ }
- // Recalculate where .value is because vue might have changed it
- $block = $selectbox.closest('.block');
- $value = $block.find('.value');
- // display:block overrides the hide-collapse rule
- $value.css('display', '');
- }
- },
- multiSelect: $dropdown.hasClass('js-multiselect'),
- inputMeta: $dropdown.data('input-meta'),
- clicked: function(options) {
- const { $el, e, isMarking } = options;
- const user = options.selectedObj;
-
- if ($dropdown.hasClass('js-multiselect')) {
- const isActive = $el.hasClass('is-active');
- const previouslySelected = $dropdown.closest('.selectbox')
- .find("input[name='" + ($dropdown.data('field-name')) + "'][value!=0]");
-
- // Enables support for limiting the number of users selected
- // Automatically removes the first on the list if more users are selected
- checkMaxSelect();
-
- if (user.beforeDivider && user.name.toLowerCase() === 'unassigned') {
- // Unassigned selected
- previouslySelected.each((index, element) => {
- const id = parseInt(element.value, 10);
- element.remove();
- });
- emitSidebarEvent('sidebar.removeAllAssignees');
- } else if (isActive) {
- // user selected
- emitSidebarEvent('sidebar.addAssignee', user);
+ if (getSelected().find(u => u === gon.current_user_id)) {
+ $('.assign-to-me-link').hide();
+ } else {
+ $('.assign-to-me-link').show();
+ }
+ }
- // Remove unassigned selection (if it was previously selected)
- const unassignedSelected = $dropdown.closest('.selectbox')
- .find("input[name='" + ($dropdown.data('field-name')) + "'][value=0]");
+ var isIssueIndex, isMRIndex, page, selected;
+ page = $('body').data('page');
+ isIssueIndex = page === 'projects:issues:index';
+ isMRIndex = (page === page && page === 'projects:merge_requests:index');
+ if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) {
+ e.preventDefault();
- if (unassignedSelected) {
- unassignedSelected.remove();
- }
- } else {
- if (previouslySelected.length === 0) {
- // Select unassigned because there is no more selected users
- this.addInput($dropdown.data('field-name'), 0, {});
- }
+ const isSelecting = (user.id !== selectedId);
+ selectedId = isSelecting ? user.id : selectedIdDefault;
- // User unselected
- emitSidebarEvent('sidebar.removeAssignee', user);
- }
+ if (selectedId === gon.current_user_id) {
+ $('.assign-to-me-link').hide();
+ } else {
+ $('.assign-to-me-link').show();
+ }
+ return;
+ }
+ if ($el.closest('.add-issues-modal').length) {
+ gl.issueBoards.ModalStore.store.filter[$dropdown.data('field-name')] = user.id;
+ } else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
+ return Issuable.filterResults($dropdown.closest('form'));
+ } else if ($dropdown.hasClass('js-filter-submit')) {
+ return $dropdown.closest('form').submit();
+ } else if (!$dropdown.hasClass('js-multiselect')) {
+ selected = $dropdown.closest('.selectbox').find("input[name='" + ($dropdown.data('field-name')) + "']").val();
+ return assignTo(selected);
+ }
- if (getSelected().find(u => u === gon.current_user_id)) {
- $('.assign-to-me-link').hide();
- } else {
- $('.assign-to-me-link').show();
- }
- }
+ // Automatically close dropdown after assignee is selected
+ // since CE has no multiple assignees
+ // EE does not have a max-select
+ if ($dropdown.data('max-select') &&
+ getSelected().length === $dropdown.data('max-select')) {
+ // Close the dropdown
+ $dropdown.dropdown('toggle');
+ }
+ },
+ id: function (user) {
+ return user.id;
+ },
+ opened: function(e) {
+ const $el = $(e.currentTarget);
+ const selected = getSelected();
+ if ($dropdown.hasClass('js-issue-board-sidebar') && selected.length === 0) {
+ this.addInput($dropdown.data('field-name'), 0, {});
+ }
+ $el.find('.is-active').removeClass('is-active');
- var isIssueIndex, isMRIndex, page, selected;
- page = $('body').data('page');
- isIssueIndex = page === 'projects:issues:index';
- isMRIndex = (page === page && page === 'projects:merge_requests:index');
- if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) {
- e.preventDefault();
+ function highlightSelected(id) {
+ $el.find(`li[data-user-id="${id}"] .dropdown-menu-user-link`).addClass('is-active');
+ }
- const isSelecting = (user.id !== selectedId);
- selectedId = isSelecting ? user.id : selectedIdDefault;
+ if (selected.length > 0) {
+ getSelected().forEach(selectedId => highlightSelected(selectedId));
+ } else if ($dropdown.hasClass('js-issue-board-sidebar')) {
+ highlightSelected(0);
+ } else {
+ highlightSelected(selectedId);
+ }
+ },
+ updateLabel: $dropdown.data('dropdown-title'),
+ renderRow: function(user) {
+ var avatar, img, listClosingTags, listWithName, listWithUserName, username;
+ username = user.username ? "@" + user.username : "";
+ avatar = user.avatar_url ? user.avatar_url : false;
- if (selectedId === gon.current_user_id) {
- $('.assign-to-me-link').hide();
- } else {
- $('.assign-to-me-link').show();
- }
- return;
- }
- if ($el.closest('.add-issues-modal').length) {
- gl.issueBoards.ModalStore.store.filter[$dropdown.data('field-name')] = user.id;
- } else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
- return Issuable.filterResults($dropdown.closest('form'));
- } else if ($dropdown.hasClass('js-filter-submit')) {
- return $dropdown.closest('form').submit();
- } else if (!$dropdown.hasClass('js-multiselect')) {
- selected = $dropdown.closest('.selectbox').find("input[name='" + ($dropdown.data('field-name')) + "']").val();
- return assignTo(selected);
- }
- },
- id: function (user) {
- return user.id;
- },
- opened: function(e) {
- const $el = $(e.currentTarget);
- if ($dropdown.hasClass('js-issue-board-sidebar')) {
- selectedId = parseInt($dropdown[0].dataset.selected, 10) || selectedIdDefault;
- }
- $el.find('.is-active').removeClass('is-active');
+ let selected = false;
- function highlightSelected(id) {
- $el.find(`li[data-user-id="${id}"] .dropdown-menu-user-link`).addClass('is-active');
- }
+ if (this.multiSelect) {
+ selected = getSelected().find(u => user.id === u);
- if ($selectbox[0]) {
- getSelected().forEach(selectedId => highlightSelected(selectedId));
- } else {
- highlightSelected(selectedId);
- }
- },
- updateLabel: $dropdown.data('dropdown-title'),
- renderRow: function(user) {
- var avatar, img, listClosingTags, listWithName, listWithUserName, username;
- username = user.username ? "@" + user.username : "";
- avatar = user.avatar_url ? user.avatar_url : false;
+ const fieldName = this.fieldName;
+ const field = $dropdown.closest('.selectbox').find("input[name='" + fieldName + "'][value='" + user.id + "']");
- let selected = user.id === parseInt(selectedId, 10);
+ if (field.length) {
+ selected = true;
+ }
+ } else {
+ selected = user.id === selectedId;
+ }
- if (this.multiSelect) {
- const fieldName = this.fieldName;
- const field = $dropdown.closest('.selectbox').find("input[name='" + fieldName + "'][value='" + user.id + "']");
+ img = "";
+ if (user.beforeDivider != null) {
+ `<li><a href='#' class='${selected === true ? 'is-active' : ''}'>${user.name}</a></li>`;
+ } else {
+ if (avatar) {
+ img = "<img src='" + avatar + "' class='avatar avatar-inline' width='32' />";
+ }
+ }
- if (field.length) {
- selected = true;
+ return `
+ <li data-user-id=${user.id}>
+ <a href='#' class='dropdown-menu-user-link ${selected === true ? 'is-active' : ''}'>
+ ${img}
+ <strong class='dropdown-menu-user-full-name'>
+ ${user.name}
+ </strong>
+ ${username ? `<span class='dropdown-menu-user-username'>${username}</span>` : ''}
+ </a>
+ </li>
+ `;
+ }
+ });
+ };
+ })(this));
+ $('.ajax-users-select').each((function(_this) {
+ return function(i, select) {
+ var firstUser, showAnyUser, showEmailUser, showNullUser;
+ var options = {};
+ options.skipLdap = $(select).hasClass('skip_ldap');
+ options.projectId = $(select).data('project-id');
+ options.groupId = $(select).data('group-id');
+ options.showCurrentUser = $(select).data('current-user');
+ options.pushCodeToProtectedBranches = $(select).data('push-code-to-protected-branches');
+ options.authorId = $(select).data('author-id');
+ options.skipUsers = $(select).data('skip-users');
+ showNullUser = $(select).data('null-user');
+ showAnyUser = $(select).data('any-user');
+ showEmailUser = $(select).data('email-user');
+ firstUser = $(select).data('first-user');
+ return $(select).select2({
+ placeholder: "Search for a user",
+ multiple: $(select).hasClass('multiselect'),
+ minimumInputLength: 0,
+ query: function(query) {
+ return _this.users(query.term, options, function(users) {
+ var anyUser, data, emailUser, index, j, len, name, nullUser, obj, ref;
+ data = {
+ results: users
+ };
+ if (query.term.length === 0) {
+ if (firstUser) {
+ // Move current user to the front of the list
+ ref = data.results;
+ for (index = j = 0, len = ref.length; j < len; index = (j += 1)) {
+ obj = ref[index];
+ if (obj.username === firstUser) {
+ data.results.splice(index, 1);
+ data.results.unshift(obj);
+ break;
+ }
}
}
-
- img = "";
- if (user.beforeDivider != null) {
- `<li><a href='#' class='${selected === true ? 'is-active' : ''}'>${user.name}</a></li>`;
- } else {
- if (avatar) {
- img = "<img src='" + avatar + "' class='avatar avatar-inline' width='32' />";
+ if (showNullUser) {
+ nullUser = {
+ name: 'Unassigned',
+ id: 0
+ };
+ data.results.unshift(nullUser);
+ }
+ if (showAnyUser) {
+ name = showAnyUser;
+ if (name === true) {
+ name = 'Any User';
}
+ anyUser = {
+ name: name,
+ id: null
+ };
+ data.results.unshift(anyUser);
}
-
- return `
- <li data-user-id=${user.id}>
- <a href='#' class='dropdown-menu-user-link ${selected === true ? 'is-active' : ''}'>
- ${img}
- <strong class='dropdown-menu-user-full-name'>
- ${user.name}
- </strong>
- ${username ? `<span class='dropdown-menu-user-username'>${username}</span>` : ''}
- </a>
- </li>
- `;
}
- });
- };
- })(this));
- $('.ajax-users-select').each((function(_this) {
- return function(i, select) {
- var firstUser, showAnyUser, showEmailUser, showNullUser;
- var options = {};
- options.skipLdap = $(select).hasClass('skip_ldap');
- options.projectId = $(select).data('project-id');
- options.groupId = $(select).data('group-id');
- options.showCurrentUser = $(select).data('current-user');
- options.pushCodeToProtectedBranches = $(select).data('push-code-to-protected-branches');
- options.authorId = $(select).data('author-id');
- options.skipUsers = $(select).data('skip-users');
- showNullUser = $(select).data('null-user');
- showAnyUser = $(select).data('any-user');
- showEmailUser = $(select).data('email-user');
- firstUser = $(select).data('first-user');
- return $(select).select2({
- placeholder: "Search for a user",
- multiple: $(select).hasClass('multiselect'),
- minimumInputLength: 0,
- query: function(query) {
- return _this.users(query.term, options, function(users) {
- var anyUser, data, emailUser, index, j, len, name, nullUser, obj, ref;
- data = {
- results: users
- };
- if (query.term.length === 0) {
- if (firstUser) {
- // Move current user to the front of the list
- ref = data.results;
- for (index = j = 0, len = ref.length; j < len; index = (j += 1)) {
- obj = ref[index];
- if (obj.username === firstUser) {
- data.results.splice(index, 1);
- data.results.unshift(obj);
- break;
- }
- }
- }
- if (showNullUser) {
- nullUser = {
- name: 'Unassigned',
- id: 0
- };
- data.results.unshift(nullUser);
- }
- if (showAnyUser) {
- name = showAnyUser;
- if (name === true) {
- name = 'Any User';
- }
- anyUser = {
- name: name,
- id: null
- };
- data.results.unshift(anyUser);
- }
- }
- if (showEmailUser && data.results.length === 0 && query.term.match(/^[^@]+@[^@]+$/)) {
- var trimmed = query.term.trim();
- emailUser = {
- name: "Invite \"" + query.term + "\"",
- username: trimmed,
- id: trimmed
- };
- data.results.unshift(emailUser);
- }
- return query.callback(data);
- });
- },
- initSelection: function() {
- var args;
- args = 1 <= arguments.length ? slice.call(arguments, 0) : [];
- return _this.initSelection.apply(_this, args);
- },
- formatResult: function() {
- var args;
- args = 1 <= arguments.length ? slice.call(arguments, 0) : [];
- return _this.formatResult.apply(_this, args);
- },
- formatSelection: function() {
- var args;
- args = 1 <= arguments.length ? slice.call(arguments, 0) : [];
- return _this.formatSelection.apply(_this, args);
- },
- dropdownCssClass: "ajax-users-dropdown",
- // we do not want to escape markup since we are displaying html in results
- escapeMarkup: function(m) {
- return m;
+ if (showEmailUser && data.results.length === 0 && query.term.match(/^[^@]+@[^@]+$/)) {
+ var trimmed = query.term.trim();
+ emailUser = {
+ name: "Invite \"" + query.term + "\"",
+ username: trimmed,
+ id: trimmed
+ };
+ data.results.unshift(emailUser);
}
+ return query.callback(data);
});
- };
- })(this));
- }
-
- UsersSelect.prototype.initSelection = function(element, callback) {
- var id, nullUser;
- id = $(element).val();
- if (id === "0") {
- nullUser = {
- name: 'Unassigned'
- };
- return callback(nullUser);
- } else if (id !== "") {
- return this.user(id, callback);
- }
- };
-
- UsersSelect.prototype.formatResult = function(user) {
- var avatar;
- if (user.avatar_url) {
- avatar = user.avatar_url;
- } else {
- avatar = gon.default_avatar_url;
- }
- return "<div class='user-result " + (!user.username ? 'no-username' : void 0) + "'> <div class='user-image'><img class='avatar s24' src='" + avatar + "'></div> <div class='user-name'>" + user.name + "</div> <div class='user-username'>" + (user.username || "") + "</div> </div>";
- };
-
- UsersSelect.prototype.formatSelection = function(user) {
- return user.name;
- };
-
- UsersSelect.prototype.user = function(user_id, callback) {
- if (!/^\d+$/.test(user_id)) {
- return false;
- }
-
- var url;
- url = this.buildUrl(this.userPath);
- url = url.replace(':id', user_id);
- return $.ajax({
- url: url,
- dataType: "json"
- }).done(function(user) {
- return callback(user);
- });
- };
-
- // Return users list. Filtered by query
- // Only active users retrieved
- UsersSelect.prototype.users = function(query, options, callback) {
- var url;
- url = this.buildUrl(this.usersPath);
- return $.ajax({
- url: url,
- data: {
- search: query,
- per_page: 20,
- active: true,
- project_id: options.projectId || null,
- group_id: options.groupId || null,
- skip_ldap: options.skipLdap || null,
- todo_filter: options.todoFilter || null,
- todo_state_filter: options.todoStateFilter || null,
- current_user: options.showCurrentUser || null,
- push_code_to_protected_branches: options.pushCodeToProtectedBranches || null,
- author_id: options.authorId || null,
- skip_users: options.skipUsers || null
},
- dataType: "json"
- }).done(function(users) {
- return callback(users);
+ initSelection: function() {
+ var args;
+ args = 1 <= arguments.length ? [].slice.call(arguments, 0) : [];
+ return _this.initSelection.apply(_this, args);
+ },
+ formatResult: function() {
+ var args;
+ args = 1 <= arguments.length ? [].slice.call(arguments, 0) : [];
+ return _this.formatResult.apply(_this, args);
+ },
+ formatSelection: function() {
+ var args;
+ args = 1 <= arguments.length ? [].slice.call(arguments, 0) : [];
+ return _this.formatSelection.apply(_this, args);
+ },
+ dropdownCssClass: "ajax-users-dropdown",
+ // we do not want to escape markup since we are displaying html in results
+ escapeMarkup: function(m) {
+ return m;
+ }
});
};
-
- UsersSelect.prototype.buildUrl = function(url) {
- if (gon.relative_url_root != null) {
- url = gon.relative_url_root.replace(/\/$/, '') + url;
- }
- return url;
+ })(this));
+}
+
+UsersSelect.prototype.initSelection = function(element, callback) {
+ var id, nullUser;
+ id = $(element).val();
+ if (id === "0") {
+ nullUser = {
+ name: 'Unassigned'
};
-
- return UsersSelect;
- })();
-}).call(window);
+ return callback(nullUser);
+ } else if (id !== "") {
+ return this.user(id, callback);
+ }
+};
+
+UsersSelect.prototype.formatResult = function(user) {
+ var avatar;
+ if (user.avatar_url) {
+ avatar = user.avatar_url;
+ } else {
+ avatar = gon.default_avatar_url;
+ }
+ return "<div class='user-result " + (!user.username ? 'no-username' : void 0) + "'> <div class='user-image'><img class='avatar s24' src='" + avatar + "'></div> <div class='user-name'>" + user.name + "</div> <div class='user-username'>" + (user.username || "") + "</div> </div>";
+};
+
+UsersSelect.prototype.formatSelection = function(user) {
+ return user.name;
+};
+
+UsersSelect.prototype.user = function(user_id, callback) {
+ if (!/^\d+$/.test(user_id)) {
+ return false;
+ }
+
+ var url;
+ url = this.buildUrl(this.userPath);
+ url = url.replace(':id', user_id);
+ return $.ajax({
+ url: url,
+ dataType: "json"
+ }).done(function(user) {
+ return callback(user);
+ });
+};
+
+// Return users list. Filtered by query
+// Only active users retrieved
+UsersSelect.prototype.users = function(query, options, callback) {
+ var url;
+ url = this.buildUrl(this.usersPath);
+ return $.ajax({
+ url: url,
+ data: {
+ search: query,
+ per_page: 20,
+ active: true,
+ project_id: options.projectId || null,
+ group_id: options.groupId || null,
+ skip_ldap: options.skipLdap || null,
+ todo_filter: options.todoFilter || null,
+ todo_state_filter: options.todoStateFilter || null,
+ current_user: options.showCurrentUser || null,
+ push_code_to_protected_branches: options.pushCodeToProtectedBranches || null,
+ author_id: options.authorId || null,
+ skip_users: options.skipUsers || null
+ },
+ dataType: "json"
+ }).done(function(users) {
+ return callback(users);
+ });
+};
+
+UsersSelect.prototype.buildUrl = function(url) {
+ if (gon.relative_url_root != null) {
+ url = gon.relative_url_root.replace(/\/$/, '') + url;
+ }
+ return url;
+};
+
+export default UsersSelect;
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js
index 3c23b8e472b..e8e22ad93a5 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js
@@ -1,7 +1,7 @@
/* global Flash */
import '~/lib/utils/datetime_utility';
-import { statusClassToSvgMap } from '../../vue_shared/pipeline_svg_icons';
+import { statusIconEntityMap } from '../../vue_shared/ci_status_icons';
import MemoryUsage from './mr_widget_memory_usage';
import MRWidgetService from '../services/mr_widget_service';
@@ -16,7 +16,7 @@ export default {
},
computed: {
svg() {
- return statusClassToSvgMap.icon_status_success;
+ return statusIconEntityMap.icon_status_success;
},
},
methods: {
@@ -56,7 +56,7 @@ export default {
<div class="ci-widget">
<div class="ci-status-icon ci-status-icon-success">
<span class="js-icon-link icon-link">
- <span
+ <span class="ci-status-icon"
v-html="svg"
aria-hidden="true"></span>
</span>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js
index 4a1fd881169..cfd34970f11 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js
@@ -12,6 +12,15 @@ export default {
commitsText() {
return gl.text.pluralize('commit', this.mr.divergedCommitsCount);
},
+ branchNameClipboardData() {
+ // This supports code in app/assets/javascripts/copy_to_clipboard.js that
+ // works around ClipboardJS limitations to allow the context-specific
+ // copy/pasting of plain text or GFM.
+ return JSON.stringify({
+ text: this.mr.sourceBranch,
+ gfm: `\`${this.mr.sourceBranch}\``,
+ });
+ },
},
methods: {
isBranchTitleLong(branchTitle) {
@@ -61,32 +70,31 @@ export default {
</span>
</div>
<div class="normal">
- <b>Request to merge</b>
- <span
- class="label-branch"
- :class="{'label-truncated has-tooltip': isBranchTitleLong(mr.sourceBranch)}"
- :title="isBranchTitleLong(mr.sourceBranch) ? mr.sourceBranch : ''"
- data-placement="bottom"
- v-html="mr.sourceBranchLink"></span>
- <button
- class="btn btn-transparent btn-clipboard has-tooltip"
- data-title="Copy branch name to clipboard"
- :data-clipboard-text="mr.sourceBranch">
- <i
- aria-hidden="true"
- class="fa fa-clipboard"></i>
- </button>
- <b>into</b>
- <span
- class="label-branch"
- :class="{'label-truncated has-tooltip': isBranchTitleLong(mr.targetBranch)}"
- :title="isBranchTitleLong(mr.targetBranch) ? mr.targetBranch : ''"
- data-placement="bottom">
- <a
- :href="mr.targetBranchCommitsPath">
- {{mr.targetBranch}}
- </a>
- </span>
+ <strong>
+ Request to merge
+ <span
+ class="label-branch"
+ :class="{'label-truncated has-tooltip': isBranchTitleLong(mr.sourceBranch)}"
+ :title="isBranchTitleLong(mr.sourceBranch) ? mr.sourceBranch : ''"
+ data-placement="bottom"
+ v-html="mr.sourceBranchLink"></span>
+ <button
+ class="btn btn-transparent btn-clipboard has-tooltip"
+ data-title="Copy branch name to clipboard"
+ :data-clipboard-text="branchNameClipboardData">
+ <i
+ aria-hidden="true"
+ class="fa fa-clipboard"></i>
+ </button>
+ into
+ <span
+ class="label-branch"
+ :class="{'label-truncated has-tooltip': isBranchTitleLong(mr.targetBranch)}"
+ :title="isBranchTitleLong(mr.targetBranch) ? mr.targetBranch : ''"
+ data-placement="bottom">
+ <a :href="mr.targetBranchPath">{{mr.targetBranch}}</a>
+ </span>
+ </strong>
<span
v-if="shouldShowCommitsBehindText"
class="diverged-commits-count">
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.js
index 801b9fb1ba1..c02e10128e2 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.js
@@ -1,6 +1,6 @@
-import PipelineStage from '../../pipelines/components/stage';
-import pipelineStatusIcon from '../../vue_shared/components/pipeline_status_icon';
-import { statusClassToSvgMap } from '../../vue_shared/pipeline_svg_icons';
+import PipelineStage from '../../pipelines/components/stage.vue';
+import ciIcon from '../../vue_shared/components/ci_icon.vue';
+import { statusIconEntityMap } from '../../vue_shared/ci_status_icons';
export default {
name: 'MRWidgetPipeline',
@@ -9,7 +9,7 @@ export default {
},
components: {
'pipeline-stage': PipelineStage,
- 'pipeline-status-icon': pipelineStatusIcon,
+ ciIcon,
},
computed: {
hasCIError() {
@@ -18,17 +18,20 @@ export default {
return hasCI && !ciStatus;
},
svg() {
- return statusClassToSvgMap.icon_status_failed;
+ return statusIconEntityMap.icon_status_failed;
},
stageText() {
return this.mr.pipeline.details.stages.length > 1 ? 'stages' : 'stage';
},
+ status() {
+ return this.mr.pipeline.details.status || {};
+ },
},
template: `
<div class="mr-widget-heading">
<div class="ci-widget">
<template v-if="hasCIError">
- <div class="ci-status-icon ci-status-icon-failed js-ci-error">
+ <div class="ci-status-icon ci-status-icon-failed ci-error js-ci-error">
<span class="js-icon-link icon-link">
<span
v-html="svg"
@@ -38,13 +41,22 @@ export default {
<span>Could not connect to the CI server. Please check your settings and try again.</span>
</template>
<template v-else>
- <pipeline-status-icon :pipelineStatus="mr.pipelineDetailedStatus" />
+ <div>
+ <a
+ class="icon-link"
+ :href="this.status.details_path">
+ <ci-icon :status="status" />
+ </a>
+ </div>
<span>
Pipeline
<a
:href="mr.pipeline.path"
class="pipeline-id">#{{mr.pipeline.id}}</a>
{{mr.pipeline.details.status.label}}
+ </span>
+ <span
+ v-if="mr.pipeline.details.stages.length > 0">
with {{stageText}}
</span>
<div class="mr-widget-pipeline-graph">
@@ -61,7 +73,7 @@ export default {
for
<a
:href="mr.pipeline.commit.commit_path"
- class="monospace js-commit-link">
+ class="commit-sha js-commit-link">
{{mr.pipeline.commit.short_id}}</a>.
</span>
<span
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.js
index fcccb17f58d..4063859d5d0 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.js
@@ -1,8 +1,23 @@
+import eventHub from '../../event_hub';
+
export default {
name: 'MRWidgetAutoMergeFailed',
props: {
mr: { type: Object, required: true },
},
+ data() {
+ return {
+ isRefreshing: false,
+ };
+ },
+ methods: {
+ refreshWidget() {
+ this.isRefreshing = true;
+ eventHub.$emit('MRWidgetUpdateRequested', () => {
+ this.isRefreshing = false;
+ });
+ },
+ },
template: `
<div class="mr-widget-body">
<button
@@ -13,8 +28,19 @@ export default {
</button>
<span class="bold danger">
This merge request failed to be merged automatically.
+ <button
+ @click="refreshWidget"
+ :class="{ disabled: isRefreshing }"
+ type="button"
+ class="btn btn-xs btn-default">
+ <i
+ v-if="isRefreshing"
+ class="fa fa-spinner fa-spin"
+ aria-hidden="true" />
+ Refresh
+ </button>
</span>
- <div class="merge-error-text">
+ <div class="merge-error-text danger bold">
{{mr.mergeError}}
</div>
</div>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.js
index 7e66441e5ff..fc2e42c6821 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.js
@@ -20,7 +20,7 @@ export default {
<p>
The changes were not merged into
<a
- :href="mr.targetBranchCommitsPath"
+ :href="mr.targetBranchPath"
class="label-branch">
{{mr.targetBranch}}</a>.
</p>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_locked.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_locked.js
index e3c27dfb76d..0bd31731a0b 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_locked.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_locked.js
@@ -16,7 +16,7 @@ export default {
The changes will be merged into
<span class="label-branch">
<a :href="mr.targetBranchPath">{{mr.targetBranch}}</a>
- </span>
+ </span>.
</p>
</section>
</div>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.js
index bcdbedcd46b..419d174f3ff 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.js
@@ -87,7 +87,7 @@ export default {
:href="mr.targetBranchPath"
class="label-branch">
{{mr.targetBranch}}
- </a>
+ </a>.
</p>
<p v-if="mr.shouldRemoveSourceBranch">
The source branch will be removed.
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_nothing_to_merge.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_nothing_to_merge.js
index 8c4535f1337..375a382615a 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_nothing_to_merge.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_nothing_to_merge.js
@@ -1,17 +1,42 @@
+import emptyStateSVG from 'icons/_mr_widget_empty_state.svg';
+
export default {
name: 'MRWidgetNothingToMerge',
+ props: {
+ mr: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return { emptyStateSVG };
+ },
template: `
- <div class="mr-widget-body">
- <button
- type="button"
- class="btn btn-success btn-small"
- disabled="true">
- Merge
- </button>
- <span class="bold">
- There is nothing to merge from source branch into target branch.
- Please push new commits or use a different branch.
- </span>
+ <div class="mr-widget-body empty-state">
+ <div class="row">
+ <div class="artwork col-sm-5 col-sm-push-7 col-xs-12 text-center">
+ <span v-html="emptyStateSVG"></span>
+ </div>
+ <div class="text col-sm-7 col-sm-pull-5 col-xs-12">
+ <span>
+ Merge requests are a place to propose changes you have made to a project
+ and discuss those changes with others.
+ </span>
+ <p>
+ Interested parties can even contribute by pushing commits if they want to.
+ </p>
+ <p>
+ Currently there are no changes in this merge request's source branch.
+ Please push new commits or use a different branch.
+ </p>
+ <a
+ v-if="mr.newBlobPath"
+ :href="mr.newBlobPath"
+ class="btn btn-inverted btn-save">
+ Create file
+ </a>
+ </div>
+ </div>
</div>
`,
};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js
index ebcc03e531b..74613a1089e 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js
@@ -33,7 +33,7 @@ export default {
return this.useCommitMessageWithDescription ? withoutDesc : withDesc;
},
mergeButtonClass() {
- const defaultClass = 'btn btn-success accept-merge-request';
+ const defaultClass = 'btn btn-small btn-success accept-merge-request';
const failedClass = `${defaultClass} btn-danger`;
const inActionClass = `${defaultClass} btn-info`;
const { pipeline, isPipelineActive, isPipelineFailed, hasCI, ciStatus } = this.mr;
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_sha_mismatch.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_sha_mismatch.js
new file mode 100644
index 00000000000..79f8ef408e6
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_sha_mismatch.js
@@ -0,0 +1,16 @@
+export default {
+ name: 'MRWidgetSHAMismatch',
+ template: `
+ <div class="mr-widget-body">
+ <button
+ type="button"
+ class="btn btn-success btn-small"
+ disabled="true">
+ Merge
+ </button>
+ <span class="bold">
+ The source branch HEAD has recently changed. Please reload the page and review the changes before merging.
+ </span>
+ </div>
+ `,
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/dependencies.js b/app/assets/javascripts/vue_merge_request_widget/dependencies.js
index b2eb32ead5f..bfe30ee4c08 100644
--- a/app/assets/javascripts/vue_merge_request_widget/dependencies.js
+++ b/app/assets/javascripts/vue_merge_request_widget/dependencies.js
@@ -27,6 +27,7 @@ export { default as NothingToMergeState } from './components/states/mr_widget_no
export { default as MissingBranchState } from './components/states/mr_widget_missing_branch';
export { default as NotAllowedState } from './components/states/mr_widget_not_allowed';
export { default as ReadyToMergeState } from './components/states/mr_widget_ready_to_merge';
+export { default as SHAMismatchState } from './components/states/mr_widget_sha_mismatch';
export { default as UnresolvedDiscussionsState } from './components/states/mr_widget_unresolved_discussions';
export { default as PipelineBlockedState } from './components/states/mr_widget_pipeline_blocked';
export { default as PipelineFailedState } from './components/states/mr_widget_pipeline_failed';
diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js
index 7c6c2d21714..5452e19bd8e 100644
--- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js
+++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js
@@ -16,6 +16,7 @@ import {
MissingBranchState,
NotAllowedState,
ReadyToMergeState,
+ SHAMismatchState,
UnresolvedDiscussionsState,
PipelineBlockedState,
PipelineFailedState,
@@ -203,6 +204,7 @@ export default {
'mr-widget-not-allowed': NotAllowedState,
'mr-widget-missing-branch': MissingBranchState,
'mr-widget-ready-to-merge': ReadyToMergeState,
+ 'mr-widget-sha-mismatch': SHAMismatchState,
'mr-widget-squash-before-merge': SquashBeforeMerge,
'mr-widget-checking': CheckingState,
'mr-widget-unresolved-discussions': UnresolvedDiscussionsState,
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js b/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js
index fee4113f3c8..fb78ea92da1 100644
--- a/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js
@@ -21,6 +21,8 @@ export default function deviseState(data) {
return 'unresolvedDiscussions';
} else if (this.isPipelineBlocked) {
return 'pipelineBlocked';
+ } else if (this.hasSHAChanged) {
+ return 'shaMismatch';
} else if (this.canBeMerged) {
return 'readyToMerge';
}
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
index faafeae5c5b..06661b930e3 100644
--- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
@@ -4,6 +4,7 @@ import { getStateKey } from '../dependencies';
export default class MergeRequestStore {
constructor(data) {
+ this.startingSha = data.diff_head_sha;
this.setData(data);
}
@@ -57,6 +58,7 @@ export default class MergeRequestStore {
this.statusPath = data.status_path;
this.emailPatchesPath = data.email_patches_path;
this.plainDiffPath = data.plain_diff_path;
+ this.newBlobPath = data.new_blob_path;
this.createIssueToResolveDiscussionsPath = data.create_issue_to_resolve_discussions_path;
this.mergeCheckPath = data.merge_check_path;
this.mergeActionsContentPath = data.commit_change_content_path;
@@ -67,6 +69,7 @@ export default class MergeRequestStore {
this.canMerge = !!data.merge_path;
this.canCreateIssue = currentUser.can_create_issue || false;
this.canCancelAutomaticMerge = !!data.cancel_merge_when_pipeline_succeeds_path;
+ this.hasSHAChanged = this.sha !== this.startingSha;
this.canBeMerged = data.can_be_merged || false;
// Cherry-pick and Revert actions related
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js b/app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js
index 625d7a01c65..605dd3a1ff4 100644
--- a/app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js
@@ -16,6 +16,7 @@ const stateToComponentMap = {
mergeWhenPipelineSucceeds: 'mr-widget-merge-when-pipeline-succeeds',
failedToMerge: 'mr-widget-failed-to-merge',
autoMergeFailed: 'mr-widget-auto-merge-failed',
+ shaMismatch: 'mr-widget-sha-mismatch',
};
const statesToShowHelpWidget = [
diff --git a/app/assets/javascripts/vue_shared/ci_action_icons.js b/app/assets/javascripts/vue_shared/ci_action_icons.js
index ee41dc95beb..b21f0ab49fd 100644
--- a/app/assets/javascripts/vue_shared/ci_action_icons.js
+++ b/app/assets/javascripts/vue_shared/ci_action_icons.js
@@ -3,24 +3,19 @@ import retrySVG from 'icons/_icon_action_retry.svg';
import playSVG from 'icons/_icon_action_play.svg';
import stopSVG from 'icons/_icon_action_stop.svg';
+/**
+ * For the provided action returns the respective SVG
+ *
+ * @param {String} action
+ * @return {SVG|String}
+ */
export default function getActionIcon(action) {
- let icon;
- switch (action) {
- case 'icon_action_cancel':
- icon = cancelSVG;
- break;
- case 'icon_action_retry':
- icon = retrySVG;
- break;
- case 'icon_action_play':
- icon = playSVG;
- break;
- case 'icon_action_stop':
- icon = stopSVG;
- break;
- default:
- icon = '';
- }
+ const icons = {
+ icon_action_cancel: cancelSVG,
+ icon_action_play: playSVG,
+ icon_action_retry: retrySVG,
+ icon_action_stop: stopSVG,
+ };
- return icon;
+ return icons[action] || '';
}
diff --git a/app/assets/javascripts/vue_shared/ci_status_icons.js b/app/assets/javascripts/vue_shared/ci_status_icons.js
index 48ad9214ac8..d9d0cad38e4 100644
--- a/app/assets/javascripts/vue_shared/ci_status_icons.js
+++ b/app/assets/javascripts/vue_shared/ci_status_icons.js
@@ -41,15 +41,3 @@ export const statusIconEntityMap = {
icon_status_success: SUCCESS_SVG,
icon_status_warning: WARNING_SVG,
};
-
-export const statusCssClasses = {
- icon_status_canceled: 'canceled',
- icon_status_created: 'created',
- icon_status_failed: 'failed',
- icon_status_manual: 'manual',
- icon_status_pending: 'pending',
- icon_status_running: 'running',
- icon_status_skipped: 'skipped',
- icon_status_success: 'success',
- icon_status_warning: 'warning',
-};
diff --git a/app/assets/javascripts/vue_shared/components/ci_badge_link.vue b/app/assets/javascripts/vue_shared/components/ci_badge_link.vue
new file mode 100644
index 00000000000..caa28bff6db
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/ci_badge_link.vue
@@ -0,0 +1,52 @@
+<script>
+import ciIcon from './ci_icon.vue';
+/**
+ * Renders CI Badge link with CI icon and status text based on
+ * API response shared between all places where it is used.
+ *
+ * Receives status object containing:
+ * status: {
+ * details_path: "/gitlab-org/gitlab-ce/pipelines/8150156" // url
+ * group:"running" // used for CSS class
+ * icon: "icon_status_running" // used to render the icon
+ * label:"running" // used for potential tooltip
+ * text:"running" // text rendered
+ * }
+ *
+ * Used in:
+ * - Pipelines table - first column
+ * - Jobs table - first column
+ * - Pipeline show view - header
+ * - Job show view - header
+ * - MR widget
+ */
+
+export default {
+ props: {
+ status: {
+ type: Object,
+ required: true,
+ },
+ },
+
+ components: {
+ ciIcon,
+ },
+
+ computed: {
+ cssClass() {
+ const className = this.status.group;
+
+ return className ? `ci-status ci-${this.status.group}` : 'ci-status';
+ },
+ },
+};
+</script>
+<template>
+ <a
+ :href="status.details_path"
+ :class="cssClass">
+ <ci-icon :status="status" />
+ {{status.text}}
+ </a>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/ci_icon.vue b/app/assets/javascripts/vue_shared/components/ci_icon.vue
index 4d44baaa3c4..ec88119e16c 100644
--- a/app/assets/javascripts/vue_shared/components/ci_icon.vue
+++ b/app/assets/javascripts/vue_shared/components/ci_icon.vue
@@ -1,6 +1,27 @@
<script>
- import { statusIconEntityMap, statusCssClasses } from '../../vue_shared/ci_status_icons';
+ import { statusIconEntityMap } from '../ci_status_icons';
+ /**
+ * Renders CI icon based on API response shared between all places where it is used.
+ *
+ * Receives status object containing:
+ * status: {
+ * details_path: "/gitlab-org/gitlab-ce/pipelines/8150156" // url
+ * group:"running" // used for CSS class
+ * icon: "icon_status_running" // used to render the icon
+ * label:"running" // used for potential tooltip
+ * text:"running" // text rendered
+ * }
+ *
+ * Used in:
+ * - Pipelines table Badge
+ * - Pipelines table mini graph
+ * - Pipeline graph
+ * - Pipeline show view badge
+ * - Jobs table
+ * - Jobs show view header
+ * - Jobs show view sidebar
+ */
export default {
props: {
status: {
@@ -15,7 +36,7 @@
},
cssClass() {
- const status = statusCssClasses[this.status.icon];
+ const status = this.status.group;
return `ci-status-icon ci-status-icon-${status} js-ci-status-icon-${status}`;
},
},
diff --git a/app/assets/javascripts/vue_shared/components/commit.js b/app/assets/javascripts/vue_shared/components/commit.js
index fb68abd95a2..23bc5fbc034 100644
--- a/app/assets/javascripts/vue_shared/components/commit.js
+++ b/app/assets/javascripts/vue_shared/components/commit.js
@@ -1,4 +1,5 @@
import commitIconSvg from 'icons/_icon_commit.svg';
+import userAvatarLink from './user_avatar/user_avatar_link.vue';
export default {
props: {
@@ -110,6 +111,9 @@ export default {
return { commitIconSvg };
},
+ components: {
+ userAvatarLink,
+ },
template: `
<div class="branch-commit">
@@ -119,30 +123,28 @@ export default {
</div>
<a v-if="hasCommitRef"
- class="monospace branch-name"
+ class="ref-name"
:href="commitRef.ref_url">
{{commitRef.name}}
</a>
<div v-html="commitIconSvg" class="commit-icon js-commit-icon"></div>
- <a class="commit-id monospace"
+ <a class="commit-sha"
:href="commitUrl">
{{shortSha}}
</a>
<p class="commit-title">
<span v-if="title">
- <a v-if="hasAuthor"
+ <user-avatar-link
+ v-if="hasAuthor"
class="avatar-image-container"
- :href="author.web_url">
- <img
- class="avatar has-tooltip s20"
- :src="author.avatar_url"
- :alt="userImageAltDescription"
- :title="author.username" />
- </a>
-
+ :link-href="author.web_url"
+ :img-src="author.avatar_url"
+ :img-alt="userImageAltDescription"
+ :tooltip-text="author.username"
+ />
<a class="commit-row-message"
:href="commitUrl">
{{title}}
diff --git a/app/assets/javascripts/vue_shared/components/pipeline_status_icon.js b/app/assets/javascripts/vue_shared/components/pipeline_status_icon.js
deleted file mode 100644
index ae246ada01b..00000000000
--- a/app/assets/javascripts/vue_shared/components/pipeline_status_icon.js
+++ /dev/null
@@ -1,23 +0,0 @@
-import { statusClassToSvgMap } from '../pipeline_svg_icons';
-
-export default {
- name: 'PipelineStatusIcon',
- props: {
- pipelineStatus: { type: Object, required: true, default: () => ({}) },
- },
- computed: {
- svg() {
- return statusClassToSvgMap[this.pipelineStatus.icon];
- },
- statusClass() {
- return `ci-status-icon ci-status-icon-${this.pipelineStatus.group}`;
- },
- },
- template: `
- <div :class="statusClass">
- <a class="icon-link" :href="pipelineStatus.details_path">
- <span v-html="svg" aria-hidden="true"></span>
- </a>
- </div>
- `,
-};
diff --git a/app/assets/javascripts/vue_shared/components/pipelines_table_row.js b/app/assets/javascripts/vue_shared/components/pipelines_table_row.js
index fbae85c85f6..30d16e4ed3e 100644
--- a/app/assets/javascripts/vue_shared/components/pipelines_table_row.js
+++ b/app/assets/javascripts/vue_shared/components/pipelines_table_row.js
@@ -2,7 +2,7 @@
import AsyncButtonComponent from '../../pipelines/components/async_button.vue';
import PipelinesActionsComponent from '../../pipelines/components/pipelines_actions';
import PipelinesArtifactsComponent from '../../pipelines/components/pipelines_artifacts';
-import PipelinesStatusComponent from '../../pipelines/components/status';
+import ciBadge from './ci_badge_link.vue';
import PipelinesStageComponent from '../../pipelines/components/stage.vue';
import PipelinesUrlComponent from '../../pipelines/components/pipeline_url';
import PipelinesTimeagoComponent from '../../pipelines/components/time_ago';
@@ -39,7 +39,7 @@ export default {
'commit-component': CommitComponent,
'dropdown-stage': PipelinesStageComponent,
'pipeline-url': PipelinesUrlComponent,
- 'status-scope': PipelinesStatusComponent,
+ ciBadge,
'time-ago': PipelinesTimeagoComponent,
},
@@ -62,10 +62,12 @@ export default {
commitAuthor() {
let commitAuthorInformation;
+ if (!this.pipeline || !this.pipeline.commit) {
+ return null;
+ }
+
// 1. person who is an author of a commit might be a GitLab user
- if (this.pipeline &&
- this.pipeline.commit &&
- this.pipeline.commit.author) {
+ if (this.pipeline.commit.author) {
// 2. if person who is an author of a commit is a GitLab user
// he/she can have a GitLab avatar
if (this.pipeline.commit.author.avatar_url) {
@@ -77,11 +79,8 @@ export default {
avatar_url: this.pipeline.commit.author_gravatar_url,
});
}
- }
-
- // 4. If committer is not a GitLab User he/she can have a Gravatar
- if (this.pipeline &&
- this.pipeline.commit) {
+ // 4. If committer is not a GitLab User he/she can have a Gravatar
+ } else {
commitAuthorInformation = {
avatar_url: this.pipeline.commit.author_gravatar_url,
web_url: `mailto:${this.pipeline.commit.author_email}`,
@@ -197,11 +196,20 @@ export default {
return '';
},
+
+ pipelineStatus() {
+ if (this.pipeline.details && this.pipeline.details.status) {
+ return this.pipeline.details.status;
+ }
+ return {};
+ },
},
template: `
<tr class="commit">
- <status-scope :pipeline="pipeline"/>
+ <td class="commit-link">
+ <ci-badge :status="pipelineStatus"/>
+ </td>
<pipeline-url :pipeline="pipeline"></pipeline-url>
diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue
new file mode 100644
index 00000000000..b8db6afda12
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue
@@ -0,0 +1,80 @@
+<script>
+
+/* This is a re-usable vue component for rendering a user avatar that
+ does not need to link to the user's profile. The image and an optional
+ tooltip can be configured by props passed to this component.
+
+ Sample configuration:
+
+ <user-avatar-image
+ :img-src="userAvatarSrc"
+ :img-alt="tooltipText"
+ :tooltip-text="tooltipText"
+ tooltip-placement="top"
+ />
+
+*/
+
+import defaultAvatarUrl from 'images/no_avatar.png';
+import TooltipMixin from '../../mixins/tooltip';
+
+export default {
+ name: 'UserAvatarImage',
+ mixins: [TooltipMixin],
+ props: {
+ imgSrc: {
+ type: String,
+ required: false,
+ default: defaultAvatarUrl,
+ },
+ cssClasses: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ imgAlt: {
+ type: String,
+ required: false,
+ default: 'user avatar',
+ },
+ size: {
+ type: Number,
+ required: false,
+ default: 20,
+ },
+ tooltipText: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ tooltipPlacement: {
+ type: String,
+ required: false,
+ default: 'top',
+ },
+ },
+ computed: {
+ tooltipContainer() {
+ return this.tooltipText ? 'body' : null;
+ },
+ avatarSizeClass() {
+ return `s${this.size}`;
+ },
+ },
+};
+</script>
+
+<template>
+ <img
+ class="avatar"
+ :class="[avatarSizeClass, cssClasses]"
+ :src="imgSrc"
+ :width="size"
+ :height="size"
+ :alt="imgAlt"
+ :data-container="tooltipContainer"
+ :data-placement="tooltipPlacement"
+ :title="tooltipText"
+ ref="tooltip"
+ />
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue
new file mode 100644
index 00000000000..95898d54cf7
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue
@@ -0,0 +1,80 @@
+<script>
+
+/* This is a re-usable vue component for rendering a user avatar wrapped in
+ a clickable link (likely to the user's profile). The link, image, and
+ tooltip can be configured by props passed to this component.
+
+ Sample configuration:
+
+ <user-avatar-link
+ :link-href="userProfileUrl"
+ :img-src="userAvatarSrc"
+ :img-alt="tooltipText"
+ :img-size="20"
+ :tooltip-text="tooltipText"
+ tooltip-placement="top"
+ />
+
+*/
+
+import userAvatarImage from './user_avatar_image.vue';
+
+export default {
+ name: 'UserAvatarLink',
+ components: {
+ userAvatarImage,
+ },
+ props: {
+ linkHref: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ imgSrc: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ imgAlt: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ imgCssClasses: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ imgSize: {
+ type: Number,
+ required: false,
+ default: 20,
+ },
+ tooltipText: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ tooltipPlacement: {
+ type: String,
+ required: false,
+ default: 'top',
+ },
+ },
+};
+</script>
+
+<template>
+ <a
+ class="user-avatar-link"
+ :href="linkHref">
+ <user-avatar-image
+ :img-src="imgSrc"
+ :img-alt="imgAlt"
+ :css-classes="imgCssClasses"
+ :size="imgSize"
+ :tooltip-text="tooltipText"
+ :tooltip-placement="tooltipPlacement"
+ />
+ </a>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_svg.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_svg.vue
new file mode 100644
index 00000000000..d2ff2ac006e
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_svg.vue
@@ -0,0 +1,45 @@
+<script>
+
+/* This is a re-usable vue component for rendering a user avatar svg (typically
+ for a blank state). It will receive styles comparable to the user avatar,
+ but no image is loaded, it isn't wrapped in a link, and tooltips aren't supported.
+ The svg and avatar size can be configured by props passed to this component.
+
+ Sample configuration:
+
+ <user-avatar-svg
+ :svg="potentialApproverSvg"
+ :size="20"
+ />
+
+*/
+
+export default {
+ props: {
+ svg: {
+ type: String,
+ required: true,
+ },
+ size: {
+ type: Number,
+ required: false,
+ default: 20,
+ },
+ },
+ computed: {
+ avatarSizeClass() {
+ return `s${this.size}`;
+ },
+ },
+};
+</script>
+
+<template>
+ <svg
+ :class="avatarSizeClass"
+ :height="size"
+ :width="size"
+ v-html="svg">
+ </svg>
+</template>
+
diff --git a/app/assets/javascripts/vue_shared/pipeline_svg_icons.js b/app/assets/javascripts/vue_shared/pipeline_svg_icons.js
deleted file mode 100644
index 5af30ae74f0..00000000000
--- a/app/assets/javascripts/vue_shared/pipeline_svg_icons.js
+++ /dev/null
@@ -1,43 +0,0 @@
-import canceledSvg from 'icons/_icon_status_canceled.svg';
-import createdSvg from 'icons/_icon_status_created.svg';
-import failedSvg from 'icons/_icon_status_failed.svg';
-import manualSvg from 'icons/_icon_status_manual.svg';
-import pendingSvg from 'icons/_icon_status_pending.svg';
-import runningSvg from 'icons/_icon_status_running.svg';
-import skippedSvg from 'icons/_icon_status_skipped.svg';
-import successSvg from 'icons/_icon_status_success.svg';
-import warningSvg from 'icons/_icon_status_warning.svg';
-
-import canceledBorderlessSvg from 'icons/_icon_status_canceled_borderless.svg';
-import createdBorderlessSvg from 'icons/_icon_status_created_borderless.svg';
-import failedBorderlessSvg from 'icons/_icon_status_failed_borderless.svg';
-import manualBorderlessSvg from 'icons/_icon_status_manual_borderless.svg';
-import pendingBorderlessSvg from 'icons/_icon_status_pending_borderless.svg';
-import runningBorderlessSvg from 'icons/_icon_status_running_borderless.svg';
-import skippedBorderlessSvg from 'icons/_icon_status_skipped_borderless.svg';
-import successBorderlessSvg from 'icons/_icon_status_success_borderless.svg';
-import warningBorderlessSvg from 'icons/_icon_status_warning_borderless.svg';
-
-export const statusClassToSvgMap = {
- icon_status_canceled: canceledSvg,
- icon_status_created: createdSvg,
- icon_status_failed: failedSvg,
- icon_status_manual: manualSvg,
- icon_status_pending: pendingSvg,
- icon_status_running: runningSvg,
- icon_status_skipped: skippedSvg,
- icon_status_success: successSvg,
- icon_status_warning: warningSvg,
-};
-
-export const statusClassToBorderlessSvgMap = {
- icon_status_canceled: canceledBorderlessSvg,
- icon_status_created: createdBorderlessSvg,
- icon_status_failed: failedBorderlessSvg,
- icon_status_manual: manualBorderlessSvg,
- icon_status_pending: pendingBorderlessSvg,
- icon_status_running: runningBorderlessSvg,
- icon_status_skipped: skippedBorderlessSvg,
- icon_status_success: successBorderlessSvg,
- icon_status_warning: warningBorderlessSvg,
-};
diff --git a/app/assets/stylesheets/framework/avatar.scss b/app/assets/stylesheets/framework/avatar.scss
index 91c1ebd5a7d..4ae2b164d2e 100644
--- a/app/assets/stylesheets/framework/avatar.scss
+++ b/app/assets/stylesheets/framework/avatar.scss
@@ -10,6 +10,8 @@
border-radius: $avatar_radius;
border: 1px solid $avatar-border;
&.s16 { @include avatar-size(16px, 6px); }
+ &.s18 { @include avatar-size(18px, 6px); }
+ &.s19 { @include avatar-size(19px, 6px); }
&.s20 { @include avatar-size(20px, 7px); }
&.s24 { @include avatar-size(24px, 8px); }
&.s26 { @include avatar-size(26px, 8px); }
diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss
index ac1fc0eb8ae..3dec911d289 100644
--- a/app/assets/stylesheets/framework/blocks.scss
+++ b/app/assets/stylesheets/framework/blocks.scss
@@ -312,7 +312,7 @@
}
.empty-state {
- margin: 100px 0 0;
+ margin: 5% auto 0;
.text-content {
max-width: 460px;
@@ -335,27 +335,12 @@
}
.btn {
- margin: $btn-side-margin $btn-side-margin 0 0;
- }
-
- @media(max-width: $screen-xs-max) {
- margin-top: 50px;
- text-align: center;
+ margin: $btn-side-margin 5px;
- .btn {
+ @media(max-width: $screen-xs-max) {
width: 100%;
}
}
-
- @media(min-width: $screen-xs-max) {
- &.merge-requests .text-content {
- margin-top: 40px;
- }
-
- &.labels .text-content {
- margin-top: 70px;
- }
- }
}
.flex-container-block {
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index 5c9b71a452c..5ab48b6c874 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -97,7 +97,7 @@
.fa-chevron-down {
font-size: $dropdown-chevron-size;
position: relative;
- top: -3px;
+ top: -2px;
margin-left: 5px;
}
diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss
index e624d0d951e..637731cc479 100644
--- a/app/assets/stylesheets/framework/filters.scss
+++ b/app/assets/stylesheets/framework/filters.scss
@@ -283,17 +283,10 @@
.filtered-search-history-dropdown-toggle-button {
flex: 1;
width: auto;
- padding-right: 10px;
-
border-radius: 0;
- border-top: 0;
- border-left: 0;
- border-bottom: 0;
+ border: 0;
border-right: 1px solid $border-color;
-
color: $gl-text-color-secondary;
- line-height: 1;
-
transition: color 0.1s linear;
&:hover,
@@ -301,6 +294,17 @@
color: $gl-text-color;
border-color: $dropdown-input-focus-border;
outline: none;
+
+ svg {
+ fill: $gl-text-color;
+ }
+ }
+
+ svg {
+ height: 14px;
+ width: 14px;
+ fill: $gl-text-color-secondary;
+ vertical-align: middle;
}
.dropdown-toggle-text {
@@ -312,11 +316,6 @@
color: inherit;
}
}
-
- .fa {
- position: static;
- }
-
}
.filtered-search-history-dropdown {
diff --git a/app/assets/stylesheets/framework/gfm.scss b/app/assets/stylesheets/framework/gfm.scss
index c0de09f3968..dbdd5a4464b 100644
--- a/app/assets/stylesheets/framework/gfm.scss
+++ b/app/assets/stylesheets/framework/gfm.scss
@@ -2,7 +2,7 @@
* Styles that apply to all GFM related forms.
*/
+.gfm-commit,
.gfm-commit_range {
- font-family: $monospace_font;
- font-size: 90%;
+ @extend .commit-sha;
}
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index 6d9218310eb..ce8b27a1951 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -24,19 +24,23 @@ header {
&.navbar-gitlab {
padding: 0 16px;
- z-index: 100;
+ z-index: 400;
margin-bottom: 0;
min-height: $header-height;
background-color: $gray-light;
border: none;
border-bottom: 1px solid $border-color;
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
@media (max-width: $screen-xs-min) {
padding: 0 16px;
}
&.with-horizontal-nav {
- border-bottom: none;
+ border-color: transparent;
}
.container-fluid {
@@ -110,6 +114,16 @@ header {
}
}
+ .navbar-border {
+ height: 1px;
+ position: absolute;
+ right: 0;
+ left: 0;
+ bottom: 0;
+ background-color: $border-color;
+ opacity: 0;
+ }
+
.global-dropdown {
position: absolute;
left: -10px;
diff --git a/app/assets/stylesheets/framework/icons.scss b/app/assets/stylesheets/framework/icons.scss
index 1b7d4e42258..ef864e8f6a9 100644
--- a/app/assets/stylesheets/framework/icons.scss
+++ b/app/assets/stylesheets/framework/icons.scss
@@ -65,3 +65,7 @@
text-decoration: none;
}
}
+
+.user-avatar-link {
+ text-decoration: none;
+}
diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss
index 20c7bc93c28..9e8acf4e73c 100644
--- a/app/assets/stylesheets/framework/layout.scss
+++ b/app/assets/stylesheets/framework/layout.scss
@@ -25,6 +25,10 @@ body {
.content-wrapper {
padding-bottom: 100px;
+
+ &:not(.page-with-layout-nav) {
+ margin-top: $header-height;
+ }
}
.container {
diff --git a/app/assets/stylesheets/framework/mobile.scss b/app/assets/stylesheets/framework/mobile.scss
index eb73f7cc794..678af978edd 100644
--- a/app/assets/stylesheets/framework/mobile.scss
+++ b/app/assets/stylesheets/framework/mobile.scss
@@ -112,7 +112,7 @@
}
}
- .issue_edited_ago,
+ .issue-edited-ago,
.note_edited_ago {
display: none;
}
diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss
index b6cf5101d60..e890c2e0634 100644
--- a/app/assets/stylesheets/framework/nav.scss
+++ b/app/assets/stylesheets/framework/nav.scss
@@ -24,10 +24,10 @@
}
@mixin scrolling-links() {
- white-space: nowrap;
overflow-x: auto;
overflow-y: hidden;
-webkit-overflow-scrolling: touch;
+ display: flex;
&::-webkit-scrollbar {
display: none;
@@ -35,6 +35,7 @@
}
.nav-links {
+ display: flex;
padding: 0;
margin: 0;
list-style: none;
@@ -42,17 +43,16 @@
border-bottom: 1px solid $border-color;
li {
- display: inline-block;
+ display: flex;
a {
- display: inline-block;
padding: $gl-btn-padding;
padding-bottom: 11px;
- margin-bottom: -1px;
font-size: 14px;
line-height: 28px;
color: $gl-text-color-secondary;
border-bottom: 2px solid transparent;
+ white-space: nowrap;
&:hover,
&:active,
@@ -85,10 +85,10 @@
.container-fluid {
background-color: $gray-normal;
margin-bottom: 0;
+ display: flex;
}
li {
-
&.active a {
border-bottom: none;
color: $link-underline-blue;
@@ -137,9 +137,9 @@
}
.nav-links {
- display: inline-block;
margin-bottom: 0;
border-bottom: none;
+ float: left;
&.wide {
width: 100%;
@@ -291,6 +291,7 @@
border-bottom: 1px solid $border-color;
transition: padding $sidebar-transition-duration;
text-align: center;
+ margin-top: $header-height;
.container-fluid {
position: relative;
@@ -336,6 +337,10 @@
border-bottom: none;
height: 51px;
+ @media (min-width: $screen-sm-min) {
+ justify-content: center;
+ }
+
li {
a {
padding-top: 10px;
@@ -347,6 +352,10 @@
.scrolling-tabs-container {
position: relative;
+ .merge-request-tabs-container & {
+ overflow: hidden;
+ }
+
.nav-links {
@include scrolling-links();
}
@@ -428,7 +437,7 @@
top: ($header-height + 1) * 3;
&.affix {
- top: 0;
+ top: $header-height;
}
}
}
@@ -484,10 +493,6 @@
.inner-page-scroll-tabs {
position: relative;
- .nav-links {
- padding-bottom: 1px;
- }
-
.fade-right {
@include fade(left, $white-light);
right: 0;
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index 746c9c25620..018f61ca3a8 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -53,6 +53,7 @@
.right-sidebar-expanded {
padding-right: 0;
+ z-index: 300;
@media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
&:not(.wiki-sidebar):not(.build-sidebar) .content-wrapper {
@@ -80,6 +81,6 @@
&.affix {
position: fixed;
- top: 0;
+ top: $header-height;
}
}
diff --git a/app/assets/stylesheets/framework/timeline.scss b/app/assets/stylesheets/framework/timeline.scss
index d2164a1d333..aa0c512a277 100644
--- a/app/assets/stylesheets/framework/timeline.scss
+++ b/app/assets/stylesheets/framework/timeline.scss
@@ -3,30 +3,6 @@
margin: 0;
padding: 0;
- .timeline-entry {
- padding: $gl-padding $gl-btn-padding 0;
- border-color: $white-normal;
- color: $gl-text-color;
- border-bottom: 1px solid $border-white-light;
-
- .timeline-entry-inner {
- position: relative;
- }
-
- &:target {
- background: $line-target-blue;
- }
-
- .avatar {
- margin-right: 15px;
- }
-
- .controls {
- padding-top: 10px;
- float: right;
- }
- }
-
.note-text {
p:last-child {
margin-bottom: 0;
@@ -46,20 +22,45 @@
}
}
+.timeline-entry {
+ padding: $gl-padding $gl-btn-padding 0;
+ border-color: $white-normal;
+ color: $gl-text-color;
+ border-bottom: 1px solid $border-white-light;
+
+ .timeline-entry-inner {
+ position: relative;
+ }
+
+ &:target,
+ &.target {
+ background: $line-target-blue;
+ }
+
+ .avatar {
+ margin-right: 15px;
+ }
+
+ .controls {
+ padding-top: 10px;
+ float: right;
+ }
+}
+
@media (max-width: $screen-xs-max) {
.timeline {
&::before {
background: none;
}
+ }
- .timeline-entry .timeline-entry-inner {
- .timeline-icon {
- display: none;
- }
+ .timeline-entry .timeline-entry-inner {
+ .timeline-icon {
+ display: none;
+ }
- .timeline-content {
- margin-left: 0;
- }
+ .timeline-content {
+ margin-left: 0;
}
}
}
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index 96d8a812723..0c3407f34f8 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -139,6 +139,15 @@
line-height: 1.6em;
overflow-x: auto;
border-radius: 2px;
+
+
+ &.plain-readme {
+ background: none;
+ border: none;
+ padding: 0;
+ margin: 0;
+ font-size: 14px;
+ }
}
p > code {
@@ -169,14 +178,14 @@
}
ul.task-list {
- li.task-list-item {
+ > li.task-list-item {
list-style-type: none;
position: relative;
min-height: 22px;
padding-left: 28px;
margin-left: 0 !important;
- input.task-list-item-checkbox {
+ > input.task-list-item-checkbox {
position: absolute;
left: 8px;
top: 5px;
@@ -279,19 +288,6 @@ h6 {
/** CODE **/
pre {
font-family: $monospace_font;
-
- &.plain-readme {
- background: none;
- border: none;
- padding: 0;
- margin: 0;
- font-size: 14px;
- }
-}
-
-.monospace {
- font-family: $monospace_font;
- font-size: 90%;
}
code {
@@ -305,6 +301,24 @@ a > code {
color: $link-color;
}
+.monospace {
+ font-family: $monospace_font;
+}
+
+.commit-sha,
+.ref-name {
+ @extend .monospace;
+ font-size: 95%;
+}
+
+.git-revision-dropdown-toggle {
+ @extend .monospace;
+}
+
+.git-revision-dropdown .dropdown-content ul li a {
+ @extend .ref-name;
+}
+
/**
* Apply Markdown typography
*
diff --git a/app/assets/stylesheets/framework/wells.scss b/app/assets/stylesheets/framework/wells.scss
index 32eb750180f..1c1392f8f67 100644
--- a/app/assets/stylesheets/framework/wells.scss
+++ b/app/assets/stylesheets/framework/wells.scss
@@ -12,10 +12,14 @@
}
&.branch-info {
- .monospace,
+ .commit-sha,
.commit-info {
margin-left: 4px;
}
+
+ .ref-name {
+ font-size: 12px;
+ }
}
}
diff --git a/app/assets/stylesheets/highlight/dark.scss b/app/assets/stylesheets/highlight/dark.scss
index 09951fe3d3e..6e3829d994f 100644
--- a/app/assets/stylesheets/highlight/dark.scss
+++ b/app/assets/stylesheets/highlight/dark.scss
@@ -185,6 +185,11 @@ $dark-il: #de935f;
color: $dark-highlight-color !important;
}
+ // Links to URLs, emails, or dependencies
+ .line a {
+ color: $dark-na;
+ }
+
.hll { background-color: $dark-hll-bg; }
.c { color: $dark-c; } /* Comment */
.err { color: $dark-err; } /* Error */
diff --git a/app/assets/stylesheets/highlight/monokai.scss b/app/assets/stylesheets/highlight/monokai.scss
index b6a6d298adf..68eb0c7720f 100644
--- a/app/assets/stylesheets/highlight/monokai.scss
+++ b/app/assets/stylesheets/highlight/monokai.scss
@@ -185,6 +185,11 @@ $monokai-gi: #a6e22e;
color: $black !important;
}
+ // Links to URLs, emails, or dependencies
+ .line a {
+ color: $monokai-k;
+ }
+
.hll { background-color: $monokai-hll; }
.c { color: $monokai-c; } /* Comment */
.err { color: $monokai-err-color; background-color: $monokai-err-bg; } /* Error */
diff --git a/app/assets/stylesheets/highlight/solarized_dark.scss b/app/assets/stylesheets/highlight/solarized_dark.scss
index 4f7a50dcb4f..2cc968c32f2 100644
--- a/app/assets/stylesheets/highlight/solarized_dark.scss
+++ b/app/assets/stylesheets/highlight/solarized_dark.scss
@@ -188,6 +188,11 @@ $solarized-dark-il: #2aa198;
background-color: $solarized-dark-highlight !important;
}
+ // Links to URLs, emails, or dependencies
+ .line a {
+ color: $solarized-dark-kd;
+ }
+
/* Solarized Dark
For use with Jekyll and Pygments
diff --git a/app/assets/stylesheets/highlight/solarized_light.scss b/app/assets/stylesheets/highlight/solarized_light.scss
index 6463fe96c1b..b61b85a2cd1 100644
--- a/app/assets/stylesheets/highlight/solarized_light.scss
+++ b/app/assets/stylesheets/highlight/solarized_light.scss
@@ -196,6 +196,11 @@ $solarized-light-il: #2aa198;
background-color: $solarized-light-highlight !important;
}
+ // Links to URLs, emails, or dependencies
+ .line a {
+ color: $solarized-light-kd;
+ }
+
/* Solarized Light
For use with Jekyll and Pygments
diff --git a/app/assets/stylesheets/highlight/white.scss b/app/assets/stylesheets/highlight/white.scss
index ab2018bfbca..1daa10aef24 100644
--- a/app/assets/stylesheets/highlight/white.scss
+++ b/app/assets/stylesheets/highlight/white.scss
@@ -203,6 +203,11 @@ $white-gc-bg: #eaf2f5;
background-color: $white-highlight !important;
}
+ // Links to URLs, emails, or dependencies
+ .line a {
+ color: $white-nb;
+ }
+
.hll { background-color: $white-hll-bg; }
.c { color: $white-c; font-style: italic; }
.err { color: $white-err; background-color: $white-err-bg; }
diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss
index 724b4080ee0..14a62b6cbf0 100644
--- a/app/assets/stylesheets/pages/builds.scss
+++ b/app/assets/stylesheets/pages/builds.scss
@@ -378,7 +378,7 @@
background-color: $row-hover;
}
- .fa-spinner {
+ .fa-refresh {
font-size: 13px;
margin-left: 3px;
}
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index c8996753809..bb72f453d1b 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -206,11 +206,11 @@
margin-left: $gl-padding;
}
}
-}
-.commit-short-id {
- font-family: $monospace_font;
- font-weight: 600;
+ .commit-sha {
+ font-size: 14px;
+ font-weight: 600;
+ }
}
.commit,
@@ -271,7 +271,7 @@
}
}
- .commit-id {
+ .commit-sha {
color: $gl-link-color;
}
diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss
index d29944207c5..7bec4bd5f56 100644
--- a/app/assets/stylesheets/pages/cycle_analytics.scss
+++ b/app/assets/stylesheets/pages/cycle_analytics.scss
@@ -387,7 +387,7 @@
padding: 0 3px 0 0;
}
- .branch-name {
+ .ref-name {
color: $black;
display: inline-block;
max-width: 180px;
@@ -398,7 +398,7 @@
vertical-align: top;
}
- .short-sha {
+ .commit-sha {
color: $gl-link-color;
line-height: 1.3;
vertical-align: top;
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index 0d5c4aed971..cfb1df4df84 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -2,11 +2,6 @@
.diff-file {
margin-bottom: $gl-padding;
- .commit-short-id {
- font-family: $regular_font;
- font-weight: 400;
- }
-
.file-title,
.file-title-flex-parent {
cursor: pointer;
diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss
index af991a128f3..48d3b7b1d07 100644
--- a/app/assets/stylesheets/pages/environments.scss
+++ b/app/assets/stylesheets/pages/environments.scss
@@ -68,10 +68,6 @@
margin: 0;
}
- .avatar-image-container {
- text-decoration: none;
- }
-
.icon-play {
height: 13px;
width: 12px;
@@ -90,7 +86,7 @@
}
.build-link,
- .branch-name {
+ .ref-name {
color: $gl-text-color;
}
@@ -135,7 +131,7 @@
}
.branch-commit {
- .commit-id {
+ .commit-sha {
margin-right: 0;
}
}
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index c4210ffd823..9a63f758ce1 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -10,7 +10,6 @@
.page-content-header,
.commit-box,
.info-well,
- .notes,
.commit-ci-menu,
.files-changed {
@extend .fixed-width-container;
@@ -23,16 +22,6 @@
.merge-manually {
@extend .fixed-width-container;
}
-
- .merge-request-tabs-holder {
- &.affix {
- border-bottom: 1px solid $border-color;
-
- .nav-links {
- border: 0;
- }
- }
- }
}
.merge-request-details {
@@ -205,8 +194,17 @@
right: 0;
transition: width .3s;
background: $gray-light;
- padding: 10px 20px;
- z-index: 2;
+ padding: 0 20px;
+ z-index: 200;
+ overflow: hidden;
+
+ .issuable-sidebar {
+ width: calc(100% + 100px);
+ height: 100%;
+ overflow-y: scroll;
+ overflow-x: hidden;
+ -webkit-overflow-scrolling: touch;
+ }
&.right-sidebar-expanded {
width: $gutter_width;
@@ -220,6 +218,10 @@
}
}
+ .issuable-sidebar-header {
+ padding-top: 10px;
+ }
+
.assign-yourself .btn-link {
padding-left: 0;
}
@@ -273,11 +275,10 @@
}
width: $gutter_collapsed_width;
- padding-top: 0;
+ padding: 0;
.block {
width: $gutter_collapsed_width - 2px;
- margin-left: -19px;
padding: 15px 0 0;
border-bottom: none;
overflow: hidden;
diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss
index ad3b6e0344b..bee9b13b375 100644
--- a/app/assets/stylesheets/pages/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -51,6 +51,7 @@ ul.related-merge-requests > li {
display: -ms-flexbox;
display: -webkit-flex;
display: flex;
+ align-items: center;
.merge-request-id {
flex-shrink: 0;
@@ -59,6 +60,14 @@ ul.related-merge-requests > li {
.merge-request-info {
margin-left: 5px;
}
+
+ .row_title {
+ vertical-align: bottom;
+ }
+
+ gl-emoji {
+ font-size: 1em;
+ }
}
.merge-requests-title,
@@ -114,7 +123,6 @@ ul.related-merge-requests > li {
.related-merge-requests {
.ci-status-link {
display: block;
- margin-top: 3px;
margin-right: 5px;
}
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index 75c57e369e7..1ac9d5af21d 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -90,11 +90,6 @@
align-items: center;
padding: $gl-padding-top $gl-padding 0;
- i,
- svg {
- margin-right: 8px;
- }
-
svg {
position: relative;
top: 1px;
@@ -109,9 +104,14 @@
flex-wrap: wrap;
}
- .ci-status-icon > .icon-link svg {
+ .icon-link > .ci-status-icon > svg {
width: 22px;
height: 22px;
+ margin-right: 8px;
+ }
+
+ .ci-error {
+ margin-right: $btn-side-margin;
}
}
@@ -125,6 +125,7 @@
.dropdown-menu {
margin-top: 11px;
+ z-index: 200;
}
.ci-action-icon-wrapper {
@@ -160,6 +161,34 @@
text-transform: capitalize;
}
+ .label-branch {
+ @extend .ref-name;
+
+ color: $gl-text-color;
+ font-weight: bold;
+ overflow: hidden;
+ margin: 0 3px;
+ word-break: break-all;
+
+ &.label-truncated {
+ position: relative;
+ display: inline-block;
+ width: 250px;
+ margin-bottom: -3px;
+ white-space: nowrap;
+ text-overflow: clip;
+ line-height: 14px;
+
+ &::after {
+ position: absolute;
+ content: '...';
+ right: 0;
+ font-family: $regular_font;
+ background-color: $gray-light;
+ }
+ }
+ }
+
.js-deployment-link {
display: inline-block;
}
@@ -188,6 +217,16 @@
}
.mr-widget-body {
+ .btn {
+ font-size: 15px;
+ }
+
+ .btn-group .btn {
+ padding: 5px 10px;
+ }
+ }
+
+ .mr-widget-body {
h4 {
font-weight: 600;
font-size: 16px;
@@ -219,6 +258,7 @@
.bold {
margin-left: 5px;
font-weight: bold;
+ font-size: 15px;
color: $gl-gray-light;
}
@@ -242,6 +282,11 @@
margin-bottom: 24px;
}
+ .spacing,
+ .bold {
+ vertical-align: middle;
+ }
+
.dropdown-menu {
li a {
padding: 5px;
@@ -324,6 +369,22 @@
margin-top: 10px;
margin-left: 12px;
}
+
+ &.empty-state {
+ .artwork {
+ margin-bottom: $gl-padding;
+ }
+
+ .text {
+ span {
+ font-weight: bold;
+ }
+
+ p {
+ margin-top: $gl-padding;
+ }
+ }
+ }
}
.mr-widget-footer {
@@ -359,34 +420,6 @@
}
}
-.label-branch {
- color: $gl-text-color;
- font-family: $monospace_font;
- font-weight: bold;
- overflow: hidden;
- font-size: 90%;
- margin: 0 3px;
- word-break: break-all;
-
- &.label-truncated {
- position: relative;
- display: inline-block;
- width: 250px;
- margin-bottom: -3px;
- white-space: nowrap;
- text-overflow: clip;
- line-height: 14px;
-
- &::after {
- position: absolute;
- content: '...';
- right: 0;
- font-family: $regular_font;
- background-color: $gray-light;
- }
- }
-}
-
.commits-empty {
text-align: center;
@@ -693,12 +726,18 @@
}
.merge-request-tabs-holder {
+ top: $header-height;
+ z-index: 100;
background-color: $white-light;
+ border-bottom: 1px solid $border-color;
+
+ @media(min-width: $screen-sm-min) {
+ position: sticky;
+ position: -webkit-sticky;
+ }
&.affix {
- top: 0;
left: 0;
- z-index: 10;
transition: right .15s;
@media (max-width: $screen-xs-max) {
@@ -710,6 +749,16 @@
padding-right: $gl-padding;
}
}
+
+ .nav-links {
+ border: 0;
+ }
+}
+
+.merge-request-tabs {
+ display: flex;
+ margin-bottom: 0;
+ padding: 0;
}
.limit-container-width {
@@ -720,6 +769,15 @@
}
}
+.merge-request-tabs-container {
+ display: flex;
+ justify-content: space-between;
+
+ @media (max-width: $screen-xs-max) {
+ flex-direction: column-reverse;
+ }
+}
+
.limit-container-width:not(.container-limited) {
.merge-request-tabs-holder:not(.affix) {
.merge-request-tabs-container {
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index 62f654ed343..9db26f99a75 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -277,6 +277,7 @@
.toolbar-text {
font-size: 14px;
line-height: 16px;
+ margin-top: 2px;
@media (min-width: $screen-md-min) {
float: left;
@@ -402,3 +403,45 @@
}
}
}
+
+.uploading-container {
+ float: right;
+
+ @media (max-width: $screen-xs-max) {
+ float: left;
+ margin-top: 5px;
+ }
+}
+
+.uploading-error-icon,
+.uploading-error-message {
+ color: $gl-text-red;
+}
+
+.uploading-error-message {
+ @media (max-width: $screen-xs-max) {
+ &::after {
+ content: "\a";
+ white-space: pre;
+ }
+ }
+}
+
+.uploading-progress {
+ margin-right: 5px;
+}
+
+.attach-new-file,
+.button-attach-file,
+.retry-uploading-link {
+ color: $gl-link-color;
+ padding: 0;
+ background: none;
+ border: 0;
+ font-size: 14px;
+ line-height: 16px;
+}
+
+.markdown-selector {
+ color: $gl-link-color;
+}
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 6d7b7031c30..4b15fc2bd82 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -238,11 +238,6 @@ ul.notes {
ul {
margin: 3px 0 3px 16px !important;
-
- .gfm-commit {
- font-family: $monospace_font;
- font-size: 12px;
- }
}
p:first-child {
@@ -614,6 +609,15 @@ ul.notes {
}
.line-resolve-all-container {
+ @media (min-width: $screen-sm-min) {
+ margin-right: 0;
+ padding-left: $gl-padding;
+ }
+
+ > div {
+ white-space: nowrap;
+ }
+
.btn-group {
margin-left: -4px;
}
@@ -665,7 +669,7 @@ ul.notes {
.line-resolve-btn {
position: relative;
- top: 2px;
+ top: 0;
padding: 0;
background-color: transparent;
border: none;
@@ -686,8 +690,8 @@ ul.notes {
svg {
fill: $gray-darkest;
- height: 15px;
- width: 15px;
+ height: 16px;
+ width: 16px;
}
.loading {
diff --git a/app/assets/stylesheets/pages/pipeline_schedules.scss b/app/assets/stylesheets/pages/pipeline_schedules.scss
index 0fee54a0d19..ab417948931 100644
--- a/app/assets/stylesheets/pages/pipeline_schedules.scss
+++ b/app/assets/stylesheets/pages/pipeline_schedules.scss
@@ -31,14 +31,6 @@
margin-right: 10px;
font-size: 12px;
}
-
- .cron-unset-status {
- padding-top: 16px;
- margin-left: -16px;
- color: $gl-text-color-secondary;
- font-size: 12px;
- font-weight: 600;
- }
}
.pipeline-schedule-table-row {
@@ -69,3 +61,16 @@
color: $gl-text-color;
}
}
+
+.cron-preset-radio-input {
+ display: inline-block;
+
+ @media (max-width: $screen-md-max) {
+ display: block;
+ margin: 0 0 5px 5px;
+ }
+
+ input {
+ margin-right: 3px;
+ }
+}
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index e7553c7a4bf..292584eba28 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -8,10 +8,6 @@
white-space: nowrap;
}
- .empty-state {
- margin: 5% auto 0;
- }
-
.table-holder {
width: 100%;
@@ -162,9 +158,13 @@
float: none;
}
+ .api {
+ @extend .monospace;
+ }
+
.branch-commit {
- .branch-name {
+ .ref-name {
font-weight: bold;
max-width: 120px;
overflow: hidden;
@@ -186,12 +186,11 @@
color: $gl-text-color;
}
- .commit-id {
+ .commit-sha {
color: $gl-link-color;
}
.commit-title {
- margin-top: 4px;
max-width: 225px;
overflow: hidden;
white-space: nowrap;
@@ -224,7 +223,7 @@
.duration,
.finished-at {
color: $gl-text-color-secondary;
- margin: 4px 0;
+ margin: 0;
white-space: nowrap;
.fa {
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index c119f0c9b22..f0bf3d4c267 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -639,59 +639,6 @@ pre.light-well {
}
}
-.project-last-commit {
- background-color: $gray-light;
- border: 1px solid $border-color;
- border-radius: $border-radius-base;
- padding: 12px;
-
- @media (min-width: $screen-sm-min) {
- margin-top: $gl-padding;
- }
-
- .ci-status {
- margin-right: $gl-padding;
- }
-
- .commit-row-message {
- color: $gl-text-color;
- }
-
- .commit_short_id {
- margin-right: 5px;
- color: $gl-link-color;
- font-weight: 600;
- }
-
- .commit-author-link {
- .commit-author-name {
- font-weight: 600;
- }
- }
-}
-
-.project-show-readme {
- .row-content-block {
- background-color: inherit;
- border: none;
- }
-
- .readme-holder {
- padding: $gl-padding 0;
- border-top: 0;
-
- .edit-project-readme {
- z-index: 2;
- position: relative;
- }
-
- .wiki h1 {
- border-bottom: none;
- padding: 0;
- }
- }
-}
-
.git-clone-holder {
width: 380px;
@@ -825,7 +772,8 @@ pre.light-well {
}
.compare-form-group {
- .dropdown-menu {
+ .dropdown-menu,
+ .inline-input-group {
width: 100%;
@media (min-width: $screen-sm-min) {
@@ -844,14 +792,6 @@ pre.light-well {
width: auto;
}
}
-
- .inline-input-group {
- width: 100%;
-
- @media (min-width: $screen-sm-min) {
- width: 250px;
- }
- }
}
.clearable-input {
diff --git a/app/assets/stylesheets/print.scss b/app/assets/stylesheets/print.scss
index 6cc1cc8e263..136d0c79467 100644
--- a/app/assets/stylesheets/print.scss
+++ b/app/assets/stylesheets/print.scss
@@ -28,9 +28,6 @@ nav.navbar-collapse.collapse,
.profiler-results,
.tree-ref-holder,
.tree-holder .breadcrumb,
-.blob-commit-info,
-.file-title,
-.file-holder,
.nav,
.btn,
ul.notes-form,
@@ -43,6 +40,11 @@ ul.notes-form,
display: none!important;
}
+pre {
+ page-break-before: avoid;
+ page-break-inside: auto;
+}
+
.page-gutter {
padding-top: 0;
padding-left: 0;
diff --git a/app/assets/stylesheets/test.scss b/app/assets/stylesheets/test.scss
new file mode 100644
index 00000000000..7d9f3da79c5
--- /dev/null
+++ b/app/assets/stylesheets/test.scss
@@ -0,0 +1,17 @@
+* {
+ -o-transition: none !important;
+ -moz-transition: none !important;
+ -ms-transition: none !important;
+ -webkit-transition: none !important;
+ transition: none !important;
+ -o-transform: none !important;
+ -moz-transform: none !important;
+ -ms-transform: none !important;
+ -webkit-transform: none !important;
+ transform: none !important;
+ -webkit-animation: none !important;
+ -moz-animation: none !important;
+ -o-animation: none !important;
+ -ms-animation: none !important;
+ animation: none !important;
+}
diff --git a/app/controllers/admin/hooks_controller.rb b/app/controllers/admin/hooks_controller.rb
index a119934febc..ccfe553c89e 100644
--- a/app/controllers/admin/hooks_controller.rb
+++ b/app/controllers/admin/hooks_controller.rb
@@ -60,6 +60,7 @@ class Admin::HooksController < Admin::ApplicationController
:enable_ssl_verification,
:push_events,
:tag_push_events,
+ :repository_update_events,
:token,
:url
)
diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb
index b79ca034c5b..e2f5aa8508e 100644
--- a/app/controllers/autocomplete_controller.rb
+++ b/app/controllers/autocomplete_controller.rb
@@ -41,7 +41,7 @@ class AutocompleteController < ApplicationController
no_project = {
id: 0,
- name_with_namespace: 'No project',
+ name_with_namespace: 'No project'
}
projects.unshift(no_project) unless params[:offset_id].present?
diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb
index b199f18da1e..4cf645d6341 100644
--- a/app/controllers/concerns/issuable_actions.rb
+++ b/app/controllers/concerns/issuable_actions.rb
@@ -60,17 +60,24 @@ module IssuableActions
end
def bulk_update_params
- params.require(:update).permit(
+ permitted_keys = [
:issuable_ids,
:assignee_id,
:milestone_id,
:state_event,
:subscription_event,
- assignee_ids: [],
label_ids: [],
add_label_ids: [],
remove_label_ids: []
- )
+ ]
+
+ if resource_name == 'issue'
+ permitted_keys << { assignee_ids: [] }
+ else
+ permitted_keys.unshift(:assignee_id)
+ end
+
+ params.require(:update).permit(permitted_keys)
end
def resource_name
diff --git a/app/controllers/concerns/lfs_request.rb b/app/controllers/concerns/lfs_request.rb
index ed22b1e5470..ae91e02488a 100644
--- a/app/controllers/concerns/lfs_request.rb
+++ b/app/controllers/concerns/lfs_request.rb
@@ -23,7 +23,7 @@ module LfsRequest
render(
json: {
message: 'Git LFS is not enabled on this GitLab server, contact your admin.',
- documentation_url: help_url,
+ documentation_url: help_url
},
status: 501
)
@@ -48,7 +48,7 @@ module LfsRequest
render(
json: {
message: 'Access forbidden. Check your access level.',
- documentation_url: help_url,
+ documentation_url: help_url
},
content_type: "application/vnd.git-lfs+json",
status: 403
@@ -59,7 +59,7 @@ module LfsRequest
render(
json: {
message: 'Not found.',
- documentation_url: help_url,
+ documentation_url: help_url
},
content_type: "application/vnd.git-lfs+json",
status: 404
diff --git a/app/controllers/concerns/renders_blob.rb b/app/controllers/concerns/renders_blob.rb
index 9faf68e6d97..1d37e4cb3bd 100644
--- a/app/controllers/concerns/renders_blob.rb
+++ b/app/controllers/concerns/renders_blob.rb
@@ -3,15 +3,18 @@ module RendersBlob
def render_blob_json(blob)
viewer =
- if params[:viewer] == 'rich'
+ case params[:viewer]
+ when 'rich'
blob.rich_viewer
+ when 'auxiliary'
+ blob.auxiliary_viewer
else
blob.simple_viewer
end
return render_404 unless viewer
render json: {
- html: view_to_html_string("projects/blob/_viewer", viewer: viewer, load_asynchronously: false)
+ html: view_to_html_string("projects/blob/_viewer", viewer: viewer, load_async: false)
}
end
diff --git a/app/controllers/concerns/routable_actions.rb b/app/controllers/concerns/routable_actions.rb
index d4ab6782444..4199da9cdf5 100644
--- a/app/controllers/concerns/routable_actions.rb
+++ b/app/controllers/concerns/routable_actions.rb
@@ -4,7 +4,7 @@ module RoutableActions
def find_routable!(routable_klass, requested_full_path, extra_authorization_proc: nil)
routable = routable_klass.find_by_full_path(requested_full_path, follow_redirects: request.get?)
- if routable_authorized?(routable_klass, routable, extra_authorization_proc)
+ if routable_authorized?(routable, extra_authorization_proc)
ensure_canonical_path(routable, requested_full_path)
routable
else
@@ -13,8 +13,8 @@ module RoutableActions
end
end
- def routable_authorized?(routable_klass, routable, extra_authorization_proc)
- action = :"read_#{routable_klass.to_s.underscore}"
+ def routable_authorized?(routable, extra_authorization_proc)
+ action = :"read_#{routable.class.to_s.underscore}"
return false unless can?(current_user, action, routable)
if extra_authorization_proc
@@ -24,15 +24,15 @@ module RoutableActions
end
end
- def ensure_canonical_path(routable, requested_path)
+ def ensure_canonical_path(routable, requested_full_path)
return unless request.get?
canonical_path = routable.full_path
- if canonical_path != requested_path
- if canonical_path.casecmp(requested_path) != 0
- flash[:notice] = "Project '#{requested_path}' was moved to '#{canonical_path}'. Please update any links and bookmarks that may still have the old path."
+ if canonical_path != requested_full_path
+ if canonical_path.casecmp(requested_full_path) != 0
+ flash[:notice] = "#{routable.class.to_s.titleize} '#{requested_full_path}' was moved to '#{canonical_path}'. Please update any links and bookmarks that may still have the old path."
end
- redirect_to request.original_url.sub(requested_path, canonical_path)
+ redirect_to build_canonical_path(routable)
end
end
end
diff --git a/app/controllers/dashboard/snippets_controller.rb b/app/controllers/dashboard/snippets_controller.rb
index bcfdbe14be9..8dd91264451 100644
--- a/app/controllers/dashboard/snippets_controller.rb
+++ b/app/controllers/dashboard/snippets_controller.rb
@@ -1,11 +1,10 @@
class Dashboard::SnippetsController < Dashboard::ApplicationController
def index
- @snippets = SnippetsFinder.new.execute(
+ @snippets = SnippetsFinder.new(
current_user,
- filter: :by_user,
- user: current_user,
+ author: current_user,
scope: params[:scope]
- )
+ ).execute
@snippets = @snippets.page(params[:page])
end
end
diff --git a/app/controllers/explore/groups_controller.rb b/app/controllers/explore/groups_controller.rb
index 68228c095da..81883c543ba 100644
--- a/app/controllers/explore/groups_controller.rb
+++ b/app/controllers/explore/groups_controller.rb
@@ -1,6 +1,6 @@
class Explore::GroupsController < Explore::ApplicationController
def index
- @groups = GroupsFinder.new.execute(current_user)
+ @groups = GroupsFinder.new(current_user).execute
@groups = @groups.search(params[:filter_groups]) if params[:filter_groups].present?
@groups = @groups.sort(@sort = params[:sort])
@groups = @groups.page(params[:page])
diff --git a/app/controllers/explore/snippets_controller.rb b/app/controllers/explore/snippets_controller.rb
index 28760c3f84b..d3f0e033068 100644
--- a/app/controllers/explore/snippets_controller.rb
+++ b/app/controllers/explore/snippets_controller.rb
@@ -1,6 +1,6 @@
class Explore::SnippetsController < Explore::ApplicationController
def index
- @snippets = SnippetsFinder.new.execute(current_user, filter: :all)
+ @snippets = SnippetsFinder.new(current_user).execute
@snippets = @snippets.page(params[:page])
end
end
diff --git a/app/controllers/groups/application_controller.rb b/app/controllers/groups/application_controller.rb
index afffb813b44..c0ac47e363d 100644
--- a/app/controllers/groups/application_controller.rb
+++ b/app/controllers/groups/application_controller.rb
@@ -31,4 +31,10 @@ class Groups::ApplicationController < ApplicationController
return render_403
end
end
+
+ def build_canonical_path(group)
+ params[:group_id] = group.to_param
+
+ url_for(params)
+ end
end
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index 46c3ff10694..965ced4d372 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -64,7 +64,7 @@ class GroupsController < Groups::ApplicationController
end
def subgroups
- @nested_groups = group.children
+ @nested_groups = GroupsFinder.new(current_user, parent: group).execute
@nested_groups = @nested_groups.search(params[:filter_groups]) if params[:filter_groups].present?
end
@@ -169,4 +169,12 @@ class GroupsController < Groups::ApplicationController
@notification_setting = current_user.notification_settings_for(group)
end
end
+
+ def build_canonical_path(group)
+ return group_path(group) if action_name == 'show' # root group path
+
+ params[:id] = group.to_param
+
+ url_for(params)
+ end
end
diff --git a/app/controllers/health_controller.rb b/app/controllers/health_controller.rb
index df0fc3132ed..125746d0426 100644
--- a/app/controllers/health_controller.rb
+++ b/app/controllers/health_controller.rb
@@ -5,7 +5,7 @@ class HealthController < ActionController::Base
CHECKS = [
Gitlab::HealthChecks::DbCheck,
Gitlab::HealthChecks::RedisCheck,
- Gitlab::HealthChecks::FsShardsCheck,
+ Gitlab::HealthChecks::FsShardsCheck
].freeze
def readiness
diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb
index 3109439b2ff..1c01be06451 100644
--- a/app/controllers/jwt_controller.rb
+++ b/app/controllers/jwt_controller.rb
@@ -4,7 +4,7 @@ class JwtController < ApplicationController
before_action :authenticate_project_or_user
SERVICES = {
- Auth::ContainerRegistryAuthenticationService::AUDIENCE => Auth::ContainerRegistryAuthenticationService,
+ Auth::ContainerRegistryAuthenticationService::AUDIENCE => Auth::ContainerRegistryAuthenticationService
}.freeze
def auth
diff --git a/app/controllers/profiles/preferences_controller.rb b/app/controllers/profiles/preferences_controller.rb
index 0d891ef4004..5414142e2df 100644
--- a/app/controllers/profiles/preferences_controller.rb
+++ b/app/controllers/profiles/preferences_controller.rb
@@ -33,7 +33,7 @@ class Profiles::PreferencesController < Profiles::ApplicationController
:color_scheme_id,
:layout,
:dashboard,
- :project_view,
+ :project_view
)
end
end
diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb
index 12e4a6999ae..cb4bd0ad5f5 100644
--- a/app/controllers/projects/application_controller.rb
+++ b/app/controllers/projects/application_controller.rb
@@ -29,6 +29,13 @@ class Projects::ApplicationController < ApplicationController
@project = find_routable!(Project, path, extra_authorization_proc: auth_proc)
end
+ def build_canonical_path(project)
+ params[:namespace_id] = project.namespace.to_param
+ params[:project_id] = project.to_param
+
+ url_for(params)
+ end
+
def repository
@repository ||= project.repository
end
diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb
index 9489bbddfc4..87721fbe2f5 100644
--- a/app/controllers/projects/blob_controller.rb
+++ b/app/controllers/projects/blob_controller.rb
@@ -42,6 +42,8 @@ class Projects::BlobController < Projects::ApplicationController
environment_params = @repository.branch_exists?(@ref) ? { ref: @ref } : { commit: @commit }
@environment = EnvironmentsFinder.new(@project, current_user, environment_params).execute.last
+ @last_commit = @repository.last_commit_for_path(@commit.id, @blob.path)
+
render 'show'
end
diff --git a/app/controllers/projects/deployments_controller.rb b/app/controllers/projects/deployments_controller.rb
index b33c0b00ad9..6644deb49c9 100644
--- a/app/controllers/projects/deployments_controller.rb
+++ b/app/controllers/projects/deployments_controller.rb
@@ -6,18 +6,20 @@ class Projects::DeploymentsController < Projects::ApplicationController
deployments = environment.deployments.reorder(created_at: :desc)
deployments = deployments.where('created_at > ?', params[:after].to_time) if params[:after]&.to_time
- render json: { deployments: DeploymentSerializer.new(user: @current_user, project: project)
+ render json: { deployments: DeploymentSerializer.new(project: project)
.represent_concise(deployments) }
end
def metrics
- @metrics = deployment.metrics(1.hour)
-
+ return render_404 unless deployment.has_metrics?
+ @metrics = deployment.metrics
if @metrics&.any?
render json: @metrics, status: :ok
else
head :no_content
end
+ rescue NotImplementedError
+ render_404
end
private
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index 58d41e0478d..46438e68d54 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -11,10 +11,10 @@ class Projects::IssuesController < Projects::ApplicationController
before_action :redirect_to_external_issue_tracker, only: [:index, :new]
before_action :module_enabled
before_action :issue, only: [:edit, :update, :show, :referenced_merge_requests,
- :related_branches, :can_create_branch, :rendered_title, :create_merge_request]
+ :related_branches, :can_create_branch, :realtime_changes, :create_merge_request]
# Allow read any issue
- before_action :authorize_read_issue!, only: [:show, :rendered_title]
+ before_action :authorize_read_issue!, only: [:show, :realtime_changes]
# Allow write(create) issue
before_action :authorize_create_issue!, only: [:new, :create]
@@ -199,7 +199,7 @@ class Projects::IssuesController < Projects::ApplicationController
end
end
- def rendered_title
+ def realtime_changes
Gitlab::PollingInterval.set_header(response, interval: 3_000)
render json: {
@@ -208,8 +208,7 @@ class Projects::IssuesController < Projects::ApplicationController
description: view_context.markdown_field(@issue, :description),
description_text: @issue.description,
task_status: @issue.task_status,
- issue_number: @issue.iid,
- updated_at: @issue.updated_at,
+ updated_at: @issue.updated_at
}
end
@@ -269,7 +268,7 @@ class Projects::IssuesController < Projects::ApplicationController
def issue_params
params.require(:issue).permit(
:title, :assignee_id, :position, :description, :confidential,
- :milestone_id, :due_date, :state_event, :task_num, :lock_version, label_ids: [], assignee_ids: [],
+ :milestone_id, :due_date, :state_event, :task_num, :lock_version, label_ids: [], assignee_ids: []
)
end
diff --git a/app/controllers/projects/lfs_api_controller.rb b/app/controllers/projects/lfs_api_controller.rb
index 8a5a645ed0e..1b0d3aab3fa 100644
--- a/app/controllers/projects/lfs_api_controller.rb
+++ b/app/controllers/projects/lfs_api_controller.rb
@@ -22,7 +22,7 @@ class Projects::LfsApiController < Projects::GitHttpClientController
render(
json: {
message: 'Server supports batch API only, please update your Git LFS client to version 1.0.1 and up.',
- documentation_url: "#{Gitlab.config.gitlab.url}/help",
+ documentation_url: "#{Gitlab.config.gitlab.url}/help"
},
status: 501
)
@@ -55,7 +55,7 @@ class Projects::LfsApiController < Projects::GitHttpClientController
else
object[:error] = {
code: 404,
- message: "Object does not exist on the server or you don't have permissions to access it",
+ message: "Object does not exist on the server or you don't have permissions to access it"
}
end
end
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 207fbad7856..b99ccd453b8 100755..100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -155,8 +155,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController
format.html { define_discussion_vars }
format.json do
- if @merge_request.conflicts_can_be_resolved_in_ui?
- render json: @merge_request.conflicts
+ if @conflicts_list.can_be_resolved_in_ui?
+ render json: @conflicts_list
elsif @merge_request.can_be_merged?
render json: {
message: 'The merge conflicts for this merge request have already been resolved. Please return to the merge request.',
@@ -173,9 +173,9 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
def conflict_for_path
- return render_404 unless @merge_request.conflicts_can_be_resolved_in_ui?
+ return render_404 unless @conflicts_list.can_be_resolved_in_ui?
- file = @merge_request.conflicts.file_for_path(params[:old_path], params[:new_path])
+ file = @conflicts_list.file_for_path(params[:old_path], params[:new_path])
return render_404 unless file
@@ -183,7 +183,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
def resolve_conflicts
- return render_404 unless @merge_request.conflicts_can_be_resolved_in_ui?
+ return render_404 unless @conflicts_list.can_be_resolved_in_ui?
if @merge_request.can_be_merged?
render status: :bad_request, json: { message: 'The merge conflicts for this merge request have already been resolved.' }
@@ -191,7 +191,9 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
begin
- MergeRequests::ResolveService.new(@merge_request.source_project, current_user, params).execute(@merge_request)
+ MergeRequests::Conflicts::ResolveService.
+ new(merge_request).
+ execute(current_user, params)
flash[:notice] = 'All merge conflicts were resolved. The merge request can now be merged.'
@@ -459,7 +461,9 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
def authorize_can_resolve_conflicts!
- return render_404 unless @merge_request.conflicts_can_be_resolved_by?(current_user)
+ @conflicts_list = MergeRequests::Conflicts::ListService.new(@merge_request)
+
+ return render_404 unless @conflicts_list.can_be_resolved_by?(current_user)
end
def module_enabled
diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb
index 7fe3c3c116c..602d3dd8c1c 100644
--- a/app/controllers/projects/pipelines_controller.rb
+++ b/app/controllers/projects/pipelines_controller.rb
@@ -44,7 +44,7 @@ class Projects::PipelinesController < Projects::ApplicationController
all: @pipelines_count,
running: @running_count,
pending: @pending_count,
- finished: @finished_count,
+ finished: @finished_count
}
}
end
diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb
index 66f913f8f9d..3b2b0d9e502 100644
--- a/app/controllers/projects/snippets_controller.rb
+++ b/app/controllers/projects/snippets_controller.rb
@@ -23,12 +23,11 @@ class Projects::SnippetsController < Projects::ApplicationController
respond_to :html
def index
- @snippets = SnippetsFinder.new.execute(
+ @snippets = SnippetsFinder.new(
current_user,
- filter: :by_project,
project: @project,
scope: params[:scope]
- )
+ ).execute
@snippets = @snippets.page(params[:page])
if @snippets.out_of_range? && @snippets.total_pages != 0
redirect_to namespace_project_snippets_path(page: @snippets.total_pages)
diff --git a/app/controllers/projects/tags_controller.rb b/app/controllers/projects/tags_controller.rb
index 750c3ec486a..afbea3e2b40 100644
--- a/app/controllers/projects/tags_controller.rb
+++ b/app/controllers/projects/tags_controller.rb
@@ -38,6 +38,8 @@ class Projects::TagsController < Projects::ApplicationController
redirect_to namespace_project_tag_path(@project.namespace, @project, @tag.name)
else
@error = result[:message]
+ @message = params[:message]
+ @release_description = params[:release_description]
render action: 'new'
end
end
diff --git a/app/controllers/projects/tree_controller.rb b/app/controllers/projects/tree_controller.rb
index 5e2182c883e..f8eb8e00a5d 100644
--- a/app/controllers/projects/tree_controller.rb
+++ b/app/controllers/projects/tree_controller.rb
@@ -24,6 +24,8 @@ class Projects::TreeController < Projects::ApplicationController
end
end
+ @last_commit = @repository.last_commit_for_path(@commit.id, @tree.path) || @commit
+
respond_to do |format|
format.html
# Disable cache so browser history works
@@ -48,7 +50,7 @@ class Projects::TreeController < Projects::ApplicationController
@dir_name = File.join(@path, params[:dir_name])
@commit_params = {
file_path: @dir_name,
- commit_message: params[:commit_message],
+ commit_message: params[:commit_message]
}
end
end
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 69310b26e76..544715d62ea 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -220,7 +220,7 @@ class ProjectsController < Projects::ApplicationController
branches = BranchesFinder.new(@repository, params).execute.map(&:name)
options = {
- 'Branches' => branches.take(100),
+ 'Branches' => branches.take(100)
}
unless @repository.tag_count.zero?
@@ -365,4 +365,11 @@ class ProjectsController < Projects::ApplicationController
def project_view_files_allowed?
!project.empty_repo? && can?(current_user, :download_code, project)
end
+
+ def build_canonical_path(project)
+ params[:namespace_id] = project.namespace.to_param
+ params[:id] = project.to_param
+
+ url_for(params)
+ end
end
diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb
index 19e07e3ab86..7445f61195d 100644
--- a/app/controllers/snippets_controller.rb
+++ b/app/controllers/snippets_controller.rb
@@ -27,12 +27,8 @@ class SnippetsController < ApplicationController
return render_404 unless @user
- @snippets = SnippetsFinder.new.execute(current_user, {
- filter: :by_user,
- user: @user,
- scope: params[:scope]
- })
- .page(params[:page])
+ @snippets = SnippetsFinder.new(current_user, author: @user, scope: params[:scope])
+ .execute.page(params[:page])
render 'index'
else
@@ -103,20 +99,20 @@ class SnippetsController < ApplicationController
protected
def snippet
- @snippet ||= if current_user
- PersonalSnippet.where("author_id = ? OR visibility_level IN (?)",
- current_user.id,
- [Snippet::PUBLIC, Snippet::INTERNAL]).
- find(params[:id])
- else
- PersonalSnippet.find(params[:id])
- end
+ @snippet ||= PersonalSnippet.find_by(id: params[:id])
end
+
alias_method :awardable, :snippet
alias_method :spammable, :snippet
def authorize_read_snippet!
- authenticate_user! unless can?(current_user, :read_personal_snippet, @snippet)
+ return if can?(current_user, :read_personal_snippet, @snippet)
+
+ if current_user
+ render_404
+ else
+ authenticate_user!
+ end
end
def authorize_update_snippet!
diff --git a/app/controllers/uploads_controller.rb b/app/controllers/uploads_controller.rb
index 21a964fb391..eef53730291 100644
--- a/app/controllers/uploads_controller.rb
+++ b/app/controllers/uploads_controller.rb
@@ -21,6 +21,8 @@ class UploadsController < ApplicationController
can?(current_user, :read_project, model.project)
when User
true
+ when Appearance
+ true
else
permission = "read_#{model.class.to_s.underscore}".to_sym
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index ca89ed221c6..19fc1e5de49 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -128,15 +128,18 @@ class UsersController < ApplicationController
end
def load_snippets
- @snippets = SnippetsFinder.new.execute(
+ @snippets = SnippetsFinder.new(
current_user,
- filter: :by_user,
- user: user,
+ author: user,
scope: params[:scope]
- ).page(params[:page])
+ ).execute.page(params[:page])
end
def projects_for_current_user
ProjectsFinder.new(current_user: current_user).execute
end
+
+ def build_canonical_path(user)
+ url_for(params.merge(username: user.to_param))
+ end
end
diff --git a/app/finders/groups_finder.rb b/app/finders/groups_finder.rb
index d932a17883f..f68610e197c 100644
--- a/app/finders/groups_finder.rb
+++ b/app/finders/groups_finder.rb
@@ -1,13 +1,19 @@
class GroupsFinder < UnionFinder
- def execute(current_user = nil)
- segments = all_groups(current_user)
+ def initialize(current_user = nil, params = {})
+ @current_user = current_user
+ @params = params
+ end
- find_union(segments, Group).with_route.order_id_desc
+ def execute
+ groups = find_union(all_groups, Group).with_route.order_id_desc
+ by_parent(groups)
end
private
- def all_groups(current_user)
+ attr_reader :current_user, :params
+
+ def all_groups
groups = []
groups << current_user.authorized_groups if current_user
@@ -15,4 +21,10 @@ class GroupsFinder < UnionFinder
groups
end
+
+ def by_parent(groups)
+ return groups unless params[:parent]
+
+ groups.where(parent: params[:parent])
+ end
end
diff --git a/app/finders/notes_finder.rb b/app/finders/notes_finder.rb
index dc6a8ad1f66..02eb983bf55 100644
--- a/app/finders/notes_finder.rb
+++ b/app/finders/notes_finder.rb
@@ -67,7 +67,7 @@ class NotesFinder
when "merge_request"
MergeRequestsFinder.new(@current_user, project_id: @project.id).execute
when "snippet", "project_snippet"
- SnippetsFinder.new.execute(@current_user, filter: :by_project, project: @project)
+ SnippetsFinder.new(@current_user, project: @project).execute
when "personal_snippet"
PersonalSnippet.all
else
diff --git a/app/finders/snippets_finder.rb b/app/finders/snippets_finder.rb
index da6e6e87a6f..c04f61de79c 100644
--- a/app/finders/snippets_finder.rb
+++ b/app/finders/snippets_finder.rb
@@ -1,66 +1,74 @@
-class SnippetsFinder
- def execute(current_user, params = {})
- filter = params[:filter]
- user = params.fetch(:user, current_user)
-
- case filter
- when :all then
- snippets(current_user).fresh
- when :public then
- Snippet.are_public.fresh
- when :by_user then
- by_user(current_user, user, params[:scope])
- when :by_project
- by_project(current_user, params[:project], params[:scope])
- end
+class SnippetsFinder < UnionFinder
+ attr_accessor :current_user, :params
+
+ def initialize(current_user, params = {})
+ @current_user = current_user
+ @params = params
+ end
+
+ def execute
+ items = init_collection
+ items = by_project(items)
+ items = by_author(items)
+ items = by_visibility(items)
+
+ items.fresh
end
private
- def snippets(current_user)
- if current_user
- Snippet.public_and_internal
- else
- # Not authenticated
- #
- # Return only:
- # public snippets
- Snippet.are_public
- end
+ def init_collection
+ items = Snippet.all
+
+ accessible(items)
end
- def by_user(current_user, user, scope)
- snippets = user.snippets.fresh
+ def accessible(items)
+ segments = []
+ segments << items.public_to_user(current_user)
+ segments << authorized_to_user(items) if current_user
- if current_user
- include_private = user == current_user
- by_scope(snippets, scope, include_private)
- else
- snippets.are_public
- end
+ find_union(segments, Snippet)
end
- def by_project(current_user, project, scope)
- snippets = project.snippets.fresh
+ def authorized_to_user(items)
+ items.where(
+ 'author_id = :author_id
+ OR project_id IN (:project_ids)',
+ author_id: current_user.id,
+ project_ids: current_user.authorized_projects.select(:id))
+ end
- if current_user
- include_private = project.team.member?(current_user) || current_user.admin?
- by_scope(snippets, scope, include_private)
- else
- snippets.are_public
- end
+ def by_visibility(items)
+ visibility = params[:visibility] || visibility_from_scope
+
+ return items unless visibility
+
+ items.where(visibility_level: visibility)
+ end
+
+ def by_author(items)
+ return items unless params[:author]
+
+ items.where(author_id: params[:author].id)
+ end
+
+ def by_project(items)
+ return items unless params[:project]
+
+ items.where(project_id: params[:project].id)
end
- def by_scope(snippets, scope = nil, include_private = false)
- case scope.to_s
+ def visibility_from_scope
+ case params[:scope].to_s
when 'are_private'
- include_private ? snippets.are_private : Snippet.none
+ Snippet::PRIVATE
when 'are_internal'
- snippets.are_internal
+ Snippet::INTERNAL
when 'are_public'
- snippets.are_public
+ Snippet::PUBLIC
else
- include_private ? snippets : snippets.public_and_internal
+ nil
end
end
end
diff --git a/app/finders/users_finder.rb b/app/finders/users_finder.rb
new file mode 100644
index 00000000000..dbd50d1db7c
--- /dev/null
+++ b/app/finders/users_finder.rb
@@ -0,0 +1,74 @@
+# UsersFinder
+#
+# Used to filter users by set of params
+#
+# Arguments:
+# current_user - which user use
+# params:
+# username: string
+# extern_uid: string
+# provider: string
+# search: string
+# active: boolean
+# blocked: boolean
+# external: boolean
+#
+class UsersFinder
+ attr_accessor :current_user, :params
+
+ def initialize(current_user, params = {})
+ @current_user = current_user
+ @params = params
+ end
+
+ def execute
+ users = User.all
+ users = by_username(users)
+ users = by_search(users)
+ users = by_blocked(users)
+ users = by_active(users)
+ users = by_external_identity(users)
+ users = by_external(users)
+
+ users
+ end
+
+ private
+
+ def by_username(users)
+ return users unless params[:username]
+
+ users.where(username: params[:username])
+ end
+
+ def by_search(users)
+ return users unless params[:search].present?
+
+ users.search(params[:search])
+ end
+
+ def by_blocked(users)
+ return users unless params[:blocked]
+
+ users.blocked
+ end
+
+ def by_active(users)
+ return users unless params[:active]
+
+ users.active
+ end
+
+ def by_external_identity(users)
+ return users unless current_user.admin? && params[:extern_uid] && params[:provider]
+
+ users.joins(:identities).merge(Identity.with_extern_uid(params[:provider], params[:extern_uid]))
+ end
+
+ def by_external(users)
+ return users = users.where.not(external: true) unless current_user.admin?
+ return users unless params[:external]
+
+ users.external
+ end
+end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 97cf4863ddc..e5e64650708 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -278,4 +278,22 @@ module ApplicationHelper
def show_user_callout?
cookies[:user_callout_dismissed] == 'true'
end
+
+ def linkedin_url(user)
+ name = user.linkedin
+ if name =~ %r{\Ahttps?:\/\/(www\.)?linkedin\.com\/in\/}
+ name
+ else
+ "https://www.linkedin.com/in/#{name}"
+ end
+ end
+
+ def twitter_url(user)
+ name = user.twitter
+ if name =~ %r{\Ahttps?:\/\/(www\.)?twitter\.com\/}
+ name
+ else
+ "https://www.twitter.com/#{name}"
+ end
+ end
end
diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb
index eb37f2e0267..622e14e21ff 100644
--- a/app/helpers/blob_helper.rb
+++ b/app/helpers/blob_helper.rb
@@ -226,7 +226,7 @@ module BlobHelper
def open_raw_blob_button(blob)
return if blob.empty?
-
+
if blob.raw_binary? || blob.stored_externally?
icon = icon('download')
title = 'Download'
@@ -242,9 +242,9 @@ module BlobHelper
case viewer.render_error
when :too_large
max_size =
- if viewer.absolutely_too_large?
- viewer.absolute_max_size
- elsif viewer.too_large?
+ if viewer.can_override_max_size?
+ viewer.overridable_max_size
+ else
viewer.max_size
end
"it is larger than #{number_to_human_size(max_size)}"
@@ -278,4 +278,19 @@ module BlobHelper
options
end
+
+ def contribution_options(project)
+ options = []
+
+ if can?(current_user, :create_issue, project)
+ options << link_to("submit an issue", new_namespace_project_issue_path(project.namespace, project))
+ end
+
+ merge_project = can?(current_user, :create_merge_request, project) ? project : (current_user && current_user.fork_of(project))
+ if merge_project
+ options << link_to("create a merge request", new_namespace_project_merge_request_path(project.namespace, project))
+ end
+
+ options
+ end
end
diff --git a/app/helpers/button_helper.rb b/app/helpers/button_helper.rb
index c85e96cf78d..206d0753f08 100644
--- a/app/helpers/button_helper.rb
+++ b/app/helpers/button_helper.rb
@@ -42,7 +42,10 @@ module ButtonHelper
class: "btn #{css_class}",
data: data,
type: :button,
- title: title
+ title: title,
+ aria: {
+ label: title
+ }
end
def http_clone_button(project, placement = 'right', append_link: true)
diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb
index 97b497a0fed..d59d51905a6 100644
--- a/app/helpers/commits_helper.rb
+++ b/app/helpers/commits_helper.rb
@@ -74,12 +74,8 @@ module CommitsHelper
# Returns the sorted alphabetically links to branches, separated by a comma
def commit_branches_links(project, branches)
branches.sort.map do |branch|
- link_to(
- namespace_project_tree_path(project.namespace, project, branch)
- ) do
- content_tag :span, class: 'label label-gray' do
- icon('code-fork') + ' ' + branch
- end
+ link_to(project_ref_path(project, branch), class: "label label-gray ref-name") do
+ icon('code-fork') + " #{branch}"
end
end.join(" ").html_safe
end
@@ -88,19 +84,14 @@ module CommitsHelper
def commit_tags_links(project, tags)
sorted = VersionSorter.rsort(tags)
sorted.map do |tag|
- link_to(
- namespace_project_commits_path(project.namespace, project,
- project.repository.find_tag(tag).name)
- ) do
- content_tag :span, class: 'label label-gray' do
- icon('tag') + ' ' + tag
- end
+ link_to(project_ref_path(project, tag), class: "label label-gray ref-name") do
+ icon('tag') + " #{tag}"
end
end.join(" ").html_safe
end
def link_to_browse_code(project, commit)
- return unless current_controller?(:projects, :commits)
+ return unless current_controller?(:commits)
if @path.blank?
return link_to(
@@ -198,8 +189,8 @@ module CommitsHelper
tree_join(commit_sha, diff_new_path)),
class: 'btn view-file js-view-file'
) do
- raw('View file @') + content_tag(:span, commit_sha[0..6],
- class: 'commit-short-id')
+ raw('View file @ ') + content_tag(:span, Commit.truncate_sha(commit_sha),
+ class: 'commit-sha')
end
end
diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb
index dc144906548..4a06ee653ee 100644
--- a/app/helpers/diff_helper.rb
+++ b/app/helpers/diff_helper.rb
@@ -63,7 +63,7 @@ module DiffHelper
def parallel_diff_discussions(left, right, diff_file)
return unless @grouped_diff_discussions
-
+
discussions_left = discussions_right = nil
if left && (left.unchanged? || left.removed?)
@@ -98,7 +98,7 @@ module DiffHelper
[
content_tag(:span, link_to(truncate(blob.name, length: 40), tree)),
'@',
- content_tag(:span, commit_id, class: 'monospace'),
+ content_tag(:span, commit_id, class: 'commit-sha')
].join(' ').html_safe
end
diff --git a/app/helpers/emails_helper.rb b/app/helpers/emails_helper.rb
index f927cfc998f..3b24f183785 100644
--- a/app/helpers/emails_helper.rb
+++ b/app/helpers/emails_helper.rb
@@ -12,7 +12,7 @@ module EmailsHelper
"action" => {
"@type" => "ViewAction",
"name" => name,
- "url" => url,
+ "url" => url
}
}
diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb
index 960111ca045..751d61955b7 100644
--- a/app/helpers/events_helper.rb
+++ b/app/helpers/events_helper.rb
@@ -41,7 +41,7 @@ module EventsHelper
link_opts = {
class: "event-filter-link",
id: "#{key}_event_filter",
- title: "Filter by #{tooltip.downcase}",
+ title: "Filter by #{tooltip.downcase}"
}
content_tag :li, class: active do
@@ -164,9 +164,14 @@ module EventsHelper
def event_note_title_html(event)
if event.note_target
- link_to(event_note_target_path(event), title: event.target_title, class: 'has-tooltip') do
- "#{event.note_target_type} #{event.note_target_reference}"
- end
+ text = raw("#{event.note_target_type} ") +
+ if event.commit_note?
+ content_tag(:span, event.note_target_reference, class: 'commit-sha')
+ else
+ event.note_target_reference
+ end
+
+ link_to(text, event_note_target_path(event), title: event.target_title, class: 'has-tooltip')
else
content_tag(:strong, '(deleted)')
end
diff --git a/app/helpers/explore_helper.rb b/app/helpers/explore_helper.rb
index 7bd212a3ef9..b981a1e8242 100644
--- a/app/helpers/explore_helper.rb
+++ b/app/helpers/explore_helper.rb
@@ -10,7 +10,7 @@ module ExploreHelper
personal: params[:personal],
archived: params[:archived],
shared: params[:shared],
- namespace_id: params[:namespace_id],
+ namespace_id: params[:namespace_id]
}
options = exist_opts.merge(options).delete_if { |key, value| value.blank? }
diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb
index bcf71bc347b..fc308b3960e 100644
--- a/app/helpers/gitlab_routing_helper.rb
+++ b/app/helpers/gitlab_routing_helper.rb
@@ -54,6 +54,10 @@ module GitlabRoutingHelper
namespace_project_builds_path(project.namespace, project, *args)
end
+ def project_ref_path(project, ref_name, *args)
+ namespace_project_commits_path(project.namespace, project, ref_name, *args)
+ end
+
def project_container_registry_path(project, *args)
namespace_project_container_registry_index_path(project.namespace, project, *args)
end
diff --git a/app/helpers/icons_helper.rb b/app/helpers/icons_helper.rb
index 55fa81e95ef..f29faeca22d 100644
--- a/app/helpers/icons_helper.rb
+++ b/app/helpers/icons_helper.rb
@@ -7,9 +7,10 @@ module IconsHelper
# font-awesome-rails gem, but should we ever use a different icon pack in the
# future we won't have to change hundreds of method calls.
def icon(names, options = {})
- if (options.keys & %w[aria-hidden aria-label]).empty?
- # Add `aria-hidden` if there are no aria's set
+ if (options.keys & %w[aria-hidden aria-label data-hidden]).empty?
+ # Add 'aria-hidden' and 'data-hidden' if they are not set in options.
options['aria-hidden'] = true
+ options['data-hidden'] = true
end
options.include?(:base) ? fa_stacked_icon(names, options) : fa_icon(names, options)
@@ -19,6 +20,8 @@ module IconsHelper
case names
when "standard"
names = "key"
+ when "two-factor"
+ names = "key"
end
options.include?(:base) ? fa_stacked_icon(names, options) : fa_icon(names, options)
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index fbbce6876c2..9290e4ec133 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -67,9 +67,10 @@ module IssuablesHelper
end
def users_dropdown_label(selected_users)
- if selected_users.length == 0
+ case selected_users.length
+ when 0
"Unassigned"
- elsif selected_users.length == 1
+ when 1
selected_users[0].name
else
"#{selected_users[0].name} + #{selected_users.length - 1} more"
@@ -136,11 +137,9 @@ module IssuablesHelper
author_output << link_to_member(project, issuable.author, size: 24, by_username: true, avatar: false, mobile_classes: "hidden-sm hidden-md hidden-lg")
end
- if issuable.tasks?
- output << "&ensp;".html_safe
- output << content_tag(:span, issuable.task_status, id: "task_status", class: "hidden-xs hidden-sm")
- output << content_tag(:span, issuable.task_status_short, id: "task_status_short", class: "hidden-md hidden-lg")
- end
+ output << "&ensp;".html_safe
+ output << content_tag(:span, issuable.task_status, id: "task_status", class: "hidden-xs hidden-sm")
+ output << content_tag(:span, issuable.task_status_short, id: "task_status_short", class: "hidden-md hidden-lg")
output
end
diff --git a/app/helpers/markup_helper.rb b/app/helpers/markup_helper.rb
index b241a14740b..941cfce8370 100644
--- a/app/helpers/markup_helper.rb
+++ b/app/helpers/markup_helper.rb
@@ -1,6 +1,9 @@
require 'nokogiri'
module MarkupHelper
+ include ActionView::Helpers::TagHelper
+ include ActionView::Context
+
def plain?(filename)
Gitlab::MarkupHelper.plain?(filename)
end
@@ -32,7 +35,7 @@ module MarkupHelper
context = {
project: @project,
current_user: (current_user if defined?(current_user)),
- pipeline: :single_line,
+ pipeline: :single_line
}
gfm_body = Banzai.render(body, context)
@@ -116,13 +119,13 @@ module MarkupHelper
if gitlab_markdown?(file_name)
markdown_unsafe(text, context)
elsif asciidoc?(file_name)
- asciidoc_unsafe(text)
+ asciidoc_unsafe(text, context)
elsif plain?(file_name)
content_tag :pre, class: 'plain-readme' do
text
end
else
- other_markup_unsafe(file_name, text)
+ other_markup_unsafe(file_name, text, context)
end
rescue RuntimeError
simple_format(text)
@@ -217,12 +220,12 @@ module MarkupHelper
Banzai.render(text, context)
end
- def asciidoc_unsafe(text)
- Gitlab::Asciidoc.render(text)
+ def asciidoc_unsafe(text, context = {})
+ Gitlab::Asciidoc.render(text, context)
end
- def other_markup_unsafe(file_name, text)
- Gitlab::OtherMarkup.render(file_name, text)
+ def other_markup_unsafe(file_name, text, context = {})
+ Gitlab::OtherMarkup.render(file_name, text, context)
end
def prepare_for_rendering(html, context = {})
diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb
index 23e55539f0a..39d30631646 100644
--- a/app/helpers/merge_requests_helper.rb
+++ b/app/helpers/merge_requests_helper.rb
@@ -54,7 +54,7 @@ module MergeRequestsHelper
source_project_id: merge_request.source_project_id,
target_project_id: merge_request.target_project_id,
source_branch: merge_request.source_branch,
- target_branch: merge_request.target_branch,
+ target_branch: merge_request.target_branch
},
change_branches: true
)
diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb
index 52403640c05..375110b77e2 100644
--- a/app/helpers/notes_helper.rb
+++ b/app/helpers/notes_helper.rb
@@ -19,7 +19,7 @@ module NotesHelper
id: noteable.id,
class: noteable.class.name,
resources: noteable.class.table_name,
- project_id: noteable.project.id,
+ project_id: noteable.project.id
}.to_json
end
@@ -34,7 +34,7 @@ module NotesHelper
data = {
line_code: line_code,
- line_type: line_type,
+ line_type: line_type
}
if @use_legacy_diff_notes
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 8c26348a975..98bbcfaaba5 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -110,11 +110,8 @@ module ProjectsHelper
end
def license_short_name(project)
- return 'LICENSE' if project.repository.license_key.nil?
-
- license = Licensee::License.new(project.repository.license_key)
-
- license.nickname || license.name
+ license = project.repository.license
+ license&.nickname || license&.name || 'LICENSE'
end
def last_push_event
@@ -160,7 +157,15 @@ module ProjectsHelper
end
def project_list_cache_key(project)
- key = [project.namespace.cache_key, project.cache_key, controller.controller_name, controller.action_name, current_application_settings.cache_key, 'v2.4']
+ key = [
+ project.route.cache_key,
+ project.cache_key,
+ controller.controller_name,
+ controller.action_name,
+ current_application_settings.cache_key,
+ 'v2.4'
+ ]
+
key << pipeline_status_cache_key(project.pipeline_status) if project.pipeline_status.has_status?
key
diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb
index 8ff8db16514..9c46035057f 100644
--- a/app/helpers/search_helper.rb
+++ b/app/helpers/search_helper.rb
@@ -42,7 +42,7 @@ module SearchHelper
{ category: "Settings", label: "User settings", url: profile_path },
{ category: "Settings", label: "SSH Keys", url: profile_keys_path },
{ category: "Settings", label: "Dashboard", url: root_path },
- { category: "Settings", label: "Admin Section", url: admin_root_path },
+ { category: "Settings", label: "Admin Section", url: admin_root_path }
]
end
@@ -57,7 +57,7 @@ module SearchHelper
{ category: "Help", label: "SSH Keys Help", url: help_page_path("ssh/README") },
{ category: "Help", label: "System Hooks Help", url: help_page_path("system_hooks/system_hooks") },
{ category: "Help", label: "Webhooks Help", url: help_page_path("user/project/integrations/webhooks") },
- { category: "Help", label: "Workflow Help", url: help_page_path("workflow/README") },
+ { category: "Help", label: "Workflow Help", url: help_page_path("workflow/README") }
]
end
@@ -76,7 +76,7 @@ module SearchHelper
{ category: "Current Project", label: "Milestones", url: namespace_project_milestones_path(@project.namespace, @project) },
{ category: "Current Project", label: "Snippets", url: namespace_project_snippets_path(@project.namespace, @project) },
{ category: "Current Project", label: "Members", url: namespace_project_settings_members_path(@project.namespace, @project) },
- { category: "Current Project", label: "Wiki", url: namespace_project_wikis_path(@project.namespace, @project) },
+ { category: "Current Project", label: "Wiki", url: namespace_project_wikis_path(@project.namespace, @project) }
]
else
[]
diff --git a/app/helpers/selects_helper.rb b/app/helpers/selects_helper.rb
index 8706876ae4a..a7d1fe4aa47 100644
--- a/app/helpers/selects_helper.rb
+++ b/app/helpers/selects_helper.rb
@@ -67,7 +67,7 @@ module SelectsHelper
current_user: opts[:current_user] || false,
"push-code-to-protected-branches" => opts[:push_code_to_protected_branches],
author_id: opts[:author_id] || '',
- skip_users: opts[:skip_users] ? opts[:skip_users].map(&:id) : nil,
+ skip_users: opts[:skip_users] ? opts[:skip_users].map(&:id) : nil
}
end
end
diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb
index 4882d9b71d2..b408ec0c6a4 100644
--- a/app/helpers/sorting_helper.rb
+++ b/app/helpers/sorting_helper.rb
@@ -58,7 +58,7 @@ module SortingHelper
sort_value_due_date_soon => sort_title_due_date_soon,
sort_value_due_date_later => sort_title_due_date_later,
sort_value_start_date_soon => sort_title_start_date_soon,
- sort_value_start_date_later => sort_title_start_date_later,
+ sort_value_start_date_later => sort_title_start_date_later
}
end
diff --git a/app/helpers/submodule_helper.rb b/app/helpers/submodule_helper.rb
index a762b320d56..09b73eee8cf 100644
--- a/app/helpers/submodule_helper.rb
+++ b/app/helpers/submodule_helper.rb
@@ -1,28 +1,34 @@
module SubmoduleHelper
include Gitlab::ShellAdapter
+ VALID_SUBMODULE_PROTOCOLS = %w[http https git ssh].freeze
+
# links to files listing for submodule if submodule is a project on this server
def submodule_links(submodule_item, ref = nil, repository = @repository)
url = repository.submodule_url_for(ref, submodule_item.path)
- return url, nil unless url =~ /([^\/:]+)\/([^\/]+(?:\.git)?)\Z/
-
- namespace = $1
- project = $2
- project.chomp!('.git')
-
- if self_url?(url, namespace, project)
- return namespace_project_path(namespace, project),
- namespace_project_tree_path(namespace, project,
- submodule_item.id)
- elsif relative_self_url?(url)
- relative_self_links(url, submodule_item.id)
- elsif github_dot_com_url?(url)
- standard_links('github.com', namespace, project, submodule_item.id)
- elsif gitlab_dot_com_url?(url)
- standard_links('gitlab.com', namespace, project, submodule_item.id)
+ if url == '.' || url == './'
+ url = File.join(Gitlab.config.gitlab.url, @project.full_path)
+ end
+
+ if url =~ /([^\/:]+)\/([^\/]+(?:\.git)?)\Z/
+ namespace, project = $1, $2
+ project.sub!(/\.git\z/, '')
+
+ if self_url?(url, namespace, project)
+ [namespace_project_path(namespace, project),
+ namespace_project_tree_path(namespace, project, submodule_item.id)]
+ elsif relative_self_url?(url)
+ relative_self_links(url, submodule_item.id)
+ elsif github_dot_com_url?(url)
+ standard_links('github.com', namespace, project, submodule_item.id)
+ elsif gitlab_dot_com_url?(url)
+ standard_links('gitlab.com', namespace, project, submodule_item.id)
+ else
+ [sanitize_submodule_url(url), nil]
+ end
else
- return url, nil
+ [sanitize_submodule_url(url), nil]
end
end
@@ -73,4 +79,16 @@ module SubmoduleHelper
namespace_project_tree_path(namespace, base, commit)
]
end
+
+ def sanitize_submodule_url(url)
+ uri = URI.parse(url)
+
+ if uri.scheme.in?(VALID_SUBMODULE_PROTOCOLS)
+ uri.to_s
+ else
+ nil
+ end
+ rescue URI::InvalidURIError
+ nil
+ end
end
diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb
index f19e2f9db9c..19286fadb19 100644
--- a/app/helpers/todos_helper.rb
+++ b/app/helpers/todos_helper.rb
@@ -24,10 +24,13 @@ module TodosHelper
end
def todo_target_link(todo)
- target = todo.target_type.titleize.downcase
- link_to "#{target} #{todo.target_reference}", todo_target_path(todo),
- class: 'has-tooltip',
- title: todo.target.title
+ text = raw("#{todo.target_type.titleize.downcase} ") +
+ if todo.for_commit?
+ content_tag(:span, todo.target_reference, class: 'commit-sha')
+ else
+ todo.target_reference
+ end
+ link_to text, todo_target_path(todo), class: 'has-tooltip', title: todo.target.title
end
def todo_target_path(todo)
@@ -63,7 +66,7 @@ module TodosHelper
project_id: params[:project_id],
author_id: params[:author_id],
type: params[:type],
- action_id: params[:action_id],
+ action_id: params[:action_id]
}
end
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 54f01f8637e..043f57241a3 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -246,7 +246,7 @@ class ApplicationSetting < ActiveRecord::Base
two_factor_grace_period: 48,
user_default_external: false,
polling_interval_multiplier: 1,
- usage_ping_enabled: true
+ usage_ping_enabled: Settings.gitlab['usage_ping_enabled']
}
end
@@ -349,6 +349,14 @@ class ApplicationSetting < ActiveRecord::Base
sidekiq_throttling_enabled
end
+ def usage_ping_can_be_configured?
+ Settings.gitlab.usage_ping_enabled
+ end
+
+ def usage_ping_enabled
+ usage_ping_can_be_configured? && super
+ end
+
private
def ensure_uuid!
diff --git a/app/models/blob.rb b/app/models/blob.rb
index eaf0b713122..e75926241ba 100644
--- a/app/models/blob.rb
+++ b/app/models/blob.rb
@@ -33,12 +33,31 @@ class Blob < SimpleDelegator
BlobViewer::PDF,
BlobViewer::BinarySTL,
- BlobViewer::TextSTL,
+ BlobViewer::TextSTL
+ ].sort_by { |v| v.binary? ? 0 : 1 }.freeze
+
+ AUXILIARY_VIEWERS = [
+ BlobViewer::GitlabCiYml,
+ BlobViewer::RouteMap,
+
+ BlobViewer::Readme,
+ BlobViewer::License,
+ BlobViewer::Contributing,
+ BlobViewer::Changelog,
+
+ BlobViewer::Cartfile,
+ BlobViewer::ComposerJson,
+ BlobViewer::Gemfile,
+ BlobViewer::Gemspec,
+ BlobViewer::GodepsJson,
+ BlobViewer::PackageJson,
+ BlobViewer::Podfile,
+ BlobViewer::Podspec,
+ BlobViewer::PodspecJson,
+ BlobViewer::RequirementsTxt,
+ BlobViewer::YarnLock
].freeze
- BINARY_VIEWERS = RICH_VIEWERS.select(&:binary?).freeze
- TEXT_VIEWERS = RICH_VIEWERS.select(&:text?).freeze
-
attr_reader :project
# Wrap a Gitlab::Git::Blob object, or return nil when given nil
@@ -154,6 +173,12 @@ class Blob < SimpleDelegator
@rich_viewer = rich_viewer_class&.new(self)
end
+ def auxiliary_viewer
+ return @auxiliary_viewer if defined?(@auxiliary_viewer)
+
+ @auxiliary_viewer = auxiliary_viewer_class&.new(self)
+ end
+
def rendered_as_text?(ignore_errors: true)
simple_viewer.text? && (ignore_errors || simple_viewer.render_error.nil?)
end
@@ -180,17 +205,18 @@ class Blob < SimpleDelegator
end
def rich_viewer_class
+ viewer_class_from(RICH_VIEWERS)
+ end
+
+ def auxiliary_viewer_class
+ viewer_class_from(AUXILIARY_VIEWERS)
+ end
+
+ def viewer_class_from(classes)
return if empty? || external_storage_error?
- classes =
- if stored_externally?
- BINARY_VIEWERS + TEXT_VIEWERS
- elsif binary?
- BINARY_VIEWERS
- else # text
- TEXT_VIEWERS
- end
+ verify_binary = !stored_externally?
- classes.find { |viewer_class| viewer_class.can_render?(self) }
+ classes.find { |viewer_class| viewer_class.can_render?(self, verify_binary: verify_binary) }
end
end
diff --git a/app/models/blob_viewer/auxiliary.rb b/app/models/blob_viewer/auxiliary.rb
new file mode 100644
index 00000000000..07a207730cf
--- /dev/null
+++ b/app/models/blob_viewer/auxiliary.rb
@@ -0,0 +1,18 @@
+module BlobViewer
+ module Auxiliary
+ extend ActiveSupport::Concern
+
+ include Gitlab::Allowable
+
+ included do
+ self.loading_partial_name = 'loading_auxiliary'
+ self.type = :auxiliary
+ self.overridable_max_size = 100.kilobytes
+ self.max_size = 100.kilobytes
+ end
+
+ def visible_to?(current_user)
+ true
+ end
+ end
+end
diff --git a/app/models/blob_viewer/base.rb b/app/models/blob_viewer/base.rb
index a8b91d8d6bc..26a3778c2a3 100644
--- a/app/models/blob_viewer/base.rb
+++ b/app/models/blob_viewer/base.rb
@@ -1,18 +1,28 @@
module BlobViewer
class Base
- class_attribute :partial_name, :type, :extensions, :client_side, :binary, :switcher_icon, :switcher_title, :max_size, :absolute_max_size
+ PARTIAL_PATH_PREFIX = 'projects/blob/viewers'.freeze
- delegate :partial_path, :rich?, :simple?, :client_side?, :server_side?, :text?, :binary?, to: :class
+ class_attribute :partial_name, :loading_partial_name, :type, :extensions, :file_types, :load_async, :binary, :switcher_icon, :switcher_title, :overridable_max_size, :max_size
+
+ self.loading_partial_name = 'loading'
+
+ delegate :partial_path, :loading_partial_path, :rich?, :simple?, :text?, :binary?, to: :class
attr_reader :blob
attr_accessor :override_max_size
+ delegate :project, to: :blob
+
def initialize(blob)
@blob = blob
end
def self.partial_path
- "projects/blob/viewers/#{partial_name}"
+ File.join(PARTIAL_PATH_PREFIX, partial_name)
+ end
+
+ def self.loading_partial_path
+ File.join(PARTIAL_PATH_PREFIX, loading_partial_name)
end
def self.rich?
@@ -23,12 +33,12 @@ module BlobViewer
type == :simple
end
- def self.client_side?
- client_side
+ def self.auxiliary?
+ type == :auxiliary
end
- def self.server_side?
- !client_side?
+ def self.load_async?
+ load_async
end
def self.binary?
@@ -39,20 +49,36 @@ module BlobViewer
!binary?
end
- def self.can_render?(blob)
- !extensions || extensions.include?(blob.extension)
+ def self.can_render?(blob, verify_binary: true)
+ return false if verify_binary && binary? != blob.binary?
+ return true if extensions&.include?(blob.extension)
+ return true if file_types&.include?(Gitlab::FileDetector.type_of(blob.path))
+
+ false
end
- def too_large?
- blob.raw_size > max_size
+ def load_async?
+ self.class.load_async? && render_error.nil?
end
- def absolutely_too_large?
- blob.raw_size > absolute_max_size
+ def exceeds_overridable_max_size?
+ overridable_max_size && blob.raw_size > overridable_max_size
+ end
+
+ def exceeds_max_size?
+ max_size && blob.raw_size > max_size
end
def can_override_max_size?
- too_large? && !absolutely_too_large?
+ exceeds_overridable_max_size? && !exceeds_max_size?
+ end
+
+ def too_large?
+ if override_max_size
+ exceeds_max_size?
+ else
+ exceeds_overridable_max_size?
+ end
end
# This method is used on the server side to check whether we can attempt to
@@ -67,31 +93,13 @@ module BlobViewer
# binary from `blob_raw_url` and does its own format validation and error
# rendering, especially for potentially large binary formats.
def render_error
- return @render_error if defined?(@render_error)
-
- @render_error =
- if server_side_but_stored_externally?
- # Files that are not stored in the repository, like LFS files and
- # build artifacts, can only be rendered using a client-side viewer,
- # since we do not want to read large amounts of data into memory on the
- # server side. Client-side viewers use JS and can fetch the file from
- # `blob_raw_url` using AJAX.
- :server_side_but_stored_externally
- elsif override_max_size ? absolutely_too_large? : too_large?
- :too_large
- end
- end
-
- def prepare!
- if server_side? && blob.project
- blob.load_all_data!(blob.project.repository)
+ if too_large?
+ :too_large
end
end
- private
-
- def server_side_but_stored_externally?
- server_side? && blob.stored_externally?
+ def prepare!
+ # To be overridden by subclasses
end
end
end
diff --git a/app/models/blob_viewer/cartfile.rb b/app/models/blob_viewer/cartfile.rb
new file mode 100644
index 00000000000..d8471bc33c0
--- /dev/null
+++ b/app/models/blob_viewer/cartfile.rb
@@ -0,0 +1,15 @@
+module BlobViewer
+ class Cartfile < DependencyManager
+ include Static
+
+ self.file_types = %i(cartfile)
+
+ def manager_name
+ 'Carthage'
+ end
+
+ def manager_url
+ 'https://github.com/Carthage/Carthage'
+ end
+ end
+end
diff --git a/app/models/blob_viewer/changelog.rb b/app/models/blob_viewer/changelog.rb
new file mode 100644
index 00000000000..0464ae27f71
--- /dev/null
+++ b/app/models/blob_viewer/changelog.rb
@@ -0,0 +1,16 @@
+module BlobViewer
+ class Changelog < Base
+ include Auxiliary
+ include Static
+
+ self.partial_name = 'changelog'
+ self.file_types = %i(changelog)
+ self.binary = false
+
+ def render_error
+ return if project.repository.tag_count > 0
+
+ :no_tags
+ end
+ end
+end
diff --git a/app/models/blob_viewer/client_side.rb b/app/models/blob_viewer/client_side.rb
index 42ec68f864b..cc68236f92b 100644
--- a/app/models/blob_viewer/client_side.rb
+++ b/app/models/blob_viewer/client_side.rb
@@ -3,9 +3,9 @@ module BlobViewer
extend ActiveSupport::Concern
included do
- self.client_side = true
- self.max_size = 10.megabytes
- self.absolute_max_size = 50.megabytes
+ self.load_async = false
+ self.overridable_max_size = 10.megabytes
+ self.max_size = 50.megabytes
end
end
end
diff --git a/app/models/blob_viewer/composer_json.rb b/app/models/blob_viewer/composer_json.rb
new file mode 100644
index 00000000000..ef8b4aef8e8
--- /dev/null
+++ b/app/models/blob_viewer/composer_json.rb
@@ -0,0 +1,23 @@
+module BlobViewer
+ class ComposerJson < DependencyManager
+ include ServerSide
+
+ self.file_types = %i(composer_json)
+
+ def manager_name
+ 'Composer'
+ end
+
+ def manager_url
+ 'https://getcomposer.com/'
+ end
+
+ def package_name
+ @package_name ||= package_name_from_json('name')
+ end
+
+ def package_url
+ "https://packagist.org/packages/#{package_name}"
+ end
+ end
+end
diff --git a/app/models/blob_viewer/contributing.rb b/app/models/blob_viewer/contributing.rb
new file mode 100644
index 00000000000..fbd1dd48697
--- /dev/null
+++ b/app/models/blob_viewer/contributing.rb
@@ -0,0 +1,10 @@
+module BlobViewer
+ class Contributing < Base
+ include Auxiliary
+ include Static
+
+ self.partial_name = 'contributing'
+ self.file_types = %i(contributing)
+ self.binary = false
+ end
+end
diff --git a/app/models/blob_viewer/dependency_manager.rb b/app/models/blob_viewer/dependency_manager.rb
new file mode 100644
index 00000000000..a8d9be945dc
--- /dev/null
+++ b/app/models/blob_viewer/dependency_manager.rb
@@ -0,0 +1,43 @@
+module BlobViewer
+ class DependencyManager < Base
+ include Auxiliary
+
+ self.partial_name = 'dependency_manager'
+ self.binary = false
+
+ def manager_name
+ raise NotImplementedError
+ end
+
+ def manager_url
+ raise NotImplementedError
+ end
+
+ def package_type
+ 'package'
+ end
+
+ def package_name
+ nil
+ end
+
+ def package_url
+ nil
+ end
+
+ private
+
+ def package_name_from_json(key)
+ prepare!
+
+ JSON.parse(blob.data)[key] rescue nil
+ end
+
+ def package_name_from_method_call(name)
+ prepare!
+
+ match = blob.data.match(/#{name}\s*=\s*["'](?<name>[^"']+)["']/)
+ match[:name] if match
+ end
+ end
+end
diff --git a/app/models/blob_viewer/download.rb b/app/models/blob_viewer/download.rb
index adc06587f69..074e7204814 100644
--- a/app/models/blob_viewer/download.rb
+++ b/app/models/blob_viewer/download.rb
@@ -1,17 +1,9 @@
module BlobViewer
class Download < Base
include Simple
- # We treat the Download viewer as if it renders the content client-side,
- # so that it doesn't attempt to load the entire blob contents and is
- # rendered synchronously instead of loaded asynchronously.
- include ClientSide
+ include Static
self.partial_name = 'download'
self.binary = true
-
- # We can always render the Download viewer, even if the blob is in LFS or too large.
- def render_error
- nil
- end
end
end
diff --git a/app/models/blob_viewer/gemfile.rb b/app/models/blob_viewer/gemfile.rb
new file mode 100644
index 00000000000..fae8c8df23f
--- /dev/null
+++ b/app/models/blob_viewer/gemfile.rb
@@ -0,0 +1,15 @@
+module BlobViewer
+ class Gemfile < DependencyManager
+ include Static
+
+ self.file_types = %i(gemfile gemfile_lock)
+
+ def manager_name
+ 'Bundler'
+ end
+
+ def manager_url
+ 'http://bundler.io/'
+ end
+ end
+end
diff --git a/app/models/blob_viewer/gemspec.rb b/app/models/blob_viewer/gemspec.rb
new file mode 100644
index 00000000000..7802edeb754
--- /dev/null
+++ b/app/models/blob_viewer/gemspec.rb
@@ -0,0 +1,27 @@
+module BlobViewer
+ class Gemspec < DependencyManager
+ include ServerSide
+
+ self.file_types = %i(gemspec)
+
+ def manager_name
+ 'RubyGems'
+ end
+
+ def manager_url
+ 'https://rubygems.org/'
+ end
+
+ def package_type
+ 'gem'
+ end
+
+ def package_name
+ @package_name ||= package_name_from_method_call('name')
+ end
+
+ def package_url
+ "https://rubygems.org/gems/#{package_name}"
+ end
+ end
+end
diff --git a/app/models/blob_viewer/gitlab_ci_yml.rb b/app/models/blob_viewer/gitlab_ci_yml.rb
new file mode 100644
index 00000000000..7267c3965d3
--- /dev/null
+++ b/app/models/blob_viewer/gitlab_ci_yml.rb
@@ -0,0 +1,23 @@
+module BlobViewer
+ class GitlabCiYml < Base
+ include ServerSide
+ include Auxiliary
+
+ self.partial_name = 'gitlab_ci_yml'
+ self.loading_partial_name = 'gitlab_ci_yml_loading'
+ self.file_types = %i(gitlab_ci)
+ self.binary = false
+
+ def validation_message
+ return @validation_message if defined?(@validation_message)
+
+ prepare!
+
+ @validation_message = Ci::GitlabCiYamlProcessor.validation_message(blob.data)
+ end
+
+ def valid?
+ validation_message.blank?
+ end
+ end
+end
diff --git a/app/models/blob_viewer/godeps_json.rb b/app/models/blob_viewer/godeps_json.rb
new file mode 100644
index 00000000000..e19a602603b
--- /dev/null
+++ b/app/models/blob_viewer/godeps_json.rb
@@ -0,0 +1,15 @@
+module BlobViewer
+ class GodepsJson < DependencyManager
+ include Static
+
+ self.file_types = %i(godeps_json)
+
+ def manager_name
+ 'godep'
+ end
+
+ def manager_url
+ 'https://github.com/tools/godep'
+ end
+ end
+end
diff --git a/app/models/blob_viewer/license.rb b/app/models/blob_viewer/license.rb
new file mode 100644
index 00000000000..57355f2c3aa
--- /dev/null
+++ b/app/models/blob_viewer/license.rb
@@ -0,0 +1,20 @@
+module BlobViewer
+ class License < Base
+ include Auxiliary
+ include Static
+
+ self.partial_name = 'license'
+ self.file_types = %i(license)
+ self.binary = false
+
+ def license
+ project.repository.license
+ end
+
+ def render_error
+ return if license
+
+ :unknown_license
+ end
+ end
+end
diff --git a/app/models/blob_viewer/markup.rb b/app/models/blob_viewer/markup.rb
index 8fdbab30dd1..33b59c4f512 100644
--- a/app/models/blob_viewer/markup.rb
+++ b/app/models/blob_viewer/markup.rb
@@ -5,6 +5,7 @@ module BlobViewer
self.partial_name = 'markup'
self.extensions = Gitlab::MarkupHelper::EXTENSIONS
+ self.file_types = %i(readme)
self.binary = false
end
end
diff --git a/app/models/blob_viewer/package_json.rb b/app/models/blob_viewer/package_json.rb
new file mode 100644
index 00000000000..09221efb56c
--- /dev/null
+++ b/app/models/blob_viewer/package_json.rb
@@ -0,0 +1,23 @@
+module BlobViewer
+ class PackageJson < DependencyManager
+ include ServerSide
+
+ self.file_types = %i(package_json)
+
+ def manager_name
+ 'npm'
+ end
+
+ def manager_url
+ 'https://www.npmjs.com/'
+ end
+
+ def package_name
+ @package_name ||= package_name_from_json('name')
+ end
+
+ def package_url
+ "https://www.npmjs.com/package/#{package_name}"
+ end
+ end
+end
diff --git a/app/models/blob_viewer/podfile.rb b/app/models/blob_viewer/podfile.rb
new file mode 100644
index 00000000000..507bc734cb4
--- /dev/null
+++ b/app/models/blob_viewer/podfile.rb
@@ -0,0 +1,15 @@
+module BlobViewer
+ class Podfile < DependencyManager
+ include Static
+
+ self.file_types = %i(podfile)
+
+ def manager_name
+ 'CocoaPods'
+ end
+
+ def manager_url
+ 'https://cocoapods.org/'
+ end
+ end
+end
diff --git a/app/models/blob_viewer/podspec.rb b/app/models/blob_viewer/podspec.rb
new file mode 100644
index 00000000000..a4c242db3a9
--- /dev/null
+++ b/app/models/blob_viewer/podspec.rb
@@ -0,0 +1,27 @@
+module BlobViewer
+ class Podspec < DependencyManager
+ include ServerSide
+
+ self.file_types = %i(podspec)
+
+ def manager_name
+ 'CocoaPods'
+ end
+
+ def manager_url
+ 'https://cocoapods.org/'
+ end
+
+ def package_type
+ 'pod'
+ end
+
+ def package_name
+ @package_name ||= package_name_from_method_call('name')
+ end
+
+ def package_url
+ "https://cocoapods.org/pods/#{package_name}"
+ end
+ end
+end
diff --git a/app/models/blob_viewer/podspec_json.rb b/app/models/blob_viewer/podspec_json.rb
new file mode 100644
index 00000000000..602f4a51fd9
--- /dev/null
+++ b/app/models/blob_viewer/podspec_json.rb
@@ -0,0 +1,9 @@
+module BlobViewer
+ class PodspecJson < Podspec
+ self.file_types = %i(podspec_json)
+
+ def package_name
+ @package_name ||= package_name_from_json('name')
+ end
+ end
+end
diff --git a/app/models/blob_viewer/readme.rb b/app/models/blob_viewer/readme.rb
new file mode 100644
index 00000000000..75c373a03bb
--- /dev/null
+++ b/app/models/blob_viewer/readme.rb
@@ -0,0 +1,14 @@
+module BlobViewer
+ class Readme < Base
+ include Auxiliary
+ include Static
+
+ self.partial_name = 'readme'
+ self.file_types = %i(readme)
+ self.binary = false
+
+ def visible_to?(current_user)
+ can?(current_user, :read_wiki, project)
+ end
+ end
+end
diff --git a/app/models/blob_viewer/requirements_txt.rb b/app/models/blob_viewer/requirements_txt.rb
new file mode 100644
index 00000000000..83ac55f61d0
--- /dev/null
+++ b/app/models/blob_viewer/requirements_txt.rb
@@ -0,0 +1,15 @@
+module BlobViewer
+ class RequirementsTxt < DependencyManager
+ include Static
+
+ self.file_types = %i(requirements_txt)
+
+ def manager_name
+ 'pip'
+ end
+
+ def manager_url
+ 'https://pip.pypa.io/'
+ end
+ end
+end
diff --git a/app/models/blob_viewer/route_map.rb b/app/models/blob_viewer/route_map.rb
new file mode 100644
index 00000000000..153b4eeb2c9
--- /dev/null
+++ b/app/models/blob_viewer/route_map.rb
@@ -0,0 +1,30 @@
+module BlobViewer
+ class RouteMap < Base
+ include ServerSide
+ include Auxiliary
+
+ self.partial_name = 'route_map'
+ self.loading_partial_name = 'route_map_loading'
+ self.file_types = %i(route_map)
+ self.binary = false
+
+ def validation_message
+ return @validation_message if defined?(@validation_message)
+
+ prepare!
+
+ @validation_message =
+ begin
+ Gitlab::RouteMap.new(blob.data)
+
+ nil
+ rescue Gitlab::RouteMap::FormatError => e
+ e.message
+ end
+ end
+
+ def valid?
+ validation_message.blank?
+ end
+ end
+end
diff --git a/app/models/blob_viewer/server_side.rb b/app/models/blob_viewer/server_side.rb
index 899107d02ea..87884dcd6bf 100644
--- a/app/models/blob_viewer/server_side.rb
+++ b/app/models/blob_viewer/server_side.rb
@@ -3,9 +3,28 @@ module BlobViewer
extend ActiveSupport::Concern
included do
- self.client_side = false
- self.max_size = 2.megabytes
- self.absolute_max_size = 5.megabytes
+ self.load_async = true
+ self.overridable_max_size = 2.megabytes
+ self.max_size = 5.megabytes
+ end
+
+ def prepare!
+ if blob.project
+ blob.load_all_data!(blob.project.repository)
+ end
+ end
+
+ def render_error
+ if blob.stored_externally?
+ # Files that are not stored in the repository, like LFS files and
+ # build artifacts, can only be rendered using a client-side viewer,
+ # since we do not want to read large amounts of data into memory on the
+ # server side. Client-side viewers use JS and can fetch the file from
+ # `blob_raw_url` using AJAX.
+ return :server_side_but_stored_externally
+ end
+
+ super
end
end
end
diff --git a/app/models/blob_viewer/static.rb b/app/models/blob_viewer/static.rb
new file mode 100644
index 00000000000..c9e257e5388
--- /dev/null
+++ b/app/models/blob_viewer/static.rb
@@ -0,0 +1,14 @@
+module BlobViewer
+ module Static
+ extend ActiveSupport::Concern
+
+ included do
+ self.load_async = false
+ end
+
+ # We can always render a static viewer, even if the blob is too large.
+ def render_error
+ nil
+ end
+ end
+end
diff --git a/app/models/blob_viewer/text.rb b/app/models/blob_viewer/text.rb
index e27b2c2b493..eddca50b4d4 100644
--- a/app/models/blob_viewer/text.rb
+++ b/app/models/blob_viewer/text.rb
@@ -5,7 +5,7 @@ module BlobViewer
self.partial_name = 'text'
self.binary = false
- self.max_size = 1.megabyte
- self.absolute_max_size = 10.megabytes
+ self.overridable_max_size = 1.megabyte
+ self.max_size = 10.megabytes
end
end
diff --git a/app/models/blob_viewer/yarn_lock.rb b/app/models/blob_viewer/yarn_lock.rb
new file mode 100644
index 00000000000..31588ddcbab
--- /dev/null
+++ b/app/models/blob_viewer/yarn_lock.rb
@@ -0,0 +1,15 @@
+module BlobViewer
+ class YarnLock < DependencyManager
+ include Static
+
+ self.file_types = %i(yarn_lock)
+
+ def manager_name
+ 'Yarn'
+ end
+
+ def manager_url
+ 'https://yarnpkg.com/'
+ end
+ end
+end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 971ab7cb0ee..760ec8e5919 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -124,8 +124,8 @@ module Ci
success? || failed? || canceled?
end
- def retried?
- !self.pipeline.statuses.latest.include?(self)
+ def latest?
+ !retried?
end
def expanded_environment_name
@@ -300,8 +300,8 @@ module Ci
def execute_hooks
return unless project
build_data = Gitlab::DataBuilder::Build.build(self)
- project.execute_hooks(build_data.dup, :build_hooks)
- project.execute_services(build_data.dup, :build_hooks)
+ project.execute_hooks(build_data.dup, :job_hooks)
+ project.execute_services(build_data.dup, :job_hooks)
PagesService.new(build_data).execute
project.running_or_pending_build_count(force: true)
end
diff --git a/app/models/ci/pipeline_schedule.rb b/app/models/ci/pipeline_schedule.rb
index 6d7cc83971e..cf6e53c4ca4 100644
--- a/app/models/ci/pipeline_schedule.rb
+++ b/app/models/ci/pipeline_schedule.rb
@@ -28,10 +28,18 @@ module Ci
!active?
end
+ def deactivate!
+ update_attribute(:active, false)
+ end
+
def importing_or_inactive?
importing? || inactive?
end
+ def runnable_by_owner?
+ Ability.allowed?(owner, :create_pipeline, project)
+ end
+
def set_next_run_at
self.next_run_at = Gitlab::Ci::CronParser.new(cron, cron_timezone).next_time_from(Time.now)
end
diff --git a/app/models/commit.rb b/app/models/commit.rb
index dea18bfedef..dbc0a22829e 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -49,7 +49,7 @@ class Commit
def max_diff_options
{
max_files: DIFF_HARD_LIMIT_FILES,
- max_lines: DIFF_HARD_LIMIT_LINES,
+ max_lines: DIFF_HARD_LIMIT_LINES
}
end
@@ -326,17 +326,22 @@ class Commit
end
def raw_diffs(*args)
- use_gitaly = Gitlab::GitalyClient.feature_enabled?(:commit_raw_diffs)
- deltas_only = args.last.is_a?(Hash) && args.last[:deltas_only]
-
- if use_gitaly && !deltas_only
- Gitlab::GitalyClient::Commit.diff_from_parent(self, *args)
+ if Gitlab::GitalyClient.feature_enabled?(:commit_raw_diffs)
+ Gitlab::GitalyClient::Commit.new(project.repository).diff_from_parent(self, *args)
else
raw.diffs(*args)
end
end
- delegate :deltas, to: :raw, prefix: :raw
+ def raw_deltas
+ @deltas ||= Gitlab::GitalyClient.migrate(:commit_deltas) do |is_enabled|
+ if is_enabled
+ Gitlab::GitalyClient::Commit.new(project.repository).commit_deltas(self)
+ else
+ raw.deltas
+ end
+ end
+ end
def diffs(diff_options = nil)
Gitlab::Diff::FileCollection::Commit.new(self, diff_options: diff_options)
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index 75d04fd2b08..ffafc678968 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -18,13 +18,7 @@ class CommitStatus < ActiveRecord::Base
validates :name, presence: true
alias_attribute :author, :user
-
- scope :latest, -> do
- max_id = unscope(:select).select("max(#{quoted_table_name}.id)")
-
- where(id: max_id.group(:name, :commit_id))
- end
-
+
scope :failed_but_allowed, -> do
where(allow_failure: true, status: [:failed, :canceled])
end
@@ -37,7 +31,8 @@ class CommitStatus < ActiveRecord::Base
false, all_state_names - [:failed, :canceled, :manual])
end
- scope :retried, -> { where.not(id: latest) }
+ scope :latest, -> { where(retried: [false, nil]) }
+ scope :retried, -> { where(retried: true) }
scope :ordered, -> { order(:name) }
scope :latest_ordered, -> { latest.ordered.includes(project: :namespace) }
scope :retried_ordered, -> { retried.ordered.includes(project: :namespace) }
diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb
index 6eddeab515e..c034bf9cbc0 100644
--- a/app/models/concerns/mentionable.rb
+++ b/app/models/concerns/mentionable.rb
@@ -44,14 +44,15 @@ module Mentionable
end
def all_references(current_user = nil, extractor: nil)
+ @extractors ||= {}
+
# Use custom extractor if it's passed in the function parameters.
if extractor
- @extractor = extractor
+ @extractors[current_user] = extractor
else
- @extractor ||= Gitlab::ReferenceExtractor.
- new(project, current_user)
+ extractor = @extractors[current_user] ||= Gitlab::ReferenceExtractor.new(project, current_user)
- @extractor.reset_memoized_values
+ extractor.reset_memoized_values
end
self.class.mentionable_attrs.each do |attr, options|
@@ -62,10 +63,10 @@ module Mentionable
skip_project_check: skip_project_check?
)
- @extractor.analyze(text, options)
+ extractor.analyze(text, options)
end
- @extractor
+ extractor
end
def mentioned_users(current_user = nil)
diff --git a/app/models/concerns/protected_branch_access.rb b/app/models/concerns/protected_branch_access.rb
index c41b807df8a..a40148a4394 100644
--- a/app/models/concerns/protected_branch_access.rb
+++ b/app/models/concerns/protected_branch_access.rb
@@ -7,5 +7,27 @@ module ProtectedBranchAccess
belongs_to :protected_branch
delegate :project, to: :protected_branch
+
+ validates :access_level, presence: true, inclusion: {
+ in: [
+ Gitlab::Access::MASTER,
+ Gitlab::Access::DEVELOPER,
+ Gitlab::Access::NO_ACCESS
+ ]
+ }
+
+ def self.human_access_levels
+ {
+ Gitlab::Access::MASTER => "Masters",
+ Gitlab::Access::DEVELOPER => "Developers + Masters",
+ Gitlab::Access::NO_ACCESS => "No one"
+ }.with_indifferent_access
+ end
+
+ def check_access(user)
+ return false if access_level == Gitlab::Access::NO_ACCESS
+
+ super
+ end
end
end
diff --git a/app/models/deployment.rb b/app/models/deployment.rb
index f83d9e8edee..216cec751e3 100644
--- a/app/models/deployment.rb
+++ b/app/models/deployment.rb
@@ -103,15 +103,10 @@ class Deployment < ActiveRecord::Base
project.monitoring_service.present?
end
- def metrics(timeframe)
+ def metrics
return {} unless has_metrics?
- half_timeframe = timeframe / 2
- timeframe_start = created_at - half_timeframe
- timeframe_end = created_at + half_timeframe
-
- metrics = project.monitoring_service.metrics(environment, timeframe_start: timeframe_start, timeframe_end: timeframe_end)
- metrics&.merge(deployment_time: created_at.to_i) || {}
+ project.monitoring_service.deployment_metrics(self)
end
private
diff --git a/app/models/diff_discussion.rb b/app/models/diff_discussion.rb
index d627fbe327f..14ddd2fcc88 100644
--- a/app/models/diff_discussion.rb
+++ b/app/models/diff_discussion.rb
@@ -39,7 +39,7 @@ class DiffDiscussion < Discussion
def reply_attributes
super.merge(
original_position: original_position.to_json,
- position: position.to_json,
+ position: position.to_json
)
end
end
diff --git a/app/models/environment.rb b/app/models/environment.rb
index bf33010fd21..61572d8d69a 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -62,7 +62,7 @@ class Environment < ActiveRecord::Base
def predefined_variables
[
{ key: 'CI_ENVIRONMENT_NAME', value: name, public: true },
- { key: 'CI_ENVIRONMENT_SLUG', value: slug, public: true },
+ { key: 'CI_ENVIRONMENT_SLUG', value: slug, public: true }
]
end
@@ -150,7 +150,7 @@ class Environment < ActiveRecord::Base
end
def metrics
- project.monitoring_service.metrics(self) if has_metrics?
+ project.monitoring_service.environment_metrics(self) if has_metrics?
end
# An environment name is not necessarily suitable for use in URLs, DNS
diff --git a/app/models/hooks/project_hook.rb b/app/models/hooks/project_hook.rb
index c631e7a7df5..ee6165fd32d 100644
--- a/app/models/hooks/project_hook.rb
+++ b/app/models/hooks/project_hook.rb
@@ -5,7 +5,7 @@ class ProjectHook < WebHook
scope :confidential_issue_hooks, -> { where(confidential_issues_events: true) }
scope :note_hooks, -> { where(note_events: true) }
scope :merge_request_hooks, -> { where(merge_requests_events: true) }
- scope :build_hooks, -> { where(build_events: true) }
+ scope :job_hooks, -> { where(job_events: true) }
scope :pipeline_hooks, -> { where(pipeline_events: true) }
scope :wiki_page_hooks, -> { where(wiki_page_events: true) }
end
diff --git a/app/models/hooks/system_hook.rb b/app/models/hooks/system_hook.rb
index 777bad1e724..c645805c6da 100644
--- a/app/models/hooks/system_hook.rb
+++ b/app/models/hooks/system_hook.rb
@@ -1,4 +1,9 @@
class SystemHook < WebHook
+ scope :repository_update_hooks, -> { where(repository_update_events: true) }
+
+ default_value_for :push_events, false
+ default_value_for :repository_update_events, true
+
def async_execute(data, hook_name)
Sidekiq::Client.enqueue(SystemHookWorker, id, data, hook_name)
end
diff --git a/app/models/hooks/web_hook.rb b/app/models/hooks/web_hook.rb
index 595602e80fe..a165fdc312f 100644
--- a/app/models/hooks/web_hook.rb
+++ b/app/models/hooks/web_hook.rb
@@ -8,8 +8,9 @@ class WebHook < ActiveRecord::Base
default_value_for :note_events, false
default_value_for :merge_requests_events, false
default_value_for :tag_push_events, false
- default_value_for :build_events, false
+ default_value_for :job_events, false
default_value_for :pipeline_events, false
+ default_value_for :repository_update_events, false
default_value_for :enable_ssl_verification, true
scope :push_hooks, -> { where(push_events: true) }
@@ -31,7 +32,7 @@ class WebHook < ActiveRecord::Base
post_url = url.gsub("#{parsed_url.userinfo}@", '')
auth = {
username: CGI.unescape(parsed_url.user),
- password: CGI.unescape(parsed_url.password),
+ password: CGI.unescape(parsed_url.password)
}
response = WebHook.post(post_url,
body: data.to_json,
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 27e3ed9bc7f..a88dbb3e065 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -174,7 +174,7 @@ class Issue < ActiveRecord::Base
# Returns boolean if a related branch exists for the current issue
# ignores merge requests branchs
- def has_related_branch?
+ def has_related_branch?
project.repository.branch_names.any? do |branch|
/\A#{iid}-(?!\d+-stable)/i =~ branch
end
@@ -292,7 +292,7 @@ class Issue < ActiveRecord::Base
end
def expire_etag_cache
- key = Gitlab::Routing.url_helpers.rendered_title_namespace_project_issue_path(
+ key = Gitlab::Routing.url_helpers.realtime_changes_namespace_project_issue_path(
project.namespace,
project,
self
diff --git a/app/models/issue_assignee.rb b/app/models/issue_assignee.rb
index 0663d3aaef8..06d760b6a89 100644
--- a/app/models/issue_assignee.rb
+++ b/app/models/issue_assignee.rb
@@ -3,11 +3,4 @@ class IssueAssignee < ActiveRecord::Base
belongs_to :issue
belongs_to :assignee, class_name: "User", foreign_key: :user_id
-
- after_create :update_assignee_cache_counts
- after_destroy :update_assignee_cache_counts
-
- def update_assignee_cache_counts
- assignee&.update_cache_counts
- end
end
diff --git a/app/models/key.rb b/app/models/key.rb
index 9c74ca84753..b7956052c3f 100644
--- a/app/models/key.rb
+++ b/app/models/key.rb
@@ -74,7 +74,7 @@ class Key < ActiveRecord::Base
GitlabShellWorker.perform_async(
:remove_key,
shell_id,
- key,
+ key
)
end
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 59736f70f24..9be00880438 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -125,7 +125,6 @@ class MergeRequest < ActiveRecord::Base
participant :assignee
after_save :keep_around_commit
- after_save :update_assignee_cache_counts, if: :assignee_id_changed?
def self.reference_prefix
'!'
@@ -187,13 +186,6 @@ class MergeRequest < ActiveRecord::Base
work_in_progress?(title) ? title : "WIP: #{title}"
end
- def update_assignee_cache_counts
- # make sure we flush the cache for both the old *and* new assignees(if they exist)
- previous_assignee = User.find_by_id(assignee_id_was) if assignee_id_was
- previous_assignee&.update_cache_counts
- assignee&.update_cache_counts
- end
-
# Returns a Hash of attributes to be used for Twitter card metadata
def card_attributes
{
@@ -301,6 +293,8 @@ class MergeRequest < ActiveRecord::Base
attr_writer :target_branch_sha, :source_branch_sha
def source_branch_head
+ return unless source_project
+
source_branch_ref = @source_branch_sha || source_branch
source_project.repository.commit(source_branch_ref) if source_branch_ref
end
@@ -899,34 +893,6 @@ class MergeRequest < ActiveRecord::Base
project.repository.keep_around(self.merge_commit_sha)
end
- def conflicts
- @conflicts ||= Gitlab::Conflict::FileCollection.new(self)
- end
-
- def conflicts_can_be_resolved_by?(user)
- return false unless source_project
-
- access = ::Gitlab::UserAccess.new(user, project: source_project)
- access.can_push_to_branch?(source_branch)
- end
-
- def conflicts_can_be_resolved_in_ui?
- return @conflicts_can_be_resolved_in_ui if defined?(@conflicts_can_be_resolved_in_ui)
-
- return @conflicts_can_be_resolved_in_ui = false unless cannot_be_merged?
- return @conflicts_can_be_resolved_in_ui = false unless has_complete_diff_refs?
-
- begin
- # Try to parse each conflict. If the MR's mergeable status hasn't been updated,
- # ensure that we don't say there are conflicts to resolve when there are no conflict
- # files.
- conflicts.files.each(&:lines)
- @conflicts_can_be_resolved_in_ui = conflicts.files.length > 0
- rescue Rugged::OdbError, Gitlab::Conflict::Parser::UnresolvableError, Gitlab::Conflict::FileCollection::ConflictSideMissing
- @conflicts_can_be_resolved_in_ui = false
- end
- end
-
def has_commits?
merge_request_diff && commits_count > 0
end
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 397dc7a25ab..4d59267f71d 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -46,7 +46,7 @@ class Namespace < ActiveRecord::Base
before_destroy(prepend: true) { prepare_for_destroy }
after_destroy :rm_dir
- scope :root, -> { where('type IS NULL') }
+ scope :for_user, -> { where('type IS NULL') }
scope :with_statistics, -> do
joins('LEFT JOIN project_statistics ps ON ps.namespace_id = namespaces.id')
@@ -56,7 +56,7 @@ class Namespace < ActiveRecord::Base
'COALESCE(SUM(ps.storage_size), 0) AS storage_size',
'COALESCE(SUM(ps.repository_size), 0) AS repository_size',
'COALESCE(SUM(ps.lfs_objects_size), 0) AS lfs_objects_size',
- 'COALESCE(SUM(ps.build_artifacts_size), 0) AS build_artifacts_size',
+ 'COALESCE(SUM(ps.build_artifacts_size), 0) AS build_artifacts_size'
)
end
diff --git a/app/models/project.rb b/app/models/project.rb
index 1f550cc02e2..65745fd6d37 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -175,7 +175,7 @@ class Project < ActiveRecord::Base
has_many :builds, class_name: 'Ci::Build' # the builds are created from the commit_statuses
has_many :runner_projects, dependent: :destroy, class_name: 'Ci::RunnerProject'
has_many :runners, through: :runner_projects, source: :runner, class_name: 'Ci::Runner'
- has_many :variables, dependent: :destroy, class_name: 'Ci::Variable'
+ has_many :variables, class_name: 'Ci::Variable'
has_many :triggers, dependent: :destroy, class_name: 'Ci::Trigger'
has_many :environments, dependent: :destroy
has_many :deployments, dependent: :destroy
@@ -967,7 +967,7 @@ class Project < ActiveRecord::Base
namespace: namespace.name,
visibility_level: visibility_level,
path_with_namespace: path_with_namespace,
- default_branch: default_branch,
+ default_branch: default_branch
}
# Backward compatibility
diff --git a/app/models/project_services/bamboo_service.rb b/app/models/project_services/bamboo_service.rb
index 400020ee04a..3f5b3eb159b 100644
--- a/app/models/project_services/bamboo_service.rb
+++ b/app/models/project_services/bamboo_service.rb
@@ -52,7 +52,7 @@ class BambooService < CiService
placeholder: 'Bamboo build plan key like KEY' },
{ type: 'text', name: 'username',
placeholder: 'A user with API access, if applicable' },
- { type: 'password', name: 'password' },
+ { type: 'password', name: 'password' }
]
end
diff --git a/app/models/project_services/chat_message/pipeline_message.rb b/app/models/project_services/chat_message/pipeline_message.rb
index 47b68f00cff..3edc395033c 100644
--- a/app/models/project_services/chat_message/pipeline_message.rb
+++ b/app/models/project_services/chat_message/pipeline_message.rb
@@ -35,7 +35,7 @@ module ChatMessage
def activity
{
- title: "Pipeline #{pipeline_link} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status}",
+ title: "Pipeline #{pipeline_link} of #{ref_type} #{branch_link} by #{user_name} #{humanized_status}",
subtitle: "in #{project_link}",
text: "in #{pretty_duration(duration)}",
image: user_avatar || ''
@@ -45,7 +45,7 @@ module ChatMessage
private
def message
- "#{project_link}: Pipeline #{pipeline_link} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status} in #{pretty_duration(duration)}"
+ "#{project_link}: Pipeline #{pipeline_link} of #{ref_type} #{branch_link} by #{user_name} #{humanized_status} in #{pretty_duration(duration)}"
end
def humanized_status
@@ -70,7 +70,7 @@ module ChatMessage
end
def branch_link
- "[#{ref}](#{branch_url})"
+ "`[#{ref}](#{branch_url})`"
end
def project_link
diff --git a/app/models/project_services/chat_message/push_message.rb b/app/models/project_services/chat_message/push_message.rb
index c52dd6ef8ef..04a59d559ca 100644
--- a/app/models/project_services/chat_message/push_message.rb
+++ b/app/models/project_services/chat_message/push_message.rb
@@ -61,7 +61,7 @@ module ChatMessage
end
def removed_branch_message
- "#{user_name} removed #{ref_type} #{ref} from #{project_link}"
+ "#{user_name} removed #{ref_type} `#{ref}` from #{project_link}"
end
def push_message
@@ -102,7 +102,7 @@ module ChatMessage
end
def branch_link
- "[#{ref}](#{branch_url})"
+ "`[#{ref}](#{branch_url})`"
end
def project_link
diff --git a/app/models/project_services/chat_notification_service.rb b/app/models/project_services/chat_notification_service.rb
index 6464bf3f4a4..779ef54cfcb 100644
--- a/app/models/project_services/chat_notification_service.rb
+++ b/app/models/project_services/chat_notification_service.rb
@@ -39,7 +39,7 @@ class ChatNotificationService < Service
{ type: 'text', name: 'webhook', placeholder: "e.g. #{webhook_placeholder}" },
{ type: 'text', name: 'username', placeholder: 'e.g. GitLab' },
{ type: 'checkbox', name: 'notify_only_broken_pipelines' },
- { type: 'checkbox', name: 'notify_only_default_branch' },
+ { type: 'checkbox', name: 'notify_only_default_branch' }
]
end
diff --git a/app/models/project_services/emails_on_push_service.rb b/app/models/project_services/emails_on_push_service.rb
index f4f913ee0b6..1a236e232f9 100644
--- a/app/models/project_services/emails_on_push_service.rb
+++ b/app/models/project_services/emails_on_push_service.rb
@@ -47,7 +47,7 @@ class EmailsOnPushService < Service
help: "Send notifications from the committer's email address if the domain is part of the domain GitLab is running on (e.g. #{domains})." },
{ type: 'checkbox', name: 'disable_diffs', title: "Disable code diffs",
help: "Don't include possibly sensitive code diffs in notification body." },
- { type: 'textarea', name: 'recipients', placeholder: 'Emails separated by whitespace' },
+ { type: 'textarea', name: 'recipients', placeholder: 'Emails separated by whitespace' }
]
end
end
diff --git a/app/models/project_services/external_wiki_service.rb b/app/models/project_services/external_wiki_service.rb
index bdf6fa6a586..b4d7c977ce4 100644
--- a/app/models/project_services/external_wiki_service.rb
+++ b/app/models/project_services/external_wiki_service.rb
@@ -19,7 +19,7 @@ class ExternalWikiService < Service
def fields
[
- { type: 'text', name: 'external_wiki_url', placeholder: 'The URL of the external Wiki' },
+ { type: 'text', name: 'external_wiki_url', placeholder: 'The URL of the external Wiki' }
]
end
diff --git a/app/models/project_services/flowdock_service.rb b/app/models/project_services/flowdock_service.rb
index 10a13c3fbdc..2a05d757eb4 100644
--- a/app/models/project_services/flowdock_service.rb
+++ b/app/models/project_services/flowdock_service.rb
@@ -37,7 +37,7 @@ class FlowdockService < Service
repo: project.repository.path_to_repo,
repo_url: "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}",
commit_url: "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/commit/%s",
- diff_url: "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/compare/%s...%s",
+ diff_url: "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/compare/%s...%s"
)
end
end
diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb
index 8b181221bb0..c19fed339ba 100644
--- a/app/models/project_services/hipchat_service.rb
+++ b/app/models/project_services/hipchat_service.rb
@@ -41,7 +41,7 @@ class HipchatService < Service
placeholder: 'Leave blank for default (v2)' },
{ type: 'text', name: 'server',
placeholder: 'Leave blank for default. https://hipchat.example.com' },
- { type: 'checkbox', name: 'notify_only_broken_pipelines' },
+ { type: 'checkbox', name: 'notify_only_broken_pipelines' }
]
end
diff --git a/app/models/project_services/irker_service.rb b/app/models/project_services/irker_service.rb
index c62bb4fa120..a51d43adcb9 100644
--- a/app/models/project_services/irker_service.rb
+++ b/app/models/project_services/irker_service.rb
@@ -58,7 +58,7 @@ class IrkerService < Service
' want to use a password, you have to omit the "#" on the channel). If you ' \
' specify a default IRC URI to prepend before each recipient, you can just ' \
' give a channel name.' },
- { type: 'checkbox', name: 'colorize_messages' },
+ { type: 'checkbox', name: 'colorize_messages' }
]
end
diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb
index 97e997d3899..f388773efee 100644
--- a/app/models/project_services/jira_service.rb
+++ b/app/models/project_services/jira_service.rb
@@ -149,7 +149,7 @@ class JiraService < IssueTrackerService
data = {
user: {
name: author.name,
- url: resource_url(user_path(author)),
+ url: resource_url(user_path(author))
},
project: {
name: self.project.path_with_namespace,
diff --git a/app/models/project_services/kubernetes_service.rb b/app/models/project_services/kubernetes_service.rb
index 9c56518c991..b2494a0be6e 100644
--- a/app/models/project_services/kubernetes_service.rb
+++ b/app/models/project_services/kubernetes_service.rb
@@ -73,7 +73,7 @@ class KubernetesService < DeploymentService
{ type: 'textarea',
name: 'ca_pem',
title: 'Custom CA bundle',
- placeholder: 'Certificate Authority bundle (PEM format)' },
+ placeholder: 'Certificate Authority bundle (PEM format)' }
]
end
diff --git a/app/models/project_services/microsoft_teams_service.rb b/app/models/project_services/microsoft_teams_service.rb
index 9b218fd81b4..2facff53e26 100644
--- a/app/models/project_services/microsoft_teams_service.rb
+++ b/app/models/project_services/microsoft_teams_service.rb
@@ -35,7 +35,7 @@ class MicrosoftTeamsService < ChatNotificationService
[
{ type: 'text', name: 'webhook', placeholder: "e.g. #{webhook_placeholder}" },
{ type: 'checkbox', name: 'notify_only_broken_pipelines' },
- { type: 'checkbox', name: 'notify_only_default_branch' },
+ { type: 'checkbox', name: 'notify_only_default_branch' }
]
end
diff --git a/app/models/project_services/mock_ci_service.rb b/app/models/project_services/mock_ci_service.rb
index a8d581a1f67..546b6e0a498 100644
--- a/app/models/project_services/mock_ci_service.rb
+++ b/app/models/project_services/mock_ci_service.rb
@@ -21,7 +21,7 @@ class MockCiService < CiService
[
{ type: 'text',
name: 'mock_service_url',
- placeholder: 'http://localhost:4004' },
+ placeholder: 'http://localhost:4004' }
]
end
diff --git a/app/models/project_services/monitoring_service.rb b/app/models/project_services/monitoring_service.rb
index 59776552540..ee9cd78327a 100644
--- a/app/models/project_services/monitoring_service.rb
+++ b/app/models/project_services/monitoring_service.rb
@@ -9,8 +9,11 @@ class MonitoringService < Service
%w()
end
- # Environments have a number of metrics
- def metrics(environment, timeframe_start: nil, timeframe_end: nil)
+ def environment_metrics(environment)
+ raise NotImplementedError
+ end
+
+ def deployment_metrics(deployment)
raise NotImplementedError
end
end
diff --git a/app/models/project_services/pipelines_email_service.rb b/app/models/project_services/pipelines_email_service.rb
index ac617f409d9..f824171ad09 100644
--- a/app/models/project_services/pipelines_email_service.rb
+++ b/app/models/project_services/pipelines_email_service.rb
@@ -55,7 +55,7 @@ class PipelinesEmailService < Service
name: 'recipients',
placeholder: 'Emails separated by comma' },
{ type: 'checkbox',
- name: 'notify_only_broken_pipelines' },
+ name: 'notify_only_broken_pipelines' }
]
end
diff --git a/app/models/project_services/prometheus_service.rb b/app/models/project_services/prometheus_service.rb
index 6a4479c4dbc..ec72cb6856d 100644
--- a/app/models/project_services/prometheus_service.rb
+++ b/app/models/project_services/prometheus_service.rb
@@ -63,45 +63,31 @@ class PrometheusService < MonitoringService
{ success: false, result: err }
end
- def metrics(environment, timeframe_start: nil, timeframe_end: nil)
- with_reactive_cache(environment.slug, timeframe_start, timeframe_end) do |data|
- data
- end
+ def environment_metrics(environment)
+ with_reactive_cache(Gitlab::Prometheus::Queries::EnvironmentQuery.name, environment.id, &:itself)
+ end
+
+ def deployment_metrics(deployment)
+ metrics = with_reactive_cache(Gitlab::Prometheus::Queries::DeploymentQuery.name, deployment.id, &:itself)
+ metrics&.merge(deployment_time: created_at.to_i) || {}
end
# Cache metrics for specific environment
- def calculate_reactive_cache(environment_slug, timeframe_start, timeframe_end)
+ def calculate_reactive_cache(query_class_name, *args)
return unless active? && project && !project.pending_delete?
- timeframe_start = Time.parse(timeframe_start) if timeframe_start
- timeframe_end = Time.parse(timeframe_end) if timeframe_end
-
- timeframe_start ||= 8.hours.ago
- timeframe_end ||= Time.now
-
- memory_query = %{(sum(container_memory_usage_bytes{container_name!="POD",environment="#{environment_slug}"}) / count(container_memory_usage_bytes{container_name!="POD",environment="#{environment_slug}"})) /1024/1024}
- cpu_query = %{sum(rate(container_cpu_usage_seconds_total{container_name!="POD",environment="#{environment_slug}"}[2m])) / count(container_cpu_usage_seconds_total{container_name!="POD",environment="#{environment_slug}"}) * 100}
+ metrics = Kernel.const_get(query_class_name).new(client).query(*args)
{
success: true,
- metrics: {
- # Average Memory used in MB
- memory_values: client.query_range(memory_query, start: timeframe_start, stop: timeframe_end),
- memory_current: client.query(memory_query, time: timeframe_end),
- memory_previous: client.query(memory_query, time: timeframe_start),
- # Average CPU Utilization
- cpu_values: client.query_range(cpu_query, start: timeframe_start, stop: timeframe_end),
- cpu_current: client.query(cpu_query, time: timeframe_end),
- cpu_previous: client.query(cpu_query, time: timeframe_start)
- },
+ metrics: metrics,
last_update: Time.now.utc
}
-
rescue Gitlab::PrometheusError => err
{ success: false, result: err.message }
end
def client
- @prometheus ||= Gitlab::Prometheus.new(api_url: api_url)
+ @prometheus ||= Gitlab::PrometheusClient.new(api_url: api_url)
end
end
diff --git a/app/models/project_services/pushover_service.rb b/app/models/project_services/pushover_service.rb
index 3e618a8dbf1..fc29a5277bb 100644
--- a/app/models/project_services/pushover_service.rb
+++ b/app/models/project_services/pushover_service.rb
@@ -55,7 +55,7 @@ class PushoverService < Service
['Pushover Echo (long)', 'echo'],
['Up Down (long)', 'updown'],
['None (silent)', 'none']
- ] },
+ ] }
]
end
diff --git a/app/models/project_services/teamcity_service.rb b/app/models/project_services/teamcity_service.rb
index cbaffb8ce48..b16beb406b9 100644
--- a/app/models/project_services/teamcity_service.rb
+++ b/app/models/project_services/teamcity_service.rb
@@ -55,7 +55,7 @@ class TeamcityService < CiService
placeholder: 'Build configuration ID' },
{ type: 'text', name: 'username',
placeholder: 'A user with permissions to trigger a manual build' },
- { type: 'password', name: 'password' },
+ { type: 'password', name: 'password' }
]
end
@@ -78,7 +78,7 @@ class TeamcityService < CiService
auth = {
username: username,
- password: password,
+ password: password
}
branch = Gitlab::Git.ref_name(data[:ref])
diff --git a/app/models/protected_branch/merge_access_level.rb b/app/models/protected_branch/merge_access_level.rb
index 771e3376613..e8d35ac326f 100644
--- a/app/models/protected_branch/merge_access_level.rb
+++ b/app/models/protected_branch/merge_access_level.rb
@@ -1,13 +1,3 @@
class ProtectedBranch::MergeAccessLevel < ActiveRecord::Base
include ProtectedBranchAccess
-
- validates :access_level, presence: true, inclusion: { in: [Gitlab::Access::MASTER,
- Gitlab::Access::DEVELOPER] }
-
- def self.human_access_levels
- {
- Gitlab::Access::MASTER => "Masters",
- Gitlab::Access::DEVELOPER => "Developers + Masters"
- }.with_indifferent_access
- end
end
diff --git a/app/models/protected_branch/push_access_level.rb b/app/models/protected_branch/push_access_level.rb
index 14610cb42b7..7a2e9e5ec5d 100644
--- a/app/models/protected_branch/push_access_level.rb
+++ b/app/models/protected_branch/push_access_level.rb
@@ -1,21 +1,3 @@
class ProtectedBranch::PushAccessLevel < ActiveRecord::Base
include ProtectedBranchAccess
-
- validates :access_level, presence: true, inclusion: { in: [Gitlab::Access::MASTER,
- Gitlab::Access::DEVELOPER,
- Gitlab::Access::NO_ACCESS] }
-
- def self.human_access_levels
- {
- Gitlab::Access::MASTER => "Masters",
- Gitlab::Access::DEVELOPER => "Developers + Masters",
- Gitlab::Access::NO_ACCESS => "No one"
- }.with_indifferent_access
- end
-
- def check_access(user)
- return false if access_level == Gitlab::Access::NO_ACCESS
-
- super
- end
end
diff --git a/app/models/readme_blob.rb b/app/models/readme_blob.rb
new file mode 100644
index 00000000000..1863a08f1de
--- /dev/null
+++ b/app/models/readme_blob.rb
@@ -0,0 +1,13 @@
+class ReadmeBlob < SimpleDelegator
+ attr_reader :repository
+
+ def initialize(blob, repository)
+ @repository = repository
+
+ super(blob)
+ end
+
+ def rendered_markup
+ repository.rendered_readme
+ end
+end
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 0c797dd5814..07e0b3bae4f 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -30,7 +30,7 @@ class Repository
METHOD_CACHES_FOR_FILE_TYPES = {
readme: :rendered_readme,
changelog: :changelog,
- license: %i(license_blob license_key),
+ license: %i(license_blob license_key license),
contributing: :contribution_guide,
gitignore: :gitignore,
koding: :koding_yml,
@@ -42,13 +42,13 @@ class Repository
# variable.
#
# This only works for methods that do not take any arguments.
- def self.cache_method(name, fallback: nil)
+ def self.cache_method(name, fallback: nil, memoize_only: false)
original = :"_uncached_#{name}"
alias_method(original, name)
define_method(name) do
- cache_method_output(name, fallback: fallback) { __send__(original) }
+ cache_method_output(name, fallback: fallback, memoize_only: memoize_only) { __send__(original) }
end
end
@@ -517,8 +517,8 @@ class Repository
cache_method :avatar
def readme
- if head = tree(:head)
- head.readme
+ if readme = tree(:head)&.readme
+ ReadmeBlob.new(readme, self)
end
end
@@ -549,6 +549,13 @@ class Repository
end
cache_method :license_key
+ def license
+ return unless license_key
+
+ Licensee::License.new(license_key)
+ end
+ cache_method :license, memoize_only: true
+
def gitignore
file_on_head(:gitignore)
end
@@ -642,22 +649,8 @@ class Repository
"#{name}-#{highest_branch_id + 1}"
end
- # Remove archives older than 2 hours
def branches_sorted_by(value)
- case value
- when 'name'
- branches.sort_by(&:name)
- when 'updated_desc'
- branches.sort do |a, b|
- commit(b.dereferenced_target).committed_date <=> commit(a.dereferenced_target).committed_date
- end
- when 'updated_asc'
- branches.sort do |a, b|
- commit(a.dereferenced_target).committed_date <=> commit(b.dereferenced_target).committed_date
- end
- else
- branches
- end
+ raw_repository.local_branches(sort_by: value)
end
def tags_sorted_by(value)
@@ -833,7 +826,7 @@ class Repository
actual_options = options.merge(
parents: [our_commit, their_commit],
- tree: merge_index.write_tree(rugged),
+ tree: merge_index.write_tree(rugged)
)
commit_id = create_commit(actual_options)
@@ -1061,14 +1054,20 @@ class Repository
#
# key - The name of the key to cache the data in.
# fallback - A value to fall back to in the event of a Git error.
- def cache_method_output(key, fallback: nil, &block)
+ def cache_method_output(key, fallback: nil, memoize_only: false, &block)
ivar = cache_instance_variable_name(key)
if instance_variable_defined?(ivar)
instance_variable_get(ivar)
else
begin
- instance_variable_set(ivar, cache.fetch(key, &block))
+ value =
+ if memoize_only
+ yield
+ else
+ cache.fetch(key, &block)
+ end
+ instance_variable_set(ivar, value)
rescue Rugged::ReferenceError, Gitlab::Git::Repository::NoRepository
# if e.g. HEAD or the entire repository doesn't exist we want to
# gracefully handle this and not cache anything.
@@ -1083,8 +1082,8 @@ class Repository
def file_on_head(type)
if head = tree(:head)
- head.blobs.find do |file|
- Gitlab::FileDetector.type_of(file.name) == type
+ head.blobs.find do |blob|
+ Gitlab::FileDetector.type_of(blob.path) == type
end
end
end
@@ -1150,8 +1149,6 @@ class Repository
@project.repository_storage_path
end
- delegate :gitaly_channel, :gitaly_repository, to: :raw_repository
-
def initialize_raw_repository
Gitlab::Git::Repository.new(project.repository_storage, path_with_namespace + '.git')
end
diff --git a/app/models/route.rb b/app/models/route.rb
index 12a7fa3d01b..be77b8b51a5 100644
--- a/app/models/route.rb
+++ b/app/models/route.rb
@@ -35,7 +35,7 @@ class Route < ActiveRecord::Base
old_path = route.path
# Callbacks must be run manually
- route.update_columns(attributes)
+ route.update_columns(attributes.merge(updated_at: Time.now))
# We are not calling route.delete_conflicting_redirects here, in hopes
# of avoiding deadlocks. The parent (self, in this method) already
diff --git a/app/models/sent_notification.rb b/app/models/sent_notification.rb
index bfaf0eb2fae..0ae5864615a 100644
--- a/app/models/sent_notification.rb
+++ b/app/models/sent_notification.rb
@@ -39,7 +39,7 @@ class SentNotification < ActiveRecord::Base
noteable_type: noteable.class.name,
noteable_id: noteable_id,
- commit_id: commit_id,
+ commit_id: commit_id
)
create(attrs)
diff --git a/app/models/service.rb b/app/models/service.rb
index c71a7d169ec..8916f88076e 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -12,7 +12,7 @@ class Service < ActiveRecord::Base
default_value_for :merge_requests_events, true
default_value_for :tag_push_events, true
default_value_for :note_events, true
- default_value_for :build_events, true
+ default_value_for :job_events, true
default_value_for :pipeline_events, true
default_value_for :wiki_page_events, true
@@ -40,7 +40,7 @@ class Service < ActiveRecord::Base
scope :confidential_issue_hooks, -> { where(confidential_issues_events: true, active: true) }
scope :merge_request_hooks, -> { where(merge_requests_events: true, active: true) }
scope :note_hooks, -> { where(note_events: true, active: true) }
- scope :build_hooks, -> { where(build_events: true, active: true) }
+ scope :job_hooks, -> { where(job_events: true, active: true) }
scope :pipeline_hooks, -> { where(pipeline_events: true, active: true) }
scope :wiki_page_hooks, -> { where(wiki_page_events: true, active: true) }
scope :external_issue_trackers, -> { issue_trackers.active.without_defaults }
diff --git a/app/models/snippet.rb b/app/models/snippet.rb
index abfbefdf9a0..882e2fa0594 100644
--- a/app/models/snippet.rb
+++ b/app/models/snippet.rb
@@ -152,18 +152,5 @@ class Snippet < ActiveRecord::Base
where(table[:content].matches(pattern))
end
-
- def accessible_to(user)
- return are_public unless user.present?
- return all if user.admin?
-
- where(
- 'visibility_level IN (:visibility_levels)
- OR author_id = :author_id
- OR project_id IN (:project_ids)',
- visibility_levels: [Snippet::PUBLIC, Snippet::INTERNAL],
- author_id: user.id,
- project_ids: user.authorized_projects.select(:id))
- end
end
end
diff --git a/app/models/tree.rb b/app/models/tree.rb
index fe148b0ec65..c89b8eca9be 100644
--- a/app/models/tree.rb
+++ b/app/models/tree.rb
@@ -40,10 +40,7 @@ class Tree
readme_path = path == '/' ? readme_tree.name : File.join(path, readme_tree.name)
- git_repo = repository.raw_repository
- @readme = Gitlab::Git::Blob.find(git_repo, sha, readme_path)
- @readme.load_all_data!(git_repo)
- @readme
+ @readme = repository.blob_at(sha, readme_path)
end
def trees
diff --git a/app/models/user.rb b/app/models/user.rb
index f713a20233c..837ab78228b 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -166,8 +166,13 @@ class User < ActiveRecord::Base
enum dashboard: [:projects, :stars, :project_activity, :starred_project_activity, :groups, :todos]
# User's Project preference
- # Note: When adding an option, it MUST go on the end of the array.
- enum project_view: [:readme, :activity, :files]
+ #
+ # Note: When adding an option, it MUST go on the end of the hash with a
+ # number higher than the current max. We cannot move options and/or change
+ # their numbers.
+ #
+ # We skip 0 because this was used by an option that has since been removed.
+ enum project_view: { activity: 1, files: 2 }
alias_attribute :private_token, :authentication_token
@@ -350,7 +355,7 @@ class User < ActiveRecord::Base
end
def find_by_full_path(path, follow_redirects: false)
- namespace = Namespace.find_by_full_path(path, follow_redirects: follow_redirects)
+ namespace = Namespace.for_user.find_by_full_path(path, follow_redirects: follow_redirects)
namespace&.owner
end
@@ -929,6 +934,19 @@ class User < ActiveRecord::Base
assigned_open_issues_count(force: true)
end
+ def invalidate_cache_counts
+ invalidate_issue_cache_counts
+ invalidate_merge_request_cache_counts
+ end
+
+ def invalidate_issue_cache_counts
+ Rails.cache.delete(['users', id, 'assigned_open_issues_count'])
+ end
+
+ def invalidate_merge_request_cache_counts
+ Rails.cache.delete(['users', id, 'assigned_open_merge_requests_count'])
+ end
+
def todos_done_count(force: false)
Rails.cache.fetch(['users', id, 'todos_done_count'], force: force) do
TodosFinder.new(self, state: :done).execute.count
diff --git a/app/policies/project_snippet_policy.rb b/app/policies/project_snippet_policy.rb
index 3a96836917e..cf8ff92617f 100644
--- a/app/policies/project_snippet_policy.rb
+++ b/app/policies/project_snippet_policy.rb
@@ -13,7 +13,7 @@ class ProjectSnippetPolicy < BasePolicy
can! :read_project_snippet
end
- if @subject.private? && @subject.project.team.member?(@user)
+ if @subject.project.team.member?(@user)
can! :read_project_snippet
end
end
diff --git a/app/presenters/merge_request_presenter.rb b/app/presenters/merge_request_presenter.rb
index 255f63db5c2..0db9e31031c 100644
--- a/app/presenters/merge_request_presenter.rb
+++ b/app/presenters/merge_request_presenter.rb
@@ -76,7 +76,7 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
end
def conflict_resolution_path
- if conflicts_can_be_resolved_in_ui? && conflicts_can_be_resolved_by?(current_user)
+ if conflicts.can_be_resolved_in_ui? && conflicts.can_be_resolved_by?(current_user)
conflicts_namespace_project_merge_request_path(project.namespace, project, merge_request)
end
end
@@ -141,6 +141,10 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
private
+ def conflicts
+ @conflicts ||= MergeRequests::Conflicts::ListService.new(merge_request)
+ end
+
def closing_issues
@closing_issues ||= closes_issues(current_user)
end
diff --git a/app/serializers/merge_request_entity.rb b/app/serializers/merge_request_entity.rb
index a2542c54f7a..a49f4d834cd 100644
--- a/app/serializers/merge_request_entity.rb
+++ b/app/serializers/merge_request_entity.rb
@@ -97,6 +97,14 @@ class MergeRequestEntity < IssuableEntity
presenter(merge_request).target_branch_commits_path
end
+ expose :new_blob_path do |merge_request|
+ if can?(current_user, :push_code, merge_request.project)
+ namespace_project_new_blob_path(merge_request.project.namespace,
+ merge_request.project,
+ merge_request.source_branch)
+ end
+ end
+
expose :conflict_resolution_path do |merge_request|
presenter(merge_request).conflict_resolution_path
end
diff --git a/app/serializers/pipeline_entity.rb b/app/serializers/pipeline_entity.rb
index 51ad0a3f8ba..ea57cc97a7e 100644
--- a/app/serializers/pipeline_entity.rb
+++ b/app/serializers/pipeline_entity.rb
@@ -38,10 +38,7 @@ class PipelineEntity < Grape::Entity
expose :path do |pipeline|
if pipeline.ref
- namespace_project_tree_path(
- pipeline.project.namespace,
- pipeline.project,
- id: pipeline.ref)
+ project_ref_path(pipeline.project, pipeline.ref)
end
end
diff --git a/app/serializers/request_aware_entity.rb b/app/serializers/request_aware_entity.rb
index 3039014aaaa..d53fcfb8c1b 100644
--- a/app/serializers/request_aware_entity.rb
+++ b/app/serializers/request_aware_entity.rb
@@ -3,6 +3,7 @@ module RequestAwareEntity
included do
include Gitlab::Routing
+ include GitlabRoutingHelper
include Gitlab::Allowable
end
diff --git a/app/services/akismet_service.rb b/app/services/akismet_service.rb
index 76b9f1feda7..8e11a2a36a7 100644
--- a/app/services/akismet_service.rb
+++ b/app/services/akismet_service.rb
@@ -16,7 +16,7 @@ class AkismetService
created_at: DateTime.now,
author: owner.name,
author_email: owner.email,
- referrer: options[:referrer],
+ referrer: options[:referrer]
}
begin
diff --git a/app/services/audit_event_service.rb b/app/services/audit_event_service.rb
index 8a000585e89..5ad9a50687c 100644
--- a/app/services/audit_event_service.rb
+++ b/app/services/audit_event_service.rb
@@ -8,7 +8,7 @@ class AuditEventService
with: @details[:with],
target_id: @author.id,
target_type: 'User',
- target_details: @author.name,
+ target_details: @author.name
}
self
diff --git a/app/services/boards/issues/move_service.rb b/app/services/boards/issues/move_service.rb
index e73b1a4361a..ecabb2a48e4 100644
--- a/app/services/boards/issues/move_service.rb
+++ b/app/services/boards/issues/move_service.rb
@@ -38,7 +38,7 @@ module Boards
attrs.merge!(
add_label_ids: add_label_ids,
remove_label_ids: remove_label_ids,
- state_event: issue_state,
+ state_event: issue_state
)
end
diff --git a/app/services/ci/process_pipeline_service.rb b/app/services/ci/process_pipeline_service.rb
index 25ba54ffa0d..55af193d717 100644
--- a/app/services/ci/process_pipeline_service.rb
+++ b/app/services/ci/process_pipeline_service.rb
@@ -5,6 +5,8 @@ module Ci
def execute(pipeline)
@pipeline = pipeline
+ update_retried
+
new_builds =
stage_indexes_of_created_builds.map do |index|
process_stage(index)
@@ -71,5 +73,23 @@ module Ci
def created_builds
pipeline.builds.created
end
+
+ # This method is for compatibility and data consistency and should be removed with 9.3 version of GitLab
+ # This replicates what is db/post_migrate/20170416103934_upate_retried_for_ci_build.rb
+ # and ensures that functionality will not be broken before migration is run
+ # this updates only when there are data that needs to be updated, there are two groups with no retried flag
+ def update_retried
+ # find the latest builds for each name
+ latest_statuses = pipeline.statuses.latest
+ .group(:name)
+ .having('count(*) > 1')
+ .pluck('max(id)', 'name')
+
+ # mark builds that are retried
+ pipeline.statuses.latest
+ .where(name: latest_statuses.map(&:second))
+ .where.not(id: latest_statuses.map(&:first))
+ .update_all(retried: true) if latest_statuses.any?
+ end
end
end
diff --git a/app/services/ci/retry_build_service.rb b/app/services/ci/retry_build_service.rb
index 89da05b72bb..f51e9fd1d54 100644
--- a/app/services/ci/retry_build_service.rb
+++ b/app/services/ci/retry_build_service.rb
@@ -6,7 +6,7 @@ module Ci
description tag_list].freeze
def execute(build)
- reprocess(build).tap do |new_build|
+ reprocess!(build).tap do |new_build|
build.pipeline.mark_as_processable_after_stage(build.stage_idx)
new_build.enqueue!
@@ -17,7 +17,7 @@ module Ci
end
end
- def reprocess(build)
+ def reprocess!(build)
unless can?(current_user, :update_build, build)
raise Gitlab::Access::AccessDeniedError
end
@@ -28,7 +28,14 @@ module Ci
attributes.push([:user, current_user])
- project.builds.create(Hash[attributes])
+ Ci::Build.transaction do
+ # mark all other builds of that name as retried
+ build.pipeline.builds.latest
+ .where(name: build.name)
+ .update_all(retried: true)
+
+ project.builds.create!(Hash[attributes])
+ end
end
end
end
diff --git a/app/services/ci/retry_pipeline_service.rb b/app/services/ci/retry_pipeline_service.rb
index 5b207157345..c5a43869990 100644
--- a/app/services/ci/retry_pipeline_service.rb
+++ b/app/services/ci/retry_pipeline_service.rb
@@ -11,7 +11,7 @@ module Ci
next unless can?(current_user, :update_build, build)
Ci::RetryBuildService.new(project, current_user)
- .reprocess(build)
+ .reprocess!(build)
end
pipeline.builds.latest.skipped.find_each do |skipped|
diff --git a/app/services/issuable/bulk_update_service.rb b/app/services/issuable/bulk_update_service.rb
index 40ff9b8b867..5d42a89fced 100644
--- a/app/services/issuable/bulk_update_service.rb
+++ b/app/services/issuable/bulk_update_service.rb
@@ -7,7 +7,7 @@ module Issuable
ids = params.delete(:issuable_ids).split(",")
items = model_class.where(id: ids)
- %i(state_event milestone_id assignee_id assignee_ids add_label_ids remove_label_ids subscription_event).each do |key|
+ permitted_attrs(type).each do |key|
params.delete(key) unless params[key].present?
end
@@ -26,5 +26,17 @@ module Issuable
success: !items.count.zero?
}
end
+
+ private
+
+ def permitted_attrs(type)
+ attrs = %i(state_event milestone_id assignee_id assignee_ids add_label_ids remove_label_ids subscription_event)
+
+ if type == 'issue'
+ attrs.push(:assignee_ids)
+ else
+ attrs.push(:assignee_id)
+ end
+ end
end
end
diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb
index c1e532b504a..e94ab3e64db 100644
--- a/app/services/issuable_base_service.rb
+++ b/app/services/issuable_base_service.rb
@@ -178,6 +178,7 @@ class IssuableBaseService < BaseService
after_create(issuable)
issuable.create_cross_references!(current_user)
execute_hooks(issuable)
+ invalidate_cache_counts(issuable.assignees, issuable)
end
issuable
@@ -234,6 +235,11 @@ class IssuableBaseService < BaseService
old_assignees: old_assignees
)
+ if old_assignees != issuable.assignees
+ assignees = old_assignees + issuable.assignees.to_a
+ invalidate_cache_counts(assignees.compact, issuable)
+ end
+
after_update(issuable)
issuable.create_new_cross_references!(current_user)
execute_hooks(issuable, 'update')
@@ -324,4 +330,10 @@ class IssuableBaseService < BaseService
create_labels_note(issuable, old_labels) if issuable.labels != old_labels
end
+
+ def invalidate_cache_counts(users, issuable)
+ users.each do |user|
+ user.public_send("invalidate_#{issuable.model_name.singular}_cache_counts")
+ end
+ end
end
diff --git a/app/services/members/authorized_destroy_service.rb b/app/services/members/authorized_destroy_service.rb
index a85b9465c84..f846d72498f 100644
--- a/app/services/members/authorized_destroy_service.rb
+++ b/app/services/members/authorized_destroy_service.rb
@@ -10,7 +10,7 @@ module Members
return false if member.is_a?(GroupMember) && member.source.last_owner?(member.user)
Member.transaction do
- unassign_issues_and_merge_requests(member)
+ unassign_issues_and_merge_requests(member) unless member.invite?
member.destroy
end
@@ -26,10 +26,14 @@ module Members
def unassign_issues_and_merge_requests(member)
if member.is_a?(GroupMember)
- issue_ids = IssuesFinder.new(user, group_id: member.source_id, assignee_id: member.user_id).
- execute.pluck(:id)
+ issues = Issue.unscoped.select(1).
+ joins(:project).
+ where('issues.id = issue_assignees.issue_id AND projects.namespace_id = ?', member.source_id)
- IssueAssignee.destroy_all(issue_id: issue_ids, user_id: member.user_id)
+ # DELETE FROM issue_assignees WHERE user_id = X AND EXISTS (...)
+ IssueAssignee.unscoped.
+ where('user_id = :user_id AND EXISTS (:sub)', user_id: member.user_id, sub: issues).
+ delete_all
MergeRequestsFinder.new(user, group_id: member.source_id, assignee_id: member.user_id).
execute.
@@ -37,14 +41,20 @@ module Members
else
project = member.source
- IssueAssignee.destroy_all(
- user_id: member.user_id,
- issue_id: project.issues.opened.assigned_to(member.user).select(:id)
- )
+ # SELECT 1 FROM issues WHERE issues.id = issue_assignees.issue_id AND issues.project_id = X
+ issues = Issue.unscoped.select(1).
+ where('issues.id = issue_assignees.issue_id').
+ where(project_id: project.id)
+
+ # DELETE FROM issue_assignees WHERE user_id = X AND EXISTS (...)
+ IssueAssignee.unscoped.
+ where('user_id = :user_id AND EXISTS (:sub)', user_id: member.user_id, sub: issues).
+ delete_all
project.merge_requests.opened.assigned_to(member.user).update_all(assignee_id: nil)
- member.user.update_cache_counts
end
+
+ member.user.invalidate_cache_counts
end
end
end
diff --git a/app/services/merge_requests/conflicts/base_service.rb b/app/services/merge_requests/conflicts/base_service.rb
new file mode 100644
index 00000000000..b50875347d9
--- /dev/null
+++ b/app/services/merge_requests/conflicts/base_service.rb
@@ -0,0 +1,11 @@
+module MergeRequests
+ module Conflicts
+ class BaseService
+ attr_reader :merge_request
+
+ def initialize(merge_request)
+ @merge_request = merge_request
+ end
+ end
+ end
+end
diff --git a/app/services/merge_requests/conflicts/list_service.rb b/app/services/merge_requests/conflicts/list_service.rb
new file mode 100644
index 00000000000..9835606812c
--- /dev/null
+++ b/app/services/merge_requests/conflicts/list_service.rb
@@ -0,0 +1,36 @@
+module MergeRequests
+ module Conflicts
+ class ListService < MergeRequests::Conflicts::BaseService
+ delegate :file_for_path, :to_json, to: :conflicts
+
+ def can_be_resolved_by?(user)
+ return false unless merge_request.source_project
+
+ access = ::Gitlab::UserAccess.new(user, project: merge_request.source_project)
+ access.can_push_to_branch?(merge_request.source_branch)
+ end
+
+ def can_be_resolved_in_ui?
+ return @conflicts_can_be_resolved_in_ui if defined?(@conflicts_can_be_resolved_in_ui)
+
+ return @conflicts_can_be_resolved_in_ui = false unless merge_request.cannot_be_merged?
+ return @conflicts_can_be_resolved_in_ui = false unless merge_request.has_complete_diff_refs?
+ return @conflicts_can_be_resolved_in_ui = false if merge_request.branch_missing?
+
+ begin
+ # Try to parse each conflict. If the MR's mergeable status hasn't been
+ # updated, ensure that we don't say there are conflicts to resolve
+ # when there are no conflict files.
+ conflicts.files.each(&:lines)
+ @conflicts_can_be_resolved_in_ui = conflicts.files.length > 0
+ rescue Rugged::OdbError, Gitlab::Conflict::Parser::UnresolvableError, Gitlab::Conflict::FileCollection::ConflictSideMissing
+ @conflicts_can_be_resolved_in_ui = false
+ end
+ end
+
+ def conflicts
+ @conflicts ||= Gitlab::Conflict::FileCollection.read_only(merge_request)
+ end
+ end
+ end
+end
diff --git a/app/services/merge_requests/conflicts/resolve_service.rb b/app/services/merge_requests/conflicts/resolve_service.rb
new file mode 100644
index 00000000000..d74a82effd6
--- /dev/null
+++ b/app/services/merge_requests/conflicts/resolve_service.rb
@@ -0,0 +1,53 @@
+module MergeRequests
+ module Conflicts
+ class ResolveService < MergeRequests::Conflicts::BaseService
+ MissingFiles = Class.new(Gitlab::Conflict::ResolutionError)
+
+ def execute(current_user, params)
+ rugged = merge_request.source_project.repository.rugged
+
+ Gitlab::Conflict::FileCollection.for_resolution(merge_request) do |conflicts_for_resolution|
+ merge_index = conflicts_for_resolution.merge_index
+
+ params[:files].each do |file_params|
+ conflict_file = conflicts_for_resolution.file_for_path(file_params[:old_path], file_params[:new_path])
+
+ write_resolved_file_to_index(merge_index, rugged, conflict_file, file_params)
+ end
+
+ unless merge_index.conflicts.empty?
+ missing_files = merge_index.conflicts.map { |file| file[:ours][:path] }
+
+ raise MissingFiles, "Missing resolutions for the following files: #{missing_files.join(', ')}"
+ end
+
+ commit_params = {
+ message: params[:commit_message] || conflicts_for_resolution.default_commit_message,
+ parents: [conflicts_for_resolution.our_commit, conflicts_for_resolution.their_commit].map(&:oid),
+ tree: merge_index.write_tree(rugged)
+ }
+
+ conflicts_for_resolution.
+ project.
+ repository.
+ resolve_conflicts(current_user, merge_request.source_branch, commit_params)
+ end
+ end
+
+ private
+
+ def write_resolved_file_to_index(merge_index, rugged, file, params)
+ new_file = if params[:sections]
+ file.resolve_lines(params[:sections]).map(&:text).join("\n")
+ elsif params[:content]
+ file.resolve_content(params[:content])
+ end
+
+ our_path = file.our_path
+
+ merge_index.add(path: our_path, oid: rugged.write(new_file, :blob), mode: file.our_mode)
+ merge_index.conflict_remove(our_path)
+ end
+ end
+ end
+end
diff --git a/app/services/merge_requests/resolve_service.rb b/app/services/merge_requests/resolve_service.rb
deleted file mode 100644
index 82cd89d9a0b..00000000000
--- a/app/services/merge_requests/resolve_service.rb
+++ /dev/null
@@ -1,65 +0,0 @@
-module MergeRequests
- class ResolveService < MergeRequests::BaseService
- MissingFiles = Class.new(Gitlab::Conflict::ResolutionError)
-
- attr_accessor :conflicts, :rugged, :merge_index, :merge_request
-
- def execute(merge_request)
- @conflicts = merge_request.conflicts
- @rugged = project.repository.rugged
- @merge_index = conflicts.merge_index
- @merge_request = merge_request
-
- fetch_their_commit!
-
- params[:files].each do |file_params|
- conflict_file = merge_request.conflicts.file_for_path(file_params[:old_path], file_params[:new_path])
-
- write_resolved_file_to_index(conflict_file, file_params)
- end
-
- unless merge_index.conflicts.empty?
- missing_files = merge_index.conflicts.map { |file| file[:ours][:path] }
-
- raise MissingFiles, "Missing resolutions for the following files: #{missing_files.join(', ')}"
- end
-
- commit_params = {
- message: params[:commit_message] || conflicts.default_commit_message,
- parents: [conflicts.our_commit, conflicts.their_commit].map(&:oid),
- tree: merge_index.write_tree(rugged)
- }
-
- project.repository.resolve_conflicts(current_user, merge_request.source_branch, commit_params)
- end
-
- def write_resolved_file_to_index(file, params)
- new_file = if params[:sections]
- file.resolve_lines(params[:sections]).map(&:text).join("\n")
- elsif params[:content]
- file.resolve_content(params[:content])
- end
-
- our_path = file.our_path
-
- merge_index.add(path: our_path, oid: rugged.write(new_file, :blob), mode: file.our_mode)
- merge_index.conflict_remove(our_path)
- end
-
- # If their commit (in the target project) doesn't exist in the source project, it
- # can't be a parent for the merge commit we're about to create. If that's the case,
- # fetch the target branch ref into the source project so the commit exists in both.
- #
- def fetch_their_commit!
- return if rugged.include?(conflicts.their_commit.oid)
-
- random_string = SecureRandom.hex
-
- project.repository.fetch_ref(
- merge_request.target_project.repository.path_to_repo,
- "refs/heads/#{merge_request.target_branch}",
- "refs/tmp/#{random_string}/head"
- )
- end
- end
-end
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index c65c66d7150..646ccbdb2bf 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -298,7 +298,7 @@ class NotificationService
recipients ||= NotificationRecipientService.new(pipeline.project).build_pipeline_recipients(
pipeline,
pipeline.user,
- action: pipeline.status,
+ action: pipeline.status
).map(&:notification_email)
if recipients.any?
diff --git a/app/services/projects/update_pages_configuration_service.rb b/app/services/projects/update_pages_configuration_service.rb
index eb4809afa85..cacb74b1205 100644
--- a/app/services/projects/update_pages_configuration_service.rb
+++ b/app/services/projects/update_pages_configuration_service.rb
@@ -27,7 +27,7 @@ module Projects
{
domain: domain.domain,
certificate: domain.certificate,
- key: domain.key,
+ key: domain.key
}
end
end
diff --git a/app/services/search/snippet_service.rb b/app/services/search/snippet_service.rb
index 4f161beea4d..85da0be6fff 100644
--- a/app/services/search/snippet_service.rb
+++ b/app/services/search/snippet_service.rb
@@ -7,7 +7,7 @@ module Search
end
def execute
- snippets = Snippet.accessible_to(current_user)
+ snippets = SnippetsFinder.new(current_user).execute
Gitlab::SnippetSearchResults.new(snippets, params[:search])
end
diff --git a/app/services/system_hooks_service.rb b/app/services/system_hooks_service.rb
index af0ddbe5934..ed476fc9d0c 100644
--- a/app/services/system_hooks_service.rb
+++ b/app/services/system_hooks_service.rb
@@ -51,7 +51,7 @@ class SystemHooksService
path: model.path,
group_id: model.id,
owner_name: owner.respond_to?(:name) ? owner.name : nil,
- owner_email: owner.respond_to?(:email) ? owner.email : nil,
+ owner_email: owner.respond_to?(:email) ? owner.email : nil
)
when GroupMember
data.merge!(group_member_data(model))
@@ -113,7 +113,7 @@ class SystemHooksService
user_name: model.user.name,
user_email: model.user.email,
user_id: model.user.id,
- group_access: model.human_access,
+ group_access: model.human_access
}
end
end
diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb
index 174e7c6e95b..93bf1fb1615 100644
--- a/app/services/system_note_service.rb
+++ b/app/services/system_note_service.rb
@@ -79,7 +79,7 @@ module SystemNoteService
text_parts.join(' and ')
elsif old_assignees.any?
- "removed all assignees"
+ "removed assignee"
elsif issue.assignees.any?
"assigned to #{issue.assignees.map(&:to_reference).to_sentence}"
end
@@ -291,8 +291,8 @@ module SystemNoteService
old_diffs, new_diffs = Gitlab::Diff::InlineDiff.new(old_title, new_title).inline_diffs
- marked_old_title = Gitlab::Diff::InlineDiffMarker.new(old_title).mark(old_diffs, mode: :deletion, markdown: true)
- marked_new_title = Gitlab::Diff::InlineDiffMarker.new(new_title).mark(new_diffs, mode: :addition, markdown: true)
+ marked_old_title = Gitlab::Diff::InlineDiffMarkdownMarker.new(old_title).mark(old_diffs, mode: :deletion)
+ marked_new_title = Gitlab::Diff::InlineDiffMarkdownMarker.new(new_title).mark(new_diffs, mode: :addition)
body = "changed title from **#{marked_old_title}** to **#{marked_new_title}**"
diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml
index 4b6628169ef..e1b4e34cd2b 100644
--- a/app/views/admin/application_settings/_form.html.haml
+++ b/app/views/admin/application_settings/_form.html.haml
@@ -502,17 +502,24 @@
Let GitLab inform you when an update is available.
.form-group
.col-sm-offset-2.col-sm-10
+ - can_be_configured = @application_setting.usage_ping_can_be_configured?
.checkbox
= f.label :usage_ping_enabled do
- = f.check_box :usage_ping_enabled
+ = f.check_box :usage_ping_enabled, disabled: !can_be_configured
Usage ping enabled
- = link_to icon('question-circle'), help_page_path("user/admin_area/settings/usage_statistics", anchor: "usage-data")
+ = link_to icon('question-circle'), help_page_path("user/admin_area/settings/usage_statistics", anchor: "usage-ping")
.help-block
- Every week GitLab will report license usage back to GitLab, Inc.
- Disable this option if you do not want this to occur. To see the
- JSON payload that will be sent, visit the
- = succeed '.' do
- = link_to "Cohorts page", admin_cohorts_path(anchor: 'usage-ping')
+ - if can_be_configured
+ Every week GitLab will report license usage back to GitLab, Inc.
+ Disable this option if you do not want this to occur. To see the
+ JSON payload that will be sent, visit the
+ = succeed '.' do
+ = link_to "Cohorts page", admin_cohorts_path(anchor: 'usage-ping')
+ - else
+ The usage ping is disabled, and cannot be configured through this
+ form. For more information, see the documentation on
+ = succeed '.' do
+ = link_to 'deactivating the usage ping', help_page_path('user/admin_area/settings/usage_statistics', anchor: 'deactivate-the-usage-ping')
%fieldset
%legend Email
diff --git a/app/views/admin/health_check/show.html.haml b/app/views/admin/health_check/show.html.haml
index 6a208d76a38..4deccf4aa93 100644
--- a/app/views/admin/health_check/show.html.haml
+++ b/app/views/admin/health_check/show.html.haml
@@ -16,24 +16,15 @@
= icon('spinner')
Reset health check access token
%p.light
- Health information can be retrieved as plain text, JSON, or XML using:
+ Health information can be retrieved from the following endpoints. More information is available
+ = link_to 'here', help_page_path('user/admin_area/monitoring/health_check')
%ul
%li
- %code= health_check_url(token: current_application_settings.health_check_access_token)
+ %code= readiness_url(token: current_application_settings.health_check_access_token)
%li
- %code= health_check_url(token: current_application_settings.health_check_access_token, format: :json)
+ %code= liveness_url(token: current_application_settings.health_check_access_token)
%li
- %code= health_check_url(token: current_application_settings.health_check_access_token, format: :xml)
-
- %p.light
- You can also ask for the status of specific services:
- %ul
- %li
- %code= health_check_url(token: current_application_settings.health_check_access_token, checks: :cache)
- %li
- %code= health_check_url(token: current_application_settings.health_check_access_token, checks: :database)
- %li
- %code= health_check_url(token: current_application_settings.health_check_access_token, checks: :migrations)
+ %code= metrics_url(token: current_application_settings.health_check_access_token)
%hr
.panel.panel-default
diff --git a/app/views/admin/hooks/_form.html.haml b/app/views/admin/hooks/_form.html.haml
index 6217d5fb135..645005c6deb 100644
--- a/app/views/admin/hooks/_form.html.haml
+++ b/app/views/admin/hooks/_form.html.haml
@@ -18,19 +18,26 @@
or adding ssh key. But you can also enable extra triggers like Push events.
.prepend-top-default
+ = form.check_box :repository_update_events, class: 'pull-left'
+ .prepend-left-20
+ = form.label :repository_update_events, class: 'list-label' do
+ %strong Repository update events
+ %p.light
+ This URL will be triggered when repository is updated
+ %div
= form.check_box :push_events, class: 'pull-left'
.prepend-left-20
= form.label :push_events, class: 'list-label' do
%strong Push events
%p.light
- This url will be triggered by a push to the repository
+ This URL will be triggered for each branch updated to the repository
%div
= form.check_box :tag_push_events, class: 'pull-left'
.prepend-left-20
= form.label :tag_push_events, class: 'list-label' do
%strong Tag push events
%p.light
- This url will be triggered when a new tag is pushed to the repository
+ This URL will be triggered when a new tag is pushed to the repository
.form-group
= form.label :enable_ssl_verification, 'SSL verification', class: 'control-label checkbox'
.col-sm-10
diff --git a/app/views/admin/hooks/index.html.haml b/app/views/admin/hooks/index.html.haml
index 71117758921..e92b8bc39f4 100644
--- a/app/views/admin/hooks/index.html.haml
+++ b/app/views/admin/hooks/index.html.haml
@@ -27,7 +27,7 @@
= link_to 'Remove', admin_hook_path(hook), data: { confirm: 'Are you sure?' }, method: :delete, class: 'btn btn-remove btn-sm'
.monospace= hook.url
%div
- - %w(push_events tag_push_events issues_events note_events merge_requests_events build_events).each do |trigger|
+ - %w(repository_update_events push_events tag_push_events issues_events note_events merge_requests_events job_events).each do |trigger|
- if hook.send(trigger)
%span.label.label-gray= trigger.titleize
%span.label.label-gray SSL Verification: #{hook.enable_ssl_verification ? 'enabled' : 'disabled'}
diff --git a/app/views/devise/passwords/edit.html.haml b/app/views/devise/passwords/edit.html.haml
index 5e189e6dc54..eb0e6701627 100644
--- a/app/views/devise/passwords/edit.html.haml
+++ b/app/views/devise/passwords/edit.html.haml
@@ -6,10 +6,10 @@
= devise_error_messages!
= f.hidden_field :reset_password_token
.form-group
- = f.label 'New password', for: :password
+ = f.label 'New password', for: "user_password"
= f.password_field :password, class: "form-control top", required: true, title: 'This field is required'
.form-group
- = f.label 'Confirm new password', for: :password_confirmation
+ = f.label 'Confirm new password', for: "user_password_confirmation"
= f.password_field :password_confirmation, class: "form-control bottom", title: 'This field is required', required: true
.clearfix
= f.submit "Change your password", class: "btn btn-primary"
diff --git a/app/views/devise/sessions/_new_base.html.haml b/app/views/devise/sessions/_new_base.html.haml
index 21c751a23f8..4095f30c369 100644
--- a/app/views/devise/sessions/_new_base.html.haml
+++ b/app/views/devise/sessions/_new_base.html.haml
@@ -1,6 +1,6 @@
= form_for(resource, as: resource_name, url: session_path(resource_name), html: { class: 'new_user gl-show-field-errors', 'aria-live' => 'assertive'}) do |f|
.form-group
- = f.label "Username or email", for: :login
+ = f.label "Username or email", for: "user_login"
= f.text_field :login, class: "form-control top", autofocus: "autofocus", autocapitalize: "off", autocorrect: "off", required: true, title: "This field is required."
.form-group
= f.label :password
diff --git a/app/views/discussions/_diff_with_notes.html.haml b/app/views/discussions/_diff_with_notes.html.haml
index 78c5b0c1dda..c3f55ff821f 100644
--- a/app/views/discussions/_diff_with_notes.html.haml
+++ b/app/views/discussions/_diff_with_notes.html.haml
@@ -3,7 +3,7 @@
.diff-file.file-holder
.js-file-title.file-title
- = render "projects/diffs/file_header", diff_file: diff_file, blob: blob, diff_commit: diff_file.content_commit, project: discussion.project, url: discussion_path(discussion)
+ = render "projects/diffs/file_header", diff_file: diff_file, blob: blob, diff_commit: diff_file.content_commit, project: discussion.project, url: discussion_path(discussion), show_toggle: false
.diff-content.code.js-syntax-highlight
%table
diff --git a/app/views/discussions/_discussion.html.haml b/app/views/discussions/_discussion.html.haml
index 38e85168f40..74992e439f3 100644
--- a/app/views/discussions/_discussion.html.haml
+++ b/app/views/discussions/_discussion.html.haml
@@ -26,7 +26,7 @@
- commit = discussion.noteable
- if commit
commit
- = link_to commit.short_id, url, class: 'monospace'
+ = link_to commit.short_id, url, class: 'commit-sha'
- else
a deleted commit
- elsif discussion.diff_discussion?
diff --git a/app/views/events/_commit.html.haml b/app/views/events/_commit.html.haml
index 1bc9f604438..3c64f1be5ff 100644
--- a/app/views/events/_commit.html.haml
+++ b/app/views/events/_commit.html.haml
@@ -1,5 +1,5 @@
%li.commit
.commit-row-title
- = link_to truncate_sha(commit[:id]), namespace_project_commit_path(project.namespace, project, commit[:id]), class: "commit_short_id", alt: '', title: truncate_sha(commit[:id])
+ = link_to truncate_sha(commit[:id]), namespace_project_commit_path(project.namespace, project, commit[:id]), class: "commit-sha", alt: '', title: truncate_sha(commit[:id])
&middot;
= markdown event_commit_title(commit[:message]), project: project, pipeline: :single_line, author: event.author
diff --git a/app/views/import/base/create.js.haml b/app/views/import/base/create.js.haml
index 8e929538351..57e8c3ca1e1 100644
--- a/app/views/import/base/create.js.haml
+++ b/app/views/import/base/create.js.haml
@@ -10,4 +10,4 @@
- else
:plain
job = $("tr#repo_#{@repo_id}")
- job.find(".import-actions").html("<i class='fa fa-exclamation-circle'></i> Error saving project: #{escape_javascript(@project.errors.full_messages.join(','))}")
+ job.find(".import-actions").html("<i class='fa fa-exclamation-circle'></i> Error saving project: #{escape_javascript(h(@project.errors.full_messages.join(',')))}")
diff --git a/app/views/import/fogbugz/new_user_map.html.haml b/app/views/import/fogbugz/new_user_map.html.haml
index 9999a4362c6..c52a515226e 100644
--- a/app/views/import/fogbugz/new_user_map.html.haml
+++ b/app/views/import/fogbugz/new_user_map.html.haml
@@ -46,6 +46,3 @@
.form-actions
= submit_tag 'Continue to the next step', class: 'btn btn-create'
-
-:javascript
- new UsersSelect();
diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml
index afcc2b6e4f3..9e354987401 100644
--- a/app/views/layouts/_head.html.haml
+++ b/app/views/layouts/_head.html.haml
@@ -27,6 +27,7 @@
= stylesheet_link_tag "application", media: "all"
= stylesheet_link_tag "print", media: "print"
+ = stylesheet_link_tag "test", media: "all" if Rails.env.test?
= Gon::Base.render_data
@@ -34,6 +35,7 @@
= webpack_bundle_tag "common"
= webpack_bundle_tag "main"
= webpack_bundle_tag "raven" if current_application_settings.clientside_sentry_enabled
+ = webpack_bundle_tag "test" if Rails.env.test?
- if content_for?(:page_specific_javascripts)
= yield :page_specific_javascripts
diff --git a/app/views/layouts/_init_auto_complete.html.haml b/app/views/layouts/_init_auto_complete.html.haml
index 769f6fb0151..6caaba240bb 100644
--- a/app/views/layouts/_init_auto_complete.html.haml
+++ b/app/views/layouts/_init_auto_complete.html.haml
@@ -3,6 +3,7 @@
- if project
:javascript
+ gl.GfmAutoComplete = gl.GfmAutoComplete || {};
gl.GfmAutoComplete.dataSources = {
members: "#{members_namespace_project_autocomplete_sources_path(project.namespace, project, type: noteable_type, type_id: params[:id])}",
issues: "#{issues_namespace_project_autocomplete_sources_path(project.namespace, project)}",
@@ -11,5 +12,3 @@
milestones: "#{milestones_namespace_project_autocomplete_sources_path(project.namespace, project)}",
commands: "#{commands_namespace_project_autocomplete_sources_path(project.namespace, project, type: noteable_type, type_id: params[:id])}"
};
-
- gl.GfmAutoComplete.setup();
diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml
index 0e64ebd71b8..b689991bb6d 100644
--- a/app/views/layouts/_search.html.haml
+++ b/app/views/layouts/_search.html.haml
@@ -13,7 +13,7 @@
.location-badge= label
.search-input-wrap
.dropdown{ data: { url: search_autocomplete_path } }
- = search_field_tag 'search', nil, placeholder: 'Search', class: 'search-input dropdown-menu-toggle no-outline js-search-dashboard-options', spellcheck: false, tabindex: '1', autocomplete: 'off', data: { toggle: 'dropdown', issues_path: issues_dashboard_url, mr_path: merge_requests_dashboard_url }
+ = search_field_tag 'search', nil, placeholder: 'Search', class: 'search-input dropdown-menu-toggle no-outline js-search-dashboard-options', spellcheck: false, tabindex: '1', autocomplete: 'off', data: { toggle: 'dropdown', issues_path: issues_dashboard_url, mr_path: merge_requests_dashboard_url }, aria: { label: 'Search' }
.dropdown-menu.dropdown-select
= dropdown_content do
%ul
diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml
index 7e011ac3e75..03688e9ff21 100644
--- a/app/views/layouts/application.html.haml
+++ b/app/views/layouts/application.html.haml
@@ -2,8 +2,8 @@
%html{ lang: I18n.locale, class: "#{page_class}" }
= render "layouts/head"
%body{ class: @body_class, data: { page: body_data_page, project: "#{@project.path if @project}", group: "#{@group.path if @group}" } }
+ = render "layouts/init_auto_complete" if @gfm_form
= render "layouts/header/default", title: header_title
= render 'layouts/page', sidebar: sidebar, nav: nav
= yield :scripts_body
- = render "layouts/init_auto_complete" if @gfm_form
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index 659d548df18..9db98451f1d 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -1,4 +1,5 @@
%header.navbar.navbar-gitlab{ class: nav_header_class }
+ .navbar-border
%a.sr-only.gl-accessibility{ href: "#content-body", tabindex: "1" } Skip to content
.container-fluid
.header-content
diff --git a/app/views/layouts/nav/_profile.html.haml b/app/views/layouts/nav/_profile.html.haml
index e06301bda14..ae1e1361f0f 100644
--- a/app/views/layouts/nav/_profile.html.haml
+++ b/app/views/layouts/nav/_profile.html.haml
@@ -48,6 +48,6 @@
%span
Preferences
= nav_link(path: 'profiles#audit_log') do
- = link_to audit_log_profile_path, title: 'Audit Log' do
+ = link_to audit_log_profile_path, title: 'Authentication log' do
%span
- Audit Log
+ Authentication log
diff --git a/app/views/profiles/_event_table.html.haml b/app/views/profiles/_event_table.html.haml
index 879fc170f92..d0ad90ac6cc 100644
--- a/app/views/profiles/_event_table.html.haml
+++ b/app/views/profiles/_event_table.html.haml
@@ -9,7 +9,6 @@
Signed in with
= event.details[:with]
authentication
- %span.pull-right
- #{time_ago_in_words event.created_at} ago
+ %span.pull-right= time_ago_with_tooltip(event.created_at)
= paginate events, theme: "gitlab"
diff --git a/app/views/profiles/audit_log.html.haml b/app/views/profiles/audit_log.html.haml
index 9fe86e6b291..a24b7fd101d 100644
--- a/app/views/profiles/audit_log.html.haml
+++ b/app/views/profiles/audit_log.html.haml
@@ -1,4 +1,4 @@
-- page_title "Audit Log"
+- page_title "Authentication log"
= render 'profiles/head'
.row.prepend-top-default
diff --git a/app/views/projects/_files.html.haml b/app/views/projects/_files.html.haml
index 96c2fa87f45..426085b3e1c 100644
--- a/app/views/projects/_files.html.haml
+++ b/app/views/projects/_files.html.haml
@@ -1,6 +1,14 @@
+- commit = local_assigns.fetch(:commit) { @repository.commit }
+- ref = local_assigns.fetch(:ref) { current_ref }
+- project = local_assigns.fetch(:project) { @project }
#tree-holder.tree-holder.clearfix
.nav-block
= render 'projects/tree/tree_header', tree: @tree
- = render 'projects/tree/tree_content', tree: @tree
+ - if commit
+ .info-well.hidden-xs.project-last-commit.append-bottom-default
+ .well-segment
+ %ul.blob-commit-info
+ = render 'projects/commits/commit', commit: commit, ref: ref, project: project
+ = render 'projects/tree/tree_content', tree: @tree
diff --git a/app/views/projects/_last_commit.html.haml b/app/views/projects/_last_commit.html.haml
deleted file mode 100644
index df3b1c75508..00000000000
--- a/app/views/projects/_last_commit.html.haml
+++ /dev/null
@@ -1,12 +0,0 @@
-- ref = local_assigns.fetch(:ref)
-- status = commit.status(ref)
-- if status
- = link_to pipelines_namespace_project_commit_path(commit.project.namespace, commit.project, commit), class: "ci-status ci-#{status}" do
- = ci_icon_for_status(status)
- = ci_text_for_status(status)
-
-= link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit_short_id"
-= link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit), class: "commit-row-message"
-&middot;
-#{time_ago_with_tooltip(commit.committed_date)} by
-= commit_author_link(commit, avatar: true, size: 24)
diff --git a/app/views/projects/_last_push.html.haml b/app/views/projects/_last_push.html.haml
index 768bc1fb323..f8a6e98d280 100644
--- a/app/views/projects/_last_push.html.haml
+++ b/app/views/projects/_last_push.html.haml
@@ -5,7 +5,7 @@
.event-last-push
.event-last-push-text
%span You pushed to
- = link_to namespace_project_commits_path(event.project.namespace, event.project, event.ref_name) do
+ = link_to namespace_project_commits_path(event.project.namespace, event.project, event.ref_name, class: 'commit-sha') do
%strong= event.ref_name
- if @project && event.project != @project
%span at
diff --git a/app/views/projects/_readme.html.haml b/app/views/projects/_readme.html.haml
deleted file mode 100644
index c0d12cbc66e..00000000000
--- a/app/views/projects/_readme.html.haml
+++ /dev/null
@@ -1,21 +0,0 @@
-- if readme = @repository.readme
- %article.readme-holder
- .pull-right
- - if can?(current_user, :push_code, @project)
- = link_to icon('pencil'), namespace_project_edit_blob_path(@project.namespace, @project, tree_join(@repository.root_ref, readme.name)), class: 'light edit-project-readme'
- .file-content.wiki
- = markup(readme.name, readme.data, rendered: @repository.rendered_readme)
-- else
- .row-content-block.second-block.center
- %h3.page-title
- This project does not have a README yet
- - if can?(current_user, :push_code, @project)
- %p
- A
- %code README
- file contains information about other files in a repository and is commonly
- distributed with computer software, forming part of its documentation.
- %p
- We recommend you to
- = link_to "add a README", add_special_file_path(@project, file_name: 'README.md'), class: 'underlined-link'
- file to the repository and GitLab will render it here instead of this message.
diff --git a/app/views/projects/_zen.html.haml b/app/views/projects/_zen.html.haml
index 0c8241053e7..3b3d08ddd3c 100644
--- a/app/views/projects/_zen.html.haml
+++ b/app/views/projects/_zen.html.haml
@@ -1,10 +1,11 @@
- @gfm_form = true
+- current_text ||= nil
- supports_slash_commands = local_assigns.fetch(:supports_slash_commands, false)
.zen-backdrop
- classes << ' js-gfm-input js-autosize markdown-area'
- if defined?(f) && f
= f.text_area attr, class: classes, placeholder: placeholder, data: { supports_slash_commands: supports_slash_commands }
- else
- = text_area_tag attr, nil, class: classes, placeholder: placeholder
+ = text_area_tag attr, current_text, class: classes, placeholder: placeholder
%a.zen-control.zen-control-leave.js-zen-leave{ href: "#" }
= icon('compress')
diff --git a/app/views/projects/blame/show.html.haml b/app/views/projects/blame/show.html.haml
index ae4658d8ca1..a2ec3d44185 100644
--- a/app/views/projects/blame/show.html.haml
+++ b/app/views/projects/blame/show.html.haml
@@ -22,7 +22,7 @@
%strong
= link_to_gfm truncate(commit.title, length: 35), namespace_project_commit_path(@project.namespace, @project, commit.id), class: "cdark"
.pull-right
- = link_to commit.short_id, namespace_project_commit_path(@project.namespace, @project, commit), class: "monospace"
+ = link_to commit.short_id, namespace_project_commit_path(@project.namespace, @project, commit), class: "commit-sha"
&nbsp;
.light
= commit_author_link(commit, avatar: false)
diff --git a/app/views/projects/blob/_auxiliary_viewer.html.haml b/app/views/projects/blob/_auxiliary_viewer.html.haml
new file mode 100644
index 00000000000..9749afdc580
--- /dev/null
+++ b/app/views/projects/blob/_auxiliary_viewer.html.haml
@@ -0,0 +1,5 @@
+- blob = local_assigns.fetch(:blob)
+- auxiliary_viewer = blob.auxiliary_viewer
+- if auxiliary_viewer && auxiliary_viewer.render_error.nil? && auxiliary_viewer.visible_to?(current_user)
+ .well-segment.blob-auxiliary-viewer
+ = render 'projects/blob/viewer', viewer: auxiliary_viewer
diff --git a/app/views/projects/blob/_blob.html.haml b/app/views/projects/blob/_blob.html.haml
index f4307421ed0..8bd336269ff 100644
--- a/app/views/projects/blob/_blob.html.haml
+++ b/app/views/projects/blob/_blob.html.haml
@@ -3,8 +3,9 @@
.info-well.hidden-xs
.well-segment
%ul.blob-commit-info
- - blob_commit = @repository.last_commit_for_path(@commit.id, blob.path)
- = render blob_commit, project: @project, ref: @ref
+ = render 'projects/commits/commit', commit: @last_commit, project: @project, ref: @ref
+
+ = render "projects/blob/auxiliary_viewer", blob: blob
#blob-content-holder.blob-content-holder
%article.file-holder
diff --git a/app/views/projects/blob/_viewer.html.haml b/app/views/projects/blob/_viewer.html.haml
index 5326bb3e0cf..4252f27d007 100644
--- a/app/views/projects/blob/_viewer.html.haml
+++ b/app/views/projects/blob/_viewer.html.haml
@@ -1,12 +1,11 @@
- hidden = local_assigns.fetch(:hidden, false)
- render_error = viewer.render_error
-- load_asynchronously = local_assigns.fetch(:load_asynchronously, viewer.server_side?) && render_error.nil?
+- load_async = local_assigns.fetch(:load_async, viewer.load_async?)
-- url = url_for(params.merge(viewer: viewer.type, format: :json)) if load_asynchronously
-.blob-viewer{ data: { type: viewer.type, url: url }, class: ('hidden' if hidden) }
- - if load_asynchronously
- .text-center.prepend-top-default.append-bottom-default
- = icon('spinner spin 2x', 'aria-hidden' => 'true', 'aria-label' => 'Loading content')
+- viewer_url = local_assigns.fetch(:viewer_url) { url_for(params.merge(viewer: viewer.type, format: :json)) } if load_async
+.blob-viewer{ data: { type: viewer.type, url: viewer_url }, class: ('hidden' if hidden) }
+ - if load_async
+ = render viewer.loading_partial_path, viewer: viewer
- elsif render_error
= render 'projects/blob/render_error', viewer: viewer
- else
diff --git a/app/views/projects/blob/viewers/_changelog.html.haml b/app/views/projects/blob/viewers/_changelog.html.haml
new file mode 100644
index 00000000000..53921e63b5f
--- /dev/null
+++ b/app/views/projects/blob/viewers/_changelog.html.haml
@@ -0,0 +1,4 @@
+= icon('history fw')
+= succeed '.' do
+ To find the state of this project's repository at the time of any of these versions, check out
+ = link_to "the tags", namespace_project_tags_path(viewer.project.namespace, viewer.project)
diff --git a/app/views/projects/blob/viewers/_contributing.html.haml b/app/views/projects/blob/viewers/_contributing.html.haml
new file mode 100644
index 00000000000..c78f04c9c7c
--- /dev/null
+++ b/app/views/projects/blob/viewers/_contributing.html.haml
@@ -0,0 +1,9 @@
+= icon('book fw')
+After you've reviewed these contribution guidelines, you'll be all set to
+
+- options = contribution_options(viewer.project)
+- if options.any?
+ = succeed '.' do
+ = options.to_sentence(two_words_connector: ' or ', last_word_connector: ', or ').html_safe
+- else
+ contribute to this project.
diff --git a/app/views/projects/blob/viewers/_dependency_manager.html.haml b/app/views/projects/blob/viewers/_dependency_manager.html.haml
new file mode 100644
index 00000000000..a0f0215a5ff
--- /dev/null
+++ b/app/views/projects/blob/viewers/_dependency_manager.html.haml
@@ -0,0 +1,11 @@
+= icon('cubes fw')
+= succeed '.' do
+ This project manages its dependencies using
+ %strong= viewer.manager_name
+
+ - if viewer.package_name
+ and defines a #{viewer.package_type} named
+ %strong<
+ = link_to viewer.package_name, viewer.package_url, target: '_blank', rel: 'noopener noreferrer'
+
+= link_to 'Learn more', viewer.manager_url, target: '_blank', rel: 'noopener noreferrer'
diff --git a/app/views/projects/blob/viewers/_gitlab_ci_yml.html.haml b/app/views/projects/blob/viewers/_gitlab_ci_yml.html.haml
new file mode 100644
index 00000000000..28c5be6ebf3
--- /dev/null
+++ b/app/views/projects/blob/viewers/_gitlab_ci_yml.html.haml
@@ -0,0 +1,9 @@
+- if viewer.valid?
+ = icon('check fw')
+ This GitLab CI configuration is valid.
+- else
+ = icon('warning fw')
+ This GitLab CI configuration is invalid:
+ = viewer.validation_message
+
+= link_to 'Learn more', help_page_path('ci/yaml/README')
diff --git a/app/views/projects/blob/viewers/_gitlab_ci_yml_loading.html.haml b/app/views/projects/blob/viewers/_gitlab_ci_yml_loading.html.haml
new file mode 100644
index 00000000000..10cbf6a2f7a
--- /dev/null
+++ b/app/views/projects/blob/viewers/_gitlab_ci_yml_loading.html.haml
@@ -0,0 +1,4 @@
+= icon('spinner spin fw')
+Validating GitLab CI configuration…
+
+= link_to 'Learn more', help_page_path('ci/yaml/README')
diff --git a/app/views/projects/blob/viewers/_license.html.haml b/app/views/projects/blob/viewers/_license.html.haml
new file mode 100644
index 00000000000..fb9d0b99d09
--- /dev/null
+++ b/app/views/projects/blob/viewers/_license.html.haml
@@ -0,0 +1,8 @@
+- license = viewer.license
+
+= icon('balance-scale fw')
+This project is licensed under the
+= succeed '.' do
+ %strong= license.name
+
+= link_to 'Learn more', license.url, target: '_blank', rel: 'noopener noreferrer'
diff --git a/app/views/projects/blob/viewers/_loading.html.haml b/app/views/projects/blob/viewers/_loading.html.haml
new file mode 100644
index 00000000000..120c0540335
--- /dev/null
+++ b/app/views/projects/blob/viewers/_loading.html.haml
@@ -0,0 +1,2 @@
+.text-center.prepend-top-default.append-bottom-default
+ = icon('spinner spin 2x', 'aria-hidden' => 'true', 'aria-label' => 'Loading content…')
diff --git a/app/views/projects/blob/viewers/_loading_auxiliary.html.haml b/app/views/projects/blob/viewers/_loading_auxiliary.html.haml
new file mode 100644
index 00000000000..c7dc9e3250a
--- /dev/null
+++ b/app/views/projects/blob/viewers/_loading_auxiliary.html.haml
@@ -0,0 +1,2 @@
+= icon('spinner spin fw')
+Analyzing file…
diff --git a/app/views/projects/blob/viewers/_readme.html.haml b/app/views/projects/blob/viewers/_readme.html.haml
new file mode 100644
index 00000000000..334b33faf48
--- /dev/null
+++ b/app/views/projects/blob/viewers/_readme.html.haml
@@ -0,0 +1,4 @@
+= icon('info-circle fw')
+= succeed '.' do
+ To learn more about this project, read
+ = link_to "the wiki", namespace_project_wikis_path(viewer.project.namespace, viewer.project)
diff --git a/app/views/projects/blob/viewers/_route_map.html.haml b/app/views/projects/blob/viewers/_route_map.html.haml
new file mode 100644
index 00000000000..d0fcd55f6c1
--- /dev/null
+++ b/app/views/projects/blob/viewers/_route_map.html.haml
@@ -0,0 +1,9 @@
+- if viewer.valid?
+ = icon('check fw')
+ This Route Map is valid.
+- else
+ = icon('warning fw')
+ This Route Map is invalid:
+ = viewer.validation_message
+
+= link_to 'Learn more', help_page_path('ci/environments', anchor: 'route-map')
diff --git a/app/views/projects/blob/viewers/_route_map_loading.html.haml b/app/views/projects/blob/viewers/_route_map_loading.html.haml
new file mode 100644
index 00000000000..2318cf82f58
--- /dev/null
+++ b/app/views/projects/blob/viewers/_route_map_loading.html.haml
@@ -0,0 +1,4 @@
+= icon('spinner spin fw')
+Validating Route Map…
+
+= link_to 'Learn more', help_page_path('ci/environments', anchor: 'route-map')
diff --git a/app/views/projects/boards/_show.html.haml b/app/views/projects/boards/_show.html.haml
index 7ca0ec8ed2b..efec69662f3 100644
--- a/app/views/projects/boards/_show.html.haml
+++ b/app/views/projects/boards/_show.html.haml
@@ -3,9 +3,9 @@
- page_title "Boards"
- content_for :page_specific_javascripts do
- = page_specific_javascript_bundle_tag('common_vue')
- = page_specific_javascript_bundle_tag('filtered_search')
- = page_specific_javascript_bundle_tag('boards')
+ = webpack_bundle_tag 'common_vue'
+ = webpack_bundle_tag 'filtered_search'
+ = webpack_bundle_tag 'boards'
%script#js-board-template{ type: "text/x-template" }= render "projects/boards/components/board"
%script#js-board-modal-filter{ type: "text/x-template" }= render "shared/issuable/search_bar", type: :boards_modal
diff --git a/app/views/projects/boards/components/sidebar/_assignee.html.haml b/app/views/projects/boards/components/sidebar/_assignee.html.haml
index 642da679f97..48f8c656080 100644
--- a/app/views/projects/boards/components/sidebar/_assignee.html.haml
+++ b/app/views/projects/boards/components/sidebar/_assignee.html.haml
@@ -10,7 +10,7 @@
- if can?(current_user, :admin_issue, @project)
.selectbox.hide-collapsed
- %input{ type: "hidden",
+ %input.js-vue{ type: "hidden",
name: "issue[assignee_ids][]",
":value" => "assignee.id",
"v-if" => "issue.assignees",
@@ -18,7 +18,6 @@
.dropdown
%button.dropdown-menu-toggle.js-user-search.js-author-search.js-multiselect.js-save-user-data.js-issue-board-sidebar{ type: "button", ref: "assigneeDropdown", data: { toggle: "dropdown", field_name: "issue[assignee_ids][]", first_user: (current_user.username if current_user), current_user: "true", project_id: @project.id, null_user: "true", multi_select: "true", 'max-select' => 1, dropdown: { header: 'Assignee' } },
":data-issuable-id" => "issue.id",
- ":data-selected" => "assigneeId",
":data-issue-update" => "'#{namespace_project_issues_path(@project.namespace, @project)}/' + issue.id + '.json'" }
Select assignee
= icon("chevron-down")
diff --git a/app/views/projects/boards/components/sidebar/_milestone.html.haml b/app/views/projects/boards/components/sidebar/_milestone.html.haml
index 190e7290303..4e46351bf8a 100644
--- a/app/views/projects/boards/components/sidebar/_milestone.html.haml
+++ b/app/views/projects/boards/components/sidebar/_milestone.html.haml
@@ -16,7 +16,8 @@
name: "issue[milestone_id]",
"v-if" => "issue.milestone" }
.dropdown
- %button.dropdown-menu-toggle.js-milestone-select.js-issue-board-sidebar{ type: "button", data: { toggle: "dropdown", show_no: "true", field_name: "issue[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), ability_name: "issue", use_id: "true" },
+ %button.dropdown-menu-toggle.js-milestone-select.js-issue-board-sidebar{ type: "button", data: { toggle: "dropdown", show_no: "true", field_name: "issue[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), ability_name: "issue", use_id: "true", default_no: "true" },
+ ":data-selected" => "milestoneTitle",
":data-issuable-id" => "issue.id",
":data-issue-update" => "'#{namespace_project_issues_path(@project.namespace, @project)}/' + issue.id + '.json'" }
Milestone
diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml
index 8b35a037c55..304c512e1b5 100644
--- a/app/views/projects/branches/_branch.html.haml
+++ b/app/views/projects/branches/_branch.html.haml
@@ -6,7 +6,8 @@
- merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project))
%li{ class: "js-branch-#{branch.name}" }
%div
- = link_to namespace_project_tree_path(@project.namespace, @project, branch.name), class: 'item-title str-truncated' do
+ = link_to namespace_project_tree_path(@project.namespace, @project, branch.name), class: 'item-title str-truncated ref-name' do
+ = icon('code-fork')
= branch.name
&nbsp;
- if branch.name == @repository.root_ref
diff --git a/app/views/projects/branches/_commit.html.haml b/app/views/projects/branches/_commit.html.haml
index de607772df6..ad8f9da0621 100644
--- a/app/views/projects/branches/_commit.html.haml
+++ b/app/views/projects/branches/_commit.html.haml
@@ -1,7 +1,7 @@
.branch-commit
.icon-container.commit-icon
= custom_icon("icon_commit")
- = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit.id), class: "commit-id monospace"
+ = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit.id), class: "commit-sha"
&middot;
%span.str-truncated
= link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit.id), class: "commit-row-message"
diff --git a/app/views/projects/branches/new.html.haml b/app/views/projects/branches/new.html.haml
index 796ecdfd014..5a0eba3551f 100644
--- a/app/views/projects/branches/new.html.haml
+++ b/app/views/projects/branches/new.html.haml
@@ -17,11 +17,13 @@
.help-block.text-danger.js-branch-name-error
.form-group
= label_tag :ref, 'Create from', class: 'control-label'
- .col-sm-10.dropdown.create-from
- = hidden_field_tag :ref, default_ref
- = button_tag type: 'button', title: default_ref, class: 'dropdown-toggle form-control js-branch-select', required: true, data: { toggle: 'dropdown', selected: default_ref, field_name: 'ref' } do
- .text-left.dropdown-toggle-text= default_ref
- = render 'shared/ref_dropdown', dropdown_class: 'wide'
+ .col-sm-10.create-from
+ .dropdown
+ = hidden_field_tag :ref, default_ref
+ = button_tag type: 'button', title: default_ref, class: 'dropdown-menu-toggle wide form-control js-branch-select git-revision-dropdown-toggle', required: true, data: { toggle: 'dropdown', selected: default_ref, field_name: 'ref' } do
+ .text-left.dropdown-toggle-text= default_ref
+ = icon('chevron-down')
+ = render 'shared/ref_dropdown', dropdown_class: 'wide'
.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 a0f8f105d9a..d4cdb709b97 100644
--- a/app/views/projects/builds/_header.html.haml
+++ b/app/views/projects/builds/_header.html.haml
@@ -6,18 +6,16 @@
= render 'ci/status/badge', status: @build.detailed_status(current_user), link: false, title: @build.status_title
%strong
Job
- = link_to namespace_project_build_path(@project.namespace, @project, @build), class: 'js-build-id' do
- \##{@build.id}
+ = link_to "##{@build.id}", namespace_project_build_path(@project.namespace, @project, @build), class: 'js-build-id'
in pipeline
- = link_to pipeline_path(pipeline) do
- %strong ##{pipeline.id}
- for commit
- = link_to namespace_project_commit_path(@project.namespace, @project, pipeline.sha) do
- %strong= pipeline.short_sha
+ %strong
+ = link_to "##{pipeline.id}", pipeline_path(pipeline)
+ for
+ %strong
+ = link_to pipeline.short_sha, namespace_project_commit_path(@project.namespace, @project, pipeline.sha), class: 'commit-sha'
from
- = link_to namespace_project_commits_path(@project.namespace, @project, @build.ref) do
- %code
- = @build.ref
+ %strong
+ = link_to @build.ref, project_ref_path(@project, @build.ref), class: 'ref-name'
= render "projects/builds/user" if @build.user
diff --git a/app/views/projects/builds/_sidebar.html.haml b/app/views/projects/builds/_sidebar.html.haml
index 43191fae9e6..8032d81cd91 100644
--- a/app/views/projects/builds/_sidebar.html.haml
+++ b/app/views/projects/builds/_sidebar.html.haml
@@ -1,6 +1,6 @@
- builds = @build.pipeline.builds.to_a
-%aside.right-sidebar.right-sidebar-expanded.build-sidebar.js-build-sidebar.js-right-sidebar{ data: { "offset-top" => "153", "spy" => "affix" } }
+%aside.right-sidebar.right-sidebar-expanded.build-sidebar.js-build-sidebar.js-right-sidebar{ data: { "offset-top" => "101", "spy" => "affix" } }
.block.build-sidebar-header.visible-xs-block.visible-sm-block.append-bottom-default
Job
%strong ##{@build.id}
@@ -136,7 +136,7 @@
- else
= build.id
- if build.retried?
- %i.fa.fa-spinner.has-tooltip{ data: { container: 'body', placement: 'bottom' }, title: 'Job was retried' }
+ %i.fa.fa-refresh.has-tooltip{ data: { container: 'body', placement: 'bottom' }, title: 'Job was retried' }
:javascript
new Sidebar();
diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml
index c0019996176..e796920ac82 100644
--- a/app/views/projects/ci/builds/_build.html.haml
+++ b/app/views/projects/ci/builds/_build.html.haml
@@ -23,14 +23,14 @@
- if job.ref
.icon-container
= job.tag? ? icon('tag') : icon('code-fork')
- = link_to job.ref, namespace_project_commits_path(job.project.namespace, job.project, job.ref), class: "monospace branch-name"
+ = link_to job.ref, project_ref_path(job.project, job.ref), class: "ref-name"
- else
.light none
.icon-container.commit-icon
= custom_icon("icon_commit")
- if commit_sha
- = link_to job.short_sha, namespace_project_commit_path(job.project.namespace, job.project, job.sha), class: "commit-id monospace"
+ = link_to job.short_sha, namespace_project_commit_path(job.project.namespace, job.project, job.sha), class: "commit-sha"
- if job.stuck?
= icon('warning', class: 'text-warning has-tooltip', title: 'Job is stuck. Check runners.')
@@ -58,7 +58,7 @@
- if pipeline.user
= user_avatar(user: pipeline.user, size: 20)
- else
- %span.monospace API
+ %span.api API
- if admin
%td
diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml
index 64adb70cb81..0aef5822f81 100644
--- a/app/views/projects/commit/_commit_box.html.haml
+++ b/app/views/projects/commit/_commit_box.html.haml
@@ -1,6 +1,8 @@
.page-content-header
.header-main-content
- %strong Commit #{@commit.short_id}
+ %strong
+ Commit
+ %span.commit-sha= @commit.short_id
= clipboard_button(text: @commit.id, title: "Copy commit SHA to clipboard")
%span.hidden-xs authored
#{time_ago_with_tooltip(@commit.authored_date)}
@@ -57,7 +59,7 @@
= custom_icon("icon_commit")
%span.cgray= pluralize(@commit.parents.count, "parent")
- @commit.parents.each do |parent|
- = link_to parent.short_id, namespace_project_commit_path(@project.namespace, @project, parent), class: "monospace"
+ = link_to parent.short_id, namespace_project_commit_path(@project.namespace, @project, parent), class: "commit-sha"
%span.commit-info.branches
%i.fa.fa-spinner.fa-spin
@@ -68,9 +70,10 @@
= link_to namespace_project_pipeline_path(@project.namespace, @project, last_pipeline.id) do
= ci_icon_for_status(last_pipeline.status)
Pipeline
- = link_to "##{last_pipeline.id}", namespace_project_pipeline_path(@project.namespace, @project, last_pipeline.id), class: "monospace"
+ = link_to "##{last_pipeline.id}", namespace_project_pipeline_path(@project.namespace, @project, last_pipeline.id)
= ci_label_for_status(last_pipeline.status)
- if last_pipeline.stages.any?
+ with #{"stage".pluralize(last_pipeline.stages.count)}
.mr-widget-pipeline-graph
= render 'shared/mini_pipeline_graph', pipeline: last_pipeline, klass: 'js-commit-pipeline-graph'
in
diff --git a/app/views/projects/commit/_pipeline.html.haml b/app/views/projects/commit/_pipeline.html.haml
deleted file mode 100644
index 3ee85723ebe..00000000000
--- a/app/views/projects/commit/_pipeline.html.haml
+++ /dev/null
@@ -1,52 +0,0 @@
-.pipeline-graph-container
- .row-content-block.build-content.middle-block.pipeline-actions
- .pull-right
- - if can?(current_user, :update_pipeline, pipeline.project)
- - if pipeline.builds.latest.failed.any?(&:retryable?)
- = 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
-
- .oneline.clearfix
- - if defined?(pipeline_details) && pipeline_details
- Pipeline
- = link_to "##{pipeline.id}", namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: "monospace"
- with
- = pluralize pipeline.statuses.count(:id), "job"
- - if pipeline.ref
- for
- = link_to pipeline.ref, namespace_project_commits_path(pipeline.project.namespace, pipeline.project, pipeline.ref), class: "monospace"
- - if defined?(link_to_commit) && link_to_commit
- for commit
- = link_to pipeline.short_sha, namespace_project_commit_path(pipeline.project.namespace, pipeline.project, pipeline.sha), class: "monospace"
- - if pipeline.duration
- in
- = time_interval_in_words pipeline.duration
-
- .row-content-block.build-content.middle-block.js-pipeline-graph.hidden
- = render "projects/pipelines/graph", pipeline: pipeline
-
-- if pipeline.yaml_errors.present?
- .bs-callout.bs-callout-danger
- %h4 Found errors in your .gitlab-ci.yml:
- %ul
- - pipeline.yaml_errors.split(",").each do |error|
- %li= error
- You can also test your .gitlab-ci.yml in the #{link_to "Lint", ci_lint_path}
-
-- if pipeline.project.builds_enabled? && !pipeline.ci_yaml_file
- .bs-callout.bs-callout-warning
- \.gitlab-ci.yml not found in this commit
-
-.table-holder.pipeline-holder
- %table.table.ci-table.pipeline
- %thead
- %tr
- %th Status
- %th Job ID
- %th Name
- %th
- %th Coverage
- %th
- = render partial: "projects/stage/stage", collection: pipeline.stages, as: :stage
diff --git a/app/views/projects/commit/branches.html.haml b/app/views/projects/commit/branches.html.haml
index 2b0c9a4b4de..911c9ddce06 100644
--- a/app/views/projects/commit/branches.html.haml
+++ b/app/views/projects/commit/branches.html.haml
@@ -1,15 +1,15 @@
-- if @branches.any?
- %span
- - branch = commit_default_branch(@project, @branches)
- = link_to(namespace_project_tree_path(@project.namespace, @project, branch)) do
- %span.label.label-gray
- = branch
- - if @branches.any? || @tags.any?
- = link_to("#", class: "js-details-expand") do
- %span.label.label-gray
- \...
+- if @branches.any? || @tags.any?
+ - branch = commit_default_branch(@project, @branches)
+ = link_to(project_ref_path(@project, branch), class: "label label-gray ref-name") do
+ = icon('code-fork')
+ = branch
+
+ -# `commit_default_branch` deletes the default branch from `@branches`,
+ -# so only render this if we have more branches left
+ - if @branches.any? || @tags.any?
+ %span
+ = link_to "…", "#", class: "js-details-expand label label-gray"
+
%span.js-details-content.hide
- - if @branches.any?
- = commit_branches_links(@project, @branches)
- - if @tags.any?
- = commit_tags_links(@project, @tags)
+ = commit_branches_links(@project, @branches) if @branches.any?
+ = commit_tags_links(@project, @tags) if @tags.any?
diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml
index 8f32d2b72e5..3350a0ec152 100644
--- a/app/views/projects/commits/_commit.html.haml
+++ b/app/views/projects/commits/_commit.html.haml
@@ -37,6 +37,6 @@
.commit-actions.flex-row.hidden-xs
- if commit.status(ref)
= render_commit_status(commit, ref: ref)
+ = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit-sha btn btn-transparent"
= clipboard_button(text: commit.id, title: "Copy commit SHA to clipboard")
- = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit-short-id btn btn-transparent"
= link_to_browse_code(project, commit)
diff --git a/app/views/projects/commits/_inline_commit.html.haml b/app/views/projects/commits/_inline_commit.html.haml
index c03bc3f9df9..5fb89935467 100644
--- a/app/views/projects/commits/_inline_commit.html.haml
+++ b/app/views/projects/commits/_inline_commit.html.haml
@@ -1,6 +1,6 @@
%li.commit.inline-commit
.commit-row-title
- = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit_short_id"
+ = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit-sha"
&nbsp;
%span.str-truncated
= link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit.id), class: "commit-row-message"
diff --git a/app/views/projects/compare/_form.html.haml b/app/views/projects/compare/_form.html.haml
index 1f4c9fac54c..adb724c1b8d 100644
--- a/app/views/projects/compare/_form.html.haml
+++ b/app/views/projects/compare/_form.html.haml
@@ -7,7 +7,7 @@
.input-group.inline-input-group
%span.input-group-addon from
= hidden_field_tag :from, params[:from]
- = button_tag type: 'button', title: params[:from], class: "form-control compare-dropdown-toggle js-compare-dropdown has-tooltip", required: true, data: { refs_url: refs_namespace_project_path(@project.namespace, @project), toggle: "dropdown", target: ".js-compare-from-dropdown", selected: params[:from], field_name: :from } do
+ = button_tag type: 'button', title: params[:from], class: "form-control compare-dropdown-toggle js-compare-dropdown has-tooltip git-revision-dropdown-toggle", required: true, data: { refs_url: refs_namespace_project_path(@project.namespace, @project), toggle: "dropdown", target: ".js-compare-from-dropdown", selected: params[:from], field_name: :from } do
.dropdown-toggle-text.str-truncated= params[:from] || 'Select branch/tag'
= render 'shared/ref_dropdown'
.compare-ellipsis.inline ...
@@ -15,7 +15,7 @@
.input-group.inline-input-group
%span.input-group-addon to
= hidden_field_tag :to, params[:to]
- = button_tag type: 'button', title: params[:to], class: "form-control compare-dropdown-toggle js-compare-dropdown has-tooltip", required: true, data: { refs_url: refs_namespace_project_path(@project.namespace, @project), toggle: "dropdown", target: ".js-compare-to-dropdown", selected: params[:to], field_name: :to } do
+ = button_tag type: 'button', title: params[:to], class: "form-control compare-dropdown-toggle js-compare-dropdown has-tooltip git-revision-dropdown-toggle", required: true, data: { refs_url: refs_namespace_project_path(@project.namespace, @project), toggle: "dropdown", target: ".js-compare-to-dropdown", selected: params[:to], field_name: :to } do
.dropdown-toggle-text.str-truncated= params[:to] || 'Select branch/tag'
= render 'shared/ref_dropdown'
&nbsp;
diff --git a/app/views/projects/compare/index.html.haml b/app/views/projects/compare/index.html.haml
index 45be6581cfc..2cf14859f30 100644
--- a/app/views/projects/compare/index.html.haml
+++ b/app/views/projects/compare/index.html.haml
@@ -6,10 +6,10 @@
.sub-header-block
Compare Git revisions.
%br
- Fill input field with commit id like
- %code.label-branch 4eedf23
+ Fill input field with commit SHA like
+ %code.ref-name 4eedf23
or branch/tag name like
- %code.label-branch master
+ %code.ref-name master
and press compare button for the commits list and a code diff.
%br
Changes are shown <b>from</b> the version in the first field <b>to</b> the version in the second field.
diff --git a/app/views/projects/compare/show.html.haml b/app/views/projects/compare/show.html.haml
index 0dfc9fe20ed..a1bca2cf83a 100644
--- a/app/views/projects/compare/show.html.haml
+++ b/app/views/projects/compare/show.html.haml
@@ -16,9 +16,9 @@
There isn't anything to compare.
%p.slead
- if params[:to] == params[:from]
- %span.label-branch= params[:from]
+ %span.ref-name= params[:from]
and
- %span.label-branch= params[:to]
+ %span.ref-name= params[:to]
are the same.
- else
You'll need to use different branch names to get a valid comparison.
diff --git a/app/views/projects/cycle_analytics/show.html.haml b/app/views/projects/cycle_analytics/show.html.haml
index b158a81471c..74255167352 100644
--- a/app/views/projects/cycle_analytics/show.html.haml
+++ b/app/views/projects/cycle_analytics/show.html.haml
@@ -51,7 +51,7 @@
%ul
%li.stage-header
%span.stage-name
- {{ __('ProjectLifecycle|Stage') }}
+ {{ s__('ProjectLifecycle|Stage') }}
%i.has-tooltip.fa.fa-question-circle{ "data-placement" => "top", title: _("The phase of the development lifecycle."), "aria-hidden" => "true" }
%li.median-header
%span.stage-name
diff --git a/app/views/projects/deployments/_commit.html.haml b/app/views/projects/deployments/_commit.html.haml
index 170d786ecbf..31fd982c522 100644
--- a/app/views/projects/deployments/_commit.html.haml
+++ b/app/views/projects/deployments/_commit.html.haml
@@ -2,10 +2,10 @@
- if deployment.ref
.icon-container
= deployment.tag? ? icon('tag') : icon('code-fork')
- = link_to deployment.ref, namespace_project_commits_path(@project.namespace, @project, deployment.ref), class: "monospace branch-name"
+ = link_to deployment.ref, project_ref_path(@project, deployment.ref), class: "ref-name"
.icon-container.commit-icon
= custom_icon("icon_commit")
- = link_to deployment.short_sha, namespace_project_commit_path(@project.namespace, @project, deployment.sha), class: "commit-id monospace"
+ = link_to deployment.short_sha, namespace_project_commit_path(@project.namespace, @project, deployment.sha), class: "commit-sha"
%p.commit-title
%span
diff --git a/app/views/projects/diffs/_file_header.html.haml b/app/views/projects/diffs/_file_header.html.haml
index 7d6b3701f95..4e4fdb73ae3 100644
--- a/app/views/projects/diffs/_file_header.html.haml
+++ b/app/views/projects/diffs/_file_header.html.haml
@@ -1,4 +1,8 @@
-%i.fa.diff-toggle-caret.fa-fw
+- show_toggle = local_assigns.fetch(:show_toggle, true)
+
+- if show_toggle
+ %i.fa.diff-toggle-caret.fa-fw
+
- if defined?(blob) && blob && diff_file.submodule?
%span
= icon('archive fw')
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index 160345cfaa5..dd27e0866de 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -40,8 +40,8 @@
.form_group.prepend-top-20.sharing-and-permissions
.row.js-visibility-select
.col-md-9
- %label.label-light
- = label_tag :project_visibility, 'Project Visibility', class: 'label-light'
+ .label-light
+ = label_tag :project_visibility, 'Project Visibility', class: 'label-light', for: :project_visibility_level
= link_to "(?)", help_page_path("public_access/public_access")
%span.help-block
.col-md-3.visibility-select-container
@@ -65,7 +65,7 @@
.row
.col-md-9.project-feature.nested
= feature_fields.label :builds_access_level, "Pipelines", class: 'label-light'
- %span.help-block Submit, test and deploy your changes before merge
+ %span.help-block Build, test, and deploy your changes
.col-md-3
= project_feature_access_select(:builds_access_level)
diff --git a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml
index f458646522c..b23bbadbdb4 100644
--- a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml
+++ b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml
@@ -27,7 +27,7 @@
= custom_icon("icon_commit")
- if commit_sha
- = link_to generic_commit_status.short_sha, namespace_project_commit_path(generic_commit_status.project.namespace, generic_commit_status.project, generic_commit_status.sha), class: "commit-id monospace"
+ = link_to generic_commit_status.short_sha, namespace_project_commit_path(generic_commit_status.project.namespace, generic_commit_status.project, generic_commit_status.sha), class: "commit-sha"
- if retried
= icon('warning', class: 'text-warning has-tooltip', title: 'Status was retried.')
@@ -48,7 +48,7 @@
- if generic_commit_status.pipeline.user
= user_avatar(user: generic_commit_status.pipeline.user, size: 20)
- else
- %span.monospace API
+ %span.api API
- if admin
%td
diff --git a/app/views/projects/imports/new.html.haml b/app/views/projects/imports/new.html.haml
index 2cd8d03e30e..25a87411cac 100644
--- a/app/views/projects/imports/new.html.haml
+++ b/app/views/projects/imports/new.html.haml
@@ -10,7 +10,7 @@
.panel-body
%pre
:preserve
- #{sanitize_repo_path(@project, @project.import_error)}
+ #{h(sanitize_repo_path(@project, @project.import_error))}
= form_for @project, url: namespace_project_import_path(@project.namespace, @project), method: :post, html: { class: 'form-horizontal' } do |f|
= render "shared/import_form", f: f
diff --git a/app/views/projects/issues/_new_branch.html.haml b/app/views/projects/issues/_new_branch.html.haml
index 6bc6bf76e18..dba092c8844 100644
--- a/app/views/projects/issues/_new_branch.html.haml
+++ b/app/views/projects/issues/_new_branch.html.haml
@@ -17,7 +17,7 @@
.description
%strong Create a merge request
%span
- Creates a branch named after this issue and a merge request. The source branch is '#{@project.default_branch}' by default.
+ Creates a merge request named after this issue, with source branch created from '#{@project.default_branch}'.
%li.divider.droplab-item-ignore
%li{ role: 'button', data: { value: 'create-branch', 'text' => 'Create a branch' } }
.menu-item
@@ -26,4 +26,4 @@
.description
%strong Create a branch
%span
- Creates a branch named after this issue. The source branch is '#{@project.default_branch}' by default.
+ Creates a branch named after this issue, from '#{@project.default_branch}'.
diff --git a/app/views/projects/issues/_related_branches.html.haml b/app/views/projects/issues/_related_branches.html.haml
index 1892ebb512f..8c9f6f3b4df 100644
--- a/app/views/projects/issues/_related_branches.html.haml
+++ b/app/views/projects/issues/_related_branches.html.haml
@@ -11,5 +11,4 @@
= render_pipeline_status(pipeline)
%span.related-branch-info
%strong
- = link_to namespace_project_compare_path(@project.namespace, @project, from: @project.default_branch, to: branch), class: "label-branch" do
- = branch
+ = link_to branch, namespace_project_compare_path(@project.namespace, @project, from: @project.default_branch, to: branch), class: "ref-name"
diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml
index 4ac0bc1d028..60900e9d660 100644
--- a/app/views/projects/issues/index.html.haml
+++ b/app/views/projects/issues/index.html.haml
@@ -7,7 +7,8 @@
= render "projects/issues/head"
- content_for :page_specific_javascripts do
- = page_specific_javascript_bundle_tag('filtered_search')
+ = webpack_bundle_tag 'common_vue'
+ = webpack_bundle_tag 'filtered_search'
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, params.merge(rss_url_options), title: "#{@project.name} issues")
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index 9084883eb3e..b2401442620 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -51,12 +51,17 @@
.issue-details.issuable-details
.detail-page-description.content-block
- .issue-title-data.hidden{ "data" => { "endpoint" => rendered_title_namespace_project_issue_path(@project.namespace, @project, @issue),
- "can-update-tasks-class" => can?(current_user, :update_issue, @issue) ? 'js-task-list-container' : '',
+ #js-issuable-app{ "data" => { "endpoint" => realtime_changes_namespace_project_issue_path(@project.namespace, @project, @issue),
+ "can-update" => can?(current_user, :update_issue, @issue).to_s,
+ "issuable-ref" => @issue.to_reference,
} }
- .issue-title-entrypoint
+ %h2.title= markdown_field(@issue, :title)
+ - if @issue.description.present?
+ .description{ class: can?(current_user, :update_issue, @issue) ? 'js-task-list-container' : '' }
+ .wiki= markdown_field(@issue, :description)
+ %textarea.hidden.js-task-list-field= @issue.description
- = edited_time_ago_with_tooltip(@issue, placement: 'bottom', html_class: 'issue_edited_ago')
+ = edited_time_ago_with_tooltip(@issue, placement: 'bottom', html_class: 'issue-edited-ago js-issue-edited-ago')
#merge-requests{ data: { url: referenced_merge_requests_namespace_project_issue_url(@project.namespace, @project, @issue) } }
// This element is filled in using JavaScript.
diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml
index 11b7aaec704..94b9577e9eb 100644
--- a/app/views/projects/merge_requests/_merge_request.html.haml
+++ b/app/views/projects/merge_requests/_merge_request.html.haml
@@ -37,7 +37,7 @@
by #{link_to_member(@project, merge_request.author, avatar: false)}
- if merge_request.target_project.default_branch != merge_request.target_branch
&nbsp;
- = link_to namespace_project_commits_path(merge_request.project.namespace, merge_request.project, merge_request.target_branch) do
+ = link_to project_ref_path(merge_request.project, merge_request.target_branch), class: 'ref-name' do
= icon('code-fork')
= merge_request.target_branch
diff --git a/app/views/projects/merge_requests/_new_compare.html.haml b/app/views/projects/merge_requests/_new_compare.html.haml
index 9cf24e10842..0f37abb579c 100644
--- a/app/views/projects/merge_requests/_new_compare.html.haml
+++ b/app/views/projects/merge_requests/_new_compare.html.haml
@@ -21,8 +21,8 @@
selected: f.object.source_project_id
.merge-request-select.dropdown
= f.hidden_field :source_branch
- = dropdown_toggle f.object.source_branch || "Select source branch", { toggle: "dropdown", field_name: "#{f.object_name}[source_branch]" }, { toggle_class: "js-compare-dropdown js-source-branch" }
- .dropdown-menu.dropdown-menu-selectable.dropdown-source-branch
+ = dropdown_toggle f.object.source_branch || "Select source branch", { toggle: "dropdown", field_name: "#{f.object_name}[source_branch]" }, { toggle_class: "js-compare-dropdown js-source-branch git-revision-dropdown-toggle" }
+ .dropdown-menu.dropdown-menu-selectable.dropdown-source-branch.git-revision-dropdown
= dropdown_title("Select source branch")
= dropdown_filter("Search branches")
= dropdown_content do
@@ -51,8 +51,8 @@
selected: f.object.target_project_id
.merge-request-select.dropdown
= f.hidden_field :target_branch
- = dropdown_toggle f.object.target_branch, { toggle: "dropdown", field_name: "#{f.object_name}[target_branch]" }, { toggle_class: "js-compare-dropdown js-target-branch" }
- .dropdown-menu.dropdown-menu-selectable.dropdown-target-branch.js-target-branch-dropdown
+ = dropdown_toggle f.object.target_branch, { toggle: "dropdown", field_name: "#{f.object_name}[target_branch]" }, { toggle_class: "js-compare-dropdown js-target-branch git-revision-dropdown-toggle" }
+ .dropdown-menu.dropdown-menu-selectable.dropdown-target-branch.js-target-branch-dropdown.git-revision-dropdown
= dropdown_title("Select target branch")
= dropdown_filter("Search branches")
= dropdown_content do
diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml
index da79ca2ee75..e3ecbee5490 100644
--- a/app/views/projects/merge_requests/_new_submit.html.haml
+++ b/app/views/projects/merge_requests/_new_submit.html.haml
@@ -3,9 +3,9 @@
%p.slead
- source_title, target_title = format_mr_branch_names(@merge_request)
From
- %strong.label-branch= source_title
+ %strong.ref-name= source_title
%span into
- %strong.label-branch= target_title
+ %strong.ref-name= target_title
%span.pull-right
= link_to 'Change branches', mr_change_branches_path(@merge_request)
diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml
index 25b8567b78f..75120409bb3 100644
--- a/app/views/projects/merge_requests/_show.html.haml
+++ b/app/views/projects/merge_requests/_show.html.haml
@@ -21,46 +21,49 @@
#js-vue-mr-widget.mr-widget
- content_for :page_specific_javascripts do
- = page_specific_javascript_bundle_tag('vue_merge_request_widget')
+ = webpack_bundle_tag 'common_vue'
+ = webpack_bundle_tag 'vue_merge_request_widget'
.content-block.content-block-small.emoji-list-container
= render 'award_emoji/awards_block', awardable: @merge_request, inline: true
.merge-request-tabs-holder{ class: ("js-tabs-affix" unless ENV['RAILS_ENV'] == 'test') }
- .merge-request-tabs-container.scrolling-tabs-container.inner-page-scroll-tabs
- .fade-left= icon('angle-left')
- .fade-right= icon('angle-right')
- %ul.merge-request-tabs.nav-links.scrolling-tabs
- %li.notes-tab
- = link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#notes', action: 'notes', toggle: 'tab' } do
- Discussion
- %span.badge= @merge_request.related_notes.user.count
- - if @merge_request.source_project
- %li.commits-tab
- = link_to commits_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#commits', action: 'commits', toggle: 'tab' } do
- Commits
- %span.badge= @commits_count
- - if @pipelines.any?
- %li.pipelines-tab
- = link_to pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: '#pipelines', action: 'pipelines', toggle: 'tab' } do
- Pipelines
- %span.badge= @pipelines.size
- %li.diffs-tab
- = link_to diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#diffs', action: 'diffs', toggle: 'tab' } do
- Changes
- %span.badge= @merge_request.diff_size
- %li#resolve-count-app.line-resolve-all-container.pull-right.prepend-top-10.hidden-xs{ "v-cloak" => true }
- %resolve-count{ "inline-template" => true, ":logged-out" => "#{current_user.nil?}" }
- %div
- .line-resolve-all{ "v-show" => "discussionCount > 0",
- ":class" => "{ 'has-next-btn': !loggedOut && resolvedDiscussionCount !== discussionCount }" }
- %span.line-resolve-btn.is-disabled{ type: "button",
- ":class" => "{ 'is-active': resolvedDiscussionCount === discussionCount }" }
- = render "shared/icons/icon_status_success.svg"
- %span.line-resolve-text
- {{ resolvedDiscussionCount }}/{{ discussionCount }} {{ resolvedCountText }} resolved
- = render "discussions/new_issue_for_all_discussions", merge_request: @merge_request
- = render "discussions/jump_to_next"
+ .merge-request-tabs-container
+ .scrolling-tabs-container.inner-page-scroll-tabs.is-smaller
+ .fade-left= icon('angle-left')
+ .fade-right= icon('angle-right')
+ .nav-links.scrolling-tabs
+ %ul.merge-request-tabs
+ %li.notes-tab
+ = link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#notes', action: 'notes', toggle: 'tab' } do
+ Discussion
+ %span.badge= @merge_request.related_notes.user.count
+ - if @merge_request.source_project
+ %li.commits-tab
+ = link_to commits_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#commits', action: 'commits', toggle: 'tab' } do
+ Commits
+ %span.badge= @commits_count
+ - if @pipelines.any?
+ %li.pipelines-tab
+ = link_to pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: '#pipelines', action: 'pipelines', toggle: 'tab' } do
+ Pipelines
+ %span.badge= @pipelines.size
+ %li.diffs-tab
+ = link_to diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#diffs', action: 'diffs', toggle: 'tab' } do
+ Changes
+ %span.badge= @merge_request.diff_size
+ #resolve-count-app.line-resolve-all-container.prepend-top-10{ "v-cloak" => true }
+ %resolve-count{ "inline-template" => true, ":logged-out" => "#{current_user.nil?}" }
+ %div
+ .line-resolve-all{ "v-show" => "discussionCount > 0",
+ ":class" => "{ 'has-next-btn': !loggedOut && resolvedDiscussionCount !== discussionCount }" }
+ %span.line-resolve-btn.is-disabled{ type: "button",
+ ":class" => "{ 'is-active': resolvedDiscussionCount === discussionCount }" }
+ = render "shared/icons/icon_status_success.svg"
+ %span.line-resolve-text
+ {{ resolvedDiscussionCount }}/{{ discussionCount }} {{ resolvedCountText }} resolved
+ = render "discussions/new_issue_for_all_discussions", merge_request: @merge_request
+ = render "discussions/jump_to_next"
.tab-content#diff-notes-app
#notes.notes.tab-pane.voting_notes
diff --git a/app/views/projects/merge_requests/index.html.haml b/app/views/projects/merge_requests/index.html.haml
index 6bf0035e051..502220232a1 100644
--- a/app/views/projects/merge_requests/index.html.haml
+++ b/app/views/projects/merge_requests/index.html.haml
@@ -8,7 +8,8 @@
= render 'projects/last_push'
- content_for :page_specific_javascripts do
- = page_specific_javascript_bundle_tag('filtered_search')
+ = webpack_bundle_tag 'common_vue'
+ = webpack_bundle_tag 'filtered_search'
- if @project.merge_requests.exists?
%div{ class: container_class }
diff --git a/app/views/projects/merge_requests/show/_how_to_merge.html.haml b/app/views/projects/merge_requests/show/_how_to_merge.html.haml
index f3372c7657f..766cb272bec 100644
--- a/app/views/projects/merge_requests/show/_how_to_merge.html.haml
+++ b/app/views/projects/merge_requests/show/_how_to_merge.html.haml
@@ -49,7 +49,7 @@
%strong Tip:
= succeed '.' do
You can also checkout merge requests locally by
- = link_to 'following these guidelines', help_page_path('user/project/merge_requests.md', anchor: "checkout-merge-requests-locally"), target: '_blank', rel: 'noopener noreferrer'
+ = link_to 'following these guidelines', help_page_path('user/project/merge_requests/index.md', anchor: "checkout-merge-requests-locally"), target: '_blank', rel: 'noopener noreferrer'
:javascript
$(function(){
diff --git a/app/views/projects/merge_requests/show/_versions.html.haml b/app/views/projects/merge_requests/show/_versions.html.haml
index 11b0c55be0b..37117bc64a3 100644
--- a/app/views/projects/merge_requests/show/_versions.html.haml
+++ b/app/views/projects/merge_requests/show/_versions.html.haml
@@ -20,25 +20,27 @@
- @merge_request_diffs.each do |merge_request_diff|
%li
= link_to merge_request_version_path(@project, @merge_request, merge_request_diff, @start_sha), class: ('is-active' if merge_request_diff == @merge_request_diff) do
- %strong
- - if merge_request_diff.latest?
- latest version
- - else
- version #{version_index(merge_request_diff)}
- .monospace= short_sha(merge_request_diff.head_commit_sha)
- %small
- #{number_with_delimiter(merge_request_diff.commits_count)} #{'commit'.pluralize(merge_request_diff.commits_count)},
- = time_ago_with_tooltip(merge_request_diff.created_at)
+ %div
+ %strong
+ - if merge_request_diff.latest?
+ latest version
+ - else
+ version #{version_index(merge_request_diff)}
+ %div
+ %small.commit-sha= short_sha(merge_request_diff.head_commit_sha)
+ %div
+ %small
+ #{number_with_delimiter(merge_request_diff.commits_count)} #{'commit'.pluralize(merge_request_diff.commits_count)},
+ = time_ago_with_tooltip(merge_request_diff.created_at)
- if @merge_request_diff.base_commit_sha
and
%span.dropdown.inline.mr-version-compare-dropdown
%a.btn.btn-default.dropdown-toggle{ data: {toggle: :dropdown} }
- %span
- - if @start_version
- version #{version_index(@start_version)}
- - else
- #{@merge_request.target_branch}
+ - if @start_version
+ version #{version_index(@start_version)}
+ - else
+ %span.ref-name= @merge_request.target_branch
= icon('caret-down')
.dropdown-menu.dropdown-select.dropdown-menu-selectable
.dropdown-title
@@ -50,19 +52,25 @@
- @comparable_diffs.each do |merge_request_diff|
%li
= link_to merge_request_version_path(@project, @merge_request, @merge_request_diff, merge_request_diff.head_commit_sha), class: ('is-active' if merge_request_diff == @start_version) do
- %strong
- - if merge_request_diff.latest?
- latest version
- - else
- version #{version_index(merge_request_diff)}
- .monospace= short_sha(merge_request_diff.head_commit_sha)
- %small
- = time_ago_with_tooltip(merge_request_diff.created_at)
+ %div
+ %strong
+ - if merge_request_diff.latest?
+ latest version
+ - else
+ version #{version_index(merge_request_diff)}
+ %div
+ %small.commit-sha= short_sha(merge_request_diff.head_commit_sha)
+ %div
+ %small
+ = time_ago_with_tooltip(merge_request_diff.created_at)
%li
= link_to merge_request_version_path(@project, @merge_request, @merge_request_diff), class: ('is-active' unless @start_version) do
- %strong
- #{@merge_request.target_branch} (base)
- .monospace= short_sha(@merge_request_diff.base_commit_sha)
+ %div
+ %strong
+ %span.ref-name= @merge_request.target_branch
+ (base)
+ %div
+ %strong.commit-sha= short_sha(@merge_request_diff.base_commit_sha)
- if different_base?(@start_version, @merge_request_diff)
.content-block
diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml
index 9e292729425..e180cb8bad1 100644
--- a/app/views/projects/new.html.haml
+++ b/app/views/projects/new.html.haml
@@ -30,7 +30,7 @@
#{root_url}#{current_user.username}/
= f.hidden_field :namespace_id, value: current_user.namespace_id
.form-group.col-xs-12.col-sm-6.project-path
- = f.label :namespace_id, class: 'label-light' do
+ = f.label :path, class: 'label-light' do
%span
Project name
= f.text_field :path, placeholder: "my-awesome-project", class: "form-control", tabindex: 2, autofocus: true, required: true
diff --git a/app/views/projects/pipeline_schedules/_form.html.haml b/app/views/projects/pipeline_schedules/_form.html.haml
index 4a21cce024e..bbed10039af 100644
--- a/app/views/projects/pipeline_schedules/_form.html.haml
+++ b/app/views/projects/pipeline_schedules/_form.html.haml
@@ -1,32 +1,33 @@
- content_for :page_specific_javascripts do
- = page_specific_javascript_bundle_tag('schedule_form')
+ = webpack_bundle_tag 'common_vue'
+ = webpack_bundle_tag 'schedule_form'
= form_for [@project.namespace.becomes(Namespace), @project, @schedule], as: :schedule, html: { id: "new-pipeline-schedule-form", class: "form-horizontal js-pipeline-schedule-form" } do |f|
= form_errors(@schedule)
.form-group
- .col-md-6
+ .col-md-9
= f.label :description, 'Description', class: 'label-light'
= f.text_field :description, class: 'form-control', required: true, autofocus: true, placeholder: 'Provide a short description for this pipeline'
.form-group
- .col-md-12
+ .col-md-9
= f.label :cron, 'Interval Pattern', class: 'label-light'
#interval-pattern-input{ data: { initial_interval: @schedule.cron } }
.form-group
- .col-md-6
+ .col-md-9
= f.label :cron_timezone, 'Cron Timezone', class: 'label-light'
= dropdown_tag("Select a timezone", options: { toggle_class: 'btn js-timezone-dropdown', title: "Select a timezone", filter: true, placeholder: "Filter", data: { data: timezone_data } } )
= f.text_field :cron_timezone, value: @schedule.cron_timezone, id: 'schedule_cron_timezone', class: 'hidden', name: 'schedule[cron_timezone]', required: true
.form-group
- .col-md-6
+ .col-md-9
= f.label :ref, 'Target Branch', class: 'label-light'
- = dropdown_tag("Select target branch", options: { toggle_class: 'btn js-target-branch-dropdown', title: "Select target branch", filter: true, placeholder: "Filter", data: { data: @project.repository.branch_names } } )
+ = dropdown_tag("Select target branch", options: { toggle_class: 'btn js-target-branch-dropdown git-revision-dropdown-toggle', dropdown_class: 'git-revision-dropdown', title: "Select target branch", filter: true, placeholder: "Filter", data: { data: @project.repository.branch_names, default_branch: @project.default_branch } } )
= f.text_field :ref, value: @schedule.ref, id: 'schedule_ref', class: 'hidden', name: 'schedule[ref]', required: true
.form-group
- .col-md-6
+ .col-md-9
= f.label :active, 'Activated', class: 'label-light'
%div
= f.check_box :active, required: false, value: @schedule.active?
- active
+ Active
.footer-block.row-content-block
= f.submit 'Save pipeline schedule', class: 'btn btn-create', tabindex: 3
= link_to 'Cancel', pipeline_schedules_path(@project), class: 'btn btn-cancel'
diff --git a/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml b/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml
index 1406868488f..2cd82e1b661 100644
--- a/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml
+++ b/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml
@@ -4,12 +4,13 @@
= pipeline_schedule.description
%td.branch-name-cell
= icon('code-fork')
- = link_to pipeline_schedule.ref, namespace_project_commits_path(@project.namespace, @project, pipeline_schedule.ref), class: "branch-name"
+ = link_to pipeline_schedule.ref, project_ref_path(@project, pipeline_schedule.ref), class: "ref-name"
%td
- if pipeline_schedule.last_pipeline
.status-icon-container{ class: "ci-status-icon-#{pipeline_schedule.last_pipeline.status}" }
= link_to namespace_project_pipeline_path(@project.namespace, @project, pipeline_schedule.last_pipeline.id) do
= ci_icon_for_status(pipeline_schedule.last_pipeline.status)
+ %span ##{pipeline_schedule.last_pipeline.id}
- else
None
%td.next-run-cell
diff --git a/app/views/projects/pipeline_schedules/index.html.haml b/app/views/projects/pipeline_schedules/index.html.haml
index dd35c3055f2..25c52175e3d 100644
--- a/app/views/projects/pipeline_schedules/index.html.haml
+++ b/app/views/projects/pipeline_schedules/index.html.haml
@@ -1,12 +1,13 @@
- content_for :page_specific_javascripts do
- = page_specific_javascript_bundle_tag('schedules_index')
+ = webpack_bundle_tag 'common_vue'
+ = webpack_bundle_tag 'schedules_index'
- @no_container = true
- page_title "Pipeline Schedules"
= render "projects/pipelines/head"
%div{ class: container_class }
- #scheduling-pipelines-callout
+ #pipeline-schedules-callout{ data: { docs_url: help_page_path('ci/pipeline_schedules') } }
.top-area
- schedule_path_proc = ->(scope) { pipeline_schedules_path(@project, scope: scope) }
= render "tabs", schedule_path_proc: schedule_path_proc, all_schedules: @all_schedules, scope: @scope
@@ -21,4 +22,3 @@
- else
.light-well
.nothing-here-block No schedules
-
diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml
index ab6baaf35b6..8607da8fcdd 100644
--- a/app/views/projects/pipelines/_info.html.haml
+++ b/app/views/projects/pipelines/_info.html.haml
@@ -30,7 +30,7 @@
= pluralize @pipeline.statuses.count(:id), "job"
- if @pipeline.ref
from
- = link_to @pipeline.ref, namespace_project_commits_path(@project.namespace, @project, @pipeline.ref), class: "monospace"
+ = link_to @pipeline.ref, project_ref_path(@project, @pipeline.ref), class: "ref-name"
- if @pipeline.duration
in
= time_interval_in_words(@pipeline.duration)
@@ -40,10 +40,10 @@
.well-segment.branch-info
.icon-container.commit-icon
= custom_icon("icon_commit")
- = link_to @commit.short_id, namespace_project_commit_path(@project.namespace, @project, @pipeline.sha), class: "monospace js-details-short"
+ = link_to @commit.short_id, namespace_project_commit_path(@project.namespace, @project, @pipeline.sha), class: "commit-sha js-details-short"
= link_to("#", class: "js-details-expand hidden-xs hidden-sm") do
%span.text-expander
\...
%span.js-details-content.hide
- = link_to @pipeline.sha, namespace_project_commit_path(@project.namespace, @project, @pipeline.sha), class: "monospace commit-hash-full"
+ = link_to @pipeline.sha, namespace_project_commit_path(@project.namespace, @project, @pipeline.sha), class: "commit-sha commit-hash-full"
= clipboard_button(text: @pipeline.sha, title: "Copy commit SHA to clipboard")
diff --git a/app/views/projects/pipelines/new.html.haml b/app/views/projects/pipelines/new.html.haml
index 14a270a3039..71a8e490c3e 100644
--- a/app/views/projects/pipelines/new.html.haml
+++ b/app/views/projects/pipelines/new.html.haml
@@ -11,8 +11,8 @@
.col-sm-10
= 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",
+ options: { toggle_class: 'js-branch-select wide git-revision-dropdown-toggle',
+ filter: true, dropdown_class: "dropdown-menu-selectable git-revision-dropdown", placeholder: "Search branches",
data: { selected: params[:ref] || @project.default_branch, field_name: 'pipeline[ref]' } })
.help-block Existing branch name, tag
.form-actions
diff --git a/app/views/projects/protected_branches/_dropdown.html.haml b/app/views/projects/protected_branches/_dropdown.html.haml
index 5af0cc7a2f3..6e9c473494e 100644
--- a/app/views/projects/protected_branches/_dropdown.html.haml
+++ b/app/views/projects/protected_branches/_dropdown.html.haml
@@ -1,8 +1,8 @@
= f.hidden_field(:name)
= dropdown_tag('Select branch or create wildcard',
- options: { toggle_class: 'js-protected-branch-select js-filter-submit wide',
- filter: true, dropdown_class: "dropdown-menu-selectable", placeholder: "Search protected branches",
+ options: { toggle_class: 'js-protected-branch-select js-filter-submit wide git-revision-dropdown-toggle',
+ filter: true, dropdown_class: "dropdown-menu-selectable git-revision-dropdown", placeholder: "Search protected branches",
footer_content: true,
data: { show_no: true, show_any: true, show_upcoming: true,
selected: params[:protected_branch_name],
diff --git a/app/views/projects/protected_branches/_matching_branch.html.haml b/app/views/projects/protected_branches/_matching_branch.html.haml
index 8a5332ca5bb..27896272733 100644
--- a/app/views/projects/protected_branches/_matching_branch.html.haml
+++ b/app/views/projects/protected_branches/_matching_branch.html.haml
@@ -1,9 +1,10 @@
%tr
%td
- = link_to matching_branch.name, namespace_project_tree_path(@project.namespace, @project, matching_branch.name)
+ = link_to matching_branch.name, project_ref_path(@project, matching_branch.name), class: 'ref-name'
+
- if @project.root_ref?(matching_branch.name)
%span.label.label-info.prepend-left-5 default
%td
- commit = @project.commit(matching_branch.name)
- = link_to(commit.short_id, namespace_project_commit_path(@project.namespace, @project, commit.id), class: 'commit_short_id')
+ = link_to(commit.short_id, namespace_project_commit_path(@project.namespace, @project, commit.id), class: 'commit-sha')
= time_ago_with_tooltip(commit.committed_date)
diff --git a/app/views/projects/protected_branches/_protected_branch.html.haml b/app/views/projects/protected_branches/_protected_branch.html.haml
index b2a6b8469a3..0f80de94392 100644
--- a/app/views/projects/protected_branches/_protected_branch.html.haml
+++ b/app/views/projects/protected_branches/_protected_branch.html.haml
@@ -1,6 +1,7 @@
%tr.js-protected-branch-edit-form{ data: { url: namespace_project_protected_branch_path(@project.namespace, @project, protected_branch) } }
%td
- = protected_branch.name
+ %span.ref-name= protected_branch.name
+
- if @project.root_ref?(protected_branch.name)
%span.label.label-info.prepend-left-5 default
%td
@@ -9,7 +10,7 @@
= link_to pluralize(matching_branches.count, "matching branch"), namespace_project_protected_branch_path(@project.namespace, @project, protected_branch)
- else
- if commit = protected_branch.commit
- = link_to(commit.short_id, namespace_project_commit_path(@project.namespace, @project, commit.id), class: 'commit_short_id')
+ = link_to(commit.short_id, namespace_project_commit_path(@project.namespace, @project, commit.id), class: 'commit-sha')
= time_ago_with_tooltip(commit.committed_date)
- else
(branch was removed from repository)
diff --git a/app/views/projects/protected_branches/show.html.haml b/app/views/projects/protected_branches/show.html.haml
index f8cfe5e4b11..a806a0756ec 100644
--- a/app/views/projects/protected_branches/show.html.haml
+++ b/app/views/projects/protected_branches/show.html.haml
@@ -2,7 +2,7 @@
.row.prepend-top-default.append-bottom-default
.col-lg-3
- %h4.prepend-top-0
+ %h4.prepend-top-0.ref-name
= @protected_ref.name
.col-lg-9
diff --git a/app/views/projects/protected_tags/_dropdown.html.haml b/app/views/projects/protected_tags/_dropdown.html.haml
index c50515cfe06..c8531f96f97 100644
--- a/app/views/projects/protected_tags/_dropdown.html.haml
+++ b/app/views/projects/protected_tags/_dropdown.html.haml
@@ -1,8 +1,8 @@
= f.hidden_field(:name)
= dropdown_tag('Select tag or create wildcard',
- options: { toggle_class: 'js-protected-tag-select js-filter-submit wide',
- filter: true, dropdown_class: "dropdown-menu-selectable capitalize-header", placeholder: "Search protected tag",
+ options: { toggle_class: 'js-protected-tag-select js-filter-submit wide git-revision-dropdown-toggle',
+ filter: true, dropdown_class: "dropdown-menu-selectable capitalize-header git-revision-dropdown", placeholder: "Search protected tag",
footer_content: true,
data: { show_no: true, show_any: true, show_upcoming: true,
selected: params[:protected_tag_name],
diff --git a/app/views/projects/protected_tags/_matching_tag.html.haml b/app/views/projects/protected_tags/_matching_tag.html.haml
index 97e5cd6f9d2..f17353df122 100644
--- a/app/views/projects/protected_tags/_matching_tag.html.haml
+++ b/app/views/projects/protected_tags/_matching_tag.html.haml
@@ -1,9 +1,10 @@
%tr
%td
- = link_to matching_tag.name, namespace_project_tree_path(@project.namespace, @project, matching_tag.name)
+ = link_to matching_tag.name, project_ref_path(@project, matching_tag.name), class: 'ref-name'
+
- if @project.root_ref?(matching_tag.name)
%span.label.label-info.prepend-left-5 default
%td
- commit = @project.commit(matching_tag.name)
- = link_to(commit.short_id, namespace_project_commit_path(@project.namespace, @project, commit.id), class: 'commit_short_id')
+ = link_to(commit.short_id, namespace_project_commit_path(@project.namespace, @project, commit.id), class: 'commit-sha')
= time_ago_with_tooltip(commit.committed_date)
diff --git a/app/views/projects/protected_tags/_protected_tag.html.haml b/app/views/projects/protected_tags/_protected_tag.html.haml
index 26bd3a1f5ed..54249ec0db1 100644
--- a/app/views/projects/protected_tags/_protected_tag.html.haml
+++ b/app/views/projects/protected_tags/_protected_tag.html.haml
@@ -1,6 +1,7 @@
%tr.js-protected-tag-edit-form{ data: { url: namespace_project_protected_tag_path(@project.namespace, @project, protected_tag) } }
%td
- = protected_tag.name
+ %span.ref-name= protected_tag.name
+
- if @project.root_ref?(protected_tag.name)
%span.label.label-info.prepend-left-5 default
%td
@@ -9,7 +10,7 @@
= link_to pluralize(matching_tags.count, "matching tag"), namespace_project_protected_tag_path(@project.namespace, @project, protected_tag)
- else
- if commit = protected_tag.commit
- = link_to(commit.short_id, namespace_project_commit_path(@project.namespace, @project, commit.id), class: 'commit_short_id')
+ = link_to(commit.short_id, namespace_project_commit_path(@project.namespace, @project, commit.id), class: 'commit-sha')
= time_ago_with_tooltip(commit.committed_date)
- else
(tag was removed from repository)
diff --git a/app/views/projects/protected_tags/show.html.haml b/app/views/projects/protected_tags/show.html.haml
index 63743f28b3c..94c3612a449 100644
--- a/app/views/projects/protected_tags/show.html.haml
+++ b/app/views/projects/protected_tags/show.html.haml
@@ -2,7 +2,7 @@
.row.prepend-top-default.append-bottom-default
.col-lg-3
- %h4.prepend-top-0
+ %h4.prepend-top-0.ref-name
= @protected_ref.name
.col-lg-9
diff --git a/app/views/projects/runners/_runner.html.haml b/app/views/projects/runners/_runner.html.haml
index deeadb609f6..674f87e8220 100644
--- a/app/views/projects/runners/_runner.html.haml
+++ b/app/views/projects/runners/_runner.html.haml
@@ -1,15 +1,18 @@
%li.runner{ id: dom_id(runner) }
%h4
= runner_status_icon(runner)
- %span.monospace
- - if @project_runners.include?(runner)
- = link_to runner.short_sha, runner_path(runner)
- - if runner.locked?
- = icon('lock', class: 'has-tooltip', title: 'Locked to current projects')
- %small
- = link_to edit_namespace_project_runner_path(@project.namespace, @project, runner) do
- %i.fa.fa-edit.btn
- - else
+
+ - if @project_runners.include?(runner)
+ = link_to runner.short_sha, runner_path(runner), class: 'commit-sha'
+
+ - if runner.locked?
+ = icon('lock', class: 'has-tooltip', title: 'Locked to current projects')
+
+ %small
+ = link_to edit_namespace_project_runner_path(@project.namespace, @project, runner) do
+ %i.fa.fa-edit.btn
+ - else
+ %span.commit-sha
= runner.short_sha
.pull-right
diff --git a/app/views/projects/runners/_specific_runners.html.haml b/app/views/projects/runners/_specific_runners.html.haml
index 6b8e6bd4fee..f8835454140 100644
--- a/app/views/projects/runners/_specific_runners.html.haml
+++ b/app/views/projects/runners/_specific_runners.html.haml
@@ -9,7 +9,7 @@
(checkout the #{link_to 'GitLab Runner section', 'https://about.gitlab.com/gitlab-ci/#gitlab-runner', target: '_blank'} for information on how to install it).
%li
Specify the following URL during the Runner setup:
- %code= ci_root_url(only_path: false)
+ %code= root_url(only_path: false)
%li
Use the following registration token during setup:
%code= @project.runners_token
diff --git a/app/views/projects/settings/integrations/_project_hook.html.haml b/app/views/projects/settings/integrations/_project_hook.html.haml
index 8dc276a3bec..a6640592dba 100644
--- a/app/views/projects/settings/integrations/_project_hook.html.haml
+++ b/app/views/projects/settings/integrations/_project_hook.html.haml
@@ -3,7 +3,7 @@
.col-md-8.col-lg-7
%strong.light-header= hook.url
%div
- - %w(push_events tag_push_events issues_events confidential_issues_events note_events merge_requests_events build_events pipeline_events wiki_page_events).each do |trigger|
+ - %w(push_events tag_push_events issues_events confidential_issues_events note_events merge_requests_events job_events pipeline_events wiki_page_events).each do |trigger|
- if hook.send(trigger)
%span.label.label-gray.deploy-project-label= trigger.titleize
.col-md-4.col-lg-5.text-right-lg.prepend-top-5
diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml
index d6c4195e2d0..1ca464696ed 100644
--- a/app/views/projects/show.html.haml
+++ b/app/views/projects/show.html.haml
@@ -73,11 +73,6 @@
= link_to add_special_file_path(@project, file_name: '.gitlab-ci.yml', commit_message: 'Set up auto deploy', branch_name: 'auto-deploy', context: 'autodeploy') do
Set up auto deploy
- - if @repository.commit
- %div{ class: container_class }
- .project-last-commit
- = render 'projects/last_commit', commit: @repository.commit, ref: current_ref, project: @project
-
%div{ class: container_class }
- if @project.archived?
.text-warning.center.prepend-top-20
diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml
index 4c4f3655b97..44cb734d7b9 100644
--- a/app/views/projects/tags/_tag.html.haml
+++ b/app/views/projects/tags/_tag.html.haml
@@ -2,10 +2,9 @@
- release = @releases.find { |release| release.tag == tag.name }
%li.flex-row
.row-main-content.str-truncated
- = link_to namespace_project_tag_path(@project.namespace, @project, tag.name) do
- %span.item-title
- = icon('tag')
- = tag.name
+ = link_to namespace_project_tag_path(@project.namespace, @project, tag.name), class: 'item-title ref-name' do
+ = icon('tag')
+ = tag.name
- if protected_tag?(@project, tag)
%span.label.label-success
diff --git a/app/views/projects/tags/new.html.haml b/app/views/projects/tags/new.html.haml
index 7c607d2956b..52af295bddd 100644
--- a/app/views/projects/tags/new.html.haml
+++ b/app/views/projects/tags/new.html.haml
@@ -1,4 +1,5 @@
- page_title "New Tag"
+- default_ref = params[:ref] || @project.default_branch
- if @error
.alert.alert-danger
@@ -16,20 +17,24 @@
= text_field_tag :tag_name, params[:tag_name], required: true, tabindex: 1, autofocus: true, class: 'form-control'
.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'
- .help-block Branch name or commit SHA
+ .col-sm-10.create-from
+ .dropdown
+ = hidden_field_tag :ref, default_ref
+ = button_tag type: 'button', title: default_ref, class: 'dropdown-menu-toggle wide form-control js-branch-select', required: true, data: { toggle: 'dropdown', selected: default_ref, field_name: 'ref' } do
+ .text-left.dropdown-toggle-text= default_ref
+ = render 'shared/ref_dropdown', dropdown_class: 'wide'
+ .help-block Existing branch name, tag, or commit SHA
.form-group
= label_tag :message, nil, class: 'control-label'
.col-sm-10
- = text_area_tag :message, nil, required: false, tabindex: 3, class: 'form-control', rows: 5
+ = text_area_tag :message, @message, required: false, tabindex: 3, class: 'form-control', rows: 5
.help-block Optionally, add a message to the tag.
%hr
.form-group
= label_tag :release_description, 'Release notes', class: 'control-label'
.col-sm-10
= render layout: 'projects/md_preview', locals: { url: preview_markdown_path(@project), referenced_users: true } do
- = render 'projects/zen', attr: :release_description, classes: 'note-textarea', placeholder: "Write your release notes or drag files here..."
+ = render 'projects/zen', attr: :release_description, classes: 'note-textarea', placeholder: "Write your release notes or drag files here...", current_text: @release_description
= render 'shared/notes/hints'
.help-block Optionally, add release notes to the tag. They will be stored in the GitLab database and displayed on the tags page.
.form-actions
@@ -37,9 +42,5 @@
= link_to 'Cancel', namespace_project_tags_path(@project.namespace, @project), class: 'btn btn-cancel'
:javascript
- var availableRefs = #{@project.repository.ref_names.to_json};
-
- $("#ref").autocomplete({
- source: availableRefs,
- minLength: 1
- });
+ window.gl = window.gl || { };
+ window.gl.availableRefs = #{@project.repository.ref_names.to_json};
diff --git a/app/views/projects/tags/show.html.haml b/app/views/projects/tags/show.html.haml
index e996ae3e4fc..2b81ce4b9fa 100644
--- a/app/views/projects/tags/show.html.haml
+++ b/app/views/projects/tags/show.html.haml
@@ -6,7 +6,9 @@
.top-area.multi-line
.nav-text
.title
- %span.item-title= @tag.name
+ %span.item-title.ref-name
+ = icon('tag')
+ = @tag.name
- if protected_tag?(@project, @tag)
%span.label.label-success
protected
diff --git a/app/views/projects/tree/_readme.html.haml b/app/views/projects/tree/_readme.html.haml
index 01599060844..de57cd4ba00 100644
--- a/app/views/projects/tree/_readme.html.haml
+++ b/app/views/projects/tree/_readme.html.haml
@@ -1,8 +1,9 @@
-%article.file-holder.readme-holder
- .js-file-title.file-title
- = blob_icon readme.mode, readme.name
- = link_to namespace_project_blob_path(@project.namespace, @project, tree_join(@ref, @path, readme.name)) do
- %strong
- = readme.name
- .file-content.wiki
- = markup(readme.name, readme.data)
+- if readme.rich_viewer
+ %article.file-holder.readme-holder
+ .js-file-title.file-title
+ = blob_icon readme.mode, readme.name
+ = link_to namespace_project_blob_path(@project.namespace, @project, tree_join(@ref, readme.path)) do
+ %strong
+ = readme.name
+
+ = render 'projects/blob/viewer', viewer: readme.rich_viewer, viewer_url: namespace_project_blob_path(@project.namespace, @project, tree_join(@ref, readme.path), viewer: :rich, format: :json)
diff --git a/app/views/projects/tree/show.html.haml b/app/views/projects/tree/show.html.haml
index 42700c237fc..b51955010ce 100644
--- a/app/views/projects/tree/show.html.haml
+++ b/app/views/projects/tree/show.html.haml
@@ -7,13 +7,4 @@
= render 'projects/last_push'
%div{ class: container_class }
- #tree-holder.tree-holder.clearfix
- .nav-block
- = render 'projects/tree/tree_header', tree: @tree
-
- .info-well.hidden-xs.append-bottom-default
- .well-segment
- %ul.blob-commit-info
- = render 'projects/commits/commit', commit: @commit, project: @project, ref: @ref
-
- = render 'projects/tree/tree_content', tree: @tree
+ = render 'projects/files', commit: @last_commit, project: @project, ref: @ref
diff --git a/app/views/projects/wikis/_sidebar.html.haml b/app/views/projects/wikis/_sidebar.html.haml
index 713b758727e..c2f9e65015d 100644
--- a/app/views/projects/wikis/_sidebar.html.haml
+++ b/app/views/projects/wikis/_sidebar.html.haml
@@ -1,4 +1,4 @@
-%aside.right-sidebar.right-sidebar-expanded.wiki-sidebar.js-wiki-sidebar.js-right-sidebar{ data: { "offset-top" => "102", "spy" => "affix" } }
+%aside.right-sidebar.right-sidebar-expanded.wiki-sidebar.js-wiki-sidebar.js-right-sidebar{ data: { "offset-top" => "50", "spy" => "affix" } }
.block.wiki-sidebar-header.append-bottom-default
%a.gutter-toggle.pull-right.visible-xs-block.visible-sm-block.js-sidebar-wiki-toggle{ href: "#" }
= icon('angle-double-right')
diff --git a/app/views/projects/wikis/git_access.html.haml b/app/views/projects/wikis/git_access.html.haml
index fb0efd85dcd..68862206248 100644
--- a/app/views/projects/wikis/git_access.html.haml
+++ b/app/views/projects/wikis/git_access.html.haml
@@ -28,7 +28,7 @@
%h3 Clone your wiki
%pre.dark
:preserve
- git clone #{ content_tag(:span, default_url_to_repo(@project_wiki), class: 'clone')}
+ git clone #{ content_tag(:span, h(default_url_to_repo(@project_wiki)), class: 'clone')}
cd #{h @project_wiki.path}
%h3 Start Gollum and edit locally
diff --git a/app/views/shared/_clone_panel.html.haml b/app/views/shared/_clone_panel.html.haml
index 34a4d7398bc..0992a65f7cd 100644
--- a/app/views/shared/_clone_panel.html.haml
+++ b/app/views/shared/_clone_panel.html.haml
@@ -17,7 +17,7 @@
%li
= http_clone_button(project)
- = text_field_tag :project_clone, default_url_to_repo(project), class: "js-select-on-focus form-control", readonly: true
+ = text_field_tag :project_clone, default_url_to_repo(project), class: "js-select-on-focus form-control", readonly: true, aria: { label: 'Project clone URL' }
.input-group-btn
= clipboard_button(target: '#project_clone', title: "Copy URL to clipboard")
diff --git a/app/views/shared/_ref_dropdown.html.haml b/app/views/shared/_ref_dropdown.html.haml
index 96f68c80c48..8b2a3bee407 100644
--- a/app/views/shared/_ref_dropdown.html.haml
+++ b/app/views/shared/_ref_dropdown.html.haml
@@ -1,6 +1,6 @@
- dropdown_class = local_assigns.fetch(:dropdown_class, '')
-.dropdown-menu.dropdown-menu-selectable{ class: dropdown_class }
+.dropdown-menu.dropdown-menu-selectable.git-revision-dropdown{ class: dropdown_class }
= dropdown_title "Select Git revision"
= dropdown_filter "Filter by Git revision"
= dropdown_content
diff --git a/app/views/shared/_ref_switcher.html.haml b/app/views/shared/_ref_switcher.html.haml
index 9a8252ab087..2029eb5824a 100644
--- a/app/views/shared/_ref_switcher.html.haml
+++ b/app/views/shared/_ref_switcher.html.haml
@@ -6,8 +6,8 @@
- @options && @options.each do |key, value|
= hidden_field_tag key, value, id: nil
.dropdown
- = dropdown_toggle dropdown_toggle_text, { toggle: "dropdown", selected: dropdown_toggle_text, ref: @ref, refs_url: refs_namespace_project_path(@project.namespace, @project), field_name: 'ref', submit_form_on_click: true }, { toggle_class: "js-project-refs-dropdown" }
- .dropdown-menu.dropdown-menu-selectable{ class: ("dropdown-menu-align-right" if local_assigns[:align_right]) }
+ = dropdown_toggle dropdown_toggle_text, { toggle: "dropdown", selected: dropdown_toggle_text, ref: @ref, refs_url: refs_namespace_project_path(@project.namespace, @project), field_name: 'ref', submit_form_on_click: true }, { toggle_class: "js-project-refs-dropdown git-revision-dropdown-toggle" }
+ .dropdown-menu.dropdown-menu-selectable.git-revision-dropdown{ class: ("dropdown-menu-align-right" if local_assigns[:align_right]) }
= dropdown_title "Switch branch/tag"
= dropdown_filter "Search branches and tags"
= dropdown_content
diff --git a/app/views/shared/empty_states/_issues.html.haml b/app/views/shared/empty_states/_issues.html.haml
index c229d18903f..046b127f73c 100644
--- a/app/views/shared/empty_states/_issues.html.haml
+++ b/app/views/shared/empty_states/_issues.html.haml
@@ -3,10 +3,10 @@
- has_button = button_path || project_select_button
.row.empty-state
- .pull-right.col-xs-12{ class: "#{'col-sm-6' if has_button}" }
+ .col-xs-12
.svg-content
= render 'shared/empty_states/icons/issues.svg'
- .col-xs-12{ class: "#{'col-sm-6' if has_button}" }
+ .col-xs-12.text-center
.text-content
- if has_button && current_user
%h4
@@ -20,4 +20,3 @@
- else
.text-center
%h4 There are no issues to show.
- = link_to 'New issue', button_path, class: 'btn btn-new', title: 'New issue', id: 'new_issue_link'
diff --git a/app/views/shared/empty_states/_labels.html.haml b/app/views/shared/empty_states/_labels.html.haml
index 00fb77bdb3b..5e2f4cf109d 100644
--- a/app/views/shared/empty_states/_labels.html.haml
+++ b/app/views/shared/empty_states/_labels.html.haml
@@ -1,8 +1,8 @@
.row.empty-state.labels
- .pull-right.col-xs-12.col-sm-6
+ .col-xs-12
.svg-content
= render 'shared/empty_states/icons/labels.svg'
- .col-xs-12.col-sm-6
+ .col-xs-12.text-center
.text-content
%h4 Labels can be applied to issues and merge requests to categorize them.
%p You can also star a label to make it a priority label.
diff --git a/app/views/shared/empty_states/_merge_requests.html.haml b/app/views/shared/empty_states/_merge_requests.html.haml
index 7f2f99f3406..3e64f403b8b 100644
--- a/app/views/shared/empty_states/_merge_requests.html.haml
+++ b/app/views/shared/empty_states/_merge_requests.html.haml
@@ -3,10 +3,10 @@
- has_button = button_path || project_select_button
.row.empty-state.merge-requests
- .col-xs-12{ class: "#{'col-sm-6 pull-right' if has_button}" }
+ .col-xs-12
.svg-content
= render 'shared/empty_states/icons/merge_requests.svg'
- .col-xs-12{ class: "#{'col-sm-6' if has_button}" }
+ .col-xs-12.text-center
.text-content
- if has_button
%h4
diff --git a/app/views/shared/empty_states/icons/_pipelines_empty.svg b/app/views/shared/empty_states/icons/_pipelines_empty.svg
index 8119d5bebe0..7c672538097 100644
--- a/app/views/shared/empty_states/icons/_pipelines_empty.svg
+++ b/app/views/shared/empty_states/icons/_pipelines_empty.svg
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 250 150"><g fill="none" fill-rule="evenodd" transform="translate(0-3)"><g transform="translate(0 105)"><g fill="#e5e5e5"><rect width="78" height="4" x="34" y="21" opacity=".5" rx="2"/><path d="m152 23c0-1.105.887-2 1.998-2h4c1.104 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4c-1.104 0-1.998-.888-1.998-2m14 0c0-1.105.887-2 1.998-2h4c1.104 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4c-1.104 0-1.998-.888-1.998-2m14 0c0-1.105.887-2 1.998-2h4c1.104 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4c-1.104 0-1.998-.888-1.998-2m14 0c0-1.105.887-2 1.998-2h4c1.104 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4c-1.104 0-1.998-.888-1.998-2m14 0c0-1.105.887-2 1.998-2h4c1.104 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4c-1.104 0-1.998-.888-1.998-2"/></g><g transform="translate(0 4)"><path fill="#98d7b2" fill-rule="nonzero" d="m19 38c-10.493 0-19-8.507-19-19 0-10.493 8.507-19 19-19 10.493 0 19 8.507 19 19 0 10.493-8.507 19-19 19m0-4c8.284 0 15-6.716 15-15 0-8.284-6.716-15-15-15-8.284 0-15 6.716-15 15 0 8.284 6.716 15 15 15"/><path fill="#31af64" d="m17.07 21.02l-2.829-2.829c-.786-.786-2.047-.781-2.828 0-.786.786-.781 2.047 0 2.828l4.243 4.243c.392.392.902.587 1.412.588.512.002 1.021-.193 1.41-.582l7.79-7.79c.777-.777.775-2.042-.006-2.823-.786-.786-2.045-.784-2.823-.006l-6.37 6.37"/></g><g fill="#e52c5a" transform="translate(102)"><path fill-rule="nonzero" d="m24 47.5c-12.979 0-23.5-10.521-23.5-23.5 0-12.979 10.521-23.5 23.5-23.5 12.979 0 23.5 10.521 23.5 23.5 0 12.979-10.521 23.5-23.5 23.5m0-5c10.217 0 18.5-8.283 18.5-18.5 0-10.217-8.283-18.5-18.5-18.5-10.217 0-18.5 8.283-18.5 18.5 0 10.217 8.283 18.5 18.5 18.5"/><path d="m28.24 24l2.833-2.833c1.167-1.167 1.167-3.067-.004-4.239-1.169-1.169-3.069-1.173-4.239-.004l-2.833 2.833-2.833-2.833c-1.167-1.167-3.067-1.167-4.239.004-1.169 1.169-1.173 3.069-.004 4.239l2.833 2.833-2.833 2.833c-1.167 1.167-1.167 3.067.004 4.239 1.169 1.169 3.069 1.173 4.239.004l2.833-2.833 2.833 2.833c1.167 1.167 3.067 1.167 4.239-.004 1.169-1.169 1.173-3.069.004-4.239l-2.833-2.833"/></g><path fill="#e5e5e5" fill-rule="nonzero" d="m236 37c-7.732 0-14-6.268-14-14 0-7.732 6.268-14 14-14 7.732 0 14 6.268 14 14 0 7.732-6.268 14-14 14m0-4c5.523 0 10-4.477 10-10 0-5.523-4.477-10-10-10-5.523 0-10 4.477-10 10 0 5.523 4.477 10 10 10"/></g><g transform="translate(69 3)"><path fill="#e5e5e5" fill-rule="nonzero" d="m4 11.99v60.02c0 4.413 3.583 7.99 8 7.99h89.991c4.419 0 8-3.579 8-7.99v-60.02c0-4.413-3.583-7.99-8-7.99h-89.991c-4.419 0-8 3.579-8 7.99m-4 0c0-6.622 5.378-11.99 12-11.99h89.991c6.629 0 12 5.367 12 11.99v60.02c0 6.622-5.378 11.99-12 11.99h-89.991c-6.629 0-12-5.367-12-11.99v-60.02m52.874 80.3l-13.253-15.292h34.76l-13.253 15.292c-2.237 2.582-6.01 2.585-8.253 0m3.02-2.62c.644.743 1.564.743 2.207 0l7.516-8.673h-17.24l7.516 8.673"/><rect width="18" height="6" x="15" y="23" fill="#fc8a51" rx="3"/><rect width="18" height="6" x="39" y="39" fill="#e52c5a" rx="3"/><rect width="18" height="6" x="33" y="55" fill="#e5e5e5" rx="3"/><rect width="12" height="6" x="39" y="23" fill="#fde5d8" rx="3"/><rect width="12" height="6" x="57" y="55" fill="#e52c5a" rx="3"/><rect width="12" height="6" x="15" y="55" fill="#b5a7dd" rx="3"/><rect width="18" height="6" x="81" y="23" fill="#fc8a51" rx="3"/><rect width="18" height="6" x="15" y="39" fill="#fde5d8" rx="3"/><rect width="6" height="6" x="57" y="23" fill="#e52c5a" rx="3"/><g fill="#fde5d8"><rect width="6" height="6" x="69" y="23" rx="3"/><rect width="6" height="6" x="75" y="39" rx="3"/></g><rect width="6" height="6" x="63" y="39" fill="#e52c5a" rx="3"/></g><g transform="matrix(.70711-.70711.70711.70711 84.34 52.5)"><path fill="#6b4fbb" fill-rule="nonzero" d="m28.02 67.48c-15.927-2.825-28.02-16.738-28.02-33.476 0-18.778 15.222-34 34-34 18.778 0 34 15.222 34 34 0 16.738-12.1 30.652-28.02 33.476.015.173.023.347.023.524v21.999c0 3.314-2.693 6-6 6-3.314 0-6-2.682-6-6v-21.999c0-.177.008-.351.023-.524m5.977-7.476c14.359 0 26-11.641 26-26 0-14.359-11.641-26-26-26-14.359 0-26 11.641-26 26 0 14.359 11.641 26 26 26"/><path fill="#fff" fill-opacity=".3" stroke="#6b4fbb" stroke-width="8" d="m31 71c16.569 0 30-13.431 30-30 0-16.569-13.431-30-30-30" transform="matrix(.86603.5-.5.86603 26.663-17.507)"/></g></g></svg> \ No newline at end of file
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 250 150"><g fill="none" fill-rule="evenodd"><g transform="translate(0 102)"><g fill="#e5e5e5"><rect width="74" height="4" x="34" y="21" opacity=".5" rx="2"/><path d="m152 23c0-1.105.887-2 1.998-2h4c1.104 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4c-1.104 0-1.998-.888-1.998-2m14 0c0-1.105.887-2 1.998-2h4c1.104 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4c-1.104 0-1.998-.888-1.998-2m14 0c0-1.105.887-2 1.998-2h4c1.104 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4c-1.104 0-1.998-.888-1.998-2m14 0c0-1.105.887-2 1.998-2h4c1.104 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4c-1.104 0-1.998-.888-1.998-2m14 0c0-1.105.887-2 1.998-2h4c1.104 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4c-1.104 0-1.998-.888-1.998-2"/></g><g fill="#31af64" transform="translate(0 4)"><path fill-rule="nonzero" d="m19 38c-10.493 0-19-8.507-19-19 0-10.493 8.507-19 19-19 10.493 0 19 8.507 19 19 0 10.493-8.507 19-19 19m0-4c8.284 0 15-6.716 15-15 0-8.284-6.716-15-15-15-8.284 0-15 6.716-15 15 0 8.284 6.716 15 15 15"/><path d="m17.07 21.02l-2.829-2.829c-.786-.786-2.047-.781-2.828 0-.786.786-.781 2.047 0 2.828l4.243 4.243c.392.392.902.587 1.412.588.512.002 1.021-.193 1.41-.582l7.79-7.79c.777-.777.775-2.042-.006-2.823-.786-.786-2.045-.784-2.823-.006l-6.37 6.37"/></g><g fill="#e52c5a" transform="translate(102)"><path fill-rule="nonzero" d="m24 47.5c-12.979 0-23.5-10.521-23.5-23.5 0-12.979 10.521-23.5 23.5-23.5 12.979 0 23.5 10.521 23.5 23.5 0 12.979-10.521 23.5-23.5 23.5m0-5c10.217 0 18.5-8.283 18.5-18.5 0-10.217-8.283-18.5-18.5-18.5-10.217 0-18.5 8.283-18.5 18.5 0 10.217 8.283 18.5 18.5 18.5"/><path d="m28.24 24l2.833-2.833c1.167-1.167 1.167-3.067-.004-4.239-1.169-1.169-3.069-1.173-4.239-.004l-2.833 2.833-2.833-2.833c-1.167-1.167-3.067-1.167-4.239.004-1.169 1.169-1.173 3.069-.004 4.239l2.833 2.833-2.833 2.833c-1.167 1.167-1.167 3.067.004 4.239 1.169 1.169 3.069 1.173 4.239.004l2.833-2.833 2.833 2.833c1.167 1.167 3.067 1.167 4.239-.004 1.169-1.169 1.173-3.069.004-4.239l-2.833-2.833"/></g><path fill="#e5e5e5" fill-rule="nonzero" d="m236 37c-7.732 0-14-6.268-14-14 0-7.732 6.268-14 14-14 7.732 0 14 6.268 14 14 0 7.732-6.268 14-14 14m0-4c5.523 0 10-4.477 10-10 0-5.523-4.477-10-10-10-5.523 0-10 4.477-10 10 0 5.523 4.477 10 10 10"/></g><g transform="translate(73 4)"><path stroke="#e5e5e5" stroke-width="4" d="m64.82 76h33.18c4.419 0 8-3.579 8-7.99v-60.02c0-4.413-3.583-7.99-8-7.99h-89.991c-4.419 0-8 3.579-8 7.99v60.02c0 4.413 3.583 7.99 8 7.99h31.935l9.263 9.855c1.725 1.835 4.631 1.833 6.354 0l9.263-9.855"/><rect width="18" height="6" x="11" y="19" fill="#fc8a51" rx="3"/><rect width="18" height="6" x="35" y="35" fill="#e52c5a" rx="3"/><rect width="18" height="6" x="29" y="51" fill="#e5e5e5" rx="3"/><rect width="12" height="6" x="35" y="19" fill="#fde5d8" rx="3"/><rect width="12" height="6" x="53" y="51" fill="#e52c5a" rx="3"/><rect width="12" height="6" x="11" y="51" fill="#b5a7dd" rx="3"/><rect width="18" height="6" x="77" y="19" fill="#fc8a51" rx="3"/><rect width="18" height="6" x="11" y="35" fill="#fde5d8" rx="3"/><rect width="6" height="6" x="53" y="19" fill="#e52c5a" rx="3"/><g fill="#fde5d8"><rect width="6" height="6" x="65" y="19" rx="3"/><rect width="6" height="6" x="71" y="35" rx="3"/></g><rect width="6" height="6" x="59" y="35" fill="#e52c5a" rx="3"/></g><path fill="#6b4fbb" fill-rule="nonzero" d="m28.02 67.48c-15.927-2.825-28.02-16.738-28.02-33.476 0-18.778 15.222-34 34-34 18.778 0 34 15.222 34 34 0 16.738-12.1 30.652-28.02 33.476.015.173.023.347.023.524v21.999c0 3.314-2.693 6-6 6-3.314 0-6-2.682-6-6v-21.999c0-.177.008-.351.023-.524m5.977-7.476c14.359 0 26-11.641 26-26 0-14.359-11.641-26-26-26-14.359 0-26 11.641-26 26 0 14.359 11.641 26 26 26" transform="matrix(.70711-.70711.70711.70711 84.34 49.5)"/></g></svg>
diff --git a/app/views/shared/icons/_icon_history.svg b/app/views/shared/icons/_icon_history.svg
new file mode 100644
index 00000000000..41096da19c5
--- /dev/null
+++ b/app/views/shared/icons/_icon_history.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="1792" height="1792" viewBox="0 0 1792 1792"><path d="M1664 896q0 156-61 298t-164 245-245 164-298 61q-172 0-327-72.5T305 1387q-7-10-6.5-22.5t8.5-20.5l137-138q10-9 25-9 16 2 23 12 73 95 179 147t225 52q104 0 198.5-40.5T1258 1258t109.5-163.5T1408 896t-40.5-198.5T1258 534t-163.5-109.5T896 384q-98 0-188 35.5T548 521l137 138q31 30 14 69-17 40-59 40H192q-26 0-45-19t-19-45V256q0-42 40-59 39-17 69 14l130 129q107-101 244.5-156.5T896 128q156 0 298 61t245 164 164 245 61 298zm-640-288v448q0 14-9 23t-23 9H672q-14 0-23-9t-9-23v-64q0-14 9-23t23-9h224V608q0-14 9-23t23-9h64q14 0 23 9t9 23z"/></svg>
diff --git a/app/views/shared/icons/_mr_widget_empty_state.svg b/app/views/shared/icons/_mr_widget_empty_state.svg
new file mode 100644
index 00000000000..6a811893b2d
--- /dev/null
+++ b/app/views/shared/icons/_mr_widget_empty_state.svg
@@ -0,0 +1 @@
+<svg width="256" height="146" viewBox="0 0 256 146" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><title>illustration</title><defs><rect id="a" width="178.714" height="115.389" rx="10"/><mask id="d" x="0" y="0" width="178.714" height="115.389" fill="#fff"><use xlink:href="#a"/></mask><path d="M8.796 31.515c.395.047.8.072 1.207.072h23.065c5.536 0 10.003-4.475 10.003-9.994v-11.6C43.07 4.476 38.594 0 33.07 0H10.003C4.467 0 0 4.475 0 9.994v11.6c0 1.248.23 2.444.65 3.547H0v7.414c0 4.094 2.394 5.113 5.342 2.28l3.454-3.32z" id="b"/><mask id="e" x="0" y="0" width="43.071" height="36.437" fill="#fff"><use xlink:href="#b"/></mask><path d="M8.796 31.515c.395.047.8.072 1.207.072h23.065c5.536 0 10.003-4.475 10.003-9.994v-11.6C43.07 4.476 38.594 0 33.07 0H10.003C4.467 0 0 4.475 0 9.994v11.6c0 1.248.23 2.444.65 3.547H0v7.414c0 4.094 2.394 5.113 5.342 2.28l3.454-3.32z" id="c"/><mask id="f" x="0" y="0" width="43.071" height="36.437" fill="#fff"><use xlink:href="#c"/></mask></defs><g fill="none" fill-rule="evenodd"><g transform="translate(0 3.868)" fill="#F9F9F9"><rect x="19.286" width="77.143" height="14.182" rx="7.091"/><rect y="28.364" width="84.857" height="14.182" rx="7.091"/><rect x="133.714" y="42.546" width="122.143" height="14.182" rx="7.091"/><rect x="82.929" y="126.992" width="101.571" height="14.182" rx="7.091"/><rect x="42.429" y="99.273" width="101.571" height="14.182" rx="7.091"/><rect x="19.929" y="70.909" width="225" height="14.182" rx="7.091"/><path d="M98.37 14.182H13.488h13.81a7.098 7.098 0 0 1 7.094 7.09 7.09 7.09 0 0 1-7.094 7.092h-13.81 84.88-23.452a7.098 7.098 0 0 1-7.095-7.09 7.09 7.09 0 0 1 7.096-7.092h23.452zm162 42.545h-75.238 23.452a7.098 7.098 0 0 1 7.095 7.09 7.09 7.09 0 0 1-7.096 7.092h-23.452 75.237-23.453a7.098 7.098 0 0 1-7.095-7.09 7.09 7.09 0 0 1 7.095-7.093h23.452zM103.512 85.09H28.275h23.452a7.098 7.098 0 0 1 7.095 7.092 7.09 7.09 0 0 1-7.095 7.09H28.275h75.237H80.06a7.098 7.098 0 0 1-7.095-7.09 7.09 7.09 0 0 1 7.095-7.09h23.452zm48.215 28.365H76.49 90.3a7.098 7.098 0 0 1 7.093 7.09 7.09 7.09 0 0 1-7.094 7.092H76.49h75.237-33.096a7.098 7.098 0 0 1-7.094-7.09 7.09 7.09 0 0 1 7.095-7.092h33.097z"/></g><g transform="translate(38.57 12.248)"><use stroke="#EEE" mask="url(#d)" stroke-width="8" fill="#FFF" xlink:href="#a"/><path fill="#EEE" d="M2.57 18.694h174.215v2.58H2.57z"/><g transform="translate(21.857 38.678)"><rect fill="#B5A7DD" y=".645" width="3.857" height="1.289" rx=".645"/><rect fill="#EEE" x="9.643" width="9.643" height="2.579" rx="1.289"/><rect fill="#EEE" x="46.286" width="9.643" height="2.579" rx="1.289"/><rect fill="#EEE" x="25.071" y="14.182" width="9.643" height="2.579" rx="1.289"/><rect fill="#FC6D26" x="34.071" y="7.091" width="9.643" height="2.579" rx="1.289"/><rect fill="#FC6D26" opacity=".5" x="30.857" width="12.857" height="2.579" rx="1.289"/><rect fill="#EEE" x="9.643" y="14.182" width="12.857" height="2.579" rx="1.289"/><rect fill="#EEE" x="18.643" y="7.091" width="12.857" height="2.579" rx="1.289"/><rect fill="#FC6D26" x="21.857" width="6.429" height="2.579" rx="1.289"/><rect fill="#EEE" x="9.643" y="7.091" width="6.429" height="2.579" rx="1.289"/><rect fill="#B5A7DD" y="7.736" width="3.857" height="1.289" rx=".645"/><rect fill="#B5A7DD" y="14.826" width="3.857" height="1.289" rx=".645"/></g><g transform="translate(21.857 59.95)"><rect fill="#B5A7DD" y=".645" width="3.857" height="1.289" rx=".645"/><rect fill="#FC6D26" x="9.643" width="9.643" height="2.579" rx="1.289"/><rect fill="#EEE" x="46.286" width="9.643" height="2.579" rx="1.289"/><rect fill="#FC6D26" opacity=".5" x="25.071" y="14.182" width="9.643" height="2.579" rx="1.289"/><rect fill="#EEE" x="34.071" y="7.091" width="9.643" height="2.579" rx="1.289"/><rect fill="#FC6D26" x="30.857" width="12.857" height="2.579" rx="1.289"/><rect fill="#FC6D26" x="9.643" y="14.182" width="12.857" height="2.579" rx="1.289"/><rect fill="#EEE" x="18.643" y="7.091" width="12.857" height="2.579" rx="1.289"/><rect fill="#FC6D26" opacity=".5" x="21.857" width="6.429" height="2.579" rx="1.289"/><rect fill="#EEE" x="9.643" y="7.091" width="6.429" height="2.579" rx="1.289"/><rect fill="#B5A7DD" y="7.736" width="3.857" height="1.289" rx=".645"/><rect fill="#B5A7DD" y="14.826" width="3.857" height="1.289" rx=".645"/></g><g transform="translate(21.857 81.223)"><rect fill="#B5A7DD" y=".645" width="3.857" height="1.289" rx=".645"/><rect fill="#EEE" x="9.643" width="9.643" height="2.579" rx="1.289"/><rect fill="#EEE" x="46.286" width="9.643" height="2.579" rx="1.289"/><rect fill="#EEE" x="25.071" y="14.182" width="9.643" height="2.579" rx="1.289"/><rect fill="#EEE" x="34.071" y="7.091" width="9.643" height="2.579" rx="1.289"/><rect fill="#FC6D26" x="30.857" width="12.857" height="2.579" rx="1.289"/><rect fill="#EEE" x="9.643" y="14.182" width="12.857" height="2.579" rx="1.289"/><rect fill="#EEE" x="18.643" y="7.091" width="12.857" height="2.579" rx="1.289"/><rect fill="#FC6D26" opacity=".5" x="21.857" width="6.429" height="2.579" rx="1.289"/><rect fill="#EEE" x="9.643" y="7.091" width="6.429" height="2.579" rx="1.289"/><rect fill="#B5A7DD" y="7.736" width="3.857" height="1.289" rx=".645"/><rect fill="#B5A7DD" y="14.826" width="3.857" height="1.289" rx=".645"/></g><g transform="translate(100.93 38.033)"><rect fill="#FDE5D8" y=".645" width="3.857" height="1.289" rx=".645"/><rect fill="#EEE" x="9.643" width="9.643" height="2.579" rx="1.289"/><rect fill="#EEE" x="46.286" width="9.643" height="2.579" rx="1.289"/><rect fill="#6B4FBB" opacity=".5" x="25.071" y="14.182" width="9.643" height="2.579" rx="1.289"/><rect fill="#6B4FBB" x="34.071" y="7.091" width="9.643" height="2.579" rx="1.289"/><rect fill="#6B4FBB" opacity=".5" x="30.857" width="12.857" height="2.579" rx="1.289"/><rect fill="#6B4FBB" x="9.643" y="14.182" width="12.857" height="2.579" rx="1.289"/><rect fill="#EEE" x="18.643" y="7.091" width="12.857" height="2.579" rx="1.289"/><rect fill="#6B4FBB" x="21.857" width="6.429" height="2.579" rx="1.289"/><rect fill="#EEE" x="9.643" y="7.091" width="6.429" height="2.579" rx="1.289"/><rect fill="#FDE5D8" y="7.736" width="3.857" height="1.289" rx=".645"/><rect fill="#FDE5D8" y="14.826" width="3.857" height="1.289" rx=".645"/><rect fill="#FDE5D8" y="21.917" width="3.857" height="1.289" rx=".645"/><rect fill="#EEE" x="9.643" y="21.273" width="9.643" height="2.579" rx="1.289"/><rect fill="#EEE" x="37.286" y="14.182" width="9.643" height="2.579" rx="1.289"/><rect fill="#6B4FBB" opacity=".5" x="25.071" y="35.455" width="9.643" height="2.579" rx="1.289"/><rect fill="#6B4FBB" x="18.643" y="28.364" width="9.643" height="2.579" rx="1.289"/><rect fill="#6B4FBB" x="30.857" y="21.273" width="12.857" height="2.579" rx="1.289"/><rect fill="#EEE" x="9.643" y="35.455" width="12.857" height="2.579" rx="1.289"/><rect fill="#EEE" x="21.857" y="21.273" width="6.429" height="2.579" rx="1.289"/><rect fill="#EEE" x="9.643" y="28.364" width="6.429" height="2.579" rx="1.289"/><rect fill="#EEE" x="30.857" y="28.364" width="6.429" height="2.579" rx="1.289"/><rect fill="#EEE" x="39.857" y="28.364" width="6.429" height="2.579" rx="1.289"/><rect fill="#EEE" x="49.5" y="14.182" width="6.429" height="2.579" rx="1.289"/><rect fill="#FDE5D8" y="29.008" width="3.857" height="1.289" rx=".645"/><rect fill="#FDE5D8" y="36.099" width="3.857" height="1.289" rx=".645"/><rect fill="#FDE5D8" y="43.19" width="3.857" height="1.289" rx=".645"/><rect fill="#6B4FBB" x="9.643" y="42.546" width="9.643" height="2.579" rx="1.289"/><rect fill="#EEE" x="25.071" y="56.727" width="9.643" height="2.579" rx="1.289"/><rect fill="#6B4FBB" opacity=".5" x="34.071" y="49.636" width="9.643" height="2.579" rx="1.289"/><rect fill="#EEE" x="9.643" y="56.727" width="12.857" height="2.579" rx="1.289"/><rect fill="#6B4FBB" x="18.643" y="49.636" width="12.857" height="2.579" rx="1.289"/><rect fill="#EEE" x="21.857" y="42.546" width="6.429" height="2.579" rx="1.289"/><rect fill="#EEE" x="46.286" y="49.636" width="6.429" height="2.579" rx="1.289"/><rect fill="#EEE" x="9.643" y="49.636" width="6.429" height="2.579" rx="1.289"/><rect fill="#FDE5D8" y="50.281" width="3.857" height="1.289" rx=".645"/><rect fill="#FDE5D8" y="57.372" width="3.857" height="1.289" rx=".645"/></g></g><g transform="translate(196.07)"><use stroke="#FDE5D8" mask="url(#e)" stroke-width="8" fill="#FFF" xlink:href="#b"/><rect fill="#FDB692" x="9" y="9.025" width="18.643" height="1.934" rx=".967"/><rect fill="#FDB692" x="9" y="14.826" width="25.071" height="1.934" rx=".967"/><rect fill="#FDB692" x="9" y="20.628" width="18.643" height="1.934" rx=".967"/></g><g transform="translate(189 41.256)"><ellipse stroke="#FC6D26" stroke-width="3" fill="#FFF7F4" cx="10.286" cy="9.669" rx="9.643" ry="9.669"/><path d="M.023 9.002a8.352 8.352 0 0 0 7.94-4.308M9 .644c0-.21-.008-.416-.023-.62" stroke="#FC6D26" stroke-width="2"/><path d="M5.045 2.008A10.266 10.266 0 0 0 13.5 6.446c2.112 0 4.076-.638 5.71-1.733" stroke="#FC6D26" stroke-width="2"/><ellipse fill="#FC6D26" cx="6.75" cy="11.281" rx=".964" ry=".967"/><ellipse fill="#FC6D26" cx="13.821" cy="11.281" rx=".964" ry=".967"/></g><g transform="translate(46.93 96.05)"><ellipse stroke="#6B4FBB" stroke-width="3" fill="#F4F1FA" cx="9.643" cy="10.314" rx="9.643" ry="9.669"/><path d="M12.86 4.51h-.005L11.25 2.58 9.645 4.51H9.64L8.036 2.58 6.43 4.51h-.002L4.82 2.58 3.215 4.512h-1.75A9.646 9.646 0 0 1 9.642 0c3.447 0 6.47 1.8 8.176 4.508h-1.75l-1.605-1.93L12.86 4.51z" fill="#6B4FBB"/><ellipse fill="#6B4FBB" cx="6.107" cy="11.281" rx=".964" ry=".967"/><ellipse fill="#6B4FBB" cx="13.179" cy="11.281" rx=".964" ry=".967"/></g><g transform="matrix(-1 0 0 1 56.57 54.794)"><use stroke="#E2DCF2" mask="url(#f)" stroke-width="8" fill="#FFF" xlink:href="#c"/><rect fill="#6B4FBB" opacity=".5" x="15.429" y="9.025" width="18.643" height="1.934" rx=".967"/><rect fill="#6B4FBB" opacity=".5" x="21.857" y="14.826" width="12.214" height="1.934" rx=".967"/><rect fill="#6B4FBB" opacity=".5" x="21.857" y="20.628" width="12.214" height="1.934" rx=".967"/></g></g></svg>
diff --git a/app/views/shared/issuable/_assignees.html.haml b/app/views/shared/issuable/_assignees.html.haml
index 36bbb1148d4..217af7c9fac 100644
--- a/app/views/shared/issuable/_assignees.html.haml
+++ b/app/views/shared/issuable/_assignees.html.haml
@@ -1,9 +1,8 @@
- max_render = 3
- max = [max_render, issue.assignees.length].min
-- issue.assignees.each_with_index do |assignee, index|
- - if index < max
- = link_to_member(@project, assignee, name: false, title: "Assigned to :name")
+- issue.assignees.take(max).each do |assignee|
+ = link_to_member(@project, assignee, name: false, title: "Assigned to :name")
- if issue.assignees.length > max_render
- counter = issue.assignees.length - max_render
diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml
index 1a12f110945..6cd03f028a9 100644
--- a/app/views/shared/issuable/_filter.html.haml
+++ b/app/views/shared/issuable/_filter.html.haml
@@ -71,7 +71,6 @@
= render 'shared/labels_row', labels: @labels
:javascript
- new UsersSelect();
new LabelsSelect();
new MilestoneSelect();
new IssueStatusSelect();
diff --git a/app/views/shared/issuable/_milestone_dropdown.html.haml b/app/views/shared/issuable/_milestone_dropdown.html.haml
index f0d50828e2a..6750921338a 100644
--- a/app/views/shared/issuable/_milestone_dropdown.html.haml
+++ b/app/views/shared/issuable/_milestone_dropdown.html.haml
@@ -6,7 +6,7 @@
- if selected.present? || params[:milestone_title].present?
= hidden_field_tag(name, name == :milestone_title ? selected_text : selected.id)
= dropdown_tag(milestone_dropdown_label(selected_text), options: { title: dropdown_title, toggle_class: "js-milestone-select js-filter-submit #{extra_class}", filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone",
- placeholder: "Search milestones", footer_content: project.present?, data: { show_no: true, show_menu_above: show_menu_above, show_any: show_any, show_upcoming: show_upcoming, show_started: show_started, field_name: name, selected: selected.try(:title), project_id: project.try(:id), milestones: milestones_filter_dropdown_path, default_label: "Milestone" } }) do
+ placeholder: "Search milestones", footer_content: project.present?, data: { show_no: true, show_menu_above: show_menu_above, show_any: show_any, show_upcoming: show_upcoming, show_started: show_started, field_name: name, selected: selected_text, project_id: project.try(:id), milestones: milestones_filter_dropdown_path, default_label: "Milestone" } }) do
- if project
%ul.dropdown-footer-list
- if can? current_user, :admin_milestone, project
diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml
index f7b87171573..80974bdb066 100644
--- a/app/views/shared/issuable/_search_bar.html.haml
+++ b/app/views/shared/issuable/_search_bar.html.haml
@@ -13,13 +13,13 @@
.issues-other-filters.filtered-search-wrapper
.filtered-search-box
- if type != :boards_modal && type != :boards
- = dropdown_tag(content_tag(:i, '', class: 'fa fa-history'),
+ = dropdown_tag(custom_icon('icon_history'),
options: { wrapper_class: "filtered-search-history-dropdown-wrapper",
toggle_class: "filtered-search-history-dropdown-toggle-button",
dropdown_class: "filtered-search-history-dropdown",
content_class: "filtered-search-history-dropdown-content",
title: "Recent searches" }) do
- .js-filtered-search-history-dropdown
+ .js-filtered-search-history-dropdown{ data: { project_full_path: @project.full_path } }
.filtered-search-box-input-container
.scroll-container
%ul.tokens-container.list-unstyled
@@ -150,7 +150,6 @@
- unless type === :boards_modal
:javascript
- new UsersSelect();
new LabelsSelect();
new MilestoneSelect();
new IssueStatusSelect();
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 3a66880e177..ac84fffe831 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -3,7 +3,7 @@
= page_specific_javascript_bundle_tag('common_vue')
= page_specific_javascript_bundle_tag('sidebar')
-%aside.right-sidebar.js-right-sidebar{ data: { "offset-top" => "102", "spy" => "affix" }, class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' }
+%aside.right-sidebar.js-right-sidebar{ data: { "offset-top" => "50", "spy" => "affix" }, class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' }
.issuable-sidebar{ data: { endpoint: "#{issuable_json_path(issuable)}" } }
- can_edit_issuable = can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
.block.issuable-sidebar-header
@@ -43,7 +43,7 @@
.selectbox.hide-collapsed
= f.hidden_field 'milestone_id', value: issuable.milestone_id, id: nil
- = dropdown_tag('Milestone', options: { title: 'Assign milestone', toggle_class: 'js-milestone-select js-extra-options', filter: true, dropdown_class: 'dropdown-menu-selectable', placeholder: 'Search milestones', data: { show_no: true, field_name: "#{issuable.to_ability_name}[milestone_id]", project_id: @project.id, issuable_id: issuable.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), ability_name: issuable.to_ability_name, issue_update: issuable_json_path(issuable), use_id: true }})
+ = dropdown_tag('Milestone', options: { title: 'Assign milestone', toggle_class: 'js-milestone-select js-extra-options', filter: true, dropdown_class: 'dropdown-menu-selectable', placeholder: 'Search milestones', data: { show_no: true, field_name: "#{issuable.to_ability_name}[milestone_id]", project_id: @project.id, issuable_id: issuable.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), ability_name: issuable.to_ability_name, issue_update: issuable_json_path(issuable), use_id: true, default_no: true, selected: (issuable.milestone.name if issuable.milestone), null_default: true }})
- if issuable.has_attribute?(:time_estimate)
#issuable-time-tracker.block
// Fallback while content is loading
diff --git a/app/views/shared/issuable/_sidebar_assignees.html.haml b/app/views/shared/issuable/_sidebar_assignees.html.haml
index c36a45098a8..e9ce7b7ce9c 100644
--- a/app/views/shared/issuable/_sidebar_assignees.html.haml
+++ b/app/views/shared/issuable/_sidebar_assignees.html.haml
@@ -1,4 +1,4 @@
-- if issuable.instance_of?(Issue)
+- if issuable.is_a?(Issue)
#js-vue-sidebar-assignees{ data: { field: "#{issuable.to_ability_name}[assignee_ids]" } }
- else
.sidebar-collapsed-icon.sidebar-collapsed-user{ data: { toggle: "tooltip", placement: "left", container: "body" }, title: (issuable.assignee.name if issuable.assignee) }
@@ -33,17 +33,17 @@
- options = { toggle_class: 'js-user-search js-author-search', title: 'Assign to', filter: true, dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author', placeholder: 'Search users', data: { first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), author_id: issuable.author_id, field_name: "#{issuable.to_ability_name}[assignee_ids][]", issue_update: issuable_json_path(issuable), ability_name: issuable.to_ability_name, null_user: true } }
- - if issuable.instance_of?(Issue)
- - if issuable.assignees.length == 0
+ - title = 'Select assignee'
+
+ - if issuable.is_a?(Issue)
+ - unless issuable.assignees.any?
= hidden_field_tag "#{issuable.to_ability_name}[assignee_ids][]", 0, id: nil
- - title = 'Select assignee'
- options[:toggle_class] += ' js-multiselect js-save-user-data'
- - options[:data][:field_name] = "#{issuable.to_ability_name}[assignee_ids][]"
- - options[:data][:multi_select] = true
- - options[:data]['dropdown-title'] = title
- - options[:data]['dropdown-header'] = 'Assignee'
- - options[:data]['max-select'] = 1
- - else
- - title = 'Select assignee'
+ - data = { field_name: "#{issuable.to_ability_name}[assignee_ids][]" }
+ - data[:multi_select] = true
+ - data['dropdown-title'] = title
+ - data['dropdown-header'] = 'Assignee'
+ - data['max-select'] = 1
+ - options[:data].merge!(data)
= dropdown_tag(title, options: options)
diff --git a/app/views/shared/issuable/form/_branch_chooser.html.haml b/app/views/shared/issuable/form/_branch_chooser.html.haml
index f57b4d899ce..203d2adc8db 100644
--- a/app/views/shared/issuable/form/_branch_chooser.html.haml
+++ b/app/views/shared/issuable/form/_branch_chooser.html.haml
@@ -10,14 +10,14 @@
= form.label :source_branch, class: 'control-label'
.col-sm-10
.issuable-form-select-holder
- = form.select(:source_branch, [issuable.source_branch], {}, { class: 'source_branch select2', disabled: true })
+ = form.select(:source_branch, [issuable.source_branch], {}, { class: 'source_branch select2 ref-name', disabled: true })
.form-group
= form.label :target_branch, class: 'control-label'
.col-sm-10.target-branch-select-dropdown-container
.issuable-form-select-holder
= form.select(:target_branch, issuable.target_branches,
{ include_blank: true },
- { class: 'target_branch js-target-branch-select',
+ { class: 'target_branch js-target-branch-select ref-name',
disabled: issuable.new_record?,
data: { placeholder: "Select branch" }})
- if issuable.new_record?
diff --git a/app/views/shared/issuable/form/_issue_assignee.html.haml b/app/views/shared/issuable/form/_issue_assignee.html.haml
index c33474ac3b4..66091d95a91 100644
--- a/app/views/shared/issuable/form/_issue_assignee.html.haml
+++ b/app/views/shared/issuable/form/_issue_assignee.html.haml
@@ -1,8 +1,9 @@
- issue = issuable
+- assignees = issue.assignees
.block.assignee
.sidebar-collapsed-icon.sidebar-collapsed-user{ data: { toggle: "tooltip", placement: "left", container: "body" }, title: (issuable.assignee_list) }
- - if issue.assignees.any?
- - issue.assignees.each do |assignee|
+ - if assignees.any?
+ - assignees.each do |assignee|
= link_to_member(@project, assignee, size: 24)
- else
= icon('user', 'aria-hidden': 'true')
@@ -12,8 +13,8 @@
- if can_edit_issuable
= link_to 'Edit', '#', class: 'edit-link pull-right'
.value.hide-collapsed
- - if issue.assignees.any?
- - issue.assignees.each do |assignee|
+ - if assignees.any?
+ - assignees.each do |assignee|
= link_to_member(@project, assignee, size: 32, extra_class: 'bold') do
%span.username
= assignee.to_reference
diff --git a/app/views/shared/milestones/_sidebar.html.haml b/app/views/shared/milestones/_sidebar.html.haml
index 5e8a2a0f5d8..9bb87640319 100644
--- a/app/views/shared/milestones/_sidebar.html.haml
+++ b/app/views/shared/milestones/_sidebar.html.haml
@@ -1,4 +1,4 @@
-- affix_offset = local_assigns.fetch(:affix_offset, "102")
+- affix_offset = local_assigns.fetch(:affix_offset, "50")
- project = local_assigns[:project]
%aside.right-sidebar.js-right-sidebar{ data: { "offset-top" => affix_offset, "spy" => "affix" }, class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' }
diff --git a/app/views/shared/notes/_edit.html.haml b/app/views/shared/notes/_edit.html.haml
index 4a020865828..f4b3aac29b4 100644
--- a/app/views/shared/notes/_edit.html.haml
+++ b/app/views/shared/notes/_edit.html.haml
@@ -1,3 +1 @@
-.original-note-content.hidden{ data: { post_url: note_url(note), target_id: note.noteable.id, target_type: note.noteable.class.name.underscore } }
- #{note.note}
%textarea.hidden.js-task-list-field.original-task-list{ data: {update_url: note_url(note) } }= note.note
diff --git a/app/views/shared/notes/_hints.html.haml b/app/views/shared/notes/_hints.html.haml
index 81d97eabe65..7ce6130de60 100644
--- a/app/views/shared/notes/_hints.html.haml
+++ b/app/views/shared/notes/_hints.html.haml
@@ -9,6 +9,27 @@
- else
is
supported
- %button.toolbar-button.markdown-selector{ type: 'button', tabindex: '-1' }
- = icon('file-image-o', class: 'toolbar-button-icon')
- Attach a file
+
+ %span.uploading-container
+ %span.uploading-progress-container.hide
+ = icon('file-image-o', class: 'toolbar-button-icon')
+ %span.attaching-file-message
+ -# Populated by app/assets/javascripts/dropzone_input.js
+ %span.uploading-progress 0%
+ %span.uploading-spinner
+ = icon('spinner spin', class: 'toolbar-button-icon')
+
+ %span.uploading-error-container.hide
+ %span.uploading-error-icon
+ = icon('file-image-o', class: 'toolbar-button-icon')
+ %span.uploading-error-message
+ -# Populated by app/assets/javascripts/dropzone_input.js
+ %button.retry-uploading-link{ type: 'button' } Try again
+ or
+ %button.attach-new-file.markdown-selector{ type: 'button' } attach a new file
+
+ %button.markdown-selector.button-attach-file{ type: 'button', tabindex: '-1' }
+ = icon('file-image-o', class: 'toolbar-button-icon')
+ Attach a file
+
+ %button.btn.btn-default.btn-xs.hide.button-cancel-uploading-files{ type: 'button' } Cancel
diff --git a/app/views/shared/notes/_note.html.haml b/app/views/shared/notes/_note.html.haml
index 5c1156b06fb..a7bf610b9c7 100644
--- a/app/views/shared/notes/_note.html.haml
+++ b/app/views/shared/notes/_note.html.haml
@@ -41,6 +41,8 @@
.note-text.md
= note.redacted_note_html
= edited_time_ago_with_tooltip(note, placement: 'bottom', html_class: 'note_edited_ago')
+ .original-note-content.hidden{ data: { post_url: note_url(note), target_id: note.noteable.id, target_type: note.noteable.class.name.underscore } }
+ #{note.note}
- if note_editable
= render 'shared/notes/edit', note: note
.note-awards
diff --git a/app/views/shared/notes/_notes_with_form.html.haml b/app/views/shared/notes/_notes_with_form.html.haml
index 9930cbd96d7..05bb1970e21 100644
--- a/app/views/shared/notes/_notes_with_form.html.haml
+++ b/app/views/shared/notes/_notes_with_form.html.haml
@@ -23,4 +23,4 @@
to post a comment
:javascript
- var notes = new Notes("#{notes_url}", #{@notes.map(&:id).to_json}, #{Time.now.to_i}, "#{diff_view}")
+ var notes = new Notes("#{notes_url}", #{@notes.map(&:id).to_json}, #{Time.now.to_i}, "#{diff_view}", false)
diff --git a/app/views/shared/notifications/_custom_notifications.html.haml b/app/views/shared/notifications/_custom_notifications.html.haml
index 708adbc38f1..183ed34fba1 100644
--- a/app/views/shared/notifications/_custom_notifications.html.haml
+++ b/app/views/shared/notifications/_custom_notifications.html.haml
@@ -1,9 +1,9 @@
-.modal.fade{ tabindex: "-1", role: "dialog", id: notifications_menu_identifier("modal", notification_setting), aria: { labelledby: "custom-notifications-title" } }
+.modal.fade{ tabindex: "-1", role: "dialog", id: notifications_menu_identifier("modal", notification_setting), "aria-labelledby": "custom-notifications-title" }
.modal-dialog
.modal-content
.modal-header
- %button.close{ type: "button", data: { dismiss: "modal" }, aria: { label: "close" } }
- %span{ aria: { hidden: "true" } } ×
+ %button.close{ type: "button", "aria-label": "close", data: { dismiss: "modal" } }
+ %span{ "aria-hidden": "true" } } ×
%h4#custom-notifications-title.modal-title
Custom notification events
diff --git a/app/views/shared/web_hooks/_form.html.haml b/app/views/shared/web_hooks/_form.html.haml
index 37c3e61912c..1f0e7629fb4 100644
--- a/app/views/shared/web_hooks/_form.html.haml
+++ b/app/views/shared/web_hooks/_form.html.haml
@@ -54,10 +54,10 @@
%p.light
This URL will be triggered when a merge request is created/updated/merged
%li
- = form.check_box :build_events, class: 'pull-left'
+ = form.check_box :job_events, class: 'pull-left'
.prepend-left-20
- = form.label :build_events, class: 'list-label' do
- %strong Jobs events
+ = form.label :job_events, class: 'list-label' do
+ %strong Job events
%p.light
This URL will be triggered when the job status changes
%li
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index 03e5dd97405..2b70d70e360 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -10,7 +10,7 @@
= auto_discovery_link_tag(:atom, user_url(@user, format: :atom), title: "#{@user.name} activity")
.user-profile
- .cover-block.user-cover-block
+ .cover-block.user-cover-block.layout-nav
.cover-controls
- if @user == current_user
= link_to profile_path, class: 'btn btn-gray has-tooltip', title: 'Edit profile', 'aria-label': 'Edit profile' do
@@ -56,11 +56,11 @@
= icon('skype')
- unless @user.linkedin.blank?
.profile-link-holder.middle-dot-divider
- = link_to "https://www.linkedin.com/in/#{@user.linkedin}", title: "LinkedIn" do
+ = link_to linkedin_url(@user), title: "LinkedIn" do
= icon('linkedin-square')
- unless @user.twitter.blank?
.profile-link-holder.middle-dot-divider
- = link_to "https://twitter.com/#{@user.twitter}", title: "Twitter" do
+ = link_to twitter_url(@user), title: "Twitter" do
= icon('twitter-square')
- unless @user.website_url.blank?
.profile-link-holder.middle-dot-divider
@@ -82,7 +82,7 @@
.scrolling-tabs-container
.fade-left= icon('angle-left')
.fade-right= icon('angle-right')
- %ul.nav-links.center.user-profile-nav.scrolling-tabs
+ %ul.nav-links.user-profile-nav.scrolling-tabs
%li.js-activity-tab
= link_to user_path, data: { target: 'div#activity', action: 'activity', toggle: 'tab' } do
Activity
diff --git a/app/workers/namespaceless_project_destroy_worker.rb b/app/workers/namespaceless_project_destroy_worker.rb
new file mode 100644
index 00000000000..bfae0c77700
--- /dev/null
+++ b/app/workers/namespaceless_project_destroy_worker.rb
@@ -0,0 +1,43 @@
+# Worker to destroy projects that do not have a namespace
+#
+# It destroys everything it can without having the info about the namespace it
+# used to belong to. Projects in this state should be rare.
+# The worker will reject doing anything for projects that *do* have a
+# namespace. For those use ProjectDestroyWorker instead.
+class NamespacelessProjectDestroyWorker
+ include Sidekiq::Worker
+ include DedicatedSidekiqQueue
+
+ def self.bulk_perform_async(args_list)
+ Sidekiq::Client.push_bulk('class' => self, 'queue' => sidekiq_options['queue'], 'args' => args_list)
+ end
+
+ def perform(project_id)
+ begin
+ project = Project.unscoped.find(project_id)
+ rescue ActiveRecord::RecordNotFound
+ return
+ end
+ return unless project.namespace_id.nil? # Reject doing anything for projects that *do* have a namespace
+
+ project.team.truncate
+
+ unlink_fork(project) if project.forked?
+
+ # Override Project#remove_pages for this instance so it doesn't do anything
+ def project.remove_pages
+ end
+
+ project.destroy!
+ end
+
+ private
+
+ def unlink_fork(project)
+ merge_requests = project.forked_from_project.merge_requests.opened.from_project(project)
+
+ merge_requests.update_all(state: 'closed')
+
+ project.forked_project_link.destroy
+ end
+end
diff --git a/app/workers/pipeline_schedule_worker.rb b/app/workers/pipeline_schedule_worker.rb
index a449a765f7b..7eb0e84acb2 100644
--- a/app/workers/pipeline_schedule_worker.rb
+++ b/app/workers/pipeline_schedule_worker.rb
@@ -3,8 +3,14 @@ class PipelineScheduleWorker
include CronjobQueue
def perform
- Ci::PipelineSchedule.active.where("next_run_at < ?", Time.now).find_each do |schedule|
+ Ci::PipelineSchedule.active.where("next_run_at < ?", Time.now)
+ .preload(:owner, :project).find_each do |schedule|
begin
+ unless schedule.runnable_by_owner?
+ schedule.deactivate!
+ next
+ end
+
Ci::CreatePipelineService.new(schedule.project,
schedule.owner,
ref: schedule.ref)
diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb
index 127d8dfbb61..c29571d3c62 100644
--- a/app/workers/post_receive.rb
+++ b/app/workers/post_receive.rb
@@ -20,13 +20,32 @@ class PostReceive
# Nothing defined here yet.
else
process_project_changes(post_received)
+ process_repository_update(post_received)
end
end
- def process_project_changes(post_received)
- post_received.changes.each do |change|
- oldrev, newrev, ref = change.strip.split(' ')
+ def process_repository_update(post_received)
+ changes = []
+ refs = Set.new
+
+ post_received.changes_refs do |oldrev, newrev, ref|
+ @user ||= post_received.identify(newrev)
+ unless @user
+ log("Triggered hook for non-existing user \"#{post_received.identifier}\"")
+ return false
+ end
+
+ changes << Gitlab::DataBuilder::Repository.single_change(oldrev, newrev, ref)
+ refs << ref
+ end
+
+ hook_data = Gitlab::DataBuilder::Repository.update(post_received.project, @user, changes, refs.to_a)
+ SystemHooksService.new.execute_hooks(hook_data, :repository_update_hooks)
+ end
+
+ def process_project_changes(post_received)
+ post_received.changes_refs do |oldrev, newrev, ref|
@user ||= post_received.identify(newrev)
unless @user
diff --git a/app/workers/repository_check/clear_worker.rb b/app/workers/repository_check/clear_worker.rb
index 1f1b38540ee..85bc9103538 100644
--- a/app/workers/repository_check/clear_worker.rb
+++ b/app/workers/repository_check/clear_worker.rb
@@ -8,7 +8,7 @@ module RepositoryCheck
Project.select(:id).find_in_batches(batch_size: 100) do |batch|
Project.where(id: batch.map(&:id)).update_all(
last_repository_check_failed: nil,
- last_repository_check_at: nil,
+ last_repository_check_at: nil
)
end
end
diff --git a/app/workers/repository_check/single_repository_worker.rb b/app/workers/repository_check/single_repository_worker.rb
index 3d8bfc6fc6c..164586cf0b7 100644
--- a/app/workers/repository_check/single_repository_worker.rb
+++ b/app/workers/repository_check/single_repository_worker.rb
@@ -7,7 +7,7 @@ module RepositoryCheck
project = Project.find(project_id)
project.update_columns(
last_repository_check_failed: !check(project),
- last_repository_check_at: Time.now,
+ last_repository_check_at: Time.now
)
end
diff --git a/changelogs/unreleased/2247-emails-forwarded-to-service-desk-email-don-t-come.yml b/changelogs/unreleased/2247-emails-forwarded-to-service-desk-email-don-t-come.yml
deleted file mode 100644
index f062143960e..00000000000
--- a/changelogs/unreleased/2247-emails-forwarded-to-service-desk-email-don-t-come.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Handle incoming emails from aliases correctly
-merge_request:
-author:
diff --git a/changelogs/unreleased/24373-warning-message-go-away.yml b/changelogs/unreleased/24373-warning-message-go-away.yml
new file mode 100644
index 00000000000..c0f2fd260ba
--- /dev/null
+++ b/changelogs/unreleased/24373-warning-message-go-away.yml
@@ -0,0 +1,4 @@
+---
+title: 'Notes: Warning message should go away once resolved'
+merge_request: 10823
+author: Jacopo Beschi @jacopo-beschi
diff --git a/changelogs/unreleased/26325-system-hooks.yml b/changelogs/unreleased/26325-system-hooks.yml
new file mode 100644
index 00000000000..62b8adaeccd
--- /dev/null
+++ b/changelogs/unreleased/26325-system-hooks.yml
@@ -0,0 +1,4 @@
+---
+title: 'Backported new SystemHook event: `repository_update`'
+merge_request: 11140
+author:
diff --git a/changelogs/unreleased/30286-ci-badge-component.yml b/changelogs/unreleased/30286-ci-badge-component.yml
new file mode 100644
index 00000000000..13c2a4598c8
--- /dev/null
+++ b/changelogs/unreleased/30286-ci-badge-component.yml
@@ -0,0 +1,4 @@
+---
+title: Refactor all CI vue badges to use the same vue component
+merge_request:
+author:
diff --git a/changelogs/unreleased/30827-changes-to-audit-log.yml b/changelogs/unreleased/30827-changes-to-audit-log.yml
new file mode 100644
index 00000000000..32db3bf8e95
--- /dev/null
+++ b/changelogs/unreleased/30827-changes-to-audit-log.yml
@@ -0,0 +1,4 @@
+---
+title: Renamed users 'Audit Log'' to 'Authentication Log'
+merge_request: 11400
+author:
diff --git a/changelogs/unreleased/30949-empty-states.yml b/changelogs/unreleased/30949-empty-states.yml
new file mode 100644
index 00000000000..bef87a954b7
--- /dev/null
+++ b/changelogs/unreleased/30949-empty-states.yml
@@ -0,0 +1,4 @@
+---
+title: Center all empty states
+merge_request:
+author:
diff --git a/changelogs/unreleased/30973-network-graph-sorted-by-date-and-topo.yml b/changelogs/unreleased/30973-network-graph-sorted-by-date-and-topo.yml
deleted file mode 100644
index 42426c1865e..00000000000
--- a/changelogs/unreleased/30973-network-graph-sorted-by-date-and-topo.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Sort the network graph both by commit date and topographically
-merge_request: 11057
-author:
diff --git a/changelogs/unreleased/31106-tabs-alignment.yml b/changelogs/unreleased/31106-tabs-alignment.yml
new file mode 100644
index 00000000000..53da08cc32d
--- /dev/null
+++ b/changelogs/unreleased/31106-tabs-alignment.yml
@@ -0,0 +1,4 @@
+---
+title: prevent nav tabs from wrapping to new line
+merge_request:
+author:
diff --git a/changelogs/unreleased/31157-respect-project-features-in-wiki-search.yml b/changelogs/unreleased/31157-respect-project-features-in-wiki-search.yml
new file mode 100644
index 00000000000..721bb435a2e
--- /dev/null
+++ b/changelogs/unreleased/31157-respect-project-features-in-wiki-search.yml
@@ -0,0 +1,4 @@
+---
+title: Enforce project features when searching blobs and wikis
+merge_request:
+author:
diff --git a/changelogs/unreleased/31274-creating-schedule-trigger--causes-http-500-when-accessing-settings-ci_cd.yml b/changelogs/unreleased/31274-creating-schedule-trigger--causes-http-500-when-accessing-settings-ci_cd.yml
deleted file mode 100644
index b0c33ab3fa4..00000000000
--- a/changelogs/unreleased/31274-creating-schedule-trigger--causes-http-500-when-accessing-settings-ci_cd.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix error on CI/CD Settings page related to invalid pipeline trigger
-merge_request: 10948
-author: dosuken123
diff --git a/changelogs/unreleased/31384-new-issue-button-on-no-results-page-after-search-doesn-t-go-to-correct-form.yml b/changelogs/unreleased/31384-new-issue-button-on-no-results-page-after-search-doesn-t-go-to-correct-form.yml
new file mode 100644
index 00000000000..8d586616e07
--- /dev/null
+++ b/changelogs/unreleased/31384-new-issue-button-on-no-results-page-after-search-doesn-t-go-to-correct-form.yml
@@ -0,0 +1,4 @@
+---
+title: Remove 'New issue' button when issues search returns no results.
+merge_request: !11263
+author:
diff --git a/changelogs/unreleased/31474-issue-boards-sidebar-milestone-dropdown-should-not-be-multi-select.yml b/changelogs/unreleased/31474-issue-boards-sidebar-milestone-dropdown-should-not-be-multi-select.yml
new file mode 100644
index 00000000000..88e79e3b6ea
--- /dev/null
+++ b/changelogs/unreleased/31474-issue-boards-sidebar-milestone-dropdown-should-not-be-multi-select.yml
@@ -0,0 +1,4 @@
+---
+title: Disallow multiple selections for Milestone dropdown
+merge_request: 11084
+author:
diff --git a/changelogs/unreleased/31483-ordered-task-list.yml b/changelogs/unreleased/31483-ordered-task-list.yml
new file mode 100644
index 00000000000..c43915b3268
--- /dev/null
+++ b/changelogs/unreleased/31483-ordered-task-list.yml
@@ -0,0 +1,4 @@
+---
+title: Fix Ordered Task List Items
+merge_request: 31483
+author: Jared Deckard <jared.deckard@gmail.com>
diff --git a/changelogs/unreleased/31625-tag-editor-loses-all-inputs-when-you-try-to-add-a-tag-that-already-exists.yml b/changelogs/unreleased/31625-tag-editor-loses-all-inputs-when-you-try-to-add-a-tag-that-already-exists.yml
new file mode 100644
index 00000000000..aae760b0ef5
--- /dev/null
+++ b/changelogs/unreleased/31625-tag-editor-loses-all-inputs-when-you-try-to-add-a-tag-that-already-exists.yml
@@ -0,0 +1,4 @@
+---
+title: Keep input data after creating a tag that already exists
+merge_request: 11155
+author:
diff --git a/changelogs/unreleased/31781-print-rendered-files-not-possible.yml b/changelogs/unreleased/31781-print-rendered-files-not-possible.yml
new file mode 100644
index 00000000000..14915823ff7
--- /dev/null
+++ b/changelogs/unreleased/31781-print-rendered-files-not-possible.yml
@@ -0,0 +1,4 @@
+---
+title: Include the blob content when printing a blob page
+merge_request: 11247
+author:
diff --git a/changelogs/unreleased/31886-remover-comment-load-spinner.yml b/changelogs/unreleased/31886-remover-comment-load-spinner.yml
new file mode 100644
index 00000000000..4b36538064a
--- /dev/null
+++ b/changelogs/unreleased/31886-remover-comment-load-spinner.yml
@@ -0,0 +1,4 @@
+---
+title: Remove spinner from loading comment
+merge_request:
+author:
diff --git a/changelogs/unreleased/31902-namespace-recent-searches-to-project.yml b/changelogs/unreleased/31902-namespace-recent-searches-to-project.yml
new file mode 100644
index 00000000000..e00eb6d8f72
--- /dev/null
+++ b/changelogs/unreleased/31902-namespace-recent-searches-to-project.yml
@@ -0,0 +1,4 @@
+---
+title: Scope issue/merge request recent searches to project
+merge_request:
+author:
diff --git a/changelogs/unreleased/31998-pipelines-empty-state.yml b/changelogs/unreleased/31998-pipelines-empty-state.yml
new file mode 100644
index 00000000000..78ae222255e
--- /dev/null
+++ b/changelogs/unreleased/31998-pipelines-empty-state.yml
@@ -0,0 +1,4 @@
+---
+title: Fix Pipelines table empty state - only render empty state if we receive 0 pipelines
+merge_request:
+author:
diff --git a/changelogs/unreleased/32086-atwho-is-still-enabled-for-personal-snippet-comments-form.yml b/changelogs/unreleased/32086-atwho-is-still-enabled-for-personal-snippet-comments-form.yml
new file mode 100644
index 00000000000..0fd248e0400
--- /dev/null
+++ b/changelogs/unreleased/32086-atwho-is-still-enabled-for-personal-snippet-comments-form.yml
@@ -0,0 +1,4 @@
+---
+title: Disable reference prefixes in notes for Snippets
+merge_request: 11278
+author:
diff --git a/changelogs/unreleased/32178-prevent-merge-on-sha-change.yml b/changelogs/unreleased/32178-prevent-merge-on-sha-change.yml
new file mode 100644
index 00000000000..d3208973de6
--- /dev/null
+++ b/changelogs/unreleased/32178-prevent-merge-on-sha-change.yml
@@ -0,0 +1,4 @@
+---
+title: Add state to MR widget that prevent merges when branch changes after page load
+merge_request: 11316
+author:
diff --git a/changelogs/unreleased/32219-speed-up-yarn-install-in-ci-by-utilizing-inter-pipeline-cache.yml b/changelogs/unreleased/32219-speed-up-yarn-install-in-ci-by-utilizing-inter-pipeline-cache.yml
new file mode 100644
index 00000000000..7fb3cb3a30b
--- /dev/null
+++ b/changelogs/unreleased/32219-speed-up-yarn-install-in-ci-by-utilizing-inter-pipeline-cache.yml
@@ -0,0 +1,4 @@
+---
+title: Cache npm modules between pipelines with yarn to speed up setup-test-env
+merge_request: 11343
+author:
diff --git a/changelogs/unreleased/32340-correct-jobs-api-documentation b/changelogs/unreleased/32340-correct-jobs-api-documentation
new file mode 100644
index 00000000000..4ada62356eb
--- /dev/null
+++ b/changelogs/unreleased/32340-correct-jobs-api-documentation
@@ -0,0 +1,4 @@
+---
+title: "Correction to documention for manual steps on the Jobs API"
+merge_request: 11411
+author: Zac Sturgess \ No newline at end of file
diff --git a/changelogs/unreleased/32395-duplicate-string-in-https-docs-gitlab-com-ce-administration-environment_variables-html.yml b/changelogs/unreleased/32395-duplicate-string-in-https-docs-gitlab-com-ce-administration-environment_variables-html.yml
new file mode 100644
index 00000000000..d2be3d6cc4b
--- /dev/null
+++ b/changelogs/unreleased/32395-duplicate-string-in-https-docs-gitlab-com-ce-administration-environment_variables-html.yml
@@ -0,0 +1,4 @@
+---
+title: Removes duplicate environment variable in documentation
+merge_request:
+author:
diff --git a/changelogs/unreleased/adam-influxdb-hostname.yml b/changelogs/unreleased/adam-influxdb-hostname.yml
new file mode 100644
index 00000000000..ab201ae7894
--- /dev/null
+++ b/changelogs/unreleased/adam-influxdb-hostname.yml
@@ -0,0 +1,4 @@
+---
+title: Allow GitLab instance to start when InfluxDB hostname cannot be resolved
+merge_request: 11356
+author:
diff --git a/changelogs/unreleased/add-index-for-auto_canceled_by_id-mysql.yml b/changelogs/unreleased/add-index-for-auto_canceled_by_id-mysql.yml
new file mode 100644
index 00000000000..eac78e9ee1f
--- /dev/null
+++ b/changelogs/unreleased/add-index-for-auto_canceled_by_id-mysql.yml
@@ -0,0 +1,4 @@
+---
+title: Add indices for auto_canceled_by_id for ci_pipelines and ci_builds on PostgreSQL
+merge_request: 11034
+author:
diff --git a/changelogs/unreleased/add_ability_to_cancel_attaching_file_and_redesign_attaching_files_ui.yml b/changelogs/unreleased/add_ability_to_cancel_attaching_file_and_redesign_attaching_files_ui.yml
new file mode 100644
index 00000000000..fcf4efa2846
--- /dev/null
+++ b/changelogs/unreleased/add_ability_to_cancel_attaching_file_and_redesign_attaching_files_ui.yml
@@ -0,0 +1,4 @@
+---
+title: Add an ability to cancel attaching file and redesign attaching files UI
+merge_request: 9431
+author: blackst0ne
diff --git a/changelogs/unreleased/allow_numeric_values_in_gitlab_ci_yml.yml b/changelogs/unreleased/allow_numeric_values_in_gitlab_ci_yml.yml
new file mode 100644
index 00000000000..8c7fa53a18b
--- /dev/null
+++ b/changelogs/unreleased/allow_numeric_values_in_gitlab_ci_yml.yml
@@ -0,0 +1,4 @@
+---
+title: Allow numeric values in gitlab-ci.yml
+merge_request: 10607
+author: blackst0ne
diff --git a/changelogs/unreleased/branch-name-escape.yml b/changelogs/unreleased/branch-name-escape.yml
new file mode 100644
index 00000000000..bf46235fd79
--- /dev/null
+++ b/changelogs/unreleased/branch-name-escape.yml
@@ -0,0 +1,4 @@
+---
+title: Fixed branches dropdown rendering branch names as HTML
+merge_request:
+author:
diff --git a/changelogs/unreleased/bvl-markup-pipeline.yml b/changelogs/unreleased/bvl-markup-pipeline.yml
new file mode 100644
index 00000000000..d73bad03340
--- /dev/null
+++ b/changelogs/unreleased/bvl-markup-pipeline.yml
@@ -0,0 +1,4 @@
+---
+title: Make Asciidoc & other markup go through pipeline to prevent XSS
+merge_request:
+author:
diff --git a/changelogs/unreleased/bvl-rename-build-events-to-job-events.yml b/changelogs/unreleased/bvl-rename-build-events-to-job-events.yml
new file mode 100644
index 00000000000..2ce01a71361
--- /dev/null
+++ b/changelogs/unreleased/bvl-rename-build-events-to-job-events.yml
@@ -0,0 +1,4 @@
+---
+title: Rename build_events to job_events
+merge_request: 11287
+author:
diff --git a/changelogs/unreleased/bvl-validate-urls-in-markdown-using-uri.yml b/changelogs/unreleased/bvl-validate-urls-in-markdown-using-uri.yml
new file mode 100644
index 00000000000..03c4e531d73
--- /dev/null
+++ b/changelogs/unreleased/bvl-validate-urls-in-markdown-using-uri.yml
@@ -0,0 +1,4 @@
+---
+title: Validate URLs in markdown using URI to detect the host correctly
+merge_request:
+author:
diff --git a/changelogs/unreleased/counters_cache_invalidation.yml b/changelogs/unreleased/counters_cache_invalidation.yml
new file mode 100644
index 00000000000..1e78765ec10
--- /dev/null
+++ b/changelogs/unreleased/counters_cache_invalidation.yml
@@ -0,0 +1,4 @@
+---
+title: Invalidate cache for issue and MR counters more granularly
+merge_request:
+author:
diff --git a/changelogs/unreleased/disable-usage-ping-2.yml b/changelogs/unreleased/disable-usage-ping-2.yml
new file mode 100644
index 00000000000..4abd325f120
--- /dev/null
+++ b/changelogs/unreleased/disable-usage-ping-2.yml
@@ -0,0 +1,4 @@
+---
+title: Add hostname to usage ping
+merge_request:
+author:
diff --git a/changelogs/unreleased/disable-usage-ping.yml b/changelogs/unreleased/disable-usage-ping.yml
new file mode 100644
index 00000000000..5438eb56dba
--- /dev/null
+++ b/changelogs/unreleased/disable-usage-ping.yml
@@ -0,0 +1,4 @@
+---
+title: Allow usage ping to be disabled completely in gitlab.yml
+merge_request:
+author:
diff --git a/changelogs/unreleased/dm-async-tree-readme.yml b/changelogs/unreleased/dm-async-tree-readme.yml
new file mode 100644
index 00000000000..fb1cfeb210a
--- /dev/null
+++ b/changelogs/unreleased/dm-async-tree-readme.yml
@@ -0,0 +1,4 @@
+---
+title: Load tree readme asynchronously
+merge_request:
+author:
diff --git a/changelogs/unreleased/dm-auxiliary-viewers.yml b/changelogs/unreleased/dm-auxiliary-viewers.yml
new file mode 100644
index 00000000000..ba73a499115
--- /dev/null
+++ b/changelogs/unreleased/dm-auxiliary-viewers.yml
@@ -0,0 +1,5 @@
+---
+title: Display extra info about files on .gitlab-ci.yml, .gitlab/route-map.yml and
+ LICENSE blob pages
+merge_request:
+author:
diff --git a/changelogs/unreleased/dm-catch-uri-errors.yml b/changelogs/unreleased/dm-catch-uri-errors.yml
new file mode 100644
index 00000000000..8b635d321b3
--- /dev/null
+++ b/changelogs/unreleased/dm-catch-uri-errors.yml
@@ -0,0 +1,4 @@
+---
+title: Catch all URI errors in ExternalLinkFilter
+merge_request:
+author:
diff --git a/changelogs/unreleased/dm-consistent-commit-sha-style.yml b/changelogs/unreleased/dm-consistent-commit-sha-style.yml
new file mode 100644
index 00000000000..b6dace34d9b
--- /dev/null
+++ b/changelogs/unreleased/dm-consistent-commit-sha-style.yml
@@ -0,0 +1,4 @@
+---
+title: Consistently use monospace font for commit SHAs and branch and tag names
+merge_request:
+author:
diff --git a/changelogs/unreleased/dm-copy-mr-source-branch-as-gfm.yml b/changelogs/unreleased/dm-copy-mr-source-branch-as-gfm.yml
new file mode 100644
index 00000000000..708c82604ad
--- /dev/null
+++ b/changelogs/unreleased/dm-copy-mr-source-branch-as-gfm.yml
@@ -0,0 +1,4 @@
+---
+title: Paste a copied MR source branch name as code when pasted into a GFM form
+merge_request:
+author:
diff --git a/changelogs/unreleased/dm-dependency-linker-gemfile.yml b/changelogs/unreleased/dm-dependency-linker-gemfile.yml
new file mode 100644
index 00000000000..2d4167a1be5
--- /dev/null
+++ b/changelogs/unreleased/dm-dependency-linker-gemfile.yml
@@ -0,0 +1,4 @@
+---
+title: Autolink package names in Gemfile
+merge_request:
+author:
diff --git a/changelogs/unreleased/dm-tree-last-commit.yml b/changelogs/unreleased/dm-tree-last-commit.yml
new file mode 100644
index 00000000000..50619fd6ef2
--- /dev/null
+++ b/changelogs/unreleased/dm-tree-last-commit.yml
@@ -0,0 +1,4 @@
+---
+title: Show last commit for current tree on tree page
+merge_request:
+author:
diff --git a/changelogs/unreleased/dont-blow-up-when-email-has-no-references-header.yml b/changelogs/unreleased/dont-blow-up-when-email-has-no-references-header.yml
deleted file mode 100644
index a4345b70744..00000000000
--- a/changelogs/unreleased/dont-blow-up-when-email-has-no-references-header.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Gracefully handle failures for incoming emails which do not match on the To
- header, and have no References header
-merge_request:
-author:
diff --git a/changelogs/unreleased/dturner-username.yml b/changelogs/unreleased/dturner-username.yml
new file mode 100644
index 00000000000..09ba822ee65
--- /dev/null
+++ b/changelogs/unreleased/dturner-username.yml
@@ -0,0 +1,4 @@
+---
+title: add username field to push webhook
+merge_request:
+author: David Turner
diff --git a/changelogs/unreleased/dz-project-list-cache-key.yml b/changelogs/unreleased/dz-project-list-cache-key.yml
new file mode 100644
index 00000000000..9e4826e686a
--- /dev/null
+++ b/changelogs/unreleased/dz-project-list-cache-key.yml
@@ -0,0 +1,4 @@
+---
+title: Use route.cache_key for project list cache key
+merge_request: 11325
+author:
diff --git a/changelogs/unreleased/enable-auto-cancelling-by-default.yml b/changelogs/unreleased/enable-auto-cancelling-by-default.yml
new file mode 100644
index 00000000000..8b1659bf38b
--- /dev/null
+++ b/changelogs/unreleased/enable-auto-cancelling-by-default.yml
@@ -0,0 +1,4 @@
+---
+title: Enable cancelling non-HEAD pending pipelines by default for all projects
+merge_request: 11023
+author:
diff --git a/changelogs/unreleased/environments-button-open-same-tab.yml b/changelogs/unreleased/environments-button-open-same-tab.yml
new file mode 100644
index 00000000000..60b0d389e7f
--- /dev/null
+++ b/changelogs/unreleased/environments-button-open-same-tab.yml
@@ -0,0 +1,5 @@
+---
+title: Removed the target=_blank from the monitoring component to prevent opening
+ a new tab
+merge_request:
+author:
diff --git a/changelogs/unreleased/fix-allow-accessing-appearance-images.yml b/changelogs/unreleased/fix-allow-accessing-appearance-images.yml
new file mode 100644
index 00000000000..81118162bab
--- /dev/null
+++ b/changelogs/unreleased/fix-allow-accessing-appearance-images.yml
@@ -0,0 +1,4 @@
+---
+title: Fixes the 500 when accessing customized appearance logos
+merge_request: 11479
+author: Alexis Reigel
diff --git a/changelogs/unreleased/fix-conflict-resolution-with-corrupt-repos.yml b/changelogs/unreleased/fix-conflict-resolution-with-corrupt-repos.yml
new file mode 100644
index 00000000000..19a3c56e478
--- /dev/null
+++ b/changelogs/unreleased/fix-conflict-resolution-with-corrupt-repos.yml
@@ -0,0 +1,5 @@
+---
+title: Prevent further repository corruption when resolving conflicts from a fork
+ where both the fork and upstream projects require housekeeping
+merge_request:
+author:
diff --git a/changelogs/unreleased/fix-github-import.yml b/changelogs/unreleased/fix-github-import.yml
new file mode 100644
index 00000000000..3a57152f7a8
--- /dev/null
+++ b/changelogs/unreleased/fix-github-import.yml
@@ -0,0 +1,4 @@
+---
+title: Fix token interpolation when setting the Github remote
+merge_request:
+author:
diff --git a/changelogs/unreleased/fix-import-export-missing-attributes.yml b/changelogs/unreleased/fix-import-export-missing-attributes.yml
deleted file mode 100644
index a1338b4eb48..00000000000
--- a/changelogs/unreleased/fix-import-export-missing-attributes.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add missing project attributes to Import/Export
-merge_request:
-author:
diff --git a/changelogs/unreleased/fix-search-not-highlighting.yml b/changelogs/unreleased/fix-search-not-highlighting.yml
deleted file mode 100644
index a624530ebd1..00000000000
--- a/changelogs/unreleased/fix-search-not-highlighting.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fixed search terms not correctly highlighting
-merge_request:
-author:
diff --git a/changelogs/unreleased/fix-trailing-space-mr-widget.yml b/changelogs/unreleased/fix-trailing-space-mr-widget.yml
new file mode 100644
index 00000000000..86be36f3cf4
--- /dev/null
+++ b/changelogs/unreleased/fix-trailing-space-mr-widget.yml
@@ -0,0 +1,4 @@
+---
+title: Refactored the anchor tag to remove the trailing space in the target branch
+merge_request:
+author:
diff --git a/changelogs/unreleased/get_rid_of_pluck.yml b/changelogs/unreleased/get_rid_of_pluck.yml
new file mode 100644
index 00000000000..987af5e9317
--- /dev/null
+++ b/changelogs/unreleased/get_rid_of_pluck.yml
@@ -0,0 +1,4 @@
+---
+title: Issue assignees are now removed without loading unnecessary data into memory
+merge_request:
+author:
diff --git a/changelogs/unreleased/gitaly-local-branches.yml b/changelogs/unreleased/gitaly-local-branches.yml
new file mode 100644
index 00000000000..adcc0fa6280
--- /dev/null
+++ b/changelogs/unreleased/gitaly-local-branches.yml
@@ -0,0 +1,4 @@
+---
+title: Add suport for find_local_branches GRPC from Gitaly
+merge_request: 10059
+author:
diff --git a/changelogs/unreleased/hamlit-xss-fix.yml b/changelogs/unreleased/hamlit-xss-fix.yml
new file mode 100644
index 00000000000..ba4713846e9
--- /dev/null
+++ b/changelogs/unreleased/hamlit-xss-fix.yml
@@ -0,0 +1,4 @@
+---
+title: Fix for XSS in project import view caused by Hamlit filter usage.
+merge_request:
+author:
diff --git a/changelogs/unreleased/issue-templates-summary-lines.yml b/changelogs/unreleased/issue-templates-summary-lines.yml
new file mode 100644
index 00000000000..0c8c3d884ce
--- /dev/null
+++ b/changelogs/unreleased/issue-templates-summary-lines.yml
@@ -0,0 +1,4 @@
+---
+title: Add summary lines for collapsed details in the bug report template
+merge_request:
+author:
diff --git a/changelogs/unreleased/issue_api_change.yml b/changelogs/unreleased/issue_api_change.yml
new file mode 100644
index 00000000000..3ad2d57317c
--- /dev/null
+++ b/changelogs/unreleased/issue_api_change.yml
@@ -0,0 +1,5 @@
+---
+title: 'Issue API change: assignee_id parameter and assignee object in a response
+ have been deprecated'
+merge_request:
+author:
diff --git a/changelogs/unreleased/merge-request-poll-json-endpoint.yml b/changelogs/unreleased/merge-request-poll-json-endpoint.yml
deleted file mode 100644
index 6c41984e9b7..00000000000
--- a/changelogs/unreleased/merge-request-poll-json-endpoint.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fixed bug where merge request JSON would be displayed
-merge_request:
-author:
diff --git a/changelogs/unreleased/omega-submodules.yml b/changelogs/unreleased/omega-submodules.yml
new file mode 100644
index 00000000000..1488eb72174
--- /dev/null
+++ b/changelogs/unreleased/omega-submodules.yml
@@ -0,0 +1,4 @@
+---
+title: 'Repository browser: handle in-repository submodule urls'
+merge_request:
+author: David Turner
diff --git a/changelogs/unreleased/pipeline-schedules-callout-docs-url.yml b/changelogs/unreleased/pipeline-schedules-callout-docs-url.yml
new file mode 100644
index 00000000000..b21bb162380
--- /dev/null
+++ b/changelogs/unreleased/pipeline-schedules-callout-docs-url.yml
@@ -0,0 +1,4 @@
+---
+title: Pass docsUrl to pipeline schedules callout component.
+merge_request: !1126
+author:
diff --git a/changelogs/unreleased/protected-branches-no-one-merge.yml b/changelogs/unreleased/protected-branches-no-one-merge.yml
new file mode 100644
index 00000000000..52d93793f3d
--- /dev/null
+++ b/changelogs/unreleased/protected-branches-no-one-merge.yml
@@ -0,0 +1,4 @@
+---
+title: Allow 'no one' as an option for allowed to merge on a procted branch
+merge_request:
+author:
diff --git a/changelogs/unreleased/remove-old-isobject.yml b/changelogs/unreleased/remove-old-isobject.yml
new file mode 100644
index 00000000000..67b18642253
--- /dev/null
+++ b/changelogs/unreleased/remove-old-isobject.yml
@@ -0,0 +1,4 @@
+---
+title: Remove unused code and uses underscore
+merge_request:
+author:
diff --git a/changelogs/unreleased/rs-sanitize-submodule-urls.yml b/changelogs/unreleased/rs-sanitize-submodule-urls.yml
new file mode 100644
index 00000000000..463b3695687
--- /dev/null
+++ b/changelogs/unreleased/rs-sanitize-submodule-urls.yml
@@ -0,0 +1,4 @@
+---
+title: Sanitize submodule URLs before linking to them in the file tree view
+merge_request:
+author:
diff --git a/changelogs/unreleased/sh-fix-container-registry-s3-redirects.yml b/changelogs/unreleased/sh-fix-container-registry-s3-redirects.yml
new file mode 100644
index 00000000000..1e783811b66
--- /dev/null
+++ b/changelogs/unreleased/sh-fix-container-registry-s3-redirects.yml
@@ -0,0 +1,4 @@
+---
+title: Properly handle container registry redirects to fix metadata stored on a S3 backend
+merge_request:
+author:
diff --git a/changelogs/unreleased/snippets-finder-visibility.yml b/changelogs/unreleased/snippets-finder-visibility.yml
new file mode 100644
index 00000000000..fde2262cc8d
--- /dev/null
+++ b/changelogs/unreleased/snippets-finder-visibility.yml
@@ -0,0 +1,4 @@
+---
+title: Refactor snippets finder & dont return internal snippets for external users
+merge_request:
+author:
diff --git a/changelogs/unreleased/snippets_visibility.yml b/changelogs/unreleased/snippets_visibility.yml
new file mode 100644
index 00000000000..4c10c6882ab
--- /dev/null
+++ b/changelogs/unreleased/snippets_visibility.yml
@@ -0,0 +1,4 @@
+---
+title: Fix snippets visibility for show action - external users can not see internal snippets
+merge_request:
+author:
diff --git a/changelogs/unreleased/store-retried-in-database-for-ci-builds.yml b/changelogs/unreleased/store-retried-in-database-for-ci-builds.yml
new file mode 100644
index 00000000000..9185113f51c
--- /dev/null
+++ b/changelogs/unreleased/store-retried-in-database-for-ci-builds.yml
@@ -0,0 +1,4 @@
+---
+title: Store retried in database for CI Builds
+merge_request:
+author:
diff --git a/changelogs/unreleased/tc-clean-pending-delete-projects.yml b/changelogs/unreleased/tc-clean-pending-delete-projects.yml
new file mode 100644
index 00000000000..31b43999c31
--- /dev/null
+++ b/changelogs/unreleased/tc-clean-pending-delete-projects.yml
@@ -0,0 +1,4 @@
+---
+title: Add post-deploy migration to clean up projects in `pending_delete` state
+merge_request: 11044
+author:
diff --git a/changelogs/unreleased/tc-fix-private-subgroups-shown.yml b/changelogs/unreleased/tc-fix-private-subgroups-shown.yml
new file mode 100644
index 00000000000..82e03921854
--- /dev/null
+++ b/changelogs/unreleased/tc-fix-private-subgroups-shown.yml
@@ -0,0 +1,4 @@
+---
+title: "Do not show private groups on subgroups page if user doesn't have access to"
+merge_request:
+author:
diff --git a/changelogs/unreleased/update-admin-health-page.yml b/changelogs/unreleased/update-admin-health-page.yml
new file mode 100644
index 00000000000..51aa6682b49
--- /dev/null
+++ b/changelogs/unreleased/update-admin-health-page.yml
@@ -0,0 +1,5 @@
+---
+title: Added application readiness endpoints to the monitoring health check admin
+ view
+merge_request:
+author:
diff --git a/changelogs/unreleased/winh-pipeline-author-link.yml b/changelogs/unreleased/winh-pipeline-author-link.yml
new file mode 100644
index 00000000000..1b903d1e357
--- /dev/null
+++ b/changelogs/unreleased/winh-pipeline-author-link.yml
@@ -0,0 +1,4 @@
+---
+title: Link to commit author user page from pipelines
+merge_request: 11100
+author:
diff --git a/changelogs/unreleased/zj-clean-up-ci-variables-table.yml b/changelogs/unreleased/zj-clean-up-ci-variables-table.yml
new file mode 100644
index 00000000000..ea2db40d590
--- /dev/null
+++ b/changelogs/unreleased/zj-clean-up-ci-variables-table.yml
@@ -0,0 +1,4 @@
+---
+title: Cleanup ci_variables schema and table
+merge_request:
+author:
diff --git a/changelogs/unreleased/zj-pipeline-schedule-owner.yml b/changelogs/unreleased/zj-pipeline-schedule-owner.yml
new file mode 100644
index 00000000000..be704e173ab
--- /dev/null
+++ b/changelogs/unreleased/zj-pipeline-schedule-owner.yml
@@ -0,0 +1,4 @@
+---
+title: Add foreign key for pipeline schedule owner
+merge_request: 11233
+author:
diff --git a/config/application.rb b/config/application.rb
index bf3fb7a18c1..95ba6774916 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -106,6 +106,7 @@ module Gitlab
config.assets.precompile << "xterm/xterm.css"
config.assets.precompile << "lib/ace.js"
config.assets.precompile << "vendor/assets/fonts/*"
+ config.assets.precompile << "test.css"
# Version of your assets, change this if you want to expire all your assets
config.assets.version = '1.0'
diff --git a/config/environments/production.rb b/config/environments/production.rb
index a9d8ac4b6d4..82a19085b1d 100644
--- a/config/environments/production.rb
+++ b/config/environments/production.rb
@@ -16,7 +16,7 @@ Rails.application.configure do
# config.assets.css_compressor = :sass
# Don't fallback to assets pipeline if a precompiled asset is missed
- config.assets.compile = true
+ config.assets.compile = false
# Generate digests for assets URLs
config.assets.digest = true
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index 6097ae6534e..5a90830b5b3 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -241,6 +241,7 @@ Settings.gitlab['domain_whitelist'] ||= []
Settings.gitlab['import_sources'] ||= %w[github bitbucket gitlab google_code fogbugz git gitlab_project gitea]
Settings.gitlab['trusted_proxies'] ||= []
Settings.gitlab['no_todos_messages'] ||= YAML.load_file(Rails.root.join('config', 'no_todos_messages.yml'))
+Settings.gitlab['usage_ping_enabled'] = true if Settings.gitlab['usage_ping_enabled'].nil?
#
# CI
@@ -324,7 +325,7 @@ Settings.cron_jobs['stuck_ci_jobs_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['stuck_ci_jobs_worker']['cron'] ||= '0 * * * *'
Settings.cron_jobs['stuck_ci_jobs_worker']['job_class'] = 'StuckCiJobsWorker'
Settings.cron_jobs['pipeline_schedule_worker'] ||= Settingslogic.new({})
-Settings.cron_jobs['pipeline_schedule_worker']['cron'] ||= '0 */12 * * *'
+Settings.cron_jobs['pipeline_schedule_worker']['cron'] ||= '19 * * * *'
Settings.cron_jobs['pipeline_schedule_worker']['job_class'] = 'PipelineScheduleWorker'
Settings.cron_jobs['expire_build_artifacts_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['expire_build_artifacts_worker']['cron'] ||= '50 * * * *'
diff --git a/config/initializers/8_gitaly.rb b/config/initializers/8_gitaly.rb
index 42ec7240b0f..31c7c91d78f 100644
--- a/config/initializers/8_gitaly.rb
+++ b/config/initializers/8_gitaly.rb
@@ -1,6 +1,8 @@
require 'uri'
-# Make sure we initialize our Gitaly channels before Sidekiq starts multi-threaded execution.
if Gitlab.config.gitaly.enabled || Rails.env.test?
- Gitlab::GitalyClient.configure_channels
+ Gitlab.config.repositories.storages.keys.each do |storage|
+ # Force validation of each address
+ Gitlab::GitalyClient.address(storage)
+ end
end
diff --git a/config/initializers/ar_monkey_patch.rb b/config/initializers/ar_monkey_patch.rb
index 6979f4641b0..9266ff0f615 100644
--- a/config/initializers/ar_monkey_patch.rb
+++ b/config/initializers/ar_monkey_patch.rb
@@ -33,7 +33,7 @@ module ActiveRecord
affected_rows = relation.where(
self.class.primary_key => id,
- lock_col => previous_lock_value,
+ lock_col => previous_lock_value
).update_all(
attributes_for_update(attribute_names).map do |name|
[name, _read_attribute(name)]
diff --git a/config/initializers/hamlit.rb b/config/initializers/hamlit.rb
index 7b545d8c06c..51dbffeda05 100644
--- a/config/initializers/hamlit.rb
+++ b/config/initializers/hamlit.rb
@@ -3,7 +3,7 @@ module Hamlit
def call(template)
Engine.new(
generator: Temple::Generators::RailsOutputBuffer,
- attr_quote: '"',
+ attr_quote: '"'
).call(template.source)
end
end
@@ -11,7 +11,7 @@ end
ActionView::Template.register_template_handler(
:haml,
- Hamlit::TemplateHandler.new,
+ Hamlit::TemplateHandler.new
)
Hamlit::Filters.remove_filter('coffee')
diff --git a/config/initializers/rspec_profiling.rb b/config/initializers/rspec_profiling.rb
index a7efd74f09e..16b9d5b15e5 100644
--- a/config/initializers/rspec_profiling.rb
+++ b/config/initializers/rspec_profiling.rb
@@ -32,7 +32,7 @@ end
if Rails.env.test?
RspecProfiling.configure do |config|
- if ENV['RSPEC_PROFILING_POSTGRES_URL']
+ if ENV['RSPEC_PROFILING_POSTGRES_URL'].present?
RspecProfiling::Collectors::PSQL.prepend(RspecProfilingExt::PSQL)
config.collector = RspecProfiling::Collectors::PSQL
end
diff --git a/config/initializers/static_files.rb b/config/initializers/static_files.rb
index 74aba6c5d06..9ed96ddb0b4 100644
--- a/config/initializers/static_files.rb
+++ b/config/initializers/static_files.rb
@@ -23,21 +23,21 @@ if app.config.serve_static_files
host: dev_server.host,
port: dev_server.port,
manifest_host: dev_server.host,
- manifest_port: dev_server.port,
+ manifest_port: dev_server.port
}
if Rails.env.development?
settings.merge!(
host: Gitlab.config.gitlab.host,
port: Gitlab.config.gitlab.port,
- https: Gitlab.config.gitlab.https,
+ https: Gitlab.config.gitlab.https
)
app.config.middleware.insert_before(
Gitlab::Middleware::Static,
Gitlab::Middleware::WebpackProxy,
proxy_path: app.config.webpack.public_path,
proxy_host: dev_server.host,
- proxy_port: dev_server.port,
+ proxy_port: dev_server.port
)
end
diff --git a/config/routes/project.rb b/config/routes/project.rb
index a6c104c2d3f..c786cbdee1e 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -244,7 +244,7 @@ constraints(ProjectUrlConstrainer.new) do
get :referenced_merge_requests
get :related_branches
get :can_create_branch
- get :rendered_title
+ get :realtime_changes
post :create_merge_request
end
collection do
diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
index 433381e79d3..0ca1f565185 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -40,6 +40,7 @@
- [expire_build_instance_artifacts, 1]
- [group_destroy, 1]
- [irker, 1]
+ - [namespaceless_project_destroy, 1]
- [project_cache, 1]
- [project_destroy, 1]
- [project_export, 1]
diff --git a/config/webpack.config.js b/config/webpack.config.js
index 7e413c8493e..7bc225968de 100644
--- a/config/webpack.config.js
+++ b/config/webpack.config.js
@@ -63,6 +63,7 @@ var config = {
users: './users/users_bundle.js',
raven: './raven/index.js',
vue_merge_request_widget: './vue_merge_request_widget/index.js',
+ test: './test.js',
},
output: {
@@ -89,12 +90,12 @@ var config = {
loader: 'raw-loader',
},
{
- test: /\.gif$/,
+ test: /\.(gif|png)$/,
loader: 'url-loader',
- query: { mimetype: 'image/gif' },
+ options: { limit: 2048 },
},
{
- test: /\.(worker\.js|pdf)$/,
+ test: /\.(worker\.js|pdf|bmpr)$/,
exclude: /node_modules/,
loader: 'file-loader',
},
@@ -141,14 +142,17 @@ var config = {
'diff_notes',
'environments',
'environments_folder',
- 'sidebar',
+ 'filtered_search',
'issue_show',
'merge_conflicts',
'notebook_viewer',
'pdf_viewer',
'pipelines',
- 'balsamiq_viewer',
'pipelines_graph',
+ 'schedule_form',
+ 'schedules_index',
+ 'sidebar',
+ 'vue_merge_request_widget',
],
minChunks: function(module, count) {
return module.resource && (/vue_shared/).test(module.resource);
@@ -186,6 +190,7 @@ var config = {
'emojis': path.join(ROOT_PATH, 'fixtures/emojis'),
'empty_states': path.join(ROOT_PATH, 'app/views/shared/empty_states'),
'icons': path.join(ROOT_PATH, 'app/views/shared/icons'),
+ 'images': path.join(ROOT_PATH, 'app/assets/images'),
'vendor': path.join(ROOT_PATH, 'vendor/assets/javascripts'),
'vue$': 'vue/dist/vue.esm.js',
}
diff --git a/db/migrate/20160810142633_remove_redundant_indexes.rb b/db/migrate/20160810142633_remove_redundant_indexes.rb
index d7ab022d7bc..ea7d1f9a436 100644
--- a/db/migrate/20160810142633_remove_redundant_indexes.rb
+++ b/db/migrate/20160810142633_remove_redundant_indexes.rb
@@ -69,7 +69,7 @@ class RemoveRedundantIndexes < ActiveRecord::Migration
[:namespaces, 'index_namespaces_on_created_at_and_id'],
[:notes, 'index_notes_on_created_at_and_id'],
[:projects, 'index_projects_on_created_at_and_id'],
- [:users, 'index_users_on_created_at_and_id'],
+ [:users, 'index_users_on_created_at_and_id']
]
transaction do
diff --git a/db/migrate/20160829114652_add_markdown_cache_columns.rb b/db/migrate/20160829114652_add_markdown_cache_columns.rb
index 9cb44dfa9f9..6ad7237f4cd 100644
--- a/db/migrate/20160829114652_add_markdown_cache_columns.rb
+++ b/db/migrate/20160829114652_add_markdown_cache_columns.rb
@@ -25,7 +25,7 @@ class AddMarkdownCacheColumns < ActiveRecord::Migration
notes: [:note],
projects: [:description],
releases: [:description],
- snippets: [:title, :content],
+ snippets: [:title, :content]
}.freeze
def change
diff --git a/db/migrate/20170320171632_create_issue_assignees_table.rb b/db/migrate/20170320171632_create_issue_assignees_table.rb
deleted file mode 100644
index 23b8da37b6d..00000000000
--- a/db/migrate/20170320171632_create_issue_assignees_table.rb
+++ /dev/null
@@ -1,40 +0,0 @@
-# See http://doc.gitlab.com/ce/development/migration_style_guide.html
-# for more information on how to write migrations for GitLab.
-
-class CreateIssueAssigneesTable < ActiveRecord::Migration
- include Gitlab::Database::MigrationHelpers
-
- INDEX_NAME = 'index_issue_assignees_on_issue_id_and_user_id'
-
- # Set this constant to true if this migration requires downtime.
- DOWNTIME = false
-
- # When a migration requires downtime you **must** uncomment the following
- # constant and define a short and easy to understand explanation as to why the
- # migration requires downtime.
- # DOWNTIME_REASON = ''
-
- # When using the methods "add_concurrent_index" or "add_column_with_default"
- # you must disable the use of transactions as these methods can not run in an
- # existing transaction. When using "add_concurrent_index" make sure that this
- # method is the _only_ method called in the migration, any other changes
- # should go in a separate migration. This ensures that upon failure _only_ the
- # index creation fails and can be retried or reverted easily.
- #
- # To disable transactions uncomment the following line and remove these
- # comments:
- # disable_ddl_transaction!
-
- def up
- create_table :issue_assignees do |t|
- t.references :user, foreign_key: { on_delete: :cascade }, index: true, null: false
- t.references :issue, foreign_key: { on_delete: :cascade }, null: false
- end
-
- add_index :issue_assignees, [:issue_id, :user_id], unique: true, name: INDEX_NAME
- end
-
- def down
- drop_table :issue_assignees
- end
-end
diff --git a/db/migrate/20170320173259_migrate_assignees.rb b/db/migrate/20170320173259_migrate_assignees.rb
index ba8edbd7d32..23e7500a32d 100644
--- a/db/migrate/20170320173259_migrate_assignees.rb
+++ b/db/migrate/20170320173259_migrate_assignees.rb
@@ -37,16 +37,8 @@ class MigrateAssignees < ActiveRecord::Migration
users.project("true").where(users[:id].eq(table[:assignee_id])).exists.not
))
end
-
- execute <<-EOF
- INSERT INTO issue_assignees(issue_id, user_id)
- SELECT id, assignee_id FROM issues WHERE assignee_id IS NOT NULL
- EOF
end
def down
- execute <<-EOF
- DELETE FROM issue_assignees
- EOF
end
end
diff --git a/db/migrate/20170502065653_make_auto_cancel_pending_pipelines_on_by_default.rb b/db/migrate/20170502065653_make_auto_cancel_pending_pipelines_on_by_default.rb
new file mode 100644
index 00000000000..03bf626a08a
--- /dev/null
+++ b/db/migrate/20170502065653_make_auto_cancel_pending_pipelines_on_by_default.rb
@@ -0,0 +1,13 @@
+class MakeAutoCancelPendingPipelinesOnByDefault < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ change_column_default(:projects, :auto_cancel_pending_pipelines, 1)
+ end
+
+ def down
+ change_column_default(:projects, :auto_cancel_pending_pipelines, 0)
+ end
+end
diff --git a/db/migrate/20170502135553_create_index_ci_pipelines_auto_canceled_by_id.rb b/db/migrate/20170502135553_create_index_ci_pipelines_auto_canceled_by_id.rb
new file mode 100644
index 00000000000..b64d7e0e3f6
--- /dev/null
+++ b/db/migrate/20170502135553_create_index_ci_pipelines_auto_canceled_by_id.rb
@@ -0,0 +1,21 @@
+class CreateIndexCiPipelinesAutoCanceledById < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ # MySQL would already have the index
+ unless index_exists?(:ci_pipelines, :auto_canceled_by_id)
+ add_concurrent_index(:ci_pipelines, :auto_canceled_by_id)
+ end
+ end
+
+ def down
+ # We cannot remove index for MySQL because it's needed for foreign key
+ if Gitlab::Database.postgresql?
+ remove_concurrent_index(:ci_pipelines, :auto_canceled_by_id)
+ end
+ end
+end
diff --git a/db/migrate/20170502140503_create_index_ci_builds_auto_canceled_by_id.rb b/db/migrate/20170502140503_create_index_ci_builds_auto_canceled_by_id.rb
new file mode 100644
index 00000000000..0a8d2c8ff61
--- /dev/null
+++ b/db/migrate/20170502140503_create_index_ci_builds_auto_canceled_by_id.rb
@@ -0,0 +1,21 @@
+class CreateIndexCiBuildsAutoCanceledById < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ # MySQL would already have the index
+ unless index_exists?(:ci_builds, :auto_canceled_by_id)
+ add_concurrent_index(:ci_builds, :auto_canceled_by_id)
+ end
+ end
+
+ def down
+ # We cannot remove index for MySQL because it's needed for foreign key
+ if Gitlab::Database.postgresql?
+ remove_concurrent_index(:ci_builds, :auto_canceled_by_id)
+ end
+ end
+end
diff --git a/db/migrate/20170503004426_add_retried_to_ci_build.rb b/db/migrate/20170503004426_add_retried_to_ci_build.rb
new file mode 100644
index 00000000000..2851e3de473
--- /dev/null
+++ b/db/migrate/20170503004426_add_retried_to_ci_build.rb
@@ -0,0 +1,9 @@
+class AddRetriedToCiBuild < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ add_column(:ci_builds, :retried, :boolean)
+ end
+end
diff --git a/db/migrate/20170503023315_add_repository_update_events_to_web_hooks.rb b/db/migrate/20170503023315_add_repository_update_events_to_web_hooks.rb
new file mode 100644
index 00000000000..0faea87a962
--- /dev/null
+++ b/db/migrate/20170503023315_add_repository_update_events_to_web_hooks.rb
@@ -0,0 +1,15 @@
+class AddRepositoryUpdateEventsToWebHooks < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_column_with_default :web_hooks, :repository_update_events, :boolean, default: false, allow_null: false
+ end
+
+ def down
+ remove_column :web_hooks, :repository_update_events
+ end
+end
diff --git a/db/migrate/20170508153950_add_not_null_contraints_to_ci_variables.rb b/db/migrate/20170508153950_add_not_null_contraints_to_ci_variables.rb
new file mode 100644
index 00000000000..41c687a4f6e
--- /dev/null
+++ b/db/migrate/20170508153950_add_not_null_contraints_to_ci_variables.rb
@@ -0,0 +1,12 @@
+class AddNotNullContraintsToCiVariables < ActiveRecord::Migration
+ DOWNTIME = false
+
+ def up
+ change_column(:ci_variables, :key, :string, null: false)
+ change_column(:ci_variables, :project_id, :integer, null: false)
+ end
+
+ def down
+ # no op
+ end
+end
diff --git a/db/migrate/20170508190732_add_foreign_key_to_ci_variables.rb b/db/migrate/20170508190732_add_foreign_key_to_ci_variables.rb
new file mode 100644
index 00000000000..20ecaa2c36c
--- /dev/null
+++ b/db/migrate/20170508190732_add_foreign_key_to_ci_variables.rb
@@ -0,0 +1,24 @@
+class AddForeignKeyToCiVariables < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ execute <<~SQL
+ DELETE FROM ci_variables
+ WHERE NOT EXISTS (
+ SELECT true
+ FROM projects
+ WHERE projects.id = ci_variables.project_id
+ )
+ SQL
+
+ add_concurrent_foreign_key(:ci_variables, :projects, column: :project_id)
+ end
+
+ def down
+ remove_foreign_key(:ci_variables, column: :project_id)
+ end
+end
diff --git a/db/migrate/20170511082759_rename_web_hooks_build_events_to_job_events.rb b/db/migrate/20170511082759_rename_web_hooks_build_events_to_job_events.rb
new file mode 100644
index 00000000000..a2320a911b7
--- /dev/null
+++ b/db/migrate/20170511082759_rename_web_hooks_build_events_to_job_events.rb
@@ -0,0 +1,18 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class RenameWebHooksBuildEventsToJobEvents < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ rename_column_concurrently :web_hooks, :build_events, :job_events
+ end
+
+ def down
+ cleanup_concurrent_column_rename :web_hooks, :job_events, :build_events
+ end
+end
diff --git a/db/migrate/20170511083824_rename_services_build_events_to_job_events.rb b/db/migrate/20170511083824_rename_services_build_events_to_job_events.rb
new file mode 100644
index 00000000000..303d47078e7
--- /dev/null
+++ b/db/migrate/20170511083824_rename_services_build_events_to_job_events.rb
@@ -0,0 +1,18 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class RenameServicesBuildEventsToJobEvents < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ rename_column_concurrently :services, :build_events, :job_events
+ end
+
+ def down
+ cleanup_concurrent_column_rename :services, :job_events, :build_events
+ end
+end
diff --git a/db/migrate/20170516153305_migrate_assignee_to_separate_table.rb b/db/migrate/20170516153305_migrate_assignee_to_separate_table.rb
new file mode 100644
index 00000000000..eed9f00d8b2
--- /dev/null
+++ b/db/migrate/20170516153305_migrate_assignee_to_separate_table.rb
@@ -0,0 +1,83 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class MigrateAssigneeToSeparateTable < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ # When a migration requires downtime you **must** uncomment the following
+ # constant and define a short and easy to understand explanation as to why the
+ # migration requires downtime.
+ # DOWNTIME_REASON = ''
+
+ # When using the methods "add_concurrent_index", "remove_concurrent_index" or
+ # "add_column_with_default" you must disable the use of transactions
+ # as these methods can not run in an existing transaction.
+ # When using "add_concurrent_index" or "remove_concurrent_index" methods make sure
+ # that either of them is the _only_ method called in the migration,
+ # any other changes should go in a separate migration.
+ # This ensures that upon failure _only_ the index creation or removing fails
+ # and can be retried or reverted easily.
+ #
+ # To disable transactions uncomment the following line and remove these
+ # comments:
+ # disable_ddl_transaction!
+
+ def up
+ drop_table(:issue_assignees) if table_exists?(:issue_assignees)
+
+ if Gitlab::Database.mysql?
+ execute <<-EOF
+ CREATE TABLE issue_assignees AS
+ SELECT assignee_id AS user_id, id AS issue_id FROM issues WHERE assignee_id IS NOT NULL
+ EOF
+ else
+ ActiveRecord::Base.transaction do
+ execute('LOCK TABLE issues IN EXCLUSIVE MODE')
+
+ execute <<-EOF
+ CREATE TABLE issue_assignees AS
+ SELECT assignee_id AS user_id, id AS issue_id FROM issues WHERE assignee_id IS NOT NULL
+ EOF
+
+ execute <<-EOF
+ CREATE OR REPLACE FUNCTION replicate_assignee_id()
+ RETURNS trigger AS
+ $BODY$
+ BEGIN
+ if OLD IS NOT NULL AND OLD.assignee_id IS NOT NULL THEN
+ DELETE FROM issue_assignees WHERE issue_id = OLD.id;
+ END IF;
+
+ if NEW.assignee_id IS NOT NULL THEN
+ INSERT INTO issue_assignees (user_id, issue_id) VALUES (NEW.assignee_id, NEW.id);
+ END IF;
+
+ RETURN NEW;
+ END;
+ $BODY$
+ LANGUAGE 'plpgsql'
+ VOLATILE;
+
+ CREATE TRIGGER replicate_assignee_id
+ BEFORE INSERT OR UPDATE OF assignee_id
+ ON issues
+ FOR EACH ROW EXECUTE PROCEDURE replicate_assignee_id();
+ EOF
+ end
+ end
+ end
+
+ def down
+ drop_table(:issue_assignees) if table_exists?(:issue_assignees)
+
+ if Gitlab::Database.postgresql?
+ execute <<-EOF
+ DROP TRIGGER IF EXISTS replicate_assignee_id ON issues;
+ DROP FUNCTION IF EXISTS replicate_assignee_id();
+ EOF
+ end
+ end
+end
diff --git a/db/migrate/20170516183131_add_indices_to_issue_assignees.rb b/db/migrate/20170516183131_add_indices_to_issue_assignees.rb
new file mode 100644
index 00000000000..a1f064c6848
--- /dev/null
+++ b/db/migrate/20170516183131_add_indices_to_issue_assignees.rb
@@ -0,0 +1,41 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddIndicesToIssueAssignees < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ # When a migration requires downtime you **must** uncomment the following
+ # constant and define a short and easy to understand explanation as to why the
+ # migration requires downtime.
+ # DOWNTIME_REASON = ''
+
+ # When using the methods "add_concurrent_index", "remove_concurrent_index" or
+ # "add_column_with_default" you must disable the use of transactions
+ # as these methods can not run in an existing transaction.
+ # When using "add_concurrent_index" or "remove_concurrent_index" methods make sure
+ # that either of them is the _only_ method called in the migration,
+ # any other changes should go in a separate migration.
+ # This ensures that upon failure _only_ the index creation or removing fails
+ # and can be retried or reverted easily.
+ #
+ # To disable transactions uncomment the following line and remove these
+ # comments:
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :issue_assignees, [:issue_id, :user_id], unique: true, name: 'index_issue_assignees_on_issue_id_and_user_id'
+ add_concurrent_index :issue_assignees, :user_id, name: 'index_issue_assignees_on_user_id'
+ add_concurrent_foreign_key :issue_assignees, :users, column: :user_id, on_delete: :cascade
+ add_concurrent_foreign_key :issue_assignees, :issues, column: :issue_id, on_delete: :cascade
+ end
+
+ def down
+ remove_foreign_key :issue_assignees, column: :user_id
+ remove_foreign_key :issue_assignees, column: :issue_id
+ remove_concurrent_index :issue_assignees, [:issue_id, :user_id] if index_exists?(:issue_assignees, [:issue_id, :user_id])
+ remove_concurrent_index :issue_assignees, :user_id if index_exists?(:issue_assignees, :user_id)
+ end
+end
diff --git a/db/post_migrate/20170425121605_migrate_trigger_schedules_to_pipeline_schedules.rb b/db/post_migrate/20170425121605_migrate_trigger_schedules_to_pipeline_schedules.rb
index a44b399c4de..dae9750558f 100644
--- a/db/post_migrate/20170425121605_migrate_trigger_schedules_to_pipeline_schedules.rb
+++ b/db/post_migrate/20170425121605_migrate_trigger_schedules_to_pipeline_schedules.rb
@@ -4,6 +4,13 @@ class MigrateTriggerSchedulesToPipelineSchedules < ActiveRecord::Migration
DOWNTIME = false
def up
+ connection.execute <<~SQL
+ DELETE FROM ci_trigger_schedules WHERE NOT EXISTS
+ (SELECT true FROM projects
+ WHERE ci_trigger_schedules.project_id = projects.id
+ )
+ SQL
+
connection.execute <<-SQL
INSERT INTO ci_pipeline_schedules (
project_id,
diff --git a/db/post_migrate/20170502070007_enable_auto_cancel_pending_pipelines_for_all.rb b/db/post_migrate/20170502070007_enable_auto_cancel_pending_pipelines_for_all.rb
new file mode 100644
index 00000000000..a19b73fc114
--- /dev/null
+++ b/db/post_migrate/20170502070007_enable_auto_cancel_pending_pipelines_for_all.rb
@@ -0,0 +1,15 @@
+class EnableAutoCancelPendingPipelinesForAll < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ disable_ddl_transaction!
+
+ DOWNTIME = false
+
+ def up
+ update_column_in_batches(:projects, :auto_cancel_pending_pipelines, 1)
+ end
+
+ def down
+ # Nothing we can do!
+ end
+end
diff --git a/db/post_migrate/20170502101023_cleanup_namespaceless_pending_delete_projects.rb b/db/post_migrate/20170502101023_cleanup_namespaceless_pending_delete_projects.rb
new file mode 100644
index 00000000000..ce52de91cdd
--- /dev/null
+++ b/db/post_migrate/20170502101023_cleanup_namespaceless_pending_delete_projects.rb
@@ -0,0 +1,47 @@
+# This is the counterpart of RequeuePendingDeleteProjects and cleans all
+# projects with `pending_delete = true` and that do not have a namespace.
+class CleanupNamespacelessPendingDeleteProjects < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ @offset = 0
+
+ loop do
+ ids = pending_delete_batch
+
+ break if ids.empty?
+
+ args = ids.map { |id| Array(id) }
+
+ NamespacelessProjectDestroyWorker.bulk_perform_async(args)
+
+ @offset += 1
+ end
+ end
+
+ def down
+ # noop
+ end
+
+ private
+
+ def pending_delete_batch
+ connection.exec_query(find_batch).map{ |row| row['id'].to_i }
+ end
+
+ BATCH_SIZE = 5000
+
+ def find_batch
+ projects = Arel::Table.new(:projects)
+ projects.project(projects[:id]).
+ where(projects[:pending_delete].eq(true)).
+ where(projects[:namespace_id].eq(nil)).
+ skip(@offset * BATCH_SIZE).
+ take(BATCH_SIZE).
+ to_sql
+ end
+end
diff --git a/db/post_migrate/20170503004427_upate_retried_for_ci_build.rb b/db/post_migrate/20170503004427_upate_retried_for_ci_build.rb
new file mode 100644
index 00000000000..9b20edeb4c3
--- /dev/null
+++ b/db/post_migrate/20170503004427_upate_retried_for_ci_build.rb
@@ -0,0 +1,43 @@
+class UpateRetriedForCiBuild < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ disable_statement_timeout
+
+ with_temporary_partial_index do
+ latest_id = <<-SQL.strip_heredoc
+ SELECT MAX(ci_builds2.id)
+ FROM ci_builds ci_builds2
+ WHERE ci_builds.commit_id=ci_builds2.commit_id
+ AND ci_builds.name=ci_builds2.name
+ SQL
+
+ # This is slow update as it does single-row query
+ # This is designed to be run as idle, or a post deployment migration
+ is_retried = Arel.sql("((#{latest_id}) != ci_builds.id)")
+
+ update_column_in_batches(:ci_builds, :retried, is_retried) do |table, query|
+ query.where(table[:retried].eq(nil))
+ end
+ end
+ end
+
+ def down
+ end
+
+ def with_temporary_partial_index
+ if Gitlab::Database.postgresql?
+ execute 'CREATE INDEX CONCURRENTLY IF NOT EXISTS index_for_ci_builds_retried_migration ON ci_builds (id) WHERE retried IS NULL;'
+ end
+
+ yield
+
+ if Gitlab::Database.postgresql?
+ execute 'DROP INDEX CONCURRENTLY IF EXISTS index_for_ci_builds_retried_migration'
+ end
+ end
+end
diff --git a/db/post_migrate/20170510101043_add_foreign_key_on_pipeline_schedule_owner.rb b/db/post_migrate/20170510101043_add_foreign_key_on_pipeline_schedule_owner.rb
new file mode 100644
index 00000000000..6a870f08e89
--- /dev/null
+++ b/db/post_migrate/20170510101043_add_foreign_key_on_pipeline_schedule_owner.rb
@@ -0,0 +1,35 @@
+class AddForeignKeyOnPipelineScheduleOwner < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ execute <<-SQL
+ UPDATE ci_pipeline_schedules
+ SET owner_id = NULL
+ WHERE NOT EXISTS (
+ SELECT true
+ FROM users
+ WHERE ci_pipeline_schedules.owner_id = users.id
+ )
+ SQL
+
+ add_concurrent_foreign_key(:ci_pipeline_schedules, :users, column: :owner_id, on_delete: on_delete)
+ end
+
+ def down
+ remove_foreign_key(:ci_pipeline_schedules, column: :owner_id)
+ end
+
+ private
+
+ def on_delete
+ if Gitlab::Database.mysql?
+ :nullify
+ else
+ 'SET NULL'
+ end
+ end
+end
diff --git a/db/post_migrate/20170511100900_cleanup_rename_web_hooks_build_events_to_job_events.rb b/db/post_migrate/20170511100900_cleanup_rename_web_hooks_build_events_to_job_events.rb
new file mode 100644
index 00000000000..281be90163a
--- /dev/null
+++ b/db/post_migrate/20170511100900_cleanup_rename_web_hooks_build_events_to_job_events.rb
@@ -0,0 +1,18 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class CleanupRenameWebHooksBuildEventsToJobEvents < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ cleanup_concurrent_column_rename :web_hooks, :build_events, :job_events
+ end
+
+ def down
+ rename_column_concurrently :web_hooks, :job_events, :build_events
+ end
+end
diff --git a/db/post_migrate/20170511101000_cleanup_rename_services_build_events_to_job_events.rb b/db/post_migrate/20170511101000_cleanup_rename_services_build_events_to_job_events.rb
new file mode 100644
index 00000000000..5d26df5688f
--- /dev/null
+++ b/db/post_migrate/20170511101000_cleanup_rename_services_build_events_to_job_events.rb
@@ -0,0 +1,18 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class CleanupRenameServicesBuildEventsToJobEvents < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ cleanup_concurrent_column_rename :services, :build_events, :job_events
+ end
+
+ def down
+ rename_column_concurrently :services, :job_events, :build_events
+ end
+end
diff --git a/db/post_migrate/20170516165238_cleanup_trigger_for_issues.rb b/db/post_migrate/20170516165238_cleanup_trigger_for_issues.rb
new file mode 100644
index 00000000000..378fe5603c3
--- /dev/null
+++ b/db/post_migrate/20170516165238_cleanup_trigger_for_issues.rb
@@ -0,0 +1,39 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class CleanupTriggerForIssues < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ # When a migration requires downtime you **must** uncomment the following
+ # constant and define a short and easy to understand explanation as to why the
+ # migration requires downtime.
+ # DOWNTIME_REASON = ''
+
+ # When using the methods "add_concurrent_index", "remove_concurrent_index" or
+ # "add_column_with_default" you must disable the use of transactions
+ # as these methods can not run in an existing transaction.
+ # When using "add_concurrent_index" or "remove_concurrent_index" methods make sure
+ # that either of them is the _only_ method called in the migration,
+ # any other changes should go in a separate migration.
+ # This ensures that upon failure _only_ the index creation or removing fails
+ # and can be retried or reverted easily.
+ #
+ # To disable transactions uncomment the following line and remove these
+ # comments:
+ disable_ddl_transaction!
+
+ def up
+ if Gitlab::Database.postgresql?
+ execute <<-EOF
+ DROP TRIGGER IF EXISTS replicate_assignee_id ON issues;
+ DROP FUNCTION IF EXISTS replicate_assignee_id();
+ EOF
+ end
+ end
+
+ def down
+ end
+end
diff --git a/db/post_migrate/20170516181025_add_constraints_to_issue_assignees_table.rb b/db/post_migrate/20170516181025_add_constraints_to_issue_assignees_table.rb
new file mode 100644
index 00000000000..6fa573c5b49
--- /dev/null
+++ b/db/post_migrate/20170516181025_add_constraints_to_issue_assignees_table.rb
@@ -0,0 +1,37 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddConstraintsToIssueAssigneesTable < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ # When a migration requires downtime you **must** uncomment the following
+ # constant and define a short and easy to understand explanation as to why the
+ # migration requires downtime.
+ # DOWNTIME_REASON = ''
+
+ # When using the methods "add_concurrent_index", "remove_concurrent_index" or
+ # "add_column_with_default" you must disable the use of transactions
+ # as these methods can not run in an existing transaction.
+ # When using "add_concurrent_index" or "remove_concurrent_index" methods make sure
+ # that either of them is the _only_ method called in the migration,
+ # any other changes should go in a separate migration.
+ # This ensures that upon failure _only_ the index creation or removing fails
+ # and can be retried or reverted easily.
+ #
+ # To disable transactions uncomment the following line and remove these
+ # comments:
+ # disable_ddl_transaction!
+
+ def up
+ change_column_null :issue_assignees, :issue_id, false
+ change_column_null :issue_assignees, :user_id, false
+ end
+
+ def down
+ change_column_null :issue_assignees, :issue_id, true
+ change_column_null :issue_assignees, :user_id, true
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index b91b3e6e977..294e0b531eb 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: 20170508170547) do
+ActiveRecord::Schema.define(version: 20170516183131) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -232,8 +232,10 @@ ActiveRecord::Schema.define(version: 20170508170547) do
t.integer "lock_version"
t.string "coverage_regex"
t.integer "auto_canceled_by_id"
+ t.boolean "retried"
end
+ add_index "ci_builds", ["auto_canceled_by_id"], name: "index_ci_builds_on_auto_canceled_by_id", using: :btree
add_index "ci_builds", ["commit_id", "stage_idx", "created_at"], name: "index_ci_builds_on_commit_id_and_stage_idx_and_created_at", using: :btree
add_index "ci_builds", ["commit_id", "status", "type"], name: "index_ci_builds_on_commit_id_and_status_and_type", using: :btree
add_index "ci_builds", ["commit_id", "type", "name", "ref"], name: "index_ci_builds_on_commit_id_and_type_and_name_and_ref", using: :btree
@@ -283,6 +285,7 @@ ActiveRecord::Schema.define(version: 20170508170547) do
t.integer "pipeline_schedule_id"
end
+ add_index "ci_pipelines", ["auto_canceled_by_id"], name: "index_ci_pipelines_on_auto_canceled_by_id", using: :btree
add_index "ci_pipelines", ["pipeline_schedule_id"], name: "index_ci_pipelines_on_pipeline_schedule_id", using: :btree
add_index "ci_pipelines", ["project_id", "ref", "status"], name: "index_ci_pipelines_on_project_id_and_ref_and_status", using: :btree
add_index "ci_pipelines", ["project_id", "sha"], name: "index_ci_pipelines_on_project_id_and_sha", using: :btree
@@ -346,12 +349,12 @@ ActiveRecord::Schema.define(version: 20170508170547) do
add_index "ci_triggers", ["project_id"], name: "index_ci_triggers_on_project_id", using: :btree
create_table "ci_variables", force: :cascade do |t|
- t.string "key"
+ t.string "key", null: false
t.text "value"
t.text "encrypted_value"
t.string "encrypted_value_salt"
t.string "encrypted_value_iv"
- t.integer "project_id"
+ t.integer "project_id", null: false
end
add_index "ci_variables", ["project_id"], name: "index_ci_variables_on_project_id", using: :btree
@@ -456,7 +459,7 @@ ActiveRecord::Schema.define(version: 20170508170547) do
add_index "identities", ["user_id"], name: "index_identities_on_user_id", using: :btree
- create_table "issue_assignees", force: :cascade do |t|
+ create_table "issue_assignees", id: false, force: :cascade do |t|
t.integer "user_id", null: false
t.integer "issue_id", null: false
end
@@ -985,7 +988,7 @@ ActiveRecord::Schema.define(version: 20170508170547) do
t.text "description_html"
t.boolean "only_allow_merge_if_all_discussions_are_resolved"
t.boolean "printing_merge_request_link_enabled", default: true, null: false
- t.integer "auto_cancel_pending_pipelines", default: 0, null: false
+ t.integer "auto_cancel_pending_pipelines", default: 1, null: false
t.string "import_jid"
t.integer "cached_markdown_version"
t.datetime "last_repository_updated_at"
@@ -1122,13 +1125,13 @@ ActiveRecord::Schema.define(version: 20170508170547) do
t.boolean "merge_requests_events", default: true
t.boolean "tag_push_events", default: true
t.boolean "note_events", default: true, null: false
- t.boolean "build_events", default: false, null: false
t.string "category", default: "common", null: false
t.boolean "default", default: false
t.boolean "wiki_page_events", default: true
t.boolean "pipeline_events", default: false, null: false
t.boolean "confidential_issues_events", default: true, null: false
t.boolean "commit_events", default: true, null: false
+ t.boolean "job_events", default: false, null: false
end
add_index "services", ["project_id"], name: "index_services_on_project_id", using: :btree
@@ -1398,11 +1401,12 @@ ActiveRecord::Schema.define(version: 20170508170547) do
t.boolean "tag_push_events", default: false
t.boolean "note_events", default: false, null: false
t.boolean "enable_ssl_verification", default: true
- t.boolean "build_events", default: false, null: false
t.boolean "wiki_page_events", default: false, null: false
t.string "token"
t.boolean "pipeline_events", default: false, null: false
t.boolean "confidential_issues_events", default: false, null: false
+ t.boolean "job_events", default: false, null: false
+ t.boolean "repository_update_events", default: false, null: false
end
add_index "web_hooks", ["project_id"], name: "index_web_hooks_on_project_id", using: :btree
@@ -1412,13 +1416,15 @@ ActiveRecord::Schema.define(version: 20170508170547) do
add_foreign_key "chat_teams", "namespaces", on_delete: :cascade
add_foreign_key "ci_builds", "ci_pipelines", column: "auto_canceled_by_id", name: "fk_a2141b1522", on_delete: :nullify
add_foreign_key "ci_pipeline_schedules", "projects", name: "fk_8ead60fcc4", on_delete: :cascade
+ add_foreign_key "ci_pipeline_schedules", "users", column: "owner_id", name: "fk_9ea99f58d2", on_delete: :nullify
add_foreign_key "ci_pipelines", "ci_pipeline_schedules", column: "pipeline_schedule_id", name: "fk_3d34ab2e06", on_delete: :nullify
add_foreign_key "ci_pipelines", "ci_pipelines", column: "auto_canceled_by_id", name: "fk_262d4c2d19", on_delete: :nullify
add_foreign_key "ci_trigger_requests", "ci_triggers", column: "trigger_id", name: "fk_b8ec8b7245", on_delete: :cascade
add_foreign_key "ci_triggers", "users", column: "owner_id", name: "fk_e8e10d1964", on_delete: :cascade
+ add_foreign_key "ci_variables", "projects", name: "fk_ada5eb64b3", on_delete: :cascade
add_foreign_key "container_repositories", "projects"
- add_foreign_key "issue_assignees", "issues", on_delete: :cascade
- add_foreign_key "issue_assignees", "users", on_delete: :cascade
+ add_foreign_key "issue_assignees", "issues", name: "fk_b7d881734a", on_delete: :cascade
+ add_foreign_key "issue_assignees", "users", name: "fk_5e0c8d9154", on_delete: :cascade
add_foreign_key "issue_metrics", "issues", on_delete: :cascade
add_foreign_key "label_priorities", "labels", on_delete: :cascade
add_foreign_key "label_priorities", "projects", on_delete: :cascade
diff --git a/doc/administration/environment_variables.md b/doc/administration/environment_variables.md
index 76029b30dd8..b6676026d06 100644
--- a/doc/administration/environment_variables.md
+++ b/doc/administration/environment_variables.md
@@ -20,7 +20,6 @@ Variable | Type | Description
`GITLAB_EMAIL_FROM` | string | The e-mail address used in the "From" field in e-mails sent by GitLab
`GITLAB_EMAIL_DISPLAY_NAME` | string | The name used in the "From" field in e-mails sent by GitLab
`GITLAB_EMAIL_REPLY_TO` | string | The e-mail address used in the "Reply-To" field in e-mails sent by GitLab
-`GITLAB_EMAIL_REPLY_TO` | string | The e-mail address used in the "Reply-To" field in e-mails sent by GitLab
`GITLAB_EMAIL_SUBJECT_SUFFIX` | string | The e-mail subject suffix used in e-mails sent by GitLab
`GITLAB_UNICORN_MEMORY_MIN` | integer | The minimum memory threshold (in bytes) for the Unicorn worker killer
`GITLAB_UNICORN_MEMORY_MAX` | integer | The maximum memory threshold (in bytes) for the Unicorn worker killer
diff --git a/doc/administration/high_availability/README.md b/doc/administration/high_availability/README.md
index d5a5aef7ec0..4d3be0ab8f6 100644
--- a/doc/administration/high_availability/README.md
+++ b/doc/administration/high_availability/README.md
@@ -5,6 +5,20 @@ The solution you choose will be based on the level of scalability and
availability you require. The easiest solutions are scalable, but not necessarily
highly available.
+GitLab provides a service that is usually essential to most organizations: it
+enables people to collaborate on code in a timely fashion. Any downtime should
+therefore be short and planned. Luckily, GitLab provides a solid setup even on
+a single server without special measures. Due to the distributed nature
+of Git, developers can still commit code locally even when GitLab is not
+available. However, some GitLab features such as the issue tracker and
+Continuous Integration are not available when GitLab is down.
+
+**Keep in mind that all Highly Available solutions come with a trade-off between
+cost/complexity and uptime**. The more uptime you want, the more complex the
+solution. And the more complex the solution, the more work is involved in
+setting up and maintaining it. High availability is not free and every HA
+solution should balance the costs against the benefits.
+
## Architecture
There are two kinds of setups:
@@ -37,6 +51,10 @@ Block Device) to keep all data in sync. DRBD requires a low latency link to
remain in sync. It is not advisable to attempt to run DRBD between data centers
or in different cloud availability zones.
+> **Note:** GitLab recommends against choosing this HA method because of the
+ complexity of managing DRBD and crafting automatic failover. This is
+ *compatible* with GitLab, but not officially *supported*.
+
Components/Servers Required: 2 servers/virtual machines (one active/one passive)
![Active/Passive HA Diagram](../img/high_availability/active-passive-diagram.png)
diff --git a/doc/administration/high_availability/nfs.md b/doc/administration/high_availability/nfs.md
index c5125dc6d5a..d8e76d6ab94 100644
--- a/doc/administration/high_availability/nfs.md
+++ b/doc/administration/high_availability/nfs.md
@@ -7,6 +7,25 @@ supported natively in NFS version 4. NFSv3 also supports locking as long as
Linux Kernel 2.6.5+ is used. We recommend using version 4 and do not
specifically test NFSv3.
+## AWS Elastic File System
+
+GitLab does not recommend using AWS Elastic File System (EFS).
+
+Customers and users have reported that AWS EFS does not perform well for GitLab's
+use-case. There are several issues that can cause problems. For these reasons
+GitLab does not recommend using EFS with GitLab.
+
+- EFS bases allowed IOPS on volume size. The larger the volume, the more IOPS
+ are allocated. For smaller volumes, users may experience decent performance
+ for a period of time due to 'Burst Credits'. Over a period of weeks to months
+ credits may run out and performance will bottom out.
+- For larger volumes, allocated IOPS may not be the problem. Workloads where
+ many small files are written in a serialized manner are not well-suited for EFS.
+ EBS with an NFS server on top will perform much better.
+
+For more details on another person's experience with EFS, see
+[Amazon's Elastic File System: Burst Credits](https://www.rawkode.io/2017/04/amazons-elastic-file-system-burst-credits/)
+
### Recommended options
When you define your NFS exports, we recommend you also add the following
diff --git a/doc/api/README.md b/doc/api/README.md
index d444ce94573..1b0f6470b13 100644
--- a/doc/api/README.md
+++ b/doc/api/README.md
@@ -61,8 +61,9 @@ The following documentation is for the [internal CI API](ci/README.md):
## Authentication
-All API requests require authentication via a session cookie or token. There are
-three types of tokens available: private tokens, OAuth 2 tokens, and personal
+Most API requests require authentication via a session cookie or token. For those cases where it is not required, this will be mentioned in the documentation
+for each individual endpoint. For example, the [`/projects/:id` endpoint](projects.md).
+There are three types of tokens available: private tokens, OAuth 2 tokens, and personal
access tokens.
If authentication information is invalid or omitted, an error message will be
diff --git a/doc/api/access_requests.md b/doc/api/access_requests.md
index 21de7d18632..603fa4a8194 100644
--- a/doc/api/access_requests.md
+++ b/doc/api/access_requests.md
@@ -1,4 +1,4 @@
-# Group and project access requests
+# Group and project access requests API
>**Note:** This feature was introduced in GitLab 8.11
diff --git a/doc/api/award_emoji.md b/doc/api/award_emoji.md
index 5f3adcc397a..d6924741ee4 100644
--- a/doc/api/award_emoji.md
+++ b/doc/api/award_emoji.md
@@ -1,4 +1,4 @@
-# Award Emoji
+# Award Emoji API
> [Introduced][ce-4575] in GitLab 8.9, Snippet support in 8.12
diff --git a/doc/api/boards.md b/doc/api/boards.md
index 17d2be0ee16..69c47abc806 100644
--- a/doc/api/boards.md
+++ b/doc/api/boards.md
@@ -1,4 +1,4 @@
-# Boards
+# Issue Boards API
Every API call to boards must be authenticated.
diff --git a/doc/api/branches.md b/doc/api/branches.md
index 5717215deb6..325d0ea4ce3 100644
--- a/doc/api/branches.md
+++ b/doc/api/branches.md
@@ -1,4 +1,4 @@
-# Branches
+# Branches API
## List repository branches
diff --git a/doc/api/broadcast_messages.md b/doc/api/broadcast_messages.md
index ad254e3515e..a8a248a17f4 100644
--- a/doc/api/broadcast_messages.md
+++ b/doc/api/broadcast_messages.md
@@ -1,4 +1,4 @@
-# Broadcast Messages
+# Broadcast Messages API
> **Note:** This feature was introduced in GitLab 8.12.
diff --git a/doc/api/build_variables.md b/doc/api/build_variables.md
index 9218902e84a..2aaf1c93705 100644
--- a/doc/api/build_variables.md
+++ b/doc/api/build_variables.md
@@ -1,4 +1,4 @@
-# Build Variables
+# Build Variables API
## List project variables
diff --git a/doc/api/ci/lint.md b/doc/api/ci/lint.md
index 74def207816..6a4dca92cfe 100644
--- a/doc/api/ci/lint.md
+++ b/doc/api/ci/lint.md
@@ -1,4 +1,4 @@
-# Validate the .gitlab-ci.yml
+# Validate the .gitlab-ci.yml (API)
> [Introduced][ce-5953] in GitLab 8.12.
diff --git a/doc/api/ci/runners.md b/doc/api/ci/runners.md
index 16028d1f124..342c039dad8 100644
--- a/doc/api/ci/runners.md
+++ b/doc/api/ci/runners.md
@@ -1,4 +1,4 @@
-# Runners API
+# Register and Delete Runners API
API used by Runners to register and delete themselves.
diff --git a/doc/api/deploy_key_multiple_projects.md b/doc/api/deploy_key_multiple_projects.md
index f94dbfa4059..127f9a196de 100644
--- a/doc/api/deploy_key_multiple_projects.md
+++ b/doc/api/deploy_key_multiple_projects.md
@@ -1,4 +1,4 @@
-# Adding deploy keys to multiple projects
+# Adding deploy keys to multiple projects via API
If you want to easily add the same deploy key to multiple projects in the same
group, this can be achieved quite easily with the API.
diff --git a/doc/api/deploy_keys.md b/doc/api/deploy_keys.md
index c3fe7f84ef2..4fa800ecb9c 100644
--- a/doc/api/deploy_keys.md
+++ b/doc/api/deploy_keys.md
@@ -1,4 +1,4 @@
-# Deploy Keys
+# Deploy Keys API
## List all deploy keys
diff --git a/doc/api/enviroments.md b/doc/api/enviroments.md
index 49930f01945..5ca766bf87d 100644
--- a/doc/api/enviroments.md
+++ b/doc/api/enviroments.md
@@ -1,4 +1,4 @@
-# Environments
+# Environments API
## List environments
diff --git a/doc/api/groups.md b/doc/api/groups.md
index bc61bfec9b9..2b3d8e125c8 100644
--- a/doc/api/groups.md
+++ b/doc/api/groups.md
@@ -1,4 +1,4 @@
-# Groups
+# Groups API
## List groups
diff --git a/doc/api/issues.md b/doc/api/issues.md
index 1d43b1298b9..3f949ca5667 100644
--- a/doc/api/issues.md
+++ b/doc/api/issues.md
@@ -1,4 +1,4 @@
-# Issues
+# Issues API
Every API call to issues must be authenticated.
@@ -100,7 +100,7 @@ Example response:
]
```
-**Note**: `assignee` column is deprecated, it shows the first assignee only.
+**Note**: `assignee` column is deprecated, now we show it as a single-sized array `assignees` to conform to the GitLab EE API.
## List group issues
@@ -192,7 +192,7 @@ Example response:
]
```
-**Note**: `assignee` column is deprecated, it shows the first assignee only.
+**Note**: `assignee` column is deprecated, now we show it as a single-sized array `assignees` to conform to the GitLab EE API.
## List project issues
@@ -284,7 +284,7 @@ Example response:
]
```
-**Note**: `assignee` column is deprecated, it shows the first assignee only.
+**Note**: `assignee` column is deprecated, now we show it as a single-sized array `assignees` to conform to the GitLab EE API.
## Single issue
@@ -359,7 +359,7 @@ Example response:
}
```
-**Note**: `assignee` column is deprecated, it shows the first assignee only.
+**Note**: `assignee` column is deprecated, now we show it as a single-sized array `assignees` to conform to the GitLab EE API.
## New issue
@@ -375,7 +375,7 @@ POST /projects/:id/issues
| `title` | string | yes | The title of an issue |
| `description` | string | no | The description of an issue |
| `confidential` | boolean | no | Set an issue to be confidential. Default is `false`. |
-| `assignee_ids` | Array[integer] | no | The ID of a user to assign issue |
+| `assignee_ids` | Array[integer] | no | The ID of the users to assign issue |
| `milestone_id` | integer | no | The ID of a milestone to assign issue |
| `labels` | string | no | Comma-separated label names for an issue |
| `created_at` | string | no | Date time string, ISO 8601 formatted, e.g. `2016-03-11T03:45:40Z` (requires admin or project owner rights) |
@@ -421,7 +421,7 @@ Example response:
}
```
-**Note**: `assignee` column is deprecated, it shows the first assignee only.
+**Note**: `assignee` column is deprecated, now we show it as a single-sized array `assignees` to conform to the GitLab EE API.
## Edit issue
@@ -439,7 +439,7 @@ PUT /projects/:id/issues/:issue_iid
| `title` | string | no | The title of an issue |
| `description` | string | no | The description of an issue |
| `confidential` | boolean | no | Updates an issue to be confidential |
-| `assignee_ids` | Array[integer] | no | The ID of a user to assign the issue to |
+| `assignee_ids` | Array[integer] | no | The ID of the users to assign the issue to |
| `milestone_id` | integer | no | The ID of a milestone to assign the issue to |
| `labels` | string | no | Comma-separated label names for an issue |
| `state_event` | string | no | The state event of an issue. Set `close` to close the issue and `reopen` to reopen it |
@@ -484,7 +484,7 @@ Example response:
}
```
-**Note**: `assignee` column is deprecated, it shows the first assignee only.
+**Note**: `assignee` column is deprecated, now we show it as a single-sized array `assignees` to conform to the GitLab EE API.
## Delete an issue
@@ -570,7 +570,7 @@ Example response:
}
```
-**Note**: `assignee` column is deprecated, it shows the first assignee only.
+**Note**: `assignee` column is deprecated, now we show it as a single-sized array `assignees` to conform to the GitLab EE API.
## Subscribe to an issue
@@ -635,7 +635,7 @@ Example response:
}
```
-**Note**: `assignee` column is deprecated, it shows the first assignee only.
+**Note**: `assignee` column is deprecated, now we show it as a single-sized array `assignees` to conform to the GitLab EE API.
## Unsubscribe from an issue
@@ -757,7 +757,7 @@ Example response:
}
```
-**Note**: `assignee` column is deprecated, it shows the first assignee only.
+**Note**: `assignee` column is deprecated, now we show it as a single-sized array `assignees` to conform to the GitLab EE API.
## Set a time estimate for an issue
diff --git a/doc/api/jobs.md b/doc/api/jobs.md
index 404da3dc603..297115e94ac 100644
--- a/doc/api/jobs.md
+++ b/doc/api/jobs.md
@@ -11,7 +11,7 @@ GET /projects/:id/jobs
| Attribute | Type | Required | Description |
|-----------|---------|----------|---------------------|
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
-| `scope` | string **or** array of strings | no | The scope of jobs to show, one or array of: `created`, `pending`, `running`, `failed`, `success`, `canceled`, `skipped`; showing all jobs if none provided |
+| `scope` | string **or** array of strings | no | The scope of jobs to show, one or array of: `created`, `pending`, `running`, `failed`, `success`, `canceled`, `skipped`, `manual`; showing all jobs if none provided |
```
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" 'https://gitlab.example.com/api/v4/projects/1/jobs?scope[]=pending&scope[]=running'
@@ -125,7 +125,7 @@ GET /projects/:id/pipelines/:pipeline_id/jobs
|---------------|--------------------------------|----------|----------------------|
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `pipeline_id` | integer | yes | The ID of a pipeline |
-| `scope` | string **or** array of strings | no | The scope of jobs to show, one or array of: `created`, `pending`, `running`, `failed`, `success`, `canceled`, `skipped`; showing all jobs if none provided |
+| `scope` | string **or** array of strings | no | The scope of jobs to show, one or array of: `created`, `pending`, `running`, `failed`, `success`, `canceled`, `skipped`, `manual`; showing all jobs if none provided |
```
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" 'https://gitlab.example.com/api/v4/projects/1/pipelines/6/jobs?scope[]=pending&scope[]=running'
diff --git a/doc/api/keys.md b/doc/api/keys.md
index 3ace1040f38..376ac27df3a 100644
--- a/doc/api/keys.md
+++ b/doc/api/keys.md
@@ -1,4 +1,4 @@
-# Keys
+# Keys API
## Get SSH key with user by ID of an SSH key
diff --git a/doc/api/labels.md b/doc/api/labels.md
index 778348ea371..ec93cf50e7a 100644
--- a/doc/api/labels.md
+++ b/doc/api/labels.md
@@ -1,4 +1,4 @@
-# Labels
+# Labels API
## List labels
diff --git a/doc/api/members.md b/doc/api/members.md
index 3c661284f11..3234f833eae 100644
--- a/doc/api/members.md
+++ b/doc/api/members.md
@@ -1,4 +1,4 @@
-# Group and project members
+# Group and project members API
**Valid access levels**
diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md
index dde855b2bd4..cb22b67f556 100644
--- a/doc/api/merge_requests.md
+++ b/doc/api/merge_requests.md
@@ -1,4 +1,4 @@
-# Merge requests
+# Merge requests API
## List merge requests
diff --git a/doc/api/milestones.md b/doc/api/milestones.md
index 7640eeb8d00..a082d548499 100644
--- a/doc/api/milestones.md
+++ b/doc/api/milestones.md
@@ -1,4 +1,4 @@
-# Milestones
+# Milestones API
## List project milestones
diff --git a/doc/api/namespaces.md b/doc/api/namespaces.md
index eef06d5f324..4ad6071a0ed 100644
--- a/doc/api/namespaces.md
+++ b/doc/api/namespaces.md
@@ -1,4 +1,4 @@
-# Namespaces
+# Namespaces API
Usernames and groupnames fall under a special category called namespaces.
diff --git a/doc/api/notes.md b/doc/api/notes.md
index b71fea5fc9f..388e6989df2 100644
--- a/doc/api/notes.md
+++ b/doc/api/notes.md
@@ -1,4 +1,4 @@
-# Notes
+# Notes API
Notes are comments on snippets, issues or merge requests.
diff --git a/doc/api/notification_settings.md b/doc/api/notification_settings.md
index 43047917f77..3a2c398e355 100644
--- a/doc/api/notification_settings.md
+++ b/doc/api/notification_settings.md
@@ -1,4 +1,4 @@
-# Notification settings
+# Notification settings API
>**Note:** This feature was [introduced][ce-5632] in GitLab 8.12.
diff --git a/doc/api/pipeline_triggers.md b/doc/api/pipeline_triggers.md
index d639e8a0991..9030ae32d17 100644
--- a/doc/api/pipeline_triggers.md
+++ b/doc/api/pipeline_triggers.md
@@ -1,4 +1,4 @@
-# Pipeline triggers
+# Pipeline triggers API
You can read more about [triggering pipelines through the API](../ci/triggers/README.md).
diff --git a/doc/api/projects.md b/doc/api/projects.md
index 188fbe7447d..6b919f71792 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -1,5 +1,4 @@
-# Projects
-
+# Projects API
### Project visibility level
@@ -17,8 +16,6 @@ Constants for project visibility levels are next:
* `public`:
The project can be cloned without any authentication.
-
-
## List projects
Get a list of visible projects for authenticated user. When being accessed without authentication, all public projects are returned.
diff --git a/doc/api/repositories.md b/doc/api/repositories.md
index 859cbd63831..bccef924375 100644
--- a/doc/api/repositories.md
+++ b/doc/api/repositories.md
@@ -1,4 +1,4 @@
-# Repositories
+# Repositories API
## List repository tree
diff --git a/doc/api/repository_files.md b/doc/api/repository_files.md
index aec91abd390..0b5782a8cc4 100644
--- a/doc/api/repository_files.md
+++ b/doc/api/repository_files.md
@@ -1,4 +1,4 @@
-# Repository files
+# Repository files API
**CRUD for repository files**
diff --git a/doc/api/services.md b/doc/api/services.md
index 0f42c256099..49b87a4228c 100644
--- a/doc/api/services.md
+++ b/doc/api/services.md
@@ -1,4 +1,4 @@
-# Services
+# Services API
## Asana
@@ -516,7 +516,7 @@ Example response:
"merge_requests_events": true,
"tag_push_events": true,
"note_events": true,
- "build_events": true,
+ "job_events": true,
"pipeline_events": true,
"properties": {
"token": "9koXpg98eAheJpvBs5tK"
diff --git a/doc/api/session.md b/doc/api/session.md
index 056cc32597c..7dd504b67c5 100644
--- a/doc/api/session.md
+++ b/doc/api/session.md
@@ -1,4 +1,4 @@
-# Session
+# Session API
## Deprecation Notice
diff --git a/doc/api/settings.md b/doc/api/settings.md
index d99695ca986..eefbdda42ce 100644
--- a/doc/api/settings.md
+++ b/doc/api/settings.md
@@ -1,4 +1,4 @@
-# Application settings
+# Application settings API
These API calls allow you to read and modify GitLab instance application
settings as appear in `/admin/application_settings`. You have to be an
diff --git a/doc/api/sidekiq_metrics.md b/doc/api/sidekiq_metrics.md
index ea10a26bcd0..b9500916cf2 100644
--- a/doc/api/sidekiq_metrics.md
+++ b/doc/api/sidekiq_metrics.md
@@ -1,4 +1,4 @@
-# Sidekiq Metrics
+# Sidekiq Metrics API
>**Note:** This endpoint is only available on GitLab 8.9 and above.
diff --git a/doc/api/snippets.md b/doc/api/snippets.md
index e09d930698e..fb8cf97896c 100644
--- a/doc/api/snippets.md
+++ b/doc/api/snippets.md
@@ -1,4 +1,4 @@
-# Snippets
+# Snippets API
> [Introduced][ce-6373] in GitLab 8.15.
diff --git a/doc/api/system_hooks.md b/doc/api/system_hooks.md
index bad380794c1..9750475f0a6 100644
--- a/doc/api/system_hooks.md
+++ b/doc/api/system_hooks.md
@@ -1,4 +1,4 @@
-# System hooks
+# System hooks API
All methods require administrator authorization.
diff --git a/doc/api/tags.md b/doc/api/tags.md
index 0f6c4e6794e..54f092d1d30 100644
--- a/doc/api/tags.md
+++ b/doc/api/tags.md
@@ -1,4 +1,4 @@
-# Tags
+# Tags API
## List project repository tags
diff --git a/doc/api/templates/gitignores.md b/doc/api/templates/gitignores.md
index 3f2f4ed54e0..d3f5c88ca90 100644
--- a/doc/api/templates/gitignores.md
+++ b/doc/api/templates/gitignores.md
@@ -1,4 +1,4 @@
-# Gitignores
+# Gitignores API
## List gitignore templates
diff --git a/doc/api/templates/gitlab_ci_ymls.md b/doc/api/templates/gitlab_ci_ymls.md
index 27e8973da58..bdb128fc336 100644
--- a/doc/api/templates/gitlab_ci_ymls.md
+++ b/doc/api/templates/gitlab_ci_ymls.md
@@ -1,4 +1,4 @@
-# GitLab CI YMLs
+# GitLab CI YMLs API
## List GitLab CI YML templates
diff --git a/doc/api/templates/licenses.md b/doc/api/templates/licenses.md
index 33018f0c53f..8d1006e08c5 100644
--- a/doc/api/templates/licenses.md
+++ b/doc/api/templates/licenses.md
@@ -1,4 +1,4 @@
-# Licenses
+# Licenses API
## List license templates
diff --git a/doc/api/todos.md b/doc/api/todos.md
index 77667a57195..dd4c737b729 100644
--- a/doc/api/todos.md
+++ b/doc/api/todos.md
@@ -1,4 +1,4 @@
-# Todos
+# Todos API
> [Introduced][ce-3188] in GitLab 8.10.
diff --git a/doc/api/users.md b/doc/api/users.md
index 86027bcc05c..331f9a9b80b 100644
--- a/doc/api/users.md
+++ b/doc/api/users.md
@@ -1,4 +1,4 @@
-# Users
+# Users API
## List users
diff --git a/doc/api/v3_to_v4.md b/doc/api/v3_to_v4.md
index 8e002fe0022..9db8e0351cf 100644
--- a/doc/api/v3_to_v4.md
+++ b/doc/api/v3_to_v4.md
@@ -1,8 +1,10 @@
-# V3 to V4 version
+# API V3 to API V4
Since GitLab 9.0, API V4 is the preferred version to be used.
-V3 will remain working until at least GitLab 9.3. The V3 API documentation is still [available](https://gitlab.com/gitlab-org/gitlab-ce/blob/8-16-stable/doc/api/README.md).
+API V3 will be removed in GitLab 9.5, to be released on August 22, 2017. In the
+meantime, we advise you to make any necessary changes to applications that use
+V3. The V3 API documentation is still [available](https://gitlab.com/gitlab-org/gitlab-ce/blob/8-16-stable/doc/api/README.md).
Below are the changes made between V3 and V4.
diff --git a/doc/articles/how_to_configure_ldap_gitlab_ce/index.md b/doc/articles/how_to_configure_ldap_gitlab_ce/index.md
index 1702c2184f2..6892905dd94 100644
--- a/doc/articles/how_to_configure_ldap_gitlab_ce/index.md
+++ b/doc/articles/how_to_configure_ldap_gitlab_ce/index.md
@@ -1,6 +1,6 @@
# How to configure LDAP with GitLab CE
-> **Type:** admin guide ||
+> **Article [Type](../../development/writing_documentation.html#types-of-technical-articles):** admin guide ||
> **Level:** intermediary ||
> **Author:** [Chris Wilson](https://gitlab.com/MrChrisW) ||
> **Publication date:** 2017/05/03
diff --git a/doc/articles/how_to_install_git/index.md b/doc/articles/how_to_install_git/index.md
new file mode 100644
index 00000000000..66d866b2d09
--- /dev/null
+++ b/doc/articles/how_to_install_git/index.md
@@ -0,0 +1,66 @@
+# Installing Git
+
+> **Article [Type](../../development/writing_documentation.html#types-of-technical-articles):** user guide ||
+> **Level:** beginner ||
+> **Author:** [Sean Packham](https://gitlab.com/SeanPackham) ||
+> **Publication date:** 2017/05/15
+
+To begin contributing to GitLab projects
+you will need to install the Git client on your computer.
+This article will show you how to install Git on macOS, Ubuntu Linux and Windows.
+
+## Install Git on macOS using the Homebrew package manager
+
+Although it is easy to use the version of Git shipped with macOS
+or install the latest version of Git on macOS by downloading it from the project website,
+we recommend installing it via Homebrew to get access to
+an extensive selection of dependancy managed libraries and applications.
+
+If you are sure you don't need access to any additional development libraries
+or don't have approximately 15gb of available disk space for Xcode and Homebrew
+use one of the the aforementioned methods.
+
+### Installing Xcode
+
+Xcode is needed by Homebrew to build dependencies.
+You can install [XCode](https://developer.apple.com/xcode/)
+through the macOS App Store.
+
+### Installing Homebrew
+
+Once Xcode is installed browse to the [Homebrew website](http://brew.sh/index.html)
+for the official Homebrew installation instructions.
+
+### Installing Git via Homebrew
+
+With Homebrew installed you are now ready to install Git.
+Open a Terminal and enter in the following command:
+
+```bash
+brew install git
+```
+
+Congratulations you should now have Git installed via Homebrew.
+Next read our article on [adding an SSH key to GitLab](../../ssh/README.md).
+
+## Install Git on Ubuntu Linux
+
+On Ubuntu and other Linux operating systems
+it is recommended to use the built in package manager to install Git.
+
+Open a Terminal and enter in the following commands
+to install the latest Git from the official Git maintained package archives:
+
+```bash
+sudo apt-add-repository ppa:git-core/ppa
+sudo apt-get update
+sudo apt-get install git
+```
+
+Congratulations you should now have Git installed via the Ubuntu package manager.
+Next read our article on [adding an SSH key to GitLab](../../ssh/README.md).
+
+## Installing Git on Windows from the Git website
+
+Browse to the [Git website](https://git-scm.com/) and download and install Git for Windows.
+Next read our article on [adding an SSH key to GitLab](../../ssh/README.md).
diff --git a/doc/articles/index.md b/doc/articles/index.md
index 49db64134f5..342fa88e80f 100644
--- a/doc/articles/index.md
+++ b/doc/articles/index.md
@@ -12,6 +12,10 @@ They are written by members of the GitLab Team and by
- **LDAP**
- [How to configure LDAP with GitLab CE](how_to_configure_ldap_gitlab_ce/index.md)
+## Git
+
+- [How to install Git](how_to_install_git/index.md)
+
## GitLab Pages
- **GitLab Pages from A to Z**
diff --git a/doc/ci/README.md b/doc/ci/README.md
index c4f9a3cb573..4f17e7062ab 100644
--- a/doc/ci/README.md
+++ b/doc/ci/README.md
@@ -66,7 +66,8 @@ learn how to leverage its potential even more.
submodules are involved
- [Auto deploy](autodeploy/index.md)
- [Use SSH keys in your build environment](ssh_keys/README.md)
-- [Trigger jobs through the GitLab API](triggers/README.md)
+- [Trigger pipelines through the GitLab API](triggers/README.md)
+- [Trigger pipelines on a schedule](../user/project/pipelines/schedules.md)
## Review Apps
@@ -108,6 +109,7 @@ Here is an collection of tutorials and guides on setting up your CI pipeline.
- [Scala](examples/test-scala-application.md)
- [Phoenix](examples/test-phoenix-application.md)
- [Run PHP Composer & NPM scripts then deploy them to a staging server](examples/deployment/composer-npm-deploy.md)
+ - [Analyze code quality with the Code Climate CLI](examples/code_climate.md)
- **Blog posts**
- [Automated Debian packaging](https://about.gitlab.com/2016/10/12/automated-debian-package-build-with-gitlab-ci/)
- [Spring boot application with GitLab CI and Kubernetes](https://about.gitlab.com/2016/12/14/continuous-delivery-of-a-spring-boot-application-with-gitlab-ci-and-kubernetes/)
diff --git a/doc/ci/api/README.md b/doc/ci/api/README.md
index 4ca8d92d7cc..98f37935427 100644
--- a/doc/ci/api/README.md
+++ b/doc/ci/api/README.md
@@ -1,3 +1 @@
-# GitLab CI API
-
This document was moved to a [new location](../../api/ci/README.md).
diff --git a/doc/ci/api/builds.md b/doc/ci/api/builds.md
index f5bd3181c02..0563a367609 100644
--- a/doc/ci/api/builds.md
+++ b/doc/ci/api/builds.md
@@ -1,3 +1 @@
-# Builds API
-
This document was moved to a [new location](../../api/ci/builds.md).
diff --git a/doc/ci/api/runners.md b/doc/ci/api/runners.md
index b14ea99db76..1027363851c 100644
--- a/doc/ci/api/runners.md
+++ b/doc/ci/api/runners.md
@@ -1,3 +1 @@
-# Runners API
-
This document was moved to a [new location](../../api/ci/runners.md).
diff --git a/doc/ci/docker/using_docker_build.md b/doc/ci/docker/using_docker_build.md
index ffa0831290a..408d46a756c 100644
--- a/doc/ci/docker/using_docker_build.md
+++ b/doc/ci/docker/using_docker_build.md
@@ -37,7 +37,7 @@ GitLab Runner then executes job scripts as the `gitlab-runner` user.
```bash
sudo gitlab-ci-multi-runner register -n \
- --url https://gitlab.com/ci \
+ --url https://gitlab.com/ \
--registration-token REGISTRATION_TOKEN \
--executor shell \
--description "My Runner"
@@ -94,7 +94,7 @@ In order to do that, follow the steps:
```bash
sudo gitlab-ci-multi-runner register -n \
- --url https://gitlab.com/ci \
+ --url https://gitlab.com/ \
--registration-token REGISTRATION_TOKEN \
--executor docker \
--description "My Docker Runner" \
@@ -112,7 +112,7 @@ In order to do that, follow the steps:
```
[[runners]]
- url = "https://gitlab.com/ci"
+ url = "https://gitlab.com/"
token = TOKEN
executor = "docker"
[runners.docker]
@@ -179,7 +179,7 @@ In order to do that, follow the steps:
```bash
sudo gitlab-ci-multi-runner register -n \
- --url https://gitlab.com/ci \
+ --url https://gitlab.com/ \
--registration-token REGISTRATION_TOKEN \
--executor docker \
--description "My Docker Runner" \
@@ -197,7 +197,7 @@ In order to do that, follow the steps:
```
[[runners]]
- url = "https://gitlab.com/ci"
+ url = "https://gitlab.com/"
token = REGISTRATION_TOKEN
executor = "docker"
[runners.docker]
diff --git a/doc/ci/docker/using_docker_images.md b/doc/ci/docker/using_docker_images.md
index f025a7e3496..96834e15bb9 100644
--- a/doc/ci/docker/using_docker_images.md
+++ b/doc/ci/docker/using_docker_images.md
@@ -146,7 +146,7 @@ private registries that could also require authentication.
All you have to do is be explicit on the image definition in `.gitlab-ci.yml`.
```yaml
-image: my.registry.tld:5000/namepace/image:tag
+image: my.registry.tld:5000/namespace/image:tag
```
In the example above, GitLab Runner will look at `my.registry.tld:5000` for the
diff --git a/doc/ci/environments.md b/doc/ci/environments.md
index b28f3e13eae..bab765d1e12 100644
--- a/doc/ci/environments.md
+++ b/doc/ci/environments.md
@@ -442,7 +442,8 @@ and/or `production`) you can see this information in the merge request itself.
![Environment URLs in merge request](img/environments_link_url_mr.png)
-### Go directly from source files to public pages on the environment
+### <a name="route-map"></a>Go directly from source files to public pages on the environment
+
> Introduced in GitLab 8.17.
diff --git a/doc/ci/examples/README.md b/doc/ci/examples/README.md
index 33c27b39a8a..2458cb959ab 100644
--- a/doc/ci/examples/README.md
+++ b/doc/ci/examples/README.md
@@ -55,6 +55,7 @@ Apart from those, here is an collection of tutorials and guides on setting up yo
- [Using `dpl` as deployment tool](deployment/README.md)
- [Repositories with examples for various languages](https://gitlab.com/groups/gitlab-examples)
- [The .gitlab-ci.yml file for GitLab itself](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.gitlab-ci.yml)
+- [Analyze code quality with the Code Climate CLI](code_climate.md)
- **Articles:**
- [Continuous Deployment with GitLab: how to build and deploy a Debian Package with GitLab CI](https://about.gitlab.com/2016/10/12/automated-debian-package-build-with-gitlab-ci/)
diff --git a/doc/ci/examples/code_climate.md b/doc/ci/examples/code_climate.md
new file mode 100644
index 00000000000..bd53f80ce14
--- /dev/null
+++ b/doc/ci/examples/code_climate.md
@@ -0,0 +1,28 @@
+# Analyze project code quality with Code Climate CLI
+
+This example shows how to run [Code Climate CLI][cli] on your code by using\
+GitLab CI and Docker.
+
+First, you need GitLab Runner with [docker-in-docker executor](../docker/using_docker_build.md#use-docker-in-docker-executor).
+
+Once you setup the Runner add new job to `.gitlab-ci.yml`:
+
+```yaml
+codeclimate:
+ image: docker:latest
+ variables:
+ DOCKER_DRIVER: overlay
+ services:
+ - docker:dind
+ script:
+ - docker pull codeclimate/codeclimate
+ - docker run --env CODECLIMATE_CODE="$PWD" --volume "$PWD":/code --volume /var/run/docker.sock:/var/run/docker.sock --volume /tmp/cc:/tmp/cc codeclimate/codeclimate init
+ - docker run --env CODECLIMATE_CODE="$PWD" --volume "$PWD":/code --volume /var/run/docker.sock:/var/run/docker.sock --volume /tmp/cc:/tmp/cc codeclimate/codeclimate analyze -f json > codeclimate.json
+ artifacts:
+ paths: [codeclimate.json]
+```
+
+This will create a `codeclimate` job in your CI pipeline and will allow you to
+download and analyze the report artifact in JSON format.
+
+[cli]: https://github.com/codeclimate/codeclimate
diff --git a/doc/ci/examples/test-and-deploy-python-application-to-heroku.md b/doc/ci/examples/test-and-deploy-python-application-to-heroku.md
index e4d3970deac..73aebaf6d7f 100644
--- a/doc/ci/examples/test-and-deploy-python-application-to-heroku.md
+++ b/doc/ci/examples/test-and-deploy-python-application-to-heroku.md
@@ -55,11 +55,11 @@ You can do this through the [Dashboard](https://dashboard.heroku.com/).
### Create runner
First install [Docker Engine](https://docs.docker.com/installation/).
To build this project you also need to have [GitLab Runner](https://about.gitlab.com/gitlab-ci/#gitlab-runner).
-You can use public runners available on `gitlab.com/ci`, but you can register your own:
+You can use public runners available on `gitlab.com`, but you can register your own:
```
gitlab-ci-multi-runner register \
--non-interactive \
- --url "https://gitlab.com/ci/" \
+ --url "https://gitlab.com/" \
--registration-token "PROJECT_REGISTRATION_TOKEN" \
--description "python-3.5" \
--executor "docker" \
diff --git a/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md b/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md
index 42f15a27f12..6fa64a67e82 100644
--- a/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md
+++ b/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md
@@ -50,11 +50,11 @@ You can do this through the [Dashboard](https://dashboard.heroku.com/).
### Create runner
First install [Docker Engine](https://docs.docker.com/installation/).
To build this project you also need to have [GitLab Runner](https://about.gitlab.com/gitlab-ci/#gitlab-runner).
-You can use public runners available on `gitlab.com/ci`, but you can register your own:
+You can use public runners available on `gitlab.com`, but you can register your own:
```
gitlab-ci-multi-runner register \
--non-interactive \
- --url "https://gitlab.com/ci/" \
+ --url "https://gitlab.com/" \
--registration-token "PROJECT_REGISTRATION_TOKEN" \
--description "ruby-2.2" \
--executor "docker" \
diff --git a/doc/ci/img/pipeline_schedules_list.png b/doc/ci/img/pipeline_schedules_list.png
deleted file mode 100644
index 9388fac98eb..00000000000
--- a/doc/ci/img/pipeline_schedules_list.png
+++ /dev/null
Binary files differ
diff --git a/doc/ci/pipeline_schedules.md b/doc/ci/pipeline_schedules.md
deleted file mode 100644
index 0a9b0e7173f..00000000000
--- a/doc/ci/pipeline_schedules.md
+++ /dev/null
@@ -1,40 +0,0 @@
-# Pipeline Schedules
-
-> **Note**:
-- This feature was introduced in 9.1 as [Trigger Schedule][ce-105533]
-- In 9.2, the feature was [renamed to Pipeline Schedule][ce-10853]
-
-Pipeline schedules can be used to run pipelines only once, or for example every
-month on the 22nd for a certain branch.
-
-## Using Pipeline Schedules
-
-In order to schedule pipelines, navigate to your their pages **Pipelines ➔ Schedules**
-and click the **New Schedule** button.
-
-![New Schedule Form](img/pipeline_schedules_new_form.png)
-
-After entering the form, hit **Save Schedule** for the changes to have effect.
-You can check a next execution date of the scheduled trigger, which is automatically calculated by a server.
-
-## Taking ownership
-
-![Schedules list](img/pipeline_schedules_list.png)
-
-Pipelines are executed as a user, which owns a schedule. This influences what
-projects and other resources the pipeline has access to. If a user does not own
-a pipeline, you can take ownership by clicking the **Take ownership** button.
-The next time a pipeline is scheduled, your credentials will be used.
-
-> **Notes**:
-- Those pipelines won't be executed precicely. Because schedules are handled by
-Sidekiq, which runs according to its interval. For exmaple, if you set a schedule to
-create a pipeline every minute (`* * * * *`) and the Sidekiq worker performs 00:00
-and 12:00 o'clock every day (`0 */12 * * *`), only 2 pipelines will be created per day.
-To change the Sidekiq worker's frequency, you have to edit the `trigger_schedule_worker_cron`
-value in your `gitlab.rb` and restart GitLab. The Sidekiq worker's configuration
-on GiLab.com is able to be looked up at [here](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/config/gitlab.yml.example#L185).
-- Cron notation is parsed by [Rufus-Scheduler](https://github.com/jmettraux/rufus-scheduler).
-
-[ce-10533]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10533
-[ce-10853]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10853
diff --git a/doc/ci/quick_start/README.md b/doc/ci/quick_start/README.md
index 30f209f80eb..41cae58782d 100644
--- a/doc/ci/quick_start/README.md
+++ b/doc/ci/quick_start/README.md
@@ -155,7 +155,7 @@ Find more information about different Runners in the
[Runners](../runners/README.md) documentation.
You can find whether any Runners are assigned to your project by going to
-**Settings ➔ Runners**. Setting up a Runner is easy and straightforward. The
+**Settings ➔ CI/CD Pipelines**. Setting up a Runner is easy and straightforward. The
official Runner supported by GitLab is written in Go and its documentation
can be found at <https://docs.gitlab.com/runner/>.
@@ -168,7 +168,7 @@ Follow the links above to set up your own Runner or use a Shared Runner as
described in the next section.
Once the Runner has been set up, you should see it on the Runners page of your
-project, following **Settings ➔ Runners**.
+project, following **Settings ➔ CI/CD Pipelines**.
![Activated runners](img/runners_activated.png)
@@ -181,7 +181,7 @@ These are special virtual machines that run on GitLab's infrastructure and can
build any project.
To enable the **Shared Runners** you have to go to your project's
-**Settings ➔ Runners** and click **Enable shared runners**.
+**Settings ➔ CI/CD Pipelines** and click **Enable shared runners**.
[Read more on Shared Runners](../runners/README.md).
diff --git a/doc/ci/triggers/README.md b/doc/ci/triggers/README.md
index 1251313cd14..27cdaa9978b 100644
--- a/doc/ci/triggers/README.md
+++ b/doc/ci/triggers/README.md
@@ -1,6 +1,6 @@
-# Triggering jobs through the API
+# Triggering pipelines through the API
-> **Note**:
+> **Notes**:
- [Introduced][ci-229] in GitLab CE 7.14.
- GitLab 8.12 has a completely redesigned job permissions system. Read all
about the [new model and its implications](../../user/project/new_ci_build_permissions_model.md#job-triggers).
@@ -208,7 +208,7 @@ curl --request POST \
https://gitlab.example.com/api/v4/projects/9/trigger/pipeline
```
-### Using webhook to trigger job
+### Using a webhook to trigger a pipeline
You can add the following webhook to another project in order to trigger a job:
@@ -216,4 +216,18 @@ You can add the following webhook to another project in order to trigger a job:
https://gitlab.example.com/api/v4/projects/9/ref/master/trigger/pipeline?token=TOKEN&variables[UPLOAD_TO_S3]=true
```
+### Using cron to trigger nightly pipelines
+
+>**Note:**
+The following behavior can also be achieved through GitLab's UI with
+[pipeline schedules](../../user/project/pipelines/schedules.md).
+
+Whether you craft a script or just run cURL directly, you can trigger jobs
+in conjunction with cron. The example below triggers a job on the `master`
+branch of project with ID `9` every night at `00:30`:
+
+```bash
+30 0 * * * curl --request POST --form token=TOKEN --form ref=master https://gitlab.example.com/api/v4/projects/9/trigger/pipeline
+```
+
[ci-229]: https://gitlab.com/gitlab-org/gitlab-ci/merge_requests/229
diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md
index 045d3821f66..9a3bbcf2853 100644
--- a/doc/ci/variables/README.md
+++ b/doc/ci/variables/README.md
@@ -333,7 +333,7 @@ prefix the variable name with the dollar sign (`$`):
```
job_name:
script:
- - echo $CI_job_ID
+ - echo $CI_JOB_ID
```
You can also list all environment variables with the `export` command,
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index 16308a957cb..8546a99a022 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -147,6 +147,10 @@ variables:
DATABASE_URL: "postgres://postgres@postgres/my_database"
```
+>**Note:**
+Integers (as well as strings) are legal both for variable's name and value.
+Floats are not legal and cannot be used.
+
These variables can be later used in all executed commands and scripts.
The YAML-defined variables are also set to all created service containers,
thus allowing to fine tune them. Variables can be also defined on a
@@ -1152,7 +1156,7 @@ Example:
```yaml
variables:
- GET_SOURCES_ATTEMPTS: "3"
+ GET_SOURCES_ATTEMPTS: 3
```
You can set them in the global [`variables`](#variables) section or the
diff --git a/doc/development/README.md b/doc/development/README.md
index 63db332b557..934c6849ff9 100644
--- a/doc/development/README.md
+++ b/doc/development/README.md
@@ -50,6 +50,10 @@
- [Post Deployment Migrations](post_deployment_migrations.md)
- [Foreign Keys & Associations](foreign_keys.md)
+## i18n
+
+- [Internationalization for GitLab](i18n_guide.md)
+
## Compliance
- [Licensing](licensing.md) for ensuring license compliance
diff --git a/doc/development/architecture.md b/doc/development/architecture.md
index 4eb7a8eee48..b36fd52603b 100644
--- a/doc/development/architecture.md
+++ b/doc/development/architecture.md
@@ -4,7 +4,7 @@
There are two editions of GitLab: [Enterprise Edition](https://about.gitlab.com/gitlab-ee/) (EE) and [Community Edition](https://about.gitlab.com/gitlab-ce/) (CE). GitLab CE is delivered via git from the [gitlabhq repository](https://gitlab.com/gitlab-org/gitlab-ce/tree/master). New versions of GitLab are released in stable branches and the master branch is for bleeding edge development.
-EE releases are available not long after CE releases. To obtain the GitLab EE there is a [repository at gitlab.com](https://gitlab.com/subscribers/gitlab-ee). For more information about the release process see the section 'New versions and upgrading' in the readme.
+EE releases are available not long after CE releases. To obtain the GitLab EE there is a [repository at gitlab.com](https://gitlab.com/gitlab-org/gitlab-ee). For more information about the release process see the section 'New versions and upgrading' in the readme.
Both EE and CE require some add-on components called gitlab-shell and Gitaly. These components are available from the [gitlab-shell](https://gitlab.com/gitlab-org/gitlab-shell/tree/master) and [gitaly](https://gitlab.com/gitlab-org/gitaly/tree/master) repositories respectively. New versions are usually tags but staying on the master branch will give you the latest stable version. New releases are generally around the same time as GitLab CE releases with exception for informal security updates deemed critical.
diff --git a/doc/development/doc_styleguide.md b/doc/development/doc_styleguide.md
index 1e81905c081..5b09f79f143 100644
--- a/doc/development/doc_styleguide.md
+++ b/doc/development/doc_styleguide.md
@@ -198,10 +198,17 @@ You can combine one or more of the following:
the `.md` document that you're working on is located. Always prepend their
names with the name of the document that they will be included in. For
example, if there is a document called `twitter.md`, then a valid image name
- could be `twitter_login_screen.png`.
+ could be `twitter_login_screen.png`. [**Exception**: images for
+ [articles](writing_documentation.md#technical-articles) should be
+ put in a directory called `img` underneath `/articles/article_title/img/`, therefore,
+ there's no need to prepend the document name to their filenames.]
- Images should have a specific, non-generic name that will differentiate them.
- Keep all file names in lower case.
- Consider using PNG images instead of JPEG.
+- Compress all images with <https://tinypng.com/> or similar tool.
+- Compress gifs with <https://ezgif.com/optimize> or similar toll.
+- Images should be used (only when necessary) to _illustrate_ the description
+of a process, not to _replace_ it.
Inside the document:
diff --git a/doc/development/fe_guide/img/testing_triangle.png b/doc/development/fe_guide/img/testing_triangle.png
new file mode 100644
index 00000000000..7a9a848c2ee
--- /dev/null
+++ b/doc/development/fe_guide/img/testing_triangle.png
Binary files differ
diff --git a/doc/development/fe_guide/index.md b/doc/development/fe_guide/index.md
index a08694fb66a..64bcb4a0257 100644
--- a/doc/development/fe_guide/index.md
+++ b/doc/development/fe_guide/index.md
@@ -45,15 +45,11 @@ should be `new-feature`.
branch from `new-feature`, let's call it `new-feature-step-2` and repeat the process done before.
```shell
-* master
-|\
-| * new-feature
-| |\
-| | * new-feature-step-1
-| |\
-| | * new-feature-step-2
-| |\
-| | * new-feature-step-3
+master
+└─ new-feature
+ ├─ new-feature-step-1
+ ├─ new-feature-step-2
+ └─ new-feature-step-3
```
**Tips**
diff --git a/doc/development/fe_guide/testing.md b/doc/development/fe_guide/testing.md
index a8c264bbd3c..0ef9fc61a61 100644
--- a/doc/development/fe_guide/testing.md
+++ b/doc/development/fe_guide/testing.md
@@ -1,11 +1,20 @@
# Frontend Testing
-There are two types of tests you'll encounter while developing frontend code
-at GitLab. We use Karma and Jasmine for JavaScript unit testing, and RSpec
-feature tests with Capybara for integration testing.
+There are two types of test suites you'll encounter while developing frontend code
+at GitLab. We use Karma and Jasmine for JavaScript unit and integration testing, and RSpec
+feature tests with Capybara for e2e (end-to-end) integration testing.
-Feature tests need to be written for all new features. Regression tests ought
-to be written for all bug fixes to prevent them from recurring in the future.
+Unit and feature tests need to be written for all new features.
+Most of the time, you should use rspec for your feature tests.
+There are cases where the behaviour you are testing is not worth the time spent running the full application,
+for example, if you are testing styling, animation or small actions that don't involve the backend,
+you should write an integration test using Jasmine.
+
+![Testing priority triangle](img/testing_triangle.png)
+
+_This diagram demonstrates the relative priority of each test type we use_
+
+Regression tests should be written for bug fixes to prevent them from recurring in the future.
See [the Testing Standards and Style Guidelines](../testing.md)
for more information on general testing practices at GitLab.
@@ -13,10 +22,12 @@ for more information on general testing practices at GitLab.
## Karma test suite
GitLab uses the [Karma][karma] test runner with [Jasmine][jasmine] as its test
-framework for our JavaScript unit tests. For tests that rely on DOM
-manipulation, we generate HTML files using RSpec suites (see `spec/javascripts/fixtures/*.rb` for examples).
+framework for our JavaScript unit and integration tests. For integration tests,
+we generate HTML files using RSpec (see `spec/javascripts/fixtures/*.rb` for examples).
Some fixtures are still HAML templates that are translated to HTML files using the same mechanism (see `static_fixtures.rb`).
-Those will be migrated over time.
+Adding these static fixtures should be avoided as they are harder to keep up to date with real views.
+The existing static fixtures will be migrated over time.
+Please see [gitlab-org/gitlab-ce#24753](https://gitlab.com/gitlab-org/gitlab-ce/issues/24753) to track our progress.
Fixtures are served during testing by the [jasmine-jquery][jasmine-jquery] plugin.
JavaScript tests live in `spec/javascripts/`, matching the folder structure
@@ -28,7 +39,9 @@ browser and you will not have access to certain APIs, such as
[`Notification`](https://developer.mozilla.org/en-US/docs/Web/API/notification),
which will have to be stubbed.
-### Writing tests
+### Best practice
+
+#### Naming unit tests
When writing describe test blocks to test specific functions/methods,
please use the method name as the describe block name.
@@ -55,6 +68,77 @@ describe('.methodName', () => {
});
});
```
+#### Testing Promises
+
+When testing Promises you should always make sure that the test is asynchronous and rejections are handled.
+Your Promise chain should therefore end with a call of the `done` callback and `done.fail` in case an error occurred.
+
+```javascript
+// Good
+it('tests a promise', (done) => {
+ promise
+ .then((data) => {
+ expect(data).toBe(asExpected);
+ })
+ .then(done)
+ .catch(done.fail);
+});
+
+// Good
+it('tests a promise rejection', (done) => {
+ promise
+ .then(done.fail)
+ .catch((error) => {
+ expect(error).toBe(expectedError);
+ })
+ .then(done)
+ .catch(done.fail);
+});
+
+// Bad (missing done callback)
+it('tests a promise', () => {
+ promise
+ .then((data) => {
+ expect(data).toBe(asExpected);
+ })
+});
+
+// Bad (missing catch)
+it('tests a promise', (done) => {
+ promise
+ .then((data) => {
+ expect(data).toBe(asExpected);
+ })
+ .then(done)
+});
+
+// Bad (use done.fail in asynchronous tests)
+it('tests a promise', (done) => {
+ promise
+ .then((data) => {
+ expect(data).toBe(asExpected);
+ })
+ .then(done)
+ .catch(fail)
+});
+
+// Bad (missing catch)
+it('tests a promise rejection', (done) => {
+ promise
+ .catch((error) => {
+ expect(error).toBe(expectedError);
+ })
+ .then(done)
+});
+```
+
+#### Stubbing
+
+For unit tests, you should stub methods that are unrelated to the current unit you are testing.
+If you need to use a prototype method, instantiate an instance of the class and call it there instead of mocking the instance completely.
+
+For integration tests, you should stub methods that will effect the stability of the test if they
+execute their original behaviour. i.e. Network requests.
### Vue.js unit tests
See this [section][vue-test].
diff --git a/doc/development/i18n_guide.md b/doc/development/i18n_guide.md
new file mode 100644
index 00000000000..44eca68aaca
--- /dev/null
+++ b/doc/development/i18n_guide.md
@@ -0,0 +1,239 @@
+# Internationalization for GitLab
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10669) in GitLab 9.2.
+
+For working with internationalization (i18n) we use
+[GNU gettext](https://www.gnu.org/software/gettext/) given it's the most used
+tool for this task and we have a lot of applications that will help us to work
+with it.
+
+## Tools
+
+We use a couple of gems:
+
+1. [`gettext_i18n_rails`](https://github.com/grosser/gettext_i18n_rails): this
+ gem allow us to translate content from models, views and controllers. Also
+ it gives us access to the following raketasks:
+ - `rake gettext:find`: Parses almost all the files from the
+ Rails application looking for content that has been marked for
+ translation. Finally, it updates the PO files with the new content that
+ it has found.
+ - `rake gettext:pack`: Processes the PO files and generates the
+ MO files that are binary and are finally used by the application.
+
+1. [`gettext_i18n_rails_js`](https://github.com/webhippie/gettext_i18n_rails_js):
+ this gem is useful to make the translations available in JavaScript. It
+ provides the following raketask:
+ - `rake gettext:po_to_json`: Reads the contents from the PO files and
+ generates JSON files containing all the available translations.
+
+1. PO editor: there are multiple applications that can help us to work with PO
+ files, a good option is [Poedit](https://poedit.net/download) which is
+ available for macOS, GNU/Linux and Windows.
+
+## Preparing a page for translation
+
+We basically have 4 types of files:
+
+1. Ruby files: basically Models and Controllers.
+1. HAML files: these are the view files.
+1. ERB files: used for email templates.
+1. JavaScript files: we mostly need to work with VUE JS templates.
+
+### Ruby files
+
+If there is a method or variable that works with a raw string, for instance:
+
+```ruby
+def hello
+ "Hello world!"
+end
+```
+
+Or:
+
+```ruby
+hello = "Hello world!"
+```
+
+You can easily mark that content for translation with:
+
+```ruby
+def hello
+ _("Hello world!")
+end
+```
+
+Or:
+
+```ruby
+hello = _("Hello world!")
+```
+
+### HAML files
+
+Given the following content in HAML:
+
+```haml
+%h1 Hello world!
+```
+
+You can mark that content for translation with:
+
+```haml
+%h1= _("Hello world!")
+```
+
+### ERB files
+
+Given the following content in ERB:
+
+```erb
+<h1>Hello world!</h1>
+```
+
+You can mark that content for translation with:
+
+```erb
+<h1><%= _("Hello world!") %></h1>
+```
+
+### JavaScript files
+
+In JavaScript we added the `__()` (double underscore parenthesis) function
+for translations.
+
+### Updating the PO files with the new content
+
+Now that the new content is marked for translation, we need to update the PO
+files with the following command:
+
+```sh
+bundle exec rake gettext:find
+```
+
+This command will update the `locale/**/gitlab.edit.po` file with the
+new content that the parser has found.
+
+New translations will be added with their default content and will be marked
+fuzzy. To use the translation, look for the `#, fuzzy` mention in `gitlab.edit.po`
+and remove it.
+
+Translations that aren't used in the source code anymore will be marked with
+`~#`; these can be removed to keep our translation files clutter-free.
+
+## Working with special content
+
+### Interpolation
+
+- In Ruby/HAML:
+
+ ```ruby
+ _("Hello %{name}") % { name: 'Joe' }
+ ```
+
+- In JavaScript: Not supported at this moment.
+
+### Plurals
+
+- In Ruby/HAML:
+
+ ```ruby
+ n_('Apple', 'Apples', 3) => 'Apples'
+ ```
+
+ Using interpolation:
+ ```ruby
+ n_("There is a mouse.", "There are %d mice.", size) % size
+ ```
+
+- In JavaScript:
+
+ ```js
+ n__('Apple', 'Apples', 3) => 'Apples'
+ ```
+
+ Using interpolation:
+
+ ```js
+ n__('Last day', 'Last %d days', 30) => 'Last 30 days'
+ ```
+
+### Namespaces
+
+Sometimes you need to add some context to the text that you want to translate
+(if the word occurs in a sentence and/or the word is ambiguous).
+
+- In Ruby/HAML:
+
+ ```ruby
+ s_('OpenedNDaysAgo|Opened')
+ ```
+
+ In case the translation is not found it will return `Opened`.
+
+- In JavaScript:
+
+ ```js
+ s__('OpenedNDaysAgo|Opened')
+ ```
+
+### Just marking content for parsing
+
+Sometimes there are some dynamic translations that can't be found by the
+parser when running `bundle exec rake gettext:find`. For these scenarios you can
+use the [`_N` method](https://github.com/grosser/gettext_i18n_rails/blob/c09e38d481e0899ca7d3fc01786834fa8e7aab97/Readme.md#unfound-translations-with-rake-gettextfind).
+
+There is also and alternative method to [translate messages from validation errors](https://github.com/grosser/gettext_i18n_rails/blob/c09e38d481e0899ca7d3fc01786834fa8e7aab97/Readme.md#option-a).
+
+## Adding a new language
+
+Let's suppose you want to add translations for a new language, let's say French.
+
+1. The first step is to register the new language in `lib/gitlab/i18n.rb`:
+
+ ```ruby
+ ...
+ AVAILABLE_LANGUAGES = {
+ ...,
+ 'fr' => 'Français'
+ }.freeze
+ ...
+ ```
+
+1. Next, you need to add the language:
+
+ ```sh
+ bundle exec rake gettext:add_language[fr]
+ ```
+
+ If you want to add a new language for a specific region, the command is similar,
+ you just need to separate the region with an underscore (`_`). For example:
+
+ ```sh
+ bundle exec rake gettext:add_language[en_gb]
+ ```
+
+1. Now that the language is added, a new directory has been created under the
+ path: `locale/fr/`. You can now start using your PO editor to edit the PO file
+ located in: `locale/fr/gitlab.edit.po`.
+
+1. After you're done updating the translations, you need to process the PO files
+ in order to generate the binary MO files and finally update the JSON files
+ containing the translations:
+
+ ```sh
+ bundle exec rake gettext:pack
+ bundle exec rake gettext:po_to_json
+ ```
+
+1. In order to see the translated content we need to change our preferred language
+ which can be found under the user's **Settings** (`/profile`).
+
+1. After checking that the changes are ok, you can proceed to commit the new files.
+ For example:
+
+ ```sh
+ git add locale/fr/ app/assets/javascripts/locale/fr/
+ git commit -m "Add French translations for Cycle Analytics page"
+ ```
diff --git a/doc/development/ux_guide/basics.md b/doc/development/ux_guide/basics.md
index 259b214bd59..a436e9b1948 100644
--- a/doc/development/ux_guide/basics.md
+++ b/doc/development/ux_guide/basics.md
@@ -22,7 +22,7 @@ GitLab's main typeface used throughout the UI is **Source Sans Pro**. We support
### Monospace typeface
-This is the typeface used for code blocks. GitLab uses the OS default font.
+This is the typeface used for code blocks and references to commits, branches, and tags (`.commit-sha` or `.ref-name`). GitLab uses the OS default font.
- **Menlo** (Mac)
- **Consolas** (Windows)
- **Liberation Mono** (Linux)
diff --git a/doc/development/what_requires_downtime.md b/doc/development/what_requires_downtime.md
index 8da6ad684f5..c4830322fa8 100644
--- a/doc/development/what_requires_downtime.md
+++ b/doc/development/what_requires_downtime.md
@@ -139,6 +139,8 @@ Adding or removing a NOT NULL clause (or another constraint) can typically be
done without requiring downtime. However, this does require that any application
changes are deployed _first_. Thus, changing the constraints of a column should
happen in a post-deployment migration.
+NOTE: Avoid using `change_column` as it produces inefficient query because it re-defines
+the whole column type. For example, to add a NOT NULL constraint, prefer `change_column_null `
## Changing Column Types
diff --git a/doc/development/writing_documentation.md b/doc/development/writing_documentation.md
index 2814c18e0b6..eac9ec2a470 100644
--- a/doc/development/writing_documentation.md
+++ b/doc/development/writing_documentation.md
@@ -52,11 +52,13 @@ Every **Technical Article** contains, in the very beginning, a blockquote with t
- A reference to the **type of article** (user guide, admin guide, tech overview, tutorial)
- A reference to the **knowledge level** expected from the reader to be able to follow through (beginner, intermediate, advanced)
- A reference to the **author's name** and **GitLab.com handle**
+- A reference of the **publication date**
```md
-> **Type:** tutorial ||
+> **Article [Type](../../development/writing_documentation.html#types-of-technical-articles):** tutorial ||
> **Level:** intermediary ||
-> **Author:** [Name Surname](https://gitlab.com/username)
+> **Author:** [Name Surname](https://gitlab.com/username) ||
+> **Publication date:** AAAA/MM/DD
```
#### Technical Articles - Writing Method
@@ -76,14 +78,21 @@ Currently GitLab docs use Redcarpet as [markdown](../user/markdown.md) engine, b
We try to treat documentation as code, thus have implemented some testing.
Currently, the following tests are in place:
-1. `docs:check:links`: Check that all internal (relative) links work correctly
-1. `docs:check:apilint`: Check that the API docs follow some conventions
+1. `docs lint`: Check that all internal (relative) links work correctly and
+ that all cURL examples in API docs use the full switches.
If your contribution contains **only** documentation changes, you can speed up
-the CI process by prepending to the name of your branch: `docs/`. For example,
-a valid name would be `docs/update-api-issues` and it will run only the docs
-tests. If the name is `docs-update-api-issues`, the whole test suite will run
-(including docs).
+the CI process by following some branch naming conventions. You have three
+choices:
+
+| Branch name | Valid example |
+| ----------- | ------------- |
+| Starting with `docs/` | `docs/update-api-issues` |
+| Starting with `docs-` | `docs-update-api-issues` |
+| Ending in `-docs` | `123-update-api-issues-docs` |
+
+If your branch name matches any of the above, it will run only the docs
+tests. If it doesn't, the whole test suite will run (including docs).
---
diff --git a/doc/install/README.md b/doc/install/README.md
index 3bf7923a9ee..bc831a37735 100644
--- a/doc/install/README.md
+++ b/doc/install/README.md
@@ -18,8 +18,6 @@ the hardware requirements.
Useful for unsupported systems like *BSD. For an overview of the directory
structure, read the [structure documentation](structure.md).
- [Docker](https://docs.gitlab.com/omnibus/docker/) - Install GitLab using Docker.
-- [Installation on Google Cloud Platform](google_cloud_platform/index.md) - Install
- GitLab on Google Cloud Platform using our official image.
- [Installing in Kubernetes](kubernetes/index.md) - Install GitLab into a Kubernetes
Cluster using our official Helm Chart Repository.
- Testing only! [DigitalOcean and Docker Machine](digitaloceandocker.md) -
diff --git a/doc/install/google_cloud_platform/index.md b/doc/install/google_cloud_platform/index.md
index 26506111548..35220119e9b 100644
--- a/doc/install/google_cloud_platform/index.md
+++ b/doc/install/google_cloud_platform/index.md
@@ -2,6 +2,10 @@
![GCP landing page](img/gcp_landing.png)
+>**Important note:**
+GitLab has no official images in Google Cloud Platform yet. This guide serves
+as a template for when the GitLab VM will be available.
+
The fastest way to get started on [Google Cloud Platform (GCP)][gcp] is through
the [Google Cloud Launcher][launcher] program.
diff --git a/doc/install/installation.md b/doc/install/installation.md
index 5615b2a534b..5bba405f159 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -109,14 +109,19 @@ Then select 'Internet Site' and press enter to confirm the hostname.
## 2. Ruby
-**Note:** The current supported Ruby version is 2.3.x. GitLab 9.0 dropped support
-for Ruby 2.1.x.
+The Ruby interpreter is required to run GitLab.
+
+**Note:** The current supported Ruby (MRI) version is 2.3.x. GitLab 9.0 dropped
+support for Ruby 2.1.x.
The use of Ruby version managers such as [RVM], [rbenv] or [chruby] with GitLab
in production, frequently leads to hard to diagnose problems. For example,
GitLab Shell is called from OpenSSH, and having a version manager can prevent
pushing and pulling over SSH. Version managers are not supported and we strongly
-advise everyone to follow the instructions below to use a system Ruby.
+advise everyone to follow the instructions below to use a system Ruby.
+
+Linux distributions generally have older versions of Ruby available, so these
+instructions are designed to install Ruby from the official source code.
Remove the old Ruby 1.8 if present:
@@ -132,7 +137,7 @@ Download Ruby and compile it:
make
sudo make install
-Install the Bundler Gem:
+Then install the Bundler Gem:
sudo gem install bundler --no-ri --no-rdoc
diff --git a/doc/install/kubernetes/gitlab_chart.md b/doc/install/kubernetes/gitlab_chart.md
index 35d395af024..2d7edbe16e4 100644
--- a/doc/install/kubernetes/gitlab_chart.md
+++ b/doc/install/kubernetes/gitlab_chart.md
@@ -392,7 +392,7 @@ Once you [have configured](#configuration) GitLab in your `values.yml` file,
run the following:
```bash
-helm install --namepace <NAMEPACE> --name gitlab -f <CONFIG_VALUES_FILE> gitlab/gitlab
+helm install --namespace <NAMESPACE> --name gitlab -f <CONFIG_VALUES_FILE> gitlab/gitlab
```
where:
@@ -407,7 +407,7 @@ Once your GitLab Chart is installed, configuration changes and chart updates
should we done using `helm upgrade`
```bash
-helm upgrade --namepace <NAMEPACE> -f <CONFIG_VALUES_FILE> <RELEASE-NAME> gitlab/gitlab
+helm upgrade --namespace <NAMESPACE> -f <CONFIG_VALUES_FILE> <RELEASE-NAME> gitlab/gitlab
```
where:
diff --git a/doc/install/requirements.md b/doc/install/requirements.md
index 35586091f74..2e456557d77 100644
--- a/doc/install/requirements.md
+++ b/doc/install/requirements.md
@@ -122,15 +122,25 @@ To change the Unicorn workers when you have the Omnibus package please see [the
We currently support the following databases:
-- PostgreSQL (recommended)
+- PostgreSQL
- MySQL/MariaDB
-If you want to run the database separately, expect a size of about 1 MB per user.
+We _highly_ recommend the use of PostgreSQL instead of MySQL/MariaDB as not all
+features of GitLab may work with MySQL/MariaDB. For example, MySQL does not have
+the right features to support nested groups in an efficient manner; see
+<https://gitlab.com/gitlab-org/gitlab-ce/issues/30472> for more information
+about this. Existing users using GitLab with MySQL/MariaDB are advised to
+migrate to PostgreSQL instead.
+
+The server running the database should have _at least_ 5-10 GB of storage
+available, though the exact requirements depend on the size of the GitLab
+installation (e.g. the number of users, projects, etc).
### PostgreSQL Requirements
-As of GitLab 9.0, PostgreSQL 9.6 is recommended. Lower versions of PostgreSQL
-may work but primary testing and developement takes place using PostgreSQL 9.6.
+As of GitLab 9.0, PostgreSQL 9.2 or newer is required, and earlier versions are
+not supported. We highly recommend users to use at least PostgreSQL 9.6 as this
+is the PostgreSQL version used for development and testing.
Users using PostgreSQL must ensure the `pg_trgm` extension is loaded into every
GitLab database. This extension can be enabled (using a PostgreSQL super user)
@@ -165,4 +175,4 @@ about it, check the [Prometheus documentation](../administration/monitoring/prom
We support the current and the previous major release of Firefox, Chrome/Chromium, Safari and Microsoft browsers (Microsoft Edge and Internet Explorer 11).
-Each time a new browser version is released, we begin supporting that version and stop supporting the third most recent version. \ No newline at end of file
+Each time a new browser version is released, we begin supporting that version and stop supporting the third most recent version.
diff --git a/doc/integration/github.md b/doc/integration/github.md
index 4b0d33334bd..de9aedbc596 100644
--- a/doc/integration/github.md
+++ b/doc/integration/github.md
@@ -103,12 +103,54 @@ GitHub will generate an application ID and secret key for you to use.
1. Save the configuration file.
-1. [Reconfigure][] or [restart GitLab][] for the changes to take effect if you
+1. [Reconfigure GitLab][] or [restart GitLab][] for the changes to take effect if you
installed GitLab via Omnibus or from source respectively.
On the sign in page there should now be a GitHub icon below the regular sign in form.
Click the icon to begin the authentication process. GitHub will ask the user to sign in and authorize the GitLab application.
If everything goes well the user will be returned to GitLab and will be signed in.
-[reconfigure]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure
+### GitHub Enterprise with Self-Signed Certificate
+
+If you are attempting to import projects from GitHub Enterprise with a self-signed
+certificate and the imports are failing, you will need to disable SSL verification.
+It should be disabled by adding `verify_ssl` to `false` to the provider configuration.
+
+For omnibus package:
+
+```ruby
+ gitlab_rails['omniauth_providers'] = [
+ {
+ "name" => "github",
+ "app_id" => "YOUR_APP_ID",
+ "app_secret" => "YOUR_APP_SECRET",
+ "url" => "https://github.com/",
+ "verify_ssl" => false,
+ "args" => { "scope" => "user:email" }
+ }
+ ]
+```
+
+For installation from source:
+
+```
+ - { name: 'github', app_id: 'YOUR_APP_ID',
+ app_secret: 'YOUR_APP_SECRET',
+ url: "https://github.example.com/",
+ verify_ssl: false,
+ args: { scope: 'user:email' } }
+```
+
+
+For the changes to take effect, [reconfigure Gitlab] if you installed
+via Omnibus, or [restart GitLab] if you installed from source.
+
+You will also need to disable Git SSL verification on the server hosting GitLab with the following command:
+
+```
+$ git config --global http.sslVerify false
+```
+[reconfigure GitLab]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure
[restart GitLab]: ../administration/restart_gitlab.md#installations-from-source
+
+
diff --git a/doc/system_hooks/system_hooks.md b/doc/system_hooks/system_hooks.md
index ad5ffc84473..583ec5522fd 100644
--- a/doc/system_hooks/system_hooks.md
+++ b/doc/system_hooks/system_hooks.md
@@ -266,7 +266,8 @@ X-Gitlab-Event: System Hook
## Push events
-Triggered when you push to the repository except when pushing tags.
+Triggered when you push to the repository, except when pushing tags.
+It generates one event per modified branch.
**Request header**:
@@ -332,6 +333,7 @@ X-Gitlab-Event: System Hook
## Tag events
Triggered when you create (or delete) tags to the repository.
+It generates one event per modified tag.
**Request header**:
@@ -381,3 +383,49 @@ X-Gitlab-Event: System Hook
"total_commits_count": 0
}
```
+## Repository Update events
+
+Triggered only once when you push to the repository (including tags).
+
+**Request header**:
+
+```
+X-Gitlab-Event: System Hook
+```
+
+**Request body:**
+
+```json
+{
+ "event_name": "repository_update",
+ "user_id": 1,
+ "user_name": "John Smith",
+ "user_email": "admin@example.com",
+ "user_avatar": "https://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=8://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=80",
+ "project_id": 1,
+ "project": {
+ "name":"Example",
+ "description":"",
+ "web_url":"http://example.com/jsmith/example",
+ "avatar_url":null,
+ "git_ssh_url":"git@example.com:jsmith/example.git",
+ "git_http_url":"http://example.com/jsmith/example.git",
+ "namespace":"Jsmith",
+ "visibility_level":0,
+ "path_with_namespace":"jsmith/example",
+ "default_branch":"master",
+ "homepage":"http://example.com/jsmith/example",
+ "url":"git@example.com:jsmith/example.git",
+ "ssh_url":"git@example.com:jsmith/example.git",
+ "http_url":"http://example.com/jsmith/example.git",
+ },
+ "changes": [
+ {
+ "before":"8205ea8d81ce0c6b90fbe8280d118cc9fdad6130",
+ "after":"4045ea7a3df38697b3730a20fb73c8bed8a3e69e",
+ "ref":"refs/heads/master"
+ }
+ ],
+ "refs":["refs/heads/master"]
+}
+```
diff --git a/doc/topics/authentication/index.md b/doc/topics/authentication/index.md
index 3e756d96ed2..0c0d482499a 100644
--- a/doc/topics/authentication/index.md
+++ b/doc/topics/authentication/index.md
@@ -19,7 +19,7 @@ This page gathers all the resources for the topic **Authentication** within GitL
- [Enforce Two-factor Authentication (2FA)](../../security/two_factor_authentication.md#enforce-two-factor-authentication-2fa)
- **Articles:**
- [How to Configure LDAP with GitLab CE](../../articles/how_to_configure_ldap_gitlab_ce/index.md)
- - [How to Configure LDAP with GitLab EE](https://docs.gitlab.com/articles/how_to_configure_ldap_gitlab_ee/)
+ - [How to Configure LDAP with GitLab EE](https://docs.gitlab.com/ee/articles/how_to_configure_ldap_gitlab_ee/)
- [Feature Highlight: LDAP Integration](https://about.gitlab.com/2014/07/10/feature-highlight-ldap-sync/)
- [Debugging LDAP](https://about.gitlab.com/handbook/support/workflows/ldap/debugging_ldap.html)
- **Integrations:**
diff --git a/doc/topics/git/index.md b/doc/topics/git/index.md
index d13066c9015..604f9375714 100644
--- a/doc/topics/git/index.md
+++ b/doc/topics/git/index.md
@@ -22,6 +22,7 @@ We've gathered some resources to help you to get the best from Git with GitLab.
- [Cherry-picking a commit](../../user/project/merge_requests/cherry_pick_changes.md#cherry-picking-a-commit)
- [Squashing commits](../../workflow/gitlab_flow.md#squashing-commits-with-rebase)
- **Articles:**
+ - [How to install Git](../../articles/how_to_install_git/index.md)
- [Git Tips & Tricks](https://about.gitlab.com/2016/12/08/git-tips-and-tricks/)
- [Eight Tips to help you work better with Git](https://about.gitlab.com/2015/02/19/8-tips-to-help-you-work-better-with-git/)
- **Presentations:**
diff --git a/doc/university/high-availability/aws/README.md b/doc/university/high-availability/aws/README.md
index 088f1cd7290..6b8f3cd3d1d 100644
--- a/doc/university/high-availability/aws/README.md
+++ b/doc/university/high-availability/aws/README.md
@@ -159,19 +159,21 @@ subnet and security group and
***
-## Elastic File System
+## Network File System
-This new AWS offering allows us to create a file system accessible by

-EC2 instances within a VPC. Choose our VPC and the subnets will be
-
automatically configured assuming we don't need to set explicit IPs.
-The
next section allows us to add tags and choose between General
-Purpose or
Max I/O which is a good option when being accessed by a
-large number of
EC2 instances.
+GitLab requires a shared filesystem such as NFS. The file share(s) will be
+mounted on all application servers. There are a variety of ways to build an
+NFS server on AWS.
-

![Elastic File System](img/elastic-file-system.png)
+One option is to use a third-party AMI that offers NFS as a service. A [search
+for 'NFS' in the AWS Marketplace](https://aws.amazon.com/marketplace/search/results?x=0&y=0&searchTerms=NFS&page=1&ref_=nav_search_box)
+shows options such as NetApp, SoftNAS and others.
-To actually mount and install the NFS client we'll use the User Data
-section when adding our Launch Configuration.
+Another option is to build a simple NFS server using a vanilla Linux server backed
+by AWS Elastic Block Storage (EBS).
+
+> **Note:** GitLab does not recommend using AWS Elastic File System (EFS). See
+ details in [High Availability NFS documentation](../../../administration/high_availability/nfs.md#aws-elastic-file-system)
***
diff --git a/doc/user/admin_area/settings/usage_statistics.md b/doc/user/admin_area/settings/usage_statistics.md
index 733e70ca9bf..375e7f08e8b 100644
--- a/doc/user/admin_area/settings/usage_statistics.md
+++ b/doc/user/admin_area/settings/usage_statistics.md
@@ -28,60 +28,13 @@ for all signed in users.
[were added][ee-735] in GitLab Enterprise Edition
8.12. [Moved to GitLab Community Edition][ce-23361] in 9.1.
-GitLab Inc. can collect non-sensitive information about how GitLab users
-use their GitLab instance upon the activation of a ping feature
-located in the admin panel (`/admin/application_settings`).
-
-You can see the **exact** JSON payload that your instance sends to GitLab
-in the "Usage statistics" section of the admin panel.
-
-Nothing qualitative is collected. Only quantitative. That means no project
-names, author names, comment bodies, names of labels, etc.
-
-The usage ping is sent in order for GitLab Inc. to have a better understanding
-of how our users use our product, and to be more data-driven when creating or
-changing features.
-
-The total number of the following is sent back to GitLab Inc.:
-
-- Comments
-- Groups
-- Users
-- Projects
-- Issues
-- Labels
-- CI builds
-- Snippets
-- Milestones
-- Todos
-- Pushes
-- Merge requests
-- Environments
-- Triggers
-- Deploy keys
-- Pages
-- Project Services
-- Projects using the Prometheus service
-- Issue Boards
-- CI Runners
-- Deployments
-- Geo Nodes
-- LDAP Groups
-- LDAP Keys
-- LDAP Users
-- LFS objects
-- Protected branches
-- Releases
-- Remote mirrors
-- Uploads
-- Web hooks
-
-Also, we track if you've installed Mattermost with GitLab.
-For example: `"mattermost_enabled":true"`.
-
-More data will be added over time. The goal of this ping is to be as light as
-possible, so it won't have any performance impact on your installation when
-the calculation is made.
+GitLab sends a weekly payload containing usage data to GitLab Inc. The usage
+ping uses high-level data to help our product, support, and sales teams. It does
+not send any project names, usernames, or any other specific data. The
+information from the usage ping is not anonymous, it is linked to the hostname
+of the instance.
+
+You can view the exact JSON payload in the administration panel.
### Deactivate the usage ping
@@ -89,13 +42,23 @@ By default, usage ping is opt-out. If you want to deactivate this feature, go to
the Settings page of your administration panel and uncheck the Usage ping
checkbox.
-## Privacy policy
+To disable the usage ping and prevent it from being configured in future through
+the administration panel, Omnibus installs can set the following in
+[`gitlab.rb`](https://docs.gitlab.com/omnibus/settings/configuration.html#configuration-options):
+
+```ruby
+gitlab_rails['usage_ping_enabled'] = false
+```
-GitLab Inc. does **not** collect any sensitive information, like project names
-or the content of the comments. GitLab Inc. does not disclose or otherwise make
-available any of the data collected on a customer specific basis.
+And source installs can set the following in `gitlab.yml`:
-Read more about this in the [Privacy policy](https://about.gitlab.com/privacy).
+```yaml
+production: &base
+ # ...
+ gitlab:
+ # ...
+ usage_ping_enabled: false
+```
[ee-557]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/557
[ee-735]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/735
diff --git a/doc/user/group/subgroups/index.md b/doc/user/group/subgroups/index.md
index ce5da07c61a..a4726673fc4 100644
--- a/doc/user/group/subgroups/index.md
+++ b/doc/user/group/subgroups/index.md
@@ -71,8 +71,10 @@ structure.
- You need to be an Owner of a group in order to be able to create
a subgroup. For more information check the [permissions table][permissions].
- For a list of words that are not allowed to be used as group names see the
- [`namespace_validator.rb` file][reserved] under the `RESERVED` and
- `WILDCARD_ROUTES` lists.
+ [`dynamic_path_validator.rb` file][reserved] under the `TOP_LEVEL_ROUTES`, `WILDCARD_ROUTES` and `GROUP_ROUTES` lists:
+ - `TOP_LEVEL_ROUTES`: are names that are reserved as usernames or top level groups
+ - `WILDCARD_ROUTES`: are names that are reserved for child groups or projects.
+ - `GROUP_ROUTES`: are names that are reserved for all groups or projects.
To create a subgroup:
@@ -161,4 +163,4 @@ Here's a list of what you can't do with subgroups:
[ce-2772]: https://gitlab.com/gitlab-org/gitlab-ce/issues/2772
[permissions]: ../../permissions.md#group
-[reserved]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/app/validators/namespace_validator.rb
+[reserved]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/app/validators/dynamic_path_validator.rb
diff --git a/doc/user/project/integrations/img/merge_request_performance.png b/doc/user/project/integrations/img/merge_request_performance.png
new file mode 100644
index 00000000000..93b2626fed7
--- /dev/null
+++ b/doc/user/project/integrations/img/merge_request_performance.png
Binary files differ
diff --git a/doc/user/project/integrations/prometheus.md b/doc/user/project/integrations/prometheus.md
index b71d6981d1e..7aa6d070a1b 100644
--- a/doc/user/project/integrations/prometheus.md
+++ b/doc/user/project/integrations/prometheus.md
@@ -171,11 +171,15 @@ after initial deployment.
## Determining performance impact of a merge
-> [Introduced][ce-10408] in GitLab 9.1.
+> [Introduced][ce-10408] in GitLab 9.2.
-After a merge request has been approved, a sparkline will appear on the merge request page displaying the average memory usage of the application. The sparkline includes thirty minutes of data prior to the merge, a dot to indicate the merge itself, and then will begin capturing thirty minutes of data after the merge.
+Developers can view the performance impact of their changes within the merge request workflow. When a source branch has been deployed to an environment, a sparkline will appear showing the average memory consumption of the app. The dot indicates when the current changes were deployed, with up to 30 minutes of performance data displayed before and after. The sparkline will be updated after each commit has been deployed.
-This sparkline serves as a quick indicator of the impact on memory consumption of the recently merged changes. If there is a problem, action can then be taken to troubleshoot or revert the merge.
+Once merged and the target branch has been redeployed, the sparkline will switch to show the new environments this revision has been deployed to.
+
+Performance data will be available for the duration it is persisted on the Prometheus server.
+
+![Merge Request with Performance Impact](img/merge_request_performance.png)
## Troubleshooting
diff --git a/doc/user/project/integrations/webhooks.md b/doc/user/project/integrations/webhooks.md
index e15daa2feae..48d49c5d40c 100644
--- a/doc/user/project/integrations/webhooks.md
+++ b/doc/user/project/integrations/webhooks.md
@@ -74,6 +74,7 @@ X-Gitlab-Event: Push Hook
"checkout_sha": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
"user_id": 4,
"user_name": "John Smith",
+ "user_username": "jsmith",
"user_email": "john@example.com",
"user_avatar": "https://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=8://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=80",
"project_id": 15,
diff --git a/doc/user/project/issues/img/create_new_merge_request.png b/doc/user/project/issues/img/create_new_merge_request.png
new file mode 100644
index 00000000000..d4bfb6fa463
--- /dev/null
+++ b/doc/user/project/issues/img/create_new_merge_request.png
Binary files differ
diff --git a/doc/user/project/issues/issues_functionalities.md b/doc/user/project/issues/issues_functionalities.md
index 1efd07a058b..e1923e91042 100644
--- a/doc/user/project/issues/issues_functionalities.md
+++ b/doc/user/project/issues/issues_functionalities.md
@@ -16,6 +16,9 @@ An issue starts with its status (open or closed), followed by its author,
and includes many other functionalities, numbered on the image above to
explain what they mean, one by one.
+Many of the elements of the issue screen refresh automatically, such as the title and description, when they are changed by another user.
+Comments and system notes also appear automatically in response to various actions and content updates.
+
#### 1. New Issue, close issue, edit
- New issue: create a new issue in the same project
@@ -155,3 +158,9 @@ Once you wrote your comment, you can either:
- [New branch](../repository/web_editor.md#create-a-new-branch-from-an-issue):
create a new branch, followed by a new merge request which will automatically close that
issue as soon as that merge request is merged.
+
+#### 19. New merge request
+
+- Create a new merge request (with source branch) in one action. Optionally just create a new branch, as explained above.
+
+![Create new merge request](img/create_new_merge_request.png)
diff --git a/doc/user/project/new_ci_build_permissions_model.md b/doc/user/project/new_ci_build_permissions_model.md
index f846736028f..051a28efea6 100644
--- a/doc/user/project/new_ci_build_permissions_model.md
+++ b/doc/user/project/new_ci_build_permissions_model.md
@@ -89,7 +89,7 @@ to steal the tokens of other jobs.
## Pipeline triggers
-Since 9.0 [pipelnie triggers][triggers] do support the new permission model.
+Since 9.0 [pipeline triggers][triggers] do support the new permission model.
The new triggers do impersonate their associated user including their access
to projects and their project permissions. To migrate trigger to use new permisison
model use **Take ownership**.
diff --git a/doc/user/project/pages/getting_started_part_four.md b/doc/user/project/pages/getting_started_part_four.md
index 50767095aa0..bd0cb437924 100644
--- a/doc/user/project/pages/getting_started_part_four.md
+++ b/doc/user/project/pages/getting_started_part_four.md
@@ -1,8 +1,9 @@
# GitLab Pages from A to Z: Part 4
-> **Type**: user guide ||
+> **Article [Type](../../../development/writing_documentation.html#types-of-technical-articles)**: user guide ||
> **Level**: intermediate ||
-> **Author**: [Marcia Ramos](https://gitlab.com/marcia)
+> **Author**: [Marcia Ramos](https://gitlab.com/marcia) ||
+> **Publication date:** 2017/02/22
- [Part 1: Static sites and GitLab Pages domains](getting_started_part_one.md)
- [Part 2: Quick start guide - Setting up GitLab Pages](getting_started_part_two.md)
diff --git a/doc/user/project/pages/getting_started_part_one.md b/doc/user/project/pages/getting_started_part_one.md
index e92549aa0df..2f104c7becc 100644
--- a/doc/user/project/pages/getting_started_part_one.md
+++ b/doc/user/project/pages/getting_started_part_one.md
@@ -1,15 +1,16 @@
# GitLab Pages from A to Z: Part 1
-> **Type**: user guide ||
+> **Article [Type](../../../development/writing_documentation.html#types-of-technical-articles)**: user guide ||
> **Level**: beginner ||
-> **Author**: [Marcia Ramos](https://gitlab.com/marcia)
+> **Author**: [Marcia Ramos](https://gitlab.com/marcia) ||
+> **Publication date:** 2017/02/22
- **Part 1: Static sites and GitLab Pages domains**
- [Part 2: Quick start guide - Setting up GitLab Pages](getting_started_part_two.md)
- [Part 3: Setting Up Custom Domains - DNS Records and SSL/TLS Certificates](getting_started_part_three.md)
- [Part 4: Creating and tweaking `.gitlab-ci.yml` for GitLab Pages](getting_started_part_four.md)
-## GitLab Pages form A to Z
+## GitLab Pages from A to Z
This is a comprehensive guide, made for those who want to
publish a website with GitLab Pages but aren't familiar with
diff --git a/doc/user/project/pages/getting_started_part_three.md b/doc/user/project/pages/getting_started_part_three.md
index 80f16e43e20..53fd1786cfa 100644
--- a/doc/user/project/pages/getting_started_part_three.md
+++ b/doc/user/project/pages/getting_started_part_three.md
@@ -1,8 +1,9 @@
# GitLab Pages from A to Z: Part 3
-> **Type**: user guide ||
+> **Article [Type](../../../development/writing_documentation.html#types-of-technical-articles)**: user guide ||
> **Level**: beginner ||
-> **Author**: [Marcia Ramos](https://gitlab.com/marcia)
+> **Author**: [Marcia Ramos](https://gitlab.com/marcia) ||
+> **Publication date:** 2017/02/22
- [Part 1: Static sites and GitLab Pages domains](getting_started_part_one.md)
- [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
index 578ad13f5df..c91e2d8c261 100644
--- a/doc/user/project/pages/getting_started_part_two.md
+++ b/doc/user/project/pages/getting_started_part_two.md
@@ -1,8 +1,9 @@
# GitLab Pages from A to Z: Part 2
-> **Type**: user guide ||
+> **Article [Type](../../../development/writing_documentation.html#types-of-technical-articles)**: user guide ||
> **Level**: beginner ||
-> **Author**: [Marcia Ramos](https://gitlab.com/marcia)
+> **Author**: [Marcia Ramos](https://gitlab.com/marcia) ||
+> **Publication date:** 2017/02/22
- [Part 1: Static sites and GitLab Pages domains](getting_started_part_one.md)
- **Part 2: Quick start guide - Setting up GitLab Pages**
diff --git a/doc/user/project/pipelines/img/pipeline_schedules_list.png b/doc/user/project/pipelines/img/pipeline_schedules_list.png
new file mode 100644
index 00000000000..50d9d184b05
--- /dev/null
+++ b/doc/user/project/pipelines/img/pipeline_schedules_list.png
Binary files differ
diff --git a/doc/ci/img/pipeline_schedules_new_form.png b/doc/user/project/pipelines/img/pipeline_schedules_new_form.png
index ea5394fa8a6..ea5394fa8a6 100644
--- a/doc/ci/img/pipeline_schedules_new_form.png
+++ b/doc/user/project/pipelines/img/pipeline_schedules_new_form.png
Binary files differ
diff --git a/doc/user/project/pipelines/img/pipeline_schedules_ownership.png b/doc/user/project/pipelines/img/pipeline_schedules_ownership.png
new file mode 100644
index 00000000000..31ed83abb4d
--- /dev/null
+++ b/doc/user/project/pipelines/img/pipeline_schedules_ownership.png
Binary files differ
diff --git a/doc/user/project/pipelines/job_artifacts.md b/doc/user/project/pipelines/job_artifacts.md
index 5ce99843301..151ee4728ad 100644
--- a/doc/user/project/pipelines/job_artifacts.md
+++ b/doc/user/project/pipelines/job_artifacts.md
@@ -41,6 +41,10 @@ For more examples on artifacts, follow the artifacts reference in
## Browsing job artifacts
+>**Note:**
+With GitLab 9.2, PDFs, images, videos and other formats can be previewed directly
+in the job artifacts browser without the need to download them.
+
After a job finishes, if you visit the job's specific page, you can see
that there are two buttons. One is for downloading the artifacts archive and
the other for browsing its contents.
diff --git a/doc/user/project/pipelines/schedules.md b/doc/user/project/pipelines/schedules.md
new file mode 100644
index 00000000000..641876f948f
--- /dev/null
+++ b/doc/user/project/pipelines/schedules.md
@@ -0,0 +1,62 @@
+# Pipeline Schedules
+
+> **Notes**:
+- This feature was introduced in 9.1 as [Trigger Schedule][ce-10533].
+- In 9.2, the feature was [renamed to Pipeline Schedule][ce-10853].
+- Cron notation is parsed by [Rufus-Scheduler](https://github.com/jmettraux/rufus-scheduler).
+
+Pipeline schedules can be used to run pipelines only once, or for example every
+month on the 22nd for a certain branch.
+
+## Using Pipeline schedules
+
+In order to schedule a pipeline:
+
+1. Navigate to your project's **Pipelines ➔ Schedules** and click the
+ **New Schedule** button.
+1. Fill in the form
+1. Hit **Save pipeline schedule** for the changes to take effect.
+
+![New Schedule Form](img/pipeline_schedules_new_form.png)
+
+>**Attention:**
+The pipelines won't be executed precisely, because schedules are handled by
+Sidekiq, which runs according to its interval.
+See [advanced admin configuration](#advanced-admin-configuration) for more
+information.
+
+In the **Schedules** index page you can see a list of the pipelines that are
+scheduled to run. The next run is automatically calculated by the server GitLab
+is installed on.
+
+![Schedules list](img/pipeline_schedules_list.png)
+
+## Taking ownership
+
+Pipelines are executed as a user, who owns a schedule. This influences what
+projects and other resources the pipeline has access to. If a user does not own
+a pipeline, you can take ownership by clicking the **Take ownership** button.
+The next time a pipeline is scheduled, your credentials will be used.
+
+![Schedules list](img/pipeline_schedules_ownership.png)
+
+>**Note:**
+When the owner of the schedule doesn't have the ability to create pipelines
+anymore, due to e.g., being blocked or removed from the project, the schedule
+is deactivated. Another user can take ownership and activate it, so the
+schedule can be run again.
+
+## Advanced admin configuration
+
+The pipelines won't be executed precisely, because schedules are handled by
+Sidekiq, which runs according to its interval. For example, if you set a
+schedule to create a pipeline every minute (`* * * * *`) and the Sidekiq worker
+runs on 00:00 and 12:00 every day (`0 */12 * * *`), only 2 pipelines will be
+created per day. To change the Sidekiq worker's frequency, you have to edit the
+`trigger_schedule_worker_cron` value in your `gitlab.rb` and restart GitLab.
+For GitLab.com, you can check the [dedicated settings page][settings]. If you
+don't have admin access to the server, ask your administrator.
+
+[ce-10533]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10533
+[ce-10853]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10853
+[settings]: https://about.gitlab.com/gitlab-com/settings/#cron-jobs
diff --git a/doc/user/search/img/filter_issues_project.gif b/doc/user/search/img/filter_issues_project.gif
deleted file mode 100644
index d547588be5d..00000000000
--- a/doc/user/search/img/filter_issues_project.gif
+++ /dev/null
Binary files differ
diff --git a/doc/user/search/img/issue_search_filter.png b/doc/user/search/img/issue_search_filter.png
new file mode 100644
index 00000000000..f357abd6bac
--- /dev/null
+++ b/doc/user/search/img/issue_search_filter.png
Binary files differ
diff --git a/doc/user/search/index.md b/doc/user/search/index.md
index 45f443819ec..6d59dcc6c75 100644
--- a/doc/user/search/index.md
+++ b/doc/user/search/index.md
@@ -34,18 +34,22 @@ a project's **Issues** tab, and click on the field **Search or filter results...
display a dropdown menu, from which you can add filters per author, assignee, milestone, label,
and weight. When done, press **Enter** on your keyboard to filter the issues.
-![filter issues in a project](img/filter_issues_project.gif)
+![filter issues in a project](img/issue_search_filter.png)
The same process is valid for merge requests. Navigate to your project's **Merge Requests** tab,
and click **Search or filter results...**. Merge requests can be filtered by author, assignee,
milestone, and label.
-## Search History
+## Search history
You can view recent searches by clicking on the little arrow-clock icon, which is to the left of the search input. Click the search entry to run that search again. This feature is available for issues and merge requests. Searches are stored locally in your browser.
![search history](img/search_history.gif)
+## Removing search filters
+
+Individual filters can be removed by clicking on the filter's (x) button or backspacing. The entire search filter can be cleared by clicking on the search box's (x) button.
+
### Shortcut
You'll also find a shortcut on the search field on the top-right of the project's dashboard to
diff --git a/features/profile/active_tab.feature b/features/profile/active_tab.feature
index 788b7895d72..21d7d6c3800 100644
--- a/features/profile/active_tab.feature
+++ b/features/profile/active_tab.feature
@@ -23,7 +23,7 @@ Feature: Profile Active Tab
Then the active main tab should be Preferences
And no other main tabs should be active
- Scenario: On Profile Audit Log
- Given I visit Audit Log page
- Then the active main tab should be Audit Log
+ Scenario: On Profile Authentication log
+ Given I visit Authentication log page
+ Then the active main tab should be Authentication log
And no other main tabs should be active
diff --git a/features/profile/profile.feature b/features/profile/profile.feature
index 70f47c97173..3263d3e212b 100644
--- a/features/profile/profile.feature
+++ b/features/profile/profile.feature
@@ -63,7 +63,7 @@ Feature: Profile
Given I logout
And I sign in via the UI
And I have activity
- When I visit Audit Log page
+ When I visit Authentication log page
Then I should see my activity
Scenario: I visit my user page
diff --git a/features/project/project.feature b/features/project/project.feature
index aa22401c88e..23817ef3ac9 100644
--- a/features/project/project.feature
+++ b/features/project/project.feature
@@ -18,6 +18,7 @@ Feature: Project
Then I should see the default project avatar
And I should not see the "Remove avatar" button
+ @javascript
Scenario: I should have readme on page
And I visit project "Shop" page
Then I should see project "Shop" README
diff --git a/features/project/source/markdown_render.feature b/features/project/source/markdown_render.feature
index fd583618dcf..fe4466ad241 100644
--- a/features/project/source/markdown_render.feature
+++ b/features/project/source/markdown_render.feature
@@ -19,12 +19,14 @@ Feature: Project Source Markdown Render
And I click on Gitlab API in README
Then I should see correct document rendered
+ @javascript
Scenario: I view README in markdown branch
Then I should see files from repository in markdown
And I should see rendered README which contains correct links
And I click on Rake tasks in README
Then I should see correct directory rendered
+ @javascript
Scenario: I view README in markdown branch to see reference links to directory
Then I should see files from repository in markdown
And I should see rendered README which contains correct links
@@ -74,6 +76,7 @@ Feature: Project Source Markdown Render
And I click on Gitlab API in README
Then I should see correct document rendered for markdown branch
+ @javascript
Scenario: I browse directory from markdown branch
When I visit markdown branch
Then I should see files from repository in markdown branch
diff --git a/features/search.feature b/features/search.feature
index 818ef436db6..f894b6b84a1 100644
--- a/features/search.feature
+++ b/features/search.feature
@@ -9,6 +9,7 @@ Feature: Search
Given I search for "Sho"
Then I should see "Shop" project link
+ @javascript
Scenario: I should see issues I am looking for
And project has issues
When I search for "Foo"
@@ -16,6 +17,7 @@ Feature: Search
Then I should see "Foo" link in the search results
And I should not see "Bar" link in the search results
+ @javascript
Scenario: I should see merge requests I am looking for
And project has merge requests
When I search for "Foo"
@@ -23,6 +25,7 @@ Feature: Search
Then I should see "Foo" link in the search results
And I should not see "Bar" link in the search results
+ @javascript
Scenario: I should see milestones I am looking for
And project has milestones
When I search for "Foo"
@@ -78,6 +81,7 @@ Feature: Search
And I search for "Sho"
Then I should see "Shop" project link
+ @javascript
Scenario: I logout and should see issues I am looking for
Given project "Shop" is public
And I logout directly
diff --git a/features/steps/dashboard/todos.rb b/features/steps/dashboard/todos.rb
index b56558ba0d2..14c13c4818a 100644
--- a/features/steps/dashboard/todos.rb
+++ b/features/steps/dashboard/todos.rb
@@ -55,7 +55,7 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps
merge_request_reference = merge_request.to_reference(full: true)
issue_reference = issue.to_reference(full: true)
- click_link 'Mark all as done'
+ find('.js-todos-mark-all').trigger('click')
page.within('.todos-count') { expect(page).to have_content '0' }
expect(page).to have_content 'To do 0'
@@ -69,7 +69,7 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps
end
step 'I should see the todo marked as done' do
- click_link 'Done 1'
+ find('.todos-done a').trigger('click')
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, state: :done_irreversible)
@@ -79,7 +79,7 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps
merge_request_reference = merge_request.to_reference(full: true)
issue_reference = issue.to_reference(full: true)
- click_link 'Done 4'
+ find('.todos-done a').trigger('click')
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, state: :done_irreversible)
diff --git a/features/steps/explore/projects.rb b/features/steps/explore/projects.rb
index 7dc33ab5683..b2194275751 100644
--- a/features/steps/explore/projects.rb
+++ b/features/steps/explore/projects.rb
@@ -101,7 +101,7 @@ class Spinach::Features::ExploreProjects < Spinach::FeatureSteps
create(:merge_request,
title: "Bug fix for public project",
source_project: public_project,
- target_project: public_project,
+ target_project: public_project
)
end
diff --git a/features/steps/profile/active_tab.rb b/features/steps/profile/active_tab.rb
index 4724a326277..069d4e6a23d 100644
--- a/features/steps/profile/active_tab.rb
+++ b/features/steps/profile/active_tab.rb
@@ -19,7 +19,7 @@ class Spinach::Features::ProfileActiveTab < Spinach::FeatureSteps
ensure_active_main_tab('Preferences')
end
- step 'the active main tab should be Audit Log' do
- ensure_active_main_tab('Audit Log')
+ step 'the active main tab should be Authentication log' do
+ ensure_active_main_tab('Authentication log')
end
end
diff --git a/features/steps/project/builds/artifacts.rb b/features/steps/project/builds/artifacts.rb
index eec375b0532..89132ff068f 100644
--- a/features/steps/project/builds/artifacts.rb
+++ b/features/steps/project/builds/artifacts.rb
@@ -25,7 +25,7 @@ class Spinach::Features::ProjectBuildsArtifacts < Spinach::FeatureSteps
step 'I should see the build header' do
page.within('.build-header') do
- expect(page).to have_content "Job ##{@build.id} in pipeline ##{@pipeline.id} for commit #{@pipeline.short_sha}"
+ expect(page).to have_content "Job ##{@build.id} in pipeline ##{@pipeline.id} for #{@pipeline.short_sha}"
end
end
diff --git a/features/steps/project/forked_merge_requests.rb b/features/steps/project/forked_merge_requests.rb
index 310db6e6dad..29055373a57 100644
--- a/features/steps/project/forked_merge_requests.rb
+++ b/features/steps/project/forked_merge_requests.rb
@@ -5,6 +5,7 @@ class Spinach::Features::ProjectForkedMergeRequests < Spinach::FeatureSteps
include SharedPaths
include Select2Helper
include WaitForVueResource
+ include WaitForAjax
step 'I am a member of project "Shop"' do
@project = ::Project.find_by(name: "Shop")
@@ -47,6 +48,7 @@ class Spinach::Features::ProjectForkedMergeRequests < Spinach::FeatureSteps
first('.dropdown-target-project a', text: @project.path_with_namespace)
first('.js-source-branch').click
+ wait_for_ajax
first('.dropdown-source-branch .dropdown-content a', text: 'fix').click
click_button "Compare branches and continue"
@@ -62,31 +64,6 @@ class Spinach::Features::ProjectForkedMergeRequests < Spinach::FeatureSteps
click_button "Submit merge request"
end
- step 'I follow the target commit link' do
- commit = @project.repository.commit
- click_link commit.short_id(8)
- end
-
- step 'I should see the commit under the forked from project' do
- commit = @project.repository.commit
- expect(page).to have_content(commit.message)
- end
-
- step 'I click "Create Merge Request on fork" link' do
- click_link "Create Merge Request on fork"
- end
-
- step 'I see prefilled new Merge Request page for the forked project' do
- expect(current_path).to eq new_namespace_project_merge_request_path(@forked_project.namespace, @forked_project)
- expect(find("#merge_request_source_project_id").value).to eq @forked_project.id.to_s
- expect(find("#merge_request_target_project_id").value).to eq @project.id.to_s
- expect(find("#merge_request_source_branch").value).to have_content "new_design"
- expect(find("#merge_request_target_branch").value).to have_content "master"
- expect(find("#merge_request_title").value).to eq "New Design"
- verify_commit_link(".mr_target_commit", @project)
- verify_commit_link(".mr_source_commit", @forked_project)
- end
-
step 'I update the merge request title' do
fill_in "merge_request_title", with: "An Edited Forked Merge Request"
end
@@ -155,10 +132,4 @@ class Spinach::Features::ProjectForkedMergeRequests < Spinach::FeatureSteps
expect(page).to have_content @project.users.first.name
end
end
-
- # Verify a link is generated against the correct project
- def verify_commit_link(container_div, container_project)
- # This should force a wait for the javascript to execute
- expect(find(:div, container_div).find(".commit_short_id")['href']).to have_content "#{container_project.path_with_namespace}/commit"
- end
end
diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb
index d15417fa173..8133760e619 100644
--- a/features/steps/project/merge_requests.rb
+++ b/features/steps/project/merge_requests.rb
@@ -33,7 +33,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
end
step 'I click link "Merged"' do
- click_link "Merged"
+ find('#state-merged').trigger('click')
end
step 'I click link "Closed"' do
@@ -331,7 +331,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
step 'I click on the Discussion tab' do
page.within '.merge-request-tabs' do
- click_link 'Discussion'
+ find('.notes-tab').trigger('click')
end
# Waits for load
diff --git a/features/steps/project/project.rb b/features/steps/project/project.rb
index 280d70925f7..9c2196a8ef7 100644
--- a/features/steps/project/project.rb
+++ b/features/steps/project/project.rb
@@ -2,6 +2,7 @@ class Spinach::Features::Project < Spinach::FeatureSteps
include SharedAuthentication
include SharedProject
include SharedPaths
+ include WaitForAjax
step 'change project settings' do
fill_in 'project_name_edit', with: 'NewName'
@@ -86,6 +87,7 @@ class Spinach::Features::Project < Spinach::FeatureSteps
end
step 'I should see project "Shop" README' do
+ wait_for_ajax
page.within('.readme-holder') do
expect(page).to have_content 'testme'
end
diff --git a/features/steps/project/source/markdown_render.rb b/features/steps/project/source/markdown_render.rb
index abdbd795cd5..ada0ff20585 100644
--- a/features/steps/project/source/markdown_render.rb
+++ b/features/steps/project/source/markdown_render.rb
@@ -120,6 +120,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps
When 'I visit markdown branch' do
visit namespace_project_tree_path(@project.namespace, @project, "markdown")
+ wait_for_ajax
end
When 'I visit markdown branch "README.md" blob' do
diff --git a/features/steps/search.rb b/features/steps/search.rb
index f885baf8453..16c4a5ab2e4 100644
--- a/features/steps/search.rb
+++ b/features/steps/search.rb
@@ -10,12 +10,12 @@ class Spinach::Features::Search < Spinach::FeatureSteps
step 'I search for "Foo"' do
fill_in "dashboard_search", with: "Foo"
- click_button "Search"
+ find('.btn-search').trigger('click')
end
step 'I search for "rspec"' do
fill_in "dashboard_search", with: "rspec"
- click_button "Search"
+ find('.btn-search').trigger('click')
end
step 'I search for "rspec" on project page' do
@@ -25,7 +25,7 @@ class Spinach::Features::Search < Spinach::FeatureSteps
step 'I search for "Wiki content"' do
fill_in "dashboard_search", with: "content"
- click_button "Search"
+ find('.btn-search').trigger('click')
end
step 'I click "Issues" link' do
@@ -35,7 +35,7 @@ class Spinach::Features::Search < Spinach::FeatureSteps
end
step 'I click project "Shop" link' do
- click_button 'Project'
+ find('.js-search-project-dropdown').trigger('click')
page.within '.project-filter' do
click_link project.name_with_namespace
end
diff --git a/features/steps/shared/paths.rb b/features/steps/shared/paths.rb
index 46b3cb79af2..bef3eac4d26 100644
--- a/features/steps/shared/paths.rb
+++ b/features/steps/shared/paths.rb
@@ -152,7 +152,7 @@ module SharedPaths
visit profile_preferences_path
end
- step 'I visit Audit Log page' do
+ step 'I visit Authentication log page' do
visit audit_log_profile_path
end
diff --git a/features/steps/shared/project.rb b/features/steps/shared/project.rb
index 15625e045f5..c4f1c57836f 100644
--- a/features/steps/shared/project.rb
+++ b/features/steps/shared/project.rb
@@ -256,9 +256,9 @@ module SharedProject
end
step 'I should see last commit with CI status' do
- page.within ".project-last-commit" do
+ page.within ".blob-commit-info" do
expect(page).to have_content(project.commit.sha[0..6])
- expect(page).to have_content("skipped")
+ expect(page).to have_link("Commit: skipped")
end
end
diff --git a/features/support/env.rb b/features/support/env.rb
index 568eeae4479..23a1f702068 100644
--- a/features/support/env.rb
+++ b/features/support/env.rb
@@ -30,8 +30,8 @@ Spinach.hooks.before_run do
include FactoryGirl::Syntax::Methods
end
-Spinach.hooks.after_feature do |feature_data|
- if feature_data.scenarios.flat_map(&:tags).include?('javascript')
+Spinach.hooks.after_scenario do |scenario_data, step_definitions|
+ if scenario_data.tags.include?('javascript')
include WaitForRequests
wait_for_requests_complete
end
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 00d494f02f5..01cc8e8e1ca 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -53,14 +53,14 @@ module API
end
class Hook < Grape::Entity
- expose :id, :url, :created_at, :push_events, :tag_push_events
+ expose :id, :url, :created_at, :push_events, :tag_push_events, :repository_update_events
expose :enable_ssl_verification
end
class ProjectHook < Hook
expose :project_id, :issues_events, :merge_requests_events
expose :note_events, :pipeline_events, :wiki_page_events
- expose :build_events, as: :job_events
+ expose :job_events
end
class BasicProjectDetails < Grape::Entity
@@ -470,7 +470,7 @@ module API
expose :id, :title, :created_at, :updated_at, :active
expose :push_events, :issues_events, :merge_requests_events
expose :tag_push_events, :note_events, :pipeline_events
- expose :build_events, as: :job_events
+ expose :job_events
# Expose serialized properties
expose :properties do |service, options|
field_names = service.fields.
diff --git a/lib/api/groups.rb b/lib/api/groups.rb
index 09d105f6b4c..3da7d735da8 100644
--- a/lib/api/groups.rb
+++ b/lib/api/groups.rb
@@ -24,7 +24,7 @@ module API
def present_groups(groups, options = {})
options = options.reverse_merge(
with: Entities::Group,
- current_user: current_user,
+ current_user: current_user
)
groups = groups.with_statistics if options[:statistics]
@@ -52,7 +52,7 @@ module API
elsif current_user.admin
Group.all
elsif params[:all_available]
- GroupsFinder.new.execute(current_user)
+ GroupsFinder.new(current_user).execute
else
current_user.groups
end
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index 86bf567fe69..226a7ddd50e 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -91,8 +91,8 @@ module API
end
def find_project_snippet(id)
- finder_params = { filter: :by_project, project: user_project }
- SnippetsFinder.new.execute(current_user, finder_params).find(id)
+ finder_params = { project: user_project }
+ SnippetsFinder.new(current_user, finder_params).execute.find(id)
end
def find_merge_request_with_access(iid, access_level = :read_merge_request)
@@ -301,7 +301,7 @@ module API
UploadedFile.new(
file_path,
params["#{field}.name"],
- params["#{field}.type"] || 'application/octet-stream',
+ params["#{field}.type"] || 'application/octet-stream'
)
end
diff --git a/lib/api/helpers/common_helpers.rb b/lib/api/helpers/common_helpers.rb
index 6236fdd43ca..322624c6092 100644
--- a/lib/api/helpers/common_helpers.rb
+++ b/lib/api/helpers/common_helpers.rb
@@ -2,11 +2,11 @@ module API
module Helpers
module CommonHelpers
def convert_parameters_from_legacy_format(params)
- if params[:assignee_id].present?
- params[:assignee_ids] = [params.delete(:assignee_id)]
+ params.tap do |params|
+ if params[:assignee_id].present?
+ params[:assignee_ids] = [params.delete(:assignee_id)]
+ end
end
-
- params
end
end
end
diff --git a/lib/api/internal.rb b/lib/api/internal.rb
index 2a11790b215..96aaaf868ea 100644
--- a/lib/api/internal.rb
+++ b/lib/api/internal.rb
@@ -90,7 +90,7 @@ module API
{
api_version: API.version,
gitlab_version: Gitlab::VERSION,
- gitlab_rev: Gitlab::REVISION,
+ gitlab_rev: Gitlab::REVISION
}
end
diff --git a/lib/api/project_hooks.rb b/lib/api/project_hooks.rb
index 87dfd1573a4..7a345289617 100644
--- a/lib/api/project_hooks.rb
+++ b/lib/api/project_hooks.rb
@@ -54,7 +54,6 @@ module API
end
post ":id/hooks" do
hook_params = declared_params(include_missing: false)
- hook_params[:build_events] = hook_params.delete(:job_events) { false }
hook = user_project.hooks.new(hook_params)
@@ -78,7 +77,6 @@ module API
hook = user_project.hooks.find(params.delete(:hook_id))
update_params = declared_params(include_missing: false)
- update_params[:build_events] = update_params.delete(:job_events) if update_params[:job_events]
if hook.update_attributes(update_params)
present hook, with: Entities::ProjectHook
diff --git a/lib/api/project_snippets.rb b/lib/api/project_snippets.rb
index cfee38a9baf..98bc9c28527 100644
--- a/lib/api/project_snippets.rb
+++ b/lib/api/project_snippets.rb
@@ -17,8 +17,7 @@ module API
end
def snippets_for_current_user
- finder_params = { filter: :by_project, project: user_project }
- SnippetsFinder.new.execute(current_user, finder_params)
+ SnippetsFinder.new(current_user, project: user_project).execute
end
end
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index 9a6cb43abf7..ed5004e8d1a 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -69,7 +69,7 @@ module API
options = options.reverse_merge(
with: Entities::Project,
current_user: current_user,
- simple: params[:simple],
+ simple: params[:simple]
)
projects = filter_projects(projects)
@@ -226,7 +226,7 @@ module API
:shared_runners_enabled,
:snippets_enabled,
:visibility,
- :wiki_enabled,
+ :wiki_enabled
]
optional :name, type: String, desc: 'The name of the project'
optional :default_branch, type: String, desc: 'The default branch of the project'
diff --git a/lib/api/services.rb b/lib/api/services.rb
index 23ef62c2258..cb07df9e249 100644
--- a/lib/api/services.rb
+++ b/lib/api/services.rb
@@ -356,7 +356,7 @@ module API
name: :ca_pem,
type: String,
desc: 'A custom certificate authority bundle to verify the Kubernetes cluster with (PEM format)'
- },
+ }
],
'mattermost-slash-commands' => [
{
@@ -559,7 +559,7 @@ module API
SlackService,
MattermostService,
MicrosoftTeamsService,
- TeamcityService,
+ TeamcityService
]
if Rails.env.development?
@@ -577,7 +577,7 @@ module API
service_classes += [
MockCiService,
MockDeploymentService,
- MockMonitoringService,
+ MockMonitoringService
]
end
diff --git a/lib/api/snippets.rb b/lib/api/snippets.rb
index b93fdc62808..53f5953a8fb 100644
--- a/lib/api/snippets.rb
+++ b/lib/api/snippets.rb
@@ -8,11 +8,11 @@ module API
resource :snippets do
helpers do
def snippets_for_current_user
- SnippetsFinder.new.execute(current_user, filter: :by_user, user: current_user)
+ SnippetsFinder.new(current_user, author: current_user).execute
end
def public_snippets
- SnippetsFinder.new.execute(current_user, filter: :public)
+ SnippetsFinder.new(current_user, visibility: Snippet::PUBLIC).execute
end
end
diff --git a/lib/api/subscriptions.rb b/lib/api/subscriptions.rb
index dbe54d3cd31..91567909998 100644
--- a/lib/api/subscriptions.rb
+++ b/lib/api/subscriptions.rb
@@ -5,7 +5,7 @@ module API
subscribable_types = {
'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) },
+ 'labels' => proc { |id| find_project_label(id) }
}
params do
diff --git a/lib/api/users.rb b/lib/api/users.rb
index 40acaebf670..3d83720b7b9 100644
--- a/lib/api/users.rb
+++ b/lib/api/users.rb
@@ -56,16 +56,7 @@ module API
authenticated_as_admin! if params[:external].present? || (params[:extern_uid].present? && params[:provider].present?)
- users = User.all
- users = User.where(username: params[:username]) if params[:username]
- users = users.active if params[:active]
- users = users.search(params[:search]) if params[:search].present?
- users = users.blocked if params[:blocked]
-
- if current_user.admin?
- users = users.joins(:identities).merge(Identity.with_extern_uid(params[:provider], params[:extern_uid])) if params[:extern_uid] && params[:provider]
- users = users.external if params[:external]
- end
+ users = UsersFinder.new(current_user, params).execute
entity = current_user.admin? ? Entities::UserPublic : Entities::UserBasic
present paginate(users), with: entity
diff --git a/lib/api/v3/entities.rb b/lib/api/v3/entities.rb
index 56a9b019f1b..332f233bf5e 100644
--- a/lib/api/v3/entities.rb
+++ b/lib/api/v3/entities.rb
@@ -238,7 +238,8 @@ module API
class ProjectService < Grape::Entity
expose :id, :title, :created_at, :updated_at, :active
expose :push_events, :issues_events, :merge_requests_events
- expose :tag_push_events, :note_events, :build_events, :pipeline_events
+ expose :tag_push_events, :note_events, :pipeline_events
+ expose :job_events, as: :build_events
# Expose serialized properties
expose :properties do |service, options|
field_names = service.fields.
@@ -250,7 +251,8 @@ module API
class ProjectHook < ::API::Entities::Hook
expose :project_id, :issues_events, :merge_requests_events
- expose :note_events, :build_events, :pipeline_events, :wiki_page_events
+ expose :note_events, :pipeline_events, :wiki_page_events
+ expose :job_events, as: :build_events
end
class Issue < ::API::Entities::Issue
diff --git a/lib/api/v3/groups.rb b/lib/api/v3/groups.rb
index 63d464b926b..6187445fc8d 100644
--- a/lib/api/v3/groups.rb
+++ b/lib/api/v3/groups.rb
@@ -20,7 +20,7 @@ module API
def present_groups(groups, options = {})
options = options.reverse_merge(
with: Entities::Group,
- current_user: current_user,
+ current_user: current_user
)
groups = groups.with_statistics if options[:statistics]
@@ -45,7 +45,7 @@ module API
groups = if current_user.admin
Group.all
elsif params[:all_available]
- GroupsFinder.new.execute(current_user)
+ GroupsFinder.new(current_user).execute
else
current_user.groups
end
diff --git a/lib/api/v3/project_snippets.rb b/lib/api/v3/project_snippets.rb
index fc065a22d74..c41fee32610 100644
--- a/lib/api/v3/project_snippets.rb
+++ b/lib/api/v3/project_snippets.rb
@@ -18,8 +18,7 @@ module API
end
def snippets_for_current_user
- finder_params = { filter: :by_project, project: user_project }
- SnippetsFinder.new.execute(current_user, finder_params)
+ SnippetsFinder.new(current_user, project: user_project).execute
end
end
diff --git a/lib/api/v3/projects.rb b/lib/api/v3/projects.rb
index 06cc704afc6..164612cb8dd 100644
--- a/lib/api/v3/projects.rb
+++ b/lib/api/v3/projects.rb
@@ -88,7 +88,7 @@ module API
options = options.reverse_merge(
with: ::API::V3::Entities::Project,
current_user: current_user,
- simple: params[:simple],
+ simple: params[:simple]
)
projects = filter_projects(projects)
diff --git a/lib/api/v3/services.rb b/lib/api/v3/services.rb
index 61629a04174..118c6df6549 100644
--- a/lib/api/v3/services.rb
+++ b/lib/api/v3/services.rb
@@ -377,7 +377,7 @@ module API
name: :ca_pem,
type: String,
desc: 'A custom certificate authority bundle to verify the Kubernetes cluster with (PEM format)'
- },
+ }
],
'mattermost-slash-commands' => [
{
diff --git a/lib/api/v3/snippets.rb b/lib/api/v3/snippets.rb
index 07dac7e9904..0762fc02d70 100644
--- a/lib/api/v3/snippets.rb
+++ b/lib/api/v3/snippets.rb
@@ -8,11 +8,11 @@ module API
resource :snippets do
helpers do
def snippets_for_current_user
- SnippetsFinder.new.execute(current_user, filter: :by_user, user: current_user)
+ SnippetsFinder.new(current_user, author: current_user).execute
end
def public_snippets
- SnippetsFinder.new.execute(current_user, filter: :public)
+ SnippetsFinder.new(current_user, visibility: Snippet::PUBLIC).execute
end
end
diff --git a/lib/api/v3/subscriptions.rb b/lib/api/v3/subscriptions.rb
index 068750ec077..690768db82f 100644
--- a/lib/api/v3/subscriptions.rb
+++ b/lib/api/v3/subscriptions.rb
@@ -7,7 +7,7 @@ module API
'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) },
+ 'labels' => proc { |id| find_project_label(id) }
}
params do
diff --git a/lib/banzai/filter/external_link_filter.rb b/lib/banzai/filter/external_link_filter.rb
index d67d466bce8..d6327ef31cb 100644
--- a/lib/banzai/filter/external_link_filter.rb
+++ b/lib/banzai/filter/external_link_filter.rb
@@ -2,16 +2,17 @@ module Banzai
module Filter
# HTML Filter to modify the attributes of external links
class ExternalLinkFilter < HTML::Pipeline::Filter
+ SCHEMES = ['http', 'https', nil].freeze
+
def call
links.each do |node|
- href = href_to_lowercase_scheme(node["href"].to_s)
+ uri = uri(node['href'].to_s)
+ next unless uri
- unless node["href"].to_s == href
- node.set_attribute('href', href)
- end
+ node.set_attribute('href', uri.to_s)
- if href =~ %r{\A(https?:)?//[^/]} && external_url?(href)
- node.set_attribute('rel', 'nofollow noreferrer')
+ if SCHEMES.include?(uri.scheme) && external_url?(uri)
+ node.set_attribute('rel', 'nofollow noreferrer noopener')
node.set_attribute('target', '_blank')
end
end
@@ -21,27 +22,26 @@ module Banzai
private
+ def uri(href)
+ URI.parse(href)
+ rescue URI::Error
+ nil
+ end
+
def links
query = 'descendant-or-self::a[@href and not(@href = "")]'
doc.xpath(query)
end
- def href_to_lowercase_scheme(href)
- scheme_match = href.match(/\A(\w+):\/\//)
-
- if scheme_match
- scheme_match.to_s.downcase + scheme_match.post_match
- else
- href
- end
- end
+ def external_url?(uri)
+ # Relative URLs miss a hostname
+ return false unless uri.hostname
- def external_url?(url)
- !url.start_with?(internal_url)
+ uri.hostname != internal_url.hostname
end
def internal_url
- @internal_url ||= Gitlab.config.gitlab.url
+ @internal_url ||= URI.parse(Gitlab.config.gitlab.url)
end
end
end
diff --git a/lib/banzai/pipeline/markup_pipeline.rb b/lib/banzai/pipeline/markup_pipeline.rb
new file mode 100644
index 00000000000..c56d908009f
--- /dev/null
+++ b/lib/banzai/pipeline/markup_pipeline.rb
@@ -0,0 +1,13 @@
+module Banzai
+ module Pipeline
+ class MarkupPipeline < BasePipeline
+ def self.filters
+ @filters ||= FilterArray[
+ Filter::SanitizationFilter,
+ Filter::ExternalLinkFilter,
+ Filter::PlantumlFilter
+ ]
+ end
+ end
+ end
+end
diff --git a/lib/ci/ansi2html.rb b/lib/ci/ansi2html.rb
index b439b0ee29b..55402101e43 100644
--- a/lib/ci/ansi2html.rb
+++ b/lib/ci/ansi2html.rb
@@ -20,7 +20,7 @@ module Ci
italic: 0x02,
underline: 0x04,
conceal: 0x08,
- cross: 0x10,
+ cross: 0x10
}.freeze
def self.convert(ansi, state = nil)
diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb
index 15a461a16dd..b06474cda7f 100644
--- a/lib/ci/gitlab_ci_yaml_processor.rb
+++ b/lib/ci/gitlab_ci_yaml_processor.rb
@@ -70,7 +70,7 @@ module Ci
cache: job[:cache],
dependencies: job[:dependencies],
after_script: job[:after_script],
- environment: job[:environment],
+ environment: job[:environment]
}.compact
}
end
diff --git a/lib/container_registry/client.rb b/lib/container_registry/client.rb
index 7f5f6d9ddb6..c7263f302ab 100644
--- a/lib/container_registry/client.rb
+++ b/lib/container_registry/client.rb
@@ -75,10 +75,7 @@ module ContainerRegistry
def redirect_response(location)
return unless location
- # We explicitly remove authorization token
- faraday_blob.get(location) do |req|
- req['Authorization'] = ''
- end
+ faraday_redirect.get(location)
end
def faraday
@@ -93,5 +90,14 @@ module ContainerRegistry
initialize_connection(conn, @options)
end
end
+
+ # Create a new request to make sure the Authorization header is not inserted
+ # via the Faraday middleware
+ def faraday_redirect
+ @faraday_redirect ||= Faraday.new(@base_uri) do |conn|
+ conn.request :json
+ conn.adapter :net_http
+ end
+ end
end
end
diff --git a/lib/github/import.rb b/lib/github/import.rb
index 06beb607a3e..9c7eb965f93 100644
--- a/lib/github/import.rb
+++ b/lib/github/import.rb
@@ -1,4 +1,5 @@
require_relative 'error'
+
module Github
class Import
include Gitlab::ShellAdapter
@@ -6,6 +7,7 @@ module Github
class MergeRequest < ::MergeRequest
self.table_name = 'merge_requests'
+ self.reset_callbacks :create
self.reset_callbacks :save
self.reset_callbacks :commit
self.reset_callbacks :update
@@ -16,6 +18,7 @@ module Github
self.table_name = 'issues'
self.reset_callbacks :save
+ self.reset_callbacks :create
self.reset_callbacks :commit
self.reset_callbacks :update
self.reset_callbacks :validate
@@ -79,7 +82,7 @@ module Github
def fetch_repository
begin
project.create_repository unless project.repository.exists?
- project.repository.add_remote('github', "https://{options.fetch(:token)}@github.com/#{repo}.git")
+ project.repository.add_remote('github', "https://#{options.fetch(:token)}@github.com/#{repo}.git")
project.repository.set_remote_as_mirror('github')
project.repository.fetch_remote('github', forced: true)
rescue Gitlab::Shell::Error => e
diff --git a/lib/gitlab/access.rb b/lib/gitlab/access.rb
index 8c28009b9c6..4714ab18cc1 100644
--- a/lib/gitlab/access.rb
+++ b/lib/gitlab/access.rb
@@ -32,7 +32,7 @@ module Gitlab
"Guest" => GUEST,
"Reporter" => REPORTER,
"Developer" => DEVELOPER,
- "Master" => MASTER,
+ "Master" => MASTER
}
end
@@ -47,7 +47,7 @@ module Gitlab
guest: GUEST,
reporter: REPORTER,
developer: DEVELOPER,
- master: MASTER,
+ master: MASTER
}
end
@@ -60,7 +60,7 @@ module Gitlab
"Not protected: Both developers and masters can push new commits, force push, or delete the branch." => PROTECTION_NONE,
"Protected against pushes: Developers cannot push new commits, but are allowed to accept merge requests to the branch." => PROTECTION_DEV_CAN_MERGE,
"Partially protected: Developers can push new commits, but cannot force push or delete the branch. Masters can do all of those." => PROTECTION_DEV_CAN_PUSH,
- "Fully protected: Developers cannot push new commits, force push, or delete the branch. Only masters can do any of those." => PROTECTION_FULL,
+ "Fully protected: Developers cannot push new commits, force push, or delete the branch. Only masters can do any of those." => PROTECTION_FULL
}
end
diff --git a/lib/gitlab/asciidoc.rb b/lib/gitlab/asciidoc.rb
index fba80c7132e..96d38f6daa0 100644
--- a/lib/gitlab/asciidoc.rb
+++ b/lib/gitlab/asciidoc.rb
@@ -15,17 +15,17 @@ module Gitlab
#
# input - the source text in Asciidoc format
#
- def self.render(input)
+ def self.render(input, context)
asciidoc_opts = { safe: :secure,
backend: :gitlab_html5,
attributes: DEFAULT_ADOC_ATTRS }
+ context[:pipeline] = :markup
+
plantuml_setup
html = ::Asciidoctor.convert(input, asciidoc_opts)
-
- filter = Banzai::Filter::SanitizationFilter.new(html)
- html = filter.call.to_s
+ html = Banzai.render(html, context)
html.html_safe
end
diff --git a/lib/gitlab/chat_commands/command.rb b/lib/gitlab/chat_commands/command.rb
index f34ed0f4cf2..3e0c30c33b7 100644
--- a/lib/gitlab/chat_commands/command.rb
+++ b/lib/gitlab/chat_commands/command.rb
@@ -5,7 +5,7 @@ module Gitlab
Gitlab::ChatCommands::IssueShow,
Gitlab::ChatCommands::IssueNew,
Gitlab::ChatCommands::IssueSearch,
- Gitlab::ChatCommands::Deploy,
+ Gitlab::ChatCommands::Deploy
].freeze
def execute
diff --git a/lib/gitlab/ci/config/entry/legacy_validation_helpers.rb b/lib/gitlab/ci/config/entry/legacy_validation_helpers.rb
index 9b9a0a8125a..a78a85397bd 100644
--- a/lib/gitlab/ci/config/entry/legacy_validation_helpers.rb
+++ b/lib/gitlab/ci/config/entry/legacy_validation_helpers.rb
@@ -21,7 +21,13 @@ module Gitlab
def validate_variables(variables)
variables.is_a?(Hash) &&
- variables.all? { |key, value| validate_string(key) && validate_string(value) }
+ variables.flatten.all? do |value|
+ validate_string(value) || validate_integer(value)
+ end
+ end
+
+ def validate_integer(value)
+ value.is_a?(Integer)
end
def validate_string(value)
diff --git a/lib/gitlab/ci/config/entry/variables.rb b/lib/gitlab/ci/config/entry/variables.rb
index c3b0e651c3a..8acab605c91 100644
--- a/lib/gitlab/ci/config/entry/variables.rb
+++ b/lib/gitlab/ci/config/entry/variables.rb
@@ -15,6 +15,10 @@ module Gitlab
def self.default
{}
end
+
+ def value
+ Hash[@config.map { |key, value| [key.to_s, value.to_s] }]
+ end
end
end
end
diff --git a/lib/gitlab/conflict/file_collection.rb b/lib/gitlab/conflict/file_collection.rb
index 990b719ecfd..6e73361cad1 100644
--- a/lib/gitlab/conflict/file_collection.rb
+++ b/lib/gitlab/conflict/file_collection.rb
@@ -3,16 +3,33 @@ module Gitlab
class FileCollection
ConflictSideMissing = Class.new(StandardError)
- attr_reader :merge_request, :our_commit, :their_commit
+ attr_reader :merge_request, :our_commit, :their_commit, :project
- def initialize(merge_request)
- @merge_request = merge_request
- @our_commit = merge_request.source_branch_head.raw.raw_commit
- @their_commit = merge_request.target_branch_head.raw.raw_commit
- end
+ delegate :repository, to: :project
+
+ class << self
+ # We can only write when getting the merge index from the source
+ # project, because we will write to that project. We don't use this all
+ # the time because this fetches a ref into the source project, which
+ # isn't needed for reading.
+ def for_resolution(merge_request)
+ project = merge_request.source_project
+
+ new(merge_request, project).tap do |file_collection|
+ project.
+ repository.
+ with_repo_branch_commit(merge_request.target_project.repository, merge_request.target_branch) do
+
+ yield file_collection
+ end
+ end
+ end
- def repository
- merge_request.project.repository
+ # We don't need to do `with_repo_branch_commit` here, because the target
+ # project always fetches source refs when creating merge request diffs.
+ def read_only(merge_request)
+ new(merge_request, merge_request.target_project)
+ end
end
def merge_index
@@ -55,6 +72,15 @@ Merge branch '#{merge_request.target_branch}' into '#{merge_request.source_branc
#{conflict_filenames.join("\n")}
EOM
end
+
+ private
+
+ def initialize(merge_request, project)
+ @merge_request = merge_request
+ @our_commit = merge_request.source_branch_head.raw.raw_commit
+ @their_commit = merge_request.target_branch_head.raw.raw_commit
+ @project = project
+ end
end
end
end
diff --git a/lib/gitlab/cycle_analytics/permissions.rb b/lib/gitlab/cycle_analytics/permissions.rb
index bef3b95ff1b..1e11e84a9cb 100644
--- a/lib/gitlab/cycle_analytics/permissions.rb
+++ b/lib/gitlab/cycle_analytics/permissions.rb
@@ -7,7 +7,7 @@ module Gitlab
test: :read_build,
review: :read_merge_request,
staging: :read_build,
- production: :read_issue,
+ production: :read_issue
}.freeze
def self.get(*args)
diff --git a/lib/gitlab/data_builder/build.rb b/lib/gitlab/data_builder/build.rb
index f78106f5b10..8e74e18a311 100644
--- a/lib/gitlab/data_builder/build.rb
+++ b/lib/gitlab/data_builder/build.rb
@@ -36,7 +36,7 @@ module Gitlab
user: {
id: user.try(:id),
name: user.try(:name),
- email: user.try(:email),
+ email: user.try(:email)
},
commit: {
@@ -49,7 +49,7 @@ module Gitlab
status: commit.status,
duration: commit.duration,
started_at: commit.started_at,
- finished_at: commit.finished_at,
+ finished_at: commit.finished_at
},
repository: {
@@ -60,7 +60,7 @@ module Gitlab
git_http_url: project.http_url_to_repo,
git_ssh_url: project.ssh_url_to_repo,
visibility_level: project.visibility_level
- },
+ }
}
data
diff --git a/lib/gitlab/data_builder/push.rb b/lib/gitlab/data_builder/push.rb
index 1ff34553f0a..e81d19a7a2e 100644
--- a/lib/gitlab/data_builder/push.rb
+++ b/lib/gitlab/data_builder/push.rb
@@ -11,6 +11,7 @@ module Gitlab
# ref: String,
# user_id: String,
# user_name: String,
+ # user_username: String,
# user_email: String
# project_id: String,
# repository: {
@@ -51,6 +52,7 @@ module Gitlab
message: message,
user_id: user.id,
user_name: user.name,
+ user_username: user.username,
user_email: user.email,
user_avatar: user.avatar_url,
project_id: project.id,
diff --git a/lib/gitlab/data_builder/repository.rb b/lib/gitlab/data_builder/repository.rb
new file mode 100644
index 00000000000..b42dc052949
--- /dev/null
+++ b/lib/gitlab/data_builder/repository.rb
@@ -0,0 +1,35 @@
+module Gitlab
+ module DataBuilder
+ module Repository
+ extend self
+
+ # Produce a hash of post-receive data
+ def update(project, user, changes, refs)
+ {
+ event_name: 'repository_update',
+
+ user_id: user.id,
+ user_name: user.name,
+ user_email: user.email,
+ user_avatar: user.avatar_url,
+
+ project_id: project.id,
+ project: project.hook_attrs,
+
+ changes: changes,
+
+ refs: refs
+ }
+ end
+
+ # Produce a hash of partial data for a single change
+ def single_change(oldrev, newrev, ref)
+ {
+ before: oldrev,
+ after: newrev,
+ ref: ref
+ }
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb
index 298b1a1f4e6..e76c9abbe04 100644
--- a/lib/gitlab/database/migration_helpers.rb
+++ b/lib/gitlab/database/migration_helpers.rb
@@ -278,6 +278,19 @@ module Gitlab
raise 'rename_column_concurrently can not be run inside a transaction'
end
+ old_col = column_for(table, old)
+ new_type = type || old_col.type
+
+ add_column(table, new, new_type,
+ limit: old_col.limit,
+ precision: old_col.precision,
+ scale: old_col.scale)
+
+ # We set the default value _after_ adding the column so we don't end up
+ # updating any existing data with the default value. This isn't
+ # necessary since we copy over old values further down.
+ change_column_default(table, new, old_col.default) if old_col.default
+
trigger_name = rename_trigger_name(table, old, new)
quoted_table = quote_table_name(table)
quoted_old = quote_column_name(old)
@@ -291,18 +304,10 @@ module Gitlab
quoted_old, quoted_new)
end
- old_col = column_for(table, old)
- new_type = type || old_col.type
-
- add_column(table, new, new_type,
- limit: old_col.limit,
- default: old_col.default,
- null: old_col.null,
- precision: old_col.precision,
- scale: old_col.scale)
-
update_column_in_batches(table, new, Arel::Table.new(table)[old])
+ change_column_null(table, new, false) unless old_col.null
+
copy_indexes(table, old, new)
copy_foreign_keys(table, old, new)
end
diff --git a/lib/gitlab/database/rename_reserved_paths_migration/v1/migration_classes.rb b/lib/gitlab/database/rename_reserved_paths_migration/v1/migration_classes.rb
index 4fdcb682c2f..5481024db8e 100644
--- a/lib/gitlab/database/rename_reserved_paths_migration/v1/migration_classes.rb
+++ b/lib/gitlab/database/rename_reserved_paths_migration/v1/migration_classes.rb
@@ -48,6 +48,14 @@ module Gitlab
def self.name
'Namespace'
end
+
+ def kind
+ type == 'Group' ? 'group' : 'user'
+ end
+ end
+
+ class User < ActiveRecord::Base
+ self.table_name = 'users'
end
class Route < ActiveRecord::Base
diff --git a/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base.rb b/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base.rb
index de4e6e7c404..d60fd4bb551 100644
--- a/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base.rb
+++ b/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base.rb
@@ -15,7 +15,7 @@ module Gitlab
end
def path_patterns
- @path_patterns ||= paths.map { |path| "%#{path}" }
+ @path_patterns ||= paths.flat_map { |path| ["%/#{path}", path] }
end
def rename_path_for_routable(routable)
@@ -41,7 +41,8 @@ module Gitlab
new_full_path)
update_column_in_batches(:routes, :path, replace_statement) do |table, query|
- query.where(MigrationClasses::Route.arel_table[:path].matches("#{old_full_path}%"))
+ path_or_children = table[:path].matches_any([old_full_path, "#{old_full_path}/%"])
+ query.where(path_or_children)
end
end
diff --git a/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces.rb b/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces.rb
index b9f4f3cff3c..2958ad4b8e5 100644
--- a/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces.rb
+++ b/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces.rb
@@ -29,9 +29,15 @@ module Gitlab
move_repositories(namespace, old_full_path, new_full_path)
move_uploads(old_full_path, new_full_path)
move_pages(old_full_path, new_full_path)
+ rename_user(old_full_path, new_full_path) if namespace.kind == 'user'
remove_cached_html_for_projects(projects_for_namespace(namespace).map(&:id))
end
+ def rename_user(old_username, new_username)
+ MigrationClasses::User.where(username: old_username)
+ .update_all(username: new_username)
+ end
+
def move_repositories(namespace, old_full_path, new_full_path)
repo_paths_for_namespace(namespace).each do |repository_storage_path|
# Ensure old directory exists before moving it
diff --git a/lib/gitlab/dependency_linker.rb b/lib/gitlab/dependency_linker.rb
new file mode 100644
index 00000000000..c45ae8feb2c
--- /dev/null
+++ b/lib/gitlab/dependency_linker.rb
@@ -0,0 +1,18 @@
+module Gitlab
+ module DependencyLinker
+ LINKERS = [
+ GemfileLinker
+ ].freeze
+
+ def self.linker(blob_name)
+ LINKERS.find { |linker| linker.support?(blob_name) }
+ end
+
+ def self.link(blob_name, plain_text, highlighted_text)
+ linker = linker(blob_name)
+ return highlighted_text unless linker
+
+ linker.link(plain_text, highlighted_text)
+ end
+ end
+end
diff --git a/lib/gitlab/dependency_linker/base_linker.rb b/lib/gitlab/dependency_linker/base_linker.rb
new file mode 100644
index 00000000000..5f4027e7e81
--- /dev/null
+++ b/lib/gitlab/dependency_linker/base_linker.rb
@@ -0,0 +1,109 @@
+module Gitlab
+ module DependencyLinker
+ class BaseLinker
+ class_attribute :file_type
+
+ def self.support?(blob_name)
+ Gitlab::FileDetector.type_of(blob_name) == file_type
+ end
+
+ def self.link(*args)
+ new(*args).link
+ end
+
+ attr_accessor :plain_text, :highlighted_text
+
+ def initialize(plain_text, highlighted_text)
+ @plain_text = plain_text
+ @highlighted_text = highlighted_text
+ end
+
+ def link
+ link_dependencies
+
+ highlighted_lines.join.html_safe
+ end
+
+ private
+
+ def package_url(name)
+ raise NotImplementedError
+ end
+
+ def link_dependencies
+ raise NotImplementedError
+ end
+
+ def package_link(name, url = package_url(name))
+ return name unless url
+
+ %{<a href="#{ERB::Util.html_escape_once(url)}" rel="noopener noreferrer" target="_blank">#{ERB::Util.html_escape_once(name)}</a>}
+ end
+
+ # Links package names in a method call or assignment string argument.
+ #
+ # Example:
+ # link_method_call("gem")
+ # # Will link `package` in `gem "package"`, `gem("package")` and `gem = "package"`
+ #
+ # link_method_call("gem", "specific_package")
+ # # Will link `specific_package` in `gem "specific_package"`
+ #
+ # link_method_call("github", /[^\/]+\/[^\/]+/)
+ # # Will link `user/repo` in `github "user/repo"`, but not `github "package"`
+ #
+ # link_method_call(%w[add_dependency add_development_dependency])
+ # # Will link `spec.add_dependency "package"` and `spec.add_development_dependency "package"`
+ #
+ # link_method_call("name")
+ # # Will link `package` in `self.name = "package"`
+ def link_method_call(method_names, value = nil, &url_proc)
+ value =
+ case value
+ when String
+ Regexp.escape(value)
+ when nil
+ /[^'"]+/
+ else
+ value
+ end
+
+ method_names = Array(method_names).map { |name| Regexp.escape(name) }
+
+ regex = %r{
+ #{Regexp.union(method_names)} # Method name
+ \s* # Whitespace
+ [(=]? # Opening brace or equals sign
+ \s* # Whitespace
+ ['"](?<name>#{value})['"] # Package name in quotes
+ }x
+
+ link_regex(regex, &url_proc)
+ end
+
+ # Links package names based on regex.
+ #
+ # Example:
+ # link_regex(/(github:|:github =>)\s*['"](?<name>[^'"]+)['"]/)
+ # # Will link `user/repo` in `github: "user/repo"` or `:github => "user/repo"`
+ def link_regex(regex)
+ highlighted_lines.map!.with_index do |rich_line, i|
+ marker = StringRegexMarker.new(plain_lines[i], rich_line.html_safe)
+
+ marker.mark(regex, group: :name) do |text, left:, right:|
+ url = block_given? ? yield(text) : package_url(text)
+ package_link(text, url)
+ end
+ end
+ end
+
+ def plain_lines
+ @plain_lines ||= plain_text.lines
+ end
+
+ def highlighted_lines
+ @highlighted_lines ||= highlighted_text.lines
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/dependency_linker/gemfile_linker.rb b/lib/gitlab/dependency_linker/gemfile_linker.rb
new file mode 100644
index 00000000000..9b82e126528
--- /dev/null
+++ b/lib/gitlab/dependency_linker/gemfile_linker.rb
@@ -0,0 +1,29 @@
+module Gitlab
+ module DependencyLinker
+ class GemfileLinker < BaseLinker
+ self.file_type = :gemfile
+
+ private
+
+ def link_dependencies
+ # Link `gem "package_name"` to https://rubygems.org/gems/package_name
+ link_method_call("gem")
+
+ # Link `github: "user/repo"` to https://github.com/user/repo
+ link_regex(/(github:|:github\s*=>)\s*['"](?<name>[^'"]+)['"]/) do |name|
+ "https://github.com/#{name}"
+ end
+
+ # Link `git: "https://gitlab.example.com/user/repo"` to https://gitlab.example.com/user/repo
+ link_regex(%r{(git:|:git\s*=>)\s*['"](?<name>https?://[^'"]+)['"]}) { |url| url }
+
+ # Link `source "https://rubygems.org"` to https://rubygems.org
+ link_method_call("source", %r{https?://[^'"]+}) { |url| url }
+ end
+
+ def package_url(name)
+ "https://rubygems.org/gems/#{name}"
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/diff/inline_diff_markdown_marker.rb b/lib/gitlab/diff/inline_diff_markdown_marker.rb
new file mode 100644
index 00000000000..c2a2eb15931
--- /dev/null
+++ b/lib/gitlab/diff/inline_diff_markdown_marker.rb
@@ -0,0 +1,17 @@
+module Gitlab
+ module Diff
+ class InlineDiffMarkdownMarker < Gitlab::StringRangeMarker
+ MARKDOWN_SYMBOLS = {
+ addition: "+",
+ deletion: "-"
+ }.freeze
+
+ def mark(line_inline_diffs, mode: nil)
+ super(line_inline_diffs) do |text, left:, right:|
+ symbol = MARKDOWN_SYMBOLS[mode]
+ "{#{symbol}#{text}#{symbol}}"
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/diff/inline_diff_marker.rb b/lib/gitlab/diff/inline_diff_marker.rb
index 736933b1c4b..919965100ae 100644
--- a/lib/gitlab/diff/inline_diff_marker.rb
+++ b/lib/gitlab/diff/inline_diff_marker.rb
@@ -1,137 +1,21 @@
module Gitlab
module Diff
- class InlineDiffMarker
- MARKDOWN_SYMBOLS = {
- addition: "+",
- deletion: "-"
- }.freeze
-
- attr_accessor :raw_line, :rich_line
-
- def initialize(raw_line, rich_line = raw_line)
- @raw_line = raw_line
- @rich_line = ERB::Util.html_escape(rich_line)
- end
-
- def mark(line_inline_diffs, mode: nil, markdown: false)
- return rich_line unless line_inline_diffs
-
- marker_ranges = []
- line_inline_diffs.each do |inline_diff_range|
- # Map the inline-diff range based on the raw line to character positions in the rich line
- inline_diff_positions = position_mapping[inline_diff_range].flatten
- # Turn the array of character positions into ranges
- marker_ranges.concat(collapse_ranges(inline_diff_positions))
- end
-
- offset = 0
-
- # Mark each range
- marker_ranges.each_with_index do |range, index|
- before_content =
- if markdown
- "{#{MARKDOWN_SYMBOLS[mode]}"
- else
- "<span class='#{html_class_names(marker_ranges, mode, index)}'>"
- end
- after_content =
- if markdown
- "#{MARKDOWN_SYMBOLS[mode]}}"
- else
- "</span>"
- end
- offset = insert_around_range(rich_line, range, before_content, after_content, offset)
+ class InlineDiffMarker < Gitlab::StringRangeMarker
+ def mark(line_inline_diffs, mode: nil)
+ super(line_inline_diffs) do |text, left:, right:|
+ %{<span class="#{html_class_names(left, right, mode)}">#{text}</span>}
end
-
- rich_line.html_safe
end
private
- def html_class_names(marker_ranges, mode, index)
+ def html_class_names(left, right, mode)
class_names = ["idiff"]
- class_names << "left" if index == 0
- class_names << "right" if index == marker_ranges.length - 1
+ class_names << "left" if left
+ class_names << "right" if right
class_names << mode if mode
class_names.join(" ")
end
-
- # Mapping of character positions in the raw line, to the rich (highlighted) line
- def position_mapping
- @position_mapping ||= begin
- mapping = []
- rich_pos = 0
- (0..raw_line.length).each do |raw_pos|
- rich_char = rich_line[rich_pos]
-
- # The raw and rich lines are the same except for HTML tags,
- # so skip over any `<...>` segment
- while rich_char == '<'
- until rich_char == '>'
- rich_pos += 1
- rich_char = rich_line[rich_pos]
- end
-
- rich_pos += 1
- rich_char = rich_line[rich_pos]
- end
-
- # multi-char HTML entities in the rich line correspond to a single character in the raw line
- if rich_char == '&'
- multichar_mapping = [rich_pos]
- until rich_char == ';'
- rich_pos += 1
- multichar_mapping << rich_pos
- rich_char = rich_line[rich_pos]
- end
-
- mapping[raw_pos] = multichar_mapping
- else
- mapping[raw_pos] = rich_pos
- end
-
- rich_pos += 1
- end
-
- mapping
- end
- end
-
- # Takes an array of integers, and returns an array of ranges covering the same integers
- def collapse_ranges(positions)
- return [] if positions.empty?
- ranges = []
-
- start = prev = positions[0]
- range = start..prev
- positions[1..-1].each do |pos|
- if pos == prev + 1
- range = start..pos
- prev = pos
- else
- ranges << range
- start = prev = pos
- range = start..prev
- end
- end
- ranges << range
-
- ranges
- end
-
- # Inserts tags around the characters identified by the given range
- def insert_around_range(text, range, before, after, offset = 0)
- # Just to be sure
- return offset if offset + range.end + 1 > text.length
-
- text.insert(offset + range.begin, before)
- offset += before.length
-
- text.insert(offset + range.end + 1, after)
- offset += after.length
-
- offset
- end
end
end
end
diff --git a/lib/gitlab/diff/position_tracer.rb b/lib/gitlab/diff/position_tracer.rb
index c7542a8fabc..e89ff238ec7 100644
--- a/lib/gitlab/diff/position_tracer.rb
+++ b/lib/gitlab/diff/position_tracer.rb
@@ -16,7 +16,7 @@ module Gitlab
end
def trace(old_position)
- return unless old_diff_refs.complete? && new_diff_refs.complete?
+ return unless old_diff_refs&.complete? && new_diff_refs&.complete?
return unless old_position.diff_refs == old_diff_refs
# Suppose we have an MR with source branch `feature` and target branch `master`.
diff --git a/lib/gitlab/ee_compat_check.rb b/lib/gitlab/ee_compat_check.rb
index 496ee0bdcb0..38e27513281 100644
--- a/lib/gitlab/ee_compat_check.rb
+++ b/lib/gitlab/ee_compat_check.rb
@@ -131,10 +131,12 @@ module Gitlab
def check_patch(patch_path)
step("Checking out master", %w[git checkout master])
step("Resetting to latest master", %w[git reset --hard origin/master])
+ step("Fetching CE/#{ce_branch}", %W[git fetch #{CE_REPO} #{ce_branch}])
step(
"Checking if #{patch_path} applies cleanly to EE/master",
%W[git apply --check --3way #{patch_path}]
) do |output, status|
+ puts output
unless status.zero?
@failed_files = output.lines.reduce([]) do |memo, line|
if line.start_with?('error: patch failed:')
@@ -310,6 +312,17 @@ module Gitlab
Resolve them, stage the changes and commit them.
+ If the patch couldn't be applied cleanly, use the following command:
+
+ # In the EE repo
+ $ git apply --reject path/to/#{ce_branch}.patch
+
+ This option makes git apply the parts of the patch that are applicable,
+ and leave the rejected hunks in corresponding `.rej` files.
+ You can then resolve the conflicts highlighted in `.rej` by
+ manually applying the correct diff from the `.rej` file to the file with conflicts.
+ When finished, you can delete the `.rej` files and commit your changes.
+
⚠️ Don't forget to push your branch to gitlab-ee:
# In the EE repo
diff --git a/lib/gitlab/etag_caching/router.rb b/lib/gitlab/etag_caching/router.rb
index 692c909d838..ba31041d0c1 100644
--- a/lib/gitlab/etag_caching/router.rb
+++ b/lib/gitlab/etag_caching/router.rb
@@ -7,8 +7,8 @@ module Gitlab
# - Don't contain a reserved word (expect for the words used in the
# regex itself)
# - Ending in `noteable/issue/<id>/notes` for the `issue_notes` route
- # - Ending in `issues/id`/rendered_title` for the `issue_title` route
- USED_IN_ROUTES = %w[noteable issue notes issues rendered_title
+ # - Ending in `issues/id`/realtime_changes` for the `issue_title` route
+ USED_IN_ROUTES = %w[noteable issue notes issues realtime_changes
commit pipelines merge_requests new].freeze
RESERVED_WORDS = DynamicPathValidator::WILDCARD_ROUTES - USED_IN_ROUTES
RESERVED_WORDS_REGEX = Regexp.union(*RESERVED_WORDS)
@@ -18,7 +18,7 @@ module Gitlab
'issue_notes'
),
Gitlab::EtagCaching::Router::Route.new(
- %r(^(?!.*(#{RESERVED_WORDS_REGEX})).*/issues/\d+/rendered_title\z),
+ %r(^(?!.*(#{RESERVED_WORDS_REGEX})).*/issues/\d+/realtime_changes\z),
'issue_title'
),
Gitlab::EtagCaching::Router::Route.new(
@@ -40,7 +40,7 @@ module Gitlab
Gitlab::EtagCaching::Router::Route.new(
%r(^(?!.*(#{RESERVED_WORDS})).*/pipelines/\d+\.json\z),
'project_pipeline'
- ),
+ )
].freeze
def self.match(env)
diff --git a/lib/gitlab/file_detector.rb b/lib/gitlab/file_detector.rb
index c9ca4cadd1c..a8cb7fc3fe7 100644
--- a/lib/gitlab/file_detector.rb
+++ b/lib/gitlab/file_detector.rb
@@ -5,15 +5,33 @@ module Gitlab
# a README or a CONTRIBUTING file.
module FileDetector
PATTERNS = {
+ # Project files
readme: /\Areadme/i,
changelog: /\A(changelog|history|changes|news)/i,
license: /\A(licen[sc]e|copying)(\..+|\z)/i,
contributing: /\Acontributing/i,
version: 'version',
+ avatar: /\Alogo\.(png|jpg|gif)\z/,
+
+ # Configuration files
gitignore: '.gitignore',
koding: '.koding.yml',
gitlab_ci: '.gitlab-ci.yml',
- avatar: /\Alogo\.(png|jpg|gif)\z/
+ route_map: 'route-map.yml',
+
+ # Dependency files
+ cartfile: /\ACartfile/,
+ composer_json: 'composer.json',
+ gemfile: /\A(Gemfile|gems\.rb)\z/,
+ gemfile_lock: 'Gemfile.lock',
+ gemspec: /\.gemspec\z/,
+ godeps_json: 'Godeps.json',
+ package_json: 'package.json',
+ podfile: 'Podfile',
+ podspec_json: /\.podspec\.json\z/,
+ podspec: /\.podspec\z/,
+ requirements_txt: /requirements\.txt\z/,
+ yarn_lock: 'yarn.lock'
}.freeze
# Returns an Array of file types based on the given paths.
diff --git a/lib/gitlab/file_finder.rb b/lib/gitlab/file_finder.rb
new file mode 100644
index 00000000000..093d9ed8092
--- /dev/null
+++ b/lib/gitlab/file_finder.rb
@@ -0,0 +1,32 @@
+# This class finds files in a repository by name and content
+# the result is joined and sorted by file name
+module Gitlab
+ class FileFinder
+ BATCH_SIZE = 100
+
+ attr_reader :project, :ref
+
+ def initialize(project, ref)
+ @project = project
+ @ref = ref
+ end
+
+ def find(query)
+ blobs = project.repository.search_files_by_content(query, ref).first(BATCH_SIZE)
+ found_file_names = Set.new
+
+ results = blobs.map do |blob|
+ blob = Gitlab::ProjectSearchResults.parse_search_result(blob)
+ found_file_names << blob.filename
+
+ [blob.filename, blob]
+ end
+
+ project.repository.search_files_by_name(query, ref).first(BATCH_SIZE).each do |filename|
+ results << [filename, OpenStruct.new(ref: ref)] unless found_file_names.include?(filename)
+ end
+
+ results.sort_by(&:first)
+ end
+ end
+end
diff --git a/lib/gitlab/git/blob.rb b/lib/gitlab/git/blob.rb
index 12458f9f410..c1b31618e0d 100644
--- a/lib/gitlab/git/blob.rb
+++ b/lib/gitlab/git/blob.rb
@@ -90,7 +90,7 @@ module Gitlab
name: blob_entry[:name],
data: '',
path: path,
- commit_id: sha,
+ commit_id: sha
)
end
end
diff --git a/lib/gitlab/git/branch.rb b/lib/gitlab/git/branch.rb
index 586380da94a..124526e4b59 100644
--- a/lib/gitlab/git/branch.rb
+++ b/lib/gitlab/git/branch.rb
@@ -1,6 +1,40 @@
module Gitlab
module Git
class Branch < Ref
+ def initialize(repository, name, target)
+ if target.is_a?(Gitaly::FindLocalBranchResponse)
+ target = target_from_gitaly_local_branches_response(target)
+ end
+
+ super(repository, name, target)
+ end
+
+ def target_from_gitaly_local_branches_response(response)
+ # Git messages have no encoding enforcements. However, in the UI we only
+ # handle UTF-8, so basically we cross our fingers that the message force
+ # encoded to UTF-8 is readable.
+ message = response.commit_subject.dup.force_encoding('UTF-8')
+
+ # NOTE: For ease of parsing in Gitaly, we have only the subject of
+ # the commit and not the full message. This is ok, since all the
+ # code that uses `local_branches` only cares at most about the
+ # commit message.
+ # TODO: Once gitaly "takes over" Rugged consider separating the
+ # subject from the message to make it clearer when there's one
+ # available but not the other.
+ hash = {
+ id: response.commit_id,
+ message: message,
+ authored_date: Time.at(response.commit_author.date.seconds),
+ author_name: response.commit_author.name,
+ author_email: response.commit_author.email,
+ committed_date: Time.at(response.commit_committer.date.seconds),
+ committer_name: response.commit_committer.name,
+ committer_email: response.commit_committer.email
+ }
+
+ Gitlab::Git::Commit.decorate(hash)
+ end
end
end
end
diff --git a/lib/gitlab/git/commit.rb b/lib/gitlab/git/commit.rb
index f9a9b767ef4..297531db4cc 100644
--- a/lib/gitlab/git/commit.rb
+++ b/lib/gitlab/git/commit.rb
@@ -19,13 +19,7 @@ module Gitlab
def ==(other)
return false unless other.is_a?(Gitlab::Git::Commit)
- methods = [:message, :parent_ids, :authored_date, :author_name,
- :author_email, :committed_date, :committer_name,
- :committer_email]
-
- methods.all? do |method|
- send(method) == other.send(method)
- end
+ id && id == other.id
end
class << self
@@ -55,6 +49,7 @@ module Gitlab
# Commit.find(repo, 'master')
#
def find(repo, commit_id = "HEAD")
+ return commit_id if commit_id.is_a?(Gitlab::Git::Commit)
return decorate(commit_id) if commit_id.is_a?(Rugged::Commit)
obj = if commit_id.is_a?(String)
diff --git a/lib/gitlab/git/diff.rb b/lib/gitlab/git/diff.rb
index 019be151353..31d1b66b4f7 100644
--- a/lib/gitlab/git/diff.rb
+++ b/lib/gitlab/git/diff.rb
@@ -183,6 +183,8 @@ module Gitlab
when Gitaly::CommitDiffResponse
init_from_gitaly(raw_diff)
prune_diff_if_eligible(collapse)
+ when Gitaly::CommitDelta
+ init_from_gitaly(raw_diff)
when nil
raise "Nil as raw diff passed"
else
@@ -278,15 +280,15 @@ module Gitlab
end
end
- def init_from_gitaly(diff_msg)
- @diff = diff_msg.raw_chunks.join
- @new_path = encode!(diff_msg.to_path.dup)
- @old_path = encode!(diff_msg.from_path.dup)
- @a_mode = diff_msg.old_mode.to_s(8)
- @b_mode = diff_msg.new_mode.to_s(8)
- @new_file = diff_msg.from_id == BLANK_SHA
- @renamed_file = diff_msg.from_path != diff_msg.to_path
- @deleted_file = diff_msg.to_id == BLANK_SHA
+ def init_from_gitaly(msg)
+ @diff = msg.raw_chunks.join if msg.respond_to?(:raw_chunks)
+ @new_path = encode!(msg.to_path.dup)
+ @old_path = encode!(msg.from_path.dup)
+ @a_mode = msg.old_mode.to_s(8)
+ @b_mode = msg.new_mode.to_s(8)
+ @new_file = msg.from_id == BLANK_SHA
+ @renamed_file = msg.from_path != msg.to_path
+ @deleted_file = msg.to_id == BLANK_SHA
end
def prune_diff_if_eligible(collapse = false)
diff --git a/lib/gitlab/git/diff_collection.rb b/lib/gitlab/git/diff_collection.rb
index 4e45ec7c174..bcbad8ec829 100644
--- a/lib/gitlab/git/diff_collection.rb
+++ b/lib/gitlab/git/diff_collection.rb
@@ -15,7 +15,6 @@ module Gitlab
@safe_max_bytes = @safe_max_files * 5120 # Average 5 KB per file
@all_diffs = !!options.fetch(:all_diffs, false)
@no_collapse = !!options.fetch(:no_collapse, true)
- @deltas_only = !!options.fetch(:deltas_only, false)
@line_count = 0
@byte_count = 0
@@ -27,8 +26,6 @@ module Gitlab
if @populated
# @iterator.each is slower than just iterating the array in place
@array.each(&block)
- elsif @deltas_only
- each_delta(&block)
else
Gitlab::GitalyClient.migrate(:commit_raw_diffs) do
each_patch(&block)
@@ -81,14 +78,6 @@ module Gitlab
files >= @safe_max_files || @line_count > @safe_max_lines || @byte_count >= @safe_max_bytes
end
- def each_delta
- @iterator.each_delta.with_index do |delta, i|
- diff = Gitlab::Git::Diff.new(delta)
-
- yield @array[i] = diff
- end
- end
-
def each_patch
@iterator.each_with_index do |raw, i|
# First yield cached Diff instances from @array
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index 9ed12ead023..b9f1ac144b6 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -27,13 +27,15 @@ module Gitlab
# Rugged repo object
attr_reader :rugged
+ attr_reader :storage
+
# 'path' must be the path to a _bare_ git repository, e.g.
# /path/to/my-repo.git
- def initialize(repository_storage, relative_path)
- @repository_storage = repository_storage
+ def initialize(storage, relative_path)
+ @storage = storage
@relative_path = relative_path
- storage_path = Gitlab.config.repositories.storages[@repository_storage]['path']
+ storage_path = Gitlab.config.repositories.storages[@storage]['path']
@path = File.join(storage_path, @relative_path)
@name = @relative_path.split("/").last
@attributes = Gitlab::Git::Attributes.new(path)
@@ -78,14 +80,16 @@ module Gitlab
end
# Returns an Array of Branches
- def branches
- rugged.branches.map do |rugged_ref|
+ def branches(filter: nil, sort_by: nil)
+ branches = rugged.branches.each(filter).map do |rugged_ref|
begin
Gitlab::Git::Branch.new(self, rugged_ref.name, rugged_ref.target)
rescue Rugged::ReferenceError
# Omit invalid branch
end
- end.compact.sort_by(&:name)
+ end.compact
+
+ sort_branches(branches, sort_by)
end
def reload_rugged
@@ -106,15 +110,21 @@ module Gitlab
Gitlab::Git::Branch.new(self, rugged_ref.name, rugged_ref.target) if rugged_ref
end
- def local_branches
- rugged.branches.each(:local).map do |branch|
- Gitlab::Git::Branch.new(self, branch.name, branch.target)
+ def local_branches(sort_by: nil)
+ gitaly_migrate(:local_branches) do |is_enabled|
+ if is_enabled
+ gitaly_ref_client.local_branches(sort_by: sort_by).map do |gitaly_branch|
+ Gitlab::Git::Branch.new(self, gitaly_branch.name, gitaly_branch)
+ end
+ else
+ branches(filter: :local, sort_by: sort_by)
+ end
end
end
# Returns the number of valid branches
def branch_count
- Gitlab::GitalyClient.migrate(:branch_names) do |is_enabled|
+ gitaly_migrate(:branch_names) do |is_enabled|
if is_enabled
gitaly_ref_client.count_branch_names
else
@@ -133,7 +143,7 @@ module Gitlab
# Returns the number of valid tags
def tag_count
- Gitlab::GitalyClient.migrate(:tag_names) do |is_enabled|
+ gitaly_migrate(:tag_names) do |is_enabled|
if is_enabled
gitaly_ref_client.count_tag_names
else
@@ -258,7 +268,7 @@ module Gitlab
'RepoPath' => path,
'ArchivePrefix' => prefix,
'ArchivePath' => archive_file_path(prefix, storage_path, format),
- 'CommitId' => commit.id,
+ 'CommitId' => commit.id
}
end
@@ -469,19 +479,19 @@ module Gitlab
# Returns a RefName for a given SHA
def ref_name_for_sha(ref_path, sha)
- # NOTE: This feature is intentionally disabled until
- # https://gitlab.com/gitlab-org/gitaly/issues/180 is resolved
- # Gitlab::GitalyClient.migrate(:find_ref_name) do |is_enabled|
- # if is_enabled
- # gitaly_ref_client.find_ref_name(sha, ref_path)
- # else
- args = %W(#{Gitlab.config.git.bin_path} for-each-ref --count=1 #{ref_path} --contains #{sha})
+ raise ArgumentError, "sha can't be empty" unless sha.present?
+
+ gitaly_migrate(:find_ref_name) do |is_enabled|
+ if is_enabled
+ gitaly_ref_client.find_ref_name(sha, ref_path)
+ else
+ args = %W(#{Gitlab.config.git.bin_path} for-each-ref --count=1 #{ref_path} --contains #{sha})
- # Not found -> ["", 0]
- # Found -> ["b8d95eb4969eefacb0a58f6a28f6803f8070e7b9 commit\trefs/environments/production/77\n", 0]
- Gitlab::Popen.popen(args, @path).first.split.last
- # end
- # end
+ # Not found -> ["", 0]
+ # Found -> ["b8d95eb4969eefacb0a58f6a28f6803f8070e7b9 commit\trefs/environments/production/77\n", 0]
+ Gitlab::Popen.popen(args, @path).first.split.last
+ end
+ end
end
# Returns commits collection
@@ -965,11 +975,7 @@ module Gitlab
end
def gitaly_repository
- Gitlab::GitalyClient::Util.repository(@repository_storage, @relative_path)
- end
-
- def gitaly_channel
- Gitlab::GitalyClient.get_channel(@repository_storage)
+ Gitlab::GitalyClient::Util.repository(@storage, @relative_path)
end
private
@@ -1204,6 +1210,23 @@ module Gitlab
diff.each_patch
end
+ def sort_branches(branches, sort_by)
+ case sort_by
+ when 'name'
+ branches.sort_by(&:name)
+ when 'updated_desc'
+ branches.sort do |a, b|
+ b.dereferenced_target.committed_date <=> a.dereferenced_target.committed_date
+ end
+ when 'updated_asc'
+ branches.sort do |a, b|
+ a.dereferenced_target.committed_date <=> b.dereferenced_target.committed_date
+ end
+ else
+ branches
+ end
+ end
+
def gitaly_ref_client
@gitaly_ref_client ||= Gitlab::GitalyClient::Ref.new(self)
end
diff --git a/lib/gitlab/git/tree.rb b/lib/gitlab/git/tree.rb
index b722d8a9f56..d41256d9a84 100644
--- a/lib/gitlab/git/tree.rb
+++ b/lib/gitlab/git/tree.rb
@@ -35,7 +35,7 @@ module Gitlab
type: entry[:type],
mode: entry[:filemode].to_s(8),
path: path ? File.join(path, entry[:name]) : entry[:name],
- commit_id: sha,
+ commit_id: sha
)
end
end
diff --git a/lib/gitlab/git_post_receive.rb b/lib/gitlab/git_post_receive.rb
index 0e14253ab4e..742118b76a8 100644
--- a/lib/gitlab/git_post_receive.rb
+++ b/lib/gitlab/git_post_receive.rb
@@ -13,6 +13,16 @@ module Gitlab
super(identifier, project, revision)
end
+ def changes_refs
+ return enum_for(:changes_refs) unless block_given?
+
+ changes.each do |change|
+ oldrev, newrev, ref = change.strip.split(' ')
+
+ yield oldrev, newrev, ref
+ end
+ end
+
private
def deserialize_changes(changes)
diff --git a/lib/gitlab/gitaly_client.rb b/lib/gitlab/gitaly_client.rb
index c69676a1dac..72466700c05 100644
--- a/lib/gitlab/gitaly_client.rb
+++ b/lib/gitlab/gitaly_client.rb
@@ -4,56 +4,42 @@ module Gitlab
module GitalyClient
SERVER_VERSION_FILE = 'GITALY_SERVER_VERSION'.freeze
- # This function is not thread-safe because it updates Hashes in instance variables.
- def self.configure_channels
- @addresses = {}
- @channels = {}
- Gitlab.config.repositories.storages.each do |name, params|
- address = params['gitaly_address']
- unless address.present?
- raise "storage #{name.inspect} is missing a gitaly_address"
- end
+ MUTEX = Mutex.new
+ private_constant :MUTEX
- unless URI(address).scheme.in?(%w(tcp unix))
- raise "Unsupported Gitaly address: #{address.inspect} does not use URL scheme 'tcp' or 'unix'"
+ def self.stub(name, storage)
+ MUTEX.synchronize do
+ @stubs ||= {}
+ @stubs[storage] ||= {}
+ @stubs[storage][name] ||= begin
+ klass = Gitaly.const_get(name.to_s.camelcase.to_sym).const_get(:Stub)
+ addr = address(storage)
+ addr = addr.sub(%r{^tcp://}, '') if URI(addr).scheme == 'tcp'
+ klass.new(addr, :this_channel_is_insecure)
end
-
- @addresses[name] = address
- @channels[name] = new_channel(address)
end
end
- def self.new_channel(address)
- address = address.sub(%r{^tcp://}, '') if URI(address).scheme == 'tcp'
- # NOTE: When Gitaly runs on a Unix socket, permissions are
- # handled using the file system and no additional authentication is
- # required (therefore the :this_channel_is_insecure flag)
- # TODO: Add authentication support when Gitaly is running on a TCP socket.
- GRPC::Core::Channel.new(address, {}, :this_channel_is_insecure)
+ def self.clear_stubs!
+ MUTEX.synchronize do
+ @stubs = nil
+ end
end
- def self.get_channel(storage)
- if !Rails.env.production? && @channels.nil?
- # In development mode the Rails auto-loader may reset the instance
- # variables of this class. What we do here is not thread-safe. In normal
- # circumstances (including production) these instance variables have
- # been initialized from config/initializers.
- configure_channels
- end
+ def self.address(storage)
+ params = Gitlab.config.repositories.storages[storage]
+ raise "storage not found: #{storage.inspect}" if params.nil?
- @channels[storage]
- end
+ address = params['gitaly_address']
+ unless address.present?
+ raise "storage #{storage.inspect} is missing a gitaly_address"
+ end
- def self.get_address(storage)
- if !Rails.env.production? && @addresses.nil?
- # In development mode the Rails auto-loader may reset the instance
- # variables of this class. What we do here is not thread-safe. In normal
- # circumstances (including development) these instance variables have
- # been initialized from config/initializers.
- configure_channels
+ unless URI(address).scheme.in?(%w(tcp unix))
+ raise "Unsupported Gitaly address: #{address.inspect} does not use URL scheme 'tcp' or 'unix'"
end
- @addresses[storage]
+ address
end
def self.enabled?
diff --git a/lib/gitlab/gitaly_client/commit.rb b/lib/gitlab/gitaly_client/commit.rb
index 0b001a9903d..4491903d788 100644
--- a/lib/gitlab/gitaly_client/commit.rb
+++ b/lib/gitlab/gitaly_client/commit.rb
@@ -5,41 +5,55 @@ module Gitlab
# See http://stackoverflow.com/a/40884093/1856239 and https://github.com/git/git/blob/3ad8b5bf26362ac67c9020bf8c30eee54a84f56d/cache.h#L1011-L1012
EMPTY_TREE_ID = '4b825dc642cb6eb9a060e54bf8d69288fbee4904'.freeze
- attr_accessor :stub
-
def initialize(repository)
@gitaly_repo = repository.gitaly_repository
- @stub = Gitaly::Commit::Stub.new(nil, nil, channel_override: repository.gitaly_channel)
+ @repository = repository
end
def is_ancestor(ancestor_id, child_id)
+ stub = GitalyClient.stub(:commit, @repository.storage)
request = Gitaly::CommitIsAncestorRequest.new(
repository: @gitaly_repo,
ancestor_id: ancestor_id,
child_id: child_id
)
- @stub.commit_is_ancestor(request).value
+ stub.commit_is_ancestor(request).value
+ end
+
+ def diff_from_parent(commit, options = {})
+ request_params = commit_diff_request_params(commit, options)
+ request_params[:ignore_whitespace_change] = options.fetch(:ignore_whitespace_change, false)
+
+ response = diff_service_stub.commit_diff(Gitaly::CommitDiffRequest.new(request_params))
+ Gitlab::Git::DiffCollection.new(response, options)
end
- class << self
- def diff_from_parent(commit, options = {})
- repository = commit.project.repository
- gitaly_repo = repository.gitaly_repository
- stub = Gitaly::Diff::Stub.new(nil, nil, channel_override: repository.gitaly_channel)
- parent = commit.parents[0]
- parent_id = parent ? parent.id : EMPTY_TREE_ID
- request = Gitaly::CommitDiffRequest.new(
- repository: gitaly_repo,
- left_commit_id: parent_id,
- right_commit_id: commit.id,
- ignore_whitespace_change: options.fetch(:ignore_whitespace_change, false),
- paths: options.fetch(:paths, []),
- )
-
- Gitlab::Git::DiffCollection.new(stub.commit_diff(request), options)
+ def commit_deltas(commit)
+ request_params = commit_diff_request_params(commit)
+
+ response = diff_service_stub.commit_delta(Gitaly::CommitDeltaRequest.new(request_params))
+ response.flat_map do |msg|
+ msg.deltas.map { |d| Gitlab::Git::Diff.new(d) }
end
end
+
+ private
+
+ def commit_diff_request_params(commit, options = {})
+ parent_id = commit.parents[0]&.id || EMPTY_TREE_ID
+
+ {
+ repository: @gitaly_repo,
+ left_commit_id: parent_id,
+ right_commit_id: commit.id,
+ paths: options.fetch(:paths, [])
+ }
+ end
+
+ def diff_service_stub
+ GitalyClient.stub(:diff, @repository.storage)
+ end
end
end
end
diff --git a/lib/gitlab/gitaly_client/notifications.rb b/lib/gitlab/gitaly_client/notifications.rb
index a94a54883db..719554eac52 100644
--- a/lib/gitlab/gitaly_client/notifications.rb
+++ b/lib/gitlab/gitaly_client/notifications.rb
@@ -6,7 +6,7 @@ module Gitlab
# 'repository' is a Gitlab::Git::Repository
def initialize(repository)
@gitaly_repo = repository.gitaly_repository
- @stub = Gitaly::Notifications::Stub.new(nil, nil, channel_override: repository.gitaly_channel)
+ @stub = GitalyClient.stub(:notifications, repository.storage)
end
def post_receive
diff --git a/lib/gitlab/gitaly_client/ref.rb b/lib/gitlab/gitaly_client/ref.rb
index f6c77ef1a3e..227fe45642e 100644
--- a/lib/gitlab/gitaly_client/ref.rb
+++ b/lib/gitlab/gitaly_client/ref.rb
@@ -6,7 +6,7 @@ module Gitlab
# 'repository' is a Gitlab::Git::Repository
def initialize(repository)
@gitaly_repo = repository.gitaly_repository
- @stub = Gitaly::Ref::Stub.new(nil, nil, channel_override: repository.gitaly_channel)
+ @stub = GitalyClient.stub(:ref, repository.storage)
end
def default_branch_name
@@ -28,7 +28,7 @@ module Gitlab
def find_ref_name(commit_id, ref_prefix)
request = Gitaly::FindRefNameRequest.new(
- repository: @repository,
+ repository: @gitaly_repo,
commit_id: commit_id,
prefix: ref_prefix
)
@@ -44,6 +44,12 @@ module Gitlab
branch_names.count
end
+ def local_branches(sort_by: nil)
+ request = Gitaly::FindLocalBranchesRequest.new(repository: @gitaly_repo)
+ request.sort_by = sort_by_param(sort_by) if sort_by
+ consume_branches_response(stub.find_local_branches(request))
+ end
+
private
def consume_refs_response(response, prefix:)
@@ -51,6 +57,16 @@ module Gitlab
r.names.map { |name| name.sub(/\A#{Regexp.escape(prefix)}/, '') }
end
end
+
+ def sort_by_param(sort_by)
+ enum_value = Gitaly::FindLocalBranchesRequest::SortBy.resolve(sort_by.upcase.to_sym)
+ raise ArgumentError, "Invalid sort_by key `#{sort_by}`" unless enum_value
+ enum_value
+ end
+
+ def consume_branches_response(response)
+ response.flat_map { |r| r.branches }
+ end
end
end
end
diff --git a/lib/gitlab/gitaly_client/util.rb b/lib/gitlab/gitaly_client/util.rb
index 4acd297f5cb..86d055d3533 100644
--- a/lib/gitlab/gitaly_client/util.rb
+++ b/lib/gitlab/gitaly_client/util.rb
@@ -6,7 +6,7 @@ module Gitlab
Gitaly::Repository.new(
path: File.join(Gitlab.config.repositories.storages[repository_storage]['path'], relative_path),
storage_name: repository_storage,
- relative_path: relative_path,
+ relative_path: relative_path
)
end
end
diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb
index 26473f99bc3..6200bd460ea 100644
--- a/lib/gitlab/gon_helper.rb
+++ b/lib/gitlab/gon_helper.rb
@@ -17,6 +17,7 @@ module Gitlab
gon.current_user_id = current_user.id
gon.current_username = current_user.username
gon.current_user_fullname = current_user.name
+ gon.current_user_avatar_url = current_user.avatar_url
end
end
end
diff --git a/lib/gitlab/highlight.rb b/lib/gitlab/highlight.rb
index d787d5db4a0..83bc230df3e 100644
--- a/lib/gitlab/highlight.rb
+++ b/lib/gitlab/highlight.rb
@@ -13,6 +13,8 @@ module Gitlab
highlight(file_name, blob.data, repository: repository).lines.map!(&:html_safe)
end
+ attr_reader :blob_name
+
def initialize(blob_name, blob_content, repository: nil)
@formatter = Rouge::Formatters::HTMLGitlab
@repository = repository
@@ -21,16 +23,9 @@ module Gitlab
end
def highlight(text, continue: true, plain: false)
- if plain
- hl_lexer = Rouge::Lexers::PlainText
- continue = false
- else
- hl_lexer = self.lexer
- end
-
- @formatter.format(hl_lexer.lex(text, continue: continue), tag: hl_lexer.tag).html_safe
- rescue
- @formatter.format(Rouge::Lexers::PlainText.lex(text)).html_safe
+ highlighted_text = highlight_text(text, continue: continue, plain: plain)
+ highlighted_text = link_dependencies(text, highlighted_text) if blob_name
+ highlighted_text
end
def lexer
@@ -50,5 +45,27 @@ module Gitlab
Rouge::Lexer.find_fancy(language_name)
end
+
+ def highlight_text(text, continue: true, plain: false)
+ if plain
+ highlight_plain(text)
+ else
+ highlight_rich(text, continue: continue)
+ end
+ end
+
+ def highlight_plain(text)
+ @formatter.format(Rouge::Lexers::PlainText.lex(text)).html_safe
+ end
+
+ def highlight_rich(text, continue: true)
+ @formatter.format(lexer.lex(text, continue: continue), tag: lexer.tag).html_safe
+ rescue
+ highlight_plain(text)
+ end
+
+ def link_dependencies(text, highlighted_text)
+ Gitlab::DependencyLinker.link(blob_name, text, highlighted_text)
+ end
end
end
diff --git a/lib/gitlab/kubernetes.rb b/lib/gitlab/kubernetes.rb
index 3a7af363548..4a6091488c8 100644
--- a/lib/gitlab/kubernetes.rb
+++ b/lib/gitlab/kubernetes.rb
@@ -38,7 +38,7 @@ module Gitlab
url: container_exec_url(api_url, namespace, pod_name, container["name"]),
subprotocols: ['channel.k8s.io'],
headers: Hash.new { |h, k| h[k] = [] },
- created_at: created_at,
+ created_at: created_at
}
end
end
@@ -64,7 +64,7 @@ module Gitlab
tty: true,
stdin: true,
stdout: true,
- stderr: true,
+ stderr: true
}.to_query + '&' + EXEC_COMMAND
case url.scheme
diff --git a/lib/gitlab/ldap/config.rb b/lib/gitlab/ldap/config.rb
index 46deea3cc9f..6fdf68641e2 100644
--- a/lib/gitlab/ldap/config.rb
+++ b/lib/gitlab/ldap/config.rb
@@ -39,7 +39,7 @@ module Gitlab
def adapter_options
opts = base_options.merge(
- encryption: encryption,
+ encryption: encryption
)
opts.merge!(auth_options) if has_auth?
diff --git a/lib/gitlab/metrics.rb b/lib/gitlab/metrics.rb
index c6dfa4ad9bd..cb8db2f1e9f 100644
--- a/lib/gitlab/metrics.rb
+++ b/lib/gitlab/metrics.rb
@@ -49,6 +49,9 @@ module Gitlab
end
end
end
+ rescue Errno::EADDRNOTAVAIL, SocketError => ex
+ Gitlab::EnvironmentLogger.error('Cannot resolve InfluxDB address. GitLab Performance Monitoring will not work.')
+ Gitlab::EnvironmentLogger.error(ex)
end
def self.prepare_metrics(metrics)
diff --git a/lib/gitlab/other_markup.rb b/lib/gitlab/other_markup.rb
index c2adc9aa10b..31a24460f0f 100644
--- a/lib/gitlab/other_markup.rb
+++ b/lib/gitlab/other_markup.rb
@@ -5,12 +5,12 @@ module Gitlab
#
# input - the source text in a markup format
#
- def self.render(file_name, input)
+ def self.render(file_name, input, context)
html = GitHub::Markup.render(file_name, input).
force_encoding(input.encoding)
+ context[:pipeline] = :markup
- filter = Banzai::Filter::SanitizationFilter.new(html)
- html = filter.call.to_s
+ html = Banzai.render(html, context)
html.html_safe
end
diff --git a/lib/gitlab/project_search_results.rb b/lib/gitlab/project_search_results.rb
index 0b8959f2fb9..561aa9e162c 100644
--- a/lib/gitlab/project_search_results.rb
+++ b/lib/gitlab/project_search_results.rb
@@ -82,26 +82,14 @@ module Gitlab
private
def blobs
- @blobs ||= begin
- blobs = project.repository.search_files_by_content(query, repository_ref).first(100)
- found_file_names = Set.new
-
- results = blobs.map do |blob|
- blob = self.class.parse_search_result(blob)
- found_file_names << blob.filename
-
- [blob.filename, blob]
- end
-
- project.repository.search_files_by_name(query, repository_ref).first(100).each do |filename|
- results << [filename, nil] unless found_file_names.include?(filename)
- end
+ return [] unless Ability.allowed?(@current_user, :download_code, @project)
- results.sort_by(&:first)
- end
+ @blobs ||= Gitlab::FileFinder.new(project, repository_ref).find(query)
end
def wiki_blobs
+ return [] unless Ability.allowed?(@current_user, :read_wiki, @project)
+
@wiki_blobs ||= begin
if project.wiki_enabled? && query.present?
project_wiki = ProjectWiki.new(project)
diff --git a/lib/gitlab/prometheus/queries/base_query.rb b/lib/gitlab/prometheus/queries/base_query.rb
new file mode 100644
index 00000000000..2a2eb4ae57f
--- /dev/null
+++ b/lib/gitlab/prometheus/queries/base_query.rb
@@ -0,0 +1,26 @@
+module Gitlab
+ module Prometheus
+ module Queries
+ class BaseQuery
+ attr_accessor :client
+ delegate :query_range, :query, to: :client, prefix: true
+
+ def raw_memory_usage_query(environment_slug)
+ %{avg(container_memory_usage_bytes{container_name!="POD",environment="#{environment_slug}"}) / 2^20}
+ end
+
+ def raw_cpu_usage_query(environment_slug)
+ %{avg(rate(container_cpu_usage_seconds_total{container_name!="POD",environment="#{environment_slug}"}[2m])) * 100}
+ end
+
+ def initialize(client)
+ @client = client
+ end
+
+ def query(*args)
+ raise NotImplementedError
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/prometheus/queries/deployment_query.rb b/lib/gitlab/prometheus/queries/deployment_query.rb
new file mode 100644
index 00000000000..2cc08731f8d
--- /dev/null
+++ b/lib/gitlab/prometheus/queries/deployment_query.rb
@@ -0,0 +1,26 @@
+module Gitlab::Prometheus::Queries
+ class DeploymentQuery < BaseQuery
+ def query(deployment_id)
+ deployment = Deployment.find_by(id: deployment_id)
+ environment_slug = deployment.environment.slug
+
+ memory_query = raw_memory_usage_query(environment_slug)
+ memory_avg_query = %{avg(avg_over_time(container_memory_usage_bytes{container_name!="POD",environment="#{environment_slug}"}[30m]))}
+ cpu_query = raw_cpu_usage_query(environment_slug)
+ cpu_avg_query = %{avg(rate(container_cpu_usage_seconds_total{container_name!="POD",environment="#{environment_slug}"}[30m])) * 100}
+
+ timeframe_start = (deployment.created_at - 30.minutes).to_f
+ timeframe_end = (deployment.created_at + 30.minutes).to_f
+
+ {
+ memory_values: client_query_range(memory_query, start: timeframe_start, stop: timeframe_end),
+ memory_before: client_query(memory_avg_query, time: deployment.created_at.to_f),
+ memory_after: client_query(memory_avg_query, time: timeframe_end),
+
+ cpu_values: client_query_range(cpu_query, start: timeframe_start, stop: timeframe_end),
+ cpu_before: client_query(cpu_avg_query, time: deployment.created_at.to_f),
+ cpu_after: client_query(cpu_avg_query, time: timeframe_end)
+ }
+ end
+ end
+end
diff --git a/lib/gitlab/prometheus/queries/environment_query.rb b/lib/gitlab/prometheus/queries/environment_query.rb
new file mode 100644
index 00000000000..01d756d7284
--- /dev/null
+++ b/lib/gitlab/prometheus/queries/environment_query.rb
@@ -0,0 +1,20 @@
+module Gitlab::Prometheus::Queries
+ class EnvironmentQuery < BaseQuery
+ def query(environment_id)
+ environment = Environment.find_by(id: environment_id)
+ environment_slug = environment.slug
+ timeframe_start = 8.hours.ago.to_f
+ timeframe_end = Time.now.to_f
+
+ memory_query = raw_memory_usage_query(environment_slug)
+ cpu_query = raw_cpu_usage_query(environment_slug)
+
+ {
+ memory_values: client_query_range(memory_query, start: timeframe_start, stop: timeframe_end),
+ memory_current: client_query(memory_query, time: timeframe_end),
+ cpu_values: client_query_range(cpu_query, start: timeframe_start, stop: timeframe_end),
+ cpu_current: client_query(cpu_query, time: timeframe_end)
+ }
+ end
+ end
+end
diff --git a/lib/gitlab/prometheus.rb b/lib/gitlab/prometheus_client.rb
index 37125980b1c..5b51a1779dd 100644
--- a/lib/gitlab/prometheus.rb
+++ b/lib/gitlab/prometheus_client.rb
@@ -2,7 +2,7 @@ module Gitlab
PrometheusError = Class.new(StandardError)
# Helper methods to interact with Prometheus network services & resources
- class Prometheus
+ class PrometheusClient
attr_reader :api_url
def initialize(api_url:)
@@ -15,7 +15,7 @@ module Gitlab
def query(query, time: Time.now)
get_result('vector') do
- json_api_get('query', query: query, time: time.utc.to_f)
+ json_api_get('query', query: query, time: time.to_f)
end
end
diff --git a/lib/gitlab/sentry.rb b/lib/gitlab/sentry.rb
index 117fc508135..2442c2ded3b 100644
--- a/lib/gitlab/sentry.rb
+++ b/lib/gitlab/sentry.rb
@@ -11,7 +11,7 @@ module Gitlab
Raven.user_context(
id: current_user.id,
email: current_user.email,
- username: current_user.username,
+ username: current_user.username
)
end
end
diff --git a/lib/gitlab/string_range_marker.rb b/lib/gitlab/string_range_marker.rb
new file mode 100644
index 00000000000..94fba0a221a
--- /dev/null
+++ b/lib/gitlab/string_range_marker.rb
@@ -0,0 +1,102 @@
+module Gitlab
+ class StringRangeMarker
+ attr_accessor :raw_line, :rich_line
+
+ def initialize(raw_line, rich_line = raw_line)
+ @raw_line = raw_line
+ @rich_line = ERB::Util.html_escape(rich_line)
+ end
+
+ def mark(marker_ranges)
+ return rich_line unless marker_ranges
+
+ rich_marker_ranges = []
+ marker_ranges.each do |range|
+ # Map the inline-diff range based on the raw line to character positions in the rich line
+ rich_positions = position_mapping[range].flatten
+ # Turn the array of character positions into ranges
+ rich_marker_ranges.concat(collapse_ranges(rich_positions))
+ end
+
+ offset = 0
+ # Mark each range
+ rich_marker_ranges.each_with_index do |range, i|
+ offset_range = (range.begin + offset)..(range.end + offset)
+ original_text = rich_line[offset_range]
+
+ text = yield(original_text, left: i == 0, right: i == rich_marker_ranges.length - 1)
+
+ rich_line[offset_range] = text
+
+ offset += text.length - original_text.length
+ end
+
+ rich_line.html_safe
+ end
+
+ private
+
+ # Mapping of character positions in the raw line, to the rich (highlighted) line
+ def position_mapping
+ @position_mapping ||= begin
+ mapping = []
+ rich_pos = 0
+ (0..raw_line.length).each do |raw_pos|
+ rich_char = rich_line[rich_pos]
+
+ # The raw and rich lines are the same except for HTML tags,
+ # so skip over any `<...>` segment
+ while rich_char == '<'
+ until rich_char == '>'
+ rich_pos += 1
+ rich_char = rich_line[rich_pos]
+ end
+
+ rich_pos += 1
+ rich_char = rich_line[rich_pos]
+ end
+
+ # multi-char HTML entities in the rich line correspond to a single character in the raw line
+ if rich_char == '&'
+ multichar_mapping = [rich_pos]
+ until rich_char == ';'
+ rich_pos += 1
+ multichar_mapping << rich_pos
+ rich_char = rich_line[rich_pos]
+ end
+
+ mapping[raw_pos] = multichar_mapping
+ else
+ mapping[raw_pos] = rich_pos
+ end
+
+ rich_pos += 1
+ end
+
+ mapping
+ end
+ end
+
+ # Takes an array of integers, and returns an array of ranges covering the same integers
+ def collapse_ranges(positions)
+ return [] if positions.empty?
+ ranges = []
+
+ start = prev = positions[0]
+ range = start..prev
+ positions[1..-1].each do |pos|
+ if pos == prev + 1
+ range = start..pos
+ prev = pos
+ else
+ ranges << range
+ start = prev = pos
+ range = start..prev
+ end
+ end
+ ranges << range
+
+ ranges
+ end
+ end
+end
diff --git a/lib/gitlab/string_regex_marker.rb b/lib/gitlab/string_regex_marker.rb
new file mode 100644
index 00000000000..7ebf1c0428c
--- /dev/null
+++ b/lib/gitlab/string_regex_marker.rb
@@ -0,0 +1,13 @@
+module Gitlab
+ class StringRegexMarker < StringRangeMarker
+ def mark(regex, group: 0, &block)
+ regex_match = raw_line.match(regex)
+ return rich_line unless regex_match
+
+ begin_index, end_index = regex_match.offset(group)
+ name_range = begin_index..(end_index - 1)
+
+ super([name_range], &block)
+ end
+ end
+end
diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb
index 14d8e925d0e..bcba2e3e1b6 100644
--- a/lib/gitlab/usage_data.rb
+++ b/lib/gitlab/usage_data.rb
@@ -40,7 +40,6 @@ module Gitlab
projects_prometheus_active: PrometheusService.active.count,
protected_branches: ProtectedBranch.count,
releases: Release.count,
- services: Service.where(active: true).count,
snippets: Snippet.count,
todos: Todo.count,
uploads: Upload.count,
@@ -52,6 +51,7 @@ module Gitlab
def license_usage_data
usage_data = {
uuid: current_application_settings.uuid,
+ hostname: Gitlab.config.gitlab.host,
version: Gitlab::VERSION,
active_user_count: User.active.count,
recorded_at: Time.now,
diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb
index 8c5ad01e8c2..fe37e4da94f 100644
--- a/lib/gitlab/workhorse.rb
+++ b/lib/gitlab/workhorse.rb
@@ -22,11 +22,11 @@ module Gitlab
params = {
GL_ID: Gitlab::GlId.gl_id(user),
GL_REPOSITORY: Gitlab::GlRepository.gl_repository(project, is_wiki),
- RepoPath: repo_path,
+ RepoPath: repo_path
}
if Gitlab.config.gitaly.enabled
- address = Gitlab::GitalyClient.get_address(project.repository_storage)
+ address = Gitlab::GitalyClient.address(project.repository_storage)
params[:Repository] = repository.gitaly_repository.to_h
feature_enabled = case action.to_s
@@ -51,7 +51,7 @@ module Gitlab
{
StoreLFSPath: "#{Gitlab.config.lfs.storage_path}/tmp/upload",
LfsOid: oid,
- LfsSize: size,
+ LfsSize: size
}
end
@@ -62,7 +62,7 @@ module Gitlab
def send_git_blob(repository, blob)
params = {
'RepoPath' => repository.path_to_repo,
- 'BlobId' => blob.id,
+ 'BlobId' => blob.id
}
[
@@ -127,7 +127,7 @@ module Gitlab
'Subprotocols' => terminal[:subprotocols],
'Url' => terminal[:url],
'Header' => terminal[:headers],
- 'MaxSessionTime' => terminal[:max_session_time],
+ 'MaxSessionTime' => terminal[:max_session_time]
}
}
details['Terminal']['CAPem'] = terminal[:ca_pem] if terminal.has_key?(:ca_pem)
@@ -165,7 +165,7 @@ module Gitlab
encoded_message,
secret,
true,
- { iss: 'gitlab-workhorse', verify_iss: true, algorithm: 'HS256' },
+ { iss: 'gitlab-workhorse', verify_iss: true, algorithm: 'HS256' }
)
end
diff --git a/lib/tasks/gemojione.rake b/lib/tasks/gemojione.rake
index b5572a39d30..87ca39b079b 100644
--- a/lib/tasks/gemojione.rake
+++ b/lib/tasks/gemojione.rake
@@ -21,7 +21,7 @@ namespace :gemojione do
moji: emoji_hash['moji'],
description: emoji_hash['description'],
unicodeVersion: Gitlab::Emoji.emoji_unicode_version(name),
- digest: hash_digest,
+ digest: hash_digest
}
resultant_emoji_map[name] = entry
diff --git a/lib/tasks/gitlab/update_templates.rake b/lib/tasks/gitlab/update_templates.rake
index 1b04e1350ed..59c32bbe7a4 100644
--- a/lib/tasks/gitlab/update_templates.rake
+++ b/lib/tasks/gitlab/update_templates.rake
@@ -49,7 +49,7 @@ namespace :gitlab do
Template.new(
"https://gitlab.com/gitlab-org/Dockerfile.git",
/(\.{1,2}|LICENSE|CONTRIBUTING.md|\.Dockerfile)\z/
- ),
+ )
].freeze
def vendor_directory
diff --git a/lib/tasks/spec.rake b/lib/tasks/spec.rake
index 602c60be828..2eddcb3c777 100644
--- a/lib/tasks/spec.rake
+++ b/lib/tasks/spec.rake
@@ -60,7 +60,7 @@ desc "GitLab | Run specs"
task :spec do
cmds = [
%w(rake gitlab:setup),
- %w(rspec spec),
+ %w(rspec spec)
]
run_commands(cmds)
end
diff --git a/scripts/prepare_build.sh b/scripts/prepare_build.sh
index c727a0e2d88..03de59f27ad 100644
--- a/scripts/prepare_build.sh
+++ b/scripts/prepare_build.sh
@@ -4,9 +4,22 @@ export SETUP_DB=${SETUP_DB:-true}
export USE_BUNDLE_INSTALL=${USE_BUNDLE_INSTALL:-true}
export BUNDLE_INSTALL_FLAGS="--without production --jobs $(nproc) --path vendor --retry 3 --quiet"
+if [ "$USE_BUNDLE_INSTALL" != "false" ]; then
+ bundle install --clean $BUNDLE_INSTALL_FLAGS && bundle check
+fi
+
+# Only install knapsack after bundle install! Otherwise oddly some native
+# gems could not be found under some circumstance. No idea why, hours wasted.
+retry gem install knapsack fog-aws mime-types
+
+cp config/resque.yml.example config/resque.yml
+sed -i 's/localhost/redis/g' config/resque.yml
+
+cp config/gitlab.yml.example config/gitlab.yml
+
# Determine the database by looking at the job name.
-# For example, we'll get pg if the job is `rspec pg 19 20`
-export GITLAB_DATABASE=$(echo $CI_JOB_NAME | cut -f2 -d' ')
+# For example, we'll get pg if the job is `rspec-pg 19 20`
+export GITLAB_DATABASE=$(echo $CI_JOB_NAME | cut -f1 -d' ' | cut -f2 -d-)
# This would make the default database postgresql, and we could also use
# pg to mean postgresql.
@@ -24,19 +37,6 @@ else # Assume it's mysql
sed -i 's/# host:.*/host: mysql/g' config/database.yml
fi
-cp config/resque.yml.example config/resque.yml
-sed -i 's/localhost/redis/g' config/resque.yml
-
-cp config/gitlab.yml.example config/gitlab.yml
-
-if [ "$USE_BUNDLE_INSTALL" != "false" ]; then
- bundle install --clean $BUNDLE_INSTALL_FLAGS && bundle check
-fi
-
-# Only install knapsack after bundle install! Otherwise oddly some native
-# gems could not be found under some circumstance. No idea why, hours wasted.
-retry gem install knapsack fog-aws mime-types
-
if [ "$SETUP_DB" != "false" ]; then
bundle exec rake db:drop db:create db:schema:load db:migrate
diff --git a/scripts/trigger-build b/scripts/trigger-build
index 741e6361f01..565bc314ef1 100755
--- a/scripts/trigger-build
+++ b/scripts/trigger-build
@@ -8,7 +8,7 @@ params = {
"ref" => ENV["OMNIBUS_BRANCH"] || "master",
"token" => ENV["BUILD_TRIGGER_TOKEN"],
"variables[GITLAB_VERSION]" => ENV["CI_COMMIT_SHA"],
- "variables[ALTERNATIVE_SOURCES]" => true,
+ "variables[ALTERNATIVE_SOURCES]" => true
}
Dir.glob("*_VERSION").each do |version_file|
diff --git a/spec/bin/changelog_spec.rb b/spec/bin/changelog_spec.rb
index 7f4298db59f..91aff0db7cc 100644
--- a/spec/bin/changelog_spec.rb
+++ b/spec/bin/changelog_spec.rb
@@ -46,9 +46,7 @@ describe 'bin/changelog' do
it 'parses -h' do
expect do
- $stdout = StringIO.new
-
- described_class.parse(%w[foo -h bar])
+ expect { described_class.parse(%w[foo -h bar]) }.to output.to_stdout
end.to raise_error(SystemExit)
end
diff --git a/spec/controllers/admin/hooks_controller_spec.rb b/spec/controllers/admin/hooks_controller_spec.rb
new file mode 100644
index 00000000000..1d1070e90f4
--- /dev/null
+++ b/spec/controllers/admin/hooks_controller_spec.rb
@@ -0,0 +1,28 @@
+require 'spec_helper'
+
+describe Admin::HooksController do
+ let(:admin) { create(:admin) }
+
+ before do
+ sign_in(admin)
+ end
+
+ describe 'POST #create' do
+ it 'sets all parameters' do
+ hook_params = {
+ enable_ssl_verification: true,
+ push_events: true,
+ tag_push_events: true,
+ repository_update_events: true,
+ token: "TEST TOKEN",
+ url: "http://example.com"
+ }
+
+ post :create, hook: hook_params
+
+ expect(response).to have_http_status(302)
+ expect(SystemHook.all.size).to eq(1)
+ expect(SystemHook.first).to have_attributes(hook_params)
+ end
+ end
+end
diff --git a/spec/controllers/groups/milestones_controller_spec.rb b/spec/controllers/groups/milestones_controller_spec.rb
index 7cf2996ffd0..f3263bc177d 100644
--- a/spec/controllers/groups/milestones_controller_spec.rb
+++ b/spec/controllers/groups/milestones_controller_spec.rb
@@ -21,7 +21,6 @@ describe Groups::MilestonesController do
sign_in(user)
group.add_owner(user)
project.team << [user, :master]
- controller.instance_variable_set(:@group, group)
end
it_behaves_like 'milestone tabs'
@@ -29,7 +28,7 @@ describe Groups::MilestonesController do
describe "#create" do
it "creates group milestone with Chinese title" do
post :create,
- group_id: group.id,
+ group_id: group.to_param,
milestone: { project_ids: [project.id, project2.id], title: title }
expect(response).to redirect_to(group_milestone_path(group, title.to_slug.to_s, title: title))
@@ -37,9 +36,139 @@ describe Groups::MilestonesController do
end
it "redirects to new when there are no project ids" do
- post :create, group_id: group.id, milestone: { title: title, project_ids: [""] }
+ post :create, group_id: group.to_param, milestone: { title: title, project_ids: [""] }
expect(response).to render_template :new
expect(assigns(:milestone).errors).not_to be_nil
end
end
+
+ describe '#ensure_canonical_path' do
+ before do
+ sign_in(user)
+ end
+
+ context 'for a GET request' do
+ context 'when requesting the canonical path' do
+ context 'non-show path' do
+ context 'with exactly matching casing' do
+ it 'does not redirect' do
+ get :index, group_id: group.to_param
+
+ expect(response).not_to have_http_status(301)
+ end
+ end
+
+ context 'with different casing' do
+ it 'redirects to the correct casing' do
+ get :index, group_id: group.to_param.upcase
+
+ expect(response).to redirect_to(group_milestones_path(group.to_param))
+ expect(controller).not_to set_flash[:notice]
+ end
+ end
+ end
+
+ context 'show path' do
+ context 'with exactly matching casing' do
+ it 'does not redirect' do
+ get :show, group_id: group.to_param, id: title
+
+ expect(response).not_to have_http_status(301)
+ end
+ end
+
+ context 'with different casing' do
+ it 'redirects to the correct casing' do
+ get :show, group_id: group.to_param.upcase, id: title
+
+ expect(response).to redirect_to(group_milestone_path(group.to_param, title))
+ expect(controller).not_to set_flash[:notice]
+ end
+ end
+ end
+ end
+
+ context 'when requesting a redirected path' do
+ let(:redirect_route) { group.redirect_routes.create(path: 'old-path') }
+
+ it 'redirects to the canonical path' do
+ get :merge_requests, group_id: redirect_route.path, id: title
+
+ expect(response).to redirect_to(merge_requests_group_milestone_path(group.to_param, title))
+ expect(controller).to set_flash[:notice].to(group_moved_message(redirect_route, group))
+ end
+
+ context 'when the old group path is a substring of the scheme or host' do
+ let(:redirect_route) { group.redirect_routes.create(path: 'http') }
+
+ it 'does not modify the requested host' do
+ get :merge_requests, group_id: redirect_route.path, id: title
+
+ expect(response).to redirect_to(merge_requests_group_milestone_path(group.to_param, title))
+ expect(controller).to set_flash[:notice].to(group_moved_message(redirect_route, group))
+ end
+ end
+
+ context 'when the old group path is substring of groups' do
+ # I.e. /groups/oups should not become /grfoo/oups
+ let(:redirect_route) { group.redirect_routes.create(path: 'oups') }
+
+ it 'does not modify the /groups part of the path' do
+ get :merge_requests, group_id: redirect_route.path, id: title
+
+ expect(response).to redirect_to(merge_requests_group_milestone_path(group.to_param, title))
+ expect(controller).to set_flash[:notice].to(group_moved_message(redirect_route, group))
+ end
+ end
+
+ context 'when the old group path is substring of groups plus the new path' do
+ # I.e. /groups/oups/oup should not become /grfoos
+ let(:redirect_route) { group.redirect_routes.create(path: 'oups/oup') }
+
+ it 'does not modify the /groups part of the path' do
+ get :merge_requests, group_id: redirect_route.path, id: title
+
+ expect(response).to redirect_to(merge_requests_group_milestone_path(group.to_param, title))
+ expect(controller).to set_flash[:notice].to(group_moved_message(redirect_route, group))
+ end
+ end
+ end
+ end
+ end
+
+ context 'for a non-GET request' do
+ context 'when requesting the canonical path with different casing' do
+ it 'does not 404' do
+ post :create,
+ group_id: group.to_param,
+ milestone: { project_ids: [project.id, project2.id], title: title }
+
+ expect(response).not_to have_http_status(404)
+ end
+
+ it 'does not redirect to the correct casing' do
+ post :create,
+ group_id: group.to_param,
+ milestone: { project_ids: [project.id, project2.id], title: title }
+
+ expect(response).not_to have_http_status(301)
+ end
+ end
+
+ context 'when requesting a redirected path' do
+ let(:redirect_route) { group.redirect_routes.create(path: 'old-path') }
+
+ it 'returns not found' do
+ post :create,
+ group_id: redirect_route.path,
+ milestone: { project_ids: [project.id, project2.id], title: title }
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ def group_moved_message(redirect_route, group)
+ "Group '#{redirect_route.path}' was moved to '#{group.full_path}'. Please update any links and bookmarks that may still have the old path."
+ end
end
diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb
index 073b87a1cb4..4626f1ebc29 100644
--- a/spec/controllers/groups_controller_spec.rb
+++ b/spec/controllers/groups_controller_spec.rb
@@ -26,6 +26,41 @@ describe GroupsController do
end
end
+ describe 'GET #subgroups' do
+ let!(:public_subgroup) { create(:group, :public, parent: group) }
+ let!(:private_subgroup) { create(:group, :private, parent: group) }
+
+ context 'as a user' do
+ before do
+ sign_in(user)
+ end
+
+ it 'shows the public subgroups' do
+ get :subgroups, id: group.to_param
+
+ expect(assigns(:nested_groups)).to contain_exactly(public_subgroup)
+ end
+
+ context 'being member' do
+ it 'shows public and private subgroups the user is member of' do
+ private_subgroup.add_guest(user)
+
+ get :subgroups, id: group.to_param
+
+ expect(assigns(:nested_groups)).to contain_exactly(public_subgroup, private_subgroup)
+ end
+ end
+ end
+
+ context 'as a guest' do
+ it 'shows the public subgroups' do
+ get :subgroups, id: group.to_param
+
+ expect(assigns(:nested_groups)).to contain_exactly(public_subgroup)
+ end
+ end
+ end
+
describe 'GET #issues' do
let(:issue_1) { create(:issue, project: project) }
let(:issue_2) { create(:issue, project: project) }
@@ -33,7 +68,7 @@ describe GroupsController do
before do
create_list(:award_emoji, 3, awardable: issue_2)
create_list(:award_emoji, 2, awardable: issue_1)
- create_list(:award_emoji, 2, :downvote, awardable: issue_2,)
+ create_list(:award_emoji, 2, :downvote, awardable: issue_2)
sign_in(user)
end
@@ -49,26 +84,6 @@ describe GroupsController do
expect(assigns(:issues)).to eq [issue_2, issue_1]
end
end
-
- context 'when requesting the canonical path with different casing' do
- it 'redirects to the correct casing' do
- get :issues, id: group.to_param.upcase
-
- expect(response).to redirect_to(issues_group_path(group.to_param))
- expect(controller).not_to set_flash[:notice]
- end
- end
-
- context 'when requesting a redirected path' do
- let(:redirect_route) { group.redirect_routes.create(path: 'old-path') }
-
- it 'redirects to the canonical path' do
- get :issues, id: redirect_route.path
-
- expect(response).to redirect_to(issues_group_path(group.to_param))
- expect(controller).to set_flash[:notice].to(/moved/)
- end
- end
end
describe 'GET #merge_requests' do
@@ -94,26 +109,6 @@ describe GroupsController do
expect(assigns(:merge_requests)).to eq [merge_request_2, merge_request_1]
end
end
-
- context 'when requesting the canonical path with different casing' do
- it 'redirects to the correct casing' do
- get :merge_requests, id: group.to_param.upcase
-
- expect(response).to redirect_to(merge_requests_group_path(group.to_param))
- expect(controller).not_to set_flash[:notice]
- end
- end
-
- context 'when requesting a redirected path' do
- let(:redirect_route) { group.redirect_routes.create(path: 'old-path') }
-
- it 'redirects to the canonical path' do
- get :merge_requests, id: redirect_route.path
-
- expect(response).to redirect_to(merge_requests_group_path(group.to_param))
- expect(controller).to set_flash[:notice].to(/moved/)
- end
- end
end
describe 'DELETE #destroy' do
@@ -143,30 +138,6 @@ describe GroupsController do
expect(response).to redirect_to(root_path)
end
-
- context 'when requesting the canonical path with different casing' do
- it 'does not 404' do
- delete :destroy, id: group.to_param.upcase
-
- expect(response).not_to have_http_status(404)
- end
-
- it 'does not redirect to the correct casing' do
- delete :destroy, id: group.to_param.upcase
-
- expect(response).not_to redirect_to(group_path(group.to_param))
- end
- end
-
- context 'when requesting a redirected path' do
- let(:redirect_route) { group.redirect_routes.create(path: 'old-path') }
-
- it 'returns not found' do
- delete :destroy, id: redirect_route.path
-
- expect(response).to have_http_status(404)
- end
- end
end
end
@@ -189,29 +160,202 @@ describe GroupsController do
expect(assigns(:group).errors).not_to be_empty
expect(assigns(:group).path).not_to eq('new_path')
end
+ end
- context 'when requesting the canonical path with different casing' do
- it 'does not 404' do
- post :update, id: group.to_param.upcase, group: { path: 'new_path' }
+ describe '#ensure_canonical_path' do
+ before do
+ sign_in(user)
+ end
+
+ context 'for a GET request' do
+ context 'when requesting groups at the root path' do
+ before do
+ allow(request).to receive(:original_fullpath).and_return("/#{group_full_path}")
+ get :show, id: group_full_path
+ end
- expect(response).not_to have_http_status(404)
+ context 'when requesting the canonical path with different casing' do
+ let(:group_full_path) { group.to_param.upcase }
+
+ it 'redirects to the correct casing' do
+ expect(response).to redirect_to(group)
+ expect(controller).not_to set_flash[:notice]
+ end
+ end
+
+ context 'when requesting a redirected path' do
+ let(:redirect_route) { group.redirect_routes.create(path: 'old-path') }
+ let(:group_full_path) { redirect_route.path }
+
+ it 'redirects to the canonical path' do
+ expect(response).to redirect_to(group)
+ expect(controller).to set_flash[:notice].to(group_moved_message(redirect_route, group))
+ end
+
+ context 'when the old group path is a substring of the scheme or host' do
+ let(:redirect_route) { group.redirect_routes.create(path: 'http') }
+
+ it 'does not modify the requested host' do
+ expect(response).to redirect_to(group)
+ expect(controller).to set_flash[:notice].to(group_moved_message(redirect_route, group))
+ end
+ end
+
+ context 'when the old group path is substring of groups' do
+ # I.e. /groups/oups should not become /grfoo/oups
+ let(:redirect_route) { group.redirect_routes.create(path: 'oups') }
+
+ it 'does not modify the /groups part of the path' do
+ expect(response).to redirect_to(group)
+ expect(controller).to set_flash[:notice].to(group_moved_message(redirect_route, group))
+ end
+ end
+ end
end
- it 'does not redirect to the correct casing' do
- post :update, id: group.to_param.upcase, group: { path: 'new_path' }
+ context 'when requesting groups under the /groups path' do
+ context 'when requesting the canonical path' do
+ context 'non-show path' do
+ context 'with exactly matching casing' do
+ it 'does not redirect' do
+ get :issues, id: group.to_param
+
+ expect(response).not_to have_http_status(301)
+ end
+ end
+
+ context 'with different casing' do
+ it 'redirects to the correct casing' do
+ get :issues, id: group.to_param.upcase
+
+ expect(response).to redirect_to(issues_group_path(group.to_param))
+ expect(controller).not_to set_flash[:notice]
+ end
+ end
+ end
+
+ context 'show path' do
+ context 'with exactly matching casing' do
+ it 'does not redirect' do
+ get :show, id: group.to_param
+
+ expect(response).not_to have_http_status(301)
+ end
+ end
+
+ context 'with different casing' do
+ it 'redirects to the correct casing at the root path' do
+ get :show, id: group.to_param.upcase
+
+ expect(response).to redirect_to(group)
+ expect(controller).not_to set_flash[:notice]
+ end
+ end
+ end
+ end
+
+ context 'when requesting a redirected path' do
+ let(:redirect_route) { group.redirect_routes.create(path: 'old-path') }
+
+ it 'redirects to the canonical path' do
+ get :issues, id: redirect_route.path
+
+ expect(response).to redirect_to(issues_group_path(group.to_param))
+ expect(controller).to set_flash[:notice].to(group_moved_message(redirect_route, group))
+ end
+
+ context 'when the old group path is a substring of the scheme or host' do
+ let(:redirect_route) { group.redirect_routes.create(path: 'http') }
+
+ it 'does not modify the requested host' do
+ get :issues, id: redirect_route.path
+
+ expect(response).to redirect_to(issues_group_path(group.to_param))
+ expect(controller).to set_flash[:notice].to(group_moved_message(redirect_route, group))
+ end
+ end
+
+ context 'when the old group path is substring of groups' do
+ # I.e. /groups/oups should not become /grfoo/oups
+ let(:redirect_route) { group.redirect_routes.create(path: 'oups') }
- expect(response).not_to redirect_to(group_path(group.to_param))
+ it 'does not modify the /groups part of the path' do
+ get :issues, id: redirect_route.path
+
+ expect(response).to redirect_to(issues_group_path(group.to_param))
+ expect(controller).to set_flash[:notice].to(group_moved_message(redirect_route, group))
+ end
+ end
+
+ context 'when the old group path is substring of groups plus the new path' do
+ # I.e. /groups/oups/oup should not become /grfoos
+ let(:redirect_route) { group.redirect_routes.create(path: 'oups/oup') }
+
+ it 'does not modify the /groups part of the path' do
+ get :issues, id: redirect_route.path
+
+ expect(response).to redirect_to(issues_group_path(group.to_param))
+ expect(controller).to set_flash[:notice].to(group_moved_message(redirect_route, group))
+ end
+ end
+ end
end
end
- context 'when requesting a redirected path' do
- let(:redirect_route) { group.redirect_routes.create(path: 'old-path') }
+ context 'for a POST request' do
+ context 'when requesting the canonical path with different casing' do
+ it 'does not 404' do
+ post :update, id: group.to_param.upcase, group: { path: 'new_path' }
+
+ expect(response).not_to have_http_status(404)
+ end
- it 'returns not found' do
- post :update, id: redirect_route.path, group: { path: 'new_path' }
+ it 'does not redirect to the correct casing' do
+ post :update, id: group.to_param.upcase, group: { path: 'new_path' }
- expect(response).to have_http_status(404)
+ expect(response).not_to have_http_status(301)
+ end
+ end
+
+ context 'when requesting a redirected path' do
+ let(:redirect_route) { group.redirect_routes.create(path: 'old-path') }
+
+ it 'returns not found' do
+ post :update, id: redirect_route.path, group: { path: 'new_path' }
+
+ expect(response).to have_http_status(404)
+ end
end
end
+
+ context 'for a DELETE request' do
+ context 'when requesting the canonical path with different casing' do
+ it 'does not 404' do
+ delete :destroy, id: group.to_param.upcase
+
+ expect(response).not_to have_http_status(404)
+ end
+
+ it 'does not redirect to the correct casing' do
+ delete :destroy, id: group.to_param.upcase
+
+ expect(response).not_to have_http_status(301)
+ end
+ end
+
+ context 'when requesting a redirected path' do
+ let(:redirect_route) { group.redirect_routes.create(path: 'old-path') }
+
+ it 'returns not found' do
+ delete :destroy, id: redirect_route.path
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+ end
+
+ def group_moved_message(redirect_route, group)
+ "Group '#{redirect_route.path}' was moved to '#{group.full_path}'. Please update any links and bookmarks that may still have the old path."
end
end
diff --git a/spec/controllers/projects/deployments_controller_spec.rb b/spec/controllers/projects/deployments_controller_spec.rb
index 3de38bb4dac..4c69443314d 100644
--- a/spec/controllers/projects/deployments_controller_spec.rb
+++ b/spec/controllers/projects/deployments_controller_spec.rb
@@ -42,39 +42,68 @@ describe Projects::DeploymentsController do
before do
allow(controller).to receive(:deployment).and_return(deployment)
end
-
- context 'when environment has no metrics' do
+ context 'when metrics are disabled' do
before do
- expect(deployment).to receive(:metrics).and_return(nil)
+ allow(deployment).to receive(:has_metrics?).and_return false
end
- it 'returns a empty response 204 resposne' do
+ it 'responds with not found' do
get :metrics, deployment_params(id: deployment.id)
- expect(response).to have_http_status(204)
- expect(response.body).to eq('')
+
+ expect(response).to be_not_found
end
end
- context 'when environment has some metrics' do
- let(:empty_metrics) do
- {
- success: true,
- metrics: {},
- last_update: 42
- }
+ context 'when metrics are enabled' do
+ before do
+ allow(deployment).to receive(:has_metrics?).and_return true
end
- before do
- expect(deployment).to receive(:metrics).and_return(empty_metrics)
+ context 'when environment has no metrics' do
+ before do
+ expect(deployment).to receive(:metrics).and_return(nil)
+ end
+
+ it 'returns a empty response 204 resposne' do
+ get :metrics, deployment_params(id: deployment.id)
+ expect(response).to have_http_status(204)
+ expect(response.body).to eq('')
+ end
end
- it 'returns a metrics JSON document' do
- get :metrics, deployment_params(id: deployment.id)
+ context 'when environment has some metrics' do
+ let(:empty_metrics) do
+ {
+ success: true,
+ metrics: {},
+ last_update: 42
+ }
+ end
+
+ before do
+ expect(deployment).to receive(:metrics).and_return(empty_metrics)
+ end
+
+ it 'returns a metrics JSON document' do
+ get :metrics, deployment_params(id: deployment.id)
+
+ expect(response).to be_ok
+ expect(json_response['success']).to be(true)
+ expect(json_response['metrics']).to eq({})
+ expect(json_response['last_update']).to eq(42)
+ end
+ end
+
+ context 'when metrics service does not implement deployment metrics' do
+ before do
+ allow(deployment).to receive(:metrics).and_raise(NotImplementedError)
+ end
+
+ it 'responds with not found' do
+ get :metrics, deployment_params(id: deployment.id)
- expect(response).to be_ok
- expect(json_response['success']).to be(true)
- expect(json_response['metrics']).to eq({})
- expect(json_response['last_update']).to eq(42)
+ expect(response).to be_not_found
+ end
end
end
end
diff --git a/spec/controllers/projects/labels_controller_spec.rb b/spec/controllers/projects/labels_controller_spec.rb
index 05999431d8f..130b0b744b5 100644
--- a/spec/controllers/projects/labels_controller_spec.rb
+++ b/spec/controllers/projects/labels_controller_spec.rb
@@ -157,4 +157,74 @@ describe Projects::LabelsController do
end
end
end
+
+ describe '#ensure_canonical_path' do
+ before do
+ sign_in(user)
+ end
+
+ context 'for a GET request' do
+ context 'when requesting the canonical path' do
+ context 'non-show path' do
+ context 'with exactly matching casing' do
+ it 'does not redirect' do
+ get :index, namespace_id: project.namespace, project_id: project.to_param
+
+ expect(response).not_to have_http_status(301)
+ end
+ end
+
+ context 'with different casing' do
+ it 'redirects to the correct casing' do
+ get :index, namespace_id: project.namespace, project_id: project.to_param.upcase
+
+ expect(response).to redirect_to(namespace_project_labels_path(project.namespace, project))
+ expect(controller).not_to set_flash[:notice]
+ end
+ end
+ end
+ end
+
+ context 'when requesting a redirected path' do
+ let!(:redirect_route) { project.redirect_routes.create(path: project.full_path + 'old') }
+
+ it 'redirects to the canonical path' do
+ get :index, namespace_id: project.namespace, project_id: project.to_param + 'old'
+
+ expect(response).to redirect_to(namespace_project_labels_path(project.namespace, project))
+ expect(controller).to set_flash[:notice].to(project_moved_message(redirect_route, project))
+ end
+ end
+ end
+ end
+
+ context 'for a non-GET request' do
+ context 'when requesting the canonical path with different casing' do
+ it 'does not 404' do
+ post :generate, namespace_id: project.namespace, project_id: project
+
+ expect(response).not_to have_http_status(404)
+ end
+
+ it 'does not redirect to the correct casing' do
+ post :generate, namespace_id: project.namespace, project_id: project
+
+ expect(response).not_to have_http_status(301)
+ end
+ end
+
+ context 'when requesting a redirected path' do
+ let!(:redirect_route) { project.redirect_routes.create(path: project.full_path + 'old') }
+
+ it 'returns not found' do
+ post :generate, namespace_id: project.namespace, project_id: project.to_param + 'old'
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ def project_moved_message(redirect_route, project)
+ "Project '#{redirect_route.path}' was moved to '#{project.full_path}'. Please update any links and bookmarks that may still have the old path."
+ end
end
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index 37a253fde9b..0b3492a8fed 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -916,7 +916,9 @@ describe Projects::MergeRequestsController do
end
it 'returns the file in JSON format' do
- content = merge_request_with_conflicts.conflicts.file_for_path(path, path).content
+ content = MergeRequests::Conflicts::ListService.new(merge_request_with_conflicts).
+ file_for_path(path, path).
+ content
expect(json_response).to include('old_path' => path,
'new_path' => path,
@@ -1040,11 +1042,15 @@ describe Projects::MergeRequestsController do
context 'when a file has identical content to the conflict' do
before do
+ content = MergeRequests::Conflicts::ListService.new(merge_request_with_conflicts).
+ file_for_path('files/ruby/popen.rb', 'files/ruby/popen.rb').
+ content
+
resolved_files = [
{
'new_path' => 'files/ruby/popen.rb',
'old_path' => 'files/ruby/popen.rb',
- 'content' => merge_request_with_conflicts.conflicts.file_for_path('files/ruby/popen.rb', 'files/ruby/popen.rb').content
+ 'content' => content
}, {
'new_path' => 'files/ruby/regex.rb',
'old_path' => 'files/ruby/regex.rb',
diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb
index e46ef447df2..a8be6768a47 100644
--- a/spec/controllers/projects_controller_spec.rb
+++ b/spec/controllers/projects_controller_spec.rb
@@ -169,27 +169,6 @@ describe ProjectsController do
end
end
- 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, id: public_project
-
- expect(assigns(:project)).to eq(public_project)
- expect(response).to have_http_status(200)
- end
- end
-
- context "when there is a match with different casing" do
- it "redirects to the normalized path" do
- 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.full_path}")
- expect(controller).not_to set_flash[:notice]
- end
- end
- end
-
context "when the url contains .atom" do
let(:public_project_with_dot_atom) { build(:empty_project, :public, name: 'my.atom', path: 'my.atom') }
@@ -219,17 +198,6 @@ describe ProjectsController do
expect(response).to redirect_to(namespace_project_path)
end
end
-
- context 'when requesting a redirected path' do
- let!(:redirect_route) { public_project.redirect_routes.create!(path: "foo/bar") }
-
- it 'redirects to the canonical path' do
- get :show, namespace_id: 'foo', id: 'bar'
-
- expect(response).to redirect_to(public_project)
- expect(controller).to set_flash[:notice].to(/moved/)
- end
- end
end
describe "#update" do
@@ -256,34 +224,6 @@ describe ProjectsController do
expect(assigns(:repository).path).to eq(project.repository.path)
expect(response).to have_http_status(302)
end
-
- context 'when requesting the canonical path' do
- it "is case-insensitive" do
- controller.instance_variable_set(:@project, project)
-
- put :update,
- namespace_id: 'FOo',
- id: 'baR',
- project: project_params
-
- expect(project.repository.path).to include(new_path)
- expect(assigns(:repository).path).to eq(project.repository.path)
- expect(response).to have_http_status(302)
- end
- end
-
- context 'when requesting a redirected path' do
- let!(:redirect_route) { project.redirect_routes.create!(path: "foo/bar") }
-
- it 'returns not found' do
- put :update,
- namespace_id: 'foo',
- id: 'bar',
- project: project_params
-
- expect(response).to have_http_status(404)
- end
- end
end
describe "#destroy" do
@@ -319,31 +259,6 @@ describe ProjectsController do
expect(merge_request.reload.state).to eq('closed')
end
end
-
- context 'when requesting the canonical path' do
- it "is case-insensitive" do
- controller.instance_variable_set(:@project, project)
- sign_in(admin)
-
- orig_id = project.id
- delete :destroy, namespace_id: project.namespace, id: project.path.upcase
-
- expect { Project.find(orig_id) }.to raise_error(ActiveRecord::RecordNotFound)
- expect(response).to have_http_status(302)
- expect(response).to redirect_to(dashboard_projects_path)
- end
- end
-
- context 'when requesting a redirected path' do
- let!(:redirect_route) { project.redirect_routes.create!(path: "foo/bar") }
-
- it 'returns not found' do
- sign_in(admin)
- delete :destroy, namespace_id: 'foo', id: 'bar'
-
- expect(response).to have_http_status(404)
- end
- end
end
describe 'PUT #new_issue_address' do
@@ -465,17 +380,6 @@ describe ProjectsController do
expect(parsed_body["Tags"]).to include("v1.0.0")
expect(parsed_body["Commits"]).to include("123456")
end
-
- context 'when requesting a redirected path' do
- let!(:redirect_route) { public_project.redirect_routes.create!(path: "foo/bar") }
-
- it 'redirects to the canonical path' do
- get :refs, namespace_id: 'foo', id: 'bar'
-
- expect(response).to redirect_to(refs_namespace_project_path(namespace_id: public_project.namespace, id: public_project))
- expect(controller).to set_flash[:notice].to(/moved/)
- end
- end
end
describe 'POST #preview_markdown' do
@@ -487,4 +391,111 @@ describe ProjectsController do
expect(JSON.parse(response.body).keys).to match_array(%w(body references))
end
end
+
+ describe '#ensure_canonical_path' do
+ before do
+ sign_in(user)
+ end
+
+ context 'for a GET request' do
+ context 'when requesting the canonical path' do
+ context "with exactly matching casing" do
+ it "loads the project" do
+ get :show, namespace_id: public_project.namespace, id: public_project
+
+ expect(assigns(:project)).to eq(public_project)
+ expect(response).to have_http_status(200)
+ end
+ end
+
+ context "with different casing" do
+ it "redirects to the normalized path" do
+ 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.full_path}")
+ expect(controller).not_to set_flash[:notice]
+ end
+ end
+ end
+
+ context 'when requesting a redirected path' do
+ let!(:redirect_route) { public_project.redirect_routes.create!(path: "foo/bar") }
+
+ it 'redirects to the canonical path' do
+ get :show, namespace_id: 'foo', id: 'bar'
+
+ expect(response).to redirect_to(public_project)
+ expect(controller).to set_flash[:notice].to(project_moved_message(redirect_route, public_project))
+ end
+
+ it 'redirects to the canonical path (testing non-show action)' do
+ get :refs, namespace_id: 'foo', id: 'bar'
+
+ expect(response).to redirect_to(refs_namespace_project_path(namespace_id: public_project.namespace, id: public_project))
+ expect(controller).to set_flash[:notice].to(project_moved_message(redirect_route, public_project))
+ end
+ end
+ end
+
+ context 'for a POST request' do
+ context 'when requesting the canonical path with different casing' do
+ it 'does not 404' do
+ post :toggle_star, namespace_id: public_project.namespace, id: public_project.path.upcase
+
+ expect(response).not_to have_http_status(404)
+ end
+
+ it 'does not redirect to the correct casing' do
+ post :toggle_star, namespace_id: public_project.namespace, id: public_project.path.upcase
+
+ expect(response).not_to have_http_status(301)
+ end
+ end
+
+ context 'when requesting a redirected path' do
+ let!(:redirect_route) { public_project.redirect_routes.create!(path: "foo/bar") }
+
+ it 'returns not found' do
+ post :toggle_star, namespace_id: 'foo', id: 'bar'
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ context 'for a DELETE request' do
+ before do
+ sign_in(create(:admin))
+ end
+
+ context 'when requesting the canonical path with different casing' do
+ it 'does not 404' do
+ delete :destroy, namespace_id: project.namespace, id: project.path.upcase
+
+ expect(response).not_to have_http_status(404)
+ end
+
+ it 'does not redirect to the correct casing' do
+ delete :destroy, namespace_id: project.namespace, id: project.path.upcase
+
+ expect(response).not_to have_http_status(301)
+ end
+ end
+
+ context 'when requesting a redirected path' do
+ let!(:redirect_route) { project.redirect_routes.create!(path: "foo/bar") }
+
+ it 'returns not found' do
+ delete :destroy, namespace_id: 'foo', id: 'bar'
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+ end
+
+ def project_moved_message(redirect_route, project)
+ "Project '#{redirect_route.path}' was moved to '#{project.full_path}'. Please update any links and bookmarks that may still have the old path."
+ end
end
diff --git a/spec/controllers/snippets_controller_spec.rb b/spec/controllers/snippets_controller_spec.rb
index 41cd5bdcdd8..930415a4778 100644
--- a/spec/controllers/snippets_controller_spec.rb
+++ b/spec/controllers/snippets_controller_spec.rb
@@ -3,6 +3,34 @@ require 'spec_helper'
describe SnippetsController do
let(:user) { create(:user) }
+ describe 'GET #index' do
+ let(:user) { create(:user) }
+
+ context 'when username parameter is present' do
+ it 'renders snippets of a user when username is present' do
+ get :index, username: user.username
+
+ expect(response).to render_template(:index)
+ end
+ end
+
+ context 'when username parameter is not present' do
+ it 'redirects to explore snippets page when user is not logged in' do
+ get :index
+
+ expect(response).to redirect_to(explore_snippets_path)
+ end
+
+ it 'redirects to snippets dashboard page when user is logged in' do
+ sign_in(user)
+
+ get :index
+
+ expect(response).to redirect_to(dashboard_snippets_path)
+ end
+ end
+ end
+
describe 'GET #new' do
context 'when signed in' do
before do
@@ -132,7 +160,7 @@ describe SnippetsController do
it 'responds with status 404' do
get :show, id: 'doesntexist'
- expect(response).to have_http_status(404)
+ expect(response).to redirect_to(new_user_session_path)
end
end
end
@@ -478,10 +506,10 @@ describe SnippetsController do
end
context 'when not signed in' do
- it 'responds with status 404' do
+ it 'redirects to the sign in path' do
get :raw, id: 'doesntexist'
- expect(response).to have_http_status(404)
+ expect(response).to redirect_to(new_user_session_path)
end
end
end
diff --git a/spec/controllers/uploads_controller_spec.rb b/spec/controllers/uploads_controller_spec.rb
index 7dedfe160a6..8000c9dec61 100644
--- a/spec/controllers/uploads_controller_spec.rb
+++ b/spec/controllers/uploads_controller_spec.rb
@@ -473,5 +473,45 @@ describe UploadsController do
end
end
end
+
+ context 'Appearance' do
+ context 'when viewing a custom header logo' do
+ let!(:appearance) { create :appearance, header_logo: fixture_file_upload(Rails.root.join('spec/fixtures/dk.png'), 'image/png') }
+
+ context 'when not signed in' do
+ it 'responds with status 200' do
+ get :show, model: 'appearance', mounted_as: 'header_logo', id: appearance.id, filename: 'dk.png'
+
+ expect(response).to have_http_status(200)
+ end
+
+ it_behaves_like 'content not cached without revalidation' do
+ subject do
+ get :show, model: 'appearance', mounted_as: 'header_logo', id: appearance.id, filename: 'dk.png'
+ response
+ end
+ end
+ end
+ end
+
+ context 'when viewing a custom logo' do
+ let!(:appearance) { create :appearance, logo: fixture_file_upload(Rails.root.join('spec/fixtures/dk.png'), 'image/png') }
+
+ context 'when not signed in' do
+ it 'responds with status 200' do
+ get :show, model: 'appearance', mounted_as: 'logo', id: appearance.id, filename: 'dk.png'
+
+ expect(response).to have_http_status(200)
+ end
+
+ it_behaves_like 'content not cached without revalidation' do
+ subject do
+ get :show, model: 'appearance', mounted_as: 'logo', id: appearance.id, filename: 'dk.png'
+ response
+ end
+ end
+ end
+ end
+ end
end
end
diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb
index 74c5aa44ba9..d33e2ba1e53 100644
--- a/spec/controllers/users_controller_spec.rb
+++ b/spec/controllers/users_controller_spec.rb
@@ -53,40 +53,6 @@ describe UsersController do
end
end
- context 'when requesting the canonical path' do
- let(:user) { create(:user, username: 'CamelCaseUser') }
-
- before { sign_in(user) }
-
- context 'with exactly matching casing' do
- it 'responds with success' do
- get :show, username: user.username
-
- expect(response).to be_success
- end
- end
-
- context 'with different casing' do
- it 'redirects to the correct casing' do
- get :show, username: user.username.downcase
-
- expect(response).to redirect_to(user)
- expect(controller).not_to set_flash[:notice]
- end
- end
- end
-
- context 'when requesting a redirected path' do
- let(:redirect_route) { user.namespace.redirect_routes.create(path: 'old-username') }
-
- it 'redirects to the canonical path' do
- get :show, username: redirect_route.path
-
- expect(response).to redirect_to(user)
- expect(controller).to set_flash[:notice].to(/moved/)
- end
- end
-
context 'when a user by that username does not exist' do
context 'when logged out' do
it 'redirects to login page' do
@@ -131,40 +97,6 @@ describe UsersController do
expect(assigns(:contributions_calendar).projects.count).to eq(2)
end
end
-
- context 'when requesting the canonical path' do
- let(:user) { create(:user, username: 'CamelCaseUser') }
-
- before { sign_in(user) }
-
- context 'with exactly matching casing' do
- it 'responds with success' do
- get :calendar, username: user.username
-
- expect(response).to be_success
- end
- end
-
- context 'with different casing' do
- it 'redirects to the correct casing' do
- get :calendar, username: user.username.downcase
-
- expect(response).to redirect_to(user_calendar_path(user))
- expect(controller).not_to set_flash[:notice]
- end
- end
- end
-
- context 'when requesting a redirected path' do
- let(:redirect_route) { user.namespace.redirect_routes.create(path: 'old-username') }
-
- it 'redirects to the canonical path' do
- get :calendar, username: redirect_route.path
-
- expect(response).to redirect_to(user_calendar_path(user))
- expect(controller).to set_flash[:notice].to(/moved/)
- end
- end
end
describe 'GET #calendar_activities' do
@@ -187,38 +119,6 @@ describe UsersController do
get :calendar_activities, username: user.username
expect(response).to render_template('calendar_activities')
end
-
- context 'when requesting the canonical path' do
- let(:user) { create(:user, username: 'CamelCaseUser') }
-
- context 'with exactly matching casing' do
- it 'responds with success' do
- get :calendar_activities, username: user.username
-
- expect(response).to be_success
- end
- end
-
- context 'with different casing' do
- it 'redirects to the correct casing' do
- get :calendar_activities, username: user.username.downcase
-
- expect(response).to redirect_to(user_calendar_activities_path(user))
- expect(controller).not_to set_flash[:notice]
- end
- end
- end
-
- context 'when requesting a redirected path' do
- let(:redirect_route) { user.namespace.redirect_routes.create(path: 'old-username') }
-
- it 'redirects to the canonical path' do
- get :calendar_activities, username: redirect_route.path
-
- expect(response).to redirect_to(user_calendar_activities_path(user))
- expect(controller).to set_flash[:notice].to(/moved/)
- end
- end
end
describe 'GET #snippets' do
@@ -241,38 +141,6 @@ describe UsersController do
expect(JSON.parse(response.body)).to have_key('html')
end
end
-
- context 'when requesting the canonical path' do
- let(:user) { create(:user, username: 'CamelCaseUser') }
-
- context 'with exactly matching casing' do
- it 'responds with success' do
- get :snippets, username: user.username
-
- expect(response).to be_success
- end
- end
-
- context 'with different casing' do
- it 'redirects to the correct casing' do
- get :snippets, username: user.username.downcase
-
- expect(response).to redirect_to(user_snippets_path(user))
- expect(controller).not_to set_flash[:notice]
- end
- end
- end
-
- context 'when requesting a redirected path' do
- let(:redirect_route) { user.namespace.redirect_routes.create(path: 'old-username') }
-
- it 'redirects to the canonical path' do
- get :snippets, username: redirect_route.path
-
- expect(response).to redirect_to(user_snippets_path(user))
- expect(controller).to set_flash[:notice].to(/moved/)
- end
- end
end
describe 'GET #exists' do
@@ -320,4 +188,129 @@ describe UsersController do
end
end
end
+
+ describe '#ensure_canonical_path' do
+ before do
+ sign_in(user)
+ end
+
+ context 'for a GET request' do
+ context 'when requesting users at the root path' do
+ context 'when requesting the canonical path' do
+ let(:user) { create(:user, username: 'CamelCaseUser') }
+
+ context 'with exactly matching casing' do
+ it 'responds with success' do
+ get :show, username: user.username
+
+ expect(response).to be_success
+ end
+ end
+
+ context 'with different casing' do
+ it 'redirects to the correct casing' do
+ get :show, username: user.username.downcase
+
+ expect(response).to redirect_to(user)
+ expect(controller).not_to set_flash[:notice]
+ end
+ end
+ end
+
+ context 'when requesting a redirected path' do
+ let(:redirect_route) { user.namespace.redirect_routes.create(path: 'old-path') }
+
+ it 'redirects to the canonical path' do
+ get :show, username: redirect_route.path
+
+ expect(response).to redirect_to(user)
+ expect(controller).to set_flash[:notice].to(user_moved_message(redirect_route, user))
+ end
+
+ context 'when the old path is a substring of the scheme or host' do
+ let(:redirect_route) { user.namespace.redirect_routes.create(path: 'http') }
+
+ it 'does not modify the requested host' do
+ get :show, username: redirect_route.path
+
+ expect(response).to redirect_to(user)
+ expect(controller).to set_flash[:notice].to(user_moved_message(redirect_route, user))
+ end
+ end
+
+ context 'when the old path is substring of users' do
+ let(:redirect_route) { user.namespace.redirect_routes.create(path: 'ser') }
+
+ it 'redirects to the canonical path' do
+ get :show, username: redirect_route.path
+
+ expect(response).to redirect_to(user)
+ expect(controller).to set_flash[:notice].to(user_moved_message(redirect_route, user))
+ end
+ end
+ end
+ end
+
+ context 'when requesting users under the /users path' do
+ context 'when requesting the canonical path' do
+ let(:user) { create(:user, username: 'CamelCaseUser') }
+
+ context 'with exactly matching casing' do
+ it 'responds with success' do
+ get :projects, username: user.username
+
+ expect(response).to be_success
+ end
+ end
+
+ context 'with different casing' do
+ it 'redirects to the correct casing' do
+ get :projects, username: user.username.downcase
+
+ expect(response).to redirect_to(user_projects_path(user))
+ expect(controller).not_to set_flash[:notice]
+ end
+ end
+ end
+
+ context 'when requesting a redirected path' do
+ let(:redirect_route) { user.namespace.redirect_routes.create(path: 'old-path') }
+
+ it 'redirects to the canonical path' do
+ get :projects, username: redirect_route.path
+
+ expect(response).to redirect_to(user_projects_path(user))
+ expect(controller).to set_flash[:notice].to(user_moved_message(redirect_route, user))
+ end
+
+ context 'when the old path is a substring of the scheme or host' do
+ let(:redirect_route) { user.namespace.redirect_routes.create(path: 'http') }
+
+ it 'does not modify the requested host' do
+ get :projects, username: redirect_route.path
+
+ expect(response).to redirect_to(user_projects_path(user))
+ expect(controller).to set_flash[:notice].to(user_moved_message(redirect_route, user))
+ end
+ end
+
+ context 'when the old path is substring of users' do
+ let(:redirect_route) { user.namespace.redirect_routes.create(path: 'ser') }
+
+ # I.e. /users/ser should not become /ufoos/ser
+ it 'does not modify the /users part of the path' do
+ get :projects, username: redirect_route.path
+
+ expect(response).to redirect_to(user_projects_path(user))
+ expect(controller).to set_flash[:notice].to(user_moved_message(redirect_route, user))
+ end
+ end
+ end
+ end
+ end
+ end
+
+ def user_moved_message(redirect_route, user)
+ "User '#{redirect_route.path}' was moved to '#{user.full_path}'. Please update any links and bookmarks that may still have the old path."
+ end
end
diff --git a/spec/factories/ci/variables.rb b/spec/factories/ci/variables.rb
index 6653f0bb5c3..c5fba597c1c 100644
--- a/spec/factories/ci/variables.rb
+++ b/spec/factories/ci/variables.rb
@@ -2,5 +2,7 @@ FactoryGirl.define do
factory :ci_variable, class: Ci::Variable do
sequence(:key) { |n| "VARIABLE_#{n}" }
value 'VARIABLE_VALUE'
+
+ project factory: :empty_project
end
end
diff --git a/spec/factories/group_members.rb b/spec/factories/group_members.rb
index 080b2e75ea1..32cbfe28a60 100644
--- a/spec/factories/group_members.rb
+++ b/spec/factories/group_members.rb
@@ -10,5 +10,11 @@ FactoryGirl.define do
trait(:master) { access_level GroupMember::MASTER }
trait(:owner) { access_level GroupMember::OWNER }
trait(:access_request) { requested_at Time.now }
+
+ trait(:invited) do
+ user_id nil
+ invite_token 'xxx'
+ invite_email 'email@email.com'
+ end
end
end
diff --git a/spec/factories/project_hooks.rb b/spec/factories/project_hooks.rb
index 0210e871a63..cd754ea235f 100644
--- a/spec/factories/project_hooks.rb
+++ b/spec/factories/project_hooks.rb
@@ -14,7 +14,7 @@ FactoryGirl.define do
issues_events true
confidential_issues_events true
note_events true
- build_events true
+ job_events true
pipeline_events true
wiki_page_events true
end
diff --git a/spec/factories/project_members.rb b/spec/factories/project_members.rb
index d62799a5a47..fe4518caadf 100644
--- a/spec/factories/project_members.rb
+++ b/spec/factories/project_members.rb
@@ -9,5 +9,11 @@ FactoryGirl.define do
trait(:developer) { access_level ProjectMember::DEVELOPER }
trait(:master) { access_level ProjectMember::MASTER }
trait(:access_request) { requested_at Time.now }
+
+ trait(:invited) do
+ user_id nil
+ invite_token 'xxx'
+ invite_email 'email@email.com'
+ end
end
end
diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb
index 3580752a805..7a76f5f8afc 100644
--- a/spec/factories/projects.rb
+++ b/spec/factories/projects.rb
@@ -60,7 +60,9 @@ FactoryGirl.define do
trait :test_repo do
after :create do |project|
- TestEnv.copy_repo(project)
+ TestEnv.copy_repo(project,
+ bare_repo: TestEnv.factory_repo_path_bare,
+ refs: TestEnv::BRANCH_SHA)
end
end
@@ -139,7 +141,9 @@ FactoryGirl.define do
end
after :create do |project, evaluator|
- TestEnv.copy_repo(project)
+ TestEnv.copy_repo(project,
+ bare_repo: TestEnv.factory_repo_path_bare,
+ refs: TestEnv::BRANCH_SHA)
if evaluator.create_template
args = evaluator.create_template
@@ -172,7 +176,9 @@ FactoryGirl.define do
path { 'forked-gitlabhq' }
after :create do |project|
- TestEnv.copy_forked_repo_with_submodules(project)
+ TestEnv.copy_repo(project,
+ bare_repo: TestEnv.forked_repo_path_bare,
+ refs: TestEnv::FORKED_BRANCH_SHA)
end
end
diff --git a/spec/factories/services.rb b/spec/factories/services.rb
index 62aa71ae8d8..28ddd0da753 100644
--- a/spec/factories/services.rb
+++ b/spec/factories/services.rb
@@ -22,7 +22,7 @@ FactoryGirl.define do
properties({
namespace: 'somepath',
api_url: 'https://kubernetes.example.com',
- token: 'a' * 40,
+ token: 'a' * 40
})
end
diff --git a/spec/features/admin/admin_uses_repository_checks_spec.rb b/spec/features/admin/admin_uses_repository_checks_spec.rb
index 855247de2ea..ab5c42365fe 100644
--- a/spec/features/admin/admin_uses_repository_checks_spec.rb
+++ b/spec/features/admin/admin_uses_repository_checks_spec.rb
@@ -23,7 +23,7 @@ feature 'Admin uses repository checks', feature: true do
project = create(:empty_project)
project.update_columns(
last_repository_check_failed: true,
- last_repository_check_at: Time.now,
+ last_repository_check_at: Time.now
)
visit_admin_project_page(project)
diff --git a/spec/features/auto_deploy_spec.rb b/spec/features/auto_deploy_spec.rb
index 67b0f006854..6c7423e4922 100644
--- a/spec/features/auto_deploy_spec.rb
+++ b/spec/features/auto_deploy_spec.rb
@@ -5,14 +5,7 @@ describe 'Auto deploy' do
let(:project) { create(:project, :repository) }
before do
- project.create_kubernetes_service(
- active: true,
- properties: {
- namespace: project.path,
- api_url: 'https://kubernetes.example.com',
- token: 'a' * 40,
- }
- )
+ create :kubernetes_service, project: project
project.team << [user, :master]
login_as user
end
diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb
index 11ef8e1f61b..4667be49fe6 100644
--- a/spec/features/boards/sidebar_spec.rb
+++ b/spec/features/boards/sidebar_spec.rb
@@ -115,7 +115,6 @@ describe 'Issue Boards', feature: true, js: true do
click_link 'Unassigned'
end
- find('.dropdown-menu-toggle').click
wait_for_vue_resource
expect(page).to have_content('No assignee')
@@ -158,7 +157,7 @@ describe 'Issue Boards', feature: true, js: true do
end
page.within(first('.board')) do
- find('.card:nth-child(2)').click
+ find('.card:nth-child(2)').trigger('click')
end
page.within('.assignee') do
diff --git a/spec/features/copy_as_gfm_spec.rb b/spec/features/copy_as_gfm_spec.rb
index f197fb44608..be615519a09 100644
--- a/spec/features/copy_as_gfm_spec.rb
+++ b/spec/features/copy_as_gfm_spec.rb
@@ -96,7 +96,7 @@ describe 'Copy as GFM', feature: true, js: true do
# issue link
"[Issue](#{namespace_project_issue_url(@project.namespace, @project, @feat.issue)})",
# issue link with note anchor
- "[Issue](#{namespace_project_issue_url(@project.namespace, @project, @feat.issue, anchor: 'note_123')})",
+ "[Issue](#{namespace_project_issue_url(@project.namespace, @project, @feat.issue, anchor: 'note_123')})"
)
verify(
diff --git a/spec/features/dashboard/group_spec.rb b/spec/features/dashboard/group_spec.rb
index 1d4b86ed4b4..8e20fdec8ad 100644
--- a/spec/features/dashboard/group_spec.rb
+++ b/spec/features/dashboard/group_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe 'Dashboard Group', feature: true do
it 'creates new group', js: true do
visit dashboard_groups_path
- click_link 'New group'
+ find('.btn-new').trigger('click')
new_path = 'Samurai'
new_description = 'Tokugawa Shogunate'
diff --git a/spec/features/dashboard/issuables_counter_spec.rb b/spec/features/dashboard/issuables_counter_spec.rb
index 6f7bf0eba6e..354267dbee7 100644
--- a/spec/features/dashboard/issuables_counter_spec.rb
+++ b/spec/features/dashboard/issuables_counter_spec.rb
@@ -19,7 +19,7 @@ describe 'Navigation bar counter', feature: true, caching: true do
issue.assignees = []
- user.update_cache_counts
+ user.invalidate_cache_counts
Timecop.travel(3.minutes.from_now) do
visit issues_path
@@ -35,6 +35,8 @@ describe 'Navigation bar counter', feature: true, caching: true do
merge_request.update(assignee: nil)
+ user.invalidate_cache_counts
+
Timecop.travel(3.minutes.from_now) do
visit merge_requests_path
diff --git a/spec/features/dashboard/issues_spec.rb b/spec/features/dashboard/issues_spec.rb
index 86c7954e60c..7a132dba1e9 100644
--- a/spec/features/dashboard/issues_spec.rb
+++ b/spec/features/dashboard/issues_spec.rb
@@ -26,9 +26,20 @@ RSpec.describe 'Dashboard Issues', feature: true do
expect(page).not_to have_content(other_issue.title)
end
+ it 'shows checkmark when unassigned is selected for assignee', js: true do
+ find('.js-assignee-search').click
+ find('li', text: 'Unassigned').click
+ find('.js-assignee-search').click
+
+ expect(find('li[data-user-id="0"] a.is-active')).to be_visible
+ 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
+
+ expect(find('li[data-user-id="null"] a.is-active')).to be_visible
+
find('.dropdown-menu-author li a', match: :first, text: current_user.to_reference).click
find('.js-author-search', match: :first).click
diff --git a/spec/features/dashboard/milestone_filter_spec.rb b/spec/features/dashboard/milestone_filter_spec.rb
new file mode 100644
index 00000000000..d60a002a8d7
--- /dev/null
+++ b/spec/features/dashboard/milestone_filter_spec.rb
@@ -0,0 +1,60 @@
+require 'spec_helper'
+
+describe 'Dashboard > milestone filter', :feature, :js do
+ include WaitForAjax
+
+ let(:user) { create(:user) }
+ let(:project) { create(:project, name: 'test', namespace: user.namespace) }
+ let(:milestone) { create(:milestone, title: "v1.0", project: project) }
+ let(:milestone2) { create(:milestone, title: "v2.0", project: project) }
+ let!(:issue) { create :issue, author: user, project: project, milestone: milestone }
+ let!(:issue2) { create :issue, author: user, project: project, milestone: milestone2 }
+
+ before do
+ login_as(user)
+ visit issues_dashboard_path(author_id: user.id)
+ end
+
+ context 'default state' do
+ it 'shows issues with Any Milestone' do
+ page.all('.issue-info').each do |issue_info|
+ expect(issue_info.text).to match(/v\d.0/)
+ end
+ end
+ end
+
+ context 'filtering by milestone' do
+ milestone_select = '.js-milestone-select'
+
+ before do
+ find(milestone_select).click
+ wait_for_ajax
+
+ page.within('.dropdown-content') do
+ click_link 'v1.0'
+ end
+
+ find(milestone_select).click
+ wait_for_ajax
+ end
+
+ it 'shows issues with Milestone v1.0' do
+ expect(find('.issues-list')).to have_selector('.issue', count: 1)
+ expect(find('.dropdown-content')).to have_selector('a.is-active', count: 1)
+ end
+
+ it 'should not change active Milestone unless clicked' do
+ expect(find('.dropdown-content')).to have_selector('a.is-active', count: 1)
+
+ # open & close dropdown
+ find('.dropdown-menu-close').click
+
+ expect(find('.milestone-filter')).not_to have_selector('.dropdown.open')
+
+ find(milestone_select).click
+
+ expect(find('.dropdown-content')).to have_selector('a.is-active', count: 1)
+ expect(find('.dropdown-content a.is-active')).to have_content('v1.0')
+ end
+ end
+end
diff --git a/spec/features/dashboard/shortcuts_spec.rb b/spec/features/dashboard/shortcuts_spec.rb
index 4c9adcabe34..349b948eaee 100644
--- a/spec/features/dashboard/shortcuts_spec.rb
+++ b/spec/features/dashboard/shortcuts_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Dashboard shortcuts', feature: true, js: true do
+feature 'Dashboard shortcuts', :feature, :js do
context 'logged in' do
before do
login_as :user
@@ -8,21 +8,21 @@ feature 'Dashboard shortcuts', feature: true, js: true do
end
scenario 'Navigate to tabs' do
- find('body').native.send_keys([:shift, 'P'])
-
- check_page_title('Projects')
-
- find('body').native.send_key([:shift, 'I'])
+ find('body').send_keys([:shift, 'I'])
check_page_title('Issues')
- find('body').native.send_key([:shift, 'M'])
+ find('body').send_keys([:shift, 'M'])
check_page_title('Merge Requests')
- find('body').native.send_keys([:shift, 'T'])
+ find('body').send_keys([:shift, 'T'])
check_page_title('Todos')
+
+ find('body').send_keys([:shift, 'P'])
+
+ check_page_title('Projects')
end
end
@@ -32,17 +32,20 @@ feature 'Dashboard shortcuts', feature: true, js: true do
end
scenario 'Navigate to tabs' do
- find('body').native.send_keys([:shift, 'P'])
-
- expect(page).to have_content('No projects found')
-
- find('body').native.send_keys([:shift, 'G'])
+ find('body').send_keys([:shift, 'G'])
+ find('.nothing-here-block')
expect(page).to have_content('No public groups')
- find('body').native.send_keys([:shift, 'S'])
+ find('body').send_keys([:shift, 'S'])
+ find('.nothing-here-block')
expect(page).to have_selector('.snippets-list-holder')
+
+ find('body').send_keys([:shift, 'P'])
+
+ find('.nothing-here-block')
+ expect(page).to have_content('No projects found')
end
end
diff --git a/spec/features/dashboard/snippets_spec.rb b/spec/features/dashboard/snippets_spec.rb
index 62937688c22..c6ba118220a 100644
--- a/spec/features/dashboard/snippets_spec.rb
+++ b/spec/features/dashboard/snippets_spec.rb
@@ -12,4 +12,51 @@ describe 'Dashboard snippets', feature: true do
it_behaves_like 'paginated snippets'
end
+
+ context 'filtering by visibility' do
+ let(:user) { create(:user) }
+ let!(:snippets) do
+ [
+ create(:personal_snippet, :public, author: user),
+ create(:personal_snippet, :internal, author: user),
+ create(:personal_snippet, :private, author: user),
+ create(:personal_snippet, :public)
+ ]
+ end
+
+ before do
+ login_as(user)
+
+ visit dashboard_snippets_path
+ end
+
+ it 'contains all snippets of logged user' do
+ expect(page).to have_selector('.snippet-row', count: 3)
+
+ expect(page).to have_content(snippets[0].title)
+ expect(page).to have_content(snippets[1].title)
+ expect(page).to have_content(snippets[2].title)
+ end
+
+ it 'contains all private snippets of logged user when clicking on private' do
+ click_link('Private')
+
+ expect(page).to have_selector('.snippet-row', count: 1)
+ expect(page).to have_content(snippets[2].title)
+ end
+
+ it 'contains all internal snippets of logged user when clicking on internal' do
+ click_link('Internal')
+
+ expect(page).to have_selector('.snippet-row', count: 1)
+ expect(page).to have_content(snippets[1].title)
+ end
+
+ it 'contains all public snippets of logged user when clicking on public' do
+ click_link('Public')
+
+ expect(page).to have_selector('.snippet-row', count: 1)
+ expect(page).to have_content(snippets[0].title)
+ end
+ end
end
diff --git a/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb b/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb
index dc13cab2cd1..24e2419b5ce 100644
--- a/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb
+++ b/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb
@@ -14,7 +14,7 @@ feature 'Resolving all open discussions in a merge request from an issue', featu
end
it 'shows a button to resolve all discussions by creating a new issue' do
- within('li#resolve-count-app') do
+ within('#resolve-count-app') do
expect(page).to have_link "Resolve all discussions in new issue", href: new_namespace_project_issue_path(project.namespace, project, merge_request_to_resolve_discussions_of: merge_request.iid)
end
end
diff --git a/spec/features/issues/filtered_search/recent_searches_spec.rb b/spec/features/issues/filtered_search/recent_searches_spec.rb
index 08fe3b4553b..09f228bcf49 100644
--- a/spec/features/issues/filtered_search/recent_searches_spec.rb
+++ b/spec/features/issues/filtered_search/recent_searches_spec.rb
@@ -3,17 +3,17 @@ require 'spec_helper'
describe 'Recent searches', js: true, feature: true do
include FilteredSearchHelpers
- let!(:group) { create(:group) }
- let!(:project) { create(:project, group: group) }
- let!(:user) { create(:user) }
+ let(:project_1) { create(:empty_project, :public) }
+ let(:project_2) { create(:empty_project, :public) }
+ let(:project_1_local_storage_key) { "#{project_1.full_path}-issue-recent-searches" }
before do
Capybara.ignore_hidden_elements = false
- project.add_master(user)
- group.add_developer(user)
- create(:issue, project: project)
- login_as(user)
+ create(:issue, project: project_1)
+ create(:issue, project: project_2)
+ # Visit any fast-loading page so we can clear local storage without a DOM exception
+ visit '/404'
remove_recent_searches
end
@@ -22,7 +22,7 @@ describe 'Recent searches', js: true, feature: true do
end
it 'searching adds to recent searches' do
- visit namespace_project_issues_path(project.namespace, project)
+ visit namespace_project_issues_path(project_1.namespace, project_1)
input_filtered_search('foo', submit: true)
input_filtered_search('bar', submit: true)
@@ -35,8 +35,8 @@ describe 'Recent searches', js: true, feature: true do
end
it 'visiting URL with search params adds to recent searches' do
- visit namespace_project_issues_path(project.namespace, project, label_name: 'foo', search: 'bar')
- visit namespace_project_issues_path(project.namespace, project, label_name: 'qux', search: 'garply')
+ visit namespace_project_issues_path(project_1.namespace, project_1, label_name: 'foo', search: 'bar')
+ visit namespace_project_issues_path(project_1.namespace, project_1, label_name: 'qux', search: 'garply')
items = all('.filtered-search-history-dropdown-item', visible: false)
@@ -46,9 +46,9 @@ describe 'Recent searches', js: true, feature: true do
end
it 'saved recent searches are restored last on the list' do
- set_recent_searches('["saved1", "saved2"]')
+ set_recent_searches(project_1_local_storage_key, '["saved1", "saved2"]')
- visit namespace_project_issues_path(project.namespace, project, search: 'foo')
+ visit namespace_project_issues_path(project_1.namespace, project_1, search: 'foo')
items = all('.filtered-search-history-dropdown-item', visible: false)
@@ -58,9 +58,27 @@ describe 'Recent searches', js: true, feature: true do
expect(items[2].text).to eq('saved2')
end
+ it 'searches are scoped to projects' do
+ visit namespace_project_issues_path(project_1.namespace, project_1)
+
+ input_filtered_search('foo', submit: true)
+ input_filtered_search('bar', submit: true)
+
+ visit namespace_project_issues_path(project_2.namespace, project_2)
+
+ input_filtered_search('more', submit: true)
+ input_filtered_search('things', submit: true)
+
+ items = all('.filtered-search-history-dropdown-item', visible: false)
+
+ expect(items.count).to eq(2)
+ expect(items[0].text).to eq('things')
+ expect(items[1].text).to eq('more')
+ end
+
it 'clicking item fills search input' do
- set_recent_searches('["foo", "bar"]')
- visit namespace_project_issues_path(project.namespace, project)
+ set_recent_searches(project_1_local_storage_key, '["foo", "bar"]')
+ visit namespace_project_issues_path(project_1.namespace, project_1)
all('.filtered-search-history-dropdown-item', visible: false)[0].trigger('click')
wait_for_filtered_search('foo')
@@ -69,8 +87,8 @@ describe 'Recent searches', js: true, feature: true do
end
it 'clear recent searches button, clears recent searches' do
- set_recent_searches('["foo"]')
- visit namespace_project_issues_path(project.namespace, project)
+ set_recent_searches(project_1_local_storage_key, '["foo"]')
+ visit namespace_project_issues_path(project_1.namespace, project_1)
items_before = all('.filtered-search-history-dropdown-item', visible: false)
@@ -83,8 +101,8 @@ describe 'Recent searches', js: true, feature: true do
end
it 'shows flash error when failed to parse saved history' do
- set_recent_searches('fail')
- visit namespace_project_issues_path(project.namespace, project)
+ set_recent_searches(project_1_local_storage_key, 'fail')
+ visit namespace_project_issues_path(project_1.namespace, project_1)
expect(find('.flash-alert')).to have_text('An error occured while parsing recent searches')
end
diff --git a/spec/features/issues/form_spec.rb b/spec/features/issues/form_spec.rb
index 87adce3cddd..5c0907e26df 100644
--- a/spec/features/issues/form_spec.rb
+++ b/spec/features/issues/form_spec.rb
@@ -1,8 +1,9 @@
require 'rails_helper'
-describe 'New/edit issue', feature: true, js: true do
+describe 'New/edit issue', :feature, :js do
include GitlabRoutingHelper
include ActionView::Helpers::JavaScriptHelper
+ include WaitForAjax
let!(:project) { create(:project) }
let!(:user) { create(:user)}
@@ -23,9 +24,11 @@ describe 'New/edit issue', feature: true, js: true do
visit new_namespace_project_issue_path(project.namespace, project)
end
- describe 'multiple assignees' do
+ describe 'single assignee' do
before do
click_button 'Unassigned'
+
+ wait_for_ajax
end
it 'unselects other assignees when unassigned is selected' do
@@ -33,14 +36,12 @@ describe 'New/edit issue', feature: true, js: true do
click_link user2.name
end
+ click_button user2.name
+
page.within '.dropdown-menu-user' do
click_link 'Unassigned'
end
- page.within '.js-assignee-search' do
- expect(page).to have_content 'Unassigned'
- end
-
expect(find('input[name="issue[assignee_ids][]"]', visible: false).value).to match('0')
end
@@ -51,11 +52,13 @@ describe 'New/edit issue', feature: true, js: true do
expect(find('a', text: 'Assign to me', visible: false)).not_to be_visible
- page.within '.dropdown-menu-user' do
+ click_button user.name
+
+ page.within('.dropdown-menu-user') do
click_link user.name
end
- expect(find('a', text: 'Assign to me')).to be_visible
+ expect(page.find('.dropdown-menu-user', visible: false)).not_to be_visible
end
end
@@ -65,6 +68,9 @@ describe 'New/edit issue', feature: true, js: true do
expect(find('a', text: 'Assign to me')).to be_visible
click_button 'Unassigned'
+
+ wait_for_ajax
+
page.within '.dropdown-menu-user' do
click_link user2.name
end
@@ -148,29 +154,21 @@ describe 'New/edit issue', feature: true, js: true do
it 'correctly updates the selected user when changing assignee' do
click_button 'Unassigned'
+
+ wait_for_ajax
+
page.within '.dropdown-menu-user' do
click_link user.name
end
- expect(find('input[name="issue[assignee_ids][]"]', visible: false).value).to match(user.id.to_s)
-
+ expect(find('.js-assignee-search')).to have_content(user.name)
click_button user.name
- expect(find('.dropdown-menu-user a.is-active').first(:xpath, '..')['data-user-id']).to eq(user.id.to_s)
-
- # check the ::before pseudo element to ensure checkmark icon is present
- expect(before_for_selector('.dropdown-menu-selectable a.is-active')).not_to eq('')
- expect(before_for_selector('.dropdown-menu-selectable a:not(.is-active)')).to eq('')
-
page.within '.dropdown-menu-user' do
click_link user2.name
end
- expect(find('input[name="issue[assignee_ids][]"]', visible: false).value).to match(user2.id.to_s)
-
- click_button user2.name
-
- expect(find('.dropdown-menu-user a.is-active').first(:xpath, '..')['data-user-id']).to eq(user2.id.to_s)
+ expect(find('.js-assignee-search')).to have_content(user2.name)
end
end
diff --git a/spec/features/issues/note_polling_spec.rb b/spec/features/issues/note_polling_spec.rb
index 58b3215f14c..80f57906506 100644
--- a/spec/features/issues/note_polling_spec.rb
+++ b/spec/features/issues/note_polling_spec.rb
@@ -18,58 +18,109 @@ feature 'Issue notes polling', :feature, :js do
end
describe 'updates' do
- let(:user) { create(:user) }
- let(:note_text) { "Hello World" }
- let(:updated_text) { "Bye World" }
- let!(:existing_note) { create(:note, noteable: issue, project: project, author: user, note: note_text) }
+ context 'when from own user' do
+ let(:user) { create(:user) }
+ let(:note_text) { "Hello World" }
+ let(:updated_text) { "Bye World" }
+ let!(:existing_note) { create(:note, noteable: issue, project: project, author: user, note: note_text) }
- before do
- login_as(user)
- visit namespace_project_issue_path(project.namespace, project, issue)
- end
+ before do
+ login_as(user)
+ visit namespace_project_issue_path(project.namespace, project, issue)
+ end
- it 'displays the updated content' do
- expect(page).to have_selector("#note_#{existing_note.id}", text: note_text)
+ it 'has .original-note-content to compare against' do
+ expect(page).to have_selector("#note_#{existing_note.id}", text: note_text)
+ expect(page).to have_selector("#note_#{existing_note.id} .original-note-content", count: 1, visible: false)
- update_note(existing_note, updated_text)
+ update_note(existing_note, updated_text)
- expect(page).to have_selector("#note_#{existing_note.id}", text: updated_text)
- end
+ expect(page).to have_selector("#note_#{existing_note.id}", text: updated_text)
+ expect(page).to have_selector("#note_#{existing_note.id} .original-note-content", count: 1, visible: false)
+ end
- it 'when editing but have not changed anything, and an update comes in, show the updated content in the textarea' do
- find("#note_#{existing_note.id} .js-note-edit").click
+ it 'displays the updated content' do
+ expect(page).to have_selector("#note_#{existing_note.id}", text: note_text)
- expect(page).to have_field("note[note]", with: note_text)
+ update_note(existing_note, updated_text)
- update_note(existing_note, updated_text)
+ expect(page).to have_selector("#note_#{existing_note.id}", text: updated_text)
+ end
- expect(page).to have_field("note[note]", with: updated_text)
- end
+ it 'when editing but have not changed anything, and an update comes in, show the updated content in the textarea' do
+ find("#note_#{existing_note.id} .js-note-edit").click
+
+ expect(page).to have_field("note[note]", with: note_text)
+
+ update_note(existing_note, updated_text)
+
+ expect(page).to have_field("note[note]", with: updated_text)
+ end
+
+ it 'when editing but you changed some things, and an update comes in, show a warning' do
+ find("#note_#{existing_note.id} .js-note-edit").click
- it 'when editing but you changed some things, and an update comes in, show a warning' do
- find("#note_#{existing_note.id} .js-note-edit").click
+ expect(page).to have_field("note[note]", with: note_text)
- expect(page).to have_field("note[note]", with: note_text)
+ find("#note_#{existing_note.id} .js-note-text").set('something random')
- find("#note_#{existing_note.id} .js-note-text").set('something random')
+ update_note(existing_note, updated_text)
- update_note(existing_note, updated_text)
+ expect(page).to have_selector(".alert")
+ end
- expect(page).to have_selector(".alert")
+ it 'when editing but you changed some things, an update comes in, and you press cancel, show the updated content' do
+ find("#note_#{existing_note.id} .js-note-edit").click
+
+ expect(page).to have_field("note[note]", with: note_text)
+
+ find("#note_#{existing_note.id} .js-note-text").set('something random')
+
+ update_note(existing_note, updated_text)
+
+ find("#note_#{existing_note.id} .note-edit-cancel").click
+
+ expect(page).to have_selector("#note_#{existing_note.id}", text: updated_text)
+ end
end
- it 'when editing but you changed some things, an update comes in, and you press cancel, show the updated content' do
- find("#note_#{existing_note.id} .js-note-edit").click
+ context 'when from another user' do
+ let(:user1) { create(:user) }
+ let(:user2) { create(:user) }
+ let(:note_text) { "Hello World" }
+ let(:updated_text) { "Bye World" }
+ let!(:existing_note) { create(:note, noteable: issue, project: project, author: user1, note: note_text) }
+
+ before do
+ login_as(user2)
+ visit namespace_project_issue_path(project.namespace, project, issue)
+ end
- expect(page).to have_field("note[note]", with: note_text)
+ it 'has .original-note-content to compare against' do
+ expect(page).to have_selector("#note_#{existing_note.id}", text: note_text)
+ expect(page).to have_selector("#note_#{existing_note.id} .original-note-content", count: 1, visible: false)
- find("#note_#{existing_note.id} .js-note-text").set('something random')
+ update_note(existing_note, updated_text)
+
+ expect(page).to have_selector("#note_#{existing_note.id}", text: updated_text)
+ expect(page).to have_selector("#note_#{existing_note.id} .original-note-content", count: 1, visible: false)
+ end
+ end
- update_note(existing_note, updated_text)
+ context 'system notes' do
+ let(:user) { create(:user) }
+ let(:note_text) { "Some system note" }
+ let!(:system_note) { create(:system_note, noteable: issue, project: project, author: user, note: note_text) }
- find("#note_#{existing_note.id} .note-edit-cancel").click
+ before do
+ login_as(user)
+ visit namespace_project_issue_path(project.namespace, project, issue)
+ end
- expect(page).to have_selector("#note_#{existing_note.id}", text: updated_text)
+ it 'has .original-note-content to compare against' do
+ expect(page).to have_selector("#note_#{system_note.id}", text: note_text)
+ expect(page).to have_selector("#note_#{system_note.id} .original-note-content", count: 1, visible: false)
+ end
end
end
diff --git a/spec/features/issues/notes_on_issues_spec.rb b/spec/features/issues/notes_on_issues_spec.rb
new file mode 100644
index 00000000000..a4035324d2b
--- /dev/null
+++ b/spec/features/issues/notes_on_issues_spec.rb
@@ -0,0 +1,77 @@
+require 'spec_helper'
+
+describe 'Create notes on issues', :js, :feature do
+ let(:user) { create(:user) }
+
+ shared_examples 'notes with reference' do
+ let(:issue) { create(:issue, project: project) }
+ let(:note_text) { "Check #{mention.to_reference}" }
+
+ before do
+ project.team << [user, :developer]
+ login_as(user)
+ visit namespace_project_issue_path(project.namespace, project, issue)
+
+ fill_in 'note[note]', with: note_text
+ click_button 'Comment'
+
+ wait_for_ajax
+ end
+
+ it 'creates a note with reference and cross references the issue' do
+ page.within('div#notes li.note div.note-text') do
+ expect(page).to have_content(note_text)
+ expect(page.find('a')).to have_content(mention.to_reference)
+ end
+
+ find('div#notes li.note div.note-text a').click
+
+ page.within('div#notes li.note .system-note-message') do
+ expect(page).to have_content('mentioned in issue')
+ expect(page.find('a')).to have_content(issue.to_reference)
+ end
+ end
+ end
+
+ context 'mentioning issue on a private project' do
+ it_behaves_like 'notes with reference' do
+ let(:project) { create(:project, :private) }
+ let(:mention) { create(:issue, project: project) }
+ end
+ end
+
+ context 'mentioning issue on an internal project' do
+ it_behaves_like 'notes with reference' do
+ let(:project) { create(:project, :internal) }
+ let(:mention) { create(:issue, project: project) }
+ end
+ end
+
+ context 'mentioning issue on a public project' do
+ it_behaves_like 'notes with reference' do
+ let(:project) { create(:project, :public) }
+ let(:mention) { create(:issue, project: project) }
+ end
+ end
+
+ context 'mentioning merge request on a private project' do
+ it_behaves_like 'notes with reference' do
+ let(:project) { create(:project, :private) }
+ let(:mention) { create(:merge_request, source_project: project) }
+ end
+ end
+
+ context 'mentioning merge request on an internal project' do
+ it_behaves_like 'notes with reference' do
+ let(:project) { create(:project, :internal) }
+ let(:mention) { create(:merge_request, source_project: project) }
+ end
+ end
+
+ context 'mentioning merge request on a public project' do
+ it_behaves_like 'notes with reference' do
+ let(:project) { create(:project, :public) }
+ let(:mention) { create(:merge_request, source_project: project) }
+ end
+ end
+end
diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb
index 5285dda361b..06ed2dbac64 100644
--- a/spec/features/issues_spec.rb
+++ b/spec/features/issues_spec.rb
@@ -30,13 +30,6 @@ describe 'Issues', feature: true do
it 'opens new issue popup' do
expect(page).to have_content("Issue ##{issue.iid}")
end
-
- describe 'fill in' do
- before do
- fill_in 'issue_title', with: 'bug 345'
- fill_in 'issue_description', with: 'bug description'
- end
- end
end
describe 'Editing issue assignee' do
@@ -465,8 +458,6 @@ describe 'Issues', feature: true do
click_link 'Edit'
click_link @user.name
- find('.dropdown-menu-toggle').click
-
page.within '.value .author' do
expect(page).to have_content @user.name
end
@@ -474,8 +465,6 @@ describe 'Issues', feature: true do
click_link 'Edit'
click_link @user.name
- find('.dropdown-menu-toggle').click
-
page.within '.value .assign-yourself' do
expect(page).to have_content "No assignee"
end
@@ -561,18 +550,11 @@ describe 'Issues', feature: true do
expect(page).to have_content milestone.title
end
end
-
- describe 'removing assignee' do
- let(:user2) { create(:user) }
-
- before do
- issue.assignees << user2
- issue.save
- end
- end
end
describe 'new issue' do
+ let!(:issue) { create(:issue, project: project) }
+
context 'by unauthenticated user' do
before do
logout
diff --git a/spec/features/merge_requests/conflicts_spec.rb b/spec/features/merge_requests/conflicts_spec.rb
index 43977ad2fc5..04b7593ce68 100644
--- a/spec/features/merge_requests/conflicts_spec.rb
+++ b/spec/features/merge_requests/conflicts_spec.rb
@@ -151,7 +151,7 @@ feature 'Merge request conflict resolution', js: true, feature: true do
'conflict-too-large' => 'when the conflicts contain a large file',
'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',
+ 'conflict-non-utf8' => 'when the conflicts contain a non-UTF-8 file'
}.freeze
UNRESOLVABLE_CONFLICTS.each do |source_branch, description|
diff --git a/spec/features/merge_requests/diff_notes_avatars_spec.rb b/spec/features/merge_requests/diff_notes_avatars_spec.rb
index b2e170513c4..ccf047d3efa 100644
--- a/spec/features/merge_requests/diff_notes_avatars_spec.rb
+++ b/spec/features/merge_requests/diff_notes_avatars_spec.rb
@@ -91,7 +91,7 @@ feature 'Diff note avatars', feature: true, js: true do
page.within find("[id='#{position.line_code(project.repository)}']") do
find('.diff-notes-collapse').click
- expect(first('img.js-diff-comment-avatar')["title"]).to eq("#{note.author.name}: #{note.note.truncate(17)}")
+ expect(first('img.js-diff-comment-avatar')["data-original-title"]).to eq("#{note.author.name}: #{note.note.truncate(17)}")
end
end
diff --git a/spec/features/merge_requests/diff_notes_resolve_spec.rb b/spec/features/merge_requests/diff_notes_resolve_spec.rb
index 0e23c3a8849..4d549f3bdbb 100644
--- a/spec/features/merge_requests/diff_notes_resolve_spec.rb
+++ b/spec/features/merge_requests/diff_notes_resolve_spec.rb
@@ -275,7 +275,7 @@ feature 'Diff notes resolve', feature: true, js: true do
end
page.within '.line-resolve-all-container' do
- page.find('.discussion-next-btn').click
+ page.find('.discussion-next-btn').trigger('click')
end
expect(page.evaluate_script("$('body').scrollTop()")).to be > 0
diff --git a/spec/features/merge_requests/diffs_spec.rb b/spec/features/merge_requests/diffs_spec.rb
index 7dee3b852ca..4860a2a7498 100644
--- a/spec/features/merge_requests/diffs_spec.rb
+++ b/spec/features/merge_requests/diffs_spec.rb
@@ -20,6 +20,34 @@ feature 'Diffs URL', js: true, feature: true do
end
end
+ context 'when linking to note' do
+ describe 'with unresolved note' do
+ let(:note) { create :diff_note_on_merge_request, project: project, noteable: merge_request }
+ let(:fragment) { "#note_#{note.id}" }
+
+ before do
+ visit "#{diffs_namespace_project_merge_request_path(project.namespace, project, merge_request)}#{fragment}"
+ end
+
+ it 'shows expanded note' do
+ expect(page).to have_selector(fragment, visible: true)
+ end
+ end
+
+ describe 'with resolved note' do
+ let(:note) { create :diff_note_on_merge_request, :resolved, project: project, noteable: merge_request }
+ let(:fragment) { "#note_#{note.id}" }
+
+ before do
+ visit "#{diffs_namespace_project_merge_request_path(project.namespace, project, merge_request)}#{fragment}"
+ end
+
+ it 'shows expanded note' do
+ expect(page).to have_selector(fragment, visible: true)
+ end
+ end
+ end
+
context 'when merge request has overflow' do
it 'displays warning' do
allow(Commit).to receive(:max_diff_options).and_return(max_files: 3)
diff --git a/spec/features/merge_requests/merge_immediately_with_pipeline_spec.rb b/spec/features/merge_requests/merge_immediately_with_pipeline_spec.rb
index 5820784f8e7..c102722d6db 100644
--- a/spec/features/merge_requests/merge_immediately_with_pipeline_spec.rb
+++ b/spec/features/merge_requests/merge_immediately_with_pipeline_spec.rb
@@ -34,11 +34,13 @@ feature 'Merge immediately', :feature, :js do
page.within '.mr-widget-body' do
find('.dropdown-toggle').click
- click_link 'Merge immediately'
+ Sidekiq::Testing.fake! do
+ click_link 'Merge immediately'
- expect(find('.accept-merge-request.btn-info')).to have_content('Merge in progress')
+ expect(find('.accept-merge-request.btn-info')).to have_content('Merge in progress')
- wait_for_ajax
+ wait_for_vue_resource
+ end
end
end
end
diff --git a/spec/features/profiles/preferences_spec.rb b/spec/features/profiles/preferences_spec.rb
index 15c8677fcd3..d368bc4d753 100644
--- a/spec/features/profiles/preferences_spec.rb
+++ b/spec/features/profiles/preferences_spec.rb
@@ -44,7 +44,7 @@ describe 'Profile > Preferences', feature: true do
expect(page.current_path).to eq starred_dashboard_projects_path
end
- click_link 'Your projects'
+ find('.shortcuts-activity').trigger('click')
expect(page).not_to have_content("You don't have starred projects yet")
expect(page.current_path).to eq dashboard_projects_path
diff --git a/spec/features/projects/blobs/blob_show_spec.rb b/spec/features/projects/blobs/blob_show_spec.rb
index 5955623f565..fc242082278 100644
--- a/spec/features/projects/blobs/blob_show_spec.rb
+++ b/spec/features/projects/blobs/blob_show_spec.rb
@@ -5,13 +5,13 @@ feature 'File blob', :js, feature: true do
def visit_blob(path, fragment = nil)
visit namespace_project_blob_path(project.namespace, project, File.join('master', path), anchor: fragment)
+
+ wait_for_ajax
end
context 'Ruby file' do
before do
visit_blob('files/ruby/popen.rb')
-
- wait_for_ajax
end
it 'displays the blob' do
@@ -35,8 +35,6 @@ feature 'File blob', :js, feature: true do
context 'visiting directly' do
before do
visit_blob('files/markdown/ruby-style-guide.md')
-
- wait_for_ajax
end
it 'displays the blob using the rich viewer' do
@@ -104,8 +102,6 @@ feature 'File blob', :js, feature: true do
context 'visiting with a line number anchor' do
before do
visit_blob('files/markdown/ruby-style-guide.md', 'L1')
-
- wait_for_ajax
end
it 'displays the blob using the simple viewer' do
@@ -148,8 +144,6 @@ feature 'File blob', :js, feature: true do
project.update_attribute(:lfs_enabled, true)
visit_blob('files/lfs/file.md')
-
- wait_for_ajax
end
it 'displays an error' do
@@ -198,8 +192,6 @@ feature 'File blob', :js, feature: true do
context 'when LFS is disabled on the project' do
before do
visit_blob('files/lfs/file.md')
-
- wait_for_ajax
end
it 'displays the blob' do
@@ -235,8 +227,6 @@ feature 'File blob', :js, feature: true do
).execute
visit_blob('files/test.pdf')
-
- wait_for_ajax
end
it 'displays the blob' do
@@ -263,8 +253,6 @@ feature 'File blob', :js, feature: true do
project.update_attribute(:lfs_enabled, true)
visit_blob('files/lfs/lfs_object.iso')
-
- wait_for_ajax
end
it 'displays the blob' do
@@ -287,8 +275,6 @@ feature 'File blob', :js, feature: true do
context 'when LFS is disabled on the project' do
before do
visit_blob('files/lfs/lfs_object.iso')
-
- wait_for_ajax
end
it 'displays the blob' do
@@ -312,8 +298,6 @@ feature 'File blob', :js, feature: true do
context 'ZIP file' do
before do
visit_blob('Gemfile.zip')
-
- wait_for_ajax
end
it 'displays the blob' do
@@ -348,8 +332,6 @@ feature 'File blob', :js, feature: true do
).execute
visit_blob('files/empty.md')
-
- wait_for_ajax
end
it 'displays an error' do
@@ -369,4 +351,116 @@ feature 'File blob', :js, feature: true do
end
end
end
+
+ context '.gitlab-ci.yml' do
+ before do
+ project.add_master(project.creator)
+
+ Files::CreateService.new(
+ project,
+ project.creator,
+ start_branch: 'master',
+ branch_name: 'master',
+ commit_message: "Add .gitlab-ci.yml",
+ file_path: '.gitlab-ci.yml',
+ file_content: File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml'))
+ ).execute
+
+ visit_blob('.gitlab-ci.yml')
+ end
+
+ it 'displays an auxiliary viewer' do
+ aggregate_failures do
+ # shows that configuration is valid
+ expect(page).to have_content('This GitLab CI configuration is valid.')
+
+ # shows a learn more link
+ expect(page).to have_link('Learn more')
+ end
+ end
+ end
+
+ context '.gitlab/route-map.yml' do
+ before do
+ project.add_master(project.creator)
+
+ Files::CreateService.new(
+ project,
+ project.creator,
+ start_branch: 'master',
+ branch_name: 'master',
+ commit_message: "Add .gitlab/route-map.yml",
+ file_path: '.gitlab/route-map.yml',
+ file_content: <<-MAP.strip_heredoc
+ # Team data
+ - source: 'data/team.yml'
+ public: 'team/'
+ MAP
+ ).execute
+
+ visit_blob('.gitlab/route-map.yml')
+ end
+
+ it 'displays an auxiliary viewer' do
+ aggregate_failures do
+ # shows that map is valid
+ expect(page).to have_content('This Route Map is valid.')
+
+ # shows a learn more link
+ expect(page).to have_link('Learn more')
+ end
+ end
+ end
+
+ context 'LICENSE' do
+ before do
+ visit_blob('LICENSE')
+ end
+
+ it 'displays an auxiliary viewer' do
+ aggregate_failures do
+ # shows license
+ expect(page).to have_content('This project is licensed under the MIT License.')
+
+ # shows a learn more link
+ expect(page).to have_link('Learn more', 'http://choosealicense.com/licenses/mit/')
+ end
+ end
+ end
+
+ context '*.gemspec' do
+ before do
+ project.add_master(project.creator)
+
+ Files::CreateService.new(
+ project,
+ project.creator,
+ start_branch: 'master',
+ branch_name: 'master',
+ commit_message: "Add activerecord.gemspec",
+ file_path: 'activerecord.gemspec',
+ file_content: <<-SPEC.strip_heredoc
+ Gem::Specification.new do |s|
+ s.platform = Gem::Platform::RUBY
+ s.name = "activerecord"
+ end
+ SPEC
+ ).execute
+
+ visit_blob('activerecord.gemspec')
+ end
+
+ it 'displays an auxiliary viewer' do
+ aggregate_failures do
+ # shows names of dependency manager and package
+ expect(page).to have_content('This project manages its dependencies using RubyGems and defines a gem named activerecord.')
+
+ # shows a link to the gem
+ expect(page).to have_link('activerecord', 'https://rubygems.org/gems/activerecord')
+
+ # shows a learn more link
+ expect(page).to have_link('Learn more', 'http://choosealicense.com/licenses/mit/')
+ end
+ end
+ end
end
diff --git a/spec/features/projects/branches/new_branch_ref_dropdown_spec.rb b/spec/features/projects/branches/new_branch_ref_dropdown_spec.rb
index cfc782c98ad..c5e0a0f0517 100644
--- a/spec/features/projects/branches/new_branch_ref_dropdown_spec.rb
+++ b/spec/features/projects/branches/new_branch_ref_dropdown_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe 'New Branch Ref Dropdown', :js, :feature do
let(:user) { create(:user) }
let(:project) { create(:project, :public) }
- let(:toggle) { find('.create-from .dropdown-toggle') }
+ let(:toggle) { find('.create-from .dropdown-menu-toggle') }
before do
project.add_master(user)
diff --git a/spec/features/projects/features_visibility_spec.rb b/spec/features/projects/features_visibility_spec.rb
index e1781cf320a..4533a6fb144 100644
--- a/spec/features/projects/features_visibility_spec.rb
+++ b/spec/features/projects/features_visibility_spec.rb
@@ -74,7 +74,7 @@ describe 'Edit Project Settings', feature: true do
issues: namespace_project_issues_path(project.namespace, project),
wiki: namespace_project_wiki_path(project.namespace, project, :home),
snippets: namespace_project_snippets_path(project.namespace, project),
- merge_requests: namespace_project_merge_requests_path(project.namespace, project),
+ merge_requests: namespace_project_merge_requests_path(project.namespace, project)
}
end
diff --git a/spec/features/projects/files/browse_files_spec.rb b/spec/features/projects/files/browse_files_spec.rb
index 70e96efd557..4166aec1956 100644
--- a/spec/features/projects/files/browse_files_spec.rb
+++ b/spec/features/projects/files/browse_files_spec.rb
@@ -31,4 +31,16 @@ feature 'user browses project', feature: true, js: true do
expect(page).to have_content 'oid sha256:91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897'
expect(page).to have_content 'size 1575078'
end
+
+ scenario 'can see last commit for current directory' do
+ last_commit = project.repository.last_commit_for_path(project.default_branch, 'files')
+
+ click_link 'files'
+ wait_for_ajax
+
+ page.within('.blob-commit-info') do
+ expect(page).to have_content last_commit.short_id
+ expect(page).to have_content last_commit.author_name
+ end
+ end
end
diff --git a/spec/features/projects/gfm_autocomplete_load_spec.rb b/spec/features/projects/gfm_autocomplete_load_spec.rb
index dd9622f16a0..67bc9142356 100644
--- a/spec/features/projects/gfm_autocomplete_load_spec.rb
+++ b/spec/features/projects/gfm_autocomplete_load_spec.rb
@@ -10,7 +10,7 @@ describe 'GFM autocomplete loading', feature: true, js: true do
end
it 'does not load on project#show' do
- expect(evaluate_script('gl.GfmAutoComplete.dataSources')).to eq({})
+ expect(evaluate_script('gl.GfmAutoComplete')).to eq(nil)
end
it 'loads on new issue page' do
diff --git a/spec/features/projects/pipeline_schedules_spec.rb b/spec/features/projects/pipeline_schedules_spec.rb
index cdac4fe2111..03a30bfb996 100644
--- a/spec/features/projects/pipeline_schedules_spec.rb
+++ b/spec/features/projects/pipeline_schedules_spec.rb
@@ -6,6 +6,7 @@ feature 'Pipeline Schedules', :feature do
let!(:project) { create(:project) }
let!(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project) }
+ let!(:pipeline) { create(:ci_pipeline, pipeline_schedule: pipeline_schedule) }
let(:scope) { nil }
let!(:user) { create(:user) }
@@ -32,7 +33,7 @@ feature 'Pipeline Schedules', :feature do
page.within('.pipeline-schedule-table-row') do
expect(page).to have_content('pipeline schedule')
expect(page).to have_link('master')
- expect(page).to have_content('None')
+ expect(page).to have_link("##{pipeline.id}")
end
end
@@ -69,6 +70,11 @@ feature 'Pipeline Schedules', :feature do
describe 'POST /projects/pipeline_schedules/new', js: true do
let(:visit_page) { visit_new_pipeline_schedule }
+ it 'sets defaults for timezone and target branch' do
+ expect(page).to have_button('master')
+ expect(page).to have_button('UTC')
+ end
+
it 'it creates a new scheduled pipeline' do
fill_in_schedule_form
save_pipeline_schedule
@@ -117,12 +123,12 @@ feature 'Pipeline Schedules', :feature do
end
def select_timezone
- click_button 'Select a timezone'
+ find('.js-timezone-dropdown').click
click_link 'American Samoa'
end
def select_target_branch
- click_button 'Select target branch'
+ find('.js-target-branch-dropdown').click
click_link 'master'
end
diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb
index 8cc96c7b00f..5f82cf2f5e5 100644
--- a/spec/features/projects/pipelines/pipelines_spec.rb
+++ b/spec/features/projects/pipelines/pipelines_spec.rb
@@ -22,7 +22,7 @@ describe 'Pipelines', :feature, :js do
project: project,
ref: 'master',
status: 'running',
- sha: project.commit.id,
+ sha: project.commit.id
)
end
diff --git a/spec/features/projects/settings/integration_settings_spec.rb b/spec/features/projects/settings/integration_settings_spec.rb
index 7909234556e..d3232f0cc16 100644
--- a/spec/features/projects/settings/integration_settings_spec.rb
+++ b/spec/features/projects/settings/integration_settings_spec.rb
@@ -52,6 +52,7 @@ feature 'Integration settings', feature: true do
fill_in 'hook_url', with: url
check 'Tag push events'
check 'Enable SSL verification'
+ check 'Job events'
click_button 'Add webhook'
@@ -59,6 +60,7 @@ feature 'Integration settings', feature: true do
expect(page).to have_content('SSL Verification: enabled')
expect(page).to have_content('Push Events')
expect(page).to have_content('Tag Push Events')
+ expect(page).to have_content('Job events')
end
scenario 'edit existing webhook' do
diff --git a/spec/features/projects/snippets_spec.rb b/spec/features/projects/snippets_spec.rb
index d37e8ed4699..18689c17fe9 100644
--- a/spec/features/projects/snippets_spec.rb
+++ b/spec/features/projects/snippets_spec.rb
@@ -4,11 +4,27 @@ describe 'Project snippets', feature: true do
context 'when the project has snippets' do
let(:project) { create(:empty_project, :public) }
let!(:snippets) { create_list(:project_snippet, 2, :public, author: project.owner, project: project) }
- before do
- allow(Snippet).to receive(:default_per_page).and_return(1)
- visit namespace_project_snippets_path(project.namespace, project)
+ let!(:other_snippet) { create(:project_snippet) }
+
+ context 'pagination' do
+ before do
+ allow(Snippet).to receive(:default_per_page).and_return(1)
+
+ visit namespace_project_snippets_path(project.namespace, project)
+ end
+
+ it_behaves_like 'paginated snippets'
end
- it_behaves_like 'paginated snippets'
+ context 'list content' do
+ it 'contains all project snippets' do
+ visit namespace_project_snippets_path(project.namespace, project)
+
+ expect(page).to have_selector('.snippet-row', count: 2)
+
+ expect(page).to have_content(snippets[0].title)
+ expect(page).to have_content(snippets[1].title)
+ end
+ end
end
end
diff --git a/spec/features/projects/wiki/markdown_preview_spec.rb b/spec/features/projects/wiki/markdown_preview_spec.rb
index 43d8b45669e..49d7ef09e64 100644
--- a/spec/features/projects/wiki/markdown_preview_spec.rb
+++ b/spec/features/projects/wiki/markdown_preview_spec.rb
@@ -17,14 +17,14 @@ feature 'Projects > Wiki > User previews markdown changes', feature: true, js: t
login_as(user)
visit namespace_project_path(project.namespace, project)
- click_link 'Wiki'
+ find('.shortcuts-wiki').trigger('click')
WikiPages::CreateService.new(project, user, title: 'home', content: 'Home page').execute
end
context "while creating a new wiki page" do
context "when there are no spaces or hyphens in the page name" do
it "rewrites relative links as expected" do
- click_link 'New page'
+ find('.add-new-wiki').trigger('click')
page.within '#modal-new-wiki' do
fill_in :new_wiki_path, with: 'a/b/c/d'
click_button 'Create page'
@@ -73,7 +73,7 @@ feature 'Projects > Wiki > User previews markdown changes', feature: true, js: t
fill_in :new_wiki_path, with: 'a-page/b-page/c-page/d-page'
click_button 'Create page'
end
-
+
page.within '.wiki-form' do
fill_in :wiki_content, with: wiki_content
click_on "Preview"
@@ -91,7 +91,7 @@ feature 'Projects > Wiki > User previews markdown changes', feature: true, js: t
context "while editing a wiki page" do
def create_wiki_page(path)
- click_link 'New page'
+ find('.add-new-wiki').trigger('click')
page.within '#modal-new-wiki' do
fill_in :new_wiki_path, with: path
diff --git a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb
index 1ffac8cd542..5c502ce4fb5 100644
--- a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb
+++ b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Projects > Wiki > User creates wiki page', feature: true do
+feature 'Projects > Wiki > User creates wiki page', js: true, feature: true do
let(:user) { create(:user) }
background do
@@ -8,7 +8,7 @@ feature 'Projects > Wiki > User creates wiki page', feature: true do
login_as(user)
visit namespace_project_path(project.namespace, project)
- click_link 'Wiki'
+ find('.shortcuts-wiki').trigger('click')
end
context 'in the user namespace' do
diff --git a/spec/features/protected_branches_spec.rb b/spec/features/protected_branches_spec.rb
index fc9b293c393..884d1bbb10c 100644
--- a/spec/features/protected_branches_spec.rb
+++ b/spec/features/protected_branches_spec.rb
@@ -1,5 +1,4 @@
require 'spec_helper'
-Dir["./spec/features/protected_branches/*.rb"].sort.each { |f| require f }
feature 'Projected Branches', feature: true, js: true do
let(:user) { create(:user, :admin) }
diff --git a/spec/features/protected_tags_spec.rb b/spec/features/protected_tags_spec.rb
index e68448467b0..66236dbc7fc 100644
--- a/spec/features/protected_tags_spec.rb
+++ b/spec/features/protected_tags_spec.rb
@@ -1,5 +1,4 @@
require 'spec_helper'
-Dir["./spec/features/protected_tags/*.rb"].sort.each { |f| require f }
feature 'Projected Tags', feature: true, js: true do
let(:user) { create(:user, :admin) }
diff --git a/spec/features/search_spec.rb b/spec/features/search_spec.rb
index f2866d071dc..2fda7758407 100644
--- a/spec/features/search_spec.rb
+++ b/spec/features/search_spec.rb
@@ -27,7 +27,7 @@ describe "Search", feature: true do
end
it 'shows group name after filtering' do
- find('.js-search-group-dropdown').click
+ find('.js-search-group-dropdown').trigger('click')
wait_for_ajax
page.within '.search-holder' do
@@ -38,7 +38,7 @@ describe "Search", feature: true do
end
it 'filters by group projects after filtering by group' do
- find('.js-search-group-dropdown').click
+ find('.js-search-group-dropdown').trigger('click')
wait_for_ajax
page.within '.search-holder' do
@@ -48,7 +48,7 @@ describe "Search", feature: true do
expect(find('.js-search-group-dropdown')).to have_content(group.name)
page.within('.project-filter') do
- find('.js-search-project-dropdown').click
+ find('.js-search-project-dropdown').trigger('click')
wait_for_ajax
expect(page).to have_link(group_project.name_with_namespace)
@@ -57,7 +57,7 @@ describe "Search", feature: true do
it 'shows project name after filtering' do
page.within('.project-filter') do
- find('.js-search-project-dropdown').click
+ find('.js-search-project-dropdown').trigger('click')
wait_for_ajax
click_link project.name_with_namespace
diff --git a/spec/features/snippets/explore_spec.rb b/spec/features/snippets/explore_spec.rb
index 10a4597e467..fd097fe2e74 100644
--- a/spec/features/snippets/explore_spec.rb
+++ b/spec/features/snippets/explore_spec.rb
@@ -1,11 +1,11 @@
require 'rails_helper'
feature 'Explore Snippets', feature: true do
- scenario 'User should see snippets that are not private' do
- public_snippet = create(:personal_snippet, :public)
- internal_snippet = create(:personal_snippet, :internal)
- private_snippet = create(:personal_snippet, :private)
+ let!(:public_snippet) { create(:personal_snippet, :public) }
+ let!(:internal_snippet) { create(:personal_snippet, :internal) }
+ let!(:private_snippet) { create(:personal_snippet, :private) }
+ scenario 'User should see snippets that are not private' do
login_as create(:user)
visit explore_snippets_path
@@ -13,4 +13,21 @@ feature 'Explore Snippets', feature: true do
expect(page).to have_content(internal_snippet.title)
expect(page).not_to have_content(private_snippet.title)
end
+
+ scenario 'External user should see only public snippets' do
+ login_as create(:user, :external)
+ visit explore_snippets_path
+
+ expect(page).to have_content(public_snippet.title)
+ expect(page).not_to have_content(internal_snippet.title)
+ expect(page).not_to have_content(private_snippet.title)
+ end
+
+ scenario 'Not authenticated user should see only public snippets' do
+ visit explore_snippets_path
+
+ expect(page).to have_content(public_snippet.title)
+ expect(page).not_to have_content(internal_snippet.title)
+ expect(page).not_to have_content(private_snippet.title)
+ end
end
diff --git a/spec/features/snippets/internal_snippet_spec.rb b/spec/features/snippets/internal_snippet_spec.rb
new file mode 100644
index 00000000000..93382f4c359
--- /dev/null
+++ b/spec/features/snippets/internal_snippet_spec.rb
@@ -0,0 +1,23 @@
+require 'rails_helper'
+
+feature 'Internal Snippets', feature: true, js: true do
+ let(:internal_snippet) { create(:personal_snippet, :internal) }
+
+ describe 'normal user' do
+ before do
+ login_as :user
+ end
+
+ scenario 'sees internal snippets' do
+ visit snippet_path(internal_snippet)
+
+ expect(page).to have_content(internal_snippet.content)
+ end
+
+ scenario 'sees raw internal snippets' do
+ visit raw_snippet_path(internal_snippet)
+
+ expect(page).to have_content(internal_snippet.content)
+ end
+ end
+end
diff --git a/spec/features/tags/master_creates_tag_spec.rb b/spec/features/tags/master_creates_tag_spec.rb
index ca25c696f75..af25eebed13 100644
--- a/spec/features/tags/master_creates_tag_spec.rb
+++ b/spec/features/tags/master_creates_tag_spec.rb
@@ -51,10 +51,24 @@ feature 'Master creates tag', feature: true do
end
end
+ scenario 'opens dropdown for ref', js: true do
+ click_link 'New tag'
+ ref_row = find('.form-group:nth-of-type(2) .col-sm-10')
+ page.within ref_row do
+ ref_input = find('[name="ref"]', visible: false)
+ expect(ref_input.value).to eq 'master'
+ expect(find('.dropdown-toggle-text')).to have_content 'master'
+
+ find('.js-branch-select').trigger('click')
+
+ expect(find('.dropdown-menu')).to have_content 'empty-branch'
+ end
+ end
+
def create_tag_in_form(tag:, ref:, message: nil, desc: nil)
click_link 'New tag'
fill_in 'tag_name', with: tag
- fill_in 'ref', with: ref
+ find('#ref', visible: false).set(ref)
fill_in 'message', with: message unless message.nil?
fill_in 'release_description', with: desc unless desc.nil?
click_button 'Create tag'
diff --git a/spec/features/todos/todos_spec.rb b/spec/features/todos/todos_spec.rb
index be5b3af417f..55b3e3d9424 100644
--- a/spec/features/todos/todos_spec.rb
+++ b/spec/features/todos/todos_spec.rb
@@ -251,7 +251,7 @@ describe 'Dashboard Todos', feature: true do
describe 'mark all as done', js: true do
before do
visit dashboard_todos_path
- click_link 'Mark all as done'
+ find('.js-todos-mark-all').trigger('click')
end
it 'shows "All done" message!' do
@@ -308,9 +308,9 @@ describe 'Dashboard Todos', feature: true do
end
def mark_all_and_undo
- click_link 'Mark all as done'
+ find('.js-todos-mark-all').trigger('click')
wait_for_ajax
- click_link 'Undo mark all as done'
+ find('.js-todos-undo-all').trigger('click')
wait_for_ajax
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
index 0c160dd74b4..8f03024ea06 100644
--- a/spec/features/uploads/user_uploads_file_to_note_spec.rb
+++ b/spec/features/uploads/user_uploads_file_to_note_spec.rb
@@ -5,18 +5,78 @@ feature 'User uploads file to note', feature: true do
let(:user) { create(:user) }
let(:project) { create(:empty_project, creator: user, namespace: user.namespace) }
+ let(:issue) { create(:issue, project: project, author: user) }
- scenario 'they see the attached file', js: true do
- issue = create(:issue, project: project, author: user)
-
+ before do
login_as(user)
visit namespace_project_issue_path(project.namespace, project, issue)
+ end
+
+ context 'before uploading' do
+ it 'shows "Attach a file" button', js: true do
+ expect(page).to have_button('Attach a file')
+ expect(page).not_to have_selector('.uploading-progress-container', visible: true)
+ end
+ end
+
+ context 'uploading is in progress' do
+ it 'shows "Cancel" button on uploading', js: true do
+ dropzone_file([Rails.root.join('spec', 'fixtures', 'dk.png')], 0, false)
+
+ expect(page).to have_button('Cancel')
+ end
+
+ it 'cancels uploading on clicking to "Cancel" button', js: true do
+ dropzone_file([Rails.root.join('spec', 'fixtures', 'dk.png')], 0, false)
+
+ click_button 'Cancel'
+
+ expect(page).to have_button('Attach a file')
+ expect(page).not_to have_button('Cancel')
+ expect(page).not_to have_selector('.uploading-progress-container', visible: true)
+ end
+
+ it 'shows "Attaching a file" message on uploading 1 file', js: true do
+ dropzone_file([Rails.root.join('spec', 'fixtures', 'dk.png')], 0, false)
+
+ expect(page).to have_selector('.attaching-file-message', visible: true, text: 'Attaching a file -')
+ end
+
+ it 'shows "Attaching 2 files" message on uploading 2 file', js: true do
+ dropzone_file([Rails.root.join('spec', 'fixtures', 'video_sample.mp4'),
+ Rails.root.join('spec', 'fixtures', 'dk.png')], 0, false)
+
+ expect(page).to have_selector('.attaching-file-message', visible: true, text: 'Attaching 2 files -')
+ end
+
+ it 'shows error message, "retry" and "attach a new file" link a if file is too big', js: true do
+ dropzone_file([Rails.root.join('spec', 'fixtures', 'video_sample.mp4')], 0.01)
+
+ error_text = 'File is too big (0.06MiB). Max filesize: 0.01MiB.'
+
+ expect(page).to have_selector('.uploading-error-message', visible: true, text: error_text)
+ expect(page).to have_selector('.retry-uploading-link', visible: true, text: 'Try again')
+ expect(page).to have_selector('.attach-new-file', visible: true, text: 'attach a new file')
+ expect(page).not_to have_button('Attach a file')
+ end
+ end
+
+ context 'uploading is complete' do
+ it 'shows "Attach a file" button on uploading complete', js: true do
+ dropzone_file([Rails.root.join('spec', 'fixtures', 'dk.png')])
+ wait_for_ajax
+
+ expect(page).to have_button('Attach a file')
+ expect(page).not_to have_selector('.uploading-progress-container', visible: true)
+ end
- dropzone_file(Rails.root.join('spec', 'fixtures', 'dk.png'))
- click_button 'Comment'
- wait_for_ajax
+ scenario 'they see the attached file', js: true do
+ 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$})
+ expect(find('a.no-attachment-icon img[alt="dk"]')['src'])
+ .to match(%r{/#{project.full_path}/uploads/\h{32}/dk\.png$})
+ end
end
end
diff --git a/spec/features/user_callout_spec.rb b/spec/features/user_callout_spec.rb
index 848af5e3a4d..b84f834ff1e 100644
--- a/spec/features/user_callout_spec.rb
+++ b/spec/features/user_callout_spec.rb
@@ -20,7 +20,7 @@ describe 'User Callouts', js: true do
visit dashboard_projects_path
within('.user-callout') do
- find('.close').click
+ find('.close').trigger('click')
end
visit dashboard_projects_path
diff --git a/spec/features/users/snippets_spec.rb b/spec/features/users/snippets_spec.rb
index 1546a06b80c..4efbd672322 100644
--- a/spec/features/users/snippets_spec.rb
+++ b/spec/features/users/snippets_spec.rb
@@ -3,14 +3,46 @@ require 'spec_helper'
describe 'Snippets tab on a user profile', feature: true, js: true do
context 'when the user has snippets' do
let(:user) { create(:user) }
- let!(:snippets) { create_list(:snippet, 2, :public, author: user) }
- before do
- allow(Snippet).to receive(:default_per_page).and_return(1)
- visit user_path(user)
- page.within('.user-profile-nav') { click_link 'Snippets' }
- wait_for_ajax
+
+ context 'pagination' do
+ let!(:snippets) { create_list(:snippet, 2, :public, author: user) }
+
+ before do
+ allow(Snippet).to receive(:default_per_page).and_return(1)
+ visit user_path(user)
+ page.within('.user-profile-nav') { click_link 'Snippets' }
+ wait_for_ajax
+ end
+
+ it_behaves_like 'paginated snippets', remote: true
end
- it_behaves_like 'paginated snippets', remote: true
+ context 'list content' do
+ let!(:public_snippet) { create(:snippet, :public, author: user) }
+ let!(:internal_snippet) { create(:snippet, :internal, author: user) }
+ let!(:private_snippet) { create(:snippet, :private, author: user) }
+ let!(:other_snippet) { create(:snippet, :public) }
+
+ it 'contains only internal and public snippets of a user when a user is logged in' do
+ login_as(:user)
+ visit user_path(user)
+ page.within('.user-profile-nav') { click_link 'Snippets' }
+ wait_for_ajax
+
+ expect(page).to have_selector('.snippet-row', count: 2)
+
+ expect(page).to have_content(public_snippet.title)
+ expect(page).to have_content(internal_snippet.title)
+ end
+
+ it 'contains only public snippets of a user when a user is not logged in' do
+ visit user_path(user)
+ page.within('.user-profile-nav') { click_link 'Snippets' }
+ wait_for_ajax
+
+ expect(page).to have_selector('.snippet-row', count: 1)
+ expect(page).to have_content(public_snippet.title)
+ end
+ end
end
end
diff --git a/spec/finders/groups_finder_spec.rb b/spec/finders/groups_finder_spec.rb
index d5d111e8d15..5b3591550c1 100644
--- a/spec/finders/groups_finder_spec.rb
+++ b/spec/finders/groups_finder_spec.rb
@@ -3,29 +3,64 @@ require 'spec_helper'
describe GroupsFinder do
describe '#execute' do
let(:user) { create(:user) }
- let!(:private_group) { create(:group, :private) }
- let!(:internal_group) { create(:group, :internal) }
- let!(:public_group) { create(:group, :public) }
- let(:finder) { described_class.new }
- describe 'execute' do
- describe 'without a user' do
- subject { finder.execute }
+ context 'root level groups' do
+ let!(:private_group) { create(:group, :private) }
+ let!(:internal_group) { create(:group, :internal) }
+ let!(:public_group) { create(:group, :public) }
+
+ context 'without a user' do
+ subject { described_class.new.execute }
it { is_expected.to eq([public_group]) }
end
- describe 'with a user' do
- subject { finder.execute(user) }
+ context 'with a user' do
+ subject { described_class.new(user).execute }
context 'normal user' do
- it { is_expected.to eq([public_group, internal_group]) }
+ it { is_expected.to contain_exactly(public_group, internal_group) }
end
context 'external user' do
let(:user) { create(:user, external: true) }
- it { is_expected.to eq([public_group]) }
+ it { is_expected.to contain_exactly(public_group) }
+ end
+
+ context 'user is member of the private group' do
+ before do
+ private_group.add_guest(user)
+ end
+
+ it { is_expected.to contain_exactly(public_group, internal_group, private_group) }
+ end
+ end
+ end
+
+ context 'subgroups' do
+ let!(:parent_group) { create(:group, :public) }
+ let!(:public_subgroup) { create(:group, :public, parent: parent_group) }
+ let!(:internal_subgroup) { create(:group, :internal, parent: parent_group) }
+ let!(:private_subgroup) { create(:group, :private, parent: parent_group) }
+
+ context 'without a user' do
+ it 'only returns public subgroups' do
+ expect(described_class.new(nil, parent: parent_group).execute).to contain_exactly(public_subgroup)
+ end
+ end
+
+ context 'with a user' do
+ it 'returns public and internal subgroups' do
+ expect(described_class.new(user, parent: parent_group).execute).to contain_exactly(public_subgroup, internal_subgroup)
+ end
+
+ context 'being member' do
+ it 'returns public subgroups, internal subgroups, and private subgroups user is member of' do
+ private_subgroup.add_guest(user)
+
+ expect(described_class.new(user, parent: parent_group).execute).to contain_exactly(public_subgroup, internal_subgroup, private_subgroup)
+ end
end
end
end
diff --git a/spec/finders/snippets_finder_spec.rb b/spec/finders/snippets_finder_spec.rb
index cb6c80d1bd0..35f1683eef9 100644
--- a/spec/finders/snippets_finder_spec.rb
+++ b/spec/finders/snippets_finder_spec.rb
@@ -8,79 +8,145 @@ describe SnippetsFinder do
let(:project1) { create(:empty_project, :public, group: group) }
let(:project2) { create(:empty_project, :private, group: group) }
- context ':all filter' do
+ context 'all snippets visible to a user' do
let!(:snippet1) { create(:personal_snippet, :private) }
let!(:snippet2) { create(:personal_snippet, :internal) }
let!(:snippet3) { create(:personal_snippet, :public) }
+ let!(:project_snippet1) { create(:project_snippet, :private) }
+ let!(:project_snippet2) { create(:project_snippet, :internal) }
+ let!(:project_snippet3) { create(:project_snippet, :public) }
it "returns all private and internal snippets" do
- snippets = described_class.new.execute(user, filter: :all)
- expect(snippets).to include(snippet2, snippet3)
- expect(snippets).not_to include(snippet1)
+ snippets = described_class.new(user, scope: :all).execute
+ expect(snippets).to include(snippet2, snippet3, project_snippet2, project_snippet3)
+ expect(snippets).not_to include(snippet1, project_snippet1)
end
it "returns all public snippets" do
- snippets = described_class.new.execute(nil, filter: :all)
- expect(snippets).to include(snippet3)
- expect(snippets).not_to include(snippet1, snippet2)
+ snippets = described_class.new(nil, scope: :all).execute
+ expect(snippets).to include(snippet3, project_snippet3)
+ expect(snippets).not_to include(snippet1, snippet2, project_snippet1, project_snippet2)
+ end
+
+ it "returns all public and internal snippets for normal user" do
+ snippets = described_class.new(user).execute
+
+ expect(snippets).to include(snippet2, snippet3, project_snippet2, project_snippet3)
+ expect(snippets).not_to include(snippet1, project_snippet1)
+ end
+
+ it "returns all public snippets for non authorized user" do
+ snippets = described_class.new(nil).execute
+
+ expect(snippets).to include(snippet3, project_snippet3)
+ expect(snippets).not_to include(snippet1, snippet2, project_snippet1, project_snippet2)
+ end
+
+ it "returns all public and authored snippets for external user" do
+ external_user = create(:user, :external)
+ authored_snippet = create(:personal_snippet, :internal, author: external_user)
+
+ snippets = described_class.new(external_user).execute
+
+ expect(snippets).to include(snippet3, project_snippet3, authored_snippet)
+ expect(snippets).not_to include(snippet1, snippet2, project_snippet1, project_snippet2)
end
end
- context ':public filter' do
+ context 'filter by visibility' do
let!(:snippet1) { create(:personal_snippet, :private) }
let!(:snippet2) { create(:personal_snippet, :internal) }
let!(:snippet3) { create(:personal_snippet, :public) }
- it "returns public public snippets" do
- snippets = described_class.new.execute(nil, filter: :public)
+ it "returns public snippets when visibility is PUBLIC" do
+ snippets = described_class.new(nil, visibility: Snippet::PUBLIC).execute
expect(snippets).to include(snippet3)
expect(snippets).not_to include(snippet1, snippet2)
end
end
- context ':by_user filter' do
+ context 'filter by scope' do
+ let!(:snippet1) { create(:personal_snippet, :private, author: user) }
+ let!(:snippet2) { create(:personal_snippet, :internal, author: user) }
+ let!(:snippet3) { create(:personal_snippet, :public, author: user) }
+
+ it "returns all snippets for 'all' scope" do
+ snippets = described_class.new(user, scope: :all).execute
+
+ expect(snippets).to include(snippet1, snippet2, snippet3)
+ end
+
+ it "returns all snippets for 'are_private' scope" do
+ snippets = described_class.new(user, scope: :are_private).execute
+
+ expect(snippets).to include(snippet1)
+ expect(snippets).not_to include(snippet2, snippet3)
+ end
+
+ it "returns all snippets for 'are_interna;' scope" do
+ snippets = described_class.new(user, scope: :are_internal).execute
+
+ expect(snippets).to include(snippet2)
+ expect(snippets).not_to include(snippet1, snippet3)
+ end
+
+ it "returns all snippets for 'are_private' scope" do
+ snippets = described_class.new(user, scope: :are_public).execute
+
+ expect(snippets).to include(snippet3)
+ expect(snippets).not_to include(snippet1, snippet2)
+ end
+ end
+
+ context 'filter by author' do
let!(:snippet1) { create(:personal_snippet, :private, author: user) }
let!(:snippet2) { create(:personal_snippet, :internal, author: user) }
let!(:snippet3) { create(:personal_snippet, :public, author: user) }
it "returns all public and internal snippets" do
- snippets = described_class.new.execute(user1, filter: :by_user, user: user)
+ snippets = described_class.new(user1, author: user).execute
+
expect(snippets).to include(snippet2, snippet3)
expect(snippets).not_to include(snippet1)
end
it "returns internal snippets" do
- snippets = described_class.new.execute(user, filter: :by_user, user: user, scope: "are_internal")
+ snippets = described_class.new(user, author: user, visibility: Snippet::INTERNAL).execute
+
expect(snippets).to include(snippet2)
expect(snippets).not_to include(snippet1, snippet3)
end
it "returns private snippets" do
- snippets = described_class.new.execute(user, filter: :by_user, user: user, scope: "are_private")
+ snippets = described_class.new(user, author: user, visibility: Snippet::PRIVATE).execute
+
expect(snippets).to include(snippet1)
expect(snippets).not_to include(snippet2, snippet3)
end
it "returns public snippets" do
- snippets = described_class.new.execute(user, filter: :by_user, user: user, scope: "are_public")
+ snippets = described_class.new(user, author: user, visibility: Snippet::PUBLIC).execute
+
expect(snippets).to include(snippet3)
expect(snippets).not_to include(snippet1, snippet2)
end
it "returns all snippets" do
- snippets = described_class.new.execute(user, filter: :by_user, user: user)
+ snippets = described_class.new(user, author: user).execute
+
expect(snippets).to include(snippet1, snippet2, snippet3)
end
it "returns only public snippets if unauthenticated user" do
- snippets = described_class.new.execute(nil, filter: :by_user, user: user)
+ snippets = described_class.new(nil, author: user).execute
+
expect(snippets).to include(snippet3)
expect(snippets).not_to include(snippet2, snippet1)
end
end
- context 'by_project filter' do
+ context 'filter by project' do
before do
@snippet1 = create(:project_snippet, :private, project: project1)
@snippet2 = create(:project_snippet, :internal, project: project1)
@@ -88,43 +154,52 @@ describe SnippetsFinder do
end
it "returns public snippets for unauthorized user" do
- snippets = described_class.new.execute(nil, filter: :by_project, project: project1)
+ snippets = described_class.new(nil, project: project1).execute
+
expect(snippets).to include(@snippet3)
expect(snippets).not_to include(@snippet1, @snippet2)
end
it "returns public and internal snippets for non project members" do
- snippets = described_class.new.execute(user, filter: :by_project, project: project1)
+ snippets = described_class.new(user, project: project1).execute
+
expect(snippets).to include(@snippet2, @snippet3)
expect(snippets).not_to include(@snippet1)
end
it "returns public snippets for non project members" do
- snippets = described_class.new.execute(user, filter: :by_project, project: project1, scope: "are_public")
+ snippets = described_class.new(user, project: project1, visibility: Snippet::PUBLIC).execute
+
expect(snippets).to include(@snippet3)
expect(snippets).not_to include(@snippet1, @snippet2)
end
it "returns internal snippets for non project members" do
- snippets = described_class.new.execute(user, filter: :by_project, project: project1, scope: "are_internal")
+ snippets = described_class.new(user, project: project1, visibility: Snippet::INTERNAL).execute
+
expect(snippets).to include(@snippet2)
expect(snippets).not_to include(@snippet1, @snippet3)
end
it "does not return private snippets for non project members" do
- snippets = described_class.new.execute(user, filter: :by_project, project: project1, scope: "are_private")
+ snippets = described_class.new(user, project: project1, visibility: Snippet::PRIVATE).execute
+
expect(snippets).not_to include(@snippet1, @snippet2, @snippet3)
end
it "returns all snippets for project members" do
project1.team << [user, :developer]
- snippets = described_class.new.execute(user, filter: :by_project, project: project1)
+
+ snippets = described_class.new(user, project: project1).execute
+
expect(snippets).to include(@snippet1, @snippet2, @snippet3)
end
it "returns private snippets for project members" do
project1.team << [user, :developer]
- snippets = described_class.new.execute(user, filter: :by_project, project: project1, scope: "are_private")
+
+ snippets = described_class.new(user, project: project1, visibility: Snippet::PRIVATE).execute
+
expect(snippets).to include(@snippet1)
end
end
diff --git a/spec/finders/users_finder_spec.rb b/spec/finders/users_finder_spec.rb
new file mode 100644
index 00000000000..780b309b45e
--- /dev/null
+++ b/spec/finders/users_finder_spec.rb
@@ -0,0 +1,66 @@
+require 'spec_helper'
+
+describe UsersFinder do
+ describe '#execute' do
+ let!(:user1) { create(:user, username: 'johndoe') }
+ let!(:user2) { create(:user, :blocked, username: 'notsorandom') }
+ let!(:external_user) { create(:user, :external) }
+ let!(:omniauth_user) { create(:omniauth_user, provider: 'twitter', extern_uid: '123456') }
+
+ context 'with a normal user' do
+ let(:user) { create(:user) }
+
+ it 'returns all users' do
+ users = described_class.new(user).execute
+
+ expect(users).to contain_exactly(user, user1, user2, omniauth_user)
+ end
+
+ it 'filters by username' do
+ users = described_class.new(user, username: 'johndoe').execute
+
+ expect(users).to contain_exactly(user1)
+ end
+
+ it 'filters by search' do
+ users = described_class.new(user, search: 'orando').execute
+
+ expect(users).to contain_exactly(user2)
+ end
+
+ it 'filters by blocked users' do
+ users = described_class.new(user, blocked: true).execute
+
+ expect(users).to contain_exactly(user2)
+ end
+
+ it 'filters by active users' do
+ users = described_class.new(user, active: true).execute
+
+ expect(users).to contain_exactly(user, user1, omniauth_user)
+ end
+
+ it 'returns no external users' do
+ users = described_class.new(user, external: true).execute
+
+ expect(users).to contain_exactly(user, user1, user2, omniauth_user)
+ end
+ end
+
+ context 'with an admin user' do
+ let(:admin) { create(:admin) }
+
+ it 'filters by external users' do
+ users = described_class.new(admin, external: true).execute
+
+ expect(users).to contain_exactly(external_user)
+ end
+
+ it 'returns all users' do
+ users = described_class.new(admin).execute
+
+ expect(users).to contain_exactly(admin, user1, user2, external_user, omniauth_user)
+ end
+ end
+ end
+end
diff --git a/spec/fixtures/api/schemas/entities/merge_request.json b/spec/fixtures/api/schemas/entities/merge_request.json
index 0a7e0e2d5f2..e5df3e7b6d1 100644
--- a/spec/fixtures/api/schemas/entities/merge_request.json
+++ b/spec/fixtures/api/schemas/entities/merge_request.json
@@ -86,6 +86,7 @@
"email_patches_path": { "type": "string" },
"plain_diff_path": { "type": "string" },
"status_path": { "type": "string" },
+ "new_blob_path": { "type": "string" },
"merge_check_path": { "type": "string" },
"ci_environments_status_path": { "type": "string" },
"merge_commit_message_with_description": { "type": "string" },
diff --git a/spec/helpers/blob_helper_spec.rb b/spec/helpers/blob_helper_spec.rb
index 1b4393e6167..41b5df12522 100644
--- a/spec/helpers/blob_helper_spec.rb
+++ b/spec/helpers/blob_helper_spec.rb
@@ -116,10 +116,11 @@ describe BlobHelper do
let(:viewer_class) do
Class.new(BlobViewer::Base) do
- self.max_size = 1.megabyte
- self.absolute_max_size = 5.megabytes
+ include BlobViewer::ServerSide
+
+ self.overridable_max_size = 1.megabyte
+ self.max_size = 5.megabytes
self.type = :rich
- self.client_side = false
end
end
diff --git a/spec/helpers/diff_helper_spec.rb b/spec/helpers/diff_helper_spec.rb
index eae097126ce..dd6566d25bb 100644
--- a/spec/helpers/diff_helper_spec.rb
+++ b/spec/helpers/diff_helper_spec.rb
@@ -122,9 +122,9 @@ describe DiffHelper do
it "returns strings with marked inline diffs" do
marked_old_line, marked_new_line = mark_inline_diffs(old_line, new_line)
- expect(marked_old_line).to eq("abc <span class='idiff left right deletion'>&#39;def&#39;</span>")
+ expect(marked_old_line).to eq(%q{abc <span class="idiff left right deletion">&#39;def&#39;</span>})
expect(marked_old_line).to be_html_safe
- expect(marked_new_line).to eq("abc <span class='idiff left right addition'>&quot;def&quot;</span>")
+ expect(marked_new_line).to eq(%q{abc <span class="idiff left right addition">&quot;def&quot;</span>})
expect(marked_new_line).to be_html_safe
end
end
diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb
index be97973c693..54c5ba57bdf 100644
--- a/spec/helpers/projects_helper_spec.rb
+++ b/spec/helpers/projects_helper_spec.rb
@@ -66,8 +66,8 @@ describe ProjectsHelper do
describe "#project_list_cache_key", redis: true do
let(:project) { create(:project) }
- it "includes the namespace" do
- expect(helper.project_list_cache_key(project)).to include(project.namespace.cache_key)
+ it "includes the route" do
+ expect(helper.project_list_cache_key(project)).to include(project.route.cache_key)
end
it "includes the project" do
diff --git a/spec/helpers/submodule_helper_spec.rb b/spec/helpers/submodule_helper_spec.rb
index 345bc33a67b..18935be95c9 100644
--- a/spec/helpers/submodule_helper_spec.rb
+++ b/spec/helpers/submodule_helper_spec.rb
@@ -81,6 +81,19 @@ describe SubmoduleHelper do
end
end
+ context 'in-repository submodule' do
+ let(:group) { create(:group, name: "Master Project", path: "master-project") }
+ let(:project) { create(:empty_project, group: group) }
+ before do
+ self.instance_variable_set(:@project, project)
+ end
+
+ it 'in-repository' do
+ stub_url('./')
+ expect(submodule_links(submodule_item)).to eq(["/master-project/#{project.path}", "/master-project/#{project.path}/tree/hash"])
+ end
+ end
+
context 'submodule on gitlab.com' do
it 'detects ssh' do
stub_url('git@gitlab.com:gitlab-org/gitlab-ce.git')
@@ -109,6 +122,18 @@ describe SubmoduleHelper do
end
context 'submodule on unsupported' do
+ it 'sanitizes unsupported protocols' do
+ stub_url('javascript:alert("XSS");')
+
+ expect(helper.submodule_links(submodule_item)).to eq([nil, nil])
+ end
+
+ it 'sanitizes unsupported protocols disguised as a repository URL' do
+ stub_url('javascript:alert("XSS");foo/bar.git')
+
+ expect(helper.submodule_links(submodule_item)).to eq([nil, nil])
+ end
+
it 'returns original' do
stub_url('http://mygitserver.com/gitlab-org/gitlab-ce')
expect(submodule_links(submodule_item)).to eq([repo.submodule_url_for, nil])
diff --git a/spec/javascripts/blob/balsamiq/balsamiq_viewer_integration_spec.js b/spec/javascripts/blob/balsamiq/balsamiq_viewer_integration_spec.js
new file mode 100644
index 00000000000..acd0aaf2a86
--- /dev/null
+++ b/spec/javascripts/blob/balsamiq/balsamiq_viewer_integration_spec.js
@@ -0,0 +1,51 @@
+/* eslint-disable import/no-unresolved */
+
+import BalsamiqViewer from '~/blob/balsamiq/balsamiq_viewer';
+import bmprPath from '../../fixtures/blob/balsamiq/test.bmpr';
+
+describe('Balsamiq integration spec', () => {
+ let container;
+ let endpoint;
+ let balsamiqViewer;
+
+ preloadFixtures('static/balsamiq_viewer.html.raw');
+
+ beforeEach(() => {
+ loadFixtures('static/balsamiq_viewer.html.raw');
+
+ container = document.getElementById('js-balsamiq-viewer');
+ balsamiqViewer = new BalsamiqViewer(container);
+ });
+
+ describe('successful response', () => {
+ beforeEach((done) => {
+ endpoint = bmprPath;
+
+ balsamiqViewer.loadFile(endpoint).then(done).catch(done.fail);
+ });
+
+ it('does not show loading icon', () => {
+ expect(document.querySelector('.loading')).toBeNull();
+ });
+
+ it('renders the balsamiq previews', () => {
+ expect(document.querySelectorAll('.previews .preview').length).not.toEqual(0);
+ });
+ });
+
+ describe('error getting file', () => {
+ beforeEach((done) => {
+ endpoint = 'invalid/path/to/file.bmpr';
+
+ balsamiqViewer.loadFile(endpoint).then(done.fail, null).catch(done);
+ });
+
+ it('does not show loading icon', () => {
+ expect(document.querySelector('.loading')).toBeNull();
+ });
+
+ it('does not render the balsamiq previews', () => {
+ expect(document.querySelectorAll('.previews .preview').length).toEqual(0);
+ });
+ });
+});
diff --git a/spec/javascripts/blob/balsamiq/balsamiq_viewer_spec.js b/spec/javascripts/blob/balsamiq/balsamiq_viewer_spec.js
index 85816ee1f11..aa87956109f 100644
--- a/spec/javascripts/blob/balsamiq/balsamiq_viewer_spec.js
+++ b/spec/javascripts/blob/balsamiq/balsamiq_viewer_spec.js
@@ -4,17 +4,11 @@ import ClassSpecHelper from '../../helpers/class_spec_helper';
describe('BalsamiqViewer', () => {
let balsamiqViewer;
- let endpoint;
let viewer;
describe('class constructor', () => {
beforeEach(() => {
- endpoint = 'endpoint';
- viewer = {
- dataset: {
- endpoint,
- },
- };
+ viewer = {};
balsamiqViewer = new BalsamiqViewer(viewer);
});
@@ -22,25 +16,25 @@ describe('BalsamiqViewer', () => {
it('should set .viewer', () => {
expect(balsamiqViewer.viewer).toBe(viewer);
});
+ });
+
+ describe('fileLoaded', () => {
- it('should set .endpoint', () => {
- expect(balsamiqViewer.endpoint).toBe(endpoint);
- });
});
describe('loadFile', () => {
let xhr;
+ let loadFile;
+ const endpoint = 'endpoint';
beforeEach(() => {
- endpoint = 'endpoint';
xhr = jasmine.createSpyObj('xhr', ['open', 'send']);
balsamiqViewer = jasmine.createSpyObj('balsamiqViewer', ['renderFile']);
- balsamiqViewer.endpoint = endpoint;
spyOn(window, 'XMLHttpRequest').and.returnValue(xhr);
- BalsamiqViewer.prototype.loadFile.call(balsamiqViewer);
+ loadFile = BalsamiqViewer.prototype.loadFile.call(balsamiqViewer, endpoint);
});
it('should call .open', () => {
@@ -54,6 +48,10 @@ describe('BalsamiqViewer', () => {
it('should call .send', () => {
expect(xhr.send).toHaveBeenCalled();
});
+
+ it('should return a promise', () => {
+ expect(loadFile).toEqual(jasmine.any(Promise));
+ });
});
describe('renderFile', () => {
@@ -325,18 +323,4 @@ describe('BalsamiqViewer', () => {
expect(parseTitle).toBe('name');
});
});
-
- describe('onError', () => {
- beforeEach(() => {
- spyOn(window, 'Flash');
-
- BalsamiqViewer.onError();
- });
-
- ClassSpecHelper.itShouldBeAStaticMethod(BalsamiqViewer, 'onError');
-
- it('should instantiate Flash', () => {
- expect(window.Flash).toHaveBeenCalledWith('Balsamiq file could not be loaded.');
- });
- });
});
diff --git a/spec/javascripts/blob/create_branch_dropdown_spec.js b/spec/javascripts/blob/create_branch_dropdown_spec.js
index c1179e572ae..9f0d373cb81 100644
--- a/spec/javascripts/blob/create_branch_dropdown_spec.js
+++ b/spec/javascripts/blob/create_branch_dropdown_spec.js
@@ -1,5 +1,4 @@
require('~/gl_dropdown');
-require('~/lib/utils/type_utility');
require('~/blob/create_branch_dropdown');
require('~/blob/target_branch_dropdown');
diff --git a/spec/javascripts/blob/target_branch_dropdown_spec.js b/spec/javascripts/blob/target_branch_dropdown_spec.js
index bb436978a0f..76ed3dc1a2d 100644
--- a/spec/javascripts/blob/target_branch_dropdown_spec.js
+++ b/spec/javascripts/blob/target_branch_dropdown_spec.js
@@ -1,5 +1,4 @@
require('~/gl_dropdown');
-require('~/lib/utils/type_utility');
require('~/blob/create_branch_dropdown');
require('~/blob/target_branch_dropdown');
diff --git a/spec/javascripts/blob/viewer/index_spec.js b/spec/javascripts/blob/viewer/index_spec.js
index 13f122b68b2..af04e7c1e72 100644
--- a/spec/javascripts/blob/viewer/index_spec.js
+++ b/spec/javascripts/blob/viewer/index_spec.js
@@ -83,25 +83,48 @@ describe('Blob viewer', () => {
});
describe('copy blob button', () => {
+ let copyButton;
+
+ beforeEach(() => {
+ copyButton = document.querySelector('.js-copy-blob-source-btn');
+ });
+
it('disabled on load', () => {
expect(
- document.querySelector('.js-copy-blob-source-btn').classList.contains('disabled'),
+ copyButton.classList.contains('disabled'),
).toBeTruthy();
});
it('has tooltip when disabled', () => {
expect(
- document.querySelector('.js-copy-blob-source-btn').getAttribute('data-original-title'),
+ copyButton.getAttribute('data-original-title'),
).toBe('Switch to the source to copy it to the clipboard');
});
+ it('is blurred when clicked and disabled', () => {
+ spyOn(copyButton, 'blur');
+
+ copyButton.click();
+
+ expect(copyButton.blur).toHaveBeenCalled();
+ });
+
+ it('is not blurred when clicked and not disabled', () => {
+ spyOn(copyButton, 'blur');
+
+ copyButton.classList.remove('disabled');
+ copyButton.click();
+
+ expect(copyButton.blur).not.toHaveBeenCalled();
+ });
+
it('enables after switching to simple view', (done) => {
document.querySelector('.js-blob-viewer-switch-btn[data-viewer="simple"]').click();
setTimeout(() => {
expect($.ajax).toHaveBeenCalled();
expect(
- document.querySelector('.js-copy-blob-source-btn').classList.contains('disabled'),
+ copyButton.classList.contains('disabled'),
).toBeFalsy();
done();
@@ -115,7 +138,7 @@ describe('Blob viewer', () => {
expect($.ajax).toHaveBeenCalled();
expect(
- document.querySelector('.js-copy-blob-source-btn').getAttribute('data-original-title'),
+ copyButton.getAttribute('data-original-title'),
).toBe('Copy source to clipboard');
done();
diff --git a/spec/javascripts/boards/issue_card_spec.js b/spec/javascripts/boards/issue_card_spec.js
index fddde799d01..bd9b4fbfdd3 100644
--- a/spec/javascripts/boards/issue_card_spec.js
+++ b/spec/javascripts/boards/issue_card_spec.js
@@ -129,7 +129,7 @@ describe('Issue card component', () => {
it('sets title', () => {
expect(
- component.$el.querySelector('.card-assignee a').getAttribute('title'),
+ component.$el.querySelector('.card-assignee img').getAttribute('data-original-title'),
).toContain(`Assigned to ${user.name}`);
});
diff --git a/spec/javascripts/commit/pipelines/mock_data.js b/spec/javascripts/commit/pipelines/mock_data.js
deleted file mode 100644
index 10a60620f49..00000000000
--- a/spec/javascripts/commit/pipelines/mock_data.js
+++ /dev/null
@@ -1,90 +0,0 @@
-export default {
- id: 73,
- user: {
- name: 'Administrator',
- username: 'root',
- id: 1,
- state: 'active',
- avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
- web_url: 'http://localhost:3000/root',
- },
- path: '/root/review-app/pipelines/73',
- details: {
- status: {
- icon: 'icon_status_failed',
- text: 'failed',
- label: 'failed',
- group: 'failed',
- has_details: true,
- details_path: '/root/review-app/pipelines/73',
- },
- duration: null,
- finished_at: '2017-01-25T00:00:17.130Z',
- stages: [{
- name: 'build',
- title: 'build: failed',
- status: {
- icon: 'icon_status_failed',
- text: 'failed',
- label: 'failed',
- group: 'failed',
- has_details: true,
- details_path: '/root/review-app/pipelines/73#build',
- },
- path: '/root/review-app/pipelines/73#build',
- dropdown_path: '/root/review-app/pipelines/73/stage.json?stage=build',
- }],
- artifacts: [],
- manual_actions: [
- {
- name: 'stop_review',
- path: '/root/review-app/builds/1463/play',
- },
- {
- name: 'name',
- path: '/root/review-app/builds/1490/play',
- },
- ],
- },
- flags: {
- latest: true,
- triggered: false,
- stuck: false,
- yaml_errors: false,
- retryable: true,
- cancelable: false,
- },
- ref:
- {
- name: 'master',
- path: '/root/review-app/tree/master',
- tag: false,
- branch: true,
- },
- coverage: '42.21',
- commit: {
- id: 'fbd79f04fa98717641deaaeb092a4d417237c2e4',
- short_id: 'fbd79f04',
- title: 'Update .gitlab-ci.yml',
- author_name: 'Administrator',
- author_email: 'admin@example.com',
- created_at: '2017-01-16T12:13:57.000-05:00',
- committer_name: 'Administrator',
- committer_email: 'admin@example.com',
- message: 'Update .gitlab-ci.yml',
- author: {
- name: 'Administrator',
- username: 'root',
- id: 1,
- state: 'active',
- avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
- web_url: 'http://localhost:3000/root',
- },
- author_gravatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
- commit_url: 'http://localhost:3000/root/review-app/commit/fbd79f04fa98717641deaaeb092a4d417237c2e4',
- commit_path: '/root/review-app/commit/fbd79f04fa98717641deaaeb092a4d417237c2e4',
- },
- retry_path: '/root/review-app/pipelines/73/retry',
- created_at: '2017-01-16T17:13:59.800Z',
- updated_at: '2017-01-25T00:00:17.132Z',
-};
diff --git a/spec/javascripts/commit/pipelines/pipelines_spec.js b/spec/javascripts/commit/pipelines/pipelines_spec.js
index ad31448f81c..398c593eec2 100644
--- a/spec/javascripts/commit/pipelines/pipelines_spec.js
+++ b/spec/javascripts/commit/pipelines/pipelines_spec.js
@@ -1,12 +1,17 @@
import Vue from 'vue';
import PipelinesTable from '~/commit/pipelines/pipelines_table';
-import pipeline from './mock_data';
describe('Pipelines table in Commits and Merge requests', () => {
+ const jsonFixtureName = 'pipelines/pipelines.json';
+ let pipeline;
+
preloadFixtures('static/pipelines_table.html.raw');
+ preloadFixtures(jsonFixtureName);
beforeEach(() => {
loadFixtures('static/pipelines_table.html.raw');
+ const pipelines = getJSONFixture(jsonFixtureName).pipelines;
+ pipeline = pipelines.find(p => p.id === 1);
});
describe('successful request', () => {
diff --git a/spec/javascripts/droplab/drop_down_spec.js b/spec/javascripts/droplab/drop_down_spec.js
index e7786e8cc2c..2bbcebeeac0 100644
--- a/spec/javascripts/droplab/drop_down_spec.js
+++ b/spec/javascripts/droplab/drop_down_spec.js
@@ -1,5 +1,3 @@
-/* eslint-disable */
-
import DropDown from '~/droplab/drop_down';
import utils from '~/droplab/utils';
import { SELECTED_CLASS, IGNORE_CLASS } from '~/droplab/constants';
@@ -17,7 +15,7 @@ describe('DropDown', function () {
it('sets the .hidden property to true', function () {
expect(this.dropdown.hidden).toBe(true);
- })
+ });
it('sets the .list property', function () {
expect(this.dropdown.list).toBe(this.list);
@@ -152,7 +150,7 @@ describe('DropDown', function () {
it('should call addSelectedClass', function () {
expect(this.dropdown.addSelectedClass).toHaveBeenCalledWith(this.closestElement);
- })
+ });
it('should call .preventDefault', function () {
expect(this.event.preventDefault).toHaveBeenCalled();
@@ -293,36 +291,6 @@ describe('DropDown', function () {
});
});
- describe('toggle', function () {
- beforeEach(function () {
- this.dropdown = { hidden: true, show: () => {}, hide: () => {} };
-
- spyOn(this.dropdown, 'show');
- spyOn(this.dropdown, 'hide');
-
- DropDown.prototype.toggle.call(this.dropdown);
- });
-
- it('should call .show if hidden is true', function () {
- expect(this.dropdown.show).toHaveBeenCalled();
- });
-
- describe('if hidden is false', function () {
- beforeEach(function () {
- this.dropdown = { hidden: false, show: () => {}, hide: () => {} };
-
- spyOn(this.dropdown, 'show');
- spyOn(this.dropdown, 'hide');
-
- DropDown.prototype.toggle.call(this.dropdown);
- });
-
- it('should call .show if hidden is true', function () {
- expect(this.dropdown.hide).toHaveBeenCalled();
- });
- });
- });
-
describe('setData', function () {
beforeEach(function () {
this.dropdown = { render: () => {} };
@@ -399,7 +367,7 @@ describe('DropDown', function () {
expect(this.data.map).toHaveBeenCalledWith(jasmine.any(Function));
});
- it('should call .renderChildren for each data item', function() {
+ it('should call .renderChildren for each data item', function () {
expect(this.dropdown.renderChildren.calls.count()).toBe(this.data.length);
});
@@ -407,7 +375,7 @@ describe('DropDown', function () {
expect(this.renderableList.innerHTML).toBe('01');
});
- describe('if no data argument is passed' , function () {
+ describe('if no data argument is passed', function () {
beforeEach(function () {
this.data.map.calls.reset();
this.dropdown.renderChildren.calls.reset();
@@ -446,14 +414,14 @@ describe('DropDown', function () {
describe('renderChildren', function () {
beforeEach(function () {
this.templateString = 'templateString';
- this.dropdown = { setImagesSrc: () => {}, templateString: this.templateString };
+ this.dropdown = { templateString: this.templateString };
this.data = { droplab_hidden: true };
this.html = 'html';
this.template = { firstChild: { outerHTML: 'outerHTML', style: {} } };
spyOn(utils, 'template').and.returnValue(this.html);
spyOn(document, 'createElement').and.returnValue(this.template);
- spyOn(this.dropdown, 'setImagesSrc');
+ spyOn(DropDown, 'setImagesSrc');
this.renderChildren = DropDown.prototype.renderChildren.call(this.dropdown, this.data);
});
@@ -471,7 +439,7 @@ describe('DropDown', function () {
});
it('should call .setImagesSrc with the template', function () {
- expect(this.dropdown.setImagesSrc).toHaveBeenCalledWith(this.template);
+ expect(DropDown.setImagesSrc).toHaveBeenCalledWith(this.template);
});
it('should set the template display to none', function () {
@@ -496,12 +464,11 @@ describe('DropDown', function () {
describe('setImagesSrc', function () {
beforeEach(function () {
- this.dropdown = {};
this.template = { querySelectorAll: () => {} };
spyOn(this.template, 'querySelectorAll').and.returnValue([]);
- DropDown.prototype.setImagesSrc.call(this.dropdown, this.template);
+ DropDown.setImagesSrc(this.template);
});
it('should call .querySelectorAll', function () {
@@ -562,7 +529,7 @@ describe('DropDown', function () {
describe('toggle', function () {
beforeEach(function () {
- this.hidden = true
+ this.hidden = true;
this.dropdown = { hidden: this.hidden, show: () => {}, hide: () => {} };
spyOn(this.dropdown, 'show');
@@ -577,7 +544,7 @@ describe('DropDown', function () {
describe('if .hidden is false', function () {
beforeEach(function () {
- this.hidden = false
+ this.hidden = false;
this.dropdown = { hidden: this.hidden, show: () => {}, hide: () => {} };
spyOn(this.dropdown, 'show');
diff --git a/spec/javascripts/droplab/hook_spec.js b/spec/javascripts/droplab/hook_spec.js
index 8ebdcdd1404..75bf5f3d611 100644
--- a/spec/javascripts/droplab/hook_spec.js
+++ b/spec/javascripts/droplab/hook_spec.js
@@ -1,5 +1,3 @@
-/* eslint-disable */
-
import Hook from '~/droplab/hook';
import * as dropdownSrc from '~/droplab/drop_down';
@@ -73,10 +71,4 @@ describe('Hook', function () {
});
});
});
-
- describe('addEvents', function () {
- it('should exist', function () {
- expect(Hook.prototype.hasOwnProperty('addEvents')).toBe(true);
- });
- });
});
diff --git a/spec/javascripts/fixtures/balsamiq.rb b/spec/javascripts/fixtures/balsamiq.rb
new file mode 100644
index 00000000000..b5372821bf5
--- /dev/null
+++ b/spec/javascripts/fixtures/balsamiq.rb
@@ -0,0 +1,18 @@
+require 'spec_helper'
+
+describe 'Balsamiq file', '(JavaScript fixtures)', type: :controller do
+ include JavaScriptFixturesHelpers
+
+ let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
+ let(:project) { create(:project, namespace: namespace, path: 'balsamiq-project') }
+
+ before(:all) do
+ clean_frontend_fixtures('blob/balsamiq/')
+ end
+
+ it 'blob/balsamiq/test.bmpr' do |example|
+ blob = project.repository.blob_at('b89b56d79', 'files/images/balsamiq.bmpr')
+
+ store_frontend_fixture(blob.data.force_encoding('utf-8'), example.description)
+ end
+end
diff --git a/spec/javascripts/fixtures/balsamiq_viewer.html.haml b/spec/javascripts/fixtures/balsamiq_viewer.html.haml
new file mode 100644
index 00000000000..18166ba4901
--- /dev/null
+++ b/spec/javascripts/fixtures/balsamiq_viewer.html.haml
@@ -0,0 +1 @@
+.file-content.balsamiq-viewer#js-balsamiq-viewer{ data: { endpoint: '/test' } }
diff --git a/spec/javascripts/fixtures/pipelines.rb b/spec/javascripts/fixtures/pipelines.rb
new file mode 100644
index 00000000000..daafbac86db
--- /dev/null
+++ b/spec/javascripts/fixtures/pipelines.rb
@@ -0,0 +1,35 @@
+require 'spec_helper'
+
+describe Projects::PipelinesController, '(JavaScript fixtures)', type: :controller do
+ include JavaScriptFixturesHelpers
+
+ let(:admin) { create(:admin) }
+ let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
+ let(:project) { create(:project, :repository, namespace: namespace, path: 'pipelines-project') }
+ let(:commit) { create(:commit, project: project) }
+ let(:commit_without_author) { RepoHelpers.another_sample_commit }
+ let!(:user) { create(:user, email: commit.author_email) }
+ let!(:pipeline) { create(:ci_pipeline, project: project, sha: commit.id, user: user) }
+ let!(:pipeline_without_author) { create(:ci_pipeline, project: project, sha: commit_without_author.id) }
+ let!(:pipeline_without_commit) { create(:ci_pipeline, project: project, sha: '0000') }
+
+ render_views
+
+ before(:all) do
+ clean_frontend_fixtures('pipelines/')
+ end
+
+ before(:each) do
+ sign_in(admin)
+ end
+
+ it 'pipelines/pipelines.json' do |example|
+ get :index,
+ namespace_id: namespace,
+ project_id: project,
+ format: :json
+
+ expect(response).to be_success
+ store_frontend_fixture(response, example.description)
+ end
+end
diff --git a/spec/javascripts/gfm_auto_complete_spec.js b/spec/javascripts/gfm_auto_complete_spec.js
index 5dfa4008fbd..d0f15c902b5 100644
--- a/spec/javascripts/gfm_auto_complete_spec.js
+++ b/spec/javascripts/gfm_auto_complete_spec.js
@@ -1,13 +1,15 @@
/* eslint no-param-reassign: "off" */
-require('~/gfm_auto_complete');
+import GfmAutoComplete from '~/gfm_auto_complete';
+
require('vendor/jquery.caret');
require('vendor/jquery.atwho');
-const global = window.gl || (window.gl = {});
-const GfmAutoComplete = global.GfmAutoComplete;
-
describe('GfmAutoComplete', function () {
+ const gfmAutoCompleteCallbacks = GfmAutoComplete.prototype.getDefaultCallbacks.call({
+ fetchData: () => {},
+ });
+
describe('DefaultOptions.sorter', function () {
describe('assets loading', function () {
beforeEach(function () {
@@ -16,7 +18,7 @@ describe('GfmAutoComplete', function () {
this.atwhoInstance = { setting: {} };
this.items = [];
- this.sorterValue = GfmAutoComplete.DefaultOptions.sorter
+ this.sorterValue = gfmAutoCompleteCallbacks.sorter
.call(this.atwhoInstance, '', this.items);
});
@@ -38,7 +40,7 @@ describe('GfmAutoComplete', function () {
it('should enable highlightFirst if alwaysHighlightFirst is set', function () {
const atwhoInstance = { setting: { alwaysHighlightFirst: true } };
- GfmAutoComplete.DefaultOptions.sorter.call(atwhoInstance);
+ gfmAutoCompleteCallbacks.sorter.call(atwhoInstance);
expect(atwhoInstance.setting.highlightFirst).toBe(true);
});
@@ -46,7 +48,7 @@ describe('GfmAutoComplete', function () {
it('should enable highlightFirst if a query is present', function () {
const atwhoInstance = { setting: {} };
- GfmAutoComplete.DefaultOptions.sorter.call(atwhoInstance, 'query');
+ gfmAutoCompleteCallbacks.sorter.call(atwhoInstance, 'query');
expect(atwhoInstance.setting.highlightFirst).toBe(true);
});
@@ -58,7 +60,7 @@ describe('GfmAutoComplete', function () {
const items = [];
const searchKey = 'searchKey';
- GfmAutoComplete.DefaultOptions.sorter.call(atwhoInstance, query, items, searchKey);
+ gfmAutoCompleteCallbacks.sorter.call(atwhoInstance, query, items, searchKey);
expect($.fn.atwho.default.callbacks.sorter).toHaveBeenCalledWith(query, items, searchKey);
});
@@ -67,7 +69,7 @@ describe('GfmAutoComplete', function () {
describe('DefaultOptions.matcher', function () {
const defaultMatcher = (context, flag, subtext) => (
- GfmAutoComplete.DefaultOptions.matcher.call(context, flag, subtext)
+ gfmAutoCompleteCallbacks.matcher.call(context, flag, subtext)
);
const flagsUseDefaultMatcher = ['@', '#', '!', '~', '%'];
diff --git a/spec/javascripts/gl_dropdown_spec.js b/spec/javascripts/gl_dropdown_spec.js
index eb532dff5a1..8f90ed69e64 100644
--- a/spec/javascripts/gl_dropdown_spec.js
+++ b/spec/javascripts/gl_dropdown_spec.js
@@ -2,7 +2,6 @@
require('~/gl_dropdown');
require('~/lib/utils/common_utils');
-require('~/lib/utils/type_utility');
require('~/lib/utils/url_utility');
(() => {
diff --git a/spec/javascripts/issue_show/components/app_spec.js b/spec/javascripts/issue_show/components/app_spec.js
new file mode 100644
index 00000000000..ee456869c53
--- /dev/null
+++ b/spec/javascripts/issue_show/components/app_spec.js
@@ -0,0 +1,60 @@
+import Vue from 'vue';
+import '~/render_math';
+import '~/render_gfm';
+import issuableApp from '~/issue_show/components/app.vue';
+import issueShowData from '../mock_data';
+
+const issueShowInterceptor = data => (request, next) => {
+ next(request.respondWith(JSON.stringify(data), {
+ status: 200,
+ headers: {
+ 'POLL-INTERVAL': 1,
+ },
+ }));
+};
+
+describe('Issuable output', () => {
+ document.body.innerHTML = '<span id="task_status"></span>';
+
+ let vm;
+
+ beforeEach(() => {
+ const IssuableDescriptionComponent = Vue.extend(issuableApp);
+ Vue.http.interceptors.push(issueShowInterceptor(issueShowData.initialRequest));
+
+ vm = new IssuableDescriptionComponent({
+ propsData: {
+ canUpdate: true,
+ endpoint: '/gitlab-org/gitlab-shell/issues/9/realtime_changes',
+ issuableRef: '#1',
+ initialTitle: '',
+ initialDescriptionHtml: '',
+ initialDescriptionText: '',
+ },
+ }).$mount();
+ });
+
+ afterEach(() => {
+ Vue.http.interceptors = _.without(Vue.http.interceptors, issueShowInterceptor);
+ });
+
+ it('should render a title/description and update title/description on update', (done) => {
+ setTimeout(() => {
+ expect(document.querySelector('title').innerText).toContain('this is a title (#1)');
+ expect(vm.$el.querySelector('.title').innerHTML).toContain('<p>this is a title</p>');
+ expect(vm.$el.querySelector('.wiki').innerHTML).toContain('<p>this is a description!</p>');
+ expect(vm.$el.querySelector('.js-task-list-field').value).toContain('this is a description');
+
+ Vue.http.interceptors.push(issueShowInterceptor(issueShowData.secondRequest));
+
+ setTimeout(() => {
+ expect(document.querySelector('title').innerText).toContain('2 (#1)');
+ expect(vm.$el.querySelector('.title').innerHTML).toContain('<p>2</p>');
+ expect(vm.$el.querySelector('.wiki').innerHTML).toContain('<p>42</p>');
+ expect(vm.$el.querySelector('.js-task-list-field').value).toContain('42');
+
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/issue_show/components/description_spec.js b/spec/javascripts/issue_show/components/description_spec.js
new file mode 100644
index 00000000000..408349cc42d
--- /dev/null
+++ b/spec/javascripts/issue_show/components/description_spec.js
@@ -0,0 +1,99 @@
+import Vue from 'vue';
+import descriptionComponent from '~/issue_show/components/description.vue';
+
+describe('Description component', () => {
+ let vm;
+
+ beforeEach(() => {
+ const Component = Vue.extend(descriptionComponent);
+
+ if (!document.querySelector('.issuable-meta')) {
+ const metaData = document.createElement('div');
+ metaData.classList.add('issuable-meta');
+ metaData.innerHTML = '<span id="task_status"></span><span id="task_status_short"></span>';
+
+ document.body.appendChild(metaData);
+ }
+
+ vm = new Component({
+ propsData: {
+ canUpdate: true,
+ descriptionHtml: 'test',
+ descriptionText: 'test',
+ updatedAt: new Date().toString(),
+ taskStatus: '',
+ },
+ }).$mount();
+ });
+
+ it('animates description changes', (done) => {
+ vm.descriptionHtml = 'changed';
+
+ Vue.nextTick(() => {
+ expect(
+ vm.$el.querySelector('.wiki').classList.contains('issue-realtime-pre-pulse'),
+ ).toBeTruthy();
+
+ setTimeout(() => {
+ expect(
+ vm.$el.querySelector('.wiki').classList.contains('issue-realtime-trigger-pulse'),
+ ).toBeTruthy();
+
+ done();
+ });
+ });
+ });
+
+ it('re-inits the TaskList when description changed', (done) => {
+ spyOn(gl, 'TaskList');
+ vm.descriptionHtml = 'changed';
+
+ setTimeout(() => {
+ expect(
+ gl.TaskList,
+ ).toHaveBeenCalled();
+
+ done();
+ });
+ });
+
+ it('does not re-init the TaskList when canUpdate is false', (done) => {
+ spyOn(gl, 'TaskList');
+ vm.canUpdate = false;
+ vm.descriptionHtml = 'changed';
+
+ setTimeout(() => {
+ expect(
+ gl.TaskList,
+ ).not.toHaveBeenCalled();
+
+ done();
+ });
+ });
+
+ describe('taskStatus', () => {
+ it('adds full taskStatus', (done) => {
+ vm.taskStatus = '1 of 1';
+
+ setTimeout(() => {
+ expect(
+ document.querySelector('.issuable-meta #task_status').textContent.trim(),
+ ).toBe('1 of 1');
+
+ done();
+ });
+ });
+
+ it('adds short taskStatus', (done) => {
+ vm.taskStatus = '1 of 1';
+
+ setTimeout(() => {
+ expect(
+ document.querySelector('.issuable-meta #task_status_short').textContent.trim(),
+ ).toBe('1/1 task');
+
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/issue_show/components/title_spec.js b/spec/javascripts/issue_show/components/title_spec.js
new file mode 100644
index 00000000000..2f953e7e92e
--- /dev/null
+++ b/spec/javascripts/issue_show/components/title_spec.js
@@ -0,0 +1,67 @@
+import Vue from 'vue';
+import titleComponent from '~/issue_show/components/title.vue';
+
+describe('Title component', () => {
+ let vm;
+
+ beforeEach(() => {
+ const Component = Vue.extend(titleComponent);
+ vm = new Component({
+ propsData: {
+ issuableRef: '#1',
+ titleHtml: 'Testing <img />',
+ titleText: 'Testing',
+ },
+ }).$mount();
+ });
+
+ it('renders title HTML', () => {
+ expect(
+ vm.$el.innerHTML.trim(),
+ ).toBe('Testing <img>');
+ });
+
+ it('updates page title when changing titleHtml', (done) => {
+ spyOn(vm, 'setPageTitle');
+ vm.titleHtml = 'test';
+
+ Vue.nextTick(() => {
+ expect(
+ vm.setPageTitle,
+ ).toHaveBeenCalled();
+
+ done();
+ });
+ });
+
+ it('animates title changes', (done) => {
+ vm.titleHtml = 'test';
+
+ Vue.nextTick(() => {
+ expect(
+ vm.$el.classList.contains('issue-realtime-pre-pulse'),
+ ).toBeTruthy();
+
+ setTimeout(() => {
+ expect(
+ vm.$el.classList.contains('issue-realtime-trigger-pulse'),
+ ).toBeTruthy();
+
+ done();
+ });
+ });
+ });
+
+ it('updates page title after changing title', (done) => {
+ vm.titleHtml = 'changed';
+ vm.titleText = 'changed';
+
+ Vue.nextTick(() => {
+ expect(
+ document.querySelector('title').textContent.trim(),
+ ).toContain('changed');
+
+ done();
+ });
+ });
+});
diff --git a/spec/javascripts/issue_show/issue_title_description_spec.js b/spec/javascripts/issue_show/issue_title_description_spec.js
deleted file mode 100644
index 1ec4fe58b08..00000000000
--- a/spec/javascripts/issue_show/issue_title_description_spec.js
+++ /dev/null
@@ -1,60 +0,0 @@
-import Vue from 'vue';
-import $ from 'jquery';
-import '~/render_math';
-import '~/render_gfm';
-import issueTitleDescription from '~/issue_show/issue_title_description.vue';
-import issueShowData from './mock_data';
-
-window.$ = $;
-
-const issueShowInterceptor = data => (request, next) => {
- next(request.respondWith(JSON.stringify(data), {
- status: 200,
- headers: {
- 'POLL-INTERVAL': 1,
- },
- }));
-};
-
-describe('Issue Title', () => {
- document.body.innerHTML = '<span id="task_status"></span>';
-
- let IssueTitleDescriptionComponent;
-
- beforeEach(() => {
- IssueTitleDescriptionComponent = Vue.extend(issueTitleDescription);
- });
-
- afterEach(() => {
- Vue.http.interceptors = _.without(Vue.http.interceptors, issueShowInterceptor);
- });
-
- it('should render a title/description and update title/description on update', (done) => {
- Vue.http.interceptors.push(issueShowInterceptor(issueShowData.initialRequest));
-
- const issueShowComponent = new IssueTitleDescriptionComponent({
- propsData: {
- canUpdateIssue: '.css-stuff',
- endpoint: '/gitlab-org/gitlab-shell/issues/9/rendered_title',
- },
- }).$mount();
-
- setTimeout(() => {
- expect(document.querySelector('title').innerText).toContain('this is a title (#1)');
- expect(issueShowComponent.$el.querySelector('.title').innerHTML).toContain('<p>this is a title</p>');
- expect(issueShowComponent.$el.querySelector('.wiki').innerHTML).toContain('<p>this is a description!</p>');
- expect(issueShowComponent.$el.querySelector('.js-task-list-field').innerText).toContain('this is a description');
-
- Vue.http.interceptors.push(issueShowInterceptor(issueShowData.secondRequest));
-
- setTimeout(() => {
- expect(document.querySelector('title').innerText).toContain('2 (#1)');
- expect(issueShowComponent.$el.querySelector('.title').innerHTML).toContain('<p>2</p>');
- expect(issueShowComponent.$el.querySelector('.wiki').innerHTML).toContain('<p>42</p>');
- expect(issueShowComponent.$el.querySelector('.js-task-list-field').innerText).toContain('42');
-
- done();
- });
- });
- });
-});
diff --git a/spec/javascripts/issue_show/mock_data.js b/spec/javascripts/issue_show/mock_data.js
index ad5a7b63470..6683d581bc5 100644
--- a/spec/javascripts/issue_show/mock_data.js
+++ b/spec/javascripts/issue_show/mock_data.js
@@ -4,23 +4,23 @@ export default {
title_text: 'this is a title',
description: '<p>this is a description!</p>',
description_text: 'this is a description',
- issue_number: 1,
task_status: '2 of 4 completed',
+ updated_at: new Date().toString(),
},
secondRequest: {
title: '<p>2</p>',
title_text: '2',
description: '<p>42</p>',
description_text: '42',
- issue_number: 1,
task_status: '0 of 0 completed',
+ updated_at: new Date().toString(),
},
issueSpecRequest: {
title: '<p>this is a title</p>',
title_text: 'this is a title',
description: '<li class="task-list-item enabled"><input type="checkbox" class="task-list-item-checkbox">Task List Item</li>',
description_text: '- [ ] Task List Item',
- issue_number: 1,
task_status: '0 of 1 completed',
+ updated_at: new Date().toString(),
},
};
diff --git a/spec/javascripts/labels_issue_sidebar_spec.js b/spec/javascripts/labels_issue_sidebar_spec.js
index 37e038c16da..53aba191b19 100644
--- a/spec/javascripts/labels_issue_sidebar_spec.js
+++ b/spec/javascripts/labels_issue_sidebar_spec.js
@@ -2,7 +2,6 @@
/* global IssuableContext */
/* global LabelsSelect */
-require('~/lib/utils/type_utility');
require('~/gl_dropdown');
require('select2');
require('vendor/jquery.nicescroll');
diff --git a/spec/javascripts/lib/utils/ajax_cache_spec.js b/spec/javascripts/lib/utils/ajax_cache_spec.js
index 7b466a11b92..e1747a82329 100644
--- a/spec/javascripts/lib/utils/ajax_cache_spec.js
+++ b/spec/javascripts/lib/utils/ajax_cache_spec.js
@@ -5,19 +5,13 @@ describe('AjaxCache', () => {
const dummyResponse = {
important: 'dummy data',
};
- let ajaxSpy = (url) => {
- expect(url).toBe(dummyEndpoint);
- const deferred = $.Deferred();
- deferred.resolve(dummyResponse);
- return deferred.promise();
- };
beforeEach(() => {
AjaxCache.internalStorage = { };
- spyOn(jQuery, 'ajax').and.callFake(url => ajaxSpy(url));
+ AjaxCache.pendingRequests = { };
});
- describe('#get', () => {
+ describe('get', () => {
it('returns undefined if cache is empty', () => {
const data = AjaxCache.get(dummyEndpoint);
@@ -41,7 +35,7 @@ describe('AjaxCache', () => {
});
});
- describe('#hasData', () => {
+ describe('hasData', () => {
it('returns false if cache is empty', () => {
expect(AjaxCache.hasData(dummyEndpoint)).toBe(false);
});
@@ -59,9 +53,9 @@ describe('AjaxCache', () => {
});
});
- describe('#purge', () => {
+ describe('remove', () => {
it('does nothing if cache is empty', () => {
- AjaxCache.purge(dummyEndpoint);
+ AjaxCache.remove(dummyEndpoint);
expect(AjaxCache.internalStorage).toEqual({ });
});
@@ -69,7 +63,7 @@ describe('AjaxCache', () => {
it('does nothing if cache contains no matching data', () => {
AjaxCache.internalStorage['not matching'] = dummyResponse;
- AjaxCache.purge(dummyEndpoint);
+ AjaxCache.remove(dummyEndpoint);
expect(AjaxCache.internalStorage['not matching']).toBe(dummyResponse);
});
@@ -77,14 +71,27 @@ describe('AjaxCache', () => {
it('removes matching data', () => {
AjaxCache.internalStorage[dummyEndpoint] = dummyResponse;
- AjaxCache.purge(dummyEndpoint);
+ AjaxCache.remove(dummyEndpoint);
expect(AjaxCache.internalStorage).toEqual({ });
});
});
- describe('#retrieve', () => {
+ describe('retrieve', () => {
+ let ajaxSpy;
+
+ beforeEach(() => {
+ spyOn(jQuery, 'ajax').and.callFake(url => ajaxSpy(url));
+ });
+
it('stores and returns data from Ajax call if cache is empty', (done) => {
+ ajaxSpy = (url) => {
+ expect(url).toBe(dummyEndpoint);
+ const deferred = $.Deferred();
+ deferred.resolve(dummyResponse);
+ return deferred.promise();
+ };
+
AjaxCache.retrieve(dummyEndpoint)
.then((data) => {
expect(data).toBe(dummyResponse);
@@ -94,6 +101,28 @@ describe('AjaxCache', () => {
.catch(fail);
});
+ it('makes no Ajax call if request is pending', () => {
+ const responseDeferred = $.Deferred();
+
+ ajaxSpy = (url) => {
+ expect(url).toBe(dummyEndpoint);
+ // neither reject nor resolve to keep request pending
+ return responseDeferred.promise();
+ };
+
+ const unexpectedResponse = data => fail(`Did not expect response: ${data}`);
+
+ AjaxCache.retrieve(dummyEndpoint)
+ .then(unexpectedResponse)
+ .catch(fail);
+
+ AjaxCache.retrieve(dummyEndpoint)
+ .then(unexpectedResponse)
+ .catch(fail);
+
+ expect($.ajax.calls.count()).toBe(1);
+ });
+
it('returns undefined if Ajax call fails and cache is empty', (done) => {
const dummyStatusText = 'exploded';
const dummyErrorMessage = 'server exploded';
diff --git a/spec/javascripts/lib/utils/common_utils_spec.js b/spec/javascripts/lib/utils/common_utils_spec.js
index 5eb147ed888..42a9067ade5 100644
--- a/spec/javascripts/lib/utils/common_utils_spec.js
+++ b/spec/javascripts/lib/utils/common_utils_spec.js
@@ -41,6 +41,16 @@ require('~/lib/utils/common_utils');
const paramsArray = gl.utils.getUrlParamsArray();
expect(paramsArray[0][0] !== '?').toBe(true);
});
+
+ it('should decode params', () => {
+ history.pushState('', '', '?label_name%5B%5D=test');
+
+ expect(
+ gl.utils.getUrlParamsArray()[0],
+ ).toBe('label_name[]=test');
+
+ history.pushState('', '', '?');
+ });
});
describe('gl.utils.handleLocationHash', () => {
diff --git a/spec/javascripts/lib/utils/poll_spec.js b/spec/javascripts/lib/utils/poll_spec.js
index 918b6d32c43..22f30191ab9 100644
--- a/spec/javascripts/lib/utils/poll_spec.js
+++ b/spec/javascripts/lib/utils/poll_spec.js
@@ -1,9 +1,5 @@
-import Vue from 'vue';
-import VueResource from 'vue-resource';
import Poll from '~/lib/utils/poll';
-Vue.use(VueResource);
-
const waitForAllCallsToFinish = (service, waitForCount, successCallback) => {
const timer = () => {
setTimeout(() => {
@@ -12,45 +8,33 @@ const waitForAllCallsToFinish = (service, waitForCount, successCallback) => {
} else {
timer();
}
- }, 5);
+ }, 0);
};
timer();
};
-class ServiceMock {
- constructor(endpoint) {
- this.service = Vue.resource(endpoint);
- }
+function mockServiceCall(service, response, shouldFail = false) {
+ const action = shouldFail ? Promise.reject : Promise.resolve;
+ const responseObject = response;
+
+ if (!responseObject.headers) responseObject.headers = {};
- fetch() {
- return this.service.get();
- }
+ service.fetch.and.callFake(action.bind(Promise, responseObject));
}
describe('Poll', () => {
- let callbacks;
- let service;
+ const service = jasmine.createSpyObj('service', ['fetch']);
+ const callbacks = jasmine.createSpyObj('callbacks', ['success', 'error']);
- beforeEach(() => {
- callbacks = {
- success: () => {},
- error: () => {},
- };
-
- service = new ServiceMock('endpoint');
-
- spyOn(callbacks, 'success');
- spyOn(callbacks, 'error');
- spyOn(service, 'fetch').and.callThrough();
+ afterEach(() => {
+ callbacks.success.calls.reset();
+ callbacks.error.calls.reset();
+ service.fetch.calls.reset();
});
it('calls the success callback when no header for interval is provided', (done) => {
- const successInterceptor = (request, next) => {
- next(request.respondWith(JSON.stringify([]), { status: 200 }));
- };
-
- Vue.http.interceptors.push(successInterceptor);
+ mockServiceCall(service, { status: 200 });
new Poll({
resource: service,
@@ -63,18 +47,12 @@ describe('Poll', () => {
expect(callbacks.success).toHaveBeenCalled();
expect(callbacks.error).not.toHaveBeenCalled();
- Vue.http.interceptors = _.without(Vue.http.interceptors, successInterceptor);
-
done();
- }, 0);
+ });
});
it('calls the error callback whe the http request returns an error', (done) => {
- const errorInterceptor = (request, next) => {
- next(request.respondWith(JSON.stringify([]), { status: 500 }));
- };
-
- Vue.http.interceptors.push(errorInterceptor);
+ mockServiceCall(service, { status: 500 }, true);
new Poll({
resource: service,
@@ -86,42 +64,29 @@ describe('Poll', () => {
waitForAllCallsToFinish(service, 1, () => {
expect(callbacks.success).not.toHaveBeenCalled();
expect(callbacks.error).toHaveBeenCalled();
- Vue.http.interceptors = _.without(Vue.http.interceptors, errorInterceptor);
done();
});
});
it('should call the success callback when the interval header is -1', (done) => {
- const intervalInterceptor = (request, next) => {
- next(request.respondWith(JSON.stringify([]), { status: 200, headers: { 'poll-interval': -1 } }));
- };
-
- Vue.http.interceptors.push(intervalInterceptor);
+ mockServiceCall(service, { status: 200, headers: { 'poll-interval': -1 } });
new Poll({
resource: service,
method: 'fetch',
successCallback: callbacks.success,
errorCallback: callbacks.error,
- }).makeRequest();
-
- setTimeout(() => {
+ }).makeRequest().then(() => {
expect(callbacks.success).toHaveBeenCalled();
expect(callbacks.error).not.toHaveBeenCalled();
- Vue.http.interceptors = _.without(Vue.http.interceptors, intervalInterceptor);
-
done();
- }, 0);
+ }).catch(done.fail);
});
it('starts polling when http status is 200 and interval header is provided', (done) => {
- const pollInterceptor = (request, next) => {
- next(request.respondWith(JSON.stringify([]), { status: 200, headers: { 'poll-interval': 2 } }));
- };
-
- Vue.http.interceptors.push(pollInterceptor);
+ mockServiceCall(service, { status: 200, headers: { 'poll-interval': 1 } });
const Polling = new Poll({
resource: service,
@@ -141,19 +106,13 @@ describe('Poll', () => {
expect(callbacks.success).toHaveBeenCalled();
expect(callbacks.error).not.toHaveBeenCalled();
- Vue.http.interceptors = _.without(Vue.http.interceptors, pollInterceptor);
-
done();
});
});
describe('stop', () => {
it('stops polling when method is called', (done) => {
- const pollInterceptor = (request, next) => {
- next(request.respondWith(JSON.stringify([]), { status: 200, headers: { 'poll-interval': 2 } }));
- };
-
- Vue.http.interceptors.push(pollInterceptor);
+ mockServiceCall(service, { status: 200, headers: { 'poll-interval': 1 } });
const Polling = new Poll({
resource: service,
@@ -174,8 +133,6 @@ describe('Poll', () => {
expect(service.fetch).toHaveBeenCalledWith({ page: 1 });
expect(Polling.stop).toHaveBeenCalled();
- Vue.http.interceptors = _.without(Vue.http.interceptors, pollInterceptor);
-
done();
});
});
@@ -183,11 +140,7 @@ describe('Poll', () => {
describe('restart', () => {
it('should restart polling when its called', (done) => {
- const pollInterceptor = (request, next) => {
- next(request.respondWith(JSON.stringify([]), { status: 200, headers: { 'poll-interval': 2 } }));
- };
-
- Vue.http.interceptors.push(pollInterceptor);
+ mockServiceCall(service, { status: 200, headers: { 'poll-interval': 1 } });
const Polling = new Poll({
resource: service,
@@ -215,8 +168,6 @@ describe('Poll', () => {
expect(Polling.stop).toHaveBeenCalled();
expect(Polling.restart).toHaveBeenCalled();
- Vue.http.interceptors = _.without(Vue.http.interceptors, pollInterceptor);
-
done();
});
});
diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js
index cfd599f793e..025f08ee332 100644
--- a/spec/javascripts/notes_spec.js
+++ b/spec/javascripts/notes_spec.js
@@ -14,6 +14,7 @@ import '~/notes';
gl.utils = gl.utils || {};
describe('Notes', function() {
+ const FLASH_TYPE_ALERT = 'alert';
var commentsTemplate = 'issues/issue_with_comment.html.raw';
preloadFixtures(commentsTemplate);
@@ -78,6 +79,47 @@ import '~/notes';
});
});
+ describe('updateNote', () => {
+ let sampleComment;
+ let noteEntity;
+ let $form;
+ let $notesContainer;
+
+ beforeEach(() => {
+ this.notes = new Notes('', []);
+ window.gon.current_username = 'root';
+ window.gon.current_user_fullname = 'Administrator';
+ sampleComment = 'foo';
+ noteEntity = {
+ id: 1234,
+ html: `<li class="note note-row-1234 timeline-entry" id="note_1234">
+ <div class="note-text">${sampleComment}</div>
+ </li>`,
+ note: sampleComment,
+ valid: true
+ };
+ $form = $('form.js-main-target-form');
+ $notesContainer = $('ul.main-notes-list');
+ $form.find('textarea.js-note-text').val(sampleComment);
+ });
+
+ it('updates note and resets edit form', () => {
+ const deferred = $.Deferred();
+ spyOn($, 'ajax').and.returnValue(deferred.promise());
+ spyOn(this.notes, 'revertNoteEditForm');
+
+ $('.js-comment-button').click();
+ deferred.resolve(noteEntity);
+
+ const $targetNote = $notesContainer.find(`#note_${noteEntity.id}`);
+ const updatedNote = Object.assign({}, noteEntity);
+ updatedNote.note = 'bar';
+ this.notes.updateNote(updatedNote, $targetNote);
+
+ expect(this.notes.revertNoteEditForm).toHaveBeenCalledWith($targetNote);
+ });
+ });
+
describe('renderNote', () => {
let notes;
let note;
@@ -97,9 +139,8 @@ import '~/notes';
]);
notes = jasmine.createSpyObj('notes', [
+ 'setupNewNote',
'refresh',
- 'isNewNote',
- 'isUpdatedNote',
'collapseLongCommitList',
'updateNotesCount',
'putConflictEditWarningInPlace'
@@ -109,13 +150,15 @@ import '~/notes';
notes.updatedNotesTrackingMap = {};
spyOn(gl.utils, 'localTimeAgo');
+ spyOn(Notes, 'isNewNote').and.callThrough();
+ spyOn(Notes, 'isUpdatedNote').and.callThrough();
spyOn(Notes, 'animateAppendNote').and.callThrough();
spyOn(Notes, 'animateUpdateNote').and.callThrough();
});
describe('when adding note', () => {
it('should call .animateAppendNote', () => {
- notes.isNewNote.and.returnValue(true);
+ Notes.isNewNote.and.returnValue(true);
Notes.prototype.renderNote.call(notes, note, null, $notesList);
expect(Notes.animateAppendNote).toHaveBeenCalledWith(note.html, $notesList);
@@ -124,7 +167,8 @@ import '~/notes';
describe('when note was edited', () => {
it('should call .animateUpdateNote', () => {
- notes.isUpdatedNote.and.returnValue(true);
+ Notes.isNewNote.and.returnValue(false);
+ Notes.isUpdatedNote.and.returnValue(true);
const $note = $('<div>');
$notesList.find.and.returnValue($note);
Notes.prototype.renderNote.call(notes, note, null, $notesList);
@@ -134,7 +178,8 @@ import '~/notes';
describe('while editing', () => {
it('should update textarea if nothing has been touched', () => {
- notes.isUpdatedNote.and.returnValue(true);
+ Notes.isNewNote.and.returnValue(false);
+ Notes.isUpdatedNote.and.returnValue(true);
const $note = $(`<div class="is-editing">
<div class="original-note-content">initial</div>
<textarea class="js-note-text">initial</textarea>
@@ -146,7 +191,8 @@ import '~/notes';
});
it('should call .putConflictEditWarningInPlace', () => {
- notes.isUpdatedNote.and.returnValue(true);
+ Notes.isNewNote.and.returnValue(false);
+ Notes.isUpdatedNote.and.returnValue(true);
const $note = $(`<div class="is-editing">
<div class="original-note-content">initial</div>
<textarea class="js-note-text">different</textarea>
@@ -160,6 +206,47 @@ import '~/notes';
});
});
+ describe('isUpdatedNote', () => {
+ it('should consider same note text as the same', () => {
+ const result = Notes.isUpdatedNote(
+ {
+ note: 'initial'
+ },
+ $(`<div>
+ <div class="original-note-content">initial</div>
+ </div>`)
+ );
+
+ expect(result).toEqual(false);
+ });
+
+ it('should consider same note with trailing newline as the same', () => {
+ const result = Notes.isUpdatedNote(
+ {
+ note: 'initial\n'
+ },
+ $(`<div>
+ <div class="original-note-content">initial\n</div>
+ </div>`)
+ );
+
+ expect(result).toEqual(false);
+ });
+
+ it('should consider different notes as different', () => {
+ const result = Notes.isUpdatedNote(
+ {
+ note: 'foo'
+ },
+ $(`<div>
+ <div class="original-note-content">bar</div>
+ </div>`)
+ );
+
+ expect(result).toEqual(true);
+ });
+ });
+
describe('renderDiscussionNote', () => {
let discussionContainer;
let note;
@@ -179,15 +266,15 @@ import '~/notes';
row = jasmine.createSpyObj('row', ['prevAll', 'first', 'find']);
notes = jasmine.createSpyObj('notes', [
- 'isNewNote',
'isParallelView',
'updateNotesCount',
]);
notes.note_ids = [];
spyOn(gl.utils, 'localTimeAgo');
+ spyOn(Notes, 'isNewNote');
spyOn(Notes, 'animateAppendNote');
- notes.isNewNote.and.returnValue(true);
+ Notes.isNewNote.and.returnValue(true);
notes.isParallelView.and.returnValue(false);
row.prevAll.and.returnValue(row);
row.first.and.returnValue(row);
@@ -376,13 +463,20 @@ import '~/notes';
this.notes = new Notes('', []);
});
- it('should return true when comment has slash commands', () => {
- const sampleComment = '/wip /milestone %1.0 /merge /unassign Merging this';
+ it('should return true when comment begins with a slash command', () => {
+ const sampleComment = '/wip\n/milestone %1.0\n/merge\n/unassign Merging this';
const hasSlashCommands = this.notes.hasSlashCommands(sampleComment);
expect(hasSlashCommands).toBeTruthy();
});
+ it('should return false when comment does NOT begin with a slash command', () => {
+ const sampleComment = 'Hey, /unassign Merging this';
+ const hasSlashCommands = this.notes.hasSlashCommands(sampleComment);
+
+ expect(hasSlashCommands).toBeFalsy();
+ });
+
it('should return false when comment does NOT have any slash commands', () => {
const sampleComment = 'Looking good, Awesome!';
const hasSlashCommands = this.notes.hasSlashCommands(sampleComment);
@@ -392,14 +486,28 @@ import '~/notes';
});
describe('stripSlashCommands', () => {
- const REGEX_SLASH_COMMANDS = /\/\w+/g;
+ it('should strip slash commands from the comment which begins with a slash command', () => {
+ this.notes = new Notes();
+ const sampleComment = '/wip\n/milestone %1.0\n/merge\n/unassign Merging this';
+ const stripedComment = this.notes.stripSlashCommands(sampleComment);
+
+ expect(stripedComment).toBe('');
+ });
- it('should strip slash commands from the comment', () => {
+ it('should strip slash commands from the comment but leaves plain comment if it is present', () => {
this.notes = new Notes();
- const sampleComment = '/wip /milestone %1.0 /merge /unassign Merging this';
+ const sampleComment = '/wip\n/milestone %1.0\n/merge\n/unassign\nMerging this';
const stripedComment = this.notes.stripSlashCommands(sampleComment);
- expect(REGEX_SLASH_COMMANDS.test(stripedComment)).toBeFalsy();
+ expect(stripedComment).toBe('Merging this');
+ });
+
+ it('should NOT strip string that has slashes within', () => {
+ this.notes = new Notes();
+ const sampleComment = 'http://127.0.0.1:3000/root/gitlab-shell/issues/1';
+ const stripedComment = this.notes.stripSlashCommands(sampleComment);
+
+ expect(stripedComment).toBe(sampleComment);
});
});
@@ -411,6 +519,22 @@ import '~/notes';
beforeEach(() => {
this.notes = new Notes('', []);
+ spyOn(_, 'escape').and.callFake((comment) => {
+ const escapedString = comment.replace(/["&'<>]/g, (a) => {
+ const escapedToken = {
+ '&': '&amp;',
+ '<': '&lt;',
+ '>': '&gt;',
+ '"': '&quot;',
+ "'": '&#x27;',
+ '`': '&#x60;'
+ }[a];
+
+ return escapedToken;
+ });
+
+ return escapedString;
+ });
});
it('should return constructed placeholder element for regular note based on form contents', () => {
@@ -431,7 +555,21 @@ import '~/notes';
expect($tempNote.find('.timeline-content').hasClass('discussion')).toBeFalsy();
expect($tempNoteHeader.find('.hidden-xs').text().trim()).toEqual(currentUserFullname);
expect($tempNoteHeader.find('.note-headline-light').text().trim()).toEqual(`@${currentUsername}`);
- expect($tempNote.find('.note-body .note-text').text().trim()).toEqual(sampleComment);
+ expect($tempNote.find('.note-body .note-text p').text().trim()).toEqual(sampleComment);
+ });
+
+ it('should escape HTML characters from note based on form contents', () => {
+ const commentWithHtml = '<script>alert("Boom!");</script>';
+ const $tempNote = this.notes.createPlaceholderNote({
+ formContent: commentWithHtml,
+ uniqueId,
+ isDiscussionNote: false,
+ currentUsername,
+ currentUserFullname
+ });
+
+ expect(_.escape).toHaveBeenCalledWith(commentWithHtml);
+ expect($tempNote.find('.note-body .note-text p').html()).toEqual('&lt;script&gt;alert("Boom!");&lt;/script&gt;');
});
it('should return constructed placeholder element for discussion note based on form contents', () => {
@@ -447,5 +585,33 @@ import '~/notes';
expect($tempNote.find('.timeline-content').hasClass('discussion')).toBeTruthy();
});
});
+
+ describe('appendFlash', () => {
+ beforeEach(() => {
+ this.notes = new Notes();
+ });
+
+ it('shows a flash message', () => {
+ this.notes.addFlash('Error message', FLASH_TYPE_ALERT, this.notes.parentTimeline);
+
+ expect(document.querySelectorAll('.flash-alert').length).toBe(1);
+ });
+ });
+
+ describe('clearFlash', () => {
+ beforeEach(() => {
+ $(document).off('ajax:success');
+ this.notes = new Notes();
+ });
+
+ it('removes all the associated flash messages', () => {
+ this.notes.addFlash('Error message 1', FLASH_TYPE_ALERT, this.notes.parentTimeline);
+ this.notes.addFlash('Error message 2', FLASH_TYPE_ALERT, this.notes.parentTimeline);
+
+ this.notes.clearFlash();
+
+ expect(document.querySelectorAll('.flash-alert').length).toBe(0);
+ });
+ });
});
}).call(window);
diff --git a/spec/javascripts/pipeline_schedules/interval_pattern_input_spec.js b/spec/javascripts/pipeline_schedules/interval_pattern_input_spec.js
index 08fa6ca9057..845b371d90c 100644
--- a/spec/javascripts/pipeline_schedules/interval_pattern_input_spec.js
+++ b/spec/javascripts/pipeline_schedules/interval_pattern_input_spec.js
@@ -36,20 +36,6 @@ describe('Interval Pattern Input Component', function () {
expect(this.intervalPatternComponent.initialCronInterval).toBe(this.initialCronInterval);
});
- it('sets showUnsetWarning to false', function (done) {
- Vue.nextTick(() => {
- expect(this.intervalPatternComponent.showUnsetWarning).toBe(false);
- done();
- });
- });
-
- it('does not render showUnsetWarning', function (done) {
- Vue.nextTick(() => {
- expect(this.intervalPatternComponent.$el.outerHTML).not.toContain('Schedule not yet set');
- done();
- });
- });
-
it('sets isEditable to true', function (done) {
Vue.nextTick(() => {
expect(this.intervalPatternComponent.isEditable).toBe(true);
@@ -72,20 +58,6 @@ describe('Interval Pattern Input Component', function () {
expect(this.intervalPatternComponent).toBeDefined();
});
- it('sets showUnsetWarning to false', function (done) {
- Vue.nextTick(() => {
- expect(this.intervalPatternComponent.showUnsetWarning).toBe(false);
- done();
- });
- });
-
- it('does not render showUnsetWarning', function (done) {
- Vue.nextTick(() => {
- expect(this.intervalPatternComponent.$el.outerHTML).not.toContain('Schedule not yet set');
- done();
- });
- });
-
it('sets isEditable to false', function (done) {
Vue.nextTick(() => {
expect(this.intervalPatternComponent.isEditable).toBe(false);
@@ -113,20 +85,6 @@ describe('Interval Pattern Input Component', function () {
expect(this.intervalPatternComponent.initialCronInterval).toBe(defaultInitialCronInterval);
});
- it('sets showUnsetWarning to true', function (done) {
- Vue.nextTick(() => {
- expect(this.intervalPatternComponent.showUnsetWarning).toBe(true);
- done();
- });
- });
-
- it('renders showUnsetWarning to true', function (done) {
- Vue.nextTick(() => {
- expect(this.intervalPatternComponent.$el.outerHTML).toContain('Schedule not yet set');
- done();
- });
- });
-
it('sets isEditable to true', function (done) {
Vue.nextTick(() => {
expect(this.intervalPatternComponent.isEditable).toBe(true);
diff --git a/spec/javascripts/pipeline_schedules/pipeline_schedule_callout_spec.js b/spec/javascripts/pipeline_schedules/pipeline_schedule_callout_spec.js
index 1d05f37cb36..6120d224ac0 100644
--- a/spec/javascripts/pipeline_schedules/pipeline_schedule_callout_spec.js
+++ b/spec/javascripts/pipeline_schedules/pipeline_schedule_callout_spec.js
@@ -4,8 +4,15 @@ import PipelineSchedulesCallout from '~/pipeline_schedules/components/pipeline_s
const PipelineSchedulesCalloutComponent = Vue.extend(PipelineSchedulesCallout);
const cookieKey = 'pipeline_schedules_callout_dismissed';
+const docsUrl = 'help/ci/scheduled_pipelines';
describe('Pipeline Schedule Callout', () => {
+ beforeEach(() => {
+ setFixtures(`
+ <div id='pipeline-schedules-callout' data-docs-url=${docsUrl}></div>
+ `);
+ });
+
describe('independent of cookies', () => {
beforeEach(() => {
this.calloutComponent = new PipelineSchedulesCalloutComponent().$mount();
@@ -18,6 +25,10 @@ describe('Pipeline Schedule Callout', () => {
it('correctly sets illustrationSvg', () => {
expect(this.calloutComponent.illustrationSvg).toContain('<svg');
});
+
+ it('correctly sets docsUrl', () => {
+ expect(this.calloutComponent.docsUrl).toContain(docsUrl);
+ });
});
describe(`when ${cookieKey} cookie is set`, () => {
@@ -61,6 +72,10 @@ describe('Pipeline Schedule Callout', () => {
expect(this.calloutComponent.$el.outerHTML).toContain('runs pipelines in the future');
});
+ it('renders the documentation url', () => {
+ expect(this.calloutComponent.$el.outerHTML).toContain(docsUrl);
+ });
+
it('updates calloutDismissed when close button is clicked', (done) => {
this.calloutComponent.$el.querySelector('#dismiss-callout-btn').click();
diff --git a/spec/javascripts/pipelines/mock_data.js b/spec/javascripts/pipelines/mock_data.js
deleted file mode 100644
index 2365a662b9f..00000000000
--- a/spec/javascripts/pipelines/mock_data.js
+++ /dev/null
@@ -1,107 +0,0 @@
-export default {
- pipelines: [{
- id: 115,
- user: {
- name: 'Root',
- username: 'root',
- id: 1,
- state: 'active',
- avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
- web_url: 'http://localhost:3000/root',
- },
- path: '/root/review-app/pipelines/115',
- details: {
- status: {
- icon: 'icon_status_failed',
- text: 'failed',
- label: 'failed',
- group: 'failed',
- has_details: true,
- details_path: '/root/review-app/pipelines/115',
- },
- duration: null,
- finished_at: '2017-03-17T19:00:15.996Z',
- stages: [{
- name: 'build',
- title: 'build: failed',
- status: {
- icon: 'icon_status_failed',
- text: 'failed',
- label: 'failed',
- group: 'failed',
- has_details: true,
- details_path: '/root/review-app/pipelines/115#build',
- },
- path: '/root/review-app/pipelines/115#build',
- dropdown_path: '/root/review-app/pipelines/115/stage.json?stage=build',
- },
- {
- name: 'review',
- title: 'review: skipped',
- status: {
- icon: 'icon_status_skipped',
- text: 'skipped',
- label: 'skipped',
- group: 'skipped',
- has_details: true,
- details_path: '/root/review-app/pipelines/115#review',
- },
- path: '/root/review-app/pipelines/115#review',
- dropdown_path: '/root/review-app/pipelines/115/stage.json?stage=review',
- }],
- artifacts: [],
- manual_actions: [{
- name: 'stop_review',
- path: '/root/review-app/builds/3766/play',
- }],
- },
- flags: {
- latest: true,
- triggered: false,
- stuck: false,
- yaml_errors: false,
- retryable: true,
- cancelable: false,
- },
- ref: {
- name: 'thisisabranch',
- path: '/root/review-app/tree/thisisabranch',
- tag: false,
- branch: true,
- },
- commit: {
- id: '9e87f87625b26c42c59a2ee0398f81d20cdfe600',
- short_id: '9e87f876',
- title: 'Update README.md',
- created_at: '2017-03-15T22:58:28.000+00:00',
- parent_ids: ['3744f9226e699faec2662a8b267e5d3fd0bfff0e'],
- message: 'Update README.md',
- author_name: 'Root',
- author_email: 'admin@example.com',
- authored_date: '2017-03-15T22:58:28.000+00:00',
- committer_name: 'Root',
- committer_email: 'admin@example.com',
- committed_date: '2017-03-15T22:58:28.000+00:00',
- author: {
- name: 'Root',
- username: 'root',
- id: 1,
- state: 'active',
- avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
- web_url: 'http://localhost:3000/root',
- },
- author_gravatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
- commit_url: 'http://localhost:3000/root/review-app/commit/9e87f87625b26c42c59a2ee0398f81d20cdfe600',
- commit_path: '/root/review-app/commit/9e87f87625b26c42c59a2ee0398f81d20cdfe600',
- },
- retry_path: '/root/review-app/pipelines/115/retry',
- created_at: '2017-03-15T22:58:33.436Z',
- updated_at: '2017-03-17T19:00:15.997Z',
- }],
- count: {
- all: 52,
- running: 0,
- pending: 0,
- finished: 52,
- },
-};
diff --git a/spec/javascripts/pipelines/pipeline_url_spec.js b/spec/javascripts/pipelines/pipeline_url_spec.js
index 53931d67ad7..0bcc3905702 100644
--- a/spec/javascripts/pipelines/pipeline_url_spec.js
+++ b/spec/javascripts/pipelines/pipeline_url_spec.js
@@ -60,7 +60,7 @@ describe('Pipeline Url Component', () => {
expect(
component.$el.querySelector('.js-pipeline-url-user').getAttribute('href'),
).toEqual(mockData.pipeline.user.web_url);
- expect(image.getAttribute('title')).toEqual(mockData.pipeline.user.name);
+ expect(image.getAttribute('data-original-title')).toEqual(mockData.pipeline.user.name);
expect(image.getAttribute('src')).toEqual(mockData.pipeline.user.avatar_url);
});
diff --git a/spec/javascripts/pipelines/pipelines_spec.js b/spec/javascripts/pipelines/pipelines_spec.js
index e9c05f74ce6..3a56156358b 100644
--- a/spec/javascripts/pipelines/pipelines_spec.js
+++ b/spec/javascripts/pipelines/pipelines_spec.js
@@ -1,15 +1,20 @@
import Vue from 'vue';
import pipelinesComp from '~/pipelines/pipelines';
import Store from '~/pipelines/stores/pipelines_store';
-import pipelinesData from './mock_data';
describe('Pipelines', () => {
+ const jsonFixtureName = 'pipelines/pipelines.json';
+
preloadFixtures('static/pipelines.html.raw');
+ preloadFixtures(jsonFixtureName);
let PipelinesComponent;
+ let pipeline;
beforeEach(() => {
loadFixtures('static/pipelines.html.raw');
+ const pipelines = getJSONFixture(jsonFixtureName).pipelines;
+ pipeline = pipelines.find(p => p.id === 1);
PipelinesComponent = Vue.extend(pipelinesComp);
});
@@ -17,7 +22,7 @@ describe('Pipelines', () => {
describe('successfull request', () => {
describe('with pipelines', () => {
const pipelinesInterceptor = (request, next) => {
- next(request.respondWith(JSON.stringify(pipelinesData), {
+ next(request.respondWith(JSON.stringify(pipeline), {
status: 200,
}));
};
diff --git a/spec/javascripts/project_title_spec.js b/spec/javascripts/project_title_spec.js
index 3a1d4e2440f..5c51e855401 100644
--- a/spec/javascripts/project_title_spec.js
+++ b/spec/javascripts/project_title_spec.js
@@ -2,7 +2,6 @@
/* global Project */
require('select2/select2.js');
-require('~/lib/utils/type_utility');
require('~/gl_dropdown');
require('~/api');
require('~/project_select');
diff --git a/spec/javascripts/search_autocomplete_spec.js b/spec/javascripts/search_autocomplete_spec.js
index aaf058bd755..fa52a8a0dd2 100644
--- a/spec/javascripts/search_autocomplete_spec.js
+++ b/spec/javascripts/search_autocomplete_spec.js
@@ -3,7 +3,6 @@
require('~/gl_dropdown');
require('~/search_autocomplete');
require('~/lib/utils/common_utils');
-require('~/lib/utils/type_utility');
require('vendor/fuzzaldrin-plus');
(function() {
diff --git a/spec/javascripts/sidebar/sidebar_assignees_spec.js b/spec/javascripts/sidebar/sidebar_assignees_spec.js
index e0df0a3228f..865951b2ad7 100644
--- a/spec/javascripts/sidebar/sidebar_assignees_spec.js
+++ b/spec/javascripts/sidebar/sidebar_assignees_spec.js
@@ -24,6 +24,7 @@ describe('sidebar assignees', () => {
SidebarService.singleton = null;
SidebarStore.singleton = null;
SidebarMediator.singleton = null;
+ Vue.http.interceptors = _.without(Vue.http.interceptors, Mock.sidebarMockInterceptor);
});
it('calls the mediator when saves the assignees', () => {
diff --git a/spec/javascripts/sidebar/sidebar_bundle_spec.js b/spec/javascripts/sidebar/sidebar_bundle_spec.js
deleted file mode 100644
index 7760b34e071..00000000000
--- a/spec/javascripts/sidebar/sidebar_bundle_spec.js
+++ /dev/null
@@ -1,42 +0,0 @@
-import Vue from 'vue';
-import SidebarBundleDomContentLoaded from '~/sidebar/sidebar_bundle';
-import SidebarTimeTracking from '~/sidebar/components/time_tracking/sidebar_time_tracking';
-import SidebarMediator from '~/sidebar/sidebar_mediator';
-import SidebarService from '~/sidebar/services/sidebar_service';
-import SidebarStore from '~/sidebar/stores/sidebar_store';
-import Mock from './mock_data';
-
-describe('sidebar bundle', () => {
- gl.sidebarOptions = Mock.mediator;
-
- beforeEach(() => {
- spyOn(SidebarTimeTracking.methods, 'listenForSlashCommands').and.callFake(() => { });
- preloadFixtures('issues/open-issue.html.raw');
- Vue.http.interceptors.push(Mock.sidebarMockInterceptor);
- loadFixtures('issues/open-issue.html.raw');
- spyOn(Vue.prototype, '$mount');
- SidebarBundleDomContentLoaded();
- this.mediator = new SidebarMediator();
- });
-
- afterEach(() => {
- SidebarService.singleton = null;
- SidebarStore.singleton = null;
- SidebarMediator.singleton = null;
- });
-
- it('the mediator should be already defined with some data', () => {
- SidebarBundleDomContentLoaded();
-
- expect(this.mediator.store).toBeDefined();
- expect(this.mediator.service).toBeDefined();
- expect(this.mediator.store.currentUser).toEqual(Mock.mediator.currentUser);
- expect(this.mediator.store.rootPath).toEqual(Mock.mediator.rootPath);
- expect(this.mediator.store.endPoint).toEqual(Mock.mediator.endPoint);
- expect(this.mediator.store.editable).toEqual(Mock.mediator.editable);
- });
-
- it('the sidebar time tracking and assignees components to have been mounted', () => {
- expect(Vue.prototype.$mount).toHaveBeenCalledTimes(2);
- });
-});
diff --git a/spec/javascripts/sidebar/sidebar_mediator_spec.js b/spec/javascripts/sidebar/sidebar_mediator_spec.js
index 2b00fa17334..e246f41ee82 100644
--- a/spec/javascripts/sidebar/sidebar_mediator_spec.js
+++ b/spec/javascripts/sidebar/sidebar_mediator_spec.js
@@ -14,6 +14,7 @@ describe('Sidebar mediator', () => {
SidebarService.singleton = null;
SidebarStore.singleton = null;
SidebarMediator.singleton = null;
+ Vue.http.interceptors = _.without(Vue.http.interceptors, Mock.sidebarMockInterceptor);
});
it('assigns yourself ', () => {
diff --git a/spec/javascripts/sidebar/sidebar_service_spec.js b/spec/javascripts/sidebar/sidebar_service_spec.js
index d41162096a6..91a4dd669a7 100644
--- a/spec/javascripts/sidebar/sidebar_service_spec.js
+++ b/spec/javascripts/sidebar/sidebar_service_spec.js
@@ -10,6 +10,7 @@ describe('Sidebar service', () => {
afterEach(() => {
SidebarService.singleton = null;
+ Vue.http.interceptors = _.without(Vue.http.interceptors, Mock.sidebarMockInterceptor);
});
it('gets the data', (done) => {
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_deployment_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_deployment_spec.js
index 2f971b39d16..d4b200875df 100644
--- a/spec/javascripts/vue_mr_widget/components/mr_widget_deployment_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_deployment_spec.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import deploymentComponent from '~/vue_merge_request_widget/components/mr_widget_deployment';
import MRWidgetService from '~/vue_merge_request_widget/services/mr_widget_service';
-import { statusClassToSvgMap } from '~/vue_shared/pipeline_svg_icons';
+import { statusIconEntityMap } from '~/vue_shared/ci_status_icons';
const deploymentMockData = [
{
@@ -46,7 +46,7 @@ describe('MRWidgetDeployment', () => {
describe('svg', () => {
it('should have the proper SVG icon', () => {
const vm = createComponent(deploymentMockData);
- expect(vm.svg).toEqual(statusClassToSvgMap.icon_status_success);
+ expect(vm.svg).toEqual(statusIconEntityMap.icon_status_success);
});
});
});
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js
index 48f816c8460..7f3eea7d2e5 100644
--- a/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js
@@ -48,10 +48,12 @@ describe('MRWidgetHeader', () => {
describe('template', () => {
let vm;
let el;
+ const sourceBranchPath = '/foo/bar/mr-widget-refactor';
const mr = {
divergedCommitsCount: 12,
sourceBranch: 'mr-widget-refactor',
- sourceBranchLink: '/foo/bar/mr-widget-refactor',
+ sourceBranchLink: `<a href="${sourceBranchPath}">mr-widget-refactor</a>`,
+ targetBranchPath: 'foo/bar/commits-path',
targetBranch: 'master',
isOpen: true,
emailPatchesPath: '/mr/email-patches',
@@ -65,8 +67,13 @@ describe('MRWidgetHeader', () => {
it('should render template elements correctly', () => {
expect(el.classList.contains('mr-source-target')).toBeTruthy();
- expect(el.querySelectorAll('.label-branch')[0].textContent).toContain(mr.sourceBranch);
- expect(el.querySelectorAll('.label-branch')[1].textContent).toContain(mr.targetBranch);
+ const sourceBranchLink = el.querySelectorAll('.label-branch')[0];
+ const targetBranchLink = el.querySelectorAll('.label-branch')[1];
+
+ expect(sourceBranchLink.textContent).toContain(mr.sourceBranch);
+ expect(targetBranchLink.textContent).toContain(mr.targetBranch);
+ expect(sourceBranchLink.querySelector('a').getAttribute('href')).toEqual(sourceBranchPath);
+ expect(targetBranchLink.querySelector('a').getAttribute('href')).toEqual(mr.targetBranchPath);
expect(el.querySelector('.diverged-commits-count').textContent).toContain('12 commits behind');
expect(el.textContent).toContain('Check out branch');
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js
index 1b418c7dfcf..647b59520f8 100644
--- a/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js
@@ -1,5 +1,5 @@
import Vue from 'vue';
-import { statusClassToSvgMap } from '~/vue_shared/pipeline_svg_icons';
+import { statusIconEntityMap } from '~/vue_shared/ci_status_icons';
import pipelineComponent from '~/vue_merge_request_widget/components/mr_widget_pipeline';
import mockData from '../mock_data';
@@ -24,7 +24,7 @@ describe('MRWidgetPipeline', () => {
describe('components', () => {
it('should have components added', () => {
expect(pipelineComponent.components['pipeline-stage']).toBeDefined();
- expect(pipelineComponent.components['pipeline-status-icon']).toBeDefined();
+ expect(pipelineComponent.components.ciIcon).toBeDefined();
});
});
@@ -33,7 +33,7 @@ describe('MRWidgetPipeline', () => {
it('should have the proper SVG icon', () => {
const vm = createComponent({ pipeline: mockData.pipeline });
- expect(vm.svg).toEqual(statusClassToSvgMap.icon_status_failed);
+ expect(vm.svg).toEqual(statusIconEntityMap.icon_status_failed);
});
});
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_closed_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_closed_spec.js
index 78a70725e94..47303d1e80f 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_closed_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_closed_spec.js
@@ -3,7 +3,7 @@ import closedComponent from '~/vue_merge_request_widget/components/states/mr_wid
const mr = {
targetBranch: 'good-branch',
- targetBranchCommitsPath: '/good-branch',
+ targetBranchPath: '/good-branch',
closedBy: {
name: 'Fatih Acet',
username: 'fatihacet',
@@ -44,7 +44,7 @@ describe('MRWidgetClosed', () => {
expect(el.querySelector('h4').textContent).toContain('Closed by');
expect(el.querySelector('h4').textContent).toContain(mr.closedBy.name);
expect(el.textContent).toContain('The changes were not merged into');
- expect(el.querySelector('.label-branch').getAttribute('href')).toEqual(mr.targetBranchCommitsPath);
+ expect(el.querySelector('.label-branch').getAttribute('href')).toEqual(mr.targetBranchPath);
expect(el.querySelector('.label-branch').textContent).toContain(mr.targetBranch);
});
});
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_nothing_to_merge_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_nothing_to_merge_spec.js
index d40c67b189d..a8a02fa6b66 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_nothing_to_merge_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_nothing_to_merge_spec.js
@@ -4,14 +4,26 @@ import nothingToMergeComponent from '~/vue_merge_request_widget/components/state
describe('MRWidgetNothingToMerge', () => {
describe('template', () => {
const Component = Vue.extend(nothingToMergeComponent);
+ const newBlobPath = '/foo';
const vm = new Component({
el: document.createElement('div'),
+ propsData: {
+ mr: { newBlobPath },
+ },
});
+
it('should have correct elements', () => {
expect(vm.$el.classList.contains('mr-widget-body')).toBeTruthy();
- expect(vm.$el.querySelector('button').getAttribute('disabled')).toBeTruthy();
- expect(vm.$el.innerText).toContain('There is nothing to merge from source branch into target branch.');
+ expect(vm.$el.querySelector('a').href).toContain(newBlobPath);
+ expect(vm.$el.innerText).toContain('Currently there are no changes in this merge request\'s source branch');
expect(vm.$el.innerText).toContain('Please push new commits or use a different branch.');
});
+
+ it('should not show new blob link if there is no link available', () => {
+ vm.mr.newBlobPath = null;
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelector('a')).toEqual(null);
+ });
+ });
});
});
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js
index 74df99415c9..d043ad38b8b 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js
@@ -80,7 +80,7 @@ describe('MRWidgetReadyToMerge', () => {
});
describe('mergeButtonClass', () => {
- const defaultClass = 'btn btn-success accept-merge-request';
+ const defaultClass = 'btn btn-small btn-success accept-merge-request';
const failedClass = `${defaultClass} btn-danger`;
const inActionClass = `${defaultClass} btn-info`;
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_sha_mismatch_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_sha_mismatch_spec.js
new file mode 100644
index 00000000000..5fb1d69a8b3
--- /dev/null
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_sha_mismatch_spec.js
@@ -0,0 +1,16 @@
+import Vue from 'vue';
+import shaMismatchComponent from '~/vue_merge_request_widget/components/states/mr_widget_sha_mismatch';
+
+describe('MRWidgetSHAMismatch', () => {
+ describe('template', () => {
+ const Component = Vue.extend(shaMismatchComponent);
+ const vm = new Component({
+ el: document.createElement('div'),
+ });
+ it('should have correct elements', () => {
+ expect(vm.$el.classList.contains('mr-widget-body')).toBeTruthy();
+ expect(vm.$el.querySelector('button').getAttribute('disabled')).toBeTruthy();
+ expect(vm.$el.innerText).toContain('The source branch HEAD has recently changed. Please reload the page and review the changes before merging.');
+ });
+ });
+});
diff --git a/spec/javascripts/vue_mr_widget/stores/get_state_key_spec.js b/spec/javascripts/vue_mr_widget/stores/get_state_key_spec.js
index ee944f4d4e5..9a331d99865 100644
--- a/spec/javascripts/vue_mr_widget/stores/get_state_key_spec.js
+++ b/spec/javascripts/vue_mr_widget/stores/get_state_key_spec.js
@@ -25,6 +25,9 @@ describe('getStateKey', () => {
context.canBeMerged = true;
expect(bound()).toEqual('readyToMerge');
+ context.hasSHAChanged = true;
+ expect(bound()).toEqual('shaMismatch');
+
context.isPipelineBlocked = true;
expect(bound()).toEqual('pipelineBlocked');
diff --git a/spec/javascripts/vue_mr_widget/stores/mr_widget_store_spec.js b/spec/javascripts/vue_mr_widget/stores/mr_widget_store_spec.js
new file mode 100644
index 00000000000..56dd0198ae2
--- /dev/null
+++ b/spec/javascripts/vue_mr_widget/stores/mr_widget_store_spec.js
@@ -0,0 +1,22 @@
+import MergeRequestStore from '~/vue_merge_request_widget/stores/mr_widget_store';
+import mockData from '../mock_data';
+
+describe('MergeRequestStore', () => {
+ describe('setData', () => {
+ let store;
+
+ beforeEach(() => {
+ store = new MergeRequestStore(mockData);
+ });
+
+ it('should set hasSHAChanged when the diff SHA changes', () => {
+ store.setData({ ...mockData, diff_head_sha: 'a-different-string' });
+ expect(store.hasSHAChanged).toBe(true);
+ });
+
+ it('should not set hasSHAChanged when other data changes', () => {
+ store.setData({ ...mockData, work_in_progress: !mockData.work_in_progress });
+ expect(store.hasSHAChanged).toBe(false);
+ });
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/ci_badge_link_spec.js b/spec/javascripts/vue_shared/components/ci_badge_link_spec.js
new file mode 100644
index 00000000000..daed4da3e15
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/ci_badge_link_spec.js
@@ -0,0 +1,89 @@
+import Vue from 'vue';
+import ciBadge from '~/vue_shared/components/ci_badge_link.vue';
+
+describe('CI Badge Link Component', () => {
+ let CIBadge;
+
+ const statuses = {
+ canceled: {
+ text: 'canceled',
+ label: 'canceled',
+ group: 'canceled',
+ icon: 'icon_status_canceled',
+ details_path: 'status/canceled',
+ },
+ created: {
+ text: 'created',
+ label: 'created',
+ group: 'created',
+ icon: 'icon_status_created',
+ details_path: 'status/created',
+ },
+ failed: {
+ text: 'failed',
+ label: 'failed',
+ group: 'failed',
+ icon: 'icon_status_failed',
+ details_path: 'status/failed',
+ },
+ manual: {
+ text: 'manual',
+ label: 'manual action',
+ group: 'manual',
+ icon: 'icon_status_manual',
+ details_path: 'status/manual',
+ },
+ pending: {
+ text: 'pending',
+ label: 'pending',
+ group: 'pending',
+ icon: 'icon_status_pending',
+ details_path: 'status/pending',
+ },
+ running: {
+ text: 'running',
+ label: 'running',
+ group: 'running',
+ icon: 'icon_status_running',
+ details_path: 'status/running',
+ },
+ skipped: {
+ text: 'skipped',
+ label: 'skipped',
+ group: 'skipped',
+ icon: 'icon_status_skipped',
+ details_path: 'status/skipped',
+ },
+ success_warining: {
+ text: 'passed',
+ label: 'passed',
+ group: 'success_with_warnings',
+ icon: 'icon_status_warning',
+ details_path: 'status/warning',
+ },
+ success: {
+ text: 'passed',
+ label: 'passed',
+ group: 'passed',
+ icon: 'icon_status_success',
+ details_path: 'status/passed',
+ },
+ };
+
+ it('should render each status badge', () => {
+ CIBadge = Vue.extend(ciBadge);
+ Object.keys(statuses).map((status) => {
+ const vm = new CIBadge({
+ propsData: {
+ status: statuses[status],
+ },
+ }).$mount();
+
+ expect(vm.$el.getAttribute('href')).toEqual(statuses[status].details_path);
+ expect(vm.$el.textContent.trim()).toEqual(statuses[status].text);
+ expect(vm.$el.getAttribute('class')).toEqual(`ci-status ci-${statuses[status].group}`);
+ expect(vm.$el.querySelector('svg')).toBeDefined();
+ return vm;
+ });
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/ci_icon_spec.js b/spec/javascripts/vue_shared/components/ci_icon_spec.js
index 98dc6caa622..d8664408595 100644
--- a/spec/javascripts/vue_shared/components/ci_icon_spec.js
+++ b/spec/javascripts/vue_shared/components/ci_icon_spec.js
@@ -25,6 +25,7 @@ describe('CI Icon component', () => {
propsData: {
status: {
icon: 'icon_status_success',
+ group: 'success',
},
},
}).$mount();
@@ -37,6 +38,7 @@ describe('CI Icon component', () => {
propsData: {
status: {
icon: 'icon_status_failed',
+ group: 'failed',
},
},
}).$mount();
@@ -49,6 +51,7 @@ describe('CI Icon component', () => {
propsData: {
status: {
icon: 'icon_status_warning',
+ group: 'warning',
},
},
}).$mount();
@@ -61,6 +64,7 @@ describe('CI Icon component', () => {
propsData: {
status: {
icon: 'icon_status_pending',
+ group: 'pending',
},
},
}).$mount();
@@ -73,6 +77,7 @@ describe('CI Icon component', () => {
propsData: {
status: {
icon: 'icon_status_running',
+ group: 'running',
},
},
}).$mount();
@@ -85,6 +90,7 @@ describe('CI Icon component', () => {
propsData: {
status: {
icon: 'icon_status_created',
+ group: 'created',
},
},
}).$mount();
@@ -97,6 +103,7 @@ describe('CI Icon component', () => {
propsData: {
status: {
icon: 'icon_status_skipped',
+ group: 'skipped',
},
},
}).$mount();
@@ -109,6 +116,7 @@ describe('CI Icon component', () => {
propsData: {
status: {
icon: 'icon_status_canceled',
+ group: 'canceled',
},
},
}).$mount();
@@ -121,6 +129,7 @@ describe('CI Icon component', () => {
propsData: {
status: {
icon: 'icon_status_manual',
+ group: 'manual',
},
},
}).$mount();
diff --git a/spec/javascripts/vue_shared/components/commit_spec.js b/spec/javascripts/vue_shared/components/commit_spec.js
index df547299d75..0638483e7aa 100644
--- a/spec/javascripts/vue_shared/components/commit_spec.js
+++ b/spec/javascripts/vue_shared/components/commit_spec.js
@@ -61,16 +61,16 @@ describe('Commit component', () => {
});
it('should render a link to the ref url', () => {
- expect(component.$el.querySelector('.branch-name').getAttribute('href')).toEqual(props.commitRef.ref_url);
+ expect(component.$el.querySelector('.ref-name').getAttribute('href')).toEqual(props.commitRef.ref_url);
});
it('should render the ref name', () => {
- expect(component.$el.querySelector('.branch-name').textContent).toContain(props.commitRef.name);
+ expect(component.$el.querySelector('.ref-name').textContent).toContain(props.commitRef.name);
});
it('should render the commit short sha with a link to the commit url', () => {
- expect(component.$el.querySelector('.commit-id').getAttribute('href')).toEqual(props.commitUrl);
- expect(component.$el.querySelector('.commit-id').textContent).toContain(props.shortSha);
+ expect(component.$el.querySelector('.commit-sha').getAttribute('href')).toEqual(props.commitUrl);
+ expect(component.$el.querySelector('.commit-sha').textContent).toContain(props.shortSha);
});
it('should render the given commitIconSvg', () => {
@@ -86,7 +86,7 @@ describe('Commit component', () => {
it('Should render the author avatar with title and alt attributes', () => {
expect(
- component.$el.querySelector('.commit-title .avatar-image-container img').getAttribute('title'),
+ component.$el.querySelector('.commit-title .avatar-image-container img').getAttribute('data-original-title'),
).toContain(props.author.username);
expect(
component.$el.querySelector('.commit-title .avatar-image-container img').getAttribute('alt'),
diff --git a/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js b/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js
index 699625cdbb7..286118917e8 100644
--- a/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js
+++ b/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js
@@ -1,27 +1,47 @@
import Vue from 'vue';
import tableRowComp from '~/vue_shared/components/pipelines_table_row';
-import pipeline from '../../commit/pipelines/mock_data';
describe('Pipelines Table Row', () => {
- let component;
-
- beforeEach(() => {
+ const jsonFixtureName = 'pipelines/pipelines.json';
+ const buildComponent = (pipeline) => {
const PipelinesTableRowComponent = Vue.extend(tableRowComp);
-
- component = new PipelinesTableRowComponent({
+ return new PipelinesTableRowComponent({
el: document.querySelector('.test-dom-element'),
propsData: {
pipeline,
service: {},
},
}).$mount();
+ };
+
+ let component;
+ let pipeline;
+ let pipelineWithoutAuthor;
+ let pipelineWithoutCommit;
+
+ preloadFixtures(jsonFixtureName);
+
+ beforeEach(() => {
+ const pipelines = getJSONFixture(jsonFixtureName).pipelines;
+ pipeline = pipelines.find(p => p.id === 1);
+ pipelineWithoutAuthor = pipelines.find(p => p.id === 2);
+ pipelineWithoutCommit = pipelines.find(p => p.id === 3);
+ });
+
+ afterEach(() => {
+ component.$destroy();
});
it('should render a table row', () => {
+ component = buildComponent(pipeline);
expect(component.$el).toEqual('TR');
});
describe('status column', () => {
+ beforeEach(() => {
+ component = buildComponent(pipeline);
+ });
+
it('should render a pipeline link', () => {
expect(
component.$el.querySelector('td.commit-link a').getAttribute('href'),
@@ -36,6 +56,10 @@ describe('Pipelines Table Row', () => {
});
describe('information column', () => {
+ beforeEach(() => {
+ component = buildComponent(pipeline);
+ });
+
it('should render a pipeline link', () => {
expect(
component.$el.querySelector('td:nth-child(2) a').getAttribute('href'),
@@ -55,7 +79,7 @@ describe('Pipelines Table Row', () => {
).toEqual(pipeline.user.web_url);
expect(
- component.$el.querySelector('td:nth-child(2) img').getAttribute('title'),
+ component.$el.querySelector('td:nth-child(2) img').getAttribute('data-original-title'),
).toEqual(pipeline.user.name);
});
});
@@ -63,13 +87,59 @@ describe('Pipelines Table Row', () => {
describe('commit column', () => {
it('should render link to commit', () => {
- expect(
- component.$el.querySelector('td:nth-child(3) .commit-id').getAttribute('href'),
- ).toEqual(pipeline.commit.commit_path);
+ component = buildComponent(pipeline);
+
+ const commitLink = component.$el.querySelector('.branch-commit .commit-sha');
+ expect(commitLink.getAttribute('href')).toEqual(pipeline.commit.commit_path);
+ });
+
+ const findElements = () => {
+ const commitTitleElement = component.$el.querySelector('.branch-commit .commit-title');
+ const commitAuthorElement = commitTitleElement.querySelector('a.avatar-image-container');
+
+ if (!commitAuthorElement) {
+ return { commitAuthorElement };
+ }
+
+ const commitAuthorLink = commitAuthorElement.getAttribute('href');
+ const commitAuthorName = commitAuthorElement.querySelector('img.avatar').getAttribute('data-original-title');
+
+ return { commitAuthorElement, commitAuthorLink, commitAuthorName };
+ };
+
+ it('renders nothing without commit', () => {
+ expect(pipelineWithoutCommit.commit).toBe(null);
+ component = buildComponent(pipelineWithoutCommit);
+
+ const { commitAuthorElement } = findElements();
+
+ expect(commitAuthorElement).toBe(null);
+ });
+
+ it('renders commit author', () => {
+ component = buildComponent(pipeline);
+ const { commitAuthorLink, commitAuthorName } = findElements();
+
+ expect(commitAuthorLink).toEqual(pipeline.commit.author.web_url);
+ expect(commitAuthorName).toEqual(pipeline.commit.author.username);
+ });
+
+ it('renders commit with unregistered author', () => {
+ expect(pipelineWithoutAuthor.commit.author).toBe(null);
+ component = buildComponent(pipelineWithoutAuthor);
+
+ const { commitAuthorLink, commitAuthorName } = findElements();
+
+ expect(commitAuthorLink).toEqual(`mailto:${pipelineWithoutAuthor.commit.author_email}`);
+ expect(commitAuthorName).toEqual(pipelineWithoutAuthor.commit.author_name);
});
});
describe('stages column', () => {
+ beforeEach(() => {
+ component = buildComponent(pipeline);
+ });
+
it('should render an icon for each stage', () => {
expect(
component.$el.querySelectorAll('td:nth-child(4) .js-builds-dropdown-button').length,
@@ -78,6 +148,10 @@ describe('Pipelines Table Row', () => {
});
describe('actions column', () => {
+ beforeEach(() => {
+ component = buildComponent(pipeline);
+ });
+
it('should render the provided actions', () => {
expect(
component.$el.querySelectorAll('td:nth-child(6) ul li').length,
diff --git a/spec/javascripts/vue_shared/components/pipelines_table_spec.js b/spec/javascripts/vue_shared/components/pipelines_table_spec.js
index 4d3ced944d7..6cc178b8f1d 100644
--- a/spec/javascripts/vue_shared/components/pipelines_table_spec.js
+++ b/spec/javascripts/vue_shared/components/pipelines_table_spec.js
@@ -1,13 +1,19 @@
import Vue from 'vue';
import pipelinesTableComp from '~/vue_shared/components/pipelines_table';
import '~/lib/utils/datetime_utility';
-import pipeline from '../../commit/pipelines/mock_data';
describe('Pipelines Table', () => {
+ const jsonFixtureName = 'pipelines/pipelines.json';
+
+ let pipeline;
let PipelinesTableComponent;
+ preloadFixtures(jsonFixtureName);
+
beforeEach(() => {
PipelinesTableComponent = Vue.extend(pipelinesTableComp);
+ const pipelines = getJSONFixture(jsonFixtureName).pipelines;
+ pipeline = pipelines.find(p => p.id === 1);
});
describe('table', () => {
diff --git a/spec/javascripts/vue_shared/components/user_avatar_image_spec.js b/spec/javascripts/vue_shared/components/user_avatar_image_spec.js
new file mode 100644
index 00000000000..8daa7610274
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/user_avatar_image_spec.js
@@ -0,0 +1,54 @@
+import Vue from 'vue';
+import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
+
+const UserAvatarImageComponent = Vue.extend(UserAvatarImage);
+
+describe('User Avatar Image Component', function () {
+ describe('Initialization', function () {
+ beforeEach(function () {
+ this.propsData = {
+ size: 99,
+ imgSrc: 'myavatarurl.com',
+ imgAlt: 'mydisplayname',
+ cssClasses: 'myextraavatarclass',
+ tooltipText: 'tooltip text',
+ tooltipPlacement: 'bottom',
+ };
+
+ this.userAvatarImage = new UserAvatarImageComponent({
+ propsData: this.propsData,
+ }).$mount();
+ });
+
+ it('should return a defined Vue component', function () {
+ expect(this.userAvatarImage).toBeDefined();
+ });
+
+ it('should have <img> as a child element', function () {
+ expect(this.userAvatarImage.$el.tagName).toBe('IMG');
+ });
+
+ it('should properly compute tooltipContainer', function () {
+ expect(this.userAvatarImage.tooltipContainer).toBe('body');
+ });
+
+ it('should properly render tooltipContainer', function () {
+ expect(this.userAvatarImage.$el.getAttribute('data-container')).toBe('body');
+ });
+
+ it('should properly compute avatarSizeClass', function () {
+ expect(this.userAvatarImage.avatarSizeClass).toBe('s99');
+ });
+
+ it('should properly render img css', function () {
+ const classList = this.userAvatarImage.$el.classList;
+ const containsAvatar = classList.contains('avatar');
+ const containsSizeClass = classList.contains('s99');
+ const containsCustomClass = classList.contains('myextraavatarclass');
+
+ expect(containsAvatar).toBe(true);
+ expect(containsSizeClass).toBe(true);
+ expect(containsCustomClass).toBe(true);
+ });
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/user_avatar_link_spec.js b/spec/javascripts/vue_shared/components/user_avatar_link_spec.js
new file mode 100644
index 00000000000..52e450e9ba5
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/user_avatar_link_spec.js
@@ -0,0 +1,50 @@
+import Vue from 'vue';
+import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
+
+describe('User Avatar Link Component', function () {
+ beforeEach(function () {
+ this.propsData = {
+ linkHref: 'myavatarurl.com',
+ imgSize: 99,
+ imgSrc: 'myavatarurl.com',
+ imgAlt: 'mydisplayname',
+ imgCssClasses: 'myextraavatarclass',
+ tooltipText: 'tooltip text',
+ tooltipPlacement: 'bottom',
+ };
+
+ const UserAvatarLinkComponent = Vue.extend(UserAvatarLink);
+
+ this.userAvatarLink = new UserAvatarLinkComponent({
+ propsData: this.propsData,
+ }).$mount();
+
+ this.userAvatarImage = this.userAvatarLink.$children[0];
+ });
+
+ it('should return a defined Vue component', function () {
+ expect(this.userAvatarLink).toBeDefined();
+ });
+
+ it('should have user-avatar-image registered as child component', function () {
+ expect(this.userAvatarLink.$options.components.userAvatarImage).toBeDefined();
+ });
+
+ it('user-avatar-link should have user-avatar-image as child component', function () {
+ expect(this.userAvatarImage).toBeDefined();
+ });
+
+ it('should render <a> as a child element', function () {
+ expect(this.userAvatarLink.$el.tagName).toBe('A');
+ });
+
+ it('should have <img> as a child element', function () {
+ expect(this.userAvatarLink.$el.querySelector('img')).not.toBeNull();
+ });
+
+ it('should return neccessary props as defined', function () {
+ _.each(this.propsData, (val, key) => {
+ expect(this.userAvatarLink[key]).toBeDefined();
+ });
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/user_avatar_svg_spec.js b/spec/javascripts/vue_shared/components/user_avatar_svg_spec.js
new file mode 100644
index 00000000000..b8d639ffbec
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/user_avatar_svg_spec.js
@@ -0,0 +1,29 @@
+import Vue from 'vue';
+import UserAvatarSvg from '~/vue_shared/components/user_avatar/user_avatar_svg.vue';
+import avatarSvg from 'icons/_icon_random.svg';
+
+const UserAvatarSvgComponent = Vue.extend(UserAvatarSvg);
+
+describe('User Avatar Svg Component', function () {
+ describe('Initialization', function () {
+ beforeEach(function () {
+ this.propsData = {
+ size: 99,
+ svg: avatarSvg,
+ };
+
+ this.userAvatarSvg = new UserAvatarSvgComponent({
+ propsData: this.propsData,
+ }).$mount();
+ });
+
+ it('should return a defined Vue component', function () {
+ expect(this.userAvatarSvg).toBeDefined();
+ });
+
+ it('should have <svg> as a child element', function () {
+ expect(this.userAvatarSvg.$el.tagName).toEqual('svg');
+ expect(this.userAvatarSvg.$el.innerHTML).toContain('<path');
+ });
+ });
+});
diff --git a/spec/lib/banzai/filter/external_link_filter_spec.rb b/spec/lib/banzai/filter/external_link_filter_spec.rb
index d9e4525cb28..0f8ec8de7a0 100644
--- a/spec/lib/banzai/filter/external_link_filter_spec.rb
+++ b/spec/lib/banzai/filter/external_link_filter_spec.rb
@@ -1,5 +1,22 @@
require 'spec_helper'
+shared_examples 'an external link with rel attribute' do
+ it 'adds rel="nofollow" to external links' do
+ expect(doc.at_css('a')).to have_attribute('rel')
+ expect(doc.at_css('a')['rel']).to include 'nofollow'
+ end
+
+ it 'adds rel="noreferrer" to external links' do
+ expect(doc.at_css('a')).to have_attribute('rel')
+ expect(doc.at_css('a')['rel']).to include 'noreferrer'
+ end
+
+ it 'adds rel="noopener" to external links' do
+ expect(doc.at_css('a')).to have_attribute('rel')
+ expect(doc.at_css('a')['rel']).to include 'noopener'
+ end
+end
+
describe Banzai::Filter::ExternalLinkFilter, lib: true do
include FilterSpecHelper
@@ -22,49 +39,58 @@ describe Banzai::Filter::ExternalLinkFilter, lib: true do
context 'for root links on document' do
let(:doc) { filter %q(<a href="https://google.com/">Google</a>) }
- it 'adds rel="nofollow" to external links' do
- expect(doc.at_css('a')).to have_attribute('rel')
- expect(doc.at_css('a')['rel']).to include 'nofollow'
+ it_behaves_like 'an external link with rel attribute'
+ end
+
+ context 'for nested links on document' do
+ let(:doc) { filter %q(<p><a href="https://google.com/">Google</a></p>) }
+
+ it_behaves_like 'an external link with rel attribute'
+ end
+
+ context 'for invalid urls' do
+ it 'skips broken hrefs' do
+ doc = filter %q(<p><a href="don't crash on broken urls">Google</a></p>)
+ expected = %q(<p><a href="don't%20crash%20on%20broken%20urls">Google</a></p>)
+
+ expect(doc.to_html).to eq(expected)
end
- it 'adds rel="noreferrer" to external links' do
- expect(doc.at_css('a')).to have_attribute('rel')
- expect(doc.at_css('a')['rel']).to include 'noreferrer'
+ it 'skips improperly formatted mailtos' do
+ doc = filter %q(<p><a href="mailto://jblogs@example.com">Email</a></p>)
+ expected = %q(<p><a href="mailto://jblogs@example.com">Email</a></p>)
+
+ expect(doc.to_html).to eq(expected)
end
end
- context 'for nested links on document' do
- let(:doc) { filter %q(<p><a href="https://google.com/">Google</a></p>) }
+ context 'for links with a username' do
+ context 'with a valid username' do
+ let(:doc) { filter %q(<a href="https://user@google.com/">Google</a>) }
- it 'adds rel="nofollow" to external links' do
- expect(doc.at_css('a')).to have_attribute('rel')
- expect(doc.at_css('a')['rel']).to include 'nofollow'
+ it_behaves_like 'an external link with rel attribute'
end
- it 'adds rel="noreferrer" to external links' do
- expect(doc.at_css('a')).to have_attribute('rel')
- expect(doc.at_css('a')['rel']).to include 'noreferrer'
+ context 'with an impersonated username' do
+ let(:internal) { Gitlab.config.gitlab.url }
+
+ let(:doc) { filter %Q(<a href="https://#{internal}@example.com" target="_blank">Reverse Tabnabbing</a>) }
+
+ it_behaves_like 'an external link with rel attribute'
end
end
context 'for non-lowercase scheme links' do
- let(:doc_with_http) { filter %q(<p><a href="httP://google.com/">Google</a></p>) }
- let(:doc_with_https) { filter %q(<p><a href="hTTpS://google.com/">Google</a></p>) }
-
- it 'adds rel="nofollow" to external links' do
- expect(doc_with_http.at_css('a')).to have_attribute('rel')
- expect(doc_with_https.at_css('a')).to have_attribute('rel')
+ context 'with http' do
+ let(:doc) { filter %q(<p><a href="httP://google.com/">Google</a></p>) }
- expect(doc_with_http.at_css('a')['rel']).to include 'nofollow'
- expect(doc_with_https.at_css('a')['rel']).to include 'nofollow'
+ it_behaves_like 'an external link with rel attribute'
end
- it 'adds rel="noreferrer" to external links' do
- expect(doc_with_http.at_css('a')).to have_attribute('rel')
- expect(doc_with_https.at_css('a')).to have_attribute('rel')
+ context 'with https' do
+ let(:doc) { filter %q(<p><a href="hTTpS://google.com/">Google</a></p>) }
- expect(doc_with_http.at_css('a')['rel']).to include 'noreferrer'
- expect(doc_with_https.at_css('a')['rel']).to include 'noreferrer'
+ it_behaves_like 'an external link with rel attribute'
end
it 'skips internal links' do
@@ -84,14 +110,6 @@ describe Banzai::Filter::ExternalLinkFilter, lib: true do
context 'for protocol-relative links' do
let(:doc) { filter %q(<p><a href="//google.com/">Google</a></p>) }
- it 'adds rel="nofollow" to external links' do
- expect(doc.at_css('a')).to have_attribute('rel')
- expect(doc.at_css('a')['rel']).to include 'nofollow'
- end
-
- it 'adds rel="noreferrer" to external links' do
- expect(doc.at_css('a')).to have_attribute('rel')
- expect(doc.at_css('a')['rel']).to include 'noreferrer'
- end
+ it_behaves_like 'an external link with rel attribute'
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 53abc056602..fe2c00bb2ca 100644
--- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
+++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
@@ -225,7 +225,7 @@ module Ci
before_script: ["pwd"],
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"] },
+ production: { script: "deploy", type: "deploy", only: ["master@path", "deploy"] }
})
config_processor = GitlabCiYamlProcessor.new(config, 'fork')
@@ -381,7 +381,7 @@ module Ci
before_script: ["pwd"],
rspec: { script: "rspec", type: "test", except: ["master", "deploy", "test@fork"] },
staging: { script: "deploy", type: "deploy", except: ["master"] },
- production: { script: "deploy", type: "deploy", except: ["master@fork"] },
+ production: { script: "deploy", type: "deploy", except: ["master@fork"] }
})
config_processor = GitlabCiYamlProcessor.new(config, 'fork')
@@ -716,7 +716,7 @@ module Ci
expect(config_processor.builds_for_stage_and_ref("test", "master").first[:options][:cache]).to eq(
paths: ["logs/", "binaries/"],
untracked: true,
- key: 'key',
+ key: 'key'
)
end
@@ -734,7 +734,7 @@ module Ci
expect(config_processor.builds_for_stage_and_ref("test", "master").first[:options][:cache]).to eq(
paths: ["logs/", "binaries/"],
untracked: true,
- key: 'key',
+ key: 'key'
)
end
@@ -743,7 +743,7 @@ module Ci
cache: { paths: ["logs/", "binaries/"], untracked: true, key: 'global' },
rspec: {
script: "rspec",
- cache: { paths: ["test/"], untracked: false, key: 'local' },
+ cache: { paths: ["test/"], untracked: false, key: 'local' }
}
})
@@ -753,7 +753,7 @@ module Ci
expect(config_processor.builds_for_stage_and_ref("test", "master").first[:options][:cache]).to eq(
paths: ["test/"],
untracked: false,
- key: 'local',
+ key: 'local'
)
end
end
diff --git a/spec/lib/container_registry/blob_spec.rb b/spec/lib/container_registry/blob_spec.rb
index f06e5fd54a2..ab010c6dfeb 100644
--- a/spec/lib/container_registry/blob_spec.rb
+++ b/spec/lib/container_registry/blob_spec.rb
@@ -98,7 +98,7 @@ describe ContainerRegistry::Blob do
context 'for a valid address' do
before do
stub_request(:get, location).
- with(headers: { 'Authorization' => nil }).
+ with { |request| !request.headers.include?('Authorization') }.
to_return(
status: 200,
headers: { 'Content-Type' => 'application/json' },
diff --git a/spec/lib/container_registry/client_spec.rb b/spec/lib/container_registry/client_spec.rb
new file mode 100644
index 00000000000..ec03b533383
--- /dev/null
+++ b/spec/lib/container_registry/client_spec.rb
@@ -0,0 +1,39 @@
+# coding: utf-8
+require 'spec_helper'
+
+describe ContainerRegistry::Client do
+ let(:token) { '12345' }
+ let(:options) { { token: token } }
+ let(:client) { described_class.new("http://container-registry", options) }
+
+ describe '#blob' do
+ it 'GET /v2/:name/blobs/:digest' do
+ stub_request(:get, "http://container-registry/v2/group/test/blobs/sha256:0123456789012345").
+ with(headers: {
+ 'Accept' => 'application/octet-stream',
+ 'Authorization' => "bearer #{token}"
+ }).
+ to_return(status: 200, body: "Blob")
+
+ expect(client.blob('group/test', 'sha256:0123456789012345')).to eq('Blob')
+ end
+
+ it 'follows 307 redirect for GET /v2/:name/blobs/:digest' do
+ stub_request(:get, "http://container-registry/v2/group/test/blobs/sha256:0123456789012345").
+ with(headers: {
+ 'Accept' => 'application/octet-stream',
+ 'Authorization' => "bearer #{token}"
+ }).
+ to_return(status: 307, body: "", headers: { Location: 'http://redirected' })
+ # We should probably use hash_excluding here, but that requires an update to WebMock:
+ # https://github.com/bblimke/webmock/blob/master/lib/webmock/matchers/hash_excluding_matcher.rb
+ stub_request(:get, "http://redirected/").
+ with { |request| !request.headers.include?('Authorization') }.
+ to_return(status: 200, body: "Successfully redirected")
+
+ response = client.blob('group/test', 'sha256:0123456789012345')
+
+ expect(response).to eq('Successfully redirected')
+ end
+ end
+end
diff --git a/spec/lib/expand_variables_spec.rb b/spec/lib/expand_variables_spec.rb
index 90628917943..7faa0f31b68 100644
--- a/spec/lib/expand_variables_spec.rb
+++ b/spec/lib/expand_variables_spec.rb
@@ -25,7 +25,7 @@ describe ExpandVariables do
result: 'keyvalueresult',
variables: [
{ key: 'variable', value: 'value' },
- { key: 'variable2', value: 'result' },
+ { key: 'variable2', value: 'result' }
] },
{ value: 'key${variable}${variable2}',
result: 'keyvalueresult',
@@ -37,7 +37,7 @@ describe ExpandVariables do
result: 'keyresultvalue',
variables: [
{ key: 'variable', value: 'value' },
- { key: 'variable2', value: 'result' },
+ { key: 'variable2', value: 'result' }
] },
{ value: 'key${variable2}${variable}',
result: 'keyresultvalue',
@@ -49,7 +49,7 @@ describe ExpandVariables do
result: 'review/feature/add-review-apps',
variables: [
{ key: 'CI_COMMIT_REF_NAME', value: 'feature/add-review-apps' }
- ] },
+ ] }
]
tests.each do |test|
diff --git a/spec/lib/gitlab/asciidoc_spec.rb b/spec/lib/gitlab/asciidoc_spec.rb
index 0f47fb2fbd9..2c7ebb15fd7 100644
--- a/spec/lib/gitlab/asciidoc_spec.rb
+++ b/spec/lib/gitlab/asciidoc_spec.rb
@@ -22,7 +22,22 @@ module Gitlab
expect(Asciidoctor).to receive(:convert)
.with(input, expected_asciidoc_opts).and_return(html)
- expect(render(input)).to eq(html)
+ expect(render(input, context)).to eq(html)
+ end
+
+ context "with asciidoc_opts" do
+ it "merges the options with default ones" do
+ expected_asciidoc_opts = {
+ safe: :secure,
+ backend: :gitlab_html5,
+ attributes: described_class::DEFAULT_ADOC_ATTRS
+ }
+
+ expect(Asciidoctor).to receive(:convert)
+ .with(input, expected_asciidoc_opts).and_return(html)
+
+ render(input, context)
+ end
end
context "XSS" do
@@ -33,7 +48,7 @@ module Gitlab
},
'images' => {
input: 'image:https://localhost.com/image.png[Alt text" onerror="alert(7)]',
- output: "<div>\n<p><span><img src=\"https://localhost.com/image.png\" alt=\"Alt text\"></span></p>\n</div>"
+ output: "<img src=\"https://localhost.com/image.png\" alt=\"Alt text\">"
},
'pre' => {
input: '```mypre"><script>alert(3)</script>',
@@ -43,10 +58,18 @@ module Gitlab
links.each do |name, data|
it "does not convert dangerous #{name} into HTML" do
- expect(render(data[:input])).to eq(data[:output])
+ expect(render(data[:input], context)).to include(data[:output])
end
end
end
+
+ context 'external links' do
+ it 'adds the `rel` attribute to the link' do
+ output = render('link:https://google.com[Google]', context)
+
+ expect(output).to include('rel="nofollow noreferrer noopener"')
+ end
+ end
end
def render(*args)
diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb
index d4a43192d03..50bc3ef1b7c 100644
--- a/spec/lib/gitlab/auth_spec.rb
+++ b/spec/lib/gitlab/auth_spec.rb
@@ -175,7 +175,7 @@ describe Gitlab::Auth, lib: true do
user = create(
:user,
username: 'normal_user',
- password: 'my-secret',
+ password: 'my-secret'
)
expect(gl_auth.find_for_git_client(user.username, user.password, project: nil, ip: 'ip'))
@@ -186,7 +186,7 @@ describe Gitlab::Auth, lib: true do
user = create(
:user,
username: 'oauth2',
- password: 'my-secret',
+ password: 'my-secret'
)
expect(gl_auth.find_for_git_client(user.username, user.password, project: nil, ip: 'ip'))
diff --git a/spec/lib/gitlab/backup/manager_spec.rb b/spec/lib/gitlab/backup/manager_spec.rb
index f84782ab440..c59ff7fb290 100644
--- a/spec/lib/gitlab/backup/manager_spec.rb
+++ b/spec/lib/gitlab/backup/manager_spec.rb
@@ -151,7 +151,7 @@ describe Backup::Manager, lib: true do
allow(Dir).to receive(:glob).and_return(
[
'1451606400_2016_01_01_gitlab_backup.tar',
- '1451520000_2015_12_31_gitlab_backup.tar',
+ '1451520000_2015_12_31_gitlab_backup.tar'
]
)
end
diff --git a/spec/lib/gitlab/ci/config/entry/global_spec.rb b/spec/lib/gitlab/ci/config/entry/global_spec.rb
index 684d01e9056..23270ad5053 100644
--- a/spec/lib/gitlab/ci/config/entry/global_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/global_spec.rb
@@ -113,7 +113,7 @@ describe Gitlab::Ci::Config::Entry::Global do
describe '#variables_value' do
it 'returns variables' do
- expect(global.variables_value).to eq(VAR: 'value')
+ expect(global.variables_value).to eq('VAR' => 'value')
end
end
@@ -154,7 +154,7 @@ describe Gitlab::Ci::Config::Entry::Global do
services: ['postgres:9.1', 'mysql:5.5'],
stage: 'test',
cache: { key: 'k', untracked: true, paths: ['public/'] },
- variables: { VAR: 'value' },
+ variables: { 'VAR' => 'value' },
ignore: false,
after_script: ['make clean'] },
spinach: { name: :spinach,
@@ -167,7 +167,7 @@ describe Gitlab::Ci::Config::Entry::Global do
cache: { key: 'k', untracked: true, paths: ['public/'] },
variables: {},
ignore: false,
- after_script: ['make clean'] },
+ after_script: ['make clean'] }
)
end
end
diff --git a/spec/lib/gitlab/ci/config/entry/variables_spec.rb b/spec/lib/gitlab/ci/config/entry/variables_spec.rb
index f15f02f403e..84bfef9e8ad 100644
--- a/spec/lib/gitlab/ci/config/entry/variables_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/variables_spec.rb
@@ -13,6 +13,14 @@ describe Gitlab::Ci::Config::Entry::Variables do
it 'returns hash with key value strings' do
expect(entry.value).to eq config
end
+
+ context 'with numeric keys and values in the config' do
+ let(:config) { { 10 => 20 } }
+
+ it 'converts numeric key and numeric value into strings' do
+ expect(entry.value).to eq('10' => '20')
+ end
+ end
end
describe '#errors' do
diff --git a/spec/lib/gitlab/conflict/file_collection_spec.rb b/spec/lib/gitlab/conflict/file_collection_spec.rb
index 39d892c18c0..27f23ea70dc 100644
--- a/spec/lib/gitlab/conflict/file_collection_spec.rb
+++ b/spec/lib/gitlab/conflict/file_collection_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe Gitlab::Conflict::FileCollection, lib: true do
let(:merge_request) { create(:merge_request, source_branch: 'conflict-resolvable', target_branch: 'conflict-start') }
- let(:file_collection) { Gitlab::Conflict::FileCollection.new(merge_request) }
+ let(:file_collection) { described_class.read_only(merge_request) }
describe '#files' do
it 'returns an array of Conflict::Files' do
diff --git a/spec/lib/gitlab/contributions_calendar_spec.rb b/spec/lib/gitlab/contributions_calendar_spec.rb
index e18a219ef36..79632e2b6a3 100644
--- a/spec/lib/gitlab/contributions_calendar_spec.rb
+++ b/spec/lib/gitlab/contributions_calendar_spec.rb
@@ -47,7 +47,7 @@ describe Gitlab::ContributionsCalendar do
action: Event::CREATED,
target: @targets[project],
author: contributor,
- created_at: day,
+ created_at: day
)
end
diff --git a/spec/lib/gitlab/data_builder/push_spec.rb b/spec/lib/gitlab/data_builder/push_spec.rb
index dbcfb9b7400..e59cba35b2f 100644
--- a/spec/lib/gitlab/data_builder/push_spec.rb
+++ b/spec/lib/gitlab/data_builder/push_spec.rb
@@ -35,6 +35,7 @@ describe Gitlab::DataBuilder::Push, lib: true do
it { expect(data[:ref]).to eq('refs/tags/v1.1.0') }
it { expect(data[:user_id]).to eq(user.id) }
it { expect(data[:user_name]).to eq(user.name) }
+ it { expect(data[:user_username]).to eq(user.username) }
it { expect(data[:user_email]).to eq(user.email) }
it { expect(data[:user_avatar]).to eq(user.avatar_url) }
it { expect(data[:project_id]).to eq(project.id) }
diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb
index 737fac14f92..dfa3ae9142e 100644
--- a/spec/lib/gitlab/database/migration_helpers_spec.rb
+++ b/spec/lib/gitlab/database/migration_helpers_spec.rb
@@ -382,13 +382,16 @@ describe Gitlab::Database::MigrationHelpers, lib: true do
expect(model).to receive(:add_column).
with(:users, :new, :integer,
limit: old_column.limit,
- default: old_column.default,
- null: old_column.null,
precision: old_column.precision,
scale: old_column.scale)
+ expect(model).to receive(:change_column_default).
+ with(:users, :new, old_column.default)
+
expect(model).to receive(:update_column_in_batches)
+ expect(model).to receive(:change_column_null).with(:users, :new, false)
+
expect(model).to receive(:copy_indexes).with(:users, :old, :new)
expect(model).to receive(:copy_foreign_keys).with(:users, :old, :new)
@@ -406,13 +409,16 @@ describe Gitlab::Database::MigrationHelpers, lib: true do
expect(model).to receive(:add_column).
with(:users, :new, :integer,
limit: old_column.limit,
- default: old_column.default,
- null: old_column.null,
precision: old_column.precision,
scale: old_column.scale)
+ expect(model).to receive(:change_column_default).
+ with(:users, :new, old_column.default)
+
expect(model).to receive(:update_column_in_batches)
+ expect(model).to receive(:change_column_null).with(:users, :new, false)
+
expect(model).to receive(:copy_indexes).with(:users, :old, :new)
expect(model).to receive(:copy_foreign_keys).with(:users, :old, :new)
diff --git a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb
index 64bc5fc0429..a3ab4e3dd9e 100644
--- a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb
+++ b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb
@@ -107,6 +107,15 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameBase do
expect(new_path).to eq('the-path0')
end
+ it "doesn't rename routes that start with a similar name" do
+ other_namespace = create(:namespace, path: 'the-path-but-not-really')
+ project = create(:empty_project, path: 'the-project', namespace: other_namespace)
+
+ subject.rename_path_for_routable(migration_namespace(namespace))
+
+ expect(project.route.reload.path).to eq('the-path-but-not-really/the-project')
+ end
+
context "the-path namespace -> subgroup -> the-path0 project" do
it "updates the route of the project correctly" do
subgroup = create(:group, path: "subgroup", parent: namespace)
diff --git a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb
index a25c5da488a..c56fded7516 100644
--- a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb
+++ b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb
@@ -23,6 +23,7 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameNamespaces do
found_ids = subject.namespaces_for_paths(type: :child).
map(&:id)
+
expect(found_ids).to contain_exactly(child.id)
end
end
@@ -39,6 +40,22 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameNamespaces do
found_ids = subject.namespaces_for_paths(type: :child).
map(&:id)
+
+ expect(found_ids).to contain_exactly(namespace.id)
+ end
+
+ it 'has no namespaces that look the same' do
+ _root_namespace = create(:namespace, path: 'THE-path')
+ _similar_path = create(:namespace,
+ path: 'not-really-the-path',
+ parent: create(:namespace))
+ namespace = create(:namespace,
+ path: 'the-path',
+ parent: create(:namespace))
+
+ found_ids = subject.namespaces_for_paths(type: :child).
+ map(&:id)
+
expect(found_ids).to contain_exactly(namespace.id)
end
end
@@ -53,6 +70,20 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameNamespaces do
found_ids = subject.namespaces_for_paths(type: :top_level).
map(&:id)
+
+ expect(found_ids).to contain_exactly(root_namespace.id)
+ end
+
+ it 'has no namespaces that just look the same' do
+ root_namespace = create(:namespace, path: 'the-path')
+ _similar_path = create(:namespace, path: 'not-really-the-path')
+ _child_namespace = create(:namespace,
+ path: 'the-path',
+ parent: create(:namespace))
+
+ found_ids = subject.namespaces_for_paths(type: :top_level).
+ map(&:id)
+
expect(found_ids).to contain_exactly(root_namespace.id)
end
end
@@ -106,7 +137,7 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameNamespaces do
end
describe "#rename_namespace" do
- let(:namespace) { create(:namespace, path: 'the-path') }
+ let(:namespace) { create(:group, name: 'the-path') }
it 'renames paths & routes for the namespace' do
expect(subject).to receive(:rename_path_for_routable).
@@ -146,6 +177,31 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameNamespaces do
subject.rename_namespace(namespace)
end
+
+ it "doesn't rename users for other namespaces" do
+ expect(subject).not_to receive(:rename_user)
+
+ subject.rename_namespace(namespace)
+ end
+
+ it 'renames the username of a namespace for a user' do
+ user = create(:user, username: 'the-path')
+
+ expect(subject).to receive(:rename_user).with('the-path', 'the-path0')
+
+ subject.rename_namespace(user.namespace)
+ end
+ end
+
+ describe '#rename_user' do
+ it 'renames a username' do
+ subject = described_class.new([], migration)
+ user = create(:user, username: 'broken')
+
+ subject.rename_user('broken', 'broken0')
+
+ expect(user.reload.username).to eq('broken0')
+ end
end
describe '#rename_namespaces' do
diff --git a/spec/lib/gitlab/dependency_linker/gemfile_linker_spec.rb b/spec/lib/gitlab/dependency_linker/gemfile_linker_spec.rb
new file mode 100644
index 00000000000..2e52097a946
--- /dev/null
+++ b/spec/lib/gitlab/dependency_linker/gemfile_linker_spec.rb
@@ -0,0 +1,60 @@
+require 'rails_helper'
+
+describe Gitlab::DependencyLinker::GemfileLinker, lib: true do
+ describe '.support?' do
+ it 'supports Gemfile' do
+ expect(described_class.support?('Gemfile')).to be_truthy
+ end
+
+ it 'supports gems.rb' do
+ expect(described_class.support?('gems.rb')).to be_truthy
+ end
+
+ it 'does not support other files' do
+ expect(described_class.support?('Gemfile.lock')).to be_falsey
+ end
+ end
+
+ describe '#link' do
+ let(:file_name) { 'Gemfile' }
+
+ let(:file_content) do
+ <<-CONTENT.strip_heredoc
+ source 'https://rubygems.org'
+
+ gem "rails", '4.2.6', github: "rails/rails"
+ gem 'rails-deprecated_sanitizer', '~> 1.0.3'
+ gem 'responders', '~> 2.0', :github => 'rails/responders'
+ gem 'sprockets', '~> 3.6.0', git: 'https://gitlab.example.com/gems/sprockets'
+ gem 'default_value_for', '~> 3.0.0'
+ CONTENT
+ end
+
+ subject { Gitlab::Highlight.highlight(file_name, file_content) }
+
+ def link(name, url)
+ %{<a href="#{url}" rel="noopener noreferrer" target="_blank">#{name}</a>}
+ end
+
+ it 'links sources' do
+ expect(subject).to include(link('https://rubygems.org', 'https://rubygems.org'))
+ end
+
+ it 'links dependencies' do
+ expect(subject).to include(link('rails', 'https://rubygems.org/gems/rails'))
+ expect(subject).to include(link('rails-deprecated_sanitizer', 'https://rubygems.org/gems/rails-deprecated_sanitizer'))
+ expect(subject).to include(link('responders', 'https://rubygems.org/gems/responders'))
+ expect(subject).to include(link('sprockets', 'https://rubygems.org/gems/sprockets'))
+ expect(subject).to include(link('default_value_for', 'https://rubygems.org/gems/default_value_for'))
+ end
+
+ it 'links GitHub repos' do
+ expect(subject).to include(link('rails/rails', 'https://github.com/rails/rails'))
+ expect(subject).to include(link('rails/responders', 'https://github.com/rails/responders'))
+ end
+
+ it 'links Git repos' do
+ expect(subject).to include(link('https://gitlab.example.com/gems/sprockets', 'https://gitlab.example.com/gems/sprockets'))
+ end
+ end
+end
diff --git a/spec/lib/gitlab/dependency_linker_spec.rb b/spec/lib/gitlab/dependency_linker_spec.rb
new file mode 100644
index 00000000000..03d5b61d70c
--- /dev/null
+++ b/spec/lib/gitlab/dependency_linker_spec.rb
@@ -0,0 +1,13 @@
+require 'rails_helper'
+
+describe Gitlab::DependencyLinker, lib: true do
+ describe '.link' do
+ it 'links using GemfileLinker' do
+ blob_name = 'Gemfile'
+
+ expect(described_class::GemfileLinker).to receive(:link)
+
+ described_class.link(blob_name, nil, nil)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/diff/highlight_spec.rb b/spec/lib/gitlab/diff/highlight_spec.rb
index c6bd4e81f4f..7d7d4a55e63 100644
--- a/spec/lib/gitlab/diff/highlight_spec.rb
+++ b/spec/lib/gitlab/diff/highlight_spec.rb
@@ -34,7 +34,7 @@ describe Gitlab::Diff::Highlight, lib: true do
end
it 'highlights and marks added lines' do
- code = %Q{+<span id="LC9" class="line" lang="ruby"> <span class="k">raise</span> <span class="no"><span class='idiff left'>RuntimeError</span></span><span class="p"><span class='idiff'>,</span></span><span class='idiff right'> </span><span class="s2">"System commands must be given as an array of strings"</span></span>\n}
+ code = %Q{+<span id="LC9" class="line" lang="ruby"> <span class="k">raise</span> <span class="no"><span class="idiff left">RuntimeError</span></span><span class="p"><span class="idiff">,</span></span><span class="idiff right"> </span><span class="s2">"System commands must be given as an array of strings"</span></span>\n}
expect(subject[5].text).to eq(code)
end
@@ -67,7 +67,7 @@ describe Gitlab::Diff::Highlight, lib: true do
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/diff/inline_diff_markdown_marker_spec.rb b/spec/lib/gitlab/diff/inline_diff_markdown_marker_spec.rb
new file mode 100644
index 00000000000..d6e8b8ac4b2
--- /dev/null
+++ b/spec/lib/gitlab/diff/inline_diff_markdown_marker_spec.rb
@@ -0,0 +1,14 @@
+require 'spec_helper'
+
+describe Gitlab::Diff::InlineDiffMarkdownMarker, lib: true do
+ describe '#mark' do
+ let(:raw) { "abc 'def'" }
+ let(:inline_diffs) { [2..5] }
+ let(:subject) { described_class.new(raw).mark(inline_diffs, mode: :deletion) }
+
+ it 'marks the range' do
+ expect(subject).to eq("ab{-c &#39;d-}ef&#39;")
+ expect(subject).to be_html_safe
+ end
+ end
+end
diff --git a/spec/lib/gitlab/diff/inline_diff_marker_spec.rb b/spec/lib/gitlab/diff/inline_diff_marker_spec.rb
index 198ff977f24..95da344802d 100644
--- a/spec/lib/gitlab/diff/inline_diff_marker_spec.rb
+++ b/spec/lib/gitlab/diff/inline_diff_marker_spec.rb
@@ -1,26 +1,26 @@
require 'spec_helper'
describe Gitlab::Diff::InlineDiffMarker, lib: true do
- describe '#inline_diffs' do
+ describe '#mark' do
context "when the rich text is html safe" do
- let(:raw) { "abc 'def'" }
+ let(:raw) { "abc 'def'" }
let(:rich) { %{<span class="abc">abc</span><span class="space"> </span><span class="def">&#39;def&#39;</span>}.html_safe }
let(:inline_diffs) { [2..5] }
- let(:subject) { Gitlab::Diff::InlineDiffMarker.new(raw, rich).mark(inline_diffs) }
+ let(:subject) { described_class.new(raw, rich).mark(inline_diffs) }
- it 'marks the inline diffs' do
- expect(subject).to eq(%{<span class="abc">ab<span class='idiff left'>c</span></span><span class="space"><span class='idiff'> </span></span><span class="def"><span class='idiff right'>&#39;d</span>ef&#39;</span>})
+ it 'marks the range' do
+ expect(subject).to eq(%{<span class="abc">ab<span class="idiff left">c</span></span><span class="space"><span class="idiff"> </span></span><span class="def"><span class="idiff right">&#39;d</span>ef&#39;</span>})
expect(subject).to be_html_safe
end
end
context "when the text text is not html safe" do
- let(:raw) { "abc 'def'" }
+ let(:raw) { "abc 'def'" }
let(:inline_diffs) { [2..5] }
- let(:subject) { Gitlab::Diff::InlineDiffMarker.new(raw).mark(inline_diffs) }
+ let(:subject) { described_class.new(raw).mark(inline_diffs) }
- it 'marks the inline diffs' do
- expect(subject).to eq(%{ab<span class='idiff left right'>c &#39;d</span>ef&#39;})
+ it 'marks the range' do
+ expect(subject).to eq(%{ab<span class="idiff left right">c &#39;d</span>ef&#39;})
expect(subject).to be_html_safe
end
end
diff --git a/spec/lib/gitlab/diff/position_tracer_spec.rb b/spec/lib/gitlab/diff/position_tracer_spec.rb
index a10a251dc4a..4d202a76e1b 100644
--- a/spec/lib/gitlab/diff/position_tracer_spec.rb
+++ b/spec/lib/gitlab/diff/position_tracer_spec.rb
@@ -1372,7 +1372,7 @@ describe Gitlab::Diff::PositionTracer, lib: true do
nil,
{ old_path: file_name, new_path: file_name, old_line: 5, new_line: 5 },
{ old_path: file_name, old_line: 6 },
- { new_path: file_name, new_line: 7 },
+ { new_path: file_name, new_line: 7 }
]
expect_positions(old_position_attrs, new_position_attrs)
@@ -1444,7 +1444,7 @@ describe Gitlab::Diff::PositionTracer, lib: true do
nil,
{ old_path: file_name, new_path: file_name, old_line: 5, new_line: 5 },
{ old_path: file_name, old_line: 6 },
- { new_path: file_name, new_line: 7 },
+ { new_path: file_name, new_line: 7 }
]
expect_positions(old_position_attrs, new_position_attrs)
@@ -1498,7 +1498,7 @@ describe Gitlab::Diff::PositionTracer, lib: true do
{ old_path: file_name, new_path: file_name, old_line: 5, new_line: 4 },
{ old_path: file_name, new_path: file_name, old_line: 6, new_line: 5 },
nil,
- { new_path: file_name, new_line: 6 },
+ { new_path: file_name, new_line: 6 }
]
expect_positions(old_position_attrs, new_position_attrs)
@@ -1746,7 +1746,7 @@ describe Gitlab::Diff::PositionTracer, lib: true do
{ old_path: file_name, new_path: file_name, old_line: 4, new_line: 5 },
{ old_path: file_name, old_line: 5 },
{ new_path: file_name, new_line: 6 },
- { new_path: file_name, new_line: 7 },
+ { new_path: file_name, new_line: 7 }
]
expect_positions(old_position_attrs, new_position_attrs)
diff --git a/spec/lib/gitlab/etag_caching/router_spec.rb b/spec/lib/gitlab/etag_caching/router_spec.rb
index f3dacb4ef04..5ae4a19263c 100644
--- a/spec/lib/gitlab/etag_caching/router_spec.rb
+++ b/spec/lib/gitlab/etag_caching/router_spec.rb
@@ -14,7 +14,7 @@ describe Gitlab::EtagCaching::Router do
it 'matches issue title endpoint' do
env = build_env(
- '/my-group/my-project/issues/123/rendered_title'
+ '/my-group/my-project/issues/123/realtime_changes'
)
result = described_class.match(env)
diff --git a/spec/lib/gitlab/file_finder_spec.rb b/spec/lib/gitlab/file_finder_spec.rb
new file mode 100644
index 00000000000..5a32ffd462c
--- /dev/null
+++ b/spec/lib/gitlab/file_finder_spec.rb
@@ -0,0 +1,21 @@
+require 'spec_helper'
+
+describe Gitlab::FileFinder, lib: true do
+ describe '#find' do
+ let(:project) { create(:project, :public, :repository) }
+ let(:finder) { described_class.new(project, project.default_branch) }
+
+ it 'finds by name' do
+ results = finder.find('files')
+ expect(results.map(&:first)).to include('files/images/wm.svg')
+ end
+
+ it 'finds by content' do
+ results = finder.find('files')
+
+ blob = results.select { |result| result.first == "CHANGELOG" }.flatten.last
+
+ expect(blob.filename).to eq("CHANGELOG")
+ end
+ end
+end
diff --git a/spec/lib/gitlab/git/branch_spec.rb b/spec/lib/gitlab/git/branch_spec.rb
index cdf1b8beee3..9eac7660cd1 100644
--- a/spec/lib/gitlab/git/branch_spec.rb
+++ b/spec/lib/gitlab/git/branch_spec.rb
@@ -7,6 +7,51 @@ describe Gitlab::Git::Branch, seed_helper: true do
it { is_expected.to be_kind_of Array }
+ describe 'initialize' do
+ let(:commit_id) { 'f00' }
+ let(:commit_subject) { "My commit".force_encoding('ASCII-8BIT') }
+ let(:committer) do
+ Gitaly::FindLocalBranchCommitAuthor.new(
+ name: generate(:name),
+ email: generate(:email),
+ date: Google::Protobuf::Timestamp.new(seconds: 123)
+ )
+ end
+ let(:author) do
+ Gitaly::FindLocalBranchCommitAuthor.new(
+ name: generate(:name),
+ email: generate(:email),
+ date: Google::Protobuf::Timestamp.new(seconds: 456)
+ )
+ end
+ let(:gitaly_branch) do
+ Gitaly::FindLocalBranchResponse.new(
+ name: 'foo', commit_id: commit_id, commit_subject: commit_subject,
+ commit_author: author, commit_committer: committer
+ )
+ end
+ let(:attributes) do
+ {
+ id: commit_id,
+ message: commit_subject,
+ authored_date: Time.at(author.date.seconds),
+ author_name: author.name,
+ author_email: author.email,
+ committed_date: Time.at(committer.date.seconds),
+ committer_name: committer.name,
+ committer_email: committer.email
+ }
+ end
+ let(:branch) { described_class.new(repository, 'foo', gitaly_branch) }
+
+ it 'parses Gitaly::FindLocalBranchResponse correctly' do
+ expect(Gitlab::Git::Commit).to receive(:decorate).
+ with(hash_including(attributes)).and_call_original
+
+ expect(branch.dereferenced_target.message.encoding).to be(Encoding::UTF_8)
+ end
+ end
+
describe '#size' do
subject { super().size }
it { is_expected.to eq(SeedRepo::Repo::BRANCHES.size) }
diff --git a/spec/lib/gitlab/git/diff_spec.rb b/spec/lib/gitlab/git/diff_spec.rb
index 7253a2edeff..4189aaef643 100644
--- a/spec/lib/gitlab/git/diff_spec.rb
+++ b/spec/lib/gitlab/git/diff_spec.rb
@@ -120,7 +120,7 @@ EOT
new_mode: 0100644,
from_id: '357406f3075a57708d0163752905cc1576fceacc',
to_id: '8e5177d718c561d36efde08bad36b43687ee6bf0',
- raw_chunks: raw_chunks,
+ raw_chunks: raw_chunks
)
)
end
diff --git a/spec/lib/gitlab/git/encoding_helper_spec.rb b/spec/lib/gitlab/git/encoding_helper_spec.rb
index f6ac7b23d1d..1a3bf802a07 100644
--- a/spec/lib/gitlab/git/encoding_helper_spec.rb
+++ b/spec/lib/gitlab/git/encoding_helper_spec.rb
@@ -19,8 +19,8 @@ describe Gitlab::Git::EncodingHelper do
[
'removes invalid bytes from ASCII-8bit encoded multibyte string. This can occur when a git diff match line truncates in the middle of a multibyte character. This occurs after the second word in this example. The test string is as short as we can get while still triggering the error condition when not looking at `detect[:confidence]`.',
"mu ns\xC3\n Lorem ipsum dolor sit amet, consectetur adipisicing ut\xC3\xA0y\xC3\xB9abcd\xC3\xB9efg kia elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non p\n {: .normal_pn}\n \n-Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in\n# *Lorem ipsum\xC3\xB9l\xC3\xB9l\xC3\xA0 dolor\xC3\xB9k\xC3\xB9 sit\xC3\xA8b\xC3\xA8 N\xC3\xA8 amet b\xC3\xA0d\xC3\xAC*\n+# *consectetur\xC3\xB9l\xC3\xB9l\xC3\xA0 adipisicing\xC3\xB9k\xC3\xB9 elit\xC3\xA8b\xC3\xA8 N\xC3\xA8 sed do\xC3\xA0d\xC3\xAC*{: .italic .smcaps}\n \n \xEF\x9B\xA1 eiusmod tempor incididunt, ut\xC3\xAAn\xC3\xB9 labore et dolore. Tw\xC4\x83nj\xC3\xAC magna aliqua. Ut enim ad minim veniam\n {: .normal}\n@@ -9,5 +9,5 @@ quis nostrud\xC3\xAAt\xC3\xB9 exercitiation ullamco laboris m\xC3\xB9s\xC3\xB9k\xC3\xB9abc\xC3\xB9 nisi ".force_encoding('ASCII-8BIT'),
- "mu ns\n Lorem ipsum dolor sit amet, consectetur adipisicing ut\xC3\xA0y\xC3\xB9abcd\xC3\xB9efg kia elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non p\n {: .normal_pn}\n \n-Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in\n# *Lorem ipsum\xC3\xB9l\xC3\xB9l\xC3\xA0 dolor\xC3\xB9k\xC3\xB9 sit\xC3\xA8b\xC3\xA8 N\xC3\xA8 amet b\xC3\xA0d\xC3\xAC*\n+# *consectetur\xC3\xB9l\xC3\xB9l\xC3\xA0 adipisicing\xC3\xB9k\xC3\xB9 elit\xC3\xA8b\xC3\xA8 N\xC3\xA8 sed do\xC3\xA0d\xC3\xAC*{: .italic .smcaps}\n \n \xEF\x9B\xA1 eiusmod tempor incididunt, ut\xC3\xAAn\xC3\xB9 labore et dolore. Tw\xC4\x83nj\xC3\xAC magna aliqua. Ut enim ad minim veniam\n {: .normal}\n@@ -9,5 +9,5 @@ quis nostrud\xC3\xAAt\xC3\xB9 exercitiation ullamco laboris m\xC3\xB9s\xC3\xB9k\xC3\xB9abc\xC3\xB9 nisi ",
- ],
+ "mu ns\n Lorem ipsum dolor sit amet, consectetur adipisicing ut\xC3\xA0y\xC3\xB9abcd\xC3\xB9efg kia elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non p\n {: .normal_pn}\n \n-Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in\n# *Lorem ipsum\xC3\xB9l\xC3\xB9l\xC3\xA0 dolor\xC3\xB9k\xC3\xB9 sit\xC3\xA8b\xC3\xA8 N\xC3\xA8 amet b\xC3\xA0d\xC3\xAC*\n+# *consectetur\xC3\xB9l\xC3\xB9l\xC3\xA0 adipisicing\xC3\xB9k\xC3\xB9 elit\xC3\xA8b\xC3\xA8 N\xC3\xA8 sed do\xC3\xA0d\xC3\xAC*{: .italic .smcaps}\n \n \xEF\x9B\xA1 eiusmod tempor incididunt, ut\xC3\xAAn\xC3\xB9 labore et dolore. Tw\xC4\x83nj\xC3\xAC magna aliqua. Ut enim ad minim veniam\n {: .normal}\n@@ -9,5 +9,5 @@ quis nostrud\xC3\xAAt\xC3\xB9 exercitiation ullamco laboris m\xC3\xB9s\xC3\xB9k\xC3\xB9abc\xC3\xB9 nisi "
+ ]
].each do |description, test_string, xpect|
it description do
expect(ext_class.encode!(test_string)).to eq(xpect)
@@ -37,18 +37,18 @@ describe Gitlab::Git::EncodingHelper do
[
"encodes valid utf8 encoded string to utf8",
"λ, λ, λ".encode("UTF-8"),
- "λ, λ, λ".encode("UTF-8"),
+ "λ, λ, λ".encode("UTF-8")
],
[
"encodes valid ASCII-8BIT encoded string to utf8",
"ascii only".encode("ASCII-8BIT"),
- "ascii only".encode("UTF-8"),
+ "ascii only".encode("UTF-8")
],
[
"encodes valid ISO-8859-1 encoded string to utf8",
"Rüby ist eine Programmiersprache. Wir verlängern den text damit ICU die Sprache erkennen kann.".encode("ISO-8859-1", "UTF-8"),
- "Rüby ist eine Programmiersprache. Wir verlängern den text damit ICU die Sprache erkennen kann.".encode("UTF-8"),
- ],
+ "Rüby ist eine Programmiersprache. Wir verlängern den text damit ICU die Sprache erkennen kann.".encode("UTF-8")
+ ]
].each do |description, test_string, xpect|
it description do
r = ext_class.encode_utf8(test_string.force_encoding('UTF-8'))
@@ -77,8 +77,8 @@ describe Gitlab::Git::EncodingHelper do
[
'removes invalid bytes from ASCII-8bit encoded multibyte string.',
"Lorem ipsum\xC3\n dolor sit amet, xy\xC3\xA0y\xC3\xB9abcd\xC3\xB9efg".force_encoding('ASCII-8BIT'),
- "Lorem ipsum\n dolor sit amet, xyàyùabcdùefg",
- ],
+ "Lorem ipsum\n dolor sit amet, xyàyùabcdùefg"
+ ]
].each do |description, test_string, xpect|
it description do
expect(ext_class.encode!(test_string)).to eq(xpect)
diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
index fea186fd4f4..cb107c6d1f9 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -26,6 +26,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
context 'with gitaly enabled' do
before { stub_gitaly }
+ after { Gitlab::GitalyClient.clear_stubs! }
it 'gets the branch name from GitalyClient' do
expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:default_branch_name)
@@ -120,6 +121,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
context 'with gitaly enabled' do
before { stub_gitaly }
+ after { Gitlab::GitalyClient.clear_stubs! }
it 'gets the branch names from GitalyClient' do
expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:branch_names)
@@ -157,6 +159,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
context 'with gitaly enabled' do
before { stub_gitaly }
+ after { Gitlab::GitalyClient.clear_stubs! }
it 'gets the tag names from GitalyClient' do
expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:tag_names)
@@ -1046,6 +1049,28 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
end
+ describe '#ref_name_for_sha' do
+ let(:ref_path) { 'refs/heads' }
+ let(:sha) { repository.find_branch('master').dereferenced_target.id }
+ let(:ref_name) { 'refs/heads/master' }
+
+ it 'returns the ref name for the given sha' do
+ expect(repository.ref_name_for_sha(ref_path, sha)).to eq(ref_name)
+ end
+
+ it "returns an empty name if the ref doesn't exist" do
+ expect(repository.ref_name_for_sha(ref_path, "000000")).to eq("")
+ end
+
+ it "raise an exception if the ref is empty" do
+ expect { repository.ref_name_for_sha(ref_path, "") }.to raise_error(ArgumentError)
+ end
+
+ it "raise an exception if the ref is nil" do
+ expect { repository.ref_name_for_sha(ref_path, nil) }.to raise_error(ArgumentError)
+ end
+ end
+
describe '#find_commits' do
it 'should return a return a collection of commits' do
commits = repository.find_commits
@@ -1080,7 +1105,9 @@ describe Gitlab::Git::Repository, seed_helper: true do
ref = double()
allow(ref).to receive(:name) { 'bad-branch' }
allow(ref).to receive(:target) { raise Rugged::ReferenceError }
- allow(repository.rugged).to receive(:branches) { [ref] }
+ branches = double()
+ allow(branches).to receive(:each) { [ref].each }
+ allow(repository.rugged).to receive(:branches) { branches }
end
it 'should return empty branches' do
@@ -1264,7 +1291,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
describe '#local_branches' do
before(:all) do
- @repo = Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH)
+ @repo = Gitlab::Git::Repository.new('default', File.join(TEST_MUTABLE_REPO_PATH, '.git'))
end
after(:all) do
@@ -1279,6 +1306,29 @@ describe Gitlab::Git::Repository, seed_helper: true do
expect(@repo.local_branches.any? { |branch| branch.name == 'remote_branch' }).to eq(false)
expect(@repo.local_branches.any? { |branch| branch.name == 'local_branch' }).to eq(true)
end
+
+ context 'with gitaly enabled' do
+ before { stub_gitaly }
+ after { Gitlab::GitalyClient.clear_stubs! }
+
+ it 'gets the branches from GitalyClient' do
+ expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:local_branches).
+ and_return([])
+ @repo.local_branches
+ end
+
+ it 'wraps GRPC not found' do
+ expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:local_branches).
+ and_raise(GRPC::NotFound)
+ expect { @repo.local_branches }.to raise_error(Gitlab::Git::Repository::NoRepository)
+ end
+
+ it 'wraps GRPC exceptions' do
+ expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:local_branches).
+ and_raise(GRPC::Unknown)
+ expect { @repo.local_branches }.to raise_error(Gitlab::Git::CommandError)
+ end
+ end
end
def create_remote_branch(remote_name, branch_name, source_branch_name)
diff --git a/spec/lib/gitlab/git/util_spec.rb b/spec/lib/gitlab/git/util_spec.rb
index 69d3ca55397..88c871855df 100644
--- a/spec/lib/gitlab/git/util_spec.rb
+++ b/spec/lib/gitlab/git/util_spec.rb
@@ -6,7 +6,7 @@ describe Gitlab::Git::Util do
["", 0],
["foo", 1],
["foo\n", 1],
- ["foo\n\n", 2],
+ ["foo\n\n", 2]
].each do |string, line_count|
it "counts #{line_count} lines in #{string.inspect}" do
expect(described_class.count_lines(string)).to eq(line_count)
diff --git a/spec/lib/gitlab/gitaly_client/commit_spec.rb b/spec/lib/gitlab/gitaly_client/commit_spec.rb
index abe08ccdfa1..cf1bc74779e 100644
--- a/spec/lib/gitlab/gitaly_client/commit_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/commit_spec.rb
@@ -1,23 +1,24 @@
require 'spec_helper'
describe Gitlab::GitalyClient::Commit do
- describe '.diff_from_parent' do
- let(:diff_stub) { double('Gitaly::Diff::Stub') }
- let(:project) { create(:project, :repository) }
- let(:repository_message) { project.repository.gitaly_repository }
- let(:commit) { project.commit('913c66a37b4a45b9769037c55c2d238bd0942d2e') }
+ let(:diff_stub) { double('Gitaly::Diff::Stub') }
+ let(:project) { create(:project, :repository) }
+ let(:repository) { project.repository }
+ let(:repository_message) { repository.gitaly_repository }
+ let(:commit) { project.commit('913c66a37b4a45b9769037c55c2d238bd0942d2e') }
+ describe '#diff_from_parent' do
context 'when a commit has a parent' do
it 'sends an RPC request with the parent ID as left commit' do
request = Gitaly::CommitDiffRequest.new(
repository: repository_message,
left_commit_id: 'cfe32cf61b73a0d5e9f13e774abde7ff789b1660',
- right_commit_id: commit.id,
+ right_commit_id: commit.id
)
expect_any_instance_of(Gitaly::Diff::Stub).to receive(:commit_diff).with(request)
- described_class.diff_from_parent(commit)
+ described_class.new(repository).diff_from_parent(commit)
end
end
@@ -27,17 +28,17 @@ describe Gitlab::GitalyClient::Commit do
request = Gitaly::CommitDiffRequest.new(
repository: repository_message,
left_commit_id: '4b825dc642cb6eb9a060e54bf8d69288fbee4904',
- right_commit_id: initial_commit.id,
+ right_commit_id: initial_commit.id
)
expect_any_instance_of(Gitaly::Diff::Stub).to receive(:commit_diff).with(request)
- described_class.diff_from_parent(initial_commit)
+ described_class.new(repository).diff_from_parent(initial_commit)
end
end
it 'returns a Gitlab::Git::DiffCollection' do
- ret = described_class.diff_from_parent(commit)
+ ret = described_class.new(repository).diff_from_parent(commit)
expect(ret).to be_kind_of(Gitlab::Git::DiffCollection)
end
@@ -47,7 +48,38 @@ describe Gitlab::GitalyClient::Commit do
expect(Gitlab::Git::DiffCollection).to receive(:new).with(kind_of(Enumerable), options)
- described_class.diff_from_parent(commit, options)
+ described_class.new(repository).diff_from_parent(commit, options)
+ end
+ end
+
+ describe '#commit_deltas' do
+ context 'when a commit has a parent' do
+ it 'sends an RPC request with the parent ID as left commit' do
+ request = Gitaly::CommitDeltaRequest.new(
+ repository: repository_message,
+ left_commit_id: 'cfe32cf61b73a0d5e9f13e774abde7ff789b1660',
+ right_commit_id: commit.id
+ )
+
+ expect_any_instance_of(Gitaly::Diff::Stub).to receive(:commit_delta).with(request).and_return([])
+
+ described_class.new(repository).commit_deltas(commit)
+ end
+ end
+
+ context 'when a commit does not have a parent' do
+ it 'sends an RPC request with empty tree ref as left commit' do
+ initial_commit = project.commit('1a0b36b3cdad1d2ee32457c102a8c0b7056fa863')
+ request = Gitaly::CommitDeltaRequest.new(
+ repository: repository_message,
+ left_commit_id: '4b825dc642cb6eb9a060e54bf8d69288fbee4904',
+ right_commit_id: initial_commit.id
+ )
+
+ expect_any_instance_of(Gitaly::Diff::Stub).to receive(:commit_delta).with(request).and_return([])
+
+ described_class.new(repository).commit_deltas(initial_commit)
+ end
end
end
end
diff --git a/spec/lib/gitlab/gitaly_client/ref_spec.rb b/spec/lib/gitlab/gitaly_client/ref_spec.rb
index 255f23e6270..d8cd2dcbd2a 100644
--- a/spec/lib/gitlab/gitaly_client/ref_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/ref_spec.rb
@@ -9,6 +9,13 @@ describe Gitlab::GitalyClient::Ref do
allow(Gitlab.config.gitaly).to receive(:enabled).and_return(true)
end
+ after do
+ # When we say `expect_any_instance_of(Gitaly::Ref::Stub)` a double is created,
+ # and because GitalyClient shares stubs these will get passed from example to
+ # example, which will cause an error, so we clean the stubs after each example.
+ Gitlab::GitalyClient.clear_stubs!
+ end
+
describe '#branch_names' do
it 'sends a find_all_branch_names message' do
expect_any_instance_of(Gitaly::Ref::Stub).
@@ -38,4 +45,27 @@ describe Gitlab::GitalyClient::Ref do
client.default_branch_name
end
end
+
+ describe '#local_branches' do
+ it 'sends a find_local_branches message' do
+ expect_any_instance_of(Gitaly::Ref::Stub).
+ to receive(:find_local_branches).with(gitaly_request_with_repo_path(repo_path)).
+ and_return([])
+
+ client.local_branches
+ end
+
+ it 'parses and sends the sort parameter' do
+ expect_any_instance_of(Gitaly::Ref::Stub).
+ to receive(:find_local_branches).
+ with(gitaly_request_with_params(sort_by: :UPDATED_DESC)).
+ and_return([])
+
+ client.local_branches(sort_by: 'updated_desc')
+ end
+
+ it 'raises an argument error if an invalid sort_by parameter is passed' do
+ expect { client.local_branches(sort_by: 'invalid_sort') }.to raise_error(ArgumentError)
+ end
+ end
end
diff --git a/spec/lib/gitlab/gitaly_client_spec.rb b/spec/lib/gitlab/gitaly_client_spec.rb
index 55fcf91fb6e..08ee0dff6b2 100644
--- a/spec/lib/gitlab/gitaly_client_spec.rb
+++ b/spec/lib/gitlab/gitaly_client_spec.rb
@@ -1,14 +1,19 @@
require 'spec_helper'
describe Gitlab::GitalyClient, lib: true do
- describe '.new_channel' do
+ describe '.stub' do
+ before { described_class.clear_stubs! }
+
context 'when passed a UNIX socket address' do
- it 'passes the address as-is to GRPC::Core::Channel initializer' do
+ it 'passes the address as-is to GRPC' do
address = 'unix:/tmp/gitaly.sock'
+ allow(Gitlab.config.repositories).to receive(:storages).and_return({
+ 'default' => { 'gitaly_address' => address }
+ })
- expect(GRPC::Core::Channel).to receive(:new).with(address, any_args)
+ expect(Gitaly::Commit::Stub).to receive(:new).with(address, any_args)
- described_class.new_channel(address)
+ described_class.stub(:commit, 'default')
end
end
@@ -17,9 +22,13 @@ describe Gitlab::GitalyClient, lib: true do
address = 'localhost:9876'
prefixed_address = "tcp://#{address}"
- expect(GRPC::Core::Channel).to receive(:new).with(address, any_args)
+ allow(Gitlab.config.repositories).to receive(:storages).and_return({
+ 'default' => { 'gitaly_address' => prefixed_address }
+ })
+
+ expect(Gitaly::Commit::Stub).to receive(:new).with(address, any_args)
- described_class.new_channel(prefixed_address)
+ described_class.stub(:commit, 'default')
end
end
end
diff --git a/spec/lib/gitlab/highlight_spec.rb b/spec/lib/gitlab/highlight_spec.rb
index e49799ad105..e57b3053871 100644
--- a/spec/lib/gitlab/highlight_spec.rb
+++ b/spec/lib/gitlab/highlight_spec.rb
@@ -57,4 +57,15 @@ describe Gitlab::Highlight, lib: true do
end
end
end
+
+ describe '#highlight' do
+ subject { described_class.highlight(file_name, file_content, nowrap: false) }
+
+ it 'links dependencies via DependencyLinker' do
+ expect(Gitlab::DependencyLinker).to receive(:link).
+ with('file.name', 'Contents', anything).and_call_original
+
+ described_class.highlight('file.name', 'Contents')
+ end
+ end
end
diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json
index fdbb6a0556d..e3599d6fe59 100644
--- a/spec/lib/gitlab/import_export/project.json
+++ b/spec/lib/gitlab/import_export/project.json
@@ -6997,7 +6997,7 @@
"merge_requests_events": true,
"tag_push_events": true,
"note_events": true,
- "build_events": true,
+ "job_events": true,
"type": "TeamcityService",
"category": "ci",
"default": false,
@@ -7041,7 +7041,7 @@
"merge_requests_events": true,
"tag_push_events": true,
"note_events": true,
- "build_events": true,
+ "job_events": true,
"type": "RedmineService",
"category": "issue_tracker",
"default": false,
@@ -7063,7 +7063,7 @@
"merge_requests_events": true,
"tag_push_events": true,
"note_events": true,
- "build_events": true,
+ "job_events": true,
"type": "PushoverService",
"category": "common",
"default": false,
@@ -7085,7 +7085,7 @@
"merge_requests_events": true,
"tag_push_events": true,
"note_events": true,
- "build_events": true,
+ "job_events": true,
"type": "PivotalTrackerService",
"category": "common",
"default": false,
@@ -7108,7 +7108,7 @@
"merge_requests_events": true,
"tag_push_events": true,
"note_events": true,
- "build_events": true,
+ "job_events": true,
"type": "JiraService",
"category": "issue_tracker",
"default": false,
@@ -7130,7 +7130,7 @@
"merge_requests_events": true,
"tag_push_events": true,
"note_events": true,
- "build_events": true,
+ "job_events": true,
"type": "IrkerService",
"category": "common",
"default": false,
@@ -7174,7 +7174,7 @@
"merge_requests_events": true,
"tag_push_events": true,
"note_events": true,
- "build_events": true,
+ "job_events": true,
"type": "GemnasiumService",
"category": "common",
"default": false,
@@ -7196,7 +7196,7 @@
"merge_requests_events": true,
"tag_push_events": true,
"note_events": true,
- "build_events": true,
+ "job_events": true,
"type": "FlowdockService",
"category": "common",
"default": false,
@@ -7218,7 +7218,7 @@
"merge_requests_events": true,
"tag_push_events": true,
"note_events": true,
- "build_events": true,
+ "job_events": true,
"type": "ExternalWikiService",
"category": "common",
"default": false,
@@ -7240,7 +7240,7 @@
"merge_requests_events": true,
"tag_push_events": true,
"note_events": true,
- "build_events": true,
+ "job_events": true,
"type": "EmailsOnPushService",
"category": "common",
"default": false,
@@ -7262,7 +7262,7 @@
"merge_requests_events": true,
"tag_push_events": true,
"note_events": true,
- "build_events": true,
+ "job_events": true,
"type": "DroneCiService",
"category": "ci",
"default": false,
@@ -7284,7 +7284,7 @@
"merge_requests_events": true,
"tag_push_events": true,
"note_events": true,
- "build_events": true,
+ "job_events": true,
"type": "CustomIssueTrackerService",
"category": "issue_tracker",
"default": false,
@@ -7306,7 +7306,7 @@
"merge_requests_events": true,
"tag_push_events": true,
"note_events": true,
- "build_events": true,
+ "job_events": true,
"type": "CampfireService",
"category": "common",
"default": false,
@@ -7328,7 +7328,7 @@
"merge_requests_events": true,
"tag_push_events": true,
"note_events": true,
- "build_events": true,
+ "job_events": true,
"type": "BuildkiteService",
"category": "ci",
"default": false,
@@ -7350,7 +7350,7 @@
"merge_requests_events": true,
"tag_push_events": true,
"note_events": true,
- "build_events": true,
+ "job_events": true,
"type": "BambooService",
"category": "ci",
"default": false,
@@ -7372,7 +7372,7 @@
"merge_requests_events": true,
"tag_push_events": true,
"note_events": true,
- "build_events": true,
+ "job_events": true,
"type": "AssemblaService",
"category": "common",
"default": false,
@@ -7394,7 +7394,7 @@
"merge_requests_events": true,
"tag_push_events": true,
"note_events": true,
- "build_events": true,
+ "job_events": true,
"type": "AssemblaService",
"category": "common",
"default": false,
@@ -7416,7 +7416,7 @@
"merge_requests_events": true,
"tag_push_events": true,
"note_events": true,
- "build_events": true,
+ "job_events": true,
"category": "common",
"default": false,
"wiki_page_events": true,
diff --git a/spec/lib/gitlab/import_export/relation_factory_spec.rb b/spec/lib/gitlab/import_export/relation_factory_spec.rb
index 06cd8ab87ed..5417c7534ea 100644
--- a/spec/lib/gitlab/import_export/relation_factory_spec.rb
+++ b/spec/lib/gitlab/import_export/relation_factory_spec.rb
@@ -33,7 +33,7 @@ describe Gitlab::ImportExport::RelationFactory, lib: true do
'tag_push_events' => false,
'note_events' => true,
'enable_ssl_verification' => true,
- 'build_events' => false,
+ 'job_events' => false,
'wiki_page_events' => true,
'token' => token
}
@@ -95,7 +95,7 @@ describe Gitlab::ImportExport::RelationFactory, lib: true do
'random_id' => 99,
'milestone_id' => 99,
'project_id' => 99,
- 'user_id' => 99,
+ 'user_id' => 99
}
end
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index d2ceb1cf9ae..c22fba11225 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -231,6 +231,7 @@ CommitStatus:
- lock_version
- coverage_regex
- auto_canceled_by_id
+- retried
Ci::Variable:
- id
- project_id
@@ -291,7 +292,7 @@ Service:
- tag_push_events
- note_events
- pipeline_events
-- build_events
+- job_events
- category
- default
- wiki_page_events
@@ -311,11 +312,12 @@ ProjectHook:
- note_events
- pipeline_events
- enable_ssl_verification
-- build_events
+- job_events
- wiki_page_events
- token
- group_id
- confidential_issues_events
+- repository_update_events
ProtectedBranch:
- id
- project_id
diff --git a/spec/lib/gitlab/other_markup_spec.rb b/spec/lib/gitlab/other_markup_spec.rb
index d6d53e8586c..c0f5fa9dc1f 100644
--- a/spec/lib/gitlab/other_markup_spec.rb
+++ b/spec/lib/gitlab/other_markup_spec.rb
@@ -13,7 +13,7 @@ describe Gitlab::OtherMarkup, lib: true do
}
links.each do |name, data|
it "does not convert dangerous #{name} into HTML" do
- expect(render(data[:file], data[:input])).to eq(data[:output])
+ expect(render(data[:file], data[:input], context)).to eq(data[:output])
end
end
end
diff --git a/spec/lib/gitlab/project_search_results_spec.rb b/spec/lib/gitlab/project_search_results_spec.rb
index a7c8e7f1f57..1b8690ba613 100644
--- a/spec/lib/gitlab/project_search_results_spec.rb
+++ b/spec/lib/gitlab/project_search_results_spec.rb
@@ -22,11 +22,40 @@ describe Gitlab::ProjectSearchResults, lib: true do
end
describe 'blob search' do
- let(:project) { create(:project, :repository) }
- let(:results) { described_class.new(user, project, 'files').objects('blobs') }
+ let(:project) { create(:project, :public, :repository) }
+
+ subject(:results) { described_class.new(user, project, 'files').objects('blobs') }
+
+ context 'when repository is disabled' do
+ let(:project) { create(:project, :public, :repository, :repository_disabled) }
+
+ it 'hides blobs from members' do
+ project.add_reporter(user)
+
+ is_expected.to be_empty
+ end
+
+ it 'hides blobs from non-members' do
+ is_expected.to be_empty
+ end
+ end
+
+ context 'when repository is internal' do
+ let(:project) { create(:project, :public, :repository, :repository_private) }
+
+ it 'finds blobs for members' do
+ project.add_reporter(user)
+
+ is_expected.not_to be_empty
+ end
+
+ it 'hides blobs from non-members' do
+ is_expected.to be_empty
+ end
+ end
it 'finds by name' do
- expect(results).to include(["files/images/wm.svg", nil])
+ expect(results.map(&:first)).to include('files/images/wm.svg')
end
it 'finds by content' do
@@ -70,6 +99,46 @@ describe Gitlab::ProjectSearchResults, lib: true do
end
end
+ describe 'wiki search' do
+ let(:project) { create(:project, :public) }
+ let(:wiki) { build(:project_wiki, project: project) }
+ let!(:wiki_page) { wiki.create_page('Title', 'Content') }
+
+ subject(:results) { described_class.new(user, project, 'Content').objects('wiki_blobs') }
+
+ context 'when wiki is disabled' do
+ let(:project) { create(:project, :public, :wiki_disabled) }
+
+ it 'hides wiki blobs from members' do
+ project.add_reporter(user)
+
+ is_expected.to be_empty
+ end
+
+ it 'hides wiki blobs from non-members' do
+ is_expected.to be_empty
+ end
+ end
+
+ context 'when wiki is internal' do
+ let(:project) { create(:project, :public, :wiki_private) }
+
+ it 'finds wiki blobs for members' do
+ project.add_reporter(user)
+
+ is_expected.not_to be_empty
+ end
+
+ it 'hides wiki blobs from non-members' do
+ is_expected.to be_empty
+ end
+ end
+
+ it 'finds by content' do
+ expect(results).to include("master:Title.md:1:Content\n")
+ end
+ end
+
it 'does not list issues on private projects' do
issue = create(:issue, project: project)
@@ -79,7 +148,6 @@ describe Gitlab::ProjectSearchResults, lib: true do
end
describe 'confidential issues' do
- let(:project) { create(:empty_project) }
let(:query) { 'issue' }
let(:author) { create(:user) }
let(:assignee) { create(:user) }
@@ -277,6 +345,7 @@ describe Gitlab::ProjectSearchResults, lib: true do
context 'by commit hash' do
let(:project) { create(:project, :public, :repository) }
let(:commit) { project.repository.commit('0b4bc9a') }
+
commit_hashes = { short: '0b4bc9a', full: '0b4bc9a49b562e85de7cc9e834518ea6828729b9' }
commit_hashes.each do |type, commit_hash|
diff --git a/spec/lib/gitlab/prometheus/queries/deployment_query_spec.rb b/spec/lib/gitlab/prometheus/queries/deployment_query_spec.rb
new file mode 100644
index 00000000000..d957dd932c4
--- /dev/null
+++ b/spec/lib/gitlab/prometheus/queries/deployment_query_spec.rb
@@ -0,0 +1,37 @@
+require 'spec_helper'
+
+describe Gitlab::Prometheus::Queries::DeploymentQuery, lib: true do
+ let(:environment) { create(:environment, slug: 'environment-slug') }
+ let(:deployment) { create(:deployment, environment: environment) }
+
+ let(:client) { double('prometheus_client') }
+ subject { described_class.new(client) }
+
+ around do |example|
+ time_without_subsecond_values = Time.local(2008, 9, 1, 12, 0, 0)
+ Timecop.freeze(time_without_subsecond_values) { example.run }
+ end
+
+ it 'sends appropriate queries to prometheus' do
+ start_time = (deployment.created_at - 30.minutes).to_f
+ stop_time = (deployment.created_at + 30.minutes).to_f
+ created_at = deployment.created_at.to_f
+
+ expect(client).to receive(:query_range).with('avg(container_memory_usage_bytes{container_name!="POD",environment="environment-slug"}) / 2^20',
+ start: start_time, stop: stop_time)
+ expect(client).to receive(:query).with('avg(avg_over_time(container_memory_usage_bytes{container_name!="POD",environment="environment-slug"}[30m]))',
+ time: created_at)
+ expect(client).to receive(:query).with('avg(avg_over_time(container_memory_usage_bytes{container_name!="POD",environment="environment-slug"}[30m]))',
+ time: stop_time)
+
+ expect(client).to receive(:query_range).with('avg(rate(container_cpu_usage_seconds_total{container_name!="POD",environment="environment-slug"}[2m])) * 100',
+ start: start_time, stop: stop_time)
+ expect(client).to receive(:query).with('avg(rate(container_cpu_usage_seconds_total{container_name!="POD",environment="environment-slug"}[30m])) * 100',
+ time: created_at)
+ expect(client).to receive(:query).with('avg(rate(container_cpu_usage_seconds_total{container_name!="POD",environment="environment-slug"}[30m])) * 100',
+ time: stop_time)
+
+ expect(subject.query(deployment.id)).to eq(memory_values: nil, memory_before: nil, memory_after: nil,
+ cpu_values: nil, cpu_before: nil, cpu_after: nil)
+ end
+end
diff --git a/spec/lib/gitlab/prometheus_spec.rb b/spec/lib/gitlab/prometheus_client_spec.rb
index 9d67e3d2f37..2d8bd2f6b97 100644
--- a/spec/lib/gitlab/prometheus_spec.rb
+++ b/spec/lib/gitlab/prometheus_client_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::Prometheus, lib: true do
+describe Gitlab::PrometheusClient, lib: true do
include PrometheusHelpers
subject { described_class.new(api_url: 'https://prometheus.example.com') }
diff --git a/spec/lib/gitlab/repo_path_spec.rb b/spec/lib/gitlab/repo_path_spec.rb
index f94c9c2e315..f9025397107 100644
--- a/spec/lib/gitlab/repo_path_spec.rb
+++ b/spec/lib/gitlab/repo_path_spec.rb
@@ -29,7 +29,7 @@ describe ::Gitlab::RepoPath do
before do
allow(Gitlab.config.repositories).to receive(:storages).and_return({
'storage1' => { 'path' => '/foo' },
- 'storage2' => { 'path' => '/bar' },
+ 'storage2' => { 'path' => '/bar' }
})
end
diff --git a/spec/lib/gitlab/string_range_marker_spec.rb b/spec/lib/gitlab/string_range_marker_spec.rb
new file mode 100644
index 00000000000..7c77772b3f6
--- /dev/null
+++ b/spec/lib/gitlab/string_range_marker_spec.rb
@@ -0,0 +1,36 @@
+require 'spec_helper'
+
+describe Gitlab::StringRangeMarker, lib: true do
+ describe '#mark' do
+ context "when the rich text is html safe" do
+ let(:raw) { "abc <def>" }
+ let(:rich) { %{<span class="abc">abc</span><span class="space"> </span><span class="def">&lt;def&gt;</span>}.html_safe }
+ let(:inline_diffs) { [2..5] }
+ subject do
+ described_class.new(raw, rich).mark(inline_diffs) do |text, left:, right:|
+ "LEFT#{text}RIGHT"
+ end
+ end
+
+ it 'marks the inline diffs' do
+ expect(subject).to eq(%{<span class="abc">abLEFTcRIGHT</span><span class="space">LEFT RIGHT</span><span class="def">LEFT&lt;dRIGHTef&gt;</span>})
+ expect(subject).to be_html_safe
+ end
+ end
+
+ context "when the rich text is not html safe" do
+ let(:raw) { "abc <def>" }
+ let(:inline_diffs) { [2..5] }
+ subject do
+ described_class.new(raw).mark(inline_diffs) do |text, left:, right:|
+ "LEFT#{text}RIGHT"
+ end
+ end
+
+ it 'marks the inline diffs' do
+ expect(subject).to eq(%{abLEFTc &lt;dRIGHTef&gt;})
+ expect(subject).to be_html_safe
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/string_regex_marker_spec.rb b/spec/lib/gitlab/string_regex_marker_spec.rb
new file mode 100644
index 00000000000..2f5cf6c6e3b
--- /dev/null
+++ b/spec/lib/gitlab/string_regex_marker_spec.rb
@@ -0,0 +1,18 @@
+require 'spec_helper'
+
+describe Gitlab::StringRegexMarker, lib: true do
+ describe '#mark' do
+ let(:raw) { %{"name": "AFNetworking"} }
+ let(:rich) { %{<span class="key">"name"</span><span class="punctuation">: </span><span class="value">"AFNetworking"</span>}.html_safe }
+ subject do
+ described_class.new(raw, rich).mark(/"[^"]+":\s*"(?<name>[^"]+)"/, group: :name) do |text, left:, right:|
+ %{<a href="#">#{text}</a>}
+ end
+ end
+
+ it 'marks the inline diffs' do
+ expect(subject).to eq(%{<span class="key">"name"</span><span class="punctuation">: </span><span class="value">"<a href="#">AFNetworking</a>"</span>})
+ expect(subject).to be_html_safe
+ end
+ end
+end
diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb
index 9046d5c413f..b47e1b56fa9 100644
--- a/spec/lib/gitlab/usage_data_spec.rb
+++ b/spec/lib/gitlab/usage_data_spec.rb
@@ -17,6 +17,7 @@ describe Gitlab::UsageData do
edition
version
uuid
+ hostname
))
end
@@ -49,7 +50,6 @@ describe Gitlab::UsageData do
pages_domains
protected_branches
releases
- services
snippets
todos
uploads
diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb
index beb1791a429..fdbb55fc874 100644
--- a/spec/lib/gitlab/workhorse_spec.rb
+++ b/spec/lib/gitlab/workhorse_spec.rb
@@ -202,7 +202,7 @@ describe Gitlab::Workhorse, lib: true do
context 'when Gitaly is enabled' do
let(:gitaly_params) do
{
- GitalyAddress: Gitlab::GitalyClient.get_address('default'),
+ GitalyAddress: Gitlab::GitalyClient.address('default')
}
end
@@ -214,7 +214,7 @@ describe Gitlab::Workhorse, lib: true do
repo_param = { Repository: {
path: repo_path,
storage_name: 'default',
- relative_path: project.full_path + '.git',
+ relative_path: project.full_path + '.git'
} }
expect(subject).to include(repo_param)
diff --git a/spec/migrations/cleanup_namespaceless_pending_delete_projects_spec.rb b/spec/migrations/cleanup_namespaceless_pending_delete_projects_spec.rb
new file mode 100644
index 00000000000..49e750a3f4d
--- /dev/null
+++ b/spec/migrations/cleanup_namespaceless_pending_delete_projects_spec.rb
@@ -0,0 +1,32 @@
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20170502101023_cleanup_namespaceless_pending_delete_projects.rb')
+
+describe CleanupNamespacelessPendingDeleteProjects do
+ before do
+ # Stub after_save callbacks that will fail when Project has no namespace
+ allow_any_instance_of(Project).to receive(:ensure_dir_exist).and_return(nil)
+ allow_any_instance_of(Project).to receive(:update_project_statistics).and_return(nil)
+ end
+
+ describe '#up' do
+ it 'only cleans up pending delete projects' do
+ create(:empty_project)
+ create(:empty_project, pending_delete: true)
+ project = build(:empty_project, pending_delete: true, namespace_id: nil)
+ project.save(validate: false)
+
+ expect(NamespacelessProjectDestroyWorker).to receive(:bulk_perform_async).with([[project.id]])
+
+ described_class.new.up
+ end
+
+ it 'does nothing when no pending delete projects without namespace found' do
+ create(:empty_project)
+ create(:empty_project, pending_delete: true)
+
+ expect(NamespacelessProjectDestroyWorker).not_to receive(:bulk_perform_async)
+
+ described_class.new.up
+ end
+ end
+end
diff --git a/spec/migrations/migrate_build_events_to_pipeline_events_spec.rb b/spec/migrations/migrate_build_events_to_pipeline_events_spec.rb
deleted file mode 100644
index 57eb03e3c80..00000000000
--- a/spec/migrations/migrate_build_events_to_pipeline_events_spec.rb
+++ /dev/null
@@ -1,74 +0,0 @@
-require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20170301205640_migrate_build_events_to_pipeline_events.rb')
-
-# This migration uses multiple threads, and thus different transactions. This
-# means data created in this spec may not be visible to some threads. To work
-# around this we use the TRUNCATE cleaning strategy.
-describe MigrateBuildEventsToPipelineEvents, truncate: true do
- let(:migration) { described_class.new }
- let(:project_with_pipeline_service) { create(:empty_project) }
- let(:project_with_build_service) { create(:empty_project) }
-
- before do
- ActiveRecord::Base.connection.execute <<-SQL
- INSERT INTO services (properties, build_events, pipeline_events, type)
- VALUES
- ('{"notify_only_broken_builds":true}', true, false, 'SlackService')
- , ('{"notify_only_broken_builds":true}', true, false, 'MattermostService')
- , ('{"notify_only_broken_builds":true}', true, false, 'HipchatService')
- ;
- SQL
-
- ActiveRecord::Base.connection.execute <<-SQL
- INSERT INTO services
- (properties, build_events, pipeline_events, type, project_id)
- VALUES
- ('{"notify_only_broken_builds":true}', true, false,
- 'BuildsEmailService', #{project_with_pipeline_service.id})
- , ('{"notify_only_broken_pipelines":true}', false, true,
- 'PipelinesEmailService', #{project_with_pipeline_service.id})
- , ('{"notify_only_broken_builds":true}', true, false,
- 'BuildsEmailService', #{project_with_build_service.id})
- ;
- SQL
- end
-
- describe '#up' do
- before do
- silence_migration = Module.new do
- # rubocop:disable Rails/Delegate
- def execute(query)
- connection.execute(query)
- end
- end
-
- migration.extend(silence_migration)
- migration.up
- end
-
- it 'migrates chat service properly' do
- [SlackService, MattermostService, HipchatService].each do |service|
- expect(service.count).to eq(1)
-
- verify_service_record(service.first)
- end
- end
-
- it 'migrates pipelines email service only if it has none before' do
- Project.find_each do |project|
- pipeline_service_count =
- project.services.where(type: 'PipelinesEmailService').count
-
- expect(pipeline_service_count).to eq(1)
-
- verify_service_record(project.pipelines_email_service)
- end
- end
-
- def verify_service_record(service)
- expect(service.notify_only_broken_pipelines).to be(true)
- expect(service.build_events).to be(false)
- expect(service.pipeline_events).to be(true)
- end
- end
-end
diff --git a/spec/migrations/migrate_user_project_view_spec.rb b/spec/migrations/migrate_user_project_view_spec.rb
index dacaa834aa9..70f8e0d6082 100644
--- a/spec/migrations/migrate_user_project_view_spec.rb
+++ b/spec/migrations/migrate_user_project_view_spec.rb
@@ -5,7 +5,12 @@ require Rails.root.join('db', 'post_migrate', '20170406142253_migrate_user_proje
describe MigrateUserProjectView do
let(:migration) { described_class.new }
- let!(:user) { create(:user, project_view: 'readme') }
+ let!(:user) { create(:user) }
+
+ before do
+ # 0 is the numeric value for the old 'readme' option
+ user.update_column(:project_view, 0)
+ end
describe '#up' do
it 'updates project view setting with new value' do
diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb
index c2c19c62048..fa229542f70 100644
--- a/spec/models/application_setting_spec.rb
+++ b/spec/models/application_setting_spec.rb
@@ -89,7 +89,7 @@ describe ApplicationSetting, models: true do
storages = {
'custom1' => 'tmp/tests/custom_repositories_1',
'custom2' => 'tmp/tests/custom_repositories_2',
- 'custom3' => 'tmp/tests/custom_repositories_3',
+ 'custom3' => 'tmp/tests/custom_repositories_3'
}
allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
@@ -211,4 +211,66 @@ describe ApplicationSetting, models: true do
expect(setting.domain_blacklist).to contain_exactly('example.com', 'test.com', 'foo.bar')
end
end
+
+ describe 'usage ping settings' do
+ context 'when the usage ping is disabled in gitlab.yml' do
+ before do
+ allow(Settings.gitlab).to receive(:usage_ping_enabled).and_return(false)
+ end
+
+ it 'does not allow the usage ping to be configured' do
+ expect(setting.usage_ping_can_be_configured?).to be_falsey
+ end
+
+ context 'when the usage ping is disabled in the DB' do
+ before do
+ setting.usage_ping_enabled = false
+ end
+
+ it 'returns false for usage_ping_enabled' do
+ expect(setting.usage_ping_enabled).to be_falsey
+ end
+ end
+
+ context 'when the usage ping is enabled in the DB' do
+ before do
+ setting.usage_ping_enabled = true
+ end
+
+ it 'returns false for usage_ping_enabled' do
+ expect(setting.usage_ping_enabled).to be_falsey
+ end
+ end
+ end
+
+ context 'when the usage ping is enabled in gitlab.yml' do
+ before do
+ allow(Settings.gitlab).to receive(:usage_ping_enabled).and_return(true)
+ end
+
+ it 'allows the usage ping to be configured' do
+ expect(setting.usage_ping_can_be_configured?).to be_truthy
+ end
+
+ context 'when the usage ping is disabled in the DB' do
+ before do
+ setting.usage_ping_enabled = false
+ end
+
+ it 'returns false for usage_ping_enabled' do
+ expect(setting.usage_ping_enabled).to be_falsey
+ end
+ end
+
+ context 'when the usage ping is enabled in the DB' do
+ before do
+ setting.usage_ping_enabled = true
+ end
+
+ it 'returns true for usage_ping_enabled' do
+ expect(setting.usage_ping_enabled).to be_truthy
+ end
+ end
+ end
+ end
end
diff --git a/spec/models/blob_spec.rb b/spec/models/blob_spec.rb
index f84c6b48173..f19e1af65a6 100644
--- a/spec/models/blob_spec.rb
+++ b/spec/models/blob_spec.rb
@@ -271,6 +271,52 @@ describe Blob do
end
end
+ describe '#auxiliary_viewer' do
+ context 'when the blob has an external storage error' do
+ before do
+ project.lfs_enabled = false
+ end
+
+ it 'returns nil' do
+ blob = fake_blob(path: 'LICENSE', lfs: true)
+
+ expect(blob.auxiliary_viewer).to be_nil
+ end
+ end
+
+ context 'when the blob is empty' do
+ it 'returns nil' do
+ blob = fake_blob(data: '')
+
+ expect(blob.auxiliary_viewer).to be_nil
+ end
+ end
+
+ context 'when the blob is stored externally' do
+ it 'returns a matching viewer' do
+ blob = fake_blob(path: 'LICENSE', lfs: true)
+
+ expect(blob.auxiliary_viewer).to be_a(BlobViewer::License)
+ end
+ end
+
+ context 'when the blob is binary' do
+ it 'returns nil' do
+ blob = fake_blob(path: 'LICENSE', binary: true)
+
+ expect(blob.auxiliary_viewer).to be_nil
+ end
+ end
+
+ context 'when the blob is text-based' do
+ it 'returns a matching text-based viewer' do
+ blob = fake_blob(path: 'LICENSE')
+
+ expect(blob.auxiliary_viewer).to be_a(BlobViewer::License)
+ end
+ end
+ end
+
describe '#rendered_as_text?' do
context 'when ignoring errors' do
context 'when the simple viewer is text-based' do
diff --git a/spec/models/blob_viewer/base_spec.rb b/spec/models/blob_viewer/base_spec.rb
index 740ad9d275e..92fbf64a6b7 100644
--- a/spec/models/blob_viewer/base_spec.rb
+++ b/spec/models/blob_viewer/base_spec.rb
@@ -7,10 +7,12 @@ describe BlobViewer::Base, model: true do
let(:viewer_class) do
Class.new(described_class) do
+ include BlobViewer::ServerSide
+
self.extensions = %w(pdf)
- self.max_size = 1.megabyte
- self.absolute_max_size = 5.megabytes
- self.client_side = false
+ self.binary = true
+ self.overridable_max_size = 1.megabyte
+ self.max_size = 5.megabytes
end
end
@@ -18,14 +20,47 @@ describe BlobViewer::Base, model: true do
describe '.can_render?' do
context 'when the extension is supported' do
- let(:blob) { fake_blob(path: 'file.pdf') }
+ context 'when the binaryness matches' do
+ let(:blob) { fake_blob(path: 'file.pdf', binary: true) }
- it 'returns true' do
- expect(viewer_class.can_render?(blob)).to be_truthy
+ it 'returns true' do
+ expect(viewer_class.can_render?(blob)).to be_truthy
+ end
+ end
+
+ context 'when the binaryness does not match' do
+ let(:blob) { fake_blob(path: 'file.pdf', binary: false) }
+
+ it 'returns false' do
+ expect(viewer_class.can_render?(blob)).to be_falsey
+ end
+ end
+ end
+
+ context 'when the file type is supported' do
+ before do
+ viewer_class.file_types = %i(license)
+ viewer_class.binary = false
+ end
+
+ context 'when the binaryness matches' do
+ let(:blob) { fake_blob(path: 'LICENSE', binary: false) }
+
+ it 'returns true' do
+ expect(viewer_class.can_render?(blob)).to be_truthy
+ end
+ end
+
+ context 'when the binaryness does not match' do
+ let(:blob) { fake_blob(path: 'LICENSE', binary: true) }
+
+ it 'returns false' do
+ expect(viewer_class.can_render?(blob)).to be_falsey
+ end
end
end
- context 'when the extension is not supported' do
+ context 'when the extension and file type are not supported' do
let(:blob) { fake_blob(path: 'file.txt') }
it 'returns false' do
@@ -34,45 +69,45 @@ describe BlobViewer::Base, model: true do
end
end
- describe '#too_large?' do
- context 'when the blob size is larger than the max size' do
+ describe '#exceeds_overridable_max_size?' do
+ context 'when the blob size is larger than the overridable max size' do
let(:blob) { fake_blob(path: 'file.pdf', size: 2.megabytes) }
it 'returns true' do
- expect(viewer.too_large?).to be_truthy
+ expect(viewer.exceeds_overridable_max_size?).to be_truthy
end
end
- context 'when the blob size is smaller than the max size' do
+ context 'when the blob size is smaller than the overridable max size' do
let(:blob) { fake_blob(path: 'file.pdf', size: 10.kilobytes) }
it 'returns false' do
- expect(viewer.too_large?).to be_falsey
+ expect(viewer.exceeds_overridable_max_size?).to be_falsey
end
end
end
- describe '#absolutely_too_large?' do
- context 'when the blob size is larger than the absolute max size' do
+ describe '#exceeds_max_size?' do
+ context 'when the blob size is larger than the max size' do
let(:blob) { fake_blob(path: 'file.pdf', size: 10.megabytes) }
it 'returns true' do
- expect(viewer.absolutely_too_large?).to be_truthy
+ expect(viewer.exceeds_max_size?).to be_truthy
end
end
- context 'when the blob size is smaller than the absolute max size' do
+ context 'when the blob size is smaller than the max size' do
let(:blob) { fake_blob(path: 'file.pdf', size: 2.megabytes) }
it 'returns false' do
- expect(viewer.absolutely_too_large?).to be_falsey
+ expect(viewer.exceeds_max_size?).to be_falsey
end
end
end
describe '#can_override_max_size?' do
- context 'when the blob size is larger than the max size' do
- context 'when the blob size is larger than the absolute max size' do
+ context 'when the blob size is larger than the overridable max size' do
+ context 'when the blob size is larger than the max size' do
let(:blob) { fake_blob(path: 'file.pdf', size: 10.megabytes) }
it 'returns false' do
@@ -80,7 +115,7 @@ describe BlobViewer::Base, model: true do
end
end
- context 'when the blob size is smaller than the absolute max size' do
+ context 'when the blob size is smaller than the max size' do
let(:blob) { fake_blob(path: 'file.pdf', size: 2.megabytes) }
it 'returns true' do
@@ -89,7 +124,7 @@ describe BlobViewer::Base, model: true do
end
end
- context 'when the blob size is smaller than the max size' do
+ context 'when the blob size is smaller than the overridable max size' do
let(:blob) { fake_blob(path: 'file.pdf', size: 10.kilobytes) }
it 'returns false' do
@@ -104,7 +139,7 @@ describe BlobViewer::Base, model: true do
viewer.override_max_size = true
end
- context 'when the blob size is larger than the absolute max size' do
+ context 'when the blob size is larger than the max size' do
let(:blob) { fake_blob(path: 'file.pdf', size: 10.megabytes) }
it 'returns :too_large' do
@@ -112,7 +147,7 @@ describe BlobViewer::Base, model: true do
end
end
- context 'when the blob size is smaller than the absolute max size' do
+ context 'when the blob size is smaller than the max size' do
let(:blob) { fake_blob(path: 'file.pdf', size: 2.megabytes) }
it 'returns nil' do
@@ -122,7 +157,7 @@ describe BlobViewer::Base, model: true do
end
context 'when the max size is not overridden' do
- context 'when the blob size is larger than the max size' do
+ context 'when the blob size is larger than the overridable max size' do
let(:blob) { fake_blob(path: 'file.pdf', size: 2.megabytes) }
it 'returns :too_large' do
@@ -130,7 +165,7 @@ describe BlobViewer::Base, model: true do
end
end
- context 'when the blob size is smaller than the max size' do
+ context 'when the blob size is smaller than the overridable max size' do
let(:blob) { fake_blob(path: 'file.pdf', size: 10.kilobytes) }
it 'returns nil' do
@@ -138,49 +173,5 @@ describe BlobViewer::Base, model: true do
end
end
end
-
- context 'when the viewer is server side but the blob is stored externally' do
- let(:project) { build(:empty_project, lfs_enabled: true) }
-
- let(:blob) { fake_blob(path: 'file.pdf', lfs: true) }
-
- before do
- allow(Gitlab.config.lfs).to receive(:enabled).and_return(true)
- end
-
- it 'return :server_side_but_stored_externally' do
- expect(viewer.render_error).to eq(:server_side_but_stored_externally)
- end
- end
- end
-
- describe '#prepare!' do
- context 'when the viewer is server side' do
- let(:blob) { fake_blob(path: 'file.md') }
-
- before do
- viewer_class.client_side = false
- end
-
- it 'loads all blob data' do
- expect(blob).to receive(:load_all_data!)
-
- viewer.prepare!
- end
- end
-
- context 'when the viewer is client side' do
- let(:blob) { fake_blob(path: 'file.md') }
-
- before do
- viewer_class.client_side = true
- end
-
- it "doesn't load all blob data" do
- expect(blob).not_to receive(:load_all_data!)
-
- viewer.prepare!
- end
- end
end
end
diff --git a/spec/models/blob_viewer/changelog_spec.rb b/spec/models/blob_viewer/changelog_spec.rb
new file mode 100644
index 00000000000..9066c5a05ac
--- /dev/null
+++ b/spec/models/blob_viewer/changelog_spec.rb
@@ -0,0 +1,27 @@
+require 'spec_helper'
+
+describe BlobViewer::Changelog, model: true do
+ include FakeBlobHelpers
+
+ let(:project) { create(:project, :repository) }
+ let(:blob) { fake_blob(path: 'CHANGELOG') }
+ subject { described_class.new(blob) }
+
+ describe '#render_error' do
+ context 'when there are no tags' do
+ before do
+ allow(project.repository).to receive(:tag_count).and_return(0)
+ end
+
+ it 'returns :no_tags' do
+ expect(subject.render_error).to eq(:no_tags)
+ end
+ end
+
+ context 'when there are tags' do
+ it 'returns nil' do
+ expect(subject.render_error).to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/models/blob_viewer/composer_json_spec.rb b/spec/models/blob_viewer/composer_json_spec.rb
new file mode 100644
index 00000000000..df4f1f4815c
--- /dev/null
+++ b/spec/models/blob_viewer/composer_json_spec.rb
@@ -0,0 +1,25 @@
+require 'spec_helper'
+
+describe BlobViewer::ComposerJson, model: true do
+ include FakeBlobHelpers
+
+ let(:project) { build(:project) }
+ let(:data) do
+ <<-SPEC.strip_heredoc
+ {
+ "name": "laravel/laravel",
+ "homepage": "https://laravel.com/"
+ }
+ SPEC
+ end
+ let(:blob) { fake_blob(path: 'composer.json', data: data) }
+ subject { described_class.new(blob) }
+
+ describe '#package_name' do
+ it 'returns the package name' do
+ expect(subject).to receive(:prepare!)
+
+ expect(subject.package_name).to eq('laravel/laravel')
+ end
+ end
+end
diff --git a/spec/models/blob_viewer/gemspec_spec.rb b/spec/models/blob_viewer/gemspec_spec.rb
new file mode 100644
index 00000000000..81e932de290
--- /dev/null
+++ b/spec/models/blob_viewer/gemspec_spec.rb
@@ -0,0 +1,25 @@
+require 'spec_helper'
+
+describe BlobViewer::Gemspec, model: true do
+ include FakeBlobHelpers
+
+ let(:project) { build(:project) }
+ let(:data) do
+ <<-SPEC.strip_heredoc
+ Gem::Specification.new do |s|
+ s.platform = Gem::Platform::RUBY
+ s.name = "activerecord"
+ end
+ SPEC
+ end
+ let(:blob) { fake_blob(path: 'activerecord.gemspec', data: data) }
+ subject { described_class.new(blob) }
+
+ describe '#package_name' do
+ it 'returns the package name' do
+ expect(subject).to receive(:prepare!)
+
+ expect(subject.package_name).to eq('activerecord')
+ end
+ end
+end
diff --git a/spec/models/blob_viewer/gitlab_ci_yml_spec.rb b/spec/models/blob_viewer/gitlab_ci_yml_spec.rb
new file mode 100644
index 00000000000..0c6c24ece21
--- /dev/null
+++ b/spec/models/blob_viewer/gitlab_ci_yml_spec.rb
@@ -0,0 +1,32 @@
+require 'spec_helper'
+
+describe BlobViewer::GitlabCiYml, model: true do
+ include FakeBlobHelpers
+
+ let(:project) { build(:project) }
+ let(:data) { File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml')) }
+ let(:blob) { fake_blob(path: '.gitlab-ci.yml', data: data) }
+ subject { described_class.new(blob) }
+
+ describe '#validation_message' do
+ it 'calls prepare! on the viewer' do
+ expect(subject).to receive(:prepare!)
+
+ subject.validation_message
+ end
+
+ context 'when the configuration is valid' do
+ it 'returns nil' do
+ expect(subject.validation_message).to be_nil
+ end
+ end
+
+ context 'when the configuration is invalid' do
+ let(:data) { 'oof' }
+
+ it 'returns the error message' do
+ expect(subject.validation_message).to eq('Invalid configuration format')
+ end
+ end
+ end
+end
diff --git a/spec/models/blob_viewer/license_spec.rb b/spec/models/blob_viewer/license_spec.rb
new file mode 100644
index 00000000000..944ddd32b92
--- /dev/null
+++ b/spec/models/blob_viewer/license_spec.rb
@@ -0,0 +1,34 @@
+require 'spec_helper'
+
+describe BlobViewer::License, model: true do
+ include FakeBlobHelpers
+
+ let(:project) { create(:project, :repository) }
+ let(:blob) { fake_blob(path: 'LICENSE') }
+ subject { described_class.new(blob) }
+
+ describe '#license' do
+ it 'returns the blob project repository license' do
+ expect(subject.license).not_to be_nil
+ expect(subject.license).to eq(project.repository.license)
+ end
+ end
+
+ describe '#render_error' do
+ context 'when there is no license' do
+ before do
+ allow(project.repository).to receive(:license).and_return(nil)
+ end
+
+ it 'returns :unknown_license' do
+ expect(subject.render_error).to eq(:unknown_license)
+ end
+ end
+
+ context 'when there is a license' do
+ it 'returns nil' do
+ expect(subject.render_error).to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/models/blob_viewer/package_json_spec.rb b/spec/models/blob_viewer/package_json_spec.rb
new file mode 100644
index 00000000000..5c9a9c81963
--- /dev/null
+++ b/spec/models/blob_viewer/package_json_spec.rb
@@ -0,0 +1,25 @@
+require 'spec_helper'
+
+describe BlobViewer::PackageJson, model: true do
+ include FakeBlobHelpers
+
+ let(:project) { build(:project) }
+ let(:data) do
+ <<-SPEC.strip_heredoc
+ {
+ "name": "module-name",
+ "version": "10.3.1"
+ }
+ SPEC
+ end
+ let(:blob) { fake_blob(path: 'package.json', data: data) }
+ subject { described_class.new(blob) }
+
+ describe '#package_name' do
+ it 'returns the package name' do
+ expect(subject).to receive(:prepare!)
+
+ expect(subject.package_name).to eq('module-name')
+ end
+ end
+end
diff --git a/spec/models/blob_viewer/podspec_json_spec.rb b/spec/models/blob_viewer/podspec_json_spec.rb
new file mode 100644
index 00000000000..42a00940bc5
--- /dev/null
+++ b/spec/models/blob_viewer/podspec_json_spec.rb
@@ -0,0 +1,25 @@
+require 'spec_helper'
+
+describe BlobViewer::PodspecJson, model: true do
+ include FakeBlobHelpers
+
+ let(:project) { build(:project) }
+ let(:data) do
+ <<-SPEC.strip_heredoc
+ {
+ "name": "AFNetworking",
+ "version": "2.0.0"
+ }
+ SPEC
+ end
+ let(:blob) { fake_blob(path: 'AFNetworking.podspec.json', data: data) }
+ subject { described_class.new(blob) }
+
+ describe '#package_name' do
+ it 'returns the package name' do
+ expect(subject).to receive(:prepare!)
+
+ expect(subject.package_name).to eq('AFNetworking')
+ end
+ end
+end
diff --git a/spec/models/blob_viewer/podspec_spec.rb b/spec/models/blob_viewer/podspec_spec.rb
new file mode 100644
index 00000000000..6c9f0f42d53
--- /dev/null
+++ b/spec/models/blob_viewer/podspec_spec.rb
@@ -0,0 +1,25 @@
+require 'spec_helper'
+
+describe BlobViewer::Podspec, model: true do
+ include FakeBlobHelpers
+
+ let(:project) { build(:project) }
+ let(:data) do
+ <<-SPEC.strip_heredoc
+ Pod::Spec.new do |spec|
+ spec.name = 'Reachability'
+ spec.version = '3.1.0'
+ end
+ SPEC
+ end
+ let(:blob) { fake_blob(path: 'Reachability.podspec', data: data) }
+ subject { described_class.new(blob) }
+
+ describe '#package_name' do
+ it 'returns the package name' do
+ expect(subject).to receive(:prepare!)
+
+ expect(subject.package_name).to eq('Reachability')
+ end
+ end
+end
diff --git a/spec/models/blob_viewer/route_map_spec.rb b/spec/models/blob_viewer/route_map_spec.rb
new file mode 100644
index 00000000000..4854e0262d9
--- /dev/null
+++ b/spec/models/blob_viewer/route_map_spec.rb
@@ -0,0 +1,38 @@
+require 'spec_helper'
+
+describe BlobViewer::RouteMap, model: true do
+ include FakeBlobHelpers
+
+ let(:project) { build(:project) }
+ let(:data) do
+ <<-MAP.strip_heredoc
+ # Team data
+ - source: 'data/team.yml'
+ public: 'team/'
+ MAP
+ end
+ let(:blob) { fake_blob(path: '.gitlab/route-map.yml', data: data) }
+ subject { described_class.new(blob) }
+
+ describe '#validation_message' do
+ it 'calls prepare! on the viewer' do
+ expect(subject).to receive(:prepare!)
+
+ subject.validation_message
+ end
+
+ context 'when the configuration is valid' do
+ it 'returns nil' do
+ expect(subject.validation_message).to be_nil
+ end
+ end
+
+ context 'when the configuration is invalid' do
+ let(:data) { 'oof' }
+
+ it 'returns the error message' do
+ expect(subject.validation_message).to eq('Route map is not an array')
+ end
+ end
+ end
+end
diff --git a/spec/models/blob_viewer/server_side_spec.rb b/spec/models/blob_viewer/server_side_spec.rb
new file mode 100644
index 00000000000..f047953d540
--- /dev/null
+++ b/spec/models/blob_viewer/server_side_spec.rb
@@ -0,0 +1,41 @@
+require 'spec_helper'
+
+describe BlobViewer::ServerSide, model: true do
+ include FakeBlobHelpers
+
+ let(:project) { build(:empty_project) }
+
+ let(:viewer_class) do
+ Class.new(BlobViewer::Base) do
+ include BlobViewer::ServerSide
+ end
+ end
+
+ subject { viewer_class.new(blob) }
+
+ describe '#prepare!' do
+ let(:blob) { fake_blob(path: 'file.txt') }
+
+ it 'loads all blob data' do
+ expect(blob).to receive(:load_all_data!)
+
+ subject.prepare!
+ end
+ end
+
+ describe '#render_error' do
+ context 'when the blob is stored externally' do
+ let(:project) { build(:empty_project, lfs_enabled: true) }
+
+ let(:blob) { fake_blob(path: 'file.pdf', lfs: true) }
+
+ before do
+ allow(Gitlab.config.lfs).to receive(:enabled).and_return(true)
+ end
+
+ it 'return :server_side_but_stored_externally' do
+ expect(subject.render_error).to eq(:server_side_but_stored_externally)
+ end
+ end
+ end
+end
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 5231ce28c9d..e971b4bc3f9 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -972,7 +972,7 @@ describe Ci::Build, :models do
'fix-1-foo' => 'fix-1-foo',
'a' * 63 => 'a' * 63,
'a' * 64 => 'a' * 63,
- 'FOO' => 'foo',
+ 'FOO' => 'foo'
}.each do |ref, slug|
it "transforms #{ref} to #{slug}" do
build.ref = ref
@@ -1144,7 +1144,7 @@ describe Ci::Build, :models do
{ key: 'CI_PIPELINE_ID', value: pipeline.id.to_s, public: true },
{ key: 'CI_REGISTRY_USER', value: 'gitlab-ci-token', public: true },
{ key: 'CI_REGISTRY_PASSWORD', value: build.token, public: false },
- { key: 'CI_REPOSITORY_URL', value: build.repo_url, public: false },
+ { key: 'CI_REPOSITORY_URL', value: build.repo_url, public: false }
]
end
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 06e990a0574..157d17fbb68 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -60,8 +60,8 @@ describe Ci::Pipeline, models: true do
subject { pipeline.retried }
before do
- @build1 = FactoryGirl.create :ci_build, pipeline: pipeline, name: 'deploy'
- @build2 = FactoryGirl.create :ci_build, pipeline: pipeline, name: 'deploy'
+ @build1 = create(:ci_build, pipeline: pipeline, name: 'deploy', retried: true)
+ @build2 = create(:ci_build, pipeline: pipeline, name: 'deploy')
end
it 'returns old builds' do
@@ -70,31 +70,31 @@ describe Ci::Pipeline, models: true do
end
describe "coverage" do
- let(:project) { FactoryGirl.create :empty_project, build_coverage_regex: "/.*/" }
- let(:pipeline) { FactoryGirl.create :ci_empty_pipeline, project: project }
+ let(:project) { create(:empty_project, build_coverage_regex: "/.*/") }
+ let(:pipeline) { create(:ci_empty_pipeline, project: project) }
it "calculates average when there are two builds with coverage" do
- FactoryGirl.create :ci_build, name: "rspec", coverage: 30, pipeline: pipeline
- FactoryGirl.create :ci_build, name: "rubocop", coverage: 40, pipeline: pipeline
+ create(:ci_build, name: "rspec", coverage: 30, pipeline: pipeline)
+ create(:ci_build, name: "rubocop", coverage: 40, pipeline: pipeline)
expect(pipeline.coverage).to eq("35.00")
end
it "calculates average when there are two builds with coverage and one with nil" do
- FactoryGirl.create :ci_build, name: "rspec", coverage: 30, pipeline: pipeline
- FactoryGirl.create :ci_build, name: "rubocop", coverage: 40, pipeline: pipeline
- FactoryGirl.create :ci_build, pipeline: pipeline
+ create(:ci_build, name: "rspec", coverage: 30, pipeline: pipeline)
+ create(:ci_build, name: "rubocop", coverage: 40, pipeline: pipeline)
+ create(:ci_build, pipeline: pipeline)
expect(pipeline.coverage).to eq("35.00")
end
it "calculates average when there are two builds with coverage and one is retried" do
- FactoryGirl.create :ci_build, name: "rspec", coverage: 30, pipeline: pipeline
- FactoryGirl.create :ci_build, name: "rubocop", coverage: 30, pipeline: pipeline
- FactoryGirl.create :ci_build, name: "rubocop", coverage: 40, pipeline: pipeline
+ create(:ci_build, name: "rspec", coverage: 30, pipeline: pipeline)
+ create(:ci_build, name: "rubocop", coverage: 30, pipeline: pipeline, retried: true)
+ create(:ci_build, name: "rubocop", coverage: 40, pipeline: pipeline)
expect(pipeline.coverage).to eq("35.00")
end
it "calculates average when there is one build without coverage" do
- FactoryGirl.create :ci_build, pipeline: pipeline
+ FactoryGirl.create(:ci_build, pipeline: pipeline)
expect(pipeline.coverage).to be_nil
end
end
@@ -222,13 +222,15 @@ describe Ci::Pipeline, models: true do
%w(deploy running)])
end
- context 'when commit status is retried' do
+ context 'when commit status is retried' do
before do
create(:commit_status, pipeline: pipeline,
stage: 'build',
name: 'mac',
stage_idx: 0,
status: 'success')
+
+ pipeline.process!
end
it 'ignores the previous state' do
@@ -489,6 +491,10 @@ describe Ci::Pipeline, models: true do
context 'there are multiple of the same name' do
let!(:manual2) { create(:ci_build, :manual, pipeline: pipeline, name: 'deploy') }
+ before do
+ manual.update(retried: true)
+ end
+
it 'returns latest one' do
is_expected.to contain_exactly(manual2)
end
diff --git a/spec/models/ci/stage_spec.rb b/spec/models/ci/stage_spec.rb
index 372b662fab2..8f6ab908987 100644
--- a/spec/models/ci/stage_spec.rb
+++ b/spec/models/ci/stage_spec.rb
@@ -102,6 +102,10 @@ describe Ci::Stage, models: true do
context 'and builds are retried' do
let!(:new_build) { create_job(:ci_build, status: :success) }
+ before do
+ stage_build.update(retried: true)
+ end
+
it "returns status of latest build" do
is_expected.to eq('success')
end
diff --git a/spec/models/ci/variable_spec.rb b/spec/models/ci/variable_spec.rb
index 048d25869bc..fe8c52d5353 100644
--- a/spec/models/ci/variable_spec.rb
+++ b/spec/models/ci/variable_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Ci::Variable, models: true do
- subject { Ci::Variable.new }
+ subject { build(:ci_variable) }
let(:secret_value) { 'secret' }
diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb
index 852889d4540..72f83d63224 100644
--- a/spec/models/commit_spec.rb
+++ b/spec/models/commit_spec.rb
@@ -388,32 +388,4 @@ eos
expect(described_class.valid_hash?('a' * 41)).to be false
end
end
-
- describe '#raw_diffs' do
- context 'Gitaly commit_raw_diffs feature enabled' do
- before do
- allow(Gitlab::GitalyClient).to receive(:feature_enabled?).with(:commit_raw_diffs).and_return(true)
- end
-
- context 'when a truthy deltas_only is not passed to args' do
- it 'fetches diffs from Gitaly server' do
- expect(Gitlab::GitalyClient::Commit).to receive(:diff_from_parent).
- with(commit)
-
- commit.raw_diffs
- end
- end
-
- context 'when a truthy deltas_only is passed to args' do
- it 'fetches diffs using Rugged' do
- opts = { deltas_only: true }
-
- expect(Gitlab::GitalyClient::Commit).not_to receive(:diff_from_parent)
- expect(commit.raw).to receive(:diffs).with(opts)
-
- commit.raw_diffs(opts)
- end
- end
- end
- end
end
diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb
index 0ee85489574..6947affcc1e 100644
--- a/spec/models/commit_status_spec.rb
+++ b/spec/models/commit_status_spec.rb
@@ -157,9 +157,9 @@ describe CommitStatus, :models do
subject { described_class.latest.order(:id) }
let(:statuses) do
- [create_status(name: 'aa', ref: 'bb', status: 'running'),
- create_status(name: 'cc', ref: 'cc', status: 'pending'),
- create_status(name: 'aa', ref: 'cc', status: 'success'),
+ [create_status(name: 'aa', ref: 'bb', status: 'running', retried: true),
+ create_status(name: 'cc', ref: 'cc', status: 'pending', retried: true),
+ create_status(name: 'aa', ref: 'cc', status: 'success', retried: true),
create_status(name: 'cc', ref: 'bb', status: 'success'),
create_status(name: 'aa', ref: 'bb', status: 'success')]
end
@@ -169,6 +169,22 @@ describe CommitStatus, :models do
end
end
+ describe '.retried' do
+ subject { described_class.retried.order(:id) }
+
+ let(:statuses) do
+ [create_status(name: 'aa', ref: 'bb', status: 'running', retried: true),
+ create_status(name: 'cc', ref: 'cc', status: 'pending', retried: true),
+ create_status(name: 'aa', ref: 'cc', status: 'success', retried: true),
+ create_status(name: 'cc', ref: 'bb', status: 'success'),
+ create_status(name: 'aa', ref: 'bb', status: 'success')]
+ end
+
+ it 'returns unique statuses' do
+ is_expected.to contain_exactly(*statuses.values_at(0, 1, 2))
+ end
+ end
+
describe '.running_or_pending' do
subject { described_class.running_or_pending.order(:id) }
@@ -181,7 +197,7 @@ describe CommitStatus, :models do
end
it 'returns statuses that are running or pending' do
- is_expected.to eq(statuses.values_at(0, 1))
+ is_expected.to contain_exactly(*statuses.values_at(0, 1))
end
end
diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb
index 212fcd884a8..4bda7d4314a 100644
--- a/spec/models/deployment_spec.rb
+++ b/spec/models/deployment_spec.rb
@@ -52,7 +52,7 @@ describe Deployment, models: true do
describe '#metrics' do
let(:deployment) { create(:deployment) }
- subject { deployment.metrics(1.hour) }
+ subject { deployment.metrics }
context 'metrics are disabled' do
it { is_expected.to eq({}) }
@@ -63,16 +63,17 @@ describe Deployment, models: true do
{
success: true,
metrics: {},
- last_update: 42
+ last_update: 42,
+ deployment_time: 1494408956
}
end
before do
- allow(deployment.project).to receive_message_chain(:monitoring_service, :metrics)
+ allow(deployment.project).to receive_message_chain(:monitoring_service, :deployment_metrics)
.with(any_args).and_return(simple_metrics)
end
- it { is_expected.to eq(simple_metrics.merge(deployment_time: deployment.created_at.utc.to_i)) }
+ it { is_expected.to eq(simple_metrics) }
end
end
diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb
index 28e5c3f80f4..12519de8636 100644
--- a/spec/models/environment_spec.rb
+++ b/spec/models/environment_spec.rb
@@ -393,7 +393,7 @@ describe Environment, models: true do
it 'returns the metrics from the deployment service' do
expect(project.monitoring_service)
- .to receive(:metrics).with(environment)
+ .to receive(:environment_metrics).with(environment)
.and_return(:fake_metrics)
is_expected.to eq(:fake_metrics)
@@ -438,7 +438,7 @@ describe Environment, models: true do
"foo**bar" => "foo-bar" + SUFFIX,
"*-foo" => "env-foo" + SUFFIX,
"staging-12345678-" => "staging-12345678" + SUFFIX,
- "staging-12345678-01234567" => "staging-12345678" + SUFFIX,
+ "staging-12345678-01234567" => "staging-12345678" + SUFFIX
}.each do |name, matcher|
it "returns a slug matching #{matcher}, given #{name}" do
slug = described_class.new(name: name).generate_slug
diff --git a/spec/models/global_milestone_spec.rb b/spec/models/global_milestone_spec.rb
index 55b87d1c48a..a14efda3eda 100644
--- a/spec/models/global_milestone_spec.rb
+++ b/spec/models/global_milestone_spec.rb
@@ -137,7 +137,7 @@ describe GlobalMilestone, models: true do
[
milestone1_project1,
milestone1_project2,
- milestone1_project3,
+ milestone1_project3
]
milestones_relation = Milestone.where(id: milestones.map(&:id))
diff --git a/spec/models/hooks/system_hook_spec.rb b/spec/models/hooks/system_hook_spec.rb
index 8acec805584..4340170888d 100644
--- a/spec/models/hooks/system_hook_spec.rb
+++ b/spec/models/hooks/system_hook_spec.rb
@@ -1,6 +1,19 @@
require "spec_helper"
describe SystemHook, models: true do
+ context 'default attributes' do
+ let(:system_hook) { build(:system_hook) }
+
+ it 'sets defined default parameters' do
+ attrs = {
+ push_events: false,
+ repository_update_events: true,
+ enable_ssl_verification: true
+ }
+ expect(system_hook).to have_attributes(attrs)
+ end
+ end
+
describe "execute" do
let(:system_hook) { create(:system_hook) }
let(:user) { create(:user) }
@@ -105,4 +118,12 @@ describe SystemHook, models: true do
).once
end
end
+
+ describe '.repository_update_hooks' do
+ it 'returns hooks for repository update events only' do
+ hook = create(:system_hook, repository_update_events: true)
+ create(:system_hook, repository_update_events: false)
+ expect(SystemHook.repository_update_hooks).to eq([hook])
+ end
+ end
end
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index 725f5c2311f..bb4e70db2e9 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -38,46 +38,6 @@ describe Issue, models: true do
end
end
- describe "before_save" do
- describe "#update_cache_counts when an issue is reassigned" do
- let(:issue) { create(:issue) }
- let(:assignee) { create(:user) }
-
- context "when previous assignee exists" do
- before do
- issue.project.team << [assignee, :developer]
- issue.assignees << assignee
- end
-
- it "updates cache counts for new assignee" do
- user = create(:user)
-
- expect(user).to receive(:update_cache_counts)
-
- issue.assignees << user
- end
-
- it "updates cache counts for previous assignee" do
- issue.assignees.first
-
- expect_any_instance_of(User).to receive(:update_cache_counts)
-
- issue.assignees.destroy_all
- end
- end
-
- context "when previous assignee does not exist" do
- it "updates cache count for the new assignee" do
- issue.assignees = []
-
- expect_any_instance_of(User).to receive(:update_cache_counts)
-
- issue.assignees << assignee
- end
- end
- end
- end
-
describe '#card_attributes' do
it 'includes the author name' do
allow(subject).to receive(:author).and_return(double(name: 'Robert'))
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index ef349530761..ce870fcc1d3 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -87,48 +87,6 @@ describe MergeRequest, models: true do
end
end
- describe "before_save" do
- describe "#update_cache_counts when a merge request is reassigned" do
- let(:project) { create :project }
- let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
- let(:assignee) { create :user }
-
- context "when previous assignee exists" do
- before do
- project.team << [assignee, :developer]
- merge_request.update(assignee: assignee)
- end
-
- it "updates cache counts for new assignee" do
- user = create(:user)
-
- expect(user).to receive(:update_cache_counts)
-
- merge_request.update(assignee: user)
- end
-
- it "updates cache counts for previous assignee" do
- old_assignee = merge_request.assignee
- allow(User).to receive(:find_by_id).with(old_assignee.id).and_return(old_assignee)
-
- expect(old_assignee).to receive(:update_cache_counts)
-
- merge_request.update(assignee: nil)
- end
- end
-
- context "when previous assignee does not exist" do
- it "updates cache count for the new assignee" do
- merge_request.update(assignee: nil)
-
- expect_any_instance_of(User).to receive(:update_cache_counts)
-
- merge_request.update(assignee: assignee)
- end
- end
- end
- end
-
describe '#card_attributes' do
it 'includes the author name' do
allow(subject).to receive(:author).and_return(double(name: 'Robert'))
@@ -1310,71 +1268,6 @@ describe MergeRequest, models: true do
end
end
- describe '#conflicts_can_be_resolved_in_ui?' do
- def create_merge_request(source_branch)
- create(:merge_request, source_branch: source_branch, target_branch: 'conflict-start') do |mr|
- mr.mark_as_unmergeable
- end
- end
-
- it 'returns a falsey value when the MR can be merged without conflicts' do
- merge_request = create_merge_request('master')
- merge_request.mark_as_mergeable
-
- expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_falsey
- end
-
- it 'returns a falsey value when the MR is marked as having conflicts, but has none' do
- merge_request = create_merge_request('master')
-
- expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_falsey
- end
-
- it 'returns a falsey value when the MR has a missing ref after a force push' do
- merge_request = create_merge_request('conflict-resolvable')
- allow(merge_request.conflicts).to receive(:merge_index).and_raise(Rugged::OdbError)
-
- expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_falsey
- end
-
- it 'returns a falsey value when the MR does not support new diff notes' do
- merge_request = create_merge_request('conflict-resolvable')
- merge_request.merge_request_diff.update_attributes(start_commit_sha: nil)
-
- expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_falsey
- end
-
- it 'returns a falsey value when the conflicts contain a large file' do
- merge_request = create_merge_request('conflict-too-large')
-
- expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_falsey
- end
-
- it 'returns a falsey value when the conflicts contain a binary file' do
- merge_request = create_merge_request('conflict-binary-file')
-
- expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_falsey
- end
-
- it 'returns a falsey value when the conflicts contain a file edited in one branch and deleted in another' do
- merge_request = create_merge_request('conflict-missing-side')
-
- expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_falsey
- end
-
- it 'returns a truthy value when the conflicts are resolvable in the UI' do
- merge_request = create_merge_request('conflict-resolvable')
-
- expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_truthy
- end
-
- it 'returns a truthy value when the conflicts have to be resolved in an editor' do
- merge_request = create_merge_request('conflict-contains-conflict-markers')
-
- expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_truthy
- end
- end
-
describe "#source_project_missing?" do
let(:project) { create(:empty_project) }
let(:fork_project) { create(:empty_project, forked_from_project: project) }
diff --git a/spec/models/project_authorization_spec.rb b/spec/models/project_authorization_spec.rb
index 33ef67f97a7..cd0a4a94809 100644
--- a/spec/models/project_authorization_spec.rb
+++ b/spec/models/project_authorization_spec.rb
@@ -16,7 +16,7 @@ describe ProjectAuthorization do
it 'inserts rows in batches' do
described_class.insert_authorizations([
[user.id, project1.id, Gitlab::Access::MASTER],
- [user.id, project2.id, Gitlab::Access::MASTER],
+ [user.id, project2.id, Gitlab::Access::MASTER]
], 1)
expect(user.project_authorizations.count).to eq(2)
diff --git a/spec/models/project_services/asana_service_spec.rb b/spec/models/project_services/asana_service_spec.rb
index 48aef3a93f2..95c35162d96 100644
--- a/spec/models/project_services/asana_service_spec.rb
+++ b/spec/models/project_services/asana_service_spec.rb
@@ -28,7 +28,7 @@ describe AsanaService, models: true do
commits: messages.map do |m|
{
message: m,
- url: 'https://gitlab.com/',
+ url: 'https://gitlab.com/'
}
end
}
diff --git a/spec/models/project_services/chat_message/issue_message_spec.rb b/spec/models/project_services/chat_message/issue_message_spec.rb
index 34e2d94b1ed..c159ab00ab1 100644
--- a/spec/models/project_services/chat_message/issue_message_spec.rb
+++ b/spec/models/project_services/chat_message/issue_message_spec.rb
@@ -48,7 +48,7 @@ describe ChatMessage::IssueMessage, models: true do
title: "#100 Issue title",
title_link: "http://url.com",
text: "issue description",
- color: color,
+ color: color
}
])
end
diff --git a/spec/models/project_services/chat_message/merge_message_spec.rb b/spec/models/project_services/chat_message/merge_message_spec.rb
index fa0a1f4a5b7..61f17031172 100644
--- a/spec/models/project_services/chat_message/merge_message_spec.rb
+++ b/spec/models/project_services/chat_message/merge_message_spec.rb
@@ -22,7 +22,7 @@ describe ChatMessage::MergeMessage, models: true do
state: 'opened',
description: 'merge request description',
source_branch: 'source_branch',
- target_branch: 'target_branch',
+ target_branch: 'target_branch'
}
}
end
diff --git a/spec/models/project_services/chat_message/note_message_spec.rb b/spec/models/project_services/chat_message/note_message_spec.rb
index 7cd9c61ee2b..7996536218a 100644
--- a/spec/models/project_services/chat_message/note_message_spec.rb
+++ b/spec/models/project_services/chat_message/note_message_spec.rb
@@ -15,7 +15,7 @@ describe ChatMessage::NoteMessage, models: true do
project_url: 'http://somewhere.com',
repository: {
name: 'project_name',
- url: 'http://somewhere.com',
+ url: 'http://somewhere.com'
},
object_attributes: {
id: 10,
diff --git a/spec/models/project_services/chat_message/pipeline_message_spec.rb b/spec/models/project_services/chat_message/pipeline_message_spec.rb
index e005be42b0d..7d2599dc703 100644
--- a/spec/models/project_services/chat_message/pipeline_message_spec.rb
+++ b/spec/models/project_services/chat_message/pipeline_message_spec.rb
@@ -62,7 +62,7 @@ describe ChatMessage::PipelineMessage do
def build_message(status_text = status, name = user[:name])
"<http://example.gitlab.com|project_name>:" \
" Pipeline <http://example.gitlab.com/pipelines/123|#123>" \
- " of <http://example.gitlab.com/commits/develop|develop> branch" \
+ " of branch `<http://example.gitlab.com/commits/develop|develop>`" \
" by #{name} #{status_text} in 02:00:10"
end
end
@@ -81,7 +81,7 @@ describe ChatMessage::PipelineMessage do
expect(subject.pretext).to be_empty
expect(subject.attachments).to eq(message)
expect(subject.activity).to eq({
- title: 'Pipeline [#123](http://example.gitlab.com/pipelines/123) of [develop](http://example.gitlab.com/commits/develop) branch by hacker passed',
+ title: 'Pipeline [#123](http://example.gitlab.com/pipelines/123) of branch `[develop](http://example.gitlab.com/commits/develop)` by hacker passed',
subtitle: 'in [project_name](http://example.gitlab.com)',
text: 'in 02:00:10',
image: ''
@@ -98,7 +98,7 @@ describe ChatMessage::PipelineMessage do
expect(subject.pretext).to be_empty
expect(subject.attachments).to eq(message)
expect(subject.activity).to eq({
- title: 'Pipeline [#123](http://example.gitlab.com/pipelines/123) of [develop](http://example.gitlab.com/commits/develop) branch by hacker failed',
+ title: 'Pipeline [#123](http://example.gitlab.com/pipelines/123) of branch `[develop](http://example.gitlab.com/commits/develop)` by hacker failed',
subtitle: 'in [project_name](http://example.gitlab.com)',
text: 'in 02:00:10',
image: ''
@@ -113,7 +113,7 @@ describe ChatMessage::PipelineMessage do
expect(subject.pretext).to be_empty
expect(subject.attachments).to eq(message)
expect(subject.activity).to eq({
- title: 'Pipeline [#123](http://example.gitlab.com/pipelines/123) of [develop](http://example.gitlab.com/commits/develop) branch by API failed',
+ title: 'Pipeline [#123](http://example.gitlab.com/pipelines/123) of branch `[develop](http://example.gitlab.com/commits/develop)` by API failed',
subtitle: 'in [project_name](http://example.gitlab.com)',
text: 'in 02:00:10',
image: ''
@@ -125,8 +125,8 @@ describe ChatMessage::PipelineMessage do
def build_markdown_message(status_text = status, name = user[:name])
"[project_name](http://example.gitlab.com):" \
" Pipeline [#123](http://example.gitlab.com/pipelines/123)" \
- " of [develop](http://example.gitlab.com/commits/develop)" \
- " branch by #{name} #{status_text} in 02:00:10"
+ " of branch `[develop](http://example.gitlab.com/commits/develop)`" \
+ " by #{name} #{status_text} in 02:00:10"
end
end
end
diff --git a/spec/models/project_services/chat_message/push_message_spec.rb b/spec/models/project_services/chat_message/push_message_spec.rb
index 63eb078c44e..e38117b75f6 100644
--- a/spec/models/project_services/chat_message/push_message_spec.rb
+++ b/spec/models/project_services/chat_message/push_message_spec.rb
@@ -21,19 +21,19 @@ describe ChatMessage::PushMessage, models: true do
before do
args[:commits] = [
{ message: 'message1', url: 'http://url1.com', id: 'abcdefghijkl', author: { name: 'author1' } },
- { message: 'message2', url: 'http://url2.com', id: '123456789012', author: { name: 'author2' } },
+ { message: 'message2', url: 'http://url2.com', id: '123456789012', author: { name: 'author2' } }
]
end
context 'without markdown' do
it 'returns a message regarding pushes' do
expect(subject.pretext).to eq(
- 'test.user pushed to branch <http://url.com/commits/master|master> of '\
+ 'test.user pushed to branch `<http://url.com/commits/master|master>` of '\
'<http://url.com|project_name> (<http://url.com/compare/before...after|Compare changes>)')
expect(subject.attachments).to eq([{
text: "<http://url1.com|abcdefgh>: message1 - author1\n\n"\
"<http://url2.com|12345678>: message2 - author2",
- color: color,
+ color: color
}])
end
end
@@ -45,7 +45,7 @@ describe ChatMessage::PushMessage, models: true do
it 'returns a message regarding pushes' do
expect(subject.pretext).to eq(
- 'test.user pushed to branch [master](http://url.com/commits/master) of [project_name](http://url.com) ([Compare changes](http://url.com/compare/before...after))')
+ 'test.user pushed to branch `[master](http://url.com/commits/master)` of [project_name](http://url.com) ([Compare changes](http://url.com/compare/before...after))')
expect(subject.attachments).to eq(
"[abcdefgh](http://url1.com): message1 - author1\n\n[12345678](http://url2.com): message2 - author2")
expect(subject.activity).to eq({
@@ -74,7 +74,7 @@ describe ChatMessage::PushMessage, models: true do
context 'without markdown' do
it 'returns a message regarding pushes' do
expect(subject.pretext).to eq('test.user pushed new tag ' \
- '<http://url.com/commits/new_tag|new_tag> to ' \
+ '`<http://url.com/commits/new_tag|new_tag>` to ' \
'<http://url.com|project_name>')
expect(subject.attachments).to be_empty
end
@@ -87,7 +87,7 @@ describe ChatMessage::PushMessage, models: true do
it 'returns a message regarding pushes' do
expect(subject.pretext).to eq(
- 'test.user pushed new tag [new_tag](http://url.com/commits/new_tag) to [project_name](http://url.com)')
+ 'test.user pushed new tag `[new_tag](http://url.com/commits/new_tag)` to [project_name](http://url.com)')
expect(subject.attachments).to be_empty
expect(subject.activity).to eq({
title: 'test.user created tag',
@@ -107,7 +107,7 @@ describe ChatMessage::PushMessage, models: true do
context 'without markdown' do
it 'returns a message regarding a new branch' do
expect(subject.pretext).to eq(
- 'test.user pushed new branch <http://url.com/commits/master|master> to '\
+ 'test.user pushed new branch `<http://url.com/commits/master|master>` to '\
'<http://url.com|project_name>')
expect(subject.attachments).to be_empty
end
@@ -120,7 +120,7 @@ describe ChatMessage::PushMessage, models: true do
it 'returns a message regarding a new branch' do
expect(subject.pretext).to eq(
- 'test.user pushed new branch [master](http://url.com/commits/master) to [project_name](http://url.com)')
+ 'test.user pushed new branch `[master](http://url.com/commits/master)` to [project_name](http://url.com)')
expect(subject.attachments).to be_empty
expect(subject.activity).to eq({
title: 'test.user created branch',
@@ -140,7 +140,7 @@ describe ChatMessage::PushMessage, models: true do
context 'without markdown' do
it 'returns a message regarding a removed branch' do
expect(subject.pretext).to eq(
- 'test.user removed branch master from <http://url.com|project_name>')
+ 'test.user removed branch `master` from <http://url.com|project_name>')
expect(subject.attachments).to be_empty
end
end
@@ -152,7 +152,7 @@ describe ChatMessage::PushMessage, models: true do
it 'returns a message regarding a removed branch' do
expect(subject.pretext).to eq(
- 'test.user removed branch master from [project_name](http://url.com)')
+ 'test.user removed branch `master` from [project_name](http://url.com)')
expect(subject.attachments).to be_empty
expect(subject.activity).to eq({
title: 'test.user removed branch',
diff --git a/spec/models/project_services/chat_message/wiki_page_message_spec.rb b/spec/models/project_services/chat_message/wiki_page_message_spec.rb
index 0df7db2abc2..4ca1b8aa7b7 100644
--- a/spec/models/project_services/chat_message/wiki_page_message_spec.rb
+++ b/spec/models/project_services/chat_message/wiki_page_message_spec.rb
@@ -53,7 +53,7 @@ describe ChatMessage::WikiPageMessage, models: true do
expect(subject.attachments).to eq([
{
text: "Wiki page description",
- color: color,
+ color: color
}
])
end
@@ -66,7 +66,7 @@ describe ChatMessage::WikiPageMessage, models: true do
expect(subject.attachments).to eq([
{
text: "Wiki page description",
- color: color,
+ color: color
}
])
end
diff --git a/spec/models/project_services/kubernetes_service_spec.rb b/spec/models/project_services/kubernetes_service_spec.rb
index e69eb0098dd..c1c2f2a7219 100644
--- a/spec/models/project_services/kubernetes_service_spec.rb
+++ b/spec/models/project_services/kubernetes_service_spec.rb
@@ -54,7 +54,7 @@ describe KubernetesService, models: true, caching: true do
'a' * 63 => true,
'a' * 64 => false,
'a.b' => false,
- 'a*b' => false,
+ 'a*b' => false
}.each do |namespace, validity|
it "validates #{namespace} as #{validity ? 'valid' : 'invalid'}" do
subject.namespace = namespace
@@ -168,7 +168,7 @@ describe KubernetesService, models: true, caching: true do
{ key: 'KUBE_TOKEN', value: 'token', public: false },
{ key: 'KUBE_NAMESPACE', value: 'my-project', public: true },
{ key: 'KUBE_CA_PEM', value: 'CA PEM DATA', public: true },
- { key: 'KUBE_CA_PEM_FILE', value: 'CA PEM DATA', public: true, file: true },
+ { key: 'KUBE_CA_PEM_FILE', value: 'CA PEM DATA', public: true, file: true }
)
end
end
@@ -179,7 +179,7 @@ describe KubernetesService, models: true, caching: true do
{ key: 'KUBE_URL', value: 'https://kube.domain.com', public: true },
{ key: 'KUBE_TOKEN', value: 'token', public: false },
{ key: 'KUBE_CA_PEM', value: 'CA PEM DATA', public: true },
- { key: 'KUBE_CA_PEM_FILE', value: 'CA PEM DATA', public: true, file: true },
+ { key: 'KUBE_CA_PEM_FILE', value: 'CA PEM DATA', public: true, file: true }
)
end
diff --git a/spec/models/project_services/pivotaltracker_service_spec.rb b/spec/models/project_services/pivotaltracker_service_spec.rb
index 45b2f1068bf..a76e909d04d 100644
--- a/spec/models/project_services/pivotaltracker_service_spec.rb
+++ b/spec/models/project_services/pivotaltracker_service_spec.rb
@@ -40,7 +40,7 @@ describe PivotaltrackerService, models: true do
name: 'Some User'
},
url: 'https://example.com/commit',
- message: 'commit message',
+ message: 'commit message'
}
]
}
diff --git a/spec/models/project_services/prometheus_service_spec.rb b/spec/models/project_services/prometheus_service_spec.rb
index 82a3e2698c1..1f9d3c07b51 100644
--- a/spec/models/project_services/prometheus_service_spec.rb
+++ b/spec/models/project_services/prometheus_service_spec.rb
@@ -6,6 +6,7 @@ describe PrometheusService, models: true, caching: true do
let(:project) { create(:prometheus_project) }
let(:service) { project.prometheus_service }
+ let(:environment_query) { Gitlab::Prometheus::Queries::EnvironmentQuery }
describe "Associations" do
it { is_expected.to belong_to :project }
@@ -45,49 +46,56 @@ describe PrometheusService, models: true, caching: true do
end
end
- describe '#metrics' do
+ describe '#environment_metrics' do
let(:environment) { build_stubbed(:environment, slug: 'env-slug') }
around do |example|
Timecop.freeze { example.run }
end
- context 'with valid data without time range' do
- subject { service.metrics(environment) }
+ context 'with valid data' do
+ subject { service.environment_metrics(environment) }
before do
- stub_reactive_cache(service, prometheus_data, 'env-slug', nil, nil)
+ stub_reactive_cache(service, prometheus_data, environment_query, environment.id)
end
it 'returns reactive data' do
is_expected.to eq(prometheus_data)
end
end
+ end
+
+ describe '#deployment_metrics' do
+ let(:deployment) { build_stubbed(:deployment)}
+ let(:deployment_query) { Gitlab::Prometheus::Queries::DeploymentQuery }
+
+ around do |example|
+ Timecop.freeze { example.run }
+ end
- context 'with valid data with time range' do
- let(:t_start) { 1.hour.ago.utc }
- let(:t_end) { Time.now.utc }
- subject { service.metrics(environment, timeframe_start: t_start, timeframe_end: t_end) }
+ context 'with valid data' do
+ subject { service.deployment_metrics(deployment) }
before do
- stub_reactive_cache(service, prometheus_data, 'env-slug', t_start, t_end)
+ stub_reactive_cache(service, prometheus_data, deployment_query, deployment.id)
end
it 'returns reactive data' do
- is_expected.to eq(prometheus_data)
+ is_expected.to eq(prometheus_data.merge(deployment_time: deployment.created_at.to_i))
end
end
end
describe '#calculate_reactive_cache' do
- let(:environment) { build_stubbed(:environment, slug: 'env-slug') }
+ let(:environment) { create(:environment, slug: 'env-slug') }
around do |example|
Timecop.freeze { example.run }
end
subject do
- service.calculate_reactive_cache(environment.slug, nil, nil)
+ service.calculate_reactive_cache(environment_query.to_s, environment.id)
end
context 'when service is inactive' do
diff --git a/spec/models/project_snippet_spec.rb b/spec/models/project_snippet_spec.rb
index d9d7c0b0aaa..5fe4885eeb4 100644
--- a/spec/models/project_snippet_spec.rb
+++ b/spec/models/project_snippet_spec.rb
@@ -5,9 +5,6 @@ describe ProjectSnippet, models: true do
it { is_expected.to belong_to(:project) }
end
- describe "Mass assignment" do
- end
-
describe "Validation" do
it { is_expected.to validate_presence_of(:project) }
end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 28aa44d8458..f2b4e9070b4 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -973,7 +973,7 @@ describe Project, models: true do
before do
storages = {
'default' => { 'path' => 'tmp/tests/repositories' },
- 'picked' => { 'path' => 'tmp/tests/repositories' },
+ 'picked' => { 'path' => 'tmp/tests/repositories' }
}
allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
end
diff --git a/spec/models/project_statistics_spec.rb b/spec/models/project_statistics_spec.rb
index ff29f6f66ba..c5ffbda9821 100644
--- a/spec/models/project_statistics_spec.rb
+++ b/spec/models/project_statistics_spec.rb
@@ -35,7 +35,7 @@ describe ProjectStatistics, models: true do
commit_count: 8.exabytes - 1,
repository_size: 2.exabytes,
lfs_objects_size: 2.exabytes,
- build_artifacts_size: 4.exabytes - 1,
+ build_artifacts_size: 4.exabytes - 1
)
statistics.reload
@@ -149,7 +149,7 @@ describe ProjectStatistics, models: true do
it "sums all storage counters" do
statistics.update!(
repository_size: 2,
- lfs_objects_size: 3,
+ lfs_objects_size: 3
)
statistics.reload
diff --git a/spec/models/protected_branch/merge_access_level_spec.rb b/spec/models/protected_branch/merge_access_level_spec.rb
new file mode 100644
index 00000000000..1e7242e9fa8
--- /dev/null
+++ b/spec/models/protected_branch/merge_access_level_spec.rb
@@ -0,0 +1,5 @@
+require 'spec_helper'
+
+describe ProtectedBranch::MergeAccessLevel, :models do
+ it { is_expected.to validate_inclusion_of(:access_level).in_array([Gitlab::Access::MASTER, Gitlab::Access::DEVELOPER, Gitlab::Access::NO_ACCESS]) }
+end
diff --git a/spec/models/protected_branch/push_access_level_spec.rb b/spec/models/protected_branch/push_access_level_spec.rb
new file mode 100644
index 00000000000..de68351198c
--- /dev/null
+++ b/spec/models/protected_branch/push_access_level_spec.rb
@@ -0,0 +1,5 @@
+require 'spec_helper'
+
+describe ProtectedBranch::PushAccessLevel, :models do
+ it { is_expected.to validate_inclusion_of(:access_level).in_array([Gitlab::Access::MASTER, Gitlab::Access::DEVELOPER, Gitlab::Access::NO_ACCESS]) }
+end
diff --git a/spec/models/protected_branch_spec.rb b/spec/models/protected_branch_spec.rb
index 179a443c43d..ca347cf92c9 100644
--- a/spec/models/protected_branch_spec.rb
+++ b/spec/models/protected_branch_spec.rb
@@ -7,9 +7,6 @@ describe ProtectedBranch, models: true do
it { is_expected.to belong_to(:project) }
end
- describe "Mass assignment" do
- end
-
describe 'Validation' do
it { is_expected.to validate_presence_of(:project) }
it { is_expected.to validate_presence_of(:name) }
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index dd6514b3b50..718b7d5e86b 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe Repository, models: true do
include RepoHelpers
- TestBlob = Struct.new(:name)
+ TestBlob = Struct.new(:path)
let(:project) { create(:project, :repository) }
let(:repository) { project.repository }
@@ -110,22 +110,11 @@ describe Repository, models: true do
end
describe '#ref_name_for_sha' do
- context 'ref found' do
- it 'returns the ref' do
- allow_any_instance_of(Gitlab::Popen).to receive(:popen).
- and_return(["b8d95eb4969eefacb0a58f6a28f6803f8070e7b9 commit\trefs/environments/production/77\n", 0])
+ it 'returns the ref' do
+ allow(repository.raw_repository).to receive(:ref_name_for_sha).
+ and_return('refs/environments/production/77')
- expect(repository.ref_name_for_sha('bla', '0' * 40)).to eq 'refs/environments/production/77'
- end
- end
-
- context 'ref not found' do
- it 'returns nil' do
- allow_any_instance_of(Gitlab::Popen).to receive(:popen).
- and_return(["", 0])
-
- expect(repository.ref_name_for_sha('bla', '0' * 40)).to eq nil
- end
+ expect(repository.ref_name_for_sha('bla', '0' * 40)).to eq 'refs/environments/production/77'
end
end
@@ -565,31 +554,31 @@ describe Repository, models: true do
it 'accepts changelog' do
expect(repository.tree).to receive(:blobs).and_return([TestBlob.new('changelog')])
- expect(repository.changelog.name).to eq('changelog')
+ expect(repository.changelog.path).to eq('changelog')
end
it 'accepts news instead of changelog' do
expect(repository.tree).to receive(:blobs).and_return([TestBlob.new('news')])
- expect(repository.changelog.name).to eq('news')
+ expect(repository.changelog.path).to eq('news')
end
it 'accepts history instead of changelog' do
expect(repository.tree).to receive(:blobs).and_return([TestBlob.new('history')])
- expect(repository.changelog.name).to eq('history')
+ expect(repository.changelog.path).to eq('history')
end
it 'accepts changes instead of changelog' do
expect(repository.tree).to receive(:blobs).and_return([TestBlob.new('changes')])
- expect(repository.changelog.name).to eq('changes')
+ expect(repository.changelog.path).to eq('changes')
end
it 'is case-insensitive' do
expect(repository.tree).to receive(:blobs).and_return([TestBlob.new('CHANGELOG')])
- expect(repository.changelog.name).to eq('CHANGELOG')
+ expect(repository.changelog.path).to eq('CHANGELOG')
end
end
@@ -624,7 +613,7 @@ describe Repository, models: true do
repository.create_file(user, 'LICENSE', 'Copyright!',
message: 'Add LICENSE', branch_name: 'master')
- expect(repository.license_blob.name).to eq('LICENSE')
+ expect(repository.license_blob.path).to eq('LICENSE')
end
%w[LICENSE LICENCE LiCensE LICENSE.md LICENSE.foo COPYING COPYING.md].each do |filename|
@@ -654,7 +643,7 @@ describe Repository, models: true do
expect(repository.license_key).to be_nil
end
- it 'detects license file with no recognizable open-source license content' do
+ it 'returns nil when the content is not recognizable' do
repository.create_file(user, 'LICENSE', 'Copyright!',
message: 'Add LICENSE', branch_name: 'master')
@@ -670,12 +659,45 @@ describe Repository, models: true do
end
end
+ describe '#license' do
+ before do
+ repository.delete_file(user, 'LICENSE',
+ message: 'Remove LICENSE', branch_name: 'master')
+ end
+
+ it 'returns nil when no license is detected' do
+ expect(repository.license).to be_nil
+ end
+
+ it 'returns nil when the repository does not exist' do
+ expect(repository).to receive(:exists?).and_return(false)
+
+ expect(repository.license).to be_nil
+ end
+
+ it 'returns nil when the content is not recognizable' do
+ repository.create_file(user, 'LICENSE', 'Copyright!',
+ message: 'Add LICENSE', branch_name: 'master')
+
+ expect(repository.license).to be_nil
+ end
+
+ it 'returns the license' do
+ license = Licensee::License.new('mit')
+ repository.create_file(user, 'LICENSE',
+ license.content,
+ message: 'Add LICENSE', branch_name: 'master')
+
+ expect(repository.license).to eq(license)
+ end
+ end
+
describe "#gitlab_ci_yml", caching: true do
it 'returns valid file' do
files = [TestBlob.new('file'), TestBlob.new('.gitlab-ci.yml'), TestBlob.new('copying')]
expect(repository.tree).to receive(:blobs).and_return(files)
- expect(repository.gitlab_ci_yml.name).to eq('.gitlab-ci.yml')
+ expect(repository.gitlab_ci_yml.path).to eq('.gitlab-ci.yml')
end
it 'returns nil if not exists' do
@@ -1626,15 +1648,25 @@ describe Repository, models: true do
describe '#readme', caching: true do
context 'with a non-existing repository' do
it 'returns nil' do
- expect(repository).to receive(:tree).with(:head).and_return(nil)
+ allow(repository).to receive(:tree).with(:head).and_return(nil)
expect(repository.readme).to be_nil
end
end
context 'with an existing repository' do
- it 'returns the README' do
- expect(repository.readme).to be_an_instance_of(Gitlab::Git::Blob)
+ context 'when no README exists' do
+ it 'returns nil' do
+ allow_any_instance_of(Tree).to receive(:readme).and_return(nil)
+
+ expect(repository.readme).to be_nil
+ end
+ end
+
+ context 'when a README exists' do
+ it 'returns the README' do
+ expect(repository.readme).to be_an_instance_of(ReadmeBlob)
+ end
end
end
end
@@ -1825,11 +1857,12 @@ describe Repository, models: true do
describe '#refresh_method_caches' do
it 'refreshes the caches of the given types' do
expect(repository).to receive(:expire_method_caches).
- with(%i(rendered_readme license_blob license_key))
+ with(%i(rendered_readme license_blob license_key license))
expect(repository).to receive(:rendered_readme)
expect(repository).to receive(:license_blob)
expect(repository).to receive(:license_key)
+ expect(repository).to receive(:license)
repository.refresh_method_caches(%i(readme license))
end
@@ -1873,12 +1906,18 @@ describe Repository, models: true do
describe '#is_ancestor?' do
context 'Gitaly is_ancestor feature enabled' do
- it "asks Gitaly server if it's an ancestor" do
- commit = repository.commit
- expect(repository.raw_repository).to receive(:is_ancestor?).and_call_original
+ let(:commit) { repository.commit }
+ let(:ancestor) { commit.parents.first }
+
+ before do
+ allow(Gitlab::GitalyClient).to receive(:enabled?).and_return(true)
allow(Gitlab::GitalyClient).to receive(:feature_enabled?).with(:is_ancestor).and_return(true)
+ end
+
+ it "asks Gitaly server if it's an ancestor" do
+ expect_any_instance_of(Gitlab::GitalyClient::Commit).to receive(:is_ancestor).with(ancestor.id, commit.id)
- expect(repository.is_ancestor?(commit.id, commit.id)).to be true
+ repository.is_ancestor?(ancestor.id, commit.id)
end
end
end
diff --git a/spec/models/snippet_spec.rb b/spec/models/snippet_spec.rb
index 75b1fc7e216..1e5c96fe593 100644
--- a/spec/models/snippet_spec.rb
+++ b/spec/models/snippet_spec.rb
@@ -131,46 +131,6 @@ describe Snippet, models: true do
end
end
- describe '.accessible_to' do
- let(:author) { create(:author) }
- let(:project) { create(:empty_project) }
-
- let!(:public_snippet) { create(:snippet, :public) }
- let!(:internal_snippet) { create(:snippet, :internal) }
- let!(:private_snippet) { create(:snippet, :private, author: author) }
-
- let!(:project_public_snippet) { create(:snippet, :public, project: project) }
- let!(:project_internal_snippet) { create(:snippet, :internal, project: project) }
- let!(:project_private_snippet) { create(:snippet, :private, project: project) }
-
- it 'returns only public snippets when user is blank' do
- expect(described_class.accessible_to(nil)).to match_array [public_snippet, project_public_snippet]
- end
-
- it 'returns only public, and internal snippets for regular users' do
- user = create(:user)
-
- expect(described_class.accessible_to(user)).to match_array [public_snippet, internal_snippet, project_public_snippet, project_internal_snippet]
- end
-
- it 'returns public, internal snippets and project private snippets for project members' do
- member = create(:user)
- project.team << [member, :developer]
-
- expect(described_class.accessible_to(member)).to match_array [public_snippet, internal_snippet, project_public_snippet, project_internal_snippet, project_private_snippet]
- end
-
- it 'returns private snippets where the user is the author' do
- expect(described_class.accessible_to(author)).to match_array [public_snippet, internal_snippet, private_snippet, project_public_snippet, project_internal_snippet]
- end
-
- it 'returns all snippets when for admins' do
- admin = create(:admin)
-
- expect(described_class.accessible_to(admin)).to match_array [public_snippet, internal_snippet, private_snippet, project_public_snippet, project_internal_snippet, project_private_snippet]
- end
- end
-
describe '#participants' do
let(:project) { create(:empty_project, :public) }
let(:snippet) { create(:snippet, content: 'foo', project: project) }
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index b845e85b295..6a15830a15c 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -676,7 +676,7 @@ describe User, models: true do
protocol_and_expectation = {
'http' => false,
'ssh' => true,
- '' => true,
+ '' => true
}
protocol_and_expectation.each do |protocol, expected|
@@ -929,10 +929,20 @@ describe User, models: true do
end
context 'with a group route matching the given path' do
- let!(:group) { create(:group, path: 'group_path') }
+ context 'when the group namespace has an owner_id (legacy data)' do
+ let!(:group) { create(:group, path: 'group_path', owner: user) }
- it 'returns nil' do
- expect(User.find_by_full_path('group_path')).to eq(nil)
+ it 'returns nil' do
+ expect(User.find_by_full_path('group_path')).to eq(nil)
+ end
+ end
+
+ context 'when the group namespace does not have an owner_id' do
+ let!(:group) { create(:group, path: 'group_path') }
+
+ it 'returns nil' do
+ expect(User.find_by_full_path('group_path')).to eq(nil)
+ end
end
end
end
@@ -1777,4 +1787,32 @@ describe User, models: true do
expect(user.preferred_language).to eq('en')
end
end
+
+ context '#invalidate_issue_cache_counts' do
+ let(:user) { build_stubbed(:user) }
+
+ it 'invalidates cache for issue counter' do
+ cache_mock = double
+
+ expect(cache_mock).to receive(:delete).with(['users', user.id, 'assigned_open_issues_count'])
+
+ allow(Rails).to receive(:cache).and_return(cache_mock)
+
+ user.invalidate_issue_cache_counts
+ end
+ end
+
+ context '#invalidate_merge_request_cache_counts' do
+ let(:user) { build_stubbed(:user) }
+
+ it 'invalidates cache for Merge Request counter' do
+ cache_mock = double
+
+ expect(cache_mock).to receive(:delete).with(['users', user.id, 'assigned_open_merge_requests_count'])
+
+ allow(Rails).to receive(:cache).and_return(cache_mock)
+
+ user.invalidate_merge_request_cache_counts
+ end
+ end
end
diff --git a/spec/policies/project_snippet_policy_spec.rb b/spec/policies/project_snippet_policy_spec.rb
index d0758af57dd..e1771b636b8 100644
--- a/spec/policies/project_snippet_policy_spec.rb
+++ b/spec/policies/project_snippet_policy_spec.rb
@@ -1,7 +1,9 @@
require 'spec_helper'
describe ProjectSnippetPolicy, models: true do
- let(:current_user) { create(:user) }
+ let(:regular_user) { create(:user) }
+ let(:external_user) { create(:user, :external) }
+ let(:project) { create(:empty_project) }
let(:author_permissions) do
[
@@ -10,13 +12,15 @@ describe ProjectSnippetPolicy, models: true do
]
end
- subject { described_class.abilities(current_user, project_snippet).to_set }
+ def abilities(user, snippet_visibility)
+ snippet = create(:project_snippet, snippet_visibility, project: project)
- context 'public snippet' do
- let(:project_snippet) { create(:project_snippet, :public) }
+ described_class.abilities(user, snippet).to_set
+ end
+ context 'public snippet' do
context 'no user' do
- let(:current_user) { nil }
+ subject { abilities(nil, :public) }
it do
is_expected.to include(:read_project_snippet)
@@ -25,6 +29,17 @@ describe ProjectSnippetPolicy, models: true do
end
context 'regular user' do
+ subject { abilities(regular_user, :public) }
+
+ it do
+ is_expected.to include(:read_project_snippet)
+ is_expected.not_to include(*author_permissions)
+ end
+ end
+
+ context 'external user' do
+ subject { abilities(external_user, :public) }
+
it do
is_expected.to include(:read_project_snippet)
is_expected.not_to include(*author_permissions)
@@ -33,10 +48,8 @@ describe ProjectSnippetPolicy, models: true do
end
context 'internal snippet' do
- let(:project_snippet) { create(:project_snippet, :internal) }
-
context 'no user' do
- let(:current_user) { nil }
+ subject { abilities(nil, :internal) }
it do
is_expected.not_to include(:read_project_snippet)
@@ -45,6 +58,28 @@ describe ProjectSnippetPolicy, models: true do
end
context 'regular user' do
+ subject { abilities(regular_user, :internal) }
+
+ it do
+ is_expected.to include(:read_project_snippet)
+ is_expected.not_to include(*author_permissions)
+ end
+ end
+
+ context 'external user' do
+ subject { abilities(external_user, :internal) }
+
+ it do
+ is_expected.not_to include(:read_project_snippet)
+ is_expected.not_to include(*author_permissions)
+ end
+ end
+
+ context 'project team member external user' do
+ subject { abilities(external_user, :internal) }
+
+ before { project.team << [external_user, :developer] }
+
it do
is_expected.to include(:read_project_snippet)
is_expected.not_to include(*author_permissions)
@@ -53,10 +88,8 @@ describe ProjectSnippetPolicy, models: true do
end
context 'private snippet' do
- let(:project_snippet) { create(:project_snippet, :private) }
-
context 'no user' do
- let(:current_user) { nil }
+ subject { abilities(nil, :private) }
it do
is_expected.not_to include(:read_project_snippet)
@@ -65,6 +98,8 @@ describe ProjectSnippetPolicy, models: true do
end
context 'regular user' do
+ subject { abilities(regular_user, :private) }
+
it do
is_expected.not_to include(:read_project_snippet)
is_expected.not_to include(*author_permissions)
@@ -72,7 +107,9 @@ describe ProjectSnippetPolicy, models: true do
end
context 'snippet author' do
- let(:project_snippet) { create(:project_snippet, :private, author: current_user) }
+ let(:snippet) { create(:project_snippet, :private, author: regular_user) }
+
+ subject { described_class.abilities(regular_user, snippet).to_set }
it do
is_expected.to include(:read_project_snippet)
@@ -80,8 +117,21 @@ describe ProjectSnippetPolicy, models: true do
end
end
- context 'project team member' do
- before { project_snippet.project.team << [current_user, :developer] }
+ context 'project team member normal user' do
+ subject { abilities(regular_user, :private) }
+
+ before { project.team << [regular_user, :developer] }
+
+ it do
+ is_expected.to include(:read_project_snippet)
+ is_expected.not_to include(*author_permissions)
+ end
+ end
+
+ context 'project team member external user' do
+ subject { abilities(external_user, :private) }
+
+ before { project.team << [external_user, :developer] }
it do
is_expected.to include(:read_project_snippet)
@@ -90,7 +140,7 @@ describe ProjectSnippetPolicy, models: true do
end
context 'admin user' do
- let(:current_user) { create(:admin) }
+ subject { abilities(create(:admin), :private) }
it do
is_expected.to include(:read_project_snippet)
diff --git a/spec/presenters/merge_request_presenter_spec.rb b/spec/presenters/merge_request_presenter_spec.rb
index e599ddaf943..44720fc4448 100644
--- a/spec/presenters/merge_request_presenter_spec.rb
+++ b/spec/presenters/merge_request_presenter_spec.rb
@@ -73,12 +73,12 @@ describe MergeRequestPresenter do
describe '#conflict_resolution_path' do
let(:project) { create :empty_project }
let(:user) { create :user }
- let(:path) { described_class.new(resource, current_user: user).conflict_resolution_path }
+ let(:presenter) { described_class.new(resource, current_user: user) }
+ let(:path) { presenter.conflict_resolution_path }
context 'when MR cannot be resolved in UI' do
it 'does not return conflict resolution path' do
- allow(resource).to receive(:conflicts_can_be_resolved_in_ui?) { true }
- allow(resource).to receive(:conflicts_can_be_resolved_by?).with(user) { false }
+ allow(presenter).to receive_message_chain(:conflicts, :can_be_resolved_in_ui?) { false }
expect(path).to be_nil
end
@@ -86,8 +86,8 @@ describe MergeRequestPresenter do
context 'when conflicts cannot be resolved by user' do
it 'does not return conflict resolution path' do
- allow(resource).to receive(:conflicts_can_be_resolved_in_ui?) { false }
- allow(resource).to receive(:conflicts_can_be_resolved_by?).with(user) { true }
+ allow(presenter).to receive_message_chain(:conflicts, :can_be_resolved_in_ui?) { true }
+ allow(presenter).to receive_message_chain(:conflicts, :can_be_resolved_by?).with(user) { false }
expect(path).to be_nil
end
@@ -95,8 +95,8 @@ describe MergeRequestPresenter do
context 'when able to access conflict resolution UI' do
it 'does return conflict resolution path' do
- allow(resource).to receive(:conflicts_can_be_resolved_in_ui?) { true }
- allow(resource).to receive(:conflicts_can_be_resolved_by?).with(user) { true }
+ allow(presenter).to receive_message_chain(:conflicts, :can_be_resolved_in_ui?) { true }
+ allow(presenter).to receive_message_chain(:conflicts, :can_be_resolved_by?).with(user) { true }
expect(path)
.to eq("/#{project.full_path}/merge_requests/#{resource.iid}/conflicts")
diff --git a/spec/requests/api/commit_statuses_spec.rb b/spec/requests/api/commit_statuses_spec.rb
index 1233cdc64c4..1c163cee152 100644
--- a/spec/requests/api/commit_statuses_spec.rb
+++ b/spec/requests/api/commit_statuses_spec.rb
@@ -26,8 +26,8 @@ describe API::CommitStatuses do
create(:commit_status, { pipeline: commit, ref: commit.ref }.merge(opts))
end
- let!(:status1) { create_status(master, status: 'running') }
- let!(:status2) { create_status(master, name: 'coverage', status: 'pending') }
+ let!(:status1) { create_status(master, status: 'running', retried: true) }
+ let!(:status2) { create_status(master, name: 'coverage', status: 'pending', retried: true) }
let!(:status3) { create_status(develop, status: 'running', allow_failure: true) }
let!(:status4) { create_status(master, name: 'coverage', status: 'success') }
let!(:status5) { create_status(develop, name: 'coverage', status: 'success') }
diff --git a/spec/requests/api/files_spec.rb b/spec/requests/api/files_spec.rb
index fa28047d49c..deb2cac6869 100644
--- a/spec/requests/api/files_spec.rb
+++ b/spec/requests/api/files_spec.rb
@@ -329,7 +329,7 @@ describe API::Files do
end
let(:get_params) do
{
- ref: 'master',
+ ref: 'master'
}
end
diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb
index ed93a8815d3..90b36374ded 100644
--- a/spec/requests/api/groups_spec.rb
+++ b/spec/requests/api/groups_spec.rb
@@ -73,7 +73,7 @@ describe API::Groups do
storage_size: 702,
repository_size: 123,
lfs_objects_size: 234,
- build_artifacts_size: 345,
+ build_artifacts_size: 345
}.stringify_keys
exposed_attributes = attributes.dup
exposed_attributes['job_artifacts_size'] = exposed_attributes.delete('build_artifacts_size')
diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb
index da2b56c040b..79cac721202 100644
--- a/spec/requests/api/issues_spec.rb
+++ b/spec/requests/api/issues_spec.rb
@@ -1124,7 +1124,7 @@ describe API::Issues do
end
context 'CE restrictions' do
- it 'updates an issue with several assignee but only one has been applied' do
+ it 'updates an issue with several assignees but only one has been applied' do
put api("/projects/#{project.id}/issues/#{issue.iid}", user),
assignee_ids: [user2.id, guest.id]
diff --git a/spec/requests/api/project_hooks_spec.rb b/spec/requests/api/project_hooks_spec.rb
index aee0e17a153..0f9330b062d 100644
--- a/spec/requests/api/project_hooks_spec.rb
+++ b/spec/requests/api/project_hooks_spec.rb
@@ -60,7 +60,7 @@ describe API::ProjectHooks, 'ProjectHooks' do
expect(json_response['merge_requests_events']).to eq(hook.merge_requests_events)
expect(json_response['tag_push_events']).to eq(hook.tag_push_events)
expect(json_response['note_events']).to eq(hook.note_events)
- expect(json_response['job_events']).to eq(hook.build_events)
+ expect(json_response['job_events']).to eq(hook.job_events)
expect(json_response['pipeline_events']).to eq(hook.pipeline_events)
expect(json_response['wiki_page_events']).to eq(hook.wiki_page_events)
expect(json_response['enable_ssl_verification']).to eq(hook.enable_ssl_verification)
@@ -148,7 +148,7 @@ describe API::ProjectHooks, 'ProjectHooks' do
expect(json_response['merge_requests_events']).to eq(hook.merge_requests_events)
expect(json_response['tag_push_events']).to eq(hook.tag_push_events)
expect(json_response['note_events']).to eq(hook.note_events)
- expect(json_response['job_events']).to eq(hook.build_events)
+ expect(json_response['job_events']).to eq(hook.job_events)
expect(json_response['pipeline_events']).to eq(hook.pipeline_events)
expect(json_response['wiki_page_events']).to eq(hook.wiki_page_events)
expect(json_response['enable_ssl_verification']).to eq(hook.enable_ssl_verification)
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index ab70ce5cd2f..d5c3b5b34ad 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -661,7 +661,7 @@ describe API::Projects do
'name' => user.namespace.name,
'path' => user.namespace.path,
'kind' => user.namespace.kind,
- 'full_path' => user.namespace.full_path,
+ 'full_path' => user.namespace.full_path
})
end
diff --git a/spec/requests/api/system_hooks_spec.rb b/spec/requests/api/system_hooks_spec.rb
index c7b84173570..2eb191d6049 100644
--- a/spec/requests/api/system_hooks_spec.rb
+++ b/spec/requests/api/system_hooks_spec.rb
@@ -32,8 +32,9 @@ describe API::SystemHooks do
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
+ expect(json_response.first['push_events']).to be false
expect(json_response.first['tag_push_events']).to be false
+ expect(json_response.first['repository_update_events']).to be true
end
end
end
diff --git a/spec/requests/api/v3/files_spec.rb b/spec/requests/api/v3/files_spec.rb
index 5bcbb441979..378ca1720ff 100644
--- a/spec/requests/api/v3/files_spec.rb
+++ b/spec/requests/api/v3/files_spec.rb
@@ -53,7 +53,7 @@ describe API::V3::Files do
let(:params) do
{
file_path: 'app/models/application.rb',
- ref: 'master',
+ ref: 'master'
}
end
@@ -263,7 +263,7 @@ describe API::V3::Files do
let(:get_params) do
{
file_path: file_path,
- ref: 'master',
+ ref: 'master'
}
end
diff --git a/spec/requests/api/v3/groups_spec.rb b/spec/requests/api/v3/groups_spec.rb
index 065cb7ecfe4..bc261b5e07c 100644
--- a/spec/requests/api/v3/groups_spec.rb
+++ b/spec/requests/api/v3/groups_spec.rb
@@ -69,7 +69,7 @@ describe API::V3::Groups do
storage_size: 702,
repository_size: 123,
lfs_objects_size: 234,
- build_artifacts_size: 345,
+ build_artifacts_size: 345
}.stringify_keys
project1.statistics.update!(attributes)
diff --git a/spec/requests/api/v3/project_hooks_spec.rb b/spec/requests/api/v3/project_hooks_spec.rb
index a3a4c77d09d..1969d1c7f2b 100644
--- a/spec/requests/api/v3/project_hooks_spec.rb
+++ b/spec/requests/api/v3/project_hooks_spec.rb
@@ -58,7 +58,7 @@ describe API::ProjectHooks, 'ProjectHooks' do
expect(json_response['merge_requests_events']).to eq(hook.merge_requests_events)
expect(json_response['tag_push_events']).to eq(hook.tag_push_events)
expect(json_response['note_events']).to eq(hook.note_events)
- expect(json_response['build_events']).to eq(hook.build_events)
+ expect(json_response['build_events']).to eq(hook.job_events)
expect(json_response['pipeline_events']).to eq(hook.pipeline_events)
expect(json_response['wiki_page_events']).to eq(hook.wiki_page_events)
expect(json_response['enable_ssl_verification']).to eq(hook.enable_ssl_verification)
@@ -143,7 +143,7 @@ describe API::ProjectHooks, 'ProjectHooks' do
expect(json_response['merge_requests_events']).to eq(hook.merge_requests_events)
expect(json_response['tag_push_events']).to eq(hook.tag_push_events)
expect(json_response['note_events']).to eq(hook.note_events)
- expect(json_response['build_events']).to eq(hook.build_events)
+ expect(json_response['build_events']).to eq(hook.job_events)
expect(json_response['pipeline_events']).to eq(hook.pipeline_events)
expect(json_response['wiki_page_events']).to eq(hook.wiki_page_events)
expect(json_response['enable_ssl_verification']).to eq(hook.enable_ssl_verification)
diff --git a/spec/requests/api/v3/projects_spec.rb b/spec/requests/api/v3/projects_spec.rb
index e15b90d7a9e..dc7c3d125b1 100644
--- a/spec/requests/api/v3/projects_spec.rb
+++ b/spec/requests/api/v3/projects_spec.rb
@@ -227,7 +227,7 @@ describe API::V3::Projects do
storage_size: 702,
repository_size: 123,
lfs_objects_size: 234,
- build_artifacts_size: 345,
+ build_artifacts_size: 345
}
project4.statistics.update!(attributes)
@@ -706,7 +706,7 @@ describe API::V3::Projects do
'name' => user.namespace.name,
'path' => user.namespace.path,
'kind' => user.namespace.kind,
- 'full_path' => user.namespace.full_path,
+ 'full_path' => user.namespace.full_path
})
end
diff --git a/spec/requests/api/v3/system_hooks_spec.rb b/spec/requests/api/v3/system_hooks_spec.rb
index 72c7d14b8ba..ae427541abb 100644
--- a/spec/requests/api/v3/system_hooks_spec.rb
+++ b/spec/requests/api/v3/system_hooks_spec.rb
@@ -31,8 +31,9 @@ describe API::V3::SystemHooks do
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['push_events']).to be false
expect(json_response.first['tag_push_events']).to be false
+ expect(json_response.first['repository_update_events']).to be true
end
end
end
diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb
index 108f73bb965..286de277ae7 100644
--- a/spec/requests/ci/api/builds_spec.rb
+++ b/spec/requests/ci/api/builds_spec.rb
@@ -185,7 +185,7 @@ describe Ci::API::Builds do
{ "key" => "CI_PIPELINE_TRIGGERED", "value" => "true", "public" => true },
{ "key" => "DB_NAME", "value" => "postgres", "public" => true },
{ "key" => "SECRET_KEY", "value" => "secret_value", "public" => false },
- { "key" => "TRIGGER_KEY_1", "value" => "TRIGGER_VALUE_1", "public" => false },
+ { "key" => "TRIGGER_KEY_1", "value" => "TRIGGER_VALUE_1", "public" => false }
)
end
end
diff --git a/spec/requests/lfs_http_spec.rb b/spec/requests/lfs_http_spec.rb
index 5d495bc9e7d..0c9b4121adf 100644
--- a/spec/requests/lfs_http_spec.rb
+++ b/spec/requests/lfs_http_spec.rb
@@ -425,7 +425,7 @@ describe 'Git LFS API and storage' do
'size' => sample_size,
'error' => {
'code' => 404,
- 'message' => "Object does not exist on the server or you don't have permissions to access it",
+ 'message' => "Object does not exist on the server or you don't have permissions to access it"
}
}
]
@@ -456,7 +456,7 @@ describe 'Git LFS API and storage' do
'size' => 1575078,
'error' => {
'code' => 404,
- 'message' => "Object does not exist on the server or you don't have permissions to access it",
+ 'message' => "Object does not exist on the server or you don't have permissions to access it"
}
}
]
@@ -493,7 +493,7 @@ describe 'Git LFS API and storage' do
'size' => 1575078,
'error' => {
'code' => 404,
- 'message' => "Object does not exist on the server or you don't have permissions to access it",
+ 'message' => "Object does not exist on the server or you don't have permissions to access it"
}
},
{
diff --git a/spec/requests/openid_connect_spec.rb b/spec/requests/openid_connect_spec.rb
index fbb69bc0920..05176c3beaa 100644
--- a/spec/requests/openid_connect_spec.rb
+++ b/spec/requests/openid_connect_spec.rb
@@ -61,7 +61,7 @@ describe 'OpenID Connect requests' do
email: private_email.email,
public_email: public_email.email,
website_url: 'https://example.com',
- avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png"),
+ avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png")
)
end
@@ -79,7 +79,7 @@ describe 'OpenID Connect requests' do
'email_verified' => true,
'website' => 'https://example.com',
'profile' => 'http://localhost/alice',
- 'picture' => "http://localhost/uploads/user/avatar/#{user.id}/dk.png",
+ 'picture' => "http://localhost/uploads/user/avatar/#{user.id}/dk.png"
})
end
end
diff --git a/spec/routing/routing_spec.rb b/spec/routing/routing_spec.rb
index 9f6defe1450..abacc50a371 100644
--- a/spec/routing/routing_spec.rb
+++ b/spec/routing/routing_spec.rb
@@ -249,17 +249,34 @@ describe RootController, 'routing' do
end
end
-# new_user_session GET /users/sign_in(.:format) devise/sessions#new
-# user_session POST /users/sign_in(.:format) devise/sessions#create
-# destroy_user_session DELETE /users/sign_out(.:format) devise/sessions#destroy
-# user_omniauth_authorize /users/auth/:provider(.:format) omniauth_callbacks#passthru
-# user_omniauth_callback /users/auth/:action/callback(.:format) omniauth_callbacks#(?-mix:(?!))
-# user_password POST /users/password(.:format) devise/passwords#create
-# new_user_password GET /users/password/new(.:format) devise/passwords#new
-# edit_user_password GET /users/password/edit(.:format) devise/passwords#edit
-# PUT /users/password(.:format) devise/passwords#update
describe "Authentication", "routing" do
- # pending
+ it "GET /users/sign_in" do
+ expect(get("/users/sign_in")).to route_to('sessions#new')
+ end
+
+ it "POST /users/sign_in" do
+ expect(post("/users/sign_in")).to route_to('sessions#create')
+ end
+
+ it "DELETE /users/sign_out" do
+ expect(delete("/users/sign_out")).to route_to('sessions#destroy')
+ end
+
+ it "POST /users/password" do
+ expect(post("/users/password")).to route_to('passwords#create')
+ end
+
+ it "GET /users/password/new" do
+ expect(get("/users/password/new")).to route_to('passwords#new')
+ end
+
+ it "GET /users/password/edit" do
+ expect(get("/users/password/edit")).to route_to('passwords#edit')
+ end
+
+ it "PUT /users/password" do
+ expect(put("/users/password")).to route_to('passwords#update')
+ end
end
describe "Groups", "routing" do
diff --git a/spec/serializers/analytics_issue_entity_spec.rb b/spec/serializers/analytics_issue_entity_spec.rb
index 68086216ba9..75d606d5eb3 100644
--- a/spec/serializers/analytics_issue_entity_spec.rb
+++ b/spec/serializers/analytics_issue_entity_spec.rb
@@ -9,7 +9,7 @@ describe AnalyticsIssueEntity do
iid: "1",
id: "1",
created_at: "2016-11-12 15:04:02.948604",
- author: user,
+ author: user
}
end
diff --git a/spec/serializers/analytics_issue_serializer_spec.rb b/spec/serializers/analytics_issue_serializer_spec.rb
index ba24cf8e481..7c14c198a74 100644
--- a/spec/serializers/analytics_issue_serializer_spec.rb
+++ b/spec/serializers/analytics_issue_serializer_spec.rb
@@ -16,7 +16,7 @@ describe AnalyticsIssueSerializer do
iid: "1",
id: "1",
created_at: "2016-11-12 15:04:02.948604",
- author: user,
+ author: user
}
end
diff --git a/spec/serializers/merge_request_entity_spec.rb b/spec/serializers/merge_request_entity_spec.rb
index bb6e83ae4bd..b75c73e78c2 100644
--- a/spec/serializers/merge_request_entity_spec.rb
+++ b/spec/serializers/merge_request_entity_spec.rb
@@ -65,6 +65,23 @@ describe MergeRequestEntity do
.to eq(resource.merge_commit_message(include_description: true))
end
+ describe 'new_blob_path' do
+ context 'when user can push to project' do
+ it 'returns path' do
+ project.add_developer(user)
+
+ expect(subject[:new_blob_path])
+ .to eq("/#{resource.project.full_path}/new/#{resource.source_branch}")
+ end
+ end
+
+ context 'when user cannot push to project' do
+ it 'returns nil' do
+ expect(subject[:new_blob_path]).to be_nil
+ end
+ end
+ end
+
describe 'diff_head_sha' do
before do
allow(resource).to receive(:diff_head_sha) { 'sha' }
diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb
index 1ff1438ba06..b536103ed65 100644
--- a/spec/services/ci/create_pipeline_service_spec.rb
+++ b/spec/services/ci/create_pipeline_service_spec.rb
@@ -27,12 +27,14 @@ describe Ci::CreatePipelineService, services: true do
)
end
- it { expect(pipeline).to be_kind_of(Ci::Pipeline) }
- it { expect(pipeline).to be_valid }
- it { expect(pipeline).to eq(project.pipelines.last) }
- it { expect(pipeline).to have_attributes(user: user) }
- it { expect(pipeline).to have_attributes(status: 'pending') }
- it { expect(pipeline.builds.first).to be_kind_of(Ci::Build) }
+ it 'creates a pipeline' do
+ expect(pipeline).to be_kind_of(Ci::Pipeline)
+ expect(pipeline).to be_valid
+ expect(pipeline).to eq(project.pipelines.last)
+ expect(pipeline).to have_attributes(user: user)
+ expect(pipeline).to have_attributes(status: 'pending')
+ expect(pipeline.builds.first).to be_kind_of(Ci::Build)
+ end
context '#update_merge_requests_head_pipeline' do
it 'updates head pipeline of each merge request' do
diff --git a/spec/services/ci/process_pipeline_service_spec.rb b/spec/services/ci/process_pipeline_service_spec.rb
index 1d0a28210fb..fc5de5d069a 100644
--- a/spec/services/ci/process_pipeline_service_spec.rb
+++ b/spec/services/ci/process_pipeline_service_spec.rb
@@ -443,6 +443,21 @@ describe Ci::ProcessPipelineService, '#execute', :services do
end
end
+ context 'updates a list of retried builds' do
+ subject { described_class.retried.order(:id) }
+
+ let!(:build_retried) { create_build('build') }
+ let!(:build) { create_build('build') }
+ let!(:test) { create_build('test') }
+
+ it 'returns unique statuses' do
+ process_pipeline
+
+ expect(all_builds.latest).to contain_exactly(build, test)
+ expect(all_builds.retried).to contain_exactly(build_retried)
+ end
+ end
+
def process_pipeline
described_class.new(pipeline.project, user).execute(pipeline)
end
diff --git a/spec/services/ci/retry_build_service_spec.rb b/spec/services/ci/retry_build_service_spec.rb
index b2d37657770..7254e6b357a 100644
--- a/spec/services/ci/retry_build_service_spec.rb
+++ b/spec/services/ci/retry_build_service_spec.rb
@@ -22,7 +22,7 @@ describe Ci::RetryBuildService, :services do
%i[type lock_version target_url base_tags
commit_id deployments erased_by_id last_deployment project_id
runner_id tag_taggings taggings tags trigger_request_id
- user_id auto_canceled_by_id].freeze
+ user_id auto_canceled_by_id retried].freeze
shared_examples 'build duplication' do
let(:build) do
@@ -115,7 +115,7 @@ describe Ci::RetryBuildService, :services do
end
describe '#reprocess' do
- let(:new_build) { service.reprocess(build) }
+ let(:new_build) { service.reprocess!(build) }
context 'when user has ability to execute build' do
before do
@@ -131,11 +131,16 @@ describe Ci::RetryBuildService, :services do
it 'does not enqueue the new build' do
expect(new_build).to be_created
end
+
+ it 'does mark old build as retried' do
+ expect(new_build).to be_latest
+ expect(build.reload).to be_retried
+ end
end
context 'when user does not have ability to execute build' do
it 'raises an error' do
- expect { service.reprocess(build) }
+ expect { service.reprocess!(build) }
.to raise_error Gitlab::Access::AccessDeniedError
end
end
diff --git a/spec/services/ci/retry_pipeline_service_spec.rb b/spec/services/ci/retry_pipeline_service_spec.rb
index 40e151545c9..d941d56c0d8 100644
--- a/spec/services/ci/retry_pipeline_service_spec.rb
+++ b/spec/services/ci/retry_pipeline_service_spec.rb
@@ -13,7 +13,7 @@ describe Ci::RetryPipelineService, '#execute', :services do
context 'when there are already retried jobs present' do
before do
- create_build('rspec', :canceled, 0)
+ create_build('rspec', :canceled, 0, retried: true)
create_build('rspec', :failed, 0)
end
diff --git a/spec/services/cohorts_service_spec.rb b/spec/services/cohorts_service_spec.rb
index 1e99442fdcb..77595d7ba2d 100644
--- a/spec/services/cohorts_service_spec.rb
+++ b/spec/services/cohorts_service_spec.rb
@@ -89,7 +89,7 @@ describe CohortsService do
activity_months: [{ total: 2, percentage: 100 }],
total: 2,
inactive: 1
- },
+ }
]
expect(described_class.new.execute).to eq(months_included: 12,
diff --git a/spec/services/create_deployment_service_spec.rb b/spec/services/create_deployment_service_spec.rb
index a883705bd45..f35d7a33548 100644
--- a/spec/services/create_deployment_service_spec.rb
+++ b/spec/services/create_deployment_service_spec.rb
@@ -255,7 +255,7 @@ describe CreateDeploymentService, services: true do
environment: 'production',
ref: 'master',
tag: false,
- sha: '97de212e80737a608d939f648d959671fb0a0142b',
+ sha: '97de212e80737a608d939f648d959671fb0a0142b'
}
end
diff --git a/spec/services/issuable/bulk_update_service_spec.rb b/spec/services/issuable/bulk_update_service_spec.rb
index 5b1639ca0d6..6437d00e451 100644
--- a/spec/services/issuable/bulk_update_service_spec.rb
+++ b/spec/services/issuable/bulk_update_service_spec.rb
@@ -62,7 +62,7 @@ describe Issuable::BulkUpdateService, services: true do
expect(result[:count]).to eq(1)
end
- it 'updates the assignee to the use ID passed' do
+ it 'updates the assignee to the user ID passed' do
assignee = create(:user)
project.team << [assignee, :developer]
@@ -100,7 +100,7 @@ describe Issuable::BulkUpdateService, services: true do
expect(result[:count]).to eq(1)
end
- it 'updates the assignee to the use ID passed' do
+ it 'updates the assignee to the user ID passed' do
assignee = create(:user)
project.team << [assignee, :developer]
expect { bulk_update(issue, assignee_ids: [assignee.id]) }
@@ -163,7 +163,7 @@ describe Issuable::BulkUpdateService, services: true do
{
label_ids: labels.map(&:id),
add_label_ids: add_labels.map(&:id),
- remove_label_ids: remove_labels.map(&:id),
+ remove_label_ids: remove_labels.map(&:id)
}
end
diff --git a/spec/services/issues/build_service_spec.rb b/spec/services/issues/build_service_spec.rb
index 55d635235b0..bed25fe7ccf 100644
--- a/spec/services/issues/build_service_spec.rb
+++ b/spec/services/issues/build_service_spec.rb
@@ -136,7 +136,7 @@ describe Issues::BuildService, services: true do
user,
title: 'Issue #1',
description: 'Issue description',
- milestone_id: milestone.id,
+ milestone_id: milestone.id
).execute
expect(issue.title).to eq('Issue #1')
diff --git a/spec/services/issues/close_service_spec.rb b/spec/services/issues/close_service_spec.rb
index 51840531711..0a1f41719f7 100644
--- a/spec/services/issues/close_service_spec.rb
+++ b/spec/services/issues/close_service_spec.rb
@@ -51,8 +51,10 @@ describe Issues::CloseService, services: true do
end
end
- it { expect(issue).to be_valid }
- it { expect(issue).to be_closed }
+ it 'closes the issue' do
+ expect(issue).to be_valid
+ expect(issue).to be_closed
+ end
it 'sends email to user2 about assign of new issue' do
email = ActionMailer::Base.deliveries.last
@@ -96,9 +98,11 @@ describe Issues::CloseService, services: true do
described_class.new(project, user).close_issue(issue)
end
- it { expect(issue).to be_valid }
- it { expect(issue).to be_opened }
- it { expect(todo.reload).to be_pending }
+ it 'closes the issue' do
+ expect(issue).to be_valid
+ expect(issue).to be_opened
+ expect(todo.reload).to be_pending
+ end
end
end
end
diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb
index 01edc46496d..dab1a3469f7 100644
--- a/spec/services/issues/create_service_spec.rb
+++ b/spec/services/issues/create_service_spec.rb
@@ -118,6 +118,22 @@ describe Issues::CreateService, services: true do
end
end
+ context 'when assignee is set' do
+ let(:opts) do
+ { title: 'Title',
+ description: 'Description',
+ assignees: [assignee] }
+ end
+
+ it 'invalidates open issues counter for assignees when issue is assigned' do
+ project.team << [assignee, :master]
+
+ described_class.new(project, user, opts).execute
+
+ expect(assignee.assigned_open_issues_count).to eq 1
+ end
+ end
+
it 'executes issue hooks when issue is not confidential' do
opts = { title: 'Title', description: 'Description', confidential: false }
diff --git a/spec/services/issues/resolve_discussions_spec.rb b/spec/services/issues/resolve_discussions_spec.rb
index c3b4c2176ee..86f218dec12 100644
--- a/spec/services/issues/resolve_discussions_spec.rb
+++ b/spec/services/issues/resolve_discussions_spec.rb
@@ -77,7 +77,7 @@ describe Issues::ResolveDiscussions, services: true do
_second_discussion = Discussion.new([create(:diff_note_on_merge_request, :resolved,
noteable: merge_request,
project: merge_request.target_project,
- line_number: 15,
+ line_number: 15
)])
service = DummyService.new(
project,
diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb
index 1954d8739f6..5184c1d5f19 100644
--- a/spec/services/issues/update_service_spec.rb
+++ b/spec/services/issues/update_service_spec.rb
@@ -59,6 +59,13 @@ describe Issues::UpdateService, services: true do
expect(issue.due_date).to eq Date.tomorrow
end
+ it 'updates open issue counter for assignees when issue is reassigned' do
+ update_issue(assignee_ids: [user2.id])
+
+ expect(user3.assigned_open_issues_count).to eq 0
+ expect(user2.assigned_open_issues_count).to eq 1
+ end
+
it 'sorts issues as specified by parameters' do
issue1 = create(:issue, project: project, assignees: [user3])
issue2 = create(:issue, project: project, assignees: [user3])
diff --git a/spec/services/members/authorized_destroy_service_spec.rb b/spec/services/members/authorized_destroy_service_spec.rb
index ab440d18e9f..8a6732faa19 100644
--- a/spec/services/members/authorized_destroy_service_spec.rb
+++ b/spec/services/members/authorized_destroy_service_spec.rb
@@ -10,6 +10,27 @@ describe Members::AuthorizedDestroyService, services: true do
Issue.assigned_to(user).count + MergeRequest.assigned_to(user).count
end
+ context 'Invited users' do
+ # Regression spec for issue: https://gitlab.com/gitlab-org/gitlab-ce/issues/32504
+ it 'destroys invited project member' do
+ project.team << [member_user, :developer]
+
+ member = create :project_member, :invited, project: project
+
+ expect { described_class.new(member, member_user).execute }
+ .to change { Member.count }.from(2).to(1)
+ end
+
+ it 'destroys invited group member' do
+ group.add_developer(member_user)
+
+ member = create :group_member, :invited, group: group
+
+ expect { described_class.new(member, member_user).execute }
+ .to change { Member.count }.from(2).to(1)
+ end
+ end
+
context 'Group member' do
it "unassigns issues and merge requests" do
group.add_developer(member_user)
diff --git a/spec/services/merge_requests/conflicts/list_service_spec.rb b/spec/services/merge_requests/conflicts/list_service_spec.rb
new file mode 100644
index 00000000000..23982b9e6e1
--- /dev/null
+++ b/spec/services/merge_requests/conflicts/list_service_spec.rb
@@ -0,0 +1,80 @@
+require 'spec_helper'
+
+describe MergeRequests::Conflicts::ListService do
+ describe '#can_be_resolved_in_ui?' do
+ def create_merge_request(source_branch)
+ create(:merge_request, source_branch: source_branch, target_branch: 'conflict-start') do |mr|
+ mr.mark_as_unmergeable
+ end
+ end
+
+ def conflicts_service(merge_request)
+ described_class.new(merge_request)
+ end
+
+ it 'returns a falsey value when the MR can be merged without conflicts' do
+ merge_request = create_merge_request('master')
+ merge_request.mark_as_mergeable
+
+ expect(conflicts_service(merge_request).can_be_resolved_in_ui?).to be_falsey
+ end
+
+ it 'returns a falsey value when the MR is marked as having conflicts, but has none' do
+ merge_request = create_merge_request('master')
+
+ expect(conflicts_service(merge_request).can_be_resolved_in_ui?).to be_falsey
+ end
+
+ it 'returns a falsey value when one of the MR branches is missing' do
+ merge_request = create_merge_request('conflict-resolvable')
+ merge_request.project.repository.rm_branch(merge_request.author, 'conflict-resolvable')
+
+ expect(conflicts_service(merge_request).can_be_resolved_in_ui?).to be_falsey
+ end
+
+ it 'returns a falsey value when the MR has a missing ref after a force push' do
+ merge_request = create_merge_request('conflict-resolvable')
+ service = conflicts_service(merge_request)
+ allow(service.conflicts).to receive(:merge_index).and_raise(Rugged::OdbError)
+
+ expect(service.can_be_resolved_in_ui?).to be_falsey
+ end
+
+ it 'returns a falsey value when the MR does not support new diff notes' do
+ merge_request = create_merge_request('conflict-resolvable')
+ merge_request.merge_request_diff.update_attributes(start_commit_sha: nil)
+
+ expect(conflicts_service(merge_request).can_be_resolved_in_ui?).to be_falsey
+ end
+
+ it 'returns a falsey value when the conflicts contain a large file' do
+ merge_request = create_merge_request('conflict-too-large')
+
+ expect(conflicts_service(merge_request).can_be_resolved_in_ui?).to be_falsey
+ end
+
+ it 'returns a falsey value when the conflicts contain a binary file' do
+ merge_request = create_merge_request('conflict-binary-file')
+
+ expect(conflicts_service(merge_request).can_be_resolved_in_ui?).to be_falsey
+ end
+
+ it 'returns a falsey value when the conflicts contain a file edited in one branch and deleted in another' do
+ merge_request = create_merge_request('conflict-missing-side')
+
+ expect(conflicts_service(merge_request).can_be_resolved_in_ui?).to be_falsey
+ end
+
+ it 'returns a truthy value when the conflicts are resolvable in the UI' do
+ merge_request = create_merge_request('conflict-resolvable')
+
+ expect(conflicts_service(merge_request).can_be_resolved_in_ui?).to be_truthy
+ end
+
+ it 'returns a truthy value when the conflicts have to be resolved in an editor' do
+ merge_request = create_merge_request('conflict-contains-conflict-markers')
+
+ expect(conflicts_service(merge_request).can_be_resolved_in_ui?).to be_truthy
+ end
+ end
+end
diff --git a/spec/services/merge_requests/resolve_service_spec.rb b/spec/services/merge_requests/conflicts/resolve_service_spec.rb
index 3afd6b92900..19e8d5cc5f1 100644
--- a/spec/services/merge_requests/resolve_service_spec.rb
+++ b/spec/services/merge_requests/conflicts/resolve_service_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe MergeRequests::ResolveService do
+describe MergeRequests::Conflicts::ResolveService do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
@@ -24,6 +24,8 @@ describe MergeRequests::ResolveService do
end
describe '#execute' do
+ let(:service) { described_class.new(merge_request) }
+
context 'with section params' do
let(:params) do
{
@@ -50,7 +52,7 @@ describe MergeRequests::ResolveService do
context 'when the source and target project are the same' do
before do
- described_class.new(project, user, params).execute(merge_request)
+ service.execute(user, params)
end
it 'creates a commit with the message' do
@@ -74,15 +76,26 @@ describe MergeRequests::ResolveService do
branch_name: 'conflict-start')
end
- before do
- described_class.new(fork_project, user, params).execute(merge_request_from_fork)
+ def resolve_conflicts
+ described_class.new(merge_request_from_fork).execute(user, params)
+ end
+
+ it 'gets conflicts from the source project' do
+ expect(fork_project.repository.rugged).to receive(:merge_commits).and_call_original
+ expect(project.repository.rugged).not_to receive(:merge_commits)
+
+ resolve_conflicts
end
it 'creates a commit with the message' do
+ resolve_conflicts
+
expect(merge_request_from_fork.source_branch_head.message).to eq(params[:commit_message])
end
it 'creates a commit with the correct parents' do
+ resolve_conflicts
+
expect(merge_request_from_fork.source_branch_head.parents.map(&:id)).
to eq(['404fa3fc7c2c9b5dacff102f353bdf55b1be2813',
target_head])
@@ -115,7 +128,7 @@ describe MergeRequests::ResolveService do
end
before do
- described_class.new(project, user, params).execute(merge_request)
+ service.execute(user, params)
end
it 'creates a commit with the message' do
@@ -154,15 +167,15 @@ describe MergeRequests::ResolveService do
}
end
- let(:service) { described_class.new(project, user, invalid_params) }
-
it 'raises a MissingResolution error' do
- expect { service.execute(merge_request) }.
+ expect { service.execute(user, invalid_params) }.
to raise_error(Gitlab::Conflict::File::MissingResolution)
end
end
context 'when the content of a file is unchanged' do
+ let(:list_service) { MergeRequests::Conflicts::ListService.new(merge_request) }
+
let(:invalid_params) do
{
files: [
@@ -173,17 +186,15 @@ describe MergeRequests::ResolveService do
}, {
old_path: 'files/ruby/regex.rb',
new_path: 'files/ruby/regex.rb',
- content: merge_request.conflicts.file_for_path('files/ruby/regex.rb', 'files/ruby/regex.rb').content
+ content: list_service.conflicts.file_for_path('files/ruby/regex.rb', 'files/ruby/regex.rb').content
}
],
commit_message: 'This is a commit message!'
}
end
- let(:service) { described_class.new(project, user, invalid_params) }
-
it 'raises a MissingResolution error' do
- expect { service.execute(merge_request) }.
+ expect { service.execute(user, invalid_params) }.
to raise_error(Gitlab::Conflict::File::MissingResolution)
end
end
@@ -202,11 +213,9 @@ describe MergeRequests::ResolveService do
}
end
- let(:service) { described_class.new(project, user, invalid_params) }
-
it 'raises a MissingFiles error' do
- expect { service.execute(merge_request) }.
- to raise_error(MergeRequests::ResolveService::MissingFiles)
+ expect { service.execute(user, invalid_params) }.
+ to raise_error(described_class::MissingFiles)
end
end
end
diff --git a/spec/services/merge_requests/create_service_spec.rb b/spec/services/merge_requests/create_service_spec.rb
index ace82380cc9..b70e9d534a4 100644
--- a/spec/services/merge_requests/create_service_spec.rb
+++ b/spec/services/merge_requests/create_service_spec.rb
@@ -27,10 +27,12 @@ describe MergeRequests::CreateService, services: true do
@merge_request = service.execute
end
- it { expect(@merge_request).to be_valid }
- it { expect(@merge_request.title).to eq('Awesome merge_request') }
- it { expect(@merge_request.assignee).to be_nil }
- it { expect(@merge_request.merge_params['force_remove_source_branch']).to eq('1') }
+ it 'creates an MR' do
+ expect(@merge_request).to be_valid
+ expect(@merge_request.title).to eq('Awesome merge_request')
+ expect(@merge_request.assignee).to be_nil
+ expect(@merge_request.merge_params['force_remove_source_branch']).to eq('1')
+ end
it 'executes hooks with default action' do
expect(service).to have_received(:execute_hooks).with(@merge_request)
@@ -144,6 +146,26 @@ describe MergeRequests::CreateService, services: true do
expect(merge_request.assignee).to eq(assignee)
end
+ context 'when assignee is set' do
+ let(:opts) do
+ {
+ title: 'Title',
+ description: 'Description',
+ assignee_id: assignee.id,
+ source_branch: 'feature',
+ target_branch: 'master'
+ }
+ end
+
+ it 'invalidates open merge request counter for assignees when merge request is assigned' do
+ project.team << [assignee, :master]
+
+ described_class.new(project, user, opts).execute
+
+ expect(assignee.assigned_open_merge_requests_count).to eq 1
+ end
+ end
+
context "when issuable feature is private" do
before do
project.project_feature.update(issues_access_level: ProjectFeature::PRIVATE,
diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb
index 03215a4624a..1f109eab268 100644
--- a/spec/services/merge_requests/refresh_service_spec.rb
+++ b/spec/services/merge_requests/refresh_service_spec.rb
@@ -348,7 +348,7 @@ describe MergeRequests::RefreshService, services: true do
title: 'fixup! Fix issue',
work_in_progress?: true,
to_reference: 'ccccccc'
- ),
+ )
])
refresh_service.execute(@oldrev, @newrev, 'refs/heads/wip')
reload_mrs
diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb
index 07f5440cc36..860a7798857 100644
--- a/spec/services/merge_requests/update_service_spec.rb
+++ b/spec/services/merge_requests/update_service_spec.rb
@@ -59,14 +59,16 @@ describe MergeRequests::UpdateService, services: true do
end
end
- it { expect(@merge_request).to be_valid }
- it { expect(@merge_request.title).to eq('New title') }
- it { expect(@merge_request.assignee).to eq(user2) }
- it { expect(@merge_request).to be_closed }
- it { expect(@merge_request.labels.count).to eq(1) }
- it { expect(@merge_request.labels.first.title).to eq(label.name) }
- it { expect(@merge_request.target_branch).to eq('target') }
- it { expect(@merge_request.merge_params['force_remove_source_branch']).to eq('1') }
+ it 'mathces base expectations' do
+ expect(@merge_request).to be_valid
+ expect(@merge_request.title).to eq('New title')
+ expect(@merge_request.assignee).to eq(user2)
+ expect(@merge_request).to be_closed
+ expect(@merge_request.labels.count).to eq(1)
+ expect(@merge_request.labels.first.title).to eq(label.name)
+ expect(@merge_request.target_branch).to eq('target')
+ expect(@merge_request.merge_params['force_remove_source_branch']).to eq('1')
+ end
it 'executes hooks with update action' do
expect(service).to have_received(:execute_hooks).
@@ -148,9 +150,11 @@ describe MergeRequests::UpdateService, services: true do
end
end
- it { expect(@merge_request).to be_valid }
- it { expect(@merge_request.state).to eq('merged') }
- it { expect(@merge_request.merge_error).to be_nil }
+ it 'merges the MR' do
+ expect(@merge_request).to be_valid
+ expect(@merge_request.state).to eq('merged')
+ expect(@merge_request.merge_error).to be_nil
+ end
end
context 'with finished pipeline' do
@@ -167,8 +171,10 @@ describe MergeRequests::UpdateService, services: true do
end
end
- it { expect(@merge_request).to be_valid }
- it { expect(@merge_request.state).to eq('merged') }
+ it 'merges the MR' do
+ expect(@merge_request).to be_valid
+ expect(@merge_request.state).to eq('merged')
+ end
end
context 'with active pipeline' do
@@ -202,8 +208,10 @@ describe MergeRequests::UpdateService, services: true do
end
end
- it { expect(@merge_request.state).to eq('opened') }
- it { expect(@merge_request.merge_error).not_to be_nil }
+ it 'does not merge the MR' do
+ expect(@merge_request.state).to eq('opened')
+ expect(@merge_request.merge_error).not_to be_nil
+ end
end
context 'MR can not be merged when note sha != MR sha' do
@@ -299,6 +307,15 @@ describe MergeRequests::UpdateService, services: true do
end
end
+ context 'when the assignee changes' do
+ it 'updates open merge request counter for assignees when merge request is reassigned' do
+ update_merge_request(assignee_id: user2.id)
+
+ expect(user3.assigned_open_merge_requests_count).to eq 0
+ expect(user2.assigned_open_merge_requests_count).to eq 1
+ end
+ end
+
context 'when the target branch change' do
before do
update_merge_request({ target_branch: 'target' })
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index 74f96b97909..de3bbc6b6a1 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -350,7 +350,7 @@ describe NotificationService, services: true do
create(:note_on_personal_snippet, noteable: snippet, note: 'note', author: @u_participant),
create(:note_on_personal_snippet, noteable: snippet, note: 'note', author: @u_mentioned),
create(:note_on_personal_snippet, noteable: snippet, note: 'note', author: @u_disabled),
- create(:note_on_personal_snippet, noteable: snippet, note: 'note', author: @u_note_author),
+ create(:note_on_personal_snippet, noteable: snippet, note: 'note', author: @u_note_author)
]
end
diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb
index 516566eddef..7a9cd7553b1 100644
--- a/spec/services/system_note_service_spec.rb
+++ b/spec/services/system_note_service_spec.rb
@@ -178,7 +178,7 @@ describe SystemNoteService, services: true do
end
it 'builds a correct phrase when assignee removed' do
- expect(build_note([assignee1], [])).to eq 'removed all assignees'
+ expect(build_note([assignee1], [])).to eq 'removed assignee'
end
it 'builds a correct phrase when assignees changed' do
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index e2d5928e5b2..a58f4e664b7 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -10,7 +10,7 @@ require 'shoulda/matchers'
require 'rspec/retry'
rspec_profiling_is_configured =
- ENV['RSPEC_PROFILING_POSTGRES_URL'] ||
+ ENV['RSPEC_PROFILING_POSTGRES_URL'].present? ||
ENV['RSPEC_PROFILING']
branch_can_be_profiled =
ENV['GITLAB_DATABASE'] == 'postgresql' &&
diff --git a/spec/support/dropzone_helper.rb b/spec/support/dropzone_helper.rb
index 984ec7d2741..02fdeb08afe 100644
--- a/spec/support/dropzone_helper.rb
+++ b/spec/support/dropzone_helper.rb
@@ -6,32 +6,52 @@ module DropzoneHelper
# Dropzone events to perform the actual upload.
#
# This method waits for the upload to complete before returning.
- def dropzone_file(file_path)
+ # max_file_size is an optional parameter.
+ # If it's not 0, then it used in dropzone.maxFilesize parameter.
+ # wait_for_queuecomplete is an optional parameter.
+ # If it's 'false', then the helper will NOT wait for backend response
+ # It lets to test behaviors while AJAX is processing.
+ def dropzone_file(files, max_file_size = 0, wait_for_queuecomplete = true)
# Generate a fake file input that Capybara can attach to
page.execute_script <<-JS.strip_heredoc
+ $('#fakeFileInput').remove();
var fakeFileInput = window.$('<input/>').attr(
- {id: 'fakeFileInput', type: 'file'}
+ {id: 'fakeFileInput', type: 'file', multiple: true}
).appendTo('body');
window._dropzoneComplete = false;
JS
- # Attach the file to the fake input selector with Capybara
- attach_file('fakeFileInput', file_path)
+ # Attach files to the fake input selector with Capybara
+ attach_file('fakeFileInput', files)
# 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.options.autoProcessQueue = false;
+
+ if (#{max_file_size} > 0) {
+ dropzone.options.maxFilesize = #{max_file_size};
+ }
+
dropzone.on('queuecomplete', function() {
window._dropzoneComplete = true;
});
- dropzone.listeners[0].events.drop(e);
+
+ var fileList = [$('#fakeFileInput')[0].files];
+
+ $.map(fileList, function(file){
+ var e = jQuery.Event('drop', { dataTransfer : { files : file } });
+
+ dropzone.listeners[0].events.drop(e);
+ });
+
+ dropzone.processQueue();
JS
- # Wait until Dropzone's fired `queuecomplete`
- loop until page.evaluate_script('window._dropzoneComplete === true')
+ if wait_for_queuecomplete
+ # Wait until Dropzone's fired `queuecomplete`
+ loop until page.evaluate_script('window._dropzoneComplete === true')
+ end
end
end
diff --git a/spec/support/filtered_search_helpers.rb b/spec/support/filtered_search_helpers.rb
index 36be0bb6bf8..37cc308e613 100644
--- a/spec/support/filtered_search_helpers.rb
+++ b/spec/support/filtered_search_helpers.rb
@@ -73,11 +73,11 @@ module FilteredSearchHelpers
end
def remove_recent_searches
- execute_script('window.localStorage.removeItem(\'issue-recent-searches\');')
+ execute_script('window.localStorage.clear();')
end
- def set_recent_searches(input)
- execute_script("window.localStorage.setItem('issue-recent-searches', '#{input}');")
+ def set_recent_searches(key, input)
+ execute_script("window.localStorage.setItem('#{key}', '#{input}');")
end
def wait_for_filtered_search(text)
diff --git a/spec/support/generate-seed-repo-rb b/spec/support/generate-seed-repo-rb
new file mode 100755
index 00000000000..7335f74c0e9
--- /dev/null
+++ b/spec/support/generate-seed-repo-rb
@@ -0,0 +1,162 @@
+#!/usr/bin/env ruby
+#
+# # generate-seed-repo-rb
+#
+# This script generates the seed_repo.rb file used by lib/gitlab/git
+# tests. The seed_repo.rb file needs to be updated anytime there is a
+# Git push to https://gitlab.com/gitlab-org/gitlab-git-test.
+#
+# Usage:
+#
+# ./spec/support/generate-seed-repo-rb > spec/support/seed_repo.rb
+#
+#
+
+require 'erb'
+require 'tempfile'
+
+SOURCE = 'https://gitlab.com/gitlab-org/gitlab-git-test.git'.freeze
+SCRIPT_NAME = 'generate-seed-repo-rb'.freeze
+REPO_NAME = 'gitlab-git-test.git'.freeze
+
+def main
+ Dir.mktmpdir do |dir|
+ unless system(*%W[git clone --bare #{SOURCE} #{REPO_NAME}], chdir: dir)
+ abort "git clone failed"
+ end
+ repo = File.join(dir, REPO_NAME)
+ erb = ERB.new(DATA.read)
+ erb.run(binding)
+ end
+end
+
+def capture!(cmd, dir)
+ output = IO.popen(cmd, 'r', chdir: dir) { |io| io.read }
+ raise "command failed with #{$?}: #{cmd.join(' ')}" unless $?.success?
+ output.chomp
+end
+
+main
+
+__END__
+# This file is generated by <%= SCRIPT_NAME %>. Do not edit this file manually.
+#
+# Seed repo:
+<%= capture!(%w{git log --format=#\ %H\ %s}, repo) %>
+
+module SeedRepo
+ module BigCommit
+ 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".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".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".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".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".freeze
+ PARENT_ID = nil
+ MESSAGE = "Initial commit".freeze
+ AUTHOR_FULL_NAME = "Dmitriy Zaporozhets".freeze
+ FILES = ["LICENSE", ".gitignore", "README.md"].freeze
+ FILES_COUNT = 3
+ end
+
+ module LastCommit
+ ID = <%= capture!(%w[git show -s --format=%H HEAD], repo).inspect %>.freeze
+ PARENT_ID = <%= capture!(%w[git show -s --format=%P HEAD], repo).split.last.inspect %>.freeze
+ MESSAGE = <%= capture!(%w[git show -s --format=%s HEAD], repo).inspect %>.freeze
+ AUTHOR_FULL_NAME = <%= capture!(%w[git show -s --format=%an HEAD], repo).inspect %>.freeze
+ FILES = <%=
+ parents = capture!(%w[git show -s --format=%P HEAD], repo).split
+ merge_base = parents.size > 1 ? capture!(%w[git merge-base] + parents, repo) : parents.first
+ capture!( %W[git diff --name-only #{merge_base}..HEAD --], repo).split("\n").inspect
+ %>.freeze
+ FILES_COUNT = FILES.count
+ end
+
+ module Repo
+ HEAD = "master".freeze
+ BRANCHES = %w[
+<%= capture!(%W[git for-each-ref --format=#{' ' * 3}%(refname:strip=2) refs/heads/], repo) %>
+ ].freeze
+ TAGS = %w[
+<%= capture!(%W[git for-each-ref --format=#{' ' * 3}%(refname:strip=2) refs/tags/], repo) %>
+ ].freeze
+ end
+
+ module RubyBlob
+ ID = "7e3e39ebb9b2bf433b4ad17313770fbe4051649c".freeze
+ NAME = "popen.rb".freeze
+ CONTENT = <<-eos.freeze
+require 'fileutils'
+require 'open3'
+
+module Popen
+ extend self
+
+ def popen(cmd, path=nil)
+ unless cmd.is_a?(Array)
+ raise RuntimeError, "System commands must be given as an array of strings"
+ end
+
+ path ||= Dir.pwd
+
+ vars = {
+ "PWD" => path
+ }
+
+ options = {
+ chdir: path
+ }
+
+ unless File.directory?(path)
+ FileUtils.mkdir_p(path)
+ end
+
+ @cmd_output = ""
+ @cmd_status = 0
+
+ Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|
+ @cmd_output << stdout.read
+ @cmd_output << stderr.read
+ @cmd_status = wait_thr.value.exitstatus
+ end
+
+ return @cmd_output, @cmd_status
+ end
+end
+ eos
+ end
+end
diff --git a/spec/support/kubernetes_helpers.rb b/spec/support/kubernetes_helpers.rb
index b5ed71ba3be..d2a1ded57ff 100644
--- a/spec/support/kubernetes_helpers.rb
+++ b/spec/support/kubernetes_helpers.rb
@@ -5,7 +5,7 @@ module KubernetesHelpers
{
"kind" => "APIResourceList",
"resources" => [
- { "name" => "pods", "namespaced" => true, "kind" => "Pod" },
+ { "name" => "pods", "namespaced" => true, "kind" => "Pod" }
]
}
end
@@ -22,13 +22,13 @@ module KubernetesHelpers
"metadata" => {
"name" => "kube-pod",
"creationTimestamp" => "2016-11-25T19:55:19Z",
- "labels" => { "app" => app },
+ "labels" => { "app" => app }
},
"spec" => {
"containers" => [
{ "name" => "container-0" },
- { "name" => "container-1" },
- ],
+ { "name" => "container-1" }
+ ]
},
"status" => { "phase" => "Running" }
}
diff --git a/spec/support/matchers/gitaly_matchers.rb b/spec/support/matchers/gitaly_matchers.rb
index 65dbc01f6e4..ed14bcec9f2 100644
--- a/spec/support/matchers/gitaly_matchers.rb
+++ b/spec/support/matchers/gitaly_matchers.rb
@@ -1,3 +1,9 @@
RSpec::Matchers.define :gitaly_request_with_repo_path do |path|
match { |actual| actual.repository.path == path }
end
+
+RSpec::Matchers.define :gitaly_request_with_params do |params|
+ match do |actual|
+ params.reduce(true) { |r, (key, val)| r && actual.send(key) == val }
+ end
+end
diff --git a/spec/support/milestone_tabs_examples.rb b/spec/support/milestone_tabs_examples.rb
index c69f8e11008..4ad8b0a16e1 100644
--- a/spec/support/milestone_tabs_examples.rb
+++ b/spec/support/milestone_tabs_examples.rb
@@ -1,7 +1,7 @@
shared_examples 'milestone tabs' do
def go(path, extra_params = {})
params = if milestone.is_a?(GlobalMilestone)
- { group_id: group.id, id: milestone.safe_title, title: milestone.title }
+ { group_id: group.to_param, id: milestone.safe_title, title: milestone.title }
else
{ namespace_id: project.namespace.to_param, project_id: project, id: milestone.iid }
end
diff --git a/spec/support/prometheus_helpers.rb b/spec/support/prometheus_helpers.rb
index 51987c7767d..6b9ebcf2bb3 100644
--- a/spec/support/prometheus_helpers.rb
+++ b/spec/support/prometheus_helpers.rb
@@ -1,10 +1,10 @@
module PrometheusHelpers
def prometheus_memory_query(environment_slug)
- %{(sum(container_memory_usage_bytes{container_name!="POD",environment="#{environment_slug}"}) / count(container_memory_usage_bytes{container_name!="POD",environment="#{environment_slug}"})) /1024/1024}
+ %{avg(container_memory_usage_bytes{container_name!="POD",environment="#{environment_slug}"}) / 2^20}
end
def prometheus_cpu_query(environment_slug)
- %{sum(rate(container_cpu_usage_seconds_total{container_name!="POD",environment="#{environment_slug}"}[2m])) / count(container_cpu_usage_seconds_total{container_name!="POD",environment="#{environment_slug}"}) * 100}
+ %{avg(rate(container_cpu_usage_seconds_total{container_name!="POD",environment="#{environment_slug}"}[2m])) * 100}
end
def prometheus_ping_url(prometheus_query)
@@ -88,10 +88,8 @@ module PrometheusHelpers
metrics: {
memory_values: prometheus_values_body('matrix').dig(:data, :result),
memory_current: prometheus_value_body('vector').dig(:data, :result),
- memory_previous: prometheus_value_body('vector').dig(:data, :result),
cpu_values: prometheus_values_body('matrix').dig(:data, :result),
- cpu_current: prometheus_value_body('vector').dig(:data, :result),
- cpu_previous: prometheus_value_body('vector').dig(:data, :result)
+ cpu_current: prometheus_value_body('vector').dig(:data, :result)
},
last_update: last_update
}
diff --git a/spec/features/protected_branches/access_control_ce_spec.rb b/spec/support/protected_branches/access_control_ce_shared_examples.rb
index d30e7947106..7fda4ade665 100644
--- a/spec/features/protected_branches/access_control_ce_spec.rb
+++ b/spec/support/protected_branches/access_control_ce_shared_examples.rb
@@ -31,7 +31,7 @@ RSpec.shared_examples "protected branches > access control > CE" do
within(".protected-branches-list") do
find(".js-allowed-to-push").click
-
+
within('.js-allowed-to-push-container') do
expect(first("li")).to have_content("Roles")
click_on access_type_name
diff --git a/spec/features/protected_tags/access_control_ce_spec.rb b/spec/support/protected_tags/access_control_ce_shared_examples.rb
index 12622cd548a..12622cd548a 100644
--- a/spec/features/protected_tags/access_control_ce_spec.rb
+++ b/spec/support/protected_tags/access_control_ce_shared_examples.rb
diff --git a/spec/support/repo_helpers.rb b/spec/support/repo_helpers.rb
index e9d5c7b12ae..3c6956cf5e0 100644
--- a/spec/support/repo_helpers.rb
+++ b/spec/support/repo_helpers.rb
@@ -92,11 +92,11 @@ eos
changes = [
{
line_code: 'a5cc2925ca8258af241be7e5b0381edf30266302_20_20',
- file_path: '.gitignore',
+ file_path: '.gitignore'
},
{
line_code: '7445606fbf8f3683cd42bdc54b05d7a0bc2dfc44_4_6',
- file_path: '.gitmodules',
+ file_path: '.gitmodules'
}
]
diff --git a/spec/support/seed_repo.rb b/spec/support/seed_repo.rb
index 99a500bbbb1..cfe7fc980a8 100644
--- a/spec/support/seed_repo.rb
+++ b/spec/support/seed_repo.rb
@@ -1,4 +1,8 @@
+# This file is generated by generate-seed-repo-rb. Do not edit this file manually.
+#
# Seed repo:
+# 4b4918a572fa86f9771e5ba40fbd48e1eb03e2c6 Merge branch 'master' into 'master'
+# 0e1b353b348f8477bdbec1ef47087171c5032cd9 adds an executable with different permissions
# 0e50ec4d3c7ce42ab74dda1d422cb2cbffe1e326 Merge branch 'lfs_pointers' into 'master'
# 33bcff41c232a11727ac6d660bd4b0c2ba86d63d Add valid and invalid lfs pointers
# 732401c65e924df81435deb12891ef570167d2e2 Update year in license file
@@ -94,7 +98,12 @@ module SeedRepo
master
merge-test
].freeze
- 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
diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb
index 8e31c26591b..b168098edea 100644
--- a/spec/support/test_env.rb
+++ b/spec/support/test_env.rb
@@ -40,8 +40,8 @@ module TestEnv
'wip' => 'b9238ee',
'csv' => '3dd0896',
'v1.1.0' => 'b83d6e3',
- 'add-ipython-files' => '6d85bb69',
- 'add-pdf-file' => 'e774ebd3'
+ 'add-ipython-files' => '6d85bb6',
+ 'add-pdf-file' => 'e774ebd'
}.freeze
# gitlab-test-fork is a fork of gitlab-fork, but we don't necessarily
@@ -120,7 +120,7 @@ module TestEnv
end
def setup_gitaly
- socket_path = Gitlab::GitalyClient.get_address('default').sub(/\Aunix:/, '')
+ socket_path = Gitlab::GitalyClient.address('default').sub(/\Aunix:/, '')
gitaly_dir = File.dirname(socket_path)
unless File.directory?(gitaly_dir) || system('rake', "gitlab:gitaly:install[#{gitaly_dir}]")
@@ -133,7 +133,8 @@ module TestEnv
def start_gitaly(gitaly_dir)
gitaly_exec = File.join(gitaly_dir, 'gitaly')
gitaly_config = File.join(gitaly_dir, 'config.toml')
- @gitaly_pid = spawn(gitaly_exec, gitaly_config, [:out, :err] => '/dev/null')
+ log_file = Rails.root.join('log/gitaly-test.log').to_s
+ @gitaly_pid = spawn(gitaly_exec, gitaly_config, [:out, :err] => log_file)
end
def stop_gitaly
@@ -154,14 +155,14 @@ module TestEnv
FORKED_BRANCH_SHA)
end
- def setup_repo(repo_path, repo_path_bare, repo_name, branch_sha)
+ def setup_repo(repo_path, repo_path_bare, repo_name, refs)
clone_url = "https://gitlab.com/gitlab-org/#{repo_name}.git"
unless File.directory?(repo_path)
system(*%W(#{Gitlab.config.git.bin_path} clone -q #{clone_url} #{repo_path}))
end
- set_repo_refs(repo_path, branch_sha)
+ set_repo_refs(repo_path, refs)
unless File.directory?(repo_path_bare)
# We must copy bare repositories because we will push to them.
@@ -169,13 +170,12 @@ module TestEnv
end
end
- def copy_repo(project)
- base_repo_path = File.expand_path(factory_repo_path_bare)
+ def copy_repo(project, bare_repo:, refs:)
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.cp_r("#{File.expand_path(bare_repo)}/.", target_repo_path)
FileUtils.chmod_R 0755, target_repo_path
- set_repo_refs(target_repo_path, BRANCH_SHA)
+ set_repo_refs(target_repo_path, refs)
end
def repos_path
@@ -190,15 +190,6 @@ module TestEnv
Gitlab.config.pages.path
end
- 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.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
- set_repo_refs(target_repo_path, FORKED_BRANCH_SHA)
- end
-
# When no cached assets exist, manually hit the root path to create them
#
# Otherwise they'd be created by the first test, often timing out and
@@ -210,16 +201,20 @@ module TestEnv
Capybara.current_session.visit '/'
end
+ def factory_repo_path_bare
+ "#{factory_repo_path}_bare"
+ end
+
+ def forked_repo_path_bare
+ "#{forked_repo_path}_bare"
+ end
+
private
def factory_repo_path
@factory_repo_path ||= Rails.root.join('tmp', 'tests', factory_repo_name)
end
- def factory_repo_path_bare
- "#{factory_repo_path}_bare"
- end
-
def factory_repo_name
'gitlab-test'
end
@@ -228,10 +223,6 @@ module TestEnv
@forked_repo_path ||= Rails.root.join('tmp', 'tests', forked_repo_name)
end
- def forked_repo_path_bare
- "#{forked_repo_path}_bare"
- end
-
def forked_repo_name
'gitlab-test-fork'
end
@@ -243,19 +234,22 @@ module TestEnv
end
def set_repo_refs(repo_path, branch_sha)
- instructions = branch_sha.map {|branch, sha| "update refs/heads/#{branch}\x00#{sha}\x00" }.join("\x00") << "\x00"
+ instructions = branch_sha.map { |branch, sha| "update refs/heads/#{branch}\x00#{sha}\x00" }.join("\x00") << "\x00"
update_refs = %W(#{Gitlab.config.git.bin_path} update-ref --stdin -z)
reset = proc do
- IO.popen(update_refs, "w") {|io| io.write(instructions) }
- $?.success?
+ Dir.chdir(repo_path) do
+ IO.popen(update_refs, "w") { |io| io.write(instructions) }
+ $?.success?
+ end
end
- Dir.chdir(repo_path) do
- # Try to reset without fetching to avoid using the network.
- unless reset.call
- raise 'Could not fetch test seed repository.' unless system(*%W(#{Gitlab.config.git.bin_path} fetch origin))
- raise 'The fetched test seed does not contain the required revision.' unless reset.call
- end
+ # Try to reset without fetching to avoid using the network.
+ unless reset.call
+ raise 'Could not fetch test seed repository.' unless system(*%W(#{Gitlab.config.git.bin_path} -C #{repo_path} fetch origin))
+
+ # Before we used Git clone's --mirror option, bare repos could end up
+ # with missing refs, clearing them and retrying should fix the issue.
+ cleanup && init unless reset.call
end
end
end
diff --git a/spec/support/wait_for_requests.rb b/spec/support/wait_for_requests.rb
index a18c8e03aa6..d41e83ae128 100644
--- a/spec/support/wait_for_requests.rb
+++ b/spec/support/wait_for_requests.rb
@@ -10,17 +10,12 @@ module WaitForRequests
def wait_for_requests_complete
Gitlab::Testing::RequestBlockerMiddleware.block_requests!
wait_for('pending AJAX requests complete') do
- Gitlab::Testing::RequestBlockerMiddleware.num_active_requests.zero? &&
- finished_all_requests?
+ Gitlab::Testing::RequestBlockerMiddleware.num_active_requests.zero?
end
ensure
Gitlab::Testing::RequestBlockerMiddleware.allow_requests!
end
- def finished_all_requests?
- finished_all_ajax_requests? && finished_all_vue_resource_requests?
- end
-
# Waits until the passed block returns true
def wait_for(condition_name, max_wait_time: Capybara.default_max_wait_time, polling_interval: 0.01)
wait_until = Time.now + max_wait_time.seconds
diff --git a/spec/support/workhorse_helpers.rb b/spec/support/workhorse_helpers.rb
index 47673cd4c3a..ef1f9f68671 100644
--- a/spec/support/workhorse_helpers.rb
+++ b/spec/support/workhorse_helpers.rb
@@ -9,7 +9,7 @@ module WorkhorseHelpers
header = split_header.join(':')
[
type,
- JSON.parse(Base64.urlsafe_decode64(header)),
+ JSON.parse(Base64.urlsafe_decode64(header))
]
end
end
diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb
index df2f2ce95e6..0ff1a988a9e 100644
--- a/spec/tasks/gitlab/backup_rake_spec.rb
+++ b/spec/tasks/gitlab/backup_rake_spec.rb
@@ -236,7 +236,6 @@ describe 'gitlab:app namespace rake task' do
'custom' => { 'path' => Settings.absolute('tmp/tests/custom_storage'), 'gitaly_address' => gitaly_address }
}
allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
- Gitlab::GitalyClient.configure_channels
# Create the projects now, after mocking the settings but before doing the backup
project_a
@@ -352,7 +351,7 @@ describe 'gitlab:app namespace rake task' do
end
it 'name has human readable time' do
- expect(@backup_tar).to match(/\d+_\d{4}_\d{2}_\d{2}_\d+\.\d+\.\d+(-pre)?_gitlab_backup.tar$/)
+ expect(@backup_tar).to match(/\d+_\d{4}_\d{2}_\d{2}_\d+\.\d+\.\d+.*_gitlab_backup.tar$/)
end
end
end # gitlab:app namespace
diff --git a/spec/tasks/gitlab/gitaly_rake_spec.rb b/spec/tasks/gitlab/gitaly_rake_spec.rb
index aaf998a546f..4a636decafd 100644
--- a/spec/tasks/gitlab/gitaly_rake_spec.rb
+++ b/spec/tasks/gitlab/gitaly_rake_spec.rb
@@ -80,28 +80,28 @@ describe 'gitlab:gitaly namespace rake task' do
it 'prints storage configuration in a TOML format' do
config = {
'default' => { 'path' => '/path/to/default' },
- 'nfs_01' => { 'path' => '/path/to/nfs_01' },
+ 'nfs_01' => { 'path' => '/path/to/nfs_01' }
}
allow(Gitlab.config.repositories).to receive(:storages).and_return(config)
- orig_stdout = $stdout
- $stdout = StringIO.new
-
- header = ''
+ expected_output = ''
Timecop.freeze do
- header = <<~TOML
+ expected_output = <<~TOML
# Gitaly storage configuration generated from #{Gitlab.config.source} on #{Time.current.to_s(:long)}
# This is in TOML format suitable for use in Gitaly's config.toml file.
+ [[storage]]
+ name = "default"
+ path = "/path/to/default"
+ [[storage]]
+ name = "nfs_01"
+ path = "/path/to/nfs_01"
TOML
- run_rake_task('gitlab:gitaly:storage_config')
end
- output = $stdout.string
- $stdout = orig_stdout
-
- expect(output).to include(header)
+ expect { run_rake_task('gitlab:gitaly:storage_config')}.
+ to output(expected_output).to_stdout
- parsed_output = TOML.parse(output)
+ parsed_output = TOML.parse(expected_output)
config.each do |name, params|
expect(parsed_output['storage']).to include({ 'name' => name, 'path' => params['path'] })
end
diff --git a/spec/views/projects/_last_commit.html.haml_spec.rb b/spec/views/projects/_last_commit.html.haml_spec.rb
deleted file mode 100644
index eea1695b171..00000000000
--- a/spec/views/projects/_last_commit.html.haml_spec.rb
+++ /dev/null
@@ -1,22 +0,0 @@
-require 'spec_helper'
-
-describe 'projects/_last_commit', :view do
- let(:project) { create(:project, :repository) }
-
- context 'when there is a pipeline present for the commit' do
- context 'when pipeline is blocked' do
- let!(:pipeline) do
- create(:ci_pipeline, :blocked, project: project,
- sha: project.commit.id)
- end
-
- it 'shows correct pipeline badge' do
- render 'projects/last_commit', commit: project.commit,
- project: project,
- ref: :master
-
- expect(rendered).to have_text "blocked #{project.commit.short_id}"
- end
- end
- end
-end
diff --git a/spec/views/projects/blob/_viewer.html.haml_spec.rb b/spec/views/projects/blob/_viewer.html.haml_spec.rb
index 501f90c5f9a..c6b0ed8da3c 100644
--- a/spec/views/projects/blob/_viewer.html.haml_spec.rb
+++ b/spec/views/projects/blob/_viewer.html.haml_spec.rb
@@ -10,9 +10,9 @@ describe 'projects/blob/_viewer.html.haml', :view do
include BlobViewer::Rich
self.partial_name = 'text'
- self.max_size = 1.megabyte
- self.absolute_max_size = 5.megabytes
- self.client_side = false
+ self.overridable_max_size = 1.megabyte
+ self.max_size = 5.megabytes
+ self.load_async = true
end
end
@@ -35,9 +35,9 @@ describe 'projects/blob/_viewer.html.haml', :view do
render partial: 'projects/blob/viewer', locals: { viewer: viewer }
end
- context 'when the viewer is server side' do
+ context 'when the viewer is loaded asynchronously' do
before do
- viewer_class.client_side = false
+ viewer_class.load_async = true
end
context 'when there is no render error' do
@@ -47,10 +47,10 @@ describe 'projects/blob/_viewer.html.haml', :view do
expect(rendered).to have_css('.blob-viewer[data-url]')
end
- it 'displays a spinner' do
+ it 'renders the loading indicator' do
render_view
- expect(rendered).to have_css('i[aria-label="Loading content"]')
+ expect(view).to render_template('projects/blob/viewers/_loading')
end
end
@@ -65,9 +65,9 @@ describe 'projects/blob/_viewer.html.haml', :view do
end
end
- context 'when the viewer is client side' do
+ context 'when the viewer is loaded synchronously' do
before do
- viewer_class.client_side = true
+ viewer_class.load_async = false
end
context 'when there is no render error' do
diff --git a/spec/views/projects/imports/new.html.haml_spec.rb b/spec/views/projects/imports/new.html.haml_spec.rb
new file mode 100644
index 00000000000..9b293065797
--- /dev/null
+++ b/spec/views/projects/imports/new.html.haml_spec.rb
@@ -0,0 +1,22 @@
+require "spec_helper"
+
+describe "projects/imports/new.html.haml" do
+ let(:user) { create(:user) }
+
+ context 'when import fails' do
+ let(:project) { create(:project_empty_repo, import_status: :failed, import_error: '<a href="http://googl.com">Foo</a>', import_type: :gitlab_project, import_source: '/var/opt/gitlab/gitlab-rails/shared/tmp/project_exports/uploads/t.tar.gz', import_url: nil) }
+
+ before do
+ sign_in(user)
+ project.team << [user, :master]
+ end
+
+ it "escapes HTML in import errors" do
+ assign(:project, project)
+
+ render
+
+ expect(rendered).not_to have_link('Foo', href: "http://googl.com")
+ end
+ end
+end
diff --git a/spec/views/projects/pipelines/_stage.html.haml_spec.rb b/spec/views/projects/pipelines/_stage.html.haml_spec.rb
index 10095ad7694..9c91c4e0fbd 100644
--- a/spec/views/projects/pipelines/_stage.html.haml_spec.rb
+++ b/spec/views/projects/pipelines/_stage.html.haml_spec.rb
@@ -39,9 +39,8 @@ describe 'projects/pipelines/_stage', :view do
context 'when there are retried builds present' do
before do
- create_list(:ci_build, 2, name: 'test:build',
- stage: stage.name,
- pipeline: pipeline)
+ create(:ci_build, name: 'test:build', stage: stage.name, pipeline: pipeline, retried: true)
+ create(:ci_build, name: 'test:build', stage: stage.name, pipeline: pipeline)
end
it 'shows only latest builds' do
diff --git a/spec/views/projects/tree/show.html.haml_spec.rb b/spec/views/projects/tree/show.html.haml_spec.rb
index 900f8d4732f..33eba3e6d3d 100644
--- a/spec/views/projects/tree/show.html.haml_spec.rb
+++ b/spec/views/projects/tree/show.html.haml_spec.rb
@@ -21,17 +21,17 @@ describe 'projects/tree/show' do
let(:tree) { repository.tree(commit.id, path) }
before do
+ assign(:id, File.join(ref, path))
assign(:ref, ref)
- assign(:commit, commit)
- assign(:id, commit.id)
- assign(:tree, tree)
assign(:path, path)
+ assign(:last_commit, commit)
+ assign(:tree, tree)
end
it 'displays correctly' do
render
expect(rendered).to have_css('.js-project-refs-dropdown .dropdown-toggle-text', text: ref)
- expect(rendered).to have_css('.readme-holder .file-content', text: ref)
+ expect(rendered).to have_css('.readme-holder')
end
end
end
diff --git a/spec/workers/git_garbage_collect_worker_spec.rb b/spec/workers/git_garbage_collect_worker_spec.rb
index 7a590f64e3c..8c5303b61cc 100644
--- a/spec/workers/git_garbage_collect_worker_spec.rb
+++ b/spec/workers/git_garbage_collect_worker_spec.rb
@@ -105,7 +105,7 @@ describe GitGarbageCollectWorker do
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],
+ parents: [old_commit]
)
GitOperationService.new(nil, project.repository).send(
:update_ref,
diff --git a/spec/workers/namespaceless_project_destroy_worker_spec.rb b/spec/workers/namespaceless_project_destroy_worker_spec.rb
new file mode 100644
index 00000000000..8533b7b85e9
--- /dev/null
+++ b/spec/workers/namespaceless_project_destroy_worker_spec.rb
@@ -0,0 +1,79 @@
+require 'spec_helper'
+
+describe NamespacelessProjectDestroyWorker do
+ subject { described_class.new }
+
+ before do
+ # Stub after_save callbacks that will fail when Project has no namespace
+ allow_any_instance_of(Project).to receive(:ensure_dir_exist).and_return(nil)
+ allow_any_instance_of(Project).to receive(:update_project_statistics).and_return(nil)
+ end
+
+ describe '#perform' do
+ context 'project has namespace' do
+ it 'does not do anything' do
+ project = create(:empty_project)
+
+ subject.perform(project.id)
+
+ expect(Project.unscoped.all).to include(project)
+ end
+ end
+
+ context 'project has no namespace' do
+ let!(:project) do
+ project = build(:empty_project, namespace_id: nil)
+ project.save(validate: false)
+ project
+ end
+
+ context 'project not a fork of another project' do
+ it "truncates the project's team" do
+ expect_any_instance_of(ProjectTeam).to receive(:truncate)
+
+ subject.perform(project.id)
+ end
+
+ it 'deletes the project' do
+ subject.perform(project.id)
+
+ expect(Project.unscoped.all).not_to include(project)
+ end
+
+ it 'does not call unlink_fork' do
+ is_expected.not_to receive(:unlink_fork)
+
+ subject.perform(project.id)
+ end
+
+ it 'does not do anything in Project#remove_pages method' do
+ expect(Gitlab::PagesTransfer).not_to receive(:new)
+
+ subject.perform(project.id)
+ end
+ end
+
+ context 'project forked from another' do
+ let!(:parent_project) { create(:empty_project) }
+
+ before do
+ create(:forked_project_link, forked_to_project: project, forked_from_project: parent_project)
+ end
+
+ it 'closes open merge requests' do
+ merge_request = create(:merge_request, source_project: project, target_project: parent_project)
+
+ subject.perform(project.id)
+
+ expect(merge_request.reload).to be_closed
+ end
+
+ it 'destroys the link' do
+ subject.perform(project.id)
+
+ expect(parent_project.forked_project_links).to be_empty
+ end
+ end
+ end
+ end
+end
diff --git a/spec/workers/pipeline_schedule_worker_spec.rb b/spec/workers/pipeline_schedule_worker_spec.rb
index 91d5a16993f..9c650354d72 100644
--- a/spec/workers/pipeline_schedule_worker_spec.rb
+++ b/spec/workers/pipeline_schedule_worker_spec.rb
@@ -11,40 +11,53 @@ describe PipelineScheduleWorker do
end
before do
- project.add_master(user)
-
stub_ci_pipeline_to_return_yaml_file
- end
- context 'when there is a scheduled pipeline within next_run_at' do
- let(:next_run_at) { 2.days.ago }
+ pipeline_schedule.update_column(:next_run_at, 1.day.ago)
+ end
+ context 'when the schedule is runnable by the user' do
before do
- pipeline_schedule.update_column(:next_run_at, next_run_at)
+ project.add_master(user)
end
- it 'creates a new pipeline' do
- expect { subject }.to change { project.pipelines.count }.by(1)
- end
+ context 'when there is a scheduled pipeline within next_run_at' do
+ it 'creates a new pipeline' do
+ expect { subject }.to change { project.pipelines.count }.by(1)
+ end
- it 'updates the next_run_at field' do
- subject
+ it 'updates the next_run_at field' do
+ subject
+
+ expect(pipeline_schedule.reload.next_run_at).to be > Time.now
+ end
- expect(pipeline_schedule.reload.next_run_at).to be > Time.now
+ it 'sets the schedule on the pipeline' do
+ subject
+
+ expect(project.pipelines.last.pipeline_schedule).to eq(pipeline_schedule)
+ end
end
- it 'sets the schedule on the pipeline' do
- subject
- expect(project.pipelines.last.pipeline_schedule).to eq(pipeline_schedule)
+ context 'inactive schedule' do
+ before do
+ pipeline_schedule.deactivate!
+ end
+
+ it 'does not creates a new pipeline' do
+ expect { subject }.not_to change { project.pipelines.count }
+ end
end
end
- context 'inactive schedule' do
- before do
- pipeline_schedule.update(active: false)
+ context 'when the schedule is not runnable by the user' do
+ it 'deactivates the schedule' do
+ subject
+
+ expect(pipeline_schedule.reload.active).to be_falsy
end
- it 'does not creates a new pipeline' do
+ it 'does not schedule a pipeline' do
expect { subject }.not_to change { project.pipelines.count }
end
end
diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb
index 0260416dbe2..f4bc63bcc6a 100644
--- a/spec/workers/post_receive_spec.rb
+++ b/spec/workers/post_receive_spec.rb
@@ -4,13 +4,16 @@ describe PostReceive do
let(:changes) { "123456 789012 refs/heads/tést\n654321 210987 refs/tags/tag" }
let(:wrongly_encoded_changes) { changes.encode("ISO-8859-1").force_encoding("UTF-8") }
let(:base64_changes) { Base64.encode64(wrongly_encoded_changes) }
- let(:project) { create(:project, :repository) }
let(:project_identifier) { "project-#{project.id}" }
let(:key) { create(:key, user: project.owner) }
let(:key_id) { key.shell_id }
- context "as a resque worker" do
- it "reponds to #perform" do
+ let(:project) do
+ create(:project, :repository, auto_cancel_pending_pipelines: 'disabled')
+ end
+
+ context "as a sidekiq worker" do
+ it "responds to #perform" do
expect(described_class.new).to respond_to(:perform)
end
end
@@ -93,6 +96,27 @@ describe PostReceive do
end
end
+ describe '#process_repository_update' do
+ let(:changes) {'123456 789012 refs/heads/tést'}
+ let(:fake_hook_data) do
+ { event_name: 'repository_update' }
+ end
+
+ before do
+ allow_any_instance_of(Gitlab::GitPostReceive).to receive(:identify).and_return(project.owner)
+ allow_any_instance_of(Gitlab::DataBuilder::Repository).to receive(:update).and_return(fake_hook_data)
+ # silence hooks so we can isolate
+ allow_any_instance_of(Key).to receive(:post_create_hook).and_return(true)
+ allow(subject).to receive(:process_project_changes).and_return(true)
+ end
+
+ it 'calls SystemHooksService' do
+ expect_any_instance_of(SystemHooksService).to receive(:execute_hooks).with(fake_hook_data, :repository_update_hooks).and_return(true)
+
+ subject.perform(pwd(project), key_id, base64_changes)
+ end
+ end
+
context "webhook" do
it "fetches the correct project" do
expect(Project).to receive(:find_by).with(id: project.id.to_s)
diff --git a/spec/workers/project_cache_worker_spec.rb b/spec/workers/project_cache_worker_spec.rb
index c23ffdf99c0..a4ba5f7c943 100644
--- a/spec/workers/project_cache_worker_spec.rb
+++ b/spec/workers/project_cache_worker_spec.rb
@@ -45,6 +45,18 @@ describe ProjectCacheWorker do
worker.perform(project.id, %w(readme))
end
+
+ context 'with plain readme' do
+ it 'refreshes the method caches' do
+ allow(MarkupHelper).to receive(:gitlab_markdown?).and_return(false)
+ allow(MarkupHelper).to receive(:plain?).and_return(true)
+
+ expect_any_instance_of(Repository).to receive(:refresh_method_caches).
+ with(%i(readme)).
+ and_call_original
+ worker.perform(project.id, %w(readme))
+ end
+ end
end
end
diff --git a/spec/workers/repository_check/clear_worker_spec.rb b/spec/workers/repository_check/clear_worker_spec.rb
index a3b70c74787..3b1a64c5057 100644
--- a/spec/workers/repository_check/clear_worker_spec.rb
+++ b/spec/workers/repository_check/clear_worker_spec.rb
@@ -5,7 +5,7 @@ describe RepositoryCheck::ClearWorker do
project = create(:empty_project)
project.update_columns(
last_repository_check_failed: true,
- last_repository_check_at: Time.now,
+ last_repository_check_at: Time.now
)
described_class.new.perform