summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.eslintrc.yml1
-rw-r--r--.gitlab-ci.yml9
-rw-r--r--.gitlab/CODEOWNERS.disabled4
-rw-r--r--.gitlab/issue_templates/Feature proposal.md12
-rw-r--r--.rubocop_todo.yml6
-rw-r--r--CHANGELOG.md14
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--GITLAB_WORKHORSE_VERSION2
-rw-r--r--Gemfile8
-rw-r--r--Gemfile.lock17
-rw-r--r--Gemfile.rails4.lock35
-rw-r--r--app/assets/javascripts/api.js30
-rw-r--r--app/assets/javascripts/behaviors/markdown/render_gfm.js2
-rw-r--r--app/assets/javascripts/behaviors/requires_input.js7
-rw-r--r--app/assets/javascripts/boards/components/issue_due_date.vue20
-rw-r--r--app/assets/javascripts/boards/components/project_select.vue15
-rw-r--r--app/assets/javascripts/clusters/clusters_bundle.js10
-rw-r--r--app/assets/javascripts/clusters/components/applications.vue87
-rw-r--r--app/assets/javascripts/clusters/constants.js1
-rw-r--r--app/assets/javascripts/clusters/stores/clusters_store.js6
-rw-r--r--app/assets/javascripts/diffs/components/app.vue18
-rw-r--r--app/assets/javascripts/diffs/components/diff_content.vue2
-rw-r--r--app/assets/javascripts/diffs/components/diff_file.vue17
-rw-r--r--app/assets/javascripts/diffs/components/no_changes.vue53
-rw-r--r--app/assets/javascripts/diffs/index.js2
-rw-r--r--app/assets/javascripts/diffs/store/actions.js31
-rw-r--r--app/assets/javascripts/diffs/store/mutations.js47
-rw-r--r--app/assets/javascripts/diffs/store/utils.js9
-rw-r--r--app/assets/javascripts/dismissable_callout.js27
-rw-r--r--app/assets/javascripts/environments/components/environments_table.vue2
-rw-r--r--app/assets/javascripts/ide/constants.js1
-rw-r--r--app/assets/javascripts/ide/index.js10
-rw-r--r--app/assets/javascripts/image_diff/helpers/badge_helper.js2
-rw-r--r--app/assets/javascripts/issuable_suggestions/components/app.vue2
-rw-r--r--app/assets/javascripts/jobs/components/trigger_block.vue62
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js6
-rw-r--r--app/assets/javascripts/lib/utils/dom_utils.js5
-rw-r--r--app/assets/javascripts/lib/utils/file_upload.js13
-rw-r--r--app/assets/javascripts/lib/utils/http_status.js2
-rw-r--r--app/assets/javascripts/lib/utils/users_cache.js28
-rw-r--r--app/assets/javascripts/main.js2
-rw-r--r--app/assets/javascripts/milestone_select.js8
-rw-r--r--app/assets/javascripts/monitoring/components/charts/area.vue97
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard.vue10
-rw-r--r--app/assets/javascripts/notes/components/comment_form.vue16
-rw-r--r--app/assets/javascripts/notes/components/note_edited_text.vue5
-rw-r--r--app/assets/javascripts/notes/components/note_header.vue9
-rw-r--r--app/assets/javascripts/notes/components/noteable_discussion.vue19
-rw-r--r--app/assets/javascripts/notes/components/notes_app.vue8
-rw-r--r--app/assets/javascripts/notes/mixins/discussion_navigation.js53
-rw-r--r--app/assets/javascripts/notes/stores/actions.js8
-rw-r--r--app/assets/javascripts/notes/stores/getters.js13
-rw-r--r--app/assets/javascripts/notes/stores/mutations.js3
-rw-r--r--app/assets/javascripts/notifications_dropdown.js4
-rw-r--r--app/assets/javascripts/pages/dashboard/projects/index.js6
-rw-r--r--app/assets/javascripts/pages/groups/clusters/index/index.js6
-rw-r--r--app/assets/javascripts/pages/groups/index.js10
-rw-r--r--app/assets/javascripts/pages/profiles/show/emoji_menu.js1
-rw-r--r--app/assets/javascripts/pages/projects/clusters/index/index.js6
-rw-r--r--app/assets/javascripts/pages/projects/edit/index.js4
-rw-r--r--app/assets/javascripts/pages/projects/index.js6
-rw-r--r--app/assets/javascripts/pages/projects/project.js9
-rw-r--r--app/assets/javascripts/pages/projects/serverless/index.js5
-rw-r--r--app/assets/javascripts/pages/projects/settings/repository/form.js2
-rw-r--r--app/assets/javascripts/pages/projects/shared/project_avatar.js16
-rw-r--r--app/assets/javascripts/pages/users/user_tabs.js8
-rw-r--r--app/assets/javascripts/persistent_user_callout.js34
-rw-r--r--app/assets/javascripts/registry/components/app.vue12
-rw-r--r--app/assets/javascripts/registry/components/collapsible_container.vue61
-rw-r--r--app/assets/javascripts/registry/components/table_registry.vue56
-rw-r--r--app/assets/javascripts/registry/stores/actions.js36
-rw-r--r--app/assets/javascripts/registry/stores/index.js28
-rw-r--r--app/assets/javascripts/registry/stores/mutations.js1
-rw-r--r--app/assets/javascripts/registry/stores/state.js26
-rw-r--r--app/assets/javascripts/serverless/components/empty_state.vue40
-rw-r--r--app/assets/javascripts/serverless/components/function_row.vue40
-rw-r--r--app/assets/javascripts/serverless/components/functions.vue123
-rw-r--r--app/assets/javascripts/serverless/event_hub.js3
-rw-r--r--app/assets/javascripts/serverless/serverless_bundle.js106
-rw-r--r--app/assets/javascripts/serverless/services/get_functions_service.js11
-rw-r--r--app/assets/javascripts/serverless/stores/serverless_store.js24
-rw-r--r--app/assets/javascripts/star.js15
-rw-r--r--app/assets/javascripts/terminal/index.js2
-rw-r--r--app/assets/javascripts/terminal/terminal.js57
-rw-r--r--app/assets/javascripts/user_popovers.js107
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_icon.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue23
-rw-r--r--app/assets/javascripts/vue_shared/components/diff_viewer/viewers/mode_changed.vue30
-rw-r--r--app/assets/javascripts/vue_shared/components/diff_viewer/viewers/renamed.vue3
-rw-r--r--app/assets/javascripts/vue_shared/components/issue/issue_assignees.vue94
-rw-r--r--app/assets/javascripts/vue_shared/components/issue/issue_milestone.vue90
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/field.vue15
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue3
-rw-r--r--app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue104
-rw-r--r--app/assets/stylesheets/application.scss5
-rw-r--r--app/assets/stylesheets/bootstrap_migration.scss6
-rw-r--r--app/assets/stylesheets/components/popover.scss9
-rw-r--r--app/assets/stylesheets/csslab.scss1
-rw-r--r--app/assets/stylesheets/framework.scss1
-rw-r--r--app/assets/stylesheets/framework/avatar.scss4
-rw-r--r--app/assets/stylesheets/framework/buttons.scss10
-rw-r--r--app/assets/stylesheets/framework/common.scss15
-rw-r--r--app/assets/stylesheets/framework/contextual_sidebar.scss2
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss33
-rw-r--r--app/assets/stylesheets/framework/files.scss2
-rw-r--r--app/assets/stylesheets/framework/flex_grid.scss52
-rw-r--r--app/assets/stylesheets/framework/gitlab_theme.scss10
-rw-r--r--app/assets/stylesheets/framework/header.scss31
-rw-r--r--app/assets/stylesheets/framework/markdown_area.scss2
-rw-r--r--app/assets/stylesheets/framework/mobile.scss9
-rw-r--r--app/assets/stylesheets/framework/typography.scss4
-rw-r--r--app/assets/stylesheets/framework/variables.scss14
-rw-r--r--app/assets/stylesheets/framework/variables_overrides.scss8
-rw-r--r--app/assets/stylesheets/page_bundles/_ide_mixins.scss18
-rw-r--r--app/assets/stylesheets/page_bundles/ide.scss17
-rw-r--r--app/assets/stylesheets/pages/boards.scss2
-rw-r--r--app/assets/stylesheets/pages/builds.scss17
-rw-r--r--app/assets/stylesheets/pages/diff.scss1
-rw-r--r--app/assets/stylesheets/pages/issues.scss10
-rw-r--r--app/assets/stylesheets/pages/note_form.scss14
-rw-r--r--app/assets/stylesheets/pages/notes.scss47
-rw-r--r--app/assets/stylesheets/pages/profile.scss7
-rw-r--r--app/assets/stylesheets/pages/projects.scss389
-rw-r--r--app/assets/stylesheets/pages/search.scss2
-rw-r--r--app/assets/stylesheets/pages/wiki.scss2
-rw-r--r--app/controllers/application_controller.rb8
-rw-r--r--app/controllers/clusters/applications_controller.rb2
-rw-r--r--app/controllers/clusters/clusters_controller.rb12
-rw-r--r--app/controllers/concerns/issuable_collections.rb10
-rw-r--r--app/controllers/concerns/renders_commits.rb6
-rw-r--r--app/controllers/concerns/snippets_actions.rb2
-rw-r--r--app/controllers/concerns/uploads_actions.rb1
-rw-r--r--app/controllers/dashboard/projects_controller.rb2
-rw-r--r--app/controllers/explore/projects_controller.rb2
-rw-r--r--app/controllers/import/github_controller.rb2
-rw-r--r--app/controllers/notification_settings_controller.rb10
-rw-r--r--app/controllers/projects/commits_controller.rb5
-rw-r--r--app/controllers/projects/compare_controller.rb6
-rw-r--r--app/controllers/projects/environments_controller.rb4
-rw-r--r--app/controllers/projects/jobs_controller.rb18
-rw-r--r--app/controllers/projects/merge_requests_controller.rb12
-rw-r--r--app/controllers/projects/serverless/functions_controller.rb37
-rw-r--r--app/controllers/projects/settings/repository_controller.rb19
-rw-r--r--app/controllers/users_controller.rb4
-rw-r--r--app/finders/issuable_finder.rb42
-rw-r--r--app/finders/projects/serverless/functions_finder.rb31
-rw-r--r--app/finders/remote_mirror_finder.rb15
-rw-r--r--app/helpers/appearances_helper.rb7
-rw-r--r--app/helpers/blob_helper.rb4
-rw-r--r--app/helpers/button_helper.rb5
-rw-r--r--app/helpers/dropdowns_helper.rb9
-rw-r--r--app/helpers/emails_helper.rb25
-rw-r--r--app/helpers/events_helper.rb4
-rw-r--r--app/helpers/icons_helper.rb2
-rw-r--r--app/helpers/ide_helper.rb16
-rw-r--r--app/helpers/issuables_helper.rb2
-rw-r--r--app/helpers/nav_helper.rb4
-rw-r--r--app/helpers/projects_helper.rb34
-rw-r--r--app/helpers/selects_helper.rb5
-rw-r--r--app/helpers/sentry_helper.rb11
-rw-r--r--app/helpers/sorting_helper.rb47
-rw-r--r--app/helpers/visibility_level_helper.rb2
-rw-r--r--app/helpers/workhorse_helper.rb9
-rw-r--r--app/mailers/emails/projects.rb15
-rw-r--r--app/mailers/emails/remote_mirrors.rb12
-rw-r--r--app/mailers/notify.rb9
-rw-r--r--app/mailers/previews/notify_preview.rb8
-rw-r--r--app/models/broadcast_message.rb42
-rw-r--r--app/models/ci/bridge.rb34
-rw-r--r--app/models/ci/build.rb4
-rw-r--r--app/models/ci/pipeline.rb19
-rw-r--r--app/models/clusters/applications/cert_manager.rb4
-rw-r--r--app/models/clusters/applications/knative.rb36
-rw-r--r--app/models/clusters/applications/runner.rb2
-rw-r--r--app/models/clusters/cluster.rb10
-rw-r--r--app/models/commit.rb4
-rw-r--r--app/models/concerns/awardable.rb13
-rw-r--r--app/models/concerns/discussion_on_diff.rb5
-rw-r--r--app/models/concerns/fast_destroy_all.rb5
-rw-r--r--app/models/concerns/issuable.rb28
-rw-r--r--app/models/concerns/storage/legacy_namespace.rb2
-rw-r--r--app/models/concerns/with_uploads.rb31
-rw-r--r--app/models/event.rb2
-rw-r--r--app/models/member.rb19
-rw-r--r--app/models/members/project_member.rb2
-rw-r--r--app/models/merge_request.rb84
-rw-r--r--app/models/namespace.rb1
-rw-r--r--app/models/note.rb2
-rw-r--r--app/models/pool_repository.rb81
-rw-r--r--app/models/project.rb66
-rw-r--r--app/models/project_import_data.rb8
-rw-r--r--app/models/protected_branch.rb1
-rw-r--r--app/models/protected_tag.rb1
-rw-r--r--app/models/remote_mirror.rb8
-rw-r--r--app/models/repository.rb1
-rw-r--r--app/models/shard.rb4
-rw-r--r--app/models/upload.rb19
-rw-r--r--app/models/uploads/base.rb19
-rw-r--r--app/models/uploads/fog.rb43
-rw-r--r--app/models/uploads/local.rb56
-rw-r--r--app/presenters/group_clusterable_presenter.rb2
-rw-r--r--app/presenters/member_presenter.rb8
-rw-r--r--app/presenters/project_presenter.rb140
-rw-r--r--app/serializers/cluster_application_entity.rb1
-rw-r--r--app/serializers/diff_file_base_entity.rb101
-rw-r--r--app/serializers/diff_file_entity.rb97
-rw-r--r--app/serializers/discussion_diff_file_entity.rb4
-rw-r--r--app/serializers/discussion_entity.rb15
-rw-r--r--app/serializers/projects/serverless/service_entity.rb33
-rw-r--r--app/serializers/projects/serverless/service_serializer.rb9
-rw-r--r--app/serializers/trigger_variable_entity.rb3
-rw-r--r--app/services/ci/compare_reports_base_service.rb47
-rw-r--r--app/services/ci/compare_test_reports_service.rb36
-rw-r--r--app/services/clusters/applications/create_service.rb2
-rw-r--r--app/services/clusters/build_service.rb21
-rw-r--r--app/services/clusters/gcp/fetch_operation_service.rb13
-rw-r--r--app/services/clusters/gcp/finalize_creation_service.rb16
-rw-r--r--app/services/merge_requests/refresh_service.rb22
-rw-r--r--app/services/notification_recipient_service.rb23
-rw-r--r--app/services/notification_service.rb40
-rw-r--r--app/services/projects/cleanup_service.rb52
-rw-r--r--app/services/projects/fork_service.rb2
-rw-r--r--app/validators/url_validator.rb1
-rw-r--r--app/views/admin/dashboard/index.html.haml2
-rw-r--r--app/views/admin/groups/show.html.haml2
-rw-r--r--app/views/admin/hooks/edit.html.haml2
-rw-r--r--app/views/admin/hooks/index.html.haml2
-rw-r--r--app/views/clusters/clusters/_gcp_signup_offer_banner.html.haml4
-rw-r--r--app/views/dashboard/activity.html.haml3
-rw-r--r--app/views/dashboard/groups/index.html.haml2
-rw-r--r--app/views/dashboard/issues.html.haml2
-rw-r--r--app/views/dashboard/merge_requests.html.haml2
-rw-r--r--app/views/dashboard/projects/index.html.haml2
-rw-r--r--app/views/dashboard/projects/starred.html.haml2
-rw-r--r--app/views/dashboard/todos/index.html.haml2
-rw-r--r--app/views/errors/access_denied.html.haml2
-rw-r--r--app/views/explore/groups/index.html.haml2
-rw-r--r--app/views/explore/projects/index.html.haml2
-rw-r--r--app/views/explore/projects/starred.html.haml2
-rw-r--r--app/views/explore/projects/trending.html.haml2
-rw-r--r--app/views/groups/_home_panel.html.haml2
-rw-r--r--app/views/groups/group_members/_new_group_member.html.haml2
-rw-r--r--app/views/ide/_show.html.haml10
-rw-r--r--app/views/ide/index.html.haml18
-rw-r--r--app/views/layouts/_head.html.haml1
-rw-r--r--app/views/layouts/header/_default.html.haml2
-rw-r--r--app/views/layouts/nav/_dashboard.html.haml35
-rw-r--r--app/views/layouts/nav/sidebar/_project.html.haml10
-rw-r--r--app/views/notify/_note_email.html.haml23
-rw-r--r--app/views/notify/_note_email.text.erb13
-rw-r--r--app/views/notify/remote_mirror_update_failed_email.html.haml46
-rw-r--r--app/views/notify/remote_mirror_update_failed_email.text.erb7
-rw-r--r--app/views/notify/repository_cleanup_failure_email.text.erb3
-rw-r--r--app/views/notify/repository_cleanup_success_email.text.erb3
-rw-r--r--app/views/projects/_files.html.haml6
-rw-r--r--app/views/projects/_home_panel.html.haml130
-rw-r--r--app/views/projects/_stat_anchor_list.html.haml4
-rw-r--r--app/views/projects/blob/_blob.html.haml2
-rw-r--r--app/views/projects/blob/preview.html.haml2
-rw-r--r--app/views/projects/blob/viewers/_markup.html.haml2
-rw-r--r--app/views/projects/buttons/_clone.html.haml31
-rw-r--r--app/views/projects/buttons/_download.html.haml2
-rw-r--r--app/views/projects/buttons/_fork.html.haml6
-rw-r--r--app/views/projects/buttons/_notifications.html.haml27
-rw-r--r--app/views/projects/buttons/_star.html.haml12
-rw-r--r--app/views/projects/cleanup/_show.html.haml31
-rw-r--r--app/views/projects/commit/_commit_box.html.haml6
-rw-r--r--app/views/projects/edit.html.haml2
-rw-r--r--app/views/projects/empty.html.haml122
-rw-r--r--app/views/projects/environments/index.html.haml1
-rw-r--r--app/views/projects/labels/index.html.haml2
-rw-r--r--app/views/projects/merge_requests/show.html.haml3
-rw-r--r--app/views/projects/mirrors/_authentication_method.html.haml4
-rw-r--r--app/views/projects/mirrors/_mirror_repos.html.haml12
-rw-r--r--app/views/projects/mirrors/_mirror_repos_form.html.haml2
-rw-r--r--app/views/projects/project_members/_new_project_group.html.haml2
-rw-r--r--app/views/projects/project_members/_new_project_member.html.haml2
-rw-r--r--app/views/projects/serverless/functions/index.html.haml15
-rw-r--r--app/views/projects/settings/repository/show.html.haml1
-rw-r--r--app/views/projects/show.html.haml16
-rw-r--r--app/views/projects/snippets/show.html.haml2
-rw-r--r--app/views/projects/tree/_tree_header.html.haml8
-rw-r--r--app/views/projects/wikis/show.html.haml2
-rw-r--r--app/views/search/results/_blob.html.haml6
-rw-r--r--app/views/search/results/_wiki_blob.html.haml4
-rw-r--r--app/views/shared/_mobile_clone_panel.html.haml12
-rw-r--r--app/views/shared/_remote_mirror_update_button.html.haml2
-rw-r--r--app/views/shared/_sort_dropdown.html.haml16
-rw-r--r--app/views/shared/issuable/_filter.html.haml32
-rw-r--r--app/views/shared/issuable/_search_bar.html.haml5
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml2
-rw-r--r--app/views/shared/issuable/_sort_dropdown.html.haml20
-rw-r--r--app/views/shared/issuable/form/_metadata_merge_request_assignee.html.haml2
-rw-r--r--app/views/shared/members/_access_request_links.html.haml17
-rw-r--r--app/views/shared/members/_member.html.haml2
-rw-r--r--app/views/shared/notes/_note.html.haml2
-rw-r--r--app/views/shared/notifications/_button.html.haml6
-rw-r--r--app/views/shared/projects/_list.html.haml13
-rw-r--r--app/views/shared/projects/_project.html.haml141
-rw-r--r--app/views/users/show.html.haml12
-rw-r--r--app/workers/all_queues.yml8
-rw-r--r--app/workers/concerns/object_pool_queue.rb12
-rw-r--r--app/workers/delete_stored_files_worker.rb22
-rw-r--r--app/workers/git_garbage_collect_worker.rb2
-rw-r--r--app/workers/new_note_worker.rb9
-rw-r--r--app/workers/object_pool/create_worker.rb44
-rw-r--r--app/workers/object_pool/join_worker.rb20
-rw-r--r--app/workers/object_pool/schedule_join_worker.rb19
-rw-r--r--app/workers/remote_mirror_notification_worker.rb15
-rw-r--r--app/workers/remove_old_web_hook_logs_worker.rb14
-rw-r--r--app/workers/repository_cleanup_worker.rb39
-rw-r--r--app/workers/repository_update_remote_mirror_worker.rb3
-rw-r--r--changelogs/unreleased/19376-post-bfg-cleanup.yml5
-rw-r--r--changelogs/unreleased/20422-hide-ui-variables-by-default.yml6
-rw-r--r--changelogs/unreleased/22548-reopen-error-message.yml6
-rw-r--r--changelogs/unreleased/28682-can-merge-branch-before-build-is-started.yml5
-rw-r--r--changelogs/unreleased/39849_controller_sorts.yml5
-rw-r--r--changelogs/unreleased/48889-populate-merge_commit_sha.yml6
-rw-r--r--changelogs/unreleased/49713-main-navbar-is-broken-in-certain-viewport-widths.yml5
-rw-r--r--changelogs/unreleased/50157-extended-user-centric-tooltips.yml5
-rw-r--r--changelogs/unreleased/51101-can-add-an-existing-group-member-into-a-group-project-with-new-permissions-but-permissions-are-not-overridde.yml5
-rw-r--r--changelogs/unreleased/51122-fix-navigating-discussions.yml5
-rw-r--r--changelogs/unreleased/51138-54026-breadcrumb-subgroups-ellipsis.yml5
-rw-r--r--changelogs/unreleased/51243-further-improvements-to-project-overview-ui.yml5
-rw-r--r--changelogs/unreleased/51944-redesign-project-lists-ui.yml5
-rw-r--r--changelogs/unreleased/52007-frontmatter-toml-json.yml5
-rw-r--r--changelogs/unreleased/52285-omniauth-jwt-ppk-support.yml5
-rw-r--r--changelogs/unreleased/52774-fix-svgs-in-ie-11.yml5
-rw-r--r--changelogs/unreleased/53493-list-id-email-header.yml5
-rw-r--r--changelogs/unreleased/53994-add-missing-ci_builds-partial-indices.yml5
-rw-r--r--changelogs/unreleased/54160-use-reports-syntax-for-sast-in-auto-devops.yml5
-rw-r--r--changelogs/unreleased/54626-able-to-download-a-single-archive-file-with-api-by-ref-name.yml5
-rw-r--r--changelogs/unreleased/54857-fix-templates-path-traversal.yml5
-rw-r--r--changelogs/unreleased/54975-fix-web-hooks-rake-task.yml5
-rw-r--r--changelogs/unreleased/55104-frozenerror-can-t-modify-frozen-string.yml5
-rw-r--r--changelogs/unreleased/55116-runtimeerror-can-t-modify-frozen-string.yml5
-rw-r--r--changelogs/unreleased/55138-fix-mr-discussions-count.yml5
-rw-r--r--changelogs/unreleased/55183-frozenerror-can-t-modify-frozen-string-in-app-mailers-notify-rb.yml5
-rw-r--r--changelogs/unreleased/55191-update-workhorse.yml5
-rw-r--r--changelogs/unreleased/cert-manager-email.yml5
-rw-r--r--changelogs/unreleased/commit-badge-style-fix.yml5
-rw-r--r--changelogs/unreleased/define-default-value-for-only-except-keys.yml2
-rw-r--r--changelogs/unreleased/deprecated-instance-find.yml5
-rw-r--r--changelogs/unreleased/diff-empty-state-fixes.yml5
-rw-r--r--changelogs/unreleased/dm-remove-prune-web-hook-logs-worker.yml5
-rw-r--r--changelogs/unreleased/expose-mr-pipeline-variables.yml5
-rw-r--r--changelogs/unreleased/fix-calendar-events-fetching-error.yml5
-rw-r--r--changelogs/unreleased/fix-gb-encrypt-ci-build-token.yml5
-rw-r--r--changelogs/unreleased/fix-n-plus-1-queries-projects.yml6
-rw-r--r--changelogs/unreleased/fj-clean-content-headers.yml5
-rw-r--r--changelogs/unreleased/gt-add-top-padding-for-nested-environment-items-loading-icon.yml5
-rw-r--r--changelogs/unreleased/gt-remove-unnecessary-line-before-reply-holder.yml5
-rw-r--r--changelogs/unreleased/gt-show-primary-button-when-all-labels-are-prioritized.yml5
-rw-r--r--changelogs/unreleased/gt-update-environment-breadcrumb.yml5
-rw-r--r--changelogs/unreleased/gt-update-navigation-theme-colors.yml5
-rw-r--r--changelogs/unreleased/mg-fix-knative-application-row.yml5
-rw-r--r--changelogs/unreleased/move-group-issues-search-cte-up-the-chain.yml5
-rw-r--r--changelogs/unreleased/osw-remove-unnused-data-from-diff-discussions.yml5
-rw-r--r--changelogs/unreleased/osw-update-mr-metrics-with-events-data.yml5
-rw-r--r--changelogs/unreleased/profile-fixing.yml5
-rw-r--r--changelogs/unreleased/remote-mirror-update-failed-notification.yml5
-rw-r--r--changelogs/unreleased/remove-blob-search-limit.yml5
-rw-r--r--changelogs/unreleased/sh-fix-github-import-without-oauth2-config.yml5
-rw-r--r--changelogs/unreleased/sh-handle-invalid-gpg-sig.yml5
-rw-r--r--changelogs/unreleased/sh-ignore-arrays-url-sanitizer.yml5
-rw-r--r--changelogs/unreleased/sh-json-serialize-broadcast-messages.yml5
-rw-r--r--changelogs/unreleased/sh-truncate-with-periods.yml5
-rw-r--r--changelogs/unreleased/store-correlation-logs.yml5
-rw-r--r--changelogs/unreleased/tc-backfill-hashed-project_repositories.yml5
-rw-r--r--changelogs/unreleased/triggermesh-phase2-serverless-list.yml5
-rw-r--r--changelogs/unreleased/update-gitlab-runner-helm-chart-to-0-1-39.yml5
-rw-r--r--changelogs/unreleased/upgrade-to-workhorse-7-6-0.yml5
-rw-r--r--changelogs/unreleased/usage-count.yml5
-rw-r--r--changelogs/unreleased/winh-dropdown-divider-color.yml5
-rw-r--r--changelogs/unreleased/winh-dropdown-item-padding.yml5
-rw-r--r--changelogs/unreleased/winh-issue-boards-project-dropdown-close.yml5
-rw-r--r--changelogs/unreleased/winh-markdown-preview-lists.yml5
-rw-r--r--changelogs/unreleased/winh-merge-request-diff-discussion-commit-id.yml5
-rw-r--r--changelogs/unreleased/winh-milestone-select.yml5
-rw-r--r--changelogs/unreleased/winh-princess-mononospace.yml5
-rw-r--r--changelogs/unreleased/zj-backup-restore-object-pools.yml5
-rw-r--r--changelogs/unreleased/zj-pool-repository-creation.yml5
-rw-r--r--config/application.rb1
-rw-r--r--config/dependency_decisions.yml11
-rw-r--r--config/gitlab.yml.example16
-rw-r--r--config/initializers/1_settings.rb4
-rw-r--r--config/initializers/correlation_id.rb3
-rw-r--r--config/initializers/lograge.rb1
-rw-r--r--config/initializers/postgresql_opclasses_support.rb2
-rw-r--r--config/initializers/sentry.rb2
-rw-r--r--config/initializers/sidekiq.rb5
-rw-r--r--config/routes/project.rb5
-rw-r--r--config/sidekiq_queues.yml4
-rw-r--r--danger/documentation/Dangerfile19
-rw-r--r--db/fixtures/development/04_project.rb14
-rw-r--r--db/fixtures/development/10_merge_requests.rb12
-rw-r--r--db/fixtures/development/24_forks.rb16
-rw-r--r--db/fixtures/production/001_application_settings.rb2
-rw-r--r--db/fixtures/production/002_admin.rb (renamed from db/fixtures/production/001_admin.rb)0
-rw-r--r--db/migrate/20181101191341_create_clusters_applications_cert_manager.rb2
-rw-r--r--db/migrate/20181108091549_cleanup_environments_external_url.rb2
-rw-r--r--db/migrate/20181115140140_add_encrypted_runners_token_to_settings.rb2
-rw-r--r--db/migrate/20181116050532_knative_external_ip.rb2
-rw-r--r--db/migrate/20181116141415_add_encrypted_runners_token_to_namespaces.rb2
-rw-r--r--db/migrate/20181116141504_add_encrypted_runners_token_to_projects.rb2
-rw-r--r--db/migrate/20181119081539_add_merge_request_id_to_ci_pipelines.rb2
-rw-r--r--db/migrate/20181120091639_add_foreign_key_to_ci_pipelines_merge_requests.rb6
-rw-r--r--db/migrate/20181120151656_add_token_encrypted_to_ci_runners.rb2
-rw-r--r--db/migrate/20181121101842_add_ci_builds_partial_index_on_project_id_and_status.rb33
-rw-r--r--db/migrate/20181121101843_remove_redundant_ci_builds_partial_index.rb33
-rw-r--r--db/migrate/20181128123704_add_state_to_pool_repository.rb19
-rw-r--r--db/migrate/20181129104854_add_token_encrypted_to_ci_builds.rb11
-rw-r--r--db/migrate/20181129104944_add_index_to_ci_builds_token_encrypted.rb17
-rw-r--r--db/migrate/20181203002526_add_project_bfg_object_map_column.rb9
-rw-r--r--db/post_migrate/20161221153951_rename_reserved_project_names.rb1
-rw-r--r--db/post_migrate/20170313133418_rename_more_reserved_project_names.rb1
-rw-r--r--db/post_migrate/20181010133639_backfill_store_project_full_path_in_repo.rb2
-rw-r--r--db/post_migrate/20181026091631_migrate_forbidden_redirect_uris.rb2
-rw-r--r--db/post_migrate/20181121111200_schedule_runners_token_encryption.rb2
-rw-r--r--db/post_migrate/20181130102132_backfill_hashed_project_repositories.rb26
-rw-r--r--db/post_migrate/20181204154019_populate_mr_metrics_with_events_data.rb38
-rw-r--r--db/schema.rb12
-rw-r--r--doc/administration/auth/README.md2
-rw-r--r--doc/administration/auth/jwt.md36
-rw-r--r--doc/administration/gitaly/index.md19
-rw-r--r--doc/administration/repository_storage_paths.md19
-rw-r--r--doc/administration/repository_storage_types.md17
-rw-r--r--doc/api/README.md96
-rw-r--r--doc/api/jobs.md37
-rw-r--r--doc/api/merge_requests.md5
-rw-r--r--doc/api/milestones.md3
-rw-r--r--doc/api/search.md28
-rw-r--r--doc/ci/README.md1
-rw-r--r--doc/ci/caching/index.md16
-rw-r--r--doc/ci/interactive_web_terminal/index.md8
-rw-r--r--doc/ci/merge_request_pipelines/img/merge_request.pngbin0 -> 57512 bytes
-rw-r--r--doc/ci/merge_request_pipelines/img/pipeline_detail.pngbin0 -> 42583 bytes
-rw-r--r--doc/ci/merge_request_pipelines/index.md84
-rw-r--r--doc/ci/triggers/README.md3
-rw-r--r--doc/ci/triggers/img/trigger_variables.pngbin3637 -> 30193 bytes
-rw-r--r--doc/ci/variables/README.md162
-rw-r--r--doc/ci/yaml/README.md81
-rw-r--r--doc/development/automatic_ce_ee_merge.md260
-rw-r--r--doc/development/background_migrations.md4
-rw-r--r--doc/development/code_review.md4
-rw-r--r--doc/development/documentation/index.md24
-rw-r--r--doc/development/documentation/site_architecture/global_nav.md342
-rw-r--r--doc/development/documentation/site_architecture/index.md59
-rw-r--r--doc/development/fe_guide/vuex.md6
-rw-r--r--doc/development/feature_flags.md10
-rw-r--r--doc/development/migration_style_guide.md16
-rw-r--r--doc/development/profiling.md7
-rw-r--r--doc/development/prometheus_metrics.md2
-rw-r--r--doc/development/sql.md4
-rw-r--r--doc/development/testing_guide/best_practices.md10
-rw-r--r--doc/development/testing_guide/ci.md4
-rw-r--r--doc/development/testing_guide/review_apps.md35
-rw-r--r--doc/development/what_requires_downtime.md14
-rw-r--r--doc/install/README.md103
-rw-r--r--doc/install/docker.md6
-rw-r--r--doc/install/kubernetes/gitlab_chart.md11
-rw-r--r--doc/install/kubernetes/index.md42
-rw-r--r--doc/integration/recaptcha.md6
-rw-r--r--doc/raketasks/backup_restore.md1
-rw-r--r--doc/raketasks/web_hooks.md6
-rw-r--r--doc/security/rack_attack.md3
-rw-r--r--doc/topics/autodevops/index.md9
-rw-r--r--doc/user/group/clusters/index.md126
-rw-r--r--doc/user/group/index.md3
-rw-r--r--doc/user/group/subgroups/index.md1
-rw-r--r--doc/user/project/clusters/index.md37
-rw-r--r--doc/user/project/clusters/serverless/img/install-knative.pngbin102861 -> 31222 bytes
-rw-r--r--doc/user/project/clusters/serverless/img/serverless-page.pngbin0 -> 31743 bytes
-rw-r--r--doc/user/project/issues/img/similar_issues.pngbin0 -> 68153 bytes
-rw-r--r--doc/user/project/issues/index.md4
-rw-r--r--doc/user/project/issues/similar_issues.md16
-rw-r--r--doc/user/project/merge_requests/index.md10
-rw-r--r--doc/user/project/repository/img/repository_cleanup.pngbin0 -> 20833 bytes
-rw-r--r--doc/user/project/repository/reducing_the_repo_size_using_git.md109
-rw-r--r--doc/user/project/settings/import_export.md4
-rw-r--r--doc/user/project/web_ide/index.md12
-rw-r--r--doc/workflow/notifications.md1
-rw-r--r--jest.config.js (renamed from config/jest.config.js)9
-rw-r--r--lib/api/api.rb4
-rw-r--r--lib/api/helpers.rb6
-rw-r--r--lib/api/job_artifacts.rb24
-rw-r--r--lib/api/namespaces.rb17
-rw-r--r--lib/api/search.rb7
-rw-r--r--lib/api/templates.rb2
-rw-r--r--lib/backup/repository.rb16
-rw-r--r--lib/banzai/filter/front_matter_filter.rb34
-rw-r--r--lib/banzai/filter/milestone_reference_filter.rb66
-rw-r--r--lib/banzai/filter/user_reference_filter.rb2
-rw-r--r--lib/banzai/filter/yaml_front_matter_filter.rb27
-rw-r--r--lib/banzai/pipeline/pre_process_pipeline.rb2
-rw-r--r--lib/gitlab/background_migration/backfill_hashed_project_repositories.rb134
-rw-r--r--lib/gitlab/background_migration/populate_merge_request_metrics_with_events_data_improved.rb99
-rw-r--r--lib/gitlab/bitbucket_server_import/importer.rb2
-rw-r--r--lib/gitlab/branch_push_merge_commit_analyzer.rb132
-rw-r--r--lib/gitlab/checks/diff_check.rb3
-rw-r--r--lib/gitlab/ci/build/policy/refs.rb12
-rw-r--r--lib/gitlab/ci/config/entry/except_policy.rb17
-rw-r--r--lib/gitlab/ci/config/entry/job.rb15
-rw-r--r--lib/gitlab/ci/config/entry/only_policy.rb18
-rw-r--r--lib/gitlab/ci/config/entry/policy.rb15
-rw-r--r--lib/gitlab/ci/parsers.rb21
-rw-r--r--lib/gitlab/ci/parsers/parser_error.rb9
-rw-r--r--lib/gitlab/ci/parsers/test.rb21
-rw-r--r--lib/gitlab/ci/parsers/test/junit.rb2
-rw-r--r--lib/gitlab/ci/status/bridge/common.rb27
-rw-r--r--lib/gitlab/ci/status/bridge/factory.rb15
-rw-r--r--lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml3
-rw-r--r--lib/gitlab/correlation_id.rb40
-rw-r--r--lib/gitlab/database/count.rb2
-rw-r--r--lib/gitlab/database/count/exact_count_strategy.rb2
-rw-r--r--lib/gitlab/database/migration_helpers.rb3
-rw-r--r--lib/gitlab/file_finder.rb57
-rw-r--r--lib/gitlab/git/object_pool.rb62
-rw-r--r--lib/gitlab/git/repository.rb7
-rw-r--r--lib/gitlab/git/repository_cleaner.rb28
-rw-r--r--lib/gitlab/gitaly_client.rb1
-rw-r--r--lib/gitlab/gitaly_client/cleanup_service.rb36
-rw-r--r--lib/gitlab/gitaly_client/object_pool_service.rb45
-rw-r--r--lib/gitlab/gpg/commit.rb24
-rw-r--r--lib/gitlab/grape_logging/loggers/correlation_id_logger.rb14
-rw-r--r--lib/gitlab/import_export/import_export.yml2
-rw-r--r--lib/gitlab/import_export/repo_restorer.rb1
-rw-r--r--lib/gitlab/import_sources.rb4
-rw-r--r--lib/gitlab/json_logger.rb1
-rw-r--r--lib/gitlab/middleware/correlation_id.rb35
-rw-r--r--lib/gitlab/project_search_results.rb43
-rw-r--r--lib/gitlab/search/found_blob.rb162
-rw-r--r--lib/gitlab/search/query.rb6
-rw-r--r--lib/gitlab/search_results.rb36
-rw-r--r--lib/gitlab/sentry.rb13
-rw-r--r--lib/gitlab/sidekiq_middleware/correlation_injector.rb14
-rw-r--r--lib/gitlab/sidekiq_middleware/correlation_logger.rb15
-rw-r--r--lib/gitlab/ssh_public_key.rb6
-rw-r--r--lib/gitlab/template/finders/global_template_finder.rb4
-rw-r--r--lib/gitlab/template/finders/repo_template_finder.rb5
-rw-r--r--lib/gitlab/url_blocker.rb9
-rw-r--r--lib/gitlab/url_sanitizer.rb1
-rw-r--r--lib/gitlab/usage_data.rb18
-rw-r--r--lib/gitlab/utils.rb11
-rw-r--r--lib/gitlab/wiki_file_finder.rb6
-rw-r--r--lib/gitlab/workhorse.rb2
-rw-r--r--lib/omni_auth/strategies/jwt.rb17
-rw-r--r--lib/tasks/gitlab/web_hook.rake45
-rw-r--r--locale/gitlab.pot216
-rw-r--r--package.json18
-rw-r--r--qa/qa.rb6
-rw-r--r--qa/qa/page/base.rb24
-rw-r--r--qa/qa/page/component/clone_panel.rb31
-rw-r--r--qa/qa/page/component/legacy_clone_panel.rb52
-rw-r--r--qa/qa/page/merge_request/new.rb8
-rw-r--r--qa/qa/page/merge_request/show.rb7
-rw-r--r--qa/qa/page/project/commit/show.rb27
-rw-r--r--qa/qa/page/project/menu.rb82
-rw-r--r--qa/qa/page/project/settings/mirroring_repositories.rb91
-rw-r--r--qa/qa/page/project/settings/repository.rb10
-rw-r--r--qa/qa/page/project/show.rb82
-rw-r--r--qa/qa/page/project/wiki/show.rb2
-rw-r--r--qa/qa/resource/file.rb2
-rw-r--r--qa/qa/resource/merge_request.rb6
-rw-r--r--qa/qa/resource/project.rb6
-rw-r--r--qa/qa/resource/repository/project_push.rb16
-rw-r--r--qa/qa/runtime/browser.rb7
-rw-r--r--qa/qa/runtime/env.rb5
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/project/view_project_activity_spec.rb10
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/merge_request/create_merge_request_spec.rb4
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/merge_request/squash_merge_request_spec.rb7
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/add_file_template_spec.rb14
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/clone_spec.rb18
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/push_mirroring_over_http_spec.rb45
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb4
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/use_ssh_key_spec.rb3
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/user_views_raw_diff_patch_requests_spec.rb61
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb12
-rw-r--r--qa/qa/support/page/logging.rb20
-rwxr-xr-xscripts/review_apps/review-apps.sh11
-rw-r--r--spec/controllers/application_controller_spec.rb8
-rw-r--r--spec/controllers/groups_controller_spec.rb3
-rw-r--r--spec/controllers/import/github_controller_spec.rb9
-rw-r--r--spec/controllers/projects/avatars_controller_spec.rb35
-rw-r--r--spec/controllers/projects/commits_controller_spec.rb30
-rw-r--r--spec/controllers/projects/issues_controller_spec.rb2
-rw-r--r--spec/controllers/projects/jobs_controller_spec.rb114
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb16
-rw-r--r--spec/controllers/projects/raw_controller_spec.rb74
-rw-r--r--spec/controllers/projects/serverless/functions_controller_spec.rb72
-rw-r--r--spec/controllers/projects/settings/repository_controller_spec.rb33
-rw-r--r--spec/controllers/projects/wikis_controller_spec.rb82
-rw-r--r--spec/controllers/projects_controller_spec.rb2
-rw-r--r--spec/controllers/snippets_controller_spec.rb21
-rw-r--r--spec/factories/ci/bridge.rb17
-rw-r--r--spec/factories/pool_repositories.rb23
-rw-r--r--spec/features/dashboard/merge_requests_spec.rb1
-rw-r--r--spec/features/groups/clusters/user_spec.rb126
-rw-r--r--spec/features/issuables/default_sort_order_spec.rb179
-rw-r--r--spec/features/issuables/sorting_list_spec.rb226
-rw-r--r--spec/features/issues/filtered_search/filter_issues_spec.rb2
-rw-r--r--spec/features/issues/user_sorts_issues_spec.rb8
-rw-r--r--spec/features/merge_request/user_awards_emoji_spec.rb12
-rw-r--r--spec/features/merge_request/user_expands_diff_spec.rb7
-rw-r--r--spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb18
-rw-r--r--spec/features/merge_requests/user_sorts_merge_requests_spec.rb12
-rw-r--r--spec/features/projects/clusters/applications_spec.rb38
-rw-r--r--spec/features/projects/files/user_browses_files_spec.rb1
-rw-r--r--spec/features/projects/jobs_spec.rb87
-rw-r--r--spec/features/projects/labels/issues_sorted_by_priority_spec.rb4
-rw-r--r--spec/features/projects/labels/update_prioritization_spec.rb15
-rw-r--r--spec/features/projects/serverless/functions_spec.rb49
-rw-r--r--spec/features/projects/settings/repository_settings_spec.rb35
-rw-r--r--spec/features/projects/show/developer_views_empty_project_instructions_spec.rb49
-rw-r--r--spec/features/projects/show/user_manages_notifications_spec.rb15
-rw-r--r--spec/features/projects/show/user_sees_collaboration_links_spec.rb23
-rw-r--r--spec/features/projects/show/user_sees_git_instructions_spec.rb4
-rw-r--r--spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb90
-rw-r--r--spec/features/tags/master_views_tags_spec.rb2
-rw-r--r--spec/finders/group_members_finder_spec.rb2
-rw-r--r--spec/finders/issues_finder_spec.rb127
-rw-r--r--spec/finders/projects/serverless/functions_finder_spec.rb60
-rw-r--r--spec/fixtures/api/schemas/cluster_status.json3
-rw-r--r--spec/fixtures/api/schemas/job/trigger.json3
-rw-r--r--spec/fixtures/bfg_object_map.txt1
-rw-r--r--spec/fixtures/security-reports/feature-branch/gl-dependency-scanning-report.json32
-rw-r--r--spec/fixtures/security-reports/master/gl-dependency-scanning-report.json32
-rw-r--r--spec/frontend/.eslintrc.yml2
-rw-r--r--spec/frontend/dummy_spec.js1
-rw-r--r--spec/frontend/helpers/test_constants.js2
-rw-r--r--spec/frontend/pages/profiles/show/emoji_menu_spec.js (renamed from spec/javascripts/pages/profiles/show/emoji_menu_spec.js)6
-rw-r--r--spec/frontend/test_setup.js16
-rw-r--r--spec/frontend/vue_shared/components/notes/timeline_entry_item_spec.js (renamed from spec/javascripts/vue_shared/components/notes/timeline_entry_item_spec.js)0
-rw-r--r--spec/helpers/emails_helper_spec.rb55
-rw-r--r--spec/helpers/events_helper_spec.rb32
-rw-r--r--spec/helpers/projects_helper_spec.rb37
-rw-r--r--spec/helpers/sorting_helper_spec.rb47
-rw-r--r--spec/initializers/lograge_spec.rb38
-rw-r--r--spec/javascripts/api_spec.js51
-rw-r--r--spec/javascripts/boards/mock_data.js69
-rw-r--r--spec/javascripts/clusters/components/applications_spec.js111
-rw-r--r--spec/javascripts/clusters/services/mock_data.js2
-rw-r--r--spec/javascripts/clusters/stores/clusters_store_spec.js1
-rw-r--r--spec/javascripts/diffs/components/app_spec.js91
-rw-r--r--spec/javascripts/diffs/components/diff_file_spec.js26
-rw-r--r--spec/javascripts/diffs/components/no_changes_spec.js41
-rw-r--r--spec/javascripts/diffs/mock_data/diff_discussions.js2
-rw-r--r--spec/javascripts/diffs/store/actions_spec.js93
-rw-r--r--spec/javascripts/diffs/store/mutations_spec.js85
-rw-r--r--spec/javascripts/diffs/store/utils_spec.js26
-rw-r--r--spec/javascripts/image_diff/helpers/badge_helper_spec.js4
-rw-r--r--spec/javascripts/jobs/components/trigger_block_spec.js28
-rw-r--r--spec/javascripts/lib/utils/dom_utils_spec.js54
-rw-r--r--spec/javascripts/lib/utils/file_upload_spec.js36
-rw-r--r--spec/javascripts/lib/utils/users_cache_spec.js110
-rw-r--r--spec/javascripts/notes/components/note_app_spec.js23
-rw-r--r--spec/javascripts/notes/components/note_edited_text_spec.js2
-rw-r--r--spec/javascripts/notes/components/note_header_spec.js3
-rw-r--r--spec/javascripts/notes/components/noteable_discussion_spec.js47
-rw-r--r--spec/javascripts/notes/mock_data.js4
-rw-r--r--spec/javascripts/notes/stores/actions_spec.js2
-rw-r--r--spec/javascripts/notes/stores/mutation_spec.js71
-rw-r--r--spec/javascripts/registry/components/app_spec.js61
-rw-r--r--spec/javascripts/registry/components/collapsible_container_spec.js43
-rw-r--r--spec/javascripts/registry/stores/actions_spec.js50
-rw-r--r--spec/javascripts/user_popovers_spec.js66
-rw-r--r--spec/javascripts/vue_shared/components/diff_viewer/diff_viewer_spec.js26
-rw-r--r--spec/javascripts/vue_shared/components/diff_viewer/viewers/mode_changed_spec.js23
-rw-r--r--spec/javascripts/vue_shared/components/issue/issue_assignees_spec.js114
-rw-r--r--spec/javascripts/vue_shared/components/issue/issue_milestone_spec.js234
-rw-r--r--spec/javascripts/vue_shared/components/markdown/field_spec.js2
-rw-r--r--spec/javascripts/vue_shared/components/user_avatar/user_avatar_image_spec.js13
-rw-r--r--spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js4
-rw-r--r--spec/javascripts/vue_shared/components/user_popover/user_popover_spec.js133
-rw-r--r--spec/lib/backup/repository_spec.rb13
-rw-r--r--spec/lib/banzai/filter/front_matter_filter_spec.rb140
-rw-r--r--spec/lib/banzai/filter/milestone_reference_filter_spec.rb47
-rw-r--r--spec/lib/banzai/filter/user_reference_filter_spec.rb2
-rw-r--r--spec/lib/banzai/filter/yaml_front_matter_filter_spec.rb53
-rw-r--r--spec/lib/gitlab/background_migration/backfill_hashed_project_repositories_spec.rb90
-rw-r--r--spec/lib/gitlab/background_migration/populate_merge_request_metrics_with_events_data_improved_spec.rb57
-rw-r--r--spec/lib/gitlab/branch_push_merge_commit_analyzer_spec.rb62
-rw-r--r--spec/lib/gitlab/ci/config/entry/except_policy_spec.rb15
-rw-r--r--spec/lib/gitlab/ci/config/entry/global_spec.rb6
-rw-r--r--spec/lib/gitlab/ci/config/entry/job_spec.rb3
-rw-r--r--spec/lib/gitlab/ci/config/entry/jobs_spec.rb6
-rw-r--r--spec/lib/gitlab/ci/config/entry/only_policy_spec.rb15
-rw-r--r--spec/lib/gitlab/ci/config/entry/policy_spec.rb167
-rw-r--r--spec/lib/gitlab/ci/parsers_spec.rb (renamed from spec/lib/gitlab/ci/parsers/test_spec.rb)8
-rw-r--r--spec/lib/gitlab/correlation_id_spec.rb77
-rw-r--r--spec/lib/gitlab/database/count/exact_count_strategy_spec.rb6
-rw-r--r--spec/lib/gitlab/git/object_pool_spec.rb89
-rw-r--r--spec/lib/gitlab/git/repository_cleaner_spec.rb32
-rw-r--r--spec/lib/gitlab/gitaly_client/cleanup_service_spec.rb19
-rw-r--r--spec/lib/gitlab/gitaly_client/object_pool_service_spec.rb46
-rw-r--r--spec/lib/gitlab/gpg/commit_spec.rb22
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml1
-rw-r--r--spec/lib/gitlab/json_logger_spec.rb6
-rw-r--r--spec/lib/gitlab/project_search_results_spec.rb131
-rw-r--r--spec/lib/gitlab/search/found_blob_spec.rb138
-rw-r--r--spec/lib/gitlab/sentry_spec.rb22
-rw-r--r--spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb3
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/correlation_injector_spec.rb47
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/correlation_logger_spec.rb35
-rw-r--r--spec/lib/gitlab/template/finders/global_template_finder_spec.rb35
-rw-r--r--spec/lib/gitlab/template/finders/repo_template_finders_spec.rb4
-rw-r--r--spec/lib/gitlab/url_blocker_spec.rb21
-rw-r--r--spec/lib/gitlab/url_sanitizer_spec.rb1
-rw-r--r--spec/lib/gitlab/usage_data_spec.rb25
-rw-r--r--spec/lib/gitlab/utils_spec.rb34
-rw-r--r--spec/lib/gitlab/workhorse_spec.rb2
-rw-r--r--spec/lib/omni_auth/strategies/jwt_spec.rb70
-rw-r--r--spec/mailers/notify_spec.rb10
-rw-r--r--spec/migrations/populate_mr_metrics_with_events_data_spec.rb47
-rw-r--r--spec/models/appearance_spec.rb2
-rw-r--r--spec/models/broadcast_message_spec.rb37
-rw-r--r--spec/models/ci/bridge_spec.rb25
-rw-r--r--spec/models/ci/build_spec.rb4
-rw-r--r--spec/models/ci/pipeline_spec.rb44
-rw-r--r--spec/models/clusters/applications/knative_spec.rb42
-rw-r--r--spec/models/clusters/applications/runner_spec.rb6
-rw-r--r--spec/models/commit_spec.rb2
-rw-r--r--spec/models/concerns/discussion_on_diff_spec.rb28
-rw-r--r--spec/models/concerns/token_authenticatable_spec.rb86
-rw-r--r--spec/models/group_spec.rb6
-rw-r--r--spec/models/member_spec.rb23
-rw-r--r--spec/models/members/group_member_spec.rb22
-rw-r--r--spec/models/members/project_member_spec.rb19
-rw-r--r--spec/models/merge_request_spec.rb26
-rw-r--r--spec/models/namespace_spec.rb4
-rw-r--r--spec/models/pool_repository_spec.rb6
-rw-r--r--spec/models/project_import_data_spec.rb42
-rw-r--r--spec/models/project_spec.rb83
-rw-r--r--spec/models/remote_mirror_spec.rb39
-rw-r--r--spec/models/uploads/fog_spec.rb69
-rw-r--r--spec/models/uploads/local_spec.rb45
-rw-r--r--spec/models/user_spec.rb12
-rw-r--r--spec/presenters/group_member_presenter_spec.rb8
-rw-r--r--spec/presenters/project_member_presenter_spec.rb6
-rw-r--r--spec/presenters/project_presenter_spec.rb96
-rw-r--r--spec/requests/api/helpers_spec.rb30
-rw-r--r--spec/requests/api/jobs_spec.rb130
-rw-r--r--spec/requests/api/members_spec.rb31
-rw-r--r--spec/requests/api/projects_spec.rb2
-rw-r--r--spec/rubocop/cop/migration/add_timestamps_spec.rb6
-rw-r--r--spec/rubocop/cop/migration/datetime_spec.rb8
-rw-r--r--spec/rubocop/cop/migration/timestamps_spec.rb6
-rw-r--r--spec/serializers/diff_file_entity_spec.rb33
-rw-r--r--spec/serializers/discussion_diff_file_entity_spec.rb39
-rw-r--r--spec/serializers/discussion_entity_spec.rb8
-rw-r--r--spec/serializers/trigger_variable_entity_spec.rb49
-rw-r--r--spec/services/ci/create_pipeline_service_spec.rb89
-rw-r--r--spec/services/ci/retry_build_service_spec.rb6
-rw-r--r--spec/services/clusters/applications/create_service_spec.rb25
-rw-r--r--spec/services/clusters/build_service_spec.rb25
-rw-r--r--spec/services/merge_requests/refresh_service_spec.rb73
-rw-r--r--spec/services/notification_service_spec.rb54
-rw-r--r--spec/services/projects/cleanup_service_spec.rb44
-rw-r--r--spec/services/projects/fork_service_spec.rb30
-rw-r--r--spec/support/helpers/email_helpers.rb9
-rw-r--r--spec/support/helpers/fake_migration_classes.rb2
-rw-r--r--spec/support/helpers/features/sorting_helpers.rb4
-rw-r--r--spec/support/helpers/javascript_fixtures_helpers.rb7
-rw-r--r--spec/support/helpers/kubernetes_helpers.rb66
-rw-r--r--spec/support/helpers/test_env.rb3
-rw-r--r--spec/support/shared_examples/controllers/set_sort_order_from_user_preference_shared_examples.rb32
-rw-r--r--spec/support/shared_examples/file_finder.rb13
-rw-r--r--spec/support/shared_examples/models/member_shared_examples.rb77
-rw-r--r--spec/support/shared_examples/models/with_uploads_shared_examples.rb60
-rw-r--r--spec/support/shared_examples/notify_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/only_except_policy_examples.rb167
-rw-r--r--spec/support/shared_examples/serializers/diff_file_entity_examples.rb46
-rw-r--r--spec/tasks/gitlab/backup_rake_spec.rb1
-rw-r--r--spec/tasks/gitlab/web_hook_rake_spec.rb92
-rw-r--r--spec/uploaders/namespace_file_uploader_spec.rb27
-rw-r--r--spec/uploaders/personal_file_uploader_spec.rb27
-rw-r--r--spec/validators/url_validator_spec.rb29
-rw-r--r--spec/views/projects/_home_panel.html.haml_spec.rb2
-rw-r--r--spec/workers/object_pool/create_worker_spec.rb59
-rw-r--r--spec/workers/object_pool/join_worker_spec.rb35
-rw-r--r--spec/workers/prune_web_hook_logs_worker_spec.rb16
-rw-r--r--spec/workers/rebase_worker_spec.rb2
-rw-r--r--spec/workers/remove_old_web_hook_logs_worker_spec.rb18
-rw-r--r--spec/workers/repository_cleanup_worker_spec.rb55
-rw-r--r--spec/workers/repository_update_remote_mirror_worker_spec.rb13
-rw-r--r--vendor/gitignore/CMake.gitignore1
-rw-r--r--vendor/gitignore/Drupal.gitignore77
-rw-r--r--vendor/gitignore/Global/Emacs.gitignore4
-rw-r--r--vendor/gitignore/Global/PuTTY.gitignore2
-rw-r--r--vendor/gitignore/Global/Virtuoso.gitignore18
-rw-r--r--vendor/gitignore/Global/Xcode.gitignore74
-rw-r--r--vendor/gitignore/Node.gitignore5
-rw-r--r--vendor/gitignore/Python.gitignore1
-rw-r--r--vendor/gitignore/Smalltalk.gitignore9
-rw-r--r--vendor/gitignore/TeX.gitignore4
-rw-r--r--vendor/gitignore/Unity.gitignore4
-rw-r--r--vendor/gitignore/VisualStudio.gitignore4
-rw-r--r--vendor/licenses.csv154
-rw-r--r--yarn.lock319
800 files changed, 15514 insertions, 3813 deletions
diff --git a/.eslintrc.yml b/.eslintrc.yml
index ecd9f57b075..b0794bb7434 100644
--- a/.eslintrc.yml
+++ b/.eslintrc.yml
@@ -33,3 +33,4 @@ rules:
vue/no-unused-components: off
vue/no-use-v-if-with-v-for: off
vue/no-v-html: off
+ vue/use-v-on-exact: off
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index a97414cbba8..b26c2d16d77 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -488,7 +488,6 @@ danger-review:
<<: *pull-cache
image: registry.gitlab.com/gitlab-org/gitlab-build-images:danger
stage: test
- allow_failure: true
dependencies: []
before_script: []
only:
@@ -637,7 +636,7 @@ gitlab:setup-mysql:
# Frontend-related jobs
gitlab:assets:compile:
<<: *dedicated-no-docs-and-no-qa-pull-cache-job
- image: dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.5.3-git-2.18-chrome-69.0-node-8.x-yarn-1.2-graphicsmagick-1.3.29-docker-18.06.1
+ image: dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.5.3-git-2.18-chrome-69.0-node-8.x-yarn-1.12-graphicsmagick-1.3.29-docker-18.06.1
dependencies: []
services:
- docker:stable-dind
@@ -949,6 +948,8 @@ no_ee_check:
# GitLab Review apps
review-deploy:
<<: *review-base
+ retry: 2
+ allow_failure: true
variables:
GIT_DEPTH: "1"
HOST_SUFFIX: "${CI_ENVIRONMENT_SLUG}"
@@ -978,6 +979,8 @@ review-deploy:
.review-qa-base: &review-qa-base
<<: *review-docker
+ retry: 2
+ allow_failure: true
variables:
<<: *review-docker-variables
API_TOKEN: "${GITLAB_BOT_MULTI_PROJECT_PIPELINE_POLLING_TOKEN}"
@@ -1005,10 +1008,8 @@ review-deploy:
review-qa-smoke:
<<: *review-qa-base
- # retry: 2
script:
- gitlab-qa Test::Instance::Smoke "${QA_IMAGE}" "${CI_ENVIRONMENT_URL}"
- allow_failure: true
review-qa-all:
<<: *review-qa-base
diff --git a/.gitlab/CODEOWNERS.disabled b/.gitlab/CODEOWNERS.disabled
index a4b773b15a9..82e914a502f 100644
--- a/.gitlab/CODEOWNERS.disabled
+++ b/.gitlab/CODEOWNERS.disabled
@@ -6,8 +6,8 @@
/doc/ @axil @marcia
# Frontend maintainers should see everything in `app/assets/`
-app/assets/ @ClemMakesApps @fatihacet @filipa @iamphill @mikegreiling @timzallmann
-*.scss @annabeldunstone @ClemMakesApps @fatihacet @filipa @iamphill @mikegreiling @timzallmann
+app/assets/ @ClemMakesApps @fatihacet @filipa @iamphill @mikegreiling @timzallmann @kushalpandya
+*.scss @annabeldunstone @ClemMakesApps @fatihacet @filipa @iamphill @mikegreiling @timzallmann @kushalpandya
# Someone from the database team should review changes in `db/`
db/ @abrandl @NikolayS
diff --git a/.gitlab/issue_templates/Feature proposal.md b/.gitlab/issue_templates/Feature proposal.md
index c4065d3c4ea..ad517f0457d 100644
--- a/.gitlab/issue_templates/Feature proposal.md
+++ b/.gitlab/issue_templates/Feature proposal.md
@@ -1,14 +1,22 @@
### Problem to solve
+<!--- What problem do we solve? -->
+
+### Target audience
+
+<!--- For whom are we doing this? Include either a persona from https://design.gitlab.com/#/getting-started/personas or define a specific company role. e.a. "Release Manager" or "Security Analyst" -->
+
### Further details
-(Include use cases, benefits, and/or goals)
+<!--- Include use cases, benefits, and/or goals (contributes to our vision?) -->
### Proposal
+<!--- How are we going to solve the problem? -->
+
### What does success look like, and how can we measure that?
-(If no way to measure success, link to an issue that will implement a way to measure this)
+<!--- If no way to measure success, link to an issue that will implement a way to measure this -->
### Links / references
diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml
index 571df7534cb..3ab76965287 100644
--- a/.rubocop_todo.yml
+++ b/.rubocop_todo.yml
@@ -107,12 +107,6 @@ Lint/UriEscapeUnescape:
Metrics/LineLength:
Max: 1310
-# Offense count: 2
-Naming/ConstantName:
- Exclude:
- - 'lib/gitlab/import_sources.rb'
- - 'lib/gitlab/ssh_public_key.rb'
-
# Offense count: 11
# Configuration parameters: EnforcedStyle.
# SupportedStyles: lowercase, uppercase
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d41e5c8642f..d1e324c5518 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,13 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
+## 11.5.3 (2018-12-06)
+
+### Security (1 change)
+
+- Prevent a path traversal attack on global file templates.
+
+
## 11.5.2 (2018-12-03)
### Removed (1 change)
@@ -621,6 +628,13 @@ entry.
- Check frozen string in style builds. (gfyoung)
+## 11.3.12 (2018-12-06)
+
+### Security (1 change)
+
+- Prevent a path traversal attack on global file templates.
+
+
## 11.3.11 (2018-11-26)
### Security (33 changes)
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 26aaba0e866..bd8bf882d06 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-1.2.0
+1.7.0
diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION
index ba7f754d0c3..ae9a76b9249 100644
--- a/GITLAB_WORKHORSE_VERSION
+++ b/GITLAB_WORKHORSE_VERSION
@@ -1 +1 @@
-7.4.0
+8.0.0
diff --git a/Gemfile b/Gemfile
index 022f7e5dbcc..f43f334c801 100644
--- a/Gemfile
+++ b/Gemfile
@@ -5,7 +5,7 @@ end
gem_versions = {}
gem_versions['activerecord_sane_schema_dumper'] = rails5? ? '1.0' : '0.2'
-gem_versions['rails'] = rails5? ? '5.0.7' : '4.2.10'
+gem_versions['rails'] = rails5? ? '5.0.7' : '4.2.11'
gem_versions['rails-i18n'] = rails5? ? '~> 5.1' : '~> 4.0.9'
# The 2.0.6 version of rack requires monkeypatch to be present in
@@ -263,6 +263,9 @@ gem 'ace-rails-ap', '~> 4.1.0'
# Detect and convert string character encoding
gem 'charlock_holmes', '~> 0.7.5'
+# Detect mime content type from content
+gem 'mimemagic', '~> 0.3.2'
+
# Faster blank
gem 'fast_blank'
@@ -274,6 +277,7 @@ gem 'webpack-rails', '~> 0.9.10'
gem 'rack-proxy', '~> 0.6.0'
gem 'sass-rails', '~> 5.0.6'
+gem 'sass', '~> 3.5'
gem 'uglifier', '~> 2.7.2'
gem 'addressable', '~> 2.5.2'
@@ -432,7 +436,7 @@ group :ed25519 do
end
# Gitaly GRPC client
-gem 'gitaly-proto', '~> 1.2.0', require: 'gitaly'
+gem 'gitaly-proto', '~> 1.3.0', require: 'gitaly'
gem 'grpc', '~> 1.15.0'
gem 'google-protobuf', '~> 3.6'
diff --git a/Gemfile.lock b/Gemfile.lock
index 699d77615aa..b9780d4c23f 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -82,6 +82,7 @@ GEM
erubi (>= 1.0.0)
rack (>= 0.9.0)
bindata (2.4.3)
+ binding_ninja (0.2.2)
binding_of_caller (0.8.0)
debug_inspector (>= 0.0.1)
bootsnap (1.3.2)
@@ -273,7 +274,7 @@ GEM
gettext_i18n_rails (>= 0.7.1)
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
- gitaly-proto (1.2.0)
+ gitaly-proto (1.3.0)
grpc (~> 1.0)
github-markup (1.7.0)
gitlab-default_value_for (3.1.1)
@@ -458,7 +459,7 @@ GEM
mime-types (3.2.2)
mime-types-data (~> 3.2015)
mime-types-data (3.2018.0812)
- mimemagic (0.3.0)
+ mimemagic (0.3.2)
mini_magick (4.8.0)
mini_mime (1.0.1)
mini_portile2 (2.3.0)
@@ -724,8 +725,8 @@ GEM
rspec-mocks (3.7.0)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.7.0)
- rspec-parameterized (0.4.0)
- binding_of_caller
+ rspec-parameterized (0.4.1)
+ binding_ninja (>= 0.2.1)
parser
proc_to_ast
rspec (>= 2.13, < 4)
@@ -806,7 +807,7 @@ GEM
selenium-webdriver (3.12.0)
childprocess (~> 0.5)
rubyzip (~> 1.2)
- sentry-raven (2.7.2)
+ sentry-raven (2.7.4)
faraday (>= 0.7.6, < 1.0)
settingslogic (2.0.9)
sexp_processor (4.11.0)
@@ -895,7 +896,7 @@ GEM
get_process_mem (~> 0)
unicorn (>= 4, < 6)
uniform_notifier (1.10.0)
- unparser (0.2.7)
+ unparser (0.4.2)
abstract_type (~> 0.0.7)
adamantium (~> 0.2.0)
concord (~> 0.1.5)
@@ -1006,7 +1007,7 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3)
- gitaly-proto (~> 1.2.0)
+ gitaly-proto (~> 1.3.0)
github-markup (~> 1.7.0)
gitlab-default_value_for (~> 3.1.1)
gitlab-markup (~> 1.6.5)
@@ -1050,6 +1051,7 @@ DEPENDENCIES
loofah (~> 2.2)
mail_room (~> 0.9.1)
method_source (~> 0.8)
+ mimemagic (~> 0.3.2)
mini_magick
minitest (~> 5.7.0)
mysql2 (~> 0.4.10)
@@ -1127,6 +1129,7 @@ DEPENDENCIES
rufus-scheduler (~> 3.4)
rugged (~> 0.27)
sanitize (~> 4.6)
+ sass (~> 3.5)
sass-rails (~> 5.0.6)
scss_lint (~> 0.56.0)
seed-fu (~> 2.3.7)
diff --git a/Gemfile.rails4.lock b/Gemfile.rails4.lock
index 15e0b782d5b..3d81c570b89 100644
--- a/Gemfile.rails4.lock
+++ b/Gemfile.rails4.lock
@@ -79,6 +79,7 @@ GEM
erubi (>= 1.0.0)
rack (>= 0.9.0)
bindata (2.4.3)
+ binding_ninja (0.2.2)
binding_of_caller (0.8.0)
debug_inspector (>= 0.0.1)
bootsnap (1.3.2)
@@ -272,7 +273,7 @@ GEM
gettext_i18n_rails (>= 0.7.1)
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
- gitaly-proto (1.2.0)
+ gitaly-proto (1.3.0)
grpc (~> 1.0)
github-markup (1.7.0)
gitlab-markup (1.6.5)
@@ -455,7 +456,7 @@ GEM
mime-types (3.2.2)
mime-types-data (~> 3.2015)
mime-types-data (3.2018.0812)
- mimemagic (0.3.0)
+ mimemagic (0.3.2)
mini_magick (4.8.0)
mini_mime (1.0.1)
mini_portile2 (2.3.0)
@@ -618,16 +619,16 @@ GEM
rack
rack-test (0.6.3)
rack (>= 1.0)
- rails (4.2.10)
- actionmailer (= 4.2.10)
- actionpack (= 4.2.10)
- actionview (= 4.2.10)
- activejob (= 4.2.10)
- activemodel (= 4.2.10)
- activerecord (= 4.2.10)
- activesupport (= 4.2.10)
+ rails (4.2.11)
+ actionmailer (= 4.2.11)
+ actionpack (= 4.2.11)
+ actionview (= 4.2.11)
+ activejob (= 4.2.11)
+ activemodel (= 4.2.11)
+ activerecord (= 4.2.11)
+ activesupport (= 4.2.11)
bundler (>= 1.3.0, < 2.0)
- railties (= 4.2.10)
+ railties (= 4.2.11)
sprockets-rails
rails-deprecated_sanitizer (1.0.3)
activesupport (>= 4.2.0.alpha)
@@ -715,8 +716,8 @@ GEM
rspec-mocks (3.7.0)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.7.0)
- rspec-parameterized (0.4.0)
- binding_of_caller
+ rspec-parameterized (0.4.1)
+ binding_ninja (>= 0.2.1)
parser
proc_to_ast
rspec (>= 2.13, < 4)
@@ -889,7 +890,7 @@ GEM
get_process_mem (~> 0)
unicorn (>= 4, < 6)
uniform_notifier (1.10.0)
- unparser (0.2.7)
+ unparser (0.4.2)
abstract_type (~> 0.0.7)
adamantium (~> 0.2.0)
concord (~> 0.1.5)
@@ -998,7 +999,7 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3)
- gitaly-proto (~> 1.2.0)
+ gitaly-proto (~> 1.3.0)
github-markup (~> 1.7.0)
gitlab-markup (~> 1.6.5)
gitlab-sidekiq-fetcher
@@ -1041,6 +1042,7 @@ DEPENDENCIES
loofah (~> 2.2)
mail_room (~> 0.9.1)
method_source (~> 0.8)
+ mimemagic (~> 0.3.2)
mini_magick
minitest (~> 5.7.0)
mysql2 (~> 0.4.10)
@@ -1084,7 +1086,7 @@ DEPENDENCIES
rack-cors (~> 1.0.0)
rack-oauth2 (~> 1.2.1)
rack-proxy (~> 0.6.0)
- rails (= 4.2.10)
+ rails (= 4.2.11)
rails-deprecated_sanitizer (~> 1.0.3)
rails-i18n (~> 4.0.9)
rainbow (~> 3.0)
@@ -1117,6 +1119,7 @@ DEPENDENCIES
rufus-scheduler (~> 3.4)
rugged (~> 0.27)
sanitize (~> 4.6)
+ sass (~> 3.5)
sass-rails (~> 5.0.6)
scss_lint (~> 0.56.0)
seed-fu (~> 2.3.7)
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index f8dbe412f80..e2740981a4b 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -14,13 +14,16 @@ const Api = {
projectMergeRequestPath: '/api/:version/projects/:id/merge_requests/:mrid',
projectMergeRequestChangesPath: '/api/:version/projects/:id/merge_requests/:mrid/changes',
projectMergeRequestVersionsPath: '/api/:version/projects/:id/merge_requests/:mrid/versions',
+ projectRunnersPath: '/api/:version/projects/:id/runners',
mergeRequestsPath: '/api/:version/merge_requests',
groupLabelsPath: '/groups/:namespace_path/-/labels',
issuableTemplatePath: '/:namespace_path/:project_path/templates/:type/:key',
projectTemplatePath: '/api/:version/projects/:id/templates/:type/:key',
projectTemplatesPath: '/api/:version/projects/:id/templates/:type',
usersPath: '/api/:version/users.json',
- userStatusPath: '/api/:version/user/status',
+ userPath: '/api/:version/users/:id',
+ userStatusPath: '/api/:version/users/:id/status',
+ userPostStatusPath: '/api/:version/user/status',
commitPath: '/api/:version/projects/:id/repository/commits',
commitPipelinesPath: '/:project_id/commit/:sha/pipelines',
branchSinglePath: '/api/:version/projects/:id/repository/branches/:branch',
@@ -124,6 +127,15 @@ const Api = {
return axios.get(url);
},
+ projectRunners(projectPath, config = {}) {
+ const url = Api.buildUrl(Api.projectRunnersPath).replace(
+ ':id',
+ encodeURIComponent(projectPath),
+ );
+
+ return axios.get(url, config);
+ },
+
mergeRequests(params = {}) {
const url = Api.buildUrl(Api.mergeRequestsPath);
@@ -244,6 +256,20 @@ const Api = {
});
},
+ user(id, options) {
+ const url = Api.buildUrl(this.userPath).replace(':id', encodeURIComponent(id));
+ return axios.get(url, {
+ params: options,
+ });
+ },
+
+ userStatus(id, options) {
+ const url = Api.buildUrl(this.userStatusPath).replace(':id', encodeURIComponent(id));
+ return axios.get(url, {
+ params: options,
+ });
+ },
+
branches(id, query = '', options = {}) {
const url = Api.buildUrl(this.createBranchPath).replace(':id', encodeURIComponent(id));
@@ -266,7 +292,7 @@ const Api = {
},
postUserStatus({ emoji, message }) {
- const url = Api.buildUrl(this.userStatusPath);
+ const url = Api.buildUrl(this.userPostStatusPath);
return axios.put(url, {
emoji,
diff --git a/app/assets/javascripts/behaviors/markdown/render_gfm.js b/app/assets/javascripts/behaviors/markdown/render_gfm.js
index a2d4331b6d1..fc9286d15e6 100644
--- a/app/assets/javascripts/behaviors/markdown/render_gfm.js
+++ b/app/assets/javascripts/behaviors/markdown/render_gfm.js
@@ -3,6 +3,7 @@ import syntaxHighlight from '~/syntax_highlight';
import renderMath from './render_math';
import renderMermaid from './render_mermaid';
import highlightCurrentUser from './highlight_current_user';
+import initUserPopovers from '../../user_popovers';
// Render GitLab flavoured Markdown
//
@@ -13,6 +14,7 @@ $.fn.renderGFM = function renderGFM() {
renderMath(this.find('.js-render-math'));
renderMermaid(this.find('.js-render-mermaid'));
highlightCurrentUser(this.find('.gfm-project_member').get());
+ initUserPopovers(this.find('.gfm-project_member').get());
return this;
};
diff --git a/app/assets/javascripts/behaviors/requires_input.js b/app/assets/javascripts/behaviors/requires_input.js
index c09d9ccddd6..d8056e48d4e 100644
--- a/app/assets/javascripts/behaviors/requires_input.js
+++ b/app/assets/javascripts/behaviors/requires_input.js
@@ -50,10 +50,11 @@ function hideOrShowHelpBlock(form) {
}
$(() => {
- const $form = $('form.js-requires-input');
- if ($form) {
+ $('form.js-requires-input').each((i, el) => {
+ const $form = $(el);
+
$form.requiresInput();
hideOrShowHelpBlock($form);
$('.select2.js-select-namespace').change(() => hideOrShowHelpBlock($form));
- }
+ });
});
diff --git a/app/assets/javascripts/boards/components/issue_due_date.vue b/app/assets/javascripts/boards/components/issue_due_date.vue
index 15937b1091a..e038198e6f0 100644
--- a/app/assets/javascripts/boards/components/issue_due_date.vue
+++ b/app/assets/javascripts/boards/components/issue_due_date.vue
@@ -15,6 +15,16 @@ export default {
type: String,
required: true,
},
+ cssClass: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ tooltipPlacement: {
+ type: String,
+ required: false,
+ default: 'bottom',
+ },
},
computed: {
title() {
@@ -66,15 +76,13 @@ export default {
<template>
<span>
- <span ref="issueDueDate" class="board-card-info card-number">
- <icon
- :class="{ 'text-danger': isPastDue, 'board-card-info-icon': true }"
- name="calendar"
- /><time :class="{ 'text-danger': isPastDue }" datetime="date" class="board-card-info-text">{{
+ <span ref="issueDueDate" :class="cssClass" class="board-card-info card-number">
+ <icon :class="{ 'text-danger': isPastDue, 'board-card-info-icon': true }" name="calendar" />
+ <time :class="{ 'text-danger': isPastDue }" datetime="date" class="board-card-info-text">{{
body
}}</time>
</span>
- <gl-tooltip :target="() => $refs.issueDueDate" placement="bottom">
+ <gl-tooltip :target="() => $refs.issueDueDate" :placement="tooltipPlacement">
<span class="bold">{{ __('Due date') }}</span> <br />
<span :class="{ 'text-danger-muted': isPastDue }">{{ title }}</span>
</gl-tooltip>
diff --git a/app/assets/javascripts/boards/components/project_select.vue b/app/assets/javascripts/boards/components/project_select.vue
index 31651658fe6..d899b7fbd8c 100644
--- a/app/assets/javascripts/boards/components/project_select.vue
+++ b/app/assets/javascripts/boards/components/project_select.vue
@@ -92,20 +92,7 @@ export default {
{{ selectedProjectName }} <icon name="chevron-down" />
</button>
<div class="dropdown-menu dropdown-menu-selectable dropdown-menu-full-width">
- <div class="dropdown-title">
- <span>Projects</span>
- <button
- aria-label="Close"
- type="button"
- class="dropdown-title-button dropdown-menu-close"
- >
- <icon
- name="merge-request-close-m"
- data-hidden="true"
- class="dropdown-menu-close-icon"
- />
- </button>
- </div>
+ <div class="dropdown-title">Projects</div>
<div class="dropdown-input">
<input class="dropdown-input-field" type="search" placeholder="Search projects" />
<icon name="search" class="dropdown-input-search" data-hidden="true" />
diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js
index aff32d95db1..cf70a48f076 100644
--- a/app/assets/javascripts/clusters/clusters_bundle.js
+++ b/app/assets/javascripts/clusters/clusters_bundle.js
@@ -1,6 +1,6 @@
import Visibility from 'visibilityjs';
import Vue from 'vue';
-import PersistentUserCallout from '../persistent_user_callout';
+import initDismissableCallout from '~/dismissable_callout';
import { s__, sprintf } from '../locale';
import Flash from '../flash';
import Poll from '../lib/utils/poll';
@@ -67,7 +67,7 @@ export default class Clusters {
this.showTokenButton = document.querySelector('.js-show-cluster-token');
this.tokenField = document.querySelector('.js-cluster-token');
- Clusters.initDismissableCallout();
+ initDismissableCallout('.js-cluster-security-warning');
initSettingsPanels();
setupToggleButtons(document.querySelector('.js-cluster-enable-toggle-area'));
this.initApplications(clusterType);
@@ -108,12 +108,6 @@ export default class Clusters {
});
}
- static initDismissableCallout() {
- const callout = document.querySelector('.js-cluster-security-warning');
-
- if (callout) new PersistentUserCallout(callout); // eslint-disable-line no-new
- }
-
addListeners() {
if (this.showTokenButton) this.showTokenButton.addEventListener('click', this.showToken);
eventHub.$on('installApplication', this.installApplication);
diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue
index 9a96d0fa6d7..489615f1f78 100644
--- a/app/assets/javascripts/clusters/components/applications.vue
+++ b/app/assets/javascripts/clusters/components/applications.vue
@@ -84,6 +84,9 @@ export default {
ingressExternalIp() {
return this.applications.ingress.externalIp;
},
+ certManagerInstalled() {
+ return this.applications.cert_manager.status === APPLICATION_STATUS.INSTALLED;
+ },
ingressDescription() {
const extraCostParagraph = sprintf(
_.escape(
@@ -130,9 +133,9 @@ export default {
return sprintf(
_.escape(
s__(
- `ClusterIntegration|cert-manager is a native Kubernetes certificate management controller that helps with issuing certificates.
- Installing cert-manager on your cluster will issue a certificate by %{letsEncrypt} and ensure that certificates
- are valid and up to date.`,
+ `ClusterIntegration|Cert-Manager is a native Kubernetes certificate management controller that helps with issuing certificates.
+ Installing Cert-Manager on your cluster will issue a certificate by %{letsEncrypt} and ensure that certificates
+ are valid and up-to-date.`,
),
),
{
@@ -259,6 +262,16 @@ export default {
</span>
</div>
<input v-else type="text" class="form-control js-ip-address" readonly value="?" />
+ <p class="form-text text-muted">
+ {{
+ s__(`ClusterIntegration|Point a wildcard DNS to this
+ generated IP address in order to access
+ your application after it has been deployed.`)
+ }}
+ <a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">
+ {{ __('More information') }}
+ </a>
+ </p>
</div>
<p v-if="!ingressExternalIp" class="settings-message js-no-ip-message">
@@ -272,17 +285,6 @@ export default {
{{ __('More information') }}
</a>
</p>
-
- <p>
- {{
- s__(`ClusterIntegration|Point a wildcard DNS to this
- generated IP address in order to access
- your application after it has been deployed.`)
- }}
- <a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">
- {{ __('More information') }}
- </a>
- </p>
</template>
<div v-html="ingressDescription"></div>
</div>
@@ -295,11 +297,41 @@ export default {
:status-reason="applications.cert_manager.statusReason"
:request-status="applications.cert_manager.requestStatus"
:request-reason="applications.cert_manager.requestReason"
+ :install-application-request-params="{ email: applications.cert_manager.email }"
:disabled="!helmInstalled"
- class="hide-bottom-border rounded-bottom"
title-link="https://cert-manager.readthedocs.io/en/latest/#"
>
- <div slot="description" v-html="certManagerDescription"></div>
+ <template>
+ <div slot="description">
+ <p v-html="certManagerDescription"></p>
+ <div class="form-group">
+ <label for="cert-manager-issuer-email">
+ {{ s__('ClusterIntegration|Issuer Email') }}
+ </label>
+ <div class="input-group">
+ <input
+ v-model="applications.cert_manager.email"
+ :readonly="certManagerInstalled"
+ type="text"
+ class="form-control js-email"
+ />
+ </div>
+ <p class="form-text text-muted">
+ {{
+ s__(`ClusterIntegration|Issuers represent a certificate authority.
+ You must provide an email address for your Issuer. `)
+ }}
+ <a
+ href="http://docs.cert-manager.io/en/latest/reference/issuers.html?highlight=email"
+ target="_blank"
+ rel="noopener noreferrer"
+ >
+ {{ __('More information') }}
+ </a>
+ </p>
+ </div>
+ </div>
+ </template>
</application-row>
<application-row
v-if="isProjectCluster"
@@ -382,20 +414,22 @@ export default {
/>
</span>
</div>
+
+ <p v-if="ingressInstalled" class="form-text text-muted">
+ {{
+ s__(`ClusterIntegration|Replace this with your own hostname if you want.
+ If you do so, point hostname to Ingress IP Address from above.`)
+ }}
+ <a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">
+ {{ __('More information') }}
+ </a>
+ </p>
</div>
- <p v-if="ingressInstalled">
- {{
- s__(`ClusterIntegration|Replace this with your own hostname if you want.
- If you do so, point hostname to Ingress IP Address from above.`)
- }}
- <a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">
- {{ __('More information') }}
- </a>
- </p>
</template>
</div>
</application-row>
<application-row
+ v-if="isProjectCluster"
id="knative"
:logo-url="knativeLogo"
:title="applications.knative.title"
@@ -405,7 +439,6 @@ export default {
:request-reason="applications.knative.requestReason"
:install-application-request-params="{ hostname: applications.knative.hostname }"
:disabled="!helmInstalled"
- class="hide-bottom-border rounded-bottom"
title-link="https://github.com/knative/docs"
>
<div slot="description">
@@ -432,7 +465,7 @@ export default {
/>
</div>
</template>
- <template v-else>
+ <template v-else-if="helmInstalled">
<div class="form-group">
<label for="knative-domainname">
{{ s__('ClusterIntegration|Knative Domain Name:') }}
diff --git a/app/assets/javascripts/clusters/constants.js b/app/assets/javascripts/clusters/constants.js
index 15cf4a56138..e31afadf186 100644
--- a/app/assets/javascripts/clusters/constants.js
+++ b/app/assets/javascripts/clusters/constants.js
@@ -24,3 +24,4 @@ export const REQUEST_FAILURE = 'request-failure';
export const INGRESS = 'ingress';
export const JUPYTER = 'jupyter';
export const KNATIVE = 'knative';
+export const CERT_MANAGER = 'cert_manager';
diff --git a/app/assets/javascripts/clusters/stores/clusters_store.js b/app/assets/javascripts/clusters/stores/clusters_store.js
index 2d69da8eaec..c750daab112 100644
--- a/app/assets/javascripts/clusters/stores/clusters_store.js
+++ b/app/assets/javascripts/clusters/stores/clusters_store.js
@@ -1,5 +1,5 @@
import { s__ } from '../../locale';
-import { INGRESS, JUPYTER, KNATIVE } from '../constants';
+import { INGRESS, JUPYTER, KNATIVE, CERT_MANAGER } from '../constants';
export default class ClusterStore {
constructor() {
@@ -30,6 +30,7 @@ export default class ClusterStore {
statusReason: null,
requestStatus: null,
requestReason: null,
+ email: null,
},
runner: {
title: s__('ClusterIntegration|GitLab Runner'),
@@ -103,6 +104,9 @@ export default class ClusterStore {
if (appId === INGRESS) {
this.state.applications.ingress.externalIp = serverAppEntry.external_ip;
+ } else if (appId === CERT_MANAGER) {
+ this.state.applications.cert_manager.email =
+ this.state.applications.cert_manager.email || serverAppEntry.email;
} else if (appId === JUPYTER) {
this.state.applications.jupyter.hostname =
serverAppEntry.hostname ||
diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue
index bf9244df7f7..f0e82b1ed27 100644
--- a/app/assets/javascripts/diffs/components/app.vue
+++ b/app/assets/javascripts/diffs/components/app.vue
@@ -42,6 +42,11 @@ export default {
type: Object,
required: true,
},
+ changesEmptyStateIllustration: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
data() {
return {
@@ -63,7 +68,7 @@ export default {
plainDiffPath: state => state.diffs.plainDiffPath,
emailPatchPath: state => state.diffs.emailPatchPath,
}),
- ...mapState('diffs', ['showTreeList', 'isLoading']),
+ ...mapState('diffs', ['showTreeList', 'isLoading', 'startVersion']),
...mapGetters('diffs', ['isParallelView']),
...mapGetters(['isNotesFetched', 'getNoteableData']),
targetBranch() {
@@ -79,6 +84,13 @@ export default {
showCompareVersions() {
return this.mergeRequestDiffs && this.mergeRequestDiff;
},
+ renderDiffFiles() {
+ return (
+ this.diffFiles.length > 0 ||
+ (this.startVersion &&
+ this.startVersion.version_index === this.mergeRequestDiff.version_index)
+ );
+ },
},
watch: {
diffViewType() {
@@ -191,7 +203,7 @@ export default {
<div v-show="showTreeList" class="diff-tree-list"><tree-list /></div>
<div class="diff-files-holder">
<commit-widget v-if="commit" :commit="commit" />
- <template v-if="diffFiles.length > 0">
+ <template v-if="renderDiffFiles">
<diff-file
v-for="file in diffFiles"
:key="file.newPath"
@@ -199,7 +211,7 @@ export default {
:can-current-user-fork="canCurrentUserFork"
/>
</template>
- <no-changes v-else />
+ <no-changes v-else :changes-empty-state-illustration="changesEmptyStateIllustration" />
</div>
</div>
</div>
diff --git a/app/assets/javascripts/diffs/components/diff_content.vue b/app/assets/javascripts/diffs/components/diff_content.vue
index e405d8b20ae..11cc4c09fed 100644
--- a/app/assets/javascripts/diffs/components/diff_content.vue
+++ b/app/assets/javascripts/diffs/components/diff_content.vue
@@ -90,6 +90,8 @@ export default {
:old-sha="diffFile.diff_refs.base_sha"
:file-hash="diffFile.file_hash"
:project-path="projectPath"
+ :a-mode="diffFile.a_mode"
+ :b-mode="diffFile.b_mode"
>
<image-diff-overlay
slot="image-overlay"
diff --git a/app/assets/javascripts/diffs/components/diff_file.vue b/app/assets/javascripts/diffs/components/diff_file.vue
index f7e3655ea40..bed29efb253 100644
--- a/app/assets/javascripts/diffs/components/diff_file.vue
+++ b/app/assets/javascripts/diffs/components/diff_file.vue
@@ -4,6 +4,7 @@ import _ from 'underscore';
import { __, sprintf } from '~/locale';
import createFlash from '~/flash';
import { GlLoadingIcon } from '@gitlab/ui';
+import eventHub from '../../notes/event_hub';
import DiffFileHeader from './diff_file_header.vue';
import DiffContent from './diff_content.vue';
@@ -52,7 +53,9 @@ export default {
(!this.file.highlighted_diff_lines &&
!this.isLoadingCollapsedDiff &&
!this.file.too_large &&
- this.file.text)
+ this.file.text &&
+ !this.file.renamed_file &&
+ !this.file.mode_changed)
);
},
showLoadingIcon() {
@@ -73,6 +76,9 @@ export default {
}
},
},
+ created() {
+ eventHub.$on(`loadCollapsedDiff/${this.file.file_hash}`, this.handleLoadCollapsedDiff);
+ },
methods: {
...mapActions('diffs', ['loadCollapsedDiff', 'assignDiscussionsToDiff']),
handleToggle() {
@@ -143,9 +149,8 @@ export default {
<a
:href="file.fork_path"
class="js-fork-suggestion-button btn btn-grouped btn-inverted btn-success"
+ >Fork</a
>
- Fork
- </a>
<button
class="js-cancel-fork-suggestion-button btn btn-grouped"
type="button"
@@ -163,9 +168,9 @@ export default {
<gl-loading-icon v-if="showLoadingIcon" class="diff-content loading" />
<div v-else-if="showExpandMessage" class="nothing-here-block diff-collapsed">
{{ __('This diff is collapsed.') }}
- <a class="click-to-expand js-click-to-expand" href="#" @click.prevent="handleToggle">
- {{ __('Click to expand it.') }}
- </a>
+ <a class="click-to-expand js-click-to-expand" href="#" @click.prevent="handleToggle">{{
+ __('Click to expand it.')
+ }}</a>
</div>
<div v-if="file.too_large" class="nothing-here-block diff-collapsed js-too-large-diff">
{{ __('This source diff could not be displayed because it is too large.') }}
diff --git a/app/assets/javascripts/diffs/components/no_changes.vue b/app/assets/javascripts/diffs/components/no_changes.vue
index 25ec157ed25..47e9627a957 100644
--- a/app/assets/javascripts/diffs/components/no_changes.vue
+++ b/app/assets/javascripts/diffs/components/no_changes.vue
@@ -1,34 +1,51 @@
<script>
-import { mapState } from 'vuex';
-import emptyImage from '~/../../views/shared/icons/_mr_widget_empty_state.svg';
+import { mapGetters } from 'vuex';
+import _ from 'underscore';
+import { GlButton } from '@gitlab/ui';
+import { __, sprintf } from '~/locale';
export default {
- data() {
- return {
- emptyImage,
- };
+ components: {
+ GlButton,
+ },
+ props: {
+ changesEmptyStateIllustration: {
+ type: String,
+ required: true,
+ },
},
computed: {
- ...mapState({
- sourceBranch: state => state.notes.noteableData.source_branch,
- targetBranch: state => state.notes.noteableData.target_branch,
- newBlobPath: state => state.notes.noteableData.new_blob_path,
- }),
+ ...mapGetters(['getNoteableData']),
+ emptyStateText() {
+ return sprintf(
+ __(
+ 'No changes between %{ref_start}%{source_branch}%{ref_end} and %{ref_start}%{target_branch}%{ref_end}',
+ ),
+ {
+ ref_start: '<span class="ref-name">',
+ ref_end: '</span>',
+ source_branch: _.escape(this.getNoteableData.source_branch),
+ target_branch: _.escape(this.getNoteableData.target_branch),
+ },
+ false,
+ );
+ },
},
};
</script>
<template>
- <div class="row empty-state nothing-here-block">
- <div class="col-xs-12">
- <div class="svg-content"><span v-html="emptyImage"></span></div>
+ <div class="row empty-state">
+ <div class="col-12">
+ <div class="svg-content svg-250"><img :src="changesEmptyStateIllustration" /></div>
</div>
- <div class="col-xs-12">
+ <div class="col-12">
<div class="text-content text-center">
- No changes between <span class="ref-name">{{ sourceBranch }}</span> and
- <span class="ref-name">{{ targetBranch }}</span>
+ <span v-html="emptyStateText"></span>
<div class="text-center">
- <a :href="newBlobPath" class="btn btn-success"> {{ __('Create commit') }} </a>
+ <gl-button :href="getNoteableData.new_blob_path" variant="success">{{
+ __('Create commit')
+ }}</gl-button>
</div>
</div>
</div>
diff --git a/app/assets/javascripts/diffs/index.js b/app/assets/javascripts/diffs/index.js
index 06ef4207d85..915cacb374f 100644
--- a/app/assets/javascripts/diffs/index.js
+++ b/app/assets/javascripts/diffs/index.js
@@ -17,6 +17,7 @@ export default function initDiffsApp(store) {
endpoint: dataset.endpoint,
projectPath: dataset.projectPath,
currentUser: JSON.parse(dataset.currentUserData) || {},
+ changesEmptyStateIllustration: dataset.changesEmptyStateIllustration,
};
},
computed: {
@@ -31,6 +32,7 @@ export default function initDiffsApp(store) {
currentUser: this.currentUser,
projectPath: this.projectPath,
shouldShow: this.activeTab === 'diffs',
+ changesEmptyStateIllustration: this.changesEmptyStateIllustration,
},
});
},
diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js
index c0456c18e44..00a4bb6d3a3 100644
--- a/app/assets/javascripts/diffs/store/actions.js
+++ b/app/assets/javascripts/diffs/store/actions.js
@@ -3,8 +3,9 @@ import axios from '~/lib/utils/axios_utils';
import Cookies from 'js-cookie';
import createFlash from '~/flash';
import { s__ } from '~/locale';
-import { handleLocationHash, historyPushState } from '~/lib/utils/common_utils';
+import { handleLocationHash, historyPushState, scrollToElement } from '~/lib/utils/common_utils';
import { mergeUrlParams, getLocationHash } from '~/lib/utils/url_utility';
+import eventHub from '../../notes/event_hub';
import { getDiffPositionByLineCode, getNoteFormData } from './utils';
import * as types from './mutation_types';
import {
@@ -53,6 +54,10 @@ export const assignDiscussionsToDiff = (
diffPositionByLineCode,
});
});
+
+ Vue.nextTick(() => {
+ eventHub.$emit('scrollToDiscussion');
+ });
};
export const removeDiscussionsFromDiff = ({ commit }, removeDiscussion) => {
@@ -60,6 +65,27 @@ export const removeDiscussionsFromDiff = ({ commit }, removeDiscussion) => {
commit(types.REMOVE_LINE_DISCUSSIONS_FOR_FILE, { fileHash: file_hash, lineCode: line_code, id });
};
+export const renderFileForDiscussionId = ({ commit, rootState, state }, discussionId) => {
+ const discussion = rootState.notes.discussions.find(d => d.id === discussionId);
+
+ if (discussion) {
+ const file = state.diffFiles.find(f => f.file_hash === discussion.diff_file.file_hash);
+
+ if (file) {
+ if (!file.renderIt) {
+ commit(types.RENDER_FILE, file);
+ }
+
+ if (file.collapsed) {
+ eventHub.$emit(`loadCollapsedDiff/${file.file_hash}`);
+ scrollToElement(document.getElementById(file.file_hash));
+ } else {
+ eventHub.$emit('scrollToDiscussion');
+ }
+ }
+ }
+};
+
export const startRenderDiffsQueue = ({ state, commit }) => {
const checkItem = () =>
new Promise(resolve => {
@@ -192,8 +218,9 @@ export const toggleFileDiscussions = ({ getters, dispatch }, diff) => {
});
};
-export const saveDiffDiscussion = ({ dispatch }, { note, formData }) => {
+export const saveDiffDiscussion = ({ state, dispatch }, { note, formData }) => {
const postData = getNoteFormData({
+ commit: state.commit,
note,
...formData,
});
diff --git a/app/assets/javascripts/diffs/store/mutations.js b/app/assets/javascripts/diffs/store/mutations.js
index 331fb052371..2ea884d1293 100644
--- a/app/assets/javascripts/diffs/store/mutations.js
+++ b/app/assets/javascripts/diffs/store/mutations.js
@@ -123,22 +123,23 @@ export default {
diffPosition: diffPositionByLineCode[line.line_code],
latestDiff,
});
+ const mapDiscussions = (line, extraCheck = () => true) => ({
+ ...line,
+ discussions: extraCheck()
+ ? line.discussions
+ .filter(() => !line.discussions.some(({ id }) => discussion.id === id))
+ .concat(lineCheck(line) ? discussion : line.discussions)
+ : [],
+ });
state.diffFiles = state.diffFiles.map(diffFile => {
if (diffFile.file_hash === fileHash) {
const file = { ...diffFile };
if (file.highlighted_diff_lines) {
- file.highlighted_diff_lines = file.highlighted_diff_lines.map(line => {
- if (!line.discussions.some(({ id }) => discussion.id === id) && lineCheck(line)) {
- return {
- ...line,
- discussions: line.discussions.concat(discussion),
- };
- }
-
- return line;
- });
+ file.highlighted_diff_lines = file.highlighted_diff_lines.map(line =>
+ mapDiscussions(line),
+ );
}
if (file.parallel_diff_lines) {
@@ -148,20 +149,8 @@ export default {
if (left || right) {
return {
- left: {
- ...line.left,
- discussions:
- left && !line.left.discussions.some(({ id }) => id === discussion.id)
- ? line.left.discussions.concat(discussion)
- : (line.left && line.left.discussions) || [],
- },
- right: {
- ...line.right,
- discussions:
- right && !left && !line.right.discussions.some(({ id }) => id === discussion.id)
- ? line.right.discussions.concat(discussion)
- : (line.right && line.right.discussions) || [],
- },
+ left: line.left ? mapDiscussions(line.left) : null,
+ right: line.right ? mapDiscussions(line.right, () => !left) : null,
};
}
@@ -170,7 +159,7 @@ export default {
}
if (!file.parallel_diff_lines || !file.highlighted_diff_lines) {
- file.discussions = file.discussions.concat(discussion);
+ file.discussions = (file.discussions || []).concat(discussion);
}
return file;
@@ -180,7 +169,7 @@ export default {
});
},
- [types.REMOVE_LINE_DISCUSSIONS_FOR_FILE](state, { fileHash, lineCode, id }) {
+ [types.REMOVE_LINE_DISCUSSIONS_FOR_FILE](state, { fileHash, lineCode }) {
const selectedFile = state.diffFiles.find(f => f.file_hash === fileHash);
if (selectedFile) {
if (selectedFile.parallel_diff_lines) {
@@ -193,7 +182,7 @@ export default {
const side = targetLine.left && targetLine.left.line_code === lineCode ? 'left' : 'right';
Object.assign(targetLine[side], {
- discussions: [],
+ discussions: targetLine[side].discussions.filter(discussion => discussion.notes.length),
});
}
}
@@ -205,14 +194,14 @@ export default {
if (targetInlineLine) {
Object.assign(targetInlineLine, {
- discussions: [],
+ discussions: targetInlineLine.discussions.filter(discussion => discussion.notes.length),
});
}
}
if (selectedFile.discussions && selectedFile.discussions.length) {
selectedFile.discussions = selectedFile.discussions.filter(
- discussion => discussion.id !== id,
+ discussion => discussion.notes.length,
);
}
}
diff --git a/app/assets/javascripts/diffs/store/utils.js b/app/assets/javascripts/diffs/store/utils.js
index 54b9ee4d2d6..cbaa0e26395 100644
--- a/app/assets/javascripts/diffs/store/utils.js
+++ b/app/assets/javascripts/diffs/store/utils.js
@@ -27,6 +27,7 @@ export const getReversePosition = linePosition => {
export function getFormData(params) {
const {
+ commit,
note,
noteableType,
noteableData,
@@ -66,7 +67,7 @@ export function getFormData(params) {
position,
noteable_type: noteableType,
noteable_id: noteableData.id,
- commit_id: '',
+ commit_id: commit && commit.id,
type:
diffFile.diff_refs.start_sha && diffFile.diff_refs.head_sha
? DIFF_NOTE_TYPE
@@ -324,5 +325,9 @@ export const generateTreeList = files =>
export const getDiffMode = diffFile => {
const diffModeKey = Object.keys(diffModes).find(key => diffFile[`${key}_file`]);
- return diffModes[diffModeKey] || diffModes.replaced;
+ return (
+ diffModes[diffModeKey] ||
+ (diffFile.mode_changed && diffModes.mode_changed) ||
+ diffModes.replaced
+ );
};
diff --git a/app/assets/javascripts/dismissable_callout.js b/app/assets/javascripts/dismissable_callout.js
new file mode 100644
index 00000000000..5185b019376
--- /dev/null
+++ b/app/assets/javascripts/dismissable_callout.js
@@ -0,0 +1,27 @@
+import $ from 'jquery';
+import axios from '~/lib/utils/axios_utils';
+import { __ } from '~/locale';
+import Flash from '~/flash';
+
+export default function initDismissableCallout(alertSelector) {
+ const alertEl = document.querySelector(alertSelector);
+ if (!alertEl) {
+ return;
+ }
+
+ const closeButtonEl = alertEl.getElementsByClassName('close')[0];
+ const { dismissEndpoint, featureId } = closeButtonEl.dataset;
+
+ closeButtonEl.addEventListener('click', () => {
+ axios
+ .post(dismissEndpoint, {
+ feature_name: featureId,
+ })
+ .then(() => {
+ $(alertEl).alert('close');
+ })
+ .catch(() => {
+ Flash(__('An error occurred while dismissing the alert. Refresh the page and try again.'));
+ });
+ });
+}
diff --git a/app/assets/javascripts/environments/components/environments_table.vue b/app/assets/javascripts/environments/components/environments_table.vue
index 5164d87c5fa..533e90e2222 100644
--- a/app/assets/javascripts/environments/components/environments_table.vue
+++ b/app/assets/javascripts/environments/components/environments_table.vue
@@ -70,7 +70,7 @@ export default {
<template v-if="shouldRenderFolderContent(model)">
<div v-if="model.isLoadingFolderContent" :key="`loading-item-${i}`">
- <gl-loading-icon :size="2" />
+ <gl-loading-icon :size="2" class="prepend-top-16" />
</div>
<template v-else>
diff --git a/app/assets/javascripts/ide/constants.js b/app/assets/javascripts/ide/constants.js
index 3b201f006aa..09245ed0296 100644
--- a/app/assets/javascripts/ide/constants.js
+++ b/app/assets/javascripts/ide/constants.js
@@ -26,6 +26,7 @@ export const diffModes = {
new: 'new',
deleted: 'deleted',
renamed: 'renamed',
+ mode_changed: 'mode_changed',
};
export const rightSidebarViews = {
diff --git a/app/assets/javascripts/ide/index.js b/app/assets/javascripts/ide/index.js
index fbf944499d5..6351948f750 100644
--- a/app/assets/javascripts/ide/index.js
+++ b/app/assets/javascripts/ide/index.js
@@ -1,5 +1,6 @@
import Vue from 'vue';
import { mapActions } from 'vuex';
+import _ from 'underscore';
import Translate from '~/vue_shared/translate';
import ide from './components/ide.vue';
import store from './stores';
@@ -13,19 +14,19 @@ Vue.use(Translate);
*
* @param {Element} el - The element that will contain the IDE.
* @param {Object} options - Extra options for the IDE (Used by EE).
- * @param {(e:Element) => Object} options.extraInitialData -
- * Function that returns extra properties to seed initial data.
* @param {Component} options.rootComponent -
* Component that overrides the root component.
+ * @param {(store:Vuex.Store, el:Element) => Vuex.Store} options.extendStore -
+ * Function that receives the default store and returns an extended one.
*/
export function initIde(el, options = {}) {
if (!el) return null;
- const { extraInitialData = () => ({}), rootComponent = ide } = options;
+ const { rootComponent = ide, extendStore = _.identity } = options;
return new Vue({
el,
- store,
+ store: extendStore(store, el),
router,
created() {
this.setEmptyStateSvgs({
@@ -41,7 +42,6 @@ export function initIde(el, options = {}) {
});
this.setInitialData({
clientsidePreviewEnabled: parseBoolean(el.dataset.clientsidePreviewEnabled),
- ...extraInitialData(el),
});
},
methods: {
diff --git a/app/assets/javascripts/image_diff/helpers/badge_helper.js b/app/assets/javascripts/image_diff/helpers/badge_helper.js
index eddaeda9578..000157efad0 100644
--- a/app/assets/javascripts/image_diff/helpers/badge_helper.js
+++ b/app/assets/javascripts/image_diff/helpers/badge_helper.js
@@ -12,7 +12,7 @@ export function createImageBadge(noteId, { x, y }, classNames = []) {
}
export function addImageBadge(containerEl, { coordinate, badgeText, noteId }) {
- const buttonEl = createImageBadge(noteId, coordinate, ['badge']);
+ const buttonEl = createImageBadge(noteId, coordinate, ['badge', 'badge-pill']);
buttonEl.innerText = badgeText;
containerEl.appendChild(buttonEl);
diff --git a/app/assets/javascripts/issuable_suggestions/components/app.vue b/app/assets/javascripts/issuable_suggestions/components/app.vue
index eea0701312b..575c860851c 100644
--- a/app/assets/javascripts/issuable_suggestions/components/app.vue
+++ b/app/assets/javascripts/issuable_suggestions/components/app.vue
@@ -27,7 +27,7 @@ export default {
apollo: {
issues: {
query,
- debounce: 250,
+ debounce: 1000,
skip() {
return this.isSearchEmpty;
},
diff --git a/app/assets/javascripts/jobs/components/trigger_block.vue b/app/assets/javascripts/jobs/components/trigger_block.vue
index 4a9b2903eec..3cd3b743108 100644
--- a/app/assets/javascripts/jobs/components/trigger_block.vue
+++ b/app/assets/javascripts/jobs/components/trigger_block.vue
@@ -1,6 +1,9 @@
<script>
+import { __ } from '~/locale';
import { GlButton } from '@gitlab/ui';
+const HIDDEN_VALUE = '••••••';
+
export default {
components: {
GlButton,
@@ -13,17 +16,26 @@ export default {
},
data() {
return {
- areVariablesVisible: false,
+ showVariableValues: false,
};
},
computed: {
hasVariables() {
return this.trigger.variables && this.trigger.variables.length > 0;
},
+ getToggleButtonText() {
+ return this.showVariableValues ? __('Hide values') : __('Reveal values');
+ },
+ hasValues() {
+ return this.trigger.variables.some(v => v.value);
+ },
},
methods: {
- revealVariables() {
- this.areVariablesVisible = true;
+ toggleValues() {
+ this.showVariableValues = !this.showVariableValues;
+ },
+ getDisplayValue(value) {
+ return this.showVariableValues ? value : HIDDEN_VALUE;
},
},
};
@@ -33,31 +45,33 @@ export default {
<div class="build-widget block">
<h4 class="title">{{ __('Trigger') }}</h4>
- <p v-if="trigger.short_token" class="js-short-token">
+ <p
+ v-if="trigger.short_token"
+ class="js-short-token"
+ :class="{ 'append-bottom-0': !hasVariables }"
+ >
<span class="build-light-text"> {{ __('Token') }} </span> {{ trigger.short_token }}
</p>
- <p v-if="hasVariables">
- <gl-button
- v-if="!areVariablesVisible"
- type="button"
- class="btn btn-default group js-reveal-variables"
- @click="revealVariables"
- >
- {{ __('Reveal Variables') }}
- </gl-button>
- </p>
+ <template v-if="hasVariables">
+ <p class="trigger-variables-btn-container">
+ <span class="build-light-text"> {{ __('Variables:') }} </span>
- <dl v-if="areVariablesVisible" class="js-build-variables trigger-build-variables">
- <template v-for="variable in trigger.variables">
- <dt :key="`${variable.key}-variable`" class="js-build-variable trigger-build-variable">
- {{ variable.key }}
- </dt>
+ <gl-button v-if="hasValues" class="group js-reveal-variables" @click="toggleValues">
+ {{ getToggleButtonText }}
+ </gl-button>
+ </p>
- <dd :key="`${variable.key}-value`" class="js-build-value trigger-build-value">
- {{ variable.value }}
- </dd>
- </template>
- </dl>
+ <table class="js-build-variables trigger-build-variables">
+ <tr v-for="(variable, index) in trigger.variables" :key="`${variable.key}-${index}`">
+ <td class="js-build-variable trigger-build-variable trigger-variables-table-cell">
+ {{ variable.key }}
+ </td>
+ <td class="js-build-value trigger-build-value trigger-variables-table-cell">
+ {{ getDisplayValue(variable.value) }}
+ </td>
+ </tr>
+ </table>
+ </template>
</div>
</template>
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index 040d0bc659e..9e22cdc04e9 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -192,8 +192,12 @@ export const contentTop = () => {
const mrTabsHeight = $('.merge-request-tabs').height() || 0;
const headerHeight = $('.navbar-gitlab').height() || 0;
const diffFilesChanged = $('.js-diff-files-changed').height() || 0;
+ const diffFileLargeEnoughScreen =
+ 'matchMedia' in window ? window.matchMedia('min-width: 768') : true;
+ const diffFileTitleBar =
+ (diffFileLargeEnoughScreen && $('.diff-file .file-title-flex-parent:visible').height()) || 0;
- return perfBar + mrTabsHeight + headerHeight + diffFilesChanged;
+ return perfBar + mrTabsHeight + headerHeight + diffFilesChanged + diffFileTitleBar;
};
export const scrollToElement = element => {
diff --git a/app/assets/javascripts/lib/utils/dom_utils.js b/app/assets/javascripts/lib/utils/dom_utils.js
index 6f42382246d..7933c234384 100644
--- a/app/assets/javascripts/lib/utils/dom_utils.js
+++ b/app/assets/javascripts/lib/utils/dom_utils.js
@@ -7,3 +7,8 @@ export const addClassIfElementExists = (element, className) => {
};
export const isInVueNoteablePage = () => isInIssuePage() || isInEpicPage() || isInMRPage();
+
+export const canScrollUp = ({ scrollTop }, margin = 0) => scrollTop > margin;
+
+export const canScrollDown = ({ scrollTop, offsetHeight, scrollHeight }, margin = 0) =>
+ scrollTop + offsetHeight < scrollHeight - margin;
diff --git a/app/assets/javascripts/lib/utils/file_upload.js b/app/assets/javascripts/lib/utils/file_upload.js
new file mode 100644
index 00000000000..b41ffb44971
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/file_upload.js
@@ -0,0 +1,13 @@
+export default (buttonSelector, fileSelector) => {
+ const btn = document.querySelector(buttonSelector);
+ const fileInput = document.querySelector(fileSelector);
+ const form = btn.closest('form');
+
+ btn.addEventListener('click', () => {
+ fileInput.click();
+ });
+
+ fileInput.addEventListener('change', () => {
+ form.querySelector('.js-filename').textContent = fileInput.value.replace(/^.*[\\\/]/, ''); // eslint-disable-line no-useless-escape
+ });
+};
diff --git a/app/assets/javascripts/lib/utils/http_status.js b/app/assets/javascripts/lib/utils/http_status.js
index e4852c85378..14c02218990 100644
--- a/app/assets/javascripts/lib/utils/http_status.js
+++ b/app/assets/javascripts/lib/utils/http_status.js
@@ -16,7 +16,9 @@ const httpStatusCodes = {
IM_USED: 226,
MULTIPLE_CHOICES: 300,
BAD_REQUEST: 400,
+ FORBIDDEN: 403,
NOT_FOUND: 404,
+ UNPROCESSABLE_ENTITY: 422,
};
export const successCodes = [
diff --git a/app/assets/javascripts/lib/utils/users_cache.js b/app/assets/javascripts/lib/utils/users_cache.js
index c0d45e017b4..9f980fd4899 100644
--- a/app/assets/javascripts/lib/utils/users_cache.js
+++ b/app/assets/javascripts/lib/utils/users_cache.js
@@ -22,6 +22,34 @@ class UsersCache extends Cache {
});
// missing catch is intentional, error handling depends on use case
}
+
+ retrieveById(userId) {
+ if (this.hasData(userId) && this.get(userId).username) {
+ return Promise.resolve(this.get(userId));
+ }
+
+ return Api.user(userId).then(({ data }) => {
+ this.internalStorage[userId] = data;
+ return data;
+ });
+ // missing catch is intentional, error handling depends on use case
+ }
+
+ retrieveStatusById(userId) {
+ if (this.hasData(userId) && this.get(userId).status) {
+ return Promise.resolve(this.get(userId).status);
+ }
+
+ return Api.userStatus(userId).then(({ data }) => {
+ if (!this.hasData(userId)) {
+ this.internalStorage[userId] = {};
+ }
+ this.internalStorage[userId].status = data;
+
+ return data;
+ });
+ // missing catch is intentional, error handling depends on use case
+ }
}
export default new UsersCache();
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index a88b575ad99..c866e8d180a 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -30,6 +30,7 @@ import initUsagePingConsent from './usage_ping_consent';
import initPerformanceBar from './performance_bar';
import initSearchAutocomplete from './search_autocomplete';
import GlFieldErrors from './gl_field_errors';
+import initUserPopovers from './user_popovers';
// expose jQuery as global (TODO: remove these)
window.jQuery = jQuery;
@@ -78,6 +79,7 @@ document.addEventListener('DOMContentLoaded', () => {
initTodoToggle();
initLogoAnimation();
initUsagePingConsent();
+ initUserPopovers();
if (document.querySelector('.search')) initSearchAutocomplete();
if (document.querySelector('#js-peek')) initPerformanceBar({ container: '#js-peek' });
diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js
index d32f39881dd..75c18a9b6a0 100644
--- a/app/assets/javascripts/milestone_select.js
+++ b/app/assets/javascripts/milestone_select.js
@@ -155,7 +155,7 @@ export default class MilestoneSelect {
const { $el, e } = clickEvent;
let selected = clickEvent.selectedObj;
- let data, boardsStore;
+ let data, modalStoreFilter;
if (!selected) return;
if (options.handleClick) {
@@ -179,11 +179,11 @@ export default class MilestoneSelect {
}
if ($dropdown.closest('.add-issues-modal').length) {
- boardsStore = ModalStore.store.filter;
+ modalStoreFilter = ModalStore.store.filter;
}
- if (boardsStore) {
- boardsStore[$dropdown.data('fieldName')] = selected.name;
+ if (modalStoreFilter) {
+ modalStoreFilter[$dropdown.data('fieldName')] = selected.name;
e.preventDefault();
} else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
return Issuable.filterResults($dropdown.closest('form'));
diff --git a/app/assets/javascripts/monitoring/components/charts/area.vue b/app/assets/javascripts/monitoring/components/charts/area.vue
new file mode 100644
index 00000000000..12224e36ba2
--- /dev/null
+++ b/app/assets/javascripts/monitoring/components/charts/area.vue
@@ -0,0 +1,97 @@
+<script>
+import { GlAreaChart } from '@gitlab/ui';
+import dateFormat from 'dateformat';
+
+export default {
+ components: {
+ GlAreaChart,
+ },
+ props: {
+ graphData: {
+ type: Object,
+ required: true,
+ validator(data) {
+ return (
+ data.queries &&
+ Array.isArray(data.queries) &&
+ data.queries.filter(query => {
+ if (Array.isArray(query.result)) {
+ return (
+ query.result.filter(res => Array.isArray(res.values)).length === query.result.length
+ );
+ }
+ return false;
+ }).length === data.queries.length
+ );
+ },
+ },
+ },
+ computed: {
+ chartData() {
+ return this.graphData.queries.reduce((accumulator, query) => {
+ const xLabel = `${query.unit}`;
+ accumulator[xLabel] = {};
+ query.result.forEach(res =>
+ res.values.forEach(v => {
+ accumulator[xLabel][v.time.toISOString()] = v.value;
+ }),
+ );
+ return accumulator;
+ }, {});
+ },
+ chartOptions() {
+ return {
+ xAxis: {
+ name: 'Time',
+ type: 'time',
+ axisLabel: {
+ formatter: date => dateFormat(date, 'h:MMtt'),
+ },
+ nameTextStyle: {
+ padding: [18, 0, 0, 0],
+ },
+ },
+ yAxis: {
+ name: this.graphData.y_label,
+ axisLabel: {
+ formatter: value => value.toFixed(3),
+ },
+ nameTextStyle: {
+ padding: [0, 0, 36, 0],
+ },
+ },
+ legend: {
+ formatter: this.xAxisLabel,
+ },
+ };
+ },
+ xAxisLabel() {
+ return this.graphData.queries.map(query => query.label).join(', ');
+ },
+ },
+ methods: {
+ formatTooltipText(params) {
+ const [date, value] = params;
+ return [dateFormat(date, 'dd mmm yyyy, h:MMtt'), value.toFixed(3)];
+ },
+ onCreated(chart) {
+ this.$emit('created', chart);
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="prometheus-graph">
+ <div class="prometheus-graph-header">
+ <h5 class="prometheus-graph-title">{{ graphData.title }}</h5>
+ <div class="prometheus-graph-widgets"><slot></slot></div>
+ </div>
+ <gl-area-chart
+ :data="chartData"
+ :option="chartOptions"
+ :format-tooltip-text="formatTooltipText"
+ @created="onCreated"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue
index 218c508a608..2d9c5050c9b 100644
--- a/app/assets/javascripts/monitoring/components/dashboard.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard.vue
@@ -4,6 +4,7 @@ import { s__ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
import Flash from '../../flash';
import MonitoringService from '../services/monitoring_service';
+import MonitorAreaChart from './charts/area.vue';
import GraphGroup from './graph_group.vue';
import Graph from './graph.vue';
import EmptyState from './empty_state.vue';
@@ -12,6 +13,7 @@ import eventHub from '../event_hub';
export default {
components: {
+ MonitorAreaChart,
Graph,
GraphGroup,
EmptyState,
@@ -102,6 +104,9 @@ export default {
};
},
computed: {
+ graphComponent() {
+ return gon.features && gon.features.areaChart ? MonitorAreaChart : Graph;
+ },
forceRedraw() {
return this.elWidth;
},
@@ -207,7 +212,8 @@ export default {
:name="groupData.group"
:show-panels="showPanels"
>
- <graph
+ <component
+ :is="graphComponent"
v-for="(graphData, graphIndex) in groupData.metrics"
:key="graphIndex"
:graph-data="graphData"
@@ -220,7 +226,7 @@ export default {
>
<!-- EE content -->
{{ null }}
- </graph>
+ </component>
</graph-group>
</div>
<empty-state
diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue
index 841fcec96e8..ce56beb1e6b 100644
--- a/app/assets/javascripts/notes/components/comment_form.vue
+++ b/app/assets/javascripts/notes/components/comment_form.vue
@@ -247,15 +247,19 @@ Please check your network connection and try again.`;
} else {
this.reopenIssue()
.then(() => this.enableButton())
- .catch(() => {
+ .catch(({ data }) => {
this.enableButton();
this.toggleStateButtonLoading(false);
- Flash(
- sprintf(
- __('Something went wrong while reopening the %{issuable}. Please try again later'),
- { issuable: this.noteableDisplayName },
- ),
+ let errorMessage = sprintf(
+ __('Something went wrong while reopening the %{issuable}. Please try again later'),
+ { issuable: this.noteableDisplayName },
);
+
+ if (data) {
+ errorMessage = Object.values(data).join('\n');
+ }
+
+ Flash(errorMessage);
});
}
},
diff --git a/app/assets/javascripts/notes/components/note_edited_text.vue b/app/assets/javascripts/notes/components/note_edited_text.vue
index 3d3dbbd7fe1..15ce49d7c31 100644
--- a/app/assets/javascripts/notes/components/note_edited_text.vue
+++ b/app/assets/javascripts/notes/components/note_edited_text.vue
@@ -39,7 +39,10 @@ export default {
<div :class="className">
{{ actionText }}
<template v-if="editedBy">
- by <a :href="editedBy.path" class="js-vue-author author-link"> {{ editedBy.name }} </a>
+ by
+ <a :href="editedBy.path" :data-user-id="editedBy.id" class="js-user-link author-link">
+ {{ editedBy.name }}
+ </a>
</template>
{{ actionDetailText }}
<time-ago-tooltip :time="editedAt" tooltip-placement="bottom" />
diff --git a/app/assets/javascripts/notes/components/note_header.vue b/app/assets/javascripts/notes/components/note_header.vue
index e1a58e7cb26..7b39901024d 100644
--- a/app/assets/javascripts/notes/components/note_header.vue
+++ b/app/assets/javascripts/notes/components/note_header.vue
@@ -73,7 +73,14 @@ export default {
{{ __('Toggle discussion') }}
</button>
</div>
- <a v-if="hasAuthor" v-once :href="author.path">
+ <a
+ v-if="hasAuthor"
+ v-once
+ :href="author.path"
+ class="js-user-link"
+ :data-user-id="author.id"
+ :data-username="author.username"
+ >
<span class="note-header-author-name">{{ author.name }}</span>
<span v-if="author.status_tooltip_html" v-html="author.status_tooltip_html"></span>
<span class="note-headline-light"> @{{ author.username }} </span>
diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue
index f4991a41325..5c9a28b8512 100644
--- a/app/assets/javascripts/notes/components/noteable_discussion.vue
+++ b/app/assets/javascripts/notes/components/noteable_discussion.vue
@@ -81,6 +81,7 @@ export default {
'nextUnresolvedDiscussionId',
'unresolvedDiscussionsCount',
'hasUnresolvedDiscussions',
+ 'showJumpToNextDiscussion',
]),
author() {
return this.initialDiscussion.author;
@@ -121,6 +122,12 @@ export default {
resolvedText() {
return this.discussion.resolved_by_push ? __('Automatically resolved') : __('Resolved');
},
+ shouldShowJumpToNextDiscussion() {
+ return this.showJumpToNextDiscussion(
+ this.discussion.id,
+ this.discussionsByDiffOrder ? 'diff' : 'discussion',
+ );
+ },
shouldRenderDiffs() {
return this.discussion.diff_discussion && this.renderDiffFile;
},
@@ -153,10 +160,14 @@ export default {
return expanded || this.alwaysExpanded || isResolvedNonDiffDiscussion;
},
actionText() {
- const commitId = this.discussion.commit_id ? truncateSha(this.discussion.commit_id) : '';
const linkStart = `<a href="${_.escape(this.discussion.discussion_path)}">`;
const linkEnd = '</a>';
+ let { commit_id: commitId } = this.discussion;
+ if (commitId) {
+ commitId = `<span class="commit-sha">${truncateSha(commitId)}</span>`;
+ }
+
let text = s__('MergeRequests|started a discussion');
if (this.discussion.for_commit) {
@@ -387,7 +398,7 @@ Please check your network connection and try again.`;
<div class="discussion-with-resolve-btn">
<button
type="button"
- class="js-vue-discussion-reply btn btn-text-field mr-sm-2 qa-discussion-reply"
+ class="js-vue-discussion-reply btn btn-text-field qa-discussion-reply"
title="Add a reply"
@click="showReplyForm"
>
@@ -396,7 +407,7 @@ Please check your network connection and try again.`;
<div v-if="discussion.resolvable">
<button
type="button"
- class="btn btn-default mr-sm-2"
+ class="btn btn-default ml-sm-2"
@click="resolveHandler();"
>
<i v-if="isResolving" aria-hidden="true" class="fa fa-spinner fa-spin"></i>
@@ -418,7 +429,7 @@ Please check your network connection and try again.`;
<icon name="issue-new" />
</a>
</div>
- <div v-if="hasUnresolvedDiscussions" class="btn-group" role="group">
+ <div v-if="shouldShowJumpToNextDiscussion" class="btn-group" role="group">
<button
v-gl-tooltip
class="btn btn-default discussion-next-btn"
diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue
index 6e6efb04753..27f896cee35 100644
--- a/app/assets/javascripts/notes/components/notes_app.vue
+++ b/app/assets/javascripts/notes/components/notes_app.vue
@@ -12,6 +12,7 @@ import placeholderNote from '../../vue_shared/components/notes/placeholder_note.
import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue';
import skeletonLoadingContainer from '../../vue_shared/components/notes/skeleton_note.vue';
import highlightCurrentUser from '~/behaviors/markdown/highlight_current_user';
+import initUserPopovers from '../../user_popovers';
export default {
name: 'NotesApp',
@@ -101,12 +102,15 @@ export default {
if (parentElement && parentElement.classList.contains('js-vue-notes-event')) {
parentElement.addEventListener('toggleAward', event => {
const { awardName, noteId } = event.detail;
- this.actionToggleAward({ awardName, noteId });
+ this.toggleAward({ awardName, noteId });
});
}
},
updated() {
- this.$nextTick(() => highlightCurrentUser(this.$el.querySelectorAll('.gfm-project_member')));
+ this.$nextTick(() => {
+ highlightCurrentUser(this.$el.querySelectorAll('.gfm-project_member'));
+ initUserPopovers(this.$el.querySelectorAll('.js-user-link'));
+ });
},
methods: {
...mapActions([
diff --git a/app/assets/javascripts/notes/mixins/discussion_navigation.js b/app/assets/javascripts/notes/mixins/discussion_navigation.js
index f7c4deee1f8..3d89d907777 100644
--- a/app/assets/javascripts/notes/mixins/discussion_navigation.js
+++ b/app/assets/javascripts/notes/mixins/discussion_navigation.js
@@ -1,29 +1,56 @@
import { scrollToElement } from '~/lib/utils/common_utils';
+import eventHub from '../../notes/event_hub';
export default {
methods: {
- jumpToDiscussion(id) {
- if (id) {
- const activeTab = window.mrTabs.currentAction;
- const selector =
- activeTab === 'diffs'
- ? `ul.notes[data-discussion-id="${id}"]`
- : `div.discussion[data-discussion-id="${id}"]`;
- const el = document.querySelector(selector);
+ diffsJump(id) {
+ const selector = `ul.notes[data-discussion-id="${id}"]`;
- if (activeTab === 'commits' || activeTab === 'pipelines') {
- window.mrTabs.activateTab('show');
- }
+ eventHub.$once('scrollToDiscussion', () => {
+ const el = document.querySelector(selector);
if (el) {
- this.expandDiscussion({ discussionId: id });
-
scrollToElement(el);
+
return true;
}
+
+ return false;
+ });
+
+ this.expandDiscussion({ discussionId: id });
+ },
+ discussionJump(id) {
+ const selector = `div.discussion[data-discussion-id="${id}"]`;
+
+ const el = document.querySelector(selector);
+
+ this.expandDiscussion({ discussionId: id });
+
+ if (el) {
+ scrollToElement(el);
+
+ return true;
}
return false;
},
+ jumpToDiscussion(id) {
+ if (id) {
+ const activeTab = window.mrTabs.currentAction;
+
+ if (activeTab === 'diffs') {
+ this.diffsJump(id);
+ } else if (activeTab === 'commits' || activeTab === 'pipelines') {
+ window.mrTabs.eventHub.$once('MergeRequestTabChange', () => {
+ setTimeout(() => this.discussionJump(id), 0);
+ });
+
+ window.mrTabs.tabShown('show');
+ } else {
+ this.discussionJump(id);
+ }
+ }
+ },
},
};
diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js
index b4befdd6e4a..4716ab52333 100644
--- a/app/assets/javascripts/notes/stores/actions.js
+++ b/app/assets/javascripts/notes/stores/actions.js
@@ -17,7 +17,13 @@ import { __ } from '~/locale';
let eTagPoll;
-export const expandDiscussion = ({ commit }, data) => commit(types.EXPAND_DISCUSSION, data);
+export const expandDiscussion = ({ commit, dispatch }, data) => {
+ if (data.discussionId) {
+ dispatch('diffs/renderFileForDiscussionId', data.discussionId, { root: true });
+ }
+
+ commit(types.EXPAND_DISCUSSION, data);
+};
export const collapseDiscussion = ({ commit }, data) => commit(types.COLLAPSE_DISCUSSION, data);
diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js
index 2ed8aac059a..0ffc0cb2593 100644
--- a/app/assets/javascripts/notes/stores/getters.js
+++ b/app/assets/javascripts/notes/stores/getters.js
@@ -57,6 +57,17 @@ export const unresolvedDiscussionsCount = state => state.unresolvedDiscussionsCo
export const resolvableDiscussionsCount = state => state.resolvableDiscussionsCount;
export const hasUnresolvedDiscussions = state => state.hasUnresolvedDiscussions;
+export const showJumpToNextDiscussion = (state, getters) => (discussionId, mode = 'discussion') => {
+ const orderedDiffs =
+ mode !== 'discussion'
+ ? getters.unresolvedDiscussionsIdsByDiff
+ : getters.unresolvedDiscussionsIdsByDate;
+
+ const indexOf = orderedDiffs.indexOf(discussionId);
+
+ return indexOf !== -1 && indexOf < orderedDiffs.length - 1;
+};
+
export const isDiscussionResolved = (state, getters) => discussionId =>
getters.resolvedDiscussionsById[discussionId] !== undefined;
@@ -104,7 +115,7 @@ export const unresolvedDiscussionsIdsByDate = (state, getters) =>
// line numbers.
export const unresolvedDiscussionsIdsByDiff = (state, getters) =>
getters.allResolvableDiscussions
- .filter(d => !d.resolved)
+ .filter(d => !d.resolved && d.active)
.sort((a, b) => {
if (!a.diff_file || !b.diff_file) {
return 0;
diff --git a/app/assets/javascripts/notes/stores/mutations.js b/app/assets/javascripts/notes/stores/mutations.js
index bea396e5bb6..39ff0ff73d7 100644
--- a/app/assets/javascripts/notes/stores/mutations.js
+++ b/app/assets/javascripts/notes/stores/mutations.js
@@ -22,6 +22,7 @@ export default {
if (isDiscussion && isInMRPage()) {
noteData.resolvable = note.resolvable;
noteData.resolved = false;
+ noteData.active = true;
noteData.resolve_path = note.resolve_path;
noteData.resolve_with_issue_path = note.resolve_with_issue_path;
noteData.diff_discussion = false;
@@ -245,7 +246,7 @@ export default {
discussion =>
!discussion.individual_note &&
discussion.resolvable &&
- discussion.notes.some(note => !note.resolved),
+ discussion.notes.some(note => note.resolvable && !note.resolved),
).length;
state.hasUnresolvedDiscussions = state.unresolvedDiscussionsCount > 1;
},
diff --git a/app/assets/javascripts/notifications_dropdown.js b/app/assets/javascripts/notifications_dropdown.js
index c4c8cf86cb0..e7fa05faa8a 100644
--- a/app/assets/javascripts/notifications_dropdown.js
+++ b/app/assets/javascripts/notifications_dropdown.js
@@ -12,6 +12,10 @@ export default function notificationsDropdown() {
const form = $(this).parents('.notification-form:first');
form.find('.js-notification-loading').toggleClass('fa-bell fa-spin fa-spinner');
+ if (form.hasClass('no-label')) {
+ form.find('.js-notification-loading').toggleClass('hidden');
+ form.find('.js-notifications-icon').toggleClass('hidden');
+ }
form.find('#notification_setting_level').val(notificationLevel);
form.submit();
});
diff --git a/app/assets/javascripts/pages/dashboard/projects/index.js b/app/assets/javascripts/pages/dashboard/projects/index.js
index 0c585e162cb..8f98be79640 100644
--- a/app/assets/javascripts/pages/dashboard/projects/index.js
+++ b/app/assets/javascripts/pages/dashboard/projects/index.js
@@ -1,3 +1,7 @@
import ProjectsList from '~/projects_list';
+import Star from '../../../star';
-document.addEventListener('DOMContentLoaded', () => new ProjectsList());
+document.addEventListener('DOMContentLoaded', () => {
+ new ProjectsList(); // eslint-disable-line no-new
+ new Star('.project-row'); // eslint-disable-line no-new
+});
diff --git a/app/assets/javascripts/pages/groups/clusters/index/index.js b/app/assets/javascripts/pages/groups/clusters/index/index.js
index 21efc4f6d00..845a5f7042c 100644
--- a/app/assets/javascripts/pages/groups/clusters/index/index.js
+++ b/app/assets/javascripts/pages/groups/clusters/index/index.js
@@ -1,7 +1,5 @@
-import PersistentUserCallout from '~/persistent_user_callout';
+import initDismissableCallout from '~/dismissable_callout';
document.addEventListener('DOMContentLoaded', () => {
- const callout = document.querySelector('.gcp-signup-offer');
-
- if (callout) new PersistentUserCallout(callout); // eslint-disable-line no-new
+ initDismissableCallout('.gcp-signup-offer');
});
diff --git a/app/assets/javascripts/pages/groups/index.js b/app/assets/javascripts/pages/groups/index.js
index 00e2d7fc998..bf80d8b8193 100644
--- a/app/assets/javascripts/pages/groups/index.js
+++ b/app/assets/javascripts/pages/groups/index.js
@@ -1,12 +1,6 @@
-import PersistentUserCallout from '~/persistent_user_callout';
+import initDismissableCallout from '~/dismissable_callout';
import initGkeDropdowns from '~/projects/gke_cluster_dropdowns';
-function initCallout() {
- const callout = document.querySelector('.gcp-signup-offer');
-
- if (callout) new PersistentUserCallout(callout); // eslint-disable-line no-new
-}
-
document.addEventListener('DOMContentLoaded', () => {
const { page } = document.body.dataset;
const newClusterViews = [
@@ -16,7 +10,7 @@ document.addEventListener('DOMContentLoaded', () => {
];
if (newClusterViews.indexOf(page) > -1) {
- initCallout();
+ initDismissableCallout('.gcp-signup-offer');
initGkeDropdowns();
}
});
diff --git a/app/assets/javascripts/pages/profiles/show/emoji_menu.js b/app/assets/javascripts/pages/profiles/show/emoji_menu.js
index 094837b40e0..286c1f1e929 100644
--- a/app/assets/javascripts/pages/profiles/show/emoji_menu.js
+++ b/app/assets/javascripts/pages/profiles/show/emoji_menu.js
@@ -1,3 +1,4 @@
+import '~/commons/bootstrap';
import { AwardsHandler } from '~/awards_handler';
class EmojiMenu extends AwardsHandler {
diff --git a/app/assets/javascripts/pages/projects/clusters/index/index.js b/app/assets/javascripts/pages/projects/clusters/index/index.js
index 21efc4f6d00..845a5f7042c 100644
--- a/app/assets/javascripts/pages/projects/clusters/index/index.js
+++ b/app/assets/javascripts/pages/projects/clusters/index/index.js
@@ -1,7 +1,5 @@
-import PersistentUserCallout from '~/persistent_user_callout';
+import initDismissableCallout from '~/dismissable_callout';
document.addEventListener('DOMContentLoaded', () => {
- const callout = document.querySelector('.gcp-signup-offer');
-
- if (callout) new PersistentUserCallout(callout); // eslint-disable-line no-new
+ initDismissableCallout('.gcp-signup-offer');
});
diff --git a/app/assets/javascripts/pages/projects/edit/index.js b/app/assets/javascripts/pages/projects/edit/index.js
index f5b1cf85e68..899d5925956 100644
--- a/app/assets/javascripts/pages/projects/edit/index.js
+++ b/app/assets/javascripts/pages/projects/edit/index.js
@@ -3,8 +3,8 @@ import initSettingsPanels from '~/settings_panels';
import setupProjectEdit from '~/project_edit';
import initConfirmDangerModal from '~/confirm_danger_modal';
import mountBadgeSettings from '~/pages/shared/mount_badge_settings';
+import fileUpload from '~/lib/utils/file_upload';
import initProjectLoadingSpinner from '../shared/save_project_loader';
-import projectAvatar from '../shared/project_avatar';
import initProjectPermissionsSettings from '../shared/permissions';
document.addEventListener('DOMContentLoaded', () => {
@@ -12,7 +12,7 @@ document.addEventListener('DOMContentLoaded', () => {
setupProjectEdit();
// Initialize expandable settings panels
initSettingsPanels();
- projectAvatar();
+ fileUpload('.js-choose-project-avatar-button', '.js-project-avatar-input');
initProjectPermissionsSettings();
initConfirmDangerModal();
mountBadgeSettings(PROJECT_BADGE);
diff --git a/app/assets/javascripts/pages/projects/index.js b/app/assets/javascripts/pages/projects/index.js
index b0345b4e50d..5659e13981a 100644
--- a/app/assets/javascripts/pages/projects/index.js
+++ b/app/assets/javascripts/pages/projects/index.js
@@ -1,5 +1,5 @@
+import initDismissableCallout from '~/dismissable_callout';
import initGkeDropdowns from '~/projects/gke_cluster_dropdowns';
-import PersistentUserCallout from '../../persistent_user_callout';
import Project from './project';
import ShortcutsNavigation from '../../behaviors/shortcuts/shortcuts_navigation';
@@ -12,9 +12,7 @@ document.addEventListener('DOMContentLoaded', () => {
];
if (newClusterViews.indexOf(page) > -1) {
- const callout = document.querySelector('.gcp-signup-offer');
- if (callout) new PersistentUserCallout(callout); // eslint-disable-line no-new
-
+ initDismissableCallout('.gcp-signup-offer');
initGkeDropdowns();
}
diff --git a/app/assets/javascripts/pages/projects/project.js b/app/assets/javascripts/pages/projects/project.js
index a6bee49a6b1..b288989b252 100644
--- a/app/assets/javascripts/pages/projects/project.js
+++ b/app/assets/javascripts/pages/projects/project.js
@@ -13,6 +13,9 @@ export default class Project {
const $cloneOptions = $('ul.clone-options-dropdown');
const $projectCloneField = $('#project_clone');
const $cloneBtnLabel = $('.js-git-clone-holder .js-clone-dropdown-label');
+ const mobileCloneField = document.querySelector(
+ '.js-mobile-git-clone .js-clone-dropdown-label',
+ );
const selectedCloneOption = $cloneBtnLabel.text().trim();
if (selectedCloneOption.length > 0) {
@@ -36,7 +39,11 @@ export default class Project {
$label.text(activeText);
});
- $projectCloneField.val(url);
+ if (mobileCloneField) {
+ mobileCloneField.dataset.clipboardText = url;
+ } else {
+ $projectCloneField.val(url);
+ }
$('.js-git-empty .js-clone').text(url);
});
// Ref switcher
diff --git a/app/assets/javascripts/pages/projects/serverless/index.js b/app/assets/javascripts/pages/projects/serverless/index.js
new file mode 100644
index 00000000000..7b08620773c
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/serverless/index.js
@@ -0,0 +1,5 @@
+import ServerlessBundle from '~/serverless/serverless_bundle';
+
+document.addEventListener('DOMContentLoaded', () => {
+ new ServerlessBundle(); // eslint-disable-line no-new
+});
diff --git a/app/assets/javascripts/pages/projects/settings/repository/form.js b/app/assets/javascripts/pages/projects/settings/repository/form.js
index a52861c9efa..3e02893f24c 100644
--- a/app/assets/javascripts/pages/projects/settings/repository/form.js
+++ b/app/assets/javascripts/pages/projects/settings/repository/form.js
@@ -7,6 +7,7 @@ import initDeployKeys from '~/deploy_keys';
import ProtectedBranchCreate from '~/protected_branches/protected_branch_create';
import ProtectedBranchEditList from '~/protected_branches/protected_branch_edit_list';
import DueDateSelectors from '~/due_date_select';
+import fileUpload from '~/lib/utils/file_upload';
export default () => {
new ProtectedTagCreate();
@@ -16,4 +17,5 @@ export default () => {
new ProtectedBranchCreate();
new ProtectedBranchEditList();
new DueDateSelectors();
+ fileUpload('.js-choose-file', '.js-object-map-input');
};
diff --git a/app/assets/javascripts/pages/projects/shared/project_avatar.js b/app/assets/javascripts/pages/projects/shared/project_avatar.js
deleted file mode 100644
index 1e69ecb481d..00000000000
--- a/app/assets/javascripts/pages/projects/shared/project_avatar.js
+++ /dev/null
@@ -1,16 +0,0 @@
-import $ from 'jquery';
-
-export default function projectAvatar() {
- $('.js-choose-project-avatar-button').bind('click', function onClickAvatar() {
- const form = $(this).closest('form');
- return form.find('.js-project-avatar-input').click();
- });
-
- $('.js-project-avatar-input').bind('change', function onClickAvatarInput() {
- const form = $(this).closest('form');
- const filename = $(this)
- .val()
- .replace(/^.*[\\\/]/, ''); // eslint-disable-line no-useless-escape
- return form.find('.js-avatar-filename').text(filename);
- });
-}
diff --git a/app/assets/javascripts/pages/users/user_tabs.js b/app/assets/javascripts/pages/users/user_tabs.js
index aa537d4a43e..1c3fd58ca74 100644
--- a/app/assets/javascripts/pages/users/user_tabs.js
+++ b/app/assets/javascripts/pages/users/user_tabs.js
@@ -151,8 +151,10 @@ export default class UserTabs {
loadTab(action, endpoint) {
this.toggleLoading(true);
+ const params = action === 'projects' ? { skip_namespace: true } : {};
+
return axios
- .get(endpoint)
+ .get(endpoint, { params })
.then(({ data }) => {
const tabSelector = `div#${action}`;
this.$parentEl.find(tabSelector).html(data.html);
@@ -188,7 +190,7 @@ export default class UserTabs {
requestParams: { limit: 10 },
});
UserTabs.renderMostRecentBlocks('#js-overview .projects-block', {
- requestParams: { limit: 10, skip_pagination: true },
+ requestParams: { limit: 10, skip_pagination: true, skip_namespace: true, compact_mode: true },
});
this.loaded.overview = true;
@@ -206,6 +208,8 @@ export default class UserTabs {
loadActivityCalendar() {
const $calendarWrap = this.$parentEl.find('.tab-pane.active .user-calendar');
+ if (!$calendarWrap.length) return;
+
const calendarPath = $calendarWrap.data('calendarPath');
AjaxCache.retrieve(calendarPath)
diff --git a/app/assets/javascripts/persistent_user_callout.js b/app/assets/javascripts/persistent_user_callout.js
deleted file mode 100644
index 1e34e74a152..00000000000
--- a/app/assets/javascripts/persistent_user_callout.js
+++ /dev/null
@@ -1,34 +0,0 @@
-import axios from './lib/utils/axios_utils';
-import { __ } from './locale';
-import Flash from './flash';
-
-export default class PersistentUserCallout {
- constructor(container) {
- const { dismissEndpoint, featureId } = container.dataset;
- this.container = container;
- this.dismissEndpoint = dismissEndpoint;
- this.featureId = featureId;
-
- this.init();
- }
-
- init() {
- const closeButton = this.container.querySelector('.js-close');
- closeButton.addEventListener('click', event => this.dismiss(event));
- }
-
- dismiss(event) {
- event.preventDefault();
-
- axios
- .post(this.dismissEndpoint, {
- feature_name: this.featureId,
- })
- .then(() => {
- this.container.remove();
- })
- .catch(() => {
- Flash(__('An error occurred while dismissing the alert. Refresh the page and try again.'));
- });
- }
-}
diff --git a/app/assets/javascripts/registry/components/app.vue b/app/assets/javascripts/registry/components/app.vue
index 6233fb169e9..9af5660f764 100644
--- a/app/assets/javascripts/registry/components/app.vue
+++ b/app/assets/javascripts/registry/components/app.vue
@@ -1,15 +1,13 @@
<script>
import { mapGetters, mapActions } from 'vuex';
import { GlLoadingIcon } from '@gitlab/ui';
-import Flash from '../../flash';
import store from '../stores';
-import collapsibleContainer from './collapsible_container.vue';
-import { errorMessages, errorMessagesTypes } from '../constants';
+import CollapsibleContainer from './collapsible_container.vue';
export default {
name: 'RegistryListApp',
components: {
- collapsibleContainer,
+ CollapsibleContainer,
GlLoadingIcon,
},
props: {
@@ -26,7 +24,7 @@ export default {
this.setMainEndpoint(this.endpoint);
},
mounted() {
- this.fetchRepos().catch(() => Flash(errorMessages[errorMessagesTypes.FETCH_REPOS]));
+ this.fetchRepos();
},
methods: {
...mapActions(['setMainEndpoint', 'fetchRepos']),
@@ -38,9 +36,9 @@ export default {
<gl-loading-icon v-if="isLoading" :size="3" />
<collapsible-container
- v-for="(item, index) in repos"
+ v-for="item in repos"
v-else-if="!isLoading && repos.length"
- :key="index"
+ :key="item.id"
:repo="item"
/>
diff --git a/app/assets/javascripts/registry/components/collapsible_container.vue b/app/assets/javascripts/registry/components/collapsible_container.vue
index 6514c05a9c7..5451c61026c 100644
--- a/app/assets/javascripts/registry/components/collapsible_container.vue
+++ b/app/assets/javascripts/registry/components/collapsible_container.vue
@@ -1,22 +1,24 @@
<script>
import { mapActions } from 'vuex';
-import { GlLoadingIcon } from '@gitlab/ui';
-import Flash from '../../flash';
-import clipboardButton from '../../vue_shared/components/clipboard_button.vue';
-import tooltip from '../../vue_shared/directives/tooltip';
-import tableRegistry from './table_registry.vue';
+import { GlLoadingIcon, GlButton, GlTooltipDirective } from '@gitlab/ui';
+import createFlash from '../../flash';
+import ClipboardButton from '../../vue_shared/components/clipboard_button.vue';
+import Icon from '../../vue_shared/components/icon.vue';
+import TableRegistry from './table_registry.vue';
import { errorMessages, errorMessagesTypes } from '../constants';
import { __ } from '../../locale';
export default {
name: 'CollapsibeContainerRegisty',
components: {
- clipboardButton,
- tableRegistry,
+ ClipboardButton,
+ TableRegistry,
GlLoadingIcon,
+ GlButton,
+ Icon,
},
directives: {
- tooltip,
+ GlTooltip: GlTooltipDirective,
},
props: {
repo: {
@@ -29,30 +31,30 @@ export default {
isOpen: false,
};
},
+ computed: {
+ iconName() {
+ return this.isOpen ? 'angle-up' : 'angle-right';
+ },
+ },
methods: {
...mapActions(['fetchRepos', 'fetchList', 'deleteRepo']),
-
toggleRepo() {
this.isOpen = !this.isOpen;
if (this.isOpen) {
- this.fetchList({ repo: this.repo }).catch(() =>
- this.showError(errorMessagesTypes.FETCH_REGISTRY),
- );
+ this.fetchList({ repo: this.repo });
}
},
-
handleDeleteRepository() {
this.deleteRepo(this.repo)
.then(() => {
- Flash(__('This container registry has been scheduled for deletion.'), 'notice');
+ createFlash(__('This container registry has been scheduled for deletion.'), 'notice');
this.fetchRepos();
})
.catch(() => this.showError(errorMessagesTypes.DELETE_REPO));
},
-
showError(message) {
- Flash(errorMessages[message]);
+ createFlash(errorMessages[message]);
},
},
};
@@ -61,18 +63,9 @@ export default {
<template>
<div class="container-image">
<div class="container-image-head">
- <button type="button" class="js-toggle-repo btn-link" @click="toggleRepo">
- <i
- :class="{
- 'fa-chevron-right': !isOpen,
- 'fa-chevron-up': isOpen,
- }"
- class="fa"
- aria-hidden="true"
- >
- </i>
- {{ repo.name }}
- </button>
+ <gl-button class="js-toggle-repo btn-link align-baseline" @click="toggleRepo">
+ <icon :name="iconName" /> {{ repo.name }}
+ </gl-button>
<clipboard-button
v-if="repo.location"
@@ -82,17 +75,17 @@ export default {
/>
<div class="controls d-none d-sm-block float-right">
- <button
+ <gl-button
v-if="repo.canDelete"
- v-tooltip
+ v-gl-tooltip
:title="s__('ContainerRegistry|Remove repository')"
:aria-label="s__('ContainerRegistry|Remove repository')"
- type="button"
- class="js-remove-repo btn btn-danger"
+ class="js-remove-repo"
+ variant="danger"
@click="handleDeleteRepository"
>
- <i class="fa fa-trash" aria-hidden="true"> </i>
- </button>
+ <icon name="remove" />
+ </gl-button>
</div>
</div>
diff --git a/app/assets/javascripts/registry/components/table_registry.vue b/app/assets/javascripts/registry/components/table_registry.vue
index 6735c3ff7cf..78c7671856a 100644
--- a/app/assets/javascripts/registry/components/table_registry.vue
+++ b/app/assets/javascripts/registry/components/table_registry.vue
@@ -1,21 +1,24 @@
<script>
import { mapActions } from 'vuex';
+import { GlButton, GlTooltipDirective } from '@gitlab/ui';
import { n__ } from '../../locale';
-import Flash from '../../flash';
-import clipboardButton from '../../vue_shared/components/clipboard_button.vue';
-import tablePagination from '../../vue_shared/components/table_pagination.vue';
-import tooltip from '../../vue_shared/directives/tooltip';
+import createFlash from '../../flash';
+import ClipboardButton from '../../vue_shared/components/clipboard_button.vue';
+import TablePagination from '../../vue_shared/components/table_pagination.vue';
+import Icon from '../../vue_shared/components/icon.vue';
import timeagoMixin from '../../vue_shared/mixins/timeago';
import { errorMessages, errorMessagesTypes } from '../constants';
import { numberToHumanSize } from '../../lib/utils/number_utils';
export default {
components: {
- clipboardButton,
- tablePagination,
+ ClipboardButton,
+ TablePagination,
+ GlButton,
+ Icon,
},
directives: {
- tooltip,
+ GlTooltip: GlTooltipDirective,
},
mixins: [timeagoMixin],
props: {
@@ -31,29 +34,24 @@ export default {
},
methods: {
...mapActions(['fetchList', 'deleteRegistry']),
-
layers(item) {
return item.layers ? n__('%d layer', '%d layers', item.layers) : '';
},
-
formatSize(size) {
return numberToHumanSize(size);
},
-
handleDeleteRegistry(registry) {
this.deleteRegistry(registry)
.then(() => this.fetchList({ repo: this.repo }))
.catch(() => this.showError(errorMessagesTypes.DELETE_REGISTRY));
},
-
onPageChange(pageNumber) {
this.fetchList({ repo: this.repo, page: pageNumber }).catch(() =>
this.showError(errorMessagesTypes.FETCH_REGISTRY),
);
},
-
showError(message) {
- Flash(errorMessages[message]);
+ createFlash(errorMessages[message]);
},
},
};
@@ -71,10 +69,9 @@ export default {
</tr>
</thead>
<tbody>
- <tr v-for="(item, i) in repo.list" :key="i">
+ <tr v-for="item in repo.list" :key="item.tag">
<td>
{{ item.tag }}
-
<clipboard-button
v-if="item.location"
:title="item.location"
@@ -83,37 +80,34 @@ export default {
/>
</td>
<td>
- <span v-tooltip :title="item.revision" data-placement="bottom">
- {{ item.shortRevision }}
- </span>
+ <span v-gl-tooltip.bottom :title="item.revision">{{ item.shortRevision }}</span>
</td>
<td>
{{ formatSize(item.size) }}
- <template v-if="item.size && item.layers">
- &middot;
- </template>
+ <template v-if="item.size && item.layers"
+ >&middot;</template
+ >
{{ layers(item) }}
</td>
<td>
- <span v-tooltip :title="tooltipTitle(item.createdAt)" data-placement="bottom">
- {{ timeFormated(item.createdAt) }}
- </span>
+ <span v-gl-tooltip.bottom :title="tooltipTitle(item.createdAt)">{{
+ timeFormated(item.createdAt)
+ }}</span>
</td>
<td class="content">
- <button
+ <gl-button
v-if="item.canDelete"
- v-tooltip
+ v-gl-tooltip
:title="s__('ContainerRegistry|Remove tag')"
:aria-label="s__('ContainerRegistry|Remove tag')"
- type="button"
- class="js-delete-registry btn btn-danger d-none d-sm-block float-right"
- data-container="body"
+ variant="danger"
+ class="js-delete-registry d-none d-sm-block float-right"
@click="handleDeleteRegistry(item);"
>
- <i class="fa fa-trash" aria-hidden="true"> </i>
- </button>
+ <icon name="remove" />
+ </gl-button>
</td>
</tr>
</tbody>
diff --git a/app/assets/javascripts/registry/stores/actions.js b/app/assets/javascripts/registry/stores/actions.js
index a78aa90b7b5..51d057c62c1 100644
--- a/app/assets/javascripts/registry/stores/actions.js
+++ b/app/assets/javascripts/registry/stores/actions.js
@@ -1,39 +1,45 @@
-import Vue from 'vue';
-import VueResource from 'vue-resource';
+import axios from '~/lib/utils/axios_utils';
+import createFlash from '~/flash';
import * as types from './mutation_types';
-
-Vue.use(VueResource);
+import { errorMessages, errorMessagesTypes } from '../constants';
export const fetchRepos = ({ commit, state }) => {
commit(types.TOGGLE_MAIN_LOADING);
- return Vue.http
+ return axios
.get(state.endpoint)
- .then(res => res.json())
- .then(response => {
+ .then(({ data }) => {
+ commit(types.TOGGLE_MAIN_LOADING);
+ commit(types.SET_REPOS_LIST, data);
+ })
+ .catch(() => {
commit(types.TOGGLE_MAIN_LOADING);
- commit(types.SET_REPOS_LIST, response);
+ createFlash(errorMessages[errorMessagesTypes.FETCH_REPOS]);
});
};
export const fetchList = ({ commit }, { repo, page }) => {
commit(types.TOGGLE_REGISTRY_LIST_LOADING, repo);
- return Vue.http.get(repo.tagsPath, { params: { page } }).then(response => {
- const { headers } = response;
+ return axios
+ .get(repo.tagsPath, { params: { page } })
+ .then(response => {
+ const { headers, data } = response;
- return response.json().then(resp => {
commit(types.TOGGLE_REGISTRY_LIST_LOADING, repo);
- commit(types.SET_REGISTRY_LIST, { repo, resp, headers });
+ commit(types.SET_REGISTRY_LIST, { repo, resp: data, headers });
+ })
+ .catch(() => {
+ commit(types.TOGGLE_REGISTRY_LIST_LOADING, repo);
+ createFlash(errorMessages[errorMessagesTypes.FETCH_REGISTRY]);
});
- });
};
// eslint-disable-next-line no-unused-vars
-export const deleteRepo = ({ commit }, repo) => Vue.http.delete(repo.destroyPath);
+export const deleteRepo = ({ commit }, repo) => axios.delete(repo.destroyPath);
// eslint-disable-next-line no-unused-vars
-export const deleteRegistry = ({ commit }, image) => Vue.http.delete(image.destroyPath);
+export const deleteRegistry = ({ commit }, image) => axios.delete(image.destroyPath);
export const setMainEndpoint = ({ commit }, data) => commit(types.SET_MAIN_ENDPOINT, data);
export const toggleLoading = ({ commit }) => commit(types.TOGGLE_MAIN_LOADING);
diff --git a/app/assets/javascripts/registry/stores/index.js b/app/assets/javascripts/registry/stores/index.js
index 78b67881210..1bb06bd6e81 100644
--- a/app/assets/javascripts/registry/stores/index.js
+++ b/app/assets/javascripts/registry/stores/index.js
@@ -3,36 +3,12 @@ import Vuex from 'vuex';
import * as actions from './actions';
import * as getters from './getters';
import mutations from './mutations';
+import createState from './state';
Vue.use(Vuex);
export default new Vuex.Store({
- state: {
- isLoading: false,
- endpoint: '', // initial endpoint to fetch the repos list
- /**
- * Each object in `repos` has the following strucure:
- * {
- * name: String,
- * isLoading: Boolean,
- * tagsPath: String // endpoint to request the list
- * destroyPath: String // endpoit to delete the repo
- * list: Array // List of the registry images
- * }
- *
- * Each registry image inside `list` has the following structure:
- * {
- * tag: String,
- * revision: String
- * shortRevision: String
- * size: Number
- * layers: Number
- * createdAt: String
- * destroyPath: String // endpoit to delete each image
- * }
- */
- repos: [],
- },
+ state: createState(),
actions,
getters,
mutations,
diff --git a/app/assets/javascripts/registry/stores/mutations.js b/app/assets/javascripts/registry/stores/mutations.js
index 69c051cd2d6..1ac699c538f 100644
--- a/app/assets/javascripts/registry/stores/mutations.js
+++ b/app/assets/javascripts/registry/stores/mutations.js
@@ -48,6 +48,7 @@ export default {
[types.TOGGLE_REGISTRY_LIST_LOADING](state, list) {
const listToUpdate = state.repos.find(el => el.id === list.id);
+
listToUpdate.isLoading = !listToUpdate.isLoading;
},
};
diff --git a/app/assets/javascripts/registry/stores/state.js b/app/assets/javascripts/registry/stores/state.js
new file mode 100644
index 00000000000..feeac10cbe1
--- /dev/null
+++ b/app/assets/javascripts/registry/stores/state.js
@@ -0,0 +1,26 @@
+export default () => ({
+ isLoading: false,
+ endpoint: '', // initial endpoint to fetch the repos list
+ /**
+ * Each object in `repos` has the following strucure:
+ * {
+ * name: String,
+ * isLoading: Boolean,
+ * tagsPath: String // endpoint to request the list
+ * destroyPath: String // endpoit to delete the repo
+ * list: Array // List of the registry images
+ * }
+ *
+ * Each registry image inside `list` has the following structure:
+ * {
+ * tag: String,
+ * revision: String
+ * shortRevision: String
+ * size: Number
+ * layers: Number
+ * createdAt: String
+ * destroyPath: String // endpoit to delete each image
+ * }
+ */
+ repos: [],
+});
diff --git a/app/assets/javascripts/serverless/components/empty_state.vue b/app/assets/javascripts/serverless/components/empty_state.vue
new file mode 100644
index 00000000000..2683805f2f7
--- /dev/null
+++ b/app/assets/javascripts/serverless/components/empty_state.vue
@@ -0,0 +1,40 @@
+<script>
+export default {
+ props: {
+ clustersPath: {
+ type: String,
+ required: true,
+ },
+ helpPath: {
+ type: String,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="row empty-state js-empty-state">
+ <div class="col-12">
+ <div class="text-content">
+ <h4 class="state-title text-center">
+ {{ s__('Serverless|Getting started with serverless') }}
+ </h4>
+ <p class="state-description">
+ {{
+ s__(`Serverless| In order to start using functions as a service,
+ you must first install Knative on your Kubernetes cluster.`)
+ }}
+
+ <a :href="helpPath"> {{ __('More information') }} </a>
+ </p>
+
+ <div class="text-center">
+ <a :href="clustersPath" class="btn btn-success">
+ {{ s__('Serverless|Install Knative') }}
+ </a>
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/serverless/components/function_row.vue b/app/assets/javascripts/serverless/components/function_row.vue
new file mode 100644
index 00000000000..31f5427c771
--- /dev/null
+++ b/app/assets/javascripts/serverless/components/function_row.vue
@@ -0,0 +1,40 @@
+<script>
+import Timeago from '~/vue_shared/components/time_ago_tooltip.vue';
+
+export default {
+ components: {
+ Timeago,
+ },
+ props: {
+ func: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ name() {
+ return this.func.name;
+ },
+ url() {
+ return this.func.url;
+ },
+ image() {
+ return this.func.image;
+ },
+ timestamp() {
+ return this.func.created_at;
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="gl-responsive-table-row">
+ <div class="table-section section-20">{{ name }}</div>
+ <div class="table-section section-50">
+ <a :href="url">{{ url }}</a>
+ </div>
+ <div class="table-section section-20">{{ image }}</div>
+ <div class="table-section section-10"><timeago :time="timestamp" /></div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/serverless/components/functions.vue b/app/assets/javascripts/serverless/components/functions.vue
new file mode 100644
index 00000000000..7874a7b6b6a
--- /dev/null
+++ b/app/assets/javascripts/serverless/components/functions.vue
@@ -0,0 +1,123 @@
+<script>
+import { GlSkeletonLoading } from '@gitlab/ui';
+import FunctionRow from './function_row.vue';
+import EmptyState from './empty_state.vue';
+
+export default {
+ components: {
+ FunctionRow,
+ EmptyState,
+ GlSkeletonLoading,
+ },
+ props: {
+ functions: {
+ type: Array,
+ required: true,
+ default: () => [],
+ },
+ installed: {
+ type: Boolean,
+ required: true,
+ },
+ clustersPath: {
+ type: String,
+ required: true,
+ },
+ helpPath: {
+ type: String,
+ required: true,
+ },
+ loadingData: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ hasFunctionData: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <section id="serverless-functions">
+ <div v-if="installed">
+ <div v-if="hasFunctionData">
+ <div class="ci-table js-services-list function-element">
+ <div class="gl-responsive-table-row table-row-header" role="row">
+ <div class="table-section section-20" role="rowheader">
+ {{ s__('Serverless|Function') }}
+ </div>
+ <div class="table-section section-50" role="rowheader">
+ {{ s__('Serverless|Domain') }}
+ </div>
+ <div class="table-section section-20" role="rowheader">
+ {{ s__('Serverless|Runtime') }}
+ </div>
+ <div class="table-section section-10" role="rowheader">
+ {{ s__('Serverless|Last Update') }}
+ </div>
+ </div>
+ <template v-if="loadingData">
+ <div v-for="j in 3" :key="j" class="gl-responsive-table-row">
+ <gl-skeleton-loading />
+ </div>
+ </template>
+ <template v-else>
+ <function-row v-for="f in functions" :key="f.name" :func="f" />
+ </template>
+ </div>
+ </div>
+ <div v-else class="empty-state js-empty-state">
+ <div class="text-content">
+ <h4 class="state-title text-center">{{ s__('Serverless|No functions available') }}</h4>
+ <p class="state-description">
+ {{
+ s__(`Serverless|There is currently no function data available from Knative.
+ This could be for a variety of reasons including:`)
+ }}
+ </p>
+ <ul>
+ <li>Your repository does not have a corresponding <code>serverless.yml</code> file.</li>
+ <li>Your <code>gitlab-ci.yml</code> file is not properly configured.</li>
+ <li>
+ The functions listed in the <code>serverless.yml</code> file don't match the namespace
+ of your cluster.
+ </li>
+ <li>The deploy job has not finished.</li>
+ </ul>
+
+ <p>
+ {{
+ s__(`Serverless|If you believe none of these apply, please check
+ back later as the function data may be in the process of becoming
+ available.`)
+ }}
+ </p>
+ <div class="text-center">
+ <a :href="helpPath" class="btn btn-success">
+ {{ s__('Serverless|Learn more about Serverless') }}
+ </a>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <empty-state v-else :clusters-path="clustersPath" :help-path="helpPath" />
+ </section>
+</template>
+
+<style>
+.top-area {
+ border-bottom: 0;
+}
+
+.function-element {
+ border-bottom: 1px solid #e5e5e5;
+ border-bottom-color: rgb(229, 229, 229);
+ border-bottom-style: solid;
+ border-bottom-width: 1px;
+}
+</style>
diff --git a/app/assets/javascripts/serverless/event_hub.js b/app/assets/javascripts/serverless/event_hub.js
new file mode 100644
index 00000000000..0948c2e5352
--- /dev/null
+++ b/app/assets/javascripts/serverless/event_hub.js
@@ -0,0 +1,3 @@
+import Vue from 'vue';
+
+export default new Vue();
diff --git a/app/assets/javascripts/serverless/serverless_bundle.js b/app/assets/javascripts/serverless/serverless_bundle.js
new file mode 100644
index 00000000000..3e3b81ba247
--- /dev/null
+++ b/app/assets/javascripts/serverless/serverless_bundle.js
@@ -0,0 +1,106 @@
+import Visibility from 'visibilityjs';
+import Vue from 'vue';
+import { s__ } from '../locale';
+import Flash from '../flash';
+import Poll from '../lib/utils/poll';
+import ServerlessStore from './stores/serverless_store';
+import GetFunctionsService from './services/get_functions_service';
+import Functions from './components/functions.vue';
+
+export default class Serverless {
+ constructor() {
+ const { statusPath, clustersPath, helpPath, installed } = document.querySelector(
+ '.js-serverless-functions-page',
+ ).dataset;
+
+ this.service = new GetFunctionsService(statusPath);
+ this.knativeInstalled = installed !== undefined;
+ this.store = new ServerlessStore(this.knativeInstalled, clustersPath, helpPath);
+ this.initServerless();
+ this.functionLoadCount = 0;
+
+ if (statusPath && this.knativeInstalled) {
+ this.initPolling();
+ }
+ }
+
+ initServerless() {
+ const { store } = this;
+ const el = document.querySelector('#js-serverless-functions');
+
+ this.functions = new Vue({
+ el,
+ data() {
+ return {
+ state: store.state,
+ };
+ },
+ render(createElement) {
+ return createElement(Functions, {
+ props: {
+ functions: this.state.functions,
+ installed: this.state.installed,
+ clustersPath: this.state.clustersPath,
+ helpPath: this.state.helpPath,
+ loadingData: this.state.loadingData,
+ hasFunctionData: this.state.hasFunctionData,
+ },
+ });
+ },
+ });
+ }
+
+ initPolling() {
+ this.poll = new Poll({
+ resource: this.service,
+ method: 'fetchData',
+ successCallback: data => this.handleSuccess(data),
+ errorCallback: () => this.handleError(),
+ });
+
+ if (!Visibility.hidden()) {
+ this.poll.makeRequest();
+ } else {
+ this.service
+ .fetchData()
+ .then(data => this.handleSuccess(data))
+ .catch(() => this.handleError());
+ }
+
+ Visibility.change(() => {
+ if (!Visibility.hidden() && !this.destroyed) {
+ this.poll.restart();
+ } else {
+ this.poll.stop();
+ }
+ });
+ }
+
+ handleSuccess(data) {
+ if (data.status === 200) {
+ this.store.updateFunctionsFromServer(data.data);
+ this.store.updateLoadingState(false);
+ } else if (data.status === 204) {
+ /* Time out after 3 attempts to retrieve data */
+ this.functionLoadCount += 1;
+ if (this.functionLoadCount === 3) {
+ this.poll.stop();
+ this.store.toggleNoFunctionData();
+ }
+ }
+ }
+
+ static handleError() {
+ Flash(s__('Serverless|An error occurred while retrieving serverless components'));
+ }
+
+ destroy() {
+ this.destroyed = true;
+
+ if (this.poll) {
+ this.poll.stop();
+ }
+
+ this.functions.$destroy();
+ }
+}
diff --git a/app/assets/javascripts/serverless/services/get_functions_service.js b/app/assets/javascripts/serverless/services/get_functions_service.js
new file mode 100644
index 00000000000..303b42dc66c
--- /dev/null
+++ b/app/assets/javascripts/serverless/services/get_functions_service.js
@@ -0,0 +1,11 @@
+import axios from '~/lib/utils/axios_utils';
+
+export default class GetFunctionsService {
+ constructor(endpoint) {
+ this.endpoint = endpoint;
+ }
+
+ fetchData() {
+ return axios.get(this.endpoint);
+ }
+}
diff --git a/app/assets/javascripts/serverless/stores/serverless_store.js b/app/assets/javascripts/serverless/stores/serverless_store.js
new file mode 100644
index 00000000000..774c15b5b12
--- /dev/null
+++ b/app/assets/javascripts/serverless/stores/serverless_store.js
@@ -0,0 +1,24 @@
+export default class ServerlessStore {
+ constructor(knativeInstalled = false, clustersPath, helpPath) {
+ this.state = {
+ functions: [],
+ hasFunctionData: true,
+ loadingData: true,
+ installed: knativeInstalled,
+ clustersPath,
+ helpPath,
+ };
+ }
+
+ updateFunctionsFromServer(functions = []) {
+ this.state.functions = functions;
+ }
+
+ updateLoadingState(loadingData) {
+ this.state.loadingData = loadingData;
+ }
+
+ toggleNoFunctionData() {
+ this.state.hasFunctionData = false;
+ }
+}
diff --git a/app/assets/javascripts/star.js b/app/assets/javascripts/star.js
index 9af5d5b23cb..7404dfbf22a 100644
--- a/app/assets/javascripts/star.js
+++ b/app/assets/javascripts/star.js
@@ -5,11 +5,12 @@ import { spriteIcon } from './lib/utils/common_utils';
import axios from './lib/utils/axios_utils';
export default class Star {
- constructor() {
- $('.project-home-panel .toggle-star').on('click', function toggleStarClickCallback() {
+ constructor(container = '.project-home-panel') {
+ $(`${container} .toggle-star`).on('click', function toggleStarClickCallback() {
const $this = $(this);
const $starSpan = $this.find('span');
- const $startIcon = $this.find('svg');
+ const $starIcon = $this.find('svg');
+ const iconClasses = $starIcon.attr('class').split(' ');
axios
.post($this.data('endpoint'))
@@ -22,12 +23,12 @@ export default class Star {
if (isStarred) {
$starSpan.removeClass('starred').text(s__('StarProject|Star'));
- $startIcon.remove();
- $this.prepend(spriteIcon('star-o', 'icon'));
+ $starIcon.remove();
+ $this.prepend(spriteIcon('star-o', iconClasses));
} else {
$starSpan.addClass('starred').text(__('Unstar'));
- $startIcon.remove();
- $this.prepend(spriteIcon('star', 'icon'));
+ $starIcon.remove();
+ $this.prepend(spriteIcon('star', iconClasses));
}
})
.catch(() => Flash('Star toggle failed. Try again later.'));
diff --git a/app/assets/javascripts/terminal/index.js b/app/assets/javascripts/terminal/index.js
index 49aeb377c74..8faff59fd45 100644
--- a/app/assets/javascripts/terminal/index.js
+++ b/app/assets/javascripts/terminal/index.js
@@ -1,3 +1,3 @@
import Terminal from './terminal';
-export default () => new Terminal({ selector: '#terminal' });
+export default () => new Terminal(document.getElementById('terminal'));
diff --git a/app/assets/javascripts/terminal/terminal.js b/app/assets/javascripts/terminal/terminal.js
index b24aa8a3a34..560f50ebf8f 100644
--- a/app/assets/javascripts/terminal/terminal.js
+++ b/app/assets/javascripts/terminal/terminal.js
@@ -1,9 +1,15 @@
+import _ from 'underscore';
import $ from 'jquery';
import { Terminal } from 'xterm';
import * as fit from 'xterm/lib/addons/fit/fit';
+import { canScrollUp, canScrollDown } from '~/lib/utils/dom_utils';
+
+const SCROLL_MARGIN = 5;
+
+Terminal.applyAddon(fit);
export default class GLTerminal {
- constructor(options = {}) {
+ constructor(element, options = {}) {
this.options = Object.assign(
{},
{
@@ -13,7 +19,8 @@ export default class GLTerminal {
options,
);
- this.container = document.querySelector(options.selector);
+ this.container = element;
+ this.onDispose = [];
this.setSocketUrl();
this.createTerminal();
@@ -34,8 +41,6 @@ export default class GLTerminal {
}
createTerminal() {
- Terminal.applyAddon(fit);
-
this.terminal = new Terminal(this.options);
this.socket = new WebSocket(this.socketUrl, ['terminal.gitlab.com']);
@@ -72,4 +77,48 @@ export default class GLTerminal {
handleSocketFailure() {
this.terminal.write('\r\nConnection failure');
}
+
+ addScrollListener(onScrollLimit) {
+ const viewport = this.container.querySelector('.xterm-viewport');
+ const listener = _.throttle(() => {
+ onScrollLimit({
+ canScrollUp: canScrollUp(viewport, SCROLL_MARGIN),
+ canScrollDown: canScrollDown(viewport, SCROLL_MARGIN),
+ });
+ });
+
+ this.onDispose.push(() => viewport.removeEventListener('scroll', listener));
+ viewport.addEventListener('scroll', listener);
+
+ // don't forget to initialize value before scroll!
+ listener({ target: viewport });
+ }
+
+ disable() {
+ this.terminal.setOption('cursorBlink', false);
+ this.terminal.setOption('theme', { foreground: '#707070' });
+ this.terminal.setOption('disableStdin', true);
+ this.socket.close();
+ }
+
+ dispose() {
+ this.terminal.off('data');
+ this.terminal.dispose();
+ this.socket.close();
+
+ this.onDispose.forEach(fn => fn());
+ this.onDispose.length = 0;
+ }
+
+ scrollToTop() {
+ this.terminal.scrollToTop();
+ }
+
+ scrollToBottom() {
+ this.terminal.scrollToBottom();
+ }
+
+ fit() {
+ this.terminal.fit();
+ }
}
diff --git a/app/assets/javascripts/user_popovers.js b/app/assets/javascripts/user_popovers.js
new file mode 100644
index 00000000000..948f4d5e631
--- /dev/null
+++ b/app/assets/javascripts/user_popovers.js
@@ -0,0 +1,107 @@
+import Vue from 'vue';
+
+import UsersCache from './lib/utils/users_cache';
+import UserPopover from './vue_shared/components/user_popover/user_popover.vue';
+
+let renderedPopover;
+let renderFn;
+
+const handleUserPopoverMouseOut = event => {
+ const { target } = event;
+ target.removeEventListener('mouseleave', handleUserPopoverMouseOut);
+
+ if (renderFn) {
+ clearTimeout(renderFn);
+ }
+ if (renderedPopover) {
+ renderedPopover.$destroy();
+ renderedPopover = null;
+ }
+};
+
+/**
+ * Adds a UserPopover component to the body, hands over as much data as the target element has in data attributes.
+ * loads based on data-user-id more data about a user from the API and sets it on the popover
+ */
+const handleUserPopoverMouseOver = event => {
+ const { target } = event;
+ // Add listener to actually remove it again
+ target.addEventListener('mouseleave', handleUserPopoverMouseOut);
+
+ renderFn = setTimeout(() => {
+ // Helps us to use current markdown setup without maybe breaking or duplicating for now
+ if (target.dataset.user) {
+ target.dataset.userId = target.dataset.user;
+ // Removing titles so its not showing tooltips also
+ target.dataset.originalTitle = '';
+ target.setAttribute('title', '');
+ }
+
+ const { userId, username, name, avatarUrl } = target.dataset;
+ const user = {
+ userId,
+ username,
+ name,
+ avatarUrl,
+ location: null,
+ bio: null,
+ organization: null,
+ status: null,
+ loaded: false,
+ };
+ if (userId || username) {
+ const UserPopoverComponent = Vue.extend(UserPopover);
+ renderedPopover = new UserPopoverComponent({
+ propsData: {
+ target,
+ user,
+ },
+ });
+
+ renderedPopover.$mount();
+
+ UsersCache.retrieveById(userId)
+ .then(userData => {
+ if (!userData) {
+ return;
+ }
+
+ Object.assign(user, {
+ avatarUrl: userData.avatar_url,
+ username: userData.username,
+ name: userData.name,
+ location: userData.location,
+ bio: userData.bio,
+ organization: userData.organization,
+ loaded: true,
+ });
+
+ UsersCache.retrieveStatusById(userId)
+ .then(status => {
+ if (!status) {
+ return;
+ }
+
+ Object.assign(user, {
+ status,
+ });
+ })
+ .catch(() => {
+ throw new Error(`User status for "${userId}" could not be retrieved!`);
+ });
+ })
+ .catch(() => {
+ renderedPopover.$destroy();
+ renderedPopover = null;
+ });
+ }
+ }, 200); // 200ms delay so not every mouseover triggers Popover + API Call
+};
+
+export default elements => {
+ const userLinks = elements || [...document.querySelectorAll('.js-user-link')];
+
+ userLinks.forEach(el => {
+ el.addEventListener('mouseenter', handleUserPopoverMouseOver);
+ });
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_icon.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_icon.vue
index e3adc7f7af5..4b57693e8f1 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_icon.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_icon.vue
@@ -13,5 +13,7 @@ export default {
</script>
<template>
- <div class="circle-icon-container append-right-default"><icon :name="name" /></div>
+ <div class="circle-icon-container append-right-default align-self-start align-self-lg-center">
+ <icon :name="name" />
+ </div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue b/app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue
index bb2e0e12c11..75c66ed850b 100644
--- a/app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue
@@ -1,7 +1,10 @@
<script>
+import { diffModes } from '~/ide/constants';
import { viewerInformationForPath } from '../content_viewer/lib/viewer_utils';
import ImageDiffViewer from './viewers/image_diff_viewer.vue';
import DownloadDiffViewer from './viewers/download_diff_viewer.vue';
+import RenamedFile from './viewers/renamed.vue';
+import ModeChanged from './viewers/mode_changed.vue';
export default {
props: {
@@ -30,9 +33,25 @@ export default {
required: false,
default: '',
},
+ aMode: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ bMode: {
+ type: String,
+ required: false,
+ default: null,
+ },
},
computed: {
viewer() {
+ if (this.diffMode === diffModes.renamed) {
+ return RenamedFile;
+ } else if (this.diffMode === diffModes.mode_changed) {
+ return ModeChanged;
+ }
+
if (!this.newPath) return null;
const previewInfo = viewerInformationForPath(this.newPath);
@@ -67,8 +86,10 @@ export default {
:new-path="fullNewPath"
:old-path="fullOldPath"
:project-path="projectPath"
+ :a-mode="aMode"
+ :b-mode="bMode"
>
- <slot slot="image-overlay" name="image-overlay"> </slot>
+ <slot slot="image-overlay" name="image-overlay"></slot>
</component>
<slot></slot>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/mode_changed.vue b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/mode_changed.vue
new file mode 100644
index 00000000000..3c7a4ea6183
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/mode_changed.vue
@@ -0,0 +1,30 @@
+<script>
+import { sprintf, __ } from '~/locale';
+
+export default {
+ props: {
+ aMode: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ bMode: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ },
+ computed: {
+ outputText() {
+ return sprintf(__('File mode changed from %{a_mode} to %{b_mode}'), {
+ a_mode: this.aMode,
+ b_mode: this.bMode,
+ });
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="nothing-here-block">{{ outputText }}</div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/renamed.vue b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/renamed.vue
new file mode 100644
index 00000000000..5c1ea59b471
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/renamed.vue
@@ -0,0 +1,3 @@
+<template>
+ <div class="nothing-here-block">{{ __('File moved') }}</div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/issue/issue_assignees.vue b/app/assets/javascripts/vue_shared/components/issue/issue_assignees.vue
new file mode 100644
index 00000000000..7e79e63aa1e
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/issue/issue_assignees.vue
@@ -0,0 +1,94 @@
+<script>
+import { GlTooltipDirective } from '@gitlab/ui';
+import { __, sprintf } from '~/locale';
+
+import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
+
+export default {
+ components: {
+ UserAvatarLink,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ assignees: {
+ type: Array,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ maxVisibleAssignees: 2,
+ maxAssigneeAvatars: 3,
+ maxAssignees: 99,
+ };
+ },
+ computed: {
+ countOverLimit() {
+ return this.assignees.length - this.maxVisibleAssignees;
+ },
+ assigneesToShow() {
+ if (this.assignees.length > this.maxAssigneeAvatars) {
+ return this.assignees.slice(0, this.maxVisibleAssignees);
+ }
+ return this.assignees;
+ },
+ assigneesCounterTooltip() {
+ const { countOverLimit, maxAssignees } = this;
+ const count = countOverLimit > maxAssignees ? maxAssignees : countOverLimit;
+
+ return sprintf(__('%{count} more assignees'), { count });
+ },
+ shouldRenderAssigneesCounter() {
+ const assigneesCount = this.assignees.length;
+ if (assigneesCount <= this.maxAssigneeAvatars) {
+ return false;
+ }
+
+ return assigneesCount > this.countOverLimit;
+ },
+ assigneeCounterLabel() {
+ if (this.countOverLimit > this.maxAssignees) {
+ return `${this.maxAssignees}+`;
+ }
+
+ return `+${this.countOverLimit}`;
+ },
+ },
+ methods: {
+ avatarUrlTitle(assignee) {
+ return sprintf(__('Avatar for %{assigneeName}'), {
+ assigneeName: assignee.name,
+ });
+ },
+ },
+};
+</script>
+<template>
+ <div class="issue-assignees">
+ <user-avatar-link
+ v-for="assignee in assigneesToShow"
+ :key="assignee.id"
+ :link-href="assignee.web_url"
+ :img-alt="avatarUrlTitle(assignee)"
+ :img-src="assignee.avatar_url"
+ :img-size="24"
+ class="js-no-trigger"
+ tooltip-placement="bottom"
+ >
+ <span class="js-assignee-tooltip">
+ <span class="bold d-block">{{ __('Assignee') }}</span> {{ assignee.name }}
+ <span class="text-white-50">@{{ assignee.username }}</span>
+ </span>
+ </user-avatar-link>
+ <span
+ v-if="shouldRenderAssigneesCounter"
+ v-gl-tooltip
+ :title="assigneesCounterTooltip"
+ class="avatar-counter"
+ data-placement="bottom"
+ >{{ assigneeCounterLabel }}</span
+ >
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/issue/issue_milestone.vue b/app/assets/javascripts/vue_shared/components/issue/issue_milestone.vue
new file mode 100644
index 00000000000..d5d967e25bf
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/issue/issue_milestone.vue
@@ -0,0 +1,90 @@
+<script>
+import { GlTooltip } from '@gitlab/ui';
+import { __, sprintf } from '~/locale';
+import timeagoMixin from '~/vue_shared/mixins/timeago';
+import { timeFor, parsePikadayDate, dateInWords } from '~/lib/utils/datetime_utility';
+import Icon from '~/vue_shared/components/icon.vue';
+
+export default {
+ components: {
+ Icon,
+ GlTooltip,
+ },
+ mixins: [timeagoMixin],
+ props: {
+ milestone: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ milestoneDue: this.milestone.due_date ? parsePikadayDate(this.milestone.due_date) : null,
+ milestoneStart: this.milestone.start_date
+ ? parsePikadayDate(this.milestone.start_date)
+ : null,
+ };
+ },
+ computed: {
+ isMilestoneStarted() {
+ if (!this.milestoneStart) {
+ return false;
+ }
+ return Date.now() > this.milestoneStart;
+ },
+ isMilestonePastDue() {
+ if (!this.milestoneDue) {
+ return false;
+ }
+ return Date.now() > this.milestoneDue;
+ },
+ milestoneDatesAbsolute() {
+ if (this.milestoneDue) {
+ return `(${dateInWords(this.milestoneDue)})`;
+ } else if (this.milestoneStart) {
+ return `(${dateInWords(this.milestoneStart)})`;
+ }
+ return '';
+ },
+ milestoneDatesHuman() {
+ if (this.milestoneStart || this.milestoneDue) {
+ if (this.milestoneDue) {
+ return timeFor(
+ this.milestoneDue,
+ sprintf(__('Expired %{expiredOn}'), {
+ expiredOn: this.timeFormated(this.milestoneDue),
+ }),
+ );
+ }
+
+ return sprintf(
+ this.isMilestoneStarted ? __('Started %{startsIn}') : __('Starts %{startsIn}'),
+ {
+ startsIn: this.timeFormated(this.milestoneStart),
+ },
+ );
+ }
+ return '';
+ },
+ },
+};
+</script>
+<template>
+ <div ref="milestoneDetails" class="issue-milestone-details">
+ <icon :size="16" class="inline icon" name="clock" />
+ <span class="milestone-title">{{ milestone.title }}</span>
+ <gl-tooltip :target="() => $refs.milestoneDetails" placement="bottom" class="js-item-milestone">
+ <span class="bold">{{ __('Milestone') }}</span> <br />
+ <span>{{ milestone.title }}</span> <br />
+ <span
+ v-if="milestoneStart || milestoneDue"
+ :class="{
+ 'text-danger-muted': isMilestonePastDue,
+ 'text-tertiary': !isMilestonePastDue,
+ }"
+ ><span>{{ milestoneDatesHuman }}</span
+ ><br /><span>{{ milestoneDatesAbsolute }}</span>
+ </span>
+ </gl-tooltip>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue
index 21d6519191f..43def2673eb 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/field.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue
@@ -1,6 +1,6 @@
<script>
import $ from 'jquery';
-import { s__ } from '~/locale';
+import { __ } from '~/locale';
import Flash from '../../../flash';
import GLForm from '../../../gl_form';
import markdownHeader from './header.vue';
@@ -99,11 +99,12 @@ export default {
if (text) {
this.markdownPreviewLoading = true;
+ this.markdownPreview = __('Loading…');
this.$http
.post(this.versionedPreviewPath(), { text })
.then(resp => resp.json())
.then(data => this.renderMarkdown(data))
- .catch(() => new Flash(s__('Error loading markdown preview')));
+ .catch(() => new Flash(__('Error loading markdown preview')));
} else {
this.renderMarkdown();
}
@@ -162,10 +163,12 @@ export default {
/>
</div>
</div>
- <div v-show="previewMarkdown" class="md md-preview-holder md-preview js-vue-md-preview">
- <div ref="markdown-preview" v-html="markdownPreview"></div>
- <span v-if="markdownPreviewLoading"> Loading... </span>
- </div>
+ <div
+ v-show="previewMarkdown"
+ ref="markdown-preview"
+ class="md-preview js-vue-md-preview md md-preview-holder"
+ v-html="markdownPreview"
+ ></div>
<template v-if="previewMarkdown && !markdownPreviewLoading">
<div v-if="referencedCommands" class="referenced-commands" v-html="referencedCommands"></div>
<div v-if="shouldShowReferencedUsers" class="referenced-users">
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue
index e742900dbcb..373794fb1f2 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue
@@ -44,6 +44,7 @@ export default {
class="sidebar-collapsed-icon"
data-placement="left"
data-container="body"
+ data-boundary="viewport"
@click="handleClick"
>
<i aria-hidden="true" data-hidden="true" class="fa fa-tags"> </i>
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
index 01b8b94f9e3..e833a8e0483 100644
--- 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
@@ -67,7 +67,7 @@ export default {
// In both cases we should render the defaultAvatarUrl
sanitizedSource() {
let baseSrc = this.imgSrc === '' || this.imgSrc === null ? defaultAvatarUrl : this.imgSrc;
- if (baseSrc.indexOf('?') === -1) baseSrc += `?width=${this.size}`;
+ if (!baseSrc.startsWith('data:') && !baseSrc.includes('?')) baseSrc += `?width=${this.size}`;
return baseSrc;
},
resultantSrcAttribute() {
@@ -97,6 +97,7 @@ export default {
class="avatar"
/>
<gl-tooltip
+ v-if="tooltipText || $slots.default"
:target="() => $refs.userAvatarImage"
:placement="tooltipPlacement"
boundary="window"
diff --git a/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue b/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue
new file mode 100644
index 00000000000..7fbadcc0111
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue
@@ -0,0 +1,104 @@
+<script>
+import { GlPopover, GlSkeletonLoading } from '@gitlab/ui';
+import { __, sprintf } from '~/locale';
+import UserAvatarImage from '../user_avatar/user_avatar_image.vue';
+import { glEmojiTag } from '../../../emoji';
+
+export default {
+ name: 'UserPopover',
+ components: {
+ GlPopover,
+ GlSkeletonLoading,
+ UserAvatarImage,
+ },
+ props: {
+ target: {
+ type: HTMLAnchorElement,
+ required: true,
+ },
+ user: {
+ type: Object,
+ required: true,
+ default: null,
+ },
+ loaded: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ computed: {
+ jobLine() {
+ if (this.user.bio && this.user.organization) {
+ return sprintf(__('%{bio} at %{organization}'), {
+ bio: this.user.bio,
+ organization: this.user.organization,
+ });
+ } else if (this.user.bio) {
+ return this.user.bio;
+ } else if (this.user.organization) {
+ return this.user.organization;
+ }
+ return null;
+ },
+ statusHtml() {
+ if (this.user.status.emoji && this.user.status.message) {
+ return `${glEmojiTag(this.user.status.emoji)} ${this.user.status.message}`;
+ } else if (this.user.status.message) {
+ return this.user.status.message;
+ }
+ return '';
+ },
+ nameIsLoading() {
+ return !this.user.name;
+ },
+ jobInfoIsLoading() {
+ return !this.user.loaded && this.user.organization === null;
+ },
+ locationIsLoading() {
+ return !this.user.loaded && this.user.location === null;
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-popover :target="target" boundary="viewport" placement="top" show>
+ <div class="user-popover d-flex">
+ <div class="p-1 flex-shrink-1">
+ <user-avatar-image :img-src="user.avatarUrl" :size="60" css-classes="mr-2" />
+ </div>
+ <div class="p-1 w-100">
+ <h5 class="m-0">
+ {{ user.name }}
+ <gl-skeleton-loading
+ v-if="nameIsLoading"
+ :lines="1"
+ class="animation-container-small mb-1"
+ />
+ </h5>
+ <div class="text-secondary mb-2">
+ <span v-if="user.username">@{{ user.username }}</span>
+ <gl-skeleton-loading v-else :lines="1" class="animation-container-small mb-1" />
+ </div>
+ <div class="text-secondary">
+ {{ jobLine }}
+ <gl-skeleton-loading
+ v-if="jobInfoIsLoading"
+ :lines="1"
+ class="animation-container-small mb-1"
+ />
+ </div>
+ <div class="text-secondary">
+ {{ user.location }}
+ <gl-skeleton-loading
+ v-if="locationIsLoading"
+ :lines="1"
+ class="animation-container-small mb-1"
+ />
+ </div>
+ <div v-if="user.status" class="mt-2"><span v-html="statusHtml"></span></div>
+ </div>
+ </div>
+ </gl-popover>
+</template>
diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss
index bd1cca69c03..985fac11c87 100644
--- a/app/assets/stylesheets/application.scss
+++ b/app/assets/stylesheets/application.scss
@@ -35,6 +35,11 @@
@import "pages/**/*";
/*
+ * Component specific styles, will be moved to gitlab-ui
+ */
+@import "components/**/*";
+
+/*
* Code highlight
*/
@import "highlight/dark";
diff --git a/app/assets/stylesheets/bootstrap_migration.scss b/app/assets/stylesheets/bootstrap_migration.scss
index 62024b8c555..f0671e36130 100644
--- a/app/assets/stylesheets/bootstrap_migration.scss
+++ b/app/assets/stylesheets/bootstrap_migration.scss
@@ -18,8 +18,10 @@ $input-border: $border-color;
$padding-base-vertical: $gl-vert-padding;
$padding-base-horizontal: $gl-padding;
-html {
- // Override default font size used in bs4
+body,
+.form-control,
+.search form {
+ // Override default font size used in non-csslab UI
font-size: 14px;
}
diff --git a/app/assets/stylesheets/components/popover.scss b/app/assets/stylesheets/components/popover.scss
new file mode 100644
index 00000000000..2f4d30fe923
--- /dev/null
+++ b/app/assets/stylesheets/components/popover.scss
@@ -0,0 +1,9 @@
+.popover {
+ min-width: 300px;
+
+ .popover-body .user-popover {
+ padding: $gl-padding-8;
+ font-size: $gl-font-size-small;
+ line-height: $gl-line-height;
+ }
+}
diff --git a/app/assets/stylesheets/csslab.scss b/app/assets/stylesheets/csslab.scss
new file mode 100644
index 00000000000..acaa41e2677
--- /dev/null
+++ b/app/assets/stylesheets/csslab.scss
@@ -0,0 +1 @@
+@import "../../../node_modules/@gitlab/csslab/dist/css/csslab-slim";
diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss
index 4041f2b4479..834e7ffce81 100644
--- a/app/assets/stylesheets/framework.scss
+++ b/app/assets/stylesheets/framework.scss
@@ -65,3 +65,4 @@
@import 'framework/feature_highlight';
@import 'framework/terms';
@import 'framework/read_more';
+@import 'framework/flex_grid';
diff --git a/app/assets/stylesheets/framework/avatar.scss b/app/assets/stylesheets/framework/avatar.scss
index fcf282a7d7c..e132aa4c216 100644
--- a/app/assets/stylesheets/framework/avatar.scss
+++ b/app/assets/stylesheets/framework/avatar.scss
@@ -21,6 +21,7 @@
&.s46 { @include avatar-size(46px, 15px); }
&.s48 { @include avatar-size(48px, 10px); }
&.s60 { @include avatar-size(60px, 12px); }
+ &.s64 { @include avatar-size(64px, 14px); }
&.s70 { @include avatar-size(70px, 14px); }
&.s90 { @include avatar-size(90px, 15px); }
&.s100 { @include avatar-size(100px, 15px); }
@@ -80,6 +81,7 @@
&.s40 { font-size: 16px; line-height: 38px; }
&.s48 { font-size: 20px; line-height: 46px; }
&.s60 { font-size: 32px; line-height: 58px; }
+ &.s64 { font-size: 32px; line-height: 64px; }
&.s70 { font-size: 34px; line-height: 70px; }
&.s90 { font-size: 36px; line-height: 88px; }
&.s100 { font-size: 36px; line-height: 98px; }
@@ -106,6 +108,7 @@
width: 100%;
height: 100%;
display: flex;
+ text-decoration: none;
}
.avatar {
@@ -118,6 +121,7 @@
}
&.s40 { min-width: 40px; min-height: 40px; }
+ &.s64 { min-width: 64px; min-height: 64px; }
}
.avatar-counter {
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index 219fd99b097..a4a9276c580 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -142,8 +142,14 @@
&.btn-sm {
padding: 4px 10px;
- font-size: 13px;
- line-height: 18px;
+ font-size: $gl-btn-small-font-size;
+ line-height: $gl-btn-small-line-height;
+ }
+
+ &.btn-xs {
+ padding: 2px $gl-btn-padding;
+ font-size: $gl-btn-xs-font-size;
+ line-height: $gl-btn-xs-line-height;
}
&.btn-success,
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index 626c8f92d1d..e037b02a30c 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -386,3 +386,18 @@ img.emoji {
.flex-no-shrink { flex-shrink: 0; }
.mw-460 { max-width: 460px; }
.ws-initial { white-space: initial; }
+.min-height-0 { min-height: 0; }
+
+.gl-pl-0 { padding-left: 0; }
+.gl-pl-1 { padding-left: #{0.5 * $grid-size}; }
+.gl-pl-2 { padding-left: $grid-size; }
+.gl-pl-3 { padding-left: #{2 * $grid-size}; }
+.gl-pl-4 { padding-left: #{3 * $grid-size}; }
+.gl-pl-5 { padding-left: #{4 * $grid-size}; }
+
+.gl-pr-0 { padding-right: 0; }
+.gl-pr-1 { padding-right: #{0.5 * $grid-size}; }
+.gl-pr-2 { padding-right: $grid-size; }
+.gl-pr-3 { padding-right: #{2 * $grid-size}; }
+.gl-pr-4 { padding-right: #{3 * $grid-size}; }
+.gl-pr-5 { padding-right: #{4 * $grid-size}; }
diff --git a/app/assets/stylesheets/framework/contextual_sidebar.scss b/app/assets/stylesheets/framework/contextual_sidebar.scss
index 6f103e4e89a..8b6a7017c47 100644
--- a/app/assets/stylesheets/framework/contextual_sidebar.scss
+++ b/app/assets/stylesheets/framework/contextual_sidebar.scss
@@ -261,7 +261,7 @@
height: 1px;
margin: 4px -1px;
padding: 0;
- background-color: $dropdown-divider-color;
+ background-color: $dropdown-divider-bg;
}
> .active {
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index ce5d36a340f..b47b1cb76dc 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -176,9 +176,9 @@
display: block;
font-weight: $gl-font-weight-normal;
position: relative;
- padding: 8px 16px;
+ padding: $dropdown-item-padding-y $dropdown-item-padding-x;
color: $gl-text-color;
- line-height: normal;
+ line-height: $gl-btn-line-height;
white-space: normal;
overflow: hidden;
text-align: left;
@@ -290,14 +290,18 @@
}
}
+ .dropdown-item {
+ @include dropdown-link;
+ }
+
.divider {
height: 1px;
margin: #{$grid-size / 2} 0;
padding: 0;
- background-color: $dropdown-divider-color;
+ background-color: $dropdown-divider-bg;
&:hover {
- background-color: $dropdown-divider-color;
+ background-color: $dropdown-divider-bg;
}
}
@@ -306,7 +310,7 @@
height: 1px;
margin-top: 8px;
margin-bottom: 8px;
- background-color: $dropdown-divider-color;
+ background-color: $dropdown-divider-bg;
}
.dropdown-menu-empty-item a {
@@ -319,8 +323,8 @@
.dropdown-header {
color: $gl-text-color-secondary;
font-size: 13px;
- line-height: 22px;
- padding: 8px 16px;
+ line-height: $gl-line-height;
+ padding: $dropdown-item-padding-y $dropdown-item-padding-x;
}
&.capitalize-header .dropdown-header {
@@ -329,13 +333,8 @@
.dropdown-bold-header {
font-weight: $gl-font-weight-bold;
- line-height: 22px;
- padding: 0 16px;
- }
-
- .separator + .dropdown-header,
- .separator + .dropdown-bold-header {
- padding-top: 10px;
+ line-height: $gl-line-height;
+ padding: $dropdown-item-padding-y $dropdown-item-padding-x;
}
.unclickable {
@@ -542,7 +541,7 @@
text-align: center;
text-overflow: ellipsis;
white-space: nowrap;
- border-bottom: 1px solid $dropdown-divider-color;
+ border-bottom: 1px solid $dropdown-divider-bg;
overflow: hidden;
}
@@ -621,7 +620,7 @@
padding: 0 7px;
color: $gl-gray-700;
line-height: 30px;
- border: 1px solid $dropdown-divider-color;
+ border: 1px solid $dropdown-divider-bg;
border-radius: 2px;
outline: 0;
@@ -656,7 +655,7 @@
padding-top: 10px;
margin-top: 10px;
font-size: 13px;
- border-top: 1px solid $dropdown-divider-color;
+ border-top: 1px solid $dropdown-divider-bg;
}
.dropdown-footer-content {
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index 037a5adfb7e..3ac7b6b704b 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -24,7 +24,7 @@
}
}
- table {
+ &:not(.use-csslab) table {
@extend .table;
}
diff --git a/app/assets/stylesheets/framework/flex_grid.scss b/app/assets/stylesheets/framework/flex_grid.scss
new file mode 100644
index 00000000000..10537fd5549
--- /dev/null
+++ b/app/assets/stylesheets/framework/flex_grid.scss
@@ -0,0 +1,52 @@
+.flex-grid {
+ .grid-row {
+ border-bottom: 1px solid $border-color;
+ padding: 0;
+
+ &:last-child {
+ border-bottom: 0;
+ }
+
+ @include media-breakpoint-down(md) {
+ border-bottom: 0;
+ border-right: 1px solid $border-color;
+
+ &:last-child {
+ border-right: 0;
+ }
+ }
+
+ @include media-breakpoint-down(xs) {
+ border-right: 0;
+ border-bottom: 1px solid $border-color;
+
+ &:last-child {
+ border-bottom: 0;
+ }
+ }
+ }
+
+ .grid-cell {
+ padding: 10px $gl-padding;
+ border-right: 1px solid $border-color;
+
+ &:last-child {
+ border-right: 0;
+ }
+
+ @include media-breakpoint-up(md) {
+ flex: 1;
+ }
+
+ @include media-breakpoint-down(md) {
+ border-right: 0;
+ flex: none;
+ }
+ }
+}
+
+.card {
+ .card-body.flex-grid {
+ padding: 0;
+ }
+}
diff --git a/app/assets/stylesheets/framework/gitlab_theme.scss b/app/assets/stylesheets/framework/gitlab_theme.scss
index b8bb9e1e07b..0ef50e139f2 100644
--- a/app/assets/stylesheets/framework/gitlab_theme.scss
+++ b/app/assets/stylesheets/framework/gitlab_theme.scss
@@ -22,6 +22,10 @@
.container-fluid {
.navbar-toggler {
border-left: 1px solid lighten($border-and-box-shadow, 10%);
+
+ svg {
+ fill: $search-and-nav-links;
+ }
}
}
@@ -309,12 +313,14 @@ body {
.navbar-nav {
> li {
> a:hover,
- > a:focus {
+ > a:focus,
+ > button:hover {
color: $theme-gray-900;
}
&.active > a,
- &.active > a:hover {
+ &.active > a:hover,
+ &.active > button {
color: $white-light;
}
}
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index 39410ac56af..7d283dcfb71 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -33,6 +33,7 @@
.close-icon {
display: block;
+ margin: auto;
}
}
@@ -90,12 +91,6 @@
padding: 2px 8px;
margin: 5px 2px 5px -8px;
border-radius: $border-radius-default;
-
- .tanuki-logo {
- @include media-breakpoint-up(sm) {
- margin-right: 8px;
- }
- }
}
.project-item-select {
@@ -127,12 +122,6 @@
}
}
- li.dropdown-bold-header {
- color: $gl-text-color-secondary;
- font-size: 12px;
- padding: 0 16px;
- }
-
.navbar-collapse {
flex: 0 0 auto;
border-top: 0;
@@ -180,12 +169,6 @@
color: currentColor;
background-color: transparent;
}
-
- .more-icon,
- .close-icon {
- fill: $white-light;
- margin: auto;
- }
}
.navbar-nav {
@@ -383,6 +366,16 @@
top: 1px;
}
}
+
+ .dropdown-menu li a .identicon {
+ width: 17px;
+ height: 17px;
+ font-size: $gl-font-size-xs;
+ vertical-align: middle;
+ text-indent: 0;
+ line-height: $gl-font-size-xs + 2px;
+ display: inline-block;
+ }
}
.breadcrumbs-list {
@@ -531,7 +524,7 @@
left: auto;
li.current-user {
- padding: 5px 18px;
+ padding: $dropdown-item-padding-y $dropdown-item-padding-x;
.user-name {
display: block;
diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss
index 0f6fb16774c..2b110e23fb8 100644
--- a/app/assets/stylesheets/framework/markdown_area.scss
+++ b/app/assets/stylesheets/framework/markdown_area.scss
@@ -131,7 +131,7 @@
width: 100%;
}
-.md {
+.md:not(.use-csslab) {
&.md-preview-holder {
// Reset ul style types since we're nested inside a ul already
@include bulleted-list;
diff --git a/app/assets/stylesheets/framework/mobile.scss b/app/assets/stylesheets/framework/mobile.scss
index 6d20c46b99d..3bb046d0e51 100644
--- a/app/assets/stylesheets/framework/mobile.scss
+++ b/app/assets/stylesheets/framework/mobile.scss
@@ -39,15 +39,6 @@
.git-clone-holder {
display: none;
}
-
- // Display Star and Fork buttons without counters on mobile.
- .project-repo-buttons {
- display: block;
-
- .count-buttons .count-badge {
- margin-top: $gl-padding-8;
- }
- }
}
.group-buttons {
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index b3b99df5790..0c81dc2e156 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -368,11 +368,11 @@ code {
* Apply Markdown typography
*
*/
-.wiki {
+.wiki:not(.use-csslab) {
@include md-typography;
}
-.md {
+.md:not(.use-csslab) {
@include md-typography;
}
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index bf2868710eb..a92481b3ebb 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -172,6 +172,7 @@ $theme-light-red-700: #a62e21;
$black: #000;
$black-transparent: rgba(0, 0, 0, 0.3);
+$shadow-color: rgba($black, 0.1);
$almost-black: #242424;
$border-white-light: darken($white-light, $darken-border-factor);
@@ -197,6 +198,8 @@ $well-light-text-color: #5b6169;
$gl-font-size: 14px;
$gl-font-size-xs: 11px;
$gl-font-size-small: 12px;
+$gl-font-size-medium: 1.43rem;
+$gl-font-size-large: 16px;
$gl-font-weight-normal: 400;
$gl-font-weight-bold: 600;
$gl-text-color: #2e2e2e;
@@ -270,9 +273,11 @@ $performance-bar-height: 35px;
$flash-height: 52px;
$context-header-height: 60px;
$breadcrumb-min-height: 48px;
-$project-title-row-height: 24px;
+$project-title-row-height: 64px;
+$project-avatar-mobile-size: 24px;
$gl-line-height: 16px;
$gl-line-height-24: 24px;
+$gl-line-height-14: 14px;
/*
* Common component specific colors
@@ -332,7 +337,6 @@ $dropdown-max-height: 312px;
$dropdown-vertical-offset: 4px;
$dropdown-empty-row-bg: rgba(#000, 0.04);
$dropdown-shadow-color: rgba(#000, 0.1);
-$dropdown-divider-color: rgba(#000, 0.1);
$dropdown-title-btn-color: #bfbfbf;
$dropdown-input-fa-color: #c7c7c7;
$dropdown-input-focus-shadow: rgba($blue-300, 0.4);
@@ -366,6 +370,10 @@ $gl-btn-padding: 10px;
$gl-btn-line-height: 16px;
$gl-btn-vert-padding: 8px;
$gl-btn-horz-padding: 12px;
+$gl-btn-small-font-size: 13px;
+$gl-btn-small-line-height: 18px;
+$gl-btn-xs-font-size: 13px;
+$gl-btn-xs-line-height: 13px;
/*
* Badges
@@ -396,7 +404,7 @@ $award-emoji-positive-add-lines: #bb9c13;
* Search Box
*/
$search-input-border-color: rgba($blue-400, 0.8);
-$search-input-width: 240px;
+$search-input-width: 200px;
$search-input-active-width: 320px;
$location-icon-color: #e7e9ed;
diff --git a/app/assets/stylesheets/framework/variables_overrides.scss b/app/assets/stylesheets/framework/variables_overrides.scss
index 711de02cd39..5ca76bb6c5a 100644
--- a/app/assets/stylesheets/framework/variables_overrides.scss
+++ b/app/assets/stylesheets/framework/variables_overrides.scss
@@ -20,3 +20,11 @@ $warning: $orange-500;
$danger: $red-500;
$zindex-modal-backdrop: 1040;
$nav-divider-margin-y: ($grid-size / 2);
+$dropdown-divider-bg: $theme-gray-200;
+$dropdown-item-padding-y: 8px;
+$dropdown-item-padding-x: 12px;
+$popover-max-width: 300px;
+$popover-border-width: 1px;
+$popover-border-color: $border-color;
+$popover-box-shadow: 0 $border-radius-small $border-radius-default 0 $shadow-color;
+$popover-arrow-outer-color: $shadow-color;
diff --git a/app/assets/stylesheets/page_bundles/_ide_mixins.scss b/app/assets/stylesheets/page_bundles/_ide_mixins.scss
new file mode 100644
index 00000000000..896a3466cb4
--- /dev/null
+++ b/app/assets/stylesheets/page_bundles/_ide_mixins.scss
@@ -0,0 +1,18 @@
+@mixin ide-trace-view {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ margin-top: -$grid-size;
+ margin-bottom: -$grid-size;
+
+ &.build-page .top-bar {
+ top: 0;
+ height: auto;
+ font-size: 12px;
+ border-top-right-radius: $border-radius-default;
+ }
+
+ .top-bar {
+ margin-left: -$gl-padding;
+ }
+}
diff --git a/app/assets/stylesheets/page_bundles/ide.scss b/app/assets/stylesheets/page_bundles/ide.scss
index 07d82e984ba..98d0a2d43ea 100644
--- a/app/assets/stylesheets/page_bundles/ide.scss
+++ b/app/assets/stylesheets/page_bundles/ide.scss
@@ -1,5 +1,6 @@
@import 'framework/variables';
@import 'framework/mixins';
+@import './ide_mixins';
$search-list-icon-width: 18px;
$ide-activity-bar-width: 60px;
@@ -1111,11 +1112,7 @@ $ide-commit-header-height: 48px;
}
.ide-pipeline {
- display: flex;
- flex-direction: column;
- height: 100%;
- margin-top: -$grid-size;
- margin-bottom: -$grid-size;
+ @include ide-trace-view();
.empty-state {
margin-top: auto;
@@ -1133,17 +1130,9 @@ $ide-commit-header-height: 48px;
}
}
- .build-trace,
- .top-bar {
+ .build-trace {
margin-left: -$gl-padding;
}
-
- &.build-page .top-bar {
- top: 0;
- height: auto;
- font-size: 12px;
- border-top-right-radius: $border-radius-default;
- }
}
.ide-pipeline-list {
diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss
index c6074eb9df4..37984a8666f 100644
--- a/app/assets/stylesheets/pages/boards.scss
+++ b/app/assets/stylesheets/pages/boards.scss
@@ -41,7 +41,7 @@
.issue-board-dropdown-content {
margin: 0 8px 10px;
padding-bottom: 10px;
- border-bottom: 1px solid $dropdown-divider-color;
+ border-bottom: 1px solid $dropdown-divider-bg;
> p {
margin: 0;
diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss
index 81cb519883b..57918eafd6f 100644
--- a/app/assets/stylesheets/pages/builds.scss
+++ b/app/assets/stylesheets/pages/builds.scss
@@ -228,9 +228,16 @@
padding: 16px 0;
}
+ .trigger-variables-btn-container {
+ @extend .d-flex;
+ justify-content: space-between;
+ align-items: center;
+ }
+
.trigger-build-variables {
margin: 0;
overflow-x: auto;
+ width: 100%;
-ms-overflow-style: scrollbar;
-webkit-overflow-scrolling: touch;
}
@@ -243,7 +250,15 @@
.trigger-build-value {
padding: 2px 4px;
color: $black;
- background-color: $white-light;
+ }
+
+ .trigger-variables-table-cell {
+ font-size: $gl-font-size-small;
+ line-height: $gl-line-height;
+ border: 1px solid $theme-gray-200;
+ padding: $gl-padding-4 6px;
+ width: 50%;
+ vertical-align: top;
}
.badge.badge-pill {
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index 5405f20a760..18c62cb4f1e 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -914,6 +914,7 @@
padding: 0;
width: (2px * $image-comment-cursor-left-offset);
height: (2px * $image-comment-cursor-top-offset);
+ color: $blue-400;
// center the indicator to match the top left click region
margin-top: (-1px * $image-comment-cursor-top-offset) + 2;
margin-left: (-1px * $image-comment-cursor-left-offset) + 1;
diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss
index 8ea34f5d19d..bb6b6f84849 100644
--- a/app/assets/stylesheets/pages/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -259,6 +259,16 @@ ul.related-merge-requests > li {
display: block;
}
+.issue-sort-dropdown {
+ .btn-group {
+ width: 100%;
+ }
+
+ .reverse-sort-btn {
+ color: $gl-text-color-secondary;
+ }
+}
+
@include media-breakpoint-up(sm) {
.emoji-block .row {
display: flex;
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index 97b3f696139..5b30295adf9 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -178,7 +178,7 @@
table {
.discussion-form-container {
- padding: $gl-padding-top $gl-padding $gl-padding;
+ padding: $gl-padding;
}
}
@@ -237,11 +237,12 @@ table {
}
.discussion-body,
-.diff-file {
+.diff-file,
+.commit-diff {
.discussion-reply-holder {
background-color: $white-light;
- padding: 10px 16px;
border-radius: 0 0 3px 3px;
+ padding: $gl-padding;
&.is-replying {
padding-bottom: $gl-padding;
@@ -254,7 +255,6 @@ table {
display: flex;
}
-
.discussion-actions {
display: table;
@@ -275,8 +275,10 @@ table {
}
}
- .btn {
- width: 100%;
+ @include media-breakpoint-down(xs) {
+ .btn {
+ width: 100%;
+ }
}
.btn-text-field {
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 39d01c49fd7..2adfa0d312e 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -14,7 +14,7 @@ $note-form-margin-left: 72px;
}
@mixin outline-comment() {
- margin: $gl-padding;
+ margin: $gl-padding $gl-padding 0;
border: 1px solid $border-color;
border-radius: $border-radius-default;
}
@@ -27,8 +27,10 @@ $note-form-margin-left: 72px;
}
}
-.main-notes-list {
- @include vertical-line(36px);
+.issuable-discussion {
+ .main-notes-list {
+ @include vertical-line(36px);
+ }
}
.notes {
@@ -76,10 +78,10 @@ $note-form-margin-left: 72px;
.card {
border: 0;
}
+ }
- li.note {
- border-bottom: 1px solid $border-color;
- }
+ li.note {
+ border-bottom: 1px solid $border-color;
}
.replies-toggle {
@@ -161,20 +163,6 @@ $note-form-margin-left: 72px;
position: relative;
border-bottom: 0;
- &:target,
- &.target {
- border-bottom: 1px solid $white-normal;
-
- &:not(:first-child) {
- border-top: 1px solid $white-normal;
- margin-top: -1px;
- }
-
- .timeline-entry-inner {
- border-bottom: 0;
- }
- }
-
&.being-posted {
pointer-events: none;
opacity: 0.5;
@@ -462,7 +450,7 @@ $note-form-margin-left: 72px;
font-family: $regular-font;
td {
- border: 1px solid $white-normal;
+ border: 1px solid $border-color;
border-left: 0;
&.notes_content {
@@ -504,8 +492,6 @@ $note-form-margin-left: 72px;
}
.note-wrapper {
- @include outline-comment();
-
&.system-note {
border: 0;
margin-left: 20px;
@@ -514,23 +500,14 @@ $note-form-margin-left: 72px;
.discussion-reply-holder {
border-radius: 0 0 $border-radius-default $border-radius-default;
- border-top: 1px solid $border-color;
position: relative;
}
}
.commit-diff {
- .notes {
- @include vertical-line(52px);
- }
-
.notes_content {
background-color: $white-light;
}
-
- .discussion-reply-holder {
- border-top: 1px solid $border-color;
- }
}
.discussion-header,
@@ -943,12 +920,6 @@ $note-form-margin-left: 72px;
border-bottom: 1px solid $border-color;
}
- .note-wrapper.outlined {
- margin: 0;
- border: 0;
- border-radius: 0;
- }
-
.discussion-form-container {
padding: $gl-padding;
}
diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss
index 132f3fea92b..a4831b64344 100644
--- a/app/assets/stylesheets/pages/profile.scss
+++ b/app/assets/stylesheets/pages/profile.scss
@@ -98,7 +98,6 @@
// Limits the width of the user bio for readability.
max-width: 600px;
margin: 10px auto;
- padding: 0 16px;
}
.user-avatar-button {
@@ -222,7 +221,11 @@
}
.profile-header {
- margin: 0 auto;
+ margin: 0 $gl-padding;
+
+ &.with-no-profile-tabs {
+ margin-bottom: $gl-padding-24;
+ }
.avatar-holder {
width: 90px;
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 6cc21072acd..0ce0db038a7 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -144,7 +144,6 @@
.group-home-panel {
padding-top: 24px;
padding-bottom: 24px;
- border-bottom: 1px solid $border-color;
.group-avatar {
float: none;
@@ -155,7 +154,6 @@
}
}
- .project-title,
.group-title {
margin-top: 10px;
margin-bottom: 10px;
@@ -195,25 +193,69 @@
}
.project-home-panel {
- padding-top: $gl-padding-8;
- padding-bottom: $gl-padding-24;
-
- .project-title-row {
- margin-right: $gl-padding-8;
- }
+ padding-top: $gl-padding;
+ padding-bottom: $gl-padding;
.project-avatar {
width: $project-title-row-height;
height: $project-title-row-height;
flex-shrink: 0;
flex-basis: $project-title-row-height;
- margin: 0 $gl-padding-8 0 0;
+ margin: 0 $gl-padding 0 0;
}
.project-title {
+ margin-top: 8px;
+ margin-bottom: 5px;
font-size: 20px;
- line-height: $project-title-row-height;
+ line-height: $gl-line-height-24;
font-weight: bold;
+
+ .icon {
+ font-size: $gl-font-size-large;
+ }
+
+ .project-visibility {
+ color: $gl-text-color-secondary;
+ }
+
+ .project-tag-list {
+ font-size: $gl-font-size;
+ font-weight: $gl-font-weight-normal;
+
+ .icon {
+ position: relative;
+ top: 3px;
+ margin-right: $gl-padding-4;
+ }
+ }
+ }
+
+ .project-title-row {
+ @include media-breakpoint-down(sm) {
+ .project-avatar {
+ width: $project-avatar-mobile-size;
+ height: $project-avatar-mobile-size;
+ flex-basis: $project-avatar-mobile-size;
+
+ .avatar {
+ font-size: 20px;
+ line-height: 46px;
+ }
+ }
+
+ .project-title {
+ margin-top: 4px;
+ margin-bottom: 2px;
+ font-size: $gl-font-size;
+ line-height: $gl-font-size-large;
+ }
+
+ .project-tag-list,
+ .project-metadata {
+ font-size: $gl-font-size-small;
+ }
+ }
}
.project-metadata {
@@ -222,16 +264,6 @@
line-height: $gl-btn-line-height;
color: $gl-text-color-secondary;
- .icon {
- margin-right: $gl-padding-4;
- font-size: 16px;
- }
-
- .project-visibility,
- .project-license,
- .project-tag-list {
- margin-right: $gl-padding-8;
- }
.project-license {
.btn {
@@ -240,12 +272,22 @@
}
}
- .project-tag-list,
- .project-license {
- .icon {
- position: relative;
- top: 2px;
- }
+ .access-request-link,
+ .project-tag-list {
+ padding-left: $gl-padding-8;
+ border-left: 1px solid $gl-text-color-secondary;
+ }
+ }
+
+ .project-description {
+ @include media-breakpoint-up(md) {
+ font-size: $gl-font-size-large;
+ }
+ }
+
+ .notifications-btn {
+ .fa-bell {
+ margin-right: 0;
}
}
}
@@ -298,14 +340,6 @@
vertical-align: top;
margin-top: $gl-padding;
- .count-badge {
- height: $input-height;
-
- .icon {
- top: -1px;
- }
- }
-
.count-badge-count,
.count-badge-button {
border: 1px solid $border-color;
@@ -319,29 +353,25 @@
.count-badge-count {
padding: 0 12px;
- border-right: 0;
- border-radius: $border-radius-base 0 0 $border-radius-base;
background: $gray-light;
+ border-radius: 0 $border-radius-base $border-radius-base 0;
}
.count-badge-button {
- border-radius: 0 $border-radius-base $border-radius-base 0;
+ border-right: 0;
+ border-radius: $border-radius-base 0 0 $border-radius-base;
}
}
.project-clone-holder {
display: inline-block;
- margin: $gl-padding $gl-padding-8 0 0;
+ margin: $gl-padding 0 0;
input {
height: $input-height;
}
}
- .clone-dropdown-btn {
- background-color: $white-light;
- }
-
.clone-options-dropdown {
min-width: 240px;
@@ -355,6 +385,31 @@
}
}
+.project-repo-buttons {
+ .icon {
+ top: 0;
+ }
+
+ .count-badge,
+ .btn-xs {
+ height: 24px;
+ }
+
+ .dropdown-toggle,
+ .clone-dropdown-btn {
+ .fa {
+ color: unset;
+ }
+ }
+
+ .btn {
+ .notifications-icon {
+ top: 1px;
+ margin-right: 0;
+ }
+ }
+}
+
.split-one {
display: inline-table;
margin-right: 12px;
@@ -715,10 +770,10 @@
border-bottom: 1px solid $border-color;
}
-.project-stats {
+.project-stats,
+.project-buttons {
font-size: 0;
text-align: center;
- border-bottom: 1px solid $border-color;
.scrolling-tabs-container {
.scrolling-tabs {
@@ -786,23 +841,43 @@
font-size: $gl-font-size;
line-height: $gl-btn-line-height;
color: $gl-text-color-secondary;
- white-space: nowrap;
+ white-space: pre-wrap;
}
.stat-link {
border-bottom: 0;
+ color: $black;
&:hover,
&:focus {
- color: $gl-text-color;
text-decoration: underline;
border-bottom: 0;
}
+
+ .project-stat-value {
+ color: $gl-text-color;
+ }
+
+ .icon {
+ color: $gl-text-color-secondary;
+ }
+
+ .add-license-link {
+ &,
+ .icon {
+ color: $blue-600;
+ }
+ }
}
.btn {
- padding: $gl-btn-vert-padding $gl-btn-horz-padding;
+ margin-top: $gl-padding;
+ padding: $gl-btn-vert-padding $gl-btn-padding;
line-height: $gl-btn-line-height;
+
+ .icon {
+ top: 0;
+ }
}
.btn-missing {
@@ -811,6 +886,13 @@
}
}
+.project-buttons {
+ .stat-text {
+ @extend .btn;
+ @extend .btn-default;
+ }
+}
+
.repository-languages-bar {
height: 8px;
margin-bottom: $gl-padding-8;
@@ -887,34 +969,73 @@ pre.light-well {
@include basic-list-stats;
display: flex;
align-items: center;
- }
+ color: $gl-text-color-secondary;
+ padding: $gl-padding 0;
- h3 {
- font-size: $gl-font-size;
+ @include media-breakpoint-up(lg) {
+ padding: $gl-padding-24 0;
+ }
+
+ &.no-description {
+ @include media-breakpoint-up(sm) {
+ .avatar-container {
+ align-self: center;
+ }
+
+ .metadata-info {
+ margin-bottom: 0;
+ }
+ }
+ }
}
- .avatar-container,
- .controls {
- flex: 0 0 auto;
+ h2 {
+ font-size: $gl-font-size-medium;
+ font-weight: $gl-font-weight-bold;
+ margin-bottom: 0;
+
+ @include media-breakpoint-up(sm) {
+ .namespace-name {
+ font-weight: $gl-font-weight-normal;
+ }
+ }
}
.avatar-container {
+ flex: 0 0 auto;
align-self: flex-start;
}
.project-details {
min-width: 0;
+ line-height: $gl-line-height;
+
+ .flex-wrapper {
+ min-width: 0;
+ margin-top: -$gl-padding-8; // negative margin required for flex-wrap
+ }
p,
.commit-row-message {
@include str-truncated(100%);
margin-bottom: 0;
}
- }
- .controls {
- margin-left: auto;
- text-align: right;
+ .user-access-role {
+ margin: 0;
+ }
+
+ @include media-breakpoint-up(md) {
+ .description {
+ color: $gl-text-color;
+ }
+ }
+
+ @include media-breakpoint-down(md) {
+ .user-access-role {
+ line-height: $gl-line-height-14;
+ }
+ }
}
.ci-status-link {
@@ -926,6 +1047,149 @@ pre.light-well {
text-decoration: none;
}
}
+
+ .controls {
+ margin-top: $gl-padding;
+
+ @include media-breakpoint-down(md) {
+ margin-top: 0;
+ }
+
+ @include media-breakpoint-down(xs) {
+ margin-top: $gl-padding-8;
+ }
+
+ .icon-wrapper {
+ color: inherit;
+ margin-right: $gl-padding;
+
+ @include media-breakpoint-down(md) {
+ margin-right: 0;
+ margin-left: $gl-padding-8;
+ }
+
+ @include media-breakpoint-down(xs) {
+ &:first-child {
+ margin-left: 0;
+ }
+ }
+ }
+
+ .ci-status-link {
+ display: inline-flex;
+ }
+ }
+
+ .star-button {
+ .icon {
+ top: 0;
+ }
+ }
+
+ .icon-container {
+ @include media-breakpoint-down(xs) {
+ margin-right: $gl-padding-8;
+ }
+ }
+
+ &.compact {
+ .project-row {
+ padding: $gl-padding 0;
+ }
+
+ h2 {
+ font-size: $gl-font-size;
+ }
+
+ .avatar-container {
+ @include avatar-size(40px, 10px);
+ min-height: 40px;
+ min-width: 40px;
+
+ .identicon.s64 {
+ font-size: 16px;
+ }
+ }
+
+ .controls {
+ @include media-breakpoint-up(sm) {
+ margin-top: 0;
+ }
+ }
+
+ .updated-note {
+ @include media-breakpoint-up(sm) {
+ margin-top: $gl-padding-8;
+ }
+ }
+
+ .icon-wrapper {
+ margin-left: $gl-padding-8;
+ margin-right: 0;
+
+ @include media-breakpoint-down(xs) {
+ &:first-child {
+ margin-left: 0;
+ }
+ }
+ }
+
+ .user-access-role {
+ line-height: $gl-line-height-14;
+ }
+ }
+
+ @include media-breakpoint-down(md) {
+ h2 {
+ font-size: $gl-font-size;
+ }
+
+ .avatar-container {
+ @include avatar-size(40px, 10px);
+ min-height: 40px;
+ min-width: 40px;
+
+ .identicon.s64 {
+ font-size: 16px;
+ }
+ }
+ }
+
+ @include media-breakpoint-down(md) {
+ .updated-note {
+ margin-top: $gl-padding-8;
+ text-align: right;
+ }
+ }
+
+ .forks,
+ .pipeline-status,
+ .updated-note {
+ display: flex;
+ }
+
+ @include media-breakpoint-down(md) {
+ &:not(.explore) {
+ .forks {
+ display: none;
+
+ }
+ }
+
+ &.explore {
+ .pipeline-status,
+ .updated-note {
+ display: none !important;
+ }
+ }
+ }
+
+ @include media-breakpoint-down(xs) {
+ .updated-note {
+ margin-top: 0;
+ text-align: left;
+ }
+ }
}
.card .projects-list li {
@@ -934,8 +1198,6 @@ pre.light-well {
}
.git-clone-holder {
- width: 320px;
-
.btn-clipboard {
border: 1px solid $border-color;
}
@@ -958,6 +1220,15 @@ pre.light-well {
}
}
+.git-clone-holder,
+.mobile-git-clone {
+ .btn {
+ .icon {
+ fill: $white;
+ }
+ }
+}
+
.cannot-be-merged,
.cannot-be-merged:hover {
color: $red-500;
diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss
index 04151b1cd59..149c3254d84 100644
--- a/app/assets/stylesheets/pages/search.scss
+++ b/app/assets/stylesheets/pages/search.scss
@@ -101,8 +101,6 @@ input[type='checkbox']:hover {
.dropdown-header {
// Necessary because glDropdown doesn't support a second style of headers
font-weight: $gl-font-weight-bold;
- // .dropdown-menu li has 1px side padding
- padding: $gl-padding-8 17px;
color: $gl-text-color;
font-size: $gl-font-size;
line-height: 16px;
diff --git a/app/assets/stylesheets/pages/wiki.scss b/app/assets/stylesheets/pages/wiki.scss
index 800f5c68e39..82e887aa62a 100644
--- a/app/assets/stylesheets/pages/wiki.scss
+++ b/app/assets/stylesheets/pages/wiki.scss
@@ -180,7 +180,7 @@ ul.wiki-pages-list.content-list {
}
}
-.wiki {
+.wiki:not(.use-csslab) {
table {
@include markdown-table;
}
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 65c1576d9d2..7c8c1392c1c 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -8,7 +8,6 @@ class ApplicationController < ActionController::Base
include GitlabRoutingHelper
include PageLayoutHelper
include SafeParamsHelper
- include SentryHelper
include WorkhorseHelper
include EnforcesTwoFactorAuthentication
include WithPerformanceBar
@@ -129,6 +128,7 @@ class ApplicationController < ActionController::Base
payload[:ua] = request.env["HTTP_USER_AGENT"]
payload[:remote_ip] = request.remote_ip
+ payload[Gitlab::CorrelationId::LOG_KEY] = Gitlab::CorrelationId.current_id
logged_user = auth_user
@@ -155,7 +155,7 @@ class ApplicationController < ActionController::Base
end
def log_exception(exception)
- Raven.capture_exception(exception) if sentry_enabled?
+ Gitlab::Sentry.track_acceptable_exception(exception)
backtrace_cleaner = Gitlab.rails5? ? request.env["action_dispatch.backtrace_cleaner"] : env
application_trace = ActionDispatch::ExceptionWrapper.new(backtrace_cleaner, exception).application_trace
@@ -487,4 +487,8 @@ class ApplicationController < ActionController::Base
def impersonator
@impersonator ||= User.find(session[:impersonator_id]) if session[:impersonator_id]
end
+
+ def sentry_context
+ Gitlab::Sentry.context(current_user)
+ end
end
diff --git a/app/controllers/clusters/applications_controller.rb b/app/controllers/clusters/applications_controller.rb
index 250f42f3096..c4e7fc950f9 100644
--- a/app/controllers/clusters/applications_controller.rb
+++ b/app/controllers/clusters/applications_controller.rb
@@ -23,6 +23,6 @@ class Clusters::ApplicationsController < Clusters::BaseController
end
def create_cluster_application_params
- params.permit(:application, :hostname)
+ params.permit(:application, :hostname, :email)
end
end
diff --git a/app/controllers/clusters/clusters_controller.rb b/app/controllers/clusters/clusters_controller.rb
index 2e9c77ae55c..9aa8b758539 100644
--- a/app/controllers/clusters/clusters_controller.rb
+++ b/app/controllers/clusters/clusters_controller.rb
@@ -181,15 +181,15 @@ class Clusters::ClustersController < Clusters::BaseController
end
def gcp_cluster
- @gcp_cluster = ::Clusters::Cluster.new.tap do |cluster|
- cluster.build_provider_gcp
- end.present(current_user: current_user)
+ cluster = Clusters::BuildService.new(clusterable.subject).execute
+ cluster.build_provider_gcp
+ @gcp_cluster = cluster.present(current_user: current_user)
end
def user_cluster
- @user_cluster = ::Clusters::Cluster.new.tap do |cluster|
- cluster.build_platform_kubernetes
- end.present(current_user: current_user)
+ cluster = Clusters::BuildService.new(clusterable.subject).execute
+ cluster.build_platform_kubernetes
+ @user_cluster = cluster.present(current_user: current_user)
end
def validate_gcp_token
diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb
index 0837599977f..789e0dc736e 100644
--- a/app/controllers/concerns/issuable_collections.rb
+++ b/app/controllers/concerns/issuable_collections.rb
@@ -102,7 +102,7 @@ module IssuableCollections
elsif @group
options[:group_id] = @group.id
options[:include_subgroups] = true
- options[:use_cte_for_search] = true
+ options[:attempt_group_search_optimizations] = true
end
params.permit(finder_type.valid_params).merge(options)
@@ -126,6 +126,8 @@ module IssuableCollections
sort_param = params[:sort]
sort_param ||= user_preference[issuable_sorting_field]
+ return sort_param if Gitlab::Database.read_only?
+
if user_preference[issuable_sorting_field] != sort_param
user_preference.update_attribute(issuable_sorting_field, sort_param)
end
@@ -167,12 +169,6 @@ module IssuableCollections
case value
when 'id_asc' then sort_value_oldest_created
when 'id_desc' then sort_value_recently_created
- when 'created_asc' then sort_value_created_date
- when 'created_desc' then sort_value_created_date
- when 'due_date_asc' then sort_value_due_date
- when 'due_date_desc' then sort_value_due_date
- when 'milestone_due_asc' then sort_value_milestone
- when 'milestone_due_desc' then sort_value_milestone
when 'downvotes_asc' then sort_value_popularity
when 'downvotes_desc' then sort_value_popularity
else value
diff --git a/app/controllers/concerns/renders_commits.rb b/app/controllers/concerns/renders_commits.rb
index f48e0586211..ed9b898a2a3 100644
--- a/app/controllers/concerns/renders_commits.rb
+++ b/app/controllers/concerns/renders_commits.rb
@@ -26,4 +26,10 @@ module RendersCommits
commits
end
+
+ def valid_ref?(ref_name)
+ return true unless ref_name.present?
+
+ Gitlab::GitRefValidator.validate(ref_name)
+ end
end
diff --git a/app/controllers/concerns/snippets_actions.rb b/app/controllers/concerns/snippets_actions.rb
index 8c22490700c..014232a7d05 100644
--- a/app/controllers/concerns/snippets_actions.rb
+++ b/app/controllers/concerns/snippets_actions.rb
@@ -10,6 +10,8 @@ module SnippetsActions
def raw
disposition = params[:inline] == 'false' ? 'attachment' : 'inline'
+ workhorse_set_content_type!
+
send_data(
convert_line_endings(@snippet.content),
type: 'text/plain; charset=utf-8',
diff --git a/app/controllers/concerns/uploads_actions.rb b/app/controllers/concerns/uploads_actions.rb
index 5912fffc058..0eea0cdd50f 100644
--- a/app/controllers/concerns/uploads_actions.rb
+++ b/app/controllers/concerns/uploads_actions.rb
@@ -38,6 +38,7 @@ module UploadsActions
return render_404 unless uploader
+ workhorse_set_content_type!
send_upload(uploader, attachment: uploader.filename, disposition: disposition)
end
diff --git a/app/controllers/dashboard/projects_controller.rb b/app/controllers/dashboard/projects_controller.rb
index 57e612d89d3..f073b6de444 100644
--- a/app/controllers/dashboard/projects_controller.rb
+++ b/app/controllers/dashboard/projects_controller.rb
@@ -56,7 +56,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
projects = ProjectsFinder
.new(params: finder_params, current_user: current_user)
.execute
- .includes(:route, :creator, namespace: [:route, :owner])
+ .includes(:route, :creator, :group, namespace: [:route, :owner])
.page(finder_params[:page])
prepare_projects_for_rendering(projects)
diff --git a/app/controllers/explore/projects_controller.rb b/app/controllers/explore/projects_controller.rb
index 7ecbc32cf4e..778fdda8dbd 100644
--- a/app/controllers/explore/projects_controller.rb
+++ b/app/controllers/explore/projects_controller.rb
@@ -57,7 +57,7 @@ class Explore::ProjectsController < Explore::ApplicationController
def load_projects
projects = ProjectsFinder.new(current_user: current_user, params: params)
.execute
- .includes(:route, namespace: :route)
+ .includes(:route, :creator, :group, namespace: [:route, :owner])
.page(params[:page])
.without_count
diff --git a/app/controllers/import/github_controller.rb b/app/controllers/import/github_controller.rb
index 58565aaf8c9..d4c26fa0709 100644
--- a/app/controllers/import/github_controller.rb
+++ b/app/controllers/import/github_controller.rb
@@ -7,7 +7,7 @@ class Import::GithubController < Import::BaseController
rescue_from Octokit::Unauthorized, with: :provider_unauthorized
def new
- if logged_in_with_provider?
+ if github_import_configured? && logged_in_with_provider?
go_to_provider_for_permissions
elsif session[access_token_key]
redirect_to status_import_url
diff --git a/app/controllers/notification_settings_controller.rb b/app/controllers/notification_settings_controller.rb
index 84dce74ace8..384f308269a 100644
--- a/app/controllers/notification_settings_controller.rb
+++ b/app/controllers/notification_settings_controller.rb
@@ -16,7 +16,11 @@ class NotificationSettingsController < ApplicationController
@notification_setting = current_user.notification_settings.find(params[:id])
@saved = @notification_setting.update(notification_setting_params_for(@notification_setting.source))
- render_response
+ if params[:hide_label].present?
+ render_response("projects/buttons/_notifications")
+ else
+ render_response
+ end
end
private
@@ -37,9 +41,9 @@ class NotificationSettingsController < ApplicationController
can?(current_user, ability_name, resource)
end
- def render_response
+ def render_response(response_template = "shared/notifications/_button")
render json: {
- html: view_to_html_string("shared/notifications/_button", notification_setting: @notification_setting),
+ html: view_to_html_string(response_template, notification_setting: @notification_setting),
saved: @saved
}
end
diff --git a/app/controllers/projects/commits_controller.rb b/app/controllers/projects/commits_controller.rb
index e40a1a1d744..2510a31c9b3 100644
--- a/app/controllers/projects/commits_controller.rb
+++ b/app/controllers/projects/commits_controller.rb
@@ -11,6 +11,7 @@ class Projects::CommitsController < Projects::ApplicationController
before_action :require_non_empty_project
before_action :assign_ref_vars, except: :commits_root
before_action :authorize_download_code!
+ before_action :validate_ref!, except: :commits_root
before_action :set_commits, except: :commits_root
def commits_root
@@ -54,6 +55,10 @@ class Projects::CommitsController < Projects::ApplicationController
private
+ def validate_ref!
+ render_404 unless valid_ref?(@ref)
+ end
+
def set_commits
render_404 unless @path.empty? || request.format == :atom || @repository.blob_at(@commit.id, @path) || @repository.tree(@commit.id, @path).entries.present?
@limit, @offset = (params[:limit] || 40).to_i, (params[:offset] || 0).to_i
diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb
index 2917925947f..5586c2fc631 100644
--- a/app/controllers/projects/compare_controller.rb
+++ b/app/controllers/projects/compare_controller.rb
@@ -65,12 +65,6 @@ class Projects::CompareController < Projects::ApplicationController
private
- def valid_ref?(ref_name)
- return true unless ref_name.present?
-
- Gitlab::GitRefValidator.validate(ref_name)
- end
-
def validate_refs!
valid = [head_ref, start_ref].map { |ref| valid_ref?(ref) }
diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb
index e940f382a19..a63eea0ca0e 100644
--- a/app/controllers/projects/environments_controller.rb
+++ b/app/controllers/projects/environments_controller.rb
@@ -11,6 +11,10 @@ class Projects::EnvironmentsController < Projects::ApplicationController
before_action :verify_api_request!, only: :terminal_websocket_authorize
before_action :expire_etag_cache, only: [:index]
+ before_action do
+ push_frontend_feature_flag(:area_chart, project)
+ end
+
def index
@environments = project.environments
.with_state(params[:scope] || :available)
diff --git a/app/controllers/projects/jobs_controller.rb b/app/controllers/projects/jobs_controller.rb
index 3ecf94c008e..c58b30eace7 100644
--- a/app/controllers/projects/jobs_controller.rb
+++ b/app/controllers/projects/jobs_controller.rb
@@ -140,15 +140,22 @@ class Projects::JobsController < Projects::ApplicationController
def raw
if trace_artifact_file
+ workhorse_set_content_type!
send_upload(trace_artifact_file,
send_params: raw_send_params,
redirect_params: raw_redirect_params)
else
build.trace.read do |stream|
if stream.file?
+ workhorse_set_content_type!
send_file stream.path, type: 'text/plain; charset=utf-8', disposition: 'inline'
else
- send_data stream.raw, type: 'text/plain; charset=utf-8', disposition: 'inline', filename: 'job.log'
+ # In this case we can't use workhorse_set_content_type! and let
+ # Workhorse handle the response because the data is streamed directly
+ # to the user but, because we have the trace content, we can calculate
+ # the proper content type and disposition here.
+ raw_data = stream.raw
+ send_data raw_data, type: 'text/plain; charset=utf-8', disposition: raw_trace_content_disposition(raw_data), filename: 'job.log'
end
end
end
@@ -201,4 +208,13 @@ class Projects::JobsController < Projects::ApplicationController
def build_path(build)
project_job_path(build.project, build)
end
+
+ def raw_trace_content_disposition(raw_data)
+ mime_type = MimeMagic.by_magic(raw_data)
+
+ # if mime_type is nil can also represent 'text/plain'
+ return 'inline' if mime_type.nil? || mime_type.type == 'text/plain'
+
+ 'attachment'
+ end
end
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index d521db79f85..da9316d5f22 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -122,17 +122,21 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
respond_to do |format|
format.html do
- if @merge_request.valid?
- redirect_to([@merge_request.target_project.namespace.becomes(Namespace), @merge_request.target_project, @merge_request])
- else
+ if @merge_request.errors.present?
define_edit_vars
render :edit
+ else
+ redirect_to project_merge_request_path(@merge_request.target_project, @merge_request)
end
end
format.json do
- render json: serializer.represent(@merge_request, serializer: 'basic')
+ if merge_request.errors.present?
+ render json: @merge_request.errors, status: :bad_request
+ else
+ render json: serializer.represent(@merge_request, serializer: 'basic')
+ end
end
end
rescue ActiveRecord::StaleObjectError
diff --git a/app/controllers/projects/serverless/functions_controller.rb b/app/controllers/projects/serverless/functions_controller.rb
new file mode 100644
index 00000000000..0af2b7ef343
--- /dev/null
+++ b/app/controllers/projects/serverless/functions_controller.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module Projects
+ module Serverless
+ class FunctionsController < Projects::ApplicationController
+ include ProjectUnauthorized
+
+ before_action :authorize_read_cluster!
+
+ INDEX_PRIMING_INTERVAL = 10_000
+ INDEX_POLLING_INTERVAL = 30_000
+
+ def index
+ finder = Projects::Serverless::FunctionsFinder.new(project.clusters)
+
+ respond_to do |format|
+ format.json do
+ functions = finder.execute
+
+ if functions.any?
+ Gitlab::PollingInterval.set_header(response, interval: INDEX_POLLING_INTERVAL)
+ render json: Projects::Serverless::ServiceSerializer.new(current_user: @current_user).represent(functions)
+ else
+ Gitlab::PollingInterval.set_header(response, interval: INDEX_PRIMING_INTERVAL)
+ head :no_content
+ end
+ end
+
+ format.html do
+ @installed = finder.installed?
+ render
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/app/controllers/projects/settings/repository_controller.rb b/app/controllers/projects/settings/repository_controller.rb
index 1d76c90d4eb..30724de7f6a 100644
--- a/app/controllers/projects/settings/repository_controller.rb
+++ b/app/controllers/projects/settings/repository_controller.rb
@@ -5,6 +5,7 @@ module Projects
class RepositoryController < Projects::ApplicationController
before_action :authorize_admin_project!
before_action :remote_mirror, only: [:show]
+ before_action :check_cleanup_feature_flag!, only: :cleanup
def show
render_show
@@ -20,8 +21,26 @@ module Projects
render_show
end
+ def cleanup
+ cleanup_params = params.require(:project).permit(:bfg_object_map)
+ result = Projects::UpdateService.new(project, current_user, cleanup_params).execute
+
+ if result[:status] == :success
+ RepositoryCleanupWorker.perform_async(project.id, current_user.id)
+ flash[:notice] = _('Repository cleanup has started. You will receive an email once the cleanup operation is complete.')
+ else
+ flash[:alert] = _('Failed to upload object map file')
+ end
+
+ redirect_to project_settings_repository_path(project)
+ end
+
private
+ def check_cleanup_feature_flag!
+ render_404 unless ::Feature.enabled?(:project_cleanup, project)
+ end
+
def render_show
@deploy_keys = DeployKeysPresenter.new(@project, current_user: current_user)
@deploy_tokens = @project.deploy_tokens.active
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index 8b040dc080e..072d62ddf38 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -58,11 +58,13 @@ class UsersController < ApplicationController
load_projects
skip_pagination = Gitlab::Utils.to_boolean(params[:skip_pagination])
+ skip_namespace = Gitlab::Utils.to_boolean(params[:skip_namespace])
+ compact_mode = Gitlab::Utils.to_boolean(params[:compact_mode])
respond_to do |format|
format.html { render 'show' }
format.json do
- pager_json("shared/projects/_list", @projects.count, projects: @projects, skip_pagination: skip_pagination)
+ pager_json("shared/projects/_list", @projects.count, projects: @projects, skip_pagination: skip_pagination, skip_namespace: skip_namespace, compact_mode: compact_mode)
end
end
end
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index e04e3a2a7e0..b73a3fa6e01 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -27,12 +27,13 @@
# created_before: datetime
# updated_after: datetime
# updated_before: datetime
-# use_cte_for_search: boolean
+# attempt_group_search_optimizations: boolean
#
class IssuableFinder
prepend FinderWithCrossProjectAccess
include FinderMethods
include CreatedAtFilter
+ include Gitlab::Utils::StrongMemoize
requires_cross_project_access unless: -> { project? }
@@ -75,8 +76,9 @@ class IssuableFinder
items = init_collection
items = filter_items(items)
- # This has to be last as we may use a CTE as an optimization fence by
- # passing the use_cte_for_search param
+ # This has to be last as we may use a CTE as an optimization fence
+ # by passing the attempt_group_search_optimizations param and
+ # enabling the use_cte_for_group_issues_search feature flag
# https://www.postgresql.org/docs/current/static/queries-with.html
items = by_search(items)
@@ -85,6 +87,8 @@ class IssuableFinder
def filter_items(items)
items = by_project(items)
+ items = by_group(items)
+ items = by_subquery(items)
items = by_scope(items)
items = by_created_at(items)
items = by_updated_at(items)
@@ -282,12 +286,31 @@ class IssuableFinder
end
# rubocop: enable CodeReuse/ActiveRecord
+ def use_subquery_for_search?
+ strong_memoize(:use_subquery_for_search) do
+ attempt_group_search_optimizations? &&
+ Feature.enabled?(:use_subquery_for_group_issues_search, default_enabled: false)
+ end
+ end
+
+ def use_cte_for_search?
+ strong_memoize(:use_cte_for_search) do
+ attempt_group_search_optimizations? &&
+ !use_subquery_for_search? &&
+ Feature.enabled?(:use_cte_for_group_issues_search, default_enabled: true)
+ end
+ end
+
private
def init_collection
klass.all
end
+ def attempt_group_search_optimizations?
+ search && Gitlab::Database.postgresql? && params[:attempt_group_search_optimizations]
+ end
+
def count_key(value)
Array(value).last.to_sym
end
@@ -351,12 +374,13 @@ class IssuableFinder
end
# rubocop: enable CodeReuse/ActiveRecord
- def use_cte_for_search?
- return false unless search
- return false unless Gitlab::Database.postgresql?
- return false unless Feature.enabled?(:use_cte_for_group_issues_search, default_enabled: true)
-
- params[:use_cte_for_search]
+ # Wrap projects and groups in a subquery if the conditions are met.
+ def by_subquery(items)
+ if use_subquery_for_search?
+ klass.where(id: items.select(:id)) # rubocop: disable CodeReuse/ActiveRecord
+ else
+ items
+ end
end
# rubocop: disable CodeReuse/ActiveRecord
diff --git a/app/finders/projects/serverless/functions_finder.rb b/app/finders/projects/serverless/functions_finder.rb
new file mode 100644
index 00000000000..2b5d67e79d7
--- /dev/null
+++ b/app/finders/projects/serverless/functions_finder.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module Projects
+ module Serverless
+ class FunctionsFinder
+ def initialize(clusters)
+ @clusters = clusters
+ end
+
+ def execute
+ knative_services.flatten.compact
+ end
+
+ def installed?
+ clusters_with_knative_installed.exists?
+ end
+
+ private
+
+ def knative_services
+ clusters_with_knative_installed.preload_knative.map do |cluster|
+ cluster.application_knative.services_for(ns: cluster.platform_kubernetes&.actual_namespace)
+ end
+ end
+
+ def clusters_with_knative_installed
+ @clusters.with_knative_installed
+ end
+ end
+ end
+end
diff --git a/app/finders/remote_mirror_finder.rb b/app/finders/remote_mirror_finder.rb
new file mode 100644
index 00000000000..420db0077aa
--- /dev/null
+++ b/app/finders/remote_mirror_finder.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class RemoteMirrorFinder
+ attr_accessor :params
+
+ def initialize(params)
+ @params = params
+ end
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def execute
+ RemoteMirror.find_by(id: params[:id])
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+end
diff --git a/app/helpers/appearances_helper.rb b/app/helpers/appearances_helper.rb
index ed13c5cfdd6..3f69af50f25 100644
--- a/app/helpers/appearances_helper.rb
+++ b/app/helpers/appearances_helper.rb
@@ -2,7 +2,12 @@
module AppearancesHelper
def brand_title
- current_appearance&.title.presence || 'GitLab Community Edition'
+ current_appearance&.title.presence || default_brand_title
+ end
+
+ def default_brand_title
+ # This resides in a separate method so that EE can easily redefine it.
+ 'GitLab Community Edition'
end
def brand_image
diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb
index 638744a1426..bd42f00944f 100644
--- a/app/helpers/blob_helper.rb
+++ b/app/helpers/blob_helper.rb
@@ -140,6 +140,8 @@ module BlobHelper
Gitlab::Sanitizers::SVG.clean(data)
end
+ # Remove once https://gitlab.com/gitlab-org/gitlab-ce/issues/36103 is closed
+ # and :workhorse_set_content_type flag is removed
# If we blindly set the 'real' content type when serving a Git blob we
# are enabling XSS attacks. An attacker could upload e.g. a Javascript
# file to a Git repository, trick the browser of a victim into
@@ -161,6 +163,8 @@ module BlobHelper
end
def content_disposition(blob, inline)
+ # Remove the following line when https://gitlab.com/gitlab-org/gitlab-ce/issues/36103
+ # is closed and :workhorse_set_content_type flag is removed
return 'attachment' if blob.extension == 'svg'
inline ? 'inline' : 'attachment'
diff --git a/app/helpers/button_helper.rb b/app/helpers/button_helper.rb
index 7f071d55a6b..494c754e7d5 100644
--- a/app/helpers/button_helper.rb
+++ b/app/helpers/button_helper.rb
@@ -85,13 +85,14 @@ module ButtonHelper
dropdown_item_with_description('SSH', dropdown_description, href: append_url, data: { clone_type: 'ssh' })
end
- def dropdown_item_with_description(title, description, href: nil, data: nil)
+ def dropdown_item_with_description(title, description, href: nil, data: nil, default: false)
+ active_class = "is-active" if default
button_content = content_tag(:strong, title, class: 'dropdown-menu-inner-title')
button_content << content_tag(:span, description, class: 'dropdown-menu-inner-content') if description
content_tag (href ? :a : :span),
(href ? button_content : title),
- class: "#{title.downcase}-selector",
+ class: "#{title.downcase}-selector #{active_class}",
href: (href if href),
data: (data if data)
end
diff --git a/app/helpers/dropdowns_helper.rb b/app/helpers/dropdowns_helper.rb
index 4b6c5b215e8..8d8c62f1291 100644
--- a/app/helpers/dropdowns_helper.rb
+++ b/app/helpers/dropdowns_helper.rb
@@ -11,6 +11,10 @@ module DropdownsHelper
dropdown_output = dropdown_toggle(toggle_text, data_attr, options)
+ if options.key?(:toggle_link)
+ dropdown_output = dropdown_toggle_link(toggle_text, data_attr, options)
+ end
+
dropdown_output << content_tag(:div, class: "dropdown-menu dropdown-select #{options[:dropdown_class] if options.key?(:dropdown_class)}") do
output = []
@@ -49,6 +53,11 @@ module DropdownsHelper
end
end
+ def dropdown_toggle_link(toggle_text, data_attr, options = {})
+ output = content_tag(:a, toggle_text, class: "dropdown-toggle-text #{options[:toggle_class] if options.key?(:toggle_class)}", id: (options[:id] if options.key?(:id)), data: data_attr)
+ output.html_safe
+ end
+
def dropdown_title(title, options: {})
content_tag :div, class: "dropdown-title" do
title_output = []
diff --git a/app/helpers/emails_helper.rb b/app/helpers/emails_helper.rb
index 2d2e89a2a50..e4c46ceeaa2 100644
--- a/app/helpers/emails_helper.rb
+++ b/app/helpers/emails_helper.rb
@@ -98,4 +98,29 @@ module EmailsHelper
"#{string} on #{Gitlab.config.gitlab.host}"
end
+
+ def create_list_id_string(project, list_id_max_length = 255)
+ project_path_as_domain = project.full_path.downcase
+ .split('/').reverse.join('/')
+ .gsub(%r{[^a-z0-9\/]}, '-')
+ .gsub(%r{\/+}, '.')
+ .gsub(/(\A\.+|\.+\z)/, '')
+
+ max_domain_length = list_id_max_length - Gitlab.config.gitlab.host.length - project.id.to_s.length - 2
+
+ if max_domain_length < 3
+ return project.id.to_s + "..." + Gitlab.config.gitlab.host
+ end
+
+ if project_path_as_domain.length > max_domain_length
+ project_path_as_domain = project_path_as_domain.slice(0, max_domain_length)
+
+ last_dot_index = project_path_as_domain[0..-2].rindex(".")
+ last_dot_index ||= max_domain_length - 2
+
+ project_path_as_domain = project_path_as_domain.slice(0, last_dot_index).concat("..")
+ end
+
+ project.id.to_s + "." + project_path_as_domain + "." + Gitlab.config.gitlab.host
+ end
end
diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb
index 3ce2398f1de..1371e9993b4 100644
--- a/app/helpers/events_helper.rb
+++ b/app/helpers/events_helper.rb
@@ -161,6 +161,10 @@ module EventsHelper
project_commit_url(event.project, event.note_target, anchor: dom_id(event.target))
elsif event.project_snippet_note?
project_snippet_url(event.project, event.note_target, anchor: dom_id(event.target))
+ elsif event.issue_note?
+ project_issue_url(event.project, id: event.note_target, anchor: dom_id(event.target))
+ elsif event.merge_request_note?
+ project_merge_request_url(event.project, id: event.note_target, anchor: dom_id(event.target))
else
polymorphic_url([event.project.namespace.becomes(Namespace),
event.project, event.note_target],
diff --git a/app/helpers/icons_helper.rb b/app/helpers/icons_helper.rb
index b0f63de2fb8..4e11772b252 100644
--- a/app/helpers/icons_helper.rb
+++ b/app/helpers/icons_helper.rb
@@ -42,7 +42,7 @@ module IconsHelper
end
def sprite_icon(icon_name, size: nil, css_class: nil)
- if Gitlab::Sentry.should_raise?
+ if Gitlab::Sentry.should_raise_for_dev?
unless known_sprites.include?(icon_name)
exception = ArgumentError.new("#{icon_name} is not a known icon in @gitlab-org/gitlab-svg")
raise exception
diff --git a/app/helpers/ide_helper.rb b/app/helpers/ide_helper.rb
new file mode 100644
index 00000000000..8e50bbc6c04
--- /dev/null
+++ b/app/helpers/ide_helper.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module IdeHelper
+ def ide_data
+ {
+ "empty-state-svg-path" => image_path('illustrations/multi_file_editor_empty.svg'),
+ "no-changes-state-svg-path" => image_path('illustrations/multi-editor_no_changes_empty.svg'),
+ "committed-state-svg-path" => image_path('illustrations/multi-editor_all_changes_committed_empty.svg'),
+ "pipelines-empty-state-svg-path": image_path('illustrations/pipelines_empty.svg'),
+ "promotion-svg-path": image_path('illustrations/web-ide_promotion.svg'),
+ "ci-help-page-path" => help_page_path('ci/quick_start/README'),
+ "web-ide-help-page-path" => help_page_path('user/project/web_ide/index.html'),
+ "clientside-preview-enabled": Gitlab::CurrentSettings.current_application_settings.web_ide_clientside_preview_enabled.to_s
+ }
+ end
+end
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index dfa86f52e40..da991458ea7 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -179,7 +179,7 @@ module IssuablesHelper
output << "Opened #{time_ago_with_tooltip(issuable.created_at)} by ".html_safe
output << content_tag(:strong) do
- author_output = link_to_member(project, issuable.author, size: 24, mobile_classes: "d-none d-sm-inline", tooltip: true)
+ author_output = link_to_member(project, issuable.author, size: 24, mobile_classes: "d-none d-sm-inline")
author_output << link_to_member(project, issuable.author, size: 24, by_username: true, avatar: false, mobile_classes: "d-block d-sm-none")
if status = user_status(issuable.author)
diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb
index a7fe8c3d59c..05da5ebdb22 100644
--- a/app/helpers/nav_helper.rb
+++ b/app/helpers/nav_helper.rb
@@ -47,8 +47,8 @@ module NavHelper
class_names
end
- def show_separator?
- Gitlab::Sherlock.enabled? || can?(current_user, :read_instance_statistics)
+ def has_extra_nav_icons?
+ Gitlab::Sherlock.enabled? || can?(current_user, :read_instance_statistics) || current_user.admin?
end
def page_has_markdown?
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 0a7f930110a..1186eb3ddcc 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -2,7 +2,7 @@
module ProjectsHelper
def link_to_project(project)
- link_to [project.namespace.becomes(Namespace), project], title: h(project.name) do
+ link_to namespace_project_path(namespace_id: project.namespace, id: project), title: h(project.name) do
title = content_tag(:span, project.name, class: 'project-name')
if project.namespace
@@ -50,6 +50,12 @@ module ProjectsHelper
default_opts = { avatar: true, name: true, title: ":name" }
opts = default_opts.merge(opts)
+ data_attrs = {
+ user_id: author.id,
+ username: author.username,
+ name: author.name
+ }
+
return "(deleted)" unless author
author_html = []
@@ -65,7 +71,7 @@ module ProjectsHelper
author_html = author_html.join.html_safe
if opts[:name]
- link_to(author_html, user_path(author), class: "author-link #{"#{opts[:extra_class]}" if opts[:extra_class]} #{"#{opts[:mobile_classes]}" if opts[:mobile_classes]}").html_safe
+ link_to(author_html, user_path(author), class: "author-link js-user-link #{"#{opts[:extra_class]}" if opts[:extra_class]} #{"#{opts[:mobile_classes]}" if opts[:mobile_classes]}", data: data_attrs).html_safe
else
title = opts[:title].sub(":name", sanitize(author.name))
link_to(author_html, user_path(author), class: "author-link has-tooltip", title: title, data: { container: 'body' }).html_safe
@@ -257,6 +263,10 @@ module ProjectsHelper
"xcode://clone?repo=#{CGI.escape(default_url_to_repo(project))}"
end
+ def link_to_bfg
+ link_to 'BFG', 'https://rtyley.github.io/bfg-repo-cleaner/', target: '_blank', rel: 'noopener noreferrer'
+ end
+
def legacy_render_context(params)
params[:legacy_render] ? { markdown_engine: :redcarpet } : {}
end
@@ -307,6 +317,7 @@ module ProjectsHelper
settings: :admin_project,
builds: :read_build,
clusters: :read_cluster,
+ serverless: :read_cluster,
labels: :read_label,
issues: :read_issue,
project_members: :read_project_member,
@@ -380,6 +391,10 @@ module ProjectsHelper
end
end
+ def sidebar_operations_link_path(project = @project)
+ metrics_project_environments_path(project) if can?(current_user, :read_environment, project)
+ end
+
def project_last_activity(project)
if project.last_activity_at
time_ago_with_tooltip(project.last_activity_at, placement: 'bottom', html_class: 'last_activity_time_ago')
@@ -500,6 +515,20 @@ module ProjectsHelper
end
end
+ def explore_projects_tab?
+ current_page?(explore_projects_path) ||
+ current_page?(trending_explore_projects_path) ||
+ current_page?(starred_explore_projects_path)
+ end
+
+ def show_merge_request_count?(merge_requests, compact_mode)
+ merge_requests && !compact_mode && Feature.enabled?(:project_list_show_mr_count, default_enabled: true)
+ end
+
+ def show_issue_count?(issues, compact_mode)
+ issues && !compact_mode && Feature.enabled?(:project_list_show_issue_count, default_enabled: true)
+ end
+
def sidebar_projects_paths
%w[
projects#show
@@ -545,6 +574,7 @@ module ProjectsHelper
%w[
environments
clusters
+ functions
user
gcp
]
diff --git a/app/helpers/selects_helper.rb b/app/helpers/selects_helper.rb
index cf60696ef39..2f802e4eab8 100644
--- a/app/helpers/selects_helper.rb
+++ b/app/helpers/selects_helper.rb
@@ -29,6 +29,11 @@ module SelectsHelper
classes = Array.wrap(opts[:class])
classes << 'ajax-groups-select'
+ # EE requires this line to be present, but there is no easy way of injecting
+ # this into EE without causing merge conflicts. Given this line is very
+ # simple and not really EE specific on its own, we just include it in CE.
+ classes << 'multiselect' if opts[:multiple]
+
opts[:class] = classes.join(' ')
select2_tag(id, opts)
diff --git a/app/helpers/sentry_helper.rb b/app/helpers/sentry_helper.rb
deleted file mode 100644
index d53eaef9952..00000000000
--- a/app/helpers/sentry_helper.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-# frozen_string_literal: true
-
-module SentryHelper
- def sentry_enabled?
- Gitlab::Sentry.enabled?
- end
-
- def sentry_context
- Gitlab::Sentry.context(current_user)
- end
-end
diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb
index 74113aee89d..67c808b167a 100644
--- a/app/helpers/sorting_helper.rb
+++ b/app/helpers/sorting_helper.rb
@@ -136,6 +136,53 @@ module SortingHelper
link_to item, path, class: sorted_by == item ? 'is-active' : ''
end
+ def issuable_sort_option_overrides
+ {
+ sort_value_oldest_created => sort_value_created_date,
+ sort_value_oldest_updated => sort_value_recently_updated,
+ sort_value_milestone_later => sort_value_milestone
+ }
+ end
+
+ def issuable_reverse_sort_order_hash
+ {
+ sort_value_created_date => sort_value_oldest_created,
+ sort_value_recently_created => sort_value_oldest_created,
+ sort_value_recently_updated => sort_value_oldest_updated,
+ sort_value_milestone => sort_value_milestone_later
+ }.merge(issuable_sort_option_overrides)
+ end
+
+ def issuable_sort_option_title(sort_value)
+ sort_value = issuable_sort_option_overrides[sort_value] || sort_value
+
+ sort_options_hash[sort_value]
+ end
+
+ def issuable_sort_direction_button(sort_value)
+ link_class = 'btn btn-default has-tooltip reverse-sort-btn qa-reverse-sort'
+ reverse_sort = issuable_reverse_sort_order_hash[sort_value]
+
+ if reverse_sort
+ reverse_url = page_filter_path(sort: reverse_sort, label: true)
+ else
+ reverse_url = '#'
+ link_class += ' disabled'
+ end
+
+ link_to(reverse_url, type: 'button', class: link_class, title: 'Sort direction') do
+ icon_suffix =
+ case sort_value
+ when sort_value_milestone, sort_value_due_date, /_asc\z/
+ 'lowest'
+ else
+ 'highest'
+ end
+
+ sprite_icon("sort-#{icon_suffix}", size: 16)
+ end
+ end
+
# Titles.
def sort_title_access_level_asc
s_('SortOptions|Access level, ascending')
diff --git a/app/helpers/visibility_level_helper.rb b/app/helpers/visibility_level_helper.rb
index e690350a0d1..712f0f808dd 100644
--- a/app/helpers/visibility_level_helper.rb
+++ b/app/helpers/visibility_level_helper.rb
@@ -140,7 +140,7 @@ module VisibilityLevelHelper
end
def project_visibility_icon_description(level)
- "#{project_visibility_level_description(level)}"
+ "#{visibility_level_label(level)} - #{project_visibility_level_description(level)}"
end
def visibility_level_label(level)
diff --git a/app/helpers/workhorse_helper.rb b/app/helpers/workhorse_helper.rb
index 49c08dce96c..e9fc39e451b 100644
--- a/app/helpers/workhorse_helper.rb
+++ b/app/helpers/workhorse_helper.rb
@@ -6,8 +6,13 @@ module WorkhorseHelper
# Send a Git blob through Workhorse
def send_git_blob(repository, blob, inline: true)
headers.store(*Gitlab::Workhorse.send_git_blob(repository, blob))
+
headers['Content-Disposition'] = content_disposition(blob, inline)
headers['Content-Type'] = safe_content_type(blob)
+
+ # If enabled, this will override the values set above
+ workhorse_set_content_type!
+
render plain: ""
end
@@ -40,4 +45,8 @@ module WorkhorseHelper
def set_workhorse_internal_api_content_type
headers['Content-Type'] = Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE
end
+
+ def workhorse_set_content_type!
+ headers[Gitlab::Workhorse::DETECT_HEADER] = "true" if Feature.enabled?(:workhorse_set_content_type)
+ end
end
diff --git a/app/mailers/emails/projects.rb b/app/mailers/emails/projects.rb
index d7e6c2ba7b2..2500622caa7 100644
--- a/app/mailers/emails/projects.rb
+++ b/app/mailers/emails/projects.rb
@@ -24,6 +24,21 @@ module Emails
subject: subject("Project export error"))
end
+ def repository_cleanup_success_email(project, user)
+ @project = project
+ @user = user
+
+ mail(to: user.notification_email, subject: subject("Project cleanup has completed"))
+ end
+
+ def repository_cleanup_failure_email(project, user, error)
+ @project = project
+ @user = user
+ @error = error
+
+ mail(to: user.notification_email, subject: subject("Project cleanup failure"))
+ end
+
def repository_push_email(project_id, opts = {})
@message =
Gitlab::Email::Message::RepositoryPush.new(self, project_id, opts)
diff --git a/app/mailers/emails/remote_mirrors.rb b/app/mailers/emails/remote_mirrors.rb
new file mode 100644
index 00000000000..2018eb7260b
--- /dev/null
+++ b/app/mailers/emails/remote_mirrors.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module Emails
+ module RemoteMirrors
+ def remote_mirror_update_failed_email(remote_mirror_id, recipient_id)
+ @remote_mirror = RemoteMirrorFinder.new(id: remote_mirror_id).execute
+ @project = @remote_mirror.project
+
+ mail(to: recipient(recipient_id), subject: subject('Remote mirror update failed'))
+ end
+ end
+end
diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb
index 662f3e00047..15710bee4d4 100644
--- a/app/mailers/notify.rb
+++ b/app/mailers/notify.rb
@@ -3,6 +3,7 @@
class Notify < BaseMailer
include ActionDispatch::Routing::PolymorphicRoutes
include GitlabRoutingHelper
+ include EmailsHelper
include Emails::Issues
include Emails::MergeRequests
@@ -13,6 +14,7 @@ class Notify < BaseMailer
include Emails::Pipelines
include Emails::Members
include Emails::AutoDevops
+ include Emails::RemoteMirrors
helper MergeRequestsHelper
helper DiffHelper
@@ -128,7 +130,7 @@ class Notify < BaseMailer
address.display_name = reply_display_name(model)
end
- fallback_reply_message_id = "<reply-#{reply_key}@#{Gitlab.config.gitlab.host}>".freeze
+ fallback_reply_message_id = "<reply-#{reply_key}@#{Gitlab.config.gitlab.host}>"
headers['References'] ||= []
headers['References'].unshift(fallback_reply_message_id)
@@ -166,7 +168,7 @@ class Notify < BaseMailer
headers['In-Reply-To'] = message_id(model)
headers['References'] = [message_id(model)]
- headers[:subject]&.prepend('Re: ')
+ headers[:subject] = "Re: #{headers[:subject]}" if headers[:subject]
mail_thread(model, headers)
end
@@ -178,7 +180,7 @@ class Notify < BaseMailer
headers['X-GitLab-Discussion-ID'] = note.discussion.id if note.part_of_discussion?
- headers[:subject]&.prepend('Re: ')
+ headers[:subject] = "Re: #{headers[:subject]}" if headers[:subject]
mail_thread(model, headers)
end
@@ -193,6 +195,7 @@ class Notify < BaseMailer
headers['X-GitLab-Project'] = @project.name
headers['X-GitLab-Project-Id'] = @project.id
headers['X-GitLab-Project-Path'] = @project.full_path
+ headers['List-Id'] = "#{@project.full_path} <#{create_list_id_string(@project)}>"
end
def add_unsubscription_headers_and_links
diff --git a/app/mailers/previews/notify_preview.rb b/app/mailers/previews/notify_preview.rb
index e7e8d96eca4..2ac4610967d 100644
--- a/app/mailers/previews/notify_preview.rb
+++ b/app/mailers/previews/notify_preview.rb
@@ -145,6 +145,10 @@ class NotifyPreview < ActionMailer::Preview
Notify.autodevops_disabled_email(pipeline, user.email).message
end
+ def remote_mirror_update_failed_email
+ Notify.remote_mirror_update_failed_email(remote_mirror.id, user.id).message
+ end
+
private
def project
@@ -167,6 +171,10 @@ class NotifyPreview < ActionMailer::Preview
@pipeline = Ci::Pipeline.last
end
+ def remote_mirror
+ @remote_mirror ||= RemoteMirror.last
+ end
+
def user
@user ||= User.last
end
diff --git a/app/models/broadcast_message.rb b/app/models/broadcast_message.rb
index baf8adb318b..277f7c2717c 100644
--- a/app/models/broadcast_message.rb
+++ b/app/models/broadcast_message.rb
@@ -16,14 +16,20 @@ class BroadcastMessage < ActiveRecord::Base
default_value_for :color, '#E75E40'
default_value_for :font, '#FFFFFF'
- CACHE_KEY = 'broadcast_message_current'.freeze
+ CACHE_KEY = 'broadcast_message_current_json'.freeze
+ LEGACY_CACHE_KEY = 'broadcast_message_current'.freeze
after_commit :flush_redis_cache
def self.current
- messages = Rails.cache.fetch(CACHE_KEY, expires_in: cache_expires_in) { current_and_future_messages.to_a }
+ raw_messages = Rails.cache.fetch(CACHE_KEY, expires_in: cache_expires_in) do
+ remove_legacy_cache_key
+ current_and_future_messages.to_json
+ end
- return messages if messages.empty?
+ messages = decode_messages(raw_messages)
+
+ return [] unless messages&.present?
now_or_future = messages.select(&:now_or_future?)
@@ -34,6 +40,27 @@ class BroadcastMessage < ActiveRecord::Base
now_or_future.select(&:now?)
end
+ def self.decode_messages(raw_messages)
+ return unless raw_messages&.present?
+
+ message_list = ActiveSupport::JSON.decode(raw_messages)
+
+ return unless message_list.is_a?(Array)
+
+ valid_attr = BroadcastMessage.attribute_names
+
+ message_list.map do |raw|
+ BroadcastMessage.new(raw) if valid_cache_entry?(raw, valid_attr)
+ end.compact
+ rescue ActiveSupport::JSON.parse_error
+ end
+
+ def self.valid_cache_entry?(raw, valid_attr)
+ return false unless raw.is_a?(Hash)
+
+ (raw.keys - valid_attr).empty?
+ end
+
def self.current_and_future_messages
where('ends_at > :now', now: Time.zone.now).order_id_asc
end
@@ -42,6 +69,14 @@ class BroadcastMessage < ActiveRecord::Base
nil
end
+ # This can be removed in GitLab 12.0+
+ # The old cache key had an indefinite lifetime, and in an HA
+ # environment a one-shot migration would not work because the cache
+ # would be repopulated by a node that has not been upgraded.
+ def self.remove_legacy_cache_key
+ Rails.cache.delete(LEGACY_CACHE_KEY)
+ end
+
def active?
started? && !ended?
end
@@ -68,5 +103,6 @@ class BroadcastMessage < ActiveRecord::Base
def flush_redis_cache
Rails.cache.delete(CACHE_KEY)
+ self.class.remove_legacy_cache_key
end
end
diff --git a/app/models/ci/bridge.rb b/app/models/ci/bridge.rb
new file mode 100644
index 00000000000..29aa00a66d9
--- /dev/null
+++ b/app/models/ci/bridge.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+module Ci
+ class Bridge < CommitStatus
+ include Importable
+ include AfterCommitQueue
+ include Gitlab::Utils::StrongMemoize
+
+ belongs_to :project
+ validates :ref, presence: true
+
+ def self.retry(bridge, current_user)
+ raise NotImplementedError
+ end
+
+ def tags
+ [:bridge]
+ end
+
+ def detailed_status(current_user)
+ Gitlab::Ci::Status::Bridge::Factory
+ .new(self, current_user)
+ .fabricate!
+ end
+
+ def predefined_variables
+ raise NotImplementedError
+ end
+
+ def execute_hooks
+ raise NotImplementedError
+ end
+ end
+end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index d60861dc95f..e2917049902 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -120,7 +120,7 @@ module Ci
acts_as_taggable
- add_authentication_token_field :token
+ add_authentication_token_field :token, encrypted: true, fallback: true
before_save :update_artifacts_size, if: :artifacts_file_changed?
before_save :ensure_token
@@ -742,7 +742,7 @@ module Ci
def collect_test_reports!(test_reports)
test_reports.get_suite(group_name).tap do |test_suite|
each_report(Ci::JobArtifact::TEST_REPORT_FILE_TYPES) do |file_type, blob|
- Gitlab::Ci::Parsers::Test.fabricate!(file_type).parse!(blob, test_suite)
+ Gitlab::Ci::Parsers.fabricate!(file_type).parse!(blob, test_suite)
end
end
end
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 60ff2181a95..d06022a0fb7 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -605,13 +605,18 @@ module Ci
end
def predefined_variables
- Gitlab::Ci::Variables::Collection.new
- .append(key: 'CI_PIPELINE_IID', value: iid.to_s)
- .append(key: 'CI_CONFIG_PATH', value: ci_yaml_file_path)
- .append(key: 'CI_PIPELINE_SOURCE', value: source.to_s)
- .append(key: 'CI_COMMIT_MESSAGE', value: git_commit_message.to_s)
- .append(key: 'CI_COMMIT_TITLE', value: git_commit_full_title.to_s)
- .append(key: 'CI_COMMIT_DESCRIPTION', value: git_commit_description.to_s)
+ Gitlab::Ci::Variables::Collection.new.tap do |variables|
+ variables.append(key: 'CI_PIPELINE_IID', value: iid.to_s)
+ variables.append(key: 'CI_CONFIG_PATH', value: ci_yaml_file_path)
+ variables.append(key: 'CI_PIPELINE_SOURCE', value: source.to_s)
+ variables.append(key: 'CI_COMMIT_MESSAGE', value: git_commit_message.to_s)
+ variables.append(key: 'CI_COMMIT_TITLE', value: git_commit_full_title.to_s)
+ variables.append(key: 'CI_COMMIT_DESCRIPTION', value: git_commit_description.to_s)
+
+ if merge_request? && merge_request
+ variables.concat(merge_request.predefined_variables)
+ end
+ end
end
def queued_duration
diff --git a/app/models/clusters/applications/cert_manager.rb b/app/models/clusters/applications/cert_manager.rb
index 077e2bda143..74ef7c7e145 100644
--- a/app/models/clusters/applications/cert_manager.rb
+++ b/app/models/clusters/applications/cert_manager.rb
@@ -14,6 +14,10 @@ module Clusters
default_value_for :version, VERSION
+ default_value_for :email do |cert_manager|
+ cert_manager.cluster&.user&.email
+ end
+
validates :email, presence: true
def chart
diff --git a/app/models/clusters/applications/knative.rb b/app/models/clusters/applications/knative.rb
index c0aaa8dce20..168a24da738 100644
--- a/app/models/clusters/applications/knative.rb
+++ b/app/models/clusters/applications/knative.rb
@@ -15,6 +15,9 @@ module Clusters
include ::Clusters::Concerns::ApplicationVersion
include ::Clusters::Concerns::ApplicationData
include AfterCommitQueue
+ include ReactiveCaching
+
+ self.reactive_cache_key = ->(knative) { [knative.class.model_name.singular, knative.id] }
state_machine :status do
before_transition any => [:installed] do |application|
@@ -29,6 +32,8 @@ module Clusters
validates :hostname, presence: true, hostname: true
+ scope :for_cluster, -> (cluster) { where(cluster: cluster) }
+
def chart
'knative/knative'
end
@@ -55,12 +60,39 @@ module Clusters
ClusterWaitForIngressIpAddressWorker.perform_async(name, id)
end
+ def client
+ cluster.kubeclient.knative_client
+ end
+
+ def services
+ with_reactive_cache do |data|
+ data[:services]
+ end
+ end
+
+ def calculate_reactive_cache
+ { services: read_services }
+ end
+
def ingress_service
cluster.kubeclient.get_service('knative-ingressgateway', 'istio-system')
end
- def client
- cluster.platform_kubernetes.kubeclient.knative_client
+ def services_for(ns: namespace)
+ return unless services
+ return [] unless ns
+
+ services.select do |service|
+ service.dig('metadata', 'namespace') == ns
+ end
+ end
+
+ private
+
+ def read_services
+ client.get_services.as_json
+ rescue Kubeclient::ResourceNotFoundError
+ []
end
end
end
diff --git a/app/models/clusters/applications/runner.rb b/app/models/clusters/applications/runner.rb
index 67746e34913..c931b340b24 100644
--- a/app/models/clusters/applications/runner.rb
+++ b/app/models/clusters/applications/runner.rb
@@ -3,7 +3,7 @@
module Clusters
module Applications
class Runner < ActiveRecord::Base
- VERSION = '0.1.38'.freeze
+ VERSION = '0.1.39'.freeze
self.table_name = 'clusters_applications_runners'
diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb
index c9bd1728dbd..7fe43cd2de0 100644
--- a/app/models/clusters/cluster.rb
+++ b/app/models/clusters/cluster.rb
@@ -93,6 +93,16 @@ module Clusters
where('NOT EXISTS (?)', subquery)
end
+ scope :with_knative_installed, -> { joins(:application_knative).merge(Clusters::Applications::Knative.installed) }
+
+ scope :preload_knative, -> {
+ preload(
+ :kubernetes_namespace,
+ :platform_kubernetes,
+ :application_knative
+ )
+ }
+
def self.ancestor_clusters_for_clusterable(clusterable, hierarchy_order: :asc)
hierarchy_groups = clusterable.ancestors_upto(hierarchy_order: hierarchy_order).eager_load(:clusters)
hierarchy_groups = hierarchy_groups.merge(current_scope) if current_scope
diff --git a/app/models/commit.rb b/app/models/commit.rb
index 2c89da88b9b..a422a0995ff 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -177,7 +177,9 @@ class Commit
def title
return full_title if full_title.length < 100
- full_title.truncate(81, separator: ' ', omission: '…')
+ # Use three dots instead of the ellipsis Unicode character because
+ # some clients show the raw Unicode value in the merge commit.
+ full_title.truncate(81, separator: ' ', omission: '...')
end
# Returns the full commits title
diff --git a/app/models/concerns/awardable.rb b/app/models/concerns/awardable.rb
index 60b7ec2815c..14bc56f0eee 100644
--- a/app/models/concerns/awardable.rb
+++ b/app/models/concerns/awardable.rb
@@ -43,14 +43,19 @@ module Awardable
end
def order_upvotes_desc
- order_votes_desc(AwardEmoji::UPVOTE_NAME)
+ order_votes(AwardEmoji::UPVOTE_NAME, 'DESC')
+ end
+
+ def order_upvotes_asc
+ order_votes(AwardEmoji::UPVOTE_NAME, 'ASC')
end
def order_downvotes_desc
- order_votes_desc(AwardEmoji::DOWNVOTE_NAME)
+ order_votes(AwardEmoji::DOWNVOTE_NAME, 'DESC')
end
- def order_votes_desc(emoji_name)
+ # Order votes by emoji, optional sort order param `descending` defaults to true
+ def order_votes(emoji_name, direction)
awardable_table = self.arel_table
awards_table = AwardEmoji.arel_table
@@ -62,7 +67,7 @@ module Awardable
)
).join_sources
- joins(join_clause).group(awardable_table[:id]).reorder("COUNT(award_emoji.id) DESC")
+ joins(join_clause).group(awardable_table[:id]).reorder("COUNT(award_emoji.id) #{direction}")
end
end
diff --git a/app/models/concerns/discussion_on_diff.rb b/app/models/concerns/discussion_on_diff.rb
index c180d7b7c9a..266c37fa3a1 100644
--- a/app/models/concerns/discussion_on_diff.rb
+++ b/app/models/concerns/discussion_on_diff.rb
@@ -38,12 +38,13 @@ module DiscussionOnDiff
end
# Returns an array of at most 16 highlighted lines above a diff note
- def truncated_diff_lines(highlight: true)
+ def truncated_diff_lines(highlight: true, diff_limit: nil)
return [] if diff_line.nil? && first_note.is_a?(LegacyDiffNote)
+ diff_limit = [diff_limit, NUMBER_OF_TRUNCATED_DIFF_LINES].compact.min
lines = highlight ? highlighted_diff_lines : diff_lines
- initial_line_index = [diff_line.index - NUMBER_OF_TRUNCATED_DIFF_LINES + 1, 0].max
+ initial_line_index = [diff_line.index - diff_limit + 1, 0].max
prev_lines = []
diff --git a/app/models/concerns/fast_destroy_all.rb b/app/models/concerns/fast_destroy_all.rb
index 2bfa7da6c1c..1e3afd641ed 100644
--- a/app/models/concerns/fast_destroy_all.rb
+++ b/app/models/concerns/fast_destroy_all.rb
@@ -70,13 +70,14 @@ module FastDestroyAll
module Helpers
extend ActiveSupport::Concern
+ include AfterCommitQueue
class_methods do
##
# This method is to be defined on models which have fast destroyable models as children,
# and let us avoid to use `dependent: :destroy` hook
- def use_fast_destroy(relation)
- before_destroy(prepend: true) do
+ def use_fast_destroy(relation, opts = {})
+ set_callback :destroy, :before, opts.merge(prepend: true) do
perform_fast_destroy(public_send(relation)) # rubocop:disable GitlabSecurity/PublicSend
end
end
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 5080fe03cc8..0d363ec68b7 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -145,14 +145,16 @@ module Issuable
def sort_by_attribute(method, excluded_labels: [])
sorted =
case method.to_s
- when 'downvotes_desc' then order_downvotes_desc
- when 'label_priority' then order_labels_priority(excluded_labels: excluded_labels)
- when 'milestone' then order_milestone_due_asc
- when 'milestone_due_asc' then order_milestone_due_asc
- when 'milestone_due_desc' then order_milestone_due_desc
- when 'popularity' then order_upvotes_desc
- when 'priority' then order_due_date_and_labels_priority(excluded_labels: excluded_labels)
- when 'upvotes_desc' then order_upvotes_desc
+ when 'downvotes_desc' then order_downvotes_desc
+ when 'label_priority' then order_labels_priority(excluded_labels: excluded_labels)
+ when 'label_priority_desc' then order_labels_priority('DESC', excluded_labels: excluded_labels)
+ when 'milestone', 'milestone_due_asc' then order_milestone_due_asc
+ when 'milestone_due_desc' then order_milestone_due_desc
+ when 'popularity', 'popularity_desc' then order_upvotes_desc
+ when 'popularity_asc' then order_upvotes_asc
+ when 'priority', 'priority_asc' then order_due_date_and_labels_priority(excluded_labels: excluded_labels)
+ when 'priority_desc' then order_due_date_and_labels_priority('DESC', excluded_labels: excluded_labels)
+ when 'upvotes_desc' then order_upvotes_desc
else order_by(method)
end
@@ -160,7 +162,7 @@ module Issuable
sorted.with_order_id_desc
end
- def order_due_date_and_labels_priority(excluded_labels: [])
+ def order_due_date_and_labels_priority(direction = 'ASC', excluded_labels: [])
# The order_ methods also modify the query in other ways:
#
# - For milestones, we add a JOIN.
@@ -177,11 +179,11 @@ module Issuable
order_milestone_due_asc
.order_labels_priority(excluded_labels: excluded_labels, extra_select_columns: [milestones_due_date])
- .reorder(Gitlab::Database.nulls_last_order(milestones_due_date, 'ASC'),
- Gitlab::Database.nulls_last_order('highest_priority', 'ASC'))
+ .reorder(Gitlab::Database.nulls_last_order(milestones_due_date, direction),
+ Gitlab::Database.nulls_last_order('highest_priority', direction))
end
- def order_labels_priority(excluded_labels: [], extra_select_columns: [])
+ def order_labels_priority(direction = 'ASC', excluded_labels: [], extra_select_columns: [])
params = {
target_type: name,
target_column: "#{table_name}.id",
@@ -198,7 +200,7 @@ module Issuable
select(select_columns.join(', '))
.group(arel_table[:id])
- .reorder(Gitlab::Database.nulls_last_order('highest_priority', 'ASC'))
+ .reorder(Gitlab::Database.nulls_last_order('highest_priority', direction))
end
def with_label(title, sort = nil)
diff --git a/app/models/concerns/storage/legacy_namespace.rb b/app/models/concerns/storage/legacy_namespace.rb
index af699eeebce..498996f4f80 100644
--- a/app/models/concerns/storage/legacy_namespace.rb
+++ b/app/models/concerns/storage/legacy_namespace.rb
@@ -4,6 +4,8 @@ module Storage
module LegacyNamespace
extend ActiveSupport::Concern
+ include Gitlab::ShellAdapter
+
def move_dir
proj_with_tags = first_project_with_container_registry_tags
diff --git a/app/models/concerns/with_uploads.rb b/app/models/concerns/with_uploads.rb
index 2bdef2a40e4..d79c0eae77e 100644
--- a/app/models/concerns/with_uploads.rb
+++ b/app/models/concerns/with_uploads.rb
@@ -17,6 +17,8 @@
module WithUploads
extend ActiveSupport::Concern
+ include FastDestroyAll::Helpers
+ include FeatureGate
# Currently there is no simple way how to select only not-mounted
# uploads, it should be all FileUploaders so we select them by
@@ -25,21 +27,40 @@ module WithUploads
included do
has_many :uploads, as: :model
+ has_many :file_uploads, -> { where(uploader: FILE_UPLOADERS) }, class_name: 'Upload', as: :model
- before_destroy :destroy_file_uploads
+ # TODO: when feature flag is removed, we can use just dependent: destroy
+ # option on :file_uploads
+ before_destroy :remove_file_uploads
+
+ use_fast_destroy :file_uploads, if: :fast_destroy_enabled?
+ end
+
+ def retrieve_upload(_identifier, paths)
+ uploads.find_by(path: paths)
end
+ private
+
# mounted uploads are deleted in carrierwave's after_commit hook,
# but FileUploaders which are not mounted must be deleted explicitly and
# it can not be done in after_commit because FileUploader requires loads
# associated model on destroy (which is already deleted in after_commit)
- def destroy_file_uploads
- self.uploads.where(uploader: FILE_UPLOADERS).find_each do |upload|
+ def remove_file_uploads
+ fast_destroy_enabled? ? delete_uploads : destroy_uploads
+ end
+
+ def delete_uploads
+ file_uploads.delete_all(:delete_all)
+ end
+
+ def destroy_uploads
+ file_uploads.find_each do |upload|
upload.destroy
end
end
- def retrieve_upload(_identifier, paths)
- uploads.find_by(path: paths)
+ def fast_destroy_enabled?
+ Feature.enabled?(:fast_destroy_uploads, self)
end
end
diff --git a/app/models/event.rb b/app/models/event.rb
index 2e690f8c013..2ceef412af5 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -87,7 +87,7 @@ class Event < ActiveRecord::Base
scope :with_associations, -> do
# We're using preload for "push_event_payload" as otherwise the association
# is not always available (depending on the query being built).
- includes(:author, :project, project: :namespace)
+ includes(:author, :project, project: [:project_feature, :import_data, :namespace])
.preload(:target, :push_event_payload)
end
diff --git a/app/models/member.rb b/app/models/member.rb
index bc8ac14d148..9fc95ea00c3 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -7,6 +7,7 @@ class Member < ActiveRecord::Base
include Expirable
include Gitlab::Access
include Presentable
+ include Gitlab::Utils::StrongMemoize
attr_accessor :raw_invite_token
@@ -22,6 +23,7 @@ class Member < ActiveRecord::Base
message: "already exists in source",
allow_nil: true }
validates :access_level, inclusion: { in: Gitlab::Access.all_values }, presence: true
+ validate :higher_access_level_than_group, unless: :importing?
validates :invite_email,
presence: {
if: :invite?
@@ -364,6 +366,15 @@ class Member < ActiveRecord::Base
end
# rubocop: enable CodeReuse/ServiceClass
+ # Find the user's group member with a highest access level
+ def highest_group_member
+ strong_memoize(:highest_group_member) do
+ next unless user_id && source&.ancestors&.any?
+
+ GroupMember.where(source: source.ancestors, user_id: user_id).order(:access_level).last
+ end
+ end
+
private
def send_invite
@@ -430,4 +441,12 @@ class Member < ActiveRecord::Base
def notifiable_options
{}
end
+
+ def higher_access_level_than_group
+ if highest_group_member && highest_group_member.access_level >= access_level
+ error_parameters = { access: highest_group_member.human_access, group_name: highest_group_member.group.name }
+
+ errors.add(:access_level, s_("should be higher than %{access} inherited membership from group %{group_name}") % error_parameters)
+ end
+ end
end
diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb
index 537f2a3a231..016c18ce6c8 100644
--- a/app/models/members/project_member.rb
+++ b/app/models/members/project_member.rb
@@ -3,8 +3,6 @@
class ProjectMember < Member
SOURCE_TYPE = 'Project'.freeze
- include Gitlab::ShellAdapter
-
belongs_to :project, foreign_key: 'source_id'
# Make sure project member points only to project as it source
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index f40dff7c1bd..a13cac73d04 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -539,15 +539,26 @@ class MergeRequest < ActiveRecord::Base
def validate_branches
if target_project == source_project && target_branch == source_branch
- errors.add :branch_conflict, "You can not use same project/branch for source and target"
+ errors.add :branch_conflict, "You can't use same project/branch for source and target"
+ return
end
if opened?
- similar_mrs = self.target_project.merge_requests.where(source_branch: source_branch, target_branch: target_branch, source_project_id: source_project.try(:id)).opened
- similar_mrs = similar_mrs.where('id not in (?)', self.id) if self.id
- if similar_mrs.any?
- errors.add :validate_branches,
- "Cannot Create: This merge request already exists: #{similar_mrs.pluck(:title)}"
+ similar_mrs = target_project
+ .merge_requests
+ .where(source_branch: source_branch, target_branch: target_branch)
+ .where(source_project_id: source_project&.id)
+ .opened
+
+ similar_mrs = similar_mrs.where.not(id: id) if persisted?
+
+ conflict = similar_mrs.first
+
+ if conflict.present?
+ errors.add(
+ :validate_branches,
+ "Another open merge request already exists for this source branch: #{conflict.to_reference}"
+ )
end
end
end
@@ -967,6 +978,7 @@ class MergeRequest < ActiveRecord::Base
def mergeable_ci_state?
return true unless project.only_allow_merge_if_pipeline_succeeds?
+ return true unless head_pipeline
actual_head_pipeline&.success? || actual_head_pipeline&.skipped?
end
@@ -1070,14 +1082,53 @@ class MergeRequest < ActiveRecord::Base
actual_head_pipeline&.has_test_reports?
end
- # rubocop: disable CodeReuse/ServiceClass
+ def predefined_variables
+ Gitlab::Ci::Variables::Collection.new.tap do |variables|
+ variables.append(key: 'CI_MERGE_REQUEST_ID', value: id.to_s)
+ variables.append(key: 'CI_MERGE_REQUEST_IID', value: iid.to_s)
+
+ variables.append(key: 'CI_MERGE_REQUEST_REF_PATH',
+ value: ref_path.to_s)
+
+ variables.append(key: 'CI_MERGE_REQUEST_PROJECT_ID',
+ value: project.id.to_s)
+
+ variables.append(key: 'CI_MERGE_REQUEST_PROJECT_PATH',
+ value: project.full_path)
+
+ variables.append(key: 'CI_MERGE_REQUEST_PROJECT_URL',
+ value: project.web_url)
+
+ variables.append(key: 'CI_MERGE_REQUEST_TARGET_BRANCH_NAME',
+ value: target_branch.to_s)
+
+ if source_project
+ variables.append(key: 'CI_MERGE_REQUEST_SOURCE_PROJECT_ID',
+ value: source_project.id.to_s)
+
+ variables.append(key: 'CI_MERGE_REQUEST_SOURCE_PROJECT_PATH',
+ value: source_project.full_path)
+
+ variables.append(key: 'CI_MERGE_REQUEST_SOURCE_PROJECT_URL',
+ value: source_project.web_url)
+
+ variables.append(key: 'CI_MERGE_REQUEST_SOURCE_BRANCH_NAME',
+ value: source_branch.to_s)
+ end
+ end
+ end
+
def compare_test_reports
unless has_test_reports?
return { status: :error, status_reason: 'This merge request does not have test reports' }
end
- with_reactive_cache(:compare_test_results) do |data|
- unless Ci::CompareTestReportsService.new(project)
+ compare_reports(Ci::CompareTestReportsService)
+ end
+
+ def compare_reports(service_class)
+ with_reactive_cache(service_class.name) do |data|
+ unless service_class.new(project)
.latest?(base_pipeline, actual_head_pipeline, data)
raise InvalidateReactiveCache
end
@@ -1085,19 +1136,14 @@ class MergeRequest < ActiveRecord::Base
data
end || { status: :parsing }
end
- # rubocop: enable CodeReuse/ServiceClass
- # rubocop: disable CodeReuse/ServiceClass
def calculate_reactive_cache(identifier, *args)
- case identifier.to_sym
- when :compare_test_results
- Ci::CompareTestReportsService.new(project).execute(
- base_pipeline, actual_head_pipeline)
- else
- raise NotImplementedError, "Unknown identifier: #{identifier}"
- end
+ service_class = identifier.constantize
+
+ raise NameError, service_class unless service_class < Ci::CompareReportsBaseService
+
+ service_class.new(project).execute(base_pipeline, actual_head_pipeline)
end
- # rubocop: enable CodeReuse/ServiceClass
def all_commits
# MySQL doesn't support LIMIT in a subquery.
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 8865c164b11..3c9b1d32a53 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -3,7 +3,6 @@
class Namespace < ActiveRecord::Base
include CacheMarkdownField
include Sortable
- include Gitlab::ShellAdapter
include Gitlab::VisibilityLevel
include Routable
include AfterCommitQueue
diff --git a/app/models/note.rb b/app/models/note.rb
index a6ae4f58ac4..17c7d97fa0a 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -131,7 +131,7 @@ class Note < ActiveRecord::Base
scope :with_associations, -> do
# FYI noteable cannot be loaded for LegacyDiffNote for commits
includes(:author, :noteable, :updated_by,
- project: [:project_members, { group: [:group_members] }])
+ project: [:project_members, :namespace, { group: [:group_members] }])
end
scope :with_metadata, -> { includes(:system_note_metadata) }
diff --git a/app/models/pool_repository.rb b/app/models/pool_repository.rb
index bad0e30ceb5..47da0209c2f 100644
--- a/app/models/pool_repository.rb
+++ b/app/models/pool_repository.rb
@@ -1,12 +1,93 @@
# frozen_string_literal: true
+# The PoolRepository model is the database equivalent of an ObjectPool for Gitaly
+# That is; PoolRepository is the record in the database, ObjectPool is the
+# repository on disk
class PoolRepository < ActiveRecord::Base
include Shardable
+ include AfterCommitQueue
+
+ has_one :source_project, class_name: 'Project'
+ validates :source_project, presence: true
has_many :member_projects, class_name: 'Project'
after_create :correct_disk_path
+ state_machine :state, initial: :none do
+ state :scheduled
+ state :ready
+ state :failed
+
+ event :schedule do
+ transition none: :scheduled
+ end
+
+ event :mark_ready do
+ transition [:scheduled, :failed] => :ready
+ end
+
+ event :mark_failed do
+ transition all => :failed
+ end
+
+ state all - [:ready] do
+ def joinable?
+ false
+ end
+ end
+
+ state :ready do
+ def joinable?
+ true
+ end
+ end
+
+ after_transition none: :scheduled do |pool, _|
+ pool.run_after_commit do
+ ::ObjectPool::CreateWorker.perform_async(pool.id)
+ end
+ end
+
+ after_transition scheduled: :ready do |pool, _|
+ pool.run_after_commit do
+ ::ObjectPool::ScheduleJoinWorker.perform_async(pool.id)
+ end
+ end
+ end
+
+ def create_object_pool
+ object_pool.create
+ end
+
+ # The members of the pool should have fetched the missing objects to their own
+ # objects directory. If the caller fails to do so, data loss might occur
+ def delete_object_pool
+ object_pool.delete
+ end
+
+ def link_repository(repository)
+ object_pool.link(repository.raw)
+ end
+
+ # This RPC can cause data loss, as not all objects are present the local repository
+ # No execution path yet, will be added through:
+ # https://gitlab.com/gitlab-org/gitaly/issues/1415
+ def delete_repository_alternate(repository)
+ object_pool.unlink_repository(repository.raw)
+ end
+
+ def object_pool
+ @object_pool ||= Gitlab::Git::ObjectPool.new(
+ shard.name,
+ disk_path + '.git',
+ source_project.repository.raw)
+ end
+
+ def inspect
+ "#<#{self.class.name} id:#{id} state:#{state} disk_path:#{disk_path} source_project: #{source_project.full_path}>"
+ end
+
private
def correct_disk_path
diff --git a/app/models/project.rb b/app/models/project.rb
index 587bada469e..67262ecce85 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -339,6 +339,7 @@ class Project < ActiveRecord::Base
presence: true,
inclusion: { in: ->(_object) { Gitlab.config.repositories.storages.keys } }
validates :variables, variable_duplicates: { scope: :environment_scope }
+ validates :bfg_object_map, file_size: { maximum: :max_attachment_size }
# Scopes
scope :pending_delete, -> { where(pending_delete: true) }
@@ -412,6 +413,9 @@ class Project < ActiveRecord::Base
only_integer: true,
message: 'needs to be beetween 10 minutes and 1 month' }
+ # Used by Projects::CleanupService to hold a map of rewritten object IDs
+ mount_uploader :bfg_object_map, AttachmentUploader
+
# Returns a project, if it is not about to be removed.
#
# id - The ID of the project to retrieve.
@@ -570,6 +574,8 @@ class Project < ActiveRecord::Base
.base_and_ancestors(upto: top, hierarchy_order: hierarchy_order)
end
+ alias_method :ancestors, :ancestors_upto
+
def lfs_enabled?
return namespace.lfs_enabled? if self[:lfs_enabled].nil?
@@ -649,6 +655,11 @@ class Project < ActiveRecord::Base
end
end
+ def latest_successful_build_for(job_name, ref = default_branch)
+ builds = latest_successful_builds_for(ref)
+ builds.find_by!(name: job_name)
+ end
+
def merge_base_commit(first_commit_id, second_commit_id)
sha = repository.merge_base(first_commit_id, second_commit_id)
commit_by(oid: sha) if sha
@@ -738,15 +749,9 @@ class Project < ActiveRecord::Base
return if data.nil? && credentials.nil?
project_import_data = import_data || build_import_data
- if data
- project_import_data.data ||= {}
- project_import_data.data = project_import_data.data.merge(data)
- end
- if credentials
- project_import_data.credentials ||= {}
- project_import_data.credentials = project_import_data.credentials.merge(credentials)
- end
+ project_import_data.merge_data(data.to_h)
+ project_import_data.merge_credentials(credentials.to_h)
project_import_data
end
@@ -1579,6 +1584,7 @@ class Project < ActiveRecord::Base
import_state.remove_jid
update_project_counter_caches
after_create_default_branch
+ join_pool_repository
refresh_markdown_cache!
end
@@ -1971,8 +1977,52 @@ class Project < ActiveRecord::Base
Ability.allowed?(user, :read_project_snippet, self)
end
+ def max_attachment_size
+ Gitlab::CurrentSettings.max_attachment_size.megabytes.to_i
+ end
+
+ def object_pool_params
+ return {} unless !forked? && git_objects_poolable?
+
+ {
+ repository_storage: repository_storage,
+ pool_repository: pool_repository || create_new_pool_repository
+ }
+ end
+
+ # Git objects are only poolable when the project is or has:
+ # - Hashed storage -> The object pool will have a remote to its members, using relative paths.
+ # If the repository path changes we would have to update the remote.
+ # - Public -> User will be able to fetch Git objects that might not exist
+ # in their own repository.
+ # - Repository -> Else the disk path will be empty, and there's nothing to pool
+ def git_objects_poolable?
+ hashed_storage?(:repository) &&
+ public? &&
+ repository_exists? &&
+ Gitlab::CurrentSettings.hashed_storage_enabled &&
+ Feature.enabled?(:object_pools, self)
+ end
+
private
+ def create_new_pool_repository
+ pool = begin
+ create_pool_repository!(shard: Shard.by_name(repository_storage), source_project: self)
+ rescue ActiveRecord::RecordNotUnique
+ pool_repository(true)
+ end
+
+ pool.schedule unless pool.scheduled?
+ pool
+ end
+
+ def join_pool_repository
+ return unless pool_repository
+
+ ObjectPool::JoinWorker.perform_async(pool_repository.id, self.id)
+ end
+
def use_hashed_storage
if self.new_record? && Gitlab::CurrentSettings.hashed_storage_enabled
self.storage_version = LATEST_STORAGE_VERSION
diff --git a/app/models/project_import_data.rb b/app/models/project_import_data.rb
index 2c3080c6d8d..525725034a5 100644
--- a/app/models/project_import_data.rb
+++ b/app/models/project_import_data.rb
@@ -22,4 +22,12 @@ class ProjectImportData < ActiveRecord::Base
# bang doesn't work here - attr_encrypted makes it not to work
self.credentials = self.credentials.deep_symbolize_keys unless self.credentials.blank?
end
+
+ def merge_data(hash)
+ self.data = data.to_h.merge(hash) unless hash.empty?
+ end
+
+ def merge_credentials(hash)
+ self.credentials = credentials.to_h.merge(hash) unless hash.empty?
+ end
end
diff --git a/app/models/protected_branch.rb b/app/models/protected_branch.rb
index 6c1073265a1..d075440b147 100644
--- a/app/models/protected_branch.rb
+++ b/app/models/protected_branch.rb
@@ -1,7 +1,6 @@
# frozen_string_literal: true
class ProtectedBranch < ActiveRecord::Base
- include Gitlab::ShellAdapter
include ProtectedRef
protected_ref_access_levels :merge, :push
diff --git a/app/models/protected_tag.rb b/app/models/protected_tag.rb
index 94746141945..d28ebabfe49 100644
--- a/app/models/protected_tag.rb
+++ b/app/models/protected_tag.rb
@@ -1,7 +1,6 @@
# frozen_string_literal: true
class ProtectedTag < ActiveRecord::Base
- include Gitlab::ShellAdapter
include ProtectedRef
validates :name, uniqueness: { scope: :project_id }
diff --git a/app/models/remote_mirror.rb b/app/models/remote_mirror.rb
index b7b4d0f1be9..5a6895aefab 100644
--- a/app/models/remote_mirror.rb
+++ b/app/models/remote_mirror.rb
@@ -65,10 +65,14 @@ class RemoteMirror < ActiveRecord::Base
)
end
- after_transition started: :failed do |remote_mirror, _|
+ after_transition started: :failed do |remote_mirror|
Gitlab::Metrics.add_event(:remote_mirrors_failed)
remote_mirror.update(last_update_at: Time.now)
+
+ remote_mirror.run_after_commit do
+ RemoteMirrorNotificationWorker.perform_async(remote_mirror.id)
+ end
end
end
@@ -135,8 +139,8 @@ class RemoteMirror < ActiveRecord::Base
end
def mark_as_failed(error_message)
- update_fail
update_column(:last_error, Gitlab::UrlSanitizer.sanitize(error_message))
+ update_fail
end
def url=(value)
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 35dd120856d..015a179f374 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -17,7 +17,6 @@ class Repository
#{REF_ENVIRONMENTS}
].freeze
- include Gitlab::ShellAdapter
include Gitlab::RepositoryCacheAdapter
attr_accessor :full_path, :disk_path, :project, :is_wiki
diff --git a/app/models/shard.rb b/app/models/shard.rb
index 2e75bc91df0..e39d4232486 100644
--- a/app/models/shard.rb
+++ b/app/models/shard.rb
@@ -18,7 +18,9 @@ class Shard < ActiveRecord::Base
end
def self.by_name(name)
- find_or_create_by(name: name)
+ transaction(requires_new: true) do
+ find_or_create_by(name: name)
+ end
rescue ActiveRecord::RecordNotUnique
retry
end
diff --git a/app/models/upload.rb b/app/models/upload.rb
index e01e9c6a4f0..20860f14b83 100644
--- a/app/models/upload.rb
+++ b/app/models/upload.rb
@@ -25,6 +25,25 @@ class Upload < ActiveRecord::Base
Digest::SHA256.file(path).hexdigest
end
+ class << self
+ ##
+ # FastDestroyAll concerns
+ def begin_fast_destroy
+ {
+ Uploads::Local => Uploads::Local.new.keys(with_files_stored_locally),
+ Uploads::Fog => Uploads::Fog.new.keys(with_files_stored_remotely)
+ }
+ end
+
+ ##
+ # FastDestroyAll concerns
+ def finalize_fast_destroy(keys)
+ keys.each do |store_class, paths|
+ store_class.new.delete_keys_async(paths)
+ end
+ end
+ end
+
def absolute_path
raise ObjectStorage::RemoteStoreError, "Remote object has no absolute path." unless local?
return path unless relative_path?
diff --git a/app/models/uploads/base.rb b/app/models/uploads/base.rb
new file mode 100644
index 00000000000..f9814159958
--- /dev/null
+++ b/app/models/uploads/base.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Uploads
+ class Base
+ BATCH_SIZE = 100
+
+ attr_reader :logger
+
+ def initialize(logger: nil)
+ @logger ||= Rails.logger
+ end
+
+ def delete_keys_async(keys_to_delete)
+ keys_to_delete.each_slice(BATCH_SIZE) do |batch|
+ DeleteStoredFilesWorker.perform_async(self.class, batch)
+ end
+ end
+ end
+end
diff --git a/app/models/uploads/fog.rb b/app/models/uploads/fog.rb
new file mode 100644
index 00000000000..b44e273e9ab
--- /dev/null
+++ b/app/models/uploads/fog.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+module Uploads
+ class Fog < Base
+ include ::Gitlab::Utils::StrongMemoize
+
+ def available?
+ object_store.enabled
+ end
+
+ def keys(relation)
+ return [] unless available?
+
+ relation.pluck(:path)
+ end
+
+ def delete_keys(keys)
+ keys.each do |key|
+ connection.delete_object(bucket_name, key)
+ end
+ end
+
+ private
+
+ def object_store
+ Gitlab.config.uploads.object_store
+ end
+
+ def bucket_name
+ return unless available?
+
+ object_store.remote_directory
+ end
+
+ def connection
+ return unless available?
+
+ strong_memoize(:connection) do
+ ::Fog::Storage.new(object_store.connection.to_hash.deep_symbolize_keys)
+ end
+ end
+ end
+end
diff --git a/app/models/uploads/local.rb b/app/models/uploads/local.rb
new file mode 100644
index 00000000000..2901c33c359
--- /dev/null
+++ b/app/models/uploads/local.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+module Uploads
+ class Local < Base
+ def keys(relation)
+ relation.includes(:model).find_each.map(&:absolute_path)
+ end
+
+ def delete_keys(keys)
+ keys.each do |path|
+ delete_file(path)
+ end
+ end
+
+ private
+
+ def delete_file(path)
+ unless exists?(path)
+ logger.warn("File '#{path}' doesn't exist, skipping")
+ return
+ end
+
+ unless in_uploads?(path)
+ message = "Path '#{path}' is not in uploads dir, skipping"
+ logger.warn(message)
+ Gitlab::Sentry.track_exception(RuntimeError.new(message), extra: { uploads_dir: storage_dir })
+ return
+ end
+
+ FileUtils.rm(path)
+ delete_dir!(File.dirname(path))
+ end
+
+ def exists?(path)
+ path.present? && File.exist?(path)
+ end
+
+ def in_uploads?(path)
+ path.start_with?(storage_dir)
+ end
+
+ def delete_dir!(path)
+ Dir.rmdir(path)
+ rescue Errno::ENOENT
+ # Ignore: path does not exist
+ rescue Errno::ENOTDIR
+ # Ignore: path is not a dir
+ rescue Errno::ENOTEMPTY, Errno::EEXIST
+ # Ignore: dir is not empty
+ end
+
+ def storage_dir
+ @storage_dir ||= File.realpath(Gitlab.config.uploads.storage_path)
+ end
+ end
+end
diff --git a/app/presenters/group_clusterable_presenter.rb b/app/presenters/group_clusterable_presenter.rb
index d963c188559..ef6bbc0d109 100644
--- a/app/presenters/group_clusterable_presenter.rb
+++ b/app/presenters/group_clusterable_presenter.rb
@@ -31,6 +31,6 @@ class GroupClusterablePresenter < ClusterablePresenter
override :learn_more_link
def learn_more_link
- link_to(s_('ClusterIntegration|Learn more about group Kubernetes clusters'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer')
+ link_to(s_('ClusterIntegration|Learn more about group Kubernetes clusters'), help_page_path('user/group/clusters/index'), target: '_blank', rel: 'noopener noreferrer')
end
end
diff --git a/app/presenters/member_presenter.rb b/app/presenters/member_presenter.rb
index 2497bea4aff..9e9b6973b8e 100644
--- a/app/presenters/member_presenter.rb
+++ b/app/presenters/member_presenter.rb
@@ -7,6 +7,14 @@ class MemberPresenter < Gitlab::View::Presenter::Delegated
member.class.access_level_roles
end
+ def valid_level_roles
+ return access_level_roles unless member.highest_group_member
+
+ access_level_roles.reject do |_name, level|
+ member.highest_group_member.access_level > level
+ end
+ end
+
def can_resend_invite?
invite? &&
can?(current_user, admin_member_permission, source)
diff --git a/app/presenters/project_presenter.rb b/app/presenters/project_presenter.rb
index d61124fa787..9bd64ea217e 100644
--- a/app/presenters/project_presenter.rb
+++ b/app/presenters/project_presenter.rb
@@ -6,27 +6,27 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
include GitlabRoutingHelper
include StorageHelper
include TreeHelper
+ include IconsHelper
include ChecksCollaboration
include Gitlab::Utils::StrongMemoize
presents :project
- AnchorData = Struct.new(:enabled, :label, :link, :class_modifier)
+ AnchorData = Struct.new(:is_link, :label, :link, :class_modifier, :icon)
MAX_TAGS_TO_SHOW = 3
+ def statistic_icon(icon_name = 'plus-square-o')
+ sprite_icon(icon_name, size: 16, css_class: 'icon append-right-4')
+ end
+
def statistics_anchors(show_auto_devops_callout:)
[
- readme_anchor_data,
- changelog_anchor_data,
- contribution_guide_anchor_data,
- files_anchor_data,
+ license_anchor_data,
commits_anchor_data,
branches_anchor_data,
tags_anchor_data,
- gitlab_ci_anchor_data,
- autodevops_anchor_data(show_auto_devops_callout: show_auto_devops_callout),
- kubernetes_cluster_anchor_data
- ].compact.select { |item| item.enabled }
+ files_anchor_data
+ ].compact.select(&:is_link)
end
def statistics_buttons(show_auto_devops_callout:)
@@ -37,27 +37,28 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
autodevops_anchor_data(show_auto_devops_callout: show_auto_devops_callout),
kubernetes_cluster_anchor_data,
gitlab_ci_anchor_data
- ].compact.reject { |item| item.enabled }
+ ].compact.reject(&:is_link)
end
def empty_repo_statistics_anchors
[
- files_anchor_data,
+ license_anchor_data,
commits_anchor_data,
branches_anchor_data,
tags_anchor_data,
- autodevops_anchor_data,
- kubernetes_cluster_anchor_data
- ].compact.select { |item| item.enabled }
+ files_anchor_data
+ ].compact.select { |item| item.is_link }
end
def empty_repo_statistics_buttons
[
new_file_anchor_data,
readme_anchor_data,
+ changelog_anchor_data,
+ contribution_guide_anchor_data,
autodevops_anchor_data,
kubernetes_cluster_anchor_data
- ].compact.reject { |item| item.enabled }
+ ].compact.reject { |item| item.is_link }
end
def default_view
@@ -113,7 +114,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
end
def add_contribution_guide_path
- add_special_file_path(file_name: 'CONTRIBUTING.md', commit_message: 'Add contribution guide')
+ add_special_file_path(file_name: 'CONTRIBUTING.md', commit_message: 'Add CONTRIBUTING')
end
def add_ci_yml_path
@@ -149,32 +150,52 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
def files_anchor_data
AnchorData.new(true,
- _('Files (%{human_size})') % { human_size: storage_counter(statistics.total_repository_size) },
+ statistic_icon('doc-code') +
+ _('%{strong_start}%{human_size}%{strong_end} Files').html_safe % {
+ human_size: storage_counter(statistics.total_repository_size),
+ strong_start: '<strong class="project-stat-value">'.html_safe,
+ strong_end: '</strong>'.html_safe
+ },
empty_repo? ? nil : project_tree_path(project))
end
def commits_anchor_data
AnchorData.new(true,
- n_('Commit (%{commit_count})', 'Commits (%{commit_count})', statistics.commit_count) % { commit_count: number_with_delimiter(statistics.commit_count) },
+ statistic_icon('commit') +
+ n_('%{strong_start}%{commit_count}%{strong_end} Commit', '%{strong_start}%{commit_count}%{strong_end} Commits', statistics.commit_count).html_safe % {
+ commit_count: number_with_delimiter(statistics.commit_count),
+ strong_start: '<strong class="project-stat-value">'.html_safe,
+ strong_end: '</strong>'.html_safe
+ },
empty_repo? ? nil : project_commits_path(project, repository.root_ref))
end
def branches_anchor_data
AnchorData.new(true,
- n_('Branch (%{branch_count})', 'Branches (%{branch_count})', repository.branch_count) % { branch_count: number_with_delimiter(repository.branch_count) },
+ statistic_icon('branch') +
+ n_('%{strong_start}%{branch_count}%{strong_end} Branch', '%{strong_start}%{branch_count}%{strong_end} Branches', repository.branch_count).html_safe % {
+ branch_count: number_with_delimiter(repository.branch_count),
+ strong_start: '<strong class="project-stat-value">'.html_safe,
+ strong_end: '</strong>'.html_safe
+ },
empty_repo? ? nil : project_branches_path(project))
end
def tags_anchor_data
AnchorData.new(true,
- n_('Tag (%{tag_count})', 'Tags (%{tag_count})', repository.tag_count) % { tag_count: number_with_delimiter(repository.tag_count) },
+ statistic_icon('label') +
+ n_('%{strong_start}%{tag_count}%{strong_end} Tag', '%{strong_start}%{tag_count}%{strong_end} Tags', repository.tag_count).html_safe % {
+ tag_count: number_with_delimiter(repository.tag_count),
+ strong_start: '<strong class="project-stat-value">'.html_safe,
+ strong_end: '</strong>'.html_safe
+ },
empty_repo? ? nil : project_tags_path(project))
end
def new_file_anchor_data
if current_user && can_current_user_push_to_default_branch?
AnchorData.new(false,
- _('New file'),
+ statistic_icon + _('New file'),
project_new_blob_path(project, default_branch || 'master'),
'success')
end
@@ -183,40 +204,45 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
def readme_anchor_data
if current_user && can_current_user_push_to_default_branch? && repository.readme.nil?
AnchorData.new(false,
- _('Add Readme'),
+ statistic_icon + _('Add README'),
add_readme_path)
elsif repository.readme
- AnchorData.new(true,
- _('Readme'),
- default_view != 'readme' ? readme_path : '#readme')
+ AnchorData.new(false,
+ statistic_icon('doc-text') + _('README'),
+ default_view != 'readme' ? readme_path : '#readme',
+ 'default',
+ 'doc-text')
end
end
def changelog_anchor_data
if current_user && can_current_user_push_to_default_branch? && repository.changelog.blank?
AnchorData.new(false,
- _('Add Changelog'),
+ statistic_icon + _('Add CHANGELOG'),
add_changelog_path)
elsif repository.changelog.present?
- AnchorData.new(true,
- _('Changelog'),
- changelog_path)
+ AnchorData.new(false,
+ statistic_icon('doc-text') + _('CHANGELOG'),
+ changelog_path,
+ 'default')
end
end
def license_anchor_data
+ icon = statistic_icon('scale')
+
if repository.license_blob.present?
AnchorData.new(true,
- license_short_name,
+ icon + content_tag(:strong, license_short_name, class: 'project-stat-value'),
license_path)
else
if current_user && can_current_user_push_to_default_branch?
- AnchorData.new(false,
- _('Add license'),
+ AnchorData.new(true,
+ content_tag(:span, icon + _('Add license'), class: 'add-license-link d-flex'),
add_license_path)
else
- AnchorData.new(false,
- _('No license. All rights reserved'),
+ AnchorData.new(true,
+ icon + content_tag(:strong, _('No license. All rights reserved'), class: 'project-stat-value'),
nil)
end
end
@@ -225,22 +251,29 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
def contribution_guide_anchor_data
if current_user && can_current_user_push_to_default_branch? && repository.contribution_guide.blank?
AnchorData.new(false,
- _('Add Contribution guide'),
+ statistic_icon + _('Add CONTRIBUTING'),
add_contribution_guide_path)
elsif repository.contribution_guide.present?
- AnchorData.new(true,
- _('Contribution guide'),
+ AnchorData.new(false,
+ statistic_icon('doc-text') + _('CONTRIBUTING'),
contribution_guide_path)
end
end
def autodevops_anchor_data(show_auto_devops_callout: false)
if current_user && can?(current_user, :admin_pipeline, project) && repository.gitlab_ci_yml.blank? && !show_auto_devops_callout
- AnchorData.new(auto_devops_enabled?,
- auto_devops_enabled? ? _('Auto DevOps enabled') : _('Enable Auto DevOps'),
- project_settings_ci_cd_path(project, anchor: 'autodevops-settings'))
+ if auto_devops_enabled?
+ AnchorData.new(false,
+ statistic_icon('doc-text') + _('Auto DevOps enabled'),
+ project_settings_ci_cd_path(project, anchor: 'autodevops-settings'),
+ 'default')
+ else
+ AnchorData.new(false,
+ statistic_icon + _('Enable Auto DevOps'),
+ project_settings_ci_cd_path(project, anchor: 'autodevops-settings'))
+ end
elsif auto_devops_enabled?
- AnchorData.new(true,
+ AnchorData.new(false,
_('Auto DevOps enabled'),
nil)
end
@@ -248,27 +281,32 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
def kubernetes_cluster_anchor_data
if current_user && can?(current_user, :create_cluster, project)
- cluster_link = clusters.count == 1 ? project_cluster_path(project, clusters.first) : project_clusters_path(project)
if clusters.empty?
- cluster_link = new_project_cluster_path(project)
- end
+ AnchorData.new(false,
+ statistic_icon + _('Add Kubernetes cluster'),
+ new_project_cluster_path(project))
+ else
+ cluster_link = clusters.count == 1 ? project_cluster_path(project, clusters.first) : project_clusters_path(project)
- AnchorData.new(!clusters.empty?,
- clusters.empty? ? _('Add Kubernetes cluster') : _('Kubernetes configured'),
- cluster_link)
+ AnchorData.new(false,
+ _('Kubernetes configured'),
+ cluster_link,
+ 'default')
+ end
end
end
def gitlab_ci_anchor_data
if current_user && can_current_user_push_code? && repository.gitlab_ci_yml.blank? && !auto_devops_enabled?
AnchorData.new(false,
- _('Set up CI/CD'),
+ statistic_icon + _('Set up CI/CD'),
add_ci_yml_path)
elsif repository.gitlab_ci_yml.present?
- AnchorData.new(true,
- _('CI/CD configuration'),
- ci_configuration_path)
+ AnchorData.new(false,
+ statistic_icon('doc-text') + _('CI/CD configuration'),
+ ci_configuration_path,
+ 'default')
end
end
diff --git a/app/serializers/cluster_application_entity.rb b/app/serializers/cluster_application_entity.rb
index 2bd17e58086..7b1a0be75ca 100644
--- a/app/serializers/cluster_application_entity.rb
+++ b/app/serializers/cluster_application_entity.rb
@@ -6,4 +6,5 @@ class ClusterApplicationEntity < Grape::Entity
expose :status_reason
expose :external_ip, if: -> (e, _) { e.respond_to?(:external_ip) }
expose :hostname, if: -> (e, _) { e.respond_to?(:hostname) }
+ expose :email, if: -> (e, _) { e.respond_to?(:email) }
end
diff --git a/app/serializers/diff_file_base_entity.rb b/app/serializers/diff_file_base_entity.rb
new file mode 100644
index 00000000000..06a8db78476
--- /dev/null
+++ b/app/serializers/diff_file_base_entity.rb
@@ -0,0 +1,101 @@
+# frozen_string_literal: true
+
+class DiffFileBaseEntity < Grape::Entity
+ include RequestAwareEntity
+ include BlobHelper
+ include SubmoduleHelper
+ include DiffHelper
+ include TreeHelper
+ include ChecksCollaboration
+ include Gitlab::Utils::StrongMemoize
+
+ expose :content_sha
+ expose :submodule?, as: :submodule
+
+ expose :submodule_link do |diff_file|
+ memoized_submodule_links(diff_file).first
+ end
+
+ expose :submodule_tree_url do |diff_file|
+ memoized_submodule_links(diff_file).last
+ end
+
+ expose :edit_path, if: -> (_, options) { options[:merge_request] } do |diff_file|
+ merge_request = options[:merge_request]
+
+ options = merge_request.persisted? ? { from_merge_request_iid: merge_request.iid } : {}
+
+ next unless merge_request.source_project
+
+ project_edit_blob_path(merge_request.source_project,
+ tree_join(merge_request.source_branch, diff_file.new_path),
+ options)
+ end
+
+ expose :old_path_html do |diff_file|
+ old_path, _ = mark_inline_diffs(diff_file.old_path, diff_file.new_path)
+ old_path
+ end
+
+ expose :new_path_html do |diff_file|
+ _, new_path = mark_inline_diffs(diff_file.old_path, diff_file.new_path)
+ new_path
+ end
+
+ expose :formatted_external_url, if: -> (_, options) { options[:environment] } do |diff_file|
+ options[:environment].formatted_external_url
+ end
+
+ expose :external_url, if: -> (_, options) { options[:environment] } do |diff_file|
+ options[:environment].external_url_for(diff_file.new_path, diff_file.content_sha)
+ end
+
+ expose :blob, using: BlobEntity
+
+ expose :can_modify_blob do |diff_file|
+ merge_request = options[:merge_request]
+
+ next unless diff_file.blob
+
+ if merge_request&.source_project && current_user
+ can_modify_blob?(diff_file.blob, merge_request.source_project, merge_request.source_branch)
+ else
+ false
+ end
+ end
+
+ expose :file_hash do |diff_file|
+ Digest::SHA1.hexdigest(diff_file.file_path)
+ end
+
+ expose :file_path
+ expose :old_path
+ expose :new_path
+ expose :new_file?, as: :new_file
+ expose :collapsed?, as: :collapsed
+ expose :text?, as: :text
+ expose :diff_refs
+ expose :stored_externally?, as: :stored_externally
+ expose :external_storage
+ expose :renamed_file?, as: :renamed_file
+ expose :deleted_file?, as: :deleted_file
+ expose :mode_changed?, as: :mode_changed
+ expose :a_mode
+ expose :b_mode
+
+ private
+
+ def memoized_submodule_links(diff_file)
+ strong_memoize(:submodule_links) do
+ if diff_file.submodule?
+ submodule_links(diff_file.blob, diff_file.content_sha, diff_file.repository)
+ else
+ []
+ end
+ end
+ end
+
+ def current_user
+ request.current_user
+ end
+end
diff --git a/app/serializers/diff_file_entity.rb b/app/serializers/diff_file_entity.rb
index 63ea8e8f95f..f0881829efd 100644
--- a/app/serializers/diff_file_entity.rb
+++ b/app/serializers/diff_file_entity.rb
@@ -1,63 +1,12 @@
# frozen_string_literal: true
-class DiffFileEntity < Grape::Entity
- include RequestAwareEntity
+class DiffFileEntity < DiffFileBaseEntity
include CommitsHelper
- include DiffHelper
- include SubmoduleHelper
- include BlobHelper
include IconsHelper
- include TreeHelper
- include ChecksCollaboration
- include Gitlab::Utils::StrongMemoize
- expose :submodule?, as: :submodule
-
- expose :submodule_link do |diff_file|
- memoized_submodule_links(diff_file).first
- end
-
- expose :submodule_tree_url do |diff_file|
- memoized_submodule_links(diff_file).last
- end
-
- expose :blob, using: BlobEntity
-
- expose :can_modify_blob do |diff_file|
- merge_request = options[:merge_request]
-
- next unless diff_file.blob
-
- if merge_request&.source_project && current_user
- can_modify_blob?(diff_file.blob, merge_request.source_project, merge_request.source_branch)
- else
- false
- end
- end
-
- expose :file_hash do |diff_file|
- Digest::SHA1.hexdigest(diff_file.file_path)
- end
-
- expose :file_path
expose :too_large?, as: :too_large
- expose :collapsed?, as: :collapsed
- expose :new_file?, as: :new_file
-
- expose :deleted_file?, as: :deleted_file
- expose :renamed_file?, as: :renamed_file
- expose :old_path
- expose :new_path
- expose :mode_changed?, as: :mode_changed
- expose :a_mode
- expose :b_mode
- expose :text?, as: :text
expose :added_lines
expose :removed_lines
- expose :diff_refs
- expose :content_sha
- expose :stored_externally?, as: :stored_externally
- expose :external_storage
expose :load_collapsed_diff_url, if: -> (diff_file, options) { diff_file.text? && options[:merge_request] } do |diff_file|
merge_request = options[:merge_request]
@@ -75,36 +24,6 @@ class DiffFileEntity < Grape::Entity
)
end
- expose :formatted_external_url, if: -> (_, options) { options[:environment] } do |diff_file|
- options[:environment].formatted_external_url
- end
-
- expose :external_url, if: -> (_, options) { options[:environment] } do |diff_file|
- options[:environment].external_url_for(diff_file.new_path, diff_file.content_sha)
- end
-
- expose :old_path_html do |diff_file|
- old_path, _ = mark_inline_diffs(diff_file.old_path, diff_file.new_path)
- old_path
- end
-
- expose :new_path_html do |diff_file|
- _, new_path = mark_inline_diffs(diff_file.old_path, diff_file.new_path)
- new_path
- end
-
- expose :edit_path, if: -> (_, options) { options[:merge_request] } do |diff_file|
- merge_request = options[:merge_request]
-
- options = merge_request.persisted? ? { from_merge_request_iid: merge_request.iid } : {}
-
- next unless merge_request.source_project
-
- project_edit_blob_path(merge_request.source_project,
- tree_join(merge_request.source_branch, diff_file.new_path),
- options)
- end
-
expose :view_path, if: -> (_, options) { options[:merge_request] } do |diff_file|
merge_request = options[:merge_request]
@@ -145,18 +64,4 @@ class DiffFileEntity < Grape::Entity
# Used for parallel diffs
expose :parallel_diff_lines, using: DiffLineParallelEntity, if: -> (diff_file, _) { diff_file.text? }
-
- def current_user
- request.current_user
- end
-
- def memoized_submodule_links(diff_file)
- strong_memoize(:submodule_links) do
- if diff_file.submodule?
- submodule_links(diff_file.blob, diff_file.content_sha, diff_file.repository)
- else
- []
- end
- end
- end
end
diff --git a/app/serializers/discussion_diff_file_entity.rb b/app/serializers/discussion_diff_file_entity.rb
new file mode 100644
index 00000000000..419e7edf94f
--- /dev/null
+++ b/app/serializers/discussion_diff_file_entity.rb
@@ -0,0 +1,4 @@
+# frozen_string_literal: true
+
+class DiscussionDiffFileEntity < DiffFileBaseEntity
+end
diff --git a/app/serializers/discussion_entity.rb b/app/serializers/discussion_entity.rb
index b6786a0d597..b2d9d52bd22 100644
--- a/app/serializers/discussion_entity.rb
+++ b/app/serializers/discussion_entity.rb
@@ -36,7 +36,7 @@ class DiscussionEntity < Grape::Entity
new_project_issue_path(discussion.project, merge_request_to_resolve_discussions_of: discussion.noteable.iid, discussion_to_resolve: discussion.id)
end
- expose :diff_file, using: DiffFileEntity, if: -> (d, _) { d.diff_discussion? }
+ expose :diff_file, using: DiscussionDiffFileEntity, if: -> (d, _) { d.diff_discussion? }
expose :diff_discussion?, as: :diff_discussion
@@ -46,19 +46,6 @@ class DiscussionEntity < Grape::Entity
expose :truncated_diff_lines, using: DiffLineEntity, if: -> (d, _) { d.diff_discussion? && d.on_text? && (d.expanded? || render_truncated_diff_lines?) }
- expose :image_diff_html, if: -> (d, _) { d.diff_discussion? && d.on_image? } do |discussion|
- diff_file = discussion.diff_file
- partial = diff_file.new_file? || diff_file.deleted_file? ? 'single_image_diff' : 'replaced_image_diff'
- options[:context].render_to_string(
- partial: "projects/diffs/#{partial}",
- locals: { diff_file: diff_file,
- position: discussion.position.to_json,
- click_to_comment: false },
- layout: false,
- formats: [:html]
- )
- end
-
expose :for_commit?, as: :for_commit
expose :commit_id
diff --git a/app/serializers/projects/serverless/service_entity.rb b/app/serializers/projects/serverless/service_entity.rb
new file mode 100644
index 00000000000..4f1f62d145b
--- /dev/null
+++ b/app/serializers/projects/serverless/service_entity.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+module Projects
+ module Serverless
+ class ServiceEntity < Grape::Entity
+ include RequestAwareEntity
+
+ expose :name do |service|
+ service.dig('metadata', 'name')
+ end
+
+ expose :namespace do |service|
+ service.dig('metadata', 'namespace')
+ end
+
+ expose :created_at do |service|
+ service.dig('metadata', 'creationTimestamp')
+ end
+
+ expose :url do |service|
+ "http://#{service.dig('status', 'domain')}"
+ end
+
+ expose :description do |service|
+ service.dig('spec', 'runLatest', 'configuration', 'revisionTemplate', 'metadata', 'annotations', 'Description')
+ end
+
+ expose :image do |service|
+ service.dig('spec', 'runLatest', 'configuration', 'build', 'template', 'name')
+ end
+ end
+ end
+end
diff --git a/app/serializers/projects/serverless/service_serializer.rb b/app/serializers/projects/serverless/service_serializer.rb
new file mode 100644
index 00000000000..adfd48a8c7d
--- /dev/null
+++ b/app/serializers/projects/serverless/service_serializer.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module Projects
+ module Serverless
+ class ServiceSerializer < BaseSerializer
+ entity Projects::Serverless::ServiceEntity
+ end
+ end
+end
diff --git a/app/serializers/trigger_variable_entity.rb b/app/serializers/trigger_variable_entity.rb
index 56203113631..4b28db42e76 100644
--- a/app/serializers/trigger_variable_entity.rb
+++ b/app/serializers/trigger_variable_entity.rb
@@ -3,5 +3,6 @@
class TriggerVariableEntity < Grape::Entity
include RequestAwareEntity
- expose :key, :value, :public
+ expose :key, :public
+ expose :value, if: ->(_, _) { can?(request.current_user, :admin_build, request.project) }
end
diff --git a/app/services/ci/compare_reports_base_service.rb b/app/services/ci/compare_reports_base_service.rb
new file mode 100644
index 00000000000..d5625857599
--- /dev/null
+++ b/app/services/ci/compare_reports_base_service.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+module Ci
+ class CompareReportsBaseService < ::BaseService
+ def execute(base_pipeline, head_pipeline)
+ comparer = comparer_class.new(get_report(base_pipeline), get_report(head_pipeline))
+ {
+ status: :parsed,
+ key: key(base_pipeline, head_pipeline),
+ data: serializer_class
+ .new(project: project)
+ .represent(comparer).as_json
+ }
+ rescue Gitlab::Ci::Parsers::ParserError => e
+ {
+ status: :error,
+ key: key(base_pipeline, head_pipeline),
+ status_reason: e.message
+ }
+ end
+
+ def latest?(base_pipeline, head_pipeline, data)
+ data&.fetch(:key, nil) == key(base_pipeline, head_pipeline)
+ end
+
+ private
+
+ def key(base_pipeline, head_pipeline)
+ [
+ base_pipeline&.id, base_pipeline&.updated_at,
+ head_pipeline&.id, head_pipeline&.updated_at
+ ]
+ end
+
+ def comparer_class
+ raise NotImplementedError
+ end
+
+ def serializer_class
+ raise NotImplementedError
+ end
+
+ def get_report(pipeline)
+ raise NotImplementedError
+ end
+ end
+end
diff --git a/app/services/ci/compare_test_reports_service.rb b/app/services/ci/compare_test_reports_service.rb
index 2293f95f56b..382d5b8995f 100644
--- a/app/services/ci/compare_test_reports_service.rb
+++ b/app/services/ci/compare_test_reports_service.rb
@@ -1,39 +1,17 @@
# frozen_string_literal: true
module Ci
- class CompareTestReportsService < ::BaseService
- def execute(base_pipeline, head_pipeline)
- # rubocop: disable CodeReuse/Serializer
- comparer = Gitlab::Ci::Reports::TestReportsComparer
- .new(base_pipeline&.test_reports, head_pipeline.test_reports)
-
- {
- status: :parsed,
- key: key(base_pipeline, head_pipeline),
- data: TestReportsComparerSerializer
- .new(project: project)
- .represent(comparer).as_json
- }
- rescue => e
- {
- status: :error,
- key: key(base_pipeline, head_pipeline),
- status_reason: e.message
- }
- # rubocop: enable CodeReuse/Serializer
+ class CompareTestReportsService < CompareReportsBaseService
+ def comparer_class
+ Gitlab::Ci::Reports::TestReportsComparer
end
- def latest?(base_pipeline, head_pipeline, data)
- data&.fetch(:key, nil) == key(base_pipeline, head_pipeline)
+ def serializer_class
+ TestReportsComparerSerializer
end
- private
-
- def key(base_pipeline, head_pipeline)
- [
- base_pipeline&.id, base_pipeline&.updated_at,
- head_pipeline&.id, head_pipeline&.updated_at
- ]
+ def get_report(pipeline)
+ pipeline&.test_reports
end
end
end
diff --git a/app/services/clusters/applications/create_service.rb b/app/services/clusters/applications/create_service.rb
index a89772e82dc..92c2c1b9834 100644
--- a/app/services/clusters/applications/create_service.rb
+++ b/app/services/clusters/applications/create_service.rb
@@ -20,7 +20,7 @@ module Clusters
end
if application.has_attribute?(:email)
- application.email = current_user.email
+ application.email = params[:email]
end
if application.respond_to?(:oauth_application)
diff --git a/app/services/clusters/build_service.rb b/app/services/clusters/build_service.rb
new file mode 100644
index 00000000000..8de73831164
--- /dev/null
+++ b/app/services/clusters/build_service.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+module Clusters
+ class BuildService
+ def initialize(subject)
+ @subject = subject
+ end
+
+ def execute
+ ::Clusters::Cluster.new.tap do |cluster|
+ case @subject
+ when ::Project
+ cluster.cluster_type = :project_type
+ when ::Group
+ cluster.cluster_type = :group_type
+ else
+ raise NotImplementedError
+ end
+ end
+ end
+ end
+end
diff --git a/app/services/clusters/gcp/fetch_operation_service.rb b/app/services/clusters/gcp/fetch_operation_service.rb
index 02c96a1e286..6c648b443a0 100644
--- a/app/services/clusters/gcp/fetch_operation_service.rb
+++ b/app/services/clusters/gcp/fetch_operation_service.rb
@@ -11,8 +11,21 @@ module Clusters
yield(operation) if block_given?
rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e
+ logger.error(
+ exception: e.class.name,
+ service: self.class.name,
+ provider_id: provider.id,
+ message: e.message
+ )
+
provider.make_errored!("Failed to request to CloudPlatform; #{e.message}")
end
+
+ private
+
+ def logger
+ @logger ||= Gitlab::Kubernetes::Logger.build
+ end
end
end
end
diff --git a/app/services/clusters/gcp/finalize_creation_service.rb b/app/services/clusters/gcp/finalize_creation_service.rb
index e029323774c..301059f0326 100644
--- a/app/services/clusters/gcp/finalize_creation_service.rb
+++ b/app/services/clusters/gcp/finalize_creation_service.rb
@@ -16,10 +16,13 @@ module Clusters
ClusterPlatformConfigureWorker.perform_async(cluster.id)
rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e
+ log_service_error(e.class.name, provider.id, e.message)
provider.make_errored!("Failed to request to CloudPlatform; #{e.message}")
rescue Kubeclient::HttpError => e
+ log_service_error(e.class.name, provider.id, e.message)
provider.make_errored!("Failed to run Kubeclient: #{e.message}")
rescue ActiveRecord::RecordInvalid => e
+ log_service_error(e.class.name, provider.id, e.message)
provider.make_errored!("Failed to configure Google Kubernetes Engine Cluster: #{e.message}")
end
@@ -105,6 +108,19 @@ module Clusters
def cluster
@cluster ||= provider.cluster
end
+
+ def logger
+ @logger ||= Gitlab::Kubernetes::Logger.build
+ end
+
+ def log_service_error(exception, provider_id, message)
+ logger.error(
+ exception: exception.class.name,
+ service: self.class.name,
+ provider_id: provider_id,
+ message: message
+ )
+ end
end
end
end
diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb
index 667b5916f38..f712b8863cd 100644
--- a/app/services/merge_requests/refresh_service.rb
+++ b/app/services/merge_requests/refresh_service.rb
@@ -58,13 +58,27 @@ module MergeRequests
.preload(:latest_merge_request_diff)
.where(target_branch: @push.branch_name).to_a
.select(&:diff_head_commit)
+ .select do |merge_request|
+ commit_ids.include?(merge_request.diff_head_sha) &&
+ merge_request.merge_request_diff.state != 'empty'
+ end
+ merge_requests = filter_merge_requests(merge_requests)
+
+ return if merge_requests.empty?
- merge_requests = merge_requests.select do |merge_request|
- commit_ids.include?(merge_request.diff_head_sha) &&
- merge_request.merge_request_diff.state != 'empty'
+ commit_analyze_enabled = Feature.enabled?(:branch_push_merge_commit_analyze, @project, default_enabled: true)
+ if commit_analyze_enabled
+ analyzer = Gitlab::BranchPushMergeCommitAnalyzer.new(
+ @commits.reverse,
+ relevant_commit_ids: merge_requests.map(&:diff_head_sha)
+ )
end
- filter_merge_requests(merge_requests).each do |merge_request|
+ merge_requests.each do |merge_request|
+ if commit_analyze_enabled
+ merge_request.merge_commit_sha = analyzer.get_merge_commit(merge_request.diff_head_sha)
+ end
+
MergeRequests::PostMergeService
.new(merge_request.target_project, @current_user)
.execute(merge_request)
diff --git a/app/services/notification_recipient_service.rb b/app/services/notification_recipient_service.rb
index 9c236d7f41d..68cdc69023a 100644
--- a/app/services/notification_recipient_service.rb
+++ b/app/services/notification_recipient_service.rb
@@ -24,6 +24,10 @@ module NotificationRecipientService
Builder::MergeRequestUnmergeable.new(*args).notification_recipients
end
+ def self.build_project_maintainers_recipients(*args)
+ Builder::ProjectMaintainers.new(*args).notification_recipients
+ end
+
module Builder
class Base
def initialize(*)
@@ -380,5 +384,24 @@ module NotificationRecipientService
nil
end
end
+
+ class ProjectMaintainers < Base
+ attr_reader :target
+
+ def initialize(target, action:)
+ @target = target
+ @action = action
+ end
+
+ def build!
+ return [] unless project
+
+ add_recipients(project.team.maintainers, :watch, nil)
+ end
+
+ def acting_user
+ nil
+ end
+ end
end
end
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index 5904bfbf88d..ff035fea216 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -429,26 +429,26 @@ class NotificationService
end
def pages_domain_verification_succeeded(domain)
- recipients_for_pages_domain(domain).each do |user|
- mailer.pages_domain_verification_succeeded_email(domain, user).deliver_later
+ project_maintainers_recipients(domain, action: 'succeeded').each do |recipient|
+ mailer.pages_domain_verification_succeeded_email(domain, recipient.user).deliver_later
end
end
def pages_domain_verification_failed(domain)
- recipients_for_pages_domain(domain).each do |user|
- mailer.pages_domain_verification_failed_email(domain, user).deliver_later
+ project_maintainers_recipients(domain, action: 'failed').each do |recipient|
+ mailer.pages_domain_verification_failed_email(domain, recipient.user).deliver_later
end
end
def pages_domain_enabled(domain)
- recipients_for_pages_domain(domain).each do |user|
- mailer.pages_domain_enabled_email(domain, user).deliver_later
+ project_maintainers_recipients(domain, action: 'enabled').each do |recipient|
+ mailer.pages_domain_enabled_email(domain, recipient.user).deliver_later
end
end
def pages_domain_disabled(domain)
- recipients_for_pages_domain(domain).each do |user|
- mailer.pages_domain_disabled_email(domain, user).deliver_later
+ project_maintainers_recipients(domain, action: 'disabled').each do |recipient|
+ mailer.pages_domain_disabled_email(domain, recipient.user).deliver_later
end
end
@@ -466,6 +466,22 @@ class NotificationService
end
end
+ def repository_cleanup_success(project, user)
+ mailer.send(:repository_cleanup_success_email, project, user).deliver_later
+ end
+
+ def repository_cleanup_failure(project, user, error)
+ mailer.send(:repository_cleanup_failure_email, project, user, error).deliver_later
+ end
+
+ def remote_mirror_update_failed(remote_mirror)
+ recipients = project_maintainers_recipients(remote_mirror, action: 'update_failed')
+
+ recipients.each do |recipient|
+ mailer.remote_mirror_update_failed_email(remote_mirror.id, recipient.user.id).deliver_later
+ end
+ end
+
protected
def new_resource_email(target, method)
@@ -561,12 +577,8 @@ class NotificationService
private
- def recipients_for_pages_domain(domain)
- project = domain.project
-
- return [] unless project
-
- notifiable_users(project.team.maintainers, :watch, target: project)
+ def project_maintainers_recipients(target, action:)
+ NotificationRecipientService.build_project_maintainers_recipients(target, action: action)
end
def notifiable?(*args)
diff --git a/app/services/projects/cleanup_service.rb b/app/services/projects/cleanup_service.rb
new file mode 100644
index 00000000000..12103ea34b5
--- /dev/null
+++ b/app/services/projects/cleanup_service.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+module Projects
+ # The CleanupService removes data from the project repository following a
+ # BFG rewrite: https://rtyley.github.io/bfg-repo-cleaner/
+ #
+ # Before executing this service, all refs rewritten by BFG should have been
+ # pushed to the repository
+ class CleanupService < BaseService
+ NoUploadError = StandardError.new("Couldn't find uploaded object map")
+
+ include Gitlab::Utils::StrongMemoize
+
+ # Attempt to clean up the project following the push. Warning: this is
+ # destructive!
+ #
+ # path is the path of an upload of a BFG object map file. It contains a line
+ # per rewritten object, with the old and new SHAs space-separated. It can be
+ # used to update or remove content that references the objects that BFG has
+ # altered
+ #
+ # Currently, only the project repository is modified by this service, but we
+ # may wish to modify other data sources in the future.
+ def execute
+ apply_bfg_object_map!
+
+ # Remove older objects that are no longer referenced
+ GitGarbageCollectWorker.new.perform(project.id, :gc)
+
+ # The cache may now be inaccurate, and holding onto it could prevent
+ # bugs assuming the presence of some object from manifesting for some
+ # time. Better to feel the pain immediately.
+ project.repository.expire_all_method_caches
+
+ project.bfg_object_map.remove!
+ end
+
+ private
+
+ def apply_bfg_object_map!
+ raise NoUploadError unless project.bfg_object_map.exists?
+
+ project.bfg_object_map.open do |io|
+ repository_cleaner.apply_bfg_object_map(io)
+ end
+ end
+
+ def repository_cleaner
+ @repository_cleaner ||= Gitlab::Git::RepositoryCleaner.new(repository.raw)
+ end
+ end
+end
diff --git a/app/services/projects/fork_service.rb b/app/services/projects/fork_service.rb
index 8dc0e044875..91091c4393d 100644
--- a/app/services/projects/fork_service.rb
+++ b/app/services/projects/fork_service.rb
@@ -54,6 +54,8 @@ module Projects
new_params[:avatar] = @project.avatar
end
+ new_params.merge!(@project.object_pool_params)
+
new_project = CreateService.new(current_user, new_params).execute
return new_project unless new_project.persisted?
diff --git a/app/validators/url_validator.rb b/app/validators/url_validator.rb
index 216acf79cbd..5feb0b0f05b 100644
--- a/app/validators/url_validator.rb
+++ b/app/validators/url_validator.rb
@@ -69,6 +69,7 @@ class UrlValidator < ActiveModel::EachValidator
ports: [],
allow_localhost: true,
allow_local_network: true,
+ ascii_only: false,
enforce_user: false
}
end
diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml
index 7ac79cc77f5..6756299cf43 100644
--- a/app/views/admin/dashboard/index.html.haml
+++ b/app/views/admin/dashboard/index.html.haml
@@ -174,7 +174,7 @@
%h4 Latest projects
- @projects.each do |project|
%p
- = link_to project.full_name, [:admin, project.namespace.becomes(Namespace), project], class: 'str-truncated-60'
+ = link_to project.full_name, admin_project_path(project), class: 'str-truncated-60'
%span.light.float-right
#{time_ago_with_tooltip(project.created_at)}
.col-md-4
diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml
index 5f205d1bcbc..da2ebb08405 100644
--- a/app/views/admin/groups/show.html.haml
+++ b/app/views/admin/groups/show.html.haml
@@ -101,7 +101,7 @@
= _('Add user(s) to the group:')
.card-body.form-holder
%p.light
- - link_to_help = link_to(_("here"), help_page_path("user/permissions"), class: "vlink")
+ - link_to_help = link_to(_("here"), help_page_path("user/permissions"))
= _('Read more about project permissions <strong>%{link_to_help}</strong>').html_safe % { link_to_help: link_to_help }
= form_tag admin_group_members_update_path(@group), id: "new_project_member", class: "bulk_import", method: :put do
diff --git a/app/views/admin/hooks/edit.html.haml b/app/views/admin/hooks/edit.html.haml
index 486d0477f20..9c6c74ed965 100644
--- a/app/views/admin/hooks/edit.html.haml
+++ b/app/views/admin/hooks/edit.html.haml
@@ -4,7 +4,7 @@
Edit System Hook
%p.light
- #{link_to 'System hooks ', help_page_path('system_hooks/system_hooks'), class: 'vlink'} can be
+ #{link_to 'System hooks ', help_page_path('system_hooks/system_hooks')} can be
used for binding events when GitLab creates a User or Project.
%hr
diff --git a/app/views/admin/hooks/index.html.haml b/app/views/admin/hooks/index.html.haml
index 5d462d7b732..b65bf07160a 100644
--- a/app/views/admin/hooks/index.html.haml
+++ b/app/views/admin/hooks/index.html.haml
@@ -4,7 +4,7 @@
%h4.prepend-top-0
= page_title
%p
- #{link_to 'System hooks ', help_page_path('system_hooks/system_hooks'), class: 'vlink'} can be
+ #{link_to 'System hooks ', help_page_path('system_hooks/system_hooks')} can be
used for binding events when GitLab creates a User or Project.
.col-lg-8.append-bottom-default
diff --git a/app/views/clusters/clusters/_gcp_signup_offer_banner.html.haml b/app/views/clusters/clusters/_gcp_signup_offer_banner.html.haml
index 85d1002243b..73b11d509d3 100644
--- a/app/views/clusters/clusters/_gcp_signup_offer_banner.html.haml
+++ b/app/views/clusters/clusters/_gcp_signup_offer_banner.html.haml
@@ -1,6 +1,6 @@
- link = link_to(s_('ClusterIntegration|sign up'), 'https://console.cloud.google.com/freetrial?utm_campaign=2018_cpanel&utm_source=gitlab&utm_medium=referral', target: '_blank', rel: 'noopener noreferrer')
-.bs-callout.gcp-signup-offer.alert.alert-block.alert-dismissable.prepend-top-default.append-bottom-default{ role: 'alert', data: { feature_id: UserCalloutsHelper::GCP_SIGNUP_OFFER, dismiss_endpoint: user_callouts_path } }
- %button.close.js-close{ type: "button" } &times;
+.bs-callout.gcp-signup-offer.alert.alert-block.alert-dismissable.prepend-top-default.append-bottom-default{ role: 'alert' }
+ %button.close{ type: "button", data: { feature_id: UserCalloutsHelper::GCP_SIGNUP_OFFER, dismiss_endpoint: user_callouts_path } } &times;
.gcp-signup-offer--content
.gcp-signup-offer--icon.append-right-8
= sprite_icon("information", size: 16)
diff --git a/app/views/dashboard/activity.html.haml b/app/views/dashboard/activity.html.haml
index 4dbda5c754b..31d4b3da4f1 100644
--- a/app/views/dashboard/activity.html.haml
+++ b/app/views/dashboard/activity.html.haml
@@ -4,9 +4,6 @@
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, dashboard_projects_url(rss_url_options), title: "All activity")
-
-= render_if_exists "shared/gold_trial_callout"
-
- page_title "Activity"
- header_title "Activity", activity_dashboard_path
diff --git a/app/views/dashboard/groups/index.html.haml b/app/views/dashboard/groups/index.html.haml
index 2f7add600e4..50f39f93283 100644
--- a/app/views/dashboard/groups/index.html.haml
+++ b/app/views/dashboard/groups/index.html.haml
@@ -1,8 +1,6 @@
- @hide_top_links = true
- page_title "Groups"
- header_title "Groups", dashboard_groups_path
-
-= render_if_exists "shared/gold_trial_callout"
= render 'dashboard/groups_head'
- if params[:filter].blank? && @groups.empty?
diff --git a/app/views/dashboard/issues.html.haml b/app/views/dashboard/issues.html.haml
index afd46412fab..fdd5c19d562 100644
--- a/app/views/dashboard/issues.html.haml
+++ b/app/views/dashboard/issues.html.haml
@@ -4,8 +4,6 @@
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, safe_params.merge(rss_url_options).to_h, title: "#{current_user.name} issues")
-= render_if_exists "shared/gold_trial_callout"
-
.page-title-holder
%h1.page-title= _('Issues')
diff --git a/app/views/dashboard/merge_requests.html.haml b/app/views/dashboard/merge_requests.html.haml
index 3e5f13b92e3..77cfa1271df 100644
--- a/app/views/dashboard/merge_requests.html.haml
+++ b/app/views/dashboard/merge_requests.html.haml
@@ -2,8 +2,6 @@
- page_title _("Merge Requests")
- @breadcrumb_link = merge_requests_dashboard_path(assignee_username: current_user.username)
-= render_if_exists "shared/gold_trial_callout"
-
.page-title-holder
%h1.page-title= _('Merge Requests')
diff --git a/app/views/dashboard/projects/index.html.haml b/app/views/dashboard/projects/index.html.haml
index 446b4715b2d..deed774a4a5 100644
--- a/app/views/dashboard/projects/index.html.haml
+++ b/app/views/dashboard/projects/index.html.haml
@@ -4,8 +4,6 @@
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, dashboard_projects_url(rss_url_options), title: "All activity")
-= render_if_exists "shared/gold_trial_callout"
-
- page_title "Projects"
- header_title "Projects", dashboard_projects_path
diff --git a/app/views/dashboard/projects/starred.html.haml b/app/views/dashboard/projects/starred.html.haml
index ad08409c8fe..8933d9e31ff 100644
--- a/app/views/dashboard/projects/starred.html.haml
+++ b/app/views/dashboard/projects/starred.html.haml
@@ -4,8 +4,6 @@
- page_title "Starred Projects"
- header_title "Projects", dashboard_projects_path
-= render_if_exists "shared/gold_trial_callout"
-
%div{ class: container_class }
= render "projects/last_push"
= render 'dashboard/projects_head'
diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml
index 47729321961..d2593179f17 100644
--- a/app/views/dashboard/todos/index.html.haml
+++ b/app/views/dashboard/todos/index.html.haml
@@ -2,8 +2,6 @@
- page_title "Todos"
- header_title "Todos", dashboard_todos_path
-= render_if_exists "shared/gold_trial_callout"
-
.page-title-holder
%h1.page-title= _('Todos')
diff --git a/app/views/errors/access_denied.html.haml b/app/views/errors/access_denied.html.haml
index 8ae29b9d337..46931b5932d 100644
--- a/app/views/errors/access_denied.html.haml
+++ b/app/views/errors/access_denied.html.haml
@@ -9,7 +9,7 @@
%p
= message
%p
- = s_('403|Please contact your GitLab administrator to get the permission.')
+ = s_('403|Please contact your GitLab administrator to get permission.')
.action-container.js-go-back{ style: 'display: none' }
%a{ href: 'javascript:history.back()', class: 'btn btn-success' }
= s_('Go Back')
diff --git a/app/views/explore/groups/index.html.haml b/app/views/explore/groups/index.html.haml
index 869be4e8581..a3eafc61d0a 100644
--- a/app/views/explore/groups/index.html.haml
+++ b/app/views/explore/groups/index.html.haml
@@ -2,8 +2,6 @@
- page_title _("Groups")
- header_title _("Groups"), dashboard_groups_path
-= render_if_exists "shared/gold_trial_callout"
-
- if current_user
= render 'dashboard/groups_head'
- else
diff --git a/app/views/explore/projects/index.html.haml b/app/views/explore/projects/index.html.haml
index d18dec7bd8e..452f390695c 100644
--- a/app/views/explore/projects/index.html.haml
+++ b/app/views/explore/projects/index.html.haml
@@ -2,8 +2,6 @@
- page_title _("Projects")
- header_title _("Projects"), dashboard_projects_path
-= render_if_exists "shared/gold_trial_callout"
-
- if current_user
= render 'dashboard/projects_head'
- else
diff --git a/app/views/explore/projects/starred.html.haml b/app/views/explore/projects/starred.html.haml
index d18dec7bd8e..452f390695c 100644
--- a/app/views/explore/projects/starred.html.haml
+++ b/app/views/explore/projects/starred.html.haml
@@ -2,8 +2,6 @@
- page_title _("Projects")
- header_title _("Projects"), dashboard_projects_path
-= render_if_exists "shared/gold_trial_callout"
-
- if current_user
= render 'dashboard/projects_head'
- else
diff --git a/app/views/explore/projects/trending.html.haml b/app/views/explore/projects/trending.html.haml
index d18dec7bd8e..452f390695c 100644
--- a/app/views/explore/projects/trending.html.haml
+++ b/app/views/explore/projects/trending.html.haml
@@ -2,8 +2,6 @@
- page_title _("Projects")
- header_title _("Projects"), dashboard_projects_path
-= render_if_exists "shared/gold_trial_callout"
-
- if current_user
= render 'dashboard/projects_head'
- else
diff --git a/app/views/groups/_home_panel.html.haml b/app/views/groups/_home_panel.html.haml
index a0760c2073b..6219da2c715 100644
--- a/app/views/groups/_home_panel.html.haml
+++ b/app/views/groups/_home_panel.html.haml
@@ -1,4 +1,4 @@
-.group-home-panel.text-center
+.group-home-panel.text-center.border-bottom
%div{ class: container_class }
.avatar-container.s70.group-avatar
= group_icon(@group, class: "avatar s70 avatar-tile")
diff --git a/app/views/groups/group_members/_new_group_member.html.haml b/app/views/groups/group_members/_new_group_member.html.haml
index 04683ec5a9a..c8cdc2cc3e4 100644
--- a/app/views/groups/group_members/_new_group_member.html.haml
+++ b/app/views/groups/group_members/_new_group_member.html.haml
@@ -8,7 +8,7 @@
.col-md-3.col-lg-2
= select_tag :access_level, options_for_select(GroupMember.access_level_roles, @group_member.access_level), class: "form-control project-access-select"
.form-text.text-muted.append-bottom-10
- = link_to "Read more", help_page_path("user/permissions"), class: "vlink"
+ = link_to "Read more", help_page_path("user/permissions")
about role permissions
.col-md-3.col-lg-2
diff --git a/app/views/ide/_show.html.haml b/app/views/ide/_show.html.haml
new file mode 100644
index 00000000000..b24d6e27536
--- /dev/null
+++ b/app/views/ide/_show.html.haml
@@ -0,0 +1,10 @@
+- @body_class = 'ide-layout'
+- page_title 'IDE'
+
+- content_for :page_specific_javascripts do
+ = stylesheet_link_tag 'page_bundles/ide'
+
+#ide.ide-loading{ data: ide_data() }
+ .text-center
+ = icon('spinner spin 2x')
+ %h2.clgray= _('Loading the GitLab IDE...')
diff --git a/app/views/ide/index.html.haml b/app/views/ide/index.html.haml
index d8bd37fe986..0323f9d093d 100644
--- a/app/views/ide/index.html.haml
+++ b/app/views/ide/index.html.haml
@@ -1,17 +1 @@
-- @body_class = 'ide-layout'
-- page_title 'IDE'
-
-- content_for :page_specific_javascripts do
- = stylesheet_link_tag 'page_bundles/ide'
-
-#ide.ide-loading{ data: {"empty-state-svg-path" => image_path('illustrations/multi_file_editor_empty.svg'),
- "no-changes-state-svg-path" => image_path('illustrations/multi-editor_no_changes_empty.svg'),
- "committed-state-svg-path" => image_path('illustrations/multi-editor_all_changes_committed_empty.svg'),
- "pipelines-empty-state-svg-path": image_path('illustrations/pipelines_empty.svg'),
- "promotion-svg-path": image_path('illustrations/web-ide_promotion.svg'),
- "ci-help-page-path" => help_page_path('ci/quick_start/README'),
- "web-ide-help-page-path" => help_page_path('user/project/web_ide/index.html'),
- "clientside-preview-enabled": Gitlab::CurrentSettings.current_application_settings.web_ide_clientside_preview_enabled.to_s } }
- .text-center
- = icon('spinner spin 2x')
- %h2.clgray= _('Loading the GitLab IDE...')
+= render 'ide/show'
diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml
index ac5916d129c..08a6359f777 100644
--- a/app/views/layouts/_head.html.haml
+++ b/app/views/layouts/_head.html.haml
@@ -36,6 +36,7 @@
= stylesheet_link_tag "print", media: "print"
= stylesheet_link_tag "test", media: "all" if Rails.env.test?
= stylesheet_link_tag 'performance_bar' if performance_bar_enabled?
+ = stylesheet_link_tag 'csslab' if Feature.enabled?(:csslab)
= Gon::Base.render_data
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index b7d69539eb7..e8d0d809181 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -15,7 +15,7 @@
= brand_header_logo
- logo_text = brand_header_logo_type
- if logo_text.present?
- %span.logo-text.d-none.d-sm-block
+ %span.logo-text.d-none.d-lg-block.prepend-left-8
= logo_text
- if current_user
diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml
index ea5f2b166b4..7057a5a142f 100644
--- a/app/views/layouts/nav/_dashboard.html.haml
+++ b/app/views/layouts/nav/_dashboard.html.haml
@@ -1,3 +1,5 @@
+-# WAIT! Before adding more items to the nav bar, please see
+-# https://gitlab.com/gitlab-org/gitlab-ce/issues/49713 for more information.
%ul.list-unstyled.navbar-sub-nav
- if dashboard_nav_link?(:projects)
= nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: { id: 'nav-projects-dropdown', class: "home dropdown header-projects qa-projects-dropdown" }) do
@@ -16,22 +18,22 @@
= render "layouts/nav/groups_dropdown/show"
- if dashboard_nav_link?(:activity)
- = nav_link(path: 'dashboard#activity', html_options: { class: "d-none d-lg-block d-xl-block" }) do
+ = nav_link(path: 'dashboard#activity', html_options: { class: ["d-none d-xl-block", ("d-lg-block" unless has_extra_nav_icons?)] }) do
= link_to activity_dashboard_path, class: 'dashboard-shortcuts-activity', title: _('Activity') do
= _('Activity')
- if dashboard_nav_link?(:milestones)
- = nav_link(controller: 'dashboard/milestones', html_options: { class: "d-none d-lg-block d-xl-block" }) do
+ = nav_link(controller: 'dashboard/milestones', html_options: { class: ["d-none d-xl-block", ("d-lg-block" unless has_extra_nav_icons?)] }) do
= link_to dashboard_milestones_path, class: 'dashboard-shortcuts-milestones', title: _('Milestones') do
= _('Milestones')
- if dashboard_nav_link?(:snippets)
- = nav_link(controller: 'dashboard/snippets', html_options: { class: "d-none d-lg-block d-xl-block" }) do
+ = nav_link(controller: 'dashboard/snippets', html_options: { class: ["d-none d-xl-block", ("d-lg-block" unless has_extra_nav_icons?)] }) do
= link_to dashboard_snippets_path, class: 'dashboard-shortcuts-snippets', title: _('Snippets') do
= _('Snippets')
- if any_dashboard_nav_link?([:groups, :milestones, :activity, :snippets])
- %li.header-more.dropdown.d-lg-none.d-xl-none
+ %li.header-more.dropdown.d-xl-none{ class: ('d-lg-none' unless has_extra_nav_icons?) }
%a{ href: "#", data: { toggle: "dropdown" } }
= _('More')
= sprite_icon('angle-down', css_class: 'caret-down')
@@ -52,6 +54,21 @@
= link_to dashboard_snippets_path, class: 'dashboard-shortcuts-snippets', title: _('Snippets') do
= _('Snippets')
+ = render_if_exists 'dashboard/operations/nav_link'
+ - if can?(current_user, :read_instance_statistics)
+ = nav_link(controller: [:conversational_development_index, :cohorts]) do
+ = link_to instance_statistics_root_path, title: _('Instance Statistics'), aria: { label: _('Instance Statistics') }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
+ = _('Instance Statistics')
+ - if current_user.admin?
+ = nav_link(controller: 'admin/dashboard') do
+ = link_to admin_root_path, class: 'admin-icon qa-admin-area-link', title: _('Admin Area'), aria: { label: _('Admin Area') }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
+ = _('Admin Area')
+ - if Gitlab::Sherlock.enabled?
+ %li
+ = link_to sherlock_transactions_path, class: 'admin-icon', title: _('Sherlock Transactions'),
+ data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
+ = _('Sherlock Transactions')
+
-# Shortcut to Dashboard > Projects
- if dashboard_nav_link?(:projects)
%li.hidden
@@ -64,19 +81,17 @@
= link_to '#', class: 'dashboard-shortcuts-web-ide', title: _('Web IDE') do
= _('Web IDE')
- - if show_separator?
- %li.line-separator.d-none.d-sm-block
= render_if_exists 'dashboard/operations/nav_link'
- if can?(current_user, :read_instance_statistics)
- = nav_link(controller: [:conversational_development_index, :cohorts]) do
+ = nav_link(controller: [:conversational_development_index, :cohorts], html_options: { class: "d-none d-lg-block d-xl-block"}) do
= link_to instance_statistics_root_path, title: _('Instance Statistics'), aria: { label: _('Instance Statistics') }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= sprite_icon('chart', size: 18)
- if current_user.admin?
- = nav_link(controller: 'admin/dashboard') do
- = link_to admin_root_path, class: 'admin-icon qa-admin-area-link', title: _('Admin area'), aria: { label: _('Admin area') }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
+ = nav_link(controller: 'admin/dashboard', html_options: { class: "d-none d-lg-block d-xl-block"}) do
+ = link_to admin_root_path, class: 'admin-icon qa-admin-area-link', title: _('Admin Area'), aria: { label: _('Admin Area') }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= sprite_icon('admin', size: 18)
- if Gitlab::Sherlock.enabled?
%li
- = link_to sherlock_transactions_path, class: 'admin-icon', title: _('Sherlock Transactions'),
+ = link_to sherlock_transactions_path, class: 'admin-icon d-none d-lg-block d-xl-block', title: _('Sherlock Transactions'),
data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= icon('tachometer fw')
diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml
index ab15889a465..bdd0108db0d 100644
--- a/app/views/layouts/nav/sidebar/_project.html.haml
+++ b/app/views/layouts/nav/sidebar/_project.html.haml
@@ -196,7 +196,7 @@
- if project_nav_tab? :operations
= nav_link(controller: sidebar_operations_paths) do
- = link_to metrics_project_environments_path(@project), class: 'shortcuts-operations' do
+ = link_to sidebar_operations_link_path, class: 'shortcuts-operations' do
.nav-icon-container
= sprite_icon('cloud-gear')
%span.nav-item-name
@@ -204,7 +204,7 @@
%ul.sidebar-sub-level-items
= nav_link(controller: sidebar_operations_paths, html_options: { class: "fly-out-top-item" } ) do
- = link_to metrics_project_environments_path(@project) do
+ = link_to sidebar_operations_link_path do
%strong.fly-out-top-item-name
= _('Operations')
%li.divider.fly-out-top-item
@@ -222,6 +222,12 @@
%span
= _('Environments')
+ - if project_nav_tab? :serverless
+ = nav_link(controller: :functions) do
+ = link_to project_serverless_functions_path(@project), title: _('Serverless') do
+ %span
+ = _('Serverless')
+
- if project_nav_tab? :clusters
- show_cluster_hint = show_gke_cluster_integration_callout?(@project)
= nav_link(controller: [:clusters, :user, :gcp]) do
diff --git a/app/views/notify/_note_email.html.haml b/app/views/notify/_note_email.html.haml
index 94bd6f96dbc..1fbae2f64ed 100644
--- a/app/views/notify/_note_email.html.haml
+++ b/app/views/notify/_note_email.html.haml
@@ -1,13 +1,18 @@
-- discussion = @note.discussion if @note.part_of_discussion?
+- note = local_assigns.fetch(:note, @note)
+- diff_limit = local_assigns.fetch(:diff_limit, nil)
+- target_url = local_assigns.fetch(:target_url, @target_url)
+- note_style = local_assigns.fetch(:note_style, "")
+
+- discussion = note.discussion if note.part_of_discussion?
- diff_discussion = discussion&.diff_discussion?
- on_image = discussion.on_image? if diff_discussion
- if discussion
- phrase_end_char = on_image ? "." : ":"
- %p.details
+ %p{ style: "color: #777777;" }
= succeed phrase_end_char do
- = link_to @note.author_name, user_url(@note.author)
+ = link_to note.author_name, user_url(note.author)
- if diff_discussion
- if discussion.new_discussion?
@@ -15,16 +20,16 @@
- else
commented on a discussion
- on #{link_to discussion.file_path, @target_url}
+ on #{link_to discussion.file_path, target_url}
- else
- if discussion.new_discussion?
started a new discussion
- else
- commented on a #{link_to 'discussion', @target_url}
+ commented on a #{link_to 'discussion', target_url}
- elsif Gitlab::CurrentSettings.email_author_in_body
%p.details
- #{link_to @note.author_name, user_url(@note.author)} commented:
+ #{link_to note.author_name, user_url(note.author)} commented:
- if diff_discussion && !on_image
= content_for :head do
@@ -32,11 +37,11 @@
%table
= render partial: "projects/diffs/line",
- collection: discussion.truncated_diff_lines,
+ collection: discussion.truncated_diff_lines(diff_limit: diff_limit),
as: :line,
locals: { diff_file: discussion.diff_file,
plain: true,
email: true }
-%div
- = markdown(@note.note, pipeline: :email, author: @note.author)
+%div{ style: note_style }
+ = markdown(note.note, pipeline: :email, author: note.author)
diff --git a/app/views/notify/_note_email.text.erb b/app/views/notify/_note_email.text.erb
index c319cb55e87..4bf252b6ce1 100644
--- a/app/views/notify/_note_email.text.erb
+++ b/app/views/notify/_note_email.text.erb
@@ -1,6 +1,9 @@
-<% discussion = @note.discussion if @note.part_of_discussion? -%>
+<% note = local_assigns.fetch(:note, @note) -%>
+<% diff_limit = local_assigns.fetch(:diff_limit, nil) -%>
+
+<% discussion = note.discussion if note.part_of_discussion? -%>
<% if discussion && !discussion.individual_note? -%>
-<%= @note.author_name -%>
+<%= note.author_name -%>
<% if discussion.new_discussion? -%>
<%= " started a new discussion" -%>
<% else -%>
@@ -13,14 +16,14 @@
<% elsif Gitlab::CurrentSettings.email_author_in_body -%>
-<%= "#{@note.author_name} commented:" -%>
+<%= "#{note.author_name} commented:" -%>
<% end -%>
<% if discussion&.diff_discussion? -%>
-<% discussion.truncated_diff_lines(highlight: false).each do |line| -%>
+<% discussion.truncated_diff_lines(highlight: false, diff_limit: diff_limit).each do |line| -%>
<%= "> #{line.text}\n" -%>
<% end -%>
<% end -%>
-<%= @note.note -%>
+<%= note.note -%>
diff --git a/app/views/notify/remote_mirror_update_failed_email.html.haml b/app/views/notify/remote_mirror_update_failed_email.html.haml
new file mode 100644
index 00000000000..4fb0a4c5a8a
--- /dev/null
+++ b/app/views/notify/remote_mirror_update_failed_email.html.haml
@@ -0,0 +1,46 @@
+%tr.alert{ style: "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif;" }
+ %td{ style: "padding:10px;border-radius:3px;font-size:14px;line-height:1.3;text-align:center;overflow:hidden;background-color:#d22f57;color:#ffffff;" }
+ %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;margin:0 auto;" }
+ %tbody
+ %tr
+ %td{ style: "vertical-align:middle;color:#ffffff;text-align:center;padding-right:5px;line-height:1;" }
+ %img{ alt: "✖", height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-x-red-inverted.gif'), style: "display:block;", width: "13" }/
+ %td{ style: "vertical-align:middle;color:#ffffff;text-align:center;" }
+ A remote mirror update has failed.
+%tr.spacer{ style: "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif;" }
+ %td{ style: "height:18px;font-size:18px;line-height:18px;" }
+ &nbsp;
+%tr.section{ style: "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif;" }
+ %td{ style: "padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;" }
+ %table.table-info{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;" }
+ %tbody{ style: "font-size:15px;line-height:1.4;color:#8c8c8c;" }
+ %tr
+ %td{ style: "font-weight:300;padding:14px 0;margin:0;" } Project
+ %td{ style: "font-weight:500;padding:14px 0;margin:0;color:#333333;width:75%;padding-left:5px;" }
+ - namespace_url = @project.group ? group_url(@project.group) : user_url(@project.namespace.owner)
+ %a.muted{ href: namespace_url, style: "color:#333333;text-decoration:none;" }
+ = @project.owner_name
+ \/
+ %a.muted{ href: project_url(@project), style: "color:#333333;text-decoration:none;" }
+ = @project.name
+ %tr
+ %td{ style: "font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Remote mirror
+ %td{ style: "font-weight:500;padding:14px 0;margin:0;color:#333333;width:75%;padding-left:5px;border-top:1px solid #ededed;" }
+ = @remote_mirror.safe_url
+ %tr
+ %td{ style: "font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Last update at
+ %td{ style: "font-weight:500;padding:14px 0;margin:0;color:#333333;width:75%;padding-left:5px;border-top:1px solid #ededed;" }
+ = @remote_mirror.last_update_at
+
+%tr.table-warning{ style: "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif;" }
+ %td{ style: "border: 1px solid #ededed; border-bottom: 0; border-radius: 4px 4px 0 0; overflow: hidden; background-color: #fdf4f6; color: #d22852; font-size: 14px; line-height: 1.4; text-align: center; padding: 8px 16px;" }
+ Logs may contain sensitive data. Please consider before forwarding this email.
+%tr.section{ style: "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif;" }
+ %td{ style: "padding: 0 16px; border: 1px solid #ededed; border-radius: 4px; overflow: hidden; border-top: 0; border-radius: 0 0 4px 4px;" }
+ %table.builds{ border: "0", cellpadding: "0", cellspacing: "0", style: "width: 100%; border-collapse: collapse;" }
+ %tbody
+ %tr.build-log
+ %td{ colspan: "2", style: "padding: 0 0 16px;" }
+ %pre{ style: "font-family: Monaco,'Lucida Console','Courier New',Courier,monospace; background-color: #fafafa; border-radius: 4px; overflow: hidden; white-space: pre-wrap; word-break: break-all; font-size:13px; line-height: 1.4; padding: 16px 8px; color: #333333; margin: 0;" }
+ = @remote_mirror.last_error
+
diff --git a/app/views/notify/remote_mirror_update_failed_email.text.erb b/app/views/notify/remote_mirror_update_failed_email.text.erb
new file mode 100644
index 00000000000..c6f29f0ad1c
--- /dev/null
+++ b/app/views/notify/remote_mirror_update_failed_email.text.erb
@@ -0,0 +1,7 @@
+A remote mirror update has failed.
+
+Project: <%= @project.human_name %> ( <%= project_url(@project) %> )
+Remote mirror: <%= @remote_mirror.safe_url %>
+Last update at: <%= @remote_mirror.last_update_at %>
+Last error:
+<%= @remote_mirror.last_error %>
diff --git a/app/views/notify/repository_cleanup_failure_email.text.erb b/app/views/notify/repository_cleanup_failure_email.text.erb
new file mode 100644
index 00000000000..f5a426a51d1
--- /dev/null
+++ b/app/views/notify/repository_cleanup_failure_email.text.erb
@@ -0,0 +1,3 @@
+Repository cleanup failed on <%= @project.web_url %>
+
+<%= @error %>
diff --git a/app/views/notify/repository_cleanup_success_email.text.erb b/app/views/notify/repository_cleanup_success_email.text.erb
new file mode 100644
index 00000000000..e6e95da2fcc
--- /dev/null
+++ b/app/views/notify/repository_cleanup_success_email.text.erb
@@ -0,0 +1,3 @@
+Repository cleanup succeeded on <%= @project.web_url %>
+
+Repository size is now <%= "%.1f" % (@project.repository.size || 0) %> MiB
diff --git a/app/views/projects/_files.html.haml b/app/views/projects/_files.html.haml
index 79530e78154..22a721ee9ad 100644
--- a/app/views/projects/_files.html.haml
+++ b/app/views/projects/_files.html.haml
@@ -1,7 +1,9 @@
+- is_project_overview = local_assigns.fetch(:is_project_overview, false)
- commit = local_assigns.fetch(:commit) { @repository.commit }
- ref = local_assigns.fetch(:ref) { current_ref }
- project = local_assigns.fetch(:project) { @project }
- content_url = local_assigns.fetch(:content_url) { @tree.readme ? project_blob_path(@project, tree_join(@ref, @tree.readme.path)) : project_tree_path(@project, @ref) }
+- show_auto_devops_callout = show_auto_devops_callout?(@project)
#tree-holder.tree-holder.clearfix
.nav-block
@@ -10,4 +12,8 @@
- if commit
= render 'shared/commit_well', commit: commit, ref: ref, project: project
+ - if is_project_overview
+ .project-buttons.append-bottom-default
+ = render 'stat_anchor_list', anchors: @project.statistics_buttons(show_auto_devops_callout: show_auto_devops_callout)
+
= render 'projects/tree/tree_content', tree: @tree, content_url: content_url
diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml
index dcef4dd5b69..e191b009db2 100644
--- a/app/views/projects/_home_panel.html.haml
+++ b/app/views/projects/_home_panel.html.haml
@@ -1,83 +1,75 @@
- empty_repo = @project.empty_repo?
-- license = @project.license_anchor_data
+- show_auto_devops_callout = show_auto_devops_callout?(@project)
.project-home-panel{ class: ("empty-project" if empty_repo) }
- .limit-container-width{ class: container_class }
- .project-header.d-flex.flex-row.flex-wrap.align-items-center.append-bottom-8
- .project-title-row.d-flex.align-items-center
- .avatar-container.project-avatar.float-none
- = project_icon(@project, alt: @project.name, class: 'avatar avatar-tile s24', width: 24, height: 24)
- %h1.project-title.d-flex.align-items-baseline.qa-project-name
- = @project.name
- .project-metadata.d-flex.flex-row.flex-wrap.align-items-baseline
- .project-visibility.d-inline-flex.align-items-baseline.visibility-icon.has-tooltip{ data: { container: 'body' }, title: visibility_icon_description(@project) }
- = visibility_level_icon(@project.visibility_level, fw: false, options: {class: 'icon'})
- = visibility_level_label(@project.visibility_level)
- - if license.present?
- .project-license.d-inline-flex.align-items-baseline
- = link_to_if license.link, sprite_icon('scale', size: 16, css_class: 'icon') + license.label, license.link, class: license.enabled ? 'btn btn-link btn-secondary-hover-link' : 'btn btn-link'
- - if @project.tag_list.present?
- .project-tag-list.d-inline-flex.align-items-baseline.has-tooltip{ data: { container: 'body' }, title: @project.has_extra_tags? ? @project.tag_list.join(', ') : nil }
- = sprite_icon('tag', size: 16, css_class: 'icon')
- = @project.tags_to_show
- - if @project.has_extra_tags?
- = _("+ %{count} more") % { count: @project.count_of_extra_tags_not_shown }
+ .project-header.row.append-bottom-8
+ .project-title-row.col-md-12.col-lg-7.d-flex
+ .avatar-container.project-avatar.float-none
+ = project_icon(@project, alt: @project.name, class: 'avatar avatar-tile s64', width: 64, height: 64)
+ .d-flex.flex-column.flex-wrap.align-items-baseline
+ .d-inline-flex.align-items-baseline
+ %h1.project-title.qa-project-name
+ = @project.name
+ %span.project-visibility.prepend-left-8.d-inline-flex.align-items-baseline.visibility-icon.has-tooltip{ data: { container: 'body' }, title: visibility_icon_description(@project) }
+ = visibility_level_icon(@project.visibility_level, fw: false, options: {class: 'icon'})
+ .project-metadata.d-flex.align-items-center
+ - if can?(current_user, :read_project, @project)
+ %span.text-secondary
+ = s_('ProjectPage|Project ID: %{project_id}') % { project_id: @project.id }
+ - if current_user
+ %span.access-request-links.prepend-left-8
+ = render 'shared/members/access_request_links', source: @project
+ - if @project.tag_list.present?
+ %span.project-tag-list.d-inline-flex.prepend-left-8.has-tooltip{ data: { container: 'body' }, title: @project.has_extra_tags? ? @project.tag_list.join(', ') : nil }
+ = sprite_icon('tag', size: 16, css_class: 'icon append-right-4')
+ = @project.tags_to_show
+ - if @project.has_extra_tags?
+ = _("+ %{count} more") % { count: @project.count_of_extra_tags_not_shown }
- .project-home-desc
- - if @project.description.present?
- .project-description
- .project-description-markdown.read-more-container
- = markdown_field(@project, :description)
- %button.btn.btn-blank.btn-link.text-secondary.js-read-more-trigger.text-secondary.d-lg-none{ type: "button" }
- = _("Read more")
-
- - if can?(current_user, :read_project, @project)
- .text-secondary.prepend-top-8
- = s_('ProjectPage|Project ID: %{project_id}') % { project_id: @project.id }
-
- - if @project.forked?
- %p
- - if @project.fork_source
- #{ s_('ForkedFromProjectPath|Forked from') }
- = link_to project_path(@project.fork_source) do
- = fork_source_name(@project)
- - else
- - deleted_message = s_('ForkedFromProjectPath|Forked from %{project_name} (deleted)')
- = deleted_message % { project_name: fork_source_name(@project) }
-
- - if @project.badges.present?
- .project-badges.prepend-top-default.append-bottom-default
- - @project.badges.each do |badge|
- %a.append-right-8{ href: badge.rendered_link_url(@project),
- target: '_blank',
- rel: 'noopener noreferrer' }>
- %img.project-badge{ src: badge.rendered_image_url(@project),
- 'aria-hidden': true,
- alt: 'Project badge' }>
+ .project-repo-buttons.col-md-12.col-lg-5.d-inline-flex.flex-wrap.justify-content-lg-end
+ - if current_user
+ .d-inline-flex
+ = render 'projects/buttons/notifications', notification_setting: @notification_setting, btn_class: 'btn-xs'
- .project-repo-buttons.d-inline-flex.flex-wrap
.count-buttons.d-inline-flex
= render 'projects/buttons/star'
= render 'projects/buttons/fork'
- if can?(current_user, :download_code, @project)
- .project-clone-holder.d-inline-flex.d-sm-none
+ .project-clone-holder.d-inline-flex.d-md-none.btn-block
= render "shared/mobile_clone_panel"
- .project-clone-holder.d-none.d-sm-inline-flex
- = render "shared/clone_panel"
+ .project-clone-holder.d-none.d-md-inline-flex
+ = render "projects/buttons/clone"
- - if show_xcode_link?(@project)
- .project-action-button.project-xcode.inline
- = render "projects/buttons/xcode_link"
+ - if can?(current_user, :download_code, @project)
+ %nav.project-stats
+ .nav-links.quick-links.mt-3
+ = render 'stat_anchor_list', anchors: @project.statistics_anchors(show_auto_devops_callout: show_auto_devops_callout)
- - if current_user
- - if can?(current_user, :download_code, @project)
- .d-none.d-sm-inline-flex
- = render 'projects/buttons/download', project: @project, ref: @ref
- .d-none.d-sm-inline-flex
- = render 'projects/buttons/dropdown'
+ .project-home-desc.mt-1
+ - if @project.description.present?
+ .project-description
+ .project-description-markdown.read-more-container
+ = markdown_field(@project, :description)
+ %button.btn.btn-blank.btn-link.js-read-more-trigger.d-lg-none{ type: "button" }
+ = _("Read more")
+
+ - if @project.forked?
+ %p
+ - if @project.fork_source
+ #{ s_('ForkedFromProjectPath|Forked from') }
+ = link_to project_path(@project.fork_source) do
+ = fork_source_name(@project)
+ - else
+ - deleted_message = s_('ForkedFromProjectPath|Forked from %{project_name} (deleted)')
+ = deleted_message % { project_name: fork_source_name(@project) }
- .d-none.d-sm-inline-flex
- = render 'shared/notifications/button', notification_setting: @notification_setting
- .d-none.d-sm-inline-flex
- = render 'shared/members/access_request_buttons', source: @project
+ - if @project.badges.present?
+ .project-badges.mb-2
+ - @project.badges.each do |badge|
+ %a.append-right-8{ href: badge.rendered_link_url(@project),
+ target: '_blank',
+ rel: 'noopener noreferrer' }>
+ %img.project-badge{ src: badge.rendered_image_url(@project),
+ 'aria-hidden': true,
+ alt: 'Project badge' }>
diff --git a/app/views/projects/_stat_anchor_list.html.haml b/app/views/projects/_stat_anchor_list.html.haml
index 4cf49f3cf62..8e3d759b683 100644
--- a/app/views/projects/_stat_anchor_list.html.haml
+++ b/app/views/projects/_stat_anchor_list.html.haml
@@ -4,5 +4,5 @@
%ul.nav
- anchors.each do |anchor|
%li.nav-item
- = link_to_if anchor.link, anchor.label, anchor.link, class: anchor.enabled ? 'nav-link stat-link' : "nav-link btn btn-#{anchor.class_modifier || 'missing'}" do
- .stat-text= anchor.label
+ = link_to_if anchor.link, anchor.label, anchor.link, class: anchor.is_link ? 'nav-link stat-link d-flex align-items-center' : "nav-link btn btn-#{anchor.class_modifier || 'missing'} d-flex align-items-center" do
+ .stat-text.d-flex.align-items-center= anchor.label
diff --git a/app/views/projects/blob/_blob.html.haml b/app/views/projects/blob/_blob.html.haml
index cf273aab108..95c5eb32c7f 100644
--- a/app/views/projects/blob/_blob.html.haml
+++ b/app/views/projects/blob/_blob.html.haml
@@ -9,6 +9,6 @@
= render "projects/blob/auxiliary_viewer", blob: blob
#blob-content-holder.blob-content-holder
- %article.file-holder
+ %article.file-holder{ class: ('use-csslab' if Feature.enabled?(:csslab)) }
= render 'projects/blob/header', blob: blob
= render 'projects/blob/content', blob: blob
diff --git a/app/views/projects/blob/preview.html.haml b/app/views/projects/blob/preview.html.haml
index eb65cd90ea8..ff460a3831c 100644
--- a/app/views/projects/blob/preview.html.haml
+++ b/app/views/projects/blob/preview.html.haml
@@ -1,7 +1,7 @@
.diff-file.file-holder
.diff-content
- if markup?(@blob.name)
- .file-content.wiki
+ .file-content.wiki.md{ class: ('use-csslab' if Feature.enabled?(:csslab)) }
= markup(@blob.name, @content, legacy_render_context(params))
- else
.file-content.code.js-syntax-highlight
diff --git a/app/views/projects/blob/viewers/_markup.html.haml b/app/views/projects/blob/viewers/_markup.html.haml
index bd12cadf240..6edbfd91b21 100644
--- a/app/views/projects/blob/viewers/_markup.html.haml
+++ b/app/views/projects/blob/viewers/_markup.html.haml
@@ -2,5 +2,5 @@
- context = legacy_render_context(params)
- unless context[:markdown_engine] == :redcarpet
- context[:rendered] = blob.rendered_markup if blob.respond_to?(:rendered_markup)
-.file-content.wiki
+.file-content.wiki.md{ class: ('use-csslab' if Feature.enabled?(:csslab)) }
= markup(blob.name, blob.data, context)
diff --git a/app/views/projects/buttons/_clone.html.haml b/app/views/projects/buttons/_clone.html.haml
new file mode 100644
index 00000000000..d453a3a9dac
--- /dev/null
+++ b/app/views/projects/buttons/_clone.html.haml
@@ -0,0 +1,31 @@
+- project = project || @project
+
+.git-clone-holder.js-git-clone-holder.input-group
+ - if allowed_protocols_present?
+ .input-group-text.clone-dropdown-btn.btn
+ %span.js-clone-dropdown-label
+ = enabled_project_button(project, enabled_protocol)
+ - else
+ %a#clone-dropdown.input-group-text.btn.btn-primary.btn-xs.clone-dropdown-btn.qa-clone-dropdown{ href: '#', data: { toggle: 'dropdown' } }
+ %span.append-right-4.js-clone-dropdown-label
+ = _('Clone')
+ = sprite_icon("arrow-down", css_class: "icon")
+ %form.p-3.dropdown-menu.dropdown-menu-right.dropdown-menu-large.dropdown-menu-selectable.clone-options-dropdown.qa-clone-options
+ %li.pb-2
+ %label.label-bold
+ = _('Clone with SSH')
+ .input-group
+ = text_field_tag :ssh_project_clone, project.ssh_url_to_repo, class: "js-select-on-focus form-control qa-ssh-clone-url", readonly: true, aria: { label: 'Project clone URL' }
+ .input-group-append
+ = clipboard_button(target: '#ssh_project_clone', title: _("Copy URL to clipboard"), class: "input-group-text btn-default btn-clipboard")
+ = render_if_exists 'projects/buttons/geo'
+ %li
+ %label.label-bold
+ = _('Clone with %{http_label}') % { http_label: gitlab_config.protocol.upcase }
+ .input-group
+ = text_field_tag :http_project_clone, project.http_url_to_repo, class: "js-select-on-focus form-control qa-http-clone-url", readonly: true, aria: { label: 'Project clone URL' }
+ .input-group-append
+ = clipboard_button(target: '#http_project_clone', title: _("Copy URL to clipboard"), class: "input-group-text btn-default btn-clipboard")
+ = render_if_exists 'projects/buttons/geo'
+
+= render_if_exists 'shared/geo_info_modal', project: project
diff --git a/app/views/projects/buttons/_download.html.haml b/app/views/projects/buttons/_download.html.haml
index f7551434d47..4eb53faa6ff 100644
--- a/app/views/projects/buttons/_download.html.haml
+++ b/app/views/projects/buttons/_download.html.haml
@@ -5,8 +5,8 @@
.project-action-button.dropdown.inline>
%button.btn.has-tooltip{ title: s_('DownloadSource|Download'), 'data-toggle' => 'dropdown', 'aria-label' => s_('DownloadSource|Download'), 'data-display' => 'static' }
= sprite_icon('download')
- = icon("caret-down")
%span.sr-only= _('Select Archive Format')
+ = sprite_icon("arrow-down")
%ul.dropdown-menu.dropdown-menu-right{ role: 'menu' }
%li.dropdown-header
#{ _('Source code') }
diff --git a/app/views/projects/buttons/_fork.html.haml b/app/views/projects/buttons/_fork.html.haml
index 8da27ca7cb3..bc0a89bea62 100644
--- a/app/views/projects/buttons/_fork.html.haml
+++ b/app/views/projects/buttons/_fork.html.haml
@@ -1,9 +1,6 @@
- unless @project.empty_repo?
- if current_user && can?(current_user, :fork_project, @project)
.count-badge.d-inline-flex.align-item-stretch.append-right-8
- %span.fork-count.count-badge-count.d-flex.align-items-center
- = link_to project_forks_path(@project), title: n_(s_('ProjectOverview|Fork'), s_('ProjectOverview|Forks'), @project.forks_count), class: 'count' do
- = @project.forks_count
- if current_user.already_forked?(@project) && current_user.manageable_namespaces.size < 2
= link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: s_('ProjectOverview|Go to your fork'), class: 'btn btn-default has-tooltip count-badge-button d-flex align-items-center fork-btn' do
= sprite_icon('fork', { css_class: 'icon' })
@@ -15,3 +12,6 @@
title: (s_('ProjectOverview|You have reached your project limit') unless can_create_fork) do
= sprite_icon('fork', { css_class: 'icon' })
%span= s_('ProjectOverview|Fork')
+ %span.fork-count.count-badge-count.d-flex.align-items-center
+ = link_to project_forks_path(@project), title: n_(s_('ProjectOverview|Fork'), s_('ProjectOverview|Forks'), @project.forks_count), class: 'count' do
+ = @project.forks_count
diff --git a/app/views/projects/buttons/_notifications.html.haml b/app/views/projects/buttons/_notifications.html.haml
new file mode 100644
index 00000000000..745983ace7e
--- /dev/null
+++ b/app/views/projects/buttons/_notifications.html.haml
@@ -0,0 +1,27 @@
+- btn_class = local_assigns.fetch(:btn_class, "btn-xs")
+
+- if notification_setting
+ .js-notification-dropdown.notification-dropdown.project-action-button.dropdown.inline
+ = form_for notification_setting, remote: true, html: { class: "inline notification-form no-label" } do |f|
+ = hidden_setting_source_input(notification_setting)
+ = hidden_field_tag "hide_label", true
+ = f.hidden_field :level, class: "notification_setting_level"
+ .js-notification-toggle-btns
+ %div{ class: ("btn-group" if notification_setting.custom?) }
+ - if notification_setting.custom?
+ %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: _("Notification setting - %{notification_title}") % { notification_title: notification_title(notification_setting.level) }, class: "#{btn_class}", "aria-label" => _("Notification setting - %{notification_title}") % { notification_title: notification_title(notification_setting.level) }, data: { container: "body", toggle: "modal", target: "#" + notifications_menu_identifier("modal", notification_setting), display: 'static' } }
+ = sprite_icon("notifications", css_class: "icon notifications-icon js-notifications-icon")
+ %span.js-notification-loading.fa.hidden
+ %button.btn.dropdown-toggle{ data: { toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" } }
+ = sprite_icon("arrow-down", css_class: "icon")
+ .sr-only Toggle dropdown
+ - else
+ %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: "Notification setting - #{notification_title(notification_setting.level)}", class: "#{btn_class}", "aria-label" => "Notification setting: #{notification_title(notification_setting.level)}", data: { container: "body", toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" } }
+ = sprite_icon("notifications", css_class: "icon notifications-icon js-notifications-icon")
+ %span.js-notification-loading.fa.hidden
+ = sprite_icon("arrow-down", css_class: "icon")
+
+ = render "shared/notifications/notification_dropdown", notification_setting: notification_setting
+
+ = content_for :scripts_body do
+ = render "shared/notifications/custom_notifications", notification_setting: notification_setting
diff --git a/app/views/projects/buttons/_star.html.haml b/app/views/projects/buttons/_star.html.haml
index 0d04ecb3a58..090d1549aa7 100644
--- a/app/views/projects/buttons/_star.html.haml
+++ b/app/views/projects/buttons/_star.html.haml
@@ -1,19 +1,19 @@
- if current_user
.count-badge.d-inline-flex.align-item-stretch.append-right-8
- %span.star-count.count-badge-count.d-flex.align-items-center
- = @project.star_count
- %button.count-badge-button.btn.btn-default.d-flex.align-items-center.star-btn.toggle-star{ type: "button", data: { endpoint: toggle_star_project_path(@project, :json) } }
+ %button.count-badge-button.btn.btn-default.btn-xs.d-flex.align-items-center.star-btn.toggle-star{ type: "button", data: { endpoint: toggle_star_project_path(@project, :json) } }
- if current_user.starred?(@project)
= sprite_icon('star', { css_class: 'icon' })
%span.starred= s_('ProjectOverview|Unstar')
- else
= sprite_icon('star-o', { css_class: 'icon' })
%span= s_('ProjectOverview|Star')
+ %span.star-count.count-badge-count.d-flex.align-items-center
+ = @project.star_count
- else
.count-badge.d-inline-flex.align-item-stretch.append-right-8
- %span.star-count.count-badge-count.d-flex.align-items-center
- = @project.star_count
- = link_to new_user_session_path, class: 'btn btn-default has-tooltip count-badge-button d-flex align-items-center star-btn', title: s_('ProjectOverview|You must sign in to star a project') do
+ = link_to new_user_session_path, class: 'btn btn-default btn-xs has-tooltip count-badge-button d-flex align-items-center star-btn', title: s_('ProjectOverview|You must sign in to star a project') do
= sprite_icon('star-o', { css_class: 'icon' })
%span= s_('ProjectOverview|Star')
+ %span.star-count.count-badge-count.d-flex.align-items-center
+ = @project.star_count
diff --git a/app/views/projects/cleanup/_show.html.haml b/app/views/projects/cleanup/_show.html.haml
new file mode 100644
index 00000000000..778d27fc61d
--- /dev/null
+++ b/app/views/projects/cleanup/_show.html.haml
@@ -0,0 +1,31 @@
+- return unless Feature.enabled?(:project_cleanup, @project)
+
+- expanded = Rails.env.test?
+
+%section.settings.no-animate#cleanup{ class: ('expanded' if expanded) }
+ .settings-header
+ %h4= _('Repository cleanup')
+ %button.btn.js-settings-toggle
+ = expanded ? _('Collapse') : _('Expand')
+ %p
+ = _("Clean up after running %{bfg} on the repository" % { bfg: link_to_bfg }).html_safe
+ = link_to icon('question-circle'),
+ help_page_path('user/project/repository/reducing_the_repo_size_using_git.md'),
+ target: '_blank', rel: 'noopener noreferrer'
+
+ .settings-content
+ - url = cleanup_namespace_project_settings_repository_path(@project.namespace, @project)
+ = form_for @project, url: url, method: :post, authenticity_token: true, html: { class: 'js-requires-input' } do |f|
+ %fieldset.prepend-top-0.append-bottom-10
+ .append-bottom-10
+ %h5.prepend-top-0
+ = _("Upload object map")
+ %button.btn.btn-default.js-choose-file{ type: "button" }
+ = _("Choose a file")
+ %span.prepend-left-default.js-filename
+ = _("No file selected")
+ = f.file_field :bfg_object_map, accept: 'text/plain', class: "hidden js-object-map-input", required: true
+ .form-text.text-muted
+ = _("The maximum file size allowed is %{max_attachment_size}mb") % { max_attachment_size: Gitlab::CurrentSettings.max_attachment_size }
+ = f.submit _('Start cleanup'), class: 'btn btn-success'
+
diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml
index aab5712d197..2a919a767c0 100644
--- a/app/views/projects/commit/_commit_box.html.haml
+++ b/app/views/projects/commit/_commit_box.html.haml
@@ -28,7 +28,7 @@
= link_to project_tree_path(@project, @commit), class: "btn btn-default append-right-10 d-none d-sm-none d-md-inline" do
#{ _('Browse files') }
.dropdown.inline
- %a.btn.btn-default.dropdown-toggle{ data: { toggle: "dropdown" } }
+ %a.btn.btn-default.dropdown-toggle.qa-options-button{ data: { toggle: "dropdown" } }
%span= _('Options')
= icon('caret-down')
%ul.dropdown-menu.dropdown-menu-right
@@ -48,8 +48,8 @@
%li.dropdown-header
#{ _('Download') }
- unless @commit.parents.length > 1
- %li= link_to s_("DownloadCommit|Email Patches"), project_commit_path(@project, @commit, format: :patch)
- %li= link_to s_("DownloadCommit|Plain Diff"), project_commit_path(@project, @commit, format: :diff)
+ %li= link_to s_("DownloadCommit|Email Patches"), project_commit_path(@project, @commit, format: :patch), class: "qa-email-patches"
+ %li= link_to s_("DownloadCommit|Plain Diff"), project_commit_path(@project, @commit, format: :diff), class: "qa-plain-diff"
.commit-box{ data: { project_path: project_path(@project) } }
%h3.commit-title
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index f376df29878..1b52821af15 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -53,7 +53,7 @@
= _("Project avatar in repository: %{link}").html_safe % { link: @project.avatar_in_git }
.prepend-top-5.append-bottom-10
%button.btn.js-choose-project-avatar-button{ type: 'button' }= _("Choose file...")
- %span.file_name.prepend-left-default.js-avatar-filename= _("No file chosen")
+ %span.file_name.prepend-left-default.js-filename= _("No file chosen")
= f.file_field :avatar, class: "js-project-avatar-input hidden"
.form-text.text-muted= _("The maximum file size allowed is 200KB.")
- if @project.avatar?
diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml
index 936900a0087..081990ac9b7 100644
--- a/app/views/projects/empty.html.haml
+++ b/app/views/projects/empty.html.haml
@@ -4,11 +4,10 @@
= render partial: 'flash_messages', locals: { project: @project }
-= render "home_panel"
+%div{ class: [container_class, ("limit-container-width" unless fluid_layout)] }
+ = render "home_panel"
-.project-empty-note-panel
- %div{ class: [container_class, ("limit-container-width" unless fluid_layout)] }
- .prepend-top-20
+ .project-empty-note-panel
%h4.append-bottom-20
= _('The repository for this project is empty')
@@ -32,66 +31,65 @@
= _('Otherwise it is recommended you start with one of the options below.')
.prepend-top-20
-%nav.project-stats{ class: [container_class, ("limit-container-width" unless fluid_layout)] }
- .scrolling-tabs-container.inner-page-scroll-tabs.is-smaller
- .fade-left= icon('angle-left')
- .fade-right= icon('angle-right')
- .nav-links.scrolling-tabs.quick-links
- = render 'stat_anchor_list', anchors: @project.empty_repo_statistics_anchors
- = render 'stat_anchor_list', anchors: @project.empty_repo_statistics_buttons
+ %nav.project-buttons
+ .scrolling-tabs-container.inner-page-scroll-tabs.is-smaller.qa-quick-actions
+ .fade-left= icon('angle-left')
+ .fade-right= icon('angle-right')
+ .nav-links.scrolling-tabs.quick-links
+ = render 'stat_anchor_list', anchors: @project.empty_repo_statistics_buttons
-- if can?(current_user, :push_code, @project)
- %div{ class: [container_class, ("limit-container-width" unless fluid_layout)] }
- .prepend-top-20
- .empty_wrapper
- %h3#repo-command-line-instructions.page-title-empty
- Command line instructions
- .git-empty.js-git-empty
- %fieldset
- %h5 Git global setup
- %pre.bg-light
- :preserve
- git config --global user.name "#{h git_user_name}"
- git config --global user.email "#{h git_user_email}"
+ - if can?(current_user, :push_code, @project)
+ %div
+ .prepend-top-20
+ .empty_wrapper
+ %h3#repo-command-line-instructions.page-title-empty
+ = _('Command line instructions')
+ .git-empty.js-git-empty
+ %fieldset
+ %h5= _('Git global setup')
+ %pre.bg-light
+ :preserve
+ git config --global user.name "#{h git_user_name}"
+ git config --global user.email "#{h git_user_email}"
- %fieldset
- %h5 Create a new repository
- %pre.bg-light
- :preserve
- git clone #{ content_tag(:span, default_url_to_repo, class: 'js-clone')}
- cd #{h @project.path}
- touch README.md
- git add README.md
- git commit -m "add README"
- - if @project.can_current_user_push_to_default_branch?
- %span><
- git push -u origin master
+ %fieldset
+ %h5= _('Create a new repository')
+ %pre.bg-light
+ :preserve
+ git clone #{ content_tag(:span, default_url_to_repo, class: 'js-clone')}
+ cd #{h @project.path}
+ touch README.md
+ git add README.md
+ git commit -m "add README"
+ - if @project.can_current_user_push_to_default_branch?
+ %span><
+ git push -u origin master
- %fieldset
- %h5 Existing folder
- %pre.bg-light
- :preserve
- cd existing_folder
- git init
- git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'js-clone')}
- git add .
- git commit -m "Initial commit"
- - if @project.can_current_user_push_to_default_branch?
- %span><
- git push -u origin master
+ %fieldset
+ %h5= _('Existing folder')
+ %pre.bg-light
+ :preserve
+ cd existing_folder
+ git init
+ git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'js-clone')}
+ git add .
+ git commit -m "Initial commit"
+ - if @project.can_current_user_push_to_default_branch?
+ %span><
+ git push -u origin master
- %fieldset
- %h5 Existing Git repository
- %pre.bg-light
- :preserve
- cd existing_repo
- git remote rename origin old-origin
- git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'js-clone')}
- - if @project.can_current_user_push_to_default_branch?
- %span><
- git push -u origin --all
- git push -u origin --tags
+ %fieldset
+ %h5= _('Existing Git repository')
+ %pre.bg-light
+ :preserve
+ cd existing_repo
+ git remote rename origin old-origin
+ git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'js-clone')}
+ - if @project.can_current_user_push_to_default_branch?
+ %span><
+ git push -u origin --all
+ git push -u origin --tags
- - if can? current_user, :remove_project, @project
- .prepend-top-20
- = link_to 'Remove project', [@project.namespace.becomes(Namespace), @project], data: { confirm: remove_project_message(@project)}, method: :delete, class: "btn btn-inverted btn-remove float-right"
+ - if can? current_user, :remove_project, @project
+ .prepend-top-20
+ = link_to _('Remove project'), [@project.namespace.becomes(Namespace), @project], data: { confirm: remove_project_message(@project)}, method: :delete, class: "btn btn-inverted btn-remove float-right"
diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml
index 6c0ad34c486..d66de7ab698 100644
--- a/app/views/projects/environments/index.html.haml
+++ b/app/views/projects/environments/index.html.haml
@@ -1,6 +1,5 @@
- @no_container = true
- page_title _("Environments")
-- add_to_breadcrumbs(_("Pipelines"), project_pipelines_path(@project))
#environments-list-view{ data: { environments_data: environments_list_data,
"can-create-deployment" => can?(current_user, :create_deployment, @project).to_s,
diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml
index 2c6484c2c99..56b06374d6d 100644
--- a/app/views/projects/labels/index.html.haml
+++ b/app/views/projects/labels/index.html.haml
@@ -5,7 +5,7 @@
- subscribed = params[:subscribed]
- labels_or_filters = @labels.exists? || @prioritized_labels.exists? || search.present? || subscribed.present?
-- if @labels.present? && can_admin_label
+- if labels_or_filters && can_admin_label
- content_for(:header_content) do
.nav-controls
= link_to _('New label'), new_project_label_path(@project), class: "btn btn-success qa-label-create-new"
diff --git a/app/views/projects/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml
index 4ebb029e48b..c178206dda4 100644
--- a/app/views/projects/merge_requests/show.html.haml
+++ b/app/views/projects/merge_requests/show.html.haml
@@ -77,7 +77,8 @@
#js-diffs-app.diffs.tab-pane{ data: { "is-locked" => @merge_request.discussion_locked?,
endpoint: diffs_project_merge_request_path(@project, @merge_request, 'json', request.query_parameters),
current_user_data: UserSerializer.new(project: @project).represent(current_user, {}, MergeRequestUserEntity).to_json,
- project_path: project_path(@merge_request.project)} }
+ project_path: project_path(@merge_request.project),
+ changes_empty_state_illustration: image_path('illustrations/merge_request_changes_empty.svg') } }
.mr-loading-status
= spinner
diff --git a/app/views/projects/mirrors/_authentication_method.html.haml b/app/views/projects/mirrors/_authentication_method.html.haml
index 3effdf934fb..293a2e3ebfe 100644
--- a/app/views/projects/mirrors/_authentication_method.html.haml
+++ b/app/views/projects/mirrors/_authentication_method.html.haml
@@ -8,14 +8,14 @@
= f.label :auth_method, _('Authentication method'), class: 'label-bold'
= f.select :auth_method,
options_for_select(auth_options, mirror.auth_method),
- {}, { class: "form-control js-mirror-auth-type" }
+ {}, { class: "form-control js-mirror-auth-type qa-authentication-method" }
.form-group
.collapse.js-well-changing-auth
.changing-auth-method= icon('spinner spin lg')
.well-password-auth.collapse.js-well-password-auth
= f.label :password, _("Password"), class: "label-bold"
- = f.password_field :password, value: mirror.password, class: 'form-control', autocomplete: 'new-password'
+ = f.password_field :password, value: mirror.password, class: 'form-control qa-password', autocomplete: 'new-password'
- unless is_push
.well-ssh-auth.collapse.js-well-ssh-auth
%p.js-ssh-public-key-present{ class: ('collapse' unless ssh_public_key_present) }
diff --git a/app/views/projects/mirrors/_mirror_repos.html.haml b/app/views/projects/mirrors/_mirror_repos.html.haml
index dde0fae740b..21b105e6f80 100644
--- a/app/views/projects/mirrors/_mirror_repos.html.haml
+++ b/app/views/projects/mirrors/_mirror_repos.html.haml
@@ -1,7 +1,7 @@
- expanded = Rails.env.test?
- protocols = Gitlab::UrlSanitizer::ALLOWED_SCHEMES.join('|')
-%section.settings.project-mirror-settings.js-mirror-settings.no-animate#js-push-remote-settings{ class: ('expanded' if expanded) }
+%section.settings.project-mirror-settings.js-mirror-settings.no-animate.qa-mirroring-repositories-settings#js-push-remote-settings{ class: ('expanded' if expanded) }
.settings-header
%h4= _('Mirroring repositories')
%button.btn.js-settings-toggle
@@ -20,7 +20,7 @@
.form-group.has-feedback
= label_tag :url, _('Git repository URL'), class: 'label-light'
- = text_field_tag :url, nil, class: 'form-control js-mirror-url js-repo-url', placeholder: _('Input your repository URL'), required: true, pattern: "(#{protocols}):\/\/.+"
+ = text_field_tag :url, nil, class: 'form-control js-mirror-url js-repo-url qa-mirror-repository-url-input', placeholder: _('Input your repository URL'), required: true, pattern: "(#{protocols}):\/\/.+"
= render 'projects/mirrors/instructions'
@@ -32,7 +32,7 @@
= link_to icon('question-circle'), help_page_path('user/project/protected_branches')
.panel-footer
- = f.submit _('Mirror repository'), class: 'btn btn-success js-mirror-submit', name: :update_remote_mirror
+ = f.submit _('Mirror repository'), class: 'btn btn-success js-mirror-submit qa-mirror-repository-button', name: :update_remote_mirror
.panel.panel-default
.table-responsive
@@ -50,10 +50,10 @@
= render_if_exists 'projects/mirrors/table_pull_row'
- @project.remote_mirrors.each_with_index do |mirror, index|
- if mirror.enabled
- %tr
- %td= mirror.safe_url
+ %tr.qa-mirrored-repository-row
+ %td.qa-mirror-repository-url= mirror.safe_url
%td= _('Push')
- %td= mirror.last_update_at.present? ? time_ago_with_tooltip(mirror.last_update_at) : _('Never')
+ %td.qa-mirror-last-update-at= mirror.last_update_at.present? ? time_ago_with_tooltip(mirror.last_update_at) : _('Never')
%td
- if mirror.last_error.present?
.badge.mirror-error-badge{ data: { toggle: 'tooltip', html: 'true' }, title: html_escape(mirror.last_error.try(:strip)) }= _('Error')
diff --git a/app/views/projects/mirrors/_mirror_repos_form.html.haml b/app/views/projects/mirrors/_mirror_repos_form.html.haml
index a2cce83bfab..b49f1d9315e 100644
--- a/app/views/projects/mirrors/_mirror_repos_form.html.haml
+++ b/app/views/projects/mirrors/_mirror_repos_form.html.haml
@@ -1,5 +1,5 @@
.form-group
= label_tag :mirror_direction, _('Mirror direction'), class: 'label-light'
- = select_tag :mirror_direction, options_for_select([[_('Push'), 'push']]), class: 'form-control js-mirror-direction', disabled: true
+ = select_tag :mirror_direction, options_for_select([[_('Push'), 'push']]), class: 'form-control js-mirror-direction qa-mirror-direction', disabled: true
= render partial: "projects/mirrors/mirror_repos_push", locals: { f: f }
diff --git a/app/views/projects/project_members/_new_project_group.html.haml b/app/views/projects/project_members/_new_project_group.html.haml
index 74570769117..88e68f89024 100644
--- a/app/views/projects/project_members/_new_project_group.html.haml
+++ b/app/views/projects/project_members/_new_project_group.html.haml
@@ -10,7 +10,7 @@
= select_tag :link_group_access, options_for_select(ProjectGroupLink.access_options, ProjectGroupLink.default_access), class: "form-control select-control"
= icon('chevron-down')
.form-text.text-muted.append-bottom-10
- = link_to _("Read more"), help_page_path("user/permissions"), class: "vlink"
+ = link_to _("Read more"), help_page_path("user/permissions")
about role permissions
.form-group
= label_tag :expires_at, _('Access expiration date'), class: 'label-bold'
diff --git a/app/views/projects/project_members/_new_project_member.html.haml b/app/views/projects/project_members/_new_project_member.html.haml
index 5e21442bb60..1de7d9c6957 100644
--- a/app/views/projects/project_members/_new_project_member.html.haml
+++ b/app/views/projects/project_members/_new_project_member.html.haml
@@ -10,7 +10,7 @@
= select_tag :access_level, options_for_select(ProjectMember.access_level_roles, @project_member.access_level), class: "form-control project-access-select select-control"
= icon('chevron-down')
.form-text.text-muted.append-bottom-10
- = link_to "Read more", help_page_path("user/permissions"), class: "vlink"
+ = link_to "Read more", help_page_path("user/permissions")
about role permissions
.form-group
.clearable-input
diff --git a/app/views/projects/serverless/functions/index.html.haml b/app/views/projects/serverless/functions/index.html.haml
new file mode 100644
index 00000000000..f650fa0f38f
--- /dev/null
+++ b/app/views/projects/serverless/functions/index.html.haml
@@ -0,0 +1,15 @@
+- @no_container = true
+- @content_class = "limit-container-width" unless fluid_layout
+- breadcrumb_title 'Serverless'
+- page_title 'Serverless'
+- status_path = project_serverless_functions_path(@project, format: :json)
+- clusters_path = project_clusters_path(@project)
+
+.serverless-functions-page.js-serverless-functions-page{ data: { status_path: status_path, installed: @installed, clusters_path: clusters_path, help_path: help_page_path('user/project/clusters/serverless/index') } }
+
+%div{ class: [container_class, ('limit-container-width' unless fluid_layout)] }
+ .js-serverless-functions-notice
+ .flash-container
+
+ .top-area.adjust
+ .serverless-functions-table#js-serverless-functions
diff --git a/app/views/projects/settings/repository/show.html.haml b/app/views/projects/settings/repository/show.html.haml
index c14e95a382c..cb3a035c49e 100644
--- a/app/views/projects/settings/repository/show.html.haml
+++ b/app/views/projects/settings/repository/show.html.haml
@@ -13,3 +13,4 @@
= render "projects/protected_tags/index"
= render @deploy_keys
= render "projects/deploy_tokens/index"
+= render "projects/cleanup/show"
diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml
index f29ce4f5c06..c87a084740b 100644
--- a/app/views/projects/show.html.haml
+++ b/app/views/projects/show.html.haml
@@ -1,7 +1,6 @@
- @no_container = true
- breadcrumb_title _("Details")
- @content_class = "limit-container-width" unless fluid_layout
-- show_auto_devops_callout = show_auto_devops_callout?(@project)
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, project_path(@project, rss_url_options), title: "#{@project.name} activity")
@@ -15,20 +14,11 @@
%div{ class: [container_class, ("limit-container-width" unless fluid_layout)] }
= render "projects/last_push"
-= render "home_panel"
-
-- if can?(current_user, :download_code, @project)
- %nav.project-stats{ class: [container_class, ("limit-container-width" unless fluid_layout)] }
- .scrolling-tabs-container.inner-page-scroll-tabs.is-smaller
- .fade-left= icon('angle-left')
- .fade-right= icon('angle-right')
- .nav-links.scrolling-tabs.quick-links
- = render 'stat_anchor_list', anchors: @project.statistics_anchors(show_auto_devops_callout: show_auto_devops_callout)
- = render 'stat_anchor_list', anchors: @project.statistics_buttons(show_auto_devops_callout: show_auto_devops_callout)
+ = render "home_panel"
+ - if can?(current_user, :download_code, @project) && @project.repository_languages.present?
= repository_languages_bar(@project.repository_languages)
-%div{ class: [container_class, ("limit-container-width" unless fluid_layout)] }
- if @project.archived?
.text-warning.center.prepend-top-20
%p
@@ -41,4 +31,4 @@
= render 'shared/auto_devops_callout'
%div{ class: project_child_container_class(view_path) }
- = render view_path
+ = render view_path, is_project_overview: true
diff --git a/app/views/projects/snippets/show.html.haml b/app/views/projects/snippets/show.html.haml
index f495b4eaf30..da48cb207a4 100644
--- a/app/views/projects/snippets/show.html.haml
+++ b/app/views/projects/snippets/show.html.haml
@@ -6,7 +6,7 @@
= render 'shared/snippets/header'
.project-snippets
- %article.file-holder.snippet-file-content
+ %article.file-holder.snippet-file-content{ class: ('use-csslab' if Feature.enabled?(:csslab)) }
= render 'shared/snippets/blob'
.row-content-block.top-block.content-component-block
diff --git a/app/views/projects/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml
index 601e3f25852..4e9a119ac66 100644
--- a/app/views/projects/tree/_tree_header.html.haml
+++ b/app/views/projects/tree/_tree_header.html.haml
@@ -20,7 +20,7 @@
- if can_collaborate || can_create_mr_from_fork
%li.breadcrumb-item
- %a.btn.add-to-tree{ addtotree_toggle_attributes }
+ %a.btn.add-to-tree.qa-add-to-tree{ addtotree_toggle_attributes }
= sprite_icon('plus', size: 16, css_class: 'float-left')
= sprite_icon('arrow-down', size: 16, css_class: 'float-left')
- if on_top_of_branch?
@@ -30,7 +30,7 @@
%li.dropdown-header
#{ _('This directory') }
%li
- = link_to project_new_blob_path(@project, @id) do
+ = link_to project_new_blob_path(@project, @id), class: 'qa-new-file-option' do
#{ _('New file') }
%li
= link_to '#modal-upload-blob', { 'data-target' => '#modal-upload-blob', 'data-toggle' => 'modal' } do
@@ -85,4 +85,8 @@
= link_to ide_edit_path(@project, @ref, @path), class: 'btn btn-default qa-web-ide-button' do
= _('Web IDE')
+ - if show_xcode_link?(@project)
+ .project-action-button.project-xcode.inline
+ = render "projects/buttons/xcode_link"
+
= render 'projects/buttons/download', project: @project, ref: @ref
diff --git a/app/views/projects/wikis/show.html.haml b/app/views/projects/wikis/show.html.haml
index cc38ec12fd8..4d5fd55364c 100644
--- a/app/views/projects/wikis/show.html.haml
+++ b/app/views/projects/wikis/show.html.haml
@@ -26,7 +26,7 @@
= (s_("WikiHistoricalPage|You can view the %{most_recent_link} or browse the %{history_link}.") % { most_recent_link: most_recent_link, history_link: history_link }).html_safe
.prepend-top-default.append-bottom-default
- .wiki
+ .wiki.md{ class: ('use-csslab' if Feature.enabled?(:csslab)) }
= render_wiki_content(@page, legacy_render_context(params))
= render 'sidebar'
diff --git a/app/views/search/results/_blob.html.haml b/app/views/search/results/_blob.html.haml
index a8d4d4af93a..2a602095845 100644
--- a/app/views/search/results/_blob.html.haml
+++ b/app/views/search/results/_blob.html.haml
@@ -1,7 +1,7 @@
- project = find_project_for_result_blob(blob)
- return unless project
-- file_name, blob = parse_search_result(blob)
-- blob_link = project_blob_path(project, tree_join(blob.ref, file_name))
+- blob = parse_search_result(blob)
+- blob_link = project_blob_path(project, tree_join(blob.ref, blob.filename))
-= render partial: 'search/results/blob_data', locals: { blob: blob, project: project, file_name: file_name, blob_link: blob_link }
+= render partial: 'search/results/blob_data', locals: { blob: blob, project: project, file_name: blob.filename, blob_link: blob_link }
diff --git a/app/views/search/results/_wiki_blob.html.haml b/app/views/search/results/_wiki_blob.html.haml
index 4346217c230..389e4cc75b9 100644
--- a/app/views/search/results/_wiki_blob.html.haml
+++ b/app/views/search/results/_wiki_blob.html.haml
@@ -1,5 +1,5 @@
- project = find_project_for_result_blob(wiki_blob)
-- file_name, wiki_blob = parse_search_result(wiki_blob)
+- wiki_blob = parse_search_result(wiki_blob)
- wiki_blob_link = project_wiki_path(project, wiki_blob.basename)
-= render partial: 'search/results/blob_data', locals: { blob: wiki_blob, project: project, file_name: file_name, blob_link: wiki_blob_link }
+= render partial: 'search/results/blob_data', locals: { blob: wiki_blob, project: project, file_name: wiki_blob.filename, blob_link: wiki_blob_link }
diff --git a/app/views/shared/_mobile_clone_panel.html.haml b/app/views/shared/_mobile_clone_panel.html.haml
index 998985cabe1..b43662947a8 100644
--- a/app/views/shared/_mobile_clone_panel.html.haml
+++ b/app/views/shared/_mobile_clone_panel.html.haml
@@ -1,13 +1,13 @@
- project = project || @project
- ssh_copy_label = _("Copy SSH clone URL")
-- http_copy_label = _("Copy HTTPS clone URL")
+- http_copy_label = _('Copy %{http_label} clone URL') % { http_label: gitlab_config.protocol.upcase }
-.btn-group.mobile-git-clone.js-mobile-git-clone
- = clipboard_button(button_text: default_clone_label, target: '#project_clone', hide_button_icon: true, class: "input-group-text clone-dropdown-btn js-clone-dropdown-label btn btn-default")
- %button.btn.btn-default.dropdown-toggle.js-dropdown-toggle{ type: "button", data: { toggle: "dropdown" } }
- = icon("caret-down", class: "dropdown-btn-icon")
+.btn-group.mobile-git-clone.js-mobile-git-clone.btn-block
+ = clipboard_button(button_text: default_clone_label, text: default_url_to_repo(project), hide_button_icon: true, class: "btn-primary flex-fill bold justify-content-center input-group-text clone-dropdown-btn js-clone-dropdown-label")
+ %button.btn.btn-primary.dropdown-toggle.js-dropdown-toggle{ type: "button", data: { toggle: "dropdown" } }
+ = sprite_icon("arrow-down", css_class: "dropdown-btn-icon icon")
%ul.dropdown-menu.dropdown-menu-selectable.dropdown-menu-right.clone-options-dropdown{ data: { dropdown: true } }
%li
- = dropdown_item_with_description(ssh_copy_label, project.ssh_url_to_repo, href: project.ssh_url_to_repo, data: { clone_type: 'ssh' })
+ = dropdown_item_with_description(ssh_copy_label, project.ssh_url_to_repo, href: project.ssh_url_to_repo, data: { clone_type: 'ssh' }, default: true)
%li
= dropdown_item_with_description(http_copy_label, project.http_url_to_repo, href: project.http_url_to_repo, data: { clone_type: 'http' })
diff --git a/app/views/shared/_remote_mirror_update_button.html.haml b/app/views/shared/_remote_mirror_update_button.html.haml
index f32cff18fa8..721a2af8069 100644
--- a/app/views/shared/_remote_mirror_update_button.html.haml
+++ b/app/views/shared/_remote_mirror_update_button.html.haml
@@ -2,5 +2,5 @@
%button.btn.disabled{ type: 'button', data: { toggle: 'tooltip', container: 'body' }, title: _('Updating') }
= icon("refresh spin")
- else
- = link_to update_now_project_mirror_path(@project, sync_remote: true), method: :post, class: "btn", data: { toggle: 'tooltip', container: 'body' }, title: _('Update now') do
+ = link_to update_now_project_mirror_path(@project, sync_remote: true), method: :post, class: "btn qa-update-now-button", data: { toggle: 'tooltip', container: 'body' }, title: _('Update now') do
= icon("refresh")
diff --git a/app/views/shared/_sort_dropdown.html.haml b/app/views/shared/_sort_dropdown.html.haml
deleted file mode 100644
index e4463c1e0d8..00000000000
--- a/app/views/shared/_sort_dropdown.html.haml
+++ /dev/null
@@ -1,16 +0,0 @@
-- sorted_by = sort_options_hash[@sort]
-- viewing_issues = controller.controller_name == 'issues' || controller.action_name == 'issues'
-
-.dropdown.inline.prepend-left-10
- %button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown', display: 'static' } }
- = sorted_by
- = icon('chevron-down')
- %ul.dropdown-menu.dropdown-menu-right.dropdown-menu-selectable.dropdown-menu-sort
- %li
- = sortable_item(sort_title_priority, page_filter_path(sort: sort_value_priority, label: true), sorted_by)
- = sortable_item(sort_title_created_date, page_filter_path(sort: sort_value_created_date, label: true), sorted_by)
- = sortable_item(sort_title_recently_updated, page_filter_path(sort: sort_value_recently_updated, label: true), sorted_by)
- = sortable_item(sort_title_milestone, page_filter_path(sort: sort_value_milestone, label: true), sorted_by)
- = sortable_item(sort_title_due_date, page_filter_path(sort: sort_value_due_date, label: true), sorted_by) if viewing_issues
- = sortable_item(sort_title_popularity, page_filter_path(sort: sort_value_popularity, label: true), sorted_by)
- = sortable_item(sort_title_label_priority, page_filter_path(sort: sort_value_label_priority, label: true), sorted_by)
diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml
new file mode 100644
index 00000000000..2ca4657851c
--- /dev/null
+++ b/app/views/shared/issuable/_filter.html.haml
@@ -0,0 +1,32 @@
+.issues-filters
+ .issues-details-filters.row-content-block.second-block
+ = form_tag page_filter_path(without: [:assignee_id, :author_id, :milestone_title, :label_name, :search]), method: :get, class: 'filter-form js-filter-form' do
+ - if params[:search].present?
+ = hidden_field_tag :search, params[:search]
+ .issues-other-filters
+ .filter-item.inline
+ - if params[:author_id].present?
+ = hidden_field_tag(:author_id, params[:author_id])
+ = dropdown_tag(user_dropdown_label(params[:author_id], "Author"), options: { toggle_class: "js-user-search js-filter-submit js-author-search", title: "Filter by author", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-author js-filter-submit",
+ placeholder: "Search authors", data: { any_user: "Any Author", first_user: current_user&.username, current_user: true, project_id: @project&.id, group_id: @group&.id, selected: params[:author_id], field_name: "author_id", default_label: "Author" } })
+
+ .filter-item.inline
+ - if params[:assignee_id].present?
+ = hidden_field_tag(:assignee_id, params[:assignee_id])
+ = dropdown_tag(user_dropdown_label(params[:assignee_id], "Assignee"), options: { toggle_class: "js-user-search js-filter-submit js-assignee-search", title: "Filter by assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee js-filter-submit",
+ placeholder: "Search assignee", data: { any_user: "Any Assignee", first_user: current_user&.username, null_user: true, current_user: true, project_id: @project&.id, group_id: @group&.id, selected: params[:assignee_id], field_name: "assignee_id", default_label: "Assignee" } })
+
+ .filter-item.inline.milestone-filter
+ = render "shared/issuable/milestone_dropdown", selected: finder.milestones.try(:first), name: :milestone_title, show_any: true, show_upcoming: true, show_started: true
+
+ .filter-item.inline.labels-filter
+ = render "shared/issuable/label_dropdown", selected: selected_labels, use_id: false, selected_toggle: params[:label_name], data_options: { field_name: "label_name[]" }
+
+ - unless @no_filters_set
+ .float-right
+ = render 'shared/issuable/sort_dropdown'
+
+ - has_labels = @labels && @labels.any?
+ .row-content-block.second-block.filtered-labels{ class: ("hidden" unless has_labels) }
+ - if has_labels
+ = render 'shared/labels_row', labels: @labels
diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml
index 7c5af0b9775..46634693067 100644
--- a/app/views/shared/issuable/_search_bar.html.haml
+++ b/app/views/shared/issuable/_search_bar.html.haml
@@ -2,7 +2,6 @@
- board = local_assigns.fetch(:board, nil)
- block_css_class = type != :boards_modal ? 'row-content-block second-block' : ''
- user_can_admin_list = board && can?(current_user, :admin_list, board.parent)
-- show_sorting_dropdown = local_assigns.fetch(:show_sorting_dropdown, true)
.issues-filters
.issues-details-filters.filtered-search-block{ class: block_css_class, "v-pre" => type == :boards_modal }
@@ -142,5 +141,5 @@
- if @project
#js-add-issues-btn.prepend-left-10{ data: { can_admin_list: can?(current_user, :admin_list, @project) } }
#js-toggle-focus-btn
- - elsif show_sorting_dropdown
- = render 'shared/sort_dropdown'
+ - elsif type != :boards_modal
+ = render 'shared/issuable/sort_dropdown'
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 5295e656ab0..9eecfa39390 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -16,7 +16,7 @@
- if current_user
.block.todo.hide-expanded
= render "shared/issuable/sidebar_todo", todo: todo, issuable: issuable, is_collapsed: true
- .block.assignee
+ .block.assignee.qa-assignee-block
= render "shared/issuable/sidebar_assignees", issuable: issuable, can_edit_issuable: can_edit_issuable, signed_in: current_user.present?
= render_if_exists 'shared/issuable/sidebar_item_epic', issuable: issuable
diff --git a/app/views/shared/issuable/_sort_dropdown.html.haml b/app/views/shared/issuable/_sort_dropdown.html.haml
new file mode 100644
index 00000000000..c211b9fcaa2
--- /dev/null
+++ b/app/views/shared/issuable/_sort_dropdown.html.haml
@@ -0,0 +1,20 @@
+- sort_value = @sort
+- sort_title = issuable_sort_option_title(sort_value)
+- viewing_issues = controller.controller_name == 'issues' || controller.action_name == 'issues'
+
+.dropdown.inline.prepend-left-10.issue-sort-dropdown
+ .btn-group{ role: 'group' }
+ .btn-group{ role: 'group' }
+ %button.dropdown-toggle{ type: 'button', data: { toggle: 'dropdown', display: 'static' }, class: 'btn btn-default' }
+ = sort_title
+ = icon('chevron-down')
+ %ul.dropdown-menu.dropdown-menu-right.dropdown-menu-selectable.dropdown-menu-sort
+ %li
+ = sortable_item(sort_title_priority, page_filter_path(sort: sort_value_priority, label: true), sort_title)
+ = sortable_item(sort_title_created_date, page_filter_path(sort: sort_value_created_date, label: true), sort_title)
+ = sortable_item(sort_title_recently_updated, page_filter_path(sort: sort_value_recently_updated, label: true), sort_title)
+ = sortable_item(sort_title_milestone, page_filter_path(sort: sort_value_milestone, label: true), sort_title)
+ = sortable_item(sort_title_due_date, page_filter_path(sort: sort_value_due_date, label: true), sort_title) if viewing_issues
+ = sortable_item(sort_title_popularity, page_filter_path(sort: sort_value_popularity, label: true), sort_title)
+ = sortable_item(sort_title_label_priority, page_filter_path(sort: sort_value_label_priority, label: true), sort_title)
+ = issuable_sort_direction_button(sort_value)
diff --git a/app/views/shared/issuable/form/_metadata_merge_request_assignee.html.haml b/app/views/shared/issuable/form/_metadata_merge_request_assignee.html.haml
index 3521f71f409..60c34094108 100644
--- a/app/views/shared/issuable/form/_metadata_merge_request_assignee.html.haml
+++ b/app/views/shared/issuable/form/_metadata_merge_request_assignee.html.haml
@@ -5,4 +5,4 @@
= dropdown_tag(user_dropdown_label(issuable.assignee_id, "Assignee"), options: { toggle_class: "js-dropdown-keep-input js-user-search js-issuable-form-dropdown js-assignee-search", title: "Select assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee js-filter-submit",
placeholder: "Search assignee", data: { first_user: current_user.try(:username), null_user: true, current_user: true, project_id: issuable.project.try(:id), selected: issuable.assignee_id, field_name: "#{issuable.class.model_name.param_key}[assignee_id]", default_label: "Assignee"} })
- = link_to 'Assign to me', '#', class: "assign-to-me-link #{'hide' if issuable.assignee_id == current_user.id}"
+ = link_to 'Assign to me', '#', class: "assign-to-me-link qa-assign-to-me-link #{'hide' if issuable.assignee_id == current_user.id}"
diff --git a/app/views/shared/members/_access_request_links.html.haml b/app/views/shared/members/_access_request_links.html.haml
new file mode 100644
index 00000000000..f7227b9101e
--- /dev/null
+++ b/app/views/shared/members/_access_request_links.html.haml
@@ -0,0 +1,17 @@
+- model_name = source.model_name.to_s.downcase
+
+- if can?(current_user, :"destroy_#{model_name}_member", source.members.find_by(user_id: current_user.id)) # rubocop: disable CodeReuse/ActiveRecord
+ - link_text = source.is_a?(Group) ? _('Leave group') : _('Leave project')
+ = link_to link_text, polymorphic_path([:leave, source, :members]),
+ method: :delete,
+ data: { confirm: leave_confirmation_message(source) },
+ class: 'access-request-link'
+- elsif requester = source.requesters.find_by(user_id: current_user.id) # rubocop: disable CodeReuse/ActiveRecord
+ = link_to _('Withdraw Access Request'), polymorphic_path([:leave, source, :members]),
+ method: :delete,
+ data: { confirm: remove_member_message(requester) },
+ class: 'access-request-link'
+- elsif source.request_access_enabled && can?(current_user, :request_access, source)
+ = link_to _('Request Access'), polymorphic_path([:request_access, source, :members]),
+ method: :post,
+ class: 'access-request-link'
diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml
index a7fd75d85d7..6b3841ebbc4 100644
--- a/app/views/shared/members/_member.html.haml
+++ b/app/views/shared/members/_member.html.haml
@@ -75,7 +75,7 @@
= dropdown_title(_("Change permissions"))
.dropdown-content
%ul
- - member.access_level_roles.each do |role, role_id|
+ - member.valid_level_roles.each do |role, role_id|
%li
= link_to role, "javascript:void(0)",
class: ("is-active" if member.access_level == role_id),
diff --git a/app/views/shared/notes/_note.html.haml b/app/views/shared/notes/_note.html.haml
index bc918430823..e125d7f108a 100644
--- a/app/views/shared/notes/_note.html.haml
+++ b/app/views/shared/notes/_note.html.haml
@@ -5,7 +5,7 @@
- note_editable = can?(current_user, :admin_note, note)
- note_counter = local_assigns.fetch(:note_counter, 0)
-%li.timeline-entry.note-wrapper.outlined{ id: dom_id(note),
+%li.timeline-entry.note-wrapper{ id: dom_id(note),
class: ["note", "note-row-#{note.id}", ('system-note' if note.system)],
data: { author_id: note.author.id,
editable: note_editable,
diff --git a/app/views/shared/notifications/_button.html.haml b/app/views/shared/notifications/_button.html.haml
index f6c7ca70ebd..30860988bbb 100644
--- a/app/views/shared/notifications/_button.html.haml
+++ b/app/views/shared/notifications/_button.html.haml
@@ -1,3 +1,5 @@
+- btn_class = local_assigns.fetch(:btn_class, nil)
+
- if notification_setting
.js-notification-dropdown.notification-dropdown.project-action-button.dropdown.inline
= form_for notification_setting, remote: true, html: { class: "inline notification-form" } do |f|
@@ -6,14 +8,14 @@
.js-notification-toggle-btns
%div{ class: ("btn-group" if notification_setting.custom?) }
- if notification_setting.custom?
- %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: "Notification setting", "aria-label" => "Notification setting: #{notification_title(notification_setting.level)}", data: { container: "body", toggle: "modal", target: "#" + notifications_menu_identifier("modal", notification_setting), display: 'static' } }
+ %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: _("Notification setting"), class: "#{btn_class}", "aria-label" => _("Notification setting - %{notification_title}") % { notification_title: notification_title(notification_setting.level) }, data: { container: "body", toggle: "modal", target: "#" + notifications_menu_identifier("modal", notification_setting), display: 'static' } }
= icon("bell", class: "js-notification-loading")
= notification_title(notification_setting.level)
%button.btn.dropdown-toggle{ data: { toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" } }
= icon('caret-down')
.sr-only Toggle dropdown
- else
- %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: "Notification setting", "aria-label" => "Notification setting: #{notification_title(notification_setting.level)}", data: { container: "body", toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" } }
+ %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: "Notification setting", class: "#{btn_class}", "aria-label" => "Notification setting: #{notification_title(notification_setting.level)}", data: { container: "body", toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" } }
= icon("bell", class: "js-notification-loading")
= notification_title(notification_setting.level)
= icon("caret-down")
diff --git a/app/views/shared/projects/_list.html.haml b/app/views/shared/projects/_list.html.haml
index 06eb3d03e31..15c29e14cc0 100644
--- a/app/views/shared/projects/_list.html.haml
+++ b/app/views/shared/projects/_list.html.haml
@@ -2,24 +2,29 @@
- avatar = true unless local_assigns[:avatar] == false
- use_creator_avatar = false unless local_assigns[:use_creator_avatar] == true
- stars = true unless local_assigns[:stars] == false
-- forks = false unless local_assigns[:forks] == true
+- forks = true unless local_assigns[:forks] == false
+- merge_requests = true unless local_assigns[:merge_requests] == false
+- issues = true unless local_assigns[:issues] == false
+- pipeline_status = true unless local_assigns[:pipeline_status] == false
- ci = false unless local_assigns[:ci] == true
- skip_namespace = false unless local_assigns[:skip_namespace] == true
- user = local_assigns[:user]
- show_last_commit_as_description = false unless local_assigns[:show_last_commit_as_description] == true
- remote = false unless local_assigns[:remote] == true
- skip_pagination = false unless local_assigns[:skip_pagination] == true
+- compact_mode = false unless local_assigns[:compact_mode] == true
+- css_classes = "#{'compact' if compact_mode} #{'explore' if explore_projects_tab?}"
.js-projects-list-holder
- if any_projects?(projects)
- load_pipeline_status(projects)
-
- %ul.projects-list
+ %ul.projects-list{ class: css_classes }
- projects.each_with_index do |project, i|
- css_class = (i >= projects_limit) || project.pending_delete? ? 'hide' : nil
= render "shared/projects/project", project: project, skip_namespace: skip_namespace,
avatar: avatar, stars: stars, css_class: css_class, ci: ci, use_creator_avatar: use_creator_avatar,
- forks: forks, show_last_commit_as_description: show_last_commit_as_description, user: user
+ forks: forks, show_last_commit_as_description: show_last_commit_as_description, user: user, merge_requests: merge_requests,
+ issues: issues, pipeline_status: pipeline_status, compact_mode: compact_mode
- if @private_forks_count && @private_forks_count > 0
%li.project-row.private-forks-notice
diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml
index aba790e1217..9dde77fccef 100644
--- a/app/views/shared/projects/_project.html.haml
+++ b/app/views/shared/projects/_project.html.haml
@@ -1,62 +1,107 @@
- avatar = true unless local_assigns[:avatar] == false
- stars = true unless local_assigns[:stars] == false
-- forks = false unless local_assigns[:forks] == true
+- forks = true unless local_assigns[:forks] == false
+- merge_requests = true unless local_assigns[:merge_requests] == false
+- issues = true unless local_assigns[:issues] == false
+- pipeline_status = true unless local_assigns[:pipeline_status] == false
- skip_namespace = false unless local_assigns[:skip_namespace] == true
- access = max_project_member_access(project)
-- css_class = '' unless local_assigns[:css_class]
+- compact_mode = false unless local_assigns[:compact_mode] == true
- show_last_commit_as_description = false unless local_assigns[:show_last_commit_as_description] == true && can_show_last_commit_in_list?(project)
+- css_class = '' unless local_assigns[:css_class]
- css_class += " no-description" if project.description.blank? && !show_last_commit_as_description
- cache_key = project_list_cache_key(project)
- updated_tooltip = time_ago_with_tooltip(project.last_activity_date)
+- css_details_class = compact_mode ? "d-flex flex-column flex-sm-row flex-md-row align-items-sm-center" : "align-items-center flex-md-fill flex-lg-column d-sm-flex d-lg-block"
+- css_controls_class = compact_mode ? "" : "align-items-md-end align-items-lg-center flex-lg-row"
-%li.project-row{ class: css_class }
+%li.project-row.d-flex{ class: css_class }
= cache(cache_key) do
- if avatar
- .avatar-container.s40
+ .avatar-container.s64.flex-grow-0.flex-shrink-0
= link_to project_path(project), class: dom_class(project) do
- if project.creator && use_creator_avatar
- = image_tag avatar_icon_for_user(project.creator, 40), class: "avatar s40", alt:''
+ = image_tag avatar_icon_for_user(project.creator, 64), class: "avatar s65", alt:''
- else
- = project_icon(project, alt: '', class: 'avatar project-avatar s40', width: 40, height: 40)
- .project-details
- %h3.prepend-top-0.append-bottom-0
- = link_to project_path(project), class: 'text-plain' do
- %span.project-full-name><
- %span.namespace-name
- - if project.namespace && !skip_namespace
- = project.namespace.human_name
- \/
- %span.project-name<
- = project.name
-
- - if access&.nonzero?
- -# haml-lint:disable UnnecessaryStringOutput
- = ' ' # prevent haml from eating the space between elements
- %span.user-access-role= Gitlab::Access.human_access(access)
-
- - if show_last_commit_as_description
- .description.prepend-top-5
- = link_to_markdown(project.commit.title, project_commit_path(project, project.commit), class: "commit-row-message")
- - elsif project.description.present?
- .description.prepend-top-5
- = markdown_field(project, :description)
-
- .controls
- .prepend-top-0
- - if project.archived
- %span.prepend-left-10.badge.badge-warning archived
- - if can?(current_user, :read_cross_project) && project.pipeline_status.has_status?
- %span.prepend-left-10
- = render_project_pipeline_status(project.pipeline_status)
- - if forks
- %span.prepend-left-10
- = sprite_icon('fork', size: 12)
- = number_with_delimiter(project.forks_count)
- - if stars
- %span.prepend-left-10
- = icon('star')
- = number_with_delimiter(project.star_count)
- %span.prepend-left-10.visibility-icon.has-tooltip{ data: { container: 'body', placement: 'left' }, title: visibility_icon_description(project) }
- = visibility_level_icon(project.visibility_level, fw: true)
- .prepend-top-0
- updated #{updated_tooltip}
+ = project_icon(project, alt: '', class: 'avatar project-avatar s64', width: 64, height: 64)
+ .project-details.flex-sm-fill{ class: css_details_class }
+ .flex-wrapper.flex-fill
+ .d-flex.align-items-center.flex-wrap
+ %h2.d-flex.prepend-top-8
+ = link_to project_path(project), class: 'text-plain' do
+ %span.project-full-name.append-right-8><
+ %span.namespace-name
+ - if project.namespace && !skip_namespace
+ = project.namespace.human_name
+ \/
+ %span.project-name<
+ = project.name
+
+ %span.metadata-info.visibility-icon.append-right-10.prepend-top-8.has-tooltip{ data: { container: 'body', placement: 'top' }, title: visibility_icon_description(project) }
+ = visibility_level_icon(project.visibility_level, fw: true)
+
+ - if explore_projects_tab? && project.repository.license
+ %span.metadata-info.d-inline-flex.align-items-center.append-right-10.prepend-top-8
+ = sprite_icon('scale', size: 14, css_class: 'append-right-4')
+ = project.repository.license.name
+
+ - if !explore_projects_tab? && access&.nonzero?
+ -# haml-lint:disable UnnecessaryStringOutput
+ = ' ' # prevent haml from eating the space between elements
+ .metadata-info.prepend-top-8
+ %span.user-access-role.d-block= Gitlab::Access.human_access(access)
+
+ - if show_last_commit_as_description
+ .description.d-none.d-sm-block.prepend-top-8.append-right-default
+ = link_to_markdown(project.commit.title, project_commit_path(project, project.commit), class: "commit-row-message")
+ - elsif project.description.present?
+ .description.d-none.d-sm-block.prepend-top-8.append-right-default
+ = markdown_field(project, :description)
+
+ .controls.d-flex.flex-row.flex-sm-column.flex-md-column.align-items-center.align-items-sm-end.flex-wrap.flex-shrink-0{ class: css_controls_class }
+ .icon-container.d-flex.align-items-center
+ - if project.archived
+ %span.d-flex.icon-wrapper.badge.badge-warning archived
+ - if stars
+ %span.d-flex.align-items-center.icon-wrapper.stars.has-tooltip{ data: { container: 'body', placement: 'top' }, title: _('Stars') }
+ = sprite_icon('star', size: 14, css_class: 'append-right-4')
+ = number_with_delimiter(project.star_count)
+ - if forks
+ = link_to project_forks_path(project),
+ class: "align-items-center icon-wrapper forks has-tooltip",
+ title: _('Forks'), data: { container: 'body', placement: 'top' } do
+ = sprite_icon('fork', size: 14, css_class: 'append-right-4')
+ = number_with_delimiter(project.forks_count)
+ - if show_merge_request_count?(merge_requests, compact_mode)
+ = link_to project_merge_requests_path(project),
+ class: "d-none d-lg-flex align-items-center icon-wrapper merge-requests has-tooltip",
+ title: _('Merge Requests'), data: { container: 'body', placement: 'top' } do
+ = sprite_icon('git-merge', size: 14, css_class: 'append-right-4')
+ = number_with_delimiter(project.open_merge_requests_count)
+ - if show_issue_count?(issues, compact_mode)
+ = link_to project_issues_path(project),
+ class: "d-none d-lg-flex align-items-center icon-wrapper issues has-tooltip",
+ title: _('Issues'), data: { container: 'body', placement: 'top' } do
+ = sprite_icon('issues', size: 14, css_class: 'append-right-4')
+ = number_with_delimiter(project.open_issues_count)
+ - if pipeline_status && can?(current_user, :read_cross_project) && project.pipeline_status.has_status?
+ %span.icon-wrapper.pipeline-status
+ = render_project_pipeline_status(project.pipeline_status, tooltip_placement: 'top')
+ .updated-note
+ %span Updated #{updated_tooltip}
+
+ .d-none.d-lg-flex.align-item-stretch
+ - unless compact_mode
+ - if current_user
+ %button.star-button.btn.btn-default.d-flex.align-items-center.star-btn.toggle-star{ type: "button", data: { endpoint: toggle_star_project_path(project, :json) } }
+ - if current_user.starred?(project)
+ = sprite_icon('star', { css_class: 'icon' })
+ %span.starred= s_('ProjectOverview|Unstar')
+ - else
+ = sprite_icon('star-o', { css_class: 'icon' })
+ %span= s_('ProjectOverview|Star')
+
+ - else
+ = link_to new_user_session_path, class: 'btn btn-default has-tooltip count-badge-button d-flex align-items-center star-btn', title: s_('ProjectOverview|You must sign in to star a project') do
+ = sprite_icon('star-o', { css_class: 'icon' })
+ %span= s_('ProjectOverview|Star')
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index d11476738e4..dd2cd36eac2 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -31,12 +31,12 @@
data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= icon('users')
- .profile-header
+ .profile-header{ class: [('with-no-profile-tabs' if profile_tabs.empty?)] }
.avatar-holder
= link_to avatar_icon_for_user(@user, 400), target: '_blank', rel: 'noopener noreferrer' do
= image_tag avatar_icon_for_user(@user, 90), class: "avatar s90", alt: ''
- .user-info.prepend-left-default.append-right-default
+ .user-info
.cover-title
= @user.name
@@ -81,10 +81,10 @@
= icon('briefcase')
= @user.organization
- - if @user.bio.present?
- .cover-desc
- %p.profile-user-bio
- = @user.bio
+ - if @user.bio.present?
+ .cover-desc
+ %p.profile-user-bio
+ = @user.bio
- unless profile_tabs.empty?
.scrolling-tabs-container
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index e51da79c6b5..bc26b3f8ef2 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -10,7 +10,6 @@
- cronjob:prune_old_events
- cronjob:remove_expired_group_links
- cronjob:remove_expired_members
-- cronjob:remove_old_web_hook_logs
- cronjob:remove_unreferenced_lfs_objects
- cronjob:repository_archive_cache
- cronjob:repository_check_dispatch
@@ -86,6 +85,10 @@
- todos_destroyer:todos_destroyer_project_private
- todos_destroyer:todos_destroyer_private_features
+- object_pool:object_pool_create
+- object_pool:object_pool_schedule_join
+- object_pool:object_pool_join
+
- default
- mailers # ActionMailer::DeliveryJob.queue_name
@@ -121,6 +124,7 @@
- propagate_service_template
- reactive_caching
- rebase
+- remote_mirror_notification
- repository_fork
- repository_import
- repository_remove_remote
@@ -133,3 +137,5 @@
- create_note_diff_file
- delete_diff_files
- detect_repository_languages
+- repository_cleanup
+- delete_stored_files
diff --git a/app/workers/concerns/object_pool_queue.rb b/app/workers/concerns/object_pool_queue.rb
new file mode 100644
index 00000000000..5b648df9c72
--- /dev/null
+++ b/app/workers/concerns/object_pool_queue.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+##
+# Concern for setting Sidekiq settings for the various ObjectPool queues
+#
+module ObjectPoolQueue
+ extend ActiveSupport::Concern
+
+ included do
+ queue_namespace :object_pool
+ end
+end
diff --git a/app/workers/delete_stored_files_worker.rb b/app/workers/delete_stored_files_worker.rb
new file mode 100644
index 00000000000..ff7931849d8
--- /dev/null
+++ b/app/workers/delete_stored_files_worker.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+class DeleteStoredFilesWorker
+ include ApplicationWorker
+
+ def perform(class_name, keys)
+ klass = begin
+ class_name.constantize
+ rescue NameError
+ nil
+ end
+
+ unless klass
+ message = "Unknown class '#{class_name}'"
+ logger.error(message)
+ Gitlab::Sentry.track_exception(RuntimeError.new(message))
+ return
+ end
+
+ klass.new(logger: logger).delete_keys(keys)
+ end
+end
diff --git a/app/workers/git_garbage_collect_worker.rb b/app/workers/git_garbage_collect_worker.rb
index 2d381c6fd6c..d3628b23189 100644
--- a/app/workers/git_garbage_collect_worker.rb
+++ b/app/workers/git_garbage_collect_worker.rb
@@ -28,6 +28,8 @@ class GitGarbageCollectWorker
# Refresh the branch cache in case garbage collection caused a ref lookup to fail
flush_ref_caches(project) if task == :gc
+ project.repository.expire_statistics_caches
+
# In case pack files are deleted, release libgit2 cache and open file
# descriptors ASAP instead of waiting for Ruby garbage collection
project.cleanup
diff --git a/app/workers/new_note_worker.rb b/app/workers/new_note_worker.rb
index 42f5b945a75..98f9f45e608 100644
--- a/app/workers/new_note_worker.rb
+++ b/app/workers/new_note_worker.rb
@@ -8,11 +8,18 @@ class NewNoteWorker
# rubocop: disable CodeReuse/ActiveRecord
def perform(note_id, _params = {})
if note = Note.find_by(id: note_id)
- NotificationService.new.new_note(note)
+ NotificationService.new.new_note(note) unless skip_notification?(note)
Notes::PostProcessService.new(note).execute
else
Rails.logger.error("NewNoteWorker: couldn't find note with ID=#{note_id}, skipping job")
end
end
+
+ private
+
+ # EE-only method
+ def skip_notification?(note)
+ false
+ end
# rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/workers/object_pool/create_worker.rb b/app/workers/object_pool/create_worker.rb
new file mode 100644
index 00000000000..135b99886dc
--- /dev/null
+++ b/app/workers/object_pool/create_worker.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+module ObjectPool
+ class CreateWorker
+ include ApplicationWorker
+ include ObjectPoolQueue
+ include ExclusiveLeaseGuard
+
+ attr_reader :pool
+
+ def perform(pool_id)
+ @pool = PoolRepository.find_by_id(pool_id)
+ return unless pool
+
+ try_obtain_lease do
+ perform_pool_creation
+ end
+ end
+
+ private
+
+ def perform_pool_creation
+ return unless pool.failed? || pool.scheduled?
+
+ # If this is a retry and the previous execution failed, deletion will
+ # bring the pool back to a pristine state
+ pool.delete_object_pool if pool.failed?
+
+ pool.create_object_pool
+ pool.mark_ready
+ rescue => e
+ pool.mark_failed
+ raise e
+ end
+
+ def lease_key
+ "object_pool:create:#{pool.id}"
+ end
+
+ def lease_timeout
+ 1.hour
+ end
+ end
+end
diff --git a/app/workers/object_pool/join_worker.rb b/app/workers/object_pool/join_worker.rb
new file mode 100644
index 00000000000..07676011b2a
--- /dev/null
+++ b/app/workers/object_pool/join_worker.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module ObjectPool
+ class JoinWorker
+ include ApplicationWorker
+ include ObjectPoolQueue
+
+ def perform(pool_id, project_id)
+ pool = PoolRepository.find_by_id(pool_id)
+ return unless pool&.joinable?
+
+ project = Project.find_by_id(project_id)
+ return unless project
+
+ pool.link_repository(project.repository)
+
+ Projects::HousekeepingService.new(project).execute
+ end
+ end
+end
diff --git a/app/workers/object_pool/schedule_join_worker.rb b/app/workers/object_pool/schedule_join_worker.rb
new file mode 100644
index 00000000000..647a8b72435
--- /dev/null
+++ b/app/workers/object_pool/schedule_join_worker.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module ObjectPool
+ class ScheduleJoinWorker
+ include ApplicationWorker
+ include ObjectPoolQueue
+
+ def perform(pool_id)
+ pool = PoolRepository.find_by_id(pool_id)
+ return unless pool&.joinable?
+
+ pool.member_projects.find_each do |project|
+ next if project.forked? && !project.import_finished?
+
+ ObjectPool::JoinWorker.perform_async(pool.id, project.id)
+ end
+ end
+ end
+end
diff --git a/app/workers/remote_mirror_notification_worker.rb b/app/workers/remote_mirror_notification_worker.rb
new file mode 100644
index 00000000000..70c2e857d09
--- /dev/null
+++ b/app/workers/remote_mirror_notification_worker.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class RemoteMirrorNotificationWorker
+ include ApplicationWorker
+
+ def perform(remote_mirror_id)
+ remote_mirror = RemoteMirrorFinder.new(id: remote_mirror_id).execute
+
+ # We check again if there's an error because a newer run since this job was
+ # fired could've completed successfully.
+ return unless remote_mirror && remote_mirror.last_error.present?
+
+ NotificationService.new.remote_mirror_update_failed(remote_mirror)
+ end
+end
diff --git a/app/workers/remove_old_web_hook_logs_worker.rb b/app/workers/remove_old_web_hook_logs_worker.rb
deleted file mode 100644
index 0f486f8991d..00000000000
--- a/app/workers/remove_old_web_hook_logs_worker.rb
+++ /dev/null
@@ -1,14 +0,0 @@
-# frozen_string_literal: true
-
-class RemoveOldWebHookLogsWorker
- include ApplicationWorker
- include CronjobQueue
-
- WEB_HOOK_LOG_LIFETIME = 2.days
-
- # rubocop: disable DestroyAll
- def perform
- WebHookLog.destroy_all(['created_at < ?', Time.now - WEB_HOOK_LOG_LIFETIME])
- end
- # rubocop: enable DestroyAll
-end
diff --git a/app/workers/repository_cleanup_worker.rb b/app/workers/repository_cleanup_worker.rb
new file mode 100644
index 00000000000..aa26c173a72
--- /dev/null
+++ b/app/workers/repository_cleanup_worker.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+class RepositoryCleanupWorker
+ include ApplicationWorker
+
+ sidekiq_options retry: 3
+
+ sidekiq_retries_exhausted do |msg, err|
+ next if err.is_a?(ActiveRecord::RecordNotFound)
+
+ args = msg['args'] + [msg['error_message']]
+
+ new.perform_failure(*args)
+ end
+
+ def perform(project_id, user_id)
+ project = Project.find(project_id)
+ user = User.find(user_id)
+
+ Projects::CleanupService.new(project, user).execute
+
+ notification_service.repository_cleanup_success(project, user)
+ end
+
+ def perform_failure(project_id, user_id, error)
+ project = Project.find(project_id)
+ user = User.find(user_id)
+
+ # Ensure the file is removed
+ project.bfg_object_map.remove!
+ notification_service.repository_cleanup_failure(project, user, error)
+ end
+
+ private
+
+ def notification_service
+ @notification_service ||= NotificationService.new
+ end
+end
diff --git a/app/workers/repository_update_remote_mirror_worker.rb b/app/workers/repository_update_remote_mirror_worker.rb
index 9d4e67deb9c..c0bae08ba85 100644
--- a/app/workers/repository_update_remote_mirror_worker.rb
+++ b/app/workers/repository_update_remote_mirror_worker.rb
@@ -5,7 +5,6 @@ class RepositoryUpdateRemoteMirrorWorker
UpdateError = Class.new(StandardError)
include ApplicationWorker
- include Gitlab::ShellAdapter
sidekiq_options retry: 3, dead: false
@@ -16,7 +15,7 @@ class RepositoryUpdateRemoteMirrorWorker
end
def perform(remote_mirror_id, scheduled_time)
- remote_mirror = RemoteMirror.find(remote_mirror_id)
+ remote_mirror = RemoteMirrorFinder.new(id: remote_mirror_id).execute
return if remote_mirror.updated_since?(scheduled_time)
raise UpdateAlreadyInProgressError if remote_mirror.update_in_progress?
diff --git a/changelogs/unreleased/19376-post-bfg-cleanup.yml b/changelogs/unreleased/19376-post-bfg-cleanup.yml
new file mode 100644
index 00000000000..fc1bcc30db9
--- /dev/null
+++ b/changelogs/unreleased/19376-post-bfg-cleanup.yml
@@ -0,0 +1,5 @@
+---
+title: Use BFG object maps to clean projects
+merge_request: 23189
+author:
+type: added
diff --git a/changelogs/unreleased/20422-hide-ui-variables-by-default.yml b/changelogs/unreleased/20422-hide-ui-variables-by-default.yml
new file mode 100644
index 00000000000..60285d49718
--- /dev/null
+++ b/changelogs/unreleased/20422-hide-ui-variables-by-default.yml
@@ -0,0 +1,6 @@
+---
+title: Pipeline trigger variable values are hidden in the UI by default. Maintainers
+ have the option to reveal them.
+merge_request: 23518
+author: jhampton
+type: added
diff --git a/changelogs/unreleased/22548-reopen-error-message.yml b/changelogs/unreleased/22548-reopen-error-message.yml
new file mode 100644
index 00000000000..79c20eccb12
--- /dev/null
+++ b/changelogs/unreleased/22548-reopen-error-message.yml
@@ -0,0 +1,6 @@
+---
+title: Show error message when attempting to reopen an MR and there is an open MR
+ for the same branch
+merge_request: 16447
+author: Akos Gyimesi
+type: fixed
diff --git a/changelogs/unreleased/28682-can-merge-branch-before-build-is-started.yml b/changelogs/unreleased/28682-can-merge-branch-before-build-is-started.yml
deleted file mode 100644
index 5ffd93e098f..00000000000
--- a/changelogs/unreleased/28682-can-merge-branch-before-build-is-started.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Strictly require a pipeline to merge.
-merge_request: 22911
-author:
-type: changed
diff --git a/changelogs/unreleased/39849_controller_sorts.yml b/changelogs/unreleased/39849_controller_sorts.yml
new file mode 100644
index 00000000000..5fad0cb4ede
--- /dev/null
+++ b/changelogs/unreleased/39849_controller_sorts.yml
@@ -0,0 +1,5 @@
+---
+title: Allow sorting issues and MRs in reverse order
+merge_request: 21438
+author:
+type: changed
diff --git a/changelogs/unreleased/48889-populate-merge_commit_sha.yml b/changelogs/unreleased/48889-populate-merge_commit_sha.yml
new file mode 100644
index 00000000000..0e25d8ecfb0
--- /dev/null
+++ b/changelogs/unreleased/48889-populate-merge_commit_sha.yml
@@ -0,0 +1,6 @@
+---
+title: Fix "merged with [commit]" info for merge requests being merged automatically
+ by other actions
+merge_request: 22794
+author:
+type: fixed
diff --git a/changelogs/unreleased/49713-main-navbar-is-broken-in-certain-viewport-widths.yml b/changelogs/unreleased/49713-main-navbar-is-broken-in-certain-viewport-widths.yml
new file mode 100644
index 00000000000..0b5d1a6b05a
--- /dev/null
+++ b/changelogs/unreleased/49713-main-navbar-is-broken-in-certain-viewport-widths.yml
@@ -0,0 +1,5 @@
+---
+title: Resolve Main navbar is broken in certain viewport widths
+merge_request: 23348
+author:
+type: fixed
diff --git a/changelogs/unreleased/50157-extended-user-centric-tooltips.yml b/changelogs/unreleased/50157-extended-user-centric-tooltips.yml
new file mode 100644
index 00000000000..3b55a867b87
--- /dev/null
+++ b/changelogs/unreleased/50157-extended-user-centric-tooltips.yml
@@ -0,0 +1,5 @@
+---
+title: Extended user centric tooltips on issue and MR page
+merge_request: 23231
+author:
+type: added
diff --git a/changelogs/unreleased/51101-can-add-an-existing-group-member-into-a-group-project-with-new-permissions-but-permissions-are-not-overridde.yml b/changelogs/unreleased/51101-can-add-an-existing-group-member-into-a-group-project-with-new-permissions-but-permissions-are-not-overridde.yml
new file mode 100644
index 00000000000..96f33a72cc5
--- /dev/null
+++ b/changelogs/unreleased/51101-can-add-an-existing-group-member-into-a-group-project-with-new-permissions-but-permissions-are-not-overridde.yml
@@ -0,0 +1,5 @@
+---
+title: Restrict member access level to be higher than that of any parent group
+merge_request: 23226
+author:
+type: fixed
diff --git a/changelogs/unreleased/51122-fix-navigating-discussions.yml b/changelogs/unreleased/51122-fix-navigating-discussions.yml
new file mode 100644
index 00000000000..94d76654589
--- /dev/null
+++ b/changelogs/unreleased/51122-fix-navigating-discussions.yml
@@ -0,0 +1,5 @@
+---
+title: Fix navigating by unresolved discussions on Merge Request page
+merge_request: 22789
+author:
+type: fixed
diff --git a/changelogs/unreleased/51138-54026-breadcrumb-subgroups-ellipsis.yml b/changelogs/unreleased/51138-54026-breadcrumb-subgroups-ellipsis.yml
new file mode 100644
index 00000000000..f695d5aeff8
--- /dev/null
+++ b/changelogs/unreleased/51138-54026-breadcrumb-subgroups-ellipsis.yml
@@ -0,0 +1,5 @@
+---
+title: "Make auto-generated icons for subgroups in the breadcrumb dropdown display as a circle"
+merge_request: 23062
+author: Thomas Pathier
+type: fix \ No newline at end of file
diff --git a/changelogs/unreleased/51243-further-improvements-to-project-overview-ui.yml b/changelogs/unreleased/51243-further-improvements-to-project-overview-ui.yml
new file mode 100644
index 00000000000..ddb5eaa89d0
--- /dev/null
+++ b/changelogs/unreleased/51243-further-improvements-to-project-overview-ui.yml
@@ -0,0 +1,5 @@
+---
+title: Design improvements to project overview page
+merge_request: 22196
+author:
+type: changed
diff --git a/changelogs/unreleased/51944-redesign-project-lists-ui.yml b/changelogs/unreleased/51944-redesign-project-lists-ui.yml
new file mode 100644
index 00000000000..56f9a86a686
--- /dev/null
+++ b/changelogs/unreleased/51944-redesign-project-lists-ui.yml
@@ -0,0 +1,5 @@
+---
+title: Redesign project lists UI
+merge_request: 22682
+author:
+type: other
diff --git a/changelogs/unreleased/52007-frontmatter-toml-json.yml b/changelogs/unreleased/52007-frontmatter-toml-json.yml
new file mode 100644
index 00000000000..bdada19f3a7
--- /dev/null
+++ b/changelogs/unreleased/52007-frontmatter-toml-json.yml
@@ -0,0 +1,5 @@
+---
+title: Changed frontmatter filtering to support YAML, JSON, TOML, and arbitrary languages
+merge_request: 23331
+author: Travis Miller
+type: changed
diff --git a/changelogs/unreleased/52285-omniauth-jwt-ppk-support.yml b/changelogs/unreleased/52285-omniauth-jwt-ppk-support.yml
new file mode 100644
index 00000000000..3ef564238c5
--- /dev/null
+++ b/changelogs/unreleased/52285-omniauth-jwt-ppk-support.yml
@@ -0,0 +1,5 @@
+---
+title: Support RSA and ECDSA algorithms in Omniauth JWT provider
+merge_request: 23411
+author: Michael Tsyganov
+type: fixed
diff --git a/changelogs/unreleased/52774-fix-svgs-in-ie-11.yml b/changelogs/unreleased/52774-fix-svgs-in-ie-11.yml
new file mode 100644
index 00000000000..656a915a281
--- /dev/null
+++ b/changelogs/unreleased/52774-fix-svgs-in-ie-11.yml
@@ -0,0 +1,5 @@
+---
+title: Ensure that SVG sprite icons are properly rendered in IE11
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/53493-list-id-email-header.yml b/changelogs/unreleased/53493-list-id-email-header.yml
new file mode 100644
index 00000000000..09a0639f6f5
--- /dev/null
+++ b/changelogs/unreleased/53493-list-id-email-header.yml
@@ -0,0 +1,5 @@
+---
+title: Add project identifier as List-Id email Header to ease filtering
+merge_request: 22817
+author: Olivier Crête
+type: added
diff --git a/changelogs/unreleased/53994-add-missing-ci_builds-partial-indices.yml b/changelogs/unreleased/53994-add-missing-ci_builds-partial-indices.yml
new file mode 100644
index 00000000000..4673ba38bae
--- /dev/null
+++ b/changelogs/unreleased/53994-add-missing-ci_builds-partial-indices.yml
@@ -0,0 +1,5 @@
+---
+title: Add partial index for ci_builds on project_id and status
+merge_request: 23268
+author:
+type: performance
diff --git a/changelogs/unreleased/54160-use-reports-syntax-for-sast-in-auto-devops.yml b/changelogs/unreleased/54160-use-reports-syntax-for-sast-in-auto-devops.yml
new file mode 100644
index 00000000000..86c5a0c5a95
--- /dev/null
+++ b/changelogs/unreleased/54160-use-reports-syntax-for-sast-in-auto-devops.yml
@@ -0,0 +1,5 @@
+---
+title: Use reports syntax for SAST in Auto DevOps
+merge_request: 23163
+author:
+type: changed
diff --git a/changelogs/unreleased/54626-able-to-download-a-single-archive-file-with-api-by-ref-name.yml b/changelogs/unreleased/54626-able-to-download-a-single-archive-file-with-api-by-ref-name.yml
new file mode 100644
index 00000000000..fa905b47ca2
--- /dev/null
+++ b/changelogs/unreleased/54626-able-to-download-a-single-archive-file-with-api-by-ref-name.yml
@@ -0,0 +1,5 @@
+---
+title: Add new endpoint to download single artifact file for a ref
+merge_request: 23538
+author:
+type: added
diff --git a/changelogs/unreleased/54857-fix-templates-path-traversal.yml b/changelogs/unreleased/54857-fix-templates-path-traversal.yml
new file mode 100644
index 00000000000..0da02432c60
--- /dev/null
+++ b/changelogs/unreleased/54857-fix-templates-path-traversal.yml
@@ -0,0 +1,5 @@
+---
+title: Prevent a path traversal attack on global file templates
+merge_request:
+author:
+type: security
diff --git a/changelogs/unreleased/54975-fix-web-hooks-rake-task.yml b/changelogs/unreleased/54975-fix-web-hooks-rake-task.yml
new file mode 100644
index 00000000000..107a93e5b12
--- /dev/null
+++ b/changelogs/unreleased/54975-fix-web-hooks-rake-task.yml
@@ -0,0 +1,5 @@
+---
+title: Fix gitlab:web_hook tasks
+merge_request: 23635
+author:
+type: fixed
diff --git a/changelogs/unreleased/55104-frozenerror-can-t-modify-frozen-string.yml b/changelogs/unreleased/55104-frozenerror-can-t-modify-frozen-string.yml
new file mode 100644
index 00000000000..994859b1d1d
--- /dev/null
+++ b/changelogs/unreleased/55104-frozenerror-can-t-modify-frozen-string.yml
@@ -0,0 +1,5 @@
+---
+title: Fix a frozen string error in app/mailers/notify.rb
+merge_request: 23683
+author:
+type: fixed
diff --git a/changelogs/unreleased/55116-runtimeerror-can-t-modify-frozen-string.yml b/changelogs/unreleased/55116-runtimeerror-can-t-modify-frozen-string.yml
new file mode 100644
index 00000000000..a98e70465b2
--- /dev/null
+++ b/changelogs/unreleased/55116-runtimeerror-can-t-modify-frozen-string.yml
@@ -0,0 +1,5 @@
+---
+title: Fix a frozen string error in lib/gitlab/utils.rb
+merge_request: 23690
+author:
+type: fixed
diff --git a/changelogs/unreleased/55138-fix-mr-discussions-count.yml b/changelogs/unreleased/55138-fix-mr-discussions-count.yml
new file mode 100644
index 00000000000..667e9b971d8
--- /dev/null
+++ b/changelogs/unreleased/55138-fix-mr-discussions-count.yml
@@ -0,0 +1,5 @@
+---
+title: Fix MR resolved discussion counts being too low
+merge_request: 23710
+author:
+type: fixed
diff --git a/changelogs/unreleased/55183-frozenerror-can-t-modify-frozen-string-in-app-mailers-notify-rb.yml b/changelogs/unreleased/55183-frozenerror-can-t-modify-frozen-string-in-app-mailers-notify-rb.yml
new file mode 100644
index 00000000000..685a8309c72
--- /dev/null
+++ b/changelogs/unreleased/55183-frozenerror-can-t-modify-frozen-string-in-app-mailers-notify-rb.yml
@@ -0,0 +1,5 @@
+---
+title: Fix a potential frozen string error in app/mailers/notify.rb
+merge_request: 23728
+author:
+type: fixed
diff --git a/changelogs/unreleased/55191-update-workhorse.yml b/changelogs/unreleased/55191-update-workhorse.yml
new file mode 100644
index 00000000000..d16518e673a
--- /dev/null
+++ b/changelogs/unreleased/55191-update-workhorse.yml
@@ -0,0 +1,5 @@
+---
+title: Update GitLab Workhorse to v8.0.0
+merge_request: 23740
+author:
+type: other
diff --git a/changelogs/unreleased/cert-manager-email.yml b/changelogs/unreleased/cert-manager-email.yml
new file mode 100644
index 00000000000..530608d9660
--- /dev/null
+++ b/changelogs/unreleased/cert-manager-email.yml
@@ -0,0 +1,5 @@
+---
+title: Ability to override email for cert-manager
+merge_request: 23503
+author: Amit Rathi
+type: added
diff --git a/changelogs/unreleased/commit-badge-style-fix.yml b/changelogs/unreleased/commit-badge-style-fix.yml
new file mode 100644
index 00000000000..d7b37717853
--- /dev/null
+++ b/changelogs/unreleased/commit-badge-style-fix.yml
@@ -0,0 +1,5 @@
+---
+title: Fixed styling of image comment badges on commits
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/define-default-value-for-only-except-keys.yml b/changelogs/unreleased/define-default-value-for-only-except-keys.yml
index 3e5ecdcf51e..ed0e982f0fc 100644
--- a/changelogs/unreleased/define-default-value-for-only-except-keys.yml
+++ b/changelogs/unreleased/define-default-value-for-only-except-keys.yml
@@ -1,5 +1,5 @@
---
title: Define the default value for only/except policies
-merge_request: 23531
+merge_request: 23765
author:
type: changed
diff --git a/changelogs/unreleased/deprecated-instance-find.yml b/changelogs/unreleased/deprecated-instance-find.yml
new file mode 100644
index 00000000000..d2ba821e124
--- /dev/null
+++ b/changelogs/unreleased/deprecated-instance-find.yml
@@ -0,0 +1,5 @@
+---
+title: 'Fix deprecation: You are passing an instance of ActiveRecord::Base to'
+merge_request: 23369
+author: Jasper Maes
+type: other
diff --git a/changelogs/unreleased/diff-empty-state-fixes.yml b/changelogs/unreleased/diff-empty-state-fixes.yml
new file mode 100644
index 00000000000..0d347dd17e4
--- /dev/null
+++ b/changelogs/unreleased/diff-empty-state-fixes.yml
@@ -0,0 +1,5 @@
+---
+title: Fixed merge request diffs empty states
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/dm-remove-prune-web-hook-logs-worker.yml b/changelogs/unreleased/dm-remove-prune-web-hook-logs-worker.yml
new file mode 100644
index 00000000000..fb0c508400c
--- /dev/null
+++ b/changelogs/unreleased/dm-remove-prune-web-hook-logs-worker.yml
@@ -0,0 +1,5 @@
+---
+title: Remove old webhook logs after 90 days, as documented, instead of after 2
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/expose-mr-pipeline-variables.yml b/changelogs/unreleased/expose-mr-pipeline-variables.yml
new file mode 100644
index 00000000000..b77b9a69d5c
--- /dev/null
+++ b/changelogs/unreleased/expose-mr-pipeline-variables.yml
@@ -0,0 +1,5 @@
+---
+title: Expose merge request pipeline variables
+merge_request: 23398
+author:
+type: changed
diff --git a/changelogs/unreleased/fix-calendar-events-fetching-error.yml b/changelogs/unreleased/fix-calendar-events-fetching-error.yml
new file mode 100644
index 00000000000..ad4a40cd9a0
--- /dev/null
+++ b/changelogs/unreleased/fix-calendar-events-fetching-error.yml
@@ -0,0 +1,5 @@
+---
+title: Fix calendar events fetching error on private profile page
+merge_request: 23718
+author: Harry Kiselev
+type: other
diff --git a/changelogs/unreleased/fix-gb-encrypt-ci-build-token.yml b/changelogs/unreleased/fix-gb-encrypt-ci-build-token.yml
new file mode 100644
index 00000000000..04fc88bc3d3
--- /dev/null
+++ b/changelogs/unreleased/fix-gb-encrypt-ci-build-token.yml
@@ -0,0 +1,5 @@
+---
+title: Encrypt CI/CD builds authentication tokens
+merge_request: 23436
+author:
+type: security
diff --git a/changelogs/unreleased/fix-n-plus-1-queries-projects.yml b/changelogs/unreleased/fix-n-plus-1-queries-projects.yml
new file mode 100644
index 00000000000..cb625784267
--- /dev/null
+++ b/changelogs/unreleased/fix-n-plus-1-queries-projects.yml
@@ -0,0 +1,6 @@
+---
+title: Fix some N+1 queries related to Admin Dashboard, User Dashboards and Activity
+ Stream
+merge_request: 23034
+author:
+type: performance
diff --git a/changelogs/unreleased/fj-clean-content-headers.yml b/changelogs/unreleased/fj-clean-content-headers.yml
new file mode 100644
index 00000000000..59e25ca6578
--- /dev/null
+++ b/changelogs/unreleased/fj-clean-content-headers.yml
@@ -0,0 +1,5 @@
+---
+title: Added feature flag to signal content headers detection by Workhorse
+merge_request: 22667
+author:
+type: added
diff --git a/changelogs/unreleased/gt-add-top-padding-for-nested-environment-items-loading-icon.yml b/changelogs/unreleased/gt-add-top-padding-for-nested-environment-items-loading-icon.yml
new file mode 100644
index 00000000000..606314b5780
--- /dev/null
+++ b/changelogs/unreleased/gt-add-top-padding-for-nested-environment-items-loading-icon.yml
@@ -0,0 +1,5 @@
+---
+title: Add top padding for nested environment items loading icon
+merge_request: 23580
+author: George Tsiolis
+type: fixed
diff --git a/changelogs/unreleased/gt-remove-unnecessary-line-before-reply-holder.yml b/changelogs/unreleased/gt-remove-unnecessary-line-before-reply-holder.yml
new file mode 100644
index 00000000000..142a9c1f2cc
--- /dev/null
+++ b/changelogs/unreleased/gt-remove-unnecessary-line-before-reply-holder.yml
@@ -0,0 +1,5 @@
+---
+title: Remove unnecessary line before reply holder
+merge_request: 23092
+author: George Tsiolis
+type: changed
diff --git a/changelogs/unreleased/gt-show-primary-button-when-all-labels-are-prioritized.yml b/changelogs/unreleased/gt-show-primary-button-when-all-labels-are-prioritized.yml
new file mode 100644
index 00000000000..eed31950a76
--- /dev/null
+++ b/changelogs/unreleased/gt-show-primary-button-when-all-labels-are-prioritized.yml
@@ -0,0 +1,5 @@
+---
+title: Show primary button when all labels are prioritized
+merge_request: 23648
+author: George Tsiolis
+type: other
diff --git a/changelogs/unreleased/gt-update-environment-breadcrumb.yml b/changelogs/unreleased/gt-update-environment-breadcrumb.yml
new file mode 100644
index 00000000000..53b9673a96c
--- /dev/null
+++ b/changelogs/unreleased/gt-update-environment-breadcrumb.yml
@@ -0,0 +1,5 @@
+---
+title: Update environments breadcrumb
+merge_request: 23751
+author: George Tsiolis
+type: changed
diff --git a/changelogs/unreleased/gt-update-navigation-theme-colors.yml b/changelogs/unreleased/gt-update-navigation-theme-colors.yml
new file mode 100644
index 00000000000..749587a6343
--- /dev/null
+++ b/changelogs/unreleased/gt-update-navigation-theme-colors.yml
@@ -0,0 +1,5 @@
+---
+title: Update header navigation theme colors
+merge_request: 23734
+author: George Tsiolis
+type: fixed
diff --git a/changelogs/unreleased/mg-fix-knative-application-row.yml b/changelogs/unreleased/mg-fix-knative-application-row.yml
new file mode 100644
index 00000000000..95142d380a4
--- /dev/null
+++ b/changelogs/unreleased/mg-fix-knative-application-row.yml
@@ -0,0 +1,5 @@
+---
+title: Hide Knative from group cluster applications until supported
+merge_request: 23577
+author:
+type: fixed
diff --git a/changelogs/unreleased/move-group-issues-search-cte-up-the-chain.yml b/changelogs/unreleased/move-group-issues-search-cte-up-the-chain.yml
new file mode 100644
index 00000000000..0269e7b6196
--- /dev/null
+++ b/changelogs/unreleased/move-group-issues-search-cte-up-the-chain.yml
@@ -0,0 +1,5 @@
+---
+title: Fix error when searching for group issues with priority or popularity sort
+merge_request: 23445
+author:
+type: fixed
diff --git a/changelogs/unreleased/osw-remove-unnused-data-from-diff-discussions.yml b/changelogs/unreleased/osw-remove-unnused-data-from-diff-discussions.yml
new file mode 100644
index 00000000000..58d9a19d038
--- /dev/null
+++ b/changelogs/unreleased/osw-remove-unnused-data-from-diff-discussions.yml
@@ -0,0 +1,5 @@
+---
+title: Remove unused data from discussions endpoint
+merge_request: 23570
+author:
+type: performance
diff --git a/changelogs/unreleased/osw-update-mr-metrics-with-events-data.yml b/changelogs/unreleased/osw-update-mr-metrics-with-events-data.yml
new file mode 100644
index 00000000000..09a10a86adc
--- /dev/null
+++ b/changelogs/unreleased/osw-update-mr-metrics-with-events-data.yml
@@ -0,0 +1,5 @@
+---
+title: Populate MR metrics with events table information (migration)
+merge_request: 23564
+author:
+type: performance
diff --git a/changelogs/unreleased/profile-fixing.yml b/changelogs/unreleased/profile-fixing.yml
new file mode 100644
index 00000000000..7e255d997d8
--- /dev/null
+++ b/changelogs/unreleased/profile-fixing.yml
@@ -0,0 +1,5 @@
+---
+title: Fix bottom paddings of profile header and some markup updates of profile
+merge_request: 23168
+author: Harry Kiselev
+type: other
diff --git a/changelogs/unreleased/remote-mirror-update-failed-notification.yml b/changelogs/unreleased/remote-mirror-update-failed-notification.yml
new file mode 100644
index 00000000000..50ec8624ae5
--- /dev/null
+++ b/changelogs/unreleased/remote-mirror-update-failed-notification.yml
@@ -0,0 +1,5 @@
+---
+title: Send a notification email to project maintainers when a mirror update fails
+merge_request: 23595
+author:
+type: added
diff --git a/changelogs/unreleased/remove-blob-search-limit.yml b/changelogs/unreleased/remove-blob-search-limit.yml
new file mode 100644
index 00000000000..5bad3a83dbb
--- /dev/null
+++ b/changelogs/unreleased/remove-blob-search-limit.yml
@@ -0,0 +1,5 @@
+---
+title: Remove limit of 100 when searching repository code.
+merge_request: 8671
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-fix-github-import-without-oauth2-config.yml b/changelogs/unreleased/sh-fix-github-import-without-oauth2-config.yml
new file mode 100644
index 00000000000..ad548a6ff35
--- /dev/null
+++ b/changelogs/unreleased/sh-fix-github-import-without-oauth2-config.yml
@@ -0,0 +1,5 @@
+---
+title: Allow GitHub imports via token even if OAuth2 provider not configured
+merge_request: 23703
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-handle-invalid-gpg-sig.yml b/changelogs/unreleased/sh-handle-invalid-gpg-sig.yml
new file mode 100644
index 00000000000..185e2547e16
--- /dev/null
+++ b/changelogs/unreleased/sh-handle-invalid-gpg-sig.yml
@@ -0,0 +1,5 @@
+---
+title: Gracefully handle unknown/invalid GPG keys
+merge_request: 23492
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-ignore-arrays-url-sanitizer.yml b/changelogs/unreleased/sh-ignore-arrays-url-sanitizer.yml
new file mode 100644
index 00000000000..c010bd1f540
--- /dev/null
+++ b/changelogs/unreleased/sh-ignore-arrays-url-sanitizer.yml
@@ -0,0 +1,5 @@
+---
+title: Only allow strings in URL::Sanitizer.valid?
+merge_request: 23675
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-json-serialize-broadcast-messages.yml b/changelogs/unreleased/sh-json-serialize-broadcast-messages.yml
new file mode 100644
index 00000000000..e8bee64f780
--- /dev/null
+++ b/changelogs/unreleased/sh-json-serialize-broadcast-messages.yml
@@ -0,0 +1,5 @@
+---
+title: Avoid caching BroadcastMessage as an ActiveRecord object
+merge_request: 23662
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-truncate-with-periods.yml b/changelogs/unreleased/sh-truncate-with-periods.yml
new file mode 100644
index 00000000000..b1c6b4f9cbd
--- /dev/null
+++ b/changelogs/unreleased/sh-truncate-with-periods.yml
@@ -0,0 +1,5 @@
+---
+title: Truncate merge request titles with periods instead of ellipsis
+merge_request: 23558
+author:
+type: changed
diff --git a/changelogs/unreleased/store-correlation-logs.yml b/changelogs/unreleased/store-correlation-logs.yml
new file mode 100644
index 00000000000..d5f6c789a17
--- /dev/null
+++ b/changelogs/unreleased/store-correlation-logs.yml
@@ -0,0 +1,5 @@
+---
+title: Log and pass correlation-id between Unicorn, Sidekiq and Gitaly
+merge_request:
+author:
+type: added
diff --git a/changelogs/unreleased/tc-backfill-hashed-project_repositories.yml b/changelogs/unreleased/tc-backfill-hashed-project_repositories.yml
new file mode 100644
index 00000000000..90a5c8c4e2c
--- /dev/null
+++ b/changelogs/unreleased/tc-backfill-hashed-project_repositories.yml
@@ -0,0 +1,5 @@
+---
+title: Fill project_repositories for hashed storage projects
+merge_request: 23482
+author:
+type: added
diff --git a/changelogs/unreleased/triggermesh-phase2-serverless-list.yml b/changelogs/unreleased/triggermesh-phase2-serverless-list.yml
new file mode 100644
index 00000000000..22e1a35dd90
--- /dev/null
+++ b/changelogs/unreleased/triggermesh-phase2-serverless-list.yml
@@ -0,0 +1,5 @@
+---
+title: Introduce Knative and Serverless Components
+merge_request: 23174
+author: Chris Baumbauer
+type: added
diff --git a/changelogs/unreleased/update-gitlab-runner-helm-chart-to-0-1-39.yml b/changelogs/unreleased/update-gitlab-runner-helm-chart-to-0-1-39.yml
new file mode 100644
index 00000000000..dffcdb0bb5a
--- /dev/null
+++ b/changelogs/unreleased/update-gitlab-runner-helm-chart-to-0-1-39.yml
@@ -0,0 +1,5 @@
+---
+title: Update used version of Runner Helm Chart to 0.1.39
+merge_request: 23633
+author:
+type: other
diff --git a/changelogs/unreleased/upgrade-to-workhorse-7-6-0.yml b/changelogs/unreleased/upgrade-to-workhorse-7-6-0.yml
new file mode 100644
index 00000000000..1389693b9a9
--- /dev/null
+++ b/changelogs/unreleased/upgrade-to-workhorse-7-6-0.yml
@@ -0,0 +1,5 @@
+---
+title: Upgrade workhorse to 7.6.0
+merge_request: 23694
+author:
+type: other
diff --git a/changelogs/unreleased/usage-count.yml b/changelogs/unreleased/usage-count.yml
new file mode 100644
index 00000000000..efff2615ce4
--- /dev/null
+++ b/changelogs/unreleased/usage-count.yml
@@ -0,0 +1,5 @@
+---
+title: Use approximate count for big tables for usage statistics.
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/winh-dropdown-divider-color.yml b/changelogs/unreleased/winh-dropdown-divider-color.yml
new file mode 100644
index 00000000000..6b6ecd831b8
--- /dev/null
+++ b/changelogs/unreleased/winh-dropdown-divider-color.yml
@@ -0,0 +1,5 @@
+---
+title: Change dropdown divider color to gray-200 (#dfdfdf)
+merge_request: 23592
+author:
+type: changed
diff --git a/changelogs/unreleased/winh-dropdown-item-padding.yml b/changelogs/unreleased/winh-dropdown-item-padding.yml
new file mode 100644
index 00000000000..9f18abba9d1
--- /dev/null
+++ b/changelogs/unreleased/winh-dropdown-item-padding.yml
@@ -0,0 +1,5 @@
+---
+title: Adjust dropdown item and header padding to comply with design specs
+merge_request: 23552
+author:
+type: changed
diff --git a/changelogs/unreleased/winh-issue-boards-project-dropdown-close.yml b/changelogs/unreleased/winh-issue-boards-project-dropdown-close.yml
new file mode 100644
index 00000000000..18f7da56edb
--- /dev/null
+++ b/changelogs/unreleased/winh-issue-boards-project-dropdown-close.yml
@@ -0,0 +1,5 @@
+---
+title: Remove close icon from projects dropdown in issue boards
+merge_request: 23567
+author:
+type: changed
diff --git a/changelogs/unreleased/winh-markdown-preview-lists.yml b/changelogs/unreleased/winh-markdown-preview-lists.yml
new file mode 100644
index 00000000000..6e47726283d
--- /dev/null
+++ b/changelogs/unreleased/winh-markdown-preview-lists.yml
@@ -0,0 +1,5 @@
+---
+title: Remove unnecessary div from MarkdownField to apply list styles correctly
+merge_request: 23733
+author:
+type: fixed
diff --git a/changelogs/unreleased/winh-merge-request-diff-discussion-commit-id.yml b/changelogs/unreleased/winh-merge-request-diff-discussion-commit-id.yml
new file mode 100644
index 00000000000..2ce16a2b6b7
--- /dev/null
+++ b/changelogs/unreleased/winh-merge-request-diff-discussion-commit-id.yml
@@ -0,0 +1,5 @@
+---
+title: Pass commit when posting diff discussions
+merge_request: 23371
+author:
+type: fixed
diff --git a/changelogs/unreleased/winh-milestone-select.yml b/changelogs/unreleased/winh-milestone-select.yml
new file mode 100644
index 00000000000..8464fc6c541
--- /dev/null
+++ b/changelogs/unreleased/winh-milestone-select.yml
@@ -0,0 +1,5 @@
+---
+title: Fix milestone select in issue sidebar of issue boards
+merge_request: 23625
+author:
+type: fixed
diff --git a/changelogs/unreleased/winh-princess-mononospace.yml b/changelogs/unreleased/winh-princess-mononospace.yml
new file mode 100644
index 00000000000..e2d33de375e
--- /dev/null
+++ b/changelogs/unreleased/winh-princess-mononospace.yml
@@ -0,0 +1,5 @@
+---
+title: Make commit IDs in merge request discussion header monospace
+merge_request: 23562
+author:
+type: changed
diff --git a/changelogs/unreleased/zj-backup-restore-object-pools.yml b/changelogs/unreleased/zj-backup-restore-object-pools.yml
new file mode 100644
index 00000000000..26e1d49aa04
--- /dev/null
+++ b/changelogs/unreleased/zj-backup-restore-object-pools.yml
@@ -0,0 +1,5 @@
+---
+title: Restore Object Pools when restoring an object pool
+merge_request: 23682
+author:
+type: added
diff --git a/changelogs/unreleased/zj-pool-repository-creation.yml b/changelogs/unreleased/zj-pool-repository-creation.yml
new file mode 100644
index 00000000000..a24b96e4924
--- /dev/null
+++ b/changelogs/unreleased/zj-pool-repository-creation.yml
@@ -0,0 +1,5 @@
+---
+title: Allow public forks to be deduplicated
+merge_request: 23508
+author:
+type: added
diff --git a/config/application.rb b/config/application.rb
index 63a5b483fc2..f10b8ed5bd2 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -154,6 +154,7 @@ module Gitlab
config.assets.precompile << "locale/**/app.js"
config.assets.precompile << "emoji_sprites.css"
config.assets.precompile << "errors.css"
+ config.assets.precompile << "csslab.css"
# Import gitlab-svgs directly from vendored directory
config.assets.paths << "#{config.root}/node_modules/@gitlab/svgs/dist"
diff --git a/config/dependency_decisions.yml b/config/dependency_decisions.yml
index 84d47bd52ad..af76bace577 100644
--- a/config/dependency_decisions.yml
+++ b/config/dependency_decisions.yml
@@ -470,8 +470,8 @@
- - :license
- pikaday
- MIT
- - :who:
- :why:
+ - :who: Filipa Lacerda
+ :why: MIT License
:versions: []
:when: 2017-10-17 17:46:12.367554000 Z
- - :license
@@ -592,3 +592,10 @@
in compiled/distributed product so attribution not needed.
:versions: []
:when: 2018-10-02 19:23:54.840151000 Z
+- - :license
+ - echarts
+ - Apache 2.0
+ - :who: Adriel Santiago
+ :why: https://github.com/apache/incubator-echarts/blob/master/LICENSE
+ :versions: []
+ :when: 2018-12-07 20:46:12.421256000 Z
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index 58b7c248aaf..1c16b999e55 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -548,15 +548,15 @@ production: &base
# app_id: 'YOUR_APP_ID',
# app_secret: 'YOUR_APP_SECRET' }
# - { name: 'jwt',
- # app_secret: 'YOUR_APP_SECRET',
# args: {
- # algorithm: 'HS256',
- # uid_claim: 'email',
- # required_claims: ["name", "email"],
- # info_map: { name: "name", email: "email" },
- # auth_url: 'https://example.com/',
- # valid_within: null,
- # }
+ # secret: 'YOUR_APP_SECRET',
+ # algorithm: 'HS256', # Supported algorithms: 'RS256', 'RS384', 'RS512', 'ES256', 'ES384', 'ES512', 'HS256', 'HS384', 'HS512'
+ # uid_claim: 'email',
+ # required_claims: ['name', 'email'],
+ # info_map: { name: 'name', email: 'email' },
+ # auth_url: 'https://example.com/',
+ # valid_within: 3600 # 1 hour
+ # }
# }
# - { name: 'saml',
# label: 'Our SAML Provider',
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index 82e3b490378..db35fa96ea2 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -302,10 +302,6 @@ Settings.cron_jobs['gitlab_usage_ping_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['gitlab_usage_ping_worker']['cron'] ||= Settings.__send__(:cron_for_usage_ping)
Settings.cron_jobs['gitlab_usage_ping_worker']['job_class'] = 'GitlabUsagePingWorker'
-Settings.cron_jobs['remove_old_web_hook_logs_worker'] ||= Settingslogic.new({})
-Settings.cron_jobs['remove_old_web_hook_logs_worker']['cron'] ||= '40 0 * * *'
-Settings.cron_jobs['remove_old_web_hook_logs_worker']['job_class'] = 'RemoveOldWebHookLogsWorker'
-
Settings.cron_jobs['stuck_merge_jobs_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['stuck_merge_jobs_worker']['cron'] ||= '0 */2 * * *'
Settings.cron_jobs['stuck_merge_jobs_worker']['job_class'] = 'StuckMergeJobsWorker'
diff --git a/config/initializers/correlation_id.rb b/config/initializers/correlation_id.rb
new file mode 100644
index 00000000000..2a7c138dc40
--- /dev/null
+++ b/config/initializers/correlation_id.rb
@@ -0,0 +1,3 @@
+# frozen_string_literal: true
+
+Rails.application.config.middleware.use(Gitlab::Middleware::CorrelationId)
diff --git a/config/initializers/lograge.rb b/config/initializers/lograge.rb
index 840404e0ec0..c897bc30e76 100644
--- a/config/initializers/lograge.rb
+++ b/config/initializers/lograge.rb
@@ -29,6 +29,7 @@ unless Sidekiq.server?
gitaly_calls = Gitlab::GitalyClient.get_request_count
payload[:gitaly_calls] = gitaly_calls if gitaly_calls > 0
payload[:response] = event.payload[:response] if event.payload[:response]
+ payload[Gitlab::CorrelationId::LOG_KEY] = Gitlab::CorrelationId.current_id
payload
end
diff --git a/config/initializers/postgresql_opclasses_support.rb b/config/initializers/postgresql_opclasses_support.rb
index 07b06629dea..5d643b75dd8 100644
--- a/config/initializers/postgresql_opclasses_support.rb
+++ b/config/initializers/postgresql_opclasses_support.rb
@@ -81,7 +81,7 @@ module ActiveRecord
if index_name.length > max_index_length
raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' is too long; the limit is #{max_index_length} characters"
end
- if table_exists?(table_name) && index_name_exists?(table_name, index_name, false)
+ if data_source_exists?(table_name) && index_name_exists?(table_name, index_name, false)
raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' already exists"
end
index_columns = quoted_columns_for_index(column_names, options).join(", ")
diff --git a/config/initializers/sentry.rb b/config/initializers/sentry.rb
index 17d09293205..2a6c5148f71 100644
--- a/config/initializers/sentry.rb
+++ b/config/initializers/sentry.rb
@@ -24,4 +24,4 @@ def configure_sentry
end
end
-configure_sentry if Rails.env.production?
+configure_sentry if Rails.env.production? || Rails.env.development?
diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb
index 4210be2c701..6aba6c7c21d 100644
--- a/config/initializers/sidekiq.rb
+++ b/config/initializers/sidekiq.rb
@@ -21,6 +21,7 @@ Sidekiq.configure_server do |config|
chain.add Gitlab::SidekiqMiddleware::Shutdown
chain.add Gitlab::SidekiqMiddleware::RequestStoreMiddleware unless ENV['SIDEKIQ_REQUEST_STORE'] == '0'
chain.add Gitlab::SidekiqMiddleware::BatchLoader
+ chain.add Gitlab::SidekiqMiddleware::CorrelationLogger
chain.add Gitlab::SidekiqStatus::ServerMiddleware
end
@@ -31,6 +32,7 @@ Sidekiq.configure_server do |config|
config.client_middleware do |chain|
chain.add Gitlab::SidekiqStatus::ClientMiddleware
+ chain.add Gitlab::SidekiqMiddleware::CorrelationInjector
end
config.on :startup do
@@ -39,7 +41,7 @@ Sidekiq.configure_server do |config|
ActiveRecord::Base.clear_all_connections!
end
- if Feature.enabled?(:gitlab_sidekiq_reliable_fetcher)
+ if Feature::FlipperFeature.table_exists? && Feature.enabled?(:gitlab_sidekiq_reliable_fetcher)
Sidekiq::ReliableFetcher.setup_reliable_fetch!(config)
end
@@ -75,6 +77,7 @@ Sidekiq.configure_client do |config|
config.redis = queues_config_hash
config.client_middleware do |chain|
+ chain.add Gitlab::SidekiqMiddleware::CorrelationInjector
chain.add Gitlab::SidekiqStatus::ClientMiddleware
end
end
diff --git a/config/routes/project.rb b/config/routes/project.rb
index 3f1ad90dfca..7d0623cb904 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -245,6 +245,10 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
end
end
+ namespace :serverless do
+ resources :functions, only: [:index]
+ end
+
scope '-' do
get 'archive/*id', constraints: { format: Gitlab::PathRegex.archive_formats_regex, id: /.+?/ }, to: 'repositories#archive', as: 'archive'
@@ -432,6 +436,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
resource :integrations, only: [:show]
resource :repository, only: [:show], controller: :repository do
post :create_deploy_token, path: 'deploy_token/create'
+ post :cleanup
end
end
diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
index 53e1c8778b6..3ee32678f34 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -81,3 +81,7 @@
- [delete_diff_files, 1]
- [detect_repository_languages, 1]
- [auto_devops, 2]
+ - [object_pool, 1]
+ - [repository_cleanup, 1]
+ - [delete_stored_files, 1]
+ - [remote_mirror_notification, 2]
diff --git a/danger/documentation/Dangerfile b/danger/documentation/Dangerfile
index 87c61d6e90d..be7b301866d 100644
--- a/danger/documentation/Dangerfile
+++ b/danger/documentation/Dangerfile
@@ -24,23 +24,24 @@ The following files require a review from the Documentation team:
* #{docs_paths_to_review.map { |path| "`#{path}`" }.join("\n* ")}
-When your content is ready for review, mention a technical writer in a separate
-comment and explain what needs to be reviewed.
-
-You are welcome to mention them sooner if you have questions about writing or updating
-the documentation. GitLabbers are also welcome to use the [#docs](https://gitlab.slack.com/archives/C16HYA2P5) channel on Slack.
-
-Who to ping [based on DevOps stages](https://about.gitlab.com/handbook/product/categories/#devops-stages):
+When your content is ready for review, assign the MR to a technical writer
+according to the [DevOps stages](https://about.gitlab.com/handbook/product/categories/#devops-stages)
+in the table below. If necessary, mention them in a comment explaining what needs
+to be reviewed.
| Tech writer | Stage(s) |
| ------------ | ------------------------------------------------------------ |
-| `@marcia` | ~Create ~Release |
+| `@marcia` | ~Create ~Release + ~"development guidelines" |
| `@axil` | ~Distribution ~Gitaly ~Gitter ~Monitoring ~Packaging ~Secure |
| `@eread` | ~Manage ~Configure ~Geo ~Verify |
| `@mikelewis` | ~Plan |
+You are welcome to mention them sooner if you have questions about writing or
+updating the documentation. GitLabbers are also welcome to use the
+[#docs](https://gitlab.slack.com/archives/C16HYA2P5) channel on Slack.
+
If you are not sure which category the change falls within, or the change is not
-part of one of these categories, you can mention one of the usernames above.
+part of one of these categories, mention one of the usernames above.
MARKDOWN
unless gitlab.mr_labels.include?('Documentation')
diff --git a/db/fixtures/development/04_project.rb b/db/fixtures/development/04_project.rb
index 089de211380..aa8686ac7d8 100644
--- a/db/fixtures/development/04_project.rb
+++ b/db/fixtures/development/04_project.rb
@@ -71,13 +71,17 @@ Sidekiq::Testing.inline! do
params[:storage_version] = Project::LATEST_STORAGE_VERSION
end
- project = Projects::CreateService.new(User.first, params).execute
- # Seed-Fu runs this entire fixture in a transaction, so the `after_commit`
- # hook won't run until after the fixture is loaded. That is too late
- # since the Sidekiq::Testing block has already exited. Force clearing
- # the `after_commit` queue to ensure the job is run now.
+ project = nil
+
Sidekiq::Worker.skipping_transaction_check do
+ project = Projects::CreateService.new(User.first, params).execute
+
+ # Seed-Fu runs this entire fixture in a transaction, so the `after_commit`
+ # hook won't run until after the fixture is loaded. That is too late
+ # since the Sidekiq::Testing block has already exited. Force clearing
+ # the `after_commit` queue to ensure the job is run now.
project.send(:_run_after_commit_queue)
+ project.import_state.send(:_run_after_commit_queue)
end
if project.valid? && project.valid_repo?
diff --git a/db/fixtures/development/10_merge_requests.rb b/db/fixtures/development/10_merge_requests.rb
index bcfdd058a1c..8bdc7c6556c 100644
--- a/db/fixtures/development/10_merge_requests.rb
+++ b/db/fixtures/development/10_merge_requests.rb
@@ -25,7 +25,9 @@ Gitlab::Seeder.quiet do
developer = project.team.developers.sample
break unless developer
- MergeRequests::CreateService.new(project, developer, params).execute
+ Sidekiq::Worker.skipping_transaction_check do
+ MergeRequests::CreateService.new(project, developer, params).execute
+ end
print '.'
end
end
@@ -39,7 +41,9 @@ Gitlab::Seeder.quiet do
target_branch: 'master',
title: 'Can be automatically merged'
}
- MergeRequests::CreateService.new(project, User.admins.first, params).execute
+ Sidekiq::Worker.skipping_transaction_check do
+ MergeRequests::CreateService.new(project, User.admins.first, params).execute
+ end
print '.'
params = {
@@ -47,6 +51,8 @@ Gitlab::Seeder.quiet do
target_branch: 'feature',
title: 'Cannot be automatically merged'
}
- MergeRequests::CreateService.new(project, User.admins.first, params).execute
+ Sidekiq::Worker.skipping_transaction_check do
+ MergeRequests::CreateService.new(project, User.admins.first, params).execute
+ end
print '.'
end
diff --git a/db/fixtures/development/24_forks.rb b/db/fixtures/development/24_forks.rb
new file mode 100644
index 00000000000..61e39c871e6
--- /dev/null
+++ b/db/fixtures/development/24_forks.rb
@@ -0,0 +1,16 @@
+require './spec/support/sidekiq'
+
+Sidekiq::Testing.inline! do
+ Gitlab::Seeder.quiet do
+ User.all.sample(10).each do |user|
+ source_project = Project.public_only.sample
+ fork_project = Projects::ForkService.new(source_project, user, namespace: user.namespace).execute
+
+ if fork_project.valid?
+ puts '.'
+ else
+ puts 'F'
+ end
+ end
+ end
+end
diff --git a/db/fixtures/production/001_application_settings.rb b/db/fixtures/production/001_application_settings.rb
new file mode 100644
index 00000000000..ab15717e9a9
--- /dev/null
+++ b/db/fixtures/production/001_application_settings.rb
@@ -0,0 +1,2 @@
+puts "Creating the default ApplicationSetting record.".color(:green)
+Gitlab::CurrentSettings.current_application_settings
diff --git a/db/fixtures/production/001_admin.rb b/db/fixtures/production/002_admin.rb
index 1c7c89f7bbd..1c7c89f7bbd 100644
--- a/db/fixtures/production/001_admin.rb
+++ b/db/fixtures/production/002_admin.rb
diff --git a/db/migrate/20181101191341_create_clusters_applications_cert_manager.rb b/db/migrate/20181101191341_create_clusters_applications_cert_manager.rb
index 4966b89964a..0b6155356d9 100644
--- a/db/migrate/20181101191341_create_clusters_applications_cert_manager.rb
+++ b/db/migrate/20181101191341_create_clusters_applications_cert_manager.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-class CreateClustersApplicationsCertManager < ActiveRecord::Migration
+class CreateClustersApplicationsCertManager < ActiveRecord::Migration[4.2]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
diff --git a/db/migrate/20181108091549_cleanup_environments_external_url.rb b/db/migrate/20181108091549_cleanup_environments_external_url.rb
index 8d6c20a4b15..8439f6e55e6 100644
--- a/db/migrate/20181108091549_cleanup_environments_external_url.rb
+++ b/db/migrate/20181108091549_cleanup_environments_external_url.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-class CleanupEnvironmentsExternalUrl < ActiveRecord::Migration
+class CleanupEnvironmentsExternalUrl < ActiveRecord::Migration[4.2]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
diff --git a/db/migrate/20181115140140_add_encrypted_runners_token_to_settings.rb b/db/migrate/20181115140140_add_encrypted_runners_token_to_settings.rb
index 36d9ad45b19..5b2bb4f6b08 100644
--- a/db/migrate/20181115140140_add_encrypted_runners_token_to_settings.rb
+++ b/db/migrate/20181115140140_add_encrypted_runners_token_to_settings.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-class AddEncryptedRunnersTokenToSettings < ActiveRecord::Migration
+class AddEncryptedRunnersTokenToSettings < ActiveRecord::Migration[4.2]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
diff --git a/db/migrate/20181116050532_knative_external_ip.rb b/db/migrate/20181116050532_knative_external_ip.rb
index f1f903fb692..5645b040a23 100644
--- a/db/migrate/20181116050532_knative_external_ip.rb
+++ b/db/migrate/20181116050532_knative_external_ip.rb
@@ -3,7 +3,7 @@
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
-class KnativeExternalIp < ActiveRecord::Migration
+class KnativeExternalIp < ActiveRecord::Migration[4.2]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
diff --git a/db/migrate/20181116141415_add_encrypted_runners_token_to_namespaces.rb b/db/migrate/20181116141415_add_encrypted_runners_token_to_namespaces.rb
index b92b1b50218..dcf565cd6c0 100644
--- a/db/migrate/20181116141415_add_encrypted_runners_token_to_namespaces.rb
+++ b/db/migrate/20181116141415_add_encrypted_runners_token_to_namespaces.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-class AddEncryptedRunnersTokenToNamespaces < ActiveRecord::Migration
+class AddEncryptedRunnersTokenToNamespaces < ActiveRecord::Migration[4.2]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
diff --git a/db/migrate/20181116141504_add_encrypted_runners_token_to_projects.rb b/db/migrate/20181116141504_add_encrypted_runners_token_to_projects.rb
index 53e475bd180..13cd80e5c8b 100644
--- a/db/migrate/20181116141504_add_encrypted_runners_token_to_projects.rb
+++ b/db/migrate/20181116141504_add_encrypted_runners_token_to_projects.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-class AddEncryptedRunnersTokenToProjects < ActiveRecord::Migration
+class AddEncryptedRunnersTokenToProjects < ActiveRecord::Migration[4.2]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
diff --git a/db/migrate/20181119081539_add_merge_request_id_to_ci_pipelines.rb b/db/migrate/20181119081539_add_merge_request_id_to_ci_pipelines.rb
index 65476109c61..f96d80787f9 100644
--- a/db/migrate/20181119081539_add_merge_request_id_to_ci_pipelines.rb
+++ b/db/migrate/20181119081539_add_merge_request_id_to_ci_pipelines.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-class AddMergeRequestIdToCiPipelines < ActiveRecord::Migration
+class AddMergeRequestIdToCiPipelines < ActiveRecord::Migration[4.2]
DOWNTIME = false
def up
diff --git a/db/migrate/20181120091639_add_foreign_key_to_ci_pipelines_merge_requests.rb b/db/migrate/20181120091639_add_foreign_key_to_ci_pipelines_merge_requests.rb
index c2b5b239279..f8b46395941 100644
--- a/db/migrate/20181120091639_add_foreign_key_to_ci_pipelines_merge_requests.rb
+++ b/db/migrate/20181120091639_add_foreign_key_to_ci_pipelines_merge_requests.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-class AddForeignKeyToCiPipelinesMergeRequests < ActiveRecord::Migration
+class AddForeignKeyToCiPipelinesMergeRequests < ActiveRecord::Migration[4.2]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
@@ -8,7 +8,7 @@ class AddForeignKeyToCiPipelinesMergeRequests < ActiveRecord::Migration
disable_ddl_transaction!
def up
- add_concurrent_index :ci_pipelines, :merge_request_id
+ add_concurrent_index :ci_pipelines, :merge_request_id, where: 'merge_request_id IS NOT NULL'
add_concurrent_foreign_key :ci_pipelines, :merge_requests, column: :merge_request_id, on_delete: :cascade
end
@@ -17,6 +17,6 @@ class AddForeignKeyToCiPipelinesMergeRequests < ActiveRecord::Migration
remove_foreign_key :ci_pipelines, :merge_requests
end
- remove_concurrent_index :ci_pipelines, :merge_request_id
+ remove_concurrent_index :ci_pipelines, :merge_request_id, where: 'merge_request_id IS NOT NULL'
end
end
diff --git a/db/migrate/20181120151656_add_token_encrypted_to_ci_runners.rb b/db/migrate/20181120151656_add_token_encrypted_to_ci_runners.rb
index 40db6b399ab..8b990451adc 100644
--- a/db/migrate/20181120151656_add_token_encrypted_to_ci_runners.rb
+++ b/db/migrate/20181120151656_add_token_encrypted_to_ci_runners.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-class AddTokenEncryptedToCiRunners < ActiveRecord::Migration
+class AddTokenEncryptedToCiRunners < ActiveRecord::Migration[4.2]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
diff --git a/db/migrate/20181121101842_add_ci_builds_partial_index_on_project_id_and_status.rb b/db/migrate/20181121101842_add_ci_builds_partial_index_on_project_id_and_status.rb
new file mode 100644
index 00000000000..a524709faf8
--- /dev/null
+++ b/db/migrate/20181121101842_add_ci_builds_partial_index_on_project_id_and_status.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddCiBuildsPartialIndexOnProjectIdAndStatus < ActiveRecord::Migration[4.2]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index(*index_arguments)
+ end
+
+ def down
+ remove_concurrent_index(*index_arguments)
+ end
+
+ private
+
+ def index_arguments
+ [
+ :ci_builds,
+ [:project_id, :status],
+ {
+ name: 'index_ci_builds_project_id_and_status_for_live_jobs_partial2',
+ where: "(((type)::text = 'Ci::Build'::text) AND ((status)::text = ANY (ARRAY[('running'::character varying)::text, ('pending'::character varying)::text, ('created'::character varying)::text])))"
+ }
+ ]
+ end
+end
diff --git a/db/migrate/20181121101843_remove_redundant_ci_builds_partial_index.rb b/db/migrate/20181121101843_remove_redundant_ci_builds_partial_index.rb
new file mode 100644
index 00000000000..e4fb703e887
--- /dev/null
+++ b/db/migrate/20181121101843_remove_redundant_ci_builds_partial_index.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class RemoveRedundantCiBuildsPartialIndex < ActiveRecord::Migration[4.2]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ remove_concurrent_index(*index_arguments)
+ end
+
+ def down
+ add_concurrent_index(*index_arguments)
+ end
+
+ private
+
+ def index_arguments
+ [
+ :ci_builds,
+ [:project_id, :status],
+ {
+ name: 'index_ci_builds_project_id_and_status_for_live_jobs_partial',
+ where: "((status)::text = ANY (ARRAY[('running'::character varying)::text, ('pending'::character varying)::text, ('created'::character varying)::text]))"
+ }
+ ]
+ end
+end
diff --git a/db/migrate/20181128123704_add_state_to_pool_repository.rb b/db/migrate/20181128123704_add_state_to_pool_repository.rb
new file mode 100644
index 00000000000..714232ede56
--- /dev/null
+++ b/db/migrate/20181128123704_add_state_to_pool_repository.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class AddStateToPoolRepository < ActiveRecord::Migration[5.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ # Given the table is empty, and the non concurrent methods are chosen so
+ # the transactions don't have to be disabled
+ # rubocop: disable Migration/AddConcurrentForeignKey, Migration/AddIndex
+ def change
+ add_column(:pool_repositories, :state, :string, null: true)
+
+ add_column :pool_repositories, :source_project_id, :integer
+ add_index :pool_repositories, :source_project_id, unique: true
+ add_foreign_key :pool_repositories, :projects, column: :source_project_id, on_delete: :nullify
+ end
+ # rubocop: enable Migration/AddConcurrentForeignKey, Migration/AddIndex
+end
diff --git a/db/migrate/20181129104854_add_token_encrypted_to_ci_builds.rb b/db/migrate/20181129104854_add_token_encrypted_to_ci_builds.rb
new file mode 100644
index 00000000000..11b98203793
--- /dev/null
+++ b/db/migrate/20181129104854_add_token_encrypted_to_ci_builds.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class AddTokenEncryptedToCiBuilds < ActiveRecord::Migration[5.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ add_column :ci_builds, :token_encrypted, :string
+ end
+end
diff --git a/db/migrate/20181129104944_add_index_to_ci_builds_token_encrypted.rb b/db/migrate/20181129104944_add_index_to_ci_builds_token_encrypted.rb
new file mode 100644
index 00000000000..f90aca008e5
--- /dev/null
+++ b/db/migrate/20181129104944_add_index_to_ci_builds_token_encrypted.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class AddIndexToCiBuildsTokenEncrypted < ActiveRecord::Migration[5.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :ci_builds, :token_encrypted, unique: true, where: 'token_encrypted IS NOT NULL'
+ end
+
+ def down
+ remove_concurrent_index :ci_builds, :token_encrypted
+ end
+end
diff --git a/db/migrate/20181203002526_add_project_bfg_object_map_column.rb b/db/migrate/20181203002526_add_project_bfg_object_map_column.rb
new file mode 100644
index 00000000000..8b42cd6f941
--- /dev/null
+++ b/db/migrate/20181203002526_add_project_bfg_object_map_column.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class AddProjectBfgObjectMapColumn < ActiveRecord::Migration[5.0]
+ DOWNTIME = false
+
+ def change
+ add_column :projects, :bfg_object_map, :string
+ end
+end
diff --git a/db/post_migrate/20161221153951_rename_reserved_project_names.rb b/db/post_migrate/20161221153951_rename_reserved_project_names.rb
index b7665e98490..50e1c8449ba 100644
--- a/db/post_migrate/20161221153951_rename_reserved_project_names.rb
+++ b/db/post_migrate/20161221153951_rename_reserved_project_names.rb
@@ -1,6 +1,5 @@
class RenameReservedProjectNames < ActiveRecord::Migration[4.2]
include Gitlab::Database::MigrationHelpers
- include Gitlab::ShellAdapter
DOWNTIME = false
diff --git a/db/post_migrate/20170313133418_rename_more_reserved_project_names.rb b/db/post_migrate/20170313133418_rename_more_reserved_project_names.rb
index cac3fd713eb..bef669b459d 100644
--- a/db/post_migrate/20170313133418_rename_more_reserved_project_names.rb
+++ b/db/post_migrate/20170313133418_rename_more_reserved_project_names.rb
@@ -1,6 +1,5 @@
class RenameMoreReservedProjectNames < ActiveRecord::Migration[4.2]
include Gitlab::Database::MigrationHelpers
- include Gitlab::ShellAdapter
DOWNTIME = false
diff --git a/db/post_migrate/20181010133639_backfill_store_project_full_path_in_repo.rb b/db/post_migrate/20181010133639_backfill_store_project_full_path_in_repo.rb
index e9ab45ae9a1..247f5980f7e 100644
--- a/db/post_migrate/20181010133639_backfill_store_project_full_path_in_repo.rb
+++ b/db/post_migrate/20181010133639_backfill_store_project_full_path_in_repo.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-class BackfillStoreProjectFullPathInRepo < ActiveRecord::Migration
+class BackfillStoreProjectFullPathInRepo < ActiveRecord::Migration[4.2]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
diff --git a/db/post_migrate/20181026091631_migrate_forbidden_redirect_uris.rb b/db/post_migrate/20181026091631_migrate_forbidden_redirect_uris.rb
index ff5510e8eb7..7c2df832882 100644
--- a/db/post_migrate/20181026091631_migrate_forbidden_redirect_uris.rb
+++ b/db/post_migrate/20181026091631_migrate_forbidden_redirect_uris.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-class MigrateForbiddenRedirectUris < ActiveRecord::Migration
+class MigrateForbiddenRedirectUris < ActiveRecord::Migration[4.2]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
diff --git a/db/post_migrate/20181121111200_schedule_runners_token_encryption.rb b/db/post_migrate/20181121111200_schedule_runners_token_encryption.rb
index 753e052f7a7..ba82072fc98 100644
--- a/db/post_migrate/20181121111200_schedule_runners_token_encryption.rb
+++ b/db/post_migrate/20181121111200_schedule_runners_token_encryption.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-class ScheduleRunnersTokenEncryption < ActiveRecord::Migration
+class ScheduleRunnersTokenEncryption < ActiveRecord::Migration[4.2]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
diff --git a/db/post_migrate/20181130102132_backfill_hashed_project_repositories.rb b/db/post_migrate/20181130102132_backfill_hashed_project_repositories.rb
new file mode 100644
index 00000000000..7814cdba58a
--- /dev/null
+++ b/db/post_migrate/20181130102132_backfill_hashed_project_repositories.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+class BackfillHashedProjectRepositories < ActiveRecord::Migration[4.2]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ BATCH_SIZE = 1_000
+ DELAY_INTERVAL = 5.minutes
+ MIGRATION = 'BackfillHashedProjectRepositories'
+
+ disable_ddl_transaction!
+
+ class Project < ActiveRecord::Base
+ include EachBatch
+
+ self.table_name = 'projects'
+ end
+
+ def up
+ queue_background_migration_jobs_by_range_at_intervals(Project, MIGRATION, DELAY_INTERVAL)
+ end
+
+ def down
+ # no-op: since there could have been existing rows before the migration do not remove anything
+ end
+end
diff --git a/db/post_migrate/20181204154019_populate_mr_metrics_with_events_data.rb b/db/post_migrate/20181204154019_populate_mr_metrics_with_events_data.rb
new file mode 100644
index 00000000000..1e43e3dd790
--- /dev/null
+++ b/db/post_migrate/20181204154019_populate_mr_metrics_with_events_data.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class PopulateMrMetricsWithEventsData < ActiveRecord::Migration[4.2]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ BATCH_SIZE = 10_000
+ MIGRATION = 'PopulateMergeRequestMetricsWithEventsDataImproved'
+ PREVIOUS_MIGRATION = 'PopulateMergeRequestMetricsWithEventsData'
+
+ disable_ddl_transaction!
+
+ def up
+ # Perform any ongoing background migration that might still be running from
+ # previous try (see https://gitlab.com/gitlab-org/gitlab-ce/issues/47676).
+ Gitlab::BackgroundMigration.steal(PREVIOUS_MIGRATION)
+
+ say 'Scheduling `PopulateMergeRequestMetricsWithEventsData` jobs'
+ # It will update around 4_000_000 records in batches of 10_000 merge
+ # requests (running between 5 minutes) and should take around 53 hours to complete.
+ # Apparently, production PostgreSQL is able to vacuum 10k-20k dead_tuples
+ # per minute. So this should give us enough space.
+ #
+ # More information about the updates in `PopulateMergeRequestMetricsWithEventsDataImproved` class.
+ #
+ MergeRequest.all.each_batch(of: BATCH_SIZE) do |relation, index|
+ range = relation.pluck('MIN(id)', 'MAX(id)').first
+
+ BackgroundMigrationWorker.perform_in(index * 8.minutes, MIGRATION, range)
+ end
+ end
+
+ def down
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 65a69c2850c..e5e19eb7745 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20181126153547) do
+ActiveRecord::Schema.define(version: 20181204154019) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -345,6 +345,7 @@ ActiveRecord::Schema.define(version: 20181126153547) do
t.boolean "protected"
t.integer "failure_reason"
t.datetime_with_timezone "scheduled_at"
+ t.string "token_encrypted"
t.index ["artifacts_expire_at"], name: "index_ci_builds_on_artifacts_expire_at", where: "(artifacts_file <> ''::text)", using: :btree
t.index ["auto_canceled_by_id"], name: "index_ci_builds_on_auto_canceled_by_id", using: :btree
t.index ["commit_id", "stage_idx", "created_at"], name: "index_ci_builds_on_commit_id_and_stage_idx_and_created_at", using: :btree
@@ -353,6 +354,7 @@ ActiveRecord::Schema.define(version: 20181126153547) do
t.index ["commit_id", "type", "ref"], name: "index_ci_builds_on_commit_id_and_type_and_ref", using: :btree
t.index ["id"], name: "partial_index_ci_builds_on_id_with_legacy_artifacts", where: "(artifacts_file <> ''::text)", using: :btree
t.index ["project_id", "id"], name: "index_ci_builds_on_project_id_and_id", using: :btree
+ t.index ["project_id", "status"], name: "index_ci_builds_project_id_and_status_for_live_jobs_partial2", where: "(((type)::text = 'Ci::Build'::text) AND ((status)::text = ANY (ARRAY[('running'::character varying)::text, ('pending'::character varying)::text, ('created'::character varying)::text])))", using: :btree
t.index ["protected"], name: "index_ci_builds_on_protected", using: :btree
t.index ["runner_id"], name: "index_ci_builds_on_runner_id", using: :btree
t.index ["scheduled_at"], name: "partial_index_ci_builds_on_scheduled_at_with_scheduled_jobs", where: "((scheduled_at IS NOT NULL) AND ((type)::text = 'Ci::Build'::text) AND ((status)::text = 'scheduled'::text))", using: :btree
@@ -360,6 +362,7 @@ ActiveRecord::Schema.define(version: 20181126153547) do
t.index ["stage_id"], name: "index_ci_builds_on_stage_id", using: :btree
t.index ["status", "type", "runner_id"], name: "index_ci_builds_on_status_and_type_and_runner_id", using: :btree
t.index ["token"], name: "index_ci_builds_on_token", unique: true, using: :btree
+ t.index ["token_encrypted"], name: "index_ci_builds_on_token_encrypted", unique: true, where: "(token_encrypted IS NOT NULL)", using: :btree
t.index ["updated_at"], name: "index_ci_builds_on_updated_at", using: :btree
t.index ["user_id"], name: "index_ci_builds_on_user_id", using: :btree
end
@@ -476,7 +479,7 @@ ActiveRecord::Schema.define(version: 20181126153547) do
t.integer "iid"
t.integer "merge_request_id"
t.index ["auto_canceled_by_id"], name: "index_ci_pipelines_on_auto_canceled_by_id", using: :btree
- t.index ["merge_request_id"], name: "index_ci_pipelines_on_merge_request_id", using: :btree
+ t.index ["merge_request_id"], name: "index_ci_pipelines_on_merge_request_id", where: "(merge_request_id IS NOT NULL)", using: :btree
t.index ["pipeline_schedule_id"], name: "index_ci_pipelines_on_pipeline_schedule_id", using: :btree
t.index ["project_id", "iid"], name: "index_ci_pipelines_on_project_id_and_iid", unique: true, where: "(iid IS NOT NULL)", using: :btree
t.index ["project_id", "ref", "status", "id"], name: "index_ci_pipelines_on_project_id_and_ref_and_status_and_id", using: :btree
@@ -1509,8 +1512,11 @@ ActiveRecord::Schema.define(version: 20181126153547) do
create_table "pool_repositories", id: :bigserial, force: :cascade do |t|
t.integer "shard_id", null: false
t.string "disk_path"
+ t.string "state"
+ t.integer "source_project_id"
t.index ["disk_path"], name: "index_pool_repositories_on_disk_path", unique: true, using: :btree
t.index ["shard_id"], name: "index_pool_repositories_on_shard_id", using: :btree
+ t.index ["source_project_id"], name: "index_pool_repositories_on_source_project_id", unique: true, using: :btree
end
create_table "programming_languages", force: :cascade do |t|
@@ -1681,6 +1687,7 @@ ActiveRecord::Schema.define(version: 20181126153547) do
t.boolean "remote_mirror_available_overridden"
t.bigint "pool_repository_id"
t.string "runners_token_encrypted"
+ t.string "bfg_object_map"
t.index ["ci_id"], name: "index_projects_on_ci_id", using: :btree
t.index ["created_at"], name: "index_projects_on_created_at", using: :btree
t.index ["creator_id"], name: "index_projects_on_creator_id", using: :btree
@@ -2389,6 +2396,7 @@ ActiveRecord::Schema.define(version: 20181126153547) do
add_foreign_key "oauth_openid_requests", "oauth_access_grants", column: "access_grant_id", name: "fk_oauth_openid_requests_oauth_access_grants_access_grant_id"
add_foreign_key "pages_domains", "projects", name: "fk_ea2f6dfc6f", on_delete: :cascade
add_foreign_key "personal_access_tokens", "users"
+ add_foreign_key "pool_repositories", "projects", column: "source_project_id", on_delete: :nullify
add_foreign_key "pool_repositories", "shards", on_delete: :restrict
add_foreign_key "project_authorizations", "projects", on_delete: :cascade
add_foreign_key "project_authorizations", "users", on_delete: :cascade
diff --git a/doc/administration/auth/README.md b/doc/administration/auth/README.md
index 373d4239f71..54be7b616cc 100644
--- a/doc/administration/auth/README.md
+++ b/doc/administration/auth/README.md
@@ -10,7 +10,7 @@ providers.
- [LDAP](ldap.md) Includes Active Directory, Apple Open Directory, Open LDAP,
and 389 Server
- [OmniAuth](../../integration/omniauth.md) Sign in via Twitter, GitHub, GitLab.com, Google,
- Bitbucket, Facebook, Shibboleth, Crowd, Azure and Authentiq ID
+ Bitbucket, Facebook, Shibboleth, Crowd, Azure, Authentiq ID, and JWT
- [CAS](../../integration/cas.md) Configure GitLab to sign in using CAS
- [SAML](../../integration/saml.md) Configure GitLab as a SAML 2.0 Service Provider
- [Okta](okta.md) Configure GitLab to sign in using Okta
diff --git a/doc/administration/auth/jwt.md b/doc/administration/auth/jwt.md
index 8b00f52ffc1..497298503ad 100644
--- a/doc/administration/auth/jwt.md
+++ b/doc/administration/auth/jwt.md
@@ -26,15 +26,15 @@ JWT will provide you with a secret key for you to use.
```ruby
gitlab_rails['omniauth_providers'] = [
{ name: 'jwt',
- app_secret: 'YOUR_APP_SECRET',
args: {
- algorithm: 'HS256',
- uid_claim: 'email',
- required_claims: ["name", "email"],
- info_maps: { name: "name", email: "email" },
- auth_url: 'https://example.com/',
- valid_within: nil,
- }
+ secret: 'YOUR_APP_SECRET',
+ algorithm: 'HS256', # Supported algorithms: 'RS256', 'RS384', 'RS512', 'ES256', 'ES384', 'ES512', 'HS256', 'HS384', 'HS512'
+ uid_claim: 'email',
+ required_claims: ['name', 'email'],
+ info_maps: { name: 'name', email: 'email' },
+ auth_url: 'https://example.com/',
+ valid_within: 3600 # 1 hour
+ }
}
]
```
@@ -43,15 +43,15 @@ JWT will provide you with a secret key for you to use.
```
- { name: 'jwt',
- app_secret: 'YOUR_APP_SECRET',
args: {
- algorithm: 'HS256',
- uid_claim: 'email',
- required_claims: ["name", "email"],
- info_map: { name: "name", email: "email" },
- auth_url: 'https://example.com/',
- valid_within: null,
- }
+ secret: 'YOUR_APP_SECRET',
+ algorithm: 'HS256', # Supported algorithms: 'RS256', 'RS384', 'RS512', 'ES256', 'ES384', 'ES512', 'HS256', 'HS384', 'HS512'
+ uid_claim: 'email',
+ required_claims: ['name', 'email'],
+ info_map: { name: 'name', email: 'email' },
+ auth_url: 'https://example.com/',
+ valid_within: 3600 # 1 hour
+ }
}
```
@@ -60,7 +60,7 @@ JWT will provide you with a secret key for you to use.
1. Change `YOUR_APP_SECRET` to the client secret and set `auth_url` to your redirect URL.
1. Save the configuration file.
-1. [Reconfigure GitLab][] or [restart GitLab][] for the changes to take effect if you
+1. [Reconfigure][] 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 JWT icon below the regular sign in form.
@@ -68,5 +68,5 @@ Click the icon to begin the authentication process. JWT will ask the user to
sign in and authorize the GitLab application. If everything goes well, the user
will be redirected to GitLab and will be signed in.
-[reconfigure GitLab]: ../restart_gitlab.md#omnibus-gitlab-reconfigure
+[reconfigure]: ../restart_gitlab.md#omnibus-gitlab-reconfigure
[restart GitLab]: ../restart_gitlab.md#installations-from-source
diff --git a/doc/administration/gitaly/index.md b/doc/administration/gitaly/index.md
index dc6a71e2ebd..d7c45e7d91d 100644
--- a/doc/administration/gitaly/index.md
+++ b/doc/administration/gitaly/index.md
@@ -49,6 +49,25 @@ Starting with GitLab 11.4, Gitaly is a replacement for NFS except
when the [Elastic Search indexer](https://gitlab.com/gitlab-org/gitlab-elasticsearch-indexer)
is used.
+### Network architecture
+
+- gitlab-rails shards repositories into "repository storages"
+- gitlab-rails/config/gitlab.yml contains a map from storage names to
+ (Gitaly address, Gitaly token) pairs
+- the `storage name` -\> `(Gitaly address, Gitaly token)` map in
+ gitlab.yml is the single source of truth for the Gitaly network
+ topology
+- a (Gitaly address, Gitaly token) corresponds to a Gitaly server
+- a Gitaly server hosts one or more storages
+- Gitaly addresses must be specified in such a way that they resolve
+ correctly for ALL Gitaly clients
+- Gitaly clients are: unicorn, sidekiq, gitlab-workhorse,
+ gitlab-shell, and Gitaly itself
+- special case: a Gitaly server must be able to make RPC calls **to
+ itself** via its own (Gitaly address, Gitaly token) pair as
+ specified in gitlab-rails/config/gitlab.yml
+- Gitaly servers must not be exposed to the public internet
+
Gitaly network traffic is unencrypted so you should use a firewall to
restrict access to your Gitaly server.
diff --git a/doc/administration/repository_storage_paths.md b/doc/administration/repository_storage_paths.md
index c03f23a8931..7f25423171f 100644
--- a/doc/administration/repository_storage_paths.md
+++ b/doc/administration/repository_storage_paths.md
@@ -11,6 +11,25 @@ storage load between several mount points.
> - The paths are defined in key-value pairs. The key is an arbitrary name you
> can pick to name the file path.
> - The target directories and any of its subpaths must not be a symlink.
+> - No target directory may be a sub-directory of another; no nesting.
+
+Example: this is OK:
+
+```
+default:
+ path: /mnt/git-storage-1
+storage2:
+ path: /mnt/git-storage-2
+```
+
+This is not OK because it nests storage paths:
+
+```
+default:
+ path: /mnt/git-storage-1
+storage2:
+ path: /mnt/git-storage-1/git-storage-2 # <- NOT OK because of nesting
+```
## Configure GitLab
diff --git a/doc/administration/repository_storage_types.md b/doc/administration/repository_storage_types.md
index 9379944b250..12238ba7b32 100644
--- a/doc/administration/repository_storage_types.md
+++ b/doc/administration/repository_storage_types.md
@@ -94,6 +94,23 @@ need to be performed on these nodes as well. Database changes will propagate wit
You must make sure the migration event was already processed or otherwise it may migrate
the files back to Hashed state again.
+#### Hashed object pools
+
+For deduplication of public forks and their parent repository, objects are pooled
+in an object pool. These object pools are a third repository where shared objects
+are stored.
+
+```ruby
+# object pool paths
+"@pools/#{hash[0..1]}/#{hash[2..3]}/#{hash}.git"
+```
+
+The object pool feature is behind the `object_pools` feature flag, and can be
+enabled for individual projects by executing
+`Feature.enable(:object_pools, Project.find(<id>))`. Note that the project has to
+be on hashed storage, should not be a fork itself, and hashed storage should be
+enabled for all new projects.
+
##### Attachments
To rollback single Attachment migration, rename `aa/bb/abcdef1234567890...` folder back to `namespace/project`.
diff --git a/doc/api/README.md b/doc/api/README.md
index b49c3a198f1..fd5e88cb9d5 100644
--- a/doc/api/README.md
+++ b/doc/api/README.md
@@ -5,78 +5,86 @@ under [`/lib/api`](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/lib/api).
The main GitLab API is a [REST](https://en.wikipedia.org/wiki/Representational_state_transfer) API. Therefore, documentation in this section assumes knowledge of REST concepts.
-## Resources
+## API Resources
-Documentation for various API resources can be found separately in the
-following locations:
+The following API resources are available:
-- [Award Emoji](award_emoji.md)
+- [Applications](applications.md)
+- [Avatar](avatar.md)
+- [Award emoji](award_emoji.md)
- [Branches](branches.md)
-- [Broadcast Messages](broadcast_messages.md)
-- [Project-level Variables](project_level_variables.md)
-- [Group-level Variables](group_level_variables.md)
-- [Code Snippets](snippets.md)
+- [Broadcast messages](broadcast_messages.md)
+- [Code snippets](snippets.md)
- [Commits](commits.md)
-- [Custom Attributes](custom_attributes.md)
+- [Custom attributes](custom_attributes.md)
+- [Deploy keys](deploy_keys.md), and [deploy keys for multiple projects](deploy_key_multiple_projects.md)
- [Deployments](deployments.md)
-- [Deploy Keys](deploy_keys.md)
-- [Dockerfile templates](templates/dockerfiles.md)
+- [Discussions](discussions.md) (threaded comments)
- [Environments](environments.md)
- [Events](events.md)
- [Feature flags](features.md)
-- [Gitignore templates](templates/gitignores.md)
-- [GitLab CI Config templates](templates/gitlab_ci_ymls.md)
-- [Groups](groups.md)
-- [Group Access Requests](access_requests.md)
-- [Group Badges](group_badges.md)
-- [Group Members](members.md)
+- Group-related resources, including:
+ - [Groups](groups.md)
+ - [Group access requests](access_requests.md)
+ - [Group badges](group_badges.md)
+ - [Group issue boards](group_boards.md)
+ - [Group-level variables](group_level_variables.md)
+ - [Group members](members.md)
+ - [Group milestones](group_milestones.md)
- [Issues](issues.md)
-- [Issue Boards](boards.md)
-- [Group Issue Boards](group_boards.md)
+- [Issue boards](boards.md)
- [Jobs](jobs.md)
- [Keys](keys.md)
- [Labels](labels.md)
- [Markdown](markdown.md)
-- [Merge Requests](merge_requests.md)
-- [Project milestones](milestones.md)
-- [Group milestones](group_milestones.md)
+- [Merge requests](merge_requests.md)
- [Namespaces](namespaces.md)
- [Notes](notes.md) (comments)
-- [Discussions](discussions.md) (threaded comments)
-- [Resource Label Events](resource_label_events.md)
- [Notification settings](notification_settings.md)
-- [Open source license templates](templates/licenses.md)
-- [Pages Domains](pages_domains.md)
+- [Pages domains](pages_domains.md)
- [Pipelines](pipelines.md)
-- [Pipeline Triggers](pipeline_triggers.md)
-- [Pipeline Schedules](pipeline_schedules.md)
-- [Projects](projects.md) including setting Webhooks
-- [Project Access Requests](access_requests.md)
-- [Project Badges](project_badges.md)
-- [Project import/export](project_import_export.md)
-- [Project Members](members.md)
-- [Project Snippets](project_snippets.md)
-- [Project Templates](project_templates.md)
-- [Protected Branches](protected_branches.md)
-- [Protected Tags](protected_tags.md)
+- [Pipeline schedules](pipeline_schedules.md)
+- [Pipeline triggers](pipeline_triggers.md) and [triggering pipelines](../ci/triggers/README.md)
+- Project-related resources, including:
+ - [Projects](projects.md) including setting Webhooks
+ - [Project access requests](access_requests.md)
+ - [Project badges](project_badges.md)
+ - [Project-level variables](project_level_variables.md)
+ - [Project import/export](project_import_export.md)
+ - [Project members](members.md)
+ - [Project milestones](milestones.md)
+ - [Project snippets](project_snippets.md)
+ - [Project templates](project_templates.md) (see also [Templates API Resources](#templates-api-resources))
+- [Protected branches](protected_branches.md)
+- [Protected tags](protected_tags.md)
- [Repositories](repositories.md)
-- [Repository Files](repository_files.md)
-- [Repository Submodules](repository_submodules.md)
+- [Repository files](repository_files.md)
+- [Repository submodules](repository_submodules.md)
+- [Resource label events](resource_label_events.md)
- [Runners](runners.md)
- [Search](search.md)
- [Services](services.md)
- [Settings](settings.md)
- [Sidekiq metrics](sidekiq_metrics.md)
-- [System Hooks](system_hooks.md)
+- [System hooks](system_hooks.md)
- [Tags](tags.md)
- [Todos](todos.md)
-- [Triggering Pipelines](../ci/triggers/README.md)
- [Users](users.md)
-- [Validate CI configuration](lint.md)
-- [V3 to V4](v3_to_v4.md)
+- [Validate CI configuration](lint.md) (linting)
- [Version](version.md)
- [Wikis](wikis.md)
+See also [V3 to V4](v3_to_v4.md).
+
+### Templates API Resources
+
+Endpoints are available for:
+
+- [Dockerfile templates](templates/dockerfiles.md).
+- [gitignore templates](templates/gitignores.md).
+- [GitLab CI YAML templates](templates/gitlab_ci_ymls.md).
+- [Open source license templates](templates/licenses.md).
+
## Road to GraphQL
Going forward, we will start on moving to
@@ -98,7 +106,7 @@ specification.
## Compatibility Guidelines
The HTTP API is versioned using a single number, the current one being 4. This
-number symbolises the same as the major version number as described by
+number symbolizes the same as the major version number as described by
[SemVer](https://semver.org/). This mean that backward incompatible changes
will require this version number to change. However, the minor version is
not explicit. This allows for a stable API endpoint, but also means new
diff --git a/doc/api/jobs.md b/doc/api/jobs.md
index aa290ff4cf8..589c48ee08d 100644
--- a/doc/api/jobs.md
+++ b/doc/api/jobs.md
@@ -404,7 +404,7 @@ Example response:
[ce-5347]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5347
-## Download a single artifact file
+## Download a single artifact file by job ID
> Introduced in GitLab 10.0
@@ -438,6 +438,41 @@ Example response:
| 400 | Invalid path provided |
| 404 | Build not found or no file/artifacts |
+## Download a single artifact file from specific tag or branch
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/23538) in GitLab 11.5.
+
+Download a single artifact file from a specific tag or branch from within the
+job's artifacts archive. The file is extracted from the archive and streamed to
+the client.
+
+```
+GET /projects/:id/jobs/artifacts/:ref_name/raw/*artifact_path?job=name
+```
+
+Parameters:
+
+| 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. |
+| `ref_name` | string | yes | Branch or tag name in repository. HEAD or SHA references are not supported. |
+| `artifact_path` | string | yes | Path to a file inside the artifacts archive. |
+| `job` | string | yes | The name of the job. |
+
+Example request:
+
+```sh
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/jobs/artifacts/master/raw/some/release/file.pdf?job=pdf"
+```
+
+Possible response status codes:
+
+| Status | Description |
+|-----------|--------------------------------------|
+| 200 | Sends a single artifact file |
+| 400 | Invalid path provided |
+| 404 | Build not found or no file/artifacts |
+
## Get a trace file
Get a trace of a specific job of a project
diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md
index fc03cf6cc39..9ff6c73b1b6 100644
--- a/doc/api/merge_requests.md
+++ b/doc/api/merge_requests.md
@@ -974,10 +974,9 @@ curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://git
Merge changes submitted with MR using this API.
+If merge request is unable to be accepted (ie: Work in Progress, Closed, Pipeline Pending Completion, or Failed while requiring Success) - you'll get a `405` and the error message 'Method Not Allowed'
-If it has some conflicts and can not be merged - you'll get a `405` and the error message 'Branch cannot be merged'
-
-If merge request is already merged or closed - you'll get a `406` and the error message 'Method Not Allowed'
+If it has some conflicts and can not be merged - you'll get a `406` and the error message 'Branch cannot be merged'
If the `sha` parameter is passed and does not match the HEAD of the source - you'll get a `409` and the error message 'SHA does not match HEAD of source branch'
diff --git a/doc/api/milestones.md b/doc/api/milestones.md
index 8f1a5c8e19b..7ac97edc7ae 100644
--- a/doc/api/milestones.md
+++ b/doc/api/milestones.md
@@ -1,4 +1,4 @@
-# Milestones API
+# Project milestones API
## List project milestones
@@ -45,7 +45,6 @@ Example Response:
]
```
-
## Get single milestone
Gets a single project milestone.
diff --git a/doc/api/search.md b/doc/api/search.md
index 9716f682ace..7e3ae7404a3 100644
--- a/doc/api/search.md
+++ b/doc/api/search.md
@@ -722,6 +722,23 @@ Example response:
### Scope: wiki_blobs
+Filters are available for this scope:
+
+- filename
+- path
+- extension
+
+To use a filter simply include it in your query like: `a query filename:some_name*`.
+You may use wildcards (`*`) to use glob matching.
+
+Wiki blobs searches are performed on both filenames and contents. Search
+results:
+
+- Found in filenames are displayed before results found in contents.
+- May contain multiple matches for the same blob because the search string
+ might be found in both the filename and content, or might appear multiple
+ times in the content.
+
```bash
curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/6/search?scope=wiki_blobs&search=bye
```
@@ -777,14 +794,21 @@ Example response:
### Scope: blobs
Filters are available for this scope:
+
- filename
- path
- extension
-to use a filter simply include it in your query like so: `a query filename:some_name*`.
-
+To use a filter simply include it in your query like: `a query filename:some_name*`.
You may use wildcards (`*`) to use glob matching.
+Blobs searches are performed on both filenames and contents. Search results:
+
+- Found in filenames are displayed before results found in contents.
+- May contain multiple matches for the same blob because the search string
+ might be found in both the filename and content, or might appear multiple
+ times in the content.
+
```bash
curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/6/search?scope=blobs&search=installation
```
diff --git a/doc/ci/README.md b/doc/ci/README.md
index dba1f38abe2..4e066a0df97 100644
--- a/doc/ci/README.md
+++ b/doc/ci/README.md
@@ -71,6 +71,7 @@ learn how to leverage its potential even more.
- [Caching dependencies](caching/index.md)
- [Git submodules](git_submodules.md) - How to run your CI jobs when Git
submodules are involved
+- [Pipelines for merge requests](merge_request_pipelines/index.md)
- [Use SSH keys in your build environment](ssh_keys/README.md)
- [Trigger pipelines through the GitLab API](triggers/README.md)
- [Trigger pipelines on a schedule](../user/project/pipelines/schedules.md)
diff --git a/doc/ci/caching/index.md b/doc/ci/caching/index.md
index 758ab37861b..f93ccc4e3c1 100644
--- a/doc/ci/caching/index.md
+++ b/doc/ci/caching/index.md
@@ -29,8 +29,8 @@ needed to compile the project:
Cache was designed to be used to speed up invocations of subsequent runs of a
given job, by keeping things like dependencies (e.g., npm packages, Go vendor
packages, etc.) so they don't have to be re-fetched from the public internet.
- While the cache can be abused to pass intermediate build results between stages,
- there may be cases where artifacts are a better fit.
+ While the cache can be abused to pass intermediate build results between
+ stages, there may be cases where artifacts are a better fit.
- `artifacts`: **Use for stage results that will be passed between stages.**
Artifacts were designed to upload some compiled/generated bits of the build,
and they can be fetched by any number of concurrent Runners. They are
@@ -39,11 +39,13 @@ needed to compile the project:
directories relative to the build directory** and specifying paths which don't
comply to this rule trigger an unintuitive and illogical error message (an
enhancement is discussed at
- https://gitlab.com/gitlab-org/gitlab-ce/issues/15530). Artifacts need to be
- uploaded to the GitLab instance (not only the GitLab runner) before the next
- stage job(s) can start, so you need to evaluate carefully whether your
- bandwidth allows you to profit from parallelization with stages and shared
- artifacts before investing time in changes to the setup.
+ [https://gitlab.com/gitlab-org/gitlab-ce/issues/15530](https://gitlab.com/gitlab-org/gitlab-ce/issues/15530)
+ ). Artifacts need to be uploaded to the GitLab instance (not only the GitLab
+ runner) before the next stage job(s) can start, so you need to evaluate
+ carefully whether your bandwidth allows you to profit from parallelization
+ with stages and shared artifacts before investing time in changes to the
+ setup.
+
It's sometimes confusing because the name artifact sounds like something that
is only useful outside of the job, like for downloading a final image. But
diff --git a/doc/ci/interactive_web_terminal/index.md b/doc/ci/interactive_web_terminal/index.md
index 2c799e83a5f..d8136b80c11 100644
--- a/doc/ci/interactive_web_terminal/index.md
+++ b/doc/ci/interactive_web_terminal/index.md
@@ -1,4 +1,4 @@
-# Getting started with interactive web terminals **[CORE ONLY]**
+# Interactive Web Terminals **[CORE ONLY]**
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/50144) in GitLab 11.3.
@@ -15,7 +15,7 @@ progress.
Two things need to be configured for the interactive web terminal to work:
- The Runner needs to have [`[session_server]` configured
- properly][session-server]
+ properly](https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-session_server-section)
- If you are using a reverse proxy with your GitLab instance, web terminals need to be
[enabled](../../administration/integration/terminal.md#enabling-and-disabling-terminal-support)
@@ -45,9 +45,7 @@ the terminal and type commands like a normal shell.
If you have the terminal open and the job has finished with its tasks, the
terminal will block the job from finishing for the duration configured in
-[`[session_server].terminal_max_retention_time`][session-server] until you
+[`[session_server].terminal_max_retention_time`](https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-session_server-section) until you
close the terminal window.
![finished job with terminal open](img/finished_job_with_terminal_open.png)
-
-[session-server]: https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-session_server-section
diff --git a/doc/ci/merge_request_pipelines/img/merge_request.png b/doc/ci/merge_request_pipelines/img/merge_request.png
new file mode 100644
index 00000000000..1fe2eec2008
--- /dev/null
+++ b/doc/ci/merge_request_pipelines/img/merge_request.png
Binary files differ
diff --git a/doc/ci/merge_request_pipelines/img/pipeline_detail.png b/doc/ci/merge_request_pipelines/img/pipeline_detail.png
new file mode 100644
index 00000000000..def1781dd75
--- /dev/null
+++ b/doc/ci/merge_request_pipelines/img/pipeline_detail.png
Binary files differ
diff --git a/doc/ci/merge_request_pipelines/index.md b/doc/ci/merge_request_pipelines/index.md
new file mode 100644
index 00000000000..706e83abf44
--- /dev/null
+++ b/doc/ci/merge_request_pipelines/index.md
@@ -0,0 +1,84 @@
+# Pipelines for merge requests
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/15310) in GitLab 11.6
+
+Usually, when a developer creates a new merge request, a pipeline runs on the
+new change and checks if it's qualified to be merged into a target branch. This
+pipeline should contain only necessary jobs for checking the new changes.
+For example, unit tests, lint checks, and Review Apps are often used in this cycle.
+
+With pipelines for merge requests, you can design a specific pipeline structure
+for merge requests. All you need to do is just adding `only: [merge_requests]` to
+the jobs that you want it to run for only merge requests.
+Every time, when developers create or update merge requests, a pipeline runs on
+their new commits at every push to GitLab.
+
+NOTE: **Note**:
+If you use both this feature and the [Merge When Pipeline Succeeds](../../user/project/merge_requests/merge_when_pipeline_succeeds.md)
+feature, pipelines for merge requests take precendence over the other regular pipelines.
+
+For example, consider a GitLab CI/CD configuration in .gitlab-ci.yml as follows:
+
+```yaml
+build:
+ stage: build
+ script: ./build
+ only:
+ - branches
+ - tags
+ - merge_requests
+
+test:
+ stage: test
+ script: ./test
+ only:
+ - merge_requests
+
+deploy:
+ stage: deploy
+ script: ./deploy
+```
+
+After a developer updated code in a merge request with whatever methods (e.g. `git push`),
+GitLab detects that the code is updated and create a new pipeline for the merge request.
+The pipeline fetches the latest code from the source branch and run tests against it.
+In this example, the pipeline contains only `build` and `test` jobs.
+Since `deploy` job does not have the `only: [merge_requests]` rule,
+deployment jobs will not happen in the merge request.
+
+Consider this pipeline list viewed from the **Pipelines** tab in a merge request:
+
+![Merge request page](img/merge_request.png)
+
+Note that pipelines tagged as **merge request** indicate that they were triggered
+when a merge request was created or updated.
+
+The same tag is shown on the pipeline's details:
+
+![Pipeline's details](img/pipeline_detail.png)
+
+## Important notes about merge requests from forked projects
+
+Note that the current behavior is subject to change. In the usual contribution
+flow, external contributors follow the following steps:
+
+1. Fork a parent project.
+1. Create a merge request from the forked project that targets the `master` branch
+in the parent project.
+1. A pipeline runs on the merge request.
+1. A mainatiner from the parent project checks the pipeline result, and merge
+into a target branch if the latest pipeline has passed.
+
+Currently, those pipelines are created in a **forked** project, not in the
+parent project. This means you cannot completely trust the pipeline result,
+because, technically, external contributors can disguise their pipeline results
+by tweaking their GitLab Runner in the forked project.
+
+There are multiple reasons about why GitLab doesn't allow those pipelines to be
+created in the parent project, but one of the biggest reasons is security concern.
+External users could steal secret variables from the parent project by modifying
+.gitlab-ci.yml, which could be some sort of credentials. This should not happen.
+
+We're discussing a secure solution of running pipelines for merge requests
+that submitted from forked projects,
+see [the issue about the permission extension](https://gitlab.com/gitlab-org/gitlab-ce/issues/23902).
diff --git a/doc/ci/triggers/README.md b/doc/ci/triggers/README.md
index bffb0121603..c9a60feb73f 100644
--- a/doc/ci/triggers/README.md
+++ b/doc/ci/triggers/README.md
@@ -148,7 +148,7 @@ file. The parameter is of the form:
variables[key]=value
```
-This information is also exposed in the UI.
+This information is also exposed in the UI. Please note that _values_ are only viewable by Owners and Maintainers.
![Job variables in UI](img/trigger_variables.png)
@@ -172,6 +172,7 @@ stages:
- package
run_tests:
+ stage: test
script:
- make test
diff --git a/doc/ci/triggers/img/trigger_variables.png b/doc/ci/triggers/img/trigger_variables.png
index 0c2a761cfa9..f862155b47f 100644
--- a/doc/ci/triggers/img/trigger_variables.png
+++ b/doc/ci/triggers/img/trigger_variables.png
Binary files differ
diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md
index bdbcf8c9435..87799be8ab4 100644
--- a/doc/ci/variables/README.md
+++ b/doc/ci/variables/README.md
@@ -40,75 +40,86 @@ Starting with GitLab 9.0, we have deprecated some variables. Read the
strongly advised to use the new variables as we will remove the old ones in
future GitLab releases.**
-| Variable | GitLab | Runner | Description |
-|-------------------------------- |--------|--------|-------------|
-| **ARTIFACT_DOWNLOAD_ATTEMPTS** | 8.15 | 1.9 | Number of attempts to download artifacts running a job |
-| **CI** | all | 0.4 | Mark that job is executed in CI environment |
-| **CI_COMMIT_REF_NAME** | 9.0 | all | The branch or tag name for which project is built |
-| **CI_COMMIT_REF_SLUG** | 9.0 | all | `$CI_COMMIT_REF_NAME` lowercased, shortened to 63 bytes, and with everything except `0-9` and `a-z` replaced with `-`. No leading / trailing `-`. Use in URLs, host names and domain names. |
-| **CI_COMMIT_SHA** | 9.0 | all | The commit revision for which project is built |
-| **CI_COMMIT_BEFORE_SHA** | 11.2 | all | The previous latest commit present on a branch before a push request. |
-| **CI_COMMIT_TAG** | 9.0 | 0.5 | The commit tag name. Present only when building tags. |
-| **CI_COMMIT_MESSAGE** | 10.8 | all | The full commit message. |
-| **CI_COMMIT_TITLE** | 10.8 | all | The title of the commit - the full first line of the message |
-| **CI_COMMIT_DESCRIPTION** | 10.8 | all | The description of the commit: the message without first line, if the title is shorter than 100 characters; full message in other case. |
-| **CI_CONFIG_PATH** | 9.4 | 0.5 | The path to CI config file. Defaults to `.gitlab-ci.yml` |
-| **CI_DEBUG_TRACE** | all | 1.7 | Whether [debug tracing](#debug-tracing) is enabled |
-| **CI_DEPLOY_USER** | 10.8 | all | Authentication username of the [GitLab Deploy Token][gitlab-deploy-token], only present if the Project has one related.|
-| **CI_DEPLOY_PASSWORD** | 10.8 | all | Authentication password of the [GitLab Deploy Token][gitlab-deploy-token], only present if the Project has one related.|
-| **CI_DISPOSABLE_ENVIRONMENT** | all | 10.1 | Marks that the job is executed in a disposable environment (something that is created only for this job and disposed of/destroyed after the execution - all executors except `shell` and `ssh`). If the environment is disposable, it is set to true, otherwise it is not defined at all. |
-| **CI_ENVIRONMENT_NAME** | 8.15 | all | The name of the environment for this job |
-| **CI_ENVIRONMENT_SLUG** | 8.15 | all | A simplified version of the environment name, suitable for inclusion in DNS, URLs, Kubernetes labels, etc. |
-| **CI_ENVIRONMENT_URL** | 9.3 | all | The URL of the environment for this job |
-| **CI_JOB_ID** | 9.0 | all | The unique id of the current job that GitLab CI uses internally |
-| **CI_JOB_MANUAL** | 8.12 | all | The flag to indicate that job was manually started |
-| **CI_JOB_NAME** | 9.0 | 0.5 | The name of the job as defined in `.gitlab-ci.yml` |
-| **CI_JOB_STAGE** | 9.0 | 0.5 | The name of the stage as defined in `.gitlab-ci.yml` |
-| **CI_JOB_TOKEN** | 9.0 | 1.2 | Token used for authenticating with the [GitLab Container Registry][registry] and downloading [dependent repositories][dependent-repositories] |
-| **CI_NODE_INDEX** | 11.5 | all | Index of the job in the job set. If the job is not parallelized, this variable is not set. |
-| **CI_NODE_TOTAL** | 11.5 | all | Total number of instances of this job running in parallel. If the job is not parallelized, this variable is set to `1`. |
-| **CI_JOB_URL** | 11.1 | 0.5 | Job details URL |
-| **CI_REPOSITORY_URL** | 9.0 | all | The URL to clone the Git repository |
-| **CI_RUNNER_DESCRIPTION** | 8.10 | 0.5 | The description of the runner as saved in GitLab |
-| **CI_RUNNER_ID** | 8.10 | 0.5 | The unique id of runner being used |
-| **CI_RUNNER_TAGS** | 8.10 | 0.5 | The defined runner tags |
-| **CI_RUNNER_VERSION** | all | 10.6 | GitLab Runner version that is executing the current job |
-| **CI_RUNNER_REVISION** | all | 10.6 | GitLab Runner revision that is executing the current job |
-| **CI_RUNNER_EXECUTABLE_ARCH** | all | 10.6 | The OS/architecture of the GitLab Runner executable (note that this is not necessarily the same as the environment of the executor) |
-| **CI_PIPELINE_ID** | 8.10 | 0.5 | The unique id of the current pipeline that GitLab CI uses internally |
-| **CI_PIPELINE_IID** | 11.0 | all | The unique id of the current pipeline scoped to project |
-| **CI_PIPELINE_TRIGGERED** | all | all | The flag to indicate that job was [triggered] |
-| **CI_PIPELINE_SOURCE** | 10.0 | all | Indicates how the pipeline was triggered. Possible options are: `push`, `web`, `trigger`, `schedule`, `api`, and `pipeline`. For pipelines created before GitLab 9.5, this will show as `unknown` |
-| **CI_PROJECT_DIR** | all | all | The full path where the repository is cloned and where the job is run |
-| **CI_PROJECT_ID** | all | all | The unique id of the current project that GitLab CI uses internally |
-| **CI_PROJECT_NAME** | 8.10 | 0.5 | The project name that is currently being built (actually it is project folder name) |
-| **CI_PROJECT_NAMESPACE** | 8.10 | 0.5 | The project namespace (username or groupname) that is currently being built |
-| **CI_PROJECT_PATH** | 8.10 | 0.5 | The namespace with project name |
-| **CI_PROJECT_PATH_SLUG** | 9.3 | all | `$CI_PROJECT_PATH` lowercased and with everything except `0-9` and `a-z` replaced with `-`. Use in URLs and domain names. |
-| **CI_PIPELINE_URL** | 11.1 | 0.5 | Pipeline details URL |
-| **CI_PROJECT_URL** | 8.10 | 0.5 | The HTTP address to access project |
-| **CI_PROJECT_VISIBILITY** | 10.3 | all | The project visibility (internal, private, public) |
-| **CI_REGISTRY** | 8.10 | 0.5 | If the Container Registry is enabled it returns the address of GitLab's Container Registry |
-| **CI_REGISTRY_IMAGE** | 8.10 | 0.5 | If the Container Registry is enabled for the project it returns the address of the registry tied to the specific project |
-| **CI_REGISTRY_PASSWORD** | 9.0 | all | The password to use to push containers to the GitLab Container Registry |
-| **CI_REGISTRY_USER** | 9.0 | all | The username to use to push containers to the GitLab Container Registry |
-| **CI_SERVER** | all | all | Mark that job is executed in CI environment |
-| **CI_SERVER_NAME** | all | all | The name of CI server that is used to coordinate jobs |
-| **CI_SERVER_REVISION** | all | all | GitLab revision that is used to schedule jobs |
-| **CI_SERVER_VERSION** | all | all | GitLab version that is used to schedule jobs |
-| **CI_SERVER_VERSION_MAJOR** | 11.4 | all | GitLab version major component |
-| **CI_SERVER_VERSION_MINOR** | 11.4 | all | GitLab version minor component |
-| **CI_SERVER_VERSION_PATCH** | 11.4 | all | GitLab version patch component |
-| **CI_SHARED_ENVIRONMENT** | all | 10.1 | Marks that the job is executed in a shared environment (something that is persisted across CI invocations like `shell` or `ssh` executor). If the environment is shared, it is set to true, otherwise it is not defined at all. |
-| **GET_SOURCES_ATTEMPTS** | 8.15 | 1.9 | Number of attempts to fetch sources running a job |
-| **GITLAB_CI** | all | all | Mark that job is executed in GitLab CI environment |
-| **GITLAB_USER_EMAIL** | 8.12 | all | The email of the user who started the job |
-| **GITLAB_USER_ID** | 8.12 | all | The id of the user who started the job |
-| **GITLAB_USER_LOGIN** | 10.0 | all | The login username of the user who started the job |
-| **GITLAB_USER_NAME** | 10.0 | all | The real name of the user who started the job |
-| **RESTORE_CACHE_ATTEMPTS** | 8.15 | 1.9 | Number of attempts to restore the cache running a job |
-
-## 9.0 Renaming
+| Variable | GitLab | Runner | Description |
+|-------------------------------------------|--------|--------|-------------|
+| **ARTIFACT_DOWNLOAD_ATTEMPTS** | 8.15 | 1.9 | Number of attempts to download artifacts running a job |
+| **CI** | all | 0.4 | Mark that job is executed in CI environment |
+| **CI_COMMIT_REF_NAME** | 9.0 | all | The branch or tag name for which project is built |
+| **CI_COMMIT_REF_SLUG** | 9.0 | all | `$CI_COMMIT_REF_NAME` lowercased, shortened to 63 bytes, and with everything except `0-9` and `a-z` replaced with `-`. No leading / trailing `-`. Use in URLs, host names and domain names. |
+| **CI_COMMIT_SHA** | 9.0 | all | The commit revision for which project is built |
+| **CI_COMMIT_BEFORE_SHA** | 11.2 | all | The previous latest commit present on a branch before a push request. |
+| **CI_COMMIT_TAG** | 9.0 | 0.5 | The commit tag name. Present only when building tags. |
+| **CI_COMMIT_MESSAGE** | 10.8 | all | The full commit message. |
+| **CI_COMMIT_TITLE** | 10.8 | all | The title of the commit - the full first line of the message |
+| **CI_COMMIT_DESCRIPTION** | 10.8 | all | The description of the commit: the message without first line, if the title is shorter than 100 characters; full message in other case. |
+| **CI_CONFIG_PATH** | 9.4 | 0.5 | The path to CI config file. Defaults to `.gitlab-ci.yml` |
+| **CI_DEBUG_TRACE** | all | 1.7 | Whether [debug tracing](#debug-tracing) is enabled |
+| **CI_DEPLOY_USER** | 10.8 | all | Authentication username of the [GitLab Deploy Token][gitlab-deploy-token], only present if the Project has one related.|
+| **CI_DEPLOY_PASSWORD** | 10.8 | all | Authentication password of the [GitLab Deploy Token][gitlab-deploy-token], only present if the Project has one related.|
+| **CI_DISPOSABLE_ENVIRONMENT** | all | 10.1 | Marks that the job is executed in a disposable environment (something that is created only for this job and disposed of/destroyed after the execution - all executors except `shell` and `ssh`). If the environment is disposable, it is set to true, otherwise it is not defined at all. |
+| **CI_ENVIRONMENT_NAME** | 8.15 | all | The name of the environment for this job |
+| **CI_ENVIRONMENT_SLUG** | 8.15 | all | A simplified version of the environment name, suitable for inclusion in DNS, URLs, Kubernetes labels, etc. |
+| **CI_ENVIRONMENT_URL** | 9.3 | all | The URL of the environment for this job |
+| **CI_JOB_ID** | 9.0 | all | The unique id of the current job that GitLab CI uses internally |
+| **CI_JOB_MANUAL** | 8.12 | all | The flag to indicate that job was manually started |
+| **CI_JOB_NAME** | 9.0 | 0.5 | The name of the job as defined in `.gitlab-ci.yml` |
+| **CI_JOB_STAGE** | 9.0 | 0.5 | The name of the stage as defined in `.gitlab-ci.yml` |
+| **CI_JOB_TOKEN** | 9.0 | 1.2 | Token used for authenticating with the [GitLab Container Registry][registry] and downloading [dependent repositories][dependent-repositories] |
+| **CI_MERGE_REQUEST_ID** | 11.6 | all | The ID of the merge request if it's [pipelines for merge requests](../merge_request_pipelines/index.md) |
+| **CI_MERGE_REQUEST_IID** | 11.6 | all | The IID of the merge request if it's [pipelines for merge requests](../merge_request_pipelines/index.md) |
+| **CI_MERGE_REQUEST_REF_PATH** | 11.6 | all | The ref path of the merge request if it's [pipelines for merge requests](../merge_request_pipelines/index.md). (e.g. `refs/merge-requests/1/head`) |
+| **CI_MERGE_REQUEST_PROJECT_ID** | 11.6 | all | The ID of the project of the merge request if it's [pipelines for merge requests](../merge_request_pipelines/index.md) |
+| **CI_MERGE_REQUEST_PROJECT_PATH** | 11.6 | all | The path of the project of the merge request if it's [pipelines for merge requests](../merge_request_pipelines/index.md) (e.g. `namespace/awesome-project`) |
+| **CI_MERGE_REQUEST_PROJECT_URL** | 11.6 | all | The URL of the project of the merge request if it's [pipelines for merge requests](../merge_request_pipelines/index.md) (e.g. `http://192.168.10.15:3000/namespace/awesome-project`) |
+| **CI_MERGE_REQUEST_TARGET_BRANCH_NAME** | 11.6 | all | The target branch name of the merge request if it's [pipelines for merge requests](../merge_request_pipelines/index.md) |
+| **CI_MERGE_REQUEST_SOURCE_PROJECT_ID** | 11.6 | all | The ID of the source project of the merge request if it's [pipelines for merge requests](../merge_request_pipelines/index.md) |
+| **CI_MERGE_REQUEST_SOURCE_PROJECT_PATH** | 11.6 | all | The path of the source project of the merge request if it's [pipelines for merge requests](../merge_request_pipelines/index.md) |
+| **CI_MERGE_REQUEST_SOURCE_PROJECT_URL** | 11.6 | all | The URL of the source project of the merge request if it's [pipelines for merge requests](../merge_request_pipelines/index.md) |
+| **CI_MERGE_REQUEST_SOURCE_BRANCH_NAME** | 11.6 | all | The source branch name of the merge request if it's [pipelines for merge requests](../merge_request_pipelines/index.md) |
+| **CI_NODE_INDEX** | 11.5 | all | Index of the job in the job set. If the job is not parallelized, this variable is not set. |
+| **CI_NODE_TOTAL** | 11.5 | all | Total number of instances of this job running in parallel. If the job is not parallelized, this variable is set to `1`. |
+| **CI_JOB_URL** | 11.1 | 0.5 | Job details URL |
+| **CI_REPOSITORY_URL** | 9.0 | all | The URL to clone the Git repository |
+| **CI_RUNNER_DESCRIPTION** | 8.10 | 0.5 | The description of the runner as saved in GitLab |
+| **CI_RUNNER_ID** | 8.10 | 0.5 | The unique id of runner being used |
+| **CI_RUNNER_TAGS** | 8.10 | 0.5 | The defined runner tags |
+| **CI_RUNNER_VERSION** | all | 10.6 | GitLab Runner version that is executing the current job |
+| **CI_RUNNER_REVISION** | all | 10.6 | GitLab Runner revision that is executing the current job |
+| **CI_RUNNER_EXECUTABLE_ARCH** | all | 10.6 | The OS/architecture of the GitLab Runner executable (note that this is not necessarily the same as the environment of the executor) |
+| **CI_PIPELINE_ID** | 8.10 | 0.5 | The unique id of the current pipeline that GitLab CI uses internally |
+| **CI_PIPELINE_IID** | 11.0 | all | The unique id of the current pipeline scoped to project |
+| **CI_PIPELINE_TRIGGERED** | all | all | The flag to indicate that job was [triggered] |
+| **CI_PIPELINE_SOURCE** | 10.0 | all | Indicates how the pipeline was triggered. Possible options are: `push`, `web`, `trigger`, `schedule`, `api`, and `pipeline`. For pipelines created before GitLab 9.5, this will show as `unknown` |
+| **CI_PROJECT_DIR** | all | all | The full path where the repository is cloned and where the job is run |
+| **CI_PROJECT_ID** | all | all | The unique id of the current project that GitLab CI uses internally |
+| **CI_PROJECT_NAME** | 8.10 | 0.5 | The project name that is currently being built (actually it is project folder name) |
+| **CI_PROJECT_NAMESPACE** | 8.10 | 0.5 | The project namespace (username or groupname) that is currently being built |
+| **CI_PROJECT_PATH** | 8.10 | 0.5 | The namespace with project name |
+| **CI_PROJECT_PATH_SLUG** | 9.3 | all | `$CI_PROJECT_PATH` lowercased and with everything except `0-9` and `a-z` replaced with `-`. Use in URLs and domain names. |
+| **CI_PIPELINE_URL** | 11.1 | 0.5 | Pipeline details URL |
+| **CI_PROJECT_URL** | 8.10 | 0.5 | The HTTP address to access project |
+| **CI_PROJECT_VISIBILITY** | 10.3 | all | The project visibility (internal, private, public) |
+| **CI_REGISTRY** | 8.10 | 0.5 | If the Container Registry is enabled it returns the address of GitLab's Container Registry |
+| **CI_REGISTRY_IMAGE** | 8.10 | 0.5 | If the Container Registry is enabled for the project it returns the address of the registry tied to the specific project |
+| **CI_REGISTRY_PASSWORD** | 9.0 | all | The password to use to push containers to the GitLab Container Registry |
+| **CI_REGISTRY_USER** | 9.0 | all | The username to use to push containers to the GitLab Container Registry |
+| **CI_SERVER** | all | all | Mark that job is executed in CI environment |
+| **CI_SERVER_NAME** | all | all | The name of CI server that is used to coordinate jobs |
+| **CI_SERVER_REVISION** | all | all | GitLab revision that is used to schedule jobs |
+| **CI_SERVER_VERSION** | all | all | GitLab version that is used to schedule jobs |
+| **CI_SERVER_VERSION_MAJOR** | 11.4 | all | GitLab version major component |
+| **CI_SERVER_VERSION_MINOR** | 11.4 | all | GitLab version minor component |
+| **CI_SERVER_VERSION_PATCH** | 11.4 | all | GitLab version patch component |
+| **CI_SHARED_ENVIRONMENT** | all | 10.1 | Marks that the job is executed in a shared environment (something that is persisted across CI invocations like `shell` or `ssh` executor). If the environment is shared, it is set to true, otherwise it is not defined at all. |
+| **GET_SOURCES_ATTEMPTS** | 8.15 | 1.9 | Number of attempts to fetch sources running a job |
+| **GITLAB_CI** | all | all | Mark that job is executed in GitLab CI environment |
+| **GITLAB_USER_EMAIL** | 8.12 | all | The email of the user who started the job |
+| **GITLAB_USER_ID** | 8.12 | all | The id of the user who started the job |
+| **GITLAB_USER_LOGIN** | 10.0 | all | The login username of the user who started the job |
+| **GITLAB_USER_NAME** | 10.0 | all | The real name of the user who started the job |
+| **RESTORE_CACHE_ATTEMPTS** | 8.15 | 1.9 | Number of attempts to restore the cache running a job |
+
+## GitLab 9.0 renaming
To follow conventions of naming across GitLab, and to further move away from the
`build` term and toward `job` CI variables have been renamed for the 9.0
@@ -137,7 +148,7 @@ future GitLab releases.**
## `.gitlab-ci.yml` defined variables
NOTE **Note:**
-This feature requires GitLab Runner 0.5.0 or higher and GitLab CI 7.14 or higher.
+This feature requires GitLab Runner 0.5.0 or higher and GitLab 7.14 or higher.
GitLab CI allows you to add to `.gitlab-ci.yml` variables that are set in the
build environment. The variables are hence saved in the repository, and they
@@ -176,8 +187,7 @@ script:
## Variables
-NOTE: **Note:**
-Group-level variables were added in GitLab 9.4.
+> Group-level variables were introduced in GitLab 9.4.
CAUTION: **Important:**
Be aware that variables are not masked, and their values can be shown
@@ -206,8 +216,7 @@ Once you set them, they will be available for all subsequent pipelines. You can
### Protected variables
->**Notes:**
-This feature requires GitLab 9.3 or higher.
+> Introduced in GitLab 9.3.
Variables could be protected. Whenever a variable is
protected, it would only be securely passed to pipelines running on the
@@ -228,8 +237,7 @@ Variables can be specified for a single pipeline run when a [manual pipeline](..
## Deployment variables
-NOTE: **Note:**
-This feature requires GitLab CI 8.15 or higher.
+> Introduced in GitLab 8.15.
[Project services](../../user/project/integrations/project_services.md) that are
responsible for deployment configuration may define their own variables that
@@ -490,7 +498,7 @@ export CI_REGISTRY_PASSWORD="longalfanumstring"
## Variables expressions
-> Variables expressions were added in GitLab 10.7.
+> Introduced in GitLab 10.7.
It is possible to use variables expressions with only / except policies in
`.gitlab-ci.yml`. By using this approach you can limit what jobs are going to
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index af7e41db443..4dafde0462a 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -55,27 +55,27 @@ A job is defined by a list of parameters that define the job behavior.
| Keyword | Required | Description |
|---------------|----------|-------------|
-| script | yes | Defines a shell script which is executed by Runner |
-| extends | no | Defines a configuration entry that this job is going to inherit from |
-| image | no | Use docker image, covered in [Using Docker Images](../docker/using_docker_images.md#define-image-and-services-from-gitlab-ciyml) |
-| services | no | Use docker services, covered in [Using Docker Images](../docker/using_docker_images.md#define-image-and-services-from-gitlab-ciyml) |
-| stage | no | Defines a job stage (default: `test`) |
-| type | no | Alias for `stage` |
-| variables | no | Define job variables on a job level |
-| only | no | Defines a list of git refs for which job is created |
-| except | no | Defines a list of git refs for which job is not created |
-| tags | no | Defines a list of tags which are used to select Runner |
-| allow_failure | no | Allow job to fail. Failed job doesn't contribute to commit status |
-| when | no | Define when to run job. Can be `on_success`, `on_failure`, `always` or `manual` |
-| dependencies | no | Define other jobs that a job depends on so that you can pass artifacts between them|
-| artifacts | no | Define list of [job artifacts](#artifacts) |
-| cache | no | Define list of files that should be cached between subsequent runs |
-| before_script | no | Override a set of commands that are executed before job |
-| after_script | no | Override a set of commands that are executed after job |
-| environment | no | Defines a name of environment to which deployment is done by this job |
-| coverage | no | Define code coverage settings for a given job |
-| retry | no | Define when and how many times a job can be auto-retried in case of a failure |
-| parallel | no | Defines how many instances of a job should be run in parallel |
+| [script](#script) | yes | Defines a shell script which is executed by Runner |
+| [extends](#extends) | no | Defines a configuration entry that this job is going to inherit from |
+| [image](#image-and-services) | no | Use docker image, covered in [Using Docker Images](../docker/using_docker_images.md#define-image-and-services-from-gitlab-ciyml) |
+| [services](#image-and-services) | no | Use docker services, covered in [Using Docker Images](../docker/using_docker_images.md#define-image-and-services-from-gitlab-ciyml) |
+| [stage](#stage) | no | Defines a job stage (default: `test`) |
+| type | no | Alias for `stage` |
+| [variables](#variables) | no | Define job variables on a job level |
+| [only](#only-and-except-simplified) | no | Defines a list of git refs for which job is created |
+| [except](#only-and-except-simplified) | no | Defines a list of git refs for which job is not created |
+| [tags](#tags) | no | Defines a list of tags which are used to select Runner |
+| [allow_failure](#allow_failure) | no | Allow job to fail. Failed job doesn't contribute to commit status |
+| [when](#when) | no | Define when to run job. Can be `on_success`, `on_failure`, `always` or `manual` |
+| [dependencies](#dependencies) | no | Define other jobs that a job depends on so that you can pass artifacts between them|
+| [artifacts](#artifacts) | no | Define list of [job artifacts](#artifacts) |
+| [cache](#cache) | no | Define list of files that should be cached between subsequent runs |
+| [before_script](#before_script-and-after_script) | no | Override a set of commands that are executed before job |
+| [after_script](#before_script-and-after_script) | no | Override a set of commands that are executed after job |
+| [environment](#environment) | no | Defines a name of environment to which deployment is done by this job |
+| [coverage](#coverage) | no | Define code coverage settings for a given job |
+| [retry](#retry) | no | Define when and how many times a job can be auto-retried in case of a failure |
+| [parallel](#parallel) | no | Defines how many instances of a job should be run in parallel |
### `extends`
@@ -342,15 +342,16 @@ In addition, `only` and `except` allow the use of special keywords:
| **Value** | **Description** |
| --------- | ---------------- |
-| `branches` | When a branch is pushed. |
-| `tags` | When a tag is pushed. |
-| `api` | When pipeline has been triggered by a second pipelines API (not triggers API). |
-| `external` | When using CI services other than GitLab. |
-| `pipelines` | For multi-project triggers, created using the API with `CI_JOB_TOKEN`. |
-| `pushes` | Pipeline is triggered by a `git push` by the user. |
-| `schedules` | For [scheduled pipelines][schedules]. |
-| `triggers` | For pipelines created using a trigger token. |
-| `web` | For pipelines created using **Run pipeline** button in GitLab UI (under your project's **Pipelines**). |
+| `branches` | When a git reference of a pipeline is a branch. |
+| `tags` | When a git reference of a pipeline is a tag. |
+| `api` | When pipeline has been triggered by a second pipelines API (not triggers API). |
+| `external` | When using CI services other than GitLab. |
+| `pipelines` | For multi-project triggers, created using the API with `CI_JOB_TOKEN`. |
+| `pushes` | Pipeline is triggered by a `git push` by the user. |
+| `schedules` | For [scheduled pipelines][schedules]. |
+| `triggers` | For pipelines created using a trigger token. |
+| `web` | For pipelines created using **Run pipeline** button in GitLab UI (under your project's **Pipelines**). |
+| `merge_requests` | When a merge request is created or updated (See [pipelines for merge requests](../merge_request_pipelines/index.md)). |
In the example below, `job` will run only for refs that start with `issue-`,
whereas all branches will be skipped:
@@ -391,6 +392,24 @@ job:
The above example will run `job` for all branches on `gitlab-org/gitlab-ce`,
except master.
+If a job does not have neither `only` nor `except` rule,
+`only: ['branches', 'tags']` is set by default.
+
+For example,
+
+```yaml
+job:
+ script: echo 'test'
+```
+
+is translated to
+
+```yaml
+job:
+ script: echo 'test'
+ only: ['branches', 'tags']
+```
+
## `only` and `except` (complex)
> `refs` and `kubernetes` policies introduced in GitLab 10.0
@@ -552,7 +571,7 @@ osx job:
`allow_failure` is used when you want to allow a job to fail without impacting
the rest of the CI suite. Failed jobs don't contribute to the commit status.
-The default value is `false`.
+The default value is `false`, except for [manual](#whenmanual) jobs.
When enabled and the job fails, the pipeline will be successful/green for all
intents and purposes, but a "CI build passed with warnings" message will be
diff --git a/doc/development/automatic_ce_ee_merge.md b/doc/development/automatic_ce_ee_merge.md
index e4eb26b3aca..8208e9007ea 100644
--- a/doc/development/automatic_ce_ee_merge.md
+++ b/doc/development/automatic_ce_ee_merge.md
@@ -1,33 +1,27 @@
# Automatic CE->EE merge
-Whenever a commit is pushed to the CE `master` branch, it is automatically
-merged into the EE `master` branch. If the commit produces any conflicts, it is
-instead reverted from CE `master`. When this happens, a merge request will be
-set up automatically that can be used to reinstate the changes. This merge
-request will be assigned to the author of the conflicting commit, or the merge
-request author if the commit author could not be associated with a GitLab user.
-If no author could be found, the merge request is assigned to a random member of
-the Delivery team. It is then up to this team member to figure out who to assign
-the merge request to.
-
-Because some commits can not be reverted if new commits depend on them, we also
-run a job periodically that processes a range of commits and tries to merge or
-revert them. This should ensure that all commits are either merged into EE
-`master`, or reverted, instead of just being left behind in CE.
+Commits pushed to CE `master` are automatically merged into EE `master` roughly
+every 5 minutes. Changes are merged using the `ours` merge strategy in the
+context of EE. This means that any merge conflicts are resolved by taking the EE
+changes and discarding the CE changes. This removes the need for resolving
+conflicts or reverting changes, at the cost of **absolutely requiring** EE merge
+requests to be created whenever a CE merge request causes merge conflicts.
+Failing to do so can result in changes not making their way into EE.
+
+## Always create an EE merge request if there are conflicts
+
+In CI there is a job called `ee_compat_check`, which checks if a CE MR causes
+merge conflicts with EE. If this job reports conflicts, you **must** create an
+EE merge request. If you are an external contributor you can ask the reviewer to
+do this for you.
## Always merge EE merge requests before their CE counterparts
**In order to avoid conflicts in the CE->EE merge, you should always merge the
EE version of your CE merge request first, if present.**
-The rationale for this is that as CE->EE merges are done automatically, it can
-happen that:
-
-1. A CE merge request that needs EE-specific changes is merged.
-1. The automatic CE->EE merge happens.
-1. Conflicts due to the CE merge request occur since its EE merge request isn't
- merged yet.
-1. The CE changes are reverted.
+Failing to do so will lead to CE changes being discarded when merging into EE,
+if they cause merge conflicts.
## Avoiding CE->EE merge conflicts beforehand
@@ -45,76 +39,184 @@ detect if the current branch's changes will conflict during the CE->EE merge.
The job reports what files are conflicting and how to set up a merge request
against EE.
-## How to reinstate changes
-
-When a commit is reverted, the corresponding merge request to reinstate the
-changes will include all the details necessary to ensure the changes make it
-back into CE and EE. However, you still need to manually set up an EE merge
-request that resolves the conflicts.
+#### How the job works
+
+1. Generates the diff between your branch and current CE `master`
+1. Tries to apply it to current EE `master`
+1. If it applies cleanly, the job succeeds, otherwise...
+1. Detects a branch with the `ee-` prefix or `-ee` suffix in EE
+1. If it exists, generate the diff between this branch and current EE `master`
+1. Tries to apply it to current EE `master`
+1. If it applies cleanly, the job succeeds
+
+In the case where the job fails, it means you should create an `ee-<ce_branch>`
+or `<ce_branch>-ee` branch, push it to EE and open a merge request against EE
+`master`.
+At this point if you retry the failing job in your CE merge request, it should
+now pass.
+
+Notes:
+
+- This task is not a silver-bullet, its current goal is to bring awareness to
+ developers that their work needs to be ported to EE.
+- Community contributors shouldn't be required to submit merge requests against
+ EE, but reviewers should take actions by either creating such EE merge request
+ or asking a GitLab developer to do it **before the merge request is merged**.
+- If you branch is too far behind `master`, the job will fail. In that case you
+ should rebase your branch upon latest `master`.
+- Code reviews for merge requests often consist of multiple iterations of
+ feedback and fixes. There is no need to update your EE MR after each
+ iteration. Instead, create an EE MR as soon as you see the
+ `ee_compat_check` job failing. After you receive the final approval
+ from a Maintainer (but **before the CE MR is merged**) update the EE MR.
+ This helps to identify significant conflicts sooner, but also reduces the
+ number of times you have to resolve conflicts.
+- Please remember to
+ [always have your EE merge request merged before the CE version](#always-merge-ee-merge-requests-before-their-ce-counterparts).
+- You can use [`git rerere`](https://git-scm.com/docs/git-rerere)
+ to avoid resolving the same conflicts multiple times.
+
+### Cherry-picking from CE to EE
+
+For avoiding merge conflicts, we use a method of creating equivalent branches
+for CE and EE. If the `ee-compat-check` job fails, this process is required.
+
+This method only requires that you have cloned both CE and EE into your computer.
+If you don't have them yet, please go ahead and clone them:
+
+- Clone CE repo: `git clone git@gitlab.com:gitlab-org/gitlab-ce.git`
+- Clone EE repo: `git clone git@gitlab.com:gitlab-org/gitlab-ee.git`
+
+And the only additional setup we need is to add CE as remote of EE and vice-versa:
+
+- Open two terminal windows, one in CE, and another one in EE:
+ - In EE: `git remote add ce git@gitlab.com:gitlab-org/gitlab-ce.git`
+ - In CE: `git remote add ee git@gitlab.com:gitlab-org/gitlab-ee.git`
+
+That's all setup we need, so that we can cherry-pick a commit from CE to EE, and
+from EE to CE.
+
+Now, every time you create an MR for CE and EE:
+
+1. Open two terminal windows, one in CE, and another one in EE
+1. In the CE terminal:
+ 1. Create the CE branch, e.g., `branch-example`
+ 1. Make your changes and push a commit (commit A)
+ 1. Create the CE merge request in GitLab
+1. In the EE terminal:
+ 1. Create the EE-equivalent branch ending with `-ee`, e.g.,
+ `git checkout -b branch-example-ee`
+ 1. Fetch the CE branch: `git fetch ce branch-example`
+ 1. Cherry-pick the commit A: `git cherry-pick commit-A-SHA`
+ 1. If Git prompts you to fix the conflicts, do a `git status`
+ to check which files contain conflicts, fix them, save the files
+ 1. Add the changes with `git add .` but **DO NOT commit** them
+ 1. Continue cherry-picking: `git cherry-pick --continue`
+ 1. Push to EE: `git push origin branch-example-ee`
+1. Create the EE-equivalent MR and link to the CE MR from the
+description "Ports [CE-MR-LINK] to EE"
+1. Once all the jobs are passing in both CE and EE, you've addressed the
+feedback from your own team, and got them approved, the merge requests can be merged.
+1. When both MRs are ready, the EE merge request will be merged first, and the
+CE-equivalent will be merged next.
+
+**Important notes:**
+
+- The commit SHA can be easily found from the GitLab UI. From a merge request,
+open the tab **Commits** and click the copy icon to copy the commit SHA.
+- To cherry-pick a **commit range**, such as [A > B > C > D] use:
+
+ ```shell
+ git cherry-pick "oldest-commit-SHA^..newest-commit-SHA"
+ ```
+
+ For example, suppose the commit A is the oldest, and its SHA is `4f5e4018c09ed797fdf446b3752f82e46f5af502`,
+ and the commit D is the newest, and its SHA is `80e1c9e56783bd57bd7129828ec20b252ebc0538`.
+ The cherry-pick command will be:
+
+ ```shell
+ git cherry-pick "4f5e4018c09ed797fdf446b3752f82e46f5af502^..80e1c9e56783bd57bd7129828ec20b252ebc0538"
+ ```
+
+- To cherry-pick a **merge commit**, use the flag `-m 1`. For example, suppose that the
+merge commit SHA is `138f5e2f20289bb376caffa0303adb0cac859ce1`:
+
+ ```shell
+ git cherry-pick -m 1 138f5e2f20289bb376caffa0303adb0cac859ce1
+ ```
+- To cherry-pick multiple commits, such as B and D in a range [A > B > C > D], use:
+
+ ```shell
+ git cherry-pick commmit-B-SHA commit-D-SHA
+ ```
+
+ For example, suppose commit B SHA = `4f5e4018c09ed797fdf446b3752f82e46f5af502`,
+ and the commit D SHA = `80e1c9e56783bd57bd7129828ec20b252ebc0538`.
+ The cherry-pick command will be:
+
+ ```shell
+ git cherry-pick 4f5e4018c09ed797fdf446b3752f82e46f5af502 80e1c9e56783bd57bd7129828ec20b252ebc0538
+ ```
+
+ This case is particularly useful when you have a merge commit in a sequence of
+ commits and you want to cherry-pick all but the merge commit.
+
+- If you push more commits to the CE branch, you can safely repeat the procedure
+to cherry-pick them to the EE-equivalent branch. You can do that as many times as
+necessary, using the same CE and EE branches.
+- If you submitted the merge request to the CE repo and the `ee-compat-check` job passed,
+you are not required to submit the EE-equivalent MR, but it's still recommended. If the
+job failed, you are required to submit the EE MR so that you can fix the conflicts in EE
+before merging your changes into CE.
+
+---
+
+[Return to Development documentation](README.md)
-Each merge request used to reinstate changes will have the "reverted" label
-applied. Please do not remove this label, as it will be used to determine how
-many times commits are reverted and how long it takes to reinstate the changes.
-
-An example merge request can be found in [CE merge request
-23280](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/23280).
+## FAQ
-## How it works
+### How does automatic merging work?
The automatic merging is performed using a project called [Merge
-Train](https://gitlab.com/gitlab-org/merge-train/). For every commit to merge or
-revert, we generate patches using `git format-patch` which we then try to apply
-using `git am --3way`. If this succeeds we push the changes to EE, if this fails
-we decide what to do based on the failure reason:
-
-1. If the patch could not be applied because it was already applied, we just
- skip it.
-1. If the patch caused conflicts, we revert the source commits.
-
-Commits are reverted in reverse order, ensuring that if commit B depends on A,
-and both conflict, we first revert B followed by reverting A.
+Train](https://gitlab.com/gitlab-org/merge-train/). This project will clone CE
+and EE master, and merge CE master into EE master using `git merge
+--strategy=ours`. This process runs roughly every 5 minutes.
-## FAQ
-
-### Why?
+For more information on the exact implementation you can refer to the source
+code.
-We want to work towards being able to deploy continuously, but this requires
-that `master` is always stable and has all the changes we need. If CE `master`
-can not be merged into EE `master` due to merge conflicts, this prevents _any_
-change from CE making its way into EE. Since GitLab.com runs on EE, this
-effectively prevents us from deploying changes.
+### Why merge automatically?
-Past experiences and data have shown that periodic CE to EE merge requests do
-not scale, and often take a very long time to complete. For example, [in this
+As we work towards continuous deployments and a single repository for both CE
+and EE, we need to first make sure that all CE changes make their way into CE as
+fast as possible. Past experiences and data have shown that periodic CE to EE
+merge requests do not scale, and often take a very long time to complete. For
+example, [in this
comment](https://gitlab.com/gitlab-org/release/framework/issues/49#note_114614619)
we determined that the average time to close an upstream merge request is around
5 hours, with peaks up to several days. Periodic merge requests are also
frustrating to work with, because they often include many changes unrelated to
your own changes.
-Automatically merging or reverting commits allows us to keep merging changes
-from CE into EE, as we never have to wait hours for somebody to resolve a set of
-merge conflicts.
-
-### Does the CE to EE merge take into account merge commits?
-
-No. When merging CE changes into EE, merge commits are ignored.
+To resolve these problems, we now merge changes using the `ours` strategy to
+automatically resolve merge conflicts. This removes the need for resolving
+conflicts in a periodic merge request, and allows us to merge changes from CE
+into EE much faster.
-### My changes are reverted, but I set up an EE MR to resolve conflicts
+### My CE merge request caused conflicts after it was merged. What do I do?
-Most likely the automatic merge job ran before the EE merge request was merged.
-If this keeps happening, consider reporting a bug in the [Merge Train issue
-tracker](https://gitlab.com/gitlab-org/merge-train/issues).
+If you notice this, you should set up an EE merge request that resolves these
+conflicts as **soon as possible**. Failing to do so can lead to your changes not
+being available in EE, which may break tests. This in turn would prevent us from
+being able to deploy.
-### My changes keep getting reverted, and this is really annoying!
+### Won't this setup be risky?
-This is understandable, but the solution to this is fairly straightforward:
-simply set up an EE merge request for every CE merge request, and resolve your
-conflicts before the changes are reverted.
-
-### Will we allow certain people to still merge changes, even if they conflict?
-
-No.
+No, not if there is an EE merge request for every CE merge request that causes
+conflicts _and_ that EE merge request is merged first. In the past we may have
+been a bit more relaxed when it comes to enforcing EE merge requests, but to
+enable automatic merging have to start requiring such merge requests even for
+the smallest conflicts.
### Some files I work with often conflict, how can I best deal with this?
@@ -123,11 +225,3 @@ so that the EE specific changes are not intertwined with CE code. For Ruby code
you can do this by moving the EE code to a separate module, which can then be
injected into the appropriate classes or modules. See [Guidelines for
implementing Enterprise Edition features](ee_features.md) for more information.
-
-### Will changelog entries be reverted automatically?
-
-Only if the changelog was added in the commit that was reverted. If a changelog
-entry was added in a separate commit, it is possible for it to be left behind.
-Since changelog entries are related to the changes in question, there is no real
-reason to commit the changelog separately, and as such this should not be a big
-problem.
diff --git a/doc/development/background_migrations.md b/doc/development/background_migrations.md
index bb9a296ef12..dd4a9e058d7 100644
--- a/doc/development/background_migrations.md
+++ b/doc/development/background_migrations.md
@@ -211,7 +211,7 @@ existing data. Since we're dealing with a lot of rows we'll schedule jobs in
batches instead of doing this one by one:
```ruby
-class ScheduleExtractServicesUrl < ActiveRecord::Migration
+class ScheduleExtractServicesUrl < ActiveRecord::Migration[4.2]
disable_ddl_transaction!
class Service < ActiveRecord::Base
@@ -242,7 +242,7 @@ jobs and manually run on any un-migrated rows. Such a migration would look like
this:
```ruby
-class ConsumeRemainingExtractServicesUrlJobs < ActiveRecord::Migration
+class ConsumeRemainingExtractServicesUrlJobs < ActiveRecord::Migration[4.2]
disable_ddl_transaction!
class Service < ActiveRecord::Base
diff --git a/doc/development/code_review.md b/doc/development/code_review.md
index 7788d155154..25ea2211b64 100644
--- a/doc/development/code_review.md
+++ b/doc/development/code_review.md
@@ -10,7 +10,7 @@ code is effective, understandable, maintainable, and secure.
## Getting your merge request reviewed, approved, and merged
You are strongly encouraged to get your code **reviewed** by a
-[reviewer](https://about.gitlab.com/handbook/engineering/#reviewer) as soon as
+[reviewer](https://about.gitlab.com/handbook/engineering/workflow/code-review/#reviewer) as soon as
there is any code to review, to get a second opinion on the chosen solution and
implementation, and an extra pair of eyes looking for bugs, logic problems, or
uncovered edge cases. The reviewer can be from a different team, but it is
@@ -24,7 +24,7 @@ If you need assistance with security scans or comments, feel free to include the
Security Team (`@gitlab-com/gl-security`) in the review.
Depending on the areas your merge request touches, it must be **approved** by one
-or more [maintainers](https://about.gitlab.com/handbook/engineering/#maintainer):
+or more [maintainers](https://about.gitlab.com/handbook/engineering/workflow/code-review/#maintainer):
For approvals, we use the approval functionality found in the merge request
widget. Reviewers can add their approval by [approving additionally](https://docs.gitlab.com/ee/user/project/merge_requests/merge_request_approvals.html#adding-or-removing-an-approval).
diff --git a/doc/development/documentation/index.md b/doc/development/documentation/index.md
index b7990e1b558..4e5b4a85a97 100644
--- a/doc/development/documentation/index.md
+++ b/doc/development/documentation/index.md
@@ -368,6 +368,26 @@ You can combine one or more of the following:
= link_to 'Help page', help_page_path('user/permissions')
```
+### GitLab `/help` tests
+
+Several [rspec tests](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/spec/features/help_pages_spec.rb)
+are run to ensure GitLab documentation renders and works correctly. In particular, that [main docs landing page](../../README.md) will work correctly from `/help`.
+For example, [GitLab.com's `/help`](https://gitlab.com/help).
+
+CAUTION: **Caution:**
+Because the rspec tests only run in a full pipeline, and not a special [docs-only pipeline](#branch-naming), it is possible
+to merge changes that will break `master` from a merge request with a successful docs-only pipeline run.
+
+## Docs site architecture
+
+Read through [docs architecture](site_architecture/index.md) to learn
+how we architecture, build, and deploy the docs site, <https://docs.gitlab.com>, and
+to check all the assets and libraries available.
+
+### Global navigation
+
+Read through the [global navigation](site_architecture/global_nav.md) doc.
+
## General Documentation vs Technical Articles
### General documentation
@@ -552,6 +572,7 @@ Currently, the following tests are in place:
As CE is merged into EE once a day, it's important to avoid merge conflicts.
Submitting an EE-equivalent merge request cherry-picking all commits from CE to EE is
essential to avoid them.
+1. In a full pipeline, tests for [`/help`](#gitlab-help-tests).
### Linting
@@ -679,6 +700,3 @@ GitLab uses [danger bot](https://github.com/danger/danger) for some elements in
code review. For docs changes in merge requests, whenever a change under `/doc`
is made, the bot leaves a comment for the author to mention `@gl-docsteam`, so
that the docs can be properly reviewed.
-
-[gitlab-map]: https://gitlab.com/gitlab-org/gitlab-design/raw/master/production/resources/gitlab-map.png
-[graffle]: https://gitlab.com/gitlab-org/gitlab-design/blob/d8d39f4a87b90fb9ae89ca12dc565347b4900d5e/production/resources/gitlab-map.graffle
diff --git a/doc/development/documentation/site_architecture/global_nav.md b/doc/development/documentation/site_architecture/global_nav.md
new file mode 100644
index 00000000000..62ca7d6c805
--- /dev/null
+++ b/doc/development/documentation/site_architecture/global_nav.md
@@ -0,0 +1,342 @@
+---
+description: "Learn how GitLab docs' global navigation works and how to add new items."
+---
+
+# Global navigation
+
+> [Introduced](https://gitlab.com/gitlab-com/gitlab-docs/merge_requests/362)
+in November 2018 for GitLab 11.6.
+
+The global nav adds to the left sidebar the ability to
+navigate and explore the contents of GitLab's documentation.
+
+The global nav should be maintained consistent through time to allow the
+users to locate their most-visited links easily to facilitate navigation.
+Therefore, any updates must be carefully considered by the technical writers.
+
+## Adding new items to the global nav
+
+To add a new doc to the nav, first and foremost, check with the technical writing team:
+
+- If it's applicable
+- What's the exact position the doc will be added to the nav
+
+Once you get their approval and their guidance in regards to the position on the nav,
+read trhough this page to understand how it works, and submit a merge request to the
+docs site, adding the doc you wish to include in the nav into the
+[global nav data file](https://gitlab.com/gitlab-com/gitlab-docs/blob/master/content/_data/global-nav.yaml).
+
+Don't forget to ask a technical writer to review your changes before merging.
+
+## How it works
+
+The global nav has 3 components:
+
+- **Section**
+ - Category
+ - Doc
+
+The available sections are described on the table below:
+
+| Section | Description |
+| ------------- | ------------------------------------------ |
+| User | Documentation for the GitLab's user UI. |
+| Administrator | Documentation for the GitLab's admin area. |
+| Contributor | Documentation for developing GitLab. |
+
+The majority of the links available on the nav were added according to the UI.
+The match is not perfect, as for some UI nav items the documentation doesn't
+apply, and there are also other links to help the new users to discover the
+documentation. The docs under **Administration** are ordered alphabetically
+for clarity.
+
+To see the improvements planned, check the
+[global nav epic](https://gitlab.com/groups/gitlab-com/-/epics/21).
+
+CAUTION: **Attention!**
+**Do not** [add items](#adding-new-items-to-the-global-nav) to the global nav without
+the consent of one of the technical writers.
+
+## Composition
+
+The global nav is built from two files:
+
+- [Data](#data-file)
+- [Layout](#layout-file)
+
+The data file feeds the layout with the links to the docs. The layout organizes
+the data among the nav in containers properly [styled](#css-classes).
+
+### Data file
+
+The [data file](https://gitlab.com/gitlab-com/gitlab-docs/blob/master/content/_data/global-nav.yaml)
+is structured in three components: sections, categories, and docs.
+
+#### Sections
+
+Each section represents the higher-level nav item. It's composed by
+title and URL:
+
+```yaml
+sections:
+ - section_title: Text
+ section_url: 'link'
+```
+
+The section can stand alone or contain categories within.
+
+#### Categories
+
+Each category within a section composes the second level of the nav.
+It includes the category title and link. It can stand alone in the nav or contain
+a third level of sub-items.
+
+Example of section with one stand-alone category:
+
+```yaml
+- section_title: Section title
+ section_url: 'section-link'
+ section_categories:
+ - category_title: Category title
+ category_url: 'category-link'
+```
+
+Example of section with two stand-alone categories:
+
+```yaml
+- section_title: Section title
+ section_url: 'section-link'
+ section_categories:
+ - category_title: Category 1 title
+ category_url: 'category-1-link'
+
+ - category_title: Category 2 title
+ category_url: 'category-2-link'
+```
+
+For clarity, **always** add a blank line between categories.
+
+If a category URL is not present in CE (it's an EE-only document), add the
+attribute `ee_only: true` below the category link. Example:
+
+```yaml
+- category_title: Category title
+ category_url: 'category-link'
+ ee_only: true
+```
+
+If the category links to an external URL, e.g., [GitLab Design System](https://design.gitlab.com),
+add the attribute `external_url: true` below the category title. Example:
+
+```yaml
+- category_title: GitLab Design System
+ category_url: 'https://design.gitlab.com'
+ external_url: true
+```
+
+#### Docs
+
+Each doc represents the third level of nav links. They must be always
+added within a category.
+
+Example with one doc link:
+
+```yaml
+- category_title: Category title
+ category_url: 'category-link'
+ docs:
+ - doc_title: Document title
+ doc_url: 'doc-link'
+```
+
+A category supports as many docs as necessary, but, for clarity, try to not
+overpopulate a category.
+
+Example with multiple docs:
+
+```yaml
+- category_title: Category title
+ category_url: 'category-link'
+ docs:
+ - doc_title: Document 1 title
+ doc_url: 'doc-1-link'
+ - doc_title: Document 2 title
+ doc_url: 'doc-2-link'
+```
+
+Whenever a document is only present in EE, add the attribute `ee-only: true`
+below the doc link. Example:
+
+```yaml
+- doc_title: Document 2 title
+ doc_url: 'doc-2-link'
+ ee_only: true
+```
+
+If you need to add a document in an external URL, add the attribute `external_url`
+below the doc link:
+
+```yaml
+- doc_title: Document 2 title
+ doc_url: 'doc-2-link'
+ external_url: true
+```
+
+All nav links are clickable. If the higher-level link does not have a link
+of its own, it must link to its first sub-item link, mimicking GitLab's navigation.
+This must be avoided so that we don't have duplicated links nor two `.active` links
+at the same time.
+
+Example:
+
+```yaml
+- category_title: Operations
+ category_url: 'user/project/integrations/prometheus_library/'
+ # until we have a link to operations, the first doc link is
+ # repeated in the category link
+ docs:
+ - doc_title: Metrics
+ doc_url: 'user/project/integrations/prometheus_library/'
+```
+
+#### Syntax
+
+For all components (sections, categories, and docs), **respect the indentation**
+and the following syntax rules.
+
+##### Titles
+
+- Use sentence case, capitalizing feature names.
+- There's no need to wrap the titles, unless there's a special char in it. E.g.,
+ in `GitLab CI/CD`, there's a `/` present, therefore, it must be wrapped in quotes.
+ As convention, wrap the titles in double quotes: `category_title: "GitLab CI/CD"`.
+
+##### URLs
+
+- As convention, always wrap URLs in single quotes `'url'`.
+- Always use relative paths against the home of CE and EE. Examples:
+ - For `https://docs.gitlab.com/ee/README.html`, the relative URL is `README.html`.
+ - For `https://docs.gitlab.com/ee/user/project/cycle_analytics.html`, the relative
+ URL is `user/project/cycle_analytics.html`
+- For `README.html` files, add the complete path `path/to/README.html`.
+- For `index.html` files, use the clean (canonical) URL: `path/to/`.
+- For EE-only docs, use the same relative path, but add the attribute `ee_only: true` below
+ the `doc_url` or `category_url`, as explained above. This will guarantee that when
+ the user is looking at the CE docs, it will link to the EE docs. It also displays
+ an "info" icon on the CE nav to make the user aware that it's a different link.
+
+DANGER: **Important!**
+All links present on the data file must end in `.html`, not `.md`. Do not
+start any relative link with a forward slash `/`.
+
+Examples:
+
+```yaml
+- category_title: Issues
+ category_url: 'user/project/issues/'
+ # note that the above URL does not start with a slash and
+ # does not include index.html at the end
+
+ docs:
+ - doc_title: Service Desk
+ doc_url: 'user/project/service_desk.html'
+ ee_only: true
+ # note that the URL above ends in html and, as the
+ # document is EE-only, the attribute ee_only is set to true.
+```
+
+### Layout file (logic)
+
+The [layout](https://gitlab.com/gitlab-com/gitlab-docs/blob/master/layouts/global_nav.html)
+is fed by the [data file](#data-file), builds the global nav, and is rendered by the
+[default](https://gitlab.com/gitlab-com/gitlab-docs/blob/master/layouts/default.html) layout.
+
+There are three main considerations on the logic built for the nav:
+
+- [Path](#path): first-level directories underneath `docs.gitlab.com/`:
+ - `https://docs.gitlab.com/ce/`
+ - `https://docs.gitlab.com/ee/`
+ - `https://docs.gitlab.com/omnibus/`
+ - `https://docs.gitlab.com/runner/`
+ - `https://docs.gitlab.com/debug/`
+ - `https://docs.gitlab.com/*`
+- [EE-only](#ee-only-docs): documentation only available in `/ee/`, not on `/ce/`, e.g.:
+ - `https://docs.gitlab.com/ee/user/group/epics/`
+ - `https://docs.gitlab.com/ee/user/project/security_dashboard.html`
+- [Default URL](#default-url): between CE and EE docs, the default is `ee`, therefore, all docs
+ should link to `/ee/` unless if on `/ce/` linking internally to `ce`.
+
+#### Path
+
+To use relative paths in the data file, we defined the variable `dir`
+from the root's first-child directory, which defines the path to build
+all the nav links to other pages:
+
+```html
+<% dir = @item.identifier.to_s[%r{(?<=/)[^/]+}] %>
+```
+
+For instance, for `https://docs.gitlab.com/ce/user/index.html`,
+`dir` == `ce`, and for `https://docs.gitlab.com/omnibus/README.html`,
+`dir` == `omnibus`.
+
+#### Default URL
+
+The default and canonical URL for GitLab documentation is
+`http://docs.gitlab.com/ee/`, thus, all links
+in the docs site should link to `/ee/` except when linking
+among `/ce/` docs themselves.
+
+Therefore, if the user is looking at `/ee/`, `/omnibus/`,
+`/runner/`, or any other highest-level dir, the nav should
+point to `/ee/` docs.
+
+On the other hand, if the user is looking at `/ce/` docs,
+all the links in the CE nav should link internally to `/ce/`
+files, except for [`ee-only` docs](#ee-only-docs).
+
+
+```html
+<% if dir != 'ce' %>
+ <a href="/ee/<%= sec[:section_url] %>">...</a>
+ <% else %>
+ <a href="/<%= dir %>/<%= sec[:section_url] %>">...</a>
+ <% end %>
+ ...
+<% end %>
+```
+
+This also allows the nav to be displayed on other
+highest-level dirs (`/omnibus/`, `/runner/`, etc),
+linking them back to `/ee/`.
+
+The same logic is applied to all sections (`sec[:section_url]`),
+categories (`cat[:category_url]`), and docs (`doc[:doc_url]`) URLs.
+
+#### `ee-only` docs
+
+If the user is looking at the CE nav, a given doc is present only
+in `/ee/`, it's tagged in the data file by `ee-only`, linking it
+directly to `/ee/`.
+
+```html
+<% if dir == 'ce' && cat[:ee_only] %>
+ <a href="/ee/<%= cat[:category_url] %>">...</a>
+<% end %>
+```
+
+To make it clear that it it's a different link, an icon is displayed
+on the nav link indicating that the `ee-only` doc is not available in CE.
+
+The `ee-only` attribute is available for `categories` (`<% if dir == 'ce' && cat[:ee_only] %>`)
+and `docs` (`<% if dir == 'ce' && doc[:ee_only] %>`), but not for `sections`.
+
+### CSS classes
+
+The nav is styled in the general `stylesheet.scss`. To change
+its styles, keep them grouped for better development among the team.
+
+The URL components have their unique styles set by the CSS classes `.level-0`,
+`.level-1`, and `.level-2`. To adjust the link's font size, padding, color, etc,
+use these classes. This way we guarantee that the rules for each link do not conflict
+ with other rules in the stylesheets.
diff --git a/doc/development/documentation/site_architecture/index.md b/doc/development/documentation/site_architecture/index.md
new file mode 100644
index 00000000000..956bf90a5d9
--- /dev/null
+++ b/doc/development/documentation/site_architecture/index.md
@@ -0,0 +1,59 @@
+---
+description: "Learn how GitLab's documentation website is architectured."
+---
+
+# Docs site architecture
+
+Learn how we build and architecture [`gitlab-docs`](https://gitlab.com/gitlab-com/gitlab-docs)
+and deploy it to <https://docs.gitlab.com>.
+
+## Assets
+
+To provide an optimized site structure, design, and a search-engine friendly
+website, along with a discoverable documentation, we use a few assets for
+the GitLab Documentation website.
+
+### Libraries
+
+- [Bootstrap 3.3 components](https://getbootstrap.com/docs/3.3/components/)
+- [Bootstrap 3.3 JS](https://getbootstrap.com/docs/3.3/javascript/)
+- [jQuery](https://jquery.com/) 3.2.1
+- [Clipboard JS](https://clipboardjs.com/)
+- [Font Awesome 4.7.0](https://fontawesome.com/v4.7.0/icons/)
+
+### SEO
+
+- [Schema.org](https://schema.org/)
+- [Google Analytics](https://marketingplatform.google.com/about/analytics/)
+- [Google Tag Manager](https://developers.google.com/tag-manager/)
+
+## Global nav
+
+To understand how the global nav (left sidebar) is built, please
+read through the [global navigation](global_nav.md) doc.
+
+## Deployment
+
+The docs site is deployed to production with GitLab Pages, and previewed in
+merge requests with Review Apps.
+
+The deployment aspects will be soon transfered from the [original document](https://gitlab.com/gitlab-com/gitlab-docs/blob/master/README.md)
+to this page.
+
+<!--
+## Repositories
+
+TBA
+
+## Search engine
+
+TBA
+
+## Versions
+
+TBA
+
+## Helpers
+
+TBA
+-->
diff --git a/doc/development/fe_guide/vuex.md b/doc/development/fe_guide/vuex.md
index 0f57835fb87..65963b959f7 100644
--- a/doc/development/fe_guide/vuex.md
+++ b/doc/development/fe_guide/vuex.md
@@ -137,6 +137,7 @@ By following this pattern we guarantee:
#### Dispatching actions
To dispatch an action from a component, use the `mapActions` helper:
+
```javascript
import { mapActions } from 'vuex';
@@ -204,6 +205,7 @@ export const getUsersWithPets = (state, getters) => {
```
To access a getter from a component, use the `mapGetters` helper:
+
```javascript
import { mapGetters } from 'vuex';
@@ -226,6 +228,7 @@ export const ADD_USER = 'ADD_USER';
### How to include the store in your application
The store should be included in the main component of your application:
+
```javascript
// app.vue
import store from 'store'; // it will include the index.js file
@@ -364,7 +367,8 @@ Because we're currently using [`babel-plugin-rewire`](https://github.com/speedsk
`[vuex] actions should be function or object with "handler" function`
To prevent this error from happening, you need to export an empty function as `default`:
-```
+
+```javascript
// getters.js or actions.js
// prevent babel-plugin-rewire from generating an invalid default during karma tests
diff --git a/doc/development/feature_flags.md b/doc/development/feature_flags.md
index 1019a1fd0e2..b6161cd6163 100644
--- a/doc/development/feature_flags.md
+++ b/doc/development/feature_flags.md
@@ -113,7 +113,15 @@ feature flag. You can stub a feature flag as follows:
stub_feature_flags(my_feature_flag: false)
```
-## Enabling a feature flag
+## Enabling a feature flag (in development)
+
+In the rails console (`rails c`), enter the following command to enable your feature flag
+
+```ruby
+Feature.enable(:feature_flag_name)
+```
+
+## Enabling a feature flag (in production)
Check how to [roll out changes using feature flags](rolling_out_changes_using_feature_flags.md).
diff --git a/doc/development/migration_style_guide.md b/doc/development/migration_style_guide.md
index a99267bfbba..d0a054c3290 100644
--- a/doc/development/migration_style_guide.md
+++ b/doc/development/migration_style_guide.md
@@ -67,7 +67,7 @@ body:
For example:
```ruby
-class MyMigration < ActiveRecord::Migration
+class MyMigration < ActiveRecord::Migration[4.2]
DOWNTIME = true
DOWNTIME_REASON = 'This migration requires downtime because ...'
@@ -95,7 +95,7 @@ migration. For this to work your migration needs to include the module
`Gitlab::Database::MultiThreadedMigration`:
```ruby
-class MyMigration < ActiveRecord::Migration
+class MyMigration < ActiveRecord::Migration[4.2]
include Gitlab::Database::MigrationHelpers
include Gitlab::Database::MultiThreadedMigration
end
@@ -105,7 +105,7 @@ You can then use the method `with_multiple_threads` to perform work in separate
threads. For example:
```ruby
-class MyMigration < ActiveRecord::Migration
+class MyMigration < ActiveRecord::Migration[4.2]
include Gitlab::Database::MigrationHelpers
include Gitlab::Database::MultiThreadedMigration
@@ -139,7 +139,7 @@ by calling the method `disable_ddl_transaction!` in the body of your migration
class like so:
```ruby
-class MyMigration < ActiveRecord::Migration
+class MyMigration < ActiveRecord::Migration[4.2]
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
@@ -167,7 +167,7 @@ the method `disable_ddl_transaction!` in the body of your migration class like
so:
```ruby
-class MyMigration < ActiveRecord::Migration
+class MyMigration < ActiveRecord::Migration[4.2]
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
@@ -193,7 +193,7 @@ Here's an example where we add a new column with a foreign key
constraint. Note it includes `index: true` to create an index for it.
```ruby
-class Migration < ActiveRecord::Migration
+class Migration < ActiveRecord::Migration[4.2]
def change
add_reference :model, :other_model, index: true, foreign_key: { on_delete: :cascade }
@@ -216,7 +216,7 @@ For example, to add the column `foo` to the `projects` table with a default
value of `10` you'd write the following:
```ruby
-class MyMigration < ActiveRecord::Migration
+class MyMigration < ActiveRecord::Migration[4.2]
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
@@ -365,7 +365,7 @@ If you need more complex logic you can define and use models local to a
migration. For example:
```ruby
-class MyMigration < ActiveRecord::Migration
+class MyMigration < ActiveRecord::Migration[4.2]
class Project < ActiveRecord::Base
self.table_name = 'projects'
end
diff --git a/doc/development/profiling.md b/doc/development/profiling.md
index 0ca8bb67a77..0b0c6dfc8cf 100644
--- a/doc/development/profiling.md
+++ b/doc/development/profiling.md
@@ -77,8 +77,11 @@ that builds on this to add some additional niceties, such as allowing
configuration with a single Yaml file for multiple URLs, and uploading of the
profile and log output to S3.
-For GitLab.com, you can find the latest results here:
-<http://redash.gitlab.com/dashboard/gitlab-profiler-statistics>
+For GitLab.com, currently the latest profiling data has been [moved from
+Redash to Looker](https://gitlab.com/gitlab-com/Product/issues/5#note_121194467).
+We are [currently investigating how to make this data
+public](https://gitlab.com/meltano/looker/issues/294).
+
## Sherlock
diff --git a/doc/development/prometheus_metrics.md b/doc/development/prometheus_metrics.md
index b6b6d9665ea..0511e735843 100644
--- a/doc/development/prometheus_metrics.md
+++ b/doc/development/prometheus_metrics.md
@@ -30,7 +30,7 @@ You might want to add additional database migration that makes a decision what t
For example: you might be interested in migrating all dependent data to a different metric.
```ruby
-class ImportCommonMetrics < ActiveRecord::Migration
+class ImportCommonMetrics < ActiveRecord::Migration[4.2]
include Gitlab::Database::MigrationHelpers
require Rails.root.join('db/importers/common_metrics_importer.rb')
diff --git a/doc/development/sql.md b/doc/development/sql.md
index e1e1d31a85f..06005a0a6f8 100644
--- a/doc/development/sql.md
+++ b/doc/development/sql.md
@@ -106,7 +106,7 @@ transaction. Transactions for migrations can be disabled using the following
pattern:
```ruby
-class MigrationName < ActiveRecord::Migration
+class MigrationName < ActiveRecord::Migration[4.2]
disable_ddl_transaction!
end
```
@@ -114,7 +114,7 @@ end
For example:
```ruby
-class AddUsersLowerUsernameEmailIndexes < ActiveRecord::Migration
+class AddUsersLowerUsernameEmailIndexes < ActiveRecord::Migration[4.2]
disable_ddl_transaction!
def up
diff --git a/doc/development/testing_guide/best_practices.md b/doc/development/testing_guide/best_practices.md
index 72abda26e3d..24f4d457d45 100644
--- a/doc/development/testing_guide/best_practices.md
+++ b/doc/development/testing_guide/best_practices.md
@@ -19,6 +19,16 @@ Here are some things to keep in mind regarding test performance:
## RSpec
+To run rspec tests:
+
+```sh
+# run all tests
+bundle exec rspec
+
+# run test for path
+bundle exec rspec spec/[path]/[to]/[spec].rb
+```
+
### General guidelines
- Use a single, top-level `describe ClassName` block.
diff --git a/doc/development/testing_guide/ci.md b/doc/development/testing_guide/ci.md
index 8d9706a9501..d685cacf9ea 100644
--- a/doc/development/testing_guide/ci.md
+++ b/doc/development/testing_guide/ci.md
@@ -31,11 +31,7 @@ After that, the next pipeline will use the up-to-date
The GitLab test suite is [monitored] for the `master` branch, and any branch
that includes `rspec-profile` in their name.
-A [public dashboard] is available for everyone to see. Feel free to look at the
-slowest test files and try to improve them.
-
[monitored]: ../performance.md#rspec-profiling
-[public dashboard]: https://redash.gitlab.com/public/dashboards/l1WhHXaxrCWM5Ai9D7YDqHKehq6OU3bx5gssaiWe?org_slug=default
## CI setup
diff --git a/doc/development/testing_guide/review_apps.md b/doc/development/testing_guide/review_apps.md
index a6ed9e85a41..309babb5f94 100644
--- a/doc/development/testing_guide/review_apps.md
+++ b/doc/development/testing_guide/review_apps.md
@@ -62,6 +62,41 @@ You can also manually start the `review-qa-all`: it runs the full QA suite.
Note that both jobs first wait for the `review-deploy` job to be finished.
+## How to?
+
+### Find my Review App slug?
+
+1. Open the `review-deploy` job.
+1. Look for `Checking for previous deployment of review-*`.
+1. For instance for `Checking for previous deployment of review-qa-raise-e-12chm0`,
+ your Review App slug would be `review-qa-raise-e-12chm0` in this case.
+
+### Run a Rails console?
+
+1. [Filter Workloads by your Review App slug](https://console.cloud.google.com/kubernetes/workload?project=gitlab-review-apps)
+ , e.g. `review-29951-issu-id2qax`.
+1. Find and open the `task-runner` Deployment, e.g. `review-29951-issu-id2qax-task-runner`.
+1. Click on the Pod in the "Managed pods" section, e.g. `review-29951-issu-id2qax-task-runner-d5455cc8-2lsvz`.
+1. Click on the `KUBECTL` dropdown, then `Exec` -> `task-runner`.
+1. Replace `-c task-runner -- ls` with `-- /srv/gitlab/bin/rails c` from the
+ default command or
+ - Run `kubectl exec --namespace review-apps-ce -it review-29951-issu-id2qax-task-runner-d5455cc8-2lsvz -- /srv/gitlab/bin/rails c`
+ and
+ - Replace `review-apps-ce` with `review-apps-ee` if the Review App
+ is running EE, and
+ - Replace `review-29951-issu-id2qax-task-runner-d5455cc8-2lsvz`
+ with your Pod's name.
+
+### Dig into a Pod's logs?
+
+1. [Filter Workloads by your Review App slug](https://console.cloud.google.com/kubernetes/workload?project=gitlab-review-apps)
+ , e.g. `review-1979-1-mul-dnvlhv`.
+1. Find and open the `migrations` Deployment, e.g.
+ `review-1979-1-mul-dnvlhv-migrations.1`.
+1. Click on the Pod in the "Managed pods" section, e.g.
+ `review-1979-1-mul-dnvlhv-migrations.1-nqwtx`.
+1. Click on the `Container logs` link.
+
## Frequently Asked Questions
**Isn't it too much to trigger CNG image builds on every test run? This creates
diff --git a/doc/development/what_requires_downtime.md b/doc/development/what_requires_downtime.md
index 3630a28fae9..24edd05da2f 100644
--- a/doc/development/what_requires_downtime.md
+++ b/doc/development/what_requires_downtime.md
@@ -88,7 +88,7 @@ renaming. For example
```ruby
# A regular migration in db/migrate
-class RenameUsersUpdatedAtToUpdatedAtTimestamp < ActiveRecord::Migration
+class RenameUsersUpdatedAtToUpdatedAtTimestamp < ActiveRecord::Migration[4.2]
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
@@ -118,7 +118,7 @@ We can perform this cleanup using
```ruby
# A post-deployment migration in db/post_migrate
-class CleanupUsersUpdatedAtRename < ActiveRecord::Migration
+class CleanupUsersUpdatedAtRename < ActiveRecord::Migration[4.2]
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
@@ -157,7 +157,7 @@ as follows:
```ruby
# A regular migration in db/migrate
-class ChangeUsersUsernameStringToText < ActiveRecord::Migration
+class ChangeUsersUsernameStringToText < ActiveRecord::Migration[4.2]
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
@@ -178,7 +178,7 @@ Next we need to clean up our changes using a post-deployment migration:
```ruby
# A post-deployment migration in db/post_migrate
-class ChangeUsersUsernameStringToTextCleanup < ActiveRecord::Migration
+class ChangeUsersUsernameStringToTextCleanup < ActiveRecord::Migration[4.2]
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
@@ -213,7 +213,7 @@ the work / load over a longer time period, without slowing down deployments.
For example, to change the column type using a background migration:
```ruby
-class ExampleMigration < ActiveRecord::Migration
+class ExampleMigration < ActiveRecord::Migration[4.2]
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
@@ -257,7 +257,7 @@ release) by a cleanup migration, which should steal from the queue and handle
any remaining rows. For example:
```ruby
-class MigrateRemainingIssuesClosedAt < ActiveRecord::Migration
+class MigrateRemainingIssuesClosedAt < ActiveRecord::Migration[4.2]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
@@ -322,7 +322,7 @@ Migrations can take advantage of this by using the method
`add_concurrent_index`. For example:
```ruby
-class MyMigration < ActiveRecord::Migration
+class MyMigration < ActiveRecord::Migration[4.2]
def up
add_concurrent_index :projects, :column_name
end
diff --git a/doc/install/README.md b/doc/install/README.md
index 92116305775..ae48306e65e 100644
--- a/doc/install/README.md
+++ b/doc/install/README.md
@@ -5,8 +5,25 @@ description: Read through the GitLab installation methods.
# Installation
-GitLab can be installed via various ways. Check the [installation methods][methods]
-for an overview.
+GitLab can be installed in most GNU/Linux distributions and in a number
+of cloud providers. To get the best experience from GitLab you need to balance
+performance, reliability, ease of administration (backups, upgrades and troubleshooting),
+and cost of hosting.
+
+There are many ways you can install GitLab depending on your platform:
+
+1. **Omnibus Gitlab**: The official deb/rpm packages that contain a bundle of GitLab
+ and the various components it depends on like PostgreSQL, Redis, Sidekiq, etc.
+1. **GitLab Helm chart**: The cloud native Helm chart for installing GitLab and all
+ its components on Kubernetes.
+1. **Docker**: The Omnibus GitLab packages dockerized.
+1. **Source**: Install GitLab and all its components from scratch.
+
+TIP: **If in doubt, choose Omnibus:**
+The Omnibus GitLab packages are mature, scalable, support
+[high availability](../administration/high_availability/README.md) and are used
+today on GitLab.com. The Helm charts are recommended for those who are familiar
+with Kubernetes.
## Requirements
@@ -14,36 +31,58 @@ Before installing GitLab, make sure to check the [requirements documentation](re
which includes useful information on the supported Operating Systems as well as
the hardware requirements.
-## Installation methods
-
-- [Installation using the Omnibus packages](https://about.gitlab.com/downloads/) -
- Install GitLab using our official deb/rpm repositories. This is the
- recommended way.
-- [Installation from source](installation.md) - Install GitLab from source.
- Useful for unsupported systems like *BSD. For an overview of the directory
- structure, read the [structure documentation](structure.md).
-- [Docker](docker.md) - Install GitLab using Docker.
-
-## Install GitLab on cloud providers
-
-- [Installing in Kubernetes](kubernetes/index.md): Install GitLab into a Kubernetes
- Cluster using our official Helm Chart Repository.
-- [Install GitLab on OpenShift](openshift_and_gitlab/index.md)
-- [Install GitLab on DC/OS](https://mesosphere.com/blog/gitlab-dcos/) via [GitLab-Mesosphere integration](https://about.gitlab.com/2016/09/16/announcing-gitlab-and-mesosphere/)
-- [Install GitLab on Azure](azure/index.md)
-- [Install GitLab on Google Cloud Platform](google_cloud_platform/index.md)
-- [Install GitLab on Google Kubernetes Engine (GKE)](https://about.gitlab.com/2017/01/23/video-tutorial-idea-to-production-on-google-container-engine-gke/): video tutorial on
-the full process of installing GitLab on Google Kubernetes Engine (GKE), pushing an application to GitLab, building the app with GitLab CI/CD, and deploying to production.
-- [Install on AWS](aws/index.md): Install GitLab on AWS using the community AMIs that GitLab provides.
-- [Getting started with GitLab and DigitalOcean](https://about.gitlab.com/2016/04/27/getting-started-with-gitlab-and-digitalocean/): requirements, installation process, updates.
-- [Demo: Cloud Native Development with GitLab](https://about.gitlab.com/2017/04/18/cloud-native-demo/): video demonstration on how to install GitLab on Kubernetes, build a project, create Review Apps, store Docker images in Container Registry, deploy to production on Kubernetes, and monitor with Prometheus.
-- _Testing only!_ [DigitalOcean and Docker Machine](digitaloceandocker.md) -
- Quickly test any version of GitLab on DigitalOcean using Docker Machine.
+## Installing GitLab using the Omnibus GitLab package (recommended)
+
+The Omnibus GitLab package uses our official deb/rpm repositories. This is
+recommended for most users.
+
+If you need additional flexibility and resilience, we recommend deploying
+GitLab as described in our [High Availability documentation](../administration/high_availability/README.md).
+
+[**> Install GitLab using the Omnibus GitLab package.**](https://about.gitlab.com/install/)
+
+## Installing GitLab on Kubernetes via the GitLab Helm charts
+
+NOTE: **Kubernetes experience required:**
+We recommend being familiar with Kubernetes before using it to deploy GitLab in
+production. The methods for management, observability, and some concepts are
+different than traditional deployments.
+
+When installing GitLab on Kubernetes, there are some trade-offs that you
+need to be aware of:
-## Database
+- Administration and troubleshooting requires Kubernetes knowledge.
+- It can be more expensive for smaller installations. The default installation
+ requires more resources than a single node Omnibus deployment, as most services
+ are deployed in a redundant fashion.
+- There are some feature [limitations to be aware of](kubernetes/gitlab_chart.md#limitations).
-While the recommended database is PostgreSQL, we provide information to install
-GitLab using MySQL. Check the [MySQL documentation](database_mysql.md) for more
-information.
+[**> Install GitLab on Kubernetes using the GitLab Helm charts.**](kubernetes/index.md)
-[methods]: https://about.gitlab.com/installation/
+## Installing GitLab with Docker
+
+GitLab maintains a set of official Docker images based on the Omnibus GitLab package.
+
+[**> Install GitLab using the official GitLab Docker images.**](docker.md)
+
+## Installing GitLab from source
+
+If the GitLab Omnibus package is not available in your distribution, you can
+install GitLab from source: Useful for unsupported systems like *BSD. For an
+overview of the directory structure, read the [structure documentation](structure.md).
+
+[**> Install GitLab from source.**](installation.md)
+
+## Installing GitLab on cloud providers
+
+GitLab can be installed on a variety of cloud providers by using any of
+the above methods, provided the cloud provider supports it.
+
+- [Install on AWS](aws/index.md): Install Omnibus GitLab on AWS using the community AMIs that GitLab provides.
+- [Install GitLab on Google Cloud Platform](google_cloud_platform/index.md): Install Omnibus GitLab on a VM in GCP.
+- [Install GitLab on Azure](azure/index.md): Install Omnibus GitLab from Azure Marketplace.
+- [Install GitLab on OpenShift](openshift_and_gitlab/index.md): Install GitLab using the Docker image on OpenShift.
+- [Install GitLab on DC/OS](https://mesosphere.com/blog/gitlab-dcos/): Install GitLab on Mesosphere DC/OS via the [GitLab-Mesosphere integration](https://about.gitlab.com/2016/09/16/announcing-gitlab-and-mesosphere/).
+- [Install GitLab on DigitalOcean](https://about.gitlab.com/2016/04/27/getting-started-with-gitlab-and-digitalocean/): Install Omnibus GitLab on DigitalOcean.
+- _Testing only!_ [DigitalOcean and Docker Machine](digitaloceandocker.md):
+ Quickly test any version of GitLab on DigitalOcean using Docker Machine.
diff --git a/doc/install/docker.md b/doc/install/docker.md
index e90f6645b0c..d0129f0f5c4 100644
--- a/doc/install/docker.md
+++ b/doc/install/docker.md
@@ -8,9 +8,9 @@ GitLab provides official Docker images to allowing you to easily take advantage
GitLab maintains a set of [official Docker images](https://hub.docker.com/r/gitlab) based on our [Omnibus GitLab package](https://docs.gitlab.com/omnibus/README.html). These images include:
-- [GitLab Community Edition](https://hub.docker.com/r/gitlab/gitlab-ce/).
-- [GitLab Enterprise Edition](https://hub.docker.com/r/gitlab/gitlab-ee/).
-- [GitLab Runner](https://hub.docker.com/r/gitlab/gitlab-runner/).
+- [GitLab Community Edition](https://hub.docker.com/r/gitlab/gitlab-ce/)
+- [GitLab Enterprise Edition](https://hub.docker.com/r/gitlab/gitlab-ee/)
+- [GitLab Runner](https://hub.docker.com/r/gitlab/gitlab-runner/)
A [complete usage guide](https://docs.gitlab.com/omnibus/docker/) to these images is available, as well as the [Dockerfile used for building the images](https://gitlab.com/gitlab-org/omnibus-gitlab/tree/master/docker).
diff --git a/doc/install/kubernetes/gitlab_chart.md b/doc/install/kubernetes/gitlab_chart.md
index 5749eb0a9ec..74e2598f860 100644
--- a/doc/install/kubernetes/gitlab_chart.md
+++ b/doc/install/kubernetes/gitlab_chart.md
@@ -1,7 +1,14 @@
# GitLab Helm Chart
-This is the official and recommended way to install GitLab on a cloud native environment.
-For more information on other available GitLab Helm Charts, see the [charts overview](index.md#chart-overview).
+This is the official way to install GitLab on a cloud native environment.
+
+NOTE: **Kubernetes experience required:**
+Our Helm charts are recommended for those who are familiar with Kubernetes.
+If you're not sure if Kubernetes is for you, our
+[Omnibus GitLab packages](../README.md#install-gitlab-using-the-omnibus-gitlab-package-recommended)
+are mature, scalable, support [high availability](../../administration/high_availability/README.md)
+and are used today on GitLab.com.
+It is not necessary to have GitLab installed on Kubernetes in order to use [GitLab Kubernetes integration](https://docs.gitlab.com/ee/user/project/clusters/index.html).
## Introduction
diff --git a/doc/install/kubernetes/index.md b/doc/install/kubernetes/index.md
index 69171fbb341..281630174e7 100644
--- a/doc/install/kubernetes/index.md
+++ b/doc/install/kubernetes/index.md
@@ -4,11 +4,19 @@ description: 'Read through the different methods to deploy GitLab on Kubernetes.
# Installing GitLab on Kubernetes
+NOTE: **Kubernetes experience required:**
+Our Helm charts are recommended for those who are familiar with Kubernetes.
+If you're not sure if Kubernetes is for you, our
+[Omnibus GitLab packages](../README.md#install-gitlab-using-the-omnibus-gitlab-package-recommended)
+are mature, scalable, support [high availability](../../administration/high_availability/README.md)
+and are used today on GitLab.com.
+It is not necessary to have GitLab installed on Kubernetes in order to use [GitLab Kubernetes integration](https://docs.gitlab.com/ee/user/project/clusters/index.html).
+
The easiest method to deploy GitLab on [Kubernetes](https://kubernetes.io/) is
-to take advantage of GitLab's Helm charts. [Helm] is a package
-management tool for Kubernetes, allowing apps to be easily managed via their
-Charts. A [Chart] is a detailed description of the application including how it
-should be deployed, upgraded, and configured.
+to take advantage of GitLab's Helm charts. [Helm](https://github.com/kubernetes/helm/blob/master/README.md)
+is a package management tool for Kubernetes, allowing apps to be easily managed via their
+Charts. A [Chart](https://github.com/kubernetes/charts) is a detailed description
+of the application including how it should be deployed, upgraded, and configured.
## GitLab Chart
@@ -32,29 +40,3 @@ and you'd like to leverage the Runner's
it can be deployed with the GitLab Runner chart.
Learn more about [gitlab-runner chart](gitlab_runner_chart.md).
-
-## Deprecated Charts
-
-CAUTION: **Deprecated:**
-These charts are **deprecated**. We recommend using the [GitLab Chart](gitlab_chart.md)
-instead.
-
-### GitLab-Omnibus Chart
-
-This chart is based on the [GitLab Omnibus Docker images](https://docs.gitlab.com/omnibus/docker/).
-It deploys and configures nearly all features of GitLab, including:
-
-- a [GitLab Runner](https://docs.gitlab.com/runner/)
-- [Container Registry](../../user/project/container_registry.html#gitlab-container-registry)
-- [Mattermost](https://docs.gitlab.com/omnibus/gitlab-mattermost/)
-- [automatic SSL](https://github.com/kubernetes/charts/tree/master/stable/kube-lego)
-- and an [NGINX load balancer](https://github.com/kubernetes/ingress/tree/master/controllers/nginx).
-
-Learn more about the [gitlab-omnibus chart](gitlab_omnibus.md).
-
-### Community Contributed Charts
-
-The community has also contributed GitLab [CE](https://github.com/kubernetes/charts/tree/master/stable/gitlab-ce) and [EE](https://github.com/kubernetes/charts/tree/master/stable/gitlab-ee) charts to the [Helm Stable Repository](https://github.com/kubernetes/charts#repository-structure). These charts are [deprecated](https://github.com/kubernetes/charts/issues/1138) in favor of the [official Chart](gitlab_chart.md).
-
-[chart]: https://github.com/kubernetes/charts
-[helm]: https://github.com/kubernetes/helm/blob/master/README.md
diff --git a/doc/integration/recaptcha.md b/doc/integration/recaptcha.md
index 8fdadb008ec..825c3654492 100644
--- a/doc/integration/recaptcha.md
+++ b/doc/integration/recaptcha.md
@@ -9,9 +9,9 @@ to confirm that a real user, not a bot, is attempting to create an account.
To use reCAPTCHA, first you must create a site and private key.
1. Go to the URL: <https://www.google.com/recaptcha/admin>.
-1. Fill out the form necessary to obtain reCAPTCHA keys.
-1. Login to your GitLab server, with administrator credentials.
-1. Go to Applications Settings on Admin Area (`admin/application_settings`).
+1. Fill out the form necessary to obtain reCAPTCHA v2 keys.
+1. Log in to your GitLab server, with administrator credentials.
+1. Go to Reporting Applications Settings in the Admin Area (`admin/application_settings/reporting`).
1. Fill all recaptcha fields with keys from previous steps.
1. Check the `Enable reCAPTCHA` checkbox.
1. Save the configuration.
diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md
index a63656fafef..57bc71d2903 100644
--- a/doc/raketasks/backup_restore.md
+++ b/doc/raketasks/backup_restore.md
@@ -657,6 +657,7 @@ Restoring database tables:
- Loading fixture wikis...[SKIPPING]
Restoring repositories:
- Restoring repository abcd... [DONE]
+- Object pool 1 ...
Deleting tmp directories...[DONE]
```
diff --git a/doc/raketasks/web_hooks.md b/doc/raketasks/web_hooks.md
index 5f3143f76cd..df3dab118b2 100644
--- a/doc/raketasks/web_hooks.md
+++ b/doc/raketasks/web_hooks.md
@@ -38,8 +38,6 @@
## List the webhooks from projects in a given **NAMESPACE**:
# omnibus-gitlab
- sudo gitlab-rake gitlab:web_hook:list NAMESPACE=/
+ sudo gitlab-rake gitlab:web_hook:list NAMESPACE=acme
# source installations
- bundle exec rake gitlab:web_hook:list NAMESPACE=/ RAILS_ENV=production
-
-> Note: `/` is the global namespace.
+ bundle exec rake gitlab:web_hook:list NAMESPACE=acme RAILS_ENV=production
diff --git a/doc/security/rack_attack.md b/doc/security/rack_attack.md
index dcdc9f42c22..ad83dc05a93 100644
--- a/doc/security/rack_attack.md
+++ b/doc/security/rack_attack.md
@@ -10,8 +10,7 @@ Rack Attack offers IP whitelisting, blacklisting, Fail2ban style filtering and
tracking.
**Note:** Starting with 11.2, Rack Attack is disabled by default. To continue
-using this feature, please enable it in your `gitlab.rb` by setting
-`gitlab_rails['rack_attack_git_basic_auth'] = true`.
+using this feature, please enable it by [configuring `gitlab.rb` as described in Settings](#settings).
By default, user sign-in, user sign-up (if enabled), and user password reset is
limited to 6 requests per minute. After trying for 6 times, the client will
diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md
index 63e7497cbbc..7885cffd107 100644
--- a/doc/topics/autodevops/index.md
+++ b/doc/topics/autodevops/index.md
@@ -132,7 +132,8 @@ in three places:
- either under the project's CI/CD settings while [enabling Auto DevOps](#enabling-auto-devops)
- or in instance-wide settings in the **admin area > Settings** under the "Continuous Integration and Delivery" section
-- or at the project or group level as a variable: `AUTO_DEVOPS_DOMAIN` (required if you want to use [multiple clusters](#using-multiple-kubernetes-clusters))
+- or at the project as a variable: `AUTO_DEVOPS_DOMAIN` (required if you want to use [multiple clusters](#using-multiple-kubernetes-clusters))
+- or at the group level as a variable: `AUTO_DEVOPS_DOMAIN`
A wildcard DNS A record matching the base domain(s) is required, for example,
given a base domain of `example.com`, you'd need a DNS entry like:
@@ -203,6 +204,12 @@ and verifying that your app is deployed as a review app in the Kubernetes
cluster with the `review/*` environment scope. Similarly, you can check the
other environments.
+NOTE: **Note:**
+Auto DevOps is not supported for a group with multiple clusters, as it
+is not possible to set `AUTO_DEVOPS_DOMAIN` per environment on the group
+level. This will be resolved in the future with the [following issue](
+https://gitlab.com/gitlab-org/gitlab-ce/issues/52363).
+
## Enabling/Disabling Auto DevOps
When first using Auto Devops, review the [requirements](#requirements) to ensure all necessary components to make
diff --git a/doc/user/group/clusters/index.md b/doc/user/group/clusters/index.md
new file mode 100644
index 00000000000..adc43921d47
--- /dev/null
+++ b/doc/user/group/clusters/index.md
@@ -0,0 +1,126 @@
+# Group-level Kubernetes clusters
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/34758) in GitLab 11.6.
+
+CAUTION: **Warning:**
+Group Cluster integration is currently in **Beta**.
+
+## Overview
+
+Similar to [project Kubernetes
+clusters](../../project/clusters/index.md), Group-level Kubernetes
+clusters allow you to connect a Kubernetes cluster to your group,
+enabling you to use the same cluster across multiple projects.
+
+## Installing applications
+
+GitLab provides a one-click install for various applications that can be
+added directly to your cluster.
+
+NOTE: **Note:**
+Applications will be installed in a dedicated namespace called
+`gitlab-managed-apps`. If you have added an existing Kubernetes cluster
+with Tiller already installed, you should be careful as GitLab cannot
+detect it. In this event, installing Tiller via the applications will
+result in the cluster having it twice. This can lead to confusion during
+deployments.
+
+| Application | GitLab version | Description | Helm Chart |
+| ----------- | -------------- | ----------- | ---------- |
+| [Helm Tiller](https://docs.helm.sh) | 10.2+ | Helm is a package manager for Kubernetes and is required to install all the other applications. It is installed in its own pod inside the cluster which can run the `helm` CLI in a safe environment. | n/a |
+| [Ingress](https://kubernetes.io/docs/concepts/services-networking/ingress) | 10.2+ | Ingress can provide load balancing, SSL termination, and name-based virtual hosting. It acts as a web proxy for your applications and is useful if you want to use [Auto DevOps](../../../topics/autodevops/index.md) or deploy your own web apps. | [stable/nginx-ingress](https://github.com/helm/charts/tree/master/stable/nginx-ingress) |
+
+## RBAC compatibility
+
+For each project under a group with a Kubernetes cluster, GitLab will
+create a restricted service account with [`edit`
+privileges](https://kubernetes.io/docs/reference/access-authn-authz/rbac/#user-facing-roles)
+in the project namespace.
+
+NOTE: **Note:**
+RBAC support was introduced in
+[GitLab 11.4](https://gitlab.com/gitlab-org/gitlab-ce/issues/29398), and
+Project namespace restriction was introduced in
+[GitLab 11.5](https://gitlab.com/gitlab-org/gitlab-ce/issues/51716).
+
+## Cluster precedence
+
+GitLab will use the project's cluster before using any cluster belonging
+to the group containing the project if the project's cluster is available and not disabled.
+
+In the case of sub-groups, GitLab will use the cluster of the closest ancestor group
+to the project, provided the cluster is not disabled.
+
+## Multiple Kubernetes clusters **[PREMIUM]**
+
+With GitLab Premium, you can associate more than one Kubernetes clusters to your
+group. That way you can have different clusters for different environments,
+like dev, staging, production, etc.
+
+Add another cluster similar to the first one and make sure to
+[set an environment scope](#environment-scopes) that will
+differentiate the new cluster from the rest.
+
+NOTE: **Note:**
+Auto DevOps is not supported for a group with multiple clusters, as it
+is not possible to set `AUTO_DEVOPS_DOMAIN` per environment on the group
+level. This will be resolved in the future with the [following issue](
+https://gitlab.com/gitlab-org/gitlab-ce/issues/52363).
+
+## Environment scopes **[PREMIUM]**
+
+When adding more than one Kubernetes cluster to your project, you need
+to differentiate them with an environment scope. The environment scope
+associates clusters with [environments](../../../ci/environments.md)
+similar to how the [environment-specific
+variables](../../../ci/variables/README.md#limiting-environment-scopes-of-variables)
+work.
+
+While evaluating which environment matches the environment scope of a
+cluster, [cluster precedence](#cluster-precedence) will take
+effect. The cluster at the project level will take precedence, followed
+by the closest ancestor group, followed by that groups' parent and so
+on.
+
+For example, let's say we have the following Kubernetes clusters:
+
+| Cluster | Environment scope | Where |
+| ---------- | ------------------- | ----------|
+| Project | `*` | Project |
+| Staging | `staging/*` | Project |
+| Production | `production/*` | Project |
+| Test | `test` | Group |
+| Development| `*` | Group |
+
+
+And the following environments are set in [`.gitlab-ci.yml`](../../../ci/yaml/README.md):
+
+```yaml
+stages:
+- test
+- deploy
+
+test:
+ stage: test
+ script: sh test
+
+deploy to staging:
+ stage: deploy
+ script: make deploy
+ environment:
+ name: staging/$CI_COMMIT_REF_NAME
+ url: https://staging.example.com/
+
+deploy to production:
+ stage: deploy
+ script: make deploy
+ environment:
+ name: production/$CI_COMMIT_REF_NAME
+ url: https://example.com/
+```
+
+The result will then be:
+
+- The Project cluster will be used for the `test` job.
+- The Staging cluster will be used for the `deploy to staging` job.
+- The Production cluster will be used for the `deploy to production` job.
diff --git a/doc/user/group/index.md b/doc/user/group/index.md
index 36b9318c0e0..5fea683a7fd 100644
--- a/doc/user/group/index.md
+++ b/doc/user/group/index.md
@@ -269,6 +269,7 @@ Define project templates at a group-level by setting a group as a template sourc
- **Projects**: view all projects within that group, add members to each project,
access each project's settings, and remove any project from the same screen.
- **Webhooks**: configure [webhooks](../project/integrations/webhooks.md) to your group.
+- **Kubernetes cluster integration**: connect your GitLab group with [Kubernetes clusters](clusters/index.md).
- **Audit Events**: view [Audit Events](https://docs.gitlab.com/ee/administration/audit_events.html#audit-events)
for the group. **[STARTER ONLY]**
-- **Pipelines quota**: keep track of the [pipeline quota](../admin_area/settings/continuous_integration.md) for the group
+- **Pipelines quota**: keep track of the [pipeline quota](../admin_area/settings/continuous_integration.md) for the group.
diff --git a/doc/user/group/subgroups/index.md b/doc/user/group/subgroups/index.md
index 8db36c4a0e8..943b0c693c0 100644
--- a/doc/user/group/subgroups/index.md
+++ b/doc/user/group/subgroups/index.md
@@ -167,7 +167,6 @@ Here's a list of what you can't do with subgroups:
- [GitLab Pages](../../project/pages/index.md) are not currently working for
projects hosted under a subgroup. That means that only projects hosted under
the first parent group will work.
-- Group level labels don't work in subgroups / sub projects
- It is not possible to share a project with a group that's an ancestor of
the group the project is in. That means you can only share as you walk down
the hierarchy. For example, `group/subgroup01/project` **cannot** be shared
diff --git a/doc/user/project/clusters/index.md b/doc/user/project/clusters/index.md
index 66ad1843e93..95bd5d1cf4e 100644
--- a/doc/user/project/clusters/index.md
+++ b/doc/user/project/clusters/index.md
@@ -17,6 +17,11 @@ your account with Google Kubernetes Engine (GKE) so that you can [create new
clusters](#adding-and-creating-a-new-gke-cluster-via-gitlab) from within GitLab,
or provide the credentials to an [existing Kubernetes cluster](#adding-an-existing-kubernetes-cluster).
+NOTE: **Note:**
+From [GitLab 11.6](https://gitlab.com/gitlab-org/gitlab-ce/issues/34758) you
+can also associate a Kubernetes cluster to your groups. Learn more about
+[group Kubernetes clusters](../../group/clusters/index.md).
+
## Adding and creating a new GKE cluster via GitLab
TIP: **Tip:**
@@ -245,25 +250,27 @@ install it manually.
## Installing applications
-GitLab provides a one-click install for various applications which will be
-added directly to your configured cluster. Those applications are needed for
-[Review Apps](../../../ci/review_apps/index.md) and [deployments](../../../ci/environments.md).
+GitLab provides a one-click install for various applications which can
+be added directly to your configured cluster. Those applications are
+needed for [Review Apps](../../../ci/review_apps/index.md) and
+[deployments](../../../ci/environments.md).
NOTE: **Note:**
With the exception of Knative, the applications will be installed in a dedicated namespace called
`gitlab-managed-apps`. In case you have added an existing Kubernetes cluster
with Tiller already installed, you should be careful as GitLab cannot
-detect it. By installing it via the applications will result into having it
-twice, which can lead to confusion during deployments.
+detect it. In this event, installing Tiller via the applications will
+result in the cluster having it twice. This can lead to confusion during
+deployments.
| Application | GitLab version | Description | Helm Chart |
| ----------- | :------------: | ----------- | --------------- |
| [Helm Tiller](https://docs.helm.sh/) | 10.2+ | Helm is a package manager for Kubernetes and is required to install all the other applications. It is installed in its own pod inside the cluster which can run the `helm` CLI in a safe environment. | n/a |
| [Ingress](https://kubernetes.io/docs/concepts/services-networking/ingress/) | 10.2+ | Ingress can provide load balancing, SSL termination, and name-based virtual hosting. It acts as a web proxy for your applications and is useful if you want to use [Auto DevOps] or deploy your own web apps. | [stable/nginx-ingress](https://github.com/helm/charts/tree/master/stable/nginx-ingress) |
-| [Cert Manager](http://docs.cert-manager.io/en/latest/) | 11.6+ | Cert Manager is a native Kubernetes certificate management controller that helps with issuing certificates. Installing Cert Manager on your cluster will issue a certificate by [Let's Encrypt](https://letsencrypt.org/) and ensure that certificates are valid and up to date. The email address used by Let's Encrypt registration will be taken from the GitLab user that installed Cert Manager on the cluster. | [stable/cert-manager](https://github.com/helm/charts/tree/master/stable/cert-manager) |
+| [Cert Manager](http://docs.cert-manager.io/en/latest/) | 11.6+ | Cert Manager is a native Kubernetes certificate management controller that helps with issuing certificates. Installing Cert Manager on your cluster will issue a certificate by [Let's Encrypt](https://letsencrypt.org/) and ensure that certificates are valid and up-to-date. | [stable/cert-manager](https://github.com/helm/charts/tree/master/stable/cert-manager) |
| [Prometheus](https://prometheus.io/docs/introduction/overview/) | 10.4+ | Prometheus is an open-source monitoring and alerting system useful to supervise your deployed applications. | [stable/prometheus](https://github.com/helm/charts/tree/master/stable/prometheus) |
| [GitLab Runner](https://docs.gitlab.com/runner/) | 10.6+ | GitLab Runner is the open source project that is used to run your jobs and send the results back to GitLab. It is used in conjunction with [GitLab CI/CD](https://about.gitlab.com/features/gitlab-ci-cd/), the open-source continuous integration service included with GitLab that coordinates the jobs. When installing the GitLab Runner via the applications, it will run in **privileged mode** by default. Make sure you read the [security implications](#security-implications) before doing so. | [runner/gitlab-runner](https://gitlab.com/charts/gitlab-runner) |
-| [JupyterHub](http://jupyter.org/) | 11.0+ | [JupyterHub](https://jupyterhub.readthedocs.io/en/stable/) is a multi-user service for managing notebooks across a team. [Jupyter Notebooks](https://jupyter-notebook.readthedocs.io/en/latest/) provide a web-based interactive programming environment used for data analysis, visualization, and machine learning. We use [this](https://gitlab.com/gitlab-org/jupyterhub-user-image/blob/master/Dockerfile) custom Jupyter image that installs additional useful packages on top of the base Jupyter. You will also see ready-to-use DevOps Runbooks built with Nurtch's [Rubix library](https://github.com/amit1rrr/rubix). More information on creating executable runbooks can be found at [Nurtch Documentation](http://docs.nurtch.com/en/latest). **Note**: Authentication will be enabled for any user of the GitLab server via OAuth2. HTTPS will be supported in a future release. | [jupyter/jupyterhub](https://jupyterhub.github.io/helm-chart/) |
+| [JupyterHub](http://jupyter.org/) | 11.0+ | [JupyterHub](https://jupyterhub.readthedocs.io/en/stable/) is a multi-user service for managing notebooks across a team. [Jupyter Notebooks](https://jupyter-notebook.readthedocs.io/en/latest/) provide a web-based interactive programming environment used for data analysis, visualization, and machine learning. We use a [custom Jupyter image](https://gitlab.com/gitlab-org/jupyterhub-user-image/blob/master/Dockerfile) that installs additional useful packages on top of the base Jupyter. You will also see ready-to-use DevOps Runbooks built with Nurtch's [Rubix library](https://github.com/amit1rrr/rubix). More information on creating executable runbooks can be found in [our Nurtch documentation](runbooks/index.md#nurtch-executable-runbooks). **Note**: Authentication will be enabled for any user of the GitLab server via OAuth2. HTTPS will be supported in a future release. | [jupyter/jupyterhub](https://jupyterhub.github.io/helm-chart/) |
| [Knative](https://cloud.google.com/knative) | 11.5+ | Knative provides a platform to create, deploy, and manage serverless workloads from a Kubernetes cluster. It is used in conjunction with, and includes [Istio](https://istio.io) to provide an external IP address for all programs hosted by Knative. You will be prompted to enter a wildcard domain where your applications will be exposed. Configure your DNS server to use the external IP address for that domain. For any application created and installed, they will be accessible as `<program_name>.<kubernetes_namespace>.<domain_name>`. This will require your kubernetes cluster to have [RBAC enabled](#role-based-access-control-rbac). | [knative/knative](https://storage.googleapis.com/triggermesh-charts)
NOTE: **Note:**
@@ -347,17 +354,13 @@ to reach your apps. This heavily depends on your domain provider, but in case
you aren't sure, just create an A record with a wildcard host like
`*.example.com.`.
-## Setting the environment scope
+## Setting the environment scope **[PREMIUM]**
-NOTE: **Note:**
-This is only available for [GitLab Premium][ee] where you can add more than
-one Kubernetes cluster.
-
-When adding more than one Kubernetes clusters to your project, you need to
-differentiate them with an environment scope. The environment scope associates
-clusters and [environments](../../../ci/environments.md) in an 1:1 relationship
-similar to how the
-[environment-specific variables](../../../ci/variables/README.md#limiting-environment-scopes-of-variables)
+When adding more than one Kubernetes clusters to your project, you need
+to differentiate them with an environment scope. The environment scope
+associates clusters with [environments](../../../ci/environments.md)
+similar to how the [environment-specific
+variables](../../../ci/variables/README.md#limiting-environment-scopes-of-variables)
work.
The default environment scope is `*`, which means all jobs, regardless of their
diff --git a/doc/user/project/clusters/serverless/img/install-knative.png b/doc/user/project/clusters/serverless/img/install-knative.png
index dd576a9df35..a9fcc127240 100644
--- a/doc/user/project/clusters/serverless/img/install-knative.png
+++ b/doc/user/project/clusters/serverless/img/install-knative.png
Binary files differ
diff --git a/doc/user/project/clusters/serverless/img/serverless-page.png b/doc/user/project/clusters/serverless/img/serverless-page.png
new file mode 100644
index 00000000000..473ee801f10
--- /dev/null
+++ b/doc/user/project/clusters/serverless/img/serverless-page.png
Binary files differ
diff --git a/doc/user/project/issues/img/similar_issues.png b/doc/user/project/issues/img/similar_issues.png
new file mode 100644
index 00000000000..153430d4be7
--- /dev/null
+++ b/doc/user/project/issues/img/similar_issues.png
Binary files differ
diff --git a/doc/user/project/issues/index.md b/doc/user/project/issues/index.md
index d71273ba970..200b3a642a1 100644
--- a/doc/user/project/issues/index.md
+++ b/doc/user/project/issues/index.md
@@ -155,3 +155,7 @@ Read through the [API documentation](../../../api/issues.md).
### Bulk editing issues
Find out about [bulk editing issues](../../project/bulk_editing.md).
+
+### Similar issues
+
+Find out about [similar issues](similar_issues.md).
diff --git a/doc/user/project/issues/similar_issues.md b/doc/user/project/issues/similar_issues.md
new file mode 100644
index 00000000000..e90ecd88ec6
--- /dev/null
+++ b/doc/user/project/issues/similar_issues.md
@@ -0,0 +1,16 @@
+# Similar issues
+
+> [Introduced][ce-22866] in GitLab 11.6.
+
+Similar issues suggests issues that are similar when new issues are being created.
+This features requires [GraphQL] to be enabled.
+
+![Similar issues](img/similar_issues.png)
+
+You can see the similar issues when typing in the title in the new issue form.
+This searches both titles and descriptions across all issues the user has access
+to in the current project. It then displays the first 5 issues sorted by most
+recently updated.
+
+[GraphQL]: ../../../api/graphql/index.md
+[ce-22866]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22866
diff --git a/doc/user/project/merge_requests/index.md b/doc/user/project/merge_requests/index.md
index 5b54b6ecdd5..85d8d804133 100644
--- a/doc/user/project/merge_requests/index.md
+++ b/doc/user/project/merge_requests/index.md
@@ -259,6 +259,16 @@ all your changes will be available to preview by anyone with the Review Apps lin
[Read more about Review Apps.](../../../ci/review_apps/index.md)
+## Pipelines for merge requests
+
+When a developer updates a merge request, a pipeline should quickly report back
+its result to the developer, but often pipelines take long time to complete
+because general branch pipelines contain unnecessary jobs from the merge request standpoint.
+You can customize a specific pipeline structure for merge requests in order to
+speed the cycle up by running only important jobs.
+
+Learn more about [pipelines for merge requests](../../../ci/merge_request_pipelines/index.md).
+
## Pipeline status in merge requests
If you've set up [GitLab CI/CD](../../../ci/README.md) in your project,
diff --git a/doc/user/project/repository/img/repository_cleanup.png b/doc/user/project/repository/img/repository_cleanup.png
new file mode 100644
index 00000000000..2749392ffa4
--- /dev/null
+++ b/doc/user/project/repository/img/repository_cleanup.png
Binary files differ
diff --git a/doc/user/project/repository/reducing_the_repo_size_using_git.md b/doc/user/project/repository/reducing_the_repo_size_using_git.md
index d534c8cbe4b..672567a8d7d 100644
--- a/doc/user/project/repository/reducing_the_repo_size_using_git.md
+++ b/doc/user/project/repository/reducing_the_repo_size_using_git.md
@@ -1,43 +1,105 @@
# Reducing the repository size using Git
A GitLab Enterprise Edition administrator can set a [repository size limit][admin-repo-size]
-which will prevent you to exceed it.
+which will prevent you from exceeding it.
When a project has reached its size limit, you will not be able to push to it,
create a new merge request, or merge existing ones. You will still be able to
create new issues, and clone the project though. Uploading LFS objects will
also be denied.
-In order to lift these restrictions, the administrator of the GitLab instance
-needs to increase the limit on the particular project that exceeded it or you
-need to instruct Git to rewrite changes.
-
If you exceed the repository size limit, your first thought might be to remove
-some data, make a new commit and push back to the repository. Unfortunately,
-it's not so easy and that workflow won't work. Deleting files in a commit doesn't
-actually reduce the size of the repo since the earlier commits and blobs are
-still around. What you need to do is rewrite history with Git's
-[`filter-branch` option][gitscm].
+some data, make a new commit and push back to the repository. Perhaps you can
+move some blobs to LFS, or remove some old dependency updates from history.
+Unfortunately, it's not so easy and that workflow won't work. Deleting files in
+a commit doesn't actually reduce the size of the repo since the earlier commits
+and blobs are still around. What you need to do is rewrite history with Git's
+[`filter-branch` option][gitscm], or a tool like the [BFG Repo-Cleaner][bfg].
Note that even with that method, until `git gc` runs on the GitLab side, the
-"removed" commits and blobs will still be around. And if a commit was ever
-included in an MR, or if a build was run for a commit, or if a user commented
-on it, it will be kept around too. So, in these cases the size will not decrease.
-
-The only fool proof way to actually decrease the repository size is to prune all
-the unneeded stuff locally, and then create a new project on GitLab and start
-using that instead.
+"removed" commits and blobs will still be around. You also need to be able to
+push the rewritten history to GitLab, which may be impossible if you've already
+exceeded the maximum size limit.
-With that being said, you can try reducing your repository size with the
-following method.
-
-## Using `git filter-branch` to purge files
+In order to lift these restrictions, the administrator of the GitLab instance
+needs to increase the limit on the particular project that exceeded it, so it's
+always better to spot that you're approaching the limit and act proactively to
+stay underneath it. If you hit the limit, and your admin can't - or won't -
+temporarily increase it for you, your only option is to prune all the unneeded
+stuff locally, and then create a new project on GitLab and start using that
+instead.
+
+If you can continue to use the original project, we recommend [using the
+BFG Repo-Cleaner](#using-the-bfg-repo-cleaner). It's faster and simpler than
+`git filter-branch`, and GitLab can use its account of what has changed to clean
+up its own internal state, maximizing the space saved.
> **Warning:**
> Make sure to first make a copy of your repository since rewriting history will
> purge the files and information you are about to delete. Also make sure to
> inform any collaborators to not use `pull` after your changes, but use `rebase`.
+> **Warning:**
+> This process is not suitable for removing sensitive data like password or keys
+> from your repository. Information about commits, including file content, is
+> cached in the database, and will remain visible even after they have been
+> removed from the repository.
+
+## Using the BFG Repo-Cleaner
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/19376) in GitLab 11.6.
+
+1. [Install BFG](https://rtyley.github.io/bfg-repo-cleaner/).
+
+1. Navigate to your repository:
+
+ ```
+ cd my_repository/
+ ```
+
+1. Change to the branch you want to remove the big file from:
+
+ ```
+ git checkout master
+ ```
+
+1. Create a commit removing the large file from the branch, if it still exists:
+
+ ```
+ git rm path/to/big_file.mpg
+ git commit -m 'Remove unneeded large file'
+ ```
+
+1. Rewrite history:
+
+ ```
+ bfg --delete-files path/to/big_file.mpg
+ ```
+
+ An object map file will be written to `object-id-map.old-new.txt`. Keep it
+ around - you'll need it for the final step!
+
+1. Force-push the changes to GitLab:
+
+ ```
+ git push --force-with-lease origin master
+ ```
+
+ If this step fails, someone has changed the `master` branch while you were
+ rewriting history. You could restore the branch and re-run BFG to preserve
+ their changes, or use `git push --force` to overwrite their changes.
+
+1. Navigate to **Project > Settings > Repository > Repository Cleanup**:
+
+ ![Repository settings cleanup form](img/repository_cleanup.png)
+
+ Upload the `object-id-map.old-new.txt` file and press **Start cleanup**.
+ This will remove any internal git references to the old commits, and run
+ `git gc` against the repository. You will receive an email once it has
+ completed.
+
+## Using `git filter-branch`
+
1. Navigate to your repository:
```
@@ -70,11 +132,6 @@ following method.
Your repository should now be below the size limit.
-> **Note:**
-> As an alternative to `filter-branch`, you can use the `bfg` tool with a
-> command like: `bfg --delete-files path/to/big_file.mpg`. Read the
-> [BFG Repo-Cleaner][bfg] documentation for more information.
-
[admin-repo-size]: https://docs.gitlab.com/ee/user/admin_area/settings/account_and_limit_settings.html#repository-size-limit
[bfg]: https://rtyley.github.io/bfg-repo-cleaner/
[gitscm]: https://git-scm.com/book/en/v2/Git-Tools-Rewriting-History#The-Nuclear-Option:-filter-branch
diff --git a/doc/user/project/settings/import_export.md b/doc/user/project/settings/import_export.md
index f94671fcf87..cb68c9318bc 100644
--- a/doc/user/project/settings/import_export.md
+++ b/doc/user/project/settings/import_export.md
@@ -24,6 +24,10 @@
> Otherwise, a supplementary comment is left to mention the original author and
> the MRs, notes or issues will be owned by the importer.
> - Control project Import/Export with the [API](../../../api/project_import_export.md).
+> - If an imported project contains merge requests originated from forks,
+> then new branches associated with such merge requests will be created
+> within a project during the import/export. Thus, the number of branches
+> in the exported project could be bigger than in the original project.
Existing projects running on any GitLab instance or GitLab.com can be exported
with all their related data and be moved into a new GitLab instance.
diff --git a/doc/user/project/web_ide/index.md b/doc/user/project/web_ide/index.md
index e6b1f6b6aae..55e53b865af 100644
--- a/doc/user/project/web_ide/index.md
+++ b/doc/user/project/web_ide/index.md
@@ -1,6 +1,6 @@
# Web IDE
-> [Introduced in](https://gitlab.com/gitlab-org/gitlab-ee/issues/4539) [GitLab Ultimate][ee] 10.4.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/4539) in [GitLab Ultimate][ee] 10.4.
> [Brought to GitLab Core](https://gitlab.com/gitlab-org/gitlab-ce/issues/44157) in 10.7.
The Web IDE makes it faster and easier to contribute changes to your projects
@@ -15,7 +15,7 @@ and from merge requests.
## File finder
-> [Introduced in](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/18323) [GitLab Core][ce] 10.8.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/18323) in [GitLab Core][ce] 10.8.
The file finder allows you to quickly open files in the current branch by
searching. The file finder is launched using the keyboard shortcut `Command-p`,
@@ -65,7 +65,7 @@ shows you a preview of the merge request diff if you commit your changes.
## View CI job logs
-> [Introduced in](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/19279) [GitLab Core][ce] 11.0.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/19279) in [GitLab Core][ce] 11.0.
The Web IDE can be used to quickly fix failing tests by opening the branch or
merge request in the Web IDE and opening the logs of the failed job. The status
@@ -77,7 +77,7 @@ left.
## Switching merge requests
-> [Introduced in](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/19318) [GitLab Core][ce] 11.0.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/19318) in [GitLab Core][ce] 11.0.
Switching between your authored and assigned merge requests can be done without
leaving the Web IDE. Click the dropdown in the top of the sidebar to open a list
@@ -86,7 +86,7 @@ switching to a different merge request.
## Switching branches
-> [Introduced in](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/20850) [GitLab Core][ce] 11.2.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/20850) in [GitLab Core][ce] 11.2.
Switching between branches of the current project repository can be done without
leaving the Web IDE. Click the dropdown in the top of the sidebar to open a list
@@ -95,7 +95,7 @@ switching to a different branch.
## Client Side Evaluation
-> [Introduced in](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/19764) [GitLab Core][ce] 11.2.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/19764) in [GitLab Core][ce] 11.2.
The Web IDE can be used to preview JavaScript projects right in the browser.
This feature uses CodeSandbox to compile and bundle the JavaScript used to
diff --git a/doc/workflow/notifications.md b/doc/workflow/notifications.md
index 020aa73f809..6ce789998a4 100644
--- a/doc/workflow/notifications.md
+++ b/doc/workflow/notifications.md
@@ -135,6 +135,7 @@ Notification emails include headers that provide extra content about the notific
| X-GitLab-Pipeline-Id | Only in pipeline emails, the ID of the pipeline the notification is for |
| X-GitLab-Reply-Key | A unique token to support reply by email |
| X-GitLab-NotificationReason | The reason for being notified. "mentioned", "assigned", etc |
+| List-Id | The path of the project in a RFC 2919 mailing list identifier useful for email organization, for example, with GMail filters |
#### X-GitLab-NotificationReason
diff --git a/config/jest.config.js b/jest.config.js
index 23e62f49be1..4dab7c2891a 100644
--- a/config/jest.config.js
+++ b/jest.config.js
@@ -14,8 +14,10 @@ if (process.env.CI) {
// eslint-disable-next-line import/no-commonjs
module.exports = {
testMatch: ['<rootDir>/spec/frontend/**/*_spec.js'],
+ moduleFileExtensions: ['js', 'json', 'vue'],
moduleNameMapper: {
'^~(.*)$': '<rootDir>/app/assets/javascripts$1',
+ '^helpers(.*)$': '<rootDir>/spec/frontend/helpers$1',
},
collectCoverageFrom: ['<rootDir>/app/assets/javascripts/**/*.{js,vue}'],
coverageDirectory: '<rootDir>/coverage-frontend/',
@@ -23,5 +25,10 @@ module.exports = {
cacheDirectory: '<rootDir>/tmp/cache/jest',
modulePathIgnorePatterns: ['<rootDir>/.yarn-cache/'],
reporters,
- rootDir: '..', // necessary because this file is in the config/ subdirectory
+ setupTestFrameworkScriptFile: '<rootDir>/spec/frontend/test_setup.js',
+ restoreMocks: true,
+ transform: {
+ '^.+\\.js$': 'babel-jest',
+ '^.+\\.vue$': 'vue-jest',
+ },
};
diff --git a/lib/api/api.rb b/lib/api/api.rb
index a4bf0d77eb1..8abb24e6f69 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -20,7 +20,8 @@ module API
Gitlab::GrapeLogging::Loggers::RouteLogger.new,
Gitlab::GrapeLogging::Loggers::UserLogger.new,
Gitlab::GrapeLogging::Loggers::QueueDurationLogger.new,
- Gitlab::GrapeLogging::Loggers::PerfLogger.new
+ Gitlab::GrapeLogging::Loggers::PerfLogger.new,
+ Gitlab::GrapeLogging::Loggers::CorrelationIdLogger.new
]
allow_access_with_scope :api
@@ -84,7 +85,6 @@ module API
content_type :txt, "text/plain"
# Ensure the namespace is right, otherwise we might load Grape::API::Helpers
- helpers ::SentryHelper
helpers ::API::Helpers
helpers ::API::Helpers::CommonHelpers
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index 9fda73d5b92..2cceb2ec798 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -368,10 +368,10 @@ module API
end
def handle_api_exception(exception)
- if sentry_enabled? && report_exception?(exception)
+ if report_exception?(exception)
define_params_for_grape_middleware
- sentry_context
- Raven.capture_exception(exception, extra: params)
+ Gitlab::Sentry.context(current_user)
+ Gitlab::Sentry.track_acceptable_exception(exception, extra: params)
end
# lifted from https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb#L60
diff --git a/lib/api/job_artifacts.rb b/lib/api/job_artifacts.rb
index 7c2d8ff11bf..a4068a200b3 100644
--- a/lib/api/job_artifacts.rb
+++ b/lib/api/job_artifacts.rb
@@ -35,6 +35,29 @@ module API
end
# rubocop: enable CodeReuse/ActiveRecord
+ desc 'Download a specific file from artifacts archive from a ref' do
+ detail 'This feature was introduced in GitLab 11.5'
+ end
+ params do
+ requires :ref_name, type: String, desc: 'The ref from repository'
+ requires :job, type: String, desc: 'The name for the job'
+ requires :artifact_path, type: String, desc: 'Artifact path'
+ end
+ get ':id/jobs/artifacts/:ref_name/raw/*artifact_path',
+ format: false,
+ requirements: { ref_name: /.+/ } do
+ authorize_download_artifacts!
+
+ build = user_project.latest_successful_build_for(params[:job], params[:ref_name])
+
+ path = Gitlab::Ci::Build::Artifacts::Path
+ .new(params[:artifact_path])
+
+ bad_request! unless path.valid?
+
+ send_artifacts_entry(build, path)
+ end
+
desc 'Download the artifacts archive from a job' do
detail 'This feature was introduced in GitLab 8.5'
end
@@ -65,6 +88,7 @@ module API
path = Gitlab::Ci::Build::Artifacts::Path
.new(params[:artifact_path])
+
bad_request! unless path.valid?
send_artifacts_entry(build, path)
diff --git a/lib/api/namespaces.rb b/lib/api/namespaces.rb
index 06a57e3cd6f..3cc09f6ac3f 100644
--- a/lib/api/namespaces.rb
+++ b/lib/api/namespaces.rb
@@ -6,20 +6,35 @@ module API
before { authenticate! }
+ helpers do
+ params :optional_list_params_ee do
+ # EE::API::Namespaces would override this helper
+ end
+
+ # EE::API::Namespaces would override this method
+ def custom_namespace_present_options
+ {}
+ end
+ end
+
resource :namespaces do
desc 'Get a namespaces list' do
success Entities::Namespace
end
params do
optional :search, type: String, desc: "Search query for namespaces"
+
use :pagination
+ use :optional_list_params_ee
end
get do
namespaces = current_user.admin ? Namespace.all : current_user.namespaces
namespaces = namespaces.search(params[:search]) if params[:search].present?
- present paginate(namespaces), with: Entities::Namespace, current_user: current_user
+ options = { with: Entities::Namespace, current_user: current_user }
+
+ present paginate(namespaces), options.reverse_merge(custom_namespace_present_options)
end
desc 'Get a namespace by ID' do
diff --git a/lib/api/search.rb b/lib/api/search.rb
index 5900e1cccc2..f5db692afe5 100644
--- a/lib/api/search.rb
+++ b/lib/api/search.rb
@@ -35,12 +35,7 @@ module API
end
def process_results(results)
- case params[:scope]
- when 'blobs', 'wiki_blobs'
- paginate(results).map { |blob| blob[1] }
- else
- paginate(results)
- end
+ paginate(results)
end
def snippets?
diff --git a/lib/api/templates.rb b/lib/api/templates.rb
index 8dab19d50c2..51f357d9477 100644
--- a/lib/api/templates.rb
+++ b/lib/api/templates.rb
@@ -82,7 +82,7 @@ module API
params do
requires :name, type: String, desc: 'The name of the template'
end
- get "templates/#{template_type}/:name" do
+ get "templates/#{template_type}/:name", requirements: { name: /[\w\.-]+/ } do
finder = TemplateFinder.build(template_type, nil, name: declared(params)[:name])
new_template = finder.execute
diff --git a/lib/backup/repository.rb b/lib/backup/repository.rb
index c8a5377bfa0..184c7418e75 100644
--- a/lib/backup/repository.rb
+++ b/lib/backup/repository.rb
@@ -4,6 +4,7 @@ require 'yaml'
module Backup
class Repository
+ include Gitlab::ShellAdapter
attr_reader :progress
def initialize(progress)
@@ -75,7 +76,6 @@ module Backup
def restore
prepare_directories
- gitlab_shell = Gitlab::Shell.new
Project.find_each(batch_size: 1000) do |project|
progress.print " * #{project.full_path} ... "
@@ -118,6 +118,8 @@ module Backup
end
end
end
+
+ restore_object_pools
end
protected
@@ -159,5 +161,17 @@ module Backup
def display_repo_path(project)
project.hashed_storage?(:repository) ? "#{project.full_path} (#{project.disk_path})" : project.full_path
end
+
+ def restore_object_pools
+ PoolRepository.includes(:source_project).find_each do |pool|
+ progress.puts " - Object pool #{pool.disk_path}..."
+
+ pool.source_project ||= pool.member_projects.first.root_of_fork_network
+ pool.state = 'none'
+ pool.save
+
+ pool.schedule
+ end
+ end
end
end
diff --git a/lib/banzai/filter/front_matter_filter.rb b/lib/banzai/filter/front_matter_filter.rb
new file mode 100644
index 00000000000..a27d18facd1
--- /dev/null
+++ b/lib/banzai/filter/front_matter_filter.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+module Banzai
+ module Filter
+ class FrontMatterFilter < HTML::Pipeline::Filter
+ DELIM_LANG = {
+ '---' => 'yaml',
+ '+++' => 'toml',
+ ';;;' => 'json'
+ }.freeze
+
+ DELIM = Regexp.union(DELIM_LANG.keys)
+
+ PATTERN = %r{
+ \A(?:[^\r\n]*coding:[^\r\n]*)? # optional encoding line
+ \s*
+ ^(?<delim>#{DELIM})[ \t]*(?<lang>\S*) # opening front matter marker (optional language specifier)
+ \s*
+ ^(?<front_matter>.*?) # front matter (not greedy)
+ \s*
+ ^\k<delim> # closing front matter marker
+ \s*
+ }mx
+
+ def call
+ html.sub(PATTERN) do |_match|
+ lang = $~[:lang].presence || DELIM_LANG[$~[:delim]]
+
+ ["```#{lang}", $~[:front_matter], "```", "\n"].join("\n")
+ end
+ end
+ end
+ end
+end
diff --git a/lib/banzai/filter/milestone_reference_filter.rb b/lib/banzai/filter/milestone_reference_filter.rb
index 328c8c1803b..c70c3f0c04e 100644
--- a/lib/banzai/filter/milestone_reference_filter.rb
+++ b/lib/banzai/filter/milestone_reference_filter.rb
@@ -4,6 +4,8 @@ module Banzai
module Filter
# HTML filter that replaces milestone references with links.
class MilestoneReferenceFilter < AbstractReferenceFilter
+ include Gitlab::Utils::StrongMemoize
+
self.reference_type = :milestone
def self.object_class
@@ -13,16 +15,34 @@ module Banzai
# Links to project milestones contain the IID, but when we're handling
# 'regular' references, we need to use the global ID to disambiguate
# between group and project milestones.
- def find_object(project, id)
- return unless project.is_a?(Project)
+ def find_object(parent, id)
+ return unless valid_context?(parent)
- find_milestone_with_finder(project, id: id)
+ find_milestone_with_finder(parent, id: id)
end
- def find_object_from_link(project, iid)
- return unless project.is_a?(Project)
+ def find_object_from_link(parent, iid)
+ return unless valid_context?(parent)
+
+ find_milestone_with_finder(parent, iid: iid)
+ end
+
+ def valid_context?(parent)
+ strong_memoize(:valid_context) do
+ group_context?(parent) || project_context?(parent)
+ end
+ end
+
+ def group_context?(parent)
+ strong_memoize(:group_context) do
+ parent.is_a?(Group)
+ end
+ end
- find_milestone_with_finder(project, iid: iid)
+ def project_context?(parent)
+ strong_memoize(:project_context) do
+ parent.is_a?(Project)
+ end
end
def references_in(text, pattern = Milestone.reference_pattern)
@@ -44,13 +64,15 @@ module Banzai
def find_milestone(project_ref, namespace_ref, milestone_id, milestone_name)
project_path = full_project_path(namespace_ref, project_ref)
- project = parent_from_ref(project_path)
- return unless project && project.is_a?(Project)
+ # Returns group if project is not found by path
+ parent = parent_from_ref(project_path)
+
+ return unless parent
milestone_params = milestone_params(milestone_id, milestone_name)
- find_milestone_with_finder(project, milestone_params)
+ find_milestone_with_finder(parent, milestone_params)
end
def milestone_params(iid, name)
@@ -61,16 +83,28 @@ module Banzai
end
end
- def find_milestone_with_finder(project, params)
- finder_params = { project_ids: [project.id], order: nil, state: 'all' }
+ def find_milestone_with_finder(parent, params)
+ finder_params = milestone_finder_params(parent, params[:iid].present?)
+
+ MilestonesFinder.new(finder_params).find_by(params)
+ end
- # We don't support IID lookups for group milestones, because IIDs can
- # clash between group and project milestones.
- if project.group && !params[:iid]
- finder_params[:group_ids] = project.group.self_and_ancestors_ids
+ def milestone_finder_params(parent, find_by_iid)
+ { order: nil, state: 'all' }.tap do |params|
+ params[:project_ids] = parent.id if project_context?(parent)
+
+ # We don't support IID lookups because IIDs can clash between
+ # group/project milestones and group/subgroup milestones.
+ params[:group_ids] = self_and_ancestors_ids(parent) unless find_by_iid
end
+ end
- MilestonesFinder.new(finder_params).find_by(params)
+ def self_and_ancestors_ids(parent)
+ if group_context?(parent)
+ parent.self_and_ancestors_ids
+ elsif project_context?(parent)
+ parent.group&.self_and_ancestors_ids
+ end
end
def url_for_object(milestone, project)
diff --git a/lib/banzai/filter/user_reference_filter.rb b/lib/banzai/filter/user_reference_filter.rb
index 11960047e5b..8cda67867a8 100644
--- a/lib/banzai/filter/user_reference_filter.rb
+++ b/lib/banzai/filter/user_reference_filter.rb
@@ -106,7 +106,7 @@ module Banzai
end
def link_class
- reference_class(:project_member)
+ reference_class(:project_member, tooltip: false)
end
def link_to_all(link_content: nil)
diff --git a/lib/banzai/filter/yaml_front_matter_filter.rb b/lib/banzai/filter/yaml_front_matter_filter.rb
deleted file mode 100644
index 295964dd75d..00000000000
--- a/lib/banzai/filter/yaml_front_matter_filter.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-# frozen_string_literal: true
-
-module Banzai
- module Filter
- class YamlFrontMatterFilter < HTML::Pipeline::Filter
- DELIM = '---'.freeze
-
- # Hat-tip to Middleman: https://git.io/v2e0z
- PATTERN = %r{
- \A(?:[^\r\n]*coding:[^\r\n]*\r?\n)?
- (?<start>#{DELIM})[ ]*\r?\n
- (?<frontmatter>.*?)[ ]*\r?\n?
- ^(?<stop>#{DELIM})[ ]*\r?\n?
- \r?\n?
- (?<content>.*)
- }mx.freeze
-
- def call
- match = PATTERN.match(html)
-
- return html unless match
-
- "```yaml\n#{match['frontmatter']}\n```\n\n#{match['content']}"
- end
- end
- end
-end
diff --git a/lib/banzai/pipeline/pre_process_pipeline.rb b/lib/banzai/pipeline/pre_process_pipeline.rb
index c937f783180..4c2b4ca1665 100644
--- a/lib/banzai/pipeline/pre_process_pipeline.rb
+++ b/lib/banzai/pipeline/pre_process_pipeline.rb
@@ -5,7 +5,7 @@ module Banzai
class PreProcessPipeline < BasePipeline
def self.filters
FilterArray[
- Filter::YamlFrontMatterFilter,
+ Filter::FrontMatterFilter,
Filter::BlockquoteFenceFilter,
]
end
diff --git a/lib/gitlab/background_migration/backfill_hashed_project_repositories.rb b/lib/gitlab/background_migration/backfill_hashed_project_repositories.rb
new file mode 100644
index 00000000000..2f76f2f7434
--- /dev/null
+++ b/lib/gitlab/background_migration/backfill_hashed_project_repositories.rb
@@ -0,0 +1,134 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ # Class that will create fill the project_repositories table
+ # for all projects that are on hashed storage and an entry is
+ # is missing in this table.
+ class BackfillHashedProjectRepositories
+ # Shard model
+ class Shard < ActiveRecord::Base
+ self.table_name = 'shards'
+ end
+
+ # Class that will find or create the shard by name.
+ # There is only a small set of shards, which would
+ # not change quickly, so look them up from memory
+ # instead of hitting the DB each time.
+ class ShardFinder
+ def find_shard_id(name)
+ shard_id = shards.fetch(name, nil)
+ return shard_id if shard_id.present?
+
+ Shard.transaction(requires_new: true) do
+ create!(name)
+ end
+ rescue ActiveRecord::RecordNotUnique
+ reload!
+ retry
+ end
+
+ private
+
+ def create!(name)
+ Shard.create!(name: name).tap { |shard| @shards[name] = shard.id }
+ end
+
+ def shards
+ @shards ||= reload!
+ end
+
+ def reload!
+ @shards = Hash[*Shard.all.map { |shard| [shard.name, shard.id] }.flatten]
+ end
+ end
+
+ # ProjectRegistry model
+ class ProjectRepository < ActiveRecord::Base
+ self.table_name = 'project_repositories'
+
+ belongs_to :project, inverse_of: :project_repository
+ end
+
+ # Project model
+ class Project < ActiveRecord::Base
+ self.table_name = 'projects'
+
+ HASHED_PATH_PREFIX = '@hashed'
+
+ HASHED_STORAGE_FEATURES = {
+ repository: 1,
+ attachments: 2
+ }.freeze
+
+ has_one :project_repository, inverse_of: :project
+
+ class << self
+ def on_hashed_storage
+ where(Project.arel_table[:storage_version]
+ .gteq(HASHED_STORAGE_FEATURES[:repository]))
+ end
+
+ def without_project_repository
+ joins(left_outer_join_project_repository)
+ .where(ProjectRepository.arel_table[:project_id].eq(nil))
+ end
+
+ def left_outer_join_project_repository
+ projects_table = Project.arel_table
+ repository_table = ProjectRepository.arel_table
+
+ projects_table
+ .join(repository_table, Arel::Nodes::OuterJoin)
+ .on(projects_table[:id].eq(repository_table[:project_id]))
+ .join_sources
+ end
+ end
+
+ def hashed_storage?
+ self.storage_version && self.storage_version >= 1
+ end
+
+ def hashed_disk_path
+ "#{HASHED_PATH_PREFIX}/#{disk_hash[0..1]}/#{disk_hash[2..3]}/#{disk_hash}"
+ end
+
+ def disk_hash
+ @disk_hash ||= Digest::SHA2.hexdigest(id.to_s)
+ end
+ end
+
+ def perform(start_id, stop_id)
+ Gitlab::Database.bulk_insert(:project_repositories, project_repositories(start_id, stop_id))
+ end
+
+ private
+
+ def project_repositories(start_id, stop_id)
+ Project.on_hashed_storage
+ .without_project_repository
+ .where(id: start_id..stop_id)
+ .map { |project| build_attributes_for_project(project) }
+ .compact
+ end
+
+ def build_attributes_for_project(project)
+ return unless project.hashed_storage?
+
+ {
+ project_id: project.id,
+ shard_id: find_shard_id(project.repository_storage),
+ disk_path: project.hashed_disk_path
+ }
+ end
+
+ def find_shard_id(repository_storage)
+ shard_finder.find_shard_id(repository_storage)
+ end
+
+ def shard_finder
+ @shard_finder ||= ShardFinder.new
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/populate_merge_request_metrics_with_events_data_improved.rb b/lib/gitlab/background_migration/populate_merge_request_metrics_with_events_data_improved.rb
new file mode 100644
index 00000000000..37592d67dd9
--- /dev/null
+++ b/lib/gitlab/background_migration/populate_merge_request_metrics_with_events_data_improved.rb
@@ -0,0 +1,99 @@
+# frozen_string_literal: true
+# rubocop:disable Style/Documentation
+
+module Gitlab
+ module BackgroundMigration
+ class PopulateMergeRequestMetricsWithEventsDataImproved
+ CLOSED_EVENT_ACTION = 3
+ MERGED_EVENT_ACTION = 7
+
+ def perform(min_merge_request_id, max_merge_request_id)
+ insert_metrics_for_range(min_merge_request_id, max_merge_request_id)
+ update_metrics_with_events_data(min_merge_request_id, max_merge_request_id)
+ end
+
+ # Inserts merge_request_metrics records for merge_requests without it for
+ # a given merge request batch.
+ def insert_metrics_for_range(min, max)
+ metrics_not_exists_clause =
+ <<-SQL.strip_heredoc
+ NOT EXISTS (SELECT 1 FROM merge_request_metrics
+ WHERE merge_request_metrics.merge_request_id = merge_requests.id)
+ SQL
+
+ MergeRequest.where(metrics_not_exists_clause).where(id: min..max).each_batch do |batch|
+ select_sql = batch.select(:id, :created_at, :updated_at).to_sql
+
+ execute("INSERT INTO merge_request_metrics (merge_request_id, created_at, updated_at) #{select_sql}")
+ end
+ end
+
+ def update_metrics_with_events_data(min, max)
+ if Gitlab::Database.postgresql?
+ psql_update_metrics_with_events_data(min, max)
+ else
+ mysql_update_metrics_with_events_data(min, max)
+ end
+ end
+
+ def psql_update_metrics_with_events_data(min, max)
+ update_sql = <<-SQL.strip_heredoc
+ UPDATE merge_request_metrics
+ SET (latest_closed_at,
+ latest_closed_by_id) =
+ ( SELECT updated_at,
+ author_id
+ FROM events
+ WHERE target_id = merge_request_id
+ AND target_type = 'MergeRequest'
+ AND action = #{CLOSED_EVENT_ACTION}
+ ORDER BY id DESC
+ LIMIT 1 ),
+ merged_by_id =
+ ( SELECT author_id
+ FROM events
+ WHERE target_id = merge_request_id
+ AND target_type = 'MergeRequest'
+ AND action = #{MERGED_EVENT_ACTION}
+ ORDER BY id DESC
+ LIMIT 1 )
+ WHERE merge_request_id BETWEEN #{min} AND #{max}
+ SQL
+
+ execute(update_sql)
+ end
+
+ def mysql_update_metrics_with_events_data(min, max)
+ closed_updated_at_subquery = mysql_events_select(:updated_at, CLOSED_EVENT_ACTION)
+ closed_author_id_subquery = mysql_events_select(:author_id, CLOSED_EVENT_ACTION)
+ merged_author_id_subquery = mysql_events_select(:author_id, MERGED_EVENT_ACTION)
+
+ update_sql = <<-SQL.strip_heredoc
+ UPDATE merge_request_metrics
+ SET latest_closed_at = (#{closed_updated_at_subquery}),
+ latest_closed_by_id = (#{closed_author_id_subquery}),
+ merged_by_id = (#{merged_author_id_subquery})
+ WHERE merge_request_id BETWEEN #{min} AND #{max}
+ SQL
+
+ execute(update_sql)
+ end
+
+ def mysql_events_select(column, action)
+ <<-SQL.strip_heredoc
+ SELECT #{column} FROM events
+ WHERE target_id = merge_request_id
+ AND target_type = 'MergeRequest'
+ AND action = #{action}
+ ORDER BY id DESC
+ LIMIT 1
+ SQL
+ end
+
+ def execute(sql)
+ @connection ||= ActiveRecord::Base.connection
+ @connection.execute(sql)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/bitbucket_server_import/importer.rb b/lib/gitlab/bitbucket_server_import/importer.rb
index d4080536d81..28cfb46e2d4 100644
--- a/lib/gitlab/bitbucket_server_import/importer.rb
+++ b/lib/gitlab/bitbucket_server_import/importer.rb
@@ -3,8 +3,6 @@
module Gitlab
module BitbucketServerImport
class Importer
- include Gitlab::ShellAdapter
-
attr_reader :recover_missing_commits
attr_reader :project, :project_key, :repository_slug, :client, :errors, :users
attr_accessor :logger
diff --git a/lib/gitlab/branch_push_merge_commit_analyzer.rb b/lib/gitlab/branch_push_merge_commit_analyzer.rb
new file mode 100644
index 00000000000..a8f601f2451
--- /dev/null
+++ b/lib/gitlab/branch_push_merge_commit_analyzer.rb
@@ -0,0 +1,132 @@
+# frozen_string_literal: true
+
+module Gitlab
+ # Analyse a graph of commits from a push to a branch,
+ # for each commit, analyze that if it is the head of a merge request,
+ # then what should its merge_commit be, relative to the branch.
+ #
+ # A----->B----->C----->D target branch
+ # | ^
+ # | |
+ # +-->E----->F--+ merged branch
+ # | ^
+ # | |
+ # +->G--+
+ #
+ # (See merge-commit-analyze-after branch in gitlab-test)
+ #
+ # Assuming
+ # - A is already in remote
+ # - B~D are all in its own branch with its own merge request, targeting the target branch
+ #
+ # When D is finally pushed to the target branch,
+ # what are the merge commits for all the other merge requests?
+ #
+ # We can walk backwards from the HEAD commit D,
+ # and find status of its parents.
+ # First we determine if commit belongs to the target branch (i.e. A, B, C, D),
+ # and then determine its merge commit.
+ #
+ # +--------+-----------------+--------------+
+ # | Commit | Direct ancestor | Merge commit |
+ # +--------+-----------------+--------------+
+ # | D | Y | D |
+ # +--------+-----------------+--------------+
+ # | C | Y | C |
+ # +--------+-----------------+--------------+
+ # | F | | C |
+ # +--------+-----------------+--------------+
+ # | B | Y | B |
+ # +--------+-----------------+--------------+
+ # | E | | C |
+ # +--------+-----------------+--------------+
+ # | G | | C |
+ # +--------+-----------------+--------------+
+ #
+ # By examining the result, it can be said that
+ #
+ # - If commit is direct ancestor of HEAD, its merge commit is itself.
+ # - Otherwise, the merge commit is the same as its child's merge commit.
+ #
+ class BranchPushMergeCommitAnalyzer
+ class CommitDecorator < SimpleDelegator
+ attr_accessor :merge_commit
+ attr_writer :direct_ancestor # boolean
+
+ def direct_ancestor?
+ @direct_ancestor
+ end
+
+ # @param child_commit [CommitDecorator]
+ # @param first_parent [Boolean] whether `self` is the first parent of `child_commit`
+ def set_merge_commit(child_commit:)
+ @merge_commit ||= direct_ancestor? ? self : child_commit.merge_commit
+ end
+ end
+
+ # @param commits [Array] list of commits, must be ordered from the child (tip) of the graph back to the ancestors
+ def initialize(commits, relevant_commit_ids: nil)
+ @commits = commits
+ @id_to_commit = {}
+ @commits.each do |commit|
+ @id_to_commit[commit.id] = CommitDecorator.new(commit)
+
+ if relevant_commit_ids
+ relevant_commit_ids.delete(commit.id)
+ break if relevant_commit_ids.empty? # Only limit the analyze up to relevant_commit_ids
+ end
+ end
+
+ analyze
+ end
+
+ def get_merge_commit(id)
+ get_commit(id).merge_commit.id
+ end
+
+ private
+
+ def analyze
+ head_commit = get_commit(@commits.first.id)
+ head_commit.direct_ancestor = true
+ head_commit.merge_commit = head_commit
+
+ mark_all_direct_ancestors(head_commit)
+
+ # Analyzing a commit requires its child commit be analyzed first,
+ # which is the case here since commits are ordered from child to parent.
+ @id_to_commit.each_value do |commit|
+ analyze_parents(commit)
+ end
+ end
+
+ def analyze_parents(commit)
+ commit.parent_ids.each do |parent_commit_id|
+ parent_commit = get_commit(parent_commit_id)
+
+ next unless parent_commit # parent commit may not be part of new commits
+
+ parent_commit.set_merge_commit(child_commit: commit)
+ end
+ end
+
+ # Mark all direct ancestors.
+ # If child commit is a direct ancestor, its first parent is also a direct ancestor.
+ # We assume direct ancestors matches the trail of the target branch over time,
+ # This assumption is correct most of the time, especially for gitlab managed merges,
+ # but there are exception cases which can't be solved (https://stackoverflow.com/a/49754723/474597)
+ def mark_all_direct_ancestors(commit)
+ loop do
+ commit = get_commit(commit.parent_ids.first)
+
+ break unless commit
+
+ commit.direct_ancestor = true
+ end
+ end
+
+ def get_commit(id)
+ @id_to_commit[id]
+ end
+ end
+end
diff --git a/lib/gitlab/checks/diff_check.rb b/lib/gitlab/checks/diff_check.rb
index 49d361fcef7..8ee345ab45a 100644
--- a/lib/gitlab/checks/diff_check.rb
+++ b/lib/gitlab/checks/diff_check.rb
@@ -11,6 +11,7 @@ module Gitlab
}.freeze
def validate!
+ return if deletion? || newrev.nil?
return unless should_run_diff_validations?
return if commits.empty?
return unless uses_raw_delta_validations?
@@ -28,7 +29,7 @@ module Gitlab
private
def should_run_diff_validations?
- newrev && oldrev && !deletion? && validate_lfs_file_locks?
+ validate_lfs_file_locks?
end
def validate_lfs_file_locks?
diff --git a/lib/gitlab/ci/build/policy/refs.rb b/lib/gitlab/ci/build/policy/refs.rb
index 10934536536..0e9bb5c94bb 100644
--- a/lib/gitlab/ci/build/policy/refs.rb
+++ b/lib/gitlab/ci/build/policy/refs.rb
@@ -32,10 +32,14 @@ module Gitlab
return true if pipeline.source == pattern
return true if pipeline.source&.pluralize == pattern
- if pattern.first == "/" && pattern.last == "/"
- Regexp.new(pattern[1...-1]) =~ pipeline.ref
- else
- pattern == pipeline.ref
+ # patterns can be matched only when branch or tag is used
+ # the pattern matching does not work for merge requests pipelines
+ if pipeline.branch? || pipeline.tag?
+ if pattern.first == "/" && pattern.last == "/"
+ Regexp.new(pattern[1...-1]) =~ pipeline.ref
+ else
+ pattern == pipeline.ref
+ end
end
end
end
diff --git a/lib/gitlab/ci/config/entry/except_policy.rb b/lib/gitlab/ci/config/entry/except_policy.rb
deleted file mode 100644
index 46ded35325d..00000000000
--- a/lib/gitlab/ci/config/entry/except_policy.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Ci
- class Config
- module Entry
- ##
- # Entry that represents an only/except trigger policy for the job.
- #
- class ExceptPolicy < Policy
- def self.default
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/ci/config/entry/job.rb b/lib/gitlab/ci/config/entry/job.rb
index 085be5da08d..50942fbdb40 100644
--- a/lib/gitlab/ci/config/entry/job.rb
+++ b/lib/gitlab/ci/config/entry/job.rb
@@ -16,6 +16,13 @@ module Gitlab
dependencies before_script after_script variables
environment coverage retry parallel extends].freeze
+ DEFAULT_ONLY_POLICY = {
+ refs: %w(branches tags)
+ }.freeze
+
+ DEFAULT_EXCEPT_POLICY = {
+ }.freeze
+
validations do
validates :config, allowed_keys: ALLOWED_KEYS
validates :config, presence: true
@@ -65,10 +72,10 @@ module Gitlab
entry :services, Entry::Services,
description: 'Services that will be used to execute this job.'
- entry :only, Entry::OnlyPolicy,
+ entry :only, Entry::Policy,
description: 'Refs policy this job will be executed for.'
- entry :except, Entry::ExceptPolicy,
+ entry :except, Entry::Policy,
description: 'Refs policy this job will be executed for.'
entry :variables, Entry::Variables,
@@ -154,8 +161,8 @@ module Gitlab
services: services_value,
stage: stage_value,
cache: cache_value,
- only: only_value,
- except: except_value,
+ only: DEFAULT_ONLY_POLICY.deep_merge(only_value.to_h),
+ except: DEFAULT_EXCEPT_POLICY.deep_merge(except_value.to_h),
variables: variables_defined? ? variables_value : nil,
environment: environment_defined? ? environment_value : nil,
environment_name: environment_defined? ? environment_value[:name] : nil,
diff --git a/lib/gitlab/ci/config/entry/only_policy.rb b/lib/gitlab/ci/config/entry/only_policy.rb
deleted file mode 100644
index 9a581b8e97e..00000000000
--- a/lib/gitlab/ci/config/entry/only_policy.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Ci
- class Config
- module Entry
- ##
- # Entry that represents an only/except trigger policy for the job.
- #
- class OnlyPolicy < Policy
- def self.default
- { refs: %w[branches tags] }
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/ci/config/entry/policy.rb b/lib/gitlab/ci/config/entry/policy.rb
index 81e74a639fc..998da1f6837 100644
--- a/lib/gitlab/ci/config/entry/policy.rb
+++ b/lib/gitlab/ci/config/entry/policy.rb
@@ -5,9 +5,12 @@ module Gitlab
class Config
module Entry
##
- # Base class for OnlyPolicy and ExceptPolicy
+ # Entry that represents an only/except trigger policy for the job.
#
class Policy < ::Gitlab::Config::Entry::Simplifiable
+ strategy :RefsPolicy, if: -> (config) { config.is_a?(Array) }
+ strategy :ComplexPolicy, if: -> (config) { config.is_a?(Hash) }
+
class RefsPolicy < ::Gitlab::Config::Entry::Node
include ::Gitlab::Config::Entry::Validatable
@@ -63,16 +66,6 @@ module Gitlab
def self.default
end
-
- ##
- # Class-level execution won't be inherited by subclasses by default.
- # Therefore, we need to explicitly execute that for OnlyPolicy and ExceptPolicy
- def self.inherited(klass)
- super
-
- klass.strategy :RefsPolicy, if: -> (config) { config.is_a?(Array) }
- klass.strategy :ComplexPolicy, if: -> (config) { config.is_a?(Hash) }
- end
end
end
end
diff --git a/lib/gitlab/ci/parsers.rb b/lib/gitlab/ci/parsers.rb
new file mode 100644
index 00000000000..eb63e6c8363
--- /dev/null
+++ b/lib/gitlab/ci/parsers.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Parsers
+ ParserNotFoundError = Class.new(ParserError)
+
+ def self.parsers
+ {
+ junit: ::Gitlab::Ci::Parsers::Test::Junit
+ }
+ end
+
+ def self.fabricate!(file_type)
+ parsers.fetch(file_type.to_sym).new
+ rescue KeyError
+ raise ParserNotFoundError, "Cannot find any parser matching file type '#{file_type}'"
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/parsers/parser_error.rb b/lib/gitlab/ci/parsers/parser_error.rb
new file mode 100644
index 00000000000..ef327737cdb
--- /dev/null
+++ b/lib/gitlab/ci/parsers/parser_error.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Parsers
+ ParserError = Class.new(StandardError)
+ end
+ end
+end
diff --git a/lib/gitlab/ci/parsers/test.rb b/lib/gitlab/ci/parsers/test.rb
deleted file mode 100644
index c6bc9662b07..00000000000
--- a/lib/gitlab/ci/parsers/test.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Ci
- module Parsers
- module Test
- ParserNotFoundError = Class.new(StandardError)
-
- PARSERS = {
- junit: ::Gitlab::Ci::Parsers::Test::Junit
- }.freeze
-
- def self.fabricate!(file_type)
- PARSERS.fetch(file_type.to_sym).new
- rescue KeyError
- raise ParserNotFoundError, "Cannot find any parser matching file type '#{file_type}'"
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/ci/parsers/test/junit.rb b/lib/gitlab/ci/parsers/test/junit.rb
index 2791730fd26..dca60eabc1c 100644
--- a/lib/gitlab/ci/parsers/test/junit.rb
+++ b/lib/gitlab/ci/parsers/test/junit.rb
@@ -5,7 +5,7 @@ module Gitlab
module Parsers
module Test
class Junit
- JunitParserError = Class.new(StandardError)
+ JunitParserError = Class.new(Gitlab::Ci::Parsers::ParserError)
def parse!(xml_data, test_suite)
root = Hash.from_xml(xml_data)
diff --git a/lib/gitlab/ci/status/bridge/common.rb b/lib/gitlab/ci/status/bridge/common.rb
new file mode 100644
index 00000000000..c6cb620f7a0
--- /dev/null
+++ b/lib/gitlab/ci/status/bridge/common.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Status
+ module Bridge
+ module Common
+ def label
+ subject.description
+ end
+
+ def has_details?
+ false
+ end
+
+ def has_action?
+ false
+ end
+
+ def details_path
+ raise NotImplementedError
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/status/bridge/factory.rb b/lib/gitlab/ci/status/bridge/factory.rb
new file mode 100644
index 00000000000..910de865483
--- /dev/null
+++ b/lib/gitlab/ci/status/bridge/factory.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Status
+ module Bridge
+ class Factory < Status::Factory
+ def self.common_helpers
+ Status::Bridge::Common
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml
index 3b2cae07c12..d0613aa59e1 100644
--- a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml
@@ -164,7 +164,8 @@ sast:
- setup_docker
- sast
artifacts:
- paths: [gl-sast-report.json]
+ reports:
+ sast: gl-sast-report.json
only:
refs:
- branches
diff --git a/lib/gitlab/correlation_id.rb b/lib/gitlab/correlation_id.rb
new file mode 100644
index 00000000000..0f9bde4390e
--- /dev/null
+++ b/lib/gitlab/correlation_id.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module CorrelationId
+ LOG_KEY = 'correlation_id'.freeze
+
+ class << self
+ def use_id(correlation_id, &blk)
+ # always generate a id if null is passed
+ correlation_id ||= new_id
+
+ ids.push(correlation_id || new_id)
+
+ begin
+ yield(current_id)
+ ensure
+ ids.pop
+ end
+ end
+
+ def current_id
+ ids.last
+ end
+
+ def current_or_new_id
+ current_id || new_id
+ end
+
+ private
+
+ def ids
+ Thread.current[:correlation_id] ||= []
+ end
+
+ def new_id
+ SecureRandom.uuid
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/count.rb b/lib/gitlab/database/count.rb
index c996d786909..f3d37ccd72a 100644
--- a/lib/gitlab/database/count.rb
+++ b/lib/gitlab/database/count.rb
@@ -40,7 +40,7 @@ module Gitlab
if strategy.enabled?
models_with_missing_counts = models - counts_by_model.keys
- break if models_with_missing_counts.empty?
+ break counts_by_model if models_with_missing_counts.empty?
counts = strategy.new(models_with_missing_counts).count
diff --git a/lib/gitlab/database/count/exact_count_strategy.rb b/lib/gitlab/database/count/exact_count_strategy.rb
index 0276fe2b54f..fa6951eda22 100644
--- a/lib/gitlab/database/count/exact_count_strategy.rb
+++ b/lib/gitlab/database/count/exact_count_strategy.rb
@@ -20,6 +20,8 @@ module Gitlab
models.each_with_object({}) do |model, data|
data[model] = model.count
end
+ rescue *CONNECTION_ERRORS
+ {}
end
def self.enabled?
diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb
index 134d1e7a724..d9578852db6 100644
--- a/lib/gitlab/database/migration_helpers.rb
+++ b/lib/gitlab/database/migration_helpers.rb
@@ -975,9 +975,10 @@ into similar problems in the future (e.g. when new tables are created).
raise "#{model_class} does not have an ID to use for batch ranges" unless model_class.column_names.include?('id')
jobs = []
+ table_name = model_class.quoted_table_name
model_class.each_batch(of: batch_size) do |relation|
- start_id, end_id = relation.pluck('MIN(id), MAX(id)').first
+ start_id, end_id = relation.pluck("MIN(#{table_name}.id), MAX(#{table_name}.id)").first
if jobs.length >= BACKGROUND_MIGRATION_JOB_BUFFER_SIZE
# Note: This code path generally only helps with many millions of rows
diff --git a/lib/gitlab/file_finder.rb b/lib/gitlab/file_finder.rb
index b4db3f93c9c..3958814208c 100644
--- a/lib/gitlab/file_finder.rb
+++ b/lib/gitlab/file_finder.rb
@@ -4,8 +4,6 @@
# the result is joined and sorted by file name
module Gitlab
class FileFinder
- BATCH_SIZE = 100
-
attr_reader :project, :ref
delegate :repository, to: :project
@@ -16,60 +14,35 @@ module Gitlab
end
def find(query)
- query = Gitlab::Search::Query.new(query) do
- filter :filename, matcher: ->(filter, blob) { blob.filename =~ /#{filter[:regex_value]}$/i }
- filter :path, matcher: ->(filter, blob) { blob.filename =~ /#{filter[:regex_value]}/i }
- filter :extension, matcher: ->(filter, blob) { blob.filename =~ /\.#{filter[:regex_value]}$/i }
+ query = Gitlab::Search::Query.new(query, encode_binary: true) do
+ filter :filename, matcher: ->(filter, blob) { blob.binary_filename =~ /#{filter[:regex_value]}$/i }
+ filter :path, matcher: ->(filter, blob) { blob.binary_filename =~ /#{filter[:regex_value]}/i }
+ filter :extension, matcher: ->(filter, blob) { blob.binary_filename =~ /\.#{filter[:regex_value]}$/i }
end
- by_content = find_by_content(query.term)
-
- already_found = Set.new(by_content.map(&:filename))
- by_filename = find_by_filename(query.term, except: already_found)
+ files = find_by_filename(query.term) + find_by_content(query.term)
- files = (by_content + by_filename)
- .sort_by(&:filename)
+ files = query.filter_results(files) if query.filters.any?
- query.filter_results(files).map { |blob| [blob.filename, blob] }
+ files
end
private
def find_by_content(query)
- results = repository.search_files_by_content(query, ref).first(BATCH_SIZE)
- results.map { |result| Gitlab::ProjectSearchResults.parse_search_result(result, project) }
- end
-
- def find_by_filename(query, except: [])
- filenames = search_filenames(query, except)
-
- blobs(filenames).map do |blob|
- Gitlab::SearchResults::FoundBlob.new(
- id: blob.id,
- filename: blob.path,
- basename: File.basename(blob.path, File.extname(blob.path)),
- ref: ref,
- startline: 1,
- data: blob.data,
- project: project
- )
+ repository.search_files_by_content(query, ref).map do |result|
+ Gitlab::Search::FoundBlob.new(content_match: result, project: project, ref: ref, repository: repository)
end
end
- def search_filenames(query, except)
- filenames = repository.search_files_by_name(query, ref).first(BATCH_SIZE)
-
- filenames.delete_if { |filename| except.include?(filename) } unless except.empty?
-
- filenames
- end
-
- def blob_refs(filenames)
- filenames.map { |filename| [ref, filename] }
+ def find_by_filename(query)
+ search_filenames(query).map do |filename|
+ Gitlab::Search::FoundBlob.new(blob_filename: filename, project: project, ref: ref, repository: repository)
+ end
end
- def blobs(filenames)
- Gitlab::Git::Blob.batch(repository, blob_refs(filenames), blob_size_limit: 1024)
+ def search_filenames(query)
+ repository.search_files_by_name(query, ref)
end
end
end
diff --git a/lib/gitlab/git/object_pool.rb b/lib/gitlab/git/object_pool.rb
new file mode 100644
index 00000000000..558699a6318
--- /dev/null
+++ b/lib/gitlab/git/object_pool.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Git
+ class ObjectPool
+ # GL_REPOSITORY has to be passed for Gitlab::Git::Repositories, but not
+ # used for ObjectPools.
+ GL_REPOSITORY = ""
+
+ delegate :exists?, :size, to: :repository
+ delegate :delete, to: :object_pool_service
+
+ attr_reader :storage, :relative_path, :source_repository
+
+ def initialize(storage, relative_path, source_repository)
+ @storage = storage
+ @relative_path = relative_path
+ @source_repository = source_repository
+ end
+
+ def create
+ object_pool_service.create(source_repository)
+ end
+
+ def link(to_link_repo)
+ remote_name = to_link_repo.object_pool_remote_name
+ repository.set_config(
+ "remote.#{remote_name}.url" => relative_path_to(to_link_repo.relative_path),
+ "remote.#{remote_name}.tagOpt" => "--no-tags",
+ "remote.#{remote_name}.fetch" => "+refs/*:refs/remotes/#{remote_name}/*"
+ )
+
+ object_pool_service.link_repository(to_link_repo)
+ end
+
+ def gitaly_object_pool
+ Gitaly::ObjectPool.new(repository: to_gitaly_repository)
+ end
+
+ def to_gitaly_repository
+ Gitlab::GitalyClient::Util.repository(storage, relative_path, GL_REPOSITORY)
+ end
+
+ # Allows for reusing other RPCs by 'tricking' Gitaly to think its a repository
+ def repository
+ @repository ||= Gitlab::Git::Repository.new(storage, relative_path, GL_REPOSITORY)
+ end
+
+ private
+
+ def object_pool_service
+ @object_pool_service ||= Gitlab::GitalyClient::ObjectPoolService.new(self)
+ end
+
+ def relative_path_to(pool_member_path)
+ pool_path = Pathname.new("#{relative_path}#{File::SEPARATOR}")
+
+ Pathname.new(pool_member_path).relative_path_from(pool_path).to_s
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index 0a541031884..5bbedc9d5e3 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -69,6 +69,13 @@ module Gitlab
attr_reader :storage, :gl_repository, :relative_path
+ # This remote name has to be stable for all types of repositories that
+ # can join an object pool. If it's structure ever changes, a migration
+ # has to be performed on the object pools to update the remote names.
+ # Else the pool can't be updated anymore and is left in an inconsistent
+ # state.
+ alias_method :object_pool_remote_name, :gl_repository
+
# This initializer method is only used on the client side (gitlab-ce).
# Gitaly-ruby uses a different initializer.
def initialize(storage, relative_path, gl_repository)
diff --git a/lib/gitlab/git/repository_cleaner.rb b/lib/gitlab/git/repository_cleaner.rb
new file mode 100644
index 00000000000..2d1d8435cf3
--- /dev/null
+++ b/lib/gitlab/git/repository_cleaner.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Git
+ class RepositoryCleaner
+ include Gitlab::Git::WrapsGitalyErrors
+
+ attr_reader :repository
+
+ # 'repository' is a Gitlab::Git::Repository
+ def initialize(repository)
+ @repository = repository
+ end
+
+ def apply_bfg_object_map(io)
+ wrapped_gitaly_errors do
+ gitaly_cleanup_client.apply_bfg_object_map(io)
+ end
+ end
+
+ private
+
+ def gitaly_cleanup_client
+ @gitaly_cleanup_client ||= Gitlab::GitalyClient::CleanupService.new(repository)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/gitaly_client.rb b/lib/gitlab/gitaly_client.rb
index 9be553a8b86..11021ee06b3 100644
--- a/lib/gitlab/gitaly_client.rb
+++ b/lib/gitlab/gitaly_client.rb
@@ -193,6 +193,7 @@ module Gitlab
feature = feature_stack && feature_stack[0]
metadata['call_site'] = feature.to_s if feature
metadata['gitaly-servers'] = address_metadata(remote_storage) if remote_storage
+ metadata['x-gitlab-correlation-id'] = Gitlab::CorrelationId.current_id if Gitlab::CorrelationId.current_id
metadata.merge!(server_feature_flags)
diff --git a/lib/gitlab/gitaly_client/cleanup_service.rb b/lib/gitlab/gitaly_client/cleanup_service.rb
new file mode 100644
index 00000000000..8e412a9b3ef
--- /dev/null
+++ b/lib/gitlab/gitaly_client/cleanup_service.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GitalyClient
+ class CleanupService
+ attr_reader :repository, :gitaly_repo, :storage
+
+ # 'repository' is a Gitlab::Git::Repository
+ def initialize(repository)
+ @repository = repository
+ @gitaly_repo = repository.gitaly_repository
+ @storage = repository.storage
+ end
+
+ def apply_bfg_object_map(io)
+ first_request = Gitaly::ApplyBfgObjectMapRequest.new(repository: gitaly_repo)
+
+ enum = Enumerator.new do |y|
+ y.yield first_request
+
+ while data = io.read(RepositoryService::MAX_MSG_SIZE)
+ y.yield Gitaly::ApplyBfgObjectMapRequest.new(object_map: data)
+ end
+ end
+
+ GitalyClient.call(
+ storage,
+ :cleanup_service,
+ :apply_bfg_object_map,
+ enum,
+ timeout: GitalyClient.no_timeout
+ )
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/gitaly_client/object_pool_service.rb b/lib/gitlab/gitaly_client/object_pool_service.rb
new file mode 100644
index 00000000000..272ce73ad64
--- /dev/null
+++ b/lib/gitlab/gitaly_client/object_pool_service.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GitalyClient
+ class ObjectPoolService
+ attr_reader :object_pool, :storage
+
+ def initialize(object_pool)
+ @object_pool = object_pool.gitaly_object_pool
+ @storage = object_pool.storage
+ end
+
+ def create(repository)
+ request = Gitaly::CreateObjectPoolRequest.new(
+ object_pool: object_pool,
+ origin: repository.gitaly_repository)
+
+ GitalyClient.call(storage, :object_pool_service, :create_object_pool, request)
+ end
+
+ def delete
+ request = Gitaly::DeleteObjectPoolRequest.new(object_pool: object_pool)
+
+ GitalyClient.call(storage, :object_pool_service, :delete_object_pool, request)
+ end
+
+ def link_repository(repository)
+ request = Gitaly::LinkRepositoryToObjectPoolRequest.new(
+ object_pool: object_pool,
+ repository: repository.gitaly_repository
+ )
+
+ GitalyClient.call(storage, :object_pool_service, :link_repository_to_object_pool,
+ request, timeout: GitalyClient.fast_timeout)
+ end
+
+ def unlink_repository(repository)
+ request = Gitaly::UnlinkRepositoryFromObjectPoolRequest.new(repository: repository.gitaly_repository)
+
+ GitalyClient.call(storage, :object_pool_service, :unlink_repository_from_object_pool,
+ request, timeout: GitalyClient.fast_timeout)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/gpg/commit.rb b/lib/gitlab/gpg/commit.rb
index 31bab20b044..4fbb87385c3 100644
--- a/lib/gitlab/gpg/commit.rb
+++ b/lib/gitlab/gpg/commit.rb
@@ -44,9 +44,8 @@ module Gitlab
def update_signature!(cached_signature)
using_keychain do |gpg_key|
cached_signature.update!(attributes(gpg_key))
+ @signature = cached_signature
end
-
- @signature = cached_signature
end
private
@@ -59,11 +58,15 @@ module Gitlab
# the proper signature.
# NOTE: the invoked method is #fingerprint but it's only returning
# 16 characters (the format used by keyid) instead of 40.
- gpg_key = find_gpg_key(verified_signature.fingerprint)
+ fingerprint = verified_signature&.fingerprint
+
+ break unless fingerprint
+
+ gpg_key = find_gpg_key(fingerprint)
if gpg_key
Gitlab::Gpg::CurrentKeyChain.add(gpg_key.key)
- @verified_signature = nil
+ clear_memoization(:verified_signature)
end
yield gpg_key
@@ -71,9 +74,16 @@ module Gitlab
end
def verified_signature
- @verified_signature ||= GPGME::Crypto.new.verify(signature_text, signed_text: signed_text) do |verified_signature|
+ strong_memoize(:verified_signature) { gpgme_signature }
+ end
+
+ def gpgme_signature
+ GPGME::Crypto.new.verify(signature_text, signed_text: signed_text) do |verified_signature|
+ # Return the first signature for now: https://gitlab.com/gitlab-org/gitlab-ce/issues/54932
break verified_signature
end
+ rescue GPGME::Error
+ nil
end
def create_cached_signature!
@@ -92,7 +102,7 @@ module Gitlab
commit_sha: @commit.sha,
project: @commit.project,
gpg_key: gpg_key,
- gpg_key_primary_keyid: gpg_key&.keyid || verified_signature.fingerprint,
+ gpg_key_primary_keyid: gpg_key&.keyid || verified_signature&.fingerprint,
gpg_key_user_name: user_infos[:name],
gpg_key_user_email: user_infos[:email],
verification_status: verification_status
@@ -102,7 +112,7 @@ module Gitlab
def verification_status(gpg_key)
return :unknown_key unless gpg_key
return :unverified_key unless gpg_key.verified?
- return :unverified unless verified_signature.valid?
+ return :unverified unless verified_signature&.valid?
if gpg_key.verified_and_belongs_to_email?(@commit.committer_email)
:verified
diff --git a/lib/gitlab/grape_logging/loggers/correlation_id_logger.rb b/lib/gitlab/grape_logging/loggers/correlation_id_logger.rb
new file mode 100644
index 00000000000..fa4c5d86d44
--- /dev/null
+++ b/lib/gitlab/grape_logging/loggers/correlation_id_logger.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+# This module adds additional correlation id the grape logger
+module Gitlab
+ module GrapeLogging
+ module Loggers
+ class CorrelationIdLogger < ::GrapeLogging::Loggers::Base
+ def parameters(_, _)
+ { Gitlab::CorrelationId::LOG_KEY => Gitlab::CorrelationId.current_id }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml
index 93065879ec6..d10d4f2f746 100644
--- a/lib/gitlab/import_export/import_export.yml
+++ b/lib/gitlab/import_export/import_export.yml
@@ -115,6 +115,7 @@ excluded_attributes:
- :remote_mirror_available_overridden
- :description_html
- :repository_languages
+ - :bfg_object_map
namespaces:
- :runners_token
- :runners_token_encrypted
@@ -142,6 +143,7 @@ excluded_attributes:
statuses:
- :trace
- :token
+ - :token_encrypted
- :when
- :artifacts_file
- :artifacts_metadata
diff --git a/lib/gitlab/import_export/repo_restorer.rb b/lib/gitlab/import_export/repo_restorer.rb
index 921a06b4023..91167a9c4fb 100644
--- a/lib/gitlab/import_export/repo_restorer.rb
+++ b/lib/gitlab/import_export/repo_restorer.rb
@@ -4,7 +4,6 @@ module Gitlab
module ImportExport
class RepoRestorer
include Gitlab::ImportExport::CommandLineUtil
- include Gitlab::ShellAdapter
def initialize(project:, shared:, path_to_bundle:)
@project = project
diff --git a/lib/gitlab/import_sources.rb b/lib/gitlab/import_sources.rb
index f46bb837cf7..67952ca0f2d 100644
--- a/lib/gitlab/import_sources.rb
+++ b/lib/gitlab/import_sources.rb
@@ -10,7 +10,7 @@ module Gitlab
ImportSource = Struct.new(:name, :title, :importer)
# We exclude `bare_repository` here as it has no import class associated
- ImportTable = [
+ IMPORT_TABLE = [
ImportSource.new('github', 'GitHub', Gitlab::GithubImport::ParallelImporter),
ImportSource.new('bitbucket', 'Bitbucket Cloud', Gitlab::BitbucketImport::Importer),
ImportSource.new('bitbucket_server', 'Bitbucket Server', Gitlab::BitbucketServerImport::Importer),
@@ -45,7 +45,7 @@ module Gitlab
end
def import_table
- ImportTable
+ IMPORT_TABLE
end
end
end
diff --git a/lib/gitlab/json_logger.rb b/lib/gitlab/json_logger.rb
index 3bff77731f6..a5a5759cc89 100644
--- a/lib/gitlab/json_logger.rb
+++ b/lib/gitlab/json_logger.rb
@@ -10,6 +10,7 @@ module Gitlab
data = {}
data[:severity] = severity
data[:time] = timestamp.utc.iso8601(3)
+ data[Gitlab::CorrelationId::LOG_KEY] = Gitlab::CorrelationId.current_id
case message
when String
diff --git a/lib/gitlab/middleware/correlation_id.rb b/lib/gitlab/middleware/correlation_id.rb
new file mode 100644
index 00000000000..73542dd422e
--- /dev/null
+++ b/lib/gitlab/middleware/correlation_id.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+# A dumb middleware that steals correlation id
+# and sets it as a global context for the request
+module Gitlab
+ module Middleware
+ class CorrelationId
+ include ActionView::Helpers::TagHelper
+
+ def initialize(app)
+ @app = app
+ end
+
+ def call(env)
+ ::Gitlab::CorrelationId.use_id(correlation_id(env)) do
+ @app.call(env)
+ end
+ end
+
+ private
+
+ def correlation_id(env)
+ if Gitlab.rails5?
+ request(env).request_id
+ else
+ request(env).uuid
+ end
+ end
+
+ def request(env)
+ ActionDispatch::Request.new(env)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/project_search_results.rb b/lib/gitlab/project_search_results.rb
index 04df881bf03..a68f8801c2a 100644
--- a/lib/gitlab/project_search_results.rb
+++ b/lib/gitlab/project_search_results.rb
@@ -17,9 +17,9 @@ module Gitlab
when 'notes'
notes.page(page).per(per_page)
when 'blobs'
- Kaminari.paginate_array(blobs).page(page).per(per_page)
+ paginated_blobs(blobs, page)
when 'wiki_blobs'
- Kaminari.paginate_array(wiki_blobs).page(page).per(per_page)
+ paginated_blobs(wiki_blobs, page)
when 'commits'
Kaminari.paginate_array(commits).page(page).per(per_page)
else
@@ -55,37 +55,6 @@ module Gitlab
@commits_count ||= commits.count
end
- def self.parse_search_result(result, project = nil)
- ref = nil
- filename = nil
- basename = nil
-
- data = []
- startline = 0
-
- result.each_line.each_with_index do |line, index|
- prefix ||= line.match(/^(?<ref>[^:]*):(?<filename>[^\x00]*)\x00(?<startline>\d+)\x00/)&.tap do |matches|
- ref = matches[:ref]
- filename = matches[:filename]
- startline = matches[:startline]
- startline = startline.to_i - index
- extname = Regexp.escape(File.extname(filename))
- basename = filename.sub(/#{extname}$/, '')
- end
-
- data << line.sub(prefix.to_s, '')
- end
-
- FoundBlob.new(
- filename: filename,
- basename: basename,
- ref: ref,
- startline: startline,
- data: data.join,
- project: project
- )
- end
-
def single_commit_result?
return false if commits_count != 1
@@ -97,6 +66,14 @@ module Gitlab
private
+ def paginated_blobs(blobs, page)
+ results = Kaminari.paginate_array(blobs).page(page).per(per_page)
+
+ Gitlab::Search::FoundBlob.preload_blobs(results)
+
+ results
+ end
+
def blobs
return [] unless Ability.allowed?(@current_user, :download_code, @project)
diff --git a/lib/gitlab/search/found_blob.rb b/lib/gitlab/search/found_blob.rb
new file mode 100644
index 00000000000..a62ab1521a7
--- /dev/null
+++ b/lib/gitlab/search/found_blob.rb
@@ -0,0 +1,162 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Search
+ class FoundBlob
+ include EncodingHelper
+ include Presentable
+ include BlobLanguageFromGitAttributes
+ include Gitlab::Utils::StrongMemoize
+
+ attr_reader :project, :content_match, :blob_filename
+
+ FILENAME_REGEXP = /\A(?<ref>[^:]*):(?<filename>[^\x00]*)\x00/.freeze
+ CONTENT_REGEXP = /^(?<ref>[^:]*):(?<filename>[^\x00]*)\x00(?<startline>\d+)\x00/.freeze
+
+ def self.preload_blobs(blobs)
+ to_fetch = blobs.select { |blob| blob.is_a?(self) && blob.blob_filename }
+
+ to_fetch.each { |blob| blob.fetch_blob }
+ end
+
+ def initialize(opts = {})
+ @id = opts.fetch(:id, nil)
+ @binary_filename = opts.fetch(:filename, nil)
+ @binary_basename = opts.fetch(:basename, nil)
+ @ref = opts.fetch(:ref, nil)
+ @startline = opts.fetch(:startline, nil)
+ @binary_data = opts.fetch(:data, nil)
+ @per_page = opts.fetch(:per_page, 20)
+ @project = opts.fetch(:project, nil)
+ # Some caller does not have project object (e.g. elastic search),
+ # yet they can trigger many calls in one go,
+ # causing duplicated queries.
+ # Allow those to just pass project_id instead.
+ @project_id = opts.fetch(:project_id, nil)
+ @content_match = opts.fetch(:content_match, nil)
+ @blob_filename = opts.fetch(:blob_filename, nil)
+ @repository = opts.fetch(:repository, nil)
+ end
+
+ def id
+ @id ||= parsed_content[:id]
+ end
+
+ def ref
+ @ref ||= parsed_content[:ref]
+ end
+
+ def startline
+ @startline ||= parsed_content[:startline]
+ end
+
+ # binary_filename is used for running filters on all matches,
+ # for grepped results (which use content_match), we get
+ # filename from the beginning of the grepped result which is faster
+ # then parsing whole snippet
+ def binary_filename
+ @binary_filename ||= content_match ? search_result_filename : parsed_content[:binary_filename]
+ end
+
+ def filename
+ @filename ||= encode_utf8(@binary_filename || parsed_content[:binary_filename])
+ end
+
+ def basename
+ @basename ||= encode_utf8(@binary_basename || parsed_content[:binary_basename])
+ end
+
+ def data
+ @data ||= encode_utf8(@binary_data || parsed_content[:binary_data])
+ end
+
+ def path
+ filename
+ end
+
+ def project_id
+ @project_id || @project&.id
+ end
+
+ def present
+ super(presenter_class: BlobPresenter)
+ end
+
+ def fetch_blob
+ path = [ref, blob_filename]
+ missing_blob = { binary_filename: blob_filename }
+
+ BatchLoader.for(path).batch(default_value: missing_blob) do |refs, loader|
+ Gitlab::Git::Blob.batch(repository, refs, blob_size_limit: 1024).each do |blob|
+ # if the blob couldn't be fetched for some reason,
+ # show at least the blob filename
+ data = {
+ id: blob.id,
+ binary_filename: blob.path,
+ binary_basename: File.basename(blob.path, File.extname(blob.path)),
+ ref: ref,
+ startline: 1,
+ binary_data: blob.data,
+ project: project
+ }
+
+ loader.call([ref, blob.path], data)
+ end
+ end
+ end
+
+ private
+
+ def search_result_filename
+ content_match.match(FILENAME_REGEXP) { |matches| matches[:filename] }
+ end
+
+ def parsed_content
+ strong_memoize(:parsed_content) do
+ if content_match
+ parse_search_result
+ elsif blob_filename
+ fetch_blob
+ else
+ {}
+ end
+ end
+ end
+
+ def parse_search_result
+ ref = nil
+ filename = nil
+ basename = nil
+
+ data = []
+ startline = 0
+
+ content_match.each_line.each_with_index do |line, index|
+ prefix ||= line.match(CONTENT_REGEXP)&.tap do |matches|
+ ref = matches[:ref]
+ filename = matches[:filename]
+ startline = matches[:startline]
+ startline = startline.to_i - index
+ extname = Regexp.escape(File.extname(filename))
+ basename = filename.sub(/#{extname}$/, '')
+ end
+
+ data << line.sub(prefix.to_s, '')
+ end
+
+ {
+ binary_filename: filename,
+ binary_basename: basename,
+ ref: ref,
+ startline: startline,
+ binary_data: data.join,
+ project: project
+ }
+ end
+
+ def repository
+ @repository ||= project.repository
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/search/query.rb b/lib/gitlab/search/query.rb
index 7f69083a492..ba0e16607a6 100644
--- a/lib/gitlab/search/query.rb
+++ b/lib/gitlab/search/query.rb
@@ -3,6 +3,8 @@
module Gitlab
module Search
class Query < SimpleDelegator
+ include EncodingHelper
+
def initialize(query, filter_opts = {}, &block)
@raw_query = query.dup
@filters = []
@@ -50,7 +52,9 @@ module Gitlab
end
def parse_filter(filter, input)
- filter[:parser].call(input)
+ result = filter[:parser].call(input)
+
+ @filter_options[:encode_binary] ? encode_binary(result) : result
end
end
end
diff --git a/lib/gitlab/search_results.rb b/lib/gitlab/search_results.rb
index 458737f31eb..491148ec1a6 100644
--- a/lib/gitlab/search_results.rb
+++ b/lib/gitlab/search_results.rb
@@ -2,42 +2,6 @@
module Gitlab
class SearchResults
- class FoundBlob
- include EncodingHelper
- include Presentable
- include BlobLanguageFromGitAttributes
-
- attr_reader :id, :filename, :basename, :ref, :startline, :data, :project
-
- def initialize(opts = {})
- @id = opts.fetch(:id, nil)
- @filename = encode_utf8(opts.fetch(:filename, nil))
- @basename = encode_utf8(opts.fetch(:basename, nil))
- @ref = opts.fetch(:ref, nil)
- @startline = opts.fetch(:startline, nil)
- @data = encode_utf8(opts.fetch(:data, nil))
- @per_page = opts.fetch(:per_page, 20)
- @project = opts.fetch(:project, nil)
- # Some caller does not have project object (e.g. elastic search),
- # yet they can trigger many calls in one go,
- # causing duplicated queries.
- # Allow those to just pass project_id instead.
- @project_id = opts.fetch(:project_id, nil)
- end
-
- def path
- filename
- end
-
- def project_id
- @project_id || @project&.id
- end
-
- def present
- super(presenter_class: BlobPresenter)
- end
- end
-
attr_reader :current_user, :query, :per_page
# Limit search results by passed projects
diff --git a/lib/gitlab/sentry.rb b/lib/gitlab/sentry.rb
index 8079c5882c4..46d01964eac 100644
--- a/lib/gitlab/sentry.rb
+++ b/lib/gitlab/sentry.rb
@@ -3,7 +3,8 @@
module Gitlab
module Sentry
def self.enabled?
- Rails.env.production? && Gitlab::CurrentSettings.sentry_enabled?
+ (Rails.env.production? || Rails.env.development?) &&
+ Gitlab::CurrentSettings.sentry_enabled?
end
def self.context(current_user = nil)
@@ -31,7 +32,7 @@ module Gitlab
def self.track_exception(exception, issue_url: nil, extra: {})
track_acceptable_exception(exception, issue_url: issue_url, extra: extra)
- raise exception if should_raise?
+ raise exception if should_raise_for_dev?
end
# This should be used when you do not want to raise an exception in
@@ -43,7 +44,11 @@ module Gitlab
extra[:issue_url] = issue_url if issue_url
context # Make sure we've set everything we know in the context
- Raven.capture_exception(exception, extra: extra)
+ tags = {
+ Gitlab::CorrelationId::LOG_KEY.to_sym => Gitlab::CorrelationId.current_id
+ }
+
+ Raven.capture_exception(exception, tags: tags, extra: extra)
end
end
@@ -55,7 +60,7 @@ module Gitlab
end
end
- def self.should_raise?
+ def self.should_raise_for_dev?
Rails.env.development? || Rails.env.test?
end
end
diff --git a/lib/gitlab/sidekiq_middleware/correlation_injector.rb b/lib/gitlab/sidekiq_middleware/correlation_injector.rb
new file mode 100644
index 00000000000..b807b3a03ed
--- /dev/null
+++ b/lib/gitlab/sidekiq_middleware/correlation_injector.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module SidekiqMiddleware
+ class CorrelationInjector
+ def call(worker_class, job, queue, redis_pool)
+ job[Gitlab::CorrelationId::LOG_KEY] ||=
+ Gitlab::CorrelationId.current_or_new_id
+
+ yield
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/sidekiq_middleware/correlation_logger.rb b/lib/gitlab/sidekiq_middleware/correlation_logger.rb
new file mode 100644
index 00000000000..cb8ff4a6284
--- /dev/null
+++ b/lib/gitlab/sidekiq_middleware/correlation_logger.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module SidekiqMiddleware
+ class CorrelationLogger
+ def call(worker, job, queue)
+ correlation_id = job[Gitlab::CorrelationId::LOG_KEY]
+
+ Gitlab::CorrelationId.use_id(correlation_id) do
+ yield
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ssh_public_key.rb b/lib/gitlab/ssh_public_key.rb
index 47571239b5c..6df54852d02 100644
--- a/lib/gitlab/ssh_public_key.rb
+++ b/lib/gitlab/ssh_public_key.rb
@@ -4,7 +4,7 @@ module Gitlab
class SSHPublicKey
Technology = Struct.new(:name, :key_class, :supported_sizes)
- Technologies = [
+ TECHNOLOGIES = [
Technology.new(:rsa, OpenSSL::PKey::RSA, [1024, 2048, 3072, 4096]),
Technology.new(:dsa, OpenSSL::PKey::DSA, [1024, 2048, 3072]),
Technology.new(:ecdsa, OpenSSL::PKey::EC, [256, 384, 521]),
@@ -12,11 +12,11 @@ module Gitlab
].freeze
def self.technology(name)
- Technologies.find { |tech| tech.name.to_s == name.to_s }
+ TECHNOLOGIES.find { |tech| tech.name.to_s == name.to_s }
end
def self.technology_for_key(key)
- Technologies.find { |tech| key.is_a?(tech.key_class) }
+ TECHNOLOGIES.find { |tech| key.is_a?(tech.key_class) }
end
def self.supported_sizes(name)
diff --git a/lib/gitlab/template/finders/global_template_finder.rb b/lib/gitlab/template/finders/global_template_finder.rb
index 76bb9eb611e..2dd4b7a4092 100644
--- a/lib/gitlab/template/finders/global_template_finder.rb
+++ b/lib/gitlab/template/finders/global_template_finder.rb
@@ -18,6 +18,10 @@ module Gitlab
def find(key)
file_name = "#{key}#{@extension}"
+ # The key is untrusted input, so ensure we can't be directed outside
+ # of base_dir
+ Gitlab::Utils.check_path_traversal!(file_name)
+
directory = select_directory(file_name)
directory ? File.join(category_directory(directory), file_name) : nil
end
diff --git a/lib/gitlab/template/finders/repo_template_finder.rb b/lib/gitlab/template/finders/repo_template_finder.rb
index b92cefefb8f..8e234148a63 100644
--- a/lib/gitlab/template/finders/repo_template_finder.rb
+++ b/lib/gitlab/template/finders/repo_template_finder.rb
@@ -26,6 +26,11 @@ module Gitlab
def find(key)
file_name = "#{key}#{@extension}"
+
+ # The key is untrusted input, so ensure we can't be directed outside
+ # of base_dir inside the repository
+ Gitlab::Utils.check_path_traversal!(file_name)
+
directory = select_directory(file_name)
raise FileNotFoundError if directory.nil?
diff --git a/lib/gitlab/url_blocker.rb b/lib/gitlab/url_blocker.rb
index b8040f73cee..44c71f8431d 100644
--- a/lib/gitlab/url_blocker.rb
+++ b/lib/gitlab/url_blocker.rb
@@ -8,7 +8,7 @@ module Gitlab
BlockedUrlError = Class.new(StandardError)
class << self
- def validate!(url, allow_localhost: false, allow_local_network: true, enforce_user: false, ports: [], protocols: [])
+ def validate!(url, ports: [], protocols: [], allow_localhost: false, allow_local_network: true, ascii_only: false, enforce_user: false)
return true if url.nil?
# Param url can be a string, URI or Addressable::URI
@@ -22,6 +22,7 @@ module Gitlab
validate_port!(port, ports) if ports.any?
validate_user!(uri.user) if enforce_user
validate_hostname!(uri.hostname)
+ validate_unicode_restriction!(uri) if ascii_only
begin
addrs_info = Addrinfo.getaddrinfo(uri.hostname, port, nil, :STREAM).map do |addr|
@@ -91,6 +92,12 @@ module Gitlab
raise BlockedUrlError, "Hostname or IP address invalid"
end
+ def validate_unicode_restriction!(uri)
+ return if uri.to_s.ascii_only?
+
+ raise BlockedUrlError, "URI must be ascii only #{uri.to_s.dump}"
+ end
+
def validate_localhost!(addrs_info)
local_ips = ["::", "0.0.0.0"]
local_ips.concat(Socket.ip_address_list.map(&:ip_address))
diff --git a/lib/gitlab/url_sanitizer.rb b/lib/gitlab/url_sanitizer.rb
index 035268bc4f2..880712de5fe 100644
--- a/lib/gitlab/url_sanitizer.rb
+++ b/lib/gitlab/url_sanitizer.rb
@@ -14,6 +14,7 @@ module Gitlab
def self.valid?(url)
return false unless url.present?
+ return false unless url.is_a?(String)
uri = Addressable::URI.parse(url.strip)
diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb
index bfcc8efdc96..008e9cd1d24 100644
--- a/lib/gitlab/usage_data.rb
+++ b/lib/gitlab/usage_data.rb
@@ -2,6 +2,8 @@
module Gitlab
class UsageData
+ APPROXIMATE_COUNT_MODELS = [Label, MergeRequest, Note, Todo].freeze
+
class << self
def data(force_refresh: false)
Rails.cache.fetch('usage_data', force: force_refresh, expires_in: 2.weeks) { uncached_data }
@@ -73,12 +75,9 @@ module Gitlab
issues: count(Issue),
keys: count(Key),
label_lists: count(List.label),
- labels: count(Label),
lfs_objects: count(LfsObject),
- merge_requests: count(MergeRequest),
milestone_lists: count(List.milestone),
milestones: count(Milestone),
- notes: count(Note),
pages_domains: count(PagesDomain),
projects: count(Project),
projects_imported_from_github: count(Project.where(import_type: 'github')),
@@ -86,10 +85,9 @@ module Gitlab
releases: count(Release),
remote_mirrors: count(RemoteMirror),
snippets: count(Snippet),
- todos: count(Todo),
uploads: count(Upload),
web_hooks: count(WebHook)
- }.merge(services_usage)
+ }.merge(services_usage).merge(approximate_counts)
}
end
# rubocop: enable CodeReuse/ActiveRecord
@@ -164,6 +162,16 @@ module Gitlab
fallback
end
# rubocop: enable CodeReuse/ActiveRecord
+
+ def approximate_counts
+ approx_counts = Gitlab::Database::Count.approximate_counts(APPROXIMATE_COUNT_MODELS)
+
+ APPROXIMATE_COUNT_MODELS.each_with_object({}) do |model, result|
+ key = model.name.underscore.pluralize.to_sym
+
+ result[key] = approx_counts[model] || -1
+ end
+ end
end
end
end
diff --git a/lib/gitlab/utils.rb b/lib/gitlab/utils.rb
index e0e8f598ba4..a81cee0d6d2 100644
--- a/lib/gitlab/utils.rb
+++ b/lib/gitlab/utils.rb
@@ -4,6 +4,15 @@ module Gitlab
module Utils
extend self
+ # Ensure that the relative path will not traverse outside the base directory
+ def check_path_traversal!(path)
+ raise StandardError.new("Invalid path") if path.start_with?("..#{File::SEPARATOR}") ||
+ path.include?("#{File::SEPARATOR}..#{File::SEPARATOR}") ||
+ path.end_with?("#{File::SEPARATOR}..")
+
+ path
+ end
+
# Run system command without outputting to stdout.
#
# @param cmd [Array<String>]
@@ -51,7 +60,7 @@ module Gitlab
# Converts newlines into HTML line break elements
def nlbr(str)
- ActionView::Base.full_sanitizer.sanitize(str, tags: []).gsub(/\r?\n/, '<br>').html_safe
+ ActionView::Base.full_sanitizer.sanitize(+str, tags: []).gsub(/\r?\n/, '<br>').html_safe
end
def remove_line_breaks(str)
diff --git a/lib/gitlab/wiki_file_finder.rb b/lib/gitlab/wiki_file_finder.rb
index a00cd65594c..5303b3582ab 100644
--- a/lib/gitlab/wiki_file_finder.rb
+++ b/lib/gitlab/wiki_file_finder.rb
@@ -2,6 +2,8 @@
module Gitlab
class WikiFileFinder < FileFinder
+ BATCH_SIZE = 100
+
attr_reader :repository
def initialize(project, ref)
@@ -12,13 +14,11 @@ module Gitlab
private
- def search_filenames(query, except)
+ def search_filenames(query)
safe_query = Regexp.escape(query.tr(' ', '-'))
safe_query = Regexp.new(safe_query, Regexp::IGNORECASE)
filenames = repository.ls_files(ref)
- filenames.delete_if { |filename| except.include?(filename) } unless except.empty?
-
filenames.grep(safe_query).first(BATCH_SIZE)
end
end
diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb
index e1f777e9cd1..265f6213a99 100644
--- a/lib/gitlab/workhorse.rb
+++ b/lib/gitlab/workhorse.rb
@@ -13,6 +13,7 @@ module Gitlab
INTERNAL_API_REQUEST_HEADER = 'Gitlab-Workhorse-Api-Request'.freeze
NOTIFICATION_CHANNEL = 'workhorse:notifications'.freeze
ALLOWED_GIT_HTTP_ACTIONS = %w[git_receive_pack git_upload_pack info_refs].freeze
+ DETECT_HEADER = 'Gitlab-Workhorse-Detect-Content-Type'.freeze
# Supposedly the effective key size for HMAC-SHA256 is 256 bits, i.e. 32
# bytes https://tools.ietf.org/html/rfc4868#section-2.6
@@ -30,7 +31,6 @@ module Gitlab
GL_USERNAME: user&.username,
ShowAllRefs: show_all_refs,
Repository: repository.gitaly_repository.to_h,
- RepoPath: 'ignored but not allowed to be empty in gitlab-workhorse',
GitConfigOptions: [],
GitalyServer: {
address: Gitlab::GitalyClient.address(project.repository_storage),
diff --git a/lib/omni_auth/strategies/jwt.rb b/lib/omni_auth/strategies/jwt.rb
index a792903fde7..2f3d477a591 100644
--- a/lib/omni_auth/strategies/jwt.rb
+++ b/lib/omni_auth/strategies/jwt.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
require 'omniauth'
+require 'openssl'
require 'jwt'
module OmniAuth
@@ -37,7 +38,19 @@ module OmniAuth
end
def decoded
- @decoded ||= ::JWT.decode(request.params['jwt'], options.secret, options.algorithm).first
+ secret =
+ case options.algorithm
+ when *%w[RS256 RS384 RS512]
+ OpenSSL::PKey::RSA.new(options.secret).public_key
+ when *%w[ES256 ES384 ES512]
+ OpenSSL::PKey::EC.new(options.secret).tap { |key| key.private_key = nil }
+ when *%w(HS256 HS384 HS512)
+ options.secret
+ else
+ raise NotImplementedError, "Unsupported algorithm: #{options.algorithm}"
+ end
+
+ @decoded ||= ::JWT.decode(request.params['jwt'], secret, true, { algorithm: options.algorithm }).first
(options.required_claims || []).each do |field|
raise ClaimInvalid, "Missing required '#{field}' claim" unless @decoded.key?(field.to_s)
@@ -45,7 +58,7 @@ module OmniAuth
raise ClaimInvalid, "Missing required 'iat' claim" if options.valid_within && !@decoded["iat"]
- if options.valid_within && (Time.now.to_i - @decoded["iat"]).abs > options.valid_within
+ if options.valid_within && (Time.now.to_i - @decoded["iat"]).abs > options.valid_within.to_i
raise ClaimInvalid, "'iat' timestamp claim is too skewed from present"
end
diff --git a/lib/tasks/gitlab/web_hook.rake b/lib/tasks/gitlab/web_hook.rake
index 5a1c8006052..15cec80b6a6 100644
--- a/lib/tasks/gitlab/web_hook.rake
+++ b/lib/tasks/gitlab/web_hook.rake
@@ -25,11 +25,22 @@ namespace :gitlab do
web_hook_url = ENV['URL']
namespace_path = ENV['NAMESPACE']
- projects = find_projects(namespace_path)
- project_ids = projects.pluck(:id)
+ web_hooks = find_web_hooks(namespace_path)
puts "Removing webhooks with the url '#{web_hook_url}' ... "
- count = WebHook.where(url: web_hook_url, project_id: project_ids, type: 'ProjectHook').delete_all
+
+ # FIXME: Hook URLs are now encrypted, so there is no way to efficiently
+ # find them all in SQL. For now, check them in Ruby. If this is too slow,
+ # we could consider storing a hash of the URL alongside the encrypted
+ # value to speed up searches
+ count = 0
+ web_hooks.find_each do |hook|
+ next unless hook.url == web_hook_url
+
+ hook.destroy!
+ count += 1
+ end
+
puts "#{count} webhooks were removed."
end
@@ -37,29 +48,37 @@ namespace :gitlab do
task list: :environment do
namespace_path = ENV['NAMESPACE']
- projects = find_projects(namespace_path)
- web_hooks = projects.all.map(&:hooks).flatten
- web_hooks.each do |hook|
+ web_hooks = find_web_hooks(namespace_path)
+ web_hooks.find_each do |hook|
puts "#{hook.project.name.truncate(20).ljust(20)} -> #{hook.url}"
end
- puts "\n#{web_hooks.size} webhooks found."
+ puts "\n#{web_hooks.count} webhooks found."
end
end
def find_projects(namespace_path)
if namespace_path.blank?
Project
- elsif namespace_path == '/'
- Project.in_namespace(nil)
else
- namespace = Namespace.where(path: namespace_path).first
- if namespace
- Project.in_namespace(namespace.id)
- else
+ namespace = Namespace.find_by_full_path(namespace_path)
+
+ unless namespace
puts "Namespace not found: #{namespace_path}".color(:red)
exit 2
end
+
+ Project.in_namespace(namespace.id)
+ end
+ end
+
+ def find_web_hooks(namespace_path)
+ if namespace_path.blank?
+ ProjectHook
+ else
+ project_ids = find_projects(namespace_path).select(:id)
+
+ ProjectHook.where(project_id: project_ids)
end
end
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index f023a9be3eb..abd1ce4a13a 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -97,6 +97,9 @@ msgstr[1] ""
msgid "%{actionText} & %{openOrClose} %{noteable}"
msgstr ""
+msgid "%{bio} at %{organization}"
+msgstr ""
+
msgid "%{commit_author_link} authored %{commit_timeago}"
msgstr ""
@@ -141,6 +144,24 @@ msgstr ""
msgid "%{percent}%% complete"
msgstr ""
+msgid "%{strong_start}%{branch_count}%{strong_end} Branch"
+msgid_plural "%{strong_start}%{branch_count}%{strong_end} Branches"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "%{strong_start}%{commit_count}%{strong_end} Commit"
+msgid_plural "%{strong_start}%{commit_count}%{strong_end} Commits"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "%{strong_start}%{human_size}%{strong_end} Files"
+msgstr ""
+
+msgid "%{strong_start}%{tag_count}%{strong_end} Tag"
+msgid_plural "%{strong_start}%{tag_count}%{strong_end} Tags"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%{text} %{files}"
msgid_plural "%{text} %{files} files"
msgstr[0] ""
@@ -225,7 +246,7 @@ msgstr ""
msgid "2FA enabled"
msgstr ""
-msgid "403|Please contact your GitLab administrator to get the permission."
+msgid "403|Please contact your GitLab administrator to get permission."
msgstr ""
msgid "403|You don't have the permission to access this page."
@@ -333,16 +354,16 @@ msgstr ""
msgid "Activity"
msgstr ""
-msgid "Add Changelog"
+msgid "Add CHANGELOG"
msgstr ""
-msgid "Add Contribution guide"
+msgid "Add CONTRIBUTING"
msgstr ""
msgid "Add Kubernetes cluster"
msgstr ""
-msgid "Add Readme"
+msgid "Add README"
msgstr ""
msgid "Add a homepage to your wiki that contains information about your project and GitLab will display it here instead of this message."
@@ -384,9 +405,6 @@ msgstr ""
msgid "Admin Overview"
msgstr ""
-msgid "Admin area"
-msgstr ""
-
msgid "AdminArea| You are about to permanently delete the user %{username}. Issues, merge requests, and groups linked to them will be transferred to a system-wide \"Ghost-user\". To avoid data loss, consider using the %{strong_start}block user%{strong_end} feature instead. Once you %{strong_start}Delete user%{strong_end}, it cannot be undone or recovered."
msgstr ""
@@ -828,6 +846,9 @@ msgstr ""
msgid "Available specific runners"
msgstr ""
+msgid "Avatar for %{assigneeName}"
+msgstr ""
+
msgid "Avatar will be removed. Are you sure?"
msgstr ""
@@ -945,11 +966,6 @@ msgstr ""
msgid "Branch %{branchName} was not found in this project's repository."
msgstr ""
-msgid "Branch (%{branch_count})"
-msgid_plural "Branches (%{branch_count})"
-msgstr[0] ""
-msgstr[1] ""
-
msgid "Branch <strong>%{branch_name}</strong> was created. To set up auto deploy, choose a GitLab CI Yaml template and commit your changes. %{link_to_autodeploy_doc}"
msgstr ""
@@ -1103,6 +1119,9 @@ msgstr ""
msgid "ByAuthor|by"
msgstr ""
+msgid "CHANGELOG"
+msgstr ""
+
msgid "CI / CD"
msgstr ""
@@ -1160,6 +1179,9 @@ msgstr ""
msgid "CICD|instance enabled"
msgstr ""
+msgid "CONTRIBUTING"
+msgstr ""
+
msgid "Callback URL"
msgstr ""
@@ -1202,9 +1224,6 @@ msgstr ""
msgid "ChangeTypeAction|This will create a new commit in order to revert the existing changes."
msgstr ""
-msgid "Changelog"
-msgstr ""
-
msgid "Changes are shown as if the <b>source</b> revision was being merged into the <b>target</b> revision."
msgstr ""
@@ -1244,6 +1263,9 @@ msgstr ""
msgid "Choose a branch/tag (e.g. %{master}) or enter a commit (e.g. %{sha}) to see what's changed or to create a merge request."
msgstr ""
+msgid "Choose a file"
+msgstr ""
+
msgid "Choose a template..."
msgstr ""
@@ -1385,9 +1407,18 @@ msgstr ""
msgid "Clients"
msgstr ""
+msgid "Clone"
+msgstr ""
+
msgid "Clone repository"
msgstr ""
+msgid "Clone with %{http_label}"
+msgstr ""
+
+msgid "Clone with SSH"
+msgstr ""
+
msgid "Close"
msgstr ""
@@ -1445,6 +1476,9 @@ msgstr ""
msgid "ClusterIntegration|Cert-Manager"
msgstr ""
+msgid "ClusterIntegration|Cert-Manager is a native Kubernetes certificate management controller that helps with issuing certificates. Installing Cert-Manager on your cluster will issue a certificate by %{letsEncrypt} and ensure that certificates are valid and up-to-date."
+msgstr ""
+
msgid "ClusterIntegration|Certificate Authority bundle (PEM format)"
msgstr ""
@@ -1559,6 +1593,12 @@ msgstr ""
msgid "ClusterIntegration|Integration status"
msgstr ""
+msgid "ClusterIntegration|Issuer Email"
+msgstr ""
+
+msgid "ClusterIntegration|Issuers represent a certificate authority. You must provide an email address for your Issuer. "
+msgstr ""
+
msgid "ClusterIntegration|Jupyter Hostname"
msgstr ""
@@ -1778,9 +1818,6 @@ msgstr ""
msgid "ClusterIntegration|access to Google Kubernetes Engine"
msgstr ""
-msgid "ClusterIntegration|cert-manager is a native Kubernetes certificate management controller that helps with issuing certificates. Installing cert-manager on your cluster will issue a certificate by %{letsEncrypt} and ensure that certificates are valid and up to date."
-msgstr ""
-
msgid "ClusterIntegration|check the pricing here"
msgstr ""
@@ -1808,6 +1845,9 @@ msgstr ""
msgid "Collapse sidebar"
msgstr ""
+msgid "Command line instructions"
+msgstr ""
+
msgid "Comment"
msgstr ""
@@ -1828,11 +1868,6 @@ msgid_plural "Commits"
msgstr[0] ""
msgstr[1] ""
-msgid "Commit (%{commit_count})"
-msgid_plural "Commits (%{commit_count})"
-msgstr[0] ""
-msgstr[1] ""
-
msgid "Commit Message"
msgstr ""
@@ -2016,9 +2051,6 @@ msgstr ""
msgid "Contribution Charts"
msgstr ""
-msgid "Contribution guide"
-msgstr ""
-
msgid "Contributions for <strong>%{calendar_date}</strong>"
msgstr ""
@@ -2043,10 +2075,10 @@ msgstr ""
msgid "ConvDev Index"
msgstr ""
-msgid "Copy %{protocol} clone URL"
+msgid "Copy %{http_label} clone URL"
msgstr ""
-msgid "Copy HTTPS clone URL"
+msgid "Copy %{protocol} clone URL"
msgstr ""
msgid "Copy ID to clipboard"
@@ -2109,6 +2141,9 @@ msgstr ""
msgid "Create a new issue"
msgstr ""
+msgid "Create a new repository"
+msgstr ""
+
msgid "Create a personal access token on your account to pull or push via %{protocol}."
msgstr ""
@@ -2867,6 +2902,12 @@ msgstr ""
msgid "Everyone can contribute"
msgstr ""
+msgid "Existing Git repository"
+msgstr ""
+
+msgid "Existing folder"
+msgstr ""
+
msgid "Expand"
msgstr ""
@@ -2879,6 +2920,9 @@ msgstr ""
msgid "Expiration date"
msgstr ""
+msgid "Expired %{expiredOn}"
+msgstr ""
+
msgid "Expires in %{expires_at}"
msgstr ""
@@ -2939,6 +2983,9 @@ msgstr ""
msgid "Failed to update issues, please try again."
msgstr ""
+msgid "Failed to upload object map file"
+msgstr ""
+
msgid "Failure"
msgstr ""
@@ -2972,9 +3019,6 @@ msgstr ""
msgid "Files"
msgstr ""
-msgid "Files (%{human_size})"
-msgstr ""
-
msgid "Filter"
msgstr ""
@@ -3050,6 +3094,9 @@ msgstr ""
msgid "Forking in progress"
msgstr ""
+msgid "Forks"
+msgstr ""
+
msgid "Format"
msgstr ""
@@ -3101,6 +3148,9 @@ msgstr ""
msgid "Git"
msgstr ""
+msgid "Git global setup"
+msgstr ""
+
msgid "Git repository URL"
msgstr ""
@@ -3370,6 +3420,9 @@ msgid_plural "Hide values"
msgstr[0] ""
msgstr[1] ""
+msgid "Hide values"
+msgstr ""
+
msgid "Hide whitespace changes"
msgstr ""
@@ -3869,6 +3922,9 @@ msgstr ""
msgid "Loading..."
msgstr ""
+msgid "Loading…"
+msgstr ""
+
msgid "Lock"
msgstr ""
@@ -4315,6 +4371,9 @@ msgstr ""
msgid "No changes"
msgstr ""
+msgid "No changes between %{ref_start}%{source_branch}%{ref_end} and %{ref_start}%{target_branch}%{ref_end}"
+msgstr ""
+
msgid "No connection could be made to a Gitaly Server, please check your logs!"
msgstr ""
@@ -4333,6 +4392,9 @@ msgstr ""
msgid "No file chosen"
msgstr ""
+msgid "No file selected"
+msgstr ""
+
msgid "No files found."
msgstr ""
@@ -4429,6 +4491,12 @@ msgstr ""
msgid "Notification events"
msgstr ""
+msgid "Notification setting"
+msgstr ""
+
+msgid "Notification setting - %{notification_title}"
+msgstr ""
+
msgid "NotificationEvent|Close issue"
msgstr ""
@@ -5340,6 +5408,9 @@ msgstr ""
msgid "Quick actions can be used in the issues description and comment boxes."
msgstr ""
+msgid "README"
+msgstr ""
+
msgid "Read more"
msgstr ""
@@ -5349,9 +5420,6 @@ msgstr ""
msgid "Read more about project permissions <strong>%{link_to_help}</strong>"
msgstr ""
-msgid "Readme"
-msgstr ""
-
msgid "Real-time features"
msgstr ""
@@ -5489,6 +5557,12 @@ msgstr ""
msgid "Repository URL"
msgstr ""
+msgid "Repository cleanup"
+msgstr ""
+
+msgid "Repository cleanup has started. You will receive an email once the cleanup operation is complete."
+msgstr ""
+
msgid "Repository maintenance"
msgstr ""
@@ -5561,14 +5635,14 @@ msgstr ""
msgid "Retry verification"
msgstr ""
-msgid "Reveal Variables"
-msgstr ""
-
msgid "Reveal value"
msgid_plural "Reveal values"
msgstr[0] ""
msgstr[1] ""
+msgid "Reveal values"
+msgstr ""
+
msgid "Revert this commit"
msgstr ""
@@ -5815,6 +5889,45 @@ msgstr ""
msgid "Server version"
msgstr ""
+msgid "Serverless"
+msgstr ""
+
+msgid "Serverless| In order to start using functions as a service, you must first install Knative on your Kubernetes cluster."
+msgstr ""
+
+msgid "Serverless|An error occurred while retrieving serverless components"
+msgstr ""
+
+msgid "Serverless|Domain"
+msgstr ""
+
+msgid "Serverless|Function"
+msgstr ""
+
+msgid "Serverless|Getting started with serverless"
+msgstr ""
+
+msgid "Serverless|If you believe none of these apply, please check back later as the function data may be in the process of becoming available."
+msgstr ""
+
+msgid "Serverless|Install Knative"
+msgstr ""
+
+msgid "Serverless|Last Update"
+msgstr ""
+
+msgid "Serverless|Learn more about Serverless"
+msgstr ""
+
+msgid "Serverless|No functions available"
+msgstr ""
+
+msgid "Serverless|Runtime"
+msgstr ""
+
+msgid "Serverless|There is currently no function data available from Knative. This could be for a variety of reasons including:"
+msgstr ""
+
msgid "Service Templates"
msgstr ""
@@ -6156,12 +6269,18 @@ msgstr ""
msgid "Starred projects"
msgstr ""
+msgid "Stars"
+msgstr ""
+
msgid "Start a %{new_merge_request} with these changes"
msgstr ""
msgid "Start and due date"
msgstr ""
+msgid "Start cleanup"
+msgstr ""
+
msgid "Start date"
msgstr ""
@@ -6171,6 +6290,12 @@ msgstr ""
msgid "Started"
msgstr ""
+msgid "Started %{startsIn}"
+msgstr ""
+
+msgid "Starts %{startsIn}"
+msgstr ""
+
msgid "Starts at (UTC)"
msgstr ""
@@ -6237,11 +6362,6 @@ msgstr ""
msgid "System metrics (Kubernetes)"
msgstr ""
-msgid "Tag (%{tag_count})"
-msgid_plural "Tags (%{tag_count})"
-msgstr[0] ""
-msgstr[1] ""
-
msgid "Tags"
msgstr ""
@@ -6377,6 +6497,9 @@ msgstr ""
msgid "The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage."
msgstr ""
+msgid "The maximum file size allowed is %{max_attachment_size}mb"
+msgstr ""
+
msgid "The maximum file size allowed is 200KB."
msgstr ""
@@ -7059,6 +7182,9 @@ msgstr ""
msgid "Upload file"
msgstr ""
+msgid "Upload object map"
+msgstr ""
+
msgid "UploadLink|click to upload"
msgstr ""
@@ -7155,6 +7281,9 @@ msgstr ""
msgid "Variables are applied to environments via the runner. They can be protected by only exposing them to protected branches or tags. You can use variables for passwords, secret keys, or whatever you want."
msgstr ""
+msgid "Variables:"
+msgstr ""
+
msgid "Various container registry settings."
msgstr ""
@@ -7932,6 +8061,9 @@ msgid_plural "replies"
msgstr[0] ""
msgstr[1] ""
+msgid "should be higher than %{access} inherited membership from group %{group_name}"
+msgstr ""
+
msgid "source"
msgstr ""
diff --git a/package.json b/package.json
index 680a5bb1cde..cf7e43f14dd 100644
--- a/package.json
+++ b/package.json
@@ -6,7 +6,7 @@
"eslint": "eslint --max-warnings 0 --report-unused-disable-directives --ext .js,.vue .",
"eslint-fix": "eslint --max-warnings 0 --report-unused-disable-directives --ext .js,.vue --fix .",
"eslint-report": "eslint --max-warnings 0 --ext .js,.vue --format html --output-file ./eslint-report.html --no-inline-config .",
- "jest": "BABEL_ENV=jest jest --config=config/jest.config.js",
+ "jest": "BABEL_ENV=jest jest",
"karma": "BABEL_ENV=${BABEL_ENV:=karma} karma start --single-run true config/karma.config.js",
"karma-coverage": "BABEL_ENV=coverage karma start --single-run true config/karma.config.js",
"karma-start": "BABEL_ENV=karma karma start config/karma.config.js",
@@ -25,8 +25,9 @@
"@babel/plugin-syntax-dynamic-import": "^7.0.0",
"@babel/plugin-syntax-import-meta": "^7.0.0",
"@babel/preset-env": "^7.1.0",
- "@gitlab/svgs": "^1.40.0",
- "@gitlab/ui": "^1.11.0",
+ "@gitlab/csslab": "^1.8.0",
+ "@gitlab/svgs": "^1.42.0",
+ "@gitlab/ui": "^1.15.0",
"apollo-boost": "^0.1.20",
"apollo-client": "^2.4.5",
"autosize": "^4.0.0",
@@ -57,6 +58,7 @@
"diff": "^3.4.0",
"document-register-element": "1.3.0",
"dropzone": "^4.2.0",
+ "echarts": "^4.2.0-rc.2",
"emoji-unicode-version": "^0.2.1",
"exports-loader": "^0.7.0",
"file-loader": "^2.0.0",
@@ -116,7 +118,7 @@
"xterm": "^3.5.0"
},
"devDependencies": {
- "@gitlab/eslint-config": "^1.2.0",
+ "@gitlab/eslint-config": "^1.4.0",
"@vue/test-utils": "^1.0.0-beta.25",
"axios-mock-adapter": "^1.15.0",
"babel-core": "^7.0.0-bridge",
@@ -129,10 +131,10 @@
"babel-types": "^6.26.0",
"chalk": "^2.4.1",
"commander": "^2.18.0",
- "eslint": "~5.6.0",
+ "eslint": "~5.9.0",
"eslint-import-resolver-jest": "^2.1.1",
"eslint-import-resolver-webpack": "^0.10.1",
- "eslint-plugin-html": "4.0.5",
+ "eslint-plugin-html": "5.0.0",
"eslint-plugin-import": "^2.14.0",
"eslint-plugin-jasmine": "^2.10.1",
"eslint-plugin-jest": "^22.1.0",
@@ -155,6 +157,10 @@
"karma-webpack": "^4.0.0-beta.0",
"nodemon": "^1.18.4",
"prettier": "1.15.2",
+ "vue-jest": "^3.0.1",
"webpack-dev-server": "^3.1.10"
+ },
+ "engines": {
+ "yarn": "^1.10.0"
}
}
diff --git a/qa/qa.rb b/qa/qa.rb
index aa0b78b37e8..bf05b6b53ca 100644
--- a/qa/qa.rb
+++ b/qa/qa.rb
@@ -158,6 +158,10 @@ module QA
autoload :Activity, 'qa/page/project/activity'
autoload :Menu, 'qa/page/project/menu'
+ module Commit
+ autoload :Show, 'qa/page/project/commit/show'
+ end
+
module Import
autoload :Github, 'qa/page/project/import/github'
end
@@ -184,6 +188,7 @@ module QA
autoload :Runners, 'qa/page/project/settings/runners'
autoload :MergeRequest, 'qa/page/project/settings/merge_request'
autoload :Members, 'qa/page/project/settings/members'
+ autoload :MirroringRepositories, 'qa/page/project/settings/mirroring_repositories'
end
module Issue
@@ -272,6 +277,7 @@ module QA
#
module Component
autoload :ClonePanel, 'qa/page/component/clone_panel'
+ autoload :LegacyClonePanel, 'qa/page/component/legacy_clone_panel'
autoload :Dropzone, 'qa/page/component/dropzone'
autoload :GroupsFilter, 'qa/page/component/groups_filter'
autoload :Select2, 'qa/page/component/select2'
diff --git a/qa/qa/page/base.rb b/qa/qa/page/base.rb
index 91e229c4c8c..88ade66f47d 100644
--- a/qa/qa/page/base.rb
+++ b/qa/qa/page/base.rb
@@ -15,7 +15,7 @@ module QA
def_delegators :evaluator, :view, :views
def refresh
- visit current_url
+ page.refresh
end
def wait(max: 60, time: 0.1, reload: true)
@@ -80,8 +80,8 @@ module QA
page.evaluate_script('xhr.status') == 200
end
- def find_element(name)
- find(element_selector_css(name))
+ def find_element(name, wait: Capybara.default_max_wait_time)
+ find(element_selector_css(name), wait: wait)
end
def all_elements(name)
@@ -100,6 +100,14 @@ module QA
find_element(name).set(content)
end
+ def select_element(name, value)
+ element = find_element(name)
+
+ return if element.text.downcase.to_s == value.to_s
+
+ element.select value.to_s.capitalize
+ end
+
def has_element?(name)
has_css?(element_selector_css(name))
end
@@ -110,6 +118,12 @@ module QA
end
end
+ def within_element_by_index(name, index)
+ page.within all_elements(name)[index] do
+ yield
+ end
+ end
+
def scroll_to_element(name, *args)
scroll_to(element_selector_css(name), *args)
end
@@ -118,6 +132,10 @@ module QA
Page::Element.new(name).selector_css
end
+ def click_link_with_text(text)
+ click_link text
+ end
+
def self.path
raise NotImplementedError
end
diff --git a/qa/qa/page/component/clone_panel.rb b/qa/qa/page/component/clone_panel.rb
index 94e761b0e0c..d37b63c716a 100644
--- a/qa/qa/page/component/clone_panel.rb
+++ b/qa/qa/page/component/clone_panel.rb
@@ -5,26 +5,20 @@ module QA
module Component
module ClonePanel
def self.included(base)
- base.view 'app/views/shared/_clone_panel.html.haml' do
+ base.view 'app/views/projects/buttons/_clone.html.haml' do
element :clone_dropdown
- element :clone_options_dropdown, '.clone-options-dropdown' # rubocop:disable QA/ElementWithPattern
- element :project_repository_location, 'text_field_tag :project_clone' # rubocop:disable QA/ElementWithPattern
+ element :clone_options
+ element :ssh_clone_url
+ element :http_clone_url
end
end
- def choose_repository_clone_http
- choose_repository_clone('HTTP', 'http')
+ def repository_clone_http_location
+ repository_clone_location(:http_clone_url)
end
- def choose_repository_clone_ssh
- # It's not always beginning with ssh:// so detecting with @
- # would be more reliable because ssh would always contain it.
- # We can't use .git because HTTP also contain that part.
- choose_repository_clone('SSH', '@')
- end
-
- def repository_location
- Git::Location.new(find('#project_clone').value)
+ def repository_clone_ssh_location
+ repository_clone_location(:ssh_clone_url)
end
def wait_for_push
@@ -34,16 +28,13 @@ module QA
private
- def choose_repository_clone(kind, detect_text)
+ def repository_clone_location(kind)
wait(reload: false) do
click_element :clone_dropdown
- page.within('.clone-options-dropdown') do
- click_link(kind)
+ within_element :clone_options do
+ Git::Location.new(find_element(kind).value)
end
-
- # Ensure git clone textbox was updated
- repository_location.git_uri.include?(detect_text)
end
end
end
diff --git a/qa/qa/page/component/legacy_clone_panel.rb b/qa/qa/page/component/legacy_clone_panel.rb
new file mode 100644
index 00000000000..99132190f3f
--- /dev/null
+++ b/qa/qa/page/component/legacy_clone_panel.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+module QA
+ module Page
+ module Component
+ module LegacyClonePanel
+ def self.included(base)
+ base.view 'app/views/shared/_clone_panel.html.haml' do
+ element :clone_dropdown
+ element :clone_options_dropdown, '.clone-options-dropdown' # rubocop:disable QA/ElementWithPattern
+ element :project_repository_location, 'text_field_tag :project_clone' # rubocop:disable QA/ElementWithPattern
+ end
+ end
+
+ def choose_repository_clone_http
+ choose_repository_clone('HTTP', 'http')
+ end
+
+ def choose_repository_clone_ssh
+ # It's not always beginning with ssh:// so detecting with @
+ # would be more reliable because ssh would always contain it.
+ # We can't use .git because HTTP also contain that part.
+ choose_repository_clone('SSH', '@')
+ end
+
+ def repository_location
+ Git::Location.new(find('#project_clone').value)
+ end
+
+ def wait_for_push
+ sleep 5
+ refresh
+ end
+
+ private
+
+ def choose_repository_clone(kind, detect_text)
+ wait(reload: false) do
+ click_element :clone_dropdown
+
+ page.within('.clone-options-dropdown') do
+ click_link(kind)
+ end
+
+ # Ensure git clone textbox was updated
+ repository_location.git_uri.include?(detect_text)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/merge_request/new.rb b/qa/qa/page/merge_request/new.rb
index 1f8f1fbca8e..20d9c336367 100644
--- a/qa/qa/page/merge_request/new.rb
+++ b/qa/qa/page/merge_request/new.rb
@@ -26,6 +26,10 @@ module QA
element :issuable_label
end
+ view 'app/views/shared/issuable/form/_metadata_merge_request_assignee.html.haml' do
+ element :assign_to_me_link
+ end
+
def create_merge_request
click_element :issuable_create_button
end
@@ -50,6 +54,10 @@ module QA
click_link label.title
end
+
+ def assign_to_me
+ click_element :assign_to_me_link
+ end
end
end
end
diff --git a/qa/qa/page/merge_request/show.rb b/qa/qa/page/merge_request/show.rb
index 2fd30e15ffb..869dc0b9d21 100644
--- a/qa/qa/page/merge_request/show.rb
+++ b/qa/qa/page/merge_request/show.rb
@@ -52,6 +52,7 @@ module QA
end
view 'app/views/shared/issuable/_sidebar.html.haml' do
+ element :assignee_block
element :labels_block
end
@@ -100,6 +101,12 @@ module QA
end
end
+ def has_assignee?(username)
+ page.within(element_selector_css(:assignee_block)) do
+ has_text?(username)
+ end
+ end
+
def has_label?(label)
page.within(element_selector_css(:labels_block)) do
element = find('span', text: label)
diff --git a/qa/qa/page/project/commit/show.rb b/qa/qa/page/project/commit/show.rb
new file mode 100644
index 00000000000..9770b8a657c
--- /dev/null
+++ b/qa/qa/page/project/commit/show.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module QA
+ module Page
+ module Project
+ module Commit
+ class Show < Page::Base
+ view 'app/views/projects/commit/_commit_box.html.haml' do
+ element :options_button
+ element :email_patches
+ element :plain_diff
+ end
+
+ def select_email_patches
+ click_element :options_button
+ click_element :email_patches
+ end
+
+ def select_plain_diff
+ click_element :options_button
+ click_element :plain_diff
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/project/menu.rb b/qa/qa/page/project/menu.rb
index cb4a10e1b6a..835e1ed00b5 100644
--- a/qa/qa/page/project/menu.rb
+++ b/qa/qa/page/project/menu.rb
@@ -29,11 +29,9 @@ module QA
element :fly_out, "classList.add('fly-out-list')" # rubocop:disable QA/ElementWithPattern
end
- def click_repository_settings
- hover_settings do
- within_submenu do
- click_link('Repository')
- end
+ def click_ci_cd_pipelines
+ within_sidebar do
+ click_element :link_pipelines
end
end
@@ -45,11 +43,9 @@ module QA
end
end
- def click_operations_environments
- hover_operations do
- within_submenu do
- click_element(:operations_environments_link)
- end
+ def click_issues
+ within_sidebar do
+ click_link('Issues')
end
end
@@ -61,61 +57,71 @@ module QA
end
end
- def click_operations_kubernetes
+ def click_merge_requests
+ within_sidebar do
+ click_link('Merge Requests')
+ end
+ end
+
+ def click_operations_environments
hover_operations do
within_submenu do
- click_link('Kubernetes')
+ click_element(:operations_environments_link)
end
end
end
- def click_ci_cd_pipelines
- within_sidebar do
- click_element :link_pipelines
+ def click_operations_kubernetes
+ hover_operations do
+ within_submenu do
+ click_link('Kubernetes')
+ end
end
end
- def go_to_settings
+ def click_milestones
within_sidebar do
- click_on 'Settings'
+ click_element :milestones_link
end
end
- def click_issues
+ def click_repository
within_sidebar do
- click_link('Issues')
+ click_link('Repository')
end
end
- def go_to_labels
- hover_issues do
+ def click_repository_settings
+ hover_settings do
within_submenu do
- click_element(:labels_link)
+ click_link('Repository')
end
end
end
- def click_merge_requests
+ def click_wiki
within_sidebar do
- click_link('Merge Requests')
+ click_link('Wiki')
end
end
- def click_milestones
+ def go_to_activity
within_sidebar do
- click_element :milestones_link
+ click_on 'Activity'
end
end
- def click_wiki
- within_sidebar do
- click_link('Wiki')
+ def go_to_labels
+ hover_issues do
+ within_submenu do
+ click_element(:labels_link)
+ end
end
end
- def click_repository
+ def go_to_settings
within_sidebar do
- click_link('Repository')
+ click_on 'Settings'
end
end
@@ -129,17 +135,17 @@ module QA
end
end
- def hover_settings
+ def hover_operations
within_sidebar do
- find('.qa-settings-item').hover
+ find('.shortcuts-operations').hover
yield
end
end
- def hover_operations
+ def hover_settings
within_sidebar do
- find('.shortcuts-operations').hover
+ find('.qa-settings-item').hover
yield
end
@@ -151,12 +157,6 @@ module QA
end
end
- def go_to_activity
- within_sidebar do
- click_on 'Activity'
- end
- end
-
def within_submenu
page.within('.fly-out-list') do
yield
diff --git a/qa/qa/page/project/settings/mirroring_repositories.rb b/qa/qa/page/project/settings/mirroring_repositories.rb
new file mode 100644
index 00000000000..a73be7dfeda
--- /dev/null
+++ b/qa/qa/page/project/settings/mirroring_repositories.rb
@@ -0,0 +1,91 @@
+# frozen_string_literal: true
+
+module QA
+ module Page
+ module Project
+ module Settings
+ class MirroringRepositories < Page::Base
+ view 'app/views/projects/mirrors/_authentication_method.html.haml' do
+ element :authentication_method
+ element :password
+ end
+
+ view 'app/views/projects/mirrors/_mirror_repos.html.haml' do
+ element :mirror_repository_url_input
+ element :mirror_repository_button
+ element :mirror_repository_url
+ element :mirror_last_update_at
+ element :mirrored_repository_row
+ end
+
+ view 'app/views/projects/mirrors/_mirror_repos_form.html.haml' do
+ element :mirror_direction
+ end
+
+ view 'app/views/shared/_remote_mirror_update_button.html.haml' do
+ element :update_now_button
+ end
+
+ def repository_url=(value)
+ fill_element :mirror_repository_url_input, value
+ end
+
+ def password=(value)
+ fill_element :password, value
+ end
+
+ def mirror_direction=(value)
+ raise ArgumentError, "Mirror direction must be :push or :pull" unless [:push, :pull].include? value
+
+ select_element(:mirror_direction, value)
+ end
+
+ def authentication_method=(value)
+ raise ArgumentError, "Authentication method must be :password or :none" unless [:password, :none].include? value
+
+ select_element(:authentication_method, value)
+ end
+
+ def mirror_repository
+ click_element :mirror_repository_button
+ end
+
+ def update(url)
+ row_index = find_repository_row_index url
+
+ within_element_by_index(:mirrored_repository_row, row_index) do
+ click_element :update_now_button
+ end
+
+ # Wait a few seconds for the sync to occur and then refresh the page
+ # so that 'last update' shows 'just now' or a period in seconds
+ sleep 5
+ refresh
+
+ wait(time: 1) do
+ within_element_by_index(:mirrored_repository_row, row_index) do
+ last_update = find_element(:mirror_last_update_at, wait: 0)
+ last_update.has_text?('just now') || last_update.has_text?('seconds')
+ end
+ end
+
+ # Fail early if the page still shows that there has been no update
+ within_element_by_index(:mirrored_repository_row, row_index) do
+ find_element(:mirror_last_update_at, wait: 0).assert_no_text('Never')
+ end
+ end
+
+ private
+
+ def find_repository_row_index(target_url)
+ all_elements(:mirror_repository_url).index do |url|
+ # The url might be a sanitized url but the target_url won't be so
+ # we compare just the paths instead of the full url
+ URI.parse(url.text).path == target_url.path
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/project/settings/repository.rb b/qa/qa/page/project/settings/repository.rb
index 53ebe28970b..ac0b87aca5e 100644
--- a/qa/qa/page/project/settings/repository.rb
+++ b/qa/qa/page/project/settings/repository.rb
@@ -13,6 +13,10 @@ module QA
element :protected_branches_settings
end
+ view 'app/views/projects/mirrors/_mirror_repos.html.haml' do
+ element :mirroring_repositories_settings
+ end
+
def expand_deploy_keys(&block)
expand_section(:deploy_keys_settings) do
DeployKeys.perform(&block)
@@ -30,6 +34,12 @@ module QA
DeployTokens.perform(&block)
end
end
+
+ def expand_mirroring_repositories(&block)
+ expand_section(:mirroring_repositories_settings) do
+ MirroringRepositories.perform(&block)
+ end
+ end
end
end
end
diff --git a/qa/qa/page/project/show.rb b/qa/qa/page/project/show.rb
index d6dddf03ffb..945b244df15 100644
--- a/qa/qa/page/project/show.rb
+++ b/qa/qa/page/project/show.rb
@@ -6,6 +6,11 @@ module QA
class Show < Page::Base
include Page::Component::ClonePanel
+ view 'app/views/layouts/header/_new_dropdown.haml' do
+ element :new_menu_toggle
+ element :new_issue_link, "link_to _('New issue'), new_project_issue_path(@project)" # rubocop:disable QA/ElementWithPattern
+ end
+
view 'app/views/projects/_last_push.html.haml' do
element :create_merge_request
end
@@ -14,14 +19,12 @@ module QA
element :project_name
end
- view 'app/views/layouts/header/_new_dropdown.haml' do
- element :new_menu_toggle
- element :new_issue_link, "link_to _('New issue'), new_project_issue_path(@project)" # rubocop:disable QA/ElementWithPattern
+ view 'app/views/projects/_files.html.haml' do
+ element :tree_holder, '.tree-holder' # rubocop:disable QA/ElementWithPattern
end
- view 'app/views/shared/_ref_switcher.html.haml' do
- element :branches_select
- element :branches_dropdown
+ view 'app/views/projects/buttons/_dropdown.html.haml' do
+ element :create_new_dropdown
end
view 'app/views/projects/buttons/_fork.html.haml' do
@@ -29,46 +32,57 @@ module QA
element :fork_link, "link_to new_project_fork_path(@project)" # rubocop:disable QA/ElementWithPattern
end
- view 'app/views/projects/_files.html.haml' do
- element :tree_holder, '.tree-holder' # rubocop:disable QA/ElementWithPattern
+ view 'app/views/projects/empty.html.haml' do
+ element :quick_actions
end
- view 'app/views/projects/buttons/_dropdown.html.haml' do
- element :create_new_dropdown
- element :new_file_option
+ view 'app/views/projects/tree/_tree_content.html.haml' do
+ element :file_tree
end
view 'app/views/projects/tree/_tree_header.html.haml' do
+ element :add_to_tree
+ element :new_file_option
element :web_ide_button
end
- view 'app/views/projects/tree/_tree_content.html.haml' do
- element :file_tree
+ view 'app/views/shared/_ref_switcher.html.haml' do
+ element :branches_select
+ element :branches_dropdown
end
- def project_name
- find('.qa-project-name').text
+ def create_first_new_file!
+ within_element(:quick_actions) do
+ click_link_with_text 'New file'
+ end
end
def create_new_file!
- click_element :create_new_dropdown
+ click_element :add_to_tree
click_element :new_file_option
end
+ def fork_project
+ click_on 'Fork'
+ end
+
def go_to_file(filename)
within_element(:file_tree) do
click_on filename
end
end
- def switch_to_branch(branch_name)
- find_element(:branches_select).click
-
- within_element(:branches_dropdown) do
- click_on branch_name
+ def go_to_commit(commit_msg)
+ within_element(:file_tree) do
+ click_on commit_msg
end
end
+ def go_to_new_issue
+ click_element :new_menu_toggle
+ click_link 'New issue'
+ end
+
def last_commit_content
find_element(:commit_content).text
end
@@ -81,24 +95,26 @@ module QA
click_element :create_merge_request
end
- def wait_for_import
- wait(reload: true) do
- has_css?('.tree-holder')
- end
+ def open_web_ide!
+ click_element :web_ide_button
end
- def go_to_new_issue
- click_element :new_menu_toggle
-
- click_link 'New issue'
+ def project_name
+ find('.qa-project-name').text
end
- def fork_project
- click_on 'Fork'
+ def switch_to_branch(branch_name)
+ find_element(:branches_select).click
+
+ within_element(:branches_dropdown) do
+ click_on branch_name
+ end
end
- def open_web_ide!
- click_element :web_ide_button
+ def wait_for_import
+ wait(reload: true) do
+ has_css?('.tree-holder')
+ end
end
end
end
diff --git a/qa/qa/page/project/wiki/show.rb b/qa/qa/page/project/wiki/show.rb
index a7c4455d080..dffbc5d60a2 100644
--- a/qa/qa/page/project/wiki/show.rb
+++ b/qa/qa/page/project/wiki/show.rb
@@ -5,7 +5,7 @@ module QA
module Project
module Wiki
class Show < Page::Base
- include Page::Component::ClonePanel
+ include Page::Component::LegacyClonePanel
view 'app/views/projects/wikis/pages.html.haml' do
element :clone_repository_link, 'Clone repository' # rubocop:disable QA/ElementWithPattern
diff --git a/qa/qa/resource/file.rb b/qa/qa/resource/file.rb
index effc5a7940b..57e82ac19ad 100644
--- a/qa/qa/resource/file.rb
+++ b/qa/qa/resource/file.rb
@@ -22,7 +22,7 @@ module QA
def fabricate!
project.visit!
- Page::Project::Show.perform(&:create_new_file!)
+ Page::Project::Show.perform(&:create_first_new_file!)
Page::File::Form.perform do |page|
page.add_name(@name)
diff --git a/qa/qa/resource/merge_request.rb b/qa/qa/resource/merge_request.rb
index 77afb3cfcba..7150098a00a 100644
--- a/qa/qa/resource/merge_request.rb
+++ b/qa/qa/resource/merge_request.rb
@@ -58,11 +58,15 @@ module QA
populate(:target, :source)
project.visit!
- Page::Project::Show.perform(&:new_merge_request)
+ Page::Project::Show.perform do |project|
+ project.wait_for_push
+ project.new_merge_request
+ end
Page::MergeRequest::New.perform do |page|
page.fill_title(@title)
page.fill_description(@description)
page.choose_milestone(@milestone) if @milestone
+ page.assign_to_me if @assignee == 'me'
labels.each do |label|
page.select_label(label)
end
diff --git a/qa/qa/resource/project.rb b/qa/qa/resource/project.rb
index 7fdf69278f9..1fafbf5d73e 100644
--- a/qa/qa/resource/project.rb
+++ b/qa/qa/resource/project.rb
@@ -14,15 +14,13 @@ module QA
attribute :repository_ssh_location do
Page::Project::Show.perform do |page|
- page.choose_repository_clone_ssh
- page.repository_location
+ page.repository_clone_ssh_location
end
end
attribute :repository_http_location do
Page::Project::Show.perform do |page|
- page.choose_repository_clone_http
- page.repository_location
+ page.repository_clone_http_location
end
end
diff --git a/qa/qa/resource/repository/project_push.rb b/qa/qa/resource/repository/project_push.rb
index c9fafe3419f..37feab4ad70 100644
--- a/qa/qa/resource/repository/project_push.rb
+++ b/qa/qa/resource/repository/project_push.rb
@@ -20,23 +20,11 @@ module QA
end
def repository_http_uri
- @repository_http_uri ||= begin
- project.visit!
- Page::Project::Show.act do
- choose_repository_clone_http
- repository_location.uri
- end
- end
+ @repository_http_uri ||= project.repository_http_location.uri
end
def repository_ssh_uri
- @repository_ssh_uri ||= begin
- project.visit!
- Page::Project::Show.act do
- choose_repository_clone_ssh
- repository_location.uri
- end
- end
+ @repository_ssh_uri ||= project.repository_ssh_location.uri
end
end
end
diff --git a/qa/qa/runtime/browser.rb b/qa/qa/runtime/browser.rb
index 7fd2ba25527..b706d6565d2 100644
--- a/qa/qa/runtime/browser.rb
+++ b/qa/qa/runtime/browser.rb
@@ -70,6 +70,13 @@ module QA
options.add_argument("disable-gpu")
end
+ # Use the same profile on QA runs if CHROME_REUSE_PROFILE is true.
+ # Useful to speed up local QA.
+ if QA::Runtime::Env.reuse_chrome_profile?
+ qa_profile_dir = ::File.expand_path('../../tmp/qa-profile', __dir__)
+ options.add_argument("user-data-dir=#{qa_profile_dir}")
+ end
+
# Disable /dev/shm use in CI. See https://gitlab.com/gitlab-org/gitlab-ee/issues/4252
options.add_argument("disable-dev-shm-usage") if QA::Runtime::Env.running_in_ci?
diff --git a/qa/qa/runtime/env.rb b/qa/qa/runtime/env.rb
index 3bc2b44ccd8..dae5aa3f794 100644
--- a/qa/qa/runtime/env.rb
+++ b/qa/qa/runtime/env.rb
@@ -30,6 +30,11 @@ module QA
enabled?(ENV['CHROME_HEADLESS'])
end
+ # set to 'true' to have Chrome use a fixed profile directory
+ def reuse_chrome_profile?
+ enabled?(ENV['CHROME_REUSE_PROFILE'], default: false)
+ end
+
def accept_insecure_certs?
enabled?(ENV['ACCEPT_INSECURE_CERTS'])
end
diff --git a/qa/qa/specs/features/browser_ui/1_manage/project/view_project_activity_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/project/view_project_activity_spec.rb
index 275de3d332c..d4cedc9362d 100644
--- a/qa/qa/specs/features/browser_ui/1_manage/project/view_project_activity_spec.rb
+++ b/qa/qa/specs/features/browser_ui/1_manage/project/view_project_activity_spec.rb
@@ -5,17 +5,17 @@ module QA
describe 'Project activity' do
it 'user creates an event in the activity page upon Git push' do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
- Page::Main::Login.act { sign_in_using_credentials }
+ Page::Main::Login.perform(&:sign_in_using_credentials)
- Resource::Repository::ProjectPush.fabricate! do |push|
+ project_push = Resource::Repository::ProjectPush.fabricate! do |push|
push.file_name = 'README.md'
push.file_content = '# This is a test project'
push.commit_message = 'Add README.md'
end
+ project_push.project.visit!
- Page::Project::Menu.act { go_to_activity }
-
- Page::Project::Activity.act { go_to_push_events }
+ Page::Project::Menu.perform(&:go_to_activity)
+ Page::Project::Activity.perform(&:go_to_push_events)
expect(page).to have_content('pushed new branch master')
end
diff --git a/qa/qa/specs/features/browser_ui/3_create/merge_request/create_merge_request_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/create_merge_request_spec.rb
index d33947f41da..6ddd7dde2cf 100644
--- a/qa/qa/specs/features/browser_ui/3_create/merge_request/create_merge_request_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/merge_request/create_merge_request_spec.rb
@@ -4,6 +4,8 @@ module QA
context 'Create' do
describe 'Merge request creation' do
it 'user creates a new merge request' do
+ gitlab_account_username = "@#{Runtime::User.username}"
+
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
@@ -27,6 +29,7 @@ module QA
merge_request.description = 'Great feature with milestone'
merge_request.project = current_project
merge_request.milestone = current_milestone
+ merge_request.assignee = 'me'
merge_request.labels.push(new_label)
end
@@ -34,6 +37,7 @@ module QA
expect(merge_request).to have_content('This is a merge request with a milestone')
expect(merge_request).to have_content('Great feature with milestone')
expect(merge_request).to have_content(/Opened [\w\s]+ ago/)
+ expect(merge_request).to have_assignee(gitlab_account_username)
expect(merge_request).to have_label(new_label.title)
end
diff --git a/qa/qa/specs/features/browser_ui/3_create/merge_request/squash_merge_request_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/squash_merge_request_spec.rb
index 6ff7360c413..4126f967ee2 100644
--- a/qa/qa/specs/features/browser_ui/3_create/merge_request/squash_merge_request_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/merge_request/squash_merge_request_spec.rb
@@ -5,7 +5,7 @@ module QA
describe 'Merge request squashing' do
it 'user squashes commits while merging' do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
- Page::Main::Login.act { sign_in_using_credentials }
+ Page::Main::Login.perform(&:sign_in_using_credentials)
project = Resource::Project.fabricate! do |project|
project.name = "squash-before-merge"
@@ -38,13 +38,12 @@ module QA
Git::Repository.perform do |repository|
repository.uri = Page::Project::Show.act do
- choose_repository_clone_http
- repository_location.uri
+ repository_clone_http_location.uri
end
repository.use_default_credentials
- repository.act { clone }
+ repository.clone
expect(repository.commits.size).to eq 3
end
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/add_file_template_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/add_file_template_spec.rb
index 297485dd81e..de5c535c757 100644
--- a/qa/qa/specs/features/browser_ui/3_create/repository/add_file_template_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/add_file_template_spec.rb
@@ -7,7 +7,7 @@ module QA
def login
Runtime::Browser.visit(:gitlab, Page::Main::Login)
- Page::Main::Login.act { sign_in_using_credentials }
+ Page::Main::Login.perform(&:sign_in_using_credentials)
end
before(:all) do
@@ -18,7 +18,15 @@ module QA
project.description = 'Add file templates via the Files view'
end
- Page::Main::Menu.act { sign_out }
+ # There's no 'New File' dropdown when the project is blank, so we first
+ # add a dummy file so that the dropdown will appear
+ Resource::File.fabricate! do |file|
+ file.project = @project
+ file.name = 'README.md'
+ file.content = '# Readme'
+ end
+
+ Page::Main::Menu.perform(&:sign_out)
end
templates = [
@@ -55,7 +63,7 @@ module QA
login
@project.visit!
- Page::Project::Show.act { create_new_file! }
+ Page::Project::Show.perform(&:create_new_file!)
Page::File::Form.perform do |page|
page.select_template template[:file_name], template[:name]
end
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/clone_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/clone_spec.rb
index 6a0add56fe0..571cae4a3c5 100644
--- a/qa/qa/specs/features/browser_ui/3_create/repository/clone_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/clone_spec.rb
@@ -4,15 +4,12 @@ module QA
context 'Create' do
describe 'Git clone over HTTP', :ldap_no_tls do
let(:location) do
- Page::Project::Show.act do
- choose_repository_clone_http
- repository_location
- end
+ Page::Project::Show.perform(&:repository_clone_http_location).uri
end
before do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
- Page::Main::Login.act { sign_in_using_credentials }
+ Page::Main::Login.perform(&:sign_in_using_credentials)
project = Resource::Project.fabricate! do |scenario|
scenario.name = 'project-with-code'
@@ -21,7 +18,7 @@ module QA
project.visit!
Git::Repository.perform do |repository|
- repository.uri = location.uri
+ repository.uri = location
repository.use_default_credentials
repository.act do
@@ -32,14 +29,15 @@ module QA
push_changes
end
end
+ Page::Project::Show.perform(&:wait_for_push)
end
it 'user performs a deep clone' do
Git::Repository.perform do |repository|
- repository.uri = location.uri
+ repository.uri = location
repository.use_default_credentials
- repository.act { clone }
+ repository.clone
expect(repository.commits.size).to eq 2
end
@@ -47,10 +45,10 @@ module QA
it 'user performs a shallow clone' do
Git::Repository.perform do |repository|
- repository.uri = location.uri
+ repository.uri = location
repository.use_default_credentials
- repository.act { shallow_clone }
+ repository.shallow_clone
expect(repository.commits.size).to eq 1
expect(repository.commits.first).to include 'Add Readme'
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/push_mirroring_over_http_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_mirroring_over_http_spec.rb
new file mode 100644
index 00000000000..2d0e281ab59
--- /dev/null
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_mirroring_over_http_spec.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+module QA
+ context 'Create' do
+ describe 'Push mirror a repository over HTTP' do
+ it 'configures and syncs a (push) mirrored repository' do
+ Runtime::Browser.visit(:gitlab, Page::Main::Login)
+ Page::Main::Login.perform(&:sign_in_using_credentials)
+
+ target_project = Resource::Project.fabricate! do |project|
+ project.name = 'push-mirror-target-project'
+ end
+ target_project_uri = target_project.repository_http_location.uri
+ target_project_uri.user = Runtime::User.username
+
+ source_project_push = Resource::Repository::ProjectPush.fabricate! do |push|
+ push.file_name = 'README.md'
+ push.file_content = '# This is a test project'
+ push.commit_message = 'Add README.md'
+ end
+ source_project_push.project.visit!
+
+ Page::Project::Show.perform(&:wait_for_push)
+
+ Page::Project::Menu.perform(&:click_repository_settings)
+ Page::Project::Settings::Repository.perform do |settings|
+ settings.expand_mirroring_repositories do |mirror_settings|
+ # Configure the source project to push to the target project
+ mirror_settings.repository_url = target_project_uri
+ mirror_settings.mirror_direction = :push
+ mirror_settings.authentication_method = :password
+ mirror_settings.password = Runtime::User.password
+ mirror_settings.mirror_repository
+ mirror_settings.update target_project_uri
+ end
+ end
+
+ # Check that the target project has the commit from the source
+ target_project.visit!
+ expect(page).to have_content('README.md')
+ expect(page).to have_content('This is a test project')
+ end
+ end
+ end
+end
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb
index 92f596a44d9..ad6426df420 100644
--- a/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb
@@ -7,12 +7,12 @@ module QA
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
- Resource::Repository::ProjectPush.fabricate! do |push|
+ project_push = Resource::Repository::ProjectPush.fabricate! do |push|
push.file_name = 'README.md'
push.file_content = '# This is a test project'
push.commit_message = 'Add README.md'
end
-
+ project_push.project.visit!
Page::Project::Show.act { wait_for_push }
expect(page).to have_content('README.md')
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/use_ssh_key_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/use_ssh_key_spec.rb
index 9c764424129..509a639c130 100644
--- a/qa/qa/specs/features/browser_ui/3_create/repository/use_ssh_key_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/use_ssh_key_spec.rb
@@ -16,13 +16,14 @@ module QA
resource.title = key_title
end
- Resource::Repository::ProjectPush.fabricate! do |push|
+ project_push = Resource::Repository::ProjectPush.fabricate! do |push|
push.ssh_key = key
push.file_name = 'README.md'
push.file_content = '# Test Use SSH Key'
push.commit_message = 'Add README.md'
end
+ project_push.project.visit!
Page::Project::Show.act { wait_for_push }
expect(page).to have_content('README.md')
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/user_views_raw_diff_patch_requests_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/user_views_raw_diff_patch_requests_spec.rb
new file mode 100644
index 00000000000..75ad18a4111
--- /dev/null
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/user_views_raw_diff_patch_requests_spec.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+module QA
+ context 'Create' do
+ describe 'Commit data' do
+ before(:context) do
+ Runtime::Browser.visit(:gitlab, Page::Main::Login)
+ Page::Main::Login.perform(&:sign_in_using_credentials)
+
+ @project = Resource::Repository::ProjectPush.fabricate! do |push|
+ push.file_name = 'README.md'
+ push.file_content = '# This is a test project'
+ push.commit_message = 'Add README.md'
+ end
+
+ # first file added has no parent commit, thus no diff data
+ # add second file to repo to enable diff from initial commit
+ @commit_message = 'Add second file'
+
+ @project.visit!
+ Page::Project::Show.perform(&:create_new_file!)
+ Page::File::Form.perform do |f|
+ f.add_name('second')
+ f.add_content('second file content')
+ f.add_commit_message(@commit_message)
+ f.commit_changes
+ end
+ end
+
+ def view_commit
+ @project.visit!
+ Page::Project::Show.perform do |page|
+ page.go_to_commit(@commit_message)
+ end
+ end
+
+ def raw_content
+ find('pre').text
+ end
+
+ it 'user views raw email patch' do
+ view_commit
+
+ Page::Project::Commit::Show.perform(&:select_email_patches)
+
+ expect(page).to have_content('From: Administrator <admin@example.com>')
+ expect(page).to have_content('Subject: [PATCH] Add second file')
+ expect(page).to have_content('diff --git a/second b/second')
+ end
+
+ it 'user views raw commit diff' do
+ view_commit
+
+ Page::Project::Commit::Show.perform(&:select_plain_diff)
+
+ expect(raw_content).to start_with('diff --git a/second b/second')
+ expect(page).to have_content('+second file content')
+ end
+ end
+ end
+end
diff --git a/qa/qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb b/qa/qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb
index e7374377104..f176ec31abd 100644
--- a/qa/qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb
@@ -7,7 +7,7 @@ module QA
def login
Runtime::Browser.visit(:gitlab, Page::Main::Login)
- Page::Main::Login.act { sign_in_using_credentials }
+ Page::Main::Login.perform(&:sign_in_using_credentials)
end
before(:all) do
@@ -21,14 +21,14 @@ module QA
# Add a file via the regular Files view because the Web IDE isn't
# available unless there is a file present
- Page::Project::Show.act { create_new_file! }
+ Page::Project::Show.perform(&:create_first_new_file!)
Page::File::Form.perform do |page|
page.add_name('dummy')
page.add_content('Enable the Web IDE')
page.commit_changes
end
- Page::Main::Menu.act { sign_out }
+ Page::Main::Menu.perform(&:sign_out)
end
templates = [
@@ -65,7 +65,7 @@ module QA
login
@project.visit!
- Page::Project::Show.act { open_web_ide! }
+ Page::Project::Show.perform(&:open_web_ide!)
Page::Project::WebIDE::Edit.perform do |page|
page.create_new_file_from_template template[:file_name], template[:name]
@@ -75,9 +75,7 @@ module QA
expect(page).to have_button('Undo')
expect(page).to have_content(content[0..100])
- Page::Project::WebIDE::Edit.perform do |page|
- page.commit_changes
- end
+ Page::Project::WebIDE::Edit.perform(&:commit_changes)
expect(page).to have_content(template[:file_name])
expect(page).to have_content(content[0..100])
diff --git a/qa/qa/support/page/logging.rb b/qa/qa/support/page/logging.rb
index cf5cd3a79f8..43bc16d8c9a 100644
--- a/qa/qa/support/page/logging.rb
+++ b/qa/qa/support/page/logging.rb
@@ -37,8 +37,8 @@ module QA
exists
end
- def find_element(name)
- log("finding :#{name}")
+ def find_element(name, wait: Capybara.default_max_wait_time)
+ log("finding :#{name} (wait: #{wait})")
element = super
@@ -71,6 +71,12 @@ module QA
super
end
+ def select_element(name, value)
+ log(%Q(selecting "#{value}" in :#{name}))
+
+ super
+ end
+
def has_element?(name)
found = super
@@ -89,6 +95,16 @@ module QA
element
end
+ def within_element_by_index(name, index)
+ log("within elements :#{name} at index #{index}")
+
+ element = super
+
+ log("end within elements :#{name} at index #{index}")
+
+ element
+ end
+
private
def log(msg)
diff --git a/scripts/review_apps/review-apps.sh b/scripts/review_apps/review-apps.sh
index f3f788e0217..b50bf2161cb 100755
--- a/scripts/review_apps/review-apps.sh
+++ b/scripts/review_apps/review-apps.sh
@@ -289,8 +289,8 @@ function get_job_id() {
local url="https://gitlab.com/api/v4/projects/${CI_PROJECT_ID}/pipelines/${CI_PIPELINE_ID}/jobs?per_page=100&page=${page}${query_string}"
echoerr "GET ${url}"
- local job_id=$(curl --silent --show-error --header "PRIVATE-TOKEN: ${API_TOKEN}" "${url}" | jq ".[] | select(.name == \"${job_name}\") | .id")
- [[ "${job_id}" == "" && "${page}" -lt "$max_page" ]] || break
+ local job_id=$(curl --silent --show-error --header "PRIVATE-TOKEN: ${API_TOKEN}" "${url}" | jq "map(select(.name == \"${job_name}\")) | map(.id) | last")
+ [[ "${job_id}" == "null" && "${page}" -lt "$max_page" ]] || break
((page++))
done
@@ -328,17 +328,18 @@ function wait_for_job_to_be_done() {
# In case the job hasn't finished yet. Keep trying until the job times out.
local interval=30
- local elapsed=0
+ local elapsed_seconds=0
while true; do
local job_status=$(curl --silent --show-error --header "PRIVATE-TOKEN: ${API_TOKEN}" "${url}" | jq ".status" | sed -e s/\"//g)
[[ "${job_status}" == "pending" || "${job_status}" == "running" ]] || break
printf "."
- ((elapsed+=$interval))
+ ((elapsed_seconds+=$interval))
sleep ${interval}
done
- echoerr "Waited '${job_name}' for ${elapsed} seconds."
+ local elapsed_minutes=$((elapsed_seconds / 60))
+ echoerr "Waited '${job_name}' for ${elapsed_minutes} minutes."
if [[ "${job_status}" == "failed" ]]; then
echo "The '${job_name}' failed."
diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb
index ac92b2ca657..c2bd7fd9808 100644
--- a/spec/controllers/application_controller_spec.rb
+++ b/spec/controllers/application_controller_spec.rb
@@ -460,6 +460,14 @@ describe ApplicationController do
expect(controller.last_payload.has_key?(:response)).to be_falsey
end
+ it 'does log correlation id' do
+ Gitlab::CorrelationId.use_id('new-id') do
+ get :index
+ end
+
+ expect(controller.last_payload).to include('correlation_id' => 'new-id')
+ end
+
context '422 errors' do
it 'logs a response with a string' do
response = spy(ActionDispatch::Response, status: 422, body: 'Hello world', content_type: 'application/json', cookies: {})
diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb
index f6c85102830..4b0dc4c9b69 100644
--- a/spec/controllers/groups_controller_spec.rb
+++ b/spec/controllers/groups_controller_spec.rb
@@ -226,9 +226,10 @@ describe GroupsController do
end
context 'searching' do
- # Remove as part of https://gitlab.com/gitlab-org/gitlab-ce/issues/52271
before do
+ # Remove in https://gitlab.com/gitlab-org/gitlab-ce/issues/54643
stub_feature_flags(use_cte_for_group_issues_search: false)
+ stub_feature_flags(use_subquery_for_group_issues_search: true)
end
it 'works with popularity sort' do
diff --git a/spec/controllers/import/github_controller_spec.rb b/spec/controllers/import/github_controller_spec.rb
index 9bbd97ec305..780e49f7b93 100644
--- a/spec/controllers/import/github_controller_spec.rb
+++ b/spec/controllers/import/github_controller_spec.rb
@@ -16,6 +16,15 @@ describe Import::GithubController do
get :new
end
+
+ it "prompts for an access token if GitHub not configured" do
+ allow(controller).to receive(:github_import_configured?).and_return(false)
+ expect(controller).not_to receive(:go_to_provider_for_permissions)
+
+ get :new
+
+ expect(response).to have_http_status(200)
+ end
end
describe "GET callback" do
diff --git a/spec/controllers/projects/avatars_controller_spec.rb b/spec/controllers/projects/avatars_controller_spec.rb
index 14059cff74c..5a77a7ac06f 100644
--- a/spec/controllers/projects/avatars_controller_spec.rb
+++ b/spec/controllers/projects/avatars_controller_spec.rb
@@ -26,12 +26,37 @@ describe Projects::AvatarsController do
context 'when the avatar is stored in the repository' do
let(:filepath) { 'files/images/logo-white.png' }
- it 'sends the avatar' do
- subject
+ context 'when feature flag workhorse_set_content_type is' do
+ before do
+ stub_feature_flags(workhorse_set_content_type: flag_value)
+ end
- expect(response).to have_gitlab_http_status(200)
- expect(response.header['Content-Type']).to eq('image/png')
- expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with('git-blob:')
+ context 'enabled' do
+ let(:flag_value) { true }
+
+ it 'sends the avatar' do
+ subject
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response.header['Content-Disposition']).to eq('inline')
+ expect(response.header['Content-Type']).to eq 'image/png'
+ expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with('git-blob:')
+ expect(response.header[Gitlab::Workhorse::DETECT_HEADER]).to eq "true"
+ end
+ end
+
+ context 'disabled' do
+ let(:flag_value) { false }
+
+ it 'sends the avatar' do
+ subject
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response.header['Content-Type']).to eq('image/png')
+ expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with('git-blob:')
+ expect(response.header[Gitlab::Workhorse::DETECT_HEADER]).to eq nil
+ end
+ end
end
end
diff --git a/spec/controllers/projects/commits_controller_spec.rb b/spec/controllers/projects/commits_controller_spec.rb
index 5c72dab698c..80513650636 100644
--- a/spec/controllers/projects/commits_controller_spec.rb
+++ b/spec/controllers/projects/commits_controller_spec.rb
@@ -53,6 +53,12 @@ describe Projects::CommitsController do
it { is_expected.to respond_with(:not_found) }
end
+
+ context "branch with invalid format, valid file" do
+ let(:id) { 'branch with space/README.md' }
+
+ it { is_expected.to respond_with(:not_found) }
+ end
end
context "when the ref name ends in .atom" do
@@ -94,6 +100,30 @@ describe Projects::CommitsController do
end
end
end
+
+ describe "GET /commits/:id/signatures" do
+ render_views
+
+ before do
+ get(:signatures,
+ namespace_id: project.namespace,
+ project_id: project,
+ id: id,
+ format: :json)
+ end
+
+ context "valid branch" do
+ let(:id) { 'master' }
+
+ it { is_expected.to respond_with(:success) }
+ end
+
+ context "invalid branch format" do
+ let(:id) { 'some branch' }
+
+ it { is_expected.to respond_with(:not_found) }
+ end
+ end
end
context 'token authentication' do
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index 02930edbf72..6240ab6d867 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -42,6 +42,8 @@ describe Projects::IssuesController do
it_behaves_like "issuables list meta-data", :issue
+ it_behaves_like 'set sort order from user preference'
+
it "returns index" do
get :index, namespace_id: project.namespace, project_id: project
diff --git a/spec/controllers/projects/jobs_controller_spec.rb b/spec/controllers/projects/jobs_controller_spec.rb
index da3d658d061..fca313dafb1 100644
--- a/spec/controllers/projects/jobs_controller_spec.rb
+++ b/spec/controllers/projects/jobs_controller_spec.rb
@@ -401,18 +401,56 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
context 'with variables' do
before do
create(:ci_pipeline_variable, pipeline: pipeline, key: :TRIGGER_KEY_1, value: 'TRIGGER_VALUE_1')
+ end
- get_show(id: job.id, format: :json)
+ context 'user is a maintainer' do
+ before do
+ project.add_maintainer(user)
+
+ get_show(id: job.id, format: :json)
+ end
+
+ it 'returns a job_detail' do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('job/job_details')
+ end
+
+ it 'exposes trigger information and variables' do
+ expect(json_response['trigger']['short_token']).to eq 'toke'
+ expect(json_response['trigger']['variables'].length).to eq 1
+ end
+
+ it 'exposes correct variable properties' do
+ first_variable = json_response['trigger']['variables'].first
+
+ expect(first_variable['key']).to eq "TRIGGER_KEY_1"
+ expect(first_variable['value']).to eq "TRIGGER_VALUE_1"
+ expect(first_variable['public']).to eq false
+ end
end
- it 'exposes trigger information and variables' do
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to match_response_schema('job/job_details')
- expect(json_response['trigger']['short_token']).to eq 'toke'
- expect(json_response['trigger']['variables'].length).to eq 1
- expect(json_response['trigger']['variables'].first['key']).to eq "TRIGGER_KEY_1"
- expect(json_response['trigger']['variables'].first['value']).to eq "TRIGGER_VALUE_1"
- expect(json_response['trigger']['variables'].first['public']).to eq false
+ context 'user is not a mantainer' do
+ before do
+ get_show(id: job.id, format: :json)
+ end
+
+ it 'returns a job_detail' do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('job/job_details')
+ end
+
+ it 'exposes trigger information and variables' do
+ expect(json_response['trigger']['short_token']).to eq 'toke'
+ expect(json_response['trigger']['variables'].length).to eq 1
+ end
+
+ it 'exposes correct variable properties' do
+ first_variable = json_response['trigger']['variables'].first
+
+ expect(first_variable['key']).to eq "TRIGGER_KEY_1"
+ expect(first_variable['value']).to be_nil
+ expect(first_variable['public']).to eq false
+ end
end
end
end
@@ -838,23 +876,48 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
context "when job has a trace artifact" do
let(:job) { create(:ci_build, :trace_artifact, pipeline: pipeline) }
- it 'returns a trace' do
- response = subject
+ context 'when feature flag workhorse_set_content_type is' do
+ before do
+ stub_feature_flags(workhorse_set_content_type: flag_value)
+ end
- expect(response).to have_gitlab_http_status(:ok)
- expect(response.headers["Content-Type"]).to eq("text/plain; charset=utf-8")
- expect(response.body).to eq(job.job_artifacts_trace.open.read)
+ context 'enabled' do
+ let(:flag_value) { true }
+
+ it "sets #{Gitlab::Workhorse::DETECT_HEADER} header" do
+ response = subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.headers["Content-Type"]).to eq("text/plain; charset=utf-8")
+ expect(response.body).to eq(job.job_artifacts_trace.open.read)
+ expect(response.header[Gitlab::Workhorse::DETECT_HEADER]).to eq "true"
+ end
+ end
+
+ context 'disabled' do
+ let(:flag_value) { false }
+
+ it 'returns a trace' do
+ response = subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.headers["Content-Type"]).to eq("text/plain; charset=utf-8")
+ expect(response.body).to eq(job.job_artifacts_trace.open.read)
+ expect(response.header[Gitlab::Workhorse::DETECT_HEADER]).to be nil
+ end
+ end
end
end
context "when job has a trace file" do
let(:job) { create(:ci_build, :trace_live, pipeline: pipeline) }
- it "send a trace file" do
+ it 'sends a trace file' do
response = subject
expect(response).to have_gitlab_http_status(:ok)
expect(response.headers["Content-Type"]).to eq("text/plain; charset=utf-8")
+ expect(response.headers["Content-Disposition"]).to match(/^inline/)
expect(response.body).to eq("BUILD TRACE")
end
end
@@ -866,12 +929,27 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
job.update_column(:trace, "Sample trace")
end
- it "send a trace file" do
+ it 'sends a trace file' do
response = subject
expect(response).to have_gitlab_http_status(:ok)
- expect(response.headers["Content-Type"]).to eq("text/plain; charset=utf-8")
- expect(response.body).to eq("Sample trace")
+ expect(response.headers['Content-Type']).to eq('text/plain; charset=utf-8')
+ expect(response.headers['Content-Disposition']).to match(/^inline/)
+ expect(response.body).to eq('Sample trace')
+ end
+
+ context 'when trace format is not text/plain' do
+ before do
+ job.update_column(:trace, '<html></html>')
+ end
+
+ it 'sets content disposition to attachment' do
+ response = subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.headers['Content-Type']).to eq('text/plain; charset=utf-8')
+ expect(response.headers['Content-Disposition']).to match(/^attachment/)
+ end
end
end
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index e62523c65c9..a37a831ddbb 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -160,6 +160,8 @@ describe Projects::MergeRequestsController do
it_behaves_like "issuables list meta-data", :merge_request
+ it_behaves_like 'set sort order from user preference'
+
context 'when page param' do
let(:last_page) { project.merge_requests.page().total_pages }
let!(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) }
@@ -290,6 +292,20 @@ describe Projects::MergeRequestsController do
it_behaves_like 'update invalid issuable', MergeRequest
end
+
+ context 'two merge requests with the same source branch' do
+ it 'does not allow a closed merge request to be reopened if another one is open' do
+ merge_request.close!
+ create(:merge_request, source_project: merge_request.source_project, source_branch: merge_request.source_branch)
+
+ update_merge_request(state_event: 'reopen')
+
+ errors = assigns[:merge_request].errors
+
+ expect(errors[:validate_branches]).to include(/Another open merge request already exists for this source branch/)
+ expect(merge_request.reload).to be_closed
+ end
+ end
end
describe 'POST merge' do
diff --git a/spec/controllers/projects/raw_controller_spec.rb b/spec/controllers/projects/raw_controller_spec.rb
index 6b658bf5295..d3cd15fbcd7 100644
--- a/spec/controllers/projects/raw_controller_spec.rb
+++ b/spec/controllers/projects/raw_controller_spec.rb
@@ -14,26 +14,74 @@ describe Projects::RawController do
context 'regular filename' do
let(:filepath) { 'master/README.md' }
- it 'delivers ASCII file' do
- subject
-
- expect(response).to have_gitlab_http_status(200)
- expect(response.header['Content-Type']).to eq('text/plain; charset=utf-8')
- expect(response.header['Content-Disposition'])
- .to eq('inline')
- expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with('git-blob:')
+ context 'when feature flag workhorse_set_content_type is' do
+ before do
+ stub_feature_flags(workhorse_set_content_type: flag_value)
+
+ subject
+ end
+
+ context 'enabled' do
+ let(:flag_value) { true }
+
+ it 'delivers ASCII file' do
+ expect(response).to have_gitlab_http_status(200)
+ expect(response.header['Content-Type']).to eq('text/plain; charset=utf-8')
+ expect(response.header['Content-Disposition']).to eq('inline')
+ expect(response.header[Gitlab::Workhorse::DETECT_HEADER]).to eq "true"
+ expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with('git-blob:')
+ end
+ end
+
+ context 'disabled' do
+ let(:flag_value) { false }
+
+ it 'delivers ASCII file' do
+ expect(response).to have_gitlab_http_status(200)
+ expect(response.header['Content-Type']).to eq('text/plain; charset=utf-8')
+ expect(response.header['Content-Disposition']).to eq('inline')
+ expect(response.header[Gitlab::Workhorse::DETECT_HEADER]).to eq nil
+ expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with('git-blob:')
+ end
+ end
end
end
context 'image header' do
let(:filepath) { 'master/files/images/6049019_460s.jpg' }
- it 'sets image content type header' do
- subject
+ context 'when feature flag workhorse_set_content_type is' do
+ before do
+ stub_feature_flags(workhorse_set_content_type: flag_value)
+ end
+
+ context 'enabled' do
+ let(:flag_value) { true }
+
+ it 'leaves image content disposition' do
+ subject
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response.header['Content-Type']).to eq('image/jpeg')
+ expect(response.header['Content-Disposition']).to eq('inline')
+ expect(response.header[Gitlab::Workhorse::DETECT_HEADER]).to eq "true"
+ expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with('git-blob:')
+ end
+ end
+
+ context 'disabled' do
+ let(:flag_value) { false }
+
+ it 'sets image content type header' do
+ subject
- expect(response).to have_gitlab_http_status(200)
- expect(response.header['Content-Type']).to eq('image/jpeg')
- expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with('git-blob:')
+ expect(response).to have_gitlab_http_status(200)
+ expect(response.header['Content-Type']).to eq('image/jpeg')
+ expect(response.header['Content-Disposition']).to eq('inline')
+ expect(response.header[Gitlab::Workhorse::DETECT_HEADER]).to eq nil
+ expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with('git-blob:')
+ end
+ end
end
end
diff --git a/spec/controllers/projects/serverless/functions_controller_spec.rb b/spec/controllers/projects/serverless/functions_controller_spec.rb
new file mode 100644
index 00000000000..284b582b1f5
--- /dev/null
+++ b/spec/controllers/projects/serverless/functions_controller_spec.rb
@@ -0,0 +1,72 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Projects::Serverless::FunctionsController do
+ include KubernetesHelpers
+ include ReactiveCachingHelpers
+
+ let(:user) { create(:user) }
+ let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+ let(:knative) { create(:clusters_applications_knative, :installed, cluster: cluster) }
+ let(:service) { cluster.platform_kubernetes }
+ let(:project) { cluster.project}
+
+ let(:namespace) do
+ create(:cluster_kubernetes_namespace,
+ cluster: cluster,
+ cluster_project: cluster.cluster_project,
+ project: cluster.cluster_project.project)
+ end
+
+ before do
+ project.add_maintainer(user)
+ sign_in(user)
+ end
+
+ def params(opts = {})
+ opts.reverse_merge(namespace_id: project.namespace.to_param,
+ project_id: project.to_param)
+ end
+
+ describe 'GET #index' do
+ context 'empty cache' do
+ it 'has no data' do
+ get :index, params({ format: :json })
+
+ expect(response).to have_gitlab_http_status(204)
+ end
+
+ it 'renders an html page' do
+ get :index, params
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+ end
+ end
+
+ describe 'GET #index with data', :use_clean_rails_memory_store_caching do
+ before do
+ stub_reactive_cache(knative, services: kube_knative_services_body(namespace: namespace.namespace, name: cluster.project.name)["items"])
+ end
+
+ it 'has data' do
+ get :index, params({ format: :json })
+
+ expect(response).to have_gitlab_http_status(200)
+
+ expect(json_response).to contain_exactly(
+ a_hash_including(
+ "name" => project.name,
+ "url" => "http://#{project.name}.#{namespace.namespace}.example.com"
+ )
+ )
+ end
+
+ it 'has data in html' do
+ get :index, params
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+ end
+end
diff --git a/spec/controllers/projects/settings/repository_controller_spec.rb b/spec/controllers/projects/settings/repository_controller_spec.rb
index 9cee40b7553..70f79a47e63 100644
--- a/spec/controllers/projects/settings/repository_controller_spec.rb
+++ b/spec/controllers/projects/settings/repository_controller_spec.rb
@@ -17,4 +17,37 @@ describe Projects::Settings::RepositoryController do
expect(response).to render_template(:show)
end
end
+
+ describe 'PUT cleanup' do
+ before do
+ allow(RepositoryCleanupWorker).to receive(:perform_async)
+ end
+
+ def do_put!
+ object_map = fixture_file_upload('spec/fixtures/bfg_object_map.txt')
+
+ put :cleanup, namespace_id: project.namespace, project_id: project, project: { object_map: object_map }
+ end
+
+ context 'feature enabled' do
+ it 'enqueues a RepositoryCleanupWorker' do
+ stub_feature_flags(project_cleanup: true)
+
+ do_put!
+
+ expect(response).to redirect_to project_settings_repository_path(project)
+ expect(RepositoryCleanupWorker).to have_received(:perform_async).once
+ end
+ end
+
+ context 'feature disabled' do
+ it 'shows a 404 error' do
+ stub_feature_flags(project_cleanup: false)
+
+ do_put!
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+ end
end
diff --git a/spec/controllers/projects/wikis_controller_spec.rb b/spec/controllers/projects/wikis_controller_spec.rb
index 6d75152857b..b974d927856 100644
--- a/spec/controllers/projects/wikis_controller_spec.rb
+++ b/spec/controllers/projects/wikis_controller_spec.rb
@@ -52,24 +52,56 @@ describe Projects::WikisController do
let(:path) { upload_file_to_wiki(project, user, file_name) }
- before do
- subject
- end
-
subject { get :show, namespace_id: project.namespace, project_id: project, id: path }
context 'when file is an image' do
let(:file_name) { 'dk.png' }
- it 'renders the content inline' do
- expect(response.headers['Content-Disposition']).to match(/^inline/)
- end
+ context 'when feature flag workhorse_set_content_type is' do
+ before do
+ stub_feature_flags(workhorse_set_content_type: flag_value)
+
+ subject
+ end
- context 'when file is a svg' do
- let(:file_name) { 'unsanitized.svg' }
+ context 'enabled' do
+ let(:flag_value) { true }
- it 'renders the content as an attachment' do
- expect(response.headers['Content-Disposition']).to match(/^attachment/)
+ it 'delivers the image' do
+ expect(response.headers['Content-Type']).to eq('image/png')
+ expect(response.headers['Content-Disposition']).to match(/^inline/)
+ expect(response.headers[Gitlab::Workhorse::DETECT_HEADER]).to eq "true"
+ end
+
+ context 'when file is a svg' do
+ let(:file_name) { 'unsanitized.svg' }
+
+ it 'delivers the image' do
+ expect(response.headers['Content-Type']).to eq('image/svg+xml')
+ expect(response.headers['Content-Disposition']).to match(/^attachment/)
+ expect(response.headers[Gitlab::Workhorse::DETECT_HEADER]).to eq "true"
+ end
+ end
+ end
+
+ context 'disabled' do
+ let(:flag_value) { false }
+
+ it 'renders the content inline' do
+ expect(response.headers['Content-Type']).to eq('image/png')
+ expect(response.headers['Content-Disposition']).to match(/^inline/)
+ expect(response.headers[Gitlab::Workhorse::DETECT_HEADER]).to eq nil
+ end
+
+ context 'when file is a svg' do
+ let(:file_name) { 'unsanitized.svg' }
+
+ it 'renders the content as an attachment' do
+ expect(response.headers['Content-Type']).to eq('image/svg+xml')
+ expect(response.headers['Content-Disposition']).to match(/^attachment/)
+ expect(response.headers[Gitlab::Workhorse::DETECT_HEADER]).to eq nil
+ end
+ end
end
end
end
@@ -77,8 +109,32 @@ describe Projects::WikisController do
context 'when file is a pdf' do
let(:file_name) { 'git-cheat-sheet.pdf' }
- it 'sets the content type to application/octet-stream' do
- expect(response.headers['Content-Type']).to eq 'application/octet-stream'
+ context 'when feature flag workhorse_set_content_type is' do
+ before do
+ stub_feature_flags(workhorse_set_content_type: flag_value)
+
+ subject
+ end
+
+ context 'enabled' do
+ let(:flag_value) { true }
+
+ it 'sets the content type to sets the content response headers' do
+ expect(response.headers['Content-Type']).to eq 'application/octet-stream'
+ expect(response.headers['Content-Disposition']).to match(/^inline/)
+ expect(response.headers[Gitlab::Workhorse::DETECT_HEADER]).to eq "true"
+ end
+ end
+
+ context 'disabled' do
+ let(:flag_value) { false }
+
+ it 'sets the content response headers' do
+ expect(response.headers['Content-Type']).to eq 'application/octet-stream'
+ expect(response.headers['Content-Disposition']).to match(/^inline/)
+ expect(response.headers[Gitlab::Workhorse::DETECT_HEADER]).to eq nil
+ end
+ end
end
end
end
diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb
index 7849bec4762..576191a5788 100644
--- a/spec/controllers/projects_controller_spec.rb
+++ b/spec/controllers/projects_controller_spec.rb
@@ -279,7 +279,7 @@ describe ProjectsController do
expected_query = /#{public_project.fork_network.find_forks_in(other_user.namespace).to_sql}/
expect { get(:show, namespace_id: public_project.namespace, id: public_project) }
- .not_to exceed_query_limit(1).for_query(expected_query)
+ .not_to exceed_query_limit(2).for_query(expected_query)
end
end
end
diff --git a/spec/controllers/snippets_controller_spec.rb b/spec/controllers/snippets_controller_spec.rb
index 9effe47ab05..957bab638b1 100644
--- a/spec/controllers/snippets_controller_spec.rb
+++ b/spec/controllers/snippets_controller_spec.rb
@@ -437,7 +437,10 @@ describe SnippetsController do
end
context 'when signed in user is the author' do
+ let(:flag_value) { false }
+
before do
+ stub_feature_flags(workhorse_set_content_type: flag_value)
get :raw, id: personal_snippet.to_param
end
@@ -451,6 +454,24 @@ describe SnippetsController do
expect(response.header['Content-Disposition']).to match(/inline/)
end
+
+ context 'when feature flag workhorse_set_content_type is' do
+ context 'enabled' do
+ let(:flag_value) { true }
+
+ it "sets #{Gitlab::Workhorse::DETECT_HEADER} header" do
+ expect(response).to have_gitlab_http_status(200)
+ expect(response.header[Gitlab::Workhorse::DETECT_HEADER]).to eq "true"
+ end
+ end
+
+ context 'disabled' do
+ it "does not set #{Gitlab::Workhorse::DETECT_HEADER} header" do
+ expect(response).to have_gitlab_http_status(200)
+ expect(response.header[Gitlab::Workhorse::DETECT_HEADER]).to be nil
+ end
+ end
+ end
end
end
diff --git a/spec/factories/ci/bridge.rb b/spec/factories/ci/bridge.rb
new file mode 100644
index 00000000000..5f83b80ad7b
--- /dev/null
+++ b/spec/factories/ci/bridge.rb
@@ -0,0 +1,17 @@
+FactoryBot.define do
+ factory :ci_bridge, class: Ci::Bridge do
+ name ' bridge'
+ stage 'test'
+ stage_idx 0
+ ref 'master'
+ tag false
+ created_at 'Di 29. Okt 09:50:00 CET 2013'
+ status :success
+
+ pipeline factory: :ci_pipeline
+
+ after(:build) do |bridge, evaluator|
+ bridge.project ||= bridge.pipeline.project
+ end
+ end
+end
diff --git a/spec/factories/pool_repositories.rb b/spec/factories/pool_repositories.rb
index 2ed0844ed47..265a4643f46 100644
--- a/spec/factories/pool_repositories.rb
+++ b/spec/factories/pool_repositories.rb
@@ -1,5 +1,26 @@
FactoryBot.define do
factory :pool_repository do
- shard
+ shard { Shard.by_name("default") }
+ state :none
+
+ before(:create) do |pool|
+ pool.source_project = create(:project, :repository)
+ end
+
+ trait :scheduled do
+ state :scheduled
+ end
+
+ trait :failed do
+ state :failed
+ end
+
+ trait :ready do
+ state :ready
+
+ after(:create) do |pool|
+ pool.create_object_pool
+ end
+ end
end
end
diff --git a/spec/features/dashboard/merge_requests_spec.rb b/spec/features/dashboard/merge_requests_spec.rb
index 9ffa75aee47..282bf542e77 100644
--- a/spec/features/dashboard/merge_requests_spec.rb
+++ b/spec/features/dashboard/merge_requests_spec.rb
@@ -6,7 +6,6 @@ describe 'Dashboard Merge Requests' do
include ProjectForksHelper
let(:current_user) { create :user }
- let(:user) { current_user }
let(:project) { create(:project) }
let(:public_project) { create(:project, :public, :repository) }
diff --git a/spec/features/groups/clusters/user_spec.rb b/spec/features/groups/clusters/user_spec.rb
new file mode 100644
index 00000000000..2410cd92e3f
--- /dev/null
+++ b/spec/features/groups/clusters/user_spec.rb
@@ -0,0 +1,126 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'User Cluster', :js do
+ include GoogleApi::CloudPlatformHelpers
+
+ let(:group) { create(:group) }
+ let(:user) { create(:user) }
+
+ before do
+ group.add_maintainer(user)
+ gitlab_sign_in(user)
+
+ allow(Groups::ClustersController).to receive(:STATUS_POLLING_INTERVAL) { 100 }
+ allow_any_instance_of(Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService).to receive(:execute)
+ end
+
+ context 'when user does not have a cluster and visits cluster index page' do
+ before do
+ visit group_clusters_path(group)
+
+ click_link 'Add Kubernetes cluster'
+ click_link 'Add existing cluster'
+ end
+
+ context 'when user filled form with valid parameters' do
+ shared_examples 'valid cluster user form' do
+ it 'user sees a cluster details page' do
+ subject
+
+ expect(page).to have_content('Kubernetes cluster integration')
+ expect(page.find_field('cluster[name]').value).to eq('dev-cluster')
+ expect(page.find_field('cluster[platform_kubernetes_attributes][api_url]').value)
+ .to have_content('http://example.com')
+ expect(page.find_field('cluster[platform_kubernetes_attributes][token]').value)
+ .to have_content('my-token')
+ end
+ end
+
+ before do
+ fill_in 'cluster_name', with: 'dev-cluster'
+ fill_in 'cluster_platform_kubernetes_attributes_api_url', with: 'http://example.com'
+ fill_in 'cluster_platform_kubernetes_attributes_token', with: 'my-token'
+ end
+
+ subject { click_button 'Add Kubernetes cluster' }
+
+ it_behaves_like 'valid cluster user form'
+
+ context 'RBAC is enabled for the cluster' do
+ before do
+ check 'cluster_platform_kubernetes_attributes_authorization_type'
+ end
+
+ it_behaves_like 'valid cluster user form'
+
+ it 'user sees a cluster details page with RBAC enabled' do
+ subject
+
+ expect(page.find_field('cluster[platform_kubernetes_attributes][authorization_type]', disabled: true)).to be_checked
+ end
+ end
+ end
+
+ context 'when user filled form with invalid parameters' do
+ before do
+ click_button 'Add Kubernetes cluster'
+ end
+
+ it 'user sees a validation error' do
+ expect(page).to have_css('#error_explanation')
+ end
+ end
+ end
+
+ context 'when user does have a cluster and visits cluster page' do
+ let(:cluster) { create(:cluster, :provided_by_user, cluster_type: :group_type, groups: [group]) }
+
+ before do
+ visit group_cluster_path(group, cluster)
+ end
+
+ it 'user sees a cluster details page' do
+ expect(page).to have_button('Save changes')
+ end
+
+ context 'when user disables the cluster' do
+ before do
+ page.find(:css, '.js-cluster-enable-toggle-area .js-project-feature-toggle').click
+ page.within('#cluster-integration') { click_button 'Save changes' }
+ end
+
+ it 'user sees the successful message' do
+ expect(page).to have_content('Kubernetes cluster was successfully updated.')
+ end
+ end
+
+ context 'when user changes cluster parameters' do
+ before do
+ fill_in 'cluster_name', with: 'my-dev-cluster'
+ fill_in 'cluster_platform_kubernetes_attributes_token', with: 'new-token'
+ page.within('#js-cluster-details') { click_button 'Save changes' }
+ end
+
+ it 'user sees the successful message' do
+ expect(page).to have_content('Kubernetes cluster was successfully updated.')
+ expect(cluster.reload.name).to eq('my-dev-cluster')
+ expect(cluster.reload.platform_kubernetes.token).to eq('new-token')
+ end
+ end
+
+ context 'when user destroy the cluster' do
+ before do
+ page.accept_confirm do
+ click_link 'Remove integration'
+ end
+ end
+
+ it 'user sees creation form with the successful message' do
+ expect(page).to have_content('Kubernetes cluster integration was successfully removed.')
+ expect(page).to have_link('Add Kubernetes cluster')
+ end
+ end
+ end
+end
diff --git a/spec/features/issuables/default_sort_order_spec.rb b/spec/features/issuables/default_sort_order_spec.rb
deleted file mode 100644
index caee7a67aec..00000000000
--- a/spec/features/issuables/default_sort_order_spec.rb
+++ /dev/null
@@ -1,179 +0,0 @@
-require 'spec_helper'
-
-describe 'Projects > Issuables > Default sort order' do
- let(:project) { create(:project, :public) }
-
- let(:first_created_issuable) { issuables.order_created_asc.first }
- let(:last_created_issuable) { issuables.order_created_desc.first }
-
- let(:first_updated_issuable) { issuables.order_updated_asc.first }
- let(:last_updated_issuable) { issuables.order_updated_desc.first }
-
- context 'for merge requests' do
- include MergeRequestHelpers
-
- let!(:issuables) do
- timestamps = [{ created_at: 3.minutes.ago, updated_at: 20.seconds.ago },
- { created_at: 2.minutes.ago, updated_at: 30.seconds.ago },
- { created_at: 4.minutes.ago, updated_at: 10.seconds.ago }]
-
- timestamps.each_with_index do |ts, i|
- create issuable_type, { title: "#{issuable_type}_#{i}",
- source_branch: "#{issuable_type}_#{i}",
- source_project: project }.merge(ts)
- end
-
- MergeRequest.all
- end
-
- context 'in the "merge requests" tab', :js do
- let(:issuable_type) { :merge_request }
-
- it 'is "last created"' do
- visit_merge_requests project
-
- expect(first_merge_request).to include(last_created_issuable.title)
- expect(last_merge_request).to include(first_created_issuable.title)
- end
- end
-
- context 'in the "merge requests / open" tab', :js do
- let(:issuable_type) { :merge_request }
-
- it 'is "created date"' do
- visit_merge_requests_with_state(project, 'open')
-
- expect(selected_sort_order).to eq('created date')
- expect(first_merge_request).to include(last_created_issuable.title)
- expect(last_merge_request).to include(first_created_issuable.title)
- end
- end
-
- context 'in the "merge requests / merged" tab', :js do
- let(:issuable_type) { :merged_merge_request }
-
- it 'is "last updated"' do
- visit_merge_requests_with_state(project, 'merged')
-
- expect(find('.issues-other-filters')).to have_content('Last updated')
- expect(first_merge_request).to include(last_updated_issuable.title)
- expect(last_merge_request).to include(first_updated_issuable.title)
- end
- end
-
- context 'in the "merge requests / closed" tab', :js do
- let(:issuable_type) { :closed_merge_request }
-
- it 'is "last updated"' do
- visit_merge_requests_with_state(project, 'closed')
-
- expect(find('.issues-other-filters')).to have_content('Last updated')
- expect(first_merge_request).to include(last_updated_issuable.title)
- expect(last_merge_request).to include(first_updated_issuable.title)
- end
- end
-
- context 'in the "merge requests / all" tab', :js do
- let(:issuable_type) { :merge_request }
-
- it 'is "created date"' do
- visit_merge_requests_with_state(project, 'all')
-
- expect(find('.issues-other-filters')).to have_content('Created date')
- expect(first_merge_request).to include(last_created_issuable.title)
- expect(last_merge_request).to include(first_created_issuable.title)
- end
- end
- end
-
- context 'for issues' do
- include IssueHelpers
-
- let!(:issuables) do
- timestamps = [{ created_at: 3.minutes.ago, updated_at: 20.seconds.ago },
- { created_at: 2.minutes.ago, updated_at: 30.seconds.ago },
- { created_at: 4.minutes.ago, updated_at: 10.seconds.ago }]
-
- timestamps.each_with_index do |ts, i|
- create issuable_type, { title: "#{issuable_type}_#{i}",
- project: project }.merge(ts)
- end
-
- Issue.all
- end
-
- context 'in the "issues" tab', :js do
- let(:issuable_type) { :issue }
-
- it 'is "created date"' do
- visit_issues project
-
- expect(find('.issues-other-filters')).to have_content('Created date')
- expect(first_issue).to include(last_created_issuable.title)
- expect(last_issue).to include(first_created_issuable.title)
- end
- end
-
- context 'in the "issues / open" tab', :js do
- let(:issuable_type) { :issue }
-
- it 'is "created date"' do
- visit_issues_with_state(project, 'open')
-
- expect(find('.issues-other-filters')).to have_content('Created date')
- expect(first_issue).to include(last_created_issuable.title)
- expect(last_issue).to include(first_created_issuable.title)
- end
- end
-
- context 'in the "issues / closed" tab', :js do
- let(:issuable_type) { :closed_issue }
-
- it 'is "last updated"' do
- visit_issues_with_state(project, 'closed')
-
- expect(find('.issues-other-filters')).to have_content('Last updated')
- expect(first_issue).to include(last_updated_issuable.title)
- expect(last_issue).to include(first_updated_issuable.title)
- end
- end
-
- context 'in the "issues / all" tab', :js do
- let(:issuable_type) { :issue }
-
- it 'is "created date"' do
- visit_issues_with_state(project, 'all')
-
- expect(find('.issues-other-filters')).to have_content('Created date')
- expect(first_issue).to include(last_created_issuable.title)
- expect(last_issue).to include(first_created_issuable.title)
- end
- end
-
- context 'when the sort in the URL is id_desc' do
- let(:issuable_type) { :issue }
-
- before do
- visit_issues(project, sort: 'id_desc')
- end
-
- it 'shows the sort order as created date' do
- expect(find('.issues-other-filters')).to have_content('Created date')
- expect(first_issue).to include(last_created_issuable.title)
- expect(last_issue).to include(first_created_issuable.title)
- end
- end
- end
-
- def selected_sort_order
- find('.filter-dropdown-container .dropdown button').text.downcase
- end
-
- def visit_merge_requests_with_state(project, state)
- visit_merge_requests project, state: state
- end
-
- def visit_issues_with_state(project, state)
- visit_issues project, state: state
- end
-end
diff --git a/spec/features/issuables/sorting_list_spec.rb b/spec/features/issuables/sorting_list_spec.rb
new file mode 100644
index 00000000000..0601dd47c03
--- /dev/null
+++ b/spec/features/issuables/sorting_list_spec.rb
@@ -0,0 +1,226 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+describe 'Sort Issuable List' do
+ let(:project) { create(:project, :public) }
+
+ let(:first_created_issuable) { issuables.order_created_asc.first }
+ let(:last_created_issuable) { issuables.order_created_desc.first }
+
+ let(:first_updated_issuable) { issuables.order_updated_asc.first }
+ let(:last_updated_issuable) { issuables.order_updated_desc.first }
+
+ context 'for merge requests' do
+ include MergeRequestHelpers
+
+ let!(:issuables) do
+ timestamps = [{ created_at: 3.minutes.ago, updated_at: 20.seconds.ago },
+ { created_at: 2.minutes.ago, updated_at: 30.seconds.ago },
+ { created_at: 4.minutes.ago, updated_at: 10.seconds.ago }]
+
+ timestamps.each_with_index do |ts, i|
+ create issuable_type, { title: "#{issuable_type}_#{i}",
+ source_branch: "#{issuable_type}_#{i}",
+ source_project: project }.merge(ts)
+ end
+
+ MergeRequest.all
+ end
+
+ context 'default sort order' do
+ context 'in the "merge requests" tab', :js do
+ let(:issuable_type) { :merge_request }
+
+ it 'is "last created"' do
+ visit_merge_requests project
+
+ expect(first_merge_request).to include(last_created_issuable.title)
+ expect(last_merge_request).to include(first_created_issuable.title)
+ end
+ end
+
+ context 'in the "merge requests / open" tab', :js do
+ let(:issuable_type) { :merge_request }
+
+ it 'is "created date"' do
+ visit_merge_requests_with_state(project, 'open')
+
+ expect(selected_sort_order).to eq('created date')
+ expect(first_merge_request).to include(last_created_issuable.title)
+ expect(last_merge_request).to include(first_created_issuable.title)
+ end
+ end
+
+ context 'in the "merge requests / merged" tab', :js do
+ let(:issuable_type) { :merged_merge_request }
+
+ it 'is "last updated"' do
+ visit_merge_requests_with_state(project, 'merged')
+
+ expect(find('.issues-other-filters')).to have_content('Last updated')
+ expect(first_merge_request).to include(last_updated_issuable.title)
+ expect(last_merge_request).to include(first_updated_issuable.title)
+ end
+ end
+
+ context 'in the "merge requests / closed" tab', :js do
+ let(:issuable_type) { :closed_merge_request }
+
+ it 'is "last updated"' do
+ visit_merge_requests_with_state(project, 'closed')
+
+ expect(find('.issues-other-filters')).to have_content('Last updated')
+ expect(first_merge_request).to include(last_updated_issuable.title)
+ expect(last_merge_request).to include(first_updated_issuable.title)
+ end
+ end
+
+ context 'in the "merge requests / all" tab', :js do
+ let(:issuable_type) { :merge_request }
+
+ it 'is "created date"' do
+ visit_merge_requests_with_state(project, 'all')
+
+ expect(find('.issues-other-filters')).to have_content('Created date')
+ expect(first_merge_request).to include(last_created_issuable.title)
+ expect(last_merge_request).to include(first_created_issuable.title)
+ end
+ end
+
+ context 'custom sorting' do
+ let(:issuable_type) { :merge_request }
+
+ it 'supports sorting in asc and desc order' do
+ visit_merge_requests_with_state(project, 'open')
+
+ page.within('.issues-other-filters') do
+ click_button('Created date')
+ click_link('Last updated')
+ end
+
+ expect(first_merge_request).to include(last_updated_issuable.title)
+ expect(last_merge_request).to include(first_updated_issuable.title)
+
+ find('.issues-other-filters .filter-dropdown-container .qa-reverse-sort').click
+
+ expect(first_merge_request).to include(first_updated_issuable.title)
+ expect(last_merge_request).to include(last_updated_issuable.title)
+ end
+ end
+ end
+ end
+
+ context 'for issues' do
+ include IssueHelpers
+
+ let!(:issuables) do
+ timestamps = [{ created_at: 3.minutes.ago, updated_at: 20.seconds.ago },
+ { created_at: 2.minutes.ago, updated_at: 30.seconds.ago },
+ { created_at: 4.minutes.ago, updated_at: 10.seconds.ago }]
+
+ timestamps.each_with_index do |ts, i|
+ create issuable_type, { title: "#{issuable_type}_#{i}",
+ project: project }.merge(ts)
+ end
+
+ Issue.all
+ end
+
+ context 'default sort order' do
+ context 'in the "issues" tab', :js do
+ let(:issuable_type) { :issue }
+
+ it 'is "created date"' do
+ visit_issues project
+
+ expect(find('.issues-other-filters')).to have_content('Created date')
+ expect(first_issue).to include(last_created_issuable.title)
+ expect(last_issue).to include(first_created_issuable.title)
+ end
+ end
+
+ context 'in the "issues / open" tab', :js do
+ let(:issuable_type) { :issue }
+
+ it 'is "created date"' do
+ visit_issues_with_state(project, 'open')
+
+ expect(find('.issues-other-filters')).to have_content('Created date')
+ expect(first_issue).to include(last_created_issuable.title)
+ expect(last_issue).to include(first_created_issuable.title)
+ end
+ end
+
+ context 'in the "issues / closed" tab', :js do
+ let(:issuable_type) { :closed_issue }
+
+ it 'is "last updated"' do
+ visit_issues_with_state(project, 'closed')
+
+ expect(find('.issues-other-filters')).to have_content('Last updated')
+ expect(first_issue).to include(last_updated_issuable.title)
+ expect(last_issue).to include(first_updated_issuable.title)
+ end
+ end
+
+ context 'in the "issues / all" tab', :js do
+ let(:issuable_type) { :issue }
+
+ it 'is "created date"' do
+ visit_issues_with_state(project, 'all')
+
+ expect(find('.issues-other-filters')).to have_content('Created date')
+ expect(first_issue).to include(last_created_issuable.title)
+ expect(last_issue).to include(first_created_issuable.title)
+ end
+ end
+
+ context 'when the sort in the URL is id_desc' do
+ let(:issuable_type) { :issue }
+
+ before do
+ visit_issues(project, sort: 'id_desc')
+ end
+
+ it 'shows the sort order as created date' do
+ expect(find('.issues-other-filters')).to have_content('Created date')
+ expect(first_issue).to include(last_created_issuable.title)
+ expect(last_issue).to include(first_created_issuable.title)
+ end
+ end
+ end
+
+ context 'custom sorting' do
+ let(:issuable_type) { :issue }
+
+ it 'supports sorting in asc and desc order' do
+ visit_issues_with_state(project, 'open')
+
+ page.within('.issues-other-filters') do
+ click_button('Created date')
+ click_link('Last updated')
+ end
+
+ expect(first_issue).to include(last_updated_issuable.title)
+ expect(last_issue).to include(first_updated_issuable.title)
+
+ find('.issues-other-filters .filter-dropdown-container .qa-reverse-sort').click
+
+ expect(first_issue).to include(first_updated_issuable.title)
+ expect(last_issue).to include(last_updated_issuable.title)
+ end
+ end
+ end
+
+ def selected_sort_order
+ find('.filter-dropdown-container .dropdown button').text.downcase
+ end
+
+ def visit_merge_requests_with_state(project, state)
+ visit_merge_requests project, state: state
+ end
+
+ def visit_issues_with_state(project, state)
+ visit_issues project, state: state
+ end
+end
diff --git a/spec/features/issues/filtered_search/filter_issues_spec.rb b/spec/features/issues/filtered_search/filter_issues_spec.rb
index 4d9b8262f21..a29380a180e 100644
--- a/spec/features/issues/filtered_search/filter_issues_spec.rb
+++ b/spec/features/issues/filtered_search/filter_issues_spec.rb
@@ -430,7 +430,7 @@ describe 'Filter issues', :js do
expect_issues_list_count(2)
- sort_toggle = find('.filter-dropdown-container .dropdown-menu-toggle')
+ sort_toggle = find('.filter-dropdown-container .dropdown')
sort_toggle.click
find('.filter-dropdown-container .dropdown-menu li a', text: 'Created date').click
diff --git a/spec/features/issues/user_sorts_issues_spec.rb b/spec/features/issues/user_sorts_issues_spec.rb
index 3bc93933183..eebd2d57cca 100644
--- a/spec/features/issues/user_sorts_issues_spec.rb
+++ b/spec/features/issues/user_sorts_issues_spec.rb
@@ -20,9 +20,9 @@ describe "User sorts issues" do
end
it 'keeps the sort option' do
- find('.filter-dropdown-container button.dropdown-menu-toggle').click
+ find('.filter-dropdown-container .dropdown').click
- page.within('.content ul.dropdown-menu.dropdown-menu-right li') do
+ page.within('ul.dropdown-menu.dropdown-menu-right li') do
click_link('Milestone')
end
@@ -40,9 +40,9 @@ describe "User sorts issues" do
end
it "sorts by popularity" do
- find(".filter-dropdown-container button.dropdown-menu-toggle").click
+ find('.filter-dropdown-container .dropdown').click
- page.within(".content ul.dropdown-menu.dropdown-menu-right li") do
+ page.within('ul.dropdown-menu.dropdown-menu-right li') do
click_link("Popularity")
end
diff --git a/spec/features/merge_request/user_awards_emoji_spec.rb b/spec/features/merge_request/user_awards_emoji_spec.rb
index 859a4c65562..93376bc8ce0 100644
--- a/spec/features/merge_request/user_awards_emoji_spec.rb
+++ b/spec/features/merge_request/user_awards_emoji_spec.rb
@@ -4,11 +4,14 @@ describe 'Merge request > User awards emoji', :js do
let(:project) { create(:project, :public, :repository) }
let(:user) { project.creator }
let(:merge_request) { create(:merge_request, source_project: project, author: create(:user)) }
+ let!(:note) { create(:note, noteable: merge_request, project: merge_request.project) }
describe 'logged in' do
before do
sign_in(user)
visit project_merge_request_path(project, merge_request)
+
+ wait_for_requests
end
it 'adds award to merge request' do
@@ -36,6 +39,15 @@ describe 'Merge request > User awards emoji', :js do
expect(page).to have_selector('.emoji-menu', count: 1)
end
+ it 'adds awards to note' do
+ first('.js-note-emoji').click
+ first('.emoji-menu .js-emoji-btn').click
+
+ wait_for_requests
+
+ expect(page).to have_selector('.js-awards-block')
+ end
+
describe 'the project is archived' do
let(:project) { create(:project, :public, :repository, :archived) }
diff --git a/spec/features/merge_request/user_expands_diff_spec.rb b/spec/features/merge_request/user_expands_diff_spec.rb
index 02fe6352a0f..3560b8d90bb 100644
--- a/spec/features/merge_request/user_expands_diff_spec.rb
+++ b/spec/features/merge_request/user_expands_diff_spec.rb
@@ -2,16 +2,19 @@ require 'spec_helper'
describe 'User expands diff', :js do
let(:project) { create(:project, :public, :repository) }
- let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
+ let(:merge_request) { create(:merge_request, source_branch: 'expand-collapse-files', source_project: project, target_project: project) }
before do
+ allow(Gitlab::Git::Diff).to receive(:size_limit).and_return(100.kilobytes)
+ allow(Gitlab::Git::Diff).to receive(:collapse_limit).and_return(10.kilobytes)
+
visit(diffs_project_merge_request_path(project, merge_request))
wait_for_requests
end
it 'allows user to expand diff' do
- page.within find('[id="19763941ab80e8c09871c0a425f0560d9053bcb3"]') do
+ page.within find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd"]') do
click_link 'Click to expand it.'
wait_for_requests
diff --git a/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb b/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb
index 328f96e6ed7..ba4806821f9 100644
--- a/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb
+++ b/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb
@@ -361,8 +361,14 @@ describe 'Merge request > User resolves diff notes and discussions', :js do
end
end
- it 'shows jump to next discussion button' do
- expect(page.all('.discussion-reply-holder', count: 2)).to all(have_selector('.discussion-next-btn'))
+ it 'shows jump to next discussion button except on last discussion' do
+ wait_for_requests
+
+ all_discussion_replies = page.all('.discussion-reply-holder')
+
+ expect(all_discussion_replies.count).to eq(2)
+ expect(all_discussion_replies.first.all('.discussion-next-btn').count).to eq(1)
+ expect(all_discussion_replies.last.all('.discussion-next-btn').count).to eq(0)
end
it 'displays next discussion even if hidden' do
@@ -380,7 +386,13 @@ describe 'Merge request > User resolves diff notes and discussions', :js do
page.find('.discussion-next-btn').click
end
- expect(find('.discussion-with-resolve-btn')).to have_selector('.btn', text: 'Resolve discussion')
+ page.all('.note-discussion').first do
+ expect(page.find('.discussion-with-resolve-btn')).to have_selector('.btn', text: 'Resolve discussion')
+ end
+
+ page.all('.note-discussion').last do
+ expect(page.find('.discussion-with-resolve-btn')).not.to have_selector('.btn', text: 'Resolve discussion')
+ end
end
end
diff --git a/spec/features/merge_requests/user_sorts_merge_requests_spec.rb b/spec/features/merge_requests/user_sorts_merge_requests_spec.rb
index 61e8f1c4662..fa887110c13 100644
--- a/spec/features/merge_requests/user_sorts_merge_requests_spec.rb
+++ b/spec/features/merge_requests/user_sorts_merge_requests_spec.rb
@@ -19,9 +19,9 @@ describe 'User sorts merge requests' do
end
it 'keeps the sort option' do
- find('.filter-dropdown-container button.dropdown-menu-toggle').click
+ find('.filter-dropdown-container .dropdown').click
- page.within('.content ul.dropdown-menu.dropdown-menu-right li') do
+ page.within('ul.dropdown-menu.dropdown-menu-right li') do
click_link('Milestone')
end
@@ -49,9 +49,9 @@ describe 'User sorts merge requests' do
it 'separates remember sorting with issues' do
create(:issue, project: project)
- find('.filter-dropdown-container button.dropdown-menu-toggle').click
+ find('.filter-dropdown-container .dropdown').click
- page.within('.content ul.dropdown-menu.dropdown-menu-right li') do
+ page.within('ul.dropdown-menu.dropdown-menu-right li') do
click_link('Milestone')
end
@@ -70,9 +70,9 @@ describe 'User sorts merge requests' do
end
it 'sorts by popularity' do
- find('.filter-dropdown-container button.dropdown-menu-toggle').click
+ find('.filter-dropdown-container .dropdown').click
- page.within('.content ul.dropdown-menu.dropdown-menu-right li') do
+ page.within('ul.dropdown-menu.dropdown-menu-right li') do
click_link('Popularity')
end
diff --git a/spec/features/projects/clusters/applications_spec.rb b/spec/features/projects/clusters/applications_spec.rb
index 71d715237f5..8918a7b7b9c 100644
--- a/spec/features/projects/clusters/applications_spec.rb
+++ b/spec/features/projects/clusters/applications_spec.rb
@@ -70,6 +70,44 @@ describe 'Clusters Applications', :js do
end
end
+ context 'when user installs Cert Manager' do
+ before do
+ allow(ClusterInstallAppWorker).to receive(:perform_async)
+ allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_in)
+ allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_async)
+
+ create(:clusters_applications_helm, :installed, cluster: cluster)
+
+ page.within('.js-cluster-application-row-cert_manager') do
+ click_button 'Install'
+ end
+ end
+
+ it 'shows status transition' do
+ def email_form_value
+ page.find('.js-email').value
+ end
+
+ page.within('.js-cluster-application-row-cert_manager') do
+ expect(email_form_value).to eq(cluster.user.email)
+ expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Install')
+
+ page.find('.js-email').set("new_email@example.org")
+ Clusters::Cluster.last.application_cert_manager.make_installing!
+
+ expect(email_form_value).to eq('new_email@example.org')
+ expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Installing')
+
+ Clusters::Cluster.last.application_cert_manager.make_installed!
+
+ expect(email_form_value).to eq('new_email@example.org')
+ expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Installed')
+ end
+
+ expect(page).to have_content('Cert-Manager was successfully installed on your Kubernetes cluster')
+ end
+ end
+
context 'when user installs Ingress' do
context 'when user installs application: Ingress' do
before do
diff --git a/spec/features/projects/files/user_browses_files_spec.rb b/spec/features/projects/files/user_browses_files_spec.rb
index f3cf3a282e5..66268355345 100644
--- a/spec/features/projects/files/user_browses_files_spec.rb
+++ b/spec/features/projects/files/user_browses_files_spec.rb
@@ -11,6 +11,7 @@ describe "User browses files" do
let(:user) { project.owner }
before do
+ stub_feature_flags(csslab: false)
sign_in(user)
end
diff --git a/spec/features/projects/jobs_spec.rb b/spec/features/projects/jobs_spec.rb
index d7c4abffddd..651c02c7ecc 100644
--- a/spec/features/projects/jobs_spec.rb
+++ b/spec/features/projects/jobs_spec.rb
@@ -346,44 +346,85 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do
describe 'Variables' do
let(:trigger_request) { create(:ci_trigger_request) }
+ let(:job) { create(:ci_build, pipeline: pipeline, trigger_request: trigger_request) }
- let(:job) do
- create :ci_build, pipeline: pipeline, trigger_request: trigger_request
- end
+ context 'when user is a maintainer' do
+ shared_examples 'no reveal button variables behavior' do
+ it 'renders a hidden value with no reveal values button', :js do
+ expect(page).to have_content('Token')
+ expect(page).to have_content('Variables')
+
+ expect(page).not_to have_css('.js-reveal-variables')
+
+ expect(page).to have_selector('.js-build-variable', text: 'TRIGGER_KEY_1')
+ expect(page).to have_selector('.js-build-value', text: '••••••')
+ end
+ end
+
+ context 'when variables are stored in trigger_request' do
+ before do
+ trigger_request.update_attribute(:variables, { 'TRIGGER_KEY_1' => 'TRIGGER_VALUE_1' } )
+
+ visit project_job_path(project, job)
+ end
+
+ it_behaves_like 'no reveal button variables behavior'
+ end
- shared_examples 'expected variables behavior' do
- it 'shows variable key and value after click', :js do
- expect(page).to have_content('Token')
- expect(page).to have_css('.js-reveal-variables')
- expect(page).not_to have_css('.js-build-variable')
- expect(page).not_to have_css('.js-build-value')
+ context 'when variables are stored in pipeline_variables' do
+ before do
+ create(:ci_pipeline_variable, pipeline: pipeline, key: 'TRIGGER_KEY_1', value: 'TRIGGER_VALUE_1')
- click_button 'Reveal Variables'
+ visit project_job_path(project, job)
+ end
- expect(page).not_to have_css('.js-reveal-variables')
- expect(page).to have_selector('.js-build-variable', text: 'TRIGGER_KEY_1')
- expect(page).to have_selector('.js-build-value', text: 'TRIGGER_VALUE_1')
+ it_behaves_like 'no reveal button variables behavior'
end
end
- context 'when variables are stored in trigger_request' do
+ context 'when user is a maintainer' do
before do
- trigger_request.update_attribute(:variables, { 'TRIGGER_KEY_1' => 'TRIGGER_VALUE_1' } )
+ project.add_maintainer(user)
+ end
- visit project_job_path(project, job)
+ shared_examples 'reveal button variables behavior' do
+ it 'renders a hidden value with a reveal values button', :js do
+ expect(page).to have_content('Token')
+ expect(page).to have_content('Variables')
+
+ expect(page).to have_css('.js-reveal-variables')
+
+ expect(page).to have_selector('.js-build-variable', text: 'TRIGGER_KEY_1')
+ expect(page).to have_selector('.js-build-value', text: '••••••')
+ end
+
+ it 'reveals values on button click', :js do
+ click_button 'Reveal values'
+
+ expect(page).to have_selector('.js-build-variable', text: 'TRIGGER_KEY_1')
+ expect(page).to have_selector('.js-build-value', text: 'TRIGGER_VALUE_1')
+ end
end
- it_behaves_like 'expected variables behavior'
- end
+ context 'when variables are stored in trigger_request' do
+ before do
+ trigger_request.update_attribute(:variables, { 'TRIGGER_KEY_1' => 'TRIGGER_VALUE_1' } )
- context 'when variables are stored in pipeline_variables' do
- before do
- create(:ci_pipeline_variable, pipeline: pipeline, key: 'TRIGGER_KEY_1', value: 'TRIGGER_VALUE_1')
+ visit project_job_path(project, job)
+ end
- visit project_job_path(project, job)
+ it_behaves_like 'reveal button variables behavior'
end
- it_behaves_like 'expected variables behavior'
+ context 'when variables are stored in pipeline_variables' do
+ before do
+ create(:ci_pipeline_variable, pipeline: pipeline, key: 'TRIGGER_KEY_1', value: 'TRIGGER_VALUE_1')
+
+ visit project_job_path(project, job)
+ end
+
+ it_behaves_like 'reveal button variables behavior'
+ end
end
end
diff --git a/spec/features/projects/labels/issues_sorted_by_priority_spec.rb b/spec/features/projects/labels/issues_sorted_by_priority_spec.rb
index b778c72bc76..25417cf4955 100644
--- a/spec/features/projects/labels/issues_sorted_by_priority_spec.rb
+++ b/spec/features/projects/labels/issues_sorted_by_priority_spec.rb
@@ -32,7 +32,7 @@ describe 'Issue prioritization' do
visit project_issues_path(project, sort: 'label_priority')
# Ensure we are indicating that issues are sorted by priority
- expect(page).to have_selector('.dropdown-menu-toggle', text: 'Label priority')
+ expect(page).to have_selector('.dropdown', text: 'Label priority')
page.within('.issues-holder') do
issue_titles = all('.issues-list .issue-title-text').map(&:text)
@@ -70,7 +70,7 @@ describe 'Issue prioritization' do
sign_in user
visit project_issues_path(project, sort: 'label_priority')
- expect(page).to have_selector('.dropdown-menu-toggle', text: 'Label priority')
+ expect(page).to have_selector('.dropdown', text: 'Label priority')
page.within('.issues-holder') do
issue_titles = all('.issues-list .issue-title-text').map(&:text)
diff --git a/spec/features/projects/labels/update_prioritization_spec.rb b/spec/features/projects/labels/update_prioritization_spec.rb
index 996040fde02..055a0c83a11 100644
--- a/spec/features/projects/labels/update_prioritization_spec.rb
+++ b/spec/features/projects/labels/update_prioritization_spec.rb
@@ -115,6 +115,21 @@ describe 'Prioritize labels' do
end
end
+ it 'user can see a primary button when there are only prioritized labels', :js do
+ visit project_labels_path(project)
+
+ page.within('.other-labels') do
+ all('.js-toggle-priority').each do |el|
+ el.click
+ end
+ wait_for_requests
+ end
+
+ page.within('.breadcrumbs-container') do
+ expect(page).to have_link('New label')
+ end
+ end
+
it 'shows a help message about prioritized labels' do
visit project_labels_path(project)
diff --git a/spec/features/projects/serverless/functions_spec.rb b/spec/features/projects/serverless/functions_spec.rb
new file mode 100644
index 00000000000..766c63725b3
--- /dev/null
+++ b/spec/features/projects/serverless/functions_spec.rb
@@ -0,0 +1,49 @@
+require 'spec_helper'
+
+describe 'Functions', :js do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_maintainer(user)
+ gitlab_sign_in(user)
+ end
+
+ context 'when user does not have a cluster and visits the serverless page' do
+ before do
+ visit project_serverless_functions_path(project)
+ end
+
+ it 'sees an empty state' do
+ expect(page).to have_link('Install Knative')
+ expect(page).to have_selector('.empty-state')
+ end
+ end
+
+ context 'when the user does have a cluster and visits the serverless page' do
+ let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+
+ before do
+ visit project_serverless_functions_path(project)
+ end
+
+ it 'sees an empty state' do
+ expect(page).to have_link('Install Knative')
+ expect(page).to have_selector('.empty-state')
+ end
+ end
+
+ context 'when the user has a cluster and knative installed and visits the serverless page' do
+ let!(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+ let(:knative) { create(:clusters_applications_knative, :installed, cluster: cluster) }
+ let(:project) { knative.cluster.project }
+
+ before do
+ visit project_serverless_functions_path(project)
+ end
+
+ it 'sees an empty listing of serverless functions' do
+ expect(page).to have_selector('.gl-responsive-table-row')
+ end
+ end
+end
diff --git a/spec/features/projects/settings/repository_settings_spec.rb b/spec/features/projects/settings/repository_settings_spec.rb
index b7a22316d26..418e22f8c35 100644
--- a/spec/features/projects/settings/repository_settings_spec.rb
+++ b/spec/features/projects/settings/repository_settings_spec.rb
@@ -196,5 +196,40 @@ describe 'Projects > Settings > Repository settings' do
end
end
end
+
+ context 'repository cleanup settings' do
+ let(:object_map_file) { Rails.root.join('spec', 'fixtures', 'bfg_object_map.txt') }
+
+ context 'feature enabled' do
+ it 'uploads an object map file', :js do
+ stub_feature_flags(project_cleanup: true)
+
+ visit project_settings_repository_path(project)
+
+ expect(page).to have_content('Repository cleanup')
+
+ page.within('#cleanup') do
+ attach_file('project[bfg_object_map]', object_map_file, visible: false)
+
+ Sidekiq::Testing.fake! do
+ click_button 'Start cleanup'
+ end
+ end
+
+ expect(page).to have_content('Repository cleanup has started')
+ expect(RepositoryCleanupWorker.jobs.count).to eq(1)
+ end
+ end
+
+ context 'feature disabled' do
+ it 'does not show the settings' do
+ stub_feature_flags(project_cleanup: false)
+
+ visit project_settings_repository_path(project)
+
+ expect(page).not_to have_content('Repository cleanup')
+ end
+ end
+ end
end
end
diff --git a/spec/features/projects/show/developer_views_empty_project_instructions_spec.rb b/spec/features/projects/show/developer_views_empty_project_instructions_spec.rb
index 227bdf524fe..8ba91fe7fd7 100644
--- a/spec/features/projects/show/developer_views_empty_project_instructions_spec.rb
+++ b/spec/features/projects/show/developer_views_empty_project_instructions_spec.rb
@@ -10,54 +10,9 @@ describe 'Projects > Show > Developer views empty project instructions' do
sign_in(developer)
end
- context 'without an SSH key' do
- it 'defaults to HTTP' do
- visit_project
-
- expect_instructions_for('http')
- end
-
- it 'switches to SSH', :js do
- visit_project
-
- select_protocol('SSH')
-
- expect_instructions_for('ssh')
- end
- end
-
- context 'with an SSH key' do
- before do
- create(:personal_key, user: developer)
- end
-
- it 'defaults to SSH' do
- visit_project
-
- expect_instructions_for('ssh')
- end
-
- it 'switches to HTTP', :js do
- visit_project
-
- select_protocol('HTTP')
-
- expect_instructions_for('http')
- end
- end
-
- def visit_project
+ it 'displays "git clone" instructions' do
visit project_path(project)
- end
-
- def select_protocol(protocol)
- find('#clone-dropdown').click
- find(".#{protocol.downcase}-selector").click
- end
-
- def expect_instructions_for(protocol)
- msg = :"#{protocol.downcase}_url_to_repo"
- expect(page).to have_content("git clone #{project.send(msg)}")
+ expect(page).to have_content("git clone")
end
end
diff --git a/spec/features/projects/show/user_manages_notifications_spec.rb b/spec/features/projects/show/user_manages_notifications_spec.rb
index 546619e88ec..88f3397608f 100644
--- a/spec/features/projects/show/user_manages_notifications_spec.rb
+++ b/spec/features/projects/show/user_manages_notifications_spec.rb
@@ -8,13 +8,18 @@ describe 'Projects > Show > User manages notifications', :js do
visit project_path(project)
end
- it 'changes the notification setting' do
+ def click_notifications_button
first('.notifications-btn').click
+ end
+
+ it 'changes the notification setting' do
+ click_notifications_button
click_link 'On mention'
- page.within '#notifications-button' do
- expect(page).to have_content 'On mention'
- end
+ wait_for_requests
+
+ click_notifications_button
+ expect(find('.update-notification.is-active')).to have_content('On mention')
end
context 'custom notification settings' do
@@ -38,7 +43,7 @@ describe 'Projects > Show > User manages notifications', :js do
end
it 'shows notification settings checkbox' do
- first('.notifications-btn').click
+ click_notifications_button
page.find('a[data-notification-level="custom"]').click
page.within('.custom-notifications-form') do
diff --git a/spec/features/projects/show/user_sees_collaboration_links_spec.rb b/spec/features/projects/show/user_sees_collaboration_links_spec.rb
index 7b3711531c6..24777788248 100644
--- a/spec/features/projects/show/user_sees_collaboration_links_spec.rb
+++ b/spec/features/projects/show/user_sees_collaboration_links_spec.rb
@@ -21,18 +21,6 @@ describe 'Projects > Show > Collaboration links' do
end
end
- # The project header
- page.within('.project-home-panel') do
- aggregate_failures 'dropdown links in the project home panel' do
- expect(page).to have_link('New issue')
- expect(page).to have_link('New merge request')
- expect(page).to have_link('New snippet')
- expect(page).to have_link('New file')
- expect(page).to have_link('New branch')
- expect(page).to have_link('New tag')
- end
- end
-
# The dropdown above the tree
page.within('.repo-breadcrumb') do
aggregate_failures 'dropdown links above the repo tree' do
@@ -61,17 +49,6 @@ describe 'Projects > Show > Collaboration links' do
end
end
- page.within('.project-home-panel') do
- aggregate_failures 'dropdown links' do
- expect(page).not_to have_link('New issue')
- expect(page).not_to have_link('New merge request')
- expect(page).not_to have_link('New snippet')
- expect(page).not_to have_link('New file')
- expect(page).not_to have_link('New branch')
- expect(page).not_to have_link('New tag')
- end
- end
-
page.within('.repo-breadcrumb') do
aggregate_failures 'dropdown links' do
expect(page).not_to have_link('New file')
diff --git a/spec/features/projects/show/user_sees_git_instructions_spec.rb b/spec/features/projects/show/user_sees_git_instructions_spec.rb
index 9a82fee1b5d..ffa80235083 100644
--- a/spec/features/projects/show/user_sees_git_instructions_spec.rb
+++ b/spec/features/projects/show/user_sees_git_instructions_spec.rb
@@ -29,7 +29,7 @@ describe 'Projects > Show > User sees Git instructions' do
expect(element.text).to include(project.http_url_to_repo)
end
- expect(page).to have_field('project_clone', with: project.http_url_to_repo) unless user_has_ssh_key
+ expect(page).to have_field('http_project_clone', with: project.http_url_to_repo) unless user_has_ssh_key
end
end
@@ -41,7 +41,7 @@ describe 'Projects > Show > User sees Git instructions' do
expect(page).to have_content(project.title)
end
- expect(page).to have_field('project_clone', with: project.http_url_to_repo) unless user_has_ssh_key
+ expect(page).to have_field('http_project_clone', with: project.http_url_to_repo) unless user_has_ssh_key
end
end
diff --git a/spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb b/spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb
index df2b492ae6b..dcca1d388c7 100644
--- a/spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb
+++ b/spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb
@@ -21,7 +21,7 @@ describe 'Projects > Show > User sees setup shortcut buttons' do
end
it 'no Auto DevOps button if can not manage pipelines' do
- page.within('.project-stats') do
+ page.within('.project-buttons') do
expect(page).not_to have_link('Enable Auto DevOps')
expect(page).not_to have_link('Auto DevOps enabled')
end
@@ -30,7 +30,7 @@ describe 'Projects > Show > User sees setup shortcut buttons' do
it '"Auto DevOps enabled" button not linked' do
visit project_path(project)
- page.within('.project-stats') do
+ page.within('.project-buttons') do
expect(page).to have_text('Auto DevOps enabled')
end
end
@@ -45,19 +45,19 @@ describe 'Projects > Show > User sees setup shortcut buttons' do
end
it '"New file" button linked to new file page' do
- page.within('.project-stats') do
+ page.within('.project-buttons') do
expect(page).to have_link('New file', href: project_new_blob_path(project, project.default_branch || 'master'))
end
end
- it '"Add Readme" button linked to new file populated for a readme' do
- page.within('.project-stats') do
- expect(page).to have_link('Add Readme', href: presenter.add_readme_path)
+ it '"Add README" button linked to new file populated for a README' do
+ page.within('.project-buttons') do
+ expect(page).to have_link('Add README', href: presenter.add_readme_path)
end
end
it '"Add license" button linked to new file populated for a license' do
- page.within('.project-metadata') do
+ page.within('.project-stats') do
expect(page).to have_link('Add license', href: presenter.add_license_path)
end
end
@@ -67,7 +67,7 @@ describe 'Projects > Show > User sees setup shortcut buttons' do
it '"Auto DevOps enabled" anchor linked to settings page' do
visit project_path(project)
- page.within('.project-stats') do
+ page.within('.project-buttons') do
expect(page).to have_link('Auto DevOps enabled', href: project_settings_ci_cd_path(project, anchor: 'autodevops-settings'))
end
end
@@ -77,7 +77,7 @@ describe 'Projects > Show > User sees setup shortcut buttons' do
let(:project) { create(:project, :public, :empty_repo, auto_devops_attributes: { enabled: false }) }
it '"Enable Auto DevOps" button linked to settings page' do
- page.within('.project-stats') do
+ page.within('.project-buttons') do
expect(page).to have_link('Enable Auto DevOps', href: project_settings_ci_cd_path(project, anchor: 'autodevops-settings'))
end
end
@@ -86,7 +86,7 @@ describe 'Projects > Show > User sees setup shortcut buttons' do
describe 'Kubernetes cluster button' do
it '"Add Kubernetes cluster" button linked to clusters page' do
- page.within('.project-stats') do
+ page.within('.project-buttons') do
expect(page).to have_link('Add Kubernetes cluster', href: new_project_cluster_path(project))
end
end
@@ -96,7 +96,7 @@ describe 'Projects > Show > User sees setup shortcut buttons' do
visit project_path(project)
- page.within('.project-stats') do
+ page.within('.project-buttons') do
expect(page).to have_link('Kubernetes configured', href: project_cluster_path(project, cluster))
end
end
@@ -119,7 +119,7 @@ describe 'Projects > Show > User sees setup shortcut buttons' do
it '"Auto DevOps enabled" button not linked' do
visit project_path(project)
- page.within('.project-stats') do
+ page.within('.project-buttons') do
expect(page).to have_text('Auto DevOps enabled')
end
end
@@ -129,14 +129,14 @@ describe 'Projects > Show > User sees setup shortcut buttons' do
let(:project) { create(:project, :public, :repository, auto_devops_attributes: { enabled: false }) }
it 'no Auto DevOps button if can not manage pipelines' do
- page.within('.project-stats') do
+ page.within('.project-buttons') do
expect(page).not_to have_link('Enable Auto DevOps')
expect(page).not_to have_link('Auto DevOps enabled')
end
end
it 'no Kubernetes cluster button if can not manage clusters' do
- page.within('.project-stats') do
+ page.within('.project-buttons') do
expect(page).not_to have_link('Add Kubernetes cluster')
expect(page).not_to have_link('Kubernetes configured')
end
@@ -151,59 +151,59 @@ describe 'Projects > Show > User sees setup shortcut buttons' do
sign_in(user)
end
- context 'Readme button' do
+ context 'README button' do
before do
allow(Project).to receive(:find_by_full_path)
.with(project.full_path, follow_redirects: true)
.and_return(project)
end
- context 'when the project has a populated Readme' do
- it 'show the "Readme" anchor' do
+ context 'when the project has a populated README' do
+ it 'show the "README" anchor' do
visit project_path(project)
expect(project.repository.readme).not_to be_nil
- page.within('.project-stats') do
- expect(page).not_to have_link('Add Readme', href: presenter.add_readme_path)
- expect(page).to have_link('Readme', href: presenter.readme_path)
+ page.within('.project-buttons') do
+ expect(page).not_to have_link('Add README', href: presenter.add_readme_path)
+ expect(page).to have_link('README', href: presenter.readme_path)
end
end
- context 'when the project has an empty Readme' do
- it 'show the "Readme" anchor' do
+ context 'when the project has an empty README' do
+ it 'show the "README" anchor' do
allow(project.repository).to receive(:readme).and_return(fake_blob(path: 'README.md', data: '', size: 0))
visit project_path(project)
- page.within('.project-stats') do
- expect(page).not_to have_link('Add Readme', href: presenter.add_readme_path)
- expect(page).to have_link('Readme', href: presenter.readme_path)
+ page.within('.project-buttons') do
+ expect(page).not_to have_link('Add README', href: presenter.add_readme_path)
+ expect(page).to have_link('README', href: presenter.readme_path)
end
end
end
end
- context 'when the project does not have a Readme' do
- it 'shows the "Add Readme" button' do
+ context 'when the project does not have a README' do
+ it 'shows the "Add README" button' do
allow(project.repository).to receive(:readme).and_return(nil)
visit project_path(project)
- page.within('.project-stats') do
- expect(page).to have_link('Add Readme', href: presenter.add_readme_path)
+ page.within('.project-buttons') do
+ expect(page).to have_link('Add README', href: presenter.add_readme_path)
end
end
end
end
- it 'no "Add Changelog" button if the project already has a changelog' do
+ it 'no "Add CHANGELOG" button if the project already has a changelog' do
visit project_path(project)
expect(project.repository.changelog).not_to be_nil
- page.within('.project-stats') do
- expect(page).not_to have_link('Add Changelog')
+ page.within('.project-buttons') do
+ expect(page).not_to have_link('Add CHANGELOG')
end
end
@@ -212,18 +212,18 @@ describe 'Projects > Show > User sees setup shortcut buttons' do
expect(project.repository.license_blob).not_to be_nil
- page.within('.project-stats') do
+ page.within('.project-buttons') do
expect(page).not_to have_link('Add license')
end
end
- it 'no "Add Contribution guide" button if the project already has a contribution guide' do
+ it 'no "Add CONTRIBUTING" button if the project already has a contribution guide' do
visit project_path(project)
expect(project.repository.contribution_guide).not_to be_nil
- page.within('.project-stats') do
- expect(page).not_to have_link('Add Contribution guide')
+ page.within('.project-buttons') do
+ expect(page).not_to have_link('Add CONTRIBUTING')
end
end
@@ -232,7 +232,7 @@ describe 'Projects > Show > User sees setup shortcut buttons' do
it 'no "Set up CI/CD" button if the project has Auto DevOps enabled' do
visit project_path(project)
- page.within('.project-stats') do
+ page.within('.project-buttons') do
expect(page).not_to have_link('Set up CI/CD')
end
end
@@ -246,7 +246,7 @@ describe 'Projects > Show > User sees setup shortcut buttons' do
expect(project.repository.gitlab_ci_yml).to be_nil
- page.within('.project-stats') do
+ page.within('.project-buttons') do
expect(page).to have_link('Set up CI/CD', href: presenter.add_ci_yml_path)
end
end
@@ -266,7 +266,7 @@ describe 'Projects > Show > User sees setup shortcut buttons' do
visit project_path(project)
- page.within('.project-stats') do
+ page.within('.project-buttons') do
expect(page).not_to have_link('Set up CI/CD')
end
end
@@ -278,7 +278,7 @@ describe 'Projects > Show > User sees setup shortcut buttons' do
it '"Auto DevOps enabled" anchor linked to settings page' do
visit project_path(project)
- page.within('.project-stats') do
+ page.within('.project-buttons') do
expect(page).to have_link('Auto DevOps enabled', href: project_settings_ci_cd_path(project, anchor: 'autodevops-settings'))
end
end
@@ -290,7 +290,7 @@ describe 'Projects > Show > User sees setup shortcut buttons' do
it '"Enable Auto DevOps" button linked to settings page' do
visit project_path(project)
- page.within('.project-stats') do
+ page.within('.project-buttons') do
expect(page).to have_link('Enable Auto DevOps', href: project_settings_ci_cd_path(project, anchor: 'autodevops-settings'))
end
end
@@ -302,7 +302,7 @@ describe 'Projects > Show > User sees setup shortcut buttons' do
expect(page).to have_selector('.js-autodevops-banner')
- page.within('.project-stats') do
+ page.within('.project-buttons') do
expect(page).not_to have_link('Enable Auto DevOps')
expect(page).not_to have_link('Auto DevOps enabled')
end
@@ -323,7 +323,7 @@ describe 'Projects > Show > User sees setup shortcut buttons' do
visit project_path(project)
- page.within('.project-stats') do
+ page.within('.project-buttons') do
expect(page).not_to have_link('Enable Auto DevOps')
expect(page).not_to have_link('Auto DevOps enabled')
end
@@ -335,7 +335,7 @@ describe 'Projects > Show > User sees setup shortcut buttons' do
it '"Add Kubernetes cluster" button linked to clusters page' do
visit project_path(project)
- page.within('.project-stats') do
+ page.within('.project-buttons') do
expect(page).to have_link('Add Kubernetes cluster', href: new_project_cluster_path(project))
end
end
@@ -345,7 +345,7 @@ describe 'Projects > Show > User sees setup shortcut buttons' do
visit project_path(project)
- page.within('.project-stats') do
+ page.within('.project-buttons') do
expect(page).to have_link('Kubernetes configured', href: project_cluster_path(project, cluster))
end
end
diff --git a/spec/features/tags/master_views_tags_spec.rb b/spec/features/tags/master_views_tags_spec.rb
index 3f4fe549f3e..36cfeb5ed84 100644
--- a/spec/features/tags/master_views_tags_spec.rb
+++ b/spec/features/tags/master_views_tags_spec.rb
@@ -13,7 +13,7 @@ describe 'Maintainer views tags' do
before do
visit project_path(project)
- click_on 'Add Readme'
+ click_on 'Add README'
fill_in :commit_message, with: 'Add a README file', visible: true
click_button 'Commit changes'
visit project_tags_path(project)
diff --git a/spec/finders/group_members_finder_spec.rb b/spec/finders/group_members_finder_spec.rb
index f545da3aee4..8975ea0f063 100644
--- a/spec/finders/group_members_finder_spec.rb
+++ b/spec/finders/group_members_finder_spec.rb
@@ -19,7 +19,7 @@ describe GroupMembersFinder, '#execute' do
end
it 'returns members for nested group', :nested_groups do
- group.add_maintainer(user2)
+ group.add_developer(user2)
nested_group.request_access(user4)
member1 = group.add_maintainer(user1)
member3 = nested_group.add_maintainer(user2)
diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb
index 515f6f70b99..80f7232f282 100644
--- a/spec/finders/issues_finder_spec.rb
+++ b/spec/finders/issues_finder_spec.rb
@@ -640,4 +640,131 @@ describe IssuesFinder do
end
end
end
+
+ describe '#use_subquery_for_search?' do
+ let(:finder) { described_class.new(nil, params) }
+
+ before do
+ allow(Gitlab::Database).to receive(:postgresql?).and_return(true)
+ stub_feature_flags(use_subquery_for_group_issues_search: true)
+ end
+
+ context 'when there is no search param' do
+ let(:params) { { attempt_group_search_optimizations: true } }
+
+ it 'returns false' do
+ expect(finder.use_subquery_for_search?).to be_falsey
+ end
+ end
+
+ context 'when the database is not Postgres' do
+ let(:params) { { search: 'foo', attempt_group_search_optimizations: true } }
+
+ before do
+ allow(Gitlab::Database).to receive(:postgresql?).and_return(false)
+ end
+
+ it 'returns false' do
+ expect(finder.use_subquery_for_search?).to be_falsey
+ end
+ end
+
+ context 'when the attempt_group_search_optimizations param is falsey' do
+ let(:params) { { search: 'foo' } }
+
+ it 'returns false' do
+ expect(finder.use_subquery_for_search?).to be_falsey
+ end
+ end
+
+ context 'when the use_subquery_for_group_issues_search flag is disabled' do
+ let(:params) { { search: 'foo', attempt_group_search_optimizations: true } }
+
+ before do
+ stub_feature_flags(use_subquery_for_group_issues_search: false)
+ end
+
+ it 'returns false' do
+ expect(finder.use_subquery_for_search?).to be_falsey
+ end
+ end
+
+ context 'when all conditions are met' do
+ let(:params) { { search: 'foo', attempt_group_search_optimizations: true } }
+
+ it 'returns true' do
+ expect(finder.use_subquery_for_search?).to be_truthy
+ end
+ end
+ end
+
+ describe '#use_cte_for_search?' do
+ let(:finder) { described_class.new(nil, params) }
+
+ before do
+ allow(Gitlab::Database).to receive(:postgresql?).and_return(true)
+ stub_feature_flags(use_cte_for_group_issues_search: true)
+ stub_feature_flags(use_subquery_for_group_issues_search: false)
+ end
+
+ context 'when there is no search param' do
+ let(:params) { { attempt_group_search_optimizations: true } }
+
+ it 'returns false' do
+ expect(finder.use_cte_for_search?).to be_falsey
+ end
+ end
+
+ context 'when the database is not Postgres' do
+ let(:params) { { search: 'foo', attempt_group_search_optimizations: true } }
+
+ before do
+ allow(Gitlab::Database).to receive(:postgresql?).and_return(false)
+ end
+
+ it 'returns false' do
+ expect(finder.use_cte_for_search?).to be_falsey
+ end
+ end
+
+ context 'when the attempt_group_search_optimizations param is falsey' do
+ let(:params) { { search: 'foo' } }
+
+ it 'returns false' do
+ expect(finder.use_cte_for_search?).to be_falsey
+ end
+ end
+
+ context 'when the use_cte_for_group_issues_search flag is disabled' do
+ let(:params) { { search: 'foo', attempt_group_search_optimizations: true } }
+
+ before do
+ stub_feature_flags(use_cte_for_group_issues_search: false)
+ end
+
+ it 'returns false' do
+ expect(finder.use_cte_for_search?).to be_falsey
+ end
+ end
+
+ context 'when use_subquery_for_search? is true' do
+ let(:params) { { search: 'foo', attempt_group_search_optimizations: true } }
+
+ before do
+ stub_feature_flags(use_subquery_for_group_issues_search: true)
+ end
+
+ it 'returns false' do
+ expect(finder.use_cte_for_search?).to be_falsey
+ end
+ end
+
+ context 'when all conditions are met' do
+ let(:params) { { search: 'foo', attempt_group_search_optimizations: true } }
+
+ it 'returns true' do
+ expect(finder.use_cte_for_search?).to be_truthy
+ end
+ end
+ end
end
diff --git a/spec/finders/projects/serverless/functions_finder_spec.rb b/spec/finders/projects/serverless/functions_finder_spec.rb
new file mode 100644
index 00000000000..60d02b12054
--- /dev/null
+++ b/spec/finders/projects/serverless/functions_finder_spec.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Projects::Serverless::FunctionsFinder do
+ include KubernetesHelpers
+ include ReactiveCachingHelpers
+
+ let(:user) { create(:user) }
+ let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+ let(:service) { cluster.platform_kubernetes }
+ let(:project) { cluster.project}
+
+ let(:namespace) do
+ create(:cluster_kubernetes_namespace,
+ cluster: cluster,
+ cluster_project: cluster.cluster_project,
+ project: cluster.cluster_project.project)
+ end
+
+ before do
+ project.add_maintainer(user)
+ end
+
+ describe 'retrieve data from knative' do
+ it 'does not have knative installed' do
+ expect(described_class.new(project.clusters).execute).to be_empty
+ end
+
+ context 'has knative installed' do
+ let!(:knative) { create(:clusters_applications_knative, :installed, cluster: cluster) }
+
+ it 'there are no functions' do
+ expect(described_class.new(project.clusters).execute).to be_empty
+ end
+
+ it 'there are functions', :use_clean_rails_memory_store_caching do
+ stub_reactive_cache(knative, services: kube_knative_services_body(namespace: namespace.namespace, name: cluster.project.name)["items"])
+
+ expect(described_class.new(project.clusters).execute).not_to be_empty
+ end
+ end
+ end
+
+ describe 'verify if knative is installed' do
+ context 'knative is not installed' do
+ it 'does not have knative installed' do
+ expect(described_class.new(project.clusters).installed?).to be false
+ end
+ end
+
+ context 'knative is installed' do
+ let!(:knative) { create(:clusters_applications_knative, :installed, cluster: cluster) }
+
+ it 'does have knative installed' do
+ expect(described_class.new(project.clusters).installed?).to be true
+ end
+ end
+ end
+end
diff --git a/spec/fixtures/api/schemas/cluster_status.json b/spec/fixtures/api/schemas/cluster_status.json
index ccef17a6615..3d9e0628f63 100644
--- a/spec/fixtures/api/schemas/cluster_status.json
+++ b/spec/fixtures/api/schemas/cluster_status.json
@@ -32,7 +32,8 @@
},
"status_reason": { "type": ["string", "null"] },
"external_ip": { "type": ["string", "null"] },
- "hostname": { "type": ["string", "null"] }
+ "hostname": { "type": ["string", "null"] },
+ "email": { "type": ["string", "null"] }
},
"required" : [ "name", "status" ]
}
diff --git a/spec/fixtures/api/schemas/job/trigger.json b/spec/fixtures/api/schemas/job/trigger.json
index 1c7e9cc7693..807178c662c 100644
--- a/spec/fixtures/api/schemas/job/trigger.json
+++ b/spec/fixtures/api/schemas/job/trigger.json
@@ -12,12 +12,11 @@
"type": "object",
"required": [
"key",
- "value",
"public"
],
"properties": {
"key": { "type": "string" },
- "value": { "type": "string" },
+ "value": { "type": "string", "optional": true },
"public": { "type": "boolean" }
},
"additionalProperties": false
diff --git a/spec/fixtures/bfg_object_map.txt b/spec/fixtures/bfg_object_map.txt
new file mode 100644
index 00000000000..c60171d8770
--- /dev/null
+++ b/spec/fixtures/bfg_object_map.txt
@@ -0,0 +1 @@
+f1d2d2f924e986ac86fdf7b36c94bcdf32beec15 e242ed3bffccdf271b7fbaf34ed72d089537b42f
diff --git a/spec/fixtures/security-reports/feature-branch/gl-dependency-scanning-report.json b/spec/fixtures/security-reports/feature-branch/gl-dependency-scanning-report.json
index 314f04107eb..ce66f562175 100644
--- a/spec/fixtures/security-reports/feature-branch/gl-dependency-scanning-report.json
+++ b/spec/fixtures/security-reports/feature-branch/gl-dependency-scanning-report.json
@@ -11,7 +11,13 @@
"name": "Gemnasium"
},
"location": {
- "file": "app/pom.xml"
+ "file": "app/pom.xml",
+ "dependency": {
+ "package": {
+ "name": "io.netty/netty"
+ },
+ "version": "3.9.1.Final"
+ }
},
"identifiers": [
{
@@ -55,7 +61,13 @@
"name": "Gemnasium"
},
"location": {
- "file": "app/requirements.txt"
+ "file": "app/requirements.txt",
+ "dependency": {
+ "package": {
+ "name": "Django"
+ },
+ "version": "1.11.3"
+ }
},
"identifiers": [
{
@@ -93,7 +105,13 @@
"name": "Gemnasium"
},
"location": {
- "file": "rails/Gemfile.lock"
+ "file": "rails/Gemfile.lock",
+ "dependency": {
+ "package": {
+ "name": "nokogiri"
+ },
+ "version": "1.8.0"
+ }
},
"identifiers": [
{
@@ -131,7 +149,13 @@
"name": "bundler-audit"
},
"location": {
- "file": "sast-sample-rails/Gemfile.lock"
+ "file": "sast-sample-rails/Gemfile.lock",
+ "dependency": {
+ "package": {
+ "name": "ffi"
+ },
+ "version": "1.9.18"
+ }
},
"identifiers": [
{
diff --git a/spec/fixtures/security-reports/master/gl-dependency-scanning-report.json b/spec/fixtures/security-reports/master/gl-dependency-scanning-report.json
index 314f04107eb..ce66f562175 100644
--- a/spec/fixtures/security-reports/master/gl-dependency-scanning-report.json
+++ b/spec/fixtures/security-reports/master/gl-dependency-scanning-report.json
@@ -11,7 +11,13 @@
"name": "Gemnasium"
},
"location": {
- "file": "app/pom.xml"
+ "file": "app/pom.xml",
+ "dependency": {
+ "package": {
+ "name": "io.netty/netty"
+ },
+ "version": "3.9.1.Final"
+ }
},
"identifiers": [
{
@@ -55,7 +61,13 @@
"name": "Gemnasium"
},
"location": {
- "file": "app/requirements.txt"
+ "file": "app/requirements.txt",
+ "dependency": {
+ "package": {
+ "name": "Django"
+ },
+ "version": "1.11.3"
+ }
},
"identifiers": [
{
@@ -93,7 +105,13 @@
"name": "Gemnasium"
},
"location": {
- "file": "rails/Gemfile.lock"
+ "file": "rails/Gemfile.lock",
+ "dependency": {
+ "package": {
+ "name": "nokogiri"
+ },
+ "version": "1.8.0"
+ }
},
"identifiers": [
{
@@ -131,7 +149,13 @@
"name": "bundler-audit"
},
"location": {
- "file": "sast-sample-rails/Gemfile.lock"
+ "file": "sast-sample-rails/Gemfile.lock",
+ "dependency": {
+ "package": {
+ "name": "ffi"
+ },
+ "version": "1.9.18"
+ }
},
"identifiers": [
{
diff --git a/spec/frontend/.eslintrc.yml b/spec/frontend/.eslintrc.yml
index 6d73977a891..046215e4c93 100644
--- a/spec/frontend/.eslintrc.yml
+++ b/spec/frontend/.eslintrc.yml
@@ -6,4 +6,4 @@ plugins:
settings:
import/resolver:
jest:
- jestConfigFile: "config/jest.config.js"
+ jestConfigFile: "jest.config.js"
diff --git a/spec/frontend/dummy_spec.js b/spec/frontend/dummy_spec.js
deleted file mode 100644
index 2bfef25e9c6..00000000000
--- a/spec/frontend/dummy_spec.js
+++ /dev/null
@@ -1 +0,0 @@
-it('does nothing', () => {});
diff --git a/spec/frontend/helpers/test_constants.js b/spec/frontend/helpers/test_constants.js
new file mode 100644
index 00000000000..8dc4aef87e1
--- /dev/null
+++ b/spec/frontend/helpers/test_constants.js
@@ -0,0 +1,2 @@
+// eslint-disable-next-line import/prefer-default-export
+export const TEST_HOST = 'http://test.host';
diff --git a/spec/javascripts/pages/profiles/show/emoji_menu_spec.js b/spec/frontend/pages/profiles/show/emoji_menu_spec.js
index 864bda65736..efc338b36eb 100644
--- a/spec/javascripts/pages/profiles/show/emoji_menu_spec.js
+++ b/spec/frontend/pages/profiles/show/emoji_menu_spec.js
@@ -1,7 +1,7 @@
import $ from 'jquery';
import axios from '~/lib/utils/axios_utils';
import EmojiMenu from '~/pages/profiles/show/emoji_menu';
-import { TEST_HOST } from 'spec/test_constants';
+import { TEST_HOST } from 'helpers/test_constants';
describe('EmojiMenu', () => {
const dummyEmojiTag = '<dummy></tag>';
@@ -56,7 +56,7 @@ describe('EmojiMenu', () => {
});
it('does not make an axios requst', done => {
- spyOn(axios, 'request').and.stub();
+ jest.spyOn(axios, 'request').mockReturnValue();
emojiMenu.addAward(dummyVotesBlock(), dummyAwardUrl, dummyEmoji, false, () => {
expect(axios.request).not.toHaveBeenCalled();
@@ -67,7 +67,7 @@ describe('EmojiMenu', () => {
describe('bindEvents', () => {
beforeEach(() => {
- spyOn(emojiMenu, 'registerEventListener').and.stub();
+ jest.spyOn(emojiMenu, 'registerEventListener').mockReturnValue();
});
it('binds event listeners to custom toggle button', () => {
diff --git a/spec/frontend/test_setup.js b/spec/frontend/test_setup.js
new file mode 100644
index 00000000000..7ad2e97e7e6
--- /dev/null
+++ b/spec/frontend/test_setup.js
@@ -0,0 +1,16 @@
+const testTimeoutInMs = 300;
+jest.setTimeout(testTimeoutInMs);
+
+let testStartTime;
+
+// https://github.com/facebook/jest/issues/6947
+beforeEach(() => {
+ testStartTime = Date.now();
+});
+
+afterEach(() => {
+ const elapsedTimeInMs = Date.now() - testStartTime;
+ if (elapsedTimeInMs > testTimeoutInMs) {
+ throw new Error(`Test took too long (${elapsedTimeInMs}ms > ${testTimeoutInMs}ms)!`);
+ }
+});
diff --git a/spec/javascripts/vue_shared/components/notes/timeline_entry_item_spec.js b/spec/frontend/vue_shared/components/notes/timeline_entry_item_spec.js
index c15635f2105..c15635f2105 100644
--- a/spec/javascripts/vue_shared/components/notes/timeline_entry_item_spec.js
+++ b/spec/frontend/vue_shared/components/notes/timeline_entry_item_spec.js
diff --git a/spec/helpers/emails_helper_spec.rb b/spec/helpers/emails_helper_spec.rb
index 139387e0b24..3820cf5cb9d 100644
--- a/spec/helpers/emails_helper_spec.rb
+++ b/spec/helpers/emails_helper_spec.rb
@@ -73,4 +73,59 @@ describe EmailsHelper do
end
end
end
+
+ describe '#create_list_id_string' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:full_path, :list_id_path) do
+ "01234" | "01234"
+ "5/0123" | "012.."
+ "45/012" | "012.."
+ "012" | "012"
+ "23/01" | "01.23"
+ "2/01" | "01.2"
+ "234/01" | "01.."
+ "4/2/0" | "0.2.4"
+ "45/2/0" | "0.2.."
+ "5/23/0" | "0.."
+ "0-2/5" | "5.0-2"
+ "0_2/5" | "5.0-2"
+ "0.2/5" | "5.0-2"
+ end
+
+ with_them do
+ it 'ellipcizes different variants' do
+ project = double("project")
+ allow(project).to receive(:full_path).and_return(full_path)
+ allow(project).to receive(:id).and_return(12345)
+ # Set a max length that gives only 5 chars for the project full path
+ max_length = "12345..#{Gitlab.config.gitlab.host}".length + 5
+ list_id = create_list_id_string(project, max_length)
+
+ expect(list_id).to eq("12345.#{list_id_path}.#{Gitlab.config.gitlab.host}")
+ expect(list_id).to satisfy { |s| s.length <= max_length }
+ end
+ end
+ end
+
+ describe 'Create realistic List-Id identifier' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:full_path, :list_id_path) do
+ "gitlab-org/gitlab-ce" | "gitlab-ce.gitlab-org"
+ "project-name/subproject_name/my.project" | "my-project.subproject-name.project-name"
+ end
+
+ with_them do
+ it 'Produces the right List-Id' do
+ project = double("project")
+ allow(project).to receive(:full_path).and_return(full_path)
+ allow(project).to receive(:id).and_return(12345)
+ list_id = create_list_id_string(project)
+
+ expect(list_id).to eq("12345.#{list_id_path}.#{Gitlab.config.gitlab.host}")
+ expect(list_id).to satisfy { |s| s.length <= 255 }
+ end
+ end
+ end
end
diff --git a/spec/helpers/events_helper_spec.rb b/spec/helpers/events_helper_spec.rb
index 8d0679e5699..3d15306d4d2 100644
--- a/spec/helpers/events_helper_spec.rb
+++ b/spec/helpers/events_helper_spec.rb
@@ -84,4 +84,36 @@ describe EventsHelper do
expect(helper.event_feed_url(event)).to eq(push_event_feed_url(event))
end
end
+
+ describe '#event_note_target_url' do
+ let(:project) { create(:project, :public, :repository) }
+ let(:event) { create(:event, project: project) }
+ let(:project_base_url) { namespace_project_url(namespace_id: project.namespace, id: project) }
+
+ subject { helper.event_note_target_url(event) }
+
+ it 'returns a commit note url' do
+ event.target = create(:note_on_commit, note: '+1 from me')
+
+ expect(subject).to eq("#{project_base_url}/commit/#{event.target.commit_id}#note_#{event.target.id}")
+ end
+
+ it 'returns a project snippet note url' do
+ event.target = create(:note, :on_snippet, note: 'keep going')
+
+ expect(subject).to eq("#{project_base_url}/snippets/#{event.note_target.id}#note_#{event.target.id}")
+ end
+
+ it 'returns a project issue url' do
+ event.target = create(:note_on_issue, note: 'nice work')
+
+ expect(subject).to eq("#{project_base_url}/issues/#{event.note_target.iid}#note_#{event.target.id}")
+ end
+
+ it 'returns a merge request url' do
+ event.target = create(:note_on_merge_request, note: 'LGTM!')
+
+ expect(subject).to eq("#{project_base_url}/merge_requests/#{event.note_target.iid}#note_#{event.target.id}")
+ end
+ end
end
diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb
index 976b6c312b4..486416c3370 100644
--- a/spec/helpers/projects_helper_spec.rb
+++ b/spec/helpers/projects_helper_spec.rb
@@ -229,6 +229,18 @@ describe ProjectsHelper do
end
end
+ describe '#link_to_project' do
+ let(:group) { create(:group, name: 'group name with space') }
+ let(:project) { create(:project, group: group, name: 'project name with space') }
+ subject { link_to_project(project) }
+
+ it 'returns an HTML link to the project' do
+ expect(subject).to match(%r{/#{group.full_path}/#{project.path}})
+ expect(subject).to include('group name with space /')
+ expect(subject).to include('project name with space')
+ end
+ end
+
describe '#link_to_member_avatar' do
let(:user) { build_stubbed(:user) }
let(:expected) { double }
@@ -471,6 +483,31 @@ describe ProjectsHelper do
end
end
+ describe 'link_to_bfg' do
+ subject { helper.link_to_bfg }
+
+ it 'generates a hardcoded link to the BFG Repo-Cleaner' do
+ result = helper.link_to_bfg
+ doc = Nokogiri::HTML.fragment(result)
+
+ expect(doc.children.size).to eq(1)
+
+ link = doc.children.first
+
+ aggregate_failures do
+ expect(result).to be_html_safe
+
+ expect(link.name).to eq('a')
+ expect(link[:target]).to eq('_blank')
+ expect(link[:rel]).to eq('noopener noreferrer')
+ expect(link[:href]).to eq('https://rtyley.github.io/bfg-repo-cleaner/')
+ expect(link.inner_html).to eq('BFG')
+
+ expect(result).to be_html_safe
+ end
+ end
+ end
+
describe '#legacy_render_context' do
it 'returns the redcarpet engine' do
params = { legacy_render: '1' }
diff --git a/spec/helpers/sorting_helper_spec.rb b/spec/helpers/sorting_helper_spec.rb
new file mode 100644
index 00000000000..f405268d198
--- /dev/null
+++ b/spec/helpers/sorting_helper_spec.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+describe SortingHelper do
+ include ApplicationHelper
+ include IconsHelper
+
+ describe '#issuable_sort_option_title' do
+ it 'returns correct title for issuable_sort_option_overrides key' do
+ expect(issuable_sort_option_title('created_asc')).to eq('Created date')
+ end
+
+ it 'returns correct title for a valid sort value' do
+ expect(issuable_sort_option_title('priority')).to eq('Priority')
+ end
+
+ it 'returns nil for invalid sort value' do
+ expect(issuable_sort_option_title('invalid_key')).to eq(nil)
+ end
+ end
+
+ describe '#issuable_sort_direction_button' do
+ before do
+ allow(self).to receive(:request).and_return(double(path: 'http://test.com', query_parameters: { label_name: 'test_label' }))
+ end
+
+ it 'keeps label filter param' do
+ expect(issuable_sort_direction_button('created_date')).to include('label_name=test_label')
+ end
+
+ it 'returns icon with sort-highest when sort is created_date' do
+ expect(issuable_sort_direction_button('created_date')).to include('sort-highest')
+ end
+
+ it 'returns icon with sort-lowest when sort is asc' do
+ expect(issuable_sort_direction_button('created_asc')).to include('sort-lowest')
+ end
+
+ it 'returns icon with sort-lowest when sorting by milestone' do
+ expect(issuable_sort_direction_button('milestone')).to include('sort-lowest')
+ end
+
+ it 'returns icon with sort-lowest when sorting by due_date' do
+ expect(issuable_sort_direction_button('due_date')).to include('sort-lowest')
+ end
+ end
+end
diff --git a/spec/initializers/lograge_spec.rb b/spec/initializers/lograge_spec.rb
new file mode 100644
index 00000000000..af54a777373
--- /dev/null
+++ b/spec/initializers/lograge_spec.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'lograge', type: :request do
+ let(:headers) { { 'X-Request-ID' => 'new-correlation-id' } }
+
+ context 'for API requests' do
+ subject { get("/api/v4/endpoint", {}, headers) }
+
+ it 'logs to api_json log' do
+ # we assert receiving parameters by grape logger
+ expect_any_instance_of(Gitlab::GrapeLogging::Formatters::LogrageWithTimestamp).to receive(:call)
+ .with(anything, anything, anything, a_hash_including("correlation_id" => "new-correlation-id"))
+ .and_call_original
+
+ subject
+ end
+ end
+
+ context 'for Controller requests' do
+ subject { get("/", {}, headers) }
+
+ it 'logs to production_json log' do
+ # formatter receives a hash with correlation id
+ expect(Lograge.formatter).to receive(:call)
+ .with(a_hash_including("correlation_id" => "new-correlation-id"))
+ .and_call_original
+
+ # a log file receives a line with correlation id
+ expect(Lograge.logger).to receive(:send)
+ .with(anything, include('"correlation_id":"new-correlation-id"'))
+ .and_call_original
+
+ subject
+ end
+ end
+end
diff --git a/spec/javascripts/api_spec.js b/spec/javascripts/api_spec.js
index 7de38913bae..9d55c615450 100644
--- a/spec/javascripts/api_spec.js
+++ b/spec/javascripts/api_spec.js
@@ -180,6 +180,23 @@ describe('Api', () => {
});
});
+ describe('projectRunners', () => {
+ it('fetches the runners of a project', done => {
+ const projectPath = 7;
+ const params = { scope: 'active' };
+ const mockData = [{ id: 4 }];
+ const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectPath}/runners`;
+ mock.onGet(expectedUrl, { params }).reply(200, mockData);
+
+ Api.projectRunners(projectPath, { params })
+ .then(({ data }) => {
+ expect(data).toEqual(mockData);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
describe('newLabel', () => {
it('creates a new label', done => {
const namespace = 'some namespace';
@@ -316,6 +333,40 @@ describe('Api', () => {
});
});
+ describe('user', () => {
+ it('fetches single user', done => {
+ const userId = '123456';
+ const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/users/${userId}`;
+ mock.onGet(expectedUrl).reply(200, {
+ name: 'testuser',
+ });
+
+ Api.user(userId)
+ .then(({ data }) => {
+ expect(data.name).toBe('testuser');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
+ describe('user status', () => {
+ it('fetches single user status', done => {
+ const userId = '123456';
+ const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/users/${userId}/status`;
+ mock.onGet(expectedUrl).reply(200, {
+ message: 'testmessage',
+ });
+
+ Api.userStatus(userId)
+ .then(({ data }) => {
+ expect(data.message).toBe('testmessage');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
describe('commitPipelines', () => {
it('fetches pipelines for a given commit', done => {
const projectId = 'example/foobar';
diff --git a/spec/javascripts/boards/mock_data.js b/spec/javascripts/boards/mock_data.js
index c28e41ec175..14fff9223f4 100644
--- a/spec/javascripts/boards/mock_data.js
+++ b/spec/javascripts/boards/mock_data.js
@@ -1,5 +1,11 @@
import BoardService from '~/boards/services/board_service';
+export const boardObj = {
+ id: 1,
+ name: 'test',
+ milestone_id: null,
+};
+
export const listObj = {
id: 300,
position: 0,
@@ -40,6 +46,12 @@ export const BoardsMockData = {
},
],
},
+ '/test/issue-boards/milestones.json': [
+ {
+ id: 1,
+ title: 'test',
+ },
+ ],
},
POST: {
'/test/-/boards/1/lists': listObj,
@@ -70,3 +82,60 @@ export const mockBoardService = (opts = {}) => {
boardId,
});
};
+
+export const mockAssigneesList = [
+ {
+ id: 2,
+ name: 'Terrell Graham',
+ username: 'monserrate.gleichner',
+ state: 'active',
+ avatar_url: 'https://www.gravatar.com/avatar/598fd02741ac58b88854a99d16704309?s=80&d=identicon',
+ web_url: 'http://127.0.0.1:3001/monserrate.gleichner',
+ path: '/monserrate.gleichner',
+ },
+ {
+ id: 12,
+ name: 'Susy Johnson',
+ username: 'tana_harvey',
+ state: 'active',
+ avatar_url: 'https://www.gravatar.com/avatar/e021a7b0f3e4ae53b5068d487e68c031?s=80&d=identicon',
+ web_url: 'http://127.0.0.1:3001/tana_harvey',
+ path: '/tana_harvey',
+ },
+ {
+ id: 20,
+ name: 'Conchita Eichmann',
+ username: 'juliana_gulgowski',
+ state: 'active',
+ avatar_url: 'https://www.gravatar.com/avatar/c43c506cb6fd7b37017d3b54b94aa937?s=80&d=identicon',
+ web_url: 'http://127.0.0.1:3001/juliana_gulgowski',
+ path: '/juliana_gulgowski',
+ },
+ {
+ id: 6,
+ name: 'Bryce Turcotte',
+ username: 'melynda',
+ state: 'active',
+ avatar_url: 'https://www.gravatar.com/avatar/cc2518f2c6f19f8fac49e1a5ee092a9b?s=80&d=identicon',
+ web_url: 'http://127.0.0.1:3001/melynda',
+ path: '/melynda',
+ },
+ {
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ state: 'active',
+ avatar_url: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ web_url: 'http://127.0.0.1:3001/root',
+ path: '/root',
+ },
+];
+
+export const mockMilestone = {
+ id: 1,
+ state: 'active',
+ title: 'Milestone title',
+ description: 'Harum corporis aut consequatur quae dolorem error sequi quia.',
+ start_date: '2018-01-01',
+ due_date: '2019-12-31',
+};
diff --git a/spec/javascripts/clusters/components/applications_spec.js b/spec/javascripts/clusters/components/applications_spec.js
index 928bf70f3a2..14ef1193984 100644
--- a/spec/javascripts/clusters/components/applications_spec.js
+++ b/spec/javascripts/clusters/components/applications_spec.js
@@ -1,5 +1,6 @@
import Vue from 'vue';
import applications from '~/clusters/components/applications.vue';
+import { CLUSTER_TYPE } from '~/clusters/constants';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('Applications', () => {
@@ -14,9 +15,10 @@ describe('Applications', () => {
vm.$destroy();
});
- describe('', () => {
+ describe('Project cluster applications', () => {
beforeEach(() => {
vm = mountComponent(Applications, {
+ type: CLUSTER_TYPE.PROJECT,
applications: {
helm: { title: 'Helm Tiller' },
ingress: { title: 'Ingress' },
@@ -30,31 +32,76 @@ describe('Applications', () => {
});
it('renders a row for Helm Tiller', () => {
- expect(vm.$el.querySelector('.js-cluster-application-row-helm')).toBeDefined();
+ expect(vm.$el.querySelector('.js-cluster-application-row-helm')).not.toBeNull();
});
it('renders a row for Ingress', () => {
- expect(vm.$el.querySelector('.js-cluster-application-row-ingress')).toBeDefined();
+ expect(vm.$el.querySelector('.js-cluster-application-row-ingress')).not.toBeNull();
});
it('renders a row for Cert-Manager', () => {
- expect(vm.$el.querySelector('.js-cluster-application-row-cert_manager')).toBeDefined();
+ expect(vm.$el.querySelector('.js-cluster-application-row-cert_manager')).not.toBeNull();
});
it('renders a row for Prometheus', () => {
- expect(vm.$el.querySelector('.js-cluster-application-row-prometheus')).toBeDefined();
+ expect(vm.$el.querySelector('.js-cluster-application-row-prometheus')).not.toBeNull();
});
it('renders a row for GitLab Runner', () => {
- expect(vm.$el.querySelector('.js-cluster-application-row-runner')).toBeDefined();
+ expect(vm.$el.querySelector('.js-cluster-application-row-runner')).not.toBeNull();
});
it('renders a row for Jupyter', () => {
- expect(vm.$el.querySelector('.js-cluster-application-row-jupyter')).not.toBe(null);
+ expect(vm.$el.querySelector('.js-cluster-application-row-jupyter')).not.toBeNull();
});
it('renders a row for Knative', () => {
- expect(vm.$el.querySelector('.js-cluster-application-row-knative')).not.toBe(null);
+ expect(vm.$el.querySelector('.js-cluster-application-row-knative')).not.toBeNull();
+ });
+ });
+
+ describe('Group cluster applications', () => {
+ beforeEach(() => {
+ vm = mountComponent(Applications, {
+ type: CLUSTER_TYPE.GROUP,
+ applications: {
+ helm: { title: 'Helm Tiller' },
+ ingress: { title: 'Ingress' },
+ cert_manager: { title: 'Cert-Manager' },
+ runner: { title: 'GitLab Runner' },
+ prometheus: { title: 'Prometheus' },
+ jupyter: { title: 'JupyterHub' },
+ knative: { title: 'Knative' },
+ },
+ });
+ });
+
+ it('renders a row for Helm Tiller', () => {
+ expect(vm.$el.querySelector('.js-cluster-application-row-helm')).not.toBeNull();
+ });
+
+ it('renders a row for Ingress', () => {
+ expect(vm.$el.querySelector('.js-cluster-application-row-ingress')).not.toBeNull();
+ });
+
+ it('renders a row for Cert-Manager', () => {
+ expect(vm.$el.querySelector('.js-cluster-application-row-cert_manager')).not.toBeNull();
+ });
+
+ it('renders a row for Prometheus', () => {
+ expect(vm.$el.querySelector('.js-cluster-application-row-prometheus')).toBeNull();
+ });
+
+ it('renders a row for GitLab Runner', () => {
+ expect(vm.$el.querySelector('.js-cluster-application-row-runner')).toBeNull();
+ });
+
+ it('renders a row for Jupyter', () => {
+ expect(vm.$el.querySelector('.js-cluster-application-row-jupyter')).toBeNull();
+ });
+
+ it('renders a row for Knative', () => {
+ expect(vm.$el.querySelector('.js-cluster-application-row-knative')).toBeNull();
});
});
@@ -129,6 +176,54 @@ describe('Applications', () => {
});
});
+ describe('Cert-Manager application', () => {
+ describe('when not installed', () => {
+ it('renders email & allows editing', () => {
+ vm = mountComponent(Applications, {
+ applications: {
+ helm: { title: 'Helm Tiller', status: 'installed' },
+ ingress: { title: 'Ingress', status: 'installed', externalIp: '1.1.1.1' },
+ cert_manager: {
+ title: 'Cert-Manager',
+ email: 'before@example.com',
+ status: 'installable',
+ },
+ runner: { title: 'GitLab Runner' },
+ prometheus: { title: 'Prometheus' },
+ jupyter: { title: 'JupyterHub', hostname: '', status: 'installable' },
+ knative: { title: 'Knative', hostname: '', status: 'installable' },
+ },
+ });
+
+ expect(vm.$el.querySelector('.js-email').value).toEqual('before@example.com');
+ expect(vm.$el.querySelector('.js-email').getAttribute('readonly')).toBe(null);
+ });
+ });
+
+ describe('when installed', () => {
+ it('renders email in readonly', () => {
+ vm = mountComponent(Applications, {
+ applications: {
+ helm: { title: 'Helm Tiller', status: 'installed' },
+ ingress: { title: 'Ingress', status: 'installed', externalIp: '1.1.1.1' },
+ cert_manager: {
+ title: 'Cert-Manager',
+ email: 'after@example.com',
+ status: 'installed',
+ },
+ runner: { title: 'GitLab Runner' },
+ prometheus: { title: 'Prometheus' },
+ jupyter: { title: 'JupyterHub', hostname: '', status: 'installable' },
+ knative: { title: 'Knative', hostname: '', status: 'installable' },
+ },
+ });
+
+ expect(vm.$el.querySelector('.js-email').value).toEqual('after@example.com');
+ expect(vm.$el.querySelector('.js-email').getAttribute('readonly')).toEqual('readonly');
+ });
+ });
+ });
+
describe('Jupyter application', () => {
describe('with ingress installed with ip & jupyter installable', () => {
it('renders hostname active input', () => {
diff --git a/spec/javascripts/clusters/services/mock_data.js b/spec/javascripts/clusters/services/mock_data.js
index 540d7f30858..3c3d9977ffb 100644
--- a/spec/javascripts/clusters/services/mock_data.js
+++ b/spec/javascripts/clusters/services/mock_data.js
@@ -42,6 +42,7 @@ const CLUSTERS_MOCK_DATA = {
name: 'cert_manager',
status: APPLICATION_STATUS.ERROR,
status_reason: 'Cannot connect',
+ email: 'test@example.com',
},
],
},
@@ -86,6 +87,7 @@ const CLUSTERS_MOCK_DATA = {
name: 'cert_manager',
status: APPLICATION_STATUS.ERROR,
status_reason: 'Cannot connect',
+ email: 'test@example.com',
},
],
},
diff --git a/spec/javascripts/clusters/stores/clusters_store_spec.js b/spec/javascripts/clusters/stores/clusters_store_spec.js
index 7ea0878ad45..1ca55549094 100644
--- a/spec/javascripts/clusters/stores/clusters_store_spec.js
+++ b/spec/javascripts/clusters/stores/clusters_store_spec.js
@@ -115,6 +115,7 @@ describe('Clusters Store', () => {
statusReason: mockResponseData.applications[6].status_reason,
requestStatus: null,
requestReason: null,
+ email: mockResponseData.applications[6].email,
},
},
});
diff --git a/spec/javascripts/diffs/components/app_spec.js b/spec/javascripts/diffs/components/app_spec.js
index 1e2f7ff4fd8..a2cbc0f3c72 100644
--- a/spec/javascripts/diffs/components/app_spec.js
+++ b/spec/javascripts/diffs/components/app_spec.js
@@ -1,33 +1,44 @@
-import Vue from 'vue';
-import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
+import Vuex from 'vuex';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
import { TEST_HOST } from 'spec/test_constants';
import App from '~/diffs/components/app.vue';
+import NoChanges from '~/diffs/components/no_changes.vue';
+import DiffFile from '~/diffs/components/diff_file.vue';
import createDiffsStore from '../create_diffs_store';
describe('diffs/components/app', () => {
const oldMrTabs = window.mrTabs;
- const Component = Vue.extend(App);
-
+ let store;
let vm;
- beforeEach(() => {
- // setup globals (needed for component to mount :/)
- window.mrTabs = jasmine.createSpyObj('mrTabs', ['resetViewContainer']);
- window.mrTabs.expandViewContainer = jasmine.createSpy();
- window.location.hash = 'ABC_123';
+ function createComponent(props = {}, extendStore = () => {}) {
+ const localVue = createLocalVue();
- // setup component
- const store = createDiffsStore();
+ localVue.use(Vuex);
+
+ store = createDiffsStore();
store.state.diffs.isLoading = false;
- vm = mountComponentWithStore(Component, {
- store,
- props: {
+ extendStore(store);
+
+ vm = shallowMount(localVue.extend(App), {
+ localVue,
+ propsData: {
endpoint: `${TEST_HOST}/diff/endpoint`,
projectPath: 'namespace/project',
currentUser: {},
+ changesEmptyStateIllustration: '',
+ ...props,
},
+ store,
});
+ }
+
+ beforeEach(() => {
+ // setup globals (needed for component to mount :/)
+ window.mrTabs = jasmine.createSpyObj('mrTabs', ['resetViewContainer']);
+ window.mrTabs.expandViewContainer = jasmine.createSpy();
+ window.location.hash = 'ABC_123';
});
afterEach(() => {
@@ -35,21 +46,53 @@ describe('diffs/components/app', () => {
window.mrTabs = oldMrTabs;
// reset component
- vm.$destroy();
+ vm.destroy();
});
it('does not show commit info', () => {
- expect(vm.$el).not.toContainElement('.blob-commit-info');
+ createComponent();
+
+ expect(vm.contains('.blob-commit-info')).toBe(false);
});
it('sets highlighted row if hash exists in location object', done => {
- vm.$props.shouldShow = true;
-
- vm.$nextTick()
- .then(() => {
- expect(vm.$store.state.diffs.highlightedRow).toBe('ABC_123');
- })
- .then(done)
- .catch(done.fail);
+ createComponent({
+ shouldShow: true,
+ });
+
+ // Component uses $nextTick so we wait until that has finished
+ setTimeout(() => {
+ expect(store.state.diffs.highlightedRow).toBe('ABC_123');
+
+ done();
+ });
+ });
+
+ describe('empty state', () => {
+ it('renders empty state when no diff files exist', () => {
+ createComponent();
+
+ expect(vm.contains(NoChanges)).toBe(true);
+ });
+
+ it('does not render empty state when diff files exist', () => {
+ createComponent({}, () => {
+ store.state.diffs.diffFiles.push({
+ id: 1,
+ });
+ });
+
+ expect(vm.contains(NoChanges)).toBe(false);
+ expect(vm.findAll(DiffFile).length).toBe(1);
+ });
+
+ it('does not render empty state when versions match', () => {
+ createComponent({}, () => {
+ store.state.diffs.startVersion = { version_index: 1 };
+ store.state.diffs.mergeRequestDiff = { version_index: 1 };
+ });
+
+ expect(vm.contains(NoChanges)).toBe(false);
+ });
});
});
diff --git a/spec/javascripts/diffs/components/diff_file_spec.js b/spec/javascripts/diffs/components/diff_file_spec.js
index 51bb4807960..1af49282c36 100644
--- a/spec/javascripts/diffs/components/diff_file_spec.js
+++ b/spec/javascripts/diffs/components/diff_file_spec.js
@@ -74,6 +74,32 @@ describe('DiffFile', () => {
});
});
+ it('should be collapsed for renamed files', done => {
+ vm.file.renderIt = true;
+ vm.file.collapsed = false;
+ vm.file.highlighted_diff_lines = null;
+ vm.file.renamed_file = true;
+
+ vm.$nextTick(() => {
+ expect(vm.$el.innerText).not.toContain('This diff is collapsed');
+
+ done();
+ });
+ });
+
+ it('should be collapsed for mode changed files', done => {
+ vm.file.renderIt = true;
+ vm.file.collapsed = false;
+ vm.file.highlighted_diff_lines = null;
+ vm.file.mode_changed = true;
+
+ vm.$nextTick(() => {
+ expect(vm.$el.innerText).not.toContain('This diff is collapsed');
+
+ done();
+ });
+ });
+
it('should have loading icon while loading a collapsed diffs', done => {
vm.file.collapsed = true;
vm.isLoadingCollapsedDiff = true;
diff --git a/spec/javascripts/diffs/components/no_changes_spec.js b/spec/javascripts/diffs/components/no_changes_spec.js
index 7237274eb43..e45d34bf9d5 100644
--- a/spec/javascripts/diffs/components/no_changes_spec.js
+++ b/spec/javascripts/diffs/components/no_changes_spec.js
@@ -1 +1,40 @@
-// TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034
+import { createLocalVue, shallowMount } from '@vue/test-utils';
+import Vuex from 'vuex';
+import { createStore } from '~/mr_notes/stores';
+import NoChanges from '~/diffs/components/no_changes.vue';
+
+describe('Diff no changes empty state', () => {
+ let vm;
+
+ function createComponent(extendStore = () => {}) {
+ const localVue = createLocalVue();
+ localVue.use(Vuex);
+
+ const store = createStore();
+ extendStore(store);
+
+ vm = shallowMount(localVue.extend(NoChanges), {
+ localVue,
+ store,
+ propsData: {
+ changesEmptyStateIllustration: '',
+ },
+ });
+ }
+
+ afterEach(() => {
+ vm.destroy();
+ });
+
+ it('prevents XSS', () => {
+ createComponent(store => {
+ // eslint-disable-next-line no-param-reassign
+ store.state.notes.noteableData = {
+ source_branch: '<script>alert("test");</script>',
+ target_branch: '<script>alert("test");</script>',
+ };
+ });
+
+ expect(vm.contains('script')).toBe(false);
+ });
+});
diff --git a/spec/javascripts/diffs/mock_data/diff_discussions.js b/spec/javascripts/diffs/mock_data/diff_discussions.js
index 5ffe5a366ba..44313caba29 100644
--- a/spec/javascripts/diffs/mock_data/diff_discussions.js
+++ b/spec/javascripts/diffs/mock_data/diff_discussions.js
@@ -489,8 +489,6 @@ export default {
diff_discussion: true,
truncated_diff_lines:
'<tr class="line_holder new" id="">\n<td class="diff-line-num new old_line" data-linenumber="1">\n \n</td>\n<td class="diff-line-num new new_line" data-linenumber="1">\n1\n</td>\n<td class="line_content new noteable_line"><span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n</td>\n</tr>\n<tr class="line_holder new" id="">\n<td class="diff-line-num new old_line" data-linenumber="1">\n \n</td>\n<td class="diff-line-num new new_line" data-linenumber="2">\n2\n</td>\n<td class="line_content new noteable_line"><span id="LC2" class="line" lang="plaintext"></span>\n</td>\n</tr>\n',
- image_diff_html:
- '<div class="image js-replaced-image" data="">\n<div class="two-up view">\n<div class="wrap">\n<div class="frame deleted">\n<img alt="CHANGELOG" src="http://localhost:3000/gitlab-org/gitlab-test/raw/e63f41fe459e62e1228fcef60d7189127aeba95a/CHANGELOG" />\n</div>\n<p class="image-info hide">\n<span class="meta-filesize">22.3 KB</span>\n|\n<strong>W:</strong>\n<span class="meta-width"></span>\n|\n<strong>H:</strong>\n<span class="meta-height"></span>\n</p>\n</div>\n<div class="wrap">\n<div class="added frame js-image-frame" data-note-type="DiffNote" data-position="{&quot;base_sha&quot;:&quot;e63f41fe459e62e1228fcef60d7189127aeba95a&quot;,&quot;start_sha&quot;:&quot;d9eaefe5a676b820c57ff18cf5b68316025f7962&quot;,&quot;head_sha&quot;:&quot;c48ee0d1bf3b30453f5b32250ce03134beaa6d13&quot;,&quot;old_path&quot;:&quot;CHANGELOG&quot;,&quot;new_path&quot;:&quot;CHANGELOG&quot;,&quot;position_type&quot;:&quot;text&quot;,&quot;old_line&quot;:null,&quot;new_line&quot;:2}">\n<img alt="CHANGELOG" draggable="false" src="http://localhost:3000/gitlab-org/gitlab-test/raw/c48ee0d1bf3b30453f5b32250ce03134beaa6d13/CHANGELOG" />\n</div>\n\n<p class="image-info hide">\n<span class="meta-filesize">22.3 KB</span>\n|\n<strong>W:</strong>\n<span class="meta-width"></span>\n|\n<strong>H:</strong>\n<span class="meta-height"></span>\n</p>\n</div>\n</div>\n<div class="swipe view hide">\n<div class="swipe-frame">\n<div class="frame deleted">\n<img alt="CHANGELOG" src="http://localhost:3000/gitlab-org/gitlab-test/raw/e63f41fe459e62e1228fcef60d7189127aeba95a/CHANGELOG" />\n</div>\n<div class="swipe-wrap">\n<div class="added frame js-image-frame" data-note-type="DiffNote" data-position="{&quot;base_sha&quot;:&quot;e63f41fe459e62e1228fcef60d7189127aeba95a&quot;,&quot;start_sha&quot;:&quot;d9eaefe5a676b820c57ff18cf5b68316025f7962&quot;,&quot;head_sha&quot;:&quot;c48ee0d1bf3b30453f5b32250ce03134beaa6d13&quot;,&quot;old_path&quot;:&quot;CHANGELOG&quot;,&quot;new_path&quot;:&quot;CHANGELOG&quot;,&quot;position_type&quot;:&quot;text&quot;,&quot;old_line&quot;:null,&quot;new_line&quot;:2}">\n<img alt="CHANGELOG" draggable="false" src="http://localhost:3000/gitlab-org/gitlab-test/raw/c48ee0d1bf3b30453f5b32250ce03134beaa6d13/CHANGELOG" />\n</div>\n\n</div>\n<span class="swipe-bar">\n<span class="top-handle"></span>\n<span class="bottom-handle"></span>\n</span>\n</div>\n</div>\n<div class="onion-skin view hide">\n<div class="onion-skin-frame">\n<div class="frame deleted">\n<img alt="CHANGELOG" src="http://localhost:3000/gitlab-org/gitlab-test/raw/e63f41fe459e62e1228fcef60d7189127aeba95a/CHANGELOG" />\n</div>\n<div class="added frame js-image-frame" data-note-type="DiffNote" data-position="{&quot;base_sha&quot;:&quot;e63f41fe459e62e1228fcef60d7189127aeba95a&quot;,&quot;start_sha&quot;:&quot;d9eaefe5a676b820c57ff18cf5b68316025f7962&quot;,&quot;head_sha&quot;:&quot;c48ee0d1bf3b30453f5b32250ce03134beaa6d13&quot;,&quot;old_path&quot;:&quot;CHANGELOG&quot;,&quot;new_path&quot;:&quot;CHANGELOG&quot;,&quot;position_type&quot;:&quot;text&quot;,&quot;old_line&quot;:null,&quot;new_line&quot;:2}">\n<img alt="CHANGELOG" draggable="false" src="http://localhost:3000/gitlab-org/gitlab-test/raw/c48ee0d1bf3b30453f5b32250ce03134beaa6d13/CHANGELOG" />\n</div>\n\n<div class="controls">\n<div class="transparent"></div>\n<div class="drag-track">\n<div class="dragger" style="left: 0px;"></div>\n</div>\n<div class="opaque"></div>\n</div>\n</div>\n</div>\n</div>\n<div class="view-modes hide">\n<ul class="view-modes-menu">\n<li class="two-up" data-mode="two-up">2-up</li>\n<li class="swipe" data-mode="swipe">Swipe</li>\n<li class="onion-skin" data-mode="onion-skin">Onion skin</li>\n</ul>\n</div>\n',
};
export const imageDiffDiscussions = [
diff --git a/spec/javascripts/diffs/store/actions_spec.js b/spec/javascripts/diffs/store/actions_spec.js
index 4b339a0553f..033b5e86dbe 100644
--- a/spec/javascripts/diffs/store/actions_spec.js
+++ b/spec/javascripts/diffs/store/actions_spec.js
@@ -26,9 +26,12 @@ import actions, {
toggleTreeOpen,
scrollToFile,
toggleShowTreeList,
+ renderFileForDiscussionId,
} from '~/diffs/store/actions';
+import eventHub from '~/notes/event_hub';
import * as types from '~/diffs/store/mutation_types';
import axios from '~/lib/utils/axios_utils';
+import mockDiffFile from 'spec/diffs/mock_data/diff_file';
import testAction from '../../helpers/vuex_action_helper';
describe('DiffsStoreActions', () => {
@@ -607,11 +610,18 @@ describe('DiffsStoreActions', () => {
});
describe('saveDiffDiscussion', () => {
- beforeEach(() => {
- spyOnDependency(actions, 'getNoteFormData').and.returnValue('testData');
- });
-
it('dispatches actions', done => {
+ const commitId = 'something';
+ const formData = {
+ diffFile: { ...mockDiffFile },
+ noteableData: {},
+ };
+ const note = {};
+ const state = {
+ commit: {
+ id: commitId,
+ },
+ };
const dispatch = jasmine.createSpy('dispatch').and.callFake(name => {
switch (name) {
case 'saveNote':
@@ -625,11 +635,19 @@ describe('DiffsStoreActions', () => {
}
});
- saveDiffDiscussion({ dispatch }, { note: {}, formData: {} })
+ saveDiffDiscussion({ state, dispatch }, { note, formData })
.then(() => {
- expect(dispatch.calls.argsFor(0)).toEqual(['saveNote', 'testData', { root: true }]);
- expect(dispatch.calls.argsFor(1)).toEqual(['updateDiscussion', 'test', { root: true }]);
- expect(dispatch.calls.argsFor(2)).toEqual(['assignDiscussionsToDiff', ['discussion']]);
+ const { calls } = dispatch;
+
+ expect(calls.count()).toBe(5);
+ expect(calls.argsFor(0)).toEqual(['saveNote', jasmine.any(Object), { root: true }]);
+
+ const postData = calls.argsFor(0)[1];
+
+ expect(postData.data.note.commit_id).toBe(commitId);
+
+ expect(calls.argsFor(1)).toEqual(['updateDiscussion', 'test', { root: true }]);
+ expect(calls.argsFor(2)).toEqual(['assignDiscussionsToDiff', ['discussion']]);
})
.then(done)
.catch(done.fail);
@@ -719,4 +737,63 @@ describe('DiffsStoreActions', () => {
expect(localStorage.setItem).toHaveBeenCalledWith('mr_tree_show', true);
});
});
+
+ describe('renderFileForDiscussionId', () => {
+ const rootState = {
+ notes: {
+ discussions: [
+ {
+ id: '123',
+ diff_file: {
+ file_hash: 'HASH',
+ },
+ },
+ {
+ id: '456',
+ diff_file: {
+ file_hash: 'HASH',
+ },
+ },
+ ],
+ },
+ };
+ let commit;
+ let $emit;
+ let scrollToElement;
+ const state = ({ collapsed, renderIt }) => ({
+ diffFiles: [
+ {
+ file_hash: 'HASH',
+ collapsed,
+ renderIt,
+ },
+ ],
+ });
+
+ beforeEach(() => {
+ commit = jasmine.createSpy('commit');
+ scrollToElement = spyOnDependency(actions, 'scrollToElement').and.stub();
+ $emit = spyOn(eventHub, '$emit');
+ });
+
+ it('renders and expands file for the given discussion id', () => {
+ const localState = state({ collapsed: true, renderIt: false });
+
+ renderFileForDiscussionId({ rootState, state: localState, commit }, '123');
+
+ expect(commit).toHaveBeenCalledWith('RENDER_FILE', localState.diffFiles[0]);
+ expect($emit).toHaveBeenCalledTimes(1);
+ expect(scrollToElement).toHaveBeenCalledTimes(1);
+ });
+
+ it('jumps to discussion on already rendered and expanded file', () => {
+ const localState = state({ collapsed: false, renderIt: true });
+
+ renderFileForDiscussionId({ rootState, state: localState, commit }, '123');
+
+ expect(commit).not.toHaveBeenCalled();
+ expect($emit).toHaveBeenCalledTimes(1);
+ expect(scrollToElement).not.toHaveBeenCalled();
+ });
+ });
});
diff --git a/spec/javascripts/diffs/store/mutations_spec.js b/spec/javascripts/diffs/store/mutations_spec.js
index 23e8761bc55..f3449bec6ec 100644
--- a/spec/javascripts/diffs/store/mutations_spec.js
+++ b/spec/javascripts/diffs/store/mutations_spec.js
@@ -277,6 +277,87 @@ describe('DiffsStoreMutations', () => {
expect(state.diffFiles[0].highlighted_diff_lines[0].discussions[0].id).toEqual(1);
});
+ it('updates existing discussion', () => {
+ const diffPosition = {
+ base_sha: 'ed13df29948c41ba367caa757ab3ec4892509910',
+ head_sha: 'b921914f9a834ac47e6fd9420f78db0f83559130',
+ new_line: null,
+ new_path: '500-lines-4.txt',
+ old_line: 5,
+ old_path: '500-lines-4.txt',
+ start_sha: 'ed13df29948c41ba367caa757ab3ec4892509910',
+ };
+
+ const state = {
+ latestDiff: true,
+ diffFiles: [
+ {
+ file_hash: 'ABC',
+ parallel_diff_lines: [
+ {
+ left: {
+ line_code: 'ABC_1',
+ discussions: [],
+ },
+ right: {
+ line_code: 'ABC_1',
+ discussions: [],
+ },
+ },
+ ],
+ highlighted_diff_lines: [
+ {
+ line_code: 'ABC_1',
+ discussions: [],
+ },
+ ],
+ },
+ ],
+ };
+ const discussion = {
+ id: 1,
+ line_code: 'ABC_1',
+ diff_discussion: true,
+ resolvable: true,
+ original_position: diffPosition,
+ position: diffPosition,
+ diff_file: {
+ file_hash: state.diffFiles[0].file_hash,
+ },
+ };
+
+ const diffPositionByLineCode = {
+ ABC_1: diffPosition,
+ };
+
+ mutations[types.SET_LINE_DISCUSSIONS_FOR_FILE](state, {
+ discussion,
+ diffPositionByLineCode,
+ });
+
+ expect(state.diffFiles[0].parallel_diff_lines[0].left.discussions.length).toEqual(1);
+ expect(state.diffFiles[0].parallel_diff_lines[0].left.discussions[0].id).toEqual(1);
+ expect(state.diffFiles[0].parallel_diff_lines[0].right.discussions).toEqual([]);
+
+ expect(state.diffFiles[0].highlighted_diff_lines[0].discussions.length).toEqual(1);
+ expect(state.diffFiles[0].highlighted_diff_lines[0].discussions[0].id).toEqual(1);
+
+ mutations[types.SET_LINE_DISCUSSIONS_FOR_FILE](state, {
+ discussion: {
+ ...discussion,
+ resolved: true,
+ notes: ['test'],
+ },
+ diffPositionByLineCode,
+ });
+
+ expect(state.diffFiles[0].parallel_diff_lines[0].left.discussions[0].notes.length).toBe(1);
+ expect(state.diffFiles[0].highlighted_diff_lines[0].discussions[0].notes.length).toBe(1);
+
+ expect(state.diffFiles[0].parallel_diff_lines[0].left.discussions[0].resolved).toBe(true);
+ expect(state.diffFiles[0].highlighted_diff_lines[0].discussions[0].resolved).toBe(true);
+ });
+
it('should add legacy discussions to the given line', () => {
const diffPosition = {
base_sha: 'ed13df29948c41ba367caa757ab3ec4892509910',
@@ -356,10 +437,12 @@ describe('DiffsStoreMutations', () => {
{
id: 1,
line_code: 'ABC_1',
+ notes: [],
},
{
id: 2,
line_code: 'ABC_1',
+ notes: [],
},
],
},
@@ -376,10 +459,12 @@ describe('DiffsStoreMutations', () => {
{
id: 1,
line_code: 'ABC_1',
+ notes: [],
},
{
id: 2,
line_code: 'ABC_1',
+ notes: [],
},
],
},
diff --git a/spec/javascripts/diffs/store/utils_spec.js b/spec/javascripts/diffs/store/utils_spec.js
index d4ef17c5ef8..f096638e3d6 100644
--- a/spec/javascripts/diffs/store/utils_spec.js
+++ b/spec/javascripts/diffs/store/utils_spec.js
@@ -150,7 +150,7 @@ describe('DiffsStoreUtils', () => {
note: {
noteable_type: options.noteableType,
noteable_id: options.noteableData.id,
- commit_id: '',
+ commit_id: undefined,
type: DIFF_NOTE_TYPE,
line_code: options.noteTargetLine.line_code,
note: options.note,
@@ -209,7 +209,7 @@ describe('DiffsStoreUtils', () => {
note: {
noteable_type: options.noteableType,
noteable_id: options.noteableData.id,
- commit_id: '',
+ commit_id: undefined,
type: LEGACY_DIFF_NOTE_TYPE,
line_code: options.noteTargetLine.line_code,
note: options.note,
@@ -559,4 +559,26 @@ describe('DiffsStoreUtils', () => {
]);
});
});
+
+ describe('getDiffMode', () => {
+ it('returns mode when matched in file', () => {
+ expect(
+ utils.getDiffMode({
+ renamed_file: true,
+ }),
+ ).toBe('renamed');
+ });
+
+ it('returns mode_changed if key has no match', () => {
+ expect(
+ utils.getDiffMode({
+ mode_changed: true,
+ }),
+ ).toBe('mode_changed');
+ });
+
+ it('defaults to replaced', () => {
+ expect(utils.getDiffMode({})).toBe('replaced');
+ });
+ });
});
diff --git a/spec/javascripts/image_diff/helpers/badge_helper_spec.js b/spec/javascripts/image_diff/helpers/badge_helper_spec.js
index 8ea05203d00..b3001d45e3c 100644
--- a/spec/javascripts/image_diff/helpers/badge_helper_spec.js
+++ b/spec/javascripts/image_diff/helpers/badge_helper_spec.js
@@ -61,6 +61,10 @@ describe('badge helper', () => {
expect(buttonEl).toBeDefined();
});
+ it('should add badge classes', () => {
+ expect(buttonEl.className).toContain('badge badge-pill');
+ });
+
it('should set the badge text', () => {
expect(buttonEl.innerText).toEqual(badgeText);
});
diff --git a/spec/javascripts/jobs/components/trigger_block_spec.js b/spec/javascripts/jobs/components/trigger_block_spec.js
index 7254851a9e7..448197b82c0 100644
--- a/spec/javascripts/jobs/components/trigger_block_spec.js
+++ b/spec/javascripts/jobs/components/trigger_block_spec.js
@@ -31,8 +31,8 @@ describe('Trigger block', () => {
});
describe('with variables', () => {
- describe('reveal variables', () => {
- it('reveals variables on click', done => {
+ describe('hide/reveal variables', () => {
+ it('should toggle variables on click', done => {
vm = mountComponent(Component, {
trigger: {
short_token: 'bd7e',
@@ -48,6 +48,10 @@ describe('Trigger block', () => {
vm.$nextTick()
.then(() => {
expect(vm.$el.querySelector('.js-build-variables')).not.toBeNull();
+ expect(vm.$el.querySelector('.js-reveal-variables').textContent.trim()).toEqual(
+ 'Hide values',
+ );
+
expect(vm.$el.querySelector('.js-build-variables').textContent).toContain(
'UPLOAD_TO_GCS',
);
@@ -58,6 +62,26 @@ describe('Trigger block', () => {
);
expect(vm.$el.querySelector('.js-build-variables').textContent).toContain('true');
+
+ vm.$el.querySelector('.js-reveal-variables').click();
+ })
+ .then(vm.$nextTick)
+ .then(() => {
+ expect(vm.$el.querySelector('.js-reveal-variables').textContent.trim()).toEqual(
+ 'Reveal values',
+ );
+
+ expect(vm.$el.querySelector('.js-build-variables').textContent).toContain(
+ 'UPLOAD_TO_GCS',
+ );
+
+ expect(vm.$el.querySelector('.js-build-value').textContent).toContain('••••••');
+
+ expect(vm.$el.querySelector('.js-build-variables').textContent).toContain(
+ 'UPLOAD_TO_S3',
+ );
+
+ expect(vm.$el.querySelector('.js-build-value').textContent).toContain('••••••');
})
.then(done)
.catch(done.fail);
diff --git a/spec/javascripts/lib/utils/dom_utils_spec.js b/spec/javascripts/lib/utils/dom_utils_spec.js
index 1fb2e4584a0..2bcf37f35c7 100644
--- a/spec/javascripts/lib/utils/dom_utils_spec.js
+++ b/spec/javascripts/lib/utils/dom_utils_spec.js
@@ -1,4 +1,6 @@
-import { addClassIfElementExists } from '~/lib/utils/dom_utils';
+import { addClassIfElementExists, canScrollUp, canScrollDown } from '~/lib/utils/dom_utils';
+
+const TEST_MARGIN = 5;
describe('DOM Utils', () => {
describe('addClassIfElementExists', () => {
@@ -34,4 +36,54 @@ describe('DOM Utils', () => {
addClassIfElementExists(childElement, className);
});
});
+
+ describe('canScrollUp', () => {
+ [1, 100].forEach(scrollTop => {
+ it(`is true if scrollTop is > 0 (${scrollTop})`, () => {
+ expect(canScrollUp({ scrollTop })).toBe(true);
+ });
+ });
+
+ [0, -10].forEach(scrollTop => {
+ it(`is false if scrollTop is <= 0 (${scrollTop})`, () => {
+ expect(canScrollUp({ scrollTop })).toBe(false);
+ });
+ });
+
+ it('is true if scrollTop is > margin', () => {
+ expect(canScrollUp({ scrollTop: TEST_MARGIN + 1 }, TEST_MARGIN)).toBe(true);
+ });
+
+ it('is false if scrollTop is <= margin', () => {
+ expect(canScrollUp({ scrollTop: TEST_MARGIN }, TEST_MARGIN)).toBe(false);
+ });
+ });
+
+ describe('canScrollDown', () => {
+ let element;
+
+ beforeEach(() => {
+ element = { scrollTop: 7, offsetHeight: 22, scrollHeight: 30 };
+ });
+
+ it('is true if element can be scrolled down', () => {
+ expect(canScrollDown(element)).toBe(true);
+ });
+
+ it('is false if element cannot be scrolled down', () => {
+ element.scrollHeight -= 1;
+
+ expect(canScrollDown(element)).toBe(false);
+ });
+
+ it('is true if element can be scrolled down, with margin given', () => {
+ element.scrollHeight += TEST_MARGIN;
+
+ expect(canScrollDown(element, TEST_MARGIN)).toBe(true);
+ });
+
+ it('is false if element cannot be scrolled down, with margin given', () => {
+ expect(canScrollDown(element, TEST_MARGIN)).toBe(false);
+ });
+ });
});
diff --git a/spec/javascripts/lib/utils/file_upload_spec.js b/spec/javascripts/lib/utils/file_upload_spec.js
new file mode 100644
index 00000000000..92c9cc70aaf
--- /dev/null
+++ b/spec/javascripts/lib/utils/file_upload_spec.js
@@ -0,0 +1,36 @@
+import fileUpload from '~/lib/utils/file_upload';
+
+describe('File upload', () => {
+ beforeEach(() => {
+ setFixtures(`
+ <form>
+ <button class="js-button" type="button">Click me!</button>
+ <input type="text" class="js-input" />
+ <span class="js-filename"></span>
+ </form>
+ `);
+
+ fileUpload('.js-button', '.js-input');
+ });
+
+ it('clicks file input after clicking button', () => {
+ const btn = document.querySelector('.js-button');
+ const input = document.querySelector('.js-input');
+
+ spyOn(input, 'click');
+
+ btn.click();
+
+ expect(input.click).toHaveBeenCalled();
+ });
+
+ it('updates file name text', () => {
+ const input = document.querySelector('.js-input');
+
+ input.value = 'path/to/file/index.js';
+
+ input.dispatchEvent(new CustomEvent('change'));
+
+ expect(document.querySelector('.js-filename').textContent).toEqual('index.js');
+ });
+});
diff --git a/spec/javascripts/lib/utils/users_cache_spec.js b/spec/javascripts/lib/utils/users_cache_spec.js
index 6adc19bdd51..acb5e024acd 100644
--- a/spec/javascripts/lib/utils/users_cache_spec.js
+++ b/spec/javascripts/lib/utils/users_cache_spec.js
@@ -3,7 +3,9 @@ import UsersCache from '~/lib/utils/users_cache';
describe('UsersCache', () => {
const dummyUsername = 'win';
- const dummyUser = 'has a farm';
+ const dummyUserId = 123;
+ const dummyUser = { name: 'has a farm', username: 'farmer' };
+ const dummyUserStatus = 'my status';
beforeEach(() => {
UsersCache.internalStorage = {};
@@ -135,4 +137,110 @@ describe('UsersCache', () => {
.catch(done.fail);
});
});
+
+ describe('retrieveById', () => {
+ let apiSpy;
+
+ beforeEach(() => {
+ spyOn(Api, 'user').and.callFake(id => apiSpy(id));
+ });
+
+ it('stores and returns data from API call if cache is empty', done => {
+ apiSpy = id => {
+ expect(id).toBe(dummyUserId);
+ return Promise.resolve({
+ data: dummyUser,
+ });
+ };
+
+ UsersCache.retrieveById(dummyUserId)
+ .then(user => {
+ expect(user).toBe(dummyUser);
+ expect(UsersCache.internalStorage[dummyUserId]).toBe(dummyUser);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('returns undefined if Ajax call fails and cache is empty', done => {
+ const dummyError = new Error('server exploded');
+ apiSpy = id => {
+ expect(id).toBe(dummyUserId);
+ return Promise.reject(dummyError);
+ };
+
+ UsersCache.retrieveById(dummyUserId)
+ .then(user => fail(`Received unexpected user: ${JSON.stringify(user)}`))
+ .catch(error => {
+ expect(error).toBe(dummyError);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('makes no Ajax call if matching data exists', done => {
+ UsersCache.internalStorage[dummyUserId] = dummyUser;
+ apiSpy = () => fail(new Error('expected no Ajax call!'));
+
+ UsersCache.retrieveById(dummyUserId)
+ .then(user => {
+ expect(user).toBe(dummyUser);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
+ describe('retrieveStatusById', () => {
+ let apiSpy;
+
+ beforeEach(() => {
+ spyOn(Api, 'userStatus').and.callFake(id => apiSpy(id));
+ });
+
+ it('stores and returns data from API call if cache is empty', done => {
+ apiSpy = id => {
+ expect(id).toBe(dummyUserId);
+ return Promise.resolve({
+ data: dummyUserStatus,
+ });
+ };
+
+ UsersCache.retrieveStatusById(dummyUserId)
+ .then(userStatus => {
+ expect(userStatus).toBe(dummyUserStatus);
+ expect(UsersCache.internalStorage[dummyUserId].status).toBe(dummyUserStatus);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('returns undefined if Ajax call fails and cache is empty', done => {
+ const dummyError = new Error('server exploded');
+ apiSpy = id => {
+ expect(id).toBe(dummyUserId);
+ return Promise.reject(dummyError);
+ };
+
+ UsersCache.retrieveStatusById(dummyUserId)
+ .then(userStatus => fail(`Received unexpected user: ${JSON.stringify(userStatus)}`))
+ .catch(error => {
+ expect(error).toBe(dummyError);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('makes no Ajax call if matching data exists', done => {
+ UsersCache.internalStorage[dummyUserId] = { status: dummyUserStatus };
+ apiSpy = () => fail(new Error('expected no Ajax call!'));
+
+ UsersCache.retrieveStatusById(dummyUserId)
+ .then(userStatus => {
+ expect(userStatus).toBe(dummyUserStatus);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
});
diff --git a/spec/javascripts/notes/components/note_app_spec.js b/spec/javascripts/notes/components/note_app_spec.js
index 0081f42c330..22bee049f9c 100644
--- a/spec/javascripts/notes/components/note_app_spec.js
+++ b/spec/javascripts/notes/components/note_app_spec.js
@@ -30,6 +30,8 @@ describe('note_app', () => {
jasmine.addMatchers(vueMatchers);
$('body').attr('data-page', 'projects:merge_requests:show');
+ setFixtures('<div class="js-vue-notes-event"><div id="app"></div></div>');
+
const IssueNotesApp = Vue.extend(notesApp);
store = createStore();
@@ -43,6 +45,7 @@ describe('note_app', () => {
return mountComponentWithStore(IssueNotesApp, {
props,
store,
+ el: document.getElementById('app'),
});
};
});
@@ -283,4 +286,24 @@ describe('note_app', () => {
}, 0);
});
});
+
+ describe('emoji awards', () => {
+ it('dispatches toggleAward after toggleAward event', () => {
+ const toggleAwardEvent = new CustomEvent('toggleAward', {
+ detail: {
+ awardName: 'test',
+ noteId: 1,
+ },
+ });
+
+ spyOn(vm.$store, 'dispatch');
+
+ vm.$el.parentElement.dispatchEvent(toggleAwardEvent);
+
+ expect(vm.$store.dispatch).toHaveBeenCalledWith('toggleAward', {
+ awardName: 'test',
+ noteId: 1,
+ });
+ });
+ });
});
diff --git a/spec/javascripts/notes/components/note_edited_text_spec.js b/spec/javascripts/notes/components/note_edited_text_spec.js
index e0b991c32ec..e4c8d954d50 100644
--- a/spec/javascripts/notes/components/note_edited_text_spec.js
+++ b/spec/javascripts/notes/components/note_edited_text_spec.js
@@ -39,7 +39,7 @@ describe('note_edited_text', () => {
});
it('should render provided user information', () => {
- const authorLink = vm.$el.querySelector('.js-vue-author');
+ const authorLink = vm.$el.querySelector('.js-user-link');
expect(authorLink.getAttribute('href')).toEqual(props.editedBy.path);
expect(authorLink.textContent.trim()).toEqual(props.editedBy.name);
diff --git a/spec/javascripts/notes/components/note_header_spec.js b/spec/javascripts/notes/components/note_header_spec.js
index 379780f43a0..6d1a7ef370f 100644
--- a/spec/javascripts/notes/components/note_header_spec.js
+++ b/spec/javascripts/notes/components/note_header_spec.js
@@ -42,6 +42,9 @@ describe('note_header component', () => {
it('should render user information', () => {
expect(vm.$el.querySelector('.note-header-author-name').textContent.trim()).toEqual('Root');
expect(vm.$el.querySelector('.note-header-info a').getAttribute('href')).toEqual('/root');
+ expect(vm.$el.querySelector('.note-header-info a').dataset.userId).toEqual('1');
+ expect(vm.$el.querySelector('.note-header-info a').dataset.username).toEqual('root');
+ expect(vm.$el.querySelector('.note-header-info a').classList).toContain('js-user-link');
});
it('should render timestamp link', () => {
diff --git a/spec/javascripts/notes/components/noteable_discussion_spec.js b/spec/javascripts/notes/components/noteable_discussion_spec.js
index ab9c52346d6..106a4ac2546 100644
--- a/spec/javascripts/notes/components/noteable_discussion_spec.js
+++ b/spec/javascripts/notes/components/noteable_discussion_spec.js
@@ -42,12 +42,14 @@ describe('noteable_discussion component', () => {
const discussion = { ...discussionMock };
discussion.diff_file = mockDiffFile;
discussion.diff_discussion = true;
- const diffDiscussionVm = new Component({
+
+ vm.$destroy();
+ vm = new Component({
store,
propsData: { discussion },
}).$mount();
- expect(diffDiscussionVm.$el.querySelector('.discussion-header')).not.toBeNull();
+ expect(vm.$el.querySelector('.discussion-header')).not.toBeNull();
});
describe('actions', () => {
@@ -83,6 +85,7 @@ describe('noteable_discussion component', () => {
it('expands next unresolved discussion', done => {
const discussion2 = getJSONFixture(discussionWithTwoUnresolvedNotes)[0];
discussion2.resolved = false;
+ discussion2.active = true;
discussion2.id = 'next'; // prepare this for being identified as next one (to be jumped to)
vm.$store.dispatch('setInitialNotes', [discussionMock, discussion2]);
window.mrTabs.currentAction = 'show';
@@ -129,4 +132,44 @@ describe('noteable_discussion component', () => {
expect(note).toEqual(data);
});
});
+
+ describe('commit discussion', () => {
+ const commitId = 'razupaltuff';
+
+ beforeEach(() => {
+ vm.$destroy();
+
+ store.state.diffs = {
+ projectPath: 'something',
+ };
+
+ vm.$destroy();
+ vm = new Component({
+ propsData: {
+ discussion: {
+ ...discussionMock,
+ for_commit: true,
+ commit_id: commitId,
+ diff_discussion: true,
+ diff_file: {
+ ...mockDiffFile,
+ },
+ },
+ renderDiffFile: true,
+ },
+ store,
+ }).$mount();
+ });
+
+ it('displays a monospace started a discussion on commit', () => {
+ const truncatedCommitId = commitId.substr(0, 8);
+
+ expect(vm.$el).toContainText(`started a discussion on commit ${truncatedCommitId}`);
+
+ const commitElement = vm.$el.querySelector('.commit-sha');
+
+ expect(commitElement).not.toBe(null);
+ expect(commitElement).toHaveText(truncatedCommitId);
+ });
+ });
});
diff --git a/spec/javascripts/notes/mock_data.js b/spec/javascripts/notes/mock_data.js
index ad0e793b915..7ae45c40c28 100644
--- a/spec/javascripts/notes/mock_data.js
+++ b/spec/javascripts/notes/mock_data.js
@@ -305,6 +305,7 @@ export const discussionMock = {
],
individual_note: false,
resolvable: true,
+ active: true,
};
export const loggedOutnoteableData = {
@@ -1173,6 +1174,7 @@ export const discussion1 = {
id: 'abc1',
resolvable: true,
resolved: false,
+ active: true,
diff_file: {
file_path: 'about.md',
},
@@ -1209,6 +1211,7 @@ export const discussion2 = {
id: 'abc2',
resolvable: true,
resolved: false,
+ active: true,
diff_file: {
file_path: 'README.md',
},
@@ -1226,6 +1229,7 @@ export const discussion2 = {
export const discussion3 = {
id: 'abc3',
resolvable: true,
+ active: true,
resolved: false,
diff_file: {
file_path: 'README.md',
diff --git a/spec/javascripts/notes/stores/actions_spec.js b/spec/javascripts/notes/stores/actions_spec.js
index 24c2b3e6570..2e3cd5e8f36 100644
--- a/spec/javascripts/notes/stores/actions_spec.js
+++ b/spec/javascripts/notes/stores/actions_spec.js
@@ -124,7 +124,7 @@ describe('Actions Notes Store', () => {
{ discussionId: discussionMock.id },
{ notes: [discussionMock] },
[{ type: 'EXPAND_DISCUSSION', payload: { discussionId: discussionMock.id } }],
- [],
+ [{ type: 'diffs/renderFileForDiscussionId', payload: discussionMock.id }],
done,
);
});
diff --git a/spec/javascripts/notes/stores/mutation_spec.js b/spec/javascripts/notes/stores/mutation_spec.js
index 52cdc16353a..3fbae82f16c 100644
--- a/spec/javascripts/notes/stores/mutation_spec.js
+++ b/spec/javascripts/notes/stores/mutation_spec.js
@@ -9,6 +9,11 @@ import {
individualNote,
} from '../mock_data';
+const RESOLVED_NOTE = { resolvable: true, resolved: true };
+const UNRESOLVED_NOTE = { resolvable: true, resolved: false };
+const SYSTEM_NOTE = { resolvable: false, resolved: false };
+const WEIRD_NOTE = { resolvable: false, resolved: true };
+
describe('Notes Store mutations', () => {
describe('ADD_NEW_NOTE', () => {
let state;
@@ -449,49 +454,61 @@ describe('Notes Store mutations', () => {
});
describe('UPDATE_RESOLVABLE_DISCUSSIONS_COUNTS', () => {
- it('updates resolvableDiscussionsCount', () => {
- const state = {
- discussions: [
- { individual_note: false, resolvable: true, notes: [] },
- { individual_note: true, resolvable: true, notes: [] },
- { individual_note: false, resolvable: false, notes: [] },
- ],
- resolvableDiscussionsCount: 0,
- };
-
- mutations.UPDATE_RESOLVABLE_DISCUSSIONS_COUNTS(state);
-
- expect(state.resolvableDiscussionsCount).toBe(1);
- });
-
- it('updates unresolvedDiscussionsCount', () => {
+ it('with unresolvable discussions, updates state', () => {
const state = {
discussions: [
- { individual_note: false, resolvable: true, notes: [{ resolved: false }] },
- { individual_note: true, resolvable: true, notes: [{ resolved: false }] },
- { individual_note: false, resolvable: false, notes: [{ resolved: false }] },
+ { individual_note: false, resolvable: true, notes: [UNRESOLVED_NOTE] },
+ { individual_note: true, resolvable: true, notes: [UNRESOLVED_NOTE] },
+ { individual_note: false, resolvable: false, notes: [UNRESOLVED_NOTE] },
],
- unresolvedDiscussionsCount: 0,
};
mutations.UPDATE_RESOLVABLE_DISCUSSIONS_COUNTS(state);
- expect(state.unresolvedDiscussionsCount).toBe(1);
+ expect(state).toEqual(
+ jasmine.objectContaining({
+ resolvableDiscussionsCount: 1,
+ unresolvedDiscussionsCount: 1,
+ hasUnresolvedDiscussions: false,
+ }),
+ );
});
- it('updates hasUnresolvedDiscussions', () => {
+ it('with resolvable discussions, updates state', () => {
const state = {
discussions: [
- { individual_note: false, resolvable: true, notes: [{ resolved: false }] },
- { individual_note: false, resolvable: true, notes: [{ resolved: false }] },
- { individual_note: false, resolvable: false, notes: [{ resolved: false }] },
+ {
+ individual_note: false,
+ resolvable: true,
+ notes: [RESOLVED_NOTE, SYSTEM_NOTE, RESOLVED_NOTE],
+ },
+ {
+ individual_note: false,
+ resolvable: true,
+ notes: [RESOLVED_NOTE, SYSTEM_NOTE, WEIRD_NOTE],
+ },
+ {
+ individual_note: false,
+ resolvable: true,
+ notes: [SYSTEM_NOTE, RESOLVED_NOTE, WEIRD_NOTE, UNRESOLVED_NOTE],
+ },
+ {
+ individual_note: false,
+ resolvable: true,
+ notes: [UNRESOLVED_NOTE],
+ },
],
- hasUnresolvedDiscussions: 0,
};
mutations.UPDATE_RESOLVABLE_DISCUSSIONS_COUNTS(state);
- expect(state.hasUnresolvedDiscussions).toBe(true);
+ expect(state).toEqual(
+ jasmine.objectContaining({
+ resolvableDiscussionsCount: 4,
+ unresolvedDiscussionsCount: 2,
+ hasUnresolvedDiscussions: true,
+ }),
+ );
});
});
});
diff --git a/spec/javascripts/registry/components/app_spec.js b/spec/javascripts/registry/components/app_spec.js
index 92ff960277a..67118ac03a5 100644
--- a/spec/javascripts/registry/components/app_spec.js
+++ b/spec/javascripts/registry/components/app_spec.js
@@ -1,37 +1,30 @@
-import _ from 'underscore';
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
import Vue from 'vue';
import registry from '~/registry/components/app.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import { TEST_HOST } from 'spec/test_constants';
import { reposServerResponse } from '../mock_data';
describe('Registry List', () => {
+ const Component = Vue.extend(registry);
let vm;
- let Component;
+ let mock;
beforeEach(() => {
- Component = Vue.extend(registry);
+ mock = new MockAdapter(axios);
});
afterEach(() => {
+ mock.restore();
vm.$destroy();
});
describe('with data', () => {
- const interceptor = (request, next) => {
- next(
- request.respondWith(JSON.stringify(reposServerResponse), {
- status: 200,
- }),
- );
- };
-
beforeEach(() => {
- Vue.http.interceptors.push(interceptor);
- vm = mountComponent(Component, { endpoint: 'foo' });
- });
+ mock.onGet(`${TEST_HOST}/foo`).replyOnce(200, reposServerResponse);
- afterEach(() => {
- Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor);
+ vm = mountComponent(Component, { endpoint: `${TEST_HOST}/foo` });
});
it('should render a list of repos', done => {
@@ -64,9 +57,9 @@ describe('Registry List', () => {
Vue.nextTick(() => {
vm.$el.querySelector('.js-toggle-repo').click();
Vue.nextTick(() => {
- expect(vm.$el.querySelector('.js-toggle-repo i').className).toEqual(
- 'fa fa-chevron-up',
- );
+ expect(
+ vm.$el.querySelector('.js-toggle-repo use').getAttribute('xlink:href'),
+ ).toContain('angle-up');
done();
});
});
@@ -76,21 +69,10 @@ describe('Registry List', () => {
});
describe('without data', () => {
- const interceptor = (request, next) => {
- next(
- request.respondWith(JSON.stringify([]), {
- status: 200,
- }),
- );
- };
-
beforeEach(() => {
- Vue.http.interceptors.push(interceptor);
- vm = mountComponent(Component, { endpoint: 'foo' });
- });
+ mock.onGet(`${TEST_HOST}/foo`).replyOnce(200, []);
- afterEach(() => {
- Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor);
+ vm = mountComponent(Component, { endpoint: `${TEST_HOST}/foo` });
});
it('should render empty message', done => {
@@ -109,21 +91,10 @@ describe('Registry List', () => {
});
describe('while loading data', () => {
- const interceptor = (request, next) => {
- next(
- request.respondWith(JSON.stringify(reposServerResponse), {
- status: 200,
- }),
- );
- };
-
beforeEach(() => {
- Vue.http.interceptors.push(interceptor);
- vm = mountComponent(Component, { endpoint: 'foo' });
- });
+ mock.onGet(`${TEST_HOST}/foo`).replyOnce(200, []);
- afterEach(() => {
- Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor);
+ vm = mountComponent(Component, { endpoint: `${TEST_HOST}/foo` });
});
it('should render a loading spinner', done => {
diff --git a/spec/javascripts/registry/components/collapsible_container_spec.js b/spec/javascripts/registry/components/collapsible_container_spec.js
index 256a242f784..a3f7ff76dc7 100644
--- a/spec/javascripts/registry/components/collapsible_container_spec.js
+++ b/spec/javascripts/registry/components/collapsible_container_spec.js
@@ -1,14 +1,24 @@
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
import Vue from 'vue';
import collapsibleComponent from '~/registry/components/collapsible_container.vue';
import store from '~/registry/stores';
-import { repoPropsData } from '../mock_data';
+import * as types from '~/registry/stores/mutation_types';
+
+import { repoPropsData, registryServerResponse, reposServerResponse } from '../mock_data';
describe('collapsible registry container', () => {
let vm;
- let Component;
+ let mock;
+ const Component = Vue.extend(collapsibleComponent);
beforeEach(() => {
- Component = Vue.extend(collapsibleComponent);
+ mock = new MockAdapter(axios);
+
+ mock.onGet(repoPropsData.tagsPath).replyOnce(200, registryServerResponse, {});
+
+ store.commit(types.SET_REPOS_LIST, reposServerResponse);
+
vm = new Component({
store,
propsData: {
@@ -18,24 +28,23 @@ describe('collapsible registry container', () => {
});
afterEach(() => {
+ mock.restore();
vm.$destroy();
});
describe('toggle', () => {
it('should be closed by default', () => {
expect(vm.$el.querySelector('.container-image-tags')).toBe(null);
- expect(vm.$el.querySelector('.container-image-head i').className).toEqual(
- 'fa fa-chevron-right',
- );
+ expect(vm.iconName).toEqual('angle-right');
});
it('should be open when user clicks on closed repo', done => {
vm.$el.querySelector('.js-toggle-repo').click();
+
Vue.nextTick(() => {
- expect(vm.$el.querySelector('.container-image-tags')).toBeDefined();
- expect(vm.$el.querySelector('.container-image-head i').className).toEqual(
- 'fa fa-chevron-up',
- );
+ expect(vm.$el.querySelector('.container-image-tags')).not.toBeNull();
+ expect(vm.iconName).toEqual('angle-up');
+
done();
});
});
@@ -45,12 +54,12 @@ describe('collapsible registry container', () => {
Vue.nextTick(() => {
vm.$el.querySelector('.js-toggle-repo').click();
- Vue.nextTick(() => {
- expect(vm.$el.querySelector('.container-image-tags')).toBe(null);
- expect(vm.$el.querySelector('.container-image-head i').className).toEqual(
- 'fa fa-chevron-right',
- );
- done();
+ setTimeout(() => {
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelector('.container-image-tags')).toBe(null);
+ expect(vm.iconName).toEqual('angle-right');
+ done();
+ });
});
});
});
@@ -58,7 +67,7 @@ describe('collapsible registry container', () => {
describe('delete repo', () => {
it('should be possible to delete a repo', () => {
- expect(vm.$el.querySelector('.js-remove-repo')).toBeDefined();
+ expect(vm.$el.querySelector('.js-remove-repo')).not.toBeNull();
});
});
});
diff --git a/spec/javascripts/registry/stores/actions_spec.js b/spec/javascripts/registry/stores/actions_spec.js
index bc4c444655a..c9aa82dba90 100644
--- a/spec/javascripts/registry/stores/actions_spec.js
+++ b/spec/javascripts/registry/stores/actions_spec.js
@@ -1,42 +1,34 @@
-import Vue from 'vue';
-import VueResource from 'vue-resource';
-import _ from 'underscore';
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
import * as actions from '~/registry/stores/actions';
import * as types from '~/registry/stores/mutation_types';
+import state from '~/registry/stores/state';
+import { TEST_HOST } from 'spec/test_constants';
import testAction from '../../helpers/vuex_action_helper';
import {
- defaultState,
reposServerResponse,
registryServerResponse,
parsedReposServerResponse,
} from '../mock_data';
-Vue.use(VueResource);
-
describe('Actions Registry Store', () => {
- let interceptor;
let mockedState;
+ let mock;
beforeEach(() => {
- mockedState = defaultState;
+ mockedState = state();
+ mockedState.endpoint = `${TEST_HOST}/endpoint.json`;
+ mock = new MockAdapter(axios);
});
- describe('server requests', () => {
- afterEach(() => {
- Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor);
- });
+ afterEach(() => {
+ mock.restore();
+ });
+ describe('server requests', () => {
describe('fetchRepos', () => {
beforeEach(() => {
- interceptor = (request, next) => {
- next(
- request.respondWith(JSON.stringify(reposServerResponse), {
- status: 200,
- }),
- );
- };
-
- Vue.http.interceptors.push(interceptor);
+ mock.onGet(`${TEST_HOST}/endpoint.json`).replyOnce(200, reposServerResponse, {});
});
it('should set receveived repos', done => {
@@ -56,23 +48,15 @@ describe('Actions Registry Store', () => {
});
describe('fetchList', () => {
+ let repo;
beforeEach(() => {
- interceptor = (request, next) => {
- next(
- request.respondWith(JSON.stringify(registryServerResponse), {
- status: 200,
- }),
- );
- };
+ mockedState.repos = parsedReposServerResponse;
+ [, repo] = mockedState.repos;
- Vue.http.interceptors.push(interceptor);
+ mock.onGet(repo.tagsPath).replyOnce(200, registryServerResponse, {});
});
it('should set received list', done => {
- mockedState.repos = parsedReposServerResponse;
-
- const repo = mockedState.repos[1];
-
testAction(
actions.fetchList,
{ repo },
diff --git a/spec/javascripts/user_popovers_spec.js b/spec/javascripts/user_popovers_spec.js
new file mode 100644
index 00000000000..6cf8dd81b36
--- /dev/null
+++ b/spec/javascripts/user_popovers_spec.js
@@ -0,0 +1,66 @@
+import initUserPopovers from '~/user_popovers';
+import UsersCache from '~/lib/utils/users_cache';
+
+describe('User Popovers', () => {
+ const selector = '.js-user-link';
+
+ const dummyUser = { name: 'root' };
+ const dummyUserStatus = { message: 'active' };
+
+ const triggerEvent = (eventName, el) => {
+ const event = document.createEvent('MouseEvents');
+ event.initMouseEvent(eventName, true, true, window);
+
+ el.dispatchEvent(event);
+ };
+
+ beforeEach(() => {
+ setFixtures(`
+ <a href="/root" data-user-id="1" class="js-user-link" data-username="root" data-original-title="" title="">
+ Root
+ </a>
+ `);
+
+ const usersCacheSpy = () => Promise.resolve(dummyUser);
+ spyOn(UsersCache, 'retrieveById').and.callFake(userId => usersCacheSpy(userId));
+
+ const userStatusCacheSpy = () => Promise.resolve(dummyUserStatus);
+ spyOn(UsersCache, 'retrieveStatusById').and.callFake(userId => userStatusCacheSpy(userId));
+
+ initUserPopovers(document.querySelectorAll('.js-user-link'));
+ });
+
+ it('Should Show+Hide Popover on mouseenter and mouseleave', done => {
+ triggerEvent('mouseenter', document.querySelector(selector));
+
+ setTimeout(() => {
+ const shownPopover = document.querySelector('.popover');
+
+ expect(shownPopover).not.toBeNull();
+
+ expect(shownPopover.innerHTML).toContain(dummyUser.name);
+ expect(UsersCache.retrieveById).toHaveBeenCalledWith('1');
+
+ triggerEvent('mouseleave', document.querySelector(selector));
+
+ setTimeout(() => {
+ // After Mouse leave it should be hidden now
+ expect(document.querySelector('.popover')).toBeNull();
+ done();
+ });
+ }, 210); // We need to wait until the 200ms mouseover delay is over, only then the popover will be visible
+ });
+
+ it('Should Not show a popover on short mouse over', done => {
+ triggerEvent('mouseenter', document.querySelector(selector));
+
+ setTimeout(() => {
+ expect(document.querySelector('.popover')).toBeNull();
+ expect(UsersCache.retrieveById).not.toHaveBeenCalledWith('1');
+
+ triggerEvent('mouseleave', document.querySelector(selector));
+
+ done();
+ });
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/diff_viewer/diff_viewer_spec.js b/spec/javascripts/vue_shared/components/diff_viewer/diff_viewer_spec.js
index 67a3a2e08bc..6add6cdac4d 100644
--- a/spec/javascripts/vue_shared/components/diff_viewer/diff_viewer_spec.js
+++ b/spec/javascripts/vue_shared/components/diff_viewer/diff_viewer_spec.js
@@ -68,4 +68,30 @@ describe('DiffViewer', () => {
done();
});
});
+
+ it('renders renamed component', () => {
+ createComponent({
+ diffMode: 'renamed',
+ newPath: 'test.abc',
+ newSha: 'ABC',
+ oldPath: 'testold.abc',
+ oldSha: 'DEF',
+ });
+
+ expect(vm.$el.textContent).toContain('File moved');
+ });
+
+ it('renders mode changed component', () => {
+ createComponent({
+ diffMode: 'mode_changed',
+ newPath: 'test.abc',
+ newSha: 'ABC',
+ oldPath: 'testold.abc',
+ oldSha: 'DEF',
+ aMode: '123',
+ bMode: '321',
+ });
+
+ expect(vm.$el.textContent).toContain('File mode changed from 123 to 321');
+ });
});
diff --git a/spec/javascripts/vue_shared/components/diff_viewer/viewers/mode_changed_spec.js b/spec/javascripts/vue_shared/components/diff_viewer/viewers/mode_changed_spec.js
new file mode 100644
index 00000000000..c4358f0d9cb
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/diff_viewer/viewers/mode_changed_spec.js
@@ -0,0 +1,23 @@
+import { shallowMount } from '@vue/test-utils';
+import ModeChanged from '~/vue_shared/components/diff_viewer/viewers/mode_changed.vue';
+
+describe('Diff viewer mode changed component', () => {
+ let vm;
+
+ beforeEach(() => {
+ vm = shallowMount(ModeChanged, {
+ propsData: {
+ aMode: '123',
+ bMode: '321',
+ },
+ });
+ });
+
+ afterEach(() => {
+ vm.destroy();
+ });
+
+ it('renders aMode & bMode', () => {
+ expect(vm.text()).toContain('File mode changed from 123 to 321');
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/issue/issue_assignees_spec.js b/spec/javascripts/vue_shared/components/issue/issue_assignees_spec.js
new file mode 100644
index 00000000000..9eac75fac96
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/issue/issue_assignees_spec.js
@@ -0,0 +1,114 @@
+import Vue from 'vue';
+
+import IssueAssignees from '~/vue_shared/components/issue/issue_assignees.vue';
+
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import { mockAssigneesList } from 'spec/boards/mock_data';
+
+const createComponent = (assignees = mockAssigneesList, cssClass = '') => {
+ const Component = Vue.extend(IssueAssignees);
+
+ return mountComponent(Component, {
+ assignees,
+ cssClass,
+ });
+};
+
+describe('IssueAssigneesComponent', () => {
+ let vm;
+
+ beforeEach(() => {
+ vm = createComponent();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('data', () => {
+ it('returns default data props', () => {
+ expect(vm.maxVisibleAssignees).toBe(2);
+ expect(vm.maxAssigneeAvatars).toBe(3);
+ expect(vm.maxAssignees).toBe(99);
+ });
+ });
+
+ describe('computed', () => {
+ describe('countOverLimit', () => {
+ it('should return difference between assignees count and maxVisibleAssignees', () => {
+ expect(vm.countOverLimit).toBe(mockAssigneesList.length - vm.maxVisibleAssignees);
+ });
+ });
+
+ describe('assigneesToShow', () => {
+ it('should return assignees containing only 2 items when count more than maxAssigneeAvatars', () => {
+ expect(vm.assigneesToShow.length).toBe(2);
+ });
+
+ it('should return all assignees as it is when count less than maxAssigneeAvatars', () => {
+ vm.assignees = mockAssigneesList.slice(0, 3); // Set 3 Assignees
+
+ expect(vm.assigneesToShow.length).toBe(3);
+ });
+ });
+
+ describe('assigneesCounterTooltip', () => {
+ it('should return string containing count of remaining assignees when count more than maxAssigneeAvatars', () => {
+ expect(vm.assigneesCounterTooltip).toBe('3 more assignees');
+ });
+ });
+
+ describe('shouldRenderAssigneesCounter', () => {
+ it('should return `false` when assignees count less than maxAssigneeAvatars', () => {
+ vm.assignees = mockAssigneesList.slice(0, 3); // Set 3 Assignees
+
+ expect(vm.shouldRenderAssigneesCounter).toBe(false);
+ });
+
+ it('should return `true` when assignees count more than maxAssigneeAvatars', () => {
+ expect(vm.shouldRenderAssigneesCounter).toBe(true);
+ });
+ });
+
+ describe('assigneeCounterLabel', () => {
+ it('should return count of additional assignees total assignees count more than maxAssigneeAvatars', () => {
+ expect(vm.assigneeCounterLabel).toBe('+3');
+ });
+ });
+ });
+
+ describe('methods', () => {
+ describe('avatarUrlTitle', () => {
+ it('returns string containing alt text for assignee avatar', () => {
+ expect(vm.avatarUrlTitle(mockAssigneesList[0])).toBe('Avatar for Terrell Graham');
+ });
+ });
+ });
+
+ describe('template', () => {
+ it('renders component root element with class `issue-assignees`', () => {
+ expect(vm.$el.classList.contains('issue-assignees')).toBe(true);
+ });
+
+ it('renders assignee avatars', () => {
+ expect(vm.$el.querySelectorAll('.user-avatar-link').length).toBe(2);
+ });
+
+ it('renders assignee tooltips', () => {
+ const tooltipText = vm.$el
+ .querySelectorAll('.user-avatar-link')[0]
+ .querySelector('.js-assignee-tooltip').innerText;
+
+ expect(tooltipText).toContain('Assignee');
+ expect(tooltipText).toContain('Terrell Graham');
+ expect(tooltipText).toContain('@monserrate.gleichner');
+ });
+
+ it('renders additional assignees count', () => {
+ const avatarCounterEl = vm.$el.querySelector('.avatar-counter');
+
+ expect(avatarCounterEl.innerText.trim()).toBe('+3');
+ expect(avatarCounterEl.getAttribute('data-original-title')).toBe('3 more assignees');
+ });
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/issue/issue_milestone_spec.js b/spec/javascripts/vue_shared/components/issue/issue_milestone_spec.js
new file mode 100644
index 00000000000..8fca2637326
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/issue/issue_milestone_spec.js
@@ -0,0 +1,234 @@
+import Vue from 'vue';
+
+import IssueMilestone from '~/vue_shared/components/issue/issue_milestone.vue';
+
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import { mockMilestone } from 'spec/boards/mock_data';
+
+const createComponent = (milestone = mockMilestone) => {
+ const Component = Vue.extend(IssueMilestone);
+
+ return mountComponent(Component, {
+ milestone,
+ });
+};
+
+describe('IssueMilestoneComponent', () => {
+ let vm;
+
+ beforeEach(() => {
+ vm = createComponent();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('computed', () => {
+ describe('isMilestoneStarted', () => {
+ it('should return `false` when milestoneStart prop is not defined', done => {
+ const vmStartUndefined = createComponent(
+ Object.assign({}, mockMilestone, {
+ start_date: '',
+ }),
+ );
+
+ Vue.nextTick()
+ .then(() => {
+ expect(vmStartUndefined.isMilestoneStarted).toBe(false);
+ })
+ .then(done)
+ .catch(done.fail);
+
+ vmStartUndefined.$destroy();
+ });
+
+ it('should return `true` when milestone start date is past current date', done => {
+ const vmStarted = createComponent(
+ Object.assign({}, mockMilestone, {
+ start_date: '1990-07-22',
+ }),
+ );
+
+ Vue.nextTick()
+ .then(() => {
+ expect(vmStarted.isMilestoneStarted).toBe(true);
+ })
+ .then(done)
+ .catch(done.fail);
+
+ vmStarted.$destroy();
+ });
+ });
+
+ describe('isMilestonePastDue', () => {
+ it('should return `false` when milestoneDue prop is not defined', done => {
+ const vmDueUndefined = createComponent(
+ Object.assign({}, mockMilestone, {
+ due_date: '',
+ }),
+ );
+
+ Vue.nextTick()
+ .then(() => {
+ expect(vmDueUndefined.isMilestonePastDue).toBe(false);
+ })
+ .then(done)
+ .catch(done.fail);
+
+ vmDueUndefined.$destroy();
+ });
+
+ it('should return `true` when milestone due is past current date', done => {
+ const vmPastDue = createComponent(
+ Object.assign({}, mockMilestone, {
+ due_date: '1990-07-22',
+ }),
+ );
+
+ Vue.nextTick()
+ .then(() => {
+ expect(vmPastDue.isMilestonePastDue).toBe(true);
+ })
+ .then(done)
+ .catch(done.fail);
+
+ vmPastDue.$destroy();
+ });
+ });
+
+ describe('milestoneDatesAbsolute', () => {
+ it('returns string containing absolute milestone due date', () => {
+ expect(vm.milestoneDatesAbsolute).toBe('(December 31, 2019)');
+ });
+
+ it('returns string containing absolute milestone start date when due date is not present', done => {
+ const vmDueUndefined = createComponent(
+ Object.assign({}, mockMilestone, {
+ due_date: '',
+ }),
+ );
+
+ Vue.nextTick()
+ .then(() => {
+ expect(vmDueUndefined.milestoneDatesAbsolute).toBe('(January 1, 2018)');
+ })
+ .then(done)
+ .catch(done.fail);
+
+ vmDueUndefined.$destroy();
+ });
+
+ it('returns empty string when both milestone start and due dates are not present', done => {
+ const vmDatesUndefined = createComponent(
+ Object.assign({}, mockMilestone, {
+ start_date: '',
+ due_date: '',
+ }),
+ );
+
+ Vue.nextTick()
+ .then(() => {
+ expect(vmDatesUndefined.milestoneDatesAbsolute).toBe('');
+ })
+ .then(done)
+ .catch(done.fail);
+
+ vmDatesUndefined.$destroy();
+ });
+ });
+
+ describe('milestoneDatesHuman', () => {
+ it('returns string containing milestone due date when date is yet to be due', done => {
+ const vmFuture = createComponent(
+ Object.assign({}, mockMilestone, {
+ due_date: `${new Date().getFullYear() + 10}-01-01`,
+ }),
+ );
+
+ Vue.nextTick()
+ .then(() => {
+ expect(vmFuture.milestoneDatesHuman).toContain('years remaining');
+ })
+ .then(done)
+ .catch(done.fail);
+
+ vmFuture.$destroy();
+ });
+
+ it('returns string containing milestone start date when date has already started and due date is not present', done => {
+ const vmStarted = createComponent(
+ Object.assign({}, mockMilestone, {
+ start_date: '1990-07-22',
+ due_date: '',
+ }),
+ );
+
+ Vue.nextTick()
+ .then(() => {
+ expect(vmStarted.milestoneDatesHuman).toContain('Started');
+ })
+ .then(done)
+ .catch(done.fail);
+
+ vmStarted.$destroy();
+ });
+
+ it('returns string containing milestone start date when date is yet to start and due date is not present', done => {
+ const vmStarts = createComponent(
+ Object.assign({}, mockMilestone, {
+ start_date: `${new Date().getFullYear() + 10}-01-01`,
+ due_date: '',
+ }),
+ );
+
+ Vue.nextTick()
+ .then(() => {
+ expect(vmStarts.milestoneDatesHuman).toContain('Starts');
+ })
+ .then(done)
+ .catch(done.fail);
+
+ vmStarts.$destroy();
+ });
+
+ it('returns empty string when milestone start and due dates are not present', done => {
+ const vmDatesUndefined = createComponent(
+ Object.assign({}, mockMilestone, {
+ start_date: '',
+ due_date: '',
+ }),
+ );
+
+ Vue.nextTick()
+ .then(() => {
+ expect(vmDatesUndefined.milestoneDatesHuman).toBe('');
+ })
+ .then(done)
+ .catch(done.fail);
+
+ vmDatesUndefined.$destroy();
+ });
+ });
+ });
+
+ describe('template', () => {
+ it('renders component root element with class `issue-milestone-details`', () => {
+ expect(vm.$el.classList.contains('issue-milestone-details')).toBe(true);
+ });
+
+ it('renders milestone icon', () => {
+ expect(vm.$el.querySelector('svg use').getAttribute('xlink:href')).toContain('clock');
+ });
+
+ it('renders milestone title', () => {
+ expect(vm.$el.querySelector('.milestone-title').innerText.trim()).toBe(mockMilestone.title);
+ });
+
+ it('renders milestone tooltip', () => {
+ expect(vm.$el.querySelector('.js-item-milestone').innerText.trim()).toContain(
+ mockMilestone.title,
+ );
+ });
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/markdown/field_spec.js b/spec/javascripts/vue_shared/components/markdown/field_spec.js
index abb17440c0e..79e0e756a7a 100644
--- a/spec/javascripts/vue_shared/components/markdown/field_spec.js
+++ b/spec/javascripts/vue_shared/components/markdown/field_spec.js
@@ -80,7 +80,7 @@ describe('Markdown field component', () => {
previewLink.click();
Vue.nextTick(() => {
- expect(vm.$el.querySelector('.md-preview').textContent.trim()).toContain('Loading...');
+ expect(vm.$el.querySelector('.md-preview').textContent.trim()).toContain('Loading…');
done();
});
diff --git a/spec/javascripts/vue_shared/components/user_avatar/user_avatar_image_spec.js b/spec/javascripts/vue_shared/components/user_avatar/user_avatar_image_spec.js
index 5c4aa7cf844..c5045afc5b0 100644
--- a/spec/javascripts/vue_shared/components/user_avatar/user_avatar_image_spec.js
+++ b/spec/javascripts/vue_shared/components/user_avatar/user_avatar_image_spec.js
@@ -2,6 +2,7 @@ import Vue from 'vue';
import { placeholderImage } from '~/lazy_loader';
import userAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
import mountComponent, { mountComponentWithSlots } from 'spec/helpers/vue_mount_component_helper';
+import defaultAvatarUrl from '~/../images/no_avatar.png';
const DEFAULT_PROPS = {
size: 99,
@@ -76,6 +77,18 @@ describe('User Avatar Image Component', function() {
});
});
+ describe('Initialization without src', function() {
+ beforeEach(function() {
+ vm = mountComponent(UserAvatarImage);
+ });
+
+ it('should have default avatar image', function() {
+ const imageElement = vm.$el.querySelector('img');
+
+ expect(imageElement.getAttribute('src')).toBe(defaultAvatarUrl);
+ });
+ });
+
describe('dynamic tooltip content', () => {
const props = DEFAULT_PROPS;
const slots = {
diff --git a/spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js b/spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js
index 0151ad23ba2..f2472fd377c 100644
--- a/spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js
+++ b/spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js
@@ -74,9 +74,7 @@ describe('User Avatar Link Component', function() {
describe('username', function() {
it('should not render avatar image tooltip', function() {
- expect(
- this.userAvatarLink.$el.querySelector('.js-user-avatar-image-toolip').innerText.trim(),
- ).toEqual('');
+ expect(this.userAvatarLink.$el.querySelector('.js-user-avatar-image-toolip')).toBeNull();
});
it('should render username prop in <span>', function() {
diff --git a/spec/javascripts/vue_shared/components/user_popover/user_popover_spec.js b/spec/javascripts/vue_shared/components/user_popover/user_popover_spec.js
new file mode 100644
index 00000000000..1578b0f81f9
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/user_popover/user_popover_spec.js
@@ -0,0 +1,133 @@
+import Vue from 'vue';
+import userPopover from '~/vue_shared/components/user_popover/user_popover.vue';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
+
+const DEFAULT_PROPS = {
+ loaded: true,
+ user: {
+ username: 'root',
+ name: 'Administrator',
+ location: 'Vienna',
+ bio: null,
+ organization: null,
+ status: null,
+ },
+};
+
+const UserPopover = Vue.extend(userPopover);
+
+describe('User Popover Component', () => {
+ let vm;
+
+ beforeEach(() => {
+ setFixtures(`
+ <a href="/root" data-user-id="1" class="js-user-link" title="testuser">
+ Root
+ </a>
+ `);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('Empty', () => {
+ beforeEach(() => {
+ vm = mountComponent(UserPopover, {
+ target: document.querySelector('.js-user-link'),
+ user: {
+ name: null,
+ username: null,
+ location: null,
+ bio: null,
+ organization: null,
+ status: null,
+ },
+ });
+ });
+
+ it('should return skeleton loaders', () => {
+ expect(vm.$el.querySelectorAll('.animation-container').length).toBe(4);
+ });
+ });
+
+ describe('basic data', () => {
+ it('should show basic fields', () => {
+ vm = mountComponent(UserPopover, {
+ ...DEFAULT_PROPS,
+ target: document.querySelector('.js-user-link'),
+ });
+
+ expect(vm.$el.textContent).toContain(DEFAULT_PROPS.user.name);
+ expect(vm.$el.textContent).toContain(DEFAULT_PROPS.user.username);
+ expect(vm.$el.textContent).toContain(DEFAULT_PROPS.user.location);
+ });
+ });
+
+ describe('job data', () => {
+ it('should show only bio if no organization is available', () => {
+ const testProps = Object.assign({}, DEFAULT_PROPS);
+ testProps.user.bio = 'Engineer';
+
+ vm = mountComponent(UserPopover, {
+ ...testProps,
+ target: document.querySelector('.js-user-link'),
+ });
+
+ expect(vm.$el.textContent).toContain('Engineer');
+ });
+
+ it('should show only organization if no bio is available', () => {
+ const testProps = Object.assign({}, DEFAULT_PROPS);
+ testProps.user.organization = 'GitLab';
+
+ vm = mountComponent(UserPopover, {
+ ...testProps,
+ target: document.querySelector('.js-user-link'),
+ });
+
+ expect(vm.$el.textContent).toContain('GitLab');
+ });
+
+ it('should have full job line when we have bio and organization', () => {
+ const testProps = Object.assign({}, DEFAULT_PROPS);
+ testProps.user.bio = 'Engineer';
+ testProps.user.organization = 'GitLab';
+
+ vm = mountComponent(UserPopover, {
+ ...DEFAULT_PROPS,
+ target: document.querySelector('.js-user-link'),
+ });
+
+ expect(vm.$el.textContent).toContain('Engineer at GitLab');
+ });
+ });
+
+ describe('status data', () => {
+ it('should show only message', () => {
+ const testProps = Object.assign({}, DEFAULT_PROPS);
+ testProps.user.status = { message: 'Hello World' };
+
+ vm = mountComponent(UserPopover, {
+ ...DEFAULT_PROPS,
+ target: document.querySelector('.js-user-link'),
+ });
+
+ expect(vm.$el.textContent).toContain('Hello World');
+ });
+
+ it('should show message and emoji', () => {
+ const testProps = Object.assign({}, DEFAULT_PROPS);
+ testProps.user.status = { emoji: 'basketball_player', message: 'Hello World' };
+
+ vm = mountComponent(UserPopover, {
+ ...DEFAULT_PROPS,
+ target: document.querySelector('.js-user-link'),
+ status: { emoji: 'basketball_player', message: 'Hello World' },
+ });
+
+ expect(vm.$el.textContent).toContain('Hello World');
+ expect(vm.$el.innerHTML).toContain('<gl-emoji data-name="basketball_player"');
+ });
+ });
+});
diff --git a/spec/lib/backup/repository_spec.rb b/spec/lib/backup/repository_spec.rb
index fdeea814bb2..5ace5c5b1a2 100644
--- a/spec/lib/backup/repository_spec.rb
+++ b/spec/lib/backup/repository_spec.rb
@@ -67,6 +67,19 @@ describe Backup::Repository do
end
end
end
+
+ context 'restoring object pools' do
+ it 'schedules restoring of the pool' do
+ pool_repository = create(:pool_repository, :failed)
+ pool_repository.delete_object_pool
+
+ subject.restore
+
+ pool_repository.reload
+ expect(pool_repository).not_to be_failed
+ expect(pool_repository.object_pool.exists?).to be(true)
+ end
+ end
end
describe '#prepare_directories', :seed_helper do
diff --git a/spec/lib/banzai/filter/front_matter_filter_spec.rb b/spec/lib/banzai/filter/front_matter_filter_spec.rb
new file mode 100644
index 00000000000..3071dc7cf21
--- /dev/null
+++ b/spec/lib/banzai/filter/front_matter_filter_spec.rb
@@ -0,0 +1,140 @@
+require 'rails_helper'
+
+describe Banzai::Filter::FrontMatterFilter do
+ include FilterSpecHelper
+
+ it 'allows for `encoding:` before the front matter' do
+ content = <<~MD
+ # encoding: UTF-8
+ ---
+ foo: foo
+ bar: bar
+ ---
+
+ # Header
+
+ Content
+ MD
+
+ output = filter(content)
+
+ expect(output).not_to match 'encoding'
+ end
+
+ it 'converts YAML front matter to a fenced code block' do
+ content = <<~MD
+ ---
+ foo: :foo_symbol
+ bar: :bar_symbol
+ ---
+
+ # Header
+
+ Content
+ MD
+
+ output = filter(content)
+
+ aggregate_failures do
+ expect(output).not_to include '---'
+ expect(output).to include "```yaml\nfoo: :foo_symbol\n"
+ end
+ end
+
+ it 'converts TOML frontmatter to a fenced code block' do
+ content = <<~MD
+ +++
+ foo = :foo_symbol
+ bar = :bar_symbol
+ +++
+
+ # Header
+
+ Content
+ MD
+
+ output = filter(content)
+
+ aggregate_failures do
+ expect(output).not_to include '+++'
+ expect(output).to include "```toml\nfoo = :foo_symbol\n"
+ end
+ end
+
+ it 'converts JSON front matter to a fenced code block' do
+ content = <<~MD
+ ;;;
+ {
+ "foo": ":foo_symbol",
+ "bar": ":bar_symbol"
+ }
+ ;;;
+
+ # Header
+
+ Content
+ MD
+
+ output = filter(content)
+
+ aggregate_failures do
+ expect(output).not_to include ';;;'
+ expect(output).to include "```json\n{\n \"foo\": \":foo_symbol\",\n"
+ end
+ end
+
+ it 'converts arbitrary front matter to a fenced code block' do
+ content = <<~MD
+ ---arbitrary
+ foo = :foo_symbol
+ bar = :bar_symbol
+ ---
+
+ # Header
+
+ Content
+ MD
+
+ output = filter(content)
+
+ aggregate_failures do
+ expect(output).not_to include '---arbitrary'
+ expect(output).to include "```arbitrary\nfoo = :foo_symbol\n"
+ end
+ end
+
+ context 'on content without front matter' do
+ it 'returns the content unmodified' do
+ content = <<~MD
+ # This is some Markdown
+
+ It has no YAML front matter to parse.
+ MD
+
+ expect(filter(content)).to eq content
+ end
+ end
+
+ context 'on front matter without content' do
+ it 'converts YAML front matter to a fenced code block' do
+ content = <<~MD
+ ---
+ foo: :foo_symbol
+ bar: :bar_symbol
+ ---
+ MD
+
+ output = filter(content)
+
+ aggregate_failures do
+ expect(output).to eq <<~MD
+ ```yaml
+ foo: :foo_symbol
+ bar: :bar_symbol
+ ```
+
+ MD
+ end
+ end
+ end
+end
diff --git a/spec/lib/banzai/filter/milestone_reference_filter_spec.rb b/spec/lib/banzai/filter/milestone_reference_filter_spec.rb
index 91d4a60ba95..1a87cfa5b45 100644
--- a/spec/lib/banzai/filter/milestone_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/milestone_reference_filter_spec.rb
@@ -351,21 +351,50 @@ describe Banzai::Filter::MilestoneReferenceFilter do
end
context 'group context' do
- let(:context) { { project: nil, group: create(:group) } }
- let(:milestone) { create(:milestone, project: project) }
+ let(:group) { create(:group) }
+ let(:context) { { project: nil, group: group } }
- it 'links to a valid reference' do
- reference = "#{project.full_path}%#{milestone.iid}"
+ context 'when project milestone' do
+ let(:milestone) { create(:milestone, project: project) }
- result = reference_filter("See #{reference}", context)
+ it 'links to a valid reference' do
+ reference = "#{project.full_path}%#{milestone.iid}"
- expect(result.css('a').first.attr('href')).to eq(urls.milestone_url(milestone))
+ result = reference_filter("See #{reference}", context)
+
+ expect(result.css('a').first.attr('href')).to eq(urls.milestone_url(milestone))
+ end
+
+ it 'ignores internal references' do
+ exp = act = "See %#{milestone.iid}"
+
+ expect(reference_filter(act, context).to_html).to eq exp
+ end
end
- it 'ignores internal references' do
- exp = act = "See %#{milestone.iid}"
+ context 'when group milestone' do
+ let(:group_milestone) { create(:milestone, title: 'group_milestone', group: group) }
- expect(reference_filter(act, context).to_html).to eq exp
+ context 'for subgroups', :nested_groups do
+ let(:sub_group) { create(:group, parent: group) }
+ let(:sub_group_milestone) { create(:milestone, title: 'sub_group_milestone', group: sub_group) }
+
+ it 'links to a valid reference of subgroup and group milestones' do
+ [group_milestone, sub_group_milestone].each do |milestone|
+ reference = "%#{milestone.title}"
+
+ result = reference_filter("See #{reference}", { project: nil, group: sub_group })
+
+ expect(result.css('a').first.attr('href')).to eq(urls.milestone_url(milestone))
+ end
+ end
+ end
+
+ it 'ignores internal references' do
+ exp = act = "See %#{group_milestone.iid}"
+
+ expect(reference_filter(act, context).to_html).to eq exp
+ end
end
end
diff --git a/spec/lib/banzai/filter/user_reference_filter_spec.rb b/spec/lib/banzai/filter/user_reference_filter_spec.rb
index 334d29a5368..1e8a44b4549 100644
--- a/spec/lib/banzai/filter/user_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/user_reference_filter_spec.rb
@@ -120,7 +120,7 @@ describe Banzai::Filter::UserReferenceFilter do
it 'includes default classes' do
doc = reference_filter("Hey #{reference}")
- expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-project_member has-tooltip'
+ expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-project_member'
end
context 'when a project is not specified' do
diff --git a/spec/lib/banzai/filter/yaml_front_matter_filter_spec.rb b/spec/lib/banzai/filter/yaml_front_matter_filter_spec.rb
deleted file mode 100644
index 9f1b862ef19..00000000000
--- a/spec/lib/banzai/filter/yaml_front_matter_filter_spec.rb
+++ /dev/null
@@ -1,53 +0,0 @@
-require 'rails_helper'
-
-describe Banzai::Filter::YamlFrontMatterFilter do
- include FilterSpecHelper
-
- it 'allows for `encoding:` before the frontmatter' do
- content = <<-MD.strip_heredoc
- # encoding: UTF-8
- ---
- foo: foo
- ---
-
- # Header
-
- Content
- MD
-
- output = filter(content)
-
- expect(output).not_to match 'encoding'
- end
-
- it 'converts YAML frontmatter to a fenced code block' do
- content = <<-MD.strip_heredoc
- ---
- bar: :bar_symbol
- ---
-
- # Header
-
- Content
- MD
-
- output = filter(content)
-
- aggregate_failures do
- expect(output).not_to include '---'
- expect(output).to include "```yaml\nbar: :bar_symbol\n```"
- end
- end
-
- context 'on content without frontmatter' do
- it 'returns the content unmodified' do
- content = <<-MD.strip_heredoc
- # This is some Markdown
-
- It has no YAML frontmatter to parse.
- MD
-
- expect(filter(content)).to eq content
- end
- end
-end
diff --git a/spec/lib/gitlab/background_migration/backfill_hashed_project_repositories_spec.rb b/spec/lib/gitlab/background_migration/backfill_hashed_project_repositories_spec.rb
new file mode 100644
index 00000000000..b6c1edbbf8b
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/backfill_hashed_project_repositories_spec.rb
@@ -0,0 +1,90 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::BackgroundMigration::BackfillHashedProjectRepositories, :migration, schema: 20181130102132 do
+ let(:namespaces) { table(:namespaces) }
+ let(:project_repositories) { table(:project_repositories) }
+ let(:projects) { table(:projects) }
+ let(:shards) { table(:shards) }
+ let(:group) { namespaces.create!(name: 'foo', path: 'foo') }
+ let(:shard) { shards.create!(name: 'default') }
+
+ describe described_class::ShardFinder do
+ describe '#find_shard_id' do
+ it 'creates a new shard when it does not exist yet' do
+ expect { subject.find_shard_id('other') }.to change(shards, :count).by(1)
+ end
+
+ it 'returns the shard when it exists' do
+ shards.create(id: 5, name: 'other')
+
+ shard_id = subject.find_shard_id('other')
+
+ expect(shard_id).to eq(5)
+ end
+
+ it 'only queries the database once to retrieve shards' do
+ subject.find_shard_id('default')
+
+ expect { subject.find_shard_id('default') }.not_to exceed_query_limit(0)
+ end
+ end
+ end
+
+ describe described_class::Project do
+ describe '.on_hashed_storage' do
+ it 'finds projects with repository on hashed storage' do
+ projects.create!(id: 1, name: 'foo', path: 'foo', namespace_id: group.id, storage_version: 1)
+ projects.create!(id: 2, name: 'bar', path: 'bar', namespace_id: group.id, storage_version: 2)
+ projects.create!(id: 3, name: 'baz', path: 'baz', namespace_id: group.id, storage_version: 0)
+ projects.create!(id: 4, name: 'zoo', path: 'zoo', namespace_id: group.id, storage_version: nil)
+
+ expect(described_class.on_hashed_storage.pluck(:id)).to match_array([1, 2])
+ end
+ end
+
+ describe '.without_project_repository' do
+ it 'finds projects which do not have a projects_repositories entry' do
+ projects.create!(id: 1, name: 'foo', path: 'foo', namespace_id: group.id)
+ projects.create!(id: 2, name: 'bar', path: 'bar', namespace_id: group.id)
+ project_repositories.create!(project_id: 2, disk_path: '@phony/foo/bar', shard_id: shard.id)
+
+ expect(described_class.without_project_repository.pluck(:id)).to contain_exactly(1)
+ end
+ end
+ end
+
+ describe '#perform' do
+ it 'creates a project_repository row for projects on hashed storage that need one' do
+ projects.create!(id: 1, name: 'foo', path: 'foo', namespace_id: group.id, storage_version: 1)
+ projects.create!(id: 2, name: 'bar', path: 'bar', namespace_id: group.id, storage_version: 2)
+
+ expect { described_class.new.perform(1, projects.last.id) }.to change(project_repositories, :count).by(2)
+ end
+
+ it 'does nothing for projects on hashed storage that have already a project_repository row' do
+ projects.create!(id: 1, name: 'foo', path: 'foo', namespace_id: group.id, storage_version: 1)
+ project_repositories.create!(project_id: 1, disk_path: '@phony/foo/bar', shard_id: shard.id)
+
+ expect { described_class.new.perform(1, projects.last.id) }.not_to change(project_repositories, :count)
+ end
+
+ it 'does nothing for projects on legacy storage' do
+ projects.create!(name: 'foo', path: 'foo', namespace_id: group.id, storage_version: 0)
+
+ expect { described_class.new.perform(1, projects.last.id) }.not_to change(project_repositories, :count)
+ end
+
+ it 'inserts rows in a single query' do
+ projects.create!(name: 'foo', path: 'foo', namespace_id: group.id, storage_version: 1, repository_storage: shard.name)
+
+ control_count = ActiveRecord::QueryRecorder.new { described_class.new.perform(1, projects.last.id) }
+
+ projects.create!(name: 'bar', path: 'bar', namespace_id: group.id, storage_version: 1, repository_storage: shard.name)
+ projects.create!(name: 'zoo', path: 'zoo', namespace_id: group.id, storage_version: 1, repository_storage: shard.name)
+
+ expect { described_class.new.perform(1, projects.last.id) }.not_to exceed_query_limit(control_count)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/populate_merge_request_metrics_with_events_data_improved_spec.rb b/spec/lib/gitlab/background_migration/populate_merge_request_metrics_with_events_data_improved_spec.rb
new file mode 100644
index 00000000000..d1d64574627
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/populate_merge_request_metrics_with_events_data_improved_spec.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Gitlab::BackgroundMigration::PopulateMergeRequestMetricsWithEventsDataImproved, :migration, schema: 20181204154019 do
+ let(:namespaces) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+ let(:users) { table(:users) }
+ let(:events) { table(:events) }
+
+ let(:user) { users.create!(email: 'test@example.com', projects_limit: 100, username: 'test') }
+
+ let(:namespace) { namespaces.create(name: 'gitlab', path: 'gitlab-org') }
+ let(:project) { projects.create(namespace_id: namespace.id, name: 'foo') }
+ let(:merge_requests) { table(:merge_requests) }
+
+ def create_merge_request(id, params = {})
+ params.merge!(id: id,
+ target_project_id: project.id,
+ target_branch: 'master',
+ source_project_id: project.id,
+ source_branch: 'mr name',
+ title: "mr name#{id}")
+
+ merge_requests.create(params)
+ end
+
+ def create_merge_request_event(id, params = {})
+ params.merge!(id: id,
+ project_id: project.id,
+ author_id: user.id,
+ target_type: 'MergeRequest')
+
+ events.create(params)
+ end
+
+ describe '#perform' do
+ it 'creates and updates closed and merged events' do
+ timestamp = Time.new('2018-01-01 12:00:00').utc
+
+ create_merge_request(1)
+ create_merge_request_event(1, target_id: 1, action: 3, updated_at: timestamp)
+ create_merge_request_event(2, target_id: 1, action: 3, updated_at: timestamp + 10.seconds)
+
+ create_merge_request_event(3, target_id: 1, action: 7, updated_at: timestamp)
+ create_merge_request_event(4, target_id: 1, action: 7, updated_at: timestamp + 10.seconds)
+
+ subject.perform(1, 1)
+
+ merge_request = MergeRequest.first
+
+ expect(merge_request.metrics).to have_attributes(latest_closed_by_id: user.id,
+ latest_closed_at: timestamp + 10.seconds,
+ merged_by_id: user.id)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/branch_push_merge_commit_analyzer_spec.rb b/spec/lib/gitlab/branch_push_merge_commit_analyzer_spec.rb
new file mode 100644
index 00000000000..1e969542975
--- /dev/null
+++ b/spec/lib/gitlab/branch_push_merge_commit_analyzer_spec.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::BranchPushMergeCommitAnalyzer do
+ let(:project) { create(:project, :repository) }
+ let(:oldrev) { 'merge-commit-analyze-before' }
+ let(:newrev) { 'merge-commit-analyze-after' }
+ let(:commits) { project.repository.commits_between(oldrev, newrev).reverse }
+
+ subject { described_class.new(commits) }
+
+ describe '#get_merge_commit' do
+ let(:expected_merge_commits) do
+ {
+ '646ece5cfed840eca0a4feb21bcd6a81bb19bda3' => '646ece5cfed840eca0a4feb21bcd6a81bb19bda3',
+ '29284d9bcc350bcae005872d0be6edd016e2efb5' => '29284d9bcc350bcae005872d0be6edd016e2efb5',
+ '5f82584f0a907f3b30cfce5bb8df371454a90051' => '29284d9bcc350bcae005872d0be6edd016e2efb5',
+ '8a994512e8c8f0dfcf22bb16df6e876be7a61036' => '29284d9bcc350bcae005872d0be6edd016e2efb5',
+ '689600b91aabec706e657e38ea706ece1ee8268f' => '29284d9bcc350bcae005872d0be6edd016e2efb5',
+ 'db46a1c5a5e474aa169b6cdb7a522d891bc4c5f9' => 'db46a1c5a5e474aa169b6cdb7a522d891bc4c5f9'
+ }
+ end
+
+ it 'returns correct merge commit SHA for each commit' do
+ expected_merge_commits.each do |commit, merge_commit|
+ expect(subject.get_merge_commit(commit)).to eq(merge_commit)
+ end
+ end
+
+ context 'when one parent has two children' do
+ let(:oldrev) { '1adbdefe31288f3bbe4b614853de4908a0b6f792' }
+ let(:newrev) { '5f82584f0a907f3b30cfce5bb8df371454a90051' }
+
+ let(:expected_merge_commits) do
+ {
+ '5f82584f0a907f3b30cfce5bb8df371454a90051' => '5f82584f0a907f3b30cfce5bb8df371454a90051',
+ '8a994512e8c8f0dfcf22bb16df6e876be7a61036' => '5f82584f0a907f3b30cfce5bb8df371454a90051',
+ '689600b91aabec706e657e38ea706ece1ee8268f' => '689600b91aabec706e657e38ea706ece1ee8268f'
+ }
+ end
+
+ it 'returns correct merge commit SHA for each commit' do
+ expected_merge_commits.each do |commit, merge_commit|
+ expect(subject.get_merge_commit(commit)).to eq(merge_commit)
+ end
+ end
+ end
+
+ context 'when relevant_commit_ids is provided' do
+ let(:relevant_commit_id) { '8a994512e8c8f0dfcf22bb16df6e876be7a61036' }
+ subject { described_class.new(commits, relevant_commit_ids: [relevant_commit_id]) }
+
+ it 'returns correct merge commit' do
+ expected_merge_commits.each do |commit, merge_commit|
+ subject = described_class.new(commits, relevant_commit_ids: [commit])
+ expect(subject.get_merge_commit(commit)).to eq(merge_commit)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/entry/except_policy_spec.rb b/spec/lib/gitlab/ci/config/entry/except_policy_spec.rb
deleted file mode 100644
index d036bf2f4d1..00000000000
--- a/spec/lib/gitlab/ci/config/entry/except_policy_spec.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-describe Gitlab::Ci::Config::Entry::ExceptPolicy do
- let(:entry) { described_class.new(config) }
-
- it_behaves_like 'correct only except policy'
-
- describe '.default' do
- it 'does not have a default value' do
- expect(described_class.default).to be_nil
- end
- end
-end
diff --git a/spec/lib/gitlab/ci/config/entry/global_spec.rb b/spec/lib/gitlab/ci/config/entry/global_spec.rb
index 12f4b9dc624..61d78f86b51 100644
--- a/spec/lib/gitlab/ci/config/entry/global_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/global_spec.rb
@@ -161,7 +161,8 @@ describe Gitlab::Ci::Config::Entry::Global do
variables: { 'VAR' => 'value' },
ignore: false,
after_script: ['make clean'],
- only: { refs: %w[branches tags] } },
+ only: { refs: %w[branches tags] },
+ except: {} },
spinach: { name: :spinach,
before_script: [],
script: %w[spinach],
@@ -173,7 +174,8 @@ describe Gitlab::Ci::Config::Entry::Global do
variables: {},
ignore: false,
after_script: ['make clean'],
- only: { refs: %w[branches tags] } }
+ only: { refs: %w[branches tags] },
+ except: {} }
)
end
end
diff --git a/spec/lib/gitlab/ci/config/entry/job_spec.rb b/spec/lib/gitlab/ci/config/entry/job_spec.rb
index c1f4a060063..8e32cede3b5 100644
--- a/spec/lib/gitlab/ci/config/entry/job_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/job_spec.rb
@@ -259,7 +259,8 @@ describe Gitlab::Ci::Config::Entry::Job do
stage: 'test',
ignore: false,
after_script: %w[cleanup],
- only: { refs: %w[branches tags] })
+ only: { refs: %w[branches tags] },
+ except: {})
end
end
end
diff --git a/spec/lib/gitlab/ci/config/entry/jobs_spec.rb b/spec/lib/gitlab/ci/config/entry/jobs_spec.rb
index 2a753408f54..1a2c30d3571 100644
--- a/spec/lib/gitlab/ci/config/entry/jobs_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/jobs_spec.rb
@@ -68,13 +68,15 @@ describe Gitlab::Ci::Config::Entry::Jobs do
commands: 'rspec',
ignore: false,
stage: 'test',
- only: { refs: %w[branches tags] } },
+ only: { refs: %w[branches tags] },
+ except: {} },
spinach: { name: :spinach,
script: %w[spinach],
commands: 'spinach',
ignore: false,
stage: 'test',
- only: { refs: %w[branches tags] } })
+ only: { refs: %w[branches tags] },
+ except: {} })
end
end
diff --git a/spec/lib/gitlab/ci/config/entry/only_policy_spec.rb b/spec/lib/gitlab/ci/config/entry/only_policy_spec.rb
deleted file mode 100644
index 5518b68e51a..00000000000
--- a/spec/lib/gitlab/ci/config/entry/only_policy_spec.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-describe Gitlab::Ci::Config::Entry::OnlyPolicy do
- let(:entry) { described_class.new(config) }
-
- it_behaves_like 'correct only except policy'
-
- describe '.default' do
- it 'haa a default value' do
- expect(described_class.default).to eq( { refs: %w[branches tags] } )
- end
- end
-end
diff --git a/spec/lib/gitlab/ci/config/entry/policy_spec.rb b/spec/lib/gitlab/ci/config/entry/policy_spec.rb
index cf40a22af2e..83001b7fdd8 100644
--- a/spec/lib/gitlab/ci/config/entry/policy_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/policy_spec.rb
@@ -1,8 +1,173 @@
-require 'spec_helper'
+require 'fast_spec_helper'
+require_dependency 'active_model'
describe Gitlab::Ci::Config::Entry::Policy do
let(:entry) { described_class.new(config) }
+ context 'when using simplified policy' do
+ describe 'validations' do
+ context 'when entry config value is valid' do
+ context 'when config is a branch or tag name' do
+ let(:config) { %w[master feature/branch] }
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+
+ describe '#value' do
+ it 'returns refs hash' do
+ expect(entry.value).to eq(refs: config)
+ end
+ end
+ end
+
+ context 'when config is a regexp' do
+ let(:config) { ['/^issue-.*$/'] }
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+ end
+
+ context 'when config is a special keyword' do
+ let(:config) { %w[tags triggers branches] }
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+ end
+ end
+
+ context 'when entry value is not valid' do
+ let(:config) { [1] }
+
+ describe '#errors' do
+ it 'saves errors' do
+ expect(entry.errors)
+ .to include /policy config should be an array of strings or regexps/
+ end
+ end
+ end
+ end
+ end
+
+ context 'when using complex policy' do
+ context 'when specifying refs policy' do
+ let(:config) { { refs: ['master'] } }
+
+ it 'is a correct configuraton' do
+ expect(entry).to be_valid
+ expect(entry.value).to eq(refs: %w[master])
+ end
+ end
+
+ context 'when specifying kubernetes policy' do
+ let(:config) { { kubernetes: 'active' } }
+
+ it 'is a correct configuraton' do
+ expect(entry).to be_valid
+ expect(entry.value).to eq(kubernetes: 'active')
+ end
+ end
+
+ context 'when specifying invalid kubernetes policy' do
+ let(:config) { { kubernetes: 'something' } }
+
+ it 'reports an error about invalid policy' do
+ expect(entry.errors).to include /unknown value: something/
+ end
+ end
+
+ context 'when specifying valid variables expressions policy' do
+ let(:config) { { variables: ['$VAR == null'] } }
+
+ it 'is a correct configuraton' do
+ expect(entry).to be_valid
+ expect(entry.value).to eq(config)
+ end
+ end
+
+ context 'when specifying variables expressions in invalid format' do
+ let(:config) { { variables: '$MY_VAR' } }
+
+ it 'reports an error about invalid format' do
+ expect(entry.errors).to include /should be an array of strings/
+ end
+ end
+
+ context 'when specifying invalid variables expressions statement' do
+ let(:config) { { variables: ['$MY_VAR =='] } }
+
+ it 'reports an error about invalid statement' do
+ expect(entry.errors).to include /invalid expression syntax/
+ end
+ end
+
+ context 'when specifying invalid variables expressions token' do
+ let(:config) { { variables: ['$MY_VAR == 123'] } }
+
+ it 'reports an error about invalid expression' do
+ expect(entry.errors).to include /invalid expression syntax/
+ end
+ end
+
+ context 'when using invalid variables expressions regexp' do
+ let(:config) { { variables: ['$MY_VAR =~ /some ( thing/'] } }
+
+ it 'reports an error about invalid expression' do
+ expect(entry.errors).to include /invalid expression syntax/
+ end
+ end
+
+ context 'when specifying a valid changes policy' do
+ let(:config) { { changes: %w[some/* paths/**/*.rb] } }
+
+ it 'is a correct configuraton' do
+ expect(entry).to be_valid
+ expect(entry.value).to eq(config)
+ end
+ end
+
+ context 'when changes policy is invalid' do
+ let(:config) { { changes: [1, 2] } }
+
+ it 'returns errors' do
+ expect(entry.errors).to include /changes should be an array of strings/
+ end
+ end
+
+ context 'when specifying unknown policy' do
+ let(:config) { { refs: ['master'], invalid: :something } }
+
+ it 'returns error about invalid key' do
+ expect(entry.errors).to include /unknown keys: invalid/
+ end
+ end
+
+ context 'when policy is empty' do
+ let(:config) { {} }
+
+ it 'is not a valid configuration' do
+ expect(entry.errors).to include /can't be blank/
+ end
+ end
+ end
+
+ context 'when policy strategy does not match' do
+ let(:config) { 'string strategy' }
+
+ it 'returns information about errors' do
+ expect(entry.errors)
+ .to include /has to be either an array of conditions or a hash/
+ end
+ end
+
describe '.default' do
it 'does not have a default value' do
expect(described_class.default).to be_nil
diff --git a/spec/lib/gitlab/ci/parsers/test_spec.rb b/spec/lib/gitlab/ci/parsers_spec.rb
index 0b85b432677..4b647bffe59 100644
--- a/spec/lib/gitlab/ci/parsers/test_spec.rb
+++ b/spec/lib/gitlab/ci/parsers_spec.rb
@@ -1,6 +1,8 @@
+# frozen_string_literal: true
+
require 'spec_helper'
-describe Gitlab::Ci::Parsers::Test do
+describe Gitlab::Ci::Parsers do
describe '.fabricate!' do
subject { described_class.fabricate!(file_type) }
@@ -8,7 +10,7 @@ describe Gitlab::Ci::Parsers::Test do
let(:file_type) { 'junit' }
it 'fabricates the class' do
- is_expected.to be_a(described_class::Junit)
+ is_expected.to be_a(described_class::Test::Junit)
end
end
@@ -16,7 +18,7 @@ describe Gitlab::Ci::Parsers::Test do
let(:file_type) { 'undefined' }
it 'raises an error' do
- expect { subject }.to raise_error(Gitlab::Ci::Parsers::Test::ParserNotFoundError)
+ expect { subject }.to raise_error(Gitlab::Ci::Parsers::ParserNotFoundError)
end
end
end
diff --git a/spec/lib/gitlab/correlation_id_spec.rb b/spec/lib/gitlab/correlation_id_spec.rb
new file mode 100644
index 00000000000..584d1f48386
--- /dev/null
+++ b/spec/lib/gitlab/correlation_id_spec.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+describe Gitlab::CorrelationId do
+ describe '.use_id' do
+ it 'yields when executed' do
+ expect { |blk| described_class.use_id('id', &blk) }.to yield_control
+ end
+
+ it 'stacks correlation ids' do
+ described_class.use_id('id1') do
+ described_class.use_id('id2') do |current_id|
+ expect(current_id).to eq('id2')
+ end
+ end
+ end
+
+ it 'for missing correlation id it generates random one' do
+ described_class.use_id('id1') do
+ described_class.use_id(nil) do |current_id|
+ expect(current_id).not_to be_empty
+ expect(current_id).not_to eq('id1')
+ end
+ end
+ end
+ end
+
+ describe '.current_id' do
+ subject { described_class.current_id }
+
+ it 'returns last correlation id' do
+ described_class.use_id('id1') do
+ described_class.use_id('id2') do
+ is_expected.to eq('id2')
+ end
+ end
+ end
+ end
+
+ describe '.current_or_new_id' do
+ subject { described_class.current_or_new_id }
+
+ context 'when correlation id is set' do
+ it 'returns last correlation id' do
+ described_class.use_id('id1') do
+ is_expected.to eq('id1')
+ end
+ end
+ end
+
+ context 'when correlation id is missing' do
+ it 'returns a new correlation id' do
+ expect(described_class).to receive(:new_id)
+ .and_call_original
+
+ is_expected.not_to be_empty
+ end
+ end
+ end
+
+ describe '.ids' do
+ subject { described_class.send(:ids) }
+
+ it 'returns empty list if not correlation is used' do
+ is_expected.to be_empty
+ end
+
+ it 'returns list if correlation ids are used' do
+ described_class.use_id('id1') do
+ described_class.use_id('id2') do
+ is_expected.to eq(%w(id1 id2))
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/count/exact_count_strategy_spec.rb b/spec/lib/gitlab/database/count/exact_count_strategy_spec.rb
index f518bb3dc3e..3991c737a26 100644
--- a/spec/lib/gitlab/database/count/exact_count_strategy_spec.rb
+++ b/spec/lib/gitlab/database/count/exact_count_strategy_spec.rb
@@ -16,6 +16,12 @@ describe Gitlab::Database::Count::ExactCountStrategy do
expect(subject).to eq({ Project => 3, Identity => 1 })
end
+
+ it 'returns default value if count times out' do
+ allow(models.first).to receive(:count).and_raise(ActiveRecord::StatementInvalid.new(''))
+
+ expect(subject).to eq({})
+ end
end
describe '.enabled?' do
diff --git a/spec/lib/gitlab/git/object_pool_spec.rb b/spec/lib/gitlab/git/object_pool_spec.rb
new file mode 100644
index 00000000000..363c2aa67af
--- /dev/null
+++ b/spec/lib/gitlab/git/object_pool_spec.rb
@@ -0,0 +1,89 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Git::ObjectPool do
+ let(:pool_repository) { create(:pool_repository) }
+ let(:source_repository) { pool_repository.source_project.repository }
+
+ subject { pool_repository.object_pool }
+
+ describe '#storage' do
+ it "equals the pool repository's shard name" do
+ expect(subject.storage).not_to be_nil
+ expect(subject.storage).to eq(pool_repository.shard_name)
+ end
+ end
+
+ describe '#create' do
+ before do
+ subject.create
+ end
+
+ context "when the pool doesn't exist yet" do
+ it 'creates the pool' do
+ expect(subject.exists?).to be(true)
+ end
+ end
+
+ context 'when the pool already exists' do
+ it 'raises an FailedPrecondition' do
+ expect do
+ subject.create
+ end.to raise_error(GRPC::FailedPrecondition)
+ end
+ end
+ end
+
+ describe '#exists?' do
+ context "when the object pool doesn't exist" do
+ it 'returns false' do
+ expect(subject.exists?).to be(false)
+ end
+ end
+
+ context 'when the object pool exists' do
+ let(:pool) { create(:pool_repository, :ready) }
+
+ subject { pool.object_pool }
+
+ it 'returns true' do
+ expect(subject.exists?).to be(true)
+ end
+ end
+ end
+
+ describe '#link' do
+ let!(:pool_repository) { create(:pool_repository, :ready) }
+
+ context 'when no remotes are set' do
+ it 'sets a remote' do
+ subject.link(source_repository)
+
+ repo = Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ Rugged::Repository.new(subject.repository.path)
+ end
+
+ expect(repo.remotes.count).to be(1)
+ expect(repo.remotes.first.name).to eq(source_repository.object_pool_remote_name)
+ end
+ end
+
+ context 'when the remote is already set' do
+ before do
+ subject.link(source_repository)
+ end
+
+ it "doesn't raise an error" do
+ subject.link(source_repository)
+
+ repo = Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ Rugged::Repository.new(subject.repository.path)
+ end
+
+ expect(repo.remotes.count).to be(1)
+ expect(repo.remotes.first.name).to eq(source_repository.object_pool_remote_name)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/git/repository_cleaner_spec.rb b/spec/lib/gitlab/git/repository_cleaner_spec.rb
new file mode 100644
index 00000000000..a9d9e67ef94
--- /dev/null
+++ b/spec/lib/gitlab/git/repository_cleaner_spec.rb
@@ -0,0 +1,32 @@
+require 'spec_helper'
+
+describe Gitlab::Git::RepositoryCleaner do
+ let(:project) { create(:project, :repository) }
+ let(:repository) { project.repository }
+ let(:head_sha) { repository.head_commit.id }
+
+ let(:object_map) { StringIO.new("#{head_sha} #{'0' * 40}") }
+
+ subject(:cleaner) { described_class.new(repository.raw) }
+
+ describe '#apply_bfg_object_map' do
+ it 'removes internal references pointing at SHAs in the object map' do
+ # Create some refs we expect to be removed
+ repository.keep_around(head_sha)
+ repository.create_ref(head_sha, 'refs/environments/1')
+ repository.create_ref(head_sha, 'refs/merge-requests/1')
+ repository.create_ref(head_sha, 'refs/heads/_keep')
+ repository.create_ref(head_sha, 'refs/tags/_keep')
+
+ cleaner.apply_bfg_object_map(object_map)
+
+ aggregate_failures do
+ expect(repository.kept_around?(head_sha)).to be_falsy
+ expect(repository.ref_exists?('refs/environments/1')).to be_falsy
+ expect(repository.ref_exists?('refs/merge-requests/1')).to be_falsy
+ expect(repository.ref_exists?('refs/heads/_keep')).to be_truthy
+ expect(repository.ref_exists?('refs/tags/_keep')).to be_truthy
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/gitaly_client/cleanup_service_spec.rb b/spec/lib/gitlab/gitaly_client/cleanup_service_spec.rb
new file mode 100644
index 00000000000..369deff732a
--- /dev/null
+++ b/spec/lib/gitlab/gitaly_client/cleanup_service_spec.rb
@@ -0,0 +1,19 @@
+require 'spec_helper'
+
+describe Gitlab::GitalyClient::CleanupService do
+ let(:project) { create(:project) }
+ let(:storage_name) { project.repository_storage }
+ let(:relative_path) { project.disk_path + '.git' }
+ let(:client) { described_class.new(project.repository) }
+
+ describe '#apply_bfg_object_map' do
+ it 'sends an apply_bfg_object_map message' do
+ expect_any_instance_of(Gitaly::CleanupService::Stub)
+ .to receive(:apply_bfg_object_map)
+ .with(kind_of(Enumerator), kind_of(Hash))
+ .and_return(double)
+
+ client.apply_bfg_object_map(StringIO.new)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/gitaly_client/object_pool_service_spec.rb b/spec/lib/gitlab/gitaly_client/object_pool_service_spec.rb
new file mode 100644
index 00000000000..149b7ec5bb0
--- /dev/null
+++ b/spec/lib/gitlab/gitaly_client/object_pool_service_spec.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::GitalyClient::ObjectPoolService do
+ let(:pool_repository) { create(:pool_repository) }
+ let(:project) { create(:project, :repository) }
+ let(:raw_repository) { project.repository.raw }
+ let(:object_pool) { pool_repository.object_pool }
+
+ subject { described_class.new(object_pool) }
+
+ before do
+ subject.create(raw_repository)
+ end
+
+ describe '#create' do
+ it 'exists on disk' do
+ expect(object_pool.repository.exists?).to be(true)
+ end
+
+ context 'when the pool already exists' do
+ it 'returns an error' do
+ expect do
+ subject.create(raw_repository)
+ end.to raise_error(GRPC::FailedPrecondition)
+ end
+ end
+ end
+
+ describe '#delete' do
+ it 'removes the repository from disk' do
+ subject.delete
+
+ expect(object_pool.repository.exists?).to be(false)
+ end
+
+ context 'when called twice' do
+ it "doesn't raise an error" do
+ subject.delete
+
+ expect { object_pool.delete }.not_to raise_error
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/gpg/commit_spec.rb b/spec/lib/gitlab/gpg/commit_spec.rb
index 8c6d673391b..8229f0eb794 100644
--- a/spec/lib/gitlab/gpg/commit_spec.rb
+++ b/spec/lib/gitlab/gpg/commit_spec.rb
@@ -26,6 +26,28 @@ describe Gitlab::Gpg::Commit do
end
end
+ context 'invalid signature' do
+ let!(:commit) { create :commit, project: project, sha: commit_sha, committer_email: GpgHelpers::User1.emails.first }
+
+ let!(:user) { create(:user, email: GpgHelpers::User1.emails.first) }
+
+ before do
+ allow(Gitlab::Git::Commit).to receive(:extract_signature_lazily)
+ .with(Gitlab::Git::Repository, commit_sha)
+ .and_return(
+ [
+ # Corrupt the key
+ GpgHelpers::User1.signed_commit_signature.tr('=', 'a'),
+ GpgHelpers::User1.signed_commit_base_data
+ ]
+ )
+ end
+
+ it 'returns nil' do
+ expect(described_class.new(commit).signature).to be_nil
+ end
+ end
+
context 'known key' do
context 'user matches the key uid' do
context 'user email matches the email committer' do
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index 7df129da95a..bae5b21c26f 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -287,6 +287,7 @@ project:
- statistics
- container_repositories
- uploads
+- file_uploads
- import_state
- members_and_requesters
- build_trace_section_names
diff --git a/spec/lib/gitlab/json_logger_spec.rb b/spec/lib/gitlab/json_logger_spec.rb
index 0a62785f880..cff7dd58c8c 100644
--- a/spec/lib/gitlab/json_logger_spec.rb
+++ b/spec/lib/gitlab/json_logger_spec.rb
@@ -7,6 +7,10 @@ describe Gitlab::JsonLogger do
let(:now) { Time.now }
describe '#format_message' do
+ before do
+ allow(Gitlab::CorrelationId).to receive(:current_id).and_return('new-correlation-id')
+ end
+
it 'formats strings' do
output = subject.format_message('INFO', now, 'test', 'Hello world')
data = JSON.parse(output)
@@ -14,6 +18,7 @@ describe Gitlab::JsonLogger do
expect(data['severity']).to eq('INFO')
expect(data['time']).to eq(now.utc.iso8601(3))
expect(data['message']).to eq('Hello world')
+ expect(data['correlation_id']).to eq('new-correlation-id')
end
it 'formats hashes' do
@@ -24,6 +29,7 @@ describe Gitlab::JsonLogger do
expect(data['time']).to eq(now.utc.iso8601(3))
expect(data['hello']).to eq(1)
expect(data['message']).to be_nil
+ expect(data['correlation_id']).to eq('new-correlation-id')
end
end
end
diff --git a/spec/lib/gitlab/project_search_results_spec.rb b/spec/lib/gitlab/project_search_results_spec.rb
index 4a0dc3686ec..6831274d37c 100644
--- a/spec/lib/gitlab/project_search_results_spec.rb
+++ b/spec/lib/gitlab/project_search_results_spec.rb
@@ -54,11 +54,18 @@ describe Gitlab::ProjectSearchResults do
end
it 'finds by name' do
- expect(results.map(&:first)).to include(expected_file_by_name)
+ expect(results.map(&:filename)).to include(expected_file_by_name)
+ end
+
+ it "loads all blobs for filename matches in single batch" do
+ expect(Gitlab::Git::Blob).to receive(:batch).once.and_call_original
+
+ expected = project.repository.search_files_by_name(query, 'master')
+ expect(results.map(&:filename)).to include(*expected)
end
it 'finds by content' do
- blob = results.select { |result| result.first == expected_file_by_content }.flatten.last
+ blob = results.select { |result| result.filename == expected_file_by_content }.flatten.last
expect(blob.filename).to eq(expected_file_by_content)
end
@@ -122,126 +129,6 @@ describe Gitlab::ProjectSearchResults do
let(:blob_type) { 'blobs' }
let(:entity) { project }
end
-
- describe 'parsing results' do
- let(:results) { project.repository.search_files_by_content('feature', 'master') }
- let(:search_result) { results.first }
-
- subject { described_class.parse_search_result(search_result) }
-
- it "returns a valid FoundBlob" do
- is_expected.to be_an Gitlab::SearchResults::FoundBlob
- expect(subject.id).to be_nil
- expect(subject.path).to eq('CHANGELOG')
- expect(subject.filename).to eq('CHANGELOG')
- expect(subject.basename).to eq('CHANGELOG')
- expect(subject.ref).to eq('master')
- expect(subject.startline).to eq(188)
- expect(subject.data.lines[2]).to eq(" - Feature: Replace teams with group membership\n")
- end
-
- context 'when the matching filename contains a colon' do
- let(:search_result) { "master:testdata/project::function1.yaml\x001\x00---\n" }
-
- it 'returns a valid FoundBlob' do
- expect(subject.filename).to eq('testdata/project::function1.yaml')
- expect(subject.basename).to eq('testdata/project::function1')
- expect(subject.ref).to eq('master')
- expect(subject.startline).to eq(1)
- expect(subject.data).to eq("---\n")
- end
- end
-
- context 'when the matching content contains a number surrounded by colons' do
- let(:search_result) { "master:testdata/foo.txt\x001\x00blah:9:blah" }
-
- it 'returns a valid FoundBlob' do
- expect(subject.filename).to eq('testdata/foo.txt')
- expect(subject.basename).to eq('testdata/foo')
- expect(subject.ref).to eq('master')
- expect(subject.startline).to eq(1)
- expect(subject.data).to eq('blah:9:blah')
- end
- end
-
- context 'when the matching content contains multiple null bytes' do
- let(:search_result) { "master:testdata/foo.txt\x001\x00blah\x001\x00foo" }
-
- it 'returns a valid FoundBlob' do
- expect(subject.filename).to eq('testdata/foo.txt')
- expect(subject.basename).to eq('testdata/foo')
- expect(subject.ref).to eq('master')
- expect(subject.startline).to eq(1)
- expect(subject.data).to eq("blah\x001\x00foo")
- end
- end
-
- context 'when the search result ends with an empty line' do
- let(:results) { project.repository.search_files_by_content('Role models', 'master') }
-
- it 'returns a valid FoundBlob that ends with an empty line' do
- expect(subject.filename).to eq('files/markdown/ruby-style-guide.md')
- expect(subject.basename).to eq('files/markdown/ruby-style-guide')
- expect(subject.ref).to eq('master')
- expect(subject.startline).to eq(1)
- expect(subject.data).to eq("# Prelude\n\n> Role models are important. <br/>\n> -- Officer Alex J. Murphy / RoboCop\n\n")
- end
- end
-
- context 'when the search returns non-ASCII data' do
- context 'with UTF-8' do
- let(:results) { project.repository.search_files_by_content('файл', 'master') }
-
- it 'returns results as UTF-8' do
- expect(subject.filename).to eq('encoding/russian.rb')
- expect(subject.basename).to eq('encoding/russian')
- expect(subject.ref).to eq('master')
- expect(subject.startline).to eq(1)
- expect(subject.data).to eq("Хороший файл\n")
- end
- end
-
- context 'with UTF-8 in the filename' do
- let(:results) { project.repository.search_files_by_content('webhook', 'master') }
-
- it 'returns results as UTF-8' do
- expect(subject.filename).to eq('encoding/テスト.txt')
- expect(subject.basename).to eq('encoding/テスト')
- expect(subject.ref).to eq('master')
- expect(subject.startline).to eq(3)
- expect(subject.data).to include('WebHookの確認')
- end
- end
-
- context 'with ISO-8859-1' do
- let(:search_result) { "master:encoding/iso8859.txt\x001\x00\xC4\xFC\nmaster:encoding/iso8859.txt\x002\x00\nmaster:encoding/iso8859.txt\x003\x00foo\n".force_encoding(Encoding::ASCII_8BIT) }
-
- it 'returns results as UTF-8' do
- expect(subject.filename).to eq('encoding/iso8859.txt')
- expect(subject.basename).to eq('encoding/iso8859')
- expect(subject.ref).to eq('master')
- expect(subject.startline).to eq(1)
- expect(subject.data).to eq("Äü\n\nfoo\n")
- end
- end
- end
-
- context "when filename has extension" do
- let(:search_result) { "master:CONTRIBUTE.md\x005\x00- [Contribute to GitLab](#contribute-to-gitlab)\n" }
-
- it { expect(subject.path).to eq('CONTRIBUTE.md') }
- it { expect(subject.filename).to eq('CONTRIBUTE.md') }
- it { expect(subject.basename).to eq('CONTRIBUTE') }
- end
-
- context "when file under directory" do
- let(:search_result) { "master:a/b/c.md\x005\x00a b c\n" }
-
- it { expect(subject.path).to eq('a/b/c.md') }
- it { expect(subject.filename).to eq('a/b/c.md') }
- it { expect(subject.basename).to eq('a/b/c') }
- end
- end
end
describe 'wiki search' do
diff --git a/spec/lib/gitlab/search/found_blob_spec.rb b/spec/lib/gitlab/search/found_blob_spec.rb
new file mode 100644
index 00000000000..74157e5c67c
--- /dev/null
+++ b/spec/lib/gitlab/search/found_blob_spec.rb
@@ -0,0 +1,138 @@
+# coding: utf-8
+
+require 'spec_helper'
+
+describe Gitlab::Search::FoundBlob do
+ describe 'parsing results' do
+ let(:project) { create(:project, :public, :repository) }
+ let(:results) { project.repository.search_files_by_content('feature', 'master') }
+ let(:search_result) { results.first }
+
+ subject { described_class.new(content_match: search_result, project: project) }
+
+ it "returns a valid FoundBlob" do
+ is_expected.to be_an described_class
+ expect(subject.id).to be_nil
+ expect(subject.path).to eq('CHANGELOG')
+ expect(subject.filename).to eq('CHANGELOG')
+ expect(subject.basename).to eq('CHANGELOG')
+ expect(subject.ref).to eq('master')
+ expect(subject.startline).to eq(188)
+ expect(subject.data.lines[2]).to eq(" - Feature: Replace teams with group membership\n")
+ end
+
+ it "doesn't parses content if not needed" do
+ expect(subject).not_to receive(:parse_search_result)
+ expect(subject.project_id).to eq(project.id)
+ expect(subject.binary_filename).to eq('CHANGELOG')
+ end
+
+ it "parses content only once when needed" do
+ expect(subject).to receive(:parse_search_result).once.and_call_original
+ expect(subject.filename).to eq('CHANGELOG')
+ expect(subject.startline).to eq(188)
+ end
+
+ context 'when the matching filename contains a colon' do
+ let(:search_result) { "master:testdata/project::function1.yaml\x001\x00---\n" }
+
+ it 'returns a valid FoundBlob' do
+ expect(subject.filename).to eq('testdata/project::function1.yaml')
+ expect(subject.basename).to eq('testdata/project::function1')
+ expect(subject.ref).to eq('master')
+ expect(subject.startline).to eq(1)
+ expect(subject.data).to eq("---\n")
+ end
+ end
+
+ context 'when the matching content contains a number surrounded by colons' do
+ let(:search_result) { "master:testdata/foo.txt\x001\x00blah:9:blah" }
+
+ it 'returns a valid FoundBlob' do
+ expect(subject.filename).to eq('testdata/foo.txt')
+ expect(subject.basename).to eq('testdata/foo')
+ expect(subject.ref).to eq('master')
+ expect(subject.startline).to eq(1)
+ expect(subject.data).to eq('blah:9:blah')
+ end
+ end
+
+ context 'when the matching content contains multiple null bytes' do
+ let(:search_result) { "master:testdata/foo.txt\x001\x00blah\x001\x00foo" }
+
+ it 'returns a valid FoundBlob' do
+ expect(subject.filename).to eq('testdata/foo.txt')
+ expect(subject.basename).to eq('testdata/foo')
+ expect(subject.ref).to eq('master')
+ expect(subject.startline).to eq(1)
+ expect(subject.data).to eq("blah\x001\x00foo")
+ end
+ end
+
+ context 'when the search result ends with an empty line' do
+ let(:results) { project.repository.search_files_by_content('Role models', 'master') }
+
+ it 'returns a valid FoundBlob that ends with an empty line' do
+ expect(subject.filename).to eq('files/markdown/ruby-style-guide.md')
+ expect(subject.basename).to eq('files/markdown/ruby-style-guide')
+ expect(subject.ref).to eq('master')
+ expect(subject.startline).to eq(1)
+ expect(subject.data).to eq("# Prelude\n\n> Role models are important. <br/>\n> -- Officer Alex J. Murphy / RoboCop\n\n")
+ end
+ end
+
+ context 'when the search returns non-ASCII data' do
+ context 'with UTF-8' do
+ let(:results) { project.repository.search_files_by_content('файл', 'master') }
+
+ it 'returns results as UTF-8' do
+ expect(subject.filename).to eq('encoding/russian.rb')
+ expect(subject.basename).to eq('encoding/russian')
+ expect(subject.ref).to eq('master')
+ expect(subject.startline).to eq(1)
+ expect(subject.data).to eq("Хороший файл\n")
+ end
+ end
+
+ context 'with UTF-8 in the filename' do
+ let(:results) { project.repository.search_files_by_content('webhook', 'master') }
+
+ it 'returns results as UTF-8' do
+ expect(subject.filename).to eq('encoding/テスト.txt')
+ expect(subject.basename).to eq('encoding/テスト')
+ expect(subject.ref).to eq('master')
+ expect(subject.startline).to eq(3)
+ expect(subject.data).to include('WebHookの確認')
+ end
+ end
+
+ context 'with ISO-8859-1' do
+ let(:search_result) { "master:encoding/iso8859.txt\x001\x00\xC4\xFC\nmaster:encoding/iso8859.txt\x002\x00\nmaster:encoding/iso8859.txt\x003\x00foo\n".force_encoding(Encoding::ASCII_8BIT) }
+
+ it 'returns results as UTF-8' do
+ expect(subject.filename).to eq('encoding/iso8859.txt')
+ expect(subject.basename).to eq('encoding/iso8859')
+ expect(subject.ref).to eq('master')
+ expect(subject.startline).to eq(1)
+ expect(subject.data).to eq("Äü\n\nfoo\n")
+ end
+ end
+ end
+
+ context "when filename has extension" do
+ let(:search_result) { "master:CONTRIBUTE.md\x005\x00- [Contribute to GitLab](#contribute-to-gitlab)\n" }
+
+ it { expect(subject.path).to eq('CONTRIBUTE.md') }
+ it { expect(subject.filename).to eq('CONTRIBUTE.md') }
+ it { expect(subject.basename).to eq('CONTRIBUTE') }
+ end
+
+ context "when file under directory" do
+ let(:search_result) { "master:a/b/c.md\x005\x00a b c\n" }
+
+ it { expect(subject.path).to eq('a/b/c.md') }
+ it { expect(subject.filename).to eq('a/b/c.md') }
+ it { expect(subject.basename).to eq('a/b/c') }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/sentry_spec.rb b/spec/lib/gitlab/sentry_spec.rb
index d3b41b27b80..1128eaf8560 100644
--- a/spec/lib/gitlab/sentry_spec.rb
+++ b/spec/lib/gitlab/sentry_spec.rb
@@ -19,14 +19,15 @@ describe Gitlab::Sentry do
end
it 'raises the exception if it should' do
- expect(described_class).to receive(:should_raise?).and_return(true)
+ expect(described_class).to receive(:should_raise_for_dev?).and_return(true)
expect { described_class.track_exception(exception) }
.to raise_error(RuntimeError)
end
context 'when exceptions should not be raised' do
before do
- allow(described_class).to receive(:should_raise?).and_return(false)
+ allow(described_class).to receive(:should_raise_for_dev?).and_return(false)
+ allow(Gitlab::CorrelationId).to receive(:current_id).and_return('cid')
end
it 'logs the exception with all attributes passed' do
@@ -35,8 +36,14 @@ describe Gitlab::Sentry do
issue_url: 'http://gitlab.com/gitlab-org/gitlab-ce/issues/1'
}
+ expected_tags = {
+ correlation_id: 'cid'
+ }
+
expect(Raven).to receive(:capture_exception)
- .with(exception, extra: a_hash_including(expected_extras))
+ .with(exception,
+ tags: a_hash_including(expected_tags),
+ extra: a_hash_including(expected_extras))
described_class.track_exception(
exception,
@@ -58,6 +65,7 @@ describe Gitlab::Sentry do
before do
allow(described_class).to receive(:enabled?).and_return(true)
+ allow(Gitlab::CorrelationId).to receive(:current_id).and_return('cid')
end
it 'calls Raven.capture_exception' do
@@ -66,8 +74,14 @@ describe Gitlab::Sentry do
issue_url: 'http://gitlab.com/gitlab-org/gitlab-ce/issues/1'
}
+ expected_tags = {
+ correlation_id: 'cid'
+ }
+
expect(Raven).to receive(:capture_exception)
- .with(exception, extra: a_hash_including(expected_extras))
+ .with(exception,
+ tags: a_hash_including(expected_tags),
+ extra: a_hash_including(expected_extras))
described_class.track_acceptable_exception(
exception,
diff --git a/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb b/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb
index 2421b1e5a1a..f773f370ee2 100644
--- a/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb
+++ b/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb
@@ -12,7 +12,8 @@ describe Gitlab::SidekiqLogging::StructuredLogger do
"queue_namespace" => "cronjob",
"jid" => "da883554ee4fe414012f5f42",
"created_at" => timestamp.to_f,
- "enqueued_at" => timestamp.to_f
+ "enqueued_at" => timestamp.to_f,
+ "correlation_id" => 'cid'
}
end
let(:logger) { double() }
diff --git a/spec/lib/gitlab/sidekiq_middleware/correlation_injector_spec.rb b/spec/lib/gitlab/sidekiq_middleware/correlation_injector_spec.rb
new file mode 100644
index 00000000000..a138ad7c910
--- /dev/null
+++ b/spec/lib/gitlab/sidekiq_middleware/correlation_injector_spec.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::SidekiqMiddleware::CorrelationInjector do
+ class TestWorker
+ include ApplicationWorker
+ end
+
+ before do |example|
+ Sidekiq.client_middleware do |chain|
+ chain.add described_class
+ end
+ end
+
+ after do |example|
+ Sidekiq.client_middleware do |chain|
+ chain.remove described_class
+ end
+
+ Sidekiq::Queues.clear_all
+ end
+
+ around do |example|
+ Sidekiq::Testing.fake! do
+ example.run
+ end
+ end
+
+ it 'injects into payload the correlation id' do
+ expect_any_instance_of(described_class).to receive(:call).and_call_original
+
+ Gitlab::CorrelationId.use_id('new-correlation-id') do
+ TestWorker.perform_async(1234)
+ end
+
+ expected_job_params = {
+ "class" => "TestWorker",
+ "args" => [1234],
+ "correlation_id" => "new-correlation-id"
+ }
+
+ expect(Sidekiq::Queues.jobs_by_worker).to a_hash_including(
+ "TestWorker" => a_collection_containing_exactly(
+ a_hash_including(expected_job_params)))
+ end
+end
diff --git a/spec/lib/gitlab/sidekiq_middleware/correlation_logger_spec.rb b/spec/lib/gitlab/sidekiq_middleware/correlation_logger_spec.rb
new file mode 100644
index 00000000000..94ae4ffa184
--- /dev/null
+++ b/spec/lib/gitlab/sidekiq_middleware/correlation_logger_spec.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::SidekiqMiddleware::CorrelationLogger do
+ class TestWorker
+ include ApplicationWorker
+ end
+
+ before do |example|
+ Sidekiq::Testing.server_middleware do |chain|
+ chain.add described_class
+ end
+ end
+
+ after do |example|
+ Sidekiq::Testing.server_middleware do |chain|
+ chain.remove described_class
+ end
+ end
+
+ it 'injects into payload the correlation id' do
+ expect_any_instance_of(described_class).to receive(:call).and_call_original
+
+ expect_any_instance_of(TestWorker).to receive(:perform).with(1234) do
+ expect(Gitlab::CorrelationId.current_id).to eq('new-correlation-id')
+ end
+
+ Sidekiq::Client.push(
+ 'queue' => 'test',
+ 'class' => TestWorker,
+ 'args' => [1234],
+ 'correlation_id' => 'new-correlation-id')
+ end
+end
diff --git a/spec/lib/gitlab/template/finders/global_template_finder_spec.rb b/spec/lib/gitlab/template/finders/global_template_finder_spec.rb
new file mode 100644
index 00000000000..c7f58fbd2a5
--- /dev/null
+++ b/spec/lib/gitlab/template/finders/global_template_finder_spec.rb
@@ -0,0 +1,35 @@
+require 'spec_helper'
+
+describe Gitlab::Template::Finders::GlobalTemplateFinder do
+ let(:base_dir) { Dir.mktmpdir }
+
+ def create_template!(name_with_category)
+ full_path = File.join(base_dir, name_with_category)
+ FileUtils.mkdir_p(File.dirname(full_path))
+ FileUtils.touch(full_path)
+ end
+
+ after do
+ FileUtils.rm_rf(base_dir)
+ end
+
+ subject(:finder) { described_class.new(base_dir, '', 'Foo' => '', 'Bar' => 'bar') }
+
+ describe '.find' do
+ it 'finds a template in the Foo category' do
+ create_template!('test-template')
+
+ expect(finder.find('test-template')).to be_present
+ end
+
+ it 'finds a template in the Bar category' do
+ create_template!('bar/test-template')
+
+ expect(finder.find('test-template')).to be_present
+ end
+
+ it 'does not permit path traversal requests' do
+ expect { finder.find('../foo') }.to raise_error(/Invalid path/)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/template/finders/repo_template_finders_spec.rb b/spec/lib/gitlab/template/finders/repo_template_finders_spec.rb
index 2eabccd5dff..e329d55d837 100644
--- a/spec/lib/gitlab/template/finders/repo_template_finders_spec.rb
+++ b/spec/lib/gitlab/template/finders/repo_template_finders_spec.rb
@@ -25,6 +25,10 @@ describe Gitlab::Template::Finders::RepoTemplateFinder do
expect(result).to eq('files/html/500.html')
end
+
+ it 'does not permit path traversal requests' do
+ expect { finder.find('../foo') }.to raise_error(/Invalid path/)
+ end
end
describe '#list_files_for' do
diff --git a/spec/lib/gitlab/url_blocker_spec.rb b/spec/lib/gitlab/url_blocker_spec.rb
index 39e0a17a307..62970bd8cb6 100644
--- a/spec/lib/gitlab/url_blocker_spec.rb
+++ b/spec/lib/gitlab/url_blocker_spec.rb
@@ -249,6 +249,27 @@ describe Gitlab::UrlBlocker do
end
end
end
+
+ context 'when ascii_only is true' do
+ it 'returns true for unicode domain' do
+ expect(described_class.blocked_url?('https://𝕘itⅼαƄ.com/foo/foo.bar', ascii_only: true)).to be true
+ end
+
+ it 'returns true for unicode tld' do
+ expect(described_class.blocked_url?('https://gitlab.ᴄοm/foo/foo.bar', ascii_only: true)).to be true
+ end
+
+ it 'returns true for unicode path' do
+ expect(described_class.blocked_url?('https://gitlab.com/𝒇οο/𝒇οο.Ƅαꮁ', ascii_only: true)).to be true
+ end
+
+ it 'returns true for IDNA deviations' do
+ expect(described_class.blocked_url?('https://mißile.com/foo/foo.bar', ascii_only: true)).to be true
+ expect(described_class.blocked_url?('https://miςςile.com/foo/foo.bar', ascii_only: true)).to be true
+ expect(described_class.blocked_url?('https://git‍lab.com/foo/foo.bar', ascii_only: true)).to be true
+ expect(described_class.blocked_url?('https://git‌lab.com/foo/foo.bar', ascii_only: true)).to be true
+ end
+ end
end
describe '#validate_hostname!' do
diff --git a/spec/lib/gitlab/url_sanitizer_spec.rb b/spec/lib/gitlab/url_sanitizer_spec.rb
index b41a81a8167..6e98a999766 100644
--- a/spec/lib/gitlab/url_sanitizer_spec.rb
+++ b/spec/lib/gitlab/url_sanitizer_spec.rb
@@ -41,6 +41,7 @@ describe Gitlab::UrlSanitizer do
false | '123://invalid:url'
false | 'valid@project:url.git'
false | 'valid:pass@project:url.git'
+ false | %w(test array)
true | 'ssh://example.com'
true | 'ssh://:@example.com'
true | 'ssh://foo@example.com'
diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb
index e2de612ff46..deb19fe1a4b 100644
--- a/spec/lib/gitlab/usage_data_spec.rb
+++ b/spec/lib/gitlab/usage_data_spec.rb
@@ -213,4 +213,29 @@ describe Gitlab::UsageData do
expect(described_class.count(relation, fallback: 15)).to eq(15)
end
end
+
+ describe '#approximate_counts' do
+ it 'gets approximate counts for selected models' do
+ create(:label)
+
+ expect(Gitlab::Database::Count).to receive(:approximate_counts)
+ .with(described_class::APPROXIMATE_COUNT_MODELS).once.and_call_original
+
+ counts = described_class.approximate_counts.values
+
+ expect(counts.count).to eq(described_class::APPROXIMATE_COUNT_MODELS.count)
+ expect(counts.any? { |count| count < 0 }).to be_falsey
+ end
+
+ it 'returns default values if counts can not be retrieved' do
+ described_class::APPROXIMATE_COUNT_MODELS.map do |model|
+ model.name.underscore.pluralize.to_sym
+ end
+
+ expect(Gitlab::Database::Count).to receive(:approximate_counts)
+ .and_return({})
+
+ expect(described_class.approximate_counts.values.uniq).to eq([-1])
+ end
+ end
end
diff --git a/spec/lib/gitlab/utils_spec.rb b/spec/lib/gitlab/utils_spec.rb
index 3579ed9a759..f5a4b7e2ebf 100644
--- a/spec/lib/gitlab/utils_spec.rb
+++ b/spec/lib/gitlab/utils_spec.rb
@@ -2,7 +2,33 @@ require 'spec_helper'
describe Gitlab::Utils do
delegate :to_boolean, :boolean_to_yes_no, :slugify, :random_string, :which, :ensure_array_from_string,
- :bytes_to_megabytes, :append_path, to: :described_class
+ :bytes_to_megabytes, :append_path, :check_path_traversal!, to: :described_class
+
+ describe '.check_path_traversal!' do
+ it 'detects path traversal at the start of the string' do
+ expect { check_path_traversal!('../foo') }.to raise_error(/Invalid path/)
+ end
+
+ it 'detects path traversal at the start of the string, even to just the subdirectory' do
+ expect { check_path_traversal!('../') }.to raise_error(/Invalid path/)
+ end
+
+ it 'detects path traversal in the middle of the string' do
+ expect { check_path_traversal!('foo/../../bar') }.to raise_error(/Invalid path/)
+ end
+
+ it 'detects path traversal at the end of the string when slash-terminates' do
+ expect { check_path_traversal!('foo/../') }.to raise_error(/Invalid path/)
+ end
+
+ it 'detects path traversal at the end of the string' do
+ expect { check_path_traversal!('foo/..') }.to raise_error(/Invalid path/)
+ end
+
+ it 'does nothing for a safe string' do
+ expect(check_path_traversal!('./foo')).to eq('./foo')
+ end
+ end
describe '.slugify' do
{
@@ -18,6 +44,12 @@ describe Gitlab::Utils do
end
end
+ describe '.nlbr' do
+ it 'replaces new lines with <br>' do
+ expect(described_class.nlbr("<b>hello</b>\n<i>world</i>".freeze)).to eq("hello<br>world")
+ end
+ end
+
describe '.remove_line_breaks' do
using RSpec::Parameterized::TableSyntax
diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb
index b3f55a2e1bd..7213eee5675 100644
--- a/spec/lib/gitlab/workhorse_spec.rb
+++ b/spec/lib/gitlab/workhorse_spec.rb
@@ -246,7 +246,6 @@ describe Gitlab::Workhorse do
GL_ID: "user-#{user.id}",
GL_USERNAME: user.username,
GL_REPOSITORY: "project-#{project.id}",
- RepoPath: repo_path,
ShowAllRefs: false
}
end
@@ -261,7 +260,6 @@ describe Gitlab::Workhorse do
GL_ID: "user-#{user.id}",
GL_USERNAME: user.username,
GL_REPOSITORY: "wiki-#{project.id}",
- RepoPath: repo_path,
ShowAllRefs: false
}
end
diff --git a/spec/lib/omni_auth/strategies/jwt_spec.rb b/spec/lib/omni_auth/strategies/jwt_spec.rb
index 88d6d0b559a..c2e2db27362 100644
--- a/spec/lib/omni_auth/strategies/jwt_spec.rb
+++ b/spec/lib/omni_auth/strategies/jwt_spec.rb
@@ -4,12 +4,10 @@ describe OmniAuth::Strategies::Jwt do
include Rack::Test::Methods
include DeviseHelpers
- context '.decoded' do
- let(:strategy) { described_class.new({}) }
+ context '#decoded' do
+ subject { described_class.new({}) }
let(:timestamp) { Time.now.to_i }
let(:jwt_config) { Devise.omniauth_configs[:jwt] }
- let(:key) { JWT.encode(claims, jwt_config.strategy.secret) }
-
let(:claims) do
{
id: 123,
@@ -18,19 +16,55 @@ describe OmniAuth::Strategies::Jwt do
iat: timestamp
}
end
+ let(:algorithm) { 'HS256' }
+ let(:secret) { jwt_config.strategy.secret }
+ let(:private_key) { secret }
+ let(:payload) { JWT.encode(claims, private_key, algorithm) }
before do
- allow_any_instance_of(OmniAuth::Strategy).to receive(:options).and_return(jwt_config.strategy)
- allow_any_instance_of(Rack::Request).to receive(:params).and_return({ 'jwt' => key })
+ subject.options[:secret] = secret
+ subject.options[:algorithm] = algorithm
+
+ expect_next_instance_of(Rack::Request) do |rack_request|
+ expect(rack_request).to receive(:params).and_return('jwt' => payload)
+ end
end
- it 'decodes the user information' do
- result = strategy.decoded
+ ECDSA_NAMED_CURVES = {
+ 'ES256' => 'prime256v1',
+ 'ES384' => 'secp384r1',
+ 'ES512' => 'secp521r1'
+ }.freeze
- expect(result["id"]).to eq(123)
- expect(result["name"]).to eq("user_example")
- expect(result["email"]).to eq("user@example.com")
- expect(result["iat"]).to eq(timestamp)
+ {
+ OpenSSL::PKey::RSA => %w[RS256 RS384 RS512],
+ OpenSSL::PKey::EC => %w[ES256 ES384 ES512],
+ String => %w[HS256 HS384 HS512]
+ }.each do |private_key_class, algorithms|
+ algorithms.each do |algorithm|
+ context "when the #{algorithm} algorithm is used" do
+ let(:algorithm) { algorithm }
+ let(:secret) do
+ if private_key_class == OpenSSL::PKey::RSA
+ private_key_class.generate(2048)
+ .to_pem
+ elsif private_key_class == OpenSSL::PKey::EC
+ private_key_class.new(ECDSA_NAMED_CURVES[algorithm])
+ .tap { |key| key.generate_key! }
+ .to_pem
+ else
+ private_key_class.new(jwt_config.strategy.secret)
+ end
+ end
+ let(:private_key) { private_key_class ? private_key_class.new(secret) : secret }
+
+ it 'decodes the user information' do
+ result = subject.decoded
+
+ expect(result).to eq(claims.stringify_keys)
+ end
+ end
+ end
end
context 'required claims is missing' do
@@ -43,7 +77,7 @@ describe OmniAuth::Strategies::Jwt do
end
it 'raises error' do
- expect { strategy.decoded }.to raise_error(OmniAuth::Strategies::Jwt::ClaimInvalid)
+ expect { subject.decoded }.to raise_error(OmniAuth::Strategies::Jwt::ClaimInvalid)
end
end
@@ -57,11 +91,12 @@ describe OmniAuth::Strategies::Jwt do
end
before do
- jwt_config.strategy.valid_within = Time.now.to_i
+ # Omniauth config values are always strings!
+ subject.options[:valid_within] = 2.days.to_s
end
it 'raises error' do
- expect { strategy.decoded }.to raise_error(OmniAuth::Strategies::Jwt::ClaimInvalid)
+ expect { subject.decoded }.to raise_error(OmniAuth::Strategies::Jwt::ClaimInvalid)
end
end
@@ -76,11 +111,12 @@ describe OmniAuth::Strategies::Jwt do
end
before do
- jwt_config.strategy.valid_within = 2.seconds
+ # Omniauth config values are always strings!
+ subject.options[:valid_within] = 2.seconds.to_s
end
it 'raises error' do
- expect { strategy.decoded }.to raise_error(OmniAuth::Strategies::Jwt::ClaimInvalid)
+ expect { subject.decoded }.to raise_error(OmniAuth::Strategies::Jwt::ClaimInvalid)
end
end
end
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index 150c00e4bfe..f6e5c9d33ac 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -4,6 +4,7 @@ require 'email_spec'
describe Notify do
include EmailSpec::Helpers
include EmailSpec::Matchers
+ include EmailHelpers
include RepoHelpers
include_context 'gitlab email notification'
@@ -27,15 +28,6 @@ describe Notify do
description: 'My awesome description!')
end
- def have_referable_subject(referable, reply: false)
- prefix = referable.project ? "#{referable.project.name} | " : ''
- prefix.prepend('Re: ') if reply
-
- suffix = "#{referable.title} (#{referable.to_reference})"
-
- have_subject [prefix, suffix].compact.join
- end
-
context 'for a project' do
shared_examples 'an assignee email' do
it 'is sent to the assignee as the author' do
diff --git a/spec/migrations/populate_mr_metrics_with_events_data_spec.rb b/spec/migrations/populate_mr_metrics_with_events_data_spec.rb
new file mode 100644
index 00000000000..291a52b904d
--- /dev/null
+++ b/spec/migrations/populate_mr_metrics_with_events_data_spec.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20181204154019_populate_mr_metrics_with_events_data.rb')
+
+describe PopulateMrMetricsWithEventsData, :migration, :sidekiq do
+ let(:namespaces) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+ let(:namespace) { namespaces.create(name: 'gitlab', path: 'gitlab-org') }
+ let(:project) { projects.create(namespace_id: namespace.id, name: 'foo') }
+ let(:merge_requests) { table(:merge_requests) }
+
+ def create_merge_request(id)
+ params = {
+ id: id,
+ target_project_id: project.id,
+ target_branch: 'master',
+ source_project_id: project.id,
+ source_branch: 'mr name',
+ title: "mr name#{id}"
+ }
+
+ merge_requests.create!(params)
+ end
+
+ it 'correctly schedules background migrations' do
+ create_merge_request(1)
+ create_merge_request(2)
+ create_merge_request(3)
+
+ stub_const("#{described_class.name}::BATCH_SIZE", 2)
+
+ Sidekiq::Testing.fake! do
+ Timecop.freeze do
+ migrate!
+
+ expect(described_class::MIGRATION)
+ .to be_scheduled_delayed_migration(8.minutes, 1, 2)
+
+ expect(described_class::MIGRATION)
+ .to be_scheduled_delayed_migration(16.minutes, 3, 3)
+
+ expect(BackgroundMigrationWorker.jobs.size).to eq(2)
+ end
+ end
+ end
+end
diff --git a/spec/models/appearance_spec.rb b/spec/models/appearance_spec.rb
index 77b07cf1ac9..35415030154 100644
--- a/spec/models/appearance_spec.rb
+++ b/spec/models/appearance_spec.rb
@@ -20,7 +20,7 @@ describe Appearance do
end
context 'with uploads' do
- it_behaves_like 'model with mounted uploader', false do
+ it_behaves_like 'model with uploads', false do
let(:model_object) { create(:appearance, :with_logo) }
let(:upload_attribute) { :logo }
let(:uploader_class) { AttachmentUploader }
diff --git a/spec/models/broadcast_message_spec.rb b/spec/models/broadcast_message_spec.rb
index 5326f9cb8c0..d6e5b557870 100644
--- a/spec/models/broadcast_message_spec.rb
+++ b/spec/models/broadcast_message_spec.rb
@@ -58,6 +58,12 @@ describe BroadcastMessage do
end
end
+ it 'does not create new records' do
+ create(:broadcast_message)
+
+ expect { described_class.current }.not_to change { described_class.count }
+ end
+
it 'includes messages that need to be displayed in the future' do
create(:broadcast_message)
@@ -77,9 +83,37 @@ describe BroadcastMessage do
it 'does not clear the cache if only a future message should be displayed' do
create(:broadcast_message, :future)
- expect(Rails.cache).not_to receive(:delete)
+ expect(Rails.cache).not_to receive(:delete).with(described_class::CACHE_KEY)
expect(described_class.current.length).to eq(0)
end
+
+ it 'clears the legacy cache key' do
+ create(:broadcast_message, :future)
+
+ expect(Rails.cache).to receive(:delete).with(described_class::LEGACY_CACHE_KEY)
+ expect(described_class.current.length).to eq(0)
+ end
+
+ it 'gracefully handles bad cache entry' do
+ allow(described_class).to receive(:current_and_future_messages).and_return('{')
+
+ expect(described_class.current).to be_empty
+ end
+
+ it 'gracefully handles an empty hash' do
+ allow(described_class).to receive(:current_and_future_messages).and_return('{}')
+
+ expect(described_class.current).to be_empty
+ end
+
+ it 'gracefully handles unknown attributes' do
+ message = create(:broadcast_message)
+
+ allow(described_class).to receive(:current_and_future_messages)
+ .and_return([{ bad_attr: 1 }, message])
+
+ expect(described_class.current).to eq([message])
+ end
end
describe '#active?' do
@@ -143,6 +177,7 @@ describe BroadcastMessage do
message = create(:broadcast_message)
expect(Rails.cache).to receive(:delete).with(described_class::CACHE_KEY)
+ expect(Rails.cache).to receive(:delete).with(described_class::LEGACY_CACHE_KEY)
message.flush_redis_cache
end
diff --git a/spec/models/ci/bridge_spec.rb b/spec/models/ci/bridge_spec.rb
new file mode 100644
index 00000000000..741cdfef1a5
--- /dev/null
+++ b/spec/models/ci/bridge_spec.rb
@@ -0,0 +1,25 @@
+require 'spec_helper'
+
+describe Ci::Bridge do
+ set(:project) { create(:project) }
+ set(:pipeline) { create(:ci_pipeline, project: project) }
+
+ let(:bridge) do
+ create(:ci_bridge, pipeline: pipeline)
+ end
+
+ describe '#tags' do
+ it 'only has a bridge tag' do
+ expect(bridge.tags).to eq [:bridge]
+ end
+ end
+
+ describe '#detailed_status' do
+ let(:user) { create(:user) }
+ let(:status) { bridge.detailed_status(user) }
+
+ it 'returns detailed status object' do
+ expect(status).to be_a Gitlab::Ci::Status::Success
+ end
+ end
+end
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 4cdcae5f670..89f78f629d4 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -1925,7 +1925,7 @@ describe Ci::Build do
context 'when token is empty' do
before do
- build.token = nil
+ build.update_columns(token: nil, token_encrypted: nil)
end
it { is_expected.to be_nil}
@@ -2141,7 +2141,7 @@ describe Ci::Build do
end
before do
- build.token = 'my-token'
+ build.set_token('my-token')
build.yaml_variables = []
end
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index ba9540c84d4..b67c6a4cffa 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -350,6 +350,50 @@ describe Ci::Pipeline, :mailer do
CI_COMMIT_TITLE
CI_COMMIT_DESCRIPTION]
end
+
+ context 'when source is merge request' do
+ let(:pipeline) do
+ create(:ci_pipeline, source: :merge_request, merge_request: merge_request)
+ end
+
+ let(:merge_request) do
+ create(:merge_request,
+ source_project: project,
+ source_branch: 'feature',
+ target_project: project,
+ target_branch: 'master')
+ end
+
+ it 'exposes merge request pipeline variables' do
+ expect(subject.to_hash)
+ .to include(
+ 'CI_MERGE_REQUEST_ID' => merge_request.id.to_s,
+ 'CI_MERGE_REQUEST_IID' => merge_request.iid.to_s,
+ 'CI_MERGE_REQUEST_REF_PATH' => merge_request.ref_path.to_s,
+ 'CI_MERGE_REQUEST_PROJECT_ID' => merge_request.project.id.to_s,
+ 'CI_MERGE_REQUEST_PROJECT_PATH' => merge_request.project.full_path,
+ 'CI_MERGE_REQUEST_PROJECT_URL' => merge_request.project.web_url,
+ 'CI_MERGE_REQUEST_TARGET_BRANCH_NAME' => merge_request.target_branch.to_s,
+ 'CI_MERGE_REQUEST_SOURCE_PROJECT_ID' => merge_request.source_project.id.to_s,
+ 'CI_MERGE_REQUEST_SOURCE_PROJECT_PATH' => merge_request.source_project.full_path,
+ 'CI_MERGE_REQUEST_SOURCE_PROJECT_URL' => merge_request.source_project.web_url,
+ 'CI_MERGE_REQUEST_SOURCE_BRANCH_NAME' => merge_request.source_branch.to_s)
+ end
+
+ context 'when source project does not exist' do
+ before do
+ merge_request.update_column(:source_project_id, nil)
+ end
+
+ it 'does not expose source project related variables' do
+ expect(subject.to_hash.keys).not_to include(
+ %w[CI_MERGE_REQUEST_SOURCE_PROJECT_ID
+ CI_MERGE_REQUEST_SOURCE_PROJECT_PATH
+ CI_MERGE_REQUEST_SOURCE_PROJECT_URL
+ CI_MERGE_REQUEST_SOURCE_BRANCH_NAME])
+ end
+ end
+ end
end
describe '#protected_ref?' do
diff --git a/spec/models/clusters/applications/knative_spec.rb b/spec/models/clusters/applications/knative_spec.rb
index d43d88c2924..a1579b90436 100644
--- a/spec/models/clusters/applications/knative_spec.rb
+++ b/spec/models/clusters/applications/knative_spec.rb
@@ -1,6 +1,9 @@
require 'rails_helper'
describe Clusters::Applications::Knative do
+ include KubernetesHelpers
+ include ReactiveCachingHelpers
+
let(:knative) { create(:clusters_applications_knative) }
include_examples 'cluster application core specs', :clusters_applications_knative
@@ -121,4 +124,43 @@ describe Clusters::Applications::Knative do
describe 'validations' do
it { is_expected.to validate_presence_of(:hostname) }
end
+
+ describe '#services' do
+ let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+ let(:service) { cluster.platform_kubernetes }
+ let(:knative) { create(:clusters_applications_knative, cluster: cluster) }
+
+ let(:namespace) do
+ create(:cluster_kubernetes_namespace,
+ cluster: cluster,
+ cluster_project: cluster.cluster_project,
+ project: cluster.cluster_project.project)
+ end
+
+ subject { knative.services }
+
+ before do
+ stub_kubeclient_discover(service.api_url)
+ stub_kubeclient_knative_services
+ end
+
+ it 'should have an unintialized cache' do
+ is_expected.to be_nil
+ end
+
+ context 'when using synchronous reactive cache' do
+ before do
+ stub_reactive_cache(knative, services: kube_response(kube_knative_services_body))
+ synchronous_reactive_cache(knative)
+ end
+
+ it 'should have cached services' do
+ is_expected.not_to be_nil
+ end
+
+ it 'should match our namespace' do
+ expect(knative.services_for(ns: namespace)).not_to be_nil
+ end
+ end
+ end
end
diff --git a/spec/models/clusters/applications/runner_spec.rb b/spec/models/clusters/applications/runner_spec.rb
index 97e50809647..47daa79873e 100644
--- a/spec/models/clusters/applications/runner_spec.rb
+++ b/spec/models/clusters/applications/runner_spec.rb
@@ -18,7 +18,7 @@ describe Clusters::Applications::Runner do
let(:application) { create(:clusters_applications_runner, :scheduled, version: '0.1.30') }
it 'updates the application version' do
- expect(application.reload.version).to eq('0.1.38')
+ expect(application.reload.version).to eq('0.1.39')
end
end
end
@@ -46,7 +46,7 @@ describe Clusters::Applications::Runner do
it 'should be initialized with 4 arguments' do
expect(subject.name).to eq('runner')
expect(subject.chart).to eq('runner/gitlab-runner')
- expect(subject.version).to eq('0.1.38')
+ expect(subject.version).to eq('0.1.39')
expect(subject).not_to be_rbac
expect(subject.repository).to eq('https://charts.gitlab.io')
expect(subject.files).to eq(gitlab_runner.files)
@@ -64,7 +64,7 @@ describe Clusters::Applications::Runner do
let(:gitlab_runner) { create(:clusters_applications_runner, :errored, runner: ci_runner, version: '0.1.13') }
it 'should be initialized with the locked version' do
- expect(subject.version).to eq('0.1.38')
+ expect(subject.version).to eq('0.1.39')
end
end
end
diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb
index 2a0039a0635..a2d2d77746d 100644
--- a/spec/models/commit_spec.rb
+++ b/spec/models/commit_spec.rb
@@ -204,7 +204,7 @@ describe Commit do
message = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales id felis id blandit. Vivamus egestas lacinia lacus, sed rutrum mauris.'
allow(commit).to receive(:safe_message).and_return(message)
- expect(commit.title).to eq('Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales id felis…')
+ expect(commit.title).to eq('Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales id...')
end
it "truncates a message with a newline before 80 characters at the newline" do
diff --git a/spec/models/concerns/discussion_on_diff_spec.rb b/spec/models/concerns/discussion_on_diff_spec.rb
index 8cd129dc851..73eb7a1160d 100644
--- a/spec/models/concerns/discussion_on_diff_spec.rb
+++ b/spec/models/concerns/discussion_on_diff_spec.rb
@@ -12,6 +12,34 @@ describe DiscussionOnDiff do
expect(truncated_lines.count).to be <= DiffDiscussion::NUMBER_OF_TRUNCATED_DIFF_LINES
end
+
+ context 'with truncated diff lines diff limit set' do
+ let(:truncated_lines) do
+ subject.truncated_diff_lines(
+ diff_limit: diff_limit
+ )
+ end
+
+ context 'when diff limit is higher than default' do
+ let(:diff_limit) { DiffDiscussion::NUMBER_OF_TRUNCATED_DIFF_LINES + 1 }
+
+ it 'returns fewer lines than the default' do
+ expect(subject.diff_lines.count).to be > diff_limit
+
+ expect(truncated_lines.count).to be <= DiffDiscussion::NUMBER_OF_TRUNCATED_DIFF_LINES
+ end
+ end
+
+ context 'when diff_limit is lower than default' do
+ let(:diff_limit) { 3 }
+
+ it 'returns fewer lines than the default' do
+ expect(subject.diff_lines.count).to be > DiffDiscussion::NUMBER_OF_TRUNCATED_DIFF_LINES
+
+ expect(truncated_lines.count).to be <= diff_limit
+ end
+ end
+ end
end
context "when some diff lines are meta" do
diff --git a/spec/models/concerns/token_authenticatable_spec.rb b/spec/models/concerns/token_authenticatable_spec.rb
index 0cdf430e9ab..55d83bc3a6b 100644
--- a/spec/models/concerns/token_authenticatable_spec.rb
+++ b/spec/models/concerns/token_authenticatable_spec.rb
@@ -351,3 +351,89 @@ describe PersonalAccessToken, 'TokenAuthenticatable' do
end
end
end
+
+describe Ci::Build, 'TokenAuthenticatable' do
+ let(:token_field) { :token }
+ let(:build) { FactoryBot.build(:ci_build) }
+
+ it_behaves_like 'TokenAuthenticatable'
+
+ describe 'generating new token' do
+ context 'token is not generated yet' do
+ describe 'token field accessor' do
+ it 'makes it possible to access token' do
+ expect(build.token).to be_nil
+
+ build.save!
+
+ expect(build.token).to be_present
+ end
+ end
+
+ describe "ensure_token" do
+ subject { build.ensure_token }
+
+ it { is_expected.to be_a String }
+ it { is_expected.not_to be_blank }
+
+ it 'does not persist token' do
+ expect(build).not_to be_persisted
+ end
+ end
+
+ describe 'ensure_token!' do
+ it 'persists a new token' do
+ expect(build.ensure_token!).to eq build.reload.token
+ expect(build).to be_persisted
+ end
+
+ it 'persists new token as an encrypted string' do
+ build.ensure_token!
+
+ encrypted = Gitlab::CryptoHelper.aes256_gcm_encrypt(build.token)
+
+ expect(build.read_attribute('token_encrypted')).to eq encrypted
+ end
+
+ it 'does not persist a token in a clear text' do
+ build.ensure_token!
+
+ expect(build.read_attribute('token')).to be_nil
+ end
+ end
+ end
+
+ describe '#reset_token!' do
+ it 'persists a new token' do
+ build.save!
+
+ build.token.yield_self do |previous_token|
+ build.reset_token!
+
+ expect(build.token).not_to eq previous_token
+ expect(build.token).to be_a String
+ end
+ end
+ end
+ end
+
+ describe 'setting a new token' do
+ subject { build.set_token('0123456789') }
+
+ it 'returns the token' do
+ expect(subject).to eq '0123456789'
+ end
+
+ it 'writes a new encrypted token' do
+ expect(build.read_attribute('token_encrypted')).to be_nil
+ expect(subject).to eq '0123456789'
+ expect(build.read_attribute('token_encrypted')).to be_present
+ end
+
+ it 'does not write a new cleartext token' do
+ expect(build.read_attribute('token')).to be_nil
+ expect(subject).to eq '0123456789'
+ expect(build.read_attribute('token')).to be_nil
+ end
+ end
+end
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index 0c3a49cd0f2..e63881242f6 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -76,7 +76,7 @@ describe Group do
before do
group.add_developer(user)
- sub_group.add_developer(user)
+ sub_group.add_maintainer(user)
end
it 'also gets notification settings from parent groups' do
@@ -498,7 +498,7 @@ describe Group do
it 'returns member users on every nest level without duplication' do
group.add_developer(user_a)
nested_group.add_developer(user_b)
- deep_nested_group.add_developer(user_a)
+ deep_nested_group.add_maintainer(user_a)
expect(group.users_with_descendants).to contain_exactly(user_a, user_b)
expect(nested_group.users_with_descendants).to contain_exactly(user_a, user_b)
@@ -739,7 +739,7 @@ describe Group do
end
context 'with uploads' do
- it_behaves_like 'model with mounted uploader', true do
+ it_behaves_like 'model with uploads', true do
let(:model_object) { create(:group, :with_avatar) }
let(:upload_attribute) { :avatar }
let(:uploader_class) { AttachmentUploader }
diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb
index fca1b1f90d9..188beac1582 100644
--- a/spec/models/member_spec.rb
+++ b/spec/models/member_spec.rb
@@ -53,6 +53,29 @@ describe Member do
expect(member).to be_valid
end
end
+
+ context "when a child member inherits its access level" do
+ let(:user) { create(:user) }
+ let(:member) { create(:group_member, :developer, user: user) }
+ let(:child_group) { create(:group, parent: member.group) }
+ let(:child_member) { build(:group_member, group: child_group, user: user) }
+
+ it "requires a higher level" do
+ child_member.access_level = GroupMember::REPORTER
+
+ child_member.validate
+
+ expect(child_member).not_to be_valid
+ end
+
+ it "is valid with a higher level" do
+ child_member.access_level = GroupMember::MAINTAINER
+
+ child_member.validate
+
+ expect(child_member).to be_valid
+ end
+ end
end
describe 'Scopes & finders' do
diff --git a/spec/models/members/group_member_spec.rb b/spec/models/members/group_member_spec.rb
index 97959ed4304..a3451c67bd8 100644
--- a/spec/models/members/group_member_spec.rb
+++ b/spec/models/members/group_member_spec.rb
@@ -50,4 +50,26 @@ describe GroupMember do
group_member.destroy
end
end
+
+ context 'access levels', :nested_groups do
+ context 'with parent group' do
+ it_behaves_like 'inherited access level as a member of entity' do
+ let(:entity) { create(:group, parent: parent_entity) }
+ end
+ end
+
+ context 'with parent group and a sub subgroup' do
+ it_behaves_like 'inherited access level as a member of entity' do
+ let(:subgroup) { create(:group, parent: parent_entity) }
+ let(:entity) { create(:group, parent: subgroup) }
+ end
+
+ context 'when only the subgroup has the member' do
+ it_behaves_like 'inherited access level as a member of entity' do
+ let(:parent_entity) { create(:group, parent: create(:group)) }
+ let(:entity) { create(:group, parent: parent_entity) }
+ end
+ end
+ end
+ end
end
diff --git a/spec/models/members/project_member_spec.rb b/spec/models/members/project_member_spec.rb
index 334d4f95f53..99d3ab41b97 100644
--- a/spec/models/members/project_member_spec.rb
+++ b/spec/models/members/project_member_spec.rb
@@ -11,10 +11,6 @@ describe ProjectMember do
it { is_expected.to validate_inclusion_of(:access_level).in_array(Gitlab::Access.values) }
end
- describe 'modules' do
- it { is_expected.to include_module(Gitlab::ShellAdapter) }
- end
-
describe '.access_level_roles' do
it 'returns Gitlab::Access.options' do
expect(described_class.access_level_roles).to eq(Gitlab::Access.options)
@@ -124,4 +120,19 @@ describe ProjectMember do
end
it_behaves_like 'members notifications', :project
+
+ context 'access levels' do
+ context 'with parent group' do
+ it_behaves_like 'inherited access level as a member of entity' do
+ let(:entity) { create(:project, group: parent_entity) }
+ end
+ end
+
+ context 'with parent group and a subgroup', :nested_groups do
+ it_behaves_like 'inherited access level as a member of entity' do
+ let(:subgroup) { create(:group, parent: parent_entity) }
+ let(:entity) { create(:project, group: subgroup) }
+ end
+ end
+ end
end
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 9b60054e14a..bf4117fbcaf 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -1339,6 +1339,30 @@ describe MergeRequest do
end
end
+ describe '#calculate_reactive_cache' do
+ let(:project) { create(:project, :repository) }
+ let(:merge_request) { create(:merge_request, source_project: project) }
+ subject { merge_request.calculate_reactive_cache(service_class_name) }
+
+ context 'when given an unknown service class name' do
+ let(:service_class_name) { 'Integer' }
+
+ it 'raises a NameError exception' do
+ expect { subject }.to raise_error(NameError, service_class_name)
+ end
+ end
+
+ context 'when given a known service class name' do
+ let(:service_class_name) { 'Ci::CompareTestReportsService' }
+
+ it 'does not raises a NameError exception' do
+ allow_any_instance_of(service_class_name.constantize).to receive(:execute).and_return(nil)
+
+ expect { subject }.not_to raise_error(NameError)
+ end
+ end
+ end
+
describe '#compare_test_reports' do
subject { merge_request.compare_test_reports }
@@ -1885,7 +1909,7 @@ describe MergeRequest do
allow(subject).to receive(:head_pipeline) { nil }
end
- it { expect(subject.mergeable_ci_state?).to be_falsey }
+ it { expect(subject.mergeable_ci_state?).to be_truthy }
end
end
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index 6ee19c0ddf4..18b54cce834 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -249,7 +249,7 @@ describe Namespace do
move_dir_result
end
- expect(Gitlab::Sentry).to receive(:should_raise?).and_return(false) # like prod
+ expect(Gitlab::Sentry).to receive(:should_raise_for_dev?).and_return(false) # like prod
namespace.update(path: namespace.full_path + '_new')
end
@@ -538,7 +538,7 @@ describe Namespace do
it 'returns member users on every nest level without duplication' do
group.add_developer(user_a)
nested_group.add_developer(user_b)
- deep_nested_group.add_developer(user_a)
+ deep_nested_group.add_maintainer(user_a)
expect(group.users_with_descendants).to contain_exactly(user_a, user_b)
expect(nested_group.users_with_descendants).to contain_exactly(user_a, user_b)
diff --git a/spec/models/pool_repository_spec.rb b/spec/models/pool_repository_spec.rb
index 541e78507e5..3d3878b8c39 100644
--- a/spec/models/pool_repository_spec.rb
+++ b/spec/models/pool_repository_spec.rb
@@ -5,6 +5,7 @@ require 'spec_helper'
describe PoolRepository do
describe 'associations' do
it { is_expected.to belong_to(:shard) }
+ it { is_expected.to have_one(:source_project) }
it { is_expected.to have_many(:member_projects) }
end
@@ -12,15 +13,14 @@ describe PoolRepository do
let!(:pool_repository) { create(:pool_repository) }
it { is_expected.to validate_presence_of(:shard) }
+ it { is_expected.to validate_presence_of(:source_project) }
end
describe '#disk_path' do
it 'sets the hashed disk_path' do
pool = create(:pool_repository)
- elements = File.split(pool.disk_path)
-
- expect(elements).to all( match(/\d{2,}/) )
+ expect(pool.disk_path).to match(%r{\A@pools/\h{2}/\h{2}/\h{64}})
end
end
end
diff --git a/spec/models/project_import_data_spec.rb b/spec/models/project_import_data_spec.rb
new file mode 100644
index 00000000000..e9910c0a5d1
--- /dev/null
+++ b/spec/models/project_import_data_spec.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ProjectImportData do
+ describe '#merge_data' do
+ it 'writes the Hash to the attribute if it is nil' do
+ row = described_class.new
+
+ row.merge_data('number' => 10)
+
+ expect(row.data).to eq({ 'number' => 10 })
+ end
+
+ it 'merges the Hash into an existing Hash if one was present' do
+ row = described_class.new(data: { 'number' => 10 })
+
+ row.merge_data('foo' => 'bar')
+
+ expect(row.data).to eq({ 'number' => 10, 'foo' => 'bar' })
+ end
+ end
+
+ describe '#merge_credentials' do
+ it 'writes the Hash to the attribute if it is nil' do
+ row = described_class.new
+
+ row.merge_credentials('number' => 10)
+
+ expect(row.credentials).to eq({ 'number' => 10 })
+ end
+
+ it 'merges the Hash into an existing Hash if one was present' do
+ row = described_class.new
+
+ row.credentials = { 'number' => 10 }
+ row.merge_credentials('foo' => 'bar')
+
+ expect(row.credentials).to eq({ 'number' => 10, 'foo' => 'bar' })
+ end
+ end
+end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 50920d9d1fc..5e63f14b720 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -1897,7 +1897,7 @@ describe Project do
end
end
- describe '#latest_successful_builds_for' do
+ describe '#latest_successful_builds_for and #latest_successful_build_for' do
def create_pipeline(status = 'success')
create(:ci_pipeline, project: project,
sha: project.commit.sha,
@@ -1919,14 +1919,16 @@ describe Project do
it 'gives the latest builds from latest pipeline' do
pipeline1 = create_pipeline
pipeline2 = create_pipeline
- build1_p2 = create_build(pipeline2, 'test')
create_build(pipeline1, 'test')
create_build(pipeline1, 'test2')
+ build1_p2 = create_build(pipeline2, 'test')
build2_p2 = create_build(pipeline2, 'test2')
latest_builds = project.latest_successful_builds_for
+ single_build = project.latest_successful_build_for(build1_p2.name)
expect(latest_builds).to contain_exactly(build2_p2, build1_p2)
+ expect(single_build).to eq(build1_p2)
end
end
@@ -1936,16 +1938,22 @@ describe Project do
context 'standalone pipeline' do
it 'returns builds for ref for default_branch' do
builds = project.latest_successful_builds_for
+ single_build = project.latest_successful_build_for(build.name)
expect(builds).to contain_exactly(build)
+ expect(single_build).to eq(build)
end
- it 'returns empty relation if the build cannot be found' do
+ it 'returns empty relation if the build cannot be found for #latest_successful_builds_for' do
builds = project.latest_successful_builds_for('TAIL')
expect(builds).to be_kind_of(ActiveRecord::Relation)
expect(builds).to be_empty
end
+
+ it 'returns exception if the build cannot be found for #latest_successful_build_for' do
+ expect { project.latest_successful_build_for(build.name, 'TAIL') }.to raise_error(ActiveRecord::RecordNotFound)
+ end
end
context 'with some pending pipeline' do
@@ -1954,9 +1962,11 @@ describe Project do
end
it 'gives the latest build from latest pipeline' do
- latest_build = project.latest_successful_builds_for
+ latest_builds = project.latest_successful_builds_for
+ last_single_build = project.latest_successful_build_for(build.name)
- expect(latest_build).to contain_exactly(build)
+ expect(latest_builds).to contain_exactly(build)
+ expect(last_single_build).to eq(build)
end
end
end
@@ -3898,7 +3908,7 @@ describe Project do
end
context 'with uploads' do
- it_behaves_like 'model with mounted uploader', true do
+ it_behaves_like 'model with uploads', true do
let(:model_object) { create(:project, :with_avatar) }
let(:upload_attribute) { :avatar }
let(:uploader_class) { AttachmentUploader }
@@ -4092,6 +4102,67 @@ describe Project do
end
end
+ describe '#object_pool_params' do
+ let(:project) { create(:project, :repository, :public) }
+
+ subject { project.object_pool_params }
+
+ before do
+ stub_application_setting(hashed_storage_enabled: true)
+ end
+
+ context 'when the objects cannot be pooled' do
+ let(:project) { create(:project, :repository, :private) }
+
+ it { is_expected.to be_empty }
+ end
+
+ context 'when a pool is created' do
+ it 'returns that pool repository' do
+ expect(subject).not_to be_empty
+ expect(subject[:pool_repository]).to be_persisted
+ end
+ end
+ end
+
+ describe '#git_objects_poolable?' do
+ subject { project }
+
+ context 'when the feature flag is turned off' do
+ before do
+ stub_feature_flags(object_pools: false)
+ end
+
+ let(:project) { create(:project, :repository, :public) }
+
+ it { is_expected.not_to be_git_objects_poolable }
+ end
+
+ context 'when the feature flag is enabled' do
+ context 'when not using hashed storage' do
+ let(:project) { create(:project, :legacy_storage, :public, :repository) }
+
+ it { is_expected.not_to be_git_objects_poolable }
+ end
+
+ context 'when the project is not public' do
+ let(:project) { create(:project, :private) }
+
+ it { is_expected.not_to be_git_objects_poolable }
+ end
+
+ context 'when objects are poolable' do
+ let(:project) { create(:project, :repository, :public) }
+
+ before do
+ stub_application_setting(hashed_storage_enabled: true)
+ end
+
+ it { is_expected.to be_git_objects_poolable }
+ end
+ end
+ end
+
def rugged_config
rugged_repo(project.repository).config
end
diff --git a/spec/models/remote_mirror_spec.rb b/spec/models/remote_mirror_spec.rb
index b12ca79847c..5d3c25062d5 100644
--- a/spec/models/remote_mirror_spec.rb
+++ b/spec/models/remote_mirror_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-describe RemoteMirror do
+describe RemoteMirror, :mailer do
include GitHelpers
describe 'URL validation' do
@@ -137,6 +137,43 @@ describe RemoteMirror do
end
end
+ describe '#mark_as_failed' do
+ let(:remote_mirror) { create(:remote_mirror) }
+ let(:error_message) { 'http://user:pass@test.com/root/repoC.git/' }
+ let(:sanitized_error_message) { 'http://*****:*****@test.com/root/repoC.git/' }
+
+ subject do
+ remote_mirror.update_start
+ remote_mirror.mark_as_failed(error_message)
+ end
+
+ it 'sets the update_status to failed' do
+ subject
+
+ expect(remote_mirror.reload.update_status).to eq('failed')
+ end
+
+ it 'saves the sanitized error' do
+ subject
+
+ expect(remote_mirror.last_error).to eq(sanitized_error_message)
+ end
+
+ context 'notifications' do
+ let(:user) { create(:user) }
+
+ before do
+ remote_mirror.project.add_maintainer(user)
+ end
+
+ it 'notifies the project maintainers' do
+ perform_enqueued_jobs { subject }
+
+ should_email(user)
+ end
+ end
+ end
+
context 'when remote mirror gets destroyed' do
it 'removes remote' do
mirror = create_mirror(url: 'http://foo:bar@test.com')
diff --git a/spec/models/uploads/fog_spec.rb b/spec/models/uploads/fog_spec.rb
new file mode 100644
index 00000000000..4a44cf5ab0f
--- /dev/null
+++ b/spec/models/uploads/fog_spec.rb
@@ -0,0 +1,69 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Uploads::Fog do
+ let(:data_store) { described_class.new }
+
+ before do
+ stub_uploads_object_storage(FileUploader)
+ end
+
+ describe '#available?' do
+ subject { data_store.available? }
+
+ context 'when object storage is enabled' do
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when object storage is disabled' do
+ before do
+ stub_uploads_object_storage(FileUploader, enabled: false)
+ end
+
+ it { is_expected.to be_falsy }
+ end
+ end
+
+ context 'model with uploads' do
+ let(:project) { create(:project) }
+ let(:relation) { project.uploads }
+
+ describe '#keys' do
+ let!(:uploads) { create_list(:upload, 2, :object_storage, uploader: FileUploader, model: project) }
+ subject { data_store.keys(relation) }
+
+ it 'returns keys' do
+ is_expected.to match_array(relation.pluck(:path))
+ end
+ end
+
+ describe '#delete_keys' do
+ let(:keys) { data_store.keys(relation) }
+ let!(:uploads) { create_list(:upload, 2, :with_file, :issuable_upload, model: project) }
+ subject { data_store.delete_keys(keys) }
+
+ before do
+ uploads.each { |upload| upload.build_uploader.migrate!(2) }
+ end
+
+ it 'deletes multiple data' do
+ paths = relation.pluck(:path)
+
+ ::Fog::Storage.new(FileUploader.object_store_credentials).tap do |connection|
+ paths.each do |path|
+ expect(connection.get_object('uploads', path)[:body]).not_to be_nil
+ end
+ end
+
+ subject
+
+ ::Fog::Storage.new(FileUploader.object_store_credentials).tap do |connection|
+ paths.each do |path|
+ expect { connection.get_object('uploads', path)[:body] }.to raise_error(Excon::Error::NotFound)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/models/uploads/local_spec.rb b/spec/models/uploads/local_spec.rb
new file mode 100644
index 00000000000..3468399f370
--- /dev/null
+++ b/spec/models/uploads/local_spec.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Uploads::Local do
+ let(:data_store) { described_class.new }
+
+ before do
+ stub_uploads_object_storage(FileUploader)
+ end
+
+ context 'model with uploads' do
+ let(:project) { create(:project) }
+ let(:relation) { project.uploads }
+
+ describe '#keys' do
+ let!(:uploads) { create_list(:upload, 2, uploader: FileUploader, model: project) }
+ subject { data_store.keys(relation) }
+
+ it 'returns keys' do
+ is_expected.to match_array(relation.map(&:absolute_path))
+ end
+ end
+
+ describe '#delete_keys' do
+ let(:keys) { data_store.keys(relation) }
+ let!(:uploads) { create_list(:upload, 2, :with_file, :issuable_upload, model: project) }
+ subject { data_store.delete_keys(keys) }
+
+ it 'deletes multiple data' do
+ paths = relation.map(&:absolute_path)
+
+ paths.each do |path|
+ expect(File.exist?(path)).to be_truthy
+ end
+
+ subject
+
+ paths.each do |path|
+ expect(File.exist?(path)).to be_falsey
+ end
+ end
+ end
+ end
+end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index e5490e0a156..ff075e65c76 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -2325,11 +2325,11 @@ describe User do
context 'user is member of all groups' do
before do
- group.add_owner(user)
- nested_group_1.add_owner(user)
- nested_group_1_1.add_owner(user)
- nested_group_2.add_owner(user)
- nested_group_2_1.add_owner(user)
+ group.add_reporter(user)
+ nested_group_1.add_developer(user)
+ nested_group_1_1.add_maintainer(user)
+ nested_group_2.add_developer(user)
+ nested_group_2_1.add_maintainer(user)
end
it 'returns all groups' do
@@ -3231,7 +3231,7 @@ describe User do
end
context 'with uploads' do
- it_behaves_like 'model with mounted uploader', false do
+ it_behaves_like 'model with uploads', false do
let(:model_object) { create(:user, :with_avatar) }
let(:upload_attribute) { :avatar }
let(:uploader_class) { AttachmentUploader }
diff --git a/spec/presenters/group_member_presenter_spec.rb b/spec/presenters/group_member_presenter_spec.rb
index c00e41725d9..bb66523a83d 100644
--- a/spec/presenters/group_member_presenter_spec.rb
+++ b/spec/presenters/group_member_presenter_spec.rb
@@ -135,4 +135,12 @@ describe GroupMemberPresenter do
end
end
end
+
+ it_behaves_like '#valid_level_roles', :group do
+ let(:expected_roles) { { 'Developer' => 30, 'Maintainer' => 40, 'Owner' => 50, 'Reporter' => 20 } }
+
+ before do
+ entity.parent = group
+ end
+ end
end
diff --git a/spec/presenters/project_member_presenter_spec.rb b/spec/presenters/project_member_presenter_spec.rb
index 83db5c56cdf..73ef113a1c5 100644
--- a/spec/presenters/project_member_presenter_spec.rb
+++ b/spec/presenters/project_member_presenter_spec.rb
@@ -135,4 +135,10 @@ describe ProjectMemberPresenter do
end
end
end
+
+ it_behaves_like '#valid_level_roles', :project do
+ before do
+ entity.group = group
+ end
+ end
end
diff --git a/spec/presenters/project_presenter_spec.rb b/spec/presenters/project_presenter_spec.rb
index 7b0192fa9c8..456de5f1b9a 100644
--- a/spec/presenters/project_presenter_spec.rb
+++ b/spec/presenters/project_presenter_spec.rb
@@ -165,32 +165,32 @@ describe ProjectPresenter do
describe '#files_anchor_data' do
it 'returns files data' do
- expect(presenter.files_anchor_data).to have_attributes(enabled: true,
- label: 'Files (0 Bytes)',
+ expect(presenter.files_anchor_data).to have_attributes(is_link: true,
+ label: a_string_including('0 Bytes'),
link: nil)
end
end
describe '#commits_anchor_data' do
it 'returns commits data' do
- expect(presenter.commits_anchor_data).to have_attributes(enabled: true,
- label: 'Commits (0)',
+ expect(presenter.commits_anchor_data).to have_attributes(is_link: true,
+ label: a_string_including('0'),
link: nil)
end
end
describe '#branches_anchor_data' do
it 'returns branches data' do
- expect(presenter.branches_anchor_data).to have_attributes(enabled: true,
- label: "Branches (0)",
+ expect(presenter.branches_anchor_data).to have_attributes(is_link: true,
+ label: a_string_including('0'),
link: nil)
end
end
describe '#tags_anchor_data' do
it 'returns tags data' do
- expect(presenter.tags_anchor_data).to have_attributes(enabled: true,
- label: "Tags (0)",
+ expect(presenter.tags_anchor_data).to have_attributes(is_link: true,
+ label: a_string_including('0'),
link: nil)
end
end
@@ -202,32 +202,32 @@ describe ProjectPresenter do
describe '#files_anchor_data' do
it 'returns files data' do
- expect(presenter.files_anchor_data).to have_attributes(enabled: true,
- label: 'Files (0 Bytes)',
+ expect(presenter.files_anchor_data).to have_attributes(is_link: true,
+ label: a_string_including('0 Bytes'),
link: presenter.project_tree_path(project))
end
end
describe '#commits_anchor_data' do
it 'returns commits data' do
- expect(presenter.commits_anchor_data).to have_attributes(enabled: true,
- label: 'Commits (0)',
+ expect(presenter.commits_anchor_data).to have_attributes(is_link: true,
+ label: a_string_including('0'),
link: presenter.project_commits_path(project, project.repository.root_ref))
end
end
describe '#branches_anchor_data' do
it 'returns branches data' do
- expect(presenter.branches_anchor_data).to have_attributes(enabled: true,
- label: "Branches (#{project.repository.branches.size})",
+ expect(presenter.branches_anchor_data).to have_attributes(is_link: true,
+ label: a_string_including("#{project.repository.branches.size}"),
link: presenter.project_branches_path(project))
end
end
describe '#tags_anchor_data' do
it 'returns tags data' do
- expect(presenter.tags_anchor_data).to have_attributes(enabled: true,
- label: "Tags (#{project.repository.tags.size})",
+ expect(presenter.tags_anchor_data).to have_attributes(is_link: true,
+ label: a_string_including("#{project.repository.tags.size}"),
link: presenter.project_tags_path(project))
end
end
@@ -236,8 +236,8 @@ describe ProjectPresenter do
it 'returns new file data if user can push' do
project.add_developer(user)
- expect(presenter.new_file_anchor_data).to have_attributes(enabled: false,
- label: "New file",
+ expect(presenter.new_file_anchor_data).to have_attributes(is_link: false,
+ label: a_string_including("New file"),
link: presenter.project_new_blob_path(project, 'master'),
class_modifier: 'success')
end
@@ -264,8 +264,8 @@ describe ProjectPresenter do
project.add_developer(user)
allow(project.repository).to receive(:readme).and_return(nil)
- expect(presenter.readme_anchor_data).to have_attributes(enabled: false,
- label: 'Add Readme',
+ expect(presenter.readme_anchor_data).to have_attributes(is_link: false,
+ label: a_string_including('Add README'),
link: presenter.add_readme_path)
end
end
@@ -274,21 +274,21 @@ describe ProjectPresenter do
it 'returns anchor data' do
allow(project.repository).to receive(:readme).and_return(double(name: 'readme'))
- expect(presenter.readme_anchor_data).to have_attributes(enabled: true,
- label: 'Readme',
+ expect(presenter.readme_anchor_data).to have_attributes(is_link: false,
+ label: a_string_including('README'),
link: presenter.readme_path)
end
end
end
describe '#changelog_anchor_data' do
- context 'when user can push and CHANGELOG does not exists' do
+ context 'when user can push and CHANGELOG does not exist' do
it 'returns anchor data' do
project.add_developer(user)
allow(project.repository).to receive(:changelog).and_return(nil)
- expect(presenter.changelog_anchor_data).to have_attributes(enabled: false,
- label: 'Add Changelog',
+ expect(presenter.changelog_anchor_data).to have_attributes(is_link: false,
+ label: a_string_including('Add CHANGELOG'),
link: presenter.add_changelog_path)
end
end
@@ -297,21 +297,21 @@ describe ProjectPresenter do
it 'returns anchor data' do
allow(project.repository).to receive(:changelog).and_return(double(name: 'foo'))
- expect(presenter.changelog_anchor_data).to have_attributes(enabled: true,
- label: 'Changelog',
+ expect(presenter.changelog_anchor_data).to have_attributes(is_link: false,
+ label: a_string_including('CHANGELOG'),
link: presenter.changelog_path)
end
end
end
describe '#license_anchor_data' do
- context 'when user can push and LICENSE does not exists' do
+ context 'when user can push and LICENSE does not exist' do
it 'returns anchor data' do
project.add_developer(user)
allow(project.repository).to receive(:license_blob).and_return(nil)
- expect(presenter.license_anchor_data).to have_attributes(enabled: false,
- label: 'Add license',
+ expect(presenter.license_anchor_data).to have_attributes(is_link: true,
+ label: a_string_including('Add license'),
link: presenter.add_license_path)
end
end
@@ -320,21 +320,21 @@ describe ProjectPresenter do
it 'returns anchor data' do
allow(project.repository).to receive(:license_blob).and_return(double(name: 'foo'))
- expect(presenter.license_anchor_data).to have_attributes(enabled: true,
- label: presenter.license_short_name,
+ expect(presenter.license_anchor_data).to have_attributes(is_link: true,
+ label: a_string_including(presenter.license_short_name),
link: presenter.license_path)
end
end
end
describe '#contribution_guide_anchor_data' do
- context 'when user can push and CONTRIBUTING does not exists' do
+ context 'when user can push and CONTRIBUTING does not exist' do
it 'returns anchor data' do
project.add_developer(user)
allow(project.repository).to receive(:contribution_guide).and_return(nil)
- expect(presenter.contribution_guide_anchor_data).to have_attributes(enabled: false,
- label: 'Add Contribution guide',
+ expect(presenter.contribution_guide_anchor_data).to have_attributes(is_link: false,
+ label: a_string_including('Add CONTRIBUTING'),
link: presenter.add_contribution_guide_path)
end
end
@@ -343,8 +343,8 @@ describe ProjectPresenter do
it 'returns anchor data' do
allow(project.repository).to receive(:contribution_guide).and_return(double(name: 'foo'))
- expect(presenter.contribution_guide_anchor_data).to have_attributes(enabled: true,
- label: 'Contribution guide',
+ expect(presenter.contribution_guide_anchor_data).to have_attributes(is_link: false,
+ label: a_string_including('CONTRIBUTING'),
link: presenter.contribution_guide_path)
end
end
@@ -355,20 +355,20 @@ describe ProjectPresenter do
it 'returns anchor data' do
allow(project).to receive(:auto_devops_enabled?).and_return(true)
- expect(presenter.autodevops_anchor_data).to have_attributes(enabled: true,
- label: 'Auto DevOps enabled',
+ expect(presenter.autodevops_anchor_data).to have_attributes(is_link: false,
+ label: a_string_including('Auto DevOps enabled'),
link: nil)
end
end
- context 'when user can admin pipeline and CI yml does not exists' do
+ context 'when user can admin pipeline and CI yml does not exist' do
it 'returns anchor data' do
project.add_maintainer(user)
allow(project).to receive(:auto_devops_enabled?).and_return(false)
allow(project.repository).to receive(:gitlab_ci_yml).and_return(nil)
- expect(presenter.autodevops_anchor_data).to have_attributes(enabled: false,
- label: 'Enable Auto DevOps',
+ expect(presenter.autodevops_anchor_data).to have_attributes(is_link: false,
+ label: a_string_including('Enable Auto DevOps'),
link: presenter.project_settings_ci_cd_path(project, anchor: 'autodevops-settings'))
end
end
@@ -380,8 +380,8 @@ describe ProjectPresenter do
project.add_maintainer(user)
cluster = create(:cluster, projects: [project])
- expect(presenter.kubernetes_cluster_anchor_data).to have_attributes(enabled: true,
- label: 'Kubernetes configured',
+ expect(presenter.kubernetes_cluster_anchor_data).to have_attributes(is_link: false,
+ label: a_string_including('Kubernetes configured'),
link: presenter.project_cluster_path(project, cluster))
end
@@ -390,16 +390,16 @@ describe ProjectPresenter do
create(:cluster, :production_environment, projects: [project])
create(:cluster, projects: [project])
- expect(presenter.kubernetes_cluster_anchor_data).to have_attributes(enabled: true,
- label: 'Kubernetes configured',
+ expect(presenter.kubernetes_cluster_anchor_data).to have_attributes(is_link: false,
+ label: a_string_including('Kubernetes configured'),
link: presenter.project_clusters_path(project))
end
it 'returns link to create a cluster if no cluster exists' do
project.add_maintainer(user)
- expect(presenter.kubernetes_cluster_anchor_data).to have_attributes(enabled: false,
- label: 'Add Kubernetes cluster',
+ expect(presenter.kubernetes_cluster_anchor_data).to have_attributes(is_link: false,
+ label: a_string_including('Add Kubernetes cluster'),
link: presenter.new_project_cluster_path(project))
end
end
diff --git a/spec/requests/api/helpers_spec.rb b/spec/requests/api/helpers_spec.rb
index 2c40e266f5f..f7916441313 100644
--- a/spec/requests/api/helpers_spec.rb
+++ b/spec/requests/api/helpers_spec.rb
@@ -5,7 +5,6 @@ require_relative '../../../config/initializers/sentry'
describe API::Helpers do
include API::APIGuard::HelperMethods
include described_class
- include SentryHelper
include TermsHelper
let(:user) { create(:user) }
@@ -224,8 +223,15 @@ describe API::Helpers do
describe '.handle_api_exception' do
before do
- allow_any_instance_of(self.class).to receive(:sentry_enabled?).and_return(true)
allow_any_instance_of(self.class).to receive(:rack_response)
+ allow(Gitlab::Sentry).to receive(:enabled?).and_return(true)
+
+ stub_application_setting(
+ sentry_enabled: true,
+ sentry_dsn: "dummy://12345:67890@sentry.localdomain/sentry/42"
+ )
+ configure_sentry
+ Raven.client.configuration.encoding = 'json'
end
it 'does not report a MethodNotAllowed exception to Sentry' do
@@ -241,10 +247,13 @@ describe API::Helpers do
exception = RuntimeError.new('test error')
allow(exception).to receive(:backtrace).and_return(caller)
- expect_any_instance_of(self.class).to receive(:sentry_context)
- expect(Raven).to receive(:capture_exception).with(exception, extra: {})
+ expect(Raven).to receive(:capture_exception).with(exception, tags: {
+ correlation_id: 'new-correlation-id'
+ }, extra: {})
- handle_api_exception(exception)
+ Gitlab::CorrelationId.use_id('new-correlation-id') do
+ handle_api_exception(exception)
+ end
end
context 'with a personal access token given' do
@@ -255,7 +264,6 @@ describe API::Helpers do
# We need to stub at a lower level than #sentry_enabled? otherwise
# Sentry is not enabled when the request below is made, and the test
# would pass even without the fix
- expect(Gitlab::Sentry).to receive(:enabled?).twice.and_return(true)
expect(ProjectsFinder).to receive(:new).and_raise('Runtime Error!')
get api('/projects', personal_access_token: token)
@@ -272,17 +280,7 @@ describe API::Helpers do
# Sentry events are an array of the form [auth_header, data, options]
let(:event_data) { Raven.client.transport.events.first[1] }
- before do
- stub_application_setting(
- sentry_enabled: true,
- sentry_dsn: "dummy://12345:67890@sentry.localdomain/sentry/42"
- )
- configure_sentry
- Raven.client.configuration.encoding = 'json'
- end
-
it 'sends the params, excluding confidential values' do
- expect(Gitlab::Sentry).to receive(:enabled?).twice.and_return(true)
expect(ProjectsFinder).to receive(:new).and_raise('Runtime Error!')
get api('/projects', user), password: 'dont_send_this', other_param: 'send_this'
diff --git a/spec/requests/api/jobs_spec.rb b/spec/requests/api/jobs_spec.rb
index 8770365c893..cd4e480ca64 100644
--- a/spec/requests/api/jobs_spec.rb
+++ b/spec/requests/api/jobs_spec.rb
@@ -586,6 +586,136 @@ describe API::Jobs do
end
end
+ describe 'GET id/jobs/artifacts/:ref_name/raw/*artifact_path?job=name' do
+ context 'when job has artifacts' do
+ let(:job) { create(:ci_build, :artifacts, pipeline: pipeline, user: api_user) }
+ let(:artifact) { 'other_artifacts_0.1.2/another-subdirectory/banana_sample.gif' }
+ let(:visibility_level) { Gitlab::VisibilityLevel::PUBLIC }
+ let(:public_builds) { true }
+
+ before do
+ stub_artifacts_object_storage
+ job.success
+
+ project.update(visibility_level: visibility_level,
+ public_builds: public_builds)
+
+ get_artifact_file(artifact)
+ end
+
+ context 'when user is anonymous' do
+ let(:api_user) { nil }
+
+ context 'when project is public' do
+ let(:visibility_level) { Gitlab::VisibilityLevel::PUBLIC }
+ let(:public_builds) { true }
+
+ it 'allows to access artifacts' do
+ expect(response).to have_gitlab_http_status(200)
+ expect(response.headers.to_h)
+ .to include('Content-Type' => 'application/json',
+ 'Gitlab-Workhorse-Send-Data' => /artifacts-entry/)
+ end
+ end
+
+ context 'when project is public with builds access disabled' do
+ let(:visibility_level) { Gitlab::VisibilityLevel::PUBLIC }
+ let(:public_builds) { false }
+
+ it 'rejects access to artifacts' do
+ expect(response).to have_gitlab_http_status(403)
+ expect(json_response).to have_key('message')
+ expect(response.headers.to_h)
+ .not_to include('Gitlab-Workhorse-Send-Data' => /artifacts-entry/)
+ end
+ end
+
+ context 'when project is private' do
+ let(:visibility_level) { Gitlab::VisibilityLevel::PRIVATE }
+ let(:public_builds) { true }
+
+ it 'rejects access and hides existence of artifacts' do
+ expect(response).to have_gitlab_http_status(404)
+ expect(json_response).to have_key('message')
+ expect(response.headers.to_h)
+ .not_to include('Gitlab-Workhorse-Send-Data' => /artifacts-entry/)
+ end
+ end
+ end
+
+ context 'when user is authorized' do
+ let(:visibility_level) { Gitlab::VisibilityLevel::PRIVATE }
+ let(:public_builds) { true }
+
+ it 'returns a specific artifact file for a valid path' do
+ expect(Gitlab::Workhorse)
+ .to receive(:send_artifacts_entry)
+ .and_call_original
+
+ get_artifact_file(artifact)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response.headers.to_h)
+ .to include('Content-Type' => 'application/json',
+ 'Gitlab-Workhorse-Send-Data' => /artifacts-entry/)
+ end
+ end
+
+ context 'with branch name containing slash' do
+ before do
+ pipeline.reload
+ pipeline.update(ref: 'improve/awesome',
+ sha: project.commit('improve/awesome').sha)
+ end
+
+ it 'returns a specific artifact file for a valid path' do
+ get_artifact_file(artifact, 'improve/awesome')
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response.headers.to_h)
+ .to include('Content-Type' => 'application/json',
+ 'Gitlab-Workhorse-Send-Data' => /artifacts-entry/)
+ end
+ end
+
+ context 'non-existing job' do
+ shared_examples 'not found' do
+ it { expect(response).to have_gitlab_http_status(:not_found) }
+ end
+
+ context 'has no such ref' do
+ before do
+ get_artifact_file('some/artifact', 'wrong-ref')
+ end
+
+ it_behaves_like 'not found'
+ end
+
+ context 'has no such job' do
+ before do
+ get_artifact_file('some/artifact', pipeline.ref, 'wrong-job-name')
+ end
+
+ it_behaves_like 'not found'
+ end
+ end
+ end
+
+ context 'when job does not have artifacts' do
+ let(:job) { create(:ci_build, pipeline: pipeline, user: api_user) }
+
+ it 'does not return job artifact file' do
+ get_artifact_file('some/artifact')
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+
+ def get_artifact_file(artifact_path, ref = pipeline.ref, job_name = job.name)
+ get api("/projects/#{project.id}/jobs/artifacts/#{ref}/raw/#{artifact_path}", api_user), job: job_name
+ end
+ end
+
describe 'GET /projects/:id/jobs/:job_id/trace' do
before do
get api("/projects/#{project.id}/jobs/#{job.id}/trace", api_user)
diff --git a/spec/requests/api/members_spec.rb b/spec/requests/api/members_spec.rb
index 93e1c3a2294..bb32d581176 100644
--- a/spec/requests/api/members_spec.rb
+++ b/spec/requests/api/members_spec.rb
@@ -224,6 +224,37 @@ describe API::Members do
end
end
+ context 'access levels' do
+ it 'does not create the member if group level is higher', :nested_groups do
+ parent = create(:group)
+
+ group.update(parent: parent)
+ project.update(group: group)
+ parent.add_developer(stranger)
+
+ post api("/#{source_type.pluralize}/#{source.id}/members", maintainer),
+ user_id: stranger.id, access_level: Member::REPORTER
+
+ expect(response).to have_gitlab_http_status(400)
+ expect(json_response['message']['access_level']).to eq(["should be higher than Developer inherited membership from group #{parent.name}"])
+ end
+
+ it 'creates the member if group level is lower', :nested_groups do
+ parent = create(:group)
+
+ group.update(parent: parent)
+ project.update(group: group)
+ parent.add_developer(stranger)
+
+ post api("/#{source_type.pluralize}/#{source.id}/members", maintainer),
+ user_id: stranger.id, access_level: Member::MAINTAINER
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response['id']).to eq(stranger.id)
+ expect(json_response['access_level']).to eq(Member::MAINTAINER)
+ end
+ end
+
it "returns 409 if member already exists" do
post api("/#{source_type.pluralize}/#{source.id}/members", maintainer),
user_id: maintainer.id, access_level: Member::MAINTAINER
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 62b6a3ce42e..e40db55cd20 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -1906,7 +1906,7 @@ describe API::Projects do
let(:group) { create(:group) }
let(:group2) do
group = create(:group, name: 'group2_name')
- group.add_owner(user2)
+ group.add_maintainer(user2)
group
end
diff --git a/spec/rubocop/cop/migration/add_timestamps_spec.rb b/spec/rubocop/cop/migration/add_timestamps_spec.rb
index 3a41c91add2..fae0177d5f5 100644
--- a/spec/rubocop/cop/migration/add_timestamps_spec.rb
+++ b/spec/rubocop/cop/migration/add_timestamps_spec.rb
@@ -11,7 +11,7 @@ describe RuboCop::Cop::Migration::AddTimestamps do
subject(:cop) { described_class.new }
let(:migration_with_add_timestamps) do
%q(
- class Users < ActiveRecord::Migration
+ class Users < ActiveRecord::Migration[4.2]
DOWNTIME = false
def change
@@ -24,7 +24,7 @@ describe RuboCop::Cop::Migration::AddTimestamps do
let(:migration_without_add_timestamps) do
%q(
- class Users < ActiveRecord::Migration
+ class Users < ActiveRecord::Migration[4.2]
DOWNTIME = false
def change
@@ -36,7 +36,7 @@ describe RuboCop::Cop::Migration::AddTimestamps do
let(:migration_with_add_timestamps_with_timezone) do
%q(
- class Users < ActiveRecord::Migration
+ class Users < ActiveRecord::Migration[4.2]
DOWNTIME = false
def change
diff --git a/spec/rubocop/cop/migration/datetime_spec.rb b/spec/rubocop/cop/migration/datetime_spec.rb
index 9e844325371..f2d9483d8d3 100644
--- a/spec/rubocop/cop/migration/datetime_spec.rb
+++ b/spec/rubocop/cop/migration/datetime_spec.rb
@@ -12,7 +12,7 @@ describe RuboCop::Cop::Migration::Datetime do
let(:migration_with_datetime) do
%q(
- class Users < ActiveRecord::Migration
+ class Users < ActiveRecord::Migration[4.2]
DOWNTIME = false
def change
@@ -25,7 +25,7 @@ describe RuboCop::Cop::Migration::Datetime do
let(:migration_with_timestamp) do
%q(
- class Users < ActiveRecord::Migration
+ class Users < ActiveRecord::Migration[4.2]
DOWNTIME = false
def change
@@ -38,7 +38,7 @@ describe RuboCop::Cop::Migration::Datetime do
let(:migration_without_datetime) do
%q(
- class Users < ActiveRecord::Migration
+ class Users < ActiveRecord::Migration[4.2]
DOWNTIME = false
def change
@@ -50,7 +50,7 @@ describe RuboCop::Cop::Migration::Datetime do
let(:migration_with_datetime_with_timezone) do
%q(
- class Users < ActiveRecord::Migration
+ class Users < ActiveRecord::Migration[4.2]
DOWNTIME = false
def change
diff --git a/spec/rubocop/cop/migration/timestamps_spec.rb b/spec/rubocop/cop/migration/timestamps_spec.rb
index 685bdb21803..1812818692a 100644
--- a/spec/rubocop/cop/migration/timestamps_spec.rb
+++ b/spec/rubocop/cop/migration/timestamps_spec.rb
@@ -11,7 +11,7 @@ describe RuboCop::Cop::Migration::Timestamps do
subject(:cop) { described_class.new }
let(:migration_with_timestamps) do
%q(
- class Users < ActiveRecord::Migration
+ class Users < ActiveRecord::Migration[4.2]
DOWNTIME = false
def change
@@ -27,7 +27,7 @@ describe RuboCop::Cop::Migration::Timestamps do
let(:migration_without_timestamps) do
%q(
- class Users < ActiveRecord::Migration
+ class Users < ActiveRecord::Migration[4.2]
DOWNTIME = false
def change
@@ -42,7 +42,7 @@ describe RuboCop::Cop::Migration::Timestamps do
let(:migration_with_timestamps_with_timezone) do
%q(
- class Users < ActiveRecord::Migration
+ class Users < ActiveRecord::Migration[4.2]
DOWNTIME = false
def change
diff --git a/spec/serializers/diff_file_entity_spec.rb b/spec/serializers/diff_file_entity_spec.rb
index 7497b8f27bd..073c13c2cbb 100644
--- a/spec/serializers/diff_file_entity_spec.rb
+++ b/spec/serializers/diff_file_entity_spec.rb
@@ -13,39 +13,6 @@ describe DiffFileEntity do
subject { entity.as_json }
- shared_examples 'diff file entity' do
- it 'exposes correct attributes' do
- expect(subject).to include(
- :submodule, :submodule_link, :submodule_tree_url, :file_path,
- :deleted_file, :old_path, :new_path, :mode_changed,
- :a_mode, :b_mode, :text, :old_path_html,
- :new_path_html, :highlighted_diff_lines, :parallel_diff_lines,
- :blob, :file_hash, :added_lines, :removed_lines, :diff_refs, :content_sha,
- :stored_externally, :external_storage, :too_large, :collapsed, :new_file,
- :context_lines_path
- )
- end
-
- it 'includes viewer' do
- expect(subject[:viewer].with_indifferent_access)
- .to match_schema('entities/diff_viewer')
- end
-
- # Converted diff files from GitHub import does not contain blob file
- # and content sha.
- context 'when diff file does not have a blob and content sha' do
- it 'exposes some attributes as nil' do
- allow(diff_file).to receive(:content_sha).and_return(nil)
- allow(diff_file).to receive(:blob).and_return(nil)
-
- expect(subject[:context_lines_path]).to be_nil
- expect(subject[:view_path]).to be_nil
- expect(subject[:highlighted_diff_lines]).to be_nil
- expect(subject[:can_modify_blob]).to be_nil
- end
- end
- end
-
context 'when there is no merge request' do
it_behaves_like 'diff file entity'
end
diff --git a/spec/serializers/discussion_diff_file_entity_spec.rb b/spec/serializers/discussion_diff_file_entity_spec.rb
new file mode 100644
index 00000000000..101ac918a98
--- /dev/null
+++ b/spec/serializers/discussion_diff_file_entity_spec.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe DiscussionDiffFileEntity do
+ include RepoHelpers
+
+ let(:project) { create(:project, :repository) }
+ let(:repository) { project.repository }
+ let(:commit) { project.commit(sample_commit.id) }
+ let(:diff_refs) { commit.diff_refs }
+ let(:diff) { commit.raw_diffs.first }
+ let(:diff_file) { Gitlab::Diff::File.new(diff, diff_refs: diff_refs, repository: repository) }
+ let(:entity) { described_class.new(diff_file, request: {}) }
+
+ subject { entity.as_json }
+
+ context 'when there is no merge request' do
+ it_behaves_like 'diff file discussion entity'
+ end
+
+ context 'when there is a merge request' do
+ let(:user) { create(:user) }
+ let(:request) { EntityRequest.new(project: project, current_user: user) }
+ let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
+ let(:entity) { described_class.new(diff_file, request: request, merge_request: merge_request) }
+
+ it_behaves_like 'diff file discussion entity'
+
+ it 'exposes additional attributes' do
+ expect(subject).to include(:edit_path)
+ end
+
+ it 'exposes no diff lines' do
+ expect(subject).not_to include(:highlighted_diff_lines,
+ :parallel_diff_lines)
+ end
+ end
+end
diff --git a/spec/serializers/discussion_entity_spec.rb b/spec/serializers/discussion_entity_spec.rb
index 0590304e832..138749b0fdf 100644
--- a/spec/serializers/discussion_entity_spec.rb
+++ b/spec/serializers/discussion_entity_spec.rb
@@ -74,13 +74,5 @@ describe DiscussionEntity do
:active
)
end
-
- context 'when diff file is a image' do
- it 'exposes image attributes' do
- allow(discussion).to receive(:on_image?).and_return(true)
-
- expect(subject.keys).to include(:image_diff_html)
- end
- end
end
end
diff --git a/spec/serializers/trigger_variable_entity_spec.rb b/spec/serializers/trigger_variable_entity_spec.rb
new file mode 100644
index 00000000000..66567c05f52
--- /dev/null
+++ b/spec/serializers/trigger_variable_entity_spec.rb
@@ -0,0 +1,49 @@
+require 'spec_helper'
+
+describe TriggerVariableEntity do
+ let(:project) { create(:project) }
+ let(:request) { double('request') }
+ let(:user) { create(:user) }
+ let(:variable) { { key: 'TEST_KEY', value: 'TEST_VALUE' } }
+
+ subject { described_class.new(variable, request: request).as_json }
+
+ before do
+ allow(request).to receive(:current_user).and_return(user)
+ allow(request).to receive(:project).and_return(project)
+ end
+
+ it 'exposes the variable key' do
+ expect(subject).to include(:key)
+ end
+
+ context 'when user has access to the value' do
+ context 'when user is maintainer' do
+ before do
+ project.team.add_maintainer(user)
+ end
+
+ it 'exposes the variable value' do
+ expect(subject).to include(:value)
+ end
+ end
+
+ context 'when user is owner' do
+ let(:user) { project.owner }
+
+ it 'exposes the variable value' do
+ expect(subject).to include(:value)
+ end
+ end
+ end
+
+ context 'when user does not have access to the value' do
+ before do
+ project.team.add_developer(user)
+ end
+
+ it 'does not expose the variable value' do
+ expect(subject).not_to include(:value)
+ end
+ end
+end
diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb
index ccc6b0ef1c7..ffa47d527f7 100644
--- a/spec/services/ci/create_pipeline_service_spec.rb
+++ b/spec/services/ci/create_pipeline_service_spec.rb
@@ -810,6 +810,95 @@ describe Ci::CreatePipelineService do
end
end
end
+
+ context "when config uses regular expression for only keyword" do
+ let(:config) do
+ {
+ build: {
+ stage: 'build',
+ script: 'echo',
+ only: ["/^#{ref_name}$/"]
+ }
+ }
+ end
+
+ context 'when merge request is specified' do
+ let(:merge_request) do
+ create(:merge_request,
+ source_project: project,
+ source_branch: ref_name,
+ target_project: project,
+ target_branch: 'master')
+ end
+
+ it 'does not create a merge request pipeline' do
+ expect(pipeline).not_to be_persisted
+
+ expect(pipeline.errors[:base])
+ .to eq(['No stages / jobs for this pipeline.'])
+ end
+ end
+ end
+
+ context "when config uses variables for only keyword" do
+ let(:config) do
+ {
+ build: {
+ stage: 'build',
+ script: 'echo',
+ only: {
+ variables: %w($CI)
+ }
+ }
+ }
+ end
+
+ context 'when merge request is specified' do
+ let(:merge_request) do
+ create(:merge_request,
+ source_project: project,
+ source_branch: ref_name,
+ target_project: project,
+ target_branch: 'master')
+ end
+
+ it 'does not create a merge request pipeline' do
+ expect(pipeline).not_to be_persisted
+
+ expect(pipeline.errors[:base])
+ .to eq(['No stages / jobs for this pipeline.'])
+ end
+ end
+ end
+
+ context "when config has 'except: [tags]'" do
+ let(:config) do
+ {
+ build: {
+ stage: 'build',
+ script: 'echo',
+ except: ['tags']
+ }
+ }
+ end
+
+ context 'when merge request is specified' do
+ let(:merge_request) do
+ create(:merge_request,
+ source_project: project,
+ source_branch: ref_name,
+ target_project: project,
+ target_branch: 'master')
+ end
+
+ it 'does not create a merge request pipeline' do
+ expect(pipeline).not_to be_persisted
+
+ expect(pipeline.errors[:base])
+ .to eq(['No stages / jobs for this pipeline.'])
+ end
+ end
+ end
end
context 'when source is web' do
diff --git a/spec/services/ci/retry_build_service_spec.rb b/spec/services/ci/retry_build_service_spec.rb
index e779675744c..87185891470 100644
--- a/spec/services/ci/retry_build_service_spec.rb
+++ b/spec/services/ci/retry_build_service_spec.rb
@@ -20,9 +20,9 @@ describe Ci::RetryBuildService do
CLONE_ACCESSORS = described_class::CLONE_ACCESSORS
REJECT_ACCESSORS =
- %i[id status user token coverage trace runner artifacts_expire_at
- artifacts_file artifacts_metadata artifacts_size created_at
- updated_at started_at finished_at queued_at erased_by
+ %i[id status user token token_encrypted coverage trace runner
+ artifacts_expire_at artifacts_file artifacts_metadata artifacts_size
+ created_at updated_at started_at finished_at queued_at erased_by
erased_at auto_canceled_by job_artifacts job_artifacts_archive
job_artifacts_metadata job_artifacts_trace job_artifacts_junit
job_artifacts_sast job_artifacts_dependency_scanning
diff --git a/spec/services/clusters/applications/create_service_spec.rb b/spec/services/clusters/applications/create_service_spec.rb
index 0bd7719345e..1a2ca23748a 100644
--- a/spec/services/clusters/applications/create_service_spec.rb
+++ b/spec/services/clusters/applications/create_service_spec.rb
@@ -31,6 +31,31 @@ describe Clusters::Applications::CreateService do
subject
end
+ context 'cert manager application' do
+ let(:params) do
+ {
+ application: 'cert_manager',
+ email: 'test@example.com'
+ }
+ end
+
+ before do
+ allow_any_instance_of(Clusters::Applications::ScheduleInstallationService).to receive(:execute)
+ end
+
+ it 'creates the application' do
+ expect do
+ subject
+
+ cluster.reload
+ end.to change(cluster, :application_cert_manager)
+ end
+
+ it 'sets the email' do
+ expect(subject.email).to eq('test@example.com')
+ end
+ end
+
context 'jupyter application' do
let(:params) do
{
diff --git a/spec/services/clusters/build_service_spec.rb b/spec/services/clusters/build_service_spec.rb
new file mode 100644
index 00000000000..da0cb42b3a1
--- /dev/null
+++ b/spec/services/clusters/build_service_spec.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Clusters::BuildService do
+ describe '#execute' do
+ subject { described_class.new(cluster_subject).execute }
+
+ describe 'when cluster subject is a project' do
+ let(:cluster_subject) { build(:project) }
+
+ it 'sets the cluster_type to project_type' do
+ is_expected.to be_project_type
+ end
+ end
+
+ describe 'when cluster subject is a group' do
+ let(:cluster_subject) { build(:group) }
+
+ it 'sets the cluster_type to group_type' do
+ is_expected.to be_group_type
+ end
+ end
+ end
+end
diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb
index d29a1091d95..1d9c75dedce 100644
--- a/spec/services/merge_requests/refresh_service_spec.rb
+++ b/spec/services/merge_requests/refresh_service_spec.rb
@@ -621,4 +621,77 @@ describe MergeRequests::RefreshService do
@fork_build_failed_todo.reload
end
end
+
+ describe 'updating merge_commit' do
+ let(:service) { described_class.new(project, user) }
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :repository) }
+
+ let(:oldrev) { TestEnv::BRANCH_SHA['merge-commit-analyze-before'] }
+ let(:newrev) { TestEnv::BRANCH_SHA['merge-commit-analyze-after'] } # Pretend branch is now updated
+
+ let!(:merge_request) do
+ create(
+ :merge_request,
+ source_project: project,
+ source_branch: 'merge-commit-analyze-after',
+ target_branch: 'merge-commit-analyze-before',
+ target_project: project,
+ merge_user: user
+ )
+ end
+
+ let!(:merge_request_side_branch) do
+ create(
+ :merge_request,
+ source_project: project,
+ source_branch: 'merge-commit-analyze-side-branch',
+ target_branch: 'merge-commit-analyze-before',
+ target_project: project,
+ merge_user: user
+ )
+ end
+
+ subject { service.execute(oldrev, newrev, 'refs/heads/merge-commit-analyze-before') }
+
+ context 'feature enabled' do
+ before do
+ stub_feature_flags(branch_push_merge_commit_analyze: true)
+ end
+
+ it "updates merge requests' merge_commits" do
+ expect(Gitlab::BranchPushMergeCommitAnalyzer).to receive(:new).and_wrap_original do |original_method, commits|
+ expect(commits.map(&:id)).to eq(%w{646ece5cfed840eca0a4feb21bcd6a81bb19bda3 29284d9bcc350bcae005872d0be6edd016e2efb5 5f82584f0a907f3b30cfce5bb8df371454a90051 8a994512e8c8f0dfcf22bb16df6e876be7a61036 689600b91aabec706e657e38ea706ece1ee8268f db46a1c5a5e474aa169b6cdb7a522d891bc4c5f9})
+
+ original_method.call(commits)
+ end
+
+ subject
+
+ merge_request.reload
+ merge_request_side_branch.reload
+
+ expect(merge_request.merge_commit.id).to eq('646ece5cfed840eca0a4feb21bcd6a81bb19bda3')
+ expect(merge_request_side_branch.merge_commit.id).to eq('29284d9bcc350bcae005872d0be6edd016e2efb5')
+ end
+ end
+
+ context 'when feature is disabled' do
+ before do
+ stub_feature_flags(branch_push_merge_commit_analyze: false)
+ end
+
+ it "does not trigger analysis" do
+ expect(Gitlab::BranchPushMergeCommitAnalyzer).not_to receive(:new)
+
+ subject
+
+ merge_request.reload
+ merge_request_side_branch.reload
+
+ expect(merge_request.merge_commit).to eq(nil)
+ expect(merge_request_side_branch.merge_commit).to eq(nil)
+ end
+ end
+ end
end
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index 2d8da7673dc..68ac3a00ab0 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -2146,6 +2146,60 @@ describe NotificationService, :mailer do
end
end
+ describe 'Repository cleanup' do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+
+ describe '#repository_cleanup_success' do
+ it 'emails the specified user only' do
+ notification.repository_cleanup_success(project, user)
+
+ should_email(user)
+ end
+ end
+
+ describe '#repository_cleanup_failure' do
+ it 'emails the specified user only' do
+ notification.repository_cleanup_failure(project, user, 'Some error')
+
+ should_email(user)
+ end
+ end
+ end
+
+ context 'Remote mirror notifications' do
+ describe '#remote_mirror_update_failed' do
+ let(:project) { create(:project) }
+ let(:remote_mirror) { create(:remote_mirror, project: project) }
+ let(:u_blocked) { create(:user, :blocked) }
+ let(:u_silence) { create_user_with_notification(:disabled, 'silent-maintainer', project) }
+ let(:u_owner) { project.owner }
+ let(:u_maintainer1) { create(:user) }
+ let(:u_maintainer2) { create(:user) }
+ let(:u_developer) { create(:user) }
+
+ before do
+ project.add_maintainer(u_blocked)
+ project.add_maintainer(u_silence)
+ project.add_maintainer(u_maintainer1)
+ project.add_maintainer(u_maintainer2)
+ project.add_developer(u_developer)
+
+ # Mock remote update
+ allow(project.repository).to receive(:async_remove_remote)
+ allow(project.repository).to receive(:add_remote)
+
+ reset_delivered_emails!
+ end
+
+ it 'emails current watching maintainers' do
+ notification.remote_mirror_update_failed(remote_mirror)
+
+ should_only_email(u_maintainer1, u_maintainer2, u_owner)
+ end
+ end
+ end
+
def build_team(project)
@u_watcher = create_global_setting_for(create(:user), :watch)
@u_participating = create_global_setting_for(create(:user), :participating)
diff --git a/spec/services/projects/cleanup_service_spec.rb b/spec/services/projects/cleanup_service_spec.rb
new file mode 100644
index 00000000000..3d4587ce2a1
--- /dev/null
+++ b/spec/services/projects/cleanup_service_spec.rb
@@ -0,0 +1,44 @@
+require 'spec_helper'
+
+describe Projects::CleanupService do
+ let(:project) { create(:project, :repository, bfg_object_map: fixture_file_upload('spec/fixtures/bfg_object_map.txt')) }
+ let(:object_map) { project.bfg_object_map }
+
+ subject(:service) { described_class.new(project) }
+
+ describe '#execute' do
+ it 'runs the apply_bfg_object_map gitaly RPC' do
+ expect_next_instance_of(Gitlab::Git::RepositoryCleaner) do |cleaner|
+ expect(cleaner).to receive(:apply_bfg_object_map).with(kind_of(IO))
+ end
+
+ service.execute
+ end
+
+ it 'runs garbage collection on the repository' do
+ expect_next_instance_of(GitGarbageCollectWorker) do |worker|
+ expect(worker).to receive(:perform)
+ end
+
+ service.execute
+ end
+
+ it 'clears the repository cache' do
+ expect(project.repository).to receive(:expire_all_method_caches)
+
+ service.execute
+ end
+
+ it 'removes the object map file' do
+ service.execute
+
+ expect(object_map.exists?).to be_falsy
+ end
+
+ it 'raises an error if no object map can be found' do
+ object_map.remove!
+
+ expect { service.execute }.to raise_error(described_class::NoUploadError)
+ end
+ end
+end
diff --git a/spec/services/projects/fork_service_spec.rb b/spec/services/projects/fork_service_spec.rb
index a3d24ae312a..26e8d829345 100644
--- a/spec/services/projects/fork_service_spec.rb
+++ b/spec/services/projects/fork_service_spec.rb
@@ -2,7 +2,8 @@ require 'spec_helper'
describe Projects::ForkService do
include ProjectForksHelper
- let(:gitlab_shell) { Gitlab::Shell.new }
+ include Gitlab::ShellAdapter
+
context 'when forking a new project' do
describe 'fork by user' do
before do
@@ -235,6 +236,33 @@ describe Projects::ForkService do
end
end
+ context 'when forking with object pools' do
+ let(:fork_from_project) { create(:project, :public) }
+ let(:forker) { create(:user) }
+
+ before do
+ stub_feature_flags(object_pools: true)
+ end
+
+ context 'when no pool exists' do
+ it 'creates a new object pool' do
+ forked_project = fork_project(fork_from_project, forker)
+
+ expect(forked_project.pool_repository).to eq(fork_from_project.pool_repository)
+ end
+ end
+
+ context 'when a pool already exists' do
+ let!(:pool_repository) { create(:pool_repository, source_project: fork_from_project) }
+
+ it 'joins the object pool' do
+ forked_project = fork_project(fork_from_project, forker)
+
+ expect(forked_project.pool_repository).to eq(fork_from_project.pool_repository)
+ end
+ end
+ end
+
context 'when linking fork to an existing project' do
let(:fork_from_project) { create(:project, :public) }
let(:fork_to_project) { create(:project, :public) }
diff --git a/spec/support/helpers/email_helpers.rb b/spec/support/helpers/email_helpers.rb
index 1fb8252459f..ad6e1064499 100644
--- a/spec/support/helpers/email_helpers.rb
+++ b/spec/support/helpers/email_helpers.rb
@@ -34,4 +34,13 @@ module EmailHelpers
def find_email_for(user)
ActionMailer::Base.deliveries.find { |d| d.to.include?(user.notification_email) }
end
+
+ def have_referable_subject(referable, include_project: true, reply: false)
+ prefix = (include_project && referable.project ? "#{referable.project.name} | " : '').freeze
+ prefix = "Re: #{prefix}" if reply
+
+ suffix = "#{referable.title} (#{referable.to_reference})"
+
+ have_subject [prefix, suffix].compact.join
+ end
end
diff --git a/spec/support/helpers/fake_migration_classes.rb b/spec/support/helpers/fake_migration_classes.rb
index b0fc8422857..c7766df7a52 100644
--- a/spec/support/helpers/fake_migration_classes.rb
+++ b/spec/support/helpers/fake_migration_classes.rb
@@ -1,4 +1,4 @@
-class FakeRenameReservedPathMigrationV1 < ActiveRecord::Migration
+class FakeRenameReservedPathMigrationV1 < ActiveRecord::Migration[4.2]
include Gitlab::Database::RenameReservedPathsMigration::V1
def version
diff --git a/spec/support/helpers/features/sorting_helpers.rb b/spec/support/helpers/features/sorting_helpers.rb
index a1ae428586e..003ecb251fe 100644
--- a/spec/support/helpers/features/sorting_helpers.rb
+++ b/spec/support/helpers/features/sorting_helpers.rb
@@ -13,9 +13,9 @@ module Spec
module Features
module SortingHelpers
def sort_by(value)
- find('.filter-dropdown-container button.dropdown-menu-toggle').click
+ find('.filter-dropdown-container .dropdown').click
- page.within('.content ul.dropdown-menu.dropdown-menu-right li') do
+ page.within('ul.dropdown-menu.dropdown-menu-right li') do
click_link(value)
end
end
diff --git a/spec/support/helpers/javascript_fixtures_helpers.rb b/spec/support/helpers/javascript_fixtures_helpers.rb
index 086a345dca8..89c5ec7a718 100644
--- a/spec/support/helpers/javascript_fixtures_helpers.rb
+++ b/spec/support/helpers/javascript_fixtures_helpers.rb
@@ -6,6 +6,13 @@ module JavaScriptFixturesHelpers
FIXTURE_PATH = 'spec/javascripts/fixtures'.freeze
+ def self.included(base)
+ base.around do |example|
+ # pick an arbitrary date from the past, so tests are not time dependent
+ Timecop.freeze(Time.utc(2015, 7, 3, 10)) { example.run }
+ end
+ end
+
# Public: Removes all fixture files from given directory
#
# directory_name - directory of the fixtures (relative to FIXTURE_PATH)
diff --git a/spec/support/helpers/kubernetes_helpers.rb b/spec/support/helpers/kubernetes_helpers.rb
index bef951e1517..39bd305d88a 100644
--- a/spec/support/helpers/kubernetes_helpers.rb
+++ b/spec/support/helpers/kubernetes_helpers.rb
@@ -34,6 +34,17 @@ module KubernetesHelpers
WebMock.stub_request(:get, deployments_url).to_return(response || kube_deployments_response)
end
+ def stub_kubeclient_knative_services(**options)
+ options[:name] ||= "kubetest"
+ options[:namespace] ||= "default"
+ options[:domain] ||= "example.com"
+
+ stub_kubeclient_discover(service.api_url)
+ knative_url = service.api_url + "/apis/serving.knative.dev/v1alpha1/services"
+
+ WebMock.stub_request(:get, knative_url).to_return(kube_response(kube_knative_services_body(options)))
+ end
+
def stub_kubeclient_get_secret(api_url, **options)
options[:metadata_name] ||= "default-token-1"
options[:namespace] ||= "default"
@@ -181,6 +192,13 @@ module KubernetesHelpers
}
end
+ def kube_knative_services_body(**options)
+ {
+ "kind" => "List",
+ "items" => [kube_service(options)]
+ }
+ end
+
# This is a partial response, it will have many more elements in reality but
# these are the ones we care about at the moment
def kube_pod(name: "kube-pod", app: "valid-pod-label", status: "Running", track: nil)
@@ -224,6 +242,54 @@ module KubernetesHelpers
}
end
+ def kube_service(name: "kubetest", namespace: "default", domain: "example.com")
+ {
+ "metadata" => {
+ "creationTimestamp" => "2018-11-21T06:16:33Z",
+ "name" => name,
+ "namespace" => namespace,
+ "selfLink" => "/apis/serving.knative.dev/v1alpha1/namespaces/#{namespace}/services/#{name}"
+ },
+ "spec" => {
+ "generation" => 2
+ },
+ "status" => {
+ "domain" => "#{name}.#{namespace}.#{domain}",
+ "domainInternal" => "#{name}.#{namespace}.svc.cluster.local",
+ "latestCreatedRevisionName" => "#{name}-00002",
+ "latestReadyRevisionName" => "#{name}-00002",
+ "observedGeneration" => 2
+ }
+ }
+ end
+
+ def kube_service_full(name: "kubetest", namespace: "kube-ns", domain: "example.com")
+ {
+ "metadata" => {
+ "creationTimestamp" => "2018-11-21T06:16:33Z",
+ "name" => name,
+ "namespace" => namespace,
+ "selfLink" => "/apis/serving.knative.dev/v1alpha1/namespaces/#{namespace}/services/#{name}",
+ "annotation" => {
+ "description" => "This is a test description"
+ }
+ },
+ "spec" => {
+ "generation" => 2,
+ "build" => {
+ "template" => "go-1.10.3"
+ }
+ },
+ "status" => {
+ "domain" => "#{name}.#{namespace}.#{domain}",
+ "domainInternal" => "#{name}.#{namespace}.svc.cluster.local",
+ "latestCreatedRevisionName" => "#{name}-00002",
+ "latestReadyRevisionName" => "#{name}-00002",
+ "observedGeneration" => 2
+ }
+ }
+ end
+
def kube_terminals(service, pod)
pod_name = pod['metadata']['name']
containers = pod['spec']['containers']
diff --git a/spec/support/helpers/test_env.rb b/spec/support/helpers/test_env.rb
index 1f00cdf7e92..d52c40ff4f1 100644
--- a/spec/support/helpers/test_env.rb
+++ b/spec/support/helpers/test_env.rb
@@ -54,6 +54,9 @@ module TestEnv
'add_images_and_changes' => '010d106',
'update-gitlab-shell-v-6-0-1' => '2f61d70',
'update-gitlab-shell-v-6-0-3' => 'de78448',
+ 'merge-commit-analyze-before' => '1adbdef',
+ 'merge-commit-analyze-side-branch' => '8a99451',
+ 'merge-commit-analyze-after' => '646ece5',
'2-mb-file' => 'bf12d25',
'before-create-delete-modify-move' => '845009f',
'between-create-delete-modify-move' => '3f5f443',
diff --git a/spec/support/shared_examples/controllers/set_sort_order_from_user_preference_shared_examples.rb b/spec/support/shared_examples/controllers/set_sort_order_from_user_preference_shared_examples.rb
new file mode 100644
index 00000000000..b34948be670
--- /dev/null
+++ b/spec/support/shared_examples/controllers/set_sort_order_from_user_preference_shared_examples.rb
@@ -0,0 +1,32 @@
+shared_examples 'set sort order from user preference' do
+ describe '#set_sort_order_from_user_preference' do
+ # There is no issuable_sorting_field defined in any CE controllers yet,
+ # however any other field present in user_preferences table can be used for testing.
+ let(:sorting_field) { :issue_notes_filter }
+ let(:sorting_param) { 'any' }
+
+ before do
+ allow(controller).to receive(:issuable_sorting_field).and_return(sorting_field)
+ end
+
+ context 'when database is in read-only mode' do
+ it 'it does not update user preference' do
+ allow(Gitlab::Database).to receive(:read_only?).and_return(true)
+
+ expect_any_instance_of(UserPreference).not_to receive(:update_attribute).with(sorting_field, sorting_param)
+
+ get :index, namespace_id: project.namespace, project_id: project, sort: sorting_param
+ end
+ end
+
+ context 'when database is not in read-only mode' do
+ it 'updates user preference' do
+ allow(Gitlab::Database).to receive(:read_only?).and_return(false)
+
+ expect_any_instance_of(UserPreference).to receive(:update_attribute).with(sorting_field, sorting_param)
+
+ get :index, namespace_id: project.namespace, project_id: project, sort: sorting_param
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/file_finder.rb b/spec/support/shared_examples/file_finder.rb
index ef144bdf61c..0dc351b5149 100644
--- a/spec/support/shared_examples/file_finder.rb
+++ b/spec/support/shared_examples/file_finder.rb
@@ -3,18 +3,19 @@ shared_examples 'file finder' do
let(:search_results) { subject.find(query) }
it 'finds by name' do
- filename, blob = search_results.find { |_, blob| blob.filename == expected_file_by_name }
- expect(filename).to eq(expected_file_by_name)
- expect(blob).to be_a(Gitlab::SearchResults::FoundBlob)
+ blob = search_results.find { |blob| blob.filename == expected_file_by_name }
+
+ expect(blob.filename).to eq(expected_file_by_name)
+ expect(blob).to be_a(Gitlab::Search::FoundBlob)
expect(blob.ref).to eq(subject.ref)
expect(blob.data).not_to be_empty
end
it 'finds by content' do
- filename, blob = search_results.find { |_, blob| blob.filename == expected_file_by_content }
+ blob = search_results.find { |blob| blob.filename == expected_file_by_content }
- expect(filename).to eq(expected_file_by_content)
- expect(blob).to be_a(Gitlab::SearchResults::FoundBlob)
+ expect(blob.filename).to eq(expected_file_by_content)
+ expect(blob).to be_a(Gitlab::Search::FoundBlob)
expect(blob.ref).to eq(subject.ref)
expect(blob.data).not_to be_empty
end
diff --git a/spec/support/shared_examples/models/member_shared_examples.rb b/spec/support/shared_examples/models/member_shared_examples.rb
new file mode 100644
index 00000000000..77376496854
--- /dev/null
+++ b/spec/support/shared_examples/models/member_shared_examples.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+shared_examples_for 'inherited access level as a member of entity' do
+ let(:parent_entity) { create(:group) }
+ let(:user) { create(:user) }
+ let(:member) { entity.is_a?(Group) ? entity.group_member(user) : entity.project_member(user) }
+
+ context 'with root parent_entity developer member' do
+ before do
+ parent_entity.add_developer(user)
+ end
+
+ it 'is allowed to be a maintainer of the entity' do
+ entity.add_maintainer(user)
+
+ expect(member.access_level).to eq(Gitlab::Access::MAINTAINER)
+ end
+
+ it 'is not allowed to be a reporter of the entity' do
+ entity.add_reporter(user)
+
+ expect(member).to be_nil
+ end
+
+ it 'is allowed to change to be a developer of the entity' do
+ entity.add_maintainer(user)
+
+ expect { member.update(access_level: Gitlab::Access::DEVELOPER) }
+ .to change { member.access_level }.to(Gitlab::Access::DEVELOPER)
+ end
+
+ it 'is not allowed to change to be a guest of the entity' do
+ entity.add_maintainer(user)
+
+ expect { member.update(access_level: Gitlab::Access::GUEST) }
+ .not_to change { member.reload.access_level }
+ end
+
+ it "shows an error if the member can't be updated" do
+ entity.add_maintainer(user)
+
+ member.update(access_level: Gitlab::Access::REPORTER)
+
+ expect(member.errors.full_messages).to eq(["Access level should be higher than Developer inherited membership from group #{parent_entity.name}"])
+ end
+
+ it 'allows changing the level from a non existing member' do
+ non_member_user = create(:user)
+
+ entity.add_maintainer(non_member_user)
+
+ non_member = entity.is_a?(Group) ? entity.group_member(non_member_user) : entity.project_member(non_member_user)
+
+ expect { non_member.update(access_level: Gitlab::Access::GUEST) }
+ .to change { non_member.reload.access_level }
+ end
+ end
+end
+
+shared_examples_for '#valid_level_roles' do |entity_name|
+ let(:member_user) { create(:user) }
+ let(:group) { create(:group) }
+ let(:entity) { create(entity_name) }
+ let(:entity_member) { create("#{entity_name}_member", :developer, source: entity, user: member_user) }
+ let(:presenter) { described_class.new(entity_member, current_user: member_user) }
+ let(:expected_roles) { { 'Developer' => 30, 'Maintainer' => 40, 'Reporter' => 20 } }
+
+ it 'returns all roles when no parent member is present' do
+ expect(presenter.valid_level_roles).to eq(entity_member.class.access_level_roles)
+ end
+
+ it 'returns higher roles when a parent member is present' do
+ group.add_reporter(member_user)
+
+ expect(presenter.valid_level_roles).to eq(expected_roles)
+ end
+end
diff --git a/spec/support/shared_examples/models/with_uploads_shared_examples.rb b/spec/support/shared_examples/models/with_uploads_shared_examples.rb
index 47ad0c6345d..1d11b855459 100644
--- a/spec/support/shared_examples/models/with_uploads_shared_examples.rb
+++ b/spec/support/shared_examples/models/with_uploads_shared_examples.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-shared_examples_for 'model with mounted uploader' do |supports_fileuploads|
+shared_examples_for 'model with uploads' do |supports_fileuploads|
describe '.destroy' do
before do
stub_uploads_object_storage(uploader_class)
@@ -8,16 +8,62 @@ shared_examples_for 'model with mounted uploader' do |supports_fileuploads|
model_object.public_send(upload_attribute).migrate!(ObjectStorage::Store::REMOTE)
end
- it 'deletes remote uploads' do
- expect_any_instance_of(CarrierWave::Storage::Fog::File).to receive(:delete).and_call_original
+ context 'with mounted uploader' do
+ it 'deletes remote uploads' do
+ expect_any_instance_of(CarrierWave::Storage::Fog::File).to receive(:delete).and_call_original
- expect { model_object.destroy }.to change { Upload.count }.by(-1)
+ expect { model_object.destroy }.to change { Upload.count }.by(-1)
+ end
end
- it 'deletes any FileUploader uploads which are not mounted', skip: !supports_fileuploads do
- create(:upload, uploader: FileUploader, model: model_object)
+ context 'with not mounted uploads', :sidekiq, skip: !supports_fileuploads do
+ context 'with local files' do
+ let!(:uploads) { create_list(:upload, 2, uploader: FileUploader, model: model_object) }
- expect { model_object.destroy }.to change { Upload.count }.by(-2)
+ it 'deletes any FileUploader uploads which are not mounted' do
+ expect { model_object.destroy }.to change { Upload.count }.by(-3)
+ end
+
+ it 'deletes local files' do
+ expect_any_instance_of(Uploads::Local).to receive(:delete_keys).with(uploads.map(&:absolute_path))
+
+ model_object.destroy
+ end
+ end
+
+ context 'with remote files' do
+ let!(:uploads) { create_list(:upload, 2, :object_storage, uploader: FileUploader, model: model_object) }
+
+ it 'deletes any FileUploader uploads which are not mounted' do
+ expect { model_object.destroy }.to change { Upload.count }.by(-3)
+ end
+
+ it 'deletes remote files' do
+ expect_any_instance_of(Uploads::Fog).to receive(:delete_keys).with(uploads.map(&:path))
+
+ model_object.destroy
+ end
+ end
+
+ describe 'destroy strategy depending on feature flag' do
+ let!(:upload) { create(:upload, uploader: FileUploader, model: model_object) }
+
+ it 'does not destroy uploads by default' do
+ expect(model_object).to receive(:delete_uploads)
+ expect(model_object).not_to receive(:destroy_uploads)
+
+ model_object.destroy
+ end
+
+ it 'uses before destroy callback if feature flag is disabled' do
+ stub_feature_flags(fast_destroy_uploads: false)
+
+ expect(model_object).to receive(:destroy_uploads)
+ expect(model_object).not_to receive(:delete_uploads)
+
+ model_object.destroy
+ end
+ end
end
end
end
diff --git a/spec/support/shared_examples/notify_shared_examples.rb b/spec/support/shared_examples/notify_shared_examples.rb
index 66536e80db2..a38354060cf 100644
--- a/spec/support/shared_examples/notify_shared_examples.rb
+++ b/spec/support/shared_examples/notify_shared_examples.rb
@@ -1,5 +1,5 @@
shared_context 'gitlab email notification' do
- set(:project) { create(:project, :repository) }
+ set(:project) { create(:project, :repository, name: 'a-known-name') }
set(:recipient) { create(:user, email: 'recipient@example.com') }
let(:gitlab_sender_display_name) { Gitlab.config.gitlab.email_display_name }
@@ -62,9 +62,11 @@ end
shared_examples 'an email with X-GitLab headers containing project details' do
it 'has X-GitLab-Project headers' do
aggregate_failures do
+ full_path_as_domain = "#{project.name}.#{project.namespace.path}"
is_expected.to have_header('X-GitLab-Project', /#{project.name}/)
is_expected.to have_header('X-GitLab-Project-Id', /#{project.id}/)
is_expected.to have_header('X-GitLab-Project-Path', /#{project.full_path}/)
+ is_expected.to have_header('List-Id', "#{project.full_path} <#{project.id}.#{full_path_as_domain}.#{Gitlab.config.gitlab.host}>")
end
end
end
diff --git a/spec/support/shared_examples/only_except_policy_examples.rb b/spec/support/shared_examples/only_except_policy_examples.rb
deleted file mode 100644
index 35240af1d74..00000000000
--- a/spec/support/shared_examples/only_except_policy_examples.rb
+++ /dev/null
@@ -1,167 +0,0 @@
-# frozen_string_literal: true
-
-shared_examples 'correct only except policy' do
- context 'when using simplified policy' do
- describe 'validations' do
- context 'when entry config value is valid' do
- context 'when config is a branch or tag name' do
- let(:config) { %w[master feature/branch] }
-
- describe '#valid?' do
- it 'is valid' do
- expect(entry).to be_valid
- end
- end
-
- describe '#value' do
- it 'returns refs hash' do
- expect(entry.value).to eq(refs: config)
- end
- end
- end
-
- context 'when config is a regexp' do
- let(:config) { ['/^issue-.*$/'] }
-
- describe '#valid?' do
- it 'is valid' do
- expect(entry).to be_valid
- end
- end
- end
-
- context 'when config is a special keyword' do
- let(:config) { %w[tags triggers branches] }
-
- describe '#valid?' do
- it 'is valid' do
- expect(entry).to be_valid
- end
- end
- end
- end
-
- context 'when entry value is not valid' do
- let(:config) { [1] }
-
- describe '#errors' do
- it 'saves errors' do
- expect(entry.errors)
- .to include /policy config should be an array of strings or regexps/
- end
- end
- end
- end
- end
-
- context 'when using complex policy' do
- context 'when specifying refs policy' do
- let(:config) { { refs: ['master'] } }
-
- it 'is a correct configuraton' do
- expect(entry).to be_valid
- expect(entry.value).to eq(refs: %w[master])
- end
- end
-
- context 'when specifying kubernetes policy' do
- let(:config) { { kubernetes: 'active' } }
-
- it 'is a correct configuraton' do
- expect(entry).to be_valid
- expect(entry.value).to eq(kubernetes: 'active')
- end
- end
-
- context 'when specifying invalid kubernetes policy' do
- let(:config) { { kubernetes: 'something' } }
-
- it 'reports an error about invalid policy' do
- expect(entry.errors).to include /unknown value: something/
- end
- end
-
- context 'when specifying valid variables expressions policy' do
- let(:config) { { variables: ['$VAR == null'] } }
-
- it 'is a correct configuraton' do
- expect(entry).to be_valid
- expect(entry.value).to eq(config)
- end
- end
-
- context 'when specifying variables expressions in invalid format' do
- let(:config) { { variables: '$MY_VAR' } }
-
- it 'reports an error about invalid format' do
- expect(entry.errors).to include /should be an array of strings/
- end
- end
-
- context 'when specifying invalid variables expressions statement' do
- let(:config) { { variables: ['$MY_VAR =='] } }
-
- it 'reports an error about invalid statement' do
- expect(entry.errors).to include /invalid expression syntax/
- end
- end
-
- context 'when specifying invalid variables expressions token' do
- let(:config) { { variables: ['$MY_VAR == 123'] } }
-
- it 'reports an error about invalid expression' do
- expect(entry.errors).to include /invalid expression syntax/
- end
- end
-
- context 'when using invalid variables expressions regexp' do
- let(:config) { { variables: ['$MY_VAR =~ /some ( thing/'] } }
-
- it 'reports an error about invalid expression' do
- expect(entry.errors).to include /invalid expression syntax/
- end
- end
-
- context 'when specifying a valid changes policy' do
- let(:config) { { changes: %w[some/* paths/**/*.rb] } }
-
- it 'is a correct configuraton' do
- expect(entry).to be_valid
- expect(entry.value).to eq(config)
- end
- end
-
- context 'when changes policy is invalid' do
- let(:config) { { changes: [1, 2] } }
-
- it 'returns errors' do
- expect(entry.errors).to include /changes should be an array of strings/
- end
- end
-
- context 'when specifying unknown policy' do
- let(:config) { { refs: ['master'], invalid: :something } }
-
- it 'returns error about invalid key' do
- expect(entry.errors).to include /unknown keys: invalid/
- end
- end
-
- context 'when policy is empty' do
- let(:config) { {} }
-
- it 'is not a valid configuration' do
- expect(entry.errors).to include /can't be blank/
- end
- end
- end
-
- context 'when policy strategy does not match' do
- let(:config) { 'string strategy' }
-
- it 'returns information about errors' do
- expect(entry.errors)
- .to include /has to be either an array of conditions or a hash/
- end
- end
-end
diff --git a/spec/support/shared_examples/serializers/diff_file_entity_examples.rb b/spec/support/shared_examples/serializers/diff_file_entity_examples.rb
new file mode 100644
index 00000000000..b8065886c42
--- /dev/null
+++ b/spec/support/shared_examples/serializers/diff_file_entity_examples.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+shared_examples 'diff file base entity' do
+ it 'exposes essential attributes' do
+ expect(subject).to include(:content_sha, :submodule, :submodule_link,
+ :submodule_tree_url, :old_path_html,
+ :new_path_html, :blob, :can_modify_blob,
+ :file_hash, :file_path, :old_path, :new_path,
+ :collapsed, :text, :diff_refs, :stored_externally,
+ :external_storage, :renamed_file, :deleted_file,
+ :mode_changed, :a_mode, :b_mode, :new_file)
+ end
+
+ # Converted diff files from GitHub import does not contain blob file
+ # and content sha.
+ context 'when diff file does not have a blob and content sha' do
+ it 'exposes some attributes as nil' do
+ allow(diff_file).to receive(:content_sha).and_return(nil)
+ allow(diff_file).to receive(:blob).and_return(nil)
+
+ expect(subject[:context_lines_path]).to be_nil
+ expect(subject[:view_path]).to be_nil
+ expect(subject[:highlighted_diff_lines]).to be_nil
+ expect(subject[:can_modify_blob]).to be_nil
+ end
+ end
+end
+
+shared_examples 'diff file entity' do
+ it_behaves_like 'diff file base entity'
+
+ it 'exposes correct attributes' do
+ expect(subject).to include(:too_large, :added_lines, :removed_lines,
+ :context_lines_path, :highlighted_diff_lines,
+ :parallel_diff_lines)
+ end
+
+ it 'includes viewer' do
+ expect(subject[:viewer].with_indifferent_access)
+ .to match_schema('entities/diff_viewer')
+ end
+end
+
+shared_examples 'diff file discussion entity' do
+ it_behaves_like 'diff file base entity'
+end
diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb
index 8c4360d4cf0..3b8f7f5fe7d 100644
--- a/spec/tasks/gitlab/backup_rake_spec.rb
+++ b/spec/tasks/gitlab/backup_rake_spec.rb
@@ -74,6 +74,7 @@ describe 'gitlab:app namespace rake task' do
it 'invokes restoration on match' do
allow(YAML).to receive(:load_file)
.and_return({ gitlab_version: gitlab_version })
+
expect(Rake::Task['gitlab:db:drop_tables']).to receive(:invoke)
expect(Rake::Task['gitlab:backup:db:restore']).to receive(:invoke)
expect(Rake::Task['gitlab:backup:repo:restore']).to receive(:invoke)
diff --git a/spec/tasks/gitlab/web_hook_rake_spec.rb b/spec/tasks/gitlab/web_hook_rake_spec.rb
new file mode 100644
index 00000000000..7bdf33ff6b0
--- /dev/null
+++ b/spec/tasks/gitlab/web_hook_rake_spec.rb
@@ -0,0 +1,92 @@
+require 'rake_helper'
+
+describe 'gitlab:web_hook namespace rake tasks' do
+ set(:group) { create(:group) }
+
+ set(:project1) { create(:project, namespace: group) }
+ set(:project2) { create(:project, namespace: group) }
+ set(:other_group_project) { create(:project) }
+
+ let(:url) { 'http://example.com' }
+ let(:hook_urls) { (project1.hooks + project2.hooks).map(&:url) }
+ let(:other_group_hook_urls) { other_group_project.hooks.map(&:url) }
+
+ before do
+ Rake.application.rake_require 'tasks/gitlab/web_hook'
+ end
+
+ describe 'gitlab:web_hook:add' do
+ it 'adds a web hook to all projects' do
+ stub_env('URL' => url)
+ run_rake_task('gitlab:web_hook:add')
+
+ expect(hook_urls).to contain_exactly(url, url)
+ expect(other_group_hook_urls).to contain_exactly(url)
+ end
+
+ it 'adds a web hook to projects in the specified namespace' do
+ stub_env('URL' => url, 'NAMESPACE' => group.full_path)
+ run_rake_task('gitlab:web_hook:add')
+
+ expect(hook_urls).to contain_exactly(url, url)
+ expect(other_group_hook_urls).to be_empty
+ end
+
+ it 'raises an error if an unknown namespace is specified' do
+ stub_env('URL' => url, 'NAMESPACE' => group.full_path)
+
+ group.destroy
+
+ expect { run_rake_task('gitlab:web_hook:add') }.to raise_error(SystemExit)
+ end
+ end
+
+ describe 'gitlab:web_hook:rm' do
+ let!(:hook1) { create(:project_hook, project: project1, url: url) }
+ let!(:hook2) { create(:project_hook, project: project2, url: url) }
+ let!(:other_group_hook) { create(:project_hook, project: other_group_project, url: url) }
+ let!(:other_url_hook) { create(:project_hook, url: other_url, project: project1) }
+
+ let(:other_url) { 'http://other.example.com' }
+
+ it 'removes a web hook from all projects by URL' do
+ stub_env('URL' => url)
+ run_rake_task('gitlab:web_hook:rm')
+
+ expect(hook_urls).to contain_exactly(other_url)
+ expect(other_group_hook_urls).to be_empty
+ end
+
+ it 'removes a web hook from projects in the specified namespace by URL' do
+ stub_env('NAMESPACE' => group.full_path, 'URL' => url)
+ run_rake_task('gitlab:web_hook:rm')
+
+ expect(hook_urls).to contain_exactly(other_url)
+ expect(other_group_hook_urls).to contain_exactly(url)
+ end
+
+ it 'raises an error if an unknown namespace is specified' do
+ stub_env('URL' => url, 'NAMESPACE' => group.full_path)
+
+ group.destroy
+
+ expect { run_rake_task('gitlab:web_hook:rm') }.to raise_error(SystemExit)
+ end
+ end
+
+ describe 'gitlab:web_hook:list' do
+ let!(:hook1) { create(:project_hook, project: project1) }
+ let!(:hook2) { create(:project_hook, project: project2) }
+ let!(:other_group_hook) { create(:project_hook, project: other_group_project) }
+
+ it 'lists all web hooks' do
+ expect { run_rake_task('gitlab:web_hook:list') }.to output(/3 webhooks found/).to_stdout
+ end
+
+ it 'lists web hooks in a particular namespace' do
+ stub_env('NAMESPACE', group.full_path)
+
+ expect { run_rake_task('gitlab:web_hook:list') }.to output(/2 webhooks found/).to_stdout
+ end
+ end
+end
diff --git a/spec/uploaders/namespace_file_uploader_spec.rb b/spec/uploaders/namespace_file_uploader_spec.rb
index d09725ee4be..77401814194 100644
--- a/spec/uploaders/namespace_file_uploader_spec.rb
+++ b/spec/uploaders/namespace_file_uploader_spec.rb
@@ -1,18 +1,22 @@
require 'spec_helper'
-IDENTIFIER = %r{\h+/\S+}
-
describe NamespaceFileUploader do
let(:group) { build_stubbed(:group) }
let(:uploader) { described_class.new(group) }
let(:upload) { create(:upload, :namespace_upload, model: group) }
+ let(:identifier) { %r{\h+/\S+} }
subject { uploader }
- it_behaves_like 'builds correct paths',
- store_dir: %r[uploads/-/system/namespace/\d+],
- upload_path: IDENTIFIER,
- absolute_path: %r[#{CarrierWave.root}/uploads/-/system/namespace/\d+/#{IDENTIFIER}]
+ it_behaves_like 'builds correct paths' do
+ let(:patterns) do
+ {
+ store_dir: %r[uploads/-/system/namespace/\d+],
+ upload_path: identifier,
+ absolute_path: %r[#{CarrierWave.root}/uploads/-/system/namespace/\d+/#{identifier}]
+ }
+ end
+ end
context "object_store is REMOTE" do
before do
@@ -21,9 +25,14 @@ describe NamespaceFileUploader do
include_context 'with storage', described_class::Store::REMOTE
- it_behaves_like 'builds correct paths',
- store_dir: %r[namespace/\d+/\h+],
- upload_path: IDENTIFIER
+ it_behaves_like 'builds correct paths' do
+ let(:patterns) do
+ {
+ store_dir: %r[namespace/\d+/\h+],
+ upload_path: identifier
+ }
+ end
+ end
end
context '.base_dir' do
diff --git a/spec/uploaders/personal_file_uploader_spec.rb b/spec/uploaders/personal_file_uploader_spec.rb
index 7700b14ce6b..2896e9a112d 100644
--- a/spec/uploaders/personal_file_uploader_spec.rb
+++ b/spec/uploaders/personal_file_uploader_spec.rb
@@ -1,18 +1,22 @@
require 'spec_helper'
-IDENTIFIER = %r{\h+/\S+}
-
describe PersonalFileUploader do
let(:model) { create(:personal_snippet) }
let(:uploader) { described_class.new(model) }
let(:upload) { create(:upload, :personal_snippet_upload) }
+ let(:identifier) { %r{\h+/\S+} }
subject { uploader }
- it_behaves_like 'builds correct paths',
- store_dir: %r[uploads/-/system/personal_snippet/\d+],
- upload_path: IDENTIFIER,
- absolute_path: %r[#{CarrierWave.root}/uploads/-/system/personal_snippet/\d+/#{IDENTIFIER}]
+ it_behaves_like 'builds correct paths' do
+ let(:patterns) do
+ {
+ store_dir: %r[uploads/-/system/personal_snippet/\d+],
+ upload_path: identifier,
+ absolute_path: %r[#{CarrierWave.root}/uploads/-/system/personal_snippet/\d+/#{identifier}]
+ }
+ end
+ end
context "object_store is REMOTE" do
before do
@@ -21,9 +25,14 @@ describe PersonalFileUploader do
include_context 'with storage', described_class::Store::REMOTE
- it_behaves_like 'builds correct paths',
- store_dir: %r[\d+/\h+],
- upload_path: IDENTIFIER
+ it_behaves_like 'builds correct paths' do
+ let(:patterns) do
+ {
+ store_dir: %r[\d+/\h+],
+ upload_path: identifier
+ }
+ end
+ end
end
describe '#to_h' do
diff --git a/spec/validators/url_validator_spec.rb b/spec/validators/url_validator_spec.rb
index 082d09d3f16..f3f3386382f 100644
--- a/spec/validators/url_validator_spec.rb
+++ b/spec/validators/url_validator_spec.rb
@@ -143,4 +143,33 @@ describe UrlValidator do
end
end
end
+
+ context 'when ascii_only is' do
+ let(:url) { 'https://𝕘itⅼαƄ.com/foo/foo.bar'}
+ let(:validator) { described_class.new(attributes: [:link_url], ascii_only: ascii_only) }
+
+ context 'true' do
+ let(:ascii_only) { true }
+
+ it 'prevents unicode characters' do
+ badge.link_url = url
+
+ subject
+
+ expect(badge.errors.empty?).to be false
+ end
+ end
+
+ context 'false (default)' do
+ let(:ascii_only) { false }
+
+ it 'does not prevent unicode characters' do
+ badge.link_url = url
+
+ subject
+
+ expect(badge.errors.empty?).to be true
+ end
+ end
+ end
end
diff --git a/spec/views/projects/_home_panel.html.haml_spec.rb b/spec/views/projects/_home_panel.html.haml_spec.rb
index fc1fe5739c3..006c93686d5 100644
--- a/spec/views/projects/_home_panel.html.haml_spec.rb
+++ b/spec/views/projects/_home_panel.html.haml_spec.rb
@@ -23,7 +23,7 @@ describe 'projects/_home_panel' do
it 'makes it possible to set notification level' do
render
- expect(view).to render_template('shared/notifications/_button')
+ expect(view).to render_template('projects/buttons/_notifications')
expect(rendered).to have_selector('.notification-dropdown')
end
end
diff --git a/spec/workers/object_pool/create_worker_spec.rb b/spec/workers/object_pool/create_worker_spec.rb
new file mode 100644
index 00000000000..06416489472
--- /dev/null
+++ b/spec/workers/object_pool/create_worker_spec.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ObjectPool::CreateWorker do
+ let(:pool) { create(:pool_repository, :scheduled) }
+
+ subject { described_class.new }
+
+ describe '#perform' do
+ context 'when the pool creation is successful' do
+ it 'marks the pool as ready' do
+ subject.perform(pool.id)
+
+ expect(pool.reload).to be_ready
+ end
+ end
+
+ context 'when a the pool already exists' do
+ before do
+ pool.create_object_pool
+ end
+
+ it 'cleans up the pool' do
+ expect do
+ subject.perform(pool.id)
+ end.to raise_error(GRPC::FailedPrecondition)
+
+ expect(pool.reload.failed?).to be(true)
+ end
+ end
+
+ context 'when the server raises an unknown error' do
+ before do
+ allow_any_instance_of(PoolRepository).to receive(:create_object_pool).and_raise(GRPC::Internal)
+ end
+
+ it 'marks the pool as failed' do
+ expect do
+ subject.perform(pool.id)
+ end.to raise_error(GRPC::Internal)
+
+ expect(pool.reload.failed?).to be(true)
+ end
+ end
+
+ context 'when the pool creation failed before' do
+ let(:pool) { create(:pool_repository, :failed) }
+
+ it 'deletes the pool first' do
+ expect_any_instance_of(PoolRepository).to receive(:delete_object_pool)
+
+ subject.perform(pool.id)
+
+ expect(pool.reload).to be_ready
+ end
+ end
+ end
+end
diff --git a/spec/workers/object_pool/join_worker_spec.rb b/spec/workers/object_pool/join_worker_spec.rb
new file mode 100644
index 00000000000..906bc22c8d2
--- /dev/null
+++ b/spec/workers/object_pool/join_worker_spec.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ObjectPool::JoinWorker do
+ let(:pool) { create(:pool_repository, :ready) }
+ let(:project) { pool.source_project }
+ let(:repository) { project.repository }
+
+ subject { described_class.new }
+
+ describe '#perform' do
+ context "when the pool is not joinable" do
+ let(:pool) { create(:pool_repository, :scheduled) }
+
+ it "doesn't raise an error" do
+ expect do
+ subject.perform(pool.id, project.id)
+ end.not_to raise_error
+ end
+ end
+
+ context 'when the pool has been joined before' do
+ before do
+ pool.link_repository(repository)
+ end
+
+ it 'succeeds in joining' do
+ expect do
+ subject.perform(pool.id, project.id)
+ end.not_to raise_error
+ end
+ end
+ end
+end
diff --git a/spec/workers/prune_web_hook_logs_worker_spec.rb b/spec/workers/prune_web_hook_logs_worker_spec.rb
index d7d64a1f641..b3ec71d4a00 100644
--- a/spec/workers/prune_web_hook_logs_worker_spec.rb
+++ b/spec/workers/prune_web_hook_logs_worker_spec.rb
@@ -5,18 +5,20 @@ describe PruneWebHookLogsWorker do
before do
hook = create(:project_hook)
- 5.times do
- create(:web_hook_log, web_hook: hook, created_at: 5.months.ago)
- end
-
+ create(:web_hook_log, web_hook: hook, created_at: 5.months.ago)
+ create(:web_hook_log, web_hook: hook, created_at: 4.months.ago)
+ create(:web_hook_log, web_hook: hook, created_at: 91.days.ago)
+ create(:web_hook_log, web_hook: hook, created_at: 89.days.ago)
+ create(:web_hook_log, web_hook: hook, created_at: 2.months.ago)
+ create(:web_hook_log, web_hook: hook, created_at: 1.month.ago)
create(:web_hook_log, web_hook: hook, response_status: '404')
end
- it 'removes all web hook logs older than one month' do
+ it 'removes all web hook logs older than 90 days' do
described_class.new.perform
- expect(WebHookLog.count).to eq(1)
- expect(WebHookLog.first.response_status).to eq('404')
+ expect(WebHookLog.count).to eq(4)
+ expect(WebHookLog.last.response_status).to eq('404')
end
end
end
diff --git a/spec/workers/rebase_worker_spec.rb b/spec/workers/rebase_worker_spec.rb
index 936b9deaecc..900332ed6b3 100644
--- a/spec/workers/rebase_worker_spec.rb
+++ b/spec/workers/rebase_worker_spec.rb
@@ -19,7 +19,7 @@ describe RebaseWorker, '#perform' do
expect(MergeRequests::RebaseService)
.to receive(:new).with(forked_project, merge_request.author).and_call_original
- subject.perform(merge_request, merge_request.author)
+ subject.perform(merge_request.id, merge_request.author.id)
end
end
end
diff --git a/spec/workers/remove_old_web_hook_logs_worker_spec.rb b/spec/workers/remove_old_web_hook_logs_worker_spec.rb
deleted file mode 100644
index 6d26ba5dfa0..00000000000
--- a/spec/workers/remove_old_web_hook_logs_worker_spec.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-require 'spec_helper'
-
-describe RemoveOldWebHookLogsWorker do
- subject { described_class.new }
-
- describe '#perform' do
- let!(:week_old_record) { create(:web_hook_log, created_at: Time.now - 1.week) }
- let!(:three_days_old_record) { create(:web_hook_log, created_at: Time.now - 3.days) }
- let!(:one_day_old_record) { create(:web_hook_log, created_at: Time.now - 1.day) }
-
- it 'removes web hook logs older than 2 days' do
- subject.perform
-
- expect(WebHookLog.all).to include(one_day_old_record)
- expect(WebHookLog.all).not_to include(week_old_record, three_days_old_record)
- end
- end
-end
diff --git a/spec/workers/repository_cleanup_worker_spec.rb b/spec/workers/repository_cleanup_worker_spec.rb
new file mode 100644
index 00000000000..3adae0b6cfa
--- /dev/null
+++ b/spec/workers/repository_cleanup_worker_spec.rb
@@ -0,0 +1,55 @@
+require 'spec_helper'
+
+describe RepositoryCleanupWorker do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+
+ subject(:worker) { described_class.new }
+
+ describe '#perform' do
+ it 'executes the cleanup service and sends a success notification' do
+ expect_next_instance_of(Projects::CleanupService) do |service|
+ expect(service.project).to eq(project)
+ expect(service.current_user).to eq(user)
+
+ expect(service).to receive(:execute)
+ end
+
+ expect_next_instance_of(NotificationService) do |service|
+ expect(service).to receive(:repository_cleanup_success).with(project, user)
+ end
+
+ worker.perform(project.id, user.id)
+ end
+
+ it 'raises an error if the project cannot be found' do
+ project.destroy
+
+ expect { worker.perform(project.id, user.id) }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+
+ it 'raises an error if the user cannot be found' do
+ user.destroy
+
+ expect { worker.perform(project.id, user.id) }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+ end
+
+ describe '#sidekiq_retries_exhausted' do
+ let(:job) { { 'args' => [project.id, user.id], 'error_message' => 'Error' } }
+
+ it 'does not send a failure notification for a RecordNotFound error' do
+ expect(NotificationService).not_to receive(:new)
+
+ described_class.sidekiq_retries_exhausted_block.call(job, ActiveRecord::RecordNotFound.new)
+ end
+
+ it 'sends a failure notification' do
+ expect_next_instance_of(NotificationService) do |service|
+ expect(service).to receive(:repository_cleanup_failure).with(project, user, 'Error')
+ end
+
+ described_class.sidekiq_retries_exhausted_block.call(job, StandardError.new)
+ end
+ end
+end
diff --git a/spec/workers/repository_update_remote_mirror_worker_spec.rb b/spec/workers/repository_update_remote_mirror_worker_spec.rb
index 4f1ad2474f5..d73b0b53713 100644
--- a/spec/workers/repository_update_remote_mirror_worker_spec.rb
+++ b/spec/workers/repository_update_remote_mirror_worker_spec.rb
@@ -25,12 +25,19 @@ describe RepositoryUpdateRemoteMirrorWorker do
it 'sets status as failed when update remote mirror service executes with errors' do
error_message = 'fail!'
- expect_any_instance_of(Projects::UpdateRemoteMirrorService).to receive(:execute).with(remote_mirror).and_return(status: :error, message: error_message)
+ expect_next_instance_of(Projects::UpdateRemoteMirrorService) do |service|
+ expect(service).to receive(:execute).with(remote_mirror).and_return(status: :error, message: error_message)
+ end
+
+ # Mock the finder so that it returns an object we can set expectations on
+ expect_next_instance_of(RemoteMirrorFinder) do |finder|
+ expect(finder).to receive(:execute).and_return(remote_mirror)
+ end
+ expect(remote_mirror).to receive(:mark_as_failed).with(error_message)
+
expect do
subject.perform(remote_mirror.id, Time.now)
end.to raise_error(RepositoryUpdateRemoteMirrorWorker::UpdateError, error_message)
-
- expect(remote_mirror.reload.update_status).to eq('failed')
end
it 'does nothing if last_update_started_at is higher than the time the job was scheduled in' do
diff --git a/vendor/gitignore/CMake.gitignore b/vendor/gitignore/CMake.gitignore
index 9ea395f15ee..7e25564f9ec 100644
--- a/vendor/gitignore/CMake.gitignore
+++ b/vendor/gitignore/CMake.gitignore
@@ -1,3 +1,4 @@
+CMakeLists.txt.user
CMakeCache.txt
CMakeFiles
CMakeScripts
diff --git a/vendor/gitignore/Drupal.gitignore b/vendor/gitignore/Drupal.gitignore
index 072b683190f..50d3eef8a33 100644
--- a/vendor/gitignore/Drupal.gitignore
+++ b/vendor/gitignore/Drupal.gitignore
@@ -1,39 +1,46 @@
-# Ignore configuration files that may contain sensitive information.
-sites/*/*settings*.php
-sites/example.sites.php
+# gitignore template for Drupal 8 projects
+#
+# earlier versions of Drupal are tracked in `community/Python/`
-# Ignore paths that contain generated content.
-files/
-sites/*/files
-sites/*/private
-sites/*/translations
+# Ignore configuration files that may contain sensitive information
+/sites/*/*settings*.php
+/sites/*/*services*.yml
-# Ignore default text files
-robots.txt
-/CHANGELOG.txt
-/COPYRIGHT.txt
-/INSTALL*.txt
+# Ignore paths that may contain user-generated content
+/sites/*/files
+/sites/*/public
+/sites/*/private
+/sites/*/files-public
+/sites/*/files-private
+
+# Ignore paths that may contain temporary files
+/sites/*/translations
+/sites/*/tmp
+/sites/*/cache
+
+# Ignore drupal core (if not versioning drupal sources)
+/core
+/modules/README.txt
+/profiles/README.txt
+/sites/README.txt
+/sites/example.sites.php
+/sites/example.settings.local.php
+/sites/development.services.yml
+/themes/README.txt
+/vendor
+/.csslintrc
+/.editorconfig
+/.eslintignore
+/.eslintrc.json
+/.gitattributes
+/.htaccess
+/autoload.php
+/composer.json
+/composer.lock
+/example.gitignore
+/index.php
/LICENSE.txt
-/MAINTAINERS.txt
-/UPGRADE.txt
/README.txt
-sites/README.txt
-sites/all/libraries/README.txt
-sites/all/modules/README.txt
-sites/all/themes/README.txt
-
-# Ignore everything but the "sites" folder ( for non core developer )
-.htaccess
-web.config
-authorize.php
-cron.php
-index.php
-install.php
-update.php
-xmlrpc.php
-/includes
-/misc
-/modules
-/profiles
-/scripts
-/themes
+/robots.txt
+/update.php
+/web.config
diff --git a/vendor/gitignore/Global/Emacs.gitignore b/vendor/gitignore/Global/Emacs.gitignore
index 3ac7904dcd2..d40e86599b5 100644
--- a/vendor/gitignore/Global/Emacs.gitignore
+++ b/vendor/gitignore/Global/Emacs.gitignore
@@ -43,3 +43,7 @@ flycheck_*.el
# directory configuration
.dir-locals.el
+
+# network security
+/network-security.data
+
diff --git a/vendor/gitignore/Global/PuTTY.gitignore b/vendor/gitignore/Global/PuTTY.gitignore
new file mode 100644
index 00000000000..c37466b1c79
--- /dev/null
+++ b/vendor/gitignore/Global/PuTTY.gitignore
@@ -0,0 +1,2 @@
+# Private key
+*.ppk
diff --git a/vendor/gitignore/Global/Virtuoso.gitignore b/vendor/gitignore/Global/Virtuoso.gitignore
new file mode 100644
index 00000000000..2de03673a6c
--- /dev/null
+++ b/vendor/gitignore/Global/Virtuoso.gitignore
@@ -0,0 +1,18 @@
+# Gitignore for Cadence Virtuoso
+################################################################
+
+# Log files
+*.log
+panic*.log.*
+
+# OpenAccess database lock files
+*.cdslck*
+
+# Run directories for layout vs. schematic and design rule check
+lvsRunDir/*
+drcRunDir/*
+
+# Abstract generation tool
+abstract.log*
+abstract.record*
+
diff --git a/vendor/gitignore/Global/Xcode.gitignore b/vendor/gitignore/Global/Xcode.gitignore
index b01314d3a64..cd0c7d3e45a 100644
--- a/vendor/gitignore/Global/Xcode.gitignore
+++ b/vendor/gitignore/Global/Xcode.gitignore
@@ -2,11 +2,17 @@
#
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
-## Build generated
+## User settings
+xcuserdata/
+
+## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
+*.xcscmblueprint
+*.xccheckout
+
+## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
build/
DerivedData/
-
-## Various settings
+*.moved-aside
*.pbxuser
!default.pbxuser
*.mode1v3
@@ -15,65 +21,3 @@ DerivedData/
!default.mode2v3
*.perspectivev3
!default.perspectivev3
-xcuserdata/
-
-## Other
-*.moved-aside
-*.xccheckout
-*.xcscmblueprint
-
-## Obj-C/Swift specific
-*.hmap
-*.ipa
-*.dSYM.zip
-*.dSYM
-
-## Playgrounds
-timeline.xctimeline
-playground.xcworkspace
-
-# Swift Package Manager
-#
-# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
-# Packages/
-# Package.pins
-# Package.resolved
-.build/
-
-# CocoaPods
-#
-# We recommend against adding the Pods directory to your .gitignore. However
-# you should judge for yourself, the pros and cons are mentioned at:
-# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
-#
-# Pods/
-#
-# Add this line if you want to avoid checking in source code from the Xcode workspace
-# *.xcworkspace
-
-# Carthage
-#
-# Add this line if you want to avoid checking in source code from Carthage dependencies.
-# Carthage/Checkouts
-
-Carthage/Build
-
-# fastlane
-#
-# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
-# screenshots whenever they are needed.
-# For more information about the recommended setup visit:
-# https://docs.fastlane.tools/best-practices/source-control/#source-control
-
-fastlane/report.xml
-fastlane/Preview.html
-fastlane/screenshots/**/*.png
-fastlane/test_output
-
-# Code Injection
-#
-# After new code Injection tools there's a generated folder /iOSInjectionProject
-# https://github.com/johnno1962/injectionforxcode
-
-iOSInjectionProject/
-
diff --git a/vendor/gitignore/Node.gitignore b/vendor/gitignore/Node.gitignore
index e1da6ae8ea5..ebfe43954dc 100644
--- a/vendor/gitignore/Node.gitignore
+++ b/vendor/gitignore/Node.gitignore
@@ -70,7 +70,10 @@ typings/
.vuepress/dist
# Serverless directories
-.serverless
+.serverless/
# FuseBox cache
.fusebox/
+
+#DynamoDB Local files
+.dynamodb/
diff --git a/vendor/gitignore/Python.gitignore b/vendor/gitignore/Python.gitignore
index 510c73d0fdb..11614af2870 100644
--- a/vendor/gitignore/Python.gitignore
+++ b/vendor/gitignore/Python.gitignore
@@ -20,6 +20,7 @@ parts/
sdist/
var/
wheels/
+share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
diff --git a/vendor/gitignore/Smalltalk.gitignore b/vendor/gitignore/Smalltalk.gitignore
index 943995e1172..178d87af45b 100644
--- a/vendor/gitignore/Smalltalk.gitignore
+++ b/vendor/gitignore/Smalltalk.gitignore
@@ -1,8 +1,11 @@
# changes file
*.changes
+*.chg
# system image
*.image
+*.img7
+*.img
# Pharo Smalltalk Debug log file
PharoDebug.log
@@ -10,6 +13,12 @@ PharoDebug.log
# Squeak Smalltalk Debug log file
SqueakDebug.log
+# Dolphin Smalltalk source file
+*.sml
+
+# Dolphin Smalltalk error file
+*.errors
+
# Monticello package cache
/package-cache
diff --git a/vendor/gitignore/TeX.gitignore b/vendor/gitignore/TeX.gitignore
index 753f2b954ff..edd1f60b726 100644
--- a/vendor/gitignore/TeX.gitignore
+++ b/vendor/gitignore/TeX.gitignore
@@ -205,6 +205,10 @@ pythontex-files-*/
# todonotes
*.tdo
+# vhistory
+*.hst
+*.ver
+
# easy-todo
*.lod
diff --git a/vendor/gitignore/Unity.gitignore b/vendor/gitignore/Unity.gitignore
index 833e6d4291c..93c9ce52191 100644
--- a/vendor/gitignore/Unity.gitignore
+++ b/vendor/gitignore/Unity.gitignore
@@ -35,3 +35,7 @@ sysinfo.txt
# Builds
*.apk
*.unitypackage
+
+# Crashlytics generated file
+Assets/StreamingAssets/crashlytics-build.properties
+
diff --git a/vendor/gitignore/VisualStudio.gitignore b/vendor/gitignore/VisualStudio.gitignore
index 4d13c54854e..4ba92b04afb 100644
--- a/vendor/gitignore/VisualStudio.gitignore
+++ b/vendor/gitignore/VisualStudio.gitignore
@@ -20,6 +20,8 @@
[Rr]eleases/
x64/
x86/
+[Aa][Rr][Mm]/
+[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
@@ -229,6 +231,8 @@ orleans.codegen.cs
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
+# ASP.NET Core default setup: bower directory is configured as wwwroot/lib/ and bower restore is true
+**/wwwroot/lib/
# RIA/Silverlight projects
Generated_Code/
diff --git a/vendor/licenses.csv b/vendor/licenses.csv
index f6fd1efaa83..d706d76358a 100644
--- a/vendor/licenses.csv
+++ b/vendor/licenses.csv
@@ -67,10 +67,15 @@
@babel/template,7.1.2,MIT
@babel/traverse,7.1.0,MIT
@babel/types,7.1.2,MIT
-@gitlab/svgs,1.35.0,MIT
-@gitlab/ui,1.11.0,MIT
+@gitlab/csslab,1.8.0,MIT
+@gitlab/svgs,1.41.0,MIT
+@gitlab/ui,1.15.0,MIT
@sindresorhus/is,0.7.0,MIT
+@types/async,2.0.50,MIT
@types/jquery,2.0.48,MIT
+@types/node,10.12.9,MIT
+@types/semver,5.5.0,MIT
+@types/zen-observable,0.8.0,MIT
@vue/component-compiler-utils,2.2.0,MIT
@webassemblyjs/ast,1.7.6,MIT
@webassemblyjs/floating-point-hex-parser,1.7.6,MIT
@@ -98,13 +103,14 @@ accepts,1.3.5,MIT
ace-rails-ap,4.1.2,MIT
acorn,5.7.3,MIT
acorn-dynamic-import,3.0.0,MIT
-actionmailer,4.2.10,MIT
-actionpack,4.2.10,MIT
-actionview,4.2.10,MIT
-activejob,4.2.10,MIT
-activemodel,4.2.10,MIT
-activerecord,4.2.10,MIT
-activesupport,4.2.10,MIT
+actioncable,5.0.7,MIT
+actionmailer,5.0.7,MIT
+actionpack,5.0.7,MIT
+actionview,5.0.7,MIT
+activejob,5.0.7,MIT
+activemodel,5.0.7,MIT
+activerecord,5.0.7,MIT
+activesupport,5.0.7,MIT
acts-as-taggable-on,5.0.0,MIT
addressable,2.5.2,Apache 2.0
aes_key_wrap,1.0.1,MIT
@@ -119,24 +125,35 @@ ansi-regex,3.0.0,MIT
ansi-styles,2.2.1,MIT
ansi-styles,3.2.1,MIT
anymatch,2.0.0,ISC
+apollo-boost,0.1.20,MIT
+apollo-cache,1.1.20,MIT
+apollo-cache-inmemory,1.3.9,MIT
+apollo-client,2.4.5,MIT
+apollo-link,1.2.3,MIT
+apollo-link-dedup,1.0.10,MIT
+apollo-link-error,1.1.1,MIT
+apollo-link-http,1.5.5,MIT
+apollo-link-http-common,0.2.5,MIT
+apollo-link-state,0.4.2,MIT
+apollo-utilities,1.0.25,MIT
aproba,1.2.0,ISC
are-we-there-yet,1.1.4,ISC
-arel,6.0.4,MIT
+arel,7.1.4,MIT
arr-diff,4.0.0,MIT
arr-flatten,1.1.0,MIT
arr-union,3.1.0,MIT
array-flatten,1.1.1,MIT
array-uniq,1.0.3,MIT
array-unique,0.3.2,MIT
-asana,0.6.0,MIT
-asciidoctor,1.5.6.2,MIT
+asana,0.8.1,MIT
+asciidoctor,1.5.8,MIT
asciidoctor-plantuml,0.0.8,MIT
asn1.js,4.10.1,MIT
assert,1.4.1,MIT
assign-symbols,1.0.0,MIT
async-each,1.0.1,MIT
async-limiter,1.0.0,MIT
-atob,2.0.3,(MIT OR Apache-2.0)
+atob,2.1.2,(MIT OR Apache-2.0)
atomic,1.1.99,Apache 2.0
attr_encrypted,3.1.0,MIT
attr_required,1.0.0,MIT
@@ -147,12 +164,13 @@ babel-code-frame,6.26.0,MIT
babel-loader,8.0.4,MIT
babel-polyfill,6.23.0,MIT
babel-runtime,6.26.0,MIT
+babel-standalone,6.26.0,MIT
babosa,1.0.2,MIT
balanced-match,1.0.0,MIT
base,0.11.2,MIT
base32,0.3.2,MIT
base64-js,1.2.3,MIT
-batch-loader,1.2.1,MIT
+batch-loader,1.2.2,MIT
bcrypt,3.1.12,MIT
bcrypt_pbkdf,1.0.0,MIT
bfj,6.1.1,MIT
@@ -163,7 +181,7 @@ bindata,2.4.3,ruby
bluebird,3.5.1,MIT
bn.js,4.11.8,MIT
body-parser,1.18.2,MIT
-bootstrap,4.1.1,MIT
+bootstrap,4.1.3,MIT
bootstrap-vue,2.0.0-rc.11,MIT
bootstrap_form,2.7.0,MIT
brace-expansion,1.1.11,MIT
@@ -216,7 +234,7 @@ clipboard,1.7.1,MIT
cliui,4.0.0,ISC
clone-response,1.0.2,MIT
code-point-at,1.1.0,MIT
-codesandbox-api,0.0.18,MIT
+codesandbox-api,0.0.20,MIT
codesandbox-import-util-types,1.2.11,LGPL
codesandbox-import-utils,1.2.11,LGPL
coercible,1.0.0,MIT
@@ -224,14 +242,15 @@ collection-visit,1.0.0,MIT
color-convert,1.9.3,MIT
color-name,1.1.3,MIT
commander,2.13.0,MIT
-commander,2.18.0,MIT
+commander,2.19.0,MIT
commondir,1.0.1,MIT
commonmarker,0.17.13,MIT
component-emitter,1.2.1,MIT
compression-webpack-plugin,2.0.0,MIT
concat-map,0.0.1,MIT
concat-stream,1.6.2,MIT
-concurrent-ruby-ext,1.0.5,MIT
+concurrent-ruby-ext,1.1.3,MIT
+config-chain,1.1.12,MIT
connection_pool,2.2.2,MIT
console-browserify,1.1.0,MIT
console-control-strings,1.1.0,ISC
@@ -244,6 +263,7 @@ cookie,0.3.1,MIT
cookie-signature,1.0.6,MIT
copy-concurrently,1.0.5,ISC
copy-descriptor,0.1.1,MIT
+copy-to-clipboard,3.0.8,MIT
core-js,2.3.0,MIT
core-js,2.5.7,MIT
core-util-is,1.0.2,MIT
@@ -262,7 +282,6 @@ css-selector-tokenizer,0.7.0,MIT
css_parser,1.5.0,MIT
cssesc,0.1.0,MIT
cyclist,0.2.2,MIT*
-d3,4.12.2,New BSD
d3,4.13.0,New BSD
d3-array,1.2.1,New BSD
d3-axis,1.0.8,New BSD
@@ -275,7 +294,6 @@ d3-drag,1.2.1,New BSD
d3-dsv,1.0.8,New BSD
d3-ease,1.0.3,New BSD
d3-force,1.1.0,New BSD
-d3-format,1.2.1,New BSD
d3-format,1.2.2,New BSD
d3-geo,1.9.1,New BSD
d3-hierarchy,1.1.5,New BSD
@@ -287,7 +305,6 @@ d3-queue,3.0.7,New BSD
d3-random,1.1.0,New BSD
d3-request,1.0.6,New BSD
d3-scale,1.0.7,New BSD
-d3-selection,1.2.0,New BSD
d3-selection,1.3.0,New BSD
d3-shape,1.2.0,New BSD
d3-time,1.0.8,New BSD
@@ -302,7 +319,7 @@ date-now,0.1.4,MIT
dateformat,3.0.3,MIT
de-indent,1.0.2,MIT
debug,2.6.9,MIT
-debug,3.2.5,MIT
+debug,3.2.6,MIT
debugger-ruby_core_source,1.3.8,MIT
decamelize,2.0.0,MIT
deckar01-task_list,2.0.0,MIT
@@ -310,8 +327,7 @@ declarative,0.0.10,MIT
declarative-option,0.1.0,MIT
decode-uri-component,0.2.0,MIT
decompress-response,3.3.0,MIT
-deep-extend,0.4.2,MIT
-default_value_for,3.0.2,MIT
+deep-extend,0.6.0,MIT
define-properties,1.1.3,MIT
define-property,0.2.5,MIT
define-property,1.0.0,MIT
@@ -330,6 +346,7 @@ devise-two-factor,3.0.0,MIT
diff,3.5.0,New BSD
diffie-hellman,5.0.2,MIT
diffy,3.1.0,MIT
+discordrb-webhooks-blackst0ne,3.3.0,MIT
document-register-element,1.3.0,MIT
dom-serializer,0.1.0,MIT
domain-browser,1.1.7,MIT
@@ -345,8 +362,10 @@ dropzone,4.2.0,MIT
duplexer,0.1.1,MIT
duplexer3,0.1.4,New BSD
duplexify,3.5.3,MIT
+echarts,4.2.0-rc.2,Apache 2.0
ed25519,1.2.4,MIT
editions,1.3.4,MIT
+editorconfig,0.15.2,MIT
ee-first,1.1.1,MIT
ejs,2.6.1,Apache 2.0
electron-to-chromium,1.3.73,ISC
@@ -368,7 +387,7 @@ es-to-primitive,1.1.1,MIT
es6-promise,3.0.2,MIT
escape-html,1.0.3,MIT
escape-string-regexp,1.0.5,MIT
-escape_utils,1.1.1,MIT
+escape_utils,1.2.1,MIT
escaper,2.5.3,MIT
eslint-scope,4.0.0,Simplified BSD
esrecurse,4.2.1,Simplified BSD
@@ -447,9 +466,10 @@ get-value,2.0.6,MIT
get_process_mem,0.2.0,MIT
gettext_i18n_rails,1.8.0,MIT
gettext_i18n_rails_js,1.3.0,MIT
-gitaly-proto,0.123.0,MIT
+gitaly-proto,1.3.0,MIT
github-markup,1.7.0,MIT
-gitlab-markup,1.6.4,MIT
+gitlab-default_value_for,3.1.1,MIT
+gitlab-markup,1.6.5,MIT
gitlab-sidekiq-fetcher,0.3.0,LGPL
gitlab_omniauth-ldap,2.0.4,MIT
glob,7.1.3,ISC
@@ -464,7 +484,7 @@ google-protobuf,3.6.1,New BSD
googleapis-common-protos-types,1.0.2,Apache 2.0
googleauth,0.6.6,Apache 2.0
got,8.3.0,MIT
-gpgme,2.0.13,LGPL-2.1+
+gpgme,2.0.18,LGPL-2.1+
graceful-fs,4.1.11,ISC
grape,1.1.0,MIT
grape-entity,0.7.1,MIT
@@ -473,6 +493,9 @@ grape_logging,1.7.0,MIT
graphiql-rails,1.4.10,MIT
graphlibrary,2.2.0,MIT
graphql,1.8.1,MIT
+graphql,14.0.2,MIT
+graphql-anywhere,4.1.22,MIT
+graphql-tag,2.10.0,MIT
grpc,1.15.0,Apache 2.0
gzip-size,5.0.0,MIT
hamlit,2.8.8,MIT
@@ -496,6 +519,7 @@ hashie,3.5.7,MIT
hashie-forbidden_attributes,0.1.1,MIT
he,1.1.1,MIT
health_check,2.6.0,MIT
+highlight.js,9.13.1,New BSD
hipchat,1.5.2,MIT
hmac-drbg,1.0.1,MIT
hoopy,0.1.4,MIT
@@ -503,16 +527,16 @@ html-pipeline,2.8.4,MIT
html2text,0.2.0,MIT
htmlentities,4.3.4,MIT
htmlparser2,3.9.2,MIT
-http,2.2.2,MIT
+http,3.3.0,MIT
http-cache-semantics,3.8.1,Simplified BSD
http-cookie,1.0.3,MIT
http-errors,1.6.2,MIT
-http-form_data,1.0.3,MIT
+http-form_data,2.1.1,MIT
http_parser.rb,0.6.0,MIT
httparty,0.13.7,MIT
httpclient,2.8.3,ruby
https-browserify,1.0.0,MIT
-i18n,0.9.5,MIT
+i18n,1.1.1,MIT
icalendar,2.4.1,ruby
ice_nine,0.11.2,MIT
iconv-lite,0.4.19,MIT
@@ -523,6 +547,7 @@ ieee754,1.1.11,New BSD
iferr,0.1.5,MIT
ignore-walk,3.0.1,ISC
immediate,3.0.6,MIT
+immutable-tuple,0.4.9,MIT
import-local,1.0.0,MIT
imports-loader,0.8.0,MIT
imurmurhash,0.1.4,MIT
@@ -578,12 +603,14 @@ isobject,2.1.0,MIT
isobject,3.0.1,MIT
istextorbinary,2.2.1,MIT
isurl,1.0.0,MIT
+iterall,1.2.2,MIT
jed,1.1.1,MIT
jira-ruby,1.4.1,MIT
jquery,3.3.1,MIT
jquery-atwho-rails,1.3.2,MIT
jquery-ujs,1.2.2,MIT
jquery.waitforimages,2.2.0,MIT
+js-beautify,1.8.8,MIT
js-cookie,2.1.3,MIT
js-levenshtein,1.1.4,MIT
js-tokens,3.0.2,MIT
@@ -611,7 +638,7 @@ kind-of,3.2.2,MIT
kind-of,4.0.0,MIT
kind-of,5.1.0,MIT
kind-of,6.0.2,MIT
-kubeclient,3.1.0,MIT
+kubeclient,4.0.0,MIT
lazy-cache,2.0.2,MIT
lcid,2.0.0,MIT
licensee,8.9.2,MIT
@@ -631,7 +658,7 @@ lodash.isequal,4.5.0,MIT
lodash.mergewith,4.6.0,MIT
lodash.startcase,4.4.0,MIT
lograge,0.10.0,MIT
-loofah,2.2.2,MIT
+loofah,2.2.3,MIT
loose-envify,1.4.0,MIT
lowercase-keys,1.0.0,MIT
lru-cache,4.1.3,ISC
@@ -653,17 +680,17 @@ memory-fs,0.4.1,MIT
merge-descriptors,1.0.1,MIT
merge-source-map,1.1.0,MIT
mermaid,8.0.0-rc.8,MIT
-method_source,0.9.0,MIT
+method_source,0.9.2,MIT
methods,1.1.2,MIT
micromatch,3.1.10,MIT
miller-rabin,4.0.1,MIT
mime,1.4.1,MIT
mime,2.3.1,MIT
-mime-db,1.33.0,MIT
-mime-types,2.1.18,MIT
-mime-types,3.1,MIT
-mime-types-data,3.2016.0521,MIT
-mimemagic,0.3.0,MIT
+mime-db,1.37.0,MIT
+mime-types,2.1.21,MIT
+mime-types,3.2.2,MIT
+mime-types-data,3.2018.0812,MIT
+mimemagic,0.3.2,MIT
mimic-fn,1.1.0,MIT
mimic-response,1.0.0,MIT
mini_magick,4.8.0,MIT
@@ -695,20 +722,22 @@ mustermann,1.0.3,MIT
mustermann-grape,1.0.0,MIT
mute-stream,0.0.7,ISC
mysql2,0.4.10,MIT
+nakayoshi_fork,0.0.4,MIT
nan,2.10.0,MIT
nanomatch,1.2.9,MIT
-needle,2.2.1,MIT
+needle,2.2.4,MIT
negotiator,0.6.1,MIT
neo-async,2.5.0,MIT
net-ldap,0.16.0,MIT
net-ssh,5.0.1,MIT
netrc,0.11.0,MIT
nice-try,1.0.4,MIT
+nio4r,2.3.1,MIT
node-fetch,1.6.3,MIT
node-libs-browser,2.1.0,MIT
-node-pre-gyp,0.10.0,New BSD
+node-pre-gyp,0.10.3,New BSD
node-releases,1.0.0-alpha.12,CC-BY-4.0
-nokogiri,1.8.4,MIT
+nokogiri,1.8.5,MIT
nokogumbo,1.5.0,Apache 2.0
nopt,4.0.1,ISC
normalize-path,2.1.1,MIT
@@ -752,6 +781,7 @@ onetime,2.0.1,MIT
opencollective,1.0.3,MIT
opener,1.5.1,(WTFPL OR MIT)
opn,4.0.2,MIT
+optimism,0.6.8,MIT
org-ruby,0.9.12,MIT
orm_adapter,0.5.0,MIT
os,1.0.0,MIT
@@ -818,6 +848,7 @@ process-nextick-args,1.0.7,MIT
process-nextick-args,2.0.0,MIT
prometheus-client-mmap,0.9.4,Apache 2.0
promise-inflight,1.0.1,ISC
+proto-list,1.2.4,ISC
proxy-addr,2.0.4,MIT
prr,1.0.1,MIT
pseudomap,1.0.2,ISC
@@ -835,20 +866,20 @@ qs,6.5.1,New BSD
query-string,5.1.1,MIT
querystring,0.2.0,MIT
querystring-es3,0.2.1,MIT
-rack,1.6.10,MIT
+rack,2.0.6,MIT
rack-accept,0.4.5,MIT
rack-attack,4.4.1,MIT
rack-cors,1.0.2,MIT
rack-oauth2,1.2.3,MIT
-rack-protection,2.0.3,MIT
+rack-protection,2.0.4,MIT
rack-proxy,0.6.0,MIT
rack-test,0.6.3,MIT
-rails,4.2.10,MIT
+rails,5.0.7,MIT
rails-deprecated_sanitizer,1.0.3,MIT
-rails-dom-testing,1.0.9,MIT
+rails-dom-testing,2.0.3,MIT
rails-html-sanitizer,1.0.4,MIT
-rails-i18n,4.0.9,MIT
-railties,4.2.10,MIT
+rails-i18n,5.1.1,MIT
+railties,5.0.7,MIT
rainbow,3.0.0,MIT
raindrops,0.18.0,LGPL-2.1+
rake,12.3.1,MIT
@@ -862,7 +893,7 @@ raw-loader,0.5.1,MIT
rb-fsevent,0.10.2,MIT
rb-inotify,0.9.10,MIT
rbtrace,0.4.10,MIT
-rc,1.2.5,(BSD-2-Clause OR MIT OR Apache-2.0)
+rc,1.2.8,(BSD-2-Clause OR MIT OR Apache-2.0)
rdoc,6.0.4,ruby
re2,1.1.1,New BSD
readable-stream,2.0.6,MIT
@@ -877,7 +908,7 @@ redis-activesupport,5.0.4,MIT
redis-namespace,1.6.0,MIT
redis-rack,2.0.4,MIT
redis-rails,5.0.2,MIT
-redis-store,1.4.1,MIT
+redis-store,1.6.0,MIT
regenerate,1.4.0,MIT
regenerate-unicode-properties,7.0.0,MIT
regenerator-runtime,0.10.5,MIT
@@ -920,7 +951,7 @@ ruby-fogbugz,0.2.1,MIT
ruby-prof,0.17.0,Simplified BSD
ruby-progressbar,1.9.0,MIT
ruby-saml,1.7.2,MIT
-ruby_parser,3.9.0,MIT
+ruby_parser,3.11.0,MIT
rubyntlm,0.6.2,MIT
rubypants,0.2.0,BSD
rufus-scheduler,3.4.0,MIT
@@ -949,9 +980,9 @@ seed-fu,2.3.7,MIT
select,1.1.2,MIT
select2,3.5.2-browserify,Apache*
select2-rails,3.5.9.3,MIT
-semver,5.5.1,ISC
+semver,5.6.0,ISC
send,0.16.2,MIT
-sentry-raven,2.7.2,Apache 2.0
+sentry-raven,2.7.4,Apache 2.0
serialize-javascript,1.4.0,New BSD
serve-static,1.13.2,MIT
set-blocking,2.0.0,ISC
@@ -963,18 +994,19 @@ setimmediate,1.0.5,MIT
setprototypeof,1.0.3,ISC
setprototypeof,1.1.0,ISC
settingslogic,2.0.9,MIT
-sexp_processor,4.9.0,MIT
+sexp_processor,4.11.0,MIT
sha.js,2.4.10,MIT
sha1,1.1.1,New BSD
shebang-command,1.2.0,MIT
shebang-regex,1.0.0,MIT
-sidekiq,5.2.1,LGPL
+sidekiq,5.2.3,LGPL
sidekiq-cron,0.6.0,MIT
+sigmund,1.0.1,ISC
signal-exit,3.0.2,ISC
signet,0.11.0,Apache 2.0
slack-notifier,1.5.1,MIT
slugify,1.3.1,MIT
-smooshpack,0.0.48,LGPL
+smooshpack,0.0.53,LGPL
snapdragon,0.8.1,MIT
snapdragon-node,2.1.1,MIT
snapdragon-util,3.0.1,MIT
@@ -1018,6 +1050,7 @@ style-loader,0.23.0,MIT
supports-color,2.0.0,MIT
supports-color,5.5.0,MIT
svg4everybody,2.1.9,CC0-1.0
+symbol-observable,1.2.0,MIT
sys-filesystem,1.1.6,Artistic 2.0
tapable,1.1.0,MIT
tar,4.4.4,ISC
@@ -1029,6 +1062,7 @@ thread_safe,0.3.6,Apache 2.0
three,0.84.0,MIT
three-orbit-controls,82.1.0,MIT
three-stl-loader,1.0.4,MIT
+throttle-debounce,2.0.1,MIT
through,2.3.8,MIT
through2,2.0.3,MIT
tilt,2.0.8,MIT
@@ -1043,6 +1077,7 @@ to-fast-properties,2.0.0,MIT
to-object-path,0.3.0,MIT
to-regex,3.0.2,MIT
to-regex-range,2.1.1,MIT
+toggle-selection,1.0.6,MIT
toml-rb,1.0.0,MIT
trim-right,1.0.1,MIT
trollop,2.1.3,MIT
@@ -1079,6 +1114,7 @@ urix,0.1.0,MIT
url,0.11.0,MIT
url-loader,1.1.1,MIT
url-parse-lax,3.0.0,MIT
+url-search-params-polyfill,5.0.0,MIT
url-to-options,1.0.1,MIT
use,2.0.2,MIT
util,0.10.3,MIT
@@ -1094,6 +1130,7 @@ visibilityjs,1.2.4,MIT
vm-browserify,0.0.4,MIT
vmstat,2.3.0,MIT
vue,2.5.17,MIT
+vue-apollo,3.0.0-beta.25,ISC
vue-functional-data-merge,2.0.6,MIT
vue-hot-reload-api,2.3.0,MIT
vue-loader,15.4.2,MIT
@@ -1112,6 +1149,8 @@ webpack-cli,3.1.0,MIT
webpack-rails,0.9.11,MIT
webpack-sources,1.3.0,MIT
webpack-stats-plugin,0.2.1,MIT
+websocket-driver,0.6.5,MIT
+websocket-extensions,0.1.3,MIT
which,1.3.0,ISC
which-module,2.0.0,ISC
wide-align,1.1.2,ISC
@@ -1131,3 +1170,6 @@ yallist,2.1.2,ISC
yallist,3.0.2,ISC
yargs,12.0.2,MIT
yargs-parser,10.1.0,ISC
+zen-observable,0.8.11,MIT
+zen-observable-ts,0.8.10,MIT
+zrender,4.0.5,New BSD
diff --git a/yarn.lock b/yarn.lock
index d4906a6a212..a6b43f785dc 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -616,32 +616,40 @@
lodash "^4.17.10"
to-fast-properties "^2.0.0"
-"@gitlab/eslint-config@^1.2.0":
- version "1.2.0"
- resolved "https://registry.yarnpkg.com/@gitlab/eslint-config/-/eslint-config-1.2.0.tgz#115568a70edabbc024f1bc13ba1ba499a9ba05a9"
- integrity sha512-TnZO5T7JjLQjw30aIGtKIsAX4pRnSbqOir3Ji5zPwtCVWY53DnG6Lcesgy7WYdsnnkt3oQPXFTOZlkymUs2PsA==
+"@gitlab/csslab@^1.8.0":
+ version "1.8.0"
+ resolved "https://registry.yarnpkg.com/@gitlab/csslab/-/csslab-1.8.0.tgz#54a2457fdc80f006665f0e578a5532780954ccfa"
+ integrity sha512-RZylRElufH1kwsBQlIDaVcrcXMyD5IEGrU6ABUd8W3LG8/F9jJ4Y3Ys7EPTpK/qFJyx86AutTtFGRxRNlMx85w==
+ dependencies:
+ bootstrap "4.1.3"
+
+"@gitlab/eslint-config@^1.4.0":
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/@gitlab/eslint-config/-/eslint-config-1.4.0.tgz#2e59e55a7cd024e3a450d2a896060ec4d763a5dc"
+ integrity sha512-nkecTWRNS/KD9q5lHFSc3J6zO/g1/OV9DaKiay+0nLjnGO9jQVRArRIYpnzgbUz2p15jOMVToVafW0YbbHZkwg==
dependencies:
babel-eslint "^10.0.1"
eslint-config-airbnb-base "^13.1.0"
- eslint-config-prettier "^3.1.0"
+ eslint-config-prettier "^3.3.0"
eslint-plugin-filenames "^1.3.2"
eslint-plugin-import "^2.14.0"
eslint-plugin-promise "^4.0.1"
- eslint-plugin-vue "^5.0.0-beta.3"
+ eslint-plugin-vue "^5.0.0"
-"@gitlab/svgs@^1.40.0":
- version "1.41.0"
- resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.41.0.tgz#f80e3a0e259f3550af00685556ea925e471276d3"
- integrity sha512-tKUXyqe54efWBsjQBUcvNF0AvqmE2NI2No3Bnix/gKDRImzIlcgIkM67Y8zoJv1D0w4CO87WcaG5GLpIFIT1Pg==
+"@gitlab/svgs@^1.42.0":
+ version "1.42.0"
+ resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.42.0.tgz#54eb88606bb79b74373a3aa49d8c10557fb1fd7a"
+ integrity sha512-mVm1kyV/M1fTbQcW8Edbk7BPT2syQf+ot9qwFzLFiFXAn3jXTi6xy+DS+0cgoTnglSUsXVl4qcVAQjt8YoOOOQ==
-"@gitlab/ui@^1.11.0":
- version "1.11.0"
- resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-1.11.0.tgz#b771c2c3d627cf9efbe98c71ee5739624f2ff51f"
- integrity sha512-hGMHM45kcv9725R6G+n/HxvF3KfVb9oBGRNf1+4n3xAGmtXJ2NlPdIXIsDaye3EeVF9PTOtjLuaqrcp6AGNqZg==
+"@gitlab/ui@^1.15.0":
+ version "1.15.0"
+ resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-1.15.0.tgz#288e189cb99de354aeb4598f9ac8cced5f47e139"
+ integrity sha512-Aiv/WABr8lBVJk0eoanSoO07Lr5Nnvuq82IjDnNzcw9enB1DAKvlstC2r9iiMfg1pVgV/uLdDeRFqH9eI1X4Rg==
dependencies:
babel-standalone "^6.26.0"
bootstrap-vue "^2.0.0-rc.11"
copy-to-clipboard "^3.0.8"
+ echarts "^4.2.0-rc.2"
highlight.js "^9.13.1"
js-beautify "^1.8.8"
lodash "^4.17.11"
@@ -698,6 +706,16 @@
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-5.5.0.tgz#146c2a29ee7d3bae4bf2fcb274636e264c813c45"
integrity sha512-41qEJgBH/TWgo5NFSvBCJ1qkoi3Q6ONSF2avrHq1LVEZfYpdHmj0y9SuTK+u9ZhG1sYQKBL1AWXKyLWP4RaUoQ==
+"@types/strip-bom@^3.0.0":
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/@types/strip-bom/-/strip-bom-3.0.0.tgz#14a8ec3956c2e81edb7520790aecf21c290aebd2"
+ integrity sha1-FKjsOVbC6B7bdSB5CuzyHCkK69I=
+
+"@types/strip-json-comments@0.0.30":
+ version "0.0.30"
+ resolved "https://registry.yarnpkg.com/@types/strip-json-comments/-/strip-json-comments-0.0.30.tgz#9aa30c04db212a9a0649d6ae6fd50accc40748a1"
+ integrity sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ==
+
"@types/zen-observable@^0.8.0":
version "0.8.0"
resolved "https://registry.yarnpkg.com/@types/zen-observable/-/zen-observable-0.8.0.tgz#8b63ab7f1aa5321248aad5ac890a485656dcea4d"
@@ -922,6 +940,11 @@ acorn-jsx@^4.1.1:
dependencies:
acorn "^5.0.3"
+acorn-jsx@^5.0.0:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.0.1.tgz#32a064fd925429216a09b141102bfdd185fae40e"
+ integrity sha512-HJ7CfNHrfJLlNTzIEUTj43LNWGkqpRLxm3YjAlcD0ACydk9XynzYsCBHxut+iqt+1aBXkx9UP/w/ZqMr13XIzg==
+
acorn-walk@^6.0.1:
version "6.1.1"
resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-6.1.1.tgz#d363b66f5fac5f018ff9c3a1e7b6f8e310cc3913"
@@ -932,7 +955,7 @@ acorn@^5.0.0, acorn@^5.0.3, acorn@^5.5.3, acorn@^5.6.0, acorn@^5.6.2, acorn@^5.7
resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.3.tgz#67aa231bf8812974b85235a96771eb6bd07ea279"
integrity sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==
-acorn@^6.0.1:
+acorn@^6.0.1, acorn@^6.0.2:
version "6.0.4"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.0.4.tgz#77377e7353b72ec5104550aa2d2097a2fd40b754"
integrity sha512-VY4i5EKSKkofY2I+6QLTbTTN/UvEQPCo6eiwzzSaSWfpaDhOmStMCMod6wmuPciNq+XS0faCglFu2lHZpdHUtg==
@@ -947,12 +970,12 @@ ajv-errors@^1.0.0:
resolved "https://registry.yarnpkg.com/ajv-errors/-/ajv-errors-1.0.0.tgz#ecf021fa108fd17dfb5e6b383f2dd233e31ffc59"
integrity sha1-7PAh+hCP0X37Xms4Py3SM+Mf/Fk=
-ajv-keywords@^3.0.0, ajv-keywords@^3.1.0:
+ajv-keywords@^3.1.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.2.0.tgz#e86b819c602cf8821ad637413698f1dec021847a"
integrity sha1-6GuBnGAs+IIa1jdBNpjx3sAhhHo=
-ajv@^6.0.1, ajv@^6.1.0, ajv@^6.5.3:
+ajv@^6.1.0, ajv@^6.5.3:
version "6.5.3"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.5.3.tgz#71a569d189ecf4f4f321224fecb166f071dd90f9"
integrity sha512-LqZ9wY+fx3UMiiPd741yB2pj3hhil+hQc8taf4o2QGRFpWgZ2V5C8HA165DY9sS3fJwsk7uT7ZlFEyC3Ig3lLg==
@@ -972,6 +995,16 @@ ajv@^6.5.5:
json-schema-traverse "^0.4.1"
uri-js "^4.2.2"
+ajv@^6.6.1:
+ version "6.6.1"
+ resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.6.1.tgz#6360f5ed0d80f232cc2b294c362d5dc2e538dd61"
+ integrity sha512-ZoJjft5B+EJBjUyu9C9Hc0OZyPZSSlOF+plzouTrg6UlA8f+e/n8NIgBFG/9tppJtpPWfthHakK7juJdNDODww==
+ dependencies:
+ fast-deep-equal "^2.0.1"
+ fast-json-stable-stringify "^2.0.0"
+ json-schema-traverse "^0.4.1"
+ uri-js "^4.2.2"
+
amdefine@>=0.0.4:
version "1.0.1"
resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5"
@@ -1309,7 +1342,7 @@ asynckit@^0.4.0:
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
integrity sha1-x57Zf380y48robyXkLzDZkdLS3k=
-atob@^2.0.0:
+atob@^2.0.0, atob@^2.1.1:
version "2.1.2"
resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9"
integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==
@@ -1483,7 +1516,7 @@ babel-plugin-syntax-object-rest-spread@^6.13.0:
resolved "https://registry.yarnpkg.com/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz#fd6536f2bce13836ffa3a5458c4903a597bb3bf5"
integrity sha1-/WU28rzhODb/o6VFjEkDpZe7O/U=
-babel-plugin-transform-es2015-modules-commonjs@^6.26.2:
+babel-plugin-transform-es2015-modules-commonjs@^6.26.0, babel-plugin-transform-es2015-modules-commonjs@^6.26.2:
version "6.26.2"
resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.26.2.tgz#58a793863a9e7ca870bdc5a881117ffac27db6f3"
integrity sha512-CV9ROOHEdrjcwhIaJNBGMBCodN+1cfkwtM1SbUHmvyy35KGT7fohbpOxkE2uLz1o6odKK2Ck/tz47z+VqQfi9Q==
@@ -2195,6 +2228,11 @@ clone-response@1.0.2:
dependencies:
mimic-response "^1.0.0"
+clone@2.x:
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f"
+ integrity sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=
+
co@^4.6.0:
version "4.6.0"
resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
@@ -2350,7 +2388,7 @@ concat-stream@^1.5.0:
readable-stream "^2.2.2"
typedarray "^0.0.6"
-config-chain@~1.1.5:
+config-chain@^1.1.12, config-chain@~1.1.5:
version "1.1.12"
resolved "https://registry.yarnpkg.com/config-chain/-/config-chain-1.1.12.tgz#0fde8d091200eb5e808caf25fe618c02f48e4efa"
integrity sha512-a1eOIcu8+7lUInge4Rpf/n4Krkf3Dd9lqhljRzII1/Zno/kRtUWnznPO3jOKBmTEktkt3fkxisUcivoj0ebzoA==
@@ -2603,6 +2641,16 @@ css-selector-tokenizer@^0.7.0:
fastparse "^1.1.1"
regexpu-core "^1.0.0"
+css@^2.1.0:
+ version "2.2.4"
+ resolved "https://registry.yarnpkg.com/css/-/css-2.2.4.tgz#c646755c73971f2bba6a601e2cf2fd71b1298929"
+ integrity sha512-oUnjmWpy0niI3x/mPL8dVEI1l7MnG3+HHyRPHf+YFSbK+svOhXpmSOcDURUh2aOCgl2grzrOPt1nHLuCVFULLw==
+ dependencies:
+ inherits "^2.0.3"
+ source-map "^0.6.1"
+ source-map-resolve "^0.5.2"
+ urix "^0.1.0"
+
cssesc@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-0.1.0.tgz#c814903e45623371a0477b40109aaafbeeaddbb4"
@@ -2937,6 +2985,13 @@ debug@^3.1.0, debug@^3.2.5:
dependencies:
ms "^2.1.1"
+debug@^4.0.1, debug@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.0.tgz#373687bffa678b38b1cd91f861b63850035ddc87"
+ integrity sha512-heNPJUJIqC+xB6ayLAMHaIrmN9HKa7aQO8MGqKpvCA+uJYVcvR6l5kgdrhRuwPFHU7P5/A1w0BjByPHwpfTDKg==
+ dependencies:
+ ms "^2.1.1"
+
debug@~3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
@@ -3278,12 +3333,19 @@ ecc-jsbn@~0.1.1:
jsbn "~0.1.0"
safer-buffer "^2.1.0"
+echarts@^4.2.0-rc.2:
+ version "4.2.0-rc.2"
+ resolved "https://registry.yarnpkg.com/echarts/-/echarts-4.2.0-rc.2.tgz#6a98397aafa81b65cbf0bc15d9afdbfb244df91e"
+ integrity sha512-5Y4Kyi4eNsRM9Cnl7Q8C6PFVjznBJv1VIiMm/VSQ9zyqeo+ce1695GqUd9v4zfVx+Ow1gnwMJX67h0FNvarScw==
+ dependencies:
+ zrender "4.0.5"
+
editions@^1.3.3:
version "1.3.4"
resolved "https://registry.yarnpkg.com/editions/-/editions-1.3.4.tgz#3662cb592347c3168eb8e498a0ff73271d67f50b"
integrity sha512-gzao+mxnYDzIysXKMQi/+M1mjy/rjestjg6OPoYTtI+3Izp23oiGZitsl9lPDPiTGXbcSIk1iJWhliSaglxnUg==
-editorconfig@^0.15.0:
+editorconfig@^0.15.0, editorconfig@^0.15.2:
version "0.15.2"
resolved "https://registry.yarnpkg.com/editorconfig/-/editorconfig-0.15.2.tgz#047be983abb9ab3c2eefe5199cb2b7c5689f0702"
integrity sha512-GWjSI19PVJAM9IZRGOS+YKI8LN+/sjkSjNyvxL5ucqP9/IqtYNXBaQ/6c/hkPNYQHyOHra2KoXZI/JVpuqwmcQ==
@@ -3507,10 +3569,10 @@ eslint-config-airbnb-base@^13.1.0:
object.assign "^4.1.0"
object.entries "^1.0.4"
-eslint-config-prettier@^3.1.0:
- version "3.1.0"
- resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-3.1.0.tgz#2c26d2cdcfa3a05f0642cd7e6e4ef3316cdabfa2"
- integrity sha512-QYGfmzuc4q4J6XIhlp8vRKdI/fI0tQfQPy1dME3UOLprE+v4ssH/3W9LM2Q7h5qBcy5m0ehCrBDU2YF8q6OY8w==
+eslint-config-prettier@^3.3.0:
+ version "3.3.0"
+ resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-3.3.0.tgz#41afc8d3b852e757f06274ed6c44ca16f939a57d"
+ integrity sha512-Bc3bh5bAcKNvs3HOpSi6EfGA2IIp7EzWcg2tS4vP7stnXu/J1opihHDM7jI9JCIckyIDTgZLSWn7J3HY0j2JfA==
dependencies:
get-stdin "^6.0.0"
@@ -3565,12 +3627,12 @@ eslint-plugin-filenames@^1.3.2:
lodash.snakecase "4.1.1"
lodash.upperfirst "4.3.1"
-eslint-plugin-html@4.0.5:
- version "4.0.5"
- resolved "https://registry.yarnpkg.com/eslint-plugin-html/-/eslint-plugin-html-4.0.5.tgz#e8ec7e16485124460f3bff312016feb0a54d9659"
- integrity sha512-yULqYldzhYXTwZEaJXM30HhfgJdtTzuVH3LeoANybESHZ5+2ztLD72BsB2wR124/kk/PvQqZofDFSdNIk+kykw==
+eslint-plugin-html@5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/eslint-plugin-html/-/eslint-plugin-html-5.0.0.tgz#396e30a60dedee0122fe08f11d13c5ab22f20d32"
+ integrity sha512-f7p/7YQdgQUFVAX3nB4dnMQbrDeTalcA01PDhuvTLk0ZadCwM4Pb+639SRuqEf1zMkIxckLY+ckCr0hVP5zl6A==
dependencies:
- htmlparser2 "^3.8.2"
+ htmlparser2 "^3.10.0"
eslint-plugin-import@^2.14.0:
version "2.14.0"
@@ -3603,12 +3665,12 @@ eslint-plugin-promise@^4.0.1:
resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-4.0.1.tgz#2d074b653f35a23d1ba89d8e976a985117d1c6a2"
integrity sha512-Si16O0+Hqz1gDHsys6RtFRrW7cCTB6P7p3OJmKp3Y3dxpQE2qwOA7d3xnV+0mBmrPoi0RBnxlCKvqu70te6wjg==
-eslint-plugin-vue@^5.0.0-beta.3:
- version "5.0.0-beta.3"
- resolved "https://registry.yarnpkg.com/eslint-plugin-vue/-/eslint-plugin-vue-5.0.0-beta.3.tgz#f3fa9f109b76e20fc1e45a71ce7c6d567118924e"
- integrity sha512-EOQo3ax4CIM6Itcl522p4cGlSBgR/KZBJo2Xc29PWknbYH/DRZorGutF8NATUpbZ4HYOG+Gcyd1nL08iyYF3Tg==
+eslint-plugin-vue@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/eslint-plugin-vue/-/eslint-plugin-vue-5.0.0.tgz#4a2cc1c0e71ea45e1bd9c1a60f925bfe68bb5710"
+ integrity sha512-mSv2Ebz3RaPP+XJO/mu7F+SdR9lrMyGISSExnarLFqqf3pF5wTmwWNrhHW1o9zKzKI811UVTIIkWJJvgO6SsUQ==
dependencies:
- vue-eslint-parser "^3.2.1"
+ vue-eslint-parser "^4.0.2"
eslint-restricted-globals@^0.1.1:
version "0.1.1"
@@ -3641,16 +3703,16 @@ eslint-visitor-keys@^1.0.0:
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#3f3180fb2e291017716acb4c9d6d5b5c34a6a81d"
integrity sha512-qzm/XxIbxm/FHyH341ZrbnMUpe+5Bocte9xkmFMzPMjRaZMcXww+MpBptFvtU+79L362nqiLhekCxCxDPaUMBQ==
-eslint@~5.6.0:
- version "5.6.0"
- resolved "https://registry.yarnpkg.com/eslint/-/eslint-5.6.0.tgz#b6f7806041af01f71b3f1895cbb20971ea4b6223"
- integrity sha512-/eVYs9VVVboX286mBK7bbKnO1yamUy2UCRjiY6MryhQL2PaaXCExsCQ2aO83OeYRhU2eCU/FMFP+tVMoOrzNrA==
+eslint@~5.9.0:
+ version "5.9.0"
+ resolved "https://registry.yarnpkg.com/eslint/-/eslint-5.9.0.tgz#b234b6d15ef84b5849c6de2af43195a2d59d408e"
+ integrity sha512-g4KWpPdqN0nth+goDNICNXGfJF7nNnepthp46CAlJoJtC5K/cLu3NgCM3AHu1CkJ5Hzt9V0Y0PBAO6Ay/gGb+w==
dependencies:
"@babel/code-frame" "^7.0.0"
ajv "^6.5.3"
chalk "^2.1.0"
cross-spawn "^6.0.5"
- debug "^3.1.0"
+ debug "^4.0.1"
doctrine "^2.1.0"
eslint-scope "^4.0.0"
eslint-utils "^1.3.1"
@@ -3677,12 +3739,12 @@ eslint@~5.6.0:
path-is-inside "^1.0.2"
pluralize "^7.0.0"
progress "^2.0.0"
- regexpp "^2.0.0"
+ regexpp "^2.0.1"
require-uncached "^1.0.3"
semver "^5.5.1"
strip-ansi "^4.0.0"
strip-json-comments "^2.0.1"
- table "^4.0.3"
+ table "^5.0.2"
text-table "^0.2.0"
espree@^4.0.0:
@@ -3693,6 +3755,15 @@ espree@^4.0.0:
acorn "^5.6.0"
acorn-jsx "^4.1.1"
+espree@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/espree/-/espree-4.1.0.tgz#728d5451e0fd156c04384a7ad89ed51ff54eb25f"
+ integrity sha512-I5BycZW6FCVIub93TeVY1s7vjhP9CY6cXCznIRfiig7nRviKZYdRnj/sHEWC6A7WE9RDWOFq9+7OsWSYz8qv2w==
+ dependencies:
+ acorn "^6.0.2"
+ acorn-jsx "^5.0.0"
+ eslint-visitor-keys "^1.0.0"
+
esprima@2.7.x, esprima@^2.7.1:
version "2.7.3"
resolved "https://registry.yarnpkg.com/esprima/-/esprima-2.7.3.tgz#96e3b70d5779f6ad49cd032673d1c312767ba581"
@@ -3982,6 +4053,13 @@ extglob@^2.0.4:
snapdragon "^0.8.1"
to-regex "^3.0.1"
+extract-from-css@^0.4.4:
+ version "0.4.4"
+ resolved "https://registry.yarnpkg.com/extract-from-css/-/extract-from-css-0.4.4.tgz#1ea7df2e7c7c6eb9922fa08e8adaea486f6f8f92"
+ integrity sha1-HqffLnx8brmSL6COitrqSG9vj5I=
+ dependencies:
+ css "^2.1.0"
+
extsprintf@1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05"
@@ -4126,6 +4204,14 @@ finalhandler@1.1.1:
statuses "~1.4.0"
unpipe "~1.0.0"
+find-babel-config@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/find-babel-config/-/find-babel-config-1.1.0.tgz#acc01043a6749fec34429be6b64f542ebb5d6355"
+ integrity sha1-rMAQQ6Z0n+w0Qpvmtk9ULrtdY1U=
+ dependencies:
+ json5 "^0.5.1"
+ path-exists "^3.0.0"
+
find-cache-dir@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-1.0.0.tgz#9288e3e9e3cc3748717d39eade17cf71fc30ee6f"
@@ -4395,7 +4481,7 @@ glob-parent@^3.1.0:
is-glob "^3.1.0"
path-dirname "^1.0.0"
-"glob@5 - 7", glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2:
+"glob@5 - 7", glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3:
version "7.1.3"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.3.tgz#3960832d3f1574108342dafd3a67b332c0969df1"
integrity sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==
@@ -4759,7 +4845,19 @@ html-entities@^1.2.0:
resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.2.0.tgz#41948caf85ce82fed36e4e6a0ed371a6664379e2"
integrity sha1-QZSMr4XOgv7Tbk5qDtNxpmZDeeI=
-htmlparser2@^3.8.2, htmlparser2@^3.9.0:
+htmlparser2@^3.10.0:
+ version "3.10.0"
+ resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.0.tgz#5f5e422dcf6119c0d983ed36260ce9ded0bee464"
+ integrity sha512-J1nEUGv+MkXS0weHNWVKJJ+UrLfePxRWpN3C9bEi9fLxL2+ggW94DQvgYVXsaT30PGwYRIZKNZXuyMhp3Di4bQ==
+ dependencies:
+ domelementtype "^1.3.0"
+ domhandler "^2.3.0"
+ domutils "^1.5.1"
+ entities "^1.1.1"
+ inherits "^2.0.1"
+ readable-stream "^3.0.6"
+
+htmlparser2@^3.9.0:
version "3.9.2"
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.9.2.tgz#1bdf87acca0f3f9e53fa4fcceb0f4b4cbb00b338"
integrity sha1-G9+HrMoPP55T+k/M6w9LTLsAszg=
@@ -5960,6 +6058,17 @@ jquery.waitforimages@^2.2.0:
resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.3.1.tgz#958ce29e81c9790f31be7792df5d4d95fc57fbca"
integrity sha512-Ubldcmxp5np52/ENotGxlLe6aGMvmF4R8S6tZjsP6Knsaxd/xp3Zrh50cG93lR6nPXyUFwzN3ZSOQI0wRJNdGg==
+js-beautify@^1.6.14:
+ version "1.8.9"
+ resolved "https://registry.yarnpkg.com/js-beautify/-/js-beautify-1.8.9.tgz#08e3c05ead3ecfbd4f512c3895b1cda76c87d523"
+ integrity sha512-MwPmLywK9RSX0SPsUJjN7i+RQY9w/yC17Lbrq9ViEefpLRgqAR2BgrMN2AbifkUuhDV8tRauLhLda/9+bE0YQA==
+ dependencies:
+ config-chain "^1.1.12"
+ editorconfig "^0.15.2"
+ glob "^7.1.3"
+ mkdirp "~0.5.0"
+ nopt "~4.0.1"
+
js-beautify@^1.8.8:
version "1.8.8"
resolved "https://registry.yarnpkg.com/js-beautify/-/js-beautify-1.8.8.tgz#1eb175b73a3571a5f1ed8d98e7cf2b05bfa98471"
@@ -6427,7 +6536,7 @@ lodash.upperfirst@4.3.1:
resolved "https://registry.yarnpkg.com/lodash.upperfirst/-/lodash.upperfirst-4.3.1.tgz#1365edf431480481ef0d1c68957a5ed99d49f7ce"
integrity sha1-E2Xt9DFIBIHvDRxolXpe2Z1J984=
-lodash@^4.13.1, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.3.0, lodash@^4.5.0:
+lodash@4.x, lodash@^4.13.1, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.3.0, lodash@^4.5.0:
version "4.17.11"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d"
integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==
@@ -6912,6 +7021,14 @@ nice-try@^1.0.4:
resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.4.tgz#d93962f6c52f2c1558c0fbda6d512819f1efe1c4"
integrity sha512-2NpiFHqC87y/zFke0fC0spBXL3bBsoh/p5H1EFhshxjCR5+0g2d6BiXbUFz9v1sAcxsk2htp2eQnNIci2dIYcA==
+node-cache@^4.1.1:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/node-cache/-/node-cache-4.2.0.tgz#48ac796a874e762582692004a376d26dfa875811"
+ integrity sha512-obRu6/f7S024ysheAjoYFEEBqqDWv4LOMNJEuO8vMeEw2AT4z+NCzO4hlc2lhI4vATzbCQv6kke9FVdx0RbCOw==
+ dependencies:
+ clone "2.x"
+ lodash "4.x"
+
node-fetch@1.6.3:
version "1.6.3"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.6.3.tgz#dc234edd6489982d58e8f0db4f695029abcd8c04"
@@ -7106,7 +7223,7 @@ oauth-sign@~0.9.0:
resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455"
integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==
-object-assign@^4.0.1, object-assign@^4.1.0:
+object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=
@@ -8058,6 +8175,15 @@ read-pkg@^3.0.0:
string_decoder "~1.1.1"
util-deprecate "~1.0.1"
+readable-stream@^3.0.6:
+ version "3.0.6"
+ resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.0.6.tgz#351302e4c68b5abd6a2ed55376a7f9a25be3057a"
+ integrity sha512-9E1oLoOWfhSXHGv6QlwXJim7uNzd9EVlWK+21tCU9Ju/kR0/p2AZYPz4qSchgO8PlLIH4FpZYfzwS+rEksZjIg==
+ dependencies:
+ inherits "^2.0.3"
+ string_decoder "^1.1.1"
+ util-deprecate "^1.0.1"
+
readable-stream@~2.0.6:
version "2.0.6"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.0.6.tgz#8f90341e68a53ccc928788dacfcd11b36eb9b78e"
@@ -8131,10 +8257,10 @@ regex-not@^1.0.0, regex-not@^1.0.2:
extend-shallow "^3.0.2"
safe-regex "^1.1.0"
-regexpp@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-2.0.0.tgz#b2a7534a85ca1b033bcf5ce9ff8e56d4e0755365"
- integrity sha512-g2FAVtR8Uh8GO1Nv5wpxW7VFVwHcCEr4wyA8/MHiRkO8uHoR5ntAA8Uq3P1vvMTX/BeQiRVSpDGLd+Wn5HNOTA==
+regexpp@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-2.0.1.tgz#8d19d31cf632482b589049f8281f93dbcba4d07f"
+ integrity sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==
regexpu-core@^1.0.0:
version "1.0.0"
@@ -8666,11 +8792,13 @@ slash@^1.0.0:
resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55"
integrity sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=
-slice-ansi@1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-1.0.0.tgz#044f1a49d8842ff307aad6b505ed178bd950134d"
- integrity sha512-POqxBK6Lb3q6s047D/XsDVNPnF9Dl8JSaqe9h9lURl0OdNqy/ujDrOiIHtsqXMGbWWTIomRzAMaTyawAU//Reg==
+slice-ansi@2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-2.0.0.tgz#5373bdb8559b45676e8541c66916cdd6251612e7"
+ integrity sha512-4j2WTWjp3GsZ+AOagyzVbzp4vWGtZ0hEZ/gDY/uTvm6MTxUfTUIsnMIFb1bn8o0RuXiqUw15H1bue8f22Vw2oQ==
dependencies:
+ ansi-styles "^3.2.0"
+ astral-regex "^1.0.0"
is-fullwidth-code-point "^2.0.0"
slugify@^1.3.1:
@@ -8811,6 +8939,17 @@ source-map-resolve@^0.5.0:
source-map-url "^0.4.0"
urix "^0.1.0"
+source-map-resolve@^0.5.2:
+ version "0.5.2"
+ resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.2.tgz#72e2cc34095543e43b2c62b2c4c10d4a9054f259"
+ integrity sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==
+ dependencies:
+ atob "^2.1.1"
+ decode-uri-component "^0.2.0"
+ resolve-url "^0.2.1"
+ source-map-url "^0.4.0"
+ urix "^0.1.0"
+
source-map-support@^0.4.15:
version "0.4.18"
resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.18.tgz#0286a6de8be42641338594e97ccea75f0a2c585f"
@@ -9075,6 +9214,13 @@ string_decoder@^1.0.0, string_decoder@~1.1.1:
dependencies:
safe-buffer "~5.1.0"
+string_decoder@^1.1.1:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.2.0.tgz#fe86e738b19544afe70469243b2a1ee9240eae8d"
+ integrity sha512-6YqyX6ZWEYguAxgZzHGL7SsCeGx3V2TtOTqZz1xSTSWnqsbWwbptafNyvf/ACquZUXV3DANr5BDIwNYe1mN42w==
+ dependencies:
+ safe-buffer "~5.1.0"
+
string_decoder@~0.10.x:
version "0.10.31"
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94"
@@ -9118,7 +9264,7 @@ strip-eof@^1.0.0:
resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf"
integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=
-strip-json-comments@^2.0.1, strip-json-comments@~2.0.1:
+strip-json-comments@^2.0.0, strip-json-comments@^2.0.1, strip-json-comments@~2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo=
@@ -9165,16 +9311,14 @@ symbol-tree@^3.2.2:
resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.2.tgz#ae27db38f660a7ae2e1c3b7d1bc290819b8519e6"
integrity sha1-rifbOPZgp64uHDt9G8KQgZuFGeY=
-table@^4.0.3:
- version "4.0.3"
- resolved "http://registry.npmjs.org/table/-/table-4.0.3.tgz#00b5e2b602f1794b9acaf9ca908a76386a7813bc"
- integrity sha512-S7rnFITmBH1EnyKcvxBh1LjYeQMmnZtCXSEbHcH6S0NoKit24ZuFO/T1vDcLdYsLQkM188PVVhQmzKIuThNkKg==
+table@^5.0.2:
+ version "5.1.1"
+ resolved "https://registry.yarnpkg.com/table/-/table-5.1.1.tgz#92030192f1b7b51b6eeab23ed416862e47b70837"
+ integrity sha512-NUjapYb/qd4PeFW03HnAuOJ7OMcBkJlqeClWxeNlQ0lXGSb52oZXGzkO0/I0ARegQ2eUT1g2VDJH0eUxDRcHmw==
dependencies:
- ajv "^6.0.1"
- ajv-keywords "^3.0.0"
- chalk "^2.1.0"
- lodash "^4.17.4"
- slice-ansi "1.0.0"
+ ajv "^6.6.1"
+ lodash "^4.17.11"
+ slice-ansi "2.0.0"
string-width "^2.1.1"
tapable@^0.1.8:
@@ -9407,6 +9551,16 @@ tryer@^1.0.0:
resolved "https://registry.yarnpkg.com/tryer/-/tryer-1.0.0.tgz#027b69fa823225e551cace3ef03b11f6ab37c1d7"
integrity sha1-Antp+oIyJeVRys4+8DsR9qs3wdc=
+tsconfig@^7.0.0:
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/tsconfig/-/tsconfig-7.0.0.tgz#84538875a4dc216e5c4a5432b3a4dec3d54e91b7"
+ integrity sha512-vZXmzPrL+EmC4T/4rVlT2jNVMWCi/O4DIiSj3UHg1OE5kCKbk4mfrXc6dZksLgRM/TZlKnousKH9bbTazUWRRw==
+ dependencies:
+ "@types/strip-bom" "^3.0.0"
+ "@types/strip-json-comments" "0.0.30"
+ strip-bom "^3.0.0"
+ strip-json-comments "^2.0.0"
+
tslib@^1.9.0:
version "1.9.3"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286"
@@ -9676,7 +9830,7 @@ useragent@2.2.1:
lru-cache "2.2.x"
tmp "0.0.x"
-util-deprecate@~1.0.1:
+util-deprecate@^1.0.1, util-deprecate@~1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
@@ -9758,17 +9912,17 @@ vue-apollo@^3.0.0-beta.25:
chalk "^2.4.1"
throttle-debounce "^2.0.0"
-vue-eslint-parser@^3.2.1:
- version "3.2.2"
- resolved "https://registry.yarnpkg.com/vue-eslint-parser/-/vue-eslint-parser-3.2.2.tgz#47c971ee4c39b0ee7d7f5e154cb621beb22f7a34"
- integrity sha512-dprI6ggKCTwV22r+i8dtUGquiOCn063xyDmb7BV/BjG5Oc/m5EoMNrWevpvTcrlGuFZmYVPs5fgsu8UIxmMKzg==
+vue-eslint-parser@^4.0.2:
+ version "4.0.3"
+ resolved "https://registry.yarnpkg.com/vue-eslint-parser/-/vue-eslint-parser-4.0.3.tgz#80cf162e484387b2640371ad21ba1f86e0c10a61"
+ integrity sha512-AUeQsYdO6+7QXCems+WvGlrXd37PHv/zcRQSQdY1xdOMwdFAPEnMBsv7zPvk0TPGulXkK/5p/ITgrjiYB7k3ag==
dependencies:
- debug "^3.1.0"
+ debug "^4.1.0"
eslint-scope "^4.0.0"
eslint-visitor-keys "^1.0.0"
- espree "^4.0.0"
+ espree "^4.1.0"
esquery "^1.0.1"
- lodash "^4.17.10"
+ lodash "^4.17.11"
vue-functional-data-merge@^2.0.5:
version "2.0.6"
@@ -9780,6 +9934,22 @@ vue-hot-reload-api@^2.3.0:
resolved "https://registry.yarnpkg.com/vue-hot-reload-api/-/vue-hot-reload-api-2.3.0.tgz#97976142405d13d8efae154749e88c4e358cf926"
integrity sha512-2j/t+wIbyVMP5NvctQoSUvLkYKoWAAk2QlQiilrM2a6/ulzFgdcLUJfTvs4XQ/3eZhHiBmmEojbjmM4AzZj8JA==
+vue-jest@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/vue-jest/-/vue-jest-3.0.1.tgz#127cded1a57cdfcf01fa8a10ce29579e2cb3a04d"
+ integrity sha512-otS+n341cTsp0pF7tuTu2x43b23x/+K0LZdAXV+ewKYIMZRqhuQaJTECWEt/cN/YZw2JC6hUM6xybdnOB4ZQ+g==
+ dependencies:
+ babel-plugin-transform-es2015-modules-commonjs "^6.26.0"
+ chalk "^2.1.0"
+ extract-from-css "^0.4.4"
+ find-babel-config "^1.1.0"
+ js-beautify "^1.6.14"
+ node-cache "^4.1.1"
+ object-assign "^4.1.1"
+ source-map "^0.5.6"
+ tsconfig "^7.0.0"
+ vue-template-es2015-compiler "^1.6.0"
+
vue-loader@^15.4.2:
version "15.4.2"
resolved "https://registry.yarnpkg.com/vue-loader/-/vue-loader-15.4.2.tgz#812bb26e447dd3b84c485eb634190d914ce125e2"
@@ -10296,3 +10466,8 @@ zen-observable@^0.8.0:
version "0.8.11"
resolved "https://registry.yarnpkg.com/zen-observable/-/zen-observable-0.8.11.tgz#d3415885eeeb42ee5abb9821c95bb518fcd6d199"
integrity sha512-N3xXQVr4L61rZvGMpWe8XoCGX8vhU35dPyQ4fm5CY/KDlG0F75un14hjbckPXTDuKUY6V0dqR2giT6xN8Y4GEQ==
+
+zrender@4.0.5:
+ version "4.0.5"
+ resolved "https://registry.yarnpkg.com/zrender/-/zrender-4.0.5.tgz#6e8f738971ce2cd624aac82b2156729b1c0e5a82"
+ integrity sha512-SintgipGEJPT9Sz2ABRoE4ZD7Yzy7oR7j7KP6H+C9FlbHWnLUfGVK7E8UV27pGwlxAMB0EsnrqhXx5XjAfv/KA==